@classic-homes/theme-svelte 0.1.7 → 0.1.9
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/Alert.svelte.d.ts +1 -1
- package/dist/lib/components/Badge.svelte.d.ts +1 -1
- package/dist/lib/components/Card.svelte +31 -8
- package/dist/lib/components/Card.svelte.d.ts +16 -4
- package/dist/lib/components/Toast.svelte.d.ts +1 -1
- package/dist/lib/composables/index.d.ts +1 -0
- package/dist/lib/composables/index.js +2 -0
- package/dist/lib/composables/useForm.svelte.d.ts +6 -0
- package/dist/lib/composables/useForm.svelte.js +31 -4
- package/dist/lib/composables/usePersistedForm.svelte.d.ts +78 -0
- package/dist/lib/composables/usePersistedForm.svelte.js +260 -0
- package/dist/lib/index.d.ts +1 -1
- package/dist/lib/index.js +1 -1
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Snippet } from 'svelte';
|
|
2
2
|
declare const Alert: import("svelte").Component<{
|
|
3
3
|
[key: string]: unknown;
|
|
4
|
-
variant?: "default" | "destructive" | "
|
|
4
|
+
variant?: "default" | "destructive" | "warning" | "error" | "success" | "info" | undefined;
|
|
5
5
|
class?: string;
|
|
6
6
|
children: Snippet;
|
|
7
7
|
}, {}, "">;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Snippet } from 'svelte';
|
|
2
2
|
declare const Badge: import("svelte").Component<{
|
|
3
3
|
[key: string]: unknown;
|
|
4
|
-
variant?: "default" | "secondary" | "destructive" | "outline" | "
|
|
4
|
+
variant?: "default" | "secondary" | "destructive" | "outline" | "warning" | "success" | "info" | undefined;
|
|
5
5
|
size?: "default" | "sm" | "dot" | undefined;
|
|
6
6
|
clickable?: boolean;
|
|
7
7
|
class?: string;
|
|
@@ -5,8 +5,14 @@
|
|
|
5
5
|
* Features:
|
|
6
6
|
* - Clean border styling with sharp look
|
|
7
7
|
* - Optional hover shadow effect for interactive cards
|
|
8
|
-
* -
|
|
8
|
+
* - Multiple variants: default, featured, highlight, warning
|
|
9
9
|
* - Consistent with brand guidelines
|
|
10
|
+
*
|
|
11
|
+
* Variants:
|
|
12
|
+
* - default: Standard card with 1px border
|
|
13
|
+
* - featured: Thicker 2px border for emphasis
|
|
14
|
+
* - highlight: Teal border (brand-core-1) for positive/info callouts
|
|
15
|
+
* - warning: Red border (brand-red-primary) for warnings/alerts
|
|
10
16
|
*/
|
|
11
17
|
import type { Snippet } from 'svelte';
|
|
12
18
|
import { cn } from '../utils.js';
|
|
@@ -14,9 +20,15 @@
|
|
|
14
20
|
interface Props {
|
|
15
21
|
/** Enable hover shadow effect for interactive cards */
|
|
16
22
|
interactive?: boolean;
|
|
17
|
-
/**
|
|
18
|
-
|
|
19
|
-
|
|
23
|
+
/**
|
|
24
|
+
* Card variant
|
|
25
|
+
* - default: Standard 1px border
|
|
26
|
+
* - featured: Thicker 2px border
|
|
27
|
+
* - highlight: Teal border for positive/info callouts
|
|
28
|
+
* - warning: Red border for warnings/alerts
|
|
29
|
+
*/
|
|
30
|
+
variant?: 'default' | 'featured' | 'highlight' | 'warning';
|
|
31
|
+
/** Border color override (ignored for highlight/warning variants) */
|
|
20
32
|
borderColor?: 'default' | 'primary' | 'muted';
|
|
21
33
|
/** Additional classes */
|
|
22
34
|
class?: string;
|
|
@@ -39,15 +51,26 @@
|
|
|
39
51
|
primary: 'border-primary',
|
|
40
52
|
muted: 'border-gray-200',
|
|
41
53
|
};
|
|
54
|
+
|
|
55
|
+
// Variant-specific border colors (highlight and warning override borderColor)
|
|
56
|
+
const variantBorderClasses = $derived({
|
|
57
|
+
default: borderColorClasses[borderColor],
|
|
58
|
+
featured: borderColorClasses[borderColor],
|
|
59
|
+
highlight: 'border-brand-core-1',
|
|
60
|
+
warning: 'border-brand-red-primary',
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Variants with thick borders
|
|
64
|
+
const thickBorderVariants = ['featured', 'highlight', 'warning'];
|
|
42
65
|
</script>
|
|
43
66
|
|
|
44
67
|
<div
|
|
45
68
|
class={cn(
|
|
46
69
|
'rounded-md bg-card text-card-foreground',
|
|
47
|
-
// Border thickness
|
|
48
|
-
variant
|
|
49
|
-
// Border color
|
|
50
|
-
|
|
70
|
+
// Border thickness - thick for featured, highlight, warning
|
|
71
|
+
thickBorderVariants.includes(variant) ? 'border-2' : 'border',
|
|
72
|
+
// Border color based on variant
|
|
73
|
+
variantBorderClasses[variant],
|
|
51
74
|
// Interactive hover effect
|
|
52
75
|
interactive && 'hover:shadow-lg transition-shadow duration-200',
|
|
53
76
|
className
|
|
@@ -4,16 +4,28 @@
|
|
|
4
4
|
* Features:
|
|
5
5
|
* - Clean border styling with sharp look
|
|
6
6
|
* - Optional hover shadow effect for interactive cards
|
|
7
|
-
* -
|
|
7
|
+
* - Multiple variants: default, featured, highlight, warning
|
|
8
8
|
* - Consistent with brand guidelines
|
|
9
|
+
*
|
|
10
|
+
* Variants:
|
|
11
|
+
* - default: Standard card with 1px border
|
|
12
|
+
* - featured: Thicker 2px border for emphasis
|
|
13
|
+
* - highlight: Teal border (brand-core-1) for positive/info callouts
|
|
14
|
+
* - warning: Red border (brand-red-primary) for warnings/alerts
|
|
9
15
|
*/
|
|
10
16
|
import type { Snippet } from 'svelte';
|
|
11
17
|
interface Props {
|
|
12
18
|
/** Enable hover shadow effect for interactive cards */
|
|
13
19
|
interactive?: boolean;
|
|
14
|
-
/**
|
|
15
|
-
|
|
16
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Card variant
|
|
22
|
+
* - default: Standard 1px border
|
|
23
|
+
* - featured: Thicker 2px border
|
|
24
|
+
* - highlight: Teal border for positive/info callouts
|
|
25
|
+
* - warning: Red border for warnings/alerts
|
|
26
|
+
*/
|
|
27
|
+
variant?: 'default' | 'featured' | 'highlight' | 'warning';
|
|
28
|
+
/** Border color override (ignored for highlight/warning variants) */
|
|
17
29
|
borderColor?: 'default' | 'primary' | 'muted';
|
|
18
30
|
/** Additional classes */
|
|
19
31
|
class?: string;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Snippet } from 'svelte';
|
|
2
2
|
declare const Toast: import("svelte").Component<{
|
|
3
3
|
[key: string]: unknown;
|
|
4
|
-
type?: "
|
|
4
|
+
type?: "warning" | "error" | "success" | "info" | undefined;
|
|
5
5
|
title?: string;
|
|
6
6
|
message: string;
|
|
7
7
|
class?: string;
|
|
@@ -6,3 +6,4 @@
|
|
|
6
6
|
*/
|
|
7
7
|
export { useForm, type UseFormOptions, type UseFormReturn, type FieldError, type FormState, type InferFormData, } from './useForm.svelte.js';
|
|
8
8
|
export { useAsync, runAsync, type UseAsyncOptions, type UseAsyncReturn, } from './useAsync.svelte.js';
|
|
9
|
+
export { usePersistedForm, type UsePersistedFormOptions, type UsePersistedFormReturn, type InferPersistedFormData, } from './usePersistedForm.svelte.js';
|
|
@@ -8,3 +8,5 @@
|
|
|
8
8
|
export { useForm, } from './useForm.svelte.js';
|
|
9
9
|
// Async operation composable
|
|
10
10
|
export { useAsync, runAsync, } from './useAsync.svelte.js';
|
|
11
|
+
// Persisted form composable with localStorage draft saving
|
|
12
|
+
export { usePersistedForm, } from './usePersistedForm.svelte.js';
|
|
@@ -84,6 +84,8 @@ export interface UseFormReturn<T extends z.ZodObject<z.ZodRawShape>> {
|
|
|
84
84
|
readonly data: z.infer<T>;
|
|
85
85
|
/** Field errors (reactive) */
|
|
86
86
|
readonly errors: Record<string, string>;
|
|
87
|
+
/** Global error message for API/submission errors (reactive) */
|
|
88
|
+
readonly globalError: string;
|
|
87
89
|
/** Whether form is submitting (reactive) */
|
|
88
90
|
readonly isSubmitting: boolean;
|
|
89
91
|
/** Whether form has been submitted (reactive) */
|
|
@@ -110,8 +112,12 @@ export interface UseFormReturn<T extends z.ZodObject<z.ZodRawShape>> {
|
|
|
110
112
|
clearErrors: () => void;
|
|
111
113
|
/** Set a specific error */
|
|
112
114
|
setError: (field: string, message: string) => void;
|
|
115
|
+
/** Set the global error message */
|
|
116
|
+
setGlobalError: (message: string) => void;
|
|
113
117
|
/** Mark a field as dirty */
|
|
114
118
|
markDirty: (field: string) => void;
|
|
119
|
+
/** Handle field blur - validates the field and marks dirty */
|
|
120
|
+
handleBlur: (field: keyof z.infer<T>) => void;
|
|
115
121
|
}
|
|
116
122
|
/**
|
|
117
123
|
* Create a form handler with Zod validation and Svelte 5 runes
|
|
@@ -51,12 +51,13 @@ export function useForm(options) {
|
|
|
51
51
|
// Reactive state using Svelte 5 runes
|
|
52
52
|
let data = $state(cloneInitialValues());
|
|
53
53
|
let errors = $state({});
|
|
54
|
+
let globalError = $state('');
|
|
54
55
|
let isSubmitting = $state(false);
|
|
55
56
|
let isSubmitted = $state(false);
|
|
56
57
|
let dirtyFields = $state(new Set());
|
|
57
58
|
// Derived state
|
|
58
59
|
const isDirty = $derived(dirtyFields.size > 0);
|
|
59
|
-
const isValid = $derived(Object.keys(errors).length === 0);
|
|
60
|
+
const isValid = $derived(Object.keys(errors).length === 0 && globalError === '');
|
|
60
61
|
/**
|
|
61
62
|
* Set nested value in object using dot notation path
|
|
62
63
|
*/
|
|
@@ -159,6 +160,9 @@ export function useForm(options) {
|
|
|
159
160
|
*/
|
|
160
161
|
async function handleSubmit(event) {
|
|
161
162
|
event?.preventDefault();
|
|
163
|
+
// Clear previous errors
|
|
164
|
+
errors = {};
|
|
165
|
+
globalError = '';
|
|
162
166
|
isSubmitted = true;
|
|
163
167
|
// Validate all fields
|
|
164
168
|
if (!validate()) {
|
|
@@ -180,38 +184,56 @@ export function useForm(options) {
|
|
|
180
184
|
}
|
|
181
185
|
catch (error) {
|
|
182
186
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
187
|
+
const message = errorMessage || err.message || 'An error occurred';
|
|
188
|
+
// Set global error for display in form
|
|
189
|
+
globalError = message;
|
|
183
190
|
if (onError) {
|
|
184
191
|
onError(err);
|
|
185
192
|
}
|
|
186
193
|
if (showToastOnError) {
|
|
187
|
-
toastStore.error(
|
|
194
|
+
toastStore.error(message);
|
|
188
195
|
}
|
|
189
196
|
}
|
|
190
197
|
finally {
|
|
191
198
|
isSubmitting = false;
|
|
192
199
|
}
|
|
193
200
|
}
|
|
201
|
+
/**
|
|
202
|
+
* Handle field blur - validates the field and marks dirty
|
|
203
|
+
*/
|
|
204
|
+
function handleBlur(field) {
|
|
205
|
+
markDirty(field);
|
|
206
|
+
validateField(field);
|
|
207
|
+
}
|
|
194
208
|
/**
|
|
195
209
|
* Reset form to initial values
|
|
196
210
|
*/
|
|
197
211
|
function reset() {
|
|
198
212
|
data = cloneInitialValues();
|
|
199
213
|
errors = {};
|
|
214
|
+
globalError = '';
|
|
200
215
|
isSubmitted = false;
|
|
201
216
|
dirtyFields = new Set();
|
|
202
217
|
}
|
|
203
218
|
/**
|
|
204
|
-
* Clear all errors
|
|
219
|
+
* Clear all errors (field and global)
|
|
205
220
|
*/
|
|
206
221
|
function clearErrors() {
|
|
207
222
|
errors = {};
|
|
223
|
+
globalError = '';
|
|
208
224
|
}
|
|
209
225
|
/**
|
|
210
|
-
* Set a specific error manually
|
|
226
|
+
* Set a specific field error manually
|
|
211
227
|
*/
|
|
212
228
|
function setError(field, message) {
|
|
213
229
|
errors = { ...errors, [field]: message };
|
|
214
230
|
}
|
|
231
|
+
/**
|
|
232
|
+
* Set the global error message
|
|
233
|
+
*/
|
|
234
|
+
function setGlobalError(message) {
|
|
235
|
+
globalError = message;
|
|
236
|
+
}
|
|
215
237
|
return {
|
|
216
238
|
get data() {
|
|
217
239
|
return data;
|
|
@@ -219,6 +241,9 @@ export function useForm(options) {
|
|
|
219
241
|
get errors() {
|
|
220
242
|
return errors;
|
|
221
243
|
},
|
|
244
|
+
get globalError() {
|
|
245
|
+
return globalError;
|
|
246
|
+
},
|
|
222
247
|
get isSubmitting() {
|
|
223
248
|
return isSubmitting;
|
|
224
249
|
},
|
|
@@ -240,6 +265,8 @@ export function useForm(options) {
|
|
|
240
265
|
reset,
|
|
241
266
|
clearErrors,
|
|
242
267
|
setError,
|
|
268
|
+
setGlobalError,
|
|
243
269
|
markDirty,
|
|
270
|
+
handleBlur,
|
|
244
271
|
};
|
|
245
272
|
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* usePersistedForm - Form state management with localStorage persistence
|
|
3
|
+
*
|
|
4
|
+
* Composes with useForm to add:
|
|
5
|
+
* - Automatic draft saving to localStorage with debounce
|
|
6
|
+
* - Draft restoration on mount
|
|
7
|
+
* - Configurable expiration time
|
|
8
|
+
* - Field exclusion from persistence (e.g., consent fields)
|
|
9
|
+
* - Duplicate submission prevention with cooldown
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```svelte
|
|
13
|
+
* <script lang="ts">
|
|
14
|
+
* import { usePersistedForm } from '@classic-homes/theme-svelte';
|
|
15
|
+
* import { z } from 'zod';
|
|
16
|
+
*
|
|
17
|
+
* const schema = z.object({
|
|
18
|
+
* email: z.string().email(),
|
|
19
|
+
* name: z.string().min(1),
|
|
20
|
+
* consent: z.boolean(),
|
|
21
|
+
* });
|
|
22
|
+
*
|
|
23
|
+
* const form = usePersistedForm({
|
|
24
|
+
* schema,
|
|
25
|
+
* initialValues: { email: '', name: '', consent: false },
|
|
26
|
+
* onSubmit: async (data) => {
|
|
27
|
+
* await submitForm(data);
|
|
28
|
+
* },
|
|
29
|
+
* storageKey: 'my-form-draft',
|
|
30
|
+
* excludeFields: ['consent'], // Don't persist consent
|
|
31
|
+
* expirationMs: 7 * 24 * 60 * 60 * 1000, // 7 days
|
|
32
|
+
* onDraftRestored: () => {
|
|
33
|
+
* console.log('Draft was restored');
|
|
34
|
+
* },
|
|
35
|
+
* });
|
|
36
|
+
* </script>
|
|
37
|
+
*
|
|
38
|
+
* <form onsubmit={form.handleSubmit}>
|
|
39
|
+
* <input bind:value={form.data.email} />
|
|
40
|
+
* {#if form.draftRestored}
|
|
41
|
+
* <div>Your previous draft has been restored.</div>
|
|
42
|
+
* {/if}
|
|
43
|
+
* </form>
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
import { z } from 'zod';
|
|
47
|
+
import { type UseFormOptions, type UseFormReturn } from './useForm.svelte.js';
|
|
48
|
+
export interface UsePersistedFormOptions<T extends z.ZodObject<z.ZodRawShape>> extends UseFormOptions<T> {
|
|
49
|
+
/** localStorage key for draft persistence */
|
|
50
|
+
storageKey: string;
|
|
51
|
+
/** Fields to exclude from persistence (e.g., consent fields that require re-confirmation) */
|
|
52
|
+
excludeFields?: (keyof z.infer<T>)[];
|
|
53
|
+
/** Expiration time in milliseconds (default: 7 days) */
|
|
54
|
+
expirationMs?: number;
|
|
55
|
+
/** Callback when a draft is restored */
|
|
56
|
+
onDraftRestored?: () => void;
|
|
57
|
+
/** Debounce time for saving drafts in milliseconds (default: 500ms) */
|
|
58
|
+
debounceMs?: number;
|
|
59
|
+
/** Cooldown time between submissions in milliseconds (default: 5000ms) */
|
|
60
|
+
submitCooldownMs?: number;
|
|
61
|
+
}
|
|
62
|
+
export interface UsePersistedFormReturn<T extends z.ZodObject<z.ZodRawShape>> extends UseFormReturn<T> {
|
|
63
|
+
/** Whether a draft was restored on mount (reactive) */
|
|
64
|
+
readonly draftRestored: boolean;
|
|
65
|
+
/** Clear the stored draft */
|
|
66
|
+
clearDraft: () => void;
|
|
67
|
+
/** Dismiss the draft restored notification */
|
|
68
|
+
dismissDraftNotification: () => void;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Create a form handler with localStorage persistence
|
|
72
|
+
* Composes with useForm for all base functionality
|
|
73
|
+
*/
|
|
74
|
+
export declare function usePersistedForm<T extends z.ZodObject<z.ZodRawShape>>(options: UsePersistedFormOptions<T>): UsePersistedFormReturn<T>;
|
|
75
|
+
/**
|
|
76
|
+
* Type helper to extract form data type from schema
|
|
77
|
+
*/
|
|
78
|
+
export type InferPersistedFormData<T extends z.ZodObject<z.ZodRawShape>> = z.infer<T>;
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* usePersistedForm - Form state management with localStorage persistence
|
|
3
|
+
*
|
|
4
|
+
* Composes with useForm to add:
|
|
5
|
+
* - Automatic draft saving to localStorage with debounce
|
|
6
|
+
* - Draft restoration on mount
|
|
7
|
+
* - Configurable expiration time
|
|
8
|
+
* - Field exclusion from persistence (e.g., consent fields)
|
|
9
|
+
* - Duplicate submission prevention with cooldown
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```svelte
|
|
13
|
+
* <script lang="ts">
|
|
14
|
+
* import { usePersistedForm } from '@classic-homes/theme-svelte';
|
|
15
|
+
* import { z } from 'zod';
|
|
16
|
+
*
|
|
17
|
+
* const schema = z.object({
|
|
18
|
+
* email: z.string().email(),
|
|
19
|
+
* name: z.string().min(1),
|
|
20
|
+
* consent: z.boolean(),
|
|
21
|
+
* });
|
|
22
|
+
*
|
|
23
|
+
* const form = usePersistedForm({
|
|
24
|
+
* schema,
|
|
25
|
+
* initialValues: { email: '', name: '', consent: false },
|
|
26
|
+
* onSubmit: async (data) => {
|
|
27
|
+
* await submitForm(data);
|
|
28
|
+
* },
|
|
29
|
+
* storageKey: 'my-form-draft',
|
|
30
|
+
* excludeFields: ['consent'], // Don't persist consent
|
|
31
|
+
* expirationMs: 7 * 24 * 60 * 60 * 1000, // 7 days
|
|
32
|
+
* onDraftRestored: () => {
|
|
33
|
+
* console.log('Draft was restored');
|
|
34
|
+
* },
|
|
35
|
+
* });
|
|
36
|
+
* </script>
|
|
37
|
+
*
|
|
38
|
+
* <form onsubmit={form.handleSubmit}>
|
|
39
|
+
* <input bind:value={form.data.email} />
|
|
40
|
+
* {#if form.draftRestored}
|
|
41
|
+
* <div>Your previous draft has been restored.</div>
|
|
42
|
+
* {/if}
|
|
43
|
+
* </form>
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
import { useForm } from './useForm.svelte.js';
|
|
47
|
+
import { toastStore } from '../stores/toast.svelte.js';
|
|
48
|
+
const DEFAULT_EXPIRATION_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
|
|
49
|
+
const DEFAULT_DEBOUNCE_MS = 500;
|
|
50
|
+
const DEFAULT_SUBMIT_COOLDOWN_MS = 5000;
|
|
51
|
+
/**
|
|
52
|
+
* Check if localStorage is available (also checks for browser environment)
|
|
53
|
+
*/
|
|
54
|
+
function isStorageAvailable() {
|
|
55
|
+
if (typeof window === 'undefined' || typeof localStorage === 'undefined') {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
const test = '__storage_test__';
|
|
60
|
+
localStorage.setItem(test, test);
|
|
61
|
+
localStorage.removeItem(test);
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Create a form handler with localStorage persistence
|
|
70
|
+
* Composes with useForm for all base functionality
|
|
71
|
+
*/
|
|
72
|
+
export function usePersistedForm(options) {
|
|
73
|
+
const { storageKey, excludeFields = [], expirationMs = DEFAULT_EXPIRATION_MS, onDraftRestored, debounceMs = DEFAULT_DEBOUNCE_MS, submitCooldownMs = DEFAULT_SUBMIT_COOLDOWN_MS, onSubmit, initialValues, ...baseOptions } = options;
|
|
74
|
+
// Persistence state
|
|
75
|
+
let draftRestored = $state(false);
|
|
76
|
+
let lastSubmitTime = $state(0);
|
|
77
|
+
let debounceTimer = null;
|
|
78
|
+
/**
|
|
79
|
+
* Load draft from localStorage
|
|
80
|
+
*/
|
|
81
|
+
function loadDraft() {
|
|
82
|
+
if (!isStorageAvailable())
|
|
83
|
+
return null;
|
|
84
|
+
try {
|
|
85
|
+
const stored = localStorage.getItem(storageKey);
|
|
86
|
+
if (!stored)
|
|
87
|
+
return null;
|
|
88
|
+
const draft = JSON.parse(stored);
|
|
89
|
+
// Check if draft has expired
|
|
90
|
+
if (Date.now() - draft.timestamp > expirationMs) {
|
|
91
|
+
clearDraft();
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
return draft.data;
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
// Silently fail - corrupt data
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Clear draft from localStorage
|
|
103
|
+
*/
|
|
104
|
+
function clearDraft() {
|
|
105
|
+
if (!isStorageAvailable())
|
|
106
|
+
return;
|
|
107
|
+
try {
|
|
108
|
+
localStorage.removeItem(storageKey);
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
// Silently fail
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Get the initial values, merging with any restored draft
|
|
116
|
+
*/
|
|
117
|
+
function getInitialValues() {
|
|
118
|
+
const draft = loadDraft();
|
|
119
|
+
if (draft && Object.keys(draft).length > 0) {
|
|
120
|
+
// Schedule callback for after initialization
|
|
121
|
+
if (typeof window !== 'undefined') {
|
|
122
|
+
queueMicrotask(() => {
|
|
123
|
+
draftRestored = true;
|
|
124
|
+
onDraftRestored?.();
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
// Merge draft with initial values (keeps excluded fields at defaults)
|
|
128
|
+
return { ...JSON.parse(JSON.stringify(initialValues)), ...draft };
|
|
129
|
+
}
|
|
130
|
+
return JSON.parse(JSON.stringify(initialValues));
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Wrapped onSubmit with cooldown and draft clearing
|
|
134
|
+
*/
|
|
135
|
+
async function wrappedOnSubmit(data) {
|
|
136
|
+
// Check for duplicate submission
|
|
137
|
+
const now = Date.now();
|
|
138
|
+
if (now - lastSubmitTime < submitCooldownMs) {
|
|
139
|
+
toastStore.warning('Please wait before submitting again');
|
|
140
|
+
throw new Error('Submission cooldown active');
|
|
141
|
+
}
|
|
142
|
+
try {
|
|
143
|
+
await onSubmit(data);
|
|
144
|
+
lastSubmitTime = now;
|
|
145
|
+
// Clear draft on successful submission
|
|
146
|
+
clearDraft();
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
// Reset cooldown on error to allow retry
|
|
150
|
+
lastSubmitTime = 0;
|
|
151
|
+
throw error;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
// Create base form with potentially restored initial values
|
|
155
|
+
const baseForm = useForm({
|
|
156
|
+
...baseOptions,
|
|
157
|
+
initialValues: getInitialValues(),
|
|
158
|
+
onSubmit: wrappedOnSubmit,
|
|
159
|
+
});
|
|
160
|
+
/**
|
|
161
|
+
* Get data to persist (excludes specified fields)
|
|
162
|
+
*/
|
|
163
|
+
function getDataToPersist() {
|
|
164
|
+
const dataCopy = { ...baseForm.data };
|
|
165
|
+
for (const field of excludeFields) {
|
|
166
|
+
delete dataCopy[field];
|
|
167
|
+
}
|
|
168
|
+
return dataCopy;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Save draft to localStorage with debounce
|
|
172
|
+
*/
|
|
173
|
+
function saveDraft() {
|
|
174
|
+
if (!isStorageAvailable())
|
|
175
|
+
return;
|
|
176
|
+
if (debounceTimer) {
|
|
177
|
+
clearTimeout(debounceTimer);
|
|
178
|
+
}
|
|
179
|
+
debounceTimer = setTimeout(() => {
|
|
180
|
+
try {
|
|
181
|
+
const draft = {
|
|
182
|
+
data: getDataToPersist(),
|
|
183
|
+
timestamp: Date.now(),
|
|
184
|
+
};
|
|
185
|
+
localStorage.setItem(storageKey, JSON.stringify(draft));
|
|
186
|
+
}
|
|
187
|
+
catch (error) {
|
|
188
|
+
// Silently fail - localStorage may be full or unavailable
|
|
189
|
+
console.warn('Failed to save form draft', error);
|
|
190
|
+
}
|
|
191
|
+
}, debounceMs);
|
|
192
|
+
}
|
|
193
|
+
// Watch for form changes and save draft
|
|
194
|
+
$effect(() => {
|
|
195
|
+
// Access data to create dependency
|
|
196
|
+
JSON.stringify(baseForm.data);
|
|
197
|
+
if (baseForm.isDirty) {
|
|
198
|
+
saveDraft();
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
/**
|
|
202
|
+
* Extended reset that also clears the draft
|
|
203
|
+
*/
|
|
204
|
+
function reset() {
|
|
205
|
+
baseForm.reset();
|
|
206
|
+
draftRestored = false;
|
|
207
|
+
clearDraft();
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Dismiss the draft restored notification
|
|
211
|
+
*/
|
|
212
|
+
function dismissDraftNotification() {
|
|
213
|
+
draftRestored = false;
|
|
214
|
+
}
|
|
215
|
+
return {
|
|
216
|
+
// Delegate all base form properties
|
|
217
|
+
get data() {
|
|
218
|
+
return baseForm.data;
|
|
219
|
+
},
|
|
220
|
+
get errors() {
|
|
221
|
+
return baseForm.errors;
|
|
222
|
+
},
|
|
223
|
+
get globalError() {
|
|
224
|
+
return baseForm.globalError;
|
|
225
|
+
},
|
|
226
|
+
get isSubmitting() {
|
|
227
|
+
return baseForm.isSubmitting;
|
|
228
|
+
},
|
|
229
|
+
get isSubmitted() {
|
|
230
|
+
return baseForm.isSubmitted;
|
|
231
|
+
},
|
|
232
|
+
get isDirty() {
|
|
233
|
+
return baseForm.isDirty;
|
|
234
|
+
},
|
|
235
|
+
get isValid() {
|
|
236
|
+
return baseForm.isValid;
|
|
237
|
+
},
|
|
238
|
+
// Persistence-specific properties
|
|
239
|
+
get draftRestored() {
|
|
240
|
+
return draftRestored;
|
|
241
|
+
},
|
|
242
|
+
// Delegate base form methods
|
|
243
|
+
setField: baseForm.setField,
|
|
244
|
+
setFields: baseForm.setFields,
|
|
245
|
+
setNestedField: baseForm.setNestedField,
|
|
246
|
+
validateField: baseForm.validateField,
|
|
247
|
+
validate: baseForm.validate,
|
|
248
|
+
handleSubmit: baseForm.handleSubmit,
|
|
249
|
+
clearErrors: baseForm.clearErrors,
|
|
250
|
+
setError: baseForm.setError,
|
|
251
|
+
setGlobalError: baseForm.setGlobalError,
|
|
252
|
+
markDirty: baseForm.markDirty,
|
|
253
|
+
handleBlur: baseForm.handleBlur,
|
|
254
|
+
// Override reset to also clear draft
|
|
255
|
+
reset,
|
|
256
|
+
// Persistence-specific methods
|
|
257
|
+
clearDraft,
|
|
258
|
+
dismissDraftNotification,
|
|
259
|
+
};
|
|
260
|
+
}
|
package/dist/lib/index.d.ts
CHANGED
|
@@ -72,5 +72,5 @@ export { cn } from './utils.js';
|
|
|
72
72
|
export { tv, type VariantProps } from 'tailwind-variants';
|
|
73
73
|
export { validateNonEmptyArray, validateRequired, validateOneOf, validateRange, validateProps, createValidator, type ValidationResult, } from './validation.js';
|
|
74
74
|
export { perfStart, perfEnd, measure, measureAsync, getPerformanceEntries, clearPerformanceEntries, createPerfMonitor, type PerformanceMark, } from './performance.js';
|
|
75
|
-
export { useForm, useAsync, runAsync, type UseFormOptions, type UseFormReturn, type FieldError, type FormState, type InferFormData, type UseAsyncOptions, type UseAsyncReturn, } from './composables/index.js';
|
|
75
|
+
export { useForm, useAsync, runAsync, usePersistedForm, type UseFormOptions, type UseFormReturn, type FieldError, type FormState, type InferFormData, type UseAsyncOptions, type UseAsyncReturn, type UsePersistedFormOptions, type UsePersistedFormReturn, type InferPersistedFormData, } from './composables/index.js';
|
|
76
76
|
export * from './schemas/index.js';
|
package/dist/lib/index.js
CHANGED
|
@@ -89,6 +89,6 @@ export { validateNonEmptyArray, validateRequired, validateOneOf, validateRange,
|
|
|
89
89
|
// Performance monitoring utilities (dev-only, tree-shaken in production)
|
|
90
90
|
export { perfStart, perfEnd, measure, measureAsync, getPerformanceEntries, clearPerformanceEntries, createPerfMonitor, } from './performance.js';
|
|
91
91
|
// Composables - reusable Svelte 5 state management patterns
|
|
92
|
-
export { useForm, useAsync, runAsync, } from './composables/index.js';
|
|
92
|
+
export { useForm, useAsync, runAsync, usePersistedForm, } from './composables/index.js';
|
|
93
93
|
// Validation schemas - Zod schemas for forms and data validation
|
|
94
94
|
export * from './schemas/index.js';
|