zabi-components 5.0.21 → 6.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 (89) hide show
  1. package/README.md +42 -44
  2. package/THEME.md +473 -0
  3. package/THEMING.md +248 -0
  4. package/dist/atoms/Card.svelte +40 -67
  5. package/dist/atoms/Card.svelte.d.ts +3 -3
  6. package/dist/atoms/CardContent.svelte +1 -1
  7. package/dist/atoms/CardHeader.svelte +10 -1
  8. package/dist/atoms/CardHeader.svelte.d.ts +3 -0
  9. package/dist/atoms/Checkbox.svelte +1 -7
  10. package/dist/atoms/CodeBlock.svelte +7 -7
  11. package/dist/atoms/ColorPicker.svelte +81 -73
  12. package/dist/atoms/Input.svelte +16 -12
  13. package/dist/atoms/Input.svelte.d.ts +2 -0
  14. package/dist/atoms/List.svelte +47 -0
  15. package/dist/atoms/List.svelte.d.ts +19 -0
  16. package/dist/atoms/ListItem.svelte +148 -0
  17. package/dist/atoms/ListItem.svelte.d.ts +35 -0
  18. package/dist/atoms/Progress.svelte +4 -6
  19. package/dist/atoms/Select.svelte +65 -20
  20. package/dist/atoms/Select.svelte.d.ts +13 -1
  21. package/dist/atoms/Textarea.svelte +13 -11
  22. package/dist/atoms/Textarea.svelte.d.ts +1 -0
  23. package/dist/atoms/Toast.svelte +1 -1
  24. package/dist/atoms/Toggle.svelte +1 -7
  25. package/dist/atoms/Tooltip.svelte +8 -15
  26. package/dist/atoms/index.d.ts +2 -0
  27. package/dist/atoms/index.js +2 -0
  28. package/dist/components/atoms/index.d.ts +2 -0
  29. package/dist/components/atoms/index.d.ts.map +1 -1
  30. package/dist/components/index.d.ts +4 -1
  31. package/dist/components/index.d.ts.map +1 -1
  32. package/dist/components/molecules/index.d.ts +0 -1
  33. package/dist/components/molecules/index.d.ts.map +1 -1
  34. package/dist/components/molecules/navigation-menu-context.d.ts +7 -0
  35. package/dist/components/molecules/navigation-menu-context.d.ts.map +1 -0
  36. package/dist/components/organisms/index.d.ts +2 -1
  37. package/dist/components/organisms/index.d.ts.map +1 -1
  38. package/dist/index.d.ts +4 -1
  39. package/dist/index.js +5 -9
  40. package/dist/lib/showcase/components-catalog.d.ts +3 -0
  41. package/dist/lib/showcase/components-catalog.d.ts.map +1 -0
  42. package/dist/lib/showcase/components-showcase-constants.d.ts +63 -0
  43. package/dist/lib/showcase/components-showcase-constants.d.ts.map +1 -0
  44. package/dist/lib/showcase/docs-sidebar-helpers.d.ts +15 -0
  45. package/dist/lib/showcase/docs-sidebar-helpers.d.ts.map +1 -0
  46. package/dist/molecules/Alert.svelte +9 -4
  47. package/dist/molecules/ComponentDemo.svelte +53 -50
  48. package/dist/molecules/ContactForm.svelte +113 -76
  49. package/dist/molecules/ContactForm.svelte.d.ts +1 -4
  50. package/dist/molecules/Dropdown.svelte +4 -2
  51. package/dist/molecules/ImageUpload.svelte +27 -4
  52. package/dist/molecules/ImageUpload.svelte.d.ts +1 -0
  53. package/dist/molecules/Modal.svelte +46 -45
  54. package/dist/molecules/NavigationMenu.svelte +7 -10
  55. package/dist/molecules/NavigationMenuContent.svelte +9 -9
  56. package/dist/molecules/NavigationMenuItem.svelte +7 -6
  57. package/dist/molecules/NavigationMenuLink.svelte +8 -6
  58. package/dist/molecules/NavigationMenuList.svelte +0 -7
  59. package/dist/molecules/NavigationMenuTrigger.svelte +8 -11
  60. package/dist/molecules/Section.svelte +12 -12
  61. package/dist/molecules/SlideUp.svelte +32 -2
  62. package/dist/molecules/Tabs.svelte +74 -7
  63. package/dist/molecules/index.d.ts +0 -1
  64. package/dist/molecules/index.js +0 -1
  65. package/dist/molecules/navigation-menu-context.d.ts +6 -0
  66. package/dist/molecules/navigation-menu-context.js +1 -0
  67. package/dist/organisms/Navbar.svelte +170 -53
  68. package/dist/organisms/Navbar.svelte.d.ts +31 -4
  69. package/dist/organisms/SidebarNavigation.svelte +464 -0
  70. package/dist/organisms/SidebarNavigation.svelte.d.ts +49 -0
  71. package/dist/organisms/SidebarPanel.svelte +175 -0
  72. package/dist/organisms/SidebarPanel.svelte.d.ts +32 -0
  73. package/dist/organisms/index.d.ts +2 -1
  74. package/dist/organisms/index.js +2 -1
  75. package/dist/types/page.types.d.ts +32 -0
  76. package/dist/types/page.types.d.ts.map +1 -0
  77. package/dist/types/variants.ts +76 -0
  78. package/dist/zabi-components-colors.css +347 -324
  79. package/dist/zabi-components-theme-dark-only.css +159 -151
  80. package/dist/zabi-components-theme-dark.css +159 -151
  81. package/dist/zabi-components-theme-only.css +245 -230
  82. package/dist/zabi-components-theme.css +245 -230
  83. package/dist/zabi-components.css +722 -84
  84. package/docs/theme-imports.md +112 -0
  85. package/package.json +20 -9
  86. package/dist/molecules/Sidebar.svelte +0 -324
  87. package/dist/molecules/Sidebar.svelte.d.ts +0 -30
  88. package/dist/organisms/Navigation.svelte +0 -116
  89. package/dist/organisms/Navigation.svelte.d.ts +0 -20
@@ -1,6 +1,6 @@
1
1
  <script lang="ts">
2
2
  import Input from "./Input.svelte";
3
- import { onMount, onDestroy } from "svelte";
3
+ import { onMount, onDestroy, tick } from "svelte";
4
4
 
5
5
  interface Props {
6
6
  value?: string;
@@ -20,11 +20,10 @@
20
20
  }: Props = $props();
21
21
 
22
22
  let isOpen = $state(false);
23
- let pickerContainer: HTMLDivElement;
24
- let colorMap: HTMLCanvasElement;
25
- let colorMapContainer: HTMLDivElement;
26
- let hueSlider: HTMLInputElement;
27
23
  let isDragging = $state(false);
24
+ let pickerContainer: HTMLDivElement | undefined;
25
+ let colorMap: HTMLCanvasElement | undefined;
26
+ let colorMapContainer: HTMLDivElement | undefined;
28
27
 
29
28
  // HSL values (0-360, 0-100, 0-100)
30
29
  let hue = $state(0);
@@ -96,46 +95,56 @@
96
95
  }
97
96
  }
98
97
 
99
- const variant = $derived(() => {
100
- if (value && !isValidHex(value)) {
101
- return "error";
102
- }
103
- return "default";
104
- });
98
+ const variant = $derived(value && !isValidHex(value) ? "error" : "default");
105
99
 
106
- const message = $derived(() => {
107
- if (value && !isValidHex(value)) {
108
- return "Please enter a valid hex color (e.g., #ff0000 or #f00)";
109
- }
110
- return "";
111
- });
100
+ const message = $derived(
101
+ value && !isValidHex(value)
102
+ ? "Please enter a valid hex color (e.g., #ff0000 or #f00)"
103
+ : "",
104
+ );
112
105
 
113
106
  function handleInput(event: Event) {
114
107
  if (onchange) onchange(event);
115
108
  updateFromHex(value);
116
109
  }
117
110
 
118
- function handleBlur(event: Event) {
119
- const target = event.target as HTMLInputElement;
120
- let hexValue = target.value.trim();
111
+ function normalizeHexValue(rawValue: string): string {
112
+ let hexValue = rawValue.trim();
121
113
 
122
114
  if (hexValue && !hexValue.startsWith("#")) {
123
115
  hexValue = "#" + hexValue;
124
116
  }
125
117
 
126
- if (hexValue === "" || isValidHex(hexValue)) {
127
- value = hexValue;
128
- updateFromHex(hexValue);
118
+ return hexValue;
119
+ }
120
+
121
+ function commitCurrentValue(event: Event) {
122
+ const normalizedHex = normalizeHexValue(value);
123
+
124
+ if (normalizedHex === "" || isValidHex(normalizedHex)) {
125
+ value = normalizedHex;
126
+ updateFromHex(normalizedHex);
129
127
  if (onchange) onchange(event);
130
128
  }
131
129
  }
132
130
 
133
- function togglePicker() {
131
+ function handleBlur(event: Event) {
132
+ commitCurrentValue(event);
133
+ }
134
+
135
+ async function togglePicker() {
134
136
  if (!disabled) {
137
+ if (isOpen) {
138
+ commitCurrentValue(new Event("change"));
139
+ }
135
140
  isOpen = !isOpen;
136
141
  if (isOpen && value && isValidHex(value)) {
137
142
  updateFromHex(value);
138
143
  }
144
+ if (isOpen) {
145
+ await tick();
146
+ drawColorMap();
147
+ }
139
148
  }
140
149
  }
141
150
 
@@ -178,6 +187,10 @@
178
187
  }
179
188
  }
180
189
 
190
+ function handleMouseUp() {
191
+ isDragging = false;
192
+ }
193
+
181
194
  function handleHueChange(event: Event) {
182
195
  const target = event.target as HTMLInputElement;
183
196
  hue = parseInt(target.value);
@@ -185,26 +198,38 @@
185
198
  drawColorMap();
186
199
  }
187
200
 
188
- $effect(() => {
189
- if (isOpen && colorMap) {
190
- // Use setTimeout to ensure canvas is rendered
191
- setTimeout(() => {
192
- drawColorMap();
193
- }, 0);
194
- }
195
- });
201
+ function setPickerContainer(node: HTMLDivElement) {
202
+ pickerContainer = node;
203
+ return {
204
+ destroy() {
205
+ if (pickerContainer === node) {
206
+ pickerContainer = undefined;
207
+ }
208
+ },
209
+ };
210
+ }
196
211
 
197
- $effect(() => {
198
- if (hue !== undefined && colorMap && isOpen) {
199
- drawColorMap();
200
- }
201
- });
212
+ function setColorMap(node: HTMLCanvasElement) {
213
+ colorMap = node;
214
+ return {
215
+ destroy() {
216
+ if (colorMap === node) {
217
+ colorMap = undefined;
218
+ }
219
+ },
220
+ };
221
+ }
202
222
 
203
- $effect(() => {
204
- if (value && isValidHex(value) && !isDragging) {
205
- updateFromHex(value);
206
- }
207
- });
223
+ function setColorMapContainer(node: HTMLDivElement) {
224
+ colorMapContainer = node;
225
+ return {
226
+ destroy() {
227
+ if (colorMapContainer === node) {
228
+ colorMapContainer = undefined;
229
+ }
230
+ },
231
+ };
232
+ }
208
233
 
209
234
  function handleClickOutside(event: MouseEvent) {
210
235
  if (
@@ -214,6 +239,7 @@
214
239
  '[aria-label="Open color picker"]',
215
240
  )
216
241
  ) {
242
+ commitCurrentValue(new Event("change"));
217
243
  isOpen = false;
218
244
  }
219
245
  }
@@ -223,36 +249,19 @@
223
249
  updateFromHex(value);
224
250
  }
225
251
  window.addEventListener("mousedown", handleClickOutside);
226
- window.addEventListener("mouseup", () => {
227
- isDragging = false;
228
- });
252
+ window.addEventListener("mouseup", handleMouseUp);
229
253
  window.addEventListener("mousemove", handleColorMapDrag);
230
254
  });
231
255
 
232
256
  onDestroy(() => {
233
257
  window.removeEventListener("mousedown", handleClickOutside);
234
- window.removeEventListener("mouseup", () => {});
258
+ window.removeEventListener("mouseup", handleMouseUp);
235
259
  window.removeEventListener("mousemove", handleColorMapDrag);
236
260
  });
237
261
 
238
- const displayColor = $derived(() => {
239
- if (value && isValidHex(value)) {
240
- return value;
241
- }
242
- return placeholder;
243
- });
244
-
245
- const currentHslColor = $derived(() => {
246
- return `hsl(${hue}, 100%, 50%)`;
247
- });
248
-
249
- const pickerIndicatorX = $derived(() => {
250
- return `${(saturation / 100) * 100}%`;
251
- });
252
-
253
- const pickerIndicatorY = $derived(() => {
254
- return `${100 - (lightness / 100) * 100}%`;
255
- });
262
+ const displayColor = $derived(value && isValidHex(value) ? value : placeholder);
263
+ const pickerIndicatorX = $derived(`${saturation}%`);
264
+ const pickerIndicatorY = $derived(`${100 - lightness}%`);
256
265
  </script>
257
266
 
258
267
  <div {...restProps}>
@@ -263,8 +272,8 @@
263
272
  {label}
264
273
  {placeholder}
265
274
  {disabled}
266
- variant={variant()}
267
- message={message()}
275
+ {variant}
276
+ {message}
268
277
  oninput={handleInput}
269
278
  onblur={handleBlur}
270
279
  aria-label="Hex color input"
@@ -276,22 +285,22 @@
276
285
  onclick={togglePicker}
277
286
  {disabled}
278
287
  class="w-11 h-11 rounded-lg border-2 border-card shrink-0 cursor-pointer hover:ring-2 hover:ring-brand-500 transition-colors disabled:cursor-not-allowed disabled:opacity-50"
279
- style="background-color: {displayColor()};"
288
+ style="background-color: {displayColor};"
280
289
  aria-label="Open color picker"
281
290
  aria-expanded={isOpen}
282
291
  ></button>
283
292
 
284
293
  {#if isOpen}
285
294
  <div
286
- bind:this={pickerContainer}
287
- class="absolute top-12 right-0 z-50 bg-input rounded-2xl shadow-lg p-4 w-80"
295
+ use:setPickerContainer
296
+ class="absolute top-12 right-0 z-50 border border-input-border bg-input rounded-2xl shadow-lg p-4 w-80"
288
297
  role="dialog"
289
298
  aria-label="Color picker"
290
299
  >
291
300
  <div class="space-y-4">
292
301
  <!-- Color Map -->
293
302
  <div
294
- bind:this={colorMapContainer}
303
+ use:setColorMapContainer
295
304
  class="relative w-full h-48 rounded-xl overflow-hidden cursor-crosshair"
296
305
  onmousedown={(e) => {
297
306
  isDragging = true;
@@ -301,7 +310,7 @@
301
310
  tabindex="0"
302
311
  >
303
312
  <canvas
304
- bind:this={colorMap}
313
+ use:setColorMap
305
314
  width={256}
306
315
  height={192}
307
316
  class="w-full h-full"
@@ -309,7 +318,7 @@
309
318
  <!-- Color indicator -->
310
319
  <div
311
320
  class="absolute w-4 h-4 border-2 border-white rounded-full shadow-lg pointer-events-none transform -translate-x-1/2 -translate-y-1/2"
312
- style="left: {pickerIndicatorX()}; top: {pickerIndicatorY()};"
321
+ style="left: {pickerIndicatorX}; top: {pickerIndicatorY};"
313
322
  ></div>
314
323
  </div>
315
324
 
@@ -323,7 +332,6 @@
323
332
  style="background: linear-gradient(to right, hsl(0,100%,50%), hsl(60,100%,50%), hsl(120,100%,50%), hsl(180,100%,50%), hsl(240,100%,50%), hsl(300,100%,50%), hsl(360,100%,50%));"
324
333
  ></div>
325
334
  <input
326
- bind:this={hueSlider}
327
335
  type="range"
328
336
  min="0"
329
337
  max="360"
@@ -1,21 +1,16 @@
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
  type?: string;
16
9
  name?: string;
10
+ class?: string;
17
11
  label?: string;
18
12
  placeholder?: string;
13
+ required?: boolean;
19
14
  disabled?: boolean;
20
15
  size?: SizeVariant;
21
16
  variant?: SemanticVariant;
@@ -29,8 +24,10 @@
29
24
  value = $bindable(""),
30
25
  type = "text",
31
26
  name = "",
27
+ class: className = "",
32
28
  label = "",
33
29
  placeholder = "",
30
+ required = false,
34
31
  disabled = false,
35
32
  size = "md",
36
33
  variant = "default",
@@ -71,15 +68,15 @@
71
68
  ? "border-warning focus:border-warning focus:ring-warning"
72
69
  : variant === "error"
73
70
  ? "border-error focus:border-error focus:ring-error"
74
- : "border-0 focus:ring-2 focus:ring-brand-500"; // default - no border
71
+ : "border-input-border focus:border-input-border focus:ring-2 focus:ring-brand-500";
75
72
  });
76
73
 
77
74
  const inputClasses = $derived(() => {
78
75
  const sizeStyles = sizeClass();
79
76
  const baseClasses =
80
- "w-full min-w-48 bg-input hover:bg-input-hover focus:bg-input-focus disabled:bg-input-disabled rounded-lg transition-all duration-200 placeholder:text-description text-body focus:outline-none focus:ring-offset-0 disabled:opacity-50 disabled:cursor-not-allowed";
77
+ "w-full min-w-48 border bg-input hover:bg-input-hover focus:bg-input-focus disabled:bg-input-disabled rounded-lg transition-all duration-200 placeholder:text-description text-body focus:outline-none focus:ring-offset-0 disabled:opacity-50 disabled:cursor-not-allowed";
81
78
 
82
- return `${baseClasses} ${sizeStyles.padding} ${sizeStyles.text} ${sizeStyles.leading} ${variantClass()}`.trim();
79
+ return `${baseClasses} ${sizeStyles.padding} ${sizeStyles.text} ${sizeStyles.leading} ${variantClass()} ${className}`.trim();
83
80
  });
84
81
 
85
82
  const labelClasses = $derived(
@@ -121,16 +118,23 @@
121
118
  {name}
122
119
  bind:value
123
120
  {placeholder}
121
+ {required}
124
122
  {disabled}
125
123
  class={inputClasses()}
126
124
  oninput={handleInput}
127
125
  {onblur}
128
126
  aria-invalid={variant === "error" ? "true" : undefined}
127
+ aria-required={required ? "true" : undefined}
129
128
  aria-describedby={message ? `${inputId}-message` : undefined}
130
129
  {...restProps}
131
130
  />
132
131
  {#if message && variant !== "default"}
133
- <p id={`${inputId}-message`} class={messageClasses()} role="alert">
132
+ <p
133
+ id={`${inputId}-message`}
134
+ class={messageClasses()}
135
+ role={variant === "error" ? "alert" : "status"}
136
+ aria-live={variant === "error" ? "assertive" : "polite"}
137
+ >
134
138
  {#if getIcon()}
135
139
  {@const Icon = getIcon()}
136
140
  <Icon size={14} class="shrink-0" />
@@ -3,8 +3,10 @@ interface Props {
3
3
  value?: string;
4
4
  type?: string;
5
5
  name?: string;
6
+ class?: string;
6
7
  label?: string;
7
8
  placeholder?: string;
9
+ required?: boolean;
8
10
  disabled?: boolean;
9
11
  size?: SizeVariant;
10
12
  variant?: SemanticVariant;
@@ -0,0 +1,47 @@
1
+ <script lang="ts">
2
+ import ListItemRow, { type ListItemData } from "./ListItem.svelte";
3
+
4
+ export type ListItem = ListItemData;
5
+
6
+ interface Props {
7
+ /** Collection of items rendered in the list. */
8
+ items: ListItemData[];
9
+ /** Accessible label for the list container. */
10
+ ariaLabel?: string;
11
+ /** Optional active item id for current state styling. */
12
+ selectedId?: string;
13
+ /** Whether to show the arrow icon for each row. */
14
+ showArrow?: boolean;
15
+ /** Optional custom class names for the list container. */
16
+ className?: string;
17
+ /** Item click callback. */
18
+ onclick?: (item: ListItem, event: MouseEvent) => void;
19
+ }
20
+
21
+ let {
22
+ items,
23
+ ariaLabel = "List items",
24
+ selectedId = "",
25
+ showArrow = true,
26
+ className = "",
27
+ onclick,
28
+ ...restProps
29
+ }: Props = $props();
30
+
31
+ const listClasses = $derived(
32
+ `space-y-1 overflow-hidden rounded-xl border border-border bg-surface p-1 ${className}`.trim(),
33
+ );
34
+ </script>
35
+
36
+ <ul class={listClasses} role="list" aria-label={ariaLabel} {...restProps}>
37
+ {#each items as item (item.id)}
38
+ <li>
39
+ <ListItemRow
40
+ {item}
41
+ selected={item.id === selectedId}
42
+ {showArrow}
43
+ {onclick}
44
+ />
45
+ </li>
46
+ {/each}
47
+ </ul>
@@ -0,0 +1,19 @@
1
+ import { type ListItemData } from "./ListItem.svelte";
2
+ export type ListItem = ListItemData;
3
+ interface Props {
4
+ /** Collection of items rendered in the list. */
5
+ items: ListItemData[];
6
+ /** Accessible label for the list container. */
7
+ ariaLabel?: string;
8
+ /** Optional active item id for current state styling. */
9
+ selectedId?: string;
10
+ /** Whether to show the arrow icon for each row. */
11
+ showArrow?: boolean;
12
+ /** Optional custom class names for the list container. */
13
+ className?: string;
14
+ /** Item click callback. */
15
+ onclick?: (item: ListItem, event: MouseEvent) => void;
16
+ }
17
+ declare const List: import("svelte").Component<Props, {}, "">;
18
+ type List = ReturnType<typeof List>;
19
+ export default List;
@@ -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
 
@@ -41,7 +39,7 @@
41
39
 
42
40
  <div
43
41
  id={progressId}
44
- class="w-full bg-input rounded-full overflow-hidden {sizeClasses[size]}"
42
+ class="w-full border border-input-border bg-input rounded-full overflow-hidden {sizeClasses[size]}"
45
43
  role="progressbar"
46
44
  aria-valuenow={value}
47
45
  aria-valuemin="0"