@duro-app/ui 0.23.0 → 0.25.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 (100) hide show
  1. package/dist/components/ButtonGroup/ButtonGroup.d.ts +14 -0
  2. package/dist/components/ButtonGroup/ButtonGroup.d.ts.map +1 -0
  3. package/dist/components/ButtonGroup/ButtonGroup.meta.d.ts +3 -0
  4. package/dist/components/ButtonGroup/ButtonGroup.meta.d.ts.map +1 -0
  5. package/dist/components/ButtonGroup/ButtonGroup.stories.d.ts +11 -0
  6. package/dist/components/ButtonGroup/ButtonGroup.stories.d.ts.map +1 -0
  7. package/dist/components/ButtonGroup/styles.css.d.ts +45 -0
  8. package/dist/components/ButtonGroup/styles.css.d.ts.map +1 -0
  9. package/dist/components/CheckboxGroup/CheckboxGroup.d.ts.map +1 -1
  10. package/dist/components/CheckboxGroup/CheckboxGroup.meta.d.ts.map +1 -1
  11. package/dist/components/Combobox/Combobox.d.ts +44 -0
  12. package/dist/components/Combobox/Combobox.d.ts.map +1 -0
  13. package/dist/components/Combobox/Combobox.meta.d.ts +3 -0
  14. package/dist/components/Combobox/Combobox.meta.d.ts.map +1 -0
  15. package/dist/components/Combobox/Combobox.stories.d.ts +9 -0
  16. package/dist/components/Combobox/Combobox.stories.d.ts.map +1 -0
  17. package/dist/components/Combobox/ComboboxContext.d.ts +19 -0
  18. package/dist/components/Combobox/ComboboxContext.d.ts.map +1 -0
  19. package/dist/components/Combobox/styles.css.d.ts +112 -0
  20. package/dist/components/Combobox/styles.css.d.ts.map +1 -0
  21. package/dist/components/Combobox/useComboboxRoot.d.ts +15 -0
  22. package/dist/components/Combobox/useComboboxRoot.d.ts.map +1 -0
  23. package/dist/components/Field/Field.d.ts +4 -0
  24. package/dist/components/Field/Field.d.ts.map +1 -1
  25. package/dist/components/Field/Field.stories.d.ts +3 -0
  26. package/dist/components/Field/Field.stories.d.ts.map +1 -1
  27. package/dist/components/Field/FieldContext.d.ts +5 -0
  28. package/dist/components/Field/FieldContext.d.ts.map +1 -1
  29. package/dist/components/Field/styles.css.d.ts +26 -0
  30. package/dist/components/Field/styles.css.d.ts.map +1 -1
  31. package/dist/components/Form/Form.d.ts +7 -2
  32. package/dist/components/Form/Form.d.ts.map +1 -1
  33. package/dist/components/Form/Form.stories.d.ts +6 -0
  34. package/dist/components/Form/Form.stories.d.ts.map +1 -1
  35. package/dist/components/Form/FormContext.d.ts +9 -2
  36. package/dist/components/Form/FormContext.d.ts.map +1 -1
  37. package/dist/components/Input/Input.d.ts.map +1 -1
  38. package/dist/components/List/List.d.ts.map +1 -1
  39. package/dist/components/List/List.meta.d.ts.map +1 -1
  40. package/dist/components/List/List.stories.d.ts.map +1 -1
  41. package/dist/components/RadioGroup/RadioGroup.d.ts.map +1 -1
  42. package/dist/components/RadioGroup/RadioGroup.meta.d.ts.map +1 -1
  43. package/dist/components/Table/ColumnFilter.d.ts +8 -0
  44. package/dist/components/Table/ColumnFilter.d.ts.map +1 -0
  45. package/dist/components/Table/Pagination.d.ts +7 -0
  46. package/dist/components/Table/Pagination.d.ts.map +1 -0
  47. package/dist/components/Table/SortIndicator.d.ts +16 -0
  48. package/dist/components/Table/SortIndicator.d.ts.map +1 -0
  49. package/dist/components/Table/Table.d.ts +7 -1
  50. package/dist/components/Table/Table.d.ts.map +1 -1
  51. package/dist/components/Table/Table.stories.d.ts +4 -0
  52. package/dist/components/Table/Table.stories.d.ts.map +1 -1
  53. package/dist/components/Table/styles.css.d.ts +11 -3
  54. package/dist/components/Table/styles.css.d.ts.map +1 -1
  55. package/dist/components/Table/useDataTable.d.ts +32 -0
  56. package/dist/components/Table/useDataTable.d.ts.map +1 -0
  57. package/dist/components/Textarea/Textarea.d.ts.map +1 -1
  58. package/dist/index.css +1 -1
  59. package/dist/index.d.ts +3 -0
  60. package/dist/index.d.ts.map +1 -1
  61. package/dist/index.js +2863 -2026
  62. package/dist/index.js.map +1 -1
  63. package/package.json +3 -2
  64. package/src/components/ActionBar/ActionBar.stories.tsx +3 -3
  65. package/src/components/ButtonGroup/ButtonGroup.meta.ts +32 -0
  66. package/src/components/ButtonGroup/ButtonGroup.stories.tsx +103 -0
  67. package/src/components/ButtonGroup/ButtonGroup.tsx +59 -0
  68. package/src/components/ButtonGroup/styles.css.ts +28 -0
  69. package/src/components/CheckboxGroup/CheckboxGroup.meta.ts +4 -1
  70. package/src/components/CheckboxGroup/CheckboxGroup.tsx +2 -9
  71. package/src/components/Combobox/Combobox.meta.ts +35 -0
  72. package/src/components/Combobox/Combobox.stories.tsx +119 -0
  73. package/src/components/Combobox/Combobox.tsx +247 -0
  74. package/src/components/Combobox/ComboboxContext.ts +26 -0
  75. package/src/components/Combobox/styles.css.ts +119 -0
  76. package/src/components/Combobox/useComboboxRoot.ts +235 -0
  77. package/src/components/Field/Field.stories.tsx +60 -0
  78. package/src/components/Field/Field.tsx +97 -9
  79. package/src/components/Field/FieldContext.ts +5 -0
  80. package/src/components/Field/styles.css.ts +26 -0
  81. package/src/components/Form/Form.stories.tsx +275 -0
  82. package/src/components/Form/Form.tsx +73 -7
  83. package/src/components/Form/FormContext.ts +10 -1
  84. package/src/components/Input/Input.tsx +2 -1
  85. package/src/components/List/List.meta.ts +8 -2
  86. package/src/components/List/List.stories.tsx +35 -15
  87. package/src/components/List/List.tsx +1 -5
  88. package/src/components/RadioGroup/RadioGroup.meta.ts +4 -1
  89. package/src/components/RadioGroup/RadioGroup.tsx +1 -4
  90. package/src/components/Table/ColumnFilter.tsx +46 -0
  91. package/src/components/Table/Pagination.tsx +82 -0
  92. package/src/components/Table/SortIndicator.tsx +53 -0
  93. package/src/components/Table/Table.stories.tsx +397 -0
  94. package/src/components/Table/Table.tsx +58 -24
  95. package/src/components/Table/styles.css.ts +16 -6
  96. package/src/components/Table/useDataTable.ts +99 -0
  97. package/src/components/Textarea/Textarea.tsx +2 -1
  98. package/src/docs/helpers.tsx +2 -2
  99. package/src/index.ts +5 -0
  100. package/src/strict.css +5 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@duro-app/ui",
3
- "version": "0.23.0",
3
+ "version": "0.25.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -34,6 +34,7 @@
34
34
  },
35
35
  "peerDependencies": {
36
36
  "@hookform/resolvers": "^5.0.0",
37
+ "@tanstack/react-table": ">=8.0.0",
37
38
  "effect": "^3.0.0",
38
39
  "react": "^19.0.0",
39
40
  "react-dom": "^19.0.0",
@@ -52,7 +53,7 @@
52
53
  }
53
54
  },
54
55
  "dependencies": {
55
- "@duro-app/tokens": "^0.23.0"
56
+ "@duro-app/tokens": "^0.25.0"
56
57
  },
57
58
  "devDependencies": {
58
59
  "@babel/preset-typescript": "^7.28.0",
@@ -85,7 +85,7 @@ function SelectableTable({
85
85
 
86
86
  return (
87
87
  <>
88
- <Table.Root columns={5}>
88
+ <Table.Root>
89
89
  <Table.Header>
90
90
  <Table.Row>
91
91
  <Table.HeaderCell aria-label="Select">
@@ -172,7 +172,7 @@ export const Emphasized: Story = {
172
172
  }
173
173
  return (
174
174
  <>
175
- <Table.Root columns={3}>
175
+ <Table.Root>
176
176
  <Table.Header>
177
177
  <Table.Row>
178
178
  <Table.HeaderCell aria-label="Select">
@@ -230,7 +230,7 @@ export const Emphasized: Story = {
230
230
  export const CustomLabel: Story = {
231
231
  render: () => (
232
232
  <>
233
- <Table.Root columns={2}>
233
+ <Table.Root>
234
234
  <Table.Header>
235
235
  <Table.Row>
236
236
  <Table.HeaderCell>Name</Table.HeaderCell>
@@ -0,0 +1,32 @@
1
+ import type {ComponentMeta} from '../component-meta'
2
+
3
+ export const meta: ComponentMeta = {
4
+ description:
5
+ 'Groups related buttons together with consistent spacing and layout. Supports horizontal and vertical orientation with alignment control.',
6
+ whenToUse: [
7
+ 'Dialog footers with Save/Cancel actions',
8
+ 'Toolbar-style groupings of related actions',
9
+ 'Form submission areas with multiple actions',
10
+ ],
11
+ whenNotToUse: [
12
+ 'Single button — just use Button directly',
13
+ 'Wrapping tags or badges — use Cluster instead',
14
+ 'Navigation items — use Inline or SideNav',
15
+ ],
16
+ relatedTo: [
17
+ {
18
+ component: 'Inline',
19
+ relationship:
20
+ 'Inline is a general-purpose horizontal layout; ButtonGroup adds role="group" and disabled support',
21
+ },
22
+ {
23
+ component: 'ActionBar',
24
+ relationship:
25
+ 'ActionBar is a floating bulk-selection toolbar; ButtonGroup is a static grouping',
26
+ },
27
+ ],
28
+ example: `<ButtonGroup align="end" gap="sm">
29
+ <Button variant="secondary">Cancel</Button>
30
+ <Button variant="primary">Save</Button>
31
+ </ButtonGroup>`,
32
+ }
@@ -0,0 +1,103 @@
1
+ import type {Meta, StoryObj} from '@storybook/react'
2
+ import {expect} from 'storybook/test'
3
+ import {ButtonGroup} from './ButtonGroup'
4
+ import {Button} from '../Button/Button'
5
+
6
+ const meta: Meta<typeof ButtonGroup> = {
7
+ title: 'Components/ButtonGroup',
8
+ component: ButtonGroup,
9
+ argTypes: {
10
+ orientation: {control: 'select', options: ['horizontal', 'vertical']},
11
+ align: {control: 'select', options: ['start', 'end', 'center']},
12
+ gap: {control: 'select', options: ['xs', 'sm', 'md']},
13
+ disabled: {control: 'boolean'},
14
+ },
15
+ }
16
+
17
+ export default meta
18
+ type Story = StoryObj<typeof ButtonGroup>
19
+
20
+ export const Default: Story = {
21
+ render: () => (
22
+ <ButtonGroup>
23
+ <Button variant="primary">Save</Button>
24
+ <Button variant="secondary">Cancel</Button>
25
+ <Button variant="danger">Delete</Button>
26
+ </ButtonGroup>
27
+ ),
28
+ play: async ({canvas}) => {
29
+ const group = canvas.getByRole('group')
30
+ await expect(group).toBeInTheDocument()
31
+ const buttons = canvas.getAllByRole('button')
32
+ await expect(buttons.length).toBe(3)
33
+ },
34
+ }
35
+
36
+ export const Vertical: Story = {
37
+ render: () => (
38
+ <ButtonGroup orientation="vertical">
39
+ <Button variant="primary">Save</Button>
40
+ <Button variant="secondary">Cancel</Button>
41
+ <Button variant="danger">Delete</Button>
42
+ </ButtonGroup>
43
+ ),
44
+ play: async ({canvas}) => {
45
+ const group = canvas.getByRole('group')
46
+ await expect(group).toBeInTheDocument()
47
+ const buttons = canvas.getAllByRole('button')
48
+ await expect(buttons.length).toBe(3)
49
+ },
50
+ }
51
+
52
+ export const EndAligned: Story = {
53
+ render: () => (
54
+ <ButtonGroup align="end">
55
+ <Button variant="secondary">Cancel</Button>
56
+ <Button variant="primary">Save</Button>
57
+ </ButtonGroup>
58
+ ),
59
+ play: async ({canvas}) => {
60
+ const group = canvas.getByRole('group')
61
+ await expect(group).toBeInTheDocument()
62
+ const buttons = canvas.getAllByRole('button')
63
+ await expect(buttons.length).toBe(2)
64
+ },
65
+ }
66
+
67
+ export const Disabled: Story = {
68
+ render: () => (
69
+ <ButtonGroup disabled>
70
+ <Button variant="primary">Save</Button>
71
+ <Button variant="secondary">Cancel</Button>
72
+ <Button variant="danger">Delete</Button>
73
+ </ButtonGroup>
74
+ ),
75
+ play: async ({canvas}) => {
76
+ const group = canvas.getByRole('group')
77
+ await expect(group).toBeInTheDocument()
78
+ await expect(group).toHaveStyle({opacity: '0.5', pointerEvents: 'none'})
79
+ },
80
+ }
81
+
82
+ export const WithGaps: Story = {
83
+ render: () => (
84
+ <div style={{display: 'flex', flexDirection: 'column', gap: 24}}>
85
+ <ButtonGroup gap="xs">
86
+ <Button variant="primary">XS Gap</Button>
87
+ <Button variant="secondary">XS Gap</Button>
88
+ </ButtonGroup>
89
+ <ButtonGroup gap="sm">
90
+ <Button variant="primary">SM Gap</Button>
91
+ <Button variant="secondary">SM Gap</Button>
92
+ </ButtonGroup>
93
+ <ButtonGroup gap="md">
94
+ <Button variant="primary">MD Gap</Button>
95
+ <Button variant="secondary">MD Gap</Button>
96
+ </ButtonGroup>
97
+ </div>
98
+ ),
99
+ play: async ({canvas}) => {
100
+ const groups = canvas.getAllByRole('group')
101
+ await expect(groups.length).toBe(3)
102
+ },
103
+ }
@@ -0,0 +1,59 @@
1
+ import type {ReactNode} from 'react'
2
+ import {html} from 'react-strict-dom'
3
+ import {styles} from './styles.css'
4
+
5
+ export interface ButtonGroupProps {
6
+ children: ReactNode
7
+ /** Layout direction. Default: 'horizontal' */
8
+ orientation?: 'horizontal' | 'vertical'
9
+ /** Alignment within container. Default: 'start' */
10
+ align?: 'start' | 'end' | 'center'
11
+ /** Disable all buttons in group */
12
+ disabled?: boolean
13
+ /** Gap between buttons. Default: 'sm' */
14
+ gap?: 'xs' | 'sm' | 'md'
15
+ }
16
+
17
+ const gapMap = {
18
+ xs: styles.gapXs,
19
+ sm: styles.gapSm,
20
+ md: styles.gapMd,
21
+ } as const
22
+
23
+ const horizontalAlignMap = {
24
+ start: styles.alignStart,
25
+ center: styles.alignCenter,
26
+ end: styles.alignEnd,
27
+ } as const
28
+
29
+ const verticalAlignMap = {
30
+ start: styles.verticalAlignStart,
31
+ center: styles.verticalAlignCenter,
32
+ end: styles.verticalAlignEnd,
33
+ } as const
34
+
35
+ export function ButtonGroup({
36
+ children,
37
+ orientation = 'horizontal',
38
+ align = 'start',
39
+ disabled = false,
40
+ gap = 'sm',
41
+ }: ButtonGroupProps) {
42
+ const isVertical = orientation === 'vertical'
43
+ const alignStyle = isVertical ? verticalAlignMap[align] : horizontalAlignMap[align]
44
+
45
+ return (
46
+ <html.div
47
+ role="group"
48
+ style={[
49
+ styles.base,
50
+ isVertical ? styles.vertical : styles.horizontal,
51
+ gapMap[gap],
52
+ alignStyle,
53
+ disabled && styles.disabled,
54
+ ]}
55
+ >
56
+ {children}
57
+ </html.div>
58
+ )
59
+ }
@@ -0,0 +1,28 @@
1
+ import {css} from 'react-strict-dom'
2
+ import {spacing} from '@duro-app/tokens/tokens/spacing.css'
3
+
4
+ export const styles = css.create({
5
+ base: {
6
+ display: 'flex',
7
+ },
8
+ horizontal: {
9
+ flexDirection: 'row',
10
+ flexWrap: 'wrap',
11
+ },
12
+ vertical: {
13
+ flexDirection: 'column',
14
+ },
15
+ alignStart: {justifyContent: 'flex-start'},
16
+ alignCenter: {justifyContent: 'center'},
17
+ alignEnd: {justifyContent: 'flex-end'},
18
+ verticalAlignStart: {alignItems: 'flex-start'},
19
+ verticalAlignCenter: {alignItems: 'center'},
20
+ verticalAlignEnd: {alignItems: 'flex-end'},
21
+ gapXs: {gap: spacing.xs},
22
+ gapSm: {gap: spacing.sm},
23
+ gapMd: {gap: spacing.md},
24
+ disabled: {
25
+ opacity: 0.5,
26
+ pointerEvents: 'none',
27
+ },
28
+ })
@@ -13,7 +13,10 @@ export const meta: ComponentMeta = {
13
13
  'Many options (10+) — consider a filterable list or Select with multi-select',
14
14
  ],
15
15
  relatedTo: [
16
- {component: 'RadioGroup', relationship: 'RadioGroup for single-select; CheckboxGroup for multi-select'},
16
+ {
17
+ component: 'RadioGroup',
18
+ relationship: 'RadioGroup for single-select; CheckboxGroup for multi-select',
19
+ },
17
20
  {component: 'Checkbox', relationship: 'CheckboxGroup.Item wraps Checkbox internally'},
18
21
  {component: 'ToggleGroup', relationship: 'ToggleGroup for toolbar-style multi-select'},
19
22
  ],
@@ -46,9 +46,7 @@ function Root({
46
46
  const onToggle = useCallback(
47
47
  (itemValue: string, checked: boolean) => {
48
48
  if (disabled) return
49
- const next = checked
50
- ? [...value, itemValue]
51
- : value.filter((v) => v !== itemValue)
49
+ const next = checked ? [...value, itemValue] : value.filter((v) => v !== itemValue)
52
50
  setValue(next)
53
51
  },
54
52
  [disabled, value, setValue],
@@ -88,12 +86,7 @@ function Item({value, disabled: itemDisabled = false, children}: ItemProps) {
88
86
  )
89
87
 
90
88
  return (
91
- <Checkbox
92
- value={value}
93
- checked={isChecked}
94
- disabled={isDisabled}
95
- onChange={handleChange}
96
- >
89
+ <Checkbox value={value} checked={isChecked} disabled={isDisabled} onChange={handleChange}>
97
90
  {children}
98
91
  </Checkbox>
99
92
  )
@@ -0,0 +1,35 @@
1
+ import type {ComponentMeta} from '../component-meta'
2
+
3
+ export const meta: ComponentMeta = {
4
+ description:
5
+ 'Searchable dropdown for selecting a value from a filterable list. Combines a text input with a popup listbox. Compound component — Root is required.',
6
+ whenToUse: [
7
+ 'Large option lists where users benefit from typing to filter (e.g. country, city, user picker)',
8
+ 'Autocomplete or typeahead scenarios',
9
+ 'Table column filters with known categorical values',
10
+ ],
11
+ whenNotToUse: [
12
+ 'Small lists (fewer than 7 options) — use Select instead',
13
+ 'Free-form text entry — use Input',
14
+ 'Multiple selections — use CheckboxGroup or ToggleGroup',
15
+ ],
16
+ anatomy: {
17
+ required: ['Root', 'Input', 'Popup', 'Item'],
18
+ optional: ['Empty'],
19
+ },
20
+ relatedTo: [
21
+ {component: 'Select', relationship: 'Select for short lists without search; Combobox for long filterable lists'},
22
+ {component: 'Input', relationship: 'Input for free-form text; Combobox for constrained selection'},
23
+ {component: 'Menu', relationship: 'Menu triggers actions; Combobox picks a value'},
24
+ ],
25
+ example: `<Combobox.Root name="country" defaultValue="us" initialLabels={{us: 'United States'}}>
26
+ <Combobox.Input placeholder="Search countries…" />
27
+ <Combobox.Popup>
28
+ <Combobox.Item value="us">United States</Combobox.Item>
29
+ <Combobox.Item value="gb">United Kingdom</Combobox.Item>
30
+ <Combobox.Item value="fr">France</Combobox.Item>
31
+ <Combobox.Item value="de">Germany</Combobox.Item>
32
+ <Combobox.Empty>No countries found</Combobox.Empty>
33
+ </Combobox.Popup>
34
+ </Combobox.Root>`,
35
+ }
@@ -0,0 +1,119 @@
1
+ import {useState} from 'react'
2
+ import type {Meta, StoryObj} from '@storybook/react'
3
+ import {expect} from 'storybook/test'
4
+ import {Combobox} from './Combobox'
5
+
6
+ const meta: Meta = {
7
+ title: 'Components/Combobox',
8
+ }
9
+
10
+ export default meta
11
+ type Story = StoryObj
12
+
13
+ const fruits = ['Apple', 'Banana', 'Cherry', 'Date', 'Elderberry', 'Fig', 'Grape', 'Honeydew']
14
+
15
+ export const Default: Story = {
16
+ render: () => (
17
+ <Combobox.Root>
18
+ <Combobox.Input placeholder="Search fruit...">
19
+ <Combobox.Trigger />
20
+ </Combobox.Input>
21
+ <Combobox.Popup>
22
+ {fruits.map((fruit) => (
23
+ <Combobox.Item key={fruit} value={fruit.toLowerCase()}>
24
+ <Combobox.ItemText>{fruit}</Combobox.ItemText>
25
+ </Combobox.Item>
26
+ ))}
27
+ <Combobox.Empty>No fruits found</Combobox.Empty>
28
+ </Combobox.Popup>
29
+ </Combobox.Root>
30
+ ),
31
+ play: async ({canvas}) => {
32
+ const input = canvas.getByRole('combobox')
33
+ await expect(input).toBeInTheDocument()
34
+ await expect(input).toHaveAttribute('aria-expanded', 'false')
35
+ },
36
+ }
37
+
38
+ export const WithDefaultValue: Story = {
39
+ render: () => (
40
+ <Combobox.Root defaultValue="cherry" initialLabels={{cherry: 'Cherry'}}>
41
+ <Combobox.Input placeholder="Search fruit...">
42
+ <Combobox.Trigger />
43
+ </Combobox.Input>
44
+ <Combobox.Popup>
45
+ {fruits.map((fruit) => (
46
+ <Combobox.Item key={fruit} value={fruit.toLowerCase()}>
47
+ <Combobox.ItemText>{fruit}</Combobox.ItemText>
48
+ </Combobox.Item>
49
+ ))}
50
+ <Combobox.Empty>No fruits found</Combobox.Empty>
51
+ </Combobox.Popup>
52
+ </Combobox.Root>
53
+ ),
54
+ play: async ({canvas}) => {
55
+ const input = canvas.getByRole('combobox')
56
+ await expect(input).toHaveValue('Cherry')
57
+ },
58
+ }
59
+
60
+ function ControlledDemo() {
61
+ const [value, setValue] = useState<string | null>(null)
62
+
63
+ return (
64
+ <div style={{display: 'flex', flexDirection: 'column', gap: 16}}>
65
+ <Combobox.Root value={value} onValueChange={setValue}>
66
+ <Combobox.Input placeholder="Pick a role...">
67
+ <Combobox.Trigger />
68
+ </Combobox.Input>
69
+ <Combobox.Popup>
70
+ <Combobox.Item value="admin"><Combobox.ItemText>Admin</Combobox.ItemText></Combobox.Item>
71
+ <Combobox.Item value="editor"><Combobox.ItemText>Editor</Combobox.ItemText></Combobox.Item>
72
+ <Combobox.Item value="viewer"><Combobox.ItemText>Viewer</Combobox.ItemText></Combobox.Item>
73
+ <Combobox.Item value="moderator"><Combobox.ItemText>Moderator</Combobox.ItemText></Combobox.Item>
74
+ <Combobox.Item value="analyst"><Combobox.ItemText>Analyst</Combobox.ItemText></Combobox.Item>
75
+ <Combobox.Empty>No roles found</Combobox.Empty>
76
+ </Combobox.Popup>
77
+ </Combobox.Root>
78
+ <span style={{fontSize: 14, color: '#888'}}>
79
+ Selected: {value ?? 'none'}
80
+ </span>
81
+ </div>
82
+ )
83
+ }
84
+
85
+ export const Controlled: Story = {
86
+ render: () => <ControlledDemo />,
87
+ play: async ({canvas}) => {
88
+ await expect(canvas.getByText('Selected: none')).toBeInTheDocument()
89
+ },
90
+ }
91
+
92
+ export const ManyItems: Story = {
93
+ render: () => {
94
+ const countries = [
95
+ 'Argentina', 'Australia', 'Austria', 'Belgium', 'Brazil', 'Canada',
96
+ 'Chile', 'China', 'Colombia', 'Denmark', 'Egypt', 'Finland',
97
+ 'France', 'Germany', 'Greece', 'India', 'Indonesia', 'Ireland',
98
+ 'Italy', 'Japan', 'Kenya', 'Mexico', 'Netherlands', 'New Zealand',
99
+ 'Norway', 'Peru', 'Poland', 'Portugal', 'South Korea', 'Spain',
100
+ 'Sweden', 'Switzerland', 'Thailand', 'Turkey', 'United Kingdom', 'United States',
101
+ ]
102
+
103
+ return (
104
+ <Combobox.Root>
105
+ <Combobox.Input placeholder="Search country...">
106
+ <Combobox.Trigger />
107
+ </Combobox.Input>
108
+ <Combobox.Popup>
109
+ {countries.map((country) => (
110
+ <Combobox.Item key={country} value={country.toLowerCase()}>
111
+ <Combobox.ItemText>{country}</Combobox.ItemText>
112
+ </Combobox.Item>
113
+ ))}
114
+ <Combobox.Empty>No countries found</Combobox.Empty>
115
+ </Combobox.Popup>
116
+ </Combobox.Root>
117
+ )
118
+ },
119
+ }