zabi-components 5.0.21 → 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 (64) hide show
  1. package/README.md +1 -0
  2. package/dist/atoms/CardContent.svelte +1 -1
  3. package/dist/atoms/CardHeader.svelte +1 -1
  4. package/dist/atoms/Checkbox.svelte +1 -7
  5. package/dist/atoms/CodeBlock.svelte +6 -6
  6. package/dist/atoms/ColorPicker.svelte +80 -72
  7. package/dist/atoms/Input.svelte +14 -10
  8. package/dist/atoms/Input.svelte.d.ts +2 -0
  9. package/dist/atoms/List.svelte +47 -0
  10. package/dist/atoms/List.svelte.d.ts +19 -0
  11. package/dist/atoms/ListItem.svelte +148 -0
  12. package/dist/atoms/ListItem.svelte.d.ts +35 -0
  13. package/dist/atoms/Progress.svelte +3 -5
  14. package/dist/atoms/Select.svelte +59 -16
  15. package/dist/atoms/Select.svelte.d.ts +13 -1
  16. package/dist/atoms/Textarea.svelte +11 -9
  17. package/dist/atoms/Textarea.svelte.d.ts +1 -0
  18. package/dist/atoms/Toggle.svelte +1 -7
  19. package/dist/atoms/Tooltip.svelte +8 -15
  20. package/dist/atoms/index.d.ts +2 -0
  21. package/dist/atoms/index.js +2 -0
  22. package/dist/components/atoms/index.d.ts +2 -0
  23. package/dist/components/atoms/index.d.ts.map +1 -1
  24. package/dist/components/index.d.ts +4 -0
  25. package/dist/components/index.d.ts.map +1 -1
  26. package/dist/components/molecules/index.d.ts +0 -1
  27. package/dist/components/molecules/index.d.ts.map +1 -1
  28. package/dist/components/molecules/navigation-menu-context.d.ts +7 -0
  29. package/dist/components/molecules/navigation-menu-context.d.ts.map +1 -0
  30. package/dist/components/organisms/index.d.ts +2 -0
  31. package/dist/components/organisms/index.d.ts.map +1 -1
  32. package/dist/index.d.ts +4 -0
  33. package/dist/index.js +5 -8
  34. package/dist/molecules/Alert.svelte +8 -3
  35. package/dist/molecules/ContactForm.svelte +63 -31
  36. package/dist/molecules/ContactForm.svelte.d.ts +1 -4
  37. package/dist/molecules/Dropdown.svelte +2 -1
  38. package/dist/molecules/ImageUpload.svelte +26 -3
  39. package/dist/molecules/ImageUpload.svelte.d.ts +1 -0
  40. package/dist/molecules/Modal.svelte +44 -43
  41. package/dist/molecules/NavigationMenu.svelte +7 -10
  42. package/dist/molecules/NavigationMenuContent.svelte +8 -8
  43. package/dist/molecules/NavigationMenuItem.svelte +7 -6
  44. package/dist/molecules/NavigationMenuLink.svelte +7 -5
  45. package/dist/molecules/NavigationMenuList.svelte +0 -7
  46. package/dist/molecules/NavigationMenuTrigger.svelte +7 -7
  47. package/dist/molecules/SlideUp.svelte +30 -0
  48. package/dist/molecules/Tabs.svelte +71 -4
  49. package/dist/molecules/index.d.ts +0 -1
  50. package/dist/molecules/index.js +0 -1
  51. package/dist/molecules/navigation-menu-context.d.ts +6 -0
  52. package/dist/molecules/navigation-menu-context.js +1 -0
  53. package/dist/organisms/Navigation.svelte +7 -4
  54. package/dist/organisms/Navigation.svelte.d.ts +1 -0
  55. package/dist/organisms/SidebarNavigation.svelte +420 -0
  56. package/dist/organisms/SidebarNavigation.svelte.d.ts +44 -0
  57. package/dist/organisms/SidebarProjectPanel.svelte +174 -0
  58. package/dist/organisms/SidebarProjectPanel.svelte.d.ts +32 -0
  59. package/dist/organisms/index.d.ts +2 -0
  60. package/dist/organisms/index.js +2 -0
  61. package/dist/zabi-components.css +302 -0
  62. package/package.json +5 -4
  63. package/dist/molecules/Sidebar.svelte +0 -324
  64. package/dist/molecules/Sidebar.svelte.d.ts +0 -30
package/README.md CHANGED
@@ -1556,6 +1556,7 @@ Zabi Components is built with accessibility in mind:
1556
1556
  - Logical tab order
1557
1557
 
1558
1558
  See [ACCESSIBILITY.md](./docs/ACCESSIBILITY.md) and [KEYBOARD_NAVIGATION.md](./docs/KEYBOARD_NAVIGATION.md) for detailed documentation.
1559
+ See [DESIGN_SYSTEM_REMEDIATION_TRACKER.md](./docs/DESIGN_SYSTEM_REMEDIATION_TRACKER.md), [AUDIT_RUNBOOK.md](./docs/AUDIT_RUNBOOK.md), and [AUDIT_SCORECARD_TEMPLATE.md](./docs/AUDIT_SCORECARD_TEMPLATE.md) for audit and remediation workflows.
1559
1560
 
1560
1561
  ## Variant System
1561
1562
 
@@ -17,7 +17,7 @@
17
17
  }: Props = $props();
18
18
  </script>
19
19
 
20
- <div class="p-6 pt-0 {className}" {...restProps}>
20
+ <div class={className} {...restProps}>
21
21
  {#if image}
22
22
  <img
23
23
  src={image}
@@ -24,7 +24,7 @@
24
24
  });
25
25
  </script>
26
26
 
27
- <header class="flex flex-col space-y-1.5 p-6 {className}" {...restProps}>
27
+ <header class="flex flex-col space-y-1.5 pb-4{className}" {...restProps}>
28
28
  {#if title}
29
29
  <svelte:element this={headingTag} class={headingClasses()}>
30
30
  {title}
@@ -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;
@@ -92,31 +92,31 @@
92
92
 
93
93
  :global(.language-svelte .token.tag),
94
94
  :global(.language-html .token.tag) {
95
- color: #f92672;
95
+ color: var(--color-error-text);
96
96
  }
97
97
 
98
98
  :global(.language-svelte .token.attr-name),
99
99
  :global(.language-html .token.attr-name) {
100
- color: #a6e22e;
100
+ color: var(--color-success-text);
101
101
  }
102
102
 
103
103
  :global(.language-svelte .token.attr-value),
104
104
  :global(.language-html .token.attr-value) {
105
- color: #e6db74;
105
+ color: var(--color-warning-text);
106
106
  }
107
107
 
108
108
  :global(.language-javascript .token.keyword),
109
109
  :global(.language-typescript .token.keyword) {
110
- color: #66d9ef;
110
+ color: var(--color-info-text);
111
111
  }
112
112
 
113
113
  :global(.language-javascript .token.string),
114
114
  :global(.language-typescript .token.string) {
115
- color: #e6db74;
115
+ color: var(--color-warning-text);
116
116
  }
117
117
 
118
118
  :global(.language-javascript .token.function),
119
119
  :global(.language-typescript .token.function) {
120
- color: #a6e22e;
120
+ color: var(--color-success-text);
121
121
  }
122
122
  </style>
@@ -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,14 +285,14 @@
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}
295
+ use:setPickerContainer
287
296
  class="absolute top-12 right-0 z-50 bg-input rounded-2xl shadow-lg p-4 w-80"
288
297
  role="dialog"
289
298
  aria-label="Color picker"
@@ -291,7 +300,7 @@
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",
@@ -79,7 +76,7 @@
79
76
  const baseClasses =
80
77
  "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";
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}