@duro-app/ui 0.21.0 → 0.23.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 (96) hide show
  1. package/dist/components/ActionBar/ActionBar.stories.d.ts.map +1 -1
  2. package/dist/components/Alert/Alert.meta.d.ts.map +1 -1
  3. package/dist/components/Callout/Callout.meta.d.ts.map +1 -1
  4. package/dist/components/Card/Card.meta.d.ts.map +1 -1
  5. package/dist/components/CheckboxGroup/CheckboxGroup.d.ts +22 -0
  6. package/dist/components/CheckboxGroup/CheckboxGroup.d.ts.map +1 -0
  7. package/dist/components/CheckboxGroup/CheckboxGroup.meta.d.ts +3 -0
  8. package/dist/components/CheckboxGroup/CheckboxGroup.meta.d.ts.map +1 -0
  9. package/dist/components/CheckboxGroup/CheckboxGroup.stories.d.ts +9 -0
  10. package/dist/components/CheckboxGroup/CheckboxGroup.stories.d.ts.map +1 -0
  11. package/dist/components/CheckboxGroup/styles.css.d.ts +12 -0
  12. package/dist/components/CheckboxGroup/styles.css.d.ts.map +1 -0
  13. package/dist/components/DetailPanel/DetailPanel.d.ts.map +1 -1
  14. package/dist/components/DetailPanel/DetailPanel.meta.d.ts.map +1 -1
  15. package/dist/components/DetailPanel/DetailPanel.stories.d.ts.map +1 -1
  16. package/dist/components/DetailPanel/styles.css.d.ts +4 -0
  17. package/dist/components/DetailPanel/styles.css.d.ts.map +1 -1
  18. package/dist/components/Dialog/Dialog.meta.d.ts.map +1 -1
  19. package/dist/components/Drawer/Drawer.meta.d.ts.map +1 -1
  20. package/dist/components/EmptyState/EmptyState.meta.d.ts.map +1 -1
  21. package/dist/components/Field/Field.stories.d.ts.map +1 -1
  22. package/dist/components/InputGroup/InputGroup.meta.d.ts.map +1 -1
  23. package/dist/components/List/List.d.ts +46 -0
  24. package/dist/components/List/List.d.ts.map +1 -0
  25. package/dist/components/List/List.meta.d.ts +3 -0
  26. package/dist/components/List/List.meta.d.ts.map +1 -0
  27. package/dist/components/List/List.stories.d.ts +9 -0
  28. package/dist/components/List/List.stories.d.ts.map +1 -0
  29. package/dist/components/List/styles.css.d.ts +77 -0
  30. package/dist/components/List/styles.css.d.ts.map +1 -0
  31. package/dist/components/Panel/Panel.d.ts.map +1 -1
  32. package/dist/components/Panel/Panel.meta.d.ts.map +1 -1
  33. package/dist/components/Panel/Panel.stories.d.ts.map +1 -1
  34. package/dist/components/Panel/styles.css.d.ts +4 -0
  35. package/dist/components/Panel/styles.css.d.ts.map +1 -1
  36. package/dist/components/RadioGroup/RadioGroup.d.ts +22 -0
  37. package/dist/components/RadioGroup/RadioGroup.d.ts.map +1 -0
  38. package/dist/components/RadioGroup/RadioGroup.meta.d.ts +3 -0
  39. package/dist/components/RadioGroup/RadioGroup.meta.d.ts.map +1 -0
  40. package/dist/components/RadioGroup/RadioGroup.stories.d.ts +9 -0
  41. package/dist/components/RadioGroup/RadioGroup.stories.d.ts.map +1 -0
  42. package/dist/components/RadioGroup/styles.css.d.ts +60 -0
  43. package/dist/components/RadioGroup/styles.css.d.ts.map +1 -0
  44. package/dist/components/StatusIcon/StatusIcon.meta.d.ts.map +1 -1
  45. package/dist/components/Table/Table.d.ts +4 -3
  46. package/dist/components/Table/Table.d.ts.map +1 -1
  47. package/dist/components/Table/Table.stories.d.ts.map +1 -1
  48. package/dist/components/Table/styles.css.d.ts +1 -1
  49. package/dist/components/Tabs/Tabs.stories.d.ts.map +1 -1
  50. package/dist/components/ThemeProvider/ThemeProvider.meta.d.ts.map +1 -1
  51. package/dist/components/Tooltip/Tooltip.stories.d.ts.map +1 -1
  52. package/dist/index.css +1 -1
  53. package/dist/index.d.ts +3 -0
  54. package/dist/index.d.ts.map +1 -1
  55. package/dist/index.js +1771 -1386
  56. package/dist/index.js.map +1 -1
  57. package/package.json +2 -2
  58. package/src/components/ActionBar/ActionBar.stories.tsx +10 -0
  59. package/src/components/Alert/Alert.meta.ts +4 -1
  60. package/src/components/Callout/Callout.meta.ts +4 -1
  61. package/src/components/Card/Card.meta.ts +4 -1
  62. package/src/components/Checkbox/styles.css.ts +1 -1
  63. package/src/components/CheckboxGroup/CheckboxGroup.meta.ts +25 -0
  64. package/src/components/CheckboxGroup/CheckboxGroup.stories.tsx +88 -0
  65. package/src/components/CheckboxGroup/CheckboxGroup.tsx +105 -0
  66. package/src/components/CheckboxGroup/styles.css.ts +13 -0
  67. package/src/components/DetailPanel/DetailPanel.meta.ts +4 -1
  68. package/src/components/DetailPanel/DetailPanel.stories.tsx +54 -27
  69. package/src/components/DetailPanel/DetailPanel.tsx +3 -16
  70. package/src/components/DetailPanel/styles.css.ts +5 -0
  71. package/src/components/Dialog/Dialog.meta.ts +8 -2
  72. package/src/components/Drawer/Drawer.meta.ts +4 -1
  73. package/src/components/EmptyState/EmptyState.meta.ts +1 -4
  74. package/src/components/Field/Field.stories.tsx +6 -0
  75. package/src/components/Icon/Icon.meta.ts +5 -5
  76. package/src/components/InputGroup/InputGroup.meta.ts +4 -1
  77. package/src/components/List/List.meta.ts +39 -0
  78. package/src/components/List/List.stories.tsx +200 -0
  79. package/src/components/List/List.tsx +118 -0
  80. package/src/components/List/styles.css.ts +99 -0
  81. package/src/components/Panel/Panel.meta.ts +12 -3
  82. package/src/components/Panel/Panel.stories.tsx +17 -10
  83. package/src/components/Panel/Panel.tsx +2 -10
  84. package/src/components/Panel/styles.css.ts +5 -0
  85. package/src/components/RadioGroup/RadioGroup.meta.ts +25 -0
  86. package/src/components/RadioGroup/RadioGroup.stories.tsx +83 -0
  87. package/src/components/RadioGroup/RadioGroup.tsx +113 -0
  88. package/src/components/RadioGroup/styles.css.ts +66 -0
  89. package/src/components/StatusIcon/StatusIcon.meta.ts +1 -4
  90. package/src/components/Table/Table.stories.tsx +25 -9
  91. package/src/components/Table/Table.tsx +31 -15
  92. package/src/components/Table/styles.css.ts +2 -2
  93. package/src/components/Tabs/Tabs.stories.tsx +7 -0
  94. package/src/components/ThemeProvider/ThemeProvider.meta.ts +1 -3
  95. package/src/components/Tooltip/Tooltip.stories.tsx +15 -0
  96. package/src/index.ts +3 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@duro-app/ui",
3
- "version": "0.21.0",
3
+ "version": "0.23.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -52,7 +52,7 @@
52
52
  }
53
53
  },
54
54
  "dependencies": {
55
- "@duro-app/tokens": "^0.21.0"
55
+ "@duro-app/tokens": "^0.23.0"
56
56
  },
57
57
  "devDependencies": {
58
58
  "@babel/preset-typescript": "^7.28.0",
@@ -13,6 +13,16 @@ interface ActionBarStoryArgs {
13
13
 
14
14
  const meta: Meta<ActionBarStoryArgs> = {
15
15
  title: 'Components/ActionBar',
16
+ argTypes: {
17
+ selectedItemCount: {
18
+ control: 'number',
19
+ description: 'Number of selected items displayed in the action bar',
20
+ },
21
+ isEmphasized: {
22
+ control: 'boolean',
23
+ description: 'Whether the action bar uses the emphasized (inverse) style',
24
+ },
25
+ },
16
26
  }
17
27
 
18
28
  export default meta
@@ -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>
@@ -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
+ })
@@ -18,7 +18,10 @@ export const meta: ComponentMeta = {
18
18
  optional: ['Title', 'Footer', 'Close'],
19
19
  },
20
20
  relatedTo: [
21
- {component: 'Drawer', relationship: 'Drawer is modal with backdrop; DetailPanel is non-modal and in-flow'},
21
+ {
22
+ component: 'Drawer',
23
+ relationship: 'Drawer is modal with backdrop; DetailPanel is non-modal and in-flow',
24
+ },
22
25
  {component: 'Panel', relationship: 'DetailPanel uses Panel structure internally'},
23
26
  {component: 'Table', relationship: 'Common pattern: Table + DetailPanel for master-detail'},
24
27
  ],
@@ -2,6 +2,7 @@ import type {Meta, StoryObj} from '@storybook/react'
2
2
  import {useState} from 'react'
3
3
  import {css, html} from 'react-strict-dom'
4
4
  import {DetailPanel} from './DetailPanel'
5
+ import {Panel} from '../Panel/Panel'
5
6
  import {Button} from '../Button/Button'
6
7
  import {Text} from '../Text/Text'
7
8
  import {Heading} from '../Heading/Heading'
@@ -56,9 +57,8 @@ function DefaultDemo() {
56
57
  </DetailPanel.Header>
57
58
  <DetailPanel.Body>
58
59
  <Text>
59
- This is a non-modal detail panel. The content behind it
60
- remains interactive — you can still click, scroll, and
61
- interact with the main area.
60
+ This is a non-modal detail panel. The content behind it remains interactive — you can
61
+ still click, scroll, and interact with the main area.
62
62
  </Text>
63
63
  </DetailPanel.Body>
64
64
  <DetailPanel.Footer>
@@ -137,9 +137,15 @@ function TableSelectionDemo() {
137
137
  <Heading level={3}>Users</Heading>
138
138
  <html.div>
139
139
  <html.div style={tableStyles.headerRow}>
140
- <html.span style={tableStyles.cellText}><Text variant="label">Name</Text></html.span>
141
- <html.span style={tableStyles.cellText}><Text variant="label">Role</Text></html.span>
142
- <html.span style={tableStyles.cellText}><Text variant="label">Status</Text></html.span>
140
+ <html.span style={tableStyles.cellText}>
141
+ <Text variant="label">Name</Text>
142
+ </html.span>
143
+ <html.span style={tableStyles.cellText}>
144
+ <Text variant="label">Role</Text>
145
+ </html.span>
146
+ <html.span style={tableStyles.cellText}>
147
+ <Text variant="label">Status</Text>
148
+ </html.span>
143
149
  </html.div>
144
150
  {users.map((user) => (
145
151
  <html.div
@@ -147,9 +153,15 @@ function TableSelectionDemo() {
147
153
  style={[tableStyles.row, selectedId === user.id && tableStyles.rowSelected]}
148
154
  onClick={() => setSelectedId(selectedId === user.id ? null : user.id)}
149
155
  >
150
- <html.span style={tableStyles.cellText}><Text>{user.name}</Text></html.span>
151
- <html.span style={tableStyles.cellText}><Text>{user.role}</Text></html.span>
152
- <html.span style={tableStyles.cellText}><Text>{user.status}</Text></html.span>
156
+ <html.span style={tableStyles.cellText}>
157
+ <Text>{user.name}</Text>
158
+ </html.span>
159
+ <html.span style={tableStyles.cellText}>
160
+ <Text>{user.role}</Text>
161
+ </html.span>
162
+ <html.span style={tableStyles.cellText}>
163
+ <Text>{user.status}</Text>
164
+ </html.span>
153
165
  </html.div>
154
166
  ))}
155
167
  </html.div>
@@ -170,22 +182,37 @@ function TableSelectionDemo() {
170
182
  <DetailPanel.Body>
171
183
  {selectedUser && (
172
184
  <Stack gap="md">
173
- <Stack gap="xs">
174
- <Text variant="label">Email</Text>
175
- <Text variant="bodySm">{selectedUser.email}</Text>
176
- </Stack>
177
- <Stack gap="xs">
178
- <Text variant="label">Role</Text>
179
- <Text variant="bodySm">{selectedUser.role}</Text>
180
- </Stack>
181
- <Stack gap="xs">
182
- <Text variant="label">Status</Text>
183
- <Inline gap="sm">
184
- <Badge variant={selectedUser.status === 'Active' ? 'success' : 'default'}>
185
- {selectedUser.status}
186
- </Badge>
187
- </Inline>
188
- </Stack>
185
+ <Panel.Root>
186
+ <Panel.Header>
187
+ <Text variant="label">Contact</Text>
188
+ </Panel.Header>
189
+ <Panel.Body>
190
+ <Stack gap="xs">
191
+ <Text variant="caption">Email</Text>
192
+ <Text variant="bodySm">{selectedUser.email}</Text>
193
+ </Stack>
194
+ </Panel.Body>
195
+ </Panel.Root>
196
+
197
+ <Panel.Root>
198
+ <Panel.Header>
199
+ <Text variant="label">Role &amp; Status</Text>
200
+ </Panel.Header>
201
+ <Panel.Body>
202
+ <Stack gap="sm">
203
+ <Inline gap="sm" align="center">
204
+ <Text variant="caption">Role</Text>
205
+ <Text variant="bodySm">{selectedUser.role}</Text>
206
+ </Inline>
207
+ <Inline gap="sm" align="center">
208
+ <Text variant="caption">Status</Text>
209
+ <Badge variant={selectedUser.status === 'Active' ? 'success' : 'default'}>
210
+ {selectedUser.status}
211
+ </Badge>
212
+ </Inline>
213
+ </Stack>
214
+ </Panel.Body>
215
+ </Panel.Root>
189
216
  </Stack>
190
217
  )}
191
218
  </DetailPanel.Body>
@@ -225,8 +252,8 @@ function MediumSizeDemo() {
225
252
  </DetailPanel.Header>
226
253
  <DetailPanel.Body>
227
254
  <Text>
228
- This panel is 480px wide, providing more room for complex
229
- content like forms or detailed information.
255
+ This panel is 480px wide, providing more room for complex content like forms or
256
+ detailed information.
230
257
  </Text>
231
258
  </DetailPanel.Body>
232
259
  </DetailPanel.Content>
@@ -194,12 +194,7 @@ function Content({children, size = 'sm', label}: ContentProps) {
194
194
  if (!present) return null
195
195
 
196
196
  return (
197
- <html.div
198
- style={[
199
- styles.wrapper,
200
- closing ? wrapperCloseMap[size] : wrapperOpenMap[size],
201
- ]}
202
- >
197
+ <html.div style={[styles.wrapper, closing ? wrapperCloseMap[size] : wrapperOpenMap[size]]}>
203
198
  <html.div
204
199
  ref={panelRef}
205
200
  role="complementary"
@@ -250,11 +245,7 @@ interface BodyProps {
250
245
  }
251
246
 
252
247
  function Body({children, padded = true}: BodyProps) {
253
- return (
254
- <html.div style={[styles.body, padded && styles.bodyPadded]}>
255
- {children}
256
- </html.div>
257
- )
248
+ return <html.div style={[styles.body, padded && styles.bodyPadded]}>{children}</html.div>
258
249
  }
259
250
 
260
251
  // --- Footer ---
@@ -282,11 +273,7 @@ function Close({children, 'aria-label': ariaLabel = 'Close'}: CloseProps) {
282
273
  }
283
274
 
284
275
  return (
285
- <html.button
286
- onClick={requestClose}
287
- aria-label={ariaLabel}
288
- style={styles.closeButton}
289
- >
276
+ <html.button onClick={requestClose} aria-label={ariaLabel} style={styles.closeButton}>
290
277
  <svg width={16} height={16} viewBox="0 0 16 16" fill="none">
291
278
  <path
292
279
  d="M4 4l8 8M12 4l-8 8"
@@ -137,6 +137,11 @@ export const styles = css.create({
137
137
  body: {
138
138
  flex: 1,
139
139
  overflowY: 'auto',
140
+ // Minimal inset prevents child borders from touching panel structural borders
141
+ paddingTop: spacing.sm,
142
+ paddingBottom: spacing.sm,
143
+ paddingLeft: spacing.sm,
144
+ paddingRight: spacing.sm,
140
145
  },
141
146
  bodyPadded: {
142
147
  paddingTop: spacing.lg,
@@ -20,8 +20,14 @@ export const meta: ComponentMeta = {
20
20
  optional: ['Trigger', 'Header', 'Title', 'Description', 'Body', 'Footer', 'Close'],
21
21
  },
22
22
  relatedTo: [
23
- {component: 'Drawer', relationship: 'Drawer slides from an edge; Dialog is centered with scale animation'},
24
- {component: 'DetailPanel', relationship: 'DetailPanel is non-modal and in-flow; Dialog is modal with backdrop'},
23
+ {
24
+ component: 'Drawer',
25
+ relationship: 'Drawer slides from an edge; Dialog is centered with scale animation',
26
+ },
27
+ {
28
+ component: 'DetailPanel',
29
+ relationship: 'DetailPanel is non-modal and in-flow; Dialog is modal with backdrop',
30
+ },
25
31
  ],
26
32
  example: `<Dialog.Root open={open} onOpenChange={setOpen}>
27
33
  <Dialog.Trigger>
@@ -21,7 +21,10 @@ export const meta: ComponentMeta = {
21
21
  },
22
22
  relatedTo: [
23
23
  {component: 'Dialog', relationship: 'Dialog is centered modal; Drawer slides from an edge'},
24
- {component: 'DetailPanel', relationship: 'DetailPanel is non-modal and in-flow; Drawer is modal with backdrop'},
24
+ {
25
+ component: 'DetailPanel',
26
+ relationship: 'DetailPanel is non-modal and in-flow; Drawer is modal with backdrop',
27
+ },
25
28
  ],
26
29
  example: `<Drawer.Root anchor="right" open={open} onOpenChange={setOpen}>
27
30
  <Drawer.Trigger>
@@ -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} />}
@@ -6,6 +6,12 @@ import {Input} from '../Input/Input'
6
6
 
7
7
  const meta: Meta = {
8
8
  title: 'Components/Field',
9
+ argTypes: {
10
+ invalid: {
11
+ control: 'boolean',
12
+ description: 'Whether the field is in an invalid/error state',
13
+ },
14
+ },
9
15
  }
10
16
 
11
17
  export default meta
@@ -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>
@@ -0,0 +1,39 @@
1
+ import type {ComponentMeta} from '../component-meta'
2
+
3
+ export const meta: ComponentMeta = {
4
+ description:
5
+ 'Vertical list of interactive items. Ideal for narrow spaces (detail panels, sidebars) where Table would truncate. Supports selection, descriptions, and action slots.',
6
+ whenToUse: [
7
+ 'Displaying items in a narrow container (< 480px)',
8
+ 'Detail panels, drawers, and sidebars',
9
+ 'Lists needing selection (single or multiple) with compact layout',
10
+ ],
11
+ whenNotToUse: [
12
+ 'Tabular data with many columns — use Table',
13
+ 'Navigation menus — use SideNav or Menu',
14
+ 'Simple text lists without interaction — use Stack with Text',
15
+ ],
16
+ anatomy: {
17
+ required: ['Root', 'Item', 'Content', 'Text'],
18
+ optional: ['Description', 'Actions', 'Empty'],
19
+ },
20
+ relatedTo: [
21
+ {component: 'Table', relationship: 'Table for wide multi-column data; List for narrow compact items'},
22
+ {component: 'DetailPanel', relationship: 'List fits naturally inside DetailPanel.Body'},
23
+ {component: 'Menu', relationship: 'Menu for action menus; List for data display with selection'},
24
+ ],
25
+ example: `<List.Root selectionMode="multiple" aria-label="Certificates">
26
+ <List.Item selected onClick={toggle}>
27
+ <Checkbox checked />
28
+ <List.Content>
29
+ <List.Text>web-server-prod-2024.pem</List.Text>
30
+ <List.Description>Issued: Jan 15, 2024 · Expires: Jan 15, 2025</List.Description>
31
+ </List.Content>
32
+ <List.Actions>
33
+ <Badge variant="success">Active</Badge>
34
+ <Button variant="danger" size="small">Revoke</Button>
35
+ </List.Actions>
36
+ </List.Item>
37
+ <List.Empty>No certificates found.</List.Empty>
38
+ </List.Root>`,
39
+ }