@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.
- package/dist/components/Alert/Alert.meta.d.ts.map +1 -1
- package/dist/components/ButtonGroup/ButtonGroup.d.ts +14 -0
- package/dist/components/ButtonGroup/ButtonGroup.d.ts.map +1 -0
- package/dist/components/ButtonGroup/ButtonGroup.meta.d.ts +3 -0
- package/dist/components/ButtonGroup/ButtonGroup.meta.d.ts.map +1 -0
- package/dist/components/ButtonGroup/ButtonGroup.stories.d.ts +11 -0
- package/dist/components/ButtonGroup/ButtonGroup.stories.d.ts.map +1 -0
- package/dist/components/ButtonGroup/styles.css.d.ts +45 -0
- package/dist/components/ButtonGroup/styles.css.d.ts.map +1 -0
- package/dist/components/Callout/Callout.meta.d.ts.map +1 -1
- package/dist/components/Card/Card.meta.d.ts.map +1 -1
- package/dist/components/CheckboxGroup/CheckboxGroup.d.ts +22 -0
- package/dist/components/CheckboxGroup/CheckboxGroup.d.ts.map +1 -0
- package/dist/components/CheckboxGroup/CheckboxGroup.meta.d.ts +3 -0
- package/dist/components/CheckboxGroup/CheckboxGroup.meta.d.ts.map +1 -0
- package/dist/components/CheckboxGroup/CheckboxGroup.stories.d.ts +9 -0
- package/dist/components/CheckboxGroup/CheckboxGroup.stories.d.ts.map +1 -0
- package/dist/components/CheckboxGroup/styles.css.d.ts +12 -0
- package/dist/components/CheckboxGroup/styles.css.d.ts.map +1 -0
- package/dist/components/EmptyState/EmptyState.meta.d.ts.map +1 -1
- package/dist/components/InputGroup/InputGroup.meta.d.ts.map +1 -1
- package/dist/components/List/List.d.ts.map +1 -1
- package/dist/components/List/List.meta.d.ts.map +1 -1
- package/dist/components/List/List.stories.d.ts.map +1 -1
- package/dist/components/RadioGroup/RadioGroup.d.ts +22 -0
- package/dist/components/RadioGroup/RadioGroup.d.ts.map +1 -0
- package/dist/components/RadioGroup/RadioGroup.meta.d.ts +3 -0
- package/dist/components/RadioGroup/RadioGroup.meta.d.ts.map +1 -0
- package/dist/components/RadioGroup/RadioGroup.stories.d.ts +9 -0
- package/dist/components/RadioGroup/RadioGroup.stories.d.ts.map +1 -0
- package/dist/components/RadioGroup/styles.css.d.ts +60 -0
- package/dist/components/RadioGroup/styles.css.d.ts.map +1 -0
- package/dist/components/StatusIcon/StatusIcon.meta.d.ts.map +1 -1
- package/dist/components/Table/ColumnFilter.d.ts +8 -0
- package/dist/components/Table/ColumnFilter.d.ts.map +1 -0
- package/dist/components/Table/Pagination.d.ts +7 -0
- package/dist/components/Table/Pagination.d.ts.map +1 -0
- package/dist/components/Table/SortIndicator.d.ts +16 -0
- package/dist/components/Table/SortIndicator.d.ts.map +1 -0
- package/dist/components/Table/Table.d.ts +10 -3
- package/dist/components/Table/Table.d.ts.map +1 -1
- package/dist/components/Table/Table.stories.d.ts +4 -0
- package/dist/components/Table/Table.stories.d.ts.map +1 -1
- package/dist/components/Table/styles.css.d.ts +12 -4
- package/dist/components/Table/styles.css.d.ts.map +1 -1
- package/dist/components/Table/useDataTable.d.ts +32 -0
- package/dist/components/Table/useDataTable.d.ts.map +1 -0
- package/dist/components/ThemeProvider/ThemeProvider.meta.d.ts.map +1 -1
- package/dist/index.css +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2399 -1927
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
- package/src/components/ActionBar/ActionBar.stories.tsx +3 -3
- package/src/components/Alert/Alert.meta.ts +4 -1
- package/src/components/ButtonGroup/ButtonGroup.meta.ts +24 -0
- package/src/components/ButtonGroup/ButtonGroup.stories.tsx +103 -0
- package/src/components/ButtonGroup/ButtonGroup.tsx +59 -0
- package/src/components/ButtonGroup/styles.css.ts +28 -0
- package/src/components/Callout/Callout.meta.ts +4 -1
- package/src/components/Card/Card.meta.ts +4 -1
- package/src/components/Checkbox/styles.css.ts +1 -1
- package/src/components/CheckboxGroup/CheckboxGroup.meta.ts +25 -0
- package/src/components/CheckboxGroup/CheckboxGroup.stories.tsx +88 -0
- package/src/components/CheckboxGroup/CheckboxGroup.tsx +105 -0
- package/src/components/CheckboxGroup/styles.css.ts +13 -0
- package/src/components/EmptyState/EmptyState.meta.ts +1 -4
- package/src/components/Icon/Icon.meta.ts +5 -5
- package/src/components/InputGroup/InputGroup.meta.ts +4 -1
- package/src/components/List/List.meta.ts +8 -2
- package/src/components/List/List.stories.tsx +35 -15
- package/src/components/List/List.tsx +1 -5
- package/src/components/RadioGroup/RadioGroup.meta.ts +25 -0
- package/src/components/RadioGroup/RadioGroup.stories.tsx +83 -0
- package/src/components/RadioGroup/RadioGroup.tsx +113 -0
- package/src/components/RadioGroup/styles.css.ts +66 -0
- package/src/components/StatusIcon/StatusIcon.meta.ts +1 -4
- package/src/components/Table/ColumnFilter.tsx +46 -0
- package/src/components/Table/Pagination.tsx +82 -0
- package/src/components/Table/SortIndicator.tsx +56 -0
- package/src/components/Table/Table.stories.tsx +355 -9
- package/src/components/Table/Table.tsx +62 -17
- package/src/components/Table/styles.css.ts +18 -8
- package/src/components/Table/useDataTable.ts +113 -0
- package/src/components/ThemeProvider/ThemeProvider.meta.ts +1 -3
- package/src/docs/helpers.tsx +2 -2
- package/src/index.ts +5 -0
- 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.
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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">
|
|
@@ -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
|
-
{
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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}>
|