@dashforge/tw 0.2.0-beta → 0.3.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.
- package/A11Y.md +130 -0
- package/CHANGELOG.md +339 -82
- package/CONSUMER-VALIDATION.md +130 -0
- package/dist/index.esm.js +351 -47
- package/dist/src/components/AppShell/AppShell.d.ts +14 -0
- package/dist/src/components/AppShell/AppShell.d.ts.map +1 -1
- package/dist/src/components/AppShell/appShell.variants.d.ts.map +1 -1
- package/dist/src/components/Autocomplete/Autocomplete.d.ts.map +1 -1
- package/dist/src/components/Autocomplete/autocomplete.variants.d.ts.map +1 -1
- package/dist/src/components/Breadcrumbs/breadcrumbs.variants.d.ts.map +1 -1
- package/dist/src/components/Button/Button.d.ts.map +1 -1
- package/dist/src/components/Checkbox/Checkbox.d.ts.map +1 -1
- package/dist/src/components/LeftNav/leftNav.variants.d.ts.map +1 -1
- package/dist/src/components/NumberField/NumberField.d.ts.map +1 -1
- package/dist/src/components/RadioGroup/RadioGroup.d.ts.map +1 -1
- package/dist/src/components/Snackbar/snackbar.variants.d.ts.map +1 -1
- package/dist/src/components/Switch/switch.variants.d.ts.map +1 -1
- package/dist/src/components/TextField/TextField.d.ts.map +1 -1
- package/dist/src/components/TextField/textField.types.d.ts +29 -3
- package/dist/src/components/TextField/textField.types.d.ts.map +1 -1
- package/dist/src/components/TextField/textField.variants.d.ts +6 -0
- package/dist/src/components/TextField/textField.variants.d.ts.map +1 -1
- package/dist/src/index.d.ts +1 -1
- package/package.json +3 -3
- package/src/components/AppShell/AppShell.tsx +126 -1
- package/src/components/AppShell/appShell.variants.ts +8 -2
- package/src/components/Autocomplete/Autocomplete.tsx +77 -4
- package/src/components/Autocomplete/autocomplete.variants.ts +6 -0
- package/src/components/Breadcrumbs/breadcrumbs.variants.ts +5 -0
- package/src/components/Button/Button.tsx +11 -0
- package/src/components/Checkbox/Checkbox.tsx +63 -5
- package/src/components/LeftNav/leftNav.variants.ts +12 -1
- package/src/components/NumberField/NumberField.tsx +30 -2
- package/src/components/RadioGroup/RadioGroup.tsx +23 -1
- package/src/components/Snackbar/snackbar.variants.ts +4 -2
- package/src/components/Switch/switch.variants.ts +6 -1
- package/src/components/TextField/TextField.tsx +29 -0
- package/src/components/TextField/textField.types.ts +23 -3
- package/src/components/TextField/textField.variants.ts +18 -0
- package/src/index.ts +1 -1
- package/LICENSE +0 -21
package/A11Y.md
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# A11Y audit — `@dashforge/tw`
|
|
2
|
+
|
|
3
|
+
Per-component accessibility status for every shipped `@dashforge/tw`
|
|
4
|
+
component (24 total). Audit date: 2026-05-17, against the 0.2.0-beta
|
|
5
|
+
codebase (+ the three pending lib fixes for Checkbox / RadioGroup /
|
|
6
|
+
NumberField + the Button `aria-busy` improvement landing in 0.2.1-beta).
|
|
7
|
+
|
|
8
|
+
Reference: [WCAG 2.1 AA](https://www.w3.org/TR/WCAG21/) +
|
|
9
|
+
[WAI-ARIA Authoring Practices](https://www.w3.org/WAI/ARIA/apg/).
|
|
10
|
+
|
|
11
|
+
## Summary
|
|
12
|
+
|
|
13
|
+
| Group | Components | Status |
|
|
14
|
+
|---|---|---|
|
|
15
|
+
| Foundation (presentational) | Typography, Box, Stack, Grid, Container, AspectRatio | ✅ N/A — pass-through to HTML semantics |
|
|
16
|
+
| Foundation (a11y primitive) | VisuallyHidden, Divider | ✅ Conformant |
|
|
17
|
+
| Form controls (Radix-backed) | Checkbox, Switch, RadioGroup | ✅ Inherits WAI-ARIA APG from Radix |
|
|
18
|
+
| Form controls (custom) | Button, TextField, Textarea, NumberField, OTPField, Autocomplete, DateTimePicker | ✅ Conformant (see per-row notes) |
|
|
19
|
+
| Layout / Navigation | AppShell, TopBar, LeftNav, Breadcrumbs | ✅ Conformant |
|
|
20
|
+
| Overlays (Radix-backed) | ConfirmDialog | ✅ Inherits from Radix Dialog |
|
|
21
|
+
| Notifications | Snackbar | ✅ Conformant (aria-live polite) |
|
|
22
|
+
|
|
23
|
+
Zero blocking failures. One enhancement landed: Button now exposes
|
|
24
|
+
`aria-busy={loading}` so assistive tech distinguishes loading from
|
|
25
|
+
plain disabled.
|
|
26
|
+
|
|
27
|
+
## Per-component findings
|
|
28
|
+
|
|
29
|
+
### Foundation
|
|
30
|
+
|
|
31
|
+
| Component | Status | Notes |
|
|
32
|
+
|---|---|---|
|
|
33
|
+
| **Typography** | ✅ | Renders the variant-appropriate HTML tag (`<h1>`–`<h6>`, `<p>`, `<small>`) so screen-reader heading navigation works out of the box. `as` / `asChild` let consumers re-tag without losing styles. |
|
|
34
|
+
| **Box** | ✅ | Pass-through `<div>` (or `as`/`asChild`-overridden tag). No interactive semantics. |
|
|
35
|
+
| **Stack** | ✅ | Pass-through `<div>`. Display-only flex container. |
|
|
36
|
+
| **Grid** | ✅ | Pass-through `<div>` with `display: grid`. Layout-only. |
|
|
37
|
+
| **Container** | ✅ | Pass-through `<div>` or `<main>` (via `asChild`). Layout-only. |
|
|
38
|
+
| **Divider** | ✅ | Horizontal: `<hr role="separator" aria-orientation="horizontal">`. Vertical: `<div role="separator" aria-orientation="vertical">`. Labeled variant keeps the role on the wrapper + decorative line segments carry `aria-hidden`. |
|
|
39
|
+
| **AspectRatio** | ✅ | Pass-through `<div>` carrying a `style={aspect-ratio: ...}`. No interactive semantics. |
|
|
40
|
+
| **VisuallyHidden** | ✅ | IS the a11y primitive — WebAIM clip technique via Tailwind's `sr-only`. Chooses `<span>` by default (works inside buttons / links without breaking inline flow). |
|
|
41
|
+
|
|
42
|
+
### Tier-1 form controls
|
|
43
|
+
|
|
44
|
+
| Component | Status | Notes |
|
|
45
|
+
|-------------|--------|-------|
|
|
46
|
+
| **Button** | ✅ | Native `<button>` (or Slot via `asChild`). `disabled` + `aria-disabled` mirrored. **`aria-busy={true}` while loading** (added 2026-05-17) — SR users hear "busy" instead of just "dimmed". Loading spinner is `aria-hidden`. `focus-visible:ring-*` matches the variant color. |
|
|
47
|
+
| **TextField** | ✅ | `<label htmlFor>` via `useId()`. `aria-invalid` on error, `aria-describedby` linked to the helper-text id. Required marker is `aria-hidden` decorative + the real signal is the native `required` attribute. `placeholder:text-neutral-400`. |
|
|
48
|
+
| **Checkbox** | ✅ | Radix `Checkbox.Root` (WAI-ARIA APG). Indicator now uses Radix-native mounting (no React conditional) so checked-state changes from any source — click, programmatic, bridge — propagate to AT. |
|
|
49
|
+
| **Switch** | ✅ | Radix `Switch.Root` (WAI-ARIA APG). `role="switch"` + `aria-checked`. Thumb animation gated on `data-state` — pure CSS, no React state for the visual. |
|
|
50
|
+
|
|
51
|
+
### Tier-2 / 3 form controls
|
|
52
|
+
|
|
53
|
+
| Component | Status | Notes |
|
|
54
|
+
|-------------------|--------|-------|
|
|
55
|
+
| **RadioGroup** | ✅ | Radix `RadioGroup.Root` + `Item`. `role="radiogroup"` on the container, `role="radio"` + `aria-checked` per option. Group label linked via `aria-labelledby`. Per-option RBAC hide preserves the selected option as disabled (so the user can see what they picked even after a perm change). |
|
|
56
|
+
| **Textarea** | ✅ | Same label + helper-text wiring as TextField. `<textarea>` native element, `resize` axis controlled via CSS. |
|
|
57
|
+
| **NumberField** | ✅ | Native `<input type="number">` — keyboard arrow keys are the accessible way to change the value (browser default). The custom +/− stepper is **intentionally `aria-hidden`** and `tabIndex={-1}` — visible/touch affordance only, with `aria-label="Increment"` / `"Decrement"` for any AT that does walk into hidden subtrees. |
|
|
58
|
+
| **OTPField** | ✅ | One real `<input>` (visually rendered as N segmented slots via decorative `aria-hidden` `<div>`s). `autoComplete="one-time-code"` for SMS autofill on iOS. `inputMode="numeric"` (default) for mobile keyboards. Backspace / Delete / paste all work natively because the underlying element IS a single input. |
|
|
59
|
+
| **Autocomplete** | ✅ | Full WAI-ARIA Combobox pattern: `role="combobox"` + `aria-autocomplete="list"` + `aria-expanded` + `aria-controls` + `aria-activedescendant`. Listbox: `role="listbox"` + per-option `role="option"` + `aria-selected`. Async load row: `aria-live="polite"` + `aria-busy="true"`. Clear / open buttons carry `aria-label`. Chip remove buttons: `aria-label="Remove {label}"`. |
|
|
60
|
+
| **DateTimePicker** | ✅ | Native HTML5 `<input type="date">` / `"time"` / `"datetime-local"`. Browser ships a fully-accessible picker for each. `aria-invalid` + `aria-describedby` wired to the helper text. |
|
|
61
|
+
|
|
62
|
+
### Layout / navigation
|
|
63
|
+
|
|
64
|
+
| Component | Status | Notes |
|
|
65
|
+
|---------------|--------|-------|
|
|
66
|
+
| **AppShell** | ✅ | `<aside>` for desktop nav, `<aside aria-hidden={!navOpen}>` for the mobile drawer copy (so it doesn't double-announce when hidden). Escape closes the drawer. `<main>` landmark wraps content. `<header>` / `<footer>` are native landmarks. **Known limitation**: no focus trap inside the mobile drawer — focus returns to the toggle button on Escape close, which is acceptable for a docs site / typical web app but not full WAI-ARIA modal-drawer compliance. Can be added in a future revision. |
|
|
67
|
+
| **TopBar** | ✅ | Renders as `<header>` (banner landmark) by default. `asDiv` opt-out for nested layouts where a banner landmark would duplicate. |
|
|
68
|
+
| **LeftNav** | ✅ | `<nav aria-label>` landmark. `aria-current="page"` on the active row. Groups use `aria-expanded` + `aria-controls` linking the header `<button>` to a `role="region" aria-labelledby` panel. Collapse toggle: `aria-pressed` + dynamic `aria-label`. Per-row RBAC hide is matched by an a11y-tree removal (not just visual), so the announced nav matches what the user can actually interact with. |
|
|
69
|
+
| **Breadcrumbs** | ✅ | `<nav aria-label="Breadcrumb">` + `<ol>` (screen readers announce position-in-list). `aria-current="page"` on the last (or `current: true`) crumb. Separators carry `aria-hidden="true"`. Truncation ellipsis: `<span aria-label="More breadcrumbs">`. |
|
|
70
|
+
|
|
71
|
+
### Overlays + notifications
|
|
72
|
+
|
|
73
|
+
| Component | Status | Notes |
|
|
74
|
+
|------------------|--------|-------|
|
|
75
|
+
| **ConfirmDialog** | ✅ | Radix `Dialog.Root` (WAI-ARIA APG). `role="dialog"` + `aria-modal="true"` + `aria-labelledby` on the title. Radix handles focus trap, focus restoration on close, Escape close, scroll lock. |
|
|
76
|
+
| **Snackbar** | ✅ | Stack is `role="region" aria-label="Notifications" aria-live="polite" aria-atomic="false"`. Polite (not assertive) so notifications don't interrupt the user's screen-reader stream; `atomic="false"` so only the changed item is announced, not the whole queue. Per-item dismiss button: `aria-label="Dismiss notification"`. |
|
|
77
|
+
|
|
78
|
+
## Cross-cutting concerns
|
|
79
|
+
|
|
80
|
+
- **Focus visibility** — every interactive component carries
|
|
81
|
+
`focus-visible:ring-*` matching its color variant. `outline-none` is
|
|
82
|
+
always paired with a ring so keyboard users never lose the focus
|
|
83
|
+
indicator (regression-tested via the inventory grep:
|
|
84
|
+
`Button / Checkbox / ConfirmDialog / RadioGroup / Switch / TextField
|
|
85
|
+
/ Textarea / NumberField / OTPField / Autocomplete / DateTimePicker
|
|
86
|
+
/ Breadcrumbs / LeftNav / Snackbar` all have `focus-visible:ring`).
|
|
87
|
+
- **Color contrast** — the design tokens emit foreground/background
|
|
88
|
+
pairs that meet WCAG 2.1 AA (4.5:1 for normal text, 3:1 for large
|
|
89
|
+
text). Independent verification is on the dashforge-tokens roadmap.
|
|
90
|
+
- **Reduced motion** — animations (Switch thumb slide, Snackbar slide-in,
|
|
91
|
+
ConfirmDialog fade) use the default `transition-*` utilities. A
|
|
92
|
+
`prefers-reduced-motion` audit pass is filed as a future enhancement
|
|
93
|
+
(current behavior: animations always play; not a WCAG failure but
|
|
94
|
+
reduces polish for users with motion sensitivity).
|
|
95
|
+
- **Form error association** — every form control with `helperText` +
|
|
96
|
+
`error` wires `aria-invalid` and `aria-describedby` to the helper id
|
|
97
|
+
so the error message is announced alongside the field name.
|
|
98
|
+
|
|
99
|
+
## Known limitations & future work
|
|
100
|
+
|
|
101
|
+
1. **AppShell mobile drawer focus trap** — currently relies on Escape
|
|
102
|
+
to close + focus naturally returning to the toggle button. A future
|
|
103
|
+
release can add a real focus trap (probably via `focus-trap-react`
|
|
104
|
+
or hand-rolled).
|
|
105
|
+
2. **`prefers-reduced-motion` audit** — animations always play. Should
|
|
106
|
+
gate the Switch thumb slide, Snackbar slide-in, ConfirmDialog fade
|
|
107
|
+
on the media query.
|
|
108
|
+
3. **High-contrast Windows mode** — not explicitly tested. The `border`
|
|
109
|
+
utility renders a 1px border that should survive forced-colors mode,
|
|
110
|
+
but full validation pending.
|
|
111
|
+
4. **Color contrast regression suite** — should be added to CI so a
|
|
112
|
+
token recolor doesn't silently regress AA compliance.
|
|
113
|
+
|
|
114
|
+
## Audit methodology
|
|
115
|
+
|
|
116
|
+
- ARIA role / attribute inventory via `grep -nE "aria-[a-z]+=|role=|onKeyDown"`
|
|
117
|
+
per component source.
|
|
118
|
+
- Keyboard handler audit by reading each component's `onKeyDown`,
|
|
119
|
+
`onPaste`, `onFocus`, `onBlur` and tracing into the registered
|
|
120
|
+
bridge / Radix primitive.
|
|
121
|
+
- Focus visibility inventory via `grep "focus-visible:ring"` across
|
|
122
|
+
all `*.variants.ts`.
|
|
123
|
+
- Label association verification: `useId()` → `htmlFor=inputId` +
|
|
124
|
+
`aria-describedby=helperId` chain checked for TextField, Textarea,
|
|
125
|
+
NumberField, OTPField, DateTimePicker.
|
|
126
|
+
|
|
127
|
+
No automated lighthouse / axe scan was run as part of this audit —
|
|
128
|
+
that's a separate CI integration on the roadmap. Manual screen-reader
|
|
129
|
+
verification (NVDA on Windows, VoiceOver on macOS / iOS) should be
|
|
130
|
+
periodically refreshed; results land in this file as they happen.
|
package/CHANGELOG.md
CHANGED
|
@@ -12,111 +12,368 @@ 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.3.0-beta] — 2026-05-18
|
|
16
|
+
|
|
17
|
+
**Sprint 2 release.** Bundle of 9 fixes across 7 components + 1 new
|
|
18
|
+
public API (TextField inline adornments). Two WCAG enhancements
|
|
19
|
+
close known a11y gaps from the 0.2.1 A11Y audit. End-to-end
|
|
20
|
+
consumer validation in the `dash` app (`/test-{foundation,tw,layout,
|
|
21
|
+
providers}`) caught 1 functional bug + 1 cosmetic gap on Autocomplete
|
|
22
|
+
that were both invisible to unit tests + docs lab.
|
|
23
|
+
|
|
24
|
+
**Minor bump because of TextField `slotProps.prefix/suffix`** — new
|
|
25
|
+
public API on the existing `slotProps` surface, strictly additive
|
|
26
|
+
(empty configs add zero layout cost; existing TextField usages keep
|
|
27
|
+
working byte-identical). All other changes are patches that would
|
|
28
|
+
have shipped as `0.2.2-beta` in isolation.
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
|
|
32
|
+
- **TextField inline adornments via `slotProps.prefix` + `slotProps.suffix`** —
|
|
33
|
+
closes a long-standing doc/lib drift where the
|
|
34
|
+
`text-field.mdx` already documented this API but the lib didn't
|
|
35
|
+
expose it. New shape:
|
|
36
|
+
```tsx
|
|
37
|
+
<TextField
|
|
38
|
+
name="price"
|
|
39
|
+
type="number"
|
|
40
|
+
slotProps={{
|
|
41
|
+
prefix: { children: '$' },
|
|
42
|
+
suffix: { children: 'USD' },
|
|
43
|
+
}}
|
|
44
|
+
/>
|
|
45
|
+
```
|
|
46
|
+
Both slots accept `{ children?: ReactNode; className?: string }`.
|
|
47
|
+
Rendered inside the inputWrapper with `aria-hidden="true"` +
|
|
48
|
+
`pointer-events-none` (purely visual decoration — input remains
|
|
49
|
+
the labeled control, click on adornment doesn't steal focus).
|
|
50
|
+
21/21 TextField tests still pass.
|
|
51
|
+
|
|
52
|
+
- **AppShell mobile drawer focus trap** (WCAG 2.4.3 Focus Order).
|
|
53
|
+
Hand-rolled (no `focus-trap-react` dep). On drawer open: captures
|
|
54
|
+
`document.activeElement`, moves focus to first focusable inside
|
|
55
|
+
drawer, intercepts `Tab` / `Shift+Tab` to wrap focus within the
|
|
56
|
+
drawer subtree. On close: restores focus to the captured element
|
|
57
|
+
(typically the hamburger toggle). Plus `role="dialog"` +
|
|
58
|
+
`aria-modal="true"` on the drawer `<aside>` while open so screen
|
|
59
|
+
readers announce it as a modal overlay. Verified end-to-end in
|
|
60
|
+
dash: all 5 check points (closed ARIA, open ARIA, focus in,
|
|
61
|
+
tab-wrap, Esc-close) pass.
|
|
62
|
+
|
|
63
|
+
- **`prefers-reduced-motion` gates** on six substantial motions
|
|
64
|
+
(WCAG 2.3.3 Animation from Interactions): Switch thumb slide,
|
|
65
|
+
AppShell drawer slide-in + backdrop fade, Snackbar item enter,
|
|
66
|
+
Autocomplete chevron flip, LeftNav rail-mode width transition.
|
|
67
|
+
Uses Tailwind's `motion-reduce:` variant — the animated end-state
|
|
68
|
+
still applies, only the smooth tween is suppressed for users who
|
|
69
|
+
request reduced motion. Color fades (`transition-colors` on hover
|
|
70
|
+
states) are NOT gated — out of WCAG 2.3.3 scope (vestibular
|
|
71
|
+
concern is translate/rotate/major-state-change, not micro fades).
|
|
72
|
+
|
|
73
|
+
- **Checkbox indeterminate dash glyph**. Previously the Indicator
|
|
74
|
+
rendered `<CheckIcon />` for BOTH the `checked` and `indeterminate`
|
|
75
|
+
Radix states. Now renders `<DashIcon />` (horizontal stroke) for
|
|
76
|
+
indeterminate and `<CheckIcon />` for fully checked. Toggle via
|
|
77
|
+
Tailwind `group-data-[state=…]:hidden` selectors on the Indicator
|
|
78
|
+
— pure CSS, zero React state, Radix `data-state` remains the
|
|
79
|
+
single source of truth. Closes a cosmetic regression introduced
|
|
80
|
+
in 0.2.1-beta when we dropped `forceMount` to fix the indicator
|
|
81
|
+
mount bug.
|
|
82
|
+
|
|
83
|
+
### Fixed
|
|
84
|
+
|
|
85
|
+
- **Autocomplete `loadOptions` mode — selection now commits** the
|
|
86
|
+
clicked option's label to the input. Previously, in async-loaded
|
|
87
|
+
mode, clicking an option closed the popover but the input kept
|
|
88
|
+
showing the user's search query instead of the selected label.
|
|
89
|
+
Root cause: `commitSelection` searched only the static `options`
|
|
90
|
+
prop (empty `[]` when `loadOptions` is configured) for the label
|
|
91
|
+
lookup, not the effective pool (`asyncOptions` when the fetch
|
|
92
|
+
resolved). The fix uses
|
|
93
|
+
`loadOptions && asyncOptions !== null ? asyncOptions : options`
|
|
94
|
+
and adds those refs to the `useCallback` deps. 38/40 Autocomplete
|
|
95
|
+
tests pass (2 perf-timing flakes unrelated, both >100ms over
|
|
96
|
+
threshold on loaded machine).
|
|
97
|
+
|
|
98
|
+
- **Autocomplete chip remove (×), clear (×), and dropdown caret (▾)
|
|
99
|
+
icons replaced with inline SVG** (`CloseIcon` + `ChevronDownIcon`,
|
|
100
|
+
mirroring CheckIcon's pattern). Previously rendered as Unicode
|
|
101
|
+
glyphs that came out as chunky font characters inconsistent with
|
|
102
|
+
the rest of the design system. Bonus: chevron flips 180° on
|
|
103
|
+
popover open via `[&[aria-expanded=true]>svg]:rotate-180` (CSS-
|
|
104
|
+
only, no React state). Zero icon-library dep added.
|
|
105
|
+
|
|
106
|
+
- **LeftNav `itemLink` + Breadcrumbs `link` slots no longer
|
|
107
|
+
underline by default**. Tailwind's preflight removes the browser-
|
|
108
|
+
default anchor underline globally, but environments that disable
|
|
109
|
+
preflight (e.g. apps coexisting with MUI in the same page tree —
|
|
110
|
+
this is exactly how our docs lab is set up) get the underline
|
|
111
|
+
back. Explicit `no-underline hover:no-underline` on both slots
|
|
112
|
+
makes the appearance consistent regardless of preflight state.
|
|
113
|
+
Same root cause fix covers TopBar too — TopBar typically renders
|
|
114
|
+
Breadcrumbs in its center slot, so fixing the Breadcrumbs link
|
|
115
|
+
fixes TopBar transitively.
|
|
116
|
+
|
|
117
|
+
### Internal
|
|
118
|
+
|
|
119
|
+
- **`libs/dashforge/tw/CONSUMER-VALIDATION.md`** — Sprint 2 P1
|
|
120
|
+
deliverable. Per-component status table for the dash-consumer
|
|
121
|
+
end-to-end validation pass (24 components, 7-point check each).
|
|
122
|
+
Records the pattern lesson that motivated the Autocomplete
|
|
123
|
+
async fix: the bug was invisible to both unit tests (using
|
|
124
|
+
static `options`) and the docs lab (static demo) — only a real
|
|
125
|
+
consumer with `loadOptions` configured exposed it.
|
|
126
|
+
|
|
127
|
+
### Compatibility
|
|
128
|
+
|
|
129
|
+
| Compatibility axis | Pre-`0.3.0` | Post-`0.3.0` |
|
|
130
|
+
|---|---|---|
|
|
131
|
+
| Public API surface | unchanged | **+ TextField `slotProps.prefix` / `slotProps.suffix`** (additive — opt-in via slotProps, zero impact on existing usages) |
|
|
132
|
+
| Peer deps | `react ^18 \|\| ^19`, `tw-theme workspace`, `tw-tokens workspace` | unchanged |
|
|
133
|
+
| Bridge deps | `forms` / `rbac` / `ui-core` `workspace:*` | unchanged |
|
|
134
|
+
| Behavior changes that consumers might observe | — | Autocomplete chip remove + clear + caret render as crisp SVG instead of Unicode glyphs (cosmetic); LeftNav + Breadcrumbs links no longer underline in preflight-off environments; Switch / drawer / snackbar / chevron animations respect `prefers-reduced-motion: reduce`; Checkbox indeterminate now shows a dash glyph instead of check |
|
|
135
|
+
|
|
136
|
+
### Migration
|
|
137
|
+
|
|
138
|
+
No code changes required. Drop-in upgrade from `0.2.1-beta`:
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
pnpm up @dashforge/tw@^0.3.0-beta
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
To adopt the new TextField adornments, no migration — opt in by
|
|
145
|
+
adding `slotProps={{ prefix: { children: '$' } }}` to any existing
|
|
146
|
+
TextField usage. The two slots are independent (you can use one
|
|
147
|
+
without the other).
|
|
148
|
+
|
|
149
|
+
## [0.2.1-beta] — 2026-05-17
|
|
150
|
+
|
|
151
|
+
**Hardening release.** Four targeted fixes — three in form-control
|
|
152
|
+
runtime behaviour, one in the Button accessibility contract — surfaced
|
|
153
|
+
while building live-preview demos for the docs site. No public API
|
|
154
|
+
change on any component; strictly additive on the `<Button>` props
|
|
155
|
+
contract (a new `aria-busy` attribute is emitted automatically when
|
|
156
|
+
`loading` is true). Drop-in upgrade from `0.2.0-beta`.
|
|
157
|
+
|
|
158
|
+
Theme of the three form-control fixes: the **same root cause** —
|
|
159
|
+
"controlled-without-an-owner" — under three different surface
|
|
160
|
+
appearances. In standalone uncontrolled mode (no `DashFormProvider`,
|
|
161
|
+
no `value` / `checked` prop, only `defaultValue` / `defaultChecked`),
|
|
162
|
+
each component was sitting in a controlled mode without anyone able
|
|
163
|
+
to update the controlled prop on the user's keystrokes / clicks, so
|
|
164
|
+
React would snap the input right back. The fixes vary by component
|
|
165
|
+
implementation (Radix-backed → discriminated spread of `value` vs
|
|
166
|
+
`defaultValue`; Radix indicator → drop `forceMount` + React
|
|
167
|
+
conditional; native `<input>` → local `useState` for the uncontrolled
|
|
168
|
+
case) but the pattern is identical. A11Y.md (new doc, separate
|
|
169
|
+
commit) documents the broader pattern audit.
|
|
170
|
+
|
|
171
|
+
### Fixed
|
|
172
|
+
|
|
173
|
+
- **Checkbox** — the indicator's check glyph never appeared when the
|
|
174
|
+
user clicked a Checkbox that was rendered standalone-uncontrolled
|
|
175
|
+
(no `DashFormProvider`, no `checked` prop). The control turned blue
|
|
176
|
+
via `data-[state=checked]:bg-primary-500` but the React-conditional
|
|
177
|
+
`<CheckIcon />` was gated on a stale `resolvedChecked` snapshot.
|
|
178
|
+
Dropped `forceMount` + the conditional; the Radix `Indicator` now
|
|
179
|
+
owns the mount decision, tracking Radix's internal `data-state`
|
|
180
|
+
directly. Mounts in all three modes (controlled, uncontrolled,
|
|
181
|
+
bridge). 14/14 tests pass.
|
|
182
|
+
- **RadioGroup** — clicking a different radio in standalone-uncontrolled
|
|
183
|
+
mode had no visible effect (the selection snapped back to
|
|
184
|
+
`defaultValue`). `<RadixRadioGroup.Root>` was passed `value={…}`
|
|
185
|
+
always, putting Radix in controlled mode against a never-updated
|
|
186
|
+
snapshot. Discriminated spread now picks `value` only in form mode
|
|
187
|
+
or when the consumer explicitly passes `value`; standalone-with-only-
|
|
188
|
+
`defaultValue` uses `defaultValue` so Radix manages its own state.
|
|
189
|
+
11/11 tests pass.
|
|
190
|
+
- **NumberField** — typing into the input or clicking the +/− stepper
|
|
191
|
+
had no visible effect in standalone-uncontrolled mode, for the same
|
|
192
|
+
reason (controlled `<input value={…}>` with no setter). Added a
|
|
193
|
+
local `useState<string>` seeded from `defaultValue` (mirrors the
|
|
194
|
+
OTPField pattern); `handleChange` + `stepBy` now both update it.
|
|
195
|
+
8/8 tests pass.
|
|
196
|
+
- **Button** — sets `aria-busy={true}` while `loading`, so assistive
|
|
197
|
+
tech distinguishes "wait for the action to finish" from plain
|
|
198
|
+
"disabled" (which previously was the only signal — same DOM
|
|
199
|
+
attribute regardless of whether the disable came from `loading`,
|
|
200
|
+
`disabled={true}`, or RBAC). 19/19 tests pass.
|
|
201
|
+
|
|
202
|
+
### Internal
|
|
203
|
+
|
|
204
|
+
- A11Y.md added at package root — per-component status table mapped
|
|
205
|
+
to WCAG 2.1 AA / WAI-ARIA APG. Documents that 23 of 24 components
|
|
206
|
+
were already conformant pre-release (only Button needed the
|
|
207
|
+
`aria-busy` enhancement above). Known non-blocking limitations
|
|
208
|
+
filed: AppShell mobile drawer focus trap, `prefers-reduced-motion`
|
|
209
|
+
pass, color-contrast CI suite, lighthouse/axe automated scan.
|
|
210
|
+
|
|
211
|
+
### Compatibility
|
|
212
|
+
|
|
213
|
+
| Compatibility axis | Pre-`0.2.1` | Post-`0.2.1` |
|
|
214
|
+
|---|---|---|
|
|
215
|
+
| Public API surface | unchanged | unchanged + `aria-busy` auto-emitted on `<Button loading>` |
|
|
216
|
+
| Peer deps | `react ^18 \|\| ^19`, `tw-theme workspace`, `tw-tokens workspace` | unchanged |
|
|
217
|
+
| Bridge deps | `forms` / `rbac` / `ui-core` `workspace:*` | unchanged |
|
|
218
|
+
|
|
15
219
|
## [0.2.0-beta] — 2026-05-17
|
|
16
220
|
|
|
17
221
|
**Foundation release.** Eight layout / structural primitives added on top of
|
|
18
|
-
the F3–F7 component catalogue
|
|
19
|
-
the
|
|
222
|
+
the F3–F7 component catalogue (16 components → 24), plus a coverage
|
|
223
|
+
hardening pass bringing the package from **460 → 592 unit tests** across
|
|
224
|
+
**32 files**. End-to-end validated in the `dash` consumer app: mount
|
|
225
|
+
**12.1 ms** / re-render **7–8.6 ms** for a page with 50+ primitive
|
|
226
|
+
instances.
|
|
20
227
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
primitives incrementally; existing code keeps working
|
|
228
|
+
**No public API change** on any of the 16 previously-shipped components
|
|
229
|
+
— strictly additive minor bump. Consumers upgrading from `0.1.0-beta`
|
|
230
|
+
can adopt the new primitives incrementally; existing code keeps working
|
|
231
|
+
unchanged.
|
|
24
232
|
|
|
25
233
|
### Added — Foundation primitives (F9)
|
|
26
234
|
|
|
27
235
|
The `Box ≠ flex`, `Stack = flex 1D`, `Grid = flex 2D` rule is the spine of
|
|
28
236
|
this layer: each primitive has a single, non-overlapping responsibility so
|
|
29
|
-
"which one do I use?" has one answer per scenario.
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
`color
|
|
237
|
+
"which one do I use?" has one answer per scenario. The rule is enforced at
|
|
238
|
+
the TypeScript prop type level — `<Box direction="row">` is a compile error.
|
|
239
|
+
|
|
240
|
+
- **`Typography`** — semantic typed text. Twelve variants (`h1`–`h6`,
|
|
241
|
+
`subtitle1/2`, `body1/2`, `caption`, `overline`) × nine intent colors ×
|
|
242
|
+
five weight overrides + alignment + truncate / noWrap / gutterBottom.
|
|
243
|
+
Default HTML tag inferred from variant (h1→`<h1>`, body1→`<p>`, …),
|
|
244
|
+
overridable via `as` or `asChild` (Radix Slot). Reads color from a
|
|
245
|
+
parent `<Box>` via `color="inherit"`.
|
|
246
|
+
Source: `src/components/Typography/{Typography.tsx, typography.types.ts, typography.variants.ts}`.
|
|
247
|
+
|
|
37
248
|
- **`Box`** — surface primitive consolidating MUI's Box + Paper + Card +
|
|
38
|
-
Surface into one. Five variants (`plain` · `outlined` · `elevated` ·
|
|
39
|
-
`soft` · `solid`) × seven intent colors = 21 compound visuals
|
|
40
|
-
elevation levels (0
|
|
41
|
-
+ rounded scale +
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
249
|
+
Joy Surface into one. Five variants (`plain` · `outlined` · `elevated` ·
|
|
250
|
+
`soft` · `solid`) × seven intent colors = **21 compound visuals**
|
|
251
|
+
emitted by the TV recipe + six elevation levels (`0`–`5`) + token-scale
|
|
252
|
+
spacing (`p`/`px`/`py`/`m`/`mx`/`my`) + rounded scale +
|
|
253
|
+
`fullWidth`/`fullHeight`. Strictly no flex / no grid by design — wrap
|
|
254
|
+
in Stack/Grid for layout.
|
|
255
|
+
Source: `src/components/Box/box.variants.ts` (compound matrix lives here).
|
|
256
|
+
|
|
257
|
+
- **`Stack`** — the **only** flex container in `@dashforge/tw`.
|
|
258
|
+
`direction` + `align` + `justify` + token-scale `gap` + `wrap`, plus a
|
|
259
|
+
runtime `divider` prop that inserts N-1 separators between children
|
|
260
|
+
(`React.Children.toArray` semantics: Fragments count as one child —
|
|
261
|
+
documented in `Stack.tsx` header + asserted in the test suite).
|
|
262
|
+
Source: `src/components/Stack/{Stack.tsx, stack.types.ts, stack.variants.ts}`.
|
|
263
|
+
|
|
47
264
|
- **`Grid`** — CSS Grid container + item, polymorphic in role. MUI v2 API
|
|
48
|
-
surface (`<Grid container>` + `<Grid xs={6}>`) backed by real CSS
|
|
49
|
-
(`display: grid` + `col-span-*`), not flexbox
|
|
50
|
-
TypeScript
|
|
51
|
-
responsive col-span mapping (xs/sm/md/lg/xl
|
|
265
|
+
surface (`<Grid container>` + `<Grid xs={6}>`) backed by **real CSS
|
|
266
|
+
Grid** (`display: grid` + `col-span-*`), not flexbox like MUI v2's own
|
|
267
|
+
internals. Discriminated-union TypeScript: `<Grid container xs={6}>`
|
|
268
|
+
fails compilation. 70-entry responsive `col-span` mapping (xs/sm/md/lg/xl
|
|
269
|
+
× `1..12/auto/full`) in the TV recipe.
|
|
270
|
+
Source: `src/components/Grid/{Grid.tsx, grid.types.ts, grid.variants.ts}`.
|
|
52
271
|
|
|
53
272
|
### Added — Foundation completions (F10)
|
|
54
273
|
|
|
55
|
-
Closes the foundation surface to match what Chakra/Mantine/Joy ship at
|
|
56
|
-
layout-primitive level.
|
|
274
|
+
Closes the foundation surface to match what Chakra/Mantine/Joy ship at
|
|
275
|
+
the layout-primitive level.
|
|
57
276
|
|
|
58
277
|
- **`Container`** — centered max-width page wrapper with the canonical
|
|
59
278
|
responsive padding ramp (`px-4 sm:px-6 lg:px-8`). Six sizes
|
|
60
|
-
(sm/md/lg/xl/2xl/fluid) mapped to Tailwind's
|
|
61
|
-
`centerContent` opt-in for marketing/
|
|
62
|
-
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
-
|
|
68
|
-
|
|
279
|
+
(`sm` / `md` / `lg` / `xl` / `2xl` / `fluid`) mapped to Tailwind's
|
|
280
|
+
`max-w-screen-*` aliases + `centerContent` opt-in for marketing /
|
|
281
|
+
sign-in layouts.
|
|
282
|
+
Source: `src/components/Container/{Container.tsx, container.types.ts, container.variants.ts}`.
|
|
283
|
+
|
|
284
|
+
- **`Divider`** — visual separator with two rendering modes selected by
|
|
285
|
+
`children` presence. Line-only renders `<hr>` with `role="separator"` +
|
|
286
|
+
`aria-orientation`; labeled mode renders the "OR" separator pattern as
|
|
287
|
+
two flex segments around the label, with a 32 px stub on the squashed
|
|
288
|
+
side for `align="start"` / `"end"`. orientation × variant
|
|
289
|
+
(solid/dashed/dotted) × color (7 intents) × align (3) axes.
|
|
290
|
+
Source: `src/components/Divider/{Divider.tsx, divider.types.ts, divider.variants.ts}`.
|
|
291
|
+
|
|
292
|
+
- **`AspectRatio`** — content-shape primitive using the **native CSS
|
|
293
|
+
`aspect-ratio` property** (supported since 2021, ~98% browser coverage).
|
|
69
294
|
Number or CSS-string ratio. Pairs with `sx="rounded-xl overflow-hidden"`
|
|
70
|
-
for the canonical clipped media pattern (documented as the #1 gotcha
|
|
295
|
+
for the canonical clipped media pattern (documented as the #1 gotcha
|
|
296
|
+
in `AspectRatio.tsx` header and the public MDX docs).
|
|
297
|
+
Source: `src/components/AspectRatio/{AspectRatio.tsx, aspectRatio.types.ts}`.
|
|
298
|
+
|
|
71
299
|
- **`VisuallyHidden`** — the a11y primitive. Uses Tailwind's `sr-only`
|
|
72
|
-
(WebAIM clip technique). Hides children from sighted users
|
|
73
|
-
them in the accessibility tree — icon button labels,
|
|
74
|
-
skip links. Default tag is
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
- **
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
300
|
+
utility (WebAIM clip technique). Hides children from sighted users
|
|
301
|
+
while keeping them in the accessibility tree — icon button labels,
|
|
302
|
+
status announcers (`aria-live="polite"`), skip links. Default tag is
|
|
303
|
+
`<span>` for the 99% case (inline label inside a button or link).
|
|
304
|
+
Source: `src/components/VisuallyHidden/{VisuallyHidden.tsx, visuallyHidden.types.ts}`.
|
|
305
|
+
|
|
306
|
+
### Internal
|
|
307
|
+
|
|
308
|
+
- **+132 edge case unit tests** added (`460 → 592` total across `32`
|
|
309
|
+
files). Reorganised here under `Internal` rather than `Added` because
|
|
310
|
+
the tests are not part of the public API surface.
|
|
311
|
+
|
|
312
|
+
- **Box (+33)** — every one of the 21 compound surface variants
|
|
313
|
+
(outlined / soft / solid × 7 intents) asserted explicitly with the
|
|
314
|
+
light + dark pair; plain / elevated color-agnostic invariants;
|
|
315
|
+
spacing axis coexistence (`p` + `px` + `py` × tailwind-merge
|
|
316
|
+
precedence); `elevation` × `variant` interaction; rounded edge
|
|
317
|
+
values. File: `src/components/Box/Box.test.tsx`.
|
|
318
|
+
|
|
319
|
+
- **Grid (+38)** — every responsive breakpoint × representative span
|
|
320
|
+
enumeration (5 × 5 = 25), full cascade test (`xs={12} sm={6} md={4}
|
|
321
|
+
lg={3} xl={2}`), every `autoFlow` value, every `cols` value,
|
|
322
|
+
`spacingX` / `spacingY` independence, empty container + orphan item
|
|
323
|
+
handling, deep nesting (Grid inside Grid item). File:
|
|
324
|
+
`src/components/Grid/Grid.test.tsx`.
|
|
325
|
+
|
|
326
|
+
- **Stack (+29)** — array divider, conditional / null children, mixed
|
|
327
|
+
text + element children, nested Stack with divider, divider key
|
|
328
|
+
stability across re-renders, every gap step (11 token values),
|
|
329
|
+
every `align`/`justify` value (11), empty Stack with and without
|
|
330
|
+
divider. File: `src/components/Stack/Stack.test.tsx`.
|
|
331
|
+
|
|
332
|
+
- **Typography / Container / Divider / AspectRatio / VisuallyHidden
|
|
333
|
+
(+32)** — multi-axis combinations (`variant` + `color` + `weight` +
|
|
334
|
+
`align` + `truncate` + `gutterBottom`), full variant catalogues
|
|
335
|
+
iterated with `it.each`, extreme ratio values (21/9, 9/16, 0.5, 3),
|
|
336
|
+
nested fluid/capped Container pattern, `aria-live="polite"`
|
|
337
|
+
announcer pattern.
|
|
338
|
+
|
|
339
|
+
- **End-to-end consumer validation.** New page
|
|
340
|
+
`~/projects/web/learn/dash/src/pages/TestFoundation.tsx` (linked into
|
|
341
|
+
the `dash` consumer app via a `file:` package override) mounts all
|
|
342
|
+
eight primitives inside `<DashforgeTailwindProvider>` wrapped in a
|
|
343
|
+
React `<Profiler>` with `onRender` logger. Measured: **mount 12.1 ms,
|
|
344
|
+
update 7–8.6 ms** for a page with 50+ primitive instances — within the
|
|
345
|
+
60 fps frame budget. No `React.memo` applied — the primitives are
|
|
346
|
+
pure (no `useState`, no `useEffect`, only className resolution), so
|
|
347
|
+
React's reconciler trivially handles the re-render.
|
|
348
|
+
|
|
349
|
+
### Architecture
|
|
350
|
+
|
|
351
|
+
- **`Box ≠ flex`, `Stack = flex 1D`, `Grid = flex 2D`** — single
|
|
352
|
+
responsibility per primitive, enforced at the TypeScript prop type
|
|
353
|
+
level. `Box` exposes no `display` / `flex*` / `grid*` props at all —
|
|
354
|
+
trying to pass them fails compilation. The rule rules out the MUI
|
|
355
|
+
failure mode where every `<Box display="flex" gap={2}>` quietly becomes
|
|
356
|
+
the de facto flex container of the codebase, drowning the
|
|
357
|
+
surface-vs-layout distinction. When you read `<Stack>` in a JSX tree
|
|
358
|
+
you know it's flex without reading any further.
|
|
112
359
|
|
|
113
360
|
### Compatibility
|
|
114
361
|
|
|
115
362
|
- **Peer deps unchanged**: `@dashforge/tw-theme@^0.1.0-beta` and
|
|
116
|
-
`@dashforge/tw-tokens@^0.1.0-beta` (neither was modified
|
|
117
|
-
|
|
118
|
-
`@dashforge/
|
|
119
|
-
|
|
363
|
+
`@dashforge/tw-tokens@^0.1.0-beta` (neither package was modified in
|
|
364
|
+
this cycle). Bridge layer (`@dashforge/forms`, `@dashforge/ui-core`,
|
|
365
|
+
`@dashforge/rbac`) likewise unchanged at the workspace `0.2.3-beta`
|
|
366
|
+
version.
|
|
367
|
+
|
|
368
|
+
- **No breaking changes**: the public API of the 16 previously-shipped
|
|
369
|
+
components is byte-identical. Diff `0.1.0-beta..0.2.0-beta` against
|
|
370
|
+
`src/index.ts` shows only additive exports (the new Foundation
|
|
371
|
+
primitives + their `*Variants` recipes + their `*Props` types).
|
|
372
|
+
|
|
373
|
+
- **Bundle size impact** (gzipped, when fully exercised):
|
|
374
|
+
`dist/index.esm.js` grew from 255 KB to 272 KB (+17 KB / +6.7%) for
|
|
375
|
+
the eight new primitives. Tree-shaking unaffected — consumers only
|
|
376
|
+
pay for what they import.
|
|
120
377
|
|
|
121
378
|
---
|
|
122
379
|
|