@pienter/ui 0.2.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 +64 -0
- package/CONVENTIONS.md +1499 -0
- package/LICENSE +21 -0
- package/README.md +71 -0
- package/components/action/button/Button.astro +95 -0
- package/components/action/button/Button.vue +92 -0
- package/components/action/button/IconButton.astro +86 -0
- package/components/action/button/IconButton.vue +85 -0
- package/components/action/button/button.css +194 -0
- package/components/action/toggle/Toggle.vue +38 -0
- package/components/action/toggle/toggle.css +77 -0
- package/components/action/toggle-group/ToggleGroup.vue +67 -0
- package/components/action/toggle-group/toggle-group.css +78 -0
- package/components/display/avatar/Avatar.astro +17 -0
- package/components/display/avatar/Avatar.vue +30 -0
- package/components/display/avatar/AvatarStack.astro +9 -0
- package/components/display/avatar/AvatarStack.vue +11 -0
- package/components/display/avatar/avatar.css +58 -0
- package/components/display/badge/Badge.astro +15 -0
- package/components/display/badge/Badge.vue +23 -0
- package/components/display/badge/badge.css +56 -0
- package/components/display/empty/Empty.astro +9 -0
- package/components/display/empty/Empty.vue +11 -0
- package/components/display/empty/empty.css +39 -0
- package/components/display/icon/Icon.astro +52 -0
- package/components/display/icon/Icon.vue +57 -0
- package/components/display/icon/icon.css +47 -0
- package/components/feedback/alert/Alert.astro +52 -0
- package/components/feedback/alert/Alert.vue +60 -0
- package/components/feedback/alert/alert.css +78 -0
- package/components/feedback/progress/Progress.astro +68 -0
- package/components/feedback/progress/Progress.vue +82 -0
- package/components/feedback/progress/progress.css +68 -0
- package/components/feedback/skeleton/Skeleton.astro +32 -0
- package/components/feedback/skeleton/Skeleton.vue +39 -0
- package/components/feedback/skeleton/skeleton.css +56 -0
- package/components/feedback/spinner/Spinner.astro +25 -0
- package/components/feedback/spinner/Spinner.vue +36 -0
- package/components/feedback/spinner/spinner.css +91 -0
- package/components/feedback/toast/Toast.astro +50 -0
- package/components/feedback/toast/Toast.vue +74 -0
- package/components/feedback/toast/toast.css +128 -0
- package/components/form/checkbox/Checkbox.astro +79 -0
- package/components/form/checkbox/Checkbox.vue +95 -0
- package/components/form/checkbox/checkbox.css +59 -0
- package/components/form/combobox/Combobox.vue +508 -0
- package/components/form/combobox/combobox.css +110 -0
- package/components/form/date-input/DateInput.astro +105 -0
- package/components/form/date-input/DateInput.vue +121 -0
- package/components/form/date-input/date-input.css +19 -0
- package/components/form/form/Form.astro +106 -0
- package/components/form/form/Form.vue +181 -0
- package/components/form/form/form.css +46 -0
- package/components/form/input-otp/InputOTP.astro +147 -0
- package/components/form/input-otp/InputOTP.vue +209 -0
- package/components/form/input-otp/input-otp.css +52 -0
- package/components/form/label/Label.astro +13 -0
- package/components/form/label/Label.vue +20 -0
- package/components/form/label/label.css +11 -0
- package/components/form/number-field/NumberField.astro +142 -0
- package/components/form/number-field/NumberField.vue +155 -0
- package/components/form/number-field/number-field.css +115 -0
- package/components/form/radio-group/RadioGroup.astro +105 -0
- package/components/form/radio-group/RadioGroup.vue +110 -0
- package/components/form/radio-group/radio-group.css +114 -0
- package/components/form/radio-group/types.ts +14 -0
- package/components/form/select/Segmented.vue +36 -0
- package/components/form/select/Select.astro +105 -0
- package/components/form/select/Select.vue +109 -0
- package/components/form/select/select.css +96 -0
- package/components/form/slider/Slider.astro +205 -0
- package/components/form/slider/Slider.vue +321 -0
- package/components/form/slider/slider.css +115 -0
- package/components/form/switch/Switch.astro +75 -0
- package/components/form/switch/Switch.vue +89 -0
- package/components/form/switch/switch.css +64 -0
- package/components/form/tags-input/TagsInput.astro +153 -0
- package/components/form/tags-input/TagsInput.vue +207 -0
- package/components/form/tags-input/tags-input.css +128 -0
- package/components/form/text-input/TextInput.astro +84 -0
- package/components/form/text-input/TextInput.vue +99 -0
- package/components/form/text-input/text-input.css +165 -0
- package/components/form/textarea/Textarea.astro +86 -0
- package/components/form/textarea/Textarea.vue +102 -0
- package/components/form/textarea/textarea.css +25 -0
- package/components/layout/accordion/Accordion.vue +59 -0
- package/components/layout/accordion/accordion.css +87 -0
- package/components/layout/card/Card.astro +13 -0
- package/components/layout/card/Card.vue +20 -0
- package/components/layout/card/card.css +55 -0
- package/components/layout/collapsible/Collapsible.vue +77 -0
- package/components/layout/collapsible/collapsible.css +76 -0
- package/components/layout/separator/Separator.astro +31 -0
- package/components/layout/separator/Separator.vue +33 -0
- package/components/layout/separator/separator.css +27 -0
- package/components/layout/table/DataTable.vue +127 -0
- package/components/layout/table/Table.astro +116 -0
- package/components/layout/table/Table.vue +146 -0
- package/components/layout/table/TableRow.vue +59 -0
- package/components/layout/table/table.css +201 -0
- package/components/layout/table/types.ts +35 -0
- package/components/layout/table/useTable.ts +7 -0
- package/components/navigation/breadcrumb/Breadcrumb.astro +36 -0
- package/components/navigation/breadcrumb/Breadcrumb.vue +36 -0
- package/components/navigation/breadcrumb/breadcrumb.css +37 -0
- package/components/navigation/navbar/Navbar.astro +62 -0
- package/components/navigation/navbar/Navbar.vue +50 -0
- package/components/navigation/navbar/navbar.css +77 -0
- package/components/navigation/pagination/Pagination.vue +107 -0
- package/components/navigation/pagination/pagination.css +53 -0
- package/components/navigation/sidebar/Sidebar.astro +132 -0
- package/components/navigation/sidebar/Sidebar.vue +174 -0
- package/components/navigation/sidebar/SidebarItemRender.astro +83 -0
- package/components/navigation/sidebar/SidebarItemRender.vue +98 -0
- package/components/navigation/sidebar/sidebar.css +303 -0
- package/components/navigation/sidebar/types.ts +72 -0
- package/components/navigation/tabs/Tabs.vue +84 -0
- package/components/navigation/tabs/tabs.css +39 -0
- package/components/overlay/alert-dialog/AlertDialog.astro +112 -0
- package/components/overlay/alert-dialog/AlertDialog.vue +117 -0
- package/components/overlay/alert-dialog/alert-dialog.css +57 -0
- package/components/overlay/command/Command.vue +356 -0
- package/components/overlay/command/command.css +179 -0
- package/components/overlay/dropdown-menu/DropdownMenu.vue +143 -0
- package/components/overlay/dropdown-menu/dropdown-menu.css +120 -0
- package/components/overlay/modal/Modal.astro +66 -0
- package/components/overlay/modal/Modal.vue +85 -0
- package/components/overlay/modal/modal.css +60 -0
- package/components/overlay/popover/Popover.vue +113 -0
- package/components/overlay/popover/popover.css +53 -0
- package/components/overlay/sheet/Sheet.vue +88 -0
- package/components/overlay/sheet/sheet.css +108 -0
- package/components/overlay/tooltip/Tooltip.vue +210 -0
- package/components/overlay/tooltip/tooltip.css +50 -0
- package/composables/useUrlSort.ts +48 -0
- package/icons/alert-triangle.ts +1 -0
- package/icons/arrow-down.ts +1 -0
- package/icons/arrow-up-down.ts +5 -0
- package/icons/arrow-up.ts +1 -0
- package/icons/bell.ts +1 -0
- package/icons/check.ts +1 -0
- package/icons/chevron-down.ts +1 -0
- package/icons/chevron-left.ts +1 -0
- package/icons/chevron-right.ts +1 -0
- package/icons/chevron-up-down.ts +5 -0
- package/icons/chevron-up.ts +1 -0
- package/icons/circle-alert.ts +1 -0
- package/icons/circle-check.ts +1 -0
- package/icons/clipboard.ts +1 -0
- package/icons/download.ts +1 -0
- package/icons/edit.ts +1 -0
- package/icons/external-link.ts +1 -0
- package/icons/eye.ts +1 -0
- package/icons/file.ts +1 -0
- package/icons/filter.ts +1 -0
- package/icons/folder.ts +1 -0
- package/icons/image.ts +1 -0
- package/icons/inbox.ts +1 -0
- package/icons/index.ts +91 -0
- package/icons/info.ts +1 -0
- package/icons/layers.ts +1 -0
- package/icons/link-2.ts +1 -0
- package/icons/list.ts +1 -0
- package/icons/loader.ts +3 -0
- package/icons/menu.ts +1 -0
- package/icons/more-horizontal.ts +1 -0
- package/icons/more-vertical.ts +1 -0
- package/icons/plus-circle.ts +1 -0
- package/icons/plus.ts +1 -0
- package/icons/save.ts +1 -0
- package/icons/search.ts +1 -0
- package/icons/send.ts +1 -0
- package/icons/settings.ts +1 -0
- package/icons/tool.ts +1 -0
- package/icons/trash-2.ts +1 -0
- package/icons/trash.ts +1 -0
- package/icons/upload-cloud.ts +1 -0
- package/icons/upload.ts +1 -0
- package/icons/x.ts +1 -0
- package/package.json +150 -0
- package/styles/0-settings/colors.css +241 -0
- package/styles/0-settings/index.css +5 -0
- package/styles/0-settings/layout.css +52 -0
- package/styles/0-settings/motion.css +11 -0
- package/styles/0-settings/spacing.css +15 -0
- package/styles/0-settings/typography.css +37 -0
- package/styles/0-utils/index.css +1 -0
- package/styles/1-reset/index.css +1 -0
- package/styles/1-reset/reset.css +26 -0
- package/styles/2-base/base.css +42 -0
- package/styles/2-base/forms.css +23 -0
- package/styles/2-base/index.css +2 -0
- package/styles/3-layout/container.css +57 -0
- package/styles/3-layout/index.css +2 -0
- package/styles/3-layout/section.css +17 -0
- package/styles/5-utilities/accessibility.css +13 -0
- package/styles/5-utilities/index.css +2 -0
- package/styles/5-utilities/text.css +5 -0
- package/styles/main.css +8 -0
- package/styles/styles.d.ts +6 -0
- package/utils/a11y/focus.ts +68 -0
- package/utils/a11y/id.ts +10 -0
- package/utils/a11y/index.ts +9 -0
- package/utils/a11y/keyboard.ts +32 -0
- package/utils/a11y/live-region.ts +36 -0
- package/utils/controllers/dialog.ts +205 -0
- package/utils/controllers/disclosure.ts +117 -0
- package/utils/controllers/form.ts +524 -0
- package/utils/controllers/index.ts +39 -0
- package/utils/controllers/menu.ts +255 -0
- package/utils/controllers/number-field.ts +103 -0
- package/utils/controllers/otp.ts +252 -0
- package/utils/controllers/popover.ts +434 -0
- package/utils/controllers/sidebar.ts +610 -0
- package/utils/controllers/slider.ts +336 -0
- package/utils/controllers/tags-input.ts +255 -0
- package/utils/controllers/toast.ts +426 -0
- package/utils/dom/index.ts +1 -0
- package/utils/dom/scroll-lock.ts +48 -0
- package/utils/index.ts +3 -0
- package/utils/sort/index.ts +3 -0
- package/utils/sort/serialize.ts +19 -0
- package/utils/sort/state.ts +11 -0
- package/utils/sort/types.ts +15 -0
- package/utils/validation/form.ts +93 -0
- package/utils/validation/index.ts +13 -0
- package/utils/validation/rules.ts +31 -0
|
@@ -0,0 +1,303 @@
|
|
|
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
|
+
* ============================================================ */
|
|
8
|
+
.pui-sidebar {
|
|
9
|
+
--pui-sidebar-width: var(--space-sidebar-width);
|
|
10
|
+
--pui-sidebar-width-collapsed: var(--space-sidebar-width-collapsed);
|
|
11
|
+
|
|
12
|
+
display: block;
|
|
13
|
+
width: var(--pui-sidebar-width);
|
|
14
|
+
flex-shrink: 0;
|
|
15
|
+
background: var(--bg-clr-surface);
|
|
16
|
+
border-inline-end: var(--stroke-sm) solid var(--border-clr-base);
|
|
17
|
+
transition: width 180ms ease;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.pui-sidebar[data-state='collapsed'] {
|
|
21
|
+
width: var(--pui-sidebar-width-collapsed);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/* ============================================================
|
|
25
|
+
* Panel — the visible column / drawer body containing brand, menu,
|
|
26
|
+
* footer.
|
|
27
|
+
* ============================================================ */
|
|
28
|
+
.pui-sidebar__panel {
|
|
29
|
+
display: flex;
|
|
30
|
+
flex-direction: column;
|
|
31
|
+
height: 100%;
|
|
32
|
+
min-height: 100%;
|
|
33
|
+
padding: var(--space-s);
|
|
34
|
+
gap: var(--space-s);
|
|
35
|
+
background: var(--bg-clr-surface);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/* ============================================================
|
|
39
|
+
* Backdrop — only meaningful in mobile drawer mode. Hidden on desktop.
|
|
40
|
+
* ============================================================ */
|
|
41
|
+
.pui-sidebar__backdrop {
|
|
42
|
+
display: none;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/* ============================================================
|
|
46
|
+
* Brand region — top of the panel. Layout owned by consumer markup
|
|
47
|
+
* inside the slot; we just provide consistent spacing.
|
|
48
|
+
* ============================================================ */
|
|
49
|
+
.pui-sidebar__brand {
|
|
50
|
+
display: flex;
|
|
51
|
+
align-items: center;
|
|
52
|
+
gap: var(--space-2xs);
|
|
53
|
+
padding-block: var(--space-2xs);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/* Hide brand inner text when collapsed; consumer markup with
|
|
57
|
+
* `data-keep-when-collapsed` survives. */
|
|
58
|
+
.pui-sidebar[data-state='collapsed']
|
|
59
|
+
.pui-sidebar__brand
|
|
60
|
+
> :not([data-keep-when-collapsed]) {
|
|
61
|
+
/* leave layout untouched — many consumers want a wordmark to fade
|
|
62
|
+
* out while the mark stays — this is the default rule. Consumers
|
|
63
|
+
* mark the persistent element. */
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/* ============================================================
|
|
67
|
+
* Menu list — the data-driven menu region.
|
|
68
|
+
* ============================================================ */
|
|
69
|
+
.pui-sidebar__menu {
|
|
70
|
+
list-style: none;
|
|
71
|
+
margin: 0;
|
|
72
|
+
padding: 0;
|
|
73
|
+
display: flex;
|
|
74
|
+
flex-direction: column;
|
|
75
|
+
gap: var(--space-3xs);
|
|
76
|
+
flex: 1;
|
|
77
|
+
min-height: 0;
|
|
78
|
+
overflow-y: auto;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.pui-sidebar__item {
|
|
82
|
+
list-style: none;
|
|
83
|
+
margin: 0;
|
|
84
|
+
padding: 0;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/* ============================================================
|
|
88
|
+
* Link / group-toggle — share visual treatment so the menu reads
|
|
89
|
+
* as one column of equally-weighted entries.
|
|
90
|
+
* ============================================================ */
|
|
91
|
+
.pui-sidebar__link,
|
|
92
|
+
.pui-sidebar__group-toggle {
|
|
93
|
+
display: flex;
|
|
94
|
+
align-items: center;
|
|
95
|
+
gap: var(--space-2xs);
|
|
96
|
+
width: 100%;
|
|
97
|
+
padding: var(--space-2xs) var(--space-xs);
|
|
98
|
+
border-radius: var(--radius-sm);
|
|
99
|
+
background: transparent;
|
|
100
|
+
border: 0;
|
|
101
|
+
color: var(--text-clr-base);
|
|
102
|
+
text-decoration: none;
|
|
103
|
+
font: inherit;
|
|
104
|
+
font-size: var(--step--1);
|
|
105
|
+
font-weight: var(--fw-semibold);
|
|
106
|
+
text-align: start;
|
|
107
|
+
cursor: pointer;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.pui-sidebar__link:hover,
|
|
111
|
+
.pui-sidebar__group-toggle:hover {
|
|
112
|
+
background: var(--bg-clr-accent);
|
|
113
|
+
color: var(--text-clr-brand);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.pui-sidebar__link[aria-current='page'] {
|
|
117
|
+
background: var(--bg-clr-accent);
|
|
118
|
+
color: var(--text-clr-brand-selected);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.pui-sidebar__link[aria-disabled='true'] {
|
|
122
|
+
color: var(--text-clr-muted);
|
|
123
|
+
pointer-events: none;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.pui-sidebar__link:focus-visible,
|
|
127
|
+
.pui-sidebar__group-toggle:focus-visible {
|
|
128
|
+
outline: 3px solid var(--outline-clr-base);
|
|
129
|
+
outline-offset: 2px;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.pui-sidebar__link-label {
|
|
133
|
+
flex: 1;
|
|
134
|
+
min-width: 0;
|
|
135
|
+
overflow: hidden;
|
|
136
|
+
text-overflow: ellipsis;
|
|
137
|
+
white-space: nowrap;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.pui-sidebar__badge {
|
|
141
|
+
margin-inline-start: auto;
|
|
142
|
+
padding-inline: var(--space-3xs);
|
|
143
|
+
border-radius: var(--radius-pill);
|
|
144
|
+
background: var(--bg-clr-brand);
|
|
145
|
+
color: var(--bg-clr-brand-ink);
|
|
146
|
+
font-size: var(--step--2);
|
|
147
|
+
font-weight: var(--fw-bold);
|
|
148
|
+
line-height: 1.4;
|
|
149
|
+
min-width: 1.25rem;
|
|
150
|
+
text-align: center;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/* ============================================================
|
|
154
|
+
* Group — collapsible container.
|
|
155
|
+
* ============================================================ */
|
|
156
|
+
.pui-sidebar__group {
|
|
157
|
+
list-style: none;
|
|
158
|
+
margin: 0;
|
|
159
|
+
padding: 0;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.pui-sidebar__group-caret {
|
|
163
|
+
margin-inline-start: auto;
|
|
164
|
+
transition: transform 150ms ease;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.pui-sidebar__group-toggle[aria-expanded='true'] .pui-sidebar__group-caret {
|
|
168
|
+
transform: rotate(180deg);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.pui-sidebar__group[data-active='true'] > .pui-sidebar__group-toggle {
|
|
172
|
+
color: var(--text-clr-brand-selected);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
.pui-sidebar__group-items {
|
|
176
|
+
list-style: none;
|
|
177
|
+
margin: 0;
|
|
178
|
+
padding: 0;
|
|
179
|
+
padding-inline-start: var(--space-m);
|
|
180
|
+
display: flex;
|
|
181
|
+
flex-direction: column;
|
|
182
|
+
gap: var(--space-3xs);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.pui-sidebar__group-items[hidden] {
|
|
186
|
+
display: none;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/* ============================================================
|
|
190
|
+
* Section header — non-interactive label.
|
|
191
|
+
* ============================================================ */
|
|
192
|
+
.pui-sidebar__section {
|
|
193
|
+
list-style: none;
|
|
194
|
+
margin: 0;
|
|
195
|
+
padding: var(--space-s) var(--space-xs) var(--space-3xs);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.pui-sidebar__section-label {
|
|
199
|
+
font-family: var(--ff-mono);
|
|
200
|
+
font-size: var(--step--2);
|
|
201
|
+
text-transform: uppercase;
|
|
202
|
+
letter-spacing: var(--ls-eyebrow);
|
|
203
|
+
color: var(--text-clr-muted);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/* ============================================================
|
|
207
|
+
* Footer region — bottom of the panel.
|
|
208
|
+
* ============================================================ */
|
|
209
|
+
.pui-sidebar__footer {
|
|
210
|
+
margin-block-start: auto;
|
|
211
|
+
padding-block-start: var(--space-2xs);
|
|
212
|
+
border-block-start: var(--stroke-sm) solid var(--border-clr-base);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/* ============================================================
|
|
216
|
+
* Collapsed-state visual rules — hide labels and badges, keep icons.
|
|
217
|
+
* Section headers also hidden because they're label-only.
|
|
218
|
+
* ============================================================ */
|
|
219
|
+
.pui-sidebar[data-state='collapsed'] .pui-sidebar__link-label,
|
|
220
|
+
.pui-sidebar[data-state='collapsed'] .pui-sidebar__badge,
|
|
221
|
+
.pui-sidebar[data-state='collapsed'] .pui-sidebar__group-caret,
|
|
222
|
+
.pui-sidebar[data-state='collapsed'] .pui-sidebar__section {
|
|
223
|
+
display: none;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
.pui-sidebar[data-state='collapsed'] .pui-sidebar__link,
|
|
227
|
+
.pui-sidebar[data-state='collapsed'] .pui-sidebar__group-toggle {
|
|
228
|
+
justify-content: center;
|
|
229
|
+
padding-inline: var(--space-3xs);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/* In collapsed mode we also hide nested group items (they'd
|
|
233
|
+
* overflow the narrow column). Hover-to-expand with tooltip is a
|
|
234
|
+
* Phase 4 deferral. */
|
|
235
|
+
.pui-sidebar[data-state='collapsed'] .pui-sidebar__group-items {
|
|
236
|
+
display: none;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/* ============================================================
|
|
240
|
+
* Mobile drawer mode — below the breakpoint, the column transforms
|
|
241
|
+
* into a fixed-overlay drawer. The breakpoint default lives in JS
|
|
242
|
+
* (768px), CSS mirrors with the same number. If a consumer changes
|
|
243
|
+
* `:drawer-breakpoint`, they're responsible for matching CSS via
|
|
244
|
+
* a custom stylesheet — out of scope for v1.
|
|
245
|
+
* ============================================================ */
|
|
246
|
+
@media (max-width: 767px) {
|
|
247
|
+
.pui-sidebar {
|
|
248
|
+
width: 0;
|
|
249
|
+
border-inline-end: none;
|
|
250
|
+
transition: none;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
.pui-sidebar__panel {
|
|
254
|
+
position: fixed;
|
|
255
|
+
inset-block: 0;
|
|
256
|
+
inset-inline-start: 0;
|
|
257
|
+
width: min(20rem, 80vw);
|
|
258
|
+
box-shadow: var(--shadow-spark-lg);
|
|
259
|
+
transform: translateX(-100%);
|
|
260
|
+
transition: transform 220ms ease;
|
|
261
|
+
z-index: 50;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
.pui-sidebar[data-drawer-state='open'] .pui-sidebar__panel {
|
|
265
|
+
transform: translateX(0);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
.pui-sidebar__backdrop {
|
|
269
|
+
display: block;
|
|
270
|
+
position: fixed;
|
|
271
|
+
inset: 0;
|
|
272
|
+
background: var(--bg-clr-backdrop);
|
|
273
|
+
opacity: 0;
|
|
274
|
+
pointer-events: none;
|
|
275
|
+
transition: opacity 220ms ease;
|
|
276
|
+
z-index: 40;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
.pui-sidebar[data-drawer-state='open'] .pui-sidebar__backdrop {
|
|
280
|
+
opacity: 1;
|
|
281
|
+
pointer-events: auto;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/* In drawer mode, the desktop "collapsed" state is irrelevant —
|
|
285
|
+
* the panel is full-width drawer when open and hidden otherwise. */
|
|
286
|
+
.pui-sidebar[data-state='collapsed'] .pui-sidebar__link-label,
|
|
287
|
+
.pui-sidebar[data-state='collapsed'] .pui-sidebar__badge,
|
|
288
|
+
.pui-sidebar[data-state='collapsed'] .pui-sidebar__group-caret,
|
|
289
|
+
.pui-sidebar[data-state='collapsed'] .pui-sidebar__section {
|
|
290
|
+
display: revert;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
.pui-sidebar[data-state='collapsed'] .pui-sidebar__link,
|
|
294
|
+
.pui-sidebar[data-state='collapsed'] .pui-sidebar__group-toggle {
|
|
295
|
+
justify-content: flex-start;
|
|
296
|
+
padding-inline: var(--space-xs);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
.pui-sidebar[data-state='collapsed'] .pui-sidebar__group-items {
|
|
300
|
+
display: flex;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sidebar driver-data discriminated union.
|
|
3
|
+
*
|
|
4
|
+
* The Sidebar component takes an `:items` prop of `SidebarItem[]`. Each
|
|
5
|
+
* entry is one of three shapes, distinguished by the `type` field:
|
|
6
|
+
*
|
|
7
|
+
* - `link` — a navigable menu entry (renders as `<a>`)
|
|
8
|
+
* - `group` — an expandable container with nested items
|
|
9
|
+
* - `section` — a non-interactive section header / divider with a label
|
|
10
|
+
*
|
|
11
|
+
* Active-state highlighting is derived from the Sidebar's `:active-href`
|
|
12
|
+
* prop using the hierarchical-match algorithm documented in
|
|
13
|
+
* `packages/ui/CONVENTIONS.md`. Consumers do NOT mark items active.
|
|
14
|
+
*
|
|
15
|
+
* See the Sidebar audit in `./AUDIT.md` for the
|
|
16
|
+
* full hybrid-API archetype rules.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** A navigable menu entry. */
|
|
20
|
+
export interface SidebarLink {
|
|
21
|
+
type: 'link';
|
|
22
|
+
/** Visible label. */
|
|
23
|
+
label: string;
|
|
24
|
+
/** Target href; matched against the Sidebar's `activeHref` prop. */
|
|
25
|
+
href: string;
|
|
26
|
+
/**
|
|
27
|
+
* Optional icon name from the @pienter/ui icon set. Rendered as a
|
|
28
|
+
* leading icon. v1 accepts a string (icon name); may grow to
|
|
29
|
+
* `string | Component` if rich icon shapes become a need.
|
|
30
|
+
*/
|
|
31
|
+
icon?: string;
|
|
32
|
+
/** Optional trailing badge (e.g. unread count or "New"). */
|
|
33
|
+
badge?: string | number;
|
|
34
|
+
/** Disabled items are non-interactive and skipped by keyboard nav. */
|
|
35
|
+
disabled?: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** An expandable container with nested items. */
|
|
39
|
+
export interface SidebarGroup {
|
|
40
|
+
type: 'group';
|
|
41
|
+
/** Visible label on the group toggle button. */
|
|
42
|
+
label: string;
|
|
43
|
+
/** Optional leading icon on the group toggle. */
|
|
44
|
+
icon?: string;
|
|
45
|
+
/** Nested items — links, sections, or further groups. */
|
|
46
|
+
items: SidebarItem[];
|
|
47
|
+
/**
|
|
48
|
+
* Whether the group starts expanded on first mount. Ignored after
|
|
49
|
+
* the consumer has explicit control of `expandedGroups`. Default
|
|
50
|
+
* `false`.
|
|
51
|
+
*/
|
|
52
|
+
defaultOpen?: boolean;
|
|
53
|
+
/**
|
|
54
|
+
* Optional explicit key used by the controller's `expandGroup` /
|
|
55
|
+
* `collapseGroup` methods and by the Sidebar's `expandedGroups`
|
|
56
|
+
* prop. When omitted, the key is derived from `label` via
|
|
57
|
+
* kebab-case slugification (e.g. "Member admin" → "member-admin").
|
|
58
|
+
* REQUIRED when multiple groups share a label, otherwise their
|
|
59
|
+
* derived keys would collide.
|
|
60
|
+
*/
|
|
61
|
+
key?: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** A non-interactive section header / divider with a label. */
|
|
65
|
+
export interface SidebarSection {
|
|
66
|
+
type: 'section';
|
|
67
|
+
/** Visible label rendered as a small caption. */
|
|
68
|
+
label: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Discriminated union over all sidebar item shapes. */
|
|
72
|
+
export type SidebarItem = SidebarLink | SidebarGroup | SidebarSection;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="pui-tabs" role="tablist">
|
|
3
|
+
<button
|
|
4
|
+
v-for="tab in tabs"
|
|
5
|
+
:key="tab.key"
|
|
6
|
+
type="button"
|
|
7
|
+
class="pui-tabs__item"
|
|
8
|
+
role="tab"
|
|
9
|
+
:aria-selected="modelValue === tab.key ? 'true' : 'false'"
|
|
10
|
+
:tabindex="modelValue === tab.key ? 0 : -1"
|
|
11
|
+
:disabled="tab.disabled || undefined"
|
|
12
|
+
@click="emit('update:modelValue', tab.key)"
|
|
13
|
+
@keydown="handleKeydown($event)"
|
|
14
|
+
>
|
|
15
|
+
{{ tab.label }}
|
|
16
|
+
</button>
|
|
17
|
+
</div>
|
|
18
|
+
</template>
|
|
19
|
+
|
|
20
|
+
<script setup lang="ts">
|
|
21
|
+
interface TabItem {
|
|
22
|
+
key: string;
|
|
23
|
+
label: string;
|
|
24
|
+
disabled?: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const props = withDefaults(
|
|
28
|
+
defineProps<{
|
|
29
|
+
modelValue?: string;
|
|
30
|
+
tabs: TabItem[];
|
|
31
|
+
}>(),
|
|
32
|
+
{
|
|
33
|
+
modelValue: undefined,
|
|
34
|
+
},
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
const emit = defineEmits<{
|
|
38
|
+
'update:modelValue': [value: string];
|
|
39
|
+
}>();
|
|
40
|
+
|
|
41
|
+
function handleKeydown(event: KeyboardEvent): void {
|
|
42
|
+
const target = event.currentTarget as HTMLElement;
|
|
43
|
+
const items = Array.from(
|
|
44
|
+
target.parentElement!.querySelectorAll<HTMLElement>(
|
|
45
|
+
'.pui-tabs__item:not([disabled])',
|
|
46
|
+
),
|
|
47
|
+
);
|
|
48
|
+
if (items.length === 0) return;
|
|
49
|
+
const currentIndex = items.indexOf(target);
|
|
50
|
+
let nextIndex: number;
|
|
51
|
+
|
|
52
|
+
switch (event.key) {
|
|
53
|
+
case 'ArrowRight':
|
|
54
|
+
nextIndex = (currentIndex + 1) % items.length;
|
|
55
|
+
break;
|
|
56
|
+
case 'ArrowLeft':
|
|
57
|
+
nextIndex = (currentIndex - 1 + items.length) % items.length;
|
|
58
|
+
break;
|
|
59
|
+
case 'Home':
|
|
60
|
+
nextIndex = 0;
|
|
61
|
+
break;
|
|
62
|
+
case 'End':
|
|
63
|
+
nextIndex = items.length - 1;
|
|
64
|
+
break;
|
|
65
|
+
default:
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
event.preventDefault();
|
|
70
|
+
// Disabled tabs are excluded from both `items` (`:not([disabled])`) and
|
|
71
|
+
// `enabledTabs`, so the indices line up.
|
|
72
|
+
const enabledTabs = props.tabs.filter((t) => !t.disabled);
|
|
73
|
+
const nextEl = items[nextIndex];
|
|
74
|
+
const nextTab = enabledTabs[nextIndex];
|
|
75
|
+
if (nextEl && nextTab) {
|
|
76
|
+
nextEl.focus();
|
|
77
|
+
emit('update:modelValue', nextTab.key);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
</script>
|
|
81
|
+
|
|
82
|
+
<style>
|
|
83
|
+
@import './tabs.css';
|
|
84
|
+
</style>
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
@layer components {
|
|
2
|
+
.pui-tabs {
|
|
3
|
+
display: flex;
|
|
4
|
+
gap: var(--space-s);
|
|
5
|
+
border-block-end: var(--stroke-sm) solid var(--border-clr-base);
|
|
6
|
+
}
|
|
7
|
+
.pui-tabs__item {
|
|
8
|
+
appearance: none;
|
|
9
|
+
background: transparent;
|
|
10
|
+
border: 0;
|
|
11
|
+
padding: var(--space-2xs) 0;
|
|
12
|
+
font: inherit;
|
|
13
|
+
font-size: var(--step--1);
|
|
14
|
+
font-weight: var(--fw-semibold);
|
|
15
|
+
color: var(--text-clr-muted);
|
|
16
|
+
cursor: pointer;
|
|
17
|
+
border-block-end: 2px solid transparent;
|
|
18
|
+
margin-block-end: -1px;
|
|
19
|
+
transition: color 0.12s ease;
|
|
20
|
+
}
|
|
21
|
+
.pui-tabs__item[aria-selected='true'] {
|
|
22
|
+
color: var(--text-clr-base);
|
|
23
|
+
border-block-end-color: var(--border-clr-brand);
|
|
24
|
+
}
|
|
25
|
+
.pui-tabs__item:hover {
|
|
26
|
+
color: var(--text-clr-base);
|
|
27
|
+
}
|
|
28
|
+
.pui-tabs__item:focus-visible {
|
|
29
|
+
outline: 3px solid var(--outline-clr-base);
|
|
30
|
+
outline-offset: 2px;
|
|
31
|
+
}
|
|
32
|
+
.pui-tabs__item:disabled {
|
|
33
|
+
opacity: 0.4;
|
|
34
|
+
cursor: not-allowed;
|
|
35
|
+
}
|
|
36
|
+
.pui-tabs__item:disabled:hover {
|
|
37
|
+
color: var(--text-clr-muted);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
---
|
|
2
|
+
import './alert-dialog.css';
|
|
3
|
+
import { generateId } from '../../../utils/a11y/id.js';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
id?: string;
|
|
7
|
+
open?: boolean;
|
|
8
|
+
title: string;
|
|
9
|
+
description: string;
|
|
10
|
+
confirmLabel?: string;
|
|
11
|
+
cancelLabel?: string;
|
|
12
|
+
destructive?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const {
|
|
16
|
+
id,
|
|
17
|
+
open = false,
|
|
18
|
+
title,
|
|
19
|
+
description,
|
|
20
|
+
confirmLabel = 'Confirm',
|
|
21
|
+
cancelLabel = 'Cancel',
|
|
22
|
+
destructive = false,
|
|
23
|
+
...rest
|
|
24
|
+
} = Astro.props;
|
|
25
|
+
|
|
26
|
+
const dialogId = id ?? generateId('alertdialog');
|
|
27
|
+
const titleId = `${dialogId}-title`;
|
|
28
|
+
const descriptionId = `${dialogId}-desc`;
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
<dialog
|
|
32
|
+
id={dialogId}
|
|
33
|
+
class="pui-alert-dialog"
|
|
34
|
+
data-state="closed"
|
|
35
|
+
data-pui-alert-dialog
|
|
36
|
+
data-open-initially={open ? 'true' : undefined}
|
|
37
|
+
aria-labelledby={titleId}
|
|
38
|
+
aria-describedby={descriptionId}
|
|
39
|
+
{...rest}
|
|
40
|
+
>
|
|
41
|
+
<div class="pui-alert-dialog__head">
|
|
42
|
+
<h2 id={titleId} class="pui-alert-dialog__title">{title}</h2>
|
|
43
|
+
<p id={descriptionId} class="pui-alert-dialog__description">
|
|
44
|
+
{description}
|
|
45
|
+
</p>
|
|
46
|
+
</div>
|
|
47
|
+
<div class="pui-alert-dialog__actions">
|
|
48
|
+
<button
|
|
49
|
+
type="button"
|
|
50
|
+
class="pui-btn"
|
|
51
|
+
data-size="md"
|
|
52
|
+
data-dialog-cancel
|
|
53
|
+
data-alert-dialog-action="cancel">{cancelLabel}</button
|
|
54
|
+
>
|
|
55
|
+
<button
|
|
56
|
+
type="button"
|
|
57
|
+
class="pui-btn"
|
|
58
|
+
data-variant={destructive ? 'danger' : 'primary'}
|
|
59
|
+
data-size="md"
|
|
60
|
+
data-alert-dialog-action="confirm">{confirmLabel}</button
|
|
61
|
+
>
|
|
62
|
+
</div>
|
|
63
|
+
</dialog>
|
|
64
|
+
|
|
65
|
+
<script>
|
|
66
|
+
import { mountDialog } from '@pienter/ui/utils/controllers/dialog.js';
|
|
67
|
+
|
|
68
|
+
// AlertDialog uses its own selector marker (`data-pui-alert-dialog`) so the
|
|
69
|
+
// mount block can hard-code the locked alertdialog config (role,
|
|
70
|
+
// persistent, initialFocus: 'cancel') without branching on data-attrs.
|
|
71
|
+
document
|
|
72
|
+
.querySelectorAll<HTMLDialogElement>('dialog[data-pui-alert-dialog]')
|
|
73
|
+
.forEach((el) => {
|
|
74
|
+
// Idempotent — guard against double-mounts (HMR, multiple <AlertDialog> imports).
|
|
75
|
+
if ((el as unknown as { _dialog?: unknown })._dialog) return;
|
|
76
|
+
const control = mountDialog(el, {
|
|
77
|
+
role: 'alertdialog',
|
|
78
|
+
persistent: true,
|
|
79
|
+
initialFocus: 'cancel',
|
|
80
|
+
});
|
|
81
|
+
(el as unknown as { _dialog: typeof control })._dialog = control;
|
|
82
|
+
if (el.dataset.openInitially === 'true') control.open();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// Event-delegated confirm/cancel button handlers. Dispatching custom
|
|
86
|
+
// `confirm` / `cancel` events on the dialog lets Astro consumers wire
|
|
87
|
+
// callbacks via `myDialog.addEventListener('confirm', ...)` without
|
|
88
|
+
// touching the controller. The controller's close() is invoked here so
|
|
89
|
+
// consumers don't have to repeat the boilerplate at every call-site.
|
|
90
|
+
document.addEventListener('click', (e) => {
|
|
91
|
+
const target = e.target as HTMLElement | null;
|
|
92
|
+
if (!target) return;
|
|
93
|
+
const action = target.closest<HTMLElement>('[data-alert-dialog-action]');
|
|
94
|
+
if (!action) return;
|
|
95
|
+
const dialog = action.closest<HTMLDialogElement>(
|
|
96
|
+
'dialog[data-pui-alert-dialog]',
|
|
97
|
+
);
|
|
98
|
+
if (!dialog) return;
|
|
99
|
+
const actionName = action.getAttribute('data-alert-dialog-action');
|
|
100
|
+
const detail = { dialogId: dialog.id };
|
|
101
|
+
if (actionName === 'confirm') {
|
|
102
|
+
dialog.dispatchEvent(
|
|
103
|
+
new CustomEvent('confirm', { bubbles: true, detail }),
|
|
104
|
+
);
|
|
105
|
+
} else if (actionName === 'cancel') {
|
|
106
|
+
dialog.dispatchEvent(
|
|
107
|
+
new CustomEvent('cancel', { bubbles: true, detail }),
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
(dialog as unknown as { _dialog?: { close(): void } })._dialog?.close();
|
|
111
|
+
});
|
|
112
|
+
</script>
|