zabi-components 7.0.1 → 8.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. package/README.md +46 -3
  2. package/THEMING.md +255 -15
  3. package/dist/atoms/ActionPanel.svelte +12 -7
  4. package/dist/atoms/ActionPanel.svelte.d.ts +2 -0
  5. package/dist/atoms/Badge.svelte +93 -56
  6. package/dist/atoms/Badge.svelte.d.ts +15 -3
  7. package/dist/atoms/Button.svelte +49 -49
  8. package/dist/atoms/Button.svelte.d.ts +3 -0
  9. package/dist/atoms/Card.svelte +31 -28
  10. package/dist/atoms/Card.svelte.d.ts +5 -2
  11. package/dist/atoms/CardContent.svelte +10 -2
  12. package/dist/atoms/CardContent.svelte.d.ts +2 -0
  13. package/dist/atoms/CardFooter.svelte +10 -2
  14. package/dist/atoms/CardFooter.svelte.d.ts +2 -0
  15. package/dist/atoms/CardHeader.svelte +10 -2
  16. package/dist/atoms/CardHeader.svelte.d.ts +2 -0
  17. package/dist/atoms/Checkbox.svelte +4 -0
  18. package/dist/atoms/Checkbox.svelte.d.ts +2 -0
  19. package/dist/atoms/CodeBlock.svelte +15 -7
  20. package/dist/atoms/CodeBlock.svelte.d.ts +9 -3
  21. package/dist/atoms/ColorPicker.svelte +19 -9
  22. package/dist/atoms/ColorPicker.svelte.d.ts +3 -1
  23. package/dist/atoms/Container.svelte +2 -3
  24. package/dist/atoms/Divider.svelte +4 -4
  25. package/dist/atoms/FeatureCard.svelte +4 -3
  26. package/dist/atoms/Heading.svelte +56 -27
  27. package/dist/atoms/Heading.svelte.d.ts +17 -4
  28. package/dist/atoms/IconButton.svelte +30 -37
  29. package/dist/atoms/Input.svelte +26 -34
  30. package/dist/atoms/List.svelte +10 -2
  31. package/dist/atoms/List.svelte.d.ts +2 -0
  32. package/dist/atoms/ListItem.svelte +2 -2
  33. package/dist/atoms/ListItemLeading.svelte +4 -2
  34. package/dist/atoms/ListItemLeading.svelte.d.ts +1 -1
  35. package/dist/atoms/OptimizedImage.svelte +11 -2
  36. package/dist/atoms/OptimizedImage.svelte.d.ts +2 -0
  37. package/dist/atoms/Progress.svelte +4 -1
  38. package/dist/atoms/Progress.svelte.d.ts +2 -0
  39. package/dist/atoms/Radio.svelte +4 -0
  40. package/dist/atoms/Radio.svelte.d.ts +2 -0
  41. package/dist/atoms/Select.svelte +65 -67
  42. package/dist/atoms/Select.svelte.d.ts +4 -0
  43. package/dist/atoms/SelectionControl.svelte +5 -1
  44. package/dist/atoms/SelectionControl.svelte.d.ts +2 -0
  45. package/dist/atoms/Skeleton.svelte +5 -3
  46. package/dist/atoms/Table.svelte +2 -1
  47. package/dist/atoms/Text.svelte +34 -5
  48. package/dist/atoms/Text.svelte.d.ts +7 -1
  49. package/dist/atoms/Textarea.svelte +7 -6
  50. package/dist/atoms/ThemeToggle.svelte +17 -14
  51. package/dist/atoms/ThemeToggle.svelte.d.ts +2 -0
  52. package/dist/atoms/Toast.svelte +10 -8
  53. package/dist/atoms/Toggle.svelte +21 -13
  54. package/dist/atoms/Toggle.svelte.d.ts +7 -0
  55. package/dist/atoms/Tooltip.svelte +89 -57
  56. package/dist/atoms/Tooltip.svelte.d.ts +15 -0
  57. package/dist/components/index.d.ts +3 -1
  58. package/dist/components/index.d.ts.map +1 -1
  59. package/dist/components/organisms/index.d.ts +1 -0
  60. package/dist/components/organisms/index.d.ts.map +1 -1
  61. package/dist/components/util/cn.d.ts +8 -0
  62. package/dist/components/util/cn.d.ts.map +1 -0
  63. package/dist/components/util/ssr-safe.d.ts +4 -4
  64. package/dist/components/util/ssr-safe.d.ts.map +1 -1
  65. package/dist/index.d.ts +3 -1
  66. package/dist/index.js +3 -6
  67. package/dist/molecules/Alert.svelte +43 -64
  68. package/dist/molecules/Alert.svelte.d.ts +7 -1
  69. package/dist/molecules/ComponentDemo.svelte +10 -2
  70. package/dist/molecules/ComponentDemo.svelte.d.ts +2 -0
  71. package/dist/molecules/ContactForm.svelte +12 -2
  72. package/dist/molecules/ContactForm.svelte.d.ts +2 -0
  73. package/dist/molecules/Dropdown.svelte +40 -11
  74. package/dist/molecules/Dropdown.svelte.d.ts +4 -0
  75. package/dist/molecules/EmptyState.svelte +2 -1
  76. package/dist/molecules/Form.svelte +25 -14
  77. package/dist/molecules/Form.svelte.d.ts +13 -5
  78. package/dist/molecules/FormField.svelte +2 -1
  79. package/dist/molecules/Header.svelte +13 -3
  80. package/dist/molecules/Header.svelte.d.ts +2 -0
  81. package/dist/molecules/ImageUpload.svelte +54 -15
  82. package/dist/molecules/ImageUpload.svelte.d.ts +16 -5
  83. package/dist/molecules/Modal.svelte +110 -31
  84. package/dist/molecules/Modal.svelte.d.ts +9 -6
  85. package/dist/molecules/NavigationMenu.svelte +15 -3
  86. package/dist/molecules/NavigationMenu.svelte.d.ts +4 -0
  87. package/dist/molecules/NavigationMenuContent.svelte +36 -38
  88. package/dist/molecules/NavigationMenuContent.svelte.d.ts +2 -0
  89. package/dist/molecules/NavigationMenuItem.svelte +10 -2
  90. package/dist/molecules/NavigationMenuItem.svelte.d.ts +2 -0
  91. package/dist/molecules/NavigationMenuLink.svelte +10 -2
  92. package/dist/molecules/NavigationMenuLink.svelte.d.ts +2 -0
  93. package/dist/molecules/NavigationMenuList.svelte +10 -2
  94. package/dist/molecules/NavigationMenuList.svelte.d.ts +2 -0
  95. package/dist/molecules/NavigationMenuTrigger.svelte +16 -3
  96. package/dist/molecules/NavigationMenuTrigger.svelte.d.ts +2 -0
  97. package/dist/molecules/Page.svelte +8 -2
  98. package/dist/molecules/Page.svelte.d.ts +2 -0
  99. package/dist/molecules/PropsTable.svelte +6 -3
  100. package/dist/molecules/PropsTable.svelte.d.ts +2 -0
  101. package/dist/molecules/RadioGroup.svelte +5 -1
  102. package/dist/molecules/RadioGroup.svelte.d.ts +2 -0
  103. package/dist/molecules/Section.svelte +12 -4
  104. package/dist/molecules/Section.svelte.d.ts +3 -1
  105. package/dist/molecules/SidebarBrandHeader.svelte +13 -4
  106. package/dist/molecules/SidebarBrandHeader.svelte.d.ts +2 -0
  107. package/dist/molecules/SidebarFooter.svelte +12 -4
  108. package/dist/molecules/SidebarFooter.svelte.d.ts +2 -0
  109. package/dist/molecules/SidebarNavSection.svelte +12 -4
  110. package/dist/molecules/SidebarNavSection.svelte.d.ts +2 -0
  111. package/dist/molecules/SlideUp.svelte +85 -21
  112. package/dist/molecules/SlideUp.svelte.d.ts +5 -4
  113. package/dist/molecules/Tabs.svelte +25 -19
  114. package/dist/molecules/Tabs.svelte.d.ts +8 -4
  115. package/dist/molecules/Toaster.svelte +3 -2
  116. package/dist/molecules/ToasterToast.svelte +41 -10
  117. package/dist/molecules/ToasterToast.svelte.d.ts +2 -0
  118. package/dist/organisms/SidebarAccountPanel.svelte +4 -0
  119. package/dist/organisms/SidebarAccountPanel.svelte.d.ts +2 -0
  120. package/dist/organisms/SidebarNavigation.svelte +102 -59
  121. package/dist/organisms/SidebarNavigation.svelte.d.ts +2 -0
  122. package/dist/organisms/SidebarPanel.svelte +19 -11
  123. package/dist/organisms/SidebarPanel.svelte.d.ts +2 -0
  124. package/dist/organisms/SidebarShell.svelte +112 -0
  125. package/dist/organisms/SidebarShell.svelte.d.ts +52 -0
  126. package/dist/organisms/TopNavbar.svelte +88 -17
  127. package/dist/organisms/TopNavbar.svelte.d.ts +2 -0
  128. package/dist/organisms/index.d.ts +1 -0
  129. package/dist/organisms/index.js +1 -0
  130. package/dist/routes/lib/variant-utils.d.ts +26 -0
  131. package/dist/routes/lib/{variant-utils.ts → variant-utils.js} +15 -46
  132. package/dist/types/events.js +1 -0
  133. package/dist/types/index.js +3 -0
  134. package/dist/util/cn.d.ts +7 -0
  135. package/dist/util/cn.js +48 -0
  136. package/dist/util/ssr-safe.d.ts +4 -4
  137. package/dist/zabi-components-colors.css +3 -437
  138. package/dist/zabi-components-theme-dark-only.css +223 -103
  139. package/dist/zabi-components-theme-dark.css +223 -103
  140. package/dist/zabi-components-theme-only.css +347 -107
  141. package/dist/zabi-components-theme.css +347 -107
  142. package/dist/zabi-components.css +953 -843
  143. package/docs/lucide-icons.md +3 -1
  144. package/package.json +20 -25
  145. package/dist/lib/layout-width-tokens.d.ts +0 -17
  146. package/dist/lib/layout-width-tokens.d.ts.map +0 -1
  147. package/dist/lib/showcase/component-docs/Button.d.ts +0 -3
  148. package/dist/lib/showcase/component-docs/Button.d.ts.map +0 -1
  149. package/dist/lib/showcase/component-docs/Input.d.ts +0 -3
  150. package/dist/lib/showcase/component-docs/Input.d.ts.map +0 -1
  151. package/dist/lib/showcase/component-docs/List.d.ts +0 -3
  152. package/dist/lib/showcase/component-docs/List.d.ts.map +0 -1
  153. package/dist/lib/showcase/component-docs/Modal.d.ts +0 -3
  154. package/dist/lib/showcase/component-docs/Modal.d.ts.map +0 -1
  155. package/dist/lib/showcase/component-docs/Radio.d.ts +0 -3
  156. package/dist/lib/showcase/component-docs/Radio.d.ts.map +0 -1
  157. package/dist/lib/showcase/component-docs/SidebarNavigation.d.ts +0 -3
  158. package/dist/lib/showcase/component-docs/SidebarNavigation.d.ts.map +0 -1
  159. package/dist/lib/showcase/component-docs/Skeleton.d.ts +0 -3
  160. package/dist/lib/showcase/component-docs/Skeleton.d.ts.map +0 -1
  161. package/dist/lib/showcase/component-docs/_shared.d.ts +0 -12
  162. package/dist/lib/showcase/component-docs/_shared.d.ts.map +0 -1
  163. package/dist/lib/showcase/component-docs/index.d.ts +0 -4
  164. package/dist/lib/showcase/component-docs/index.d.ts.map +0 -1
  165. package/dist/lib/showcase/components-catalog.d.ts +0 -3
  166. package/dist/lib/showcase/components-catalog.d.ts.map +0 -1
  167. package/dist/lib/showcase/components-showcase-constants.d.ts +0 -89
  168. package/dist/lib/showcase/components-showcase-constants.d.ts.map +0 -1
  169. package/dist/lib/showcase/docs-sidebar-helpers.d.ts +0 -15
  170. package/dist/lib/showcase/docs-sidebar-helpers.d.ts.map +0 -1
  171. package/dist/routes/lib/focus-utils.d.ts +0 -12
  172. package/dist/routes/lib/focus-utils.ts +0 -110
  173. package/dist/routes/lib/ssr-safe.ts +0 -58
  174. package/scripts/fix-lucide-svelte-icon-dts.js +0 -35
@@ -1,40 +1,66 @@
1
1
  <script lang="ts">
2
+ import type { Snippet } from 'svelte';
2
3
  import {
3
4
  focusFirstElement,
5
+ getFocusableElements,
4
6
  returnFocus,
5
7
  saveFocus,
6
- trapFocus,
7
8
  } from '../util/focus-utils.js';
8
9
  import { generateId } from "../util/ssr-safe.js";
10
+ import { cn } from "../util/cn.js";
9
11
 
10
12
  interface Props {
13
+ /** Extra classes for the host element. */
14
+ class?: string;
11
15
  isOpen?: boolean;
12
16
  title?: string;
13
17
  onclick?: (event: Event) => void;
14
18
  onkeydown?: (event: Event) => void;
19
+ children?: Snippet;
15
20
  }
16
21
 
17
22
  let {
23
+ class: className = "",
18
24
  isOpen = $bindable(false),
19
25
  title = '',
20
26
  onclick,
21
27
  onkeydown,
22
28
  children,
23
29
  ...restProps
24
- } = $props<Props & { children?: any }>();
30
+ }: Props = $props();
25
31
 
26
32
  const slideTitleId = generateId('slideup-title');
27
33
 
28
34
  let slideUpContainer = $state<HTMLDivElement>();
29
- let cleanupFocusTrap: (() => void) | null = null;
35
+ let focusActive = false;
36
+
37
+ /** Ref-counted on `<body>`; shares the counter with Modal so nested overlays unlock once. */
38
+ function lockBodyScroll(): () => void {
39
+ const body = document.body;
40
+ const count = Number(body.dataset.zabiScrollLock ?? '0');
41
+ if (count === 0) {
42
+ body.dataset.zabiScrollLockOverflow = body.style.overflow;
43
+ body.style.overflow = 'hidden';
44
+ }
45
+ body.dataset.zabiScrollLock = String(count + 1);
46
+ return () => {
47
+ const next = Number(body.dataset.zabiScrollLock ?? '1') - 1;
48
+ if (next <= 0) {
49
+ body.style.overflow = body.dataset.zabiScrollLockOverflow ?? '';
50
+ delete body.dataset.zabiScrollLock;
51
+ delete body.dataset.zabiScrollLockOverflow;
52
+ } else {
53
+ body.dataset.zabiScrollLock = String(next);
54
+ }
55
+ };
56
+ }
30
57
 
31
58
  function closeSlideUp(event?: Event) {
32
59
  isOpen = false;
33
- if (cleanupFocusTrap) {
34
- cleanupFocusTrap();
35
- cleanupFocusTrap = null;
60
+ if (focusActive) {
61
+ focusActive = false;
62
+ returnFocus();
36
63
  }
37
- returnFocus();
38
64
  if (onclick && event) {
39
65
  onclick(event);
40
66
  }
@@ -44,17 +70,51 @@
44
70
  const container = slideUpContainer;
45
71
  if (isOpen && container) {
46
72
  saveFocus();
47
- cleanupFocusTrap = trapFocus(container);
48
- setTimeout(() => {
73
+ focusActive = true;
74
+ const unlockScroll = lockBodyScroll();
75
+ const t = setTimeout(() => {
49
76
  focusFirstElement(container);
50
77
  }, 0);
51
- } else if (!isOpen && cleanupFocusTrap) {
52
- cleanupFocusTrap();
53
- cleanupFocusTrap = null;
54
- returnFocus();
78
+ return () => {
79
+ clearTimeout(t);
80
+ unlockScroll();
81
+ if (focusActive) {
82
+ focusActive = false;
83
+ returnFocus();
84
+ }
85
+ };
55
86
  }
56
87
  });
57
88
 
89
+ /** Re-queries focusables on every Tab so late-rendered content stays inside the trap. */
90
+ function handleTrapKeydown(event: KeyboardEvent) {
91
+ const container = slideUpContainer;
92
+ if (event.key !== 'Tab' || !container) return;
93
+ const owner = (event.target as Element | null)?.closest?.('[role="dialog"]');
94
+ if (owner && owner !== container) return;
95
+
96
+ const focusable = getFocusableElements(container);
97
+ if (focusable.length === 0) {
98
+ event.preventDefault();
99
+ container.focus();
100
+ return;
101
+ }
102
+ const first = focusable[0];
103
+ const last = focusable[focusable.length - 1];
104
+ const active = document.activeElement as HTMLElement | null;
105
+
106
+ if (!active || !focusable.includes(active)) {
107
+ event.preventDefault();
108
+ first.focus();
109
+ } else if (event.shiftKey && active === first) {
110
+ event.preventDefault();
111
+ last.focus();
112
+ } else if (!event.shiftKey && active === last) {
113
+ event.preventDefault();
114
+ first.focus();
115
+ }
116
+ }
117
+
58
118
  function handleBackdropClick(event: Event) {
59
119
  if (event.target === event.currentTarget) {
60
120
  closeSlideUp(event);
@@ -63,7 +123,8 @@
63
123
 
64
124
  function handleKeydown(event: Event) {
65
125
  const keyboardEvent = event as KeyboardEvent;
66
- if (keyboardEvent.key === 'Escape') {
126
+ if (keyboardEvent.key === 'Escape' && !keyboardEvent.defaultPrevented) {
127
+ keyboardEvent.preventDefault();
67
128
  closeSlideUp(event);
68
129
  }
69
130
  onkeydown?.(event);
@@ -71,19 +132,22 @@
71
132
  </script>
72
133
 
73
134
  {#if isOpen}
135
+ <!-- svelte-ignore a11y_click_events_have_key_events -->
74
136
  <div
75
137
  class="fixed inset-0 z-modal cursor-pointer bg-overlay"
76
138
  onclick={handleBackdropClick}
77
139
  onkeydown={handleKeydown}
78
- role="dialog"
79
- aria-modal="true"
80
- aria-labelledby={title ? slideTitleId : undefined}
81
- tabindex="-1"
82
- {...restProps}
140
+ role="presentation"
83
141
  >
84
142
  <div
85
143
  bind:this={slideUpContainer}
86
- class="fixed bottom-0 left-0 right-0 z-modal flex max-h-[90vh] cursor-default flex-col overflow-y-auto rounded-t-3xl bg-card shadow-xl animate-[slideUp_0.3s_ease-out]"
144
+ class={cn("fixed bottom-0 left-0 right-0 z-modal flex max-h-[90vh] cursor-default flex-col overflow-y-auto rounded-t-overlay border-t border-border-overlay bg-surface-overlay shadow-lg animate-[slideUp_0.3s_ease-out]", className)}
145
+ role="dialog"
146
+ aria-modal="true"
147
+ aria-labelledby={title ? slideTitleId : undefined}
148
+ tabindex="-1"
149
+ onkeydown={handleTrapKeydown}
150
+ {...restProps}
87
151
  >
88
152
  {#if title}
89
153
  <div class="flex items-center justify-between px-6 pb-4 pt-6">
@@ -96,7 +160,7 @@
96
160
  <button
97
161
  type="button"
98
162
  onclick={closeSlideUp}
99
- 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"
163
+ class="focus-ring flex h-8 w-8 cursor-pointer items-center justify-center rounded-full text-2xl text-description transition-colors hover:bg-surface-overlay-hover hover:text-headline"
100
164
  aria-label="Close"
101
165
  >
102
166
  ×
@@ -1,12 +1,13 @@
1
+ import type { Snippet } from 'svelte';
1
2
  interface Props {
3
+ /** Extra classes for the host element. */
4
+ class?: string;
2
5
  isOpen?: boolean;
3
6
  title?: string;
4
7
  onclick?: (event: Event) => void;
5
8
  onkeydown?: (event: Event) => void;
9
+ children?: Snippet;
6
10
  }
7
- type $$ComponentProps = Props & {
8
- children?: any;
9
- };
10
- declare const SlideUp: import("svelte").Component<$$ComponentProps, {}, "isOpen">;
11
+ declare const SlideUp: import("svelte").Component<Props, {}, "isOpen">;
11
12
  type SlideUp = ReturnType<typeof SlideUp>;
12
13
  export default SlideUp;
@@ -1,40 +1,49 @@
1
1
  <script lang="ts">
2
+ import type { Snippet } from "svelte";
2
3
  import { generateId } from "../util/ssr-safe.js";
4
+ import { cn } from "../util/cn.js";
3
5
 
4
6
  interface Props {
7
+ /** Extra classes for the host element. */
8
+ class?: string;
5
9
  tabs?: Array<{
6
10
  id: string;
7
11
  label: string;
8
12
  disabled?: boolean;
9
13
  }>;
14
+ /** Selected tab id; supports `bind:activeTab`. */
10
15
  activeTab?: string;
11
16
  variant?: "default" | "pills";
12
17
  onclick?: (event: Event) => void;
13
18
  onkeydown?: (event: Event) => void;
19
+ children?: Snippet<[{ activeTab: string }]>;
14
20
  }
15
21
 
16
22
  let {
23
+ class: className = "",
17
24
  tabs = [],
18
- activeTab = "",
25
+ activeTab = $bindable(""),
19
26
  variant = "default",
20
27
  children,
21
28
  ...restProps
22
- }: Props & { children?: any } = $props();
29
+ }: Props = $props();
23
30
 
24
31
  const tabsBaseId = generateId("tabs");
25
32
 
26
- function selectTab(tabId: string) {
33
+ /** Button refs keyed by tab id — roving tabindex needs focus to follow selection. */
34
+ const tabElements: Record<string, HTMLButtonElement | undefined> = {};
35
+
36
+ function selectTab(tabId: string, moveFocus = false) {
27
37
  activeTab = tabId;
38
+ if (moveFocus) {
39
+ tabElements[tabId]?.focus();
40
+ }
28
41
  }
29
42
 
30
43
  function getEnabledTabs() {
31
44
  return tabs.filter((tab) => !tab.disabled);
32
45
  }
33
46
 
34
- function getTabIndex(tabId: string) {
35
- return tabs.findIndex((tab) => tab.id === tabId);
36
- }
37
-
38
47
  function getTabId(tabId: string) {
39
48
  return `${tabsBaseId}-tab-${tabId}`;
40
49
  }
@@ -72,40 +81,37 @@
72
81
  const nextIndex =
73
82
  (fallbackIndex + direction + enabledTabs.length) %
74
83
  enabledTabs.length;
75
- selectTab(enabledTabs[nextIndex].id);
84
+ selectTab(enabledTabs[nextIndex].id, true);
76
85
  } else if (event.key === "Home") {
77
86
  event.preventDefault();
78
- selectTab(enabledTabs[0].id);
87
+ selectTab(enabledTabs[0].id, true);
79
88
  } else if (event.key === "End") {
80
89
  event.preventDefault();
81
- selectTab(enabledTabs[enabledTabs.length - 1].id);
82
- } else if (event.key === "Enter" || event.key === " ") {
83
- event.preventDefault();
84
- if (currentIndex === -1) {
85
- selectTab(enabledTabs[0].id);
86
- }
90
+ selectTab(enabledTabs[enabledTabs.length - 1].id, true);
87
91
  }
92
+ // Enter/Space: native <button> click already selects the tab.
88
93
  }
89
94
  </script>
90
95
 
91
- <div class="tabs-container">
96
+ <div class={cn("tabs-container", className)}>
92
97
  <div
93
98
  class="flex border-b border-base-200"
94
99
  role="tablist"
95
- tabindex="0"
100
+ tabindex="-1"
96
101
  onkeydown={handleKeydown}
97
102
  >
98
103
  {#each tabs as tab (tab.id)}
99
104
  <button
105
+ bind:this={tabElements[tab.id]}
100
106
  type="button"
101
107
  role="tab"
102
108
  id={getTabId(tab.id)}
103
- class="focus-ring cursor-pointer border-b-2 px-4 py-2 text-sm font-medium transition-colors focus:outline-none focus-visible:outline-none active:bg-base-100 disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:border-transparent disabled:hover:text-description {activeTab ===
109
+ class="focus-ring cursor-pointer border-b-2 px-4 py-2 text-sm font-medium transition-colors focus:outline-none focus-visible:outline-none active:bg-surface-active disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:border-transparent disabled:hover:text-description {activeTab ===
104
110
  tab.id
105
111
  ? variant === 'pills'
106
112
  ? 'border-brand-500 bg-brand-100 text-brand-700'
107
113
  : 'border-brand-500 text-body'
108
- : 'border-transparent text-description hover:border-base-300 hover:text-body'}"
114
+ : 'border-transparent text-description hover:border-border-medium hover:text-body'}"
109
115
  onclick={() => selectTab(tab.id)}
110
116
  disabled={tab.disabled}
111
117
  aria-selected={activeTab === tab.id}
@@ -1,17 +1,21 @@
1
+ import type { Snippet } from "svelte";
1
2
  interface Props {
3
+ /** Extra classes for the host element. */
4
+ class?: string;
2
5
  tabs?: Array<{
3
6
  id: string;
4
7
  label: string;
5
8
  disabled?: boolean;
6
9
  }>;
10
+ /** Selected tab id; supports `bind:activeTab`. */
7
11
  activeTab?: string;
8
12
  variant?: "default" | "pills";
9
13
  onclick?: (event: Event) => void;
10
14
  onkeydown?: (event: Event) => void;
15
+ children?: Snippet<[{
16
+ activeTab: string;
17
+ }]>;
11
18
  }
12
- type $$ComponentProps = Props & {
13
- children?: any;
14
- };
15
- declare const Tabs: import("svelte").Component<$$ComponentProps, {}, "">;
19
+ declare const Tabs: import("svelte").Component<Props, {}, "activeTab">;
16
20
  type Tabs = ReturnType<typeof Tabs>;
17
21
  export default Tabs;
@@ -1,6 +1,7 @@
1
1
  <script lang="ts">
2
2
  import ToasterToast from './ToasterToast.svelte';
3
3
  import { toastStore } from './toast-store.js';
4
+ import { cn } from "../util/cn.js";
4
5
 
5
6
  interface Props {
6
7
  class?: string;
@@ -10,11 +11,11 @@
10
11
  </script>
11
12
 
12
13
  <div
13
- class="pointer-events-none fixed bottom-4 right-4 z-toast flex min-w-0 w-[min(24rem,calc(100vw-2rem))] flex-col gap-2 p-4 {className}"
14
+ class={cn("pointer-events-none fixed bottom-4 right-4 z-toast flex min-w-0 w-[min(24rem,calc(100vw-2rem))] flex-col gap-2 p-4", className)}
14
15
  role="region"
15
16
  aria-label="Notifications"
16
- aria-live="polite"
17
17
  >
18
+ <!-- Each toast owns its role="status"/"alert"; a region-level aria-live would announce twice. -->
18
19
  {#each $toastStore as toast (toast.id)}
19
20
  <ToasterToast {toast} />
20
21
  {/each}
@@ -12,12 +12,15 @@
12
12
  } from '@lucide/svelte';
13
13
  import Button from '../atoms/Button.svelte';
14
14
  import { toastStore, type ToastItem } from './toast-store.js';
15
+ import { cn } from "../util/cn.js";
15
16
 
16
17
  interface Props {
18
+ /** Extra classes for the host element. */
19
+ class?: string;
17
20
  toast: ToastItem;
18
21
  }
19
22
 
20
- let { toast }: Props = $props();
23
+ let { class: className = "", toast }: Props = $props();
21
24
 
22
25
  /** Seconds until auto-dismiss; `0` means no countdown (manual dismiss only). */
23
26
  function autoDismissSeconds(duration: number | undefined): number {
@@ -31,6 +34,10 @@
31
34
  let count = $state(0);
32
35
  let timerActive = $state(false);
33
36
  let isExpanded = $state(false);
37
+ /** Hover or keyboard focus inside the toast pauses auto-dismiss (WCAG 2.2.1). */
38
+ let hovered = $state(false);
39
+ let focusWithin = $state(false);
40
+ const paused = $derived(hovered || focusWithin);
34
41
 
35
42
  let intervalRef: ReturnType<typeof setInterval> | undefined;
36
43
 
@@ -90,6 +97,7 @@
90
97
  timerActive = true;
91
98
 
92
99
  intervalRef = setInterval(() => {
100
+ if (paused) return;
93
101
  count -= 1;
94
102
  if (count <= 0) {
95
103
  stopTimer();
@@ -113,19 +121,38 @@
113
121
  isExpanded = false;
114
122
  }
115
123
 
124
+ function handleFocusOut(event: FocusEvent) {
125
+ const next = event.relatedTarget as Node | null;
126
+ if (!next || !(event.currentTarget as HTMLElement).contains(next)) {
127
+ focusWithin = false;
128
+ }
129
+ }
130
+
116
131
  const toastEnter = { y: 18, duration: 260, opacity: 0, easing: cubicOut };
117
132
  const toastLeave = { y: 14, duration: 300, opacity: 0, easing: cubicOut };
118
133
  </script>
119
134
 
120
135
  <div
121
- class="pointer-events-auto relative w-full min-w-[18rem] overflow-hidden rounded-2xl border border-border bg-surface-1 shadow-xl"
136
+ class={cn("pointer-events-auto relative w-full min-w-[18rem] overflow-hidden rounded-overlay border border-border-overlay bg-surface-overlay shadow-lg", className)}
122
137
  in:fly={toastEnter}
123
138
  out:fly={toastLeave}
124
- role={toast.type === 'error' ? 'alert' : 'status'}
139
+ onmouseenter={() => (hovered = true)}
140
+ onmouseleave={() => (hovered = false)}
141
+ onfocusin={() => (focusWithin = true)}
142
+ onfocusout={handleFocusOut}
143
+ role="group"
144
+ aria-label={headerTitle}
145
+ data-paused={paused}
125
146
  >
126
147
  <div class="p-4">
127
148
  <div class="flex items-start gap-3">
128
- <div class="shrink-0 pt-0.5">
149
+ <!-- Live region holds only title + message, so the countdown and buttons are never re-announced. -->
150
+ <div
151
+ class="flex min-w-0 flex-1 items-start gap-3"
152
+ role={toast.type === 'error' ? 'alert' : 'status'}
153
+ aria-atomic="true"
154
+ >
155
+ <div class="shrink-0 mt-0.5">
129
156
  {#if toast.type === 'success'}
130
157
  <CheckCircle class="size-5 {statusIconClass}" aria-hidden="true" />
131
158
  {:else if toast.type === 'error'}
@@ -138,12 +165,16 @@
138
165
  </div>
139
166
  <h4 class="min-w-0 flex-1 text-base font-semibold text-headline">
140
167
  {headerTitle}
168
+ {#if toast.message.trim() && toast.message.trim() !== headerTitle}
169
+ <span class="sr-only">{toast.message}</span>
170
+ {/if}
141
171
  </h4>
142
- <div class="flex shrink-0 items-center gap-0.5">
172
+ </div>
173
+ <div class="flex shrink-0 items-center gap-1">
143
174
  {#if hasExpandable}
144
175
  <button
145
176
  type="button"
146
- class="focus-ring cursor-pointer rounded-md p-1 text-description transition-colors hover:bg-base-100 hover:text-headline focus:outline-none"
177
+ class="focus-ring cursor-pointer rounded-control p-1 text-description transition-colors hover:bg-surface-overlay-hover hover:text-headline focus:outline-none"
147
178
  aria-expanded={isExpanded}
148
179
  aria-controls="toaster-expand-{toast.id}"
149
180
  aria-label={isExpanded ? 'Collapse details' : 'Expand details'}
@@ -159,7 +190,7 @@
159
190
  {/if}
160
191
  <button
161
192
  type="button"
162
- class="focus-ring cursor-pointer rounded-md p-1 text-description transition-colors hover:bg-base-100 hover:text-headline focus:outline-none"
193
+ class="focus-ring cursor-pointer rounded-control p-1 text-description transition-colors hover:bg-surface-overlay-hover hover:text-headline focus:outline-none"
163
194
  onclick={handleDismiss}
164
195
  aria-label="Dismiss notification"
165
196
  >
@@ -189,11 +220,11 @@
189
220
 
190
221
  {#if timerActive}
191
222
  <div class="px-4 pb-3" transition:fade={{ duration: 180 }}>
192
- <p class="text-xs text-description">
193
- This message will close in {count} seconds.
223
+ <p class="text-xs text-description" data-toast-countdown>
224
+ {paused ? 'Paused — closes' : 'This message will close'} in {count} seconds.
194
225
  <button
195
226
  type="button"
196
- class="focus-ring cursor-pointer rounded-sm text-link underline-offset-2 hover:underline focus:outline-none"
227
+ class="focus-ring cursor-pointer rounded-control text-link underline-offset-2 hover:underline focus:outline-none"
197
228
  onclick={stopTimer}
198
229
  >
199
230
  Click to stop
@@ -1,5 +1,7 @@
1
1
  import { type ToastItem } from './toast-store.js';
2
2
  interface Props {
3
+ /** Extra classes for the host element. */
4
+ class?: string;
3
5
  toast: ToastItem;
4
6
  }
5
7
  declare const ToasterToast: import("svelte").Component<Props, {}, "">;
@@ -6,6 +6,8 @@
6
6
  import { LogOut, Moon, Sun, User } from "@lucide/svelte";
7
7
 
8
8
  interface Props {
9
+ /** Extra classes for the host element. */
10
+ class?: string;
9
11
  /** Target id for `aria-controls` from the sidebar footer/nav. */
10
12
  panelId?: string;
11
13
  style?: string;
@@ -24,6 +26,7 @@
24
26
  }
25
27
 
26
28
  let {
29
+ class: className = "",
27
30
  panelId = generateId("account-panel"),
28
31
  style = "",
29
32
  profileName = "Account",
@@ -96,6 +99,7 @@
96
99
  <div
97
100
  id={panelId}
98
101
  {style}
102
+ class={className}
99
103
  role="dialog"
100
104
  aria-modal="false"
101
105
  aria-label="Account panel"
@@ -1,4 +1,6 @@
1
1
  interface Props {
2
+ /** Extra classes for the host element. */
3
+ class?: string;
2
4
  /** Target id for `aria-controls` from the sidebar footer/nav. */
3
5
  panelId?: string;
4
6
  style?: string;