@classic-homes/theme-svelte 0.1.4 → 0.1.5

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 (40) hide show
  1. package/dist/lib/components/Combobox.svelte +187 -0
  2. package/dist/lib/components/Combobox.svelte.d.ts +38 -0
  3. package/dist/lib/components/DateTimePicker.svelte +415 -0
  4. package/dist/lib/components/DateTimePicker.svelte.d.ts +31 -0
  5. package/dist/lib/components/MultiSelect.svelte +244 -0
  6. package/dist/lib/components/MultiSelect.svelte.d.ts +40 -0
  7. package/dist/lib/components/NumberInput.svelte +205 -0
  8. package/dist/lib/components/NumberInput.svelte.d.ts +33 -0
  9. package/dist/lib/components/OTPInput.svelte +213 -0
  10. package/dist/lib/components/OTPInput.svelte.d.ts +23 -0
  11. package/dist/lib/components/RadioGroup.svelte +124 -0
  12. package/dist/lib/components/RadioGroup.svelte.d.ts +31 -0
  13. package/dist/lib/components/Signature.svelte +1070 -0
  14. package/dist/lib/components/Signature.svelte.d.ts +74 -0
  15. package/dist/lib/components/Slider.svelte +136 -0
  16. package/dist/lib/components/Slider.svelte.d.ts +30 -0
  17. package/dist/lib/components/layout/DashboardLayout.svelte +63 -16
  18. package/dist/lib/components/layout/DashboardLayout.svelte.d.ts +12 -10
  19. package/dist/lib/components/layout/QuickLinks.svelte +49 -29
  20. package/dist/lib/components/layout/QuickLinks.svelte.d.ts +4 -2
  21. package/dist/lib/components/layout/Sidebar.svelte +345 -86
  22. package/dist/lib/components/layout/Sidebar.svelte.d.ts +12 -0
  23. package/dist/lib/components/layout/sidebar/SidebarFlyout.svelte +182 -0
  24. package/dist/lib/components/layout/sidebar/SidebarFlyout.svelte.d.ts +18 -0
  25. package/dist/lib/components/layout/sidebar/SidebarNavItem.svelte +369 -0
  26. package/dist/lib/components/layout/sidebar/SidebarNavItem.svelte.d.ts +25 -0
  27. package/dist/lib/components/layout/sidebar/SidebarSearch.svelte +121 -0
  28. package/dist/lib/components/layout/sidebar/SidebarSearch.svelte.d.ts +17 -0
  29. package/dist/lib/components/layout/sidebar/SidebarSection.svelte +144 -0
  30. package/dist/lib/components/layout/sidebar/SidebarSection.svelte.d.ts +25 -0
  31. package/dist/lib/components/layout/sidebar/index.d.ts +10 -0
  32. package/dist/lib/components/layout/sidebar/index.js +10 -0
  33. package/dist/lib/index.d.ts +9 -1
  34. package/dist/lib/index.js +8 -0
  35. package/dist/lib/schemas/auth.d.ts +6 -6
  36. package/dist/lib/stores/sidebar.svelte.d.ts +54 -0
  37. package/dist/lib/stores/sidebar.svelte.js +171 -1
  38. package/dist/lib/types/components.d.ts +105 -0
  39. package/dist/lib/types/layout.d.ts +32 -2
  40. 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,369 @@
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 && !active && 'hover:bg-sidebar-accent hover:text-sidebar-accent-foreground',
81
+ !isLight && active && 'bg-sidebar-primary text-sidebar-primary-foreground',
82
+ disabled && 'opacity-50 pointer-events-none cursor-not-allowed'
83
+ );
84
+ }
85
+
86
+ function handleClick(e: MouseEvent) {
87
+ if (item.disabled) {
88
+ e.preventDefault();
89
+ return;
90
+ }
91
+ onNavigate?.(item);
92
+ }
93
+
94
+ function handleMouseEnter(e: MouseEvent) {
95
+ onMouseEnter?.(item, e);
96
+ }
97
+ </script>
98
+
99
+ <!-- Reusable snippets -->
100
+ {#snippet navIcon(navItem: NavItem)}
101
+ {#if navItem.loading}
102
+ <span class="h-5 w-5 shrink-0 flex items-center justify-center">
103
+ <svg class="h-4 w-4 animate-spin" viewBox="0 0 24 24" fill="none">
104
+ <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
105
+ <path
106
+ class="opacity-75"
107
+ fill="currentColor"
108
+ d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
109
+ />
110
+ </svg>
111
+ </span>
112
+ {:else if icon}
113
+ <span class="h-5 w-5 shrink-0">
114
+ {@render icon(navItem)}
115
+ </span>
116
+ {:else if navItem.icon}
117
+ <span class="h-5 w-5 shrink-0 flex items-center justify-center">
118
+ <svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
119
+ <circle cx="12" cy="12" r="10" stroke-width="2" />
120
+ </svg>
121
+ </span>
122
+ {/if}
123
+ {/snippet}
124
+
125
+ {#snippet navBadge(badge: string | number | undefined)}
126
+ {#if badge !== undefined}
127
+ <span class="rounded-full bg-primary px-2 py-0.5 text-xs font-medium text-primary-foreground">
128
+ {badge}
129
+ </span>
130
+ {/if}
131
+ {/snippet}
132
+
133
+ {#snippet externalIndicator()}
134
+ <svg class="h-4 w-4 opacity-50" fill="none" viewBox="0 0 24 24" stroke="currentColor">
135
+ <path
136
+ stroke-linecap="round"
137
+ stroke-linejoin="round"
138
+ stroke-width="2"
139
+ d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
140
+ />
141
+ </svg>
142
+ {/snippet}
143
+
144
+ {#snippet highlightedName(
145
+ name: string,
146
+ highlight: { before: string; match: string; after: string } | null
147
+ )}
148
+ {#if highlight}
149
+ <span class="flex-1 truncate">
150
+ {highlight.before}<mark class="bg-yellow-200 dark:bg-yellow-900 text-inherit rounded px-0.5"
151
+ >{highlight.match}</mark
152
+ >{highlight.after}
153
+ </span>
154
+ {:else}
155
+ <span class="flex-1 truncate">{name}</span>
156
+ {/if}
157
+ {/snippet}
158
+
159
+ {#if hasChildren && !collapsed}
160
+ <!-- Item with children - expandable submenu -->
161
+ <div>
162
+ <div
163
+ class={cn(
164
+ 'flex w-full items-center rounded-md transition-colors overflow-hidden',
165
+ isLight && !item.active && 'hover:bg-accent',
166
+ isLight && item.active && 'bg-primary/10',
167
+ !isLight && !item.active && 'hover:bg-sidebar-accent',
168
+ !isLight && item.active && 'bg-sidebar-primary'
169
+ )}
170
+ >
171
+ {#if item.href}
172
+ <!-- Navigable parent: link for main content -->
173
+ <a
174
+ href={item.disabled ? undefined : item.href}
175
+ class={cn(
176
+ 'flex flex-1 items-center gap-3 px-3 py-2 text-sm font-medium transition-colors overflow-hidden',
177
+ 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-1',
178
+ isLight && !item.active && 'text-foreground hover:text-accent-foreground',
179
+ isLight && item.active && 'text-primary',
180
+ !isLight && !item.active && 'hover:text-sidebar-accent-foreground',
181
+ !isLight && item.active && 'text-sidebar-primary-foreground',
182
+ item.disabled && 'opacity-50 pointer-events-none cursor-not-allowed'
183
+ )}
184
+ target={item.external ? '_blank' : undefined}
185
+ rel={item.external ? 'noopener noreferrer' : undefined}
186
+ aria-current={item.active ? 'page' : undefined}
187
+ aria-disabled={item.disabled || undefined}
188
+ tabindex={item.disabled ? -1 : undefined}
189
+ onclick={handleClick}
190
+ >
191
+ {@render navIcon(item)}
192
+ {@render highlightedName(item.name, itemHighlight)}
193
+ {@render navBadge(item.badge)}
194
+ {#if item.external}{@render externalIndicator()}{/if}
195
+ </a>
196
+ {:else}
197
+ <!-- Non-navigable parent: button for entire main area -->
198
+ <button
199
+ type="button"
200
+ class={cn(
201
+ 'flex flex-1 items-center gap-3 px-3 py-2 text-sm font-medium transition-colors overflow-hidden text-left',
202
+ 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-1',
203
+ isLight && !item.active && 'text-foreground',
204
+ isLight && item.active && 'text-primary',
205
+ !isLight && !item.active && '',
206
+ !isLight && item.active && 'text-sidebar-primary-foreground',
207
+ item.disabled && 'opacity-50 pointer-events-none cursor-not-allowed'
208
+ )}
209
+ onclick={() => !item.disabled && toggleItem()}
210
+ aria-expanded={isExpanded}
211
+ aria-disabled={item.disabled || undefined}
212
+ disabled={item.disabled}
213
+ >
214
+ {@render navIcon(item)}
215
+ {@render highlightedName(item.name, itemHighlight)}
216
+ {@render navBadge(item.badge)}
217
+ </button>
218
+ {/if}
219
+
220
+ <!-- Separate expand/collapse toggle button -->
221
+ <button
222
+ type="button"
223
+ class={cn(
224
+ 'flex items-center justify-center p-2 transition-colors rounded-md',
225
+ 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-1',
226
+ isLight ? 'hover:bg-accent/50' : 'hover:bg-sidebar-accent/50'
227
+ )}
228
+ onclick={(e) => {
229
+ e.preventDefault();
230
+ e.stopPropagation();
231
+ toggleItem();
232
+ }}
233
+ aria-expanded={isExpanded}
234
+ aria-label={isExpanded ? `Collapse ${item.name}` : `Expand ${item.name}`}
235
+ >
236
+ <svg
237
+ class={cn(
238
+ 'h-4 w-4 transition-transform duration-200',
239
+ isExpanded ? 'rotate-0' : '-rotate-90'
240
+ )}
241
+ fill="none"
242
+ viewBox="0 0 24 24"
243
+ stroke="currentColor"
244
+ >
245
+ <path
246
+ stroke-linecap="round"
247
+ stroke-linejoin="round"
248
+ stroke-width="2"
249
+ d="M19 9l-7 7-7-7"
250
+ />
251
+ </svg>
252
+ </button>
253
+ </div>
254
+
255
+ {#if isExpanded}
256
+ <ul
257
+ class="mt-1 space-y-1 pl-4 motion-reduce:transition-none overflow-hidden"
258
+ transition:slide={{ duration: 200 }}
259
+ >
260
+ {#each item.children as child}
261
+ <li>
262
+ <a
263
+ href={child.disabled ? undefined : child.href}
264
+ class={cn(
265
+ 'flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition-colors overflow-hidden',
266
+ 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-1',
267
+ getItemStateClasses(child.active, child.disabled)
268
+ )}
269
+ target={child.external ? '_blank' : undefined}
270
+ rel={child.external ? 'noopener noreferrer' : undefined}
271
+ aria-current={child.active ? 'page' : undefined}
272
+ aria-disabled={child.disabled || undefined}
273
+ tabindex={child.disabled ? -1 : undefined}
274
+ onclick={(e) => {
275
+ if (child.disabled) {
276
+ e.preventDefault();
277
+ return;
278
+ }
279
+ onNavigate?.(child);
280
+ }}
281
+ >
282
+ {@render navIcon(child)}
283
+ {@render highlightedName(child.name, highlightText(child.name, searchQuery))}
284
+ {@render navBadge(child.badge)}
285
+ {#if child.external}{@render externalIndicator()}{/if}
286
+ </a>
287
+ </li>
288
+ {/each}
289
+ </ul>
290
+ {/if}
291
+ </div>
292
+ {:else}
293
+ <!-- Regular item or collapsed item -->
294
+ {#if item.href}
295
+ <!-- Item with href - render as link -->
296
+ <a
297
+ href={item.disabled ? undefined : item.href}
298
+ class={cn(
299
+ 'flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition-colors overflow-hidden relative',
300
+ 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-1',
301
+ getItemStateClasses(item.active, item.disabled),
302
+ collapsed && !isMobile && 'justify-center px-2'
303
+ )}
304
+ target={item.external ? '_blank' : undefined}
305
+ rel={item.external ? 'noopener noreferrer' : undefined}
306
+ aria-current={item.active ? 'page' : undefined}
307
+ aria-disabled={item.disabled || undefined}
308
+ tabindex={item.disabled ? -1 : undefined}
309
+ title={collapsed && !isMobile
310
+ ? item.external
311
+ ? `${item.name} (opens in new tab)`
312
+ : item.name
313
+ : undefined}
314
+ onclick={handleClick}
315
+ onmouseenter={handleMouseEnter}
316
+ onmouseleave={onMouseLeave}
317
+ >
318
+ {@render navIcon(item)}
319
+
320
+ {#if !collapsed || isMobile}
321
+ {@render highlightedName(item.name, itemHighlight)}
322
+ {@render navBadge(item.badge)}
323
+ {#if item.external}{@render externalIndicator()}{/if}
324
+ {/if}
325
+
326
+ <!-- Visual indicator for collapsed items with children -->
327
+ {#if collapsed && !isMobile && hasChildren}
328
+ <span class="absolute bottom-0.5 right-0.5">
329
+ <svg class="h-2 w-2 opacity-50" fill="currentColor" viewBox="0 0 8 8">
330
+ <path d="M0 0 L8 4 L0 8 Z" />
331
+ </svg>
332
+ </span>
333
+ {/if}
334
+ </a>
335
+ {:else}
336
+ <!-- Item without href (parent with children only, or placeholder) - render as button -->
337
+ <button
338
+ type="button"
339
+ class={cn(
340
+ 'flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition-colors overflow-hidden w-full text-left relative',
341
+ 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-1',
342
+ getItemStateClasses(item.active, item.disabled),
343
+ collapsed && !isMobile && 'justify-center px-2'
344
+ )}
345
+ title={collapsed && !isMobile ? item.name : undefined}
346
+ aria-haspopup={hasChildren ? 'menu' : undefined}
347
+ aria-disabled={item.disabled || undefined}
348
+ disabled={item.disabled}
349
+ onmouseenter={handleMouseEnter}
350
+ onmouseleave={onMouseLeave}
351
+ >
352
+ {@render navIcon(item)}
353
+
354
+ {#if !collapsed || isMobile}
355
+ {@render highlightedName(item.name, itemHighlight)}
356
+ {@render navBadge(item.badge)}
357
+ {/if}
358
+
359
+ <!-- Visual indicator for collapsed items with children -->
360
+ {#if collapsed && !isMobile && hasChildren}
361
+ <span class="absolute bottom-0.5 right-0.5">
362
+ <svg class="h-2 w-2 opacity-50" fill="currentColor" viewBox="0 0 8 8">
363
+ <path d="M0 0 L8 4 L0 8 Z" />
364
+ </svg>
365
+ </span>
366
+ {/if}
367
+ </button>
368
+ {/if}
369
+ {/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;