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,6 +1,8 @@
1
1
  import type { Snippet } from "svelte";
2
2
  interface Props {
3
3
  value?: string;
4
+ class?: string;
5
+ /** @deprecated use `class`. */
4
6
  className?: string;
5
7
  children?: Snippet;
6
8
  }
@@ -1,15 +1,21 @@
1
1
  <script lang="ts">
2
2
  import type { Snippet } from "svelte";
3
+ import { cn } from "../util/cn.js";
3
4
 
4
5
  interface Props {
5
6
  children?: Snippet;
7
+ class?: string;
8
+ /** @deprecated use `class`. */
6
9
  className?: string;
7
10
  }
8
11
 
9
- let { children, className = "" }: Props = $props();
12
+ let { children, class: classAttr = "", className: legacyClass = "" }: Props = $props();
13
+
14
+ /** `class` is the public prop; `className` is a deprecated alias. */
15
+ const className = $derived(cn(`${classAttr} ${legacyClass}`));
10
16
  </script>
11
17
 
12
- <div class="mx-auto w-full space-y-10 {className}">
18
+ <div class={cn("mx-auto w-full space-y-10", className)}>
13
19
  {#if children}
14
20
  {@render children()}
15
21
  {/if}
@@ -1,6 +1,8 @@
1
1
  import type { Snippet } from "svelte";
2
2
  interface Props {
3
3
  children?: Snippet;
4
+ class?: string;
5
+ /** @deprecated use `class`. */
4
6
  className?: string;
5
7
  }
6
8
  declare const Page: import("svelte").Component<Props, {}, "">;
@@ -1,17 +1,20 @@
1
1
  <script lang="ts">
2
2
  import type { ComponentProp } from "../types/page.types";
3
3
  import Table from "../atoms/Table.svelte";
4
+ import { cn } from "../util/cn.js";
4
5
 
5
6
  interface Props {
7
+ /** Extra classes for the host element. */
8
+ class?: string;
6
9
  props: ComponentProp[];
7
10
  caption?: string;
8
11
  }
9
12
 
10
- let { props, caption = "Props / API" }: Props = $props();
13
+ let { class: className = "", props, caption = "Props / API" }: Props = $props();
11
14
  </script>
12
15
 
13
16
  {#if props.length === 0}
14
- <div class="rounded-lg border border-border bg-base-50 px-4 py-4 text-sm text-description">
17
+ <div class={cn("rounded-control border border-border bg-base-50 px-4 py-4 text-sm text-description", className)}>
15
18
  No documented props.
16
19
  </div>
17
20
  {:else}
@@ -37,7 +40,7 @@
37
40
  </thead>
38
41
  <tbody>
39
42
  {#each props as prop}
40
- <tr class="hover:bg-base-50/70">
43
+ <tr class="hover:bg-surface-hover">
41
44
  <td class="border-b border-border px-4 py-3 font-mono text-body">
42
45
  {prop.name}
43
46
  </td>
@@ -1,5 +1,7 @@
1
1
  import type { ComponentProp } from "../types/page.types";
2
2
  interface Props {
3
+ /** Extra classes for the host element. */
4
+ class?: string;
3
5
  props: ComponentProp[];
4
6
  caption?: string;
5
7
  }
@@ -7,6 +7,7 @@
7
7
  } from "../atoms/selection-control.styles";
8
8
  import { generateId } from "../util/ssr-safe.js";
9
9
  import { SvelteMap } from "svelte/reactivity";
10
+ import { cn } from "../util/cn.js";
10
11
 
11
12
  export type RadioGroupOption = {
12
13
  value: string;
@@ -16,6 +17,8 @@
16
17
  };
17
18
 
18
19
  interface Props {
20
+ /** Extra classes for the host element. */
21
+ class?: string;
19
22
  options: RadioGroupOption[];
20
23
  /** Selected value; `bind:value`. Use `undefined` for no selection. */
21
24
  value?: string | undefined;
@@ -28,6 +31,7 @@
28
31
  }
29
32
 
30
33
  let {
34
+ class: className = "",
31
35
  options,
32
36
  defaultValue,
33
37
  value = $bindable(defaultValue),
@@ -150,7 +154,7 @@
150
154
  </script>
151
155
 
152
156
  <fieldset
153
- class="space-y-3"
157
+ class={cn("space-y-3", className)}
154
158
  aria-disabled={disabled ? "true" : undefined}
155
159
  onkeydown={handleKeydown}
156
160
  {...restProps}
@@ -5,6 +5,8 @@ export type RadioGroupOption = {
5
5
  description?: string;
6
6
  };
7
7
  interface Props {
8
+ /** Extra classes for the host element. */
9
+ class?: string;
8
10
  options: RadioGroupOption[];
9
11
  /** Selected value; `bind:value`. Use `undefined` for no selection. */
10
12
  value?: string | undefined;
@@ -2,17 +2,20 @@
2
2
  import type { Snippet } from "svelte";
3
3
  import type { SizeVariant } from "../types/variants.js";
4
4
  import Heading from "../atoms/Heading.svelte";
5
+ import { cn } from "../util/cn.js";
5
6
 
6
7
  interface Props {
7
8
  title?: string;
8
9
  description?: string;
9
10
  /** Title heading level (1–6). */
10
- headingLevel?: number;
11
+ headingLevel?: 1 | 2 | 3 | 4 | 5 | 6;
11
12
  size?: SizeVariant;
12
13
  maxWidth?: "sm" | "md" | "lg" | "xl" | "2xl" | "full" | "none";
13
14
  background?: "default" | "muted" | "accent" | "transparent";
14
15
  padding?: "none" | "sm" | "md" | "lg" | "xl";
15
16
  centered?: boolean;
17
+ class?: string;
18
+ /** @deprecated use `class`. */
16
19
  className?: string;
17
20
  children?: Snippet;
18
21
  }
@@ -26,11 +29,16 @@
26
29
  background = "default",
27
30
  padding = "lg",
28
31
  centered = false,
29
- className = "",
32
+ class: classAttr = "",
33
+ className: legacyClass = "",
30
34
  children,
31
35
  ...restProps
32
36
  }: Props = $props();
33
37
 
38
+ /** `class` is the public prop; `className` is a deprecated alias.
39
+ * Both are merged here so existing call sites keep working. */
40
+ const className = $derived(cn(`${classAttr} ${legacyClass}`));
41
+
34
42
  const sizeClasses = $derived.by(() => {
35
43
  switch (size) {
36
44
  case "sm":
@@ -97,13 +105,13 @@
97
105
  const base = "w-full";
98
106
  const maxWidthClass = maxWidthClasses;
99
107
  const centeredClass = centered ? "mx-auto" : "";
100
- return `${base} ${maxWidthClass} ${centeredClass}`.trim();
108
+ return cn(`${base} ${maxWidthClass} ${centeredClass}`);
101
109
  });
102
110
 
103
111
  const contentClasses = $derived.by(() => {
104
112
  const base = "px-4 sm:px-6 lg:px-8";
105
113
  const centeredClass = centered ? "text-center" : "";
106
- return `${base} ${centeredClass}`.trim();
114
+ return cn(`${base} ${centeredClass}`);
107
115
  });
108
116
  </script>
109
117
 
@@ -4,12 +4,14 @@ interface Props {
4
4
  title?: string;
5
5
  description?: string;
6
6
  /** Title heading level (1–6). */
7
- headingLevel?: number;
7
+ headingLevel?: 1 | 2 | 3 | 4 | 5 | 6;
8
8
  size?: SizeVariant;
9
9
  maxWidth?: "sm" | "md" | "lg" | "xl" | "2xl" | "full" | "none";
10
10
  background?: "default" | "muted" | "accent" | "transparent";
11
11
  padding?: "none" | "sm" | "md" | "lg" | "xl";
12
12
  centered?: boolean;
13
+ class?: string;
14
+ /** @deprecated use `class`. */
13
15
  className?: string;
14
16
  children?: Snippet;
15
17
  }
@@ -1,9 +1,13 @@
1
1
  <script lang="ts">
2
+ import { cn } from "../util/cn.js";
3
+
2
4
  interface Props {
3
5
  collapsed?: boolean;
4
6
  brandName?: string;
5
7
  logoSrc?: string;
6
8
  logoAlt?: string;
9
+ class?: string;
10
+ /** @deprecated use `class`. */
7
11
  className?: string;
8
12
  }
9
13
 
@@ -12,9 +16,14 @@
12
16
  brandName = "",
13
17
  logoSrc = "",
14
18
  logoAlt = "",
15
- className = "",
19
+ class: classAttr = "",
20
+ className: legacyClass = "",
16
21
  }: Props = $props();
17
22
 
23
+ /** `class` is the public prop; `className` is a deprecated alias.
24
+ * Both are merged here so existing call sites keep working. */
25
+ const className = $derived(`${classAttr} ${legacyClass}`.trim());
26
+
18
27
  const trimmedName = $derived(brandName.trim());
19
28
  const monogram = $derived(
20
29
  trimmedName
@@ -35,11 +44,11 @@
35
44
 
36
45
  {#if showLogo || trimmedName}
37
46
  <div
38
- class={`flex w-full min-w-0 items-center gap-2.5 ${className}`.trim()}
47
+ class={cn("flex w-full min-w-0 items-center gap-3", className)}
39
48
  >
40
49
  {#if showLogo}
41
50
  <span
42
- class="flex size-9 shrink-0 items-center justify-center overflow-hidden rounded-xl bg-background ring-1 ring-border"
51
+ class="flex size-9 shrink-0 items-center justify-center overflow-hidden rounded-container bg-background ring-1 ring-border"
43
52
  >
44
53
  <img
45
54
  src={logoSrc}
@@ -51,7 +60,7 @@
51
60
  </span>
52
61
  {:else if showMonogram}
53
62
  <span
54
- class="flex size-9 shrink-0 items-center justify-center rounded-xl bg-action-primary-subtle text-xs font-bold text-headline ring-1 ring-border"
63
+ class="flex size-9 shrink-0 items-center justify-center rounded-container bg-action-primary-subtle text-xs font-bold text-headline ring-1 ring-border"
55
64
  aria-hidden="true"
56
65
  >
57
66
  {monogram}
@@ -3,6 +3,8 @@ interface Props {
3
3
  brandName?: string;
4
4
  logoSrc?: string;
5
5
  logoAlt?: string;
6
+ class?: string;
7
+ /** @deprecated use `class`. */
6
8
  className?: string;
7
9
  }
8
10
  declare const SidebarBrandHeader: import("svelte").Component<Props, {}, "">;
@@ -1,6 +1,7 @@
1
1
  <script lang="ts">
2
2
  import type { Snippet } from "svelte";
3
3
  import { fixedSidebarFlyout } from "../util/fixed-sidebar-flyout.js";
4
+ import { cn } from "../util/cn.js";
4
5
 
5
6
  interface Props {
6
7
  collapsed?: boolean;
@@ -15,6 +16,8 @@
15
16
  isLightMode?: boolean;
16
17
  onLogout?: () => void;
17
18
  onThemeToggle?: (nextIsLightMode: boolean) => void;
19
+ class?: string;
20
+ /** @deprecated use `class`. */
18
21
  className?: string;
19
22
  profilePanelOpen?: boolean;
20
23
  profilePanelControlsId?: string;
@@ -35,18 +38,23 @@
35
38
  isLightMode = $bindable(false),
36
39
  onLogout,
37
40
  onThemeToggle,
38
- className = "",
41
+ class: classAttr = "",
42
+ className: legacyClass = "",
39
43
  profilePanelOpen = false,
40
44
  profilePanelControlsId = "",
41
45
  onProfileClick,
42
46
  profilePanel,
43
47
  }: Props = $props();
44
48
 
49
+ /** `class` is the public prop; `className` is a deprecated alias.
50
+ * Both are merged here so existing call sites keep working. */
51
+ const className = $derived(cn(`${classAttr} ${legacyClass}`));
52
+
45
53
  const showFooter = $derived(showProfile || showLogout || showThemeToggle);
46
54
  const showPanelLauncher = $derived(showProfile);
47
55
 
48
56
  const avatarClasses =
49
- "size-10 rounded-xl bg-action-primary text-action-primary flex items-center justify-center text-sm font-semibold shrink-0 ring-1 ring-border-focus";
57
+ "size-10 rounded-container bg-action-primary text-action-primary flex items-center justify-center text-sm font-semibold shrink-0 ring-1 ring-border-focus";
50
58
 
51
59
  function getTextToneClass(isMuted = false): string {
52
60
  return isMuted ? "text-description" : "text-headline";
@@ -60,14 +68,14 @@
60
68
  {#if showFooter}
61
69
  <div class="w-full shrink-0" data-sidebar-flyout-root>
62
70
  <footer
63
- class={`flex w-full shrink-0 flex-col gap-2.5 border-t border-border pt-3 pb-1 ${className}`.trim()}
71
+ class={cn(`flex w-full shrink-0 flex-col gap-3 border-t border-border pt-3 pb-1 ${className}`)}
64
72
  aria-label="Account and settings"
65
73
  >
66
74
  {#if showPanelLauncher}
67
75
  <button
68
76
  type="button"
69
77
  data-sidebar-flyout-anchor="profile"
70
- class="w-full cursor-pointer rounded-xl px-2 py-2 outline-none transition-colors hover:bg-nav-menu-hover hover:text-nav-menu-item-hover hover:ring-1 hover:ring-border/60 active:bg-base-200 focus-ring focus-ring--nav"
78
+ class="w-full cursor-pointer rounded-container px-2 py-2 outline-none transition-colors hover:bg-nav-menu-hover hover:text-nav-menu-item-hover hover:ring-1 hover:ring-border/60 active:bg-surface-active focus-ring focus-ring--nav"
71
79
  aria-haspopup="dialog"
72
80
  aria-expanded={profilePanelOpen}
73
81
  aria-controls={profilePanelControlsId.trim()
@@ -12,6 +12,8 @@ interface Props {
12
12
  isLightMode?: boolean;
13
13
  onLogout?: () => void;
14
14
  onThemeToggle?: (nextIsLightMode: boolean) => void;
15
+ class?: string;
16
+ /** @deprecated use `class`. */
15
17
  className?: string;
16
18
  profilePanelOpen?: boolean;
17
19
  profilePanelControlsId?: string;
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import type { Snippet } from "svelte";
3
+ import { cn } from "../util/cn.js";
3
4
 
4
5
  interface Props {
5
6
  title?: string;
@@ -8,6 +9,8 @@
8
9
  /** `aria-label` on the `<ul>` (required when title is hidden in collapsed mode). */
9
10
  listAriaLabel: string;
10
11
  collapsed?: boolean;
12
+ class?: string;
13
+ /** @deprecated use `class`. */
11
14
  className?: string;
12
15
  children: Snippet;
13
16
  }
@@ -17,10 +20,15 @@
17
20
  sectionKey = "",
18
21
  listAriaLabel,
19
22
  collapsed = false,
20
- className = "",
23
+ class: classAttr = "",
24
+ className: legacyClass = "",
21
25
  children,
22
26
  }: Props = $props();
23
27
 
28
+ /** `class` is the public prop; `className` is a deprecated alias.
29
+ * Both are merged here so existing call sites keep working. */
30
+ const className = $derived(cn(`${classAttr} ${legacyClass}`));
31
+
24
32
  const trimmedTitle = $derived(title.trim());
25
33
  const headingId = $derived(
26
34
  trimmedTitle
@@ -30,17 +38,17 @@
30
38
  const showHeading = $derived(Boolean(trimmedTitle) && !collapsed);
31
39
  </script>
32
40
 
33
- <div class={`flex w-full flex-col gap-1.5 ${className}`.trim()}>
41
+ <div class={cn(`flex w-full flex-col gap-2 ${className}`)}>
34
42
  {#if showHeading && headingId}
35
43
  <h2
36
- class="px-0 text-[10px] font-semibold uppercase tracking-[0.12em] text-description"
44
+ class="px-0 text-xs font-semibold uppercase tracking-wider text-description"
37
45
  id={headingId}
38
46
  >
39
47
  {trimmedTitle}
40
48
  </h2>
41
49
  {/if}
42
50
  <ul
43
- class="flex w-full flex-col gap-0.5 px-0"
51
+ class="flex w-full flex-col gap-1 px-0"
44
52
  aria-labelledby={showHeading && headingId ? headingId : undefined}
45
53
  aria-label={showHeading ? undefined : listAriaLabel}
46
54
  >
@@ -6,6 +6,8 @@ interface Props {
6
6
  /** `aria-label` on the `<ul>` (required when title is hidden in collapsed mode). */
7
7
  listAriaLabel: string;
8
8
  collapsed?: boolean;
9
+ class?: string;
10
+ /** @deprecated use `class`. */
9
11
  className?: string;
10
12
  children: Snippet;
11
13
  }
@@ -1,40 +1,66 @@
1
1
  <script lang="ts">
2
+ import type { Snippet } from 'svelte';
2
3
  import {
3
4
  focusFirstElement,
5
+ getFocusableElements,
4
6
  returnFocus,
5
7
  saveFocus,
6
- trapFocus,
7
8
  } from '../util/focus-utils.js';
8
9
  import { generateId } from "../util/ssr-safe.js";
10
+ import { cn } from "../util/cn.js";
9
11
 
10
12
  interface Props {
13
+ /** Extra classes for the host element. */
14
+ class?: string;
11
15
  isOpen?: boolean;
12
16
  title?: string;
13
17
  onclick?: (event: Event) => void;
14
18
  onkeydown?: (event: Event) => void;
19
+ children?: Snippet;
15
20
  }
16
21
 
17
22
  let {
23
+ class: className = "",
18
24
  isOpen = $bindable(false),
19
25
  title = '',
20
26
  onclick,
21
27
  onkeydown,
22
28
  children,
23
29
  ...restProps
24
- } = $props<Props & { children?: any }>();
30
+ }: Props = $props();
25
31
 
26
32
  const slideTitleId = generateId('slideup-title');
27
33
 
28
34
  let slideUpContainer = $state<HTMLDivElement>();
29
- let cleanupFocusTrap: (() => void) | null = null;
35
+ let focusActive = false;
36
+
37
+ /** Ref-counted on `<body>`; shares the counter with Modal so nested overlays unlock once. */
38
+ function lockBodyScroll(): () => void {
39
+ const body = document.body;
40
+ const count = Number(body.dataset.zabiScrollLock ?? '0');
41
+ if (count === 0) {
42
+ body.dataset.zabiScrollLockOverflow = body.style.overflow;
43
+ body.style.overflow = 'hidden';
44
+ }
45
+ body.dataset.zabiScrollLock = String(count + 1);
46
+ return () => {
47
+ const next = Number(body.dataset.zabiScrollLock ?? '1') - 1;
48
+ if (next <= 0) {
49
+ body.style.overflow = body.dataset.zabiScrollLockOverflow ?? '';
50
+ delete body.dataset.zabiScrollLock;
51
+ delete body.dataset.zabiScrollLockOverflow;
52
+ } else {
53
+ body.dataset.zabiScrollLock = String(next);
54
+ }
55
+ };
56
+ }
30
57
 
31
58
  function closeSlideUp(event?: Event) {
32
59
  isOpen = false;
33
- if (cleanupFocusTrap) {
34
- cleanupFocusTrap();
35
- cleanupFocusTrap = null;
60
+ if (focusActive) {
61
+ focusActive = false;
62
+ returnFocus();
36
63
  }
37
- returnFocus();
38
64
  if (onclick && event) {
39
65
  onclick(event);
40
66
  }
@@ -44,17 +70,51 @@
44
70
  const container = slideUpContainer;
45
71
  if (isOpen && container) {
46
72
  saveFocus();
47
- cleanupFocusTrap = trapFocus(container);
48
- setTimeout(() => {
73
+ focusActive = true;
74
+ const unlockScroll = lockBodyScroll();
75
+ const t = setTimeout(() => {
49
76
  focusFirstElement(container);
50
77
  }, 0);
51
- } else if (!isOpen && cleanupFocusTrap) {
52
- cleanupFocusTrap();
53
- cleanupFocusTrap = null;
54
- returnFocus();
78
+ return () => {
79
+ clearTimeout(t);
80
+ unlockScroll();
81
+ if (focusActive) {
82
+ focusActive = false;
83
+ returnFocus();
84
+ }
85
+ };
55
86
  }
56
87
  });
57
88
 
89
+ /** Re-queries focusables on every Tab so late-rendered content stays inside the trap. */
90
+ function handleTrapKeydown(event: KeyboardEvent) {
91
+ const container = slideUpContainer;
92
+ if (event.key !== 'Tab' || !container) return;
93
+ const owner = (event.target as Element | null)?.closest?.('[role="dialog"]');
94
+ if (owner && owner !== container) return;
95
+
96
+ const focusable = getFocusableElements(container);
97
+ if (focusable.length === 0) {
98
+ event.preventDefault();
99
+ container.focus();
100
+ return;
101
+ }
102
+ const first = focusable[0];
103
+ const last = focusable[focusable.length - 1];
104
+ const active = document.activeElement as HTMLElement | null;
105
+
106
+ if (!active || !focusable.includes(active)) {
107
+ event.preventDefault();
108
+ first.focus();
109
+ } else if (event.shiftKey && active === first) {
110
+ event.preventDefault();
111
+ last.focus();
112
+ } else if (!event.shiftKey && active === last) {
113
+ event.preventDefault();
114
+ first.focus();
115
+ }
116
+ }
117
+
58
118
  function handleBackdropClick(event: Event) {
59
119
  if (event.target === event.currentTarget) {
60
120
  closeSlideUp(event);
@@ -63,7 +123,8 @@
63
123
 
64
124
  function handleKeydown(event: Event) {
65
125
  const keyboardEvent = event as KeyboardEvent;
66
- if (keyboardEvent.key === 'Escape') {
126
+ if (keyboardEvent.key === 'Escape' && !keyboardEvent.defaultPrevented) {
127
+ keyboardEvent.preventDefault();
67
128
  closeSlideUp(event);
68
129
  }
69
130
  onkeydown?.(event);
@@ -71,19 +132,22 @@
71
132
  </script>
72
133
 
73
134
  {#if isOpen}
135
+ <!-- svelte-ignore a11y_click_events_have_key_events -->
74
136
  <div
75
137
  class="fixed inset-0 z-modal cursor-pointer bg-overlay"
76
138
  onclick={handleBackdropClick}
77
139
  onkeydown={handleKeydown}
78
- role="dialog"
79
- aria-modal="true"
80
- aria-labelledby={title ? slideTitleId : undefined}
81
- tabindex="-1"
82
- {...restProps}
140
+ role="presentation"
83
141
  >
84
142
  <div
85
143
  bind:this={slideUpContainer}
86
- class="fixed bottom-0 left-0 right-0 z-modal flex max-h-[90vh] cursor-default flex-col overflow-y-auto rounded-t-3xl bg-card shadow-xl animate-[slideUp_0.3s_ease-out]"
144
+ class={cn("fixed bottom-0 left-0 right-0 z-modal flex max-h-[90vh] cursor-default flex-col overflow-y-auto rounded-t-overlay border-t border-border-overlay bg-surface-overlay shadow-lg animate-[slideUp_0.3s_ease-out]", className)}
145
+ role="dialog"
146
+ aria-modal="true"
147
+ aria-labelledby={title ? slideTitleId : undefined}
148
+ tabindex="-1"
149
+ onkeydown={handleTrapKeydown}
150
+ {...restProps}
87
151
  >
88
152
  {#if title}
89
153
  <div class="flex items-center justify-between px-6 pb-4 pt-6">
@@ -96,7 +160,7 @@
96
160
  <button
97
161
  type="button"
98
162
  onclick={closeSlideUp}
99
- class="flex h-8 w-8 cursor-pointer items-center justify-center rounded-full text-2xl text-description transition-colors hover:bg-base-100 hover:text-headline"
163
+ class="focus-ring flex h-8 w-8 cursor-pointer items-center justify-center rounded-full text-2xl text-description transition-colors hover:bg-surface-overlay-hover hover:text-headline"
100
164
  aria-label="Close"
101
165
  >
102
166
  ×
@@ -1,12 +1,13 @@
1
+ import type { Snippet } from 'svelte';
1
2
  interface Props {
3
+ /** Extra classes for the host element. */
4
+ class?: string;
2
5
  isOpen?: boolean;
3
6
  title?: string;
4
7
  onclick?: (event: Event) => void;
5
8
  onkeydown?: (event: Event) => void;
9
+ children?: Snippet;
6
10
  }
7
- type $$ComponentProps = Props & {
8
- children?: any;
9
- };
10
- declare const SlideUp: import("svelte").Component<$$ComponentProps, {}, "isOpen">;
11
+ declare const SlideUp: import("svelte").Component<Props, {}, "isOpen">;
11
12
  type SlideUp = ReturnType<typeof SlideUp>;
12
13
  export default SlideUp;