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,6 +1,10 @@
1
1
  <script lang="ts">
2
2
  import ThemeToggle from "../atoms/ThemeToggle.svelte";
3
+ import IconButton from "../atoms/IconButton.svelte";
4
+ import { ExternalLink, Menu, X } from "@lucide/svelte";
3
5
  import type { Component, Snippet } from "svelte";
6
+ import { generateId } from "../util/ssr-safe.js";
7
+ import { cn } from "../util/cn.js";
4
8
 
5
9
  export interface TopNavbarNavItem {
6
10
  label: string;
@@ -15,6 +19,8 @@
15
19
  /** `aria-label` on `<nav>` when multiple nav landmarks exist. */
16
20
  ariaLabel?: string;
17
21
  showThemeToggle?: boolean;
22
+ class?: string;
23
+ /** @deprecated use `class`. */
18
24
  className?: string;
19
25
  /** Slim mode: only the `nav` region (no full chrome bar). */
20
26
  embedded?: boolean;
@@ -30,7 +36,8 @@
30
36
  brandHref,
31
37
  ariaLabel,
32
38
  showThemeToggle = true,
33
- className = "",
39
+ class: classAttr = "",
40
+ className: legacyClass = "",
34
41
  embedded = false,
35
42
  items = [],
36
43
  navVariant = "header",
@@ -42,12 +49,24 @@
42
49
  ...restProps
43
50
  }: Props & { nav?: Snippet; actions?: Snippet } = $props();
44
51
 
52
+ /** `class` is the public prop; `className` is a deprecated alias.
53
+ * Both are merged here so existing call sites keep working. */
54
+ const className = $derived(cn(`${classAttr} ${legacyClass}`));
55
+
56
+ const mobileMenuId = generateId("topnavbar-menu");
57
+
45
58
  let isMenuOpen = $state(false);
46
59
 
47
60
  function toggleMenu() {
48
61
  isMenuOpen = !isMenuOpen;
49
62
  }
50
63
 
64
+ function handleWindowKeydown(event: KeyboardEvent) {
65
+ if (event.key === "Escape" && isMenuOpen) {
66
+ isMenuOpen = false;
67
+ }
68
+ }
69
+
51
70
  function handleNavLinkClick(event: MouseEvent) {
52
71
  if (preventNavigation) {
53
72
  event.preventDefault();
@@ -57,6 +76,11 @@
57
76
  }
58
77
  }
59
78
 
79
+ /** Absolute URLs leave the site, so they are marked and open in a new tab. */
80
+ function isExternal(href: string): boolean {
81
+ return href.startsWith("http://") || href.startsWith("https://");
82
+ }
83
+
60
84
  /** App routes: prefix match except `/` and absolute URLs (exact match). */
61
85
  function isNavItemActive(href: string): boolean {
62
86
  if (href.startsWith("http://") || href.startsWith("https://")) {
@@ -78,8 +102,30 @@
78
102
  return "flex flex-col gap-1 grow h-full items-center justify-center min-h-0 min-w-0 relative shrink-0 cursor-pointer w-full md:w-auto";
79
103
  }
80
104
 
81
- function getIconContainerClasses(): string {
82
- return "focus-ring focus-ring--nav group/nav-item flex flex-col items-center justify-center overflow-clip relative rounded-[20px] shrink-0 text-nav-menu-item transition-colors duration-200 outline-none hover:bg-nav-menu-hover hover:text-nav-menu-item-hover aria-[current=page]:bg-nav-menu-active aria-[current=page]:text-nav-menu-item-active";
105
+ /**
106
+ * The active state is resolved here rather than with
107
+ * `aria-[current=page]:text-nav-menu-item-active`.
108
+ *
109
+ * That variant was dead: the semantic utilities are hand-written in
110
+ * app.css and sit outside Tailwind's cascade layer, so an unlayered
111
+ * `.text-nav-menu-item` beats anything Tailwind generates for a variant of
112
+ * the same property. The fill happened to survive only because no plain
113
+ * `bg-*` class competed with it — so the selected tab painted its pill but
114
+ * kept the idle label colour (#27272a where #3c52ba was intended), which
115
+ * is exactly the "active state does almost nothing" failure the sidebar
116
+ * had.
117
+ *
118
+ * Plain classes chosen in JS cannot lose that way.
119
+ */
120
+ function getIconContainerClasses(isActive: boolean): string {
121
+ const base =
122
+ "focus-ring focus-ring--nav group/nav-item flex flex-col items-center justify-center overflow-clip relative rounded-pill shrink-0 transition-colors duration-200 outline-none";
123
+ return isActive
124
+ ? cn(base, "bg-nav-menu-active text-nav-menu-item-active")
125
+ : cn(
126
+ base,
127
+ "text-nav-menu-item hover:bg-nav-menu-hover hover:text-nav-menu-item-hover",
128
+ );
83
129
  }
84
130
 
85
131
  function getStateLayerClasses(): string {
@@ -87,7 +133,9 @@
87
133
  }
88
134
 
89
135
  function getLabelClasses(): string {
90
- return "font-medium leading-4 relative shrink-0 text-center text-nowrap tracking-[0.5px] whitespace-pre text-xs text-inherit";
136
+ // `whitespace-pre` used to sit beside `text-nowrap` — same property,
137
+ // and it preserved literal whitespace as a side effect.
138
+ return "font-medium leading-4 relative shrink-0 text-center text-nowrap tracking-wide text-xs text-inherit";
91
139
  }
92
140
 
93
141
  function getIconWrapperClasses(): string {
@@ -99,12 +147,15 @@
99
147
  <ul class="{ulClasses} list-none m-0 p-0">
100
148
  {#each items as item (item.href)}
101
149
  {@const isActive = isNavItemActive(item.href)}
150
+ {@const external = isExternal(item.href)}
102
151
  <li class={getNavItemClasses()}>
103
152
  <a
104
153
  href={item.href}
105
- class={getIconContainerClasses()}
154
+ class={getIconContainerClasses(isActive)}
106
155
  onclick={handleNavLinkClick}
107
156
  aria-current={isActive ? "page" : undefined}
157
+ target={external ? "_blank" : undefined}
158
+ rel={external ? "noopener noreferrer" : undefined}
108
159
  >
109
160
  <div class={getStateLayerClasses()}>
110
161
  {#if item.iconFilled && isActive}
@@ -118,9 +169,17 @@
118
169
  <Icon size={16} class="w-4 h-4" />
119
170
  </div>
120
171
  {/if}
121
- <p class={getLabelClasses()}>
172
+ <span class={getLabelClasses()}>
122
173
  {item.label}
123
- </p>
174
+ </span>
175
+ {#if external}
176
+ <ExternalLink
177
+ size={12}
178
+ class="shrink-0 text-current opacity-70"
179
+ aria-hidden="true"
180
+ />
181
+ <span class="sr-only">(opens in a new tab)</span>
182
+ {/if}
124
183
  </div>
125
184
  </a>
126
185
  </li>
@@ -128,6 +187,8 @@
128
187
  </ul>
129
188
  {/snippet}
130
189
 
190
+ <svelte:window onkeydown={handleWindowKeydown} />
191
+
131
192
  {#if embedded}
132
193
  <nav class={className} aria-label={ariaLabel} {...restProps}>
133
194
  {#if nav}
@@ -138,7 +199,7 @@
138
199
  </nav>
139
200
  {:else}
140
201
  <nav
141
- class="border-b border-border bg-background sticky top-0 z-50 {className}"
202
+ class={cn("border-b border-border bg-background sticky top-0 z-sticky", className)}
142
203
  aria-label={ariaLabel}
143
204
  {...restProps}
144
205
  >
@@ -148,7 +209,7 @@
148
209
  {#if brandHref}
149
210
  <a
150
211
  href={brandHref}
151
- class="focus-ring text-xl font-bold text-headline hover:opacity-90"
212
+ class="focus-ring text-xl font-bold text-headline transition-colors hover:text-link"
152
213
  >
153
214
  {brand}
154
215
  </a>
@@ -158,7 +219,7 @@
158
219
  </div>
159
220
 
160
221
  <div class="hidden md:block">
161
- <div class="ml-10 flex items-baseline space-x-4">
222
+ <div class="ml-10 flex items-center space-x-4">
162
223
  {#if nav}
163
224
  {@render nav()}
164
225
  {:else if items.length > 0}
@@ -177,19 +238,29 @@
177
238
  </div>
178
239
 
179
240
  <div class="md:hidden">
180
- <button
181
- type="button"
182
- class="cursor-pointer text-description hover:text-body"
241
+ <!--
242
+ Was a bare ☰ glyph in a text-2xl span: font-dependent,
243
+ off the icon scale, and with no disclosure semantics, so
244
+ a screen reader could not tell the menu was open.
245
+ -->
246
+ <IconButton
247
+ variant="ghost"
248
+ label={isMenuOpen ? "Close menu" : "Open menu"}
183
249
  onclick={toggleMenu}
184
- aria-label="Toggle menu"
250
+ aria-expanded={isMenuOpen}
251
+ aria-controls={mobileMenuId}
185
252
  >
186
- <span class="text-2xl">☰</span>
187
- </button>
253
+ {#if isMenuOpen}
254
+ <X size={20} />
255
+ {:else}
256
+ <Menu size={20} />
257
+ {/if}
258
+ </IconButton>
188
259
  </div>
189
260
  </div>
190
261
 
191
262
  {#if isMenuOpen}
192
- <div class="md:hidden">
263
+ <div class="md:hidden" id={mobileMenuId}>
193
264
  <div
194
265
  class="px-2 pt-2 pb-3 space-y-1 sm:px-3 border-t border-border"
195
266
  >
@@ -17,6 +17,8 @@ interface Props {
17
17
  /** `aria-label` on `<nav>` when multiple nav landmarks exist. */
18
18
  ariaLabel?: string;
19
19
  showThemeToggle?: boolean;
20
+ class?: string;
21
+ /** @deprecated use `class`. */
20
22
  className?: string;
21
23
  /** Slim mode: only the `nav` region (no full chrome bar). */
22
24
  embedded?: boolean;
@@ -1,4 +1,5 @@
1
1
  export { default as TopNavbar } from './TopNavbar.svelte';
2
2
  export { default as SidebarNavigation } from './SidebarNavigation.svelte';
3
+ export { default as SidebarShell } from './SidebarShell.svelte';
3
4
  export { default as SidebarAccountPanel } from './SidebarAccountPanel.svelte';
4
5
  export { default as SidebarPanel } from './SidebarPanel.svelte';
@@ -1,4 +1,5 @@
1
1
  export { default as TopNavbar } from './TopNavbar.svelte';
2
2
  export { default as SidebarNavigation } from './SidebarNavigation.svelte';
3
+ export { default as SidebarShell } from './SidebarShell.svelte';
3
4
  export { default as SidebarAccountPanel } from './SidebarAccountPanel.svelte';
4
5
  export { default as SidebarPanel } from './SidebarPanel.svelte';
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Variant class helpers — semantic design tokens only (see src/app.css).
3
+ */
4
+ import type { SemanticVariant } from '../../types/variants.js';
5
+ export type Variant = SemanticVariant;
6
+ /**
7
+ * Border + focus ring classes for text inputs (Input, Textarea).
8
+ */
9
+ export declare function getInputVariantClasses(variant: Variant): string;
10
+ /**
11
+ * Card surfaces — border + background using semantic tokens.
12
+ */
13
+ export declare function getCardVariantClasses(variant: Variant): string;
14
+ /**
15
+ * Generic variant classes for borders, text, or backgrounds.
16
+ */
17
+ export declare function getVariantClasses(variant: Variant, type: 'border' | 'text' | 'bg'): string;
18
+ export declare function getAllVariantClasses(variant: Variant): {
19
+ border: string;
20
+ text: string;
21
+ bg: string;
22
+ };
23
+ /**
24
+ * Create a variant class map for use in reactive statements
25
+ */
26
+ export declare function createVariantClassMap<T extends string>(variants: T[], prefix: string): Record<T, string>;
@@ -1,55 +1,37 @@
1
1
  /**
2
2
  * Variant class helpers — semantic design tokens only (see src/app.css).
3
3
  */
4
-
5
- import type { SemanticVariant } from '../../types/variants.js';
6
-
7
- export type Variant = SemanticVariant;
8
-
9
4
  /**
10
5
  * Border + focus ring classes for text inputs (Input, Textarea).
11
6
  */
12
- export function getInputVariantClasses(variant: Variant): string {
13
- const variantMap: Record<Variant, string> = {
14
- default:
15
- 'focus-ring border-input-border focus-visible:border-input-border',
16
- success:
17
- 'focus-ring border-success focus-visible:border-success',
18
- warning:
19
- 'focus-ring border-warning focus-visible:border-warning',
7
+ export function getInputVariantClasses(variant) {
8
+ const variantMap = {
9
+ default: 'focus-ring border-input-border focus-visible:border-input-border',
10
+ success: 'focus-ring border-success focus-visible:border-success',
11
+ warning: 'focus-ring border-warning focus-visible:border-warning',
20
12
  error: 'focus-ring border-error focus-visible:border-error',
21
13
  info: 'focus-ring border-info focus-visible:border-info',
22
14
  };
23
-
24
15
  return variantMap[variant] || variantMap.default;
25
16
  }
26
-
27
17
  /**
28
18
  * Card surfaces — border + background using semantic tokens.
29
19
  */
30
- export function getCardVariantClasses(variant: Variant): string {
31
- const variantMap: Record<Variant, string> = {
20
+ export function getCardVariantClasses(variant) {
21
+ const variantMap = {
32
22
  default: 'border-border bg-card',
33
23
  success: 'border-success bg-card',
34
24
  warning: 'border-warning bg-card',
35
25
  error: 'border-error bg-card',
36
26
  info: 'border-info bg-card',
37
27
  };
38
-
39
28
  return variantMap[variant] || variantMap.default;
40
29
  }
41
-
42
30
  /**
43
31
  * Generic variant classes for borders, text, or backgrounds.
44
32
  */
45
- export function getVariantClasses(
46
- variant: Variant,
47
- type: 'border' | 'text' | 'bg',
48
- ): string {
49
- const variantMap: Record<
50
- Variant,
51
- Record<'border' | 'text' | 'bg', string>
52
- > = {
33
+ export function getVariantClasses(variant, type) {
34
+ const variantMap = {
53
35
  default: {
54
36
  border: 'border-border',
55
37
  text: 'text-body',
@@ -76,34 +58,21 @@ export function getVariantClasses(
76
58
  bg: 'bg-card',
77
59
  },
78
60
  };
79
-
80
61
  return variantMap[variant]?.[type] || variantMap.default[type];
81
62
  }
82
-
83
- export function getAllVariantClasses(variant: Variant): {
84
- border: string;
85
- text: string;
86
- bg: string;
87
- } {
63
+ export function getAllVariantClasses(variant) {
88
64
  return {
89
65
  border: getVariantClasses(variant, 'border'),
90
66
  text: getVariantClasses(variant, 'text'),
91
67
  bg: getVariantClasses(variant, 'bg'),
92
68
  };
93
69
  }
94
-
95
70
  /**
96
71
  * Create a variant class map for use in reactive statements
97
72
  */
98
- export function createVariantClassMap<T extends string>(
99
- variants: T[],
100
- prefix: string,
101
- ): Record<T, string> {
102
- return variants.reduce(
103
- (acc, variant) => {
104
- acc[variant] = `${prefix}-${variant}`;
105
- return acc;
106
- },
107
- {} as Record<T, string>,
108
- );
73
+ export function createVariantClassMap(variants, prefix) {
74
+ return variants.reduce((acc, variant) => {
75
+ acc[variant] = `${prefix}-${variant}`;
76
+ return acc;
77
+ }, {});
109
78
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,3 @@
1
+ export * from './events.js';
2
+ export * from './variants.js';
3
+ export * from './page.types.js';
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Merge class values, last conflicting utility wins.
3
+ *
4
+ * Falsy entries are dropped, so `cn(base, isActive && activeClasses, className)`
5
+ * is safe.
6
+ */
7
+ export declare function cn(...classes: (string | false | null | undefined)[]): string;
@@ -0,0 +1,48 @@
1
+ import { extendTailwindMerge } from "tailwind-merge";
2
+ /**
3
+ * Class merger for every component's final `class` value.
4
+ *
5
+ * The library promises that a call-site utility wins over the component's own.
6
+ * Plain concatenation does not deliver that: `rounded-control` and
7
+ * `rounded-container` are equal-specificity utilities, so the winner is
8
+ * whichever Tailwind emits later in the stylesheet, not whichever the caller
9
+ * passed. The sidebar's search field asked for `rounded-container` and rendered
10
+ * at 8px, because `rounded-control` happened to come later.
11
+ *
12
+ * tailwind-merge resolves conflicts by class group and keeps the last one, so
13
+ * the promise holds for real.
14
+ *
15
+ * Almost all of this repo's semantic utilities already work with the stock
16
+ * config — `bg-card` vs `bg-surface-overlay`, `text-headline` vs
17
+ * `text-description`, `border-border` vs `border-input-border` all resolve,
18
+ * because tailwind-merge treats an unrecognised colour-ish value as a colour.
19
+ * The radii are the exception: `control`, `container`, `overlay` and `pill` are
20
+ * role names, not scale values, so they have to be declared or tailwind-merge
21
+ * keeps both and we are back to stylesheet order deciding.
22
+ */
23
+ const RADIUS_ROLES = ["control", "container", "overlay", "pill"];
24
+ /**
25
+ * Every corner group tailwind-merge knows about, so `rounded-t-overlay` on a
26
+ * sheet is still beaten by a call-site `rounded-t-control`. Declaring only the
27
+ * base `rounded` group silently leaves the per-side ones unmerged.
28
+ */
29
+ const RADIUS_GROUPS = [
30
+ "rounded",
31
+ "rounded-s", "rounded-e", "rounded-t", "rounded-r", "rounded-b", "rounded-l",
32
+ "rounded-ss", "rounded-se", "rounded-ee", "rounded-es",
33
+ "rounded-tl", "rounded-tr", "rounded-br", "rounded-bl",
34
+ ];
35
+ const twMerge = extendTailwindMerge({
36
+ extend: {
37
+ classGroups: Object.fromEntries(RADIUS_GROUPS.map((group) => [group, [{ [group]: RADIUS_ROLES }]])),
38
+ },
39
+ });
40
+ /**
41
+ * Merge class values, last conflicting utility wins.
42
+ *
43
+ * Falsy entries are dropped, so `cn(base, isActive && activeClasses, className)`
44
+ * is safe.
45
+ */
46
+ export function cn(...classes) {
47
+ return twMerge(classes.filter(Boolean).join(" "));
48
+ }
@@ -9,8 +9,8 @@ export declare function safeDocument(): Document | undefined;
9
9
  export declare function safeLocalStorage(): Storage | undefined;
10
10
  export declare function safeSessionStorage(): Storage | undefined;
11
11
  export declare function safeRequestAnimationFrame(callback: FrameRequestCallback): number | undefined;
12
- export declare function safeSetTimeout(callback: () => void, delay: number): NodeJS.Timeout | undefined;
13
- export declare function safeClearTimeout(id: NodeJS.Timeout | undefined): void;
14
- export declare function safeSetInterval(callback: () => void, delay: number): NodeJS.Timeout | undefined;
15
- export declare function safeClearInterval(id: NodeJS.Timeout | undefined): void;
12
+ export declare function safeSetTimeout(callback: () => void, delay: number): ReturnType<typeof setTimeout> | undefined;
13
+ export declare function safeClearTimeout(id: ReturnType<typeof setTimeout> | undefined): void;
14
+ export declare function safeSetInterval(callback: () => void, delay: number): ReturnType<typeof setInterval> | undefined;
15
+ export declare function safeClearInterval(id: ReturnType<typeof setInterval> | undefined): void;
16
16
  export declare function generateId(prefix?: string): string;