@dashforge/tw 0.5.0-beta → 0.7.0-beta

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 (105) hide show
  1. package/CHANGELOG.md +526 -0
  2. package/THEME-AUDIT.md +175 -0
  3. package/dist/index.esm.js +2504 -51
  4. package/dist/src/components/Accordion/accordion.variants.d.ts.map +1 -1
  5. package/dist/src/components/Box/box.variants.d.ts.map +1 -1
  6. package/dist/src/components/DataGrid/DataGrid.d.ts +29 -0
  7. package/dist/src/components/DataGrid/DataGrid.d.ts.map +1 -0
  8. package/dist/src/components/DataGrid/dataGrid.types.d.ts +171 -0
  9. package/dist/src/components/DataGrid/dataGrid.types.d.ts.map +1 -0
  10. package/dist/src/components/DataGrid/dataGrid.variants.d.ts +216 -0
  11. package/dist/src/components/DataGrid/dataGrid.variants.d.ts.map +1 -0
  12. package/dist/src/components/Dialog/dialog.variants.d.ts.map +1 -1
  13. package/dist/src/components/Divider/divider.variants.d.ts.map +1 -1
  14. package/dist/src/components/Pagination/Pagination.d.ts.map +1 -1
  15. package/dist/src/components/Pagination/pagination.variants.d.ts.map +1 -1
  16. package/dist/src/components/Popover/popover.variants.d.ts.map +1 -1
  17. package/dist/src/components/Skeleton/skeleton.variants.d.ts.map +1 -1
  18. package/dist/src/components/Table/Table.d.ts +24 -0
  19. package/dist/src/components/Table/Table.d.ts.map +1 -0
  20. package/dist/src/components/Table/cells/RenderButton.d.ts +13 -0
  21. package/dist/src/components/Table/cells/RenderButton.d.ts.map +1 -0
  22. package/dist/src/components/Table/cells/RenderChip.d.ts +80 -0
  23. package/dist/src/components/Table/cells/RenderChip.d.ts.map +1 -0
  24. package/dist/src/components/Table/cells/RenderText.d.ts +21 -0
  25. package/dist/src/components/Table/cells/RenderText.d.ts.map +1 -0
  26. package/dist/src/components/Table/cells/RenderTwoLine.d.ts +20 -0
  27. package/dist/src/components/Table/cells/RenderTwoLine.d.ts.map +1 -0
  28. package/dist/src/components/Table/cells/RowActionsMenu.d.ts +36 -0
  29. package/dist/src/components/Table/cells/RowActionsMenu.d.ts.map +1 -0
  30. package/dist/src/components/Table/cells/index.d.ts +11 -0
  31. package/dist/src/components/Table/cells/index.d.ts.map +1 -0
  32. package/dist/src/components/Table/table.types.d.ts +312 -0
  33. package/dist/src/components/Table/table.types.d.ts.map +1 -0
  34. package/dist/src/components/Table/table.variants.d.ts +285 -0
  35. package/dist/src/components/Table/table.variants.d.ts.map +1 -0
  36. package/dist/src/components/Tabs/tabs.variants.d.ts.map +1 -1
  37. package/dist/src/components/Tooltip/tooltip.variants.d.ts.map +1 -1
  38. package/dist/src/components/Typography/typography.variants.d.ts.map +1 -1
  39. package/dist/src/components/_shared/data/getNestedValue.d.ts +20 -0
  40. package/dist/src/components/_shared/data/getNestedValue.d.ts.map +1 -0
  41. package/dist/src/components/_shared/data/useColumnAutoDetect.d.ts +45 -0
  42. package/dist/src/components/_shared/data/useColumnAutoDetect.d.ts.map +1 -0
  43. package/dist/src/components/_shared/data/useControllableState.d.ts +23 -0
  44. package/dist/src/components/_shared/data/useControllableState.d.ts.map +1 -0
  45. package/dist/src/components/_shared/data/useDebouncedValue.d.ts +10 -0
  46. package/dist/src/components/_shared/data/useDebouncedValue.d.ts.map +1 -0
  47. package/dist/src/components/_shared/data/useTableFilter.d.ts +13 -0
  48. package/dist/src/components/_shared/data/useTableFilter.d.ts.map +1 -0
  49. package/dist/src/components/_shared/data/useTableSearch.d.ts +23 -0
  50. package/dist/src/components/_shared/data/useTableSearch.d.ts.map +1 -0
  51. package/dist/src/components/_shared/data/useTableSelection.d.ts +25 -0
  52. package/dist/src/components/_shared/data/useTableSelection.d.ts.map +1 -0
  53. package/dist/src/components/_shared/data/useTableSort.d.ts +20 -0
  54. package/dist/src/components/_shared/data/useTableSort.d.ts.map +1 -0
  55. package/dist/src/components/_shared/data/useVirtualizer.d.ts +107 -0
  56. package/dist/src/components/_shared/data/useVirtualizer.d.ts.map +1 -0
  57. package/dist/src/index.d.ts +10 -1
  58. package/dist/src/index.d.ts.map +1 -1
  59. package/package.json +3 -3
  60. package/src/components/Accordion/accordion.variants.ts +5 -3
  61. package/src/components/Box/Box.test.tsx +32 -11
  62. package/src/components/Box/box.variants.ts +23 -4
  63. package/src/components/DataGrid/DataGrid.test.tsx +496 -0
  64. package/src/components/DataGrid/DataGrid.tsx +831 -0
  65. package/src/components/DataGrid/dataGrid.types.ts +189 -0
  66. package/src/components/DataGrid/dataGrid.variants.ts +144 -0
  67. package/src/components/Dialog/dialog.variants.ts +10 -6
  68. package/src/components/Divider/Divider.test.tsx +3 -1
  69. package/src/components/Divider/Divider.tsx +1 -1
  70. package/src/components/Divider/divider.variants.ts +2 -1
  71. package/src/components/Pagination/Pagination.tsx +3 -1
  72. package/src/components/Pagination/pagination.variants.ts +19 -13
  73. package/src/components/Popover/popover.variants.ts +10 -4
  74. package/src/components/Skeleton/skeleton.variants.ts +2 -1
  75. package/src/components/Table/Table.test.tsx +603 -0
  76. package/src/components/Table/Table.tsx +816 -0
  77. package/src/components/Table/cells/RenderButton.tsx +23 -0
  78. package/src/components/Table/cells/RenderChip.tsx +103 -0
  79. package/src/components/Table/cells/RenderText.tsx +37 -0
  80. package/src/components/Table/cells/RenderTwoLine.tsx +34 -0
  81. package/src/components/Table/cells/RowActionsMenu.tsx +147 -0
  82. package/src/components/Table/cells/index.ts +14 -0
  83. package/src/components/Table/table.types.ts +346 -0
  84. package/src/components/Table/table.variants.ts +207 -0
  85. package/src/components/Tabs/tabs.variants.ts +19 -8
  86. package/src/components/Tooltip/tooltip.variants.ts +12 -2
  87. package/src/components/Typography/Typography.test.tsx +3 -1
  88. package/src/components/Typography/typography.variants.ts +6 -2
  89. package/src/components/_shared/data/getNestedValue.test.ts +50 -0
  90. package/src/components/_shared/data/getNestedValue.ts +32 -0
  91. package/src/components/_shared/data/useColumnAutoDetect.test.ts +123 -0
  92. package/src/components/_shared/data/useColumnAutoDetect.ts +100 -0
  93. package/src/components/_shared/data/useControllableState.ts +48 -0
  94. package/src/components/_shared/data/useDebouncedValue.ts +24 -0
  95. package/src/components/_shared/data/useTableFilter.ts +37 -0
  96. package/src/components/_shared/data/useTableSearch.test.ts +115 -0
  97. package/src/components/_shared/data/useTableSearch.ts +66 -0
  98. package/src/components/_shared/data/useTableSelection.test.ts +100 -0
  99. package/src/components/_shared/data/useTableSelection.ts +100 -0
  100. package/src/components/_shared/data/useTableSort.test.ts +150 -0
  101. package/src/components/_shared/data/useTableSort.ts +141 -0
  102. package/src/components/_shared/data/useVirtualizer.test.ts +158 -0
  103. package/src/components/_shared/data/useVirtualizer.ts +199 -0
  104. package/src/components/_shared/themeIdentity.test.ts +113 -0
  105. package/src/index.ts +59 -1
package/CHANGELOG.md CHANGED
@@ -12,6 +12,532 @@ This project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
12
12
  > duplicated intentionally — no shared "lowest common denominator" headless
13
13
  > layer.
14
14
 
15
+ ## [0.7.0-beta] — 2026-05-19
16
+
17
+ **Combined Sprint 4.2 + 4.3 release.** Ships **`<DataGrid>`** — a
18
+ virtualized data table for large data sets (500 rows to millions)
19
+ — alongside a **theme identity sweep** across the whole catalog
20
+ (systemic dark-mode contrast fixes on 10 existing components).
21
+
22
+ Two coherent themes, one release:
23
+
24
+ 1. **DataGrid (Sprint 4.2)** — sibling to `<Table>` with **homemade
25
+ virtualization** (no `@tanstack/react-virtual`, no
26
+ `react-window`). Same column model, same cell renderer library,
27
+ same RBAC integration, same identity-consistent visual design as
28
+ Table. Differs at the render strategy level: only the window of
29
+ visible rows mounts in DOM. Adds sticky-left columns, server-side
30
+ mode (4 independent opt-in flags), `selectAllScope`, and optional
31
+ internal pagination.
32
+
33
+ 2. **Theme identity sweep (Sprint 4.3)** — applies the identity rule
34
+ codified in Sprint 4.1 to the WHOLE catalog. 10 existing
35
+ components (Typography, Box, Dialog, Divider, Pagination,
36
+ Popover, Skeleton, Tabs, Tooltip, plus Table-side comments)
37
+ had latent `dark:` variants on the neutral palette → those
38
+ classes double-inverted via the dashforgePreset CSS-var swap
39
+ and broke dark mode. Sweep removes the anti-pattern across the
40
+ catalog and adds a package-level regression test
41
+ (`_shared/themeIdentity.test.ts`) that scans every source file
42
+ on every test run.
43
+
44
+ **Minor bump** for the new DataGrid public API surface. Strictly
45
+ additive — zero breaking changes on existing component APIs.
46
+ Drop-in upgrade from `0.6.0-beta`.
47
+
48
+ ### Added
49
+
50
+ - **`<DataGrid>`** — virtualized data table for large data sets.
51
+ Sibling to `<Table>`. Required props: `rows`, `cols`,
52
+ `getRowId`, `rowHeight` (fixed for v1), `height` (bounded
53
+ container).
54
+ - **Homemade virtualization** via the new `useVirtualizer` hook
55
+ in `_shared/data/`. Scroll-event based with
56
+ `requestAnimationFrame` debounce (60 fps throttling) +
57
+ `ResizeObserver` for container resize. Spacer-row approach
58
+ preserves `<table>` semantics — screen readers see continuous
59
+ structure with `aria-hidden` spacers above/below the visible
60
+ window. Variable row height deferred to v1-bis.
61
+ - **Sticky left column** via `cols[i].sticky: 'left'`. CSS
62
+ `position: sticky; left: 0` with z-index ladder for the
63
+ top-left corner intersection with the sticky header
64
+ (`thead` z-10 < sticky cell z-[1] < sticky header corner z-20).
65
+ Right-sticky deferred to v1-bis.
66
+ - **Server-side mode flags** (4 independent opt-ins):
67
+ - `serverSideSort` — emits `onSortChange` but doesn't sort locally
68
+ - `serverSideFilter` — emits `onFilterChange` but doesn't filter
69
+ - `serverSideSearch` — emits `onSearchQueryChange` (debounced)
70
+ - `serverSidePagination` — uses `totalCount` for the virtual
71
+ scrollbar math; `rows` is the server-returned page slice
72
+ - **`selectAllScope`** prop (`'visible' | 'allLoaded'`, default
73
+ `'allLoaded'`) controls the header "select all" checkbox
74
+ semantic.
75
+ - **Optional internal pagination** via the `pagination` prop —
76
+ renders the Sprint 4 `<Pagination>` component below the
77
+ virtualized scroll. Composes naturally with virtualization
78
+ (each page is virtualized internally if large enough).
79
+ - **Bundle impact**: +8.4 KB gz on top of `0.6.0-beta`
80
+ (`<Table>` baseline). Zero new runtime deps.
81
+ - **Variants**: `plain` / `lines` (default) / `striped` /
82
+ `bordered`. **No `card` variant** — incompatible with
83
+ `<table>` virtualization spacer-rows.
84
+ - 24 unit tests + 12 `useVirtualizer` math tests + dash smoke
85
+ test at `/test-data-grid` with 10 000 generated rows.
86
+
87
+ - **`useVirtualizer` hook** in `_shared/data/` — exported
88
+ internally only (not in the public API surface). Other future
89
+ virtualized components (TreeView, future GanttGrid, etc.) can
90
+ reuse it.
91
+
92
+ - **`sticky?: 'left' | 'right'`** added to `TableColumn<T>` for
93
+ DataGrid integration. Currently ignored by Table (non-virtualized);
94
+ honored by DataGrid v1 for `'left'` only.
95
+
96
+ - **Doc MDX `/tw/docs/components/data-grid`** — emphasizes the
97
+ Table vs DataGrid decision matrix (24-row comparison table +
98
+ "Use Table when..." / "Use DataGrid when..." + "Switch later"
99
+ path showing the 3-line diff to migrate).
100
+
101
+ ### Changed
102
+
103
+ - **Internal refactor — shared data helpers moved**:
104
+ `Table/_internal/` → `_shared/data/` (13 files). The helpers
105
+ (`getNestedValue`, `useTableSearch`, `useTableSort`,
106
+ `useTableSelection`, `useTableFilter`, `useColumnAutoDetect`,
107
+ `useDebouncedValue`, `useControllableState`) are now shared
108
+ between Table and DataGrid. **Pure path move** — function names
109
+ unchanged. Public API surface unchanged (only `getNestedValue`
110
+ was ever re-exported; the re-export path updated transparently).
111
+
112
+ - **Theme identity sweep** — 10 existing components had latent
113
+ `dark:` Tailwind variants on the neutral palette, which the
114
+ dashforgePreset CSS-var swap double-inverted and broke in dark
115
+ mode. Removed across the catalog:
116
+
117
+ | Component | Sites fixed | Pattern category |
118
+ |---|---|---|
119
+ | Accordion (`accordion.variants.ts`) | 3 | A (drop `dark:` — auto-invert) |
120
+ | Box (`box.variants.ts`) | 4 | A + B (corrected dark targets) + D (solid neutral now auto-inverts as accent) |
121
+ | Dialog (`dialog.variants.ts`) | 6 | A × 5 + B × 1 |
122
+ | Divider (`divider.variants.ts` + `Divider.tsx`) | 2 | A × 2 |
123
+ | Pagination (`pagination.variants.ts` + `Pagination.tsx`) | 10 | A × 7 + B × 3 (`dark:bg-neutral-900` → `bg-neutral-50` auto-invert) |
124
+ | Popover (`popover.variants.ts`) | 4 | A × 3 + B × 1 |
125
+ | Skeleton (`skeleton.variants.ts`) | 1 | A |
126
+ | Tabs (`tabs.variants.ts`) | 8 | A × 7 + B × 1 (`dark:bg-neutral-900` → `dark:bg-neutral-200` for proper active-pill elevation) |
127
+ | Tooltip (`tooltip.variants.ts`) | 2 | **Bug fix** — was previously invisible in dark mode (double-inverted to dark surface on dark page). Now auto-inverts: dark tooltip on light page, light tooltip on dark page (high contrast preserved). |
128
+ | Typography (`typography.variants.ts`) | 2 | A × 2 |
129
+
130
+ **Box solid neutral** now uses `bg-neutral-900 text-neutral-50`
131
+ (both auto-invert) — the accent surface flips with the page in
132
+ dark mode (light surface + dark text) for identity consistency.
133
+ Previously was `bg-neutral-900 text-white dark:bg-neutral-100
134
+ dark:text-neutral-900` — the `text-white` (no auto-invert) had
135
+ forced the double-invert pattern to stay readable.
136
+
137
+ Affected test files updated to assert the new canonical
138
+ patterns (Box × 4 + Typography × 1 + Divider × 1 = 6 tests
139
+ retargeted).
140
+
141
+ ### Internal
142
+
143
+ - **`THEME-AUDIT.md`** at package root — categorizes every site
144
+ in the catalog as **A** (drop `dark:` — auto-invert), **B**
145
+ (keep `dark:` because `bg-white`/`fill-white` is hardcoded),
146
+ **C** (color palette — intentional design choice, not affected
147
+ by the rule), or **D** (component-specific exception). 42
148
+ violations catalogued across 10 components.
149
+
150
+ - **Package-level theme identity regression test** —
151
+ `_shared/themeIdentity.test.ts` replaces the Table-only one
152
+ from Sprint 4.1. Scans every `.ts`/`.tsx` source file under
153
+ `src/components/**` (excluding tests) on every test run.
154
+ Allows the Category B legitimate pattern
155
+ (`bg-white dark:bg-neutral-N` — `bg-white` doesn't auto-invert,
156
+ the `dark:` is required). Catches accidental re-introductions
157
+ of the anti-pattern at PR time.
158
+
159
+ - **+76 new unit tests** for Sprint 4.2 + 4.3:
160
+ - `useVirtualizer` math (12) — window calc, overscan, padding
161
+ invariant, edge cases (totalCount=0, very large dataset)
162
+ - `<DataGrid>` (24) — rendering, virtualized window, sort,
163
+ search, selection, selectAllScope, sticky left column,
164
+ server-side mode flags, RBAC, internal pagination, smart
165
+ defaults, i18n
166
+ - `_shared/themeIdentity.test.ts` package-level scanner (40
167
+ file-scan tests; 0 violations reported)
168
+
169
+ Full TW suite at **961/961 passing** across **48 files**.
170
+
171
+ - **`TestDataGrid.tsx`** added to the `dash` consumer
172
+ (`/test-data-grid`) — 10 000 generated rows + sticky col +
173
+ selectAllScope toggle + variant/density/size switcher + RBAC
174
+ per-column demo + custom empty state + React.Profiler logging.
175
+
176
+ - **Sidebar entries** for the new `DataGrid` doc page added to
177
+ `dashforge-docs-lab/src/tw-docs/sidebar.model.ts` (new "Data"
178
+ subgroup under "UI Components" now contains Table + DataGrid).
179
+
180
+ ### Compatibility
181
+
182
+ | Axis | Pre-`0.7.0` | Post-`0.7.0` |
183
+ |---|---|---|
184
+ | Public API surface | 32 components | **+1 `<DataGrid>` + types (`DataGridProps`, `DataGridSelectAllScope`, `DataGridServerSideFlags`, `DataGridPaginationConfig`, `DataGridSlotProps`) + `dataGridVariants` recipe** + `sticky` axis on `TableColumn<T>` (additive, optional) |
185
+ | Peer deps | `react ^18 \|\| ^19`, `tw-theme workspace`, `tw-tokens workspace` | unchanged |
186
+ | Bridge deps | `forms` / `rbac` / `ui-core` `workspace:*` | unchanged |
187
+ | **New runtime deps** | — | **none** (homemade virtualization — constraint honored) |
188
+ | Breaking changes | — | **Zero** on public APIs. Internal refactor: `Table/_internal/` helpers moved to `_shared/data/`. Only externally-visible re-export was `getNestedValue` (path updated internally; export name preserved). |
189
+ | Bundle size | 402 KB raw / 91 KB gz | **445 KB raw / 98.4 KB gz** (+43 KB raw / +7.4 KB gz / **+8.1% gz**) — just under the 10% reviewer-sign-off threshold |
190
+ | Tests passing | 828/828 (46 files) | **961/961 (48 files)** — +133 from Sprint 4.2 + 4.3 |
191
+
192
+ > **Bundle sign-off** : the +8.1% gz delta is JUST under the 10%
193
+ > threshold from PERFORMANCE.md. Justification: DataGrid is the
194
+ > highest-leverage addition before 1.0 — it unlocks the entire
195
+ > "1 000+ row admin views" use case at €0 vs MUI X DataGrid Pro's
196
+ > €499/year. The theme identity sweep is net-neutral on bundle
197
+ > (removed redundant classes, replaced with smaller ones); the
198
+ > delta is entirely DataGrid + virtualizer code (+8 KB gz).
199
+
200
+ ### Migration
201
+
202
+ No code changes required:
203
+
204
+ ```bash
205
+ pnpm up @dashforge/tw@^0.7.0-beta
206
+ ```
207
+
208
+ To adopt the new DataGrid:
209
+
210
+ ```tsx
211
+ import {
212
+ DataGrid,
213
+ RenderTwoLine,
214
+ RenderChip,
215
+ RowActionsMenu,
216
+ } from '@dashforge/tw';
217
+
218
+ <DataGrid
219
+ rows={users} // can be 10k, 100k, millions
220
+ cols={columns} // SAME shape as Table's TableColumn<T>
221
+ getRowId={(r) => r.id}
222
+ rowHeight={48} // required for virtualization
223
+ height="600px" // required for bounded container
224
+ enableSearch
225
+ rowSelection="multiple"
226
+ selectAllScope="allLoaded"
227
+ bulkActions={(rows) => (
228
+ <Button color="danger">Delete {rows.length}</Button>
229
+ )}
230
+ rowActions={(row) => (
231
+ <RowActionsMenu
232
+ row={row}
233
+ actions={[
234
+ { label: 'Edit', onClick: edit },
235
+ { label: 'Delete', onClick: del, color: 'danger' },
236
+ ]}
237
+ />
238
+ )}
239
+ />
240
+ ```
241
+
242
+ To switch an existing Table to DataGrid when you outgrow the row
243
+ count (~500+ rows), the migration is mechanical (3 lines):
244
+
245
+ ```diff
246
+ - <Table
247
+ + <DataGrid
248
+ rows={users}
249
+ cols={columns}
250
+ getRowId={(row) => row.id}
251
+ + rowHeight={48}
252
+ + height="600px"
253
+ />
254
+ ```
255
+
256
+ Everything else (sort / search / selection / row actions / cell
257
+ renderers / RBAC / i18n labels / sx / slotProps) keeps working
258
+ identically. **Expandable rows** are the one feature you lose
259
+ on Table→DataGrid swap — they ship in DataGrid v1-bis.
260
+
261
+ ### Heads-up — companion releases coming
262
+
263
+ - **Sprint 4.2-bis → `0.8.0-beta`** (estimated): DataGrid v1-bis —
264
+ per-column filter UI chips (text/number/boolean/date), column
265
+ resize via drag, column reorder via drag, column visibility
266
+ dialog, right-sticky columns.
267
+ - **Sprint 5 → `0.9.0-beta`** + starter kits v1: separate repos
268
+ `dashforge-starter-mui` + `dashforge-starter-tw` with Auth + RBAC
269
+ + form CRUD + dashboard with DataGrid admin views.
270
+ - **Sprint 6 → `1.0.0-rc.1 → 1.0.0`**: final A11Y audit, bundle
271
+ lockdown, beta freeze, cut `1.0.0`.
272
+
273
+ ## [0.6.0-beta] — 2026-05-19
274
+
275
+ **Sprint 4.1 release.** Ships the central piece of the data layer
276
+ — a market-grounded `<Table>` built from scratch with NO new
277
+ runtime deps. The design references Stripe (visual style),
278
+ Atlassian / Jira (column UX), and Pencil & Paper UX research
279
+ (density tiers, hover-revealed row actions, selection patterns).
280
+ The existing MUI Table inherited from `hub-ws/admin` informed the
281
+ public API shape but not the implementation (we discarded the
282
+ `@mui/x-data-grid` backing — Table v1 is a hand-rolled native
283
+ `<table>`).
284
+
285
+ **Companion to Sprint 4.2** (DataGrid, virtualization, 10k+ rows)
286
+ — Table v1 covers the 80% admin-list use case up to ~500 rows.
287
+
288
+ **Minor bump** for the new public API surface — 15+ new exports.
289
+ Strictly additive — zero breaking changes. Drop-in upgrade from
290
+ `0.5.0-beta`.
291
+
292
+ ### Added
293
+
294
+ - **`<Table>`** — declarative-first data table component.
295
+ - **Smart defaults**: column types auto-detected from the first
296
+ non-null value across visible rows. Number columns get
297
+ `text-right` + `tabular-nums` (digit grid alignment), date
298
+ columns get `tabular-nums` too, booleans get `text-center`,
299
+ strings get `text-left`. **The library never changes the font
300
+ family** — `tabular-nums` is a font-feature setting that
301
+ preserves the consumer's theme `font-sans`. Explicit
302
+ `align` / `tabularNums` / `monospace` per column wins.
303
+ - **Sort**: per-column `sortable: true` (default comparator
304
+ handles string / number / Date / boolean / bigint with
305
+ null-last invariant) or custom `(a, b) => number`. Click
306
+ cycles asc → desc → none. **Shift-click** adds the column to
307
+ a multi-sort model. Controlled via `sortModel` /
308
+ `onSortChange` or uncontrolled with internal state.
309
+ - **Search**: `enableSearch` renders a debounced input above
310
+ the table (default 200 ms). Matches any column flagged
311
+ `searchable: true`. **Nested keys supported** at runtime via
312
+ `getNestedValue(row, 'address.city')` — the type-level
313
+ autocomplete from `NestedKeyOf<T>` matches the runtime
314
+ behavior. Stringification covers string / number / boolean /
315
+ Date / array / object.
316
+ - **Selection**: `rowSelection: 'none' | 'single' | 'multiple'`,
317
+ optional select-all checkbox in the header (multiple mode),
318
+ `selectedRowIds` controlled state, **sticky bulk-action
319
+ footer** rendered only when at least one row is selected
320
+ (Pencil & Paper UX pattern: "appear only when selected").
321
+ - **Expandable rows**: `expandable={{ render, expandedRowIds?,
322
+ onExpandChange? }}`. Chevron toggle in a dedicated column;
323
+ `aria-expanded` updates; expanded detail renders as a
324
+ full-width sibling `<tr>` below the row.
325
+ - **Row actions revealed on hover**: `rowActions={(row) => …}`
326
+ slot — by default hidden via opacity, revealed on
327
+ `tr:hover` / `tr:focus-within` (Stripe pattern — reduces
328
+ visual density while keeping actions discoverable).
329
+ - **RBAC at 3 levels**: table-level `access` (hides / disables
330
+ the whole table), per-column `cols[i].access` (`hide` removes
331
+ the column entirely from header AND every row cell), and
332
+ per-action `actions[i].access` on `<RowActionsMenu>`.
333
+ - **A11Y**: semantic `<table>` + `<th scope="col">` + `aria-sort`
334
+ on sortable headers + `aria-selected` on selected rows +
335
+ `aria-expanded` on expand toggles + `<caption>` (optional,
336
+ `sr-only` by default) + keyboard nav via Tab + Enter/Space.
337
+ - **Loading state**: `loading={true}` renders N `<Skeleton>` rows
338
+ (the Sprint 4 component) with `aria-busy="true"`. Count
339
+ configurable via `loadingRowCount` (default 5).
340
+ - **i18n**: column `header` accepts plain strings (pass
341
+ `t('...')`); all internal default strings configurable via
342
+ `labels` prop with English defaults (search placeholder, a11y
343
+ announcements for sort / select / expand, selection counter
344
+ with `{count}` placeholder, density / filter labels). Same
345
+ pattern as `<Pagination>`.
346
+ - **5 variants** (`plain` · `lines` default — Stripe-style ·
347
+ `striped` · `bordered` · `card`) × **3 sizes** (`sm` · `md` ·
348
+ `lg`) × **3 densities** (`compact` 40px · `comfortable`
349
+ default 48px · `spacious` 56px — Pencil & Paper UX research).
350
+ - **Sticky header** by default (overridable).
351
+ - **`sx` + 15 slot props** for the standard customization
352
+ escape hatches.
353
+
354
+ - **Cell renderer library** — pre-built renderers for common
355
+ patterns, exported from `@dashforge/tw`:
356
+ - `RenderText` — one-line, optionally truncated / muted
357
+ - `RenderTwoLine` — bold primary + muted secondary
358
+ (name + email pattern)
359
+ - `RenderChip` — internal status badge, 7 intent colors × 3
360
+ variants (soft / solid / outline) × 2 sizes (sm / md)
361
+ - `RenderButton` — inline button wrapper (defaults `ghost` +
362
+ `sm`)
363
+ - `RowActionsMenu` — 3-dot Popover-backed menu with per-action
364
+ RBAC
365
+
366
+ - **Helpers exported for power use**:
367
+ - `getNestedValue(row, path)` — dotted-path lookup powering
368
+ cell rendering. Useful in consumer custom renderers.
369
+
370
+ - **Theme identity regression guard** —
371
+ `_internal/themeIdentity.test.ts` scans every Table source file
372
+ and fails if `dark:*-neutral-N` classes are introduced. The
373
+ dashforgePreset auto-inverts the neutral palette via CSS var
374
+ swap; adding `dark:` variants on neutral creates double
375
+ inversion and breaks dark mode. The Sprint 4.1 fix removes the
376
+ anti-pattern from Table; **Sprint 4.3 will sweep the rest of
377
+ the catalog** (Typography, Box, etc. carry the same latent bug).
378
+
379
+ - **Smoke-test page** in `dash` consumer at `/test-table` —
380
+ exercises every Table feature with 30 realistic users + nested
381
+ meta + chip-rendered status + RBAC per-column + i18n labels in
382
+ Italian + 5 variants × 3 sizes × 3 densities switcher.
383
+
384
+ - **Doc MDX** `/tw/docs/components/table` — full feature catalog,
385
+ i18n example with `react-i18next`, override matrix
386
+ (align / tabularNums / monospace), 15+ slot props reference,
387
+ cell renderer library, A11Y guarantees, roadmap.
388
+
389
+ ### Internal
390
+
391
+ - **Theme identity rule** (now codified): the dashforgePreset
392
+ default IS the Dashforge visual identity. Consumer apps consume
393
+ it as-is and never override. Adding `dark:` variants on the
394
+ neutral palette = double inversion = breaks dark mode. Use
395
+ canonical patterns (LeftNav `itemActive` for selected,
396
+ auto-invert for `bg-neutral-*` and `text-neutral-*`).
397
+ - **Font family rule**: the library never picks a font family —
398
+ `tabular-nums` is safe to auto-apply (font-feature setting),
399
+ `font-mono` is consumer opt-in only via `col.monospace: true`.
400
+ The dashforgePreset does not own the `fontFamily` axis; the
401
+ consumer configures their mono stack in their own
402
+ `tailwind.config.ts` `theme.extend.fontFamily.mono`.
403
+ - **+147 new unit tests** for Table:
404
+ - `getNestedValue` (11) — nested keys, null-safety, zero / empty
405
+ string / false preservation
406
+ - `useTableSearch` (16) — stringification across primitive types
407
+ + nested keys
408
+ - `useTableSort` (15) — null-last invariant (direction-independent),
409
+ multi-column tie-breaking, custom comparator
410
+ - `useTableSelection` (13) — single / multiple / none modes
411
+ - `useColumnAutoDetect` (14) — type inference + align /
412
+ tabularNums / monospace resolution
413
+ - `themeIdentity` (18 file scanner) — regression guard for
414
+ `dark:` on neutral palette
415
+ - `Table.test.tsx` (57) — rendering, smart defaults, sort,
416
+ search, selection, expandable, row actions, RBAC, variants,
417
+ densities, sizes, sx + slotProps, i18n
418
+ - Full TW suite at **828/828 passing** (46 files; +147 from
419
+ Table, +newer guard).
420
+
421
+ ### Compatibility
422
+
423
+ | Axis | Pre-`0.6.0` | Post-`0.6.0` |
424
+ |---|---|---|
425
+ | Public API surface | 31 components | **+ 1 (`Table`)** + 5 cell renderers + `getNestedValue` helper + `Table*` types (`TableProps`, `TableColumn`, `TableSortModel`, `TableFilterModel`, `TableLabels`, `TableCellContext`, `NestedKeyOf`, `TableRowAction`, …) |
426
+ | Peer deps | `react ^18 \|\| ^19`, `tw-theme workspace`, `tw-tokens workspace` | unchanged |
427
+ | Bridge deps | `forms` / `rbac` / `ui-core` `workspace:*` | unchanged |
428
+ | New runtime deps | — | **none** (no `@tanstack/*`, no DnD libs — constraint honored) |
429
+ | Breaking changes | — | Zero |
430
+ | Bundle size | 336 KB raw / 73.9 KB gzipped | **402 KB raw / 91 KB gzipped** (+66 KB raw / +17 KB gz / **+23% gz**) |
431
+ | Migration | — | Drop-in. Zero code changes required on existing usages. |
432
+
433
+ > **Bundle regression note**: the +23% gz delta is **above the
434
+ > 10% reviewer-sign-off threshold** documented in
435
+ > `PERFORMANCE.md`. Justification:
436
+ > Table is the lib's central data-display primitive — it includes
437
+ > sort + search + selection + expandable + RBAC at 3 levels + 5
438
+ > cell renderers + RowActionsMenu (using Popover) + 5 variants ×
439
+ > 3 sizes × 3 densities. The bundle weight is proportional to
440
+ > the feature surface. Sign-off rationale: this is the
441
+ > "MUI X DataGrid Community alternative" component — the single
442
+ > highest-value addition before 1.0. Sprint 4.3 (theme identity
443
+ > sweep) will recover -1 to -3 KB gz by removing redundant
444
+ > `dark:` variants across the catalog.
445
+
446
+ ### Migration
447
+
448
+ No code changes required:
449
+
450
+ ```bash
451
+ pnpm up @dashforge/tw@^0.6.0-beta
452
+ ```
453
+
454
+ To adopt the new Table:
455
+
456
+ ```tsx
457
+ import { Table, RenderTwoLine, RenderChip, RowActionsMenu } from '@dashforge/tw';
458
+ import { useTranslation } from 'react-i18next';
459
+
460
+ function UsersTable({ users }) {
461
+ const { t } = useTranslation();
462
+ const [selected, setSelected] = useState<string[]>([]);
463
+
464
+ return (
465
+ <Table
466
+ rows={users}
467
+ cols={[
468
+ {
469
+ field: 'name',
470
+ header: t('users.fields.name'),
471
+ sortable: true,
472
+ searchable: true,
473
+ cellRenderer: ({ row }) => (
474
+ <RenderTwoLine primary={row.name} secondary={row.email} />
475
+ ),
476
+ },
477
+ {
478
+ field: 'salary',
479
+ header: t('users.fields.salary'),
480
+ sortable: true,
481
+ // Right-aligned + tabular-nums automatically. Font family
482
+ // stays whatever the consumer's theme provides — opt into
483
+ // `monospace: true` if you want font-mono explicitly.
484
+ access: {
485
+ resource: 'employee.salary',
486
+ action: 'read',
487
+ onUnauthorized: 'hide',
488
+ },
489
+ },
490
+ {
491
+ field: 'status',
492
+ header: t('users.fields.status'),
493
+ cellRenderer: ({ value }) => (
494
+ <RenderChip
495
+ color={value === 'active' ? 'success' : 'warning'}
496
+ >
497
+ {String(value)}
498
+ </RenderChip>
499
+ ),
500
+ },
501
+ ]}
502
+ getRowId={(r) => r.id}
503
+ enableSearch
504
+ rowSelection="multiple"
505
+ selectedRowIds={selected}
506
+ onSelectionChange={setSelected}
507
+ bulkActions={(rows) => (
508
+ <Button color="danger">
509
+ {t('actions.delete', { count: rows.length })}
510
+ </Button>
511
+ )}
512
+ rowActions={(row) => (
513
+ <RowActionsMenu
514
+ row={row}
515
+ actions={[
516
+ { label: t('actions.edit'), onClick: edit },
517
+ { label: t('actions.delete'), onClick: del, color: 'danger' },
518
+ ]}
519
+ />
520
+ )}
521
+ labels={{
522
+ searchPlaceholder: t('table.search'),
523
+ selectedCount: t('table.selected', { defaultValue: '{count} selected' }),
524
+ }}
525
+ />
526
+ );
527
+ }
528
+ ```
529
+
530
+ ### Heads-up — companion releases coming
531
+
532
+ - **Sprint 4.3 → `0.7.0-beta`**: theme identity audit across the
533
+ whole catalog (Typography, Box, etc. carry the same latent
534
+ `dark:` anti-pattern Table just fixed). Expected bundle delta:
535
+ **−1 to −3 KB gz** (removing redundant classes).
536
+ - **Sprint 4.2 → `0.8.0-beta`**: DataGrid with homemade
537
+ virtualization (`IntersectionObserver`-based, no new deps) for
538
+ 10k+ row data sets, advanced filter model, sticky columns,
539
+ per-column RBAC.
540
+
15
541
  ## [0.5.0-beta] — 2026-05-19
16
542
 
17
543
  **Sprint 4 release.** Two TW-only utility primitives — `<Skeleton>`