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
@@ -0,0 +1,23 @@
1
+ import type { Snippet } from 'svelte';
2
+ export type FormFieldControlProps = {
3
+ id: string;
4
+ 'aria-describedby'?: string;
5
+ 'aria-invalid'?: 'true';
6
+ disabled?: boolean;
7
+ required?: boolean;
8
+ };
9
+ interface Props {
10
+ id?: string;
11
+ label: string;
12
+ description?: string;
13
+ error?: string;
14
+ required?: boolean;
15
+ disabled?: boolean;
16
+ class?: string;
17
+ labelClass?: string;
18
+ control: Snippet<[FormFieldControlProps]>;
19
+ meta?: Snippet;
20
+ }
21
+ declare const FormField: import("svelte").Component<Props, {}, "">;
22
+ type FormField = ReturnType<typeof FormField>;
23
+ export default FormField;
@@ -0,0 +1,41 @@
1
+ <script lang="ts">
2
+ import Badge from "../atoms/Badge.svelte";
3
+ import Heading from "../atoms/Heading.svelte";
4
+ import type { ComponentCategory } from "../types/page.types";
5
+
6
+ interface Props {
7
+ title: string;
8
+ description: string;
9
+ category: ComponentCategory;
10
+ variantsStates?: string[];
11
+ }
12
+
13
+ let { title, description, category, variantsStates }: Props = $props();
14
+ </script>
15
+
16
+ <header class="space-y-4">
17
+ <div class="space-y-2">
18
+ <Heading level={1} text={title} />
19
+ <p class="text-lg whitespace-pre-line text-description">{description}</p>
20
+ </div>
21
+
22
+ <div class="flex flex-wrap items-center gap-2">
23
+ <Badge variant="info" text={category} />
24
+ {#if variantsStates?.length}
25
+ <Badge variant="info" text="{variantsStates.length} variants" />
26
+ {/if}
27
+ </div>
28
+
29
+ {#if variantsStates?.length}
30
+ <div class="flex flex-wrap items-center gap-2">
31
+ <span class="text-sm font-medium text-headline">
32
+ Variants / states:
33
+ </span>
34
+ {#each variantsStates as v (v)}
35
+ <span class="rounded-md bg-base-100 px-2 py-1 text-xs text-headline">
36
+ {v}
37
+ </span>
38
+ {/each}
39
+ </div>
40
+ {/if}
41
+ </header>
@@ -0,0 +1,10 @@
1
+ import type { ComponentCategory } from "../types/page.types";
2
+ interface Props {
3
+ title: string;
4
+ description: string;
5
+ category: ComponentCategory;
6
+ variantsStates?: string[];
7
+ }
8
+ declare const Header: import("svelte").Component<Props, {}, "">;
9
+ type Header = ReturnType<typeof Header>;
10
+ export default Header;
@@ -1,11 +1,17 @@
1
1
  <script lang="ts">
2
- import { trapFocus, saveFocus, returnFocus, focusFirstElement } from "../routes/lib/focus-utils.js";
3
- import Card from "../atoms/Card.svelte";
4
- import CardHeader from "../atoms/CardHeader.svelte";
5
- import CardContent from "../atoms/CardContent.svelte";
6
- import CardFooter from "../atoms/CardFooter.svelte";
2
+ import {
3
+ trapFocus,
4
+ saveFocus,
5
+ returnFocus,
6
+ focusFirstElement,
7
+ } from '../util/focus-utils.js';
8
+ import { generateId } from "../util/ssr-safe.js";
9
+ import Card from '../atoms/Card.svelte';
10
+ import CardHeader from '../atoms/CardHeader.svelte';
11
+ import CardContent from '../atoms/CardContent.svelte';
12
+ import CardFooter from '../atoms/CardFooter.svelte';
7
13
 
8
- type Size = "sm" | "md" | "lg";
14
+ type Size = 'sm' | 'md' | 'lg';
9
15
 
10
16
  interface Props {
11
17
  isOpen?: boolean;
@@ -14,29 +20,35 @@
14
20
  size?: Size;
15
21
  onclick?: (event: Event) => void;
16
22
  onkeydown?: (event: Event) => void;
23
+ /** On the `role="dialog"` root (testing, analytics). */
24
+ "data-testid"?: string;
17
25
  }
18
26
 
19
27
  let {
20
- isOpen = false,
21
- title = "",
22
- description = "",
23
- size = "md",
28
+ isOpen = $bindable(false),
29
+ title = '',
30
+ description = '',
31
+ size = 'md',
24
32
  onclick,
25
33
  onkeydown,
34
+ "data-testid": dataTestId = undefined,
26
35
  children,
27
36
  footer,
28
37
  ...restProps
29
38
  }: Props & { children?: any; footer?: any } = $props();
30
39
 
40
+ const modalTitleId = generateId('modal-title');
41
+ const modalDescriptionId = generateId('modal-description');
42
+
31
43
  let modalContainer = $state<HTMLDivElement>();
32
44
  let cleanupFocusTrap: (() => void) | null = null;
33
45
 
34
46
  const sizeClasses = $derived(
35
47
  {
36
- sm: "w-full md:w-[24rem]",
37
- md: "w-full md:w-[28rem]",
38
- lg: "w-full md:w-[42rem]",
39
- }[size] || "w-full md:w-[28rem]",
48
+ sm: 'w-full md:w-[24rem]',
49
+ md: 'w-full md:w-[28rem]',
50
+ lg: 'w-full md:w-[42rem]',
51
+ }[size] || 'w-full md:w-[28rem]',
40
52
  );
41
53
 
42
54
  function closeModal(event?: Event) {
@@ -51,19 +63,18 @@
51
63
  }
52
64
  }
53
65
 
54
- // Handle focus trap when modal opens
55
66
  $effect(() => {
56
67
  const container = modalContainer;
57
68
  if (isOpen && container) {
58
69
  saveFocus();
59
70
  cleanupFocusTrap = trapFocus(container);
60
- // Small delay to ensure modal is rendered
61
71
  setTimeout(() => {
62
72
  focusFirstElement(container);
63
73
  }, 0);
64
74
  } else if (!isOpen && cleanupFocusTrap) {
65
75
  cleanupFocusTrap();
66
76
  cleanupFocusTrap = null;
77
+ returnFocus();
67
78
  }
68
79
  });
69
80
 
@@ -75,12 +86,10 @@
75
86
 
76
87
  function handleKeydown(event: Event) {
77
88
  const keyboardEvent = event as KeyboardEvent;
78
- if (keyboardEvent.key === "Escape") {
89
+ if (keyboardEvent.key === 'Escape') {
79
90
  closeModal(event);
80
91
  }
81
- if (onkeydown) {
82
- onkeydown(event);
83
- }
92
+ onkeydown?.(event);
84
93
  }
85
94
  </script>
86
95
 
@@ -91,8 +100,11 @@
91
100
  onkeydown={handleKeydown}
92
101
  role="dialog"
93
102
  aria-modal="true"
94
- aria-labelledby={title ? "modal-title" : undefined}
103
+ aria-labelledby={title ? modalTitleId : undefined}
104
+ aria-describedby={description ? modalDescriptionId : undefined}
95
105
  tabindex="-1"
106
+ data-testid={dataTestId}
107
+ {...restProps}
96
108
  >
97
109
  <div
98
110
  bind:this={modalContainer}
@@ -102,20 +114,21 @@
102
114
  {#if title || description}
103
115
  <CardHeader
104
116
  {description}
117
+ descriptionId={description ? modalDescriptionId : undefined}
105
118
  className="px-6 pt-6 pb-4"
106
119
  >
107
120
  {#if title}
108
121
  <div class="flex items-center justify-between">
109
122
  <h2
110
- id="modal-title"
111
- class="text-2xl font-normal leading-8 text-headline tracking-normal"
123
+ id={modalTitleId}
124
+ class="text-2xl font-normal leading-8 tracking-normal text-headline"
112
125
  >
113
126
  {title}
114
127
  </h2>
115
128
  <button
116
129
  type="button"
117
130
  onclick={closeModal}
118
- class="text-description hover:text-headline text-2xl cursor-pointer transition-colors w-8 h-8 flex items-center justify-center rounded-full hover:bg-base-100"
131
+ class="flex h-8 w-8 cursor-pointer items-center justify-center rounded-full text-2xl text-description transition-colors hover:bg-base-100 hover:text-headline"
119
132
  aria-label="Close"
120
133
  >
121
134
  ×
@@ -1,4 +1,4 @@
1
- type Size = "sm" | "md" | "lg";
1
+ type Size = 'sm' | 'md' | 'lg';
2
2
  interface Props {
3
3
  isOpen?: boolean;
4
4
  title?: string;
@@ -6,11 +6,13 @@ interface Props {
6
6
  size?: Size;
7
7
  onclick?: (event: Event) => void;
8
8
  onkeydown?: (event: Event) => void;
9
+ /** On the `role="dialog"` root (testing, analytics). */
10
+ "data-testid"?: string;
9
11
  }
10
12
  type $$ComponentProps = Props & {
11
13
  children?: any;
12
14
  footer?: any;
13
15
  };
14
- declare const Modal: import("svelte").Component<$$ComponentProps, {}, "">;
16
+ declare const Modal: import("svelte").Component<$$ComponentProps, {}, "isOpen">;
15
17
  type Modal = ReturnType<typeof Modal>;
16
18
  export default Modal;
@@ -8,8 +8,10 @@
8
8
  import NavigationMenuContent from "./NavigationMenuContent.svelte";
9
9
  import NavigationMenuLink from "./NavigationMenuLink.svelte";
10
10
 
11
+ import { generateId } from "../util/ssr-safe.js";
11
12
  import {
12
13
  NAVIGATION_MENU_CONTEXT_KEY,
14
+ navigationMenuStableInstanceId,
13
15
  type NavigationMenuContextValue,
14
16
  } from "./navigation-menu-context.js";
15
17
 
@@ -35,6 +37,8 @@
35
37
  items?: NavigationMenuItemData[];
36
38
  children?: Snippet;
37
39
  listClassName?: string;
40
+ /** Panel id / `aria-controls` prefix; pass for SSR or multiple menus. If omitted with non-empty `items`, a deterministic hash is used. */
41
+ menuId?: string;
38
42
  }
39
43
 
40
44
  let {
@@ -43,6 +47,7 @@
43
47
  items = [],
44
48
  children,
45
49
  listClassName = "",
50
+ menuId = "",
46
51
  ...restProps
47
52
  }: Props = $props();
48
53
 
@@ -50,6 +55,14 @@
50
55
  let isMobile = $state(false);
51
56
  let containerElement = $state<HTMLElement | null>(null);
52
57
 
58
+ const menuInstanceId = $derived(
59
+ menuId.trim() !== ""
60
+ ? menuId.trim()
61
+ : items.length > 0
62
+ ? navigationMenuStableInstanceId(items)
63
+ : generateId("nav-menu"),
64
+ );
65
+
53
66
  function setActiveItem(item: string | null) {
54
67
  activeItem = item;
55
68
  }
@@ -81,6 +94,9 @@
81
94
  get isMobile() {
82
95
  return isMobile;
83
96
  },
97
+ get menuInstanceId() {
98
+ return menuInstanceId;
99
+ },
84
100
  };
85
101
 
86
102
  setContext(NAVIGATION_MENU_CONTEXT_KEY, context);
@@ -19,6 +19,8 @@ interface Props {
19
19
  items?: NavigationMenuItemData[];
20
20
  children?: Snippet;
21
21
  listClassName?: string;
22
+ /** Panel id / `aria-controls` prefix; pass for SSR or multiple menus. If omitted with non-empty `items`, a deterministic hash is used. */
23
+ menuId?: string;
22
24
  }
23
25
  declare const NavigationMenu: import("svelte").Component<Props, {}, "">;
24
26
  type NavigationMenu = ReturnType<typeof NavigationMenu>;
@@ -4,6 +4,7 @@
4
4
  import { onMount } from "svelte";
5
5
  import {
6
6
  NAVIGATION_MENU_CONTEXT_KEY,
7
+ navigationMenuPanelId,
7
8
  type NavigationMenuContextValue,
8
9
  } from "./navigation-menu-context.js";
9
10
 
@@ -29,6 +30,12 @@
29
30
 
30
31
  const isActive = $derived(context?.activeItem === value);
31
32
 
33
+ const panelId = $derived(
34
+ context
35
+ ? navigationMenuPanelId(context.menuInstanceId, value)
36
+ : "",
37
+ );
38
+
32
39
  function handleKeydown(event: KeyboardEvent) {
33
40
  if (event.key === "Escape") {
34
41
  event.preventDefault();
@@ -71,7 +78,8 @@
71
78
  {#if isActive}
72
79
  <div
73
80
  bind:this={contentElement}
74
- class="absolute left-0 top-full mt-2 bg-input rounded-lg shadow-lg border border-input-border p-4 z-dropdown min-w-[200px] transition-all duration-200 ease-in-out {className}"
81
+ id={panelId || undefined}
82
+ class="absolute left-0 top-full mt-2 bg-card rounded-lg shadow-lg border border-border p-4 z-dropdown min-w-[200px] transition-all duration-200 ease-in-out {className}"
75
83
  role="menu"
76
84
  onkeydown={handleKeydown}
77
85
  data-navigation-menu-content
@@ -30,7 +30,7 @@
30
30
  }
31
31
 
32
32
  const linkClasses =
33
- "block cursor-pointer rounded-md px-4 py-2 text-sm text-nav-menu-item no-underline transition-colors duration-150 outline-none hover:bg-nav-menu-hover hover:text-nav-menu-item-hover focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-nav-menu-focus {className}";
33
+ "focus-ring focus-ring--nav block cursor-pointer rounded-md px-4 py-2 text-sm text-nav-menu-item no-underline transition-colors duration-150 outline-none hover:bg-nav-menu-hover hover:text-nav-menu-item-hover {className}";
34
34
  </script>
35
35
 
36
36
  {#if asChild}
@@ -3,6 +3,7 @@
3
3
  import { getContext } from "svelte";
4
4
  import {
5
5
  NAVIGATION_MENU_CONTEXT_KEY,
6
+ navigationMenuPanelId,
6
7
  type NavigationMenuContextValue,
7
8
  } from "./navigation-menu-context.js";
8
9
 
@@ -25,6 +26,12 @@
25
26
 
26
27
  const isActive = $derived(context?.activeItem === value);
27
28
 
29
+ const panelId = $derived(
30
+ context
31
+ ? navigationMenuPanelId(context.menuInstanceId, value)
32
+ : "",
33
+ );
34
+
28
35
  function handleClick() {
29
36
  if (context) {
30
37
  if (isActive) {
@@ -51,8 +58,9 @@
51
58
 
52
59
  <button
53
60
  type="button"
54
- class="inline-flex cursor-pointer items-center justify-center gap-2 rounded-lg bg-transparent px-4 py-2 text-sm font-medium text-nav-menu-item transition-colors duration-150 outline-none hover:bg-nav-menu-hover hover:text-nav-menu-item-hover data-[active=true]:bg-nav-menu-active data-[active=true]:text-nav-menu-item-active focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-nav-menu-focus {className}"
61
+ class="focus-ring focus-ring--nav inline-flex cursor-pointer items-center justify-center gap-2 rounded-lg bg-transparent px-4 py-2 text-sm font-medium text-nav-menu-item transition-colors duration-150 outline-none hover:bg-nav-menu-hover hover:text-nav-menu-item-hover data-[active=true]:bg-nav-menu-active data-[active=true]:text-nav-menu-item-active {className}"
55
62
  aria-expanded={isActive ? 'true' : 'false'}
63
+ aria-controls={panelId || undefined}
56
64
  aria-haspopup="true"
57
65
  onclick={handleClick}
58
66
  onkeydown={handleKeydown}
@@ -0,0 +1,16 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from "svelte";
3
+
4
+ interface Props {
5
+ children?: Snippet;
6
+ className?: string;
7
+ }
8
+
9
+ let { children, className = "" }: Props = $props();
10
+ </script>
11
+
12
+ <div class="mx-auto w-full space-y-10 {className}">
13
+ {#if children}
14
+ {@render children()}
15
+ {/if}
16
+ </div>
@@ -0,0 +1,8 @@
1
+ import type { Snippet } from "svelte";
2
+ interface Props {
3
+ children?: Snippet;
4
+ className?: string;
5
+ }
6
+ declare const Page: import("svelte").Component<Props, {}, "">;
7
+ type Page = ReturnType<typeof Page>;
8
+ export default Page;
@@ -0,0 +1,61 @@
1
+ <script lang="ts">
2
+ import type { ComponentProp } from "../types/page.types";
3
+ import Table from "../atoms/Table.svelte";
4
+
5
+ interface Props {
6
+ props: ComponentProp[];
7
+ caption?: string;
8
+ }
9
+
10
+ let { props, caption = "Props / API" }: Props = $props();
11
+ </script>
12
+
13
+ {#if props.length === 0}
14
+ <div class="rounded-lg border border-border bg-base-50 px-4 py-4 text-sm text-description">
15
+ No documented props.
16
+ </div>
17
+ {:else}
18
+ <Table caption={caption}>
19
+ <thead>
20
+ <tr class="bg-base-50">
21
+ <th class="border-b border-border px-4 py-3 text-left font-semibold text-headline">
22
+ Name
23
+ </th>
24
+ <th class="border-b border-border px-4 py-3 text-left font-semibold text-headline">
25
+ Type
26
+ </th>
27
+ <th class="border-b border-border px-4 py-3 text-left font-semibold text-headline">
28
+ Required
29
+ </th>
30
+ <th class="border-b border-border px-4 py-3 text-left font-semibold text-headline">
31
+ Default
32
+ </th>
33
+ <th class="border-b border-border px-4 py-3 text-left font-semibold text-headline">
34
+ Description
35
+ </th>
36
+ </tr>
37
+ </thead>
38
+ <tbody>
39
+ {#each props as prop}
40
+ <tr class="hover:bg-base-50/70">
41
+ <td class="border-b border-border px-4 py-3 font-mono text-body">
42
+ {prop.name}
43
+ </td>
44
+ <td class="border-b border-border px-4 py-3 text-description">
45
+ {prop.type}
46
+ </td>
47
+ <td class="border-b border-border px-4 py-3 text-description">
48
+ {prop.required ? "Yes" : "No"}
49
+ </td>
50
+ <td class="border-b border-border px-4 py-3 text-description">
51
+ {prop.defaultValue || "—"}
52
+ </td>
53
+ <td class="border-b border-border px-4 py-3 text-description">
54
+ {prop.description}
55
+ </td>
56
+ </tr>
57
+ {/each}
58
+ </tbody>
59
+ </Table>
60
+ {/if}
61
+
@@ -0,0 +1,8 @@
1
+ import type { ComponentProp } from "../types/page.types";
2
+ interface Props {
3
+ props: ComponentProp[];
4
+ caption?: string;
5
+ }
6
+ declare const PropsTable: import("svelte").Component<Props, {}, "">;
7
+ type PropsTable = ReturnType<typeof PropsTable>;
8
+ export default PropsTable;
@@ -0,0 +1,197 @@
1
+ <script lang="ts">
2
+ import {
3
+ RADIO_CHECKED_DOT_CLASSES,
4
+ RADIO_GROUP_CONTROL_SHELL,
5
+ RADIO_GROUP_OPTION_LABEL_ROW,
6
+ RADIO_GROUP_RING_OVERLAY,
7
+ } from "../atoms/selection-control.styles";
8
+ import { generateId } from "../util/ssr-safe.js";
9
+ import { SvelteMap } from "svelte/reactivity";
10
+
11
+ export type RadioGroupOption = {
12
+ value: string;
13
+ label: string;
14
+ disabled?: boolean;
15
+ description?: string;
16
+ };
17
+
18
+ interface Props {
19
+ options: RadioGroupOption[];
20
+ /** Selected value; `bind:value`. Use `undefined` for no selection. */
21
+ value?: string | undefined;
22
+ /** Uncontrolled initial value. */
23
+ defaultValue?: string | undefined;
24
+ name?: string;
25
+ disabled?: boolean;
26
+ legend?: string;
27
+ label?: string;
28
+ }
29
+
30
+ let {
31
+ options,
32
+ defaultValue,
33
+ value = $bindable(defaultValue),
34
+ name: nameProp,
35
+ disabled = false,
36
+ legend = "",
37
+ label = "",
38
+ ...restProps
39
+ }: Props = $props();
40
+
41
+ const fallbackName = generateId("radiogroup");
42
+ const groupName = $derived(nameProp || fallbackName);
43
+ const legendText = $derived(legend || label);
44
+
45
+ const enabledOptions = $derived.by(() =>
46
+ options.filter((o: RadioGroupOption) => !disabled && !o.disabled),
47
+ );
48
+
49
+ const fallbackValue = $derived.by(() => enabledOptions[0]?.value ?? "");
50
+
51
+ /** False if controlled `value` is set but missing from `options` (stale); roving tabindex then follows first enabled instead. */
52
+ const valueMatchesOption = $derived(
53
+ value === undefined ||
54
+ value === "" ||
55
+ options.some((o: RadioGroupOption) => o.value === value),
56
+ );
57
+
58
+ function isOptionDisabled(option: RadioGroupOption): boolean {
59
+ return disabled || !!option.disabled;
60
+ }
61
+
62
+ function tabIndexFor(option: RadioGroupOption): number {
63
+ if (isOptionDisabled(option)) return -1;
64
+ if (
65
+ value !== undefined &&
66
+ value !== "" &&
67
+ valueMatchesOption
68
+ ) {
69
+ return option.value === value ? 0 : -1;
70
+ }
71
+ return option.value === fallbackValue ? 0 : -1;
72
+ }
73
+
74
+ function handleChange(event: Event) {
75
+ if (disabled) return;
76
+ const target = event.target as HTMLInputElement;
77
+ value = target.value;
78
+ }
79
+
80
+ const elementsByValue = new SvelteMap<string, HTMLInputElement>();
81
+ function registerRadio(valueKey: string) {
82
+ return (node: HTMLInputElement) => {
83
+ elementsByValue.set(valueKey, node);
84
+ return () => {
85
+ elementsByValue.delete(valueKey);
86
+ };
87
+ };
88
+ }
89
+
90
+ function focusValue(nextValue: string) {
91
+ const el = elementsByValue.get(nextValue);
92
+ el?.focus();
93
+ }
94
+
95
+ function moveSelection(delta: number) {
96
+ const enabled = enabledOptions;
97
+ if (enabled.length === 0) return;
98
+
99
+ const rawIndex = enabled.findIndex((o) => o.value === value);
100
+ const currentIndex =
101
+ rawIndex === -1
102
+ ? delta > 0
103
+ ? -1
104
+ : enabled.length
105
+ : rawIndex;
106
+ const nextIndex =
107
+ (currentIndex + delta + enabled.length) % enabled.length;
108
+ const next = enabled[nextIndex];
109
+ if (!next) return;
110
+ value = next.value;
111
+ focusValue(next.value);
112
+ }
113
+
114
+ function handleKeydown(event: KeyboardEvent) {
115
+ const key = event.key;
116
+ if (
117
+ key !== "ArrowRight" &&
118
+ key !== "ArrowDown" &&
119
+ key !== "ArrowLeft" &&
120
+ key !== "ArrowUp" &&
121
+ key !== "Home" &&
122
+ key !== "End"
123
+ ) {
124
+ return;
125
+ }
126
+
127
+ const enabled = enabledOptions;
128
+ if (enabled.length === 0) return;
129
+
130
+ event.preventDefault();
131
+ if (key === "ArrowRight" || key === "ArrowDown") {
132
+ moveSelection(1);
133
+ } else if (key === "ArrowLeft" || key === "ArrowUp") {
134
+ moveSelection(-1);
135
+ } else if (key === "Home") {
136
+ value = enabled[0].value;
137
+ focusValue(enabled[0].value);
138
+ } else if (key === "End") {
139
+ const last = enabled[enabled.length - 1];
140
+ value = last.value;
141
+ focusValue(last.value);
142
+ }
143
+ }
144
+
145
+ const optionLabelWrapperClasses = RADIO_GROUP_OPTION_LABEL_ROW;
146
+
147
+ const radioControlShellClasses = RADIO_GROUP_CONTROL_SHELL;
148
+
149
+ const radioRingOverlayClasses = RADIO_GROUP_RING_OVERLAY;
150
+ </script>
151
+
152
+ <fieldset
153
+ class="space-y-3"
154
+ aria-disabled={disabled ? "true" : undefined}
155
+ onkeydown={handleKeydown}
156
+ {...restProps}
157
+ >
158
+ {#if legendText}
159
+ <legend class="text-sm font-medium text-label mb-1">
160
+ {legendText}
161
+ </legend>
162
+ {/if}
163
+
164
+ <div class="space-y-3">
165
+ {#each options as option (option.value)}
166
+ {@const optionDisabled = isOptionDisabled(option)}
167
+ {@const isChecked = value === option.value}
168
+ <label class={optionLabelWrapperClasses}>
169
+ <span class={radioControlShellClasses}>
170
+ <input
171
+ type="radio"
172
+ name={groupName}
173
+ value={option.value}
174
+ checked={isChecked}
175
+ disabled={optionDisabled}
176
+ tabindex={tabIndexFor(option)}
177
+ class="sr-only"
178
+ onchange={handleChange}
179
+ {@attach registerRadio(option.value)}
180
+ />
181
+ <span class={radioRingOverlayClasses}></span>
182
+ <span class={RADIO_CHECKED_DOT_CLASSES} aria-hidden="true"></span>
183
+ </span>
184
+ <span class="flex flex-col gap-1">
185
+ <span class="text-sm font-medium text-label">
186
+ {option.label}
187
+ </span>
188
+ {#if option.description}
189
+ <span class="text-sm text-description">
190
+ {option.description}
191
+ </span>
192
+ {/if}
193
+ </span>
194
+ </label>
195
+ {/each}
196
+ </div>
197
+ </fieldset>