zabi-components 7.0.2 → 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 +80 -22
  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 +2 -2
  138. package/dist/zabi-components-theme-dark-only.css +139 -92
  139. package/dist/zabi-components-theme-dark.css +139 -92
  140. package/dist/zabi-components-theme-only.css +268 -122
  141. package/dist/zabi-components-theme.css +268 -122
  142. package/dist/zabi-components.css +831 -883
  143. package/docs/lucide-icons.md +3 -1
  144. package/package.json +19 -24
  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
@@ -1,40 +1,49 @@
1
1
  <script lang="ts">
2
+ import type { Snippet } from "svelte";
2
3
  import { generateId } from "../util/ssr-safe.js";
4
+ import { cn } from "../util/cn.js";
3
5
 
4
6
  interface Props {
7
+ /** Extra classes for the host element. */
8
+ class?: string;
5
9
  tabs?: Array<{
6
10
  id: string;
7
11
  label: string;
8
12
  disabled?: boolean;
9
13
  }>;
14
+ /** Selected tab id; supports `bind:activeTab`. */
10
15
  activeTab?: string;
11
16
  variant?: "default" | "pills";
12
17
  onclick?: (event: Event) => void;
13
18
  onkeydown?: (event: Event) => void;
19
+ children?: Snippet<[{ activeTab: string }]>;
14
20
  }
15
21
 
16
22
  let {
23
+ class: className = "",
17
24
  tabs = [],
18
- activeTab = "",
25
+ activeTab = $bindable(""),
19
26
  variant = "default",
20
27
  children,
21
28
  ...restProps
22
- }: Props & { children?: any } = $props();
29
+ }: Props = $props();
23
30
 
24
31
  const tabsBaseId = generateId("tabs");
25
32
 
26
- function selectTab(tabId: string) {
33
+ /** Button refs keyed by tab id — roving tabindex needs focus to follow selection. */
34
+ const tabElements: Record<string, HTMLButtonElement | undefined> = {};
35
+
36
+ function selectTab(tabId: string, moveFocus = false) {
27
37
  activeTab = tabId;
38
+ if (moveFocus) {
39
+ tabElements[tabId]?.focus();
40
+ }
28
41
  }
29
42
 
30
43
  function getEnabledTabs() {
31
44
  return tabs.filter((tab) => !tab.disabled);
32
45
  }
33
46
 
34
- function getTabIndex(tabId: string) {
35
- return tabs.findIndex((tab) => tab.id === tabId);
36
- }
37
-
38
47
  function getTabId(tabId: string) {
39
48
  return `${tabsBaseId}-tab-${tabId}`;
40
49
  }
@@ -72,40 +81,37 @@
72
81
  const nextIndex =
73
82
  (fallbackIndex + direction + enabledTabs.length) %
74
83
  enabledTabs.length;
75
- selectTab(enabledTabs[nextIndex].id);
84
+ selectTab(enabledTabs[nextIndex].id, true);
76
85
  } else if (event.key === "Home") {
77
86
  event.preventDefault();
78
- selectTab(enabledTabs[0].id);
87
+ selectTab(enabledTabs[0].id, true);
79
88
  } else if (event.key === "End") {
80
89
  event.preventDefault();
81
- selectTab(enabledTabs[enabledTabs.length - 1].id);
82
- } else if (event.key === "Enter" || event.key === " ") {
83
- event.preventDefault();
84
- if (currentIndex === -1) {
85
- selectTab(enabledTabs[0].id);
86
- }
90
+ selectTab(enabledTabs[enabledTabs.length - 1].id, true);
87
91
  }
92
+ // Enter/Space: native <button> click already selects the tab.
88
93
  }
89
94
  </script>
90
95
 
91
- <div class="tabs-container">
96
+ <div class={cn("tabs-container", className)}>
92
97
  <div
93
98
  class="flex border-b border-base-200"
94
99
  role="tablist"
95
- tabindex="0"
100
+ tabindex="-1"
96
101
  onkeydown={handleKeydown}
97
102
  >
98
103
  {#each tabs as tab (tab.id)}
99
104
  <button
105
+ bind:this={tabElements[tab.id]}
100
106
  type="button"
101
107
  role="tab"
102
108
  id={getTabId(tab.id)}
103
- class="focus-ring cursor-pointer border-b-2 px-4 py-2 text-sm font-medium transition-colors focus:outline-none focus-visible:outline-none active:bg-base-100 disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:border-transparent disabled:hover:text-description {activeTab ===
109
+ class="focus-ring cursor-pointer border-b-2 px-4 py-2 text-sm font-medium transition-colors focus:outline-none focus-visible:outline-none active:bg-surface-active disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:border-transparent disabled:hover:text-description {activeTab ===
104
110
  tab.id
105
111
  ? variant === 'pills'
106
112
  ? 'border-brand-500 bg-brand-100 text-brand-700'
107
113
  : 'border-brand-500 text-body'
108
- : 'border-transparent text-description hover:border-base-300 hover:text-body'}"
114
+ : 'border-transparent text-description hover:border-border-medium hover:text-body'}"
109
115
  onclick={() => selectTab(tab.id)}
110
116
  disabled={tab.disabled}
111
117
  aria-selected={activeTab === tab.id}
@@ -1,17 +1,21 @@
1
+ import type { Snippet } from "svelte";
1
2
  interface Props {
3
+ /** Extra classes for the host element. */
4
+ class?: string;
2
5
  tabs?: Array<{
3
6
  id: string;
4
7
  label: string;
5
8
  disabled?: boolean;
6
9
  }>;
10
+ /** Selected tab id; supports `bind:activeTab`. */
7
11
  activeTab?: string;
8
12
  variant?: "default" | "pills";
9
13
  onclick?: (event: Event) => void;
10
14
  onkeydown?: (event: Event) => void;
15
+ children?: Snippet<[{
16
+ activeTab: string;
17
+ }]>;
11
18
  }
12
- type $$ComponentProps = Props & {
13
- children?: any;
14
- };
15
- declare const Tabs: import("svelte").Component<$$ComponentProps, {}, "">;
19
+ declare const Tabs: import("svelte").Component<Props, {}, "activeTab">;
16
20
  type Tabs = ReturnType<typeof Tabs>;
17
21
  export default Tabs;
@@ -1,6 +1,7 @@
1
1
  <script lang="ts">
2
2
  import ToasterToast from './ToasterToast.svelte';
3
3
  import { toastStore } from './toast-store.js';
4
+ import { cn } from "../util/cn.js";
4
5
 
5
6
  interface Props {
6
7
  class?: string;
@@ -10,11 +11,11 @@
10
11
  </script>
11
12
 
12
13
  <div
13
- class="pointer-events-none fixed bottom-4 right-4 z-toast flex min-w-0 w-[min(24rem,calc(100vw-2rem))] flex-col gap-2 p-4 {className}"
14
+ class={cn("pointer-events-none fixed bottom-4 right-4 z-toast flex min-w-0 w-[min(24rem,calc(100vw-2rem))] flex-col gap-2 p-4", className)}
14
15
  role="region"
15
16
  aria-label="Notifications"
16
- aria-live="polite"
17
17
  >
18
+ <!-- Each toast owns its role="status"/"alert"; a region-level aria-live would announce twice. -->
18
19
  {#each $toastStore as toast (toast.id)}
19
20
  <ToasterToast {toast} />
20
21
  {/each}
@@ -12,12 +12,15 @@
12
12
  } from '@lucide/svelte';
13
13
  import Button from '../atoms/Button.svelte';
14
14
  import { toastStore, type ToastItem } from './toast-store.js';
15
+ import { cn } from "../util/cn.js";
15
16
 
16
17
  interface Props {
18
+ /** Extra classes for the host element. */
19
+ class?: string;
17
20
  toast: ToastItem;
18
21
  }
19
22
 
20
- let { toast }: Props = $props();
23
+ let { class: className = "", toast }: Props = $props();
21
24
 
22
25
  /** Seconds until auto-dismiss; `0` means no countdown (manual dismiss only). */
23
26
  function autoDismissSeconds(duration: number | undefined): number {
@@ -31,6 +34,10 @@
31
34
  let count = $state(0);
32
35
  let timerActive = $state(false);
33
36
  let isExpanded = $state(false);
37
+ /** Hover or keyboard focus inside the toast pauses auto-dismiss (WCAG 2.2.1). */
38
+ let hovered = $state(false);
39
+ let focusWithin = $state(false);
40
+ const paused = $derived(hovered || focusWithin);
34
41
 
35
42
  let intervalRef: ReturnType<typeof setInterval> | undefined;
36
43
 
@@ -90,6 +97,7 @@
90
97
  timerActive = true;
91
98
 
92
99
  intervalRef = setInterval(() => {
100
+ if (paused) return;
93
101
  count -= 1;
94
102
  if (count <= 0) {
95
103
  stopTimer();
@@ -113,19 +121,38 @@
113
121
  isExpanded = false;
114
122
  }
115
123
 
124
+ function handleFocusOut(event: FocusEvent) {
125
+ const next = event.relatedTarget as Node | null;
126
+ if (!next || !(event.currentTarget as HTMLElement).contains(next)) {
127
+ focusWithin = false;
128
+ }
129
+ }
130
+
116
131
  const toastEnter = { y: 18, duration: 260, opacity: 0, easing: cubicOut };
117
132
  const toastLeave = { y: 14, duration: 300, opacity: 0, easing: cubicOut };
118
133
  </script>
119
134
 
120
135
  <div
121
- class="pointer-events-auto relative w-full min-w-[18rem] overflow-hidden rounded-2xl border border-border bg-surface-1 shadow-xl"
136
+ class={cn("pointer-events-auto relative w-full min-w-[18rem] overflow-hidden rounded-overlay border border-border-overlay bg-surface-overlay shadow-lg", className)}
122
137
  in:fly={toastEnter}
123
138
  out:fly={toastLeave}
124
- role={toast.type === 'error' ? 'alert' : 'status'}
139
+ onmouseenter={() => (hovered = true)}
140
+ onmouseleave={() => (hovered = false)}
141
+ onfocusin={() => (focusWithin = true)}
142
+ onfocusout={handleFocusOut}
143
+ role="group"
144
+ aria-label={headerTitle}
145
+ data-paused={paused}
125
146
  >
126
147
  <div class="p-4">
127
148
  <div class="flex items-start gap-3">
128
- <div class="shrink-0 pt-0.5">
149
+ <!-- Live region holds only title + message, so the countdown and buttons are never re-announced. -->
150
+ <div
151
+ class="flex min-w-0 flex-1 items-start gap-3"
152
+ role={toast.type === 'error' ? 'alert' : 'status'}
153
+ aria-atomic="true"
154
+ >
155
+ <div class="shrink-0 mt-0.5">
129
156
  {#if toast.type === 'success'}
130
157
  <CheckCircle class="size-5 {statusIconClass}" aria-hidden="true" />
131
158
  {:else if toast.type === 'error'}
@@ -138,12 +165,16 @@
138
165
  </div>
139
166
  <h4 class="min-w-0 flex-1 text-base font-semibold text-headline">
140
167
  {headerTitle}
168
+ {#if toast.message.trim() && toast.message.trim() !== headerTitle}
169
+ <span class="sr-only">{toast.message}</span>
170
+ {/if}
141
171
  </h4>
142
- <div class="flex shrink-0 items-center gap-0.5">
172
+ </div>
173
+ <div class="flex shrink-0 items-center gap-1">
143
174
  {#if hasExpandable}
144
175
  <button
145
176
  type="button"
146
- class="focus-ring cursor-pointer rounded-md p-1 text-description transition-colors hover:bg-base-100 hover:text-headline focus:outline-none"
177
+ class="focus-ring cursor-pointer rounded-control p-1 text-description transition-colors hover:bg-surface-overlay-hover hover:text-headline focus:outline-none"
147
178
  aria-expanded={isExpanded}
148
179
  aria-controls="toaster-expand-{toast.id}"
149
180
  aria-label={isExpanded ? 'Collapse details' : 'Expand details'}
@@ -159,7 +190,7 @@
159
190
  {/if}
160
191
  <button
161
192
  type="button"
162
- class="focus-ring cursor-pointer rounded-md p-1 text-description transition-colors hover:bg-base-100 hover:text-headline focus:outline-none"
193
+ class="focus-ring cursor-pointer rounded-control p-1 text-description transition-colors hover:bg-surface-overlay-hover hover:text-headline focus:outline-none"
163
194
  onclick={handleDismiss}
164
195
  aria-label="Dismiss notification"
165
196
  >
@@ -189,11 +220,11 @@
189
220
 
190
221
  {#if timerActive}
191
222
  <div class="px-4 pb-3" transition:fade={{ duration: 180 }}>
192
- <p class="text-xs text-description">
193
- This message will close in {count} seconds.
223
+ <p class="text-xs text-description" data-toast-countdown>
224
+ {paused ? 'Paused — closes' : 'This message will close'} in {count} seconds.
194
225
  <button
195
226
  type="button"
196
- class="focus-ring cursor-pointer rounded-sm text-link underline-offset-2 hover:underline focus:outline-none"
227
+ class="focus-ring cursor-pointer rounded-control text-link underline-offset-2 hover:underline focus:outline-none"
197
228
  onclick={stopTimer}
198
229
  >
199
230
  Click to stop
@@ -1,5 +1,7 @@
1
1
  import { type ToastItem } from './toast-store.js';
2
2
  interface Props {
3
+ /** Extra classes for the host element. */
4
+ class?: string;
3
5
  toast: ToastItem;
4
6
  }
5
7
  declare const ToasterToast: import("svelte").Component<Props, {}, "">;
@@ -6,6 +6,8 @@
6
6
  import { LogOut, Moon, Sun, User } from "@lucide/svelte";
7
7
 
8
8
  interface Props {
9
+ /** Extra classes for the host element. */
10
+ class?: string;
9
11
  /** Target id for `aria-controls` from the sidebar footer/nav. */
10
12
  panelId?: string;
11
13
  style?: string;
@@ -24,6 +26,7 @@
24
26
  }
25
27
 
26
28
  let {
29
+ class: className = "",
27
30
  panelId = generateId("account-panel"),
28
31
  style = "",
29
32
  profileName = "Account",
@@ -96,6 +99,7 @@
96
99
  <div
97
100
  id={panelId}
98
101
  {style}
102
+ class={className}
99
103
  role="dialog"
100
104
  aria-modal="false"
101
105
  aria-label="Account panel"
@@ -1,4 +1,6 @@
1
1
  interface Props {
2
+ /** Extra classes for the host element. */
3
+ class?: string;
2
4
  /** Target id for `aria-controls` from the sidebar footer/nav. */
3
5
  panelId?: string;
4
6
  style?: string;
@@ -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;