zabi-components 5.0.22 → 6.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.
Files changed (59) hide show
  1. package/README.md +41 -44
  2. package/THEME.md +473 -0
  3. package/THEMING.md +248 -0
  4. package/dist/atoms/Card.svelte +40 -67
  5. package/dist/atoms/Card.svelte.d.ts +3 -3
  6. package/dist/atoms/CardHeader.svelte +10 -1
  7. package/dist/atoms/CardHeader.svelte.d.ts +3 -0
  8. package/dist/atoms/CodeBlock.svelte +1 -1
  9. package/dist/atoms/ColorPicker.svelte +1 -1
  10. package/dist/atoms/Input.svelte +2 -2
  11. package/dist/atoms/Progress.svelte +1 -1
  12. package/dist/atoms/Select.svelte +7 -5
  13. package/dist/atoms/Textarea.svelte +2 -2
  14. package/dist/atoms/Toast.svelte +1 -1
  15. package/dist/components/index.d.ts +1 -2
  16. package/dist/components/index.d.ts.map +1 -1
  17. package/dist/components/organisms/index.d.ts +1 -2
  18. package/dist/components/organisms/index.d.ts.map +1 -1
  19. package/dist/index.d.ts +1 -2
  20. package/dist/index.js +1 -2
  21. package/dist/lib/showcase/components-catalog.d.ts +3 -0
  22. package/dist/lib/showcase/components-catalog.d.ts.map +1 -0
  23. package/dist/lib/showcase/components-showcase-constants.d.ts +63 -0
  24. package/dist/lib/showcase/components-showcase-constants.d.ts.map +1 -0
  25. package/dist/lib/showcase/docs-sidebar-helpers.d.ts +15 -0
  26. package/dist/lib/showcase/docs-sidebar-helpers.d.ts.map +1 -0
  27. package/dist/molecules/Alert.svelte +1 -1
  28. package/dist/molecules/ComponentDemo.svelte +53 -50
  29. package/dist/molecules/ContactForm.svelte +69 -64
  30. package/dist/molecules/Dropdown.svelte +2 -1
  31. package/dist/molecules/ImageUpload.svelte +1 -1
  32. package/dist/molecules/Modal.svelte +2 -2
  33. package/dist/molecules/NavigationMenuContent.svelte +1 -1
  34. package/dist/molecules/NavigationMenuLink.svelte +1 -1
  35. package/dist/molecules/NavigationMenuTrigger.svelte +1 -4
  36. package/dist/molecules/Section.svelte +12 -12
  37. package/dist/molecules/SlideUp.svelte +2 -2
  38. package/dist/molecules/Tabs.svelte +3 -3
  39. package/dist/organisms/Navbar.svelte +170 -53
  40. package/dist/organisms/Navbar.svelte.d.ts +31 -4
  41. package/dist/organisms/SidebarNavigation.svelte +209 -165
  42. package/dist/organisms/SidebarNavigation.svelte.d.ts +5 -0
  43. package/dist/organisms/{SidebarProjectPanel.svelte → SidebarPanel.svelte} +18 -17
  44. package/dist/organisms/{SidebarProjectPanel.svelte.d.ts → SidebarPanel.svelte.d.ts} +6 -6
  45. package/dist/organisms/index.d.ts +1 -2
  46. package/dist/organisms/index.js +1 -2
  47. package/dist/types/page.types.d.ts +32 -0
  48. package/dist/types/page.types.d.ts.map +1 -0
  49. package/dist/types/variants.ts +76 -0
  50. package/dist/zabi-components-colors.css +347 -324
  51. package/dist/zabi-components-theme-dark-only.css +159 -151
  52. package/dist/zabi-components-theme-dark.css +159 -151
  53. package/dist/zabi-components-theme-only.css +245 -230
  54. package/dist/zabi-components-theme.css +245 -230
  55. package/dist/zabi-components.css +437 -101
  56. package/docs/theme-imports.md +112 -0
  57. package/package.json +16 -6
  58. package/dist/organisms/Navigation.svelte +0 -119
  59. package/dist/organisms/Navigation.svelte.d.ts +0 -21
@@ -41,12 +41,18 @@
41
41
  onLogout?: () => void;
42
42
  onThemeToggle?: (nextIsLightMode: boolean) => void;
43
43
  onEmptyStateAction?: () => void;
44
+ /**
45
+ * Highlights a primary (e.g. category) row when the current route is a child
46
+ * of that section, while `currentPath` points at the leaf (e.g. a component).
47
+ */
48
+ activePrimaryHref?: string;
44
49
  }
45
50
 
46
51
  let {
47
52
  mode = "expanded",
48
53
  items = [],
49
54
  currentPath = "",
55
+ activePrimaryHref = "",
50
56
  ariaLabel = "Sidebar navigation",
51
57
  className = "",
52
58
  showProfile = true,
@@ -81,17 +87,31 @@
81
87
  const secondaryItems = $derived(
82
88
  items.filter((item) => item.group === "secondary"),
83
89
  );
90
+ function itemMatchesSearch(item: SidebarNavigationItem): boolean {
91
+ return item.label.toLowerCase().includes(normalizedSearchTerm);
92
+ }
93
+
94
+ /** Keep the current (and section) row visible even when it does not match the filter. */
95
+ function itemPinnedBySelection(item: SidebarNavigationItem): boolean {
96
+ if (currentPath === item.href) {
97
+ return true;
98
+ }
99
+ return Boolean(activePrimaryHref) && item.href === activePrimaryHref;
100
+ }
101
+
84
102
  const filteredPrimaryItems = $derived(
85
103
  searchMode === "input" && normalizedSearchTerm
86
- ? primaryItems.filter((item) =>
87
- item.label.toLowerCase().includes(normalizedSearchTerm),
104
+ ? primaryItems.filter(
105
+ (item) =>
106
+ itemMatchesSearch(item) || itemPinnedBySelection(item),
88
107
  )
89
108
  : primaryItems,
90
109
  );
91
110
  const filteredSecondaryItems = $derived(
92
111
  searchMode === "input" && normalizedSearchTerm
93
- ? secondaryItems.filter((item) =>
94
- item.label.toLowerCase().includes(normalizedSearchTerm),
112
+ ? secondaryItems.filter(
113
+ (item) =>
114
+ itemMatchesSearch(item) || itemPinnedBySelection(item),
95
115
  )
96
116
  : secondaryItems,
97
117
  );
@@ -103,7 +123,7 @@
103
123
  const widthClass = isCollapsed ? "w-[104px]" : "w-[266px]";
104
124
  const surfaceClasses = "bg-background border-border text-headline";
105
125
  const baseClasses =
106
- "h-full min-h-[640px] border-r flex flex-col justify-between px-6 py-6";
126
+ "flex h-full min-h-0 max-h-full flex-col overflow-hidden border-r py-6";
107
127
 
108
128
  return `${baseClasses} ${widthClass} ${surfaceClasses} ${className}`.trim();
109
129
  });
@@ -112,16 +132,20 @@
112
132
  "size-14 rounded-2xl bg-action-primary text-action-primary flex items-center justify-center font-semibold text-base shrink-0",
113
133
  );
114
134
 
135
+ /** Search trigger (collapsed icon strip or button mode): matches nav item interaction tokens. */
136
+ const searchControlStates =
137
+ "text-nav-menu-item outline-none transition-colors duration-150 hover:bg-nav-menu-hover hover:text-nav-menu-item-hover active:bg-base-200 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-nav-menu-focus";
138
+
115
139
  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",
140
+ `flex min-h-11 w-full cursor-pointer items-center justify-center rounded-2xl border border-input-border bg-input px-0 py-2.5 ${searchControlStates}`,
117
141
  );
118
142
  const searchTriggerClasses = $derived.by(() => {
119
143
  return isCollapsed
120
144
  ? 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";
145
+ : `flex min-h-11 w-full cursor-pointer items-center gap-3 rounded-2xl border border-input-border bg-input px-4 py-2.5 text-left ${searchControlStates}`;
122
146
  });
123
147
  const iconContainerClasses = $derived(
124
- "flex size-6 shrink-0 items-center justify-center leading-none",
148
+ "flex size-6 shrink-0 items-center justify-center leading-none text-current",
125
149
  );
126
150
  const shouldRenderSearchButton = $derived(
127
151
  isCollapsed || searchMode === "button" || Boolean(onSearchClick),
@@ -132,23 +156,26 @@
132
156
  }
133
157
 
134
158
  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";
159
+ const isActive =
160
+ currentPath === item.href ||
161
+ (Boolean(activePrimaryHref) && item.href === activePrimaryHref);
138
162
  const layoutClasses = isCollapsed
139
163
  ? "flex min-h-11 items-center justify-center px-0 py-2.5"
140
164
  : "flex min-h-11 items-center gap-3 px-4 py-2.5";
165
+ const structural =
166
+ "w-full cursor-pointer rounded-lg no-underline transition-colors duration-150 outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-nav-menu-focus";
141
167
 
142
168
  if (isActive) {
143
- return `${baseClasses} ${layoutClasses} bg-action-primary-subtle text-headline`;
169
+ /* Current page: keep active surface on hover (do not apply inactive hover bg). */
170
+ return `${structural} ${layoutClasses} bg-nav-menu-active text-inherit hover:bg-nav-menu-active hover:text-inherit active:opacity-90`;
144
171
  }
145
172
 
146
- return `${baseClasses} ${layoutClasses} text-description hover:bg-base-50 hover:text-headline`;
173
+ return `${structural} ${layoutClasses} text-nav-menu-item hover:bg-nav-menu-hover hover:text-nav-menu-item-hover active:bg-base-200`;
147
174
  }
148
175
 
149
176
  function getControlButtonClasses(): string {
150
177
  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";
178
+ "w-full cursor-pointer rounded-lg text-nav-menu-item transition-colors duration-150 outline-none hover:bg-nav-menu-hover hover:text-nav-menu-item-hover active:bg-base-200 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-nav-menu-focus";
152
179
  const layoutClasses = isCollapsed
153
180
  ? "flex min-h-11 items-center justify-center px-0 py-2.5"
154
181
  : "flex min-h-11 items-center gap-3 px-4 py-2.5";
@@ -202,10 +229,22 @@
202
229
  }
203
230
  return null;
204
231
  }
232
+
233
+ function getAriaCurrent(
234
+ item: SidebarNavigationItem,
235
+ ): "page" | "location" | undefined {
236
+ if (currentPath === item.href) {
237
+ return "page";
238
+ }
239
+ if (activePrimaryHref && item.href === activePrimaryHref) {
240
+ return "location";
241
+ }
242
+ return undefined;
243
+ }
205
244
  </script>
206
245
 
207
246
  <nav class={containerClasses} aria-label={ariaLabel} {...restProps}>
208
- <div class="flex w-full flex-col gap-11">
247
+ <div class="flex w-full shrink-0 flex-col gap-6 px-2">
209
248
  {#if showProfile}
210
249
  <div class="flex w-full items-center gap-3">
211
250
  <div class={avatarClasses} aria-hidden="true">
@@ -226,53 +265,101 @@
226
265
  </div>
227
266
  {/if}
228
267
 
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}
268
+ {#if showSearch}
269
+ {#if shouldRenderSearchButton}
270
+ <button
271
+ type="button"
272
+ class={searchTriggerClasses}
273
+ onclick={handleSearchClick}
274
+ aria-label={searchPlaceholder}
275
+ >
276
+ <span
277
+ class="truncate text-base text-inherit transition-colors"
237
278
  >
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}
279
+ {searchValue || searchPlaceholder}
280
+ </span>
281
+ </button>
282
+ {:else}
283
+ <div class="relative w-full">
284
+ <span
285
+ class="pointer-events-none absolute left-4 top-1/2 z-10 -translate-y-1/2"
286
+ aria-hidden="true"
287
+ >
288
+ <Search size={20} class={getTextToneClass(true)} />
289
+ </span>
290
+ <Input
291
+ type="search"
292
+ bind:value={searchValue}
293
+ placeholder={searchPlaceholder}
294
+ aria-label={searchPlaceholder}
295
+ class="w-full min-w-0 rounded-2xl pl-12"
296
+ />
297
+ </div>
259
298
  {/if}
299
+ {/if}
300
+ </div>
301
+
302
+ <div
303
+ class="flex min-h-0 flex-1 w-full flex-col gap-6 overflow-y-auto overflow-x-hidden overscroll-y-contain py-1"
304
+ role="region"
305
+ aria-label="Navigation links"
306
+ >
307
+ {#if hasFilteredItems}
308
+ <ul
309
+ class="flex w-full flex-col gap-2 px-2"
310
+ aria-label="Primary navigation"
311
+ >
312
+ {#each filteredPrimaryItems as item (item.id)}
313
+ {@const Icon = item.icon}
314
+ <li>
315
+ <a
316
+ href={item.href}
317
+ class={getNavItemClasses(item)}
318
+ onclick={(event) => handleNavigate(item, event)}
319
+ aria-current={getAriaCurrent(item)}
320
+ aria-label={isCollapsed ? item.label : undefined}
321
+ >
322
+ {#if Icon}
323
+ <span
324
+ class={iconContainerClasses}
325
+ aria-hidden="true"
326
+ >
327
+ <Icon size={20} />
328
+ </span>
329
+ {/if}
330
+ {#if !isCollapsed}
331
+ <span
332
+ class="text-base leading-snug text-inherit"
333
+ >{item.label}</span
334
+ >
335
+ {/if}
336
+ {#if !isCollapsed && getItemBadgeText(item) !== null}
337
+ <span class="ml-auto">
338
+ <Badge
339
+ variant="default"
340
+ size="sm"
341
+ text={getItemBadgeText(item) ?? ""}
342
+ />
343
+ </span>
344
+ {/if}
345
+ </a>
346
+ </li>
347
+ {/each}
348
+ </ul>
260
349
 
261
- {#if hasFilteredItems}
350
+ {#if filteredSecondaryItems.length > 0}
262
351
  <ul
263
- class="flex w-full flex-col gap-2"
264
- aria-label="Primary navigation"
352
+ class="flex w-full flex-col gap-3 px-2"
353
+ aria-label="Secondary navigation"
265
354
  >
266
- {#each filteredPrimaryItems as item (item.id)}
355
+ {#each filteredSecondaryItems as item (item.id)}
267
356
  {@const Icon = item.icon}
268
357
  <li>
269
358
  <a
270
359
  href={item.href}
271
360
  class={getNavItemClasses(item)}
272
361
  onclick={(event) => handleNavigate(item, event)}
273
- aria-current={currentPath === item.href
274
- ? "page"
275
- : undefined}
362
+ aria-current={getAriaCurrent(item)}
276
363
  aria-label={isCollapsed
277
364
  ? item.label
278
365
  : undefined}
@@ -286,135 +373,92 @@
286
373
  </span>
287
374
  {/if}
288
375
  {#if !isCollapsed}
289
- <span class="text-base leading-snug"
376
+ <span
377
+ class="text-base leading-snug text-inherit"
290
378
  >{item.label}</span
291
379
  >
292
380
  {/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
381
  </a>
303
382
  </li>
304
383
  {/each}
305
384
  </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
385
  {/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}
386
+ {:else}
387
+ <div
388
+ class="rounded-2xl border border-border border-dashed bg-base-50 px-4 py-5"
381
389
  >
382
- <span class={iconContainerClasses} aria-hidden="true">
383
- <LogOut size={20} />
384
- </span>
385
- {#if !isCollapsed}
386
- <span class="text-base">{logoutLabel}</span>
390
+ <h3 class="text-base font-semibold {getTextToneClass()}">
391
+ {normalizedSearchTerm && searchMode === "input"
392
+ ? "No matching navigation items"
393
+ : emptyStateTitle}
394
+ </h3>
395
+ <p class="mt-1 text-sm {getTextToneClass(true)}">
396
+ {normalizedSearchTerm && searchMode === "input"
397
+ ? `No results found for "${searchValue}". Try another keyword.`
398
+ : emptyStateDescription}
399
+ </p>
400
+ {#if !(normalizedSearchTerm && searchMode === "input")}
401
+ <button
402
+ type="button"
403
+ class="mt-4 inline-flex min-h-11 cursor-pointer items-center rounded-lg bg-action-primary px-3 py-2 text-sm text-action-primary outline-none transition-colors hover:bg-action-primary-hover focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-nav-menu-focus"
404
+ onclick={handleEmptyStateAction}
405
+ >
406
+ {emptyStateActionLabel}
407
+ </button>
387
408
  {/if}
388
- </button>
409
+ </div>
389
410
  {/if}
411
+ </div>
390
412
 
391
- {#if showThemeToggle}
392
- <div class={themeToggleContainerClasses}>
413
+ {#if showLogout || showThemeToggle}
414
+ <div
415
+ class="flex w-full shrink-0 flex-col gap-2 border-t border-border pt-4 px-2"
416
+ >
417
+ {#if showLogout}
393
418
  <button
394
419
  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"}
420
+ class={getControlButtonClasses()}
421
+ onclick={handleLogout}
404
422
  >
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}
423
+ <span class={iconContainerClasses} aria-hidden="true">
424
+ <LogOut size={20} />
415
425
  </span>
426
+ {#if !isCollapsed}
427
+ <span class="text-base text-inherit">{logoutLabel}</span
428
+ >
429
+ {/if}
416
430
  </button>
417
- </div>
418
- {/if}
419
- </div>
431
+ {/if}
432
+
433
+ {#if showThemeToggle}
434
+ <div class={themeToggleContainerClasses}>
435
+ <button
436
+ type="button"
437
+ class="inline-flex h-8 w-14 cursor-pointer items-center rounded-2xl p-1 outline-none transition-colors duration-150 hover:opacity-95 active:opacity-90 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-nav-menu-focus {isLightMode
438
+ ? 'bg-action-primary'
439
+ : 'bg-base-200'}"
440
+ onclick={handleThemeToggle}
441
+ role="switch"
442
+ aria-checked={isLightMode}
443
+ title={lightModeLabel}
444
+ aria-label={isLightMode
445
+ ? "Switch to dark mode"
446
+ : "Switch to light mode"}
447
+ >
448
+ <span
449
+ class="flex h-6 w-6 items-center justify-center rounded-xl leading-none shadow-sm transition-transform duration-150 active:scale-95 {isLightMode
450
+ ? 'translate-x-6 bg-base-900 text-action-primary'
451
+ : 'translate-x-0 bg-base-900 text-base-50'}"
452
+ >
453
+ {#if isLightMode}
454
+ <Sun size={14} />
455
+ {:else}
456
+ <Moon size={14} />
457
+ {/if}
458
+ </span>
459
+ </button>
460
+ </div>
461
+ {/if}
462
+ </div>
463
+ {/if}
420
464
  </nav>
@@ -38,6 +38,11 @@ interface Props {
38
38
  onLogout?: () => void;
39
39
  onThemeToggle?: (nextIsLightMode: boolean) => void;
40
40
  onEmptyStateAction?: () => void;
41
+ /**
42
+ * Highlights a primary (e.g. category) row when the current route is a child
43
+ * of that section, while `currentPath` points at the leaf (e.g. a component).
44
+ */
45
+ activePrimaryHref?: string;
41
46
  }
42
47
  declare const SidebarNavigation: Component<Props, {}, "searchValue" | "isLightMode">;
43
48
  type SidebarNavigation = ReturnType<typeof SidebarNavigation>;
@@ -4,7 +4,7 @@
4
4
  import { Search, X } from "@lucide/svelte";
5
5
  import type { Component } from "svelte";
6
6
 
7
- export interface SidebarProjectPanelItem {
7
+ export interface SidebarPanelItem {
8
8
  id: string;
9
9
  label: string;
10
10
  icon?: Component<{ size?: number; class?: string }>;
@@ -21,30 +21,30 @@
21
21
  showSearch?: boolean;
22
22
  searchPlaceholder?: string;
23
23
  searchValue?: string;
24
- items?: SidebarProjectPanelItem[];
24
+ items?: SidebarPanelItem[];
25
25
  selectedItemId?: string;
26
26
  emptyStateTitle?: string;
27
27
  emptyStateDescription?: string;
28
28
  selectLabel?: string;
29
29
  closeLabel?: string;
30
- onSelect?: (item: SidebarProjectPanelItem) => void;
30
+ onSelect?: (item: SidebarPanelItem) => void;
31
31
  onClose?: () => void;
32
32
  }
33
33
 
34
34
  let {
35
35
  className = "",
36
36
  widthClass = "w-80",
37
- ariaLabel = "Project picker panel",
38
- title = "Projects",
39
- subtitle = "Choose a project to continue",
37
+ ariaLabel = "Picker panel",
38
+ title = "Items",
39
+ subtitle = "Choose an item to continue",
40
40
  showSearch = true,
41
- searchPlaceholder = "Search projects...",
41
+ searchPlaceholder = "Search...",
42
42
  searchValue = $bindable(""),
43
43
  items = [],
44
44
  selectedItemId = $bindable(""),
45
- emptyStateTitle = "No projects found",
46
- emptyStateDescription = "Try a different keyword or create a new project.",
47
- selectLabel = "Select project",
45
+ emptyStateTitle = "No matches",
46
+ emptyStateDescription = "Try a different keyword.",
47
+ selectLabel = "Select item",
48
48
  closeLabel = "Close panel",
49
49
  onSelect,
50
50
  onClose,
@@ -68,14 +68,14 @@
68
68
  function getItemClasses(itemId: string): string {
69
69
  const isActive = selectedItemId === itemId;
70
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";
71
+ "w-full cursor-pointer 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
72
  if (isActive) {
73
73
  return `${baseClasses} bg-action-primary-subtle text-headline`;
74
74
  }
75
75
  return `${baseClasses} hover:bg-base-50 text-description`;
76
76
  }
77
77
 
78
- function handleSelect(item: SidebarProjectPanelItem): void {
78
+ function handleSelect(item: SidebarPanelItem): void {
79
79
  selectedItemId = item.id;
80
80
  if (onSelect) {
81
81
  onSelect(item);
@@ -97,7 +97,7 @@
97
97
  </div>
98
98
  <button
99
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"
100
+ class="inline-flex size-8 shrink-0 cursor-pointer 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
101
  aria-label={closeLabel}
102
102
  onclick={handleClose}
103
103
  >
@@ -124,14 +124,15 @@
124
124
  {/if}
125
125
 
126
126
  {#if filteredItems.length > 0}
127
- <ul class="space-y-2" aria-label={selectLabel}>
127
+ <ul class="space-y-2" role="listbox" aria-label={selectLabel}>
128
128
  {#each filteredItems as item (item.id)}
129
- <li>
129
+ <li role="presentation">
130
130
  <button
131
131
  type="button"
132
+ role="option"
132
133
  class={getItemClasses(item.id)}
133
134
  onclick={() => handleSelect(item)}
134
- aria-pressed={selectedItemId === item.id}
135
+ aria-selected={selectedItemId === item.id}
135
136
  >
136
137
  <span class="flex items-center justify-between gap-3">
137
138
  <span class="flex min-w-0 items-start gap-2">
@@ -171,4 +172,4 @@
171
172
  <p class="mt-1 text-sm text-description">{emptyStateDescription}</p>
172
173
  </div>
173
174
  {/if}
174
- </aside>
175
+ </aside>
@@ -1,5 +1,5 @@
1
1
  import type { Component } from "svelte";
2
- export interface SidebarProjectPanelItem {
2
+ export interface SidebarPanelItem {
3
3
  id: string;
4
4
  label: string;
5
5
  icon?: Component<{
@@ -18,15 +18,15 @@ interface Props {
18
18
  showSearch?: boolean;
19
19
  searchPlaceholder?: string;
20
20
  searchValue?: string;
21
- items?: SidebarProjectPanelItem[];
21
+ items?: SidebarPanelItem[];
22
22
  selectedItemId?: string;
23
23
  emptyStateTitle?: string;
24
24
  emptyStateDescription?: string;
25
25
  selectLabel?: string;
26
26
  closeLabel?: string;
27
- onSelect?: (item: SidebarProjectPanelItem) => void;
27
+ onSelect?: (item: SidebarPanelItem) => void;
28
28
  onClose?: () => void;
29
29
  }
30
- declare const SidebarProjectPanel: Component<Props, {}, "searchValue" | "selectedItemId">;
31
- type SidebarProjectPanel = ReturnType<typeof SidebarProjectPanel>;
32
- export default SidebarProjectPanel;
30
+ declare const SidebarPanel: Component<Props, {}, "searchValue" | "selectedItemId">;
31
+ type SidebarPanel = ReturnType<typeof SidebarPanel>;
32
+ export default SidebarPanel;
@@ -1,4 +1,3 @@
1
1
  export { default as Navbar } from './Navbar.svelte';
2
- export { default as Navigation } from './Navigation.svelte';
3
2
  export { default as SidebarNavigation } from './SidebarNavigation.svelte';
4
- export { default as SidebarProjectPanel } from './SidebarProjectPanel.svelte';
3
+ export { default as SidebarPanel } from './SidebarPanel.svelte';
@@ -1,4 +1,3 @@
1
1
  export { default as Navbar } from './Navbar.svelte';
2
- export { default as Navigation } from './Navigation.svelte';
3
2
  export { default as SidebarNavigation } from './SidebarNavigation.svelte';
4
- export { default as SidebarProjectPanel } from './SidebarProjectPanel.svelte';
3
+ export { default as SidebarPanel } from './SidebarPanel.svelte';