From 41d2eec3f71c42af89725989fd453d9ffc74f1cb Mon Sep 17 00:00:00 2001 From: Hermann_Kitio Date: Tue, 21 Apr 2026 00:18:33 +0300 Subject: [PATCH] feat(simulations): composants TimerDisplay + WordCountBar --- .../simulations/components/TimerDisplay.tsx | 45 +++++++++++++ .../simulations/components/WordCountBar.tsx | 67 +++++++++++++++++++ 2 files changed, 112 insertions(+) create mode 100644 src/features/simulations/components/TimerDisplay.tsx create mode 100644 src/features/simulations/components/WordCountBar.tsx diff --git a/src/features/simulations/components/TimerDisplay.tsx b/src/features/simulations/components/TimerDisplay.tsx new file mode 100644 index 0000000..36b30d7 --- /dev/null +++ b/src/features/simulations/components/TimerDisplay.tsx @@ -0,0 +1,45 @@ +/** + * Affichage du minuteur de simulation au format MM:SS. + * + * Règle H : purement présentationnel, reçoit secondesRestantes et isExpired par props. + * Règle L : tokens Direction H uniquement (ink-*, danger). + * + * États visuels : + * - Normal : ink-2 + * - Critique (< 120s) : danger + pulse (motion-safe uniquement — prefers-reduced-motion respecté) + * - Expiré : danger + bold + * + * A11y : role="timer" + aria-live="polite" pour annoncer les changements critiques. + */ + +import { formatTimer } from '../lib/simulationConfig' + +interface Props { + secondesRestantes: number + isExpired: boolean +} + +const SEUIL_CRITIQUE_SECONDES = 120 + +export function TimerDisplay({ secondesRestantes, isExpired }: Props) { + const isCritique = !isExpired && secondesRestantes < SEUIL_CRITIQUE_SECONDES + + const base = 'tabular-nums text-sm font-medium' + const tone = isExpired + ? 'text-danger font-bold' + : isCritique + ? 'text-danger motion-safe:animate-pulse' + : 'text-ink-2' + + return ( + + {formatTimer(secondesRestantes)} + + ) +} diff --git a/src/features/simulations/components/WordCountBar.tsx b/src/features/simulations/components/WordCountBar.tsx new file mode 100644 index 0000000..9c2623e --- /dev/null +++ b/src/features/simulations/components/WordCountBar.tsx @@ -0,0 +1,67 @@ +/** + * Barre de progression du nombre de mots avec compteur contextuel. + * + * Règle H : purement présentationnel, reçoit count et config par props. + * Règle L : tokens Direction H uniquement (success, warning, danger, ink-*, line). + * + * Couleurs selon le nombre de mots : + * - < motsCibleMin : warning (orange) — en dessous de la cible + * - motsCibleMin ≤ count ≤ motsCibleMax : success (vert) — dans la cible + * - > motsCibleMax : danger (rouge) — au-dessus de la cible + * + * La barre est graduée par rapport à motsCibleMax (100% = borne haute cible). + * Au-dessus on reste à 100% de remplissage en rouge. + */ + +import type { SimulationConfig } from '../lib/simulationConfig' + +interface Props { + count: number + config: SimulationConfig +} + +type Tone = 'warning' | 'success' | 'danger' + +function computeTone(count: number, config: SimulationConfig): Tone { + if (count > config.motsCibleMax) return 'danger' + if (count >= config.motsCibleMin) return 'success' + return 'warning' +} + +const TONE_CLASSES: Record = { + warning: { bar: 'bg-warning', text: 'text-warning' }, + success: { bar: 'bg-success', text: 'text-success' }, + danger: { bar: 'bg-danger', text: 'text-danger' }, +} + +export function WordCountBar({ count, config }: Props) { + const tone = computeTone(count, config) + const classes = TONE_CLASSES[tone] + const pct = Math.min(100, Math.round((count / config.motsCibleMax) * 100)) + + return ( +
+
+
+
+
+ + {count.toLocaleString('fr-FR')} mot{count > 1 ? 's' : ''} + + + cible {config.motsCibleMin}–{config.motsCibleMax} mots + +
+
+ ) +}