zabi-components 5.0.22 → 7.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 +139 -1478
- package/THEME.md +473 -0
- package/THEMING.md +248 -0
- package/dist/atoms/ActionPanel.svelte +116 -0
- package/dist/atoms/ActionPanel.svelte.d.ts +19 -0
- package/dist/atoms/Badge.svelte +1 -1
- package/dist/atoms/Badge.svelte.d.ts +1 -1
- package/dist/atoms/Button.svelte +35 -16
- package/dist/atoms/Button.svelte.d.ts +3 -1
- package/dist/atoms/Card.svelte +49 -67
- package/dist/atoms/Card.svelte.d.ts +4 -4
- package/dist/atoms/CardHeader.svelte +15 -2
- package/dist/atoms/CardHeader.svelte.d.ts +5 -0
- package/dist/atoms/Checkbox.svelte +50 -76
- package/dist/atoms/Checkbox.svelte.d.ts +7 -2
- package/dist/atoms/CodeBlock.svelte +20 -34
- package/dist/atoms/CodeBlock.svelte.d.ts +1 -0
- package/dist/atoms/ColorPicker.svelte +3 -7
- package/dist/atoms/Container.svelte +41 -0
- package/dist/atoms/Container.svelte.d.ts +12 -0
- package/dist/atoms/Divider.svelte +68 -0
- package/dist/atoms/Divider.svelte.d.ts +13 -0
- package/dist/atoms/FeatureCard.svelte +35 -50
- package/dist/atoms/FeatureCard.svelte.d.ts +9 -4
- package/dist/atoms/IconButton.svelte +33 -24
- package/dist/atoms/IconButton.svelte.d.ts +4 -8
- package/dist/atoms/Input.svelte +58 -26
- package/dist/atoms/Input.svelte.d.ts +6 -1
- package/dist/atoms/List.svelte +2 -7
- package/dist/atoms/List.svelte.d.ts +1 -6
- package/dist/atoms/ListItem.svelte +54 -65
- package/dist/atoms/ListItem.svelte.d.ts +7 -13
- package/dist/atoms/ListItemLeading.svelte +19 -0
- package/dist/atoms/ListItemLeading.svelte.d.ts +9 -0
- package/dist/atoms/Progress.svelte +2 -2
- package/dist/atoms/Radio.svelte +51 -0
- package/dist/atoms/Radio.svelte.d.ts +14 -0
- package/dist/atoms/Select.svelte +28 -33
- package/dist/atoms/Select.svelte.d.ts +0 -10
- package/dist/atoms/SelectionControl.svelte +98 -0
- package/dist/atoms/SelectionControl.svelte.d.ts +26 -0
- package/dist/atoms/Skeleton.svelte +39 -15
- package/dist/atoms/Skeleton.svelte.d.ts +7 -3
- package/dist/atoms/Table.svelte +27 -0
- package/dist/atoms/Table.svelte.d.ts +10 -0
- package/dist/atoms/Text.svelte +46 -0
- package/dist/atoms/Text.svelte.d.ts +13 -0
- package/dist/atoms/Textarea.svelte +74 -53
- package/dist/atoms/Textarea.svelte.d.ts +6 -2
- package/dist/atoms/ThemeToggle.svelte +35 -24
- package/dist/atoms/ThemeToggle.svelte.d.ts +1 -0
- package/dist/atoms/Toast.svelte +55 -31
- package/dist/atoms/Toast.svelte.d.ts +4 -1
- package/dist/atoms/Toggle.svelte +37 -22
- package/dist/atoms/Toggle.svelte.d.ts +2 -1
- package/dist/atoms/Tooltip.svelte +90 -11
- package/dist/atoms/index.d.ts +6 -0
- package/dist/atoms/index.js +6 -0
- package/dist/atoms/selection-control.styles.d.ts +25 -0
- package/dist/atoms/selection-control.styles.js +30 -0
- package/dist/components/atoms/index.d.ts +6 -0
- package/dist/components/atoms/index.d.ts.map +1 -1
- package/dist/components/atoms/selection-control.styles.d.ts +26 -0
- package/dist/components/atoms/selection-control.styles.d.ts.map +1 -0
- package/dist/components/index.d.ts +42 -7
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/molecules/index.d.ts +10 -0
- package/dist/components/molecules/index.d.ts.map +1 -1
- package/dist/components/molecules/navigation-menu-context.d.ts +11 -0
- package/dist/components/molecules/navigation-menu-context.d.ts.map +1 -1
- package/dist/components/molecules/toast-store.d.ts +39 -0
- package/dist/components/molecules/toast-store.d.ts.map +1 -0
- package/dist/components/organisms/index.d.ts +3 -3
- package/dist/components/organisms/index.d.ts.map +1 -1
- package/dist/components/types/page.types.d.ts +69 -0
- package/dist/components/types/page.types.d.ts.map +1 -0
- package/dist/components/types/page.types.js +1 -0
- package/dist/components/types/variants.d.ts +64 -0
- package/dist/components/types/variants.d.ts.map +1 -0
- package/dist/components/types/variants.js +44 -0
- package/dist/components/util/fixed-sidebar-flyout.d.ts +23 -0
- package/dist/components/util/fixed-sidebar-flyout.d.ts.map +1 -0
- package/dist/components/util/focus-utils.d.ts +12 -0
- package/dist/components/util/focus-utils.d.ts.map +1 -0
- package/dist/components/util/ssr-safe.d.ts +17 -0
- package/dist/components/util/ssr-safe.d.ts.map +1 -0
- package/dist/index.d.ts +42 -7
- package/dist/index.js +45 -10
- package/dist/lib/layout-width-tokens.d.ts +17 -0
- package/dist/lib/layout-width-tokens.d.ts.map +1 -0
- package/dist/lib/showcase/component-docs/Button.d.ts +3 -0
- package/dist/lib/showcase/component-docs/Button.d.ts.map +1 -0
- package/dist/lib/showcase/component-docs/Input.d.ts +3 -0
- package/dist/lib/showcase/component-docs/Input.d.ts.map +1 -0
- package/dist/lib/showcase/component-docs/List.d.ts +3 -0
- package/dist/lib/showcase/component-docs/List.d.ts.map +1 -0
- package/dist/lib/showcase/component-docs/Modal.d.ts +3 -0
- package/dist/lib/showcase/component-docs/Modal.d.ts.map +1 -0
- package/dist/lib/showcase/component-docs/Radio.d.ts +3 -0
- package/dist/lib/showcase/component-docs/Radio.d.ts.map +1 -0
- package/dist/lib/showcase/component-docs/SidebarNavigation.d.ts +3 -0
- package/dist/lib/showcase/component-docs/SidebarNavigation.d.ts.map +1 -0
- package/dist/lib/showcase/component-docs/Skeleton.d.ts +3 -0
- package/dist/lib/showcase/component-docs/Skeleton.d.ts.map +1 -0
- package/dist/lib/showcase/component-docs/_shared.d.ts +12 -0
- package/dist/lib/showcase/component-docs/_shared.d.ts.map +1 -0
- package/dist/lib/showcase/component-docs/index.d.ts +4 -0
- package/dist/lib/showcase/component-docs/index.d.ts.map +1 -0
- package/dist/lib/showcase/components-catalog.d.ts +3 -0
- package/dist/lib/showcase/components-catalog.d.ts.map +1 -0
- package/dist/lib/showcase/components-showcase-constants.d.ts +89 -0
- package/dist/lib/showcase/components-showcase-constants.d.ts.map +1 -0
- package/dist/lib/showcase/docs-sidebar-helpers.d.ts +15 -0
- package/dist/lib/showcase/docs-sidebar-helpers.d.ts.map +1 -0
- package/dist/molecules/Alert.svelte +50 -23
- package/dist/molecules/Alert.svelte.d.ts +1 -1
- package/dist/molecules/ComponentDemo.svelte +54 -51
- package/dist/molecules/ContactForm.svelte +70 -65
- package/dist/molecules/Dropdown.svelte +99 -108
- package/dist/molecules/Dropdown.svelte.d.ts +12 -6
- package/dist/molecules/EmptyState.svelte +44 -0
- package/dist/molecules/EmptyState.svelte.d.ts +11 -0
- package/dist/molecules/FormField.svelte +89 -0
- package/dist/molecules/FormField.svelte.d.ts +23 -0
- package/dist/molecules/Header.svelte +41 -0
- package/dist/molecules/Header.svelte.d.ts +10 -0
- package/dist/molecules/ImageUpload.svelte +1 -1
- package/dist/molecules/Modal.svelte +39 -26
- package/dist/molecules/Modal.svelte.d.ts +4 -2
- package/dist/molecules/NavigationMenu.svelte +16 -0
- package/dist/molecules/NavigationMenu.svelte.d.ts +2 -0
- package/dist/molecules/NavigationMenuContent.svelte +9 -1
- package/dist/molecules/NavigationMenuLink.svelte +1 -1
- package/dist/molecules/NavigationMenuTrigger.svelte +9 -4
- package/dist/molecules/Page.svelte +16 -0
- package/dist/molecules/Page.svelte.d.ts +8 -0
- package/dist/molecules/PropsTable.svelte +61 -0
- package/dist/molecules/PropsTable.svelte.d.ts +8 -0
- package/dist/molecules/RadioGroup.svelte +197 -0
- package/dist/molecules/RadioGroup.svelte.d.ts +20 -0
- package/dist/molecules/Section.svelte +14 -23
- package/dist/molecules/Section.svelte.d.ts +2 -11
- package/dist/molecules/SidebarBrandHeader.svelte +68 -0
- package/dist/molecules/SidebarBrandHeader.svelte.d.ts +10 -0
- package/dist/molecules/SidebarFooter.svelte +119 -0
- package/dist/molecules/SidebarFooter.svelte.d.ts +23 -0
- package/dist/molecules/SidebarNavSection.svelte +49 -0
- package/dist/molecules/SidebarNavSection.svelte.d.ts +14 -0
- package/dist/molecules/SlideUp.svelte +21 -16
- package/dist/molecules/SlideUp.svelte.d.ts +1 -1
- package/dist/molecules/Tabs.svelte +6 -11
- package/dist/molecules/Toaster.svelte +21 -0
- package/dist/molecules/Toaster.svelte.d.ts +6 -0
- package/dist/molecules/ToasterToast.svelte +217 -0
- package/dist/molecules/ToasterToast.svelte.d.ts +7 -0
- package/dist/molecules/index.d.ts +10 -0
- package/dist/molecules/index.js +10 -0
- package/dist/molecules/navigation-menu-context.d.ts +11 -0
- package/dist/molecules/navigation-menu-context.js +18 -0
- package/dist/molecules/toast-store.d.ts +38 -0
- package/dist/molecules/toast-store.js +34 -0
- package/dist/organisms/SidebarAccountPanel.svelte +118 -0
- package/dist/organisms/SidebarAccountPanel.svelte.d.ts +20 -0
- package/dist/organisms/SidebarNavigation.svelte +286 -212
- package/dist/organisms/SidebarNavigation.svelte.d.ts +22 -1
- package/dist/organisms/{SidebarProjectPanel.svelte → SidebarPanel.svelte} +67 -34
- package/dist/organisms/{SidebarProjectPanel.svelte.d.ts → SidebarPanel.svelte.d.ts} +9 -6
- package/dist/organisms/TopNavbar.svelte +214 -0
- package/dist/organisms/TopNavbar.svelte.d.ts +35 -0
- package/dist/organisms/index.d.ts +3 -3
- package/dist/organisms/index.js +3 -3
- package/dist/routes/lib/focus-utils.d.ts +7 -18
- package/dist/routes/lib/focus-utils.ts +32 -34
- package/dist/routes/lib/ssr-safe.ts +5 -4
- package/dist/routes/lib/variant-utils.ts +48 -39
- package/dist/types/components.d.ts +187 -0
- package/dist/types/components.d.ts.map +1 -0
- package/dist/types/events.d.ts +200 -0
- package/dist/types/events.d.ts.map +1 -0
- package/dist/types/index.d.ts +171 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/page.types.d.ts +3 -0
- package/dist/types/page.types.d.ts.map +1 -0
- package/dist/types/page.types.js +1 -0
- package/dist/types/page.types.ts +79 -0
- package/dist/types/variants.d.ts +3 -0
- package/dist/types/variants.d.ts.map +1 -0
- package/dist/types/variants.js +1 -0
- package/dist/types/variants.ts +75 -0
- package/dist/util/fixed-sidebar-flyout.d.ts +22 -0
- package/dist/util/fixed-sidebar-flyout.js +148 -0
- package/dist/util/focus-utils.d.ts +11 -0
- package/dist/util/focus-utils.js +94 -0
- package/dist/util/ssr-safe.d.ts +16 -0
- package/dist/util/ssr-safe.js +48 -0
- package/dist/zabi-components-colors.css +360 -324
- package/dist/zabi-components-theme-dark-only.css +164 -151
- package/dist/zabi-components-theme-dark.css +164 -151
- package/dist/zabi-components-theme-only.css +253 -230
- package/dist/zabi-components-theme.css +253 -230
- package/dist/zabi-components.css +1262 -241
- package/docs/theme-imports.md +112 -0
- package/package.json +32 -10
- package/dist/organisms/Navbar.svelte +0 -92
- package/dist/organisms/Navbar.svelte.d.ts +0 -14
- package/dist/organisms/Navigation.svelte +0 -119
- package/dist/organisms/Navigation.svelte.d.ts +0 -21
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import Badge from "../atoms/Badge.svelte";
|
|
3
|
+
import Button from "../atoms/Button.svelte";
|
|
4
|
+
import IconButton from "../atoms/IconButton.svelte";
|
|
3
5
|
import Input from "../atoms/Input.svelte";
|
|
4
|
-
import
|
|
6
|
+
import SidebarBrandHeader from "../molecules/SidebarBrandHeader.svelte";
|
|
7
|
+
import SidebarFooter from "../molecules/SidebarFooter.svelte";
|
|
8
|
+
import SidebarNavSection from "../molecules/SidebarNavSection.svelte";
|
|
9
|
+
import { Command, Search } from "@lucide/svelte";
|
|
10
|
+
import type { Snippet } from "svelte";
|
|
5
11
|
import type { Component } from "svelte";
|
|
12
|
+
import type { ButtonVariant, SizeVariant } from "../types/variants.js";
|
|
6
13
|
|
|
7
14
|
export interface SidebarNavigationItem {
|
|
8
15
|
id: string;
|
|
@@ -12,14 +19,25 @@
|
|
|
12
19
|
badgeText?: string | number;
|
|
13
20
|
badgeCount?: number;
|
|
14
21
|
group?: "primary" | "secondary";
|
|
22
|
+
/** Groups items under a section heading in the rail. */
|
|
23
|
+
section?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface NavSectionGroup {
|
|
27
|
+
sectionLabel: string | null;
|
|
28
|
+
items: SidebarNavigationItem[];
|
|
15
29
|
}
|
|
16
30
|
|
|
17
31
|
interface Props {
|
|
18
32
|
mode?: "expanded" | "collapsed";
|
|
33
|
+
layout?: "rail" | "card";
|
|
19
34
|
items?: SidebarNavigationItem[];
|
|
20
35
|
currentPath?: string;
|
|
21
36
|
ariaLabel?: string;
|
|
22
37
|
className?: string;
|
|
38
|
+
logoSrc?: string;
|
|
39
|
+
logoAlt?: string;
|
|
40
|
+
brandName?: string;
|
|
23
41
|
showProfile?: boolean;
|
|
24
42
|
profileName?: string;
|
|
25
43
|
profileEmail?: string;
|
|
@@ -28,6 +46,10 @@
|
|
|
28
46
|
searchMode?: "input" | "button";
|
|
29
47
|
searchPlaceholder?: string;
|
|
30
48
|
searchValue?: string;
|
|
49
|
+
/** Trigger icon when search is a button or rail is collapsed; input mode keeps `Search` on the field. */
|
|
50
|
+
searchTriggerIcon?: Component<{ size?: number; class?: string }>;
|
|
51
|
+
searchTriggerVariant?: ButtonVariant;
|
|
52
|
+
searchTriggerSize?: SizeVariant;
|
|
31
53
|
showLogout?: boolean;
|
|
32
54
|
logoutLabel?: string;
|
|
33
55
|
showThemeToggle?: boolean;
|
|
@@ -41,14 +63,25 @@
|
|
|
41
63
|
onLogout?: () => void;
|
|
42
64
|
onThemeToggle?: (nextIsLightMode: boolean) => void;
|
|
43
65
|
onEmptyStateAction?: () => void;
|
|
66
|
+
onProfileClick?: (event?: MouseEvent) => void;
|
|
67
|
+
profilePanelOpen?: boolean;
|
|
68
|
+
profilePanelControlsId?: string;
|
|
69
|
+
profilePanel?: Snippet;
|
|
70
|
+
/** Parent nav href to highlight when `currentPath` is a deeper leaf (e.g. category row). */
|
|
71
|
+
activePrimaryHref?: string;
|
|
44
72
|
}
|
|
45
73
|
|
|
46
74
|
let {
|
|
47
75
|
mode = "expanded",
|
|
76
|
+
layout = "rail",
|
|
48
77
|
items = [],
|
|
49
78
|
currentPath = "",
|
|
79
|
+
activePrimaryHref = "",
|
|
50
80
|
ariaLabel = "Sidebar navigation",
|
|
51
81
|
className = "",
|
|
82
|
+
logoSrc = "",
|
|
83
|
+
logoAlt = "",
|
|
84
|
+
brandName = "",
|
|
52
85
|
showProfile = true,
|
|
53
86
|
profileName = "Zabi",
|
|
54
87
|
profileEmail = "hello@zabi.dev",
|
|
@@ -57,6 +90,9 @@
|
|
|
57
90
|
searchMode = "input",
|
|
58
91
|
searchPlaceholder = "Search...",
|
|
59
92
|
searchValue = $bindable(""),
|
|
93
|
+
searchTriggerIcon = Command,
|
|
94
|
+
searchTriggerVariant = "outline",
|
|
95
|
+
searchTriggerSize = "sm",
|
|
60
96
|
showLogout = true,
|
|
61
97
|
logoutLabel = "Logout",
|
|
62
98
|
showThemeToggle = true,
|
|
@@ -70,10 +106,19 @@
|
|
|
70
106
|
onLogout,
|
|
71
107
|
onThemeToggle,
|
|
72
108
|
onEmptyStateAction,
|
|
109
|
+
onProfileClick,
|
|
110
|
+
profilePanelOpen = false,
|
|
111
|
+
profilePanelControlsId = "",
|
|
112
|
+
profilePanel,
|
|
73
113
|
...restProps
|
|
74
114
|
}: Props = $props();
|
|
75
115
|
|
|
76
116
|
const isCollapsed = $derived(mode === "collapsed");
|
|
117
|
+
const isCard = $derived(layout === "card");
|
|
118
|
+
const showBrandRow = $derived(
|
|
119
|
+
Boolean(logoSrc.trim() || brandName.trim()),
|
|
120
|
+
);
|
|
121
|
+
|
|
77
122
|
const normalizedSearchTerm = $derived(searchValue.trim().toLowerCase());
|
|
78
123
|
const primaryItems = $derived(
|
|
79
124
|
items.filter((item) => item.group !== "secondary"),
|
|
@@ -81,17 +126,31 @@
|
|
|
81
126
|
const secondaryItems = $derived(
|
|
82
127
|
items.filter((item) => item.group === "secondary"),
|
|
83
128
|
);
|
|
129
|
+
|
|
130
|
+
function itemMatchesSearch(item: SidebarNavigationItem): boolean {
|
|
131
|
+
return item.label.toLowerCase().includes(normalizedSearchTerm);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function itemPinnedBySelection(item: SidebarNavigationItem): boolean {
|
|
135
|
+
if (currentPath === item.href) {
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
return Boolean(activePrimaryHref) && item.href === activePrimaryHref;
|
|
139
|
+
}
|
|
140
|
+
|
|
84
141
|
const filteredPrimaryItems = $derived(
|
|
85
142
|
searchMode === "input" && normalizedSearchTerm
|
|
86
|
-
? primaryItems.filter(
|
|
87
|
-
item
|
|
143
|
+
? primaryItems.filter(
|
|
144
|
+
(item) =>
|
|
145
|
+
itemMatchesSearch(item) || itemPinnedBySelection(item),
|
|
88
146
|
)
|
|
89
147
|
: primaryItems,
|
|
90
148
|
);
|
|
91
149
|
const filteredSecondaryItems = $derived(
|
|
92
150
|
searchMode === "input" && normalizedSearchTerm
|
|
93
|
-
? secondaryItems.filter(
|
|
94
|
-
item
|
|
151
|
+
? secondaryItems.filter(
|
|
152
|
+
(item) =>
|
|
153
|
+
itemMatchesSearch(item) || itemPinnedBySelection(item),
|
|
95
154
|
)
|
|
96
155
|
: secondaryItems,
|
|
97
156
|
);
|
|
@@ -99,32 +158,52 @@
|
|
|
99
158
|
filteredPrimaryItems.length + filteredSecondaryItems.length > 0,
|
|
100
159
|
);
|
|
101
160
|
|
|
161
|
+
function partitionBySection(
|
|
162
|
+
list: SidebarNavigationItem[],
|
|
163
|
+
): NavSectionGroup[] {
|
|
164
|
+
const groups: NavSectionGroup[] = [];
|
|
165
|
+
for (const item of list) {
|
|
166
|
+
const label = item.section?.trim() || null;
|
|
167
|
+
const last = groups[groups.length - 1];
|
|
168
|
+
if (last && last.sectionLabel === label) {
|
|
169
|
+
last.items = [...last.items, item];
|
|
170
|
+
} else {
|
|
171
|
+
groups.push({ sectionLabel: label, items: [item] });
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return groups;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const primarySectionGroups = $derived(
|
|
178
|
+
partitionBySection(filteredPrimaryItems),
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
const insetX = $derived(isCollapsed ? "px-2.5" : "px-4");
|
|
182
|
+
|
|
102
183
|
const containerClasses = $derived.by(() => {
|
|
103
184
|
const widthClass = isCollapsed ? "w-[104px]" : "w-[266px]";
|
|
104
|
-
const
|
|
105
|
-
const
|
|
106
|
-
"
|
|
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}`;
|
|
107
191
|
|
|
108
192
|
return `${baseClasses} ${widthClass} ${surfaceClasses} ${className}`.trim();
|
|
109
193
|
});
|
|
110
194
|
|
|
111
|
-
const
|
|
112
|
-
|
|
195
|
+
const headerStackClasses = $derived(
|
|
196
|
+
`flex w-full shrink-0 flex-col gap-5 ${insetX}`,
|
|
113
197
|
);
|
|
114
198
|
|
|
115
|
-
const searchButtonClasses = $derived(
|
|
116
|
-
"w-full rounded-2xl bg-input transition-colors duration-150 flex min-h-11 items-center justify-center px-0 py-2.5 focus:outline-none focus:ring-2 focus:ring-brand-500 focus:ring-offset-0",
|
|
117
|
-
);
|
|
118
|
-
const searchTriggerClasses = $derived.by(() => {
|
|
119
|
-
return isCollapsed
|
|
120
|
-
? searchButtonClasses
|
|
121
|
-
: "w-full rounded-2xl bg-input transition-colors duration-150 flex min-h-11 items-center gap-3 px-4 py-2.5 text-left focus:outline-none focus:ring-2 focus:ring-brand-500 focus:ring-offset-0";
|
|
122
|
-
});
|
|
123
199
|
const iconContainerClasses = $derived(
|
|
124
|
-
"flex size-6 shrink-0 items-center justify-center leading-none",
|
|
200
|
+
"flex size-6 shrink-0 items-center justify-center leading-none text-current",
|
|
125
201
|
);
|
|
202
|
+
/** With `searchMode: "input"`, the field stays visible even if `onSearchClick` is set (e.g. Storybook). */
|
|
126
203
|
const shouldRenderSearchButton = $derived(
|
|
127
|
-
isCollapsed ||
|
|
204
|
+
isCollapsed ||
|
|
205
|
+
searchMode === "button" ||
|
|
206
|
+
(Boolean(onSearchClick) && searchMode !== "input"),
|
|
128
207
|
);
|
|
129
208
|
|
|
130
209
|
function getTextToneClass(isMuted = false): string {
|
|
@@ -132,36 +211,22 @@
|
|
|
132
211
|
}
|
|
133
212
|
|
|
134
213
|
function getNavItemClasses(item: SidebarNavigationItem): string {
|
|
135
|
-
const isActive =
|
|
136
|
-
|
|
137
|
-
|
|
214
|
+
const isActive =
|
|
215
|
+
currentPath === item.href ||
|
|
216
|
+
(Boolean(activePrimaryHref) && item.href === activePrimaryHref);
|
|
138
217
|
const layoutClasses = isCollapsed
|
|
139
|
-
? "flex min-h-
|
|
140
|
-
: "flex min-h-
|
|
218
|
+
? "flex min-h-10 items-center justify-center px-0 py-2"
|
|
219
|
+
: "flex min-h-10 items-center gap-2.5 px-2.5 py-2";
|
|
220
|
+
const structural =
|
|
221
|
+
"focus-ring focus-ring--nav w-full cursor-pointer rounded-lg no-underline transition-colors duration-150 outline-none";
|
|
141
222
|
|
|
142
223
|
if (isActive) {
|
|
143
|
-
return `${
|
|
224
|
+
return `${structural} ${layoutClasses} bg-nav-menu-active text-inherit hover:bg-nav-menu-active hover:text-inherit active:opacity-90`;
|
|
144
225
|
}
|
|
145
226
|
|
|
146
|
-
return `${
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
function getControlButtonClasses(): string {
|
|
150
|
-
const baseClasses =
|
|
151
|
-
"w-full rounded-lg text-description hover:text-headline hover:bg-base-50 transition-colors duration-150 focus:outline-none focus:ring-2 focus:ring-brand-500 focus:ring-offset-0";
|
|
152
|
-
const layoutClasses = isCollapsed
|
|
153
|
-
? "flex min-h-11 items-center justify-center px-0 py-2.5"
|
|
154
|
-
: "flex min-h-11 items-center gap-3 px-4 py-2.5";
|
|
155
|
-
|
|
156
|
-
return `${baseClasses} ${layoutClasses}`;
|
|
227
|
+
return `${structural} ${layoutClasses} text-nav-menu-item hover:bg-nav-menu-hover hover:text-nav-menu-item-hover active:bg-base-200`;
|
|
157
228
|
}
|
|
158
229
|
|
|
159
|
-
const themeToggleContainerClasses = $derived(
|
|
160
|
-
isCollapsed
|
|
161
|
-
? "flex w-full justify-center py-1"
|
|
162
|
-
: "flex w-full justify-start py-1",
|
|
163
|
-
);
|
|
164
|
-
|
|
165
230
|
function handleNavigate(item: SidebarNavigationItem, event: MouseEvent) {
|
|
166
231
|
if (onNavigate) {
|
|
167
232
|
onNavigate(item, event);
|
|
@@ -174,19 +239,6 @@
|
|
|
174
239
|
}
|
|
175
240
|
}
|
|
176
241
|
|
|
177
|
-
function handleLogout() {
|
|
178
|
-
if (onLogout) {
|
|
179
|
-
onLogout();
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
function handleThemeToggle() {
|
|
184
|
-
isLightMode = !isLightMode;
|
|
185
|
-
if (onThemeToggle) {
|
|
186
|
-
onThemeToggle(isLightMode);
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
242
|
function handleEmptyStateAction() {
|
|
191
243
|
if (onEmptyStateAction) {
|
|
192
244
|
onEmptyStateAction();
|
|
@@ -202,112 +254,162 @@
|
|
|
202
254
|
}
|
|
203
255
|
return null;
|
|
204
256
|
}
|
|
257
|
+
|
|
258
|
+
function getAriaCurrent(
|
|
259
|
+
item: SidebarNavigationItem,
|
|
260
|
+
): "page" | "location" | undefined {
|
|
261
|
+
if (currentPath === item.href) {
|
|
262
|
+
return "page";
|
|
263
|
+
}
|
|
264
|
+
if (activePrimaryHref && item.href === activePrimaryHref) {
|
|
265
|
+
return "location";
|
|
266
|
+
}
|
|
267
|
+
return undefined;
|
|
268
|
+
}
|
|
205
269
|
</script>
|
|
206
270
|
|
|
207
271
|
<nav class={containerClasses} aria-label={ariaLabel} {...restProps}>
|
|
208
|
-
<div
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
</p>
|
|
221
|
-
<p class="truncate text-sm {getTextToneClass(true)}">
|
|
222
|
-
{profileEmail}
|
|
223
|
-
</p>
|
|
224
|
-
</div>
|
|
225
|
-
{/if}
|
|
226
|
-
</div>
|
|
227
|
-
{/if}
|
|
272
|
+
<div
|
|
273
|
+
class="flex min-h-0 w-full min-w-0 flex-1 flex-col"
|
|
274
|
+
>
|
|
275
|
+
<div class={`${headerStackClasses} pb-3`.trim()}>
|
|
276
|
+
{#if showBrandRow}
|
|
277
|
+
<SidebarBrandHeader
|
|
278
|
+
collapsed={isCollapsed}
|
|
279
|
+
{brandName}
|
|
280
|
+
logoSrc={logoSrc.trim()}
|
|
281
|
+
{logoAlt}
|
|
282
|
+
/>
|
|
283
|
+
{/if}
|
|
228
284
|
|
|
229
|
-
<div class="flex w-full flex-col gap-6">
|
|
230
285
|
{#if showSearch}
|
|
231
286
|
{#if shouldRenderSearchButton}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
287
|
+
{@const TriggerIcon = searchTriggerIcon}
|
|
288
|
+
{#if isCollapsed}
|
|
289
|
+
<span
|
|
290
|
+
class="inline-flex"
|
|
291
|
+
data-sidebar-flyout-anchor="search"
|
|
292
|
+
>
|
|
293
|
+
<IconButton
|
|
294
|
+
variant={searchTriggerVariant}
|
|
295
|
+
size={searchTriggerSize}
|
|
296
|
+
label={searchPlaceholder}
|
|
297
|
+
onclick={handleSearchClick}
|
|
298
|
+
>
|
|
299
|
+
<TriggerIcon size={18} />
|
|
300
|
+
</IconButton>
|
|
240
301
|
</span>
|
|
241
|
-
|
|
302
|
+
{:else}
|
|
303
|
+
<div
|
|
304
|
+
class="w-full min-w-0"
|
|
305
|
+
data-sidebar-flyout-anchor="search"
|
|
306
|
+
>
|
|
307
|
+
<Button
|
|
308
|
+
variant={searchTriggerVariant}
|
|
309
|
+
size={searchTriggerSize}
|
|
310
|
+
isFullWidth
|
|
311
|
+
onclick={handleSearchClick}
|
|
312
|
+
>
|
|
313
|
+
<span class="flex w-full items-center justify-start gap-2.5">
|
|
314
|
+
<span class={iconContainerClasses} aria-hidden="true">
|
|
315
|
+
<TriggerIcon size={17} />
|
|
316
|
+
</span>
|
|
317
|
+
<span class="truncate text-sm text-inherit">
|
|
318
|
+
{searchValue || searchPlaceholder}
|
|
319
|
+
</span>
|
|
320
|
+
</span>
|
|
321
|
+
</Button>
|
|
322
|
+
</div>
|
|
323
|
+
{/if}
|
|
242
324
|
{:else}
|
|
243
325
|
<div class="relative w-full">
|
|
244
326
|
<span
|
|
245
|
-
class="pointer-events-none absolute left-
|
|
327
|
+
class="pointer-events-none absolute left-3 top-1/2 z-10 -translate-y-1/2"
|
|
246
328
|
aria-hidden="true"
|
|
247
329
|
>
|
|
248
|
-
<Search size={
|
|
330
|
+
<Search size={17} class={getTextToneClass(true)} />
|
|
249
331
|
</span>
|
|
250
332
|
<Input
|
|
251
333
|
type="search"
|
|
252
334
|
bind:value={searchValue}
|
|
253
335
|
placeholder={searchPlaceholder}
|
|
254
336
|
aria-label={searchPlaceholder}
|
|
255
|
-
class="w-full min-w-0 rounded-
|
|
337
|
+
class="focus-ring focus-ring--nav w-full min-w-0 min-h-10 rounded-xl border-transparent !bg-transparent py-2 pl-10 text-sm ring-1 ring-border/60 hover:!bg-nav-menu-hover focus:!bg-transparent"
|
|
256
338
|
/>
|
|
257
339
|
</div>
|
|
258
340
|
{/if}
|
|
259
341
|
{/if}
|
|
342
|
+
</div>
|
|
260
343
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
<span class="text-base leading-snug"
|
|
290
|
-
>{item.label}</span
|
|
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
|
+
{#if hasFilteredItems}
|
|
350
|
+
<div
|
|
351
|
+
class="flex w-full min-w-0 flex-col divide-y divide-border"
|
|
352
|
+
>
|
|
353
|
+
{#each primarySectionGroups as group, gi (`${gi}-${group.sectionLabel ?? "x"}`)}
|
|
354
|
+
<div class="py-4 first:pt-1 last:pb-1">
|
|
355
|
+
<SidebarNavSection
|
|
356
|
+
title={group.sectionLabel ?? ""}
|
|
357
|
+
sectionKey={`p-${gi}`}
|
|
358
|
+
listAriaLabel={group.sectionLabel
|
|
359
|
+
? `${group.sectionLabel} navigation`
|
|
360
|
+
: "Primary navigation"}
|
|
361
|
+
collapsed={isCollapsed}
|
|
362
|
+
>
|
|
363
|
+
{#each group.items as item (item.id)}
|
|
364
|
+
{@const Icon = item.icon}
|
|
365
|
+
<li>
|
|
366
|
+
<a
|
|
367
|
+
href={item.href}
|
|
368
|
+
class={getNavItemClasses(item)}
|
|
369
|
+
onclick={(event) => handleNavigate(item, event)}
|
|
370
|
+
aria-current={getAriaCurrent(item)}
|
|
371
|
+
aria-label={isCollapsed ? item.label : undefined}
|
|
291
372
|
>
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
373
|
+
{#if Icon}
|
|
374
|
+
<span
|
|
375
|
+
class={iconContainerClasses}
|
|
376
|
+
aria-hidden="true"
|
|
377
|
+
>
|
|
378
|
+
<Icon size={18} />
|
|
379
|
+
</span>
|
|
380
|
+
{/if}
|
|
381
|
+
{#if !isCollapsed}
|
|
382
|
+
<span
|
|
383
|
+
class="text-sm font-medium leading-snug text-inherit"
|
|
384
|
+
>{item.label}</span
|
|
385
|
+
>
|
|
386
|
+
{/if}
|
|
387
|
+
{#if !isCollapsed && getItemBadgeText(item) !== null}
|
|
388
|
+
<span class="ml-auto">
|
|
389
|
+
<Badge
|
|
390
|
+
variant="default"
|
|
391
|
+
size="sm"
|
|
392
|
+
text={getItemBadgeText(item) ?? ""}
|
|
393
|
+
/>
|
|
394
|
+
</span>
|
|
395
|
+
{/if}
|
|
396
|
+
</a>
|
|
397
|
+
</li>
|
|
398
|
+
{/each}
|
|
399
|
+
</SidebarNavSection>
|
|
400
|
+
</div>
|
|
401
|
+
{/each}
|
|
402
|
+
</div>
|
|
306
403
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
404
|
+
{#if filteredSecondaryItems.length > 0}
|
|
405
|
+
<div
|
|
406
|
+
class="mt-3 border-t border-border pt-4"
|
|
407
|
+
role="presentation"
|
|
408
|
+
>
|
|
409
|
+
<SidebarNavSection
|
|
410
|
+
sectionKey="secondary"
|
|
411
|
+
listAriaLabel="Secondary navigation"
|
|
412
|
+
collapsed={isCollapsed}
|
|
311
413
|
>
|
|
312
414
|
{#each filteredSecondaryItems as item (item.id)}
|
|
313
415
|
{@const Icon = item.icon}
|
|
@@ -317,9 +419,7 @@
|
|
|
317
419
|
class={getNavItemClasses(item)}
|
|
318
420
|
onclick={(event) =>
|
|
319
421
|
handleNavigate(item, event)}
|
|
320
|
-
aria-current={
|
|
321
|
-
? "page"
|
|
322
|
-
: undefined}
|
|
422
|
+
aria-current={getAriaCurrent(item)}
|
|
323
423
|
aria-label={isCollapsed
|
|
324
424
|
? item.label
|
|
325
425
|
: undefined}
|
|
@@ -329,92 +429,66 @@
|
|
|
329
429
|
class={iconContainerClasses}
|
|
330
430
|
aria-hidden="true"
|
|
331
431
|
>
|
|
332
|
-
<Icon size={
|
|
432
|
+
<Icon size={18} />
|
|
333
433
|
</span>
|
|
334
434
|
{/if}
|
|
335
435
|
{#if !isCollapsed}
|
|
336
|
-
<span
|
|
436
|
+
<span
|
|
437
|
+
class="text-sm font-medium leading-snug text-inherit"
|
|
337
438
|
>{item.label}</span
|
|
338
439
|
>
|
|
339
440
|
{/if}
|
|
340
441
|
</a>
|
|
341
442
|
</li>
|
|
342
443
|
{/each}
|
|
343
|
-
</
|
|
344
|
-
{/if}
|
|
345
|
-
{:else}
|
|
346
|
-
<div
|
|
347
|
-
class="rounded-2xl border border-border border-dashed bg-base-50 px-4 py-5"
|
|
348
|
-
>
|
|
349
|
-
<h3 class="text-base font-semibold {getTextToneClass()}">
|
|
350
|
-
{normalizedSearchTerm
|
|
351
|
-
&& searchMode === "input"
|
|
352
|
-
? "No matching navigation items"
|
|
353
|
-
: emptyStateTitle}
|
|
354
|
-
</h3>
|
|
355
|
-
<p class="mt-1 text-sm {getTextToneClass(true)}">
|
|
356
|
-
{normalizedSearchTerm
|
|
357
|
-
&& searchMode === "input"
|
|
358
|
-
? `No results found for "${searchValue}". Try another keyword.`
|
|
359
|
-
: emptyStateDescription}
|
|
360
|
-
</p>
|
|
361
|
-
{#if !(normalizedSearchTerm && searchMode === "input")}
|
|
362
|
-
<button
|
|
363
|
-
type="button"
|
|
364
|
-
class="mt-4 inline-flex min-h-11 items-center rounded-lg bg-action-primary px-3 py-2 text-sm text-action-primary hover:bg-action-primary-hover focus:outline-none focus:ring-2 focus:ring-brand-500 focus:ring-offset-0"
|
|
365
|
-
onclick={handleEmptyStateAction}
|
|
366
|
-
>
|
|
367
|
-
{emptyStateActionLabel}
|
|
368
|
-
</button>
|
|
369
|
-
{/if}
|
|
444
|
+
</SidebarNavSection>
|
|
370
445
|
</div>
|
|
371
446
|
{/if}
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
<div class="flex w-full flex-col gap-2">
|
|
376
|
-
{#if showLogout}
|
|
377
|
-
<button
|
|
378
|
-
type="button"
|
|
379
|
-
class={getControlButtonClasses()}
|
|
380
|
-
onclick={handleLogout}
|
|
447
|
+
{:else}
|
|
448
|
+
<div
|
|
449
|
+
class="rounded-xl border border-border border-dashed bg-transparent px-3.5 py-4 ring-1 ring-border/60"
|
|
381
450
|
>
|
|
382
|
-
<
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
{
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
: 'bg-base-200'}"
|
|
398
|
-
onclick={handleThemeToggle}
|
|
399
|
-
role="switch"
|
|
400
|
-
aria-checked={isLightMode}
|
|
401
|
-
aria-label={isLightMode
|
|
402
|
-
? "Switch to dark mode"
|
|
403
|
-
: "Switch to light mode"}
|
|
404
|
-
>
|
|
405
|
-
<span
|
|
406
|
-
class="flex h-6 w-6 items-center justify-center rounded-xl leading-none shadow-sm transition-transform duration-150 {isLightMode
|
|
407
|
-
? 'translate-x-6 bg-base-900 text-action-primary'
|
|
408
|
-
: 'translate-x-0 bg-base-900 text-base-50'}"
|
|
451
|
+
<h3 class="text-sm font-semibold {getTextToneClass()}">
|
|
452
|
+
{normalizedSearchTerm && searchMode === "input"
|
|
453
|
+
? "No matching navigation items"
|
|
454
|
+
: emptyStateTitle}
|
|
455
|
+
</h3>
|
|
456
|
+
<p class="mt-1 text-sm {getTextToneClass(true)}">
|
|
457
|
+
{normalizedSearchTerm && searchMode === "input"
|
|
458
|
+
? `No results found for "${searchValue}". Try another keyword.`
|
|
459
|
+
: emptyStateDescription}
|
|
460
|
+
</p>
|
|
461
|
+
{#if !(normalizedSearchTerm && searchMode === "input")}
|
|
462
|
+
<button
|
|
463
|
+
type="button"
|
|
464
|
+
class="focus-ring focus-ring--nav mt-3 inline-flex min-h-10 cursor-pointer items-center rounded-lg bg-action-primary px-3 py-2 text-sm font-medium text-action-primary outline-none transition-colors hover:bg-action-primary-hover"
|
|
465
|
+
onclick={handleEmptyStateAction}
|
|
409
466
|
>
|
|
410
|
-
{
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
<Moon size={14} />
|
|
414
|
-
{/if}
|
|
415
|
-
</span>
|
|
416
|
-
</button>
|
|
467
|
+
{emptyStateActionLabel}
|
|
468
|
+
</button>
|
|
469
|
+
{/if}
|
|
417
470
|
</div>
|
|
418
471
|
{/if}
|
|
472
|
+
</div>
|
|
419
473
|
</div>
|
|
474
|
+
|
|
475
|
+
<SidebarFooter
|
|
476
|
+
collapsed={isCollapsed}
|
|
477
|
+
{showProfile}
|
|
478
|
+
{profileName}
|
|
479
|
+
{profileEmail}
|
|
480
|
+
{profileInitials}
|
|
481
|
+
{showLogout}
|
|
482
|
+
{logoutLabel}
|
|
483
|
+
{showThemeToggle}
|
|
484
|
+
{lightModeLabel}
|
|
485
|
+
bind:isLightMode
|
|
486
|
+
{onLogout}
|
|
487
|
+
{onThemeToggle}
|
|
488
|
+
{onProfileClick}
|
|
489
|
+
{profilePanelOpen}
|
|
490
|
+
{profilePanelControlsId}
|
|
491
|
+
{profilePanel}
|
|
492
|
+
className={insetX}
|
|
493
|
+
/>
|
|
420
494
|
</nav>
|