@dashforge/tw 0.1.0-beta → 0.2.1-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 (195) hide show
  1. package/A11Y.md +130 -0
  2. package/CHANGELOG.md +231 -0
  3. package/dist/index.d.ts +1 -82
  4. package/dist/index.esm.js +1608 -11
  5. package/dist/src/components/AspectRatio/AspectRatio.d.ts +36 -0
  6. package/dist/src/components/AspectRatio/AspectRatio.d.ts.map +1 -0
  7. package/dist/src/components/AspectRatio/aspectRatio.types.d.ts +50 -0
  8. package/dist/src/components/AspectRatio/aspectRatio.types.d.ts.map +1 -0
  9. package/dist/src/components/Box/Box.d.ts +36 -0
  10. package/dist/src/components/Box/Box.d.ts.map +1 -0
  11. package/dist/src/components/Box/box.types.d.ts +54 -0
  12. package/dist/src/components/Box/box.types.d.ts.map +1 -0
  13. package/dist/src/components/Box/box.variants.d.ts +402 -0
  14. package/dist/src/components/Box/box.variants.d.ts.map +1 -0
  15. package/dist/src/components/Button/Button.d.ts.map +1 -1
  16. package/dist/src/components/Checkbox/Checkbox.d.ts.map +1 -1
  17. package/dist/src/components/Container/Container.d.ts +35 -0
  18. package/dist/src/components/Container/Container.d.ts.map +1 -0
  19. package/dist/src/components/Container/container.types.d.ts +41 -0
  20. package/dist/src/components/Container/container.types.d.ts.map +1 -0
  21. package/dist/src/components/Container/container.variants.d.ts +93 -0
  22. package/dist/src/components/Container/container.variants.d.ts.map +1 -0
  23. package/dist/src/components/Divider/Divider.d.ts +50 -0
  24. package/dist/src/components/Divider/Divider.d.ts.map +1 -0
  25. package/dist/src/components/Divider/divider.types.d.ts +47 -0
  26. package/dist/src/components/Divider/divider.types.d.ts.map +1 -0
  27. package/dist/src/components/Divider/divider.variants.d.ts +129 -0
  28. package/dist/src/components/Divider/divider.variants.d.ts.map +1 -0
  29. package/dist/src/components/Grid/Grid.d.ts +32 -0
  30. package/dist/src/components/Grid/Grid.d.ts.map +1 -0
  31. package/dist/src/components/Grid/grid.types.d.ts +103 -0
  32. package/dist/src/components/Grid/grid.types.d.ts.map +1 -0
  33. package/dist/src/components/Grid/grid.variants.d.ts +453 -0
  34. package/dist/src/components/Grid/grid.variants.d.ts.map +1 -0
  35. package/dist/src/components/NumberField/NumberField.d.ts.map +1 -1
  36. package/dist/src/components/RadioGroup/RadioGroup.d.ts.map +1 -1
  37. package/dist/src/components/Stack/Stack.d.ts +24 -0
  38. package/dist/src/components/Stack/Stack.d.ts.map +1 -0
  39. package/dist/src/components/Stack/stack.types.d.ts +61 -0
  40. package/dist/src/components/Stack/stack.types.d.ts.map +1 -0
  41. package/dist/src/components/Stack/stack.variants.d.ts +163 -0
  42. package/dist/src/components/Stack/stack.variants.d.ts.map +1 -0
  43. package/dist/src/components/Typography/Typography.d.ts +42 -0
  44. package/dist/src/components/Typography/Typography.d.ts.map +1 -0
  45. package/dist/src/components/Typography/typography.types.d.ts +61 -0
  46. package/dist/src/components/Typography/typography.types.d.ts.map +1 -0
  47. package/dist/src/components/Typography/typography.variants.d.ts +167 -0
  48. package/dist/src/components/Typography/typography.variants.d.ts.map +1 -0
  49. package/dist/src/components/VisuallyHidden/VisuallyHidden.d.ts +42 -0
  50. package/dist/src/components/VisuallyHidden/VisuallyHidden.d.ts.map +1 -0
  51. package/dist/src/components/VisuallyHidden/visuallyHidden.types.d.ts +42 -0
  52. package/dist/src/components/VisuallyHidden/visuallyHidden.types.d.ts.map +1 -0
  53. package/dist/src/index.d.ts +23 -1
  54. package/dist/src/index.d.ts.map +1 -1
  55. package/package.json +3 -3
  56. package/src/components/AspectRatio/AspectRatio.test.tsx +173 -0
  57. package/src/components/AspectRatio/AspectRatio.tsx +73 -0
  58. package/src/components/AspectRatio/aspectRatio.types.ts +54 -0
  59. package/src/components/Box/Box.test.tsx +349 -0
  60. package/src/components/Box/Box.tsx +83 -0
  61. package/src/components/Box/box.types.ts +61 -0
  62. package/src/components/Box/box.variants.ts +211 -0
  63. package/src/components/Button/Button.tsx +11 -0
  64. package/src/components/Checkbox/Checkbox.tsx +26 -2
  65. package/src/components/Container/Container.test.tsx +209 -0
  66. package/src/components/Container/Container.tsx +74 -0
  67. package/src/components/Container/container.types.ts +45 -0
  68. package/src/components/Container/container.variants.ts +81 -0
  69. package/src/components/Divider/Divider.test.tsx +241 -0
  70. package/src/components/Divider/Divider.tsx +140 -0
  71. package/src/components/Divider/divider.types.ts +52 -0
  72. package/src/components/Divider/divider.variants.ts +115 -0
  73. package/src/components/Grid/Grid.test.tsx +321 -0
  74. package/src/components/Grid/Grid.tsx +148 -0
  75. package/src/components/Grid/grid.types.ts +112 -0
  76. package/src/components/Grid/grid.variants.ts +133 -0
  77. package/src/components/NumberField/NumberField.tsx +30 -2
  78. package/src/components/RadioGroup/RadioGroup.tsx +23 -1
  79. package/src/components/Stack/Stack.test.tsx +309 -0
  80. package/src/components/Stack/Stack.tsx +115 -0
  81. package/src/components/Stack/stack.types.ts +68 -0
  82. package/src/components/Stack/stack.variants.ts +80 -0
  83. package/src/components/Typography/Typography.test.tsx +230 -0
  84. package/src/components/Typography/Typography.tsx +112 -0
  85. package/src/components/Typography/typography.types.ts +65 -0
  86. package/src/components/Typography/typography.variants.ts +113 -0
  87. package/src/components/VisuallyHidden/VisuallyHidden.test.tsx +86 -0
  88. package/src/components/VisuallyHidden/VisuallyHidden.tsx +59 -0
  89. package/src/components/VisuallyHidden/visuallyHidden.types.ts +45 -0
  90. package/src/index.ts +50 -1
  91. package/LICENSE +0 -21
  92. package/dist/components/AppShell/AppShell.d.ts +0 -32
  93. package/dist/components/AppShell/AppShell.d.ts.map +0 -1
  94. package/dist/components/AppShell/appShell.types.d.ts +0 -62
  95. package/dist/components/AppShell/appShell.types.d.ts.map +0 -1
  96. package/dist/components/AppShell/appShell.variants.d.ts +0 -65
  97. package/dist/components/AppShell/appShell.variants.d.ts.map +0 -1
  98. package/dist/components/Autocomplete/Autocomplete.d.ts +0 -32
  99. package/dist/components/Autocomplete/Autocomplete.d.ts.map +0 -1
  100. package/dist/components/Autocomplete/autocomplete.types.d.ts +0 -214
  101. package/dist/components/Autocomplete/autocomplete.types.d.ts.map +0 -1
  102. package/dist/components/Autocomplete/autocomplete.variants.d.ts +0 -214
  103. package/dist/components/Autocomplete/autocomplete.variants.d.ts.map +0 -1
  104. package/dist/components/Breadcrumbs/Breadcrumbs.d.ts +0 -23
  105. package/dist/components/Breadcrumbs/Breadcrumbs.d.ts.map +0 -1
  106. package/dist/components/Breadcrumbs/breadcrumbs.types.d.ts +0 -98
  107. package/dist/components/Breadcrumbs/breadcrumbs.types.d.ts.map +0 -1
  108. package/dist/components/Breadcrumbs/breadcrumbs.variants.d.ts +0 -85
  109. package/dist/components/Breadcrumbs/breadcrumbs.variants.d.ts.map +0 -1
  110. package/dist/components/Button/Button.d.ts +0 -44
  111. package/dist/components/Button/Button.d.ts.map +0 -1
  112. package/dist/components/Button/button.types.d.ts +0 -66
  113. package/dist/components/Button/button.types.d.ts.map +0 -1
  114. package/dist/components/Button/button.variants.d.ts +0 -104
  115. package/dist/components/Button/button.variants.d.ts.map +0 -1
  116. package/dist/components/Checkbox/Checkbox.d.ts +0 -31
  117. package/dist/components/Checkbox/Checkbox.d.ts.map +0 -1
  118. package/dist/components/Checkbox/checkbox.types.d.ts +0 -86
  119. package/dist/components/Checkbox/checkbox.types.d.ts.map +0 -1
  120. package/dist/components/Checkbox/checkbox.variants.d.ts +0 -109
  121. package/dist/components/Checkbox/checkbox.variants.d.ts.map +0 -1
  122. package/dist/components/ConfirmDialog/ConfirmDialog.d.ts +0 -38
  123. package/dist/components/ConfirmDialog/ConfirmDialog.d.ts.map +0 -1
  124. package/dist/components/ConfirmDialog/confirmDialog.types.d.ts +0 -80
  125. package/dist/components/ConfirmDialog/confirmDialog.types.d.ts.map +0 -1
  126. package/dist/components/ConfirmDialog/confirmDialog.variants.d.ts +0 -90
  127. package/dist/components/ConfirmDialog/confirmDialog.variants.d.ts.map +0 -1
  128. package/dist/components/DateTimePicker/DateTimePicker.d.ts +0 -49
  129. package/dist/components/DateTimePicker/DateTimePicker.d.ts.map +0 -1
  130. package/dist/components/DateTimePicker/dateTimePicker.types.d.ts +0 -95
  131. package/dist/components/DateTimePicker/dateTimePicker.types.d.ts.map +0 -1
  132. package/dist/components/DateTimePicker/dateTimePicker.variants.d.ts +0 -165
  133. package/dist/components/DateTimePicker/dateTimePicker.variants.d.ts.map +0 -1
  134. package/dist/components/LeftNav/LeftNav.d.ts +0 -34
  135. package/dist/components/LeftNav/LeftNav.d.ts.map +0 -1
  136. package/dist/components/LeftNav/leftNav.types.d.ts +0 -136
  137. package/dist/components/LeftNav/leftNav.types.d.ts.map +0 -1
  138. package/dist/components/LeftNav/leftNav.variants.d.ts +0 -143
  139. package/dist/components/LeftNav/leftNav.variants.d.ts.map +0 -1
  140. package/dist/components/NumberField/NumberField.d.ts +0 -18
  141. package/dist/components/NumberField/NumberField.d.ts.map +0 -1
  142. package/dist/components/NumberField/numberField.types.d.ts +0 -79
  143. package/dist/components/NumberField/numberField.types.d.ts.map +0 -1
  144. package/dist/components/NumberField/numberField.variants.d.ts +0 -169
  145. package/dist/components/NumberField/numberField.variants.d.ts.map +0 -1
  146. package/dist/components/OTPField/OTPField.d.ts +0 -25
  147. package/dist/components/OTPField/OTPField.d.ts.map +0 -1
  148. package/dist/components/OTPField/otpField.types.d.ts +0 -74
  149. package/dist/components/OTPField/otpField.types.d.ts.map +0 -1
  150. package/dist/components/OTPField/otpField.variants.d.ts +0 -105
  151. package/dist/components/OTPField/otpField.variants.d.ts.map +0 -1
  152. package/dist/components/RadioGroup/RadioGroup.d.ts +0 -35
  153. package/dist/components/RadioGroup/RadioGroup.d.ts.map +0 -1
  154. package/dist/components/RadioGroup/radioGroup.types.d.ts +0 -103
  155. package/dist/components/RadioGroup/radioGroup.types.d.ts.map +0 -1
  156. package/dist/components/RadioGroup/radioGroup.variants.d.ts +0 -166
  157. package/dist/components/RadioGroup/radioGroup.variants.d.ts.map +0 -1
  158. package/dist/components/Snackbar/Snackbar.d.ts +0 -37
  159. package/dist/components/Snackbar/Snackbar.d.ts.map +0 -1
  160. package/dist/components/Snackbar/snackbar.types.d.ts +0 -86
  161. package/dist/components/Snackbar/snackbar.types.d.ts.map +0 -1
  162. package/dist/components/Snackbar/snackbar.variants.d.ts +0 -153
  163. package/dist/components/Snackbar/snackbar.variants.d.ts.map +0 -1
  164. package/dist/components/Switch/Switch.d.ts +0 -15
  165. package/dist/components/Switch/Switch.d.ts.map +0 -1
  166. package/dist/components/Switch/switch.types.d.ts +0 -47
  167. package/dist/components/Switch/switch.types.d.ts.map +0 -1
  168. package/dist/components/Switch/switch.variants.d.ts +0 -105
  169. package/dist/components/Switch/switch.variants.d.ts.map +0 -1
  170. package/dist/components/TextField/TextField.d.ts +0 -25
  171. package/dist/components/TextField/TextField.d.ts.map +0 -1
  172. package/dist/components/TextField/textField.types.d.ts +0 -72
  173. package/dist/components/TextField/textField.types.d.ts.map +0 -1
  174. package/dist/components/TextField/textField.variants.d.ts +0 -160
  175. package/dist/components/TextField/textField.variants.d.ts.map +0 -1
  176. package/dist/components/Textarea/Textarea.d.ts +0 -16
  177. package/dist/components/Textarea/Textarea.d.ts.map +0 -1
  178. package/dist/components/Textarea/textarea.types.d.ts +0 -63
  179. package/dist/components/Textarea/textarea.types.d.ts.map +0 -1
  180. package/dist/components/Textarea/textarea.variants.d.ts +0 -197
  181. package/dist/components/Textarea/textarea.variants.d.ts.map +0 -1
  182. package/dist/components/TopBar/TopBar.d.ts +0 -33
  183. package/dist/components/TopBar/TopBar.d.ts.map +0 -1
  184. package/dist/components/TopBar/topBar.types.d.ts +0 -49
  185. package/dist/components/TopBar/topBar.types.d.ts.map +0 -1
  186. package/dist/components/TopBar/topBar.variants.d.ts +0 -79
  187. package/dist/components/TopBar/topBar.variants.d.ts.map +0 -1
  188. package/dist/components/_shared/resolveValidationState.d.ts +0 -42
  189. package/dist/components/_shared/resolveValidationState.d.ts.map +0 -1
  190. package/dist/hooks/useAccessState.d.ts +0 -37
  191. package/dist/hooks/useAccessState.d.ts.map +0 -1
  192. package/dist/index.d.ts.map +0 -1
  193. package/dist/tsconfig.lib.tsbuildinfo +0 -1
  194. package/dist/utils/cn.d.ts +0 -26
  195. package/dist/utils/cn.d.ts.map +0 -1
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,6 +12,237 @@ 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.2.1-beta] — 2026-05-17
16
+
17
+ **Hardening release.** Four targeted fixes — three in form-control
18
+ runtime behaviour, one in the Button accessibility contract — surfaced
19
+ while building live-preview demos for the docs site. No public API
20
+ change on any component; strictly additive on the `<Button>` props
21
+ contract (a new `aria-busy` attribute is emitted automatically when
22
+ `loading` is true). Drop-in upgrade from `0.2.0-beta`.
23
+
24
+ Theme of the three form-control fixes: the **same root cause** —
25
+ "controlled-without-an-owner" — under three different surface
26
+ appearances. In standalone uncontrolled mode (no `DashFormProvider`,
27
+ no `value` / `checked` prop, only `defaultValue` / `defaultChecked`),
28
+ each component was sitting in a controlled mode without anyone able
29
+ to update the controlled prop on the user's keystrokes / clicks, so
30
+ React would snap the input right back. The fixes vary by component
31
+ implementation (Radix-backed → discriminated spread of `value` vs
32
+ `defaultValue`; Radix indicator → drop `forceMount` + React
33
+ conditional; native `<input>` → local `useState` for the uncontrolled
34
+ case) but the pattern is identical. A11Y.md (new doc, separate
35
+ commit) documents the broader pattern audit.
36
+
37
+ ### Fixed
38
+
39
+ - **Checkbox** — the indicator's check glyph never appeared when the
40
+ user clicked a Checkbox that was rendered standalone-uncontrolled
41
+ (no `DashFormProvider`, no `checked` prop). The control turned blue
42
+ via `data-[state=checked]:bg-primary-500` but the React-conditional
43
+ `<CheckIcon />` was gated on a stale `resolvedChecked` snapshot.
44
+ Dropped `forceMount` + the conditional; the Radix `Indicator` now
45
+ owns the mount decision, tracking Radix's internal `data-state`
46
+ directly. Mounts in all three modes (controlled, uncontrolled,
47
+ bridge). 14/14 tests pass.
48
+ - **RadioGroup** — clicking a different radio in standalone-uncontrolled
49
+ mode had no visible effect (the selection snapped back to
50
+ `defaultValue`). `<RadixRadioGroup.Root>` was passed `value={…}`
51
+ always, putting Radix in controlled mode against a never-updated
52
+ snapshot. Discriminated spread now picks `value` only in form mode
53
+ or when the consumer explicitly passes `value`; standalone-with-only-
54
+ `defaultValue` uses `defaultValue` so Radix manages its own state.
55
+ 11/11 tests pass.
56
+ - **NumberField** — typing into the input or clicking the +/− stepper
57
+ had no visible effect in standalone-uncontrolled mode, for the same
58
+ reason (controlled `<input value={…}>` with no setter). Added a
59
+ local `useState<string>` seeded from `defaultValue` (mirrors the
60
+ OTPField pattern); `handleChange` + `stepBy` now both update it.
61
+ 8/8 tests pass.
62
+ - **Button** — sets `aria-busy={true}` while `loading`, so assistive
63
+ tech distinguishes "wait for the action to finish" from plain
64
+ "disabled" (which previously was the only signal — same DOM
65
+ attribute regardless of whether the disable came from `loading`,
66
+ `disabled={true}`, or RBAC). 19/19 tests pass.
67
+
68
+ ### Internal
69
+
70
+ - A11Y.md added at package root — per-component status table mapped
71
+ to WCAG 2.1 AA / WAI-ARIA APG. Documents that 23 of 24 components
72
+ were already conformant pre-release (only Button needed the
73
+ `aria-busy` enhancement above). Known non-blocking limitations
74
+ filed: AppShell mobile drawer focus trap, `prefers-reduced-motion`
75
+ pass, color-contrast CI suite, lighthouse/axe automated scan.
76
+
77
+ ### Compatibility
78
+
79
+ | Compatibility axis | Pre-`0.2.1` | Post-`0.2.1` |
80
+ |---|---|---|
81
+ | Public API surface | unchanged | unchanged + `aria-busy` auto-emitted on `<Button loading>` |
82
+ | Peer deps | `react ^18 \|\| ^19`, `tw-theme workspace`, `tw-tokens workspace` | unchanged |
83
+ | Bridge deps | `forms` / `rbac` / `ui-core` `workspace:*` | unchanged |
84
+
85
+ ## [0.2.0-beta] — 2026-05-17
86
+
87
+ **Foundation release.** Eight layout / structural primitives added on top of
88
+ the F3–F7 component catalogue (16 components → 24), plus a coverage
89
+ hardening pass bringing the package from **460 → 592 unit tests** across
90
+ **32 files**. End-to-end validated in the `dash` consumer app: mount
91
+ **12.1 ms** / re-render **7–8.6 ms** for a page with 50+ primitive
92
+ instances.
93
+
94
+ **No public API change** on any of the 16 previously-shipped components
95
+ — strictly additive minor bump. Consumers upgrading from `0.1.0-beta`
96
+ can adopt the new primitives incrementally; existing code keeps working
97
+ unchanged.
98
+
99
+ ### Added — Foundation primitives (F9)
100
+
101
+ The `Box ≠ flex`, `Stack = flex 1D`, `Grid = flex 2D` rule is the spine of
102
+ this layer: each primitive has a single, non-overlapping responsibility so
103
+ "which one do I use?" has one answer per scenario. The rule is enforced at
104
+ the TypeScript prop type level — `<Box direction="row">` is a compile error.
105
+
106
+ - **`Typography`** — semantic typed text. Twelve variants (`h1`–`h6`,
107
+ `subtitle1/2`, `body1/2`, `caption`, `overline`) × nine intent colors ×
108
+ five weight overrides + alignment + truncate / noWrap / gutterBottom.
109
+ Default HTML tag inferred from variant (h1→`<h1>`, body1→`<p>`, …),
110
+ overridable via `as` or `asChild` (Radix Slot). Reads color from a
111
+ parent `<Box>` via `color="inherit"`.
112
+ Source: `src/components/Typography/{Typography.tsx, typography.types.ts, typography.variants.ts}`.
113
+
114
+ - **`Box`** — surface primitive consolidating MUI's Box + Paper + Card +
115
+ Joy Surface into one. Five variants (`plain` · `outlined` · `elevated` ·
116
+ `soft` · `solid`) × seven intent colors = **21 compound visuals**
117
+ emitted by the TV recipe + six elevation levels (`0`–`5`) + token-scale
118
+ spacing (`p`/`px`/`py`/`m`/`mx`/`my`) + rounded scale +
119
+ `fullWidth`/`fullHeight`. Strictly no flex / no grid by design — wrap
120
+ in Stack/Grid for layout.
121
+ Source: `src/components/Box/box.variants.ts` (compound matrix lives here).
122
+
123
+ - **`Stack`** — the **only** flex container in `@dashforge/tw`.
124
+ `direction` + `align` + `justify` + token-scale `gap` + `wrap`, plus a
125
+ runtime `divider` prop that inserts N-1 separators between children
126
+ (`React.Children.toArray` semantics: Fragments count as one child —
127
+ documented in `Stack.tsx` header + asserted in the test suite).
128
+ Source: `src/components/Stack/{Stack.tsx, stack.types.ts, stack.variants.ts}`.
129
+
130
+ - **`Grid`** — CSS Grid container + item, polymorphic in role. MUI v2 API
131
+ surface (`<Grid container>` + `<Grid xs={6}>`) backed by **real CSS
132
+ Grid** (`display: grid` + `col-span-*`), not flexbox like MUI v2's own
133
+ internals. Discriminated-union TypeScript: `<Grid container xs={6}>`
134
+ fails compilation. 70-entry responsive `col-span` mapping (xs/sm/md/lg/xl
135
+ × `1..12/auto/full`) in the TV recipe.
136
+ Source: `src/components/Grid/{Grid.tsx, grid.types.ts, grid.variants.ts}`.
137
+
138
+ ### Added — Foundation completions (F10)
139
+
140
+ Closes the foundation surface to match what Chakra/Mantine/Joy ship at
141
+ the layout-primitive level.
142
+
143
+ - **`Container`** — centered max-width page wrapper with the canonical
144
+ responsive padding ramp (`px-4 sm:px-6 lg:px-8`). Six sizes
145
+ (`sm` / `md` / `lg` / `xl` / `2xl` / `fluid`) mapped to Tailwind's
146
+ `max-w-screen-*` aliases + `centerContent` opt-in for marketing /
147
+ sign-in layouts.
148
+ Source: `src/components/Container/{Container.tsx, container.types.ts, container.variants.ts}`.
149
+
150
+ - **`Divider`** — visual separator with two rendering modes selected by
151
+ `children` presence. Line-only renders `<hr>` with `role="separator"` +
152
+ `aria-orientation`; labeled mode renders the "OR" separator pattern as
153
+ two flex segments around the label, with a 32 px stub on the squashed
154
+ side for `align="start"` / `"end"`. orientation × variant
155
+ (solid/dashed/dotted) × color (7 intents) × align (3) axes.
156
+ Source: `src/components/Divider/{Divider.tsx, divider.types.ts, divider.variants.ts}`.
157
+
158
+ - **`AspectRatio`** — content-shape primitive using the **native CSS
159
+ `aspect-ratio` property** (supported since 2021, ~98% browser coverage).
160
+ Number or CSS-string ratio. Pairs with `sx="rounded-xl overflow-hidden"`
161
+ for the canonical clipped media pattern (documented as the #1 gotcha
162
+ in `AspectRatio.tsx` header and the public MDX docs).
163
+ Source: `src/components/AspectRatio/{AspectRatio.tsx, aspectRatio.types.ts}`.
164
+
165
+ - **`VisuallyHidden`** — the a11y primitive. Uses Tailwind's `sr-only`
166
+ utility (WebAIM clip technique). Hides children from sighted users
167
+ while keeping them in the accessibility tree — icon button labels,
168
+ status announcers (`aria-live="polite"`), skip links. Default tag is
169
+ `<span>` for the 99% case (inline label inside a button or link).
170
+ Source: `src/components/VisuallyHidden/{VisuallyHidden.tsx, visuallyHidden.types.ts}`.
171
+
172
+ ### Internal
173
+
174
+ - **+132 edge case unit tests** added (`460 → 592` total across `32`
175
+ files). Reorganised here under `Internal` rather than `Added` because
176
+ the tests are not part of the public API surface.
177
+
178
+ - **Box (+33)** — every one of the 21 compound surface variants
179
+ (outlined / soft / solid × 7 intents) asserted explicitly with the
180
+ light + dark pair; plain / elevated color-agnostic invariants;
181
+ spacing axis coexistence (`p` + `px` + `py` × tailwind-merge
182
+ precedence); `elevation` × `variant` interaction; rounded edge
183
+ values. File: `src/components/Box/Box.test.tsx`.
184
+
185
+ - **Grid (+38)** — every responsive breakpoint × representative span
186
+ enumeration (5 × 5 = 25), full cascade test (`xs={12} sm={6} md={4}
187
+ lg={3} xl={2}`), every `autoFlow` value, every `cols` value,
188
+ `spacingX` / `spacingY` independence, empty container + orphan item
189
+ handling, deep nesting (Grid inside Grid item). File:
190
+ `src/components/Grid/Grid.test.tsx`.
191
+
192
+ - **Stack (+29)** — array divider, conditional / null children, mixed
193
+ text + element children, nested Stack with divider, divider key
194
+ stability across re-renders, every gap step (11 token values),
195
+ every `align`/`justify` value (11), empty Stack with and without
196
+ divider. File: `src/components/Stack/Stack.test.tsx`.
197
+
198
+ - **Typography / Container / Divider / AspectRatio / VisuallyHidden
199
+ (+32)** — multi-axis combinations (`variant` + `color` + `weight` +
200
+ `align` + `truncate` + `gutterBottom`), full variant catalogues
201
+ iterated with `it.each`, extreme ratio values (21/9, 9/16, 0.5, 3),
202
+ nested fluid/capped Container pattern, `aria-live="polite"`
203
+ announcer pattern.
204
+
205
+ - **End-to-end consumer validation.** New page
206
+ `~/projects/web/learn/dash/src/pages/TestFoundation.tsx` (linked into
207
+ the `dash` consumer app via a `file:` package override) mounts all
208
+ eight primitives inside `<DashforgeTailwindProvider>` wrapped in a
209
+ React `<Profiler>` with `onRender` logger. Measured: **mount 12.1 ms,
210
+ update 7–8.6 ms** for a page with 50+ primitive instances — within the
211
+ 60 fps frame budget. No `React.memo` applied — the primitives are
212
+ pure (no `useState`, no `useEffect`, only className resolution), so
213
+ React's reconciler trivially handles the re-render.
214
+
215
+ ### Architecture
216
+
217
+ - **`Box ≠ flex`, `Stack = flex 1D`, `Grid = flex 2D`** — single
218
+ responsibility per primitive, enforced at the TypeScript prop type
219
+ level. `Box` exposes no `display` / `flex*` / `grid*` props at all —
220
+ trying to pass them fails compilation. The rule rules out the MUI
221
+ failure mode where every `<Box display="flex" gap={2}>` quietly becomes
222
+ the de facto flex container of the codebase, drowning the
223
+ surface-vs-layout distinction. When you read `<Stack>` in a JSX tree
224
+ you know it's flex without reading any further.
225
+
226
+ ### Compatibility
227
+
228
+ - **Peer deps unchanged**: `@dashforge/tw-theme@^0.1.0-beta` and
229
+ `@dashforge/tw-tokens@^0.1.0-beta` (neither package was modified in
230
+ this cycle). Bridge layer (`@dashforge/forms`, `@dashforge/ui-core`,
231
+ `@dashforge/rbac`) likewise unchanged at the workspace `0.2.3-beta`
232
+ version.
233
+
234
+ - **No breaking changes**: the public API of the 16 previously-shipped
235
+ components is byte-identical. Diff `0.1.0-beta..0.2.0-beta` against
236
+ `src/index.ts` shows only additive exports (the new Foundation
237
+ primitives + their `*Variants` recipes + their `*Props` types).
238
+
239
+ - **Bundle size impact** (gzipped, when fully exercised):
240
+ `dist/index.esm.js` grew from 255 KB to 272 KB (+17 KB / +6.7%) for
241
+ the eight new primitives. Tree-shaking unaffected — consumers only
242
+ pay for what they import.
243
+
244
+ ---
245
+
15
246
  ## [0.1.0-beta] — 2026-05-16
16
247
 
17
248
  First public beta. Sixteen components shipped across forms, layout, and
package/dist/index.d.ts CHANGED
@@ -1,82 +1 @@
1
- /**
2
- * @dashforge/tw
3
- *
4
- * Tailwind-rendered UI components for the Dashforge ecosystem.
5
- *
6
- * Architectural contract (plan v2 — 2026-05-15):
7
- * - **Props-driven**: each Tailwind utility that matters for the public
8
- * API becomes a typed prop. `sx` + `slotProps` are escape hatches.
9
- * - **Tokens as source of truth**: components reference colors /
10
- * spacing / radius / fontSize through `@dashforge/tw-tokens` via the
11
- * `dashforgePreset()` from `@dashforge/tw-theme`.
12
- * - **Shared bridge with MUI side**: both ecosystems consume
13
- * `@dashforge/forms` + `@dashforge/ui-core` + `@dashforge/rbac`.
14
- * No styling code is shared.
15
- * - **Variant API**: `tailwind-variants` (TV) with `slots` +
16
- * `compoundVariants`.
17
- *
18
- * F3 surface (tier-1, this release):
19
- * - `<Button>` (action, RBAC only, `asChild` polymorphism)
20
- * - `<TextField>` (bridge-integrated, 7 slots)
21
- * - `<Checkbox>` (bridge-integrated, Radix Checkbox primitive)
22
- * - `<Switch>` (bridge-integrated, Radix Switch primitive)
23
- *
24
- * @module @dashforge/tw
25
- */
26
- export { Button } from './components/Button/Button.js';
27
- export type { ButtonProps } from './components/Button/button.types.js';
28
- export { buttonVariants } from './components/Button/button.variants.js';
29
- export { TextField } from './components/TextField/TextField.js';
30
- export type { TextFieldProps, TextFieldSlotProps, } from './components/TextField/textField.types.js';
31
- export { textFieldVariants } from './components/TextField/textField.variants.js';
32
- export { Checkbox } from './components/Checkbox/Checkbox.js';
33
- export type { CheckboxProps, CheckboxSlotProps, } from './components/Checkbox/checkbox.types.js';
34
- export { checkboxVariants } from './components/Checkbox/checkbox.variants.js';
35
- export { Switch } from './components/Switch/Switch.js';
36
- export type { SwitchProps, SwitchSlotProps, } from './components/Switch/switch.types.js';
37
- export { switchVariants } from './components/Switch/switch.variants.js';
38
- export { RadioGroup } from './components/RadioGroup/RadioGroup.js';
39
- export type { RadioGroupProps, RadioGroupOption, RadioGroupSlotProps, } from './components/RadioGroup/radioGroup.types.js';
40
- export { radioGroupVariants } from './components/RadioGroup/radioGroup.variants.js';
41
- export { Textarea } from './components/Textarea/Textarea.js';
42
- export type { TextareaProps, TextareaSlotProps, } from './components/Textarea/textarea.types.js';
43
- export { textareaVariants } from './components/Textarea/textarea.variants.js';
44
- export { NumberField } from './components/NumberField/NumberField.js';
45
- export type { NumberFieldProps, NumberFieldSlotProps, } from './components/NumberField/numberField.types.js';
46
- export { numberFieldVariants } from './components/NumberField/numberField.variants.js';
47
- export { OTPField } from './components/OTPField/OTPField.js';
48
- export type { OTPFieldProps, OTPFieldSlotProps, OTPFieldMode, } from './components/OTPField/otpField.types.js';
49
- export { otpFieldVariants } from './components/OTPField/otpField.variants.js';
50
- export { Autocomplete } from './components/Autocomplete/Autocomplete.js';
51
- export type { AutocompleteProps, AutocompleteOption, AutocompleteSlotProps, AutocompleteValue, } from './components/Autocomplete/autocomplete.types.js';
52
- export { autocompleteVariants } from './components/Autocomplete/autocomplete.variants.js';
53
- export { DateTimePicker, isoToInputValue, } from './components/DateTimePicker/DateTimePicker.js';
54
- export type { DateTimePickerProps, DateTimePickerMode, DateTimePickerSlotProps, } from './components/DateTimePicker/dateTimePicker.types.js';
55
- export { dateTimePickerVariants } from './components/DateTimePicker/dateTimePicker.variants.js';
56
- export { Breadcrumbs } from './components/Breadcrumbs/Breadcrumbs.js';
57
- export type { BreadcrumbsProps, BreadcrumbItem, BreadcrumbsSlotProps, BreadcrumbLinkComponent, } from './components/Breadcrumbs/breadcrumbs.types.js';
58
- export { breadcrumbsVariants } from './components/Breadcrumbs/breadcrumbs.variants.js';
59
- export { LeftNav } from './components/LeftNav/LeftNav.js';
60
- export type { LeftNavProps, LeftNavNode, LeftNavItem, LeftNavGroup, LeftNavSlotProps, LeftNavLinkComponent, } from './components/LeftNav/leftNav.types.js';
61
- export { leftNavVariants } from './components/LeftNav/leftNav.variants.js';
62
- export { TopBar } from './components/TopBar/TopBar.js';
63
- export type { TopBarProps, TopBarSlotProps, } from './components/TopBar/topBar.types.js';
64
- export { topBarVariants } from './components/TopBar/topBar.variants.js';
65
- export { AppShell } from './components/AppShell/AppShell.js';
66
- export type { AppShellProps, AppShellSlotProps, } from './components/AppShell/appShell.types.js';
67
- export { appShellVariants } from './components/AppShell/appShell.variants.js';
68
- export { ConfirmDialogProvider, useConfirm, } from './components/ConfirmDialog/ConfirmDialog.js';
69
- export type { ConfirmDialogProviderProps, ConfirmDialogSlotProps, ConfirmFn, ConfirmOptions, ConfirmSeverity, } from './components/ConfirmDialog/confirmDialog.types.js';
70
- export { confirmDialogVariants } from './components/ConfirmDialog/confirmDialog.variants.js';
71
- export { SnackbarProvider, useSnackbar, } from './components/Snackbar/Snackbar.js';
72
- export type { SnackbarProviderProps, SnackbarApi, SnackbarOptions, SnackbarRecord, SnackbarSeverity, SnackbarPosition, SnackbarSlotProps, } from './components/Snackbar/snackbar.types.js';
73
- export { snackbarVariants } from './components/Snackbar/snackbar.variants.js';
74
- export { useAccessState } from './hooks/useAccessState.js';
75
- export { cn } from './utils/cn.js';
76
- export { tv } from 'tailwind-variants';
77
- export type { VariantProps } from 'tailwind-variants';
78
- /**
79
- * Package version (synced with `package.json` at publish time).
80
- */
81
- export declare const VERSION = "0.1.0-beta";
82
- //# sourceMappingURL=index.d.ts.map
1
+ export * from "./src/index";