@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
@@ -0,0 +1,74 @@
1
+ import { forwardRef, type ElementType, type ReactElement } from 'react';
2
+ import { Slot } from '@radix-ui/react-slot';
3
+ import { cn } from '../../utils/cn.js';
4
+ import { containerVariants } from './container.variants.js';
5
+ import type { ContainerProps } from './container.types.js';
6
+
7
+ /**
8
+ * `<Container>` — centered max-width wrapper for page-level layouts.
9
+ *
10
+ * The pattern is universal: every page-root `<div>` in a non-trivial
11
+ * web app boils down to "mx-auto + max-w-X + responsive horizontal
12
+ * padding". Container collapses that into a typed prop set so the
13
+ * decision lives in ONE place per app section (the Container size),
14
+ * not scattered as utility chains across every page file.
15
+ *
16
+ * Default size is `'xl'` (1280px) — comfortable for full app shells
17
+ * with a left nav + main + optional right rail. Drop to `'lg'` (1024px)
18
+ * for content-heavy docs/marketing, jump to `'2xl'` (1536px) for wide
19
+ * dashboards.
20
+ *
21
+ * Composition pattern — Container at the page root, Stack/Grid inside:
22
+ *
23
+ * <Container size="lg" as="main">
24
+ * <Stack gap={8}>
25
+ * <Typography variant="h1">Page title</Typography>
26
+ * <Grid container spacing={6}>
27
+ * <Grid xs={12} md={6}>...</Grid>
28
+ * </Grid>
29
+ * </Stack>
30
+ * </Container>
31
+ *
32
+ * Container handles the page chrome (centered, padded, capped width);
33
+ * Stack/Grid handle the actual layout of children. Two concerns, two
34
+ * primitives.
35
+ *
36
+ * Polymorphism rule (same as Typography/Box/Stack/Grid): when both
37
+ * `as` and `asChild` are passed, `asChild` wins.
38
+ */
39
+ export const Container = forwardRef<HTMLElement, ContainerProps>(
40
+ function Container(props, ref) {
41
+ const {
42
+ size,
43
+ px,
44
+ centerContent,
45
+ as,
46
+ asChild = false,
47
+ sx,
48
+ children,
49
+ ...rest
50
+ } = props;
51
+
52
+ const classes = cn(
53
+ containerVariants({ size, px, centerContent }),
54
+ sx,
55
+ );
56
+
57
+ if (asChild) {
58
+ return (
59
+ <Slot ref={ref} className={classes} {...rest}>
60
+ {children as ReactElement}
61
+ </Slot>
62
+ );
63
+ }
64
+
65
+ const Tag = (as ?? 'div') as ElementType;
66
+ return (
67
+ <Tag ref={ref as never} className={classes} {...rest}>
68
+ {children}
69
+ </Tag>
70
+ );
71
+ },
72
+ );
73
+
74
+ Container.displayName = 'Container';
@@ -0,0 +1,45 @@
1
+ import type { ElementType, HTMLAttributes } from 'react';
2
+ import type { ContainerVariants } from './container.variants.js';
3
+
4
+ /**
5
+ * Props for `<Container>` — page-level centered max-width wrapper.
6
+ *
7
+ * Use at the page-root level (or section-root) to constrain content
8
+ * width and apply the canonical responsive horizontal padding ramp.
9
+ * Compose Stack/Grid INSIDE Container for the actual layout work.
10
+ *
11
+ * Native attribute overrides:
12
+ * • `className` omitted in favour of `sx` (utility string, merged
13
+ * via tailwind-merge — same convention as Box/Stack/Grid/Typography).
14
+ */
15
+ export interface ContainerProps
16
+ extends Omit<HTMLAttributes<HTMLDivElement>, 'className'>,
17
+ Pick<ContainerVariants, 'size' | 'px' | 'centerContent'> {
18
+ /**
19
+ * Override the rendered HTML tag. Defaults to `'div'`. Use
20
+ * `as="main"` for the primary content region of a page,
21
+ * `as="section"` for a named section, `as="article"` for a
22
+ * self-contained article shell.
23
+ *
24
+ * Ignored when `asChild` is true.
25
+ */
26
+ as?: ElementType;
27
+
28
+ /**
29
+ * Render via Radix `Slot` — the Container styles paint onto the
30
+ * single React child instead of wrapping it. Useful for letting a
31
+ * router `<main>` element receive the Container chrome without
32
+ * adding an intermediate `<div>` in the tree.
33
+ *
34
+ * Mutually exclusive with `as` (when both are passed, `asChild` wins).
35
+ */
36
+ asChild?: boolean;
37
+
38
+ /**
39
+ * Utility classes appended to the variant chain. Resolved via
40
+ * `tailwind-merge` so the consumer's classes always win over the
41
+ * variant defaults — useful for one-off vertical padding (`sx="py-8"`),
42
+ * custom max-width (`sx="max-w-3xl"`), etc.
43
+ */
44
+ sx?: string;
45
+ }
@@ -0,0 +1,81 @@
1
+ import { tv, type VariantProps } from 'tailwind-variants';
2
+
3
+ /**
4
+ * `containerVariants` — centered max-width wrapper for page layouts.
5
+ *
6
+ * Architectural role:
7
+ *
8
+ * Every web app has the same pattern at the page-root level: a div
9
+ * that's `mx-auto`, capped at some `max-w-*`, with responsive
10
+ * horizontal padding. Without Container, every page rewrites:
11
+ *
12
+ * <div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
13
+ *
14
+ * Container collapses that into one typed prop set, with size names
15
+ * that mirror Tailwind's breakpoint vocabulary so the muscle memory
16
+ * carries over (`size="lg"` ↔ `max-w-screen-lg`).
17
+ *
18
+ * Size axis — maps to Tailwind's `max-w-screen-*` aliases:
19
+ * • sm → max-w-screen-sm (640px)
20
+ * • md → max-w-screen-md (768px)
21
+ * • lg → max-w-screen-lg (1024px) — most common doc/content cap
22
+ * • xl → max-w-screen-xl (1280px) — default; comfortable for full apps
23
+ * • 2xl → max-w-screen-2xl (1536px) — wide dashboards
24
+ * • fluid → no max-width at all (full bleed, padding still applies)
25
+ *
26
+ * Padding axis (`px`):
27
+ * • true (default) — responsive horizontal padding (px-4 sm:px-6 lg:px-8)
28
+ * The canonical Tailwind responsive padding ramp. Designed to keep
29
+ * edges from kissing the viewport on mobile and breathing more on
30
+ * larger screens.
31
+ * • false — no padding. Use when the consumer wants full bleed AND
32
+ * handles edge padding inside (e.g. a hero section with its own
33
+ * internal spacing scale).
34
+ *
35
+ * Center content axis (`centerContent`):
36
+ * • true — turns the Container into a flex column with items-center
37
+ * so the page content stacks centered horizontally. Common for
38
+ * marketing pages, sign-in flows, "single artifact" layouts.
39
+ * • false (default) — children flow normally (block stacking).
40
+ */
41
+ export const containerVariants = tv({
42
+ base: 'mx-auto w-full',
43
+
44
+ variants: {
45
+ size: {
46
+ sm: 'max-w-screen-sm',
47
+ md: 'max-w-screen-md',
48
+ lg: 'max-w-screen-lg',
49
+ xl: 'max-w-screen-xl',
50
+ '2xl': 'max-w-screen-2xl',
51
+ fluid: '',
52
+ },
53
+
54
+ /*
55
+ * Responsive padding ramp. `false` skips it entirely so the
56
+ * consumer can supply custom padding via `sx` when needed.
57
+ */
58
+ px: {
59
+ true: 'px-4 sm:px-6 lg:px-8',
60
+ false: '',
61
+ },
62
+
63
+ /*
64
+ * Stacks children centered. Mutually compatible with all sizes —
65
+ * a fluid container with centerContent is the canonical "marketing
66
+ * hero" layout.
67
+ */
68
+ centerContent: {
69
+ true: 'flex flex-col items-center',
70
+ false: '',
71
+ },
72
+ },
73
+
74
+ defaultVariants: {
75
+ size: 'xl',
76
+ px: true,
77
+ centerContent: false,
78
+ },
79
+ });
80
+
81
+ export type ContainerVariants = VariantProps<typeof containerVariants>;
@@ -0,0 +1,241 @@
1
+ // @vitest-environment jsdom
2
+ import { describe, it, expect } from 'vitest';
3
+ import { render } from '@testing-library/react';
4
+ import { Divider } from './Divider.js';
5
+
6
+ describe('<Divider>', () => {
7
+ // ─── Line-only mode ─────────────────────────────────────────────────
8
+ describe('line-only (no children)', () => {
9
+ it('renders <hr> for horizontal', () => {
10
+ const { container } = render(<Divider />);
11
+ expect(container.firstElementChild?.tagName).toBe('HR');
12
+ });
13
+
14
+ it('renders <div role="separator"> for vertical', () => {
15
+ const { container } = render(<Divider orientation="vertical" />);
16
+ const el = container.firstElementChild;
17
+ expect(el?.tagName).toBe('DIV');
18
+ expect(el?.getAttribute('role')).toBe('separator');
19
+ });
20
+
21
+ it('sets aria-orientation correctly', () => {
22
+ const { container: ch } = render(<Divider />);
23
+ expect(ch.firstElementChild?.getAttribute('aria-orientation')).toBe('horizontal');
24
+
25
+ const { container: cv } = render(<Divider orientation="vertical" />);
26
+ expect(cv.firstElementChild?.getAttribute('aria-orientation')).toBe('vertical');
27
+ });
28
+
29
+ it('horizontal applies border-t (line carrier)', () => {
30
+ const { container } = render(<Divider />);
31
+ const cls = container.firstElementChild?.className ?? '';
32
+ expect(cls).toContain('border-t');
33
+ });
34
+
35
+ it('vertical applies border-l', () => {
36
+ const { container } = render(<Divider orientation="vertical" />);
37
+ expect(container.firstElementChild?.className).toContain('border-l');
38
+ });
39
+
40
+ it('strips <hr> UA default via border-0', () => {
41
+ const { container } = render(<Divider />);
42
+ expect(container.firstElementChild?.className).toContain('border-0');
43
+ });
44
+ });
45
+
46
+ // ─── Variants — line style ──────────────────────────────────────────
47
+ describe('variant', () => {
48
+ it('default solid', () => {
49
+ const { container } = render(<Divider />);
50
+ expect(container.firstElementChild?.className).toContain('border-solid');
51
+ });
52
+
53
+ it('dashed', () => {
54
+ const { container } = render(<Divider variant="dashed" />);
55
+ expect(container.firstElementChild?.className).toContain('border-dashed');
56
+ });
57
+
58
+ it('dotted', () => {
59
+ const { container } = render(<Divider variant="dotted" />);
60
+ expect(container.firstElementChild?.className).toContain('border-dotted');
61
+ });
62
+ });
63
+
64
+ // ─── Color intent ───────────────────────────────────────────────────
65
+ describe('color', () => {
66
+ it('default neutral', () => {
67
+ const { container } = render(<Divider />);
68
+ const cls = container.firstElementChild?.className ?? '';
69
+ expect(cls).toContain('border-neutral-200');
70
+ expect(cls).toContain('dark:border-neutral-800');
71
+ });
72
+
73
+ it('color="primary"', () => {
74
+ const { container } = render(<Divider color="primary" />);
75
+ expect(container.firstElementChild?.className).toContain('border-primary-300');
76
+ });
77
+
78
+ it('color="danger"', () => {
79
+ const { container } = render(<Divider color="danger" />);
80
+ expect(container.firstElementChild?.className).toContain('border-danger-300');
81
+ });
82
+ });
83
+
84
+ // ─── Labeled mode ───────────────────────────────────────────────────
85
+ describe('labeled (with children)', () => {
86
+ it('renders 3 children: line, label, line', () => {
87
+ const { container } = render(<Divider>OR</Divider>);
88
+ const root = container.firstElementChild;
89
+ expect(root?.children.length).toBe(3);
90
+ });
91
+
92
+ it('label text is preserved', () => {
93
+ const { container } = render(<Divider>OR</Divider>);
94
+ expect(container.firstElementChild?.textContent).toBe('OR');
95
+ });
96
+
97
+ it('root has role="separator" + aria-orientation', () => {
98
+ const { container } = render(<Divider>OR</Divider>);
99
+ const root = container.firstElementChild;
100
+ expect(root?.getAttribute('role')).toBe('separator');
101
+ expect(root?.getAttribute('aria-orientation')).toBe('horizontal');
102
+ });
103
+
104
+ it('line segments are aria-hidden (separator role is on the root)', () => {
105
+ const { container } = render(<Divider>OR</Divider>);
106
+ const lines = container.firstElementChild?.querySelectorAll('span[aria-hidden]');
107
+ expect(lines?.length).toBe(2);
108
+ });
109
+
110
+ it('center alignment: both segments flex-1', () => {
111
+ const { container } = render(<Divider align="center">OR</Divider>);
112
+ const lines = container.firstElementChild?.querySelectorAll('span[aria-hidden]');
113
+ lines?.forEach((line) => {
114
+ expect(line.className).toContain('flex-1');
115
+ });
116
+ });
117
+
118
+ it('start alignment: left segment is short stub', () => {
119
+ const { container } = render(<Divider align="start">OR</Divider>);
120
+ const lines = container.firstElementChild?.querySelectorAll('span[aria-hidden]');
121
+ expect(lines?.[0].className).toContain('basis-8');
122
+ expect(lines?.[1].className).toContain('flex-1');
123
+ });
124
+
125
+ it('end alignment: right segment is short stub', () => {
126
+ const { container } = render(<Divider align="end">OR</Divider>);
127
+ const lines = container.firstElementChild?.querySelectorAll('span[aria-hidden]');
128
+ expect(lines?.[0].className).toContain('flex-1');
129
+ expect(lines?.[1].className).toContain('basis-8');
130
+ });
131
+
132
+ it('label gets shrink-0 + padding so it does not collapse', () => {
133
+ const { container } = render(<Divider>Hello</Divider>);
134
+ const label = container.firstElementChild?.children[1] as HTMLElement;
135
+ expect(label.className).toContain('shrink-0');
136
+ expect(label.className).toContain('px-3');
137
+ });
138
+
139
+ it('color applies to both segments in labeled mode', () => {
140
+ const { container } = render(<Divider color="primary">OR</Divider>);
141
+ const lines = container.firstElementChild?.querySelectorAll('span[aria-hidden]');
142
+ lines?.forEach((line) => {
143
+ expect(line.className).toContain('border-primary-300');
144
+ });
145
+ });
146
+
147
+ it('renders complex children (JSX)', () => {
148
+ const { container } = render(
149
+ <Divider>
150
+ <strong>Section title</strong>
151
+ </Divider>,
152
+ );
153
+ const label = container.firstElementChild?.children[1];
154
+ expect(label?.querySelector('strong')?.textContent).toBe('Section title');
155
+ });
156
+ });
157
+
158
+ // ─── Override ───────────────────────────────────────────────────────
159
+ describe('override', () => {
160
+ it('sx wins over variant defaults', () => {
161
+ const { container } = render(<Divider sx="border-t-4" />);
162
+ const cls = container.firstElementChild?.className ?? '';
163
+ expect(cls).toContain('border-t-4');
164
+ // tailwind-merge collapses border-t → border-t-4
165
+ expect(cls).not.toMatch(/border-t (?!-)/);
166
+ });
167
+
168
+ it('sx applies on root in labeled mode', () => {
169
+ const { container } = render(<Divider sx="my-8">OR</Divider>);
170
+ expect(container.firstElementChild?.className).toContain('my-8');
171
+ });
172
+ });
173
+
174
+ // ─── Pass-through ───────────────────────────────────────────────────
175
+ describe('pass-through', () => {
176
+ it('forwards data-* on root', () => {
177
+ const { container } = render(<Divider data-testid="d" />);
178
+ expect(container.firstElementChild?.getAttribute('data-testid')).toBe('d');
179
+ });
180
+
181
+ it('forwards data-* in labeled mode too', () => {
182
+ const { container } = render(<Divider data-testid="d">OR</Divider>);
183
+ expect(container.firstElementChild?.getAttribute('data-testid')).toBe('d');
184
+ });
185
+ });
186
+
187
+ // ─── F11-bis edge cases ─────────────────────────────────────────────
188
+ describe('color — every intent', () => {
189
+ const INTENTS = ['secondary', 'success', 'warning', 'danger', 'info'] as const;
190
+ it.each(INTENTS)('color="%s" emits border-%s-300 with dark pair', (color) => {
191
+ const { container } = render(<Divider color={color} />);
192
+ const cls = container.firstElementChild?.className ?? '';
193
+ expect(cls).toContain(`border-${color}-300`);
194
+ expect(cls).toContain(`dark:border-${color}-700`);
195
+ });
196
+ });
197
+
198
+ describe('vertical labeled mode', () => {
199
+ it('vertical + label renders all 3 children with vertical layout', () => {
200
+ const { container } = render(
201
+ <Divider orientation="vertical">section</Divider>,
202
+ );
203
+ const root = container.firstElementChild;
204
+ expect(root?.tagName).toBe('DIV');
205
+ expect(root?.getAttribute('aria-orientation')).toBe('vertical');
206
+ expect(root?.className).toContain('flex-col');
207
+ expect(root?.children.length).toBe(3);
208
+ });
209
+ });
210
+
211
+ describe('label edge values', () => {
212
+ it('numeric label (0) renders correctly', () => {
213
+ const { container } = render(<Divider>{0}</Divider>);
214
+ // 0 is a valid React child, should render as "0"
215
+ expect(container.firstElementChild?.textContent).toBe('0');
216
+ });
217
+
218
+ it('JSX with multiple elements as label', () => {
219
+ const { container } = render(
220
+ <Divider>
221
+ <strong>bold</strong>
222
+ <em> italic</em>
223
+ </Divider>,
224
+ );
225
+ const root = container.firstElementChild;
226
+ // root has 3 children (line + label-span + line); label-span has 2 nested
227
+ expect(root?.children.length).toBe(3);
228
+ expect(root?.querySelector('strong')?.textContent).toBe('bold');
229
+ expect(root?.querySelector('em')?.textContent).toBe(' italic');
230
+ });
231
+ });
232
+
233
+ describe('flexItem (no-op forward-compat hook)', () => {
234
+ it('flexItem prop does not crash or leak as attribute', () => {
235
+ const { container } = render(<Divider flexItem />);
236
+ const el = container.firstElementChild as HTMLElement;
237
+ expect(el.hasAttribute('flexitem')).toBe(false);
238
+ expect(el.hasAttribute('flexItem')).toBe(false);
239
+ });
240
+ });
241
+ });
@@ -0,0 +1,140 @@
1
+ import { forwardRef } from 'react';
2
+ import { cn } from '../../utils/cn.js';
3
+ import { dividerVariants, dividerLineVariants } from './divider.variants.js';
4
+ import type { DividerProps } from './divider.types.js';
5
+
6
+ /**
7
+ * `<Divider>` — visual separator. Two modes:
8
+ *
9
+ * 1. Line-only (no children):
10
+ *
11
+ * <Divider />
12
+ * → <hr role="separator" aria-orientation="horizontal" />
13
+ *
14
+ * Renders a single horizontal line via `border-t-*` on an `<hr>`
15
+ * (which by default has no `display`, no margin in our reset, just
16
+ * acts as the border carrier).
17
+ *
18
+ * 2. Labeled (with children):
19
+ *
20
+ * <Divider><Typography variant="overline">OR</Typography></Divider>
21
+ * → <div role="separator" aria-orientation="horizontal">
22
+ * <span aria-hidden className="flex-1 border-t" />
23
+ * <span className="px-3">OR</span>
24
+ * <span aria-hidden className="flex-1 border-t" />
25
+ * </div>
26
+ *
27
+ * Two line segments share the available space; the label sits
28
+ * between them with horizontal padding for breathing room. Each
29
+ * line segment is `aria-hidden` (the role="separator" on the root
30
+ * conveys the separator semantics — duplicating it would confuse
31
+ * screen readers).
32
+ *
33
+ * Alignment:
34
+ * • align="start" → label hugs the left, right segment grows
35
+ * • align="center" (default) → both segments equal
36
+ * • align="end" → label hugs the right, left segment grows
37
+ *
38
+ * Implemented by setting `flex-basis: 0` to either segment to keep
39
+ * it minimal. We use `basis-[2rem]` (32px short stub) rather than
40
+ * `basis-0` so a tiny line still hints at the divider on the squashed
41
+ * side — pure flex-1 vs basis-0 would collapse it invisibly.
42
+ *
43
+ * Vertical orientation:
44
+ * The line orientation flips (border-l instead of border-t). Vertical
45
+ * labeled dividers are RARE in practice — supported for parity but
46
+ * the typical use is line-only between flex row items.
47
+ *
48
+ * a11y:
49
+ * • Root element gets `role="separator"` (or implicit via `<hr>`)
50
+ * • `aria-orientation` is set explicitly so AT knows the axis
51
+ * • Labeled mode: label has no extra role; the visual layout speaks
52
+ */
53
+ export const Divider = forwardRef<HTMLElement, DividerProps>(
54
+ function Divider(props, ref) {
55
+ const {
56
+ orientation = 'horizontal',
57
+ align = 'center',
58
+ variant = 'solid',
59
+ color = 'neutral',
60
+ children,
61
+ flexItem, // currently a no-op (see types) — `self-stretch` is in TV base
62
+ sx,
63
+ ...rest
64
+ } = props;
65
+
66
+ // Reference flexItem to silence unused-var lint while keeping it in the
67
+ // public API for parity with MUI Divider. Forward-compat hook.
68
+ void flexItem;
69
+
70
+ // ─── Mode 1: line-only (no children) ─────────────────────────────
71
+ if (children == null) {
72
+ const lineClasses = cn(
73
+ dividerLineVariants({ orientation, variant, color, segment: 'full' }),
74
+ sx,
75
+ );
76
+
77
+ // Horizontal → <hr>: zero default styling once we strip the UA
78
+ // default `<hr>` border via `border-0` (in `dividerLineVariants`
79
+ // base) and apply our own `border-t-*`. Vertical → <div> because
80
+ // <hr> can't be vertical cross-browser-consistently.
81
+ if (orientation === 'horizontal') {
82
+ return (
83
+ <hr
84
+ ref={ref as never}
85
+ role="separator"
86
+ aria-orientation="horizontal"
87
+ className={cn('border-0', lineClasses)}
88
+ {...rest}
89
+ />
90
+ );
91
+ }
92
+ return (
93
+ <div
94
+ ref={ref as never}
95
+ role="separator"
96
+ aria-orientation="vertical"
97
+ className={lineClasses}
98
+ {...rest}
99
+ />
100
+ );
101
+ }
102
+
103
+ // ─── Mode 2: labeled (with children) ─────────────────────────────
104
+ const rootClasses = cn(dividerVariants({ orientation, align }), sx);
105
+
106
+ /*
107
+ * Segment widths per `align`:
108
+ * • center → both flex-1 (equal share)
109
+ * • start → left short stub, right grows
110
+ * • end → left grows, right short stub
111
+ *
112
+ * "Short stub" = 2rem so the divider visually exists on the squashed
113
+ * side without dominating the label.
114
+ */
115
+ const isHorizontal = orientation === 'horizontal';
116
+ const leftStubClass = align === 'start' ? (isHorizontal ? 'basis-8 grow-0' : 'basis-8 grow-0') : 'flex-1';
117
+ const rightStubClass = align === 'end' ? (isHorizontal ? 'basis-8 grow-0' : 'basis-8 grow-0') : 'flex-1';
118
+
119
+ const lineCommon = dividerLineVariants({ orientation, variant, color, segment: 'grow' });
120
+ const labelPadding = isHorizontal ? 'px-3' : 'py-3';
121
+
122
+ return (
123
+ <div
124
+ ref={ref as never}
125
+ role="separator"
126
+ aria-orientation={orientation}
127
+ className={rootClasses}
128
+ {...rest}
129
+ >
130
+ <span aria-hidden="true" className={cn(lineCommon, leftStubClass)} />
131
+ <span className={cn('shrink-0 text-sm text-neutral-500 dark:text-neutral-400', labelPadding)}>
132
+ {children}
133
+ </span>
134
+ <span aria-hidden="true" className={cn(lineCommon, rightStubClass)} />
135
+ </div>
136
+ );
137
+ },
138
+ );
139
+
140
+ Divider.displayName = 'Divider';
@@ -0,0 +1,52 @@
1
+ import type { HTMLAttributes, ReactNode } from 'react';
2
+ import type { DividerVariants, DividerLineVariants } from './divider.variants.js';
3
+
4
+ /**
5
+ * Props for `<Divider>` — visual separator with optional inline label.
6
+ *
7
+ * Two rendering modes:
8
+ * • Line-only (no `children`) — renders an `<hr>` (horizontal) or
9
+ * `<div role="separator">` (vertical).
10
+ * • Labeled (with `children`) — renders a flex row: two line
11
+ * segments around the centered label.
12
+ * Canonical "OR" separator pattern.
13
+ *
14
+ * The user passes ONE consistent prop set; the implementation chooses
15
+ * the mode based on whether `children` is provided.
16
+ */
17
+ export interface DividerProps
18
+ extends Omit<HTMLAttributes<HTMLElement>, 'className' | 'color' | 'children'>,
19
+ Pick<DividerVariants, 'orientation' | 'align'>,
20
+ Pick<DividerLineVariants, 'variant' | 'color'> {
21
+ /**
22
+ * Optional inline label. When present, the divider switches from
23
+ * "single line" to "two segments + label" layout — the canonical
24
+ * "OR" separator pattern between sign-in methods, section titled
25
+ * separators, etc.
26
+ *
27
+ * Accepts any node (string, Typography element, Stack with icon,
28
+ * etc.). Recommended: `<Typography variant="overline">OR</Typography>`
29
+ * for short uppercase labels.
30
+ */
31
+ children?: ReactNode;
32
+
33
+ /**
34
+ * In a flex Stack `direction="row"`, a `<Divider orientation="vertical">`
35
+ * with `flexItem` stretches to the row's cross-axis height (via
36
+ * `self-stretch`). Without it, vertical dividers may collapse to
37
+ * 0 height because there's nothing to anchor them.
38
+ *
39
+ * Implementation note: `self-stretch` is already applied on the
40
+ * vertical line segment by default in the TV — this prop is a
41
+ * no-op for now, kept for API parity with MUI's same flag (and as
42
+ * a forward-compatibility hook if we ever change the default).
43
+ */
44
+ flexItem?: boolean;
45
+
46
+ /**
47
+ * Utility classes appended to the variant chain. Resolved via
48
+ * `tailwind-merge`. Useful for one-off margin (`sx="my-8"`) or
49
+ * a custom border weight (`sx="border-t-2"`).
50
+ */
51
+ sx?: string;
52
+ }