laif-ds 1.0.2-app-data-grid.88 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/dist/CHANGELOG.md +3 -320
  2. package/dist/agent-docs/adoption-report.json +84 -164
  3. package/dist/agent-docs/components/AppCard.md +21 -25
  4. package/dist/agent-docs/components/AppEditor.md +13 -18
  5. package/dist/agent-docs/components/AppSelect.md +0 -51
  6. package/dist/agent-docs/components/Confirmer.md +3 -0
  7. package/dist/agent-docs/components/DataTable.md +3 -34
  8. package/dist/agent-docs/components/DatePicker.md +7 -8
  9. package/dist/agent-docs/components/DropdownMenu.md +0 -2
  10. package/dist/agent-docs/components/FileUploader.md +12 -12
  11. package/dist/agent-docs/components-list.md +0 -63
  12. package/dist/agent-docs/manifest.json +34 -270
  13. package/dist/components/design-tokens.js +2 -138
  14. package/dist/components/ui/app-select.js +222 -334
  15. package/dist/components/ui/button.js +1 -2
  16. package/dist/components/ui/confirmer.js +108 -26
  17. package/dist/components/ui/date-picker.js +34 -36
  18. package/dist/components/ui/dropdown-menu.js +3 -3
  19. package/dist/components/ui/tables/data-cross-table/data-cross-table.js +6 -6
  20. package/dist/components/ui/tables/data-table/data-table-constants.js +16 -36
  21. package/dist/components/ui/tables/data-table/data-table-i18n.js +1 -135
  22. package/dist/css-for-template.css +0 -27
  23. package/dist/hooks/use-mobile.js +1 -0
  24. package/dist/index.d.ts +61 -5880
  25. package/dist/index.js +402 -468
  26. package/dist/styles.css +1 -2
  27. package/dist/styles.v3.css +1 -1
  28. package/package.json +7 -25
  29. package/dist/agent-docs/components/AppDataGrid.md +0 -2603
  30. package/dist/agent-docs/components/GridFilterPanel.md +0 -112
  31. package/dist/agent-docs/components/SidebarMenuBadge.md +0 -87
  32. package/dist/components/ui/app-data-grid/app-data-grid.js +0 -896
  33. package/dist/components/ui/app-data-grid/engine/use-grid-column-resize.js +0 -110
  34. package/dist/components/ui/app-data-grid/engine/use-grid-engine.js +0 -618
  35. package/dist/components/ui/app-data-grid/engine/use-grid-infinite-loader.js +0 -50
  36. package/dist/components/ui/app-data-grid/engine/use-grid-virtualizer.js +0 -90
  37. package/dist/components/ui/app-data-grid/grid-aggregate.js +0 -5
  38. package/dist/components/ui/app-data-grid/grid-column-registry.js +0 -28
  39. package/dist/components/ui/app-data-grid/grid-columns.js +0 -204
  40. package/dist/components/ui/app-data-grid/grid-filter-client.js +0 -73
  41. package/dist/components/ui/app-data-grid/grid-filter-compile.js +0 -33
  42. package/dist/components/ui/app-data-grid/grid-filter-operators.js +0 -141
  43. package/dist/components/ui/app-data-grid/grid-grouping.js +0 -104
  44. package/dist/components/ui/app-data-grid/grid-header-matrix.js +0 -112
  45. package/dist/components/ui/app-data-grid/grid-i18n.js +0 -208
  46. package/dist/components/ui/app-data-grid/grid-mount-count.js +0 -8
  47. package/dist/components/ui/app-data-grid/grid-reducer.js +0 -200
  48. package/dist/components/ui/app-data-grid/grid-result-scope.js +0 -35
  49. package/dist/components/ui/app-data-grid/grid-tokens.js +0 -169
  50. package/dist/components/ui/app-data-grid/grid-view-preset.js +0 -97
  51. package/dist/components/ui/app-data-grid/parts/grid-actions-row.js +0 -86
  52. package/dist/components/ui/app-data-grid/parts/grid-cell-text.js +0 -67
  53. package/dist/components/ui/app-data-grid/parts/grid-column-registry-panel.js +0 -320
  54. package/dist/components/ui/app-data-grid/parts/grid-density-control.js +0 -88
  55. package/dist/components/ui/app-data-grid/parts/grid-filter-chips.js +0 -234
  56. package/dist/components/ui/app-data-grid/parts/grid-filter-editor.js +0 -201
  57. package/dist/components/ui/app-data-grid/parts/grid-filter-panel.js +0 -386
  58. package/dist/components/ui/app-data-grid/parts/grid-filter-row.js +0 -277
  59. package/dist/components/ui/app-data-grid/parts/grid-fullscreen-toggle.js +0 -28
  60. package/dist/components/ui/app-data-grid/parts/grid-group-row.js +0 -135
  61. package/dist/components/ui/app-data-grid/parts/grid-header.js +0 -338
  62. package/dist/components/ui/app-data-grid/parts/grid-pagination-control.js +0 -238
  63. package/dist/components/ui/app-data-grid/parts/grid-portal-container.js +0 -14
  64. package/dist/components/ui/app-data-grid/parts/grid-row.js +0 -181
  65. package/dist/components/ui/app-data-grid/parts/grid-scroll-area.js +0 -186
  66. package/dist/components/ui/app-data-grid/parts/grid-selection-bar.js +0 -195
  67. package/dist/components/ui/app-data-grid/parts/grid-status-surface.js +0 -66
  68. package/dist/components/ui/app-data-grid/parts/grid-table.js +0 -482
  69. package/dist/components/ui/app-data-grid/parts/grid-toolbar-slots.js +0 -224
  70. package/dist/components/ui/app-data-grid/use-grid-filters.js +0 -173
  71. /package/dist/agent-docs/components/{TruncatedCell.md → truncated-cell.md} +0 -0
package/dist/CHANGELOG.md CHANGED
@@ -5,329 +5,12 @@ All notable technical changes to the laif-ds core will be documented in this fil
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
- ## [Unreleased]
9
-
10
- ### 🐛 Fixed — overlay di AppDataGrid a schermo intero e toggle della riga (2026-09-23)
11
-
12
- - **AppDataGrid**: **A schermo intero il pannello di una cella della Riga Zero si apriva SOTTO la griglia.** La radice è `fixed` a `gridZ.fullscreen` (110), e il pannello — con l'elenco degli operatori, gli `AppSelect` e il calendario del `DatePicker` dell'editor — si portalava nel `<body>` a `z-[60]`: invisibile e irraggiungibile. Ora pannello, elenco degli operatori, elenchi e calendario di `GridFilterValueEditor`, compositore «da … a …» (`GridFilterRangeComposer`) e menu delle pill (`GridFilterChips`) si montano **dentro `[data-app-data-grid]`**, risolta dal DOM (`parts/grid-portal-container.ts`), come già il pannello delle colonne. Fuori da una griglia — le parti sono export pubblici — il portale resta il `<body>`. `Escape` chiude un livello alla volta e non esce dallo schermo intero finché un overlay è aperto. Prova: `AppDataGrid/Overlay a schermo intero e toggle della riga` (hit test con `elementFromPoint` al centro di ogni overlay). ⚠️ Chi cercava `[data-grid-filter-cell-panel]` con `closest("[data-app-data-grid]") === null` deve aggiornare l'asserzione.
13
- - **AppDataGrid**: ⚠️ **`toolbar.filterRowToggle.render` riceve `{ open, toggle }`** (`GridFilterRowToggleRenderProps`, nuovo slot `GridToolbarFilterRowToggleSlot`, entrambi esportati). Prima `render` non riceveva niente, e il TSDoc prometteva di governare `filterRowOpen` da fuori — prop che non esiste: lo stato della riga è locale. `open` dice se la riga è montata, `toggle` è lo stesso gesto del bottone della griglia (annuncia una volta a `onFilterRowOpenChange`). Un `render` senza parametri resta valido; `() => null` resta un errore del compilatore.
14
- - **DatePicker**: nuova prop opzionale **`portalContainer?: HTMLElement | null`** — l'elemento in cui si monta il calendario (difetto: il `<body>`, come prima).
15
- - **DropdownMenu**: `DropdownMenuContent` accetta **`container?: HTMLElement | null`** — l'elemento in cui si monta il menu (difetto: il `<body>`, come prima).
16
-
17
- ### 🔧 Changed — paginatore di AppDataGrid (2026-09-23)
18
-
19
- - **AppDataGrid**: ⚠️ **Breaking-ish — il difetto di `pageSize` passa da `10` a `25`.** Chi montava la griglia paginata senza dichiarare la taglia ora vede 25 righe per pagina (e 25 scheletri durante il caricamento). La taglia resta dichiarabile dall'esterno con la prop `pageSize`, con `pagination.pageSize` o con `initialState.pagination.pageSize` (precedenza invariata: `initialState` > slot > prop). `GRID_PAGE_SIZE_OPTIONS` resta `[10, 25, 50, 100]`. Decisione esplicita del proprietario.
20
- - **AppDataGrid**: ⚠️ **Nuovo formato dell'indicatore di pagina: «Pagina 2/4»** (`${i18n.pagination.page} ${i}/${N}`) al posto di «2 / 4». Chi leggeva `[data-grid-page-indicator]` per testo (`split("/")`, confronti verbatim) deve aggiornare le proprie asserzioni.
21
- - **AppDataGrid**: Il selettore «righe per pagina» è ora l'**`AppSelect` del DS** (taglia `sm`) invece di un `<select>` nativo. `[data-grid-page-size]` resta, ma sta sul WRAPPER: il controllo è il `[role="combobox"]` al suo interno, la taglia in vigore si legge da `data-grid-page-size-value`, le voci esistono solo a elenco aperto. `aria-label` (`pagination.rowsPerPage`), voci «`${n} ${perPage}`», voce aggiunta per una taglia fuori scala e guardia di identità (nessuna emissione se la taglia non cambia) invariati. L'elenco si monta DENTRO la radice della griglia, così a schermo intero si apre sopra `gridZ.fullscreen` (story `CI-CD/AppDataGrid/Conteggi (C4) → TagliaASchermoIntero`).
22
- - **AppDataGrid**: Nuovo ordine del gruppo sinistro del paginatore: **selettore delle righe per pagina → intervallo («Righe 11-20 di 33») o clausola delle righe → «Pagina i/N»**. I comandi di navigazione restano a destra.
23
- - **AppSelect**: nuova prop opzionale **`portalContainer?: HTMLElement | null`** — l'elemento in cui si monta il portale dell'elenco (difetto: il `<body>`, come prima). Serve quando il select vive dentro un contenitore con un proprio contesto di impilamento.
24
-
25
- ### 🔧 Changed — toolbar di AppDataGrid (2026-09-23)
26
-
27
- - **AppDataGrid**: ⚠️ **I tre comandi di vista sono «visibili ma opzionali».** «Nascondi/Mostra la riga filtri», «Colonne visibili» ed «Entra/Esci da schermo intero» sono resi PER DIFETTO e si spengono con `false` sulla loro chiave di `toolbar`: `fullscreen?: GridToolbarAlwaysSlot | false`, `columns?: GridToolbarSlot | false`, e il NUOVO slot `filterRowToggle?: GridToolbarAlwaysSlot | false` (reso solo con `filterMode="row"`). Chiave omessa = bottone della griglia, `{ render }` = sostituzione. `true` e le stringhe restano errori del compilatore. ⚠️ **Cambio di comportamento:** il registro colonne compare ora anche SENZA `toolbar.columns`, quando la visibilità è scrivibile (sempre senza la prop `columnVisibility`, oppure con `columnVisibility` + `onColumnVisibilityChange`); chi non lo vuole passa `columns: false`. `search` resta sempre reso. Decisione esplicita del proprietario, che ritira la decisione 26 per queste tre chiavi: i probe di `type-probes/shell-slots.probe.tsx` sono stati riscritti (`ProbeShellSlotsFullscreenBooleano` → `…FullscreenVero`/`…FullscreenStringa`, più `…ColonneVero`, `…RigaFiltriVero`, `…RigaFiltriRenderVuoto`).
28
- - **AppDataGrid**: I tre bottoni usano il `Button` del DS con **`variant="ghost"`** e senza bordo, con la veste nuova `gridToolbarGhostTriggerClass` (stessa taratura `h-6 px-2 text-xs` e anello di fuoco di `gridToolbarTriggerClass`). Nomi accessibili, `data-grid-*`, `aria-expanded` invariati.
29
- - **AppDataGrid**: **Le azioni extra stanno A DESTRA dello schermo intero.** Ordine del gruppo destro: colonne → schermo intero → prop `actions` (`[data-grid-actions-row]`) → slot `toolbar.actions`, che è ora l'ultimo elemento della toolbar. Diverge di proposito dall'ordine del mockup («Esporta» → «Schermo intero»), e la story di confronto lo asserisce come scostamento dichiarato.
8
+ ## [laif-ds@1.0.3]
30
9
 
31
10
  ### 🚀 Added
32
11
 
33
- - **AppDataGrid**: **i filtri di colonna ora filtrano anche in CLIENT-MODE.** Fino a oggi i badge della Riga Zero raggiungevano solo il ramo server: `state.query.filters` diventava `computedFilter` nel payload emesso, ma **`columnFilters` non veniva mai passato a `useReactTable`** (`engine/use-grid-engine.ts`). In client-mode l'effetto era un comando **BUGIARDO**, che è peggio di un comando morto: scrivendo in una cella la riga restava a schermo, e intanto la cella dichiarava `data-grid-filter-cell-applied="true"` e l'intestazione portava `data-grid-header-filtered` — un test E2E che aspettasse quegli attributi **sarebbe passato su un filtro che non filtra**. Misurato sull'app viva il 2026-09-22 (scheda Ruoli di un utente: valore inesistente, riga che resta; controesempio: la searchbar, che passa dalla stessa porta come `globalFilter`, filtrava 1 riga → 0). ⚠️ **NON È UNA REGRESSIONE**: `git log -S "columnFilters"` restituisce un solo commit, quello di nascita della corsia — il cablaggio non è mai esistito. ⚠️ **E NON CREDERE A `app-data-grid-emitter.stories.tsx`**, che afferma di aver misurato il filtro client battendo in una cella: è falso per costruzione, e la sua stessa play digita nella searchbar. ⚠️ **QUANDO I FILTRI SONO SOLLEVATI (prop `filters`) LA GRIGLIA NON FILTRA IN LOCALE**, ed è una condizione, non una svista: chi solleva lo stato possiede anche il filtraggio, e senza quel gate un call-site esistente avrebbe subito un **doppio filtraggio** — `app-data-grid-filters-lifted.stories.tsx` monta esattamente quella configurazione ed è rimasta verde **senza essere ri-fixturata**, che è la prova che la condizione è quella giusta. ⚠️ **SI FILTRA SULLA STESSA POPOLAZIONE CHE IL SERVER RICEVE** (`usableBadges`): filtrare su una popolazione diversa è il modo in cui la stessa griglia filtra in due modi secondo la modalità. ⚠️ **LA PARITÀ CON I DICIOTTO BACKEND NON È SORVEGLIATA DA NESSUN GATE** — `check-datatable-grid-parity` confronta il vocabolario degli operatori, non la loro semantica — quindi le sette divergenze note (logica a tre valori sui `NULL`, metacaratteri di `LIKE` che il backend non escapa, case folding della collation, `eq` su testo, float64 contro `NUMERIC`, fuso e strettezza delle date, `array_overlap`) **sono scritte nel TSDoc di `grid-filter-client.ts`**, non in un documento di analisi: una funzione che promette una parità che non ha, senza dirlo nel file, è lo stesso difetto che questa corsia tratta come tale altrove. Due di quelle classi, peraltro, **sono già spedite oggi** dalla searchbar client-vs-server: questa funzione allarga un rischio accettato, non ne apre uno nuovo. Dieci scene nuove in `app-data-grid-filtro-client.stories.tsx`.
34
- - **AppDataGrid**: Nuovo slot **`toolbar.resetFilters`** (`GridToolbarResetSlot` = `GridToolbarSlot` + `onReset?: () => void`) e nuova chiave i18n **`filters.restoreDefaults`** (inglese `"Restore defaults"`, italiano `"Ripristina i predefiniti"` in `stories/app-data-grid-i18n-it.ts`). La griglia rende un `<button data-grid-toolbar-reset-filters>` con l'icona `RotateCcw` e la veste condivisa `gridToolbarTriggerClass`, **subito dopo la ricerca e prima del grilletto «Filtri»** quando quest'ultimo c'è. ⚠️ **LA VISIBILITÀ È DEL CONSUMER, E NON È UNA COMODITÀ: È L'UNICA FORMA POSSIBILE.** La griglia non può calcolare «i filtri correnti differiscono dai default della pagina», perché i default non le arrivano mai — `initialState` è un SEME letto una volta (il `useMemo` con dipendenze `[]` in `engine/use-grid-engine.ts`, non tenuto in nessun ref e mai confrontato con niente) e `preset` porta la vista **ripristinata dell'utente**, non i default. Quindi la risposta è la **presenza di `onReset`**: senza `onReset` e senza `render` il nodo non si rende, esattamente come per `filters` e `actions`. ⚠️ **E LA GRIGLIA NON AGGIUNGE NESSUN GATE SUL CONTEGGIO DEI FILTRI**, che sarebbe la scorciatoia ovvia e sarebbe **sbagliata in un caso reale**: `gridActiveFilterCount(…) > 0` nasconderebbe il bottone quando l'utente ha **tolto** un filtro dichiarato dalla pagina — cioè proprio quando il ripristino ha lavoro da fare. ⚠️ **AMBITO DICHIARATO: FILTRI E RICERCA.** Ordinamento, taglia di pagina, larghezze, ordine e visibilità delle colonne devono **sopravvivere**, e siccome `query/preset` e `view/preset` **sostituiscono in blocco**, chi implementa `onReset` deve costruire un preset che porta i **default** per `filters`/`search` e i valori **correnti** per tutto il resto — un preset parziale non esiste. Il ritorno a `pagination.pageIndex = 0` è inevitabile e corretto: è ciò che fa qualunque cambio di filtro. ⚠️ **NON SOSTITUISCE «Ripristina vista predefinita»** nel menu azioni, che è la via d'uscita TOTALE da una vista salvata illeggibile: i due comandi coesistono e hanno ambiti diversi. ⚠️ **`render` SI RENDE COMUNQUE**: su quel ramo la condizione «differisce dai default» è per intero del consumer — la presenza di `onReset` governa il SOLO nodo che la griglia disegna. ⚠️ **IL FUOCO È GESTITO, E MISURATO.** Il bottone rimuove la condizione della propria esistenza: premuto, il consumer smette di passare `onReset` e il nodo si smonta. Tenerlo montato e `disabled` sarebbe il comando morto che questa corsia vieta, quindi al clic il fuoco si sposta sul **campo di ricerca** — l'altro comando che restringe i dati, il vicino sinistro, e l'unico bersaglio reso SEMPRE. **Non** è il grilletto «Filtri»: quello esiste solo se lo slot `filters` è dichiarato, e nella configurazione più diffusa (`filterMode: "row"`) non c'è affatto — era la ragione per cui la v1 aveva dichiarato il limite come inevitabile, e la ragione era sbagliata perché il bersaglio c'era. La play `IlRipristinoChiedeENonAgisce` **legge `document.activeElement` dopo il click** e pretende il campo di ricerca. Chi passa `render` gestisce il fuoco del proprio nodo da sé. ⚠️ **NON SI RIUSA `i18n.reset`**: quella stringa nomina il bottone di `GridFilterChips` che **svuota** i filtri applicati, questa nomina il gesto che li riporta a ciò che la pagina ha **dichiarato** — con default non vuoti i due esiti divergono, e in italiano «Azzera» e «Ripristina i predefiniti» non sono la stessa frase. ⚠️ **Il cambiamento è additivo e retrocompatibile**: due chiavi opzionali nuove, nessun consumer esistente compila diversamente e nessun nodo nuovo appare senza che qualcuno lo dichiari. L'unico `tsc` che diventa rosso è interno al repo (`gridI18nIt` è tipato `typeof defaultGridI18n` e il suo `filters` è un literal fresco), ed è il meccanismo voluto: la chiave non si spedisce non tradotta. ⚠️ **`GridToolbarResetSlot` NON è esportato dal barrel**, come i fratelli `GridToolbarSlot` e `GridToolbarDisclosureSlot`: `GridToolbarSlots` — che è esportato — basta per annotare, e nessun nome nuovo entra in `scripts/codemod/rename-map.json`. **Cinque story nuove** in `AppDataGrid/Guscio (C3)`, di cui due sulla POSIZIONE e **una sulla configurazione del consumer reale** (`filterMode="row"` **senza** slot `filters`, dove il grilletto «Filtri» non esiste affatto e l'ordine misurato è ricerca → ripristino → «Mostra la riga filtri»): senza quella, l'ordine sarebbe stato misurato solo rispetto a un nodo che il consumer non rende mai
35
-
36
- - **AppDataGrid**: Nuova story `AppDataGrid/Completa → ScenaDipinta` — le **cinque personalizzazioni del chiamante accese nello stesso posto**, che il repo esponeva già una per una e non mostrava mai insieme: **header in HTML custom** (le due intestazioni di gruppo e quella foglia di «Stato», via `header: ReactNode` + `headerLabel`), **celle in HTML custom** (`cell` — avatar con le iniziali derivate dalla ragione sociale, e la barra di avanzamento), **celle colorate** (`cellClassName` a funzione, una tinta per ciascuno dei quattro stati), **righe colorate** (`rowClassName` a funzione, sul settore) e **colonne colorate** (`headerClassName` + `cellClassName` statica sulle due superfici di «Totale»). **Nessuna prop nuova**: sono cinque canali che il contratto ha già, e la story è una dimostrazione, non un allargamento di superficie. ⚠️ **UNA COLONNA PER DIMOSTRAZIONE, e non è pignoleria**: tinta di colonna e tinta per valore sulla stessa colonna sarebbero due `bg-*` nello stesso `cn()` e `tailwind-merge` ne terrebbe **una**, quindi la scena mostrerebbe quattro cose dichiarandone cinque e nessuna play potrebbe dire quale ha vinto — da qui la mappa, che è anche l'ordine delle **sei** colonne: `Commessa` (110) in chiaro con `rowHeader`, `Cliente` (170) cella custom, `Referente` (130) in chiaro, `Stato` (150) header custom **e** celle colorate, `Avanzamento` (150) cella custom, `Totale` (120) colonna colorata. ⚠️ **Header custom e tinta per valore CONVIVONO sulla stessa colonna** e la regola non lo vieta: quella riguarda due `bg-*` nello stesso `cn()`, non un `<th>` e dei `<td>` — sono due elementi, due prop, e due asserzioni separate. ⚠️ **`Referente` non è riempitivo**: è l'unica colonna senza tinta propria, cioè l'**unico** posto da cui si vede il fondo della **riga** — ed è per questo che la play misura lì il colore risolto. ⚠️ **SEI COLONNE PER 830px, E IL NUMERO È MISURATO**: con barra laterale e pannello addon aperti — come la story si apre — il viewport della griglia misura **850px** di `clientWidth`. La prima stesura aveva sette colonne per **1120px** dichiarati, onorati esatti dal `<colgroup>`, e proprio per questo `Totale` e `Consegna` cadevano **fuori**: la dimostrazione della colonna colorata si vedeva solo scorrendo, e una vetrina che chiede di scorrere per vedere una delle cinque cose che dichiara non le sta mostrando. Da qui la somma sotto la misura e `Consegna` — la seconda linea di base, la meno portante — via, verificato in Chrome: nessun contenitore scrollabile in orizzontale e il bordo destro di `Totale` dentro quello della griglia. ⚠️ **«In pausa» è `slate-200` e non `zinc-100`**, per una ragione che sta a schermo e non nei numeri: `zinc-100` è misurabilmente diverso da `amber-50` — l'asserzione sulle quattro stringhe distinte passava — ma accanto alla tinta di riga si legge come la stessa carta crema, cioè due tinte distinte che sembravano una. ⚠️ **NIENTE COLONNE PINNATE E NIENTE `checkable` in questa scena**, per due limiti già misurati e già scritti nel repo: lo sfondo opaco di una cella pinnata copre un `bg-*` che arrivi da `rowClassName` (lacuna dichiarata in `AppDataGrid.md` e nel commento di `parts/grid-row.tsx`), e la tinta della selezione è uno stile **inline** su ogni cella, che vince su qualunque utilità (`adg-r19`) — accendere l'una o l'altra renderebbe ambigua la dimostrazione proprio nel punto che deve dimostrare. ⚠️ **LA PRECEDENZA FRA LE TRE TINTE È MISURATA SUL COLORE RISOLTO**, non dedotta dalla cascata: su una riga tinta la play legge `getComputedStyle().backgroundColor` della cella di `Totale` e di quella di `Referente` e asserisce che sono **diversi**, e che la seconda è `rgba(0, 0, 0, 0)` — cioè che la cella in chiaro lascia passare il fondo della `<tr>` invece di ridipingerlo. La sonda è scritta **nel verso giusto**: se Tailwind non emettesse le classi della story i due colori sarebbero uguali e l'asserzione andrebbe **rossa**, non verde. ⚠️ **SEI FALSIFICAZIONI ESEGUITE**, ognuna riportata al verde subito dopo e ognuna rossa sul criterio suo: togliere `cellClassName` dalla colonna colorata (`expected +0 to be 12`); sostituire la tinta di colonna con una classe che Tailwind **non può emettere** — che lascia **verde** il criterio sulle classi e rossa la sonda del colore (`expected 'rgba(0, 0, 0, 0)' not to be 'rgba(0, 0, 0, 0)'`), cioè prova che quella sonda misura il CSS e non il markup; azzerare `rowClassName`; togliere il marcatore dell'header foglia; togliere quello dell'header di gruppo; e azzerare la tinta per valore (`expected 'truncate px-3 text-left' to contain 'bg-emerald-100 text-emerald-900'`). ⚠️ **La play non scrive numeri a mano**: il conteggio atteso delle celle custom è `dataRows(grid).length` letto dal DOM, e stato e settore di ogni riga si riagganciano ai dati per `data-grid-row` (da qui `getRowId={(riga) => riga.id}`, il codice e non l'indice), con `rigaDipintaDi` che **solleva** se una riga del DOM non si ritrova nei dati invece di misurare la riga sbagliata in silenzio
37
-
38
- - **AppDataGrid**: New story `AppDataGrid/Completa → EtichettaDiGruppoNellaColonnaBloccata` — the signature scene with `groupLabelPlacement="first-column"`, the **only grouping prop of the contract the signature scene never passed at all**. It is not a duplicate of the three `AppDataGrid/Raggruppamento (P9)` stories that already cover the prop: those run on synthetic datasets **without `checkable`** (an exclusion declared there for an arithmetic constraint — with the checkbox column `aria-colcount` would be 15, not 14), without a `<tfoot>` and without the three-level header tree, so the composition the mockup actually shows — the label in the first **pinned** column, beside the checkbox cell also pinned at `left: 0` — was measured nowhere. It is the exact complement of `Raggruppata`: there `settore` carries the label and `commessa` is empty, here the inverse, and either play alone would stay green on a component that always puts the label in the same column. It also asserts the label MOVED rather than duplicated (label, toggle and count count 1 each in the group row and 1 each inside the `commessa` cell), that no column was hidden (`provaLAlberoDelleColonne` — fourteen leaves, three levels), and that the toggle still WORKS from there while the `<tfoot>` does not move. ⚠️ **Falsification executed**: dropping `etichettaDiGruppo="first-column"` from the render turns `npm run spec -- app-data-grid-completa` red on **this story only**, with «expected '' to contain 'Edilizia'». ⚠️ **And it retires a FALSE sentence** that stood in `Raggruppata`'s play — «the only way to move it would be to hide the grouping column: the rendered leaves would drop from fourteen to thirteen». That has not been true since `groupLabelPlacement` existed: the component rewrites only `group.columnId` and leaves `engine.groupByColumnIds` intact, so no column is hidden
39
- - **AppDataGrid**: `GridDensityControl` and `GridDensityControlProps` are now **public exports** of `src/components/ui/app-data-grid/index.ts`. It is the same control the toolbar used to render, unchanged: a `role="group"` with the three i18n labels and the `sr-only` full words. It exists as public surface because `adg-c12` removed the grid's own switcher — without this export the file would have had no importer left (dead code) and every consumer wanting the switch would have hand-rolled three buttons, losing the group semantics and the labels. Mount it in `toolbar.actions` to put it back where it was, or anywhere outside the table. ⚠️ **If you also pass a fixed `viewportHeight`, derive it from the LIVE density**: `gridViewportHeight` multiplies `gridRowHeight[density]` by the row count, so a constant `"default"` beside rows rendered taller promises ten rows and fits eight, and the `<tfoot>` covers rows the paginator announces — a mistake that was not possible while the control lived inside the grid
40
- - **AppDataGrid**: `AppDataGrid/Completa → ModalitaFiltri`, the scene the delivery is signed on, now turns on the mockup capabilities that were off while a prop already governed them, and **marks in a dashed orange frame every piece the library does not render** — in three declared provenances, asserted BOTH ways so a frame in the wrong category goes red rather than merely colouring wrong: `controllo` (outside `[data-app-data-grid]`: the deviations note, the filter-mode switch, the body-state switch, the chip row, the density control), `slot` (inside it, but consumer content: `toolbar.actions`, `selection.summary`) and `sonda` (the two `data-scena-*` probes, which were empty zero-height `div`s — their attributes existed and their box did not). Turned on: the **visible status row** with its megaphone (`statusRowVisible`, deviation 20 of the plan, which was `aperto` — closed by one prop, with no component change, since the wrapper was already implemented and the scene simply did not pass it); the **applied-filters chip row** (`GridFilterChips`, deviation 10 — mounted as a framed sibling because there is no slot between the toolbar and the table band, which SHOWS the deviation without closing it: whether the grid or the consumer should render it stays a product decision); and the **four body states** of the mockup's single shell — ready / empty / error with a wired «Riprova» / loading. ⚠️ **The frames carry no background and no `dark:` variant, and that is a measurement**: the first draft used `bg-orange-50/50 dark:bg-orange-950/20`, and in Tailwind v4 `dark:` is a `prefers-color-scheme` media query, not the class Storybook's theme selector toggles — with the Storybook theme on LIGHT and the OS on dark the frame received `oklab(0.266 … / 0.2)`, a dark brown wash over a `rgb(250, 250, 250)` body. Verified after the fix with the real theme switcher (page background `rgb(250,250,250)` → `rgb(10,10,10)`): one single colour triple across all frames in both themes
41
- - **AppDataGrid**: New data grid, exported from `src/components/organisms/index.ts` together with `AppDataGridProps` — native `<table>` semantics with single and multi-column sorting (capped by `maxSortedColumns`), column widths with drag resize and left/right pinning over a measured sticky band, multi-level headers with a derived `id`/`headers` chain, opt-in virtualization, an infinite source with skeleton rows, client-side row grouping with per-column `aggregate` totals rendered in `<tfoot>`, density and full screen, page-scope selection with bulk actions that carry the count in their name, saved views applied in one tick through `preset`/`onPresetApplied`, and filters with two homes — `filterMode: "panel" | "row"`, default `"panel"` — fed by the one badge constructor, so the overlay inventory and the zero row produce the same state; the fifteen requirements behind it are tracked in one place, each next to the command that measures it, in the «Registro dei requisiti» section of `docs/analysis/datatable-v2/PIANO-COMPLETAMENTO.md`, and six of them carry a signed limit, in three different shapes. **Three are partial**: R6 ships the always-reachable horizontal scrollbar but no arrow controls (`grep -c 'scrollLeft' src/components/ui/app-data-grid/parts/grid-scroll-area.tsx` returns 0), R12 ships multi-level headers but not moving or grouping columns from the header, and R14 has no prop at all (open-at-first-paint is a story default, not a contract). **Two ship in a different shape than the wording asked for**: R8 as a port — `GridViewStore` plus `createInMemoryGridViewStore`, no persistence and no server adapter — and R9 as a `Collapsible` rather than a `DropdownMenu`. **And one is half-closable here by nature**: the editorial half of R3 — deciding which filters deserve to live outside the table, scene by scene — is a choice of whoever mounts the grid, so the design system makes it possible without being able to take it. The same six are listed in the «I SEI LIMITI FIRMATI» block at the top of `src/components/ui/app-data-grid/app-data-grid.tsx`
42
- - **AppDataGrid**: New Storybook story `AppDataGrid/Completa` (`src/components/stories/app-data-grid-completa.stories.tsx`) replays the mockup scene with the real engine. **Fourteen leaf columns** over three header levels — twelve of data (Commessa, Ragione sociale, Referente, Settore, Stato, Avanzamento, Q1, Q2, Q3, Q4, Totale, Consegna) plus the two service columns, the checkbox column and the row menu — measured on the rendered table as `aria-colcount="14"` in both scenes, the same count the mockup makes in `COMPLETE_LEAF_COLUMNS`, where `sel` and `azioni` are leaves in their own right. Five cell renderers declared through `cell`: status badges, an initials avatar **derived** from the referent (in the mockup the initials were a field of the record), a progress bar with its percentage, `it-IT` amounts and `gg/mm/aaaa` dates — none of which the grid renders on its own, all of which are the call-site's, and all of which the previous shape of this story was missing. Group rows whose aggregates come from `aggregatedCell` and a `<tfoot>` whose row count and average come from `aggregate`; single-level grouping on the sector; the `columns` and `actions` toolbar slots («Colonne», «Esporta»); the search copy overridden per story by passing a whole `i18n` object; a selection bar with three actions and its own `summary` render; and a mute row-menu column (`header: ""` plus `headerLabel: "Azioni"`) — every piece coming from props and none from a story shell. It is split into two exports, `ModalitaFiltri` (paginated, no groups) and `Raggruppata` (grouped, no pager), because `paginated` does not exist on the type branch that accepts `groupBy` (`grep -n 'paginated?: never' src/components/ui/app-data-grid/app-data-grid.types.ts` returns 982); an on-screen note (`data-testid="nota-scostamenti"`) names what the target will not have — the mockup's «Seleziona tutte le 128 righe» link, since selection is page-scope only and promotion to the whole result set is not even writable (`GridSelectionSlots.mode` is a one-member union), the two filter forms left out by decision 9 («frase in prosa» and «domande pronte»), that split scene, and the group label that falls outside the pinned column — plus deviation 12, which is not an absence: the scene starts with zero applied filters where the mockup starts with six and a «Filtri (6)» badge, a bench choice made so that every count in the scene stays derivable from the corpus. The deviations from the mockup are tabulated one by one, each with its proposed outcome, in the «Scostamenti dal mockup» section of `docs/analysis/datatable-v2/PIANO-COMPLETAMENTO.md`: **thirty-seven rows, twenty-two `accettato`, seven `aperto`, seven closed by a task and one half-closed** — counts DERIVED from the table by a command, not typed by hand: `awk -F'|' '/^\| # \| Scostamento \|/{t=1;next} t&&!/^\|/{t=0} t&&/^\| *[0-9]+ *\|/{e=$(NF-1);gsub(/^ +| +$|`/,"",e);c[e]++;n++} END{print n; for(k in c) print k, c[k]}' docs/analysis/datatable-v2/PIANO-COMPLETAMENTO.md`
43
- - **AppDataGrid**: ⚠️ **What the remounted scene does NOT close, and nothing here claims it does.** **Seven** of the **thirty-seven** tabulated deviations stay `aperto` — rows **10, 13, 14, 20, 22, 27, 37** of the table in `docs/analysis/datatable-v2/PIANO-COMPLETAMENTO.md`. ⚠️ **The numbers and the row list are DERIVED from that table by a command, never typed**, because a previous wording of this entry enumerated ten items, called them nine, put an `accettato` row among them and dropped an `aperto` one: `awk -F'|' '/^\| # \| Scostamento \|/{t=1;next} t&&!/^\|/{t=0} t&&/^\| *[0-9]+ *\|/{n=$2;gsub(/ /,"",n);e=$(NF-1);gsub(/^ +| +$|`/,"",e);tot++;if(e=="aperto")printf "%s ",n} END{printf "\\nof %d\\n",tot}' docs/analysis/datatable-v2/PIANO-COMPLETAMENTO.md`. The split is **five of the component** (13, 14, 20, 22, 27), **one of governance** (10, the chip row) and **one of toolbar composition** (37, opened by `adg-r14`: with the «N filters out of view» indicator gone the row still wraps, by **7px**). ⚠️ **The prose below describes an earlier, larger set** — rows 15, 17, 19, 24, 25 and 34 have since been closed by `adg-m1`/`adg-m2`/`adg-m3`/`adg-m6`, and the table is the authority, not this paragraph. **The component ones still open**: the group label falls in the grouping column instead of the pinned one and reads «Edilizia 78 righe» rather than «Edilizia (78)» (two rows, placement and form); «Schermo intero» has no visible label, only an `aria-label`; the search keeps the primitive's stacked geometry — label above, full-width field, no inline lens — and only its **copy** was closed; the scope figure «Righe 1-10 di 128» is not rendered, and — this is the correction that matters — it is **not** a prop the story forgot: `resultTotal`exists only on`GridClientProps` (`app-data-grid.types.ts:1088`), the branch that declares `paginated?: never`(line 982), so passing it next to`paginated`is`error TS2322`, measured with `tsc --noEmit`on the real file; the status line is`sr-only`; the selection bar's «N righe selezionate su M in pagina» is a fixed template literal that `summary`cannot replace; the two trailing skeleton rows do not exist, and chasing them with`loading`would delete every data row; the viewport is **64px shorter than its content** with three header levels (measured on the rendered scene:`scrollHeight - clientHeight`= 64 = 2 ×`designTokens.table.headerHeight`), because `gridViewportHeight`counts one header level and`GridViewportHeightInput` has no input for how many there are — the consumer-composed viewport height of Fase 9 is still a pact the grid does not keep for you; the order of the toolbar slots is fixed in JSX; and the paginated grid announces **`aria-rowcount="10"`** over 128 rows where the mockup announces 133, which the component's own source already records as a defect of its own and which the story cannot fix for the same TS2322 reason. **The one governance row** is the chip row §11.3-A, which no signed decision covers and which the mockup itself asks to renegotiate — it had disappeared from this entry entirely. **The one story row** is that the scene opens with every `aria-sort`at`none`where the mockup opens sorted by Settore then Totale; it stays open because nobody has written *why*, and the decision comes before the code. ⚠️ Two of the thirteen — the toolbar order and the missing initial sort — carry **no written reason anywhere**, and the table records that instead of inventing one. ⚠️ The filter trigger reading «Filtri» with no count is **not** in this list: it is row 18 and it is`accettato`, with its reason in the table
44
- - **AppDataGrid**: ⚠️ **NO LONGER TRUE as of the AUDIT 2026-09-09 fixes below — kept here because a consumer who read it needs to know it changed.** The whole lane surface now leaves the package barrel: `src/components/organisms/index.ts` re-exports **all 114** names of `src/components/ui/app-data-grid/index.ts` (verified with the TypeScript AST, not a grep: 0 missing, 0 collisions), so every prop listed below can now be annotated and not merely passed. What follows is the original entry, left for the record — Part of the grid's surface was reachable only from `src/components/ui/app-data-grid/index.ts` and not yet from the package barrel — `grep -c '<name>' src/components/organisms/index.ts` returns 0 for `GridColumnAggregate`, `GridFilterMode`, `GridFilterType`, `GridSelectionSlots`, `GridSelectionAction`, `GridInitialOpenGroups`, the `GridFilterControl` family (`GridFilterControl`, `GridFilterControlProps`, `GridFilterControlRegistry`) and the two runtime values `createGridCustomFilterBadge` and `GRID_CUSTOM_FILTER_COLUMN_PREFIX`, so the props typed by them (`aggregate`, `filterMode`, `filterType`, `selection`, `filterControls`, `initialOpenGroups`) can be passed but not annotated by a consumer of the package
45
-
46
- ### 🗑️ Removed
47
-
48
- - **AppDataGrid**: ⚠️ **BREAKING — the density switcher is gone from the grid, and `toolbar.density` is gone from the type.** `AppDataGrid` no longer renders any density control in any configuration: the key was **deleted** from `GridToolbarSlots` rather than moved to the opt-in family, so `toolbar={{ density: {} }}` is now `error TS2353: Object literal may only specify known properties, and 'density' does not exist in type 'GridToolbarSlots'` — measured on `tsc -p tsconfig.probe.json`, not guessed. Row height is set by the `density` prop, **default `"default"` (standard, 36px), unchanged**. ⚠️ **This RETRACTS the P6 rule for that one key**: the «always rendered» family drops from three to two (`search`, `fullscreen`), for which decision 26 still holds in full. The retired argument was «a table whose density changes only if the call-site remembered a boolean is a table whose density, in practice, never changes» — a rule about the end user's experience, imposed on every call-site; density is view configuration, and whoever mounts the table is who knows whether its rows should be tight or roomy. ⚠️ **Two consequences that are not cosmetic.** First, `changeDensity` was the ONLY caller of the `view/density` dispatch, so with the control gone **omitting the `density` prop freezes density at `"default"` for good** — the reducer slice is now written only by `applyPreset`; density is de facto always controlled. Second, `onDensityChange` had no caller left at all, which would have made it a public, documented prop the component never invokes: it is now fired on **preset application** (`onDensityChange?.(preset.view.density)`), a gap that existed before this change and was masked by the toolbar control. ⚠️ **Declared limit, not fixed here:** on a grid with `density` passed, a preset's density cannot win — the render picks the prop — so the preset _announces_ its density without imposing it; who should win is an open product decision. ⚠️ **The type probes were REWRITTEN, not disabled**, and the distinction is the point: `ProbeShellSlotsDensitaBooleano` and `ProbeShellSlotsDensitaRenderVuoto` would have stayed red — hence the gate green — but for the WRONG reason (unknown property instead of incompatible value), i.e. they would have kept printing «N probe su N» while measuring nothing about decision 26. They are replaced by one `ProbeShellSlotsDensitaChiaveInesistente`, which must use an **inline literal** because `GridToolbarSlots` has only optional keys and an extra property arriving from a typed variable stays assignable — excess-property checking applies to fresh literals only. Its negative proof was **executed**: re-adding `density?: GridToolbarAlwaysSlot` turns `scripts/check-grouping-server-unrepresentable.mjs` red on that probe by name, and reverting turns it green (16 probes / 16). The gate itself grew one field for this — the expected type name, defaulting to `"AppDataGridProps"` — because a `TS2353` names `GridToolbarSlots` instead, and the hard-coded string would have reported a falling probe as compiling
49
-
50
- - **AppDataGrid**: The «N filters out of view» toolbar indicator is **gone**, together with everything that fed it — the `[data-grid-filter-row-offscreen]` button, the per-scroll geometry measurement of the zero row's cells, the reveal arithmetic (`gridFilterRowRevealScrollLeft` and its `GridFilterRowRevealInput`), the `GridFilterRowOffscreenIndicator` component, and **two i18n keys**: `filters.filterRowOffscreen` from `GridI18n` (the optional field, the `Required<Pick<…>>` list of `defaultGridI18n` and its English value) and its Italian counterpart in the stories' `gridI18nIt`. ⚠️ **This is a source-breaking change for anyone who passes a full `i18n` object literal**: an object that still carries `filters.filterRowOffscreen` no longer compiles, because TypeScript's excess-property check rejects an unknown key on an object literal; delete the key, there is no replacement. ⚠️ **And it is a WITHDRAWAL of a signed criterion, not a cleanup** — criterion 7 of Phase 9 is now revoked in both halves (`adg-r13` took the chip band, this change takes the indicator), so constraints scrolled out of the horizontal viewport are no longer named and reaching them is scrolling. The reason is toolbar width: the button cost **110px** on a row already over budget. The revocation, its measurements and what is lost with it — including the story's independent oracle for that criterion — are written up in the «Fase 9» section and in deviation **37** of `docs/analysis/datatable-v2/PIANO-COMPLETAMENTO.md`
51
-
52
- ### 🐛 Fixed
53
-
54
- - **AppDataGrid — ⚠️ IN `serverMode` LA PAGINA 2 NON È PIÙ VUOTA: LA GRIGLIA NON RI-TAGLIA LA FETTA CHE IL SERVER HA GIÀ TAGLIATO.** Il motore montava `getPaginationRowModel` e decideva `manualPagination` guardando il solo `paginated`, senza `serverMode`: con `paginated` acceso la griglia ritagliava con il **proprio** `pageIndex` righe che il server aveva già tagliato con lo **stesso** indice — `slice(10, 20)` su dieci righe ricevute, cioè `[]`. ⚠️ **ED ERA UN GUASTO SOLO, NON DUE**: da quel `[]` esce `dataRowCount = 0`, che è lo stesso numero che accende lo stato «Nessun risultato» **e** smonta il paginatore, selettore della taglia di pagina compreso — una pagina vuota senza nessun comando per uscirne, se non un'azione che passa da `restart` o un ricarico. Ogni griglia `serverMode` + `paginated` che rispondesse con la sola pagina richiesta — cioè ogni consumer che manda `limit`/`skip`, il comportamento normale e quello che il contratto documenta — aveva **la pagina 2 e tutte le successive sempre vuote**. Ora vale `tagliaInLocale = paginated && !serverMode` (`engine/use-grid-engine.ts`): la griglia taglia da sé **solo** quando la pagina è sua, ed è la riga che il gemello congelato aveva già giusta (`tables/data-table/data-table.tsx`, `manualPagination: isServerSide`). ⚠️ **NIENTE SI MUOVE PER CHI NON È IN `serverMode`**, e nemmeno il conteggio delle pagine: `getPageCount` usa `options.rowCount`, che l'engine popola da `serverConfig.totalItems`; un consumer server che ometta `totalItems` ricade su `data.length`, identico a prima, perché il taglio avveniva **dopo** quel modello. ⚠️ **QUATTRO BANCHI DI STORY CODIFICAVANO IL COMPORTAMENTO SBAGLIATO** — il loro finto server ritornava l'insieme intero — e sono stati **ri-fixturati, non pinnati**: la scena sbagliata era la fixture, perché un server vero taglia. ⚠️ **E UNA FRASE DI DOCUMENTAZIONE È STATA REVOCATA**: `app-data-grid-completa.stories.tsx` dichiarava la pagina 2 vuota come limite inevitabile («un consumer che rispondesse con la SOLA pagina richiesta vedrebbe la pagina 2 vuota»). Era la descrizione esatta del difetto, scambiata per un contratto. Story di regressione: `LaPaginaDueDelServerNonSiSvuota`, con una sonda sulla pagina **servita** dal finto server, perché senza quella la play era verde per un istante su codice rotto.
55
- - **AppDataGrid — ⚠️ IL BADGE FANTASMA: SVUOTARE UNA CELLA DELLA RIGA ZERO ORA TOGLIE IL FILTRO, NON SOLO IL
56
- VALORE.** Con `filterMode="row"`, scrivere in una cella-filtro e poi **cancellare** lasciava il badge vivo in
57
- `state.query.filters` con `value: ""`. La query ripartiva correttamente senza quel vincolo — `usableBadges`
58
- (`grid-filter-compile.ts`) scarta i badge senza valore — ma **le tre superfici del filtro derivano tutte da
59
- `state.query.filters`**, quindi la colonna restava segnata come filtrata: bordo `border-d-primary` e
60
- `data-grid-filter-cell-applied="true"` sulla casella, imbuto sull'intestazione (`filteredColumnIds`), e
61
- `gridActiveFilterCount` che continuava a contarlo. **E il fantasma era irrimovibile dalla cella**: la «×» si
62
- rende solo `hasValue`, quindi spariva insieme al valore — l'utente vedeva una colonna marcata come filtrata,
63
- non la vedeva filtrare, e non aveva nessun gesto in quella cella per spegnerla. Nei consumer che persistono
64
- la vista il badge finiva **sul server** come `{"value":"","operator":"like"}`, cioè tornava a ogni ingresso in
65
- pagina. Misurato sull'app viva il 2026-09-22. ⚠️ **I VUOTI SONO QUATTRO, NON UNO**, uno per ramo di editor:
66
- `""` (testo), `undefined` (numero: un campo numerico svuotato emette `undefined`, non `Number("")`), `""`
67
- (date/datetime, via `fromDate(undefined)`) e `[]` (lista multipla). Una correzione che guardasse il solo `""`
68
- ne mancherebbe tre. ⚠️ **IL PREDICATO È UNO SOLO E VIVE ACCANTO A CIÒ CHE DECIDE LA «×»**: il nuovo
69
- `isGridFilterValueEmpty` (`grid-filter-operators.ts`, interno alla corsia — **non esce dal barile**, per non
70
- aggiungere una voce a `scripts/check-codemod-rename-map.mjs`) è esattamente `hasValue` negato, e
71
- `parts/grid-filter-editor.tsx` ora lo chiama invece di ricalcolarlo: tenerli separati era garantirsi che un
72
- giorno divergessero, ed è la divergenza a essere il difetto. ⚠️ **`false` E `0` NON SONO VUOTI**: «è falso» su
73
- un booleano e «= 0» su un numerico sono filtri, e un `!value` li sbaglierebbe entrambi. ⚠️ **GLI OPERATORI
74
- SENZA VALORE SONO SALVI**: `eq_null`, `n_eq_null`, `checked`, `unchecked` (`GRID_VALUELESS_OPERATORS`)
75
- filtrano proprio senza valore, e la guardia li esclude esplicitamente. ⚠️ **SI INSTRADA SUL PERCORSO DI
76
- RIMOZIONE CHE ESISTEVA GIÀ**, `clearFilterRowBadge` — coi suoi due rami per i due proprietari dello stato
77
- (`onRemoveFilter` coi filtri sollevati, l'azione `query/filters` con la slice interna, che passa da `restart`
78
- e rifà la lista da pagina 0) — invece di scrivere un secondo modo di togliere un badge; `changeFilterRowValue`
79
- è stata spostata **dopo** quella dichiarazione, o la sua dipendenza cadrebbe nella zona morta del `const`.
80
- ⚠️ **IL PANNELLO NON CAMBIA, ED È DELIBERATO**: `parts/grid-filter-panel.tsx` scrive per un'altra strada
81
- (`onUpdate(badge.id, …)`) perché «Aggiungi filtro» crea un badge col solo operatore, e quel mezzo-badge deve
82
- restare vivo e visibile — per questo la correzione sta nel gestore della Riga Zero e **non** in `usableBadges`
83
- o in `gridActiveFilterCount`, che la romperebbero. ⚠️ **LA LISTA MULTIPLA GUADAGNA UNA RIMOZIONE CHE NON
84
- AVEVA**: il «pulisci» del primitivo emette `[]`, che ora è una rimozione — il costo dichiarato in
85
- `grid-filter-editor.tsx` («il badge resta vivo e senza valore») è pagato, e il commento riscritto. ⚠️ **COSTO
86
- RESIDUO, DICHIARATO**: chi sceglie un operatore non predefinito, scrive, e poi cancella con backspace perde il
87
- badge **e con lui l'operatore scelto** — riscrivendo, il badge rinasce da `createGridFilterBadge` con
88
- l'operatore di default. È lo stesso comportamento che la «×» ha sempre avuto, ma la «×» è un gesto deliberato
89
- e il backspace no. Cinque story nuove lo misurano (`R29:` in `app-data-grid-riga-zero.stories.tsx` e
90
- `app-data-grid-atomo-filtro.stories.tsx`): il gesto su testo e su numero, `0` e «è falso» che restano filtri,
91
- «è vuoto» che sopravvive, e la lista multipla svuotata che rimuove.
92
-
93
- - **AppDataGrid — ⚠️ SECONDA PASSATA DEL DEBOUNCE: ORA ASPETTA ANCHE IL FILTRAGGIO, E IN ENTRAMBE LE
94
- MODALITÀ.** La voce qui sotto aveva coalescito la sola **emissione**: la richiesta al server partiva una
95
- volta, ma la tabella continuava a rifiltrare **a ogni tasto** — e in modalità **client**, dove nessuna
96
- richiesta esiste, il ritardo non comprava niente. Misurato su una griglia client: battendo in una cella di
97
- filtro le righe passavano da 10 a 0 **entro 60ms**. Ora la griglia tiene **due slice**: quella **viva**, che
98
- gli input mostrano (la searchbar è controllata da `state.query.search`, gli editor leggono
99
- `state.query.filters`), e quella **confermata**, che filtra le righe e viene emessa — commit ed emissione
100
- sono **lo stesso evento**, così la tabella comincia a filtrare con lo stesso scatto di query che il consumer
101
- riceve. Misurato dopo: sei battute lasciano il conteggio righe **fermo** (un solo valore distinto su sei
102
- campioni) e l'input mostra già il testo intero; alla chiusura della finestra le righe si muovono una volta.
103
- ⚠️ **TanStack riceve lo scatto INTERO** — ordinamento, filtro globale e paginazione insieme — perché
104
- `query/search` e `query/filters` azzerano `pageIndex` e un misto mostrerebbe la pagina 0 dell'insieme non
105
- filtrato per un frame. ⚠️ **NUOVA PROP `debounceTime` SUL RAMO BASE** (default 200, `0` disattiva), perché
106
- `serverConfig.serverDebounceTime` vive sul solo ramo server e una griglia client ora ne ha bisogno; con
107
- entrambe vince `debounceTime`. ⚠️ **`useGridFilters` ESPONE `committedBadges`**, additivo: si passa `badges`
108
- alla griglia (gli editor devono restare vivi) e si filtra con `committedBadges`. Chi resta su `badges` non
109
- si rompe — semplicemente non ottiene il differimento, ed è il modo di fallire che è stato **scelto**: rendere
110
- `badges` il valore confermato avrebbe dato il differimento a tutti senza toccare una riga, ma avrebbe fatto
111
- **scattare la digitazione in produzione** a chi non passasse la bozza agli editor. ⚠️ **E DENTRO IL HOOK LA
112
- DIVISIONE È PER GESTO, non per nome del mutatore** — `updateBadge` (un valore che si digita o si trascina) è
113
- continuo, `addBadge` è **discreto**, perché è anche il modo in cui un badge nasce da uno switch, da un
114
- `AppSelect` o da un chip: la prima stesura lo aveva classificato continuo e, misurato, lo switch di
115
- `FiltriSoloFuori` non annunciava più il proprio filtro entro il frame. Il prezzo dichiarato è che un filtro
116
- di **testo nuovo** conferma sul primo carattere e coalesce il resto — due conferme per parola invece di una;
117
- la ricerca globale, che non passa dal hook, resta a una. **Quattro spec riscritte** una per una sul contratto
118
- nuovo, nessuna allentata (`filters-lifted` — che dichiarava a parole «il debounce è affare del consumer» —,
119
- `riga-zero`, `completa/FiltriSoloFuori` e `gruppi-iniziali`, dove «una battuta, una emissione» diventa un
120
- delta esatto di 1 e la prova sulla risemina si affila), più **due story nuove** che misurano il caso client
121
- e la sua via d'uscita
122
- - **AppDataGrid — ⚠️ CAMBIO DI COMPORTAMENTO PER OGNI CONSUMER IN MODALITÀ SERVER, anche chi non passa
123
- niente.** `serverConfig.serverDebounceTime` **è implementata**, con **default 200ms**, e le due azioni
124
- continue — `query/search` e `query/filters` — vengono **coalescite**: una parola di sei lettere nella
125
- searchbar è ora **una** chiamata a `onQueryChange` invece di sei, col payload finale. Fino a oggi quel
126
- campo era dichiarato nel tipo, il suo TSDoc diceva «NON IMPLEMENTATO» e **nessuna riga lo leggeva**: chi lo
127
- passava otteneva silenziosamente niente. Misurato nel browser prima e dopo, non dedotto: sei battute
128
- ravvicinate danno `data-emissions` fermo durante la raffica e **+1** allo scadere della finestra, mentre con
129
- `serverDebounceTime: 0` — che è lo stesso ramo sincrono di prima — le stesse sei battute danno **+6**.
130
- ⚠️ **SOLO L'EMISSIONE È DIFFERITA, MAI LO STATO**: la tabella si aggiorna sulla battuta (la searchbar è
131
- controllata da `state.query.search`, e in modalità client il filtro locale resta istantaneo), quindi il
132
- campo compra meno richieste e non una tabella più lenta — misurato leggendo il valore dell'`<input>` nello
133
- stesso istante in cui il contatore è ancora a zero. ⚠️ **LE AZIONI DISCRETE EMETTONO SUBITO E ANNULLANO LA
134
- COALESCENZA IN SOSPESO** invece di accodarsi (ordinamento, pagina, dimensione pagina, load-more, preset):
135
- altrimenti un gesto metterebbe **due** richieste in volo, che è il difetto che il debounce dovrebbe ridurre
136
- — misurato strumentando `setTimeout`/`clearTimeout`, il timer della battuta risulta cancellato e mai
137
- scaduto e il contatore sale di uno al click, portando **dentro** la ricerca battuta nel frattempo.
138
- ⚠️ **RITARDO ZERO SENZA EMETTITORE**: il ramo client dichiara `onQueryChange?: never`, quindi in client non
139
- c'è niente da coalescere e il numero di emissioni per gesto **non cambia** (misurato: tre battute, tre
140
- emissioni immediate). ⚠️ **DUE LIMITI DICHIARATI**: coalescere riduce le richieste ma **non ne sequenzia le
141
- risposte**, che restano affare del consumer; e con i filtri **SOLLEVATI** il canale è `onFiltersChange`, la
142
- callback del consumer chiamata in modo sincrono dentro l'azione, che **non** passa da `applyAndEmit` e
143
- quindi resta una chiamata per tasto. ⚠️ **E LA PROP È OMONIMA MA NON GEMELLA** di quella del `DataTable`
144
- congelato, dove (default 750) ritarda solo la comparsa dell'overlay di caricamento e non coalesce nessuna
145
- richiesta — `DataTable.md` afferma il contrario ed è falso su quel componente. **Cinque story nuove** in
146
- `AppDataGrid/Emettitore (P2)`: la searchbar, il default, l'azione discreta, `0`, e la **riga dei filtri** —
147
- quest'ultima perché ricerca e filtri sono due rami diversi di `isContinuousQueryAction` e una correzione
148
- che ne coprisse uno solo resterebbe verde su tutto il resto. La falsificazione è stata eseguita sul regime
149
- a ritardo zero, che è byte-per-byte il ramo di prima: là le due asserzioni discriminanti leggono 6 invece
150
- di 0 e 1
151
- - **AppDataGrid (story, non il componente)**: nella scena `AppDataGrid/Completa → DueCaricamenti` il server
152
- finto **risponde dopo mezzo secondo** invece che nello stesso commit, quindi il caricamento si può
153
- finalmente guardare. ⚠️ **Nessuna riga del componente cambia**: è il banco della story. Il difetto era che
154
- la scena aveva due regimi e in **nessuno dei due** un caricamento durava un tempo osservabile — con lo
155
- switch «Blocca le risposte» attivo non finisce mai, e con lo switch spento rispondeva **sincrono**
156
- (`setCaricaInfinita(true)` e la consegna nello stesso gesto), quindi gli scheletri nascevano e morivano
157
- nello stesso commit. Misurato nel browser sulla story viva, non dedotto dalla costante: **615ms** di
158
- scheletri sulla griglia infinita (dieci scheletri accodati, righe 40 → 50) e **514ms** sulla paginata (dieci
159
- scheletri, righe a **0** durante l'attesa — la semantica firmata «accoda contro sostituisce» resta intatta).
160
- Congelare a metà volo **annulla** la risposta in corso invece di lasciarla arrivare a switch chiuso
161
- (misurato: scheletri ancora a schermo dopo 1500ms, cioè tre volte il ritardo), e nessun timer sopravvive
162
- allo smontaggio (misurato strumentando `setTimeout`/`clearTimeout` e facendo cambiare story a Storybook nel
163
- medesimo documento: il timer pendente risulta cancellato e mai scaduto). ⚠️ **REVOCA metà di una frase
164
- firmata nel docblock della scena** — «non è un `delayMs`: un ritardo rende la play una gara coi tempi,
165
- mentre un server che non risponde è uno stato stabile che si misura». La seconda metà vale ancora, ed è il
166
- motivo per cui il congelamento resta e per cui le due misure firmate si prendono tutte in quel regime; la
167
- prima era una conclusione troppo larga. Il ritardo attraversa la **sola** fase 3 della play (lo sblocco),
168
- dove le attese portano ora un `timeout` dichiarato, derivato dalla costante con un margine di 6×, invece
169
- del default di `waitFor`
170
- - **AppDataGrid**: con più di una riga di intestazione, **fra le righe dell'header non passa più un pixel
171
- del corpo che scorre**. Segnalato con uno screenshot e riprodotto prima di essere corretto. Le righe di
172
- header intermedie portavano `border-d-border border-b` sulla `<tr>`: sotto `border-collapse: collapse`
173
- quel bordo viene collassato nella griglia dei bordi della **tabella** — che non è sticky — quindi scorre
174
- via col corpo, e la striscia da 1px che occupava non è coperta da niente, né dalla riga sopra, né da
175
- quella sotto, né dal fondo del `<thead>`. ⚠️ **Misurato, non dedotto** (Chrome, `devicePixelRatio` 2,
176
- scena `AppDataGrid/Header multilivello (P5) → Manuale`, `scrollTop: 137.5`, corpo dipinto `#ff0000`,
177
- lettura del pixel sul PNG del compositor): **670 campioni rossi su 888 a ciascuno dei due confini interni
178
- del `<thead>`** e zero dentro le righe; dopo il fix, **zero in tutto il blocco**. Un fondo sul `<thead>`
179
- non è il rimedio, ed è misurato anche questo: con `thead { background: #00ff00 !important }` il verde non
180
- appare **mai** al confine, che resta rosso — il fondo di un row-group non copre quella striscia. Il filo
181
- fra due livelli è ora `box-shadow: inset 0 -1px 0 0 var(--d-border)` (`gridHeaderRowSeparatorShadow`), la
182
- stessa forma con cui la Fase 2 aveva già risolto la separazione header→corpo e quella del footer, e vive
183
- in **due** posti per necessità: sulla `<tr>`, l'unico elemento che si estende su tutta la larghezza della
184
- tabella colonna fantasma inclusa, e — composto attraverso `gridPinnedOffsetStyle`, mai come una seconda
185
- dichiarazione — sulle celle **opache** di quelle righe, perché il `bg-d-background` di una cella pinnata
186
- dipinge sopra l'ombra della riga (misurato: con la sola ombra della `<tr>` il filo correva da css x 73
187
- invece che da 25). ⚠️ **Effetto collaterale visibile nel contratto delle sonde**: una cella-etichetta di
188
- gruppo pinnata di una riga intermedia porta ora **un `box-shadow` layer in più** (4 invece di 3 a
189
- sinistra, 3 invece di 2 a destra), con la geometria nuova `0px -1px 0px 0px inset` — le due spec che
190
- contano gli strati sono state aggiornate nominando il quarto, non allentando il conteggio. ⚠️ **E chiude
191
- una domanda che il repo dichiarava aperta per iscritto** (`parts/grid-filter-row.tsx`): un `box-shadow` su
192
- una `<tr>` sotto `border-collapse` **si rende**. Story di regressione:
193
- `AppDataGrid/Header multilivello (P5) → Fra due righe di header non passa il corpo che scorre`, i cui tre
194
- predicati sono stati falsificati contro il regime pre-correzione (bordi `1px`, ombra di riga `none`, e le
195
- due celle-segnaposto delle spunte senza filo)
196
-
197
- > **AUDIT 2026-09-09.** Le voci di questo blocco chiudono i rilievi confermati dell'audit di
198
- > `AppDataGrid` (`docs/analysis/datatable-v2/AUDIT-BUG-2026-09-09.md`: 8 corsie di lettura, 4
199
- > verificatori avversari, 47 rilievi confermati su 53). ⚠️ **Il contesto che le rende leggibili:** la
200
- > suite era **verde** prima dell'audit — 730 test su 102 file — quindi nessuno di questi difetti era
201
- > una regressione recente: erano zone che nessuna asserzione guardava. Dopo: **765 test su 110 file**
202
- > (766 da quando la voce qui sopra ha aggiunto la sua story di regressione), e ogni fix ha una story che
203
- > cade se il fix viene revocato.
204
-
205
- - **AppDataGrid — ⚠️ BREAKING per chi confronta il payload carattere per carattere.** Il valore di un
206
- filtro **numerico** parte come `number` e non più come `string` (`{importo: {operator: "gt", value: 100}}`,
207
- non `"100"`), e il valore di un filtro **data** è composto dai getter LOCALI — `"YYYY-MM-DD"` per
208
- `date`, `"YYYY-MM-DDTHH:mm:ss"` per `datetime` — invece di `toISOString()`: nessuno `Z`, nessun
209
- istante UTC.
210
- ⚠️ **DUE PRECISAZIONI, aggiunte dopo la verifica avversaria perché la prima stesura di questa voce
211
- prometteva più di quanto consegna.** (1) **Il numero vale per l'editor SINGOLO, non per il compositore
212
- «da…a…»**: `GridFilterRangeSide.value` è tipata `string` in `app-data-grid.types.ts`, quindi un
213
- intervallo numerico spedisce ancora due stringhe. Sono due forme dello stesso valore nello stesso
214
- modulo, ed è un debito aperto: allargare quel tipo cambia il contratto di `onApply`, quindi non è stato
215
- deciso qui. (2) **La forma `datetime` NON è la parità letterale con la tabella congelata**, che appende
216
- sei decimali — `const microseconds = "000000"` in
217
- `data-table/components/data-table-filter-inputs.tsx`. La griglia emette `"YYYY-MM-DDTHH:mm:ss"` senza
218
- di essi. Il difetto corretto è il **fuso** — non c'è più uno `Z` né un giorno sbagliato — non la
219
- coincidenza carattere per carattere, e un backend che facesse parsing rigido dei microsecondi va
220
- verificato. Erano entrambi scarti dalla parità col payload della `DataTable` congelata che il
221
- documento promette. Nel verso opposto, un valore di sola data nella forma che la congelata produce
222
- (`"2026-09-09"`) viene riletto come mezzanotte **locale** e non più UTC, così il picker non mostra
223
- più il giorno prima. Il campo numerico svuotato emette `undefined` e non `0`: `Number("")` avrebbe
224
- reso una cancellazione un filtro «maggiore di 0» che nessuno ha scritto
225
- - **AppDataGrid**: il chip di un filtro **booleano** porta il valore tradotto — «Chiusa Uguale a: Vero»
226
- / «: Falso» — dove prima «è vero» perdeva il valore (il chip era testualmente indistinguibile da un
227
- badge senza valore) e «è falso» stampava il letterale non tradotto. Gli operatori senza valore si
228
- riconoscono ora dall'**operatore** (`GRID_VALUELESS_OPERATORS`, la stessa sorgente che usa
229
- `grid-filter-compile`) e non dal valore, quindi «è vuoto» resta nudo
230
- - **AppDataGrid**: l'editor a lista non fa più cadere il select sul segnaposto quando il valore del
231
- badge non è più fra le opzioni della colonna: rende un'`<option>` sintetica marcata
232
- `data-grid-filter-editor-unknown` — **nuovo aggancio DOM** — così un filtro attivo resta visibile
233
- invece di sembrare vuoto
234
- - **AppDataGrid**: la **sorgente infinita** non si congela più quando un filtro viene applicato e poi
235
- rimosso: la chiave della guardia si azzera a ogni ripartenza, dove prima una firma che TORNAVA a un
236
- valore già consumato lasciava la lista ferma per sempre. E una griglia infinita montata **senza box
237
- di layout** (`display:none`, un pannello chiuso) non incatena più l'intera sorgente credendosi a
238
- fondo lista: non chiede nessuna pagina e la chiede all'apertura del pannello. ⚠️ Conseguenza per il
239
- consumer: la prima pagina di una griglia dentro un pannello chiuso arriva **all'apertura**, non al
240
- montaggio. Il ritorno della vista in cima dopo un cambio di query non è più confinato alla modalità
241
- infinita: vale per ogni griglia, paginata compresa
242
- - **AppDataGrid**: il **raggruppamento** su una colonna con valori assenti non collassa più i livelli.
243
- `keyOf(null)` vale `""`, e la guardia del separatore di percorso scattava una seconda volta al
244
- livello successivo **sostituendo** il percorso invece di concatenarlo: due gruppi diversi si
245
- fondevano, il conteggio raddoppiava e l'aggregato sommava la stessa riga N volte — un numero
246
- sbagliato mostrato all'utente. Un gruppo con chiave assente si rende ora con «—» come etichetta
247
- (la sua chiave resta la stringa vuota), e `initialOpenGroups` riconosce un percorso il cui antenato
248
- ha la chiave assente. ⚠️ **Limite che resta aperto e va dichiarato:** `null`, `undefined` e la
249
- stringa vuota condividono la chiave `""`, quindi tre assenze semanticamente diverse confluiscono in
250
- un solo gruppo. Con `groupLabelPlacement="first-column"` la prima colonna non perde più il proprio
251
- `aggregatedCell`
252
- - **AppDataGrid**: in modalità infinita l'annuncio «Altre righe caricate» era diventato **permanente**
253
- dopo il primo caricamento, coprendo le quattro clausole della riga di stato (selezione, conteggio,
254
- ordinamento, filtri) nella regione live e — con `statusRowVisible` — anche a schermo. Ora è uno
255
- scatto: torna alla riga di stato al primo cambio di stato dopo l'atterraggio della pagina
256
- - **AppDataGrid**: a portata di selezione **promossa** la barra non sparisce più cambiando pagina, così
257
- il ritorno alla pagina e le azioni di massa restano raggiungibili; e la conferma di un'azione
258
- distruttiva non sopravvive più allo svuotamento della selezione per riarmarsi alla spunta successiva
259
- (prima solo un array `selection.actions` RICOSTRUITO a ogni render la annullava: con `useMemo` o una
260
- costante di modulo restava armata). ⚠️ Il contratto «a selezione vuota la barra non si rende
261
- affatto» ha ora **una** eccezione dichiarata: con la promozione offerta e la portata `"all"` viva la
262
- barra resta resa a zero spunte, perché è l'unica superficie che governa quella portata — quindi
263
- `[data-grid-selection-count]` può valere `0`
264
- - **AppDataGrid**: `readGridViewPreset` rifiuta un payload v1 **troncato** con
265
- `GridViewPresetVersionError` invece di validare solo `version` e lasciare esplodere il motore su
266
- `query.sorting.map` dentro un effetto — cioè una schermata bianca invece di un errore dichiarato
267
- - **AppDataGrid (a11y)**: una colonna con `header` vuoto e senza `headerLabel` prende il proprio `id`
268
- come nome accessibile. Prima l'`aria-label` vuoto veniva **saltato** da accname e il nome del
269
- `columnheader` si ricomponeva dai contenuti del `<th>`, **larghezza della maniglia di resize
270
- compresa**: il nome accessibile della colonna era un numero di pixel
271
- - **AppSelect (a11y, WCAG 4.1.2)**: il trigger `role="combobox"` senza `label` ricade sul proprio
272
- `placeholder` come nome accessibile. La catena è `aria-labelledby` → `aria-label` → `placeholder`,
273
- **mai il valore corrente**. ⚠️ Perché conta oltre `AppSelect`: i **dieci** `<AppSelect>` che la
274
- `DataTable` congelata rende non passano né `label` né prop ARIA, e dopo il passaggio del trigger a
275
- `role="combobox"` + `tabIndex` erano diventati dieci combobox focalizzabili **senza alcun nome**, in
276
- diciotto repo consumer. La cartella congelata non è modificabile (`check-datatable-freeze.mjs`),
277
- quindi il ripiego nel primitivo condiviso è l'unico punto raggiungibile e li copre tutti e dieci.
278
- ⚠️ **Chi asseriva `aria-label` assente in quella forma va aggiornato** (è il caso della story
279
- `SenzaEtichettaNomeAccessibile`, misurata rossa e corretta)
280
- - **AppDataGrid**: `rowMenu` che ritorna un elenco **vuoto** non rende più il grilletto «⋮», come il
281
- contratto dichiarato nel tipo diceva già
282
- - **AppDataGrid**: `initialState.pagination.pageIndex` è **onorato**. `GridPagination` lo dichiara
283
- obbligatorio, quindi ogni call-site è costretto a scriverlo, e la griglia lo scartava montando
284
- sempre a pagina 1: il paginatore stampava «1 / N» sulla terza pagina di dati. E il **termine di
285
- ricerca iniziale** non si perde più: il motore leggeva `initialState.searchbarFilter` al primo
286
- livello, mentre il builder e la tabella congelata lo scrivono dentro `initialState.filters`
287
- - **AppDataGrid**: `onFiltersChange` e l'incremento di `emissions` escono ora **fuori** dall'updater
288
- di `setState`, così il contratto osservabile è «una mutazione = un annuncio» anche sotto
289
- `StrictMode`, dove prima era doppio
290
-
291
- ### 🐛 Fixed — quello che l'audit ha trovato e che NON è chiuso
292
-
293
- - **AppDataGrid**: ⚠️ **`serverConfig.serverDebounceTime` è dichiarato nel tipo, promesso dal proprio
294
- TSDoc («Millisecondi di attesa prima di emettere») e non letto da nessuna riga di codice**:
295
- l'emettitore è sincrono, quindi sei battute in una searchbar sono sei richieste HTTP senza garanzia
296
- d'ordine. Non chiuso qui perché le due strade sono decisioni: implementarlo nell'emettitore cambia
297
- il contratto di `onQueryChange`, e togliere il campo dal tipo è un breaking change per diciotto
298
- repo. Oggi il debounce lo deve fare il consumer
299
- - **AppDataGrid**: ⚠️ **con i filtri SOLLEVATI la prima pagina della lista nuova è chiesta col
300
- contatore della lista vecchia** (misurato: `pageIndex: 3` per un insieme in cui nessuna riga è stata
301
- consegnata), quindi un consumer che deriva `offset = pageIndex * pageSize` salta le prime pagine del
302
- risultato filtrato. **Tentato e revocato**, e la ragione va letta prima di ritentare: azzerare
303
- `requestCount` in `effectiveQuery` fa andare rossa `FiltriSollevatiConSorgenteInfinita` con
304
- «expected 30 to be 60» — una scena verde nella baseline il cui harness dichiara il disegno opposto,
305
- cioè che la ripartenza la fa il **consumer** in `onFiltersChange` e la griglia deve poi emettere un
306
- indice che **avanza**. I due contratti sono incompatibili e la griglia non può distinguerli: chi
307
- possiede il segnale di ripartenza è una decisione di prodotto. Scritta per intero sul commento di
308
- `effectiveQuery` in `engine/use-grid-engine.ts`, e inchiodata dalla story
309
- `DEBITO motore-1 — col chip la pagina chiesta è quella stantia, non la zero`
310
- - **AppDataGrid**: ⚠️ con `groupBy` + `resultTotal` gli `aria-rowindex` **sfondano** `aria-rowcount`
311
- (misurato: indici 1..6 contro un totale di 3), perché il conteggio torna a contare le sole righe di
312
- dati mentre gli indici contano anche le righe di gruppo. Il rimedio è in `app-data-grid.tsx`
313
- - **AppDataGrid**: ⚠️ tre rilievi in `parts/grid-filter-panel.tsx` restano **non corretti per scelta**:
314
- il file è nel perimetro congelato e il pannello è deprecato e non montato da nessuna story —
315
- `adg-r18` ha già registrato che «la sua superficie non va migrata, va lasciata morire»
316
- - **AppDataGrid**: ⚠️ `check-datatable-shared-surface` resta **rosso**, e non è chiudibile da codice:
317
- il gate confronta il merge-base con l'albero di lavoro, quindi il rimedio che nomina — separare la
318
- corsia dai primitivi condivisi — richiede **git**, cioè due commit o due PR
319
-
320
- ### 🔧 Changed
321
-
322
- - **AppDataGrid**: ⚠️ **BREAKING — il default di `filterMode` passa da `"panel"` a `"row"`** (il destructuring di `AppDataGrid` in `src/components/ui/app-data-grid/app-data-grid.tsx`). Chi non passa `filterMode` ora ottiene la **Riga Zero**: una QUARTA `<tr>` di `<thead>` (`tr[data-grid-filter-row]`), montata **al primo paint** perché il toggle nasce aperto (`useState(true)`), più il bottone **«Mostra/Nascondi la riga filtri»** (`[data-grid-toolbar-filter-row-toggle]`) nel gruppo sinistro della toolbar. **Il `<thead>` cresce di 36px** e chi budgeta l'altezza deve passare `gridViewportHeight({ …, hasFilterRow: filterMode === "row" && open })` leggendo `open` da `onFilterRowOpenChange` — un budget calcolato con `hasFilterRow: false` ora promette 36px che la griglia occupa. ⚠️ **NON È UN CAMBIO DI TIPI: `tsc` resta verde e nessun call-site smette di compilare** — il cambiamento è di COMPORTAMENTO A SCHERMO, quindi si scopre nel DOM e non nel compilatore. ⚠️ **E IL CONTO VERO, MISURATO ESEGUENDO E NON DEDOTTO, È PIÙ ALTO DI QUELLO CHE IL REPO SI ASPETTAVA: 11 scene rosse, non 14 asserzioni in due file.** `yarn run spec` passa da `1 failed | 800 passed (801)` a `12 failed | 789 passed` e torna a `1 failed | 800 passed` — l'unico rosso, prima e dopo, è il preesistente `app-sidebar.stories.tsx > Expand On Hover`. ⚠️ **E QUELLE 11 SONO LE ROSSE _NON_ PREVISTE**, perché i tre siti che l'indagine aveva enumerato — gli helper `thead tr` di `app-data-grid-header-levels.stories.tsx` (13 asserzioni) e `app-data-grid-integrazione.stories.tsx` (1), più `ultimaRigaDiThead` in `app-data-grid-grouping.stories.tsx`, il caso che non sarebbe diventato rosso — **sono stati curati nello stesso passo del flip e quindi non hanno mai suonato**: il conto qui sotto è ciò che è emerso ESEGUENDO, oltre a quello. Le rosse si dividono in tre famiglie e **nessuna delle tre era stata prevista**: **(a) un QUARTO helper della stessa classe, che l'inventario statico non aveva trovato** — `intestazioniFoglia` in `app-data-grid-guscio-registro.stories.tsx` (4 scene R16), che prendeva l'ULTIMA `thead tr` per averne le FOGLIE e, trovando la riga zero, **SOLLEVAVA** invece di confrontare due liste vuote: la sua guardia di non-vacuità ha fatto esattamente il mestiere per cui era stata scritta; **(b) chi budgeta la cornice con `hasFilterRow: false`** — `app-data-grid-cornice.stories.tsx` (dieci righe intere diventano nove), `app-data-grid-esiti.stories.tsx` (otto righe sopra il footer diventano sette), `app-data-grid-virtualization.stories.tsx` (i 36px entrano nello `scrollHeight` del viewport e sforano la tolleranza di due righe); **(c) — la famiglia che nessuna previsione conteneva — chi misura il FILO fra le righe di intestazione**, `app-data-grid-header-levels.stories.tsx` e `app-data-grid-density-fullscreen.stories.tsx`, perché `haUnaRigaSotto` (`parts/grid-header.tsx`) conta la Riga Zero come una riga sotto, per una decisione firmata il 2026-09-10, e quindi la riga delle foglie ACQUISTA il filo che quelle scene asserivano assente. Le asserzioni su `[data-grid-header-row]` e su `thead th` restano **immuni per costruzione**: la riga zero porta un marcatore proprio (`parts/grid-filter-row.tsx`) e le sue celle sono `<td>`. **Nessuna scena è stata pinnata in blocco**: 5 siti aggiornati (4 helper portati da `thead tr` a `[data-grid-header-row]` — la cura è una riga per file, perché quei file misurano i LIVELLI di intestazione e non la riga zero — più la scena del filo di `header-levels`, riscritta sulla verità della modalità nuova) e **6 scene pinnate a `filterMode="panel"` esplicito**, una per una e con la ragione scritta accanto al pin: `BordoHeaderConDueLivelli` (density-fullscreen), `AltezzaCorniceMonotona` (cornice), `FooterDeiTotali` (esiti), `CambioDensitaMantieneLeMisure` (virtualization), `ConfineDelBloccoPinnato` (banda-tabella), `FiltriSoloFuori` (completa). ⚠️ **DUE DI QUEI PIN NON SONO RUMORE TOLTO DI MEZZO, SONO COPERTURA CHE SI SAREBBE PERSA**: `BordoHeaderConDueLivelli` è ora l'UNICA scena che pinna il ramo FALSO di `haUnaRigaSotto` («l'ultima riga di intestazione non porta il filo, perché sotto c'è il corpo»), e il ramo VERO — che fino a oggi non aveva **nessuna** copertura eseguibile, perché entrambe le scene del filo montavano in `"panel"` — è pinnato dalla scena aggiornata di `header-levels`. ⚠️ **E DUE ERANO FILI TESI CHE HANNO SUONATO COME SCRITTO**: `ConfineDelBloccoPinnato` dichiarava già nel proprio commento che il conto «regge per quel motivo, non per fortuna», e `FiltriSoloFuori` enumera sette selettori di superfici di filtro «se un giorno una di esse diventasse resa per difetto, la riga che la nomina diventa rossa e dice QUALE» — è successo, e ha detto quale. ⚠️ **REVOCA UNA DECISIONE FIRMATA**, e la revoca sta qui perché chi aveva letto quella va avvisato: il TSDoc di `filterMode` dichiarava «IL DEFAULT E' `"panel"` ED E' UNA DECISIONE, non un ripiego di comodo», con la ragione — le scene esistenti che contano le righe di `<thead>` diventerebbero rosse senza che nessuno le abbia toccate. **La ragione era ed è vera, e la previsione era giusta nel meccanismo e sbagliata nei numeri**: diceva «TRE in nove punti» in header-levels e «altrettanto» in integrazione (sono 13 e 1), ometteva il caso che non diventa rosso (`grouping`) e ometteva del tutto le famiglie (b) e (c). È il **peso** della ragione a essere cambiato: il pannello era la modalità già consegnata quando la Riga Zero era nuova, e oggi la Riga Zero è la modalità che le applicazioni montano. Il prezzo si paga una volta, nelle scene, e non ogni volta in ogni call-site.
323
- - **AppDataGrid**: `filterMode="panel"` è **work in progress**. Il pannello inventario **funziona, resta nell'unione `GridFilterMode` e resta passabile**, e `GridFilterPanel` resta esportato da entrambi i barili: **niente è stato rimosso e nessun call-site smette di compilare**. Quello che cambia è che non ci si arriva più per difetto e che la modalità non riceve lavoro finché la Riga Zero non è chiusa — la stessa distinzione, e la stessa forma, della voce `GridFilterPanel` in `src/components/documents/components-list.md`, dove la nuova voce sta **accanto** alla sua. ⚠️ **LA MARCATURA È DICHIARATIVA E NON ATTIVA, ed è scritta nei tre posti che un consumer legge davvero**: il TSDoc di `filterMode` (`src/components/ui/app-data-grid/app-data-grid.types.ts`), la riga di tabella di `filterMode` in `src/components/documents/components/AppDataGrid.md`, e la voce nuova in `components-list.md`. **Nessun `console.warn` su `filterMode="panel"`, e l'omissione è una scelta misurata**: le scene che montano il pannello per MISURARE il contratto (`app-data-grid-riga-zero.stories.tsx`) sono copertura corretta, e un warn le trasformerebbe in rumore a ogni run — a maggior ragione ora che sei scene lo passano esplicitamente per pinnare la geometria senza riga zero. La porta resta aperta: un warn è additivo. ⚠️ **È REVERSIBILE CON UNA RIGA**: rimettere `filterMode = "panel"` nel destructuring di `AppDataGrid` riporta tutto com'era, perché nessun tipo è cambiato e nessun export si è mosso, e le sei scene pinnate restano corrette in entrambi i versi. ⚠️ **IL TIPO NON È STATO TOCCATO**: `GridFilterMode = "row" | "panel"` è invariato, e `filterMode` resta appesa in coda a `GridShellFields` per non spostare le righe di `onQueryChange?: never` e `paginated?: never`, citate per numero da `scripts/check-grid-coexistence-matrix.mjs`.
324
-
325
- - **AppDataGrid**: The **25 «intermediate» story files** — every one except the three showcase files (`AppDataGrid/Mockup`, `AppDataGrid/Completa`, `AppDataGrid/Confronto`) — moved from `title: "AppDataGrid/<name>"` to `title: "CI-CD/AppDataGrid/<name>"`, so the regression specs collect under one `CI-CD` sidebar node and the showcase keeps `AppDataGrid/` to itself. ⚠️ **Each one also pins `id:` in its own meta, and that is the whole point of the change being safe.** A CSF component id is DERIVED from the title (`toId`), so the retitle alone would have moved the URL of every story in those files — and those URLs are cited OUTSIDE the source: 12 files under `.laif/tasks/to-validate` (tasks awaiting human signature, naming the page to open for a criterion not yet signed) and 11 append-only run logs, which are not rewritable, so any id change would leave permanently dead links in the audit trail. `id?: ComponentId` is the documented clause for exactly this (`node_modules/storybook/dist/csf/index.d.ts:876`, «used for URLs. By default is inferred from sanitizing the title»); Storybook in use is 10.0.8. ⚠️ **MEASURED on Storybook's own `/index.json`, before and after — not deduced from the source**: `appdatagrid-*` stories 319 → **319**; ids that disappeared (broken URLs) **0**; unexpected new ids **0**; stories whose title changed at an **unchanged id** **305** (the remaining 14 are the three showcase files); total index 725 → **725**. Citations to intermediate specs that no longer resolve: **2 before, the same 2 after** — `atomo-di-filtro-c7--inventario` and `selezione-c6--barra-di-selezione` were ALREADY dangling before the rename, so the retitle broke nothing. `npm run spec` stays **102 files / 725 tests, 0 failed** (it filters by file path, not by title) and all **14** `scripts/check-*.mjs` gates exit 0 (they read paths and sources; none reads a title). ⚠️ **The id values were COPIED from the index, never retyped**: one of the twenty-five is `appdatagrid-densità-e-schermo-intero-p6`, accent included — retyping it would have normalised the accent and silently broken a URL that works today. ⚠️ **Accepted consequence: title and URL now diverge for good** — a future title rename will silently keep the pinned id, which is why every one of the 25 metas carries the reason inline and points at the «Il nodo CI-CD» section of `docs/analysis/datatable-v2/PIANO-COMPLETAMENTO.md`. ⚠️ **The seven `Manuale` stories move too, and it is stated rather than hidden**: they are the deliberately play-less bench for human task validation, and in CSF a story cannot override its meta's title, so keeping them under `AppDataGrid/` would have meant splitting files — not done. Their URLs are unchanged, so every citation by id still resolves; only their sidebar location moved
326
- - **AppDataGrid**: ⚠️ **What the reorganisation deliberately did NOT do, after an audit that measured it.** All 25 intermediate files were audited one by one (25 recon passes, then three adversarial skeptics on every proposal to cut) and **none is deletable**: 23 came back «keep» outright and the 2 «reduce» proposals were refuted **3 votes out of 3**. The reason is structural and measurable — 24 of the 25 are the only place in the repo where at least one public prop, DOM selector or exported function is exercised (`infinite`, `filterControls`, `validateGridColumnTree`, `readGridViewPreset`, `th[scope="row"]`, the `data-grid-column-registry-*` family) — and, decisively, **deleting a story turns nothing red**: `npm run check` does not run the plays, and `spec` is chained to neither `check` nor `pr-checks.yml` for two reasons the repo itself already states in `package.json` (`_comment:spec`). The lane has also already paid for one pruning (`dtv2-f1-potatura-e-spacchettamento-story` deleted ten stories and with them the source three later tasks needed). Still open and NOT addressed here: the «show/hide specs» switch (`tags: ["spec"]` plus a `sidebar.filters` filter in `.storybook/manager.ts`), which must go in a separate PR because `scripts/check-datatable-freeze.mjs` scopes lane purity on `NEW_TABLE_PATHS`, which covers the story folder but not `.storybook/`
327
- - **DataTable**: Deprecated **in the documentation only**, in favour of `AppDataGrid` — the entry is in `src/components/documents/components-list.md` under «Deprecated Components» and nothing is removed: `DataTable` is still exported and every existing call-site keeps compiling, `grep -rn '@deprecated' src/components/ui/tables/data-table/ | wc -l` returns 0 on purpose (a JSDoc tag would trip the consumer lint rules that ban deprecated APIs), the two components import nothing from each other and can be mounted on the same page, and `scripts/codemod/rename-map.json` records name by name which old export maps to which new one and which parts of the new surface have no old equivalent
328
- - **AppDataGrid**: `AppDataGrid.mockup.stories.tsx` stays published in Storybook as `AppDataGrid/Mockup`, frozen: it is the reference the delivery is signed against, and it is not public API — what leaves it is the stale «this mockup is not a component» banner (verifiable by difference: `grep -c 'non è un componente' src/components/stories/AppDataGrid.mockup.stories.tsx` went 1 → 0), not the story, because the delivery is signed by opening the two target scenes — `?path=/story/appdatagrid-completa--modalita-filtri` and `?path=/story/appdatagrid-completa--raggruppata` — next to `?path=/story/appdatagrid-mockup--modalita-filtri` from the Storybook manager, both of them, since groups and pager cannot be represented together and the mockup's single scene therefore has two counterparts
329
- - **AppDataGrid**: ⚠️ **Known limitation, open** — the two-value operator composer opens from the «Modifica valore…» menu item but **not** from the operator radio path of the filter inventory: the closing `DropdownMenu`'s `DismissableLayer` swallows the `Popover` that mounts in the same frame, and on the radio path the dismissal window outlives a single `requestAnimationFrame` because changing the group's value adds a render before the unmount (tried in four shapes, `modal={false}` included). The only known remedy is a 200ms `setTimeout`, which this lane's «zero setTimeout» convention rejects. Measured and written up in `src/components/documents/components/AppDataGrid.md` and at the `modal={false}` comment in `src/components/ui/app-data-grid/parts/grid-filter-editor.tsx`
330
- - **Gantt**: Confirmed `react-window` and `@types/react-window` stay as direct dependencies — `VariableSizeList`/`ListOnItemsRenderedProps` are imported directly by `src/components/ui/gantt/types/GanttContext.tsx`, `components/Chart/Chart.tsx`, `components/Chart/Scale/Scale.tsx`, `components/Chart/Tree/Tree.tsx`, `components/Chart/Bars/Bars.tsx` and `components/Gantt/Gantt.tsx`; the only other match in `src/` is a textual mention inside a mock message in `message-list.stories.tsx`, not an import
12
+ - **Confirmer**: Below 768px, the confirm now opens as a bottom drawer instead of a centered dialog — no prop needed, the mode is decided when the confirm opens and held for its lifetime. Buttons are full-width 44px touch targets (action on top, Cancel at the bottom), initial focus lands on Cancel, and the drawer keeps `role="alertdialog"`. A drag down or Esc rejects the confirm's promise; tapping the overlay does nothing, matching desktop
13
+ - **use-mobile**: Exports `MOBILE_BREAKPOINT` so consumers can read the same 768px threshold used internally
331
14
 
332
15
  ## [laif-ds@1.0.2]
333
16