@classic-homes/theme-svelte 0.1.3 → 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 (41) 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/AppShell.svelte +1 -1
  18. package/dist/lib/components/layout/DashboardLayout.svelte +63 -16
  19. package/dist/lib/components/layout/DashboardLayout.svelte.d.ts +12 -10
  20. package/dist/lib/components/layout/QuickLinks.svelte +49 -29
  21. package/dist/lib/components/layout/QuickLinks.svelte.d.ts +4 -2
  22. package/dist/lib/components/layout/Sidebar.svelte +345 -86
  23. package/dist/lib/components/layout/Sidebar.svelte.d.ts +12 -0
  24. package/dist/lib/components/layout/sidebar/SidebarFlyout.svelte +182 -0
  25. package/dist/lib/components/layout/sidebar/SidebarFlyout.svelte.d.ts +18 -0
  26. package/dist/lib/components/layout/sidebar/SidebarNavItem.svelte +369 -0
  27. package/dist/lib/components/layout/sidebar/SidebarNavItem.svelte.d.ts +25 -0
  28. package/dist/lib/components/layout/sidebar/SidebarSearch.svelte +121 -0
  29. package/dist/lib/components/layout/sidebar/SidebarSearch.svelte.d.ts +17 -0
  30. package/dist/lib/components/layout/sidebar/SidebarSection.svelte +144 -0
  31. package/dist/lib/components/layout/sidebar/SidebarSection.svelte.d.ts +25 -0
  32. package/dist/lib/components/layout/sidebar/index.d.ts +10 -0
  33. package/dist/lib/components/layout/sidebar/index.js +10 -0
  34. package/dist/lib/index.d.ts +9 -1
  35. package/dist/lib/index.js +8 -0
  36. package/dist/lib/schemas/auth.d.ts +6 -6
  37. package/dist/lib/stores/sidebar.svelte.d.ts +54 -0
  38. package/dist/lib/stores/sidebar.svelte.js +171 -1
  39. package/dist/lib/types/components.d.ts +105 -0
  40. package/dist/lib/types/layout.d.ts +32 -2
  41. package/package.json +1 -1
@@ -15,12 +15,18 @@
15
15
  import type { Snippet } from 'svelte';
16
16
  import type { NavSection, NavItem, QuickLink } from '../../types/layout.js';
17
17
  import { cn } from '../../utils.js';
18
+ import { sidebarStore } from '../../stores/sidebar.svelte.js';
18
19
  import LogoMain from '../LogoMain.svelte';
19
20
  import QuickLinks from './QuickLinks.svelte';
21
+ import SidebarSearch from './sidebar/SidebarSearch.svelte';
22
+ import SidebarSection from './sidebar/SidebarSection.svelte';
23
+ import SidebarFlyout from './sidebar/SidebarFlyout.svelte';
20
24
 
21
25
  interface Props {
22
26
  /** Navigation sections */
23
27
  navigation: NavSection[];
28
+ /** User roles for filtering navigation items */
29
+ userRoles?: string[];
24
30
  /** Whether sidebar is collapsed (desktop) */
25
31
  collapsed?: boolean;
26
32
  /** Whether currently on mobile */
@@ -29,6 +35,8 @@
29
35
  mobileOpen?: boolean;
30
36
  /** Callback when mobile sidebar should close */
31
37
  onClose?: () => void;
38
+ /** Callback when a navigation item is clicked */
39
+ onNavigate?: (item: NavItem) => void;
32
40
  /** Custom logo snippet */
33
41
  logo?: Snippet;
34
42
  /** Custom icon renderer for nav items */
@@ -45,16 +53,26 @@
45
53
  variant?: 'light' | 'dark';
46
54
  /** Use stronger/thicker border */
47
55
  strongBorder?: boolean;
56
+ /** Sidebar width when expanded in pixels (default: 256) */
57
+ expandedWidth?: number;
58
+ /** Sidebar width when collapsed in pixels (default: 64) */
59
+ collapsedWidth?: number;
60
+ /** Enable search/filter input for navigation */
61
+ searchable?: boolean;
62
+ /** Placeholder text for search input */
63
+ searchPlaceholder?: string;
48
64
  /** Additional classes */
49
65
  class?: string;
50
66
  }
51
67
 
52
68
  let {
53
69
  navigation,
70
+ userRoles,
54
71
  collapsed = false,
55
72
  isMobile = false,
56
73
  mobileOpen = false,
57
74
  onClose,
75
+ onNavigate,
58
76
  logo,
59
77
  icon,
60
78
  quickLinks,
@@ -63,13 +81,269 @@
63
81
  footer,
64
82
  variant = 'light',
65
83
  strongBorder = false,
84
+ expandedWidth = 256,
85
+ collapsedWidth = 64,
86
+ searchable = false,
87
+ searchPlaceholder = 'Search...',
66
88
  class: className,
67
89
  }: Props = $props();
68
90
 
91
+ // Helper to check if item is visible based on roles
92
+ function isItemVisible(item: NavItem): boolean {
93
+ if (!item.roles || item.roles.length === 0) return true;
94
+ if (!userRoles || userRoles.length === 0) return false;
95
+ return item.roles.some((role) => userRoles.includes(role));
96
+ }
97
+
98
+ // Recursively filter items based on roles
99
+ function filterItems(items: NavItem[]): NavItem[] {
100
+ return items.filter(isItemVisible).map((item) => ({
101
+ ...item,
102
+ children: item.children ? filterItems(item.children) : undefined,
103
+ }));
104
+ }
105
+
106
+ // Filter navigation based on user roles
107
+ const roleFilteredNavigation = $derived(
108
+ navigation
109
+ .map((section) => ({
110
+ ...section,
111
+ items: filterItems(section.items),
112
+ }))
113
+ .filter((section) => section.items.length > 0)
114
+ );
115
+
116
+ // Search state
117
+ let searchQuery = $state('');
118
+
119
+ // Filter items by search query
120
+ function filterItemsBySearch(items: NavItem[], query: string): NavItem[] {
121
+ return items
122
+ .filter((item) => {
123
+ const matches = item.name.toLowerCase().includes(query);
124
+ const childMatches = item.children?.some((child) =>
125
+ child.name.toLowerCase().includes(query)
126
+ );
127
+ return matches || childMatches;
128
+ })
129
+ .map((item) => ({
130
+ ...item,
131
+ // Keep all children if parent matches, otherwise filter children
132
+ children: item.name.toLowerCase().includes(query)
133
+ ? item.children
134
+ : item.children?.filter((child) => child.name.toLowerCase().includes(query)),
135
+ }));
136
+ }
137
+
138
+ // Combined filtered navigation (roles + search)
139
+ const filteredNavigation = $derived.by(() => {
140
+ if (!searchQuery.trim()) return roleFilteredNavigation;
141
+ const query = searchQuery.toLowerCase();
142
+ return roleFilteredNavigation
143
+ .map((section) => ({
144
+ ...section,
145
+ items: filterItemsBySearch(section.items, query),
146
+ }))
147
+ .filter((section) => section.items.length > 0);
148
+ });
149
+
69
150
  // Variant-based styling
70
151
  const isLight = $derived(variant === 'light');
71
-
72
152
  const sidebarWidth = $derived(collapsed ? 'w-16' : 'w-64');
153
+
154
+ // Track previous search to avoid redundant updates
155
+ let prevSearchQuery = '';
156
+
157
+ // Initialize expanded sections based on navigation data (only if no persisted state)
158
+ $effect(() => {
159
+ // Only run on initial mount (no search)
160
+ if (searchQuery.trim()) return;
161
+
162
+ // Set default expansions if no persisted state
163
+ const defaultExpanded: string[] = [];
164
+ roleFilteredNavigation.forEach((section) => {
165
+ // Expand by default unless explicitly set to collapsed
166
+ if (!section.collapsible || section.expanded !== false) {
167
+ defaultExpanded.push(section.id);
168
+ }
169
+ });
170
+ sidebarStore.setDefaultSections(defaultExpanded);
171
+ });
172
+
173
+ // Handle search-triggered expansions separately to avoid loops
174
+ $effect(() => {
175
+ const query = searchQuery.toLowerCase().trim();
176
+
177
+ // Only act when search query actually changes
178
+ if (query === prevSearchQuery) return;
179
+ prevSearchQuery = query;
180
+
181
+ if (query) {
182
+ // Force expand all sections during search
183
+ const allSectionIds = roleFilteredNavigation.map((section) => section.id);
184
+ sidebarStore.forceExpandSections(allSectionIds);
185
+
186
+ // Force expand items with matching children
187
+ const itemsToExpand: string[] = [];
188
+
189
+ const collectMatchingParents = (items: NavItem[]) => {
190
+ items.forEach((item) => {
191
+ if (item.children) {
192
+ const hasMatchingChild = item.children.some((child) =>
193
+ child.name.toLowerCase().includes(query)
194
+ );
195
+ if (hasMatchingChild) {
196
+ itemsToExpand.push(item.id);
197
+ }
198
+ collectMatchingParents(item.children);
199
+ }
200
+ });
201
+ };
202
+
203
+ roleFilteredNavigation.forEach((section) => collectMatchingParents(section.items));
204
+ if (itemsToExpand.length > 0) {
205
+ sidebarStore.forceExpandItems(itemsToExpand);
206
+ }
207
+ }
208
+ });
209
+
210
+ // Initialize expanded items based on item.expanded property (only if no persisted state)
211
+ $effect(() => {
212
+ // Only run on initial mount (no search)
213
+ if (searchQuery.trim()) return;
214
+
215
+ const defaultExpanded: string[] = [];
216
+ const collectExpanded = (items: NavItem[]) => {
217
+ items.forEach((item) => {
218
+ if (item.children && item.expanded) {
219
+ defaultExpanded.push(item.id);
220
+ }
221
+ if (item.children) {
222
+ collectExpanded(item.children);
223
+ }
224
+ });
225
+ };
226
+ roleFilteredNavigation.forEach((section) => collectExpanded(section.items));
227
+ sidebarStore.setDefaultItems(defaultExpanded);
228
+ });
229
+
230
+ // Flyout state for collapsed sidebar
231
+ let hoveredFlyoutItem = $state<NavItem | null>(null);
232
+ let flyoutTop = $state(0);
233
+ let flyoutCloseTimer: ReturnType<typeof setTimeout> | null = null;
234
+
235
+ function handleItemMouseEnter(item: NavItem, event: MouseEvent) {
236
+ if (!collapsed || isMobile) return;
237
+ // Cancel any pending close
238
+ if (flyoutCloseTimer) {
239
+ clearTimeout(flyoutCloseTimer);
240
+ flyoutCloseTimer = null;
241
+ }
242
+ const target = event.currentTarget as HTMLElement;
243
+ const rect = target.getBoundingClientRect();
244
+ flyoutTop = rect.top;
245
+ hoveredFlyoutItem = item;
246
+ }
247
+
248
+ function handleItemMouseLeave() {
249
+ // Delay closing to allow mouse to move to flyout
250
+ flyoutCloseTimer = setTimeout(() => {
251
+ hoveredFlyoutItem = null;
252
+ flyoutCloseTimer = null;
253
+ }, 150);
254
+ }
255
+
256
+ function handleFlyoutMouseEnter() {
257
+ // Cancel the close timer when mouse enters flyout
258
+ if (flyoutCloseTimer) {
259
+ clearTimeout(flyoutCloseTimer);
260
+ flyoutCloseTimer = null;
261
+ }
262
+ }
263
+
264
+ // Focus management for mobile sidebar
265
+ let mobileCloseButton = $state<HTMLButtonElement | null>(null);
266
+
267
+ $effect(() => {
268
+ if (isMobile && mobileOpen && mobileCloseButton) {
269
+ // Focus the close button when mobile sidebar opens
270
+ mobileCloseButton.focus();
271
+ }
272
+ });
273
+
274
+ // Wrap onNavigate to auto-close sidebar on mobile
275
+ function handleNavigate(item: NavItem) {
276
+ onNavigate?.(item);
277
+ // Auto-close on mobile after navigation
278
+ if (isMobile && mobileOpen) {
279
+ onClose?.();
280
+ }
281
+ }
282
+
283
+ // Swipe-to-dismiss state for mobile
284
+ let touchStartX = $state(0);
285
+ let touchCurrentX = $state(0);
286
+ let isSwiping = $state(false);
287
+
288
+ function handleTouchStart(e: TouchEvent) {
289
+ if (!isMobile || !mobileOpen) return;
290
+ touchStartX = e.touches[0].clientX;
291
+ touchCurrentX = touchStartX;
292
+ isSwiping = true;
293
+ }
294
+
295
+ function handleTouchMove(e: TouchEvent) {
296
+ if (!isSwiping) return;
297
+ touchCurrentX = e.touches[0].clientX;
298
+ }
299
+
300
+ function handleTouchEnd() {
301
+ if (!isSwiping) return;
302
+ const swipeDistance = touchStartX - touchCurrentX;
303
+ // Close if swiped left more than 100px
304
+ if (swipeDistance > 100) {
305
+ onClose?.();
306
+ }
307
+ isSwiping = false;
308
+ touchStartX = 0;
309
+ touchCurrentX = 0;
310
+ }
311
+
312
+ // Calculate swipe transform offset
313
+ const swipeOffset = $derived(
314
+ isSwiping && touchCurrentX < touchStartX ? Math.min(0, touchCurrentX - touchStartX) : 0
315
+ );
316
+
317
+ // Navigation element reference for arrow key handling
318
+ let navElement = $state<HTMLElement | null>(null);
319
+
320
+ // Handle arrow key navigation within the sidebar
321
+ function handleNavKeydown(event: KeyboardEvent) {
322
+ if (event.key !== 'ArrowUp' && event.key !== 'ArrowDown') return;
323
+
324
+ // Get all focusable nav items (links and buttons) within the nav
325
+ if (!navElement) return;
326
+ const focusable = navElement.querySelectorAll<HTMLElement>(
327
+ 'a[href]:not([disabled]):not([tabindex="-1"]), button:not([disabled]):not([tabindex="-1"])'
328
+ );
329
+ if (focusable.length === 0) return;
330
+
331
+ const focusableArray = Array.from(focusable);
332
+ const currentIndex = focusableArray.findIndex((el) => el === document.activeElement);
333
+
334
+ // Only handle if focus is within the nav
335
+ if (currentIndex === -1) return;
336
+
337
+ event.preventDefault();
338
+
339
+ if (event.key === 'ArrowDown') {
340
+ const nextIndex = currentIndex < focusableArray.length - 1 ? currentIndex + 1 : 0;
341
+ focusableArray[nextIndex].focus();
342
+ } else {
343
+ const prevIndex = currentIndex > 0 ? currentIndex - 1 : focusableArray.length - 1;
344
+ focusableArray[prevIndex].focus();
345
+ }
346
+ }
73
347
  </script>
74
348
 
75
349
  <!-- Mobile Overlay: z-40 positions below sidebar (z-50) but above main content -->
@@ -87,19 +361,25 @@
87
361
  <!-- Sidebar: z-50 positions above overlay and content, below modals (z-100+) -->
88
362
  <aside
89
363
  class={cn(
90
- 'fixed left-0 top-0 z-[var(--z-sidebar,50)] flex h-full flex-col transition-all duration-300',
364
+ 'fixed left-0 top-0 z-[var(--z-sidebar,50)] flex h-full flex-col transition-all duration-300 touch-pan-y',
91
365
  // Variant-based background and text colors
92
366
  isLight ? 'bg-background text-foreground' : 'bg-sidebar text-sidebar-foreground',
93
367
  strongBorder ? 'border-r-2 border-black' : 'border-r border-black',
94
368
  isMobile ? (mobileOpen ? 'translate-x-0 w-64' : '-translate-x-full w-64') : sidebarWidth,
369
+ // Disable transition during swipe for smooth following
370
+ isSwiping && 'transition-none',
95
371
  className
96
372
  )}
373
+ style={isSwiping && swipeOffset < 0 ? `transform: translateX(${swipeOffset}px)` : undefined}
97
374
  aria-label="Main navigation"
375
+ ontouchstart={handleTouchStart}
376
+ ontouchmove={handleTouchMove}
377
+ ontouchend={handleTouchEnd}
98
378
  >
99
379
  <!-- Logo Section -->
100
380
  <div
101
381
  class={cn(
102
- 'flex h-16 items-center border-b border-black px-4',
382
+ 'flex h-16 items-center border-b border-black px-4 overflow-hidden',
103
383
  collapsed && !isMobile && 'justify-center'
104
384
  )}
105
385
  >
@@ -114,90 +394,55 @@
114
394
  {/if}
115
395
  </div>
116
396
 
117
- <!-- Navigation -->
118
- <nav class="flex-1 overflow-y-auto py-4">
119
- {#each navigation as section}
120
- <div class="mb-4">
121
- {#if section.title && !collapsed}
122
- <h2
123
- class={cn(
124
- 'mb-2 px-4 text-xs font-semibold uppercase tracking-wider',
125
- isLight ? 'text-muted-foreground' : 'text-sidebar-foreground/60'
126
- )}
397
+ <!-- Search Input -->
398
+ {#if searchable && (!collapsed || isMobile)}
399
+ <SidebarSearch
400
+ value={searchQuery}
401
+ placeholder={searchPlaceholder}
402
+ {isLight}
403
+ onInput={(v) => (searchQuery = v)}
404
+ onClear={() => (searchQuery = '')}
405
+ />
406
+ {/if}
407
+
408
+ <!-- Navigation - keydown for arrow key navigation is intentional for a11y -->
409
+ <!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
410
+ <nav bind:this={navElement} class="flex-1 overflow-y-auto py-4" onkeydown={handleNavKeydown}>
411
+ {#each filteredNavigation as section}
412
+ <SidebarSection
413
+ {section}
414
+ {isLight}
415
+ {collapsed}
416
+ {isMobile}
417
+ {icon}
418
+ {searchQuery}
419
+ onNavigate={handleNavigate}
420
+ onItemMouseEnter={handleItemMouseEnter}
421
+ onItemMouseLeave={handleItemMouseLeave}
422
+ />
423
+ {:else}
424
+ {#if searchQuery.trim()}
425
+ <div class="px-4 py-8 text-center">
426
+ <svg
427
+ class="mx-auto h-8 w-8 text-muted-foreground/50 mb-2"
428
+ fill="none"
429
+ viewBox="0 0 24 24"
430
+ stroke="currentColor"
127
431
  >
128
- {section.title}
129
- </h2>
130
- {/if}
131
-
132
- <ul class="space-y-1 px-2">
133
- {#each section.items as item}
134
- <li>
135
- <a
136
- href={item.href}
137
- class={cn(
138
- 'flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition-colors',
139
- // Light variant styling
140
- isLight &&
141
- !item.active &&
142
- 'text-foreground hover:bg-accent hover:text-accent-foreground',
143
- isLight && item.active && 'bg-primary/10 text-primary',
144
- // Dark variant styling
145
- !isLight &&
146
- !item.active &&
147
- 'hover:bg-sidebar-accent hover:text-sidebar-accent-foreground',
148
- !isLight && item.active && 'bg-sidebar-primary text-sidebar-primary-foreground',
149
- collapsed && !isMobile && 'justify-center px-2'
150
- )}
151
- target={item.external ? '_blank' : undefined}
152
- rel={item.external ? 'noopener noreferrer' : undefined}
153
- aria-current={item.active ? 'page' : undefined}
154
- title={collapsed && !isMobile ? item.name : undefined}
155
- >
156
- {#if icon}
157
- <span class="h-5 w-5 shrink-0">
158
- {@render icon(item)}
159
- </span>
160
- {:else if item.icon}
161
- <span class="h-5 w-5 shrink-0 flex items-center justify-center">
162
- <!-- Default icon placeholder - apps should provide icon snippet -->
163
- <svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
164
- <circle cx="12" cy="12" r="10" stroke-width="2" />
165
- </svg>
166
- </span>
167
- {/if}
168
-
169
- {#if !collapsed || isMobile}
170
- <span class="flex-1 truncate">{item.name}</span>
171
-
172
- {#if item.badge !== undefined}
173
- <span
174
- class="ml-auto rounded-full bg-primary px-2 py-0.5 text-xs font-medium text-primary-foreground"
175
- >
176
- {item.badge}
177
- </span>
178
- {/if}
179
-
180
- {#if item.external}
181
- <svg
182
- class="h-4 w-4 opacity-50"
183
- fill="none"
184
- viewBox="0 0 24 24"
185
- stroke="currentColor"
186
- >
187
- <path
188
- stroke-linecap="round"
189
- stroke-linejoin="round"
190
- stroke-width="2"
191
- d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
192
- />
193
- </svg>
194
- {/if}
195
- {/if}
196
- </a>
197
- </li>
198
- {/each}
199
- </ul>
200
- </div>
432
+ <path
433
+ stroke-linecap="round"
434
+ stroke-linejoin="round"
435
+ stroke-width="2"
436
+ d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
437
+ />
438
+ </svg>
439
+ <p
440
+ class={cn('text-sm', isLight ? 'text-muted-foreground' : 'text-sidebar-foreground/60')}
441
+ >
442
+ No results for "{searchQuery}"
443
+ </p>
444
+ </div>
445
+ {/if}
201
446
  {/each}
202
447
  </nav>
203
448
 
@@ -223,8 +468,10 @@
223
468
  <!-- Mobile Close Button -->
224
469
  {#if isMobile}
225
470
  <button
471
+ bind:this={mobileCloseButton}
226
472
  class={cn(
227
473
  'absolute right-2 top-2 rounded-md p-2',
474
+ 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-1',
228
475
  isLight
229
476
  ? 'hover:bg-accent hover:text-accent-foreground'
230
477
  : 'hover:bg-sidebar-accent hover:text-sidebar-accent-foreground'
@@ -242,4 +489,16 @@
242
489
  </svg>
243
490
  </button>
244
491
  {/if}
492
+
493
+ <!-- Flyout Menu for Collapsed State -->
494
+ {#if collapsed && !isMobile && hoveredFlyoutItem}
495
+ <SidebarFlyout
496
+ item={hoveredFlyoutItem}
497
+ top={flyoutTop}
498
+ {isLight}
499
+ onNavigate={handleNavigate}
500
+ onMouseEnter={handleFlyoutMouseEnter}
501
+ onMouseLeave={handleItemMouseLeave}
502
+ />
503
+ {/if}
245
504
  </aside>
@@ -16,6 +16,8 @@ import type { NavSection, NavItem, QuickLink } from '../../types/layout.js';
16
16
  interface Props {
17
17
  /** Navigation sections */
18
18
  navigation: NavSection[];
19
+ /** User roles for filtering navigation items */
20
+ userRoles?: string[];
19
21
  /** Whether sidebar is collapsed (desktop) */
20
22
  collapsed?: boolean;
21
23
  /** Whether currently on mobile */
@@ -24,6 +26,8 @@ interface Props {
24
26
  mobileOpen?: boolean;
25
27
  /** Callback when mobile sidebar should close */
26
28
  onClose?: () => void;
29
+ /** Callback when a navigation item is clicked */
30
+ onNavigate?: (item: NavItem) => void;
27
31
  /** Custom logo snippet */
28
32
  logo?: Snippet;
29
33
  /** Custom icon renderer for nav items */
@@ -40,6 +44,14 @@ interface Props {
40
44
  variant?: 'light' | 'dark';
41
45
  /** Use stronger/thicker border */
42
46
  strongBorder?: boolean;
47
+ /** Sidebar width when expanded in pixels (default: 256) */
48
+ expandedWidth?: number;
49
+ /** Sidebar width when collapsed in pixels (default: 64) */
50
+ collapsedWidth?: number;
51
+ /** Enable search/filter input for navigation */
52
+ searchable?: boolean;
53
+ /** Placeholder text for search input */
54
+ searchPlaceholder?: string;
43
55
  /** Additional classes */
44
56
  class?: string;
45
57
  }