feat(design-system): reskin Charcoal — tokens dark-default + sidebar navy permanent

- Remplacement intégral index.css par palette Charcoal (DESIGN_SYSTEM.md v2.0)
- Dark = thème par défaut, .light = override via @custom-variant light
- Sidebar navy #0C1528 permanent (identique dark+light)
- Script anti-FOUC inline dans index.html
- Layout : radial-gradient sur <main>, sidebar 230px, max-w-[1100px]
- Renommage tokens Boréal→Charcoal sur ~45 composants
- Inversion dark: → baseline + light: sur primitives shadcn
- Fix logo blanc forcé dans sidebar
- ADR 006 mis à jour

Typecheck: OK · Tests: 122/122 

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Hermann_Kitio 2026-04-24 23:07:38 +03:00
parent 407d1bd134
commit b68f160bce
61 changed files with 1269 additions and 726 deletions

View file

@ -50,8 +50,8 @@ export function IdeesSuggestions({ idees, isLoading, error, isOpen, onClose }: P
>
<DialogContent>
<DialogHeader>
<DialogTitle className="flex items-center gap-2 text-ink-1">
<Lightbulb className="size-5 text-expria" aria-hidden="true" />
<DialogTitle className="flex items-center gap-2 text-ink-primary">
<Lightbulb className="size-5 text-brand-text" aria-hidden="true" />
Suggestions d'idées
</DialogTitle>
<DialogDescription>
@ -60,8 +60,8 @@ export function IdeesSuggestions({ idees, isLoading, error, isOpen, onClose }: P
</DialogHeader>
{isLoading && (
<div className="flex items-center gap-2 text-sm text-ink-3" aria-busy="true">
<Loader2 className="size-4 animate-spin text-expria" aria-hidden="true" />
<div className="flex items-center gap-2 text-sm text-ink-secondary" aria-busy="true">
<Loader2 className="size-4 animate-spin text-brand-text" aria-hidden="true" />
Génération des idées
</div>
)}
@ -69,18 +69,18 @@ export function IdeesSuggestions({ idees, isLoading, error, isOpen, onClose }: P
{!isLoading && message && (
<div
role="alert"
className="rounded-md border border-danger/40 bg-danger-bg px-3 py-2 text-sm text-danger"
className="rounded-md border border-danger/40 bg-danger-soft px-3 py-2 text-sm text-danger"
>
{message}
</div>
)}
{!isLoading && !message && idees && idees.length > 0 && (
<ul className="space-y-2 text-sm text-ink-2">
<ul className="space-y-2 text-sm text-ink-primary">
{idees.map((idee, i) => (
<li key={i} className="flex gap-2">
<span
className="mt-[0.4em] size-1.5 shrink-0 rounded-full bg-expria"
className="mt-[0.4em] size-1.5 shrink-0 rounded-full bg-brand"
aria-hidden="true"
/>
<span>{idee}</span>

View file

@ -30,11 +30,11 @@ export function NclcCibleSelector({ value, onChange, disabled = false }: Props)
aria-label="Niveau NCLC cible pour la correction"
disabled={disabled}
>
<legend className="text-sm font-medium text-ink-2">Objectif de correction</legend>
<legend className="text-sm font-medium text-ink-primary">Objectif de correction</legend>
<div
role="radiogroup"
aria-label="Niveau NCLC cible"
className="inline-flex overflow-hidden rounded-md border border-line bg-surface"
className="inline-flex overflow-hidden rounded-md border border-border bg-surface"
>
{OPTIONS.map((opt) => {
const active = opt.value === value
@ -51,8 +51,8 @@ export function NclcCibleSelector({ value, onChange, disabled = false }: Props)
'focus-visible:outline-none focus-visible:shadow-focus',
'disabled:cursor-not-allowed disabled:opacity-50',
active
? 'bg-expria text-white'
: 'bg-surface text-ink-2 hover:bg-canvas-2 hover:text-ink-1',
? 'bg-brand text-white'
: 'bg-surface text-ink-primary hover:bg-surface-hover hover:text-ink-primary',
)}
title={opt.hint}
>
@ -61,7 +61,7 @@ export function NclcCibleSelector({ value, onChange, disabled = false }: Props)
)
})}
</div>
<p className="text-xs text-ink-4">{OPTIONS.find((o) => o.value === value)?.hint}</p>
<p className="text-xs text-ink-secondary">{OPTIONS.find((o) => o.value === value)?.hint}</p>
</fieldset>
)
}

View file

@ -35,7 +35,7 @@ const MIN_WORDS_IDEES = 30
const LS_SIMULATION_ID_KEY = 'expria_simulation_id'
const secondaryActionBtn =
'inline-flex items-center gap-1.5 rounded-md border border-line bg-surface px-3 py-1.5 text-sm text-ink-2 transition-colors hover:border-expria hover:text-expria focus:border-expria focus:outline-none focus:shadow-focus disabled:cursor-not-allowed disabled:opacity-50'
'inline-flex items-center gap-1.5 rounded-md border border-border bg-surface px-3 py-1.5 text-sm text-ink-primary transition-colors hover:border-brand hover:text-brand-text focus:border-brand focus:outline-none focus:shadow-focus disabled:cursor-not-allowed disabled:opacity-50'
const textSchema = z.object({
texte: z
@ -199,11 +199,11 @@ export function SimulationForm({
type="button"
onClick={onBack}
disabled={isSubmitting}
className="text-sm text-ink-4 underline-offset-4 hover:text-ink-2 hover:underline disabled:pointer-events-none"
className="text-sm text-ink-secondary underline-offset-4 hover:text-ink-primary hover:underline disabled:pointer-events-none"
>
Retour
</button>
<h2 className="flex-1 text-lg font-semibold text-ink-1">{formatTache(tache)}</h2>
<h2 className="flex-1 text-lg font-semibold text-ink-primary">{formatTache(tache)}</h2>
</div>
<SujetDisplay sujet={sujet} />
@ -245,7 +245,7 @@ export function SimulationForm({
{apiError && (
<div
role="alert"
className="rounded-md border border-danger/40 bg-danger-bg px-3 py-2 text-sm text-danger"
className="rounded-md border border-danger/40 bg-danger-soft px-3 py-2 text-sm text-danger"
>
{apiError}
</div>
@ -254,7 +254,7 @@ export function SimulationForm({
{expiredBelowMin && (
<div
role="alert"
className="rounded-md border border-warning/40 bg-warning-bg px-3 py-2 text-sm text-warning"
className="rounded-md border border-warning/40 bg-warning-soft px-3 py-2 text-sm text-warning"
>
Temps écoulé. Écrivez au moins {config.motsMin} mots pour soumettre.
</div>
@ -262,26 +262,30 @@ export function SimulationForm({
<form onSubmit={handleSubmit} className="space-y-3" noValidate>
<div className="space-y-1.5">
<label htmlFor="texte" className="text-sm font-medium text-ink-2">
<label htmlFor="texte" className="text-sm font-medium text-ink-primary">
Votre production
</label>
<div className="sticky top-14 z-20 bg-canvas pb-1 lg:top-0">
<div
className={`mb-2 flex items-center gap-2 rounded-md border px-3 py-2 ${
timer.isExpired || timer.secondesRestantes < 120
? 'border-danger bg-danger-bg'
: 'border-line bg-surface'
? 'border-danger bg-danger-soft'
: 'border-border bg-surface'
}`}
>
<Clock
className={`size-4 ${
timer.isExpired || timer.secondesRestantes < 120 ? 'text-danger' : 'text-ink-3'
timer.isExpired || timer.secondesRestantes < 120
? 'text-danger'
: 'text-ink-secondary'
}`}
aria-hidden="true"
/>
<span
className={`text-xs font-medium uppercase tracking-wide ${
timer.isExpired || timer.secondesRestantes < 120 ? 'text-danger' : 'text-ink-3'
timer.isExpired || timer.secondesRestantes < 120
? 'text-danger'
: 'text-ink-secondary'
}`}
>
Temps restant
@ -305,13 +309,13 @@ export function SimulationForm({
placeholder="Rédigez votre texte ici…"
aria-invalid={!!fieldError}
aria-describedby={fieldError ? 'texte-error' : undefined}
className="w-full resize-none overflow-y-hidden rounded-md border border-line bg-surface p-3 text-sm text-ink-1 placeholder:text-ink-5 focus:border-expria focus:outline-none focus:shadow-focus disabled:cursor-not-allowed disabled:opacity-50"
className="w-full resize-none overflow-y-hidden rounded-md border border-border bg-surface p-3 text-sm text-ink-primary placeholder:text-ink-tertiary focus:border-brand focus:outline-none focus:shadow-focus disabled:cursor-not-allowed disabled:opacity-50"
/>
<WordCountBar count={wordCount} config={config} />
<NclcCibleSelector value={nclcCible} onChange={setNclcCible} disabled={isSubmitting} />
{autosave.savedAt && !fieldError && (
<p className="text-xs text-ink-4" aria-live="polite">
<p className="text-xs text-ink-secondary" aria-live="polite">
Sauvegardé à{' '}
{autosave.savedAt.toLocaleTimeString('fr-FR', {
hour: '2-digit',
@ -338,7 +342,7 @@ export function SimulationForm({
</Button>
{isSubmitting && (
<p className="text-center text-xs text-ink-4">
<p className="text-center text-xs text-ink-secondary">
La correction peut prendre jusqu'à 30 secondes.
</p>
)}

View file

@ -55,7 +55,7 @@ export function SpecialCharsKeyboard({ onInsert, disabled = false }: Props) {
<div
role="toolbar"
aria-label="Caractères spéciaux"
className="flex flex-wrap gap-1.5 rounded-md border border-line bg-canvas-2 p-2"
className="flex flex-wrap gap-1.5 rounded-md border border-border bg-surface p-2"
>
{SPECIAL_CHARS.map((char) => (
<button
@ -65,7 +65,7 @@ export function SpecialCharsKeyboard({ onInsert, disabled = false }: Props) {
onMouseDown={(e) => e.preventDefault()}
onClick={() => onInsert(char)}
aria-label={`Insérer le caractère ${char}`}
className="size-8 shrink-0 rounded-md border border-line bg-surface text-sm font-medium text-ink-1 transition-colors hover:border-expria hover:bg-expria-50 hover:text-expria focus:border-expria focus:outline-none focus:shadow-focus disabled:cursor-not-allowed disabled:opacity-50"
className="size-8 shrink-0 rounded-md border border-border bg-surface text-sm font-medium text-ink-primary transition-colors hover:border-brand hover:bg-brand-soft hover:text-brand-text focus:border-brand focus:outline-none focus:shadow-focus disabled:cursor-not-allowed disabled:opacity-50"
>
{char}
</button>

View file

@ -24,7 +24,7 @@ export function SujetCard({ sujet, onSelect }: Props) {
<Badge variant="neutral">{sujet.role}</Badge>
</div>
)}
<p className="line-clamp-3 text-sm leading-relaxed text-ink-1">{sujet.consigne}</p>
<p className="line-clamp-3 text-sm leading-relaxed text-ink-primary">{sujet.consigne}</p>
</div>
</Card>
)

View file

@ -22,9 +22,11 @@ interface Props {
function DocumentBlock({ titre, texte }: { titre: string | null; texte: string | null }) {
if (!titre && !texte) return null
return (
<article className="rounded-md border border-line bg-canvas-2 p-3">
{titre && <h4 className="mb-2 text-sm font-semibold text-ink-1">{titre}</h4>}
{texte && <p className="whitespace-pre-wrap text-sm leading-relaxed text-ink-2">{texte}</p>}
<article className="rounded-md border border-border bg-surface p-3">
{titre && <h4 className="mb-2 text-sm font-semibold text-ink-primary">{titre}</h4>}
{texte && (
<p className="whitespace-pre-wrap text-sm leading-relaxed text-ink-primary">{texte}</p>
)}
</article>
)
}
@ -38,19 +40,21 @@ export function SujetDisplay({ sujet }: Props) {
{sujet.role && (
<div className="flex items-center gap-2">
<Badge variant="neutral">Rôle</Badge>
<span className="text-sm text-ink-2">{sujet.role}</span>
<span className="text-sm text-ink-primary">{sujet.role}</span>
</div>
)}
{sujet.contexte && (
<p className="whitespace-pre-wrap text-sm leading-relaxed text-ink-3">{sujet.contexte}</p>
<p className="whitespace-pre-wrap text-sm leading-relaxed text-ink-secondary">
{sujet.contexte}
</p>
)}
<div>
<h3 className="mb-1 text-xs font-semibold uppercase tracking-wide text-ink-4">
<h3 className="mb-1 text-xs font-semibold uppercase tracking-wide text-ink-secondary">
Consigne
</h3>
<p className="whitespace-pre-wrap text-base leading-relaxed text-ink-1">
<p className="whitespace-pre-wrap text-base leading-relaxed text-ink-primary">
{sujet.consigne}
</p>
</div>

View file

@ -61,14 +61,16 @@ export function TaskSelector({ type, plan, simulationsUsed, isLoading, onSelect
return (
<div className="space-y-4">
<div>
<h2 className="text-lg font-semibold text-ink-1">Choisir une tâche</h2>
<p className="mt-1 text-sm text-ink-3">Sélectionnez la tâche que vous souhaitez simuler.</p>
<h2 className="text-lg font-semibold text-ink-primary">Choisir une tâche</h2>
<p className="mt-1 text-sm text-ink-secondary">
Sélectionnez la tâche que vous souhaitez simuler.
</p>
</div>
{quotaBlocked && (
<div
role="alert"
className="rounded-lg border border-danger/30 bg-danger-bg px-4 py-3 text-sm text-danger"
className="rounded-lg border border-danger/30 bg-danger-soft px-4 py-3 text-sm text-danger"
>
Vous avez utilisé vos 5 simulations gratuites.{' '}
<a href="/pricing" className="underline underline-offset-4">
@ -87,14 +89,14 @@ export function TaskSelector({ type, plan, simulationsUsed, isLoading, onSelect
return (
<Card key={card.key} variant="default" className="flex flex-col p-4 opacity-60">
{card.tache === null && (
<Lock className="mb-2 size-4 text-ink-4" aria-hidden="true" />
<Lock className="mb-2 size-4 text-ink-secondary" aria-hidden="true" />
)}
<span className="text-[11px] font-semibold uppercase tracking-widest text-ink-5">
<span className="text-[11px] font-semibold uppercase tracking-widest text-ink-tertiary">
{card.label}
</span>
<span className="mt-1 text-sm font-semibold text-ink-1">{card.sublabel}</span>
<span className="mt-1 text-sm font-semibold text-ink-primary">{card.sublabel}</span>
{card.lockLabel && (
<span className="mt-1.5 text-xs text-ink-4">{card.lockLabel}</span>
<span className="mt-1.5 text-xs text-ink-secondary">{card.lockLabel}</span>
)}
</Card>
)
@ -114,13 +116,13 @@ export function TaskSelector({ type, plan, simulationsUsed, isLoading, onSelect
<div className="mb-2 flex items-center justify-between">
<Badge variant="neutral">{abbrev}</Badge>
{isLoading && (
<Loader2 className="size-3.5 animate-spin text-expria" aria-hidden="true" />
<Loader2 className="size-3.5 animate-spin text-brand-text" aria-hidden="true" />
)}
</div>
<span className="text-[11px] font-semibold uppercase tracking-widest text-ink-5">
<span className="text-[11px] font-semibold uppercase tracking-widest text-ink-tertiary">
{card.label}
</span>
<span className="mt-1 text-sm font-semibold text-ink-1">{card.sublabel}</span>
<span className="mt-1 text-sm font-semibold text-ink-primary">{card.sublabel}</span>
</Card>
)
})}

View file

@ -29,7 +29,7 @@ export function TimerDisplay({ secondesRestantes, isExpired }: Props) {
? 'text-danger font-bold'
: isCritique
? 'text-danger motion-safe:animate-pulse'
: 'text-ink-2'
: 'text-ink-primary'
return (
<span

View file

@ -47,7 +47,7 @@ export function WordCountBar({ count, config }: Props) {
aria-valuemin={0}
aria-valuemax={config.motsCibleMax}
aria-label={`Progression du nombre de mots : ${count} sur une cible de ${config.motsCibleMin} à ${config.motsCibleMax} mots`}
className="h-1.5 w-full overflow-hidden rounded-full bg-canvas-2"
className="h-1.5 w-full overflow-hidden rounded-full bg-surface"
>
<div
className={`h-full rounded-full transition-[width] duration-150 ease-out ${classes.bar}`}
@ -58,7 +58,7 @@ export function WordCountBar({ count, config }: Props) {
<span className={`font-medium tabular-nums ${classes.text}`}>
{count.toLocaleString('fr-FR')} mot{count > 1 ? 's' : ''}
</span>
<span className="text-ink-4 tabular-nums">
<span className="text-ink-secondary tabular-nums">
cible {config.motsCibleMin}{config.motsCibleMax} mots
</span>
</div>

View file

@ -18,19 +18,23 @@ interface Props {
export function ConseilNclcCallout({ conseil }: Props) {
return (
<section aria-label="Plan d'action NCLC">
<h2 className="mb-3 text-base font-semibold text-ink-1">Plan d'action NCLC</h2>
<h2 className="mb-3 text-base font-semibold text-ink-primary">Plan d'action NCLC</h2>
<Card variant="raised" className="space-y-3 p-4">
<div className="flex flex-wrap items-baseline gap-x-4 gap-y-1">
<p className="text-[11px] font-semibold uppercase tracking-widest text-ink-5">Objectif</p>
<p className="text-sm font-semibold text-ink-1">{conseil.nclc_cible}</p>
<p className="text-[11px] font-semibold uppercase tracking-widest text-ink-5">Écart</p>
<p className="text-sm text-ink-2">{conseil.ecart}</p>
<p className="text-[11px] font-semibold uppercase tracking-widest text-ink-tertiary">
Objectif
</p>
<p className="text-sm font-semibold text-ink-primary">{conseil.nclc_cible}</p>
<p className="text-[11px] font-semibold uppercase tracking-widest text-ink-tertiary">
Écart
</p>
<p className="text-sm text-ink-primary">{conseil.ecart}</p>
</div>
<div className="space-y-1.5 rounded-md border border-expria/30 bg-expria-50 p-3">
<p className="text-[11px] font-semibold uppercase tracking-widest text-expria">
<div className="space-y-1.5 rounded-md border border-brand/30 bg-brand-soft p-3">
<p className="text-[11px] font-semibold uppercase tracking-widest text-brand-text">
Action prioritaire
</p>
<div className="text-sm leading-relaxed text-ink-1">
<div className="text-sm leading-relaxed text-ink-primary">
<ReactMarkdown disallowedElements={['script', 'iframe']}>
{conseil.action_prioritaire}
</ReactMarkdown>

View file

@ -25,14 +25,14 @@ export function CritereCard({ critere, erreursCodes }: Props) {
return (
<Card variant="default" className="space-y-3 p-4">
<div className="flex items-start justify-between gap-3">
<h3 className="text-sm font-semibold text-ink-1">{critere.nom}</h3>
<h3 className="text-sm font-semibold text-ink-primary">{critere.nom}</h3>
<Badge variant="nclc" className="shrink-0 tabular-nums">
{critere.score}/5
</Badge>
</div>
{critere.commentaire && (
<div className="text-sm leading-relaxed text-ink-2">
<div className="text-sm leading-relaxed text-ink-primary">
<ReactMarkdown disallowedElements={['script', 'iframe']}>
{critere.commentaire}
</ReactMarkdown>
@ -40,26 +40,26 @@ export function CritereCard({ critere, erreursCodes }: Props) {
)}
{critere.exemple && (
<div className="space-y-1.5 rounded-md border border-line bg-canvas-2 p-3">
<p className="text-[11px] font-semibold uppercase tracking-widest text-ink-5">
<div className="space-y-1.5 rounded-md border border-border bg-surface p-3">
<p className="text-[11px] font-semibold uppercase tracking-widest text-ink-tertiary">
Exemple tiré de votre texte
</p>
<p className="italic text-sm leading-relaxed text-ink-2">« {critere.exemple} »</p>
<p className="italic text-sm leading-relaxed text-ink-primary">« {critere.exemple} »</p>
</div>
)}
{critere.suggestion && (
<div className="space-y-1.5 rounded-md border border-expria/30 bg-expria-50 p-3">
<p className="text-[11px] font-semibold uppercase tracking-widest text-expria">
<div className="space-y-1.5 rounded-md border border-brand/30 bg-brand-soft p-3">
<p className="text-[11px] font-semibold uppercase tracking-widest text-brand-text">
Reformulation suggérée
</p>
<p className="text-sm leading-relaxed text-ink-1">{critere.suggestion}</p>
<p className="text-sm leading-relaxed text-ink-primary">{critere.suggestion}</p>
</div>
)}
{critere.astuce && (
<div className="flex gap-2 text-sm text-ink-3">
<span className="shrink-0 text-expria" aria-hidden="true">
<div className="flex gap-2 text-sm text-ink-secondary">
<span className="shrink-0 text-brand-text" aria-hidden="true">
💡
</span>
<span>{critere.astuce}</span>
@ -67,7 +67,7 @@ export function CritereCard({ critere, erreursCodes }: Props) {
)}
{erreursCodes.length > 0 && (
<div className="flex flex-wrap gap-1.5 border-t border-line pt-3">
<div className="flex flex-wrap gap-1.5 border-t border-border pt-3">
{erreursCodes.map((e) => (
<Badge key={`${e.code}-${e.description ?? ''}`} variant="neutral">
{e.description ?? e.code.replace(/_/g, ' ')}

View file

@ -17,9 +17,11 @@ interface Props {
export function DiagnosticCallout({ diagnostic }: Props) {
return (
<section aria-label="Frein principal">
<h2 className="mb-3 text-base font-semibold text-ink-1">Ce qui freine votre progression</h2>
<h2 className="mb-3 text-base font-semibold text-ink-primary">
Ce qui freine votre progression
</h2>
<Card variant="default" className="border-l-4 border-l-expria p-4">
<div className="text-sm leading-relaxed text-ink-1">
<div className="text-sm leading-relaxed text-ink-primary">
<ReactMarkdown disallowedElements={['script', 'iframe']}>{diagnostic}</ReactMarkdown>
</div>
</Card>

View file

@ -39,7 +39,7 @@ export function ExerciceInteractive({ exercice }: Props) {
<div className="flex flex-wrap items-center gap-2">
<Badge variant="nclc">{DIFFICULTE_LABEL[exercice.difficulte]}</Badge>
{exercice.theme && (
<span className="text-xs font-medium text-ink-4">
<span className="text-xs font-medium text-ink-secondary">
{exercice.theme.replace(/_/g, ' ')}
</span>
)}
@ -47,33 +47,35 @@ export function ExerciceInteractive({ exercice }: Props) {
</div>
{exercice.diagnostic && (
<p className="text-sm leading-relaxed text-ink-3">{exercice.diagnostic}</p>
<p className="text-sm leading-relaxed text-ink-secondary">{exercice.diagnostic}</p>
)}
{exercice.consigne && (
<div className="space-y-1.5 rounded-md border border-line bg-canvas-2 p-3">
<p className="text-[11px] font-semibold uppercase tracking-widest text-ink-5">Consigne</p>
<p className="text-sm leading-relaxed text-ink-1">{exercice.consigne}</p>
<div className="space-y-1.5 rounded-md border border-border bg-surface p-3">
<p className="text-[11px] font-semibold uppercase tracking-widest text-ink-tertiary">
Consigne
</p>
<p className="text-sm leading-relaxed text-ink-primary">{exercice.consigne}</p>
</div>
)}
{exercice.extrait && (
<div className="space-y-1.5 rounded-md border border-line bg-surface p-3">
<p className="text-[11px] font-semibold uppercase tracking-widest text-ink-5">
<div className="space-y-1.5 rounded-md border border-border bg-surface p-3">
<p className="text-[11px] font-semibold uppercase tracking-widest text-ink-tertiary">
Extrait à retravailler
</p>
<p className="italic text-sm leading-relaxed text-ink-2">« {exercice.extrait} »</p>
<p className="italic text-sm leading-relaxed text-ink-primary">« {exercice.extrait} »</p>
</div>
)}
<label className="block space-y-1.5">
<span className="text-sm font-medium text-ink-2">Votre réponse</span>
<span className="text-sm font-medium text-ink-primary">Votre réponse</span>
<textarea
rows={3}
value={tentative}
onChange={(e) => setTentative(e.target.value)}
placeholder="Écrivez votre tentative ici…"
className="w-full resize-none rounded-md border border-line bg-surface p-2 text-sm text-ink-1 placeholder:text-ink-5 focus:border-expria focus:outline-none focus:shadow-focus"
className="w-full resize-none rounded-md border border-border bg-surface p-2 text-sm text-ink-primary placeholder:text-ink-tertiary focus:border-brand focus:outline-none focus:shadow-focus"
/>
</label>
@ -101,35 +103,35 @@ export function ExerciceInteractive({ exercice }: Props) {
{indiceRevealed && exercice.indice && (
<div
className="space-y-1 rounded-md border border-warning/30 bg-warning-bg p-3"
className="space-y-1 rounded-md border border-warning/30 bg-warning-soft p-3"
aria-live="polite"
>
<p className="text-[11px] font-semibold uppercase tracking-widest text-warning">Indice</p>
<p className="text-sm leading-relaxed text-ink-1">{exercice.indice}</p>
<p className="text-sm leading-relaxed text-ink-primary">{exercice.indice}</p>
</div>
)}
{correctionRevealed && (
<div className="space-y-3" aria-live="polite">
<div className="space-y-1 rounded-md border border-success/30 bg-success-bg p-3">
<div className="space-y-1 rounded-md border border-success/30 bg-success-soft p-3">
<p className="text-[11px] font-semibold uppercase tracking-widest text-success">
Correction attendue
</p>
<p className="text-sm leading-relaxed text-ink-1">{exercice.correction}</p>
<p className="text-sm leading-relaxed text-ink-primary">{exercice.correction}</p>
</div>
{exercice.explication && (
<div className="space-y-1 rounded-md border border-line bg-canvas-2 p-3">
<p className="text-[11px] font-semibold uppercase tracking-widest text-ink-5">
<div className="space-y-1 rounded-md border border-border bg-surface p-3">
<p className="text-[11px] font-semibold uppercase tracking-widest text-ink-tertiary">
Explication
</p>
<div className="text-sm leading-relaxed text-ink-2">
<div className="text-sm leading-relaxed text-ink-primary">
<ReactMarkdown disallowedElements={['script', 'iframe']}>
{exercice.explication}
</ReactMarkdown>
</div>
</div>
)}
<p className="text-xs text-ink-4">
<p className="text-xs text-ink-secondary">
Comparez avec votre réponse ci-dessus pour repérer les différences.
</p>
</div>

View file

@ -36,7 +36,7 @@ export function JobStatusFallback({
if (hasTimedOut) {
return (
<Card variant="default" className="space-y-3 p-4">
<p className="text-sm text-ink-2" role="alert">
<p className="text-sm text-ink-primary" role="alert">
La génération prend plus de temps que prévu.
</p>
{onRetry && (
@ -50,8 +50,8 @@ export function JobStatusFallback({
return (
<Card variant="default" className="flex items-center gap-3 p-4">
<Loader2 className="size-4 animate-spin text-ink-4" aria-hidden="true" />
<p className="text-sm text-ink-3" aria-live="polite">
<Loader2 className="size-4 animate-spin text-ink-secondary" aria-hidden="true" />
<p className="text-sm text-ink-secondary" aria-live="polite">
{pendingLabel}
</p>
</Card>

View file

@ -27,12 +27,12 @@ export function ProductionModeleSection({ modele }: Props) {
<div className="space-y-4">
<Card variant="raised" className="space-y-3 p-4">
<div className="flex items-center justify-between gap-3">
<p className="text-[11px] font-semibold uppercase tracking-widest text-ink-5">
<p className="text-[11px] font-semibold uppercase tracking-widest text-ink-tertiary">
Version restructurée NCLC 9+
</p>
<Badge variant="nclc">{modele.tcf_word_count ?? ''} mots</Badge>
</div>
<p className="whitespace-pre-wrap text-sm leading-relaxed text-ink-1">
<p className="whitespace-pre-wrap text-sm leading-relaxed text-ink-primary">
{modele.production_modele_propre}
</p>
{modele.tcf_truncated && (
@ -44,14 +44,14 @@ export function ProductionModeleSection({ modele }: Props) {
{modele.notes_pedagogiques.length > 0 && (
<Card variant="default" className="space-y-3 p-4">
<p className="text-[11px] font-semibold uppercase tracking-widest text-ink-5">
<p className="text-[11px] font-semibold uppercase tracking-widest text-ink-tertiary">
Passages clés
</p>
<ul className="space-y-3">
{modele.notes_pedagogiques.map((n, i) => (
<li key={i} className="space-y-1.5 border-l-2 border-expria pl-3">
<p className="italic text-sm leading-relaxed text-ink-2">« {n.passage} »</p>
<p className="text-xs text-ink-3">{n.explication}</p>
<li key={i} className="space-y-1.5 border-l-2 border-brand pl-3">
<p className="italic text-sm leading-relaxed text-ink-primary">« {n.passage} »</p>
<p className="text-xs text-ink-secondary">{n.explication}</p>
</li>
))}
</ul>
@ -60,27 +60,27 @@ export function ProductionModeleSection({ modele }: Props) {
{modele.transformations.length > 0 && (
<Card variant="default" className="space-y-3 p-4">
<p className="text-[11px] font-semibold uppercase tracking-widest text-ink-5">
<p className="text-[11px] font-semibold uppercase tracking-widest text-ink-tertiary">
Transformations appliquées
</p>
<ul className="space-y-4">
{modele.transformations.map((t, i) => (
<li key={i} className="space-y-2">
<div className="rounded-md border border-line bg-canvas-2 p-2">
<span className="text-[10px] font-semibold uppercase tracking-widest text-ink-5">
<div className="rounded-md border border-border bg-surface p-2">
<span className="text-[10px] font-semibold uppercase tracking-widest text-ink-tertiary">
Original
</span>
<p className="text-sm text-ink-3 line-through decoration-danger decoration-1">
<p className="text-sm text-ink-secondary line-through decoration-danger decoration-1">
{t.original}
</p>
</div>
<div className="rounded-md border border-success/30 bg-success-bg p-2">
<div className="rounded-md border border-success/30 bg-success-soft p-2">
<span className="text-[10px] font-semibold uppercase tracking-widest text-success">
Amélioré
</span>
<p className="text-sm text-ink-1">{t.ameliore}</p>
<p className="text-sm text-ink-primary">{t.ameliore}</p>
</div>
<p className="text-xs text-ink-4">{t.explication}</p>
<p className="text-xs text-ink-secondary">{t.explication}</p>
</li>
))}
</ul>
@ -89,7 +89,7 @@ export function ProductionModeleSection({ modele }: Props) {
{modele.message && (
<Card variant="default" className="border-l-4 border-l-expria p-4">
<div className="text-sm leading-relaxed text-ink-1">
<div className="text-sm leading-relaxed text-ink-primary">
<ReactMarkdown disallowedElements={['script', 'iframe']}>
{modele.message}
</ReactMarkdown>

View file

@ -23,7 +23,7 @@ const SECTIONS: { key: keyof Revelation; titre: string; ton: 'ink' | 'warning' |
]
const TON_CLASS: Record<'ink' | 'warning' | 'danger', string> = {
ink: 'text-ink-2',
ink: 'text-ink-primary',
warning: 'text-warning',
danger: 'text-danger',
}
@ -31,7 +31,7 @@ const TON_CLASS: Record<'ink' | 'warning' | 'danger', string> = {
export function RevelationCards({ revelation }: Props) {
return (
<section aria-label="Lecture du correcteur">
<h2 className="mb-3 text-base font-semibold text-ink-1">Lecture du correcteur</h2>
<h2 className="mb-3 text-base font-semibold text-ink-primary">Lecture du correcteur</h2>
<div className="grid gap-3 sm:grid-cols-3">
{SECTIONS.map(({ key, titre, ton }) => (
<Card key={key} variant="default" className="p-4">
@ -40,7 +40,7 @@ export function RevelationCards({ revelation }: Props) {
>
{titre}
</p>
<div className="text-sm leading-relaxed text-ink-2">
<div className="text-sm leading-relaxed text-ink-primary">
<ReactMarkdown disallowedElements={['script', 'iframe']}>
{revelation[key]}
</ReactMarkdown>

View file

@ -30,14 +30,16 @@ export function ScoreHero({ score, nclc, nclcCible }: Props) {
<Card variant="raised" className="space-y-4 p-6">
<div className="flex flex-wrap items-end gap-8">
<div>
<p className="text-[11px] font-semibold uppercase tracking-widest text-ink-5">Score</p>
<p className="mt-1 tabular-nums text-ink-1">
<p className="text-[11px] font-semibold uppercase tracking-widest text-ink-tertiary">
Score
</p>
<p className="mt-1 tabular-nums text-ink-primary">
<span className="text-5xl font-bold">{score}</span>
<span className="text-2xl font-medium text-ink-4">/20</span>
<span className="text-2xl font-medium text-ink-secondary">/20</span>
</p>
</div>
<div>
<p className="text-[11px] font-semibold uppercase tracking-widest text-ink-5">
<p className="text-[11px] font-semibold uppercase tracking-widest text-ink-tertiary">
Niveau atteint
</p>
<Badge variant="nclc" className="mt-2">
@ -45,7 +47,9 @@ export function ScoreHero({ score, nclc, nclcCible }: Props) {
</Badge>
</div>
<div>
<p className="text-[11px] font-semibold uppercase tracking-widest text-ink-5">Objectif</p>
<p className="text-[11px] font-semibold uppercase tracking-widest text-ink-tertiary">
Objectif
</p>
<Badge variant="neutral" className="mt-2">
NCLC {nclcCible}
</Badge>
@ -55,7 +59,7 @@ export function ScoreHero({ score, nclc, nclcCible }: Props) {
{/* Jauge avec marqueur NCLC cible */}
<div className="space-y-1.5">
<div
className="relative h-2 overflow-hidden rounded-full bg-canvas-2"
className="relative h-2 overflow-hidden rounded-full bg-surface"
role="progressbar"
aria-valuenow={score}
aria-valuemin={0}
@ -63,18 +67,18 @@ export function ScoreHero({ score, nclc, nclcCible }: Props) {
aria-label={`Score ${score} sur 20`}
>
<div
className={`h-full transition-all duration-300 ${atteint ? 'bg-success' : 'bg-expria'}`}
className={`h-full transition-all duration-300 ${atteint ? 'bg-success' : 'bg-brand'}`}
style={{ width: `${percent}%` }}
/>
{/* Marqueur du seuil NCLC cible */}
<div
className="absolute top-0 h-full w-0.5 bg-ink-2"
className="absolute top-0 h-full w-0.5 bg-ink-primary"
style={{ left: `${seuilPercent}%` }}
aria-hidden="true"
title={`Seuil NCLC ${nclcCible} : ${seuilCible}/20`}
/>
</div>
<div className="flex justify-between text-xs text-ink-4 tabular-nums">
<div className="flex justify-between text-xs text-ink-secondary tabular-nums">
<span>0</span>
<span className="font-medium">
Seuil NCLC {nclcCible} : {seuilCible}/20
@ -85,11 +89,11 @@ export function ScoreHero({ score, nclc, nclcCible }: Props) {
{/* Encart d'écart */}
{atteint ? (
<p className="rounded-md border border-success/30 bg-success-bg px-3 py-2 text-sm text-success">
<p className="rounded-md border border-success/30 bg-success-soft px-3 py-2 text-sm text-success">
Objectif NCLC {nclcCible} atteint.
</p>
) : (
<p className="rounded-md border border-warning/30 bg-warning-bg px-3 py-2 text-sm text-warning">
<p className="rounded-md border border-warning/30 bg-warning-soft px-3 py-2 text-sm text-warning">
{points === 1 ? '1 point avant NCLC ' : `${points} points avant NCLC `}
{nclcCible}+
</p>