laif-ds 1.0.2-app-data-grid.80 → 1.0.2-app-data-grid.82

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 CHANGED
@@ -7,6 +7,20 @@ 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
 
12
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`.
@@ -1,8 +1,8 @@
1
1
  "use client";
2
- import { getDefaultExportFromCjs as r } from "./_commonjsHelpers.js";
3
- import { __require as e } from "../node_modules/style-to-js/cjs/index.js";
4
- var t = e();
5
- const a = /* @__PURE__ */ r(t);
2
+ import { getDefaultExportFromCjs as e } from "./_commonjsHelpers.js";
3
+ import { __require as t } from "../node_modules/eventemitter3/index2.js";
4
+ var r = t();
5
+ const m = /* @__PURE__ */ e(r);
6
6
  export {
7
- a as default
7
+ m as default
8
8
  };
@@ -1,8 +1,8 @@
1
1
  "use client";
2
- import { getDefaultExportFromCjs as e } from "./_commonjsHelpers.js";
3
- import { __require as r } from "../node_modules/extend/index.js";
4
- var t = r();
5
- const x = /* @__PURE__ */ e(t);
2
+ import { getDefaultExportFromCjs as r } from "./_commonjsHelpers.js";
3
+ import { __require as e } from "../node_modules/style-to-js/cjs/index.js";
4
+ var t = e();
5
+ const a = /* @__PURE__ */ r(t);
6
6
  export {
7
- x as default
7
+ a as default
8
8
  };
@@ -1,8 +1,8 @@
1
1
  "use client";
2
2
  import { getDefaultExportFromCjs as e } from "./_commonjsHelpers.js";
3
- import { __require as t } from "../node_modules/eventemitter3/index2.js";
4
- var r = t();
5
- const m = /* @__PURE__ */ e(r);
3
+ import { __require as r } from "../node_modules/extend/index.js";
4
+ var t = r();
5
+ const x = /* @__PURE__ */ e(t);
6
6
  export {
7
- m as default
7
+ x as default
8
8
  };
@@ -1,5 +1,5 @@
1
1
  "use client";
2
- var e = {};
2
+ var e = { exports: {} };
3
3
  export {
4
- e as __exports
4
+ e as __module
5
5
  };
@@ -1,5 +1,8 @@
1
1
  "use client";
2
- var e = { exports: {} };
2
+ import { getDefaultExportFromCjs as r } from "./_commonjsHelpers.js";
3
+ import { __require as e } from "../node_modules/classnames/index.js";
4
+ var s = e();
5
+ const o = /* @__PURE__ */ r(s);
3
6
  export {
4
- e as __module
7
+ o as default
5
8
  };
@@ -1,8 +1,5 @@
1
1
  "use client";
2
- import { getDefaultExportFromCjs as r } from "./_commonjsHelpers.js";
3
- import { __require as e } from "../node_modules/classnames/index.js";
4
- var s = e();
5
- const o = /* @__PURE__ */ r(s);
2
+ var e = {};
6
3
  export {
7
- o as default
4
+ e as __exports
8
5
  };
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "schemaVersion": "1.0.0",
3
- "generatedAt": "2026-09-22T17:28:05.419Z",
3
+ "generatedAt": "2026-09-23T08:34:28.545Z",
4
4
  "package": {
5
5
  "name": "laif-ds",
6
- "version": "1.0.2-app-data-grid.80"
6
+ "version": "1.0.2-app-data-grid.82"
7
7
  },
8
8
  "summary": {
9
9
  "scannedFiles": 489,
@@ -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` | `10` | Rows per page when `paginated`. ⚠️ **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). |
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&gt;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[]` | `[]` | The buttons the grid dresses itself, `{label, icon, onClick, data-testid}` — rendered **INSIDE `[data-grid-toolbar]`**, in its `.ml-auto` right group, between «columns» and the `toolbar.actions` slot. ⚠️ **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. |
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` | **Fase 3, live.** The shell, opt-in **per slot**: `search`, `fullscreen`, `filters`, `resetFilters`, `columns`, `actions`. ⚠️ **`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`/`density`/`fullscreen` are rendered **always** — declaring them only lets you substitute the node, and omitting them does not switch them off (the P6 rule is not retracted behind a prop). `filters`/`resetFilters`/`columns`/`actions` are **absent by default**, and the four use four **different** conditions: `columns` needs the key **and** a writable visibility map (see `onColumnVisibilityChange`); `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: 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. |
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:1081`, cited by line number from a gate), and
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]` | 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`. Its dress is the toolbar triggers' shared `gridToolbarTriggerClass` (`h-6 px-2 text-xs`, ≥24px per WCAG 2.2 SC 2.5.8) and no longer a local `cn(...)`, so a probe can assert it by IDENTITY — a `toContain("h-6")` would stay green on a copy |
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="&lt;label&gt;"]` | **`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]` | 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 |
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]` | The «columns» trigger — opt-in via `toolbar.columns`, and rendered ONLY when the visibility map can actually be written (see `onColumnVisibilityChange`) |
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]` | The «i / N» indicator, 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. On the server branch the two coincide by construction (TanStack gets `rowCount: totalItems`), so the three spec files that read it by text (`"1 / 5"` verbatim, `split("/")`) are unchanged. **ABSENT when the paginator is not navigable** (client branch) |
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]` | The «rows per page» `<select>` (values `"10"`, `"25"`, `"50"`, `"100"`; option text `«25 per page»`). Its change dispatches `query/page-size` — one emission, no remount. **The LIVE size always has an option**, even out of scale: `pageSize` is `number`, not `GridPageSizeOption`, so `pageSize={20}` would otherwise leave a controlled `<select>` at `selectedIndex` -1 — an empty selector next to 20-row pages. Absent when not navigable |
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; `search` and `fullscreen` are still rendered always, and decision 26 still
860
- holds for them.
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 three-button group (`S` / `M` / `L`) plus the full-screen toggle are always rendered in the toolbar. 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:** that panel is opt-in (`toolbar.columns`, or a consumer-mounted
1240
- `GridColumnRegistryPanel`), so on a grid that mounts neither there is now **no way, inside the table, to hide a
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` is absent by default, so on a grid that opts into no slots the header menu
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
 
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "schemaVersion": "1.1.0",
3
- "generatedAt": "2026-09-22T17:28:04.919Z",
3
+ "generatedAt": "2026-09-23T08:34:27.980Z",
4
4
  "package": {
5
5
  "name": "laif-ds",
6
- "version": "1.0.2-app-data-grid.80"
6
+ "version": "1.0.2-app-data-grid.82"
7
7
  },
8
8
  "stats": {
9
9
  "documentedComponentCount": 97,
@@ -1074,10 +1074,10 @@
1074
1074
  },
1075
1075
  "metadata": {
1076
1076
  "props": {
1077
- "totalProps": 21,
1077
+ "totalProps": 22,
1078
1078
  "requiredPropsCount": 1,
1079
- "typedPropsCount": 21,
1080
- "describedPropsCount": 21
1079
+ "typedPropsCount": 22,
1080
+ "describedPropsCount": 22
1081
1081
  },
1082
1082
  "accessibility": {
1083
1083
  "hasCoverage": true
@@ -6549,5 +6549,5 @@
6549
6549
  ]
6550
6550
  }
6551
6551
  ],
6552
- "checksum": "92e578a83a1d88b7f6993c0cbf205f625b03f71874b1a3254a5459a68f2d94b0"
6552
+ "checksum": "186b8095a19a840494aa8a707120966d122e1c00a35cb9a87ff2ff4e3e31c516"
6553
6553
  }