zabi-components 6.0.0 → 7.0.1

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 (200) hide show
  1. package/README.md +137 -1473
  2. package/dist/atoms/ActionPanel.svelte +116 -0
  3. package/dist/atoms/ActionPanel.svelte.d.ts +19 -0
  4. package/dist/atoms/Badge.svelte +1 -1
  5. package/dist/atoms/Badge.svelte.d.ts +1 -1
  6. package/dist/atoms/Button.svelte +38 -21
  7. package/dist/atoms/Button.svelte.d.ts +6 -6
  8. package/dist/atoms/Card.svelte +12 -3
  9. package/dist/atoms/Card.svelte.d.ts +2 -2
  10. package/dist/atoms/CardHeader.svelte +8 -4
  11. package/dist/atoms/CardHeader.svelte.d.ts +4 -2
  12. package/dist/atoms/Checkbox.svelte +50 -76
  13. package/dist/atoms/Checkbox.svelte.d.ts +7 -2
  14. package/dist/atoms/CodeBlock.svelte +20 -34
  15. package/dist/atoms/CodeBlock.svelte.d.ts +1 -0
  16. package/dist/atoms/ColorPicker.svelte +2 -6
  17. package/dist/atoms/Container.svelte +41 -0
  18. package/dist/atoms/Container.svelte.d.ts +12 -0
  19. package/dist/atoms/Divider.svelte +68 -0
  20. package/dist/atoms/Divider.svelte.d.ts +13 -0
  21. package/dist/atoms/FeatureCard.svelte +35 -50
  22. package/dist/atoms/FeatureCard.svelte.d.ts +9 -4
  23. package/dist/atoms/IconButton.svelte +36 -29
  24. package/dist/atoms/IconButton.svelte.d.ts +7 -13
  25. package/dist/atoms/Input.svelte +58 -26
  26. package/dist/atoms/Input.svelte.d.ts +6 -1
  27. package/dist/atoms/List.svelte +2 -7
  28. package/dist/atoms/List.svelte.d.ts +1 -6
  29. package/dist/atoms/ListItem.svelte +54 -65
  30. package/dist/atoms/ListItem.svelte.d.ts +7 -13
  31. package/dist/atoms/ListItemLeading.svelte +19 -0
  32. package/dist/atoms/ListItemLeading.svelte.d.ts +9 -0
  33. package/dist/atoms/Progress.svelte +1 -1
  34. package/dist/atoms/Radio.svelte +51 -0
  35. package/dist/atoms/Radio.svelte.d.ts +14 -0
  36. package/dist/atoms/Select.svelte +23 -30
  37. package/dist/atoms/Select.svelte.d.ts +0 -10
  38. package/dist/atoms/SelectionControl.svelte +98 -0
  39. package/dist/atoms/SelectionControl.svelte.d.ts +26 -0
  40. package/dist/atoms/Skeleton.svelte +39 -15
  41. package/dist/atoms/Skeleton.svelte.d.ts +7 -3
  42. package/dist/atoms/Table.svelte +27 -0
  43. package/dist/atoms/Table.svelte.d.ts +10 -0
  44. package/dist/atoms/Text.svelte +46 -0
  45. package/dist/atoms/Text.svelte.d.ts +13 -0
  46. package/dist/atoms/Textarea.svelte +74 -53
  47. package/dist/atoms/Textarea.svelte.d.ts +6 -2
  48. package/dist/atoms/ThemeToggle.svelte +35 -24
  49. package/dist/atoms/ThemeToggle.svelte.d.ts +1 -0
  50. package/dist/atoms/Toast.svelte +55 -31
  51. package/dist/atoms/Toast.svelte.d.ts +4 -1
  52. package/dist/atoms/Toggle.svelte +37 -22
  53. package/dist/atoms/Toggle.svelte.d.ts +2 -1
  54. package/dist/atoms/Tooltip.svelte +97 -14
  55. package/dist/atoms/Tooltip.svelte.d.ts +6 -5
  56. package/dist/atoms/index.d.ts +6 -0
  57. package/dist/atoms/index.js +6 -0
  58. package/dist/atoms/selection-control.styles.d.ts +25 -0
  59. package/dist/atoms/selection-control.styles.js +30 -0
  60. package/dist/components/atoms/index.d.ts +6 -0
  61. package/dist/components/atoms/index.d.ts.map +1 -1
  62. package/dist/components/atoms/selection-control.styles.d.ts +26 -0
  63. package/dist/components/atoms/selection-control.styles.d.ts.map +1 -0
  64. package/dist/components/index.d.ts +41 -5
  65. package/dist/components/index.d.ts.map +1 -1
  66. package/dist/components/molecules/index.d.ts +10 -0
  67. package/dist/components/molecules/index.d.ts.map +1 -1
  68. package/dist/components/molecules/navigation-menu-context.d.ts +11 -0
  69. package/dist/components/molecules/navigation-menu-context.d.ts.map +1 -1
  70. package/dist/components/molecules/toast-store.d.ts +39 -0
  71. package/dist/components/molecules/toast-store.d.ts.map +1 -0
  72. package/dist/components/organisms/index.d.ts +2 -1
  73. package/dist/components/organisms/index.d.ts.map +1 -1
  74. package/dist/components/types/page.types.d.ts +69 -0
  75. package/dist/components/types/page.types.d.ts.map +1 -0
  76. package/dist/components/types/page.types.js +1 -0
  77. package/dist/components/types/variants.d.ts +64 -0
  78. package/dist/components/types/variants.d.ts.map +1 -0
  79. package/dist/components/types/variants.js +44 -0
  80. package/dist/components/util/fixed-sidebar-flyout.d.ts +23 -0
  81. package/dist/components/util/fixed-sidebar-flyout.d.ts.map +1 -0
  82. package/dist/components/util/focus-utils.d.ts +12 -0
  83. package/dist/components/util/focus-utils.d.ts.map +1 -0
  84. package/dist/components/util/ssr-safe.d.ts +17 -0
  85. package/dist/components/util/ssr-safe.d.ts.map +1 -0
  86. package/dist/index.d.ts +41 -5
  87. package/dist/index.js +44 -8
  88. package/dist/lib/layout-width-tokens.d.ts +17 -0
  89. package/dist/lib/layout-width-tokens.d.ts.map +1 -0
  90. package/dist/lib/showcase/component-docs/Button.d.ts +3 -0
  91. package/dist/lib/showcase/component-docs/Button.d.ts.map +1 -0
  92. package/dist/lib/showcase/component-docs/Input.d.ts +3 -0
  93. package/dist/lib/showcase/component-docs/Input.d.ts.map +1 -0
  94. package/dist/lib/showcase/component-docs/List.d.ts +3 -0
  95. package/dist/lib/showcase/component-docs/List.d.ts.map +1 -0
  96. package/dist/lib/showcase/component-docs/Modal.d.ts +3 -0
  97. package/dist/lib/showcase/component-docs/Modal.d.ts.map +1 -0
  98. package/dist/lib/showcase/component-docs/Radio.d.ts +3 -0
  99. package/dist/lib/showcase/component-docs/Radio.d.ts.map +1 -0
  100. package/dist/lib/showcase/component-docs/SidebarNavigation.d.ts +3 -0
  101. package/dist/lib/showcase/component-docs/SidebarNavigation.d.ts.map +1 -0
  102. package/dist/lib/showcase/component-docs/Skeleton.d.ts +3 -0
  103. package/dist/lib/showcase/component-docs/Skeleton.d.ts.map +1 -0
  104. package/dist/lib/showcase/component-docs/_shared.d.ts +12 -0
  105. package/dist/lib/showcase/component-docs/_shared.d.ts.map +1 -0
  106. package/dist/lib/showcase/component-docs/index.d.ts +4 -0
  107. package/dist/lib/showcase/component-docs/index.d.ts.map +1 -0
  108. package/dist/lib/showcase/components-catalog.d.ts.map +1 -1
  109. package/dist/lib/showcase/components-showcase-constants.d.ts +26 -0
  110. package/dist/lib/showcase/components-showcase-constants.d.ts.map +1 -1
  111. package/dist/molecules/Alert.svelte +50 -23
  112. package/dist/molecules/Alert.svelte.d.ts +1 -1
  113. package/dist/molecules/ComponentDemo.svelte +2 -2
  114. package/dist/molecules/ContactForm.svelte +1 -1
  115. package/dist/molecules/Dropdown.svelte +99 -109
  116. package/dist/molecules/Dropdown.svelte.d.ts +12 -6
  117. package/dist/molecules/EmptyState.svelte +44 -0
  118. package/dist/molecules/EmptyState.svelte.d.ts +11 -0
  119. package/dist/molecules/FormField.svelte +89 -0
  120. package/dist/molecules/FormField.svelte.d.ts +23 -0
  121. package/dist/molecules/Header.svelte +41 -0
  122. package/dist/molecules/Header.svelte.d.ts +10 -0
  123. package/dist/molecules/Modal.svelte +37 -24
  124. package/dist/molecules/Modal.svelte.d.ts +4 -2
  125. package/dist/molecules/NavigationMenu.svelte +16 -0
  126. package/dist/molecules/NavigationMenu.svelte.d.ts +2 -0
  127. package/dist/molecules/NavigationMenuContent.svelte +9 -1
  128. package/dist/molecules/NavigationMenuLink.svelte +1 -1
  129. package/dist/molecules/NavigationMenuTrigger.svelte +9 -1
  130. package/dist/molecules/Page.svelte +16 -0
  131. package/dist/molecules/Page.svelte.d.ts +8 -0
  132. package/dist/molecules/PropsTable.svelte +61 -0
  133. package/dist/molecules/PropsTable.svelte.d.ts +8 -0
  134. package/dist/molecules/RadioGroup.svelte +197 -0
  135. package/dist/molecules/RadioGroup.svelte.d.ts +20 -0
  136. package/dist/molecules/Section.svelte +2 -11
  137. package/dist/molecules/Section.svelte.d.ts +2 -11
  138. package/dist/molecules/SidebarBrandHeader.svelte +68 -0
  139. package/dist/molecules/SidebarBrandHeader.svelte.d.ts +10 -0
  140. package/dist/molecules/SidebarFooter.svelte +119 -0
  141. package/dist/molecules/SidebarFooter.svelte.d.ts +23 -0
  142. package/dist/molecules/SidebarNavSection.svelte +49 -0
  143. package/dist/molecules/SidebarNavSection.svelte.d.ts +14 -0
  144. package/dist/molecules/SlideUp.svelte +19 -14
  145. package/dist/molecules/SlideUp.svelte.d.ts +1 -1
  146. package/dist/molecules/Tabs.svelte +4 -9
  147. package/dist/molecules/Toaster.svelte +21 -0
  148. package/dist/molecules/Toaster.svelte.d.ts +6 -0
  149. package/dist/molecules/ToasterToast.svelte +217 -0
  150. package/dist/molecules/ToasterToast.svelte.d.ts +7 -0
  151. package/dist/molecules/index.d.ts +10 -0
  152. package/dist/molecules/index.js +10 -0
  153. package/dist/molecules/navigation-menu-context.d.ts +11 -0
  154. package/dist/molecules/navigation-menu-context.js +18 -0
  155. package/dist/molecules/toast-store.d.ts +38 -0
  156. package/dist/molecules/toast-store.js +34 -0
  157. package/dist/organisms/SidebarAccountPanel.svelte +118 -0
  158. package/dist/organisms/SidebarAccountPanel.svelte.d.ts +20 -0
  159. package/dist/organisms/SidebarNavigation.svelte +267 -237
  160. package/dist/organisms/SidebarNavigation.svelte.d.ts +21 -5
  161. package/dist/organisms/SidebarPanel.svelte +54 -22
  162. package/dist/organisms/SidebarPanel.svelte.d.ts +3 -0
  163. package/dist/organisms/{Navbar.svelte → TopNavbar.svelte} +22 -17
  164. package/dist/organisms/TopNavbar.svelte.d.ts +35 -0
  165. package/dist/organisms/index.d.ts +2 -1
  166. package/dist/organisms/index.js +2 -1
  167. package/dist/routes/lib/focus-utils.d.ts +7 -18
  168. package/dist/routes/lib/focus-utils.ts +32 -34
  169. package/dist/routes/lib/ssr-safe.ts +5 -4
  170. package/dist/routes/lib/variant-utils.ts +48 -39
  171. package/dist/types/components.d.ts +187 -0
  172. package/dist/types/components.d.ts.map +1 -0
  173. package/dist/types/events.d.ts +200 -0
  174. package/dist/types/events.d.ts.map +1 -0
  175. package/dist/types/index.d.ts +171 -0
  176. package/dist/types/index.d.ts.map +1 -0
  177. package/dist/types/page.types.d.ts +2 -31
  178. package/dist/types/page.types.d.ts.map +1 -1
  179. package/dist/types/page.types.js +1 -0
  180. package/dist/types/page.types.ts +79 -0
  181. package/dist/types/variants.d.ts +3 -0
  182. package/dist/types/variants.d.ts.map +1 -0
  183. package/dist/types/variants.js +1 -0
  184. package/dist/types/variants.ts +5 -6
  185. package/dist/util/fixed-sidebar-flyout.d.ts +22 -0
  186. package/dist/util/fixed-sidebar-flyout.js +148 -0
  187. package/dist/util/focus-utils.d.ts +11 -0
  188. package/dist/util/focus-utils.js +94 -0
  189. package/dist/util/ssr-safe.d.ts +16 -0
  190. package/dist/util/ssr-safe.js +48 -0
  191. package/dist/zabi-components-colors.css +128 -115
  192. package/dist/zabi-components-theme-dark-only.css +77 -72
  193. package/dist/zabi-components-theme-dark.css +77 -72
  194. package/dist/zabi-components-theme-only.css +51 -43
  195. package/dist/zabi-components-theme.css +51 -43
  196. package/dist/zabi-components.css +952 -249
  197. package/docs/lucide-icons.md +24 -0
  198. package/package.json +22 -9
  199. package/scripts/fix-lucide-svelte-icon-dts.js +35 -0
  200. package/dist/organisms/Navbar.svelte.d.ts +0 -41
@@ -1,30 +1,54 @@
1
1
  <script lang="ts">
2
+ type Variant = "text" | "circle" | "block";
3
+
2
4
  interface Props {
3
- width?: string;
4
- height?: string;
5
- className?: string;
5
+ variant?: Variant;
6
+ /** CSS length or px number; omit to use variant default (circle stays square). */
7
+ width?: string | number;
8
+ /** CSS length or px number; omit for variant default height. */
9
+ height?: string | number;
10
+ class?: string;
6
11
  "aria-label"?: string;
7
12
  }
8
13
 
9
14
  let {
10
- width = "100%",
11
- height = "1rem",
12
- className = "",
13
- "aria-label": ariaLabel = "Loading...",
14
- ...restProps
15
+ variant = "text",
16
+ width,
17
+ height,
18
+ class: className = "",
19
+ "aria-label": ariaLabel = "Loading…",
15
20
  }: Props = $props();
16
21
 
17
- const classes = $derived(
18
- `animate-pulse bg-action-secondary rounded${className ? ` ${className}` : ""}`,
22
+ const radius: Record<Variant, string> = {
23
+ text: "rounded-md",
24
+ circle: "rounded-full",
25
+ block: "rounded-lg",
26
+ };
27
+
28
+ const defaultShape: Record<Variant, string> = {
29
+ text: "w-full h-3",
30
+ circle: "w-10 aspect-square",
31
+ block: "w-full h-32",
32
+ };
33
+
34
+ function toSize(value: string | number): string {
35
+ return typeof value === "number" ? `${value}px` : value;
36
+ }
37
+
38
+ const inlineStyle = $derived(
39
+ [
40
+ width !== undefined ? `width: ${toSize(width)}` : null,
41
+ height !== undefined ? `height: ${toSize(height)}` : null,
42
+ ]
43
+ .filter(Boolean)
44
+ .join("; "),
19
45
  );
20
- const styles = $derived(`width: ${width}; height: ${height};`);
21
46
  </script>
22
47
 
23
48
  <div
24
- class={classes}
25
- style={styles}
49
+ class="animate-pulse bg-surface-2 {defaultShape[variant]} {radius[variant]} {className}"
50
+ style={inlineStyle || undefined}
51
+ role="status"
26
52
  aria-busy="true"
27
53
  aria-label={ariaLabel}
28
- role="status"
29
- {...restProps}
30
54
  ></div>
@@ -1,7 +1,11 @@
1
+ type Variant = "text" | "circle" | "block";
1
2
  interface Props {
2
- width?: string;
3
- height?: string;
4
- className?: string;
3
+ variant?: Variant;
4
+ /** CSS length or px number; omit to use variant default (circle stays square). */
5
+ width?: string | number;
6
+ /** CSS length or px number; omit for variant default height. */
7
+ height?: string | number;
8
+ class?: string;
5
9
  "aria-label"?: string;
6
10
  }
7
11
  declare const Skeleton: import("svelte").Component<Props, {}, "">;
@@ -0,0 +1,27 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+
4
+ interface Props {
5
+ /** Visible `<caption>` when set. */
6
+ caption?: string;
7
+ class?: string;
8
+ children?: Snippet;
9
+ }
10
+
11
+ let { caption, class: className = '', children }: Props = $props();
12
+ </script>
13
+
14
+ <div
15
+ class="overflow-x-auto rounded-lg border border-border shadow-sm {className}"
16
+ >
17
+ <table class="w-full min-w-[20rem] border-collapse text-left text-sm text-body">
18
+ {#if caption}
19
+ <caption class="border-b border-border bg-base-50 px-4 py-3 text-left text-headline">
20
+ {caption}
21
+ </caption>
22
+ {/if}
23
+ {#if children}
24
+ {@render children()}
25
+ {/if}
26
+ </table>
27
+ </div>
@@ -0,0 +1,10 @@
1
+ import type { Snippet } from 'svelte';
2
+ interface Props {
3
+ /** Visible `<caption>` when set. */
4
+ caption?: string;
5
+ class?: string;
6
+ children?: Snippet;
7
+ }
8
+ declare const Table: import("svelte").Component<Props, {}, "">;
9
+ type Table = ReturnType<typeof Table>;
10
+ export default Table;
@@ -0,0 +1,46 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+
4
+ type Tone = 'body' | 'description' | 'caption' | 'headline' | 'label' | 'error';
5
+ type Size = 'sm' | 'md' | 'lg';
6
+
7
+ interface Props {
8
+ as?: 'p' | 'span' | 'div';
9
+ tone?: Tone;
10
+ size?: Size;
11
+ class?: string;
12
+ children?: Snippet;
13
+ }
14
+
15
+ let {
16
+ as: Tag = 'p',
17
+ tone = 'body',
18
+ size = 'md',
19
+ class: className = '',
20
+ children,
21
+ }: Props = $props();
22
+
23
+ const toneClass: Record<Tone, string> = {
24
+ body: 'text-body',
25
+ description: 'text-description',
26
+ caption: 'text-caption',
27
+ headline: 'text-headline',
28
+ label: 'text-label',
29
+ error: 'text-error',
30
+ };
31
+
32
+ const sizeClass: Record<Size, string> = {
33
+ sm: 'text-sm',
34
+ md: 'text-base',
35
+ lg: 'text-lg',
36
+ };
37
+ </script>
38
+
39
+ <svelte:element
40
+ this={Tag}
41
+ class={[toneClass[tone], sizeClass[size], className].filter(Boolean).join(' ')}
42
+ >
43
+ {#if children}
44
+ {@render children()}
45
+ {/if}
46
+ </svelte:element>
@@ -0,0 +1,13 @@
1
+ import type { Snippet } from 'svelte';
2
+ type Tone = 'body' | 'description' | 'caption' | 'headline' | 'label' | 'error';
3
+ type Size = 'sm' | 'md' | 'lg';
4
+ interface Props {
5
+ as?: 'p' | 'span' | 'div';
6
+ tone?: Tone;
7
+ size?: Size;
8
+ class?: string;
9
+ children?: Snippet;
10
+ }
11
+ declare const Text: import("svelte").Component<Props, {}, "">;
12
+ type Text = ReturnType<typeof Text>;
13
+ export default Text;
@@ -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-input-border focus:ring-2 focus:ring-brand-500";
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 border 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 cursor-pointer 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>;