@duro-app/ui 0.14.1 → 0.15.1
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.d.ts +17 -0
- package/dist/components/ActionBar/ActionBar.d.ts.map +1 -0
- package/dist/components/ActionBar/ActionBar.stories.d.ts +14 -0
- package/dist/components/ActionBar/ActionBar.stories.d.ts.map +1 -0
- package/dist/components/ActionBar/styles.css.d.ts +81 -0
- package/dist/components/ActionBar/styles.css.d.ts.map +1 -0
- package/dist/components/Button/Button.d.ts +1 -1
- package/dist/components/Button/Button.d.ts.map +1 -1
- package/dist/components/Button/styles.css.d.ts +6 -0
- package/dist/components/Button/styles.css.d.ts.map +1 -1
- package/dist/components/Checkbox/Checkbox.d.ts +3 -1
- package/dist/components/Checkbox/Checkbox.d.ts.map +1 -1
- package/dist/components/Select/Select.d.ts +2 -1
- package/dist/components/Select/Select.d.ts.map +1 -1
- package/dist/components/Table/Table.d.ts +3 -2
- package/dist/components/Table/Table.d.ts.map +1 -1
- package/dist/components/ThemeProvider/ThemeProvider.d.ts +2 -0
- package/dist/components/ThemeProvider/ThemeProvider.d.ts.map +1 -1
- package/dist/index.css +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1374 -1215
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/components/ActionBar/ActionBar.stories.tsx +263 -0
- package/src/components/ActionBar/ActionBar.tsx +70 -0
- package/src/components/ActionBar/styles.css.ts +106 -0
- package/src/components/Button/Button.tsx +1 -1
- package/src/components/Button/styles.css.ts +12 -0
- package/src/components/Checkbox/Checkbox.tsx +4 -0
- package/src/components/Select/Select.stories.tsx +4 -4
- package/src/components/Select/Select.tsx +8 -1
- package/src/components/Table/Table.tsx +8 -1
- package/src/components/ThemeProvider/ThemeProvider.tsx +21 -2
- package/src/components/ToggleGroup/ToggleGroup.tsx +1 -1
- package/src/components/Tooltip/Tooltip.stories.tsx +1 -1
- package/src/index.ts +6 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@duro-app/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"react-strict-dom": "^0.0.55"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@duro-app/tokens": "^0.
|
|
41
|
+
"@duro-app/tokens": "^0.15.1"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"@babel/preset-typescript": "^7.28.0",
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import {useState} from 'react'
|
|
2
|
+
import type {Meta, StoryObj} from '@storybook/react'
|
|
3
|
+
import {expect, fn, userEvent, within} from 'storybook/test'
|
|
4
|
+
import {css, html} from 'react-strict-dom'
|
|
5
|
+
import {ActionBar} from './ActionBar'
|
|
6
|
+
import {Button} from '../Button/Button'
|
|
7
|
+
import {Checkbox} from '../Checkbox/Checkbox'
|
|
8
|
+
import {Table} from '../Table/Table'
|
|
9
|
+
|
|
10
|
+
interface ActionBarStoryArgs {
|
|
11
|
+
onClearSelection: () => void
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const meta: Meta<ActionBarStoryArgs> = {
|
|
15
|
+
title: 'Components/ActionBar',
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export default meta
|
|
19
|
+
type Story = StoryObj<ActionBarStoryArgs>
|
|
20
|
+
|
|
21
|
+
const storyStyles = css.create({
|
|
22
|
+
wrapper: {
|
|
23
|
+
display: 'flex',
|
|
24
|
+
flexDirection: 'column',
|
|
25
|
+
gap: 32,
|
|
26
|
+
padding: 24,
|
|
27
|
+
},
|
|
28
|
+
label: {
|
|
29
|
+
fontSize: '0.75rem',
|
|
30
|
+
fontWeight: 600,
|
|
31
|
+
textTransform: 'uppercase' as const,
|
|
32
|
+
letterSpacing: '0.05em',
|
|
33
|
+
opacity: 0.6,
|
|
34
|
+
marginBottom: 8,
|
|
35
|
+
},
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
const users = [
|
|
39
|
+
{id: '1', name: 'Alice Johnson', email: 'alice@example.com', role: 'Admin'},
|
|
40
|
+
{id: '2', name: 'Bob Smith', email: 'bob@example.com', role: 'User'},
|
|
41
|
+
{id: '3', name: 'Carol White', email: 'carol@example.com', role: 'User'},
|
|
42
|
+
{id: '4', name: 'Dan Brown', email: 'dan@example.com', role: 'Editor'},
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
function SelectableTable({
|
|
46
|
+
initialSelected = new Set<string>(),
|
|
47
|
+
onClearSelection,
|
|
48
|
+
}: {
|
|
49
|
+
initialSelected?: Set<string>
|
|
50
|
+
onClearSelection?: () => void
|
|
51
|
+
}) {
|
|
52
|
+
const [selected, setSelected] = useState<Set<string>>(initialSelected)
|
|
53
|
+
|
|
54
|
+
const toggle = (id: string) => {
|
|
55
|
+
setSelected((prev) => {
|
|
56
|
+
const next = new Set(prev)
|
|
57
|
+
if (next.has(id)) next.delete(id)
|
|
58
|
+
else next.add(id)
|
|
59
|
+
return next
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const toggleAll = () => {
|
|
64
|
+
if (selected.size === users.length) {
|
|
65
|
+
setSelected(new Set())
|
|
66
|
+
} else {
|
|
67
|
+
setSelected(new Set(users.map((u) => u.id)))
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const clearSelection = () => {
|
|
72
|
+
setSelected(new Set())
|
|
73
|
+
onClearSelection?.()
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<>
|
|
78
|
+
<Table.Root columns={5}>
|
|
79
|
+
<Table.Header>
|
|
80
|
+
<Table.Row>
|
|
81
|
+
<Table.HeaderCell aria-label="Select">
|
|
82
|
+
<Checkbox
|
|
83
|
+
checked={selected.size === users.length}
|
|
84
|
+
onChange={toggleAll}
|
|
85
|
+
aria-label="Select all rows"
|
|
86
|
+
/>
|
|
87
|
+
</Table.HeaderCell>
|
|
88
|
+
<Table.HeaderCell>Name</Table.HeaderCell>
|
|
89
|
+
<Table.HeaderCell>Email</Table.HeaderCell>
|
|
90
|
+
<Table.HeaderCell>Role</Table.HeaderCell>
|
|
91
|
+
<Table.HeaderCell>Actions</Table.HeaderCell>
|
|
92
|
+
</Table.Row>
|
|
93
|
+
</Table.Header>
|
|
94
|
+
<Table.Body>
|
|
95
|
+
{users.map((u) => (
|
|
96
|
+
<Table.Row key={u.id}>
|
|
97
|
+
<Table.Cell>
|
|
98
|
+
<Checkbox
|
|
99
|
+
checked={selected.has(u.id)}
|
|
100
|
+
onChange={() => toggle(u.id)}
|
|
101
|
+
aria-label={`Select ${u.name}`}
|
|
102
|
+
/>
|
|
103
|
+
</Table.Cell>
|
|
104
|
+
<Table.Cell>{u.name}</Table.Cell>
|
|
105
|
+
<Table.Cell>{u.email}</Table.Cell>
|
|
106
|
+
<Table.Cell>{u.role}</Table.Cell>
|
|
107
|
+
<Table.Cell>
|
|
108
|
+
<Button variant="secondary" size="small">
|
|
109
|
+
Edit
|
|
110
|
+
</Button>
|
|
111
|
+
</Table.Cell>
|
|
112
|
+
</Table.Row>
|
|
113
|
+
))}
|
|
114
|
+
</Table.Body>
|
|
115
|
+
</Table.Root>
|
|
116
|
+
<ActionBar selectedItemCount={selected.size} onClearSelection={clearSelection}>
|
|
117
|
+
<Button variant="secondary" size="small">
|
|
118
|
+
Export
|
|
119
|
+
</Button>
|
|
120
|
+
<Button variant="danger" size="small">
|
|
121
|
+
Delete
|
|
122
|
+
</Button>
|
|
123
|
+
</ActionBar>
|
|
124
|
+
</>
|
|
125
|
+
)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export const Default: Story = {
|
|
129
|
+
args: {
|
|
130
|
+
onClearSelection: fn(),
|
|
131
|
+
},
|
|
132
|
+
render: (args) => (
|
|
133
|
+
<SelectableTable
|
|
134
|
+
initialSelected={new Set(['2'])}
|
|
135
|
+
onClearSelection={args.onClearSelection as () => void}
|
|
136
|
+
/>
|
|
137
|
+
),
|
|
138
|
+
play: async ({canvasElement}) => {
|
|
139
|
+
const canvas = within(canvasElement)
|
|
140
|
+
// The ActionBar is portalled to document.body, so query from there
|
|
141
|
+
const toolbar = document.querySelector('[role="toolbar"]')
|
|
142
|
+
await expect(toolbar).toBeInTheDocument()
|
|
143
|
+
await expect(toolbar).toHaveAccessibleName('1 selected')
|
|
144
|
+
},
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export const MultipleSelected: Story = {
|
|
148
|
+
render: () => <SelectableTable initialSelected={new Set(['1', '3', '4'])} />,
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export const Emphasized: Story = {
|
|
152
|
+
render: () => {
|
|
153
|
+
function Demo() {
|
|
154
|
+
const [selected, setSelected] = useState(new Set(['2', '3']))
|
|
155
|
+
const toggle = (id: string) => {
|
|
156
|
+
setSelected((prev) => {
|
|
157
|
+
const next = new Set(prev)
|
|
158
|
+
if (next.has(id)) next.delete(id)
|
|
159
|
+
else next.add(id)
|
|
160
|
+
return next
|
|
161
|
+
})
|
|
162
|
+
}
|
|
163
|
+
return (
|
|
164
|
+
<>
|
|
165
|
+
<Table.Root columns={3}>
|
|
166
|
+
<Table.Header>
|
|
167
|
+
<Table.Row>
|
|
168
|
+
<Table.HeaderCell aria-label="Select">
|
|
169
|
+
<Checkbox
|
|
170
|
+
checked={selected.size === users.length}
|
|
171
|
+
aria-label="Select all rows"
|
|
172
|
+
onChange={() =>
|
|
173
|
+
setSelected(
|
|
174
|
+
selected.size === users.length
|
|
175
|
+
? new Set()
|
|
176
|
+
: new Set(users.map((u) => u.id)),
|
|
177
|
+
)
|
|
178
|
+
}
|
|
179
|
+
/>
|
|
180
|
+
</Table.HeaderCell>
|
|
181
|
+
<Table.HeaderCell>Name</Table.HeaderCell>
|
|
182
|
+
<Table.HeaderCell>Email</Table.HeaderCell>
|
|
183
|
+
</Table.Row>
|
|
184
|
+
</Table.Header>
|
|
185
|
+
<Table.Body>
|
|
186
|
+
{users.map((u) => (
|
|
187
|
+
<Table.Row key={u.id}>
|
|
188
|
+
<Table.Cell>
|
|
189
|
+
<Checkbox
|
|
190
|
+
checked={selected.has(u.id)}
|
|
191
|
+
onChange={() => toggle(u.id)}
|
|
192
|
+
aria-label={`Select ${u.name}`}
|
|
193
|
+
/>
|
|
194
|
+
</Table.Cell>
|
|
195
|
+
<Table.Cell>{u.name}</Table.Cell>
|
|
196
|
+
<Table.Cell>{u.email}</Table.Cell>
|
|
197
|
+
</Table.Row>
|
|
198
|
+
))}
|
|
199
|
+
</Table.Body>
|
|
200
|
+
</Table.Root>
|
|
201
|
+
<ActionBar
|
|
202
|
+
selectedItemCount={selected.size}
|
|
203
|
+
isEmphasized
|
|
204
|
+
onClearSelection={() => setSelected(new Set())}
|
|
205
|
+
>
|
|
206
|
+
<Button variant="inverseSecondary" size="small">
|
|
207
|
+
Archive
|
|
208
|
+
</Button>
|
|
209
|
+
<Button variant="inverseSecondary" size="small">
|
|
210
|
+
Delete
|
|
211
|
+
</Button>
|
|
212
|
+
</ActionBar>
|
|
213
|
+
</>
|
|
214
|
+
)
|
|
215
|
+
}
|
|
216
|
+
return <Demo />
|
|
217
|
+
},
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export const CustomLabel: Story = {
|
|
221
|
+
render: () => (
|
|
222
|
+
<>
|
|
223
|
+
<Table.Root columns={2}>
|
|
224
|
+
<Table.Header>
|
|
225
|
+
<Table.Row>
|
|
226
|
+
<Table.HeaderCell>Name</Table.HeaderCell>
|
|
227
|
+
<Table.HeaderCell>Status</Table.HeaderCell>
|
|
228
|
+
</Table.Row>
|
|
229
|
+
</Table.Header>
|
|
230
|
+
<Table.Body>
|
|
231
|
+
<Table.Row>
|
|
232
|
+
<Table.Cell>Traefik</Table.Cell>
|
|
233
|
+
<Table.Cell>Running</Table.Cell>
|
|
234
|
+
</Table.Row>
|
|
235
|
+
</Table.Body>
|
|
236
|
+
</Table.Root>
|
|
237
|
+
<ActionBar
|
|
238
|
+
selectedItemCount={3}
|
|
239
|
+
selectedLabel={(count) => `${count} users to revoke`}
|
|
240
|
+
onClearSelection={() => {}}
|
|
241
|
+
>
|
|
242
|
+
<Button variant="danger" size="small">
|
|
243
|
+
Confirm revoke
|
|
244
|
+
</Button>
|
|
245
|
+
</ActionBar>
|
|
246
|
+
</>
|
|
247
|
+
),
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export const Hidden: Story = {
|
|
251
|
+
render: () => <SelectableTable initialSelected={new Set()} />,
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export const AllVariants: Story = {
|
|
255
|
+
render: () => (
|
|
256
|
+
<html.div style={storyStyles.wrapper}>
|
|
257
|
+
<html.div>
|
|
258
|
+
<html.span style={storyStyles.label}>Default — check boxes to see the ActionBar</html.span>
|
|
259
|
+
<SelectableTable initialSelected={new Set(['1'])} />
|
|
260
|
+
</html.div>
|
|
261
|
+
</html.div>
|
|
262
|
+
),
|
|
263
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type {ReactNode} from 'react'
|
|
2
|
+
import {createPortal} from 'react-dom'
|
|
3
|
+
import {html} from 'react-strict-dom'
|
|
4
|
+
import {useOverlayContainer} from '../ThemeProvider/ThemeProvider'
|
|
5
|
+
import {styles} from './styles.css'
|
|
6
|
+
|
|
7
|
+
export interface ActionBarProps {
|
|
8
|
+
/** Number of selected items, or 'all'. Hidden when 0. */
|
|
9
|
+
selectedItemCount: number | 'all'
|
|
10
|
+
/** Label for the selection count. Receives the count string. */
|
|
11
|
+
selectedLabel?: (count: string) => string
|
|
12
|
+
/** Whether to use emphasized (accent) styling. */
|
|
13
|
+
isEmphasized?: boolean
|
|
14
|
+
/** Called when the clear/close button is pressed. */
|
|
15
|
+
onClearSelection: () => void
|
|
16
|
+
/** Action buttons to display. */
|
|
17
|
+
children: ReactNode
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function ActionBarInner({
|
|
21
|
+
selectedItemCount,
|
|
22
|
+
selectedLabel,
|
|
23
|
+
isEmphasized = false,
|
|
24
|
+
onClearSelection,
|
|
25
|
+
children,
|
|
26
|
+
}: ActionBarProps) {
|
|
27
|
+
const container = useOverlayContainer()
|
|
28
|
+
|
|
29
|
+
if (selectedItemCount === 0) return null
|
|
30
|
+
|
|
31
|
+
const countStr = selectedItemCount === 'all' ? 'All' : `${selectedItemCount}`
|
|
32
|
+
const label = selectedLabel ? selectedLabel(countStr) : `${countStr} selected`
|
|
33
|
+
|
|
34
|
+
const bar = (
|
|
35
|
+
<html.div
|
|
36
|
+
role="toolbar"
|
|
37
|
+
aria-label={label}
|
|
38
|
+
style={[styles.overlay, isEmphasized && styles.overlayEmphasized]}
|
|
39
|
+
>
|
|
40
|
+
<html.span style={[styles.selectedCount, isEmphasized && styles.selectedCountEmphasized]}>
|
|
41
|
+
{label}
|
|
42
|
+
</html.span>
|
|
43
|
+
<html.div style={[styles.separator, isEmphasized && styles.separatorEmphasized]} />
|
|
44
|
+
<html.div style={styles.actions}>{children}</html.div>
|
|
45
|
+
<html.button
|
|
46
|
+
type="button"
|
|
47
|
+
aria-label="Clear selection"
|
|
48
|
+
onClick={onClearSelection}
|
|
49
|
+
style={[styles.closeButton, isEmphasized && styles.closeButtonEmphasized]}
|
|
50
|
+
>
|
|
51
|
+
<svg width={10} height={10} viewBox="0 0 10 10" fill="none" aria-hidden="true">
|
|
52
|
+
<path
|
|
53
|
+
d="M1 1l8 8M9 1l-8 8"
|
|
54
|
+
stroke="currentColor"
|
|
55
|
+
strokeWidth={1.5}
|
|
56
|
+
strokeLinecap="round"
|
|
57
|
+
/>
|
|
58
|
+
</svg>
|
|
59
|
+
</html.button>
|
|
60
|
+
</html.div>
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
if (container) {
|
|
64
|
+
return createPortal(bar, container)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return bar
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export const ActionBar = ActionBarInner
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import {css} from 'react-strict-dom'
|
|
2
|
+
import {colors} from '@duro-app/tokens/tokens/colors.css'
|
|
3
|
+
import {spacing, radii} from '@duro-app/tokens/tokens/spacing.css'
|
|
4
|
+
import {typography} from '@duro-app/tokens/tokens/typography.css'
|
|
5
|
+
import {shadows} from '@duro-app/tokens/tokens/shadows.css'
|
|
6
|
+
|
|
7
|
+
export const styles = css.create({
|
|
8
|
+
overlay: {
|
|
9
|
+
position: 'fixed',
|
|
10
|
+
bottom: spacing.lg,
|
|
11
|
+
left: '50%',
|
|
12
|
+
transform: 'translateX(-50%)',
|
|
13
|
+
display: 'flex',
|
|
14
|
+
flexDirection: 'row',
|
|
15
|
+
alignItems: 'center',
|
|
16
|
+
gap: spacing.md,
|
|
17
|
+
paddingTop: spacing.sm,
|
|
18
|
+
paddingBottom: spacing.sm,
|
|
19
|
+
paddingLeft: spacing.md,
|
|
20
|
+
paddingRight: spacing.md,
|
|
21
|
+
backgroundColor: colors.bgCard,
|
|
22
|
+
borderWidth: 1,
|
|
23
|
+
borderStyle: 'solid',
|
|
24
|
+
borderColor: colors.border,
|
|
25
|
+
borderRadius: radii.md,
|
|
26
|
+
boxShadow: shadows.lg,
|
|
27
|
+
zIndex: 50,
|
|
28
|
+
maxWidth: 'calc(100vw - 48px)',
|
|
29
|
+
},
|
|
30
|
+
overlayEmphasized: {
|
|
31
|
+
backgroundColor: colors.accent,
|
|
32
|
+
borderColor: colors.accent,
|
|
33
|
+
},
|
|
34
|
+
selectedCount: {
|
|
35
|
+
fontSize: typography.fontSizeSm,
|
|
36
|
+
fontWeight: typography.fontWeightMedium,
|
|
37
|
+
fontFamily: typography.fontFamily,
|
|
38
|
+
color: colors.text,
|
|
39
|
+
whiteSpace: 'nowrap',
|
|
40
|
+
},
|
|
41
|
+
selectedCountEmphasized: {
|
|
42
|
+
color: colors.accentContrast,
|
|
43
|
+
},
|
|
44
|
+
actions: {
|
|
45
|
+
display: 'flex',
|
|
46
|
+
flexDirection: 'row',
|
|
47
|
+
alignItems: 'center',
|
|
48
|
+
gap: spacing.sm,
|
|
49
|
+
},
|
|
50
|
+
closeButton: {
|
|
51
|
+
display: 'inline-flex',
|
|
52
|
+
alignItems: 'center',
|
|
53
|
+
justifyContent: 'center',
|
|
54
|
+
width: 28,
|
|
55
|
+
height: 28,
|
|
56
|
+
padding: 0,
|
|
57
|
+
backgroundColor: 'transparent',
|
|
58
|
+
borderWidth: 0,
|
|
59
|
+
borderRadius: radii.sm,
|
|
60
|
+
color: {
|
|
61
|
+
default: colors.textMuted,
|
|
62
|
+
':hover': colors.text,
|
|
63
|
+
},
|
|
64
|
+
cursor: 'pointer',
|
|
65
|
+
transitionProperty: 'color, background-color',
|
|
66
|
+
transitionDuration: '150ms',
|
|
67
|
+
transitionTimingFunction: 'ease',
|
|
68
|
+
outlineWidth: {
|
|
69
|
+
default: 0,
|
|
70
|
+
':focus-visible': 2,
|
|
71
|
+
},
|
|
72
|
+
outlineStyle: {
|
|
73
|
+
default: 'none',
|
|
74
|
+
':focus-visible': 'solid',
|
|
75
|
+
},
|
|
76
|
+
outlineColor: {
|
|
77
|
+
default: 'transparent',
|
|
78
|
+
':focus-visible': colors.accent,
|
|
79
|
+
},
|
|
80
|
+
outlineOffset: {
|
|
81
|
+
default: 0,
|
|
82
|
+
':focus-visible': 2,
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
closeButtonEmphasized: {
|
|
86
|
+
color: {
|
|
87
|
+
default: colors.accentContrast,
|
|
88
|
+
':hover': colors.accentContrast,
|
|
89
|
+
},
|
|
90
|
+
opacity: {
|
|
91
|
+
default: 0.7,
|
|
92
|
+
':hover': 1,
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
separator: {
|
|
96
|
+
width: 1,
|
|
97
|
+
alignSelf: 'stretch',
|
|
98
|
+
backgroundColor: colors.border,
|
|
99
|
+
marginTop: spacing.xs,
|
|
100
|
+
marginBottom: spacing.xs,
|
|
101
|
+
},
|
|
102
|
+
separatorEmphasized: {
|
|
103
|
+
backgroundColor: colors.accentContrast,
|
|
104
|
+
opacity: 0.3,
|
|
105
|
+
},
|
|
106
|
+
})
|
|
@@ -2,7 +2,7 @@ import type {ReactNode} from 'react'
|
|
|
2
2
|
import {html} from 'react-strict-dom'
|
|
3
3
|
import {styles} from './styles.css'
|
|
4
4
|
|
|
5
|
-
export type ButtonVariant = 'primary' | 'secondary' | 'link' | 'danger'
|
|
5
|
+
export type ButtonVariant = 'primary' | 'secondary' | 'inverseSecondary' | 'link' | 'danger'
|
|
6
6
|
export type ButtonSize = 'default' | 'small'
|
|
7
7
|
|
|
8
8
|
interface ButtonProps {
|
|
@@ -21,6 +21,7 @@ export const styles = css.create({
|
|
|
21
21
|
transitionDuration: '150ms',
|
|
22
22
|
transitionTimingFunction: 'ease',
|
|
23
23
|
textDecoration: 'none',
|
|
24
|
+
whiteSpace: 'nowrap',
|
|
24
25
|
outlineWidth: {
|
|
25
26
|
default: 0,
|
|
26
27
|
':focus-visible': 2,
|
|
@@ -85,6 +86,17 @@ export const styles = css.create({
|
|
|
85
86
|
paddingLeft: 0,
|
|
86
87
|
paddingRight: 0,
|
|
87
88
|
},
|
|
89
|
+
inverseSecondary: {
|
|
90
|
+
backgroundColor: {
|
|
91
|
+
default: 'rgba(0, 0, 0, 0.10)',
|
|
92
|
+
':hover': 'rgba(0, 0, 0, 0.18)',
|
|
93
|
+
},
|
|
94
|
+
borderColor: {
|
|
95
|
+
default: 'rgba(0, 0, 0, 0.55)',
|
|
96
|
+
':hover': 'rgba(0, 0, 0, 0.70)',
|
|
97
|
+
},
|
|
98
|
+
color: colors.accentContrast,
|
|
99
|
+
},
|
|
88
100
|
danger: {
|
|
89
101
|
backgroundColor: {
|
|
90
102
|
default: colors.error,
|
|
@@ -8,6 +8,8 @@ interface CheckboxProps {
|
|
|
8
8
|
checked?: boolean
|
|
9
9
|
defaultChecked?: boolean
|
|
10
10
|
disabled?: boolean
|
|
11
|
+
/** Accessible label when no visible label (children) is provided. */
|
|
12
|
+
'aria-label'?: string
|
|
11
13
|
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void
|
|
12
14
|
children?: ReactNode
|
|
13
15
|
}
|
|
@@ -18,6 +20,7 @@ export function Checkbox({
|
|
|
18
20
|
checked: controlledChecked,
|
|
19
21
|
defaultChecked = false,
|
|
20
22
|
disabled = false,
|
|
23
|
+
'aria-label': ariaLabel,
|
|
21
24
|
onChange,
|
|
22
25
|
children,
|
|
23
26
|
}: CheckboxProps) {
|
|
@@ -44,6 +47,7 @@ export function Checkbox({
|
|
|
44
47
|
checked={isControlled ? controlledChecked : undefined}
|
|
45
48
|
defaultChecked={!isControlled ? defaultChecked : undefined}
|
|
46
49
|
disabled={disabled}
|
|
50
|
+
aria-label={!children ? ariaLabel : undefined}
|
|
47
51
|
onChange={handleChange}
|
|
48
52
|
style={styles.input}
|
|
49
53
|
/>
|
|
@@ -12,7 +12,7 @@ type Story = StoryObj
|
|
|
12
12
|
export const Default: Story = {
|
|
13
13
|
render: () => (
|
|
14
14
|
<Select.Root defaultValue="en">
|
|
15
|
-
<Select.Trigger>
|
|
15
|
+
<Select.Trigger aria-label="Language">
|
|
16
16
|
<Select.Value />
|
|
17
17
|
<Select.Icon />
|
|
18
18
|
</Select.Trigger>
|
|
@@ -40,7 +40,7 @@ export const Default: Story = {
|
|
|
40
40
|
export const OpenAndSelect: Story = {
|
|
41
41
|
render: () => (
|
|
42
42
|
<Select.Root defaultValue="en">
|
|
43
|
-
<Select.Trigger>
|
|
43
|
+
<Select.Trigger aria-label="Language">
|
|
44
44
|
<Select.Value />
|
|
45
45
|
<Select.Icon />
|
|
46
46
|
</Select.Trigger>
|
|
@@ -75,7 +75,7 @@ export const OpenAndSelect: Story = {
|
|
|
75
75
|
export const WithPlaceholder: Story = {
|
|
76
76
|
render: () => (
|
|
77
77
|
<Select.Root>
|
|
78
|
-
<Select.Trigger>
|
|
78
|
+
<Select.Trigger aria-label="Language">
|
|
79
79
|
<Select.Value placeholder="Choose a language..." />
|
|
80
80
|
<Select.Icon />
|
|
81
81
|
</Select.Trigger>
|
|
@@ -119,7 +119,7 @@ export const WithPlaceholder: Story = {
|
|
|
119
119
|
export const KeyboardNavigation: Story = {
|
|
120
120
|
render: () => (
|
|
121
121
|
<Select.Root>
|
|
122
|
-
<Select.Trigger>
|
|
122
|
+
<Select.Trigger aria-label="Language">
|
|
123
123
|
<Select.Value placeholder="Select..." />
|
|
124
124
|
<Select.Icon />
|
|
125
125
|
</Select.Trigger>
|
|
@@ -28,7 +28,13 @@ function Root({name, defaultValue, value, onValueChange, initialLabels, children
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
// --- Trigger ---
|
|
31
|
-
function Trigger({
|
|
31
|
+
function Trigger({
|
|
32
|
+
children,
|
|
33
|
+
'aria-label': ariaLabel,
|
|
34
|
+
}: {
|
|
35
|
+
children: ReactNode
|
|
36
|
+
'aria-label'?: string
|
|
37
|
+
}) {
|
|
32
38
|
const {open, toggle, listboxId, highlightedId, triggerRef} = useSelect()
|
|
33
39
|
const localRef = useRef<HTMLButtonElement>(null)
|
|
34
40
|
|
|
@@ -43,6 +49,7 @@ function Trigger({children}: {children: ReactNode}) {
|
|
|
43
49
|
type="button"
|
|
44
50
|
role={'combobox' as 'listbox'}
|
|
45
51
|
onClick={toggle}
|
|
52
|
+
aria-label={ariaLabel}
|
|
46
53
|
aria-expanded={open}
|
|
47
54
|
aria-haspopup="listbox"
|
|
48
55
|
aria-controls={open ? listboxId : undefined}
|
|
@@ -125,7 +125,13 @@ const CellIndexContext = createContext<{index: number; total: number}>({
|
|
|
125
125
|
|
|
126
126
|
// --- HeaderCell ---
|
|
127
127
|
|
|
128
|
-
function HeaderCell({
|
|
128
|
+
function HeaderCell({
|
|
129
|
+
children,
|
|
130
|
+
'aria-label': ariaLabel,
|
|
131
|
+
}: {
|
|
132
|
+
children?: ReactNode
|
|
133
|
+
'aria-label'?: string
|
|
134
|
+
}) {
|
|
129
135
|
const {size, variant} = useTable()
|
|
130
136
|
const {index, total} = useContext(CellIndexContext)
|
|
131
137
|
const isLast = variant === 'bordered' && index === total - 1
|
|
@@ -133,6 +139,7 @@ function HeaderCell({children}: {children: ReactNode}) {
|
|
|
133
139
|
return (
|
|
134
140
|
<html.div
|
|
135
141
|
role="columnheader"
|
|
142
|
+
aria-label={ariaLabel}
|
|
136
143
|
style={[
|
|
137
144
|
styles.headerCell,
|
|
138
145
|
size === 'sm' ? styles.cellSm : styles.cellMd,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import {type ReactNode, createContext, useContext, useRef} from 'react'
|
|
2
2
|
import {css, html} from 'react-strict-dom'
|
|
3
|
+
import {colors} from '@duro-app/tokens/tokens/colors.css'
|
|
3
4
|
import {lightTheme, lightShadows} from '@duro-app/tokens/themes/light.css'
|
|
4
5
|
import {highContrastTheme, highContrastShadows} from '@duro-app/tokens/themes/high-contrast.css'
|
|
5
6
|
|
|
@@ -18,6 +19,7 @@ const themeMap: Partial<Record<ThemeName, readonly [typeof lightTheme, typeof li
|
|
|
18
19
|
const styles = css.create({
|
|
19
20
|
root: {
|
|
20
21
|
display: 'contents',
|
|
22
|
+
color: colors.text,
|
|
21
23
|
},
|
|
22
24
|
})
|
|
23
25
|
|
|
@@ -25,9 +27,26 @@ const styles = css.create({
|
|
|
25
27
|
// concrete VarGroup lacks the generic index signature. This is a known typing gap.
|
|
26
28
|
type DivStyle = Parameters<typeof html.div>[0]['style']
|
|
27
29
|
|
|
30
|
+
/** Context providing a portal target inside the themed tree. */
|
|
31
|
+
const OverlayContainerContext = createContext<React.RefObject<HTMLDivElement | null> | null>(null)
|
|
32
|
+
|
|
33
|
+
/** Returns the overlay container element for portals (inside the ThemeProvider). */
|
|
34
|
+
export function useOverlayContainer(): HTMLElement {
|
|
35
|
+
const ref = useContext(OverlayContainerContext)
|
|
36
|
+
return ref?.current ?? (typeof document !== 'undefined' ? document.body : null!)
|
|
37
|
+
}
|
|
38
|
+
|
|
28
39
|
export function ThemeProvider({theme = 'dark', children}: ThemeProviderProps) {
|
|
40
|
+
const overlayRef = useRef<HTMLDivElement>(null)
|
|
29
41
|
const overrides = themeMap[theme]
|
|
30
42
|
const themeStyles = [overrides?.[0], overrides?.[1], styles.root] as DivStyle
|
|
31
43
|
|
|
32
|
-
return
|
|
44
|
+
return (
|
|
45
|
+
<html.div style={themeStyles}>
|
|
46
|
+
<OverlayContainerContext.Provider value={overlayRef}>
|
|
47
|
+
{children}
|
|
48
|
+
<html.div ref={overlayRef} />
|
|
49
|
+
</OverlayContainerContext.Provider>
|
|
50
|
+
</html.div>
|
|
51
|
+
)
|
|
33
52
|
}
|
|
@@ -52,7 +52,7 @@ export function ToggleGroup({
|
|
|
52
52
|
return (
|
|
53
53
|
<ToggleGroupContext.Provider value={{value, toggle, disabled, orientation, size}}>
|
|
54
54
|
<html.div
|
|
55
|
-
role="
|
|
55
|
+
role="toolbar"
|
|
56
56
|
aria-orientation={orientation}
|
|
57
57
|
style={[styles.root, orientation === 'vertical' && styles.vertical]}
|
|
58
58
|
>
|
|
@@ -26,7 +26,7 @@ export const Default: Story = {
|
|
|
26
26
|
<html.div style={centerStyles.center}>
|
|
27
27
|
<Tooltip.Root content="This is a tooltip" delay={0}>
|
|
28
28
|
<Tooltip.Trigger>
|
|
29
|
-
<
|
|
29
|
+
<Badge>Hover me</Badge>
|
|
30
30
|
</Tooltip.Trigger>
|
|
31
31
|
</Tooltip.Root>
|
|
32
32
|
</html.div>
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import './strict.css'
|
|
2
2
|
|
|
3
3
|
// Components
|
|
4
|
+
export {ActionBar, type ActionBarProps} from './components/ActionBar/ActionBar'
|
|
4
5
|
export {Alert, type AlertVariant} from './components/Alert/Alert'
|
|
5
6
|
export {Icon, type IconName} from './components/Icon'
|
|
6
7
|
export {Badge, type BadgeVariant, type BadgeSize} from './components/Badge/Badge'
|
|
@@ -32,7 +33,11 @@ export {
|
|
|
32
33
|
export {Table, type TableVariant, type TableSize} from './components/Table/Table'
|
|
33
34
|
export {Tabs} from './components/Tabs/Tabs'
|
|
34
35
|
export {Textarea, type TextareaVariant} from './components/Textarea/Textarea'
|
|
35
|
-
export {
|
|
36
|
+
export {
|
|
37
|
+
ThemeProvider,
|
|
38
|
+
useOverlayContainer,
|
|
39
|
+
type ThemeName,
|
|
40
|
+
} from './components/ThemeProvider/ThemeProvider'
|
|
36
41
|
export {Text, type TextVariant, type TextColor} from './components/Text/Text'
|
|
37
42
|
export {Toggle, type ToggleSize} from './components/Toggle/Toggle'
|
|
38
43
|
export {ToggleGroup} from './components/ToggleGroup/ToggleGroup'
|