@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.
- package/CHANGELOG.md +108 -0
- package/dist/index.d.ts +1 -82
- package/dist/index.esm.js +1563 -2
- package/dist/src/components/AspectRatio/AspectRatio.d.ts +36 -0
- package/dist/src/components/AspectRatio/AspectRatio.d.ts.map +1 -0
- package/dist/src/components/AspectRatio/aspectRatio.types.d.ts +50 -0
- package/dist/src/components/AspectRatio/aspectRatio.types.d.ts.map +1 -0
- package/dist/src/components/Box/Box.d.ts +36 -0
- package/dist/src/components/Box/Box.d.ts.map +1 -0
- package/dist/src/components/Box/box.types.d.ts +54 -0
- package/dist/src/components/Box/box.types.d.ts.map +1 -0
- package/dist/src/components/Box/box.variants.d.ts +402 -0
- package/dist/src/components/Box/box.variants.d.ts.map +1 -0
- package/dist/src/components/Container/Container.d.ts +35 -0
- package/dist/src/components/Container/Container.d.ts.map +1 -0
- package/dist/src/components/Container/container.types.d.ts +41 -0
- package/dist/src/components/Container/container.types.d.ts.map +1 -0
- package/dist/src/components/Container/container.variants.d.ts +93 -0
- package/dist/src/components/Container/container.variants.d.ts.map +1 -0
- package/dist/src/components/Divider/Divider.d.ts +50 -0
- package/dist/src/components/Divider/Divider.d.ts.map +1 -0
- package/dist/src/components/Divider/divider.types.d.ts +47 -0
- package/dist/src/components/Divider/divider.types.d.ts.map +1 -0
- package/dist/src/components/Divider/divider.variants.d.ts +129 -0
- package/dist/src/components/Divider/divider.variants.d.ts.map +1 -0
- package/dist/src/components/Grid/Grid.d.ts +32 -0
- package/dist/src/components/Grid/Grid.d.ts.map +1 -0
- package/dist/src/components/Grid/grid.types.d.ts +103 -0
- package/dist/src/components/Grid/grid.types.d.ts.map +1 -0
- package/dist/src/components/Grid/grid.variants.d.ts +453 -0
- package/dist/src/components/Grid/grid.variants.d.ts.map +1 -0
- package/dist/src/components/Stack/Stack.d.ts +24 -0
- package/dist/src/components/Stack/Stack.d.ts.map +1 -0
- package/dist/src/components/Stack/stack.types.d.ts +61 -0
- package/dist/src/components/Stack/stack.types.d.ts.map +1 -0
- package/dist/src/components/Stack/stack.variants.d.ts +163 -0
- package/dist/src/components/Stack/stack.variants.d.ts.map +1 -0
- package/dist/src/components/Typography/Typography.d.ts +42 -0
- package/dist/src/components/Typography/Typography.d.ts.map +1 -0
- package/dist/src/components/Typography/typography.types.d.ts +61 -0
- package/dist/src/components/Typography/typography.types.d.ts.map +1 -0
- package/dist/src/components/Typography/typography.variants.d.ts +167 -0
- package/dist/src/components/Typography/typography.variants.d.ts.map +1 -0
- package/dist/src/components/VisuallyHidden/VisuallyHidden.d.ts +42 -0
- package/dist/src/components/VisuallyHidden/VisuallyHidden.d.ts.map +1 -0
- package/dist/src/components/VisuallyHidden/visuallyHidden.types.d.ts +42 -0
- package/dist/src/components/VisuallyHidden/visuallyHidden.types.d.ts.map +1 -0
- package/dist/src/index.d.ts +22 -0
- package/dist/src/index.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/components/AspectRatio/AspectRatio.test.tsx +173 -0
- package/src/components/AspectRatio/AspectRatio.tsx +73 -0
- package/src/components/AspectRatio/aspectRatio.types.ts +54 -0
- package/src/components/Box/Box.test.tsx +349 -0
- package/src/components/Box/Box.tsx +83 -0
- package/src/components/Box/box.types.ts +61 -0
- package/src/components/Box/box.variants.ts +211 -0
- package/src/components/Container/Container.test.tsx +209 -0
- package/src/components/Container/Container.tsx +74 -0
- package/src/components/Container/container.types.ts +45 -0
- package/src/components/Container/container.variants.ts +81 -0
- package/src/components/Divider/Divider.test.tsx +241 -0
- package/src/components/Divider/Divider.tsx +140 -0
- package/src/components/Divider/divider.types.ts +52 -0
- package/src/components/Divider/divider.variants.ts +115 -0
- package/src/components/Grid/Grid.test.tsx +321 -0
- package/src/components/Grid/Grid.tsx +148 -0
- package/src/components/Grid/grid.types.ts +112 -0
- package/src/components/Grid/grid.variants.ts +133 -0
- package/src/components/Stack/Stack.test.tsx +309 -0
- package/src/components/Stack/Stack.tsx +115 -0
- package/src/components/Stack/stack.types.ts +68 -0
- package/src/components/Stack/stack.variants.ts +80 -0
- package/src/components/Typography/Typography.test.tsx +230 -0
- package/src/components/Typography/Typography.tsx +112 -0
- package/src/components/Typography/typography.types.ts +65 -0
- package/src/components/Typography/typography.variants.ts +113 -0
- package/src/components/VisuallyHidden/VisuallyHidden.test.tsx +86 -0
- package/src/components/VisuallyHidden/VisuallyHidden.tsx +59 -0
- package/src/components/VisuallyHidden/visuallyHidden.types.ts +45 -0
- package/src/index.ts +49 -0
- package/dist/components/AppShell/AppShell.d.ts +0 -32
- package/dist/components/AppShell/AppShell.d.ts.map +0 -1
- package/dist/components/AppShell/appShell.types.d.ts +0 -62
- package/dist/components/AppShell/appShell.types.d.ts.map +0 -1
- package/dist/components/AppShell/appShell.variants.d.ts +0 -65
- package/dist/components/AppShell/appShell.variants.d.ts.map +0 -1
- package/dist/components/Autocomplete/Autocomplete.d.ts +0 -32
- package/dist/components/Autocomplete/Autocomplete.d.ts.map +0 -1
- package/dist/components/Autocomplete/autocomplete.types.d.ts +0 -214
- package/dist/components/Autocomplete/autocomplete.types.d.ts.map +0 -1
- package/dist/components/Autocomplete/autocomplete.variants.d.ts +0 -214
- package/dist/components/Autocomplete/autocomplete.variants.d.ts.map +0 -1
- package/dist/components/Breadcrumbs/Breadcrumbs.d.ts +0 -23
- package/dist/components/Breadcrumbs/Breadcrumbs.d.ts.map +0 -1
- package/dist/components/Breadcrumbs/breadcrumbs.types.d.ts +0 -98
- package/dist/components/Breadcrumbs/breadcrumbs.types.d.ts.map +0 -1
- package/dist/components/Breadcrumbs/breadcrumbs.variants.d.ts +0 -85
- package/dist/components/Breadcrumbs/breadcrumbs.variants.d.ts.map +0 -1
- package/dist/components/Button/Button.d.ts +0 -44
- package/dist/components/Button/Button.d.ts.map +0 -1
- package/dist/components/Button/button.types.d.ts +0 -66
- package/dist/components/Button/button.types.d.ts.map +0 -1
- package/dist/components/Button/button.variants.d.ts +0 -104
- package/dist/components/Button/button.variants.d.ts.map +0 -1
- package/dist/components/Checkbox/Checkbox.d.ts +0 -31
- package/dist/components/Checkbox/Checkbox.d.ts.map +0 -1
- package/dist/components/Checkbox/checkbox.types.d.ts +0 -86
- package/dist/components/Checkbox/checkbox.types.d.ts.map +0 -1
- package/dist/components/Checkbox/checkbox.variants.d.ts +0 -109
- package/dist/components/Checkbox/checkbox.variants.d.ts.map +0 -1
- package/dist/components/ConfirmDialog/ConfirmDialog.d.ts +0 -38
- package/dist/components/ConfirmDialog/ConfirmDialog.d.ts.map +0 -1
- package/dist/components/ConfirmDialog/confirmDialog.types.d.ts +0 -80
- package/dist/components/ConfirmDialog/confirmDialog.types.d.ts.map +0 -1
- package/dist/components/ConfirmDialog/confirmDialog.variants.d.ts +0 -90
- package/dist/components/ConfirmDialog/confirmDialog.variants.d.ts.map +0 -1
- package/dist/components/DateTimePicker/DateTimePicker.d.ts +0 -49
- package/dist/components/DateTimePicker/DateTimePicker.d.ts.map +0 -1
- package/dist/components/DateTimePicker/dateTimePicker.types.d.ts +0 -95
- package/dist/components/DateTimePicker/dateTimePicker.types.d.ts.map +0 -1
- package/dist/components/DateTimePicker/dateTimePicker.variants.d.ts +0 -165
- package/dist/components/DateTimePicker/dateTimePicker.variants.d.ts.map +0 -1
- package/dist/components/LeftNav/LeftNav.d.ts +0 -34
- package/dist/components/LeftNav/LeftNav.d.ts.map +0 -1
- package/dist/components/LeftNav/leftNav.types.d.ts +0 -136
- package/dist/components/LeftNav/leftNav.types.d.ts.map +0 -1
- package/dist/components/LeftNav/leftNav.variants.d.ts +0 -143
- package/dist/components/LeftNav/leftNav.variants.d.ts.map +0 -1
- package/dist/components/NumberField/NumberField.d.ts +0 -18
- package/dist/components/NumberField/NumberField.d.ts.map +0 -1
- package/dist/components/NumberField/numberField.types.d.ts +0 -79
- package/dist/components/NumberField/numberField.types.d.ts.map +0 -1
- package/dist/components/NumberField/numberField.variants.d.ts +0 -169
- package/dist/components/NumberField/numberField.variants.d.ts.map +0 -1
- package/dist/components/OTPField/OTPField.d.ts +0 -25
- package/dist/components/OTPField/OTPField.d.ts.map +0 -1
- package/dist/components/OTPField/otpField.types.d.ts +0 -74
- package/dist/components/OTPField/otpField.types.d.ts.map +0 -1
- package/dist/components/OTPField/otpField.variants.d.ts +0 -105
- package/dist/components/OTPField/otpField.variants.d.ts.map +0 -1
- package/dist/components/RadioGroup/RadioGroup.d.ts +0 -35
- package/dist/components/RadioGroup/RadioGroup.d.ts.map +0 -1
- package/dist/components/RadioGroup/radioGroup.types.d.ts +0 -103
- package/dist/components/RadioGroup/radioGroup.types.d.ts.map +0 -1
- package/dist/components/RadioGroup/radioGroup.variants.d.ts +0 -166
- package/dist/components/RadioGroup/radioGroup.variants.d.ts.map +0 -1
- package/dist/components/Snackbar/Snackbar.d.ts +0 -37
- package/dist/components/Snackbar/Snackbar.d.ts.map +0 -1
- package/dist/components/Snackbar/snackbar.types.d.ts +0 -86
- package/dist/components/Snackbar/snackbar.types.d.ts.map +0 -1
- package/dist/components/Snackbar/snackbar.variants.d.ts +0 -153
- package/dist/components/Snackbar/snackbar.variants.d.ts.map +0 -1
- package/dist/components/Switch/Switch.d.ts +0 -15
- package/dist/components/Switch/Switch.d.ts.map +0 -1
- package/dist/components/Switch/switch.types.d.ts +0 -47
- package/dist/components/Switch/switch.types.d.ts.map +0 -1
- package/dist/components/Switch/switch.variants.d.ts +0 -105
- package/dist/components/Switch/switch.variants.d.ts.map +0 -1
- package/dist/components/TextField/TextField.d.ts +0 -25
- package/dist/components/TextField/TextField.d.ts.map +0 -1
- package/dist/components/TextField/textField.types.d.ts +0 -72
- package/dist/components/TextField/textField.types.d.ts.map +0 -1
- package/dist/components/TextField/textField.variants.d.ts +0 -160
- package/dist/components/TextField/textField.variants.d.ts.map +0 -1
- package/dist/components/Textarea/Textarea.d.ts +0 -16
- package/dist/components/Textarea/Textarea.d.ts.map +0 -1
- package/dist/components/Textarea/textarea.types.d.ts +0 -63
- package/dist/components/Textarea/textarea.types.d.ts.map +0 -1
- package/dist/components/Textarea/textarea.variants.d.ts +0 -197
- package/dist/components/Textarea/textarea.variants.d.ts.map +0 -1
- package/dist/components/TopBar/TopBar.d.ts +0 -33
- package/dist/components/TopBar/TopBar.d.ts.map +0 -1
- package/dist/components/TopBar/topBar.types.d.ts +0 -49
- package/dist/components/TopBar/topBar.types.d.ts.map +0 -1
- package/dist/components/TopBar/topBar.variants.d.ts +0 -79
- package/dist/components/TopBar/topBar.variants.d.ts.map +0 -1
- package/dist/components/_shared/resolveValidationState.d.ts +0 -42
- package/dist/components/_shared/resolveValidationState.d.ts.map +0 -1
- package/dist/hooks/useAccessState.d.ts +0 -37
- package/dist/hooks/useAccessState.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/tsconfig.lib.tsbuildinfo +0 -1
- package/dist/utils/cn.d.ts +0 -26
- 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>;
|