Browse docs
Browse docs
Released 2026-05-19 · Sprint 4.1 release
Sprint 4.1 ships <Table> — the central piece of the data
layer for @dashforge/tw. Built from scratch with zero new
runtime deps, market-grounded design (Stripe visuals, Atlassian
column UX, Pencil & Paper density tiers, W3C WAI Table a11y), full
RBAC integration, full i18n surface, and a cell renderer library
ready out of the box.
This release also codifies two architectural rules that emerged during Sprint 4.1 development — both now Dashforge persistent memory:
dark: variants on the neutral
palette = double inversion = breaks dark mode (canonical pattern:
auto-invert via CSS var swap; selected states use
bg-primary-100 text-primary-900 à la LeftNav itemActive).tabular-nums (font-feature setting for digit grid alignment)
yes, font-mono (font family override) only on explicit consumer
opt-in via col.monospace: true.The first rule is enforced in the Table sources via a regression
test (themeIdentity.test.ts). The second is encoded in the
tabularNums / monospace split on TableColumn<T>.
Strictly additive minor bump. Drop-in upgrade from 0.5.0-beta
— zero breaking changes.
| Package | What changed |
|---|---|
@dashforge/tw | + <Table> + 5 cell renderers (RenderText, RenderTwoLine, RenderChip, RenderButton, RowActionsMenu) + getNestedValue helper + Table* types. 828/828 unit tests across 46 files (+147 from Sprint 4.1). Bundle: 402 KB raw / 91 KB gzipped (+23% gz vs 0.5.0-beta; justified in PERFORMANCE.md as the lib's central data primitive). |
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. |
<Table> — the central data-display primitiveimport { Table } from '@dashforge/tw';
<Table
rows={users}
cols={[
{ field: 'name', header: 'Name', sortable: true, searchable: true },
{ field: 'email', header: 'Email', searchable: true },
{ field: 'age', header: 'Age', sortable: true },
]}
getRowId={(row) => row.id}
/>| Inferred type | Alignment | tabular-nums (digit grid) |
|---|---|---|
number | right | yes |
boolean | center | no |
Date / ISO string | left | yes |
string | left | no |
tabular-nums is a font-feature setting that makes every digit
the same width — currency / metric columns align cleanly without
imposing a font family on the consumer. The library never
changes the font family (see "font family rule" above);
monospace: true is an explicit per-column opt-in.
null /
undefined always sort to the end, regardless of asc/desc.
Avoids the "null at the top in desc" surprisesortable: (a, b) => a.name.length - b.name.lengthsortModel + onSortChange or uncontrolled
internal stateenableSearch renders a debounced input above the table
(default 200 ms)searchable: truegetNestedValue (the
type-level autocomplete from NestedKeyOf<T> matches the runtime
behavior — field: 'address.city' resolves correctly)string / number / boolean / Date /
Array / object (JSON.stringify)rowSelection: 'none' | 'single' | 'multiple'selectedRowIds + onSelectionChangearia-selected on selected rows; default a11y labels translatable
via labels.ariaSelectRow / ariaSelectAllRows<Table
expandable={{
render: (row) => <UserDetail user={row} />,
expandedRowIds: expanded,
onExpandChange: setExpanded,
}}
/>Chevron toggle in a dedicated column · aria-expanded on the
button · expanded detail renders as a full-width sibling <tr>.
<Table
rowActions={(row) => (
<RowActionsMenu
row={row}
actions={[
{ label: 'Edit', onClick: edit },
{ label: 'Delete', onClick: del, color: 'danger',
access: { resource: 'user', action: 'delete', onUnauthorized: 'hide' } },
]}
/>
)}
/>Actions hidden by default via opacity, revealed on tr:hover or
tr:focus-within. Reduces visual density while keeping actions
keyboard-accessible.
| Level | Where | Effect |
|---|---|---|
| Table-level | <Table access={…}> | hide → renders null. disable → greys + disables every interactive element. |
| Per-column | cols[i].access | hide removes the column entirely from header AND every row cell. |
| Per-action | RowActionsMenu actions[i].access | hide filters out the action; disable renders it disabled. |
Column headers accept plain strings (pass t('users.name')
directly). All internal default strings (search placeholder, a11y
announcements, selection counter, density / filter labels)
configurable via labels prop. Same pattern as <Pagination>.
<Table
cols={[{ field: 'name', header: t('users.name'), sortable: true }]}
labels={{
searchPlaceholder: t('table.search.placeholder'),
selectedCount: t('table.selection.count'), // {count} placeholder
ariaSelectRow: t('table.a11y.selectRow'),
ariaSortAscending: t('table.a11y.sortAsc'),
// …all 17 strings translatable
}}
/>plain · lines (default — Stripe-style clean
dividers) · striped · bordered · cardsm · md (default) · lgcompact (40px row) · comfortable (default 48px)
· spacious (56px) — Pencil & Paper UX researchstickyHeader={false})loading={true} renders N <Skeleton> rows (Sprint 4 component)
with aria-busy="true"; loadingRowCount configurableemptyState ReactNode for no-data placeholderssx for root overrideslotProps: root, toolbar, search,
scroll, table, thead, tbody, headerRow, headerCell,
row, cell, selectionCell, expandToggleCell, expandedRow,
rowActionsCell, emptyState, loadingOverlay, bulkActionFooter5 pre-built renderers exported from @dashforge/tw:
import {
RenderText, // one-line, optionally truncated / muted
RenderTwoLine, // bold primary + muted secondary (name + email)
RenderChip, // status badge: 7 intents × 3 variants × 2 sizes
RenderButton, // inline button (ghost / sm defaults)
RowActionsMenu, // 3-dot Popover menu + per-action RBAC
} from '@dashforge/tw';RowActionsMenu reuses the <Popover> component (Sprint 3) — no
new deps.
getNestedValue helper exportedimport { getNestedValue } from '@dashforge/tw';
getNestedValue({ user: { name: 'Jane' } }, 'user.name') // 'Jane'
getNestedValue({ user: null }, 'user.name') // undefined
getNestedValue({ a: { b: 0 } }, 'a.b') // 0 (preserved)Same helper powers Table's cell rendering. Useful in your own custom renderers that need to look up the value with a different transform.
New test _internal/themeIdentity.test.ts scans every Table source
file and fails if dark:*-neutral-N Tailwind classes are
introduced. The dashforgePreset auto-inverts the neutral palette
via CSS var swap; adding dark: variants on neutral creates double
inversion and breaks dark mode. The guard catches the anti-pattern
at test time.
The canonical patterns Table now uses:
// surface (auto-inverts via CSS var swap)
'bg-neutral-50'
// elevated surface (one tier up)
'bg-neutral-100'
// primary text (auto-inverts)
'text-neutral-900'
// secondary text (auto-inverts)
'text-neutral-500'
// selected state (LeftNav `itemActive` — primary palette
// doesn't auto-invert, so dark-navy text on light-blue bg
// works in BOTH modes; guaranteed contrast).
'data-[selected=true]:bg-primary-100',
'data-[selected=true]:text-primary-900',
'data-[selected=true]:font-medium',Sprint 4.3 (0.7.0-beta) will sweep the rest of the catalog
(Typography, Box, etc. carry the same latent bug) and add the
guard at package level.
The TypeScript surface of TableColumn<T> splits the
previously-conflated monospace axis into two:
tabularNums?: boolean — auto-true for number / date columns.
Applies tabular-nums (font-feature setting, does not change
the font family).monospace?: boolean — explicit consumer opt-in only. Applies
font-mono. The dashforgePreset does not own the fontFamily
axis; the consumer configures their mono stack in their own
tailwind.config.ts theme.extend.fontFamily.mono.~/projects/web/learn/dash/src/pages/TestTable.tsx exercises every
Table feature with 30 realistic users + nested meta + chip-rendered
status + RBAC per-column + Italian i18n labels + 5×3×3
variant/size/density switcher.
+147 new unit tests for Sprint 4.1:
getNestedValue — 11 (nested keys, null-safety, zero / empty
string / false preservation, mid-tree returns)useTableSearch — 16 (stringification across primitive types,
nested keys, edge cases)useTableSort — 15 (null-last invariant, multi-column
tie-breaking, custom comparator, locale-aware string compare)useTableSelection — 13 (single / multiple / none modes,
select-all, indeterminate state)useColumnAutoDetect — 14 (type inference + align /
tabularNums / monospace resolution)themeIdentity — 18 (file scanner regression guard)Table.test.tsx — 57 (rendering, smart defaults, sort, search,
selection, expandable, row actions, RBAC, variants, densities,
sizes, sx + slotProps, i18n)Full @dashforge/tw suite at 828/828 passing across 46 files.
No code changes required:
pnpm up @dashforge/tw@^0.6.0-betaTo adopt the new Table:
import {
Table,
RenderTwoLine,
RenderChip,
RowActionsMenu,
Button,
} from '@dashforge/tw';
<Table
rows={users}
cols={[
{
field: 'name',
header: 'Name',
sortable: true,
searchable: true,
cellRenderer: ({ row }) => (
<RenderTwoLine primary={row.name} secondary={row.email} />
),
},
{
field: 'status',
header: 'Status',
cellRenderer: ({ value }) => (
<RenderChip color={value === 'active' ? 'success' : 'warning'}>
{String(value)}
</RenderChip>
),
},
]}
getRowId={(r) => r.id}
enableSearch
rowSelection="multiple"
bulkActions={(rows) => (
<Button color="danger">Delete {rows.length}</Button>
)}
rowActions={(row) => (
<RowActionsMenu
row={row}
actions={[
{ label: 'Edit', onClick: edit },
{ label: 'Delete', onClick: del, color: 'danger' },
]}
/>
)}
/>| Compatibility axis | Pre-0.6.0 | Post-0.6.0 |
|---|---|---|
| Public API surface | 31 components | +1 <Table> + 5 cell renderers (RenderText, RenderTwoLine, RenderChip, RenderButton, RowActionsMenu) + getNestedValue helper + 8+ new types (TableProps, TableColumn, TableSortModel, TableFilterModel, TableLabels, TableCellContext, TableRowAction, NestedKeyOf, …) |
| Peer deps | react ^18 || ^19, tw-theme workspace, tw-tokens workspace | unchanged |
| Bridge deps | forms / rbac / ui-core workspace:* | unchanged |
| New runtime deps | — | none (no @tanstack/*, no DnD libs — constraint honored) |
| Breaking changes | — | Zero. |
| Bundle size | 336 KB raw / 73.9 KB gz | 402 KB raw / 91 KB gz (+23% gz; justified — see PERFORMANCE.md regression budget) |
| Tests passing | 681/681 (39 files) | 828/828 (46 files) — +147 from Sprint 4.1 |
Bundle regression sign-off: the +23% gz delta is above the 10% reviewer-sign-off threshold defined in PERFORMANCE.md. Justification:
<Table>is the central data-display primitive — the highest-leverage addition before 1.0. Sprint 4.3 (0.7.0-beta) is expected to shrink the bundle by 1-3 KB gz by removing redundantdark:variants across the catalog.
| Sprint | Release | Theme |
|---|---|---|
| Sprint 4.3 | @dashforge/[email protected] | Theme identity audit across the whole catalog (Typography, Box, etc.) — applies the rule discovered in Sprint 4.1. Bundle expected to shrink. |
| Sprint 4.2 | @dashforge/[email protected] | DataGrid — homemade virtualization (IntersectionObserver-based, no new deps) for 10k+ row data sets, advanced filter model, sticky columns, per-column RBAC. The "MUI X DataGrid Pro alternative at €0" release. |
| 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. |