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:
parent
407d1bd134
commit
b68f160bce
61 changed files with 1269 additions and 726 deletions
|
|
@ -24,7 +24,7 @@ export function ProtectedRoute({ children }: ProtectedRouteProps) {
|
|||
if (isLoading) {
|
||||
return (
|
||||
<div
|
||||
className="flex min-h-screen items-center justify-center bg-canvas text-ink-4"
|
||||
className="flex min-h-screen items-center justify-center bg-canvas text-ink-secondary"
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
aria-label="Chargement de la session"
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ export function LoginPage() {
|
|||
if (isLoading || isAuthenticated) {
|
||||
return (
|
||||
<div
|
||||
className="flex min-h-screen items-center justify-center bg-canvas text-ink-4"
|
||||
className="flex min-h-screen items-center justify-center bg-canvas text-ink-secondary"
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
aria-label="Chargement de la session"
|
||||
|
|
@ -72,14 +72,14 @@ export function LoginPage() {
|
|||
|
||||
return (
|
||||
<main className="flex min-h-screen items-center justify-center bg-canvas px-4 py-8">
|
||||
<section className="w-full max-w-sm rounded-lg border border-line bg-surface p-6 shadow-sm">
|
||||
<h1 className="text-2xl font-semibold text-ink-1">Se connecter</h1>
|
||||
<p className="mt-1 text-sm text-ink-3">Accédez à votre espace Expria.</p>
|
||||
<section className="w-full max-w-sm rounded-lg border border-border bg-surface p-6 shadow-card">
|
||||
<h1 className="text-2xl font-semibold text-ink-primary">Se connecter</h1>
|
||||
<p className="mt-1 text-sm text-ink-secondary">Accédez à votre espace Expria.</p>
|
||||
|
||||
{error && (
|
||||
<div
|
||||
role="alert"
|
||||
className="mt-4 rounded-md border border-danger/40 bg-danger-bg px-3 py-2 text-sm text-danger"
|
||||
className="mt-4 rounded-md border border-danger/40 bg-danger-soft px-3 py-2 text-sm text-danger"
|
||||
>
|
||||
{error}
|
||||
</div>
|
||||
|
|
@ -121,9 +121,9 @@ export function LoginPage() {
|
|||
</Button>
|
||||
</form>
|
||||
|
||||
<p className="mt-6 text-center text-sm text-ink-3">
|
||||
<p className="mt-6 text-center text-sm text-ink-secondary">
|
||||
Pas encore de compte ?{' '}
|
||||
<Link to="/register" className="text-expria underline-offset-4 hover:underline">
|
||||
<Link to="/register" className="text-brand-text underline-offset-4 hover:underline">
|
||||
Créer un compte
|
||||
</Link>
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -84,20 +84,20 @@ export function RegisterPage() {
|
|||
|
||||
return (
|
||||
<main className="flex min-h-screen items-center justify-center bg-canvas px-4 py-8">
|
||||
<section className="w-full max-w-sm rounded-lg border border-line bg-surface p-6 shadow-sm">
|
||||
<h1 className="text-2xl font-semibold text-ink-1">Créer un compte</h1>
|
||||
<p className="mt-1 text-sm text-ink-3">Commencez votre préparation TCF Canada.</p>
|
||||
<section className="w-full max-w-sm rounded-lg border border-border bg-surface p-6 shadow-card">
|
||||
<h1 className="text-2xl font-semibold text-ink-primary">Créer un compte</h1>
|
||||
<p className="mt-1 text-sm text-ink-secondary">Commencez votre préparation TCF Canada.</p>
|
||||
|
||||
{successMessage ? (
|
||||
<>
|
||||
<div
|
||||
role="status"
|
||||
className="mt-6 rounded-md border border-success/40 bg-success-bg px-3 py-3 text-sm text-success"
|
||||
className="mt-6 rounded-md border border-success/40 bg-success-soft px-3 py-3 text-sm text-success"
|
||||
>
|
||||
{successMessage}
|
||||
</div>
|
||||
<p className="mt-6 text-center text-sm text-ink-3">
|
||||
<Link to="/login" className="text-expria underline-offset-4 hover:underline">
|
||||
<p className="mt-6 text-center text-sm text-ink-secondary">
|
||||
<Link to="/login" className="text-brand-text underline-offset-4 hover:underline">
|
||||
Retour à la connexion
|
||||
</Link>
|
||||
</p>
|
||||
|
|
@ -107,7 +107,7 @@ export function RegisterPage() {
|
|||
{formError && (
|
||||
<div
|
||||
role="alert"
|
||||
className="mt-4 rounded-md border border-danger/40 bg-danger-bg px-3 py-2 text-sm text-danger"
|
||||
className="mt-4 rounded-md border border-danger/40 bg-danger-soft px-3 py-2 text-sm text-danger"
|
||||
>
|
||||
{formError}
|
||||
</div>
|
||||
|
|
@ -185,9 +185,9 @@ export function RegisterPage() {
|
|||
</Button>
|
||||
</form>
|
||||
|
||||
<p className="mt-6 text-center text-sm text-ink-3">
|
||||
<p className="mt-6 text-center text-sm text-ink-secondary">
|
||||
Déjà un compte ?{' '}
|
||||
<Link to="/login" className="text-expria underline-offset-4 hover:underline">
|
||||
<Link to="/login" className="text-brand-text underline-offset-4 hover:underline">
|
||||
Se connecter
|
||||
</Link>
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -44,10 +44,10 @@ export function MonProfilPreparation({ plan }: Props) {
|
|||
if (isLoading || isError || !data) {
|
||||
return (
|
||||
<Card variant="default" className="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">
|
||||
Mon profil de préparation
|
||||
</p>
|
||||
<p className="mt-2 text-sm text-ink-4">
|
||||
<p className="mt-2 text-sm text-ink-secondary">
|
||||
{isError ? 'Profil temporairement indisponible.' : 'Chargement…'}
|
||||
</p>
|
||||
</Card>
|
||||
|
|
@ -58,14 +58,14 @@ export function MonProfilPreparation({ plan }: Props) {
|
|||
const remaining = Math.max(0, data.minimum - data.current)
|
||||
return (
|
||||
<Card variant="default" className="space-y-2 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">
|
||||
Mon profil de préparation
|
||||
</p>
|
||||
<p className="text-sm text-ink-2">
|
||||
<p className="text-sm text-ink-primary">
|
||||
Encore <span className="font-semibold tabular-nums">{remaining}</span>{' '}
|
||||
{remaining > 1 ? 'simulations' : 'simulation'} pour débloquer votre profil.
|
||||
</p>
|
||||
<p className="text-xs text-ink-4 tabular-nums">
|
||||
<p className="text-xs text-ink-secondary tabular-nums">
|
||||
{data.current}/{data.minimum} simulations corrigées
|
||||
</p>
|
||||
</Card>
|
||||
|
|
@ -80,27 +80,27 @@ export function MonProfilPreparation({ plan }: Props) {
|
|||
<Card variant="raised" className="space-y-3 p-4">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="space-y-1">
|
||||
<p className="text-[11px] font-semibold uppercase tracking-widest text-ink-5">
|
||||
<p className="text-[11px] font-semibold uppercase tracking-widest text-ink-tertiary">
|
||||
Indice de préparation
|
||||
</p>
|
||||
<p className="tabular-nums text-ink-1">
|
||||
<p className="tabular-nums text-ink-primary">
|
||||
<span className="text-3xl font-bold">{data.preparation_index.score}</span>
|
||||
<span className="text-lg font-medium text-ink-4">/100</span>
|
||||
<span className="text-lg font-medium text-ink-secondary">/100</span>
|
||||
</p>
|
||||
</div>
|
||||
<p className="max-w-[180px] text-right text-xs text-ink-3">
|
||||
<p className="max-w-[180px] text-right text-xs text-ink-secondary">
|
||||
{data.preparation_index.message}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="h-1.5 overflow-hidden rounded-full bg-canvas-2">
|
||||
<div className="h-1.5 overflow-hidden rounded-full bg-surface">
|
||||
<div
|
||||
className={`h-full transition-all duration-300 ${color}`}
|
||||
style={{ width: `${pct}%` }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p className="text-sm text-ink-2">
|
||||
<p className="text-sm text-ink-primary">
|
||||
{patternsCount === 0
|
||||
? 'Aucune erreur récurrente identifiée — continuez !'
|
||||
: `${patternsCount} ${patternsCount > 1 ? 'erreurs récurrentes identifiées' : 'erreur récurrente identifiée'}.`}
|
||||
|
|
|
|||
|
|
@ -9,9 +9,9 @@ import { Button } from '@/shared/components/ui/button'
|
|||
|
||||
export function PaywallBanner() {
|
||||
return (
|
||||
<div className="rounded-lg border border-expria-100 bg-expria-50 p-4 dark:border-expria/20">
|
||||
<p className="text-sm font-semibold text-ink-1">Passez à Standard pour débloquer :</p>
|
||||
<ul className="mt-2 space-y-1 text-sm text-ink-3" role="list">
|
||||
<div className="rounded-lg border border-brand-100 bg-brand-soft p-4 dark:border-brand/20">
|
||||
<p className="text-sm font-semibold text-ink-primary">Passez à Standard pour débloquer :</p>
|
||||
<ul className="mt-2 space-y-1 text-sm text-ink-secondary" role="list">
|
||||
<li>Simulations illimitées</li>
|
||||
<li>Rapport détaillé par critère</li>
|
||||
<li>Historique de vos productions</li>
|
||||
|
|
|
|||
|
|
@ -36,13 +36,13 @@ function getDisplayName(
|
|||
function DashboardSkeleton() {
|
||||
return (
|
||||
<div className="space-y-6" aria-busy="true" aria-label="Chargement du tableau de bord">
|
||||
<div className="h-8 w-48 animate-pulse rounded-md bg-canvas-2" />
|
||||
<div className="h-8 w-48 animate-pulse rounded-md bg-surface" />
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="h-24 animate-pulse rounded-lg bg-canvas-2" />
|
||||
<div className="h-24 animate-pulse rounded-lg bg-canvas-2" />
|
||||
<div className="h-24 animate-pulse rounded-lg bg-surface" />
|
||||
<div className="h-24 animate-pulse rounded-lg bg-surface" />
|
||||
</div>
|
||||
<div className="h-9 animate-pulse rounded-md bg-canvas-2" />
|
||||
<div className="h-16 animate-pulse rounded-lg bg-canvas-2" />
|
||||
<div className="h-9 animate-pulse rounded-md bg-surface" />
|
||||
<div className="h-16 animate-pulse rounded-lg bg-surface" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -78,7 +78,7 @@ export function DashboardPage() {
|
|||
<div className="space-y-6">
|
||||
{/* Salutation */}
|
||||
<section className="flex flex-wrap items-center gap-3">
|
||||
<h1 className="text-2xl font-semibold text-ink-1">Bonjour, {displayName}</h1>
|
||||
<h1 className="text-2xl font-semibold text-ink-primary">Bonjour, {displayName}</h1>
|
||||
<Badge variant="plan" planValue={data.plan}>
|
||||
{PLAN_LABELS[data.plan]}
|
||||
</Badge>
|
||||
|
|
@ -89,15 +89,15 @@ export function DashboardPage() {
|
|||
|
||||
{/* Métriques */}
|
||||
<section className="grid grid-cols-2 gap-4" aria-label="Métriques de préparation">
|
||||
<div className="rounded-lg border border-line bg-surface p-4">
|
||||
<p className="text-xs text-ink-4">Simulations restantes</p>
|
||||
<p className="mt-1 text-2xl font-semibold text-ink-1">
|
||||
<div className="rounded-lg border border-border bg-surface p-4">
|
||||
<p className="text-xs text-ink-secondary">Simulations restantes</p>
|
||||
<p className="mt-1 text-2xl font-semibold text-ink-primary">
|
||||
{data.simulations_remaining === null ? 'Illimitées' : data.simulations_remaining}
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-lg border border-line bg-surface p-4">
|
||||
<p className="text-xs text-ink-4">Niveau NCLC estimé</p>
|
||||
<p className="mt-1 text-2xl font-semibold text-ink-1">—</p>
|
||||
<div className="rounded-lg border border-border bg-surface p-4">
|
||||
<p className="text-xs text-ink-secondary">Niveau NCLC estimé</p>
|
||||
<p className="mt-1 text-2xl font-semibold text-ink-primary">—</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
|
@ -113,8 +113,8 @@ export function DashboardPage() {
|
|||
|
||||
{/* Dernières simulations */}
|
||||
<section aria-label="Dernières simulations">
|
||||
<h2 className="text-base font-semibold text-ink-1">Dernières simulations</h2>
|
||||
<p className="mt-2 text-sm text-ink-4">Aucune simulation pour l'instant.</p>
|
||||
<h2 className="text-base font-semibold text-ink-primary">Dernières simulations</h2>
|
||||
<p className="mt-2 text-sm text-ink-secondary">Aucune simulation pour l'instant.</p>
|
||||
</section>
|
||||
|
||||
{/* Mon profil de préparation — Premium uniquement (gate via hasAccess) */}
|
||||
|
|
|
|||
|
|
@ -23,42 +23,173 @@ import {
|
|||
DialogTrigger,
|
||||
} from '@/shared/components/ui/dialog'
|
||||
|
||||
// ─── palette data ────────────────────────────────────────────────────────────
|
||||
// ─── palette data — DA Charcoal ──────────────────────────────────────────────
|
||||
|
||||
const PALETTE: { token: string; var: string; light: string; dark: string }[] = [
|
||||
{ token: 'canvas', var: '--color-canvas', light: '#EEF2F8', dark: '#0D1220' },
|
||||
{ token: 'canvas-2', var: '--color-canvas-2', light: '#E6EBF4', dark: '#121A2D' },
|
||||
{ token: 'surface', var: '--color-surface', light: '#FFFFFF', dark: '#182238' },
|
||||
{ token: 'surface-hover', var: '--color-surface-hover', light: '#F8FAFD', dark: '#1E2A42' },
|
||||
{ token: 'line', var: '--color-line', light: '#DDE3ED', dark: '#27324B' },
|
||||
{ token: 'line-strong', var: '--color-line-strong', light: '#C7D0E0', dark: '#364363' },
|
||||
{ token: 'ink-1', var: '--color-ink-1', light: '#0F172A', dark: '#F1F4FA' },
|
||||
{ token: 'ink-2', var: '--color-ink-2', light: '#1E293B', dark: '#DDE3EF' },
|
||||
{ token: 'ink-3', var: '--color-ink-3', light: '#475569', dark: '#A8B2C7' },
|
||||
{ token: 'ink-4', var: '--color-ink-4', light: '#64748B', dark: '#7A8499' },
|
||||
{ token: 'ink-5', var: '--color-ink-5', light: '#94A3B8', dark: '#525C73' },
|
||||
{ token: 'expria', var: '--color-expria', light: '#1B4FD8', dark: '#5B7FFF' },
|
||||
{ token: 'expria-hover', var: '--color-expria-hover', light: '#1741B8', dark: '#6F8EFF' },
|
||||
{ token: 'expria-50', var: '--color-expria-50', light: '#EEF3FF', dark: 'rgba(91,127,255,.12)' },
|
||||
{ token: 'expria-100', var: '--color-expria-100', light: '#DCE6FF', dark: '—' },
|
||||
{ token: 'expria-200', var: '--color-expria-200', light: '#B8CDFF', dark: '—' },
|
||||
{ token: 'deep', var: '--color-deep', light: '#0B1F5C', dark: '#060B1A' },
|
||||
{ token: 'success', var: '--color-success', light: '#0E9F6E', dark: '#3DD68C' },
|
||||
interface PaletteEntry {
|
||||
token: string
|
||||
cssVar: string
|
||||
dark: string
|
||||
light: string
|
||||
group: 'Invariants' | 'Dark default' | 'Light override'
|
||||
}
|
||||
|
||||
const PALETTE: PaletteEntry[] = [
|
||||
// Invariants
|
||||
{
|
||||
token: 'success-bg',
|
||||
var: '--color-success-bg',
|
||||
light: '#E6F6F0',
|
||||
dark: 'rgba(61,214,140,.12)',
|
||||
token: 'sidebar-bg',
|
||||
cssVar: '--color-sidebar-bg',
|
||||
dark: '#0C1528',
|
||||
light: '#0C1528',
|
||||
group: 'Invariants',
|
||||
},
|
||||
{ token: 'warning', var: '--color-warning', light: '#C77A00', dark: '#F5B849' },
|
||||
{
|
||||
token: 'warning-bg',
|
||||
var: '--color-warning-bg',
|
||||
light: '#FEF3E2',
|
||||
dark: 'rgba(245,184,73,.12)',
|
||||
token: 'brand',
|
||||
cssVar: '--color-brand',
|
||||
dark: '#1B4FD8',
|
||||
light: '#1B4FD8',
|
||||
group: 'Invariants',
|
||||
},
|
||||
{
|
||||
token: 'brand-hover',
|
||||
cssVar: '--color-brand-hover',
|
||||
dark: '#1744B8',
|
||||
light: '#1744B8',
|
||||
group: 'Invariants',
|
||||
},
|
||||
{
|
||||
token: 'brand-active',
|
||||
cssVar: '--color-brand-active',
|
||||
dark: '#13379C',
|
||||
light: '#13379C',
|
||||
group: 'Invariants',
|
||||
},
|
||||
{
|
||||
token: 'warning',
|
||||
cssVar: '--color-warning',
|
||||
dark: '#F59E0B',
|
||||
light: '#F59E0B',
|
||||
group: 'Invariants',
|
||||
},
|
||||
{
|
||||
token: 'danger',
|
||||
cssVar: '--color-danger',
|
||||
dark: '#EF4444',
|
||||
light: '#EF4444',
|
||||
group: 'Invariants',
|
||||
},
|
||||
// Dual-theme (valeurs différentes dark/light)
|
||||
{
|
||||
token: 'canvas',
|
||||
cssVar: '--color-canvas',
|
||||
dark: '#111111',
|
||||
light: '#F3F4F6',
|
||||
group: 'Dark default',
|
||||
},
|
||||
{
|
||||
token: 'surface',
|
||||
cssVar: '--color-surface',
|
||||
dark: 'rgba(255,255,255,.035)',
|
||||
light: '#FFFFFF',
|
||||
group: 'Dark default',
|
||||
},
|
||||
{
|
||||
token: 'surface-hover',
|
||||
cssVar: '--color-surface-hover',
|
||||
dark: 'rgba(255,255,255,.055)',
|
||||
light: '#F8F9FB',
|
||||
group: 'Dark default',
|
||||
},
|
||||
{
|
||||
token: 'surface-solid',
|
||||
cssVar: '--color-surface-solid',
|
||||
dark: '#1E1E1E',
|
||||
light: '#FFFFFF',
|
||||
group: 'Dark default',
|
||||
},
|
||||
{
|
||||
token: 'surface-raised',
|
||||
cssVar: '--color-surface-raised',
|
||||
dark: '#222222',
|
||||
light: '#FFFFFF',
|
||||
group: 'Dark default',
|
||||
},
|
||||
{
|
||||
token: 'border',
|
||||
cssVar: '--color-border',
|
||||
dark: 'rgba(255,255,255,.06)',
|
||||
light: 'rgba(0,0,0,.07)',
|
||||
group: 'Dark default',
|
||||
},
|
||||
{
|
||||
token: 'border-strong',
|
||||
cssVar: '--color-border-strong',
|
||||
dark: 'rgba(255,255,255,.12)',
|
||||
light: 'rgba(0,0,0,.14)',
|
||||
group: 'Dark default',
|
||||
},
|
||||
{
|
||||
token: 'ink-primary',
|
||||
cssVar: '--color-ink-primary',
|
||||
dark: '#E5E5E5',
|
||||
light: '#0F0F1A',
|
||||
group: 'Dark default',
|
||||
},
|
||||
{
|
||||
token: 'ink-secondary',
|
||||
cssVar: '--color-ink-secondary',
|
||||
dark: 'rgba(255,255,255,.55)',
|
||||
light: 'rgba(0,0,0,.55)',
|
||||
group: 'Dark default',
|
||||
},
|
||||
{
|
||||
token: 'ink-tertiary',
|
||||
cssVar: '--color-ink-tertiary',
|
||||
dark: 'rgba(255,255,255,.3)',
|
||||
light: 'rgba(0,0,0,.3)',
|
||||
group: 'Dark default',
|
||||
},
|
||||
{
|
||||
token: 'brand-soft',
|
||||
cssVar: '--color-brand-soft',
|
||||
dark: 'rgba(27,79,216,.1)',
|
||||
light: 'rgba(27,79,216,.06)',
|
||||
group: 'Dark default',
|
||||
},
|
||||
{
|
||||
token: 'brand-text',
|
||||
cssVar: '--color-brand-text',
|
||||
dark: '#7DA4F0',
|
||||
light: '#1B4FD8',
|
||||
group: 'Dark default',
|
||||
},
|
||||
{
|
||||
token: 'success',
|
||||
cssVar: '--color-success',
|
||||
dark: '#4ADE80',
|
||||
light: '#16A34A',
|
||||
group: 'Dark default',
|
||||
},
|
||||
{
|
||||
token: 'success-soft',
|
||||
cssVar: '--color-success-soft',
|
||||
dark: 'rgba(74,222,128,.12)',
|
||||
light: 'rgba(22,163,74,.1)',
|
||||
group: 'Dark default',
|
||||
},
|
||||
{
|
||||
token: 'warning-soft',
|
||||
cssVar: '--color-warning-soft',
|
||||
dark: 'rgba(245,158,11,.12)',
|
||||
light: 'rgba(245,158,11,.12)',
|
||||
group: 'Dark default',
|
||||
},
|
||||
{
|
||||
token: 'danger-soft',
|
||||
cssVar: '--color-danger-soft',
|
||||
dark: 'rgba(239,68,68,.12)',
|
||||
light: 'rgba(239,68,68,.12)',
|
||||
group: 'Dark default',
|
||||
},
|
||||
{ token: 'danger', var: '--color-danger', light: '#C53030', dark: '#F06B6B' },
|
||||
{ token: 'danger-bg', var: '--color-danger-bg', light: '#FDECEC', dark: 'rgba(240,107,107,.12)' },
|
||||
]
|
||||
|
||||
// ─── section wrapper ─────────────────────────────────────────────────────────
|
||||
|
|
@ -66,7 +197,9 @@ const PALETTE: { token: string; var: string; light: string; dark: string }[] = [
|
|||
function Section({ title, children }: { title: string; children: React.ReactNode }) {
|
||||
return (
|
||||
<section className="space-y-4">
|
||||
<h2 className="text-base font-semibold text-ink-3 uppercase tracking-wider">{title}</h2>
|
||||
<h2 className="text-base font-semibold uppercase tracking-wider text-ink-secondary">
|
||||
{title}
|
||||
</h2>
|
||||
<Separator />
|
||||
{children}
|
||||
</section>
|
||||
|
|
@ -80,12 +213,14 @@ export default function DesignSystemPage() {
|
|||
const [dialogOpen, setDialogOpen] = useState(false)
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-canvas px-6 py-10 space-y-14">
|
||||
<div className="min-h-screen space-y-14 bg-canvas px-6 py-10">
|
||||
{/* ── header ── */}
|
||||
<header className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-ink-1">Design System</h1>
|
||||
<p className="text-sm text-ink-4 mt-0.5">Expria — Direction H palette · Sprint 0.5</p>
|
||||
<h1 className="text-2xl font-bold text-ink-primary">Design System</h1>
|
||||
<p className="mt-0.5 text-sm text-ink-secondary">
|
||||
Expria — DA Charcoal · dark-default + light override
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
|
|
@ -98,17 +233,17 @@ export default function DesignSystemPage() {
|
|||
|
||||
{/* ── palette ── */}
|
||||
<Section title="Palette">
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-3">
|
||||
{PALETTE.map(({ token, var: cssVar, light, dark }) => (
|
||||
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-4">
|
||||
{PALETTE.map(({ token, cssVar, light, dark }) => (
|
||||
<div key={token} className="flex flex-col gap-1.5">
|
||||
<div
|
||||
className="h-12 w-full rounded-md border border-line shadow-sm"
|
||||
className="h-12 w-full rounded-md border border-border shadow-card"
|
||||
style={{ background: `var(${cssVar})` }}
|
||||
/>
|
||||
<div className="space-y-0.5">
|
||||
<p className="text-xs font-mono font-medium text-ink-2">{token}</p>
|
||||
<p className="text-xs font-mono text-ink-4 leading-tight">☀ {light}</p>
|
||||
<p className="text-xs font-mono text-ink-4 leading-tight">☾ {dark}</p>
|
||||
<p className="font-mono text-xs font-medium text-ink-primary">{token}</p>
|
||||
<p className="font-mono text-xs leading-tight text-ink-secondary">☾ {dark}</p>
|
||||
<p className="font-mono text-xs leading-tight text-ink-secondary">☀ {light}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
|
@ -117,22 +252,22 @@ export default function DesignSystemPage() {
|
|||
|
||||
{/* ── typography ── */}
|
||||
<Section title="Typography">
|
||||
<div className="space-y-3 bg-surface rounded-lg p-6 border border-line">
|
||||
<p className="text-4xl font-bold text-ink-1">Display / 36px Bold</p>
|
||||
<p className="text-2xl font-semibold text-ink-1">Heading 1 / 24px Semibold</p>
|
||||
<p className="text-xl font-semibold text-ink-1">Heading 2 / 20px Semibold</p>
|
||||
<p className="text-lg font-medium text-ink-2">Heading 3 / 18px Medium</p>
|
||||
<p className="text-base text-ink-2">Body / 16px Regular — Plus Jakarta Sans</p>
|
||||
<p className="text-sm text-ink-3">Small / 14px Regular — secondary copy</p>
|
||||
<p className="text-xs text-ink-4">Caption / 12px Regular — labels, metadata</p>
|
||||
<p className="text-xs font-mono text-ink-3">Mono / 12px — token names, code</p>
|
||||
<div className="space-y-3 rounded-lg border border-border bg-surface p-6">
|
||||
<p className="text-4xl font-bold text-ink-primary">Display / 40px Bold</p>
|
||||
<p className="text-2xl font-semibold text-ink-primary">Heading 1 / 24px Semibold</p>
|
||||
<p className="text-xl font-semibold text-ink-primary">Heading 2 / 20px Semibold</p>
|
||||
<p className="text-lg font-medium text-ink-primary">Heading 3 / 17px Medium</p>
|
||||
<p className="text-base text-ink-primary">Body / 14px Regular — Plus Jakarta Sans</p>
|
||||
<p className="text-sm text-ink-secondary">Small / 13px Regular — secondary copy</p>
|
||||
<p className="text-xs text-ink-tertiary">Caption / 11px Regular — labels, metadata</p>
|
||||
<p className="font-mono text-xs text-ink-secondary">Mono / 11px — token names, code</p>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* ── buttons ── */}
|
||||
<Section title="Button">
|
||||
<div className="space-y-4 bg-surface rounded-lg p-6 border border-line">
|
||||
<div className="flex flex-wrap gap-2 items-center">
|
||||
<div className="space-y-4 rounded-lg border border-border bg-surface p-6">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Button>Default</Button>
|
||||
<Button variant="secondary">Secondary</Button>
|
||||
<Button variant="outline">Outline</Button>
|
||||
|
|
@ -140,13 +275,13 @@ export default function DesignSystemPage() {
|
|||
<Button variant="destructive">Destructive</Button>
|
||||
<Button variant="link">Link</Button>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2 items-center">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Button size="lg">Large</Button>
|
||||
<Button>Default</Button>
|
||||
<Button size="sm">Small</Button>
|
||||
<Button size="icon">+</Button>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2 items-center">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Button disabled>Disabled</Button>
|
||||
<Button variant="outline" disabled>
|
||||
Outline disabled
|
||||
|
|
@ -157,7 +292,7 @@ export default function DesignSystemPage() {
|
|||
|
||||
{/* ── badges ── */}
|
||||
<Section title="Badge">
|
||||
<div className="flex flex-wrap gap-2 bg-surface rounded-lg p-6 border border-line">
|
||||
<div className="flex flex-wrap gap-2 rounded-lg border border-border bg-surface p-6">
|
||||
<Badge>Default</Badge>
|
||||
<Badge variant="secondary">Secondary</Badge>
|
||||
<Badge variant="outline">Outline</Badge>
|
||||
|
|
@ -167,7 +302,7 @@ export default function DesignSystemPage() {
|
|||
|
||||
{/* ── inputs / forms ── */}
|
||||
<Section title="Input · Label · Progress · Separator">
|
||||
<div className="space-y-5 bg-surface rounded-lg p-6 border border-line max-w-md">
|
||||
<div className="max-w-md space-y-5 rounded-lg border border-border bg-surface p-6">
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="ds-email">Email</Label>
|
||||
<Input id="ds-email" type="email" placeholder="you@expria.io" />
|
||||
|
|
@ -185,33 +320,33 @@ export default function DesignSystemPage() {
|
|||
<Progress value={65} />
|
||||
</div>
|
||||
<Separator />
|
||||
<p className="text-sm text-ink-4">Content below separator</p>
|
||||
<p className="text-sm text-ink-secondary">Content below separator</p>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* ── avatar ── */}
|
||||
<Section title="Avatar">
|
||||
<div className="flex flex-wrap items-end gap-6 bg-surface rounded-lg p-6 border border-line">
|
||||
<div className="flex flex-wrap items-end gap-6 rounded-lg border border-border bg-surface p-6">
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<Avatar size="sm">
|
||||
<AvatarImage src="" />
|
||||
<AvatarFallback>HK</AvatarFallback>
|
||||
</Avatar>
|
||||
<span className="text-xs text-ink-4">sm</span>
|
||||
<span className="text-xs text-ink-secondary">sm</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<Avatar>
|
||||
<AvatarImage src="" />
|
||||
<AvatarFallback>HK</AvatarFallback>
|
||||
</Avatar>
|
||||
<span className="text-xs text-ink-4">default</span>
|
||||
<span className="text-xs text-ink-secondary">default</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<Avatar size="lg">
|
||||
<AvatarImage src="" />
|
||||
<AvatarFallback>HK</AvatarFallback>
|
||||
</Avatar>
|
||||
<span className="text-xs text-ink-4">lg</span>
|
||||
<span className="text-xs text-ink-secondary">lg</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<AvatarGroup>
|
||||
|
|
@ -222,14 +357,14 @@ export default function DesignSystemPage() {
|
|||
))}
|
||||
<AvatarGroupCount>+5</AvatarGroupCount>
|
||||
</AvatarGroup>
|
||||
<span className="text-xs text-ink-4">group</span>
|
||||
<span className="text-xs text-ink-secondary">group</span>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* ── dialog ── */}
|
||||
<Section title="Dialog">
|
||||
<div className="bg-surface rounded-lg p-6 border border-line">
|
||||
<div className="rounded-lg border border-border bg-surface p-6">
|
||||
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline">Open dialog</Button>
|
||||
|
|
@ -238,8 +373,8 @@ export default function DesignSystemPage() {
|
|||
<DialogHeader>
|
||||
<DialogTitle>Example dialog</DialogTitle>
|
||||
<DialogDescription>
|
||||
This dialog uses Direction H tokens — bg-surface, border-line, text-ink-4. Toggle
|
||||
the theme to see it adapt.
|
||||
This dialog uses DA Charcoal tokens — bg-surface-solid, border-border,
|
||||
text-ink-secondary. Toggle the theme to see it adapt.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter showCloseButton>
|
||||
|
|
|
|||
|
|
@ -25,31 +25,33 @@ export function SimulationListItem({ item }: Props) {
|
|||
return (
|
||||
<Link
|
||||
to={`/rapport/${item.id}`}
|
||||
className="block rounded-lg border border-line bg-surface p-4 shadow-sm transition-colors duration-150 hover:border-expria hover:bg-surface-hover focus-visible:outline-none focus-visible:shadow-focus"
|
||||
className="block rounded-lg border border-border bg-surface p-4 shadow-card transition-colors duration-150 hover:border-brand hover:bg-surface-hover focus-visible:outline-none focus-visible:shadow-focus"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0 space-y-1">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="text-sm font-semibold text-ink-1">{formatTache(item.tache)}</span>
|
||||
<span className="text-sm font-semibold text-ink-primary">
|
||||
{formatTache(item.tache)}
|
||||
</span>
|
||||
{isExam && <Badge variant="nclc">Examen</Badge>}
|
||||
{!hasScore && <Badge variant="neutral">En cours</Badge>}
|
||||
</div>
|
||||
<p className="text-xs text-ink-4">{formatRelativeDate(item.created_at)}</p>
|
||||
<p className="text-xs text-ink-secondary">{formatRelativeDate(item.created_at)}</p>
|
||||
</div>
|
||||
|
||||
{hasScore ? (
|
||||
<div className="shrink-0 text-right">
|
||||
<p className="tabular-nums text-ink-1">
|
||||
<p className="tabular-nums text-ink-primary">
|
||||
<span className="text-xl font-bold">{item.score}</span>
|
||||
<span className="text-sm font-medium text-ink-4">/20</span>
|
||||
<span className="text-sm font-medium text-ink-secondary">/20</span>
|
||||
</p>
|
||||
<p className="text-xs text-ink-4 tabular-nums">
|
||||
<p className="text-xs text-ink-secondary tabular-nums">
|
||||
NCLC {item.nclc}
|
||||
{item.nclc_cible ? ` / cible ${item.nclc_cible}` : ''}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="shrink-0 text-right text-xs text-ink-4">Score à venir</div>
|
||||
<div className="shrink-0 text-right text-xs text-ink-secondary">Score à venir</div>
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
|
|
|
|||
|
|
@ -35,15 +35,15 @@ const PLACEHOLDER_WIDTHS = ['w-3/4', 'w-full', 'w-3/5', 'w-4/5'] as const
|
|||
|
||||
function BlurredPreview({ onUpgrade }: { onUpgrade: () => void }) {
|
||||
return (
|
||||
<div className="relative min-h-[240px] overflow-hidden rounded-lg border border-line bg-canvas-2">
|
||||
<div className="relative min-h-[240px] overflow-hidden rounded-lg border border-border bg-surface">
|
||||
<div className="space-y-3 p-4 opacity-25 blur-sm" aria-hidden="true">
|
||||
{PLACEHOLDER_WIDTHS.map((w, i) => (
|
||||
<div key={i} className={`h-16 rounded bg-ink-4 ${w}`} />
|
||||
<div key={i} className={`h-16 rounded bg-surface-hover ${w}`} />
|
||||
))}
|
||||
</div>
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center gap-3 px-4">
|
||||
<Lock className="size-5 text-ink-4" aria-hidden="true" />
|
||||
<p className="text-sm font-medium text-ink-2">Historique disponible en Standard</p>
|
||||
<Lock className="size-5 text-ink-secondary" aria-hidden="true" />
|
||||
<p className="text-sm font-medium text-ink-primary">Historique disponible en Standard</p>
|
||||
<Button variant="upgrade" size="sm" onClick={onUpgrade}>
|
||||
Passer en Standard
|
||||
</Button>
|
||||
|
|
@ -56,7 +56,7 @@ function ListSkeleton() {
|
|||
return (
|
||||
<div className="space-y-3" aria-busy="true" aria-label="Chargement de l'historique…">
|
||||
{Array.from({ length: 5 }).map((_, i) => (
|
||||
<div key={i} className="h-20 animate-pulse rounded-lg bg-canvas-2" />
|
||||
<div key={i} className="h-20 animate-pulse rounded-lg bg-surface" />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
|
|
@ -65,8 +65,8 @@ function ListSkeleton() {
|
|||
function EmptyState() {
|
||||
return (
|
||||
<Card variant="default" className="space-y-3 p-6 text-center">
|
||||
<p className="text-sm text-ink-2">Aucune simulation pour le moment.</p>
|
||||
<p className="text-xs text-ink-4">
|
||||
<p className="text-sm text-ink-primary">Aucune simulation pour le moment.</p>
|
||||
<p className="text-xs text-ink-secondary">
|
||||
Lancez votre première simulation pour commencer à construire votre historique.
|
||||
</p>
|
||||
<div className="flex justify-center">
|
||||
|
|
@ -134,7 +134,7 @@ export function SimulationsList({
|
|||
<Button variant="secondary" size="sm" onClick={onPrev} disabled={isFirst}>
|
||||
Précédent
|
||||
</Button>
|
||||
<p className="text-xs text-ink-4 tabular-nums" aria-live="polite">
|
||||
<p className="text-xs text-ink-secondary tabular-nums" aria-live="polite">
|
||||
Page {page} sur {totalPages} — {data.pagination.total} simulations
|
||||
</p>
|
||||
<Button variant="secondary" size="sm" onClick={onNext} disabled={isLast}>
|
||||
|
|
|
|||
|
|
@ -29,16 +29,16 @@ export function HistoriquePage() {
|
|||
return (
|
||||
<main className="mx-auto max-w-3xl space-y-6 px-4 py-6">
|
||||
<header className="space-y-1">
|
||||
<h1 className="text-lg font-semibold text-ink-1">Historique</h1>
|
||||
<p className="text-sm text-ink-3">
|
||||
<h1 className="text-lg font-semibold text-ink-primary">Historique</h1>
|
||||
<p className="text-sm text-ink-secondary">
|
||||
Retrouvez toutes vos simulations passées et leur progression.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
{isPlanLoading || !planData ? (
|
||||
<div className="space-y-3" aria-busy="true">
|
||||
<div className="h-20 animate-pulse rounded-lg bg-canvas-2" />
|
||||
<div className="h-20 animate-pulse rounded-lg bg-canvas-2" />
|
||||
<div className="h-20 animate-pulse rounded-lg bg-surface" />
|
||||
<div className="h-20 animate-pulse rounded-lg bg-surface" />
|
||||
</div>
|
||||
) : (
|
||||
<SimulationsList
|
||||
|
|
|
|||
|
|
@ -19,19 +19,19 @@ const PLACEHOLDER_HEIGHTS = ['h-24', 'h-16', 'h-16', 'h-20'] as const
|
|||
|
||||
export function BlurredProgression({ onUpgrade }: Props) {
|
||||
return (
|
||||
<div className="relative min-h-[320px] overflow-hidden rounded-lg border border-line bg-canvas-2">
|
||||
<div className="relative min-h-[320px] overflow-hidden rounded-lg border border-border bg-surface">
|
||||
<div className="space-y-3 p-4 opacity-25 blur-sm" aria-hidden="true">
|
||||
{PLACEHOLDER_HEIGHTS.map((h, i) => (
|
||||
<div key={i} className={`${h} rounded bg-ink-4`} />
|
||||
<div key={i} className={`${h} rounded bg-surface-hover`} />
|
||||
))}
|
||||
</div>
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center gap-3 px-4 text-center">
|
||||
<Lock className="size-6 text-ink-4" aria-hidden="true" />
|
||||
<Lock className="size-6 text-ink-secondary" aria-hidden="true" />
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm font-semibold text-ink-2">
|
||||
<p className="text-sm font-semibold text-ink-primary">
|
||||
Profil de préparation — Exclusivité Premium
|
||||
</p>
|
||||
<p className="max-w-sm text-xs text-ink-4">
|
||||
<p className="max-w-sm text-xs text-ink-secondary">
|
||||
Analysez vos erreurs récurrentes, recevez des exercices ciblés long terme, et suivez
|
||||
votre indice de préparation au TCF Canada.
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -23,10 +23,10 @@ export function NotReadyState({ current, minimum }: Props) {
|
|||
return (
|
||||
<Card variant="raised" className="space-y-4 p-6 text-center">
|
||||
<div className="space-y-1">
|
||||
<h2 className="text-lg font-semibold text-ink-1">Profil de préparation</h2>
|
||||
<p className="text-sm leading-relaxed text-ink-3">
|
||||
<h2 className="text-lg font-semibold text-ink-primary">Profil de préparation</h2>
|
||||
<p className="text-sm leading-relaxed text-ink-secondary">
|
||||
Vous avez réalisé{' '}
|
||||
<span className="font-semibold text-ink-1 tabular-nums">
|
||||
<span className="font-semibold text-ink-primary tabular-nums">
|
||||
{current}/{minimum}
|
||||
</span>{' '}
|
||||
simulations corrigées.{' '}
|
||||
|
|
@ -37,17 +37,14 @@ export function NotReadyState({ current, minimum }: Props) {
|
|||
</div>
|
||||
|
||||
<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={current}
|
||||
aria-valuemin={0}
|
||||
aria-valuemax={minimum}
|
||||
aria-label={`Progression : ${current} sur ${minimum}`}
|
||||
>
|
||||
<div
|
||||
className="h-full bg-expria transition-all duration-300"
|
||||
style={{ width: `${pct}%` }}
|
||||
/>
|
||||
<div className="h-full bg-brand transition-all duration-300" style={{ width: `${pct}%` }} />
|
||||
</div>
|
||||
|
||||
<div className="flex justify-center">
|
||||
|
|
|
|||
|
|
@ -35,10 +35,12 @@ export function PatternExerciceCard({ exercice }: Props) {
|
|||
<div className="space-y-2">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Badge variant="neutral">{critereLabel}</Badge>
|
||||
<span className="text-xs font-medium text-ink-4">{exercice.code.replace(/_/g, ' ')}</span>
|
||||
<span className="text-xs font-medium text-ink-secondary">
|
||||
{exercice.code.replace(/_/g, ' ')}
|
||||
</span>
|
||||
</div>
|
||||
{exercice.diagnostic && (
|
||||
<p className="text-sm leading-relaxed text-ink-2">
|
||||
<p className="text-sm leading-relaxed text-ink-primary">
|
||||
<ReactMarkdown
|
||||
disallowedElements={['script', 'iframe']}
|
||||
components={{ p: ({ children }) => <span>{children}</span> }}
|
||||
|
|
@ -50,36 +52,38 @@ export function PatternExerciceCard({ exercice }: Props) {
|
|||
</div>
|
||||
|
||||
{exercice.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.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.exercice.consigne}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid gap-2 sm:grid-cols-2">
|
||||
<div className="space-y-1.5 rounded-md border border-danger/30 bg-danger-bg p-3">
|
||||
<div className="space-y-1.5 rounded-md border border-danger/30 bg-danger-soft p-3">
|
||||
<p className="text-[11px] font-semibold uppercase tracking-widest text-danger">
|
||||
Incorrect
|
||||
</p>
|
||||
<p className="text-sm leading-relaxed text-ink-1 line-through decoration-danger decoration-1">
|
||||
<p className="text-sm leading-relaxed text-ink-primary line-through decoration-danger decoration-1">
|
||||
{exercice.exercice.exemple}
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-1.5 rounded-md border border-success/30 bg-success-bg p-3">
|
||||
<div className="space-y-1.5 rounded-md border border-success/30 bg-success-soft p-3">
|
||||
<p className="text-[11px] font-semibold uppercase tracking-widest text-success">
|
||||
Correct
|
||||
</p>
|
||||
<p className="text-sm leading-relaxed text-ink-1">{exercice.exercice.correction}</p>
|
||||
<p className="text-sm leading-relaxed text-ink-primary">{exercice.exercice.correction}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3 rounded-md border border-warning/30 bg-warning-bg p-3">
|
||||
<div className="flex gap-3 rounded-md border border-warning/30 bg-warning-soft p-3">
|
||||
<Lightbulb className="mt-0.5 size-4 shrink-0 text-warning" aria-hidden="true" />
|
||||
<div className="space-y-1">
|
||||
<p className="text-[11px] font-semibold uppercase tracking-widest text-warning">
|
||||
Astuce de relecture
|
||||
</p>
|
||||
<p className="text-sm leading-relaxed text-ink-1">{exercice.exercice.astuce}</p>
|
||||
<p className="text-sm leading-relaxed text-ink-primary">{exercice.exercice.astuce}</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ export function PatternsList({ patterns }: Props) {
|
|||
if (patterns.length === 0) {
|
||||
return (
|
||||
<Card variant="default" className="p-4">
|
||||
<p className="text-sm text-ink-3">
|
||||
<p className="text-sm text-ink-secondary">
|
||||
Aucune erreur récurrente détectée sur vos 5 dernières productions. Continuez ainsi !
|
||||
</p>
|
||||
</Card>
|
||||
|
|
@ -37,10 +37,10 @@ export function PatternsList({ patterns }: Props) {
|
|||
<li key={`${p.critere}-${p.code}-${p.description ?? ''}`}>
|
||||
<Card variant="default" className="flex items-start justify-between gap-3 p-4">
|
||||
<div className="min-w-0 space-y-1">
|
||||
<p className="text-sm font-semibold text-ink-1">
|
||||
<p className="text-sm font-semibold text-ink-primary">
|
||||
{p.description ?? humanizeCode(p.code)}
|
||||
</p>
|
||||
<p className="text-xs text-ink-4">{CRITERE_LABELS[p.critere]}</p>
|
||||
<p className="text-xs text-ink-secondary">{CRITERE_LABELS[p.critere]}</p>
|
||||
</div>
|
||||
<Badge variant="nclc" className="shrink-0 tabular-nums">
|
||||
{p.frequency}/5
|
||||
|
|
|
|||
|
|
@ -29,19 +29,19 @@ export function PreparationIndexHero({ index }: Props) {
|
|||
<Card variant="raised" className="space-y-4 p-6">
|
||||
<div className="flex flex-wrap items-baseline justify-between gap-4">
|
||||
<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">
|
||||
Indice de préparation
|
||||
</p>
|
||||
<p className="mt-1 tabular-nums text-ink-1">
|
||||
<p className="mt-1 tabular-nums text-ink-primary">
|
||||
<span className="text-5xl font-bold">{index.score}</span>
|
||||
<span className="text-2xl font-medium text-ink-4">/100</span>
|
||||
<span className="text-2xl font-medium text-ink-secondary">/100</span>
|
||||
</p>
|
||||
</div>
|
||||
<p className="max-w-xs text-sm leading-relaxed text-ink-2">{index.message}</p>
|
||||
<p className="max-w-xs text-sm leading-relaxed text-ink-primary">{index.message}</p>
|
||||
</div>
|
||||
|
||||
<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={pct}
|
||||
aria-valuemin={0}
|
||||
|
|
@ -53,7 +53,7 @@ export function PreparationIndexHero({ index }: Props) {
|
|||
style={{ width: `${pct}%` }}
|
||||
/>
|
||||
</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>40</span>
|
||||
<span>70</span>
|
||||
|
|
|
|||
|
|
@ -31,13 +31,13 @@ export function ProgressionPremium({ data }: Props) {
|
|||
<PreparationIndexHero index={data.preparation_index} />
|
||||
|
||||
<section aria-label="Erreurs récurrentes">
|
||||
<h2 className="mb-3 text-base font-semibold text-ink-1">Erreurs récurrentes</h2>
|
||||
<h2 className="mb-3 text-base font-semibold text-ink-primary">Erreurs récurrentes</h2>
|
||||
<PatternsList patterns={data.patterns} />
|
||||
</section>
|
||||
|
||||
{data.exercises.length > 0 && (
|
||||
<section aria-label="Exercices long terme">
|
||||
<h2 className="mb-3 text-base font-semibold text-ink-1">Exercices long terme</h2>
|
||||
<h2 className="mb-3 text-base font-semibold text-ink-primary">Exercices long terme</h2>
|
||||
<div className="space-y-3">
|
||||
{data.exercises.map((ex, i) => (
|
||||
<PatternExerciceCard key={`${ex.code}-${i}`} exercice={ex} />
|
||||
|
|
@ -47,7 +47,7 @@ export function ProgressionPremium({ data }: Props) {
|
|||
)}
|
||||
|
||||
<Card variant="default" className="p-3">
|
||||
<p className="text-center text-xs text-ink-4">
|
||||
<p className="text-center text-xs text-ink-secondary">
|
||||
Analyse basée sur vos {data.analyzed_productions} dernières productions —{' '}
|
||||
{formatRelativeDate(data.last_analysis)}
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -21,9 +21,9 @@ import { ProgressionPremium } from '../components/ProgressionPremium'
|
|||
function Skeleton() {
|
||||
return (
|
||||
<div className="space-y-4" aria-busy="true" aria-label="Chargement de votre profil…">
|
||||
<div className="h-32 animate-pulse rounded-lg bg-canvas-2" />
|
||||
<div className="h-24 animate-pulse rounded-lg bg-canvas-2" />
|
||||
<div className="h-48 animate-pulse rounded-lg bg-canvas-2" />
|
||||
<div className="h-32 animate-pulse rounded-lg bg-surface" />
|
||||
<div className="h-24 animate-pulse rounded-lg bg-surface" />
|
||||
<div className="h-48 animate-pulse rounded-lg bg-surface" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -38,8 +38,8 @@ export function ProgressionPage() {
|
|||
return (
|
||||
<main className="mx-auto max-w-3xl space-y-6 px-4 py-6">
|
||||
<header className="space-y-1">
|
||||
<h1 className="text-lg font-semibold text-ink-1">Profil de préparation</h1>
|
||||
<p className="text-sm text-ink-3">
|
||||
<h1 className="text-lg font-semibold text-ink-primary">Profil de préparation</h1>
|
||||
<p className="text-sm text-ink-secondary">
|
||||
Repérez vos erreurs récurrentes et travaillez-les avec des exercices ciblés.
|
||||
</p>
|
||||
</header>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
})}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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, ' ')}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -59,15 +59,15 @@ function BlurredSection({
|
|||
}) {
|
||||
if (visible) return <>{children}</>
|
||||
return (
|
||||
<div className="relative min-h-[120px] overflow-hidden rounded-lg border border-line bg-canvas-2">
|
||||
<div className="relative min-h-[120px] overflow-hidden rounded-lg border border-border bg-surface">
|
||||
<div className="space-y-2 p-4 opacity-25 blur-sm" aria-hidden="true">
|
||||
{PLACEHOLDER_WIDTHS.map((w, i) => (
|
||||
<div key={i} className={`h-3 rounded bg-ink-4 ${w}`} />
|
||||
<div key={i} className={`h-3 rounded bg-surface-hover ${w}`} />
|
||||
))}
|
||||
</div>
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center gap-3 px-4">
|
||||
<Lock className="size-5 text-ink-4" aria-hidden="true" />
|
||||
<p className="text-sm font-medium text-ink-2">Disponible en Standard</p>
|
||||
<Lock className="size-5 text-ink-secondary" aria-hidden="true" />
|
||||
<p className="text-sm font-medium text-ink-primary">Disponible en Standard</p>
|
||||
<Button variant="upgrade" size="sm" onClick={onUpgrade}>
|
||||
Passer en Standard
|
||||
</Button>
|
||||
|
|
@ -81,10 +81,10 @@ function BlurredSection({
|
|||
function RapportSkeleton() {
|
||||
return (
|
||||
<div className="space-y-4" aria-busy="true" aria-label="Chargement du rapport…">
|
||||
<div className="h-40 animate-pulse rounded-lg bg-canvas-2" />
|
||||
<div className="h-28 animate-pulse rounded-lg bg-canvas-2" />
|
||||
<div className="h-32 animate-pulse rounded-lg bg-canvas-2" />
|
||||
<div className="h-48 animate-pulse rounded-lg bg-canvas-2" />
|
||||
<div className="h-40 animate-pulse rounded-lg bg-surface" />
|
||||
<div className="h-28 animate-pulse rounded-lg bg-surface" />
|
||||
<div className="h-32 animate-pulse rounded-lg bg-surface" />
|
||||
<div className="h-48 animate-pulse rounded-lg bg-surface" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -96,7 +96,7 @@ function CriteresSection({ rapport }: { rapport: Report }) {
|
|||
|
||||
return (
|
||||
<section aria-label="Détail par critère">
|
||||
<h2 className="mb-3 text-base font-semibold text-ink-1">Détail par critère</h2>
|
||||
<h2 className="mb-3 text-base font-semibold text-ink-primary">Détail par critère</h2>
|
||||
<div className="space-y-3">
|
||||
{rapport.criteres.map((c) => {
|
||||
const code = critereCodeFromNom(c.nom)
|
||||
|
|
@ -120,7 +120,9 @@ function ExercicesSection({
|
|||
if (rapport.exercices_status !== 'ready' || !rapport.exercices) {
|
||||
return (
|
||||
<section aria-label="Exercices personnalisés">
|
||||
<h2 className="mb-3 text-base font-semibold text-ink-1">Mes exercices personnalisés</h2>
|
||||
<h2 className="mb-3 text-base font-semibold text-ink-primary">
|
||||
Mes exercices personnalisés
|
||||
</h2>
|
||||
<JobStatusFallback
|
||||
status={rapport.exercices_status}
|
||||
pendingLabel="Génération des exercices en cours…"
|
||||
|
|
@ -134,7 +136,7 @@ function ExercicesSection({
|
|||
|
||||
return (
|
||||
<section aria-label="Exercices personnalisés">
|
||||
<h2 className="mb-3 text-base font-semibold text-ink-1">Mes exercices personnalisés</h2>
|
||||
<h2 className="mb-3 text-base font-semibold text-ink-primary">Mes exercices personnalisés</h2>
|
||||
<div className="space-y-3">
|
||||
{rapport.exercices.map((ex, i) => (
|
||||
<ExerciceInteractive key={`${ex.theme}-${i}`} exercice={ex} />
|
||||
|
|
@ -156,7 +158,9 @@ function ModeleSection({
|
|||
if (rapport.modele_status !== 'ready' || !rapport.modele) {
|
||||
return (
|
||||
<section aria-label="Production modèle">
|
||||
<h2 className="mb-3 text-base font-semibold text-ink-1">Version restructurée NCLC 9+</h2>
|
||||
<h2 className="mb-3 text-base font-semibold text-ink-primary">
|
||||
Version restructurée NCLC 9+
|
||||
</h2>
|
||||
<JobStatusFallback
|
||||
status={rapport.modele_status}
|
||||
pendingLabel="Production modèle en cours de génération…"
|
||||
|
|
@ -170,7 +174,9 @@ function ModeleSection({
|
|||
|
||||
return (
|
||||
<section aria-label="Production modèle">
|
||||
<h2 className="mb-3 text-base font-semibold text-ink-1">Version restructurée NCLC 9+</h2>
|
||||
<h2 className="mb-3 text-base font-semibold text-ink-primary">
|
||||
Version restructurée NCLC 9+
|
||||
</h2>
|
||||
<ProductionModeleSection modele={rapport.modele} />
|
||||
</section>
|
||||
)
|
||||
|
|
@ -209,16 +215,19 @@ export function RapportPage() {
|
|||
return (
|
||||
<main className="mx-auto max-w-3xl space-y-6 px-4 py-6">
|
||||
{/* Breadcrumb */}
|
||||
<nav aria-label="Fil d'Ariane" className="flex items-center gap-1.5 text-sm text-ink-4">
|
||||
<nav
|
||||
aria-label="Fil d'Ariane"
|
||||
className="flex items-center gap-1.5 text-sm text-ink-secondary"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={goToSimulations}
|
||||
className="transition-colors duration-150 hover:text-ink-2"
|
||||
className="transition-colors duration-150 hover:text-ink-primary"
|
||||
>
|
||||
Simulations
|
||||
</button>
|
||||
<span aria-hidden="true">›</span>
|
||||
<span aria-current="page" className="text-ink-2">
|
||||
<span aria-current="page" className="text-ink-primary">
|
||||
Rapport
|
||||
</span>
|
||||
</nav>
|
||||
|
|
@ -226,7 +235,7 @@ export function RapportPage() {
|
|||
{(isLoading || isPlanLoading) && <RapportSkeleton />}
|
||||
|
||||
{isInProgress && (
|
||||
<p className="text-center text-sm text-ink-4" aria-live="polite">
|
||||
<p className="text-center text-sm text-ink-secondary" aria-live="polite">
|
||||
Votre simulation est en cours.
|
||||
</p>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -17,10 +17,10 @@ import { SimulationForm } from '../components/SimulationForm'
|
|||
function SimulationSkeleton() {
|
||||
return (
|
||||
<div className="space-y-4" aria-busy="true" aria-label="Chargement…">
|
||||
<div className="h-6 w-40 animate-pulse rounded bg-canvas-2" />
|
||||
<div className="h-6 w-40 animate-pulse rounded bg-surface" />
|
||||
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3">
|
||||
{Array.from({ length: 6 }).map((_, i) => (
|
||||
<div key={i} className="h-24 animate-pulse rounded-lg bg-canvas-2" />
|
||||
<div key={i} className="h-24 animate-pulse rounded-lg bg-surface" />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ function SujetsSkeleton() {
|
|||
return (
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3" aria-busy="true">
|
||||
{Array.from({ length: 6 }).map((_, i) => (
|
||||
<div key={i} className="h-32 animate-pulse rounded-lg bg-canvas-2" />
|
||||
<div key={i} className="h-32 animate-pulse rounded-lg bg-surface" />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
|
|
@ -83,17 +83,17 @@ export function SujetsPage() {
|
|||
reset()
|
||||
navigate('/simulation/ee')
|
||||
}}
|
||||
className="text-sm text-ink-4 underline-offset-4 hover:text-ink-2 hover:underline"
|
||||
className="text-sm text-ink-secondary underline-offset-4 hover:text-ink-primary hover:underline"
|
||||
>
|
||||
← Retour
|
||||
</button>
|
||||
<h2 className="flex-1 text-lg font-semibold text-ink-1">
|
||||
<h2 className="flex-1 text-lg font-semibold text-ink-primary">
|
||||
Choisir un sujet — {formatTache(production.tache)}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
||||
<p className="text-sm text-ink-3">
|
||||
<p className="text-sm text-ink-secondary">
|
||||
{isLoading
|
||||
? 'Chargement des sujets…'
|
||||
: hasSujets
|
||||
|
|
@ -114,7 +114,7 @@ export function SujetsPage() {
|
|||
{isError && (
|
||||
<div
|
||||
role="alert"
|
||||
className="mb-4 rounded-md border border-danger/40 bg-danger-bg px-3 py-2 text-sm text-danger"
|
||||
className="mb-4 rounded-md border border-danger/40 bg-danger-soft px-3 py-2 text-sm text-danger"
|
||||
>
|
||||
Impossible de charger les sujets.{' '}
|
||||
<button type="button" onClick={() => refetch()} className="underline underline-offset-2">
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue