feat(simulations): finaliser flux /sujets — SimulationForm + SujetDisplay + TaskSelector type prop

- SimulationForm : bouton "Changer de sujet" → /sujets (étape 3 refonte)
- SujetDisplay : redevient présentationnel (plus de dropdown)
- TaskSelector : prop type 'EE' | 'EO' (EO_CARDS réservé usage futur — non routé)
- SimulationPage : type='EE' hardcodé (EO restera ComingSoon jusqu'au Sprint EO)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Hermann_Kitio 2026-04-21 02:48:48 +03:00
parent 43f3ce2c6c
commit 6bfdf15db9
4 changed files with 69 additions and 116 deletions

View file

@ -1,33 +1,22 @@
/**
* Affichage du sujet d'examen (consigne + documents) avec sélecteur intégré.
* Affichage du sujet d'examen (consigne + documents) purement présentationnel.
*
* - `sujet` : le sujet actuellement affiché (null = rien à rendre)
* - `sujets` : catalogue complet pour le dropdown + le tirage aléatoire
* - `onChangeSujet` : appelé avec le nouveau sujet choisi (dropdown ou random)
* Depuis la refonte /sujets (2026-04-21), le choix du sujet se fait sur une
* page dédiée (SujetsPage). Ce composant n'affiche que le sujet sélectionné.
*
* Règle H : purement présentationnel la liste et le callback viennent du parent.
* Règle L : tokens Direction H exclusivement (canvas, surface, ink-*, line, expria).
* Le contenu est admin-curé (pas du texte IA) plain-text avec whitespace-pre-wrap,
* pas de react-markdown.
*
* Le contenu des sujets est admin-curé (pas du texte IA) plain-text avec
* `whitespace-pre-wrap`, pas de react-markdown.
* Règle H : purement présentationnel le sujet vient du parent.
* Règle L : tokens Direction H exclusivement.
*/
import { Shuffle } from 'lucide-react'
import { Badge } from '@/shared/ui/Badge'
import { Card } from '@/shared/ui/Card'
import type { SujetData } from '@/entities/production/types'
interface Props {
sujet: SujetData | null
sujets: SujetData[]
isLoadingSujets: boolean
onChangeSujet: (sujet: SujetData) => void
disabled?: boolean
}
function truncate(s: string, max: number): string {
if (s.length <= max) return s
return `${s.slice(0, max).trimEnd()}`
}
function DocumentBlock({ titre, texte }: { titre: string | null; texte: string | null }) {
@ -42,67 +31,12 @@ function DocumentBlock({ titre, texte }: { titre: string | null; texte: string |
)
}
export function SujetDisplay({
sujet,
sujets,
isLoadingSujets,
onChangeSujet,
disabled = false,
}: Props) {
export function SujetDisplay({ sujet }: Props) {
if (!sujet) return null
const hasCatalog = sujets.length > 0
const canRandomize = hasCatalog && sujets.length > 1
function handleSelectChange(e: React.ChangeEvent<HTMLSelectElement>) {
const next = sujets.find((s) => s.id === e.target.value)
if (next && next.id !== sujet?.id) onChangeSujet(next)
}
function handleRandom() {
if (sujets.length === 0) return
const others = sujets.length > 1 ? sujets.filter((s) => s.id !== sujet?.id) : sujets
const pick = others[Math.floor(Math.random() * others.length)]
if (pick) onChangeSujet(pick)
}
return (
<Card variant="default" className="p-5">
<div className="space-y-4">
{(hasCatalog || isLoadingSujets) && (
<div className="flex flex-col gap-2 sm:flex-row sm:items-center">
<label htmlFor="sujet-select" className="text-xs font-semibold uppercase tracking-wide text-ink-4">
Sujet
</label>
<select
id="sujet-select"
value={sujet.id}
onChange={handleSelectChange}
disabled={disabled || isLoadingSujets || !hasCatalog}
className="min-w-0 flex-1 truncate rounded-md border border-line bg-surface px-3 py-1.5 text-sm text-ink-1 focus:border-expria focus:outline-none focus:ring-2 focus:ring-expria/20 disabled:cursor-not-allowed disabled:opacity-50"
>
{isLoadingSujets && !hasCatalog && (
<option value={sujet.id}>Chargement</option>
)}
{sujets.map((s) => (
<option key={s.id} value={s.id}>
{truncate(s.consigne.replace(/\s+/g, ' '), 80)}
</option>
))}
</select>
<button
type="button"
onClick={handleRandom}
disabled={disabled || isLoadingSujets || !canRandomize}
className="inline-flex items-center justify-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:ring-2 focus:ring-expria/20 disabled:cursor-not-allowed disabled:opacity-50"
aria-label="Tirer un sujet aléatoire"
>
<Shuffle className="size-4" aria-hidden="true" />
Changer de sujet
</button>
</div>
)}
{sujet.role && (
<div className="flex items-center gap-2">
<Badge variant="neutral">Rôle</Badge>