@classic-homes/theme-svelte 0.1.0
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/README.md +305 -0
- package/dist/lib/components/Alert.svelte +51 -0
- package/dist/lib/components/Alert.svelte.d.ts +9 -0
- package/dist/lib/components/AlertDescription.svelte +16 -0
- package/dist/lib/components/AlertDescription.svelte.d.ts +9 -0
- package/dist/lib/components/AlertDialog.svelte +136 -0
- package/dist/lib/components/AlertDialog.svelte.d.ts +79 -0
- package/dist/lib/components/AlertTitle.svelte +16 -0
- package/dist/lib/components/AlertTitle.svelte.d.ts +9 -0
- package/dist/lib/components/Avatar.svelte +56 -0
- package/dist/lib/components/Avatar.svelte.d.ts +26 -0
- package/dist/lib/components/AvatarFallback.svelte +31 -0
- package/dist/lib/components/AvatarFallback.svelte.d.ts +17 -0
- package/dist/lib/components/AvatarImage.svelte +29 -0
- package/dist/lib/components/AvatarImage.svelte.d.ts +12 -0
- package/dist/lib/components/Badge.svelte +73 -0
- package/dist/lib/components/Badge.svelte.d.ts +11 -0
- package/dist/lib/components/Button.svelte +130 -0
- package/dist/lib/components/Button.svelte.d.ts +17 -0
- package/dist/lib/components/Card.svelte +58 -0
- package/dist/lib/components/Card.svelte.d.ts +26 -0
- package/dist/lib/components/CardContent.svelte +16 -0
- package/dist/lib/components/CardContent.svelte.d.ts +9 -0
- package/dist/lib/components/CardDescription.svelte +16 -0
- package/dist/lib/components/CardDescription.svelte.d.ts +9 -0
- package/dist/lib/components/CardFooter.svelte +16 -0
- package/dist/lib/components/CardFooter.svelte.d.ts +9 -0
- package/dist/lib/components/CardHeader.svelte +16 -0
- package/dist/lib/components/CardHeader.svelte.d.ts +9 -0
- package/dist/lib/components/CardTitle.svelte +16 -0
- package/dist/lib/components/CardTitle.svelte.d.ts +9 -0
- package/dist/lib/components/Checkbox.svelte +65 -0
- package/dist/lib/components/Checkbox.svelte.d.ts +14 -0
- package/dist/lib/components/DataTable.svelte +334 -0
- package/dist/lib/components/DataTable.svelte.d.ts +103 -0
- package/dist/lib/components/Dialog.svelte +111 -0
- package/dist/lib/components/Dialog.svelte.d.ts +22 -0
- package/dist/lib/components/DropdownMenu.svelte +135 -0
- package/dist/lib/components/DropdownMenu.svelte.d.ts +33 -0
- package/dist/lib/components/FileUpload.svelte +448 -0
- package/dist/lib/components/FileUpload.svelte.d.ts +42 -0
- package/dist/lib/components/FormField.svelte +134 -0
- package/dist/lib/components/FormField.svelte.d.ts +37 -0
- package/dist/lib/components/Input.svelte +61 -0
- package/dist/lib/components/Input.svelte.d.ts +19 -0
- package/dist/lib/components/Label.svelte +33 -0
- package/dist/lib/components/Label.svelte.d.ts +11 -0
- package/dist/lib/components/LoadingLogo.svelte +124 -0
- package/dist/lib/components/LoadingLogo.svelte.d.ts +16 -0
- package/dist/lib/components/LogoMain.svelte +237 -0
- package/dist/lib/components/LogoMain.svelte.d.ts +20 -0
- package/dist/lib/components/PageHeader.svelte +90 -0
- package/dist/lib/components/PageHeader.svelte.d.ts +28 -0
- package/dist/lib/components/Section.svelte +44 -0
- package/dist/lib/components/Section.svelte.d.ts +28 -0
- package/dist/lib/components/Select.svelte +174 -0
- package/dist/lib/components/Select.svelte.d.ts +32 -0
- package/dist/lib/components/Separator.svelte +29 -0
- package/dist/lib/components/Separator.svelte.d.ts +9 -0
- package/dist/lib/components/Skeleton.svelte +35 -0
- package/dist/lib/components/Skeleton.svelte.d.ts +7 -0
- package/dist/lib/components/Spinner.svelte +50 -0
- package/dist/lib/components/Spinner.svelte.d.ts +8 -0
- package/dist/lib/components/Switch.svelte +56 -0
- package/dist/lib/components/Switch.svelte.d.ts +14 -0
- package/dist/lib/components/TabPanel.svelte +44 -0
- package/dist/lib/components/TabPanel.svelte.d.ts +12 -0
- package/dist/lib/components/Tabs.svelte +125 -0
- package/dist/lib/components/Tabs.svelte.d.ts +19 -0
- package/dist/lib/components/Textarea.svelte +54 -0
- package/dist/lib/components/Textarea.svelte.d.ts +16 -0
- package/dist/lib/components/Toast.svelte +116 -0
- package/dist/lib/components/Toast.svelte.d.ts +12 -0
- package/dist/lib/components/ToastContainer.svelte +56 -0
- package/dist/lib/components/ToastContainer.svelte.d.ts +8 -0
- package/dist/lib/components/Tooltip.svelte +55 -0
- package/dist/lib/components/Tooltip.svelte.d.ts +18 -0
- package/dist/lib/components/layout/AppShell.svelte +82 -0
- package/dist/lib/components/layout/AppShell.svelte.d.ts +44 -0
- package/dist/lib/components/layout/DashboardLayout.svelte +248 -0
- package/dist/lib/components/layout/DashboardLayout.svelte.d.ts +62 -0
- package/dist/lib/components/layout/Footer.svelte +130 -0
- package/dist/lib/components/layout/Footer.svelte.d.ts +32 -0
- package/dist/lib/components/layout/FormPageLayout.svelte +92 -0
- package/dist/lib/components/layout/FormPageLayout.svelte.d.ts +33 -0
- package/dist/lib/components/layout/Header.svelte +94 -0
- package/dist/lib/components/layout/Header.svelte.d.ts +30 -0
- package/dist/lib/components/layout/PublicLayout.svelte +180 -0
- package/dist/lib/components/layout/PublicLayout.svelte.d.ts +39 -0
- package/dist/lib/components/layout/QuickLinks.svelte +112 -0
- package/dist/lib/components/layout/QuickLinks.svelte.d.ts +27 -0
- package/dist/lib/components/layout/Sidebar.svelte +243 -0
- package/dist/lib/components/layout/Sidebar.svelte.d.ts +48 -0
- package/dist/lib/composables/index.d.ts +8 -0
- package/dist/lib/composables/index.js +10 -0
- package/dist/lib/composables/useAsync.svelte.d.ts +102 -0
- package/dist/lib/composables/useAsync.svelte.js +210 -0
- package/dist/lib/composables/useForm.svelte.d.ts +123 -0
- package/dist/lib/composables/useForm.svelte.js +245 -0
- package/dist/lib/index.d.ts +65 -0
- package/dist/lib/index.js +83 -0
- package/dist/lib/performance.d.ts +79 -0
- package/dist/lib/performance.js +170 -0
- package/dist/lib/schemas/auth.d.ts +410 -0
- package/dist/lib/schemas/auth.js +216 -0
- package/dist/lib/schemas/common.d.ts +267 -0
- package/dist/lib/schemas/common.js +268 -0
- package/dist/lib/schemas/index.d.ts +24 -0
- package/dist/lib/schemas/index.js +32 -0
- package/dist/lib/stores/sidebar.svelte.d.ts +25 -0
- package/dist/lib/stores/sidebar.svelte.js +38 -0
- package/dist/lib/stores/theme.svelte.d.ts +72 -0
- package/dist/lib/stores/theme.svelte.js +150 -0
- package/dist/lib/stores/toast.svelte.d.ts +62 -0
- package/dist/lib/stores/toast.svelte.js +93 -0
- package/dist/lib/types/components.d.ts +85 -0
- package/dist/lib/types/components.js +7 -0
- package/dist/lib/types/layout.d.ts +258 -0
- package/dist/lib/types/layout.js +7 -0
- package/dist/lib/utils.d.ts +6 -0
- package/dist/lib/utils.js +9 -0
- package/dist/lib/validation.d.ts +101 -0
- package/dist/lib/validation.js +170 -0
- package/package.json +56 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Composables - Reusable Svelte 5 runes-based state management utilities
|
|
3
|
+
*
|
|
4
|
+
* These composables provide common patterns for form handling and async operations,
|
|
5
|
+
* built with Svelte 5 runes for optimal reactivity.
|
|
6
|
+
*/
|
|
7
|
+
// Form composable with Zod validation
|
|
8
|
+
export { useForm, } from './useForm.svelte.js';
|
|
9
|
+
// Async operation composable
|
|
10
|
+
export { useAsync, runAsync, } from './useAsync.svelte.js';
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useAsync - Svelte 5 runes-based async operation state management
|
|
3
|
+
*
|
|
4
|
+
* Features:
|
|
5
|
+
* - Loading state management
|
|
6
|
+
* - Error handling with toast integration
|
|
7
|
+
* - Success/error callbacks
|
|
8
|
+
* - Retry functionality
|
|
9
|
+
* - Abort/cancel support
|
|
10
|
+
* - Debounce support
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```svelte
|
|
14
|
+
* <script lang="ts">
|
|
15
|
+
* import { useAsync } from '@classic-homes/theme-svelte';
|
|
16
|
+
*
|
|
17
|
+
* const fetchUser = useAsync({
|
|
18
|
+
* fn: async (userId: string) => {
|
|
19
|
+
* const response = await fetch(`/api/users/${userId}`);
|
|
20
|
+
* return response.json();
|
|
21
|
+
* },
|
|
22
|
+
* onSuccess: (user) => console.log('User loaded:', user),
|
|
23
|
+
* onError: (error) => console.error('Failed to load user:', error),
|
|
24
|
+
* });
|
|
25
|
+
*
|
|
26
|
+
* // Execute the async function
|
|
27
|
+
* fetchUser.execute('user-123');
|
|
28
|
+
* </script>
|
|
29
|
+
*
|
|
30
|
+
* {#if fetchUser.isLoading}
|
|
31
|
+
* <Spinner />
|
|
32
|
+
* {:else if fetchUser.error}
|
|
33
|
+
* <Alert variant="error">{fetchUser.error.message}</Alert>
|
|
34
|
+
* {:else if fetchUser.data}
|
|
35
|
+
* <UserCard user={fetchUser.data} />
|
|
36
|
+
* {/if}
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export interface UseAsyncOptions<TData, TArgs extends unknown[]> {
|
|
40
|
+
/** The async function to execute */
|
|
41
|
+
fn: (...args: TArgs) => Promise<TData>;
|
|
42
|
+
/** Initial data value */
|
|
43
|
+
initialData?: TData;
|
|
44
|
+
/** Called when the operation succeeds */
|
|
45
|
+
onSuccess?: (data: TData) => void;
|
|
46
|
+
/** Called when the operation fails */
|
|
47
|
+
onError?: (error: Error) => void;
|
|
48
|
+
/** Whether to show toast on error (default: true) */
|
|
49
|
+
showToastOnError?: boolean;
|
|
50
|
+
/** Custom error message for toast */
|
|
51
|
+
errorMessage?: string;
|
|
52
|
+
/** Whether to show toast on success (default: false) */
|
|
53
|
+
showToastOnSuccess?: boolean;
|
|
54
|
+
/** Custom success message for toast */
|
|
55
|
+
successMessage?: string;
|
|
56
|
+
/** Execute immediately on creation */
|
|
57
|
+
immediate?: boolean;
|
|
58
|
+
/** Arguments for immediate execution */
|
|
59
|
+
immediateArgs?: TArgs;
|
|
60
|
+
/** Debounce delay in milliseconds */
|
|
61
|
+
debounceMs?: number;
|
|
62
|
+
}
|
|
63
|
+
export interface UseAsyncReturn<TData, TArgs extends unknown[]> {
|
|
64
|
+
/** Current data (reactive) */
|
|
65
|
+
readonly data: TData | undefined;
|
|
66
|
+
/** Current error (reactive) */
|
|
67
|
+
readonly error: Error | undefined;
|
|
68
|
+
/** Whether the operation is in progress (reactive) */
|
|
69
|
+
readonly isLoading: boolean;
|
|
70
|
+
/** Whether the operation has been executed at least once (reactive) */
|
|
71
|
+
readonly isExecuted: boolean;
|
|
72
|
+
/** Whether the last execution was successful (reactive) */
|
|
73
|
+
readonly isSuccess: boolean;
|
|
74
|
+
/** Whether the last execution failed (reactive) */
|
|
75
|
+
readonly isError: boolean;
|
|
76
|
+
/** Execute the async function */
|
|
77
|
+
execute: (...args: TArgs) => Promise<TData | undefined>;
|
|
78
|
+
/** Reset state to initial values */
|
|
79
|
+
reset: () => void;
|
|
80
|
+
/** Retry the last execution with the same arguments */
|
|
81
|
+
retry: () => Promise<TData | undefined>;
|
|
82
|
+
/** Set data manually */
|
|
83
|
+
setData: (data: TData) => void;
|
|
84
|
+
/** Set error manually */
|
|
85
|
+
setError: (error: Error) => void;
|
|
86
|
+
/** Clear error */
|
|
87
|
+
clearError: () => void;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Create an async operation handler with Svelte 5 runes
|
|
91
|
+
*/
|
|
92
|
+
export declare function useAsync<TData, TArgs extends unknown[] = []>(options: UseAsyncOptions<TData, TArgs>): UseAsyncReturn<TData, TArgs>;
|
|
93
|
+
/**
|
|
94
|
+
* Simple wrapper for one-off async operations
|
|
95
|
+
* Returns a promise that resolves with the result and handles errors via toast
|
|
96
|
+
*/
|
|
97
|
+
export declare function runAsync<T>(fn: () => Promise<T>, options?: {
|
|
98
|
+
errorMessage?: string;
|
|
99
|
+
successMessage?: string;
|
|
100
|
+
showToastOnError?: boolean;
|
|
101
|
+
showToastOnSuccess?: boolean;
|
|
102
|
+
}): Promise<T | undefined>;
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useAsync - Svelte 5 runes-based async operation state management
|
|
3
|
+
*
|
|
4
|
+
* Features:
|
|
5
|
+
* - Loading state management
|
|
6
|
+
* - Error handling with toast integration
|
|
7
|
+
* - Success/error callbacks
|
|
8
|
+
* - Retry functionality
|
|
9
|
+
* - Abort/cancel support
|
|
10
|
+
* - Debounce support
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```svelte
|
|
14
|
+
* <script lang="ts">
|
|
15
|
+
* import { useAsync } from '@classic-homes/theme-svelte';
|
|
16
|
+
*
|
|
17
|
+
* const fetchUser = useAsync({
|
|
18
|
+
* fn: async (userId: string) => {
|
|
19
|
+
* const response = await fetch(`/api/users/${userId}`);
|
|
20
|
+
* return response.json();
|
|
21
|
+
* },
|
|
22
|
+
* onSuccess: (user) => console.log('User loaded:', user),
|
|
23
|
+
* onError: (error) => console.error('Failed to load user:', error),
|
|
24
|
+
* });
|
|
25
|
+
*
|
|
26
|
+
* // Execute the async function
|
|
27
|
+
* fetchUser.execute('user-123');
|
|
28
|
+
* </script>
|
|
29
|
+
*
|
|
30
|
+
* {#if fetchUser.isLoading}
|
|
31
|
+
* <Spinner />
|
|
32
|
+
* {:else if fetchUser.error}
|
|
33
|
+
* <Alert variant="error">{fetchUser.error.message}</Alert>
|
|
34
|
+
* {:else if fetchUser.data}
|
|
35
|
+
* <UserCard user={fetchUser.data} />
|
|
36
|
+
* {/if}
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
import { toastStore } from '../stores/toast.svelte.js';
|
|
40
|
+
/**
|
|
41
|
+
* Create an async operation handler with Svelte 5 runes
|
|
42
|
+
*/
|
|
43
|
+
export function useAsync(options) {
|
|
44
|
+
const { fn, initialData, onSuccess, onError, showToastOnError = true, errorMessage, showToastOnSuccess = false, successMessage, immediate = false, immediateArgs, debounceMs, } = options;
|
|
45
|
+
// Reactive state using Svelte 5 runes
|
|
46
|
+
let data = $state(initialData);
|
|
47
|
+
let error = $state(undefined);
|
|
48
|
+
let isLoading = $state(false);
|
|
49
|
+
let isExecuted = $state(false);
|
|
50
|
+
let lastArgs = $state(undefined);
|
|
51
|
+
// Derived state
|
|
52
|
+
const isSuccess = $derived(isExecuted && !error && !isLoading);
|
|
53
|
+
const isError = $derived(isExecuted && !!error);
|
|
54
|
+
// Debounce timer
|
|
55
|
+
let debounceTimer;
|
|
56
|
+
/**
|
|
57
|
+
* Execute the async function
|
|
58
|
+
*/
|
|
59
|
+
async function execute(...args) {
|
|
60
|
+
// Clear any pending debounce
|
|
61
|
+
if (debounceTimer) {
|
|
62
|
+
clearTimeout(debounceTimer);
|
|
63
|
+
debounceTimer = undefined;
|
|
64
|
+
}
|
|
65
|
+
// If debounce is set, delay execution
|
|
66
|
+
if (debounceMs && debounceMs > 0) {
|
|
67
|
+
return new Promise((resolve) => {
|
|
68
|
+
debounceTimer = setTimeout(async () => {
|
|
69
|
+
const result = await executeImmediate(...args);
|
|
70
|
+
resolve(result);
|
|
71
|
+
}, debounceMs);
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
return executeImmediate(...args);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Execute immediately without debounce
|
|
78
|
+
*/
|
|
79
|
+
async function executeImmediate(...args) {
|
|
80
|
+
lastArgs = args;
|
|
81
|
+
isLoading = true;
|
|
82
|
+
error = undefined;
|
|
83
|
+
isExecuted = true;
|
|
84
|
+
try {
|
|
85
|
+
const result = await fn(...args);
|
|
86
|
+
data = result;
|
|
87
|
+
if (onSuccess) {
|
|
88
|
+
onSuccess(result);
|
|
89
|
+
}
|
|
90
|
+
if (showToastOnSuccess && successMessage) {
|
|
91
|
+
toastStore.success(successMessage);
|
|
92
|
+
}
|
|
93
|
+
return result;
|
|
94
|
+
}
|
|
95
|
+
catch (err) {
|
|
96
|
+
const normalizedError = err instanceof Error ? err : new Error(String(err));
|
|
97
|
+
error = normalizedError;
|
|
98
|
+
if (onError) {
|
|
99
|
+
onError(normalizedError);
|
|
100
|
+
}
|
|
101
|
+
if (showToastOnError) {
|
|
102
|
+
toastStore.error(errorMessage || normalizedError.message || 'An error occurred');
|
|
103
|
+
}
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
finally {
|
|
107
|
+
isLoading = false;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Retry the last execution with the same arguments
|
|
112
|
+
*/
|
|
113
|
+
async function retry() {
|
|
114
|
+
if (!lastArgs) {
|
|
115
|
+
console.warn('useAsync.retry: No previous execution to retry');
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
return execute(...lastArgs);
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Reset state to initial values
|
|
122
|
+
*/
|
|
123
|
+
function reset() {
|
|
124
|
+
// Clear any pending debounce
|
|
125
|
+
if (debounceTimer) {
|
|
126
|
+
clearTimeout(debounceTimer);
|
|
127
|
+
debounceTimer = undefined;
|
|
128
|
+
}
|
|
129
|
+
data = initialData;
|
|
130
|
+
error = undefined;
|
|
131
|
+
isLoading = false;
|
|
132
|
+
isExecuted = false;
|
|
133
|
+
lastArgs = undefined;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Set data manually
|
|
137
|
+
*/
|
|
138
|
+
function setData(newData) {
|
|
139
|
+
data = newData;
|
|
140
|
+
error = undefined;
|
|
141
|
+
isExecuted = true;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Set error manually
|
|
145
|
+
*/
|
|
146
|
+
function setError(newError) {
|
|
147
|
+
error = newError;
|
|
148
|
+
isExecuted = true;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Clear error
|
|
152
|
+
*/
|
|
153
|
+
function clearError() {
|
|
154
|
+
error = undefined;
|
|
155
|
+
}
|
|
156
|
+
// Execute immediately if requested
|
|
157
|
+
if (immediate && immediateArgs) {
|
|
158
|
+
// Use $effect-like behavior by queueing the execution
|
|
159
|
+
queueMicrotask(() => {
|
|
160
|
+
execute(...immediateArgs);
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
return {
|
|
164
|
+
get data() {
|
|
165
|
+
return data;
|
|
166
|
+
},
|
|
167
|
+
get error() {
|
|
168
|
+
return error;
|
|
169
|
+
},
|
|
170
|
+
get isLoading() {
|
|
171
|
+
return isLoading;
|
|
172
|
+
},
|
|
173
|
+
get isExecuted() {
|
|
174
|
+
return isExecuted;
|
|
175
|
+
},
|
|
176
|
+
get isSuccess() {
|
|
177
|
+
return isSuccess;
|
|
178
|
+
},
|
|
179
|
+
get isError() {
|
|
180
|
+
return isError;
|
|
181
|
+
},
|
|
182
|
+
execute,
|
|
183
|
+
reset,
|
|
184
|
+
retry,
|
|
185
|
+
setData,
|
|
186
|
+
setError,
|
|
187
|
+
clearError,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Simple wrapper for one-off async operations
|
|
192
|
+
* Returns a promise that resolves with the result and handles errors via toast
|
|
193
|
+
*/
|
|
194
|
+
export async function runAsync(fn, options) {
|
|
195
|
+
const { errorMessage, successMessage, showToastOnError = true, showToastOnSuccess = false, } = options || {};
|
|
196
|
+
try {
|
|
197
|
+
const result = await fn();
|
|
198
|
+
if (showToastOnSuccess && successMessage) {
|
|
199
|
+
toastStore.success(successMessage);
|
|
200
|
+
}
|
|
201
|
+
return result;
|
|
202
|
+
}
|
|
203
|
+
catch (err) {
|
|
204
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
205
|
+
if (showToastOnError) {
|
|
206
|
+
toastStore.error(errorMessage || error.message || 'An error occurred');
|
|
207
|
+
}
|
|
208
|
+
return undefined;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useForm - Svelte 5 runes-based form state management with Zod validation
|
|
3
|
+
*
|
|
4
|
+
* Features:
|
|
5
|
+
* - Type-safe form data with Zod schema inference
|
|
6
|
+
* - Field-level and form-level validation
|
|
7
|
+
* - Automatic error handling with toast notifications
|
|
8
|
+
* - Loading state management
|
|
9
|
+
* - Support for nested objects and arrays
|
|
10
|
+
* - Dirty tracking per field
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```svelte
|
|
14
|
+
* <script lang="ts">
|
|
15
|
+
* import { useForm } from '@classic-homes/theme-svelte';
|
|
16
|
+
* import { z } from 'zod';
|
|
17
|
+
*
|
|
18
|
+
* const schema = z.object({
|
|
19
|
+
* email: z.string().email(),
|
|
20
|
+
* password: z.string().min(8),
|
|
21
|
+
* });
|
|
22
|
+
*
|
|
23
|
+
* const form = useForm({
|
|
24
|
+
* schema,
|
|
25
|
+
* initialValues: { email: '', password: '' },
|
|
26
|
+
* onSubmit: async (data) => {
|
|
27
|
+
* await loginUser(data);
|
|
28
|
+
* },
|
|
29
|
+
* });
|
|
30
|
+
* </script>
|
|
31
|
+
*
|
|
32
|
+
* <form onsubmit={form.handleSubmit}>
|
|
33
|
+
* <input
|
|
34
|
+
* bind:value={form.data.email}
|
|
35
|
+
* onblur={() => form.validateField('email')}
|
|
36
|
+
* />
|
|
37
|
+
* {#if form.errors.email}
|
|
38
|
+
* <span class="error">{form.errors.email}</span>
|
|
39
|
+
* {/if}
|
|
40
|
+
* </form>
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
import { z } from 'zod';
|
|
44
|
+
export interface UseFormOptions<T extends z.ZodObject<z.ZodRawShape>> {
|
|
45
|
+
/** Zod schema for validation */
|
|
46
|
+
schema: T;
|
|
47
|
+
/** Initial form values */
|
|
48
|
+
initialValues: z.infer<T>;
|
|
49
|
+
/** Called on successful validation and submission */
|
|
50
|
+
onSubmit: (data: z.infer<T>) => Promise<void> | void;
|
|
51
|
+
/** Called when validation or submission fails */
|
|
52
|
+
onError?: (error: Error | z.ZodError) => void;
|
|
53
|
+
/** Whether to show toast on error (default: true) */
|
|
54
|
+
showToastOnError?: boolean;
|
|
55
|
+
/** Whether to reset form after successful submission (default: false) */
|
|
56
|
+
resetOnSuccess?: boolean;
|
|
57
|
+
/** Custom error message for toast */
|
|
58
|
+
errorMessage?: string;
|
|
59
|
+
/** Custom success message for toast (if set, shows toast on success) */
|
|
60
|
+
successMessage?: string;
|
|
61
|
+
}
|
|
62
|
+
export interface FieldError {
|
|
63
|
+
message: string;
|
|
64
|
+
path: string[];
|
|
65
|
+
}
|
|
66
|
+
export interface FormState<T> {
|
|
67
|
+
/** Current form data */
|
|
68
|
+
data: T;
|
|
69
|
+
/** Field errors keyed by field name (supports nested paths like "address.city") */
|
|
70
|
+
errors: Record<string, string>;
|
|
71
|
+
/** Whether the form is currently submitting */
|
|
72
|
+
isSubmitting: boolean;
|
|
73
|
+
/** Whether the form has been submitted at least once */
|
|
74
|
+
isSubmitted: boolean;
|
|
75
|
+
/** Whether any field has been modified */
|
|
76
|
+
isDirty: boolean;
|
|
77
|
+
/** Set of field names that have been modified */
|
|
78
|
+
dirtyFields: Set<string>;
|
|
79
|
+
/** Whether the form is currently valid */
|
|
80
|
+
isValid: boolean;
|
|
81
|
+
}
|
|
82
|
+
export interface UseFormReturn<T extends z.ZodObject<z.ZodRawShape>> {
|
|
83
|
+
/** Current form data (reactive) */
|
|
84
|
+
readonly data: z.infer<T>;
|
|
85
|
+
/** Field errors (reactive) */
|
|
86
|
+
readonly errors: Record<string, string>;
|
|
87
|
+
/** Whether form is submitting (reactive) */
|
|
88
|
+
readonly isSubmitting: boolean;
|
|
89
|
+
/** Whether form has been submitted (reactive) */
|
|
90
|
+
readonly isSubmitted: boolean;
|
|
91
|
+
/** Whether any field is dirty (reactive) */
|
|
92
|
+
readonly isDirty: boolean;
|
|
93
|
+
/** Whether form is currently valid (reactive) */
|
|
94
|
+
readonly isValid: boolean;
|
|
95
|
+
/** Set a field value */
|
|
96
|
+
setField: <K extends keyof z.infer<T>>(field: K, value: z.infer<T>[K]) => void;
|
|
97
|
+
/** Set multiple field values */
|
|
98
|
+
setFields: (values: Partial<z.infer<T>>) => void;
|
|
99
|
+
/** Set a nested field value using dot notation */
|
|
100
|
+
setNestedField: (path: string, value: unknown) => void;
|
|
101
|
+
/** Validate a single field */
|
|
102
|
+
validateField: (field: keyof z.infer<T>) => boolean;
|
|
103
|
+
/** Validate the entire form */
|
|
104
|
+
validate: () => boolean;
|
|
105
|
+
/** Handle form submission */
|
|
106
|
+
handleSubmit: (event?: Event) => Promise<void>;
|
|
107
|
+
/** Reset form to initial values */
|
|
108
|
+
reset: () => void;
|
|
109
|
+
/** Clear all errors */
|
|
110
|
+
clearErrors: () => void;
|
|
111
|
+
/** Set a specific error */
|
|
112
|
+
setError: (field: string, message: string) => void;
|
|
113
|
+
/** Mark a field as dirty */
|
|
114
|
+
markDirty: (field: string) => void;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Create a form handler with Zod validation and Svelte 5 runes
|
|
118
|
+
*/
|
|
119
|
+
export declare function useForm<T extends z.ZodObject<z.ZodRawShape>>(options: UseFormOptions<T>): UseFormReturn<T>;
|
|
120
|
+
/**
|
|
121
|
+
* Type helper to extract form data type from schema
|
|
122
|
+
*/
|
|
123
|
+
export type InferFormData<T extends z.ZodObject<z.ZodRawShape>> = z.infer<T>;
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useForm - Svelte 5 runes-based form state management with Zod validation
|
|
3
|
+
*
|
|
4
|
+
* Features:
|
|
5
|
+
* - Type-safe form data with Zod schema inference
|
|
6
|
+
* - Field-level and form-level validation
|
|
7
|
+
* - Automatic error handling with toast notifications
|
|
8
|
+
* - Loading state management
|
|
9
|
+
* - Support for nested objects and arrays
|
|
10
|
+
* - Dirty tracking per field
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```svelte
|
|
14
|
+
* <script lang="ts">
|
|
15
|
+
* import { useForm } from '@classic-homes/theme-svelte';
|
|
16
|
+
* import { z } from 'zod';
|
|
17
|
+
*
|
|
18
|
+
* const schema = z.object({
|
|
19
|
+
* email: z.string().email(),
|
|
20
|
+
* password: z.string().min(8),
|
|
21
|
+
* });
|
|
22
|
+
*
|
|
23
|
+
* const form = useForm({
|
|
24
|
+
* schema,
|
|
25
|
+
* initialValues: { email: '', password: '' },
|
|
26
|
+
* onSubmit: async (data) => {
|
|
27
|
+
* await loginUser(data);
|
|
28
|
+
* },
|
|
29
|
+
* });
|
|
30
|
+
* </script>
|
|
31
|
+
*
|
|
32
|
+
* <form onsubmit={form.handleSubmit}>
|
|
33
|
+
* <input
|
|
34
|
+
* bind:value={form.data.email}
|
|
35
|
+
* onblur={() => form.validateField('email')}
|
|
36
|
+
* />
|
|
37
|
+
* {#if form.errors.email}
|
|
38
|
+
* <span class="error">{form.errors.email}</span>
|
|
39
|
+
* {/if}
|
|
40
|
+
* </form>
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
import { toastStore } from '../stores/toast.svelte.js';
|
|
44
|
+
/**
|
|
45
|
+
* Create a form handler with Zod validation and Svelte 5 runes
|
|
46
|
+
*/
|
|
47
|
+
export function useForm(options) {
|
|
48
|
+
const { schema, initialValues, onSubmit, onError, showToastOnError = true, resetOnSuccess = false, errorMessage, successMessage, } = options;
|
|
49
|
+
// Deep clone to avoid reference issues
|
|
50
|
+
const cloneInitialValues = () => JSON.parse(JSON.stringify(initialValues));
|
|
51
|
+
// Reactive state using Svelte 5 runes
|
|
52
|
+
let data = $state(cloneInitialValues());
|
|
53
|
+
let errors = $state({});
|
|
54
|
+
let isSubmitting = $state(false);
|
|
55
|
+
let isSubmitted = $state(false);
|
|
56
|
+
let dirtyFields = $state(new Set());
|
|
57
|
+
// Derived state
|
|
58
|
+
const isDirty = $derived(dirtyFields.size > 0);
|
|
59
|
+
const isValid = $derived(Object.keys(errors).length === 0);
|
|
60
|
+
/**
|
|
61
|
+
* Set nested value in object using dot notation path
|
|
62
|
+
*/
|
|
63
|
+
function setNestedValue(obj, path, value) {
|
|
64
|
+
const keys = path.split('.');
|
|
65
|
+
let current = obj;
|
|
66
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
67
|
+
const key = keys[i];
|
|
68
|
+
if (current[key] === undefined || current[key] === null) {
|
|
69
|
+
current[key] = {};
|
|
70
|
+
}
|
|
71
|
+
current = current[key];
|
|
72
|
+
}
|
|
73
|
+
current[keys[keys.length - 1]] = value;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Parse Zod errors into field-keyed error map
|
|
77
|
+
*/
|
|
78
|
+
function parseZodErrors(zodError) {
|
|
79
|
+
const fieldErrors = {};
|
|
80
|
+
for (const issue of zodError.issues) {
|
|
81
|
+
const path = issue.path.join('.');
|
|
82
|
+
// Only keep the first error for each field
|
|
83
|
+
if (!fieldErrors[path]) {
|
|
84
|
+
fieldErrors[path] = issue.message;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return fieldErrors;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Set a single field value
|
|
91
|
+
*/
|
|
92
|
+
function setField(field, value) {
|
|
93
|
+
data[field] = value;
|
|
94
|
+
markDirty(field);
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Set multiple field values
|
|
98
|
+
*/
|
|
99
|
+
function setFields(values) {
|
|
100
|
+
for (const [key, value] of Object.entries(values)) {
|
|
101
|
+
data[key] = value;
|
|
102
|
+
markDirty(key);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Set a nested field value using dot notation
|
|
107
|
+
*/
|
|
108
|
+
function setNestedField(path, value) {
|
|
109
|
+
setNestedValue(data, path, value);
|
|
110
|
+
markDirty(path);
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Mark a field as dirty
|
|
114
|
+
*/
|
|
115
|
+
function markDirty(field) {
|
|
116
|
+
dirtyFields = new Set([...dirtyFields, field]);
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Validate a single field against the schema
|
|
120
|
+
*/
|
|
121
|
+
function validateField(field) {
|
|
122
|
+
const fieldSchema = schema.shape[field];
|
|
123
|
+
if (!fieldSchema)
|
|
124
|
+
return true;
|
|
125
|
+
const fieldValue = data[field];
|
|
126
|
+
const result = fieldSchema.safeParse(fieldValue);
|
|
127
|
+
if (result.success) {
|
|
128
|
+
// Clear error for this field
|
|
129
|
+
const newErrors = { ...errors };
|
|
130
|
+
delete newErrors[field];
|
|
131
|
+
errors = newErrors;
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
// Set error for this field
|
|
136
|
+
errors = {
|
|
137
|
+
...errors,
|
|
138
|
+
[field]: result.error.issues[0]?.message || 'Invalid value',
|
|
139
|
+
};
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Validate the entire form
|
|
145
|
+
*/
|
|
146
|
+
function validate() {
|
|
147
|
+
const result = schema.safeParse(data);
|
|
148
|
+
if (result.success) {
|
|
149
|
+
errors = {};
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
errors = parseZodErrors(result.error);
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Handle form submission
|
|
159
|
+
*/
|
|
160
|
+
async function handleSubmit(event) {
|
|
161
|
+
event?.preventDefault();
|
|
162
|
+
isSubmitted = true;
|
|
163
|
+
// Validate all fields
|
|
164
|
+
if (!validate()) {
|
|
165
|
+
const errorCount = Object.keys(errors).length;
|
|
166
|
+
if (showToastOnError) {
|
|
167
|
+
toastStore.error(errorMessage || `Please fix ${errorCount} validation error${errorCount > 1 ? 's' : ''}`);
|
|
168
|
+
}
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
isSubmitting = true;
|
|
172
|
+
try {
|
|
173
|
+
await onSubmit(data);
|
|
174
|
+
if (successMessage) {
|
|
175
|
+
toastStore.success(successMessage);
|
|
176
|
+
}
|
|
177
|
+
if (resetOnSuccess) {
|
|
178
|
+
reset();
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
183
|
+
if (onError) {
|
|
184
|
+
onError(err);
|
|
185
|
+
}
|
|
186
|
+
if (showToastOnError) {
|
|
187
|
+
toastStore.error(errorMessage || err.message || 'An error occurred');
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
finally {
|
|
191
|
+
isSubmitting = false;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Reset form to initial values
|
|
196
|
+
*/
|
|
197
|
+
function reset() {
|
|
198
|
+
data = cloneInitialValues();
|
|
199
|
+
errors = {};
|
|
200
|
+
isSubmitted = false;
|
|
201
|
+
dirtyFields = new Set();
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Clear all errors
|
|
205
|
+
*/
|
|
206
|
+
function clearErrors() {
|
|
207
|
+
errors = {};
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Set a specific error manually
|
|
211
|
+
*/
|
|
212
|
+
function setError(field, message) {
|
|
213
|
+
errors = { ...errors, [field]: message };
|
|
214
|
+
}
|
|
215
|
+
return {
|
|
216
|
+
get data() {
|
|
217
|
+
return data;
|
|
218
|
+
},
|
|
219
|
+
get errors() {
|
|
220
|
+
return errors;
|
|
221
|
+
},
|
|
222
|
+
get isSubmitting() {
|
|
223
|
+
return isSubmitting;
|
|
224
|
+
},
|
|
225
|
+
get isSubmitted() {
|
|
226
|
+
return isSubmitted;
|
|
227
|
+
},
|
|
228
|
+
get isDirty() {
|
|
229
|
+
return isDirty;
|
|
230
|
+
},
|
|
231
|
+
get isValid() {
|
|
232
|
+
return isValid;
|
|
233
|
+
},
|
|
234
|
+
setField,
|
|
235
|
+
setFields,
|
|
236
|
+
setNestedField,
|
|
237
|
+
validateField,
|
|
238
|
+
validate,
|
|
239
|
+
handleSubmit,
|
|
240
|
+
reset,
|
|
241
|
+
clearErrors,
|
|
242
|
+
setError,
|
|
243
|
+
markDirty,
|
|
244
|
+
};
|
|
245
|
+
}
|