@classic-homes/theme-svelte 0.1.4 → 0.1.6

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 (55) hide show
  1. package/dist/lib/components/CardHeader.svelte +22 -2
  2. package/dist/lib/components/CardHeader.svelte.d.ts +5 -4
  3. package/dist/lib/components/Combobox.svelte +187 -0
  4. package/dist/lib/components/Combobox.svelte.d.ts +38 -0
  5. package/dist/lib/components/DateTimePicker.svelte +415 -0
  6. package/dist/lib/components/DateTimePicker.svelte.d.ts +31 -0
  7. package/dist/lib/components/HeaderSearch.svelte +340 -0
  8. package/dist/lib/components/HeaderSearch.svelte.d.ts +37 -0
  9. package/dist/lib/components/MultiSelect.svelte +244 -0
  10. package/dist/lib/components/MultiSelect.svelte.d.ts +40 -0
  11. package/dist/lib/components/NumberInput.svelte +205 -0
  12. package/dist/lib/components/NumberInput.svelte.d.ts +33 -0
  13. package/dist/lib/components/OTPInput.svelte +213 -0
  14. package/dist/lib/components/OTPInput.svelte.d.ts +23 -0
  15. package/dist/lib/components/PageHeader.svelte +6 -0
  16. package/dist/lib/components/PageHeader.svelte.d.ts +1 -1
  17. package/dist/lib/components/RadioGroup.svelte +124 -0
  18. package/dist/lib/components/RadioGroup.svelte.d.ts +31 -0
  19. package/dist/lib/components/Signature.svelte +1070 -0
  20. package/dist/lib/components/Signature.svelte.d.ts +74 -0
  21. package/dist/lib/components/Slider.svelte +136 -0
  22. package/dist/lib/components/Slider.svelte.d.ts +30 -0
  23. package/dist/lib/components/layout/AuthLayout.svelte +133 -0
  24. package/dist/lib/components/layout/AuthLayout.svelte.d.ts +48 -0
  25. package/dist/lib/components/layout/DashboardLayout.svelte +100 -74
  26. package/dist/lib/components/layout/DashboardLayout.svelte.d.ts +17 -10
  27. package/dist/lib/components/layout/ErrorLayout.svelte +206 -0
  28. package/dist/lib/components/layout/ErrorLayout.svelte.d.ts +52 -0
  29. package/dist/lib/components/layout/FormPageLayout.svelte +2 -8
  30. package/dist/lib/components/layout/Header.svelte +232 -41
  31. package/dist/lib/components/layout/Header.svelte.d.ts +71 -5
  32. package/dist/lib/components/layout/PublicLayout.svelte +54 -80
  33. package/dist/lib/components/layout/PublicLayout.svelte.d.ts +3 -1
  34. package/dist/lib/components/layout/QuickLinks.svelte +49 -29
  35. package/dist/lib/components/layout/QuickLinks.svelte.d.ts +4 -2
  36. package/dist/lib/components/layout/Sidebar.svelte +345 -86
  37. package/dist/lib/components/layout/Sidebar.svelte.d.ts +12 -0
  38. package/dist/lib/components/layout/sidebar/SidebarFlyout.svelte +182 -0
  39. package/dist/lib/components/layout/sidebar/SidebarFlyout.svelte.d.ts +18 -0
  40. package/dist/lib/components/layout/sidebar/SidebarNavItem.svelte +378 -0
  41. package/dist/lib/components/layout/sidebar/SidebarNavItem.svelte.d.ts +25 -0
  42. package/dist/lib/components/layout/sidebar/SidebarSearch.svelte +121 -0
  43. package/dist/lib/components/layout/sidebar/SidebarSearch.svelte.d.ts +17 -0
  44. package/dist/lib/components/layout/sidebar/SidebarSection.svelte +144 -0
  45. package/dist/lib/components/layout/sidebar/SidebarSection.svelte.d.ts +25 -0
  46. package/dist/lib/components/layout/sidebar/index.d.ts +10 -0
  47. package/dist/lib/components/layout/sidebar/index.js +10 -0
  48. package/dist/lib/index.d.ts +13 -2
  49. package/dist/lib/index.js +11 -0
  50. package/dist/lib/schemas/auth.d.ts +6 -6
  51. package/dist/lib/stores/sidebar.svelte.d.ts +54 -0
  52. package/dist/lib/stores/sidebar.svelte.js +171 -1
  53. package/dist/lib/types/components.d.ts +105 -0
  54. package/dist/lib/types/layout.d.ts +203 -3
  55. package/package.json +1 -1
@@ -0,0 +1,182 @@
1
+ <script lang="ts">
2
+ /**
3
+ * SidebarFlyout - Flyout menu for collapsed sidebar items
4
+ *
5
+ * Features entrance animation and keyboard navigation
6
+ */
7
+ import { fly } from 'svelte/transition';
8
+ import type { NavItem } from '../../../types/layout.js';
9
+ import { cn } from '../../../utils.js';
10
+
11
+ interface Props {
12
+ /** The item to show in the flyout */
13
+ item: NavItem;
14
+ /** Top position in pixels */
15
+ top: number;
16
+ /** Whether using light variant */
17
+ isLight?: boolean;
18
+ /** Callback when a navigation item is clicked */
19
+ onNavigate?: (item: NavItem) => void;
20
+ /** Callback when mouse enters flyout */
21
+ onMouseEnter?: () => void;
22
+ /** Callback when mouse leaves flyout */
23
+ onMouseLeave?: () => void;
24
+ }
25
+
26
+ let { item, top, isLight = true, onNavigate, onMouseEnter, onMouseLeave }: Props = $props();
27
+
28
+ let flyoutElement = $state<HTMLElement | null>(null);
29
+
30
+ // Get all focusable elements within the flyout
31
+ function getFocusableElements(): HTMLElement[] {
32
+ if (!flyoutElement) return [];
33
+ const elements = flyoutElement.querySelectorAll<HTMLElement>(
34
+ 'a[href]:not([disabled]):not([tabindex="-1"]), button:not([disabled]), [tabindex]:not([tabindex="-1"])'
35
+ );
36
+ return Array.from(elements);
37
+ }
38
+
39
+ function handleKeydown(event: KeyboardEvent) {
40
+ if (event.key === 'Escape') {
41
+ onMouseLeave?.();
42
+ return;
43
+ }
44
+
45
+ // Focus trap - cycle through focusable elements
46
+ if (event.key === 'Tab') {
47
+ const focusable = getFocusableElements();
48
+ if (focusable.length === 0) return;
49
+
50
+ const firstElement = focusable[0];
51
+ const lastElement = focusable[focusable.length - 1];
52
+
53
+ if (event.shiftKey) {
54
+ // Shift+Tab: go backwards
55
+ if (document.activeElement === firstElement || document.activeElement === flyoutElement) {
56
+ event.preventDefault();
57
+ lastElement.focus();
58
+ }
59
+ } else {
60
+ // Tab: go forwards
61
+ if (document.activeElement === lastElement) {
62
+ event.preventDefault();
63
+ firstElement.focus();
64
+ }
65
+ }
66
+ return;
67
+ }
68
+
69
+ // Arrow key navigation within flyout
70
+ if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
71
+ event.preventDefault();
72
+ const items = flyoutElement?.querySelectorAll('[role="menuitem"]');
73
+ if (!items || items.length === 0) return;
74
+
75
+ const currentIndex = Array.from(items).findIndex((el) => el === document.activeElement);
76
+
77
+ if (event.key === 'ArrowDown') {
78
+ const nextIndex = currentIndex < items.length - 1 ? currentIndex + 1 : 0;
79
+ (items[nextIndex] as HTMLElement).focus();
80
+ } else {
81
+ const prevIndex = currentIndex > 0 ? currentIndex - 1 : items.length - 1;
82
+ (items[prevIndex] as HTMLElement).focus();
83
+ }
84
+ }
85
+ }
86
+ </script>
87
+
88
+ {#snippet navBadge(badge: string | number | undefined)}
89
+ {#if badge !== undefined}
90
+ <span
91
+ class="ml-auto rounded-full bg-primary px-2 py-0.5 text-xs font-medium text-primary-foreground"
92
+ >
93
+ {badge}
94
+ </span>
95
+ {/if}
96
+ {/snippet}
97
+
98
+ <!-- svelte-ignore a11y_no_noninteractive_tabindex -->
99
+ <div
100
+ bind:this={flyoutElement}
101
+ class={cn(
102
+ 'fixed left-16 min-w-48 rounded-md border shadow-lg z-[calc(var(--z-sidebar,50)+10)]',
103
+ 'motion-reduce:transition-none',
104
+ isLight ? 'bg-background border-border' : 'bg-sidebar border-sidebar-foreground/20'
105
+ )}
106
+ style="top: {top}px;"
107
+ role="menu"
108
+ tabindex="0"
109
+ onmouseenter={onMouseEnter}
110
+ onmouseleave={onMouseLeave}
111
+ onkeydown={handleKeydown}
112
+ transition:fly={{ x: -10, duration: 150 }}
113
+ >
114
+ <div class="p-2">
115
+ <!-- Item name as header -->
116
+ <div
117
+ class={cn(
118
+ 'px-3 py-2 text-sm font-medium',
119
+ isLight ? 'text-foreground' : 'text-sidebar-foreground'
120
+ )}
121
+ >
122
+ {#if item.href && !item.disabled}
123
+ <a
124
+ href={item.href}
125
+ class="hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-1 rounded"
126
+ target={item.external ? '_blank' : undefined}
127
+ rel={item.external ? 'noopener noreferrer' : undefined}
128
+ onclick={() => onNavigate?.(item)}
129
+ >
130
+ {item.name}
131
+ {#if item.external}
132
+ <span class="text-xs opacity-50">(opens in new tab)</span>
133
+ {/if}
134
+ </a>
135
+ {:else}
136
+ <span class={item.disabled ? 'opacity-50' : ''}>
137
+ {item.name}
138
+ </span>
139
+ {/if}
140
+ </div>
141
+
142
+ <!-- Children if present -->
143
+ {#if item.children && item.children.length > 0}
144
+ <div
145
+ class={cn('border-t mt-1 pt-1', isLight ? 'border-border' : 'border-sidebar-foreground/20')}
146
+ >
147
+ {#each item.children as child}
148
+ <a
149
+ href={child.disabled ? undefined : child.href}
150
+ class={cn(
151
+ 'flex items-center gap-2 rounded-md px-3 py-2 text-sm transition-colors ml-2',
152
+ 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-1',
153
+ isLight
154
+ ? 'text-foreground hover:bg-accent hover:text-accent-foreground'
155
+ : 'text-sidebar-foreground/80 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground',
156
+ child.active &&
157
+ (isLight
158
+ ? 'bg-primary/10 text-primary'
159
+ : 'bg-sidebar-primary text-sidebar-primary-foreground'),
160
+ child.disabled && 'opacity-50 pointer-events-none cursor-not-allowed'
161
+ )}
162
+ target={child.external ? '_blank' : undefined}
163
+ rel={child.external ? 'noopener noreferrer' : undefined}
164
+ aria-disabled={child.disabled || undefined}
165
+ tabindex={child.disabled ? -1 : undefined}
166
+ onclick={(e) => {
167
+ if (child.disabled) {
168
+ e.preventDefault();
169
+ return;
170
+ }
171
+ onNavigate?.(child);
172
+ }}
173
+ role="menuitem"
174
+ >
175
+ {child.name}
176
+ {@render navBadge(child.badge)}
177
+ </a>
178
+ {/each}
179
+ </div>
180
+ {/if}
181
+ </div>
182
+ </div>
@@ -0,0 +1,18 @@
1
+ import type { NavItem } from '../../../types/layout.js';
2
+ interface Props {
3
+ /** The item to show in the flyout */
4
+ item: NavItem;
5
+ /** Top position in pixels */
6
+ top: number;
7
+ /** Whether using light variant */
8
+ isLight?: boolean;
9
+ /** Callback when a navigation item is clicked */
10
+ onNavigate?: (item: NavItem) => void;
11
+ /** Callback when mouse enters flyout */
12
+ onMouseEnter?: () => void;
13
+ /** Callback when mouse leaves flyout */
14
+ onMouseLeave?: () => void;
15
+ }
16
+ declare const SidebarFlyout: import("svelte").Component<Props, {}, "">;
17
+ type SidebarFlyout = ReturnType<typeof SidebarFlyout>;
18
+ export default SidebarFlyout;
@@ -0,0 +1,378 @@
1
+ <script lang="ts">
2
+ /**
3
+ * SidebarNavItem - Individual navigation item component
4
+ *
5
+ * Handles:
6
+ * - Items with children (expandable submenu)
7
+ * - Items without children (simple links/buttons)
8
+ * - Collapsed mode (icons only with flyout)
9
+ * - Disabled and loading states
10
+ */
11
+ import { slide } from 'svelte/transition';
12
+ import type { Snippet } from 'svelte';
13
+ import type { NavItem } from '../../../types/layout.js';
14
+ import { cn } from '../../../utils.js';
15
+ import { sidebarStore } from '../../../stores/sidebar.svelte.js';
16
+
17
+ interface Props {
18
+ /** The navigation item to render */
19
+ item: NavItem;
20
+ /** Whether using light variant */
21
+ isLight?: boolean;
22
+ /** Whether sidebar is collapsed */
23
+ collapsed?: boolean;
24
+ /** Whether currently on mobile */
25
+ isMobile?: boolean;
26
+ /** Custom icon renderer */
27
+ icon?: Snippet<[NavItem]>;
28
+ /** Search query for text highlighting */
29
+ searchQuery?: string;
30
+ /** Callback when item is clicked */
31
+ onNavigate?: (item: NavItem) => void;
32
+ /** Callback when mouse enters item (for flyout) */
33
+ onMouseEnter?: (item: NavItem, event: MouseEvent) => void;
34
+ /** Callback when mouse leaves item (for flyout) */
35
+ onMouseLeave?: () => void;
36
+ }
37
+
38
+ let {
39
+ item,
40
+ isLight = true,
41
+ collapsed = false,
42
+ isMobile = false,
43
+ icon,
44
+ searchQuery = '',
45
+ onNavigate,
46
+ onMouseEnter,
47
+ onMouseLeave,
48
+ }: Props = $props();
49
+
50
+ // Highlight matching text in item names
51
+ function highlightText(
52
+ text: string,
53
+ query: string
54
+ ): { before: string; match: string; after: string } | null {
55
+ if (!query.trim()) return null;
56
+ const lowerText = text.toLowerCase();
57
+ const lowerQuery = query.toLowerCase();
58
+ const index = lowerText.indexOf(lowerQuery);
59
+ if (index === -1) return null;
60
+ return {
61
+ before: text.slice(0, index),
62
+ match: text.slice(index, index + query.length),
63
+ after: text.slice(index + query.length),
64
+ };
65
+ }
66
+
67
+ const itemHighlight = $derived(highlightText(item.name, searchQuery));
68
+
69
+ const hasChildren = $derived(item.children && item.children.length > 0);
70
+ const isExpanded = $derived(sidebarStore.isItemExpanded(item.id));
71
+
72
+ function toggleItem() {
73
+ sidebarStore.toggleItem(item.id);
74
+ }
75
+
76
+ function getItemStateClasses(active?: boolean, disabled?: boolean): string {
77
+ return cn(
78
+ isLight && !active && 'text-foreground hover:bg-accent hover:text-accent-foreground',
79
+ isLight && active && 'bg-primary/10 text-primary',
80
+ !isLight &&
81
+ !active &&
82
+ 'text-sidebar-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground',
83
+ !isLight && active && 'bg-sidebar-primary text-sidebar-primary-foreground',
84
+ disabled && 'opacity-50 pointer-events-none cursor-not-allowed'
85
+ );
86
+ }
87
+
88
+ function handleClick(e: MouseEvent) {
89
+ if (item.disabled) {
90
+ e.preventDefault();
91
+ return;
92
+ }
93
+ onNavigate?.(item);
94
+ }
95
+
96
+ function handleMouseEnter(e: MouseEvent) {
97
+ onMouseEnter?.(item, e);
98
+ }
99
+ </script>
100
+
101
+ <!-- Reusable snippets -->
102
+ {#snippet navIcon(navItem: NavItem)}
103
+ {#if navItem.loading}
104
+ <span class="h-5 w-5 shrink-0 flex items-center justify-center">
105
+ <svg class="h-4 w-4 animate-spin" viewBox="0 0 24 24" fill="none">
106
+ <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
107
+ <path
108
+ class="opacity-75"
109
+ fill="currentColor"
110
+ d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
111
+ />
112
+ </svg>
113
+ </span>
114
+ {:else if icon}
115
+ <span class="h-5 w-5 shrink-0">
116
+ {@render icon(navItem)}
117
+ </span>
118
+ {:else if navItem.icon}
119
+ <span class="h-5 w-5 shrink-0 flex items-center justify-center">
120
+ <svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
121
+ <circle cx="12" cy="12" r="10" stroke-width="2" />
122
+ </svg>
123
+ </span>
124
+ {/if}
125
+ {/snippet}
126
+
127
+ {#snippet navBadge(badge: string | number | undefined)}
128
+ {#if badge !== undefined}
129
+ <span class="rounded-full bg-primary px-2 py-0.5 text-xs font-medium text-primary-foreground">
130
+ {badge}
131
+ </span>
132
+ {/if}
133
+ {/snippet}
134
+
135
+ {#snippet externalIndicator()}
136
+ <svg class="h-4 w-4 opacity-50" fill="none" viewBox="0 0 24 24" stroke="currentColor">
137
+ <path
138
+ stroke-linecap="round"
139
+ stroke-linejoin="round"
140
+ stroke-width="2"
141
+ d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
142
+ />
143
+ </svg>
144
+ {/snippet}
145
+
146
+ {#snippet highlightedName(
147
+ name: string,
148
+ highlight: { before: string; match: string; after: string } | null
149
+ )}
150
+ {#if highlight}
151
+ <span class="flex-1 truncate">
152
+ {highlight.before}<mark class="bg-yellow-200 dark:bg-yellow-900 text-inherit rounded px-0.5"
153
+ >{highlight.match}</mark
154
+ >{highlight.after}
155
+ </span>
156
+ {:else}
157
+ <span class="flex-1 truncate">{name}</span>
158
+ {/if}
159
+ {/snippet}
160
+
161
+ {#if hasChildren && !collapsed}
162
+ <!-- Item with children - expandable submenu -->
163
+ <div>
164
+ <div
165
+ class={cn(
166
+ 'group flex w-full items-center rounded-md transition-colors overflow-hidden',
167
+ isLight && !item.active && 'hover:bg-accent',
168
+ isLight && item.active && 'bg-primary/10',
169
+ !isLight && !item.active && 'hover:bg-sidebar-accent',
170
+ !isLight && item.active && 'bg-sidebar-primary'
171
+ )}
172
+ >
173
+ {#if item.href}
174
+ <!-- Navigable parent: link for main content -->
175
+ <a
176
+ href={item.disabled ? undefined : item.href}
177
+ class={cn(
178
+ 'flex flex-1 items-center gap-3 px-3 py-2 text-sm font-medium transition-colors overflow-hidden',
179
+ 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-1',
180
+ 'group-hover:underline underline-offset-2',
181
+ isLight && !item.active && 'text-foreground group-hover:text-accent-foreground',
182
+ isLight && item.active && 'text-primary',
183
+ !isLight &&
184
+ !item.active &&
185
+ 'text-sidebar-foreground group-hover:text-sidebar-accent-foreground',
186
+ !isLight && item.active && 'text-sidebar-primary-foreground',
187
+ item.disabled && 'opacity-50 pointer-events-none cursor-not-allowed'
188
+ )}
189
+ target={item.external ? '_blank' : undefined}
190
+ rel={item.external ? 'noopener noreferrer' : undefined}
191
+ aria-current={item.active ? 'page' : undefined}
192
+ aria-disabled={item.disabled || undefined}
193
+ tabindex={item.disabled ? -1 : undefined}
194
+ onclick={handleClick}
195
+ >
196
+ {@render navIcon(item)}
197
+ {@render highlightedName(item.name, itemHighlight)}
198
+ {@render navBadge(item.badge)}
199
+ {#if item.external}{@render externalIndicator()}{/if}
200
+ </a>
201
+ {:else}
202
+ <!-- Non-navigable parent: button for entire main area -->
203
+ <button
204
+ type="button"
205
+ class={cn(
206
+ 'flex flex-1 items-center gap-3 px-3 py-2 text-sm font-medium transition-colors overflow-hidden text-left',
207
+ 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-1',
208
+ isLight && !item.active && 'text-foreground group-hover:text-accent-foreground',
209
+ isLight && item.active && 'text-primary',
210
+ !isLight &&
211
+ !item.active &&
212
+ 'text-sidebar-foreground group-hover:text-sidebar-accent-foreground',
213
+ !isLight && item.active && 'text-sidebar-primary-foreground',
214
+ item.disabled && 'opacity-50 pointer-events-none cursor-not-allowed'
215
+ )}
216
+ onclick={() => !item.disabled && toggleItem()}
217
+ aria-expanded={isExpanded}
218
+ aria-disabled={item.disabled || undefined}
219
+ disabled={item.disabled}
220
+ >
221
+ {@render navIcon(item)}
222
+ {@render highlightedName(item.name, itemHighlight)}
223
+ {@render navBadge(item.badge)}
224
+ </button>
225
+ {/if}
226
+
227
+ <!-- Separate expand/collapse toggle button -->
228
+ <button
229
+ type="button"
230
+ class={cn(
231
+ 'flex items-center justify-center p-2 transition-colors rounded-md',
232
+ 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-1',
233
+ isLight
234
+ ? 'hover:bg-accent/50 group-hover:text-accent-foreground'
235
+ : 'text-sidebar-foreground hover:bg-sidebar-accent/50 group-hover:text-sidebar-accent-foreground'
236
+ )}
237
+ onclick={(e) => {
238
+ e.preventDefault();
239
+ e.stopPropagation();
240
+ toggleItem();
241
+ }}
242
+ aria-expanded={isExpanded}
243
+ aria-label={isExpanded ? `Collapse ${item.name}` : `Expand ${item.name}`}
244
+ >
245
+ <svg
246
+ class={cn(
247
+ 'h-4 w-4 transition-transform duration-200',
248
+ isExpanded ? 'rotate-0' : '-rotate-90'
249
+ )}
250
+ fill="none"
251
+ viewBox="0 0 24 24"
252
+ stroke="currentColor"
253
+ >
254
+ <path
255
+ stroke-linecap="round"
256
+ stroke-linejoin="round"
257
+ stroke-width="2"
258
+ d="M19 9l-7 7-7-7"
259
+ />
260
+ </svg>
261
+ </button>
262
+ </div>
263
+
264
+ {#if isExpanded}
265
+ <ul
266
+ class="mt-1 space-y-1 pl-4 motion-reduce:transition-none overflow-hidden"
267
+ transition:slide={{ duration: 200 }}
268
+ >
269
+ {#each item.children as child}
270
+ <li>
271
+ <a
272
+ href={child.disabled ? undefined : child.href}
273
+ class={cn(
274
+ 'flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition-colors overflow-hidden',
275
+ 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-1',
276
+ getItemStateClasses(child.active, child.disabled)
277
+ )}
278
+ target={child.external ? '_blank' : undefined}
279
+ rel={child.external ? 'noopener noreferrer' : undefined}
280
+ aria-current={child.active ? 'page' : undefined}
281
+ aria-disabled={child.disabled || undefined}
282
+ tabindex={child.disabled ? -1 : undefined}
283
+ onclick={(e) => {
284
+ if (child.disabled) {
285
+ e.preventDefault();
286
+ return;
287
+ }
288
+ onNavigate?.(child);
289
+ }}
290
+ >
291
+ {@render navIcon(child)}
292
+ {@render highlightedName(child.name, highlightText(child.name, searchQuery))}
293
+ {@render navBadge(child.badge)}
294
+ {#if child.external}{@render externalIndicator()}{/if}
295
+ </a>
296
+ </li>
297
+ {/each}
298
+ </ul>
299
+ {/if}
300
+ </div>
301
+ {:else}
302
+ <!-- Regular item or collapsed item -->
303
+ {#if item.href}
304
+ <!-- Item with href - render as link -->
305
+ <a
306
+ href={item.disabled ? undefined : item.href}
307
+ class={cn(
308
+ 'flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition-colors overflow-hidden relative',
309
+ 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-1',
310
+ getItemStateClasses(item.active, item.disabled),
311
+ collapsed && !isMobile && 'justify-center px-2'
312
+ )}
313
+ target={item.external ? '_blank' : undefined}
314
+ rel={item.external ? 'noopener noreferrer' : undefined}
315
+ aria-current={item.active ? 'page' : undefined}
316
+ aria-disabled={item.disabled || undefined}
317
+ tabindex={item.disabled ? -1 : undefined}
318
+ title={collapsed && !isMobile
319
+ ? item.external
320
+ ? `${item.name} (opens in new tab)`
321
+ : item.name
322
+ : undefined}
323
+ onclick={handleClick}
324
+ onmouseenter={handleMouseEnter}
325
+ onmouseleave={onMouseLeave}
326
+ >
327
+ {@render navIcon(item)}
328
+
329
+ {#if !collapsed || isMobile}
330
+ {@render highlightedName(item.name, itemHighlight)}
331
+ {@render navBadge(item.badge)}
332
+ {#if item.external}{@render externalIndicator()}{/if}
333
+ {/if}
334
+
335
+ <!-- Visual indicator for collapsed items with children -->
336
+ {#if collapsed && !isMobile && hasChildren}
337
+ <span class="absolute bottom-0.5 right-0.5">
338
+ <svg class="h-2 w-2 opacity-50" fill="currentColor" viewBox="0 0 8 8">
339
+ <path d="M0 0 L8 4 L0 8 Z" />
340
+ </svg>
341
+ </span>
342
+ {/if}
343
+ </a>
344
+ {:else}
345
+ <!-- Item without href (parent with children only, or placeholder) - render as button -->
346
+ <button
347
+ type="button"
348
+ class={cn(
349
+ 'flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition-colors overflow-hidden w-full text-left relative',
350
+ 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-1',
351
+ getItemStateClasses(item.active, item.disabled),
352
+ collapsed && !isMobile && 'justify-center px-2'
353
+ )}
354
+ title={collapsed && !isMobile ? item.name : undefined}
355
+ aria-haspopup={hasChildren ? 'menu' : undefined}
356
+ aria-disabled={item.disabled || undefined}
357
+ disabled={item.disabled}
358
+ onmouseenter={handleMouseEnter}
359
+ onmouseleave={onMouseLeave}
360
+ >
361
+ {@render navIcon(item)}
362
+
363
+ {#if !collapsed || isMobile}
364
+ {@render highlightedName(item.name, itemHighlight)}
365
+ {@render navBadge(item.badge)}
366
+ {/if}
367
+
368
+ <!-- Visual indicator for collapsed items with children -->
369
+ {#if collapsed && !isMobile && hasChildren}
370
+ <span class="absolute bottom-0.5 right-0.5">
371
+ <svg class="h-2 w-2 opacity-50" fill="currentColor" viewBox="0 0 8 8">
372
+ <path d="M0 0 L8 4 L0 8 Z" />
373
+ </svg>
374
+ </span>
375
+ {/if}
376
+ </button>
377
+ {/if}
378
+ {/if}
@@ -0,0 +1,25 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { NavItem } from '../../../types/layout.js';
3
+ interface Props {
4
+ /** The navigation item to render */
5
+ item: NavItem;
6
+ /** Whether using light variant */
7
+ isLight?: boolean;
8
+ /** Whether sidebar is collapsed */
9
+ collapsed?: boolean;
10
+ /** Whether currently on mobile */
11
+ isMobile?: boolean;
12
+ /** Custom icon renderer */
13
+ icon?: Snippet<[NavItem]>;
14
+ /** Search query for text highlighting */
15
+ searchQuery?: string;
16
+ /** Callback when item is clicked */
17
+ onNavigate?: (item: NavItem) => void;
18
+ /** Callback when mouse enters item (for flyout) */
19
+ onMouseEnter?: (item: NavItem, event: MouseEvent) => void;
20
+ /** Callback when mouse leaves item (for flyout) */
21
+ onMouseLeave?: () => void;
22
+ }
23
+ declare const SidebarNavItem: import("svelte").Component<Props, {}, "">;
24
+ type SidebarNavItem = ReturnType<typeof SidebarNavItem>;
25
+ export default SidebarNavItem;