@dashforge/tw 0.1.0-beta → 0.2.0-beta

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) hide show
  1. package/CHANGELOG.md +108 -0
  2. package/dist/index.d.ts +1 -82
  3. package/dist/index.esm.js +1563 -2
  4. package/dist/src/components/AspectRatio/AspectRatio.d.ts +36 -0
  5. package/dist/src/components/AspectRatio/AspectRatio.d.ts.map +1 -0
  6. package/dist/src/components/AspectRatio/aspectRatio.types.d.ts +50 -0
  7. package/dist/src/components/AspectRatio/aspectRatio.types.d.ts.map +1 -0
  8. package/dist/src/components/Box/Box.d.ts +36 -0
  9. package/dist/src/components/Box/Box.d.ts.map +1 -0
  10. package/dist/src/components/Box/box.types.d.ts +54 -0
  11. package/dist/src/components/Box/box.types.d.ts.map +1 -0
  12. package/dist/src/components/Box/box.variants.d.ts +402 -0
  13. package/dist/src/components/Box/box.variants.d.ts.map +1 -0
  14. package/dist/src/components/Container/Container.d.ts +35 -0
  15. package/dist/src/components/Container/Container.d.ts.map +1 -0
  16. package/dist/src/components/Container/container.types.d.ts +41 -0
  17. package/dist/src/components/Container/container.types.d.ts.map +1 -0
  18. package/dist/src/components/Container/container.variants.d.ts +93 -0
  19. package/dist/src/components/Container/container.variants.d.ts.map +1 -0
  20. package/dist/src/components/Divider/Divider.d.ts +50 -0
  21. package/dist/src/components/Divider/Divider.d.ts.map +1 -0
  22. package/dist/src/components/Divider/divider.types.d.ts +47 -0
  23. package/dist/src/components/Divider/divider.types.d.ts.map +1 -0
  24. package/dist/src/components/Divider/divider.variants.d.ts +129 -0
  25. package/dist/src/components/Divider/divider.variants.d.ts.map +1 -0
  26. package/dist/src/components/Grid/Grid.d.ts +32 -0
  27. package/dist/src/components/Grid/Grid.d.ts.map +1 -0
  28. package/dist/src/components/Grid/grid.types.d.ts +103 -0
  29. package/dist/src/components/Grid/grid.types.d.ts.map +1 -0
  30. package/dist/src/components/Grid/grid.variants.d.ts +453 -0
  31. package/dist/src/components/Grid/grid.variants.d.ts.map +1 -0
  32. package/dist/src/components/Stack/Stack.d.ts +24 -0
  33. package/dist/src/components/Stack/Stack.d.ts.map +1 -0
  34. package/dist/src/components/Stack/stack.types.d.ts +61 -0
  35. package/dist/src/components/Stack/stack.types.d.ts.map +1 -0
  36. package/dist/src/components/Stack/stack.variants.d.ts +163 -0
  37. package/dist/src/components/Stack/stack.variants.d.ts.map +1 -0
  38. package/dist/src/components/Typography/Typography.d.ts +42 -0
  39. package/dist/src/components/Typography/Typography.d.ts.map +1 -0
  40. package/dist/src/components/Typography/typography.types.d.ts +61 -0
  41. package/dist/src/components/Typography/typography.types.d.ts.map +1 -0
  42. package/dist/src/components/Typography/typography.variants.d.ts +167 -0
  43. package/dist/src/components/Typography/typography.variants.d.ts.map +1 -0
  44. package/dist/src/components/VisuallyHidden/VisuallyHidden.d.ts +42 -0
  45. package/dist/src/components/VisuallyHidden/VisuallyHidden.d.ts.map +1 -0
  46. package/dist/src/components/VisuallyHidden/visuallyHidden.types.d.ts +42 -0
  47. package/dist/src/components/VisuallyHidden/visuallyHidden.types.d.ts.map +1 -0
  48. package/dist/src/index.d.ts +22 -0
  49. package/dist/src/index.d.ts.map +1 -1
  50. package/package.json +3 -3
  51. package/src/components/AspectRatio/AspectRatio.test.tsx +173 -0
  52. package/src/components/AspectRatio/AspectRatio.tsx +73 -0
  53. package/src/components/AspectRatio/aspectRatio.types.ts +54 -0
  54. package/src/components/Box/Box.test.tsx +349 -0
  55. package/src/components/Box/Box.tsx +83 -0
  56. package/src/components/Box/box.types.ts +61 -0
  57. package/src/components/Box/box.variants.ts +211 -0
  58. package/src/components/Container/Container.test.tsx +209 -0
  59. package/src/components/Container/Container.tsx +74 -0
  60. package/src/components/Container/container.types.ts +45 -0
  61. package/src/components/Container/container.variants.ts +81 -0
  62. package/src/components/Divider/Divider.test.tsx +241 -0
  63. package/src/components/Divider/Divider.tsx +140 -0
  64. package/src/components/Divider/divider.types.ts +52 -0
  65. package/src/components/Divider/divider.variants.ts +115 -0
  66. package/src/components/Grid/Grid.test.tsx +321 -0
  67. package/src/components/Grid/Grid.tsx +148 -0
  68. package/src/components/Grid/grid.types.ts +112 -0
  69. package/src/components/Grid/grid.variants.ts +133 -0
  70. package/src/components/Stack/Stack.test.tsx +309 -0
  71. package/src/components/Stack/Stack.tsx +115 -0
  72. package/src/components/Stack/stack.types.ts +68 -0
  73. package/src/components/Stack/stack.variants.ts +80 -0
  74. package/src/components/Typography/Typography.test.tsx +230 -0
  75. package/src/components/Typography/Typography.tsx +112 -0
  76. package/src/components/Typography/typography.types.ts +65 -0
  77. package/src/components/Typography/typography.variants.ts +113 -0
  78. package/src/components/VisuallyHidden/VisuallyHidden.test.tsx +86 -0
  79. package/src/components/VisuallyHidden/VisuallyHidden.tsx +59 -0
  80. package/src/components/VisuallyHidden/visuallyHidden.types.ts +45 -0
  81. package/src/index.ts +49 -0
  82. package/dist/components/AppShell/AppShell.d.ts +0 -32
  83. package/dist/components/AppShell/AppShell.d.ts.map +0 -1
  84. package/dist/components/AppShell/appShell.types.d.ts +0 -62
  85. package/dist/components/AppShell/appShell.types.d.ts.map +0 -1
  86. package/dist/components/AppShell/appShell.variants.d.ts +0 -65
  87. package/dist/components/AppShell/appShell.variants.d.ts.map +0 -1
  88. package/dist/components/Autocomplete/Autocomplete.d.ts +0 -32
  89. package/dist/components/Autocomplete/Autocomplete.d.ts.map +0 -1
  90. package/dist/components/Autocomplete/autocomplete.types.d.ts +0 -214
  91. package/dist/components/Autocomplete/autocomplete.types.d.ts.map +0 -1
  92. package/dist/components/Autocomplete/autocomplete.variants.d.ts +0 -214
  93. package/dist/components/Autocomplete/autocomplete.variants.d.ts.map +0 -1
  94. package/dist/components/Breadcrumbs/Breadcrumbs.d.ts +0 -23
  95. package/dist/components/Breadcrumbs/Breadcrumbs.d.ts.map +0 -1
  96. package/dist/components/Breadcrumbs/breadcrumbs.types.d.ts +0 -98
  97. package/dist/components/Breadcrumbs/breadcrumbs.types.d.ts.map +0 -1
  98. package/dist/components/Breadcrumbs/breadcrumbs.variants.d.ts +0 -85
  99. package/dist/components/Breadcrumbs/breadcrumbs.variants.d.ts.map +0 -1
  100. package/dist/components/Button/Button.d.ts +0 -44
  101. package/dist/components/Button/Button.d.ts.map +0 -1
  102. package/dist/components/Button/button.types.d.ts +0 -66
  103. package/dist/components/Button/button.types.d.ts.map +0 -1
  104. package/dist/components/Button/button.variants.d.ts +0 -104
  105. package/dist/components/Button/button.variants.d.ts.map +0 -1
  106. package/dist/components/Checkbox/Checkbox.d.ts +0 -31
  107. package/dist/components/Checkbox/Checkbox.d.ts.map +0 -1
  108. package/dist/components/Checkbox/checkbox.types.d.ts +0 -86
  109. package/dist/components/Checkbox/checkbox.types.d.ts.map +0 -1
  110. package/dist/components/Checkbox/checkbox.variants.d.ts +0 -109
  111. package/dist/components/Checkbox/checkbox.variants.d.ts.map +0 -1
  112. package/dist/components/ConfirmDialog/ConfirmDialog.d.ts +0 -38
  113. package/dist/components/ConfirmDialog/ConfirmDialog.d.ts.map +0 -1
  114. package/dist/components/ConfirmDialog/confirmDialog.types.d.ts +0 -80
  115. package/dist/components/ConfirmDialog/confirmDialog.types.d.ts.map +0 -1
  116. package/dist/components/ConfirmDialog/confirmDialog.variants.d.ts +0 -90
  117. package/dist/components/ConfirmDialog/confirmDialog.variants.d.ts.map +0 -1
  118. package/dist/components/DateTimePicker/DateTimePicker.d.ts +0 -49
  119. package/dist/components/DateTimePicker/DateTimePicker.d.ts.map +0 -1
  120. package/dist/components/DateTimePicker/dateTimePicker.types.d.ts +0 -95
  121. package/dist/components/DateTimePicker/dateTimePicker.types.d.ts.map +0 -1
  122. package/dist/components/DateTimePicker/dateTimePicker.variants.d.ts +0 -165
  123. package/dist/components/DateTimePicker/dateTimePicker.variants.d.ts.map +0 -1
  124. package/dist/components/LeftNav/LeftNav.d.ts +0 -34
  125. package/dist/components/LeftNav/LeftNav.d.ts.map +0 -1
  126. package/dist/components/LeftNav/leftNav.types.d.ts +0 -136
  127. package/dist/components/LeftNav/leftNav.types.d.ts.map +0 -1
  128. package/dist/components/LeftNav/leftNav.variants.d.ts +0 -143
  129. package/dist/components/LeftNav/leftNav.variants.d.ts.map +0 -1
  130. package/dist/components/NumberField/NumberField.d.ts +0 -18
  131. package/dist/components/NumberField/NumberField.d.ts.map +0 -1
  132. package/dist/components/NumberField/numberField.types.d.ts +0 -79
  133. package/dist/components/NumberField/numberField.types.d.ts.map +0 -1
  134. package/dist/components/NumberField/numberField.variants.d.ts +0 -169
  135. package/dist/components/NumberField/numberField.variants.d.ts.map +0 -1
  136. package/dist/components/OTPField/OTPField.d.ts +0 -25
  137. package/dist/components/OTPField/OTPField.d.ts.map +0 -1
  138. package/dist/components/OTPField/otpField.types.d.ts +0 -74
  139. package/dist/components/OTPField/otpField.types.d.ts.map +0 -1
  140. package/dist/components/OTPField/otpField.variants.d.ts +0 -105
  141. package/dist/components/OTPField/otpField.variants.d.ts.map +0 -1
  142. package/dist/components/RadioGroup/RadioGroup.d.ts +0 -35
  143. package/dist/components/RadioGroup/RadioGroup.d.ts.map +0 -1
  144. package/dist/components/RadioGroup/radioGroup.types.d.ts +0 -103
  145. package/dist/components/RadioGroup/radioGroup.types.d.ts.map +0 -1
  146. package/dist/components/RadioGroup/radioGroup.variants.d.ts +0 -166
  147. package/dist/components/RadioGroup/radioGroup.variants.d.ts.map +0 -1
  148. package/dist/components/Snackbar/Snackbar.d.ts +0 -37
  149. package/dist/components/Snackbar/Snackbar.d.ts.map +0 -1
  150. package/dist/components/Snackbar/snackbar.types.d.ts +0 -86
  151. package/dist/components/Snackbar/snackbar.types.d.ts.map +0 -1
  152. package/dist/components/Snackbar/snackbar.variants.d.ts +0 -153
  153. package/dist/components/Snackbar/snackbar.variants.d.ts.map +0 -1
  154. package/dist/components/Switch/Switch.d.ts +0 -15
  155. package/dist/components/Switch/Switch.d.ts.map +0 -1
  156. package/dist/components/Switch/switch.types.d.ts +0 -47
  157. package/dist/components/Switch/switch.types.d.ts.map +0 -1
  158. package/dist/components/Switch/switch.variants.d.ts +0 -105
  159. package/dist/components/Switch/switch.variants.d.ts.map +0 -1
  160. package/dist/components/TextField/TextField.d.ts +0 -25
  161. package/dist/components/TextField/TextField.d.ts.map +0 -1
  162. package/dist/components/TextField/textField.types.d.ts +0 -72
  163. package/dist/components/TextField/textField.types.d.ts.map +0 -1
  164. package/dist/components/TextField/textField.variants.d.ts +0 -160
  165. package/dist/components/TextField/textField.variants.d.ts.map +0 -1
  166. package/dist/components/Textarea/Textarea.d.ts +0 -16
  167. package/dist/components/Textarea/Textarea.d.ts.map +0 -1
  168. package/dist/components/Textarea/textarea.types.d.ts +0 -63
  169. package/dist/components/Textarea/textarea.types.d.ts.map +0 -1
  170. package/dist/components/Textarea/textarea.variants.d.ts +0 -197
  171. package/dist/components/Textarea/textarea.variants.d.ts.map +0 -1
  172. package/dist/components/TopBar/TopBar.d.ts +0 -33
  173. package/dist/components/TopBar/TopBar.d.ts.map +0 -1
  174. package/dist/components/TopBar/topBar.types.d.ts +0 -49
  175. package/dist/components/TopBar/topBar.types.d.ts.map +0 -1
  176. package/dist/components/TopBar/topBar.variants.d.ts +0 -79
  177. package/dist/components/TopBar/topBar.variants.d.ts.map +0 -1
  178. package/dist/components/_shared/resolveValidationState.d.ts +0 -42
  179. package/dist/components/_shared/resolveValidationState.d.ts.map +0 -1
  180. package/dist/hooks/useAccessState.d.ts +0 -37
  181. package/dist/hooks/useAccessState.d.ts.map +0 -1
  182. package/dist/index.d.ts.map +0 -1
  183. package/dist/tsconfig.lib.tsbuildinfo +0 -1
  184. package/dist/utils/cn.d.ts +0 -26
  185. package/dist/utils/cn.d.ts.map +0 -1
@@ -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
+ }
@@ -0,0 +1,115 @@
1
+ import { tv, type VariantProps } from 'tailwind-variants';
2
+
3
+ /**
4
+ * `dividerVariants` — visual separator with two rendering modes (line-only
5
+ * vs labeled). Two interconnected TV recipes:
6
+ *
7
+ * • `dividerVariants` — root container (block layout, alignment)
8
+ * • `dividerLineVariants` — the actual line segments (border style,
9
+ * color, orientation)
10
+ *
11
+ * Two recipes (not one with slots) because the labeled mode renders
12
+ * THREE elements (left line · label · right line) while the line-only
13
+ * mode is ONE element. Splitting keeps each TV catalogue small and
14
+ * the type unions narrow.
15
+ *
16
+ * Mental model:
17
+ * • Without `children` → renders an `<hr>` (or a div if vertical)
18
+ * with the line styles applied directly.
19
+ * • With `children` → renders a flex row with two `<span>` line
20
+ * segments either side of the label. The label's flex-shrink keeps
21
+ * it from being squashed; the line segments share the remaining
22
+ * space according to `align`.
23
+ *
24
+ * a11y: the root always carries `role="separator"` + `aria-orientation`
25
+ * — handled in Divider.tsx, not in TV (it's a prop, not a class).
26
+ */
27
+
28
+ /*
29
+ * Root container — only relevant when label is present (flex layout).
30
+ * For line-only, the root IS the line.
31
+ */
32
+ export const dividerVariants = tv({
33
+ base: 'flex items-center',
34
+
35
+ variants: {
36
+ orientation: {
37
+ horizontal: 'w-full',
38
+ vertical: 'h-full flex-col',
39
+ },
40
+
41
+ /*
42
+ * Label alignment along the divider's main axis. Implemented by
43
+ * setting the flex-basis of the two line segments asymmetrically
44
+ * — handled in the variants for `dividerLineVariants` below via
45
+ * compound logic. The root just needs the flex layout.
46
+ */
47
+ align: {
48
+ start: '',
49
+ center: '',
50
+ end: '',
51
+ },
52
+ },
53
+
54
+ defaultVariants: {
55
+ orientation: 'horizontal',
56
+ align: 'center',
57
+ },
58
+ });
59
+
60
+ export type DividerVariants = VariantProps<typeof dividerVariants>;
61
+
62
+ /*
63
+ * Line segment(s).
64
+ *
65
+ * Border styles (solid/dashed/dotted) are applied as `border-t-{style}`
66
+ * for horizontal, `border-l-{style}` for vertical. Color drives the
67
+ * border-* color token to the intent.
68
+ *
69
+ * The `segment` axis distinguishes whether this is a line-only render
70
+ * (full width) vs a labeled-mode segment (flex-1 grows to share space).
71
+ */
72
+ export const dividerLineVariants = tv({
73
+ base: '',
74
+
75
+ variants: {
76
+ orientation: {
77
+ horizontal: 'h-0 border-t',
78
+ vertical: 'w-0 border-l self-stretch',
79
+ },
80
+
81
+ variant: {
82
+ solid: 'border-solid',
83
+ dashed: 'border-dashed',
84
+ dotted: 'border-dotted',
85
+ },
86
+
87
+ color: {
88
+ neutral: 'border-neutral-200 dark:border-neutral-800',
89
+ primary: 'border-primary-300 dark:border-primary-700',
90
+ secondary: 'border-secondary-300 dark:border-secondary-700',
91
+ success: 'border-success-300 dark:border-success-700',
92
+ warning: 'border-warning-300 dark:border-warning-700',
93
+ danger: 'border-danger-300 dark:border-danger-700',
94
+ info: 'border-info-300 dark:border-info-700',
95
+ },
96
+
97
+ /*
98
+ * `segment` controls whether the line spans full width (line-only
99
+ * mode) or grows to fill space (labeled-mode flex segment).
100
+ */
101
+ segment: {
102
+ full: 'w-full',
103
+ grow: 'flex-1',
104
+ },
105
+ },
106
+
107
+ defaultVariants: {
108
+ orientation: 'horizontal',
109
+ variant: 'solid',
110
+ color: 'neutral',
111
+ segment: 'full',
112
+ },
113
+ });
114
+
115
+ export type DividerLineVariants = VariantProps<typeof dividerLineVariants>;