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>
23 KiB
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
- Superfici piatte. Niente ombre per costruire gerarchia:
elevation: 0ovunque (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. - Nessun tint automatico.
surfaceTintColor: Colors.transparentsu AppBar, Card, Dialog, pannelli: le superfici non devono virare verso il colore primario quando si scrolla. - 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.
- 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. - Densità standard.
visualDensity: VisualDensity.standard: si rifiuta la densità desktop, anche su schermo largo. - 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.
- 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.
- 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"):
titleLargecon 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
·(spazio–punto medio–spazio) e coloreinkMuted. - 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
letterSpacingampio: 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: infoSurfaceper i pannelli di riepilogo/testata;color: surfaceMutedper i sotto-riquadri dentro un form;color: errorSurfaceper una card in stato critico. padding: EdgeInsets.zero+InkWellinterno 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:
- 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. - Identificativo (12 px,
inkMuted, 700) con, a destra, l'eventuale maniglia di trascinamento. - Titolo = l'entità,
titleLargepeso 800. - Riga meta con separatori
·,bodyMediumpeso 600inkMuted. - Badge (priorità, stato) in un
Wrap(spacing: 8, runSpacing: 8). - Descrizione integrale,
bodyLargeinterlinea 1.35 — niente troncamenti sul contenuto che serve a decidere. - Riga oraria/assegnatario preceduta da icona 16 px
inkMuted. - 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.
6.6 Pannello di riepilogo
Primo elemento della pagina, Panel con superficie tenue (primaryContainer/infoSurface), padding 18:
- riga di testata: titolo
titleLarge+ data/sottotitoloinkMuted, 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
Expandeduguali, ciascuna: icona 20 px inprimaryDark→ valoretitleMedium→ etichetta 12 pxinkMutedsu una riga sola con ellissi; - eventuale riga di stato in coda (
labelMediuminkMuted), 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:
ChoiceChipStadiumBorder,showCheckmark: false, l'etichetta include il conteggio (Tutte · 12). A riposo: superficiesurfaceMuted, bordodivider, testoinkMutedpeso 600. Selezionato: superficieinfoSurface, bordoprimary, testoprimaryDark. Disposti in una riga orizzontale scorrevole alta 44 px, spaziatura 8. - Chip-entità (selezione di un oggetto):
Material+InkWellconStadiumBorder, padding 12×8, icona 16 px + etichetta peso 600 + dato secondario 12 pxinkMuted. Selezionato = superficiesurfacee bordoprimaryDark; non selezionato =surfaceMutede bordodivider.
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:
- cerchio-icona 48×48 riempito con la superficie tenue del tono, icona del tono 26 px;
- 16 px → titolo
titleMedium; - 8 px → messaggio
inkMutedinterlinea 1.45; - 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:
- testata: padding
(20, 0, 8, 12), titolotitleLarge+ sottotitoloinkMuteda sinistra,IconButtondi chiusura a destra; - eventuale segmented button a piena larghezza (
expandedInsets: EdgeInsets.zero) quando lo sheet ha due modalità; Divider(height: 1)— l'unica linea di struttura;- corpo in
Expanded, scrollabile, padding 16. Con più modalità si usaIndexedStack, che preserva lo stato dei form.
6.15 Navigazione
- Telefono:
NavigationBarflottante — avvolta inSafeArea(minimum: (16, 0, 16, 12))e in unClipRRect(borderRadius: 24), conextendBody: truesullo Scaffold, così il contenuto scorre sotto. Le pagine lasciano ~110–120 px di padding inferiore per non finirci sotto. - Da 720 px:
NavigationRaila sinistra con etichette su tutte le voci, seguito daVerticalDivider(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
actionsdell'AppBar:IconButtoncon icona outline dentro unBadgenumerico 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:
SafeArea→Center→SingleChildScrollView(padding: 24)→ConstrainedBox(maxWidth: 440)→ colonnastretch. - Ordine: logo (altezza 128, dentro
Semantics(image: true, label:)) → 32 px → campi a 16 px di distanza → 24 px → eventuale messaggio d'errore centrato incolorScheme.erroreliveRegion→ 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)conCircularProgressIndicator(strokeWidth: 2)), restando della stessa dimensione; durante il caricamentoonPressed: null. - I campi dichiarano
autofillHintsdentro unAutofillGroupe incatenanotextInputAction: 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.tooltipsu ogniIconButtonsenza 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
- Copiare la struttura dei token: quattro classi statiche
Colors,Space,Radius,Breakpointscon i valori dei §3 (spazi, raggi e breakpoint identici) e i propri colori sui ruoli del §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).
- 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. - Definire le mappature di stato dell'app (
priorityBadge,statusBadge, o gli equivalenti del dominio) in un unico punto, accanto ai componenti. - 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.
- 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.
- nessuna ombra sulle superfici (elevation 0) e nessun