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,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"
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
import SidebarBrandHeader from "../molecules/SidebarBrandHeader.svelte";
|
|
7
7
|
import SidebarFooter from "../molecules/SidebarFooter.svelte";
|
|
8
8
|
import SidebarNavSection from "../molecules/SidebarNavSection.svelte";
|
|
9
|
+
import Tooltip from "../atoms/Tooltip.svelte";
|
|
10
|
+
import SidebarShell from "./SidebarShell.svelte";
|
|
9
11
|
import { Command, Search } from "@lucide/svelte";
|
|
10
12
|
import type { Snippet } from "svelte";
|
|
11
13
|
import type { Component } from "svelte";
|
|
@@ -34,6 +36,8 @@
|
|
|
34
36
|
items?: SidebarNavigationItem[];
|
|
35
37
|
currentPath?: string;
|
|
36
38
|
ariaLabel?: string;
|
|
39
|
+
class?: string;
|
|
40
|
+
/** @deprecated use `class`. */
|
|
37
41
|
className?: string;
|
|
38
42
|
logoSrc?: string;
|
|
39
43
|
logoAlt?: string;
|
|
@@ -78,7 +82,8 @@
|
|
|
78
82
|
currentPath = "",
|
|
79
83
|
activePrimaryHref = "",
|
|
80
84
|
ariaLabel = "Sidebar navigation",
|
|
81
|
-
|
|
85
|
+
class: classAttr = "",
|
|
86
|
+
className: legacyClass = "",
|
|
82
87
|
logoSrc = "",
|
|
83
88
|
logoAlt = "",
|
|
84
89
|
brandName = "",
|
|
@@ -113,8 +118,10 @@
|
|
|
113
118
|
...restProps
|
|
114
119
|
}: Props = $props();
|
|
115
120
|
|
|
121
|
+
/** `class` is the public prop; `className` is a deprecated alias.
|
|
122
|
+
* Both are merged here so existing call sites keep working. */
|
|
123
|
+
|
|
116
124
|
const isCollapsed = $derived(mode === "collapsed");
|
|
117
|
-
const isCard = $derived(layout === "card");
|
|
118
125
|
const showBrandRow = $derived(
|
|
119
126
|
Boolean(logoSrc.trim() || brandName.trim()),
|
|
120
127
|
);
|
|
@@ -178,24 +185,6 @@
|
|
|
178
185
|
partitionBySection(filteredPrimaryItems),
|
|
179
186
|
);
|
|
180
187
|
|
|
181
|
-
const insetX = $derived(isCollapsed ? "px-2.5" : "px-4");
|
|
182
|
-
|
|
183
|
-
const containerClasses = $derived.by(() => {
|
|
184
|
-
const widthClass = isCollapsed ? "w-[104px]" : "w-[266px]";
|
|
185
|
-
const railSurface = "border-r border-border bg-background text-headline";
|
|
186
|
-
const cardSurface =
|
|
187
|
-
"border-r border-border bg-background text-headline shadow-sm";
|
|
188
|
-
const surfaceClasses = isCard ? cardSurface : railSurface;
|
|
189
|
-
const verticalPad = isCard ? "py-4" : "py-5";
|
|
190
|
-
const baseClasses = `flex h-full min-h-0 max-h-full flex-col overflow-visible ${verticalPad}`;
|
|
191
|
-
|
|
192
|
-
return `${baseClasses} ${widthClass} ${surfaceClasses} ${className}`.trim();
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
const headerStackClasses = $derived(
|
|
196
|
-
`flex w-full shrink-0 flex-col gap-5 ${insetX}`,
|
|
197
|
-
);
|
|
198
|
-
|
|
199
188
|
const iconContainerClasses = $derived(
|
|
200
189
|
"flex size-6 shrink-0 items-center justify-center leading-none text-current",
|
|
201
190
|
);
|
|
@@ -210,21 +199,43 @@
|
|
|
210
199
|
return isMuted ? "text-description" : "text-headline";
|
|
211
200
|
}
|
|
212
201
|
|
|
213
|
-
|
|
214
|
-
|
|
202
|
+
/**
|
|
203
|
+
* The selected row used to be carried by its fill alone, and the fill was
|
|
204
|
+
* 1.05:1 against the sidebar in light mode — not a fill, a rumour. Active
|
|
205
|
+
* and idle labels were #18181b and #27272a at the same weight, a 15-unit
|
|
206
|
+
* difference in one channel that nobody can see. So in light mode there
|
|
207
|
+
* was effectively no indication of where you were.
|
|
208
|
+
*
|
|
209
|
+
* Three cues now, only one of which is colour-discrimination dependent:
|
|
210
|
+
* the label takes --color-nav-menu-item-active (defined all along and
|
|
211
|
+
* never referenced — the row said `text-inherit`), a 3px bar marks the
|
|
212
|
+
* leading edge, and the tint stays as reinforcement rather than as the
|
|
213
|
+
* whole message.
|
|
214
|
+
*/
|
|
215
|
+
function isItemActive(item: SidebarNavigationItem): boolean {
|
|
216
|
+
return (
|
|
215
217
|
currentPath === item.href ||
|
|
216
|
-
(Boolean(activePrimaryHref) && item.href === activePrimaryHref)
|
|
218
|
+
(Boolean(activePrimaryHref) && item.href === activePrimaryHref)
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function getNavItemClasses(item: SidebarNavigationItem): string {
|
|
223
|
+
const isActive = isItemActive(item);
|
|
217
224
|
const layoutClasses = isCollapsed
|
|
218
225
|
? "flex min-h-10 items-center justify-center px-0 py-2"
|
|
219
|
-
: "flex min-h-10 items-center gap-
|
|
226
|
+
: "flex min-h-10 items-center gap-3 px-2 py-2";
|
|
220
227
|
const structural =
|
|
221
|
-
"focus-ring focus-ring--nav w-full cursor-pointer rounded-
|
|
228
|
+
"focus-ring focus-ring--nav relative w-full cursor-pointer rounded-control no-underline transition-colors duration-150 outline-none";
|
|
222
229
|
|
|
223
230
|
if (isActive) {
|
|
224
|
-
return
|
|
231
|
+
return (
|
|
232
|
+
`${structural} ${layoutClasses} bg-nav-menu-active ` +
|
|
233
|
+
"text-nav-menu-item-active hover:bg-nav-menu-active " +
|
|
234
|
+
"active:bg-nav-menu-active-hover"
|
|
235
|
+
);
|
|
225
236
|
}
|
|
226
237
|
|
|
227
|
-
return `${structural} ${layoutClasses} text-nav-menu-item hover:bg-nav-menu-hover hover:text-nav-menu-item-hover active:bg-
|
|
238
|
+
return `${structural} ${layoutClasses} text-nav-menu-item hover:bg-nav-menu-hover hover:text-nav-menu-item-hover active:bg-surface-active`;
|
|
228
239
|
}
|
|
229
240
|
|
|
230
241
|
function handleNavigate(item: SidebarNavigationItem, event: MouseEvent) {
|
|
@@ -268,11 +279,15 @@
|
|
|
268
279
|
}
|
|
269
280
|
</script>
|
|
270
281
|
|
|
271
|
-
<
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
282
|
+
<SidebarShell
|
|
283
|
+
{mode}
|
|
284
|
+
{layout}
|
|
285
|
+
{ariaLabel}
|
|
286
|
+
class={classAttr}
|
|
287
|
+
className={legacyClass}
|
|
288
|
+
{...restProps}
|
|
289
|
+
>
|
|
290
|
+
{#snippet header()}
|
|
276
291
|
{#if showBrandRow}
|
|
277
292
|
<SidebarBrandHeader
|
|
278
293
|
collapsed={isCollapsed}
|
|
@@ -307,10 +322,10 @@
|
|
|
307
322
|
<Button
|
|
308
323
|
variant={searchTriggerVariant}
|
|
309
324
|
size={searchTriggerSize}
|
|
310
|
-
|
|
325
|
+
fullWidth
|
|
311
326
|
onclick={handleSearchClick}
|
|
312
327
|
>
|
|
313
|
-
<span class="flex w-full items-center justify-start gap-
|
|
328
|
+
<span class="flex w-full items-center justify-start gap-3">
|
|
314
329
|
<span class={iconContainerClasses} aria-hidden="true">
|
|
315
330
|
<TriggerIcon size={17} />
|
|
316
331
|
</span>
|
|
@@ -334,18 +349,13 @@
|
|
|
334
349
|
bind:value={searchValue}
|
|
335
350
|
placeholder={searchPlaceholder}
|
|
336
351
|
aria-label={searchPlaceholder}
|
|
337
|
-
class="focus-ring focus-ring--nav w-full min-w-0 min-h-10 rounded-
|
|
352
|
+
class="focus-ring focus-ring--nav w-full min-w-0 min-h-10 rounded-container border-transparent !bg-transparent py-2 pl-10 text-sm ring-1 ring-border/60 hover:!bg-nav-menu-hover focus:!bg-transparent"
|
|
338
353
|
/>
|
|
339
354
|
</div>
|
|
340
355
|
{/if}
|
|
341
356
|
{/if}
|
|
342
|
-
|
|
357
|
+
{/snippet}
|
|
343
358
|
|
|
344
|
-
<div
|
|
345
|
-
class={`flex min-h-0 min-w-0 flex-1 flex-col gap-3 overflow-y-auto overflow-x-hidden overscroll-y-contain pt-4 pb-1.5 ${insetX}`}
|
|
346
|
-
role="region"
|
|
347
|
-
aria-label="Navigation links"
|
|
348
|
-
>
|
|
349
359
|
{#if hasFilteredItems}
|
|
350
360
|
<div
|
|
351
361
|
class="flex w-full min-w-0 flex-col divide-y divide-border"
|
|
@@ -363,6 +373,19 @@
|
|
|
363
373
|
{#each group.items as item (item.id)}
|
|
364
374
|
{@const Icon = item.icon}
|
|
365
375
|
<li>
|
|
376
|
+
<!--
|
|
377
|
+
Collapsed rows are icon-only. aria-label
|
|
378
|
+
covers screen readers, but a sighted user
|
|
379
|
+
got an unlabelled glyph with no hover
|
|
380
|
+
affordance at all.
|
|
381
|
+
-->
|
|
382
|
+
<Tooltip
|
|
383
|
+
content={item.label}
|
|
384
|
+
placement="right"
|
|
385
|
+
disabled={!isCollapsed}
|
|
386
|
+
block
|
|
387
|
+
fixed
|
|
388
|
+
>
|
|
366
389
|
<a
|
|
367
390
|
href={item.href}
|
|
368
391
|
class={getNavItemClasses(item)}
|
|
@@ -370,6 +393,12 @@
|
|
|
370
393
|
aria-current={getAriaCurrent(item)}
|
|
371
394
|
aria-label={isCollapsed ? item.label : undefined}
|
|
372
395
|
>
|
|
396
|
+
{#if isItemActive(item)}
|
|
397
|
+
<span
|
|
398
|
+
class="absolute left-0 top-1/2 h-5 w-[3px] -translate-y-1/2 rounded-pill bg-nav-menu-item-active"
|
|
399
|
+
aria-hidden="true"
|
|
400
|
+
></span>
|
|
401
|
+
{/if}
|
|
373
402
|
{#if Icon}
|
|
374
403
|
<span
|
|
375
404
|
class={iconContainerClasses}
|
|
@@ -384,16 +413,31 @@
|
|
|
384
413
|
>{item.label}</span
|
|
385
414
|
>
|
|
386
415
|
{/if}
|
|
387
|
-
{#if
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
416
|
+
{#if getItemBadgeText(item) !== null}
|
|
417
|
+
{#if isCollapsed}
|
|
418
|
+
<!--
|
|
419
|
+
A notification count that
|
|
420
|
+
disappears when you collapse
|
|
421
|
+
the rail is a real loss; the
|
|
422
|
+
count becomes a dot rather
|
|
423
|
+
than nothing.
|
|
424
|
+
-->
|
|
425
|
+
<span
|
|
426
|
+
class="absolute right-2 top-2 size-2 rounded-pill bg-error"
|
|
427
|
+
aria-hidden="true"
|
|
428
|
+
></span>
|
|
429
|
+
{:else}
|
|
430
|
+
<span class="ml-auto">
|
|
431
|
+
<Badge
|
|
432
|
+
variant="default"
|
|
433
|
+
size="sm"
|
|
434
|
+
text={getItemBadgeText(item) ?? ""}
|
|
435
|
+
/>
|
|
436
|
+
</span>
|
|
437
|
+
{/if}
|
|
395
438
|
{/if}
|
|
396
439
|
</a>
|
|
440
|
+
</Tooltip>
|
|
397
441
|
</li>
|
|
398
442
|
{/each}
|
|
399
443
|
</SidebarNavSection>
|
|
@@ -446,7 +490,7 @@
|
|
|
446
490
|
{/if}
|
|
447
491
|
{:else}
|
|
448
492
|
<div
|
|
449
|
-
class="rounded-
|
|
493
|
+
class="rounded-container border border-border border-dashed bg-transparent px-4 py-4 ring-1 ring-border/60"
|
|
450
494
|
>
|
|
451
495
|
<h3 class="text-sm font-semibold {getTextToneClass()}">
|
|
452
496
|
{normalizedSearchTerm && searchMode === "input"
|
|
@@ -461,7 +505,7 @@
|
|
|
461
505
|
{#if !(normalizedSearchTerm && searchMode === "input")}
|
|
462
506
|
<button
|
|
463
507
|
type="button"
|
|
464
|
-
class="focus-ring focus-ring--nav mt-3 inline-flex min-h-10 cursor-pointer items-center rounded-
|
|
508
|
+
class="focus-ring focus-ring--nav mt-3 inline-flex min-h-10 cursor-pointer items-center rounded-control bg-action-primary px-3 py-2 text-sm font-medium text-action-primary outline-none transition-colors hover:bg-action-primary-hover"
|
|
465
509
|
onclick={handleEmptyStateAction}
|
|
466
510
|
>
|
|
467
511
|
{emptyStateActionLabel}
|
|
@@ -469,11 +513,9 @@
|
|
|
469
513
|
{/if}
|
|
470
514
|
</div>
|
|
471
515
|
{/if}
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
<SidebarFooter
|
|
476
|
-
collapsed={isCollapsed}
|
|
516
|
+
{#snippet footer({ insetX })}
|
|
517
|
+
<SidebarFooter
|
|
518
|
+
collapsed={isCollapsed}
|
|
477
519
|
{showProfile}
|
|
478
520
|
{profileName}
|
|
479
521
|
{profileEmail}
|
|
@@ -489,6 +531,7 @@
|
|
|
489
531
|
{profilePanelOpen}
|
|
490
532
|
{profilePanelControlsId}
|
|
491
533
|
{profilePanel}
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
534
|
+
className={insetX}
|
|
535
|
+
/>
|
|
536
|
+
{/snippet}
|
|
537
|
+
</SidebarShell>
|