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:
1 parent
7b93f92965
commit
263621456d
5 files changed
+73
-11
No files matched your search
@@ -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
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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. */
|
||||||
|
|||||||
@@ -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");
|
||||||
Reference in new issue
Block a user