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.
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
@ -461,3 +479,4 @@ Avant chaque session Claude Code, vérifier :
| Version | Date | Changements |
|---|---|---|
| 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
**Priorité :** 🟢 Mineur
**Statut :** Ouvert — à faire dans session design system
**Estimation de session :** 1 jour (palette + typo + itérations design)
**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) :
### FTD-14 — Anti-FOUC thème : script inline manquant dans `<head>`
**Priorité :** 🟡 Important
**Statut :** Ouvert — à faire avant déploiement production
**Estimation de session :** 30 min
**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
@theme {
--color-primary: #1B4FD8;
--font-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
}
**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.
```html
<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 |
|---|---|---|---|
| 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. |
---
@ -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.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.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>
)
}