laif-ds 1.0.2-app-data-grid.79 → 1.0.2-app-data-grid.81
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.
- package/dist/CHANGELOG.md +18 -1
- package/dist/agent-docs/adoption-report.json +3 -3
- package/dist/agent-docs/components/AppDataGrid.md +16 -16
- package/dist/agent-docs/components/AppSelect.md +1 -0
- package/dist/agent-docs/manifest.json +6 -6
- package/dist/components/ui/app-data-grid/app-data-grid.js +135 -128
- package/dist/components/ui/app-data-grid/engine/use-grid-engine.js +272 -252
- package/dist/components/ui/app-data-grid/grid-filter-client.js +73 -0
- package/dist/components/ui/app-data-grid/parts/grid-fullscreen-toggle.js +13 -14
- package/dist/components/ui/app-data-grid/parts/grid-pagination-control.js +103 -83
- package/dist/components/ui/app-data-grid/parts/grid-toolbar-slots.js +78 -66
- package/dist/components/ui/app-select.js +62 -60
- package/dist/index.d.ts +52 -12
- package/dist/styles.css +1 -1
- package/dist/styles.v3.css +1 -1
- package/package.json +1 -1
package/dist/CHANGELOG.md
CHANGED
|
@@ -7,8 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
### 🔧 Changed — paginatore di AppDataGrid (2026-09-23)
|
|
11
|
+
|
|
12
|
+
- **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.
|
|
13
|
+
- **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.
|
|
14
|
+
- **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`).
|
|
15
|
+
- **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.
|
|
16
|
+
- **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.
|
|
17
|
+
|
|
18
|
+
### 🔧 Changed — toolbar di AppDataGrid (2026-09-23)
|
|
19
|
+
|
|
20
|
+
- **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`).
|
|
21
|
+
- **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.
|
|
22
|
+
- **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.
|
|
23
|
+
|
|
10
24
|
### 🚀 Added
|
|
11
25
|
|
|
26
|
+
- **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`.
|
|
12
27
|
- **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
|
|
13
28
|
|
|
14
29
|
- **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
|
|
@@ -29,6 +44,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
29
44
|
|
|
30
45
|
### 🐛 Fixed
|
|
31
46
|
|
|
47
|
+
- **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.
|
|
32
48
|
- **AppDataGrid — ⚠️ IL BADGE FANTASMA: SVUOTARE UNA CELLA DELLA RIGA ZERO ORA TOGLIE IL FILTRO, NON SOLO IL
|
|
33
49
|
VALORE.** Con `filterMode="row"`, scrivere in una cella-filtro e poi **cancellare** lasciava il badge vivo in
|
|
34
50
|
`state.query.filters` con `value: ""`. La query ripartiva correttamente senza quel vincolo — `usableBadges`
|
|
@@ -296,7 +312,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
296
312
|
|
|
297
313
|
### 🔧 Changed
|
|
298
314
|
|
|
299
|
-
- **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
|
|
315
|
+
- **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.
|
|
300
316
|
- **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`.
|
|
301
317
|
|
|
302
318
|
- **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
|
|
@@ -305,6 +321,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
305
321
|
- **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
|
|
306
322
|
- **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`
|
|
307
323
|
- **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
|
|
324
|
+
|
|
308
325
|
## [laif-ds@1.0.2]
|
|
309
326
|
|
|
310
327
|
### 🐛 Fixed
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": "1.0.0",
|
|
3
|
-
"generatedAt": "2026-09-
|
|
3
|
+
"generatedAt": "2026-09-23T08:20:29.626Z",
|
|
4
4
|
"package": {
|
|
5
5
|
"name": "laif-ds",
|
|
6
|
-
"version": "1.0.2-app-data-grid.
|
|
6
|
+
"version": "1.0.2-app-data-grid.81"
|
|
7
7
|
},
|
|
8
8
|
"summary": {
|
|
9
|
-
"scannedFiles":
|
|
9
|
+
"scannedFiles": 489,
|
|
10
10
|
"deprecatedComponentsCount": 4,
|
|
11
11
|
"deprecatedComponentsUsedCount": 2,
|
|
12
12
|
"componentsWithoutExamplesCount": 5,
|
|
@@ -39,7 +39,7 @@ Three contracts are **copied from `DataTable`, never imported**, so that migrati
|
|
|
39
39
|
| `filters` | `readonly GridBadgeFilter[]` | `undefined` | The active filter badges, **lifted above the grid** (P7). **Controlled when passed**: the grid stops keeping its own filter slice and reads these, which is what makes the chips and the panel work with no `<AppDataGrid>` in the tree. Build them with `useGridFilters` and pass the same value to the chips and to the grid. Absent = the grid uses its own slice, exactly as before P7. |
|
|
40
40
|
| `columnVisibility` | `Readonly<Record<string, boolean>>` | `undefined` | Column id → visible (P7). **An absent column is visible**, because absence means nobody said anything. Controlled when passed, like `filters`, and **the registry is one place**: this map is the only truth about which columns exist. |
|
|
41
41
|
| `paginated` | `boolean` | `false` | Paginated mode. Off by default. **Not available alongside `groupBy`** — it lives on the server variant of the union only. Pagination slices the rows _before_ grouping sees them, so a group would count, and `aggregatedCell` would sum, only the current page: measured at 6 rows out of 340, with another "Italy" reappearing on the next page. That is the same number-that-looks-like-a-total the union forbids for server sources. |
|
|
42
|
-
| `pageSize` | `number` | `
|
|
42
|
+
| `pageSize` | `number` | `25` | Rows per page when `paginated`. **Default `25` since 2026-09-23 (was `10`)**; still declarable via this prop or `initialState.pagination.pageSize`. ⚠️ **Seeds the live page size and is inert afterwards** (the «rows per page» selector owns it from Fase 4 on). ⚠️ **The SKELETON count is capped at `1_000`** — `Math.min(livePageSize, 1_000)` — because that number feeds `Array.from({length})`: measured, `Infinity` raised `RangeError: Invalid array length` and `1e9` killed the process with `heap out of memory`. Data rows and the paginator are NOT capped, so above 1000 the loading page shows fewer placeholders than the page that is arriving. ⚠️ **`0` and non-integers are not validated here**, and `readGridViewPreset` refuses them: a grid mounted with `pageSize={0}` captures a preset that can never be restored (declared limit 6). |
|
|
43
43
|
| `onQueryChange` | `(state: GridServerState) => void` | `undefined` | Fires **only** on a query action (search, sort, filter, page). A view action can never reach it. **Mutually exclusive with `groupBy`**: the props type is a union, so the two together do not compile — see the `groupBy` row. |
|
|
44
44
|
| `virtualized` | `boolean` | `false` | Renders only the visible window of rows. Opt-in, so existing call-sites are unaffected. Needs a bounded viewport — see `viewportHeight`. |
|
|
45
45
|
| `viewportHeight` | `number` | `480` | Height in px of the scroll container. Applied **whenever it is passed explicitly**, and in any case when `virtualized`, `infinite` **or `groupBy`** is on (`viewportIsBounded`, `app-data-grid.tsx`). **Fase 6 added `groupBy` to that list**: a grouped grid without a bounded height grew with its content, so nothing could be recomposed when the selection bar appeared — and the scroll compensation of P9 could never fire either. Must be a number: a percentage against an auto-height parent resolves to zero. ⚠️ **From `adg-m3`, NOT passing it no longer means «content height» inside a stretched frame.** The scroll-area root now carries `min-h-0 grow` whenever the viewport is UNBOUNDED — exactly the case with no `viewportHeight`, no `virtualized`, no `infinite` and no `groupBy` — so it takes whatever vertical space its parent leaves. Measured defect on the template: the grid root stretched correctly to 617px (the consumer passes `className="flex min-h-0 flex-1"`) while the scroll area stopped ~84px short, and the residue stayed empty INSIDE the grid's own frame; the frozen table stretched by construction (`flex max-h-full min-h-0 flex-1` around an `h-full` `ScrollArea`), so this was a regression of the lane and not a choice. **`grow` and not `flex-1`, and that is the load-bearing half:** `flex: 1 1 auto` keeps the box at CONTENT height when the parent has no definite height (every Storybook scene that omits this prop), where `flex: 1 1 0%` would size it on a zero basis and collapse the frame. When this prop IS passed — or under `virtualized`/`infinite`/`groupBy` — the class is not emitted at all and the inline height stays the only source: `[data-grid-viewport].clientHeight === viewportHeight` holds word for word, and so do the budgets `gridViewportHeight` derives from it. |
|
|
@@ -54,7 +54,7 @@ Three contracts are **copied from `DataTable`, never imported**, so that migrati
|
|
|
54
54
|
| `data-testid` | `string` | `undefined` | Test hook on the container. |
|
|
55
55
|
| `loading` | `boolean` | `false` | A request is in flight. Renders skeleton rows at the same height as data rows, and suppresses **both** outcomes without data — the empty one and, from Fase 5, the error one: saying «no results» to someone who is waiting is the quickest way to make them close a page that would have shown their data a second later, and an error from the previous attempt above the skeletons of the current one describes the past. From Fase 5 loading is one of the **three** outcomes rendered inside the table, and above it the frame is silent too: no `<tfoot>`, no paginator. **From P8 skeletons and data coexist**: when the in-flight request is page N>0 of an infinite source the skeletons are **appended** after the data; otherwise (a new sort, filter, search, or the first load) they **replace** it, because the rows on screen belong to a query that is no longer the current one. |
|
|
56
56
|
| `emptyComponent` | `ReactNode` | `undefined` | Replaces the default "no results" message **on screen only**, and only for the EMPTY outcome — the error outcome has a surface of its own with its own i18n strings, and `emptyComponent` does not reach it (from Fase 5 «the state without data» is no longer one thing). From Fase 5 it renders as a **descendant of the `[data-grid-empty]` cell inside the `<tbody>`**, not in a `<div>` beside the table. The live region keeps announcing `i18n.notFoundMessage`, so with a custom node the screen and the voice say different things — see "Declared limits" below for why, and for what to do about it. |
|
|
57
|
-
| `actions` | `GridActions[]` | `[]`
|
|
57
|
+
| `actions` | `GridActions[]` | `[]` | The buttons the grid dresses itself, `{label, icon, onClick, data-testid}` — rendered **INSIDE `[data-grid-toolbar]`**, in its `.ml-auto` right group, **after the full-screen toggle** and before the `toolbar.actions` slot (order changed 2026-09-23: extra actions sit to the RIGHT of full screen). ⚠️ **MOVED by `adg-m3`, and this row used to say the opposite**: until then they were their own `role="toolbar"` row, the FIRST child of `[data-app-data-grid]`, ABOVE the toolbar. The move is not cosmetics — this prop is the compat RENAME of `DataTableProps.actions`, aimed at the corpus the codemod covers (the 297 files this document counts elsewhere), and the frozen table rendered those buttons inside its OWN bar (`tables/data-table/components/data-table-actions.tsx`), so a consumer who migrates by renaming has to find the same bar. That `AppDataGrid` is ABSENT from the last published bundle is what made the move free: no layout already in production moves. ⚠️ **TWO OR MORE COLLAPSE INTO ONE trigger** — `[data-grid-actions-trigger]`, named `i18n.tableActions.actions`, with the commands inside `[data-grid-actions-panel]`; **ONE action renders inline**. Also the frozen shape, and the reason is measured: that right group is 24px tall and already holds «columns» and the full-screen toggle, while the real corpus (`UserTable` in the template) passes THREE — three more buttons there wrap a bar that is `flex-wrap`, collapsed it grows by ONE trigger. ⚠️ **The dress is the bar's own `gridToolbarTriggerClass`** (`variant="ghost"`, h-6, text-xs), NOT the frozen `variant="default"`: the two neighbours are ghost triggers, and the lane already carries a written decision against the primary variant (its hover sheen uses `rounded-[inherit]`, an arbitrary value `check-grid-css.mjs` refuses). **Declared cost:** an «Add» action loses the emphasis it had in the frozen bar. `hideActionsRow` still wins over a populated list. |
|
|
58
58
|
| `hideActionsRow` | `boolean` | `false` | Hides the `actions` buttons even when `actions` is populated. ⚠️ **SCOPE CHANGED from `DataTable`, where it hid the WHOLE toolbar** (sorting, filters, reset, search, column visibility, actions). Here it hides only the `actions` buttons: `<div data-grid-toolbar>` renders unconditionally and keeps the search field, its `×` and the full-screen trigger. A call-site that used it to hide the entire bar gets a bar. Nailed by `app-data-grid-fix-guscio.stories.tsx :: LaPortataDiHideActionsRow`. ⚠️ **From `adg-m3` there is no ROW left to hide.** The buttons live inside the toolbar's right group, so what this prop suppresses is `[data-grid-actions-row]` and its contents — not a band of the frame, and not the toolbar, which renders either way. The semantics did not change (it still wins over a populated `actions`, and the story above still nails it); only the node it removes has moved. It keeps its `Row` name for the same reason the hook does — see `[data-grid-actions-row]` in the DOM contract. |
|
|
59
59
|
| `checkable` | `boolean` | `false` | Adds the selection checkbox column (to `colgroup`, `th` and `td` alike). |
|
|
60
60
|
| `rowSelection` | `Record<string, boolean>` | `undefined` | Controlled selection; internal when omitted. |
|
|
@@ -62,7 +62,7 @@ Three contracts are **copied from `DataTable`, never imported**, so that migrati
|
|
|
62
62
|
| `onRowSelectionChange` | `(selection) => void` | `undefined` | Fires on every selection change. |
|
|
63
63
|
| `onCheckedRowsChange` | `(rows: TData[]) => void` | `undefined` | The selected rows, already resolved to data. |
|
|
64
64
|
| `rowClassName` | `string \| ((row: GridRow<TData>) => string)` | `undefined` | Per-row classes. The callback form is resolved internally. |
|
|
65
|
-
| `toolbar` | `GridToolbarSlots` | `undefined`
|
|
65
|
+
| `toolbar` | `GridToolbarSlots` | `undefined` | **Fase 3, live.** The shell, per slot: `search`, `fullscreen`, `filterRowToggle`, `filters`, `resetFilters`, `columns`, `actions`. ⚠️ **2026-09-23 — «visible but optional»**: `fullscreen`, `columns` and `filterRowToggle` are rendered BY DEFAULT (key omitted = the grid's own ghost button; `columns` only when the visibility map is writable, `filterRowToggle` only in `filterMode="row"`) and **`false` switches them off**; `{ render }` replaces the node. `true` and strings are compiler errors (probes in `type-probes/shell-slots.probe.tsx`). Only `search` stays always rendered. ⚠️ **`density` is NOT in that list from `adg-c12`** — the key was deleted, not moved to the opt-in family, and `toolbar={{ density: {} }}` is a `TS2353`. The "always rendered" family is therefore **two** keys, not three. Each key is a **configuration object** whose PRESENCE turns the slot on and whose optional `render` replaces the node the grid would draw. `search` is rendered **always** — declaring it only lets you substitute the node. `fullscreen`/`columns`/`filterRowToggle` are rendered by default and `false` switches them off (`columns` still needs a writable visibility map, see `onColumnVisibilityChange`). `filters`/`resetFilters`/`actions` are **absent by default**, with **different** conditions: `filters` needs the key to carry `render` or `onOpen`; `resetFilters` needs the key to carry `render` or `onReset`, because **the grid cannot tell whether the command has any effect** — page defaults never reach it (`initialState` is a seed read once, `preset` carries the user's restored view), so «differs from the defaults» is the consumer's answer and the presence of `onReset` IS that answer; `actions` renders only its `render`, with no default node. ⚠️ **`resetFilters` restores FILTERS AND SEARCH ONLY** — sorting, page size, column widths/order/visibility must survive, and since `query/preset` and `view/preset` replace wholesale, whoever implements `onReset` has to build a preset carrying the **defaults** for `filters`/`search` and the **current** values for everything else; `pageIndex` returning to 0 is inevitable and correct. It renders **after the search and before the `filters` trigger**, and it does **not** replace «Restore default view» in the actions menu: that one is the total way out of an unreadable saved view. ⚠️ **Not to be confused with the top-level `actions` PROP**, which from `adg-m3` lands in the SAME `.ml-auto` group, immediately BEFORE this slot (both now AFTER the full-screen toggle, 2026-09-23): that one is the compat rename of `DataTableProps.actions` and the grid dresses its buttons itself; this one is the consumer's own node and the grid draws nothing for it. A slot is deliberately not a `ReactNode`: `ReactNode` admits `boolean` and `string`, so `toolbar={{ search: false }}` would compile and switch search off. `search: false`, `search: "si"` and `search: { render: () => null }` are all compiler errors, proven by the **three** probes in `type-probes/shell-slots.probe.tsx` rather than by this sentence — the first two guard the **property type**, the third the **return type** (`search`/`density`/`fullscreen` use `GridToolbarAlwaysSlot`, whose `render` returns `ReactElement`, not `ReactElement | null`). ⚠️ **The type closes the form, not the intention, and that is a declared limit:** `() => null`does not compile, but`() => <></>` does and paints nothing, so a substitute node that renders nothing stays writable — excluding it would mean inspecting what a function returns, which a type does not do. |
|
|
66
66
|
| `selection` | `GridSelectionSlots` | `undefined` | **Live from Fase 6.** Configures the **page-selection bar** — the band that appears **above** the scroll area (right after the filter band, the mockup's order) as soon as at least one row is ticked, and that does not exist at all while none is. ⚠️ **It used to appear UNDER it; it moved on 2026-09-03**, and the move retires a named promise — see "Page selection" below. `mode` is the one-member union `"page"` because selection is page-only (no promotion to the whole result set), which makes that decision a compile error instead of a note. `actions` is `readonly GridSelectionAction[]` — the bulk actions rendered inside the bar (`label` **without** a number, optional `icon`, `destructive?: boolean` mapped to the shared `Button`'s `destructive` variant, and `onSelect(rowIds)` receiving the **page** ids). `summary` replaces the bar's **totals band** — not the bar, and not the count: the count is the bar's accessible name via `aria-labelledby`, so a slot that replaced it would leave the group unnamed. **`summary` still passes no arguments, and that is now a decision rather than an open question**: the two things it could pass — the selected rows and their aggregates — the consumer already has from `onCheckedRowsChange` (or the `rowSelection` it controls) and from its own `aggregate` functions, so passing them here would open a second channel for the same two numbers. |
|
|
67
67
|
| `pagination` | `GridPaginationSlots` | `undefined` | **LIVE from Fase 4.** `pagination.pageSize` turns the paginator ON **and** seeds the live page size. **The precedence is decided** and resolved in ONE place (at the `useGridEngine` call): `initialState.pagination.pageSize` > `pagination.pageSize` > the top-level `pageSize` prop. **On the CLIENT branch the paginator is a read-out, not a control**: `paginated` is `never` there and the types carry no page channel, so it prints the range and the rows clause from the declared `resultTotal` and renders NO page numbers, NO first/prev/next/last and NO size select — those commands could change nothing but the wording. **It does not coexist with `groupBy`**: the render path excludes it, because grouping × pagination is the pair declared unrepresentable by `paginated?: never` (line 982) on the branch that admits `groupBy`. |
|
|
68
68
|
| `initialFullscreen` | `boolean` | `false` | **Fase 3, live.** Seeds the fullscreen state at the **first** render, so a call site that mounts with it finds `role="dialog"` immediately rather than after an effect. It is an `initial*`, not a controlled `fullscreen`: it seeds and then stops governing, so no consumer has to resynchronise a state it does not own. |
|
|
@@ -119,7 +119,7 @@ resultTotal ??
|
|
|
119
119
|
```
|
|
120
120
|
|
|
121
121
|
**Why it is not a prop.** `resultTotal` lives only on `GridClientProps`, the branch that declares
|
|
122
|
-
`paginated?: never` (`app-data-grid.types.ts:
|
|
122
|
+
`paginated?: never` (`app-data-grid.types.ts:1084`, cited by line number from a gate), and
|
|
123
123
|
`GridServerProps` does not carry it at all. `<AppDataGrid {...props} paginated resultTotal={128} />`
|
|
124
124
|
is `error TS2322`, measured with `tsc --noEmit` on the real file. Deriving the figure closes the gap
|
|
125
125
|
without adding a byte to the frozen contract.
|
|
@@ -368,7 +368,7 @@ Stable hooks, independent of the i18n language:
|
|
|
368
368
|
| `[data-grid-resize-track="<id>"]` | The visible **1px** mark inside the resize handle. It is **not a line that matches the group separator — it is that line, continued**: `w-px` + `translate-x-full` put it on the strip `[L, L+1]`, where `L` is the column boundary, which is exactly where `gridHeaderGroupShadow` (`inset 1px 0 0 0 var(--d-border)`) draws the rule between column groups. `bg-d-border` at rest, `d-primary` and 4px on hover. At rest the translation is `full` and not `1/2` because that is what puts the left edge exactly on the boundary at 1px. ⚠️ **On hover the translation is cancelled (`group-hover:translate-x-0`) so the 4px grow INWARDS, `[L-4, L]`:** with the translation still active they landed at `[L, L+4]`, past the boundary — and on a column whose boundary IS the table's right edge (the last one) those pixels fall outside the scrolling viewport, so the hover was never visible; the same happened at a right-pinned boundary, where the pinned cell paints its own background above it. Growing inwards is also the right reading: the handle belongs to the column being resized. Measured by `In hover la traccia cresce verso l'interno` with the browser's REAL pointer (`@vitest/browser/context`) — `storybook/test`'s `userEvent.hover` dispatches synthetic events that do not activate `:hover`, and the target's rect must be re-read after the hover because the driver scrolls it into view first (measured: 228px of stale offset). ⚠️ **Three geometries were rejected before this one**, and the measurement that settled it is that header cells carry NO vertical border at all (`borderRightWidth: 0px` on every one): flush at `right-0` the mark sat at `[L-1, L]`, inside the column where no other rule passes; centred with `translate-x-1/2` it sat half a pixel to the left of the groups' strip. Two specs guard the pair — `La maniglia si vede a riposo` (position and width) and `La traccia della maniglia continua la linea fra i gruppi` (the two rules coincide, DOM against DOM), both with a 0.1px tolerance because a 0.5px one swallowed exactly the half-pixel defect. ⚠️ **Three passes, and the last two are the decision:** at 1px this same subtle tone is invisible (**1.21:1** against the background, measured in the browser), and `bg-d-foreground/40` fixed the ratio (**2.82:1**) at the cost of leaving the family — a line darker than every other rule in the grid reads as content, not as a hint. The delivered form keeps the subtle tone (`--d-border`, the token behind the grid's other 28 border/divide usages) and buys perception with WIDTH. ⚠️ **The rest contrast is 1.21:1 and stays under the 3:1 of WCAG 1.4.11 — declared, not ignored:** at rest the mark carries no information missing elsewhere (the handle is already a named, focusable `separator` with `aria-valuenow`), and the hover state is accent at 4px. The spec therefore asserts the token identity and the 2px width, not a contrast floor this choice does not claim. `aria-hidden` and `pointer-events-none`: the separator is already named and must keep receiving the pointer. It exists because a fully transparent handle is an affordance that never announces itself |
|
|
369
369
|
| `[data-grid-col-filler]` | The layout-only extra `<col>` (P4). Never matched by `[data-grid-col]`. ⚠️ **From `adg-m3` it is much THINNER in the underflow regime, and still indispensable.** The slack is now distributed over the columns that fell back to `gridColumnWidth.default` and WRITTEN to their `<col>` (`distributeGridColumnSlack`, `grid-tokens.ts`), so this node absorbs only the `Math.floor` remainder — strictly fewer px than the number of elastic columns — plus whatever `gridColumnWidth.max` refused. It is NOT removable: without it `table-layout: fixed` inflates the declared widths whenever their sum is below the table width (measured: declared 160, rendered 225 on four columns in a 1000px box), and pinned offsets computed from the declared numbers desynchronise at the first paint. A grid that declares every width, or whose columns the user has dragged, keeps its FULL filler — those columns are never distributed to |
|
|
370
370
|
| `[data-grid-density-control]` | The density switcher `role="group"`; each button is `[data-grid-density="<v>"]`. ⚠️ **From `adg-c12` the grid never renders this** — it is `GridDensityControl`, a public export the CONSUMER mounts (in `toolbar.actions`, or outside the table). The attributes are unchanged, so a test that looked for it still finds it wherever it is now mounted; a test that asserted it was rendered by default must be inverted |
|
|
371
|
-
| `[data-grid-fullscreen-toggle]`
|
|
371
|
+
| `[data-grid-fullscreen-toggle]` | The full-screen toggle button (P6). Its accessible name changes with the state, and there is deliberately **no `aria-pressed`**: a toggle that both renames itself and announces «pressed» is heard twice and contradictorily. **From `adg-m3` the name is the VISIBLE text** — `innerText` is `i18n.toolbar.fullscreenEnter` / `fullscreenExit` — and the button carries **no `aria-label` and no `title`**: an `aria-label` REPLACES the accessible name, so keeping it beside visible words would reopen WCAG 2.5.3 «Label in Name», the same defect the P6 review fixed on the density buttons and Fase 3 on the search field. The icon is `aria-hidden`. **From 2026-09-23 it is a DS `Button` with `variant="ghost"`** (`data-slot="button"`, no border) whose class list ENDS with the shared `gridToolbarGhostTriggerClass` (`h-6 px-2 text-xs`, ≥24px per WCAG 2.2 SC 2.5.8), asserted by identity on that tail. Visible by default; `toolbar.fullscreen: false` removes it |
|
|
372
372
|
| `[data-grid-actions-row]` | **`adg-m3`.** The wrapper of the `actions` prop's commands — `role="toolbar"`, INSIDE `[data-grid-toolbar]`, in its `.ml-auto` right group. ⚠️ **The name says «row» and it is no longer a row**: it was one until `adg-m3`, when it moved into the bar, and it was deliberately NOT renamed — three story files resolve this selector and none of them asserts a POSITION, so renaming a layout change into a hook migration would have been the more expensive mistake. Absent when `actions` is empty or `hideActionsRow` is on |
|
|
373
373
|
| `[data-grid-action="<label>"]` | **`adg-m3`.** One command. ⚠️ **In the DOM only where it is REACHABLE:** with ONE action it is the inline button in the bar; with TWO OR MORE it lives inside `[data-grid-actions-panel]` and therefore exists only while the popover is OPEN. A test that counts these must click the trigger first — before `adg-m3` all of them were in the DOM at rest |
|
|
374
374
|
| `[data-grid-actions-trigger]` | **`adg-m3`.** The single collapsed trigger, rendered INSTEAD of the buttons from two actions up. Named `i18n.tableActions.actions`, dressed with `gridToolbarTriggerClass`, `iconRight="ChevronDown"`. It re-declares `data-slot="button"`, because with `asChild` Radix's `data-slot="popover-trigger"` would win otherwise — same reason as the columns trigger |
|
|
@@ -386,9 +386,9 @@ Stable hooks, independent of the i18n language:
|
|
|
386
386
|
| `[data-grid-filter-cell-tick="<id>"]` | The zero row's **column mark**: a 1px-wide, `h-3` (12px) span pinned to the cell's LEFT edge and vertically CENTRED, `aria-hidden` and `pointer-events-none`. Present on every filter cell except the first and except a right-pinned block's edge cell. ⚠️ **It is a node and not a `box-shadow`:** an `inset` shadow covers the box's whole side, so the mark came out as tall as the cell and welded the zero row to the headers. Its height is asserted, not just its presence, because a full-height mark would satisfy a probe that only counts nodes |
|
|
387
387
|
| `[data-grid-filter-cell-trigger="<id>"]` | The one target of a filter cell: a real `<button>`, the cell's only tab stop, opening the Fase 7 filter atom in a popover. Carries `data-filter-truncated="true"\|"false"` — **always present**, so asserting its PRESENCE is green on a cell that truncates nothing — plus `title` and an `sr-only` node with the whole text **only when truncated**. **No glyph marks the cut at any width** — the ellipsis is the CSS one, drawn by `truncate` on the text span. Its `aria-label` names the operator IN FULL in both branches, even when the visible text takes the short form |
|
|
388
388
|
| `[data-grid-filter-cell-panel="<id>"]` | The popover of a filter cell. Radix **portals it to `document.body`**, so it is not inside the grid element. Inside: `[data-grid-filter-cell-operator="<id>"]` (the operator `<select>`, also carrying `data-grid-filter-cell-type`) and the atom's editor, named `filter-row-<id>` |
|
|
389
|
-
| `[data-grid-toolbar-filter-row-toggle]`
|
|
389
|
+
| `[data-grid-toolbar-filter-row-toggle]` | The «Show / Hide filter row» button, rendered in the toolbar with `filterMode="row"` — **the DEFAULT since 2026-09-22, so it is in the toolbar of an ordinary grid**, not only of one that opted in. `aria-expanded` says whether the ZERO ROW is MOUNTED — since R13 closing it removes the `<tr>` entirely, and this button is the only way back, so it is rendered in both states. **From 2026-09-23** a DS `Button` `variant="ghost"`, and `toolbar.filterRowToggle: false` removes it (whoever does so owns the way back, e.g. via `filterRowOpen`) |
|
|
390
390
|
| `[data-grid-toolbar]` | The toolbar row (Fase 3 **named** it; the row itself predates the phase and P6 renders it always, so it is present even at a minimal call site). `flex flex-wrap items-center gap-2` — `items-center` and no longer `items-end`, because from `adg-m3` the search cell is one line tall like the others. **From `adg-m3` it has TWO groups**: the direct children up to the filter-row indicator (what NARROWS the data), then one `.ml-auto` wrapper holding «columns», the `actions` prop's own buttons (`[data-grid-actions-row]`, from `adg-m3`), the `toolbar.actions` slot and the full-screen toggle, in that order (what changes the VIEW or takes the data out). ⚠️ **`ml-auto` only does work because the search lost `flex-1`**, and the measured four-way table is in the `play` of `AppDataGrid/Confronto`: with the field growing, `ml-auto` present or absent gives the identical layout. So a probe on the group's RIGHT edge alone is vacuous — the group ends flush right in three of the four combinations. Measure the GAP before the group instead: `gap-2` is 8px, and an auto margin that works makes it more |
|
|
391
|
-
| `[data-grid-toolbar-columns]`
|
|
391
|
+
| `[data-grid-toolbar-columns]` | The «columns» trigger — a DS `Button` `variant="ghost"`. **Visible by default from 2026-09-23, even with the key omitted**, and rendered ONLY when the visibility map can actually be written: always without the `columnVisibility` prop (the reducer writes), or with `columnVisibility` + `onColumnVisibilityChange`. `toolbar.columns: false` removes it; `render` replaces it and wins over the writability check |
|
|
392
392
|
| `[data-grid-toolbar-columns-panel]` | The popover content holding `GridColumnRegistryPanel`, owned by the grid. Portaled **inside `[data-app-data-grid]`**, not to `<body>` — at full screen a body-level panel sits outside the sentinel cycle and outside `aria-modal`, so its checkboxes are neither tab-reachable nor announced |
|
|
393
393
|
| `[data-grid-toolbar-filters]` | The «filters» trigger — opt-in via `toolbar.filters`, and rendered ONLY when the slot carries `onOpen` or `render`. Also carries `data-grid-toolbar-filter-count`, its own attribute and deliberately **not** the chips row's `data-grid-filter-count`: two nodes sharing one attribute would make an exact `querySelector` ambiguous |
|
|
394
394
|
| `[data-grid-column-registry-item="<id>"]` | One row of the registry — the `visibility + position` pair. **New in Fase 3.** It is the row container, not the `<label>`: the reorder handles cannot live inside a `<label>` (a click on a label descendant toggles the control the label names, so moving a column would hide it). The panel carries one row per **leaf** column, `accessorKey` or not |
|
|
@@ -420,10 +420,10 @@ Stable hooks, independent of the i18n language:
|
|
|
420
420
|
| `[data-grid-footer-cell="<columnId>"]` | **Fase 5.** One cell per visible column (`"__select"` for the checkbox one), in the ORDER of the body cells and never one for the ghost column. Pinned cells carry `data-grid-pinned="true"` and read the SAME `--grid-pin-*` variables as the body: left/right coincide with the body cell of the same column, right band included |
|
|
421
421
|
| `[data-grid-footer-label]` | **Fase 5.** The label cell carrying the unit (`i18n.table.totalsLabel`), on the FIRST visible column that declares no total. It also carries `data-grid-footer-cell`. **Declared limit:** if EVERY visible column declares a total the row has no label. It is the second node the colSpan gate admits, and today it uses no horizontal extension — the footer renders one `<td>` per column |
|
|
422
422
|
| `[data-grid-pagination]` | The paginator container (Fase 4 filled it; the node predates it). Rendered when **the body has data rows** AND (`paginated` is true OR (`pagination.pageSize` is set AND `groupBy` is absent)). **From Fase 5 the «body has data rows» half is not optional**: above an empty, errored or loading body «Rows 1-10 of 128» is a lie with the air of a detail, so the whole paginator — container, range, indicator and rows clause — is absent. On the client branch it holds ONLY `[data-grid-page-range]` / `[data-grid-row-count]` |
|
|
423
|
-
| `[data-grid-page-indicator]`
|
|
423
|
+
| `[data-grid-page-indicator]` | The «Page i/N» indicator (`${i18n.pagination.page} ${i}/${N}`, e.g. «Pagina 2/4» — since 2026-09-23; it used to print a bare «2 / 4»), on the SAME two figures the bar navigates on (`currentPage` / `scope.pageCount`, falling back to the engine's count when no total is declared). It used to read `pageIndex + 1` and the ENGINE's count while everything else read the scope's — two derivations of one scope figure in one control, which on the client branch printed «1 / 1» next to «1 2 3 … 13» and «2 / 1» after a click. Rendered LAST in the left group, after the size select and the range. **ABSENT when the paginator is not navigable** (client branch) |
|
|
424
424
|
| `[data-grid-page-range]` | The range — «Rows 1-10 of 128» (Fase 4, NEW node). ASCII hyphen, composed inside `gridResultScope`, never an en dash. Rendered only when the total is KNOWN and non-zero — declared (`resultTotal` / `serverConfig.totalItems`) or, from `adg-m2`, DERIVED on a grid that paginates itself |
|
|
425
425
|
| `[data-grid-row-count]` | The rows clause — «1 row» / «128 rows». Rendered only on ONE page: next to a range it would print the same figure twice. **From Fase 5 never above a body with no data** — the paginator that holds it is not rendered at all there, so the old «or when empty» case is gone |
|
|
426
|
-
| `[data-grid-page-size]`
|
|
426
|
+
| `[data-grid-page-size]` | The WRAPPER of the «rows per page» `AppSelect` (since 2026-09-23; it was a native `<select>`). `AppSelect` forwards no `data-*`, so the marker sits on the wrapper: the trigger is `[role="combobox"]` inside it, the live size is `data-grid-page-size-value`, the options (`cmdk-item`, `data-value` `"10"`/`"25"`/`"50"`/`"100"`, text `«25 per page»`) exist only while the list is open. The list portals INTO the grid root (`AppSelect` `portalContainer`), so in full screen it opens above `gridZ.fullscreen` instead of behind it. Choosing a size dispatches `query/page-size` — one emission, no remount; re-choosing the current one emits nothing. **The LIVE size always has an option**, even out of scale (`pageSize={20}` adds «20 per page» in order). Rendered FIRST in the left group. Absent when not navigable |
|
|
427
427
|
| `[data-grid-page-first]` / `[data-grid-page-last]` | First / last page. Disabled at the ends, never hidden. Absent when not navigable |
|
|
428
428
|
| `[data-grid-page-prev]` / `[data-grid-page-next]` | Previous / next page. They navigate from the CLAMPED page, not from the raw `pageIndex`: with a stale high `pageIndex` (the consumer narrowed `totalItems` without dispatching) «previous» used to emit a `query/page` — a refetch — while nothing moved on screen. Absent when not navigable |
|
|
429
429
|
| `[data-grid-page-number="<n>"]` | One page button, `n` 1-based. Exactly one carries `aria-current="page"` (the STRING, not `true`); its accessible name contains the visible digit. Absent when not navigable |
|
|
@@ -856,8 +856,8 @@ must force the overflow (declare widths whose sum exceeds the container) or it p
|
|
|
856
856
|
|
|
857
857
|
The grid **no longer renders a density switcher**, and `toolbar.density` **no longer exists** as a key.
|
|
858
858
|
Row height is set by the `density` prop, default `"default"` (standard, 36px) — unchanged. This retracts
|
|
859
|
-
the P6 rule for that one key
|
|
860
|
-
|
|
859
|
+
the P6 rule for that one key. `search` is still rendered always; from 2026-09-23 `fullscreen` is rendered by
|
|
860
|
+
default and switched off with `toolbar={{ fullscreen: false }}`.
|
|
861
861
|
|
|
862
862
|
**Why.** Density is view configuration, and whoever mounts the table is who knows whether its rows should
|
|
863
863
|
be tight or roomy. The retired argument was «a table whose density changes only if the call-site
|
|
@@ -896,7 +896,7 @@ the paginator announces. While the control lived inside the grid this could not
|
|
|
896
896
|
### Density, full screen and vertical space (P6)
|
|
897
897
|
|
|
898
898
|
Density is a real control, not the provisional switch P2 left behind: `showDensityControl` **is gone**, and
|
|
899
|
-
the
|
|
899
|
+
the full-screen toggle is rendered in the toolbar by default (a ghost button; `toolbar.fullscreen: false` hides it). The
|
|
900
900
|
row height still comes from `gridRowHeight` in `grid-tokens.ts` — `32 / 36 / 48`, exact and not estimated.
|
|
901
901
|
|
|
902
902
|
**Full screen is a CSS promotion of the same DOM node.** The root gains `fixed inset-0` and the
|
|
@@ -1236,9 +1236,9 @@ filed against work that is already done.
|
|
|
1236
1236
|
|
|
1237
1237
|
**The registry is one place** (also signed) — and since the `⋮` menu was removed it is the **only** place.
|
|
1238
1238
|
`columnVisibility` is still the only truth about which columns exist, and the registry panel is the view that
|
|
1239
|
-
shows all of it. ⚠️ **Consequence to know:**
|
|
1240
|
-
|
|
1241
|
-
column** — the menu item was that way. Two places to hide a column would have been two sources for one
|
|
1239
|
+
shows all of it. ⚠️ **Consequence to know:** from 2026-09-23 its trigger is rendered by default whenever the visibility map is
|
|
1240
|
+
writable, but `toolbar.columns: false` switches it off — and on a grid that does so (and mounts no
|
|
1241
|
+
`GridColumnRegistryPanel`) there is **no way, inside the table, to hide a column** — the menu item was that way. Two places to hide a column would have been two sources for one
|
|
1242
1242
|
question; one place that the caller may not mount is a different trade, and this is where it is written down.
|
|
1243
1243
|
|
|
1244
1244
|
⚠️ **Hiding a column drops its filter too**, and that follows from the decision rather than extending it: a
|
|
@@ -1306,7 +1306,7 @@ after it. The phantom `<col>` of P4 stays exactly one.
|
|
|
1306
1306
|
grid keeps pruning until one column is left; hiding that one returns the **whole** tree instead of an empty
|
|
1307
1307
|
one. Without the floor `pruneHiddenGridColumns` returns `[]`, which renders an empty `<colgroup>`, an empty
|
|
1308
1308
|
`<thead>` and rows with no cells — and that state is **terminal**: with the header gone there is no `⋮` menu
|
|
1309
|
-
left to open, and `toolbar.columns`
|
|
1309
|
+
left to open, and `toolbar.columns` was absent by default at the time (it is rendered by default from 2026-09-23), so on a grid that opted into no slots the header menu
|
|
1310
1310
|
was the only way to hide and there was no inverse anywhere. Only a remount got out of it.
|
|
1311
1311
|
|
|
1312
1312
|
Only **one** place prunes (`visibleTree` in the engine), and that is where the floor lives — never a second
|
|
@@ -51,6 +51,7 @@ The component uses discriminated union types based on the `multiple` prop.
|
|
|
51
51
|
| `data-testid` | `string` | `undefined` | Test identifier attribute for E2E testing (e.g. Playwright) |
|
|
52
52
|
| `aria-label` | `string` | `undefined` | Accessible name forwarded to the trigger. Applied ONLY when `label` is absent |
|
|
53
53
|
| `aria-labelledby` | `string` | `undefined` | Id(s) of external element(s) naming the trigger. Applied ONLY when `label` is absent |
|
|
54
|
+
| `portalContainer` | `HTMLElement \| null` | `undefined` (`<body>`) | Element the options popover portals into. Use it when the select lives inside a container with its own stacking context (e.g. a fixed full-screen grid), where a `<body>` portal would open behind it |
|
|
54
55
|
|
|
55
56
|
### Single Select Props
|
|
56
57
|
|