feat(shared): ThemeToggle + Logo + design system rules (Sprint 0.5 étape 8)

This commit is contained in:
Hermann_Kitio 2026-04-18 01:33:24 +03:00
parent 7dfd0df6b3
commit ee6d679950
4 changed files with 120 additions and 18 deletions

View file

@ -143,6 +143,24 @@ Voir `SECURITY.md` pour le détail.
Claude Code ne crée jamais de worktree Git. Claude Code ne crée jamais de worktree Git.
Toutes les modifications se font directement dans le dossier du projet principal. Toutes les modifications se font directement dans le dossier du projet principal.
### Règle L — Tokens du design system, jamais de valeurs brutes
Toutes les couleurs dans le JSX passent exclusivement par les tokens Direction H :
- Utilitaires Tailwind : `bg-canvas`, `text-ink-2`, `border-line`, `bg-expria`, `text-danger`, etc.
- Jamais de classes couleur Tailwind par défaut : `bg-slate-100`, `text-gray-500`, `blue-600`
- Jamais de valeurs inline brutes : `#1B4FD8`, `oklch(…)`, `rgb(…)` dans les className ou style
- Pour les inline styles dynamiques uniquement : `style={{ background: 'var(--color-expria)' }}`
- Tout nouveau token est ajouté exclusivement dans `@theme {}` (et `.dark {}`) de `src/index.css`
```tsx
// ❌ JAMAIS
<div className="bg-blue-600 text-gray-500" />
<div style={{ color: '#1B4FD8' }} />
// ✅ TOUJOURS
<div className="bg-expria text-ink-3" />
<div style={{ color: 'var(--color-expria)' }} /> {/* inline style dynamique uniquement */}
```
--- ---
## 3. Structure du code — conventions ## 3. Structure du code — conventions
@ -461,3 +479,4 @@ Avant chaque session Claude Code, vérifier :
| Version | Date | Changements | | Version | Date | Changements |
|---|---|---| |---|---|---|
| 1.0 | 2026-04-17 | Création, adaptée de la version backend | | 1.0 | 2026-04-17 | Création, adaptée de la version backend |
| 1.1 | 2026-04-18 | Ajout Règle L — tokens du design system (Sprint 0.5) |

View file

@ -123,30 +123,41 @@ Vient du pattern `c.json(result, result.status)` où `result` contient déjà `s
--- ---
### FTD-11 — `@theme` Tailwind 4 non défini ### FTD-14 — Anti-FOUC thème : script inline manquant dans `<head>`
**Priorité :** 🟢 Mineur **Priorité :** 🟡 Important
**Statut :** Ouvert — à faire dans session design system **Statut :** Ouvert — à faire avant déploiement production
**Estimation de session :** 1 jour (palette + typo + itérations design) **Estimation de session :** 30 min
**Description :** `src/index.css` a été nettoyé à l'étape 11 du Sprint 0 et réduit à la seule ligne `@import 'tailwindcss';`. L'ADR 006 (§Configuration Tailwind 4) décrit le bloc `@theme { ... }` comme le mécanisme officiel de configuration Tailwind 4 (CSS-first) : **Description :** Le `ThemeProvider` applique la classe `.dark` sur `<html>` après l'hydratation React (`useEffect`). Entre le premier paint du navigateur et l'exécution de React, la page s'affiche brièvement en mode clair même si l'utilisateur a choisi le mode sombre — c'est le FOUC (Flash Of Unstyled Content).
```css **Fix :** ajouter un script inline bloquant dans le `<head>` de `index.html` qui lit `localStorage.getItem('expria-theme')` (et `prefers-color-scheme` en fallback) et applique `.dark` sur `document.documentElement` avant le premier paint. Ce script doit être minifié et inliné (non-async, non-defer) pour garantir l'exécution avant le CSS.
@theme {
--color-primary: #1B4FD8; ```html
--font-sans: 'Plus Jakarta Sans', system-ui, sans-serif; <script>
} (function(){var t=localStorage.getItem('expria-theme');
if(t==='dark'||(t!=='light'&&matchMedia('(prefers-color-scheme:dark)').matches))
document.documentElement.classList.add('dark')})()
</script>
``` ```
La palette brand Expria et la typographie ne sont pas encore décidées, donc `@theme` est volontairement absent pour ne pas poser de valeurs placeholder qu'il faudrait repasser plus tard. **Impact actuel :** visible uniquement pour les utilisateurs en mode sombre — bref flash de fond clair au chargement. Acceptable en dev, indésirable en production.
**Impact actuel :** les composants utilisent les couleurs Tailwind par défaut (`bg-slate-*`, `text-gray-*`). Visuellement cohérent mais pas brand. **Condition de résolution :** avant la première mise en production (Sprint 1 ou avant).
**À faire au Sprint 1 (design system) :** ---
- Définir la palette brand Expria (primary, secondary, neutrals, danger, success)
- Choisir + installer la typo (Plus Jakarta Sans ou autre) via `<link>` dans `index.html` ou `@import` dans `index.css`
- Ajouter le bloc `@theme` dans `src/index.css`
- Mettre à jour ADR 006 avec les valeurs retenues
**Condition de résolution :** Sprint 1 — session dédiée au design system. ### FTD-15 — Option `'system'` manquante dans ThemeProvider
**Priorité :** 🟢 Mineur
**Statut :** Reporté — après MVP
**Estimation de session :** 2h
**Description :** Le `ThemeProvider` est bi-state (`'light' | 'dark'`). L'option `'system'` (qui suit `prefers-color-scheme` en temps réel via `MediaQueryList.addEventListener`) a été volontairement différée (décision Sprint 0.5).
**À faire :**
- Étendre le type `Theme` à `'light' | 'dark' | 'system'`
- Dans `ThemeProvider`, si `theme === 'system'` : écouter `matchMedia('(prefers-color-scheme: dark)')` et appliquer/retirer `.dark` dynamiquement
- `ThemeToggle` : cycle light → dark → system (ou un sélecteur 3 états)
- Mettre à jour `getInitialTheme()` pour retourner `'system'` si aucune préférence stockée
**Condition de résolution :** après MVP — confort utilisateur, pas bloquant.
--- ---
@ -244,6 +255,7 @@ La palette brand Expria et la typographie ne sont pas encore décidées, donc `@
| ID | Description | Résolu le | Comment | | ID | Description | Résolu le | Comment |
|---|---|---|---| |---|---|---|---|
| FTD-11 | `@theme` Tailwind 4 non défini — palette et typographie absentes | 2026-04-18 | Résolu au Sprint 0.5 (design system). Palette Direction H complète (canvas/surface/ink/expria/deep/semantic) + typo Plus Jakarta Sans définis dans `src/index.css` via `@theme {}` et `.dark {}`. shadcn/ui remappé sur ces tokens. Règle L ajoutée dans `DEVELOPMENT_PRINCIPLES.md` pour garantir l'usage exclusif des tokens. |
| FTD-13 | Incompatibilité Vitest 3 / Vite 8 (conflit de types `Plugin<any>` entre le Vite 8 top-level avec Rolldown et le Vite 7 pinné de Vitest 3.2.4 ; `npm run build` cassé) | 2026-04-17 | Résolu par upgrade Vitest `3.2.4 → 4.1.4` (et `@vitest/coverage-v8` idem) à l'étape 12-bis du Sprint 0. Vitest 4.x supporte nativement Vite 8 Rolldown. Correctif complémentaire : script `typecheck` passé de `tsc --noEmit -p tsconfig.app.json` à `tsc -b --noEmit` pour couvrir aussi `tsconfig.node.json` (d'où `vite.config.ts`) et éviter qu'un bug similaire échappe à la CI. | | FTD-13 | Incompatibilité Vitest 3 / Vite 8 (conflit de types `Plugin<any>` entre le Vite 8 top-level avec Rolldown et le Vite 7 pinné de Vitest 3.2.4 ; `npm run build` cassé) | 2026-04-17 | Résolu par upgrade Vitest `3.2.4 → 4.1.4` (et `@vitest/coverage-v8` idem) à l'étape 12-bis du Sprint 0. Vitest 4.x supporte nativement Vite 8 Rolldown. Correctif complémentaire : script `typecheck` passé de `tsc --noEmit -p tsconfig.app.json` à `tsc -b --noEmit` pour couvrir aussi `tsconfig.node.json` (d'où `vite.config.ts`) et éviter qu'un bug similaire échappe à la CI. |
--- ---
@ -255,3 +267,4 @@ La palette brand Expria et la typographie ne sont pas encore décidées, donc `@
| 1.0 | 2026-04-17 | Création initiale avec 9 FTD identifiées depuis l'audit backend et les décisions d'architecture | | 1.0 | 2026-04-17 | Création initiale avec 9 FTD identifiées depuis l'audit backend et les décisions d'architecture |
| 1.1 | 2026-04-17 | Ajout FTD-10 (Semgrep CI), FTD-11 (`@theme` Tailwind 4), FTD-12 (tests `api-client`) suite à l'étape 11 du Sprint 0 | | 1.1 | 2026-04-17 | Ajout FTD-10 (Semgrep CI), FTD-11 (`@theme` Tailwind 4), FTD-12 (tests `api-client`) suite à l'étape 11 du Sprint 0 |
| 1.2 | 2026-04-17 | Ajout FTD-13 résolu (incompatibilité Vitest 3 / Vite 8) suite à l'étape 12-bis du Sprint 0 | | 1.2 | 2026-04-17 | Ajout FTD-13 résolu (incompatibilité Vitest 3 / Vite 8) suite à l'étape 12-bis du Sprint 0 |
| 1.3 | 2026-04-18 | FTD-11 résolu (design system Sprint 0.5) ; ajout FTD-14 (anti-FOUC), FTD-15 (option 'system' thème) |

View file

@ -0,0 +1,46 @@
import { cn } from '@/shared/lib/utils'
type LogoSize = 'sm' | 'md'
type LogoVariant = 'icon' | 'full'
interface LogoProps {
size?: LogoSize
variant?: LogoVariant
className?: string
}
const markStyles: Record<LogoSize, string> = {
sm: 'size-6 text-[11px]',
md: 'size-8 text-[13px]',
}
const wordmarkStyles: Record<LogoSize, string> = {
sm: 'text-sm',
md: 'text-base',
}
export function Logo({ size = 'md', variant = 'full', className }: LogoProps) {
return (
<div
className={cn(
'inline-flex items-center gap-2.5 font-bold tracking-tight text-ink-1',
className
)}
role={variant === 'icon' ? 'img' : undefined}
aria-label={variant === 'icon' ? 'Expria' : undefined}
>
<span
className={cn(
'flex shrink-0 items-center justify-center rounded-sm bg-expria font-bold tracking-tight text-white',
markStyles[size]
)}
aria-hidden="true"
>
EX
</span>
{variant === 'full' && (
<span className={cn('leading-none', wordmarkStyles[size])}>Expria</span>
)}
</div>
)
}

View file

@ -0,0 +1,24 @@
import { Moon, Sun } from 'lucide-react'
import { useTheme } from '@/shared/hooks/useTheme'
import { Button } from '@/shared/components/ui/button'
interface ThemeToggleProps {
className?: string
}
export function ThemeToggle({ className }: ThemeToggleProps) {
const { theme, setTheme } = useTheme()
const isDark = theme === 'dark'
return (
<Button
variant="ghost"
size="icon"
className={className}
aria-label={isDark ? 'Passer en mode clair' : 'Passer en mode sombre'}
onClick={() => setTheme(isDark ? 'light' : 'dark')}
>
{isDark ? <Sun className="size-4" /> : <Moon className="size-4" />}
</Button>
)
}