Rifinisce l'interfaccia su safe area, materiali, tipografia e accessibilità.

Nessun cambio di funzionalità: solo resa, accessibilità e correzioni al layout
su iPhone.

- Aggiunge `viewport-fit=cover`: senza, su iOS `env(safe-area-inset-*)` valeva 0
  e il margine per la barra gesture già presente nella bottom nav non aveva
  effetto. Aggiunge il margine superiore su hero, PageHeader e header dello
  scout, che in modalità standalone finivano sotto il notch.
- Attiva il tema scuro sulla preferenza di sistema e ne riscrive i valori:
  l'accento resta il rosso del brand (prima diventava grigio-blu) e i token
  semantici che mancavano ora ci sono. `theme-color` ha una tinta per tema.
- Sposta il feedback dal rilascio alla pressione su Button, bottom nav e comandi
  principali; aggiunge l'anello di focus da tastiera in `@layer base`, che
  mancava su tutti i bottoni scritti a mano fuori da components/ui/.
- Rende la bottom nav una superficie traslucida con sfumatura di scorrimento al
  posto della riga netta, alleggerisce il velo dei drawer e sostituisce l'alone
  rosso delle ombre con ombre neutre.
- Introduce una scala tipografica in rem con crenatura e interlinea per taglia,
  al posto dei 109 `text-[Npx]` fissi che non scalavano con l'impostazione di
  testo del sistema.
- Porta a 44px le aree di tocco dei comandi piccoli, mostra uno scheletro di
  pagina al posto della schermata bianca in home e profilo, conserva le
  dissolvenze con movimento ridotto e aggiunge il supporto a
  `prefers-reduced-transparency` e `prefers-contrast`.
- Anima l'apertura delle sezioni a tendina con Radix Collapsible, già fra le
  dipendenze, al posto di un montaggio secco.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
davideandClaude Opus 5 committed 2026-09-04 21:59:14 +02:00
1 parent 952f7b43d3
commit f3b22583ed
30 files changed
+555 -202

No files matched your search

+25
View File
@@ -6,6 +6,31 @@ qui: sta in [ROADMAP.md](ROADMAP.md).
## Versione attuale — agosto 2026 ## Versione attuale — agosto 2026
### Rifinitura dell'interfaccia (settembre 2026)
Nessun cambio di funzionalità: solo resa, accessibilità e correzioni al layout su iPhone.
- **Safe area.** Aggiunto `viewport-fit=cover`: senza, su iOS `env(safe-area-inset-*)`
valeva 0 e il margine per la barra gesture già presente nella bottom nav non aveva
effetto. Aggiunto anche il margine superiore su hero, `PageHeader` e header dello scout,
che in modalità standalone finivano sotto il notch.
- **Tema scuro attivo.** La classe `dark` segue `prefers-color-scheme` da uno script nel
`head`. I valori del tema scuro sono stati riscritti: l'accento resta il rosso del brand
(prima diventava grigio-blu) e sono stati definiti i token semantici che mancavano.
`theme-color` ora ha una tinta per tema.
- **Feedback al tocco.** `Button`, voci della bottom nav e comandi principali reagiscono
alla pressione e non al rilascio. Anello di focus da tastiera su tutti gli elementi
attivabili (`styles.css`, `@layer base`): prima era assente sui bottoni scritti a mano.
- **Materiali.** La bottom nav è una superficie traslucida con sfumatura di scorrimento al
posto della riga netta; velo dei drawer alleggerito; ombre neutre al posto dell'alone
rosso.
- **Tipografia.** Scala in `rem` con crenatura e interlinea per taglia; eliminati i 109
`text-[Npx]` fissi, che non scalavano con l'impostazione di testo del sistema.
- **Accessibilità e caricamento.** Aree di tocco portate a 44px sui comandi piccoli,
scheletro di pagina al posto della schermata bianca in home e profilo, movimento ridotto
che conserva le dissolvenze, supporto a `prefers-reduced-transparency` e
`prefers-contrast`.
### Autenticazione e dashboard amministratore (in produzione) ### Autenticazione e dashboard amministratore (in produzione)
- Login con Google tramite Supabase Auth (DD-011). Al primo accesso l'account si collega a - Login con Google tramite Supabase Auth (DD-011). Al primo accesso l'account si collega a
+2 -2
View File
@@ -4,8 +4,8 @@
"description": "L'app della squadra CRAP Volley: presenze, calendario, statistiche, classifica e scout live.", "description": "L'app della squadra CRAP Volley: presenze, calendario, statistiche, classifica e scout live.",
"start_url": "/", "start_url": "/",
"display": "standalone", "display": "standalone",
"background_color": "#111111", "background_color": "#0c0d11",
"theme_color": "#111111", "theme_color": "#f9fafb",
"orientation": "portrait", "orientation": "portrait",
"icons": [ "icons": [
{ {
+6 -8
View File
@@ -42,15 +42,13 @@ function DettaglioBadge({ def, stato }: { def: BadgeDef; stato?: BadgeStato }) {
{stato ? ( {stato ? (
<div className="rounded-2xl bg-card p-4 shadow-card ring-1 ring-border"> <div className="rounded-2xl bg-card p-4 shadow-card ring-1 ring-border">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<p className="text-[11px] font-bold uppercase tracking-wide text-muted-foreground"> <p className="text-2xs font-bold uppercase tracking-wide text-muted-foreground">
Stato attuale Stato attuale
</p> </p>
{grado ? ( {grado ? (
<span className={cn("text-[11px] font-bold uppercase", meta?.text)}> <span className={cn("text-2xs font-bold uppercase", meta?.text)}>{meta?.label}</span>
{meta?.label}
</span>
) : ( ) : (
<span className="text-[11px] font-bold uppercase text-muted-foreground"> <span className="text-2xs font-bold uppercase text-muted-foreground">
In progresso In progresso
</span> </span>
)} )}
@@ -73,7 +71,7 @@ function DettaglioBadge({ def, stato }: { def: BadgeDef; stato?: BadgeStato }) {
) : null} ) : null}
<div> <div>
<p className="mb-2 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"> <p className="mb-2 text-2xs font-bold uppercase tracking-wide text-muted-foreground">
Soglie Soglie
</p> </p>
<div className="grid grid-cols-3 gap-2"> <div className="grid grid-cols-3 gap-2">
@@ -90,7 +88,7 @@ function DettaglioBadge({ def, stato }: { def: BadgeDef; stato?: BadgeStato }) {
> >
<p <p
className={cn( className={cn(
"text-[10px] font-bold uppercase", "text-3xs font-bold uppercase",
raggiunto ? gm.text : "text-muted-foreground", raggiunto ? gm.text : "text-muted-foreground",
)} )}
> >
@@ -104,7 +102,7 @@ function DettaglioBadge({ def, stato }: { def: BadgeDef; stato?: BadgeStato }) {
> >
{def.soglie[g]} {def.soglie[g]}
</p> </p>
<p className="text-[10px] text-muted-foreground">{def.unita}</p> <p className="text-3xs text-muted-foreground">{def.unita}</p>
</div> </div>
); );
})} })}
+2 -2
View File
@@ -11,14 +11,14 @@ const items = [
export function BottomNav() { export function BottomNav() {
return ( return (
<nav className="fixed inset-x-0 bottom-0 z-40 border-t border-border bg-card/95 backdrop-blur-md"> <nav className="materiale bordo-scroll-basso fixed inset-x-0 bottom-0 z-40 border-t">
<div className="mx-auto grid max-w-md grid-cols-5 px-1 pb-[env(safe-area-inset-bottom)]"> <div className="mx-auto grid max-w-md grid-cols-5 px-1 pb-[env(safe-area-inset-bottom)]">
{items.map(({ to, label, icon: Icon }) => ( {items.map(({ to, label, icon: Icon }) => (
<Link <Link
key={to} key={to}
to={to} to={to}
activeOptions={{ exact: to === "/" }} activeOptions={{ exact: to === "/" }}
className="group flex flex-col items-center gap-1 py-2.5 text-[10px] font-semibold text-muted-foreground transition-colors data-[status=active]:text-accent" className="group flex flex-col items-center gap-1 py-2.5 text-3xs font-semibold text-muted-foreground transition-[color,transform] duration-100 ease-out active:scale-[0.92] data-[status=active]:text-accent"
> >
<Icon className="h-5 w-5" strokeWidth={2.2} /> <Icon className="h-5 w-5" strokeWidth={2.2} />
{label} {label}
+1 -1
View File
@@ -51,7 +51,7 @@ export function CelebrazioneBadge() {
{notifica.emoji} {notifica.emoji}
</span> </span>
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<p className="text-[11px] font-bold uppercase tracking-wide text-accent"> <p className="text-2xs font-bold uppercase tracking-wide text-accent">
{notifica.tono === "segreto" ? "Badge segreto sbloccato" : "Traguardo raggiunto"} {notifica.tono === "segreto" ? "Badge segreto sbloccato" : "Traguardo raggiunto"}
</p> </p>
<p className="mt-0.5 font-display text-2xl leading-none">{notifica.titolo}</p> <p className="mt-0.5 font-display text-2xl leading-none">{notifica.titolo}</p>
+15 -15
View File
@@ -45,21 +45,21 @@ function CardBadge({ b, opaco, indice = 0 }: { b: BadgeStato; opaco?: boolean; i
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<Icon className={cn("h-6 w-6", meta ? meta.text : "text-muted-foreground/50")} /> <Icon className={cn("h-6 w-6", meta ? meta.text : "text-muted-foreground/50")} />
{meta ? ( {meta ? (
<span className={cn("text-[10px] font-bold uppercase", meta.text)}>{meta.label}</span> <span className={cn("text-3xs font-bold uppercase", meta.text)}>{meta.label}</span>
) : null} ) : null}
</div> </div>
<p className="mt-1.5 text-sm font-bold leading-tight">{b.def.nome}</p> <p className="mt-1.5 text-sm font-bold leading-tight">{b.def.nome}</p>
<p className="text-[11px] text-muted-foreground"> <p className="text-2xs text-muted-foreground">
{b.valore} {b.def.unita} {b.valore} {b.def.unita}
</p> </p>
{b.prossimaSoglia ? ( {b.prossimaSoglia ? (
<> <>
<Barra percentuale={b.progresso} altezza="h-1.5" trackClassName="mt-2" /> <Barra percentuale={b.progresso} altezza="h-1.5" trackClassName="mt-2" />
<p className="mt-1 text-[10px] font-semibold text-accent">{mancanoPer(b)}</p> <p className="mt-1 text-3xs font-semibold text-accent">{mancanoPer(b)}</p>
<p className="text-[10px] text-muted-foreground">{microcopyBadge(b)}</p> <p className="text-3xs text-muted-foreground">{microcopyBadge(b)}</p>
</> </>
) : ( ) : (
<p className="mt-2 text-[10px] text-muted-foreground">{microcopyBadge(b)}</p> <p className="mt-2 text-3xs text-muted-foreground">{microcopyBadge(b)}</p>
)} )}
</Reveal> </Reveal>
); );
@@ -96,7 +96,7 @@ function SocialDrawer({
</div> </div>
</div> </div>
<div className="rounded-2xl bg-card p-4 shadow-card ring-1 ring-border"> <div className="rounded-2xl bg-card p-4 shadow-card ring-1 ring-border">
<p className="text-[11px] font-bold uppercase tracking-wide text-muted-foreground"> <p className="text-2xs font-bold uppercase tracking-wide text-muted-foreground">
Vinto Vinto
</p> </p>
<p className="mt-1 font-display text-3xl leading-none"> <p className="mt-1 font-display text-3xl leading-none">
@@ -141,7 +141,7 @@ export function CollezioneBadge({
<div className="rounded-3xl bg-hero p-4 text-primary-foreground shadow-card"> <div className="rounded-3xl bg-hero p-4 text-primary-foreground shadow-card">
<div className="flex items-end justify-between gap-3"> <div className="flex items-end justify-between gap-3">
<div className="min-w-0"> <div className="min-w-0">
<p className="text-[11px] font-semibold uppercase tracking-wide text-primary-foreground/60"> <p className="text-2xs font-semibold uppercase tracking-wide text-primary-foreground/60">
Collezione badge Collezione badge
</p> </p>
<p className="font-display text-4xl leading-none"> <p className="font-display text-4xl leading-none">
@@ -158,7 +158,7 @@ export function CollezioneBadge({
{vicino ? ( {vicino ? (
<div className="rounded-3xl bg-card p-4 shadow-card ring-1 ring-accent/30"> <div className="rounded-3xl bg-card p-4 shadow-card ring-1 ring-accent/30">
<p className="text-[11px] font-bold uppercase tracking-wide text-accent"> <p className="text-2xs font-bold uppercase tracking-wide text-accent">
Prossimo traguardo Prossimo traguardo
</p> </p>
<p className="mt-1 text-sm font-bold leading-tight">{vicino.def.nome}</p> <p className="mt-1 text-sm font-bold leading-tight">{vicino.def.nome}</p>
@@ -171,7 +171,7 @@ export function CollezioneBadge({
{c.sbloccati.length > 0 ? ( {c.sbloccati.length > 0 ? (
<div> <div>
<p className="mb-2 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"> <p className="mb-2 text-2xs font-bold uppercase tracking-wide text-muted-foreground">
Sbloccati ({c.sbloccati.length}) Sbloccati ({c.sbloccati.length})
</p> </p>
<div className="grid grid-cols-2 gap-2"> <div className="grid grid-cols-2 gap-2">
@@ -186,7 +186,7 @@ export function CollezioneBadge({
{c.inProgresso.length > 0 ? ( {c.inProgresso.length > 0 ? (
<div> <div>
<p className="mb-2 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"> <p className="mb-2 text-2xs font-bold uppercase tracking-wide text-muted-foreground">
In progresso ({c.inProgresso.length}) In progresso ({c.inProgresso.length})
</p> </p>
<div className="grid grid-cols-2 gap-2"> <div className="grid grid-cols-2 gap-2">
@@ -201,7 +201,7 @@ export function CollezioneBadge({
{socialVinti.length > 0 ? ( {socialVinti.length > 0 ? (
<div> <div>
<p className="mb-2 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"> <p className="mb-2 text-2xs font-bold uppercase tracking-wide text-muted-foreground">
Votati dai compagni Votati dai compagni
</p> </p>
<div className="grid grid-cols-2 gap-2"> <div className="grid grid-cols-2 gap-2">
@@ -210,7 +210,7 @@ export function CollezioneBadge({
<div className="rounded-2xl bg-card p-3 shadow-card ring-1 ring-accent/25"> <div className="rounded-2xl bg-card p-3 shadow-card ring-1 ring-accent/25">
<p className="text-lg leading-none">{cat.emoji}</p> <p className="text-lg leading-none">{cat.emoji}</p>
<p className="mt-1 text-sm font-bold leading-tight">{cat.nome}</p> <p className="mt-1 text-sm font-bold leading-tight">{cat.nome}</p>
<p className="text-[11px] text-muted-foreground"> <p className="text-2xs text-muted-foreground">
Vinto {social[cat.id]} {social[cat.id] === 1 ? "volta" : "volte"} Vinto {social[cat.id]} {social[cat.id] === 1 ? "volta" : "volte"}
</p> </p>
</div> </div>
@@ -221,7 +221,7 @@ export function CollezioneBadge({
) : null} ) : null}
<div> <div>
<p className="mb-2 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"> <p className="mb-2 text-2xs font-bold uppercase tracking-wide text-muted-foreground">
Badge segreti Badge segreti
</p> </p>
<div className="grid grid-cols-2 gap-2"> <div className="grid grid-cols-2 gap-2">
@@ -232,7 +232,7 @@ export function CollezioneBadge({
<div className="rounded-2xl bg-card p-3 shadow-card ring-1 ring-oro/40"> <div className="rounded-2xl bg-card p-3 shadow-card ring-1 ring-oro/40">
<Icon className="h-6 w-6 text-oro" /> <Icon className="h-6 w-6 text-oro" />
<p className="mt-1.5 text-sm font-bold leading-tight">{b.def.nome}</p> <p className="mt-1.5 text-sm font-bold leading-tight">{b.def.nome}</p>
<p className="text-[11px] text-muted-foreground"> <p className="text-2xs text-muted-foreground">
{b.def.celebrazione ?? "Badge segreto sbloccato."} {b.def.celebrazione ?? "Badge segreto sbloccato."}
</p> </p>
</div> </div>
@@ -246,7 +246,7 @@ export function CollezioneBadge({
> >
<Lucchetto className="h-6 w-6 text-muted-foreground/50" /> <Lucchetto className="h-6 w-6 text-muted-foreground/50" />
<p className="mt-1.5 text-sm font-bold leading-tight text-muted-foreground">???</p> <p className="mt-1.5 text-sm font-bold leading-tight text-muted-foreground">???</p>
<p className="text-[11px] text-muted-foreground/70">Badge segreto da scoprire</p> <p className="text-2xs text-muted-foreground/70">Badge segreto da scoprire</p>
</div> </div>
))} ))}
</div> </div>
+6 -6
View File
@@ -63,7 +63,7 @@ export function EventoCard({
</div> </div>
<div className="shrink-0 rounded-2xl bg-secondary px-3 py-2 text-center"> <div className="shrink-0 rounded-2xl bg-secondary px-3 py-2 text-center">
<p className="font-display text-xl leading-none">{evento.data.slice(8, 10)}</p> <p className="font-display text-xl leading-none">{evento.data.slice(8, 10)}</p>
<p className="text-[10px] font-semibold uppercase text-muted-foreground"> <p className="text-3xs font-semibold uppercase text-muted-foreground">
{formatData(evento.data).split(" ")[2]?.slice(0, 3)} {formatData(evento.data).split(" ")[2]?.slice(0, 3)}
</p> </p>
</div> </div>
@@ -77,7 +77,7 @@ export function EventoCard({
<div className="min-w-0"> <div className="min-w-0">
<span <span
className={cn( className={cn(
"inline-block rounded-full px-2.5 py-0.5 text-[10px] font-bold uppercase tracking-wide", "inline-block rounded-full px-2.5 py-0.5 text-3xs font-bold uppercase tracking-wide",
tipo.className, tipo.className,
)} )}
> >
@@ -87,7 +87,7 @@ export function EventoCard({
</div> </div>
<div className="shrink-0 rounded-2xl bg-secondary px-3 py-2 text-center"> <div className="shrink-0 rounded-2xl bg-secondary px-3 py-2 text-center">
<p className="font-display text-xl leading-none">{evento.data.slice(8, 10)}</p> <p className="font-display text-xl leading-none">{evento.data.slice(8, 10)}</p>
<p className="text-[10px] font-semibold uppercase text-muted-foreground"> <p className="text-3xs font-semibold uppercase text-muted-foreground">
{formatData(evento.data).split(" ")[2]?.slice(0, 3)} {formatData(evento.data).split(" ")[2]?.slice(0, 3)}
</p> </p>
</div> </div>
@@ -98,9 +98,9 @@ export function EventoCard({
<Link <Link
to={linkTo.to} to={linkTo.to}
params={linkTo.params} params={linkTo.params}
className="inline-flex items-center gap-1 rounded-full bg-primary px-3 py-1.5 text-[10px] font-bold uppercase tracking-wide text-primary-foreground transition-transform active:scale-95" className="inline-flex items-center gap-1 rounded-full bg-primary px-3 py-1.5 text-3xs font-bold uppercase tracking-wide text-primary-foreground transition-transform active:scale-95"
> >
{linkTo.label} <ArrowRight className="h-3 w-3" /> {linkTo.label} <ArrowRight className="h-3.5 w-3.5" />
</Link> </Link>
</div> </div>
)} )}
@@ -138,7 +138,7 @@ export function EventoCard({
}) })
} }
className={cn( className={cn(
"rounded-full border border-border px-2.5 py-1.5 text-[11px] font-semibold transition-all active:scale-95", "rounded-full border border-border px-2.5 py-1.5 text-2xs font-semibold transition-all active:scale-95",
attivo attivo
? cn(meta.className, "border-transparent shadow-card") ? cn(meta.className, "border-transparent shadow-card")
: "bg-background text-muted-foreground", : "bg-background text-muted-foreground",
+7 -7
View File
@@ -43,14 +43,14 @@ export function Pagelle({
return ( return (
<div className="rounded-3xl bg-card p-4 shadow-card"> <div className="rounded-3xl bg-card p-4 shadow-card">
<div className="flex items-center justify-between gap-2"> <div className="flex items-center justify-between gap-2">
<p className="inline-flex items-center gap-1.5 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"> <p className="inline-flex items-center gap-1.5 text-2xs font-bold uppercase tracking-wide text-muted-foreground">
<ClipboardCheck className="h-3.5 w-3.5" /> Pagelle anonime <ClipboardCheck className="h-3.5 w-3.5" /> Pagelle anonime
</p> </p>
<span className="rounded-full bg-secondary px-2.5 py-1 text-[10px] font-bold uppercase text-muted-foreground"> <span className="rounded-full bg-secondary px-2.5 py-1 text-3xs font-bold uppercase text-muted-foreground">
{chiuse ? "Votazioni chiuse" : `${fatti}/${daVotare.length} votati`} {chiuse ? "Votazioni chiuse" : `${fatti}/${daVotare.length} votati`}
</span> </span>
</div> </div>
<p className="mt-1 text-[11px] text-muted-foreground"> <p className="mt-1 text-2xs text-muted-foreground">
Dai un voto da 1 a 10 ai compagni: nessuno vedrà chi ha votato cosa, solo la media. Dai un voto da 1 a 10 ai compagni: nessuno vedrà chi ha votato cosa, solo la media.
</p> </p>
@@ -69,12 +69,12 @@ export function Pagelle({
<Avatar id={g.id} fallback={String(g.numero)} className="h-8 w-8 text-xs" /> <Avatar id={g.id} fallback={String(g.numero)} className="h-8 w-8 text-xs" />
<span className="min-w-0 flex-1"> <span className="min-w-0 flex-1">
<span className="block truncate text-sm font-bold leading-tight">{g.nome}</span> <span className="block truncate text-sm font-bold leading-tight">{g.nome}</span>
<span className="text-[11px] text-muted-foreground"> <span className="text-2xs text-muted-foreground">
{media ? `Media ${media.media} · ${media.voti} voti` : "Nessun voto"} {media ? `Media ${media.media} · ${media.voti} voti` : "Nessun voto"}
</span> </span>
</span> </span>
{sonoIo ? ( {sonoIo ? (
<span className="shrink-0 text-[10px] font-semibold uppercase text-muted-foreground"> <span className="shrink-0 text-3xs font-semibold uppercase text-muted-foreground">
Sei tu Sei tu
</span> </span>
) : chiuse ? ( ) : chiuse ? (
@@ -85,7 +85,7 @@ export function Pagelle({
disabled={!io || vota.isPending} disabled={!io || vota.isPending}
onClick={() => setApertoPer(aperto ? null : g.id)} onClick={() => setApertoPer(aperto ? null : g.id)}
className={cn( className={cn(
"shrink-0 rounded-full px-3 py-1 text-[10px] font-bold uppercase transition-colors disabled:opacity-50", "shrink-0 rounded-full px-3 py-1 text-3xs font-bold uppercase transition-colors disabled:opacity-50",
mio !== undefined mio !== undefined
? "bg-accent text-accent-foreground" ? "bg-accent text-accent-foreground"
: "bg-card text-foreground", : "bg-card text-foreground",
@@ -103,7 +103,7 @@ export function Pagelle({
type="button" type="button"
onClick={() => invia(g.id, v)} onClick={() => invia(g.id, v)}
className={cn( className={cn(
"rounded-lg py-1.5 text-[11px] font-bold tabular-nums transition-transform active:scale-90", "rounded-lg py-1.5 text-2xs font-bold tabular-nums transition-transform active:scale-90",
mio === v mio === v
? "bg-accent text-accent-foreground" ? "bg-accent text-accent-foreground"
: "bg-card text-foreground", : "bg-card text-foreground",
@@ -27,7 +27,7 @@ const classiInput = "w-full rounded-xl border border-border bg-background px-3 p
function Campo({ label, children }: { label: string; children: React.ReactNode }) { function Campo({ label, children }: { label: string; children: React.ReactNode }) {
return ( return (
<label className="block"> <label className="block">
<span className="text-[10px] font-bold uppercase tracking-wide text-muted-foreground"> <span className="text-3xs font-bold uppercase tracking-wide text-muted-foreground">
{label} {label}
</span> </span>
<span className="mt-1 block">{children}</span> <span className="mt-1 block">{children}</span>
+4 -4
View File
@@ -65,7 +65,7 @@ export function RosaPresenze({ eventoId }: { eventoId: string }) {
<span <span
key={s} key={s}
className={cn( className={cn(
"rounded-full px-2.5 py-1 text-[11px] font-bold", "rounded-full px-2.5 py-1 text-2xs font-bold",
n > 0 ? statoMeta[s].className : "bg-secondary text-muted-foreground", n > 0 ? statoMeta[s].className : "bg-secondary text-muted-foreground",
)} )}
> >
@@ -73,14 +73,14 @@ export function RosaPresenze({ eventoId }: { eventoId: string }) {
</span> </span>
); );
})} })}
<span className="rounded-full bg-secondary px-2.5 py-1 text-[11px] font-bold text-muted-foreground"> <span className="rounded-full bg-secondary px-2.5 py-1 text-2xs font-bold text-muted-foreground">
❔ {mancanti.length} da rispondere ❔ {mancanti.length} da rispondere
</span> </span>
</div> </div>
{io ? ( {io ? (
<div className="mt-4 border-t border-border pt-3"> <div className="mt-4 border-t border-border pt-3">
<p className="text-[10px] font-bold uppercase tracking-wide text-muted-foreground"> <p className="text-3xs font-bold uppercase tracking-wide text-muted-foreground">
La tua risposta La tua risposta
</p> </p>
<div className="mt-2 flex flex-wrap gap-1.5"> <div className="mt-2 flex flex-wrap gap-1.5">
@@ -95,7 +95,7 @@ export function RosaPresenze({ eventoId }: { eventoId: string }) {
salva.mutate({ eventoId, giocatoreId: io.id, stato: attivo ? null : s }) salva.mutate({ eventoId, giocatoreId: io.id, stato: attivo ? null : s })
} }
className={cn( className={cn(
"rounded-full border border-border px-2.5 py-1.5 text-[11px] font-semibold transition-all active:scale-95", "rounded-full border border-border px-2.5 py-1.5 text-2xs font-semibold transition-all active:scale-95",
attivo attivo
? cn(statoMeta[s].className, "border-transparent shadow-card") ? cn(statoMeta[s].className, "border-transparent shadow-card")
: "bg-background text-muted-foreground", : "bg-background text-muted-foreground",
+1 -1
View File
@@ -39,7 +39,7 @@ export function ScoutEntry({ variante = "grande" }: { variante?: "grande" | "com
{occupato ? <Lock className="h-5 w-5 shrink-0" /> : <Radio className="h-5 w-5 shrink-0" />} {occupato ? <Lock className="h-5 w-5 shrink-0" /> : <Radio className="h-5 w-5 shrink-0" />}
<span className="min-w-0 flex-1"> <span className="min-w-0 flex-1">
<span className="block font-display text-lg uppercase leading-none">{titolo}</span> <span className="block font-display text-lg uppercase leading-none">{titolo}</span>
<span className="block text-[11px] opacity-80">{sottotitolo}</span> <span className="block text-2xs opacity-80">{sottotitolo}</span>
</span> </span>
{disponibile ? <ChevronRight className="h-5 w-5 shrink-0" /> : null} {disponibile ? <ChevronRight className="h-5 w-5 shrink-0" /> : null}
</> </>
+4 -4
View File
@@ -28,7 +28,7 @@ export function SerieGriglia({ g }: { g: Giocatore }) {
</span> </span>
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<p className="text-sm font-bold leading-tight">{s.def.label}</p> <p className="text-sm font-bold leading-tight">{s.def.label}</p>
<p className="text-[11px] text-muted-foreground">{s.def.descrizione}</p> <p className="text-2xs text-muted-foreground">{s.def.descrizione}</p>
</div> </div>
<span className="inline-flex items-center gap-1 font-display text-2xl leading-none"> <span className="inline-flex items-center gap-1 font-display text-2xl leading-none">
<Flame <Flame
@@ -38,7 +38,7 @@ export function SerieGriglia({ g }: { g: Giocatore }) {
</span> </span>
</div> </div>
<Barra percentuale={s.progresso} trackClassName="mt-3" /> <Barra percentuale={s.progresso} trackClassName="mt-3" />
<p className="mt-2 text-[11px] text-muted-foreground"> <p className="mt-2 text-2xs text-muted-foreground">
{s.prossimo ? `${s.valore}/${s.prossimo} · ` : ""} {s.prossimo ? `${s.valore}/${s.prossimo} · ` : ""}
{s.messaggio} {s.messaggio}
</p> </p>
@@ -63,14 +63,14 @@ export function SerieHome({ g }: { g: Giocatore }) {
<p className="text-sm font-bold leading-tight"> <p className="text-sm font-bold leading-tight">
Serie {top.def.label.toLowerCase()}: {top.valore} Serie {top.def.label.toLowerCase()}: {top.valore}
</p> </p>
<p className="text-[11px] text-muted-foreground">{top.messaggio}</p> <p className="text-2xs text-muted-foreground">{top.messaggio}</p>
</div> </div>
</div> </div>
<div className="mt-3 grid grid-cols-3 gap-2"> <div className="mt-3 grid grid-cols-3 gap-2">
{serieGiocatore(g).map((s) => ( {serieGiocatore(g).map((s) => (
<div key={s.def.tipo} className="rounded-2xl bg-secondary p-2 text-center"> <div key={s.def.tipo} className="rounded-2xl bg-secondary p-2 text-center">
<p className="font-display text-xl leading-none">{s.valore}</p> <p className="font-display text-xl leading-none">{s.valore}</p>
<p className="mt-1 text-[10px] font-semibold uppercase text-muted-foreground"> <p className="mt-1 text-3xs font-semibold uppercase text-muted-foreground">
{s.def.label} {s.def.label}
</p> </p>
</div> </div>
+4 -4
View File
@@ -37,14 +37,14 @@ export function SondaggioCacche({ eventoId }: { eventoId: string }) {
return ( return (
<div className="rounded-3xl bg-card p-4 shadow-card"> <div className="rounded-3xl bg-card p-4 shadow-card">
<div className="flex items-center justify-between gap-2"> <div className="flex items-center justify-between gap-2">
<p className="text-[11px] font-bold uppercase tracking-wide text-muted-foreground"> <p className="text-2xs font-bold uppercase tracking-wide text-muted-foreground">
💩 Sondaggio pre-partita 💩 Sondaggio pre-partita
</p> </p>
<span className="rounded-full bg-secondary px-2.5 py-1 text-[10px] font-bold uppercase text-muted-foreground"> <span className="rounded-full bg-secondary px-2.5 py-1 text-3xs font-bold uppercase text-muted-foreground">
{dellaPartita.length} risposte {dellaPartita.length} risposte
</span> </span>
</div> </div>
<p className="mt-1 text-[11px] text-muted-foreground"> <p className="mt-1 text-2xs text-muted-foreground">
Quante cacche hai fatto prima di questa partita? Dato scientifico fondamentale. Quante cacche hai fatto prima di questa partita? Dato scientifico fondamentale.
</p> </p>
@@ -67,7 +67,7 @@ export function SondaggioCacche({ eventoId }: { eventoId: string }) {
))} ))}
</div> </div>
<div className="mt-3 flex flex-wrap items-center gap-2 text-[11px]"> <div className="mt-3 flex flex-wrap items-center gap-2 text-2xs">
<span className="rounded-full bg-secondary px-2.5 py-1 font-semibold"> <span className="rounded-full bg-secondary px-2.5 py-1 font-semibold">
Media squadra {media} Media squadra {media}
</span> </span>
+2 -2
View File
@@ -41,7 +41,7 @@ export function TurnoPalloni({ eventoId }: { eventoId: string }) {
<CircleDot className="h-4 w-4" /> <CircleDot className="h-4 w-4" />
</span> </span>
<span className="min-w-0 flex-1"> <span className="min-w-0 flex-1">
<span className="block text-[10px] font-bold uppercase tracking-wide text-muted-foreground"> <span className="block text-3xs font-bold uppercase tracking-wide text-muted-foreground">
Palloni Palloni
</span> </span>
<span className="flex items-center gap-1.5 text-sm font-bold leading-tight"> <span className="flex items-center gap-1.5 text-sm font-bold leading-tight">
@@ -50,7 +50,7 @@ export function TurnoPalloni({ eventoId }: { eventoId: string }) {
) : null} ) : null}
<span className="truncate">{giocatore ? nomeCompleto(giocatore) : "Da assegnare"}</span> <span className="truncate">{giocatore ? nomeCompleto(giocatore) : "Da assegnare"}</span>
{proposto && giocatore ? ( {proposto && giocatore ? (
<span className="shrink-0 rounded-full bg-card px-1.5 py-0.5 text-[9px] font-bold uppercase text-muted-foreground"> <span className="shrink-0 rounded-full bg-card px-1.5 py-0.5 text-3xs font-bold uppercase text-muted-foreground">
proposto proposto
</span> </span>
) : null} ) : null}
+4 -4
View File
@@ -41,14 +41,14 @@ export function VotazioneMvp({ matchId }: { matchId: string }) {
return ( return (
<div className="mt-3 rounded-2xl bg-secondary/60 p-3"> <div className="mt-3 rounded-2xl bg-secondary/60 p-3">
<div className="flex items-center justify-between gap-2"> <div className="flex items-center justify-between gap-2">
<p className="inline-flex items-center gap-1.5 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"> <p className="inline-flex items-center gap-1.5 text-2xs font-bold uppercase tracking-wide text-muted-foreground">
<Vote className="h-3.5 w-3.5" /> Voto MVP · {totale} {totale === 1 ? "voto" : "voti"} <Vote className="h-3.5 w-3.5" /> Voto MVP · {totale} {totale === 1 ? "voto" : "voti"}
</p> </p>
<button <button
type="button" type="button"
onClick={() => setAperto((v) => !v)} onClick={() => setAperto((v) => !v)}
disabled={!io} disabled={!io}
className="rounded-full bg-accent px-3 py-1 text-[10px] font-bold uppercase text-accent-foreground disabled:opacity-50" className="area-tocco rounded-full bg-accent px-3 py-1 text-3xs font-bold uppercase text-accent-foreground disabled:opacity-50"
> >
{mio ? "Cambia voto" : "Vota"} {mio ? "Cambia voto" : "Vota"}
</button> </button>
@@ -66,7 +66,7 @@ export function VotazioneMvp({ matchId }: { matchId: string }) {
)} )}
</p> </p>
{mio ? ( {mio ? (
<p className="mt-1 text-[11px] text-muted-foreground">Hai votato {mio.votato_nome}</p> <p className="mt-1 text-2xs text-muted-foreground">Hai votato {mio.votato_nome}</p>
) : null} ) : null}
{aperto ? ( {aperto ? (
@@ -91,7 +91,7 @@ export function VotazioneMvp({ matchId }: { matchId: string }) {
) : conteggio.length > 0 ? ( ) : conteggio.length > 0 ? (
<div className="mt-2 flex flex-wrap gap-1.5"> <div className="mt-2 flex flex-wrap gap-1.5">
{conteggio.map((c) => ( {conteggio.map((c) => (
<span key={c.id} className="rounded-lg bg-card px-2 py-1 text-[11px] font-semibold"> <span key={c.id} className="rounded-lg bg-card px-2 py-1 text-2xs font-semibold">
{c.nome} · {c.voti} {c.nome} · {c.voti}
</span> </span>
))} ))}
+6 -6
View File
@@ -50,7 +50,7 @@ export function VotoSocial({ matchId }: { matchId: string }) {
return ( return (
<div className="space-y-2"> <div className="space-y-2">
<div className="rounded-2xl bg-secondary/60 px-3 py-2 text-[11px] font-semibold text-muted-foreground"> <div className="rounded-2xl bg-secondary/60 px-3 py-2 text-2xs font-semibold text-muted-foreground">
<span className="inline-flex items-center gap-1.5"> <span className="inline-flex items-center gap-1.5">
<Sparkles className="h-3.5 w-3.5 text-accent" /> <Sparkles className="h-3.5 w-3.5 text-accent" />
Hai votato {fatti}/{categorieSocial.length} categorie · un solo voto per categoria Hai votato {fatti}/{categorieSocial.length} categorie · un solo voto per categoria
@@ -79,17 +79,17 @@ export function VotoSocial({ matchId }: { matchId: string }) {
</span> </span>
<span className="min-w-0 flex-1"> <span className="min-w-0 flex-1">
<span className="block truncate text-sm font-bold leading-tight">{cat.nome}</span> <span className="block truncate text-sm font-bold leading-tight">{cat.nome}</span>
<span className="block truncate text-[11px] text-muted-foreground"> <span className="block truncate text-2xs text-muted-foreground">
{mio ? `Hai votato ${mio.votato_nome}` : cat.descrizione} {mio ? `Hai votato ${mio.votato_nome}` : cat.descrizione}
</span> </span>
</span> </span>
<span <span
className={cn( className={cn(
"shrink-0 rounded-full px-2.5 py-1 text-[10px] font-bold uppercase", "shrink-0 rounded-full px-2.5 py-1 text-3xs font-bold uppercase",
mio ? "bg-success text-success-foreground" : "bg-accent text-accent-foreground", mio ? "bg-success text-success-foreground" : "bg-accent text-accent-foreground",
)} )}
> >
{mio ? <Check className="h-3 w-3" /> : "Vota"} {mio ? <Check className="h-3.5 w-3.5" /> : "Vota"}
</span> </span>
</button> </button>
@@ -117,7 +117,7 @@ export function VotoSocial({ matchId }: { matchId: string }) {
) : ( ) : (
<div className="border-t border-border px-3 py-2"> <div className="border-t border-border px-3 py-2">
{totale === 0 ? ( {totale === 0 ? (
<p className="text-[11px] text-muted-foreground"> <p className="text-2xs text-muted-foreground">
Nessun voto ancora: apri e scegli un compagno. Nessun voto ancora: apri e scegli un compagno.
</p> </p>
) : vincitore ? ( ) : vincitore ? (
@@ -127,7 +127,7 @@ export function VotoSocial({ matchId }: { matchId: string }) {
{vincitore.nome} · {vincitore.voti} {vincitore.voti === 1 ? "voto" : "voti"} {vincitore.nome} · {vincitore.voti} {vincitore.voti === 1 ? "voto" : "voti"}
</p> </p>
) : ( ) : (
<p className="text-[11px] text-muted-foreground"> <p className="text-2xs text-muted-foreground">
Parità con {totale} voti: servono altri voti per assegnare il badge. Parità con {totale} voti: servono altri voti per assegnare il badge.
</p> </p>
)} )}
+51 -23
View File
@@ -2,6 +2,8 @@ import { useState, type ReactNode } from "react";
import { ChevronDown } from "lucide-react"; import { ChevronDown } from "lucide-react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { statoMeta, type Stato } from "@/lib/crapp-data"; import { statoMeta, type Stato } from "@/lib/crapp-data";
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
import { Skeleton } from "@/components/ui/skeleton";
import { Reveal } from "@/components/motion/Reveal"; import { Reveal } from "@/components/motion/Reveal";
import { Numero } from "@/components/motion/Numero"; import { Numero } from "@/components/motion/Numero";
@@ -17,7 +19,7 @@ export function TeamLogo({ className }: { className?: string }) {
export function PageHeader({ titolo, sottotitolo }: { titolo: string; sottotitolo?: string }) { export function PageHeader({ titolo, sottotitolo }: { titolo: string; sottotitolo?: string }) {
return ( return (
<header className="bg-hero px-5 pb-8 pt-7 text-primary-foreground"> <header className="bg-hero px-5 pb-8 pt-[calc(1.75rem+env(safe-area-inset-top))] text-primary-foreground">
<div className="grid grid-cols-[minmax(0,1fr)_auto] items-center gap-4"> <div className="grid grid-cols-[minmax(0,1fr)_auto] items-center gap-4">
<div className="min-w-0"> <div className="min-w-0">
<h1 className="truncate font-display text-3xl uppercase">{titolo}</h1> <h1 className="truncate font-display text-3xl uppercase">{titolo}</h1>
@@ -31,6 +33,33 @@ export function PageHeader({ titolo, sottotitolo }: { titolo: string; sottotitol
); );
} }
/**
* Scheletro di pagina mostrato mentre i dati arrivano. Riserva lo spazio della
* struttura reale: meglio di `return null`, che lascia la schermata bianca e poi
* fa saltare il layout quando il contenuto compare.
*/
export function ScheletroPagina() {
return (
<div aria-busy="true" aria-live="polite">
<div className="bg-hero px-5 pb-10 pt-[calc(1.75rem+env(safe-area-inset-top))]">
<Skeleton className="h-3 w-24 bg-primary-foreground/20" />
<Skeleton className="mt-2 h-8 w-40 bg-primary-foreground/20" />
<div className="mt-6 grid grid-cols-3 gap-2">
{[0, 1, 2].map((i) => (
<Skeleton key={i} className="h-16 rounded-2xl bg-primary-foreground/10" />
))}
</div>
</div>
<div className="space-y-3 px-5 py-4">
{[0, 1, 2].map((i) => (
<Skeleton key={i} className="h-24 rounded-3xl" />
))}
</div>
<span className="sr-only">Caricamento in corso</span>
</div>
);
}
export function Section({ export function Section({
titolo, titolo,
azione, azione,
@@ -72,25 +101,24 @@ export function SezioneTendina({
const [aperta, setAperta] = useState(defaultAperta); const [aperta, setAperta] = useState(defaultAperta);
return ( return (
<Reveal as="section" indice={indice} className="px-5 py-4"> <Reveal as="section" indice={indice} className="px-5 py-4">
<button {/* Radix Collapsible (gia' fra le dipendenze) al posto di un `aperta ? ... : null`:
type="button" l'altezza viene animata, cosi' l'apertura non e' uno scatto secco. */}
onClick={() => setAperta((v) => !v)} <Collapsible open={aperta} onOpenChange={setAperta}>
className="mb-3 flex w-full items-center justify-between gap-3 text-left active:scale-[0.99]" <CollapsibleTrigger className="mb-3 flex w-full items-center justify-between gap-3 text-left transition-transform duration-100 ease-out active:scale-[0.99]">
aria-expanded={aperta} <h2 className="font-display text-lg uppercase tracking-wide">{titolo}</h2>
> <span className="flex shrink-0 items-center gap-2">
<h2 className="font-display text-lg uppercase tracking-wide">{titolo}</h2> {azione}
<span className="flex shrink-0 items-center gap-2"> <ChevronDown
{azione} className={cn(
<ChevronDown "h-4 w-4 text-muted-foreground transition-transform duration-200 ease-out",
className={cn( aperta && "rotate-180",
"h-4 w-4 text-muted-foreground transition-transform", )}
aperta && "rotate-180", />
)} </span>
/> </CollapsibleTrigger>
</span> {anteprima}
</button> <CollapsibleContent className="anim-tendina">{children}</CollapsibleContent>
{anteprima} </Collapsible>
{aperta ? children : null}
</Reveal> </Reveal>
); );
} }
@@ -100,7 +128,7 @@ export function StatoBadge({ stato, className }: { stato: Stato; className?: str
return ( return (
<span <span
className={cn( 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-2xs font-bold uppercase",
meta.className, meta.className,
className, className,
)} )}
@@ -124,10 +152,10 @@ export function StatTile({
<p className="font-display text-2xl leading-none"> <p className="font-display text-2xl leading-none">
{typeof valore === "number" ? <Numero valore={valore} /> : valore} {typeof valore === "number" ? <Numero valore={valore} /> : valore}
</p> </p>
<p className="mt-1 text-[11px] font-semibold uppercase tracking-wide text-muted-foreground"> <p className="mt-1 text-2xs font-semibold uppercase tracking-wide text-muted-foreground">
{label} {label}
</p> </p>
{hint ? <p className="mt-0.5 text-[11px] text-accent">{hint}</p> : null} {hint ? <p className="mt-0.5 text-2xs text-accent">{hint}</p> : null}
</div> </div>
); );
} }
+3 -1
View File
@@ -5,7 +5,9 @@ import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
const buttonVariants = cva( const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium cursor-pointer transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 disabled:cursor-not-allowed [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", // Il feedback vive sulla pressione, non sul rilascio: su touch `hover` non
// esiste e senza `active:` il bottone sembra morto finche' non alzi il dito.
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium cursor-pointer transition-[transform,background-color,color,border-color,box-shadow] duration-100 ease-out active:scale-[0.97] focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 disabled:cursor-not-allowed disabled:active:scale-100 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{ {
variants: { variants: {
variant: { variant: {
+6 -3
View File
@@ -23,7 +23,9 @@ const DrawerOverlay = React.forwardRef<
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<DrawerPrimitive.Overlay <DrawerPrimitive.Overlay
ref={ref} ref={ref}
className={cn("fixed inset-0 z-50 bg-black/80", className)} // 40%: la messa a fuoco la fa gia' il rientro in scala dello sfondo
// (shouldScaleBackground). Un velo all'80% cancella il contesto.
className={cn("fixed inset-0 z-50 bg-black/40", className)}
{...props} {...props}
/> />
)); ));
@@ -38,12 +40,13 @@ const DrawerContent = React.forwardRef<
<DrawerPrimitive.Content <DrawerPrimitive.Content
ref={ref} ref={ref}
className={cn( className={cn(
"fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background", "fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-2xl border bg-background",
className, className,
)} )}
{...props} {...props}
> >
<div className="mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted" /> {/* Maniglia nelle proporzioni di sistema (~5x36pt), non una barra spessa. */}
<div className="mx-auto mt-2.5 h-1.5 w-9 rounded-full bg-muted-foreground/40" />
{children} {children}
</DrawerPrimitive.Content> </DrawerPrimitive.Content>
</DrawerPortal> </DrawerPortal>
+30 -4
View File
@@ -84,7 +84,12 @@ export const Route = createRootRouteWithContext<{ queryClient: QueryClient }>()(
head: () => ({ head: () => ({
meta: [ meta: [
{ charSet: "utf-8" }, { charSet: "utf-8" },
{ name: "viewport", content: "width=device-width, initial-scale=1" }, // `viewport-fit=cover` non e' opzionale: senza, su iOS `env(safe-area-inset-*)`
// vale 0 e tutti i margini per notch e barra gesture diventano codice morto.
{
name: "viewport",
content: "width=device-width, initial-scale=1, viewport-fit=cover",
},
{ title: "CrAPP — L'app del CRAP Volley" }, { title: "CrAPP — L'app del CRAP Volley" },
{ {
name: "description", name: "description",
@@ -92,7 +97,18 @@ export const Route = createRootRouteWithContext<{ queryClient: QueryClient }>()(
"Convocazioni, presenze, statistiche e classifica del CRAP Volley in un'unica app mobile.", "Convocazioni, presenze, statistiche e classifica del CRAP Volley in un'unica app mobile.",
}, },
{ name: "author", content: "CRAP Volley" }, { name: "author", content: "CRAP Volley" },
{ name: "theme-color", content: "#111111" }, // Una tinta per tema: con un valore solo la barra di sistema resta nera
// anche in chiaro. I valori corrispondono a --background di styles.css.
{
name: "theme-color",
content: "#f9fafb",
media: "(prefers-color-scheme: light)",
},
{
name: "theme-color",
content: "#0c0d11",
media: "(prefers-color-scheme: dark)",
},
{ property: "og:title", content: "CrAPP — L'app del CRAP Volley" }, { property: "og:title", content: "CrAPP — L'app del CRAP Volley" },
{ {
property: "og:description", property: "og:description",
@@ -143,9 +159,19 @@ export const Route = createRootRouteWithContext<{ queryClient: QueryClient }>()(
function RootShell({ children }: { children: ReactNode }) { function RootShell({ children }: { children: ReactNode }) {
return ( return (
<html lang="en"> <html lang="it">
<head> <head>
<HeadContent /> <HeadContent />
{/* Tema scuro allineato al sistema. Sta nell'`head` come script bloccante
perche' applicato dopo il primo paint darebbe un lampo di chiaro. */}
<script
dangerouslySetInnerHTML={{
__html:
"(function(){try{var m=matchMedia('(prefers-color-scheme: dark)');" +
"var a=function(){document.documentElement.classList.toggle('dark',m.matches)};" +
"a();m.addEventListener('change',a)}catch(e){}})()",
}}
/>
</head> </head>
<body> <body>
{children} {children}
@@ -194,7 +220,7 @@ function AppShell() {
return ( return (
<> <>
<div className="mx-auto min-h-screen max-w-md bg-background pb-24"> <div className="mx-auto min-h-screen max-w-md bg-background pb-[calc(6rem+env(safe-area-inset-bottom))]">
{/* Required: nested routes render here. Removing <Outlet /> breaks all child routes. */} {/* Required: nested routes render here. Removing <Outlet /> breaks all child routes. */}
<Outlet /> <Outlet />
</div> </div>
+4 -4
View File
@@ -90,7 +90,7 @@ function messaggioErrore(e: unknown, fallback: string): string {
function Campo({ label, children }: { label: string; children: React.ReactNode }) { function Campo({ label, children }: { label: string; children: React.ReactNode }) {
return ( return (
<label className="block"> <label className="block">
<span className="text-[10px] font-bold uppercase tracking-wide text-muted-foreground"> <span className="text-3xs font-bold uppercase tracking-wide text-muted-foreground">
{label} {label}
</span> </span>
<span className="mt-1 block">{children}</span> <span className="mt-1 block">{children}</span>
@@ -375,13 +375,13 @@ function Documento({
onClick={scarica} onClick={scarica}
disabled={!path || inCorso} disabled={!path || inCorso}
className={cn( className={cn(
"premi flex items-center gap-1.5 rounded-full px-2.5 py-1 text-[11px] font-bold uppercase disabled:opacity-60", "premi flex items-center gap-1.5 rounded-full px-2.5 py-1 text-2xs font-bold uppercase disabled:opacity-60",
statoClasse[stato], statoClasse[stato],
)} )}
> >
{inCorso ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : icona} {inCorso ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : icona}
{label} {label}
{path ? <Download className="h-3 w-3" /> : null} {path ? <Download className="h-3.5 w-3.5" /> : null}
</button> </button>
); );
} }
@@ -464,7 +464,7 @@ function SchedaGiocatore({
/> />
<span <span
className={cn( className={cn(
"flex items-center gap-1.5 rounded-full px-2.5 py-1 text-[11px] font-bold uppercase", "flex items-center gap-1.5 rounded-full px-2.5 py-1 text-2xs font-bold uppercase",
statoClasse[g.numeroTessera ? "presente" : "assente"], statoClasse[g.numeroTessera ? "presente" : "assente"],
)} )}
> >
+5 -9
View File
@@ -129,12 +129,8 @@ function Calendario() {
: []; : [];
// Prossimi 4 eventi da oggi in avanti (indipendenti dal mese selezionato nella griglia). // Prossimi 4 eventi da oggi in avanti (indipendenti dal mese selezionato nella griglia).
const oggiIso = oggi const oggiIso = oggi ? `${oggi.anno}-${pad2(oggi.mese + 1)}-${pad2(oggi.giorno)}` : null;
? `${oggi.anno}-${pad2(oggi.mese + 1)}-${pad2(oggi.giorno)}` const prossimiEventi = oggiIso ? eventi.filter((e) => e.data >= oggiIso).slice(0, 4) : [];
: null;
const prossimiEventi = oggiIso
? eventi.filter((e) => e.data >= oggiIso).slice(0, 4)
: [];
return ( return (
<> <>
@@ -183,7 +179,7 @@ function Calendario() {
</button> </button>
</div> </div>
<div className="grid grid-cols-7 gap-1 text-center text-[11px] font-bold text-muted-foreground"> <div className="grid grid-cols-7 gap-1 text-center text-2xs font-bold text-muted-foreground">
{giorniIT.map((g, i) => ( {giorniIT.map((g, i) => (
<span key={i}>{g}</span> <span key={i}>{g}</span>
))} ))}
@@ -244,7 +240,7 @@ function Calendario() {
); );
})} })}
</div> </div>
<div className="mt-4 flex flex-wrap gap-3 text-[11px] font-semibold text-muted-foreground"> <div className="mt-4 flex flex-wrap gap-3 text-2xs font-semibold text-muted-foreground">
<span className="inline-flex items-center gap-1"> <span className="inline-flex items-center gap-1">
<i className="h-2.5 w-2.5 rounded-full bg-accent" /> Partita <i className="h-2.5 w-2.5 rounded-full bg-accent" /> Partita
</span> </span>
@@ -306,7 +302,7 @@ function Calendario() {
<DrawerTitle className="font-display text-2xl uppercase tracking-wide"> <DrawerTitle className="font-display text-2xl uppercase tracking-wide">
{giornoSelezionato ? `${giornoSelezionato} ${mesiIT[mese]}` : "Eventi"} {giornoSelezionato ? `${giornoSelezionato} ${mesiIT[mese]}` : "Eventi"}
</DrawerTitle> </DrawerTitle>
<DrawerClose className="absolute right-0 top-1 grid h-8 w-8 place-items-center rounded-full bg-secondary text-foreground transition-transform active:scale-90"> <DrawerClose className="area-tocco absolute right-0 top-1 grid h-8 w-8 place-items-center rounded-full bg-secondary text-foreground transition-transform active:scale-90">
<X className="h-4 w-4" /> <X className="h-4 w-4" />
<span className="sr-only">Chiudi</span> <span className="sr-only">Chiudi</span>
</DrawerClose> </DrawerClose>
+4 -4
View File
@@ -83,7 +83,7 @@ function Classifica() {
<Section titolo="Classifica"> <Section titolo="Classifica">
<div className="overflow-hidden rounded-3xl bg-card shadow-card"> <div className="overflow-hidden rounded-3xl bg-card shadow-card">
<div className="grid grid-cols-[2rem_minmax(0,1fr)_2rem_2.5rem_2.5rem] gap-2 border-b border-border px-3 py-2 text-[10px] font-bold uppercase text-muted-foreground"> <div className="grid grid-cols-[2rem_minmax(0,1fr)_2rem_2.5rem_2.5rem] gap-2 border-b border-border px-3 py-2 text-3xs font-bold uppercase text-muted-foreground">
<span>#</span> <span>#</span>
<span>Squadra</span> <span>Squadra</span>
<span className="text-center">G</span> <span className="text-center">G</span>
@@ -141,7 +141,7 @@ function Classifica() {
{m.casa ? "CRAP Volley" : m.avversario} vs{" "} {m.casa ? "CRAP Volley" : m.avversario} vs{" "}
{m.casa ? m.avversario : "CRAP Volley"} {m.casa ? m.avversario : "CRAP Volley"}
</p> </p>
<p className="text-[11px] text-muted-foreground"> <p className="text-2xs text-muted-foreground">
{formatData(m.data)} · MVP {m.mvp || "da votare"} {formatData(m.data)} · MVP {m.mvp || "da votare"}
{m.scout ? " · scoutata" : ""} {m.scout ? " · scoutata" : ""}
</p> </p>
@@ -162,7 +162,7 @@ function Classifica() {
<span <span
key={i} key={i}
className={cn( className={cn(
"rounded-lg px-2 py-1 text-[11px] font-semibold tabular-nums", "rounded-lg px-2 py-1 text-2xs font-semibold tabular-nums",
p[0] > p[1] ? "bg-secondary" : "bg-muted text-muted-foreground", p[0] > p[1] ? "bg-secondary" : "bg-muted text-muted-foreground",
)} )}
> >
@@ -170,7 +170,7 @@ function Classifica() {
</span> </span>
))} ))}
{eventoId && !m.mvp ? ( {eventoId && !m.mvp ? (
<span className="ml-auto inline-flex items-center gap-0.5 text-[11px] font-bold uppercase text-accent"> <span className="ml-auto inline-flex items-center gap-0.5 text-2xs font-bold uppercase text-accent">
Vota MVP <ChevronRight className="h-3.5 w-3.5" /> Vota MVP <ChevronRight className="h-3.5 w-3.5" />
</span> </span>
) : null} ) : null}
+4 -4
View File
@@ -134,7 +134,7 @@ function GestioneEventi() {
type="button" type="button"
onClick={() => aggiorna(daCategoria(t.id))} onClick={() => aggiorna(daCategoria(t.id))}
className={cn( className={cn(
"rounded-full py-2 text-[11px] font-bold uppercase transition-colors", "rounded-full py-2 text-2xs font-bold uppercase transition-colors",
categoriaEvento(bozza) === t.id categoriaEvento(bozza) === t.id
? "bg-card shadow-card text-foreground" ? "bg-card shadow-card text-foreground"
: "text-muted-foreground", : "text-muted-foreground",
@@ -291,7 +291,7 @@ function GestioneEventi() {
> >
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<p className="truncate text-sm font-bold leading-tight">{e.titolo}</p> <p className="truncate text-sm font-bold leading-tight">{e.titolo}</p>
<p className="text-[11px] text-muted-foreground"> <p className="text-2xs text-muted-foreground">
{formatData(e.data)} · {e.ora} · {e.luogo || "luogo da definire"} {formatData(e.data)} · {e.ora} · {e.luogo || "luogo da definire"}
</p> </p>
</div> </div>
@@ -323,7 +323,7 @@ function GestioneEventi() {
function Campo({ label, children }: { label: string; children: React.ReactNode }) { function Campo({ label, children }: { label: string; children: React.ReactNode }) {
return ( return (
<label className="block"> <label className="block">
<span className="text-[10px] font-bold uppercase tracking-wide text-muted-foreground"> <span className="text-3xs font-bold uppercase tracking-wide text-muted-foreground">
{label} {label}
</span> </span>
<span className="mt-1 block">{children}</span> <span className="mt-1 block">{children}</span>
@@ -345,7 +345,7 @@ function Interruttore({
type="button" type="button"
onClick={onClick} onClick={onClick}
className={cn( className={cn(
"rounded-full px-3 py-1.5 text-[11px] font-bold uppercase transition-colors", "rounded-full px-3 py-1.5 text-2xs font-bold uppercase transition-colors",
attivo ? "bg-accent text-accent-foreground" : "bg-secondary text-muted-foreground", attivo ? "bg-accent text-accent-foreground" : "bg-secondary text-muted-foreground",
)} )}
> >
+12 -9
View File
@@ -2,7 +2,7 @@ import { createFileRoute, Link } from "@tanstack/react-router";
import { Flame, ChevronRight } from "lucide-react"; import { Flame, ChevronRight } from "lucide-react";
import { EventoCard, linkPerEvento } from "@/components/crapp/EventoCard"; import { EventoCard, linkPerEvento } from "@/components/crapp/EventoCard";
import { PromemoriaPalloni } from "@/components/crapp/PromemoriaPalloni"; import { PromemoriaPalloni } from "@/components/crapp/PromemoriaPalloni";
import { Section, StatTile, TeamLogo } from "@/components/crapp/ui-bits"; import { ScheletroPagina, Section, StatTile, TeamLogo } from "@/components/crapp/ui-bits";
import { CompletaProfilo } from "@/components/crapp/ProfiloAmministrativo"; import { CompletaProfilo } from "@/components/crapp/ProfiloAmministrativo";
import { Reveal } from "@/components/motion/Reveal"; import { Reveal } from "@/components/motion/Reveal";
import { Barra } from "@/components/motion/Barra"; import { Barra } from "@/components/motion/Barra";
@@ -59,11 +59,14 @@ function Index() {
? (eventi.find((e) => e.tipo === "partita" && e.data === ultima.data) ?? null) ? (eventi.find((e) => e.tipo === "partita" && e.data === ultima.data) ?? null)
: null; : null;
if (!giocatore) return null; if (!giocatore) return <ScheletroPagina />;
return ( return (
<> <>
<Reveal as="section" className="bg-hero px-5 pb-10 pt-7 text-primary-foreground"> <Reveal
as="section"
className="bg-hero px-5 pb-10 pt-[calc(1.75rem+env(safe-area-inset-top))] text-primary-foreground"
>
<div className="grid grid-cols-[minmax(0,1fr)_auto] items-center gap-4"> <div className="grid grid-cols-[minmax(0,1fr)_auto] items-center gap-4">
<div className="min-w-0"> <div className="min-w-0">
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-primary-foreground/60"> <p className="text-xs font-semibold uppercase tracking-[0.2em] text-primary-foreground/60">
@@ -77,20 +80,20 @@ function Index() {
<div className="mt-6 grid grid-cols-3 gap-2 text-center"> <div className="mt-6 grid grid-cols-3 gap-2 text-center">
<div className="rounded-2xl bg-primary-foreground/10 p-3"> <div className="rounded-2xl bg-primary-foreground/10 p-3">
<p className="font-display text-2xl leading-none">{noi ? `${noi.pos}º` : "—"}</p> <p className="font-display text-2xl leading-none">{noi ? `${noi.pos}º` : "—"}</p>
<p className="text-[10px] uppercase text-primary-foreground/60">In classifica</p> <p className="text-3xs uppercase text-primary-foreground/60">In classifica</p>
</div> </div>
<div className="rounded-2xl bg-primary-foreground/10 p-3"> <div className="rounded-2xl bg-primary-foreground/10 p-3">
<p className="font-display text-2xl leading-none"> <p className="font-display text-2xl leading-none">
{noi ? `${noi.vinte}-${noi.perse}` : "—"} {noi ? `${noi.vinte}-${noi.perse}` : "—"}
</p> </p>
<p className="text-[10px] uppercase text-primary-foreground/60">Bilancio</p> <p className="text-3xs uppercase text-primary-foreground/60">Bilancio</p>
</div> </div>
<div className="rounded-2xl bg-primary-foreground/10 p-3"> <div className="rounded-2xl bg-primary-foreground/10 p-3">
<p className="inline-flex items-center gap-1 font-display text-2xl leading-none"> <p className="inline-flex items-center gap-1 font-display text-2xl leading-none">
<Flame className="h-4 w-4 text-accent" /> <Flame className="h-4 w-4 text-accent" />
{giocatore.streak} {giocatore.streak}
</p> </p>
<p className="text-[10px] uppercase text-primary-foreground/60">Streak</p> <p className="text-3xs uppercase text-primary-foreground/60">Streak</p>
</div> </div>
</div> </div>
</Reveal> </Reveal>
@@ -161,7 +164,7 @@ function Index() {
{ultima.parziali.map((p, i) => ( {ultima.parziali.map((p, i) => (
<span <span
key={i} key={i}
className="rounded-lg bg-secondary px-2 py-1 text-[11px] font-semibold tabular-nums" className="rounded-lg bg-secondary px-2 py-1 text-2xs font-semibold tabular-nums"
> >
{p[0]}-{p[1]} {p[0]}-{p[1]}
</span> </span>
@@ -196,7 +199,7 @@ function Index() {
to="/squadra" to="/squadra"
className="inline-flex items-center text-xs font-semibold text-accent" className="inline-flex items-center text-xs font-semibold text-accent"
> >
Tutti <ChevronRight className="h-3 w-3" /> Tutti <ChevronRight className="h-3.5 w-3.5" />
</Link> </Link>
} }
> >
@@ -213,7 +216,7 @@ function Index() {
<p className="mt-1 text-xs font-semibold text-accent"> <p className="mt-1 text-xs font-semibold text-accent">
{microcopyObiettivo(obiettivo)} {microcopyObiettivo(obiettivo)}
</p> </p>
<p className="mt-1 text-[11px] text-muted-foreground">{obiettivo.impatto}</p> <p className="mt-1 text-2xs text-muted-foreground">{obiettivo.impatto}</p>
</div> </div>
) : null} ) : null}
</Section> </Section>
+2 -2
View File
@@ -220,13 +220,13 @@ function PartitaDetail() {
].map((t) => ( ].map((t) => (
<div key={t.l} className="rounded-2xl bg-secondary p-2.5"> <div key={t.l} className="rounded-2xl bg-secondary p-2.5">
<p className="font-display text-xl leading-none">{t.v}</p> <p className="font-display text-xl leading-none">{t.v}</p>
<p className="mt-1 text-[10px] font-semibold uppercase text-muted-foreground"> <p className="mt-1 text-3xs font-semibold uppercase text-muted-foreground">
{t.l} {t.l}
</p> </p>
</div> </div>
))} ))}
</div> </div>
<p className="mt-4 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"> <p className="mt-4 text-2xs font-bold uppercase tracking-wide text-muted-foreground">
Dettaglio giocatori (uso interno allenatori) Dettaglio giocatori (uso interno allenatori)
</p> </p>
<div className="mt-2 space-y-1"> <div className="mt-2 space-y-1">
+11 -7
View File
@@ -3,7 +3,13 @@ import { useEffect, useRef, useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import { Flame, Camera, Trash2, Bell, LogOut, ShieldCheck, Bug, Lightbulb } from "lucide-react"; import { Flame, Camera, Trash2, Bell, LogOut, ShieldCheck, Bug, Lightbulb } from "lucide-react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { PageHeader, Section, SezioneTendina, StatTile } from "@/components/crapp/ui-bits"; import {
PageHeader,
ScheletroPagina,
Section,
SezioneTendina,
StatTile,
} from "@/components/crapp/ui-bits";
import { Avatar } from "@/components/crapp/Avatar"; import { Avatar } from "@/components/crapp/Avatar";
import { import {
caricaAvatar, caricaAvatar,
@@ -66,7 +72,7 @@ function Profilo() {
.catch(() => setNotifiche(false)); .catch(() => setNotifiche(false));
}, []); }, []);
if (!g) return null; if (!g) return <ScheletroPagina />;
async function cambiaNotifiche() { async function cambiaNotifiche() {
if (!g || inCorso) return; if (!g || inCorso) return;
@@ -97,9 +103,7 @@ function Profilo() {
} }
} }
const percPresenze = g.totaliEventi const percPresenze = g.totaliEventi ? Math.round((g.presenze / g.totaliEventi) * 100) : 0;
? Math.round((g.presenze / g.totaliEventi) * 100)
: 0;
async function onFile(e: React.ChangeEvent<HTMLInputElement>) { async function onFile(e: React.ChangeEvent<HTMLInputElement>) {
const file = e.target.files?.[0]; const file = e.target.files?.[0];
@@ -135,7 +139,7 @@ function Profilo() {
<button <button
type="button" type="button"
onClick={() => inputRef.current?.click()} onClick={() => inputRef.current?.click()}
className="absolute -bottom-1 -right-1 grid h-8 w-8 place-items-center rounded-full bg-accent-grad text-accent-foreground shadow-pop" className="area-tocco absolute -bottom-1 -right-1 grid h-8 w-8 place-items-center rounded-full bg-accent-grad text-accent-foreground shadow-pop"
aria-label="Cambia foto" aria-label="Cambia foto"
> >
<Camera className="h-4 w-4" /> <Camera className="h-4 w-4" />
@@ -217,7 +221,7 @@ function Profilo() {
> >
<span className="min-w-0"> <span className="min-w-0">
<span className="block truncate">Notifiche turno palloni</span> <span className="block truncate">Notifiche turno palloni</span>
<span className="block text-[11px] text-muted-foreground"> <span className="block text-2xs text-muted-foreground">
{supportate {supportate
? notifiche ? notifiche
? "Attive su questo dispositivo" ? "Attive su questo dispositivo"
+14 -14
View File
@@ -320,9 +320,9 @@ function ScoutBoard({
return ( return (
<> <>
<header className="sticky top-0 z-30 bg-hero px-5 pb-4 pt-6 text-primary-foreground"> <header className="sticky top-0 z-30 bg-hero px-5 pb-4 pt-[calc(1.5rem+env(safe-area-inset-top))] text-primary-foreground">
<div className="flex items-center justify-between gap-3"> <div className="flex items-center justify-between gap-3">
<span className="inline-flex items-center gap-1.5 rounded-full bg-primary-foreground/10 px-2.5 py-1 text-[10px] font-bold uppercase tracking-wide"> <span className="inline-flex items-center gap-1.5 rounded-full bg-primary-foreground/10 px-2.5 py-1 text-3xs font-bold uppercase tracking-wide">
<Radio className="h-3 w-3 text-accent" /> Scout live · Set {setCorrente} <Radio className="h-3 w-3 text-accent" /> Scout live · Set {setCorrente}
</span> </span>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@@ -330,14 +330,14 @@ function ScoutBoard({
type="button" type="button"
onClick={annulla} onClick={annulla}
disabled={azioniSet.length === 0} disabled={azioniSet.length === 0}
className="inline-flex items-center gap-1 rounded-full bg-primary-foreground/10 px-3 py-1.5 text-xs font-bold disabled:opacity-40" className="area-tocco inline-flex items-center gap-1 rounded-full bg-primary-foreground/10 px-3 py-1.5 text-xs font-bold disabled:opacity-40"
> >
<Undo2 className="h-3.5 w-3.5" /> Annulla <Undo2 className="h-3.5 w-3.5" /> Annulla
</button> </button>
<button <button
type="button" type="button"
onClick={onFine} onClick={onFine}
className="inline-flex items-center gap-1 rounded-full bg-primary-foreground/10 px-3 py-1.5 text-xs font-bold" className="area-tocco inline-flex items-center gap-1 rounded-full bg-primary-foreground/10 px-3 py-1.5 text-xs font-bold"
> >
<LogOut className="h-3.5 w-3.5" /> Rilascia <LogOut className="h-3.5 w-3.5" /> Rilascia
</button> </button>
@@ -346,14 +346,14 @@ function ScoutBoard({
<div className="mt-3 grid grid-cols-[1fr_auto_1fr] items-center gap-2 text-center"> <div className="mt-3 grid grid-cols-[1fr_auto_1fr] items-center gap-2 text-center">
<div className="min-w-0"> <div className="min-w-0">
<p className="truncate text-[11px] uppercase text-primary-foreground/60">CRAP Volley</p> <p className="truncate text-2xs uppercase text-primary-foreground/60">CRAP Volley</p>
<p className="font-display text-5xl leading-none text-accent">{puntiNoi}</p> <p className="font-display text-5xl leading-none text-accent">{puntiNoi}</p>
</div> </div>
<p className="font-display text-2xl leading-none text-primary-foreground/50"> <p className="font-display text-2xl leading-none text-primary-foreground/50">
{setNostri}-{setLoro} {setNostri}-{setLoro}
</p> </p>
<div className="min-w-0"> <div className="min-w-0">
<p className="truncate text-[11px] uppercase text-primary-foreground/60"> <p className="truncate text-2xs uppercase text-primary-foreground/60">
{avversario || "Avversario"} {avversario || "Avversario"}
</p> </p>
<p className="font-display text-5xl leading-none">{puntiLoro}</p> <p className="font-display text-5xl leading-none">{puntiLoro}</p>
@@ -365,7 +365,7 @@ function ScoutBoard({
{setChiusi.map((p, i) => ( {setChiusi.map((p, i) => (
<span <span
key={i} key={i}
className="rounded-lg bg-primary-foreground/10 px-2 py-0.5 text-[11px] font-semibold tabular-nums" className="rounded-lg bg-primary-foreground/10 px-2 py-0.5 text-2xs font-semibold tabular-nums"
> >
{p[0]}-{p[1]} {p[0]}-{p[1]}
</span> </span>
@@ -375,7 +375,7 @@ function ScoutBoard({
</header> </header>
<section className="px-5 pt-4"> <section className="px-5 pt-4">
<p className="mb-2 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"> <p className="mb-2 text-2xs font-bold uppercase tracking-wide text-muted-foreground">
1. Tocca il giocatore 1. Tocca il giocatore
</p> </p>
<div className="grid grid-cols-4 gap-2"> <div className="grid grid-cols-4 gap-2">
@@ -393,10 +393,10 @@ function ScoutBoard({
)} )}
> >
<p className="font-display text-2xl leading-none">{g.numero}</p> <p className="font-display text-2xl leading-none">{g.numero}</p>
<p className="mt-1 text-[10px] font-bold leading-tight">{g.nome}</p> <p className="mt-1 text-3xs font-bold leading-tight">{g.nome}</p>
<p <p
className={cn( className={cn(
"text-[10px] tabular-nums", "text-3xs tabular-nums",
attivo ? "text-accent-foreground/70" : "text-muted-foreground", attivo ? "text-accent-foreground/70" : "text-muted-foreground",
)} )}
> >
@@ -409,7 +409,7 @@ function ScoutBoard({
</section> </section>
<section className="px-5 pt-4"> <section className="px-5 pt-4">
<p className="mb-2 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"> <p className="mb-2 text-2xs font-bold uppercase tracking-wide text-muted-foreground">
2. Tocca l'azione 2. Tocca l'azione
</p> </p>
<div className="grid grid-cols-2 gap-2"> <div className="grid grid-cols-2 gap-2">
@@ -447,7 +447,7 @@ function ScoutBoard({
<section className="px-5 pt-4"> <section className="px-5 pt-4">
<div className="rounded-3xl bg-card p-3 shadow-card"> <div className="rounded-3xl bg-card p-3 shadow-card">
<p className="text-[11px] font-bold uppercase tracking-wide text-muted-foreground"> <p className="text-2xs font-bold uppercase tracking-wide text-muted-foreground">
Ultime azioni Ultime azioni
</p> </p>
{ultime.length === 0 ? ( {ultime.length === 0 ? (
@@ -467,7 +467,7 @@ function ScoutBoard({
<span className="truncate">{g ? `#${g.numero} ${g.nome}` : "Avversario"}</span> <span className="truncate">{g ? `#${g.numero} ${g.nome}` : "Avversario"}</span>
<span <span
className={cn( className={cn(
"shrink-0 rounded-full px-2 py-0.5 text-[10px] font-bold uppercase", "shrink-0 rounded-full px-2 py-0.5 text-3xs font-bold uppercase",
azioniMeta[a.tipo].className, azioniMeta[a.tipo].className,
)} )}
> >
@@ -483,7 +483,7 @@ function ScoutBoard({
<section className="px-5 pt-4"> <section className="px-5 pt-4">
<div className="rounded-3xl bg-card p-4 shadow-card"> <div className="rounded-3xl bg-card p-4 shadow-card">
<label className="text-[11px] font-bold uppercase tracking-wide text-muted-foreground"> <label className="text-2xs font-bold uppercase tracking-wide text-muted-foreground">
Avversario Avversario
</label> </label>
<input <input
+15 -22
View File
@@ -30,7 +30,7 @@ import {
function RuoloBadge({ ruolo }: { ruolo: string }) { function RuoloBadge({ ruolo }: { ruolo: string }) {
return ( return (
<span className="inline-flex items-center rounded-full bg-accent px-2 py-0.5 text-[10px] font-bold uppercase tracking-wide text-accent-foreground"> <span className="inline-flex items-center rounded-full bg-accent px-2 py-0.5 text-3xs font-bold uppercase tracking-wide text-accent-foreground">
{ruolo} {ruolo}
</span> </span>
); );
@@ -125,7 +125,7 @@ function Squadra() {
<span className="block truncate text-sm font-bold leading-tight">{g.nome}</span> <span className="block truncate text-sm font-bold leading-tight">{g.nome}</span>
<span className="mt-1 flex items-center gap-2"> <span className="mt-1 flex items-center gap-2">
<RuoloBadge ruolo={g.ruolo} /> <RuoloBadge ruolo={g.ruolo} />
<span className="text-[11px] text-muted-foreground"> <span className="text-2xs text-muted-foreground">
{g.numero ? `#${g.numero}` : "n° da definire"} {g.numero ? `#${g.numero}` : "n° da definire"}
</span> </span>
</span> </span>
@@ -136,7 +136,7 @@ function Squadra() {
const Icon = b.def.icon; const Icon = b.def.icon;
return ( return (
<span key={b.def.id} className="cursor-help" title={b.def.nome}> <span key={b.def.id} className="cursor-help" title={b.def.nome}>
<Icon className={cn("h-3 w-3", gradoMeta[b.grado!].text)} /> <Icon className={cn("h-3.5 w-3.5", gradoMeta[b.grado!].text)} />
</span> </span>
); );
})} })}
@@ -170,14 +170,14 @@ function Squadra() {
].map((s) => ( ].map((s) => (
<div key={s.l} className="rounded-2xl bg-secondary p-2.5 text-center"> <div key={s.l} className="rounded-2xl bg-secondary p-2.5 text-center">
<p className="font-display text-xl leading-none">{s.v}</p> <p className="font-display text-xl leading-none">{s.v}</p>
<p className="mt-1 text-[10px] font-semibold uppercase text-muted-foreground"> <p className="mt-1 text-3xs font-semibold uppercase text-muted-foreground">
{s.l} {s.l}
</p> </p>
</div> </div>
))} ))}
</div> </div>
<p className="mt-4 text-[11px] font-bold uppercase tracking-wide text-muted-foreground"> <p className="mt-4 text-2xs font-bold uppercase tracking-wide text-muted-foreground">
Badge sbloccati · {collezioneBadge(g).ottenuti}/{collezioneBadge(g).totali} Badge sbloccati · {collezioneBadge(g).ottenuti}/{collezioneBadge(g).totali}
</p> </p>
{sbloccati.length === 0 ? ( {sbloccati.length === 0 ? (
@@ -194,18 +194,13 @@ function Squadra() {
<div className={cn("rounded-2xl p-2.5 ring-1", meta.bg, meta.ring)}> <div className={cn("rounded-2xl p-2.5 ring-1", meta.bg, meta.ring)}>
<Icon className={cn("h-4 w-4", meta.text)} /> <Icon className={cn("h-4 w-4", meta.text)} />
<p className="mt-1 text-xs font-bold leading-tight">{b.def.nome}</p> <p className="mt-1 text-xs font-bold leading-tight">{b.def.nome}</p>
<p className="text-[10px] text-muted-foreground"> <p className="text-3xs text-muted-foreground">
{b.valore} {b.def.unita} {b.valore} {b.def.unita}
{b.prossimaSoglia {b.prossimaSoglia
? ` · ${b.prossimaSoglia} per ${gradoMeta[b.prossimo!].label.toLowerCase()}` ? ` · ${b.prossimaSoglia} per ${gradoMeta[b.prossimo!].label.toLowerCase()}`
: ""} : ""}
</p> </p>
<p <p className={cn("mt-1.5 text-3xs font-bold uppercase", meta.text)}>
className={cn(
"mt-1.5 text-[10px] font-bold uppercase",
meta.text,
)}
>
{meta.label} {meta.label}
</p> </p>
</div> </div>
@@ -241,7 +236,7 @@ function Squadra() {
type="button" type="button"
onClick={() => setCriterio(c.id)} onClick={() => setCriterio(c.id)}
className={cn( className={cn(
"min-w-0 flex-1 truncate rounded-full px-1.5 py-1.5 text-center text-[10px] font-bold uppercase transition-colors", "min-w-0 flex-1 truncate rounded-full px-1.5 py-1.5 text-center text-3xs font-bold uppercase transition-colors",
criterio === c.id criterio === c.id
? "bg-accent text-accent-foreground shadow-pop" ? "bg-accent text-accent-foreground shadow-pop"
: "bg-secondary text-muted-foreground", : "bg-secondary text-muted-foreground",
@@ -264,7 +259,7 @@ function Squadra() {
{g.nome} {g.nome}
{i === 0 ? <Crown className="ml-1 inline h-3.5 w-3.5 text-warning" /> : null} {i === 0 ? <Crown className="ml-1 inline h-3.5 w-3.5 text-warning" /> : null}
</p> </p>
<p className="truncate text-[11px] text-muted-foreground"> <p className="truncate text-2xs text-muted-foreground">
#{g.numero} · {g.ruolo} · {g.streak} presenze consecutive #{g.numero} · {g.ruolo} · {g.streak} presenze consecutive
</p> </p>
</div> </div>
@@ -290,7 +285,7 @@ function Squadra() {
<div className="mb-3 rounded-3xl bg-hero p-4 text-primary-foreground shadow-card"> <div className="mb-3 rounded-3xl bg-hero p-4 text-primary-foreground shadow-card">
<div className="flex items-end justify-between gap-3"> <div className="flex items-end justify-between gap-3">
<div> <div>
<p className="text-[11px] font-semibold uppercase tracking-wide text-primary-foreground/60"> <p className="text-2xs font-semibold uppercase tracking-wide text-primary-foreground/60">
Progresso collettivo Progresso collettivo
</p> </p>
<p className="font-display text-4xl leading-none"> <p className="font-display text-4xl leading-none">
@@ -326,7 +321,7 @@ function Squadra() {
</div> </div>
<span <span
className={cn( className={cn(
"rounded-full px-2 py-0.5 text-[10px] font-bold uppercase tracking-wide", "rounded-full px-2 py-0.5 text-3xs font-bold uppercase tracking-wide",
fatto fatto
? "bg-success text-success-foreground" ? "bg-success text-success-foreground"
: "bg-secondary text-muted-foreground", : "bg-secondary text-muted-foreground",
@@ -341,7 +336,7 @@ function Squadra() {
{o.scadenza ? ` · entro il ${formatData(o.scadenza)}` : ""} {o.scadenza ? ` · entro il ${formatData(o.scadenza)}` : ""}
</p> </p>
<p className="mt-1 text-xs font-semibold text-accent">{microcopyObiettivo(o)}</p> <p className="mt-1 text-xs font-semibold text-accent">{microcopyObiettivo(o)}</p>
<p className="mt-0.5 text-[11px] text-muted-foreground">{o.impatto}</p> <p className="mt-0.5 text-2xs text-muted-foreground">{o.impatto}</p>
</Reveal> </Reveal>
); );
})} })}
@@ -358,9 +353,7 @@ function Squadra() {
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Icon className="h-5 w-5 text-accent" /> <Icon className="h-5 w-5 text-accent" />
<p className="text-sm font-bold leading-tight">{b.nome}</p> <p className="text-sm font-bold leading-tight">{b.nome}</p>
<p className="ml-auto text-[10px] text-muted-foreground"> <p className="ml-auto text-3xs text-muted-foreground">{descrizioneSoglie(b)}</p>
{descrizioneSoglie(b)}
</p>
</div> </div>
<div className="mt-2 grid grid-cols-3 gap-1.5"> <div className="mt-2 grid grid-cols-3 gap-1.5">
{gradiOrdine.map((grado) => { {gradiOrdine.map((grado) => {
@@ -376,11 +369,11 @@ function Squadra() {
key={grado} key={grado}
className={cn("rounded-2xl p-2 text-center ring-1", meta.bg, meta.ring)} className={cn("rounded-2xl p-2 text-center ring-1", meta.bg, meta.ring)}
> >
<p className={cn("text-[10px] font-bold uppercase", meta.text)}> <p className={cn("text-3xs font-bold uppercase", meta.text)}>
{meta.label} {meta.label}
</p> </p>
<p className="font-display text-lg leading-none">{b.soglie[grado]}</p> <p className="font-display text-lg leading-none">{b.soglie[grado]}</p>
<p className="text-[10px] text-muted-foreground"> <p className="text-3xs text-muted-foreground">
{quanti}/{rosa.length} {quanti}/{rosa.length}
</p> </p>
</div> </div>
+304 -29
View File
@@ -7,6 +7,57 @@
@theme { @theme {
--font-display: "Bebas Neue", ui-sans-serif, sans-serif; --font-display: "Bebas Neue", ui-sans-serif, sans-serif;
--font-sans: "Barlow", ui-sans-serif, system-ui, sans-serif; --font-sans: "Barlow", ui-sans-serif, system-ui, sans-serif;
/*
* Scala tipografica: dimensioni in rem (scalano con l'impostazione di testo
* del sistema) con crenatura e interlinea *specifiche per taglia*.
* Regola: piu' il testo e' grande, piu' le lettere vanno strette e le righe
* vicine; piu' e' piccolo, piu' serve aria fra le lettere per leggerlo.
* Non usare `text-[Npx]`: aggiungere semmai una taglia qui.
*
* Icone (classi Tailwind sui componenti lucide): quattro misure, nessun
* valore fuori scala — h-3.5 inline nel testo piccolo, h-4 standard,
* h-5 nelle barre di navigazione, h-6 nei blocchi grandi.
*/
--text-3xs: 0.625rem; /* 10px */
--text-3xs--line-height: 1.4;
--text-3xs--letter-spacing: 0.015em;
--text-2xs: 0.6875rem; /* 11px */
--text-2xs--line-height: 1.4;
--text-2xs--letter-spacing: 0.01em;
--text-xs: 0.75rem;
--text-xs--line-height: 1.4;
--text-xs--letter-spacing: 0.005em;
--text-sm: 0.875rem;
--text-sm--line-height: 1.45;
--text-sm--letter-spacing: 0em;
--text-base: 1rem;
--text-base--line-height: 1.5;
--text-base--letter-spacing: 0em;
--text-lg: 1.125rem;
--text-lg--line-height: 1.4;
--text-lg--letter-spacing: -0.005em;
--text-xl: 1.25rem;
--text-xl--line-height: 1.3;
--text-xl--letter-spacing: -0.01em;
--text-2xl: 1.5rem;
--text-2xl--line-height: 1.2;
--text-2xl--letter-spacing: -0.015em;
--text-3xl: 1.875rem;
--text-3xl--line-height: 1.1;
--text-3xl--letter-spacing: -0.02em;
--text-4xl: 2.25rem;
--text-4xl--line-height: 1.05;
--text-4xl--letter-spacing: -0.022em;
--text-5xl: 3rem;
--text-5xl--line-height: 1;
--text-5xl--letter-spacing: -0.025em;
--text-6xl: 3.75rem;
--text-6xl--line-height: 1;
--text-6xl--letter-spacing: -0.028em;
--text-7xl: 4.5rem;
--text-7xl--line-height: 1;
--text-7xl--letter-spacing: -0.03em;
} }
/* /*
@@ -105,8 +156,15 @@
oklch(0.45 0.19 27) oklch(0.45 0.19 27)
); );
--gradient-accent: linear-gradient(120deg, oklch(0.58 0.22 27), oklch(0.68 0.19 40)); --gradient-accent: linear-gradient(120deg, oklch(0.58 0.22 27), oklch(0.68 0.19 40));
/*
* Ombre neutre: la profondita' la danno distanza e diffusione, non il colore.
* Le superfici piu' grandi ricevono un'ombra piu' ampia e morbida.
*/
--shadow-card: 0 1px 2px oklch(0.16 0.01 260 / 6%), 0 12px 28px -18px oklch(0.16 0.01 260 / 35%); --shadow-card: 0 1px 2px oklch(0.16 0.01 260 / 6%), 0 12px 28px -18px oklch(0.16 0.01 260 / 35%);
--shadow-pop: 0 18px 40px -20px oklch(0.58 0.22 27 / 55%); --shadow-pop: 0 2px 6px oklch(0.16 0.01 260 / 8%), 0 20px 44px -22px oklch(0.16 0.01 260 / 45%);
/* Materiale traslucido della chrome fissa (nav, barre). */
--materiale-sfondo: color-mix(in oklab, var(--card) 72%, transparent);
--materiale-bordo: color-mix(in oklab, var(--foreground) 10%, transparent);
--chart-1: oklch(0.646 0.222 41.116); --chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704); --chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392); --chart-3: oklch(0.398 0.07 227.392);
@@ -155,9 +213,15 @@
box-shadow: var(--shadow-pop); box-shadow: var(--shadow-pop);
} }
/*
* Nessuna crenatura fissa qui: la imposta la scala tipografica in @theme, che
* la calibra per taglia (stretta sui titoli grandi, larga sulle etichette
* piccole). Un valore unico per tutte le dimensioni sarebbe sbagliato da
* qualche parte per definizione.
*/
@utility font-display { @utility font-display {
font-family: var(--font-display); font-family: var(--font-display);
letter-spacing: 0.02em; font-optical-sizing: auto;
} }
/* --- Micro-animazioni (solo CSS standard, nessuna dipendenza esterna) --- */ /* --- Micro-animazioni (solo CSS standard, nessuna dipendenza esterna) --- */
@@ -265,7 +329,7 @@
/* Feedback al tocco sulle card interattive (+ hover leggero su desktop). */ /* Feedback al tocco sulle card interattive (+ hover leggero su desktop). */
@utility premi { @utility premi {
transition: transition:
transform 140ms ease-out, transform 100ms ease-out,
box-shadow 140ms ease-out; box-shadow 140ms ease-out;
&:active { &:active {
@@ -280,36 +344,235 @@
} }
} }
@media (prefers-reduced-motion: reduce) { /*
*, * Porta l'area attiva a 44x44px (minimo iOS) senza toccare la grafica: il
*::before, * comando resta piccolo, il dito ha lo spazio che gli serve.
*::after { */
animation-duration: 0.001ms !important; @utility area-tocco {
animation-iteration-count: 1 !important; position: relative;
transition-duration: 0.001ms !important;
&::after {
content: "";
position: absolute;
top: 50%;
left: 50%;
width: max(100%, 44px);
height: max(100%, 44px);
transform: translate(-50%, -50%);
} }
} }
/* --- Materiali traslucidi --- */
/*
* Chrome traslucida: il contenuto scorre sotto e si intravede sfocato.
* L'opacita' sta sotto il 75%, altrimenti la sfocatura non si vede e tanto
* vale una barra opaca.
*/
@utility materiale {
background-color: var(--materiale-sfondo);
backdrop-filter: blur(20px) saturate(180%);
-webkit-backdrop-filter: blur(20px) saturate(180%);
border-color: var(--materiale-bordo);
}
/*
* Bordo di scorrimento: al posto della riga netta sopra la nav, una striscia
* che sfuma il contenuto mentre ci passa sotto.
*/
@utility bordo-scroll-basso {
&::before {
content: "";
position: absolute;
inset-inline: 0;
bottom: 100%;
height: 1.25rem;
pointer-events: none;
backdrop-filter: blur(6px);
-webkit-backdrop-filter: blur(6px);
mask-image: linear-gradient(to top, black, transparent);
-webkit-mask-image: linear-gradient(to top, black, transparent);
}
}
/* --- Apertura/chiusura dei contenuti collassabili (Radix Collapsible) --- */
@keyframes crapp-apri {
from {
height: 0;
opacity: 0;
}
to {
height: var(--radix-collapsible-content-height);
opacity: 1;
}
}
@keyframes crapp-chiudi {
from {
height: var(--radix-collapsible-content-height);
opacity: 1;
}
to {
height: 0;
opacity: 0;
}
}
@utility anim-tendina {
overflow: hidden;
&[data-state="open"] {
animation: crapp-apri 260ms cubic-bezier(0.22, 1, 0.36, 1);
}
&[data-state="closed"] {
animation: crapp-chiudi 220ms cubic-bezier(0.4, 0, 0.6, 1);
}
}
@keyframes crapp-dissolvenza {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
/* --- Preferenze di accessibilita' --- */
/*
* Movimento ridotto non vuol dire "nessun feedback": gli spostamenti e i
* rimbalzi spariscono, le dissolvenze restano perche' aiutano a capire cosa e'
* cambiato. Azzerare *tutte* le animazioni toglieva anche quelle.
*/
@media (prefers-reduced-motion: reduce) {
.anim-reveal,
.anim-pop,
.anim-riga {
animation: crapp-dissolvenza 180ms ease-out both;
}
.anim-tendina[data-state="open"],
.anim-tendina[data-state="closed"] {
animation: none;
}
.anim-battito,
.anim-oscilla {
animation: none;
}
.anim-barra {
transition-duration: 1ms;
}
.premi {
transition-property: box-shadow, background-color;
}
.premi:active,
.premi:hover {
transform: none;
}
html {
scroll-behavior: auto;
}
}
/* Trasparenza ridotta: superfici piene, niente sfocatura da leggere sotto. */
@media (prefers-reduced-transparency: reduce) {
:root,
.dark {
--materiale-sfondo: var(--card);
}
.materiale {
backdrop-filter: none;
-webkit-backdrop-filter: none;
}
.bordo-scroll-basso::before {
display: none;
}
}
/* Contrasto alto: fondi pieni, bordi definiti, testo secondario piu' scuro. */
@media (prefers-contrast: more) {
:root {
--muted-foreground: oklch(0.4 0.015 260);
--border: oklch(0.76 0.01 255);
--materiale-sfondo: var(--card);
--materiale-bordo: var(--foreground);
}
.dark {
--muted-foreground: oklch(0.86 0.01 260);
--border: oklch(1 0 0 / 32%);
--materiale-sfondo: var(--card);
--materiale-bordo: var(--foreground);
}
.materiale {
backdrop-filter: none;
-webkit-backdrop-filter: none;
}
}
/*
* Tema scuro. Regole:
* - l'accento resta il rosso del brand, solo schiarito per reggere il fondo
* scuro (nella versione precedente diventava grigio-blu e il brand spariva);
* - `--primary` NON si inverte: l'app usa `text-primary-foreground` come
* "testo sopra il gradiente hero", quindi primary-foreground deve restare
* chiaro anche al buio;
* - ogni token semantico (success/warning/info/training/metalli/gradienti/
* ombre) va ridefinito qui, altrimenti resta il valore chiaro.
*/
.dark { .dark {
--background: oklch(0.129 0.042 264.695); --background: oklch(0.16 0.008 265);
--foreground: oklch(0.984 0.003 247.858); --foreground: oklch(0.97 0.002 250);
--card: oklch(0.208 0.042 265.755); --card: oklch(0.21 0.011 265);
--card-foreground: oklch(0.984 0.003 247.858); --card-foreground: oklch(0.97 0.002 250);
--popover: oklch(0.208 0.042 265.755); --popover: oklch(0.21 0.011 265);
--popover-foreground: oklch(0.984 0.003 247.858); --popover-foreground: oklch(0.97 0.002 250);
--primary: oklch(0.929 0.013 255.508); --primary: oklch(0.28 0.014 265);
--primary-foreground: oklch(0.208 0.042 265.755); --primary-foreground: oklch(0.98 0.002 250);
--secondary: oklch(0.279 0.041 260.031); --secondary: oklch(0.27 0.012 265);
--secondary-foreground: oklch(0.984 0.003 247.858); --secondary-foreground: oklch(0.97 0.002 250);
--muted: oklch(0.279 0.041 260.031); --muted: oklch(0.27 0.012 265);
--muted-foreground: oklch(0.704 0.04 256.788); --muted-foreground: oklch(0.73 0.015 260);
--accent: oklch(0.279 0.041 260.031); --accent: oklch(0.66 0.2 27);
--accent-foreground: oklch(0.984 0.003 247.858); --accent-foreground: oklch(0.99 0 0);
--destructive: oklch(0.704 0.191 22.216); --destructive: oklch(0.66 0.2 27);
--destructive-foreground: oklch(0.984 0.003 247.858); --destructive-foreground: oklch(0.99 0 0);
--border: oklch(1 0 0 / 10%); --border: oklch(1 0 0 / 12%);
--input: oklch(1 0 0 / 15%); --input: oklch(1 0 0 / 16%);
--ring: oklch(0.551 0.027 264.364); --ring: oklch(0.66 0.2 27);
--success: oklch(0.72 0.15 152);
--success-foreground: oklch(0.16 0.01 260);
--warning: oklch(0.83 0.14 78);
--warning-foreground: oklch(0.18 0.02 60);
--info: oklch(0.7 0.12 245);
--info-foreground: oklch(0.16 0.01 260);
--training: oklch(0.7 0.16 245);
--training-foreground: oklch(0.16 0.01 260);
--bronzo: oklch(0.7 0.11 55);
--argento: oklch(0.82 0.02 260);
--oro: oklch(0.85 0.14 85);
--gradient-hero: linear-gradient(
140deg,
oklch(0.2 0.012 265),
oklch(0.26 0.04 20) 55%,
oklch(0.46 0.19 27)
);
--gradient-accent: linear-gradient(120deg, oklch(0.62 0.21 27), oklch(0.7 0.18 40));
--shadow-card: 0 1px 2px oklch(0 0 0 / 40%), 0 12px 28px -18px oklch(0 0 0 / 70%);
--shadow-pop: 0 2px 6px oklch(0 0 0 / 45%), 0 20px 44px -22px oklch(0 0 0 / 80%);
--materiale-sfondo: color-mix(in oklab, var(--card) 70%, transparent);
--materiale-bordo: color-mix(in oklab, white 12%, transparent);
--chart-1: oklch(0.488 0.243 264.376); --chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48); --chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08); --chart-3: oklch(0.769 0.188 70.08);
@@ -336,4 +599,16 @@
font-family: var(--font-sans); font-family: var(--font-sans);
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
} }
/*
* Anello di focus da tastiera su tutto cio' che e' attivabile. Sta qui e non
* sui singoli componenti perche' l'app ha una sessantina di <button> scritti
* a mano fuori da components/ui/, nessuno dei quali lo definiva.
* `:where()` ha specificita' zero: qualsiasi `focus-visible:` di Tailwind sul
* componente continua a vincere su questa regola.
*/
:where(a, button, input, select, textarea, summary, [role="button"], [tabindex]):focus-visible {
outline: 2px solid var(--ring);
outline-offset: 2px;
}
} }