zabi-components 6.0.0 → 7.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 (197) hide show
  1. package/README.md +137 -1473
  2. package/dist/atoms/ActionPanel.svelte +116 -0
  3. package/dist/atoms/ActionPanel.svelte.d.ts +19 -0
  4. package/dist/atoms/Badge.svelte +1 -1
  5. package/dist/atoms/Badge.svelte.d.ts +1 -1
  6. package/dist/atoms/Button.svelte +35 -16
  7. package/dist/atoms/Button.svelte.d.ts +3 -1
  8. package/dist/atoms/Card.svelte +12 -3
  9. package/dist/atoms/Card.svelte.d.ts +2 -2
  10. package/dist/atoms/CardHeader.svelte +8 -4
  11. package/dist/atoms/CardHeader.svelte.d.ts +4 -2
  12. package/dist/atoms/Checkbox.svelte +50 -76
  13. package/dist/atoms/Checkbox.svelte.d.ts +7 -2
  14. package/dist/atoms/CodeBlock.svelte +20 -34
  15. package/dist/atoms/CodeBlock.svelte.d.ts +1 -0
  16. package/dist/atoms/ColorPicker.svelte +2 -6
  17. package/dist/atoms/Container.svelte +41 -0
  18. package/dist/atoms/Container.svelte.d.ts +12 -0
  19. package/dist/atoms/Divider.svelte +68 -0
  20. package/dist/atoms/Divider.svelte.d.ts +13 -0
  21. package/dist/atoms/FeatureCard.svelte +35 -50
  22. package/dist/atoms/FeatureCard.svelte.d.ts +9 -4
  23. package/dist/atoms/IconButton.svelte +33 -24
  24. package/dist/atoms/IconButton.svelte.d.ts +4 -8
  25. package/dist/atoms/Input.svelte +58 -26
  26. package/dist/atoms/Input.svelte.d.ts +6 -1
  27. package/dist/atoms/List.svelte +2 -7
  28. package/dist/atoms/List.svelte.d.ts +1 -6
  29. package/dist/atoms/ListItem.svelte +54 -65
  30. package/dist/atoms/ListItem.svelte.d.ts +7 -13
  31. package/dist/atoms/ListItemLeading.svelte +19 -0
  32. package/dist/atoms/ListItemLeading.svelte.d.ts +9 -0
  33. package/dist/atoms/Progress.svelte +1 -1
  34. package/dist/atoms/Radio.svelte +51 -0
  35. package/dist/atoms/Radio.svelte.d.ts +14 -0
  36. package/dist/atoms/Select.svelte +23 -30
  37. package/dist/atoms/Select.svelte.d.ts +0 -10
  38. package/dist/atoms/SelectionControl.svelte +98 -0
  39. package/dist/atoms/SelectionControl.svelte.d.ts +26 -0
  40. package/dist/atoms/Skeleton.svelte +39 -15
  41. package/dist/atoms/Skeleton.svelte.d.ts +7 -3
  42. package/dist/atoms/Table.svelte +27 -0
  43. package/dist/atoms/Table.svelte.d.ts +10 -0
  44. package/dist/atoms/Text.svelte +46 -0
  45. package/dist/atoms/Text.svelte.d.ts +13 -0
  46. package/dist/atoms/Textarea.svelte +74 -53
  47. package/dist/atoms/Textarea.svelte.d.ts +6 -2
  48. package/dist/atoms/ThemeToggle.svelte +35 -24
  49. package/dist/atoms/ThemeToggle.svelte.d.ts +1 -0
  50. package/dist/atoms/Toast.svelte +55 -31
  51. package/dist/atoms/Toast.svelte.d.ts +4 -1
  52. package/dist/atoms/Toggle.svelte +37 -22
  53. package/dist/atoms/Toggle.svelte.d.ts +2 -1
  54. package/dist/atoms/Tooltip.svelte +90 -11
  55. package/dist/atoms/index.d.ts +6 -0
  56. package/dist/atoms/index.js +6 -0
  57. package/dist/atoms/selection-control.styles.d.ts +25 -0
  58. package/dist/atoms/selection-control.styles.js +30 -0
  59. package/dist/components/atoms/index.d.ts +6 -0
  60. package/dist/components/atoms/index.d.ts.map +1 -1
  61. package/dist/components/atoms/selection-control.styles.d.ts +26 -0
  62. package/dist/components/atoms/selection-control.styles.d.ts.map +1 -0
  63. package/dist/components/index.d.ts +41 -5
  64. package/dist/components/index.d.ts.map +1 -1
  65. package/dist/components/molecules/index.d.ts +10 -0
  66. package/dist/components/molecules/index.d.ts.map +1 -1
  67. package/dist/components/molecules/navigation-menu-context.d.ts +11 -0
  68. package/dist/components/molecules/navigation-menu-context.d.ts.map +1 -1
  69. package/dist/components/molecules/toast-store.d.ts +39 -0
  70. package/dist/components/molecules/toast-store.d.ts.map +1 -0
  71. package/dist/components/organisms/index.d.ts +2 -1
  72. package/dist/components/organisms/index.d.ts.map +1 -1
  73. package/dist/components/types/page.types.d.ts +69 -0
  74. package/dist/components/types/page.types.d.ts.map +1 -0
  75. package/dist/components/types/page.types.js +1 -0
  76. package/dist/components/types/variants.d.ts +64 -0
  77. package/dist/components/types/variants.d.ts.map +1 -0
  78. package/dist/components/types/variants.js +44 -0
  79. package/dist/components/util/fixed-sidebar-flyout.d.ts +23 -0
  80. package/dist/components/util/fixed-sidebar-flyout.d.ts.map +1 -0
  81. package/dist/components/util/focus-utils.d.ts +12 -0
  82. package/dist/components/util/focus-utils.d.ts.map +1 -0
  83. package/dist/components/util/ssr-safe.d.ts +17 -0
  84. package/dist/components/util/ssr-safe.d.ts.map +1 -0
  85. package/dist/index.d.ts +41 -5
  86. package/dist/index.js +44 -8
  87. package/dist/lib/layout-width-tokens.d.ts +17 -0
  88. package/dist/lib/layout-width-tokens.d.ts.map +1 -0
  89. package/dist/lib/showcase/component-docs/Button.d.ts +3 -0
  90. package/dist/lib/showcase/component-docs/Button.d.ts.map +1 -0
  91. package/dist/lib/showcase/component-docs/Input.d.ts +3 -0
  92. package/dist/lib/showcase/component-docs/Input.d.ts.map +1 -0
  93. package/dist/lib/showcase/component-docs/List.d.ts +3 -0
  94. package/dist/lib/showcase/component-docs/List.d.ts.map +1 -0
  95. package/dist/lib/showcase/component-docs/Modal.d.ts +3 -0
  96. package/dist/lib/showcase/component-docs/Modal.d.ts.map +1 -0
  97. package/dist/lib/showcase/component-docs/Radio.d.ts +3 -0
  98. package/dist/lib/showcase/component-docs/Radio.d.ts.map +1 -0
  99. package/dist/lib/showcase/component-docs/SidebarNavigation.d.ts +3 -0
  100. package/dist/lib/showcase/component-docs/SidebarNavigation.d.ts.map +1 -0
  101. package/dist/lib/showcase/component-docs/Skeleton.d.ts +3 -0
  102. package/dist/lib/showcase/component-docs/Skeleton.d.ts.map +1 -0
  103. package/dist/lib/showcase/component-docs/_shared.d.ts +12 -0
  104. package/dist/lib/showcase/component-docs/_shared.d.ts.map +1 -0
  105. package/dist/lib/showcase/component-docs/index.d.ts +4 -0
  106. package/dist/lib/showcase/component-docs/index.d.ts.map +1 -0
  107. package/dist/lib/showcase/components-catalog.d.ts.map +1 -1
  108. package/dist/lib/showcase/components-showcase-constants.d.ts +26 -0
  109. package/dist/lib/showcase/components-showcase-constants.d.ts.map +1 -1
  110. package/dist/molecules/Alert.svelte +50 -23
  111. package/dist/molecules/Alert.svelte.d.ts +1 -1
  112. package/dist/molecules/ComponentDemo.svelte +2 -2
  113. package/dist/molecules/ContactForm.svelte +1 -1
  114. package/dist/molecules/Dropdown.svelte +99 -109
  115. package/dist/molecules/Dropdown.svelte.d.ts +12 -6
  116. package/dist/molecules/EmptyState.svelte +44 -0
  117. package/dist/molecules/EmptyState.svelte.d.ts +11 -0
  118. package/dist/molecules/FormField.svelte +89 -0
  119. package/dist/molecules/FormField.svelte.d.ts +23 -0
  120. package/dist/molecules/Header.svelte +41 -0
  121. package/dist/molecules/Header.svelte.d.ts +10 -0
  122. package/dist/molecules/Modal.svelte +37 -24
  123. package/dist/molecules/Modal.svelte.d.ts +4 -2
  124. package/dist/molecules/NavigationMenu.svelte +16 -0
  125. package/dist/molecules/NavigationMenu.svelte.d.ts +2 -0
  126. package/dist/molecules/NavigationMenuContent.svelte +9 -1
  127. package/dist/molecules/NavigationMenuLink.svelte +1 -1
  128. package/dist/molecules/NavigationMenuTrigger.svelte +9 -1
  129. package/dist/molecules/Page.svelte +16 -0
  130. package/dist/molecules/Page.svelte.d.ts +8 -0
  131. package/dist/molecules/PropsTable.svelte +61 -0
  132. package/dist/molecules/PropsTable.svelte.d.ts +8 -0
  133. package/dist/molecules/RadioGroup.svelte +197 -0
  134. package/dist/molecules/RadioGroup.svelte.d.ts +20 -0
  135. package/dist/molecules/Section.svelte +2 -11
  136. package/dist/molecules/Section.svelte.d.ts +2 -11
  137. package/dist/molecules/SidebarBrandHeader.svelte +68 -0
  138. package/dist/molecules/SidebarBrandHeader.svelte.d.ts +10 -0
  139. package/dist/molecules/SidebarFooter.svelte +119 -0
  140. package/dist/molecules/SidebarFooter.svelte.d.ts +23 -0
  141. package/dist/molecules/SidebarNavSection.svelte +49 -0
  142. package/dist/molecules/SidebarNavSection.svelte.d.ts +14 -0
  143. package/dist/molecules/SlideUp.svelte +19 -14
  144. package/dist/molecules/SlideUp.svelte.d.ts +1 -1
  145. package/dist/molecules/Tabs.svelte +4 -9
  146. package/dist/molecules/Toaster.svelte +21 -0
  147. package/dist/molecules/Toaster.svelte.d.ts +6 -0
  148. package/dist/molecules/ToasterToast.svelte +217 -0
  149. package/dist/molecules/ToasterToast.svelte.d.ts +7 -0
  150. package/dist/molecules/index.d.ts +10 -0
  151. package/dist/molecules/index.js +10 -0
  152. package/dist/molecules/navigation-menu-context.d.ts +11 -0
  153. package/dist/molecules/navigation-menu-context.js +18 -0
  154. package/dist/molecules/toast-store.d.ts +38 -0
  155. package/dist/molecules/toast-store.js +34 -0
  156. package/dist/organisms/SidebarAccountPanel.svelte +118 -0
  157. package/dist/organisms/SidebarAccountPanel.svelte.d.ts +20 -0
  158. package/dist/organisms/SidebarNavigation.svelte +267 -237
  159. package/dist/organisms/SidebarNavigation.svelte.d.ts +21 -5
  160. package/dist/organisms/SidebarPanel.svelte +54 -22
  161. package/dist/organisms/SidebarPanel.svelte.d.ts +3 -0
  162. package/dist/organisms/{Navbar.svelte → TopNavbar.svelte} +22 -17
  163. package/dist/organisms/TopNavbar.svelte.d.ts +35 -0
  164. package/dist/organisms/index.d.ts +2 -1
  165. package/dist/organisms/index.js +2 -1
  166. package/dist/routes/lib/focus-utils.d.ts +7 -18
  167. package/dist/routes/lib/focus-utils.ts +32 -34
  168. package/dist/routes/lib/ssr-safe.ts +5 -4
  169. package/dist/routes/lib/variant-utils.ts +48 -39
  170. package/dist/types/components.d.ts +187 -0
  171. package/dist/types/components.d.ts.map +1 -0
  172. package/dist/types/events.d.ts +200 -0
  173. package/dist/types/events.d.ts.map +1 -0
  174. package/dist/types/index.d.ts +171 -0
  175. package/dist/types/index.d.ts.map +1 -0
  176. package/dist/types/page.types.d.ts +2 -31
  177. package/dist/types/page.types.d.ts.map +1 -1
  178. package/dist/types/page.types.js +1 -0
  179. package/dist/types/page.types.ts +79 -0
  180. package/dist/types/variants.d.ts +3 -0
  181. package/dist/types/variants.d.ts.map +1 -0
  182. package/dist/types/variants.js +1 -0
  183. package/dist/types/variants.ts +5 -6
  184. package/dist/util/fixed-sidebar-flyout.d.ts +22 -0
  185. package/dist/util/fixed-sidebar-flyout.js +148 -0
  186. package/dist/util/focus-utils.d.ts +11 -0
  187. package/dist/util/focus-utils.js +94 -0
  188. package/dist/util/ssr-safe.d.ts +16 -0
  189. package/dist/util/ssr-safe.js +48 -0
  190. package/dist/zabi-components-colors.css +14 -1
  191. package/dist/zabi-components-theme-dark-only.css +5 -0
  192. package/dist/zabi-components-theme-dark.css +5 -0
  193. package/dist/zabi-components-theme-only.css +9 -1
  194. package/dist/zabi-components-theme.css +9 -1
  195. package/dist/zabi-components.css +877 -192
  196. package/package.json +19 -7
  197. package/dist/organisms/Navbar.svelte.d.ts +0 -41
@@ -1,4 +1,6 @@
1
+ import type { Snippet } from "svelte";
1
2
  import type { Component } from "svelte";
3
+ import type { ButtonVariant, SizeVariant } from "../types/variants.js";
2
4
  export interface SidebarNavigationItem {
3
5
  id: string;
4
6
  label: string;
@@ -10,13 +12,19 @@ export interface SidebarNavigationItem {
10
12
  badgeText?: string | number;
11
13
  badgeCount?: number;
12
14
  group?: "primary" | "secondary";
15
+ /** Groups items under a section heading in the rail. */
16
+ section?: string;
13
17
  }
14
18
  interface Props {
15
19
  mode?: "expanded" | "collapsed";
20
+ layout?: "rail" | "card";
16
21
  items?: SidebarNavigationItem[];
17
22
  currentPath?: string;
18
23
  ariaLabel?: string;
19
24
  className?: string;
25
+ logoSrc?: string;
26
+ logoAlt?: string;
27
+ brandName?: string;
20
28
  showProfile?: boolean;
21
29
  profileName?: string;
22
30
  profileEmail?: string;
@@ -25,6 +33,13 @@ interface Props {
25
33
  searchMode?: "input" | "button";
26
34
  searchPlaceholder?: string;
27
35
  searchValue?: string;
36
+ /** Trigger icon when search is a button or rail is collapsed; input mode keeps `Search` on the field. */
37
+ searchTriggerIcon?: Component<{
38
+ size?: number;
39
+ class?: string;
40
+ }>;
41
+ searchTriggerVariant?: ButtonVariant;
42
+ searchTriggerSize?: SizeVariant;
28
43
  showLogout?: boolean;
29
44
  logoutLabel?: string;
30
45
  showThemeToggle?: boolean;
@@ -38,12 +53,13 @@ interface Props {
38
53
  onLogout?: () => void;
39
54
  onThemeToggle?: (nextIsLightMode: boolean) => void;
40
55
  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
- */
56
+ onProfileClick?: (event?: MouseEvent) => void;
57
+ profilePanelOpen?: boolean;
58
+ profilePanelControlsId?: string;
59
+ profilePanel?: Snippet;
60
+ /** Parent nav href to highlight when `currentPath` is a deeper leaf (e.g. category row). */
45
61
  activePrimaryHref?: string;
46
62
  }
47
- declare const SidebarNavigation: Component<Props, {}, "searchValue" | "isLightMode">;
63
+ declare const SidebarNavigation: Component<Props, {}, "isLightMode" | "searchValue">;
48
64
  type SidebarNavigation = ReturnType<typeof SidebarNavigation>;
49
65
  export default SidebarNavigation;
@@ -15,6 +15,7 @@
15
15
  interface Props {
16
16
  className?: string;
17
17
  widthClass?: string;
18
+ variant?: "plain" | "elevated";
18
19
  ariaLabel?: string;
19
20
  title?: string;
20
21
  subtitle?: string;
@@ -25,15 +26,18 @@
25
26
  selectedItemId?: string;
26
27
  emptyStateTitle?: string;
27
28
  emptyStateDescription?: string;
29
+ emptyStateActionLabel?: string;
28
30
  selectLabel?: string;
29
31
  closeLabel?: string;
30
32
  onSelect?: (item: SidebarPanelItem) => void;
31
33
  onClose?: () => void;
34
+ onEmptyStateAction?: () => void;
32
35
  }
33
36
 
34
37
  let {
35
38
  className = "",
36
39
  widthClass = "w-80",
40
+ variant = "elevated",
37
41
  ariaLabel = "Picker panel",
38
42
  title = "Items",
39
43
  subtitle = "Choose an item to continue",
@@ -44,10 +48,12 @@
44
48
  selectedItemId = $bindable(""),
45
49
  emptyStateTitle = "No matches",
46
50
  emptyStateDescription = "Try a different keyword.",
51
+ emptyStateActionLabel = "",
47
52
  selectLabel = "Select item",
48
53
  closeLabel = "Close panel",
49
54
  onSelect,
50
55
  onClose,
56
+ onEmptyStateAction,
51
57
  ...restProps
52
58
  }: Props = $props();
53
59
 
@@ -60,19 +66,24 @@
60
66
  : items,
61
67
  );
62
68
 
69
+ const isElevated = $derived(variant === "elevated");
70
+
63
71
  const containerClasses = $derived.by(() => {
64
72
  const resolvedWidthClass = widthClass.trim() || "w-80";
65
- return `${resolvedWidthClass} shrink-0 rounded-2xl border border-border bg-background p-4 ${className}`.trim();
73
+ 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();
66
77
  });
67
78
 
68
79
  function getItemClasses(itemId: string): string {
69
80
  const isActive = selectedItemId === itemId;
70
81
  const baseClasses =
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";
82
+ "focus-ring focus-ring--nav w-full cursor-pointer rounded-lg px-3 py-2.5 text-left transition-colors duration-150";
72
83
  if (isActive) {
73
- return `${baseClasses} bg-action-primary-subtle text-headline`;
84
+ return `${baseClasses} bg-nav-menu-active text-inherit shadow-sm ring-1 ring-border/80`;
74
85
  }
75
- return `${baseClasses} hover:bg-base-50 text-description`;
86
+ return `${baseClasses} text-nav-menu-item hover:bg-nav-menu-hover hover:text-nav-menu-item-hover active:bg-base-200`;
76
87
  }
77
88
 
78
89
  function handleSelect(item: SidebarPanelItem): void {
@@ -87,44 +98,52 @@
87
98
  onClose();
88
99
  }
89
100
  }
101
+
102
+ function handleEmptyStateAction(): void {
103
+ if (onEmptyStateAction) {
104
+ onEmptyStateAction();
105
+ }
106
+ }
90
107
  </script>
91
108
 
92
109
  <aside class={containerClasses} aria-label={ariaLabel} {...restProps}>
93
- <div class="mb-4 flex items-start justify-between gap-3">
94
- <div class="min-w-0">
95
- <h3 class="truncate text-base font-semibold text-headline">{title}</h3>
96
- <p class="mt-1 text-sm text-description">{subtitle}</p>
110
+ <div class="mb-5 flex items-start justify-between gap-3">
111
+ <div class="min-w-0 pr-1">
112
+ <h3 class="truncate text-base font-semibold tracking-tight text-headline">
113
+ {title}
114
+ </h3>
115
+ <p class="mt-0.5 text-sm leading-relaxed text-description">{subtitle}</p>
97
116
  </div>
98
117
  <button
99
118
  type="button"
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"
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"
101
120
  aria-label={closeLabel}
102
121
  onclick={handleClose}
103
122
  >
104
- <X size={16} />
123
+ <X size={18} />
105
124
  </button>
106
125
  </div>
107
126
 
108
127
  {#if showSearch}
109
- <div class="relative mb-3">
128
+ <div class="relative mb-5">
110
129
  <span
111
- class="pointer-events-none absolute left-4 top-1/2 z-10 -translate-y-1/2 text-description"
130
+ class="pointer-events-none absolute left-3 top-1/2 z-10 -translate-y-1/2 text-description"
112
131
  aria-hidden="true"
113
132
  >
114
- <Search size={18} />
133
+ <Search size={17} />
115
134
  </span>
116
135
  <Input
117
136
  type="search"
118
137
  bind:value={searchValue}
119
138
  placeholder={searchPlaceholder}
120
139
  aria-label={searchPlaceholder}
121
- class="w-full min-w-0 rounded-xl pl-11"
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"
122
141
  />
123
142
  </div>
124
143
  {/if}
125
144
 
126
145
  {#if filteredItems.length > 0}
127
- <ul class="space-y-2" role="listbox" aria-label={selectLabel}>
146
+ <ul class="space-y-1" role="listbox" aria-label={selectLabel}>
128
147
  {#each filteredItems as item (item.id)}
129
148
  <li role="presentation">
130
149
  <button
@@ -135,18 +154,20 @@
135
154
  aria-selected={selectedItemId === item.id}
136
155
  >
137
156
  <span class="flex items-center justify-between gap-3">
138
- <span class="flex min-w-0 items-start gap-2">
157
+ <span class="flex min-w-0 items-start gap-2.5">
139
158
  {#if item.icon}
140
159
  {@const Icon = item.icon}
141
160
  <span
142
- class="mt-0.5 inline-flex size-4 shrink-0 items-center justify-center text-current"
161
+ class="mt-0.5 inline-flex size-5 shrink-0 items-center justify-center text-current"
143
162
  aria-hidden="true"
144
163
  >
145
- <Icon size={16} />
164
+ <Icon size={18} />
146
165
  </span>
147
166
  {/if}
148
167
  <span class="min-w-0">
149
- <span class="block truncate text-sm font-medium text-current">
168
+ <span
169
+ class="block truncate text-sm font-medium text-current"
170
+ >
150
171
  {item.label}
151
172
  </span>
152
173
  {#if item.description}
@@ -167,9 +188,20 @@
167
188
  {/each}
168
189
  </ul>
169
190
  {:else}
170
- <div class="rounded-xl border border-dashed border-border bg-base-50 p-4">
191
+ <div
192
+ class="rounded-xl border border-dashed border-border bg-transparent px-3.5 py-4 ring-1 ring-border/40"
193
+ >
171
194
  <h4 class="text-sm font-semibold text-headline">{emptyStateTitle}</h4>
172
- <p class="mt-1 text-sm text-description">{emptyStateDescription}</p>
195
+ <p class="mt-1 text-sm leading-relaxed text-description">{emptyStateDescription}</p>
196
+ {#if emptyStateActionLabel.trim() && onEmptyStateAction}
197
+ <button
198
+ 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"
200
+ onclick={handleEmptyStateAction}
201
+ >
202
+ {emptyStateActionLabel.trim()}
203
+ </button>
204
+ {/if}
173
205
  </div>
174
206
  {/if}
175
- </aside>
207
+ </aside>
@@ -12,6 +12,7 @@ export interface SidebarPanelItem {
12
12
  interface Props {
13
13
  className?: string;
14
14
  widthClass?: string;
15
+ variant?: "plain" | "elevated";
15
16
  ariaLabel?: string;
16
17
  title?: string;
17
18
  subtitle?: string;
@@ -22,10 +23,12 @@ interface Props {
22
23
  selectedItemId?: string;
23
24
  emptyStateTitle?: string;
24
25
  emptyStateDescription?: string;
26
+ emptyStateActionLabel?: string;
25
27
  selectLabel?: string;
26
28
  closeLabel?: string;
27
29
  onSelect?: (item: SidebarPanelItem) => void;
28
30
  onClose?: () => void;
31
+ onEmptyStateAction?: () => void;
29
32
  }
30
33
  declare const SidebarPanel: Component<Props, {}, "searchValue" | "selectedItemId">;
31
34
  type SidebarPanel = ReturnType<typeof SidebarPanel>;
@@ -2,8 +2,7 @@
2
2
  import ThemeToggle from "../atoms/ThemeToggle.svelte";
3
3
  import type { Component, Snippet } from "svelte";
4
4
 
5
- /** Link shown in the built-in nav list (when not using the `nav` snippet). */
6
- export interface NavbarNavItem {
5
+ export interface TopNavbarNavItem {
7
6
  label: string;
8
7
  href: string;
9
8
  icon?: Component<{ size?: number; class?: string }>;
@@ -12,19 +11,14 @@
12
11
 
13
12
  interface Props {
14
13
  brand?: string;
15
- /** When set, the brand is a link (use `""` to show text only). */
16
14
  brandHref?: string;
17
- /** Accessible name for the landmark when you have more than one `nav`. */
15
+ /** `aria-label` on `<nav>` when multiple nav landmarks exist. */
18
16
  ariaLabel?: string;
19
17
  showThemeToggle?: boolean;
20
18
  className?: string;
21
- /**
22
- * When true, renders only the built-in link list (or `nav` snippet) inside a `nav` landmark.
23
- * Use for headers that are not the full chrome bar.
24
- */
19
+ /** Slim mode: only the `nav` region (no full chrome bar). */
25
20
  embedded?: boolean;
26
- /** Inline nav links; ignored when the `nav` snippet is provided. */
27
- items?: NavbarNavItem[];
21
+ items?: TopNavbarNavItem[];
28
22
  navVariant?: "header" | "sidebar";
29
23
  currentPath?: string;
30
24
  preventNavigation?: boolean;
@@ -63,7 +57,18 @@
63
57
  }
64
58
  }
65
59
 
66
- const ulClasses = $derived(() => {
60
+ /** App routes: prefix match except `/` and absolute URLs (exact match). */
61
+ function isNavItemActive(href: string): boolean {
62
+ if (href.startsWith("http://") || href.startsWith("https://")) {
63
+ return currentPath === href;
64
+ }
65
+ if (href === "/") {
66
+ return currentPath === "/";
67
+ }
68
+ return currentPath === href || currentPath.startsWith(`${href}/`);
69
+ }
70
+
71
+ const ulClasses = $derived.by(() => {
67
72
  return navVariant === "sidebar"
68
73
  ? "flex flex-col gap-1"
69
74
  : "flex flex-col gap-1 md:flex-row md:items-center md:justify-between";
@@ -74,7 +79,7 @@
74
79
  }
75
80
 
76
81
  function getIconContainerClasses(): string {
77
- return "group/nav-item flex flex-col items-center justify-center overflow-clip relative rounded-[20px] shrink-0 text-nav-menu-item transition-colors duration-200 outline-none hover:bg-nav-menu-hover hover:text-nav-menu-item-hover aria-[current=page]:bg-nav-menu-active aria-[current=page]:text-nav-menu-item-active focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-nav-menu-focus";
82
+ return "focus-ring focus-ring--nav group/nav-item flex flex-col items-center justify-center overflow-clip relative rounded-[20px] shrink-0 text-nav-menu-item transition-colors duration-200 outline-none hover:bg-nav-menu-hover hover:text-nav-menu-item-hover aria-[current=page]:bg-nav-menu-active aria-[current=page]:text-nav-menu-item-active";
78
83
  }
79
84
 
80
85
  function getStateLayerClasses(): string {
@@ -91,9 +96,9 @@
91
96
  </script>
92
97
 
93
98
  {#snippet defaultNavList()}
94
- <ul class="{ulClasses()} list-none m-0 p-0">
95
- {#each items as item}
96
- {@const isActive = currentPath === item.href}
99
+ <ul class="{ulClasses} list-none m-0 p-0">
100
+ {#each items as item (item.href)}
101
+ {@const isActive = isNavItemActive(item.href)}
97
102
  <li class={getNavItemClasses()}>
98
103
  <a
99
104
  href={item.href}
@@ -133,7 +138,7 @@
133
138
  </nav>
134
139
  {:else}
135
140
  <nav
136
- class="bg-base-50 border-b border-border sticky top-0 z-50 {className}"
141
+ class="border-b border-border bg-background sticky top-0 z-50 {className}"
137
142
  aria-label={ariaLabel}
138
143
  {...restProps}
139
144
  >
@@ -143,7 +148,7 @@
143
148
  {#if brandHref}
144
149
  <a
145
150
  href={brandHref}
146
- class="text-xl font-bold text-headline hover:opacity-90 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-600"
151
+ class="focus-ring text-xl font-bold text-headline hover:opacity-90"
147
152
  >
148
153
  {brand}
149
154
  </a>
@@ -0,0 +1,35 @@
1
+ import type { Component, Snippet } from "svelte";
2
+ export interface TopNavbarNavItem {
3
+ label: string;
4
+ href: string;
5
+ icon?: Component<{
6
+ size?: number;
7
+ class?: string;
8
+ }>;
9
+ iconFilled?: Component<{
10
+ size?: number;
11
+ class?: string;
12
+ }>;
13
+ }
14
+ interface Props {
15
+ brand?: string;
16
+ brandHref?: string;
17
+ /** `aria-label` on `<nav>` when multiple nav landmarks exist. */
18
+ ariaLabel?: string;
19
+ showThemeToggle?: boolean;
20
+ className?: string;
21
+ /** Slim mode: only the `nav` region (no full chrome bar). */
22
+ embedded?: boolean;
23
+ items?: TopNavbarNavItem[];
24
+ navVariant?: "header" | "sidebar";
25
+ currentPath?: string;
26
+ preventNavigation?: boolean;
27
+ onclick?: (event: Event) => void;
28
+ }
29
+ type $$ComponentProps = Props & {
30
+ nav?: Snippet;
31
+ actions?: Snippet;
32
+ };
33
+ declare const TopNavbar: Component<$$ComponentProps, {}, "">;
34
+ type TopNavbar = ReturnType<typeof TopNavbar>;
35
+ export default TopNavbar;
@@ -1,3 +1,4 @@
1
- export { default as Navbar } from './Navbar.svelte';
1
+ export { default as TopNavbar } from './TopNavbar.svelte';
2
2
  export { default as SidebarNavigation } from './SidebarNavigation.svelte';
3
+ export { default as SidebarAccountPanel } from './SidebarAccountPanel.svelte';
3
4
  export { default as SidebarPanel } from './SidebarPanel.svelte';
@@ -1,3 +1,4 @@
1
- export { default as Navbar } from './Navbar.svelte';
1
+ export { default as TopNavbar } from './TopNavbar.svelte';
2
2
  export { default as SidebarNavigation } from './SidebarNavigation.svelte';
3
+ export { default as SidebarAccountPanel } from './SidebarAccountPanel.svelte';
3
4
  export { default as SidebarPanel } from './SidebarPanel.svelte';
@@ -1,23 +1,12 @@
1
- /**
2
- * Focus management utilities for accessibility
3
- */
4
- /**
5
- * Get all focusable elements within a container
6
- */
1
+ /** Focus helpers for overlays (modals, sheets). Requires `app.css` focus ring classes. */
2
+ /** From `app.css` `@layer components` — import the library/app stylesheet. */
3
+ export declare const FOCUS_BRAND_CLASS = "focus-brand";
4
+ export declare const FOCUS_NAV_CLASS = "focus-nav";
7
5
  export declare function getFocusableElements(container: HTMLElement): HTMLElement[];
8
- /**
9
- * Trap focus within a container element
10
- */
6
+ /** Tab wraps first↔last inside `container`. Caller should pair with `saveFocus` / `returnFocus`. */
11
7
  export declare function trapFocus(container: HTMLElement): () => void;
12
- /**
13
- * Save the currently focused element
14
- */
15
8
  export declare function saveFocus(): void;
16
- /**
17
- * Return focus to the previously focused element
18
- */
9
+ /** Restores focus from the last `saveFocus`. Skips disconnected or unfocusable nodes. */
19
10
  export declare function returnFocus(): void;
20
- /**
21
- * Focus the first focusable element in a container
22
- */
23
11
  export declare function focusFirstElement(container: HTMLElement): void;
12
+ //# sourceMappingURL=focus-utils.d.ts.map
@@ -1,10 +1,9 @@
1
- /**
2
- * Focus management utilities for accessibility
3
- */
1
+ /** Focus helpers for overlays (modals, sheets). Requires `app.css` focus ring classes. */
2
+
3
+ /** From `app.css` `@layer components` — import the library/app stylesheet. */
4
+ export const FOCUS_BRAND_CLASS = "focus-brand";
5
+ export const FOCUS_NAV_CLASS = "focus-nav";
4
6
 
5
- /**
6
- * Get all focusable elements within a container
7
- */
8
7
  export function getFocusableElements(container: HTMLElement): HTMLElement[] {
9
8
  const selector = [
10
9
  'button:not([disabled])',
@@ -17,27 +16,23 @@ export function getFocusableElements(container: HTMLElement): HTMLElement[] {
17
16
 
18
17
  return Array.from(container.querySelectorAll<HTMLElement>(selector)).filter(
19
18
  (el) => {
20
- // Filter out elements that are not visible
21
19
  const style = window.getComputedStyle(el);
22
20
  return style.display !== 'none' && style.visibility !== 'hidden';
23
21
  }
24
22
  );
25
23
  }
26
24
 
27
- /**
28
- * Trap focus within a container element
29
- */
25
+ /** Tab wraps first↔last inside `container`. Caller should pair with `saveFocus` / `returnFocus`. */
30
26
  export function trapFocus(container: HTMLElement): () => void {
31
27
  const focusableElements = getFocusableElements(container);
32
28
 
33
29
  if (focusableElements.length === 0) {
34
- return () => {}; // No-op cleanup
30
+ return () => {};
35
31
  }
36
32
 
37
33
  const firstElement = focusableElements[0];
38
34
  const lastElement = focusableElements[focusableElements.length - 1];
39
35
 
40
- // Focus first element
41
36
  firstElement.focus();
42
37
 
43
38
  function handleKeydown(event: KeyboardEvent) {
@@ -54,13 +49,11 @@ export function trapFocus(container: HTMLElement): () => void {
54
49
  }
55
50
 
56
51
  if (event.shiftKey) {
57
- // Shift + Tab
58
52
  if (currentFocus === firstElement) {
59
53
  event.preventDefault();
60
54
  lastElement.focus();
61
55
  }
62
56
  } else {
63
- // Tab
64
57
  if (currentFocus === lastElement) {
65
58
  event.preventDefault();
66
59
  firstElement.focus();
@@ -70,43 +63,48 @@ export function trapFocus(container: HTMLElement): () => void {
70
63
 
71
64
  container.addEventListener('keydown', handleKeydown);
72
65
 
73
- // Return cleanup function
74
66
  return () => {
75
67
  container.removeEventListener('keydown', handleKeydown);
76
68
  };
77
69
  }
78
70
 
79
- /**
80
- * Store the element that had focus before opening a modal/dialog
81
- */
82
- let previousFocus: HTMLElement | null = null;
71
+ /** One entry per overlay open; `returnFocus` pops LIFO (nested modals). */
72
+ const focusReturnStack: Array<HTMLElement | null> = [];
83
73
 
84
- /**
85
- * Save the currently focused element
86
- */
87
74
  export function saveFocus(): void {
88
- if (typeof document !== 'undefined') {
89
- previousFocus = document.activeElement as HTMLElement;
75
+ if (typeof document === 'undefined') {
76
+ return;
77
+ }
78
+ const active = document.activeElement;
79
+ if (active instanceof HTMLElement && active !== document.body) {
80
+ focusReturnStack.push(active);
81
+ } else {
82
+ focusReturnStack.push(null);
90
83
  }
91
84
  }
92
85
 
93
- /**
94
- * Return focus to the previously focused element
95
- */
86
+ /** Restores focus from the last `saveFocus`. Skips disconnected or unfocusable nodes. */
96
87
  export function returnFocus(): void {
97
- if (previousFocus && typeof document !== 'undefined') {
98
- previousFocus.focus();
99
- previousFocus = null;
88
+ if (typeof document === 'undefined') {
89
+ return;
90
+ }
91
+ const el = focusReturnStack.pop();
92
+ if (!el) {
93
+ return;
94
+ }
95
+ if (typeof el.focus !== 'function' || !el.isConnected) {
96
+ return;
97
+ }
98
+ try {
99
+ el.focus({ preventScroll: true });
100
+ } catch {
101
+ /* embeds / shadow roots may reject programmatic focus */
100
102
  }
101
103
  }
102
104
 
103
- /**
104
- * Focus the first focusable element in a container
105
- */
106
105
  export function focusFirstElement(container: HTMLElement): void {
107
106
  const focusableElements = getFocusableElements(container);
108
107
  if (focusableElements.length > 0) {
109
108
  focusableElements[0].focus();
110
109
  }
111
110
  }
112
-
@@ -1,7 +1,9 @@
1
- // SSR-safe utilities for zabi-components
2
- // This ensures components work properly during server-side rendering
1
+ /**
2
+ * Browser-only guards and id generation for components that run under SSR.
3
+ * `generateId()` is non-deterministic on the client (random) vs counter on the server —
4
+ * use stable ids for ARIA pairs (e.g. `menuId` on NavigationMenu), not for persisted keys.
5
+ */
3
6
 
4
- // Check if we're in a browser environment
5
7
  export function isBrowser(): boolean {
6
8
  return typeof window !== 'undefined';
7
9
  }
@@ -46,7 +48,6 @@ export function safeClearInterval(id: NodeJS.Timeout | undefined): void {
46
48
  }
47
49
  }
48
50
 
49
- // Generate SSR-safe unique IDs
50
51
  let idCounter = 0;
51
52
  export function generateId(prefix: string = 'id'): string {
52
53
  if (isBrowser()) {