Layout su schermi stretti: scout e griglie non tagliati a 360 px

Tabella scout senza larghezza minima (nome a capo, colonne compatte), griglie con
min-width 0, niente scroll orizzontale della pagina, campi a 16px contro lo zoom iOS.
Fixes #12

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
davideandClaude Sonnet 5.5 committed 2026-11-11 09:54:10 +01:00
1 parent 7b93f92965
commit 263621456d
5 files changed
+73 -11

No files matched your search

+7
View File
@@ -13,6 +13,13 @@ versioni `0.y.z` erano pre-release.
## [Non rilasciato] ## [Non rilasciato]
### Modificato
- Layout su schermi stretti (360-402 px, iPhone e Android): niente più pagina tagliata in
larghezza. La tabella per giocatore dello scout sta nello schermo (il nome si accorcia), le
griglie di riquadri non sfondano con nomi ed etichette lunghe, la pagina non scorre in
orizzontale e i campi di testo sono a 16 px, così iOS non zooma al tocco.
## [1.6.1] - 2026-11-06 ## [1.6.1] - 2026-11-06
Risultati dello scout leggibili nell'app, classifica cacche completa e pagina partita a sezioni Risultati dello scout leggibili nell'app, classifica cacche completa e pagina partita a sezioni
+10 -10
View File
@@ -124,7 +124,7 @@ export function ScoutReport({
<div <div
role="tablist" role="tablist"
aria-label="Periodo" aria-label="Periodo"
className="mt-3 grid grid-flow-col auto-cols-fr gap-1 rounded-2xl bg-secondary p-1" className="mt-3 grid grid-flow-col auto-cols-fr gap-0.5 rounded-2xl bg-secondary p-1"
> >
{[undefined, ...sets].map((s) => ( {[undefined, ...sets].map((s) => (
<button <button
@@ -134,7 +134,7 @@ export function ScoutReport({
aria-selected={s === set} aria-selected={s === set}
onClick={() => setSet(s)} onClick={() => setSet(s)}
className={cn( className={cn(
"min-w-0 truncate rounded-xl px-1 py-2 text-[13px] font-bold uppercase transition-colors", "min-w-0 truncate rounded-xl px-0 py-2 text-[13px] font-bold uppercase transition-colors",
s === set s === set
? "bg-primary text-primary-foreground shadow-card" ? "bg-primary text-primary-foreground shadow-card"
: "text-muted-foreground", : "text-muted-foreground",
@@ -185,13 +185,13 @@ export function ScoutReport({
Nessuna valutazione in questo set. Nessuna valutazione in questo set.
</p> </p>
) : ( ) : (
<div className="mt-2 overflow-x-auto rounded-2xl bg-secondary/60"> <div className="mt-2 overflow-hidden rounded-2xl bg-secondary/60">
<table className="w-full min-w-[22rem] text-[13px]"> <table className="w-full table-fixed text-[13px]">
<thead> <thead>
<tr className="text-left text-[13px] uppercase text-muted-foreground"> <tr className="text-left text-[13px] uppercase text-muted-foreground">
<th className="px-3 py-2 font-bold">Giocatore</th> <th className="whitespace-nowrap px-2 py-2 font-bold">Giocatore</th>
{fondamentali.map((f) => ( {fondamentali.map((f) => (
<th key={f.id} className="px-2 py-2 text-center font-bold"> <th key={f.id} className="w-11 px-0 py-2 text-center font-bold">
{f.label.slice(0, 3)} {f.label.slice(0, 3)}
</th> </th>
))} ))}
@@ -200,16 +200,16 @@ export function ScoutReport({
<tbody className="divide-y divide-border"> <tbody className="divide-y divide-border">
{righe.map(({ g, conteggi }) => ( {righe.map(({ g, conteggi }) => (
<tr key={g.id}> <tr key={g.id}>
<td className="max-w-40 truncate px-3 py-2 font-semibold"> <td className="px-2 py-2 font-semibold leading-tight">
<span className="mr-1.5 tabular-nums text-muted-foreground"> <span className="mr-1.5 tabular-nums text-muted-foreground">
#{g.numero} #{g.numero}
</span> </span>
{g.nome} {g.nome}
</td> </td>
{fondamentali.map((f) => ( {fondamentali.map((f) => (
<td key={f.id} className="px-2 py-2 text-center font-semibold tabular-nums"> <td key={f.id} className="px-0 py-2 text-center font-semibold tabular-nums">
<span className="text-success">{conteggi[f.id].piu}</span> <span className="text-success">{conteggi[f.id].piu}</span>
<span className="text-muted-foreground"> / </span> <span className="text-muted-foreground">/</span>
<span className="text-destructive">{conteggi[f.id].meno}</span> <span className="text-destructive">{conteggi[f.id].meno}</span>
</td> </td>
))} ))}
@@ -238,7 +238,7 @@ export function ScoutReport({
{ l: "Muri", v: totaliAzioni.muri }, { l: "Muri", v: totaliAzioni.muri },
{ l: "Errori", v: totaliAzioni.errori }, { l: "Errori", v: totaliAzioni.errori },
].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 px-1 py-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-[13px] font-semibold uppercase text-muted-foreground"> <p className="mt-1 text-[13px] font-semibold uppercase text-muted-foreground">
{t.l} {t.l}
+3 -1
View File
@@ -339,9 +339,11 @@ export function SezioneTendina({
* `input[type="date"]` ha al suo interno segmenti e icona nativi che, anche * `input[type="date"]` ha al suo interno segmenti e icona nativi che, anche
* con `appearance-none`, restano un filo più alti di un input di testo a * con `appearance-none`, restano un filo più alti di un input di testo a
* parità di padding — un'altezza esplicita allinea tutti i campi tra loro. * parità di padding — un'altezza esplicita allinea tutti i campi tra loro.
* `text-base` (16px) non è estetica: sotto i 16px iOS Safari zooma la pagina al focus e,
* dopo il blur, la lascia ingrandita e tagliata in larghezza.
*/ */
export const classiInput = export const classiInput =
"h-10 w-full min-w-0 rounded-xl border border-border bg-background px-3 py-2 text-[15px]"; "h-10 w-full min-w-0 rounded-xl border border-border bg-background px-3 py-2 text-base";
/** /**
* Etichetta + controllo di un form. `min-w-0`: dentro `grid-cols-2` questa label * Etichetta + controllo di un form. `min-w-0`: dentro `grid-cols-2` questa label
+20
View File
@@ -520,11 +520,31 @@
border-color: var(--color-border); border-color: var(--color-border);
} }
/*
* Mai scroll orizzontale della pagina (iPhone 360-402 px). `clip` e non `hidden`:
* non crea un contenitore di scroll, quindi `position: sticky` continua a funzionare.
*/
html,
body {
max-width: 100%;
overflow-x: clip;
}
/*
* Gli elementi di una griglia hanno `min-width: auto`: una colonna `1fr` non scende sotto
* la parola più lunga e sfonda la riga. Con `min-width: 0` la colonna resta nel suo posto
* e `overflow-wrap` spezza solo la parola che davvero non ci sta.
*/
.grid > * {
min-width: 0;
}
body { body {
background-color: var(--color-background); background-color: var(--color-background);
color: var(--color-foreground); color: var(--color-foreground);
font-family: var(--font-sans); font-family: var(--font-sans);
line-height: 1.5; line-height: 1.5;
overflow-wrap: break-word;
font-optical-sizing: auto; font-optical-sizing: auto;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
/* Niente pull-to-refresh accidentale né rimbalzo che scopre il fondo. */ /* Niente pull-to-refresh accidentale né rimbalzo che scopre il fondo. */
+33
View File
@@ -0,0 +1,33 @@
/**
* Check dei requisiti di layout per schermi stretti (360 px): `bun test/unit/layout-stretto.test.ts`.
*
* Non c'è un driver browser (vedi `test/README.md`), quindi si presidiano le due regole che,
* se tolte, fanno tornare la pagina tagliata in larghezza su iPhone: i campi a 16 px (sotto,
* iOS zooma al focus e non torna indietro) e la rete di sicurezza sull'overflow orizzontale.
*/
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { classiInput } from "@/components/crapp/ui-bits";
assert.match(classiInput, /\btext-base\b/, "i campi devono essere a 16px (zoom automatico iOS)");
assert.doesNotMatch(classiInput, /text-\[1[0-5]px\]|text-sm|text-xs/);
const css = readFileSync(new URL("../../src/styles.css", import.meta.url), "utf8");
assert.match(css, /overflow-x:\s*clip/, "html/body devono impedire lo scroll orizzontale");
assert.match(
css,
/\.grid\s*>\s*\*\s*\{\s*min-width:\s*0/,
"gli elementi di griglia devono poter restringersi",
);
const scout = readFileSync(
new URL("../../src/components/crapp/ScoutReport.tsx", import.meta.url),
"utf8",
);
assert.doesNotMatch(
scout,
/min-w-\[\d+rem\]/,
"la tabella scout non deve imporre una larghezza minima",
);
console.log("layout-stretto: ok");