@duro-app/ui 0.15.2 → 0.17.0

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 (103) 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 +2 -1
  10. package/dist/components/Field/Field.d.ts.map +1 -1
  11. package/dist/components/Field/Field.meta.d.ts +3 -0
  12. package/dist/components/Field/Field.meta.d.ts.map +1 -0
  13. package/dist/components/Field/FieldContext.d.ts +9 -1
  14. package/dist/components/Field/FieldContext.d.ts.map +1 -1
  15. package/dist/components/Fieldset/Fieldset.meta.d.ts +3 -0
  16. package/dist/components/Fieldset/Fieldset.meta.d.ts.map +1 -0
  17. package/dist/components/Form/Form.d.ts +11 -0
  18. package/dist/components/Form/Form.d.ts.map +1 -0
  19. package/dist/components/Form/Form.meta.d.ts +3 -0
  20. package/dist/components/Form/Form.meta.d.ts.map +1 -0
  21. package/dist/components/Form/Form.stories.d.ts +10 -0
  22. package/dist/components/Form/Form.stories.d.ts.map +1 -0
  23. package/dist/components/Form/FormContext.d.ts +3 -0
  24. package/dist/components/Form/FormContext.d.ts.map +1 -0
  25. package/dist/components/Grid/Grid.meta.d.ts +3 -0
  26. package/dist/components/Grid/Grid.meta.d.ts.map +1 -0
  27. package/dist/components/Inline/Inline.meta.d.ts +3 -0
  28. package/dist/components/Inline/Inline.meta.d.ts.map +1 -0
  29. package/dist/components/Input/Input.d.ts +3 -1
  30. package/dist/components/Input/Input.d.ts.map +1 -1
  31. package/dist/components/Input/Input.meta.d.ts +3 -0
  32. package/dist/components/Input/Input.meta.d.ts.map +1 -0
  33. package/dist/components/LinkButton/LinkButton.meta.d.ts +3 -0
  34. package/dist/components/LinkButton/LinkButton.meta.d.ts.map +1 -0
  35. package/dist/components/Menu/Menu.meta.d.ts +3 -0
  36. package/dist/components/Menu/Menu.meta.d.ts.map +1 -0
  37. package/dist/components/PageShell/PageShell.meta.d.ts +3 -0
  38. package/dist/components/PageShell/PageShell.meta.d.ts.map +1 -0
  39. package/dist/components/ScrollArea/ScrollArea.meta.d.ts +3 -0
  40. package/dist/components/ScrollArea/ScrollArea.meta.d.ts.map +1 -0
  41. package/dist/components/Select/Select.meta.d.ts +3 -0
  42. package/dist/components/Select/Select.meta.d.ts.map +1 -0
  43. package/dist/components/SideNav/SideNav.meta.d.ts +3 -0
  44. package/dist/components/SideNav/SideNav.meta.d.ts.map +1 -0
  45. package/dist/components/Stack/Stack.meta.d.ts +3 -0
  46. package/dist/components/Stack/Stack.meta.d.ts.map +1 -0
  47. package/dist/components/Switch/Switch.meta.d.ts +3 -0
  48. package/dist/components/Switch/Switch.meta.d.ts.map +1 -0
  49. package/dist/components/Table/Table.meta.d.ts +3 -0
  50. package/dist/components/Table/Table.meta.d.ts.map +1 -0
  51. package/dist/components/Tabs/Tabs.meta.d.ts +3 -0
  52. package/dist/components/Tabs/Tabs.meta.d.ts.map +1 -0
  53. package/dist/components/Textarea/Textarea.d.ts +3 -1
  54. package/dist/components/Textarea/Textarea.d.ts.map +1 -1
  55. package/dist/components/Textarea/Textarea.meta.d.ts +3 -0
  56. package/dist/components/Textarea/Textarea.meta.d.ts.map +1 -0
  57. package/dist/components/Toggle/Toggle.meta.d.ts +3 -0
  58. package/dist/components/Toggle/Toggle.meta.d.ts.map +1 -0
  59. package/dist/components/ToggleGroup/ToggleGroup.meta.d.ts +3 -0
  60. package/dist/components/ToggleGroup/ToggleGroup.meta.d.ts.map +1 -0
  61. package/dist/components/Tooltip/Tooltip.meta.d.ts +3 -0
  62. package/dist/components/Tooltip/Tooltip.meta.d.ts.map +1 -0
  63. package/dist/components/component-meta.d.ts +15 -0
  64. package/dist/components/component-meta.d.ts.map +1 -0
  65. package/dist/index.d.ts +1 -0
  66. package/dist/index.d.ts.map +1 -1
  67. package/dist/index.js +1500 -1400
  68. package/dist/index.js.map +1 -1
  69. package/package.json +19 -2
  70. package/src/components/ActionBar/ActionBar.tsx +20 -15
  71. package/src/components/Button/Button.meta.ts +23 -0
  72. package/src/components/Checkbox/Checkbox.meta.ts +21 -0
  73. package/src/components/Cluster/Cluster.meta.ts +25 -0
  74. package/src/components/Field/Field.meta.ts +37 -0
  75. package/src/components/Field/Field.tsx +56 -3
  76. package/src/components/Field/FieldContext.ts +10 -0
  77. package/src/components/Fieldset/Fieldset.meta.ts +38 -0
  78. package/src/components/Form/Form.meta.ts +38 -0
  79. package/src/components/Form/Form.stories.tsx +252 -0
  80. package/src/components/Form/Form.tsx +59 -0
  81. package/src/components/Form/FormContext.ts +7 -0
  82. package/src/components/Grid/Grid.meta.ts +30 -0
  83. package/src/components/Inline/Inline.meta.ts +23 -0
  84. package/src/components/Input/Input.meta.ts +23 -0
  85. package/src/components/Input/Input.tsx +18 -4
  86. package/src/components/LinkButton/LinkButton.meta.ts +24 -0
  87. package/src/components/Menu/Menu.meta.ts +33 -0
  88. package/src/components/PageShell/PageShell.meta.ts +24 -0
  89. package/src/components/ScrollArea/ScrollArea.meta.ts +27 -0
  90. package/src/components/Select/Select.meta.ts +34 -0
  91. package/src/components/SideNav/SideNav.meta.ts +31 -0
  92. package/src/components/Stack/Stack.meta.ts +22 -0
  93. package/src/components/Switch/Switch.meta.ts +25 -0
  94. package/src/components/Table/Table.meta.ts +36 -0
  95. package/src/components/Tabs/Tabs.meta.ts +31 -0
  96. package/src/components/Textarea/Textarea.meta.ts +22 -0
  97. package/src/components/Textarea/Textarea.tsx +20 -4
  98. package/src/components/Toggle/Toggle.meta.ts +30 -0
  99. package/src/components/ToggleGroup/ToggleGroup.meta.ts +30 -0
  100. package/src/components/ToggleGroup/ToggleGroup.stories.tsx +2 -2
  101. package/src/components/Tooltip/Tooltip.meta.ts +22 -0
  102. package/src/components/component-meta.ts +8 -0
  103. package/src/index.ts +1 -0
@@ -0,0 +1,59 @@
1
+ import {type ReactNode, useCallback, useEffect, useRef} from 'react'
2
+ import {html} from 'react-strict-dom'
3
+ import {
4
+ useForm,
5
+ FormProvider,
6
+ type UseFormReturn,
7
+ type DefaultValues,
8
+ type FieldValues,
9
+ } from 'react-hook-form'
10
+ import {effectTsResolver} from '@hookform/resolvers/effect-ts'
11
+ import type {Schema} from 'effect'
12
+ import {FormContext} from './FormContext'
13
+
14
+ export interface FormProps<T extends FieldValues> {
15
+ schema: Schema.Schema<T>
16
+ defaultValues: DefaultValues<T>
17
+ onSubmit: (data: T) => void | Promise<void>
18
+ children: ReactNode | ((methods: UseFormReturn<T>) => ReactNode)
19
+ }
20
+
21
+ export function Form<T extends FieldValues>({
22
+ schema,
23
+ defaultValues,
24
+ onSubmit,
25
+ children,
26
+ }: FormProps<T>) {
27
+ const methods = useForm<T>({
28
+ resolver: effectTsResolver(schema),
29
+ defaultValues,
30
+ mode: 'onTouched',
31
+ reValidateMode: 'onChange',
32
+ })
33
+
34
+ const formRef = useRef<HTMLFormElement>(null)
35
+ const handleSubmit = useCallback(
36
+ (e: Event) => {
37
+ e.preventDefault()
38
+ void methods.handleSubmit(onSubmit)()
39
+ },
40
+ [methods, onSubmit],
41
+ )
42
+
43
+ useEffect(() => {
44
+ const el = formRef.current
45
+ if (!el) return
46
+ el.addEventListener('submit', handleSubmit)
47
+ return () => el.removeEventListener('submit', handleSubmit)
48
+ }, [handleSubmit])
49
+
50
+ return (
51
+ <FormProvider {...methods}>
52
+ <FormContext.Provider value={true}>
53
+ <html.form ref={formRef}>
54
+ {typeof children === 'function' ? children(methods) : children}
55
+ </html.form>
56
+ </FormContext.Provider>
57
+ </FormProvider>
58
+ )
59
+ }
@@ -0,0 +1,7 @@
1
+ import {createContext, useContext} from 'react'
2
+
3
+ export const FormContext = createContext(false)
4
+
5
+ export function useFormContext() {
6
+ return useContext(FormContext)
7
+ }
@@ -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
+ }
@@ -30,6 +30,8 @@ interface InputProps {
30
30
  defaultValue?: string
31
31
  disabled?: boolean
32
32
  onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void
33
+ onBlur?: () => void
34
+ ref?: React.Ref<HTMLInputElement>
33
35
  }
34
36
 
35
37
  export function Input({
@@ -45,10 +47,20 @@ export function Input({
45
47
  defaultValue,
46
48
  disabled,
47
49
  onChange,
50
+ onBlur,
51
+ ref,
48
52
  }: InputProps) {
49
53
  const ctx = useFieldContext()
50
54
  const groupCtx = useInputGroupContext()
51
55
 
56
+ const fieldCtx = ctx?.field
57
+ const effectiveName = name ?? fieldCtx?.name
58
+ const effectiveValue = value ?? (fieldCtx ? String(fieldCtx.value ?? '') : undefined)
59
+ const effectiveOnChange = onChange ?? fieldCtx?.onChange
60
+ const effectiveOnBlur = onBlur ?? fieldCtx?.onBlur
61
+ const effectiveRef = ref ?? fieldCtx?.ref
62
+ const effectiveVariant = ctx?.invalid ? 'error' : variant
63
+
52
64
  // react-strict-dom omits web-only `pattern` from its types, but the
53
65
  // underlying DOM element supports it. Type-assert to pass it through.
54
66
  const extraProps = pattern !== undefined ? {pattern} : undefined
@@ -57,20 +69,22 @@ export function Input({
57
69
  <html.input
58
70
  id={ctx?.controlId}
59
71
  type={type}
60
- name={name}
72
+ name={effectiveName}
61
73
  placeholder={placeholder}
62
74
  required={required}
63
75
  minLength={minLength}
64
76
  autoComplete={autoComplete}
65
- value={value}
77
+ value={effectiveValue}
66
78
  defaultValue={defaultValue}
67
79
  disabled={disabled}
68
80
  aria-describedby={
69
81
  ctx ? `${ctx.descriptionId} ${ctx.invalid ? ctx.errorId : ''}`.trim() : undefined
70
82
  }
71
83
  aria-invalid={ctx?.invalid || variant === 'error' || undefined}
72
- onChange={onChange}
73
- style={[styles.base, styles[variant], groupCtx?.inGroup && styles.inGroup]}
84
+ onChange={effectiveOnChange as StrictInputProps['onChange']}
85
+ onBlur={effectiveOnBlur as StrictInputProps['onBlur']}
86
+ ref={effectiveRef as React.Ref<HTMLInputElement>}
87
+ style={[styles.base, styles[effectiveVariant], groupCtx?.inGroup && styles.inGroup]}
74
88
  {...(extraProps as Record<string, unknown>)}
75
89
  />
76
90
  )
@@ -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
+ }
@@ -0,0 +1,36 @@
1
+ import type {ComponentMeta} from '../component-meta'
2
+
3
+ export const meta: ComponentMeta = {
4
+ description:
5
+ 'Data table with CSS grid layout. Supports striped, bordered variants. Compound component — Root is required (throws without it). Requires `columns` prop on Root.',
6
+ whenToUse: [
7
+ 'Displaying tabular data with rows and columns',
8
+ 'Data lists with sortable/filterable columns',
9
+ ],
10
+ whenNotToUse: [
11
+ 'Simple key-value pairs — use Stack with Text',
12
+ 'Card-based layouts — use Grid with Card',
13
+ ],
14
+ anatomy: {
15
+ required: ['Root', 'Header', 'Body', 'Row', 'HeaderCell', 'Cell'],
16
+ },
17
+ relatedTo: [
18
+ {component: 'ScrollArea', relationship: 'Wrap Table in ScrollArea for horizontal overflow'},
19
+ ],
20
+ example: `<Table.Root variant="striped" size="md" columns={3}>
21
+ <Table.Header>
22
+ <Table.Row>
23
+ <Table.HeaderCell>Name</Table.HeaderCell>
24
+ <Table.HeaderCell>Role</Table.HeaderCell>
25
+ <Table.HeaderCell>Status</Table.HeaderCell>
26
+ </Table.Row>
27
+ </Table.Header>
28
+ <Table.Body>
29
+ <Table.Row>
30
+ <Table.Cell>Alice</Table.Cell>
31
+ <Table.Cell>Admin</Table.Cell>
32
+ <Table.Cell>Active</Table.Cell>
33
+ </Table.Row>
34
+ </Table.Body>
35
+ </Table.Root>`,
36
+ }
@@ -0,0 +1,31 @@
1
+ import type {ComponentMeta} from '../component-meta'
2
+
3
+ export const meta: ComponentMeta = {
4
+ description:
5
+ 'Tabbed interface with keyboard navigation. Supports horizontal and vertical orientation. Compound component — Root is required.',
6
+ whenToUse: [
7
+ 'Switching between content panels in the same view',
8
+ 'Settings pages with multiple sections',
9
+ 'Organizing related content without page navigation',
10
+ ],
11
+ whenNotToUse: [
12
+ 'Primary site navigation — use SideNav or links',
13
+ 'Step-by-step wizard — use a custom stepper',
14
+ ],
15
+ anatomy: {
16
+ required: ['Root', 'List', 'Tab', 'Panel'],
17
+ },
18
+ relatedTo: [
19
+ {component: 'SideNav', relationship: 'For persistent navigation, not content switching'},
20
+ ],
21
+ example: `<Tabs.Root defaultValue="general">
22
+ <Tabs.List>
23
+ <Tabs.Tab value="general">General</Tabs.Tab>
24
+ <Tabs.Tab value="security">Security</Tabs.Tab>
25
+ <Tabs.Tab value="billing" disabled>Billing</Tabs.Tab>
26
+ </Tabs.List>
27
+ <Tabs.Panel value="general">General settings content</Tabs.Panel>
28
+ <Tabs.Panel value="security">Security settings content</Tabs.Panel>
29
+ <Tabs.Panel value="billing">Billing content</Tabs.Panel>
30
+ </Tabs.Root>`,
31
+ }
@@ -0,0 +1,22 @@
1
+ import type {ComponentMeta} from '../component-meta'
2
+
3
+ export const meta: ComponentMeta = {
4
+ description:
5
+ 'Multi-line text input with automatic Field/Form integration. Same Field binding behavior as Input.',
6
+ whenToUse: [
7
+ 'Multi-line text entry (comments, messages, descriptions)',
8
+ ],
9
+ whenNotToUse: [
10
+ 'Single-line input — use Input',
11
+ ],
12
+ relatedTo: [
13
+ {component: 'Input', relationship: 'Single-line variant'},
14
+ {component: 'Field', relationship: 'Wrap in Field.Root for label + error'},
15
+ ],
16
+ example: `<Field.Root name="message">
17
+ <Field.Label>Message</Field.Label>
18
+ <Textarea placeholder="Tell us what you think..." rows={4} />
19
+ <Field.Description>At least 10 characters</Field.Description>
20
+ <Field.Error />
21
+ </Field.Root>`,
22
+ }
@@ -2,6 +2,8 @@ import {html} from 'react-strict-dom'
2
2
  import {useFieldContext} from '../Field/FieldContext'
3
3
  import {styles} from './styles.css'
4
4
 
5
+ type StrictTextareaProps = React.ComponentProps<typeof html.textarea>
6
+
5
7
  export type TextareaVariant = 'default' | 'error'
6
8
 
7
9
  interface TextareaProps {
@@ -14,6 +16,8 @@ interface TextareaProps {
14
16
  defaultValue?: string
15
17
  disabled?: boolean
16
18
  onChange?: (e: React.ChangeEvent<HTMLTextAreaElement>) => void
19
+ onBlur?: () => void
20
+ ref?: React.Ref<HTMLTextAreaElement>
17
21
  }
18
22
 
19
23
  export function Textarea({
@@ -26,25 +30,37 @@ export function Textarea({
26
30
  defaultValue,
27
31
  disabled,
28
32
  onChange,
33
+ onBlur,
34
+ ref,
29
35
  }: TextareaProps) {
30
36
  const ctx = useFieldContext()
31
37
 
38
+ const fieldCtx = ctx?.field
39
+ const effectiveName = name ?? fieldCtx?.name
40
+ const effectiveValue = value ?? (fieldCtx ? String(fieldCtx.value ?? '') : undefined)
41
+ const effectiveOnChange = onChange ?? fieldCtx?.onChange
42
+ const effectiveOnBlur = onBlur ?? fieldCtx?.onBlur
43
+ const effectiveRef = ref ?? fieldCtx?.ref
44
+ const effectiveVariant = ctx?.invalid ? 'error' : variant
45
+
32
46
  return (
33
47
  <html.textarea
34
48
  id={ctx?.controlId}
35
- name={name}
49
+ name={effectiveName}
36
50
  placeholder={placeholder}
37
51
  required={required}
38
52
  rows={rows}
39
- value={value}
53
+ value={effectiveValue}
40
54
  defaultValue={defaultValue}
41
55
  disabled={disabled}
42
56
  aria-describedby={
43
57
  ctx ? `${ctx.descriptionId} ${ctx.invalid ? ctx.errorId : ''}`.trim() : undefined
44
58
  }
45
59
  aria-invalid={ctx?.invalid || variant === 'error' || undefined}
46
- onChange={onChange}
47
- style={[styles.base, styles[variant]]}
60
+ onChange={effectiveOnChange as StrictTextareaProps['onChange']}
61
+ onBlur={effectiveOnBlur as StrictTextareaProps['onBlur']}
62
+ ref={effectiveRef as React.Ref<HTMLTextAreaElement>}
63
+ style={[styles.base, styles[effectiveVariant]]}
48
64
  />
49
65
  )
50
66
  }
@@ -0,0 +1,30 @@
1
+ import type {ComponentMeta} from '../component-meta'
2
+
3
+ export const meta: ComponentMeta = {
4
+ description:
5
+ 'Toggle button with pressed/unpressed state. Works standalone or within ToggleGroup for multi/single selection.',
6
+ whenToUse: [
7
+ 'Toolbar buttons that toggle on/off (bold, italic, grid view)',
8
+ 'Within ToggleGroup for selecting from visible options',
9
+ ],
10
+ whenNotToUse: [
11
+ 'On/off settings — use Switch',
12
+ 'Form checkboxes — use Checkbox',
13
+ 'Submitting actions — use Button',
14
+ ],
15
+ relatedTo: [
16
+ {component: 'ToggleGroup', relationship: 'Group of Toggles with single/multi select'},
17
+ {component: 'Switch', relationship: 'Switch for settings; Toggle for toolbar actions'},
18
+ {component: 'Button', relationship: 'Button for actions; Toggle for state'},
19
+ ],
20
+ example: `// Standalone
21
+ <Toggle pressed={isBold} onPressedChange={setIsBold} aria-label="Bold">
22
+ <Icon name="key" size={16} />
23
+ </Toggle>
24
+
25
+ // In a group
26
+ <ToggleGroup value={['grid']} onValueChange={setView} multiple={false}>
27
+ <Toggle value="list">List</Toggle>
28
+ <Toggle value="grid">Grid</Toggle>
29
+ </ToggleGroup>`,
30
+ }
@@ -0,0 +1,30 @@
1
+ import type {ComponentMeta} from '../component-meta'
2
+
3
+ export const meta: ComponentMeta = {
4
+ description:
5
+ 'Container for Toggle buttons enabling single or multi selection. Provides context to child Toggles.',
6
+ whenToUse: [
7
+ 'Visible option set where user picks one or more (view mode, filter categories)',
8
+ 'Segmented control pattern',
9
+ ],
10
+ whenNotToUse: [
11
+ 'Dropdown selection — use Select',
12
+ 'Many options (5+) — use Select for space efficiency',
13
+ 'Form checkboxes — use Checkbox group in Fieldset',
14
+ ],
15
+ relatedTo: [
16
+ {component: 'Toggle', relationship: 'Toggle children get group context'},
17
+ {component: 'Select', relationship: 'Select for larger option sets'},
18
+ ],
19
+ example: `<ToggleGroup
20
+ value={selected}
21
+ onValueChange={setSelected}
22
+ multiple={false}
23
+ orientation="horizontal"
24
+ size="default"
25
+ >
26
+ <Toggle value="list">List view</Toggle>
27
+ <Toggle value="grid">Grid view</Toggle>
28
+ <Toggle value="board">Board view</Toggle>
29
+ </ToggleGroup>`,
30
+ }
@@ -119,7 +119,7 @@ export const Vertical: Story = {
119
119
  </ToggleGroup>
120
120
  ),
121
121
  play: async ({canvas}) => {
122
- const group = canvas.getByRole('group')
122
+ const group = canvas.getByRole('toolbar')
123
123
  await expect(group).toHaveAttribute('aria-orientation', 'vertical')
124
124
  },
125
125
  }
@@ -153,7 +153,7 @@ export const AllVariants: Story = {
153
153
  </html.div>
154
154
  ),
155
155
  play: async ({canvas}) => {
156
- const groups = canvas.getAllByRole('group')
156
+ const groups = canvas.getAllByRole('toolbar')
157
157
  await expect(groups.length).toBe(4)
158
158
  },
159
159
  }