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
@@ -8,8 +8,11 @@
8
8
  AlertCircle,
9
9
  } from "@lucide/svelte";
10
10
  import { generateId } from "../util/ssr-safe.js";
11
+ import { cn } from "../util/cn.js";
11
12
 
12
13
  interface Props {
14
+ /** Extra classes for the host element. */
15
+ class?: string;
13
16
  value?: string | number | undefined;
14
17
  options?: Array<{
15
18
  value: string | number;
@@ -28,6 +31,8 @@
28
31
  emptyStateActionLabel?: string;
29
32
  placeholder?: string;
30
33
  label?: string;
34
+ /** When set, a hidden input submits the selected value with native forms. */
35
+ name?: string;
31
36
  required?: boolean;
32
37
  disabled?: boolean;
33
38
  size?: "sm" | "md" | "lg";
@@ -38,6 +43,7 @@
38
43
  }
39
44
 
40
45
  let {
46
+ class: className = "",
41
47
  value = $bindable(undefined),
42
48
  options = [],
43
49
  searchable = true,
@@ -52,6 +58,7 @@
52
58
  emptyStateActionLabel = "",
53
59
  placeholder = "Select an option",
54
60
  label = "",
61
+ name = "",
55
62
  required = false,
56
63
  disabled = false,
57
64
  size = "md",
@@ -66,26 +73,11 @@
66
73
  let selectContainer: HTMLDivElement;
67
74
  let searchQuery = $state("");
68
75
 
76
+ // Same fixed height scale as Button, IconButton and Input (32 / 40 / 48).
69
77
  const sizeClass = $derived(() => {
70
- if (size === "sm") {
71
- return {
72
- padding: "px-4 py-2",
73
- text: "text-sm",
74
- leading: "leading-5",
75
- };
76
- } else if (size === "lg") {
77
- return {
78
- padding: "px-4 py-3",
79
- text: "text-base",
80
- leading: "leading-6",
81
- };
82
- } else {
83
- return {
84
- padding: "px-4 py-2.5",
85
- text: "text-base",
86
- leading: "leading-6",
87
- };
88
- }
78
+ if (size === "sm") return { box: "h-8 px-3", text: "text-sm" };
79
+ if (size === "lg") return { box: "h-12 px-4", text: "text-base" };
80
+ return { box: "h-10 px-3", text: "text-sm" };
89
81
  });
90
82
 
91
83
  const variantClass = $derived(() => {
@@ -101,24 +93,24 @@
101
93
  const triggerClasses = $derived(() => {
102
94
  const sizeStyles = sizeClass();
103
95
  const baseClasses =
104
- "focus-ring flex w-full cursor-pointer items-center justify-between rounded-lg border bg-input text-body transition-all duration-200 hover:bg-input-hover active:bg-input-focus focus-visible:bg-input-focus focus:outline-none focus-visible:outline-none disabled:cursor-not-allowed disabled:bg-input-disabled disabled:opacity-50";
96
+ "focus-ring flex w-full cursor-pointer items-center justify-between gap-2 rounded-control border bg-input text-body transition-colors duration-150 hover:bg-input-hover active:bg-input-focus focus-visible:bg-input-focus focus:outline-none focus-visible:outline-none disabled:cursor-not-allowed disabled:bg-input-disabled disabled:text-action-disabled-text";
105
97
 
106
- return `${baseClasses} ${sizeStyles.padding} ${sizeStyles.text} ${sizeStyles.leading} ${variantClass()}`.trim();
98
+ return cn(`${baseClasses} ${sizeStyles.box} ${sizeStyles.text} ${variantClass()}`);
107
99
  });
108
100
 
109
101
  const labelClasses = $derived(
110
- () => "block text-sm font-medium text-label mb-1",
102
+ () => "block text-sm font-medium text-label mb-2",
111
103
  );
112
104
 
113
105
  const messageClasses = $derived(() => {
114
106
  if (variant === "error") {
115
- return "text-error text-sm mt-1 flex items-center gap-1.5 w-full";
107
+ return "text-error text-sm mt-1 flex items-center gap-2 w-full";
116
108
  } else if (variant === "success") {
117
- return "text-success text-sm mt-1 flex items-center gap-1.5 w-full";
109
+ return "text-success text-sm mt-1 flex items-center gap-2 w-full";
118
110
  } else if (variant === "warning") {
119
- return "text-warning text-sm mt-1 flex items-center gap-1.5 w-full";
111
+ return "text-warning text-sm mt-1 flex items-center gap-2 w-full";
120
112
  }
121
- return "text-description text-sm mt-1 flex items-center gap-1.5 w-full";
113
+ return "text-description text-sm mt-1 flex items-center gap-2 w-full";
122
114
  });
123
115
 
124
116
  const getIcon = $derived(() => {
@@ -130,25 +122,24 @@
130
122
 
131
123
  const selectId = generateId("select");
132
124
 
133
- const selectedLabel = $derived(() => {
134
- if (isEmpty()) {
135
- return String(placeholder || "Select an option");
136
- }
137
- const selected = options.find((opt) => opt.value === value);
138
- return selected?.label
139
- ? String(selected.label)
140
- : String(placeholder || "Select an option");
141
- });
142
-
143
125
  const isEmpty = $derived(() => {
144
126
  return value === undefined || value === null || value === "";
145
127
  });
146
128
 
147
- const normalizedOptions = $derived(() => {
148
- const fallbackOptions = (restProps as { options?: Props["options"] })
149
- .options;
150
- return Array.from(options ?? fallbackOptions ?? []);
151
- });
129
+ /** String comparison so `"2"` and `2` match — same rule as Dropdown's `selectedValue`. */
130
+ function isSameValue(
131
+ a: string | number | null | undefined,
132
+ b: string | number | null | undefined,
133
+ ): boolean {
134
+ if (a === undefined || a === null || b === undefined || b === null) {
135
+ return false;
136
+ }
137
+ return String(a) === String(b);
138
+ }
139
+
140
+ const selectedOption = $derived.by(() =>
141
+ options.find((opt) => isSameValue(opt.value, value)),
142
+ );
152
143
 
153
144
  const hasSearchQuery = $derived(() => {
154
145
  return searchQuery.trim().length > 0;
@@ -156,7 +147,7 @@
156
147
 
157
148
  const filteredOptions = $derived(() => {
158
149
  const query = searchQuery.trim().toLowerCase();
159
- const availableOptions = normalizedOptions();
150
+ const availableOptions = options;
160
151
  if (!searchable || query.length === 0) {
161
152
  return availableOptions;
162
153
  }
@@ -215,7 +206,7 @@
215
206
 
216
207
  <svelte:window onclick={handleClickOutside} onkeydown={handleKeydown} />
217
208
 
218
- <div bind:this={selectContainer} class="w-full select-container">
209
+ <div bind:this={selectContainer} class={cn("w-full select-container", className)}>
219
210
  {#if label}
220
211
  <label for={selectId} class={labelClasses()}>{label}</label>
221
212
  {/if}
@@ -250,8 +241,7 @@
250
241
  {:else}
251
242
  {isEmpty()
252
243
  ? placeholder
253
- : options.find((opt) => opt.value === value)?.label ||
254
- placeholder}
244
+ : selectedOption?.label || placeholder}
255
245
  {/if}
256
246
  </span>
257
247
  <ChevronDown
@@ -262,29 +252,32 @@
262
252
  />
263
253
  </button>
264
254
  {/snippet}
255
+ {#snippet header()}
256
+ {#if searchable && !isLoading}
257
+ <!-- Outside role="listbox": a textbox is not a valid listbox child. -->
258
+ <div class="px-3 pb-2 pt-1" style:width={menuWidth}>
259
+ <Input
260
+ type="text"
261
+ size="sm"
262
+ class="min-w-0"
263
+ placeholder={searchPlaceholder}
264
+ bind:value={searchQuery}
265
+ aria-label={searchPlaceholder}
266
+ {disabled}
267
+ />
268
+ </div>
269
+ {/if}
270
+ {/snippet}
265
271
  {#snippet children()}
266
272
  <div class="px-2 pb-2 pt-1" style:width={menuWidth}>
267
- {#if searchable && !isLoading}
268
- <div class="px-1 pb-2">
269
- <Input
270
- type="text"
271
- size="sm"
272
- class="min-w-0"
273
- placeholder={searchPlaceholder}
274
- bind:value={searchQuery}
275
- aria-label={searchPlaceholder}
276
- {disabled}
277
- />
278
- </div>
279
- {/if}
280
273
  <div
281
274
  class="overflow-y-auto px-1"
282
275
  style:max-height={maxMenuHeight}
283
276
  >
284
277
  {#if isLoading}
285
278
  <div class="space-y-2 px-2 py-2" role="status" aria-live="polite">
286
- <div class="h-8 w-full animate-pulse rounded-md bg-base-200"></div>
287
- <div class="h-8 w-full animate-pulse rounded-md bg-base-200"></div>
279
+ <div class="h-8 w-full animate-pulse rounded-control bg-base-200"></div>
280
+ <div class="h-8 w-full animate-pulse rounded-control bg-base-200"></div>
288
281
  <p class="text-xs text-description">{loadingText}</p>
289
282
  </div>
290
283
  {:else if filteredOptions().length > 0}
@@ -292,17 +285,19 @@
292
285
  {@const buttonRestProps = {
293
286
  "data-value": String(option.value),
294
287
  } as Record<string, string>}
295
- <div class="w-full my-0.5">
288
+ <div class="w-full my-1">
296
289
  <button
297
290
  type="button"
298
291
  role="option"
299
- aria-selected={value === option.value
292
+ aria-selected={isSameValue(value, option.value)
300
293
  ? true
301
294
  : undefined}
302
- class="focus-ring flex w-full items-center justify-start rounded-md border-2 px-3 py-2 text-left text-sm font-medium transition-colors focus:outline-none disabled:cursor-not-allowed disabled:opacity-50 {value ===
303
- option.value
295
+ class="focus-ring flex w-full items-center justify-start rounded-control border-2 px-3 py-2 text-left text-sm font-medium transition-colors focus:outline-none disabled:cursor-not-allowed disabled:opacity-50 {isSameValue(
296
+ value,
297
+ option.value,
298
+ )
304
299
  ? 'border-action-primary bg-transparent text-headline'
305
- : 'border-transparent bg-transparent text-body hover:bg-base-100'}"
300
+ : 'border-transparent bg-transparent text-body hover:bg-surface-overlay-hover'}"
306
301
  disabled={option.disabled}
307
302
  {...buttonRestProps}
308
303
  onclick={() =>
@@ -317,13 +312,13 @@
317
312
  {noResultsText}
318
313
  </div>
319
314
  {:else}
320
- <div class="rounded-md border border-border bg-base-50 px-3 py-3 text-sm">
315
+ <div class="rounded-control border border-border bg-surface-overlay-hover px-3 py-3 text-sm">
321
316
  <p class="font-medium text-headline">{emptyStateTitle}</p>
322
317
  <p class="mt-1 text-description">{emptyStateDescription}</p>
323
318
  {#if emptyStateActionLabel && onEmptyStateAction}
324
319
  <button
325
320
  type="button"
326
- class="mt-3 inline-flex min-h-11 cursor-pointer items-center rounded-lg bg-action-primary px-3 py-2 text-sm text-action-primary"
321
+ class="mt-3 inline-flex min-h-11 cursor-pointer items-center rounded-control bg-action-primary px-3 py-2 text-sm text-action-primary"
327
322
  onclick={onEmptyStateAction}
328
323
  >
329
324
  {emptyStateActionLabel}
@@ -335,6 +330,9 @@
335
330
  </div>
336
331
  {/snippet}
337
332
  </Dropdown>
333
+ {#if name}
334
+ <input type="hidden" {name} value={isEmpty() ? "" : String(value)} />
335
+ {/if}
338
336
  {#if message && variant !== "default"}
339
337
  <p
340
338
  id={`${selectId}-message`}
@@ -1,4 +1,6 @@
1
1
  interface Props {
2
+ /** Extra classes for the host element. */
3
+ class?: string;
2
4
  value?: string | number | undefined;
3
5
  options?: Array<{
4
6
  value: string | number;
@@ -17,6 +19,8 @@ interface Props {
17
19
  emptyStateActionLabel?: string;
18
20
  placeholder?: string;
19
21
  label?: string;
22
+ /** When set, a hidden input submits the selected value with native forms. */
23
+ name?: string;
20
24
  required?: boolean;
21
25
  disabled?: boolean;
22
26
  size?: "sm" | "md" | "lg";
@@ -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
  import {
5
6
  SELECTION_CONTROL_LABEL_ROW,
6
7
  selectionControlRingOverlayClasses,
@@ -17,6 +18,8 @@
17
18
  };
18
19
 
19
20
  interface Props {
21
+ /** Extra classes for the host element. */
22
+ class?: string;
20
23
  id?: string;
21
24
  type: SelectionControlType;
22
25
  shape: SelectionControlShape;
@@ -33,6 +36,7 @@
33
36
  }
34
37
 
35
38
  let {
39
+ class: className = "",
36
40
  id: idProp,
37
41
  type,
38
42
  shape,
@@ -69,7 +73,7 @@
69
73
  }
70
74
  </script>
71
75
 
72
- <div class="flex items-center gap-2">
76
+ <div class={cn("flex items-center gap-2", className)}>
73
77
  <label for={controlId} class={labelWrapperClasses}>
74
78
  <span class={controlContainerClasses}>
75
79
  <input
@@ -7,6 +7,8 @@ export type SelectionControlMarkProps = {
7
7
  disabled: boolean;
8
8
  };
9
9
  interface Props {
10
+ /** Extra classes for the host element. */
11
+ class?: string;
10
12
  id?: string;
11
13
  type: SelectionControlType;
12
14
  shape: SelectionControlShape;
@@ -1,4 +1,6 @@
1
1
  <script lang="ts">
2
+ import { cn } from "../util/cn.js";
3
+
2
4
  type Variant = "text" | "circle" | "block";
3
5
 
4
6
  interface Props {
@@ -20,9 +22,9 @@
20
22
  }: Props = $props();
21
23
 
22
24
  const radius: Record<Variant, string> = {
23
- text: "rounded-md",
25
+ text: "rounded-control",
24
26
  circle: "rounded-full",
25
- block: "rounded-lg",
27
+ block: "rounded-control",
26
28
  };
27
29
 
28
30
  const defaultShape: Record<Variant, string> = {
@@ -46,7 +48,7 @@
46
48
  </script>
47
49
 
48
50
  <div
49
- class="animate-pulse bg-surface-2 {defaultShape[variant]} {radius[variant]} {className}"
51
+ class={cn("animate-pulse bg-surface-2", defaultShape[variant], radius[variant], className)}
50
52
  style={inlineStyle || undefined}
51
53
  role="status"
52
54
  aria-busy="true"
@@ -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
  /** Visible `<caption>` when set. */
@@ -12,7 +13,7 @@
12
13
  </script>
13
14
 
14
15
  <div
15
- class="overflow-x-auto rounded-lg border border-border shadow-sm {className}"
16
+ class={cn("overflow-x-auto rounded-control border border-border shadow-sm", className)}
16
17
  >
17
18
  <table class="w-full min-w-[20rem] border-collapse text-left text-sm text-body">
18
19
  {#if caption}
@@ -1,13 +1,20 @@
1
1
  <script lang="ts">
2
2
  import type { Snippet } from 'svelte';
3
3
 
4
+ import { cn } from "../util/cn.js";
4
5
  type Tone = 'body' | 'description' | 'caption' | 'headline' | 'label' | 'error';
5
- type Size = 'sm' | 'md' | 'lg';
6
+ type Size = 'xs' | 'sm' | 'md' | 'lg';
7
+ type Weight = 'normal' | 'medium' | 'semibold' | 'bold';
6
8
 
7
9
  interface Props {
8
10
  as?: 'p' | 'span' | 'div';
9
11
  tone?: Tone;
10
12
  size?: Size;
13
+ /**
14
+ * Overrides the weight the tone implies — `label` sets `medium`,
15
+ * every other tone sets `normal`.
16
+ */
17
+ weight?: Weight;
11
18
  class?: string;
12
19
  children?: Snippet;
13
20
  }
@@ -16,6 +23,7 @@
16
23
  as: Tag = 'p',
17
24
  tone = 'body',
18
25
  size = 'md',
26
+ weight,
19
27
  class: className = '',
20
28
  children,
21
29
  }: Props = $props();
@@ -29,16 +37,37 @@
29
37
  error: 'text-error',
30
38
  };
31
39
 
40
+ /**
41
+ * The bottom of the same ramp `Heading` sits on, so the two components
42
+ * share one scale rather than each carrying its own. `md` matches the
43
+ * metrics of an `h6` (text-base/leading-6) and `lg` those of an `h5`
44
+ * (text-lg/leading-7) — a Text set at that size lands on the same line
45
+ * box as the heading above it. Leading is stated rather than inherited
46
+ * from Tailwind's defaults so the shared steps stay locked together if
47
+ * the type scale is retuned.
48
+ */
32
49
  const sizeClass: Record<Size, string> = {
33
- sm: 'text-sm',
34
- md: 'text-base',
35
- lg: 'text-lg',
50
+ xs: 'text-xs leading-4',
51
+ sm: 'text-sm leading-5',
52
+ md: 'text-base leading-6',
53
+ lg: 'text-lg leading-7',
36
54
  };
55
+
56
+ const weightClass: Record<Weight, string> = {
57
+ normal: 'font-normal',
58
+ medium: 'font-medium',
59
+ semibold: 'font-semibold',
60
+ bold: 'font-bold',
61
+ };
62
+
63
+ /** Labels read as labels by default; callers can still override. */
64
+ const defaultWeight: Weight = $derived(tone === 'label' ? 'medium' : 'normal');
65
+ const resolvedWeight = $derived(weight ?? defaultWeight);
37
66
  </script>
38
67
 
39
68
  <svelte:element
40
69
  this={Tag}
41
- class={[toneClass[tone], sizeClass[size], className].filter(Boolean).join(' ')}
70
+ class={cn(toneClass[tone], sizeClass[size], weightClass[resolvedWeight], className)}
42
71
  >
43
72
  {#if children}
44
73
  {@render children()}
@@ -1,10 +1,16 @@
1
1
  import type { Snippet } from 'svelte';
2
2
  type Tone = 'body' | 'description' | 'caption' | 'headline' | 'label' | 'error';
3
- type Size = 'sm' | 'md' | 'lg';
3
+ type Size = 'xs' | 'sm' | 'md' | 'lg';
4
+ type Weight = 'normal' | 'medium' | 'semibold' | 'bold';
4
5
  interface Props {
5
6
  as?: 'p' | 'span' | 'div';
6
7
  tone?: Tone;
7
8
  size?: Size;
9
+ /**
10
+ * Overrides the weight the tone implies — `label` sets `medium`,
11
+ * every other tone sets `normal`.
12
+ */
13
+ weight?: Weight;
8
14
  class?: string;
9
15
  children?: Snippet;
10
16
  }
@@ -2,6 +2,7 @@
2
2
  import { CheckCircle, AlertTriangle, AlertCircle } from '@lucide/svelte';
3
3
  import type { SemanticVariant, SizeVariant } from '../types/variants.js';
4
4
  import { generateId } from "../util/ssr-safe.js";
5
+ import { cn } from "../util/cn.js";
5
6
 
6
7
  interface Props {
7
8
  id?: string;
@@ -56,9 +57,9 @@
56
57
 
57
58
  const textareaClasses = $derived(() => {
58
59
  const baseClasses =
59
- 'focus-ring w-full border bg-input hover:bg-input-hover focus-visible:bg-input-focus disabled:bg-input-disabled rounded-lg transition-all duration-200 placeholder:text-description text-body focus:outline-none focus-visible:outline-none disabled:opacity-50 disabled:cursor-not-allowed resize-y px-4 py-2.5 text-base leading-6';
60
+ 'focus-ring w-full border bg-input hover:bg-input-hover focus-visible:bg-input-focus disabled:bg-input-disabled rounded-control transition-colors duration-150 placeholder:text-input-placeholder text-body focus:outline-none focus-visible:outline-none disabled:text-action-disabled-text disabled:cursor-not-allowed resize-y px-3 py-2 text-sm leading-6';
60
61
 
61
- return `${baseClasses} ${variantClass()} ${className}`.trim();
62
+ return cn(`${baseClasses} ${variantClass()} ${className}`);
62
63
  });
63
64
 
64
65
  const labelClasses = $derived(
@@ -67,13 +68,13 @@
67
68
 
68
69
  const messageClasses = $derived(() => {
69
70
  if (variant === 'error') {
70
- return 'mt-1 flex items-center gap-1.5 text-sm text-error';
71
+ return 'mt-1 flex items-center gap-2 text-sm text-error';
71
72
  } else if (variant === 'success') {
72
- return 'mt-1 flex items-center gap-1.5 text-sm text-success';
73
+ return 'mt-1 flex items-center gap-2 text-sm text-success';
73
74
  } else if (variant === 'warning') {
74
- return 'mt-1 flex items-center gap-1.5 text-sm text-warning';
75
+ return 'mt-1 flex items-center gap-2 text-sm text-warning';
75
76
  }
76
- return 'mt-1 flex items-center gap-1.5 text-sm text-description';
77
+ return 'mt-1 flex items-center gap-2 text-sm text-description';
77
78
  });
78
79
 
79
80
  const getIcon = $derived(() => {
@@ -2,6 +2,7 @@
2
2
  import { onMount } from "svelte";
3
3
  import { Sun, Moon } from "@lucide/svelte";
4
4
  import { FOCUS_BRAND_CLASS } from "../util/focus-utils.js";
5
+ import { cn } from "../util/cn.js";
5
6
 
6
7
  function safeLocalStorage(): Storage | undefined {
7
8
  return typeof window !== "undefined" ? localStorage : undefined;
@@ -12,6 +13,8 @@
12
13
  }
13
14
 
14
15
  interface Props {
16
+ /** Extra classes for the host element. */
17
+ class?: string;
15
18
  size?: "sm" | "md" | "lg";
16
19
  variant?: "default" | "ghost" | "outline";
17
20
  disabled?: boolean;
@@ -19,9 +22,11 @@
19
22
  }
20
23
 
21
24
  let {
25
+ class: className = "",
22
26
  size = "md",
23
27
  variant = "default",
24
28
  disabled = false,
29
+ onclick,
25
30
  ...restProps
26
31
  }: Props = $props();
27
32
 
@@ -57,10 +62,8 @@
57
62
  if (mounted && storage) {
58
63
  storage.setItem("theme", isDark ? "dark" : "light");
59
64
  }
60
-
61
- if (onclick) {
62
- (onclick as (event: Event) => void)(event);
63
- }
65
+
66
+ onclick?.(event);
64
67
  }
65
68
 
66
69
  function updateTheme() {
@@ -97,20 +100,20 @@
97
100
 
98
101
  const variantClass = $derived(() => {
99
102
  if (variant === "ghost") {
100
- return "bg-transparent hover:bg-base-100 active:bg-base-200 border-0";
103
+ return "bg-transparent hover:bg-surface-hover active:bg-surface-active border-0";
101
104
  } else if (variant === "outline") {
102
- return "bg-base-50 hover:bg-base-100 active:bg-base-200 border border-border";
105
+ return "bg-action-secondary hover:bg-action-secondary-hover active:bg-action-secondary-active border border-border";
103
106
  } else {
104
- return "bg-base-50 hover:bg-base-100 active:bg-base-200 border-0";
107
+ return "bg-action-secondary hover:bg-action-secondary-hover active:bg-action-secondary-active border-0";
105
108
  }
106
109
  });
107
110
 
108
111
  const buttonClasses = $derived(() => {
109
112
  const sizeStyles = sizeClass();
110
- return `
113
+ return cn(`
111
114
  ${sizeStyles.button}
112
115
  ${variantClass()}
113
- rounded-lg
116
+ rounded-control
114
117
  flex
115
118
  items-center
116
119
  justify-center
@@ -122,16 +125,16 @@
122
125
  disabled:opacity-50
123
126
  disabled:cursor-not-allowed
124
127
  disabled:active:scale-100
125
- disabled:hover:bg-base-50
128
+ disabled:hover:bg-action-disabled
126
129
  ${FOCUS_BRAND_CLASS}
127
- `.trim().replace(/\s+/g, " ");
130
+ `).replace(/\s+/g, " ");
128
131
  });
129
132
  </script>
130
133
 
131
134
  {#if mounted}
132
135
  <button
133
136
  onclick={toggleTheme}
134
- class={buttonClasses()}
137
+ class="{buttonClasses()} {className}"
135
138
  aria-label={isDark ? "Switch to light mode" : "Switch to dark mode"}
136
139
  aria-pressed={isDark}
137
140
  type="button"
@@ -146,12 +149,12 @@
146
149
  </button>
147
150
  {:else}
148
151
  <button
149
- class="w-10 h-10 bg-base-50 rounded-lg flex items-center justify-center text-label cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"
152
+ class="{sizeClass().button} {variantClass()} rounded-control flex items-center justify-center text-label cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"
150
153
  aria-label="Theme toggle"
151
154
  type="button"
152
155
  {disabled}
153
156
  {...restProps}
154
157
  >
155
- <Sun size={20} class="text-label" />
158
+ <Sun size={sizeClass().icon} class="text-label" />
156
159
  </button>
157
160
  {/if}
@@ -1,4 +1,6 @@
1
1
  interface Props {
2
+ /** Extra classes for the host element. */
3
+ class?: string;
2
4
  size?: "sm" | "md" | "lg";
3
5
  variant?: "default" | "ghost" | "outline";
4
6
  disabled?: boolean;
@@ -1,4 +1,6 @@
1
1
  <script lang="ts">
2
+ import { cn } from "../util/cn.js";
3
+
2
4
  interface Props {
3
5
  message?: string;
4
6
  type?: 'success' | 'error' | 'warning' | 'info';
@@ -22,14 +24,14 @@
22
24
  let isVisible = $state(true);
23
25
 
24
26
  const typeClasses: Record<NonNullable<Props['type']>, string> = {
25
- success: 'border-success bg-card text-success',
26
- error: 'border-error bg-card text-error',
27
- warning: 'border-warning bg-card text-warning',
28
- info: 'border-border bg-card text-body',
27
+ success: 'border-success bg-surface-overlay text-success',
28
+ error: 'border-error bg-surface-overlay text-error',
29
+ warning: 'border-warning bg-surface-overlay text-warning',
30
+ info: 'border-border bg-surface-overlay text-body',
29
31
  };
30
32
 
31
33
  const cardClasses =
32
- 'box-border w-full min-w-[18rem] max-w-[min(24rem,calc(100vw-2rem))] overflow-hidden rounded-lg border p-4 shadow-lg';
34
+ 'box-border w-full min-w-[18rem] max-w-[min(24rem,calc(100vw-2rem))] overflow-hidden rounded-control border p-4 shadow-lg';
33
35
 
34
36
  function closeToast(event: Event) {
35
37
  isVisible = false;
@@ -46,7 +48,7 @@
46
48
  {#if closable}
47
49
  <button
48
50
  type="button"
49
- class="focus-ring shrink-0 cursor-pointer rounded-md text-description hover:text-headline focus:outline-none"
51
+ class="focus-ring shrink-0 cursor-pointer rounded-control text-description hover:text-headline focus:outline-none"
50
52
  onclick={closeToast}
51
53
  aria-label="Close notification"
52
54
  >
@@ -62,7 +64,7 @@
62
64
  class="pointer-events-none fixed top-4 right-4 left-4 z-toast flex justify-end sm:left-auto"
63
65
  >
64
66
  <div
65
- class="pointer-events-auto {cardClasses} {typeClasses[type]} {className}"
67
+ class={cn("pointer-events-auto", cardClasses, typeClasses[type], className)}
66
68
  role="alert"
67
69
  {...restProps}
68
70
  >
@@ -71,7 +73,7 @@
71
73
  </div>
72
74
  {:else}
73
75
  <div
74
- class="relative z-toast mx-auto {cardClasses} {typeClasses[type]} {className}"
76
+ class={cn("relative z-toast mx-auto", cardClasses, typeClasses[type], className)}
75
77
  role="alert"
76
78
  {...restProps}
77
79
  >