@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.
- package/dist/lib/components/CardHeader.svelte +22 -2
- package/dist/lib/components/CardHeader.svelte.d.ts +5 -4
- 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/HeaderSearch.svelte +340 -0
- package/dist/lib/components/HeaderSearch.svelte.d.ts +37 -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/PageHeader.svelte +6 -0
- package/dist/lib/components/PageHeader.svelte.d.ts +1 -1
- 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/AuthLayout.svelte +133 -0
- package/dist/lib/components/layout/AuthLayout.svelte.d.ts +48 -0
- package/dist/lib/components/layout/DashboardLayout.svelte +100 -74
- package/dist/lib/components/layout/DashboardLayout.svelte.d.ts +17 -10
- package/dist/lib/components/layout/ErrorLayout.svelte +206 -0
- package/dist/lib/components/layout/ErrorLayout.svelte.d.ts +52 -0
- package/dist/lib/components/layout/FormPageLayout.svelte +2 -8
- package/dist/lib/components/layout/Header.svelte +232 -41
- package/dist/lib/components/layout/Header.svelte.d.ts +71 -5
- package/dist/lib/components/layout/PublicLayout.svelte +54 -80
- package/dist/lib/components/layout/PublicLayout.svelte.d.ts +3 -1
- 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 +378 -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 +13 -2
- package/dist/lib/index.js +11 -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 +203 -3
- 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 {
|
|
15
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
|
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(
|
|
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
|
-
//
|
|
157
|
+
// Load persisted sidebar state and apply prop-based defaults
|
|
129
158
|
$effect(() => {
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
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
|
-
<
|
|
162
|
-
|
|
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
|
-
|
|
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
|
-
|
|
240
|
-
</
|
|
265
|
+
{/snippet}
|
|
266
|
+
</Header>
|
|
241
267
|
{/if}
|
|
242
268
|
|
|
243
269
|
<!-- Main Content -->
|