zabi-components 7.0.1 → 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 +89 -57
- 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 +3 -437
- package/dist/zabi-components-theme-dark-only.css +223 -103
- package/dist/zabi-components-theme-dark.css +223 -103
- package/dist/zabi-components-theme-only.css +347 -107
- package/dist/zabi-components-theme.css +347 -107
- package/dist/zabi-components.css +953 -843
- package/docs/lucide-icons.md +3 -1
- package/package.json +20 -25
- 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,40 +1,66 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
2
3
|
import {
|
|
3
4
|
focusFirstElement,
|
|
5
|
+
getFocusableElements,
|
|
4
6
|
returnFocus,
|
|
5
7
|
saveFocus,
|
|
6
|
-
trapFocus,
|
|
7
8
|
} from '../util/focus-utils.js';
|
|
8
9
|
import { generateId } from "../util/ssr-safe.js";
|
|
10
|
+
import { cn } from "../util/cn.js";
|
|
9
11
|
|
|
10
12
|
interface Props {
|
|
13
|
+
/** Extra classes for the host element. */
|
|
14
|
+
class?: string;
|
|
11
15
|
isOpen?: boolean;
|
|
12
16
|
title?: string;
|
|
13
17
|
onclick?: (event: Event) => void;
|
|
14
18
|
onkeydown?: (event: Event) => void;
|
|
19
|
+
children?: Snippet;
|
|
15
20
|
}
|
|
16
21
|
|
|
17
22
|
let {
|
|
23
|
+
class: className = "",
|
|
18
24
|
isOpen = $bindable(false),
|
|
19
25
|
title = '',
|
|
20
26
|
onclick,
|
|
21
27
|
onkeydown,
|
|
22
28
|
children,
|
|
23
29
|
...restProps
|
|
24
|
-
} = $props
|
|
30
|
+
}: Props = $props();
|
|
25
31
|
|
|
26
32
|
const slideTitleId = generateId('slideup-title');
|
|
27
33
|
|
|
28
34
|
let slideUpContainer = $state<HTMLDivElement>();
|
|
29
|
-
let
|
|
35
|
+
let focusActive = false;
|
|
36
|
+
|
|
37
|
+
/** Ref-counted on `<body>`; shares the counter with Modal so nested overlays unlock once. */
|
|
38
|
+
function lockBodyScroll(): () => void {
|
|
39
|
+
const body = document.body;
|
|
40
|
+
const count = Number(body.dataset.zabiScrollLock ?? '0');
|
|
41
|
+
if (count === 0) {
|
|
42
|
+
body.dataset.zabiScrollLockOverflow = body.style.overflow;
|
|
43
|
+
body.style.overflow = 'hidden';
|
|
44
|
+
}
|
|
45
|
+
body.dataset.zabiScrollLock = String(count + 1);
|
|
46
|
+
return () => {
|
|
47
|
+
const next = Number(body.dataset.zabiScrollLock ?? '1') - 1;
|
|
48
|
+
if (next <= 0) {
|
|
49
|
+
body.style.overflow = body.dataset.zabiScrollLockOverflow ?? '';
|
|
50
|
+
delete body.dataset.zabiScrollLock;
|
|
51
|
+
delete body.dataset.zabiScrollLockOverflow;
|
|
52
|
+
} else {
|
|
53
|
+
body.dataset.zabiScrollLock = String(next);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
}
|
|
30
57
|
|
|
31
58
|
function closeSlideUp(event?: Event) {
|
|
32
59
|
isOpen = false;
|
|
33
|
-
if (
|
|
34
|
-
|
|
35
|
-
|
|
60
|
+
if (focusActive) {
|
|
61
|
+
focusActive = false;
|
|
62
|
+
returnFocus();
|
|
36
63
|
}
|
|
37
|
-
returnFocus();
|
|
38
64
|
if (onclick && event) {
|
|
39
65
|
onclick(event);
|
|
40
66
|
}
|
|
@@ -44,17 +70,51 @@
|
|
|
44
70
|
const container = slideUpContainer;
|
|
45
71
|
if (isOpen && container) {
|
|
46
72
|
saveFocus();
|
|
47
|
-
|
|
48
|
-
|
|
73
|
+
focusActive = true;
|
|
74
|
+
const unlockScroll = lockBodyScroll();
|
|
75
|
+
const t = setTimeout(() => {
|
|
49
76
|
focusFirstElement(container);
|
|
50
77
|
}, 0);
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
78
|
+
return () => {
|
|
79
|
+
clearTimeout(t);
|
|
80
|
+
unlockScroll();
|
|
81
|
+
if (focusActive) {
|
|
82
|
+
focusActive = false;
|
|
83
|
+
returnFocus();
|
|
84
|
+
}
|
|
85
|
+
};
|
|
55
86
|
}
|
|
56
87
|
});
|
|
57
88
|
|
|
89
|
+
/** Re-queries focusables on every Tab so late-rendered content stays inside the trap. */
|
|
90
|
+
function handleTrapKeydown(event: KeyboardEvent) {
|
|
91
|
+
const container = slideUpContainer;
|
|
92
|
+
if (event.key !== 'Tab' || !container) return;
|
|
93
|
+
const owner = (event.target as Element | null)?.closest?.('[role="dialog"]');
|
|
94
|
+
if (owner && owner !== container) return;
|
|
95
|
+
|
|
96
|
+
const focusable = getFocusableElements(container);
|
|
97
|
+
if (focusable.length === 0) {
|
|
98
|
+
event.preventDefault();
|
|
99
|
+
container.focus();
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const first = focusable[0];
|
|
103
|
+
const last = focusable[focusable.length - 1];
|
|
104
|
+
const active = document.activeElement as HTMLElement | null;
|
|
105
|
+
|
|
106
|
+
if (!active || !focusable.includes(active)) {
|
|
107
|
+
event.preventDefault();
|
|
108
|
+
first.focus();
|
|
109
|
+
} else if (event.shiftKey && active === first) {
|
|
110
|
+
event.preventDefault();
|
|
111
|
+
last.focus();
|
|
112
|
+
} else if (!event.shiftKey && active === last) {
|
|
113
|
+
event.preventDefault();
|
|
114
|
+
first.focus();
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
58
118
|
function handleBackdropClick(event: Event) {
|
|
59
119
|
if (event.target === event.currentTarget) {
|
|
60
120
|
closeSlideUp(event);
|
|
@@ -63,7 +123,8 @@
|
|
|
63
123
|
|
|
64
124
|
function handleKeydown(event: Event) {
|
|
65
125
|
const keyboardEvent = event as KeyboardEvent;
|
|
66
|
-
if (keyboardEvent.key === 'Escape') {
|
|
126
|
+
if (keyboardEvent.key === 'Escape' && !keyboardEvent.defaultPrevented) {
|
|
127
|
+
keyboardEvent.preventDefault();
|
|
67
128
|
closeSlideUp(event);
|
|
68
129
|
}
|
|
69
130
|
onkeydown?.(event);
|
|
@@ -71,19 +132,22 @@
|
|
|
71
132
|
</script>
|
|
72
133
|
|
|
73
134
|
{#if isOpen}
|
|
135
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
74
136
|
<div
|
|
75
137
|
class="fixed inset-0 z-modal cursor-pointer bg-overlay"
|
|
76
138
|
onclick={handleBackdropClick}
|
|
77
139
|
onkeydown={handleKeydown}
|
|
78
|
-
role="
|
|
79
|
-
aria-modal="true"
|
|
80
|
-
aria-labelledby={title ? slideTitleId : undefined}
|
|
81
|
-
tabindex="-1"
|
|
82
|
-
{...restProps}
|
|
140
|
+
role="presentation"
|
|
83
141
|
>
|
|
84
142
|
<div
|
|
85
143
|
bind:this={slideUpContainer}
|
|
86
|
-
class="fixed bottom-0 left-0 right-0 z-modal flex max-h-[90vh] cursor-default flex-col overflow-y-auto rounded-t-
|
|
144
|
+
class={cn("fixed bottom-0 left-0 right-0 z-modal flex max-h-[90vh] cursor-default flex-col overflow-y-auto rounded-t-overlay border-t border-border-overlay bg-surface-overlay shadow-lg animate-[slideUp_0.3s_ease-out]", className)}
|
|
145
|
+
role="dialog"
|
|
146
|
+
aria-modal="true"
|
|
147
|
+
aria-labelledby={title ? slideTitleId : undefined}
|
|
148
|
+
tabindex="-1"
|
|
149
|
+
onkeydown={handleTrapKeydown}
|
|
150
|
+
{...restProps}
|
|
87
151
|
>
|
|
88
152
|
{#if title}
|
|
89
153
|
<div class="flex items-center justify-between px-6 pb-4 pt-6">
|
|
@@ -96,7 +160,7 @@
|
|
|
96
160
|
<button
|
|
97
161
|
type="button"
|
|
98
162
|
onclick={closeSlideUp}
|
|
99
|
-
class="flex h-8 w-8 cursor-pointer items-center justify-center rounded-full text-2xl text-description transition-colors hover:bg-
|
|
163
|
+
class="focus-ring flex h-8 w-8 cursor-pointer items-center justify-center rounded-full text-2xl text-description transition-colors hover:bg-surface-overlay-hover hover:text-headline"
|
|
100
164
|
aria-label="Close"
|
|
101
165
|
>
|
|
102
166
|
×
|
|
@@ -1,12 +1,13 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
1
2
|
interface Props {
|
|
3
|
+
/** Extra classes for the host element. */
|
|
4
|
+
class?: string;
|
|
2
5
|
isOpen?: boolean;
|
|
3
6
|
title?: string;
|
|
4
7
|
onclick?: (event: Event) => void;
|
|
5
8
|
onkeydown?: (event: Event) => void;
|
|
9
|
+
children?: Snippet;
|
|
6
10
|
}
|
|
7
|
-
|
|
8
|
-
children?: any;
|
|
9
|
-
};
|
|
10
|
-
declare const SlideUp: import("svelte").Component<$$ComponentProps, {}, "isOpen">;
|
|
11
|
+
declare const SlideUp: import("svelte").Component<Props, {}, "isOpen">;
|
|
11
12
|
type SlideUp = ReturnType<typeof SlideUp>;
|
|
12
13
|
export default SlideUp;
|
|
@@ -1,40 +1,49 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import type { Snippet } from "svelte";
|
|
2
3
|
import { generateId } from "../util/ssr-safe.js";
|
|
4
|
+
import { cn } from "../util/cn.js";
|
|
3
5
|
|
|
4
6
|
interface Props {
|
|
7
|
+
/** Extra classes for the host element. */
|
|
8
|
+
class?: string;
|
|
5
9
|
tabs?: Array<{
|
|
6
10
|
id: string;
|
|
7
11
|
label: string;
|
|
8
12
|
disabled?: boolean;
|
|
9
13
|
}>;
|
|
14
|
+
/** Selected tab id; supports `bind:activeTab`. */
|
|
10
15
|
activeTab?: string;
|
|
11
16
|
variant?: "default" | "pills";
|
|
12
17
|
onclick?: (event: Event) => void;
|
|
13
18
|
onkeydown?: (event: Event) => void;
|
|
19
|
+
children?: Snippet<[{ activeTab: string }]>;
|
|
14
20
|
}
|
|
15
21
|
|
|
16
22
|
let {
|
|
23
|
+
class: className = "",
|
|
17
24
|
tabs = [],
|
|
18
|
-
activeTab = "",
|
|
25
|
+
activeTab = $bindable(""),
|
|
19
26
|
variant = "default",
|
|
20
27
|
children,
|
|
21
28
|
...restProps
|
|
22
|
-
}: Props
|
|
29
|
+
}: Props = $props();
|
|
23
30
|
|
|
24
31
|
const tabsBaseId = generateId("tabs");
|
|
25
32
|
|
|
26
|
-
|
|
33
|
+
/** Button refs keyed by tab id — roving tabindex needs focus to follow selection. */
|
|
34
|
+
const tabElements: Record<string, HTMLButtonElement | undefined> = {};
|
|
35
|
+
|
|
36
|
+
function selectTab(tabId: string, moveFocus = false) {
|
|
27
37
|
activeTab = tabId;
|
|
38
|
+
if (moveFocus) {
|
|
39
|
+
tabElements[tabId]?.focus();
|
|
40
|
+
}
|
|
28
41
|
}
|
|
29
42
|
|
|
30
43
|
function getEnabledTabs() {
|
|
31
44
|
return tabs.filter((tab) => !tab.disabled);
|
|
32
45
|
}
|
|
33
46
|
|
|
34
|
-
function getTabIndex(tabId: string) {
|
|
35
|
-
return tabs.findIndex((tab) => tab.id === tabId);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
47
|
function getTabId(tabId: string) {
|
|
39
48
|
return `${tabsBaseId}-tab-${tabId}`;
|
|
40
49
|
}
|
|
@@ -72,40 +81,37 @@
|
|
|
72
81
|
const nextIndex =
|
|
73
82
|
(fallbackIndex + direction + enabledTabs.length) %
|
|
74
83
|
enabledTabs.length;
|
|
75
|
-
selectTab(enabledTabs[nextIndex].id);
|
|
84
|
+
selectTab(enabledTabs[nextIndex].id, true);
|
|
76
85
|
} else if (event.key === "Home") {
|
|
77
86
|
event.preventDefault();
|
|
78
|
-
selectTab(enabledTabs[0].id);
|
|
87
|
+
selectTab(enabledTabs[0].id, true);
|
|
79
88
|
} else if (event.key === "End") {
|
|
80
89
|
event.preventDefault();
|
|
81
|
-
selectTab(enabledTabs[enabledTabs.length - 1].id);
|
|
82
|
-
} else if (event.key === "Enter" || event.key === " ") {
|
|
83
|
-
event.preventDefault();
|
|
84
|
-
if (currentIndex === -1) {
|
|
85
|
-
selectTab(enabledTabs[0].id);
|
|
86
|
-
}
|
|
90
|
+
selectTab(enabledTabs[enabledTabs.length - 1].id, true);
|
|
87
91
|
}
|
|
92
|
+
// Enter/Space: native <button> click already selects the tab.
|
|
88
93
|
}
|
|
89
94
|
</script>
|
|
90
95
|
|
|
91
|
-
<div class="tabs-container">
|
|
96
|
+
<div class={cn("tabs-container", className)}>
|
|
92
97
|
<div
|
|
93
98
|
class="flex border-b border-base-200"
|
|
94
99
|
role="tablist"
|
|
95
|
-
tabindex="
|
|
100
|
+
tabindex="-1"
|
|
96
101
|
onkeydown={handleKeydown}
|
|
97
102
|
>
|
|
98
103
|
{#each tabs as tab (tab.id)}
|
|
99
104
|
<button
|
|
105
|
+
bind:this={tabElements[tab.id]}
|
|
100
106
|
type="button"
|
|
101
107
|
role="tab"
|
|
102
108
|
id={getTabId(tab.id)}
|
|
103
|
-
class="focus-ring cursor-pointer border-b-2 px-4 py-2 text-sm font-medium transition-colors focus:outline-none focus-visible:outline-none active:bg-
|
|
109
|
+
class="focus-ring cursor-pointer border-b-2 px-4 py-2 text-sm font-medium transition-colors focus:outline-none focus-visible:outline-none active:bg-surface-active disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:border-transparent disabled:hover:text-description {activeTab ===
|
|
104
110
|
tab.id
|
|
105
111
|
? variant === 'pills'
|
|
106
112
|
? 'border-brand-500 bg-brand-100 text-brand-700'
|
|
107
113
|
: 'border-brand-500 text-body'
|
|
108
|
-
: 'border-transparent text-description hover:border-
|
|
114
|
+
: 'border-transparent text-description hover:border-border-medium hover:text-body'}"
|
|
109
115
|
onclick={() => selectTab(tab.id)}
|
|
110
116
|
disabled={tab.disabled}
|
|
111
117
|
aria-selected={activeTab === tab.id}
|
|
@@ -1,17 +1,21 @@
|
|
|
1
|
+
import type { Snippet } from "svelte";
|
|
1
2
|
interface Props {
|
|
3
|
+
/** Extra classes for the host element. */
|
|
4
|
+
class?: string;
|
|
2
5
|
tabs?: Array<{
|
|
3
6
|
id: string;
|
|
4
7
|
label: string;
|
|
5
8
|
disabled?: boolean;
|
|
6
9
|
}>;
|
|
10
|
+
/** Selected tab id; supports `bind:activeTab`. */
|
|
7
11
|
activeTab?: string;
|
|
8
12
|
variant?: "default" | "pills";
|
|
9
13
|
onclick?: (event: Event) => void;
|
|
10
14
|
onkeydown?: (event: Event) => void;
|
|
15
|
+
children?: Snippet<[{
|
|
16
|
+
activeTab: string;
|
|
17
|
+
}]>;
|
|
11
18
|
}
|
|
12
|
-
|
|
13
|
-
children?: any;
|
|
14
|
-
};
|
|
15
|
-
declare const Tabs: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
19
|
+
declare const Tabs: import("svelte").Component<Props, {}, "activeTab">;
|
|
16
20
|
type Tabs = ReturnType<typeof Tabs>;
|
|
17
21
|
export default Tabs;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import ToasterToast from './ToasterToast.svelte';
|
|
3
3
|
import { toastStore } from './toast-store.js';
|
|
4
|
+
import { cn } from "../util/cn.js";
|
|
4
5
|
|
|
5
6
|
interface Props {
|
|
6
7
|
class?: string;
|
|
@@ -10,11 +11,11 @@
|
|
|
10
11
|
</script>
|
|
11
12
|
|
|
12
13
|
<div
|
|
13
|
-
class="pointer-events-none fixed bottom-4 right-4 z-toast flex min-w-0 w-[min(24rem,calc(100vw-2rem))] flex-col gap-2 p-4
|
|
14
|
+
class={cn("pointer-events-none fixed bottom-4 right-4 z-toast flex min-w-0 w-[min(24rem,calc(100vw-2rem))] flex-col gap-2 p-4", className)}
|
|
14
15
|
role="region"
|
|
15
16
|
aria-label="Notifications"
|
|
16
|
-
aria-live="polite"
|
|
17
17
|
>
|
|
18
|
+
<!-- Each toast owns its role="status"/"alert"; a region-level aria-live would announce twice. -->
|
|
18
19
|
{#each $toastStore as toast (toast.id)}
|
|
19
20
|
<ToasterToast {toast} />
|
|
20
21
|
{/each}
|
|
@@ -12,12 +12,15 @@
|
|
|
12
12
|
} from '@lucide/svelte';
|
|
13
13
|
import Button from '../atoms/Button.svelte';
|
|
14
14
|
import { toastStore, type ToastItem } from './toast-store.js';
|
|
15
|
+
import { cn } from "../util/cn.js";
|
|
15
16
|
|
|
16
17
|
interface Props {
|
|
18
|
+
/** Extra classes for the host element. */
|
|
19
|
+
class?: string;
|
|
17
20
|
toast: ToastItem;
|
|
18
21
|
}
|
|
19
22
|
|
|
20
|
-
let { toast }: Props = $props();
|
|
23
|
+
let { class: className = "", toast }: Props = $props();
|
|
21
24
|
|
|
22
25
|
/** Seconds until auto-dismiss; `0` means no countdown (manual dismiss only). */
|
|
23
26
|
function autoDismissSeconds(duration: number | undefined): number {
|
|
@@ -31,6 +34,10 @@
|
|
|
31
34
|
let count = $state(0);
|
|
32
35
|
let timerActive = $state(false);
|
|
33
36
|
let isExpanded = $state(false);
|
|
37
|
+
/** Hover or keyboard focus inside the toast pauses auto-dismiss (WCAG 2.2.1). */
|
|
38
|
+
let hovered = $state(false);
|
|
39
|
+
let focusWithin = $state(false);
|
|
40
|
+
const paused = $derived(hovered || focusWithin);
|
|
34
41
|
|
|
35
42
|
let intervalRef: ReturnType<typeof setInterval> | undefined;
|
|
36
43
|
|
|
@@ -90,6 +97,7 @@
|
|
|
90
97
|
timerActive = true;
|
|
91
98
|
|
|
92
99
|
intervalRef = setInterval(() => {
|
|
100
|
+
if (paused) return;
|
|
93
101
|
count -= 1;
|
|
94
102
|
if (count <= 0) {
|
|
95
103
|
stopTimer();
|
|
@@ -113,19 +121,38 @@
|
|
|
113
121
|
isExpanded = false;
|
|
114
122
|
}
|
|
115
123
|
|
|
124
|
+
function handleFocusOut(event: FocusEvent) {
|
|
125
|
+
const next = event.relatedTarget as Node | null;
|
|
126
|
+
if (!next || !(event.currentTarget as HTMLElement).contains(next)) {
|
|
127
|
+
focusWithin = false;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
116
131
|
const toastEnter = { y: 18, duration: 260, opacity: 0, easing: cubicOut };
|
|
117
132
|
const toastLeave = { y: 14, duration: 300, opacity: 0, easing: cubicOut };
|
|
118
133
|
</script>
|
|
119
134
|
|
|
120
135
|
<div
|
|
121
|
-
class="pointer-events-auto relative w-full min-w-[18rem] overflow-hidden rounded-
|
|
136
|
+
class={cn("pointer-events-auto relative w-full min-w-[18rem] overflow-hidden rounded-overlay border border-border-overlay bg-surface-overlay shadow-lg", className)}
|
|
122
137
|
in:fly={toastEnter}
|
|
123
138
|
out:fly={toastLeave}
|
|
124
|
-
|
|
139
|
+
onmouseenter={() => (hovered = true)}
|
|
140
|
+
onmouseleave={() => (hovered = false)}
|
|
141
|
+
onfocusin={() => (focusWithin = true)}
|
|
142
|
+
onfocusout={handleFocusOut}
|
|
143
|
+
role="group"
|
|
144
|
+
aria-label={headerTitle}
|
|
145
|
+
data-paused={paused}
|
|
125
146
|
>
|
|
126
147
|
<div class="p-4">
|
|
127
148
|
<div class="flex items-start gap-3">
|
|
128
|
-
|
|
149
|
+
<!-- Live region holds only title + message, so the countdown and buttons are never re-announced. -->
|
|
150
|
+
<div
|
|
151
|
+
class="flex min-w-0 flex-1 items-start gap-3"
|
|
152
|
+
role={toast.type === 'error' ? 'alert' : 'status'}
|
|
153
|
+
aria-atomic="true"
|
|
154
|
+
>
|
|
155
|
+
<div class="shrink-0 mt-0.5">
|
|
129
156
|
{#if toast.type === 'success'}
|
|
130
157
|
<CheckCircle class="size-5 {statusIconClass}" aria-hidden="true" />
|
|
131
158
|
{:else if toast.type === 'error'}
|
|
@@ -138,12 +165,16 @@
|
|
|
138
165
|
</div>
|
|
139
166
|
<h4 class="min-w-0 flex-1 text-base font-semibold text-headline">
|
|
140
167
|
{headerTitle}
|
|
168
|
+
{#if toast.message.trim() && toast.message.trim() !== headerTitle}
|
|
169
|
+
<span class="sr-only">{toast.message}</span>
|
|
170
|
+
{/if}
|
|
141
171
|
</h4>
|
|
142
|
-
|
|
172
|
+
</div>
|
|
173
|
+
<div class="flex shrink-0 items-center gap-1">
|
|
143
174
|
{#if hasExpandable}
|
|
144
175
|
<button
|
|
145
176
|
type="button"
|
|
146
|
-
class="focus-ring cursor-pointer rounded-
|
|
177
|
+
class="focus-ring cursor-pointer rounded-control p-1 text-description transition-colors hover:bg-surface-overlay-hover hover:text-headline focus:outline-none"
|
|
147
178
|
aria-expanded={isExpanded}
|
|
148
179
|
aria-controls="toaster-expand-{toast.id}"
|
|
149
180
|
aria-label={isExpanded ? 'Collapse details' : 'Expand details'}
|
|
@@ -159,7 +190,7 @@
|
|
|
159
190
|
{/if}
|
|
160
191
|
<button
|
|
161
192
|
type="button"
|
|
162
|
-
class="focus-ring cursor-pointer rounded-
|
|
193
|
+
class="focus-ring cursor-pointer rounded-control p-1 text-description transition-colors hover:bg-surface-overlay-hover hover:text-headline focus:outline-none"
|
|
163
194
|
onclick={handleDismiss}
|
|
164
195
|
aria-label="Dismiss notification"
|
|
165
196
|
>
|
|
@@ -189,11 +220,11 @@
|
|
|
189
220
|
|
|
190
221
|
{#if timerActive}
|
|
191
222
|
<div class="px-4 pb-3" transition:fade={{ duration: 180 }}>
|
|
192
|
-
<p class="text-xs text-description">
|
|
193
|
-
This message will close in {count} seconds.
|
|
223
|
+
<p class="text-xs text-description" data-toast-countdown>
|
|
224
|
+
{paused ? 'Paused — closes' : 'This message will close'} in {count} seconds.
|
|
194
225
|
<button
|
|
195
226
|
type="button"
|
|
196
|
-
class="focus-ring cursor-pointer rounded-
|
|
227
|
+
class="focus-ring cursor-pointer rounded-control text-link underline-offset-2 hover:underline focus:outline-none"
|
|
197
228
|
onclick={stopTimer}
|
|
198
229
|
>
|
|
199
230
|
Click to stop
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
import { LogOut, Moon, Sun, User } from "@lucide/svelte";
|
|
7
7
|
|
|
8
8
|
interface Props {
|
|
9
|
+
/** Extra classes for the host element. */
|
|
10
|
+
class?: string;
|
|
9
11
|
/** Target id for `aria-controls` from the sidebar footer/nav. */
|
|
10
12
|
panelId?: string;
|
|
11
13
|
style?: string;
|
|
@@ -24,6 +26,7 @@
|
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
let {
|
|
29
|
+
class: className = "",
|
|
27
30
|
panelId = generateId("account-panel"),
|
|
28
31
|
style = "",
|
|
29
32
|
profileName = "Account",
|
|
@@ -96,6 +99,7 @@
|
|
|
96
99
|
<div
|
|
97
100
|
id={panelId}
|
|
98
101
|
{style}
|
|
102
|
+
class={className}
|
|
99
103
|
role="dialog"
|
|
100
104
|
aria-modal="false"
|
|
101
105
|
aria-label="Account panel"
|