zabi-components 6.0.0 → 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 (197) 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 +35 -16
  7. package/dist/atoms/Button.svelte.d.ts +3 -1
  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 +33 -24
  24. package/dist/atoms/IconButton.svelte.d.ts +4 -8
  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 +90 -11
  55. package/dist/atoms/index.d.ts +6 -0
  56. package/dist/atoms/index.js +6 -0
  57. package/dist/atoms/selection-control.styles.d.ts +25 -0
  58. package/dist/atoms/selection-control.styles.js +30 -0
  59. package/dist/components/atoms/index.d.ts +6 -0
  60. package/dist/components/atoms/index.d.ts.map +1 -1
  61. package/dist/components/atoms/selection-control.styles.d.ts +26 -0
  62. package/dist/components/atoms/selection-control.styles.d.ts.map +1 -0
  63. package/dist/components/index.d.ts +41 -5
  64. package/dist/components/index.d.ts.map +1 -1
  65. package/dist/components/molecules/index.d.ts +10 -0
  66. package/dist/components/molecules/index.d.ts.map +1 -1
  67. package/dist/components/molecules/navigation-menu-context.d.ts +11 -0
  68. package/dist/components/molecules/navigation-menu-context.d.ts.map +1 -1
  69. package/dist/components/molecules/toast-store.d.ts +39 -0
  70. package/dist/components/molecules/toast-store.d.ts.map +1 -0
  71. package/dist/components/organisms/index.d.ts +2 -1
  72. package/dist/components/organisms/index.d.ts.map +1 -1
  73. package/dist/components/types/page.types.d.ts +69 -0
  74. package/dist/components/types/page.types.d.ts.map +1 -0
  75. package/dist/components/types/page.types.js +1 -0
  76. package/dist/components/types/variants.d.ts +64 -0
  77. package/dist/components/types/variants.d.ts.map +1 -0
  78. package/dist/components/types/variants.js +44 -0
  79. package/dist/components/util/fixed-sidebar-flyout.d.ts +23 -0
  80. package/dist/components/util/fixed-sidebar-flyout.d.ts.map +1 -0
  81. package/dist/components/util/focus-utils.d.ts +12 -0
  82. package/dist/components/util/focus-utils.d.ts.map +1 -0
  83. package/dist/components/util/ssr-safe.d.ts +17 -0
  84. package/dist/components/util/ssr-safe.d.ts.map +1 -0
  85. package/dist/index.d.ts +41 -5
  86. package/dist/index.js +44 -8
  87. package/dist/lib/layout-width-tokens.d.ts +17 -0
  88. package/dist/lib/layout-width-tokens.d.ts.map +1 -0
  89. package/dist/lib/showcase/component-docs/Button.d.ts +3 -0
  90. package/dist/lib/showcase/component-docs/Button.d.ts.map +1 -0
  91. package/dist/lib/showcase/component-docs/Input.d.ts +3 -0
  92. package/dist/lib/showcase/component-docs/Input.d.ts.map +1 -0
  93. package/dist/lib/showcase/component-docs/List.d.ts +3 -0
  94. package/dist/lib/showcase/component-docs/List.d.ts.map +1 -0
  95. package/dist/lib/showcase/component-docs/Modal.d.ts +3 -0
  96. package/dist/lib/showcase/component-docs/Modal.d.ts.map +1 -0
  97. package/dist/lib/showcase/component-docs/Radio.d.ts +3 -0
  98. package/dist/lib/showcase/component-docs/Radio.d.ts.map +1 -0
  99. package/dist/lib/showcase/component-docs/SidebarNavigation.d.ts +3 -0
  100. package/dist/lib/showcase/component-docs/SidebarNavigation.d.ts.map +1 -0
  101. package/dist/lib/showcase/component-docs/Skeleton.d.ts +3 -0
  102. package/dist/lib/showcase/component-docs/Skeleton.d.ts.map +1 -0
  103. package/dist/lib/showcase/component-docs/_shared.d.ts +12 -0
  104. package/dist/lib/showcase/component-docs/_shared.d.ts.map +1 -0
  105. package/dist/lib/showcase/component-docs/index.d.ts +4 -0
  106. package/dist/lib/showcase/component-docs/index.d.ts.map +1 -0
  107. package/dist/lib/showcase/components-catalog.d.ts.map +1 -1
  108. package/dist/lib/showcase/components-showcase-constants.d.ts +26 -0
  109. package/dist/lib/showcase/components-showcase-constants.d.ts.map +1 -1
  110. package/dist/molecules/Alert.svelte +50 -23
  111. package/dist/molecules/Alert.svelte.d.ts +1 -1
  112. package/dist/molecules/ComponentDemo.svelte +2 -2
  113. package/dist/molecules/ContactForm.svelte +1 -1
  114. package/dist/molecules/Dropdown.svelte +99 -109
  115. package/dist/molecules/Dropdown.svelte.d.ts +12 -6
  116. package/dist/molecules/EmptyState.svelte +44 -0
  117. package/dist/molecules/EmptyState.svelte.d.ts +11 -0
  118. package/dist/molecules/FormField.svelte +89 -0
  119. package/dist/molecules/FormField.svelte.d.ts +23 -0
  120. package/dist/molecules/Header.svelte +41 -0
  121. package/dist/molecules/Header.svelte.d.ts +10 -0
  122. package/dist/molecules/Modal.svelte +37 -24
  123. package/dist/molecules/Modal.svelte.d.ts +4 -2
  124. package/dist/molecules/NavigationMenu.svelte +16 -0
  125. package/dist/molecules/NavigationMenu.svelte.d.ts +2 -0
  126. package/dist/molecules/NavigationMenuContent.svelte +9 -1
  127. package/dist/molecules/NavigationMenuLink.svelte +1 -1
  128. package/dist/molecules/NavigationMenuTrigger.svelte +9 -1
  129. package/dist/molecules/Page.svelte +16 -0
  130. package/dist/molecules/Page.svelte.d.ts +8 -0
  131. package/dist/molecules/PropsTable.svelte +61 -0
  132. package/dist/molecules/PropsTable.svelte.d.ts +8 -0
  133. package/dist/molecules/RadioGroup.svelte +197 -0
  134. package/dist/molecules/RadioGroup.svelte.d.ts +20 -0
  135. package/dist/molecules/Section.svelte +2 -11
  136. package/dist/molecules/Section.svelte.d.ts +2 -11
  137. package/dist/molecules/SidebarBrandHeader.svelte +68 -0
  138. package/dist/molecules/SidebarBrandHeader.svelte.d.ts +10 -0
  139. package/dist/molecules/SidebarFooter.svelte +119 -0
  140. package/dist/molecules/SidebarFooter.svelte.d.ts +23 -0
  141. package/dist/molecules/SidebarNavSection.svelte +49 -0
  142. package/dist/molecules/SidebarNavSection.svelte.d.ts +14 -0
  143. package/dist/molecules/SlideUp.svelte +19 -14
  144. package/dist/molecules/SlideUp.svelte.d.ts +1 -1
  145. package/dist/molecules/Tabs.svelte +4 -9
  146. package/dist/molecules/Toaster.svelte +21 -0
  147. package/dist/molecules/Toaster.svelte.d.ts +6 -0
  148. package/dist/molecules/ToasterToast.svelte +217 -0
  149. package/dist/molecules/ToasterToast.svelte.d.ts +7 -0
  150. package/dist/molecules/index.d.ts +10 -0
  151. package/dist/molecules/index.js +10 -0
  152. package/dist/molecules/navigation-menu-context.d.ts +11 -0
  153. package/dist/molecules/navigation-menu-context.js +18 -0
  154. package/dist/molecules/toast-store.d.ts +38 -0
  155. package/dist/molecules/toast-store.js +34 -0
  156. package/dist/organisms/SidebarAccountPanel.svelte +118 -0
  157. package/dist/organisms/SidebarAccountPanel.svelte.d.ts +20 -0
  158. package/dist/organisms/SidebarNavigation.svelte +267 -237
  159. package/dist/organisms/SidebarNavigation.svelte.d.ts +21 -5
  160. package/dist/organisms/SidebarPanel.svelte +54 -22
  161. package/dist/organisms/SidebarPanel.svelte.d.ts +3 -0
  162. package/dist/organisms/{Navbar.svelte → TopNavbar.svelte} +22 -17
  163. package/dist/organisms/TopNavbar.svelte.d.ts +35 -0
  164. package/dist/organisms/index.d.ts +2 -1
  165. package/dist/organisms/index.js +2 -1
  166. package/dist/routes/lib/focus-utils.d.ts +7 -18
  167. package/dist/routes/lib/focus-utils.ts +32 -34
  168. package/dist/routes/lib/ssr-safe.ts +5 -4
  169. package/dist/routes/lib/variant-utils.ts +48 -39
  170. package/dist/types/components.d.ts +187 -0
  171. package/dist/types/components.d.ts.map +1 -0
  172. package/dist/types/events.d.ts +200 -0
  173. package/dist/types/events.d.ts.map +1 -0
  174. package/dist/types/index.d.ts +171 -0
  175. package/dist/types/index.d.ts.map +1 -0
  176. package/dist/types/page.types.d.ts +2 -31
  177. package/dist/types/page.types.d.ts.map +1 -1
  178. package/dist/types/page.types.js +1 -0
  179. package/dist/types/page.types.ts +79 -0
  180. package/dist/types/variants.d.ts +3 -0
  181. package/dist/types/variants.d.ts.map +1 -0
  182. package/dist/types/variants.js +1 -0
  183. package/dist/types/variants.ts +5 -6
  184. package/dist/util/fixed-sidebar-flyout.d.ts +22 -0
  185. package/dist/util/fixed-sidebar-flyout.js +148 -0
  186. package/dist/util/focus-utils.d.ts +11 -0
  187. package/dist/util/focus-utils.js +94 -0
  188. package/dist/util/ssr-safe.d.ts +16 -0
  189. package/dist/util/ssr-safe.js +48 -0
  190. package/dist/zabi-components-colors.css +14 -1
  191. package/dist/zabi-components-theme-dark-only.css +5 -0
  192. package/dist/zabi-components-theme-dark.css +5 -0
  193. package/dist/zabi-components-theme-only.css +9 -1
  194. package/dist/zabi-components-theme.css +9 -1
  195. package/dist/zabi-components.css +877 -192
  196. package/package.json +19 -7
  197. package/dist/organisms/Navbar.svelte.d.ts +0 -41
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- import type { ExtendedSemanticVariant } from "../../types/variants.js";
2
+ import type { ExtendedSemanticVariant } from "../types/variants.js";
3
3
  type AlertVisualVariant = Exclude<ExtendedSemanticVariant, "default">;
4
4
 
5
5
  interface Props {
@@ -44,28 +44,55 @@
44
44
  let alertRole = $derived(
45
45
  variant === "success" || variant === "info" ? "status" : "alert",
46
46
  );
47
+ </script>
47
48
 
48
- let iconSvg = $derived({
49
- info: `<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
50
- <path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd" />
51
- </svg>`,
52
- success: `<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
53
- <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
54
- </svg>`,
55
- warning: `<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
56
- <path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd" />
57
- </svg>`,
58
- error: `<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
59
- <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
60
- </svg>`,
61
- neutral: `<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
62
- <path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zM10 9a1 1 0 100 2 1 1 0 000-2z" clip-rule="evenodd" />
63
- </svg>`,
64
- energetic: `<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
49
+ {#snippet icon()}
50
+ {#if visualVariant === "info"}
51
+ <svg class="h-5 w-5" fill="currentColor" viewBox="0 0 20 20">
52
+ <path
53
+ fill-rule="evenodd"
54
+ d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z"
55
+ clip-rule="evenodd"
56
+ />
57
+ </svg>
58
+ {:else if visualVariant === "success"}
59
+ <svg class="h-5 w-5" fill="currentColor" viewBox="0 0 20 20">
60
+ <path
61
+ fill-rule="evenodd"
62
+ d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
63
+ clip-rule="evenodd"
64
+ />
65
+ </svg>
66
+ {:else if visualVariant === "warning"}
67
+ <svg class="h-5 w-5" fill="currentColor" viewBox="0 0 20 20">
68
+ <path
69
+ fill-rule="evenodd"
70
+ d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z"
71
+ clip-rule="evenodd"
72
+ />
73
+ </svg>
74
+ {:else if visualVariant === "error"}
75
+ <svg class="h-5 w-5" fill="currentColor" viewBox="0 0 20 20">
76
+ <path
77
+ fill-rule="evenodd"
78
+ d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z"
79
+ clip-rule="evenodd"
80
+ />
81
+ </svg>
82
+ {:else if visualVariant === "neutral"}
83
+ <svg class="h-5 w-5" fill="currentColor" viewBox="0 0 20 20">
84
+ <path
85
+ fill-rule="evenodd"
86
+ d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zM10 9a1 1 0 100 2 1 1 0 000-2z"
87
+ clip-rule="evenodd"
88
+ />
89
+ </svg>
90
+ {:else}
91
+ <svg class="h-5 w-5" fill="currentColor" viewBox="0 0 20 20">
65
92
  <path d="M11 3L4 14h6l-1 7 7-11h-6l1-7z" />
66
- </svg>`,
67
- });
68
- </script>
93
+ </svg>
94
+ {/if}
95
+ {/snippet}
69
96
 
70
97
  <div
71
98
  class="relative rounded-md p-4 border {alertClasses[
@@ -81,7 +108,7 @@
81
108
  {#if closable}
82
109
  <button
83
110
  onclick={handleDismiss}
84
- class="group absolute right-2 top-2 z-10 flex h-6 w-6 cursor-pointer items-center justify-center rounded-full text-description transition-colors duration-200 hover:text-body motion-reduce:transition-none focus:outline-none focus:ring-2 focus:ring-focus-ring focus:ring-offset-2"
111
+ class="group absolute right-2 top-2 z-10 flex h-6 w-6 cursor-pointer items-center justify-center rounded-full text-description transition-colors duration-200 hover:text-body motion-reduce:transition-none focus-ring"
85
112
  aria-label="Dismiss alert"
86
113
  type="button"
87
114
  >
@@ -103,7 +130,7 @@
103
130
 
104
131
  <div class="flex items-start gap-3 {closable ? 'pr-8' : ''}">
105
132
  <div class="shrink-0 mt-0.5">
106
- {@html iconSvg[visualVariant]}
133
+ {@render icon()}
107
134
  </div>
108
135
 
109
136
  <div class="flex-1 min-w-0">
@@ -1,4 +1,4 @@
1
- import type { ExtendedSemanticVariant } from "../../types/variants.js";
1
+ import type { ExtendedSemanticVariant } from "../types/variants.js";
2
2
  interface Props {
3
3
  variant?: ExtendedSemanticVariant;
4
4
  title?: string;
@@ -29,7 +29,7 @@
29
29
  <Card
30
30
  size="lg"
31
31
  fullWidth={true}
32
- className="overflow-hidden {className}"
32
+ className="overflow-visible {className}"
33
33
  {...restProps}
34
34
  >
35
35
  <CardHeader {title} {description} />
@@ -81,7 +81,7 @@
81
81
  {#if showCode}
82
82
  <CodeBlock {code} {language} />
83
83
  {:else}
84
- <div class="min-h-[100px] flex items-center justify-center w-full">
84
+ <div class="min-h-[100px] w-full min-w-0 py-4">
85
85
  {@render children?.()}
86
86
  </div>
87
87
  {/if}
@@ -7,7 +7,7 @@
7
7
  import Card from "../atoms/Card.svelte";
8
8
  import CardHeader from "../atoms/CardHeader.svelte";
9
9
  import CardContent from "../atoms/CardContent.svelte";
10
- import type { ContactFormData } from "../../types/page.types";
10
+ import type { ContactFormData } from "../types/page.types";
11
11
 
12
12
  interface Props {
13
13
  className?: string;
@@ -1,11 +1,19 @@
1
1
  <script lang="ts">
2
- import Button from "../atoms/Button.svelte";
3
- import { generateId } from "../../routes/lib/ssr-safe.js";
2
+ import { generateId } from "../util/ssr-safe.js";
3
+ import type { Snippet } from 'svelte';
4
+
5
+ export type DropdownTriggerProps = {
6
+ 'aria-expanded': boolean;
7
+ 'aria-haspopup': 'menu' | 'listbox';
8
+ 'aria-controls': string;
9
+ };
4
10
 
5
11
  interface Props {
6
12
  isOpen?: boolean;
7
- placement?: "bottom-start" | "bottom-end" | "top-start" | "top-end";
13
+ placement?: 'bottom-start' | 'bottom-end' | 'top-start' | 'top-end';
8
14
  ariaLabel?: string;
15
+ /** `listbox` for Select-style; `menu` for actions. */
16
+ menuRole?: 'menu' | 'listbox';
9
17
  selectedValue?: string | number | null;
10
18
  options?: Array<{
11
19
  value: string | number;
@@ -13,31 +21,40 @@
13
21
  disabled?: boolean;
14
22
  }>;
15
23
  onOptionClick?: (value: string | number) => void;
24
+ trigger: Snippet<[DropdownTriggerProps]>;
25
+ children?: Snippet;
16
26
  }
17
27
 
18
28
  let {
19
- isOpen = false,
20
- placement = "bottom-start",
21
- ariaLabel = "Menu",
29
+ isOpen = $bindable(false),
30
+ placement = 'bottom-start',
31
+ ariaLabel = 'Menu',
32
+ menuRole = 'menu',
22
33
  selectedValue = null,
23
34
  options = [],
24
35
  onOptionClick,
25
- children,
26
36
  trigger,
37
+ children,
27
38
  ...restProps
28
- }: Props & { children?: any; trigger?: any } = $props();
39
+ }: Props = $props();
40
+
41
+ const menuId = generateId('dropdown-menu');
42
+ let rootEl = $state<HTMLDivElement | null>(null);
29
43
 
30
- const dropdownId = generateId("dropdown");
31
- let triggerElement = $state<HTMLElement | null>(null);
32
- let menuElement = $state<HTMLElement | null>(null);
33
44
  let openedViaKeyboard = $state(false);
34
45
 
46
+ const triggerAria = $derived({
47
+ 'aria-expanded': isOpen,
48
+ 'aria-haspopup': menuRole === 'listbox' ? 'listbox' as const : 'menu' as const,
49
+ 'aria-controls': menuId,
50
+ } satisfies DropdownTriggerProps);
51
+
35
52
  function handleKeydown(event: KeyboardEvent) {
36
53
  if (!isOpen) {
37
54
  if (
38
- event.key === "Enter" ||
39
- event.key === " " ||
40
- event.key === "ArrowDown"
55
+ event.key === 'Enter' ||
56
+ event.key === ' ' ||
57
+ event.key === 'ArrowDown'
41
58
  ) {
42
59
  event.preventDefault();
43
60
  openedViaKeyboard = true;
@@ -47,38 +64,39 @@
47
64
  }
48
65
 
49
66
  switch (event.key) {
50
- case "Escape":
67
+ case 'Escape':
51
68
  event.preventDefault();
52
69
  isOpen = false;
53
- triggerElement?.focus();
54
70
  break;
55
- case "ArrowDown":
71
+ case 'ArrowDown':
56
72
  event.preventDefault();
57
73
  focusNextItem();
58
74
  break;
59
- case "ArrowUp":
75
+ case 'ArrowUp':
60
76
  event.preventDefault();
61
77
  focusPreviousItem();
62
78
  break;
63
- case "Home":
79
+ case 'Home':
64
80
  event.preventDefault();
65
81
  focusFirstItem();
66
82
  break;
67
- case "End":
83
+ case 'End':
68
84
  event.preventDefault();
69
85
  focusLastItem();
70
86
  break;
71
- case "Tab":
87
+ case 'Tab':
72
88
  isOpen = false;
73
89
  break;
74
90
  }
75
91
  }
76
92
 
93
+ let menuElement = $state<HTMLElement | null>(null);
94
+
77
95
  function getMenuItems(): HTMLElement[] {
78
96
  if (!menuElement) return [];
79
97
  return Array.from(
80
98
  menuElement.querySelectorAll<HTMLElement>(
81
- '[role="menuitem"], button, a',
99
+ '[role="menuitem"], [role="option"]',
82
100
  ),
83
101
  );
84
102
  }
@@ -104,8 +122,7 @@
104
122
  }
105
123
 
106
124
  function focusFirstItem() {
107
- const items = getMenuItems();
108
- items[0]?.focus();
125
+ getMenuItems()[0]?.focus();
109
126
  }
110
127
 
111
128
  function focusLastItem() {
@@ -113,127 +130,107 @@
113
130
  items[items.length - 1]?.focus();
114
131
  }
115
132
 
116
- // Focus first item when dropdown opens via keyboard
117
133
  $effect(() => {
118
134
  if (isOpen && menuElement && openedViaKeyboard) {
119
- setTimeout(() => {
135
+ const t = setTimeout(() => {
120
136
  focusFirstItem();
121
137
  openedViaKeyboard = false;
122
138
  }, 0);
123
- } else if (!isOpen) {
139
+ return () => clearTimeout(t);
140
+ }
141
+ if (!isOpen) {
124
142
  openedViaKeyboard = false;
125
143
  }
126
144
  });
127
145
 
128
- // Mark selected items
129
146
  $effect(() => {
130
- if (isOpen && menuElement) {
131
- // Small delay to ensure DOM is ready
132
- setTimeout(() => {
133
- const items = getMenuItems();
134
- items.forEach((item) => {
135
- if (selectedValue !== null && selectedValue !== undefined) {
136
- const itemValue =
137
- item.getAttribute("data-value") ||
138
- item.textContent?.trim();
139
- if (String(itemValue) === String(selectedValue)) {
140
- item.setAttribute("data-selected", "true");
141
- item.setAttribute("aria-selected", "true");
142
- } else {
143
- item.removeAttribute("data-selected");
144
- item.removeAttribute("aria-selected");
145
- }
146
- } else {
147
- item.removeAttribute("data-selected");
148
- item.removeAttribute("aria-selected");
149
- }
150
- });
151
- }, 0);
147
+ if (!isOpen) return;
148
+ function onDocMouseDown(e: MouseEvent) {
149
+ const t = e.target as Node;
150
+ if (rootEl && !rootEl.contains(t)) {
151
+ isOpen = false;
152
+ }
152
153
  }
154
+ document.addEventListener('mousedown', onDocMouseDown);
155
+ return () => document.removeEventListener('mousedown', onDocMouseDown);
153
156
  });
154
157
 
155
158
  const placementClasses = $derived(() => {
156
- const base = "absolute z-dropdown min-w-[12rem]";
157
- const positioning = {
158
- "bottom-start": "top-full left-0 mt-2",
159
- "bottom-end": "top-full right-0 mt-2",
160
- "top-start": "bottom-full left-0 mb-2",
161
- "top-end": "bottom-full right-0 mb-2",
159
+ const base = 'absolute z-dropdown min-w-[12rem]';
160
+ const positioning: Record<typeof placement, string> = {
161
+ 'bottom-start': 'top-full left-0 mt-2',
162
+ 'bottom-end': 'top-full right-0 mt-2',
163
+ 'top-start': 'bottom-full left-0 mb-2',
164
+ 'top-end': 'bottom-full right-0 mb-2',
162
165
  };
163
166
  return `${base} ${positioning[placement]}`;
164
167
  });
165
168
 
166
169
  const transformClasses = $derived(() => {
167
170
  if (!isOpen) {
168
- const hiddenTransform = {
169
- "bottom-start": "translate-y-1",
170
- "bottom-end": "translate-y-1",
171
- "top-start": "-translate-y-1",
172
- "top-end": "-translate-y-1",
171
+ const hiddenTransform: Record<typeof placement, string> = {
172
+ 'bottom-start': 'translate-y-1',
173
+ 'bottom-end': 'translate-y-1',
174
+ 'top-start': '-translate-y-1',
175
+ 'top-end': '-translate-y-1',
173
176
  };
174
- return `opacity-0 invisible ${hiddenTransform[placement]}`;
177
+ return `invisible opacity-0 ${hiddenTransform[placement]}`;
175
178
  }
176
- return "opacity-100 visible translate-y-0";
179
+ return 'visible translate-y-0 opacity-100';
177
180
  });
178
181
 
179
182
  const dropdownContentClasses = $derived(() => {
180
- return `
181
- ${placementClasses()}
182
- bg-input
183
- rounded-lg
184
- shadow-lg
185
- border
186
- border-input-border
187
- py-2
188
- transition-all
189
- duration-200
190
- ease-in-out
191
- ${transformClasses()}
192
- `
193
- .trim()
194
- .replace(/\s+/g, " ");
183
+ return [
184
+ placementClasses(),
185
+ 'rounded-lg border border-border bg-card py-2 shadow-lg transition-all duration-200 ease-in-out',
186
+ transformClasses(),
187
+ ]
188
+ .join(' ')
189
+ .replace(/\s+/g, ' ')
190
+ .trim();
195
191
  });
192
+
193
+ const itemRole = $derived(menuRole === 'listbox' ? 'option' : 'menuitem');
194
+
195
+ const optionClasses =
196
+ 'focus-ring flex w-full items-center justify-start rounded-md px-3 py-2 text-left text-sm text-body transition-colors hover:bg-base-100 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50';
196
197
  </script>
197
198
 
198
199
  <div
200
+ bind:this={rootEl}
199
201
  class="relative inline-block"
200
202
  data-placement={placement}
201
203
  onkeydown={handleKeydown}
202
204
  {...restProps}
203
205
  >
204
- <div bind:this={triggerElement}>
205
- {@render trigger?.()}
206
- </div>
206
+ {@render trigger(triggerAria)}
207
207
 
208
208
  {#if isOpen}
209
209
  <div
210
210
  bind:this={menuElement}
211
- id={dropdownId}
211
+ id={menuId}
212
212
  class={dropdownContentClasses()}
213
- role="menu"
213
+ role={menuRole === 'listbox' ? 'listbox' : 'menu'}
214
214
  aria-label={ariaLabel}
215
- tabindex="-1"
216
215
  >
217
216
  {#if options.length > 0}
218
- <div class="px-2 py-2">
217
+ <div class="px-2 py-1">
219
218
  {#each options as option (option.value)}
220
- {@const buttonRestProps = {
221
- "data-value": String(option.value),
222
- } as any}
223
- <div class="w-full my-0.5">
224
- <Button
225
- variant={selectedValue === option.value
226
- ? "outline"
227
- : "ghost"}
228
- size="sm"
229
- isFullWidth={true}
230
- disabled={option.disabled}
231
- onclick={() => onOptionClick?.(option.value)}
232
- {...buttonRestProps}
233
- >
234
- {option.label}
235
- </Button>
236
- </div>
219
+ <button
220
+ type="button"
221
+ role={itemRole}
222
+ aria-selected={menuRole === 'listbox' &&
223
+ selectedValue !== null &&
224
+ String(selectedValue) === String(option.value)
225
+ ? true
226
+ : undefined}
227
+ data-value={String(option.value)}
228
+ disabled={option.disabled}
229
+ class={optionClasses}
230
+ onclick={() => onOptionClick?.(option.value)}
231
+ >
232
+ {option.label}
233
+ </button>
237
234
  {/each}
238
235
  </div>
239
236
  {:else if children}
@@ -242,10 +239,3 @@
242
239
  </div>
243
240
  {/if}
244
241
  </div>
245
-
246
- <style>
247
- :global([role="menu"] button) {
248
- justify-content: flex-start;
249
- text-align: left;
250
- }
251
- </style>
@@ -1,7 +1,15 @@
1
+ import type { Snippet } from 'svelte';
2
+ export type DropdownTriggerProps = {
3
+ 'aria-expanded': boolean;
4
+ 'aria-haspopup': 'menu' | 'listbox';
5
+ 'aria-controls': string;
6
+ };
1
7
  interface Props {
2
8
  isOpen?: boolean;
3
- placement?: "bottom-start" | "bottom-end" | "top-start" | "top-end";
9
+ placement?: 'bottom-start' | 'bottom-end' | 'top-start' | 'top-end';
4
10
  ariaLabel?: string;
11
+ /** `listbox` for Select-style; `menu` for actions. */
12
+ menuRole?: 'menu' | 'listbox';
5
13
  selectedValue?: string | number | null;
6
14
  options?: Array<{
7
15
  value: string | number;
@@ -9,11 +17,9 @@ interface Props {
9
17
  disabled?: boolean;
10
18
  }>;
11
19
  onOptionClick?: (value: string | number) => void;
20
+ trigger: Snippet<[DropdownTriggerProps]>;
21
+ children?: Snippet;
12
22
  }
13
- type $$ComponentProps = Props & {
14
- children?: any;
15
- trigger?: any;
16
- };
17
- declare const Dropdown: import("svelte").Component<$$ComponentProps, {}, "">;
23
+ declare const Dropdown: import("svelte").Component<Props, {}, "isOpen">;
18
24
  type Dropdown = ReturnType<typeof Dropdown>;
19
25
  export default Dropdown;
@@ -0,0 +1,44 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from "svelte";
3
+ import { generateId } from "../util/ssr-safe.js";
4
+
5
+ interface Props {
6
+ title: string;
7
+ description: string;
8
+ class?: string;
9
+ action?: Snippet;
10
+ media?: Snippet;
11
+ }
12
+
13
+ let {
14
+ title,
15
+ description,
16
+ class: className = "",
17
+ action,
18
+ media,
19
+ }: Props = $props();
20
+
21
+ const titleId = generateId("empty-state-title");
22
+ </script>
23
+
24
+ <section
25
+ class="mx-auto flex flex-col items-center justify-center gap-4 px-6 py-12 text-center {className}"
26
+ aria-labelledby={titleId}
27
+ >
28
+ {#if media}
29
+ <div class="text-description" aria-hidden="true">
30
+ {@render media()}
31
+ </div>
32
+ {/if}
33
+ <div class="space-y-2">
34
+ <h2 id={titleId} class="text-lg font-semibold text-headline">
35
+ {title}
36
+ </h2>
37
+ <p class="text-sm text-description">{description}</p>
38
+ </div>
39
+ {#if action}
40
+ <div class="mt-2 flex flex-wrap items-center justify-center gap-2">
41
+ {@render action()}
42
+ </div>
43
+ {/if}
44
+ </section>
@@ -0,0 +1,11 @@
1
+ import type { Snippet } from "svelte";
2
+ interface Props {
3
+ title: string;
4
+ description: string;
5
+ class?: string;
6
+ action?: Snippet;
7
+ media?: Snippet;
8
+ }
9
+ declare const EmptyState: import("svelte").Component<Props, {}, "">;
10
+ type EmptyState = ReturnType<typeof EmptyState>;
11
+ export default EmptyState;
@@ -0,0 +1,89 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import { generateId } from "../util/ssr-safe.js";
4
+
5
+ export type FormFieldControlProps = {
6
+ id: string;
7
+ 'aria-describedby'?: string;
8
+ 'aria-invalid'?: 'true';
9
+ disabled?: boolean;
10
+ required?: boolean;
11
+ };
12
+
13
+ interface Props {
14
+ id?: string;
15
+ label: string;
16
+ description?: string;
17
+ error?: string;
18
+ required?: boolean;
19
+ disabled?: boolean;
20
+ class?: string;
21
+ labelClass?: string;
22
+ control: Snippet<[FormFieldControlProps]>;
23
+ meta?: Snippet;
24
+ }
25
+
26
+ let {
27
+ id: idProp,
28
+ label,
29
+ description,
30
+ error,
31
+ required = false,
32
+ disabled = false,
33
+ class: className = '',
34
+ labelClass = '',
35
+ control,
36
+ meta,
37
+ }: Props = $props();
38
+
39
+ const fallbackId = generateId('field');
40
+ const fieldId = $derived(idProp ?? fallbackId);
41
+ const descriptionId = $derived(
42
+ description ? `${fieldId}-description` : undefined,
43
+ );
44
+ const errorId = $derived(error ? `${fieldId}-error` : undefined);
45
+ const describedBy = $derived(
46
+ [descriptionId, errorId].filter(Boolean).join(' ') || undefined,
47
+ );
48
+ </script>
49
+
50
+ <div class={['space-y-1.5', className].filter(Boolean).join(' ')}>
51
+ <div class="flex items-baseline justify-between gap-3">
52
+ <label
53
+ class={[
54
+ 'text-sm font-medium text-label',
55
+ disabled ? 'opacity-60' : '',
56
+ labelClass,
57
+ ]
58
+ .filter(Boolean)
59
+ .join(' ')}
60
+ for={fieldId}
61
+ >
62
+ {label}
63
+ {#if required}
64
+ <span class="ml-1 text-error" aria-hidden="true">*</span>
65
+ <span class="sr-only">(required)</span>
66
+ {/if}
67
+ </label>
68
+
69
+ {#if meta}
70
+ <div class="text-xs text-description">{@render meta()}</div>
71
+ {/if}
72
+ </div>
73
+
74
+ {@render control({
75
+ id: fieldId,
76
+ 'aria-describedby': describedBy,
77
+ 'aria-invalid': error ? 'true' : undefined,
78
+ disabled,
79
+ required,
80
+ })}
81
+
82
+ {#if description}
83
+ <p id={descriptionId} class="text-xs text-description">{description}</p>
84
+ {/if}
85
+
86
+ {#if error}
87
+ <p id={errorId} class="text-xs text-error" role="alert">{error}</p>
88
+ {/if}
89
+ </div>