@dashforge/tw 0.6.0-beta → 0.8.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 (107) hide show
  1. package/CHANGELOG.md +451 -0
  2. package/THEME-AUDIT.md +175 -0
  3. package/dist/index.esm.js +2009 -87
  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 +214 -0
  9. package/dist/src/components/DataGrid/dataGrid.types.d.ts.map +1 -0
  10. package/dist/src/components/DataGrid/dataGrid.variants.d.ts +222 -0
  11. package/dist/src/components/DataGrid/dataGrid.variants.d.ts.map +1 -0
  12. package/dist/src/components/DataGrid/filters/ColumnFilters.d.ts +23 -0
  13. package/dist/src/components/DataGrid/filters/ColumnFilters.d.ts.map +1 -0
  14. package/dist/src/components/DataGrid/reorder/useColumnReorder.d.ts +50 -0
  15. package/dist/src/components/DataGrid/reorder/useColumnReorder.d.ts.map +1 -0
  16. package/dist/src/components/DataGrid/resize/useColumnResize.d.ts +32 -0
  17. package/dist/src/components/DataGrid/resize/useColumnResize.d.ts.map +1 -0
  18. package/dist/src/components/DataGrid/visibility/ColumnVisibilityDialog.d.ts +33 -0
  19. package/dist/src/components/DataGrid/visibility/ColumnVisibilityDialog.d.ts.map +1 -0
  20. package/dist/src/components/Dialog/dialog.variants.d.ts.map +1 -1
  21. package/dist/src/components/Divider/divider.variants.d.ts.map +1 -1
  22. package/dist/src/components/Pagination/Pagination.d.ts.map +1 -1
  23. package/dist/src/components/Pagination/pagination.variants.d.ts.map +1 -1
  24. package/dist/src/components/Popover/popover.variants.d.ts.map +1 -1
  25. package/dist/src/components/Skeleton/skeleton.variants.d.ts.map +1 -1
  26. package/dist/src/components/Table/Table.d.ts.map +1 -1
  27. package/dist/src/components/Table/table.types.d.ts +77 -5
  28. package/dist/src/components/Table/table.types.d.ts.map +1 -1
  29. package/dist/src/components/Tabs/tabs.variants.d.ts.map +1 -1
  30. package/dist/src/components/Tooltip/tooltip.variants.d.ts.map +1 -1
  31. package/dist/src/components/Typography/typography.variants.d.ts.map +1 -1
  32. package/dist/src/components/_shared/data/getNestedValue.d.ts.map +1 -0
  33. package/dist/src/components/{Table/_internal → _shared/data}/useColumnAutoDetect.d.ts +1 -1
  34. package/dist/src/components/_shared/data/useColumnAutoDetect.d.ts.map +1 -0
  35. package/dist/src/components/_shared/data/useControllableState.d.ts.map +1 -0
  36. package/dist/src/components/_shared/data/useDebouncedValue.d.ts.map +1 -0
  37. package/dist/src/components/_shared/data/useTableFilter.d.ts +36 -0
  38. package/dist/src/components/_shared/data/useTableFilter.d.ts.map +1 -0
  39. package/dist/src/components/{Table/_internal → _shared/data}/useTableSearch.d.ts +1 -1
  40. package/dist/src/components/_shared/data/useTableSearch.d.ts.map +1 -0
  41. package/dist/src/components/{Table/_internal → _shared/data}/useTableSelection.d.ts +1 -1
  42. package/dist/src/components/_shared/data/useTableSelection.d.ts.map +1 -0
  43. package/dist/src/components/{Table/_internal → _shared/data}/useTableSort.d.ts +1 -1
  44. package/dist/src/components/_shared/data/useTableSort.d.ts.map +1 -0
  45. package/dist/src/components/_shared/data/useVirtualizer.d.ts +107 -0
  46. package/dist/src/components/_shared/data/useVirtualizer.d.ts.map +1 -0
  47. package/dist/src/index.d.ts +6 -3
  48. package/dist/src/index.d.ts.map +1 -1
  49. package/package.json +5 -5
  50. package/src/components/Accordion/accordion.variants.ts +5 -3
  51. package/src/components/Box/Box.test.tsx +32 -11
  52. package/src/components/Box/box.variants.ts +23 -4
  53. package/src/components/DataGrid/DataGrid.test.tsx +917 -0
  54. package/src/components/DataGrid/DataGrid.tsx +1199 -0
  55. package/src/components/DataGrid/dataGrid.types.ts +238 -0
  56. package/src/components/DataGrid/dataGrid.variants.ts +152 -0
  57. package/src/components/DataGrid/filters/ColumnFilters.tsx +432 -0
  58. package/src/components/DataGrid/reorder/useColumnReorder.test.ts +38 -0
  59. package/src/components/DataGrid/reorder/useColumnReorder.ts +144 -0
  60. package/src/components/DataGrid/resize/useColumnResize.ts +106 -0
  61. package/src/components/DataGrid/visibility/ColumnVisibilityDialog.tsx +200 -0
  62. package/src/components/Dialog/dialog.variants.ts +10 -6
  63. package/src/components/Divider/Divider.test.tsx +3 -1
  64. package/src/components/Divider/Divider.tsx +1 -1
  65. package/src/components/Divider/divider.variants.ts +2 -1
  66. package/src/components/Pagination/Pagination.tsx +3 -1
  67. package/src/components/Pagination/pagination.variants.ts +19 -13
  68. package/src/components/Popover/popover.variants.ts +10 -4
  69. package/src/components/Skeleton/skeleton.variants.ts +2 -1
  70. package/src/components/Table/Table.tsx +22 -8
  71. package/src/components/Table/table.types.ts +83 -5
  72. package/src/components/Tabs/tabs.variants.ts +19 -8
  73. package/src/components/Tooltip/tooltip.variants.ts +12 -2
  74. package/src/components/Typography/Typography.test.tsx +3 -1
  75. package/src/components/Typography/typography.variants.ts +6 -2
  76. package/src/components/{Table/_internal → _shared/data}/useColumnAutoDetect.test.ts +1 -1
  77. package/src/components/{Table/_internal → _shared/data}/useColumnAutoDetect.ts +1 -1
  78. package/src/components/_shared/data/useTableFilter.test.ts +166 -0
  79. package/src/components/_shared/data/useTableFilter.ts +118 -0
  80. package/src/components/{Table/_internal → _shared/data}/useTableSearch.test.ts +1 -1
  81. package/src/components/{Table/_internal → _shared/data}/useTableSearch.ts +1 -1
  82. package/src/components/{Table/_internal → _shared/data}/useTableSelection.ts +1 -1
  83. package/src/components/{Table/_internal → _shared/data}/useTableSort.test.ts +1 -1
  84. package/src/components/{Table/_internal → _shared/data}/useTableSort.ts +1 -1
  85. package/src/components/_shared/data/useVirtualizer.test.ts +158 -0
  86. package/src/components/_shared/data/useVirtualizer.ts +199 -0
  87. package/src/components/_shared/themeIdentity.test.ts +113 -0
  88. package/src/index.ts +20 -2
  89. package/dist/src/components/Table/_internal/getNestedValue.d.ts.map +0 -1
  90. package/dist/src/components/Table/_internal/useColumnAutoDetect.d.ts.map +0 -1
  91. package/dist/src/components/Table/_internal/useControllableState.d.ts.map +0 -1
  92. package/dist/src/components/Table/_internal/useDebouncedValue.d.ts.map +0 -1
  93. package/dist/src/components/Table/_internal/useTableFilter.d.ts +0 -13
  94. package/dist/src/components/Table/_internal/useTableFilter.d.ts.map +0 -1
  95. package/dist/src/components/Table/_internal/useTableSearch.d.ts.map +0 -1
  96. package/dist/src/components/Table/_internal/useTableSelection.d.ts.map +0 -1
  97. package/dist/src/components/Table/_internal/useTableSort.d.ts.map +0 -1
  98. package/src/components/Table/_internal/themeIdentity.test.ts +0 -84
  99. package/src/components/Table/_internal/useTableFilter.ts +0 -37
  100. /package/dist/src/components/{Table/_internal → _shared/data}/getNestedValue.d.ts +0 -0
  101. /package/dist/src/components/{Table/_internal → _shared/data}/useControllableState.d.ts +0 -0
  102. /package/dist/src/components/{Table/_internal → _shared/data}/useDebouncedValue.d.ts +0 -0
  103. /package/src/components/{Table/_internal → _shared/data}/getNestedValue.test.ts +0 -0
  104. /package/src/components/{Table/_internal → _shared/data}/getNestedValue.ts +0 -0
  105. /package/src/components/{Table/_internal → _shared/data}/useControllableState.ts +0 -0
  106. /package/src/components/{Table/_internal → _shared/data}/useDebouncedValue.ts +0 -0
  107. /package/src/components/{Table/_internal → _shared/data}/useTableSelection.test.ts +0 -0
package/CHANGELOG.md CHANGED
@@ -12,6 +12,457 @@ 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.8.0-beta] — 2026-05-19
16
+
17
+ **Sprint 4.2-bis — DataGrid v1-bis.** Ships the five power-user
18
+ features deferred from `0.7.0-beta`'s DataGrid v1:
19
+
20
+ - **Per-column filter UI chips** — text contains / number range /
21
+ date range / boolean radio, auto-detected by column type
22
+ - **Column visibility dialog** — toolbar "Columns" button +
23
+ modal with checkboxes
24
+ - **Column resize** — grab the right edge of a `<th>` to drag,
25
+ min/max clamping
26
+ - **Column reorder** — native HTML5 drag-and-drop on headers,
27
+ visual drop indicator
28
+ - **Right-sticky columns** — mirror of `sticky: 'left'` (Sprint 4.2)
29
+
30
+ **Minor bump** for the new public API surface (3 new DataGrid
31
+ props, 5 new `TableColumn` axes, 13 new i18n labels). **Strictly
32
+ additive — zero breaking changes** on existing component APIs.
33
+ Drop-in upgrade from `0.7.0-beta`.
34
+
35
+ Zero new runtime deps: filter UI built on the existing
36
+ `<Popover>` (Sprint 3), visibility on `<Dialog>` (Sprint 3),
37
+ resize/reorder on native pointer + HTML5 drag events.
38
+
39
+ ### Added
40
+
41
+ - **Per-column filter UI** — opt in via `cols[i].filterable: true`.
42
+ A filter icon appears in the column header; clicking it opens a
43
+ `<Popover>` with the right input for the column type:
44
+ - `number` → Min/Max inputs (operator: `between`)
45
+ - `date` → From/To date inputs (operator: `between`)
46
+ - `boolean` → All / True / False radios (operator: `equals`)
47
+ - text / unknown → single text input (operator: `contains`,
48
+ case-insensitive substring against the stringified value)
49
+ - **Override autodetect** with `cols[i].filterType` (`'text' |
50
+ 'number' | 'boolean' | 'date'`) — useful when an ID column is
51
+ typed as number but you want a text-contains UI.
52
+ - The filter model uses the new
53
+ `TableFilterOperator = 'contains' | 'equals' | 'between'`
54
+ discriminator. The `between` value shape is `[min, max]` where
55
+ each end can be `null` for an open range. Backwards-compatible:
56
+ `0.7.0-beta` only emitted `contains`; consumers reading
57
+ `filterModel` should now branch on `op`.
58
+ - **Active state** — the filter icon highlights primary-700 when
59
+ a filter is active on that column (`aria-pressed=true`).
60
+ - **i18n** via the existing `labels` prop. New keys:
61
+ `filterMin`, `filterMax`, `filterFrom`, `filterTo`,
62
+ `filterAll`, `filterTrue`, `filterFalse` (plus the existing
63
+ `filterColumn`, `filterApply`, `filterClear`).
64
+
65
+ - **Column visibility dialog** — opt in via the existing
66
+ `enableColumnVisibility` (defaults `true`). A "Columns" button
67
+ appears in the toolbar (alongside the optional search input).
68
+ Clicking opens a `<Dialog>` with one checkbox per `hideable`
69
+ column.
70
+ - `cols[i].hideable: false` excludes a column from the dialog
71
+ entirely (structurally required — typically the ID or primary
72
+ label column).
73
+ - `cols[i].defaultHidden: true` hides the column on first mount;
74
+ the user re-shows it via the dialog.
75
+ - Show all / Hide all shortcuts inside the dialog.
76
+ - Controllable via `hiddenColumns` + `onHiddenColumnsChange` for
77
+ persistence (LocalStorage, server-side prefs, etc.).
78
+ - New `labels` keys: `columnsButton`, `columnsTitle`,
79
+ `columnsDescription`, `columnsShowAll`, `columnsHideAll`,
80
+ `columnsDone`.
81
+
82
+ - **Column resize** — opt in via the existing `enableColumnResize`
83
+ (defaults `true`). Hovering the right edge of a `<th>` shows
84
+ `cursor: col-resize`; pointer-drag commits a new width.
85
+ - Native pointer events with `setPointerCapture` so the drag
86
+ continues even if the cursor leaves the handle.
87
+ - Clamping: `cols[i].minWidth` (default `40px`) /
88
+ `cols[i].maxWidth` (default `1200px`).
89
+ - Per-column opt-out via `cols[i].resizable: false`.
90
+ - Controllable via `columnWidths: Record<string, number>` +
91
+ `onColumnWidthsChange` for persistence.
92
+ - Works with sticky columns and the virtualized body — the
93
+ width is applied to `<th>` and matching `<td>` cells.
94
+
95
+ - **Column reorder** — opt in via the existing
96
+ `enableColumnReorder` (defaults `true`). Drag any column header
97
+ onto another; a 2px primary-500 vertical indicator marks the
98
+ drop side (LEFT half = insert before / RIGHT half = insert
99
+ after). Release to commit.
100
+ - Built on native HTML5 drag-and-drop (`draggable` + `dragstart`
101
+ / `dragover` / `drop` / `dragend`) — no library.
102
+ - The dragged header dims to `opacity-50` while in flight.
103
+ - Per-column opt-out via `cols[i].reorderable: false`.
104
+ - Controllable via `columnOrder: string[]` (list of `field`s in
105
+ display order) + `onColumnOrderChange`. Columns not in the
106
+ order list are appended at the end in their original `cols`
107
+ order — safe to omit.
108
+
109
+ - **Right-sticky columns** — `cols[i].sticky: 'right'` mirrors the
110
+ existing `'left'` value from Sprint 4.2. CSS
111
+ `position: sticky; right: 0` plus a left border to visually
112
+ separate the pinned column from the scrollable area.
113
+ - Header / cell z-index ladder maintained (header top-right
114
+ intersection at z-20, body sticky-right cell at z-[1]).
115
+ - Useful for trailing action columns (Edit / Delete) that should
116
+ always be visible.
117
+
118
+ ### Changed
119
+
120
+ - **`TableFilterItem.op`** now accepts `'contains' | 'equals' |
121
+ 'between'` (was effectively `'contains'`-only in `0.7.0-beta`).
122
+ Consumers reading the model should branch on `op` to handle the
123
+ three value shapes (`string` / `unknown` / `[min, max]` tuple).
124
+ Consumers only emitting filters via the new in-header UI need no
125
+ changes.
126
+
127
+ - **`useTableFilter`** rewritten to support all three operators
128
+ with auto-coercion (number from string, date from ISO string /
129
+ Date instance). Exposed `passesFilter` / `toNumber` / `toDateMs`
130
+ for testing. 28 new unit tests cover the helpers across all
131
+ operator + value-type combinations.
132
+
133
+ - **`useColumnAutoDetect`** is now also the source of truth for the
134
+ filter UI dispatch — `inferredType` maps to `TableFilterType`
135
+ via the new `resolveFilterType` helper inside `DataGrid.tsx`.
136
+
137
+ - **Bundle impact**: +16 KB raw (~+5 KB gz) on top of
138
+ `0.7.0-beta` — the new filter UI, visibility dialog, resize, and
139
+ reorder code paths combined. Zero new runtime deps.
140
+
141
+ ### Migration
142
+
143
+ No code changes required:
144
+
145
+ ```bash
146
+ pnpm up @dashforge/tw@^0.8.0-beta
147
+ ```
148
+
149
+ To adopt the new v1-bis features, opt in per-column / per-grid:
150
+
151
+ ```tsx
152
+ <DataGrid
153
+ rows={users}
154
+ cols={[
155
+ { field: 'name',
156
+ header: 'Name',
157
+ sticky: 'left',
158
+ hideable: false }, // required column
159
+
160
+ { field: 'age',
161
+ header: 'Age',
162
+ filterable: true }, // → number range filter (auto)
163
+
164
+ { field: 'joinedAt',
165
+ header: 'Joined',
166
+ filterable: true }, // → date range filter (auto)
167
+
168
+ { field: 'active',
169
+ header: 'Active',
170
+ filterable: true,
171
+ defaultHidden: true }, // hidden until user re-shows
172
+
173
+ { field: 'actions',
174
+ header: 'Actions',
175
+ sticky: 'right', // pinned to right edge
176
+ resizable: false,
177
+ reorderable: false,
178
+ cellRenderer: ({ row }) => <RowActionsMenu row={row} ... /> },
179
+ ]}
180
+ getRowId={(r) => r.id}
181
+ rowHeight={48}
182
+ height="600px"
183
+
184
+ // Per-feature opt-out flags (all default true):
185
+ enableColumnVisibility
186
+ enableColumnResize
187
+ enableColumnReorder
188
+
189
+ // Controllable state for persistence:
190
+ hiddenColumns={hidden} onHiddenColumnsChange={setHidden}
191
+ columnWidths={widths} onColumnWidthsChange={setWidths}
192
+ columnOrder={order} onColumnOrderChange={setOrder}
193
+ />
194
+ ```
195
+
196
+ `<Table>` (non-virtualized) is **unchanged** in `0.8.0-beta` —
197
+ the in-header v1-bis features are DataGrid-only for now. Promoting
198
+ them to Table is on the v1-bis roadmap.
199
+
200
+ ### Heads-up — companion releases coming
201
+
202
+ - **Sprint 5 → `0.9.0-beta`** + starter kits v1: separate repos
203
+ `dashforge-starter-mui` + `dashforge-starter-tw` with Auth + RBAC
204
+ + form CRUD + dashboard with DataGrid admin views.
205
+ - **Sprint 6 → `1.0.0-rc.1 → 1.0.0`**: final A11Y audit, bundle
206
+ lockdown, beta freeze, cut `1.0.0`.
207
+
208
+ ## [0.7.0-beta] — 2026-05-19
209
+
210
+ **Combined Sprint 4.2 + 4.3 release.** Ships **`<DataGrid>`** — a
211
+ virtualized data table for large data sets (500 rows to millions)
212
+ — alongside a **theme identity sweep** across the whole catalog
213
+ (systemic dark-mode contrast fixes on 10 existing components).
214
+
215
+ Two coherent themes, one release:
216
+
217
+ 1. **DataGrid (Sprint 4.2)** — sibling to `<Table>` with **homemade
218
+ virtualization** (no `@tanstack/react-virtual`, no
219
+ `react-window`). Same column model, same cell renderer library,
220
+ same RBAC integration, same identity-consistent visual design as
221
+ Table. Differs at the render strategy level: only the window of
222
+ visible rows mounts in DOM. Adds sticky-left columns, server-side
223
+ mode (4 independent opt-in flags), `selectAllScope`, and optional
224
+ internal pagination.
225
+
226
+ 2. **Theme identity sweep (Sprint 4.3)** — applies the identity rule
227
+ codified in Sprint 4.1 to the WHOLE catalog. 10 existing
228
+ components (Typography, Box, Dialog, Divider, Pagination,
229
+ Popover, Skeleton, Tabs, Tooltip, plus Table-side comments)
230
+ had latent `dark:` variants on the neutral palette → those
231
+ classes double-inverted via the dashforgePreset CSS-var swap
232
+ and broke dark mode. Sweep removes the anti-pattern across the
233
+ catalog and adds a package-level regression test
234
+ (`_shared/themeIdentity.test.ts`) that scans every source file
235
+ on every test run.
236
+
237
+ **Minor bump** for the new DataGrid public API surface. Strictly
238
+ additive — zero breaking changes on existing component APIs.
239
+ Drop-in upgrade from `0.6.0-beta`.
240
+
241
+ ### Added
242
+
243
+ - **`<DataGrid>`** — virtualized data table for large data sets.
244
+ Sibling to `<Table>`. Required props: `rows`, `cols`,
245
+ `getRowId`, `rowHeight` (fixed for v1), `height` (bounded
246
+ container).
247
+ - **Homemade virtualization** via the new `useVirtualizer` hook
248
+ in `_shared/data/`. Scroll-event based with
249
+ `requestAnimationFrame` debounce (60 fps throttling) +
250
+ `ResizeObserver` for container resize. Spacer-row approach
251
+ preserves `<table>` semantics — screen readers see continuous
252
+ structure with `aria-hidden` spacers above/below the visible
253
+ window. Variable row height deferred to v1-bis.
254
+ - **Sticky left column** via `cols[i].sticky: 'left'`. CSS
255
+ `position: sticky; left: 0` with z-index ladder for the
256
+ top-left corner intersection with the sticky header
257
+ (`thead` z-10 < sticky cell z-[1] < sticky header corner z-20).
258
+ Right-sticky deferred to v1-bis.
259
+ - **Server-side mode flags** (4 independent opt-ins):
260
+ - `serverSideSort` — emits `onSortChange` but doesn't sort locally
261
+ - `serverSideFilter` — emits `onFilterChange` but doesn't filter
262
+ - `serverSideSearch` — emits `onSearchQueryChange` (debounced)
263
+ - `serverSidePagination` — uses `totalCount` for the virtual
264
+ scrollbar math; `rows` is the server-returned page slice
265
+ - **`selectAllScope`** prop (`'visible' | 'allLoaded'`, default
266
+ `'allLoaded'`) controls the header "select all" checkbox
267
+ semantic.
268
+ - **Optional internal pagination** via the `pagination` prop —
269
+ renders the Sprint 4 `<Pagination>` component below the
270
+ virtualized scroll. Composes naturally with virtualization
271
+ (each page is virtualized internally if large enough).
272
+ - **Bundle impact**: +8.4 KB gz on top of `0.6.0-beta`
273
+ (`<Table>` baseline). Zero new runtime deps.
274
+ - **Variants**: `plain` / `lines` (default) / `striped` /
275
+ `bordered`. **No `card` variant** — incompatible with
276
+ `<table>` virtualization spacer-rows.
277
+ - 24 unit tests + 12 `useVirtualizer` math tests + dash smoke
278
+ test at `/test-data-grid` with 10 000 generated rows.
279
+
280
+ - **`useVirtualizer` hook** in `_shared/data/` — exported
281
+ internally only (not in the public API surface). Other future
282
+ virtualized components (TreeView, future GanttGrid, etc.) can
283
+ reuse it.
284
+
285
+ - **`sticky?: 'left' | 'right'`** added to `TableColumn<T>` for
286
+ DataGrid integration. Currently ignored by Table (non-virtualized);
287
+ honored by DataGrid v1 for `'left'` only.
288
+
289
+ - **Doc MDX `/tw/docs/components/data-grid`** — emphasizes the
290
+ Table vs DataGrid decision matrix (24-row comparison table +
291
+ "Use Table when..." / "Use DataGrid when..." + "Switch later"
292
+ path showing the 3-line diff to migrate).
293
+
294
+ ### Changed
295
+
296
+ - **Internal refactor — shared data helpers moved**:
297
+ `Table/_internal/` → `_shared/data/` (13 files). The helpers
298
+ (`getNestedValue`, `useTableSearch`, `useTableSort`,
299
+ `useTableSelection`, `useTableFilter`, `useColumnAutoDetect`,
300
+ `useDebouncedValue`, `useControllableState`) are now shared
301
+ between Table and DataGrid. **Pure path move** — function names
302
+ unchanged. Public API surface unchanged (only `getNestedValue`
303
+ was ever re-exported; the re-export path updated transparently).
304
+
305
+ - **Theme identity sweep** — 10 existing components had latent
306
+ `dark:` Tailwind variants on the neutral palette, which the
307
+ dashforgePreset CSS-var swap double-inverted and broke in dark
308
+ mode. Removed across the catalog:
309
+
310
+ | Component | Sites fixed | Pattern category |
311
+ |---|---|---|
312
+ | Accordion (`accordion.variants.ts`) | 3 | A (drop `dark:` — auto-invert) |
313
+ | Box (`box.variants.ts`) | 4 | A + B (corrected dark targets) + D (solid neutral now auto-inverts as accent) |
314
+ | Dialog (`dialog.variants.ts`) | 6 | A × 5 + B × 1 |
315
+ | Divider (`divider.variants.ts` + `Divider.tsx`) | 2 | A × 2 |
316
+ | Pagination (`pagination.variants.ts` + `Pagination.tsx`) | 10 | A × 7 + B × 3 (`dark:bg-neutral-900` → `bg-neutral-50` auto-invert) |
317
+ | Popover (`popover.variants.ts`) | 4 | A × 3 + B × 1 |
318
+ | Skeleton (`skeleton.variants.ts`) | 1 | A |
319
+ | Tabs (`tabs.variants.ts`) | 8 | A × 7 + B × 1 (`dark:bg-neutral-900` → `dark:bg-neutral-200` for proper active-pill elevation) |
320
+ | 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). |
321
+ | Typography (`typography.variants.ts`) | 2 | A × 2 |
322
+
323
+ **Box solid neutral** now uses `bg-neutral-900 text-neutral-50`
324
+ (both auto-invert) — the accent surface flips with the page in
325
+ dark mode (light surface + dark text) for identity consistency.
326
+ Previously was `bg-neutral-900 text-white dark:bg-neutral-100
327
+ dark:text-neutral-900` — the `text-white` (no auto-invert) had
328
+ forced the double-invert pattern to stay readable.
329
+
330
+ Affected test files updated to assert the new canonical
331
+ patterns (Box × 4 + Typography × 1 + Divider × 1 = 6 tests
332
+ retargeted).
333
+
334
+ ### Internal
335
+
336
+ - **`THEME-AUDIT.md`** at package root — categorizes every site
337
+ in the catalog as **A** (drop `dark:` — auto-invert), **B**
338
+ (keep `dark:` because `bg-white`/`fill-white` is hardcoded),
339
+ **C** (color palette — intentional design choice, not affected
340
+ by the rule), or **D** (component-specific exception). 42
341
+ violations catalogued across 10 components.
342
+
343
+ - **Package-level theme identity regression test** —
344
+ `_shared/themeIdentity.test.ts` replaces the Table-only one
345
+ from Sprint 4.1. Scans every `.ts`/`.tsx` source file under
346
+ `src/components/**` (excluding tests) on every test run.
347
+ Allows the Category B legitimate pattern
348
+ (`bg-white dark:bg-neutral-N` — `bg-white` doesn't auto-invert,
349
+ the `dark:` is required). Catches accidental re-introductions
350
+ of the anti-pattern at PR time.
351
+
352
+ - **+76 new unit tests** for Sprint 4.2 + 4.3:
353
+ - `useVirtualizer` math (12) — window calc, overscan, padding
354
+ invariant, edge cases (totalCount=0, very large dataset)
355
+ - `<DataGrid>` (24) — rendering, virtualized window, sort,
356
+ search, selection, selectAllScope, sticky left column,
357
+ server-side mode flags, RBAC, internal pagination, smart
358
+ defaults, i18n
359
+ - `_shared/themeIdentity.test.ts` package-level scanner (40
360
+ file-scan tests; 0 violations reported)
361
+
362
+ Full TW suite at **961/961 passing** across **48 files**.
363
+
364
+ - **`TestDataGrid.tsx`** added to the `dash` consumer
365
+ (`/test-data-grid`) — 10 000 generated rows + sticky col +
366
+ selectAllScope toggle + variant/density/size switcher + RBAC
367
+ per-column demo + custom empty state + React.Profiler logging.
368
+
369
+ - **Sidebar entries** for the new `DataGrid` doc page added to
370
+ `dashforge-docs-lab/src/tw-docs/sidebar.model.ts` (new "Data"
371
+ subgroup under "UI Components" now contains Table + DataGrid).
372
+
373
+ ### Compatibility
374
+
375
+ | Axis | Pre-`0.7.0` | Post-`0.7.0` |
376
+ |---|---|---|
377
+ | Public API surface | 32 components | **+1 `<DataGrid>` + types (`DataGridProps`, `DataGridSelectAllScope`, `DataGridServerSideFlags`, `DataGridPaginationConfig`, `DataGridSlotProps`) + `dataGridVariants` recipe** + `sticky` axis on `TableColumn<T>` (additive, optional) |
378
+ | Peer deps | `react ^18 \|\| ^19`, `tw-theme workspace`, `tw-tokens workspace` | unchanged |
379
+ | Bridge deps | `forms` / `rbac` / `ui-core` `workspace:*` | unchanged |
380
+ | **New runtime deps** | — | **none** (homemade virtualization — constraint honored) |
381
+ | 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). |
382
+ | 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 |
383
+ | Tests passing | 828/828 (46 files) | **961/961 (48 files)** — +133 from Sprint 4.2 + 4.3 |
384
+
385
+ > **Bundle sign-off** : the +8.1% gz delta is JUST under the 10%
386
+ > threshold from PERFORMANCE.md. Justification: DataGrid is the
387
+ > highest-leverage addition before 1.0 — it unlocks the entire
388
+ > "1 000+ row admin views" use case at €0 vs MUI X DataGrid Pro's
389
+ > €499/year. The theme identity sweep is net-neutral on bundle
390
+ > (removed redundant classes, replaced with smaller ones); the
391
+ > delta is entirely DataGrid + virtualizer code (+8 KB gz).
392
+
393
+ ### Migration
394
+
395
+ No code changes required:
396
+
397
+ ```bash
398
+ pnpm up @dashforge/tw@^0.7.0-beta
399
+ ```
400
+
401
+ To adopt the new DataGrid:
402
+
403
+ ```tsx
404
+ import {
405
+ DataGrid,
406
+ RenderTwoLine,
407
+ RenderChip,
408
+ RowActionsMenu,
409
+ } from '@dashforge/tw';
410
+
411
+ <DataGrid
412
+ rows={users} // can be 10k, 100k, millions
413
+ cols={columns} // SAME shape as Table's TableColumn<T>
414
+ getRowId={(r) => r.id}
415
+ rowHeight={48} // required for virtualization
416
+ height="600px" // required for bounded container
417
+ enableSearch
418
+ rowSelection="multiple"
419
+ selectAllScope="allLoaded"
420
+ bulkActions={(rows) => (
421
+ <Button color="danger">Delete {rows.length}</Button>
422
+ )}
423
+ rowActions={(row) => (
424
+ <RowActionsMenu
425
+ row={row}
426
+ actions={[
427
+ { label: 'Edit', onClick: edit },
428
+ { label: 'Delete', onClick: del, color: 'danger' },
429
+ ]}
430
+ />
431
+ )}
432
+ />
433
+ ```
434
+
435
+ To switch an existing Table to DataGrid when you outgrow the row
436
+ count (~500+ rows), the migration is mechanical (3 lines):
437
+
438
+ ```diff
439
+ - <Table
440
+ + <DataGrid
441
+ rows={users}
442
+ cols={columns}
443
+ getRowId={(row) => row.id}
444
+ + rowHeight={48}
445
+ + height="600px"
446
+ />
447
+ ```
448
+
449
+ Everything else (sort / search / selection / row actions / cell
450
+ renderers / RBAC / i18n labels / sx / slotProps) keeps working
451
+ identically. **Expandable rows** are the one feature you lose
452
+ on Table→DataGrid swap — they ship in DataGrid v1-bis.
453
+
454
+ ### Heads-up — companion releases coming
455
+
456
+ - **Sprint 4.2-bis → `0.8.0-beta`** (estimated): DataGrid v1-bis —
457
+ per-column filter UI chips (text/number/boolean/date), column
458
+ resize via drag, column reorder via drag, column visibility
459
+ dialog, right-sticky columns.
460
+ - **Sprint 5 → `0.9.0-beta`** + starter kits v1: separate repos
461
+ `dashforge-starter-mui` + `dashforge-starter-tw` with Auth + RBAC
462
+ + form CRUD + dashboard with DataGrid admin views.
463
+ - **Sprint 6 → `1.0.0-rc.1 → 1.0.0`**: final A11Y audit, bundle
464
+ lockdown, beta freeze, cut `1.0.0`.
465
+
15
466
  ## [0.6.0-beta] — 2026-05-19
16
467
 
17
468
  **Sprint 4.1 release.** Ships the central piece of the data layer
package/THEME-AUDIT.md ADDED
@@ -0,0 +1,175 @@
1
+ # THEME-AUDIT — @dashforge/tw catalog identity sweep
2
+
3
+ > Sprint 4.3 deliverable (2026-05-19). Audit of every `@dashforge/tw`
4
+ > component against the **default `dashforgePreset()` identity rule**
5
+ > (memory: `feedback_dashforge_preset_is_identity`).
6
+ >
7
+ > **Rule recap.** The `dashforgePreset()` CSS-variable swap
8
+ > auto-inverts the neutral palette:
9
+ > `bg-neutral-50` / `text-neutral-900` etc. resolve to the
10
+ > appropriate physical color via `--df-tw-color-neutral-N`. Adding a
11
+ > `dark:` variant on a neutral class **double-inverts** the value
12
+ > and breaks dark mode. The Sprint 4.1 Table fix established the
13
+ > canonical patterns; Sprint 4.3 applies them to the whole catalog.
14
+
15
+ ## Scoring legend
16
+
17
+ - **A** — Pure double inversion. REMOVE `dark:` variant entirely.
18
+ - **B** — `bg-white` / `text-white` paired with a wrong `dark:`
19
+ target. KEEP the `dark:` variant (white doesn't auto-invert) but
20
+ FIX the target — `dark:bg-neutral-900` in dark mode resolves to
21
+ near-white (broken); use `dark:bg-neutral-100` for proper dark
22
+ elevated surface.
23
+ - **C** — Intentional `dark:` on color palette (`primary`,
24
+ `success`, `warning`, `danger`, `info`, `secondary`). KEEP — color
25
+ palettes don't auto-invert; explicit `dark:` is a design choice.
26
+ - **D** — Component-specific exception (e.g. Tooltip wants
27
+ high-contrast inversion against the page). Document why kept.
28
+
29
+ ## Per-file findings
30
+
31
+ ### Accordion (`accordion.variants.ts`)
32
+
33
+ | Line | Class | Category | Fix |
34
+ |---|---|---|---|
35
+ | 6 | `border-b border-neutral-200 dark:border-neutral-800` | A | `border-b border-neutral-200` |
36
+ | 10 | `text-sm font-medium text-neutral-900 dark:text-neutral-50` | A | `text-sm font-medium text-neutral-900` |
37
+ | 18 | `overflow-hidden text-sm text-neutral-700 dark:text-neutral-300` | A | drop `dark:text-neutral-300` |
38
+
39
+ ### Box (`box.variants.ts`)
40
+
41
+ | Line | Class | Category | Fix |
42
+ |---|---|---|---|
43
+ | 58 | `bg-white dark:bg-neutral-900` (elevated) | B | `bg-white dark:bg-neutral-100` (dark-mode elevation tier above page surface) |
44
+ | 168 | `border-neutral-200 bg-white dark:border-neutral-700 dark:bg-neutral-900` (outlined) | A + B | `border-neutral-200 bg-white dark:bg-neutral-100` (border auto-inverts; bg keeps `bg-white` + corrected dark target) |
45
+ | 184 | `bg-neutral-100 text-neutral-900 dark:bg-neutral-800 dark:text-neutral-100` (soft) | A | `bg-neutral-100 text-neutral-900` |
46
+ | 200 | `bg-neutral-900 text-white dark:bg-neutral-100 dark:text-neutral-900` (solid neutral) | D | `bg-neutral-900 text-neutral-50` (auto-invert — solid neutral becomes inverted accent: dark surface in light mode, light surface in dark mode; consistent with identity rule) |
47
+
48
+ ### Dialog (`dialog.variants.ts`)
49
+
50
+ | Line | Class | Category | Fix |
51
+ |---|---|---|---|
52
+ | 29 | `w-full bg-white dark:bg-neutral-900 rounded-lg shadow-xl` | B | `dark:bg-neutral-100` |
53
+ | 30 | `border border-neutral-200 dark:border-neutral-800` | A | drop `dark:` |
54
+ | 37 | `text-lg ... text-neutral-900 dark:text-neutral-50` | A | drop `dark:` |
55
+ | 38 | `text-sm text-neutral-600 dark:text-neutral-400` | A | drop `dark:` |
56
+ | 42 | `dark:text-neutral-400 dark:hover:text-neutral-50` (close button) | A | drop both `dark:` |
57
+ | 43 | `hover:bg-neutral-100 dark:hover:bg-neutral-800` | A | drop `dark:` |
58
+
59
+ ### Divider (`divider.variants.ts` + `Divider.tsx`)
60
+
61
+ | File / Line | Class | Category | Fix |
62
+ |---|---|---|---|
63
+ | `divider.variants.ts:88` | `border-neutral-200 dark:border-neutral-800` (neutral color variant) | A | drop `dark:` |
64
+ | `Divider.tsx:131` | `text-sm text-neutral-500 dark:text-neutral-400` (label) | A | drop `dark:` |
65
+
66
+ ### Pagination (`pagination.variants.ts` + `Pagination.tsx`)
67
+
68
+ 10 violations — full sweep of borders / text / bg / hover / active-button.
69
+
70
+ | Line | Class | Category | Fix |
71
+ |---|---|---|---|
72
+ | `variants:26` summary | `text-neutral-600 dark:text-neutral-400` | A | drop |
73
+ | `variants:30, 44, 58` borders | `border border-neutral-300 dark:border-neutral-700` | A | drop |
74
+ | `variants:31, 45, 59` button/selector/input bg | `bg-white dark:bg-neutral-900` | B → drop entirely | use `bg-neutral-50` (auto-inverts; border provides separation against page) |
75
+ | `variants:32, 46, 55` button text | `text-neutral-700 dark:text-neutral-300` | A | drop |
76
+ | `variants:33, 47` button hover bg | `hover:bg-neutral-50 dark:hover:bg-neutral-800` | A | `hover:bg-neutral-100` (1 elevation tier; auto-inverts) |
77
+ | `variants:60` jumpInput text | `text-neutral-900 dark:text-neutral-50` | A | drop |
78
+ | `Pagination.tsx:241` select bg+border | `border ... dark:border-neutral-700 bg-white dark:bg-neutral-900` | B → drop | `border border-neutral-300 bg-neutral-50` |
79
+
80
+ ### Popover (`popover.variants.ts`)
81
+
82
+ | Line | Class | Category | Fix |
83
+ |---|---|---|---|
84
+ | 6 | `border border-neutral-200 dark:border-neutral-800` | A | drop |
85
+ | 7 | `bg-white dark:bg-neutral-900 p-4 shadow-lg` | B | `dark:bg-neutral-100` |
86
+ | 8 | `text-sm text-neutral-900 dark:text-neutral-50` | A | drop |
87
+ | 14 | `fill-white dark:fill-neutral-900` (arrow) | B | `fill-white dark:fill-neutral-100` (match content bg) |
88
+
89
+ ### Skeleton (`skeleton.variants.ts`)
90
+
91
+ | Line | Class | Category | Fix |
92
+ |---|---|---|---|
93
+ | 38 | `bg-neutral-200 dark:bg-neutral-800` | A | drop `dark:` (auto-inverts — skeleton placeholder slightly more visible than page surface in both modes) |
94
+
95
+ ### Tabs (`tabs.variants.ts`)
96
+
97
+ | Line | Class | Category | Fix |
98
+ |---|---|---|---|
99
+ | 22 | `border-b border-neutral-200 dark:border-neutral-800` (underline list) | A | drop |
100
+ | 24 | `... text-neutral-600 dark:text-neutral-400` (underline trigger) | A | drop |
101
+ | 25 | `hover:text-neutral-900 dark:hover:text-neutral-50` | A | drop |
102
+ | 31 | `bg-neutral-100 dark:bg-neutral-800 p-1` (pill list) | A | drop |
103
+ | 33 | `... text-neutral-600 dark:text-neutral-400` (pill trigger) | A | drop |
104
+ | 34 | `hover:text-neutral-900 dark:hover:text-neutral-50` | A | drop |
105
+ | 35 | `data-[state=active]:bg-white dark:data-[state=active]:bg-neutral-900` (pill active) | B | `dark:bg-neutral-100` |
106
+ | 36 | `data-[state=active]:text-neutral-900 dark:data-[state=active]:text-neutral-50` | A | drop |
107
+
108
+ ### Tooltip (`tooltip.variants.ts`)
109
+
110
+ Tooltip historically used a "fixed dark surface in both modes" pattern.
111
+ Per the identity rule, **auto-invert** is the right behavior: dark
112
+ tooltip on light page (light mode), light tooltip on dark page (dark
113
+ mode) — the inversion **is** the high-contrast effect.
114
+
115
+ | Line | Class | Category | Fix |
116
+ |---|---|---|---|
117
+ | 8 | `bg-neutral-900 text-neutral-50` + `dark:bg-neutral-50 dark:text-neutral-900` | A | drop the `dark:` line entirely (the base classes already auto-invert and give the high-contrast effect against the page) |
118
+ | 14 | `fill-neutral-900 dark:fill-neutral-50` (arrow) | A | drop `dark:` (arrow auto-inverts with content bg) |
119
+
120
+ ### Typography (`typography.variants.ts`)
121
+
122
+ | Line | Class | Category | Fix |
123
+ |---|---|---|---|
124
+ | 65 | `neutral: 'text-neutral-900 dark:text-neutral-100'` | A | `'text-neutral-900'` |
125
+ | 66 | `muted: 'text-neutral-600 dark:text-neutral-400'` | A | `'text-neutral-600'` |
126
+
127
+ ### Table-side (Sprint 4.1 — already fixed)
128
+
129
+ Per-file already corrected in Sprint 4.1. Sprint 4.3 just verifies
130
+ the regression guard (themeIdentity test) is promoted to global so
131
+ the rest of the catalog is covered too.
132
+
133
+ ## Summary
134
+
135
+ | Component | Violations | Fix categories |
136
+ |---|---|---|
137
+ | Accordion | 3 | A × 3 |
138
+ | Box | 4 | A + B + D |
139
+ | Dialog | 6 | A × 5 + B × 1 |
140
+ | Divider | 2 | A × 2 |
141
+ | Pagination | 10 | A × 7 + B × 3 |
142
+ | Popover | 4 | A × 3 + B × 1 |
143
+ | Skeleton | 1 | A × 1 |
144
+ | Tabs | 8 | A × 7 + B × 1 |
145
+ | Tooltip | 2 | A × 2 (was "double-inverted to preserve fixed look" — auto-invert is now the canonical choice) |
146
+ | Typography | 2 | A × 2 |
147
+ | **Total** | **42** | **31 A + 5 B + 1 D + 5 fixed already** |
148
+
149
+ ## Color palettes — NOT touched
150
+
151
+ `dark:` variants on `primary`, `secondary`, `success`, `warning`,
152
+ `danger`, `info` palettes are **intentional** — those palettes do
153
+ not auto-invert (same physical color in both modes), so the
154
+ `dark:` shift is a design choice for tone refinement. They stay.
155
+
156
+ Affected sites (kept as-is):
157
+
158
+ - `Box` compound variants (`outlined/soft/solid` × color) for the
159
+ 6 color palettes (lines 188-198 in `box.variants.ts`)
160
+ - `RenderChip` color rows (Sprint 4.1 decision)
161
+ - `RowActionsMenu` `text-danger-700 dark:text-danger-300` and same
162
+ for warning
163
+
164
+ ## Sprint 4.3 deliverables
165
+
166
+ 1. ✅ THEME-AUDIT.md (this file)
167
+ 2. ⏳ Apply fixes per-file (P3)
168
+ 3. ⏳ Promote `themeIdentity.test.ts` from `Table/_internal/` to
169
+ package-level scanner walking `libs/dashforge/tw/src/components/**`
170
+ (P2). Excludes `*.test.*` files. The test fails any future
171
+ `dark:*-neutral-N` reintroduction.
172
+ 4. ⏳ Visual regression catch in `dash` (manual check in both
173
+ light + dark mode)
174
+ 5. ⏳ Release `@dashforge/tw@0.7.0-beta` — expected bundle to
175
+ **shrink** by 1-3 KB gz (removing redundant classes).