@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
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
import './separator.css';
|
|
3
|
-
|
|
4
|
-
interface Props {
|
|
5
|
-
orientation?: 'horizontal' | 'vertical';
|
|
6
|
-
decorative?: boolean;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
const { orientation = 'horizontal', decorative = false, ...rest } = Astro.props;
|
|
10
|
-
---
|
|
11
|
-
|
|
12
|
-
{
|
|
13
|
-
decorative ? (
|
|
14
|
-
<div
|
|
15
|
-
class="pui-separator"
|
|
16
|
-
data-orientation={orientation}
|
|
17
|
-
role="none"
|
|
18
|
-
{...rest}
|
|
19
|
-
/>
|
|
20
|
-
) : orientation === 'vertical' ? (
|
|
21
|
-
<div
|
|
22
|
-
class="pui-separator"
|
|
23
|
-
data-orientation="vertical"
|
|
24
|
-
role="separator"
|
|
25
|
-
aria-orientation="vertical"
|
|
26
|
-
{...rest}
|
|
27
|
-
/>
|
|
28
|
-
) : (
|
|
29
|
-
<hr class="pui-separator" data-orientation="horizontal" {...rest} />
|
|
30
|
-
)
|
|
31
|
-
}
|
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
// Table (Astro) — the static primitive; the consumer authors the body in the default slot with
|
|
3
|
-
// the BEM classes. Sortable headers are <a>s, not events: sorting a static table is a navigation.
|
|
4
|
-
import './table.css';
|
|
5
|
-
import Icon from '../../display/icon/Icon.astro';
|
|
6
|
-
import Spinner from '../../feedback/spinner/Spinner.astro';
|
|
7
|
-
import type { IconName } from '../../../icons/index.js';
|
|
8
|
-
import {
|
|
9
|
-
formatSort,
|
|
10
|
-
toggleSort,
|
|
11
|
-
type SortDirection,
|
|
12
|
-
type SortState,
|
|
13
|
-
} from '../../../utils/sort/index.js';
|
|
14
|
-
import type { Column } from './types.js';
|
|
15
|
-
|
|
16
|
-
interface Props {
|
|
17
|
-
columns: Column[];
|
|
18
|
-
/** Which column is sorted, and how. Rendered here; the page orders the rows itself (ADR 0005). */
|
|
19
|
-
sort?: SortState;
|
|
20
|
-
/** Query parameter the header links read and write. */
|
|
21
|
-
sortParam?: string;
|
|
22
|
-
/** Frosts and disables the body while the rows are stale, leaving the header usable. */
|
|
23
|
-
loading?: boolean;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const {
|
|
27
|
-
columns,
|
|
28
|
-
sort = null,
|
|
29
|
-
sortParam = 'sort',
|
|
30
|
-
loading = false,
|
|
31
|
-
...rest
|
|
32
|
-
} = Astro.props;
|
|
33
|
-
const cols = columns.map((c) => c.width ?? 'auto').join(' ');
|
|
34
|
-
|
|
35
|
-
/** Header glyph per sort state — the shape carries the direction. */
|
|
36
|
-
const SORT_ICONS: Record<SortDirection | 'none', IconName> = {
|
|
37
|
-
asc: 'chevron-up',
|
|
38
|
-
desc: 'chevron-down',
|
|
39
|
-
none: 'chevron-up-down',
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
function sortState(key: string): SortDirection | 'none' {
|
|
43
|
-
return sort?.key === key ? sort.direction : 'none';
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/** `aria-sort` token for the header cell; only the sorted column carries one. */
|
|
47
|
-
function ariaSort(key: string): 'ascending' | 'descending' | undefined {
|
|
48
|
-
if (sort?.key !== key) return undefined;
|
|
49
|
-
return sort.direction === 'asc' ? 'ascending' : 'descending';
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/** Where this column's header links: the asc → desc → unsorted cycle, other query params preserved. */
|
|
53
|
-
function sortHref(key: string): string {
|
|
54
|
-
const url = new URL(Astro.url);
|
|
55
|
-
const value = formatSort(toggleSort(sort, key));
|
|
56
|
-
if (value) url.searchParams.set(sortParam, value);
|
|
57
|
-
else url.searchParams.delete(sortParam);
|
|
58
|
-
return url.pathname + url.search;
|
|
59
|
-
}
|
|
60
|
-
---
|
|
61
|
-
|
|
62
|
-
<div class="pui-table-wrap" {...rest}>
|
|
63
|
-
<table
|
|
64
|
-
class="pui-table"
|
|
65
|
-
style={`--pui-table-cols: ${cols}`}
|
|
66
|
-
aria-busy={loading ? 'true' : undefined}
|
|
67
|
-
>
|
|
68
|
-
<thead class="pui-table__head">
|
|
69
|
-
<tr class="pui-table__row" data-head>
|
|
70
|
-
{
|
|
71
|
-
columns.map((col) => (
|
|
72
|
-
<th
|
|
73
|
-
scope="col"
|
|
74
|
-
class="pui-table__cell"
|
|
75
|
-
data-head
|
|
76
|
-
data-align={col.align}
|
|
77
|
-
aria-sort={col.sortable ? ariaSort(col.key) : undefined}
|
|
78
|
-
>
|
|
79
|
-
{col.sortable ? (
|
|
80
|
-
<a class="pui-table__sort" href={sortHref(col.key)}>
|
|
81
|
-
{col.label}
|
|
82
|
-
<span
|
|
83
|
-
class="pui-table__sort-arrow"
|
|
84
|
-
data-sort={sortState(col.key)}
|
|
85
|
-
>
|
|
86
|
-
<Icon name={SORT_ICONS[sortState(col.key)]} size="sm" />
|
|
87
|
-
</span>
|
|
88
|
-
</a>
|
|
89
|
-
) : (
|
|
90
|
-
col.label
|
|
91
|
-
)}
|
|
92
|
-
</th>
|
|
93
|
-
))
|
|
94
|
-
}
|
|
95
|
-
</tr>
|
|
96
|
-
</thead>
|
|
97
|
-
{
|
|
98
|
-
/* `inert`, not CSS: `pointer-events` only stops the mouse and leaves the stale rows tabbable. */
|
|
99
|
-
}
|
|
100
|
-
<tbody class="pui-table__body" inert={loading ? true : undefined}>
|
|
101
|
-
<slot />
|
|
102
|
-
{
|
|
103
|
-
/* Must be a real <tr>/<td> inside the body, or the HTML parser hoists it out — see AUDIT.md. */
|
|
104
|
-
}
|
|
105
|
-
{
|
|
106
|
-
loading && (
|
|
107
|
-
<tr class="pui-table__loading" aria-hidden="true">
|
|
108
|
-
<td>
|
|
109
|
-
<Spinner size="xl" />
|
|
110
|
-
</td>
|
|
111
|
-
</tr>
|
|
112
|
-
)
|
|
113
|
-
}
|
|
114
|
-
</tbody>
|
|
115
|
-
</table>
|
|
116
|
-
</div>
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
import './breadcrumb.css';
|
|
3
|
-
|
|
4
|
-
interface Props {
|
|
5
|
-
items: { label: string; href?: string }[];
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
const { items, ...rest } = Astro.props;
|
|
9
|
-
---
|
|
10
|
-
|
|
11
|
-
<nav aria-label="Breadcrumb" {...rest}>
|
|
12
|
-
<ol class="pui-breadcrumb">
|
|
13
|
-
{
|
|
14
|
-
items.map((item, index) => (
|
|
15
|
-
<>
|
|
16
|
-
{index > 0 && (
|
|
17
|
-
<li class="pui-breadcrumb__sep" aria-hidden="true">
|
|
18
|
-
/
|
|
19
|
-
</li>
|
|
20
|
-
)}
|
|
21
|
-
{index < items.length - 1 ? (
|
|
22
|
-
<li class="pui-breadcrumb__item">
|
|
23
|
-
<a href={item.href} class="pui-breadcrumb__link">
|
|
24
|
-
{item.label}
|
|
25
|
-
</a>
|
|
26
|
-
</li>
|
|
27
|
-
) : (
|
|
28
|
-
<li class="pui-breadcrumb__item" aria-current="page">
|
|
29
|
-
{item.label}
|
|
30
|
-
</li>
|
|
31
|
-
)}
|
|
32
|
-
</>
|
|
33
|
-
))
|
|
34
|
-
}
|
|
35
|
-
</ol>
|
|
36
|
-
</nav>
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
import './navbar.css';
|
|
3
|
-
|
|
4
|
-
interface Props {
|
|
5
|
-
brandMark?: string;
|
|
6
|
-
brandName?: string;
|
|
7
|
-
brandHref?: string;
|
|
8
|
-
links?: { label: string; href: string; current?: boolean }[];
|
|
9
|
-
ariaLabel?: string;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
const {
|
|
13
|
-
brandMark,
|
|
14
|
-
brandName,
|
|
15
|
-
brandHref = '/',
|
|
16
|
-
links,
|
|
17
|
-
ariaLabel = 'Main navigation',
|
|
18
|
-
...rest
|
|
19
|
-
} = Astro.props;
|
|
20
|
-
|
|
21
|
-
const hasBrandSlot = Astro.slots.has('brand');
|
|
22
|
-
---
|
|
23
|
-
|
|
24
|
-
<header
|
|
25
|
-
class="pui-navbar container"
|
|
26
|
-
data-container-size="xl"
|
|
27
|
-
aria-label={ariaLabel}
|
|
28
|
-
{...rest}
|
|
29
|
-
>
|
|
30
|
-
<nav class="pui-navbar__content container-content">
|
|
31
|
-
{
|
|
32
|
-
hasBrandSlot ? (
|
|
33
|
-
<slot name="brand" />
|
|
34
|
-
) : (
|
|
35
|
-
<a href={brandHref} class="pui-navbar__brand">
|
|
36
|
-
{brandMark && <span class="pui-navbar__mark">{brandMark}</span>}
|
|
37
|
-
{brandName && <span class="pui-navbar__wordmark">{brandName}</span>}
|
|
38
|
-
</a>
|
|
39
|
-
)
|
|
40
|
-
}
|
|
41
|
-
{
|
|
42
|
-
links?.length && (
|
|
43
|
-
<ul class="pui-navbar__list">
|
|
44
|
-
{links.map((link) => (
|
|
45
|
-
<li>
|
|
46
|
-
<a
|
|
47
|
-
class="pui-navbar__link"
|
|
48
|
-
href={link.href}
|
|
49
|
-
aria-current={link.current ? 'page' : undefined}
|
|
50
|
-
>
|
|
51
|
-
{link.label}
|
|
52
|
-
</a>
|
|
53
|
-
</li>
|
|
54
|
-
))}
|
|
55
|
-
</ul>
|
|
56
|
-
)
|
|
57
|
-
}
|
|
58
|
-
<div class="pui-navbar__actions">
|
|
59
|
-
<slot name="actions" />
|
|
60
|
-
</div>
|
|
61
|
-
</nav>
|
|
62
|
-
</header>
|
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
import './sidebar.css';
|
|
3
|
-
import { generateId } from '../../../utils/a11y/id.js';
|
|
4
|
-
import { deriveGroupKey } from '../../../utils/controllers/sidebar.js';
|
|
5
|
-
import SidebarItemRender from './SidebarItemRender.astro';
|
|
6
|
-
import type { SidebarItem } from './types.js';
|
|
7
|
-
|
|
8
|
-
interface Props {
|
|
9
|
-
id?: string;
|
|
10
|
-
items: SidebarItem[];
|
|
11
|
-
activeHref?: string | null;
|
|
12
|
-
collapsed?: boolean;
|
|
13
|
-
drawerOpen?: boolean;
|
|
14
|
-
expandedGroups?: string[];
|
|
15
|
-
ariaLabel?: string;
|
|
16
|
-
drawerBreakpoint?: number;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const {
|
|
20
|
-
id,
|
|
21
|
-
items,
|
|
22
|
-
activeHref = null,
|
|
23
|
-
collapsed = false,
|
|
24
|
-
drawerOpen = false,
|
|
25
|
-
expandedGroups,
|
|
26
|
-
ariaLabel = 'Main navigation',
|
|
27
|
-
drawerBreakpoint = 768,
|
|
28
|
-
...rest
|
|
29
|
-
} = Astro.props;
|
|
30
|
-
|
|
31
|
-
const sidebarId = id ?? generateId('pui-sidebar');
|
|
32
|
-
|
|
33
|
-
// Pre-compute the initial expanded-groups set as a JSON string so the
|
|
34
|
-
// mount script can hydrate `mountSidebar` with it.
|
|
35
|
-
const initialExpanded = expandedGroups
|
|
36
|
-
? JSON.stringify(expandedGroups)
|
|
37
|
-
: JSON.stringify(
|
|
38
|
-
// Fall back to defaultOpen=true groups derived from item shape.
|
|
39
|
-
collectDefaultOpenKeys(items),
|
|
40
|
-
);
|
|
41
|
-
|
|
42
|
-
function collectDefaultOpenKeys(arr: SidebarItem[]): string[] {
|
|
43
|
-
const keys: string[] = [];
|
|
44
|
-
arr.forEach((it) => {
|
|
45
|
-
if (it.type === 'group') {
|
|
46
|
-
if (it.defaultOpen) keys.push(it.key ?? deriveGroupKey(it.label));
|
|
47
|
-
keys.push(...collectDefaultOpenKeys(it.items));
|
|
48
|
-
}
|
|
49
|
-
});
|
|
50
|
-
return keys;
|
|
51
|
-
}
|
|
52
|
-
---
|
|
53
|
-
|
|
54
|
-
<nav
|
|
55
|
-
id={sidebarId}
|
|
56
|
-
class="pui-sidebar"
|
|
57
|
-
data-pui-sidebar
|
|
58
|
-
data-state={collapsed ? 'collapsed' : 'expanded'}
|
|
59
|
-
data-drawer-state="closed"
|
|
60
|
-
aria-label={ariaLabel}
|
|
61
|
-
data-collapsed-initially={collapsed ? 'true' : undefined}
|
|
62
|
-
data-drawer-open-initially={drawerOpen ? 'true' : undefined}
|
|
63
|
-
data-active-href={activeHref ?? undefined}
|
|
64
|
-
data-drawer-breakpoint={String(drawerBreakpoint)}
|
|
65
|
-
data-expanded-groups={initialExpanded}
|
|
66
|
-
{...rest}
|
|
67
|
-
>
|
|
68
|
-
<div class="pui-sidebar__backdrop" data-sidebar-backdrop aria-hidden="true">
|
|
69
|
-
</div>
|
|
70
|
-
<div class="pui-sidebar__panel" data-sidebar-panel>
|
|
71
|
-
{
|
|
72
|
-
Astro.slots.has('brand') && (
|
|
73
|
-
<div class="pui-sidebar__brand">
|
|
74
|
-
<slot name="brand" />
|
|
75
|
-
</div>
|
|
76
|
-
)
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
<ul class="pui-sidebar__menu" data-sidebar-menu>
|
|
80
|
-
{items.map((item) => <SidebarItemRender item={item} />)}
|
|
81
|
-
</ul>
|
|
82
|
-
|
|
83
|
-
{
|
|
84
|
-
Astro.slots.has('footer') && (
|
|
85
|
-
<div class="pui-sidebar__footer">
|
|
86
|
-
<slot name="footer" />
|
|
87
|
-
</div>
|
|
88
|
-
)
|
|
89
|
-
}
|
|
90
|
-
</div>
|
|
91
|
-
</nav>
|
|
92
|
-
|
|
93
|
-
<script>
|
|
94
|
-
import { mountSidebar } from '@pienter/ui/utils/controllers/sidebar.js';
|
|
95
|
-
|
|
96
|
-
document.querySelectorAll<HTMLElement>('[data-pui-sidebar]').forEach((el) => {
|
|
97
|
-
const node = el as HTMLElement & {
|
|
98
|
-
_sidebar?: ReturnType<typeof mountSidebar>;
|
|
99
|
-
};
|
|
100
|
-
if (node._sidebar) return; // idempotent — guard against double-mounts
|
|
101
|
-
|
|
102
|
-
const collapsed = el.dataset.collapsedInitially === 'true';
|
|
103
|
-
const drawerOpenInitially = el.dataset.drawerOpenInitially === 'true';
|
|
104
|
-
const activeHref = el.dataset.activeHref ?? null;
|
|
105
|
-
const drawerBreakpoint = Number(el.dataset.drawerBreakpoint ?? '768');
|
|
106
|
-
let expandedGroups: string[];
|
|
107
|
-
try {
|
|
108
|
-
expandedGroups = JSON.parse(el.dataset.expandedGroups ?? '[]');
|
|
109
|
-
} catch {
|
|
110
|
-
expandedGroups = [];
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
const control = mountSidebar(el, {
|
|
114
|
-
collapsed,
|
|
115
|
-
drawerBreakpoint,
|
|
116
|
-
expandedGroups,
|
|
117
|
-
activeHref,
|
|
118
|
-
// Vue mirrors `onDrawerOpenChange` into a reactive prop whose
|
|
119
|
-
// watcher then calls openDrawer()/closeDrawer(). Astro has no
|
|
120
|
-
// reactivity, so the mount script wires the callback directly to
|
|
121
|
-
// the imperative API. Without this, backdrop click and Escape —
|
|
122
|
-
// both of which emit `onDrawerOpenChange(false)` from the
|
|
123
|
-
// controller — are no-ops in Astro.
|
|
124
|
-
onDrawerOpenChange: (open) => {
|
|
125
|
-
if (open) control.openDrawer();
|
|
126
|
-
else control.closeDrawer();
|
|
127
|
-
},
|
|
128
|
-
});
|
|
129
|
-
node._sidebar = control;
|
|
130
|
-
if (drawerOpenInitially) control.openDrawer();
|
|
131
|
-
});
|
|
132
|
-
</script>
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
import Icon from '../../display/icon/Icon.astro';
|
|
3
|
-
import type { IconName } from '../../../icons/index.js';
|
|
4
|
-
import { generateId } from '../../../utils/a11y/id.js';
|
|
5
|
-
import { deriveGroupKey } from '../../../utils/controllers/sidebar.js';
|
|
6
|
-
import type { SidebarItem } from './types.js';
|
|
7
|
-
|
|
8
|
-
interface Props {
|
|
9
|
-
item: SidebarItem;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
const { item } = Astro.props;
|
|
13
|
-
const itemsId = generateId('pui-sidebar-group');
|
|
14
|
-
const resolvedKey =
|
|
15
|
-
item.type === 'group' ? (item.key ?? deriveGroupKey(item.label)) : '';
|
|
16
|
-
---
|
|
17
|
-
|
|
18
|
-
{
|
|
19
|
-
item.type === 'link' && (
|
|
20
|
-
<li class="pui-sidebar__item">
|
|
21
|
-
<a
|
|
22
|
-
class="pui-sidebar__link"
|
|
23
|
-
data-sidebar-menu-item
|
|
24
|
-
data-sidebar-link
|
|
25
|
-
href={item.href}
|
|
26
|
-
data-disabled={item.disabled ? 'true' : undefined}
|
|
27
|
-
aria-disabled={item.disabled ? 'true' : undefined}
|
|
28
|
-
tabindex="-1"
|
|
29
|
-
>
|
|
30
|
-
{item.icon && <Icon name={item.icon as IconName} size="sm" />}
|
|
31
|
-
<span class="pui-sidebar__link-label">{item.label}</span>
|
|
32
|
-
{item.badge !== undefined && (
|
|
33
|
-
<span class="pui-sidebar__badge">{item.badge}</span>
|
|
34
|
-
)}
|
|
35
|
-
</a>
|
|
36
|
-
</li>
|
|
37
|
-
)
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
{
|
|
41
|
-
item.type === 'group' && (
|
|
42
|
-
<li
|
|
43
|
-
class="pui-sidebar__group"
|
|
44
|
-
data-sidebar-group
|
|
45
|
-
data-group-key={resolvedKey}
|
|
46
|
-
data-default-open={item.defaultOpen ? 'true' : undefined}
|
|
47
|
-
data-state="collapsed"
|
|
48
|
-
>
|
|
49
|
-
<button
|
|
50
|
-
type="button"
|
|
51
|
-
class="pui-sidebar__group-toggle"
|
|
52
|
-
data-sidebar-menu-item
|
|
53
|
-
data-sidebar-group-toggle
|
|
54
|
-
aria-expanded="false"
|
|
55
|
-
aria-controls={itemsId}
|
|
56
|
-
tabindex="-1"
|
|
57
|
-
>
|
|
58
|
-
{item.icon && <Icon name={item.icon as IconName} size="sm" />}
|
|
59
|
-
<span class="pui-sidebar__link-label">{item.label}</span>
|
|
60
|
-
<Icon name="chevron-down" size="sm" class="pui-sidebar__group-caret" />
|
|
61
|
-
</button>
|
|
62
|
-
<ul
|
|
63
|
-
id={itemsId}
|
|
64
|
-
class="pui-sidebar__group-items"
|
|
65
|
-
data-sidebar-group-items
|
|
66
|
-
role="group"
|
|
67
|
-
hidden
|
|
68
|
-
>
|
|
69
|
-
{item.items.map((child) => (
|
|
70
|
-
<Astro.self item={child} />
|
|
71
|
-
))}
|
|
72
|
-
</ul>
|
|
73
|
-
</li>
|
|
74
|
-
)
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
{
|
|
78
|
-
item.type === 'section' && (
|
|
79
|
-
<li class="pui-sidebar__section" role="presentation">
|
|
80
|
-
<span class="pui-sidebar__section-label">{item.label}</span>
|
|
81
|
-
</li>
|
|
82
|
-
)
|
|
83
|
-
}
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<!-- link -->
|
|
3
|
-
<li v-if="item.type === 'link'" class="pui-sidebar__item">
|
|
4
|
-
<a
|
|
5
|
-
class="pui-sidebar__link"
|
|
6
|
-
data-sidebar-menu-item
|
|
7
|
-
data-sidebar-link
|
|
8
|
-
:href="item.href"
|
|
9
|
-
:data-disabled="item.disabled ? 'true' : undefined"
|
|
10
|
-
:aria-disabled="item.disabled ? 'true' : undefined"
|
|
11
|
-
tabindex="-1"
|
|
12
|
-
>
|
|
13
|
-
<Icon v-if="item.icon" :name="item.icon as IconName" size="sm" />
|
|
14
|
-
<span class="pui-sidebar__link-label">{{ item.label }}</span>
|
|
15
|
-
<span v-if="item.badge !== undefined" class="pui-sidebar__badge">{{
|
|
16
|
-
item.badge
|
|
17
|
-
}}</span>
|
|
18
|
-
</a>
|
|
19
|
-
</li>
|
|
20
|
-
|
|
21
|
-
<!-- group -->
|
|
22
|
-
<li
|
|
23
|
-
v-else-if="item.type === 'group'"
|
|
24
|
-
class="pui-sidebar__group"
|
|
25
|
-
data-sidebar-group
|
|
26
|
-
:data-group-key="resolvedKey"
|
|
27
|
-
:data-default-open="item.defaultOpen ? 'true' : undefined"
|
|
28
|
-
data-state="collapsed"
|
|
29
|
-
>
|
|
30
|
-
<button
|
|
31
|
-
type="button"
|
|
32
|
-
class="pui-sidebar__group-toggle"
|
|
33
|
-
data-sidebar-menu-item
|
|
34
|
-
data-sidebar-group-toggle
|
|
35
|
-
:aria-expanded="'false'"
|
|
36
|
-
:aria-controls="itemsId"
|
|
37
|
-
tabindex="-1"
|
|
38
|
-
>
|
|
39
|
-
<Icon v-if="item.icon" :name="item.icon as IconName" size="sm" />
|
|
40
|
-
<span class="pui-sidebar__link-label">{{ item.label }}</span>
|
|
41
|
-
<Icon
|
|
42
|
-
name="chevron-down"
|
|
43
|
-
size="sm"
|
|
44
|
-
class="pui-sidebar__group-caret"
|
|
45
|
-
/>
|
|
46
|
-
</button>
|
|
47
|
-
<ul
|
|
48
|
-
:id="itemsId"
|
|
49
|
-
class="pui-sidebar__group-items"
|
|
50
|
-
data-sidebar-group-items
|
|
51
|
-
role="group"
|
|
52
|
-
hidden
|
|
53
|
-
>
|
|
54
|
-
<SidebarItemRender
|
|
55
|
-
v-for="(child, idx) in item.items"
|
|
56
|
-
:key="childKey(child, idx)"
|
|
57
|
-
:item="child"
|
|
58
|
-
/>
|
|
59
|
-
</ul>
|
|
60
|
-
</li>
|
|
61
|
-
|
|
62
|
-
<!-- section -->
|
|
63
|
-
<li
|
|
64
|
-
v-else-if="item.type === 'section'"
|
|
65
|
-
class="pui-sidebar__section"
|
|
66
|
-
role="presentation"
|
|
67
|
-
>
|
|
68
|
-
<span class="pui-sidebar__section-label">{{ item.label }}</span>
|
|
69
|
-
</li>
|
|
70
|
-
</template>
|
|
71
|
-
|
|
72
|
-
<script setup lang="ts">
|
|
73
|
-
import { computed } from 'vue';
|
|
74
|
-
import Icon from '../../display/icon/Icon.vue';
|
|
75
|
-
import type { IconName } from '../../../icons/index.js';
|
|
76
|
-
import { generateId } from '../../../utils/a11y/id.js';
|
|
77
|
-
import { deriveGroupKey } from '../../../utils/controllers/sidebar.js';
|
|
78
|
-
import type { SidebarItem } from './types.js';
|
|
79
|
-
|
|
80
|
-
const props = defineProps<{ item: SidebarItem }>();
|
|
81
|
-
|
|
82
|
-
// Stable id for the group's child list (linked from `aria-controls` on
|
|
83
|
-
// the toggle). Generated once per render of this component instance.
|
|
84
|
-
const itemsId = generateId('pui-sidebar-group');
|
|
85
|
-
|
|
86
|
-
const resolvedKey = computed(() =>
|
|
87
|
-
props.item.type === 'group'
|
|
88
|
-
? (props.item.key ?? deriveGroupKey(props.item.label))
|
|
89
|
-
: '',
|
|
90
|
-
);
|
|
91
|
-
|
|
92
|
-
function childKey(child: SidebarItem, idx: number): string {
|
|
93
|
-
if (child.type === 'link') return `link:${child.href}`;
|
|
94
|
-
if (child.type === 'group')
|
|
95
|
-
return `group:${child.key ?? deriveGroupKey(child.label)}`;
|
|
96
|
-
return `section:${child.label}:${idx}`;
|
|
97
|
-
}
|
|
98
|
-
</script>
|
|
@@ -1,112 +0,0 @@
|
|
|
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>
|