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:
co-authored by
Claude Sonnet 5
parent
5154a3c61b
commit
ac469f0cd1
@@ -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,
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user