@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.
Files changed (124) hide show
  1. package/README.md +305 -0
  2. package/dist/lib/components/Alert.svelte +51 -0
  3. package/dist/lib/components/Alert.svelte.d.ts +9 -0
  4. package/dist/lib/components/AlertDescription.svelte +16 -0
  5. package/dist/lib/components/AlertDescription.svelte.d.ts +9 -0
  6. package/dist/lib/components/AlertDialog.svelte +136 -0
  7. package/dist/lib/components/AlertDialog.svelte.d.ts +79 -0
  8. package/dist/lib/components/AlertTitle.svelte +16 -0
  9. package/dist/lib/components/AlertTitle.svelte.d.ts +9 -0
  10. package/dist/lib/components/Avatar.svelte +56 -0
  11. package/dist/lib/components/Avatar.svelte.d.ts +26 -0
  12. package/dist/lib/components/AvatarFallback.svelte +31 -0
  13. package/dist/lib/components/AvatarFallback.svelte.d.ts +17 -0
  14. package/dist/lib/components/AvatarImage.svelte +29 -0
  15. package/dist/lib/components/AvatarImage.svelte.d.ts +12 -0
  16. package/dist/lib/components/Badge.svelte +73 -0
  17. package/dist/lib/components/Badge.svelte.d.ts +11 -0
  18. package/dist/lib/components/Button.svelte +130 -0
  19. package/dist/lib/components/Button.svelte.d.ts +17 -0
  20. package/dist/lib/components/Card.svelte +58 -0
  21. package/dist/lib/components/Card.svelte.d.ts +26 -0
  22. package/dist/lib/components/CardContent.svelte +16 -0
  23. package/dist/lib/components/CardContent.svelte.d.ts +9 -0
  24. package/dist/lib/components/CardDescription.svelte +16 -0
  25. package/dist/lib/components/CardDescription.svelte.d.ts +9 -0
  26. package/dist/lib/components/CardFooter.svelte +16 -0
  27. package/dist/lib/components/CardFooter.svelte.d.ts +9 -0
  28. package/dist/lib/components/CardHeader.svelte +16 -0
  29. package/dist/lib/components/CardHeader.svelte.d.ts +9 -0
  30. package/dist/lib/components/CardTitle.svelte +16 -0
  31. package/dist/lib/components/CardTitle.svelte.d.ts +9 -0
  32. package/dist/lib/components/Checkbox.svelte +65 -0
  33. package/dist/lib/components/Checkbox.svelte.d.ts +14 -0
  34. package/dist/lib/components/DataTable.svelte +334 -0
  35. package/dist/lib/components/DataTable.svelte.d.ts +103 -0
  36. package/dist/lib/components/Dialog.svelte +111 -0
  37. package/dist/lib/components/Dialog.svelte.d.ts +22 -0
  38. package/dist/lib/components/DropdownMenu.svelte +135 -0
  39. package/dist/lib/components/DropdownMenu.svelte.d.ts +33 -0
  40. package/dist/lib/components/FileUpload.svelte +448 -0
  41. package/dist/lib/components/FileUpload.svelte.d.ts +42 -0
  42. package/dist/lib/components/FormField.svelte +134 -0
  43. package/dist/lib/components/FormField.svelte.d.ts +37 -0
  44. package/dist/lib/components/Input.svelte +61 -0
  45. package/dist/lib/components/Input.svelte.d.ts +19 -0
  46. package/dist/lib/components/Label.svelte +33 -0
  47. package/dist/lib/components/Label.svelte.d.ts +11 -0
  48. package/dist/lib/components/LoadingLogo.svelte +124 -0
  49. package/dist/lib/components/LoadingLogo.svelte.d.ts +16 -0
  50. package/dist/lib/components/LogoMain.svelte +237 -0
  51. package/dist/lib/components/LogoMain.svelte.d.ts +20 -0
  52. package/dist/lib/components/PageHeader.svelte +90 -0
  53. package/dist/lib/components/PageHeader.svelte.d.ts +28 -0
  54. package/dist/lib/components/Section.svelte +44 -0
  55. package/dist/lib/components/Section.svelte.d.ts +28 -0
  56. package/dist/lib/components/Select.svelte +174 -0
  57. package/dist/lib/components/Select.svelte.d.ts +32 -0
  58. package/dist/lib/components/Separator.svelte +29 -0
  59. package/dist/lib/components/Separator.svelte.d.ts +9 -0
  60. package/dist/lib/components/Skeleton.svelte +35 -0
  61. package/dist/lib/components/Skeleton.svelte.d.ts +7 -0
  62. package/dist/lib/components/Spinner.svelte +50 -0
  63. package/dist/lib/components/Spinner.svelte.d.ts +8 -0
  64. package/dist/lib/components/Switch.svelte +56 -0
  65. package/dist/lib/components/Switch.svelte.d.ts +14 -0
  66. package/dist/lib/components/TabPanel.svelte +44 -0
  67. package/dist/lib/components/TabPanel.svelte.d.ts +12 -0
  68. package/dist/lib/components/Tabs.svelte +125 -0
  69. package/dist/lib/components/Tabs.svelte.d.ts +19 -0
  70. package/dist/lib/components/Textarea.svelte +54 -0
  71. package/dist/lib/components/Textarea.svelte.d.ts +16 -0
  72. package/dist/lib/components/Toast.svelte +116 -0
  73. package/dist/lib/components/Toast.svelte.d.ts +12 -0
  74. package/dist/lib/components/ToastContainer.svelte +56 -0
  75. package/dist/lib/components/ToastContainer.svelte.d.ts +8 -0
  76. package/dist/lib/components/Tooltip.svelte +55 -0
  77. package/dist/lib/components/Tooltip.svelte.d.ts +18 -0
  78. package/dist/lib/components/layout/AppShell.svelte +82 -0
  79. package/dist/lib/components/layout/AppShell.svelte.d.ts +44 -0
  80. package/dist/lib/components/layout/DashboardLayout.svelte +248 -0
  81. package/dist/lib/components/layout/DashboardLayout.svelte.d.ts +62 -0
  82. package/dist/lib/components/layout/Footer.svelte +130 -0
  83. package/dist/lib/components/layout/Footer.svelte.d.ts +32 -0
  84. package/dist/lib/components/layout/FormPageLayout.svelte +92 -0
  85. package/dist/lib/components/layout/FormPageLayout.svelte.d.ts +33 -0
  86. package/dist/lib/components/layout/Header.svelte +94 -0
  87. package/dist/lib/components/layout/Header.svelte.d.ts +30 -0
  88. package/dist/lib/components/layout/PublicLayout.svelte +180 -0
  89. package/dist/lib/components/layout/PublicLayout.svelte.d.ts +39 -0
  90. package/dist/lib/components/layout/QuickLinks.svelte +112 -0
  91. package/dist/lib/components/layout/QuickLinks.svelte.d.ts +27 -0
  92. package/dist/lib/components/layout/Sidebar.svelte +243 -0
  93. package/dist/lib/components/layout/Sidebar.svelte.d.ts +48 -0
  94. package/dist/lib/composables/index.d.ts +8 -0
  95. package/dist/lib/composables/index.js +10 -0
  96. package/dist/lib/composables/useAsync.svelte.d.ts +102 -0
  97. package/dist/lib/composables/useAsync.svelte.js +210 -0
  98. package/dist/lib/composables/useForm.svelte.d.ts +123 -0
  99. package/dist/lib/composables/useForm.svelte.js +245 -0
  100. package/dist/lib/index.d.ts +65 -0
  101. package/dist/lib/index.js +83 -0
  102. package/dist/lib/performance.d.ts +79 -0
  103. package/dist/lib/performance.js +170 -0
  104. package/dist/lib/schemas/auth.d.ts +410 -0
  105. package/dist/lib/schemas/auth.js +216 -0
  106. package/dist/lib/schemas/common.d.ts +267 -0
  107. package/dist/lib/schemas/common.js +268 -0
  108. package/dist/lib/schemas/index.d.ts +24 -0
  109. package/dist/lib/schemas/index.js +32 -0
  110. package/dist/lib/stores/sidebar.svelte.d.ts +25 -0
  111. package/dist/lib/stores/sidebar.svelte.js +38 -0
  112. package/dist/lib/stores/theme.svelte.d.ts +72 -0
  113. package/dist/lib/stores/theme.svelte.js +150 -0
  114. package/dist/lib/stores/toast.svelte.d.ts +62 -0
  115. package/dist/lib/stores/toast.svelte.js +93 -0
  116. package/dist/lib/types/components.d.ts +85 -0
  117. package/dist/lib/types/components.js +7 -0
  118. package/dist/lib/types/layout.d.ts +258 -0
  119. package/dist/lib/types/layout.js +7 -0
  120. package/dist/lib/utils.d.ts +6 -0
  121. package/dist/lib/utils.js +9 -0
  122. package/dist/lib/validation.d.ts +101 -0
  123. package/dist/lib/validation.js +170 -0
  124. 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
+ }