zabi-components 5.0.20 → 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.
Files changed (69) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +355 -81
  3. package/dist/atoms/Button.svelte +1 -1
  4. package/dist/atoms/CardContent.svelte +1 -1
  5. package/dist/atoms/CardHeader.svelte +1 -1
  6. package/dist/atoms/Checkbox.svelte +1 -7
  7. package/dist/atoms/CodeBlock.svelte +6 -6
  8. package/dist/atoms/ColorPicker.svelte +80 -72
  9. package/dist/atoms/IconButton.svelte +88 -0
  10. package/dist/atoms/IconButton.svelte.d.ts +21 -0
  11. package/dist/atoms/Input.svelte +14 -10
  12. package/dist/atoms/Input.svelte.d.ts +2 -0
  13. package/dist/atoms/List.svelte +47 -0
  14. package/dist/atoms/List.svelte.d.ts +19 -0
  15. package/dist/atoms/ListItem.svelte +148 -0
  16. package/dist/atoms/ListItem.svelte.d.ts +35 -0
  17. package/dist/atoms/Progress.svelte +3 -5
  18. package/dist/atoms/Select.svelte +152 -19
  19. package/dist/atoms/Select.svelte.d.ts +23 -1
  20. package/dist/atoms/Textarea.svelte +11 -9
  21. package/dist/atoms/Textarea.svelte.d.ts +1 -0
  22. package/dist/atoms/Toggle.svelte +1 -7
  23. package/dist/atoms/Tooltip.svelte +8 -15
  24. package/dist/atoms/index.d.ts +3 -0
  25. package/dist/atoms/index.js +3 -0
  26. package/dist/components/atoms/index.d.ts +3 -0
  27. package/dist/components/atoms/index.d.ts.map +1 -1
  28. package/dist/components/index.d.ts +4 -0
  29. package/dist/components/index.d.ts.map +1 -1
  30. package/dist/components/molecules/index.d.ts +1 -1
  31. package/dist/components/molecules/navigation-menu-context.d.ts +7 -0
  32. package/dist/components/molecules/navigation-menu-context.d.ts.map +1 -0
  33. package/dist/components/organisms/index.d.ts +2 -0
  34. package/dist/components/organisms/index.d.ts.map +1 -1
  35. package/dist/index.d.ts +4 -0
  36. package/dist/index.js +5 -8
  37. package/dist/molecules/Alert.svelte +8 -3
  38. package/dist/molecules/ContactForm.svelte +63 -31
  39. package/dist/molecules/ContactForm.svelte.d.ts +1 -4
  40. package/dist/molecules/Dropdown.svelte +2 -1
  41. package/dist/molecules/ImageUpload.svelte +26 -3
  42. package/dist/molecules/ImageUpload.svelte.d.ts +1 -0
  43. package/dist/molecules/Modal.svelte +44 -43
  44. package/dist/molecules/NavigationMenu.svelte +7 -10
  45. package/dist/molecules/NavigationMenuContent.svelte +8 -8
  46. package/dist/molecules/NavigationMenuItem.svelte +7 -6
  47. package/dist/molecules/NavigationMenuLink.svelte +7 -5
  48. package/dist/molecules/NavigationMenuList.svelte +0 -7
  49. package/dist/molecules/NavigationMenuTrigger.svelte +7 -7
  50. package/dist/molecules/Section.svelte +158 -0
  51. package/dist/molecules/Section.svelte.d.ts +27 -0
  52. package/dist/molecules/SlideUp.svelte +30 -0
  53. package/dist/molecules/Tabs.svelte +71 -4
  54. package/dist/molecules/index.d.ts +1 -1
  55. package/dist/molecules/index.js +1 -1
  56. package/dist/molecules/navigation-menu-context.d.ts +6 -0
  57. package/dist/molecules/navigation-menu-context.js +1 -0
  58. package/dist/organisms/Navigation.svelte +9 -6
  59. package/dist/organisms/Navigation.svelte.d.ts +1 -0
  60. package/dist/organisms/SidebarNavigation.svelte +420 -0
  61. package/dist/organisms/SidebarNavigation.svelte.d.ts +44 -0
  62. package/dist/organisms/SidebarProjectPanel.svelte +174 -0
  63. package/dist/organisms/SidebarProjectPanel.svelte.d.ts +32 -0
  64. package/dist/organisms/index.d.ts +2 -0
  65. package/dist/organisms/index.js +2 -0
  66. package/dist/zabi-components.css +412 -0
  67. package/package.json +7 -8
  68. package/dist/molecules/Sidebar.svelte +0 -324
  69. package/dist/molecules/Sidebar.svelte.d.ts +0 -30
@@ -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;
@@ -0,0 +1,174 @@
1
+ <script lang="ts">
2
+ import Input from "../atoms/Input.svelte";
3
+ import Badge from "../atoms/Badge.svelte";
4
+ import { Search, X } from "@lucide/svelte";
5
+ import type { Component } from "svelte";
6
+
7
+ export interface SidebarProjectPanelItem {
8
+ id: string;
9
+ label: string;
10
+ icon?: Component<{ size?: number; class?: string }>;
11
+ description?: string;
12
+ badgeText?: string;
13
+ }
14
+
15
+ interface Props {
16
+ className?: string;
17
+ widthClass?: string;
18
+ ariaLabel?: string;
19
+ title?: string;
20
+ subtitle?: string;
21
+ showSearch?: boolean;
22
+ searchPlaceholder?: string;
23
+ searchValue?: string;
24
+ items?: SidebarProjectPanelItem[];
25
+ selectedItemId?: string;
26
+ emptyStateTitle?: string;
27
+ emptyStateDescription?: string;
28
+ selectLabel?: string;
29
+ closeLabel?: string;
30
+ onSelect?: (item: SidebarProjectPanelItem) => void;
31
+ onClose?: () => void;
32
+ }
33
+
34
+ let {
35
+ className = "",
36
+ widthClass = "w-80",
37
+ ariaLabel = "Project picker panel",
38
+ title = "Projects",
39
+ subtitle = "Choose a project to continue",
40
+ showSearch = true,
41
+ searchPlaceholder = "Search projects...",
42
+ searchValue = $bindable(""),
43
+ items = [],
44
+ selectedItemId = $bindable(""),
45
+ emptyStateTitle = "No projects found",
46
+ emptyStateDescription = "Try a different keyword or create a new project.",
47
+ selectLabel = "Select project",
48
+ closeLabel = "Close panel",
49
+ onSelect,
50
+ onClose,
51
+ ...restProps
52
+ }: Props = $props();
53
+
54
+ const normalizedSearchTerm = $derived(searchValue.trim().toLowerCase());
55
+ const filteredItems = $derived(
56
+ normalizedSearchTerm
57
+ ? items.filter((item) =>
58
+ item.label.toLowerCase().includes(normalizedSearchTerm),
59
+ )
60
+ : items,
61
+ );
62
+
63
+ const containerClasses = $derived.by(() => {
64
+ const resolvedWidthClass = widthClass.trim() || "w-80";
65
+ return `${resolvedWidthClass} shrink-0 rounded-2xl border border-border bg-background p-4 ${className}`.trim();
66
+ });
67
+
68
+ function getItemClasses(itemId: string): string {
69
+ const isActive = selectedItemId === itemId;
70
+ const baseClasses =
71
+ "w-full rounded-xl px-3 py-3 text-left transition-colors duration-150 focus:outline-none focus:ring-2 focus:ring-brand-500 focus:ring-offset-0";
72
+ if (isActive) {
73
+ return `${baseClasses} bg-action-primary-subtle text-headline`;
74
+ }
75
+ return `${baseClasses} hover:bg-base-50 text-description`;
76
+ }
77
+
78
+ function handleSelect(item: SidebarProjectPanelItem): void {
79
+ selectedItemId = item.id;
80
+ if (onSelect) {
81
+ onSelect(item);
82
+ }
83
+ }
84
+
85
+ function handleClose(): void {
86
+ if (onClose) {
87
+ onClose();
88
+ }
89
+ }
90
+ </script>
91
+
92
+ <aside class={containerClasses} aria-label={ariaLabel} {...restProps}>
93
+ <div class="mb-4 flex items-start justify-between gap-3">
94
+ <div class="min-w-0">
95
+ <h3 class="truncate text-base font-semibold text-headline">{title}</h3>
96
+ <p class="mt-1 text-sm text-description">{subtitle}</p>
97
+ </div>
98
+ <button
99
+ type="button"
100
+ class="inline-flex size-8 shrink-0 items-center justify-center rounded-md text-description hover:bg-base-50 hover:text-headline focus:outline-none focus:ring-2 focus:ring-brand-500 focus:ring-offset-0"
101
+ aria-label={closeLabel}
102
+ onclick={handleClose}
103
+ >
104
+ <X size={16} />
105
+ </button>
106
+ </div>
107
+
108
+ {#if showSearch}
109
+ <div class="relative mb-3">
110
+ <span
111
+ class="pointer-events-none absolute left-4 top-1/2 z-10 -translate-y-1/2 text-description"
112
+ aria-hidden="true"
113
+ >
114
+ <Search size={18} />
115
+ </span>
116
+ <Input
117
+ type="search"
118
+ bind:value={searchValue}
119
+ placeholder={searchPlaceholder}
120
+ aria-label={searchPlaceholder}
121
+ class="w-full min-w-0 rounded-xl pl-11"
122
+ />
123
+ </div>
124
+ {/if}
125
+
126
+ {#if filteredItems.length > 0}
127
+ <ul class="space-y-2" aria-label={selectLabel}>
128
+ {#each filteredItems as item (item.id)}
129
+ <li>
130
+ <button
131
+ type="button"
132
+ class={getItemClasses(item.id)}
133
+ onclick={() => handleSelect(item)}
134
+ aria-pressed={selectedItemId === item.id}
135
+ >
136
+ <span class="flex items-center justify-between gap-3">
137
+ <span class="flex min-w-0 items-start gap-2">
138
+ {#if item.icon}
139
+ {@const Icon = item.icon}
140
+ <span
141
+ class="mt-0.5 inline-flex size-4 shrink-0 items-center justify-center text-current"
142
+ aria-hidden="true"
143
+ >
144
+ <Icon size={16} />
145
+ </span>
146
+ {/if}
147
+ <span class="min-w-0">
148
+ <span class="block truncate text-sm font-medium text-current">
149
+ {item.label}
150
+ </span>
151
+ {#if item.description}
152
+ <span
153
+ class="mt-0.5 block truncate text-xs text-description"
154
+ >
155
+ {item.description}
156
+ </span>
157
+ {/if}
158
+ </span>
159
+ </span>
160
+ {#if item.badgeText}
161
+ <Badge text={item.badgeText} size="sm" />
162
+ {/if}
163
+ </span>
164
+ </button>
165
+ </li>
166
+ {/each}
167
+ </ul>
168
+ {:else}
169
+ <div class="rounded-xl border border-dashed border-border bg-base-50 p-4">
170
+ <h4 class="text-sm font-semibold text-headline">{emptyStateTitle}</h4>
171
+ <p class="mt-1 text-sm text-description">{emptyStateDescription}</p>
172
+ </div>
173
+ {/if}
174
+ </aside>
@@ -0,0 +1,32 @@
1
+ import type { Component } from "svelte";
2
+ export interface SidebarProjectPanelItem {
3
+ id: string;
4
+ label: string;
5
+ icon?: Component<{
6
+ size?: number;
7
+ class?: string;
8
+ }>;
9
+ description?: string;
10
+ badgeText?: string;
11
+ }
12
+ interface Props {
13
+ className?: string;
14
+ widthClass?: string;
15
+ ariaLabel?: string;
16
+ title?: string;
17
+ subtitle?: string;
18
+ showSearch?: boolean;
19
+ searchPlaceholder?: string;
20
+ searchValue?: string;
21
+ items?: SidebarProjectPanelItem[];
22
+ selectedItemId?: string;
23
+ emptyStateTitle?: string;
24
+ emptyStateDescription?: string;
25
+ selectLabel?: string;
26
+ closeLabel?: string;
27
+ onSelect?: (item: SidebarProjectPanelItem) => void;
28
+ onClose?: () => void;
29
+ }
30
+ declare const SidebarProjectPanel: Component<Props, {}, "searchValue" | "selectedItemId">;
31
+ type SidebarProjectPanel = ReturnType<typeof SidebarProjectPanel>;
32
+ export default SidebarProjectPanel;
@@ -1,2 +1,4 @@
1
1
  export { default as Navbar } from './Navbar.svelte';
2
2
  export { default as Navigation } from './Navigation.svelte';
3
+ export { default as SidebarNavigation } from './SidebarNavigation.svelte';
4
+ export { default as SidebarProjectPanel } from './SidebarProjectPanel.svelte';
@@ -1,2 +1,4 @@
1
1
  export { default as Navbar } from './Navbar.svelte';
2
2
  export { default as Navigation } from './Navigation.svelte';
3
+ export { default as SidebarNavigation } from './SidebarNavigation.svelte';
4
+ export { default as SidebarProjectPanel } from './SidebarProjectPanel.svelte';