@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/dist/index.esm.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { jsx, jsxs, Fragment as Fragment$1 } from 'react/jsx-runtime';
2
- import { useMemo, forwardRef, useContext, useId, useRef, useEffect, useCallback, useState, Fragment, createContext } from 'react';
2
+ import { useMemo, forwardRef, useContext, useId, useRef, useEffect, useCallback, useState, Fragment, createContext, Children, isValidElement, cloneElement } from 'react';
3
3
  import { Slot } from '@radix-ui/react-slot';
4
4
  import clsx from 'clsx';
5
5
  import { twMerge } from 'tailwind-merge';
@@ -403,6 +403,13 @@ function _object_without_properties_loose(source, excluded) {
403
403
  fullWidth,
404
404
  loading
405
405
  }), sx);
406
+ /*
407
+ * `aria-busy` announces the loading state to assistive tech.
408
+ * `disabled` alone hides the reason (perm-denied vs loading vs
409
+ * intrinsic), so SR users only hear "dimmed/inactive" without
410
+ * knowing why. Adding `aria-busy={true}` while loading distinguishes
411
+ * "wait for the action to finish" from "you can't do this".
412
+ */ const ariaBusy = loading ? true : undefined;
406
413
  // `asChild` renders through Radix Slot: the immediate child element
407
414
  // gets the resolved className, no extra Button DOM is emitted.
408
415
  if (asChild) {
@@ -411,6 +418,7 @@ function _object_without_properties_loose(source, excluded) {
411
418
  className: classes,
412
419
  "data-disabled": effectiveDisabled || undefined,
413
420
  "aria-disabled": effectiveDisabled || undefined,
421
+ "aria-busy": ariaBusy,
414
422
  children: children
415
423
  });
416
424
  }
@@ -418,6 +426,7 @@ function _object_without_properties_loose(source, excluded) {
418
426
  ref: ref,
419
427
  type: (_rest_type = rest.type) != null ? _rest_type : 'button',
420
428
  disabled: effectiveDisabled,
429
+ "aria-busy": ariaBusy,
421
430
  className: classes
422
431
  }, rest, {
423
432
  children: [
@@ -968,10 +977,9 @@ function _object_without_properties_loose(source, excluded) {
968
977
  className: cn(v.control(), slotProps == null ? void 0 : (_slotProps_control = slotProps.control) == null ? void 0 : _slotProps_control.className),
969
978
  children: jsx(RadixCheckbox.Indicator, {
970
979
  className: cn(v.indicator(), slotProps == null ? void 0 : (_slotProps_indicator = slotProps.indicator) == null ? void 0 : _slotProps_indicator.className),
971
- forceMount: true,
972
- children: resolvedChecked === true ? jsx(CheckIcon, {
980
+ children: jsx(CheckIcon, {
973
981
  className: "h-full w-full"
974
- }) : null
982
+ })
975
983
  })
976
984
  })),
977
985
  jsxs("div", {
@@ -1430,9 +1438,13 @@ function _object_without_properties_loose(source, excluded) {
1430
1438
  })
1431
1439
  ]
1432
1440
  }),
1433
- jsx(RadixRadioGroup.Root, {
1434
- name: name,
1435
- value: resolvedValue,
1441
+ jsx(RadixRadioGroup.Root, _extends({
1442
+ name: name
1443
+ }, isFormMode || explicitValue !== undefined ? {
1444
+ value: resolvedValue
1445
+ } : {
1446
+ defaultValue: defaultValue != null ? defaultValue : undefined
1447
+ }, {
1436
1448
  onValueChange: handleValueChange,
1437
1449
  onBlur: handleBlur,
1438
1450
  disabled: groupEffectiveDisabled,
@@ -1463,7 +1475,7 @@ function _object_without_properties_loose(source, excluded) {
1463
1475
  ]
1464
1476
  }, option.value);
1465
1477
  })
1466
- }),
1478
+ })),
1467
1479
  resolvedHelperText && jsx("p", {
1468
1480
  id: helperId,
1469
1481
  className: cn(resolvedError ? v.errorText() : v.helperText(), resolvedError ? slotProps == null ? void 0 : (_slotProps_errorText = slotProps.errorText) == null ? void 0 : _slotProps_errorText.className : slotProps == null ? void 0 : (_slotProps_helperText = slotProps.helperText) == null ? void 0 : _slotProps_helperText.className),
@@ -1905,6 +1917,18 @@ function _object_without_properties_loose(source, excluded) {
1905
1917
  useDashFieldMeta(name);
1906
1918
  const accessState = useAccessState(access);
1907
1919
  const inputId = useId();
1920
+ /*
1921
+ * Local state for the STANDALONE UNCONTROLLED case (no bridge, no
1922
+ * `value` prop, only `defaultValue`). Mirrors OTPField. Without this,
1923
+ * `resolvedDisplayValue` would be a snapshot computed ONCE from
1924
+ * `defaultValue` and the controlled `<input value={...}>` would
1925
+ * snap user input back on every keystroke / stepper click — same
1926
+ * trap that hit Checkbox + RadioGroup in this package.
1927
+ *
1928
+ * In form mode the bridge owns state. In standalone CONTROLLED mode
1929
+ * (consumer passes `value`) the consumer owns state. Only this
1930
+ * branch needs the local hook.
1931
+ */ const [uncontrolledValue, setUncontrolledValue] = useState(()=>formatForDisplay(defaultValue));
1908
1932
  const helperId = `${inputId}-help`;
1909
1933
  // StrictMode-safe unregister-on-unmount
1910
1934
  const unregisterRef = useRef({
@@ -1942,7 +1966,7 @@ function _object_without_properties_loose(source, excluded) {
1942
1966
  resolvedHelperText = validation.helperText;
1943
1967
  resolvedDisplayValue = userValue !== undefined ? formatForDisplay(userValue) : formatForDisplay(bridge.getValue(name));
1944
1968
  } else {
1945
- resolvedDisplayValue = userValue !== undefined ? formatForDisplay(userValue) : formatForDisplay(defaultValue);
1969
+ resolvedDisplayValue = userValue !== undefined ? formatForDisplay(userValue) : uncontrolledValue;
1946
1970
  }
1947
1971
  const writeToBridge = (parsed)=>{
1948
1972
  if (!isFormMode || !bridge) return;
@@ -1957,6 +1981,13 @@ function _object_without_properties_loose(source, excluded) {
1957
1981
  // internal logic still sees the raw string change.
1958
1982
  void registration.onChange(e);
1959
1983
  }
1984
+ // Standalone uncontrolled mode: mirror the raw input string so the
1985
+ // controlled `<input value={...}>` reflects what the user typed.
1986
+ // Partial states (e.g. "-", "1.") are kept verbatim — `parseFromInput`
1987
+ // returns `undefined` for them so `writeToBridge` is a no-op above.
1988
+ if (!isFormMode && userValue === undefined) {
1989
+ setUncontrolledValue(e.target.value);
1990
+ }
1960
1991
  userOnChange == null ? void 0 : userOnChange(e);
1961
1992
  };
1962
1993
  const handleBlur = (e)=>{
@@ -1972,6 +2003,11 @@ function _object_without_properties_loose(source, excluded) {
1972
2003
  if (typeof min === 'number') next = Math.max(min, next);
1973
2004
  if (typeof max === 'number') next = Math.min(max, next);
1974
2005
  writeToBridge(next);
2006
+ // Standalone uncontrolled: also persist the new value to local
2007
+ // state so the visible display tracks the stepper click.
2008
+ if (!isFormMode && userValue === undefined) {
2009
+ setUncontrolledValue(formatForDisplay(next));
2010
+ }
1975
2011
  };
1976
2012
  const canIncrement = !effectiveDisabled && (typeof max !== 'number' || ((_parseFromInput = parseFromInput(resolvedDisplayValue)) != null ? _parseFromInput : -Infinity) < max);
1977
2013
  const canDecrement = !effectiveDisabled && (typeof min !== 'number' || ((_parseFromInput1 = parseFromInput(resolvedDisplayValue)) != null ? _parseFromInput1 : Infinity) > min);
@@ -4926,6 +4962,1567 @@ const SnackbarContext = /*#__PURE__*/ createContext(null);
4926
4962
  });
4927
4963
  }
4928
4964
 
4965
+ /**
4966
+ * `typographyVariants` — the full type scale for the @dashforge/tw library.
4967
+ *
4968
+ * Mirrors the MUI Typography variant set (h1–h6 · subtitle1/2 · body1/2 ·
4969
+ * caption · overline) so the mental model carries over for developers
4970
+ * moving between the two ecosystems. Each variant baseline maps to a
4971
+ * Tailwind utility chain that resolves through the @dashforge/tw-tokens
4972
+ * scale (so the visual stays in sync with the rest of the system when the
4973
+ * token theme is patched).
4974
+ *
4975
+ * Variant axes are intentionally ORTHOGONAL — `variant` chooses the type
4976
+ * scale, `weight` overrides the variant's default weight (useful for "h2
4977
+ * but lighter"), `color` picks the intent, `align` picks the axis. They
4978
+ * never collide, so consumers can mix them freely.
4979
+ *
4980
+ * Two boolean flags (`truncate`, `noWrap`) encode the most common one-line
4981
+ * patterns; `gutterBottom` adds the conventional bottom margin used when a
4982
+ * heading precedes a paragraph block (mirror of MUI's same flag).
4983
+ */ const typographyVariants = tv({
4984
+ base: 'text-inherit',
4985
+ variants: {
4986
+ /*
4987
+ * `variant` is the type-scale axis.
4988
+ *
4989
+ * The default font-weight is baked into each variant (headings come
4990
+ * with semibold/bold by default). Consumers override per-instance via
4991
+ * the `weight` axis below — when set, `weight` wins because it's
4992
+ * declared later in the cn() chain and tailwind-merge resolves the
4993
+ * last `font-*` to win.
4994
+ */ variant: {
4995
+ h1: 'text-5xl font-bold leading-[1.05] tracking-[-0.025em]',
4996
+ h2: 'text-4xl font-bold leading-[1.1] tracking-[-0.022em]',
4997
+ h3: 'text-3xl font-semibold leading-[1.15] tracking-[-0.02em]',
4998
+ h4: 'text-2xl font-semibold leading-snug tracking-[-0.015em]',
4999
+ h5: 'text-xl font-semibold leading-snug',
5000
+ h6: 'text-lg font-semibold leading-normal',
5001
+ subtitle1: 'text-base font-medium leading-relaxed',
5002
+ subtitle2: 'text-sm font-medium leading-relaxed',
5003
+ body1: 'text-base font-normal leading-relaxed',
5004
+ body2: 'text-sm font-normal leading-relaxed',
5005
+ caption: 'text-xs font-normal leading-normal',
5006
+ overline: 'text-xs font-semibold uppercase tracking-[0.12em] leading-normal'
5007
+ },
5008
+ /*
5009
+ * `color` is the intent axis. Pairs with the @dashforge/tw-theme
5010
+ * reactive colour vars so the choice survives theme patches and dark
5011
+ * mode flips. `inherit` is the escape hatch — used inside a Box that
5012
+ * has set its own color (e.g. `<Box variant="solid" color="primary">`
5013
+ * paints white text).
5014
+ */ color: {
5015
+ inherit: 'text-inherit',
5016
+ primary: 'text-primary-700 dark:text-primary-400',
5017
+ secondary: 'text-secondary-700 dark:text-secondary-400',
5018
+ success: 'text-success-700 dark:text-success-400',
5019
+ warning: 'text-warning-700 dark:text-warning-400',
5020
+ danger: 'text-danger-700 dark:text-danger-400',
5021
+ info: 'text-info-700 dark:text-info-400',
5022
+ neutral: 'text-neutral-900 dark:text-neutral-100',
5023
+ muted: 'text-neutral-600 dark:text-neutral-400'
5024
+ },
5025
+ /*
5026
+ * `weight` overrides the variant's default weight. When unset, the
5027
+ * variant's own weight wins. When set, this axis appears LATER in the
5028
+ * cn() chain so tailwind-merge resolves to this value.
5029
+ */ weight: {
5030
+ normal: 'font-normal',
5031
+ medium: 'font-medium',
5032
+ semibold: 'font-semibold',
5033
+ bold: 'font-bold',
5034
+ extrabold: 'font-extrabold'
5035
+ },
5036
+ align: {
5037
+ left: 'text-left',
5038
+ center: 'text-center',
5039
+ right: 'text-right',
5040
+ justify: 'text-justify'
5041
+ },
5042
+ /*
5043
+ * `truncate` collapses to a one-line ellipsis. `noWrap` is the looser
5044
+ * sibling — keeps the text on one line but lets it overflow without
5045
+ * the `…`. Mutually-exclusive intent-wise; if both are passed,
5046
+ * `truncate` wins (later in the cn() chain).
5047
+ */ truncate: {
5048
+ true: 'truncate'
5049
+ },
5050
+ noWrap: {
5051
+ true: 'whitespace-nowrap'
5052
+ },
5053
+ /*
5054
+ * `gutterBottom` adds the conventional bottom margin used when a
5055
+ * heading precedes a paragraph block. Mirror of MUI's same prop —
5056
+ * familiar to developers crossing from the MUI side.
5057
+ */ gutterBottom: {
5058
+ true: 'mb-3'
5059
+ }
5060
+ },
5061
+ defaultVariants: {
5062
+ variant: 'body1',
5063
+ color: 'inherit',
5064
+ align: 'left'
5065
+ }
5066
+ });
5067
+
5068
+ /**
5069
+ * Default HTML tag per variant. Headings get their semantic level (h1→h1
5070
+ * etc.); subtitle/body get `<p>` (block, paragraph semantics);
5071
+ * caption/overline get `<span>` (inline, no implicit block break).
5072
+ *
5073
+ * Override per-instance via the `as` prop — useful when the semantic
5074
+ * heading level should differ from the visual scale (e.g. a hero
5075
+ * rendered as `<h2>` but visually styled `h1`).
5076
+ */ const VARIANT_TO_TAG = {
5077
+ h1: 'h1',
5078
+ h2: 'h2',
5079
+ h3: 'h3',
5080
+ h4: 'h4',
5081
+ h5: 'h5',
5082
+ h6: 'h6',
5083
+ subtitle1: 'p',
5084
+ subtitle2: 'p',
5085
+ body1: 'p',
5086
+ body2: 'p',
5087
+ caption: 'span',
5088
+ overline: 'span'
5089
+ };
5090
+ /**
5091
+ * `<Typography>` — semantic typed text, the foundation of every readable
5092
+ * surface in @dashforge/tw.
5093
+ *
5094
+ * Why this exists:
5095
+ * Tailwind ships a typographic scale (`text-xl`, `font-bold`,
5096
+ * `leading-relaxed`) but leaves the SEMANTIC HTML tag and the
5097
+ * intent-coloured palette to the consumer. That's fine for one-off
5098
+ * marketing surfaces, but at app scale it means every `<h2>` and every
5099
+ * body paragraph re-derives its own utility chain — and they drift.
5100
+ *
5101
+ * Typography moves that decision into a typed prop set: the visual
5102
+ * scale, the intent colour, the alignment, the truncation all live in
5103
+ * `tailwind-variants` and resolve to the same utility chain everywhere
5104
+ * in the app. The default HTML tag is inferred from `variant` so the
5105
+ * semantic layer follows the visual layer; `as` and `asChild` are the
5106
+ * two escape hatches when you need something else.
5107
+ *
5108
+ * Layering:
5109
+ * • Sits BENEATH every component that renders text — `<Button>`'s
5110
+ * label, `<TextField>`'s helper text, MDX prose in our own docs.
5111
+ * • Composes ON TOP of `@dashforge/tw-tokens` colour scales (so
5112
+ * `color="primary"` paints `text-primary-700` in light, `-400` in dark,
5113
+ * reactive to `setMode()`).
5114
+ * • Polymorphic via Radix Slot — pairs cleanly with router `<Link>`,
5115
+ * `<button>`, or `<a>` without injecting an extra wrapper element.
5116
+ *
5117
+ * Polymorphism rules — `as` vs `asChild`:
5118
+ * • `as` swaps the rendered tag (we still render the element ourselves).
5119
+ * • `asChild` removes our element entirely — the single React child
5120
+ * becomes the rendered tag, with our className/ref merged onto it.
5121
+ * This is the Radix Slot pattern; use when you need a router Link
5122
+ * to style as a heading.
5123
+ * When BOTH are passed, `asChild` wins. We considered making this a
5124
+ * compile-time error via discriminated unions, but the API surface
5125
+ * already has 8 axes and adding one more dimension to the props type
5126
+ * would inflate IntelliSense suggestions for marginal benefit. The
5127
+ * runtime preference is documented; the test asserts it.
5128
+ */ const Typography = /*#__PURE__*/ forwardRef(function Typography(props, ref) {
5129
+ var _ref;
5130
+ const { variant = 'body1', color, weight, align, truncate, noWrap, gutterBottom, as, asChild = false, sx, children } = props, rest = _object_without_properties_loose(props, [
5131
+ "variant",
5132
+ "color",
5133
+ "weight",
5134
+ "align",
5135
+ "truncate",
5136
+ "noWrap",
5137
+ "gutterBottom",
5138
+ "as",
5139
+ "asChild",
5140
+ "sx",
5141
+ "children"
5142
+ ]);
5143
+ const classes = cn(typographyVariants({
5144
+ variant,
5145
+ color,
5146
+ weight,
5147
+ align,
5148
+ truncate,
5149
+ noWrap,
5150
+ gutterBottom
5151
+ }), sx);
5152
+ // asChild wins over `as` when both are passed (see component header).
5153
+ if (asChild) {
5154
+ return jsx(Slot, _extends({
5155
+ ref: ref,
5156
+ className: classes
5157
+ }, rest, {
5158
+ children: children
5159
+ }));
5160
+ }
5161
+ // Resolve the rendered tag: explicit `as` > variant default > fallback.
5162
+ const Tag = (_ref = as != null ? as : VARIANT_TO_TAG[variant]) != null ? _ref : 'span';
5163
+ return jsx(Tag, _extends({
5164
+ ref: ref,
5165
+ className: classes
5166
+ }, rest, {
5167
+ children: children
5168
+ }));
5169
+ });
5170
+ Typography.displayName = 'Typography';
5171
+
5172
+ /**
5173
+ * `boxVariants` — the surface primitive for @dashforge/tw.
5174
+ *
5175
+ * Architectural choice (planned with the user — F9 deep dive):
5176
+ *
5177
+ * Box replaces FOUR overlapping concepts from MUI in one component:
5178
+ * • Box (typed div)
5179
+ * • Paper (surface with elevation)
5180
+ * • Card (Paper specialisation)
5181
+ * • Surface (Joy UI's outlined / soft / solid / plain variants)
5182
+ *
5183
+ * The reason for the consolidation: in MUI you have to compose two or
5184
+ * three of these to express even basic intent ("an outlined card with
5185
+ * warning tone"). Here, one `<Box variant="outlined" color="warning">`
5186
+ * says exactly that.
5187
+ *
5188
+ * Variant taxonomy (5 axes, intentionally non-overlapping with Stack/Grid):
5189
+ *
5190
+ * • plain — bare div + padding + radius. The escape hatch.
5191
+ * • outlined — 1px border + subtle bg tint. The "card lite".
5192
+ * • elevated — bg surface + shadow scale (0-5). The "floating panel".
5193
+ * • soft — semi-transparent intent bg + intent text. The "callout".
5194
+ * • solid — solid intent bg + contrasting text. The "CTA banner".
5195
+ *
5196
+ * `color` applies to outlined/soft/solid (each gets the 7 intent
5197
+ * variants). `elevated` is color-agnostic (always neutral surface +
5198
+ * shadow scale). `plain` is everything-agnostic.
5199
+ *
5200
+ * Spacing axes (p/px/py/m/mx/my): mapped explicitly to the 11 token
5201
+ * steps from @dashforge/tw-tokens (0, 0.5, 1, 2, 3, 4, 6, 8, 12, 16, 24).
5202
+ * Tailwind JIT requires literal class strings — building them dynamically
5203
+ * with template literals would purge them. The verbosity below is the
5204
+ * cost of keeping the bundle CSS-pure and rebuild-free.
5205
+ *
5206
+ * What this does NOT do (deliberate, see component docs):
5207
+ * • No display / flex / grid props → use Stack or Grid
5208
+ * • No position / overflow / z-index → use `sx`
5209
+ * • No animation / transition → use `sx`
5210
+ *
5211
+ * The "Box is not flex" rule is the spine of the layout layer. Without
5212
+ * it, every `<div>` in an app gravitates back to Box and the surface
5213
+ * vs layout distinction collapses — exactly the failure mode this
5214
+ * primitive exists to prevent.
5215
+ */ const boxVariants = tv({
5216
+ base: 'block',
5217
+ variants: {
5218
+ /*
5219
+ * Surface variant. Compound with `color` for outlined / soft / solid;
5220
+ * standalone for plain / elevated.
5221
+ */ variant: {
5222
+ plain: '',
5223
+ outlined: 'border',
5224
+ elevated: 'bg-white dark:bg-neutral-900',
5225
+ soft: '',
5226
+ solid: ''
5227
+ },
5228
+ /*
5229
+ * Intent color. Only meaningful when variant is outlined / soft /
5230
+ * solid (resolved via compoundVariants below). For plain / elevated
5231
+ * this axis is ignored at the visual level — but kept in the type
5232
+ * so the prop is always available without conditional typing.
5233
+ */ color: {
5234
+ primary: '',
5235
+ secondary: '',
5236
+ success: '',
5237
+ warning: '',
5238
+ danger: '',
5239
+ info: '',
5240
+ neutral: ''
5241
+ },
5242
+ /*
5243
+ * Shadow scale — relevant for `variant='elevated'`. We keep elevation
5244
+ * as a separate axis (not folded into `variant`) so consumers can
5245
+ * dial it up/down without changing the variant. Default `0` = no
5246
+ * shadow (consistent with MUI's elevation=0).
5247
+ */ elevation: {
5248
+ 0: 'shadow-none',
5249
+ 1: 'shadow-sm',
5250
+ 2: 'shadow',
5251
+ 3: 'shadow-md',
5252
+ 4: 'shadow-lg',
5253
+ 5: 'shadow-xl'
5254
+ },
5255
+ rounded: {
5256
+ none: 'rounded-none',
5257
+ sm: 'rounded-sm',
5258
+ md: 'rounded-md',
5259
+ lg: 'rounded-lg',
5260
+ xl: 'rounded-xl',
5261
+ '2xl': 'rounded-2xl',
5262
+ full: 'rounded-full'
5263
+ },
5264
+ /*
5265
+ * Spacing — six axes (p/px/py/m/mx/my), 11 token steps each.
5266
+ * Literals enumerated explicitly so Tailwind's JIT scanner finds
5267
+ * every class. Token steps mirror @dashforge/tw-tokens spacing scale.
5268
+ */ p: {
5269
+ 0: 'p-0',
5270
+ '0.5': 'p-0.5',
5271
+ 1: 'p-1',
5272
+ 2: 'p-2',
5273
+ 3: 'p-3',
5274
+ 4: 'p-4',
5275
+ 6: 'p-6',
5276
+ 8: 'p-8',
5277
+ 12: 'p-12',
5278
+ 16: 'p-16',
5279
+ 24: 'p-24'
5280
+ },
5281
+ px: {
5282
+ 0: 'px-0',
5283
+ '0.5': 'px-0.5',
5284
+ 1: 'px-1',
5285
+ 2: 'px-2',
5286
+ 3: 'px-3',
5287
+ 4: 'px-4',
5288
+ 6: 'px-6',
5289
+ 8: 'px-8',
5290
+ 12: 'px-12',
5291
+ 16: 'px-16',
5292
+ 24: 'px-24'
5293
+ },
5294
+ py: {
5295
+ 0: 'py-0',
5296
+ '0.5': 'py-0.5',
5297
+ 1: 'py-1',
5298
+ 2: 'py-2',
5299
+ 3: 'py-3',
5300
+ 4: 'py-4',
5301
+ 6: 'py-6',
5302
+ 8: 'py-8',
5303
+ 12: 'py-12',
5304
+ 16: 'py-16',
5305
+ 24: 'py-24'
5306
+ },
5307
+ m: {
5308
+ 0: 'm-0',
5309
+ '0.5': 'm-0.5',
5310
+ 1: 'm-1',
5311
+ 2: 'm-2',
5312
+ 3: 'm-3',
5313
+ 4: 'm-4',
5314
+ 6: 'm-6',
5315
+ 8: 'm-8',
5316
+ 12: 'm-12',
5317
+ 16: 'm-16',
5318
+ 24: 'm-24'
5319
+ },
5320
+ mx: {
5321
+ 0: 'mx-0',
5322
+ '0.5': 'mx-0.5',
5323
+ 1: 'mx-1',
5324
+ 2: 'mx-2',
5325
+ 3: 'mx-3',
5326
+ 4: 'mx-4',
5327
+ 6: 'mx-6',
5328
+ 8: 'mx-8',
5329
+ 12: 'mx-12',
5330
+ 16: 'mx-16',
5331
+ 24: 'mx-24'
5332
+ },
5333
+ my: {
5334
+ 0: 'my-0',
5335
+ '0.5': 'my-0.5',
5336
+ 1: 'my-1',
5337
+ 2: 'my-2',
5338
+ 3: 'my-3',
5339
+ 4: 'my-4',
5340
+ 6: 'my-6',
5341
+ 8: 'my-8',
5342
+ 12: 'my-12',
5343
+ 16: 'my-16',
5344
+ 24: 'my-24'
5345
+ },
5346
+ fullWidth: {
5347
+ true: 'w-full'
5348
+ },
5349
+ fullHeight: {
5350
+ true: 'h-full'
5351
+ }
5352
+ },
5353
+ /*
5354
+ * Compound variants — where surface × color get their actual visual.
5355
+ * Twenty-one entries: 7 (outlined) + 7 (soft) + 7 (solid).
5356
+ * `plain` and `elevated` don't appear here (no color contribution).
5357
+ *
5358
+ * Dark-mode pair is baked in: light-mode picks the 50-300 steps,
5359
+ * dark-mode picks the 800-950 steps — both reactive to setMode()
5360
+ * via the @dashforge/tw-theme CSS variables.
5361
+ */ compoundVariants: [
5362
+ // ─── outlined × color ─────────────────────────────────────────────
5363
+ {
5364
+ variant: 'outlined',
5365
+ color: 'primary',
5366
+ class: 'border-primary-300 bg-primary-50/40 dark:border-primary-800 dark:bg-primary-950/30'
5367
+ },
5368
+ {
5369
+ variant: 'outlined',
5370
+ color: 'secondary',
5371
+ class: 'border-secondary-300 bg-secondary-50/40 dark:border-secondary-800 dark:bg-secondary-950/30'
5372
+ },
5373
+ {
5374
+ variant: 'outlined',
5375
+ color: 'success',
5376
+ class: 'border-success-300 bg-success-50/40 dark:border-success-800 dark:bg-success-950/30'
5377
+ },
5378
+ {
5379
+ variant: 'outlined',
5380
+ color: 'warning',
5381
+ class: 'border-warning-300 bg-warning-50/40 dark:border-warning-800 dark:bg-warning-950/30'
5382
+ },
5383
+ {
5384
+ variant: 'outlined',
5385
+ color: 'danger',
5386
+ class: 'border-danger-300 bg-danger-50/40 dark:border-danger-800 dark:bg-danger-950/30'
5387
+ },
5388
+ {
5389
+ variant: 'outlined',
5390
+ color: 'info',
5391
+ class: 'border-info-300 bg-info-50/40 dark:border-info-800 dark:bg-info-950/30'
5392
+ },
5393
+ {
5394
+ variant: 'outlined',
5395
+ color: 'neutral',
5396
+ class: 'border-neutral-200 bg-white dark:border-neutral-700 dark:bg-neutral-900'
5397
+ },
5398
+ // ─── soft × color ─────────────────────────────────────────────────
5399
+ {
5400
+ variant: 'soft',
5401
+ color: 'primary',
5402
+ class: 'bg-primary-100 text-primary-900 dark:bg-primary-950/50 dark:text-primary-100'
5403
+ },
5404
+ {
5405
+ variant: 'soft',
5406
+ color: 'secondary',
5407
+ class: 'bg-secondary-100 text-secondary-900 dark:bg-secondary-950/50 dark:text-secondary-100'
5408
+ },
5409
+ {
5410
+ variant: 'soft',
5411
+ color: 'success',
5412
+ class: 'bg-success-100 text-success-900 dark:bg-success-950/50 dark:text-success-100'
5413
+ },
5414
+ {
5415
+ variant: 'soft',
5416
+ color: 'warning',
5417
+ class: 'bg-warning-100 text-warning-900 dark:bg-warning-950/50 dark:text-warning-100'
5418
+ },
5419
+ {
5420
+ variant: 'soft',
5421
+ color: 'danger',
5422
+ class: 'bg-danger-100 text-danger-900 dark:bg-danger-950/50 dark:text-danger-100'
5423
+ },
5424
+ {
5425
+ variant: 'soft',
5426
+ color: 'info',
5427
+ class: 'bg-info-100 text-info-900 dark:bg-info-950/50 dark:text-info-100'
5428
+ },
5429
+ {
5430
+ variant: 'soft',
5431
+ color: 'neutral',
5432
+ class: 'bg-neutral-100 text-neutral-900 dark:bg-neutral-800 dark:text-neutral-100'
5433
+ },
5434
+ // ─── solid × color ────────────────────────────────────────────────
5435
+ {
5436
+ variant: 'solid',
5437
+ color: 'primary',
5438
+ class: 'bg-primary-600 text-white dark:bg-primary-500'
5439
+ },
5440
+ {
5441
+ variant: 'solid',
5442
+ color: 'secondary',
5443
+ class: 'bg-secondary-600 text-white dark:bg-secondary-500'
5444
+ },
5445
+ {
5446
+ variant: 'solid',
5447
+ color: 'success',
5448
+ class: 'bg-success-600 text-white dark:bg-success-500'
5449
+ },
5450
+ {
5451
+ variant: 'solid',
5452
+ color: 'warning',
5453
+ class: 'bg-warning-500 text-white dark:bg-warning-600'
5454
+ },
5455
+ {
5456
+ variant: 'solid',
5457
+ color: 'danger',
5458
+ class: 'bg-danger-600 text-white dark:bg-danger-500'
5459
+ },
5460
+ {
5461
+ variant: 'solid',
5462
+ color: 'info',
5463
+ class: 'bg-info-600 text-white dark:bg-info-500'
5464
+ },
5465
+ {
5466
+ variant: 'solid',
5467
+ color: 'neutral',
5468
+ class: 'bg-neutral-900 text-white dark:bg-neutral-100 dark:text-neutral-900'
5469
+ }
5470
+ ],
5471
+ defaultVariants: {
5472
+ variant: 'plain',
5473
+ color: 'neutral',
5474
+ elevation: 0,
5475
+ rounded: 'none'
5476
+ }
5477
+ });
5478
+
5479
+ /**
5480
+ * `<Box>` — the surface primitive of @dashforge/tw.
5481
+ *
5482
+ * What it IS:
5483
+ * A polymorphic container with typed surface variants (plain /
5484
+ * outlined / elevated / soft / solid) × 7 intent colours, plus
5485
+ * spacing / sizing / rounded / elevation as enumerated token-scale
5486
+ * props. Replaces MUI's Box + Paper + Card + Surface in one component.
5487
+ *
5488
+ * What it is NOT (deliberate, enforced by the prop set):
5489
+ * • Not a flex container → use <Stack>
5490
+ * • Not a grid container → use <Grid>
5491
+ * • Not a paragraph → use <Typography>
5492
+ * • Not a button → use <Button>
5493
+ *
5494
+ * The "Box is not flex" rule is the spine of the layout layer. Without
5495
+ * it, every `<div>` in an app gravitates back to Box and the surface
5496
+ * vs layout distinction collapses. Box's job is the SURFACE around
5497
+ * content; Stack/Grid's job is the ARRANGEMENT of content. Two
5498
+ * primitives, two responsibilities.
5499
+ *
5500
+ * When BOTH `as` and `asChild` are passed, `asChild` wins. Same rule
5501
+ * as Typography — documented to avoid the dimension-bloat of a
5502
+ * discriminated union over an 11-axis props type.
5503
+ *
5504
+ * Layering:
5505
+ * • Sits BENEATH every visual chrome — every card, every panel,
5506
+ * every section background.
5507
+ * • Composes ON TOP of @dashforge/tw-tokens (color + spacing + radius
5508
+ * + shadow scales) via the dashforgePreset CSS variables.
5509
+ * • Pairs with Typography for text content, Stack/Grid for layout
5510
+ * children inside it.
5511
+ */ const Box = /*#__PURE__*/ forwardRef(function Box(props, ref) {
5512
+ const { variant, color, elevation, rounded, p, px, py, m, mx, my, fullWidth, fullHeight, as, asChild = false, sx, children } = props, rest = _object_without_properties_loose(props, [
5513
+ "variant",
5514
+ "color",
5515
+ "elevation",
5516
+ "rounded",
5517
+ "p",
5518
+ "px",
5519
+ "py",
5520
+ "m",
5521
+ "mx",
5522
+ "my",
5523
+ "fullWidth",
5524
+ "fullHeight",
5525
+ "as",
5526
+ "asChild",
5527
+ "sx",
5528
+ "children"
5529
+ ]);
5530
+ const classes = cn(boxVariants({
5531
+ variant,
5532
+ color,
5533
+ elevation,
5534
+ rounded,
5535
+ p,
5536
+ px,
5537
+ py,
5538
+ m,
5539
+ mx,
5540
+ my,
5541
+ fullWidth,
5542
+ fullHeight
5543
+ }), sx);
5544
+ if (asChild) {
5545
+ return jsx(Slot, _extends({
5546
+ ref: ref,
5547
+ className: classes
5548
+ }, rest, {
5549
+ children: children
5550
+ }));
5551
+ }
5552
+ const Tag = as != null ? as : 'div';
5553
+ return jsx(Tag, _extends({
5554
+ ref: ref,
5555
+ className: classes
5556
+ }, rest, {
5557
+ children: children
5558
+ }));
5559
+ });
5560
+ Box.displayName = 'Box';
5561
+
5562
+ /**
5563
+ * `stackVariants` — flex container 1D, the layout primitive of @dashforge/tw.
5564
+ *
5565
+ * Architectural role (planned with the user — F9 deep dive):
5566
+ *
5567
+ * Stack is the ONLY way to do flex in this library. Box is the
5568
+ * surface (border / bg / shadow); Stack is the arrangement (direction
5569
+ * / align / justify / gap). Two primitives, two responsibilities,
5570
+ * zero overlap.
5571
+ *
5572
+ * This rules out the MUI failure mode where every `<Box display="flex"
5573
+ * gap={2}>` quietly becomes the de-facto flex container, drowning
5574
+ * the surface vs layout distinction. Here, if you see `<Stack>` in
5575
+ * the JSX, you KNOW it's flex; if you see `<Box>`, you KNOW it's
5576
+ * not. The component name carries the intent.
5577
+ *
5578
+ * Axes:
5579
+ * • direction — row / col (+ reverse variants)
5580
+ * • align / justify — cross-axis / main-axis alignment
5581
+ * • gap — token-scale step (mirror Box spacing scale)
5582
+ * • wrap — flex-wrap
5583
+ * • divider — runtime-only (handled in Stack.tsx, not here)
5584
+ *
5585
+ * Sizing (`fullWidth`, `fullHeight`) is duplicated from Box because
5586
+ * Stack often plays the role of a full-width strip / full-height
5587
+ * column — re-typing `sx="w-full"` every time would be friction.
5588
+ */ const stackVariants = tv({
5589
+ base: 'flex',
5590
+ variants: {
5591
+ direction: {
5592
+ row: 'flex-row',
5593
+ col: 'flex-col',
5594
+ 'row-reverse': 'flex-row-reverse',
5595
+ 'col-reverse': 'flex-col-reverse'
5596
+ },
5597
+ align: {
5598
+ start: 'items-start',
5599
+ center: 'items-center',
5600
+ end: 'items-end',
5601
+ stretch: 'items-stretch',
5602
+ baseline: 'items-baseline'
5603
+ },
5604
+ justify: {
5605
+ start: 'justify-start',
5606
+ center: 'justify-center',
5607
+ end: 'justify-end',
5608
+ between: 'justify-between',
5609
+ around: 'justify-around',
5610
+ evenly: 'justify-evenly'
5611
+ },
5612
+ /*
5613
+ * `gap` — explicit literal mapping for the 11 token-scale steps.
5614
+ * Same set as Box's spacing axes (p, m, etc.) so muscle memory
5615
+ * carries over: `<Stack gap={4}>` aligns visually with `<Box p={4}>`.
5616
+ */ gap: {
5617
+ 0: 'gap-0',
5618
+ '0.5': 'gap-0.5',
5619
+ 1: 'gap-1',
5620
+ 2: 'gap-2',
5621
+ 3: 'gap-3',
5622
+ 4: 'gap-4',
5623
+ 6: 'gap-6',
5624
+ 8: 'gap-8',
5625
+ 12: 'gap-12',
5626
+ 16: 'gap-16',
5627
+ 24: 'gap-24'
5628
+ },
5629
+ wrap: {
5630
+ true: 'flex-wrap'
5631
+ },
5632
+ fullWidth: {
5633
+ true: 'w-full'
5634
+ },
5635
+ fullHeight: {
5636
+ true: 'h-full'
5637
+ }
5638
+ },
5639
+ defaultVariants: {
5640
+ direction: 'col'
5641
+ }
5642
+ });
5643
+
5644
+ /**
5645
+ * Walk the children, inserting `divider` BETWEEN every consecutive pair.
5646
+ *
5647
+ * Implementation notes:
5648
+ * • `React.Children.toArray` assigns auto-keys but does NOT recursively
5649
+ * flatten Fragments — it treats them as opaque single children. So
5650
+ * `<Stack><><a/><b/></><c/></Stack>` is 2 boundaries (fragment + c),
5651
+ * yielding ONE divider. Hoist items out of the fragment when you
5652
+ * need a divider between them. Documented + asserted in the tests.
5653
+ * • Dividers are wrapped in `<Fragment>` with a deterministic key
5654
+ * derived from the boundary index — stable across re-renders so
5655
+ * React reconciles correctly when children re-order.
5656
+ * • If `divider` is a valid element, we `cloneElement` once per
5657
+ * boundary (cheaper than re-rendering the JSX expression N-1 times).
5658
+ * For string/number dividers we wrap in a span automatically.
5659
+ */ function interleaveDividers(children, divider) {
5660
+ const items = Children.toArray(children);
5661
+ if (items.length <= 1) return items;
5662
+ const result = [];
5663
+ items.forEach((child, i)=>{
5664
+ result.push(child);
5665
+ if (i < items.length - 1) {
5666
+ const key = `df-stack-divider-${i}`;
5667
+ if (/*#__PURE__*/ isValidElement(divider)) {
5668
+ result.push(/*#__PURE__*/ cloneElement(divider, {
5669
+ key
5670
+ }));
5671
+ } else {
5672
+ result.push(jsx(Fragment, {
5673
+ children: divider
5674
+ }, key));
5675
+ }
5676
+ }
5677
+ });
5678
+ return result;
5679
+ }
5680
+ /**
5681
+ * `<Stack>` — flex container 1D, the layout primitive.
5682
+ *
5683
+ * This is the ONLY component in @dashforge/tw that does flex. Box
5684
+ * doesn't, Grid does CSS Grid (not flex). The strict naming → engine
5685
+ * mapping is the whole point: when you read `<Stack>` in a JSX tree,
5686
+ * you instantly know it's flex. No `<Box display="flex" ...>` traps.
5687
+ *
5688
+ * Direction defaults to `'col'` (vertical stack) — the most common
5689
+ * case for forms, sidebars, settings panels. Pass `direction="row"`
5690
+ * for horizontal layouts (toolbars, button rows, breadcrumbs).
5691
+ *
5692
+ * The `divider` prop is the runtime-only piece: TV can't encode the
5693
+ * "render this between each child" logic as a class, so we walk the
5694
+ * children at render time. The walk is O(n); for n ≤ ~10 (typical
5695
+ * Stack content) the cost is negligible. For very long Stacks (1000+
5696
+ * items), prefer to render dividers as part of each child instead.
5697
+ *
5698
+ * When `asChild` is true, the divider prop is silently ignored — Slot
5699
+ * requires a single child, and the N-1 insertion has nowhere to act.
5700
+ */ const Stack = /*#__PURE__*/ forwardRef(function Stack(props, ref) {
5701
+ const { direction, align, justify, gap, wrap, fullWidth, fullHeight, divider, as, asChild = false, sx, children } = props, rest = _object_without_properties_loose(props, [
5702
+ "direction",
5703
+ "align",
5704
+ "justify",
5705
+ "gap",
5706
+ "wrap",
5707
+ "fullWidth",
5708
+ "fullHeight",
5709
+ "divider",
5710
+ "as",
5711
+ "asChild",
5712
+ "sx",
5713
+ "children"
5714
+ ]);
5715
+ const classes = cn(stackVariants({
5716
+ direction,
5717
+ align,
5718
+ justify,
5719
+ gap,
5720
+ wrap,
5721
+ fullWidth,
5722
+ fullHeight
5723
+ }), sx);
5724
+ if (asChild) {
5725
+ // Slot expects a single child; divider has no place here.
5726
+ return jsx(Slot, _extends({
5727
+ ref: ref,
5728
+ className: classes
5729
+ }, rest, {
5730
+ children: children
5731
+ }));
5732
+ }
5733
+ const Tag = as != null ? as : 'div';
5734
+ const content = divider != null ? interleaveDividers(children, divider) : children;
5735
+ return jsx(Tag, _extends({
5736
+ ref: ref,
5737
+ className: classes
5738
+ }, rest, {
5739
+ children: content
5740
+ }));
5741
+ });
5742
+ Stack.displayName = 'Stack';
5743
+
5744
+ /**
5745
+ * `gridVariants` — CSS Grid container + item, polymorphic in role.
5746
+ *
5747
+ * Architectural choice (planned with the user — F9 deep dive):
5748
+ *
5749
+ * Pattern is MUI Grid v2 from the API side (`<Grid container>` +
5750
+ * `<Grid xs={6}>`), but engine is REAL CSS Grid under the hood
5751
+ * (`display: grid` + `grid-template-columns: repeat(N, 1fr)` +
5752
+ * `col-span-*`). MUI v2 uses flexbox + basis percentages — historical
5753
+ * IE11 reasons. In 2026, CSS Grid is universally supported AND
5754
+ * Tailwind ships `grid-cols-*` / `col-span-*` natively, so flexbox
5755
+ * would be a downgrade.
5756
+ *
5757
+ * Two distinct shapes coexist in the same TV recipe:
5758
+ *
5759
+ * • container=true — display:grid + cols + gap + autoFlow
5760
+ * • container=false — col-span at each breakpoint (xs/sm/md/lg/xl)
5761
+ *
5762
+ * The discriminated union lives in grid.types.ts (TypeScript-level);
5763
+ * here the TV catalogue exposes every axis so the consumer's choice
5764
+ * triggers the right classes regardless of which role the component
5765
+ * is playing. Unused axes produce no classes (TV silently drops
5766
+ * undefined variants), so a container Grid never accidentally emits
5767
+ * `col-span-6` and an item Grid never accidentally emits `grid`.
5768
+ *
5769
+ * Mapping table size:
5770
+ * • cols : 6 entries (12, 6, 4, 3, 2, 1)
5771
+ * • autoFlow : 5 entries
5772
+ * • span axes (5×14) : 70 entries (xs/sm/md/lg/xl × {1..12, auto, full})
5773
+ * • gap / gapX / gapY : 33 entries (11 steps × 3 axes)
5774
+ *
5775
+ * Total: ~115 literal class names. All explicit, all Tailwind-scannable.
5776
+ * Bundle CSS impact when fully exercised: ~3 KB gz (mostly the
5777
+ * responsive col-span set).
5778
+ */ const gridVariants = tv({
5779
+ base: '',
5780
+ variants: {
5781
+ // ─── CONTAINER role ────────────────────────────────────────────────
5782
+ container: {
5783
+ true: 'grid'
5784
+ },
5785
+ cols: {
5786
+ 1: 'grid-cols-1',
5787
+ 2: 'grid-cols-2',
5788
+ 3: 'grid-cols-3',
5789
+ 4: 'grid-cols-4',
5790
+ 6: 'grid-cols-6',
5791
+ 12: 'grid-cols-12'
5792
+ },
5793
+ autoFlow: {
5794
+ row: 'grid-flow-row',
5795
+ col: 'grid-flow-col',
5796
+ dense: 'grid-flow-dense',
5797
+ 'row-dense': 'grid-flow-row-dense',
5798
+ 'col-dense': 'grid-flow-col-dense'
5799
+ },
5800
+ /*
5801
+ * Container gap — token-scale step. Same set as Box/Stack so muscle
5802
+ * memory carries across primitives.
5803
+ */ spacing: {
5804
+ 0: 'gap-0',
5805
+ '0.5': 'gap-0.5',
5806
+ 1: 'gap-1',
5807
+ 2: 'gap-2',
5808
+ 3: 'gap-3',
5809
+ 4: 'gap-4',
5810
+ 6: 'gap-6',
5811
+ 8: 'gap-8',
5812
+ 12: 'gap-12',
5813
+ 16: 'gap-16',
5814
+ 24: 'gap-24'
5815
+ },
5816
+ spacingX: {
5817
+ 0: 'gap-x-0',
5818
+ '0.5': 'gap-x-0.5',
5819
+ 1: 'gap-x-1',
5820
+ 2: 'gap-x-2',
5821
+ 3: 'gap-x-3',
5822
+ 4: 'gap-x-4',
5823
+ 6: 'gap-x-6',
5824
+ 8: 'gap-x-8',
5825
+ 12: 'gap-x-12',
5826
+ 16: 'gap-x-16',
5827
+ 24: 'gap-x-24'
5828
+ },
5829
+ spacingY: {
5830
+ 0: 'gap-y-0',
5831
+ '0.5': 'gap-y-0.5',
5832
+ 1: 'gap-y-1',
5833
+ 2: 'gap-y-2',
5834
+ 3: 'gap-y-3',
5835
+ 4: 'gap-y-4',
5836
+ 6: 'gap-y-6',
5837
+ 8: 'gap-y-8',
5838
+ 12: 'gap-y-12',
5839
+ 16: 'gap-y-16',
5840
+ 24: 'gap-y-24'
5841
+ },
5842
+ // ─── ITEM role — col-span at each breakpoint ──────────────────────
5843
+ /*
5844
+ * `xs` is the base breakpoint (no prefix) — Tailwind's mobile-first
5845
+ * convention. `sm/md/lg/xl` cascade up from there.
5846
+ *
5847
+ * Special values:
5848
+ * • 'auto' → col-auto (content-sized)
5849
+ * • 'full' → col-span-full (span all columns regardless of count)
5850
+ */ xs: {
5851
+ 1: 'col-span-1',
5852
+ 2: 'col-span-2',
5853
+ 3: 'col-span-3',
5854
+ 4: 'col-span-4',
5855
+ 5: 'col-span-5',
5856
+ 6: 'col-span-6',
5857
+ 7: 'col-span-7',
5858
+ 8: 'col-span-8',
5859
+ 9: 'col-span-9',
5860
+ 10: 'col-span-10',
5861
+ 11: 'col-span-11',
5862
+ 12: 'col-span-12',
5863
+ auto: 'col-auto',
5864
+ full: 'col-span-full'
5865
+ },
5866
+ sm: {
5867
+ 1: 'sm:col-span-1',
5868
+ 2: 'sm:col-span-2',
5869
+ 3: 'sm:col-span-3',
5870
+ 4: 'sm:col-span-4',
5871
+ 5: 'sm:col-span-5',
5872
+ 6: 'sm:col-span-6',
5873
+ 7: 'sm:col-span-7',
5874
+ 8: 'sm:col-span-8',
5875
+ 9: 'sm:col-span-9',
5876
+ 10: 'sm:col-span-10',
5877
+ 11: 'sm:col-span-11',
5878
+ 12: 'sm:col-span-12',
5879
+ auto: 'sm:col-auto',
5880
+ full: 'sm:col-span-full'
5881
+ },
5882
+ md: {
5883
+ 1: 'md:col-span-1',
5884
+ 2: 'md:col-span-2',
5885
+ 3: 'md:col-span-3',
5886
+ 4: 'md:col-span-4',
5887
+ 5: 'md:col-span-5',
5888
+ 6: 'md:col-span-6',
5889
+ 7: 'md:col-span-7',
5890
+ 8: 'md:col-span-8',
5891
+ 9: 'md:col-span-9',
5892
+ 10: 'md:col-span-10',
5893
+ 11: 'md:col-span-11',
5894
+ 12: 'md:col-span-12',
5895
+ auto: 'md:col-auto',
5896
+ full: 'md:col-span-full'
5897
+ },
5898
+ lg: {
5899
+ 1: 'lg:col-span-1',
5900
+ 2: 'lg:col-span-2',
5901
+ 3: 'lg:col-span-3',
5902
+ 4: 'lg:col-span-4',
5903
+ 5: 'lg:col-span-5',
5904
+ 6: 'lg:col-span-6',
5905
+ 7: 'lg:col-span-7',
5906
+ 8: 'lg:col-span-8',
5907
+ 9: 'lg:col-span-9',
5908
+ 10: 'lg:col-span-10',
5909
+ 11: 'lg:col-span-11',
5910
+ 12: 'lg:col-span-12',
5911
+ auto: 'lg:col-auto',
5912
+ full: 'lg:col-span-full'
5913
+ },
5914
+ xl: {
5915
+ 1: 'xl:col-span-1',
5916
+ 2: 'xl:col-span-2',
5917
+ 3: 'xl:col-span-3',
5918
+ 4: 'xl:col-span-4',
5919
+ 5: 'xl:col-span-5',
5920
+ 6: 'xl:col-span-6',
5921
+ 7: 'xl:col-span-7',
5922
+ 8: 'xl:col-span-8',
5923
+ 9: 'xl:col-span-9',
5924
+ 10: 'xl:col-span-10',
5925
+ 11: 'xl:col-span-11',
5926
+ 12: 'xl:col-span-12',
5927
+ auto: 'xl:col-auto',
5928
+ full: 'xl:col-span-full'
5929
+ }
5930
+ }
5931
+ });
5932
+
5933
+ /**
5934
+ * `<Grid>` — CSS Grid container + item, polymorphic in role.
5935
+ *
5936
+ * API surface mirrors MUI Grid v2:
5937
+ * • <Grid container spacing={4} cols={12}> ← container
5938
+ * • <Grid xs={12} md={6}> ← item
5939
+ *
5940
+ * Engine is REAL CSS Grid (not flexbox, unlike MUI v2 internals).
5941
+ * Benefits over flexbox-based implementation:
5942
+ * • Native `gap` (no negative-margin tricks)
5943
+ * • `col-span-N` maps directly to `grid-column: span N`
5944
+ * • Vertical alignment without manual `align-items` per item
5945
+ * • Auto-flow for dense packing without DOM gymnastics
5946
+ *
5947
+ * TypeScript discriminated union (see grid.types.ts) makes mixing
5948
+ * impossible: `<Grid container xs={6}>` won't compile. Same for the
5949
+ * inverse. IntelliSense filters by role.
5950
+ *
5951
+ * Defaults at the component level (not at the TV level — see the
5952
+ * variants file for the rationale):
5953
+ * • container: cols defaults to 12 (MUI convention)
5954
+ * • item: xs defaults to 'full' so a forgotten prop doesn't
5955
+ * collapse the cell to 1/12th of a row
5956
+ *
5957
+ * `asChild` on a CONTAINER is technically allowed but rarely useful:
5958
+ * `display:grid` only applies to direct children, so the slotted
5959
+ * element has to already be a viable container parent. Use sparingly;
5960
+ * test asserts the wrapping still emits the right class chain.
5961
+ */ const Grid = /*#__PURE__*/ forwardRef(function Grid(props, ref) {
5962
+ const { as, asChild = false, sx, children } = props, rest = _object_without_properties_loose(props, [
5963
+ "as",
5964
+ "asChild",
5965
+ "sx",
5966
+ "children"
5967
+ ]);
5968
+ /*
5969
+ * Discriminated union runtime branch.
5970
+ *
5971
+ * TS narrows GridProps to ONE of the two shapes (container or item)
5972
+ * at the call site, but inside this function body we receive the
5973
+ * union — so we manually narrow via a single cast per branch.
5974
+ * `as GridContainerProps` / `as GridItemProps` is sound because
5975
+ * `container === true` is the literal discriminator declared in
5976
+ * the types file; the cast just makes TS read the right branch.
5977
+ */ const isContainer = props.container === true;
5978
+ let classes;
5979
+ let containerPayload = null;
5980
+ let itemPayload = null;
5981
+ if (isContainer) {
5982
+ var _containerPayload_cols;
5983
+ containerPayload = props;
5984
+ /*
5985
+ * Container branch — pass ONLY container axes to TV. Default cols
5986
+ * to 12 (MUI v2 convention) so a forgotten prop doesn't collapse
5987
+ * to grid-cols-1. The spacing axes are passed straight through;
5988
+ * TV silently drops undefined values.
5989
+ */ classes = cn(gridVariants({
5990
+ container: true,
5991
+ cols: (_containerPayload_cols = containerPayload.cols) != null ? _containerPayload_cols : 12,
5992
+ spacing: containerPayload.spacing,
5993
+ spacingX: containerPayload.spacingX,
5994
+ spacingY: containerPayload.spacingY,
5995
+ autoFlow: containerPayload.autoFlow
5996
+ }), sx);
5997
+ } else {
5998
+ var _itemPayload_xs;
5999
+ itemPayload = props;
6000
+ /*
6001
+ * Item branch — default `xs` to 'full' so a forgotten breakpoint
6002
+ * prop doesn't collapse the cell to grid-auto's minimum width.
6003
+ */ classes = cn(gridVariants({
6004
+ xs: (_itemPayload_xs = itemPayload.xs) != null ? _itemPayload_xs : 'full',
6005
+ sm: itemPayload.sm,
6006
+ md: itemPayload.md,
6007
+ lg: itemPayload.lg,
6008
+ xl: itemPayload.xl
6009
+ }), sx);
6010
+ }
6011
+ /*
6012
+ * Strip role-specific props from `rest` before spreading on the DOM
6013
+ * element. Without this, React warns:
6014
+ * "React does not recognize the `cols` prop on a DOM element"
6015
+ */ const domRest = _extends({}, rest);
6016
+ if (containerPayload) {
6017
+ delete domRest.cols;
6018
+ delete domRest.spacing;
6019
+ delete domRest.spacingX;
6020
+ delete domRest.spacingY;
6021
+ delete domRest.autoFlow;
6022
+ }
6023
+ if (itemPayload) {
6024
+ delete domRest.xs;
6025
+ delete domRest.sm;
6026
+ delete domRest.md;
6027
+ delete domRest.lg;
6028
+ delete domRest.xl;
6029
+ }
6030
+ // `container` discriminator itself must never reach the DOM.
6031
+ delete domRest.container;
6032
+ if (asChild) {
6033
+ return jsx(Slot, _extends({
6034
+ ref: ref,
6035
+ className: classes
6036
+ }, domRest, {
6037
+ children: children
6038
+ }));
6039
+ }
6040
+ const Tag = as != null ? as : 'div';
6041
+ return jsx(Tag, _extends({
6042
+ ref: ref,
6043
+ className: classes
6044
+ }, domRest, {
6045
+ children: children
6046
+ }));
6047
+ });
6048
+ Grid.displayName = 'Grid';
6049
+
6050
+ /**
6051
+ * `containerVariants` — centered max-width wrapper for page layouts.
6052
+ *
6053
+ * Architectural role:
6054
+ *
6055
+ * Every web app has the same pattern at the page-root level: a div
6056
+ * that's `mx-auto`, capped at some `max-w-*`, with responsive
6057
+ * horizontal padding. Without Container, every page rewrites:
6058
+ *
6059
+ * <div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
6060
+ *
6061
+ * Container collapses that into one typed prop set, with size names
6062
+ * that mirror Tailwind's breakpoint vocabulary so the muscle memory
6063
+ * carries over (`size="lg"` ↔ `max-w-screen-lg`).
6064
+ *
6065
+ * Size axis — maps to Tailwind's `max-w-screen-*` aliases:
6066
+ * • sm → max-w-screen-sm (640px)
6067
+ * • md → max-w-screen-md (768px)
6068
+ * • lg → max-w-screen-lg (1024px) — most common doc/content cap
6069
+ * • xl → max-w-screen-xl (1280px) — default; comfortable for full apps
6070
+ * • 2xl → max-w-screen-2xl (1536px) — wide dashboards
6071
+ * • fluid → no max-width at all (full bleed, padding still applies)
6072
+ *
6073
+ * Padding axis (`px`):
6074
+ * • true (default) — responsive horizontal padding (px-4 sm:px-6 lg:px-8)
6075
+ * The canonical Tailwind responsive padding ramp. Designed to keep
6076
+ * edges from kissing the viewport on mobile and breathing more on
6077
+ * larger screens.
6078
+ * • false — no padding. Use when the consumer wants full bleed AND
6079
+ * handles edge padding inside (e.g. a hero section with its own
6080
+ * internal spacing scale).
6081
+ *
6082
+ * Center content axis (`centerContent`):
6083
+ * • true — turns the Container into a flex column with items-center
6084
+ * so the page content stacks centered horizontally. Common for
6085
+ * marketing pages, sign-in flows, "single artifact" layouts.
6086
+ * • false (default) — children flow normally (block stacking).
6087
+ */ const containerVariants = tv({
6088
+ base: 'mx-auto w-full',
6089
+ variants: {
6090
+ size: {
6091
+ sm: 'max-w-screen-sm',
6092
+ md: 'max-w-screen-md',
6093
+ lg: 'max-w-screen-lg',
6094
+ xl: 'max-w-screen-xl',
6095
+ '2xl': 'max-w-screen-2xl',
6096
+ fluid: ''
6097
+ },
6098
+ /*
6099
+ * Responsive padding ramp. `false` skips it entirely so the
6100
+ * consumer can supply custom padding via `sx` when needed.
6101
+ */ px: {
6102
+ true: 'px-4 sm:px-6 lg:px-8',
6103
+ false: ''
6104
+ },
6105
+ /*
6106
+ * Stacks children centered. Mutually compatible with all sizes —
6107
+ * a fluid container with centerContent is the canonical "marketing
6108
+ * hero" layout.
6109
+ */ centerContent: {
6110
+ true: 'flex flex-col items-center',
6111
+ false: ''
6112
+ }
6113
+ },
6114
+ defaultVariants: {
6115
+ size: 'xl',
6116
+ px: true,
6117
+ centerContent: false
6118
+ }
6119
+ });
6120
+
6121
+ /**
6122
+ * `<Container>` — centered max-width wrapper for page-level layouts.
6123
+ *
6124
+ * The pattern is universal: every page-root `<div>` in a non-trivial
6125
+ * web app boils down to "mx-auto + max-w-X + responsive horizontal
6126
+ * padding". Container collapses that into a typed prop set so the
6127
+ * decision lives in ONE place per app section (the Container size),
6128
+ * not scattered as utility chains across every page file.
6129
+ *
6130
+ * Default size is `'xl'` (1280px) — comfortable for full app shells
6131
+ * with a left nav + main + optional right rail. Drop to `'lg'` (1024px)
6132
+ * for content-heavy docs/marketing, jump to `'2xl'` (1536px) for wide
6133
+ * dashboards.
6134
+ *
6135
+ * Composition pattern — Container at the page root, Stack/Grid inside:
6136
+ *
6137
+ * <Container size="lg" as="main">
6138
+ * <Stack gap={8}>
6139
+ * <Typography variant="h1">Page title</Typography>
6140
+ * <Grid container spacing={6}>
6141
+ * <Grid xs={12} md={6}>...</Grid>
6142
+ * </Grid>
6143
+ * </Stack>
6144
+ * </Container>
6145
+ *
6146
+ * Container handles the page chrome (centered, padded, capped width);
6147
+ * Stack/Grid handle the actual layout of children. Two concerns, two
6148
+ * primitives.
6149
+ *
6150
+ * Polymorphism rule (same as Typography/Box/Stack/Grid): when both
6151
+ * `as` and `asChild` are passed, `asChild` wins.
6152
+ */ const Container = /*#__PURE__*/ forwardRef(function Container(props, ref) {
6153
+ const { size, px, centerContent, as, asChild = false, sx, children } = props, rest = _object_without_properties_loose(props, [
6154
+ "size",
6155
+ "px",
6156
+ "centerContent",
6157
+ "as",
6158
+ "asChild",
6159
+ "sx",
6160
+ "children"
6161
+ ]);
6162
+ const classes = cn(containerVariants({
6163
+ size,
6164
+ px,
6165
+ centerContent
6166
+ }), sx);
6167
+ if (asChild) {
6168
+ return jsx(Slot, _extends({
6169
+ ref: ref,
6170
+ className: classes
6171
+ }, rest, {
6172
+ children: children
6173
+ }));
6174
+ }
6175
+ const Tag = as != null ? as : 'div';
6176
+ return jsx(Tag, _extends({
6177
+ ref: ref,
6178
+ className: classes
6179
+ }, rest, {
6180
+ children: children
6181
+ }));
6182
+ });
6183
+ Container.displayName = 'Container';
6184
+
6185
+ /**
6186
+ * `dividerVariants` — visual separator with two rendering modes (line-only
6187
+ * vs labeled). Two interconnected TV recipes:
6188
+ *
6189
+ * • `dividerVariants` — root container (block layout, alignment)
6190
+ * • `dividerLineVariants` — the actual line segments (border style,
6191
+ * color, orientation)
6192
+ *
6193
+ * Two recipes (not one with slots) because the labeled mode renders
6194
+ * THREE elements (left line · label · right line) while the line-only
6195
+ * mode is ONE element. Splitting keeps each TV catalogue small and
6196
+ * the type unions narrow.
6197
+ *
6198
+ * Mental model:
6199
+ * • Without `children` → renders an `<hr>` (or a div if vertical)
6200
+ * with the line styles applied directly.
6201
+ * • With `children` → renders a flex row with two `<span>` line
6202
+ * segments either side of the label. The label's flex-shrink keeps
6203
+ * it from being squashed; the line segments share the remaining
6204
+ * space according to `align`.
6205
+ *
6206
+ * a11y: the root always carries `role="separator"` + `aria-orientation`
6207
+ * — handled in Divider.tsx, not in TV (it's a prop, not a class).
6208
+ */ /*
6209
+ * Root container — only relevant when label is present (flex layout).
6210
+ * For line-only, the root IS the line.
6211
+ */ const dividerVariants = tv({
6212
+ base: 'flex items-center',
6213
+ variants: {
6214
+ orientation: {
6215
+ horizontal: 'w-full',
6216
+ vertical: 'h-full flex-col'
6217
+ },
6218
+ /*
6219
+ * Label alignment along the divider's main axis. Implemented by
6220
+ * setting the flex-basis of the two line segments asymmetrically
6221
+ * — handled in the variants for `dividerLineVariants` below via
6222
+ * compound logic. The root just needs the flex layout.
6223
+ */ align: {
6224
+ start: '',
6225
+ center: '',
6226
+ end: ''
6227
+ }
6228
+ },
6229
+ defaultVariants: {
6230
+ orientation: 'horizontal',
6231
+ align: 'center'
6232
+ }
6233
+ });
6234
+ /*
6235
+ * Line segment(s).
6236
+ *
6237
+ * Border styles (solid/dashed/dotted) are applied as `border-t-{style}`
6238
+ * for horizontal, `border-l-{style}` for vertical. Color drives the
6239
+ * border-* color token to the intent.
6240
+ *
6241
+ * The `segment` axis distinguishes whether this is a line-only render
6242
+ * (full width) vs a labeled-mode segment (flex-1 grows to share space).
6243
+ */ const dividerLineVariants = tv({
6244
+ base: '',
6245
+ variants: {
6246
+ orientation: {
6247
+ horizontal: 'h-0 border-t',
6248
+ vertical: 'w-0 border-l self-stretch'
6249
+ },
6250
+ variant: {
6251
+ solid: 'border-solid',
6252
+ dashed: 'border-dashed',
6253
+ dotted: 'border-dotted'
6254
+ },
6255
+ color: {
6256
+ neutral: 'border-neutral-200 dark:border-neutral-800',
6257
+ primary: 'border-primary-300 dark:border-primary-700',
6258
+ secondary: 'border-secondary-300 dark:border-secondary-700',
6259
+ success: 'border-success-300 dark:border-success-700',
6260
+ warning: 'border-warning-300 dark:border-warning-700',
6261
+ danger: 'border-danger-300 dark:border-danger-700',
6262
+ info: 'border-info-300 dark:border-info-700'
6263
+ },
6264
+ /*
6265
+ * `segment` controls whether the line spans full width (line-only
6266
+ * mode) or grows to fill space (labeled-mode flex segment).
6267
+ */ segment: {
6268
+ full: 'w-full',
6269
+ grow: 'flex-1'
6270
+ }
6271
+ },
6272
+ defaultVariants: {
6273
+ orientation: 'horizontal',
6274
+ variant: 'solid',
6275
+ color: 'neutral',
6276
+ segment: 'full'
6277
+ }
6278
+ });
6279
+
6280
+ /**
6281
+ * `<Divider>` — visual separator. Two modes:
6282
+ *
6283
+ * 1. Line-only (no children):
6284
+ *
6285
+ * <Divider />
6286
+ * → <hr role="separator" aria-orientation="horizontal" />
6287
+ *
6288
+ * Renders a single horizontal line via `border-t-*` on an `<hr>`
6289
+ * (which by default has no `display`, no margin in our reset, just
6290
+ * acts as the border carrier).
6291
+ *
6292
+ * 2. Labeled (with children):
6293
+ *
6294
+ * <Divider><Typography variant="overline">OR</Typography></Divider>
6295
+ * → <div role="separator" aria-orientation="horizontal">
6296
+ * <span aria-hidden className="flex-1 border-t" />
6297
+ * <span className="px-3">OR</span>
6298
+ * <span aria-hidden className="flex-1 border-t" />
6299
+ * </div>
6300
+ *
6301
+ * Two line segments share the available space; the label sits
6302
+ * between them with horizontal padding for breathing room. Each
6303
+ * line segment is `aria-hidden` (the role="separator" on the root
6304
+ * conveys the separator semantics — duplicating it would confuse
6305
+ * screen readers).
6306
+ *
6307
+ * Alignment:
6308
+ * • align="start" → label hugs the left, right segment grows
6309
+ * • align="center" (default) → both segments equal
6310
+ * • align="end" → label hugs the right, left segment grows
6311
+ *
6312
+ * Implemented by setting `flex-basis: 0` to either segment to keep
6313
+ * it minimal. We use `basis-[2rem]` (32px short stub) rather than
6314
+ * `basis-0` so a tiny line still hints at the divider on the squashed
6315
+ * side — pure flex-1 vs basis-0 would collapse it invisibly.
6316
+ *
6317
+ * Vertical orientation:
6318
+ * The line orientation flips (border-l instead of border-t). Vertical
6319
+ * labeled dividers are RARE in practice — supported for parity but
6320
+ * the typical use is line-only between flex row items.
6321
+ *
6322
+ * a11y:
6323
+ * • Root element gets `role="separator"` (or implicit via `<hr>`)
6324
+ * • `aria-orientation` is set explicitly so AT knows the axis
6325
+ * • Labeled mode: label has no extra role; the visual layout speaks
6326
+ */ const Divider = /*#__PURE__*/ forwardRef(function Divider(props, ref) {
6327
+ const { orientation = 'horizontal', align = 'center', variant = 'solid', color = 'neutral', children, flexItem, sx } = props, rest = _object_without_properties_loose(props, [
6328
+ "orientation",
6329
+ "align",
6330
+ "variant",
6331
+ "color",
6332
+ "children",
6333
+ "flexItem",
6334
+ "sx"
6335
+ ]);
6336
+ // ─── Mode 1: line-only (no children) ─────────────────────────────
6337
+ if (children == null) {
6338
+ const lineClasses = cn(dividerLineVariants({
6339
+ orientation,
6340
+ variant,
6341
+ color,
6342
+ segment: 'full'
6343
+ }), sx);
6344
+ // Horizontal → <hr>: zero default styling once we strip the UA
6345
+ // default `<hr>` border via `border-0` (in `dividerLineVariants`
6346
+ // base) and apply our own `border-t-*`. Vertical → <div> because
6347
+ // <hr> can't be vertical cross-browser-consistently.
6348
+ if (orientation === 'horizontal') {
6349
+ return jsx("hr", _extends({
6350
+ ref: ref,
6351
+ role: "separator",
6352
+ "aria-orientation": "horizontal",
6353
+ className: cn('border-0', lineClasses)
6354
+ }, rest));
6355
+ }
6356
+ return jsx("div", _extends({
6357
+ ref: ref,
6358
+ role: "separator",
6359
+ "aria-orientation": "vertical",
6360
+ className: lineClasses
6361
+ }, rest));
6362
+ }
6363
+ // ─── Mode 2: labeled (with children) ─────────────────────────────
6364
+ const rootClasses = cn(dividerVariants({
6365
+ orientation,
6366
+ align
6367
+ }), sx);
6368
+ /*
6369
+ * Segment widths per `align`:
6370
+ * • center → both flex-1 (equal share)
6371
+ * • start → left short stub, right grows
6372
+ * • end → left grows, right short stub
6373
+ *
6374
+ * "Short stub" = 2rem so the divider visually exists on the squashed
6375
+ * side without dominating the label.
6376
+ */ const isHorizontal = orientation === 'horizontal';
6377
+ const leftStubClass = align === 'start' ? isHorizontal ? 'basis-8 grow-0' : 'basis-8 grow-0' : 'flex-1';
6378
+ const rightStubClass = align === 'end' ? isHorizontal ? 'basis-8 grow-0' : 'basis-8 grow-0' : 'flex-1';
6379
+ const lineCommon = dividerLineVariants({
6380
+ orientation,
6381
+ variant,
6382
+ color,
6383
+ segment: 'grow'
6384
+ });
6385
+ const labelPadding = isHorizontal ? 'px-3' : 'py-3';
6386
+ return jsxs("div", _extends({
6387
+ ref: ref,
6388
+ role: "separator",
6389
+ "aria-orientation": orientation,
6390
+ className: rootClasses
6391
+ }, rest, {
6392
+ children: [
6393
+ jsx("span", {
6394
+ "aria-hidden": "true",
6395
+ className: cn(lineCommon, leftStubClass)
6396
+ }),
6397
+ jsx("span", {
6398
+ className: cn('shrink-0 text-sm text-neutral-500 dark:text-neutral-400', labelPadding),
6399
+ children: children
6400
+ }),
6401
+ jsx("span", {
6402
+ "aria-hidden": "true",
6403
+ className: cn(lineCommon, rightStubClass)
6404
+ })
6405
+ ]
6406
+ }));
6407
+ });
6408
+ Divider.displayName = 'Divider';
6409
+
6410
+ /**
6411
+ * `<AspectRatio>` — locks the aspect ratio of its child container,
6412
+ * regardless of width. The classic use is responsive images and
6413
+ * embedded media: an `<img>` that takes 100% of the available width
6414
+ * but always renders at 16:9 (or 1:1, or whatever the source ratio is)
6415
+ * — no jumping layouts during image load, no whitespace below the
6416
+ * media, no JS measurement.
6417
+ *
6418
+ * Implementation: native CSS `aspect-ratio` property. Supported in
6419
+ * every browser shipped from 2021 onward (Chrome 88, Firefox 89,
6420
+ * Safari 15, Edge 88). No padding-bottom hack — that workaround
6421
+ * predates the native property and brings ugly absolute-positioning
6422
+ * requirements on the child.
6423
+ *
6424
+ * Why a component if it's "just one CSS property"?
6425
+ * Two reasons:
6426
+ * 1. Discoverability — `<AspectRatio ratio={16/9}>` documents the
6427
+ * intent at the call site. `style={{ aspectRatio: '16/9' }}` is
6428
+ * the same thing functionally, but harder to spot in a 200-line
6429
+ * component file.
6430
+ * 2. Composition — pairs naturally with `sx="rounded-xl overflow-hidden"`
6431
+ * for the canonical "rounded clipped media" pattern. Forgetting
6432
+ * the `overflow-hidden` is the #1 mistake we want to prevent
6433
+ * through documentation (it's in this component's MDX, at the
6434
+ * top of the Notes).
6435
+ *
6436
+ * Child contract:
6437
+ * The single child is expected to fill the container — typically
6438
+ * `<img>` / `<video>` with `className="w-full h-full object-cover"`.
6439
+ * We don't force this via CSS (the consumer might want a centered
6440
+ * icon instead of a filling image) — but it's the 99% case, and the
6441
+ * docs show it first.
6442
+ */ const AspectRatio = /*#__PURE__*/ forwardRef(function AspectRatio(props, ref) {
6443
+ const { ratio = 1, as, sx, style, children } = props, rest = _object_without_properties_loose(props, [
6444
+ "ratio",
6445
+ "as",
6446
+ "sx",
6447
+ "style",
6448
+ "children"
6449
+ ]);
6450
+ /*
6451
+ * Normalise to a CSS `aspect-ratio` string. The CSS property
6452
+ * accepts both `16/9` and `16 / 9` (with spaces), but for safety
6453
+ * we convert numbers to the canonical `N / 1` form — `aspectRatio: 1.7777`
6454
+ * works too, but produces an arbitrary-looking value in DevTools.
6455
+ */ const aspectRatioValue = typeof ratio === 'number' ? `${ratio} / 1` : ratio;
6456
+ const mergedStyle = _extends({
6457
+ aspectRatio: aspectRatioValue
6458
+ }, style);
6459
+ const classes = cn('w-full', sx);
6460
+ const Tag = as != null ? as : 'div';
6461
+ return jsx(Tag, _extends({
6462
+ ref: ref,
6463
+ className: classes,
6464
+ style: mergedStyle
6465
+ }, rest, {
6466
+ children: children
6467
+ }));
6468
+ });
6469
+ AspectRatio.displayName = 'AspectRatio';
6470
+
6471
+ /**
6472
+ * `<VisuallyHidden>` — the accessibility primitive.
6473
+ *
6474
+ * Hides children from sighted users (zero pixels rendered, no layout
6475
+ * impact) while keeping them in the accessibility tree. Screen readers
6476
+ * speak the content; voice control software uses it for click targets;
6477
+ * keyboard users see nothing — same as if it weren't there.
6478
+ *
6479
+ * Implementation: Tailwind's built-in `sr-only` utility, which expands to:
6480
+ *
6481
+ * .sr-only {
6482
+ * position: absolute;
6483
+ * width: 1px;
6484
+ * height: 1px;
6485
+ * padding: 0;
6486
+ * margin: -1px;
6487
+ * overflow: hidden;
6488
+ * clip: rect(0,0,0,0);
6489
+ * white-space: nowrap;
6490
+ * border-width: 0;
6491
+ * }
6492
+ *
6493
+ * This is the canonical "visually-hidden but screen-reader-accessible"
6494
+ * pattern, also known as the WebAIM clip technique. Critically:
6495
+ *
6496
+ * • NOT `display: none` (removes from a11y tree)
6497
+ * • NOT `visibility: hidden` (also removes from a11y tree)
6498
+ * • NOT `opacity: 0` (technically still rendered, doesn't help AT)
6499
+ * • NOT `width/height: 0` (collapses, some AT skips it)
6500
+ *
6501
+ * Default tag is `<span>` (inline) — the 99% case is "label inside a
6502
+ * button or link". For block content, override with `as="div"`, but
6503
+ * be aware nesting block inside inline is invalid HTML.
6504
+ *
6505
+ * The component is intentionally tiny (~30 LoC total): one className,
6506
+ * one tag. The value is the COMPONENT NAME — `<VisuallyHidden>` reads
6507
+ * as an intentional a11y decision in code review, while
6508
+ * `className="sr-only"` looks like a typo or a forgotten utility.
6509
+ */ const VisuallyHidden = /*#__PURE__*/ forwardRef(function VisuallyHidden(props, ref) {
6510
+ const { as, sx, children } = props, rest = _object_without_properties_loose(props, [
6511
+ "as",
6512
+ "sx",
6513
+ "children"
6514
+ ]);
6515
+ const Tag = as != null ? as : 'span';
6516
+ const classes = cn('sr-only', sx);
6517
+ return jsx(Tag, _extends({
6518
+ ref: ref,
6519
+ className: classes
6520
+ }, rest, {
6521
+ children: children
6522
+ }));
6523
+ });
6524
+ VisuallyHidden.displayName = 'VisuallyHidden';
6525
+
4929
6526
  /**
4930
6527
  * @dashforge/tw
4931
6528
  *
@@ -4953,6 +6550,6 @@ const SnackbarContext = /*#__PURE__*/ createContext(null);
4953
6550
  */ // ───── Components ─────
4954
6551
  /**
4955
6552
  * Package version (synced with `package.json` at publish time).
4956
- */ const VERSION = '0.1.0-beta';
6553
+ */ const VERSION = '0.2.1-beta';
4957
6554
 
4958
- export { AppShell, Autocomplete, Breadcrumbs, Button, Checkbox, ConfirmDialogProvider, DateTimePicker, LeftNav, NumberField, OTPField, RadioGroup, SnackbarProvider, Switch, TextField, Textarea, TopBar, VERSION, appShellVariants, autocompleteVariants, breadcrumbsVariants, buttonVariants, checkboxVariants, cn, confirmDialogVariants, dateTimePickerVariants, isoToInputValue, leftNavVariants, numberFieldVariants, otpFieldVariants, radioGroupVariants, snackbarVariants, switchVariants, textFieldVariants, textareaVariants, topBarVariants, useAccessState, useConfirm, useSnackbar };
6555
+ export { AppShell, AspectRatio, Autocomplete, Box, Breadcrumbs, Button, Checkbox, ConfirmDialogProvider, Container, DateTimePicker, Divider, Grid, LeftNav, NumberField, OTPField, RadioGroup, SnackbarProvider, Stack, Switch, TextField, Textarea, TopBar, Typography, VERSION, VisuallyHidden, appShellVariants, autocompleteVariants, boxVariants, breadcrumbsVariants, buttonVariants, checkboxVariants, cn, confirmDialogVariants, containerVariants, dateTimePickerVariants, dividerLineVariants, dividerVariants, gridVariants, isoToInputValue, leftNavVariants, numberFieldVariants, otpFieldVariants, radioGroupVariants, snackbarVariants, stackVariants, switchVariants, textFieldVariants, textareaVariants, topBarVariants, typographyVariants, useAccessState, useConfirm, useSnackbar };