refactor(simulation-ee): Sprint 3.5 clean — FTD-17/18/19 résolus, factorisation SimulationForm

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Hermann_Kitio 2026-04-22 14:03:46 +03:00
parent 385b29679e
commit 18f92098cb
11 changed files with 36 additions and 66 deletions

View file

@ -0,0 +1,13 @@
/**
* Clés TanStack Query partagées pour le domaine `user`.
*
* Source unique importée par `features/dashboard/hooks/usePlan`,
* `features/simulations/pages/SimulationPage`, `features/simulations/pages/RapportPage`,
* et tout futur consommateur du statut de plan. Une clé locale inline briserait
* silencieusement le cache partagé de TanStack Query à la moindre faute de frappe.
*
* Ce module ne contient que des constantes pures aucun import React, TanStack
* ou autre dépendance runtime.
*/
export const PLAN_QUERY_KEY = ['plan'] as const

View file

@ -12,8 +12,9 @@
import { useQuery } from '@tanstack/react-query'
import { getPlanStatus } from '@/entities/user/api'
import { PLAN_QUERY_KEY } from '@/entities/user/query-keys'
export const PLAN_QUERY_KEY = ['plan'] as const
export { PLAN_QUERY_KEY }
export function usePlan() {
return useQuery({

View file

@ -13,7 +13,7 @@
import { useEffect, useRef, useState, type FormEvent } from 'react'
import { Clock, Lightbulb, Loader2, Shuffle } from 'lucide-react'
import { z } from 'zod'
import { Button } from '@/shared/components/ui/button'
import { Button } from '@/shared/ui/Button'
import { formatTache } from '@/entities/production/lib'
import { hasAccess, type Plan } from '@/entities/user/lib'
import type { SujetData, Tache } from '@/entities/production/types'
@ -32,6 +32,9 @@ import { IdeesSuggestions } from './IdeesSuggestions'
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'
const textSchema = z.object({
texte: z
.string()
@ -212,7 +215,7 @@ export function SimulationForm({
onClick={handleIdeesClick}
disabled={ideesDisabled}
title={ideesTitle}
className="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:ring-2 focus:ring-expria/20 disabled:cursor-not-allowed disabled:opacity-50"
className={secondaryActionBtn}
aria-label="Obtenir des suggestions d'idées"
>
<Lightbulb className="size-4" aria-hidden="true" />
@ -222,7 +225,7 @@ export function SimulationForm({
type="button"
onClick={onChangeSujet}
disabled={isSubmitting}
className="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:ring-2 focus:ring-expria/20 disabled:cursor-not-allowed disabled:opacity-50"
className={secondaryActionBtn}
aria-label="Changer de sujet"
>
<Shuffle className="size-4" aria-hidden="true" />
@ -302,7 +305,7 @@ 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:ring-2 focus:ring-expria/20 disabled:cursor-not-allowed disabled:opacity-50"
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"
/>
<WordCountBar count={wordCount} config={config} />
{autosave.savedAt && !fieldError && (

View file

@ -37,7 +37,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:ring-2 focus:ring-expria/20 disabled:cursor-not-allowed disabled:opacity-50"
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"
>
{char}
</button>

View file

@ -5,7 +5,6 @@
* staleTime Infinity : un rapport ne change jamais après correction.
*
* Règle H : aucune logique métier expose les données brutes.
* FTD-17 : queryKey ['plan'] déjà utilisé dans SimulationPage ['rapport', id] est distinct.
*/
import { useQuery } from '@tanstack/react-query'

View file

@ -14,9 +14,8 @@
import { useEffect } from 'react'
import ReactMarkdown from 'react-markdown'
import { Link, useNavigate, useParams } from 'react-router-dom'
import { useQuery } from '@tanstack/react-query'
import { Lock } from 'lucide-react'
import { getPlanStatus } from '@/entities/user/api'
import { usePlan } from '@/features/dashboard/hooks/usePlan'
import { isSectionVisible } from '@/entities/report/lib'
import { useRapport } from '../hooks/useRapport'
import { Card } from '@/shared/ui/Card'
@ -130,11 +129,7 @@ export function RapportPage() {
data: planData,
isLoading: isPlanLoading,
isError: isPlanError,
} = useQuery({
queryKey: ['plan'],
queryFn: getPlanStatus,
staleTime: 5 * 60 * 1000,
})
} = usePlan()
const onUpgrade = () => navigate('/plan')

View file

@ -6,15 +6,11 @@
*
* Règle D : quotas et permissions passent par canSimulate() jamais de plan === '...'
* Règle H : aucune logique métier tout est dans useSimulation() et les entités.
*
* Nota : queryKey ['plan'] est dupliqué depuis features/dashboard/hooks/usePlan.
* TanStack Query partage le cache par clé FTD-17.
*/
import { useEffect } from 'react'
import { useNavigate } from 'react-router-dom'
import { useQuery } from '@tanstack/react-query'
import { getPlanStatus } from '@/entities/user/api'
import { usePlan } from '@/features/dashboard/hooks/usePlan'
import { Button } from '@/shared/ui/Button'
import { useSimulation } from '../hooks/useSimulation'
import { TaskSelector } from '../components/TaskSelector'
@ -41,11 +37,7 @@ export function SimulationPage() {
isLoading: isPlanLoading,
isError: isPlanError,
refetch: refetchPlan,
} = useQuery({
queryKey: ['plan'],
queryFn: getPlanStatus,
staleTime: 5 * 60 * 1000,
})
} = usePlan()
const {
step,

View file

@ -55,6 +55,7 @@
--shadow-sm: 0 1px 2px rgba(15, 23, 42, 0.04);
--shadow-md: 0 4px 12px rgba(15, 23, 42, 0.06), 0 1px 3px rgba(15, 23, 42, 0.04);
--shadow-lg: 0 12px 28px rgba(15, 23, 42, 0.08), 0 2px 6px rgba(15, 23, 42, 0.04);
--shadow-focus: 0 0 0 3px rgba(27, 79, 216, 0.18);
}
/* ─── Dark mode — override des tokens couleur et ombres ──────────── */
@ -94,6 +95,7 @@
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3);
--shadow-md: 0 4px 16px rgba(0, 0, 0, 0.4);
--shadow-lg: 0 12px 32px rgba(0, 0, 0, 0.5);
--shadow-focus: 0 0 0 3px rgba(91, 127, 255, 0.32);
}
/* ─── Rendu sub-pixel global (non couvert par Tailwind) ──────────── */

View file

@ -58,7 +58,7 @@ export function Button({
className={cn(
// base
'inline-flex cursor-pointer items-center justify-center font-medium transition-colors duration-150',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-expria/20',
'focus-visible:outline-none focus-visible:shadow-focus',
'disabled:cursor-not-allowed disabled:opacity-50',
variantClasses[variant],
sizeClasses[size],

View file

@ -35,7 +35,7 @@ const variantClasses: Record<CardVariant, string> = {
default : 'shadow-sm',
raised : 'shadow-md',
interactive:
'shadow-sm cursor-pointer transition-colors duration-150 hover:border-expria hover:bg-surface-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-expria/20',
'shadow-sm cursor-pointer transition-colors duration-150 hover:border-expria hover:bg-surface-hover focus-visible:outline-none focus-visible:shadow-focus',
}
export function Card({ variant = 'default', className, children, onClick }: CardProps) {