@duro-app/ui 0.51.0 → 1.0.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/{Table-xlMa_Kqe.js → Table-P-UHkEqJ.js} +333 -329
- package/dist/Table-P-UHkEqJ.js.map +1 -0
- package/dist/components/ActionBar/ActionBar.d.ts +6 -4
- package/dist/components/ActionBar/ActionBar.d.ts.map +1 -1
- package/dist/components/ButtonGroup/ButtonGroup.d.ts +3 -1
- package/dist/components/ButtonGroup/ButtonGroup.d.ts.map +1 -1
- package/dist/components/ButtonGroup/ButtonGroup.stories.d.ts.map +1 -1
- package/dist/components/Cluster/Cluster.d.ts +2 -2
- package/dist/components/Cluster/Cluster.d.ts.map +1 -1
- package/dist/components/Cluster/Cluster.stories.d.ts.map +1 -1
- package/dist/components/ColorMode/ColorMode.d.ts +3 -2
- package/dist/components/ColorMode/ColorMode.d.ts.map +1 -1
- package/dist/components/Combobox/Combobox.stories.d.ts.map +1 -1
- package/dist/components/ConfirmDialog/ConfirmDialog.d.ts +1 -1
- package/dist/components/ConfirmDialog/ConfirmDialog.d.ts.map +1 -1
- package/dist/components/DetailPanel/DetailPanel.d.ts +3 -2
- package/dist/components/DetailPanel/DetailPanel.d.ts.map +1 -1
- package/dist/components/DetailPanel/styles.css.d.ts +15 -3
- package/dist/components/DetailPanel/styles.css.d.ts.map +1 -1
- package/dist/components/Dialog/Dialog.d.ts +3 -2
- package/dist/components/Dialog/Dialog.d.ts.map +1 -1
- package/dist/components/Dialog/styles.css.d.ts +16 -4
- package/dist/components/Dialog/styles.css.d.ts.map +1 -1
- package/dist/components/Drawer/Drawer.d.ts +3 -2
- package/dist/components/Drawer/Drawer.d.ts.map +1 -1
- package/dist/components/Drawer/styles.css.d.ts +16 -4
- package/dist/components/Drawer/styles.css.d.ts.map +1 -1
- package/dist/components/Fieldset/Fieldset.d.ts +2 -1
- package/dist/components/Fieldset/Fieldset.d.ts.map +1 -1
- package/dist/components/Grid/Grid.d.ts +2 -2
- package/dist/components/Grid/Grid.d.ts.map +1 -1
- package/dist/components/Grid/Grid.stories.d.ts.map +1 -1
- package/dist/components/Icon/Icon.d.ts +6 -4
- package/dist/components/Icon/Icon.d.ts.map +1 -1
- package/dist/components/Icon/index.d.ts +1 -1
- package/dist/components/Icon/index.d.ts.map +1 -1
- package/dist/components/Inline/Inline.d.ts +2 -2
- package/dist/components/Inline/Inline.d.ts.map +1 -1
- package/dist/components/Inline/Inline.stories.d.ts.map +1 -1
- package/dist/components/SideNav/SideNav.d.ts +32 -3
- package/dist/components/SideNav/SideNav.d.ts.map +1 -1
- package/dist/components/SideNav/SideNav.meta.d.ts.map +1 -1
- package/dist/components/SideNav/SideNav.stories.d.ts +1 -0
- package/dist/components/SideNav/SideNav.stories.d.ts.map +1 -1
- package/dist/components/SideNav/styles.css.d.ts +4 -0
- package/dist/components/SideNav/styles.css.d.ts.map +1 -1
- package/dist/components/Stack/Stack.d.ts +2 -1
- package/dist/components/Stack/Stack.d.ts.map +1 -1
- package/dist/components/Stack/Stack.stories.d.ts.map +1 -1
- package/dist/components/StatusIcon/StatusIcon.d.ts +2 -2
- package/dist/components/StatusIcon/StatusIcon.d.ts.map +1 -1
- package/dist/components/Table/Table.d.ts +1 -7
- package/dist/components/Table/Table.d.ts.map +1 -1
- package/dist/components/Table/Table.stories.d.ts.map +1 -1
- package/dist/components/Tabs/TabsContext.d.ts +2 -1
- package/dist/components/Tabs/TabsContext.d.ts.map +1 -1
- package/dist/components/ToggleGroup/ToggleGroupContext.d.ts +2 -1
- package/dist/components/ToggleGroup/ToggleGroupContext.d.ts.map +1 -1
- package/dist/components/VirtualTable/VirtualTable.d.ts +2 -1
- package/dist/components/VirtualTable/VirtualTable.d.ts.map +1 -1
- package/dist/hooks/useContainerQuery.d.ts +3 -2
- package/dist/hooks/useContainerQuery.d.ts.map +1 -1
- package/dist/index.css +1 -1
- package/dist/index.js +2120 -1983
- package/dist/index.js.map +1 -1
- package/dist/shared/types.d.ts +2 -0
- package/dist/shared/types.d.ts.map +1 -0
- package/dist/table.js +137 -139
- package/dist/table.js.map +1 -1
- package/package.json +3 -2
- package/src/components/ActionBar/ActionBar.tsx +10 -5
- package/src/components/Alert/Alert.tsx +2 -2
- package/src/components/ButtonGroup/ButtonGroup.stories.tsx +3 -2
- package/src/components/ButtonGroup/ButtonGroup.tsx +5 -2
- package/src/components/Callout/Callout.tsx +2 -2
- package/src/components/Cluster/Cluster.stories.tsx +2 -1
- package/src/components/Cluster/Cluster.tsx +3 -3
- package/src/components/ColorMode/ColorMode.tsx +4 -4
- package/src/components/Combobox/Combobox.stories.tsx +7 -3
- package/src/components/ConfirmDialog/ConfirmDialog.tsx +1 -3
- package/src/components/DetailPanel/DetailPanel.tsx +23 -6
- package/src/components/DetailPanel/styles.css.ts +14 -7
- package/src/components/Dialog/Dialog.tsx +26 -5
- package/src/components/Dialog/styles.css.ts +12 -4
- package/src/components/Drawer/Drawer.tsx +17 -4
- package/src/components/Drawer/styles.css.ts +12 -4
- package/src/components/EmptyState/EmptyState.meta.ts +1 -1
- package/src/components/EmptyState/EmptyState.stories.tsx +2 -2
- package/src/components/Fieldset/Fieldset.tsx +3 -2
- package/src/components/Grid/Grid.stories.tsx +2 -1
- package/src/components/Grid/Grid.tsx +3 -3
- package/src/components/Icon/Icon.meta.ts +3 -3
- package/src/components/Icon/Icon.tsx +63 -2
- package/src/components/Icon/index.ts +1 -1
- package/src/components/Inline/Inline.stories.tsx +2 -1
- package/src/components/Inline/Inline.tsx +3 -3
- package/src/components/SideNav/SideNav.meta.ts +13 -10
- package/src/components/SideNav/SideNav.stories.tsx +67 -8
- package/src/components/SideNav/SideNav.tsx +50 -7
- package/src/components/SideNav/styles.css.ts +7 -0
- package/src/components/Stack/Stack.stories.tsx +3 -2
- package/src/components/Stack/Stack.tsx +3 -2
- package/src/components/StatusIcon/StatusIcon.stories.tsx +1 -1
- package/src/components/StatusIcon/StatusIcon.tsx +3 -3
- package/src/components/Table/Table.stories.tsx +29 -32
- package/src/components/Table/Table.tsx +0 -13
- package/src/components/Tabs/TabsContext.ts +2 -1
- package/src/components/Toast/ToastProvider.tsx +2 -2
- package/src/components/Toggle/Toggle.meta.ts +1 -1
- package/src/components/ToggleGroup/ToggleGroupContext.ts +2 -1
- package/src/components/VirtualTable/VirtualTable.tsx +6 -5
- package/src/hooks/useContainerQuery.ts +10 -3
- package/src/shared/types.ts +3 -0
- package/dist/Table-xlMa_Kqe.js.map +0 -1
|
@@ -2,33 +2,36 @@ import type {ComponentMeta} from '../component-meta'
|
|
|
2
2
|
|
|
3
3
|
export const meta: ComponentMeta = {
|
|
4
4
|
description:
|
|
5
|
-
'Vertical side navigation.
|
|
5
|
+
'Vertical side navigation. `Section` is a labelled block that is always open — the default for primary navigation. `Group` is the same block behind a chevron (a disclosure) — reach for it only for a rare/advanced region, an unbounded data-driven list, or a rail long enough that flat stops reading as an overview; mixing the two in one rail is the healthy shape. `Item`s take an optional `icon` and show an active left-marker. Supports controlled and uncontrolled active item. Compound component — Root is required.',
|
|
6
6
|
whenToUse: [
|
|
7
|
-
'App-level sidebar navigation',
|
|
8
|
-
'Settings navigation with
|
|
9
|
-
'
|
|
7
|
+
'App-level sidebar navigation — flat `Section`s, every destination visible',
|
|
8
|
+
'Settings navigation with labelled regions',
|
|
9
|
+
'`Group` for a rare or advanced region tucked below the flat sections',
|
|
10
|
+
'`Group` when the entries are data-driven and unbounded (one per namespace, project, team)',
|
|
10
11
|
],
|
|
11
12
|
whenNotToUse: [
|
|
13
|
+
'Collapsing the everyday destinations — a nav advertises where you can go; hiding the common ones behind a chevron costs a click each and removes them from scanning. Use `Section`.',
|
|
14
|
+
'Arbitrary-depth data browsing (file tree, resource drill-down) — that needs role="tree" with roving tabindex and typeahead, not a deeper SideNav',
|
|
12
15
|
'In-page content switching — use Tabs',
|
|
13
16
|
'Top-level horizontal nav — use Inline with LinkButton',
|
|
14
17
|
],
|
|
15
18
|
anatomy: {
|
|
16
19
|
required: ['Root', 'Item'],
|
|
17
|
-
optional: ['
|
|
20
|
+
optional: ['Section', 'Group'],
|
|
18
21
|
},
|
|
19
22
|
relatedTo: [
|
|
20
23
|
{component: 'Tabs', relationship: 'Tabs switch content in-place; SideNav navigates pages'},
|
|
21
24
|
{component: 'PageShell', relationship: 'Often placed alongside PageShell'},
|
|
22
25
|
],
|
|
23
26
|
example: `<SideNav.Root defaultValue="identities">
|
|
24
|
-
{/*
|
|
27
|
+
{/* Default: always-open sections. Nothing is a click away from being seen. */}
|
|
25
28
|
<SideNav.Section label="People & access">
|
|
26
|
-
<SideNav.Item value="identities" icon={<Icon name="users" size=
|
|
27
|
-
<SideNav.Item value="grants" icon={<Icon name="key" size=
|
|
29
|
+
<SideNav.Item value="identities" icon={<Icon name="users" size="md" />}>Identities</SideNav.Item>
|
|
30
|
+
<SideNav.Item value="grants" icon={<Icon name="key" size="md" />}>Grants</SideNav.Item>
|
|
28
31
|
</SideNav.Section>
|
|
29
|
-
{/*
|
|
32
|
+
{/* Disclosure, earned: rarely visited, so it starts collapsed. */}
|
|
30
33
|
<SideNav.Group label="Advanced" defaultExpanded={false}>
|
|
31
|
-
<SideNav.Item value="plugins" icon={<Icon name="plug" size=
|
|
34
|
+
<SideNav.Item value="plugins" icon={<Icon name="plug" size="md" />}>Plugins</SideNav.Item>
|
|
32
35
|
</SideNav.Group>
|
|
33
36
|
</SideNav.Root>`,
|
|
34
37
|
}
|
|
@@ -14,18 +14,21 @@ const meta: Meta<typeof SideNav.Root> = {
|
|
|
14
14
|
export default meta
|
|
15
15
|
type Story = StoryObj<typeof SideNav.Root>
|
|
16
16
|
|
|
17
|
+
// The default shape for primary navigation: labelled `Section`s, all open, no
|
|
18
|
+
// chevrons. Every destination is visible and one click away — a nav's job is to
|
|
19
|
+
// advertise where you can go, and a collapsed group does the opposite.
|
|
17
20
|
export const Default: Story = {
|
|
18
21
|
render: (args) => (
|
|
19
22
|
<SideNav.Root {...args} defaultValue="dashboard">
|
|
20
|
-
<SideNav.
|
|
23
|
+
<SideNav.Section label="Overview">
|
|
21
24
|
<SideNav.Item value="dashboard">Dashboard</SideNav.Item>
|
|
22
25
|
<SideNav.Item value="analytics">Analytics</SideNav.Item>
|
|
23
|
-
</SideNav.
|
|
24
|
-
<SideNav.
|
|
26
|
+
</SideNav.Section>
|
|
27
|
+
<SideNav.Section label="Settings">
|
|
25
28
|
<SideNav.Item value="profile">Profile</SideNav.Item>
|
|
26
29
|
<SideNav.Item value="security">Security</SideNav.Item>
|
|
27
30
|
<SideNav.Item value="notifications">Notifications</SideNav.Item>
|
|
28
|
-
</SideNav.
|
|
31
|
+
</SideNav.Section>
|
|
29
32
|
</SideNav.Root>
|
|
30
33
|
),
|
|
31
34
|
play: async ({canvas}) => {
|
|
@@ -34,6 +37,62 @@ export const Default: Story = {
|
|
|
34
37
|
|
|
35
38
|
const activeItem = canvas.getByRole('button', {name: 'Dashboard'})
|
|
36
39
|
await expect(activeItem).toHaveAttribute('aria-current', 'page')
|
|
40
|
+
|
|
41
|
+
// Nothing is hidden: no section is a disclosure, every item is reachable.
|
|
42
|
+
await expect(canvas.queryByRole('button', {name: 'Settings'})).toBeNull()
|
|
43
|
+
await expect(canvas.getByRole('button', {name: 'Notifications'})).toBeInTheDocument()
|
|
44
|
+
|
|
45
|
+
// Each section names its own group so a screen reader hears "Settings,
|
|
46
|
+
// group" rather than one undifferentiated run of buttons.
|
|
47
|
+
const groups = canvas.getAllByRole('group')
|
|
48
|
+
await expect(groups).toHaveLength(2)
|
|
49
|
+
await expect(groups[0]).toHaveAccessibleName('Overview')
|
|
50
|
+
await expect(groups[1]).toHaveAccessibleName('Settings')
|
|
51
|
+
},
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// The healthy mix: flat sections carry the journey, and ONE collapsed Group
|
|
55
|
+
// holds the region you visit rarely. Disclosure is for the seldom-used — never
|
|
56
|
+
// for the everyday.
|
|
57
|
+
export const SectionsWithOneDisclosedGroup: Story = {
|
|
58
|
+
render: (args) => (
|
|
59
|
+
<SideNav.Root {...args} defaultValue="identities">
|
|
60
|
+
<SideNav.Section label="People & access">
|
|
61
|
+
<SideNav.Item value="identities" icon={<Icon name="users" size="md" />}>
|
|
62
|
+
Identities
|
|
63
|
+
</SideNav.Item>
|
|
64
|
+
<SideNav.Item value="grants" icon={<Icon name="key" size="md" />}>
|
|
65
|
+
Grants
|
|
66
|
+
</SideNav.Item>
|
|
67
|
+
</SideNav.Section>
|
|
68
|
+
<SideNav.Section label="Audit">
|
|
69
|
+
<SideNav.Item value="audit" icon={<Icon name="file-text" size="md" />}>
|
|
70
|
+
Audit log
|
|
71
|
+
</SideNav.Item>
|
|
72
|
+
</SideNav.Section>
|
|
73
|
+
<SideNav.Group label="Advanced">
|
|
74
|
+
<SideNav.Item value="plugins" icon={<Icon name="plug" size="md" />}>
|
|
75
|
+
Plugins
|
|
76
|
+
</SideNav.Item>
|
|
77
|
+
</SideNav.Group>
|
|
78
|
+
</SideNav.Root>
|
|
79
|
+
),
|
|
80
|
+
play: async ({canvas, userEvent}) => {
|
|
81
|
+
// Everyday destinations: visible without interaction.
|
|
82
|
+
await expect(canvas.getByRole('button', {name: 'Identities'})).toBeInTheDocument()
|
|
83
|
+
await expect(canvas.getByRole('button', {name: 'Audit log'})).toBeInTheDocument()
|
|
84
|
+
|
|
85
|
+
// The rare one is disclosed, and says so.
|
|
86
|
+
const advanced = canvas.getByRole('button', {name: 'Advanced'})
|
|
87
|
+
await expect(advanced).toHaveAttribute('aria-expanded', 'false')
|
|
88
|
+
await expect(advanced).not.toHaveAttribute('aria-controls')
|
|
89
|
+
await expect(canvas.queryByRole('button', {name: 'Plugins'})).toBeNull()
|
|
90
|
+
|
|
91
|
+
await userEvent.click(advanced)
|
|
92
|
+
await expect(advanced).toHaveAttribute('aria-expanded', 'true')
|
|
93
|
+
const panelId = advanced.getAttribute('aria-controls')
|
|
94
|
+
await expect(panelId).toBeTruthy()
|
|
95
|
+
await expect(canvas.getByRole('button', {name: 'Plugins'})).toBeInTheDocument()
|
|
37
96
|
},
|
|
38
97
|
}
|
|
39
98
|
|
|
@@ -106,18 +165,18 @@ export const FlatWithIcons: Story = {
|
|
|
106
165
|
render: (args) => (
|
|
107
166
|
<SideNav.Root {...args} defaultValue="grants">
|
|
108
167
|
<SideNav.Section label="People & access">
|
|
109
|
-
<SideNav.Item value="identities" icon={<Icon name="users" size=
|
|
168
|
+
<SideNav.Item value="identities" icon={<Icon name="users" size="md" />}>
|
|
110
169
|
Identities
|
|
111
170
|
</SideNav.Item>
|
|
112
|
-
<SideNav.Item value="grants" icon={<Icon name="key" size=
|
|
171
|
+
<SideNav.Item value="grants" icon={<Icon name="key" size="md" />}>
|
|
113
172
|
Grants
|
|
114
173
|
</SideNav.Item>
|
|
115
174
|
</SideNav.Section>
|
|
116
175
|
<SideNav.Section label="Requests & invites">
|
|
117
|
-
<SideNav.Item value="invitations" icon={<Icon name="mail" size=
|
|
176
|
+
<SideNav.Item value="invitations" icon={<Icon name="mail" size="md" />}>
|
|
118
177
|
Invitations
|
|
119
178
|
</SideNav.Item>
|
|
120
|
-
<SideNav.Item value="invites" icon={<Icon name="user-plus" size=
|
|
179
|
+
<SideNav.Item value="invites" icon={<Icon name="user-plus" size="md" />}>
|
|
121
180
|
User Invites
|
|
122
181
|
</SideNav.Item>
|
|
123
182
|
</SideNav.Section>
|
|
@@ -1,9 +1,35 @@
|
|
|
1
|
-
import {type ReactNode, useState, useCallback, useRef, useEffect} from 'react'
|
|
1
|
+
import {type ReactNode, useState, useCallback, useRef, useEffect, useId} from 'react'
|
|
2
2
|
import {html} from 'react-strict-dom'
|
|
3
3
|
import {styles} from './styles.css'
|
|
4
4
|
import {useControllableValue} from '../../hooks/useControllableValue'
|
|
5
5
|
import {SideNavContext, useSideNav} from './SideNavContext'
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* SideNav has two ways to chunk a rail, and they are NOT interchangeable.
|
|
9
|
+
*
|
|
10
|
+
* `Section` — a labelled block that is always open. This is the DEFAULT for
|
|
11
|
+
* primary navigation. A nav's job is to advertise where you can go; an
|
|
12
|
+
* always-open list keeps the whole information architecture scannable and puts
|
|
13
|
+
* every destination one click away. The uppercase label does the chunking work
|
|
14
|
+
* on its own — you get the grouping benefit without hiding anything.
|
|
15
|
+
*
|
|
16
|
+
* `Group` — the same block behind a chevron (a disclosure). Collapsing costs
|
|
17
|
+
* every destination inside it an extra click and hides it from scanning, so it
|
|
18
|
+
* has to buy something back. It does when the region is:
|
|
19
|
+
* • rare or advanced ("Advanced", "Danger zone", "Legacy") — disclose the
|
|
20
|
+
* seldom-used, never the everyday;
|
|
21
|
+
* • unbounded / data-driven (one entry per namespace, project, team) — you
|
|
22
|
+
* cannot author-flatten a list whose length you don't control;
|
|
23
|
+
* • one of many in a rail long enough (>~30 leaves) that a flat list stops
|
|
24
|
+
* reading as an overview.
|
|
25
|
+
* The healthy shape is a mix: flat Sections for the journey, one collapsed
|
|
26
|
+
* Group at the bottom.
|
|
27
|
+
*
|
|
28
|
+
* Neither is a tree. Arbitrary-depth *data* browsing (a file tree, a
|
|
29
|
+
* namespace → resource drill-down) needs `role="tree"` with roving tabindex,
|
|
30
|
+
* typeahead and aria-level — a different component, not a deeper SideNav.
|
|
31
|
+
*/
|
|
32
|
+
|
|
7
33
|
// --- Root ---
|
|
8
34
|
|
|
9
35
|
interface RootProps {
|
|
@@ -93,6 +119,8 @@ function Group({children, label, groupKey, defaultExpanded}: GroupProps) {
|
|
|
93
119
|
const {expandedGroups, toggleGroup, expandGroup, activeValue} = useSideNav()
|
|
94
120
|
const isExpanded = expandedGroups.has(key)
|
|
95
121
|
const groupRef = useRef<HTMLDivElement>(null)
|
|
122
|
+
const triggerId = useId()
|
|
123
|
+
const panelId = useId()
|
|
96
124
|
|
|
97
125
|
// Auto-expand if this group contains the active item. Uses expandGroup (not
|
|
98
126
|
// toggleGroup) so it never races the defaultExpanded effect into a closed
|
|
@@ -125,9 +153,11 @@ function Group({children, label, groupKey, defaultExpanded}: GroupProps) {
|
|
|
125
153
|
<html.div ref={groupRef} style={styles.group}>
|
|
126
154
|
<html.button
|
|
127
155
|
type="button"
|
|
156
|
+
id={triggerId}
|
|
128
157
|
onClick={() => toggleGroup(key)}
|
|
129
158
|
style={[styles.groupTrigger, hasActiveChild && styles.groupTriggerActive]}
|
|
130
159
|
aria-expanded={isExpanded}
|
|
160
|
+
aria-controls={isExpanded ? panelId : undefined}
|
|
131
161
|
>
|
|
132
162
|
<html.span style={[styles.chevron, isExpanded && styles.chevronOpen]}>
|
|
133
163
|
<svg
|
|
@@ -145,7 +175,11 @@ function Group({children, label, groupKey, defaultExpanded}: GroupProps) {
|
|
|
145
175
|
</html.span>
|
|
146
176
|
{label}
|
|
147
177
|
</html.button>
|
|
148
|
-
{isExpanded
|
|
178
|
+
{isExpanded ? (
|
|
179
|
+
<html.div id={panelId} role="group" aria-labelledby={triggerId} style={styles.items}>
|
|
180
|
+
{children}
|
|
181
|
+
</html.div>
|
|
182
|
+
) : null}
|
|
149
183
|
</html.div>
|
|
150
184
|
)
|
|
151
185
|
}
|
|
@@ -159,15 +193,24 @@ interface SectionProps {
|
|
|
159
193
|
|
|
160
194
|
/**
|
|
161
195
|
* A static, non-collapsible grouping: an always-visible uppercase header with
|
|
162
|
-
* its items below. Unlike `Group` there is no chevron, toggle, or hidden state
|
|
163
|
-
*
|
|
164
|
-
*
|
|
196
|
+
* its items below. Unlike `Group` there is no chevron, toggle, or hidden state.
|
|
197
|
+
*
|
|
198
|
+
* Reach for this first — see the note at the top of this file for when a
|
|
199
|
+
* collapsible `Group` is the better call. The label is exposed as the group's
|
|
200
|
+
* accessible name, so a screen reader announces "Networking, group" on the way
|
|
201
|
+
* into its items instead of dropping the user into an undifferentiated run of
|
|
202
|
+
* thirty buttons.
|
|
165
203
|
*/
|
|
166
204
|
function Section({children, label}: SectionProps) {
|
|
205
|
+
const labelId = useId()
|
|
167
206
|
return (
|
|
168
207
|
<html.div style={styles.section}>
|
|
169
|
-
<html.div style={styles.sectionLabel}>
|
|
170
|
-
|
|
208
|
+
<html.div id={labelId} style={styles.sectionLabel}>
|
|
209
|
+
{label}
|
|
210
|
+
</html.div>
|
|
211
|
+
<html.div role="group" aria-labelledby={labelId} style={styles.items}>
|
|
212
|
+
{children}
|
|
213
|
+
</html.div>
|
|
171
214
|
</html.div>
|
|
172
215
|
)
|
|
173
216
|
}
|
|
@@ -51,6 +51,13 @@ export const styles = css.create({
|
|
|
51
51
|
chevronOpen: {
|
|
52
52
|
transform: 'rotate(90deg)',
|
|
53
53
|
},
|
|
54
|
+
// The `role="group"` wrapper around a Section's / an expanded Group's items.
|
|
55
|
+
// Purely structural: it exists to carry the accessible name, so it must not
|
|
56
|
+
// change how the items stack.
|
|
57
|
+
items: {
|
|
58
|
+
display: 'flex',
|
|
59
|
+
flexDirection: 'column',
|
|
60
|
+
},
|
|
54
61
|
// Static (non-collapsible) section — see SideNav.Section.
|
|
55
62
|
section: {
|
|
56
63
|
display: 'flex',
|
|
@@ -3,6 +3,7 @@ import {css, html} from 'react-strict-dom'
|
|
|
3
3
|
import {Stack} from './Stack'
|
|
4
4
|
import {colors} from '@duro-app/tokens/tokens/colors.css'
|
|
5
5
|
import {spacing} from '@duro-app/tokens/tokens/spacing.css'
|
|
6
|
+
import {SPACING_KEYS} from '@duro-app/tokens/keys'
|
|
6
7
|
|
|
7
8
|
const meta: Meta<typeof Stack> = {
|
|
8
9
|
title: 'Layout/Stack',
|
|
@@ -10,7 +11,7 @@ const meta: Meta<typeof Stack> = {
|
|
|
10
11
|
argTypes: {
|
|
11
12
|
gap: {
|
|
12
13
|
control: 'select',
|
|
13
|
-
options: [
|
|
14
|
+
options: [...SPACING_KEYS],
|
|
14
15
|
},
|
|
15
16
|
align: {
|
|
16
17
|
control: 'select',
|
|
@@ -87,7 +88,7 @@ export const CenterAligned: Story = {
|
|
|
87
88
|
|
|
88
89
|
export const AllGaps: Story = {
|
|
89
90
|
render: () => {
|
|
90
|
-
const gaps =
|
|
91
|
+
const gaps = SPACING_KEYS
|
|
91
92
|
return (
|
|
92
93
|
<Stack gap="xl">
|
|
93
94
|
{gaps.map((g) => (
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type {ReactNode} from 'react'
|
|
2
2
|
import {html} from 'react-strict-dom'
|
|
3
|
+
import type {SpacingToken} from '@duro-app/tokens/keys'
|
|
3
4
|
import {styles} from './styles.css'
|
|
4
5
|
|
|
5
|
-
export type SpacingKey =
|
|
6
|
+
export type SpacingKey = SpacingToken
|
|
6
7
|
|
|
7
8
|
interface StackProps {
|
|
8
9
|
gap?: SpacingKey
|
|
@@ -19,7 +20,7 @@ const gapMap = {
|
|
|
19
20
|
xl: styles.gapXl,
|
|
20
21
|
xxl: styles.gapXxl,
|
|
21
22
|
xxxl: styles.gapXxxl,
|
|
22
|
-
} as const
|
|
23
|
+
} as const satisfies Record<SpacingToken, unknown>
|
|
23
24
|
|
|
24
25
|
const alignMap = {
|
|
25
26
|
start: styles.alignStart,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {html} from 'react-strict-dom'
|
|
2
2
|
import {Icon} from '../Icon'
|
|
3
|
-
import type {IconName} from '../Icon'
|
|
3
|
+
import type {IconName, IconSize} from '../Icon'
|
|
4
4
|
import {styles} from './styles.css'
|
|
5
5
|
|
|
6
6
|
export type StatusIconName = Extract<
|
|
@@ -11,11 +11,11 @@ export type StatusIconVariant = 'error' | 'success' | 'warning' | 'info' | 'mute
|
|
|
11
11
|
|
|
12
12
|
interface StatusIconProps {
|
|
13
13
|
name: StatusIconName
|
|
14
|
-
size?:
|
|
14
|
+
size?: IconSize
|
|
15
15
|
variant?: StatusIconVariant
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
export function StatusIcon({name, size =
|
|
18
|
+
export function StatusIcon({name, size = 'xxl', variant = 'muted'}: StatusIconProps) {
|
|
19
19
|
return (
|
|
20
20
|
<html.div style={[styles.root, styles[variant]]}>
|
|
21
21
|
<Icon name={name} size={size} />
|
|
@@ -920,39 +920,36 @@ export const Responsive: Story = {
|
|
|
920
920
|
]
|
|
921
921
|
return (
|
|
922
922
|
<html.div style={responsiveStyles.resizable}>
|
|
923
|
-
<Table.
|
|
924
|
-
<Table.SortChip options={sortable} value={sort} onChange={setSort} />
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
923
|
+
<Table.Root
|
|
924
|
+
sortChip={<Table.SortChip options={sortable} value={sort} onChange={setSort} />}
|
|
925
|
+
>
|
|
926
|
+
<Table.Header>
|
|
927
|
+
<Table.Row>
|
|
928
|
+
<Table.HeaderCell label="Principal">Principal</Table.HeaderCell>
|
|
929
|
+
<Table.HeaderCell label="Role">Role</Table.HeaderCell>
|
|
930
|
+
<Table.HeaderCell label="Resource">Resource</Table.HeaderCell>
|
|
931
|
+
<Table.HeaderCell label="Expires">Expires</Table.HeaderCell>
|
|
932
|
+
<Table.HeaderCell label="Granted by">Granted by</Table.HeaderCell>
|
|
933
|
+
<Table.HeaderCell label="Actions">Actions</Table.HeaderCell>
|
|
934
|
+
</Table.Row>
|
|
935
|
+
</Table.Header>
|
|
936
|
+
<Table.Body>
|
|
937
|
+
{grants.map((g) => (
|
|
938
|
+
<Table.Row key={g.principal}>
|
|
939
|
+
<Table.Cell>{g.principal}</Table.Cell>
|
|
940
|
+
<Table.Cell>{g.role}</Table.Cell>
|
|
941
|
+
<Table.Cell>{g.resource}</Table.Cell>
|
|
942
|
+
<Table.Cell>{g.expires}</Table.Cell>
|
|
943
|
+
<Table.Cell>{g.granted_by}</Table.Cell>
|
|
944
|
+
<Table.Cell isActions>
|
|
945
|
+
<Button size="small" variant="secondary">
|
|
946
|
+
Revoke
|
|
947
|
+
</Button>
|
|
948
|
+
</Table.Cell>
|
|
936
949
|
</Table.Row>
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
<Table.Row key={g.principal}>
|
|
941
|
-
<Table.Cell>{g.principal}</Table.Cell>
|
|
942
|
-
<Table.Cell>{g.role}</Table.Cell>
|
|
943
|
-
<Table.Cell>{g.resource}</Table.Cell>
|
|
944
|
-
<Table.Cell>{g.expires}</Table.Cell>
|
|
945
|
-
<Table.Cell>{g.granted_by}</Table.Cell>
|
|
946
|
-
<Table.Cell isActions>
|
|
947
|
-
<Button size="small" variant="secondary">
|
|
948
|
-
Revoke
|
|
949
|
-
</Button>
|
|
950
|
-
</Table.Cell>
|
|
951
|
-
</Table.Row>
|
|
952
|
-
))}
|
|
953
|
-
</Table.Body>
|
|
954
|
-
</Table.Root>
|
|
955
|
-
</Table.Container>
|
|
950
|
+
))}
|
|
951
|
+
</Table.Body>
|
|
952
|
+
</Table.Root>
|
|
956
953
|
</html.div>
|
|
957
954
|
)
|
|
958
955
|
},
|
|
@@ -445,7 +445,6 @@ export function HeaderCell({
|
|
|
445
445
|
width: _width,
|
|
446
446
|
compactWidth: _compactWidth,
|
|
447
447
|
label: _label,
|
|
448
|
-
isActions,
|
|
449
448
|
'aria-label': ariaLabel,
|
|
450
449
|
}: {
|
|
451
450
|
children?: ReactNode
|
|
@@ -465,20 +464,8 @@ export function HeaderCell({
|
|
|
465
464
|
* text content is used as the label automatically. Required when children
|
|
466
465
|
* contain JSX (icon + text, sort indicator, etc.). */
|
|
467
466
|
label?: string
|
|
468
|
-
/**
|
|
469
|
-
* @deprecated Has no effect on HeaderCell. Pass `isActions` on the
|
|
470
|
-
* matching `Table.Cell` instead — that's where the stack-mode footer
|
|
471
|
-
* layout actually applies.
|
|
472
|
-
*/
|
|
473
|
-
isActions?: boolean
|
|
474
467
|
'aria-label'?: string
|
|
475
468
|
}) {
|
|
476
|
-
if (isActions === true) {
|
|
477
|
-
devWarnOnce(
|
|
478
|
-
'headerCell-isActions',
|
|
479
|
-
'HeaderCell `isActions` prop has no effect and is deprecated. Pass `isActions` on the matching Table.Cell instead.',
|
|
480
|
-
)
|
|
481
|
-
}
|
|
482
469
|
const {size, variant} = useTable()
|
|
483
470
|
const {index, total} = useContext(CellIndexContext)
|
|
484
471
|
const isLast = variant === 'bordered' && index === total - 1
|
|
@@ -123,7 +123,7 @@ function ToastItem({entry, onDismiss}: {entry: ToastEntry; onDismiss: (id: strin
|
|
|
123
123
|
style={[styles.toast, styles[variant]]}
|
|
124
124
|
>
|
|
125
125
|
<html.div style={[styles.iconWrap, variantIconStyle[variant]]}>
|
|
126
|
-
<Icon name={variantIcon[variant]} size=
|
|
126
|
+
<Icon name={variantIcon[variant]} size="md" />
|
|
127
127
|
</html.div>
|
|
128
128
|
<html.div style={styles.content}>
|
|
129
129
|
<html.div style={styles.message}>{message}</html.div>
|
|
@@ -146,7 +146,7 @@ function ToastItem({entry, onDismiss}: {entry: ToastEntry; onDismiss: (id: strin
|
|
|
146
146
|
style={styles.closeBtn}
|
|
147
147
|
onClick={() => onDismiss(id)}
|
|
148
148
|
>
|
|
149
|
-
<Icon name="x-circle" size=
|
|
149
|
+
<Icon name="x-circle" size="sm" />
|
|
150
150
|
</html.button>
|
|
151
151
|
</html.div>
|
|
152
152
|
)
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import {createContext, useContext} from 'react'
|
|
2
2
|
import type {ToggleSize} from '../Toggle/Toggle'
|
|
3
|
+
import type {Orientation} from '../../shared/types'
|
|
3
4
|
|
|
4
|
-
export type Orientation
|
|
5
|
+
export type {Orientation}
|
|
5
6
|
|
|
6
7
|
export interface ToggleGroupContextValue {
|
|
7
8
|
value: string[]
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
type Row,
|
|
12
12
|
} from '@tanstack/react-table'
|
|
13
13
|
import {useVirtualizer} from '@tanstack/react-virtual'
|
|
14
|
-
import {breakpointsPx} from '@duro-app/tokens/tokens/breakpoints.css'
|
|
14
|
+
import {breakpointsPx, type Breakpoint} from '@duro-app/tokens/tokens/breakpoints.css'
|
|
15
15
|
import {styles} from './styles.css'
|
|
16
16
|
|
|
17
17
|
// Measure + commit before paint on the client; no-op-safe on the server.
|
|
@@ -67,7 +67,7 @@ interface VirtualTableProps<TData> {
|
|
|
67
67
|
* line. Cards disable windowing (variable heights), so keep phone lists
|
|
68
68
|
* reasonably short. Set `responsive={false}` to disable entirely.
|
|
69
69
|
*/
|
|
70
|
-
stackBelow?: number
|
|
70
|
+
stackBelow?: Breakpoint | number
|
|
71
71
|
/** Opt out of the card-up-on-narrow behavior. Default true. */
|
|
72
72
|
responsive?: boolean
|
|
73
73
|
emptyLabel?: ReactNode
|
|
@@ -98,7 +98,7 @@ export function VirtualTable<TData>({
|
|
|
98
98
|
estimateRowHeight = 44,
|
|
99
99
|
maxHeight = '70vh',
|
|
100
100
|
virtualizeThreshold = 150,
|
|
101
|
-
stackBelow =
|
|
101
|
+
stackBelow = 'sm',
|
|
102
102
|
responsive = true,
|
|
103
103
|
emptyLabel,
|
|
104
104
|
}: VirtualTableProps<TData>) {
|
|
@@ -125,6 +125,7 @@ export function VirtualTable<TData>({
|
|
|
125
125
|
// so `virtualize` is forced off while stacked.
|
|
126
126
|
const wrapRef = useRef<HTMLDivElement>(null)
|
|
127
127
|
const [stacked, setStacked] = useState(false)
|
|
128
|
+
const stackBelowPx = typeof stackBelow === 'number' ? stackBelow : breakpointsPx[stackBelow]
|
|
128
129
|
useIsoLayoutEffect(() => {
|
|
129
130
|
if (!responsive) {
|
|
130
131
|
setStacked(false)
|
|
@@ -133,7 +134,7 @@ export function VirtualTable<TData>({
|
|
|
133
134
|
const el = wrapRef.current
|
|
134
135
|
if (!el || typeof ResizeObserver === 'undefined') return
|
|
135
136
|
const apply = (width: number) => {
|
|
136
|
-
if (width > 0) setStacked(width <
|
|
137
|
+
if (width > 0) setStacked(width < stackBelowPx)
|
|
137
138
|
}
|
|
138
139
|
apply(el.getBoundingClientRect().width)
|
|
139
140
|
const ro = new ResizeObserver((entries) => {
|
|
@@ -144,7 +145,7 @@ export function VirtualTable<TData>({
|
|
|
144
145
|
})
|
|
145
146
|
ro.observe(el)
|
|
146
147
|
return () => ro.disconnect()
|
|
147
|
-
}, [responsive,
|
|
148
|
+
}, [responsive, stackBelowPx])
|
|
148
149
|
|
|
149
150
|
const virtualize = rows.length > virtualizeThreshold && !stacked
|
|
150
151
|
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import {useRef, useState, useEffect} from 'react'
|
|
2
|
+
import {breakpointsPx, type Breakpoint} from '@duro-app/tokens/tokens/breakpoints.css'
|
|
2
3
|
|
|
3
4
|
export type ContainerSize = 'compact' | 'default' | 'spacious'
|
|
4
5
|
|
|
5
6
|
interface UseContainerQueryOptions {
|
|
6
|
-
compactBelow?: number
|
|
7
|
-
spaciousAbove?: number
|
|
7
|
+
compactBelow?: Breakpoint | number
|
|
8
|
+
spaciousAbove?: Breakpoint | number
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function toPx(value: Breakpoint | number): number {
|
|
12
|
+
return typeof value === 'number' ? value : breakpointsPx[value]
|
|
8
13
|
}
|
|
9
14
|
|
|
10
15
|
export function useContainerQuery<T extends HTMLElement = HTMLElement>(
|
|
@@ -13,7 +18,9 @@ export function useContainerQuery<T extends HTMLElement = HTMLElement>(
|
|
|
13
18
|
ref: React.RefObject<T | null>
|
|
14
19
|
size: ContainerSize
|
|
15
20
|
} {
|
|
16
|
-
const {compactBelow =
|
|
21
|
+
const {compactBelow: compactOpt = 'xs', spaciousAbove: spaciousOpt = 'md'} = options
|
|
22
|
+
const compactBelow = toPx(compactOpt)
|
|
23
|
+
const spaciousAbove = toPx(spaciousOpt)
|
|
17
24
|
const ref = useRef<T | null>(null)
|
|
18
25
|
const [size, setSize] = useState<ContainerSize>('default')
|
|
19
26
|
|