zabi-components 5.0.21 → 5.0.22
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 +1 -0
- package/dist/atoms/CardContent.svelte +1 -1
- package/dist/atoms/CardHeader.svelte +1 -1
- package/dist/atoms/Checkbox.svelte +1 -7
- package/dist/atoms/CodeBlock.svelte +6 -6
- package/dist/atoms/ColorPicker.svelte +80 -72
- package/dist/atoms/Input.svelte +14 -10
- package/dist/atoms/Input.svelte.d.ts +2 -0
- package/dist/atoms/List.svelte +47 -0
- package/dist/atoms/List.svelte.d.ts +19 -0
- package/dist/atoms/ListItem.svelte +148 -0
- package/dist/atoms/ListItem.svelte.d.ts +35 -0
- package/dist/atoms/Progress.svelte +3 -5
- package/dist/atoms/Select.svelte +59 -16
- package/dist/atoms/Select.svelte.d.ts +13 -1
- package/dist/atoms/Textarea.svelte +11 -9
- package/dist/atoms/Textarea.svelte.d.ts +1 -0
- package/dist/atoms/Toggle.svelte +1 -7
- package/dist/atoms/Tooltip.svelte +8 -15
- package/dist/atoms/index.d.ts +2 -0
- package/dist/atoms/index.js +2 -0
- package/dist/components/atoms/index.d.ts +2 -0
- package/dist/components/atoms/index.d.ts.map +1 -1
- package/dist/components/index.d.ts +4 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/molecules/index.d.ts +0 -1
- package/dist/components/molecules/index.d.ts.map +1 -1
- package/dist/components/molecules/navigation-menu-context.d.ts +7 -0
- package/dist/components/molecules/navigation-menu-context.d.ts.map +1 -0
- package/dist/components/organisms/index.d.ts +2 -0
- package/dist/components/organisms/index.d.ts.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.js +5 -8
- package/dist/molecules/Alert.svelte +8 -3
- package/dist/molecules/ContactForm.svelte +63 -31
- package/dist/molecules/ContactForm.svelte.d.ts +1 -4
- package/dist/molecules/Dropdown.svelte +2 -1
- package/dist/molecules/ImageUpload.svelte +26 -3
- package/dist/molecules/ImageUpload.svelte.d.ts +1 -0
- package/dist/molecules/Modal.svelte +44 -43
- package/dist/molecules/NavigationMenu.svelte +7 -10
- package/dist/molecules/NavigationMenuContent.svelte +8 -8
- package/dist/molecules/NavigationMenuItem.svelte +7 -6
- package/dist/molecules/NavigationMenuLink.svelte +7 -5
- package/dist/molecules/NavigationMenuList.svelte +0 -7
- package/dist/molecules/NavigationMenuTrigger.svelte +7 -7
- package/dist/molecules/SlideUp.svelte +30 -0
- package/dist/molecules/Tabs.svelte +71 -4
- package/dist/molecules/index.d.ts +0 -1
- package/dist/molecules/index.js +0 -1
- package/dist/molecules/navigation-menu-context.d.ts +6 -0
- package/dist/molecules/navigation-menu-context.js +1 -0
- package/dist/organisms/Navigation.svelte +7 -4
- package/dist/organisms/Navigation.svelte.d.ts +1 -0
- package/dist/organisms/SidebarNavigation.svelte +420 -0
- package/dist/organisms/SidebarNavigation.svelte.d.ts +44 -0
- package/dist/organisms/SidebarProjectPanel.svelte +174 -0
- package/dist/organisms/SidebarProjectPanel.svelte.d.ts +32 -0
- package/dist/organisms/index.d.ts +2 -0
- package/dist/organisms/index.js +2 -0
- package/dist/zabi-components.css +302 -0
- package/package.json +5 -4
- package/dist/molecules/Sidebar.svelte +0 -324
- package/dist/molecules/Sidebar.svelte.d.ts +0 -30
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
function createId(prefix: string): string {
|
|
3
|
+
if (typeof window !== "undefined") {
|
|
4
|
+
return `${prefix}-${Math.random().toString(36).slice(2, 11)}`;
|
|
5
|
+
}
|
|
6
|
+
return `${prefix}-ssr-${Date.now()}`;
|
|
7
|
+
}
|
|
8
|
+
|
|
2
9
|
interface Props {
|
|
3
10
|
tabs?: Array<{
|
|
4
11
|
id: string;
|
|
@@ -19,17 +26,69 @@
|
|
|
19
26
|
...restProps
|
|
20
27
|
}: Props & { children?: any } = $props();
|
|
21
28
|
|
|
29
|
+
const tabsBaseId = createId("tabs");
|
|
30
|
+
|
|
22
31
|
function selectTab(tabId: string) {
|
|
23
32
|
activeTab = tabId;
|
|
24
33
|
}
|
|
25
34
|
|
|
35
|
+
function getEnabledTabs() {
|
|
36
|
+
return tabs.filter((tab) => !tab.disabled);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function getTabIndex(tabId: string) {
|
|
40
|
+
return tabs.findIndex((tab) => tab.id === tabId);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function getTabId(tabId: string) {
|
|
44
|
+
return `${tabsBaseId}-tab-${tabId}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function getPanelId(tabId: string) {
|
|
48
|
+
return `${tabsBaseId}-panel-${tabId}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
$effect(() => {
|
|
52
|
+
if (tabs.length === 0) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const activeExists = tabs.some((tab) => tab.id === activeTab && !tab.disabled);
|
|
57
|
+
if (!activeExists) {
|
|
58
|
+
const firstEnabledTab = tabs.find((tab) => !tab.disabled);
|
|
59
|
+
if (firstEnabledTab) {
|
|
60
|
+
activeTab = firstEnabledTab.id;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
26
65
|
function handleKeydown(event: KeyboardEvent) {
|
|
66
|
+
const enabledTabs = getEnabledTabs();
|
|
67
|
+
if (enabledTabs.length === 0) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const currentIndex = enabledTabs.findIndex((tab) => tab.id === activeTab);
|
|
72
|
+
const fallbackIndex = currentIndex === -1 ? 0 : currentIndex;
|
|
73
|
+
|
|
27
74
|
if (event.key === "ArrowLeft" || event.key === "ArrowRight") {
|
|
28
|
-
|
|
75
|
+
event.preventDefault();
|
|
29
76
|
const direction = event.key === "ArrowLeft" ? -1 : 1;
|
|
30
77
|
const nextIndex =
|
|
31
|
-
(
|
|
32
|
-
|
|
78
|
+
(fallbackIndex + direction + enabledTabs.length) %
|
|
79
|
+
enabledTabs.length;
|
|
80
|
+
selectTab(enabledTabs[nextIndex].id);
|
|
81
|
+
} else if (event.key === "Home") {
|
|
82
|
+
event.preventDefault();
|
|
83
|
+
selectTab(enabledTabs[0].id);
|
|
84
|
+
} else if (event.key === "End") {
|
|
85
|
+
event.preventDefault();
|
|
86
|
+
selectTab(enabledTabs[enabledTabs.length - 1].id);
|
|
87
|
+
} else if (event.key === "Enter" || event.key === " ") {
|
|
88
|
+
event.preventDefault();
|
|
89
|
+
if (currentIndex === -1) {
|
|
90
|
+
selectTab(enabledTabs[0].id);
|
|
91
|
+
}
|
|
33
92
|
}
|
|
34
93
|
}
|
|
35
94
|
</script>
|
|
@@ -45,6 +104,7 @@
|
|
|
45
104
|
<button
|
|
46
105
|
type="button"
|
|
47
106
|
role="tab"
|
|
107
|
+
id={getTabId(tab.id)}
|
|
48
108
|
class="px-4 py-2 text-sm font-medium border-b-2 transition-colors {activeTab ===
|
|
49
109
|
tab.id
|
|
50
110
|
? variant === 'pills'
|
|
@@ -54,13 +114,20 @@
|
|
|
54
114
|
onclick={() => selectTab(tab.id)}
|
|
55
115
|
disabled={tab.disabled}
|
|
56
116
|
aria-selected={activeTab === tab.id}
|
|
117
|
+
aria-controls={getPanelId(tab.id)}
|
|
118
|
+
tabindex={activeTab === tab.id ? 0 : -1}
|
|
57
119
|
>
|
|
58
120
|
{tab.label}
|
|
59
121
|
</button>
|
|
60
122
|
{/each}
|
|
61
123
|
</div>
|
|
62
124
|
|
|
63
|
-
<div
|
|
125
|
+
<div
|
|
126
|
+
class="mt-4"
|
|
127
|
+
role="tabpanel"
|
|
128
|
+
id={getPanelId(activeTab)}
|
|
129
|
+
aria-labelledby={getTabId(activeTab)}
|
|
130
|
+
>
|
|
64
131
|
{@render children?.({ activeTab })}
|
|
65
132
|
</div>
|
|
66
133
|
</div>
|
|
@@ -12,6 +12,5 @@ export { default as NavigationMenuTrigger } from './NavigationMenuTrigger.svelte
|
|
|
12
12
|
export { default as NavigationMenuContent } from './NavigationMenuContent.svelte';
|
|
13
13
|
export { default as NavigationMenuLink } from './NavigationMenuLink.svelte';
|
|
14
14
|
export { default as Section } from './Section.svelte';
|
|
15
|
-
export { default as Sidebar } from './Sidebar.svelte';
|
|
16
15
|
export { default as SlideUp } from './SlideUp.svelte';
|
|
17
16
|
export { default as Tabs } from './Tabs.svelte';
|
package/dist/molecules/index.js
CHANGED
|
@@ -12,6 +12,5 @@ export { default as NavigationMenuTrigger } from './NavigationMenuTrigger.svelte
|
|
|
12
12
|
export { default as NavigationMenuContent } from './NavigationMenuContent.svelte';
|
|
13
13
|
export { default as NavigationMenuLink } from './NavigationMenuLink.svelte';
|
|
14
14
|
export { default as Section } from './Section.svelte';
|
|
15
|
-
export { default as Sidebar } from './Sidebar.svelte';
|
|
16
15
|
export { default as SlideUp } from './SlideUp.svelte';
|
|
17
16
|
export { default as Tabs } from './Tabs.svelte';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const NAVIGATION_MENU_CONTEXT_KEY = Symbol("navigation-menu");
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
variant?: "header" | "sidebar";
|
|
13
13
|
items?: NavigationItem[];
|
|
14
14
|
currentPath?: string;
|
|
15
|
+
preventNavigation?: boolean;
|
|
15
16
|
onclick?: (event: Event) => void;
|
|
16
17
|
className?: string;
|
|
17
18
|
}
|
|
@@ -20,13 +21,16 @@
|
|
|
20
21
|
variant = "header",
|
|
21
22
|
items = [],
|
|
22
23
|
currentPath = "",
|
|
24
|
+
preventNavigation = false,
|
|
23
25
|
onclick,
|
|
24
26
|
className = "",
|
|
25
27
|
...restProps
|
|
26
28
|
}: Props & { children?: any } = $props();
|
|
27
29
|
|
|
28
|
-
function handleClick(
|
|
29
|
-
|
|
30
|
+
function handleClick(event: MouseEvent) {
|
|
31
|
+
if (preventNavigation) {
|
|
32
|
+
event.preventDefault();
|
|
33
|
+
}
|
|
30
34
|
if (onclick) {
|
|
31
35
|
onclick(event as any);
|
|
32
36
|
}
|
|
@@ -88,8 +92,7 @@
|
|
|
88
92
|
<a
|
|
89
93
|
href={item.href}
|
|
90
94
|
class={iconContainerClasses}
|
|
91
|
-
onclick={
|
|
92
|
-
role="button"
|
|
95
|
+
onclick={handleClick}
|
|
93
96
|
aria-current={isActive ? "page" : undefined}
|
|
94
97
|
>
|
|
95
98
|
<div class={getStateLayerClasses()}>
|
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import Badge from "../atoms/Badge.svelte";
|
|
3
|
+
import Input from "../atoms/Input.svelte";
|
|
4
|
+
import { Search, LogOut, Sun, Moon } from "@lucide/svelte";
|
|
5
|
+
import type { Component } from "svelte";
|
|
6
|
+
|
|
7
|
+
export interface SidebarNavigationItem {
|
|
8
|
+
id: string;
|
|
9
|
+
label: string;
|
|
10
|
+
href: string;
|
|
11
|
+
icon?: Component<{ size?: number; class?: string }>;
|
|
12
|
+
badgeText?: string | number;
|
|
13
|
+
badgeCount?: number;
|
|
14
|
+
group?: "primary" | "secondary";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface Props {
|
|
18
|
+
mode?: "expanded" | "collapsed";
|
|
19
|
+
items?: SidebarNavigationItem[];
|
|
20
|
+
currentPath?: string;
|
|
21
|
+
ariaLabel?: string;
|
|
22
|
+
className?: string;
|
|
23
|
+
showProfile?: boolean;
|
|
24
|
+
profileName?: string;
|
|
25
|
+
profileEmail?: string;
|
|
26
|
+
profileInitials?: string;
|
|
27
|
+
showSearch?: boolean;
|
|
28
|
+
searchMode?: "input" | "button";
|
|
29
|
+
searchPlaceholder?: string;
|
|
30
|
+
searchValue?: string;
|
|
31
|
+
showLogout?: boolean;
|
|
32
|
+
logoutLabel?: string;
|
|
33
|
+
showThemeToggle?: boolean;
|
|
34
|
+
lightModeLabel?: string;
|
|
35
|
+
isLightMode?: boolean;
|
|
36
|
+
emptyStateTitle?: string;
|
|
37
|
+
emptyStateDescription?: string;
|
|
38
|
+
emptyStateActionLabel?: string;
|
|
39
|
+
onNavigate?: (item: SidebarNavigationItem, event: MouseEvent) => void;
|
|
40
|
+
onSearchClick?: () => void;
|
|
41
|
+
onLogout?: () => void;
|
|
42
|
+
onThemeToggle?: (nextIsLightMode: boolean) => void;
|
|
43
|
+
onEmptyStateAction?: () => void;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let {
|
|
47
|
+
mode = "expanded",
|
|
48
|
+
items = [],
|
|
49
|
+
currentPath = "",
|
|
50
|
+
ariaLabel = "Sidebar navigation",
|
|
51
|
+
className = "",
|
|
52
|
+
showProfile = true,
|
|
53
|
+
profileName = "Zabi",
|
|
54
|
+
profileEmail = "hello@zabi.dev",
|
|
55
|
+
profileInitials = "ZA",
|
|
56
|
+
showSearch = true,
|
|
57
|
+
searchMode = "input",
|
|
58
|
+
searchPlaceholder = "Search...",
|
|
59
|
+
searchValue = $bindable(""),
|
|
60
|
+
showLogout = true,
|
|
61
|
+
logoutLabel = "Logout",
|
|
62
|
+
showThemeToggle = true,
|
|
63
|
+
lightModeLabel = "Light mode",
|
|
64
|
+
isLightMode = $bindable(false),
|
|
65
|
+
emptyStateTitle = "Create your first navigation item",
|
|
66
|
+
emptyStateDescription = "Add your first sidebar item so users can start navigating your product.",
|
|
67
|
+
emptyStateActionLabel = "Add navigation item",
|
|
68
|
+
onNavigate,
|
|
69
|
+
onSearchClick,
|
|
70
|
+
onLogout,
|
|
71
|
+
onThemeToggle,
|
|
72
|
+
onEmptyStateAction,
|
|
73
|
+
...restProps
|
|
74
|
+
}: Props = $props();
|
|
75
|
+
|
|
76
|
+
const isCollapsed = $derived(mode === "collapsed");
|
|
77
|
+
const normalizedSearchTerm = $derived(searchValue.trim().toLowerCase());
|
|
78
|
+
const primaryItems = $derived(
|
|
79
|
+
items.filter((item) => item.group !== "secondary"),
|
|
80
|
+
);
|
|
81
|
+
const secondaryItems = $derived(
|
|
82
|
+
items.filter((item) => item.group === "secondary"),
|
|
83
|
+
);
|
|
84
|
+
const filteredPrimaryItems = $derived(
|
|
85
|
+
searchMode === "input" && normalizedSearchTerm
|
|
86
|
+
? primaryItems.filter((item) =>
|
|
87
|
+
item.label.toLowerCase().includes(normalizedSearchTerm),
|
|
88
|
+
)
|
|
89
|
+
: primaryItems,
|
|
90
|
+
);
|
|
91
|
+
const filteredSecondaryItems = $derived(
|
|
92
|
+
searchMode === "input" && normalizedSearchTerm
|
|
93
|
+
? secondaryItems.filter((item) =>
|
|
94
|
+
item.label.toLowerCase().includes(normalizedSearchTerm),
|
|
95
|
+
)
|
|
96
|
+
: secondaryItems,
|
|
97
|
+
);
|
|
98
|
+
const hasFilteredItems = $derived(
|
|
99
|
+
filteredPrimaryItems.length + filteredSecondaryItems.length > 0,
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
const containerClasses = $derived.by(() => {
|
|
103
|
+
const widthClass = isCollapsed ? "w-[104px]" : "w-[266px]";
|
|
104
|
+
const surfaceClasses = "bg-background border-border text-headline";
|
|
105
|
+
const baseClasses =
|
|
106
|
+
"h-full min-h-[640px] border-r flex flex-col justify-between px-6 py-6";
|
|
107
|
+
|
|
108
|
+
return `${baseClasses} ${widthClass} ${surfaceClasses} ${className}`.trim();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const avatarClasses = $derived(
|
|
112
|
+
"size-14 rounded-2xl bg-action-primary text-action-primary flex items-center justify-center font-semibold text-base shrink-0",
|
|
113
|
+
);
|
|
114
|
+
|
|
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
|
+
const iconContainerClasses = $derived(
|
|
124
|
+
"flex size-6 shrink-0 items-center justify-center leading-none",
|
|
125
|
+
);
|
|
126
|
+
const shouldRenderSearchButton = $derived(
|
|
127
|
+
isCollapsed || searchMode === "button" || Boolean(onSearchClick),
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
function getTextToneClass(isMuted = false): string {
|
|
131
|
+
return isMuted ? "text-description" : "text-headline";
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function getNavItemClasses(item: SidebarNavigationItem): string {
|
|
135
|
+
const isActive = currentPath === item.href;
|
|
136
|
+
const baseClasses =
|
|
137
|
+
"w-full rounded-lg transition-colors duration-150 focus:outline-none focus:ring-2 focus:ring-brand-500 focus:ring-offset-0";
|
|
138
|
+
const layoutClasses = isCollapsed
|
|
139
|
+
? "flex min-h-11 items-center justify-center px-0 py-2.5"
|
|
140
|
+
: "flex min-h-11 items-center gap-3 px-4 py-2.5";
|
|
141
|
+
|
|
142
|
+
if (isActive) {
|
|
143
|
+
return `${baseClasses} ${layoutClasses} bg-action-primary-subtle text-headline`;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return `${baseClasses} ${layoutClasses} text-description hover:bg-base-50 hover:text-headline`;
|
|
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}`;
|
|
157
|
+
}
|
|
158
|
+
|
|
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
|
+
function handleNavigate(item: SidebarNavigationItem, event: MouseEvent) {
|
|
166
|
+
if (onNavigate) {
|
|
167
|
+
onNavigate(item, event);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function handleSearchClick() {
|
|
172
|
+
if (onSearchClick) {
|
|
173
|
+
onSearchClick();
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
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
|
+
function handleEmptyStateAction() {
|
|
191
|
+
if (onEmptyStateAction) {
|
|
192
|
+
onEmptyStateAction();
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function getItemBadgeText(item: SidebarNavigationItem): string | null {
|
|
197
|
+
if (item.badgeText !== undefined) {
|
|
198
|
+
return String(item.badgeText);
|
|
199
|
+
}
|
|
200
|
+
if (item.badgeCount !== undefined) {
|
|
201
|
+
return String(item.badgeCount);
|
|
202
|
+
}
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
</script>
|
|
206
|
+
|
|
207
|
+
<nav class={containerClasses} aria-label={ariaLabel} {...restProps}>
|
|
208
|
+
<div class="flex w-full flex-col gap-11">
|
|
209
|
+
{#if showProfile}
|
|
210
|
+
<div class="flex w-full items-center gap-3">
|
|
211
|
+
<div class={avatarClasses} aria-hidden="true">
|
|
212
|
+
{profileInitials}
|
|
213
|
+
</div>
|
|
214
|
+
{#if !isCollapsed}
|
|
215
|
+
<div class="min-w-0">
|
|
216
|
+
<p
|
|
217
|
+
class="truncate text-base font-bold {getTextToneClass()}"
|
|
218
|
+
>
|
|
219
|
+
{profileName}
|
|
220
|
+
</p>
|
|
221
|
+
<p class="truncate text-sm {getTextToneClass(true)}">
|
|
222
|
+
{profileEmail}
|
|
223
|
+
</p>
|
|
224
|
+
</div>
|
|
225
|
+
{/if}
|
|
226
|
+
</div>
|
|
227
|
+
{/if}
|
|
228
|
+
|
|
229
|
+
<div class="flex w-full flex-col gap-6">
|
|
230
|
+
{#if showSearch}
|
|
231
|
+
{#if shouldRenderSearchButton}
|
|
232
|
+
<button
|
|
233
|
+
type="button"
|
|
234
|
+
class={searchTriggerClasses}
|
|
235
|
+
onclick={handleSearchClick}
|
|
236
|
+
aria-label={searchPlaceholder}
|
|
237
|
+
>
|
|
238
|
+
<span class="truncate text-base {getTextToneClass(true)}">
|
|
239
|
+
{searchValue || searchPlaceholder}
|
|
240
|
+
</span>
|
|
241
|
+
</button>
|
|
242
|
+
{:else}
|
|
243
|
+
<div class="relative w-full">
|
|
244
|
+
<span
|
|
245
|
+
class="pointer-events-none absolute left-4 top-1/2 -translate-y-1/2 z-10"
|
|
246
|
+
aria-hidden="true"
|
|
247
|
+
>
|
|
248
|
+
<Search size={20} class={getTextToneClass(true)} />
|
|
249
|
+
</span>
|
|
250
|
+
<Input
|
|
251
|
+
type="search"
|
|
252
|
+
bind:value={searchValue}
|
|
253
|
+
placeholder={searchPlaceholder}
|
|
254
|
+
aria-label={searchPlaceholder}
|
|
255
|
+
class="w-full min-w-0 rounded-2xl pl-12"
|
|
256
|
+
/>
|
|
257
|
+
</div>
|
|
258
|
+
{/if}
|
|
259
|
+
{/if}
|
|
260
|
+
|
|
261
|
+
{#if hasFilteredItems}
|
|
262
|
+
<ul
|
|
263
|
+
class="flex w-full flex-col gap-2"
|
|
264
|
+
aria-label="Primary navigation"
|
|
265
|
+
>
|
|
266
|
+
{#each filteredPrimaryItems as item (item.id)}
|
|
267
|
+
{@const Icon = item.icon}
|
|
268
|
+
<li>
|
|
269
|
+
<a
|
|
270
|
+
href={item.href}
|
|
271
|
+
class={getNavItemClasses(item)}
|
|
272
|
+
onclick={(event) => handleNavigate(item, event)}
|
|
273
|
+
aria-current={currentPath === item.href
|
|
274
|
+
? "page"
|
|
275
|
+
: undefined}
|
|
276
|
+
aria-label={isCollapsed
|
|
277
|
+
? item.label
|
|
278
|
+
: undefined}
|
|
279
|
+
>
|
|
280
|
+
{#if Icon}
|
|
281
|
+
<span
|
|
282
|
+
class={iconContainerClasses}
|
|
283
|
+
aria-hidden="true"
|
|
284
|
+
>
|
|
285
|
+
<Icon size={20} />
|
|
286
|
+
</span>
|
|
287
|
+
{/if}
|
|
288
|
+
{#if !isCollapsed}
|
|
289
|
+
<span class="text-base leading-snug"
|
|
290
|
+
>{item.label}</span
|
|
291
|
+
>
|
|
292
|
+
{/if}
|
|
293
|
+
{#if !isCollapsed && getItemBadgeText(item) !== null}
|
|
294
|
+
<span class="ml-auto">
|
|
295
|
+
<Badge
|
|
296
|
+
variant="default"
|
|
297
|
+
size="sm"
|
|
298
|
+
text={getItemBadgeText(item) ?? ""}
|
|
299
|
+
/>
|
|
300
|
+
</span>
|
|
301
|
+
{/if}
|
|
302
|
+
</a>
|
|
303
|
+
</li>
|
|
304
|
+
{/each}
|
|
305
|
+
</ul>
|
|
306
|
+
|
|
307
|
+
{#if filteredSecondaryItems.length > 0}
|
|
308
|
+
<ul
|
|
309
|
+
class="flex w-full flex-col gap-3"
|
|
310
|
+
aria-label="Secondary navigation"
|
|
311
|
+
>
|
|
312
|
+
{#each filteredSecondaryItems as item (item.id)}
|
|
313
|
+
{@const Icon = item.icon}
|
|
314
|
+
<li>
|
|
315
|
+
<a
|
|
316
|
+
href={item.href}
|
|
317
|
+
class={getNavItemClasses(item)}
|
|
318
|
+
onclick={(event) =>
|
|
319
|
+
handleNavigate(item, event)}
|
|
320
|
+
aria-current={currentPath === item.href
|
|
321
|
+
? "page"
|
|
322
|
+
: undefined}
|
|
323
|
+
aria-label={isCollapsed
|
|
324
|
+
? item.label
|
|
325
|
+
: undefined}
|
|
326
|
+
>
|
|
327
|
+
{#if Icon}
|
|
328
|
+
<span
|
|
329
|
+
class={iconContainerClasses}
|
|
330
|
+
aria-hidden="true"
|
|
331
|
+
>
|
|
332
|
+
<Icon size={20} />
|
|
333
|
+
</span>
|
|
334
|
+
{/if}
|
|
335
|
+
{#if !isCollapsed}
|
|
336
|
+
<span class="text-base leading-snug"
|
|
337
|
+
>{item.label}</span
|
|
338
|
+
>
|
|
339
|
+
{/if}
|
|
340
|
+
</a>
|
|
341
|
+
</li>
|
|
342
|
+
{/each}
|
|
343
|
+
</ul>
|
|
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}
|
|
370
|
+
</div>
|
|
371
|
+
{/if}
|
|
372
|
+
</div>
|
|
373
|
+
</div>
|
|
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}
|
|
381
|
+
>
|
|
382
|
+
<span class={iconContainerClasses} aria-hidden="true">
|
|
383
|
+
<LogOut size={20} />
|
|
384
|
+
</span>
|
|
385
|
+
{#if !isCollapsed}
|
|
386
|
+
<span class="text-base">{logoutLabel}</span>
|
|
387
|
+
{/if}
|
|
388
|
+
</button>
|
|
389
|
+
{/if}
|
|
390
|
+
|
|
391
|
+
{#if showThemeToggle}
|
|
392
|
+
<div class={themeToggleContainerClasses}>
|
|
393
|
+
<button
|
|
394
|
+
type="button"
|
|
395
|
+
class="inline-flex h-8 w-14 items-center rounded-2xl p-1 transition-colors duration-150 focus:outline-none focus:ring-2 focus:ring-brand-500 focus:ring-offset-0 {isLightMode
|
|
396
|
+
? 'bg-action-primary'
|
|
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'}"
|
|
409
|
+
>
|
|
410
|
+
{#if isLightMode}
|
|
411
|
+
<Sun size={14} />
|
|
412
|
+
{:else}
|
|
413
|
+
<Moon size={14} />
|
|
414
|
+
{/if}
|
|
415
|
+
</span>
|
|
416
|
+
</button>
|
|
417
|
+
</div>
|
|
418
|
+
{/if}
|
|
419
|
+
</div>
|
|
420
|
+
</nav>
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { Component } from "svelte";
|
|
2
|
+
export interface SidebarNavigationItem {
|
|
3
|
+
id: string;
|
|
4
|
+
label: string;
|
|
5
|
+
href: string;
|
|
6
|
+
icon?: Component<{
|
|
7
|
+
size?: number;
|
|
8
|
+
class?: string;
|
|
9
|
+
}>;
|
|
10
|
+
badgeText?: string | number;
|
|
11
|
+
badgeCount?: number;
|
|
12
|
+
group?: "primary" | "secondary";
|
|
13
|
+
}
|
|
14
|
+
interface Props {
|
|
15
|
+
mode?: "expanded" | "collapsed";
|
|
16
|
+
items?: SidebarNavigationItem[];
|
|
17
|
+
currentPath?: string;
|
|
18
|
+
ariaLabel?: string;
|
|
19
|
+
className?: string;
|
|
20
|
+
showProfile?: boolean;
|
|
21
|
+
profileName?: string;
|
|
22
|
+
profileEmail?: string;
|
|
23
|
+
profileInitials?: string;
|
|
24
|
+
showSearch?: boolean;
|
|
25
|
+
searchMode?: "input" | "button";
|
|
26
|
+
searchPlaceholder?: string;
|
|
27
|
+
searchValue?: string;
|
|
28
|
+
showLogout?: boolean;
|
|
29
|
+
logoutLabel?: string;
|
|
30
|
+
showThemeToggle?: boolean;
|
|
31
|
+
lightModeLabel?: string;
|
|
32
|
+
isLightMode?: boolean;
|
|
33
|
+
emptyStateTitle?: string;
|
|
34
|
+
emptyStateDescription?: string;
|
|
35
|
+
emptyStateActionLabel?: string;
|
|
36
|
+
onNavigate?: (item: SidebarNavigationItem, event: MouseEvent) => void;
|
|
37
|
+
onSearchClick?: () => void;
|
|
38
|
+
onLogout?: () => void;
|
|
39
|
+
onThemeToggle?: (nextIsLightMode: boolean) => void;
|
|
40
|
+
onEmptyStateAction?: () => void;
|
|
41
|
+
}
|
|
42
|
+
declare const SidebarNavigation: Component<Props, {}, "searchValue" | "isLightMode">;
|
|
43
|
+
type SidebarNavigation = ReturnType<typeof SidebarNavigation>;
|
|
44
|
+
export default SidebarNavigation;
|