feat: publication IA (magazine/brief/essay) + fixes critique
Some checks failed
CI / Lint, Unit Tests & Build (push) Failing after 1m22s
CI / Deploy production (on server) (push) Has been skipped

Publication IA:
- 4 templates (magazine, brief, essay, simple) avec CSS riche
- Rewrite IA (article/exercises/tutorial/reference/mixed)
- Modération avec timeout 12s + fallback safe
- Quotas publish_enhance par tier (basic=2, pro=15, business=100)
- Détection contenu stale (hash)
- Migration DB publishedContent/publishedTemplate/publishedSourceHash

Fixes:
- cheerio v1.2: Element -> AnyNode (domhandler), decodeEntities cast
- _isShared ajouté au type Note (champ virtuel serveur)
- callout colors PDF export: extraction fonction pure testable
- admin/published: guard note.userId null
- Cmd+S fonctionne en mode dialog (pas seulement fullPage)

i18n:
- 23 clés publish* traduites dans les 15 locales
- Extension Web Clipper: 13 locales mise à jour

Tests:
- callout-colors.test.ts (6 tests)
- note-visible-in-view.test.ts (5 tests)
- entitlements.test.ts + byok-entitlements.test.ts: mock usageLog + unstubAllEnvs
- 199/199 tests passent

Tracker: user-stories.md sync avec sprint-status.yaml
This commit is contained in:
Antigravity
2026-06-28 07:32:57 +00:00
parent 902fe95a69
commit 96e7902f01
169 changed files with 5382 additions and 1527 deletions

View File

@@ -0,0 +1,286 @@
/** CSS KaTeX commun aux pages publiées */
export const KATEX_PUBLISH_CSS = `
.r-math-display {
margin: 1.75em 0;
text-align: center;
overflow-x: auto;
overflow-y: hidden;
padding: 0.25em 0;
}
.r-math-inline { display: inline; }
.r-math-inline .katex { font-size: 1.05em; }
.r-math-fallback {
font-family: 'SF Mono', Menlo, monospace;
font-size: 0.9em;
opacity: 0.85;
}
.pub-rewrite-body .katex-display { margin: 0; }
.katex { font-size: 1.1em; }
.katex-display { margin: 0; overflow-x: auto; overflow-y: hidden; }
`
export const REWRITE_SHARED_CSS = `
${KATEX_PUBLISH_CSS}
/* ─── Résumé introductif ─────────────────────────────── */
.pub-rewrite-summary {
font-size: 1.2em;
line-height: 1.7;
font-style: italic;
color: var(--pub-summary-color, #444);
margin-bottom: 2em;
padding-bottom: 1.5em;
border-bottom: 2px solid var(--pub-accent, #A47148);
}
/* ─── Exercices ──────────────────────────────────────── */
.pub-rewrite-body .pub-exercise {
border: 2px solid var(--pub-exercise-border, #e0d4c3);
border-radius: 12px;
margin: 1.75em 0;
overflow: hidden;
}
.pub-rewrite-body .pub-exercise-header {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 18px;
background: var(--pub-exercise-header-bg, #f6f1ea);
border-bottom: 1px solid var(--pub-exercise-border, #e0d4c3);
}
.pub-rewrite-body .pub-exercise-num {
font-weight: 700;
font-size: 0.82em;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--pub-accent, #A47148);
}
.pub-rewrite-body .pub-exercise-meta {
margin-left: auto;
font-size: 0.82em;
font-weight: 600;
color: #666;
}
.pub-rewrite-body .pub-exercise-body {
padding: 16px 20px;
color: #1a1a1a;
background: #fff;
}
.pub-rewrite-body .pub-exercise-body p:first-child { margin-top: 0; }
.pub-rewrite-body .pub-exercise-body p:last-child { margin-bottom: 0; }
/* Corps réécrit : texte toujours sombre sur fond clair */
.pub-rewrite-body {
color: #1a1a1a;
}
.pub-rewrite-body p,
.pub-rewrite-body li,
.pub-rewrite-body td,
.pub-rewrite-body th {
color: inherit;
}
/* Solution collapsible */
.pub-rewrite-body .pub-solution {
border-top: 1px dashed var(--pub-exercise-border, #e0d4c3);
}
.pub-rewrite-body .pub-solution > summary {
list-style: none;
cursor: pointer;
padding: 10px 20px;
font-size: 0.85em;
font-weight: 600;
color: var(--pub-accent, #A47148);
user-select: none;
display: flex;
align-items: center;
gap: 6px;
}
.pub-rewrite-body .pub-solution > summary::before { content: '▶'; font-size: 10px; transition: transform 0.2s; }
.pub-rewrite-body .pub-solution[open] > summary::before { transform: rotate(90deg); }
.pub-rewrite-body .pub-solution-body {
padding: 14px 20px 18px;
background: var(--pub-solution-bg, rgba(164,113,72,0.04));
border-top: 1px solid var(--pub-exercise-border, #e0d4c3);
color: #1a1a1a;
}
/* ─── Étapes tutoriel ────────────────────────────────── */
.pub-rewrite-body .pub-step {
display: grid;
grid-template-columns: 36px 1fr;
gap: 14px;
align-items: start;
margin: 1.5em 0;
}
.pub-rewrite-body .pub-step-num {
width: 36px;
height: 36px;
border-radius: 50%;
background: var(--pub-accent, #A47148);
color: #fff;
font-weight: 700;
font-size: 14px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
margin-top: 2px;
}
.pub-rewrite-body .pub-step-body p:first-child { margin-top: 0; }
/* ─── Définitions / Concepts ─────────────────────────── */
.pub-rewrite-body .pub-definition {
margin: 1.5em 0;
border-radius: 10px;
overflow: hidden;
border: 1px solid var(--pub-definition-border, #ddd);
}
.pub-rewrite-body .pub-definition-term {
padding: 8px 16px;
background: var(--pub-accent, #A47148);
color: #fff;
font-weight: 700;
font-size: 0.88em;
letter-spacing: 0.04em;
}
.pub-rewrite-body .pub-definition-body {
padding: 12px 16px;
background: var(--pub-definition-bg, #fafaf8);
color: #1a1a1a;
}
.pub-rewrite-body .pub-definition-body p { margin: 0.4em 0; color: #1a1a1a; }
.pub-rewrite-body .pub-definition-body p:first-child { margin-top: 0; }
/* ─── Toggle / Collapsible ───────────────────────────── */
.pub-rewrite-body .pub-toggle {
border: 1px solid var(--pub-toggle-border, #e5e5e5);
border-radius: 10px;
margin: 1.2em 0;
overflow: hidden;
}
.pub-rewrite-body .pub-toggle > .pub-toggle-trigger {
list-style: none;
display: flex;
align-items: center;
gap: 8px;
padding: 12px 16px;
cursor: pointer;
font-weight: 600;
font-size: 0.92em;
background: var(--pub-toggle-header-bg, #f7f7f5);
user-select: none;
}
.pub-rewrite-body .pub-toggle > .pub-toggle-trigger::before {
content: '▶';
font-size: 10px;
color: var(--pub-accent, #A47148);
transition: transform 0.2s;
flex-shrink: 0;
}
.pub-rewrite-body .pub-toggle[open] > .pub-toggle-trigger::before { transform: rotate(90deg); }
.pub-rewrite-body .pub-toggle-body {
padding: 14px 18px;
border-top: 1px solid var(--pub-toggle-border, #e5e5e5);
color: #1a1a1a;
background: #fff;
}
.pub-rewrite-body .pub-toggle-body p:first-child { margin-top: 0; }
/* ─── Mise en avant ──────────────────────────────────── */
.pub-rewrite-body .pub-highlight {
margin: 1.5em 0;
padding: 16px 20px;
background: var(--pub-highlight-bg, #fffbf5);
border-left: 4px solid var(--pub-accent, #A47148);
border-radius: 0 10px 10px 0;
font-weight: 600;
color: #1a1a1a;
}
.pub-rewrite-body .pub-highlight p { margin: 0; color: #1a1a1a; }
/* ─── Callouts ───────────────────────────────────────── */
.pub-rewrite-body .pub-callout {
margin: 1.4em 0;
padding: 14px 18px 14px 52px;
border-radius: 10px;
position: relative;
font-size: 0.93em;
line-height: 1.65;
}
.pub-rewrite-body .pub-callout::before {
position: absolute;
left: 16px;
top: 14px;
font-size: 18px;
}
.pub-rewrite-body .pub-callout strong {
display: block;
font-weight: 700;
margin-bottom: 4px;
font-size: 0.9em;
letter-spacing: 0.04em;
text-transform: uppercase;
color: inherit;
}
.pub-rewrite-body .pub-callout p { margin: 0.3em 0; color: inherit; }
.pub-rewrite-body .pub-callout p:first-of-type { margin-top: 0; }
.pub-rewrite-body .pub-callout-info {
background: #eff6ff;
border: 1px solid #93c5fd;
color: #1e3a8a;
}
.pub-rewrite-body .pub-callout-info::before { content: ''; }
.pub-rewrite-body .pub-callout-tip {
background: #ecfdf5;
border: 1px solid #6ee7b7;
color: #065f46;
}
.pub-rewrite-body .pub-callout-tip::before { content: '💡'; }
.pub-rewrite-body .pub-callout-warning {
background: #fffbeb;
border: 1px solid #fbbf24;
color: #78350f;
}
.pub-rewrite-body .pub-callout-warning::before { content: '⚠️'; }
/* ─── Checklist ──────────────────────────────────────── */
.pub-rewrite-body .pub-checklist {
list-style: none;
padding: 0;
margin: 1em 0;
}
.pub-rewrite-body .pub-checklist li {
padding: 8px 12px 8px 36px;
position: relative;
border-radius: 6px;
margin: 4px 0;
background: var(--pub-checklist-bg, rgba(0,0,0,0.02));
color: #1a1a1a;
}
.pub-rewrite-body .pub-checklist li::before {
content: '✓';
position: absolute;
left: 10px;
top: 8px;
font-weight: 700;
color: var(--pub-accent, #A47148);
}
/* ─── Code ───────────────────────────────────────────── */
.pub-rewrite-body .pub-code {
margin: 1.5em 0;
border-radius: 10px;
overflow: hidden;
}
.pub-rewrite-body .pub-code code {
display: block;
padding: 20px 22px;
font-family: 'SF Mono', 'Fira Code', Menlo, Consolas, monospace;
font-size: 13.5px;
line-height: 1.65;
overflow-x: auto;
}
`