zabi-components 5.0.22 → 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 (207) hide show
  1. package/README.md +139 -1478
  2. package/THEME.md +473 -0
  3. package/THEMING.md +248 -0
  4. package/dist/atoms/ActionPanel.svelte +116 -0
  5. package/dist/atoms/ActionPanel.svelte.d.ts +19 -0
  6. package/dist/atoms/Badge.svelte +1 -1
  7. package/dist/atoms/Badge.svelte.d.ts +1 -1
  8. package/dist/atoms/Button.svelte +35 -16
  9. package/dist/atoms/Button.svelte.d.ts +3 -1
  10. package/dist/atoms/Card.svelte +49 -67
  11. package/dist/atoms/Card.svelte.d.ts +4 -4
  12. package/dist/atoms/CardHeader.svelte +15 -2
  13. package/dist/atoms/CardHeader.svelte.d.ts +5 -0
  14. package/dist/atoms/Checkbox.svelte +50 -76
  15. package/dist/atoms/Checkbox.svelte.d.ts +7 -2
  16. package/dist/atoms/CodeBlock.svelte +20 -34
  17. package/dist/atoms/CodeBlock.svelte.d.ts +1 -0
  18. package/dist/atoms/ColorPicker.svelte +3 -7
  19. package/dist/atoms/Container.svelte +41 -0
  20. package/dist/atoms/Container.svelte.d.ts +12 -0
  21. package/dist/atoms/Divider.svelte +68 -0
  22. package/dist/atoms/Divider.svelte.d.ts +13 -0
  23. package/dist/atoms/FeatureCard.svelte +35 -50
  24. package/dist/atoms/FeatureCard.svelte.d.ts +9 -4
  25. package/dist/atoms/IconButton.svelte +33 -24
  26. package/dist/atoms/IconButton.svelte.d.ts +4 -8
  27. package/dist/atoms/Input.svelte +58 -26
  28. package/dist/atoms/Input.svelte.d.ts +6 -1
  29. package/dist/atoms/List.svelte +2 -7
  30. package/dist/atoms/List.svelte.d.ts +1 -6
  31. package/dist/atoms/ListItem.svelte +54 -65
  32. package/dist/atoms/ListItem.svelte.d.ts +7 -13
  33. package/dist/atoms/ListItemLeading.svelte +19 -0
  34. package/dist/atoms/ListItemLeading.svelte.d.ts +9 -0
  35. package/dist/atoms/Progress.svelte +2 -2
  36. package/dist/atoms/Radio.svelte +51 -0
  37. package/dist/atoms/Radio.svelte.d.ts +14 -0
  38. package/dist/atoms/Select.svelte +28 -33
  39. package/dist/atoms/Select.svelte.d.ts +0 -10
  40. package/dist/atoms/SelectionControl.svelte +98 -0
  41. package/dist/atoms/SelectionControl.svelte.d.ts +26 -0
  42. package/dist/atoms/Skeleton.svelte +39 -15
  43. package/dist/atoms/Skeleton.svelte.d.ts +7 -3
  44. package/dist/atoms/Table.svelte +27 -0
  45. package/dist/atoms/Table.svelte.d.ts +10 -0
  46. package/dist/atoms/Text.svelte +46 -0
  47. package/dist/atoms/Text.svelte.d.ts +13 -0
  48. package/dist/atoms/Textarea.svelte +74 -53
  49. package/dist/atoms/Textarea.svelte.d.ts +6 -2
  50. package/dist/atoms/ThemeToggle.svelte +35 -24
  51. package/dist/atoms/ThemeToggle.svelte.d.ts +1 -0
  52. package/dist/atoms/Toast.svelte +55 -31
  53. package/dist/atoms/Toast.svelte.d.ts +4 -1
  54. package/dist/atoms/Toggle.svelte +37 -22
  55. package/dist/atoms/Toggle.svelte.d.ts +2 -1
  56. package/dist/atoms/Tooltip.svelte +90 -11
  57. package/dist/atoms/index.d.ts +6 -0
  58. package/dist/atoms/index.js +6 -0
  59. package/dist/atoms/selection-control.styles.d.ts +25 -0
  60. package/dist/atoms/selection-control.styles.js +30 -0
  61. package/dist/components/atoms/index.d.ts +6 -0
  62. package/dist/components/atoms/index.d.ts.map +1 -1
  63. package/dist/components/atoms/selection-control.styles.d.ts +26 -0
  64. package/dist/components/atoms/selection-control.styles.d.ts.map +1 -0
  65. package/dist/components/index.d.ts +42 -7
  66. package/dist/components/index.d.ts.map +1 -1
  67. package/dist/components/molecules/index.d.ts +10 -0
  68. package/dist/components/molecules/index.d.ts.map +1 -1
  69. package/dist/components/molecules/navigation-menu-context.d.ts +11 -0
  70. package/dist/components/molecules/navigation-menu-context.d.ts.map +1 -1
  71. package/dist/components/molecules/toast-store.d.ts +39 -0
  72. package/dist/components/molecules/toast-store.d.ts.map +1 -0
  73. package/dist/components/organisms/index.d.ts +3 -3
  74. package/dist/components/organisms/index.d.ts.map +1 -1
  75. package/dist/components/types/page.types.d.ts +69 -0
  76. package/dist/components/types/page.types.d.ts.map +1 -0
  77. package/dist/components/types/page.types.js +1 -0
  78. package/dist/components/types/variants.d.ts +64 -0
  79. package/dist/components/types/variants.d.ts.map +1 -0
  80. package/dist/components/types/variants.js +44 -0
  81. package/dist/components/util/fixed-sidebar-flyout.d.ts +23 -0
  82. package/dist/components/util/fixed-sidebar-flyout.d.ts.map +1 -0
  83. package/dist/components/util/focus-utils.d.ts +12 -0
  84. package/dist/components/util/focus-utils.d.ts.map +1 -0
  85. package/dist/components/util/ssr-safe.d.ts +17 -0
  86. package/dist/components/util/ssr-safe.d.ts.map +1 -0
  87. package/dist/index.d.ts +42 -7
  88. package/dist/index.js +45 -10
  89. package/dist/lib/layout-width-tokens.d.ts +17 -0
  90. package/dist/lib/layout-width-tokens.d.ts.map +1 -0
  91. package/dist/lib/showcase/component-docs/Button.d.ts +3 -0
  92. package/dist/lib/showcase/component-docs/Button.d.ts.map +1 -0
  93. package/dist/lib/showcase/component-docs/Input.d.ts +3 -0
  94. package/dist/lib/showcase/component-docs/Input.d.ts.map +1 -0
  95. package/dist/lib/showcase/component-docs/List.d.ts +3 -0
  96. package/dist/lib/showcase/component-docs/List.d.ts.map +1 -0
  97. package/dist/lib/showcase/component-docs/Modal.d.ts +3 -0
  98. package/dist/lib/showcase/component-docs/Modal.d.ts.map +1 -0
  99. package/dist/lib/showcase/component-docs/Radio.d.ts +3 -0
  100. package/dist/lib/showcase/component-docs/Radio.d.ts.map +1 -0
  101. package/dist/lib/showcase/component-docs/SidebarNavigation.d.ts +3 -0
  102. package/dist/lib/showcase/component-docs/SidebarNavigation.d.ts.map +1 -0
  103. package/dist/lib/showcase/component-docs/Skeleton.d.ts +3 -0
  104. package/dist/lib/showcase/component-docs/Skeleton.d.ts.map +1 -0
  105. package/dist/lib/showcase/component-docs/_shared.d.ts +12 -0
  106. package/dist/lib/showcase/component-docs/_shared.d.ts.map +1 -0
  107. package/dist/lib/showcase/component-docs/index.d.ts +4 -0
  108. package/dist/lib/showcase/component-docs/index.d.ts.map +1 -0
  109. package/dist/lib/showcase/components-catalog.d.ts +3 -0
  110. package/dist/lib/showcase/components-catalog.d.ts.map +1 -0
  111. package/dist/lib/showcase/components-showcase-constants.d.ts +89 -0
  112. package/dist/lib/showcase/components-showcase-constants.d.ts.map +1 -0
  113. package/dist/lib/showcase/docs-sidebar-helpers.d.ts +15 -0
  114. package/dist/lib/showcase/docs-sidebar-helpers.d.ts.map +1 -0
  115. package/dist/molecules/Alert.svelte +50 -23
  116. package/dist/molecules/Alert.svelte.d.ts +1 -1
  117. package/dist/molecules/ComponentDemo.svelte +54 -51
  118. package/dist/molecules/ContactForm.svelte +70 -65
  119. package/dist/molecules/Dropdown.svelte +99 -108
  120. package/dist/molecules/Dropdown.svelte.d.ts +12 -6
  121. package/dist/molecules/EmptyState.svelte +44 -0
  122. package/dist/molecules/EmptyState.svelte.d.ts +11 -0
  123. package/dist/molecules/FormField.svelte +89 -0
  124. package/dist/molecules/FormField.svelte.d.ts +23 -0
  125. package/dist/molecules/Header.svelte +41 -0
  126. package/dist/molecules/Header.svelte.d.ts +10 -0
  127. package/dist/molecules/ImageUpload.svelte +1 -1
  128. package/dist/molecules/Modal.svelte +39 -26
  129. package/dist/molecules/Modal.svelte.d.ts +4 -2
  130. package/dist/molecules/NavigationMenu.svelte +16 -0
  131. package/dist/molecules/NavigationMenu.svelte.d.ts +2 -0
  132. package/dist/molecules/NavigationMenuContent.svelte +9 -1
  133. package/dist/molecules/NavigationMenuLink.svelte +1 -1
  134. package/dist/molecules/NavigationMenuTrigger.svelte +9 -4
  135. package/dist/molecules/Page.svelte +16 -0
  136. package/dist/molecules/Page.svelte.d.ts +8 -0
  137. package/dist/molecules/PropsTable.svelte +61 -0
  138. package/dist/molecules/PropsTable.svelte.d.ts +8 -0
  139. package/dist/molecules/RadioGroup.svelte +197 -0
  140. package/dist/molecules/RadioGroup.svelte.d.ts +20 -0
  141. package/dist/molecules/Section.svelte +14 -23
  142. package/dist/molecules/Section.svelte.d.ts +2 -11
  143. package/dist/molecules/SidebarBrandHeader.svelte +68 -0
  144. package/dist/molecules/SidebarBrandHeader.svelte.d.ts +10 -0
  145. package/dist/molecules/SidebarFooter.svelte +119 -0
  146. package/dist/molecules/SidebarFooter.svelte.d.ts +23 -0
  147. package/dist/molecules/SidebarNavSection.svelte +49 -0
  148. package/dist/molecules/SidebarNavSection.svelte.d.ts +14 -0
  149. package/dist/molecules/SlideUp.svelte +21 -16
  150. package/dist/molecules/SlideUp.svelte.d.ts +1 -1
  151. package/dist/molecules/Tabs.svelte +6 -11
  152. package/dist/molecules/Toaster.svelte +21 -0
  153. package/dist/molecules/Toaster.svelte.d.ts +6 -0
  154. package/dist/molecules/ToasterToast.svelte +217 -0
  155. package/dist/molecules/ToasterToast.svelte.d.ts +7 -0
  156. package/dist/molecules/index.d.ts +10 -0
  157. package/dist/molecules/index.js +10 -0
  158. package/dist/molecules/navigation-menu-context.d.ts +11 -0
  159. package/dist/molecules/navigation-menu-context.js +18 -0
  160. package/dist/molecules/toast-store.d.ts +38 -0
  161. package/dist/molecules/toast-store.js +34 -0
  162. package/dist/organisms/SidebarAccountPanel.svelte +118 -0
  163. package/dist/organisms/SidebarAccountPanel.svelte.d.ts +20 -0
  164. package/dist/organisms/SidebarNavigation.svelte +286 -212
  165. package/dist/organisms/SidebarNavigation.svelte.d.ts +22 -1
  166. package/dist/organisms/{SidebarProjectPanel.svelte → SidebarPanel.svelte} +67 -34
  167. package/dist/organisms/{SidebarProjectPanel.svelte.d.ts → SidebarPanel.svelte.d.ts} +9 -6
  168. package/dist/organisms/TopNavbar.svelte +214 -0
  169. package/dist/organisms/TopNavbar.svelte.d.ts +35 -0
  170. package/dist/organisms/index.d.ts +3 -3
  171. package/dist/organisms/index.js +3 -3
  172. package/dist/routes/lib/focus-utils.d.ts +7 -18
  173. package/dist/routes/lib/focus-utils.ts +32 -34
  174. package/dist/routes/lib/ssr-safe.ts +5 -4
  175. package/dist/routes/lib/variant-utils.ts +48 -39
  176. package/dist/types/components.d.ts +187 -0
  177. package/dist/types/components.d.ts.map +1 -0
  178. package/dist/types/events.d.ts +200 -0
  179. package/dist/types/events.d.ts.map +1 -0
  180. package/dist/types/index.d.ts +171 -0
  181. package/dist/types/index.d.ts.map +1 -0
  182. package/dist/types/page.types.d.ts +3 -0
  183. package/dist/types/page.types.d.ts.map +1 -0
  184. package/dist/types/page.types.js +1 -0
  185. package/dist/types/page.types.ts +79 -0
  186. package/dist/types/variants.d.ts +3 -0
  187. package/dist/types/variants.d.ts.map +1 -0
  188. package/dist/types/variants.js +1 -0
  189. package/dist/types/variants.ts +75 -0
  190. package/dist/util/fixed-sidebar-flyout.d.ts +22 -0
  191. package/dist/util/fixed-sidebar-flyout.js +148 -0
  192. package/dist/util/focus-utils.d.ts +11 -0
  193. package/dist/util/focus-utils.js +94 -0
  194. package/dist/util/ssr-safe.d.ts +16 -0
  195. package/dist/util/ssr-safe.js +48 -0
  196. package/dist/zabi-components-colors.css +360 -324
  197. package/dist/zabi-components-theme-dark-only.css +164 -151
  198. package/dist/zabi-components-theme-dark.css +164 -151
  199. package/dist/zabi-components-theme-only.css +253 -230
  200. package/dist/zabi-components-theme.css +253 -230
  201. package/dist/zabi-components.css +1262 -241
  202. package/docs/theme-imports.md +112 -0
  203. package/package.json +32 -10
  204. package/dist/organisms/Navbar.svelte +0 -92
  205. package/dist/organisms/Navbar.svelte.d.ts +0 -14
  206. package/dist/organisms/Navigation.svelte +0 -119
  207. package/dist/organisms/Navigation.svelte.d.ts +0 -21
@@ -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,7 +53,13 @@ interface Props {
38
53
  onLogout?: () => void;
39
54
  onThemeToggle?: (nextIsLightMode: boolean) => void;
40
55
  onEmptyStateAction?: () => void;
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). */
61
+ activePrimaryHref?: string;
41
62
  }
42
- declare const SidebarNavigation: Component<Props, {}, "searchValue" | "isLightMode">;
63
+ declare const SidebarNavigation: Component<Props, {}, "isLightMode" | "searchValue">;
43
64
  type SidebarNavigation = ReturnType<typeof SidebarNavigation>;
44
65
  export default SidebarNavigation;
@@ -4,7 +4,7 @@
4
4
  import { Search, X } from "@lucide/svelte";
5
5
  import type { Component } from "svelte";
6
6
 
7
- export interface SidebarProjectPanelItem {
7
+ export interface SidebarPanelItem {
8
8
  id: string;
9
9
  label: string;
10
10
  icon?: Component<{ size?: number; class?: string }>;
@@ -15,39 +15,45 @@
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;
21
22
  showSearch?: boolean;
22
23
  searchPlaceholder?: string;
23
24
  searchValue?: string;
24
- items?: SidebarProjectPanelItem[];
25
+ items?: SidebarPanelItem[];
25
26
  selectedItemId?: string;
26
27
  emptyStateTitle?: string;
27
28
  emptyStateDescription?: string;
29
+ emptyStateActionLabel?: string;
28
30
  selectLabel?: string;
29
31
  closeLabel?: string;
30
- onSelect?: (item: SidebarProjectPanelItem) => void;
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",
37
- ariaLabel = "Project picker panel",
38
- title = "Projects",
39
- subtitle = "Choose a project to continue",
40
+ variant = "elevated",
41
+ ariaLabel = "Picker panel",
42
+ title = "Items",
43
+ subtitle = "Choose an item to continue",
40
44
  showSearch = true,
41
- searchPlaceholder = "Search projects...",
45
+ searchPlaceholder = "Search...",
42
46
  searchValue = $bindable(""),
43
47
  items = [],
44
48
  selectedItemId = $bindable(""),
45
- emptyStateTitle = "No projects found",
46
- emptyStateDescription = "Try a different keyword or create a new project.",
47
- selectLabel = "Select project",
49
+ emptyStateTitle = "No matches",
50
+ emptyStateDescription = "Try a different keyword.",
51
+ emptyStateActionLabel = "",
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,22 +66,27 @@
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 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
- function handleSelect(item: SidebarProjectPanelItem): void {
89
+ function handleSelect(item: SidebarPanelItem): void {
79
90
  selectedItemId = item.id;
80
91
  if (onSelect) {
81
92
  onSelect(item);
@@ -87,65 +98,76 @@
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 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" aria-label={selectLabel}>
146
+ <ul class="space-y-1" role="listbox" aria-label={selectLabel}>
128
147
  {#each filteredItems as item (item.id)}
129
- <li>
148
+ <li role="presentation">
130
149
  <button
131
150
  type="button"
151
+ role="option"
132
152
  class={getItemClasses(item.id)}
133
153
  onclick={() => handleSelect(item)}
134
- aria-pressed={selectedItemId === item.id}
154
+ aria-selected={selectedItemId === item.id}
135
155
  >
136
156
  <span class="flex items-center justify-between gap-3">
137
- <span class="flex min-w-0 items-start gap-2">
157
+ <span class="flex min-w-0 items-start gap-2.5">
138
158
  {#if item.icon}
139
159
  {@const Icon = item.icon}
140
160
  <span
141
- 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"
142
162
  aria-hidden="true"
143
163
  >
144
- <Icon size={16} />
164
+ <Icon size={18} />
145
165
  </span>
146
166
  {/if}
147
167
  <span class="min-w-0">
148
- <span class="block truncate text-sm font-medium text-current">
168
+ <span
169
+ class="block truncate text-sm font-medium text-current"
170
+ >
149
171
  {item.label}
150
172
  </span>
151
173
  {#if item.description}
@@ -166,9 +188,20 @@
166
188
  {/each}
167
189
  </ul>
168
190
  {:else}
169
- <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
+ >
170
194
  <h4 class="text-sm font-semibold text-headline">{emptyStateTitle}</h4>
171
- <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}
172
205
  </div>
173
206
  {/if}
174
207
  </aside>
@@ -1,5 +1,5 @@
1
1
  import type { Component } from "svelte";
2
- export interface SidebarProjectPanelItem {
2
+ export interface SidebarPanelItem {
3
3
  id: string;
4
4
  label: string;
5
5
  icon?: Component<{
@@ -12,21 +12,24 @@ export interface SidebarProjectPanelItem {
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;
18
19
  showSearch?: boolean;
19
20
  searchPlaceholder?: string;
20
21
  searchValue?: string;
21
- items?: SidebarProjectPanelItem[];
22
+ items?: SidebarPanelItem[];
22
23
  selectedItemId?: string;
23
24
  emptyStateTitle?: string;
24
25
  emptyStateDescription?: string;
26
+ emptyStateActionLabel?: string;
25
27
  selectLabel?: string;
26
28
  closeLabel?: string;
27
- onSelect?: (item: SidebarProjectPanelItem) => void;
29
+ onSelect?: (item: SidebarPanelItem) => void;
28
30
  onClose?: () => void;
31
+ onEmptyStateAction?: () => void;
29
32
  }
30
- declare const SidebarProjectPanel: Component<Props, {}, "searchValue" | "selectedItemId">;
31
- type SidebarProjectPanel = ReturnType<typeof SidebarProjectPanel>;
32
- export default SidebarProjectPanel;
33
+ declare const SidebarPanel: Component<Props, {}, "searchValue" | "selectedItemId">;
34
+ type SidebarPanel = ReturnType<typeof SidebarPanel>;
35
+ export default SidebarPanel;
@@ -0,0 +1,214 @@
1
+ <script lang="ts">
2
+ import ThemeToggle from "../atoms/ThemeToggle.svelte";
3
+ import type { Component, Snippet } from "svelte";
4
+
5
+ export interface TopNavbarNavItem {
6
+ label: string;
7
+ href: string;
8
+ icon?: Component<{ size?: number; class?: string }>;
9
+ iconFilled?: Component<{ size?: number; class?: string }>;
10
+ }
11
+
12
+ interface Props {
13
+ brand?: string;
14
+ brandHref?: string;
15
+ /** `aria-label` on `<nav>` when multiple nav landmarks exist. */
16
+ ariaLabel?: string;
17
+ showThemeToggle?: boolean;
18
+ className?: string;
19
+ /** Slim mode: only the `nav` region (no full chrome bar). */
20
+ embedded?: boolean;
21
+ items?: TopNavbarNavItem[];
22
+ navVariant?: "header" | "sidebar";
23
+ currentPath?: string;
24
+ preventNavigation?: boolean;
25
+ onclick?: (event: Event) => void;
26
+ }
27
+
28
+ let {
29
+ brand = "",
30
+ brandHref,
31
+ ariaLabel,
32
+ showThemeToggle = true,
33
+ className = "",
34
+ embedded = false,
35
+ items = [],
36
+ navVariant = "header",
37
+ currentPath = "",
38
+ preventNavigation = false,
39
+ onclick,
40
+ nav,
41
+ actions,
42
+ ...restProps
43
+ }: Props & { nav?: Snippet; actions?: Snippet } = $props();
44
+
45
+ let isMenuOpen = $state(false);
46
+
47
+ function toggleMenu() {
48
+ isMenuOpen = !isMenuOpen;
49
+ }
50
+
51
+ function handleNavLinkClick(event: MouseEvent) {
52
+ if (preventNavigation) {
53
+ event.preventDefault();
54
+ }
55
+ if (onclick) {
56
+ onclick(event);
57
+ }
58
+ }
59
+
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(() => {
72
+ return navVariant === "sidebar"
73
+ ? "flex flex-col gap-1"
74
+ : "flex flex-col gap-1 md:flex-row md:items-center md:justify-between";
75
+ });
76
+
77
+ function getNavItemClasses(): string {
78
+ return "flex flex-col gap-1 grow h-full items-center justify-center min-h-0 min-w-0 relative shrink-0 cursor-pointer w-full md:w-auto";
79
+ }
80
+
81
+ function getIconContainerClasses(): string {
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";
83
+ }
84
+
85
+ function getStateLayerClasses(): string {
86
+ return "box-border flex gap-1 h-10 items-center px-4 py-2 relative shrink-0";
87
+ }
88
+
89
+ function getLabelClasses(): string {
90
+ return "font-medium leading-4 relative shrink-0 text-center text-nowrap tracking-[0.5px] whitespace-pre text-xs text-inherit";
91
+ }
92
+
93
+ function getIconWrapperClasses(): string {
94
+ return "overflow-clip relative shrink-0 size-4 text-current";
95
+ }
96
+ </script>
97
+
98
+ {#snippet defaultNavList()}
99
+ <ul class="{ulClasses} list-none m-0 p-0">
100
+ {#each items as item (item.href)}
101
+ {@const isActive = isNavItemActive(item.href)}
102
+ <li class={getNavItemClasses()}>
103
+ <a
104
+ href={item.href}
105
+ class={getIconContainerClasses()}
106
+ onclick={handleNavLinkClick}
107
+ aria-current={isActive ? "page" : undefined}
108
+ >
109
+ <div class={getStateLayerClasses()}>
110
+ {#if item.iconFilled && isActive}
111
+ {@const Icon = item.iconFilled}
112
+ <div class={getIconWrapperClasses()}>
113
+ <Icon size={16} class="w-4 h-4" />
114
+ </div>
115
+ {:else if item.icon}
116
+ {@const Icon = item.icon}
117
+ <div class={getIconWrapperClasses()}>
118
+ <Icon size={16} class="w-4 h-4" />
119
+ </div>
120
+ {/if}
121
+ <p class={getLabelClasses()}>
122
+ {item.label}
123
+ </p>
124
+ </div>
125
+ </a>
126
+ </li>
127
+ {/each}
128
+ </ul>
129
+ {/snippet}
130
+
131
+ {#if embedded}
132
+ <nav class={className} aria-label={ariaLabel} {...restProps}>
133
+ {#if nav}
134
+ {@render nav()}
135
+ {:else}
136
+ {@render defaultNavList()}
137
+ {/if}
138
+ </nav>
139
+ {:else}
140
+ <nav
141
+ class="border-b border-border bg-background sticky top-0 z-50 {className}"
142
+ aria-label={ariaLabel}
143
+ {...restProps}
144
+ >
145
+ <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
146
+ <div class="flex justify-between items-center h-16">
147
+ <div class="shrink-0">
148
+ {#if brandHref}
149
+ <a
150
+ href={brandHref}
151
+ class="focus-ring text-xl font-bold text-headline hover:opacity-90"
152
+ >
153
+ {brand}
154
+ </a>
155
+ {:else}
156
+ <span class="text-xl font-bold text-headline">{brand}</span>
157
+ {/if}
158
+ </div>
159
+
160
+ <div class="hidden md:block">
161
+ <div class="ml-10 flex items-baseline space-x-4">
162
+ {#if nav}
163
+ {@render nav()}
164
+ {:else if items.length > 0}
165
+ {@render defaultNavList()}
166
+ {/if}
167
+ </div>
168
+ </div>
169
+
170
+ <div class="hidden md:block">
171
+ <div class="ml-4 flex items-center space-x-4">
172
+ {@render actions?.()}
173
+ {#if showThemeToggle}
174
+ <ThemeToggle />
175
+ {/if}
176
+ </div>
177
+ </div>
178
+
179
+ <div class="md:hidden">
180
+ <button
181
+ type="button"
182
+ class="cursor-pointer text-description hover:text-body"
183
+ onclick={toggleMenu}
184
+ aria-label="Toggle menu"
185
+ >
186
+ <span class="text-2xl">☰</span>
187
+ </button>
188
+ </div>
189
+ </div>
190
+
191
+ {#if isMenuOpen}
192
+ <div class="md:hidden">
193
+ <div
194
+ class="px-2 pt-2 pb-3 space-y-1 sm:px-3 border-t border-border"
195
+ >
196
+ {#if nav}
197
+ {@render nav()}
198
+ {:else if items.length > 0}
199
+ {@render defaultNavList()}
200
+ {/if}
201
+ <div class="pt-4">
202
+ {@render actions?.()}
203
+ {#if showThemeToggle}
204
+ <div class="mt-2">
205
+ <ThemeToggle />
206
+ </div>
207
+ {/if}
208
+ </div>
209
+ </div>
210
+ </div>
211
+ {/if}
212
+ </div>
213
+ </nav>
214
+ {/if}
@@ -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,4 +1,4 @@
1
- export { default as Navbar } from './Navbar.svelte';
2
- export { default as Navigation } from './Navigation.svelte';
1
+ export { default as TopNavbar } from './TopNavbar.svelte';
3
2
  export { default as SidebarNavigation } from './SidebarNavigation.svelte';
4
- export { default as SidebarProjectPanel } from './SidebarProjectPanel.svelte';
3
+ export { default as SidebarAccountPanel } from './SidebarAccountPanel.svelte';
4
+ export { default as SidebarPanel } from './SidebarPanel.svelte';
@@ -1,4 +1,4 @@
1
- export { default as Navbar } from './Navbar.svelte';
2
- export { default as Navigation } from './Navigation.svelte';
1
+ export { default as TopNavbar } from './TopNavbar.svelte';
3
2
  export { default as SidebarNavigation } from './SidebarNavigation.svelte';
4
- export { default as SidebarProjectPanel } from './SidebarProjectPanel.svelte';
3
+ export { default as SidebarAccountPanel } from './SidebarAccountPanel.svelte';
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