@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,74 @@
1
+ import type { SignatureData, SignatureFont, SignatureValidationResult } from '../types/components.js';
2
+ interface Props {
3
+ /** Signature data containing image output and metadata */
4
+ value?: SignatureData | null;
5
+ /** Callback when signature changes */
6
+ onValueChange?: (data: SignatureData | null) => void;
7
+ /** Callback when signature is completed (meets validation) */
8
+ onComplete?: (data: SignatureData) => void;
9
+ /** Callback when consent state changes */
10
+ onConsentChange?: (consented: boolean) => void;
11
+ /** Input mode: draw with stylus/mouse or type with fonts */
12
+ mode?: 'draw' | 'type';
13
+ /** Whether to show mode toggle */
14
+ showModeToggle?: boolean;
15
+ /** Default typed name for type mode */
16
+ typedName?: string;
17
+ /** Stroke color (default: currentColor) */
18
+ strokeColor?: string;
19
+ /** Stroke width in pixels */
20
+ strokeWidth?: number;
21
+ /** Minimum stroke width for pressure */
22
+ minStrokeWidth?: number;
23
+ /** Maximum stroke width for pressure */
24
+ maxStrokeWidth?: number;
25
+ /** Whether to show stroke customization */
26
+ showStrokeCustomization?: boolean;
27
+ /** Available fonts for typed signatures */
28
+ fonts?: SignatureFont[];
29
+ /** Selected font family */
30
+ selectedFont?: string;
31
+ /** Preferred output format */
32
+ outputFormat?: 'png' | 'svg' | 'dataUrl';
33
+ /** PNG/image quality (0-1) */
34
+ imageQuality?: number;
35
+ /** Background color */
36
+ backgroundColor?: 'transparent' | 'white';
37
+ /** Minimum stroke points required (draw mode) */
38
+ minStrokePoints?: number;
39
+ /** Minimum stroke length in pixels (draw mode) */
40
+ minStrokeLength?: number;
41
+ /** Minimum characters for typed signature */
42
+ minTypedLength?: number;
43
+ /** Custom validation function */
44
+ validate?: (data: SignatureData) => SignatureValidationResult;
45
+ /** Whether to show consent checkbox */
46
+ showConsent?: boolean;
47
+ /** Consent checkbox text */
48
+ consentText?: string;
49
+ /** Whether consent is required */
50
+ requireConsent?: boolean;
51
+ /** Current consent state */
52
+ consented?: boolean;
53
+ /** Input ID */
54
+ id?: string;
55
+ /** Name attribute for form submission */
56
+ name?: string;
57
+ /** Whether disabled */
58
+ disabled?: boolean;
59
+ /** Whether required */
60
+ required?: boolean;
61
+ /** Error message */
62
+ error?: string;
63
+ /** Hint text */
64
+ hint?: string;
65
+ /** Label text */
66
+ label?: string;
67
+ /** Canvas height (CSS value) */
68
+ height?: string;
69
+ /** Additional class */
70
+ class?: string;
71
+ }
72
+ declare const Signature: import("svelte").Component<Props, {}, "value" | "consented">;
73
+ type Signature = ReturnType<typeof Signature>;
74
+ export default Signature;
@@ -0,0 +1,136 @@
1
+ <script lang="ts">
2
+ import { Slider as SliderPrimitive } from 'bits-ui';
3
+ import { cn } from '../utils.js';
4
+ import { tv, type VariantProps } from 'tailwind-variants';
5
+
6
+ const sliderVariants = tv({
7
+ slots: {
8
+ root: 'relative flex w-full touch-none select-none items-center',
9
+ range: 'absolute h-full bg-primary rounded-full',
10
+ thumb:
11
+ 'block rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
12
+ },
13
+ variants: {
14
+ size: {
15
+ sm: {
16
+ root: 'h-1.5',
17
+ thumb: 'h-4 w-4',
18
+ },
19
+ md: {
20
+ root: 'h-2',
21
+ thumb: 'h-5 w-5',
22
+ },
23
+ lg: {
24
+ root: 'h-3',
25
+ thumb: 'h-6 w-6',
26
+ },
27
+ },
28
+ },
29
+ defaultVariants: {
30
+ size: 'md',
31
+ },
32
+ });
33
+
34
+ type SliderVariants = VariantProps<typeof sliderVariants>;
35
+
36
+ interface Props {
37
+ /** Current value(s) - array for range slider */
38
+ value?: number[];
39
+ /** Minimum value */
40
+ min?: number;
41
+ /** Maximum value */
42
+ max?: number;
43
+ /** Step increment */
44
+ step?: number;
45
+ /** Whether the slider is disabled */
46
+ disabled?: boolean;
47
+ /** Layout orientation */
48
+ orientation?: 'horizontal' | 'vertical';
49
+ /** Callback when value changes during drag */
50
+ onValueChange?: (value: number[]) => void;
51
+ /** Callback when value is committed (drag ends) */
52
+ onValueCommit?: (value: number[]) => void;
53
+ /** Size variant */
54
+ size?: SliderVariants['size'];
55
+ /** Optional label */
56
+ label?: string;
57
+ /** Whether to show the current value */
58
+ showValue?: boolean;
59
+ /** Format function for displayed value */
60
+ formatValue?: (value: number) => string;
61
+ /** Additional class for the container */
62
+ class?: string;
63
+ }
64
+
65
+ let {
66
+ value = $bindable([50]),
67
+ min = 0,
68
+ max = 100,
69
+ step = 1,
70
+ disabled = false,
71
+ orientation = 'horizontal',
72
+ onValueChange,
73
+ onValueCommit,
74
+ size = 'md',
75
+ label,
76
+ showValue = false,
77
+ formatValue = (v) => String(v),
78
+ class: className,
79
+ }: Props = $props();
80
+
81
+ const styles = $derived(sliderVariants({ size }));
82
+
83
+ function handleValueChange(newValue: number[]) {
84
+ value = newValue;
85
+ onValueChange?.(newValue);
86
+ }
87
+
88
+ function handleValueCommit(newValue: number[]) {
89
+ onValueCommit?.(newValue);
90
+ }
91
+
92
+ const displayValue = $derived(
93
+ value.length === 1
94
+ ? formatValue(value[0])
95
+ : `${formatValue(value[0])} - ${formatValue(value[value.length - 1])}`
96
+ );
97
+ </script>
98
+
99
+ <div class={cn('w-full', className)}>
100
+ {#if label || showValue}
101
+ <div class="flex items-center justify-between mb-2">
102
+ {#if label}
103
+ <span class="text-sm font-medium leading-none">
104
+ {label}
105
+ </span>
106
+ {/if}
107
+ {#if showValue}
108
+ <span class="text-sm text-muted-foreground tabular-nums">
109
+ {displayValue}
110
+ </span>
111
+ {/if}
112
+ </div>
113
+ {/if}
114
+
115
+ <SliderPrimitive.Root
116
+ type="multiple"
117
+ {value}
118
+ {min}
119
+ {max}
120
+ {step}
121
+ {disabled}
122
+ {orientation}
123
+ onValueChange={handleValueChange}
124
+ onValueCommit={handleValueCommit}
125
+ class={cn(styles.root(), 'rounded-full bg-secondary')}
126
+ >
127
+ <SliderPrimitive.Range class={styles.range()} />
128
+ {#each value as _, i}
129
+ <SliderPrimitive.Thumb
130
+ index={i}
131
+ class={styles.thumb()}
132
+ aria-label={value.length > 1 ? `Thumb ${i + 1}` : label || 'Slider'}
133
+ />
134
+ {/each}
135
+ </SliderPrimitive.Root>
136
+ </div>
@@ -0,0 +1,30 @@
1
+ declare const Slider: import("svelte").Component<{
2
+ /** Current value(s) - array for range slider */
3
+ value?: number[];
4
+ /** Minimum value */
5
+ min?: number;
6
+ /** Maximum value */
7
+ max?: number;
8
+ /** Step increment */
9
+ step?: number;
10
+ /** Whether the slider is disabled */
11
+ disabled?: boolean;
12
+ /** Layout orientation */
13
+ orientation?: "horizontal" | "vertical";
14
+ /** Callback when value changes during drag */
15
+ onValueChange?: (value: number[]) => void;
16
+ /** Callback when value is committed (drag ends) */
17
+ onValueCommit?: (value: number[]) => void;
18
+ /** Size variant */
19
+ size?: "sm" | "md" | "lg" | undefined;
20
+ /** Optional label */
21
+ label?: string;
22
+ /** Whether to show the current value */
23
+ showValue?: boolean;
24
+ /** Format function for displayed value */
25
+ formatValue?: (value: number) => string;
26
+ /** Additional class for the container */
27
+ class?: string;
28
+ }, {}, "value">;
29
+ type Slider = ReturnType<typeof Slider>;
30
+ export default Slider;
@@ -0,0 +1,133 @@
1
+ <script lang="ts">
2
+ /**
3
+ * AuthLayout - Layout for authentication pages
4
+ *
5
+ * Features:
6
+ * - Centered card layout for auth forms
7
+ * - Logo with optional subtitle
8
+ * - Footer links (privacy, terms, etc.)
9
+ * - Optional background decoration
10
+ * - Responsive design
11
+ * - Composes AppShell for consistent base structure
12
+ *
13
+ * Use this layout for: login, signup, password reset, forgot password,
14
+ * email verification, 2FA, and other authentication flows.
15
+ */
16
+ import type { Snippet } from 'svelte';
17
+ import { cn } from '../../utils.js';
18
+ import AppShell from './AppShell.svelte';
19
+ import LogoMain from '../LogoMain.svelte';
20
+
21
+ interface FooterLink {
22
+ /** Link label */
23
+ label: string;
24
+ /** Link URL */
25
+ href: string;
26
+ /** Opens in new tab */
27
+ external?: boolean;
28
+ }
29
+
30
+ interface Props {
31
+ /** Custom logo snippet */
32
+ logo?: Snippet;
33
+ /** Logo subtitle for default logo (e.g., "Sign in to your account") */
34
+ logoSubtitle?: string;
35
+ /** Logo environment indicator for default logo */
36
+ logoEnvironment?: 'local' | 'dev' | 'demo';
37
+ /** Footer links (privacy policy, terms, etc.) */
38
+ footerLinks?: FooterLink[];
39
+ /** Custom footer content (replaces footer links) */
40
+ footer?: Snippet;
41
+ /** Show decorative background */
42
+ showBackground?: boolean;
43
+ /** Background variant */
44
+ backgroundVariant?: 'default' | 'gradient' | 'pattern';
45
+ /** Maximum width of content card */
46
+ maxWidth?: 'sm' | 'md' | 'lg';
47
+ /** Additional classes for the container */
48
+ class?: string;
49
+ /** Main content (auth form) */
50
+ children: Snippet;
51
+ }
52
+
53
+ let {
54
+ logo,
55
+ logoSubtitle,
56
+ logoEnvironment,
57
+ footerLinks = [],
58
+ footer,
59
+ showBackground = false,
60
+ backgroundVariant = 'default',
61
+ maxWidth = 'sm',
62
+ class: className,
63
+ children,
64
+ }: Props = $props();
65
+
66
+ const maxWidthClasses = {
67
+ sm: 'max-w-sm',
68
+ md: 'max-w-md',
69
+ lg: 'max-w-lg',
70
+ };
71
+
72
+ const backgroundClasses = {
73
+ default: 'bg-content-bg',
74
+ gradient: 'bg-gradient-to-br from-primary/5 via-content-bg to-accent/5',
75
+ pattern:
76
+ 'bg-content-bg bg-[radial-gradient(ellipse_at_top,_var(--tw-gradient-stops))] from-primary/10 via-content-bg to-content-bg',
77
+ };
78
+ </script>
79
+
80
+ <AppShell>
81
+ <div
82
+ class={cn(
83
+ 'flex h-screen flex-col items-center justify-center overflow-auto bg-content-bg px-4 py-8 sm:px-6 lg:px-8',
84
+ showBackground && backgroundClasses[backgroundVariant],
85
+ className
86
+ )}
87
+ >
88
+ <!-- Logo -->
89
+ <div class="mb-6 flex flex-col items-center">
90
+ {#if logo}
91
+ {@render logo()}
92
+ {:else}
93
+ <LogoMain
94
+ variant="stacked"
95
+ color="dark"
96
+ size="lg"
97
+ subtitle={logoSubtitle}
98
+ environment={logoEnvironment}
99
+ />
100
+ {/if}
101
+ </div>
102
+
103
+ <!-- Main Content Card -->
104
+ <main
105
+ id="main-content"
106
+ class={cn('w-full rounded-lg bg-background p-6 shadow-lg sm:p-8', maxWidthClasses[maxWidth])}
107
+ >
108
+ {@render children()}
109
+ </main>
110
+
111
+ <!-- Footer Links -->
112
+ {#if footer}
113
+ <footer class="mt-6">
114
+ {@render footer()}
115
+ </footer>
116
+ {:else if footerLinks.length > 0}
117
+ <footer class="mt-6">
118
+ <nav class="flex flex-wrap justify-center gap-x-6 gap-y-2" aria-label="Footer">
119
+ {#each footerLinks as link}
120
+ <a
121
+ href={link.href}
122
+ class="text-sm text-muted-foreground transition-colors hover:text-foreground"
123
+ target={link.external ? '_blank' : undefined}
124
+ rel={link.external ? 'noopener noreferrer' : undefined}
125
+ >
126
+ {link.label}
127
+ </a>
128
+ {/each}
129
+ </nav>
130
+ </footer>
131
+ {/if}
132
+ </div>
133
+ </AppShell>
@@ -0,0 +1,48 @@
1
+ /**
2
+ * AuthLayout - Layout for authentication pages
3
+ *
4
+ * Features:
5
+ * - Centered card layout for auth forms
6
+ * - Logo with optional subtitle
7
+ * - Footer links (privacy, terms, etc.)
8
+ * - Optional background decoration
9
+ * - Responsive design
10
+ * - Composes AppShell for consistent base structure
11
+ *
12
+ * Use this layout for: login, signup, password reset, forgot password,
13
+ * email verification, 2FA, and other authentication flows.
14
+ */
15
+ import type { Snippet } from 'svelte';
16
+ interface FooterLink {
17
+ /** Link label */
18
+ label: string;
19
+ /** Link URL */
20
+ href: string;
21
+ /** Opens in new tab */
22
+ external?: boolean;
23
+ }
24
+ interface Props {
25
+ /** Custom logo snippet */
26
+ logo?: Snippet;
27
+ /** Logo subtitle for default logo (e.g., "Sign in to your account") */
28
+ logoSubtitle?: string;
29
+ /** Logo environment indicator for default logo */
30
+ logoEnvironment?: 'local' | 'dev' | 'demo';
31
+ /** Footer links (privacy policy, terms, etc.) */
32
+ footerLinks?: FooterLink[];
33
+ /** Custom footer content (replaces footer links) */
34
+ footer?: Snippet;
35
+ /** Show decorative background */
36
+ showBackground?: boolean;
37
+ /** Background variant */
38
+ backgroundVariant?: 'default' | 'gradient' | 'pattern';
39
+ /** Maximum width of content card */
40
+ maxWidth?: 'sm' | 'md' | 'lg';
41
+ /** Additional classes for the container */
42
+ class?: string;
43
+ /** Main content (auth form) */
44
+ children: Snippet;
45
+ }
46
+ declare const AuthLayout: import("svelte").Component<Props, {}, "">;
47
+ type AuthLayout = ReturnType<typeof AuthLayout>;
48
+ export default AuthLayout;
@@ -11,20 +11,28 @@
11
11
  * - Integrated with sidebar store for state management
12
12
  */
13
13
  import type { Snippet } from 'svelte';
14
- import type { NavSection, NavItem, User, QuickLink } from '../../types/layout.js';
15
- import { cn } from '../../utils.js';
14
+ import type {
15
+ NavSection,
16
+ NavItem,
17
+ User,
18
+ QuickLink,
19
+ BackLink,
20
+ HeaderSearchConfig,
21
+ } from '../../types/layout.js';
16
22
  import { sidebarStore } from '../../stores/sidebar.svelte.js';
17
23
  import AppShell from './AppShell.svelte';
18
24
  import Sidebar from './Sidebar.svelte';
25
+ import Header from './Header.svelte';
19
26
 
20
- interface BackLink {
21
- /** Link label (e.g., "Back to Dashboard") */
22
- label: string;
23
- /** Link URL */
24
- href: string;
25
- /** Icon name for the icon snippet */
26
- icon?: string;
27
- }
27
+ type Breakpoint = 'sm' | 'md' | 'lg';
28
+
29
+ // Breakpoint pixel values (max-width for mobile detection)
30
+ // These match Tailwind's breakpoint - 1 (e.g., lg starts at 1024px, so mobile is <= 1023px)
31
+ const breakpointPixels: Record<Breakpoint, number> = {
32
+ sm: 639,
33
+ md: 767,
34
+ lg: 1023,
35
+ };
28
36
 
29
37
  interface Props {
30
38
  /** Navigation sections for sidebar */
@@ -49,10 +57,12 @@
49
57
  icon?: Snippet<[NavItem]>;
50
58
  /** Quick links displayed at bottom of sidebar */
51
59
  quickLinks?: QuickLink[];
52
- /** Quick links display mode: 'list' for stacked with labels, 'icons' for horizontal icons only */
60
+ /** Quick links display mode: 'list' for stacked with labels, 'icons' for stacked centered icons only */
53
61
  quickLinksDisplay?: 'list' | 'icons';
54
62
  /** Custom icon renderer for quick links */
55
63
  quickLinkIcon?: Snippet<[QuickLink]>;
64
+ /** Callback when a navigation item is clicked */
65
+ onNavigate?: (item: NavItem) => void;
56
66
  /** Custom content at start of header (after toggle button) */
57
67
  headerStart?: Snippet;
58
68
  /** Custom content at end of header */
@@ -61,6 +71,18 @@
61
71
  userMenu?: Snippet<[User]>;
62
72
  /** Sidebar footer content */
63
73
  sidebarFooter?: Snippet;
74
+ /** Sidebar width when expanded in pixels (default: 256) */
75
+ expandedWidth?: number;
76
+ /** Sidebar width when collapsed in pixels (default: 64) */
77
+ collapsedWidth?: number;
78
+ /** Enable search/filter input for navigation */
79
+ searchable?: boolean;
80
+ /** Placeholder text for search input */
81
+ searchPlaceholder?: string;
82
+ /** Header search configuration */
83
+ headerSearch?: HeaderSearchConfig;
84
+ /** Breakpoint at which to switch between mobile and desktop layouts */
85
+ mobileBreakpoint?: Breakpoint;
64
86
  /** Main content */
65
87
  children: Snippet;
66
88
  }
@@ -79,10 +101,17 @@
79
101
  quickLinks,
80
102
  quickLinksDisplay = 'list',
81
103
  quickLinkIcon,
104
+ onNavigate,
82
105
  headerStart,
83
106
  headerEnd,
84
107
  userMenu,
85
108
  sidebarFooter,
109
+ expandedWidth = 256,
110
+ collapsedWidth = 64,
111
+ searchable = false,
112
+ searchPlaceholder = 'Search...',
113
+ headerSearch,
114
+ mobileBreakpoint = 'lg',
86
115
  children,
87
116
  }: Props = $props();
88
117
 
@@ -111,7 +140,7 @@
111
140
 
112
141
  $effect(() => {
113
142
  if (typeof window !== 'undefined') {
114
- const mediaQuery = window.matchMedia('(max-width: 1023px)');
143
+ const mediaQuery = window.matchMedia(`(max-width: ${breakpointPixels[mobileBreakpoint]}px)`);
115
144
  isMobile = mediaQuery.matches;
116
145
  sidebarStore.setMobile(isMobile);
117
146
 
@@ -125,95 +154,92 @@
125
154
  }
126
155
  });
127
156
 
128
- // Initialize sidebar state
157
+ // Load persisted sidebar state and apply prop-based defaults
129
158
  $effect(() => {
130
- if (!sidebarCollapsed && !isMobile) {
131
- sidebarStore.open();
159
+ sidebarStore.initialize();
160
+
161
+ // Only apply prop-based initial state if no persisted state exists
162
+ if (!sidebarStore.hasPersistedState && !isMobile) {
163
+ if (sidebarCollapsed) {
164
+ sidebarStore.close();
165
+ } else {
166
+ sidebarStore.open();
167
+ }
132
168
  }
133
169
  });
134
170
 
135
- const sidebarWidth = $derived(isMobile ? 0 : sidebarStore.isOpen ? 256 : 64);
171
+ // Keyboard shortcuts
172
+ $effect(() => {
173
+ if (typeof window === 'undefined') return;
174
+
175
+ const handleKeydown = (e: KeyboardEvent) => {
176
+ // Toggle sidebar (Ctrl+\ or Cmd+\)
177
+ if ((e.ctrlKey || e.metaKey) && e.key === '\\') {
178
+ e.preventDefault();
179
+ sidebarStore.toggle();
180
+ return;
181
+ }
182
+
183
+ // Focus search with / key (when not in input/textarea)
184
+ if (e.key === '/' && !['INPUT', 'TEXTAREA'].includes(document.activeElement?.tagName || '')) {
185
+ e.preventDefault();
186
+ const searchInput = document.querySelector<HTMLInputElement>('[data-sidebar-search]');
187
+ if (searchInput) {
188
+ searchInput.focus();
189
+ }
190
+ }
191
+ };
192
+
193
+ window.addEventListener('keydown', handleKeydown);
194
+ return () => window.removeEventListener('keydown', handleKeydown);
195
+ });
196
+
197
+ const sidebarWidth = $derived(
198
+ isMobile ? 0 : sidebarStore.isOpen ? expandedWidth : collapsedWidth
199
+ );
136
200
  </script>
137
201
 
138
202
  <AppShell>
139
203
  <!-- Sidebar -->
140
204
  <Sidebar
141
205
  navigation={effectiveNavigation}
206
+ userRoles={user?.roles}
142
207
  variant={sidebarVariant}
143
208
  collapsed={!sidebarStore.isOpen && !isMobile}
144
209
  {isMobile}
145
210
  mobileOpen={isMobile && sidebarStore.isOpen}
146
211
  onClose={() => sidebarStore.close()}
212
+ {onNavigate}
147
213
  {logo}
148
214
  {icon}
149
215
  {quickLinks}
150
216
  {quickLinksDisplay}
151
217
  {quickLinkIcon}
152
218
  footer={sidebarFooter}
219
+ {expandedWidth}
220
+ {collapsedWidth}
221
+ {searchable}
222
+ {searchPlaceholder}
153
223
  />
154
224
 
155
225
  <!-- Main Content Area -->
156
226
  <div
157
- class="flex flex-1 flex-col transition-all duration-300"
227
+ class="flex flex-1 flex-col transition-all duration-300 motion-reduce:transition-none"
158
228
  style="margin-left: {sidebarWidth}px;"
159
229
  >
160
230
  {#if showHeader}
161
- <header
162
- class="sticky top-0 z-30 flex h-16 items-center justify-between border-b border-black bg-background px-4 lg:px-6"
231
+ <Header
232
+ showMenuButton={isMobile}
233
+ menuOpen={sidebarStore.isOpen}
234
+ onMenuClick={() => sidebarStore.toggle()}
235
+ showCollapseButton={!isMobile}
236
+ sidebarCollapsed={!sidebarStore.isOpen}
237
+ onCollapseClick={() => sidebarStore.toggle()}
238
+ title={pageTitle}
239
+ start={headerStart}
240
+ search={headerSearch}
163
241
  >
164
- <div class="flex items-center gap-4">
165
- <!-- Mobile Menu Button -->
166
- {#if isMobile}
167
- <button
168
- class="rounded-md p-2 hover:bg-accent hover:text-accent-foreground lg:hidden"
169
- onclick={() => sidebarStore.toggle()}
170
- aria-label="Open menu"
171
- >
172
- <svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
173
- <path
174
- stroke-linecap="round"
175
- stroke-linejoin="round"
176
- stroke-width="2"
177
- d="M4 6h16M4 12h16M4 18h16"
178
- />
179
- </svg>
180
- </button>
181
- {:else}
182
- <!-- Collapse Toggle Button (Desktop) -->
183
- <button
184
- class="rounded-md p-2 hover:bg-accent hover:text-accent-foreground"
185
- onclick={() => sidebarStore.toggle()}
186
- aria-label={sidebarStore.isOpen ? 'Collapse sidebar' : 'Expand sidebar'}
187
- >
188
- <svg
189
- class={cn('h-5 w-5 transition-transform', !sidebarStore.isOpen && 'rotate-180')}
190
- fill="none"
191
- viewBox="0 0 24 24"
192
- stroke="currentColor"
193
- >
194
- <path
195
- stroke-linecap="round"
196
- stroke-linejoin="round"
197
- stroke-width="2"
198
- d="M11 19l-7-7 7-7m8 14l-7-7 7-7"
199
- />
200
- </svg>
201
- </button>
202
- {/if}
203
-
204
- <!-- Page Title -->
205
- {#if pageTitle}
206
- <h2 class="text-lg font-semibold">{pageTitle}</h2>
207
- {/if}
208
-
209
- <!-- Custom Header Start -->
210
- {#if headerStart}
211
- {@render headerStart()}
212
- {/if}
213
- </div>
214
-
215
- <div class="flex items-center gap-4">
216
- <!-- Custom Header End -->
242
+ {#snippet end()}
217
243
  {#if headerEnd}
218
244
  {@render headerEnd()}
219
245
  {:else if user}
@@ -236,8 +262,8 @@
236
262
  </div>
237
263
  {/if}
238
264
  {/if}
239
- </div>
240
- </header>
265
+ {/snippet}
266
+ </Header>
241
267
  {/if}
242
268
 
243
269
  <!-- Main Content -->