@pienter/ui 0.2.0 → 0.5.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/CHANGELOG.md +259 -0
- package/CONVENTIONS.md +297 -385
- package/README.md +3 -18
- package/components/feedback/toast/toast.css +1 -1
- package/components/form/combobox/Combobox.vue +21 -38
- package/components/form/combobox/combobox.css +1 -1
- package/components/form/form/Form.vue +1 -2
- package/components/form/label/label.css +1 -1
- package/components/form/number-field/NumberField.vue +1 -1
- package/components/form/number-field/number-field.css +1 -1
- package/components/form/radio-group/RadioGroup.vue +2 -5
- package/components/form/slider/slider.css +2 -3
- package/components/form/tags-input/tags-input.css +1 -2
- package/components/form/textarea/textarea.css +1 -1
- package/components/layout/accordion/Accordion.vue +6 -27
- package/components/layout/collapsible/Collapsible.vue +9 -19
- package/components/layout/table/table.css +3 -3
- package/components/navigation/pagination/Pagination.vue +1 -1
- package/components/navigation/pagination/PaginationFooter.vue +23 -0
- package/components/navigation/pagination/pagination.css +128 -29
- package/components/navigation/sidebar/Sidebar.vue +326 -84
- package/components/navigation/sidebar/SidebarMenuItem.vue +199 -0
- package/components/navigation/sidebar/context.ts +21 -0
- package/components/navigation/sidebar/sidebar.css +346 -78
- package/components/navigation/sidebar/types.ts +13 -1
- package/components/overlay/alert-dialog/AlertDialog.vue +10 -31
- package/components/overlay/command/Command.vue +10 -38
- package/components/overlay/command/command.css +1 -1
- package/components/overlay/dropdown-menu/DropdownMenu.vue +37 -62
- package/components/overlay/modal/Modal.vue +7 -28
- package/components/overlay/popover/Popover.vue +9 -31
- package/components/overlay/sheet/Sheet.vue +7 -28
- package/components/overlay/tooltip/Tooltip.vue +14 -19
- package/{utils/controllers/dialog.ts → composables/useDialog.ts} +90 -78
- package/composables/useDisclosure.ts +113 -0
- package/composables/useEventListener.ts +16 -0
- package/{utils/controllers/menu.ts → composables/useMenu.ts} +66 -102
- package/{utils/controllers/popover.ts → composables/usePopover.ts} +107 -120
- package/package.json +6 -38
- package/styles/0-settings/colors.css +8 -1
- package/styles/0-settings/layout.css +18 -0
- package/styles/0-settings/motion.css +2 -2
- package/styles/0-settings/spacing.css +3 -1
- package/utils/navigation/sidebar.ts +97 -0
- package/utils/validation/form.ts +6 -9
- package/components/action/button/Button.astro +0 -95
- package/components/action/button/IconButton.astro +0 -86
- package/components/display/avatar/Avatar.astro +0 -17
- package/components/display/avatar/AvatarStack.astro +0 -9
- package/components/display/badge/Badge.astro +0 -15
- package/components/display/empty/Empty.astro +0 -9
- package/components/display/icon/Icon.astro +0 -52
- package/components/feedback/alert/Alert.astro +0 -52
- package/components/feedback/progress/Progress.astro +0 -68
- package/components/feedback/skeleton/Skeleton.astro +0 -32
- package/components/feedback/spinner/Spinner.astro +0 -25
- package/components/feedback/toast/Toast.astro +0 -50
- package/components/form/checkbox/Checkbox.astro +0 -79
- package/components/form/date-input/DateInput.astro +0 -105
- package/components/form/form/Form.astro +0 -106
- package/components/form/input-otp/InputOTP.astro +0 -147
- package/components/form/label/Label.astro +0 -13
- package/components/form/number-field/NumberField.astro +0 -142
- package/components/form/radio-group/RadioGroup.astro +0 -105
- package/components/form/select/Select.astro +0 -105
- package/components/form/slider/Slider.astro +0 -205
- package/components/form/switch/Switch.astro +0 -75
- package/components/form/tags-input/TagsInput.astro +0 -153
- package/components/form/text-input/TextInput.astro +0 -84
- package/components/form/textarea/Textarea.astro +0 -86
- package/components/layout/card/Card.astro +0 -13
- package/components/layout/separator/Separator.astro +0 -31
- package/components/layout/table/Table.astro +0 -116
- package/components/navigation/breadcrumb/Breadcrumb.astro +0 -36
- package/components/navigation/navbar/Navbar.astro +0 -62
- package/components/navigation/sidebar/Sidebar.astro +0 -132
- package/components/navigation/sidebar/SidebarItemRender.astro +0 -83
- package/components/navigation/sidebar/SidebarItemRender.vue +0 -98
- package/components/overlay/alert-dialog/AlertDialog.astro +0 -112
- package/components/overlay/modal/Modal.astro +0 -66
- package/utils/controllers/disclosure.ts +0 -117
- package/utils/controllers/form.ts +0 -524
- package/utils/controllers/index.ts +0 -39
- package/utils/controllers/number-field.ts +0 -103
- package/utils/controllers/otp.ts +0 -252
- package/utils/controllers/sidebar.ts +0 -610
- package/utils/controllers/slider.ts +0 -336
- package/utils/controllers/tags-input.ts +0 -255
- package/utils/controllers/toast.ts +0 -426
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ComputedRef, InjectionKey, Ref } from 'vue';
|
|
2
|
+
import type { SubmenuPosition } from '../../../utils/navigation/sidebar.js';
|
|
3
|
+
|
|
4
|
+
/** What `Sidebar.vue` hands down to the recursive `SidebarMenuItem`. */
|
|
5
|
+
export interface SidebarContext {
|
|
6
|
+
activeHref: ComputedRef<string | null>;
|
|
7
|
+
expandedKeys: Ref<Set<string>>;
|
|
8
|
+
/** Group key whose collapsed-rail flyout is open, or null. */
|
|
9
|
+
openKey: Ref<string | null>;
|
|
10
|
+
submenuActive: ComputedRef<boolean>;
|
|
11
|
+
/** Shared by every flyout; null before the first measure. */
|
|
12
|
+
submenuPos: Ref<SubmenuPosition | null>;
|
|
13
|
+
|
|
14
|
+
onToggle: (key: string, isTopLevel: boolean) => void;
|
|
15
|
+
/** `topLevelKey` is null for a plain entry, which dismisses the flyout. */
|
|
16
|
+
onEntryEnter: (topLevelKey: string | null) => void;
|
|
17
|
+
onEntryKeydown: (event: KeyboardEvent) => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const SIDEBAR_CONTEXT: InjectionKey<SidebarContext> =
|
|
21
|
+
Symbol('pui-sidebar');
|
|
@@ -1,10 +1,5 @@
|
|
|
1
1
|
@layer components {
|
|
2
|
-
/*
|
|
3
|
-
* Block — the navigation landmark, also the drawer mount point on
|
|
4
|
-
* mobile. Desktop: a fixed-width column owned by the consumer's
|
|
5
|
-
* layout shell. Mobile: occupies the full viewport with a backdrop
|
|
6
|
-
* sibling and a slide-in panel.
|
|
7
|
-
* ============================================================ */
|
|
2
|
+
/* Block — the navigation landmark, also the drawer mount point on mobile */
|
|
8
3
|
.pui-sidebar {
|
|
9
4
|
--pui-sidebar-width: var(--space-sidebar-width);
|
|
10
5
|
--pui-sidebar-width-collapsed: var(--space-sidebar-width-collapsed);
|
|
@@ -21,10 +16,19 @@
|
|
|
21
16
|
width: var(--pui-sidebar-width-collapsed);
|
|
22
17
|
}
|
|
23
18
|
|
|
24
|
-
/*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
|
|
19
|
+
/* Lift the root, not the flyout: a fixed child escapes overflow but not
|
|
20
|
+
* a stacking context. Scoped to "flyout open" because this rule is itself
|
|
21
|
+
* a context trigger, and unscoped it trapped the mobile drawer. */
|
|
22
|
+
@media (min-width: 768px) {
|
|
23
|
+
.pui-sidebar:has(.pui-sidebar__group[data-submenu-state='open']) {
|
|
24
|
+
/* `--layer-below` only means "behind the rail" while the sidebar
|
|
25
|
+
is the context containing it. */
|
|
26
|
+
isolation: isolate;
|
|
27
|
+
z-index: var(--layer-3);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/* Panel — the visible column / drawer body containing brand, menu, footer */
|
|
28
32
|
.pui-sidebar__panel {
|
|
29
33
|
display: flex;
|
|
30
34
|
flex-direction: column;
|
|
@@ -35,17 +39,12 @@
|
|
|
35
39
|
background: var(--bg-clr-surface);
|
|
36
40
|
}
|
|
37
41
|
|
|
38
|
-
/*
|
|
39
|
-
* Backdrop — only meaningful in mobile drawer mode. Hidden on desktop.
|
|
40
|
-
* ============================================================ */
|
|
42
|
+
/* Backdrop — only meaningful in mobile drawer mode */
|
|
41
43
|
.pui-sidebar__backdrop {
|
|
42
44
|
display: none;
|
|
43
45
|
}
|
|
44
46
|
|
|
45
|
-
/*
|
|
46
|
-
* Brand region — top of the panel. Layout owned by consumer markup
|
|
47
|
-
* inside the slot; we just provide consistent spacing.
|
|
48
|
-
* ============================================================ */
|
|
47
|
+
/* Brand region — top of the panel */
|
|
49
48
|
.pui-sidebar__brand {
|
|
50
49
|
display: flex;
|
|
51
50
|
align-items: center;
|
|
@@ -53,29 +52,48 @@
|
|
|
53
52
|
padding-block: var(--space-2xs);
|
|
54
53
|
}
|
|
55
54
|
|
|
56
|
-
/*
|
|
57
|
-
*
|
|
55
|
+
/* `data-keep-when-collapsed` survives; the rest would spill out of the
|
|
56
|
+
* rail, which is narrower than most product names. */
|
|
58
57
|
.pui-sidebar[data-state='collapsed']
|
|
59
58
|
.pui-sidebar__brand
|
|
60
59
|
> :not([data-keep-when-collapsed]) {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
60
|
+
display: none;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.pui-sidebar[data-state='collapsed'] .pui-sidebar__brand {
|
|
64
|
+
justify-content: center;
|
|
64
65
|
}
|
|
65
66
|
|
|
66
|
-
/*
|
|
67
|
-
|
|
68
|
-
* ============================================================ */
|
|
69
|
-
.pui-sidebar__menu {
|
|
67
|
+
/* Nav lists — the two data-driven regions */
|
|
68
|
+
:is(.pui-sidebar__top-nav, .pui-sidebar__bottom-nav) {
|
|
70
69
|
list-style: none;
|
|
71
70
|
margin: 0;
|
|
72
71
|
padding: 0;
|
|
73
72
|
display: flex;
|
|
74
73
|
flex-direction: column;
|
|
75
74
|
gap: var(--space-3xs);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.pui-sidebar__top-nav {
|
|
78
|
+
--pui-sidebar-ring-space: calc(
|
|
79
|
+
var(--outline-offset) + var(--outline-width)
|
|
80
|
+
);
|
|
81
|
+
|
|
76
82
|
flex: 1;
|
|
77
83
|
min-height: 0;
|
|
78
84
|
overflow-y: auto;
|
|
85
|
+
|
|
86
|
+
/* A scroll container clips at its padding box, which sliced the focus
|
|
87
|
+
ring off. The padding makes room inside the clip, the negative margin
|
|
88
|
+
puts the list back. */
|
|
89
|
+
padding-inline: var(--pui-sidebar-ring-space);
|
|
90
|
+
margin-inline: calc(var(--pui-sidebar-ring-space) * -1);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/* Only as tall as its entries, and it must not clip its own flyouts. */
|
|
94
|
+
.pui-sidebar__bottom-nav {
|
|
95
|
+
flex: none;
|
|
96
|
+
overflow: visible;
|
|
79
97
|
}
|
|
80
98
|
|
|
81
99
|
.pui-sidebar__item {
|
|
@@ -84,10 +102,7 @@
|
|
|
84
102
|
padding: 0;
|
|
85
103
|
}
|
|
86
104
|
|
|
87
|
-
/*
|
|
88
|
-
* Link / group-toggle — share visual treatment so the menu reads
|
|
89
|
-
* as one column of equally-weighted entries.
|
|
90
|
-
* ============================================================ */
|
|
105
|
+
/* Link / group-toggle — share visual treatment so the menu reads as one column of equally-weighted entries */
|
|
91
106
|
.pui-sidebar__link,
|
|
92
107
|
.pui-sidebar__group-toggle {
|
|
93
108
|
display: flex;
|
|
@@ -113,8 +128,9 @@
|
|
|
113
128
|
color: var(--text-clr-brand);
|
|
114
129
|
}
|
|
115
130
|
|
|
131
|
+
/* Not the neutral fill hover uses, or the two are indistinguishable. */
|
|
116
132
|
.pui-sidebar__link[aria-current='page'] {
|
|
117
|
-
background: var(--bg-clr-
|
|
133
|
+
background: var(--bg-clr-brand-soft);
|
|
118
134
|
color: var(--text-clr-brand-selected);
|
|
119
135
|
}
|
|
120
136
|
|
|
@@ -123,10 +139,19 @@
|
|
|
123
139
|
pointer-events: none;
|
|
124
140
|
}
|
|
125
141
|
|
|
142
|
+
/* Outset. Colour follows the entry's tone as `--btn-ring` does for
|
|
143
|
+
* Button, so the ring never fights the fill it sits on. */
|
|
126
144
|
.pui-sidebar__link:focus-visible,
|
|
127
|
-
.pui-sidebar__group-toggle:focus-visible
|
|
128
|
-
|
|
129
|
-
outline
|
|
145
|
+
.pui-sidebar__group-toggle:focus-visible,
|
|
146
|
+
.pui-sidebar__group-caret-toggle:focus-visible {
|
|
147
|
+
outline: var(--outline-width) solid
|
|
148
|
+
var(--pui-sidebar-ring, var(--border-clr-strong));
|
|
149
|
+
outline-offset: var(--outline-offset);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.pui-sidebar__link[aria-current='page'],
|
|
153
|
+
.pui-sidebar__group[data-active='true'] > .pui-sidebar__group-toggle {
|
|
154
|
+
--pui-sidebar-ring: var(--outline-clr-base);
|
|
130
155
|
}
|
|
131
156
|
|
|
132
157
|
.pui-sidebar__link-label {
|
|
@@ -150,9 +175,7 @@
|
|
|
150
175
|
text-align: center;
|
|
151
176
|
}
|
|
152
177
|
|
|
153
|
-
/*
|
|
154
|
-
* Group — collapsible container.
|
|
155
|
-
* ============================================================ */
|
|
178
|
+
/* Group — collapsible container */
|
|
156
179
|
.pui-sidebar__group {
|
|
157
180
|
list-style: none;
|
|
158
181
|
margin: 0;
|
|
@@ -164,6 +187,44 @@
|
|
|
164
187
|
transition: transform 150ms ease;
|
|
165
188
|
}
|
|
166
189
|
|
|
190
|
+
/* Only when the group carries an `href` — the row navigates, so the
|
|
191
|
+
* disclosure needs its own control. */
|
|
192
|
+
.pui-sidebar__group-caret-toggle {
|
|
193
|
+
display: flex;
|
|
194
|
+
align-items: center;
|
|
195
|
+
margin-inline-start: auto;
|
|
196
|
+
padding: var(--space-3xs);
|
|
197
|
+
border: 0;
|
|
198
|
+
border-radius: var(--radius-sm);
|
|
199
|
+
background: transparent;
|
|
200
|
+
color: inherit;
|
|
201
|
+
cursor: pointer;
|
|
202
|
+
|
|
203
|
+
& .pui-sidebar__group-caret {
|
|
204
|
+
margin-inline-start: 0;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
&:hover {
|
|
208
|
+
background: var(--bg-clr-accent);
|
|
209
|
+
color: var(--text-clr-brand);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
.pui-sidebar__group:has(> .pui-sidebar__group-caret-toggle) {
|
|
214
|
+
display: flex;
|
|
215
|
+
align-items: center;
|
|
216
|
+
flex-wrap: wrap;
|
|
217
|
+
|
|
218
|
+
& > .pui-sidebar__group-toggle {
|
|
219
|
+
flex: 1;
|
|
220
|
+
min-width: 0;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
& > .pui-sidebar__group-items {
|
|
224
|
+
flex-basis: 100%;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
167
228
|
.pui-sidebar__group-toggle[aria-expanded='true'] .pui-sidebar__group-caret {
|
|
168
229
|
transform: rotate(180deg);
|
|
169
230
|
}
|
|
@@ -186,9 +247,7 @@
|
|
|
186
247
|
display: none;
|
|
187
248
|
}
|
|
188
249
|
|
|
189
|
-
/*
|
|
190
|
-
* Section header — non-interactive label.
|
|
191
|
-
* ============================================================ */
|
|
250
|
+
/* Section header — non-interactive label */
|
|
192
251
|
.pui-sidebar__section {
|
|
193
252
|
list-style: none;
|
|
194
253
|
margin: 0;
|
|
@@ -203,48 +262,190 @@
|
|
|
203
262
|
color: var(--text-clr-muted);
|
|
204
263
|
}
|
|
205
264
|
|
|
206
|
-
/*
|
|
207
|
-
|
|
208
|
-
* ============================================================ */
|
|
209
|
-
.pui-sidebar__footer {
|
|
265
|
+
/* Bottom region — foot of the panel */
|
|
266
|
+
.pui-sidebar__bottom {
|
|
210
267
|
margin-block-start: auto;
|
|
211
268
|
padding-block-start: var(--space-2xs);
|
|
212
269
|
border-block-start: var(--stroke-sm) solid var(--border-clr-base);
|
|
213
270
|
}
|
|
214
271
|
|
|
215
|
-
/*
|
|
216
|
-
*
|
|
217
|
-
*
|
|
218
|
-
|
|
219
|
-
.
|
|
220
|
-
|
|
221
|
-
.pui-sidebar[data-state='collapsed'] .pui-
|
|
222
|
-
|
|
223
|
-
display: none;
|
|
272
|
+
/* Collapsed — the icon rail. Every selector is scoped with `>` to
|
|
273
|
+
* entries directly in a nav list; a descendant selector would reach into
|
|
274
|
+
* an open flyout and collapse its contents too. */
|
|
275
|
+
|
|
276
|
+
/* The expanded padding would leave ~2rem for a caption. Wide enough
|
|
277
|
+
* here to also hold the focus ring. */
|
|
278
|
+
.pui-sidebar[data-state='collapsed'] .pui-sidebar__panel {
|
|
279
|
+
padding-inline: var(--space-2xs);
|
|
224
280
|
}
|
|
225
281
|
|
|
226
|
-
.pui-sidebar[data-state='collapsed']
|
|
227
|
-
|
|
282
|
+
.pui-sidebar[data-state='collapsed']
|
|
283
|
+
:is(.pui-sidebar__top-nav, .pui-sidebar__bottom-nav)
|
|
284
|
+
> .pui-sidebar__item
|
|
285
|
+
> .pui-sidebar__link,
|
|
286
|
+
.pui-sidebar[data-state='collapsed']
|
|
287
|
+
:is(.pui-sidebar__top-nav, .pui-sidebar__bottom-nav)
|
|
288
|
+
> .pui-sidebar__group
|
|
289
|
+
> .pui-sidebar__group-toggle {
|
|
290
|
+
flex-direction: column;
|
|
228
291
|
justify-content: center;
|
|
292
|
+
gap: var(--space-3xs);
|
|
229
293
|
padding-inline: var(--space-3xs);
|
|
294
|
+
text-align: center;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
.pui-sidebar[data-state='collapsed']
|
|
298
|
+
:is(.pui-sidebar__top-nav, .pui-sidebar__bottom-nav)
|
|
299
|
+
> .pui-sidebar__item
|
|
300
|
+
> .pui-sidebar__link
|
|
301
|
+
> .pui-sidebar__link-label,
|
|
302
|
+
.pui-sidebar[data-state='collapsed']
|
|
303
|
+
:is(.pui-sidebar__top-nav, .pui-sidebar__bottom-nav)
|
|
304
|
+
> .pui-sidebar__group
|
|
305
|
+
> .pui-sidebar__group-toggle
|
|
306
|
+
> .pui-sidebar__link-label {
|
|
307
|
+
flex: none;
|
|
308
|
+
max-width: 100%;
|
|
309
|
+
font-size: var(--step--3);
|
|
310
|
+
font-weight: var(--fw-regular);
|
|
311
|
+
line-height: var(--lh-tight);
|
|
312
|
+
/* Wrap rather than ellipsise — an ellipsis hides what the caption is
|
|
313
|
+
for. `break-word`, not `anywhere`: `anywhere` shrinks min-content
|
|
314
|
+
width, so words break even when they would have fit. */
|
|
315
|
+
white-space: normal;
|
|
316
|
+
overflow-wrap: break-word;
|
|
317
|
+
display: -webkit-box;
|
|
318
|
+
-webkit-box-orient: vertical;
|
|
319
|
+
-webkit-line-clamp: 2;
|
|
320
|
+
line-clamp: 2;
|
|
230
321
|
}
|
|
231
322
|
|
|
232
|
-
/*
|
|
233
|
-
*
|
|
234
|
-
|
|
235
|
-
|
|
323
|
+
/* Scoped to the rail entry's own badge; a descendant combinator would
|
|
324
|
+
* strip the flyout's too. */
|
|
325
|
+
.pui-sidebar[data-state='collapsed']
|
|
326
|
+
:is(.pui-sidebar__top-nav, .pui-sidebar__bottom-nav)
|
|
327
|
+
> .pui-sidebar__item
|
|
328
|
+
> .pui-sidebar__link
|
|
329
|
+
> .pui-sidebar__badge,
|
|
330
|
+
.pui-sidebar[data-state='collapsed']
|
|
331
|
+
:is(.pui-sidebar__top-nav, .pui-sidebar__bottom-nav)
|
|
332
|
+
> .pui-sidebar__group
|
|
333
|
+
> .pui-sidebar__group-toggle
|
|
334
|
+
> .pui-sidebar__group-caret,
|
|
335
|
+
.pui-sidebar[data-state='collapsed']
|
|
336
|
+
:is(.pui-sidebar__top-nav, .pui-sidebar__bottom-nav)
|
|
337
|
+
> .pui-sidebar__group
|
|
338
|
+
> .pui-sidebar__group-caret-toggle,
|
|
339
|
+
.pui-sidebar[data-state='collapsed']
|
|
340
|
+
:is(.pui-sidebar__top-nav, .pui-sidebar__bottom-nav)
|
|
341
|
+
> .pui-sidebar__section {
|
|
236
342
|
display: none;
|
|
237
343
|
}
|
|
238
344
|
|
|
239
|
-
/*
|
|
240
|
-
*
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
345
|
+
/* Marks which icon the open flyout came from — hover can't, once the
|
|
346
|
+
* flyout was opened by keyboard. Before the selected rule so that wins. */
|
|
347
|
+
.pui-sidebar[data-state='collapsed']
|
|
348
|
+
:is(.pui-sidebar__top-nav, .pui-sidebar__bottom-nav)
|
|
349
|
+
> .pui-sidebar__group
|
|
350
|
+
> .pui-sidebar__group-toggle[aria-expanded='true'] {
|
|
351
|
+
background: var(--bg-clr-accent);
|
|
352
|
+
color: var(--text-clr-brand);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/* The rail hides its children, so the group is the only indicator of
|
|
356
|
+
* location and a tint alone loses to the hover fill. Reverted in drawer
|
|
357
|
+
* mode, where it would merge with its own current child. */
|
|
358
|
+
.pui-sidebar[data-state='collapsed']
|
|
359
|
+
:is(.pui-sidebar__top-nav, .pui-sidebar__bottom-nav)
|
|
360
|
+
> .pui-sidebar__group[data-active='true']
|
|
361
|
+
> .pui-sidebar__group-toggle {
|
|
362
|
+
background: var(--bg-clr-brand-soft);
|
|
363
|
+
color: var(--text-clr-brand-selected);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/* Default policy; `collapsedSubmenu="panel"` overrides it below. */
|
|
367
|
+
.pui-sidebar[data-state='collapsed']
|
|
368
|
+
:is(.pui-sidebar__top-nav, .pui-sidebar__bottom-nav)
|
|
369
|
+
> .pui-sidebar__group
|
|
370
|
+
> .pui-sidebar__group-items {
|
|
371
|
+
display: none;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/* Collapsed submenu — the flyout. `data-submenu-state`'s presence is the
|
|
375
|
+
* switch, its value the state. Fixed, with coordinates set by the tier, so
|
|
376
|
+
* it escapes the nav's `overflow-y: auto` without portalling. */
|
|
377
|
+
.pui-sidebar[data-state='collapsed']
|
|
378
|
+
.pui-sidebar__group[data-submenu-state]
|
|
379
|
+
> .pui-sidebar__group-items {
|
|
380
|
+
display: flex;
|
|
381
|
+
position: fixed;
|
|
382
|
+
/* Under the rail: the panel is in-flow inside the isolated root, and
|
|
383
|
+
in-flow content paints above negative rungs. Giving the panel a rung
|
|
384
|
+
would make it a context and put its own flyout back on top. */
|
|
385
|
+
z-index: var(--layer-below);
|
|
386
|
+
width: var(--pui-sidebar-width);
|
|
387
|
+
padding: var(--space-xs) var(--space-2xs);
|
|
388
|
+
overflow-y: auto;
|
|
389
|
+
background: var(--bg-clr-surface);
|
|
390
|
+
border-inline-end: var(--stroke-sm) solid var(--border-clr-base);
|
|
391
|
+
box-shadow: var(--shadow-lg);
|
|
392
|
+
/* No transition here: a transition belongs to the state moved *to*,
|
|
393
|
+
so declaring it only on `open` makes hiding instant. */
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
.pui-sidebar[data-state='collapsed']
|
|
397
|
+
.pui-sidebar__group[data-submenu-state='closed']
|
|
398
|
+
> .pui-sidebar__group-items {
|
|
399
|
+
/* `visibility`, not `hidden`: keeps a box to transition while staying
|
|
400
|
+
out of the tab order and the a11y tree. */
|
|
401
|
+
visibility: hidden;
|
|
402
|
+
opacity: 0;
|
|
403
|
+
/* Where the slide starts. */
|
|
404
|
+
transform: translateX(-100%);
|
|
405
|
+
/* Explicit so it survives a transition being added above. */
|
|
406
|
+
transition: none;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
.pui-sidebar[data-state='collapsed']
|
|
410
|
+
.pui-sidebar__group[data-submenu-state='open']
|
|
411
|
+
> .pui-sidebar__group-items {
|
|
412
|
+
visibility: visible;
|
|
413
|
+
opacity: 1;
|
|
414
|
+
transform: none;
|
|
415
|
+
/* `visibility` is deliberately absent: untransitioned it flips at
|
|
416
|
+
once, so Tab reaches the entries instead of walking past them. */
|
|
417
|
+
transition:
|
|
418
|
+
opacity var(--duration-fast) var(--ease-out),
|
|
419
|
+
transform var(--duration-base) var(--ease-out);
|
|
420
|
+
transition-delay: var(--duration-fast);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/* Full-width rows again, so the expanded tree's indent is dropped. */
|
|
424
|
+
.pui-sidebar__group[data-submenu-state] > .pui-sidebar__group-items {
|
|
425
|
+
padding-inline-start: var(--space-2xs);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/* It travels, so it needs its own reduced-motion escape. */
|
|
429
|
+
@media (prefers-reduced-motion: reduce) {
|
|
430
|
+
.pui-sidebar[data-state='collapsed']
|
|
431
|
+
.pui-sidebar__group[data-submenu-state]
|
|
432
|
+
> .pui-sidebar__group-items {
|
|
433
|
+
transition: none;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
.pui-sidebar[data-state='collapsed']
|
|
437
|
+
.pui-sidebar__group[data-submenu-state='closed']
|
|
438
|
+
> .pui-sidebar__group-items {
|
|
439
|
+
transform: none;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/* Mobile drawer mode — below the breakpoint, the column transforms into a fixed-overlay drawer */
|
|
246
444
|
@media (max-width: 767px) {
|
|
247
|
-
.pui-sidebar
|
|
445
|
+
.pui-sidebar,
|
|
446
|
+
/* Repeats `[data-state]`: a media query grants no extra specificity,
|
|
447
|
+
so the plain selector alone loses to the collapsed rule above. */
|
|
448
|
+
.pui-sidebar[data-state='collapsed'] {
|
|
248
449
|
width: 0;
|
|
249
450
|
border-inline-end: none;
|
|
250
451
|
transition: none;
|
|
@@ -258,13 +459,20 @@
|
|
|
258
459
|
box-shadow: var(--shadow-spark-lg);
|
|
259
460
|
transform: translateX(-100%);
|
|
260
461
|
transition: transform 220ms ease;
|
|
261
|
-
z-index:
|
|
462
|
+
z-index: var(--layer-5);
|
|
262
463
|
}
|
|
263
464
|
|
|
264
465
|
.pui-sidebar[data-drawer-state='open'] .pui-sidebar__panel {
|
|
265
466
|
transform: translateX(0);
|
|
266
467
|
}
|
|
267
468
|
|
|
469
|
+
/* Lift the whole root: the panel's own rung only ranks it against
|
|
470
|
+
siblings, and a consumer's sticky sidebar makes the root a context
|
|
471
|
+
that an app header can paint over. Inert if the root is static. */
|
|
472
|
+
.pui-sidebar[data-drawer-state='open'] {
|
|
473
|
+
z-index: var(--layer-5);
|
|
474
|
+
}
|
|
475
|
+
|
|
268
476
|
.pui-sidebar__backdrop {
|
|
269
477
|
display: block;
|
|
270
478
|
position: fixed;
|
|
@@ -273,7 +481,7 @@
|
|
|
273
481
|
opacity: 0;
|
|
274
482
|
pointer-events: none;
|
|
275
483
|
transition: opacity 220ms ease;
|
|
276
|
-
z-index:
|
|
484
|
+
z-index: var(--layer-4);
|
|
277
485
|
}
|
|
278
486
|
|
|
279
487
|
.pui-sidebar[data-drawer-state='open'] .pui-sidebar__backdrop {
|
|
@@ -281,23 +489,83 @@
|
|
|
281
489
|
pointer-events: auto;
|
|
282
490
|
}
|
|
283
491
|
|
|
284
|
-
/*
|
|
285
|
-
* the
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
.pui-sidebar[data-state='collapsed'] .pui-
|
|
289
|
-
|
|
492
|
+
/* Collapsed is irrelevant here. Each revert mirrors the scoping of
|
|
493
|
+
* the rule it undoes — a media query grants no extra specificity. The
|
|
494
|
+
* flyout needs none: `data-submenu-state` is absent below the
|
|
495
|
+
* breakpoint. */
|
|
496
|
+
.pui-sidebar[data-state='collapsed'] .pui-sidebar__panel {
|
|
497
|
+
padding-inline: var(--space-s);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
.pui-sidebar[data-state='collapsed']
|
|
501
|
+
:is(.pui-sidebar__top-nav, .pui-sidebar__bottom-nav)
|
|
502
|
+
> .pui-sidebar__item
|
|
503
|
+
> .pui-sidebar__link
|
|
504
|
+
> .pui-sidebar__badge,
|
|
505
|
+
.pui-sidebar[data-state='collapsed']
|
|
506
|
+
:is(.pui-sidebar__top-nav, .pui-sidebar__bottom-nav)
|
|
507
|
+
> .pui-sidebar__group
|
|
508
|
+
> .pui-sidebar__group-toggle
|
|
509
|
+
> .pui-sidebar__group-caret,
|
|
510
|
+
.pui-sidebar[data-state='collapsed']
|
|
511
|
+
:is(.pui-sidebar__top-nav, .pui-sidebar__bottom-nav)
|
|
512
|
+
> .pui-sidebar__section {
|
|
290
513
|
display: revert;
|
|
291
514
|
}
|
|
292
515
|
|
|
293
|
-
.
|
|
294
|
-
.pui-sidebar[data-state='collapsed']
|
|
516
|
+
/* The caret toggle is the disclosure here, so it comes back. */
|
|
517
|
+
.pui-sidebar[data-state='collapsed']
|
|
518
|
+
:is(.pui-sidebar__top-nav, .pui-sidebar__bottom-nav)
|
|
519
|
+
> .pui-sidebar__group
|
|
520
|
+
> .pui-sidebar__group-caret-toggle {
|
|
521
|
+
display: flex;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
.pui-sidebar[data-state='collapsed']
|
|
525
|
+
:is(.pui-sidebar__top-nav, .pui-sidebar__bottom-nav)
|
|
526
|
+
> .pui-sidebar__item
|
|
527
|
+
> .pui-sidebar__link,
|
|
528
|
+
.pui-sidebar[data-state='collapsed']
|
|
529
|
+
:is(.pui-sidebar__top-nav, .pui-sidebar__bottom-nav)
|
|
530
|
+
> .pui-sidebar__group
|
|
531
|
+
> .pui-sidebar__group-toggle {
|
|
532
|
+
flex-direction: row;
|
|
295
533
|
justify-content: flex-start;
|
|
534
|
+
gap: var(--space-2xs);
|
|
296
535
|
padding-inline: var(--space-xs);
|
|
536
|
+
text-align: start;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
.pui-sidebar[data-state='collapsed']
|
|
540
|
+
:is(.pui-sidebar__top-nav, .pui-sidebar__bottom-nav)
|
|
541
|
+
> .pui-sidebar__item
|
|
542
|
+
> .pui-sidebar__link
|
|
543
|
+
> .pui-sidebar__link-label,
|
|
544
|
+
.pui-sidebar[data-state='collapsed']
|
|
545
|
+
:is(.pui-sidebar__top-nav, .pui-sidebar__bottom-nav)
|
|
546
|
+
> .pui-sidebar__group
|
|
547
|
+
> .pui-sidebar__group-toggle
|
|
548
|
+
> .pui-sidebar__link-label {
|
|
549
|
+
flex: 1;
|
|
550
|
+
font-size: var(--step--1);
|
|
551
|
+
font-weight: var(--fw-semibold);
|
|
552
|
+
line-height: var(--lh-base);
|
|
297
553
|
}
|
|
298
554
|
|
|
299
|
-
.pui-sidebar[data-state='collapsed']
|
|
555
|
+
.pui-sidebar[data-state='collapsed']
|
|
556
|
+
:is(.pui-sidebar__top-nav, .pui-sidebar__bottom-nav)
|
|
557
|
+
> .pui-sidebar__group
|
|
558
|
+
> .pui-sidebar__group-items {
|
|
300
559
|
display: flex;
|
|
301
560
|
}
|
|
561
|
+
|
|
562
|
+
/* The children are visible here, so the same fill on both ran them
|
|
563
|
+
together into one block. */
|
|
564
|
+
.pui-sidebar[data-state='collapsed']
|
|
565
|
+
:is(.pui-sidebar__top-nav, .pui-sidebar__bottom-nav)
|
|
566
|
+
> .pui-sidebar__group[data-active='true']
|
|
567
|
+
> .pui-sidebar__group-toggle {
|
|
568
|
+
background: transparent;
|
|
569
|
+
}
|
|
302
570
|
}
|
|
303
571
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Sidebar driver-data discriminated union.
|
|
3
3
|
*
|
|
4
|
-
* The Sidebar
|
|
4
|
+
* The Sidebar takes `:top-items` (and optionally `:bottom-items`)
|
|
5
|
+
* of `SidebarItem[]`. Each
|
|
5
6
|
* entry is one of three shapes, distinguished by the `type` field:
|
|
6
7
|
*
|
|
7
8
|
* - `link` — a navigable menu entry (renders as `<a>`)
|
|
@@ -42,6 +43,17 @@ export interface SidebarGroup {
|
|
|
42
43
|
label: string;
|
|
43
44
|
/** Optional leading icon on the group toggle. */
|
|
44
45
|
icon?: string;
|
|
46
|
+
/**
|
|
47
|
+
* Makes the group a destination as well as a disclosure — the row
|
|
48
|
+
* becomes a link, matched against `activeHref` like any other.
|
|
49
|
+
*
|
|
50
|
+
* With an `href` the row navigates and the caret becomes a separate
|
|
51
|
+
* toggle button, because one control cannot both go somewhere and
|
|
52
|
+
* expand in place. On the collapsed rail there is no caret: the row
|
|
53
|
+
* still navigates, and the flyout opens on hover or focus as it always
|
|
54
|
+
* does. Without an `href` the whole row is the toggle, as before.
|
|
55
|
+
*/
|
|
56
|
+
href?: string;
|
|
45
57
|
/** Nested items — links, sections, or further groups. */
|
|
46
58
|
items: SidebarItem[];
|
|
47
59
|
/**
|
|
@@ -38,12 +38,9 @@
|
|
|
38
38
|
</template>
|
|
39
39
|
|
|
40
40
|
<script setup lang="ts">
|
|
41
|
-
import { ref
|
|
41
|
+
import { ref } from 'vue';
|
|
42
42
|
import { generateId } from '../../../utils/a11y/id.js';
|
|
43
|
-
import {
|
|
44
|
-
mountDialog,
|
|
45
|
-
type DialogControl,
|
|
46
|
-
} from '../../../utils/controllers/dialog.js';
|
|
43
|
+
import { useDialog } from '../../../composables/useDialog.js';
|
|
47
44
|
|
|
48
45
|
defineOptions({ inheritAttrs: false });
|
|
49
46
|
|
|
@@ -72,7 +69,6 @@ const emit = defineEmits<{
|
|
|
72
69
|
const rootRef = ref<HTMLDialogElement | null>(null);
|
|
73
70
|
const titleId = generateId('alertdialog-title');
|
|
74
71
|
const descriptionId = generateId('alertdialog-desc');
|
|
75
|
-
let control: DialogControl | null = null;
|
|
76
72
|
|
|
77
73
|
function onConfirm(): void {
|
|
78
74
|
emit('confirm');
|
|
@@ -84,31 +80,14 @@ function onCancel(): void {
|
|
|
84
80
|
emit('update:open', false);
|
|
85
81
|
}
|
|
86
82
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
// explicit confirm/cancel buttons wired in the template.
|
|
96
|
-
});
|
|
97
|
-
if (props.open) control.open();
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
watch(
|
|
101
|
-
() => props.open,
|
|
102
|
-
(isOpen) => {
|
|
103
|
-
if (!control) return;
|
|
104
|
-
if (isOpen) control.open();
|
|
105
|
-
else control.close();
|
|
106
|
-
},
|
|
107
|
-
);
|
|
108
|
-
|
|
109
|
-
onBeforeUnmount(() => {
|
|
110
|
-
control?.teardown();
|
|
111
|
-
control = null;
|
|
83
|
+
useDialog(rootRef, {
|
|
84
|
+
role: 'alertdialog',
|
|
85
|
+
persistent: true,
|
|
86
|
+
initialFocus: 'cancel',
|
|
87
|
+
open: () => props.open,
|
|
88
|
+
// No onClose — persistent dialogs suppress Escape and backdrop-click
|
|
89
|
+
// dismissal at the composable layer. Close happens only through the
|
|
90
|
+
// explicit confirm/cancel buttons wired in the template.
|
|
112
91
|
});
|
|
113
92
|
</script>
|
|
114
93
|
|