@dashforge/tw 0.2.1-beta → 0.4.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.
Files changed (86) hide show
  1. package/CHANGELOG.md +257 -0
  2. package/CONSUMER-VALIDATION.md +130 -0
  3. package/PARITY.md +483 -0
  4. package/PERFORMANCE.md +169 -0
  5. package/dist/index.esm.js +801 -42
  6. package/dist/src/components/Accordion/Accordion.d.ts +21 -0
  7. package/dist/src/components/Accordion/Accordion.d.ts.map +1 -0
  8. package/dist/src/components/Accordion/accordion.types.d.ts +74 -0
  9. package/dist/src/components/Accordion/accordion.types.d.ts.map +1 -0
  10. package/dist/src/components/Accordion/accordion.variants.d.ts +58 -0
  11. package/dist/src/components/Accordion/accordion.variants.d.ts.map +1 -0
  12. package/dist/src/components/AppShell/AppShell.d.ts +14 -0
  13. package/dist/src/components/AppShell/AppShell.d.ts.map +1 -1
  14. package/dist/src/components/AppShell/appShell.variants.d.ts.map +1 -1
  15. package/dist/src/components/Autocomplete/Autocomplete.d.ts.map +1 -1
  16. package/dist/src/components/Autocomplete/autocomplete.variants.d.ts.map +1 -1
  17. package/dist/src/components/Breadcrumbs/breadcrumbs.variants.d.ts.map +1 -1
  18. package/dist/src/components/Checkbox/Checkbox.d.ts.map +1 -1
  19. package/dist/src/components/Dialog/Dialog.d.ts +17 -0
  20. package/dist/src/components/Dialog/Dialog.d.ts.map +1 -0
  21. package/dist/src/components/Dialog/dialog.types.d.ts +75 -0
  22. package/dist/src/components/Dialog/dialog.types.d.ts.map +1 -0
  23. package/dist/src/components/Dialog/dialog.variants.d.ts +80 -0
  24. package/dist/src/components/Dialog/dialog.variants.d.ts.map +1 -0
  25. package/dist/src/components/LeftNav/leftNav.variants.d.ts.map +1 -1
  26. package/dist/src/components/Popover/Popover.d.ts +15 -0
  27. package/dist/src/components/Popover/Popover.d.ts.map +1 -0
  28. package/dist/src/components/Popover/popover.types.d.ts +45 -0
  29. package/dist/src/components/Popover/popover.types.d.ts.map +1 -0
  30. package/dist/src/components/Popover/popover.variants.d.ts +34 -0
  31. package/dist/src/components/Popover/popover.variants.d.ts.map +1 -0
  32. package/dist/src/components/Snackbar/snackbar.variants.d.ts.map +1 -1
  33. package/dist/src/components/Switch/switch.variants.d.ts.map +1 -1
  34. package/dist/src/components/Tabs/Tabs.d.ts +15 -0
  35. package/dist/src/components/Tabs/Tabs.d.ts.map +1 -0
  36. package/dist/src/components/Tabs/tabs.types.d.ts +55 -0
  37. package/dist/src/components/Tabs/tabs.types.d.ts.map +1 -0
  38. package/dist/src/components/Tabs/tabs.variants.d.ts +82 -0
  39. package/dist/src/components/Tabs/tabs.variants.d.ts.map +1 -0
  40. package/dist/src/components/TextField/TextField.d.ts.map +1 -1
  41. package/dist/src/components/TextField/textField.types.d.ts +29 -3
  42. package/dist/src/components/TextField/textField.types.d.ts.map +1 -1
  43. package/dist/src/components/TextField/textField.variants.d.ts +6 -0
  44. package/dist/src/components/TextField/textField.variants.d.ts.map +1 -1
  45. package/dist/src/components/Tooltip/Tooltip.d.ts +29 -0
  46. package/dist/src/components/Tooltip/Tooltip.d.ts.map +1 -0
  47. package/dist/src/components/Tooltip/tooltip.types.d.ts +47 -0
  48. package/dist/src/components/Tooltip/tooltip.types.d.ts.map +1 -0
  49. package/dist/src/components/Tooltip/tooltip.variants.d.ts +34 -0
  50. package/dist/src/components/Tooltip/tooltip.variants.d.ts.map +1 -0
  51. package/dist/src/index.d.ts +16 -1
  52. package/dist/src/index.d.ts.map +1 -1
  53. package/package.json +6 -1
  54. package/src/components/Accordion/Accordion.test.tsx +95 -0
  55. package/src/components/Accordion/Accordion.tsx +97 -0
  56. package/src/components/Accordion/accordion.types.ts +66 -0
  57. package/src/components/Accordion/accordion.variants.ts +30 -0
  58. package/src/components/AppShell/AppShell.tsx +126 -1
  59. package/src/components/AppShell/appShell.variants.ts +8 -2
  60. package/src/components/Autocomplete/Autocomplete.tsx +77 -4
  61. package/src/components/Autocomplete/autocomplete.variants.ts +6 -0
  62. package/src/components/Breadcrumbs/breadcrumbs.variants.ts +5 -0
  63. package/src/components/Checkbox/Checkbox.tsx +43 -9
  64. package/src/components/Dialog/Dialog.test.tsx +96 -0
  65. package/src/components/Dialog/Dialog.tsx +93 -0
  66. package/src/components/Dialog/dialog.types.ts +62 -0
  67. package/src/components/Dialog/dialog.variants.ts +62 -0
  68. package/src/components/LeftNav/leftNav.variants.ts +12 -1
  69. package/src/components/Popover/Popover.test.tsx +72 -0
  70. package/src/components/Popover/Popover.tsx +55 -0
  71. package/src/components/Popover/popover.types.ts +42 -0
  72. package/src/components/Popover/popover.variants.ts +18 -0
  73. package/src/components/Snackbar/snackbar.variants.ts +4 -2
  74. package/src/components/Switch/switch.variants.ts +6 -1
  75. package/src/components/Tabs/Tabs.test.tsx +74 -0
  76. package/src/components/Tabs/Tabs.tsx +66 -0
  77. package/src/components/Tabs/tabs.types.ts +49 -0
  78. package/src/components/Tabs/tabs.variants.ts +58 -0
  79. package/src/components/TextField/TextField.tsx +29 -0
  80. package/src/components/TextField/textField.types.ts +23 -3
  81. package/src/components/TextField/textField.variants.ts +18 -0
  82. package/src/components/Tooltip/Tooltip.test.tsx +73 -0
  83. package/src/components/Tooltip/Tooltip.tsx +70 -0
  84. package/src/components/Tooltip/tooltip.types.ts +44 -0
  85. package/src/components/Tooltip/tooltip.variants.ts +18 -0
  86. package/src/index.ts +40 -1
package/PARITY.md ADDED
@@ -0,0 +1,483 @@
1
+ # `@dashforge/tw` ↔ `@dashforge/ui` parity audit
2
+
3
+ > Sprint 3 P1 deliverable (2026-05-19). Per-component diff between the
4
+ > two Dashforge UI lines for the **10 bridge-integrated components**.
5
+ >
6
+ > **Motivation (revised post-Sprint 2).** This audit does NOT exist to
7
+ > support customer migration MUI→TW (no consumer switches across UI
8
+ > ecosystems in real life). It exists because:
9
+ >
10
+ > 1. **Internal consistency** — a refactor on `@dashforge/ui` that
11
+ > doesn't land on `@dashforge/tw` would otherwise be invisible.
12
+ > 2. **Bridge contract validation** — proves that the same
13
+ > `@dashforge/forms` schema works identically on both lines.
14
+ > 3. **Foundation for Sprint 5 starter kits** — two parallel kits
15
+ > (one MUI, one TW) require call-site-level parity to stay
16
+ > parallel-maintainable.
17
+
18
+ ## Versions audited
19
+
20
+ | Lib | Version | Date |
21
+ |---|---|---|
22
+ | `@dashforge/tw` | `0.3.0-beta` | 2026-05-18 |
23
+ | `@dashforge/ui` | `0.2.3-beta` | 2026-05-15 |
24
+
25
+ ## Scoring legend
26
+
27
+ | Symbol | Meaning |
28
+ |---|---|
29
+ | ✓ | Parity verified — the two lines accept the same prop / behave the same way at the call-site level. |
30
+ | ⚠ | Intentional delta — different prop or slightly different behavior, motivated and documented in the per-component section below. |
31
+ | ✗ | Unintended drift — bug or oversight, files a follow-up issue. |
32
+
33
+ ## Bridge contract — universal across all 10 components
34
+
35
+ Both libs share an identical bridge contract. The following props
36
+ behave **byte-identical** in both lines (verified by reading the
37
+ types files of all 10 components):
38
+
39
+ | Prop | Type | Semantics |
40
+ |---|---|---|
41
+ | `name` | `string` (required) | Field name registered with the `DashFormProvider` bridge. |
42
+ | `rules` | `unknown` | Validation rules forwarded to the bridge engine — opaque to the component. |
43
+ | `visibleWhen` | `(engine: Engine) => boolean` | Engine predicate. Component returns `null` when false. |
44
+ | `access` | `AccessRequirement` | RBAC requirement. Behaviors (`hide` / `disable` / `readonly`) identical in both libs. |
45
+ | `helperText` | `ReactNode` | Helper text slot below the control. Auto-replaced with bridge error message when (a) error is true and (b) the Form Closure v1 gate passes (`touched \|\| submitCount > 0`). |
46
+ | `error` | `boolean` | Explicit error semaphore override. Cuts ahead of the bridge's auto-error. |
47
+ | `label` | `ReactNode` | Visible label. |
48
+
49
+ The bridge integration semantics (Form Closure v1 error gating, prop
50
+ precedence, granular per-field subscriptions, StrictMode-safe
51
+ unregister on unmount) are implemented identically across both lines
52
+ via the shared `@dashforge/ui-core` + `@dashforge/forms` packages.
53
+
54
+ This is the **portability layer**: any consumer who swaps
55
+ `@dashforge/ui` for `@dashforge/tw` (or vice versa) keeps their
56
+ form schemas, validation rules, and RBAC declarations unchanged.
57
+
58
+ ## Per-component parity table
59
+
60
+ | # | Component | Signature | Behavior | Variant axis | Bridge | Overall |
61
+ |---|---|---|---|---|---|---|
62
+ | 1 | Button | ✓ | ✓ | ⚠ | ✓ | ⚠ |
63
+ | 2 | TextField | ✓ | ✓ | ⚠ | ✓ | ⚠ |
64
+ | 3 | Checkbox | ⚠ | ✓ | ✓ | ✓ | ⚠ |
65
+ | 4 | Switch | ⚠ | ✓ | ✓ | ✓ | ⚠ |
66
+ | 5 | RadioGroup | ⚠ | ✓ | ⚠ | ✓ | ⚠ |
67
+ | 6 | Textarea | ⚠ | ✓ | ⚠ | ✓ | ⚠ |
68
+ | 7 | NumberField | ✓ | ✓ | ⚠ | ✓ | ⚠ |
69
+ | 8 | OTPField | ✓ | ✓ | ⚠ | ✓ | ⚠ |
70
+ | 9 | Autocomplete | ⚠ | ⚠ | ⚠ | ✓ | ⚠ |
71
+ | 10 | DateTimePicker | ✓ | ✓ | ✓ | ✓ | ✓ |
72
+
73
+ **No `✗` rows** — no unintended drift was discovered. All deltas
74
+ are intentional and motivated below. The single `✓` overall
75
+ (DateTimePicker) is the only component where the two lines are
76
+ truly byte-identical at the prop level.
77
+
78
+ ---
79
+
80
+ ## 1. Button
81
+
82
+ ### Signature
83
+
84
+ | Prop | `@dashforge/ui` (MUI) | `@dashforge/tw` |
85
+ |---|---|---|
86
+ | Inherits | `Omit<MuiButtonProps, 'disabled'>` | `Omit<ButtonHTMLAttributes, 'className' \| 'color'>` + `Pick<TVariants, 'variant' \| 'color' \| 'size' \| 'fullWidth' \| 'loading'>` |
87
+ | `access` | ✓ | ✓ |
88
+ | `disabled` | ✓ | ✓ |
89
+ | `asChild` | ✗ | ✓ (Radix Slot polymorphism) |
90
+ | `sx` | ✗ (MUI sx-object inherited via MuiButtonProps) | ✓ (utility-string) |
91
+ | `loading` | ✗ (consumer adds it manually) | ✓ (built-in variant axis) |
92
+
93
+ ### Variant axis ⚠
94
+
95
+ | Axis | MUI | TW |
96
+ |---|---|---|
97
+ | `variant` | `contained` \| `outlined` \| `text` | `solid` \| `outline` \| `soft` \| `ghost` |
98
+ | `size` | `small` \| `medium` \| `large` | `sm` \| `md` \| `lg` |
99
+ | `color` | `primary` \| `secondary` \| `error` \| `info` \| `success` \| `warning` | same 6 intents |
100
+ | `fullWidth` | ✓ | ✓ |
101
+
102
+ **Intentional delta.** TW renames `contained → solid`, adds `soft`
103
+ + `ghost` variants, renames sizes to short forms. The MUI naming is
104
+ a vestige of Material Design vocabulary; TW adopts the
105
+ shadcn/Radix/Mantine common-denominator naming because that's what
106
+ TW-native developers expect.
107
+
108
+ ### Notes
109
+
110
+ - `loading` is a built-in TW variant (renders spinner + sets
111
+ `aria-busy`). On MUI side it's a consumer-level pattern.
112
+ - TW's `asChild` (Radix Slot) is more powerful than MUI's
113
+ `component`/`href` polymorphism — single API, no DOM doubling.
114
+
115
+ ---
116
+
117
+ ## 2. TextField
118
+
119
+ ### Signature
120
+
121
+ | Prop | MUI | TW |
122
+ |---|---|---|
123
+ | Inherits | `Omit<MuiTextFieldProps, 'name' \| 'SelectProps' \| 'InputProps' \| 'InputLabelProps' \| 'FormHelperTextProps' \| 'inputProps'>` | `Omit<InputHTMLAttributes, 'size' \| 'className'>` + `Pick<TVariants, 'size' \| 'layout' \| 'fullWidth'>` |
124
+ | `layout` | ✓ (`floating` \| `stacked` \| `inline`) | ✓ (same enum) |
125
+ | `required` | ✓ (renders asterisk) | ✓ (same) |
126
+ | `slotProps.prefix/suffix` | ✗ (use `slotProps.input.startAdornment` / `endAdornment` — MUI v9 API) | ✓ (new in `0.3.0-beta` — inline adornments) |
127
+ | `sx` | MUI sx-object | TW utility-string |
128
+
129
+ ### Variant axis ⚠
130
+
131
+ | Axis | MUI | TW |
132
+ |---|---|---|
133
+ | `variant` | `outlined` \| `filled` \| `standard` | not exposed (defaults to `outlined`-equivalent) |
134
+ | `size` | `small` \| `medium` | `sm` \| `md` \| `lg` |
135
+ | `layout` | `floating` \| `stacked` \| `inline` | same |
136
+
137
+ **Intentional delta.** TW intentionally drops MUI's
138
+ `variant="standard"` (underline-only field) — it's a Material Design
139
+ artifact that doesn't fit a typical Tailwind design system. TW also
140
+ drops MUI's `variant="filled"` for now (no design system demand).
141
+
142
+ ### Notes
143
+
144
+ - TW exposes `slotProps.prefix` / `slotProps.suffix` (added in
145
+ `0.3.0-beta`) for inline adornments. MUI achieves the same via
146
+ `slotProps.input.startAdornment` / `endAdornment` — different shape,
147
+ same outcome.
148
+ - MUI's deprecated `inputProps` / `InputProps` / `InputLabelProps` /
149
+ `FormHelperTextProps` are explicitly Omit-ed from TW (cleanly,
150
+ since TW never inherited them in the first place).
151
+
152
+ ---
153
+
154
+ ## 3. Checkbox
155
+
156
+ ### Signature ⚠
157
+
158
+ | Prop | MUI | TW |
159
+ |---|---|---|
160
+ | Inherits | `Omit<MuiCheckboxProps, 'name'>` (inherits MUI's `onChange: (event, checked) => void`) | `Pick<TVariants, 'size'>` (no HTML attrs inherited) |
161
+ | `onChange` | ✓ MUI HTML signature: `(event, checked) => void` | ✗ (not exposed) |
162
+ | `onCheckedChange` | ✗ | ✓ Radix signature: `(checked: boolean) => void` |
163
+ | `checked` / `defaultChecked` | ✓ | ✓ |
164
+ | `slotProps.indicator` | ✗ | ✓ |
165
+
166
+ **Intentional delta.** Callback signature differs: MUI uses the HTML
167
+ `onChange(event, checked)`, TW uses Radix's `onCheckedChange(checked)`.
168
+ A consumer rewriting `<Checkbox onChange={…}>` → `<Checkbox onCheckedChange={…}>`
169
+ is the only call-site change. Documented in `customization.mdx`
170
+ (Sprint 3 P2).
171
+
172
+ ### Variant axis ✓
173
+
174
+ Both libs expose `size` only (`small`/`medium`/`large` MUI ↔
175
+ `sm`/`md`/`lg` TW — same naming convention as Button).
176
+
177
+ ### Notes
178
+
179
+ - TW's Indicator renders BOTH `<CheckIcon />` and `<DashIcon />` and
180
+ toggles via Radix `data-state` (added in `0.3.0-beta`).
181
+ Indeterminate state is handled CSS-only.
182
+ - Bridge contract identical: `useDashFieldMeta(name)` granular
183
+ subscription, Form Closure v1 gating.
184
+
185
+ ---
186
+
187
+ ## 4. Switch
188
+
189
+ ### Signature ⚠
190
+
191
+ Same delta as Checkbox: TW exposes `onCheckedChange(checked)` (Radix),
192
+ MUI exposes `onChange(event, checked)` (HTML). All other props
193
+ identical (name, label, rules, visibleWhen, helperText, error, access,
194
+ checked, defaultChecked, disabled).
195
+
196
+ ### Variant axis ✓
197
+
198
+ Both libs: `size` only.
199
+
200
+ ### Notes
201
+
202
+ - TW gates the thumb-slide animation on `prefers-reduced-motion`
203
+ (added Sprint 2 0.3.0-beta).
204
+
205
+ ---
206
+
207
+ ## 5. RadioGroup
208
+
209
+ ### Signature ⚠
210
+
211
+ | Prop | MUI | TW |
212
+ |---|---|---|
213
+ | Inherits | `Omit<MuiRadioGroupProps, 'name'>` | `RadioGroupVariants` (Pick-style) |
214
+ | `onChange` | ✓ MUI signature | ✗ |
215
+ | `onValueChange` | ✗ | ✓ Radix signature: `(value: string) => void` |
216
+ | `options` | ✓ (`RadioGroupOption[]` — identical shape) | ✓ |
217
+ | `RadioGroupOption.access` | ✓ option-level RBAC | ✓ option-level RBAC |
218
+
219
+ ### Variant axis ⚠
220
+
221
+ | Axis | MUI | TW |
222
+ |---|---|---|
223
+ | `orientation` | inherited from MUI's `row` boolean | dedicated `orientation: 'horizontal' \| 'vertical'` |
224
+ | `size` | inherited (`small`/`medium`) | `sm` \| `md` \| `lg` |
225
+
226
+ **Intentional delta.** TW exposes `orientation` as a typed variant
227
+ axis (vs MUI's legacy `row={true}` boolean). MUI's `row` prop is a
228
+ Material legacy; TW's `orientation` is the Radix/Aria standard.
229
+
230
+ ### Notes
231
+
232
+ - Option-level RBAC is implemented identically in both libs
233
+ (`hide` keeps selected option visible-but-disabled to expose the
234
+ user's previous choice).
235
+ - Group-level access has precedence over option-level access in
236
+ both libs.
237
+
238
+ ---
239
+
240
+ ## 6. Textarea
241
+
242
+ ### Signature ⚠
243
+
244
+ | Prop | MUI | TW |
245
+ |---|---|---|
246
+ | Inherits | `Omit<MuiTextFieldProps, 'name'>` (with internal `multiline={true}` hard-coded) | `Omit<TextareaHTMLAttributes, 'name' \| 'size' \| 'onChange' \| 'onBlur' \| 'value' \| 'defaultValue'>` + `TextareaVariants` |
247
+ | `rows` / `minRows` | ✓ (defaults `minRows={3}`) | ✓ (`rows` defaults to 3) |
248
+ | `resize` | ✗ (CSS native, no prop) | ✓ variant axis (`none` \| `vertical` \| `horizontal` \| `both`) |
249
+
250
+ ### Variant axis ⚠
251
+
252
+ TW adds the `resize` variant axis as a typed prop. MUI relies on the
253
+ consumer adding inline `style={{ resize: 'vertical' }}` or CSS.
254
+
255
+ ### Notes
256
+
257
+ - Form bridge contract identical.
258
+ - TW exposes own `onChange` / `onBlur` typed for `HTMLTextAreaElement`;
259
+ MUI inherits from MUI's `TextFieldProps` typing for the
260
+ multiline-disguised-as-input case.
261
+
262
+ ---
263
+
264
+ ## 7. NumberField
265
+
266
+ ### Signature
267
+
268
+ | Prop | MUI | TW |
269
+ |---|---|---|
270
+ | Inherits | `Omit<MuiTextFieldProps, 'name' \| 'type' \| 'value' \| 'onChange'>` | `Omit<InputHTMLAttributes, 'name' \| 'type' \| 'size' \| ...>` + `NumberFieldVariants` |
271
+ | `value` | `number \| string \| null` | `number \| string \| null` (identical) |
272
+ | `min` / `max` / `step` | inherited HTML attrs | typed at top level |
273
+ | `showStepper` | ✗ | ✓ (TW-only: inline +/− buttons) |
274
+ | `slotProps.stepper` / `slotProps.stepperButton` | ✗ | ✓ |
275
+
276
+ ### Variant axis ⚠
277
+
278
+ TW exposes the stepper as a typed feature (`showStepper={true}`).
279
+ MUI's NumberField is a plain `type="number"` input with no stepper UI.
280
+
281
+ ### Behavior ✓
282
+
283
+ **Value contract is byte-identical** between the two lines:
284
+
285
+ - Bridge storage type: `number \| null` (NEVER `NaN`)
286
+ - UI display: `number → String(number)`, `null/undefined → ""`
287
+ - Empty string → `setValue(name, null)`
288
+ - Parseable number → `setValue(name, parsed)`
289
+ - Non-numeric input → no bridge write (UI keeps string for UX,
290
+ bridge stays at last valid value)
291
+ - Parsing uses `Number(...)` + `Number.isFinite(...)`. No locale parsing.
292
+
293
+ This contract is explicitly documented in both lines' types files —
294
+ the audit confirms they match word-for-word.
295
+
296
+ ---
297
+
298
+ ## 8. OTPField
299
+
300
+ ### Signature
301
+
302
+ | Prop | MUI | TW |
303
+ |---|---|---|
304
+ | `length` | ✓ (default 6) | ✓ (default 6) |
305
+ | `mode` | `numeric` \| `alphanumeric` \| `alpha` | `numeric` \| `alphanumeric` |
306
+ | `onComplete` | ✓ | ✓ |
307
+ | `onChange` | `(value: string) => void` | `(value: string) => void` (identical) |
308
+ | `autoFocus` | ✓ (default `true`) | not in types (defaults via native `<input autoFocus>`) |
309
+
310
+ ### Variant axis ⚠
311
+
312
+ | Axis | MUI | TW |
313
+ |---|---|---|
314
+ | Slot rendering | custom React per-slot | custom React per-slot + Tailwind variant (`size`, `tone`) |
315
+
316
+ **Intentional delta.** TW drops MUI's `alpha`-only mode (no design
317
+ system demand; consumers needing pure-alpha can use `alphanumeric`
318
+ + client-side filter). TW also exposes per-slot styling via typed
319
+ `slotProps.slot` / `slotProps.slotChar`.
320
+
321
+ ### Behavior ✓
322
+
323
+ Both libs:
324
+ - Sanitize input per `mode` on every keystroke
325
+ - Sanitize paste content and fill slots sequentially
326
+ - Emit `onChange` with the joined sanitized value (≤ `length`)
327
+ - Fire `onComplete` when all slots filled
328
+
329
+ ---
330
+
331
+ ## 9. Autocomplete
332
+
333
+ ### Signature ⚠
334
+
335
+ | Prop | MUI | TW |
336
+ |---|---|---|
337
+ | `multiple` (multi-select) | ✗ (MUI v9 inherits but Dashforge wrapper doesn't expose) | ✓ |
338
+ | `freeSolo` | ✗ (MUI inherits but wrapper hides it) | ✓ |
339
+ | `loadOptions` (async loader) | ✗ | ✓ |
340
+ | `loadDebounceMs` | ✗ | ✓ (default 250ms) |
341
+ | `optionsFromFieldData` | ✓ (MUI-only feature for runtime options from form runtime) | ✗ (TW doesn't have a runtime-data integration yet — F6+ work) |
342
+ | `getOptionValue` / `getOptionLabel` / `getOptionDisabled` | ✓ | ✓ |
343
+ | `getOptionKey` | ✗ | ✓ |
344
+ | `onChange` | `(value: TValue \| null) => void` | `onValueChange: (AutocompleteValue) => void` (AutocompleteValue = `string \| string[] \| null`) |
345
+
346
+ ### Variant axis ⚠
347
+
348
+ | Axis | MUI | TW |
349
+ |---|---|---|
350
+ | Mode | single-select only | single + multi + free-solo |
351
+ | Async | not in wrapper (use MUI's directly if needed) | first-class `loadOptions` prop |
352
+
353
+ **Intentional delta — TW is feature-ahead.** TW's Autocomplete is
354
+ the most-extended bridge component:
355
+
356
+ - **Multi-select** (chips inside input, backspace to remove last)
357
+ - **Free-solo** (Enter commits typed text as value)
358
+ - **Async loadOptions** (debounced, replaces static `options` while
359
+ the user types)
360
+
361
+ MUI's Dashforge wrapper trails behind because: (a) MUI's underlying
362
+ Autocomplete already supports these via inherited props, so wrapping
363
+ them feels redundant; (b) the MUI consumer can drop down to
364
+ `MuiAutocomplete` directly when they need these features. TW had to
365
+ implement them from scratch (built on React Aria's `useComboBox`),
366
+ so it was natural to expose them as first-class props.
367
+
368
+ **Reverse delta — MUI is ahead on**: `optionsFromFieldData` (runtime
369
+ field data integration for dynamic option lists fed by the form
370
+ engine). This is a F6+ feature on the TW roadmap.
371
+
372
+ ### Behavior ⚠
373
+
374
+ The core bridge contract is parity:
375
+
376
+ - Storage policy: option pick → mapped `option.value`; clear → `null`
377
+ - Form Closure v1 error gating
378
+ - StrictMode-safe unregister on unmount
379
+
380
+ But the **storage type** differs:
381
+ - MUI: `TValue \| null` where TValue is `string | number`
382
+ - TW: `string \| string[] \| null` (`string[]` for multi-select)
383
+
384
+ This is a real call-site delta. A consumer migrating MUI → TW with
385
+ a NumberField-style numeric `TValue` would need to switch to string
386
+ storage + cast on submission.
387
+
388
+ ---
389
+
390
+ ## 10. DateTimePicker
391
+
392
+ ### Signature ✓
393
+
394
+ All props parity:
395
+
396
+ - `mode: 'date' \| 'time' \| 'datetime'` — identical
397
+ - `value: string \| null` — identical (ISO 8601-ish naive string)
398
+ - `min` / `max` / `step` — identical
399
+ - `name` / `rules` / `visibleWhen` / `access` — identical (bridge)
400
+ - `label` / `helperText` / `error` / `required` / `disabled` — identical
401
+
402
+ ### Variant axis ✓
403
+
404
+ Layout: both libs allow `stacked` / `inline` and downgrade `floating`
405
+ to `stacked` with a dev warning (rationale: native input always
406
+ shows a placeholder mask that overlaps the floating label).
407
+
408
+ ### Behavior ✓
409
+
410
+ Both libs:
411
+ - Use native `<input type="date|time|datetime-local">`
412
+ - Bridge contract identical
413
+ - ISO string contract identical (`datetime-local` is naive — no TZ;
414
+ if you need TZ-aware persistence, both libs say to convert
415
+ upstream)
416
+
417
+ **This is the cleanest parity case in the audit** — the only ✓✓✓
418
+ across all four columns. Likely because both lines deliberately
419
+ chose native HTML5 inputs over a custom calendar widget, leaving
420
+ very little surface to diverge on.
421
+
422
+ ### Notes
423
+
424
+ - MUI exposes `@deprecated inputProps` / `InputLabelProps` shims for
425
+ MUI v7 backward compatibility. TW never had these (no MUI lineage).
426
+
427
+ ---
428
+
429
+ ## Summary of intentional deltas
430
+
431
+ 1. **Callback signatures**: TW prefers Radix-style `onCheckedChange` /
432
+ `onValueChange` (single-arg). MUI inherits HTML `onChange(event, value)`.
433
+ → **Documented in `customization.mdx` decision tree.**
434
+ 2. **Variant taxonomy**: TW uses shadcn/Radix common-denominator
435
+ vocabulary (`solid`/`outline`/`soft`/`ghost`, `sm`/`md`/`lg`).
436
+ MUI uses Material Design vocabulary (`contained`/`outlined`/`text`,
437
+ `small`/`medium`/`large`). 1:1 mappable.
438
+ 3. **`sx` shape**: TW = utility string, MUI = sx-object. Same name,
439
+ different semantics — by design (see `customization.mdx` §1).
440
+ 4. **TW-only props**: `slotProps`, `asChild` (Button), `loading`
441
+ (Button), `slotProps.prefix/suffix` (TextField), `showStepper`
442
+ (NumberField), `multiple` / `freeSolo` / `loadOptions`
443
+ (Autocomplete), `resize` variant (Textarea).
444
+ 5. **MUI-only props**: `optionsFromFieldData` (Autocomplete runtime
445
+ data integration).
446
+
447
+ ## Components NOT covered by this audit
448
+
449
+ The MUI line has these additional components that have no TW
450
+ counterpart yet (NOT in scope for this audit — they're not part of
451
+ the bridge-integrated tier):
452
+
453
+ - `Select` (MUI-only) — TW uses Autocomplete for select-like UX
454
+ - `Animate` (MUI-only utility)
455
+
456
+ The TW line has these additional components with no MUI counterpart
457
+ (Sprint 1-2 Foundation work, not in scope here):
458
+
459
+ - `Typography`, `Box`, `Stack`, `Grid`, `Container`, `Divider`,
460
+ `AspectRatio`, `VisuallyHidden` (8 Foundation primitives)
461
+
462
+ Tier-4 components (Dialog, Tabs, Tooltip, Popover, Accordion) ship in
463
+ Sprint 3 P3 — they will be MUI-side counterparts of MUI's own
464
+ `Dialog`, `Tabs`, `Tooltip`, `Popover`, `Accordion`. Future PARITY.md
465
+ revisions will include them.
466
+
467
+ ## Follow-up items
468
+
469
+ No `✗` rows surfaced, so no critical follow-ups. The intentional
470
+ deltas are documented above. The following nice-to-haves were
471
+ identified but are explicitly deferred:
472
+
473
+ | Item | Notes | Sprint |
474
+ |---|---|---|
475
+ | Optional `onChange` (HTML-style) on Checkbox / Switch / RadioGroup | For MUI-migrating consumers who want byte-identical call-sites. Could be added as an additional optional callback alongside `onCheckedChange`. | Not planned. Intentional choice; documented in customization.mdx instead. |
476
+ | `optionsFromFieldData` on TW Autocomplete | Runtime field data integration. Requires F6 infrastructure (form runtime data hooks not yet ported to TW). | Sprint 6 (post-1.0 candidate). |
477
+ | MUI-side `loadOptions` / `freeSolo` / `multiple` exposure | MUI consumer can use `MuiAutocomplete` directly for these. Not adding to the wrapper unless concrete demand. | Not planned. |
478
+ | `variant="standard"` / `"filled"` on TW TextField | No design system demand. | Not planned. |
479
+
480
+ Maintenance pact: every release that touches a bridge-integrated
481
+ component on either line MUST re-run this audit. The cost is low
482
+ (~30 min for a single-component diff) and the consistency value is
483
+ high.
package/PERFORMANCE.md ADDED
@@ -0,0 +1,169 @@
1
+ # `@dashforge/tw` performance baseline
2
+
3
+ > Sprint 3 P4 deliverable (2026-05-19). Establishes the pre-`1.0.0`
4
+ > performance commitment for `@dashforge/tw@0.4.0-beta` (29
5
+ > components). Every subsequent release MUST hold this line or
6
+ > justify the regression in `CHANGELOG.md`.
7
+
8
+ ## Bundle size
9
+
10
+ ### Full library (`dist/index.esm.js`)
11
+
12
+ Measured immediately after `nx build @dashforge/tw` on the
13
+ `0.4.0-beta` source tree.
14
+
15
+ | Metric | Value |
16
+ |---|---|
17
+ | Raw size | **312 KB** (319,924 bytes) |
18
+ | Gzipped | **68.85 KB** (70,499 bytes) |
19
+ | Brotli (estimate) | ~58 KB |
20
+
21
+ ### Historical bundle trajectory
22
+
23
+ | Version | Raw | Gzipped | Components | Delta vs prev |
24
+ |---|---|---|---|---|
25
+ | `0.1.0-beta` | 255 KB | ~57 KB | 16 (tier-1/2/3) | baseline |
26
+ | `0.2.0-beta` | 272 KB | ~60 KB | 24 (+8 foundation) | +17 KB raw |
27
+ | `0.3.0-beta` | 272 KB | ~60 KB | 24 | 0 (no new components) |
28
+ | `0.4.0-beta` | **312 KB** | **68.85 KB** | 29 (+5 tier-4) | **+40 KB raw / +8.85 KB gz** |
29
+
30
+ The `0.4.0-beta` delta is the cost of 5 new Tier-4 components +
31
+ their 5 Radix UI primitive dependencies (`@radix-ui/react-dialog`,
32
+ `-tabs`, `-tooltip`, `-popover`, `-accordion`). The Radix runtime
33
+ sub-dependencies (`@radix-ui/react-portal`, `-presence`,
34
+ `-dismissable-layer`, etc.) are shared across primitives so the
35
+ marginal cost of each new primitive after the first is smaller.
36
+
37
+ ### Tree-shaken size — per-component source weight
38
+
39
+ Tree-shaken bundle size depends on the consumer's bundler (Vite,
40
+ webpack, esbuild, Rollup). The TS source size below is a **proxy**
41
+ for the contribution each component makes — not the precise
42
+ tree-shaken cost, which includes the Radix runtime and shared
43
+ helpers (`cn()`, `tailwind-variants`).
44
+
45
+ | Component | Source bytes | Tier |
46
+ |---|---:|---|
47
+ | Autocomplete | 49,020 | Tier-3 (heaviest — multi-select + free-solo + async) |
48
+ | LeftNav | 18,002 | F6 (navigation) |
49
+ | RadioGroup | 16,946 | Tier-2 |
50
+ | NumberField | 16,289 | Tier-2 |
51
+ | DateTimePicker | 16,075 | Tier-3 |
52
+ | Checkbox | 15,732 | Tier-1 |
53
+ | Grid | 15,086 | F10 (foundation) |
54
+ | TextField | 14,772 | Tier-1 |
55
+ | Snackbar | 14,770 | F7 |
56
+ | Box | 14,039 | F9 (foundation) |
57
+ | ConfirmDialog | 13,361 | F7 |
58
+ | AppShell | 12,734 | F6 |
59
+ | Button | 12,628 | Tier-1 |
60
+ | OTPField | 12,497 | Tier-2 |
61
+ | Breadcrumbs | 11,694 | F6 |
62
+ | Typography | 11,619 | F9 |
63
+ | Divider | 11,026 | F10 |
64
+ | Textarea | 10,310 | Tier-2 |
65
+ | Stack | 9,131 | F9 |
66
+ | Switch | 8,711 | Tier-1 |
67
+ | **Dialog** | **7,421** | **Tier-4 (new)** |
68
+ | Container | 6,754 | F10 |
69
+ | **Accordion** | **6,150** | **Tier-4 (new)** |
70
+ | **Tabs** | **5,303** | **Tier-4 (new)** |
71
+ | AspectRatio | 4,871 | F10 |
72
+ | TopBar | 4,723 | F6 |
73
+ | **Tooltip** | **4,389** | **Tier-4 (new)** |
74
+ | **Popover** | **3,747** | **Tier-4 (new)** |
75
+ | VisuallyHidden | 3,744 | F10 |
76
+
77
+ The 5 new Tier-4 components contribute **~27 KB of source** combined
78
+ — roughly aligned with the +40 KB raw bundle delta (the extra
79
+ ~13 KB is Radix runtime).
80
+
81
+ ### Representative bundle subsets
82
+
83
+ If a consumer imports only a subset, expected gzipped cost
84
+ (estimated, depends on bundler):
85
+
86
+ | Subset | Components | Estimated gzipped |
87
+ |---|---|---|
88
+ | Atomic (Button only) | 1 | ~6 KB |
89
+ | Form bundle | TextField + Checkbox + Switch + RadioGroup + NumberField + Textarea + OTPField | ~28 KB |
90
+ | Layout bundle | Box + Stack + Grid + Container + AppShell + LeftNav + TopBar + Breadcrumbs | ~24 KB |
91
+ | Foundation (F9 + F10) | Typography + Box + Stack + Grid + Container + Divider + AspectRatio + VisuallyHidden | ~18 KB |
92
+ | Tier-4 overlay primitives | Dialog + Tabs + Tooltip + Popover + Accordion | ~12 KB |
93
+ | Full library | 29 | **68.85 KB** |
94
+
95
+ Estimates are upper bounds — actual tree-shaking can do better if
96
+ the consumer doesn't trigger code paths (e.g. importing
97
+ `<Autocomplete>` without using `loadOptions` won't strip the async
98
+ loader code from the bundle, but other unused components are
99
+ fully eliminable).
100
+
101
+ ## Render performance
102
+
103
+ ### Sprint 2 baseline (in `dash` consumer, React Profiler)
104
+
105
+ Measured in
106
+ `~/projects/web/learn/dash/src/pages/TestFoundation.tsx` — page
107
+ mounts all 8 Foundation primitives × ~6 instances each (50+
108
+ primitives total) inside `<DashforgeTailwindProvider>` wrapped in
109
+ `<React.Profiler>`:
110
+
111
+ | Operation | Time | Notes |
112
+ |---|---|---|
113
+ | First mount | **12.1 ms** | Cold cache, no styles compiled yet |
114
+ | Re-render (no state change) | **7.0 ms** | After style cache warm |
115
+ | Re-render (state change) | **8.6 ms** | One field updated, sibling fields untouched |
116
+
117
+ Within the **60 fps budget** (16.67 ms per frame) for the full page
118
+ mount + every subsequent interaction. The primitives are pure (no
119
+ internal `useState`, no `useEffect`, only className resolution via
120
+ `tv()` + `cn()`), so React's reconciler trivially handles re-renders
121
+ without `React.memo`.
122
+
123
+ ### Tier-4 components — expected render cost
124
+
125
+ Tier-4 components (Dialog, Tabs, Tooltip, Popover, Accordion) carry
126
+ internal Radix state machines. Render cost is dominated by Radix's
127
+ own reconciliation, which is well-optimized. Expected ranges:
128
+
129
+ | Component | Expected mount | Notes |
130
+ |---|---|---|
131
+ | Tooltip | <1 ms | Renders nothing until hover; portal mount on open is ~2 ms |
132
+ | Popover | <1 ms | Same as Tooltip |
133
+ | Tabs | ~2 ms | Static — all triggers + active panel mount once |
134
+ | Accordion | ~3 ms | Per-item triggers + lazy panel mount |
135
+ | Dialog | ~3 ms | Portal + focus trap setup on open |
136
+
137
+ These are estimates based on Radix's published benchmarks. Real
138
+ consumer-app numbers will be captured in Sprint 3 follow-up
139
+ end-to-end validation pass (`/test-tier-4` page in `dash`).
140
+
141
+ ## Regression budget policy
142
+
143
+ **Every PR that touches a component or adds a new export MUST**:
144
+
145
+ 1. Re-run `nx build @dashforge/tw` and check the new
146
+ `dist/index.esm.js` size (raw + gzipped).
147
+ 2. If the gzipped delta is **> +5%** vs the previous release,
148
+ include in the PR description:
149
+ - The bytes added (raw + gzipped)
150
+ - The reason (new component / new feature / new dep / refactor)
151
+ - A justification for why the cost is worth it
152
+ 3. If the gzipped delta is **> +10%**, the PR also requires a
153
+ reviewer's sign-off on the bundle impact explicitly (not a
154
+ silent pass).
155
+
156
+ The 5% threshold at the current 68.85 KB baseline = 3.4 KB. That's
157
+ enough headroom for a typical component-level change (e.g. adding a
158
+ single Radix primitive's worth of code) without the policy
159
+ triggering, but small enough that a careless dep addition gets
160
+ flagged.
161
+
162
+ ## Reference
163
+
164
+ - **Build command**: `nx build @dashforge/tw`
165
+ - **Measurement script**: `gzip -c dist/index.esm.js | wc -c`
166
+ - **Render perf setup**: see Sprint 2 P1
167
+ (`/test-foundation` route in `dash` consumer)
168
+ - **CHANGELOG entries**: every release records the bundle delta
169
+ in the `Compatibility` section