zabi-components 7.0.1 → 8.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. package/README.md +46 -3
  2. package/THEMING.md +255 -15
  3. package/dist/atoms/ActionPanel.svelte +12 -7
  4. package/dist/atoms/ActionPanel.svelte.d.ts +2 -0
  5. package/dist/atoms/Badge.svelte +93 -56
  6. package/dist/atoms/Badge.svelte.d.ts +15 -3
  7. package/dist/atoms/Button.svelte +49 -49
  8. package/dist/atoms/Button.svelte.d.ts +3 -0
  9. package/dist/atoms/Card.svelte +31 -28
  10. package/dist/atoms/Card.svelte.d.ts +5 -2
  11. package/dist/atoms/CardContent.svelte +10 -2
  12. package/dist/atoms/CardContent.svelte.d.ts +2 -0
  13. package/dist/atoms/CardFooter.svelte +10 -2
  14. package/dist/atoms/CardFooter.svelte.d.ts +2 -0
  15. package/dist/atoms/CardHeader.svelte +10 -2
  16. package/dist/atoms/CardHeader.svelte.d.ts +2 -0
  17. package/dist/atoms/Checkbox.svelte +4 -0
  18. package/dist/atoms/Checkbox.svelte.d.ts +2 -0
  19. package/dist/atoms/CodeBlock.svelte +15 -7
  20. package/dist/atoms/CodeBlock.svelte.d.ts +9 -3
  21. package/dist/atoms/ColorPicker.svelte +19 -9
  22. package/dist/atoms/ColorPicker.svelte.d.ts +3 -1
  23. package/dist/atoms/Container.svelte +2 -3
  24. package/dist/atoms/Divider.svelte +4 -4
  25. package/dist/atoms/FeatureCard.svelte +4 -3
  26. package/dist/atoms/Heading.svelte +56 -27
  27. package/dist/atoms/Heading.svelte.d.ts +17 -4
  28. package/dist/atoms/IconButton.svelte +30 -37
  29. package/dist/atoms/Input.svelte +26 -34
  30. package/dist/atoms/List.svelte +10 -2
  31. package/dist/atoms/List.svelte.d.ts +2 -0
  32. package/dist/atoms/ListItem.svelte +2 -2
  33. package/dist/atoms/ListItemLeading.svelte +4 -2
  34. package/dist/atoms/ListItemLeading.svelte.d.ts +1 -1
  35. package/dist/atoms/OptimizedImage.svelte +11 -2
  36. package/dist/atoms/OptimizedImage.svelte.d.ts +2 -0
  37. package/dist/atoms/Progress.svelte +4 -1
  38. package/dist/atoms/Progress.svelte.d.ts +2 -0
  39. package/dist/atoms/Radio.svelte +4 -0
  40. package/dist/atoms/Radio.svelte.d.ts +2 -0
  41. package/dist/atoms/Select.svelte +65 -67
  42. package/dist/atoms/Select.svelte.d.ts +4 -0
  43. package/dist/atoms/SelectionControl.svelte +5 -1
  44. package/dist/atoms/SelectionControl.svelte.d.ts +2 -0
  45. package/dist/atoms/Skeleton.svelte +5 -3
  46. package/dist/atoms/Table.svelte +2 -1
  47. package/dist/atoms/Text.svelte +34 -5
  48. package/dist/atoms/Text.svelte.d.ts +7 -1
  49. package/dist/atoms/Textarea.svelte +7 -6
  50. package/dist/atoms/ThemeToggle.svelte +17 -14
  51. package/dist/atoms/ThemeToggle.svelte.d.ts +2 -0
  52. package/dist/atoms/Toast.svelte +10 -8
  53. package/dist/atoms/Toggle.svelte +21 -13
  54. package/dist/atoms/Toggle.svelte.d.ts +7 -0
  55. package/dist/atoms/Tooltip.svelte +89 -57
  56. package/dist/atoms/Tooltip.svelte.d.ts +15 -0
  57. package/dist/components/index.d.ts +3 -1
  58. package/dist/components/index.d.ts.map +1 -1
  59. package/dist/components/organisms/index.d.ts +1 -0
  60. package/dist/components/organisms/index.d.ts.map +1 -1
  61. package/dist/components/util/cn.d.ts +8 -0
  62. package/dist/components/util/cn.d.ts.map +1 -0
  63. package/dist/components/util/ssr-safe.d.ts +4 -4
  64. package/dist/components/util/ssr-safe.d.ts.map +1 -1
  65. package/dist/index.d.ts +3 -1
  66. package/dist/index.js +3 -6
  67. package/dist/molecules/Alert.svelte +43 -64
  68. package/dist/molecules/Alert.svelte.d.ts +7 -1
  69. package/dist/molecules/ComponentDemo.svelte +10 -2
  70. package/dist/molecules/ComponentDemo.svelte.d.ts +2 -0
  71. package/dist/molecules/ContactForm.svelte +12 -2
  72. package/dist/molecules/ContactForm.svelte.d.ts +2 -0
  73. package/dist/molecules/Dropdown.svelte +40 -11
  74. package/dist/molecules/Dropdown.svelte.d.ts +4 -0
  75. package/dist/molecules/EmptyState.svelte +2 -1
  76. package/dist/molecules/Form.svelte +25 -14
  77. package/dist/molecules/Form.svelte.d.ts +13 -5
  78. package/dist/molecules/FormField.svelte +2 -1
  79. package/dist/molecules/Header.svelte +13 -3
  80. package/dist/molecules/Header.svelte.d.ts +2 -0
  81. package/dist/molecules/ImageUpload.svelte +54 -15
  82. package/dist/molecules/ImageUpload.svelte.d.ts +16 -5
  83. package/dist/molecules/Modal.svelte +110 -31
  84. package/dist/molecules/Modal.svelte.d.ts +9 -6
  85. package/dist/molecules/NavigationMenu.svelte +15 -3
  86. package/dist/molecules/NavigationMenu.svelte.d.ts +4 -0
  87. package/dist/molecules/NavigationMenuContent.svelte +36 -38
  88. package/dist/molecules/NavigationMenuContent.svelte.d.ts +2 -0
  89. package/dist/molecules/NavigationMenuItem.svelte +10 -2
  90. package/dist/molecules/NavigationMenuItem.svelte.d.ts +2 -0
  91. package/dist/molecules/NavigationMenuLink.svelte +10 -2
  92. package/dist/molecules/NavigationMenuLink.svelte.d.ts +2 -0
  93. package/dist/molecules/NavigationMenuList.svelte +10 -2
  94. package/dist/molecules/NavigationMenuList.svelte.d.ts +2 -0
  95. package/dist/molecules/NavigationMenuTrigger.svelte +16 -3
  96. package/dist/molecules/NavigationMenuTrigger.svelte.d.ts +2 -0
  97. package/dist/molecules/Page.svelte +8 -2
  98. package/dist/molecules/Page.svelte.d.ts +2 -0
  99. package/dist/molecules/PropsTable.svelte +6 -3
  100. package/dist/molecules/PropsTable.svelte.d.ts +2 -0
  101. package/dist/molecules/RadioGroup.svelte +5 -1
  102. package/dist/molecules/RadioGroup.svelte.d.ts +2 -0
  103. package/dist/molecules/Section.svelte +12 -4
  104. package/dist/molecules/Section.svelte.d.ts +3 -1
  105. package/dist/molecules/SidebarBrandHeader.svelte +13 -4
  106. package/dist/molecules/SidebarBrandHeader.svelte.d.ts +2 -0
  107. package/dist/molecules/SidebarFooter.svelte +12 -4
  108. package/dist/molecules/SidebarFooter.svelte.d.ts +2 -0
  109. package/dist/molecules/SidebarNavSection.svelte +12 -4
  110. package/dist/molecules/SidebarNavSection.svelte.d.ts +2 -0
  111. package/dist/molecules/SlideUp.svelte +85 -21
  112. package/dist/molecules/SlideUp.svelte.d.ts +5 -4
  113. package/dist/molecules/Tabs.svelte +25 -19
  114. package/dist/molecules/Tabs.svelte.d.ts +8 -4
  115. package/dist/molecules/Toaster.svelte +3 -2
  116. package/dist/molecules/ToasterToast.svelte +41 -10
  117. package/dist/molecules/ToasterToast.svelte.d.ts +2 -0
  118. package/dist/organisms/SidebarAccountPanel.svelte +4 -0
  119. package/dist/organisms/SidebarAccountPanel.svelte.d.ts +2 -0
  120. package/dist/organisms/SidebarNavigation.svelte +102 -59
  121. package/dist/organisms/SidebarNavigation.svelte.d.ts +2 -0
  122. package/dist/organisms/SidebarPanel.svelte +19 -11
  123. package/dist/organisms/SidebarPanel.svelte.d.ts +2 -0
  124. package/dist/organisms/SidebarShell.svelte +112 -0
  125. package/dist/organisms/SidebarShell.svelte.d.ts +52 -0
  126. package/dist/organisms/TopNavbar.svelte +88 -17
  127. package/dist/organisms/TopNavbar.svelte.d.ts +2 -0
  128. package/dist/organisms/index.d.ts +1 -0
  129. package/dist/organisms/index.js +1 -0
  130. package/dist/routes/lib/variant-utils.d.ts +26 -0
  131. package/dist/routes/lib/{variant-utils.ts → variant-utils.js} +15 -46
  132. package/dist/types/events.js +1 -0
  133. package/dist/types/index.js +3 -0
  134. package/dist/util/cn.d.ts +7 -0
  135. package/dist/util/cn.js +48 -0
  136. package/dist/util/ssr-safe.d.ts +4 -4
  137. package/dist/zabi-components-colors.css +3 -437
  138. package/dist/zabi-components-theme-dark-only.css +223 -103
  139. package/dist/zabi-components-theme-dark.css +223 -103
  140. package/dist/zabi-components-theme-only.css +347 -107
  141. package/dist/zabi-components-theme.css +347 -107
  142. package/dist/zabi-components.css +953 -843
  143. package/docs/lucide-icons.md +3 -1
  144. package/package.json +20 -25
  145. package/dist/lib/layout-width-tokens.d.ts +0 -17
  146. package/dist/lib/layout-width-tokens.d.ts.map +0 -1
  147. package/dist/lib/showcase/component-docs/Button.d.ts +0 -3
  148. package/dist/lib/showcase/component-docs/Button.d.ts.map +0 -1
  149. package/dist/lib/showcase/component-docs/Input.d.ts +0 -3
  150. package/dist/lib/showcase/component-docs/Input.d.ts.map +0 -1
  151. package/dist/lib/showcase/component-docs/List.d.ts +0 -3
  152. package/dist/lib/showcase/component-docs/List.d.ts.map +0 -1
  153. package/dist/lib/showcase/component-docs/Modal.d.ts +0 -3
  154. package/dist/lib/showcase/component-docs/Modal.d.ts.map +0 -1
  155. package/dist/lib/showcase/component-docs/Radio.d.ts +0 -3
  156. package/dist/lib/showcase/component-docs/Radio.d.ts.map +0 -1
  157. package/dist/lib/showcase/component-docs/SidebarNavigation.d.ts +0 -3
  158. package/dist/lib/showcase/component-docs/SidebarNavigation.d.ts.map +0 -1
  159. package/dist/lib/showcase/component-docs/Skeleton.d.ts +0 -3
  160. package/dist/lib/showcase/component-docs/Skeleton.d.ts.map +0 -1
  161. package/dist/lib/showcase/component-docs/_shared.d.ts +0 -12
  162. package/dist/lib/showcase/component-docs/_shared.d.ts.map +0 -1
  163. package/dist/lib/showcase/component-docs/index.d.ts +0 -4
  164. package/dist/lib/showcase/component-docs/index.d.ts.map +0 -1
  165. package/dist/lib/showcase/components-catalog.d.ts +0 -3
  166. package/dist/lib/showcase/components-catalog.d.ts.map +0 -1
  167. package/dist/lib/showcase/components-showcase-constants.d.ts +0 -89
  168. package/dist/lib/showcase/components-showcase-constants.d.ts.map +0 -1
  169. package/dist/lib/showcase/docs-sidebar-helpers.d.ts +0 -15
  170. package/dist/lib/showcase/docs-sidebar-helpers.d.ts.map +0 -1
  171. package/dist/routes/lib/focus-utils.d.ts +0 -12
  172. package/dist/routes/lib/focus-utils.ts +0 -110
  173. package/dist/routes/lib/ssr-safe.ts +0 -58
  174. package/scripts/fix-lucide-svelte-icon-dts.js +0 -35
@@ -1,5 +1,7 @@
1
1
  <script lang="ts">
2
+ import { Check, TriangleAlert, X, Info, Zap } from "@lucide/svelte";
2
3
  import type { ExtendedSemanticVariant } from "../types/variants.js";
4
+ import { cn } from "../util/cn.js";
3
5
  type AlertVisualVariant = Exclude<ExtendedSemanticVariant, "default">;
4
6
 
5
7
  interface Props {
@@ -7,6 +9,12 @@
7
9
  title?: string;
8
10
  message?: string;
9
11
  closable?: boolean;
12
+ /** Visible state; `closable` dismiss sets it to `false`. Supports `bind:open`. */
13
+ open?: boolean;
14
+ /** Shrink to content instead of filling the container. */
15
+ inline?: boolean;
16
+ class?: string;
17
+ /** @deprecated use `class` — kept so 7.x call sites keep working. */
10
18
  className?: string;
11
19
  onclick?: (event: Event) => void;
12
20
  }
@@ -16,6 +24,9 @@
16
24
  title = "",
17
25
  message = "",
18
26
  closable = false,
27
+ open = $bindable(true),
28
+ inline = false,
29
+ class: classProp = "",
19
30
  className = "",
20
31
  onclick,
21
32
  children,
@@ -23,6 +34,7 @@
23
34
  }: Props & { children?: any } = $props();
24
35
 
25
36
  function handleDismiss(event: MouseEvent) {
37
+ open = false;
26
38
  if (onclick) {
27
39
  onclick(event);
28
40
  }
@@ -32,13 +44,20 @@
32
44
  variant === "default" ? "info" : variant,
33
45
  );
34
46
 
47
+ /**
48
+ * A tinted fill plus a tinted edge, not a bare 1px outline on a
49
+ * transparent ground — an outline-only alert on a light page barely
50
+ * registers, and the pale warning and citron borders were close to
51
+ * invisible. The icon carries the family colour at its `-text` step.
52
+ */
35
53
  let alertClasses = $derived({
36
- info: "text-info border border-info",
37
- success: "text-success border border-success",
38
- warning: "text-warning border border-warning",
39
- error: "text-error border border-error",
40
- neutral: "text-neutral border border-neutral",
41
- energetic: "text-energetic border border-energetic",
54
+ info: "bg-info-subtle border-info-border text-info-text",
55
+ success: "bg-success-subtle border-success-border text-success-text",
56
+ warning: "bg-warning-subtle border-warning-border text-warning-text",
57
+ error: "bg-error-subtle border-error-border text-error-text",
58
+ neutral: "bg-neutral-subtle border-neutral-border text-neutral-text",
59
+ energetic:
60
+ "bg-energetic-subtle border-energetic-border text-energetic-text",
42
61
  });
43
62
 
44
63
  let alertRole = $derived(
@@ -48,83 +67,42 @@
48
67
 
49
68
  {#snippet icon()}
50
69
  {#if visualVariant === "info"}
51
- <svg class="h-5 w-5" fill="currentColor" viewBox="0 0 20 20">
52
- <path
53
- fill-rule="evenodd"
54
- d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z"
55
- clip-rule="evenodd"
56
- />
57
- </svg>
70
+ <Info size={20} aria-hidden="true" />
58
71
  {:else if visualVariant === "success"}
59
- <svg class="h-5 w-5" fill="currentColor" viewBox="0 0 20 20">
60
- <path
61
- fill-rule="evenodd"
62
- d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
63
- clip-rule="evenodd"
64
- />
65
- </svg>
72
+ <Check size={20} aria-hidden="true" />
66
73
  {:else if visualVariant === "warning"}
67
- <svg class="h-5 w-5" fill="currentColor" viewBox="0 0 20 20">
68
- <path
69
- fill-rule="evenodd"
70
- d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z"
71
- clip-rule="evenodd"
72
- />
73
- </svg>
74
+ <TriangleAlert size={20} aria-hidden="true" />
74
75
  {:else if visualVariant === "error"}
75
- <svg class="h-5 w-5" fill="currentColor" viewBox="0 0 20 20">
76
- <path
77
- fill-rule="evenodd"
78
- d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z"
79
- clip-rule="evenodd"
80
- />
81
- </svg>
76
+ <X size={20} aria-hidden="true" />
82
77
  {:else if visualVariant === "neutral"}
83
- <svg class="h-5 w-5" fill="currentColor" viewBox="0 0 20 20">
84
- <path
85
- fill-rule="evenodd"
86
- d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zM10 9a1 1 0 100 2 1 1 0 000-2z"
87
- clip-rule="evenodd"
88
- />
89
- </svg>
78
+ <Info size={20} aria-hidden="true" />
90
79
  {:else}
91
- <svg class="h-5 w-5" fill="currentColor" viewBox="0 0 20 20">
92
- <path d="M11 3L4 14h6l-1 7 7-11h-6l1-7z" />
93
- </svg>
80
+ <Zap size={20} aria-hidden="true" />
94
81
  {/if}
95
82
  {/snippet}
96
83
 
84
+ {#if open}
97
85
  <div
98
- class="relative rounded-md p-4 border {alertClasses[
99
- visualVariant
100
- ]} transition-all duration-200 motion-reduce:transition-none {className}"
86
+ class={cn(
87
+ 'relative rounded-container p-4 border',
88
+ inline ? 'inline-block' : 'block w-full',
89
+ alertClasses[visualVariant],
90
+ 'transition-colors duration-150 motion-reduce:transition-none',
91
+ classProp,
92
+ className,
93
+ )}
101
94
  role={alertRole}
102
- aria-live={visualVariant === "success" || visualVariant === "info"
103
- ? "polite"
104
- : "assertive"}
105
95
  aria-atomic="true"
106
96
  {...restProps}
107
97
  >
108
98
  {#if closable}
109
99
  <button
110
100
  onclick={handleDismiss}
111
- class="group absolute right-2 top-2 z-10 flex h-6 w-6 cursor-pointer items-center justify-center rounded-full text-description transition-colors duration-200 hover:text-body motion-reduce:transition-none focus-ring"
101
+ class="absolute right-2 top-2 z-10 flex size-6 cursor-pointer items-center justify-center rounded-control text-description transition-colors duration-150 hover:bg-surface-hover hover:text-body motion-reduce:transition-none focus-ring--muted"
112
102
  aria-label="Dismiss alert"
113
103
  type="button"
114
104
  >
115
- <svg
116
- class="w-4 h-4 text-current group-hover:scale-110 transition-transform duration-200 motion-reduce:transition-none"
117
- fill="none"
118
- stroke="currentColor"
119
- viewBox="0 0 24 24"
120
- >
121
- <path
122
- stroke-linecap="round"
123
- stroke-linejoin="round"
124
- stroke-width="2"
125
- d="M6 18L18 6M6 6l12 12"
126
- />
127
- </svg>
105
+ <X size={16} aria-hidden="true" />
128
106
  </button>
129
107
  {/if}
130
108
 
@@ -148,3 +126,4 @@
148
126
  </div>
149
127
  </div>
150
128
  </div>
129
+ {/if}
@@ -4,12 +4,18 @@ interface Props {
4
4
  title?: string;
5
5
  message?: string;
6
6
  closable?: boolean;
7
+ /** Visible state; `closable` dismiss sets it to `false`. Supports `bind:open`. */
8
+ open?: boolean;
9
+ /** Shrink to content instead of filling the container. */
10
+ inline?: boolean;
11
+ class?: string;
12
+ /** @deprecated use `class` — kept so 7.x call sites keep working. */
7
13
  className?: string;
8
14
  onclick?: (event: Event) => void;
9
15
  }
10
16
  type $$ComponentProps = Props & {
11
17
  children?: any;
12
18
  };
13
- declare const Alert: import("svelte").Component<$$ComponentProps, {}, "">;
19
+ declare const Alert: import("svelte").Component<$$ComponentProps, {}, "open">;
14
20
  type Alert = ReturnType<typeof Alert>;
15
21
  export default Alert;
@@ -3,12 +3,15 @@
3
3
  import Card from "../atoms/Card.svelte";
4
4
  import CardHeader from "../atoms/CardHeader.svelte";
5
5
  import CardContent from "../atoms/CardContent.svelte";
6
+ import { cn } from "../util/cn.js";
6
7
 
7
8
  interface Props {
8
9
  title: string;
9
10
  description?: string;
10
11
  code: string;
11
12
  language?: string;
13
+ class?: string;
14
+ /** @deprecated use `class`. */
12
15
  className?: string;
13
16
  }
14
17
 
@@ -17,11 +20,16 @@
17
20
  description = "",
18
21
  code,
19
22
  language = "svelte",
20
- className = "",
23
+ class: classAttr = "",
24
+ className: legacyClass = "",
21
25
  children,
22
26
  ...restProps
23
27
  } = $props<Props & { children?: any }>();
24
28
 
29
+ /** `class` is the public prop; `className` is a deprecated alias.
30
+ * Both are merged here so existing call sites keep working. */
31
+ const className = $derived(cn(`${classAttr} ${legacyClass}`));
32
+
25
33
  let showCode = $state(false);
26
34
  </script>
27
35
 
@@ -36,7 +44,7 @@
36
44
  <CardContent>
37
45
  <button
38
46
  onclick={() => (showCode = !showCode)}
39
- class="absolute top-8 right-8 z-10 flex cursor-pointer items-center gap-2 rounded-md px-3 py-1.5 text-sm text-description transition-colors duration-200 hover:bg-base-50 hover:text-body"
47
+ class="absolute top-8 right-8 z-10 flex cursor-pointer items-center gap-2 rounded-control px-3 py-2 text-sm text-description transition-colors duration-200 hover:bg-surface-hover hover:text-body"
40
48
  aria-label={showCode ? "Show preview" : "Show code"}
41
49
  >
42
50
  {#if showCode}
@@ -3,6 +3,8 @@ interface Props {
3
3
  description?: string;
4
4
  code: string;
5
5
  language?: string;
6
+ class?: string;
7
+ /** @deprecated use `class`. */
6
8
  className?: string;
7
9
  }
8
10
  type $$ComponentProps = Props & {
@@ -8,17 +8,25 @@
8
8
  import CardHeader from "../atoms/CardHeader.svelte";
9
9
  import CardContent from "../atoms/CardContent.svelte";
10
10
  import type { ContactFormData } from "../types/page.types";
11
+ import { cn } from "../util/cn.js";
11
12
 
12
13
  interface Props {
14
+ class?: string;
15
+ /** @deprecated use `class`. */
13
16
  className?: string;
14
17
  onsubmit?: (event: SubmitEvent) => void;
15
18
  }
16
19
 
17
20
  let {
18
- className = "",
21
+ class: classAttr = "",
22
+ className: legacyClass = "",
19
23
  onsubmit,
20
24
  }: Props = $props();
21
25
 
26
+ /** `class` is the public prop; `className` is a deprecated alias.
27
+ * Both are merged here so existing call sites keep working. */
28
+ const className = $derived(cn(`${classAttr} ${legacyClass}`));
29
+
22
30
  let formData = $state<ContactFormData>({
23
31
  name: "",
24
32
  email: "",
@@ -60,6 +68,8 @@
60
68
  };
61
69
 
62
70
  if (!validate(data)) {
71
+ // Block native submission so the page does not reload and wipe the errors.
72
+ event.preventDefault();
63
73
  formErrorMessage =
64
74
  "We could not submit your request. Fix the highlighted fields and try again.";
65
75
  return;
@@ -79,7 +89,7 @@
79
89
  <Form onsubmit={handleFormSubmit} className="space-y-4">
80
90
  {#if formErrorMessage}
81
91
  <div
82
- class="rounded-lg border border-error px-4 py-3 text-sm text-error"
92
+ class="rounded-control border border-error px-4 py-3 text-sm text-error"
83
93
  role="alert"
84
94
  >
85
95
  <p class="font-medium">Something went wrong</p>
@@ -1,4 +1,6 @@
1
1
  interface Props {
2
+ class?: string;
3
+ /** @deprecated use `class`. */
2
4
  className?: string;
3
5
  onsubmit?: (event: SubmitEvent) => void;
4
6
  }
@@ -1,6 +1,7 @@
1
1
  <script lang="ts">
2
2
  import { generateId } from "../util/ssr-safe.js";
3
3
  import type { Snippet } from 'svelte';
4
+ import { cn } from "../util/cn.js";
4
5
 
5
6
  export type DropdownTriggerProps = {
6
7
  'aria-expanded': boolean;
@@ -9,6 +10,8 @@
9
10
  };
10
11
 
11
12
  interface Props {
13
+ /** Extra classes for the host element. */
14
+ class?: string;
12
15
  isOpen?: boolean;
13
16
  placement?: 'bottom-start' | 'bottom-end' | 'top-start' | 'top-end';
14
17
  ariaLabel?: string;
@@ -22,10 +25,13 @@
22
25
  }>;
23
26
  onOptionClick?: (value: string | number) => void;
24
27
  trigger: Snippet<[DropdownTriggerProps]>;
28
+ /** Rendered in the popup above the menu/listbox, outside its role (e.g. a search field). */
29
+ header?: Snippet;
25
30
  children?: Snippet;
26
31
  }
27
32
 
28
33
  let {
34
+ class: className = "",
29
35
  isOpen = $bindable(false),
30
36
  placement = 'bottom-start',
31
37
  ariaLabel = 'Menu',
@@ -34,6 +40,7 @@
34
40
  options = [],
35
41
  onOptionClick,
36
42
  trigger,
43
+ header,
37
44
  children,
38
45
  ...restProps
39
46
  }: Props = $props();
@@ -49,8 +56,22 @@
49
56
  'aria-controls': menuId,
50
57
  } satisfies DropdownTriggerProps);
51
58
 
59
+ function isTextEntryTarget(target: EventTarget | null): boolean {
60
+ if (!(target instanceof HTMLElement)) return false;
61
+ if (target.isContentEditable || target instanceof HTMLTextAreaElement) {
62
+ return true;
63
+ }
64
+ return (
65
+ target instanceof HTMLInputElement &&
66
+ !['checkbox', 'radio', 'button', 'submit', 'reset'].includes(target.type)
67
+ );
68
+ }
69
+
52
70
  function handleKeydown(event: KeyboardEvent) {
53
71
  if (!isOpen) {
72
+ if (event.key === ' ' && isTextEntryTarget(event.target)) {
73
+ return;
74
+ }
54
75
  if (
55
76
  event.key === 'Enter' ||
56
77
  event.key === ' ' ||
@@ -77,10 +98,13 @@
77
98
  focusPreviousItem();
78
99
  break;
79
100
  case 'Home':
101
+ // Let text fields (e.g. Select search) keep native caret movement.
102
+ if (isTextEntryTarget(event.target)) break;
80
103
  event.preventDefault();
81
104
  focusFirstItem();
82
105
  break;
83
106
  case 'End':
107
+ if (isTextEntryTarget(event.target)) break;
84
108
  event.preventDefault();
85
109
  focusLastItem();
86
110
  break;
@@ -94,9 +118,13 @@
94
118
 
95
119
  function getMenuItems(): HTMLElement[] {
96
120
  if (!menuElement) return [];
121
+ // `menuitemradio` and `menuitemcheckbox` are menu items too. Matching
122
+ // only `menuitem` left a menu built from them with nothing to focus:
123
+ // arrows and Home/End did nothing, opening by keyboard focused nothing,
124
+ // and Tab closed the menu, so its items could not be reached at all.
97
125
  return Array.from(
98
126
  menuElement.querySelectorAll<HTMLElement>(
99
- '[role="menuitem"], [role="option"]',
127
+ '[role="menuitem"], [role="menuitemradio"], [role="menuitemcheckbox"], [role="option"]',
100
128
  ),
101
129
  );
102
130
  }
@@ -182,7 +210,7 @@
182
210
  const dropdownContentClasses = $derived(() => {
183
211
  return [
184
212
  placementClasses(),
185
- 'rounded-lg border border-border bg-card py-2 shadow-lg transition-all duration-200 ease-in-out',
213
+ 'rounded-control border border-border bg-surface-overlay py-2 shadow-lg transition-all duration-200 ease-in-out',
186
214
  transformClasses(),
187
215
  ]
188
216
  .join(' ')
@@ -193,12 +221,12 @@
193
221
  const itemRole = $derived(menuRole === 'listbox' ? 'option' : 'menuitem');
194
222
 
195
223
  const optionClasses =
196
- 'focus-ring flex w-full items-center justify-start rounded-md px-3 py-2 text-left text-sm text-body transition-colors hover:bg-base-100 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50';
224
+ 'focus-ring flex w-full items-center justify-start rounded-control px-3 py-2 text-left text-sm text-body transition-colors hover:bg-surface-overlay-hover focus:outline-none disabled:cursor-not-allowed disabled:opacity-50';
197
225
  </script>
198
226
 
199
227
  <div
200
228
  bind:this={rootEl}
201
- class="relative inline-block"
229
+ class={cn("relative inline-block", className)}
202
230
  data-placement={placement}
203
231
  onkeydown={handleKeydown}
204
232
  {...restProps}
@@ -206,13 +234,13 @@
206
234
  {@render trigger(triggerAria)}
207
235
 
208
236
  {#if isOpen}
209
- <div
210
- bind:this={menuElement}
211
- id={menuId}
212
- class={dropdownContentClasses()}
213
- role={menuRole === 'listbox' ? 'listbox' : 'menu'}
214
- aria-label={ariaLabel}
215
- >
237
+ <div bind:this={menuElement} class={dropdownContentClasses()}>
238
+ {@render header?.()}
239
+ <div
240
+ id={menuId}
241
+ role={menuRole === 'listbox' ? 'listbox' : 'menu'}
242
+ aria-label={ariaLabel}
243
+ >
216
244
  {#if options.length > 0}
217
245
  <div class="px-2 py-1">
218
246
  {#each options as option (option.value)}
@@ -236,6 +264,7 @@
236
264
  {:else if children}
237
265
  {@render children?.()}
238
266
  {/if}
267
+ </div>
239
268
  </div>
240
269
  {/if}
241
270
  </div>
@@ -5,6 +5,8 @@ export type DropdownTriggerProps = {
5
5
  'aria-controls': string;
6
6
  };
7
7
  interface Props {
8
+ /** Extra classes for the host element. */
9
+ class?: string;
8
10
  isOpen?: boolean;
9
11
  placement?: 'bottom-start' | 'bottom-end' | 'top-start' | 'top-end';
10
12
  ariaLabel?: string;
@@ -18,6 +20,8 @@ interface Props {
18
20
  }>;
19
21
  onOptionClick?: (value: string | number) => void;
20
22
  trigger: Snippet<[DropdownTriggerProps]>;
23
+ /** Rendered in the popup above the menu/listbox, outside its role (e.g. a search field). */
24
+ header?: Snippet;
21
25
  children?: Snippet;
22
26
  }
23
27
  declare const Dropdown: import("svelte").Component<Props, {}, "isOpen">;
@@ -1,6 +1,7 @@
1
1
  <script lang="ts">
2
2
  import type { Snippet } from "svelte";
3
3
  import { generateId } from "../util/ssr-safe.js";
4
+ import { cn } from "../util/cn.js";
4
5
 
5
6
  interface Props {
6
7
  title: string;
@@ -22,7 +23,7 @@
22
23
  </script>
23
24
 
24
25
  <section
25
- class="mx-auto flex flex-col items-center justify-center gap-4 px-6 py-12 text-center {className}"
26
+ class={cn("mx-auto flex flex-col items-center justify-center gap-4 px-6 py-12 text-center", className)}
26
27
  aria-labelledby={titleId}
27
28
  >
28
29
  {#if media}
@@ -1,35 +1,46 @@
1
1
  <script lang="ts">
2
- interface Props {
2
+ import type { Snippet } from "svelte";
3
+ import type { HTMLFormAttributes } from "svelte/elements";
4
+ import { cn } from "../util/cn.js";
5
+
6
+ type Props = Omit<HTMLFormAttributes, "method" | "action" | "onsubmit" | "class"> & {
3
7
  method?: "get" | "post";
4
8
  action?: string;
9
+ class?: string;
10
+ /** @deprecated use `class`. */
5
11
  className?: string;
12
+ /** Skip native constraint validation (e.g. when validating in `onsubmit`). */
13
+ novalidate?: boolean;
14
+ /**
15
+ * Runs on submit. The form still submits natively unless you call
16
+ * `event.preventDefault()` — do so for client-side handling.
17
+ */
6
18
  onsubmit?: (event: SubmitEvent) => void;
7
- }
19
+ children?: Snippet;
20
+ };
8
21
 
9
22
  let {
10
23
  method = "post",
11
24
  action = "",
12
- className = "",
25
+ class: classAttr = "",
26
+ className: legacyClass = "",
27
+ novalidate = false,
13
28
  onsubmit,
14
29
  children,
15
30
  ...restProps
16
- } = $props<Props & { children?: any }>();
17
-
18
- function handleSubmit(event: SubmitEvent) {
19
- const formData = new FormData(event.target as HTMLFormElement);
20
- const data = Object.fromEntries(formData.entries());
31
+ }: Props = $props();
21
32
 
22
- if (onsubmit) {
23
- onsubmit(event);
24
- }
25
- }
33
+ /** `class` is the public prop; `className` is a deprecated alias.
34
+ * Both are merged here so existing call sites keep working. */
35
+ const className = $derived(cn(`${classAttr} ${legacyClass}`));
26
36
  </script>
27
37
 
28
38
  <form
29
39
  {method}
30
40
  {action}
31
- class="space-y-4 {className}"
32
- onsubmit={handleSubmit}
41
+ novalidate={novalidate || undefined}
42
+ class={cn("space-y-4", className)}
43
+ onsubmit={(event) => onsubmit?.(event)}
33
44
  {...restProps}
34
45
  >
35
46
  {@render children?.()}
@@ -1,12 +1,20 @@
1
- interface Props {
1
+ import type { Snippet } from "svelte";
2
+ import type { HTMLFormAttributes } from "svelte/elements";
3
+ type Props = Omit<HTMLFormAttributes, "method" | "action" | "onsubmit" | "class"> & {
2
4
  method?: "get" | "post";
3
5
  action?: string;
6
+ class?: string;
7
+ /** @deprecated use `class`. */
4
8
  className?: string;
9
+ /** Skip native constraint validation (e.g. when validating in `onsubmit`). */
10
+ novalidate?: boolean;
11
+ /**
12
+ * Runs on submit. The form still submits natively unless you call
13
+ * `event.preventDefault()` — do so for client-side handling.
14
+ */
5
15
  onsubmit?: (event: SubmitEvent) => void;
6
- }
7
- type $$ComponentProps = Props & {
8
- children?: any;
16
+ children?: Snippet;
9
17
  };
10
- declare const Form: import("svelte").Component<$$ComponentProps, {}, "">;
18
+ declare const Form: import("svelte").Component<Props, {}, "">;
11
19
  type Form = ReturnType<typeof Form>;
12
20
  export default Form;
@@ -2,6 +2,7 @@
2
2
  import type { Snippet } from 'svelte';
3
3
  import { generateId } from "../util/ssr-safe.js";
4
4
 
5
+ import { cn } from "../util/cn.js";
5
6
  export type FormFieldControlProps = {
6
7
  id: string;
7
8
  'aria-describedby'?: string;
@@ -47,7 +48,7 @@
47
48
  );
48
49
  </script>
49
50
 
50
- <div class={['space-y-1.5', className].filter(Boolean).join(' ')}>
51
+ <div class={cn('space-y-2', className)}>
51
52
  <div class="flex items-baseline justify-between gap-3">
52
53
  <label
53
54
  class={[
@@ -2,18 +2,28 @@
2
2
  import Badge from "../atoms/Badge.svelte";
3
3
  import Heading from "../atoms/Heading.svelte";
4
4
  import type { ComponentCategory } from "../types/page.types";
5
+ import { cn } from "../util/cn.js";
5
6
 
6
7
  interface Props {
8
+ /** Extra classes for the host element. */
9
+ class?: string;
7
10
  title: string;
8
11
  description: string;
9
12
  category: ComponentCategory;
10
13
  variantsStates?: string[];
11
14
  }
12
15
 
13
- let { title, description, category, variantsStates }: Props = $props();
16
+ let {
17
+ class: className = "",
18
+ title,
19
+ description,
20
+ category,
21
+ variantsStates,
22
+ ...restProps
23
+ }: Props = $props();
14
24
  </script>
15
25
 
16
- <header class="space-y-4">
26
+ <header class={cn("space-y-4", className)} {...restProps}>
17
27
  <div class="space-y-2">
18
28
  <Heading level={1} text={title} />
19
29
  <p class="text-lg whitespace-pre-line text-description">{description}</p>
@@ -32,7 +42,7 @@
32
42
  Variants / states:
33
43
  </span>
34
44
  {#each variantsStates as v (v)}
35
- <span class="rounded-md bg-base-100 px-2 py-1 text-xs text-headline">
45
+ <span class="rounded-control bg-base-100 px-2 py-1 text-xs text-headline">
36
46
  {v}
37
47
  </span>
38
48
  {/each}
@@ -1,5 +1,7 @@
1
1
  import type { ComponentCategory } from "../types/page.types";
2
2
  interface Props {
3
+ /** Extra classes for the host element. */
4
+ class?: string;
3
5
  title: string;
4
6
  description: string;
5
7
  category: ComponentCategory;