Browse docs
Browse docs
Released 2026-07-24 · Sprint 6
Six new primitives close the last catalog gaps flagged by the Builder V2 dogfood, and the Option C theme layer that was scaffolded in 1.0.0 becomes fully consumable — every component now honors theme.components.<Name>.defaults for its visual axes and theme.components.<Name>.slotProps for its per-slot classes, so a design system can pin "our button radius is xl" or "our TextField wrapper always uses these classes" once in the theme instead of on every instance.
This is the first coupled release — all three @dashforge/tw-* packages advance to 1.2.0 together per the release strategy in #60. Consumers should bump the trio in lockstep; the @dashforge/[email protected] peer range enforces this.
No breaking changes. Every existing consumer works unchanged — every addition is additive on top of the 1.1.1 surface.
| Package | Change |
|---|---|
@dashforge/tw-tokens | 1.0.0 → 1.2.0. Coordination bump — no code changes since 1.0.0; the version exists so the trio ships in lockstep. |
@dashforge/tw-theme | 1.0.0 → 1.2.0. Option C Foundation hooks (useComponentDefaults, useSlotProps, mergeSlotProps) + TWComponentDefaults interface + #110 mode sync fix. |
@dashforge/tw | 1.1.1 → 1.2.0. Six new primitives, Option C consumed across 36 components, three fixes, one bridge extension. |
<Link> — token-driven anchor primitive. Every visual axis (color, underline, weight, size) is theme.components.Link.defaults-configurable per Option C, so a DS spec of "Link is primary + semibold + uppercase" lives once in the theme. startIcon / endIcon accept any ReactNode. external toggles target="_blank" + rel="noopener noreferrer" without any auto-glyph — the dev owns the visual affordance. asChild for router integration via Radix Slot.<Select> — token-driven enum picker. Deliberately narrower than Autocomplete (no search, no async, no typeahead-driven filter). Option shape { value, label, disabled? } where label is a ReactNode — enables reactive i18n via <Trans> / <FormattedMessage> without options-array useMemo. Bridge + standalone controlled modes. Multi-select shipped in v1. Generic V extends string | number so TS narrows the union from options.<Slider> — token-driven numeric picker. Single value or [min, max] range via range?: boolean discriminated union. Bridge commits on drag-end by default (onValueCommit), opt into per-tick commits via commitOnChange. Marks: explicit list or auto-generate at every step (auto-suppressed above 21 ticks). Value label tooltip with formatter ('auto' | 'always' | 'off'). Built on @radix-ui/react-slider.<Progress> — determinate progress indicator with two flavors on the same primitive: variant='linear' (bar) and variant='circular' (SVG arc). Display-only — <Spinner> remains the indeterminate primitive. value required. showLabel + formatLabel for units/counters.<Stepper> — declarative multi-step wizard. Compound children API (<Stepper><Step/></Stepper>) with per-step fields (RHF-triggered validation), isValid (custom async callback), visibleWhen, and access. useStep() hook exposes navigation state + API. Horizontal + vertical orientation. initialStep name-based resume + allowJumpTo: 'visited' | 'none'.<Drawer> — edge-anchored sliding panel. Genuinely distinct from Dialog (viewport-centered) and Popover (trigger-attached): anchors to one of the 4 viewport edges. 3 variants (temporary / persistent / sticky) distinguishing user-dismiss contracts. Resize handle on the free edge with pointer drag + keyboard step + localStorage persistence. z-[1400]/[1410] overlay/content stack above MUI AppBar (1300) so MUI coexistence works out of the box.The theme.components.<Name> API layer scaffolded in 1.0.0 becomes fully consumable. Every component with a variant axis honors theme.components.<Name>.defaults; every component with a slot surface (36 of them) honors theme.components.<Name>.slotProps.
patchTheme({
components: {
Button: { defaults: { radius: 'xl' } },
TextField: { slotProps: { input: { className: 'ring-2 ring-primary-500' } } },
Drawer: { defaults: { size: 'lg' }, slotProps: { title: { className: 'text-2xl' } } },
},
});Precedence chain (last wins): TV defaults → theme.components → instance props → sx. Merging is done by tailwind-merge inside cn() so class conflicts resolve deterministically.
Full narrative + per-category playgrounds in Theme System → Component Defaults and Theme System → Slot Defaults Override.
DashFormBridge.trigger@dashforge/forms gains an optional trigger(name?) method on DashFormBridge, used by <Stepper> to synchronously read fresh RHF field errors via rhf.getFieldState(). The naive read of rhf.formState.errors after await rhf.trigger() returns stale because RHF flushes on the next render — the new extension bypasses that gap. Feature-gated / non-breaking.
<Typography> UA margin reset. Every variant now ships with m-0 in its base recipe. Consumers who disable Tailwind's preflight core plugin (typical for MUI / Ant / Chakra coexistence) no longer inherit browser-default block-element margins (<h1> 0.67em, <h2> 0.83em, <p> 1em, …). Vertical rhythm is fully owned by the design system — gutterBottom, <Stack gap>, <Box mb>, sx="mt-*".<DatePicker> inside <DashForm> didn't commit day-click. Root cause: bridge.setValue(name, next) was called with next: null before the popover close, then the popover's markTouched() re-read undefined from the bridge and overwrote the just-committed date. Fix orders the writes correctly. Regression test added.value / onChange. Standalone (non-form-mode) usage of a form widget without a controlled binding used to silently render an uncontrolled input that never notified the parent — footgun. A new useStandaloneFieldWarning hook fires a console.warn in dev mode explaining the missing binding. Zero runtime cost in production.Drop-in for everything already using 1.1.1. All additions are opt-in; nothing renamed, nothing removed.
pnpm update @dashforge/tw @dashforge/tw-theme @dashforge/tw-tokensThe three packages must move together — the @dashforge/[email protected] peer range on the other two enforces this.
Full step-by-step upgrade guide: docs/migration/1.2.0.md.
Both flagged in the migration guide, both mechanical:
<Stack gap="3"> (string) → <Stack gap={3}> (numeric union) — the gap axis was widened by mistake in 1.1.x and TS accepted "3"; the range is now 0 | 0.5 | 1 | 2 | 3 | 4 | 6 | 8 | 12 | 16 | 24.<Box> / <Stack> / <Grid> / <Container> / <Divider>) no longer destroy their internal flex / gap classes when consumers pass className — but the recommended override path is now slotProps.root.className (or sx). Behavior is unchanged for sx users.| Peer | Required range |
|---|---|
react | ^18.0.0 || ^19.0.0 |
react-dom | ^18.0.0 || ^19.0.0 |
tailwindcss | ^3.4.0 |
@dashforge/tw-theme | ^1.2.0 |
@dashforge/tw-tokens | ^1.2.0 |
@dashforge/forms | ^1.0.0 |
@dashforge/ui-core | ^1.0.0 |
@dashforge/rbac | ^1.0.0 |
New runtime dep: @radix-ui/react-slider ^1.4.3 (powers <Slider>).