@classic-homes/theme-svelte 0.1.21 → 0.1.22

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.
@@ -2,6 +2,7 @@
2
2
  import { Select as SelectPrimitive } from 'bits-ui';
3
3
  import { cn } from '../utils.js';
4
4
  import { validateNonEmptyArray } from '../validation.js';
5
+ import Label from './Label.svelte';
5
6
 
6
7
  export interface SelectOption {
7
8
  value: string;
@@ -37,6 +38,8 @@
37
38
  id?: string;
38
39
  /** Error message to display */
39
40
  error?: string;
41
+ /** Hint text displayed below the select */
42
+ hint?: string;
40
43
  }
41
44
 
42
45
  let {
@@ -51,8 +54,24 @@
51
54
  label,
52
55
  id,
53
56
  error,
57
+ hint,
54
58
  }: Props = $props();
55
59
 
60
+ // Generate unique IDs for accessibility
61
+ // Using a random suffix ensures uniqueness if id is not provided
62
+ const randomSuffix = Math.random().toString(36).substring(2, 9);
63
+ const componentId = $derived(id || `select-${randomSuffix}`);
64
+ const hintId = $derived(`${componentId}-hint`);
65
+ const errorId = $derived(`${componentId}-error`);
66
+
67
+ // Compute aria-describedby based on hint and error
68
+ const describedBy = $derived.by(() => {
69
+ const ids: string[] = [];
70
+ if (hint) ids.push(hintId);
71
+ if (error) ids.push(errorId);
72
+ return ids.length > 0 ? ids.join(' ') : undefined;
73
+ });
74
+
56
75
  // Validate props in development
57
76
  $effect(() => {
58
77
  validateNonEmptyArray(options, 'options', 'Select');
@@ -94,78 +113,112 @@
94
113
  value = newValue;
95
114
  onValueChange?.(newValue);
96
115
  }
116
+
117
+ // Determine if we need a container wrapper
118
+ const hasWrapper = $derived(!!label || !!hint || !!error);
97
119
  </script>
98
120
 
99
- {#if label}
100
- <span
101
- class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 mb-1.5 block"
102
- >
103
- {label}
104
- {#if required}
105
- <span class="text-destructive">*</span>
121
+ {#if hasWrapper}
122
+ <div class="space-y-2">
123
+ {#if label}
124
+ <Label for={componentId} {disabled}>
125
+ {label}
126
+ {#if required}
127
+ <span class="text-destructive ml-0.5" aria-hidden="true">*</span>
128
+ <span class="sr-only">(required)</span>
129
+ {/if}
130
+ </Label>
106
131
  {/if}
107
- </span>
108
- {/if}
109
132
 
110
- <SelectPrimitive.Root
111
- type="single"
112
- {name}
113
- {disabled}
114
- {required}
115
- {value}
116
- items={flatItems}
117
- onValueChange={handleValueChange}
118
- >
119
- <SelectPrimitive.Trigger
120
- {id}
121
- class={cn(
122
- 'flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1',
123
- error && 'border-destructive focus:ring-destructive',
124
- className
125
- )}
126
- aria-label={label || placeholder}
127
- aria-invalid={error ? 'true' : undefined}
128
- >
129
- <span class={value ? '' : 'text-muted-foreground'}>
130
- {selectedLabel || placeholder}
131
- </span>
132
- <svg
133
- xmlns="http://www.w3.org/2000/svg"
134
- width="24"
135
- height="24"
136
- viewBox="0 0 24 24"
137
- fill="none"
138
- stroke="currentColor"
139
- stroke-width="2"
140
- stroke-linecap="round"
141
- stroke-linejoin="round"
142
- class="h-4 w-4 opacity-50"
133
+ <SelectPrimitive.Root
134
+ type="single"
135
+ {name}
136
+ {disabled}
137
+ {required}
138
+ {value}
139
+ items={flatItems}
140
+ onValueChange={handleValueChange}
143
141
  >
144
- <path d="m6 9 6 6 6-6" />
145
- </svg>
146
- </SelectPrimitive.Trigger>
147
-
148
- <SelectPrimitive.Content
149
- class="isolate relative z-50 min-w-[var(--bits-floating-anchor-width)] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[side=bottom]:border-t-0 data-[side=bottom]:rounded-t-none data-[side=top]:border-b-0 data-[side=top]:rounded-b-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2"
150
- sideOffset={-1}
151
- collisionPadding={8}
152
- avoidCollisions={true}
153
- >
154
- <div class="p-1 max-h-[300px] overflow-y-auto">
155
- {#each options as item}
156
- {#if isGroup(item)}
157
- <SelectPrimitive.Group>
158
- <div class="py-1.5 pl-8 pr-2 text-sm font-semibold">
159
- {item.label}
160
- </div>
161
- {#each item.options as option}
142
+ <SelectPrimitive.Trigger
143
+ id={componentId}
144
+ class={cn(
145
+ 'flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1',
146
+ error && 'border-destructive focus:ring-destructive',
147
+ className
148
+ )}
149
+ aria-label={!label ? placeholder : undefined}
150
+ aria-invalid={error ? 'true' : undefined}
151
+ aria-describedby={describedBy}
152
+ >
153
+ <span class={value ? '' : 'text-muted-foreground'}>
154
+ {selectedLabel || placeholder}
155
+ </span>
156
+ <svg
157
+ xmlns="http://www.w3.org/2000/svg"
158
+ width="24"
159
+ height="24"
160
+ viewBox="0 0 24 24"
161
+ fill="none"
162
+ stroke="currentColor"
163
+ stroke-width="2"
164
+ stroke-linecap="round"
165
+ stroke-linejoin="round"
166
+ class="h-4 w-4 opacity-50"
167
+ >
168
+ <path d="m6 9 6 6 6-6" />
169
+ </svg>
170
+ </SelectPrimitive.Trigger>
171
+
172
+ <SelectPrimitive.Content
173
+ class="isolate relative z-50 min-w-[var(--bits-floating-anchor-width)] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[side=bottom]:border-t-0 data-[side=bottom]:rounded-t-none data-[side=top]:border-b-0 data-[side=top]:rounded-b-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2"
174
+ sideOffset={-1}
175
+ collisionPadding={8}
176
+ avoidCollisions={true}
177
+ >
178
+ <div class="p-1 max-h-[300px] overflow-y-auto">
179
+ {#each options as item}
180
+ {#if isGroup(item)}
181
+ <SelectPrimitive.Group>
182
+ <div class="py-1.5 pl-8 pr-2 text-sm font-semibold">
183
+ {item.label}
184
+ </div>
185
+ {#each item.options as option}
186
+ <SelectPrimitive.Item
187
+ value={option.value}
188
+ label={option.label}
189
+ disabled={option.disabled}
190
+ class="relative flex w-full cursor-pointer select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[disabled]:cursor-default"
191
+ >
192
+ {#if option.value === value}
193
+ <span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
194
+ <svg
195
+ xmlns="http://www.w3.org/2000/svg"
196
+ width="16"
197
+ height="16"
198
+ viewBox="0 0 24 24"
199
+ fill="none"
200
+ stroke="currentColor"
201
+ stroke-width="2"
202
+ stroke-linecap="round"
203
+ stroke-linejoin="round"
204
+ class="h-4 w-4"
205
+ >
206
+ <polyline points="20 6 9 17 4 12" />
207
+ </svg>
208
+ </span>
209
+ {/if}
210
+ {option.label}
211
+ </SelectPrimitive.Item>
212
+ {/each}
213
+ </SelectPrimitive.Group>
214
+ {:else}
162
215
  <SelectPrimitive.Item
163
- value={option.value}
164
- label={option.label}
165
- disabled={option.disabled}
216
+ value={item.value}
217
+ label={item.label}
218
+ disabled={item.disabled}
166
219
  class="relative flex w-full cursor-pointer select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[disabled]:cursor-default"
167
220
  >
168
- {#if option.value === value}
221
+ {#if item.value === value}
169
222
  <span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
170
223
  <svg
171
224
  xmlns="http://www.w3.org/2000/svg"
@@ -183,45 +236,138 @@
183
236
  </svg>
184
237
  </span>
185
238
  {/if}
186
- {option.label}
239
+ {item.label}
187
240
  </SelectPrimitive.Item>
188
- {/each}
189
- </SelectPrimitive.Group>
190
- {:else}
191
- <SelectPrimitive.Item
192
- value={item.value}
193
- label={item.label}
194
- disabled={item.disabled}
195
- class="relative flex w-full cursor-pointer select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[disabled]:cursor-default"
196
- >
197
- {#if item.value === value}
198
- <span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
199
- <svg
200
- xmlns="http://www.w3.org/2000/svg"
201
- width="16"
202
- height="16"
203
- viewBox="0 0 24 24"
204
- fill="none"
205
- stroke="currentColor"
206
- stroke-width="2"
207
- stroke-linecap="round"
208
- stroke-linejoin="round"
209
- class="h-4 w-4"
210
- >
211
- <polyline points="20 6 9 17 4 12" />
212
- </svg>
213
- </span>
214
241
  {/if}
215
- {item.label}
216
- </SelectPrimitive.Item>
217
- {/if}
218
- {/each}
219
- </div>
220
- </SelectPrimitive.Content>
221
- </SelectPrimitive.Root>
222
-
223
- {#if error}
224
- <p class="text-sm text-destructive mt-1.5" role="alert">
225
- {error}
226
- </p>
242
+ {/each}
243
+ </div>
244
+ </SelectPrimitive.Content>
245
+ </SelectPrimitive.Root>
246
+
247
+ {#if hint && !error}
248
+ <p id={hintId} class="text-sm text-muted-foreground">
249
+ {hint}
250
+ </p>
251
+ {/if}
252
+
253
+ {#if error}
254
+ <p id={errorId} class="text-sm text-destructive" role="alert" aria-live="polite">
255
+ {error}
256
+ </p>
257
+ {/if}
258
+ </div>
259
+ {:else}
260
+ <SelectPrimitive.Root
261
+ type="single"
262
+ {name}
263
+ {disabled}
264
+ {required}
265
+ {value}
266
+ items={flatItems}
267
+ onValueChange={handleValueChange}
268
+ >
269
+ <SelectPrimitive.Trigger
270
+ id={componentId}
271
+ class={cn(
272
+ 'flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1',
273
+ error && 'border-destructive focus:ring-destructive',
274
+ className
275
+ )}
276
+ aria-label={placeholder}
277
+ aria-invalid={error ? 'true' : undefined}
278
+ aria-describedby={describedBy}
279
+ >
280
+ <span class={value ? '' : 'text-muted-foreground'}>
281
+ {selectedLabel || placeholder}
282
+ </span>
283
+ <svg
284
+ xmlns="http://www.w3.org/2000/svg"
285
+ width="24"
286
+ height="24"
287
+ viewBox="0 0 24 24"
288
+ fill="none"
289
+ stroke="currentColor"
290
+ stroke-width="2"
291
+ stroke-linecap="round"
292
+ stroke-linejoin="round"
293
+ class="h-4 w-4 opacity-50"
294
+ >
295
+ <path d="m6 9 6 6 6-6" />
296
+ </svg>
297
+ </SelectPrimitive.Trigger>
298
+
299
+ <SelectPrimitive.Content
300
+ class="isolate relative z-50 min-w-[var(--bits-floating-anchor-width)] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[side=bottom]:border-t-0 data-[side=bottom]:rounded-t-none data-[side=top]:border-b-0 data-[side=top]:rounded-b-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2"
301
+ sideOffset={-1}
302
+ collisionPadding={8}
303
+ avoidCollisions={true}
304
+ >
305
+ <div class="p-1 max-h-[300px] overflow-y-auto">
306
+ {#each options as item}
307
+ {#if isGroup(item)}
308
+ <SelectPrimitive.Group>
309
+ <div class="py-1.5 pl-8 pr-2 text-sm font-semibold">
310
+ {item.label}
311
+ </div>
312
+ {#each item.options as option}
313
+ <SelectPrimitive.Item
314
+ value={option.value}
315
+ label={option.label}
316
+ disabled={option.disabled}
317
+ class="relative flex w-full cursor-pointer select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[disabled]:cursor-default"
318
+ >
319
+ {#if option.value === value}
320
+ <span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
321
+ <svg
322
+ xmlns="http://www.w3.org/2000/svg"
323
+ width="16"
324
+ height="16"
325
+ viewBox="0 0 24 24"
326
+ fill="none"
327
+ stroke="currentColor"
328
+ stroke-width="2"
329
+ stroke-linecap="round"
330
+ stroke-linejoin="round"
331
+ class="h-4 w-4"
332
+ >
333
+ <polyline points="20 6 9 17 4 12" />
334
+ </svg>
335
+ </span>
336
+ {/if}
337
+ {option.label}
338
+ </SelectPrimitive.Item>
339
+ {/each}
340
+ </SelectPrimitive.Group>
341
+ {:else}
342
+ <SelectPrimitive.Item
343
+ value={item.value}
344
+ label={item.label}
345
+ disabled={item.disabled}
346
+ class="relative flex w-full cursor-pointer select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[disabled]:cursor-default"
347
+ >
348
+ {#if item.value === value}
349
+ <span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
350
+ <svg
351
+ xmlns="http://www.w3.org/2000/svg"
352
+ width="16"
353
+ height="16"
354
+ viewBox="0 0 24 24"
355
+ fill="none"
356
+ stroke="currentColor"
357
+ stroke-width="2"
358
+ stroke-linecap="round"
359
+ stroke-linejoin="round"
360
+ class="h-4 w-4"
361
+ >
362
+ <polyline points="20 6 9 17 4 12" />
363
+ </svg>
364
+ </span>
365
+ {/if}
366
+ {item.label}
367
+ </SelectPrimitive.Item>
368
+ {/if}
369
+ {/each}
370
+ </div>
371
+ </SelectPrimitive.Content>
372
+ </SelectPrimitive.Root>
227
373
  {/if}
@@ -30,6 +30,8 @@ interface Props {
30
30
  id?: string;
31
31
  /** Error message to display */
32
32
  error?: string;
33
+ /** Hint text displayed below the select */
34
+ hint?: string;
33
35
  }
34
36
  declare const Select: import("svelte").Component<Props, {}, "value">;
35
37
  type Select = ReturnType<typeof Select>;
@@ -1,7 +1,9 @@
1
1
  <script lang="ts">
2
2
  import { Slider as SliderPrimitive } from 'bits-ui';
3
3
  import { cn } from '../utils.js';
4
+ import { generateId, computeDescribedBy } from '../utils/form.js';
4
5
  import { tv, type VariantProps } from 'tailwind-variants';
6
+ import Label from './Label.svelte';
5
7
 
6
8
  const sliderVariants = tv({
7
9
  slots: {
@@ -58,6 +60,10 @@
58
60
  showValue?: boolean;
59
61
  /** Format function for displayed value */
60
62
  formatValue?: (value: number) => string;
63
+ /** Hint text displayed below the slider */
64
+ hint?: string;
65
+ /** Error message to display */
66
+ error?: string;
61
67
  /** Additional class for the container */
62
68
  class?: string;
63
69
  }
@@ -75,9 +81,20 @@
75
81
  label,
76
82
  showValue = false,
77
83
  formatValue = (v) => String(v),
84
+ hint,
85
+ error,
78
86
  class: className,
79
87
  }: Props = $props();
80
88
 
89
+ // Generate unique IDs for accessibility
90
+ const randomSuffix = generateId('slider').split('-').pop();
91
+ const componentId = $derived(`slider-${randomSuffix}`);
92
+ const hintId = $derived(`${componentId}-hint`);
93
+ const errorId = $derived(`${componentId}-error`);
94
+
95
+ // Compute aria-describedby based on hint and error
96
+ const describedBy = $derived(computeDescribedBy(hintId, errorId, !!hint, !!error));
97
+
81
98
  const styles = $derived(sliderVariants({ size }));
82
99
 
83
100
  function handleValueChange(newValue: number[]) {
@@ -100,9 +117,9 @@
100
117
  {#if label || showValue}
101
118
  <div class="flex items-center justify-between mb-2">
102
119
  {#if label}
103
- <span class="text-sm font-medium leading-none">
120
+ <Label for={undefined} {disabled}>
104
121
  {label}
105
- </span>
122
+ </Label>
106
123
  {/if}
107
124
  {#if showValue}
108
125
  <span class="text-sm text-muted-foreground tabular-nums">
@@ -123,6 +140,7 @@
123
140
  onValueChange={handleValueChange}
124
141
  onValueCommit={handleValueCommit}
125
142
  class={cn(styles.root(), 'rounded-full bg-secondary')}
143
+ aria-describedby={describedBy}
126
144
  >
127
145
  <SliderPrimitive.Range class={styles.range()} />
128
146
  {#each value as _, i}
@@ -133,4 +151,16 @@
133
151
  />
134
152
  {/each}
135
153
  </SliderPrimitive.Root>
154
+
155
+ {#if hint && !error}
156
+ <p id={hintId} class="text-sm text-muted-foreground mt-1.5">
157
+ {hint}
158
+ </p>
159
+ {/if}
160
+
161
+ {#if error}
162
+ <p id={errorId} class="text-sm text-destructive mt-1.5" role="alert" aria-live="polite">
163
+ {error}
164
+ </p>
165
+ {/if}
136
166
  </div>
@@ -23,6 +23,10 @@ declare const Slider: import("svelte").Component<{
23
23
  showValue?: boolean;
24
24
  /** Format function for displayed value */
25
25
  formatValue?: (value: number) => string;
26
+ /** Hint text displayed below the slider */
27
+ hint?: string;
28
+ /** Error message to display */
29
+ error?: string;
26
30
  /** Additional class for the container */
27
31
  class?: string;
28
32
  }, {}, "value">;
@@ -118,6 +118,12 @@ export interface UseFormReturn<T extends z.ZodObject<z.ZodRawShape>> {
118
118
  markDirty: (field: string) => void;
119
119
  /** Handle field blur - validates the field and marks dirty */
120
120
  handleBlur: (field: keyof z.infer<T>) => void;
121
+ /** Create a blur handler function for a field (returns a function suitable for event handlers) */
122
+ createBlurHandler: (field: keyof z.infer<T>) => () => void;
123
+ /** Get error message for an array field item */
124
+ getArrayFieldError: (arrayPath: string, index: number, fieldName: string) => string | undefined;
125
+ /** Build the full path for an array field item (e.g., 'purchasers.0.firstName') */
126
+ buildArrayFieldPath: (arrayPath: string, index: number, fieldName: string) => string;
121
127
  }
122
128
  /**
123
129
  * Create a form handler with Zod validation and Svelte 5 runes
@@ -205,6 +205,36 @@ export function useForm(options) {
205
205
  markDirty(field);
206
206
  validateField(field);
207
207
  }
208
+ /**
209
+ * Create a blur handler function for a field
210
+ * Returns a function that can be passed directly to event handlers
211
+ * This is useful when you need to pass a handler without inline function creation
212
+ */
213
+ function createBlurHandler(field) {
214
+ return () => handleBlur(field);
215
+ }
216
+ /**
217
+ * Build the full path for an array field item
218
+ * @param arrayPath - The base path to the array (e.g., 'purchasers')
219
+ * @param index - The index within the array
220
+ * @param fieldName - The field name within the array item (e.g., 'firstName')
221
+ * @returns The full dot-notation path (e.g., 'purchasers.0.firstName')
222
+ */
223
+ function buildArrayFieldPath(arrayPath, index, fieldName) {
224
+ return `${arrayPath}.${index}.${fieldName}`;
225
+ }
226
+ /**
227
+ * Get error message for an array field item
228
+ * Convenience method for accessing errors in nested array structures
229
+ * @param arrayPath - The base path to the array (e.g., 'purchasers')
230
+ * @param index - The index within the array
231
+ * @param fieldName - The field name within the array item (e.g., 'firstName')
232
+ * @returns The error message if present, undefined otherwise
233
+ */
234
+ function getArrayFieldError(arrayPath, index, fieldName) {
235
+ const path = buildArrayFieldPath(arrayPath, index, fieldName);
236
+ return errors[path];
237
+ }
208
238
  /**
209
239
  * Reset form to initial values
210
240
  */
@@ -268,5 +298,8 @@ export function useForm(options) {
268
298
  setGlobalError,
269
299
  markDirty,
270
300
  handleBlur,
301
+ createBlurHandler,
302
+ getArrayFieldError,
303
+ buildArrayFieldPath,
271
304
  };
272
305
  }
@@ -251,6 +251,9 @@ export function usePersistedForm(options) {
251
251
  setGlobalError: baseForm.setGlobalError,
252
252
  markDirty: baseForm.markDirty,
253
253
  handleBlur: baseForm.handleBlur,
254
+ createBlurHandler: baseForm.createBlurHandler,
255
+ getArrayFieldError: baseForm.getArrayFieldError,
256
+ buildArrayFieldPath: baseForm.buildArrayFieldPath,
254
257
  // Override reset to also clear draft
255
258
  reset,
256
259
  // Persistence-specific methods
@@ -71,6 +71,8 @@ export { toastStore, type Toast as ToastType, type ToastInput } from './stores/t
71
71
  export { sidebarStore } from './stores/sidebar.svelte.js';
72
72
  export { themeStore, type ThemeMode } from './stores/theme.svelte.js';
73
73
  export { cn } from './utils.js';
74
+ export { parseDate, toISOString, formatDate, formatDateTime, isSameDay } from './utils/date.js';
75
+ export { generateId, computeDescribedBy } from './utils/form.js';
74
76
  export { tv, type VariantProps } from 'tailwind-variants';
75
77
  export { validateNonEmptyArray, validateRequired, validateOneOf, validateRange, validateProps, createValidator, type ValidationResult, } from './validation.js';
76
78
  export { perfStart, perfEnd, measure, measureAsync, getPerformanceEntries, clearPerformanceEntries, createPerfMonitor, type PerformanceMark, } from './performance.js';
package/dist/lib/index.js CHANGED
@@ -93,6 +93,10 @@ export { sidebarStore } from './stores/sidebar.svelte.js';
93
93
  export { themeStore } from './stores/theme.svelte.js';
94
94
  // Utilities
95
95
  export { cn } from './utils.js';
96
+ // Date utilities
97
+ export { parseDate, toISOString, formatDate, formatDateTime, isSameDay } from './utils/date.js';
98
+ // Form utilities
99
+ export { generateId, computeDescribedBy } from './utils/form.js';
96
100
  // Re-export tailwind-variants types for consumer convenience
97
101
  export { tv } from 'tailwind-variants';
98
102
  // Validation utilities (dev-only, tree-shaken in production)
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Date utility functions for form handling
3
+ *
4
+ * These utilities help with converting between Date objects and string formats,
5
+ * particularly useful for form data binding where backend APIs expect ISO strings.
6
+ */
7
+ /**
8
+ * Parse a date from various input formats into a Date object.
9
+ *
10
+ * @param input - A Date object, ISO string, or null/undefined
11
+ * @returns A Date object or null if input is invalid/empty
12
+ *
13
+ * @example
14
+ * parseDate('2024-03-15T10:30:00.000Z') // Date object
15
+ * parseDate(new Date()) // Same Date object
16
+ * parseDate(null) // null
17
+ * parseDate('invalid') // null
18
+ */
19
+ export declare function parseDate(input: Date | string | null | undefined): Date | null;
20
+ /**
21
+ * Convert a Date object to an ISO string.
22
+ *
23
+ * @param date - A Date object or null
24
+ * @returns ISO string representation or null if date is invalid/empty
25
+ *
26
+ * @example
27
+ * toISOString(new Date('2024-03-15T10:30:00.000Z')) // '2024-03-15T10:30:00.000Z'
28
+ * toISOString(null) // null
29
+ */
30
+ export declare function toISOString(date: Date | null | undefined): string | null;
31
+ /**
32
+ * Format a Date object to YYYY-MM-DD string format.
33
+ *
34
+ * @param date - A Date object or null
35
+ * @returns Date string in YYYY-MM-DD format or empty string if date is invalid/empty
36
+ *
37
+ * @example
38
+ * formatDate(new Date('2024-03-15T10:30:00.000Z')) // '2024-03-15'
39
+ * formatDate(null) // ''
40
+ */
41
+ export declare function formatDate(date: Date | null | undefined): string;
42
+ /**
43
+ * Format a Date object to YYYY-MM-DDTHH:MM string format (datetime-local input format).
44
+ *
45
+ * @param date - A Date object or null
46
+ * @returns Date string in datetime-local format or empty string if date is invalid/empty
47
+ *
48
+ * @example
49
+ * formatDateTime(new Date('2024-03-15T10:30:00.000Z')) // '2024-03-15T10:30'
50
+ * formatDateTime(null) // ''
51
+ */
52
+ export declare function formatDateTime(date: Date | null | undefined): string;
53
+ /**
54
+ * Check if two dates are the same day (ignoring time).
55
+ *
56
+ * @param date1 - First date to compare
57
+ * @param date2 - Second date to compare
58
+ * @returns true if both dates are the same day
59
+ *
60
+ * @example
61
+ * isSameDay(new Date('2024-03-15T10:00:00'), new Date('2024-03-15T22:00:00')) // true
62
+ * isSameDay(new Date('2024-03-15'), new Date('2024-03-16')) // false
63
+ */
64
+ export declare function isSameDay(date1: Date | null | undefined, date2: Date | null | undefined): boolean;