Introduce la primitiva Card e sistema tocco e accessibilità dei componenti

`rounded-3xl bg-card p-4 shadow-card` era ricopiato a mano 22 volte: cambiare
raggio od ombra voleva dire toccare venti file. `Card` in `ui-bits.tsx` è ora
l'unica definizione delle superfici, con la gerarchia dei raggi scritta invece
che implicita (contenitore 3xl, elemento interno 2xl, controllo full).

Tocco e tastiera:
- i chip presenza di `EventoCard` erano alti ~28 px ed è il controllo più
  toccato dell'app: ora `min-h-11`, con 8 px di spazio fra l'uno e l'altro;
- stessa cura per il link «Apri partita», la chiusura di `CelebrazioneBadge` e
  il titolo di `SezioneTendina`;
- `aria-pressed` sui controlli a stato, `aria-controls` sulle tendine (che
  avevano `aria-expanded` senza il pannello a cui si riferisce), `aria-busy`
  sui caricamenti;
- `BottomNav`: lo stato attivo era comunicato solo dal colore. Ora cambia
  anche il peso del testo, compare una barretta sopra l'icona e c'è
  `aria-current="page"`. La voce «Home» diventa «Oggi», che dice cosa
  contiene. La barra è un materiale traslucido con bordo sfumato al posto
  della riga netta.
- `Barra` espone `role="progressbar"` con i valori.

Il testo sotto i 12 px è sparito (108 occorrenze fra `text-[9px]`,
`text-[10px]` e `text-[11px]`): sotto quella soglia la leggibilità cala e su
iOS non scala con Dynamic Type. La gerarchia la fanno peso e maiuscolo.

`Avatar` aveva `alt="Foto di 12"` quando il fallback è il numero di maglia —
non descrive niente e il nome è già scritto accanto, quindi alt vuoto. Aggiunti
`width`/`height` per non far saltare il layout al caricamento.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-05 12:42:24 +02:00
co-authored by Claude Opus 5
parent 1ff650ecd5
commit 5c49294ad1
16 changed files with 149 additions and 96 deletions
+30 -10
View File
@@ -1,4 +1,4 @@
import { useState, type ReactNode } from "react";
import { useId, useState, type ComponentPropsWithoutRef, type ReactNode } from "react";
import { ChevronDown } from "lucide-react";
import { cn } from "@/lib/utils";
import { statoMeta, type Stato } from "@/lib/crapp-data";
@@ -10,19 +10,37 @@ export function TeamLogo({ className }: { className?: string }) {
<img
src="/icon-192.png"
alt="CRAP Volley"
width={192}
height={192}
className={cn("shrink-0 rounded-2xl object-cover shadow-pop", className)}
/>
);
}
/**
* Superficie standard dell'app: era ripetuta a mano una ventina di volte come
* `rounded-3xl bg-card p-4 shadow-card`, quindi cambiare raggio od ombra
* voleva dire toccare venti file.
*
* Gerarchia dei raggi: contenitore `3xl` → elemento interno `2xl` →
* controllo `full`.
*/
export function Card({
className,
as: Tag = "div",
...props
}: ComponentPropsWithoutRef<"div"> & { as?: "div" | "article" | "section" }) {
return <Tag className={cn("premi rounded-3xl bg-card p-4 shadow-card", className)} {...props} />;
}
export function PageHeader({ titolo, sottotitolo }: { titolo: string; sottotitolo?: string }) {
return (
<header className="bg-hero px-5 pb-8 pt-7 text-primary-foreground">
<div className="grid grid-cols-[minmax(0,1fr)_auto] items-center gap-4">
<div className="min-w-0">
<h1 className="truncate font-display text-3xl uppercase">{titolo}</h1>
<h1 className="truncate font-display-lg text-3xl uppercase">{titolo}</h1>
{sottotitolo ? (
<p className="mt-1 truncate text-sm text-primary-foreground/70">{sottotitolo}</p>
<p className="mt-1 truncate text-sm text-primary-foreground/80">{sottotitolo}</p>
) : null}
</div>
<TeamLogo className="h-11 w-11" />
@@ -45,7 +63,7 @@ export function Section({
return (
<Reveal as="section" indice={indice} className="px-5 py-4">
<div className="mb-3 flex items-center justify-between gap-3">
<h2 className="font-display text-lg uppercase tracking-wide">{titolo}</h2>
<h2 className="font-display-sm text-lg uppercase">{titolo}</h2>
{azione}
</div>
{children}
@@ -70,15 +88,17 @@ export function SezioneTendina({
indice?: number;
}) {
const [aperta, setAperta] = useState(defaultAperta);
const id = useId();
return (
<Reveal as="section" indice={indice} className="px-5 py-4">
<button
type="button"
onClick={() => setAperta((v) => !v)}
className="mb-3 flex w-full items-center justify-between gap-3 text-left active:scale-[0.99]"
className="mb-3 flex min-h-11 w-full items-center justify-between gap-3 text-left active:scale-[0.99]"
aria-expanded={aperta}
aria-controls={id}
>
<h2 className="font-display text-lg uppercase tracking-wide">{titolo}</h2>
<h2 className="font-display-sm text-lg uppercase">{titolo}</h2>
<span className="flex shrink-0 items-center gap-2">
{azione}
<ChevronDown
@@ -90,7 +110,7 @@ export function SezioneTendina({
</span>
</button>
{anteprima}
{aperta ? children : null}
<div id={id}>{aperta ? children : null}</div>
</Reveal>
);
}
@@ -100,7 +120,7 @@ export function StatoBadge({ stato, className }: { stato: Stato; className?: str
return (
<span
className={cn(
"inline-flex items-center gap-1 rounded-full px-2.5 py-1 text-[11px] font-bold uppercase",
"inline-flex items-center gap-1 rounded-full px-2.5 py-1 text-xs font-bold uppercase",
meta.className,
className,
)}
@@ -124,10 +144,10 @@ export function StatTile({
<p className="font-display text-2xl leading-none">
{typeof valore === "number" ? <Numero valore={valore} /> : valore}
</p>
<p className="mt-1 text-[11px] font-semibold uppercase tracking-wide text-muted-foreground">
<p className="mt-1 text-xs font-semibold uppercase tracking-wide text-muted-foreground">
{label}
</p>
{hint ? <p className="mt-0.5 text-[11px] text-accent">{hint}</p> : null}
{hint ? <p className="mt-0.5 text-xs text-accent">{hint}</p> : null}
</div>
);
}