@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.
Files changed (37) hide show
  1. package/dist/components/ActionBar/ActionBar.d.ts +17 -0
  2. package/dist/components/ActionBar/ActionBar.d.ts.map +1 -0
  3. package/dist/components/ActionBar/ActionBar.stories.d.ts +14 -0
  4. package/dist/components/ActionBar/ActionBar.stories.d.ts.map +1 -0
  5. package/dist/components/ActionBar/styles.css.d.ts +81 -0
  6. package/dist/components/ActionBar/styles.css.d.ts.map +1 -0
  7. package/dist/components/Button/Button.d.ts +1 -1
  8. package/dist/components/Button/Button.d.ts.map +1 -1
  9. package/dist/components/Button/styles.css.d.ts +6 -0
  10. package/dist/components/Button/styles.css.d.ts.map +1 -1
  11. package/dist/components/Checkbox/Checkbox.d.ts +3 -1
  12. package/dist/components/Checkbox/Checkbox.d.ts.map +1 -1
  13. package/dist/components/Select/Select.d.ts +2 -1
  14. package/dist/components/Select/Select.d.ts.map +1 -1
  15. package/dist/components/Table/Table.d.ts +3 -2
  16. package/dist/components/Table/Table.d.ts.map +1 -1
  17. package/dist/components/ThemeProvider/ThemeProvider.d.ts +2 -0
  18. package/dist/components/ThemeProvider/ThemeProvider.d.ts.map +1 -1
  19. package/dist/index.css +1 -1
  20. package/dist/index.d.ts +2 -1
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +1374 -1215
  23. package/dist/index.js.map +1 -1
  24. package/package.json +2 -2
  25. package/src/components/ActionBar/ActionBar.stories.tsx +263 -0
  26. package/src/components/ActionBar/ActionBar.tsx +70 -0
  27. package/src/components/ActionBar/styles.css.ts +106 -0
  28. package/src/components/Button/Button.tsx +1 -1
  29. package/src/components/Button/styles.css.ts +12 -0
  30. package/src/components/Checkbox/Checkbox.tsx +4 -0
  31. package/src/components/Select/Select.stories.tsx +4 -4
  32. package/src/components/Select/Select.tsx +8 -1
  33. package/src/components/Table/Table.tsx +8 -1
  34. package/src/components/ThemeProvider/ThemeProvider.tsx +21 -2
  35. package/src/components/ToggleGroup/ToggleGroup.tsx +1 -1
  36. package/src/components/Tooltip/Tooltip.stories.tsx +1 -1
  37. 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.14.1",
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.4.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({children}: {children: ReactNode}) {
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({children}: {children: ReactNode}) {
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 {ReactNode} from 'react'
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 <html.div style={themeStyles}>{children}</html.div>
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="group"
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
- <html.span>Hover me</html.span>
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 {ThemeProvider, type ThemeName} from './components/ThemeProvider/ThemeProvider'
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'