@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.
Files changed (89) hide show
  1. package/CHANGELOG.md +259 -0
  2. package/CONVENTIONS.md +297 -385
  3. package/README.md +3 -18
  4. package/components/feedback/toast/toast.css +1 -1
  5. package/components/form/combobox/Combobox.vue +21 -38
  6. package/components/form/combobox/combobox.css +1 -1
  7. package/components/form/form/Form.vue +1 -2
  8. package/components/form/label/label.css +1 -1
  9. package/components/form/number-field/NumberField.vue +1 -1
  10. package/components/form/number-field/number-field.css +1 -1
  11. package/components/form/radio-group/RadioGroup.vue +2 -5
  12. package/components/form/slider/slider.css +2 -3
  13. package/components/form/tags-input/tags-input.css +1 -2
  14. package/components/form/textarea/textarea.css +1 -1
  15. package/components/layout/accordion/Accordion.vue +6 -27
  16. package/components/layout/collapsible/Collapsible.vue +9 -19
  17. package/components/layout/table/table.css +3 -3
  18. package/components/navigation/pagination/Pagination.vue +1 -1
  19. package/components/navigation/pagination/PaginationFooter.vue +23 -0
  20. package/components/navigation/pagination/pagination.css +128 -29
  21. package/components/navigation/sidebar/Sidebar.vue +326 -84
  22. package/components/navigation/sidebar/SidebarMenuItem.vue +199 -0
  23. package/components/navigation/sidebar/context.ts +21 -0
  24. package/components/navigation/sidebar/sidebar.css +346 -78
  25. package/components/navigation/sidebar/types.ts +13 -1
  26. package/components/overlay/alert-dialog/AlertDialog.vue +10 -31
  27. package/components/overlay/command/Command.vue +10 -38
  28. package/components/overlay/command/command.css +1 -1
  29. package/components/overlay/dropdown-menu/DropdownMenu.vue +37 -62
  30. package/components/overlay/modal/Modal.vue +7 -28
  31. package/components/overlay/popover/Popover.vue +9 -31
  32. package/components/overlay/sheet/Sheet.vue +7 -28
  33. package/components/overlay/tooltip/Tooltip.vue +14 -19
  34. package/{utils/controllers/dialog.ts → composables/useDialog.ts} +90 -78
  35. package/composables/useDisclosure.ts +113 -0
  36. package/composables/useEventListener.ts +16 -0
  37. package/{utils/controllers/menu.ts → composables/useMenu.ts} +66 -102
  38. package/{utils/controllers/popover.ts → composables/usePopover.ts} +107 -120
  39. package/package.json +6 -38
  40. package/styles/0-settings/colors.css +8 -1
  41. package/styles/0-settings/layout.css +18 -0
  42. package/styles/0-settings/motion.css +2 -2
  43. package/styles/0-settings/spacing.css +3 -1
  44. package/utils/navigation/sidebar.ts +97 -0
  45. package/utils/validation/form.ts +6 -9
  46. package/components/action/button/Button.astro +0 -95
  47. package/components/action/button/IconButton.astro +0 -86
  48. package/components/display/avatar/Avatar.astro +0 -17
  49. package/components/display/avatar/AvatarStack.astro +0 -9
  50. package/components/display/badge/Badge.astro +0 -15
  51. package/components/display/empty/Empty.astro +0 -9
  52. package/components/display/icon/Icon.astro +0 -52
  53. package/components/feedback/alert/Alert.astro +0 -52
  54. package/components/feedback/progress/Progress.astro +0 -68
  55. package/components/feedback/skeleton/Skeleton.astro +0 -32
  56. package/components/feedback/spinner/Spinner.astro +0 -25
  57. package/components/feedback/toast/Toast.astro +0 -50
  58. package/components/form/checkbox/Checkbox.astro +0 -79
  59. package/components/form/date-input/DateInput.astro +0 -105
  60. package/components/form/form/Form.astro +0 -106
  61. package/components/form/input-otp/InputOTP.astro +0 -147
  62. package/components/form/label/Label.astro +0 -13
  63. package/components/form/number-field/NumberField.astro +0 -142
  64. package/components/form/radio-group/RadioGroup.astro +0 -105
  65. package/components/form/select/Select.astro +0 -105
  66. package/components/form/slider/Slider.astro +0 -205
  67. package/components/form/switch/Switch.astro +0 -75
  68. package/components/form/tags-input/TagsInput.astro +0 -153
  69. package/components/form/text-input/TextInput.astro +0 -84
  70. package/components/form/textarea/Textarea.astro +0 -86
  71. package/components/layout/card/Card.astro +0 -13
  72. package/components/layout/separator/Separator.astro +0 -31
  73. package/components/layout/table/Table.astro +0 -116
  74. package/components/navigation/breadcrumb/Breadcrumb.astro +0 -36
  75. package/components/navigation/navbar/Navbar.astro +0 -62
  76. package/components/navigation/sidebar/Sidebar.astro +0 -132
  77. package/components/navigation/sidebar/SidebarItemRender.astro +0 -83
  78. package/components/navigation/sidebar/SidebarItemRender.vue +0 -98
  79. package/components/overlay/alert-dialog/AlertDialog.astro +0 -112
  80. package/components/overlay/modal/Modal.astro +0 -66
  81. package/utils/controllers/disclosure.ts +0 -117
  82. package/utils/controllers/form.ts +0 -524
  83. package/utils/controllers/index.ts +0 -39
  84. package/utils/controllers/number-field.ts +0 -103
  85. package/utils/controllers/otp.ts +0 -252
  86. package/utils/controllers/sidebar.ts +0 -610
  87. package/utils/controllers/slider.ts +0 -336
  88. package/utils/controllers/tags-input.ts +0 -255
  89. package/utils/controllers/toast.ts +0 -426
@@ -1,13 +0,0 @@
1
- ---
2
- import './card.css';
3
-
4
- interface Props {
5
- variant?: 'bordered' | 'brand' | 'muted';
6
- }
7
-
8
- const { variant, ...rest } = Astro.props;
9
- ---
10
-
11
- <div class="pui-card" data-variant={variant} {...rest}>
12
- <slot />
13
- </div>
@@ -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>