@duro-app/ui 0.20.1 → 0.22.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/ActionBar/ActionBar.meta.d.ts +3 -0
- package/dist/components/ActionBar/ActionBar.meta.d.ts.map +1 -0
- package/dist/components/ActionBar/ActionBar.stories.d.ts.map +1 -1
- package/dist/components/Alert/Alert.meta.d.ts +3 -0
- package/dist/components/Alert/Alert.meta.d.ts.map +1 -0
- package/dist/components/Badge/Badge.meta.d.ts +3 -0
- package/dist/components/Badge/Badge.meta.d.ts.map +1 -0
- package/dist/components/Callout/Callout.meta.d.ts +3 -0
- package/dist/components/Callout/Callout.meta.d.ts.map +1 -0
- package/dist/components/Card/Card.meta.d.ts +3 -0
- package/dist/components/Card/Card.meta.d.ts.map +1 -0
- package/dist/components/DetailPanel/DetailPanel.d.ts.map +1 -1
- package/dist/components/DetailPanel/DetailPanel.meta.d.ts.map +1 -1
- package/dist/components/DetailPanel/DetailPanel.stories.d.ts.map +1 -1
- package/dist/components/DetailPanel/styles.css.d.ts +4 -0
- package/dist/components/DetailPanel/styles.css.d.ts.map +1 -1
- package/dist/components/Dialog/Dialog.meta.d.ts.map +1 -1
- package/dist/components/Drawer/Drawer.meta.d.ts.map +1 -1
- package/dist/components/EmptyState/EmptyState.meta.d.ts +3 -0
- package/dist/components/EmptyState/EmptyState.meta.d.ts.map +1 -0
- package/dist/components/Field/Field.stories.d.ts.map +1 -1
- package/dist/components/Heading/Heading.meta.d.ts +3 -0
- package/dist/components/Heading/Heading.meta.d.ts.map +1 -0
- package/dist/components/Icon/Icon.meta.d.ts +3 -0
- package/dist/components/Icon/Icon.meta.d.ts.map +1 -0
- package/dist/components/InputGroup/InputGroup.meta.d.ts +3 -0
- package/dist/components/InputGroup/InputGroup.meta.d.ts.map +1 -0
- package/dist/components/List/List.d.ts +46 -0
- package/dist/components/List/List.d.ts.map +1 -0
- package/dist/components/List/List.meta.d.ts +3 -0
- package/dist/components/List/List.meta.d.ts.map +1 -0
- package/dist/components/List/List.stories.d.ts +9 -0
- package/dist/components/List/List.stories.d.ts.map +1 -0
- package/dist/components/List/styles.css.d.ts +77 -0
- package/dist/components/List/styles.css.d.ts.map +1 -0
- package/dist/components/Panel/Panel.d.ts.map +1 -1
- package/dist/components/Panel/Panel.meta.d.ts.map +1 -1
- package/dist/components/Panel/Panel.stories.d.ts.map +1 -1
- package/dist/components/Panel/styles.css.d.ts +4 -0
- package/dist/components/Panel/styles.css.d.ts.map +1 -1
- package/dist/components/Spinner/Spinner.meta.d.ts +3 -0
- package/dist/components/Spinner/Spinner.meta.d.ts.map +1 -0
- package/dist/components/StatusIcon/StatusIcon.meta.d.ts +3 -0
- package/dist/components/StatusIcon/StatusIcon.meta.d.ts.map +1 -0
- package/dist/components/Table/Table.stories.d.ts.map +1 -1
- package/dist/components/Tabs/Tabs.stories.d.ts.map +1 -1
- package/dist/components/Text/Text.meta.d.ts +3 -0
- package/dist/components/Text/Text.meta.d.ts.map +1 -0
- package/dist/components/ThemeProvider/ThemeProvider.meta.d.ts +3 -0
- package/dist/components/ThemeProvider/ThemeProvider.meta.d.ts.map +1 -0
- package/dist/components/Toggle/styles.css.d.ts +1 -1
- package/dist/components/Toggle/styles.css.d.ts.map +1 -1
- package/dist/components/Tooltip/Tooltip.stories.d.ts.map +1 -1
- package/dist/index.css +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1386 -1219
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/components/ActionBar/ActionBar.meta.ts +20 -0
- package/src/components/ActionBar/ActionBar.stories.tsx +10 -0
- package/src/components/Alert/Alert.meta.ts +25 -0
- package/src/components/Badge/Badge.meta.ts +20 -0
- package/src/components/Callout/Callout.meta.ts +22 -0
- package/src/components/Card/Card.meta.ts +25 -0
- package/src/components/DetailPanel/DetailPanel.meta.ts +4 -1
- package/src/components/DetailPanel/DetailPanel.stories.tsx +54 -27
- package/src/components/DetailPanel/DetailPanel.tsx +3 -16
- package/src/components/DetailPanel/styles.css.ts +5 -0
- package/src/components/Dialog/Dialog.meta.ts +8 -2
- package/src/components/Drawer/Drawer.meta.ts +4 -1
- package/src/components/EmptyState/EmptyState.meta.ts +22 -0
- package/src/components/Field/Field.stories.tsx +6 -0
- package/src/components/Heading/Heading.meta.ts +24 -0
- package/src/components/Icon/Icon.meta.ts +23 -0
- package/src/components/InputGroup/InputGroup.meta.ts +28 -0
- package/src/components/List/List.meta.ts +39 -0
- package/src/components/List/List.stories.tsx +200 -0
- package/src/components/List/List.tsx +118 -0
- package/src/components/List/styles.css.ts +99 -0
- package/src/components/Panel/Panel.meta.ts +12 -3
- package/src/components/Panel/Panel.stories.tsx +17 -10
- package/src/components/Panel/Panel.tsx +2 -10
- package/src/components/Panel/styles.css.ts +5 -0
- package/src/components/Spinner/Spinner.meta.ts +19 -0
- package/src/components/StatusIcon/StatusIcon.meta.ts +24 -0
- package/src/components/Table/Table.stories.tsx +16 -0
- package/src/components/Tabs/Tabs.stories.tsx +7 -0
- package/src/components/Text/Text.meta.ts +23 -0
- package/src/components/ThemeProvider/ThemeProvider.meta.ts +16 -0
- package/src/components/Toggle/styles.css.ts +2 -1
- package/src/components/Tooltip/Tooltip.stories.tsx +15 -0
- package/src/index.ts +1 -0
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import type {Meta, StoryObj} from '@storybook/react'
|
|
2
|
+
import {useState} from 'react'
|
|
3
|
+
import {expect} from 'storybook/test'
|
|
4
|
+
import {css, html} from 'react-strict-dom'
|
|
5
|
+
import {List} from './List'
|
|
6
|
+
import {Badge} from '../Badge/Badge'
|
|
7
|
+
import {Button} from '../Button/Button'
|
|
8
|
+
import {Checkbox} from '../Checkbox/Checkbox'
|
|
9
|
+
import {colors} from '@duro-app/tokens/tokens/colors.css'
|
|
10
|
+
|
|
11
|
+
const meta: Meta = {
|
|
12
|
+
title: 'Components/List',
|
|
13
|
+
argTypes: {
|
|
14
|
+
selectionMode: {
|
|
15
|
+
control: 'select',
|
|
16
|
+
options: ['none', 'single', 'multiple'],
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export default meta
|
|
22
|
+
type Story = StoryObj
|
|
23
|
+
|
|
24
|
+
// --- Default ---
|
|
25
|
+
|
|
26
|
+
export const Default: Story = {
|
|
27
|
+
render: () => (
|
|
28
|
+
<List.Root aria-label="Services">
|
|
29
|
+
<List.Item>
|
|
30
|
+
<List.Content>
|
|
31
|
+
<List.Text>Traefik</List.Text>
|
|
32
|
+
<List.Description>Reverse proxy · Port 443</List.Description>
|
|
33
|
+
</List.Content>
|
|
34
|
+
<List.Actions>
|
|
35
|
+
<Badge variant="success" size="sm">Running</Badge>
|
|
36
|
+
</List.Actions>
|
|
37
|
+
</List.Item>
|
|
38
|
+
<List.Item>
|
|
39
|
+
<List.Content>
|
|
40
|
+
<List.Text>Nginx</List.Text>
|
|
41
|
+
<List.Description>Web server · Port 80</List.Description>
|
|
42
|
+
</List.Content>
|
|
43
|
+
<List.Actions>
|
|
44
|
+
<Badge variant="success" size="sm">Running</Badge>
|
|
45
|
+
</List.Actions>
|
|
46
|
+
</List.Item>
|
|
47
|
+
<List.Item>
|
|
48
|
+
<List.Content>
|
|
49
|
+
<List.Text>Pi-hole</List.Text>
|
|
50
|
+
<List.Description>DNS filter · Port 53</List.Description>
|
|
51
|
+
</List.Content>
|
|
52
|
+
<List.Actions>
|
|
53
|
+
<Badge variant="default" size="sm">Stopped</Badge>
|
|
54
|
+
</List.Actions>
|
|
55
|
+
</List.Item>
|
|
56
|
+
</List.Root>
|
|
57
|
+
),
|
|
58
|
+
play: async ({canvas}) => {
|
|
59
|
+
const list = canvas.getByRole('list')
|
|
60
|
+
await expect(list).toBeInTheDocument()
|
|
61
|
+
await expect(canvas.getAllByRole('listitem').length).toBe(3)
|
|
62
|
+
},
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// --- With Selection ---
|
|
66
|
+
|
|
67
|
+
const narrowContainer = css.create({
|
|
68
|
+
box: {
|
|
69
|
+
maxWidth: 360,
|
|
70
|
+
borderWidth: 1,
|
|
71
|
+
borderStyle: 'solid',
|
|
72
|
+
borderColor: colors.border,
|
|
73
|
+
borderRadius: 8,
|
|
74
|
+
overflow: 'hidden',
|
|
75
|
+
},
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
function SelectionDemo() {
|
|
79
|
+
const certs = [
|
|
80
|
+
{id: 'c1', serial: 'aa:bb:cc:dd:01', issued: 'Mar 21, 2026', expires: 'Jun 19, 2026', status: 'active' as const},
|
|
81
|
+
{id: 'c2', serial: 'aa:bb:cc:dd:02', issued: 'Mar 21, 2026', expires: 'Jun 19, 2026', status: 'active' as const},
|
|
82
|
+
{id: 'c3', serial: 'ee:ff:00:11:03', issued: 'Jan 10, 2025', expires: 'Jan 10, 2026', status: 'expired' as const},
|
|
83
|
+
]
|
|
84
|
+
|
|
85
|
+
const [selected, setSelected] = useState<Set<string>>(new Set())
|
|
86
|
+
|
|
87
|
+
const toggle = (id: string) => {
|
|
88
|
+
setSelected((prev) => {
|
|
89
|
+
const next = new Set(prev)
|
|
90
|
+
if (next.has(id)) next.delete(id)
|
|
91
|
+
else next.add(id)
|
|
92
|
+
return next
|
|
93
|
+
})
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<html.div style={narrowContainer.box}>
|
|
98
|
+
<List.Root selectionMode="multiple" aria-label="Certificates">
|
|
99
|
+
{certs.map((cert) => (
|
|
100
|
+
<List.Item
|
|
101
|
+
key={cert.id}
|
|
102
|
+
selected={selected.has(cert.id)}
|
|
103
|
+
onClick={() => cert.status === 'active' && toggle(cert.id)}
|
|
104
|
+
disabled={cert.status !== 'active'}
|
|
105
|
+
>
|
|
106
|
+
{cert.status === 'active' && (
|
|
107
|
+
<Checkbox
|
|
108
|
+
checked={selected.has(cert.id)}
|
|
109
|
+
onChange={() => toggle(cert.id)}
|
|
110
|
+
aria-label={cert.serial}
|
|
111
|
+
/>
|
|
112
|
+
)}
|
|
113
|
+
<List.Content>
|
|
114
|
+
<List.Text>{cert.serial}</List.Text>
|
|
115
|
+
<List.Description>
|
|
116
|
+
Issued: {cert.issued} · Expires: {cert.expires}
|
|
117
|
+
</List.Description>
|
|
118
|
+
</List.Content>
|
|
119
|
+
<List.Actions>
|
|
120
|
+
<Badge
|
|
121
|
+
variant={cert.status === 'active' ? 'success' : 'default'}
|
|
122
|
+
size="sm"
|
|
123
|
+
>
|
|
124
|
+
{cert.status}
|
|
125
|
+
</Badge>
|
|
126
|
+
{cert.status === 'active' && (
|
|
127
|
+
<Button variant="danger" size="small">
|
|
128
|
+
Revoke
|
|
129
|
+
</Button>
|
|
130
|
+
)}
|
|
131
|
+
</List.Actions>
|
|
132
|
+
</List.Item>
|
|
133
|
+
))}
|
|
134
|
+
</List.Root>
|
|
135
|
+
</html.div>
|
|
136
|
+
)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export const WithSelection: Story = {
|
|
140
|
+
render: () => <SelectionDemo />,
|
|
141
|
+
play: async ({canvas}) => {
|
|
142
|
+
const checkboxes = canvas.getAllByRole('checkbox')
|
|
143
|
+
await expect(checkboxes.length).toBe(2)
|
|
144
|
+
},
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// --- Empty State ---
|
|
148
|
+
|
|
149
|
+
export const EmptyState: Story = {
|
|
150
|
+
render: () => (
|
|
151
|
+
<html.div style={narrowContainer.box}>
|
|
152
|
+
<List.Root aria-label="Empty list">
|
|
153
|
+
<List.Empty>No items found.</List.Empty>
|
|
154
|
+
</List.Root>
|
|
155
|
+
</html.div>
|
|
156
|
+
),
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// --- In Detail Panel context (360px) ---
|
|
160
|
+
|
|
161
|
+
function DetailPanelDemo() {
|
|
162
|
+
const users = [
|
|
163
|
+
{id: '1', name: 'Alice Johnson', email: 'alice@example.com', role: 'Admin', status: 'Active'},
|
|
164
|
+
{id: '2', name: 'Bob Smith', email: 'bob@example.com', role: 'Editor', status: 'Active'},
|
|
165
|
+
{id: '3', name: 'Carol Davis', email: 'carol@example.com', role: 'Viewer', status: 'Inactive'},
|
|
166
|
+
]
|
|
167
|
+
|
|
168
|
+
const [selectedId, setSelectedId] = useState<string | null>(null)
|
|
169
|
+
|
|
170
|
+
return (
|
|
171
|
+
<html.div style={narrowContainer.box}>
|
|
172
|
+
<List.Root selectionMode="single" aria-label="Users">
|
|
173
|
+
{users.map((user) => (
|
|
174
|
+
<List.Item
|
|
175
|
+
key={user.id}
|
|
176
|
+
selected={selectedId === user.id}
|
|
177
|
+
onClick={() => setSelectedId(selectedId === user.id ? null : user.id)}
|
|
178
|
+
>
|
|
179
|
+
<List.Content>
|
|
180
|
+
<List.Text>{user.name}</List.Text>
|
|
181
|
+
<List.Description>{user.email} · {user.role}</List.Description>
|
|
182
|
+
</List.Content>
|
|
183
|
+
<List.Actions>
|
|
184
|
+
<Badge
|
|
185
|
+
variant={user.status === 'Active' ? 'success' : 'default'}
|
|
186
|
+
size="sm"
|
|
187
|
+
>
|
|
188
|
+
{user.status}
|
|
189
|
+
</Badge>
|
|
190
|
+
</List.Actions>
|
|
191
|
+
</List.Item>
|
|
192
|
+
))}
|
|
193
|
+
</List.Root>
|
|
194
|
+
</html.div>
|
|
195
|
+
)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export const SingleSelection: Story = {
|
|
199
|
+
render: () => <DetailPanelDemo />,
|
|
200
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import {type ReactNode, createContext, useContext} from 'react'
|
|
2
|
+
import {html} from 'react-strict-dom'
|
|
3
|
+
import {styles} from './styles.css'
|
|
4
|
+
|
|
5
|
+
// --- Types ---
|
|
6
|
+
|
|
7
|
+
export type ListSelectionMode = 'none' | 'single' | 'multiple'
|
|
8
|
+
|
|
9
|
+
// --- Context ---
|
|
10
|
+
|
|
11
|
+
interface ListContextValue {
|
|
12
|
+
selectionMode: ListSelectionMode
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const ListContext = createContext<ListContextValue>({selectionMode: 'none'})
|
|
16
|
+
|
|
17
|
+
function useList() {
|
|
18
|
+
return useContext(ListContext)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// --- Root ---
|
|
22
|
+
|
|
23
|
+
interface RootProps {
|
|
24
|
+
children: ReactNode
|
|
25
|
+
/** Selection mode: 'none' (default), 'single', or 'multiple' */
|
|
26
|
+
selectionMode?: ListSelectionMode
|
|
27
|
+
/** Accessible label for the list */
|
|
28
|
+
'aria-label'?: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function Root({children, selectionMode = 'none', 'aria-label': ariaLabel}: RootProps) {
|
|
32
|
+
return (
|
|
33
|
+
<ListContext.Provider value={{selectionMode}}>
|
|
34
|
+
<html.div role="list" aria-label={ariaLabel} style={styles.root}>
|
|
35
|
+
{children}
|
|
36
|
+
</html.div>
|
|
37
|
+
</ListContext.Provider>
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// --- Item ---
|
|
42
|
+
|
|
43
|
+
interface ItemProps {
|
|
44
|
+
children: ReactNode
|
|
45
|
+
/** Whether this item is selected */
|
|
46
|
+
selected?: boolean
|
|
47
|
+
/** Whether this item is disabled */
|
|
48
|
+
disabled?: boolean
|
|
49
|
+
/** Click handler for the item */
|
|
50
|
+
onClick?: () => void
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function Item({children, selected, disabled, onClick}: ItemProps) {
|
|
54
|
+
const {selectionMode} = useList()
|
|
55
|
+
const isSelectable = selectionMode !== 'none'
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<html.div
|
|
59
|
+
role="listitem"
|
|
60
|
+
aria-selected={isSelectable ? selected : undefined}
|
|
61
|
+
aria-disabled={disabled || undefined}
|
|
62
|
+
onClick={disabled ? undefined : onClick}
|
|
63
|
+
style={[
|
|
64
|
+
styles.item,
|
|
65
|
+
selected && styles.itemSelected,
|
|
66
|
+
disabled && styles.itemDisabled,
|
|
67
|
+
]}
|
|
68
|
+
>
|
|
69
|
+
{children}
|
|
70
|
+
</html.div>
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// --- Content: wraps primary text + description ---
|
|
75
|
+
|
|
76
|
+
function Content({children}: {children: ReactNode}) {
|
|
77
|
+
return <html.div style={styles.content}>{children}</html.div>
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// --- Text: primary label ---
|
|
81
|
+
|
|
82
|
+
function ItemText({children}: {children: ReactNode}) {
|
|
83
|
+
return <html.p style={styles.text}>{children}</html.p>
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// --- Description: secondary text ---
|
|
87
|
+
|
|
88
|
+
function Description({children}: {children: ReactNode}) {
|
|
89
|
+
return <html.p style={styles.description}>{children}</html.p>
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// --- Actions: slot for buttons, badges, etc. ---
|
|
93
|
+
|
|
94
|
+
function Actions({children}: {children: ReactNode}) {
|
|
95
|
+
return <html.div style={styles.actions}>{children}</html.div>
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// --- Empty: placeholder when list has no items ---
|
|
99
|
+
|
|
100
|
+
function Empty({children}: {children: ReactNode}) {
|
|
101
|
+
return (
|
|
102
|
+
<html.div role="listitem" style={styles.empty}>
|
|
103
|
+
{children}
|
|
104
|
+
</html.div>
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// --- Export ---
|
|
109
|
+
|
|
110
|
+
export const List = {
|
|
111
|
+
Root,
|
|
112
|
+
Item,
|
|
113
|
+
Content,
|
|
114
|
+
Text: ItemText,
|
|
115
|
+
Description,
|
|
116
|
+
Actions,
|
|
117
|
+
Empty,
|
|
118
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import {css} from 'react-strict-dom'
|
|
2
|
+
import {colors} from '@duro-app/tokens/tokens/colors.css'
|
|
3
|
+
import {spacing} from '@duro-app/tokens/tokens/spacing.css'
|
|
4
|
+
import {typography} from '@duro-app/tokens/tokens/typography.css'
|
|
5
|
+
|
|
6
|
+
export const styles = css.create({
|
|
7
|
+
// --- Root ---
|
|
8
|
+
root: {
|
|
9
|
+
display: 'flex',
|
|
10
|
+
flexDirection: 'column',
|
|
11
|
+
margin: 0,
|
|
12
|
+
padding: 0,
|
|
13
|
+
},
|
|
14
|
+
|
|
15
|
+
// --- Item ---
|
|
16
|
+
item: {
|
|
17
|
+
display: 'flex',
|
|
18
|
+
alignItems: 'center',
|
|
19
|
+
gap: spacing.sm,
|
|
20
|
+
paddingTop: spacing.sm,
|
|
21
|
+
paddingBottom: spacing.sm,
|
|
22
|
+
paddingLeft: spacing.ms,
|
|
23
|
+
paddingRight: spacing.ms,
|
|
24
|
+
borderBottomWidth: 1,
|
|
25
|
+
borderBottomStyle: 'solid',
|
|
26
|
+
borderBottomColor: colors.border,
|
|
27
|
+
backgroundColor: {
|
|
28
|
+
default: 'transparent',
|
|
29
|
+
':hover': colors.bgCardHover,
|
|
30
|
+
},
|
|
31
|
+
transitionProperty: 'background-color',
|
|
32
|
+
transitionDuration: '120ms',
|
|
33
|
+
cursor: 'default',
|
|
34
|
+
},
|
|
35
|
+
itemSelected: {
|
|
36
|
+
backgroundColor: {
|
|
37
|
+
default: colors.bgCardHover,
|
|
38
|
+
':hover': colors.bgCardHover,
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
itemDisabled: {
|
|
42
|
+
opacity: 0.5,
|
|
43
|
+
pointerEvents: 'none',
|
|
44
|
+
},
|
|
45
|
+
itemLast: {
|
|
46
|
+
borderBottomWidth: 0,
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
// --- Item content area (text + description) ---
|
|
50
|
+
content: {
|
|
51
|
+
flex: 1,
|
|
52
|
+
display: 'flex',
|
|
53
|
+
flexDirection: 'column',
|
|
54
|
+
gap: 2,
|
|
55
|
+
minWidth: 0,
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
// --- Primary text ---
|
|
59
|
+
text: {
|
|
60
|
+
fontSize: typography.fontSizeSm,
|
|
61
|
+
fontWeight: typography.fontWeightMedium,
|
|
62
|
+
lineHeight: typography.lineHeight,
|
|
63
|
+
color: colors.text,
|
|
64
|
+
margin: 0,
|
|
65
|
+
overflow: 'hidden',
|
|
66
|
+
textOverflow: 'ellipsis',
|
|
67
|
+
whiteSpace: 'nowrap',
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
// --- Description text ---
|
|
71
|
+
description: {
|
|
72
|
+
fontSize: typography.fontSizeXs,
|
|
73
|
+
lineHeight: typography.lineHeight,
|
|
74
|
+
color: colors.textMuted,
|
|
75
|
+
margin: 0,
|
|
76
|
+
overflow: 'hidden',
|
|
77
|
+
textOverflow: 'ellipsis',
|
|
78
|
+
whiteSpace: 'nowrap',
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
// --- Actions slot ---
|
|
82
|
+
actions: {
|
|
83
|
+
display: 'flex',
|
|
84
|
+
alignItems: 'center',
|
|
85
|
+
gap: spacing.xs,
|
|
86
|
+
flexShrink: 0,
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
// --- Empty state ---
|
|
90
|
+
empty: {
|
|
91
|
+
display: 'flex',
|
|
92
|
+
alignItems: 'center',
|
|
93
|
+
justifyContent: 'center',
|
|
94
|
+
paddingTop: spacing.xl,
|
|
95
|
+
paddingBottom: spacing.xl,
|
|
96
|
+
color: colors.textMuted,
|
|
97
|
+
fontSize: typography.fontSizeSm,
|
|
98
|
+
},
|
|
99
|
+
})
|
|
@@ -18,9 +18,18 @@ export const meta: ComponentMeta = {
|
|
|
18
18
|
optional: ['Root', 'Header', 'Body', 'Footer'],
|
|
19
19
|
},
|
|
20
20
|
relatedTo: [
|
|
21
|
-
{
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
{
|
|
22
|
+
component: 'Card',
|
|
23
|
+
relationship: 'Card is for shallow, repeated items; Panel is for deep, organized content',
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
component: 'DetailPanel',
|
|
27
|
+
relationship: 'DetailPanel uses Panel internally for its structural layout',
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
component: 'PageShell',
|
|
31
|
+
relationship: 'PageShell is for page-level layout; Panel is for section-level',
|
|
32
|
+
},
|
|
24
33
|
],
|
|
25
34
|
example: `<Panel.Root bordered>
|
|
26
35
|
<Panel.Header>
|
|
@@ -10,6 +10,12 @@ import {Input} from '../Input/Input'
|
|
|
10
10
|
|
|
11
11
|
const meta: Meta = {
|
|
12
12
|
title: 'Components/Panel',
|
|
13
|
+
argTypes: {
|
|
14
|
+
bordered: {
|
|
15
|
+
control: 'boolean',
|
|
16
|
+
description: 'Whether the panel has an outer border for visual separation',
|
|
17
|
+
},
|
|
18
|
+
},
|
|
13
19
|
}
|
|
14
20
|
|
|
15
21
|
export default meta
|
|
@@ -25,9 +31,8 @@ export const Default: Story = {
|
|
|
25
31
|
</Panel.Header>
|
|
26
32
|
<Panel.Body>
|
|
27
33
|
<Text>
|
|
28
|
-
Panel is a structural primitive for grouping content with
|
|
29
|
-
|
|
30
|
-
and page-level layout.
|
|
34
|
+
Panel is a structural primitive for grouping content with header, body, and footer slots.
|
|
35
|
+
It fills the gap between Card and page-level layout.
|
|
31
36
|
</Text>
|
|
32
37
|
</Panel.Body>
|
|
33
38
|
<Panel.Footer>
|
|
@@ -45,12 +50,14 @@ export const Bordered: Story = {
|
|
|
45
50
|
<Panel.Root bordered>
|
|
46
51
|
<Panel.Header>
|
|
47
52
|
<Heading level={3}>Bordered Panel</Heading>
|
|
48
|
-
<Button variant="secondary" size="small">
|
|
53
|
+
<Button variant="secondary" size="small">
|
|
54
|
+
Action
|
|
55
|
+
</Button>
|
|
49
56
|
</Panel.Header>
|
|
50
57
|
<Panel.Body>
|
|
51
58
|
<Text>
|
|
52
|
-
The bordered variant adds an outer border, useful when the panel
|
|
53
|
-
|
|
59
|
+
The bordered variant adds an outer border, useful when the panel needs visual separation
|
|
60
|
+
from its surroundings.
|
|
54
61
|
</Text>
|
|
55
62
|
</Panel.Body>
|
|
56
63
|
<Panel.Footer>
|
|
@@ -67,8 +74,8 @@ export const BodyOnly: Story = {
|
|
|
67
74
|
<Panel.Root bordered>
|
|
68
75
|
<Panel.Body>
|
|
69
76
|
<Text>
|
|
70
|
-
Panel sub-components can be used independently. This panel only
|
|
71
|
-
|
|
77
|
+
Panel sub-components can be used independently. This panel only has a Body — no Header or
|
|
78
|
+
Footer.
|
|
72
79
|
</Text>
|
|
73
80
|
</Panel.Body>
|
|
74
81
|
</Panel.Root>
|
|
@@ -85,8 +92,8 @@ export const UnpaddedBody: Story = {
|
|
|
85
92
|
</Panel.Header>
|
|
86
93
|
<Panel.Body padded={false}>
|
|
87
94
|
<Text>
|
|
88
|
-
Setting padded=false removes body padding, useful for content
|
|
89
|
-
|
|
95
|
+
Setting padded=false removes body padding, useful for content that needs to extend
|
|
96
|
+
edge-to-edge (tables, images, etc).
|
|
90
97
|
</Text>
|
|
91
98
|
</Panel.Body>
|
|
92
99
|
</Panel.Root>
|
|
@@ -11,11 +11,7 @@ interface RootProps {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
function Root({children, bordered = false}: RootProps) {
|
|
14
|
-
return
|
|
15
|
-
<html.div style={[styles.root, bordered && styles.bordered]}>
|
|
16
|
-
{children}
|
|
17
|
-
</html.div>
|
|
18
|
-
)
|
|
14
|
+
return <html.div style={[styles.root, bordered && styles.bordered]}>{children}</html.div>
|
|
19
15
|
}
|
|
20
16
|
|
|
21
17
|
// --- Header ---
|
|
@@ -33,11 +29,7 @@ interface BodyProps {
|
|
|
33
29
|
}
|
|
34
30
|
|
|
35
31
|
function Body({children, padded = true}: BodyProps) {
|
|
36
|
-
return
|
|
37
|
-
<html.div style={[styles.body, padded && styles.bodyPadded]}>
|
|
38
|
-
{children}
|
|
39
|
-
</html.div>
|
|
40
|
-
)
|
|
32
|
+
return <html.div style={[styles.body, padded && styles.bodyPadded]}>{children}</html.div>
|
|
41
33
|
}
|
|
42
34
|
|
|
43
35
|
// --- Footer ---
|
|
@@ -31,6 +31,11 @@ export const styles = css.create({
|
|
|
31
31
|
body: {
|
|
32
32
|
flex: 1,
|
|
33
33
|
overflowY: 'auto',
|
|
34
|
+
// Minimal inset prevents child borders from touching panel structural borders
|
|
35
|
+
paddingTop: spacing.sm,
|
|
36
|
+
paddingBottom: spacing.sm,
|
|
37
|
+
paddingLeft: spacing.sm,
|
|
38
|
+
paddingRight: spacing.sm,
|
|
34
39
|
},
|
|
35
40
|
bodyPadded: {
|
|
36
41
|
paddingTop: spacing.lg,
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type {ComponentMeta} from '../component-meta'
|
|
2
|
+
|
|
3
|
+
export const meta: ComponentMeta = {
|
|
4
|
+
description:
|
|
5
|
+
'Animated loading indicator. Use while content is being fetched or an action is processing. Always provide a label for accessibility.',
|
|
6
|
+
whenToUse: [
|
|
7
|
+
'Page or section loading states',
|
|
8
|
+
'Button loading states (inside a button)',
|
|
9
|
+
'Async operation feedback',
|
|
10
|
+
],
|
|
11
|
+
whenNotToUse: [
|
|
12
|
+
'Skeleton loading — use placeholder skeleton patterns',
|
|
13
|
+
'Progress with known completion — use a progress bar',
|
|
14
|
+
],
|
|
15
|
+
example: `<Stack gap="md" align="center">
|
|
16
|
+
<Spinner size="lg" label="Loading data..." />
|
|
17
|
+
<Button disabled><Spinner size="sm" /> Saving...</Button>
|
|
18
|
+
</Stack>`,
|
|
19
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type {ComponentMeta} from '../component-meta'
|
|
2
|
+
|
|
3
|
+
export const meta: ComponentMeta = {
|
|
4
|
+
description:
|
|
5
|
+
'Icon with a colored background circle. Combines an Icon with a status-colored container for visual emphasis. Use for status indicators in dashboards or lists.',
|
|
6
|
+
whenToUse: [
|
|
7
|
+
'Dashboard status indicators',
|
|
8
|
+
'List item status icons with colored backgrounds',
|
|
9
|
+
'Feature or capability indicators',
|
|
10
|
+
],
|
|
11
|
+
whenNotToUse: [
|
|
12
|
+
'Inline icons without background — use Icon',
|
|
13
|
+
'Status text labels — use Badge',
|
|
14
|
+
],
|
|
15
|
+
relatedTo: [
|
|
16
|
+
{component: 'Icon', relationship: 'StatusIcon wraps Icon with background; Icon is bare SVG'},
|
|
17
|
+
{component: 'Badge', relationship: 'Badge for text status; StatusIcon for icon status'},
|
|
18
|
+
],
|
|
19
|
+
example: `<Inline gap="md">
|
|
20
|
+
<StatusIcon name="check-circle" variant="success" size="md" />
|
|
21
|
+
<StatusIcon name="alert-triangle" variant="warning" size="md" />
|
|
22
|
+
<StatusIcon name="x-circle" variant="error" size="md" />
|
|
23
|
+
</Inline>`,
|
|
24
|
+
}
|
|
@@ -11,6 +11,22 @@ import {colors} from '@duro-app/tokens/tokens/colors.css'
|
|
|
11
11
|
|
|
12
12
|
const meta: Meta = {
|
|
13
13
|
title: 'Components/Table',
|
|
14
|
+
argTypes: {
|
|
15
|
+
columns: {
|
|
16
|
+
control: 'number',
|
|
17
|
+
description: 'Number of columns in the table grid',
|
|
18
|
+
},
|
|
19
|
+
variant: {
|
|
20
|
+
control: 'select',
|
|
21
|
+
options: ['default', 'striped', 'bordered'],
|
|
22
|
+
description: 'Visual style variant of the table',
|
|
23
|
+
},
|
|
24
|
+
size: {
|
|
25
|
+
control: 'select',
|
|
26
|
+
options: ['sm', 'md', 'lg'],
|
|
27
|
+
description: 'Density/size of table rows',
|
|
28
|
+
},
|
|
29
|
+
},
|
|
14
30
|
}
|
|
15
31
|
|
|
16
32
|
export default meta
|
|
@@ -4,6 +4,13 @@ import {Tabs} from './Tabs'
|
|
|
4
4
|
|
|
5
5
|
const meta: Meta = {
|
|
6
6
|
title: 'Components/Tabs',
|
|
7
|
+
argTypes: {
|
|
8
|
+
orientation: {
|
|
9
|
+
control: 'select',
|
|
10
|
+
options: ['horizontal', 'vertical'],
|
|
11
|
+
description: 'Layout orientation of the tab list',
|
|
12
|
+
},
|
|
13
|
+
},
|
|
7
14
|
}
|
|
8
15
|
|
|
9
16
|
export default meta
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type {ComponentMeta} from '../component-meta'
|
|
2
|
+
|
|
3
|
+
export const meta: ComponentMeta = {
|
|
4
|
+
description:
|
|
5
|
+
'Body and label typography component. Renders a span or paragraph with design system typography presets. Use for all non-heading text content.',
|
|
6
|
+
whenToUse: [
|
|
7
|
+
'Body text (paragraphs, descriptions)',
|
|
8
|
+
'UI labels and metadata',
|
|
9
|
+
'Captions, code snippets, overlines',
|
|
10
|
+
],
|
|
11
|
+
whenNotToUse: [
|
|
12
|
+
'Page or section titles — use Heading',
|
|
13
|
+
'Form field labels — use Field.Label (auto-binds to form)',
|
|
14
|
+
],
|
|
15
|
+
relatedTo: [
|
|
16
|
+
{component: 'Heading', relationship: 'Heading for semantic h1-h6; Text for body/label'},
|
|
17
|
+
],
|
|
18
|
+
example: `<Stack gap="xs">
|
|
19
|
+
<Text variant="label">Email</Text>
|
|
20
|
+
<Text variant="bodySm" color="muted">We will never share your email.</Text>
|
|
21
|
+
<Text variant="code">user@example.com</Text>
|
|
22
|
+
</Stack>`,
|
|
23
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type {ComponentMeta} from '../component-meta'
|
|
2
|
+
|
|
3
|
+
export const meta: ComponentMeta = {
|
|
4
|
+
description:
|
|
5
|
+
'Theme context provider. Must wrap the entire app at the root level. Sets dark, light, or high-contrast theme for all child components via CSS custom properties.',
|
|
6
|
+
whenToUse: [
|
|
7
|
+
'App root — required for all Duro components to render correctly',
|
|
8
|
+
'Theme switching (dark/light mode toggle)',
|
|
9
|
+
],
|
|
10
|
+
whenNotToUse: [
|
|
11
|
+
'Never omit — all Duro components require ThemeProvider',
|
|
12
|
+
],
|
|
13
|
+
example: `<ThemeProvider theme="dark">
|
|
14
|
+
<App />
|
|
15
|
+
</ThemeProvider>`,
|
|
16
|
+
}
|
|
@@ -57,7 +57,8 @@ export const styles = css.create({
|
|
|
57
57
|
borderRadius: 0,
|
|
58
58
|
borderRightWidth: 1,
|
|
59
59
|
borderRightStyle: 'solid',
|
|
60
|
-
|
|
60
|
+
// Override any borderColor set by pressed state — dividers always use border token
|
|
61
|
+
borderColor: colors.border,
|
|
61
62
|
},
|
|
62
63
|
disabled: {
|
|
63
64
|
opacity: 0.5,
|