laif-ds 1.0.5 → 1.1.0

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 (93) hide show
  1. package/dist/CHANGELOG.md +23 -0
  2. package/dist/_virtual/index2.js +2 -5
  3. package/dist/_virtual/index3.js +5 -2
  4. package/dist/_virtual/index4.js +5 -5
  5. package/dist/_virtual/index5.js +5 -5
  6. package/dist/_virtual/index6.js +2 -2
  7. package/dist/_virtual/index7.js +2 -2
  8. package/dist/agent-docs/adoption-report.json +165 -39
  9. package/dist/agent-docs/components/AppCard.md +25 -21
  10. package/dist/agent-docs/components/AppDataGrid/columns.md +113 -0
  11. package/dist/agent-docs/components/AppDataGrid/dom-hooks.md +75 -0
  12. package/dist/agent-docs/components/AppDataGrid/examples.md +129 -0
  13. package/dist/agent-docs/components/AppDataGrid/filtering.md +126 -0
  14. package/dist/agent-docs/components/AppDataGrid/grouping.md +20 -0
  15. package/dist/agent-docs/components/AppDataGrid/i18n-and-accessibility.md +40 -0
  16. package/dist/agent-docs/components/AppDataGrid/layout.md +45 -0
  17. package/dist/agent-docs/components/AppDataGrid/migration-from-datatable.md +32 -0
  18. package/dist/agent-docs/components/AppDataGrid/saved-views.md +76 -0
  19. package/dist/agent-docs/components/AppDataGrid/selection.md +44 -0
  20. package/dist/agent-docs/components/AppDataGrid/server-and-pagination.md +73 -0
  21. package/dist/agent-docs/components/AppDataGrid/toolbar.md +28 -0
  22. package/dist/agent-docs/components/AppDataGrid.md +278 -0
  23. package/dist/agent-docs/components/AppEditor.md +18 -13
  24. package/dist/agent-docs/components/AppSelect.md +51 -0
  25. package/dist/agent-docs/components/DataTable.md +40 -3
  26. package/dist/agent-docs/components/DatePicker.md +8 -7
  27. package/dist/agent-docs/components/DropdownMenu.md +2 -0
  28. package/dist/agent-docs/components/FileUploader.md +12 -12
  29. package/dist/agent-docs/components/GridFilterPanel.md +128 -0
  30. package/dist/agent-docs/components/SidebarMenuBadge.md +87 -0
  31. package/dist/agent-docs/components-list.md +16 -1
  32. package/dist/agent-docs/manifest.json +253 -30
  33. package/dist/components/design-tokens.js +138 -2
  34. package/dist/components/ui/app-data-grid/app-data-grid.js +900 -0
  35. package/dist/components/ui/app-data-grid/engine/use-grid-column-resize.js +110 -0
  36. package/dist/components/ui/app-data-grid/engine/use-grid-engine.js +621 -0
  37. package/dist/components/ui/app-data-grid/engine/use-grid-infinite-loader.js +50 -0
  38. package/dist/components/ui/app-data-grid/engine/use-grid-virtualizer.js +90 -0
  39. package/dist/components/ui/app-data-grid/grid-aggregate.js +5 -0
  40. package/dist/components/ui/app-data-grid/grid-column-registry.js +28 -0
  41. package/dist/components/ui/app-data-grid/grid-columns.js +204 -0
  42. package/dist/components/ui/app-data-grid/grid-filter-client.js +73 -0
  43. package/dist/components/ui/app-data-grid/grid-filter-compile.js +33 -0
  44. package/dist/components/ui/app-data-grid/grid-filter-operators.js +141 -0
  45. package/dist/components/ui/app-data-grid/grid-grouping.js +104 -0
  46. package/dist/components/ui/app-data-grid/grid-header-matrix.js +112 -0
  47. package/dist/components/ui/app-data-grid/grid-i18n.js +208 -0
  48. package/dist/components/ui/app-data-grid/grid-mount-count.js +8 -0
  49. package/dist/components/ui/app-data-grid/grid-reducer.js +202 -0
  50. package/dist/components/ui/app-data-grid/grid-result-scope.js +35 -0
  51. package/dist/components/ui/app-data-grid/grid-tokens.js +169 -0
  52. package/dist/components/ui/app-data-grid/grid-view-preset.js +97 -0
  53. package/dist/components/ui/app-data-grid/parts/grid-actions-row.js +86 -0
  54. package/dist/components/ui/app-data-grid/parts/grid-cell-text.js +67 -0
  55. package/dist/components/ui/app-data-grid/parts/grid-column-registry-panel.js +320 -0
  56. package/dist/components/ui/app-data-grid/parts/grid-density-control.js +88 -0
  57. package/dist/components/ui/app-data-grid/parts/grid-filter-chips.js +234 -0
  58. package/dist/components/ui/app-data-grid/parts/grid-filter-editor.js +201 -0
  59. package/dist/components/ui/app-data-grid/parts/grid-filter-panel.js +397 -0
  60. package/dist/components/ui/app-data-grid/parts/grid-filter-row.js +277 -0
  61. package/dist/components/ui/app-data-grid/parts/grid-fullscreen-toggle.js +28 -0
  62. package/dist/components/ui/app-data-grid/parts/grid-group-row.js +135 -0
  63. package/dist/components/ui/app-data-grid/parts/grid-header.js +337 -0
  64. package/dist/components/ui/app-data-grid/parts/grid-pagination-control.js +238 -0
  65. package/dist/components/ui/app-data-grid/parts/grid-portal-container.js +14 -0
  66. package/dist/components/ui/app-data-grid/parts/grid-row.js +181 -0
  67. package/dist/components/ui/app-data-grid/parts/grid-scroll-area.js +185 -0
  68. package/dist/components/ui/app-data-grid/parts/grid-selection-bar.js +195 -0
  69. package/dist/components/ui/app-data-grid/parts/grid-status-surface.js +66 -0
  70. package/dist/components/ui/app-data-grid/parts/grid-table.js +482 -0
  71. package/dist/components/ui/app-data-grid/parts/grid-toolbar-slots.js +224 -0
  72. package/dist/components/ui/app-data-grid/use-grid-filters.js +183 -0
  73. package/dist/components/ui/app-select.js +334 -222
  74. package/dist/components/ui/button.js +7 -6
  75. package/dist/components/ui/date-picker.js +36 -34
  76. package/dist/components/ui/dropdown-menu.js +3 -3
  77. package/dist/components/ui/tables/data-cross-table/data-cross-table.js +6 -6
  78. package/dist/components/ui/tables/data-table/data-table-constants.js +36 -16
  79. package/dist/components/ui/tables/data-table/data-table-i18n.js +135 -1
  80. package/dist/css-for-template.css +26 -0
  81. package/dist/index.d.ts +5957 -124
  82. package/dist/index.js +462 -395
  83. package/dist/node_modules/eventemitter3/index.js +1 -1
  84. package/dist/node_modules/eventemitter3/index2.js +1 -1
  85. package/dist/node_modules/hast-util-to-jsx-runtime/lib/index.js +1 -1
  86. package/dist/node_modules/recharts/es6/util/Events.js +1 -1
  87. package/dist/node_modules/style-to-object/cjs/index.js +1 -1
  88. package/dist/node_modules/unified/lib/index.js +1 -1
  89. package/dist/node_modules/use-sync-external-store/shim/index.js +1 -1
  90. package/dist/styles.css +2 -1
  91. package/dist/styles.v3.css +1 -1
  92. package/package.json +13 -6
  93. /package/dist/agent-docs/components/{truncated-cell.md → TruncatedCell.md} +0 -0
@@ -0,0 +1,75 @@
1
+ # AppDataGrid — DOM hooks
2
+
3
+ > Part of the [AppDataGrid](../AppDataGrid.md) docs. The Props table and the `onQueryChange` payload are in the main file.
4
+
5
+ ## DOM hooks (for tests)
6
+
7
+ Stable attributes, independent of the i18n language.
8
+
9
+ | Selector / attribute | Meaning |
10
+ | ------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
11
+ | `[data-app-data-grid]` | Grid root. |
12
+ | `data-emissions` (root) | Query emissions: `0` at mount, +1 per emission, unchanged by view actions. Debounced: read in `waitFor`. |
13
+ | `data-render-count` (root) | React commits of the root since mount. |
14
+ | `data-mount-count` (root) | Mounts of a grid with this `id` / `data-testid` (stays `1` across full-screen toggles). |
15
+ | `data-loadmore-call-count` (root) | Load-more requests issued (infinite mode). |
16
+ | `data-grid-fullscreen` (root) | `"true"` / `"false"`. |
17
+ | `[data-grid-toolbar]` | Toolbar. |
18
+ | `[data-grid-search]` | Search input. |
19
+ | `[data-grid-viewport]` | Scroll container; its `clientHeight` equals `viewportHeight`. |
20
+ | `[data-grid-table]` | The `<table>`. |
21
+ | `[data-grid-col="<id>"]` | `<col>` of a column — count columns with this (the extra layout col is `[data-grid-col-filler]`). |
22
+ | `[data-grid-header="<id>"]` | Leaf `<th>` (`aria-sort`). |
23
+ | `[data-grid-header-group="<id>"]` | Group `<th>` (`scope="colgroup"`). |
24
+ | `[data-grid-header-row="<n>"]` | Header `<tr>`, 0-based. |
25
+ | `[data-grid-sort="<id>"]` | Sort button. |
26
+ | `[data-grid-sort-index]` | Multi-sort priority badge. |
27
+ | `[data-grid-resize-handle="<id>"]` | Resize handle (`aria-valuenow` = width). |
28
+ | `[data-grid-row]` | Data row, never a spacer. Use `querySelector` for the first one (the first `<tr>` may be a spacer). |
29
+ | `[data-grid-cell="<id>"]` | Body cell. |
30
+ | `[data-grid-row-header]` | Row-header cell (`<th scope="row">`). |
31
+ | `[data-grid-spacer]` | Virtualization spacer rows. |
32
+ | `[data-grid-skeleton-row]` | Skeleton row (`aria-hidden`). |
33
+ | `[data-grid-pinned="true"]` | Pinned cell (either side). |
34
+ | `[data-grid-cell-text]` | Text cell that fits. |
35
+ | `[data-grid-cell-expand]` | Focusable target of a truncated text cell. |
36
+ | `[data-grid-row-menu]` | Row-menu trigger. |
37
+ | `[data-grid-row-menu-item="<id>"]` | Row-menu item (`data-variant="destructive"` when destructive). |
38
+ | `[data-grid-live-region]` | The single status region. |
39
+ | `[data-grid-empty]` | Empty-outcome cell. |
40
+ | `[data-grid-error]` | Error-outcome cell. |
41
+ | `[data-grid-status-cell]` | The outcome cell, empty or error. |
42
+ | `[data-grid-retry]` | Retry button (absent without `onRetry`). |
43
+ | `[data-grid-footer]` | Totals `<tfoot>`. |
44
+ | `[data-grid-footer-cell="<id>"]` | Footer cell. |
45
+ | `[data-grid-pagination]` | Paginator. |
46
+ | `[data-grid-page-range]` | «Rows x-y of N». |
47
+ | `[data-grid-page-number="<n>"]` | Page button (1-based; current one has `aria-current="page"`). |
48
+ | `[data-grid-page-size]` | Wrapper of the page-size selector. |
49
+ | `tr[data-grid-filter-row]` | Filter row (absent while hidden). |
50
+ | `[data-grid-filter-cell="<id>"]` | Filter cell. |
51
+ | `[data-grid-filter-cell-trigger="<id>"]` | Filter cell's button. |
52
+ | `[data-grid-header-filtered="<id>"]` | Funnel on a filtered column's header. |
53
+ | `[data-grid-toolbar-filter-row-toggle]` | Filter-row toggle. |
54
+ | `[data-grid-toolbar-columns]` | Columns-panel trigger. |
55
+ | `[data-grid-fullscreen-toggle]` | Full-screen toggle. |
56
+ | `[data-grid-actions-row]` | Container of the `actions` prop buttons. |
57
+ | `[data-grid-action="<label>"]` | One `actions` command. |
58
+ | `[data-grid-actions-trigger]` | Collapsed trigger (two or more actions). |
59
+ | `[data-grid-group-row="<path>"]` | Group row (`aria-expanded`). |
60
+ | `[data-grid-group-toggle="<path>"]` | Group expand/collapse control. |
61
+ | `[data-grid-group-count]` | Group row count — read the attribute (the text has parentheses). |
62
+ | `[data-grid-group-select="<path>"]` | Group checkbox. |
63
+ | `[data-grid-select-all]` | Header checkbox. |
64
+ | `[data-grid-selection-bar]` | Selection bar. |
65
+ | `[data-grid-selection-count]` | Selected count. |
66
+ | `[data-grid-selection-action="<label>"]` | Bulk action. |
67
+ | `[data-grid-toolbar-custom-filters]` | Toolbar group of custom filters. |
68
+ | `[data-grid-toolbar-custom-filter="<id>"]` | One custom filter (by badge id). |
69
+ | `[data-grid-filter-chip-trigger="<id>"]` | A chip in `GridFilterChips`. |
70
+ | `[data-grid-filter-add-chip]` | «+ Filter» chip. |
71
+ | `[data-grid-filter-scope]` | Chips' scope clause. |
72
+ | `[data-grid-column-registry-item="<id>"]` | Columns-panel row. |
73
+ | `[data-grid-column-registry-toggle="<id>"]` | Columns-panel visibility checkbox. |
74
+
75
+ ---
@@ -0,0 +1,129 @@
1
+ # AppDataGrid — Examples
2
+
3
+ > Part of the [AppDataGrid](../AppDataGrid.md) docs. The Props table and the `onQueryChange` payload are in the main file.
4
+
5
+ ## Examples
6
+
7
+ ### Server grid with `onQueryChange`
8
+
9
+ ```tsx
10
+ import { useState } from "react";
11
+ import {
12
+ AppDataGrid,
13
+ type GridColumnNode,
14
+ type GridServerState,
15
+ } from "laif-ds";
16
+
17
+ const initialQuery: GridServerState = {
18
+ pagination: { pageIndex: 0, pageSize: 25 },
19
+ };
20
+
21
+ export const UsersGrid = ({ columns }: { columns: GridColumnNode<User>[] }) => {
22
+ // The grid does not emit at mount: the initial query is fetched by the consumer.
23
+ const [query, setQuery] = useState<GridServerState>(initialQuery);
24
+ const { data, isLoading, isError, refetch } = useUsersSearch(query); // your data hook
25
+
26
+ return (
27
+ <AppDataGrid
28
+ columns={columns}
29
+ data={data?.items ?? []}
30
+ getRowId={(row) => String(row.id)}
31
+ serverMode
32
+ paginated
33
+ serverConfig={{ totalItems: data?.total ?? 0, onStateChange: setQuery }}
34
+ loading={isLoading}
35
+ error={isError}
36
+ onRetry={() => void refetch()}
37
+ />
38
+ );
39
+ };
40
+ // In useUsersSearch, send query.computedFilter, query.computedSorting and query.pagination.
41
+ ```
42
+
43
+ ### Grouping with aggregates
44
+
45
+ ```tsx
46
+ const columns: GridColumnNode<Sale>[] = [
47
+ createGridStringColumn<Sale>({ accessorKey: "country", header: "Country" }),
48
+ createGridStringColumn<Sale>({ accessorKey: "city", header: "City" }),
49
+ createGridNumberColumn<Sale, number>({
50
+ accessorKey: "amount",
51
+ header: "Amount",
52
+ align: "right",
53
+ aggregatedCell: (rows) => rows.reduce((sum, r) => sum + r.amount, 0),
54
+ aggregate: (rows) => rows.reduce((sum, r) => sum + r.amount, 0),
55
+ }),
56
+ ];
57
+
58
+ <AppDataGrid
59
+ columns={columns}
60
+ data={sales}
61
+ groupBy={["country", "city"]}
62
+ initialOpenGroups="first"
63
+ groupLabelPlacement="first-column"
64
+ viewportHeight={520}
65
+ />;
66
+ ```
67
+
68
+ ### Lifted filters with chips and the filter row
69
+
70
+ ```tsx
71
+ import { useMemo, useState } from "react";
72
+ import {
73
+ AppDataGrid,
74
+ GridFilterChips,
75
+ gridResultScope,
76
+ gridViewportHeight,
77
+ toGridFilterColumns,
78
+ useGridFilters,
79
+ } from "laif-ds";
80
+
81
+ export const OrdersPage = ({ orders }: { orders: Order[] }) => {
82
+ const filters = useGridFilters({ columns: toGridFilterColumns(columns) });
83
+ const [filterRowOpen, setFilterRowOpen] = useState(true); // the row starts open
84
+
85
+ // With lifted filters the consumer filters the rows (client) or sends
86
+ // filters.computedFilter to the server.
87
+ const visibleOrders = useMemo(
88
+ () => applyMyFilters(orders, filters.committedBadges),
89
+ [orders, filters.committedBadges],
90
+ );
91
+
92
+ return (
93
+ <>
94
+ <GridFilterChips
95
+ badges={filters.badges}
96
+ onRemove={filters.removeBadge}
97
+ onClearAll={filters.clearBadges}
98
+ scope={gridResultScope({
99
+ badges: filters.badges,
100
+ resultTotal: visibleOrders.length,
101
+ })}
102
+ />
103
+ <AppDataGrid
104
+ columns={columns}
105
+ data={visibleOrders}
106
+ resultTotal={visibleOrders.length}
107
+ filters={filters.badges}
108
+ columnVisibility={filters.columnVisibility}
109
+ onColumnVisibilityChange={(next) => {
110
+ for (const [id, visible] of Object.entries(next))
111
+ if (filters.columnVisibility[id] !== visible)
112
+ filters.setColumnVisible(id, visible);
113
+ }}
114
+ onAddFilter={filters.addBadge}
115
+ onUpdateFilter={filters.updateBadge}
116
+ onRemoveFilter={filters.removeBadge}
117
+ onFilterRowOpenChange={setFilterRowOpen}
118
+ viewportHeight={gridViewportHeight({
119
+ rowCount: 10,
120
+ density: "default",
121
+ hasFilterRow: filterRowOpen,
122
+ })}
123
+ />
124
+ </>
125
+ );
126
+ };
127
+ ```
128
+
129
+ ---
@@ -0,0 +1,126 @@
1
+ # AppDataGrid — Filtering
2
+
3
+ > Part of the [AppDataGrid](../AppDataGrid.md) docs. The Props table and the `onQueryChange` payload are in the main file.
4
+
5
+ ## Filtering
6
+
7
+ ### Global search
8
+
9
+ The search field filters rows (client) or feeds `filters.searchbarFilter` and `computedFilter` (server).
10
+ Only columns with `searchable: true` are matched. Keystrokes are debounced by `debounceTime`.
11
+
12
+ ### Filter row (default, `filterMode="row"`)
13
+
14
+ A row inside `<thead>` with one filter cell per column (plus the checkbox column). Each cell is a button
15
+ that opens an editor popover; the editor and its operators are derived from the column's `filterType`:
16
+ text field, number field, boolean, date / date-time picker, single or multi list (from `options`). A
17
+ list column without options is shown as an incomplete declaration, never as a free text field. Cells of
18
+ filtered columns get an accent border; filtered headers show a funnel icon. The toolbar toggle unmounts
19
+ and remounts the row.
20
+
21
+ Without lifted `filters`, the grid owns the filter state and (on the client branch) filters `data`
22
+ itself. Client-side matching reproduces the backend operators (`like`, `starts_with`, `eq`,
23
+ `array_overlap`, `date_after`, …), with edge-case differences a client cannot replicate (database
24
+ collation/case folding of non-ASCII text, `citext` columns, decimal precision). `%` and `_` in text
25
+ filters behave as `LIKE` wildcards, as on the server.
26
+
27
+ Operators (`GridFilterOperator`): `like`, `n_like`, `starts_with`, `ends_with`, `eq`, `ne`, `eq_null`,
28
+ `n_eq_null`, `gt`, `ge`, `lt`, `le`, `checked`, `unchecked`, `date_after`, `date_before`,
29
+ `date_time_after`, `date_time_before`, `array_overlap`, `n_array_overlap`. A range is expressed as two
30
+ badges (e.g. `ge` + `le`).
31
+
32
+ ### Lifted filters (`useGridFilters`)
33
+
34
+ Lift the filter state when chips, counts or a server fetch live outside the grid. The hook owns the
35
+ badges; the grid, `GridFilterChips` and `GridFilterPanel` read/write the same store, and the chips work
36
+ without a grid in the tree.
37
+
38
+ ```ts
39
+ const filters = useGridFilters({
40
+ columns: toGridFilterColumns(leafColumns), // required, for pruning
41
+ initialBadges, // optional
42
+ initialColumnVisibility, // optional
43
+ debounceTime, // optional, default 200
44
+ onFiltersChange: (computedFilter) => refetch(computedFilter), // optional
45
+ });
46
+ ```
47
+
48
+ `GridFiltersApi`: `badges` (live, feed the grid and editors), `committedBadges` (debounced, use them to
49
+ filter rows), `computedFilter`, `columnVisibility`, `panelOpen`, `emissions`, `addBadge`, `addBadges`
50
+ (several in one mutation — use for ranges), `updateBadge`, `removeBadge`, `removeBadges`, `clearBadges`,
51
+ `restore(badges, columnVisibility)`, `setColumnVisible(id, visible)`, `setPanelOpen(open)`.
52
+
53
+ Rules when `filters` is passed to the grid:
54
+
55
+ - **You filter the rows.** The grid does not apply lifted badges to `data`. On the client, filter with
56
+ `committedBadges`; on the server, send `filters.computedFilter` (or the value received by
57
+ `onFiltersChange`).
58
+ - **The grid does not emit for them** (except in infinite mode, below). `onFiltersChange` is called
59
+ synchronously inside the hook's actions (keystrokes coalesced by the hook's `debounceTime`; adding,
60
+ removing and restoring are immediate), and only when the payload actually changes.
61
+ - **Pagination resets to page 0** when the badges' content changes (compared by content, so an inline
62
+ `filters={badges.filter(...)}` is fine).
63
+ - Wire `onAddFilter={filters.addBadge}`, `onUpdateFilter={filters.updateBadge}` and
64
+ `onRemoveFilter={filters.removeBadge}`; pass `columnVisibility={filters.columnVisibility}` with
65
+ `onColumnVisibilityChange` so hidden columns also prune their badges.
66
+
67
+ #### Infinite mode restarts on its own
68
+
69
+ With `infinite` on (and `paginated` off), a change in the content of lifted `filters` makes the grid
70
+ emit **one** `onQueryChange` with `pageIndex: 0`; later load-mores ask for 1, 2, …. Keystrokes into an
71
+ existing badge coalesce into one restart. It never fires at mount, and a change that matches the last
72
+ emission (e.g. the `restore()` after a preset) does not restart. Recommended wiring in infinite mode:
73
+ do **not** also refetch in `onFiltersChange`; if you do, treat `pageIndex: 0` as a replace and drop
74
+ superseded responses.
75
+
76
+ ### Filter chips, panel and columns panel
77
+
78
+ - `GridFilterChips` — `{ badges, onRemove, onClearAll?, scope?, onAddFilter?, onEditValue?, i18n?,
79
+ className? }`. A row of chips (one button per badge with an edit/remove menu), an optional dashed «+
80
+ Filter» chip, and a scope clause («128 rows · 2 filters») when `scope` is passed. Build `scope` with
81
+ `gridResultScope({ badges, resultTotal, rowsInPage?, pageIndex?, pageSize?, locale? })`.
82
+ - `GridFilterPanel` — the filter inventory overlay used by `filterMode="panel"`; see
83
+ [GridFilterPanel](./GridFilterPanel.md). Mount it inside the grid through `toolbar.filters.render`,
84
+ otherwise it is covered in full screen.
85
+ - `GridColumnRegistryPanel` — the standalone columns panel (`columns`, `columnVisibility`,
86
+ `onColumnVisibleChange`, `columnOrder?`, `onColumnOrderChange?`, `i18n?`, `className?`). The grid
87
+ already renders one behind `toolbar.columns`.
88
+
89
+ ### Custom filters (without a column)
90
+
91
+ A filter that cites no column («only favourites», «older than N days») is declared once as a
92
+ `GridFilterControl` and enters the state as a badge with `kind: "custom"`. It is counted, cleared by
93
+ reset, appended to `computedFilter`, and never pruned by column visibility.
94
+
95
+ ```tsx
96
+ import {
97
+ createGridCustomFilterBadge,
98
+ defaultGridI18n,
99
+ type GridFilterControl,
100
+ type GridFilterControlRegistry,
101
+ } from "laif-ds";
102
+
103
+ const olderThan: GridFilterControl = {
104
+ type: "olderThan", // registry key
105
+ Control: ({ value, onChange, label }) => (
106
+ <DaysInput value={value} onChange={onChange} label={label} />
107
+ ), // rendered by YOUR app, not by the grid
108
+ toFragment: (value, ctx) => ({ [ctx.field]: { operator: "ge", value } }), // null = not applied
109
+ describe: (value) => `Older than ${String(value)} days`, // the only text source
110
+ };
111
+ const registry: GridFilterControlRegistry = { olderThan };
112
+
113
+ filters.addBadge(
114
+ createGridCustomFilterBadge(olderThan, 30, defaultGridI18n, "older"),
115
+ );
116
+ // To change the value: rebuild with the SAME id and call filters.updateBadge("older", badge).
117
+ ```
118
+
119
+ `toFragment` and `describe` are both mandatory. `createGridCustomFilterBadge(control, value, i18n, id)`
120
+ is the only constructor; it fills `kind`, `customKey`, `customLabel` and `customFragment`. With lifted
121
+ `filters` and `onRemoveFilter`, the toolbar shows a dashed group with each custom filter's full name and
122
+ a remove button; chips show `customLabel`. Pass `filterControls={registry}` to flag badges whose key is
123
+ not registered. The grid never renders `Control`: custom filters are named and removable in the grid,
124
+ edited where your app creates them.
125
+
126
+ ---
@@ -0,0 +1,20 @@
1
+ # AppDataGrid — Grouping
2
+
3
+ > Part of the [AppDataGrid](../AppDataGrid.md) docs. The Props table and the `onQueryChange` payload are in the main file.
4
+
5
+ ## Grouping
6
+
7
+ Client branch only. Each `groupBy` entry is one level; group rows show label, chevron and count
8
+ (`data-grid-group-count`), and one cell per visible column filled by that column's `aggregatedCell`.
9
+
10
+ - Groups are ordered by first appearance (i.e. by the current sort), not alphabetically.
11
+ - Expanding is view state and never emits. Opening or closing a group above the viewport keeps what you
12
+ are reading in place; closing a group you are reading inside lands on the group row.
13
+ - Hiding the grouping column does not ungroup: the label moves to the first visible column.
14
+ - A grouped grid always has a bounded viewport (`viewportHeight`, default 480).
15
+ - `aria-rowcount` counts the traversable rows — data rows plus the group rows on screen — so it changes
16
+ as groups open and close.
17
+ - The `<tfoot>` total (`aggregate`) sums the data rows, not the group aggregates.
18
+ - Group-row checkboxes select every leaf of the group, even when it is collapsed.
19
+
20
+ ---
@@ -0,0 +1,40 @@
1
+ # AppDataGrid — i18n and accessibility
2
+
3
+ > Part of the [AppDataGrid](../AppDataGrid.md) docs. The Props table and the `onQueryChange` payload are in the main file.
4
+
5
+ ## i18n
6
+
7
+ `GridI18n` is a superset of `DataTableI18n`. Required top-level keys: `search`, `searchPlaceholder`,
8
+ `clearSearch`, `reset`, `notFoundMessage`, `searchTooltipLabel`, `selectAll`, `selectRow`, and the
9
+ sections `filters`, `stringOperators`, `numberOperators`, `booleanOperators`, `dateOperators`,
10
+ `dateTimeOperators`, `listOperators`, `sorting`, `columnVisibility`, `pagination`, `tableActions`.
11
+ Optional sections (fall back to `defaultGridI18n`): `selectColumnHeader`, `columnResize`, `columnOrder`,
12
+ `loading`, `toolbar`, `table`, `grouping`, `selection`, `status`.
13
+
14
+ `table` keys include `label` («Data grid», the `<table>` name), `skipToRows` / `skipToHeader` (bypass
15
+ links), `expandCell`, `rowMenu`, `errorMessage`, `retry`, `totalsLabel`.
16
+
17
+ Always start from the default bag: `i18n={{ ...defaultGridI18n, ...overrides }}`. There is no bundled
18
+ Italian bag; `GRID_IT_LOCALE` (`"it-IT"`) is the locale constant for number formatting in
19
+ `gridResultScope`.
20
+
21
+ ---
22
+
23
+ ## Accessibility
24
+
25
+ - Native table semantics: `aria-sort` on sortable headers; `id`/`headers` chain for multi-level headers;
26
+ `aria-rowcount` / `aria-rowindex` over the traversable body rows (header rows excluded; `-1` while an
27
+ infinite source has more); `aria-colcount`.
28
+ - One row header per row via `rowHeader: true`; `aria-label` on the table from `i18n.table.label`; two
29
+ bypass links («skip to rows», «skip to headers»).
30
+ - **Exactly one live region** (`role="status"`, polite, atomic) announces loading, results, errors, sort,
31
+ filters and selection. Skeleton rows are `aria-hidden`.
32
+ - Truncated text cells get a focusable target with the full value; cells that fit add no tab stop.
33
+ Copying a data row pastes as one spreadsheet row.
34
+ - Full screen: `role="dialog"`, focus contained, Escape exits.
35
+ - Menus and popovers are non-modal and portal inside the grid root when needed (so they stay reachable
36
+ in full screen).
37
+ - Known gaps: column resize has no single-pointer alternative to dragging (WCAG 2.5.7) and the handle's
38
+ pointer target is 8 px wide (2.5.8); infinite loading has no keyboard trigger other than scrolling.
39
+
40
+ ---
@@ -0,0 +1,45 @@
1
+ # AppDataGrid — Layout
2
+
3
+ > Part of the [AppDataGrid](../AppDataGrid.md) docs. The Props table and the `onQueryChange` payload are in the main file.
4
+
5
+ ## Layout: widths, pinning, virtualization, density, full screen
6
+
7
+ - **Widths and resize.** Every leaf header has a resize handle (drag, or arrow keys = 8 px on the
8
+ focused handle; `role="separator"` with `aria-valuenow`). Resizing never emits and commits once per
9
+ gesture (`onColumnWidthsChange`). Columns without a declared `width` fill the container; horizontal
10
+ overflow and scrollbars appear when widths exceed it.
11
+ - **Pinning.** `pinned: "left" | "right"` on columns. Offsets are computed from declared widths, so they
12
+ are correct at first paint. A pinned block is visibly sticky only under horizontal overflow.
13
+ - **Virtualization.** `virtualized` + a bounded viewport renders a window of rows (fewer than 60 `tr`
14
+ for 10,000 rows). Row heights are exact per density; switching density re-measures. Scrolling never
15
+ emits.
16
+ - **Height helpers.** `gridViewportHeight({ rowCount, density, hasFooter?, hasFilterRow?,
17
+ headerLevels? })` returns the scroll-box height to pass as `viewportHeight` (use the live density and
18
+ the filter-row state). `gridFrameHeight({ ...same, hasSelectionBar? })` returns viewport plus in-flow
19
+ bands, for computing a delta — it excludes toolbar, paginator, padding and border.
20
+ `gridFilterRowHeight` is the filter-row addend.
21
+ - **Density.** `density` prop; offer a switch with `GridDensityControl`:
22
+
23
+ ```tsx
24
+ const [density, setDensity] = useState<GridDensity>("default");
25
+ <AppDataGrid
26
+ columns={columns}
27
+ data={rows}
28
+ density={density}
29
+ toolbar={{
30
+ actions: {
31
+ render: () => (
32
+ <GridDensityControl density={density} onChange={setDensity} />
33
+ ),
34
+ },
35
+ }}
36
+ />;
37
+ ```
38
+
39
+ - **Full screen.** The toolbar toggle (or `initialFullscreen`) promotes the same DOM node to
40
+ `fixed inset-0` with a z-index above page overlays; nothing remounts or refetches and scroll position
41
+ is kept. The root becomes `role="dialog"` `aria-modal`, focus cycles inside it, Escape exits.
42
+ `requestFullscreen` is not used. If an ancestor creates a stacking context (`transform`, `filter`),
43
+ full screen renders inside that ancestor's context.
44
+
45
+ ---
@@ -0,0 +1,32 @@
1
+ # AppDataGrid — Migrating from DataTable
2
+
3
+ > Part of the [AppDataGrid](../AppDataGrid.md) docs. The Props table and the `onQueryChange` payload are in the main file.
4
+
5
+ ## Migrating from `DataTable`
6
+
7
+ `DataTable` is deprecated in favour of `AppDataGrid`; both keep working side by side, so migration can
8
+ be done call site by call site. Most exports map by name:
9
+
10
+ | `DataTable` export | `AppDataGrid` export |
11
+ | -------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
12
+ | `DataTable`, `DataTableProps` | `AppDataGrid`, `AppDataGridProps` |
13
+ | `createStringColumn`, `createNumberColumn`, `createBooleanColumn`, `createDateColumn`, `createDateTimeColumn` | `createGridStringColumn`, `createGridNumberColumn`, `createGridBooleanColumn`, `createGridDateColumn`, `createGridDateTimeColumn` |
14
+ | `createSingleSelectColumn`, `createMultiSelectColumn`, `createComputedColumn`, `createActionColumn`, `createDisplayColumn` | `createGridSingleSelectColumn`, `createGridMultiSelectColumn`, `createGridComputedColumn`, `createGridActionColumn`, `createGridDisplayColumn` |
15
+ | `createInitialState`, `createSorting` | `createGridInitialState`, `createGridSorting` |
16
+ | `ServerState`, `DataTableServerConfig`, `DataTableState` / `InitialTableState` | `GridServerState`, `GridServerConfig`, `GridInitialState` |
17
+ | `DataTableI18n`, `defaultDataTableI18n` | `GridI18n`, `defaultGridI18n` |
18
+ | `DataTableBadgeFilter`, `DataTableFilterForSearch`, `DataTableSorting`, `DataTablePagination`, `DataTableActions`, `FilterOperator`, `IColumnType` | `GridBadgeFilter`, `GridFilterForSearch`, `GridSorting`, `GridPagination`, `GridActions`, `GridFilterOperator`, `GridColumnType` |
19
+
20
+ Behaviour differences to check when migrating:
21
+
22
+ - `sortable` and `filterable` default to `true` (were `false`); `searchable` stays opt-in.
23
+ - `hideActionsRow` hides only the `actions` buttons, not the toolbar.
24
+ - `serverDebounceTime` (default 200) coalesces emissions instead of delaying the loading overlay.
25
+ - There is no `mode` prop: the server branch is selected by the server props; grouping is client-only.
26
+ - No emission at mount — perform the initial fetch yourself.
27
+ - Filters are in the header filter row by default; there is no bundled Italian i18n bag.
28
+ - Filter helpers of `DataTable` (`createFilterBadge`, `applyClientSideFilter`, `getPinnedStyle`, …) have
29
+ no public equivalent: filter badges are created by the grid, by `useGridFilters` or by
30
+ `createGridCustomFilterBadge`.
31
+
32
+ ---
@@ -0,0 +1,76 @@
1
+ # AppDataGrid — Saved views
2
+
3
+ > Part of the [AppDataGrid](../AppDataGrid.md) docs. The Props table and the `onQueryChange` payload are in the main file.
4
+
5
+ ## Saved views (presets)
6
+
7
+ A `GridViewPreset` captures one view:
8
+
9
+ ```ts
10
+ interface GridViewPreset {
11
+ version: number; // GRID_VIEW_PRESET_VERSION
12
+ id: string;
13
+ name: string;
14
+ query: {
15
+ search: string;
16
+ sorting: { id: string; desc: boolean }[]; // GridSortDescriptor[]
17
+ filters: GridBadgeFilter[];
18
+ pageSize: number;
19
+ };
20
+ view: {
21
+ density: GridDensity;
22
+ columnWidths: Record<string, number>;
23
+ columnVisibility: Record<string, boolean>;
24
+ columnOrder: string[];
25
+ };
26
+ }
27
+ ```
28
+
29
+ - **Apply** by passing a new object to `preset` (identity-based; re-passing an equal new object applies
30
+ again). One application = one `onQueryChange` emission.
31
+ - **Capture** the view half through `onDensityChange` (only for presets — own your density control),
32
+ `onColumnVisibilityChange`, `onColumnWidthsChange`, `onColumnOrderChange`. The query half you build from
33
+ your own state (`GridServerState.computedSorting` must be mapped to `GridSortDescriptor[]` by hand).
34
+ - The capture callbacks do not fire on preset application; `onQueryChange` does. Save on an explicit user
35
+ action, not on every change.
36
+ - `readGridViewPreset(raw)` validates a stored payload field by field (version, id, name, every
37
+ sort/filter element, operator vs. its `filterType`, finite numbers, integer `pageSize` ≥ 1, widths,
38
+ visibility, order). It throws `GridViewPresetVersionError` (`found`, `expected`, `reason` = offending
39
+ field path, `undefined` only for a version mismatch). It refuses, it does not sanitise: coerce values
40
+ (e.g. widths stored as strings in JSON columns) before reading.
41
+ - Not validated: the inside of a custom badge's `customFragment` (treat presets from users as privileged
42
+ input), and ids that name no existing column.
43
+ - `createInMemoryGridViewStore(initial?)` returns a `GridViewStore` (`list`, `load`, `save`, `remove`,
44
+ all async) kept in memory; implement the same interface for real persistence.
45
+
46
+ ```tsx
47
+ const filters = useGridFilters({ columns: toGridFilterColumns(columns) });
48
+ const [preset, setPreset] = useState<GridViewPreset>();
49
+ const [columnWidths, setColumnWidths] = useState<Record<string, number>>({});
50
+ const [columnOrder, setColumnOrder] = useState<string[]>([]);
51
+
52
+ <AppDataGrid
53
+ columns={columns}
54
+ data={rows}
55
+ preset={preset}
56
+ filters={filters.badges}
57
+ columnVisibility={filters.columnVisibility}
58
+ onColumnVisibilityChange={(next) => {
59
+ for (const [id, visible] of Object.entries(next))
60
+ if (filters.columnVisibility[id] !== visible)
61
+ filters.setColumnVisible(id, visible);
62
+ }}
63
+ onAddFilter={filters.addBadge}
64
+ onUpdateFilter={filters.updateBadge}
65
+ onColumnWidthsChange={(next) => setColumnWidths({ ...next })}
66
+ onColumnOrderChange={setColumnOrder}
67
+ onPresetApplied={(p) => {
68
+ filters.restore(p.query.filters, p.view.columnVisibility);
69
+ // Re-seed captured state: preset application does not call the capture callbacks.
70
+ setColumnWidths({ ...p.view.columnWidths });
71
+ setColumnOrder([...p.view.columnOrder]);
72
+ }}
73
+ />;
74
+ ```
75
+
76
+ ---
@@ -0,0 +1,44 @@
1
+ # AppDataGrid — Selection
2
+
3
+ > Part of the [AppDataGrid](../AppDataGrid.md) docs. The Props table and the `onQueryChange` payload are in the main file.
4
+
5
+ ## Selection
6
+
7
+ With `checkable`, a **selection bar** appears above the table as soon as a row is ticked (it occupies no
8
+ space otherwise). It shows the count («2 rows selected»), the scope («of 8 on this page»), a totals band
9
+ (the columns' `aggregate` over the selected rows, or `selection.summary.render()`), the bulk actions and
10
+ a dismiss / «Deselect all» button.
11
+
12
+ ```ts
13
+ type GridSelectionSlots =
14
+ | {
15
+ mode?: "page";
16
+ actions?: readonly GridSelectionAction[];
17
+ summary?: GridToolbarSlot;
18
+ }
19
+ | {
20
+ mode: "all";
21
+ actions?: readonly GridPromotableSelectionAction[];
22
+ summary?: GridToolbarSlot;
23
+ };
24
+
25
+ interface GridSelectionAction {
26
+ id?: string; // stable key, recommended when labels can repeat
27
+ label: string; // visible label WITHOUT the number: the grid appends the count
28
+ icon?: IconName;
29
+ destructive?: boolean;
30
+ onSelect: (rowIds: readonly string[]) => void;
31
+ "data-testid"?: string;
32
+ }
33
+ // GridPromotableSelectionAction: same fields, but onSelectScope(scope: GridSelectionScope) instead of onSelect.
34
+ ```
35
+
36
+ - `mode: "page"` (default): the header checkbox selects the rows of the current page.
37
+ - `mode: "all"`: the bar also offers «Select all N rows» (N = `resultTotal` or `serverConfig.totalItems`)
38
+ when the result is larger than the page. Actions then receive `{ scope, rowIds, count }`: at scope
39
+ `"all"`, `rowIds` are only the ids the grid holds — use `count` and your current query to act on the
40
+ whole result. `onSelectScope` is required there (the compiler rejects a plain `onSelect`).
41
+ - In full screen the bar takes its height from the viewport (one data row may be hidden).
42
+ - Selected rows get an accent tint on every cell plus a left bar.
43
+
44
+ ---