@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.
- package/dist/lib/components/Combobox.svelte +187 -0
- package/dist/lib/components/Combobox.svelte.d.ts +38 -0
- package/dist/lib/components/DateTimePicker.svelte +415 -0
- package/dist/lib/components/DateTimePicker.svelte.d.ts +31 -0
- package/dist/lib/components/MultiSelect.svelte +244 -0
- package/dist/lib/components/MultiSelect.svelte.d.ts +40 -0
- package/dist/lib/components/NumberInput.svelte +205 -0
- package/dist/lib/components/NumberInput.svelte.d.ts +33 -0
- package/dist/lib/components/OTPInput.svelte +213 -0
- package/dist/lib/components/OTPInput.svelte.d.ts +23 -0
- package/dist/lib/components/RadioGroup.svelte +124 -0
- package/dist/lib/components/RadioGroup.svelte.d.ts +31 -0
- package/dist/lib/components/Signature.svelte +1070 -0
- package/dist/lib/components/Signature.svelte.d.ts +74 -0
- package/dist/lib/components/Slider.svelte +136 -0
- package/dist/lib/components/Slider.svelte.d.ts +30 -0
- package/dist/lib/components/layout/DashboardLayout.svelte +63 -16
- package/dist/lib/components/layout/DashboardLayout.svelte.d.ts +12 -10
- package/dist/lib/components/layout/QuickLinks.svelte +49 -29
- package/dist/lib/components/layout/QuickLinks.svelte.d.ts +4 -2
- package/dist/lib/components/layout/Sidebar.svelte +345 -86
- package/dist/lib/components/layout/Sidebar.svelte.d.ts +12 -0
- package/dist/lib/components/layout/sidebar/SidebarFlyout.svelte +182 -0
- package/dist/lib/components/layout/sidebar/SidebarFlyout.svelte.d.ts +18 -0
- package/dist/lib/components/layout/sidebar/SidebarNavItem.svelte +369 -0
- package/dist/lib/components/layout/sidebar/SidebarNavItem.svelte.d.ts +25 -0
- package/dist/lib/components/layout/sidebar/SidebarSearch.svelte +121 -0
- package/dist/lib/components/layout/sidebar/SidebarSearch.svelte.d.ts +17 -0
- package/dist/lib/components/layout/sidebar/SidebarSection.svelte +144 -0
- package/dist/lib/components/layout/sidebar/SidebarSection.svelte.d.ts +25 -0
- package/dist/lib/components/layout/sidebar/index.d.ts +10 -0
- package/dist/lib/components/layout/sidebar/index.js +10 -0
- package/dist/lib/index.d.ts +9 -1
- package/dist/lib/index.js +8 -0
- package/dist/lib/schemas/auth.d.ts +6 -6
- package/dist/lib/stores/sidebar.svelte.d.ts +54 -0
- package/dist/lib/stores/sidebar.svelte.js +171 -1
- package/dist/lib/types/components.d.ts +105 -0
- package/dist/lib/types/layout.d.ts +32 -2
- 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;
|
|
@@ -11,21 +11,12 @@
|
|
|
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';
|
|
14
|
+
import type { NavSection, NavItem, User, QuickLink, BackLink } from '../../types/layout.js';
|
|
15
15
|
import { cn } from '../../utils.js';
|
|
16
16
|
import { sidebarStore } from '../../stores/sidebar.svelte.js';
|
|
17
17
|
import AppShell from './AppShell.svelte';
|
|
18
18
|
import Sidebar from './Sidebar.svelte';
|
|
19
19
|
|
|
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
|
-
}
|
|
28
|
-
|
|
29
20
|
interface Props {
|
|
30
21
|
/** Navigation sections for sidebar */
|
|
31
22
|
navigation: NavSection[];
|
|
@@ -49,10 +40,12 @@
|
|
|
49
40
|
icon?: Snippet<[NavItem]>;
|
|
50
41
|
/** Quick links displayed at bottom of sidebar */
|
|
51
42
|
quickLinks?: QuickLink[];
|
|
52
|
-
/** Quick links display mode: 'list' for stacked with labels, 'icons' for
|
|
43
|
+
/** Quick links display mode: 'list' for stacked with labels, 'icons' for stacked centered icons only */
|
|
53
44
|
quickLinksDisplay?: 'list' | 'icons';
|
|
54
45
|
/** Custom icon renderer for quick links */
|
|
55
46
|
quickLinkIcon?: Snippet<[QuickLink]>;
|
|
47
|
+
/** Callback when a navigation item is clicked */
|
|
48
|
+
onNavigate?: (item: NavItem) => void;
|
|
56
49
|
/** Custom content at start of header (after toggle button) */
|
|
57
50
|
headerStart?: Snippet;
|
|
58
51
|
/** Custom content at end of header */
|
|
@@ -61,6 +54,14 @@
|
|
|
61
54
|
userMenu?: Snippet<[User]>;
|
|
62
55
|
/** Sidebar footer content */
|
|
63
56
|
sidebarFooter?: Snippet;
|
|
57
|
+
/** Sidebar width when expanded in pixels (default: 256) */
|
|
58
|
+
expandedWidth?: number;
|
|
59
|
+
/** Sidebar width when collapsed in pixels (default: 64) */
|
|
60
|
+
collapsedWidth?: number;
|
|
61
|
+
/** Enable search/filter input for navigation */
|
|
62
|
+
searchable?: boolean;
|
|
63
|
+
/** Placeholder text for search input */
|
|
64
|
+
searchPlaceholder?: string;
|
|
64
65
|
/** Main content */
|
|
65
66
|
children: Snippet;
|
|
66
67
|
}
|
|
@@ -79,10 +80,15 @@
|
|
|
79
80
|
quickLinks,
|
|
80
81
|
quickLinksDisplay = 'list',
|
|
81
82
|
quickLinkIcon,
|
|
83
|
+
onNavigate,
|
|
82
84
|
headerStart,
|
|
83
85
|
headerEnd,
|
|
84
86
|
userMenu,
|
|
85
87
|
sidebarFooter,
|
|
88
|
+
expandedWidth = 256,
|
|
89
|
+
collapsedWidth = 64,
|
|
90
|
+
searchable = false,
|
|
91
|
+
searchPlaceholder = 'Search...',
|
|
86
92
|
children,
|
|
87
93
|
}: Props = $props();
|
|
88
94
|
|
|
@@ -125,36 +131,77 @@
|
|
|
125
131
|
}
|
|
126
132
|
});
|
|
127
133
|
|
|
128
|
-
//
|
|
134
|
+
// Load persisted sidebar state and apply prop-based defaults
|
|
129
135
|
$effect(() => {
|
|
130
|
-
|
|
131
|
-
|
|
136
|
+
sidebarStore.initialize();
|
|
137
|
+
|
|
138
|
+
// Only apply prop-based initial state if no persisted state exists
|
|
139
|
+
if (!sidebarStore.hasPersistedState && !isMobile) {
|
|
140
|
+
if (sidebarCollapsed) {
|
|
141
|
+
sidebarStore.close();
|
|
142
|
+
} else {
|
|
143
|
+
sidebarStore.open();
|
|
144
|
+
}
|
|
132
145
|
}
|
|
133
146
|
});
|
|
134
147
|
|
|
135
|
-
|
|
148
|
+
// Keyboard shortcuts
|
|
149
|
+
$effect(() => {
|
|
150
|
+
if (typeof window === 'undefined') return;
|
|
151
|
+
|
|
152
|
+
const handleKeydown = (e: KeyboardEvent) => {
|
|
153
|
+
// Toggle sidebar (Ctrl+\ or Cmd+\)
|
|
154
|
+
if ((e.ctrlKey || e.metaKey) && e.key === '\\') {
|
|
155
|
+
e.preventDefault();
|
|
156
|
+
sidebarStore.toggle();
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Focus search with / key (when not in input/textarea)
|
|
161
|
+
if (e.key === '/' && !['INPUT', 'TEXTAREA'].includes(document.activeElement?.tagName || '')) {
|
|
162
|
+
e.preventDefault();
|
|
163
|
+
const searchInput = document.querySelector<HTMLInputElement>('[data-sidebar-search]');
|
|
164
|
+
if (searchInput) {
|
|
165
|
+
searchInput.focus();
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
window.addEventListener('keydown', handleKeydown);
|
|
171
|
+
return () => window.removeEventListener('keydown', handleKeydown);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
const sidebarWidth = $derived(
|
|
175
|
+
isMobile ? 0 : sidebarStore.isOpen ? expandedWidth : collapsedWidth
|
|
176
|
+
);
|
|
136
177
|
</script>
|
|
137
178
|
|
|
138
179
|
<AppShell>
|
|
139
180
|
<!-- Sidebar -->
|
|
140
181
|
<Sidebar
|
|
141
182
|
navigation={effectiveNavigation}
|
|
183
|
+
userRoles={user?.roles}
|
|
142
184
|
variant={sidebarVariant}
|
|
143
185
|
collapsed={!sidebarStore.isOpen && !isMobile}
|
|
144
186
|
{isMobile}
|
|
145
187
|
mobileOpen={isMobile && sidebarStore.isOpen}
|
|
146
188
|
onClose={() => sidebarStore.close()}
|
|
189
|
+
{onNavigate}
|
|
147
190
|
{logo}
|
|
148
191
|
{icon}
|
|
149
192
|
{quickLinks}
|
|
150
193
|
{quickLinksDisplay}
|
|
151
194
|
{quickLinkIcon}
|
|
152
195
|
footer={sidebarFooter}
|
|
196
|
+
{expandedWidth}
|
|
197
|
+
{collapsedWidth}
|
|
198
|
+
{searchable}
|
|
199
|
+
{searchPlaceholder}
|
|
153
200
|
/>
|
|
154
201
|
|
|
155
202
|
<!-- Main Content Area -->
|
|
156
203
|
<div
|
|
157
|
-
class="flex flex-1 flex-col transition-all duration-300"
|
|
204
|
+
class="flex flex-1 flex-col transition-all duration-300 motion-reduce:transition-none"
|
|
158
205
|
style="margin-left: {sidebarWidth}px;"
|
|
159
206
|
>
|
|
160
207
|
{#if showHeader}
|
|
@@ -10,15 +10,7 @@
|
|
|
10
10
|
* - Integrated with sidebar store for state management
|
|
11
11
|
*/
|
|
12
12
|
import type { Snippet } from 'svelte';
|
|
13
|
-
import type { NavSection, NavItem, User, QuickLink } from '../../types/layout.js';
|
|
14
|
-
interface BackLink {
|
|
15
|
-
/** Link label (e.g., "Back to Dashboard") */
|
|
16
|
-
label: string;
|
|
17
|
-
/** Link URL */
|
|
18
|
-
href: string;
|
|
19
|
-
/** Icon name for the icon snippet */
|
|
20
|
-
icon?: string;
|
|
21
|
-
}
|
|
13
|
+
import type { NavSection, NavItem, User, QuickLink, BackLink } from '../../types/layout.js';
|
|
22
14
|
interface Props {
|
|
23
15
|
/** Navigation sections for sidebar */
|
|
24
16
|
navigation: NavSection[];
|
|
@@ -42,10 +34,12 @@ interface Props {
|
|
|
42
34
|
icon?: Snippet<[NavItem]>;
|
|
43
35
|
/** Quick links displayed at bottom of sidebar */
|
|
44
36
|
quickLinks?: QuickLink[];
|
|
45
|
-
/** Quick links display mode: 'list' for stacked with labels, 'icons' for
|
|
37
|
+
/** Quick links display mode: 'list' for stacked with labels, 'icons' for stacked centered icons only */
|
|
46
38
|
quickLinksDisplay?: 'list' | 'icons';
|
|
47
39
|
/** Custom icon renderer for quick links */
|
|
48
40
|
quickLinkIcon?: Snippet<[QuickLink]>;
|
|
41
|
+
/** Callback when a navigation item is clicked */
|
|
42
|
+
onNavigate?: (item: NavItem) => void;
|
|
49
43
|
/** Custom content at start of header (after toggle button) */
|
|
50
44
|
headerStart?: Snippet;
|
|
51
45
|
/** Custom content at end of header */
|
|
@@ -54,6 +48,14 @@ interface Props {
|
|
|
54
48
|
userMenu?: Snippet<[User]>;
|
|
55
49
|
/** Sidebar footer content */
|
|
56
50
|
sidebarFooter?: Snippet;
|
|
51
|
+
/** Sidebar width when expanded in pixels (default: 256) */
|
|
52
|
+
expandedWidth?: number;
|
|
53
|
+
/** Sidebar width when collapsed in pixels (default: 64) */
|
|
54
|
+
collapsedWidth?: number;
|
|
55
|
+
/** Enable search/filter input for navigation */
|
|
56
|
+
searchable?: boolean;
|
|
57
|
+
/** Placeholder text for search input */
|
|
58
|
+
searchPlaceholder?: string;
|
|
57
59
|
/** Main content */
|
|
58
60
|
children: Snippet;
|
|
59
61
|
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* QuickLinks - Quick navigation links section for sidebar
|
|
4
4
|
*
|
|
5
5
|
* Features:
|
|
6
|
-
* - Two display modes: 'list' (stacked with labels) and 'icons' (
|
|
6
|
+
* - Two display modes: 'list' (stacked with labels) and 'icons' (stacked centered icons only)
|
|
7
7
|
* - Light/dark variant support
|
|
8
8
|
* - External link indicator
|
|
9
9
|
* - Custom icon renderer support
|
|
@@ -16,37 +16,41 @@
|
|
|
16
16
|
interface Props {
|
|
17
17
|
/** Array of quick link items */
|
|
18
18
|
links: QuickLink[];
|
|
19
|
-
/** Display mode: 'list' for stacked with labels, 'icons' for
|
|
19
|
+
/** Display mode: 'list' for stacked with labels, 'icons' for stacked centered icons only */
|
|
20
20
|
display?: 'list' | 'icons';
|
|
21
21
|
/** Visual variant - light (default) or dark */
|
|
22
22
|
variant?: 'light' | 'dark';
|
|
23
23
|
/** Custom icon renderer */
|
|
24
24
|
icon?: Snippet<[QuickLink]>;
|
|
25
|
+
/** Accessible label for the navigation region */
|
|
26
|
+
ariaLabel?: string;
|
|
25
27
|
/** Additional classes */
|
|
26
28
|
class?: string;
|
|
27
29
|
}
|
|
28
30
|
|
|
29
|
-
let {
|
|
31
|
+
let {
|
|
32
|
+
links,
|
|
33
|
+
display = 'list',
|
|
34
|
+
variant = 'light',
|
|
35
|
+
icon,
|
|
36
|
+
ariaLabel = 'Quick links',
|
|
37
|
+
class: className,
|
|
38
|
+
}: Props = $props();
|
|
30
39
|
|
|
31
40
|
const isLight = $derived(variant === 'light');
|
|
32
41
|
</script>
|
|
33
42
|
|
|
34
43
|
{#if links.length > 0}
|
|
35
44
|
<div
|
|
36
|
-
class={cn(
|
|
37
|
-
display === 'list'
|
|
38
|
-
? 'flex flex-col gap-1'
|
|
39
|
-
: 'flex flex-row items-center justify-center gap-2',
|
|
40
|
-
className
|
|
41
|
-
)}
|
|
45
|
+
class={cn('flex flex-col gap-1', display === 'icons' && 'items-center', className)}
|
|
42
46
|
role="navigation"
|
|
43
|
-
aria-label=
|
|
47
|
+
aria-label={ariaLabel}
|
|
44
48
|
>
|
|
45
49
|
{#each links as link}
|
|
46
50
|
<a
|
|
47
51
|
href={link.href}
|
|
48
52
|
class={cn(
|
|
49
|
-
'flex items-center transition-
|
|
53
|
+
'flex items-center transition-all duration-300 motion-reduce:transition-none overflow-hidden',
|
|
50
54
|
// List mode styles
|
|
51
55
|
display === 'list' && 'gap-3 rounded-md px-3 py-2 text-sm',
|
|
52
56
|
display === 'list' &&
|
|
@@ -56,7 +60,7 @@
|
|
|
56
60
|
!isLight &&
|
|
57
61
|
'text-sidebar-foreground/70 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground',
|
|
58
62
|
// Icon mode styles
|
|
59
|
-
display === 'icons' && 'rounded-md p-2',
|
|
63
|
+
display === 'icons' && 'rounded-md p-2 gap-0',
|
|
60
64
|
display === 'icons' &&
|
|
61
65
|
isLight &&
|
|
62
66
|
'text-muted-foreground hover:bg-accent hover:text-accent-foreground',
|
|
@@ -87,24 +91,40 @@
|
|
|
87
91
|
</span>
|
|
88
92
|
{/if}
|
|
89
93
|
|
|
90
|
-
|
|
91
|
-
|
|
94
|
+
<span
|
|
95
|
+
class={cn(
|
|
96
|
+
'truncate whitespace-nowrap transition-all duration-300 motion-reduce:transition-none',
|
|
97
|
+
display === 'icons' ? 'w-0 opacity-0 flex-none' : 'flex-1 opacity-100'
|
|
98
|
+
)}
|
|
99
|
+
>
|
|
100
|
+
{link.label}
|
|
101
|
+
</span>
|
|
92
102
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
103
|
+
{#if link.badge !== undefined && display === 'list'}
|
|
104
|
+
<span
|
|
105
|
+
class="ml-auto rounded-full bg-primary px-2 py-0.5 text-xs font-medium text-primary-foreground whitespace-nowrap"
|
|
106
|
+
>
|
|
107
|
+
{link.badge}
|
|
108
|
+
</span>
|
|
109
|
+
{/if}
|
|
110
|
+
|
|
111
|
+
{#if link.external}
|
|
112
|
+
<svg
|
|
113
|
+
class={cn(
|
|
114
|
+
'h-4 w-4 shrink-0 transition-all duration-300',
|
|
115
|
+
display === 'icons' ? 'hidden' : 'opacity-50'
|
|
116
|
+
)}
|
|
117
|
+
fill="none"
|
|
118
|
+
viewBox="0 0 24 24"
|
|
119
|
+
stroke="currentColor"
|
|
120
|
+
>
|
|
121
|
+
<path
|
|
122
|
+
stroke-linecap="round"
|
|
123
|
+
stroke-linejoin="round"
|
|
124
|
+
stroke-width="2"
|
|
125
|
+
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
|
|
126
|
+
/>
|
|
127
|
+
</svg>
|
|
108
128
|
{/if}
|
|
109
129
|
</a>
|
|
110
130
|
{/each}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* QuickLinks - Quick navigation links section for sidebar
|
|
3
3
|
*
|
|
4
4
|
* Features:
|
|
5
|
-
* - Two display modes: 'list' (stacked with labels) and 'icons' (
|
|
5
|
+
* - Two display modes: 'list' (stacked with labels) and 'icons' (stacked centered icons only)
|
|
6
6
|
* - Light/dark variant support
|
|
7
7
|
* - External link indicator
|
|
8
8
|
* - Custom icon renderer support
|
|
@@ -13,12 +13,14 @@ import type { QuickLink } from '../../types/layout.js';
|
|
|
13
13
|
interface Props {
|
|
14
14
|
/** Array of quick link items */
|
|
15
15
|
links: QuickLink[];
|
|
16
|
-
/** Display mode: 'list' for stacked with labels, 'icons' for
|
|
16
|
+
/** Display mode: 'list' for stacked with labels, 'icons' for stacked centered icons only */
|
|
17
17
|
display?: 'list' | 'icons';
|
|
18
18
|
/** Visual variant - light (default) or dark */
|
|
19
19
|
variant?: 'light' | 'dark';
|
|
20
20
|
/** Custom icon renderer */
|
|
21
21
|
icon?: Snippet<[QuickLink]>;
|
|
22
|
+
/** Accessible label for the navigation region */
|
|
23
|
+
ariaLabel?: string;
|
|
22
24
|
/** Additional classes */
|
|
23
25
|
class?: string;
|
|
24
26
|
}
|