@duro-app/ui 0.22.0 → 0.24.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 (89) hide show
  1. package/dist/components/Alert/Alert.meta.d.ts.map +1 -1
  2. package/dist/components/ButtonGroup/ButtonGroup.d.ts +14 -0
  3. package/dist/components/ButtonGroup/ButtonGroup.d.ts.map +1 -0
  4. package/dist/components/ButtonGroup/ButtonGroup.meta.d.ts +3 -0
  5. package/dist/components/ButtonGroup/ButtonGroup.meta.d.ts.map +1 -0
  6. package/dist/components/ButtonGroup/ButtonGroup.stories.d.ts +11 -0
  7. package/dist/components/ButtonGroup/ButtonGroup.stories.d.ts.map +1 -0
  8. package/dist/components/ButtonGroup/styles.css.d.ts +45 -0
  9. package/dist/components/ButtonGroup/styles.css.d.ts.map +1 -0
  10. package/dist/components/Callout/Callout.meta.d.ts.map +1 -1
  11. package/dist/components/Card/Card.meta.d.ts.map +1 -1
  12. package/dist/components/CheckboxGroup/CheckboxGroup.d.ts +22 -0
  13. package/dist/components/CheckboxGroup/CheckboxGroup.d.ts.map +1 -0
  14. package/dist/components/CheckboxGroup/CheckboxGroup.meta.d.ts +3 -0
  15. package/dist/components/CheckboxGroup/CheckboxGroup.meta.d.ts.map +1 -0
  16. package/dist/components/CheckboxGroup/CheckboxGroup.stories.d.ts +9 -0
  17. package/dist/components/CheckboxGroup/CheckboxGroup.stories.d.ts.map +1 -0
  18. package/dist/components/CheckboxGroup/styles.css.d.ts +12 -0
  19. package/dist/components/CheckboxGroup/styles.css.d.ts.map +1 -0
  20. package/dist/components/EmptyState/EmptyState.meta.d.ts.map +1 -1
  21. package/dist/components/InputGroup/InputGroup.meta.d.ts.map +1 -1
  22. package/dist/components/List/List.d.ts.map +1 -1
  23. package/dist/components/List/List.meta.d.ts.map +1 -1
  24. package/dist/components/List/List.stories.d.ts.map +1 -1
  25. package/dist/components/RadioGroup/RadioGroup.d.ts +22 -0
  26. package/dist/components/RadioGroup/RadioGroup.d.ts.map +1 -0
  27. package/dist/components/RadioGroup/RadioGroup.meta.d.ts +3 -0
  28. package/dist/components/RadioGroup/RadioGroup.meta.d.ts.map +1 -0
  29. package/dist/components/RadioGroup/RadioGroup.stories.d.ts +9 -0
  30. package/dist/components/RadioGroup/RadioGroup.stories.d.ts.map +1 -0
  31. package/dist/components/RadioGroup/styles.css.d.ts +60 -0
  32. package/dist/components/RadioGroup/styles.css.d.ts.map +1 -0
  33. package/dist/components/StatusIcon/StatusIcon.meta.d.ts.map +1 -1
  34. package/dist/components/Table/ColumnFilter.d.ts +8 -0
  35. package/dist/components/Table/ColumnFilter.d.ts.map +1 -0
  36. package/dist/components/Table/Pagination.d.ts +7 -0
  37. package/dist/components/Table/Pagination.d.ts.map +1 -0
  38. package/dist/components/Table/SortIndicator.d.ts +16 -0
  39. package/dist/components/Table/SortIndicator.d.ts.map +1 -0
  40. package/dist/components/Table/Table.d.ts +10 -3
  41. package/dist/components/Table/Table.d.ts.map +1 -1
  42. package/dist/components/Table/Table.stories.d.ts +4 -0
  43. package/dist/components/Table/Table.stories.d.ts.map +1 -1
  44. package/dist/components/Table/styles.css.d.ts +12 -4
  45. package/dist/components/Table/styles.css.d.ts.map +1 -1
  46. package/dist/components/Table/useDataTable.d.ts +32 -0
  47. package/dist/components/Table/useDataTable.d.ts.map +1 -0
  48. package/dist/components/ThemeProvider/ThemeProvider.meta.d.ts.map +1 -1
  49. package/dist/index.css +1 -1
  50. package/dist/index.d.ts +3 -0
  51. package/dist/index.d.ts.map +1 -1
  52. package/dist/index.js +2399 -1927
  53. package/dist/index.js.map +1 -1
  54. package/package.json +3 -2
  55. package/src/components/ActionBar/ActionBar.stories.tsx +3 -3
  56. package/src/components/Alert/Alert.meta.ts +4 -1
  57. package/src/components/ButtonGroup/ButtonGroup.meta.ts +24 -0
  58. package/src/components/ButtonGroup/ButtonGroup.stories.tsx +103 -0
  59. package/src/components/ButtonGroup/ButtonGroup.tsx +59 -0
  60. package/src/components/ButtonGroup/styles.css.ts +28 -0
  61. package/src/components/Callout/Callout.meta.ts +4 -1
  62. package/src/components/Card/Card.meta.ts +4 -1
  63. package/src/components/Checkbox/styles.css.ts +1 -1
  64. package/src/components/CheckboxGroup/CheckboxGroup.meta.ts +25 -0
  65. package/src/components/CheckboxGroup/CheckboxGroup.stories.tsx +88 -0
  66. package/src/components/CheckboxGroup/CheckboxGroup.tsx +105 -0
  67. package/src/components/CheckboxGroup/styles.css.ts +13 -0
  68. package/src/components/EmptyState/EmptyState.meta.ts +1 -4
  69. package/src/components/Icon/Icon.meta.ts +5 -5
  70. package/src/components/InputGroup/InputGroup.meta.ts +4 -1
  71. package/src/components/List/List.meta.ts +8 -2
  72. package/src/components/List/List.stories.tsx +35 -15
  73. package/src/components/List/List.tsx +1 -5
  74. package/src/components/RadioGroup/RadioGroup.meta.ts +25 -0
  75. package/src/components/RadioGroup/RadioGroup.stories.tsx +83 -0
  76. package/src/components/RadioGroup/RadioGroup.tsx +113 -0
  77. package/src/components/RadioGroup/styles.css.ts +66 -0
  78. package/src/components/StatusIcon/StatusIcon.meta.ts +1 -4
  79. package/src/components/Table/ColumnFilter.tsx +46 -0
  80. package/src/components/Table/Pagination.tsx +82 -0
  81. package/src/components/Table/SortIndicator.tsx +56 -0
  82. package/src/components/Table/Table.stories.tsx +355 -9
  83. package/src/components/Table/Table.tsx +62 -17
  84. package/src/components/Table/styles.css.ts +18 -8
  85. package/src/components/Table/useDataTable.ts +113 -0
  86. package/src/components/ThemeProvider/ThemeProvider.meta.ts +1 -3
  87. package/src/docs/helpers.tsx +2 -2
  88. package/src/index.ts +5 -0
  89. 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.22.0",
3
+ "version": "0.24.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.22.0"
56
+ "@duro-app/tokens": "^0.24.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>
@@ -14,7 +14,10 @@ export const meta: ComponentMeta = {
14
14
  'Form field errors — use Field.Error',
15
15
  ],
16
16
  relatedTo: [
17
- {component: 'Callout', relationship: 'Callout for larger block-level messages; Alert for compact inline'},
17
+ {
18
+ component: 'Callout',
19
+ relationship: 'Callout for larger block-level messages; Alert for compact inline',
20
+ },
18
21
  ],
19
22
  example: `<Stack gap="sm">
20
23
  <Alert variant="success">Settings saved successfully.</Alert>
@@ -0,0 +1,24 @@
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
+ {component: 'Inline', relationship: 'Inline is a general-purpose horizontal layout; ButtonGroup adds role="group" and disabled support'},
18
+ {component: 'ActionBar', relationship: 'ActionBar is a floating bulk-selection toolbar; ButtonGroup is a static grouping'},
19
+ ],
20
+ example: `<ButtonGroup align="end" gap="sm">
21
+ <Button variant="secondary">Cancel</Button>
22
+ <Button variant="primary">Save</Button>
23
+ </ButtonGroup>`,
24
+ }
@@ -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
+ })
@@ -14,7 +14,10 @@ export const meta: ComponentMeta = {
14
14
  'Toast notifications — use a toast system',
15
15
  ],
16
16
  relatedTo: [
17
- {component: 'Alert', relationship: 'Alert for compact inline; Callout for block-level prominent'},
17
+ {
18
+ component: 'Alert',
19
+ relationship: 'Alert for compact inline; Callout for block-level prominent',
20
+ },
18
21
  ],
19
22
  example: `<Callout variant="warning">
20
23
  Your subscription expires in 3 days. Renew now to avoid service interruption.
@@ -14,7 +14,10 @@ export const meta: ComponentMeta = {
14
14
  'Simple spacing — use Stack or Inline',
15
15
  ],
16
16
  relatedTo: [
17
- {component: 'Panel', relationship: 'Panel for structural Header/Body/Footer; Card for visual container'},
17
+ {
18
+ component: 'Panel',
19
+ relationship: 'Panel for structural Header/Body/Footer; Card for visual container',
20
+ },
18
21
  ],
19
22
  example: `<Card variant="elevated" size="md">
20
23
  <Stack gap="md">
@@ -22,7 +22,7 @@ export const styles = css.create({
22
22
  height: 18,
23
23
  borderWidth: 1,
24
24
  borderStyle: 'solid',
25
- borderRadius: radii.sm,
25
+ borderRadius: radii.xs,
26
26
  display: 'inline-flex',
27
27
  alignItems: 'center',
28
28
  justifyContent: 'center',
@@ -0,0 +1,25 @@
1
+ import type {ComponentMeta} from '../component-meta'
2
+
3
+ export const meta: ComponentMeta = {
4
+ description:
5
+ 'Checkbox group for multi-select from a list of options. Wraps the existing Checkbox component with shared state management.',
6
+ whenToUse: [
7
+ 'Selecting multiple options from a small set',
8
+ 'Permission or feature toggles where several can be active',
9
+ ],
10
+ whenNotToUse: [
11
+ 'Single selection — use RadioGroup instead',
12
+ 'Binary on/off — use a single Checkbox or Switch',
13
+ 'Many options (10+) — consider a filterable list or Select with multi-select',
14
+ ],
15
+ relatedTo: [
16
+ {component: 'RadioGroup', relationship: 'RadioGroup for single-select; CheckboxGroup for multi-select'},
17
+ {component: 'Checkbox', relationship: 'CheckboxGroup.Item wraps Checkbox internally'},
18
+ {component: 'ToggleGroup', relationship: 'ToggleGroup for toolbar-style multi-select'},
19
+ ],
20
+ example: `<CheckboxGroup.Root defaultValue={['email']} onValueChange={(v) => console.log(v)}>
21
+ <CheckboxGroup.Item value="email">Email</CheckboxGroup.Item>
22
+ <CheckboxGroup.Item value="sms">SMS</CheckboxGroup.Item>
23
+ <CheckboxGroup.Item value="push">Push notification</CheckboxGroup.Item>
24
+ </CheckboxGroup.Root>`,
25
+ }
@@ -0,0 +1,88 @@
1
+ import {useState} from 'react'
2
+ import type {Meta, StoryObj} from '@storybook/react'
3
+ import {expect, fn} from 'storybook/test'
4
+ import {css, html} from 'react-strict-dom'
5
+ import {CheckboxGroup} from './CheckboxGroup'
6
+
7
+ const meta: Meta = {
8
+ title: 'Components/CheckboxGroup',
9
+ argTypes: {
10
+ orientation: {control: 'select', options: ['horizontal', 'vertical']},
11
+ disabled: {control: 'boolean'},
12
+ },
13
+ }
14
+
15
+ export default meta
16
+ type Story = StoryObj
17
+
18
+ export const Default: Story = {
19
+ render: () => (
20
+ <CheckboxGroup.Root defaultValue={['email']}>
21
+ <CheckboxGroup.Item value="email">Email notifications</CheckboxGroup.Item>
22
+ <CheckboxGroup.Item value="sms">SMS notifications</CheckboxGroup.Item>
23
+ <CheckboxGroup.Item value="push">Push notifications</CheckboxGroup.Item>
24
+ </CheckboxGroup.Root>
25
+ ),
26
+ play: async ({canvas}) => {
27
+ const checkboxes = canvas.getAllByRole('checkbox')
28
+ await expect(checkboxes.length).toBe(3)
29
+ await expect(checkboxes[0]).toBeChecked()
30
+ await expect(checkboxes[1]).not.toBeChecked()
31
+ await expect(checkboxes[2]).not.toBeChecked()
32
+ },
33
+ }
34
+
35
+ export const Horizontal: Story = {
36
+ render: () => (
37
+ <CheckboxGroup.Root defaultValue={['react']} orientation="horizontal">
38
+ <CheckboxGroup.Item value="react">React</CheckboxGroup.Item>
39
+ <CheckboxGroup.Item value="vue">Vue</CheckboxGroup.Item>
40
+ <CheckboxGroup.Item value="angular">Angular</CheckboxGroup.Item>
41
+ </CheckboxGroup.Root>
42
+ ),
43
+ play: async ({canvas}) => {
44
+ const group = canvas.getByRole('group')
45
+ await expect(group).toHaveAttribute('aria-orientation', 'horizontal')
46
+ },
47
+ }
48
+
49
+ export const Disabled: Story = {
50
+ render: () => (
51
+ <CheckboxGroup.Root defaultValue={['a']} disabled>
52
+ <CheckboxGroup.Item value="a">Option A</CheckboxGroup.Item>
53
+ <CheckboxGroup.Item value="b">Option B</CheckboxGroup.Item>
54
+ </CheckboxGroup.Root>
55
+ ),
56
+ play: async ({canvas}) => {
57
+ const checkboxes = canvas.getAllByRole('checkbox')
58
+ await expect(checkboxes[0]).toBeDisabled()
59
+ await expect(checkboxes[1]).toBeDisabled()
60
+ },
61
+ }
62
+
63
+ export const Controlled: Story = {
64
+ render: () => {
65
+ const [value, setValue] = useState(['read'])
66
+ return (
67
+ <html.div>
68
+ <CheckboxGroup.Root value={value} onValueChange={setValue}>
69
+ <CheckboxGroup.Item value="read">Read</CheckboxGroup.Item>
70
+ <CheckboxGroup.Item value="write">Write</CheckboxGroup.Item>
71
+ <CheckboxGroup.Item value="execute">Execute</CheckboxGroup.Item>
72
+ </CheckboxGroup.Root>
73
+ </html.div>
74
+ )
75
+ },
76
+ play: async ({canvas, userEvent}) => {
77
+ const checkboxes = canvas.getAllByRole('checkbox')
78
+ await expect(checkboxes[0]).toBeChecked()
79
+
80
+ await userEvent.click(checkboxes[1])
81
+ await expect(checkboxes[0]).toBeChecked()
82
+ await expect(checkboxes[1]).toBeChecked()
83
+
84
+ await userEvent.click(checkboxes[0])
85
+ await expect(checkboxes[0]).not.toBeChecked()
86
+ await expect(checkboxes[1]).toBeChecked()
87
+ },
88
+ }
@@ -0,0 +1,105 @@
1
+ import {type ReactNode, createContext, useContext, useCallback} from 'react'
2
+ import {html} from 'react-strict-dom'
3
+ import {useControllableValue} from '../../hooks/useControllableValue'
4
+ import {Checkbox} from '../Checkbox/Checkbox'
5
+ import {styles} from './styles.css'
6
+
7
+ // --- Context ---
8
+
9
+ interface CheckboxGroupContextValue {
10
+ value: string[]
11
+ onToggle: (itemValue: string, checked: boolean) => void
12
+ disabled: boolean
13
+ }
14
+
15
+ const CheckboxGroupContext = createContext<CheckboxGroupContextValue | null>(null)
16
+
17
+ function useCheckboxGroup() {
18
+ const ctx = useContext(CheckboxGroupContext)
19
+ if (!ctx) {
20
+ throw new Error('CheckboxGroup.Item must be used within CheckboxGroup.Root')
21
+ }
22
+ return ctx
23
+ }
24
+
25
+ // --- Root ---
26
+
27
+ interface RootProps {
28
+ value?: string[]
29
+ defaultValue?: string[]
30
+ onValueChange?: (value: string[]) => void
31
+ orientation?: 'horizontal' | 'vertical'
32
+ disabled?: boolean
33
+ children: ReactNode
34
+ }
35
+
36
+ function Root({
37
+ value: controlledValue,
38
+ defaultValue = [],
39
+ onValueChange,
40
+ orientation = 'vertical',
41
+ disabled = false,
42
+ children,
43
+ }: RootProps) {
44
+ const [value, setValue] = useControllableValue(controlledValue, defaultValue, onValueChange)
45
+
46
+ const onToggle = useCallback(
47
+ (itemValue: string, checked: boolean) => {
48
+ if (disabled) return
49
+ const next = checked
50
+ ? [...value, itemValue]
51
+ : value.filter((v) => v !== itemValue)
52
+ setValue(next)
53
+ },
54
+ [disabled, value, setValue],
55
+ )
56
+
57
+ return (
58
+ <CheckboxGroupContext.Provider value={{value, onToggle, disabled}}>
59
+ <html.div
60
+ role="group"
61
+ aria-orientation={orientation}
62
+ style={[styles.root, orientation === 'horizontal' && styles.rootHorizontal]}
63
+ >
64
+ {children}
65
+ </html.div>
66
+ </CheckboxGroupContext.Provider>
67
+ )
68
+ }
69
+
70
+ // --- Item ---
71
+
72
+ interface ItemProps {
73
+ value: string
74
+ disabled?: boolean
75
+ children: ReactNode
76
+ }
77
+
78
+ function Item({value, disabled: itemDisabled = false, children}: ItemProps) {
79
+ const {value: groupValue, onToggle, disabled: groupDisabled} = useCheckboxGroup()
80
+ const isChecked = groupValue.includes(value)
81
+ const isDisabled = groupDisabled || itemDisabled
82
+
83
+ const handleChange = useCallback(
84
+ (e: React.ChangeEvent<HTMLInputElement>) => {
85
+ onToggle(value, e.target.checked)
86
+ },
87
+ [value, onToggle],
88
+ )
89
+
90
+ return (
91
+ <Checkbox
92
+ value={value}
93
+ checked={isChecked}
94
+ disabled={isDisabled}
95
+ onChange={handleChange}
96
+ >
97
+ {children}
98
+ </Checkbox>
99
+ )
100
+ }
101
+
102
+ export const CheckboxGroup = {
103
+ Root,
104
+ Item,
105
+ }
@@ -0,0 +1,13 @@
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
+ root: {
6
+ display: 'flex',
7
+ flexDirection: 'column',
8
+ gap: spacing.sm,
9
+ },
10
+ rootHorizontal: {
11
+ flexDirection: 'row',
12
+ },
13
+ })
@@ -8,10 +8,7 @@ export const meta: ComponentMeta = {
8
8
  'Search results with no matches',
9
9
  'First-time use before any data exists',
10
10
  ],
11
- whenNotToUse: [
12
- 'Loading states — use Spinner',
13
- 'Error states — use Alert or Callout',
14
- ],
11
+ whenNotToUse: ['Loading states — use Spinner', 'Error states — use Alert or Callout'],
15
12
  example: `<Card>
16
13
  <EmptyState
17
14
  icon={<Icon name="info-circle" size={48} />}
@@ -8,12 +8,12 @@ export const meta: ComponentMeta = {
8
8
  'Status indicators alongside text',
9
9
  'Navigation or action indicators',
10
10
  ],
11
- whenNotToUse: [
12
- 'Decorative images — use img elements',
13
- 'Complex illustrations — use custom SVGs',
14
- ],
11
+ whenNotToUse: ['Decorative images — use img elements', 'Complex illustrations — use custom SVGs'],
15
12
  relatedTo: [
16
- {component: 'StatusIcon', relationship: 'StatusIcon wraps Icon with a colored background circle'},
13
+ {
14
+ component: 'StatusIcon',
15
+ relationship: 'StatusIcon wraps Icon with a colored background circle',
16
+ },
17
17
  ],
18
18
  example: `<Inline gap="sm" align="center">
19
19
  <Icon name="check-circle" size={20} />
@@ -18,7 +18,10 @@ export const meta: ComponentMeta = {
18
18
  },
19
19
  relatedTo: [
20
20
  {component: 'Input', relationship: 'InputGroup enhances Input with addons'},
21
- {component: 'Field', relationship: 'Field provides label/error; InputGroup provides visual addons'},
21
+ {
22
+ component: 'Field',
23
+ relationship: 'Field provides label/error; InputGroup provides visual addons',
24
+ },
22
25
  ],
23
26
  example: `<InputGroup.Root>
24
27
  <InputGroup.Addon>$</InputGroup.Addon>
@@ -18,9 +18,15 @@ export const meta: ComponentMeta = {
18
18
  optional: ['Description', 'Actions', 'Empty'],
19
19
  },
20
20
  relatedTo: [
21
- {component: 'Table', relationship: 'Table for wide multi-column data; List for narrow compact items'},
21
+ {
22
+ component: 'Table',
23
+ relationship: 'Table for wide multi-column data; List for narrow compact items',
24
+ },
22
25
  {component: 'DetailPanel', relationship: 'List fits naturally inside DetailPanel.Body'},
23
- {component: 'Menu', relationship: 'Menu for action menus; List for data display with selection'},
26
+ {
27
+ component: 'Menu',
28
+ relationship: 'Menu for action menus; List for data display with selection',
29
+ },
24
30
  ],
25
31
  example: `<List.Root selectionMode="multiple" aria-label="Certificates">
26
32
  <List.Item selected onClick={toggle}>