zabi-components 7.0.1 → 8.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. package/README.md +46 -3
  2. package/THEMING.md +255 -15
  3. package/dist/atoms/ActionPanel.svelte +12 -7
  4. package/dist/atoms/ActionPanel.svelte.d.ts +2 -0
  5. package/dist/atoms/Badge.svelte +93 -56
  6. package/dist/atoms/Badge.svelte.d.ts +15 -3
  7. package/dist/atoms/Button.svelte +49 -49
  8. package/dist/atoms/Button.svelte.d.ts +3 -0
  9. package/dist/atoms/Card.svelte +31 -28
  10. package/dist/atoms/Card.svelte.d.ts +5 -2
  11. package/dist/atoms/CardContent.svelte +10 -2
  12. package/dist/atoms/CardContent.svelte.d.ts +2 -0
  13. package/dist/atoms/CardFooter.svelte +10 -2
  14. package/dist/atoms/CardFooter.svelte.d.ts +2 -0
  15. package/dist/atoms/CardHeader.svelte +10 -2
  16. package/dist/atoms/CardHeader.svelte.d.ts +2 -0
  17. package/dist/atoms/Checkbox.svelte +4 -0
  18. package/dist/atoms/Checkbox.svelte.d.ts +2 -0
  19. package/dist/atoms/CodeBlock.svelte +15 -7
  20. package/dist/atoms/CodeBlock.svelte.d.ts +9 -3
  21. package/dist/atoms/ColorPicker.svelte +19 -9
  22. package/dist/atoms/ColorPicker.svelte.d.ts +3 -1
  23. package/dist/atoms/Container.svelte +2 -3
  24. package/dist/atoms/Divider.svelte +4 -4
  25. package/dist/atoms/FeatureCard.svelte +4 -3
  26. package/dist/atoms/Heading.svelte +56 -27
  27. package/dist/atoms/Heading.svelte.d.ts +17 -4
  28. package/dist/atoms/IconButton.svelte +30 -37
  29. package/dist/atoms/Input.svelte +26 -34
  30. package/dist/atoms/List.svelte +10 -2
  31. package/dist/atoms/List.svelte.d.ts +2 -0
  32. package/dist/atoms/ListItem.svelte +2 -2
  33. package/dist/atoms/ListItemLeading.svelte +4 -2
  34. package/dist/atoms/ListItemLeading.svelte.d.ts +1 -1
  35. package/dist/atoms/OptimizedImage.svelte +11 -2
  36. package/dist/atoms/OptimizedImage.svelte.d.ts +2 -0
  37. package/dist/atoms/Progress.svelte +4 -1
  38. package/dist/atoms/Progress.svelte.d.ts +2 -0
  39. package/dist/atoms/Radio.svelte +4 -0
  40. package/dist/atoms/Radio.svelte.d.ts +2 -0
  41. package/dist/atoms/Select.svelte +65 -67
  42. package/dist/atoms/Select.svelte.d.ts +4 -0
  43. package/dist/atoms/SelectionControl.svelte +5 -1
  44. package/dist/atoms/SelectionControl.svelte.d.ts +2 -0
  45. package/dist/atoms/Skeleton.svelte +5 -3
  46. package/dist/atoms/Table.svelte +2 -1
  47. package/dist/atoms/Text.svelte +34 -5
  48. package/dist/atoms/Text.svelte.d.ts +7 -1
  49. package/dist/atoms/Textarea.svelte +7 -6
  50. package/dist/atoms/ThemeToggle.svelte +17 -14
  51. package/dist/atoms/ThemeToggle.svelte.d.ts +2 -0
  52. package/dist/atoms/Toast.svelte +10 -8
  53. package/dist/atoms/Toggle.svelte +21 -13
  54. package/dist/atoms/Toggle.svelte.d.ts +7 -0
  55. package/dist/atoms/Tooltip.svelte +89 -57
  56. package/dist/atoms/Tooltip.svelte.d.ts +15 -0
  57. package/dist/components/index.d.ts +3 -1
  58. package/dist/components/index.d.ts.map +1 -1
  59. package/dist/components/organisms/index.d.ts +1 -0
  60. package/dist/components/organisms/index.d.ts.map +1 -1
  61. package/dist/components/util/cn.d.ts +8 -0
  62. package/dist/components/util/cn.d.ts.map +1 -0
  63. package/dist/components/util/ssr-safe.d.ts +4 -4
  64. package/dist/components/util/ssr-safe.d.ts.map +1 -1
  65. package/dist/index.d.ts +3 -1
  66. package/dist/index.js +3 -6
  67. package/dist/molecules/Alert.svelte +43 -64
  68. package/dist/molecules/Alert.svelte.d.ts +7 -1
  69. package/dist/molecules/ComponentDemo.svelte +10 -2
  70. package/dist/molecules/ComponentDemo.svelte.d.ts +2 -0
  71. package/dist/molecules/ContactForm.svelte +12 -2
  72. package/dist/molecules/ContactForm.svelte.d.ts +2 -0
  73. package/dist/molecules/Dropdown.svelte +40 -11
  74. package/dist/molecules/Dropdown.svelte.d.ts +4 -0
  75. package/dist/molecules/EmptyState.svelte +2 -1
  76. package/dist/molecules/Form.svelte +25 -14
  77. package/dist/molecules/Form.svelte.d.ts +13 -5
  78. package/dist/molecules/FormField.svelte +2 -1
  79. package/dist/molecules/Header.svelte +13 -3
  80. package/dist/molecules/Header.svelte.d.ts +2 -0
  81. package/dist/molecules/ImageUpload.svelte +54 -15
  82. package/dist/molecules/ImageUpload.svelte.d.ts +16 -5
  83. package/dist/molecules/Modal.svelte +110 -31
  84. package/dist/molecules/Modal.svelte.d.ts +9 -6
  85. package/dist/molecules/NavigationMenu.svelte +15 -3
  86. package/dist/molecules/NavigationMenu.svelte.d.ts +4 -0
  87. package/dist/molecules/NavigationMenuContent.svelte +36 -38
  88. package/dist/molecules/NavigationMenuContent.svelte.d.ts +2 -0
  89. package/dist/molecules/NavigationMenuItem.svelte +10 -2
  90. package/dist/molecules/NavigationMenuItem.svelte.d.ts +2 -0
  91. package/dist/molecules/NavigationMenuLink.svelte +10 -2
  92. package/dist/molecules/NavigationMenuLink.svelte.d.ts +2 -0
  93. package/dist/molecules/NavigationMenuList.svelte +10 -2
  94. package/dist/molecules/NavigationMenuList.svelte.d.ts +2 -0
  95. package/dist/molecules/NavigationMenuTrigger.svelte +16 -3
  96. package/dist/molecules/NavigationMenuTrigger.svelte.d.ts +2 -0
  97. package/dist/molecules/Page.svelte +8 -2
  98. package/dist/molecules/Page.svelte.d.ts +2 -0
  99. package/dist/molecules/PropsTable.svelte +6 -3
  100. package/dist/molecules/PropsTable.svelte.d.ts +2 -0
  101. package/dist/molecules/RadioGroup.svelte +5 -1
  102. package/dist/molecules/RadioGroup.svelte.d.ts +2 -0
  103. package/dist/molecules/Section.svelte +12 -4
  104. package/dist/molecules/Section.svelte.d.ts +3 -1
  105. package/dist/molecules/SidebarBrandHeader.svelte +13 -4
  106. package/dist/molecules/SidebarBrandHeader.svelte.d.ts +2 -0
  107. package/dist/molecules/SidebarFooter.svelte +12 -4
  108. package/dist/molecules/SidebarFooter.svelte.d.ts +2 -0
  109. package/dist/molecules/SidebarNavSection.svelte +12 -4
  110. package/dist/molecules/SidebarNavSection.svelte.d.ts +2 -0
  111. package/dist/molecules/SlideUp.svelte +85 -21
  112. package/dist/molecules/SlideUp.svelte.d.ts +5 -4
  113. package/dist/molecules/Tabs.svelte +25 -19
  114. package/dist/molecules/Tabs.svelte.d.ts +8 -4
  115. package/dist/molecules/Toaster.svelte +3 -2
  116. package/dist/molecules/ToasterToast.svelte +41 -10
  117. package/dist/molecules/ToasterToast.svelte.d.ts +2 -0
  118. package/dist/organisms/SidebarAccountPanel.svelte +4 -0
  119. package/dist/organisms/SidebarAccountPanel.svelte.d.ts +2 -0
  120. package/dist/organisms/SidebarNavigation.svelte +102 -59
  121. package/dist/organisms/SidebarNavigation.svelte.d.ts +2 -0
  122. package/dist/organisms/SidebarPanel.svelte +19 -11
  123. package/dist/organisms/SidebarPanel.svelte.d.ts +2 -0
  124. package/dist/organisms/SidebarShell.svelte +112 -0
  125. package/dist/organisms/SidebarShell.svelte.d.ts +52 -0
  126. package/dist/organisms/TopNavbar.svelte +88 -17
  127. package/dist/organisms/TopNavbar.svelte.d.ts +2 -0
  128. package/dist/organisms/index.d.ts +1 -0
  129. package/dist/organisms/index.js +1 -0
  130. package/dist/routes/lib/variant-utils.d.ts +26 -0
  131. package/dist/routes/lib/{variant-utils.ts → variant-utils.js} +15 -46
  132. package/dist/types/events.js +1 -0
  133. package/dist/types/index.js +3 -0
  134. package/dist/util/cn.d.ts +7 -0
  135. package/dist/util/cn.js +48 -0
  136. package/dist/util/ssr-safe.d.ts +4 -4
  137. package/dist/zabi-components-colors.css +3 -437
  138. package/dist/zabi-components-theme-dark-only.css +223 -103
  139. package/dist/zabi-components-theme-dark.css +223 -103
  140. package/dist/zabi-components-theme-only.css +347 -107
  141. package/dist/zabi-components-theme.css +347 -107
  142. package/dist/zabi-components.css +953 -843
  143. package/docs/lucide-icons.md +3 -1
  144. package/package.json +20 -25
  145. package/dist/lib/layout-width-tokens.d.ts +0 -17
  146. package/dist/lib/layout-width-tokens.d.ts.map +0 -1
  147. package/dist/lib/showcase/component-docs/Button.d.ts +0 -3
  148. package/dist/lib/showcase/component-docs/Button.d.ts.map +0 -1
  149. package/dist/lib/showcase/component-docs/Input.d.ts +0 -3
  150. package/dist/lib/showcase/component-docs/Input.d.ts.map +0 -1
  151. package/dist/lib/showcase/component-docs/List.d.ts +0 -3
  152. package/dist/lib/showcase/component-docs/List.d.ts.map +0 -1
  153. package/dist/lib/showcase/component-docs/Modal.d.ts +0 -3
  154. package/dist/lib/showcase/component-docs/Modal.d.ts.map +0 -1
  155. package/dist/lib/showcase/component-docs/Radio.d.ts +0 -3
  156. package/dist/lib/showcase/component-docs/Radio.d.ts.map +0 -1
  157. package/dist/lib/showcase/component-docs/SidebarNavigation.d.ts +0 -3
  158. package/dist/lib/showcase/component-docs/SidebarNavigation.d.ts.map +0 -1
  159. package/dist/lib/showcase/component-docs/Skeleton.d.ts +0 -3
  160. package/dist/lib/showcase/component-docs/Skeleton.d.ts.map +0 -1
  161. package/dist/lib/showcase/component-docs/_shared.d.ts +0 -12
  162. package/dist/lib/showcase/component-docs/_shared.d.ts.map +0 -1
  163. package/dist/lib/showcase/component-docs/index.d.ts +0 -4
  164. package/dist/lib/showcase/component-docs/index.d.ts.map +0 -1
  165. package/dist/lib/showcase/components-catalog.d.ts +0 -3
  166. package/dist/lib/showcase/components-catalog.d.ts.map +0 -1
  167. package/dist/lib/showcase/components-showcase-constants.d.ts +0 -89
  168. package/dist/lib/showcase/components-showcase-constants.d.ts.map +0 -1
  169. package/dist/lib/showcase/docs-sidebar-helpers.d.ts +0 -15
  170. package/dist/lib/showcase/docs-sidebar-helpers.d.ts.map +0 -1
  171. package/dist/routes/lib/focus-utils.d.ts +0 -12
  172. package/dist/routes/lib/focus-utils.ts +0 -110
  173. package/dist/routes/lib/ssr-safe.ts +0 -58
  174. package/scripts/fix-lucide-svelte-icon-dts.js +0 -35
@@ -6,6 +6,8 @@
6
6
  import SidebarBrandHeader from "../molecules/SidebarBrandHeader.svelte";
7
7
  import SidebarFooter from "../molecules/SidebarFooter.svelte";
8
8
  import SidebarNavSection from "../molecules/SidebarNavSection.svelte";
9
+ import Tooltip from "../atoms/Tooltip.svelte";
10
+ import SidebarShell from "./SidebarShell.svelte";
9
11
  import { Command, Search } from "@lucide/svelte";
10
12
  import type { Snippet } from "svelte";
11
13
  import type { Component } from "svelte";
@@ -34,6 +36,8 @@
34
36
  items?: SidebarNavigationItem[];
35
37
  currentPath?: string;
36
38
  ariaLabel?: string;
39
+ class?: string;
40
+ /** @deprecated use `class`. */
37
41
  className?: string;
38
42
  logoSrc?: string;
39
43
  logoAlt?: string;
@@ -78,7 +82,8 @@
78
82
  currentPath = "",
79
83
  activePrimaryHref = "",
80
84
  ariaLabel = "Sidebar navigation",
81
- className = "",
85
+ class: classAttr = "",
86
+ className: legacyClass = "",
82
87
  logoSrc = "",
83
88
  logoAlt = "",
84
89
  brandName = "",
@@ -113,8 +118,10 @@
113
118
  ...restProps
114
119
  }: Props = $props();
115
120
 
121
+ /** `class` is the public prop; `className` is a deprecated alias.
122
+ * Both are merged here so existing call sites keep working. */
123
+
116
124
  const isCollapsed = $derived(mode === "collapsed");
117
- const isCard = $derived(layout === "card");
118
125
  const showBrandRow = $derived(
119
126
  Boolean(logoSrc.trim() || brandName.trim()),
120
127
  );
@@ -178,24 +185,6 @@
178
185
  partitionBySection(filteredPrimaryItems),
179
186
  );
180
187
 
181
- const insetX = $derived(isCollapsed ? "px-2.5" : "px-4");
182
-
183
- const containerClasses = $derived.by(() => {
184
- const widthClass = isCollapsed ? "w-[104px]" : "w-[266px]";
185
- const railSurface = "border-r border-border bg-background text-headline";
186
- const cardSurface =
187
- "border-r border-border bg-background text-headline shadow-sm";
188
- const surfaceClasses = isCard ? cardSurface : railSurface;
189
- const verticalPad = isCard ? "py-4" : "py-5";
190
- const baseClasses = `flex h-full min-h-0 max-h-full flex-col overflow-visible ${verticalPad}`;
191
-
192
- return `${baseClasses} ${widthClass} ${surfaceClasses} ${className}`.trim();
193
- });
194
-
195
- const headerStackClasses = $derived(
196
- `flex w-full shrink-0 flex-col gap-5 ${insetX}`,
197
- );
198
-
199
188
  const iconContainerClasses = $derived(
200
189
  "flex size-6 shrink-0 items-center justify-center leading-none text-current",
201
190
  );
@@ -210,21 +199,43 @@
210
199
  return isMuted ? "text-description" : "text-headline";
211
200
  }
212
201
 
213
- function getNavItemClasses(item: SidebarNavigationItem): string {
214
- const isActive =
202
+ /**
203
+ * The selected row used to be carried by its fill alone, and the fill was
204
+ * 1.05:1 against the sidebar in light mode — not a fill, a rumour. Active
205
+ * and idle labels were #18181b and #27272a at the same weight, a 15-unit
206
+ * difference in one channel that nobody can see. So in light mode there
207
+ * was effectively no indication of where you were.
208
+ *
209
+ * Three cues now, only one of which is colour-discrimination dependent:
210
+ * the label takes --color-nav-menu-item-active (defined all along and
211
+ * never referenced — the row said `text-inherit`), a 3px bar marks the
212
+ * leading edge, and the tint stays as reinforcement rather than as the
213
+ * whole message.
214
+ */
215
+ function isItemActive(item: SidebarNavigationItem): boolean {
216
+ return (
215
217
  currentPath === item.href ||
216
- (Boolean(activePrimaryHref) && item.href === activePrimaryHref);
218
+ (Boolean(activePrimaryHref) && item.href === activePrimaryHref)
219
+ );
220
+ }
221
+
222
+ function getNavItemClasses(item: SidebarNavigationItem): string {
223
+ const isActive = isItemActive(item);
217
224
  const layoutClasses = isCollapsed
218
225
  ? "flex min-h-10 items-center justify-center px-0 py-2"
219
- : "flex min-h-10 items-center gap-2.5 px-2.5 py-2";
226
+ : "flex min-h-10 items-center gap-3 px-2 py-2";
220
227
  const structural =
221
- "focus-ring focus-ring--nav w-full cursor-pointer rounded-lg no-underline transition-colors duration-150 outline-none";
228
+ "focus-ring focus-ring--nav relative w-full cursor-pointer rounded-control no-underline transition-colors duration-150 outline-none";
222
229
 
223
230
  if (isActive) {
224
- return `${structural} ${layoutClasses} bg-nav-menu-active text-inherit hover:bg-nav-menu-active hover:text-inherit active:opacity-90`;
231
+ return (
232
+ `${structural} ${layoutClasses} bg-nav-menu-active ` +
233
+ "text-nav-menu-item-active hover:bg-nav-menu-active " +
234
+ "active:bg-nav-menu-active-hover"
235
+ );
225
236
  }
226
237
 
227
- return `${structural} ${layoutClasses} text-nav-menu-item hover:bg-nav-menu-hover hover:text-nav-menu-item-hover active:bg-base-200`;
238
+ return `${structural} ${layoutClasses} text-nav-menu-item hover:bg-nav-menu-hover hover:text-nav-menu-item-hover active:bg-surface-active`;
228
239
  }
229
240
 
230
241
  function handleNavigate(item: SidebarNavigationItem, event: MouseEvent) {
@@ -268,11 +279,15 @@
268
279
  }
269
280
  </script>
270
281
 
271
- <nav class={containerClasses} aria-label={ariaLabel} {...restProps}>
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()}>
282
+ <SidebarShell
283
+ {mode}
284
+ {layout}
285
+ {ariaLabel}
286
+ class={classAttr}
287
+ className={legacyClass}
288
+ {...restProps}
289
+ >
290
+ {#snippet header()}
276
291
  {#if showBrandRow}
277
292
  <SidebarBrandHeader
278
293
  collapsed={isCollapsed}
@@ -307,10 +322,10 @@
307
322
  <Button
308
323
  variant={searchTriggerVariant}
309
324
  size={searchTriggerSize}
310
- isFullWidth
325
+ fullWidth
311
326
  onclick={handleSearchClick}
312
327
  >
313
- <span class="flex w-full items-center justify-start gap-2.5">
328
+ <span class="flex w-full items-center justify-start gap-3">
314
329
  <span class={iconContainerClasses} aria-hidden="true">
315
330
  <TriggerIcon size={17} />
316
331
  </span>
@@ -334,18 +349,13 @@
334
349
  bind:value={searchValue}
335
350
  placeholder={searchPlaceholder}
336
351
  aria-label={searchPlaceholder}
337
- class="focus-ring focus-ring--nav w-full min-w-0 min-h-10 rounded-xl border-transparent !bg-transparent py-2 pl-10 text-sm ring-1 ring-border/60 hover:!bg-nav-menu-hover focus:!bg-transparent"
352
+ class="focus-ring focus-ring--nav w-full min-w-0 min-h-10 rounded-container border-transparent !bg-transparent py-2 pl-10 text-sm ring-1 ring-border/60 hover:!bg-nav-menu-hover focus:!bg-transparent"
338
353
  />
339
354
  </div>
340
355
  {/if}
341
356
  {/if}
342
- </div>
357
+ {/snippet}
343
358
 
344
- <div
345
- class={`flex min-h-0 min-w-0 flex-1 flex-col gap-3 overflow-y-auto overflow-x-hidden overscroll-y-contain pt-4 pb-1.5 ${insetX}`}
346
- role="region"
347
- aria-label="Navigation links"
348
- >
349
359
  {#if hasFilteredItems}
350
360
  <div
351
361
  class="flex w-full min-w-0 flex-col divide-y divide-border"
@@ -363,6 +373,19 @@
363
373
  {#each group.items as item (item.id)}
364
374
  {@const Icon = item.icon}
365
375
  <li>
376
+ <!--
377
+ Collapsed rows are icon-only. aria-label
378
+ covers screen readers, but a sighted user
379
+ got an unlabelled glyph with no hover
380
+ affordance at all.
381
+ -->
382
+ <Tooltip
383
+ content={item.label}
384
+ placement="right"
385
+ disabled={!isCollapsed}
386
+ block
387
+ fixed
388
+ >
366
389
  <a
367
390
  href={item.href}
368
391
  class={getNavItemClasses(item)}
@@ -370,6 +393,12 @@
370
393
  aria-current={getAriaCurrent(item)}
371
394
  aria-label={isCollapsed ? item.label : undefined}
372
395
  >
396
+ {#if isItemActive(item)}
397
+ <span
398
+ class="absolute left-0 top-1/2 h-5 w-[3px] -translate-y-1/2 rounded-pill bg-nav-menu-item-active"
399
+ aria-hidden="true"
400
+ ></span>
401
+ {/if}
373
402
  {#if Icon}
374
403
  <span
375
404
  class={iconContainerClasses}
@@ -384,16 +413,31 @@
384
413
  >{item.label}</span
385
414
  >
386
415
  {/if}
387
- {#if !isCollapsed && getItemBadgeText(item) !== null}
388
- <span class="ml-auto">
389
- <Badge
390
- variant="default"
391
- size="sm"
392
- text={getItemBadgeText(item) ?? ""}
393
- />
394
- </span>
416
+ {#if getItemBadgeText(item) !== null}
417
+ {#if isCollapsed}
418
+ <!--
419
+ A notification count that
420
+ disappears when you collapse
421
+ the rail is a real loss; the
422
+ count becomes a dot rather
423
+ than nothing.
424
+ -->
425
+ <span
426
+ class="absolute right-2 top-2 size-2 rounded-pill bg-error"
427
+ aria-hidden="true"
428
+ ></span>
429
+ {:else}
430
+ <span class="ml-auto">
431
+ <Badge
432
+ variant="default"
433
+ size="sm"
434
+ text={getItemBadgeText(item) ?? ""}
435
+ />
436
+ </span>
437
+ {/if}
395
438
  {/if}
396
439
  </a>
440
+ </Tooltip>
397
441
  </li>
398
442
  {/each}
399
443
  </SidebarNavSection>
@@ -446,7 +490,7 @@
446
490
  {/if}
447
491
  {:else}
448
492
  <div
449
- class="rounded-xl border border-border border-dashed bg-transparent px-3.5 py-4 ring-1 ring-border/60"
493
+ class="rounded-container border border-border border-dashed bg-transparent px-4 py-4 ring-1 ring-border/60"
450
494
  >
451
495
  <h3 class="text-sm font-semibold {getTextToneClass()}">
452
496
  {normalizedSearchTerm && searchMode === "input"
@@ -461,7 +505,7 @@
461
505
  {#if !(normalizedSearchTerm && searchMode === "input")}
462
506
  <button
463
507
  type="button"
464
- class="focus-ring focus-ring--nav mt-3 inline-flex min-h-10 cursor-pointer items-center rounded-lg bg-action-primary px-3 py-2 text-sm font-medium text-action-primary outline-none transition-colors hover:bg-action-primary-hover"
508
+ class="focus-ring focus-ring--nav mt-3 inline-flex min-h-10 cursor-pointer items-center rounded-control bg-action-primary px-3 py-2 text-sm font-medium text-action-primary outline-none transition-colors hover:bg-action-primary-hover"
465
509
  onclick={handleEmptyStateAction}
466
510
  >
467
511
  {emptyStateActionLabel}
@@ -469,11 +513,9 @@
469
513
  {/if}
470
514
  </div>
471
515
  {/if}
472
- </div>
473
- </div>
474
-
475
- <SidebarFooter
476
- collapsed={isCollapsed}
516
+ {#snippet footer({ insetX })}
517
+ <SidebarFooter
518
+ collapsed={isCollapsed}
477
519
  {showProfile}
478
520
  {profileName}
479
521
  {profileEmail}
@@ -489,6 +531,7 @@
489
531
  {profilePanelOpen}
490
532
  {profilePanelControlsId}
491
533
  {profilePanel}
492
- className={insetX}
493
- />
494
- </nav>
534
+ className={insetX}
535
+ />
536
+ {/snippet}
537
+ </SidebarShell>
@@ -21,6 +21,8 @@ interface Props {
21
21
  items?: SidebarNavigationItem[];
22
22
  currentPath?: string;
23
23
  ariaLabel?: string;
24
+ class?: string;
25
+ /** @deprecated use `class`. */
24
26
  className?: string;
25
27
  logoSrc?: string;
26
28
  logoAlt?: string;
@@ -3,6 +3,7 @@
3
3
  import Badge from "../atoms/Badge.svelte";
4
4
  import { Search, X } from "@lucide/svelte";
5
5
  import type { Component } from "svelte";
6
+ import { cn } from "../util/cn.js";
6
7
 
7
8
  export interface SidebarPanelItem {
8
9
  id: string;
@@ -13,6 +14,8 @@
13
14
  }
14
15
 
15
16
  interface Props {
17
+ class?: string;
18
+ /** @deprecated use `class`. */
16
19
  className?: string;
17
20
  widthClass?: string;
18
21
  variant?: "plain" | "elevated";
@@ -35,7 +38,8 @@
35
38
  }
36
39
 
37
40
  let {
38
- className = "",
41
+ class: classAttr = "",
42
+ className: legacyClass = "",
39
43
  widthClass = "w-80",
40
44
  variant = "elevated",
41
45
  ariaLabel = "Picker panel",
@@ -57,6 +61,10 @@
57
61
  ...restProps
58
62
  }: Props = $props();
59
63
 
64
+ /** `class` is the public prop; `className` is a deprecated alias.
65
+ * Both are merged here so existing call sites keep working. */
66
+ const className = $derived(cn(`${classAttr} ${legacyClass}`));
67
+
60
68
  const normalizedSearchTerm = $derived(searchValue.trim().toLowerCase());
61
69
  const filteredItems = $derived(
62
70
  normalizedSearchTerm
@@ -71,19 +79,19 @@
71
79
  const containerClasses = $derived.by(() => {
72
80
  const resolvedWidthClass = widthClass.trim() || "w-80";
73
81
  const shell = isElevated
74
- ? "rounded-3xl border border-border bg-card text-headline shadow-md ring-1 ring-border/50"
75
- : "rounded-3xl border border-border bg-card text-headline shadow-sm";
76
- return `${resolvedWidthClass} shrink-0 p-5 ${shell} ${className}`.trim();
82
+ ? "rounded-container border border-border bg-card text-headline shadow-sm ring-1 ring-border/50"
83
+ : "rounded-container border border-border bg-card text-headline shadow-sm";
84
+ return cn(`${resolvedWidthClass} shrink-0 p-5 ${shell} ${className}`);
77
85
  });
78
86
 
79
87
  function getItemClasses(itemId: string): string {
80
88
  const isActive = selectedItemId === itemId;
81
89
  const baseClasses =
82
- "focus-ring focus-ring--nav w-full cursor-pointer rounded-lg px-3 py-2.5 text-left transition-colors duration-150";
90
+ "focus-ring focus-ring--nav w-full cursor-pointer rounded-control px-3 py-2 text-left transition-colors duration-150";
83
91
  if (isActive) {
84
92
  return `${baseClasses} bg-nav-menu-active text-inherit shadow-sm ring-1 ring-border/80`;
85
93
  }
86
- return `${baseClasses} text-nav-menu-item hover:bg-nav-menu-hover hover:text-nav-menu-item-hover active:bg-base-200`;
94
+ return `${baseClasses} text-nav-menu-item hover:bg-nav-menu-hover hover:text-nav-menu-item-hover active:bg-surface-active`;
87
95
  }
88
96
 
89
97
  function handleSelect(item: SidebarPanelItem): void {
@@ -116,7 +124,7 @@
116
124
  </div>
117
125
  <button
118
126
  type="button"
119
- class="-mr-0.5 -mt-0.5 inline-flex size-9 shrink-0 cursor-pointer items-center justify-center rounded-lg text-nav-menu-item transition-colors hover:bg-nav-menu-hover hover:text-nav-menu-item-hover focus-ring focus-ring--nav"
127
+ class="-mr-0.5 -mt-0.5 inline-flex size-9 shrink-0 cursor-pointer items-center justify-center rounded-control text-nav-menu-item transition-colors hover:bg-nav-menu-hover hover:text-nav-menu-item-hover focus-ring focus-ring--nav"
120
128
  aria-label={closeLabel}
121
129
  onclick={handleClose}
122
130
  >
@@ -137,7 +145,7 @@
137
145
  bind:value={searchValue}
138
146
  placeholder={searchPlaceholder}
139
147
  aria-label={searchPlaceholder}
140
- class="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 focus-ring focus-ring--nav"
148
+ class="w-full min-w-0 min-h-10 rounded-container border-transparent !bg-transparent py-2 pl-10 text-sm ring-1 ring-border/60 hover:!bg-nav-menu-hover focus:!bg-transparent focus-ring focus-ring--nav"
141
149
  />
142
150
  </div>
143
151
  {/if}
@@ -154,7 +162,7 @@
154
162
  aria-selected={selectedItemId === item.id}
155
163
  >
156
164
  <span class="flex items-center justify-between gap-3">
157
- <span class="flex min-w-0 items-start gap-2.5">
165
+ <span class="flex min-w-0 items-start gap-3">
158
166
  {#if item.icon}
159
167
  {@const Icon = item.icon}
160
168
  <span
@@ -189,14 +197,14 @@
189
197
  </ul>
190
198
  {:else}
191
199
  <div
192
- class="rounded-xl border border-dashed border-border bg-transparent px-3.5 py-4 ring-1 ring-border/40"
200
+ class="rounded-container border border-dashed border-border bg-transparent px-4 py-4 ring-1 ring-border/40"
193
201
  >
194
202
  <h4 class="text-sm font-semibold text-headline">{emptyStateTitle}</h4>
195
203
  <p class="mt-1 text-sm leading-relaxed text-description">{emptyStateDescription}</p>
196
204
  {#if emptyStateActionLabel.trim() && onEmptyStateAction}
197
205
  <button
198
206
  type="button"
199
- class="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 transition-colors hover:bg-action-primary-hover focus-ring focus-ring--nav"
207
+ class="mt-3 inline-flex min-h-10 cursor-pointer items-center rounded-control bg-action-primary px-3 py-2 text-sm font-medium text-action-primary transition-colors hover:bg-action-primary-hover focus-ring focus-ring--nav"
200
208
  onclick={handleEmptyStateAction}
201
209
  >
202
210
  {emptyStateActionLabel.trim()}
@@ -10,6 +10,8 @@ export interface SidebarPanelItem {
10
10
  badgeText?: string;
11
11
  }
12
12
  interface Props {
13
+ class?: string;
14
+ /** @deprecated use `class`. */
13
15
  className?: string;
14
16
  widthClass?: string;
15
17
  variant?: "plain" | "elevated";
@@ -0,0 +1,112 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from "svelte";
3
+ import type { HTMLAttributes } from "svelte/elements";
4
+ import { cn } from "../util/cn.js";
5
+
6
+ /**
7
+ * The chrome of a sidebar and nothing else: width, surface, collapse
8
+ * behaviour, the inset its regions share, and a scroll container for the
9
+ * middle.
10
+ *
11
+ * `SidebarNavigation` is a fully-specified sidebar driven by ~44 props —
12
+ * excellent when your sidebar looks like that one, useless when it does
13
+ * not, because the parts are not reachable. This is the same shell with
14
+ * the regions left open, so you can drop in `SidebarBrandHeader`,
15
+ * `SidebarNavSection` and `SidebarFooter` (or your own markup) and skip
16
+ * the props you would never set.
17
+ *
18
+ * `SidebarNavigation` is built on it, so the two cannot drift apart.
19
+ *
20
+ * ```svelte
21
+ * <SidebarShell mode="collapsed">
22
+ * {#snippet header({ collapsed })}
23
+ * <SidebarBrandHeader {collapsed} brandName="Zabi" />
24
+ * {/snippet}
25
+ * <SidebarNavSection title="Main" sectionKey="main">…</SidebarNavSection>
26
+ * {#snippet footer({ collapsed, insetX })}
27
+ * <SidebarFooter {collapsed} className={insetX} />
28
+ * {/snippet}
29
+ * </SidebarShell>
30
+ * ```
31
+ */
32
+
33
+ /** Passed to every region so it can match the shell's collapse and inset. */
34
+ export interface SidebarRegionContext {
35
+ collapsed: boolean;
36
+ /** Horizontal padding utility the shell's own regions use. */
37
+ insetX: string;
38
+ }
39
+
40
+ type Props = Omit<HTMLAttributes<HTMLElement>, "class"> & {
41
+ mode?: "expanded" | "collapsed";
42
+ /** `rail` attaches to the viewport edge; `card` is a floating panel. */
43
+ layout?: "rail" | "card";
44
+ ariaLabel?: string;
45
+ class?: string;
46
+ /** @deprecated use `class`. */
47
+ className?: string;
48
+ /** Brand row, search — anything above the scrolling nav area. */
49
+ header?: Snippet<[SidebarRegionContext]>;
50
+ /** The scrolling middle. */
51
+ children?: Snippet<[SidebarRegionContext]>;
52
+ /** Account row, logout, theme toggle. Outside the scroll container. */
53
+ footer?: Snippet<[SidebarRegionContext]>;
54
+ };
55
+
56
+ let {
57
+ mode = "expanded",
58
+ layout = "rail",
59
+ ariaLabel = "Sidebar navigation",
60
+ class: classAttr = "",
61
+ className: legacyClass = "",
62
+ header,
63
+ children,
64
+ footer,
65
+ ...restProps
66
+ }: Props = $props();
67
+
68
+ const className = $derived(cn(classAttr, legacyClass));
69
+ const isCollapsed = $derived(mode === "collapsed");
70
+ const isCard = $derived(layout === "card");
71
+
72
+ const insetX = $derived(isCollapsed ? "px-2" : "px-4");
73
+
74
+ const containerClasses = $derived.by(() => {
75
+ const widthClass = isCollapsed ? "w-[72px]" : "w-[266px]";
76
+ const railSurface = "border-r border-border bg-background text-headline";
77
+ const cardSurface =
78
+ "border-r border-border bg-background text-headline shadow-sm";
79
+ const surfaceClasses = isCard ? cardSurface : railSurface;
80
+ const verticalPad = isCard ? "py-4" : "py-5";
81
+ const baseClasses = `flex h-full min-h-0 max-h-full flex-col overflow-visible ${verticalPad}`;
82
+
83
+ return cn(`${baseClasses} ${widthClass} ${surfaceClasses} ${className}`);
84
+ });
85
+
86
+ const region = $derived({ collapsed: isCollapsed, insetX });
87
+ </script>
88
+
89
+ <nav class={containerClasses} aria-label={ariaLabel} {...restProps}>
90
+ <div class="flex min-h-0 w-full min-w-0 flex-1 flex-col">
91
+ {#if header}
92
+ <div class={cn("flex w-full shrink-0 flex-col gap-5 pb-3", insetX)}>
93
+ {@render header(region)}
94
+ </div>
95
+ {/if}
96
+
97
+ <div
98
+ class={cn(
99
+ "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-2",
100
+ insetX,
101
+ )}
102
+ role="region"
103
+ aria-label="Navigation links"
104
+ >
105
+ {@render children?.(region)}
106
+ </div>
107
+ </div>
108
+
109
+ {#if footer}
110
+ {@render footer(region)}
111
+ {/if}
112
+ </nav>
@@ -0,0 +1,52 @@
1
+ import type { Snippet } from "svelte";
2
+ import type { HTMLAttributes } from "svelte/elements";
3
+ /**
4
+ * The chrome of a sidebar and nothing else: width, surface, collapse
5
+ * behaviour, the inset its regions share, and a scroll container for the
6
+ * middle.
7
+ *
8
+ * `SidebarNavigation` is a fully-specified sidebar driven by ~44 props —
9
+ * excellent when your sidebar looks like that one, useless when it does
10
+ * not, because the parts are not reachable. This is the same shell with
11
+ * the regions left open, so you can drop in `SidebarBrandHeader`,
12
+ * `SidebarNavSection` and `SidebarFooter` (or your own markup) and skip
13
+ * the props you would never set.
14
+ *
15
+ * `SidebarNavigation` is built on it, so the two cannot drift apart.
16
+ *
17
+ * ```svelte
18
+ * <SidebarShell mode="collapsed">
19
+ * {#snippet header({ collapsed })}
20
+ * <SidebarBrandHeader {collapsed} brandName="Zabi" />
21
+ * {/snippet}
22
+ * <SidebarNavSection title="Main" sectionKey="main">…</SidebarNavSection>
23
+ * {#snippet footer({ collapsed, insetX })}
24
+ * <SidebarFooter {collapsed} className={insetX} />
25
+ * {/snippet}
26
+ * </SidebarShell>
27
+ * ```
28
+ */
29
+ /** Passed to every region so it can match the shell's collapse and inset. */
30
+ export interface SidebarRegionContext {
31
+ collapsed: boolean;
32
+ /** Horizontal padding utility the shell's own regions use. */
33
+ insetX: string;
34
+ }
35
+ type Props = Omit<HTMLAttributes<HTMLElement>, "class"> & {
36
+ mode?: "expanded" | "collapsed";
37
+ /** `rail` attaches to the viewport edge; `card` is a floating panel. */
38
+ layout?: "rail" | "card";
39
+ ariaLabel?: string;
40
+ class?: string;
41
+ /** @deprecated use `class`. */
42
+ className?: string;
43
+ /** Brand row, search — anything above the scrolling nav area. */
44
+ header?: Snippet<[SidebarRegionContext]>;
45
+ /** The scrolling middle. */
46
+ children?: Snippet<[SidebarRegionContext]>;
47
+ /** Account row, logout, theme toggle. Outside the scroll container. */
48
+ footer?: Snippet<[SidebarRegionContext]>;
49
+ };
50
+ declare const SidebarShell: import("svelte").Component<Props, {}, "">;
51
+ type SidebarShell = ReturnType<typeof SidebarShell>;
52
+ export default SidebarShell;