zabi-components 7.0.2 → 8.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -3
- package/THEMING.md +255 -15
- package/dist/atoms/ActionPanel.svelte +12 -7
- package/dist/atoms/ActionPanel.svelte.d.ts +2 -0
- package/dist/atoms/Badge.svelte +93 -56
- package/dist/atoms/Badge.svelte.d.ts +15 -3
- package/dist/atoms/Button.svelte +49 -49
- package/dist/atoms/Button.svelte.d.ts +3 -0
- package/dist/atoms/Card.svelte +31 -28
- package/dist/atoms/Card.svelte.d.ts +5 -2
- package/dist/atoms/CardContent.svelte +10 -2
- package/dist/atoms/CardContent.svelte.d.ts +2 -0
- package/dist/atoms/CardFooter.svelte +10 -2
- package/dist/atoms/CardFooter.svelte.d.ts +2 -0
- package/dist/atoms/CardHeader.svelte +10 -2
- package/dist/atoms/CardHeader.svelte.d.ts +2 -0
- package/dist/atoms/Checkbox.svelte +4 -0
- package/dist/atoms/Checkbox.svelte.d.ts +2 -0
- package/dist/atoms/CodeBlock.svelte +15 -7
- package/dist/atoms/CodeBlock.svelte.d.ts +9 -3
- package/dist/atoms/ColorPicker.svelte +19 -9
- package/dist/atoms/ColorPicker.svelte.d.ts +3 -1
- package/dist/atoms/Container.svelte +2 -3
- package/dist/atoms/Divider.svelte +4 -4
- package/dist/atoms/FeatureCard.svelte +4 -3
- package/dist/atoms/Heading.svelte +56 -27
- package/dist/atoms/Heading.svelte.d.ts +17 -4
- package/dist/atoms/IconButton.svelte +30 -37
- package/dist/atoms/Input.svelte +26 -34
- package/dist/atoms/List.svelte +10 -2
- package/dist/atoms/List.svelte.d.ts +2 -0
- package/dist/atoms/ListItem.svelte +2 -2
- package/dist/atoms/ListItemLeading.svelte +4 -2
- package/dist/atoms/ListItemLeading.svelte.d.ts +1 -1
- package/dist/atoms/OptimizedImage.svelte +11 -2
- package/dist/atoms/OptimizedImage.svelte.d.ts +2 -0
- package/dist/atoms/Progress.svelte +4 -1
- package/dist/atoms/Progress.svelte.d.ts +2 -0
- package/dist/atoms/Radio.svelte +4 -0
- package/dist/atoms/Radio.svelte.d.ts +2 -0
- package/dist/atoms/Select.svelte +65 -67
- package/dist/atoms/Select.svelte.d.ts +4 -0
- package/dist/atoms/SelectionControl.svelte +5 -1
- package/dist/atoms/SelectionControl.svelte.d.ts +2 -0
- package/dist/atoms/Skeleton.svelte +5 -3
- package/dist/atoms/Table.svelte +2 -1
- package/dist/atoms/Text.svelte +34 -5
- package/dist/atoms/Text.svelte.d.ts +7 -1
- package/dist/atoms/Textarea.svelte +7 -6
- package/dist/atoms/ThemeToggle.svelte +17 -14
- package/dist/atoms/ThemeToggle.svelte.d.ts +2 -0
- package/dist/atoms/Toast.svelte +10 -8
- package/dist/atoms/Toggle.svelte +21 -13
- package/dist/atoms/Toggle.svelte.d.ts +7 -0
- package/dist/atoms/Tooltip.svelte +80 -22
- package/dist/atoms/Tooltip.svelte.d.ts +15 -0
- package/dist/components/index.d.ts +3 -1
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/organisms/index.d.ts +1 -0
- package/dist/components/organisms/index.d.ts.map +1 -1
- package/dist/components/util/cn.d.ts +8 -0
- package/dist/components/util/cn.d.ts.map +1 -0
- package/dist/components/util/ssr-safe.d.ts +4 -4
- package/dist/components/util/ssr-safe.d.ts.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -6
- package/dist/molecules/Alert.svelte +43 -64
- package/dist/molecules/Alert.svelte.d.ts +7 -1
- package/dist/molecules/ComponentDemo.svelte +10 -2
- package/dist/molecules/ComponentDemo.svelte.d.ts +2 -0
- package/dist/molecules/ContactForm.svelte +12 -2
- package/dist/molecules/ContactForm.svelte.d.ts +2 -0
- package/dist/molecules/Dropdown.svelte +40 -11
- package/dist/molecules/Dropdown.svelte.d.ts +4 -0
- package/dist/molecules/EmptyState.svelte +2 -1
- package/dist/molecules/Form.svelte +25 -14
- package/dist/molecules/Form.svelte.d.ts +13 -5
- package/dist/molecules/FormField.svelte +2 -1
- package/dist/molecules/Header.svelte +13 -3
- package/dist/molecules/Header.svelte.d.ts +2 -0
- package/dist/molecules/ImageUpload.svelte +54 -15
- package/dist/molecules/ImageUpload.svelte.d.ts +16 -5
- package/dist/molecules/Modal.svelte +110 -31
- package/dist/molecules/Modal.svelte.d.ts +9 -6
- package/dist/molecules/NavigationMenu.svelte +15 -3
- package/dist/molecules/NavigationMenu.svelte.d.ts +4 -0
- package/dist/molecules/NavigationMenuContent.svelte +36 -38
- package/dist/molecules/NavigationMenuContent.svelte.d.ts +2 -0
- package/dist/molecules/NavigationMenuItem.svelte +10 -2
- package/dist/molecules/NavigationMenuItem.svelte.d.ts +2 -0
- package/dist/molecules/NavigationMenuLink.svelte +10 -2
- package/dist/molecules/NavigationMenuLink.svelte.d.ts +2 -0
- package/dist/molecules/NavigationMenuList.svelte +10 -2
- package/dist/molecules/NavigationMenuList.svelte.d.ts +2 -0
- package/dist/molecules/NavigationMenuTrigger.svelte +16 -3
- package/dist/molecules/NavigationMenuTrigger.svelte.d.ts +2 -0
- package/dist/molecules/Page.svelte +8 -2
- package/dist/molecules/Page.svelte.d.ts +2 -0
- package/dist/molecules/PropsTable.svelte +6 -3
- package/dist/molecules/PropsTable.svelte.d.ts +2 -0
- package/dist/molecules/RadioGroup.svelte +5 -1
- package/dist/molecules/RadioGroup.svelte.d.ts +2 -0
- package/dist/molecules/Section.svelte +12 -4
- package/dist/molecules/Section.svelte.d.ts +3 -1
- package/dist/molecules/SidebarBrandHeader.svelte +13 -4
- package/dist/molecules/SidebarBrandHeader.svelte.d.ts +2 -0
- package/dist/molecules/SidebarFooter.svelte +12 -4
- package/dist/molecules/SidebarFooter.svelte.d.ts +2 -0
- package/dist/molecules/SidebarNavSection.svelte +12 -4
- package/dist/molecules/SidebarNavSection.svelte.d.ts +2 -0
- package/dist/molecules/SlideUp.svelte +85 -21
- package/dist/molecules/SlideUp.svelte.d.ts +5 -4
- package/dist/molecules/Tabs.svelte +25 -19
- package/dist/molecules/Tabs.svelte.d.ts +8 -4
- package/dist/molecules/Toaster.svelte +3 -2
- package/dist/molecules/ToasterToast.svelte +41 -10
- package/dist/molecules/ToasterToast.svelte.d.ts +2 -0
- package/dist/organisms/SidebarAccountPanel.svelte +4 -0
- package/dist/organisms/SidebarAccountPanel.svelte.d.ts +2 -0
- package/dist/organisms/SidebarNavigation.svelte +102 -59
- package/dist/organisms/SidebarNavigation.svelte.d.ts +2 -0
- package/dist/organisms/SidebarPanel.svelte +19 -11
- package/dist/organisms/SidebarPanel.svelte.d.ts +2 -0
- package/dist/organisms/SidebarShell.svelte +112 -0
- package/dist/organisms/SidebarShell.svelte.d.ts +52 -0
- package/dist/organisms/TopNavbar.svelte +88 -17
- package/dist/organisms/TopNavbar.svelte.d.ts +2 -0
- package/dist/organisms/index.d.ts +1 -0
- package/dist/organisms/index.js +1 -0
- package/dist/routes/lib/variant-utils.d.ts +26 -0
- package/dist/routes/lib/{variant-utils.ts → variant-utils.js} +15 -46
- package/dist/types/events.js +1 -0
- package/dist/types/index.js +3 -0
- package/dist/util/cn.d.ts +7 -0
- package/dist/util/cn.js +48 -0
- package/dist/util/ssr-safe.d.ts +4 -4
- package/dist/zabi-components-colors.css +2 -2
- package/dist/zabi-components-theme-dark-only.css +139 -92
- package/dist/zabi-components-theme-dark.css +139 -92
- package/dist/zabi-components-theme-only.css +268 -122
- package/dist/zabi-components-theme.css +268 -122
- package/dist/zabi-components.css +831 -883
- package/docs/lucide-icons.md +3 -1
- package/package.json +19 -24
- package/dist/lib/layout-width-tokens.d.ts +0 -17
- package/dist/lib/layout-width-tokens.d.ts.map +0 -1
- package/dist/lib/showcase/component-docs/Button.d.ts +0 -3
- package/dist/lib/showcase/component-docs/Button.d.ts.map +0 -1
- package/dist/lib/showcase/component-docs/Input.d.ts +0 -3
- package/dist/lib/showcase/component-docs/Input.d.ts.map +0 -1
- package/dist/lib/showcase/component-docs/List.d.ts +0 -3
- package/dist/lib/showcase/component-docs/List.d.ts.map +0 -1
- package/dist/lib/showcase/component-docs/Modal.d.ts +0 -3
- package/dist/lib/showcase/component-docs/Modal.d.ts.map +0 -1
- package/dist/lib/showcase/component-docs/Radio.d.ts +0 -3
- package/dist/lib/showcase/component-docs/Radio.d.ts.map +0 -1
- package/dist/lib/showcase/component-docs/SidebarNavigation.d.ts +0 -3
- package/dist/lib/showcase/component-docs/SidebarNavigation.d.ts.map +0 -1
- package/dist/lib/showcase/component-docs/Skeleton.d.ts +0 -3
- package/dist/lib/showcase/component-docs/Skeleton.d.ts.map +0 -1
- package/dist/lib/showcase/component-docs/_shared.d.ts +0 -12
- package/dist/lib/showcase/component-docs/_shared.d.ts.map +0 -1
- package/dist/lib/showcase/component-docs/index.d.ts +0 -4
- package/dist/lib/showcase/component-docs/index.d.ts.map +0 -1
- package/dist/lib/showcase/components-catalog.d.ts +0 -3
- package/dist/lib/showcase/components-catalog.d.ts.map +0 -1
- package/dist/lib/showcase/components-showcase-constants.d.ts +0 -89
- package/dist/lib/showcase/components-showcase-constants.d.ts.map +0 -1
- package/dist/lib/showcase/docs-sidebar-helpers.d.ts +0 -15
- package/dist/lib/showcase/docs-sidebar-helpers.d.ts.map +0 -1
- package/dist/routes/lib/focus-utils.d.ts +0 -12
- package/dist/routes/lib/focus-utils.ts +0 -110
- package/dist/routes/lib/ssr-safe.ts +0 -58
- package/scripts/fix-lucide-svelte-icon-dts.js +0 -35
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
2
3
|
import {
|
|
3
|
-
|
|
4
|
+
getFocusableElements,
|
|
4
5
|
saveFocus,
|
|
5
6
|
returnFocus,
|
|
6
7
|
focusFirstElement,
|
|
@@ -11,6 +12,7 @@
|
|
|
11
12
|
import CardContent from '../atoms/CardContent.svelte';
|
|
12
13
|
import CardFooter from '../atoms/CardFooter.svelte';
|
|
13
14
|
|
|
15
|
+
import { cn } from "../util/cn.js";
|
|
14
16
|
type Size = 'sm' | 'md' | 'lg';
|
|
15
17
|
|
|
16
18
|
interface Props {
|
|
@@ -18,10 +20,16 @@
|
|
|
18
20
|
title?: string;
|
|
19
21
|
description?: string;
|
|
20
22
|
size?: Size;
|
|
23
|
+
/** Render the close button even when there is no `title`. */
|
|
24
|
+
showClose?: boolean;
|
|
21
25
|
onclick?: (event: Event) => void;
|
|
22
26
|
onkeydown?: (event: Event) => void;
|
|
23
|
-
/** On the `role="dialog"`
|
|
27
|
+
/** On the `role="dialog"` panel (testing, analytics). */
|
|
24
28
|
"data-testid"?: string;
|
|
29
|
+
/** Extra classes for the dialog panel. */
|
|
30
|
+
class?: string;
|
|
31
|
+
children?: Snippet;
|
|
32
|
+
footer?: Snippet;
|
|
25
33
|
}
|
|
26
34
|
|
|
27
35
|
let {
|
|
@@ -29,19 +37,21 @@
|
|
|
29
37
|
title = '',
|
|
30
38
|
description = '',
|
|
31
39
|
size = 'md',
|
|
40
|
+
showClose = true,
|
|
41
|
+
class: className = "",
|
|
32
42
|
onclick,
|
|
33
43
|
onkeydown,
|
|
34
44
|
"data-testid": dataTestId = undefined,
|
|
35
45
|
children,
|
|
36
46
|
footer,
|
|
37
47
|
...restProps
|
|
38
|
-
}: Props
|
|
48
|
+
}: Props = $props();
|
|
39
49
|
|
|
40
50
|
const modalTitleId = generateId('modal-title');
|
|
41
51
|
const modalDescriptionId = generateId('modal-description');
|
|
42
52
|
|
|
43
53
|
let modalContainer = $state<HTMLDivElement>();
|
|
44
|
-
let
|
|
54
|
+
let focusActive = false;
|
|
45
55
|
|
|
46
56
|
const sizeClasses = $derived(
|
|
47
57
|
{
|
|
@@ -51,13 +61,33 @@
|
|
|
51
61
|
}[size] || 'w-full md:w-[28rem]',
|
|
52
62
|
);
|
|
53
63
|
|
|
64
|
+
/** Ref-counted on `<body>` so nested Modal/SlideUp overlays share one lock. */
|
|
65
|
+
function lockBodyScroll(): () => void {
|
|
66
|
+
const body = document.body;
|
|
67
|
+
const count = Number(body.dataset.zabiScrollLock ?? '0');
|
|
68
|
+
if (count === 0) {
|
|
69
|
+
body.dataset.zabiScrollLockOverflow = body.style.overflow;
|
|
70
|
+
body.style.overflow = 'hidden';
|
|
71
|
+
}
|
|
72
|
+
body.dataset.zabiScrollLock = String(count + 1);
|
|
73
|
+
return () => {
|
|
74
|
+
const next = Number(body.dataset.zabiScrollLock ?? '1') - 1;
|
|
75
|
+
if (next <= 0) {
|
|
76
|
+
body.style.overflow = body.dataset.zabiScrollLockOverflow ?? '';
|
|
77
|
+
delete body.dataset.zabiScrollLock;
|
|
78
|
+
delete body.dataset.zabiScrollLockOverflow;
|
|
79
|
+
} else {
|
|
80
|
+
body.dataset.zabiScrollLock = String(next);
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
54
85
|
function closeModal(event?: Event) {
|
|
55
86
|
isOpen = false;
|
|
56
|
-
if (
|
|
57
|
-
|
|
58
|
-
|
|
87
|
+
if (focusActive) {
|
|
88
|
+
focusActive = false;
|
|
89
|
+
returnFocus();
|
|
59
90
|
}
|
|
60
|
-
returnFocus();
|
|
61
91
|
if (onclick && event) {
|
|
62
92
|
onclick(event);
|
|
63
93
|
}
|
|
@@ -67,17 +97,52 @@
|
|
|
67
97
|
const container = modalContainer;
|
|
68
98
|
if (isOpen && container) {
|
|
69
99
|
saveFocus();
|
|
70
|
-
|
|
71
|
-
|
|
100
|
+
focusActive = true;
|
|
101
|
+
const unlockScroll = lockBodyScroll();
|
|
102
|
+
const t = setTimeout(() => {
|
|
72
103
|
focusFirstElement(container);
|
|
73
104
|
}, 0);
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
105
|
+
return () => {
|
|
106
|
+
clearTimeout(t);
|
|
107
|
+
unlockScroll();
|
|
108
|
+
if (focusActive) {
|
|
109
|
+
focusActive = false;
|
|
110
|
+
returnFocus();
|
|
111
|
+
}
|
|
112
|
+
};
|
|
78
113
|
}
|
|
79
114
|
});
|
|
80
115
|
|
|
116
|
+
/** Re-queries focusables on every Tab so content added while open stays inside the trap. */
|
|
117
|
+
function handleTrapKeydown(event: KeyboardEvent) {
|
|
118
|
+
const container = modalContainer;
|
|
119
|
+
if (event.key !== 'Tab' || !container) return;
|
|
120
|
+
// A nested dialog handles its own Tab cycle.
|
|
121
|
+
const owner = (event.target as Element | null)?.closest?.('[role="dialog"]');
|
|
122
|
+
if (owner && owner !== container) return;
|
|
123
|
+
|
|
124
|
+
const focusable = getFocusableElements(container);
|
|
125
|
+
if (focusable.length === 0) {
|
|
126
|
+
event.preventDefault();
|
|
127
|
+
container.focus();
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const first = focusable[0];
|
|
131
|
+
const last = focusable[focusable.length - 1];
|
|
132
|
+
const active = document.activeElement as HTMLElement | null;
|
|
133
|
+
|
|
134
|
+
if (!active || !focusable.includes(active)) {
|
|
135
|
+
event.preventDefault();
|
|
136
|
+
first.focus();
|
|
137
|
+
} else if (event.shiftKey && active === first) {
|
|
138
|
+
event.preventDefault();
|
|
139
|
+
last.focus();
|
|
140
|
+
} else if (!event.shiftKey && active === last) {
|
|
141
|
+
event.preventDefault();
|
|
142
|
+
first.focus();
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
81
146
|
function handleBackdropClick(event: Event) {
|
|
82
147
|
if (event.target === event.currentTarget) {
|
|
83
148
|
closeModal(event);
|
|
@@ -86,7 +151,9 @@
|
|
|
86
151
|
|
|
87
152
|
function handleKeydown(event: Event) {
|
|
88
153
|
const keyboardEvent = event as KeyboardEvent;
|
|
89
|
-
|
|
154
|
+
// `defaultPrevented` means a nested dialog already closed itself.
|
|
155
|
+
if (keyboardEvent.key === 'Escape' && !keyboardEvent.defaultPrevented) {
|
|
156
|
+
keyboardEvent.preventDefault();
|
|
90
157
|
closeModal(event);
|
|
91
158
|
}
|
|
92
159
|
onkeydown?.(event);
|
|
@@ -94,52 +161,64 @@
|
|
|
94
161
|
</script>
|
|
95
162
|
|
|
96
163
|
{#if isOpen}
|
|
164
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
97
165
|
<div
|
|
98
166
|
class="fixed inset-0 z-modal flex cursor-pointer items-end justify-center bg-overlay p-0 md:items-center md:p-4"
|
|
99
167
|
onclick={handleBackdropClick}
|
|
100
168
|
onkeydown={handleKeydown}
|
|
101
|
-
role="
|
|
102
|
-
aria-modal="true"
|
|
103
|
-
aria-labelledby={title ? modalTitleId : undefined}
|
|
104
|
-
aria-describedby={description ? modalDescriptionId : undefined}
|
|
105
|
-
tabindex="-1"
|
|
106
|
-
data-testid={dataTestId}
|
|
107
|
-
{...restProps}
|
|
169
|
+
role="presentation"
|
|
108
170
|
>
|
|
109
171
|
<div
|
|
110
172
|
bind:this={modalContainer}
|
|
111
|
-
class=
|
|
173
|
+
class={cn(
|
|
174
|
+
"flex max-h-[90vh] min-w-[320px] cursor-default flex-col overflow-y-auto rounded-t-overlay border border-border-overlay bg-surface-overlay p-0 shadow-lg animate-[slideUp_0.3s_ease-out] md:animate-none md:rounded-overlay",
|
|
175
|
+
sizeClasses,
|
|
176
|
+
className,
|
|
177
|
+
)}
|
|
178
|
+
role="dialog"
|
|
179
|
+
aria-modal="true"
|
|
180
|
+
aria-labelledby={title ? modalTitleId : undefined}
|
|
181
|
+
aria-describedby={description ? modalDescriptionId : undefined}
|
|
182
|
+
tabindex="-1"
|
|
183
|
+
data-testid={dataTestId}
|
|
184
|
+
onkeydown={handleTrapKeydown}
|
|
185
|
+
{...restProps}
|
|
112
186
|
>
|
|
113
|
-
|
|
114
|
-
|
|
187
|
+
<!-- The panel owns padding: Card's own p-6 would stack with the header's px-6/pt-6 and indent the title 24px past the body. -->
|
|
188
|
+
<Card variant="flat" fullWidth={false} className="bg-transparent! p-0!">
|
|
189
|
+
{#if title || description || showClose}
|
|
115
190
|
<CardHeader
|
|
116
191
|
{description}
|
|
117
192
|
descriptionId={description ? modalDescriptionId : undefined}
|
|
118
193
|
className="px-6 pt-6 pb-4"
|
|
119
194
|
>
|
|
120
|
-
{
|
|
121
|
-
|
|
195
|
+
<div class="flex items-center {title ? 'justify-between' : 'justify-end'}">
|
|
196
|
+
{#if title}
|
|
122
197
|
<h2
|
|
123
198
|
id={modalTitleId}
|
|
124
199
|
class="text-2xl font-normal leading-8 tracking-normal text-headline"
|
|
125
200
|
>
|
|
126
201
|
{title}
|
|
127
202
|
</h2>
|
|
203
|
+
{/if}
|
|
204
|
+
{#if showClose}
|
|
128
205
|
<button
|
|
129
206
|
type="button"
|
|
130
207
|
onclick={closeModal}
|
|
131
|
-
class="flex
|
|
208
|
+
class="focus-ring flex size-8 cursor-pointer items-center justify-center rounded-control text-2xl text-description transition-colors hover:bg-surface-overlay-hover hover:text-headline"
|
|
132
209
|
aria-label="Close"
|
|
133
210
|
>
|
|
134
211
|
×
|
|
135
212
|
</button>
|
|
136
|
-
|
|
137
|
-
|
|
213
|
+
{/if}
|
|
214
|
+
</div>
|
|
138
215
|
</CardHeader>
|
|
139
216
|
{/if}
|
|
140
217
|
|
|
141
218
|
{#if children}
|
|
142
|
-
<CardContent
|
|
219
|
+
<CardContent
|
|
220
|
+
className="flex-1 px-6 {title || description || showClose ? '' : 'pt-6'} {footer ? '' : 'pb-6'}"
|
|
221
|
+
>
|
|
143
222
|
{@render children?.()}
|
|
144
223
|
</CardContent>
|
|
145
224
|
{/if}
|
|
@@ -1,18 +1,21 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
1
2
|
type Size = 'sm' | 'md' | 'lg';
|
|
2
3
|
interface Props {
|
|
3
4
|
isOpen?: boolean;
|
|
4
5
|
title?: string;
|
|
5
6
|
description?: string;
|
|
6
7
|
size?: Size;
|
|
8
|
+
/** Render the close button even when there is no `title`. */
|
|
9
|
+
showClose?: boolean;
|
|
7
10
|
onclick?: (event: Event) => void;
|
|
8
11
|
onkeydown?: (event: Event) => void;
|
|
9
|
-
/** On the `role="dialog"`
|
|
12
|
+
/** On the `role="dialog"` panel (testing, analytics). */
|
|
10
13
|
"data-testid"?: string;
|
|
14
|
+
/** Extra classes for the dialog panel. */
|
|
15
|
+
class?: string;
|
|
16
|
+
children?: Snippet;
|
|
17
|
+
footer?: Snippet;
|
|
11
18
|
}
|
|
12
|
-
|
|
13
|
-
children?: any;
|
|
14
|
-
footer?: any;
|
|
15
|
-
};
|
|
16
|
-
declare const Modal: import("svelte").Component<$$ComponentProps, {}, "isOpen">;
|
|
19
|
+
declare const Modal: import("svelte").Component<Props, {}, "isOpen">;
|
|
17
20
|
type Modal = ReturnType<typeof Modal>;
|
|
18
21
|
export default Modal;
|
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
import NavigationMenuLink from "./NavigationMenuLink.svelte";
|
|
10
10
|
|
|
11
11
|
import { generateId } from "../util/ssr-safe.js";
|
|
12
|
+
|
|
13
|
+
import { cn } from "../util/cn.js";
|
|
12
14
|
import {
|
|
13
15
|
NAVIGATION_MENU_CONTEXT_KEY,
|
|
14
16
|
navigationMenuStableInstanceId,
|
|
@@ -33,24 +35,34 @@
|
|
|
33
35
|
|
|
34
36
|
interface Props {
|
|
35
37
|
viewport?: boolean | "mobile";
|
|
38
|
+
class?: string;
|
|
39
|
+
/** @deprecated use `class`. */
|
|
36
40
|
className?: string;
|
|
37
41
|
items?: NavigationMenuItemData[];
|
|
38
42
|
children?: Snippet;
|
|
39
43
|
listClassName?: string;
|
|
40
44
|
/** Panel id / `aria-controls` prefix; pass for SSR or multiple menus. If omitted with non-empty `items`, a deterministic hash is used. */
|
|
41
45
|
menuId?: string;
|
|
46
|
+
/** Accessible name for the `<nav>` landmark. */
|
|
47
|
+
ariaLabel?: string;
|
|
42
48
|
}
|
|
43
49
|
|
|
44
50
|
let {
|
|
45
51
|
viewport = true,
|
|
46
|
-
|
|
52
|
+
class: classAttr = "",
|
|
53
|
+
className: legacyClass = "",
|
|
47
54
|
items = [],
|
|
48
55
|
children,
|
|
49
56
|
listClassName = "",
|
|
50
57
|
menuId = "",
|
|
58
|
+
ariaLabel = "Main navigation",
|
|
51
59
|
...restProps
|
|
52
60
|
}: Props = $props();
|
|
53
61
|
|
|
62
|
+
/** `class` is the public prop; `className` is a deprecated alias.
|
|
63
|
+
* Both are merged here so existing call sites keep working. */
|
|
64
|
+
const className = $derived(cn(`${classAttr} ${legacyClass}`));
|
|
65
|
+
|
|
54
66
|
let activeItem = $state<string | null>(null);
|
|
55
67
|
let isMobile = $state(false);
|
|
56
68
|
let containerElement = $state<HTMLElement | null>(null);
|
|
@@ -104,8 +116,8 @@
|
|
|
104
116
|
|
|
105
117
|
<nav
|
|
106
118
|
bind:this={containerElement}
|
|
107
|
-
class="relative
|
|
108
|
-
aria-label=
|
|
119
|
+
class={cn("relative", className)}
|
|
120
|
+
aria-label={ariaLabel}
|
|
109
121
|
{...restProps}
|
|
110
122
|
>
|
|
111
123
|
{#if items.length > 0}
|
|
@@ -15,12 +15,16 @@ export interface NavigationMenuItemData {
|
|
|
15
15
|
}
|
|
16
16
|
interface Props {
|
|
17
17
|
viewport?: boolean | "mobile";
|
|
18
|
+
class?: string;
|
|
19
|
+
/** @deprecated use `class`. */
|
|
18
20
|
className?: string;
|
|
19
21
|
items?: NavigationMenuItemData[];
|
|
20
22
|
children?: Snippet;
|
|
21
23
|
listClassName?: string;
|
|
22
24
|
/** Panel id / `aria-controls` prefix; pass for SSR or multiple menus. If omitted with non-empty `items`, a deterministic hash is used. */
|
|
23
25
|
menuId?: string;
|
|
26
|
+
/** Accessible name for the `<nav>` landmark. */
|
|
27
|
+
ariaLabel?: string;
|
|
24
28
|
}
|
|
25
29
|
declare const NavigationMenu: import("svelte").Component<Props, {}, "">;
|
|
26
30
|
type NavigationMenu = ReturnType<typeof NavigationMenu>;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { Snippet } from "svelte";
|
|
3
3
|
import { getContext } from "svelte";
|
|
4
|
-
import {
|
|
4
|
+
import { cn } from "../util/cn.js";
|
|
5
5
|
import {
|
|
6
6
|
NAVIGATION_MENU_CONTEXT_KEY,
|
|
7
7
|
navigationMenuPanelId,
|
|
@@ -10,19 +10,25 @@
|
|
|
10
10
|
|
|
11
11
|
interface Props {
|
|
12
12
|
value?: string;
|
|
13
|
+
class?: string;
|
|
14
|
+
/** @deprecated use `class`. */
|
|
13
15
|
className?: string;
|
|
14
16
|
children?: Snippet;
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
let {
|
|
18
20
|
value = "",
|
|
19
|
-
|
|
21
|
+
class: classAttr = "",
|
|
22
|
+
className: legacyClass = "",
|
|
20
23
|
children,
|
|
21
24
|
...restProps
|
|
22
25
|
}: Props = $props();
|
|
23
26
|
|
|
27
|
+
/** `class` is the public prop; `className` is a deprecated alias.
|
|
28
|
+
* Both are merged here so existing call sites keep working. */
|
|
29
|
+
const className = $derived(cn(`${classAttr} ${legacyClass}`));
|
|
30
|
+
|
|
24
31
|
let contentElement = $state<HTMLElement | null>(null);
|
|
25
|
-
let parentItem = $state<HTMLElement | null>(null);
|
|
26
32
|
|
|
27
33
|
const context = getContext<NavigationMenuContextValue>(
|
|
28
34
|
NAVIGATION_MENU_CONTEXT_KEY,
|
|
@@ -36,42 +42,37 @@
|
|
|
36
42
|
: "",
|
|
37
43
|
);
|
|
38
44
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const trigger = parentItem?.querySelector('[data-navigation-menu-trigger]') as HTMLElement;
|
|
44
|
-
trigger?.focus();
|
|
45
|
-
}
|
|
46
|
-
}
|
|
45
|
+
/** Disclosure panel: one document listener while open, removed on close/unmount. */
|
|
46
|
+
$effect(() => {
|
|
47
|
+
if (!isActive || !contentElement) return;
|
|
48
|
+
const panel: HTMLElement = contentElement;
|
|
47
49
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
!contentElement.contains(event.target as Node) &&
|
|
52
|
-
parentItem &&
|
|
53
|
-
!parentItem.contains(event.target as Node)
|
|
54
|
-
) {
|
|
55
|
-
context?.setActiveItem(null);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
50
|
+
const parentItem = panel.closest<HTMLElement>(
|
|
51
|
+
"[data-navigation-menu-item]",
|
|
52
|
+
);
|
|
58
53
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
document.addEventListener("mousedown", handleClickOutside);
|
|
54
|
+
function handleClickOutside(event: MouseEvent) {
|
|
55
|
+
const target = event.target as Node;
|
|
56
|
+
if (!panel.contains(target) && !parentItem?.contains(target)) {
|
|
57
|
+
context?.setActiveItem(null);
|
|
58
|
+
}
|
|
65
59
|
}
|
|
66
|
-
});
|
|
67
60
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
61
|
+
function handleKeydown(event: KeyboardEvent) {
|
|
62
|
+
if (event.key !== "Escape") return;
|
|
63
|
+
event.preventDefault();
|
|
64
|
+
context?.setActiveItem(null);
|
|
65
|
+
parentItem
|
|
66
|
+
?.querySelector<HTMLElement>("[data-navigation-menu-trigger]")
|
|
67
|
+
?.focus();
|
|
74
68
|
}
|
|
69
|
+
|
|
70
|
+
document.addEventListener("mousedown", handleClickOutside);
|
|
71
|
+
panel.addEventListener("keydown", handleKeydown);
|
|
72
|
+
return () => {
|
|
73
|
+
document.removeEventListener("mousedown", handleClickOutside);
|
|
74
|
+
panel.removeEventListener("keydown", handleKeydown);
|
|
75
|
+
};
|
|
75
76
|
});
|
|
76
77
|
</script>
|
|
77
78
|
|
|
@@ -79,11 +80,8 @@
|
|
|
79
80
|
<div
|
|
80
81
|
bind:this={contentElement}
|
|
81
82
|
id={panelId || undefined}
|
|
82
|
-
class="absolute left-0 top-full mt-2 bg-
|
|
83
|
-
role="menu"
|
|
84
|
-
onkeydown={handleKeydown}
|
|
83
|
+
class={cn("absolute left-0 top-full mt-2 bg-surface-overlay rounded-control shadow-lg border border-border p-4 z-dropdown min-w-[200px] transition-all duration-200 ease-in-out", className)}
|
|
85
84
|
data-navigation-menu-content
|
|
86
|
-
tabindex="-1"
|
|
87
85
|
{...restProps}
|
|
88
86
|
>
|
|
89
87
|
{@render children?.()}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { Snippet } from "svelte";
|
|
3
3
|
import { getContext } from "svelte";
|
|
4
|
+
import { cn } from "../util/cn.js";
|
|
4
5
|
import {
|
|
5
6
|
NAVIGATION_MENU_CONTEXT_KEY,
|
|
6
7
|
type NavigationMenuContextValue,
|
|
@@ -8,17 +9,24 @@
|
|
|
8
9
|
|
|
9
10
|
interface Props {
|
|
10
11
|
value?: string;
|
|
12
|
+
class?: string;
|
|
13
|
+
/** @deprecated use `class`. */
|
|
11
14
|
className?: string;
|
|
12
15
|
children?: Snippet;
|
|
13
16
|
}
|
|
14
17
|
|
|
15
18
|
let {
|
|
16
19
|
value = "",
|
|
17
|
-
|
|
20
|
+
class: classAttr = "",
|
|
21
|
+
className: legacyClass = "",
|
|
18
22
|
children,
|
|
19
23
|
...restProps
|
|
20
24
|
}: Props = $props();
|
|
21
25
|
|
|
26
|
+
/** `class` is the public prop; `className` is a deprecated alias.
|
|
27
|
+
* Both are merged here so existing call sites keep working. */
|
|
28
|
+
const className = $derived(cn(`${classAttr} ${legacyClass}`));
|
|
29
|
+
|
|
22
30
|
const context = getContext<NavigationMenuContextValue>(
|
|
23
31
|
NAVIGATION_MENU_CONTEXT_KEY,
|
|
24
32
|
);
|
|
@@ -27,7 +35,7 @@
|
|
|
27
35
|
</script>
|
|
28
36
|
|
|
29
37
|
<li
|
|
30
|
-
class="relative
|
|
38
|
+
class={cn("relative", className)}
|
|
31
39
|
role="none"
|
|
32
40
|
data-navigation-menu-item
|
|
33
41
|
data-active={isActive}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { Snippet } from "svelte";
|
|
3
3
|
import { getContext } from "svelte";
|
|
4
|
+
import { cn } from "../util/cn.js";
|
|
4
5
|
import {
|
|
5
6
|
NAVIGATION_MENU_CONTEXT_KEY,
|
|
6
7
|
type NavigationMenuContextValue,
|
|
@@ -9,6 +10,8 @@
|
|
|
9
10
|
interface Props {
|
|
10
11
|
href?: string;
|
|
11
12
|
asChild?: boolean;
|
|
13
|
+
class?: string;
|
|
14
|
+
/** @deprecated use `class`. */
|
|
12
15
|
className?: string;
|
|
13
16
|
children?: Snippet;
|
|
14
17
|
}
|
|
@@ -16,11 +19,16 @@
|
|
|
16
19
|
let {
|
|
17
20
|
href = "#",
|
|
18
21
|
asChild = false,
|
|
19
|
-
|
|
22
|
+
class: classAttr = "",
|
|
23
|
+
className: legacyClass = "",
|
|
20
24
|
children,
|
|
21
25
|
...restProps
|
|
22
26
|
}: Props = $props();
|
|
23
27
|
|
|
28
|
+
/** `class` is the public prop; `className` is a deprecated alias.
|
|
29
|
+
* Both are merged here so existing call sites keep working. */
|
|
30
|
+
const className = $derived(cn(`${classAttr} ${legacyClass}`));
|
|
31
|
+
|
|
24
32
|
const context = getContext<NavigationMenuContextValue>(
|
|
25
33
|
NAVIGATION_MENU_CONTEXT_KEY,
|
|
26
34
|
);
|
|
@@ -30,7 +38,7 @@
|
|
|
30
38
|
}
|
|
31
39
|
|
|
32
40
|
const linkClasses =
|
|
33
|
-
"focus-ring focus-ring--nav block cursor-pointer rounded-
|
|
41
|
+
"focus-ring focus-ring--nav block cursor-pointer rounded-control px-4 py-2 text-sm text-nav-menu-item no-underline transition-colors duration-150 outline-none hover:bg-nav-menu-hover hover:text-nav-menu-item-hover {className}";
|
|
34
42
|
</script>
|
|
35
43
|
|
|
36
44
|
{#if asChild}
|
|
@@ -1,20 +1,28 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { Snippet } from "svelte";
|
|
3
|
+
import { cn } from "../util/cn.js";
|
|
3
4
|
|
|
4
5
|
interface Props {
|
|
6
|
+
class?: string;
|
|
7
|
+
/** @deprecated use `class`. */
|
|
5
8
|
className?: string;
|
|
6
9
|
children?: Snippet;
|
|
7
10
|
}
|
|
8
11
|
|
|
9
12
|
let {
|
|
10
|
-
|
|
13
|
+
class: classAttr = "",
|
|
14
|
+
className: legacyClass = "",
|
|
11
15
|
children,
|
|
12
16
|
...restProps
|
|
13
17
|
}: Props = $props();
|
|
18
|
+
|
|
19
|
+
/** `class` is the public prop; `className` is a deprecated alias.
|
|
20
|
+
* Both are merged here so existing call sites keep working. */
|
|
21
|
+
const className = $derived(cn(`${classAttr} ${legacyClass}`));
|
|
14
22
|
</script>
|
|
15
23
|
|
|
16
24
|
<ul
|
|
17
|
-
class="flex flex-row items-center gap-1 list-none m-0 p-0
|
|
25
|
+
class={cn("flex flex-row items-center gap-1 list-none m-0 p-0", className)}
|
|
18
26
|
role="menubar"
|
|
19
27
|
aria-orientation="horizontal"
|
|
20
28
|
data-navigation-menu-list
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { Snippet } from "svelte";
|
|
3
3
|
import { getContext } from "svelte";
|
|
4
|
+
import { cn } from "../util/cn.js";
|
|
4
5
|
import {
|
|
5
6
|
NAVIGATION_MENU_CONTEXT_KEY,
|
|
6
7
|
navigationMenuPanelId,
|
|
@@ -9,17 +10,24 @@
|
|
|
9
10
|
|
|
10
11
|
interface Props {
|
|
11
12
|
value?: string;
|
|
13
|
+
class?: string;
|
|
14
|
+
/** @deprecated use `class`. */
|
|
12
15
|
className?: string;
|
|
13
16
|
children?: Snippet;
|
|
14
17
|
}
|
|
15
18
|
|
|
16
19
|
let {
|
|
17
20
|
value = "",
|
|
18
|
-
|
|
21
|
+
class: classAttr = "",
|
|
22
|
+
className: legacyClass = "",
|
|
19
23
|
children,
|
|
20
24
|
...restProps
|
|
21
25
|
}: Props = $props();
|
|
22
26
|
|
|
27
|
+
/** `class` is the public prop; `className` is a deprecated alias.
|
|
28
|
+
* Both are merged here so existing call sites keep working. */
|
|
29
|
+
const className = $derived(cn(`${classAttr} ${legacyClass}`));
|
|
30
|
+
|
|
23
31
|
const context = getContext<NavigationMenuContextValue>(
|
|
24
32
|
NAVIGATION_MENU_CONTEXT_KEY,
|
|
25
33
|
);
|
|
@@ -58,10 +66,15 @@
|
|
|
58
66
|
|
|
59
67
|
<button
|
|
60
68
|
type="button"
|
|
61
|
-
class=
|
|
69
|
+
class={cn(
|
|
70
|
+
"focus-ring focus-ring--nav inline-flex cursor-pointer items-center justify-center gap-2 rounded-control bg-transparent px-4 py-2 text-sm font-medium transition-colors duration-150 outline-none",
|
|
71
|
+
isActive
|
|
72
|
+
? "bg-nav-menu-active text-nav-menu-item-active"
|
|
73
|
+
: "text-nav-menu-item hover:bg-nav-menu-hover hover:text-nav-menu-item-hover",
|
|
74
|
+
className,
|
|
75
|
+
)}
|
|
62
76
|
aria-expanded={isActive ? 'true' : 'false'}
|
|
63
77
|
aria-controls={panelId || undefined}
|
|
64
|
-
aria-haspopup="true"
|
|
65
78
|
onclick={handleClick}
|
|
66
79
|
onkeydown={handleKeydown}
|
|
67
80
|
data-navigation-menu-trigger
|