@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.
- package/dist/lib/components/Combobox.svelte +194 -76
- package/dist/lib/components/Combobox.svelte.d.ts +4 -0
- package/dist/lib/components/DateTimePicker.svelte +280 -191
- package/dist/lib/components/DateTimePicker.svelte.d.ts +9 -1
- package/dist/lib/components/Input.svelte +1 -1
- package/dist/lib/components/MultiSelect.svelte +269 -108
- package/dist/lib/components/MultiSelect.svelte.d.ts +4 -0
- package/dist/lib/components/NumberInput.svelte +198 -78
- package/dist/lib/components/NumberInput.svelte.d.ts +4 -0
- package/dist/lib/components/OTPInput.svelte +125 -35
- package/dist/lib/components/OTPInput.svelte.d.ts +8 -0
- package/dist/lib/components/RadioGroup.svelte +31 -11
- package/dist/lib/components/RadioGroup.svelte.d.ts +2 -0
- package/dist/lib/components/Select.svelte +249 -103
- package/dist/lib/components/Select.svelte.d.ts +2 -0
- package/dist/lib/components/Slider.svelte +32 -2
- package/dist/lib/components/Slider.svelte.d.ts +4 -0
- package/dist/lib/composables/useForm.svelte.d.ts +6 -0
- package/dist/lib/composables/useForm.svelte.js +33 -0
- package/dist/lib/composables/usePersistedForm.svelte.js +3 -0
- package/dist/lib/index.d.ts +2 -0
- package/dist/lib/index.js +4 -0
- package/dist/lib/utils/date.d.ts +64 -0
- package/dist/lib/utils/date.js +106 -0
- package/dist/lib/utils/form.d.ts +22 -0
- package/dist/lib/utils/form.js +31 -0
- package/package.json +1 -1
|
@@ -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
|
|
100
|
-
<
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
<
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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={
|
|
164
|
-
label={
|
|
165
|
-
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
|
|
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
|
-
{
|
|
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
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
<
|
|
120
|
+
<Label for={undefined} {disabled}>
|
|
104
121
|
{label}
|
|
105
|
-
</
|
|
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
|
package/dist/lib/index.d.ts
CHANGED
|
@@ -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;
|