Browse docs
Browse docs
Released 2026-05-19 · Combined Sprint 4.2 + 4.3 release
Two coherent themes shipped in one release:
DataGrid (Sprint 4.2) — virtualized data table for large
data sets, sibling to <Table>. Same column model, same cell
renderer library, same identity-consistent design — different
render strategy (windowed). Built with homemade
virtualization (useVirtualizer hook: scroll-event +
requestAnimationFrame debounce + ResizeObserver) — zero
new runtime deps.
Theme identity sweep (Sprint 4.3) — applies the identity
rule codified in Sprint 4.1 to the whole catalog. 42 latent
dark: Tailwind variant anti-patterns silently broke dark
mode via the dashforgePreset CSS-var double-inversion. Fixed
across 10 components + new package-level regression test that
catches re-introductions at build time.
Strictly additive minor bump. Drop-in upgrade from
0.6.0-beta — zero breaking changes on public APIs (internal
helpers moved from Table/_internal/ to _shared/data/; only
externally-visible re-export was getNestedValue, name preserved).
| Package | What changed |
|---|---|
@dashforge/tw | + <DataGrid> (virtualized table) + useVirtualizer hook + theme identity sweep across 10 components + package-level regression guard + sticky axis on TableColumn<T> (additive). 961/961 unit tests across 48 files (+133 from this release). Bundle: 445 KB raw / 98.4 KB gzipped (+8.1% gz vs 0.6.0-beta). |
Unchanged (independent versioning):
| Package | Version (unchanged) | Why |
|---|---|---|
@dashforge/tw-theme | 0.1.0-beta | No source change. |
@dashforge/tw-tokens | 0.1.0-beta | No source change. |
Bridge (forms, rbac, ui-core) | 0.2.3-beta | Shared with MUI side; no source change. |
MUI side (ui, theme-mui, theme-core, tokens) | 0.2.3-beta | Separate ecosystem; untouched. |
<DataGrid> — virtualized data tableimport { DataGrid } from '@dashforge/tw';
<DataGrid
rows={users} // 10k, 100k, or millions
cols={[
{ field: 'name', header: 'Name', sortable: true, searchable: true,
sticky: 'left' }, // pinned during horizontal scroll
{ field: 'email', header: 'Email', searchable: true },
{ field: 'age', header: 'Age', sortable: true },
]}
getRowId={(row) => row.id}
rowHeight={48} // fixed row height (required for virt math)
height="600px" // bounded container (required)
enableSearch
rowSelection="multiple"
selectAllScope="allLoaded" // or 'visible'
/>Both ship the same visual identity (Stripe-style clean lines, hover row actions, sticky header, smart defaults, canonical theme patterns). They differ at the runtime characteristics level:
| Dimension | <Table> | <DataGrid> |
|---|---|---|
| Render strategy | All rows in DOM | Windowed — only visible + spacer rows |
| Row count target | ≤ ~500 | 500 → millions |
| Row height | Auto-fit content | Fixed (rowHeight required) |
| Container height | Auto-fit | Must be bounded (height required) |
| Sticky LEFT column | ✗ | ✓ cols[i].sticky: 'left' |
| Expandable rows | ✓ | ✗ v1 (v1-bis) |
| Server-side mode | ✗ | ✓ 4 independent flags |
| Internal pagination | ✗ | ✓ opt-in |
selectAllScope | n/a | 'visible' / 'allLoaded' |
card variant | ✓ | ✗ (incompatible with virt spacer rows) |
| Bundle cost | (<Table> baseline) | +8.4 KB gz on top |
Full decision matrix + "Use when..." + "Switch later" path in the DataGrid docs page.
useVirtualizer hook in _shared/data/:
requestAnimationFrame debounce → bounded to 60 fps even on
fast scrollingResizeObserver → re-virtualize on window resize / sidebar
toggle<tr aria-hidden="true"> above + below
visible window) preserves <table> semantics + a11yPerformance: 10 000-row data set with rowHeight=48 +
height="600px" mounts ~18 <tr> (13 visible + 5 overscan) + 2
spacers. Scroll FPS bounded to 60.
<DataGrid
rows={pageOfRows} // server-returned slice
serverSideSort // emits onSortChange, no local sort
serverSideFilter // emits onFilterChange, no local filter
serverSideSearch // emits onSearchQueryChange (debounced)
serverSidePagination // uses totalCount for virt math
totalCount={1_000_000} // required when serverSidePagination
/>Mix and match independently. Use serverSideSort alone if filter
happens locally but sort hits the backend.
selectAllScopeThe header "select all" checkbox has two reasonable semantics in large data grids:
'allLoaded' (default) — toggles all rows in the rows prop'visible' — toggles only the viewport windowFor "select all in server-side dataset", the consumer wires it
themselves (fetch all ids + setSelectedRowIds(allIds)).
Via cols[i].sticky: 'left'. CSS-only (position: sticky; left: 0) with a z-index ladder for the top-left corner
intersection with the sticky header. Multiple sticky-left columns
supported, stack left-to-right. Right-sticky deferred to v1-bis.
<DataGrid
pagination={{
page,
pageSize,
onPageChange: setPage,
onPageSizeChange: setPageSize,
}}
/>Renders the Sprint 4 <Pagination> component below the scroll
container. Composes naturally with virtualization (each page is
virtualized internally if large enough).
useVirtualizer hookNew helper in _shared/data/ — exported internally only. Other
future virtualized components (TreeView, future GanttGrid, etc.)
can reuse it.
sticky?: 'left' | 'right' on TableColumn<T>Additive — Table (non-virtualized) ignores it; DataGrid honors
'left' in v1.
10 components had latent dark: Tailwind variants on the neutral
palette. The dashforgePreset() CSS-variable swap auto-inverts the
neutral palette already (bg-neutral-50 is light in light mode,
dark in dark mode). Adding dark:bg-neutral-N (or text-, border-,
ring-) creates a double inversion that breaks dark mode.
42 violations fixed across:
| Component | Sites | Most visible impact |
|---|---|---|
| Tooltip | 2 | Bug fix — tooltip was previously invisible in dark mode (same dark surface as page). Now auto-inverts to light tooltip on dark page (high contrast preserved). |
| Typography | 2 | text-neutral-900 / text-neutral-600 (muted) — body text now legible in dark mode. |
| Box | 4 | Elevated / outlined / soft / solid neutral variants — dark mode surfaces now correctly elevated instead of overpoweringly white. |
| Dialog | 6 | Content bg + border + title + description + close button — all auto-invert correctly. |
| Pagination | 10 | Buttons, borders, hover states, jump-input — clean dark mode rendering. |
| Tabs | 8 | Underline + pill variants in both modes. |
| Popover | 4 | Content bg + border + text + arrow fill. |
| Skeleton | 1 | bg-neutral-200 auto-inverts (was double-inverted). |
| Accordion | 3 | Item border + trigger text + content text. |
| Divider | 2 | Neutral border + label text. |
Categorization:
dark: — auto-invert via CSS var swap): 31 sitesdark: — bg-white / fill-white is hardcoded, dark target needed; fixed wrong targets like dark:bg-neutral-900 → dark:bg-neutral-100 for proper dark elevation): 5 sitesBox solid neutral now auto-inverts as accent surface, consistent with the identity rule): 1 siteCatalogued in detail in libs/dashforge/tw/THEME-AUDIT.md.
Table/_internal/ → _shared/data/ (13 files). The helpers
(getNestedValue, useTableSearch, useTableSort,
useTableSelection, useTableFilter, useColumnAutoDetect,
useDebouncedValue, useControllableState) are now shared
between Table and DataGrid. Pure path move — function names
unchanged, public API surface unchanged (only getNestedValue
was ever re-exported; the re-export path updated transparently).
Previously was bg-neutral-900 text-white dark:bg-neutral-100 dark:text-neutral-900 — text-white (no auto-invert) had forced
the double-invert pattern to stay readable.
Now: bg-neutral-900 text-neutral-50 (both auto-invert) — the
accent surface flips with the page in dark mode (light surface +
dark text) for identity consistency. If you need a fixed
always-dark accent, override with sx="bg-neutral-900 text-white"
explicitly — but most use cases will prefer the inverting behavior.
New _shared/themeIdentity.test.ts replaces the Table-only one
from Sprint 4.1. Scans every .ts/.tsx source file under
src/components/** (excluding tests). The test FAILS if any
dark:*-neutral-N anti-pattern is introduced — except for the
legitimate Category B pattern (bg-white dark:bg-neutral-N —
bg-white doesn't auto-invert, so the dark: IS required).
40+ source files scanned per test run. ~16 ms overhead. Catches accidental re-introductions of the anti-pattern at PR time.
New file at the package root. Catalogues every site touched in
Sprint 4.3 with categorization (A/B/C/D), the offending class,
and the canonical replacement. Mirrors the format of A11Y.md /
PARITY.md / PERFORMANCE.md.
+133 new unit tests for Sprint 4.2 + 4.3:
useVirtualizer math (12) — window calc, overscan, padding
invariant, edge cases (totalCount=0, very large dataset)<DataGrid> (24) — rendering, virtualized window, sort,
search, selection, selectAllScope, sticky left column,
server-side mode flags, RBAC, internal pagination, smart
defaults, i18n_shared/themeIdentity.test.ts package-level scanner (40
file-scan tests; 0 violations reported)Full TW suite at 961/961 passing across 48 files.
| Metric | 0.6.0-beta | 0.7.0-beta | Δ |
|---|---|---|---|
| Raw | 402 KB | 445 KB | +43 KB |
| Gzipped | 91 KB | 98.4 KB | +7.4 KB / +8.1% |
Just under the 10% reviewer-sign-off threshold from PERFORMANCE.md. The +8.1% is entirely DataGrid + virtualizer code; the theme identity sweep is net-neutral on bundle (removed redundant classes, replaced with smaller ones).
pnpm up @dashforge/tw@^0.7.0-betaNo code changes required. To adopt the new DataGrid:
import {
DataGrid,
RenderTwoLine,
RenderChip,
RowActionsMenu,
Button,
} from '@dashforge/tw';
<DataGrid
rows={users}
cols={[
{
field: 'name',
header: 'Name',
sortable: true,
searchable: true,
sticky: 'left',
cellRenderer: ({ row }) => (
<RenderTwoLine primary={row.name} secondary={row.email} />
),
},
// …
]}
getRowId={(r) => r.id}
rowHeight={48}
height="600px"
enableSearch
rowSelection="multiple"
selectAllScope="allLoaded"
bulkActions={(rows) => (
<Button color="danger">Delete {rows.length}</Button>
)}
/>3-line mechanical change:
- <Table
+ <DataGrid
rows={users}
cols={columns}
getRowId={(row) => row.id}
+ rowHeight={48}
+ height="600px"
/>Everything else keeps working (sort / search / selection / row actions / cell renderers / RBAC / i18n / sx / slotProps). One feature lost on swap: expandable rows (Table-specific until DataGrid v1-bis).
If your app has dark mode users, the theme identity sweep fixes the following bugs they may have been experiencing:
No consumer code changes needed — the fix is purely internal to the library variant recipes.
| Compatibility axis | Pre-0.7.0 | Post-0.7.0 |
|---|---|---|
| Public API surface | 32 components | +1 <DataGrid> + 5 new types (DataGridProps, DataGridSelectAllScope, DataGridServerSideFlags, DataGridPaginationConfig, DataGridSlotProps) + dataGridVariants recipe + sticky axis on TableColumn<T> (additive, optional) |
| Peer deps | react ^18 || ^19, tw-theme workspace, tw-tokens workspace | unchanged |
| Bridge deps | forms / rbac / ui-core workspace:* | unchanged |
| New runtime deps | — | none (homemade virtualization; constraint honored) |
| 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). |
| Bundle size | 402 KB raw / 91 KB gz | 445 KB raw / 98.4 KB gz (+8.1% gz; under 10% reviewer-sign-off threshold) |
| Tests passing | 828/828 (46 files) | 961/961 (48 files) — +133 from this release |
| Dark mode | broken on 10 components (latent dark: anti-pattern) | fixed across the catalog |
| Sprint | Release | Theme |
|---|---|---|
| Sprint 4.2-bis | @dashforge/[email protected] | DataGrid v1-bis — per-column filter UI chips (text/number/boolean/date), column resize, column reorder, column visibility dialog, right-sticky columns. |
| Sprint 5 | @dashforge/[email protected] + starter kits v1 | Two separate repos dashforge-starter-mui + dashforge-starter-tw — Auth + RBAC + form CRUD + dashboard with DataGrid admin views. Unlocks revenue model "kits paid + lib free". |
| Sprint 6 | @dashforge/[email protected] → 1.0.0 | Final A11Y audit (axe / lighthouse CI), bundle lockdown, beta freeze 4 weeks, cut 1.0.0. |