Responsive mobile / tablette / desktop

En-tête (onglets sur une seconde ligne en tablette, menu déroulant en
mobile), panneau d'affectation et fiche bénévole en tiroir par-dessus sous
lg: (plein écran en mobile), colonne des postes de la grille rétrécie,
liste des bénévoles réduite en mobile, écran « Postes & créneaux » empilé,
aperçu de l'éditeur de badges mis à l'échelle (glisser-déposer tactile),
ligne des types de paiement repliée, marges réduites — desktop inchangé.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
AdamThomas
2026-10-27 18:05:16 +01:00
co-authored by Claude Opus 5.5
parent 0f904f5e3d
commit 85e841ae4a
13 changed files with 269 additions and 129 deletions
+1 -1
View File
@@ -75,7 +75,7 @@ export function ConfigArtists() {
</div>
</div>
<div className="grid items-start gap-4 [grid-template-columns:repeat(auto-fill,minmax(320px,1fr))]">
<div className="grid items-start gap-4 [grid-template-columns:repeat(auto-fill,minmax(min(320px,100%),1fr))]">
{days.map((date) => (
<DayColumn key={date} date={date} artists={byDate.get(date) ?? []} />
))}
+9 -8
View File
@@ -109,8 +109,9 @@ export function ConfigGrid() {
].sort();
return (
<div className="flex min-h-0 flex-1 items-stretch">
<div className="max-h-[calc(100vh-118px)] w-[250px] flex-none overflow-auto border-r-[3px] border-ink">
<div className="flex min-h-0 flex-1 flex-col items-stretch md:flex-row">
{/* Mobile : liste des postes au-dessus du détail, hauteur bornée. */}
<div className="max-h-[260px] w-full flex-none overflow-auto border-b-[3px] border-ink md:max-h-[calc(100vh-118px)] md:w-[250px] md:border-b-0 md:border-r-[3px]">
<div className="sticky top-0 z-[2] flex items-center gap-[6px] bg-ink px-[10px] py-[7px] font-display text-[10px] font-extrabold uppercase tracking-wider text-cream">
{t('config.grid.positionsHeader', { count: totals.count })}
<HelpTip text={t('config.grid.note')} side="bottom" />
@@ -148,7 +149,7 @@ export function ConfigGrid() {
{createPosition.isError ? <ErrorNote error={createPosition.error} /> : null}
</div>
<div className="max-h-[calc(100vh-118px)] min-w-0 flex-1 overflow-auto p-4">
<div className="min-w-0 flex-1 p-3 sm:p-4 md:max-h-[calc(100vh-118px)] md:overflow-auto">
<PositionHeader
key={position.id}
position={position}
@@ -158,7 +159,7 @@ export function ConfigGrid() {
{positionShifts.isLoading ? <Loading /> : null}
{updateShift.isError ? <ErrorNote error={updateShift.error} /> : null}
<div className="grid gap-[14px] [grid-template-columns:repeat(auto-fit,minmax(300px,1fr))]">
<div className="grid gap-[14px] [grid-template-columns:repeat(auto-fit,minmax(min(300px,100%),1fr))]">
{days.map((day) => (
<DayCard
key={day}
@@ -197,15 +198,15 @@ function PositionHeader({
return (
<div className="mb-[14px] flex flex-wrap items-end gap-[14px]">
<div className="flex flex-col gap-1">
<div className="flex w-full min-w-0 flex-col gap-1 sm:w-auto">
<input
value={name}
onChange={(e) => setName(e.target.value)}
onBlur={commitName}
onKeyDown={(e) => e.key === 'Enter' && e.currentTarget.blur()}
className="border-2 border-ink bg-yellow px-2 py-1 font-display text-[22px] font-black tracking-tight"
className="w-full min-w-0 border-2 border-ink bg-yellow px-2 py-1 font-display text-[20px] font-black tracking-tight sm:text-[22px]"
/>
<div className="flex items-center gap-2">
<div className="flex flex-wrap items-center gap-2">
<select
value={position.category}
onChange={(e) =>
@@ -250,7 +251,7 @@ function PositionHeader({
{updatePosition.isError ? <ErrorNote error={updatePosition.error} /> : null}
{generate.isError ? <ErrorNote error={generate.error} /> : null}
</div>
<div className="ml-auto flex gap-2">
<div className="flex gap-2 sm:ml-auto">
<MiniStat value={`${totals.min} h`} label={t('config.grid.minTotal')} />
<MiniStat value={`${totals.ideal} h`} label={t('config.grid.idealTotal')} accent />
</div>
+1 -1
View File
@@ -32,7 +32,7 @@ export function ConfigMail() {
return (
<div className="p-4">
<div className="grid items-start gap-4 [grid-template-columns:repeat(auto-fit,minmax(320px,1fr))]">
<div className="grid items-start gap-4 [grid-template-columns:repeat(auto-fit,minmax(min(320px,100%),1fr))]">
<Panel shadow="lg">
<PanelHeader>
{t('config.mail.templateTitle')} <HelpTip text={t('config.mail.smtpBanner')} side="bottom" />
+1 -1
View File
@@ -28,7 +28,7 @@ export function ConfigRules() {
const shifts = useShifts();
return (
<div className="grid items-start gap-4 p-4 [grid-template-columns:repeat(auto-fit,minmax(320px,1fr))]">
<div className="grid items-start gap-4 p-3 sm:p-4 [grid-template-columns:repeat(auto-fit,minmax(min(320px,100%),1fr))]">
<Panel shadow="lg">
<PanelHeader>{t('config.rules.editionTitle')}</PanelHeader>
<div className="flex flex-col gap-2 p-[13px]">
+28 -14
View File
@@ -571,8 +571,10 @@ function issueParams(issue: ImportIssue): Record<string, string | number> {
return out;
}
// Mobile : nom + cotisation seulement (téléphone, email et vœux restent sur
// la fiche) ; toutes les colonnes à partir de `md:`.
const LIST_COLUMNS =
'minmax(0,2fr) minmax(0,1.4fr) minmax(0,2fr) minmax(0,1.2fr) minmax(0,1.2fr)';
'grid-cols-[minmax(0,1fr)_auto] md:grid-cols-[minmax(0,2fr)_minmax(0,1.4fr)_minmax(0,2fr)_minmax(0,1.2fr)_minmax(0,1.2fr)]';
/** Résumé d'une ligne de la liste : mode de paiement (et mécénat), statut
* d'un paiement en ligne non abouti. */
@@ -677,7 +679,7 @@ export function ConfigVolunteers() {
return (
<div className="flex min-h-0 flex-1 items-stretch">
<div className="max-h-[calc(100vh-118px)] min-w-0 flex-1 overflow-auto p-4">
<div className="min-w-0 flex-1 p-3 sm:p-4 lg:max-h-[calc(100vh-118px)] lg:overflow-auto">
<div className="mb-[14px] flex flex-wrap items-stretch gap-3">
<div className="flex flex-1 flex-col justify-center gap-[10px] border-[3px] border-ink bg-paper p-[14px] shadow-brutal-lg">
<div>
@@ -757,13 +759,15 @@ export function ConfigVolunteers() {
) : (
<div className="border-[3px] border-ink bg-paper shadow-brutal-lg">
<div
className="grid gap-[10px] bg-ink px-[10px] py-[7px] font-display text-[9.5px] font-extrabold uppercase tracking-wide text-cream"
style={{ gridTemplateColumns: LIST_COLUMNS }}
className={cx(
'grid gap-[10px] bg-ink px-[10px] py-[7px] font-display text-[9.5px] font-extrabold uppercase tracking-wide text-cream',
LIST_COLUMNS,
)}
>
<span>{t('config.vol.colName')}</span>
<span>{t('config.vol.colPhone')}</span>
<span>{t('config.vol.colEmail')}</span>
<span>{t('config.vol.colWishes')}</span>
<span className="hidden md:block">{t('config.vol.colPhone')}</span>
<span className="hidden md:block">{t('config.vol.colEmail')}</span>
<span className="hidden md:block">{t('config.vol.colWishes')}</span>
<span>{t('config.vol.colFee')}</span>
</div>
{rows.map((volunteer) => {
@@ -774,10 +778,10 @@ export function ConfigVolunteers() {
type="button"
onClick={() => setSelectedId(volunteer.id)}
className={cx(
'grid w-full items-center gap-[10px] border-b border-sand-line px-[10px] py-[7px] text-left',
'grid w-full items-center gap-[10px] border-b border-sand-line px-[10px] py-[9px] text-left md:py-[7px]',
LIST_COLUMNS,
active ? 'bg-yellow' : 'hover:bg-cream',
)}
style={{ gridTemplateColumns: LIST_COLUMNS }}
>
<span className="flex min-w-0 items-center gap-[7px]">
<span className="flex h-6 w-6 flex-none items-center justify-center border-2 border-ink bg-paper font-display text-[9.5px] font-extrabold">
@@ -788,13 +792,13 @@ export function ConfigVolunteers() {
{volunteer.first_name} {volunteer.last_name}
</span>
</span>
<span className="truncate font-mono text-[11px] opacity-80">
<span className="hidden truncate font-mono text-[11px] opacity-80 md:block">
{volunteer.phone ?? '—'}
</span>
<span className="truncate font-mono text-[10.5px] opacity-70">
<span className="hidden truncate font-mono text-[10.5px] opacity-70 md:block">
{volunteer.email ?? '—'}
</span>
<span className="truncate font-mono text-[10px] opacity-70">
<span className="hidden truncate font-mono text-[10px] opacity-70 md:block">
{t('config.vol.wishesCount', {
count:
volunteer.artist_ids.length +
@@ -815,8 +819,17 @@ export function ConfigVolunteers() {
)}
</div>
{/* Fiche : à côté de la liste sur grand écran, par-dessus (plein écran
sur mobile, tiroir à droite sur tablette) en dessous de `lg:`. */}
{selected ? (
<aside className="max-h-[calc(100vh-118px)] w-[360px] flex-none overflow-auto border-l-[3px] border-ink bg-paper">
<div
aria-hidden
onClick={() => setSelectedId(null)}
className="fixed inset-0 z-40 bg-ink/40 lg:hidden"
/>
) : null}
{selected ? (
<aside className="fixed inset-y-0 right-0 z-50 w-full overflow-auto bg-paper sm:w-[400px] sm:border-l-[3px] sm:border-ink lg:static lg:z-auto lg:max-h-[calc(100vh-118px)] lg:w-[360px] lg:flex-none lg:border-l-[3px]">
<div className="bg-ink px-[13px] py-[11px] text-cream">
<div className="flex items-center justify-between">
<div className="flex items-center gap-[6px] font-mono text-[10px] text-yellow">
@@ -826,7 +839,8 @@ export function ConfigVolunteers() {
<button
type="button"
onClick={() => setSelectedId(null)}
className="cursor-pointer font-mono text-[11px] text-cream/70 hover:text-cream"
aria-label={t('action.close')}
className="h-8 w-8 cursor-pointer border-2 border-cream/40 font-mono text-[12px] text-cream/80 hover:text-cream lg:h-auto lg:w-auto lg:border-0"
>
✕
</button>