Files
benevoles/frontend/src/components/ui.tsx
T
AdamThomasandClaude Sonnet 5 fb6e26eddb Front : les indications d'usage passent en bulle d'aide (icône « ? »)
Les explications sur comment utiliser un écran (grille des besoins, vue
bénévoles, artistes, import, disponibilités, créneaux types, seuils,
notification par email…) étaient affichées en permanence dans la page,
prenant de la place pour un contenu qu'on ne lit qu'une fois. Elles passent
dans une bulle affichée au survol (ou au focus clavier/tactile) d'un petit
repère « ? » posé à côté du titre ou du champ concerné.

- `HelpTip` (frontend/src/components/ui.tsx) : nouveau composant partagé,
  cohérent avec le design (bordures franches, pas d'arrondi) ; `side`
  ('top'/'bottom') pour éviter que la bulle ne sorte du cadre visible selon
  que le repère est en haut ou en bas de son conteneur.
- Appliqué à GrillePage, VolunteerViewPage, ConfigArtists, ConfigVolunteers
  (import, ajout manuel, disponibilités), ConfigGrid, ConfigRules, ConfigMail.
- Aucun texte déplacé n'a changé : mêmes clés i18n, seul l'endroit où elles
  s'affichent change.

Laissé en l'état (pas des indications d'usage mais des messages d'état) :
bannières identifiants de démo, liens de connexion en dev, états vides, et
la description des rôles (contenu propre des cartes, pas un mode d'emploi).

Vérifié : typecheck, lint, build, smoke (7 routes) OK.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017uUsRg7XzopkCPWimwg9ZM
2026-09-15 20:03:41 +02:00

251 lines
6.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 = '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 === '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>
);
}
/**
* Petit repère « ? » : au survol (ou au focus clavier/tactile), affiche
* `text` dans une bulle. Sert à sortir les indications d'usage du flux de la
* page plutôt que de les y afficher en permanence.
*/
export function HelpTip({
text,
side = 'top',
className,
}: {
text: string;
side?: 'top' | 'bottom';
className?: string;
}) {
return (
<span className={cx('group relative inline-flex align-middle', className)}>
<button
type="button"
className="flex h-[15px] w-[15px] flex-none cursor-help items-center justify-center border-[1.5px] border-ink bg-cream font-mono text-[9px] font-bold leading-none text-ink hover:bg-yellow focus-visible:bg-yellow focus-visible:outline-none"
aria-label={text}
>
?
</button>
<span
className={cx(
'pointer-events-none absolute left-1/2 z-20 w-max max-w-[260px] -translate-x-1/2 border-2 border-ink bg-ink px-[9px] py-[7px] font-mono text-[10px] font-normal normal-case leading-relaxed tracking-normal text-cream opacity-0 shadow-brutal-md transition-opacity duration-100 group-hover:opacity-100 group-focus-within:opacity-100',
side === 'top' ? 'bottom-full mb-[6px]' : 'top-full mt-[6px]',
)}
>
{text}
</span>
</span>
);
}
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,
)}
/>
);
}