Frontend : intégration du design Claude Design (6 écrans)

Portage du projet « Bénévoles Festival.dc.html » en implémentation statique
pilotée par un jeu de démonstration déterministe (portage du générateur du
design : 15 postes, 64 bénévoles, ~209 affectations).

- Système visuel néo-brutaliste : palette + primitives Tailwind, polices
  Archivo / DM Mono, primitives dans components/ui.tsx.
- Domaine (aligné CLAUDE.md) : types, jours/plages, moteur de validation
  (mêmes codes BLOCKING/WARNING que le back-end), sélecteurs, différentiel
  de publication.
- État applicatif : PlanningProvider (reducer) — affecter / désaffecter /
  publier / confirmer / éditer besoins et bénévoles / import simulé / pointage.
- Écrans : 01 Grille des besoins + panneau d'affectation (candidats triés,
  alertes), 02 Vue par bénévole, 03 Publication, 04 Mobile bénévole,
  05 Mobile référent, 06 Configuration (4 sous-onglets).
- i18n : aucun texte d'interface en dur, locales/fr.json.
- Vérifs : build tsc + vite, eslint (0 erreur), rendu headless des 6 écrans
  (npm run smoke).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AwswqSChc3Rwtu61pRXiY2
This commit is contained in:
AdamThomas
2026-09-09 23:16:38 +02:00
co-authored by Claude Sonnet 5
parent 5154a3c61b
commit ac469f0cd1
35 changed files with 4267 additions and 125 deletions
+217
View File
@@ -0,0 +1,217 @@
import type { ButtonHTMLAttributes, ReactNode } from 'react';
import { cx } from '@/lib/cx';
// Primitives visuelles du design « Bénévoles Festival » : bordures franches,
// ombres portées dures, libellés capitales. Aucun texte en dur ici — les
// libellés sont passés par les écrans (via i18n).
export function Panel({
children,
className,
shadow = 'md',
}: {
children: ReactNode;
className?: string;
shadow?: 'none' | 'md' | 'lg';
}) {
return (
<div
className={cx(
'border-[3px] border-ink bg-paper',
shadow === 'md' && 'shadow-brutal-md',
shadow === 'lg' && 'shadow-brutal-lg',
className,
)}
>
{children}
</div>
);
}
export function PanelHeader({
children,
hatched = false,
className,
}: {
children: ReactNode;
hatched?: boolean;
className?: string;
}) {
return (
<div
className={cx(
'px-3 py-2 font-display text-[12px] font-black uppercase tracking-wide text-cream',
hatched ? 'bg-hatch-ink text-yellow' : 'bg-ink',
className,
)}
>
{children}
</div>
);
}
type BtnVariant = 'solid' | 'ghost' | 'danger' | 'dark';
export function Btn({
variant = 'solid',
className,
type = 'button',
...props
}: ButtonHTMLAttributes<HTMLButtonElement> & { variant?: BtnVariant }) {
return (
<button
type={type}
className={cx(
'inline-flex cursor-pointer items-center justify-center gap-2 border-2 border-ink px-3 py-2',
'font-display text-[11px] font-extrabold uppercase tracking-wide transition-colors',
'disabled:cursor-not-allowed disabled:opacity-50',
variant === 'solid' && 'bg-yellow text-ink shadow-brutal-sm hover:bg-green hover:text-white',
variant === 'ghost' && 'bg-cream text-ink hover:bg-yellow',
variant === 'danger' && 'bg-cream text-ink hover:bg-orange',
variant === 'dark' && 'bg-ink text-cream hover:bg-yellow hover:text-ink',
className,
)}
{...props}
/>
);
}
export function Chip({
active,
children,
onClick,
}: {
active: boolean;
children: ReactNode;
onClick: () => void;
}) {
return (
<button
type="button"
onClick={onClick}
className={cx(
'cursor-pointer border-2 border-ink px-[9px] py-[6px] font-display text-[10.5px] tracking-wide transition-colors',
active
? 'bg-ink font-extrabold text-cream'
: 'bg-cream font-semibold text-ink hover:bg-yellow',
)}
>
{children}
</button>
);
}
type BadgeTone = 'confirmed' | 'proposed' | 'success' | 'info' | 'warn' | 'danger' | 'neutral';
export function Badge({ tone, children }: { tone: BadgeTone; children: ReactNode }) {
return (
<span
className={cx(
'inline-flex items-center border-2 px-[6px] py-[3px] font-display text-[8.5px] font-extrabold uppercase tracking-wide',
tone === 'confirmed' && 'border-ink bg-green text-white',
tone === 'proposed' && 'border-ink border-dashed bg-paper text-ink',
tone === 'success' && 'border-ink bg-green text-white',
tone === 'info' && 'border-ink bg-blue text-ink',
tone === 'warn' && 'border-ink bg-yellow-soft text-ink',
tone === 'danger' && 'border-ink bg-orange text-ink',
tone === 'neutral' && 'border-ink bg-cream text-ink',
)}
>
{children}
</span>
);
}
export function Stepper({
value,
onDecrement,
onIncrement,
suffix,
}: {
value: ReactNode;
onDecrement: () => void;
onIncrement: () => void;
suffix?: string;
}) {
return (
<span className="inline-flex items-center gap-1">
<button
type="button"
onClick={onDecrement}
className="h-7 w-7 cursor-pointer border-2 border-ink bg-cream font-display text-[14px] font-extrabold hover:bg-orange"
>
–
</button>
<span className="min-w-[46px] text-center font-mono text-[16px] font-medium">
{value}
{suffix ? ` ${suffix}` : null}
</span>
<button
type="button"
onClick={onIncrement}
className="h-7 w-7 cursor-pointer border-2 border-ink bg-yellow font-display text-[14px] font-extrabold hover:bg-green hover:text-white"
>
+
</button>
</span>
);
}
export function StatCard({
value,
label,
tone = 'paper',
}: {
value: ReactNode;
label: string;
tone?: 'paper' | 'yellow' | 'green';
}) {
return (
<div
className={cx(
'border-[3px] border-ink px-3 py-[11px] shadow-brutal-lg',
tone === 'paper' && 'bg-paper',
tone === 'yellow' && 'bg-yellow',
tone === 'green' && 'bg-green text-white',
)}
>
<div className="font-mono text-[34px] font-black leading-none">{value}</div>
<div className="mt-1 font-display text-[9.5px] font-extrabold uppercase leading-tight tracking-wide">
{label}
</div>
</div>
);
}
export function FieldLabel({ children }: { children: ReactNode }) {
return (
<span className="mb-[5px] block font-display text-[9px] font-extrabold uppercase tracking-widest">
{children}
</span>
);
}
export function TextInput({
value,
onChange,
mono = false,
className,
}: {
value: string;
onChange: (value: string) => void;
mono?: boolean;
className?: string;
}) {
return (
<input
value={value}
onChange={(e) => onChange(e.target.value)}
className={cx(
'w-full border-2 border-ink bg-cream p-[9px] text-[13px]',
mono ? 'font-mono' : 'font-display font-bold',
className,
)}
/>
);
}