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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/dist/CHANGELOG.md +7 -0
  2. package/dist/_virtual/index3.js +5 -5
  3. package/dist/_virtual/index4.js +5 -5
  4. package/dist/_virtual/index5.js +4 -4
  5. package/dist/_virtual/index6.js +2 -2
  6. package/dist/_virtual/index7.js +2 -5
  7. package/dist/_virtual/index8.js +5 -2
  8. package/dist/agent-docs/adoption-report.json +3 -3
  9. package/dist/agent-docs/components/AppDataGrid.md +11 -10
  10. package/dist/agent-docs/components/DatePicker.md +1 -0
  11. package/dist/agent-docs/components/DropdownMenu.md +2 -0
  12. package/dist/agent-docs/manifest.json +6 -6
  13. package/dist/components/ui/app-data-grid/app-data-grid.js +229 -229
  14. package/dist/components/ui/app-data-grid/parts/grid-filter-chips.js +57 -47
  15. package/dist/components/ui/app-data-grid/parts/grid-filter-editor.js +48 -44
  16. package/dist/components/ui/app-data-grid/parts/grid-filter-row.js +134 -130
  17. package/dist/components/ui/app-data-grid/parts/grid-portal-container.js +14 -0
  18. package/dist/components/ui/date-picker.js +36 -34
  19. package/dist/components/ui/dropdown-menu.js +3 -3
  20. package/dist/components/ui/gantt/components/Chart/Chart.js +1 -1
  21. package/dist/index.d.ts +46 -5
  22. package/dist/node_modules/eventemitter3/index.js +1 -1
  23. package/dist/node_modules/eventemitter3/index2.js +1 -1
  24. package/dist/node_modules/hast-util-to-jsx-runtime/lib/index.js +1 -1
  25. package/dist/node_modules/recharts/es6/util/Events.js +1 -1
  26. package/dist/node_modules/style-to-object/cjs/index.js +1 -1
  27. package/dist/node_modules/unified/lib/index.js +1 -1
  28. package/package.json +1 -1
package/dist/CHANGELOG.md CHANGED
@@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ### 🐛 Fixed — overlay di AppDataGrid a schermo intero e toggle della riga (2026-09-23)
11
+
12
+ - **AppDataGrid**: **A schermo intero il pannello di una cella della Riga Zero si apriva SOTTO la griglia.** La radice è `fixed` a `gridZ.fullscreen` (110), e il pannello — con l'elenco degli operatori, gli `AppSelect` e il calendario del `DatePicker` dell'editor — si portalava nel `<body>` a `z-[60]`: invisibile e irraggiungibile. Ora pannello, elenco degli operatori, elenchi e calendario di `GridFilterValueEditor`, compositore «da … a …» (`GridFilterRangeComposer`) e menu delle pill (`GridFilterChips`) si montano **dentro `[data-app-data-grid]`**, risolta dal DOM (`parts/grid-portal-container.ts`), come già il pannello delle colonne. Fuori da una griglia — le parti sono export pubblici — il portale resta il `<body>`. `Escape` chiude un livello alla volta e non esce dallo schermo intero finché un overlay è aperto. Prova: `AppDataGrid/Overlay a schermo intero e toggle della riga` (hit test con `elementFromPoint` al centro di ogni overlay). ⚠️ Chi cercava `[data-grid-filter-cell-panel]` con `closest("[data-app-data-grid]") === null` deve aggiornare l'asserzione.
13
+ - **AppDataGrid**: ⚠️ **`toolbar.filterRowToggle.render` riceve `{ open, toggle }`** (`GridFilterRowToggleRenderProps`, nuovo slot `GridToolbarFilterRowToggleSlot`, entrambi esportati). Prima `render` non riceveva niente, e il TSDoc prometteva di governare `filterRowOpen` da fuori — prop che non esiste: lo stato della riga è locale. `open` dice se la riga è montata, `toggle` è lo stesso gesto del bottone della griglia (annuncia una volta a `onFilterRowOpenChange`). Un `render` senza parametri resta valido; `() => null` resta un errore del compilatore.
14
+ - **DatePicker**: nuova prop opzionale **`portalContainer?: HTMLElement | null`** — l'elemento in cui si monta il calendario (difetto: il `<body>`, come prima).
15
+ - **DropdownMenu**: `DropdownMenuContent` accetta **`container?: HTMLElement | null`** — l'elemento in cui si monta il menu (difetto: il `<body>`, come prima).
16
+
10
17
  ### 🔧 Changed — paginatore di AppDataGrid (2026-09-23)
11
18
 
12
19
  - **AppDataGrid**: ⚠️ **Breaking-ish — il difetto di `pageSize` passa da `10` a `25`.** Chi montava la griglia paginata senza dichiarare la taglia ora vede 25 righe per pagina (e 25 scheletri durante il caricamento). La taglia resta dichiarabile dall'esterno con la prop `pageSize`, con `pagination.pageSize` o con `initialState.pagination.pageSize` (precedenza invariata: `initialState` > slot > prop). `GRID_PAGE_SIZE_OPTIONS` resta `[10, 25, 50, 100]`. Decisione esplicita del proprietario.
@@ -1,8 +1,8 @@
1
1
  "use client";
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);
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
- m as default
7
+ a as default
8
8
  };
@@ -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 r } from "../node_modules/extend/index.js";
4
+ var t = r();
5
+ const x = /* @__PURE__ */ e(t);
6
6
  export {
7
- a as default
7
+ x 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 r } from "../node_modules/extend/index.js";
4
- var t = r();
5
- const x = /* @__PURE__ */ e(t);
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
- x as default
7
+ m as default
8
8
  };
@@ -1,5 +1,5 @@
1
1
  "use client";
2
- var e = { exports: {} };
2
+ var e = {};
3
3
  export {
4
- e as __module
4
+ e as __exports
5
5
  };
@@ -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 = { exports: {} };
6
3
  export {
7
- o as default
4
+ e as __module
8
5
  };
@@ -1,5 +1,8 @@
1
1
  "use client";
2
- var e = {};
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 __exports
7
+ o as default
5
8
  };
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "schemaVersion": "1.0.0",
3
- "generatedAt": "2026-09-23T08:34:28.545Z",
3
+ "generatedAt": "2026-09-23T10:54:10.419Z",
4
4
  "package": {
5
5
  "name": "laif-ds",
6
- "version": "1.0.2-app-data-grid.82"
6
+ "version": "1.0.2-app-data-grid.88"
7
7
  },
8
8
  "summary": {
9
- "scannedFiles": 489,
9
+ "scannedFiles": 491,
10
10
  "deprecatedComponentsCount": 4,
11
11
  "deprecatedComponentsUsedCount": 2,
12
12
  "componentsWithoutExamplesCount": 5,
@@ -62,13 +62,13 @@ 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, 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. |
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 — for `filterRowToggle` `render` receives `{ open, toggle }` (`GridFilterRowToggleRenderProps`), the only way to read and change the zero row's local state from outside. `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. |
69
69
  | `onColumnVisibilityChange` | `(columnVisibility: Readonly<Record<string, boolean>>) => void` | `undefined` | The missing half of `columnVisibility`, and the twin of `onDensityChange`. Until Fase 3 nothing **inside** the grid wrote that map — the panel was consumer-mounted and wrote it itself — so the controlled half existed without a callback. From Fase 3 the `toolbar.columns` slot lives inside the grid and dispatches `view/column-visibility` (the column menu's hide item did too, until the `⋮` menu was removed); but when `columnVisibility` arrives as a prop the engine **overrides** that reducer slice with the prop, so the dispatch would be invisible. With this callback the grid **asks** the owner of the map for the change instead of writing where nobody reads. Receives the already-applied map, in the slice's convention: an absent key means visible. ⚠️ **Omitting it while `columnVisibility` is passed switches the control off.** In that combination nobody can write the map — the dispatch is suppressed and the request has no recipient — so the «columns» trigger is **not rendered** at all, rather than accepting a click and snapping back in the same commit. Same rule the phase already applies to `toolbar.filters` without `onOpen`: a button that does nothing is worse than a button that is not there. Measured: with the prop and no callback `[data-grid-toolbar-columns]` is not in the DOM; adding the callback brings it back. (It used to switch off TWO controls, the second being the `⋮` menu's hide item, removed with the menu.) |
70
70
  | `onColumnWidthsChange` | `(columnWidths: Readonly<Record<string, number>>) => void` | `undefined` | **The capture half of `columnWidths`, and until this prop there was none.** A width was always restorable through `preset.view.columnWidths`, and never readable: `view/column-width` lives in the reducer and no prop carried it back out, so a persistence consumer saved the widths frozen at their first value forever — which is written down as the literals `columnWidths: {}` / `columnOrder: []` in the phase's own reference harness. **Not an emission** — a width is view configuration, so it cannot refetch: `view/column-width` is a `GridViewAction`, `onQueryChange` does not appear in `applyView`, and `data-emissions` does not move. Receives the **already-applied, whole** map, in the slice's convention, exactly like `onColumnVisibilityChange`. ⚠️ **The map is PARTIAL, and the partiality is the value:** a column never dragged is **not a key**, and that absence is what `parts/grid-table.tsx` reads to decide which columns are elastic and take part in slack distribution. Completing it would switch slack distribution off at the first save AND emit a value different from what `preset.view.columnWidths` stores — the round trip would not come back. ⚠️ **One call per commit, not one per pixel:** between `pointerdown` and `pointerup` the width is written straight to the `<col>` and consolidated into state once, on release; `pointercancel` does not consolidate, so it does not notify. ⚠️ **Declared limit — the keyboard commits once per keypress**, so a held arrow key yields one notification per OS key-repeat. The grid does not smooth it: `serverDebounceTime` is declared for query _emission_, and a view action has no emitter to protect. Debounce on your side if you POST per notification. ⚠️ **Declared limit — a zero-delta gesture notifies too, and the notification is faithful.** `pointerup` commits the seeded start width whatever it is, so a still click on the handle (a mis-click, or the click that focuses the separator before using the arrows) dispatches the rendered width and the column leaves the elastic set from then on. This prop does not filter it: the dispatch is pre-existing P4 behavior, so suppressing only the notification would leave the consumer's copy diverging from the reducer slice — the one thing worse than an extra notification. ⚠️ **Declared limit — the width handed over is the RENDERED one**, i.e. post-`distributeGridColumnSlack`, so the first notification for a formerly elastic column carries a number nobody typed and nobody dragged to: well-defined but **viewport-dependent**, which means a view captured on a 1600px window and restored on a 1200px one pins a column at a width that window would never have given it. Restore itself is safe — `columnWidths[id] ?? header.width ?? gridColumnWidth.default`, then clamped. ⚠️ **Declared limit — it does NOT fire when a `preset` is applied**, deliberately: `applyPreset` dispatches `view/preset` straight to the reducer and never reaches this handler. Firing there would close a write-back loop (load the saved view → `preset` → callback → the consumer POSTs what it just GET-ed). Restoration is announced once and wholesale through `onPresetApplied`. The asymmetry with `onDensityChange`, which _does_ fire on a preset, is not a precedent — that one was rescued there because `adg-c12` removed its last caller. ⚠⚠ **And therefore the consumer MUST re-seed its captured copy from `onPresetApplied`.** `view/preset` replaces `columnWidths` wholesale and no capture callback announces it: drag `city` to 400, then restore an older view whose `columnWidths` is `{}` — the grid goes back to all-elastic while the consumer's copy still says `{city:400}`, and the next save, with nobody having touched anything, writes it back and undoes the restore in silence. One line in `onPresetApplied` fixes it. ⚠️ **No controlled half:** there is no `columnWidths` prop, because `preset.view` already imposes widths in one tick with one emission. Consequence: `preset` is guarded by object **identity**, so restoring the same saved view twice needs a new object. |
71
- | `onColumnOrderChange` | `(columnOrder: string[]) => void` | `undefined` | **The capture half of the column order, and the twin of `onColumnWidthsChange`.** `view/column-order` has a single dispatch in the whole grid and had no way out; a `GridColumnRegistryPanel` you mount yourself has always had its own `onColumnOrderChange`, but an order decided there does not reach the grid — there is no `columnOrder` prop. **Not an emission** — column order changes nothing the server needs to know, and `view/column-order` is not a `GridQueryAction`. Receives the **new, whole** order, not the move. `string[]` and not `readonly string[]` on purpose: `GridViewPresetView.columnOrder` is `string[]`, so a readonly array is `TS4104` on assignment and would force `[...columnOrder]` in every consumer; the widths map stays `Readonly<Record<…>>` because TypeScript ignores the modifier there — the asymmetry is the language's, and it is the same one the panel already has. ⚠️ **Never fires on a non-event**, and not because this point guards: the panel already rejects a move past the end of the list, one that crosses a pinned block, and an identical order. ⚠⚠ **Declared limit — without the IN-GRID registry this prop can never be invoked, and passing `toolbar.columns` is NOT enough.** The node that wires it renders only if `toolbar.columns` is present, does **not** carry its own `render` (a consumer's panel wins the precondition, deliberately), **and** `canWriteVisibility` is true — i.e. `columnVisibility` is absent, or arrives together with `onColumnVisibilityChange`. The combination «controlled visibility without its callback» switches the trigger off entirely, and with it the reorder. Pass the three together, or this is a prop that cannot fire. ⚠️ **Declared limit — it does NOT fire when a `preset` is applied**, same reason, same loop, same restore channel as `onColumnWidthsChange`, and the same **obligation** to re-seed the captured copy from `onPresetApplied`, because `view/preset` replaces `columnOrder` wholesale too. ⚠️ **Declared limit — the order is a FLAT list that does not say whose an id is.** The engine sorts **siblings** inside their group, so a saved order whose move crosses a group boundary is a visible non-event. Pre-existing P7 behavior, true of the captured value as of the applied one. ⚠️ **No controlled half:** the order lives only in the reducer, and `preset.view.columnOrder` is the way to impose it. The panel tolerates a **partial** order — it appends the ids the saved order does not name — so a view saved before a column existed does not lose it. |
71
+ | `onColumnOrderChange` | `(columnOrder: string[]) => void` | `undefined` | **The capture half of the column order, and the twin of `onColumnWidthsChange`.** `view/column-order` has a single dispatch in the whole grid and had no way out; a `GridColumnRegistryPanel` you mount yourself has always had its own `onColumnOrderChange`, but an order decided there does not reach the grid — there is no `columnOrder` prop. **Not an emission** — column order changes nothing the server needs to know, and `view/column-order` is not a `GridQueryAction`. Receives the **new, whole** order, not the move. `string[]` and not `readonly string[]` on purpose: `GridViewPresetView.columnOrder` is `string[]`, so a readonly array is `TS4104` on assignment and would force `[...columnOrder]` in every consumer; the widths map stays `Readonly<Record<…>>` because TypeScript ignores the modifier there — the asymmetry is the language's, and it is the same one the panel already has. ⚠️ **Never fires on a non-event**, and not because this point guards: the panel already rejects a move past the end of the list, one that crosses a pinned block, and an identical order. ⚠⚠ **Declared limit — without the IN-GRID registry this prop can never be invoked.** The node that wires it renders unless `toolbar.columns` is `false` (from 2026-09-23 it is on by default, key omitted included), does **not** carry its own `render` (a consumer's panel wins the precondition, deliberately), **and** `canWriteVisibility` is true — i.e. `columnVisibility` is absent, or arrives together with `onColumnVisibilityChange`. The combination «controlled visibility without its callback» switches the trigger off entirely, and with it the reorder. Pass the three together, or this is a prop that cannot fire. ⚠️ **Declared limit — it does NOT fire when a `preset` is applied**, same reason, same loop, same restore channel as `onColumnWidthsChange`, and the same **obligation** to re-seed the captured copy from `onPresetApplied`, because `view/preset` replaces `columnOrder` wholesale too. ⚠️ **Declared limit — the order is a FLAT list that does not say whose an id is.** The engine sorts **siblings** inside their group, so a saved order whose move crosses a group boundary is a visible non-event. Pre-existing P7 behavior, true of the captured value as of the applied one. ⚠️ **No controlled half:** the order lives only in the reducer, and `preset.view.columnOrder` is the way to impose it. The panel tolerates a **partial** order — it appends the ids the saved order does not name — so a view saved before a column existed does not lose it. |
72
72
  | `resultTotal` | `number` | `undefined` | **CLIENT BRANCH ONLY — Fase 4.** How many rows exist IN TOTAL, i.e. the figure the paginator prints after «of» («Rows 1-10 of 128») and the chip row's clause reads. NOT the number of rows rendered: in client mode `data` may already be a page, and `data.length` would say 10 where the scope is 128. The server-branch twin is `serverConfig.totalItems` — the two never merge, and which one you pass is decided by the branch. Omitted, the scope equals the rows actually rendered — **except on a grid that paginates itself**, where from `adg-m2` it is derived: see «The scope on a self-paginating grid». Passing it (or `pagination.pageSize`) is also what turns the paginator on on the client branch, where `paginated` is `never`. |
73
73
  | `groupLabelPlacement` | `"grouping-column" \| "first-column"` | `"grouping-column"` | **CLIENT BRANCH ONLY — M5.** Where a group row's label, chevron and count land. They are ONE node and move together, so this prop picks the cell all three fall into. `"grouping-column"` (default) puts them in the cell of the column that groups **at that level**; `"first-column"` puts them in the **first rendered column**, which is the mockup's shape. ⚠️ **It places, it does not pin**: the identity sits in a frozen left band that does not scroll away ONLY if that first column declares `pinned: "left"`. On an unpinned first column the label scrolls away with the body — the play passes because the fixture's first column happens to be pinned. **The default reproduces today's behaviour on purpose:** flipping it would move the label in every consumer's grouped grid without anyone asking, so a grid that does not pass this prop renders exactly what it rendered before. **A closed union of two forms, never a column id** — a free id would be an unverifiable `string`, and pointing it at a column that does not exist (or is hidden) would do nothing and say nothing, leaving the grid on the default while the author believes they moved it. **It hides no column, and that is why the prop exists:** moving the label left used to require *hiding* the grouping column — a group row falls back to the first visible column when its own is gone — which drops the rendered leaves by one and makes the «fourteen columns» clause of the complete scene false. `"first-column"` unhooks PLACEMENT from VISIBILITY: every column stays, and the grouping column's cell goes back to the common rule — its own aggregate if it declares one, otherwise empty (R11: the grouping value appears once, and it is the label that says it). **Server variant: `never`**, next to `groupBy` and `initialOpenGroups` — a grid that does not group has no group label to place, and without the twin this prop would be a silent no-op instead of a type error. |
74
74
  | `error` | `boolean` | `false` | **Fase 5.** The request FELL: the data exists and did not arrive. It is the PREDICATE, and `onRetry` is not one — without it a failed fetch and a filter that catches nothing are the same state for the grid (`loading={false}`, `data={[]}`), and the reader is told «no results» where they should be told to retry, so they change the filters instead. Lives on **BOTH branches** of the union. It **wins over the empty outcome** (a failed request has zero rows, so both predicates would be true) and, like the empty one, does **not** show while `loading`: an error from the previous attempt above the skeletons of the current one describes the past. |
@@ -385,8 +385,8 @@ Stable hooks, independent of the i18n language:
385
385
  | `[data-grid-filter-cell="<id>"]` | A cell of the zero row — one per leaf column plus `"__select"` when `checkable`, in the order of the columns, **never** with `colSpan`. Carries `data-grid-filter-cell-label` (the column's name) and `data-grid-filter-cell-applied="true"\|"false"`. Until `adg-r14` these were also the nodes the «N filters out of view» count was measured on; that indicator has been REMOVED from the toolbar and nothing reads their rects any more |
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
- | `[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. **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`) |
388
+ | `[data-grid-filter-cell-panel="<id>"]` | The popover of a filter cell. Portaled **inside `[data-app-data-grid]`** (since 2026-09-23; it was `document.body`), together with the operator list and every list / calendar of the editor inside it: at full screen a body-level panel at `z-[60]` opened BEHIND the `fixed` root at z 110. Look for it in the `document`, not under the cell. 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. **From 2026-09-23** a DS `Button` `variant="ghost"`, and `toolbar.filterRowToggle: false` removes it — and with it the ONLY way back, since the row's open state is local to the grid (there is no `filterRowOpen` prop; `onFilterRowOpenChange` only announces). To put your own command there, pass `toolbar.filterRowToggle: { render: ({ open, toggle }) => … }`: `open` is whether the row is mounted, `toggle` is the grid's own gesture (it flips the state and fires `onFilterRowOpenChange` once) |
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
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 |
@@ -450,10 +450,10 @@ Stable hooks, independent of the i18n language:
450
450
  | `[data-grid-filter-editor-placeholder]` | **P7.** The empty `<option>` of a select editor. Enumerate a column's choices as `select.querySelectorAll("option:not([data-grid-filter-editor-placeholder])")` |
451
451
  | `[data-grid-filter-editor-unknown]` | **AUDIT 2026-09-09.** The synthetic `<option>` a list editor renders when the badge's value is NOT among the options the column declares. Without it the `<select>` fell back to the placeholder while the filter was active, so an applied filter looked empty. Enumerating a column's DECLARED choices therefore excludes it too: `select.querySelectorAll("option:not([data-grid-filter-editor-placeholder]):not([data-grid-filter-editor-unknown])")` |
452
452
  | `[data-grid-filter-editor-empty]` | **P7.** A list editor whose column declares ZERO options. It is an INCOMPLETE DECLARATION and is marked as such — never a reason to fall back to a free text field, which would put back the very defect the phase repairs |
453
- | `[data-grid-filter-composer="&lt;columnId&gt;"]` | **P7.** The «from … to …» composer (a `PopoverContent`, portaled — look for it in the `document`, not in `canvasElement`). Its draft is LOCAL: `Escape`, «Cancel» and a click outside throw it away without touching the state or the emission counter; `[data-grid-filter-composer-apply="&lt;columnId&gt;"]` is the only gesture that promotes it. Also carries `[data-grid-filter-composer-title]`, `[data-grid-filter-composer-side="from\|to"]` (whose editors are named `&lt;columnId&gt;-from` / `-to`) and `[data-grid-filter-composer-cancel="&lt;columnId&gt;"]` |
453
+ | `[data-grid-filter-composer="&lt;columnId&gt;"]` | **P7.** The «from … to …» composer (a `PopoverContent`, portaled into the grid root that contains its anchor, or `<body>` outside a grid — look for it in the `document`, not under the anchor). Its draft is LOCAL: `Escape`, «Cancel» and a click outside throw it away without touching the state or the emission counter; `[data-grid-filter-composer-apply="&lt;columnId&gt;"]` is the only gesture that promotes it. Also carries `[data-grid-filter-composer-title]`, `[data-grid-filter-composer-side="from\|to"]` (whose editors are named `&lt;columnId&gt;-from` / `-to`) and `[data-grid-filter-composer-cancel="&lt;columnId&gt;"]` |
454
454
  | `[data-grid-filter-chip-trigger="&lt;columnId&gt;"]` | **P7 — the chip is now ONE target.** The pill's `<button>`; accessible name «Condition on &lt;column&gt;: &lt;operator&gt; &lt;value&gt;», short side ≥ 44px. It also carries `data-grid-filter-chip-operator="&lt;operator&gt;"`, the operator id declared in the DOM. ⚠️ Inside `[data-grid-filter-chip]` there is **exactly one** focusable node — count them, do not query the accessible name: with the old `✕` still there the name query stays green because the `✕` is called something else |
455
- | `[data-grid-filter-chip-edit="&lt;columnId&gt;"]` | **P7.** The «Edit value…» menu item; opens the composer through `onEditValue`, `disabled` without it. ⚠️ **Portaled**: look for it in the `document` |
456
- | `[data-grid-filter-chip-remove="&lt;columnId&gt;"]` | **P7 — same name, different node.** No longer the `✕` in the canvas: it is the «Remove» menu item, `data-variant="destructive"`, separated from «Edit value…» by a `[role="separator"]`. ⚠️ **Portaled** out of `canvasElement`. Both items are ≥ 44px on the short side |
455
+ | `[data-grid-filter-chip-edit="&lt;columnId&gt;"]` | **P7.** The «Edit value…» menu item; opens the composer through `onEditValue`, `disabled` without it. ⚠️ **Portaled** — into the grid root when the chips sit inside a grid (since 2026-09-23), otherwise `<body>`: look for it in the `document` |
456
+ | `[data-grid-filter-chip-remove="&lt;columnId&gt;"]` | **P7 — same name, different node.** No longer the `✕` in the canvas: it is the «Remove» menu item, `data-variant="destructive"`, separated from «Edit value…» by a `[role="separator"]`. ⚠️ **Portaled** (same rule as the item above). Both items are ≥ 44px on the short side |
457
457
  | `[data-grid-header-filtered="&lt;columnId&gt;"]` | **P7.** The glyph on the header of a CONSTRAINED column — a funnel `role="img"` whose accessible name is `filters.columnFiltered`. **Shape plus name FIRST, colour added** (WCAG 1.4.1): the discriminator is still the funnel outline no other header carries, plus the accessible name; the accent tint (`text-d-primary`) is added to those, not put in their place, so a reader who does not distinguish the hue still reads the shape. A probe must key on `[data-grid-header-filtered]`, never on the colour — the token is consumer-overridable. ⚠️ **Measured price:** in the light theme `--d-primary` resolves `#64c2d1` on `#fafafa` = 1.86:1, under the 3:1 WCAG 1.4.11 asks of a graphical object, and paler than the `#7a7a7a` (3.94:1) the glyph had by inheritance. It holds only because colour carries no state here; a theme wanting 3:1 on the glyph moves `--d-primary` one step darker (measured: `#3e8b97` = 3.54:1). It is not a button: the filter is removed and edited from its own pill. It is driven by the SAME list the filter count reads, so «filtered» in the header and the chip that names it cannot diverge |
458
458
  | `[data-grid-selection-bar]` | **Fase 6.** The page-selection bar — `role="group"` + `aria-labelledby` pointing at the visible count, **never `role="status"`** (the mockup's `CompleteSelectionBar` carries `role="status" aria-live="polite"`; the component does NOT, because the invariant is ONE live region per grid — `UnaSolaRegioneLive` counts them). **In flow ABOVE the scroll area since 2026-09-03**, between the filter band and the table band; its wire is therefore `gridStickyHeaderShadow` (downward, `0 1px`) and no longer the footer's upward one. ⚠️ **The node does not exist while nothing is selected** — not "exists with height 0": the frame is a `flex flex-col gap-2`, so an always-rendered wrapper would still cost 8px of gap, i.e. one data row for ever. Its height comes from `gridRowHeightClass[density]`, the same map the `selectionBar` addend of `gridFrameHeight` reads. **See: "Declared limit — in fullscreen the band is paid by the viewport"** below — the no-lost-row promise holds for a bounded frame, and in fullscreen it costs one row by decision |
459
459
  | `[data-grid-selection-count]` | **Fase 6.** The visible count — «2 rows selected». Carries the `id` the bar's `aria-labelledby` points at, and the number as its attribute value. The figure is `engine.selectedPageRowIds.length`, i.e. the ticked ids the **current row model** still knows — never `Object.keys(rowSelection).length`, which survives paging and filtering. ⚠️ **From M4 it holds the count ALONE** — «2 rows selected» — because the scope moved into the sibling `[data-grid-selection-scope]`; it is also where the bar's typography starts, `text-sm font-medium` (14px / 500) as in the mockup |
@@ -1131,9 +1131,10 @@ centre: the grid was on top. The menu now takes an inline z from that token whil
1131
1131
 
1132
1132
  The z-index that decides is the one on **Radix's popper wrapper**, not on the menu content: the wrapper is the
1133
1133
  stacking context. Radix copies the content's computed z-index onto the wrapper at mount, which is why an
1134
- inline z on the content reaches it — and why raising the content's z _after_ mount does nothing. `DropdownMenuContent`
1135
- hard-codes its Portal with no `container` passthrough, and that file is shared surface the frozen `DataTable`
1136
- renders, so this lane raises its own overlay from its own side rather than changing the primitive.
1134
+ inline z on the content reaches it — and why raising the content's z _after_ mount does nothing. The row menu keeps
1135
+ that inline z. Since 2026-09-23 `DropdownMenuContent` takes an additive `container` (default `<body>`), and the
1136
+ filter chips' menus use it to mount inside the grid root instead — the same way as the filter cell panel, the
1137
+ columns panel and the actions panel.
1137
1138
 
1138
1139
  ⚠️ **The density buttons' pressed state inverts fill and text**, rather than using `bg-d-muted`. In the shipped
1139
1140
  theme `--d-muted` and `--d-background` resolve to the **same** colour, so the pressed fill measured **1.00:1**
@@ -22,6 +22,7 @@ Date picker component that supports both **single date** and **date range** sele
22
22
  | `disabled` | `boolean` | `false` | Disables interactions and the popover. |
23
23
  | `size` | `"sm" \| "default" \| "lg"` | `"default"` | Trigger size affecting height and icon size. |
24
24
  | `clearable` | `boolean` | `false` | Show a clear (X) button to reset the selection. |
25
+ | `portalContainer` | `HTMLElement \| null` | `undefined` (`<body>`) | Element the calendar popover portals into. Use it when the picker lives inside a container with its own stacking context (e.g. a fixed full-screen grid), where a `<body>` portal would open behind it. |
25
26
  | `onClear` | `() => void` | `undefined` | Callback fired when the value is cleared via the clear button. |
26
27
  | `minDate` | `Date` | `undefined` | Minimum selectable date. Dates before this are disabled. |
27
28
  | `maxDate` | `Date` | `undefined` | Maximum selectable date. Dates after this are disabled. |
@@ -17,6 +17,8 @@ Contextual menu built on Radix Dropdown Menu with support for submenus, checkbox
17
17
 
18
18
  All components extend Radix props.
19
19
 
20
+ `DropdownMenuContent` also takes `container?: HTMLElement | null` — the element the menu portals into (default: `<body>`). Use it when the menu lives inside a container with its own stacking context (e.g. a fixed full-screen grid), where a `<body>` portal would open behind it.
21
+
20
22
  ---
21
23
 
22
24
  ## Example
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "schemaVersion": "1.1.0",
3
- "generatedAt": "2026-09-23T08:34:27.980Z",
3
+ "generatedAt": "2026-09-23T10:54:09.944Z",
4
4
  "package": {
5
5
  "name": "laif-ds",
6
- "version": "1.0.2-app-data-grid.82"
6
+ "version": "1.0.2-app-data-grid.88"
7
7
  },
8
8
  "stats": {
9
9
  "documentedComponentCount": 97,
@@ -3025,10 +3025,10 @@
3025
3025
  },
3026
3026
  "metadata": {
3027
3027
  "props": {
3028
- "totalProps": 31,
3028
+ "totalProps": 32,
3029
3029
  "requiredPropsCount": 0,
3030
- "typedPropsCount": 31,
3031
- "describedPropsCount": 31
3030
+ "typedPropsCount": 32,
3031
+ "describedPropsCount": 32
3032
3032
  },
3033
3033
  "accessibility": {
3034
3034
  "hasCoverage": true
@@ -6549,5 +6549,5 @@
6549
6549
  ]
6550
6550
  }
6551
6551
  ],
6552
- "checksum": "186b8095a19a840494aa8a707120966d122e1c00a35cb9a87ff2ff4e3e31c516"
6552
+ "checksum": "16268e4942256ecceb9cc962078a1687eb89049b0da3a1515f60c492c89bc52d"
6553
6553
  }