Files
SteUP_Dotnet/STILE_UI.md
T
MarcoE 2a0054aa30 Rifacimento della UI secondo STILE_UI.md
Adotta lo stile condiviso delle app Integry: superfici piatte, raggi ampi,
spaziature a scala fissa, gerarchia data dallo spazio prima che dalla
decorazione. La palette storica di SteUP resta intatta; sono stati aggiunti
solo i ruoli che mancavano (workspace, surfaceMuted, divider, inkMuted e le
triplette dei toni semantici).

Fondamenta
- tokens.css: ruoli colore, scale di spazio/raggi/tipografia, curve di
  movimento e scala z-index semantica. Unica fonte di verita': pilotano
  anche le variabili CSS di MudBlazor.
- Inter variabile ospitata in locale (133KB) al posto di Nunito da Google
  Fonts: l'app lavora offline, un font che arriva dalla rete in campo non
  arriva.
- base.css: shell applicativa e comportamenti da app nativa — niente
  selezione testo, niente menu al tocco lungo, niente lampo grigio sul tap,
  niente rimbalzo elastico, niente zoom a doppio tocco, scroll con inerzia.
- mudblazor-overrides.css: MudBlazor appiattito (elevation 0, nessun tint,
  target 44/48, etichette in caso naturale).
- Rimossi Bootstrap (CSS, icone e JS) e i 996 righe di CSS con ambito per
  componente del vecchio sistema: erano tutti inutilizzati.

Struttura
- Navigazione flottante in basso con icona piena sulla voce attiva; da 720px
  diventa colonna laterale. Il MudFabMenu e' sostituito da un bottom sheet:
  stessa funzione, bersagli molto piu' grandi.
- Testata alta col titolo a sinistra, fusa con la pagina.
- Nel form scheda il salvataggio sta in una barra ancorata in fondo, non in
  coda a uno scroll lungo.
- Scelta di punto vendita e articoli a schermo intero con ricerca; la
  selezione multipla usa righe da 56px invece di un menu a tendina.
- Urgenza scelta a pillole invece che da una select.

Stato e riscontro
- StatusBadgeHelper: tono, icona ed etichetta di ogni stato in un punto solo,
  cosi' lo stesso stato si presenta identico ovunque.
- Scheletri al posto degli spinner; l'overlay bloccante dice sempre cosa sta
  facendo; gli stati vuoti suggeriscono la mossa successiva.
- Rimosso il ritardo artificiale di 250ms in InspectionCard, che non
  attendeva nulla.

DESIGN.md riscritto sul nuovo sistema.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 11:09:26 +02:00

23 KiB
Raw Permalink Blame History

Specifiche grafiche — stile UI condiviso

Documento di riferimento per uniformare la grafica delle app Integry allo stile adottato in Fixiy.

Descrive forma, ritmo, tipografia e composizione dei componenti. Non contiene una palette: ogni app mantiene i propri colori e li mappa sui ruoli semantici elencati al §2. Lo stile è quello che rimane costante al variare della tinta: superfici piatte, raggi ampi, spaziature a scala fissa, gerarchia data dallo spazio prima che dalla decorazione.

Riferimento d'implementazione: Flutter + Material 3 (useMaterial3: true). I nomi dei token qui sotto sono quelli reali del codice Fixiy (lib/core/design_system.dart, lib/core/feedback.dart); in un'altra app si rinominano col prefisso dell'app, ma valori e semantica restano invariati.


1. Principi

  1. Superfici piatte. Niente ombre per costruire gerarchia: elevation: 0 ovunque (card, AppBar, NavigationBar, NavigationRail). L'unica ombra ammessa è quella del toast (elevation 6) e dei livelli modali di sistema. La separazione si ottiene con contrasto di superficie (sfondo pagina più freddo delle superfici) e con lo spazio.
  2. Nessun tint automatico. surfaceTintColor: Colors.transparent su AppBar, Card, Dialog, pannelli: le superfici non devono virare verso il colore primario quando si scrolla.
  3. Colore = significato. L'accento primario serve solo ad azioni, selezione, focus e progresso — mai a decorare. I colori semantici (urgenza, stato, esito) compaiono sempre accompagnati da icona + etichetta testuale, mai da soli.
  4. Target generosi. Ogni elemento interattivo ha lato minimo 44 px (minimumSize: Size(44, 44)), 48 px d'altezza per i bottoni principali. L'app si usa in piedi, con i guanti, in negozio.
  5. Densità standard. visualDensity: VisualDensity.standard: si rifiuta la densità desktop, anche su schermo largo.
  6. Lo spazio prima della linea. I divisori si usano solo dove l'informazione cambia natura (testata di uno sheet, righe di una lista di navigazione); altrove separa la spaziatura.
  7. Nulla di stock. Si evitano le firme visive Android di default: card con ombra, FAB circolare al centro, AppBar colorata con titolo centrato, snackbar di sistema.
  8. Scrivibile a voce alta. Testi in lingua dell'app, brevi, orientati all'azione; gli enum passano da una mappa di etichette centralizzata (labels.dart), mai formattati inline.

2. Ruoli colore (da riempire con la palette dell'app)

L'app ospite definisce i propri valori per questi ruoli e li usa con la stessa semantica. Nessun valore è fissato qui.

Ruolo Uso
primary accento unico di azione: bottone pieno, focus, selezione, indicatori di progresso, icone attive
primaryDark testo/icona sopra primaryContainer, valori numerici in evidenza
workspace sfondo dell'applicazione (scaffoldBackgroundColor), leggermente più freddo/scuro di surface
surface superficie di lavoro: card, pannelli, AppBar, barra di navigazione, dialog
surfaceMuted superficie secondaria dentro una superficie (campo di ricerca, chip non selezionato, riquadro informativo interno)
divider bordi 1 px, separatori, contorno dei controlli a riposo
ink testo principale
inkMuted testo secondario, hint, etichette di metadato, icone non attive
emergency / errorSurface tono danger: errori, azioni distruttive, priorità massima
high / warningSurface tono warning: attenzione, dati incompleti, priorità alta
success / successSurface tono success: conferme, completamenti
info / infoSurface tono info: stato informativo, pannello di riepilogo, indicatore di selezione
neutral / surfaceMuted tono neutral: stati senza carica semantica

Regola delle coppie. Ogni tono semantico è una coppia (foreground forte, superficie tenue). Le superfici tenui riempiono badge, pannelli di feedback e cerchi-icona dei dialog; il foreground forte colora icona e testo sopra di esse. I toast invertono la regola: riempimento pieno col colore forte e testo bianco, per essere leggibili sopra qualunque contenuto.

Schema colore. Si genera da un seme (ColorScheme.fromSeed) e poi si sovrascrivono esplicitamente primary, onPrimary, primaryContainer (= infoSurface), onPrimaryContainer (= primaryDark), secondaryContainer (= surfaceMuted), outline e outlineVariant (= divider). Non si lascia al seme la scelta dei contenitori: devono restare tenui e neutri.


3. Scale di token

Spaziatura (Space)

xs = 8    sm = 12    md = 16    lg = 24    xl = 32

Tutti i margini e le distanze sono multipli di 2 e, per quanto possibile, uno di questi cinque valori. Valori d'uso frequente derivati: 4 e 6 per le micro-distanze interne (titolo→sottotitolo, icona→testo), 10 tra le card di una lista, 18/20/22 per il padding dei pannelli di testata.

Raggi (Radius)

compact = 8     riquadri informativi, barre di progresso, feedback inline
control = 16    campi di testo, bottoni, toast
surface = 20    card, pannelli, dialog, sheet
pill    = 999   badge, chip, segmented button, campo di ricerca, bottoni onboarding

Il contrasto tra i raggi è voluto: superfici più arrotondate dei controlli, elementi di stato completamente stondati (pill).

Breakpoint

rail       = 720    da qui la navigazione passa da barra inferiore a NavigationRail laterale
twoColumn  = 900    soglia per layout a due colonne
contentMax = 1180   larghezza massima assoluta del contenuto

Larghezza tipica di una pagina: maxWidth: 920, centrata in alto (PageWidth = Align(topCenter) + ConstrainedBox). Su schermi larghi il contenuto non si allarga all'infinito: resta una colonna leggibile.


4. Tipografia

Famiglia principale Inter, fallback ['SF Pro Display', 'Roboto', 'sans-serif']. Scala fissa, pensata per la scansione rapida, non per l'effetto editoriale:

Stile Dim. Interlinea Peso Uso
headlineMedium 34 1.2 700 titoli di apertura
headlineSmall 28 1.25 700 titolo dell'AppBar, valore percentuale in evidenza
titleLarge 22 1.3 700 titolo di card e di sheet, testata di sezione
titleMedium 18 1.35 700 sotto-titoli, valori metrica, titolo dialog
bodyLarge 16 1.5 400 testo corrente, descrizioni
bodyMedium 15 1.45 400 metadati, righe secondarie
labelLarge 14 1.25 600 etichette dei bottoni
labelMedium 13 1.25 600 etichette di navigazione, caption

Regole d'uso:

  • Il titolo di una card è l'entità, non il tipo di record (es. il negozio, non "Manutenzione"): titleLarge con peso 800 e interlinea compressa 1.15.
  • Gli identificativi tecnici (numero ticket, codice) vanno sopra il titolo, 12 px, peso 700, colore inkMuted, letterSpacing: 0.3.
  • Le righe di metadato si compongono con separatore · (spaziopunto mediospazio) e colore inkMuted.
  • Il testo secondario non usa mai un peso inferiore a 400 né opacità: si distingue per colore (inkMuted), non per trasparenza.
  • Nessun maiuscoletto forzato, nessun letterSpacing ampio: le etichette restano in caso naturale.

5. Tema — impostazioni globali

Elemento Configurazione
Scaffold sfondo workspace; extendBody: true quando la barra di navigazione è flottante
AppBar toolbarHeight: 80, titolo a sinistra (centerTitle: false), titleSpacing 20 (28 su schermo largo), sfondo workspace (si fonde con la pagina), elevation 0, scrolledUnderElevation: 0
Card / pannelli elevation 0, margin: EdgeInsets.zero, sfondo surface, raggio surface, senza bordo
Campi di testo filled: true su surface, padding 16×16, raggio control; bordo a riposo 1 px divider, in focus 2 px primary, in errore 1 px emergency; hint e helper in inkMuted
Bottone pieno minimumSize: (44, 48), sfondo primary, testo bianco labelLarge, raggio control
Bottone contornato minimumSize: (44, 48), testo ink, bordo divider, raggio control
Bottone testuale / icona minimumSize: (44, 44)
Segmented button StadiumBorder, min (52, 44); selezionato = riempimento primary + testo bianco; non selezionato = surface + testo inkMuted; bordo divider
NavigationBar altezza 68, elevation 0, sfondo surface, indicatore infoSurface, etichette sempre visibili (alwaysShow), stile labelMedium
NavigationRail sfondo surface, indicatore primaryContainer, etichette su tutte le voci (labelType: all), etichetta selezionata in primary
Snackbar comportamento floating (ma nell'app si usa il toast custom, §6.9)
Progress colore primary

Localizzazione. Locale unico dichiarato esplicitamente con i tre delegati globali (Material, Widgets, Cupertino), così date, picker e messaggi di sistema parlano la lingua dell'app.


6. Componenti

6.1 PageWidth — contenitore di pagina

Align(topCenter) + ConstrainedBox(maxWidth: 920). Avvolge ogni contenuto di pagina. Su telefono è trasparente; su tablet/desktop impedisce righe di testo lunghissime e mantiene la stessa composizione a tutte le larghezze.

6.2 Panel — superficie base

Material con color: surface, surfaceTintColor: transparent, clipBehavior: antiAlias, forma arrotondata surface (20), padding di default 16.

È il mattone di tutto: card di lista, riquadri di riepilogo, righe raggruppate, campi compositi. Varianti d'uso:

  • con colore: color: infoSurface per i pannelli di riepilogo/testata; color: surfaceMuted per i sotto-riquadri dentro un form; color: errorSurface per una card in stato critico.
  • padding: EdgeInsets.zero + InkWell interno quando l'intera superficie è toccabile (raggio dell'ink coerente con quello del pannello).

6.3 SectionHeader — testata di sezione

Riga con, a sinistra, titleLarge + eventuale descrizione (inkMuted, 4 px sotto), e a destra un widget opzionale (bottone, badge, contatore) separato da 12 px. Apre ogni blocco tematico dentro una pagina lunga.

6.4 Badge — stato in pillola

DecoratedBox con raggio 999, superficie tenue del tono, padding 9×5, contenuto = icona 15 px + 5 px + testo 12 px peso 700, entrambi nel foreground del tono. Avvolto in Semantics(label:).

I badge si costruiscono da funzioni di mappatura (priorityBadge(priority), statusBadge(status)), non a mano nelle pagine: così ogni schermata mostra lo stesso stato con la stessa icona e lo stesso tono. Ogni nuovo stato si aggiunge a quella funzione.

6.5 Card di elenco

Composizione canonica, dall'alto:

  1. Barra di priorità verticale a tutta altezza sul bordo sinistro, larghezza 6 px, colore dal tono di urgenza (Stack + Positioned(top/bottom/left: 0)). La card usa padding asimmetrico (22, 16, 16, 16) per far respirare la barra.
  2. Identificativo (12 px, inkMuted, 700) con, a destra, l'eventuale maniglia di trascinamento.
  3. Titolo = l'entità, titleLarge peso 800.
  4. Riga meta con separatori ·, bodyMedium peso 600 inkMuted.
  5. Badge (priorità, stato) in un Wrap(spacing: 8, runSpacing: 8).
  6. Descrizione integrale, bodyLarge interlinea 1.35 — niente troncamenti sul contenuto che serve a decidere.
  7. Riga oraria/assegnatario preceduta da icona 16 px inkMuted.
  8. Barra azioni interna: secondaria contornata + primaria piena, affiancate con Expanded; sotto i 350 px di larghezza della card diventano una colonna a piena larghezza (LayoutBuilder).

La card intera espone un Semantics(label:) che riassume entità, priorità e stato.

Primo elemento della pagina, Panel con superficie tenue (primaryContainer/infoSurface), padding 18:

  • riga di testata: titolo titleLarge + data/sottotitolo inkMuted, e a destra un badge di stato del documento ("Da salvare" / "Salvato" / "Concluso");
  • eventuale fila di chip di scelta (Wrap(spacing: 8, runSpacing: 8));
  • fila di metriche in Expanded uguali, ciascuna: icona 20 px in primaryDark → valore titleMedium → etichetta 12 px inkMuted su una riga sola con ellissi;
  • eventuale riga di stato in coda (labelMedium inkMuted), es. l'ora dell'ultimo salvataggio.

Quando c'è un progresso, si aggiunge una LinearProgressIndicator con minHeight: 8 e borderRadius: 8, preceduta dal conteggio in chiaro ("3 di 7 completate") e dalla percentuale in headlineSmall.

6.7 Chip e filtri

  • Filtri: ChoiceChip StadiumBorder, showCheckmark: false, l'etichetta include il conteggio (Tutte · 12). A riposo: superficie surfaceMuted, bordo divider, testo inkMuted peso 600. Selezionato: superficie infoSurface, bordo primary, testo primaryDark. Disposti in una riga orizzontale scorrevole alta 44 px, spaziatura 8.
  • Chip-entità (selezione di un oggetto): Material + InkWell con StadiumBorder, padding 12×8, icona 16 px + etichetta peso 600 + dato secondario 12 px inkMuted. Selezionato = superficie surface e bordo primaryDark; non selezionato = surfaceMuted e bordo divider.

6.8 Campo di ricerca

Non usa il tema dei campi: è completamente stondato (raggio 999), riempito in surfaceMuted, senza bordo a riposo e in enabled, bordo 1.5 px primary solo in focus. Padding 18×14, icona di ricerca inkMuted come prefisso, pulsante "cancella" come suffisso solo quando c'è testo.

6.9 Toast

Sostituisce ogni snackbar di sistema. SnackBar floating con sfondo trasparente, elevation 0, padding zero e margine (md, 0, md, md); il contenuto è un Material con riempimento pieno del tono, raggio control, elevation 6, ombra nera al 24 %, padding 14×12, e dentro: icona bianca 22 px → 12 px → testo bianco peso 600 interlinea 1.35 → eventuale azione testuale bianca peso 700.

Prima di mostrarlo si chiama hideCurrentSnackBar(): mai una coda di toast. Durata 4 s. Si espone come scorciatoie per tono (success/error/warning/info) più una funzione generica.

6.10 Dialog

Dialog su surface, raggio surface, insetPadding 24, maxWidth: 420, padding interno 24. Composizione verticale allineata a sinistra:

  1. cerchio-icona 48×48 riempito con la superficie tenue del tono, icona del tono 26 px;
  2. 16 px → titolo titleMedium;
  3. 8 px → messaggio inkMuted interlinea 1.45;
  4. 24 px → azioni: due bottoni in Expanded (annulla contornato a sinistra, conferma pieno a destra, 12 px di distanza); se non c'è annullamento, un solo bottone a piena larghezza.

Le azioni distruttive colorano di emergency il bottone di conferma. Due sole funzioni di ingresso: conferma (ritorna booleano) e informativo (un solo bottone).

6.11 Feedback inline

Riquadro non modale per messaggi persistenti dentro la pagina: superficie tenue del tono, raggio compact (8), padding 12, icona 20 px del tono + 10 px + testo peso 600 del tono, allineati in alto. Marcato Semantics(liveRegion: true). Si usa per errori di caricamento e avvisi che devono restare visibili, a differenza del toast che è effimero.

6.12 Stato vuoto

Centrato, padding 28, larghezza massima 420: icona 44 px inkMuted → 14 px → titolo titleMedium centrato → 6 px → messaggio inkMuted centrato → 18 px → azione facoltativa (bottone contornato). Il messaggio suggerisce la mossa successiva; se lo stato vuoto dipende da filtri, l'azione li azzera.

6.13 Barra azioni fissa

Ancorata in fondo alla pagina: SafeArea(top: false), superficie surface, bordo superiore 1 px divider, padding (16, 12, 16, 16). I bottoni sono in riga con Expanded e 12 px di distanza; sotto i 390 px diventano una colonna a piena larghezza con 8 px di distanza.

6.14 Bottom sheet

Sempre isScrollControlled: true, useSafeArea: true, showDragHandle: true. Altezza fissata a una frazione dello schermo: ~0.78 per uno sheet di consultazione, ~0.92 per uno sheet di compilazione.

Struttura:

  1. testata: padding (20, 0, 8, 12), titolo titleLarge + sottotitolo inkMuted a sinistra, IconButton di chiusura a destra;
  2. eventuale segmented button a piena larghezza (expandedInsets: EdgeInsets.zero) quando lo sheet ha due modalità;
  3. Divider(height: 1) — l'unica linea di struttura;
  4. corpo in Expanded, scrollabile, padding 16. Con più modalità si usa IndexedStack, che preserva lo stato dei form.

6.15 Navigazione

  • Telefono: NavigationBar flottante — avvolta in SafeArea(minimum: (16, 0, 16, 12)) e in un ClipRRect(borderRadius: 24), con extendBody: true sullo Scaffold, così il contenuto scorre sotto. Le pagine lasciano ~110120 px di padding inferiore per non finirci sotto.
  • Da 720 px: NavigationRail a sinistra con etichette su tutte le voci, seguito da VerticalDivider(width: 1).
  • Le destinazioni usano icona outline a riposo e piena da selezionata, con la stessa etichetta in entrambe le modalità.
  • Le sezioni si tengono vive in un IndexedStack: cambiare tab non ricostruisce né perde lo scroll.
  • Le notifiche stanno nelle actions dell'AppBar: IconButton con icona outline dentro un Badge numerico visibile solo se ci sono elementi non letti, e 12 px di margine destro.

6.16 Liste di navigazione (profilo, impostazioni)

Un Panel che contiene ListTile con contentPadding: EdgeInsets.zero, icona a sinistra, Icon(chevron_right) come trailing per le voci che aprono una pagina, e Divider() tra una voce e l'altra. Le voci distruttive (uscita, eliminazione) portano l'icona in emergency e non hanno chevron.

L'intestazione del profilo è un Panel con padding 22: CircleAvatar raggio 30 su superficie tenue con l'iniziale in 22 px peso 800, 16 px, poi nome titleLarge e riga secondaria inkMuted.

6.17 Campo composito (tappabile)

Per valori che si scelgono da un picker invece che digitare: Panel surfaceMuted con padding zero, InkWell interno, padding 14, dentro un'etichetta labelMedium, 5 px, e una riga icona 19 px + 7 px + valore con ellissi. Si comporta come un campo ma apre un selettore.


7. Tema di onboarding (login e schermate pre-accesso)

Le schermate fuori dall'applicazione autenticata usano una variante derivata dello stesso tema (base.copyWith(...)), non un tema separato: cambia solo il registro, non la struttura.

  • Sfondo pagina uniforme e neutro; primario = colore d'inchiostro (accento cromatico assente: l'attenzione è sul marchio).
  • Campi con riempimento bianco e bordo neutro; in focus bordo 2 px del colore d'inchiostro.
  • Bottone principale a pillola (StadiumBorder), altezza minima 52.
  • Composizione: SafeAreaCenterSingleChildScrollView(padding: 24)ConstrainedBox(maxWidth: 440) → colonna stretch.
  • Ordine: logo (altezza 128, dentro Semantics(image: true, label:)) → 32 px → campi a 16 px di distanza → 24 px → eventuale messaggio d'errore centrato in colorScheme.error e liveRegion → 12 px → bottone di invio → 20 px → footer con versione.
  • Il bottone di invio mostra il caricamento al posto dell'icona (SizedBox.square(dimension: 18) con CircularProgressIndicator(strokeWidth: 2)), restando della stessa dimensione; durante il caricamento onPressed: null.
  • I campi dichiarano autofillHints dentro un AutofillGroup e incatenano textInputAction: next, con invio sull'ultimo.

8. Comportamenti trasversali

Caricamento. Al primo caricamento senza dati: CircularProgressIndicator centrato in un'area di almeno 48 px di padding verticale. Con dati già presenti non si sostituisce mai il contenuto con uno spinner: si aggiorna in silenzio. Ogni elenco è avvolto in un RefreshIndicator (pull-to-refresh).

Errore di caricamento. Non svuota la pagina: compare un feedback inline (§6.11) in tono danger sopra il contenuto, con l'istruzione per riprovare ("Trascina verso il basso per riprovare"). Gli errori di rete non bloccano la navigazione.

Vuoto vs. filtrato. Si distinguono: "non c'è nulla" e "i filtri non trovano nulla" hanno testi diversi, e il secondo offre l'azzeramento dei filtri.

Riordino. Nelle liste riordinabili solo gli elementi ammissibili mostrano la maniglia (ReorderableDragStartListener con Icon(drag_handle)); il riordino è disabilitato quando è attivo un filtro o una ricerca, e un suggerimento testuale compare accanto al conteggio solo sopra i 520 px di larghezza.

Conteggi sempre visibili. Titoli di sezione, filtri e badge portano il numero ("12 attività", "Allegati (3)", "Tutte · 12"): il tecnico deve sapere quanto ha davanti senza contare.

Adattamento locale. L'adattamento non è solo a livello di pagina: i singoli componenti misurano il proprio spazio con LayoutBuilder e passano da riga a colonna sotto le soglie indicate (350 px per le azioni di una card, 390 px per la barra azioni). Così lo stesso componente funziona in colonna singola e affiancato.

Accessibilità.

  • Semantics(label:) su card, badge e maniglie di riordino, con una descrizione che riassume l'entità e i suoi stati.
  • Semantics(liveRegion: true) su toast, feedback inline e messaggi d'errore dei form.
  • tooltip su ogni IconButton senza etichetta.
  • Nessuna informazione veicolata dal solo colore: sempre icona e/o testo.
  • Target ≥ 44 px anche per le icone in testata.

Testi e stati. Le date estese si scrivono per intero nella lingua dell'app; gli orari in HH:mm con zero iniziale, e i valori mancanti come --:--; i dati incompleti si dichiarano ("km finali mancanti", "Partenza da compilare") invece di lasciare un vuoto.


9. Come adottare lo stile in un'altra app

  1. Copiare la struttura dei token: quattro classi statiche Colors, Space, Radius, Breakpoints con i valori dei §3 (spazi, raggi e breakpoint identici) e i propri colori sui ruoli del §2.
  2. Ricostruire il tema con le impostazioni del §5: sono la parte che rende immediatamente riconoscibile lo stile (AppBar alta e fusa con la pagina, card piatte a raggio 20, controlli a raggio 16, target 44/48).
  3. Portare i componenti condivisi del §6 — PageWidth, Panel, SectionHeader, Badge, Feedback, EmptyState, ActionBar, toast e dialog — rinominandoli col prefisso dell'app. Sono ~600 righe senza dipendenze oltre a Material.
  4. Definire le mappature di stato dell'app (priorityBadge, statusBadge, o gli equivalenti del dominio) in un unico punto, accanto ai componenti.
  5. Vietare i valori ad-hoc: nelle pagine si usano solo i token. Un colore o un raggio scritto a mano in una pagina è un difetto da correggere, non una scorciatoia.
  6. Verificare la lista di controllo:
    • nessuna ombra sulle superfici (elevation 0) e nessun surfaceTint;
    • sfondo pagina diverso dalle superfici;
    • ogni tocco ≥ 44 px;
    • ogni stato = icona + etichetta + tono;
    • contenuto limitato a ~920 px e centrato;
    • barra di navigazione flottante sotto i 720 px, rail sopra;
    • toast custom al posto delle snackbar di sistema;
    • stato vuoto, stato di caricamento e stato d'errore previsti per ogni elenco;
    • componenti che si riorganizzano da soli sotto le soglie di larghezza.