zabi-components 5.0.20 → 5.0.22

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 (69) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +355 -81
  3. package/dist/atoms/Button.svelte +1 -1
  4. package/dist/atoms/CardContent.svelte +1 -1
  5. package/dist/atoms/CardHeader.svelte +1 -1
  6. package/dist/atoms/Checkbox.svelte +1 -7
  7. package/dist/atoms/CodeBlock.svelte +6 -6
  8. package/dist/atoms/ColorPicker.svelte +80 -72
  9. package/dist/atoms/IconButton.svelte +88 -0
  10. package/dist/atoms/IconButton.svelte.d.ts +21 -0
  11. package/dist/atoms/Input.svelte +14 -10
  12. package/dist/atoms/Input.svelte.d.ts +2 -0
  13. package/dist/atoms/List.svelte +47 -0
  14. package/dist/atoms/List.svelte.d.ts +19 -0
  15. package/dist/atoms/ListItem.svelte +148 -0
  16. package/dist/atoms/ListItem.svelte.d.ts +35 -0
  17. package/dist/atoms/Progress.svelte +3 -5
  18. package/dist/atoms/Select.svelte +152 -19
  19. package/dist/atoms/Select.svelte.d.ts +23 -1
  20. package/dist/atoms/Textarea.svelte +11 -9
  21. package/dist/atoms/Textarea.svelte.d.ts +1 -0
  22. package/dist/atoms/Toggle.svelte +1 -7
  23. package/dist/atoms/Tooltip.svelte +8 -15
  24. package/dist/atoms/index.d.ts +3 -0
  25. package/dist/atoms/index.js +3 -0
  26. package/dist/components/atoms/index.d.ts +3 -0
  27. package/dist/components/atoms/index.d.ts.map +1 -1
  28. package/dist/components/index.d.ts +4 -0
  29. package/dist/components/index.d.ts.map +1 -1
  30. package/dist/components/molecules/index.d.ts +1 -1
  31. package/dist/components/molecules/navigation-menu-context.d.ts +7 -0
  32. package/dist/components/molecules/navigation-menu-context.d.ts.map +1 -0
  33. package/dist/components/organisms/index.d.ts +2 -0
  34. package/dist/components/organisms/index.d.ts.map +1 -1
  35. package/dist/index.d.ts +4 -0
  36. package/dist/index.js +5 -8
  37. package/dist/molecules/Alert.svelte +8 -3
  38. package/dist/molecules/ContactForm.svelte +63 -31
  39. package/dist/molecules/ContactForm.svelte.d.ts +1 -4
  40. package/dist/molecules/Dropdown.svelte +2 -1
  41. package/dist/molecules/ImageUpload.svelte +26 -3
  42. package/dist/molecules/ImageUpload.svelte.d.ts +1 -0
  43. package/dist/molecules/Modal.svelte +44 -43
  44. package/dist/molecules/NavigationMenu.svelte +7 -10
  45. package/dist/molecules/NavigationMenuContent.svelte +8 -8
  46. package/dist/molecules/NavigationMenuItem.svelte +7 -6
  47. package/dist/molecules/NavigationMenuLink.svelte +7 -5
  48. package/dist/molecules/NavigationMenuList.svelte +0 -7
  49. package/dist/molecules/NavigationMenuTrigger.svelte +7 -7
  50. package/dist/molecules/Section.svelte +158 -0
  51. package/dist/molecules/Section.svelte.d.ts +27 -0
  52. package/dist/molecules/SlideUp.svelte +30 -0
  53. package/dist/molecules/Tabs.svelte +71 -4
  54. package/dist/molecules/index.d.ts +1 -1
  55. package/dist/molecules/index.js +1 -1
  56. package/dist/molecules/navigation-menu-context.d.ts +6 -0
  57. package/dist/molecules/navigation-menu-context.js +1 -0
  58. package/dist/organisms/Navigation.svelte +9 -6
  59. package/dist/organisms/Navigation.svelte.d.ts +1 -0
  60. package/dist/organisms/SidebarNavigation.svelte +420 -0
  61. package/dist/organisms/SidebarNavigation.svelte.d.ts +44 -0
  62. package/dist/organisms/SidebarProjectPanel.svelte +174 -0
  63. package/dist/organisms/SidebarProjectPanel.svelte.d.ts +32 -0
  64. package/dist/organisms/index.d.ts +2 -0
  65. package/dist/organisms/index.js +2 -0
  66. package/dist/zabi-components.css +412 -0
  67. package/package.json +7 -8
  68. package/dist/molecules/Sidebar.svelte +0 -324
  69. package/dist/molecules/Sidebar.svelte.d.ts +0 -30
@@ -0,0 +1,148 @@
1
+ <script lang="ts">
2
+ import { ArrowRight } from "@lucide/svelte";
3
+ import type { Component } from "svelte";
4
+
5
+ export interface ListItemData {
6
+ /** Unique identifier for stable rendering and active-state tracking. */
7
+ id: string;
8
+ /** Primary item label. */
9
+ label: string;
10
+ /** Optional secondary text shown under the label. */
11
+ description?: string;
12
+ /** Optional URL; when set, the item renders as an anchor. */
13
+ href?: string;
14
+ /** Optional leading icon rendered before text. */
15
+ icon?: Component<{ size?: number; class?: string }>;
16
+ /** Link target, only used when href is provided. */
17
+ target?: "_self" | "_blank" | "_parent" | "_top";
18
+ /** Link rel attribute, only used when href is provided. */
19
+ rel?: string;
20
+ /** Disabled state for the item. */
21
+ disabled?: boolean;
22
+ }
23
+
24
+ interface Props {
25
+ /** Item data to render. */
26
+ item: ListItemData;
27
+ /** Active-state styling flag. */
28
+ selected?: boolean;
29
+ /** Whether to show the right-arrow icon. */
30
+ showArrow?: boolean;
31
+ /** Item click callback. */
32
+ onclick?: (item: ListItemData, event: MouseEvent) => void;
33
+ }
34
+
35
+ let {
36
+ item,
37
+ selected = false,
38
+ showArrow = true,
39
+ onclick,
40
+ ...restProps
41
+ }: Props = $props();
42
+
43
+ const itemClasses = $derived.by(() => {
44
+ const baseClasses =
45
+ "group flex w-full items-center gap-3 rounded-xl border border-border px-4 py-3 text-left transition-all duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring focus-visible:ring-inset";
46
+ const interactiveClasses = item.disabled
47
+ ? "cursor-not-allowed opacity-50"
48
+ : "cursor-pointer hover:ring-1 hover:ring-focus focus-visible:bg-base-100";
49
+ const selectedClasses = selected
50
+ ? "bg-action-primary-subtle border-brand-500"
51
+ : "";
52
+ return `${baseClasses} ${interactiveClasses} ${selectedClasses}`.trim();
53
+ });
54
+
55
+ const ariaCurrent = $derived(item.href && selected ? "page" : undefined);
56
+
57
+ const rel = $derived.by(() => {
58
+ if (item.rel) {
59
+ return item.rel;
60
+ }
61
+ if (item.target === "_blank") {
62
+ return "noopener noreferrer";
63
+ }
64
+ return undefined;
65
+ });
66
+
67
+ const handleItemClick = (event: MouseEvent): void => {
68
+ if (item.disabled) {
69
+ event.preventDefault();
70
+ return;
71
+ }
72
+ onclick?.(item, event);
73
+ };
74
+ </script>
75
+
76
+ {#if item.href}
77
+ <a
78
+ href={item.href}
79
+ target={item.target}
80
+ {rel}
81
+ aria-current={ariaCurrent}
82
+ aria-disabled={item.disabled ? "true" : undefined}
83
+ tabindex={item.disabled ? -1 : undefined}
84
+ class={itemClasses}
85
+ onclick={handleItemClick}
86
+ {...restProps}
87
+ >
88
+ {#if item.icon}
89
+ <span
90
+ class="flex size-5 shrink-0 items-center justify-center text-description"
91
+ aria-hidden="true"
92
+ >
93
+ <item.icon size={16} />
94
+ </span>
95
+ {/if}
96
+ <span class="min-w-0 flex-1">
97
+ <span class="block text-sm font-medium text-headline"
98
+ >{item.label}</span
99
+ >
100
+ {#if item.description}
101
+ <span class="mt-0.5 block text-sm text-description"
102
+ >{item.description}</span
103
+ >
104
+ {/if}
105
+ </span>
106
+ {#if showArrow}
107
+ <ArrowRight
108
+ size={16}
109
+ class="shrink-0 text-description transition-transform duration-150 group-hover:translate-x-0.5 group-focus-visible:translate-x-0.5"
110
+ aria-hidden="true"
111
+ />
112
+ {/if}
113
+ </a>
114
+ {:else}
115
+ <button
116
+ type="button"
117
+ class={itemClasses}
118
+ disabled={item.disabled}
119
+ onclick={handleItemClick}
120
+ {...restProps}
121
+ >
122
+ {#if item.icon}
123
+ <span
124
+ class="flex size-5 shrink-0 items-center justify-center text-description"
125
+ aria-hidden="true"
126
+ >
127
+ <item.icon size={16} />
128
+ </span>
129
+ {/if}
130
+ <span class="min-w-0 flex-1">
131
+ <span class="block text-sm font-medium text-headline"
132
+ >{item.label}</span
133
+ >
134
+ {#if item.description}
135
+ <span class="mt-0.5 block text-sm text-description"
136
+ >{item.description}</span
137
+ >
138
+ {/if}
139
+ </span>
140
+ {#if showArrow}
141
+ <ArrowRight
142
+ size={16}
143
+ class="shrink-0 text-description transition-transform duration-150 group-hover:translate-x-0.5 group-focus-visible:translate-x-0.5"
144
+ aria-hidden="true"
145
+ />
146
+ {/if}
147
+ </button>
148
+ {/if}
@@ -0,0 +1,35 @@
1
+ import type { Component } from "svelte";
2
+ export interface ListItemData {
3
+ /** Unique identifier for stable rendering and active-state tracking. */
4
+ id: string;
5
+ /** Primary item label. */
6
+ label: string;
7
+ /** Optional secondary text shown under the label. */
8
+ description?: string;
9
+ /** Optional URL; when set, the item renders as an anchor. */
10
+ href?: string;
11
+ /** Optional leading icon rendered before text. */
12
+ icon?: Component<{
13
+ size?: number;
14
+ class?: string;
15
+ }>;
16
+ /** Link target, only used when href is provided. */
17
+ target?: "_self" | "_blank" | "_parent" | "_top";
18
+ /** Link rel attribute, only used when href is provided. */
19
+ rel?: string;
20
+ /** Disabled state for the item. */
21
+ disabled?: boolean;
22
+ }
23
+ interface Props {
24
+ /** Item data to render. */
25
+ item: ListItemData;
26
+ /** Active-state styling flag. */
27
+ selected?: boolean;
28
+ /** Whether to show the right-arrow icon. */
29
+ showArrow?: boolean;
30
+ /** Item click callback. */
31
+ onclick?: (item: ListItemData, event: MouseEvent) => void;
32
+ }
33
+ declare const ListItem: Component<Props, {}, "">;
34
+ type ListItem = ReturnType<typeof ListItem>;
35
+ export default ListItem;
@@ -1,4 +1,6 @@
1
1
  <script lang="ts">
2
+ import { generateId } from "../../routes/lib/ssr-safe.js";
3
+
2
4
  interface Props {
3
5
  value?: number;
4
6
  max?: number;
@@ -14,11 +16,7 @@
14
16
  ...restProps
15
17
  }: Props = $props();
16
18
 
17
- let progressId = $state(
18
- typeof window !== "undefined"
19
- ? `progress-${Math.random().toString(36).substr(2, 9)}`
20
- : `progress-ssr-${Date.now()}`,
21
- );
19
+ const progressId = generateId("progress");
22
20
 
23
21
  let percentage = $derived(Math.min(Math.max((value / max) * 100, 0), 100));
24
22
 
@@ -1,19 +1,13 @@
1
1
  <script lang="ts">
2
2
  import Dropdown from "../molecules/Dropdown.svelte";
3
+ import Button from "./Button.svelte";
3
4
  import {
4
5
  ChevronDown,
5
6
  CheckCircle,
6
7
  AlertTriangle,
7
8
  AlertCircle,
8
9
  } from "@lucide/svelte";
9
-
10
- function generateId(prefix: string = "id"): string {
11
- if (typeof window !== "undefined") {
12
- return `${prefix}-${Math.random().toString(36).substr(2, 9)}`;
13
- } else {
14
- return `${prefix}-ssr-${Date.now()}`;
15
- }
16
- }
10
+ import { generateId } from "../../routes/lib/ssr-safe.js";
17
11
 
18
12
  interface Props {
19
13
  value?: string | number | undefined;
@@ -22,30 +16,65 @@
22
16
  label: string;
23
17
  disabled?: boolean;
24
18
  }>;
19
+ /** Enables the search input inside the dropdown. */
20
+ searchable?: boolean;
21
+ /** Placeholder for the search input. */
22
+ searchPlaceholder?: string;
23
+ /** Max height for the options list (supports any valid CSS length). */
24
+ maxMenuHeight?: string;
25
+ /** Width for the dropdown menu (supports any valid CSS length). */
26
+ menuWidth?: string;
27
+ /** Message shown when no options match the search. */
28
+ noResultsText?: string;
29
+ /** Shows a loading state while options are being fetched. */
30
+ isLoading?: boolean;
31
+ /** Loading text shown in trigger and dropdown body. */
32
+ loadingText?: string;
33
+ /** Empty state title shown when there are no available options. */
34
+ emptyStateTitle?: string;
35
+ /** Empty state description shown when there are no available options. */
36
+ emptyStateDescription?: string;
37
+ /** Optional empty state action label. */
38
+ emptyStateActionLabel?: string;
25
39
  placeholder?: string;
26
40
  label?: string;
41
+ required?: boolean;
27
42
  disabled?: boolean;
28
43
  size?: "sm" | "md" | "lg";
29
44
  variant?: "default" | "success" | "warning" | "error";
30
45
  message?: string;
31
46
  onchange?: (event: Event) => void;
47
+ onEmptyStateAction?: () => void;
32
48
  }
33
49
 
34
50
  let {
35
- value = undefined,
51
+ value = $bindable(undefined),
36
52
  options = [],
53
+ searchable = true,
54
+ searchPlaceholder = "Search options",
55
+ maxMenuHeight = "60vh",
56
+ menuWidth = "100%",
57
+ noResultsText = "No results found",
58
+ isLoading = false,
59
+ loadingText = "Loading options...",
60
+ emptyStateTitle = "No options available",
61
+ emptyStateDescription = "Add an option to start making selections.",
62
+ emptyStateActionLabel = "",
37
63
  placeholder = "Select an option",
38
64
  label = "",
65
+ required = false,
39
66
  disabled = false,
40
67
  size = "md",
41
68
  variant = "default",
42
69
  message = "",
43
70
  onchange,
71
+ onEmptyStateAction,
44
72
  ...restProps
45
73
  }: Props = $props();
46
74
 
47
75
  let isOpen = $state(false);
48
76
  let selectContainer: HTMLDivElement;
77
+ let searchQuery = $state("");
49
78
 
50
79
  const sizeClass = $derived(() => {
51
80
  if (size === "sm") {
@@ -93,13 +122,13 @@
93
122
 
94
123
  const messageClasses = $derived(() => {
95
124
  if (variant === "error") {
96
- return "text-error text-sm mt-1 flex items-center gap-1.5";
125
+ return "text-error text-sm mt-1 flex items-center gap-1.5 w-full";
97
126
  } else if (variant === "success") {
98
- return "text-success text-sm mt-1 flex items-center gap-1.5";
127
+ return "text-success text-sm mt-1 flex items-center gap-1.5 w-full";
99
128
  } else if (variant === "warning") {
100
- return "text-warning text-sm mt-1 flex items-center gap-1.5";
129
+ return "text-warning text-sm mt-1 flex items-center gap-1.5 w-full";
101
130
  }
102
- return "text-description text-sm mt-1 flex items-center gap-1.5";
131
+ return "text-description text-sm mt-1 flex items-center gap-1.5 w-full";
103
132
  });
104
133
 
105
134
  const getIcon = $derived(() => {
@@ -125,8 +154,35 @@
125
154
  return value === undefined || value === null || value === "";
126
155
  });
127
156
 
157
+ const normalizedOptions = $derived(() => {
158
+ const fallbackOptions = (restProps as { options?: Props["options"] })
159
+ .options;
160
+ return Array.from(options ?? fallbackOptions ?? []);
161
+ });
162
+
163
+ const hasSearchQuery = $derived(() => {
164
+ return searchQuery.trim().length > 0;
165
+ });
166
+
167
+ const filteredOptions = $derived(() => {
168
+ const query = searchQuery.trim().toLowerCase();
169
+ const availableOptions = normalizedOptions();
170
+ if (!searchable || query.length === 0) {
171
+ return availableOptions;
172
+ }
173
+ return availableOptions.filter((option) =>
174
+ option.label.toLowerCase().includes(query),
175
+ );
176
+ });
177
+
178
+ $effect(() => {
179
+ if (!isOpen && searchQuery.length > 0) {
180
+ searchQuery = "";
181
+ }
182
+ });
183
+
128
184
  function handleOptionClick(optionValue: string | number) {
129
- if (disabled) return;
185
+ if (disabled || isLoading) return;
130
186
  value = optionValue;
131
187
  isOpen = false;
132
188
 
@@ -141,7 +197,7 @@
141
197
  }
142
198
 
143
199
  function handleTriggerClick(event: MouseEvent) {
144
- if (disabled) return;
200
+ if (disabled || isLoading) return;
145
201
  event.stopPropagation();
146
202
  isOpen = !isOpen;
147
203
  }
@@ -179,8 +235,8 @@
179
235
  {isOpen}
180
236
  placement="bottom-start"
181
237
  selectedValue={value}
182
- {options}
183
238
  onOptionClick={handleOptionClick}
239
+ ariaLabel="Select options"
184
240
  >
185
241
  {#snippet trigger()}
186
242
  <button
@@ -194,14 +250,20 @@
194
250
  aria-describedby={message ? `${selectId}-message` : undefined}
195
251
  >
196
252
  <span
197
- class="text-left flex-1 {isEmpty()
253
+ class="text-left flex-1 truncate {isLoading
254
+ ? 'text-description'
255
+ : isEmpty()
198
256
  ? 'text-description'
199
257
  : 'text-body'}"
200
258
  >
201
- {isEmpty()
259
+ {#if isLoading}
260
+ {loadingText}
261
+ {:else}
262
+ {isEmpty()
202
263
  ? placeholder
203
264
  : options.find((opt) => opt.value === value)?.label ||
204
265
  placeholder}
266
+ {/if}
205
267
  </span>
206
268
  <ChevronDown
207
269
  size={20}
@@ -211,9 +273,80 @@
211
273
  />
212
274
  </button>
213
275
  {/snippet}
276
+ {#snippet children()}
277
+ <div class="px-2 pb-2 pt-1" style:width={menuWidth}>
278
+ {#if searchable && !isLoading}
279
+ <div class="px-1 pb-2">
280
+ <input
281
+ type="text"
282
+ class="w-full rounded-md bg-input hover:bg-input-hover focus:bg-input-focus text-body placeholder:text-description px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-brand-500 disabled:opacity-50 disabled:cursor-not-allowed"
283
+ placeholder={searchPlaceholder}
284
+ bind:value={searchQuery}
285
+ aria-label={searchPlaceholder}
286
+ {disabled}
287
+ />
288
+ </div>
289
+ {/if}
290
+ <div
291
+ class="overflow-y-auto px-1"
292
+ style:max-height={maxMenuHeight}
293
+ >
294
+ {#if isLoading}
295
+ <div class="space-y-2 px-2 py-2" role="status" aria-live="polite">
296
+ <div class="h-8 w-full animate-pulse rounded-md bg-base-200"></div>
297
+ <div class="h-8 w-full animate-pulse rounded-md bg-base-200"></div>
298
+ <p class="text-xs text-description">{loadingText}</p>
299
+ </div>
300
+ {:else if filteredOptions().length > 0}
301
+ {#each filteredOptions() as option (option.value)}
302
+ {@const buttonRestProps = {
303
+ "data-value": String(option.value),
304
+ } as Record<string, string>}
305
+ <div class="w-full my-0.5">
306
+ <Button
307
+ variant={value === option.value
308
+ ? "outline"
309
+ : "ghost"}
310
+ size="sm"
311
+ isFullWidth={true}
312
+ disabled={option.disabled}
313
+ onclick={() => handleOptionClick(option.value)}
314
+ {...buttonRestProps}
315
+ >
316
+ {option.label}
317
+ </Button>
318
+ </div>
319
+ {/each}
320
+ {:else if hasSearchQuery()}
321
+ <div class="px-3 py-2 text-sm text-description">
322
+ {noResultsText}
323
+ </div>
324
+ {:else}
325
+ <div class="rounded-md border border-border bg-base-50 px-3 py-3 text-sm">
326
+ <p class="font-medium text-headline">{emptyStateTitle}</p>
327
+ <p class="mt-1 text-description">{emptyStateDescription}</p>
328
+ {#if emptyStateActionLabel && onEmptyStateAction}
329
+ <button
330
+ type="button"
331
+ class="mt-3 inline-flex min-h-11 items-center rounded-lg bg-action-primary px-3 py-2 text-sm text-action-primary"
332
+ onclick={onEmptyStateAction}
333
+ >
334
+ {emptyStateActionLabel}
335
+ </button>
336
+ {/if}
337
+ </div>
338
+ {/if}
339
+ </div>
340
+ </div>
341
+ {/snippet}
214
342
  </Dropdown>
215
343
  {#if message && variant !== "default"}
216
- <p id={`${selectId}-message`} class={messageClasses()} role="alert">
344
+ <p
345
+ id={`${selectId}-message`}
346
+ class={messageClasses()}
347
+ role={variant === "error" ? "alert" : "status"}
348
+ aria-live={variant === "error" ? "assertive" : "polite"}
349
+ >
217
350
  {#if getIcon()}
218
351
  {@const Icon = getIcon()}
219
352
  <Icon size={14} class="shrink-0" />
@@ -5,14 +5,36 @@ interface Props {
5
5
  label: string;
6
6
  disabled?: boolean;
7
7
  }>;
8
+ /** Enables the search input inside the dropdown. */
9
+ searchable?: boolean;
10
+ /** Placeholder for the search input. */
11
+ searchPlaceholder?: string;
12
+ /** Max height for the options list (supports any valid CSS length). */
13
+ maxMenuHeight?: string;
14
+ /** Width for the dropdown menu (supports any valid CSS length). */
15
+ menuWidth?: string;
16
+ /** Message shown when no options match the search. */
17
+ noResultsText?: string;
18
+ /** Shows a loading state while options are being fetched. */
19
+ isLoading?: boolean;
20
+ /** Loading text shown in trigger and dropdown body. */
21
+ loadingText?: string;
22
+ /** Empty state title shown when there are no available options. */
23
+ emptyStateTitle?: string;
24
+ /** Empty state description shown when there are no available options. */
25
+ emptyStateDescription?: string;
26
+ /** Optional empty state action label. */
27
+ emptyStateActionLabel?: string;
8
28
  placeholder?: string;
9
29
  label?: string;
30
+ required?: boolean;
10
31
  disabled?: boolean;
11
32
  size?: "sm" | "md" | "lg";
12
33
  variant?: "default" | "success" | "warning" | "error";
13
34
  message?: string;
14
35
  onchange?: (event: Event) => void;
36
+ onEmptyStateAction?: () => void;
15
37
  }
16
- declare const Select: import("svelte").Component<Props, {}, "">;
38
+ declare const Select: import("svelte").Component<Props, {}, "value">;
17
39
  type Select = ReturnType<typeof Select>;
18
40
  export default Select;
@@ -1,20 +1,14 @@
1
1
  <script lang="ts">
2
2
  import { CheckCircle, AlertTriangle, AlertCircle } from "@lucide/svelte";
3
3
  import type { SemanticVariant, SizeVariant } from "../../types/variants.js";
4
-
5
- function generateId(prefix: string = "id"): string {
6
- if (typeof window !== "undefined") {
7
- return `${prefix}-${Math.random().toString(36).substr(2, 9)}`;
8
- } else {
9
- return `${prefix}-ssr-${Date.now()}`;
10
- }
11
- }
4
+ import { generateId } from "../../routes/lib/ssr-safe.js";
12
5
 
13
6
  interface Props {
14
7
  value?: string;
15
8
  name?: string;
16
9
  label?: string;
17
10
  placeholder?: string;
11
+ required?: boolean;
18
12
  disabled?: boolean;
19
13
  rows?: number;
20
14
  size?: SizeVariant;
@@ -28,6 +22,7 @@
28
22
  name = "",
29
23
  label = "",
30
24
  placeholder = "",
25
+ required = false,
31
26
  disabled = false,
32
27
  rows = 4,
33
28
  variant = "default",
@@ -96,16 +91,23 @@
96
91
  {name}
97
92
  {value}
98
93
  {placeholder}
94
+ {required}
99
95
  {disabled}
100
96
  {rows}
101
97
  class={textareaClasses()}
102
98
  oninput={handleInput}
103
99
  aria-invalid={variant === "error" ? "true" : undefined}
100
+ aria-required={required ? "true" : undefined}
104
101
  aria-describedby={message ? `${textareaId}-message` : undefined}
105
102
  {...restProps}
106
103
  ></textarea>
107
104
  {#if message && variant !== "default"}
108
- <p id={`${textareaId}-message`} class={messageClasses()} role="alert">
105
+ <p
106
+ id={`${textareaId}-message`}
107
+ class={messageClasses()}
108
+ role={variant === "error" ? "alert" : "status"}
109
+ aria-live={variant === "error" ? "assertive" : "polite"}
110
+ >
109
111
  {#if getIcon()}
110
112
  {@const Icon = getIcon()}
111
113
  <Icon size={14} class="shrink-0" />
@@ -4,6 +4,7 @@ interface Props {
4
4
  name?: string;
5
5
  label?: string;
6
6
  placeholder?: string;
7
+ required?: boolean;
7
8
  disabled?: boolean;
8
9
  rows?: number;
9
10
  size?: SizeVariant;
@@ -1,11 +1,5 @@
1
1
  <script lang="ts">
2
- function generateId(prefix: string = "id"): string {
3
- if (typeof window !== "undefined") {
4
- return `${prefix}-${Math.random().toString(36).substr(2, 9)}`;
5
- } else {
6
- return `${prefix}-ssr-${Date.now()}`;
7
- }
8
- }
2
+ import { generateId } from "../../routes/lib/ssr-safe.js";
9
3
 
10
4
  interface Props {
11
5
  checked?: boolean;
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- import { onMount } from "svelte";
2
+ import { generateId } from "../../routes/lib/ssr-safe.js";
3
3
 
4
4
  interface Props {
5
5
  content?: string;
@@ -17,8 +17,8 @@
17
17
  ...restProps
18
18
  }: Props & { children?: any } = $props();
19
19
 
20
- let triggerId = `tooltip-trigger-${Math.random().toString(36).substr(2, 9)}`;
21
- let tooltipId = `tooltip-${Math.random().toString(36).substr(2, 9)}`;
20
+ const triggerId = generateId("tooltip-trigger");
21
+ const tooltipId = generateId("tooltip");
22
22
  let isVisible = $state(false);
23
23
  let triggerElement: HTMLElement | null = $state(null);
24
24
 
@@ -56,17 +56,10 @@
56
56
  function handleMouseLeave() {
57
57
  isVisible = false;
58
58
  }
59
-
60
- onMount(() => {
61
- if (typeof window !== "undefined") {
62
- window.addEventListener("keydown", handleKeydown);
63
- return () => {
64
- window.removeEventListener("keydown", handleKeydown);
65
- };
66
- }
67
- });
68
59
  </script>
69
60
 
61
+ <svelte:window onkeydown={handleKeydown} />
62
+
70
63
  <div
71
64
  class="tooltip-container relative inline-block"
72
65
  data-placement={placement}
@@ -249,8 +242,8 @@
249
242
 
250
243
  @media (prefers-contrast: high) {
251
244
  :root {
252
- --tooltip-bg: #000000;
253
- --tooltip-color: #ffffff;
245
+ --tooltip-bg: CanvasText;
246
+ --tooltip-color: Canvas;
254
247
  --tooltip-padding: 0.75rem 1rem;
255
248
  }
256
249
  }
@@ -276,7 +269,7 @@
276
269
  }
277
270
 
278
271
  .tooltip-container:focus-visible {
279
- outline: 2px solid var(--color-focus, #8fa8ff);
272
+ outline: 2px solid var(--color-focus);
280
273
  outline-offset: 2px;
281
274
  border-radius: var(--tooltip-radius);
282
275
  }
@@ -1,6 +1,7 @@
1
1
  export { default as Toggle } from './Toggle.svelte';
2
2
  export { default as Badge } from './Badge.svelte';
3
3
  export { default as Button } from './Button.svelte';
4
+ export { default as IconButton } from './IconButton.svelte';
4
5
  export { default as Card } from './Card.svelte';
5
6
  export { default as CardHeader } from './CardHeader.svelte';
6
7
  export { default as CardContent } from './CardContent.svelte';
@@ -9,5 +10,7 @@ export { default as Input } from './Input.svelte';
9
10
  export { default as Textarea } from './Textarea.svelte';
10
11
  export { default as Select } from './Select.svelte';
11
12
  export { default as ColorPicker } from './ColorPicker.svelte';
13
+ export { default as List } from './List.svelte';
14
+ export { default as ListItem } from './ListItem.svelte';
12
15
  export { default as Heading } from './Heading.svelte';
13
16
  export { default as ThemeToggle } from './ThemeToggle.svelte';
@@ -1,6 +1,7 @@
1
1
  export { default as Toggle } from './Toggle.svelte';
2
2
  export { default as Badge } from './Badge.svelte';
3
3
  export { default as Button } from './Button.svelte';
4
+ export { default as IconButton } from './IconButton.svelte';
4
5
  export { default as Card } from './Card.svelte';
5
6
  export { default as CardHeader } from './CardHeader.svelte';
6
7
  export { default as CardContent } from './CardContent.svelte';
@@ -9,5 +10,7 @@ export { default as Input } from './Input.svelte';
9
10
  export { default as Textarea } from './Textarea.svelte';
10
11
  export { default as Select } from './Select.svelte';
11
12
  export { default as ColorPicker } from './ColorPicker.svelte';
13
+ export { default as List } from './List.svelte';
14
+ export { default as ListItem } from './ListItem.svelte';
12
15
  export { default as Heading } from './Heading.svelte';
13
16
  export { default as ThemeToggle } from './ThemeToggle.svelte';