zabi-components 5.0.22 → 7.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (207) hide show
  1. package/README.md +139 -1478
  2. package/THEME.md +473 -0
  3. package/THEMING.md +248 -0
  4. package/dist/atoms/ActionPanel.svelte +116 -0
  5. package/dist/atoms/ActionPanel.svelte.d.ts +19 -0
  6. package/dist/atoms/Badge.svelte +1 -1
  7. package/dist/atoms/Badge.svelte.d.ts +1 -1
  8. package/dist/atoms/Button.svelte +35 -16
  9. package/dist/atoms/Button.svelte.d.ts +3 -1
  10. package/dist/atoms/Card.svelte +49 -67
  11. package/dist/atoms/Card.svelte.d.ts +4 -4
  12. package/dist/atoms/CardHeader.svelte +15 -2
  13. package/dist/atoms/CardHeader.svelte.d.ts +5 -0
  14. package/dist/atoms/Checkbox.svelte +50 -76
  15. package/dist/atoms/Checkbox.svelte.d.ts +7 -2
  16. package/dist/atoms/CodeBlock.svelte +20 -34
  17. package/dist/atoms/CodeBlock.svelte.d.ts +1 -0
  18. package/dist/atoms/ColorPicker.svelte +3 -7
  19. package/dist/atoms/Container.svelte +41 -0
  20. package/dist/atoms/Container.svelte.d.ts +12 -0
  21. package/dist/atoms/Divider.svelte +68 -0
  22. package/dist/atoms/Divider.svelte.d.ts +13 -0
  23. package/dist/atoms/FeatureCard.svelte +35 -50
  24. package/dist/atoms/FeatureCard.svelte.d.ts +9 -4
  25. package/dist/atoms/IconButton.svelte +33 -24
  26. package/dist/atoms/IconButton.svelte.d.ts +4 -8
  27. package/dist/atoms/Input.svelte +58 -26
  28. package/dist/atoms/Input.svelte.d.ts +6 -1
  29. package/dist/atoms/List.svelte +2 -7
  30. package/dist/atoms/List.svelte.d.ts +1 -6
  31. package/dist/atoms/ListItem.svelte +54 -65
  32. package/dist/atoms/ListItem.svelte.d.ts +7 -13
  33. package/dist/atoms/ListItemLeading.svelte +19 -0
  34. package/dist/atoms/ListItemLeading.svelte.d.ts +9 -0
  35. package/dist/atoms/Progress.svelte +2 -2
  36. package/dist/atoms/Radio.svelte +51 -0
  37. package/dist/atoms/Radio.svelte.d.ts +14 -0
  38. package/dist/atoms/Select.svelte +28 -33
  39. package/dist/atoms/Select.svelte.d.ts +0 -10
  40. package/dist/atoms/SelectionControl.svelte +98 -0
  41. package/dist/atoms/SelectionControl.svelte.d.ts +26 -0
  42. package/dist/atoms/Skeleton.svelte +39 -15
  43. package/dist/atoms/Skeleton.svelte.d.ts +7 -3
  44. package/dist/atoms/Table.svelte +27 -0
  45. package/dist/atoms/Table.svelte.d.ts +10 -0
  46. package/dist/atoms/Text.svelte +46 -0
  47. package/dist/atoms/Text.svelte.d.ts +13 -0
  48. package/dist/atoms/Textarea.svelte +74 -53
  49. package/dist/atoms/Textarea.svelte.d.ts +6 -2
  50. package/dist/atoms/ThemeToggle.svelte +35 -24
  51. package/dist/atoms/ThemeToggle.svelte.d.ts +1 -0
  52. package/dist/atoms/Toast.svelte +55 -31
  53. package/dist/atoms/Toast.svelte.d.ts +4 -1
  54. package/dist/atoms/Toggle.svelte +37 -22
  55. package/dist/atoms/Toggle.svelte.d.ts +2 -1
  56. package/dist/atoms/Tooltip.svelte +90 -11
  57. package/dist/atoms/index.d.ts +6 -0
  58. package/dist/atoms/index.js +6 -0
  59. package/dist/atoms/selection-control.styles.d.ts +25 -0
  60. package/dist/atoms/selection-control.styles.js +30 -0
  61. package/dist/components/atoms/index.d.ts +6 -0
  62. package/dist/components/atoms/index.d.ts.map +1 -1
  63. package/dist/components/atoms/selection-control.styles.d.ts +26 -0
  64. package/dist/components/atoms/selection-control.styles.d.ts.map +1 -0
  65. package/dist/components/index.d.ts +42 -7
  66. package/dist/components/index.d.ts.map +1 -1
  67. package/dist/components/molecules/index.d.ts +10 -0
  68. package/dist/components/molecules/index.d.ts.map +1 -1
  69. package/dist/components/molecules/navigation-menu-context.d.ts +11 -0
  70. package/dist/components/molecules/navigation-menu-context.d.ts.map +1 -1
  71. package/dist/components/molecules/toast-store.d.ts +39 -0
  72. package/dist/components/molecules/toast-store.d.ts.map +1 -0
  73. package/dist/components/organisms/index.d.ts +3 -3
  74. package/dist/components/organisms/index.d.ts.map +1 -1
  75. package/dist/components/types/page.types.d.ts +69 -0
  76. package/dist/components/types/page.types.d.ts.map +1 -0
  77. package/dist/components/types/page.types.js +1 -0
  78. package/dist/components/types/variants.d.ts +64 -0
  79. package/dist/components/types/variants.d.ts.map +1 -0
  80. package/dist/components/types/variants.js +44 -0
  81. package/dist/components/util/fixed-sidebar-flyout.d.ts +23 -0
  82. package/dist/components/util/fixed-sidebar-flyout.d.ts.map +1 -0
  83. package/dist/components/util/focus-utils.d.ts +12 -0
  84. package/dist/components/util/focus-utils.d.ts.map +1 -0
  85. package/dist/components/util/ssr-safe.d.ts +17 -0
  86. package/dist/components/util/ssr-safe.d.ts.map +1 -0
  87. package/dist/index.d.ts +42 -7
  88. package/dist/index.js +45 -10
  89. package/dist/lib/layout-width-tokens.d.ts +17 -0
  90. package/dist/lib/layout-width-tokens.d.ts.map +1 -0
  91. package/dist/lib/showcase/component-docs/Button.d.ts +3 -0
  92. package/dist/lib/showcase/component-docs/Button.d.ts.map +1 -0
  93. package/dist/lib/showcase/component-docs/Input.d.ts +3 -0
  94. package/dist/lib/showcase/component-docs/Input.d.ts.map +1 -0
  95. package/dist/lib/showcase/component-docs/List.d.ts +3 -0
  96. package/dist/lib/showcase/component-docs/List.d.ts.map +1 -0
  97. package/dist/lib/showcase/component-docs/Modal.d.ts +3 -0
  98. package/dist/lib/showcase/component-docs/Modal.d.ts.map +1 -0
  99. package/dist/lib/showcase/component-docs/Radio.d.ts +3 -0
  100. package/dist/lib/showcase/component-docs/Radio.d.ts.map +1 -0
  101. package/dist/lib/showcase/component-docs/SidebarNavigation.d.ts +3 -0
  102. package/dist/lib/showcase/component-docs/SidebarNavigation.d.ts.map +1 -0
  103. package/dist/lib/showcase/component-docs/Skeleton.d.ts +3 -0
  104. package/dist/lib/showcase/component-docs/Skeleton.d.ts.map +1 -0
  105. package/dist/lib/showcase/component-docs/_shared.d.ts +12 -0
  106. package/dist/lib/showcase/component-docs/_shared.d.ts.map +1 -0
  107. package/dist/lib/showcase/component-docs/index.d.ts +4 -0
  108. package/dist/lib/showcase/component-docs/index.d.ts.map +1 -0
  109. package/dist/lib/showcase/components-catalog.d.ts +3 -0
  110. package/dist/lib/showcase/components-catalog.d.ts.map +1 -0
  111. package/dist/lib/showcase/components-showcase-constants.d.ts +89 -0
  112. package/dist/lib/showcase/components-showcase-constants.d.ts.map +1 -0
  113. package/dist/lib/showcase/docs-sidebar-helpers.d.ts +15 -0
  114. package/dist/lib/showcase/docs-sidebar-helpers.d.ts.map +1 -0
  115. package/dist/molecules/Alert.svelte +50 -23
  116. package/dist/molecules/Alert.svelte.d.ts +1 -1
  117. package/dist/molecules/ComponentDemo.svelte +54 -51
  118. package/dist/molecules/ContactForm.svelte +70 -65
  119. package/dist/molecules/Dropdown.svelte +99 -108
  120. package/dist/molecules/Dropdown.svelte.d.ts +12 -6
  121. package/dist/molecules/EmptyState.svelte +44 -0
  122. package/dist/molecules/EmptyState.svelte.d.ts +11 -0
  123. package/dist/molecules/FormField.svelte +89 -0
  124. package/dist/molecules/FormField.svelte.d.ts +23 -0
  125. package/dist/molecules/Header.svelte +41 -0
  126. package/dist/molecules/Header.svelte.d.ts +10 -0
  127. package/dist/molecules/ImageUpload.svelte +1 -1
  128. package/dist/molecules/Modal.svelte +39 -26
  129. package/dist/molecules/Modal.svelte.d.ts +4 -2
  130. package/dist/molecules/NavigationMenu.svelte +16 -0
  131. package/dist/molecules/NavigationMenu.svelte.d.ts +2 -0
  132. package/dist/molecules/NavigationMenuContent.svelte +9 -1
  133. package/dist/molecules/NavigationMenuLink.svelte +1 -1
  134. package/dist/molecules/NavigationMenuTrigger.svelte +9 -4
  135. package/dist/molecules/Page.svelte +16 -0
  136. package/dist/molecules/Page.svelte.d.ts +8 -0
  137. package/dist/molecules/PropsTable.svelte +61 -0
  138. package/dist/molecules/PropsTable.svelte.d.ts +8 -0
  139. package/dist/molecules/RadioGroup.svelte +197 -0
  140. package/dist/molecules/RadioGroup.svelte.d.ts +20 -0
  141. package/dist/molecules/Section.svelte +14 -23
  142. package/dist/molecules/Section.svelte.d.ts +2 -11
  143. package/dist/molecules/SidebarBrandHeader.svelte +68 -0
  144. package/dist/molecules/SidebarBrandHeader.svelte.d.ts +10 -0
  145. package/dist/molecules/SidebarFooter.svelte +119 -0
  146. package/dist/molecules/SidebarFooter.svelte.d.ts +23 -0
  147. package/dist/molecules/SidebarNavSection.svelte +49 -0
  148. package/dist/molecules/SidebarNavSection.svelte.d.ts +14 -0
  149. package/dist/molecules/SlideUp.svelte +21 -16
  150. package/dist/molecules/SlideUp.svelte.d.ts +1 -1
  151. package/dist/molecules/Tabs.svelte +6 -11
  152. package/dist/molecules/Toaster.svelte +21 -0
  153. package/dist/molecules/Toaster.svelte.d.ts +6 -0
  154. package/dist/molecules/ToasterToast.svelte +217 -0
  155. package/dist/molecules/ToasterToast.svelte.d.ts +7 -0
  156. package/dist/molecules/index.d.ts +10 -0
  157. package/dist/molecules/index.js +10 -0
  158. package/dist/molecules/navigation-menu-context.d.ts +11 -0
  159. package/dist/molecules/navigation-menu-context.js +18 -0
  160. package/dist/molecules/toast-store.d.ts +38 -0
  161. package/dist/molecules/toast-store.js +34 -0
  162. package/dist/organisms/SidebarAccountPanel.svelte +118 -0
  163. package/dist/organisms/SidebarAccountPanel.svelte.d.ts +20 -0
  164. package/dist/organisms/SidebarNavigation.svelte +286 -212
  165. package/dist/organisms/SidebarNavigation.svelte.d.ts +22 -1
  166. package/dist/organisms/{SidebarProjectPanel.svelte → SidebarPanel.svelte} +67 -34
  167. package/dist/organisms/{SidebarProjectPanel.svelte.d.ts → SidebarPanel.svelte.d.ts} +9 -6
  168. package/dist/organisms/TopNavbar.svelte +214 -0
  169. package/dist/organisms/TopNavbar.svelte.d.ts +35 -0
  170. package/dist/organisms/index.d.ts +3 -3
  171. package/dist/organisms/index.js +3 -3
  172. package/dist/routes/lib/focus-utils.d.ts +7 -18
  173. package/dist/routes/lib/focus-utils.ts +32 -34
  174. package/dist/routes/lib/ssr-safe.ts +5 -4
  175. package/dist/routes/lib/variant-utils.ts +48 -39
  176. package/dist/types/components.d.ts +187 -0
  177. package/dist/types/components.d.ts.map +1 -0
  178. package/dist/types/events.d.ts +200 -0
  179. package/dist/types/events.d.ts.map +1 -0
  180. package/dist/types/index.d.ts +171 -0
  181. package/dist/types/index.d.ts.map +1 -0
  182. package/dist/types/page.types.d.ts +3 -0
  183. package/dist/types/page.types.d.ts.map +1 -0
  184. package/dist/types/page.types.js +1 -0
  185. package/dist/types/page.types.ts +79 -0
  186. package/dist/types/variants.d.ts +3 -0
  187. package/dist/types/variants.d.ts.map +1 -0
  188. package/dist/types/variants.js +1 -0
  189. package/dist/types/variants.ts +75 -0
  190. package/dist/util/fixed-sidebar-flyout.d.ts +22 -0
  191. package/dist/util/fixed-sidebar-flyout.js +148 -0
  192. package/dist/util/focus-utils.d.ts +11 -0
  193. package/dist/util/focus-utils.js +94 -0
  194. package/dist/util/ssr-safe.d.ts +16 -0
  195. package/dist/util/ssr-safe.js +48 -0
  196. package/dist/zabi-components-colors.css +360 -324
  197. package/dist/zabi-components-theme-dark-only.css +164 -151
  198. package/dist/zabi-components-theme-dark.css +164 -151
  199. package/dist/zabi-components-theme-only.css +253 -230
  200. package/dist/zabi-components-theme.css +253 -230
  201. package/dist/zabi-components.css +1262 -241
  202. package/docs/theme-imports.md +112 -0
  203. package/package.json +32 -10
  204. package/dist/organisms/Navbar.svelte +0 -92
  205. package/dist/organisms/Navbar.svelte.d.ts +0 -14
  206. package/dist/organisms/Navigation.svelte +0 -119
  207. package/dist/organisms/Navigation.svelte.d.ts +0 -21
@@ -1,15 +1,19 @@
1
1
  <script lang="ts">
2
- import { CheckCircle, AlertTriangle, AlertCircle } from "@lucide/svelte";
3
- import type { SemanticVariant, SizeVariant } from "../../types/variants.js";
4
- import { generateId } from "../../routes/lib/ssr-safe.js";
2
+ import { CheckCircle, AlertTriangle, AlertCircle } from '@lucide/svelte';
3
+ import type { SemanticVariant, SizeVariant } from '../types/variants.js';
4
+ import { generateId } from "../util/ssr-safe.js";
5
5
 
6
6
  interface Props {
7
+ id?: string;
7
8
  value?: string;
8
9
  name?: string;
10
+ class?: string;
9
11
  label?: string;
12
+ hideLabel?: boolean;
10
13
  placeholder?: string;
11
14
  required?: boolean;
12
15
  disabled?: boolean;
16
+ loading?: boolean;
13
17
  rows?: number;
14
18
  size?: SizeVariant;
15
19
  variant?: SemanticVariant;
@@ -18,95 +22,112 @@
18
22
  }
19
23
 
20
24
  let {
21
- value = "",
22
- name = "",
23
- label = "",
24
- placeholder = "",
25
+ id: idProp,
26
+ value = $bindable(''),
27
+ name = '',
28
+ class: className = '',
29
+ label = '',
30
+ hideLabel = false,
31
+ placeholder = '',
25
32
  required = false,
26
33
  disabled = false,
34
+ loading = false,
27
35
  rows = 4,
28
- variant = "default",
29
- message = "",
36
+ size = 'md',
37
+ variant = 'default',
38
+ message = '',
30
39
  oninput,
31
40
  ...restProps
32
41
  }: Props = $props();
33
42
 
34
- const textareaId = generateId("textarea");
43
+ const fallbackId = generateId('textarea');
44
+ const textareaId = $derived(idProp ?? fallbackId);
45
+ const isDisabled = $derived(disabled || loading);
35
46
 
36
47
  const variantClass = $derived(() => {
37
- return variant === "success"
38
- ? "border-success focus:border-success focus:ring-success"
39
- : variant === "warning"
40
- ? "border-warning focus:border-warning focus:ring-warning"
41
- : variant === "error"
42
- ? "border-error focus:border-error focus:ring-error"
43
- : "border-0 focus:ring-2 focus:ring-brand-500"; // default - no border
48
+ return variant === 'success'
49
+ ? 'border-success focus-visible:border-success'
50
+ : variant === 'warning'
51
+ ? 'border-warning focus-visible:border-warning'
52
+ : variant === 'error'
53
+ ? 'border-error focus-visible:border-error'
54
+ : 'border-input-border';
44
55
  });
45
56
 
46
57
  const textareaClasses = $derived(() => {
47
58
  const baseClasses =
48
- "w-full bg-input hover:bg-input-hover focus:bg-input-focus disabled:bg-input-disabled rounded-lg transition-all duration-200 placeholder:text-description text-body focus:outline-none focus:ring-offset-0 disabled:opacity-50 disabled:cursor-not-allowed resize-y px-4 py-2.5 text-base leading-6";
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';
49
60
 
50
- return `${baseClasses} ${variantClass()}`.trim();
61
+ return `${baseClasses} ${variantClass()} ${className}`.trim();
51
62
  });
52
63
 
53
64
  const labelClasses = $derived(
54
- () => "block text-sm font-medium text-label mb-1",
65
+ () => 'mb-1 block text-sm font-medium text-label',
55
66
  );
56
67
 
57
68
  const messageClasses = $derived(() => {
58
- if (variant === "error") {
59
- return "text-error text-sm mt-1 flex items-center gap-1.5";
60
- } else if (variant === "success") {
61
- return "text-success text-sm mt-1 flex items-center gap-1.5";
62
- } else if (variant === "warning") {
63
- return "text-warning text-sm mt-1 flex items-center gap-1.5";
69
+ if (variant === 'error') {
70
+ return 'mt-1 flex items-center gap-1.5 text-sm text-error';
71
+ } else if (variant === 'success') {
72
+ return 'mt-1 flex items-center gap-1.5 text-sm text-success';
73
+ } else if (variant === 'warning') {
74
+ return 'mt-1 flex items-center gap-1.5 text-sm text-warning';
64
75
  }
65
- return "text-description text-sm mt-1 flex items-center gap-1.5";
76
+ return 'mt-1 flex items-center gap-1.5 text-sm text-description';
66
77
  });
67
78
 
68
79
  const getIcon = $derived(() => {
69
- if (variant === "error") return AlertCircle;
70
- if (variant === "success") return CheckCircle;
71
- if (variant === "warning") return AlertTriangle;
80
+ if (variant === 'error') return AlertCircle;
81
+ if (variant === 'success') return CheckCircle;
82
+ if (variant === 'warning') return AlertTriangle;
72
83
  return null;
73
84
  });
74
85
 
75
86
  function handleInput(event: Event) {
76
87
  const target = event.target as HTMLTextAreaElement;
77
88
  value = target.value;
78
-
79
- if (oninput) {
80
- oninput(event);
81
- }
89
+ oninput?.(event);
82
90
  }
83
91
  </script>
84
92
 
85
93
  <div>
86
- {#if label}
94
+ {#if label && !hideLabel}
87
95
  <label for={textareaId} class={labelClasses()}>{label}</label>
88
96
  {/if}
89
- <textarea
90
- id={textareaId}
91
- {name}
92
- {value}
93
- {placeholder}
94
- {required}
95
- {disabled}
96
- {rows}
97
- class={textareaClasses()}
98
- oninput={handleInput}
99
- aria-invalid={variant === "error" ? "true" : undefined}
100
- aria-required={required ? "true" : undefined}
101
- aria-describedby={message ? `${textareaId}-message` : undefined}
102
- {...restProps}
103
- ></textarea>
104
- {#if message && variant !== "default"}
97
+ <div class="relative">
98
+ <textarea
99
+ id={textareaId}
100
+ {name}
101
+ bind:value
102
+ {placeholder}
103
+ {required}
104
+ disabled={isDisabled}
105
+ {rows}
106
+ class={textareaClasses()}
107
+ oninput={handleInput}
108
+ aria-invalid={variant === 'error' ? 'true' : undefined}
109
+ aria-required={required ? 'true' : undefined}
110
+ aria-busy={loading ? 'true' : undefined}
111
+ aria-describedby={message ? `${textareaId}-message` : undefined}
112
+ {...restProps}
113
+ ></textarea>
114
+ {#if loading}
115
+ <span
116
+ class="pointer-events-none absolute top-3 right-3 flex items-center text-description"
117
+ aria-hidden="true"
118
+ >
119
+ <span
120
+ class="inline-block size-5 shrink-0 animate-spin rounded-full border-2 border-current border-t-transparent opacity-70"
121
+ ></span>
122
+ </span>
123
+ {/if}
124
+ </div>
125
+ {#if message && variant !== 'default'}
105
126
  <p
106
127
  id={`${textareaId}-message`}
107
128
  class={messageClasses()}
108
- role={variant === "error" ? "alert" : "status"}
109
- aria-live={variant === "error" ? "assertive" : "polite"}
129
+ role={variant === 'error' ? 'alert' : 'status'}
130
+ aria-live={variant === 'error' ? 'assertive' : 'polite'}
110
131
  >
111
132
  {#if getIcon()}
112
133
  {@const Icon = getIcon()}
@@ -1,17 +1,21 @@
1
- import type { SemanticVariant, SizeVariant } from "../../types/variants.js";
1
+ import type { SemanticVariant, SizeVariant } from '../types/variants.js';
2
2
  interface Props {
3
+ id?: string;
3
4
  value?: string;
4
5
  name?: string;
6
+ class?: string;
5
7
  label?: string;
8
+ hideLabel?: boolean;
6
9
  placeholder?: string;
7
10
  required?: boolean;
8
11
  disabled?: boolean;
12
+ loading?: boolean;
9
13
  rows?: number;
10
14
  size?: SizeVariant;
11
15
  variant?: SemanticVariant;
12
16
  message?: string;
13
17
  oninput?: (event: Event) => void;
14
18
  }
15
- declare const Textarea: import("svelte").Component<Props, {}, "">;
19
+ declare const Textarea: import("svelte").Component<Props, {}, "value">;
16
20
  type Textarea = ReturnType<typeof Textarea>;
17
21
  export default Textarea;
@@ -1,6 +1,7 @@
1
1
  <script lang="ts">
2
2
  import { onMount } from "svelte";
3
3
  import { Sun, Moon } from "@lucide/svelte";
4
+ import { FOCUS_BRAND_CLASS } from "../util/focus-utils.js";
4
5
 
5
6
  function safeLocalStorage(): Storage | undefined {
6
7
  return typeof window !== "undefined" ? localStorage : undefined;
@@ -13,13 +14,15 @@
13
14
  interface Props {
14
15
  size?: "sm" | "md" | "lg";
15
16
  variant?: "default" | "ghost" | "outline";
17
+ disabled?: boolean;
16
18
  onclick?: (event: Event) => void;
17
19
  }
18
20
 
19
- let {
21
+ let {
20
22
  size = "md",
21
23
  variant = "default",
22
- ...restProps
24
+ disabled = false,
25
+ ...restProps
23
26
  }: Props = $props();
24
27
 
25
28
  let isDark = $state(false);
@@ -27,25 +30,27 @@
27
30
 
28
31
  onMount(() => {
29
32
  mounted = true;
30
- const storage = safeLocalStorage();
31
- if (storage) {
32
- const savedTheme = storage.getItem("theme");
33
- let prefersDark = false;
33
+ const doc = safeDocument();
34
+ if (doc) {
35
+ isDark = doc.documentElement.classList.contains("dark");
36
+ }
34
37
 
35
- if (window.matchMedia) {
36
- try {
37
- prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
38
- } catch (e) {
39
- prefersDark = false;
40
- }
41
- }
38
+ if (!window.matchMedia) return;
42
39
 
43
- isDark = savedTheme === "dark" || (!savedTheme && prefersDark);
40
+ const media = window.matchMedia("(prefers-color-scheme: dark)");
41
+ const handleSystemChange = (event: MediaQueryListEvent) => {
42
+ const storage = safeLocalStorage();
43
+ if (storage && storage.getItem("theme")) return;
44
+ isDark = event.matches;
44
45
  updateTheme();
45
- }
46
+ };
47
+
48
+ media.addEventListener("change", handleSystemChange);
49
+ return () => media.removeEventListener("change", handleSystemChange);
46
50
  });
47
51
 
48
52
  function toggleTheme(event: Event) {
53
+ if (disabled) return;
49
54
  isDark = !isDark;
50
55
  updateTheme();
51
56
  const storage = safeLocalStorage();
@@ -63,8 +68,10 @@
63
68
  if (mounted && doc) {
64
69
  if (isDark) {
65
70
  doc.documentElement.classList.add("dark");
71
+ doc.documentElement.style.colorScheme = "dark";
66
72
  } else {
67
73
  doc.documentElement.classList.remove("dark");
74
+ doc.documentElement.style.colorScheme = "light";
68
75
  }
69
76
  }
70
77
  }
@@ -90,11 +97,11 @@
90
97
 
91
98
  const variantClass = $derived(() => {
92
99
  if (variant === "ghost") {
93
- return "bg-transparent hover:bg-base-100 border-0";
100
+ return "bg-transparent hover:bg-base-100 active:bg-base-200 border-0";
94
101
  } else if (variant === "outline") {
95
- return "bg-base-50 hover:bg-base-100 border border-border";
102
+ return "bg-base-50 hover:bg-base-100 active:bg-base-200 border border-border";
96
103
  } else {
97
- return "bg-base-50 hover:bg-base-100 border-0";
104
+ return "bg-base-50 hover:bg-base-100 active:bg-base-200 border-0";
98
105
  }
99
106
  });
100
107
 
@@ -111,11 +118,12 @@
111
118
  cursor-pointer
112
119
  transition-colors
113
120
  duration-200
114
- focus:outline-none
115
- focus:ring-2
116
- focus:ring-brand-500
117
- focus:ring-offset-2
118
- focus:ring-offset-base-50
121
+ active:scale-[0.96]
122
+ disabled:opacity-50
123
+ disabled:cursor-not-allowed
124
+ disabled:active:scale-100
125
+ disabled:hover:bg-base-50
126
+ ${FOCUS_BRAND_CLASS}
119
127
  `.trim().replace(/\s+/g, " ");
120
128
  });
121
129
  </script>
@@ -125,7 +133,9 @@
125
133
  onclick={toggleTheme}
126
134
  class={buttonClasses()}
127
135
  aria-label={isDark ? "Switch to light mode" : "Switch to dark mode"}
136
+ aria-pressed={isDark}
128
137
  type="button"
138
+ {disabled}
129
139
  {...restProps}
130
140
  >
131
141
  {#if isDark}
@@ -136,9 +146,10 @@
136
146
  </button>
137
147
  {:else}
138
148
  <button
139
- class="w-10 h-10 bg-base-50 rounded-lg flex items-center justify-center text-label cursor-pointer"
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"
140
150
  aria-label="Theme toggle"
141
151
  type="button"
152
+ {disabled}
142
153
  {...restProps}
143
154
  >
144
155
  <Sun size={20} class="text-label" />
@@ -1,6 +1,7 @@
1
1
  interface Props {
2
2
  size?: "sm" | "md" | "lg";
3
3
  variant?: "default" | "ghost" | "outline";
4
+ disabled?: boolean;
4
5
  onclick?: (event: Event) => void;
5
6
  }
6
7
  declare const ThemeToggle: import("svelte").Component<Props, {}, "">;
@@ -1,57 +1,81 @@
1
1
  <script lang="ts">
2
2
  interface Props {
3
3
  message?: string;
4
- type?: "success" | "error" | "warning" | "info";
4
+ type?: 'success' | 'error' | 'warning' | 'info';
5
5
  closable?: boolean;
6
6
  onclick?: (event: Event) => void;
7
+ class?: string;
8
+ /** `viewport`: fixed corner; `inline`: block in flow (e.g. demos). */
9
+ layout?: 'viewport' | 'inline';
7
10
  }
8
11
 
9
12
  let {
10
- message = "",
11
- type = "info",
13
+ message = '',
14
+ type = 'info',
12
15
  closable = true,
13
16
  onclick,
17
+ class: className = '',
18
+ layout = 'viewport',
14
19
  ...restProps
15
20
  }: Props = $props();
16
21
 
17
22
  let isVisible = $state(true);
18
23
 
19
- const typeClasses = {
20
- success: "bg-green-100 border-green-300 text-success",
21
- error: "bg-red-100 border-red-300 text-error",
22
- warning: "bg-yellow-100 border-yellow-300 text-warning",
23
- info: "bg-iris-100 border-iris-300 text-body",
24
+ 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',
24
29
  };
25
30
 
31
+ 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';
33
+
26
34
  function closeToast(event: Event) {
27
35
  isVisible = false;
28
- if (onclick) {
29
- onclick(event);
30
- }
36
+ onclick?.(event);
31
37
  }
32
38
  </script>
33
39
 
40
+ {#snippet toastBody()}
41
+ <div class="flex min-w-0 items-start gap-3">
42
+ <div class="min-w-0 max-w-full flex-1 basis-0">
43
+ <p class="text-sm break-words [overflow-wrap:anywhere]">{message}</p>
44
+ </div>
45
+
46
+ {#if closable}
47
+ <button
48
+ type="button"
49
+ class="focus-ring shrink-0 cursor-pointer rounded-md text-description hover:text-headline focus:outline-none"
50
+ onclick={closeToast}
51
+ aria-label="Close notification"
52
+ >
53
+ ×
54
+ </button>
55
+ {/if}
56
+ </div>
57
+ {/snippet}
58
+
34
59
  {#if isVisible}
35
- <div
36
- class="fixed top-4 right-4 max-w-sm w-full bg-white border rounded-lg shadow-lg p-4 z-50"
37
- role="alert"
38
- {...restProps}
39
- >
40
- <div class="flex items-start">
41
- <div class="flex-1">
42
- <p class="text-sm {typeClasses[type]}">{message}</p>
60
+ {#if layout === 'viewport'}
61
+ <div
62
+ class="pointer-events-none fixed top-4 right-4 left-4 z-toast flex justify-end sm:left-auto"
63
+ >
64
+ <div
65
+ class="pointer-events-auto {cardClasses} {typeClasses[type]} {className}"
66
+ role="alert"
67
+ {...restProps}
68
+ >
69
+ {@render toastBody()}
43
70
  </div>
44
-
45
- {#if closable}
46
- <button
47
- type="button"
48
- class="ml-3 text-base-400 hover:text-base-600"
49
- onclick={closeToast}
50
- aria-label="Close notification"
51
- >
52
- ×
53
- </button>
54
- {/if}
55
71
  </div>
56
- </div>
72
+ {:else}
73
+ <div
74
+ class="relative z-toast mx-auto {cardClasses} {typeClasses[type]} {className}"
75
+ role="alert"
76
+ {...restProps}
77
+ >
78
+ {@render toastBody()}
79
+ </div>
80
+ {/if}
57
81
  {/if}
@@ -1,8 +1,11 @@
1
1
  interface Props {
2
2
  message?: string;
3
- type?: "success" | "error" | "warning" | "info";
3
+ type?: 'success' | 'error' | 'warning' | 'info';
4
4
  closable?: boolean;
5
5
  onclick?: (event: Event) => void;
6
+ class?: string;
7
+ /** `viewport`: fixed corner; `inline`: block in flow (e.g. demos). */
8
+ layout?: 'viewport' | 'inline';
6
9
  }
7
10
  declare const Toast: import("svelte").Component<Props, {}, "">;
8
11
  type Toast = ReturnType<typeof Toast>;
@@ -1,17 +1,19 @@
1
1
  <script lang="ts">
2
- import { generateId } from "../../routes/lib/ssr-safe.js";
2
+ import { generateId } from "../util/ssr-safe.js";
3
3
 
4
4
  interface Props {
5
5
  checked?: boolean;
6
6
  disabled?: boolean;
7
+ loading?: boolean;
7
8
  label?: string;
8
9
  onclick?: (event: MouseEvent) => void;
9
10
  onchange?: (event: { checked: boolean }) => void;
10
11
  }
11
12
 
12
13
  let {
13
- checked = false,
14
+ checked = $bindable(false),
14
15
  disabled = false,
16
+ loading = false,
15
17
  label = "",
16
18
  onclick,
17
19
  onchange,
@@ -19,38 +21,40 @@
19
21
  }: Props = $props();
20
22
 
21
23
  const toggleId = generateId("toggle");
24
+ const isDisabled = $derived(disabled || loading);
22
25
 
23
26
  function handleClick(event: MouseEvent) {
24
- if (disabled) return;
27
+ if (isDisabled) return;
25
28
  checked = !checked;
26
29
 
27
30
  if (onclick) onclick(event);
28
31
  if (onchange) onchange({ checked });
29
32
  }
30
33
 
31
- const toggleButtonClasses = $derived(() =>
32
- [
33
- "relative inline-flex w-10 h-6 flex-shrink-0 cursor-pointer rounded-full border-0 transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-brand-500 focus:ring-offset-2",
34
- checked ? "bg-action-primary" : "bg-base-400",
35
- disabled && "opacity-50 cursor-not-allowed",
36
- ]
37
- .filter(Boolean)
38
- .join(" "),
39
- );
34
+ const toggleButtonClasses = $derived(() => {
35
+ const base =
36
+ "focus-ring relative inline-flex w-10 h-6 flex-shrink-0 rounded-full border-0 transition-colors duration-200 ease-in-out focus:outline-none focus-visible:outline-none";
37
+ const colorClass = checked
38
+ ? "bg-action-primary hover:bg-action-primary-hover active:bg-action-primary-active"
39
+ : "bg-base-400 hover:bg-base-500 active:bg-base-500";
40
+ const stateClass = isDisabled
41
+ ? "opacity-50 cursor-not-allowed"
42
+ : "cursor-pointer";
43
+ return `${base} ${colorClass} ${stateClass}`;
44
+ });
40
45
 
41
46
  const toggleThumbClasses = $derived(() => {
42
- const baseClasses =
43
- "pointer-events-none absolute top-0.5 left-0.5 w-5 h-5 rounded-full bg-card transition-transform duration-200 ease-in-out";
47
+ const base =
48
+ "pointer-events-none absolute top-0.5 left-0.5 w-5 h-5 rounded-full bg-card shadow transition-transform duration-200 ease-in-out flex items-center justify-center";
44
49
  const positionClasses = checked ? "translate-x-4" : "translate-x-0";
45
-
46
- return `${baseClasses} ${positionClasses}`.trim();
50
+ return `${base} ${positionClasses}`;
47
51
  });
48
52
 
49
53
  function handleKeydown(event: KeyboardEvent) {
50
- if (disabled) return;
54
+ if (isDisabled) return;
51
55
  if (event.key === " " || event.key === "Enter") {
52
56
  event.preventDefault();
53
- handleClick(event as any);
57
+ handleClick(event as unknown as MouseEvent);
54
58
  }
55
59
  }
56
60
  </script>
@@ -61,19 +65,30 @@
61
65
  role="switch"
62
66
  id={toggleId}
63
67
  aria-checked={checked}
64
- aria-label={label || "Toggle"}
65
- {disabled}
68
+ aria-label={label ? undefined : "Toggle"}
69
+ aria-busy={loading ? "true" : undefined}
70
+ disabled={isDisabled}
66
71
  onclick={handleClick}
67
72
  onkeydown={handleKeydown}
68
73
  class={toggleButtonClasses()}
74
+ {...restProps}
69
75
  >
70
- <span class={toggleThumbClasses()}></span>
76
+ <span class={toggleThumbClasses()}>
77
+ {#if loading}
78
+ <span
79
+ class="inline-block size-3 shrink-0 animate-spin rounded-full border-2 border-brand-500 border-t-transparent"
80
+ aria-hidden="true"
81
+ ></span>
82
+ {/if}
83
+ </span>
71
84
  </button>
72
85
 
73
86
  {#if label}
74
87
  <label
75
88
  for={toggleId}
76
- class="text-sm font-medium text-label cursor-pointer"
89
+ class="text-sm font-medium text-label {isDisabled
90
+ ? 'cursor-not-allowed opacity-50'
91
+ : 'cursor-pointer'}"
77
92
  >
78
93
  {label}
79
94
  </label>
@@ -1,12 +1,13 @@
1
1
  interface Props {
2
2
  checked?: boolean;
3
3
  disabled?: boolean;
4
+ loading?: boolean;
4
5
  label?: string;
5
6
  onclick?: (event: MouseEvent) => void;
6
7
  onchange?: (event: {
7
8
  checked: boolean;
8
9
  }) => void;
9
10
  }
10
- declare const Toggle: import("svelte").Component<Props, {}, "">;
11
+ declare const Toggle: import("svelte").Component<Props, {}, "checked">;
11
12
  type Toggle = ReturnType<typeof Toggle>;
12
13
  export default Toggle;