@duro-app/ui 0.16.0 → 0.17.1

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 (82) hide show
  1. package/dist/components/ActionBar/ActionBar.d.ts +3 -1
  2. package/dist/components/ActionBar/ActionBar.d.ts.map +1 -1
  3. package/dist/components/Button/Button.meta.d.ts +3 -0
  4. package/dist/components/Button/Button.meta.d.ts.map +1 -0
  5. package/dist/components/Checkbox/Checkbox.meta.d.ts +3 -0
  6. package/dist/components/Checkbox/Checkbox.meta.d.ts.map +1 -0
  7. package/dist/components/Cluster/Cluster.meta.d.ts +3 -0
  8. package/dist/components/Cluster/Cluster.meta.d.ts.map +1 -0
  9. package/dist/components/Field/Field.d.ts.map +1 -1
  10. package/dist/components/Field/Field.meta.d.ts +3 -0
  11. package/dist/components/Field/Field.meta.d.ts.map +1 -0
  12. package/dist/components/Fieldset/Fieldset.meta.d.ts +3 -0
  13. package/dist/components/Fieldset/Fieldset.meta.d.ts.map +1 -0
  14. package/dist/components/Form/Form.meta.d.ts +3 -0
  15. package/dist/components/Form/Form.meta.d.ts.map +1 -0
  16. package/dist/components/Form/Form.stories.d.ts.map +1 -1
  17. package/dist/components/Grid/Grid.meta.d.ts +3 -0
  18. package/dist/components/Grid/Grid.meta.d.ts.map +1 -0
  19. package/dist/components/Inline/Inline.meta.d.ts +3 -0
  20. package/dist/components/Inline/Inline.meta.d.ts.map +1 -0
  21. package/dist/components/Input/Input.meta.d.ts +3 -0
  22. package/dist/components/Input/Input.meta.d.ts.map +1 -0
  23. package/dist/components/LinkButton/LinkButton.meta.d.ts +3 -0
  24. package/dist/components/LinkButton/LinkButton.meta.d.ts.map +1 -0
  25. package/dist/components/Menu/Menu.meta.d.ts +3 -0
  26. package/dist/components/Menu/Menu.meta.d.ts.map +1 -0
  27. package/dist/components/PageShell/PageShell.meta.d.ts +3 -0
  28. package/dist/components/PageShell/PageShell.meta.d.ts.map +1 -0
  29. package/dist/components/ScrollArea/ScrollArea.meta.d.ts +3 -0
  30. package/dist/components/ScrollArea/ScrollArea.meta.d.ts.map +1 -0
  31. package/dist/components/Select/Select.meta.d.ts +3 -0
  32. package/dist/components/Select/Select.meta.d.ts.map +1 -0
  33. package/dist/components/SideNav/SideNav.meta.d.ts +3 -0
  34. package/dist/components/SideNav/SideNav.meta.d.ts.map +1 -0
  35. package/dist/components/Stack/Stack.meta.d.ts +3 -0
  36. package/dist/components/Stack/Stack.meta.d.ts.map +1 -0
  37. package/dist/components/Switch/Switch.meta.d.ts +3 -0
  38. package/dist/components/Switch/Switch.meta.d.ts.map +1 -0
  39. package/dist/components/Table/Table.meta.d.ts +3 -0
  40. package/dist/components/Table/Table.meta.d.ts.map +1 -0
  41. package/dist/components/Tabs/Tabs.meta.d.ts +3 -0
  42. package/dist/components/Tabs/Tabs.meta.d.ts.map +1 -0
  43. package/dist/components/Textarea/Textarea.meta.d.ts +3 -0
  44. package/dist/components/Textarea/Textarea.meta.d.ts.map +1 -0
  45. package/dist/components/Toggle/Toggle.meta.d.ts +3 -0
  46. package/dist/components/Toggle/Toggle.meta.d.ts.map +1 -0
  47. package/dist/components/ToggleGroup/ToggleGroup.meta.d.ts +3 -0
  48. package/dist/components/ToggleGroup/ToggleGroup.meta.d.ts.map +1 -0
  49. package/dist/components/Tooltip/Tooltip.meta.d.ts +3 -0
  50. package/dist/components/Tooltip/Tooltip.meta.d.ts.map +1 -0
  51. package/dist/components/component-meta.d.ts +15 -0
  52. package/dist/components/component-meta.d.ts.map +1 -0
  53. package/dist/index.js +163 -167
  54. package/dist/index.js.map +1 -1
  55. package/package.json +2 -2
  56. package/src/components/ActionBar/ActionBar.tsx +20 -15
  57. package/src/components/Button/Button.meta.ts +23 -0
  58. package/src/components/Checkbox/Checkbox.meta.ts +21 -0
  59. package/src/components/Cluster/Cluster.meta.ts +25 -0
  60. package/src/components/Field/Field.meta.ts +37 -0
  61. package/src/components/Field/Field.tsx +10 -1
  62. package/src/components/Fieldset/Fieldset.meta.ts +38 -0
  63. package/src/components/Form/Form.meta.ts +38 -0
  64. package/src/components/Form/Form.stories.tsx +10 -19
  65. package/src/components/Grid/Grid.meta.ts +30 -0
  66. package/src/components/Inline/Inline.meta.ts +23 -0
  67. package/src/components/Input/Input.meta.ts +23 -0
  68. package/src/components/LinkButton/LinkButton.meta.ts +24 -0
  69. package/src/components/Menu/Menu.meta.ts +33 -0
  70. package/src/components/PageShell/PageShell.meta.ts +24 -0
  71. package/src/components/ScrollArea/ScrollArea.meta.ts +27 -0
  72. package/src/components/Select/Select.meta.ts +34 -0
  73. package/src/components/SideNav/SideNav.meta.ts +31 -0
  74. package/src/components/Stack/Stack.meta.ts +22 -0
  75. package/src/components/Switch/Switch.meta.ts +25 -0
  76. package/src/components/Table/Table.meta.ts +36 -0
  77. package/src/components/Tabs/Tabs.meta.ts +31 -0
  78. package/src/components/Textarea/Textarea.meta.ts +22 -0
  79. package/src/components/Toggle/Toggle.meta.ts +30 -0
  80. package/src/components/ToggleGroup/ToggleGroup.meta.ts +30 -0
  81. package/src/components/Tooltip/Tooltip.meta.ts +22 -0
  82. package/src/components/component-meta.ts +8 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@duro-app/ui",
3
- "version": "0.16.0",
3
+ "version": "0.17.1",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -52,7 +52,7 @@
52
52
  }
53
53
  },
54
54
  "dependencies": {
55
- "@duro-app/tokens": "^0.16.0"
55
+ "@duro-app/tokens": "^0.17.1"
56
56
  },
57
57
  "devDependencies": {
58
58
  "@babel/preset-typescript": "^7.28.0",
@@ -13,6 +13,8 @@ export interface ActionBarProps {
13
13
  isEmphasized?: boolean
14
14
  /** Called when the clear/close button is pressed. */
15
15
  onClearSelection: () => void
16
+ /** Whether the bar can be dismissed. When false, the close button is hidden. @default true */
17
+ dismissible?: boolean
16
18
  /** Action buttons to display. */
17
19
  children: ReactNode
18
20
  }
@@ -22,6 +24,7 @@ function ActionBarInner({
22
24
  selectedLabel,
23
25
  isEmphasized = false,
24
26
  onClearSelection,
27
+ dismissible = true,
25
28
  children,
26
29
  }: ActionBarProps) {
27
30
  const container = useOverlayContainer()
@@ -42,21 +45,23 @@ function ActionBarInner({
42
45
  </html.span>
43
46
  <html.div style={[styles.separator, isEmphasized && styles.separatorEmphasized]} />
44
47
  <html.div style={styles.actions}>{children}</html.div>
45
- <html.button
46
- type="button"
47
- aria-label="Clear selection"
48
- onClick={onClearSelection}
49
- style={[styles.closeButton, isEmphasized && styles.closeButtonEmphasized]}
50
- >
51
- <svg width={10} height={10} viewBox="0 0 10 10" fill="none" aria-hidden="true">
52
- <path
53
- d="M1 1l8 8M9 1l-8 8"
54
- stroke="currentColor"
55
- strokeWidth={1.5}
56
- strokeLinecap="round"
57
- />
58
- </svg>
59
- </html.button>
48
+ {dismissible && (
49
+ <html.button
50
+ type="button"
51
+ aria-label="Clear selection"
52
+ onClick={onClearSelection}
53
+ style={[styles.closeButton, isEmphasized && styles.closeButtonEmphasized]}
54
+ >
55
+ <svg width={10} height={10} viewBox="0 0 10 10" fill="none" aria-hidden="true">
56
+ <path
57
+ d="M1 1l8 8M9 1l-8 8"
58
+ stroke="currentColor"
59
+ strokeWidth={1.5}
60
+ strokeLinecap="round"
61
+ />
62
+ </svg>
63
+ </html.button>
64
+ )}
60
65
  </html.div>
61
66
  )
62
67
 
@@ -0,0 +1,23 @@
1
+ import type {ComponentMeta} from '../component-meta'
2
+
3
+ export const meta: ComponentMeta = {
4
+ description:
5
+ 'Standard interactive button. For navigation (links styled as buttons), use LinkButton instead.',
6
+ whenToUse: [
7
+ 'Triggering actions: submit, save, delete, open dialog',
8
+ 'Primary and secondary actions in forms and toolbars',
9
+ ],
10
+ whenNotToUse: [
11
+ 'Navigating to a URL — use LinkButton (renders <a>)',
12
+ 'Toggling state — use Toggle',
13
+ ],
14
+ relatedTo: [
15
+ {component: 'LinkButton', relationship: 'LinkButton for navigation; Button for actions'},
16
+ {component: 'Toggle', relationship: 'Toggle for pressed/unpressed state'},
17
+ ],
18
+ example: `<Inline gap="sm">
19
+ <Button variant="primary" type="submit">Save</Button>
20
+ <Button variant="secondary">Cancel</Button>
21
+ <Button variant="danger">Delete</Button>
22
+ </Inline>`,
23
+ }
@@ -0,0 +1,21 @@
1
+ import type {ComponentMeta} from '../component-meta'
2
+
3
+ export const meta: ComponentMeta = {
4
+ description:
5
+ 'Checkbox input with optional visible label. Supports controlled and uncontrolled modes.',
6
+ whenToUse: [
7
+ 'Boolean opt-in/opt-out (terms acceptance, feature toggle)',
8
+ 'Multi-select from a list (inside a Fieldset)',
9
+ ],
10
+ whenNotToUse: [
11
+ 'Binary toggle in settings — prefer Switch for on/off semantics',
12
+ 'Selecting one of several options — use Select or ToggleGroup',
13
+ ],
14
+ relatedTo: [
15
+ {component: 'Switch', relationship: 'Switch for on/off settings; Checkbox for opt-in/selection'},
16
+ {component: 'Toggle', relationship: 'Toggle for toolbar actions; Checkbox for form input'},
17
+ ],
18
+ example: `<Checkbox name="terms" onChange={(e) => console.log(e.target.checked)}>
19
+ I agree to the terms and conditions
20
+ </Checkbox>`,
21
+ }
@@ -0,0 +1,25 @@
1
+ import type {ComponentMeta} from '../component-meta'
2
+
3
+ export const meta: ComponentMeta = {
4
+ description:
5
+ 'Horizontal flex layout that WRAPS to the next line when items overflow.',
6
+ whenToUse: [
7
+ 'Tags, badges, or chips that should wrap when they exceed the container width',
8
+ 'Any set of inline elements with unknown/dynamic count',
9
+ 'Filter pills, keyword lists, avatar groups',
10
+ ],
11
+ whenNotToUse: [
12
+ 'Items that must stay on one line — use Inline',
13
+ 'Vertical stacking — use Stack',
14
+ ],
15
+ relatedTo: [
16
+ {component: 'Inline', relationship: 'Same but does NOT wrap (nowrap)'},
17
+ {component: 'Stack', relationship: 'Vertical equivalent'},
18
+ ],
19
+ example: `<Cluster gap="xs">
20
+ <Badge>React</Badge>
21
+ <Badge>TypeScript</Badge>
22
+ <Badge>Design Systems</Badge>
23
+ <Badge>Accessibility</Badge>
24
+ </Cluster>`,
25
+ }
@@ -0,0 +1,37 @@
1
+ import type {ComponentMeta} from '../component-meta'
2
+
3
+ export const meta: ComponentMeta = {
4
+ description:
5
+ 'Compound form field with label, description, and error display. Inside Form, auto-binds to react-hook-form via name prop. Works standalone with manual invalid prop.',
6
+ whenToUse: [
7
+ 'Any form input that needs a label and/or error message',
8
+ 'Standalone labeled input (without Form) with manual error display',
9
+ ],
10
+ whenNotToUse: [
11
+ 'Bare input without label — use Input directly (rare)',
12
+ ],
13
+ anatomy: {
14
+ required: ['Root'],
15
+ optional: ['Label', 'Description', 'Error'],
16
+ },
17
+ relatedTo: [
18
+ {component: 'Form', relationship: 'Field.Root auto-binds when name matches schema key'},
19
+ {component: 'Input', relationship: 'Place Input inside Field.Root'},
20
+ {component: 'Textarea', relationship: 'Place Textarea inside Field.Root'},
21
+ {component: 'Select', relationship: 'Place Select inside Field.Root'},
22
+ ],
23
+ example: `// Inside Form (auto-binds validation)
24
+ <Field.Root name="email">
25
+ <Field.Label>Email</Field.Label>
26
+ <Input type="email" placeholder="you@example.com" />
27
+ <Field.Description>We'll never share your email.</Field.Description>
28
+ <Field.Error />
29
+ </Field.Root>
30
+
31
+ // Standalone (manual error)
32
+ <Field.Root invalid>
33
+ <Field.Label>Email</Field.Label>
34
+ <Input variant="error" />
35
+ <Field.Error>This email is already taken.</Field.Error>
36
+ </Field.Root>`,
37
+ }
@@ -50,7 +50,16 @@ function ControlledRoot({
50
50
  },
51
51
  errorMessage: fieldState.error?.message,
52
52
  }),
53
- [id, invalid, field.value, field.onChange, field.onBlur, field.ref, field.name, fieldState.error],
53
+ [
54
+ id,
55
+ invalid,
56
+ field.value,
57
+ field.onChange,
58
+ field.onBlur,
59
+ field.ref,
60
+ field.name,
61
+ fieldState.error,
62
+ ],
54
63
  )
55
64
 
56
65
  return (
@@ -0,0 +1,38 @@
1
+ import type {ComponentMeta} from '../component-meta'
2
+
3
+ export const meta: ComponentMeta = {
4
+ description:
5
+ 'Groups related form controls with consistent gap spacing and an optional legend. Supports disabled cascade.',
6
+ whenToUse: [
7
+ 'Grouping related fields within a form (e.g., "Personal Info", "Address")',
8
+ 'Applying consistent vertical gap between form fields',
9
+ ],
10
+ whenNotToUse: [
11
+ 'Non-form content grouping — use Stack or Card',
12
+ ],
13
+ anatomy: {
14
+ required: ['Root'],
15
+ optional: ['Legend'],
16
+ },
17
+ relatedTo: [
18
+ {component: 'Form', relationship: 'Fieldset.Root goes inside Form'},
19
+ {component: 'Field', relationship: 'Field.Root goes inside Fieldset.Root'},
20
+ {component: 'Stack', relationship: 'Similar vertical spacing, but Fieldset has form semantics'},
21
+ ],
22
+ example: `<Form schema={schema} defaultValues={defaults} onSubmit={onSubmit}>
23
+ <Fieldset.Root gap="md">
24
+ <Fieldset.Legend>Account Details</Fieldset.Legend>
25
+ <Field.Root name="email">
26
+ <Field.Label>Email</Field.Label>
27
+ <Input type="email" />
28
+ <Field.Error />
29
+ </Field.Root>
30
+ <Field.Root name="password">
31
+ <Field.Label>Password</Field.Label>
32
+ <Input type="password" />
33
+ <Field.Error />
34
+ </Field.Root>
35
+ <Button type="submit">Create account</Button>
36
+ </Fieldset.Root>
37
+ </Form>`,
38
+ }
@@ -0,0 +1,38 @@
1
+ import type {ComponentMeta} from '../component-meta'
2
+
3
+ export const meta: ComponentMeta = {
4
+ description:
5
+ 'Form wrapper with Effect Schema validation and react-hook-form integration. Provides FormContext to child Field components.',
6
+ whenToUse: [
7
+ 'Any form that needs validation',
8
+ 'Multi-field forms with submit handling',
9
+ ],
10
+ whenNotToUse: [
11
+ 'Single standalone field without validation — use Field.Root directly',
12
+ 'Non-form interactive UIs — use individual components',
13
+ ],
14
+ relatedTo: [
15
+ {component: 'Field', relationship: 'Field.Root auto-binds to Form via name prop'},
16
+ {component: 'Fieldset', relationship: 'Groups fields visually within a Form'},
17
+ ],
18
+ example: `import {Schema} from 'effect'
19
+
20
+ const MySchema = Schema.Struct({
21
+ email: Schema.String.pipe(
22
+ Schema.pattern(/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/, {message: () => 'Invalid email'}),
23
+ ),
24
+ })
25
+
26
+ <Form schema={MySchema} defaultValues={{email: ''}} onSubmit={handleSubmit}>
27
+ {({formState}) => (
28
+ <Fieldset.Root gap="md">
29
+ <Field.Root name="email">
30
+ <Field.Label>Email</Field.Label>
31
+ <Input type="email" />
32
+ <Field.Error />
33
+ </Field.Root>
34
+ <Button type="submit" disabled={!formState.isValid}>Submit</Button>
35
+ </Fieldset.Root>
36
+ )}
37
+ </Form>`,
38
+ }
@@ -93,11 +93,7 @@ export const ValidationOnBlur: Story = {
93
93
  name: 'Validation — on blur',
94
94
  render: () => (
95
95
  <html.div style={wrapStyles.wrap}>
96
- <Form
97
- schema={LoginSchema}
98
- defaultValues={{username: '', password: ''}}
99
- onSubmit={fn()}
100
- >
96
+ <Form schema={LoginSchema} defaultValues={{username: '', password: ''}} onSubmit={fn()}>
101
97
  <Fieldset.Root gap="md">
102
98
  <Field.Root name="username">
103
99
  <Field.Label>Username</Field.Label>
@@ -131,10 +127,13 @@ export const ValidationOnBlur: Story = {
131
127
  await expect(alert).toHaveTextContent('Username must be at least 3 characters')
132
128
  })
133
129
 
134
- await step('Fix username by typing more — error clears (reValidateMode: onChange)', async () => {
135
- await userEvent.type(usernameInput, 'c') // now "abc"
136
- expect(canvas.queryByText('Username must be at least 3 characters')).not.toBeInTheDocument()
137
- })
130
+ await step(
131
+ 'Fix username by typing more — error clears (reValidateMode: onChange)',
132
+ async () => {
133
+ await userEvent.type(usernameInput, 'c') // now "abc"
134
+ expect(canvas.queryByText('Username must be at least 3 characters')).not.toBeInTheDocument()
135
+ },
136
+ )
138
137
 
139
138
  await step('Blur empty password — error appears', async () => {
140
139
  await userEvent.click(passwordInput)
@@ -150,11 +149,7 @@ export const WithTextarea: Story = {
150
149
  name: 'With Textarea',
151
150
  render: () => (
152
151
  <html.div style={wrapStyles.wrap}>
153
- <Form
154
- schema={FeedbackSchema}
155
- defaultValues={{email: '', message: ''}}
156
- onSubmit={fn()}
157
- >
152
+ <Form schema={FeedbackSchema} defaultValues={{email: '', message: ''}} onSubmit={fn()}>
158
153
  {({formState}) => (
159
154
  <Fieldset.Root gap="md">
160
155
  <Field.Root name="email">
@@ -193,11 +188,7 @@ export const SuccessfulSubmit: Story = {
193
188
  const onSubmit = fn()
194
189
  return (
195
190
  <html.div style={wrapStyles.wrap}>
196
- <Form
197
- schema={LoginSchema}
198
- defaultValues={{username: '', password: ''}}
199
- onSubmit={onSubmit}
200
- >
191
+ <Form schema={LoginSchema} defaultValues={{username: '', password: ''}} onSubmit={onSubmit}>
201
192
  <Fieldset.Root gap="md">
202
193
  <Field.Root name="username">
203
194
  <Field.Label>Username</Field.Label>
@@ -0,0 +1,30 @@
1
+ import type {ComponentMeta} from '../component-meta'
2
+
3
+ export const meta: ComponentMeta = {
4
+ description:
5
+ 'CSS grid layout. Supports fixed column count (1-6) or responsive auto-fit via minColumnWidth.',
6
+ whenToUse: [
7
+ 'Card grids, dashboard layouts, multi-column forms',
8
+ 'Responsive layouts that should auto-adjust column count',
9
+ ],
10
+ whenNotToUse: [
11
+ 'Single-column vertical layout — use Stack',
12
+ 'Simple horizontal row — use Inline or Cluster',
13
+ ],
14
+ relatedTo: [
15
+ {component: 'Stack', relationship: 'Single-column vertical layout'},
16
+ ],
17
+ example: `// Responsive: columns auto-fit based on min width
18
+ <Grid minColumnWidth="280px" gap="md">
19
+ <Card>A</Card>
20
+ <Card>B</Card>
21
+ <Card>C</Card>
22
+ </Grid>
23
+
24
+ // Fixed: always 3 columns
25
+ <Grid columns={3} gap="md">
26
+ <Card>A</Card>
27
+ <Card>B</Card>
28
+ <Card>C</Card>
29
+ </Grid>`,
30
+ }
@@ -0,0 +1,23 @@
1
+ import type {ComponentMeta} from '../component-meta'
2
+
3
+ export const meta: ComponentMeta = {
4
+ description:
5
+ 'Horizontal flex layout with NO wrapping. Items stay on one line and may overflow.',
6
+ whenToUse: [
7
+ 'Toolbar or button group that must stay on one row',
8
+ 'Horizontal alignment of a known, small number of items',
9
+ 'Icon + text pairs, breadcrumbs, action rows',
10
+ ],
11
+ whenNotToUse: [
12
+ 'Items that should wrap to the next line — use Cluster',
13
+ 'Vertical stacking — use Stack',
14
+ ],
15
+ relatedTo: [
16
+ {component: 'Cluster', relationship: 'Same but wraps to next line'},
17
+ {component: 'Stack', relationship: 'Vertical equivalent'},
18
+ ],
19
+ example: `<Inline gap="sm" align="center">
20
+ <Button>Save</Button>
21
+ <Button variant="secondary">Cancel</Button>
22
+ </Inline>`,
23
+ }
@@ -0,0 +1,23 @@
1
+ import type {ComponentMeta} from '../component-meta'
2
+
3
+ export const meta: ComponentMeta = {
4
+ description:
5
+ 'Text input with automatic Field/Form integration. Variant auto-switches to error when inside an invalid Field.',
6
+ whenToUse: [
7
+ 'Single-line text entry (text, email, password, url, tel, number, search)',
8
+ ],
9
+ whenNotToUse: [
10
+ 'Multi-line text — use Textarea',
11
+ 'Picking from predefined options — use Select',
12
+ ],
13
+ relatedTo: [
14
+ {component: 'Field', relationship: 'Wrap in Field.Root for label + error'},
15
+ {component: 'InputGroup', relationship: 'Wrap in InputGroup.Root for prefix/suffix addons'},
16
+ {component: 'Textarea', relationship: 'Multi-line variant'},
17
+ ],
18
+ example: `<Field.Root name="email">
19
+ <Field.Label>Email</Field.Label>
20
+ <Input type="email" placeholder="you@example.com" autoComplete="email" />
21
+ <Field.Error />
22
+ </Field.Root>`,
23
+ }
@@ -0,0 +1,24 @@
1
+ import type {ComponentMeta} from '../component-meta'
2
+
3
+ export const meta: ComponentMeta = {
4
+ description:
5
+ 'Button-styled hyperlink. Renders an <a> tag, not a <button>. Use for navigation, not actions.',
6
+ whenToUse: [
7
+ 'Navigation that should look like a button (e.g., "Get started", "View all")',
8
+ 'Links to external pages with button styling',
9
+ ],
10
+ whenNotToUse: [
11
+ 'Triggering in-page actions — use Button',
12
+ 'Plain text links — use a styled <a> tag',
13
+ ],
14
+ relatedTo: [
15
+ {component: 'Button', relationship: 'Button for actions; LinkButton for navigation'},
16
+ ],
17
+ example: `<LinkButton href="/signup" variant="primary">
18
+ Get started
19
+ </LinkButton>
20
+
21
+ <LinkButton href="https://docs.example.com" target="_blank" variant="secondary">
22
+ Read the docs
23
+ </LinkButton>`,
24
+ }
@@ -0,0 +1,33 @@
1
+ import type {ComponentMeta} from '../component-meta'
2
+
3
+ export const meta: ComponentMeta = {
4
+ description:
5
+ 'Dropdown action menu. Triggers actions (not value selection). Compound component — Root is required (throws without it).',
6
+ whenToUse: [
7
+ 'Context menu or "more actions" dropdown',
8
+ 'Navigation links in a dropdown',
9
+ 'Overflow menu for toolbar actions',
10
+ ],
11
+ whenNotToUse: [
12
+ 'Picking a value — use Select',
13
+ 'Primary navigation — use SideNav or Tabs',
14
+ ],
15
+ anatomy: {
16
+ required: ['Root', 'Trigger', 'Popup', 'Item'],
17
+ optional: ['LinkItem'],
18
+ },
19
+ relatedTo: [
20
+ {component: 'Select', relationship: 'Select picks values; Menu triggers actions'},
21
+ {component: 'Button', relationship: 'Often used as Menu.Trigger content'},
22
+ ],
23
+ example: `<Menu.Root>
24
+ <Menu.Trigger>
25
+ <Button variant="secondary">Actions</Button>
26
+ </Menu.Trigger>
27
+ <Menu.Popup>
28
+ <Menu.Item onClick={() => console.log('edit')}>Edit</Menu.Item>
29
+ <Menu.Item onClick={() => console.log('duplicate')}>Duplicate</Menu.Item>
30
+ <Menu.LinkItem href="/settings">Settings</Menu.LinkItem>
31
+ </Menu.Popup>
32
+ </Menu.Root>`,
33
+ }
@@ -0,0 +1,24 @@
1
+ import type {ComponentMeta} from '../component-meta'
2
+
3
+ export const meta: ComponentMeta = {
4
+ description:
5
+ 'Page-level layout wrapper. Centers content with max-width, padding, and an optional sticky header.',
6
+ whenToUse: [
7
+ 'Top-level page layout',
8
+ 'Any full-page view that needs centered content with consistent padding',
9
+ ],
10
+ whenNotToUse: [
11
+ 'Nested section layouts — use Stack or Card',
12
+ 'Sidebar layouts — combine with SideNav manually',
13
+ ],
14
+ relatedTo: [
15
+ {component: 'Stack', relationship: 'Use Stack for content within PageShell'},
16
+ {component: 'SideNav', relationship: 'Often placed alongside PageShell'},
17
+ ],
18
+ example: `<PageShell maxWidth="lg" padding="md" header={<Heading level={1}>Dashboard</Heading>}>
19
+ <Stack gap="lg">
20
+ <Card>Content section 1</Card>
21
+ <Card>Content section 2</Card>
22
+ </Stack>
23
+ </PageShell>`,
24
+ }
@@ -0,0 +1,27 @@
1
+ import type {ComponentMeta} from '../component-meta'
2
+
3
+ export const meta: ComponentMeta = {
4
+ description:
5
+ 'Custom scrollbar region with draggable thumb. Supports vertical and horizontal scrolling. Compound component — Root is required.',
6
+ whenToUse: [
7
+ 'Custom-styled scrollbars for content overflow areas',
8
+ 'Chat logs, code blocks, or long lists with constrained height',
9
+ ],
10
+ whenNotToUse: [
11
+ 'Simple page-level scrolling — browser scroll is fine',
12
+ 'Very short content that never overflows',
13
+ ],
14
+ anatomy: {
15
+ required: ['Root', 'Viewport', 'Content', 'Scrollbar', 'Thumb'],
16
+ },
17
+ example: `<ScrollArea.Root>
18
+ <ScrollArea.Viewport maxHeight={300}>
19
+ <ScrollArea.Content>
20
+ {/* Long content here */}
21
+ </ScrollArea.Content>
22
+ </ScrollArea.Viewport>
23
+ <ScrollArea.Scrollbar orientation="vertical">
24
+ <ScrollArea.Thumb orientation="vertical" />
25
+ </ScrollArea.Scrollbar>
26
+ </ScrollArea.Root>`,
27
+ }
@@ -0,0 +1,34 @@
1
+ import type {ComponentMeta} from '../component-meta'
2
+
3
+ export const meta: ComponentMeta = {
4
+ description:
5
+ 'Dropdown select for choosing one value from a list. Compound component — Root is required (throws without it).',
6
+ whenToUse: [
7
+ 'User must pick one option from a predefined list',
8
+ 'Form field for country, category, role selection',
9
+ ],
10
+ whenNotToUse: [
11
+ 'Triggering actions — use Menu',
12
+ 'Multiple selections — use ToggleGroup or Checkbox group',
13
+ 'Fewer than 3 options — consider Toggle or ToggleGroup',
14
+ ],
15
+ anatomy: {
16
+ required: ['Root', 'Trigger', 'Popup', 'Item'],
17
+ optional: ['Value', 'Icon', 'ItemText'],
18
+ },
19
+ relatedTo: [
20
+ {component: 'Menu', relationship: 'Menu triggers actions; Select picks a value'},
21
+ {component: 'ToggleGroup', relationship: 'For visible, small option sets'},
22
+ ],
23
+ example: `<Select.Root name="role" defaultValue="viewer">
24
+ <Select.Trigger>
25
+ <Select.Value placeholder="Select role" />
26
+ <Select.Icon />
27
+ </Select.Trigger>
28
+ <Select.Popup>
29
+ <Select.Item value="admin">Admin</Select.Item>
30
+ <Select.Item value="editor">Editor</Select.Item>
31
+ <Select.Item value="viewer">Viewer</Select.Item>
32
+ </Select.Popup>
33
+ </Select.Root>`,
34
+ }
@@ -0,0 +1,31 @@
1
+ import type {ComponentMeta} from '../component-meta'
2
+
3
+ export const meta: ComponentMeta = {
4
+ description:
5
+ 'Vertical side navigation with collapsible groups. Supports controlled and uncontrolled active item. Compound component — Root is required.',
6
+ whenToUse: [
7
+ 'App-level sidebar navigation',
8
+ 'Settings navigation with grouped sections',
9
+ ],
10
+ whenNotToUse: [
11
+ 'In-page content switching — use Tabs',
12
+ 'Top-level horizontal nav — use Inline with LinkButton',
13
+ ],
14
+ anatomy: {
15
+ required: ['Root', 'Group', 'Item'],
16
+ },
17
+ relatedTo: [
18
+ {component: 'Tabs', relationship: 'Tabs switch content in-place; SideNav navigates pages'},
19
+ {component: 'PageShell', relationship: 'Often placed alongside PageShell'},
20
+ ],
21
+ example: `<SideNav.Root defaultValue="dashboard">
22
+ <SideNav.Group label="Main">
23
+ <SideNav.Item value="dashboard">Dashboard</SideNav.Item>
24
+ <SideNav.Item value="analytics">Analytics</SideNav.Item>
25
+ </SideNav.Group>
26
+ <SideNav.Group label="Settings" defaultExpanded={false}>
27
+ <SideNav.Item value="profile">Profile</SideNav.Item>
28
+ <SideNav.Item value="billing">Billing</SideNav.Item>
29
+ </SideNav.Group>
30
+ </SideNav.Root>`,
31
+ }
@@ -0,0 +1,22 @@
1
+ import type {ComponentMeta} from '../component-meta'
2
+
3
+ export const meta: ComponentMeta = {
4
+ description:
5
+ 'Vertical flex layout. Stacks children top-to-bottom with consistent gap.',
6
+ whenToUse: [
7
+ 'Vertically stacking form fields, cards, or sections',
8
+ 'Any column-direction layout with uniform spacing',
9
+ ],
10
+ whenNotToUse: [
11
+ 'Horizontal layouts — use Inline (no wrap) or Cluster (wraps)',
12
+ 'Grid layouts — use Grid',
13
+ ],
14
+ relatedTo: [
15
+ {component: 'Inline', relationship: 'Horizontal equivalent (no wrap)'},
16
+ {component: 'Cluster', relationship: 'Horizontal equivalent (wraps)'},
17
+ ],
18
+ example: `<Stack gap="md" align="stretch">
19
+ <Card>First</Card>
20
+ <Card>Second</Card>
21
+ </Stack>`,
22
+ }
@@ -0,0 +1,25 @@
1
+ import type {ComponentMeta} from '../component-meta'
2
+
3
+ export const meta: ComponentMeta = {
4
+ description:
5
+ 'Toggle switch for on/off settings. Renders a hidden checkbox for form submission + a styled switch button.',
6
+ whenToUse: [
7
+ 'On/off toggles in settings (notifications, dark mode, feature flags)',
8
+ 'Immediate-effect toggles where the change applies instantly',
9
+ ],
10
+ whenNotToUse: [
11
+ 'Form opt-in checkboxes — use Checkbox',
12
+ 'Selecting between multiple options — use Select or ToggleGroup',
13
+ ],
14
+ relatedTo: [
15
+ {component: 'Checkbox', relationship: 'Checkbox for selection/opt-in; Switch for on/off settings'},
16
+ {component: 'Toggle', relationship: 'Toggle for toolbar pressed state; Switch for settings'},
17
+ ],
18
+ example: `<Switch
19
+ name="notifications"
20
+ defaultChecked={true}
21
+ onCheckedChange={(checked) => console.log(checked)}
22
+ >
23
+ Enable notifications
24
+ </Switch>`,
25
+ }