@classic-homes/theme-svelte 0.1.21 → 0.1.23
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 +10 -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
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { Popover, Calendar } from 'bits-ui';
|
|
3
|
+
import { tick } from 'svelte';
|
|
3
4
|
import { cn } from '../utils.js';
|
|
5
|
+
import { parseDate, toISOString } from '../utils/date.js';
|
|
6
|
+
import Label from './Label.svelte';
|
|
4
7
|
|
|
5
8
|
interface Props {
|
|
6
9
|
/** Current date/time value */
|
|
7
10
|
value?: Date | null;
|
|
11
|
+
/** Current value as ISO string (alternative to value prop for form binding) */
|
|
12
|
+
stringValue?: string | null;
|
|
8
13
|
/** Callback when value changes */
|
|
9
14
|
onValueChange?: (date: Date | null) => void;
|
|
15
|
+
/** Callback when stringValue changes */
|
|
16
|
+
onStringValueChange?: (value: string | null) => void;
|
|
10
17
|
/** Placeholder text */
|
|
11
18
|
placeholder?: string;
|
|
12
19
|
/** Whether the picker is disabled */
|
|
@@ -31,11 +38,17 @@
|
|
|
31
38
|
class?: string;
|
|
32
39
|
/** Show only date picker without time selection */
|
|
33
40
|
dateOnly?: boolean;
|
|
41
|
+
/** Label text for the picker */
|
|
42
|
+
label?: string;
|
|
43
|
+
/** Hint text displayed below the picker */
|
|
44
|
+
hint?: string;
|
|
34
45
|
}
|
|
35
46
|
|
|
36
47
|
let {
|
|
37
48
|
value = $bindable(null),
|
|
49
|
+
stringValue = $bindable(null),
|
|
38
50
|
onValueChange,
|
|
51
|
+
onStringValueChange,
|
|
39
52
|
placeholder,
|
|
40
53
|
disabled = false,
|
|
41
54
|
required = false,
|
|
@@ -48,8 +61,50 @@
|
|
|
48
61
|
error,
|
|
49
62
|
class: className,
|
|
50
63
|
dateOnly = false,
|
|
64
|
+
label,
|
|
65
|
+
hint,
|
|
51
66
|
}: Props = $props();
|
|
52
67
|
|
|
68
|
+
// Generate unique IDs for accessibility
|
|
69
|
+
// Using a random suffix ensures uniqueness if id is not provided
|
|
70
|
+
const randomSuffix = Math.random().toString(36).substring(2, 9);
|
|
71
|
+
const componentId = $derived(id || `datetimepicker-${randomSuffix}`);
|
|
72
|
+
const hintId = $derived(`${componentId}-hint`);
|
|
73
|
+
const errorId = $derived(`${componentId}-error`);
|
|
74
|
+
|
|
75
|
+
// Compute aria-describedby based on hint and error
|
|
76
|
+
const describedBy = $derived.by(() => {
|
|
77
|
+
const ids: string[] = [];
|
|
78
|
+
if (hint) ids.push(hintId);
|
|
79
|
+
if (error) ids.push(errorId);
|
|
80
|
+
return ids.length > 0 ? ids.join(' ') : undefined;
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// Sync stringValue -> value when stringValue changes externally
|
|
84
|
+
let lastStringValue = $state<string | null>(stringValue);
|
|
85
|
+
$effect(() => {
|
|
86
|
+
if (stringValue !== lastStringValue) {
|
|
87
|
+
lastStringValue = stringValue;
|
|
88
|
+
const parsed = parseDate(stringValue);
|
|
89
|
+
if (
|
|
90
|
+
parsed !== value &&
|
|
91
|
+
(parsed === null || value === null || parsed.getTime() !== value?.getTime())
|
|
92
|
+
) {
|
|
93
|
+
value = parsed;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// Sync value -> stringValue when value changes
|
|
99
|
+
$effect(() => {
|
|
100
|
+
const newStringValue = toISOString(value);
|
|
101
|
+
if (newStringValue !== stringValue) {
|
|
102
|
+
stringValue = newStringValue;
|
|
103
|
+
lastStringValue = newStringValue;
|
|
104
|
+
onStringValueChange?.(newStringValue);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
53
108
|
// Set default placeholder based on dateOnly mode
|
|
54
109
|
const defaultPlaceholder = $derived(dateOnly ? 'Select date...' : 'Select date and time...');
|
|
55
110
|
const effectivePlaceholder = $derived(placeholder ?? defaultPlaceholder);
|
|
@@ -153,7 +208,7 @@
|
|
|
153
208
|
);
|
|
154
209
|
}
|
|
155
210
|
|
|
156
|
-
function selectDate(day: number) {
|
|
211
|
+
async function selectDate(day: number) {
|
|
157
212
|
if (isDateDisabled(day)) return;
|
|
158
213
|
|
|
159
214
|
// Capture current view state before any reactive updates
|
|
@@ -178,11 +233,12 @@
|
|
|
178
233
|
isSelecting = true;
|
|
179
234
|
value = newDate;
|
|
180
235
|
onValueChange?.(newDate);
|
|
181
|
-
//
|
|
236
|
+
// Wait for effects to process before allowing external sync
|
|
237
|
+
await tick();
|
|
182
238
|
isSelecting = false;
|
|
183
239
|
}
|
|
184
240
|
|
|
185
|
-
function updateTime() {
|
|
241
|
+
async function updateTime() {
|
|
186
242
|
isSelecting = true;
|
|
187
243
|
if (!value) {
|
|
188
244
|
// If no date selected, use today
|
|
@@ -204,6 +260,8 @@
|
|
|
204
260
|
value = newDate;
|
|
205
261
|
onValueChange?.(newDate);
|
|
206
262
|
}
|
|
263
|
+
// Wait for effects to process before allowing external sync
|
|
264
|
+
await tick();
|
|
207
265
|
isSelecting = false;
|
|
208
266
|
}
|
|
209
267
|
|
|
@@ -246,208 +304,239 @@
|
|
|
246
304
|
];
|
|
247
305
|
|
|
248
306
|
const dayNames = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'];
|
|
307
|
+
|
|
308
|
+
// Determine if we need a container wrapper
|
|
309
|
+
const hasWrapper = $derived(!!label || !!hint || !!error);
|
|
249
310
|
</script>
|
|
250
311
|
|
|
251
|
-
|
|
252
|
-
<Popover.
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
{
|
|
265
|
-
</span>
|
|
266
|
-
<svg
|
|
267
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
268
|
-
width="16"
|
|
269
|
-
height="16"
|
|
270
|
-
viewBox="0 0 24 24"
|
|
271
|
-
fill="none"
|
|
272
|
-
stroke="currentColor"
|
|
273
|
-
stroke-width="2"
|
|
274
|
-
stroke-linecap="round"
|
|
275
|
-
stroke-linejoin="round"
|
|
276
|
-
class="h-4 w-4 opacity-50 shrink-0 ml-2"
|
|
277
|
-
>
|
|
278
|
-
<rect width="18" height="18" x="3" y="4" rx="2" ry="2" />
|
|
279
|
-
<line x1="16" x2="16" y1="2" y2="6" />
|
|
280
|
-
<line x1="8" x2="8" y1="2" y2="6" />
|
|
281
|
-
<line x1="3" x2="21" y1="10" y2="10" />
|
|
282
|
-
<path d="M8 14h.01" />
|
|
283
|
-
<path d="M12 14h.01" />
|
|
284
|
-
<path d="M16 14h.01" />
|
|
285
|
-
<path d="M8 18h.01" />
|
|
286
|
-
<path d="M12 18h.01" />
|
|
287
|
-
<path d="M16 18h.01" />
|
|
288
|
-
</svg>
|
|
289
|
-
</Popover.Trigger>
|
|
290
|
-
|
|
291
|
-
<Popover.Portal>
|
|
292
|
-
<Popover.Content
|
|
293
|
-
class="z-50 w-auto p-0 rounded-md border bg-popover text-popover-foreground shadow-md"
|
|
294
|
-
sideOffset={4}
|
|
295
|
-
align="start"
|
|
312
|
+
{#snippet pickerContent()}
|
|
313
|
+
<Popover.Root bind:open>
|
|
314
|
+
<Popover.Trigger
|
|
315
|
+
id={componentId}
|
|
316
|
+
{disabled}
|
|
317
|
+
class={cn(
|
|
318
|
+
'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 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
|
|
319
|
+
!value && 'text-muted-foreground',
|
|
320
|
+
error && 'border-destructive focus:ring-destructive',
|
|
321
|
+
className
|
|
322
|
+
)}
|
|
323
|
+
aria-invalid={error ? 'true' : undefined}
|
|
324
|
+
aria-describedby={describedBy}
|
|
325
|
+
aria-label={!label ? effectivePlaceholder : undefined}
|
|
296
326
|
>
|
|
297
|
-
<
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
stroke-linejoin="round"
|
|
327
|
+
<span class="truncate">
|
|
328
|
+
{displayValue || effectivePlaceholder}
|
|
329
|
+
</span>
|
|
330
|
+
<svg
|
|
331
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
332
|
+
width="16"
|
|
333
|
+
height="16"
|
|
334
|
+
viewBox="0 0 24 24"
|
|
335
|
+
fill="none"
|
|
336
|
+
stroke="currentColor"
|
|
337
|
+
stroke-width="2"
|
|
338
|
+
stroke-linecap="round"
|
|
339
|
+
stroke-linejoin="round"
|
|
340
|
+
class="h-4 w-4 opacity-50 shrink-0 ml-2"
|
|
341
|
+
>
|
|
342
|
+
<rect width="18" height="18" x="3" y="4" rx="2" ry="2" />
|
|
343
|
+
<line x1="16" x2="16" y1="2" y2="6" />
|
|
344
|
+
<line x1="8" x2="8" y1="2" y2="6" />
|
|
345
|
+
<line x1="3" x2="21" y1="10" y2="10" />
|
|
346
|
+
<path d="M8 14h.01" />
|
|
347
|
+
<path d="M12 14h.01" />
|
|
348
|
+
<path d="M16 14h.01" />
|
|
349
|
+
<path d="M8 18h.01" />
|
|
350
|
+
<path d="M12 18h.01" />
|
|
351
|
+
<path d="M16 18h.01" />
|
|
352
|
+
</svg>
|
|
353
|
+
</Popover.Trigger>
|
|
354
|
+
|
|
355
|
+
<Popover.Portal>
|
|
356
|
+
<Popover.Content
|
|
357
|
+
class="z-50 w-auto p-0 rounded-md border bg-popover text-popover-foreground shadow-md"
|
|
358
|
+
sideOffset={4}
|
|
359
|
+
align="start"
|
|
360
|
+
>
|
|
361
|
+
<div class="p-3">
|
|
362
|
+
<!-- Calendar header -->
|
|
363
|
+
<div class="flex items-center justify-between mb-2">
|
|
364
|
+
<button
|
|
365
|
+
type="button"
|
|
366
|
+
onclick={prevMonth}
|
|
367
|
+
class="h-7 w-7 inline-flex items-center justify-center rounded-md bg-transparent transition-colors duration-150 hover:bg-accent hover:text-accent-foreground"
|
|
368
|
+
aria-label="Previous month"
|
|
340
369
|
>
|
|
341
|
-
<
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
370
|
+
<svg
|
|
371
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
372
|
+
width="16"
|
|
373
|
+
height="16"
|
|
374
|
+
viewBox="0 0 24 24"
|
|
375
|
+
fill="none"
|
|
376
|
+
stroke="currentColor"
|
|
377
|
+
stroke-width="2"
|
|
378
|
+
stroke-linecap="round"
|
|
379
|
+
stroke-linejoin="round"
|
|
380
|
+
>
|
|
381
|
+
<path d="m15 18-6-6 6-6" />
|
|
382
|
+
</svg>
|
|
383
|
+
</button>
|
|
384
|
+
<span class="text-sm font-medium">
|
|
385
|
+
{monthNames[viewMonth]}
|
|
386
|
+
{viewYear}
|
|
387
|
+
</span>
|
|
388
|
+
<button
|
|
389
|
+
type="button"
|
|
390
|
+
onclick={nextMonth}
|
|
391
|
+
class="h-7 w-7 inline-flex items-center justify-center rounded-md bg-transparent transition-colors duration-150 hover:bg-accent hover:text-accent-foreground"
|
|
392
|
+
aria-label="Next month"
|
|
351
393
|
>
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
{:else}
|
|
363
|
-
<button
|
|
364
|
-
type="button"
|
|
365
|
-
onclick={() => selectDate(day)}
|
|
366
|
-
disabled={isDateDisabled(day)}
|
|
367
|
-
class={cn(
|
|
368
|
-
'h-8 w-8 inline-flex items-center justify-center rounded-md text-sm bg-transparent transition-colors duration-150',
|
|
369
|
-
'hover:bg-accent hover:text-accent-foreground',
|
|
370
|
-
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
|
|
371
|
-
'disabled:pointer-events-none disabled:opacity-50',
|
|
372
|
-
isDateSelected(day) &&
|
|
373
|
-
'bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground'
|
|
374
|
-
)}
|
|
394
|
+
<svg
|
|
395
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
396
|
+
width="16"
|
|
397
|
+
height="16"
|
|
398
|
+
viewBox="0 0 24 24"
|
|
399
|
+
fill="none"
|
|
400
|
+
stroke="currentColor"
|
|
401
|
+
stroke-width="2"
|
|
402
|
+
stroke-linecap="round"
|
|
403
|
+
stroke-linejoin="round"
|
|
375
404
|
>
|
|
376
|
-
|
|
377
|
-
</
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
</div>
|
|
405
|
+
<path d="m9 18 6-6-6-6" />
|
|
406
|
+
</svg>
|
|
407
|
+
</button>
|
|
408
|
+
</div>
|
|
381
409
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
<select
|
|
388
|
-
bind:value={hour}
|
|
389
|
-
onchange={updateTime}
|
|
390
|
-
class="h-8 rounded-md border border-input bg-background px-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
|
|
391
|
-
>
|
|
392
|
-
{#each hourOptions as h}
|
|
393
|
-
<option value={timeFormat === '12h' ? h : h}>
|
|
394
|
-
{String(h).padStart(2, '0')}
|
|
395
|
-
</option>
|
|
396
|
-
{/each}
|
|
397
|
-
</select>
|
|
398
|
-
<span class="text-muted-foreground">:</span>
|
|
399
|
-
<select
|
|
400
|
-
bind:value={minute}
|
|
401
|
-
onchange={updateTime}
|
|
402
|
-
class="h-8 rounded-md border border-input bg-background px-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
|
|
410
|
+
<!-- Day names -->
|
|
411
|
+
<div class="grid grid-cols-7 gap-1 mb-1">
|
|
412
|
+
{#each dayNames as dayName}
|
|
413
|
+
<div
|
|
414
|
+
class="h-8 w-8 flex items-center justify-center text-xs text-muted-foreground font-medium"
|
|
403
415
|
>
|
|
404
|
-
{
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
416
|
+
{dayName}
|
|
417
|
+
</div>
|
|
418
|
+
{/each}
|
|
419
|
+
</div>
|
|
420
|
+
|
|
421
|
+
<!-- Calendar days -->
|
|
422
|
+
<div class="grid grid-cols-7 gap-1">
|
|
423
|
+
{#each calendarDays as day}
|
|
424
|
+
{#if day === null}
|
|
425
|
+
<div class="h-8 w-8"></div>
|
|
426
|
+
{:else}
|
|
427
|
+
<button
|
|
428
|
+
type="button"
|
|
429
|
+
onclick={() => selectDate(day)}
|
|
430
|
+
disabled={isDateDisabled(day)}
|
|
431
|
+
class={cn(
|
|
432
|
+
'h-8 w-8 inline-flex items-center justify-center rounded-md text-sm bg-transparent transition-colors duration-150',
|
|
433
|
+
'hover:bg-accent hover:text-accent-foreground',
|
|
434
|
+
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
|
|
435
|
+
'disabled:pointer-events-none disabled:opacity-50',
|
|
436
|
+
isDateSelected(day) &&
|
|
437
|
+
'bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground'
|
|
438
|
+
)}
|
|
439
|
+
>
|
|
440
|
+
{day}
|
|
441
|
+
</button>
|
|
442
|
+
{/if}
|
|
443
|
+
{/each}
|
|
444
|
+
</div>
|
|
445
|
+
|
|
446
|
+
<!-- Time selector (hidden in dateOnly mode) -->
|
|
447
|
+
{#if !dateOnly}
|
|
448
|
+
<div class="border-t mt-3 pt-3">
|
|
449
|
+
<div class="flex items-center gap-2">
|
|
450
|
+
<span class="text-sm text-muted-foreground">Time:</span>
|
|
409
451
|
<select
|
|
410
|
-
bind:value={
|
|
452
|
+
bind:value={hour}
|
|
411
453
|
onchange={updateTime}
|
|
412
454
|
class="h-8 rounded-md border border-input bg-background px-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
|
|
413
455
|
>
|
|
414
|
-
|
|
415
|
-
|
|
456
|
+
{#each hourOptions as h}
|
|
457
|
+
<option value={timeFormat === '12h' ? h : h}>
|
|
458
|
+
{String(h).padStart(2, '0')}
|
|
459
|
+
</option>
|
|
460
|
+
{/each}
|
|
416
461
|
</select>
|
|
417
|
-
|
|
462
|
+
<span class="text-muted-foreground">:</span>
|
|
463
|
+
<select
|
|
464
|
+
bind:value={minute}
|
|
465
|
+
onchange={updateTime}
|
|
466
|
+
class="h-8 rounded-md border border-input bg-background px-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
|
|
467
|
+
>
|
|
468
|
+
{#each minuteOptions as m}
|
|
469
|
+
<option value={m}>{String(m).padStart(2, '0')}</option>
|
|
470
|
+
{/each}
|
|
471
|
+
</select>
|
|
472
|
+
{#if timeFormat === '12h'}
|
|
473
|
+
<select
|
|
474
|
+
bind:value={period}
|
|
475
|
+
onchange={updateTime}
|
|
476
|
+
class="h-8 rounded-md border border-input bg-background px-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
|
|
477
|
+
>
|
|
478
|
+
<option value="AM">AM</option>
|
|
479
|
+
<option value="PM">PM</option>
|
|
480
|
+
</select>
|
|
481
|
+
{/if}
|
|
482
|
+
</div>
|
|
418
483
|
</div>
|
|
484
|
+
{/if}
|
|
485
|
+
|
|
486
|
+
<!-- Actions -->
|
|
487
|
+
<div class="border-t mt-3 pt-3 flex justify-between">
|
|
488
|
+
<button
|
|
489
|
+
type="button"
|
|
490
|
+
onclick={clearValue}
|
|
491
|
+
class="text-sm text-muted-foreground hover:text-foreground"
|
|
492
|
+
>
|
|
493
|
+
Clear
|
|
494
|
+
</button>
|
|
495
|
+
<button
|
|
496
|
+
type="button"
|
|
497
|
+
onclick={() => (open = false)}
|
|
498
|
+
class="text-sm font-medium text-primary hover:underline"
|
|
499
|
+
>
|
|
500
|
+
Done
|
|
501
|
+
</button>
|
|
419
502
|
</div>
|
|
420
|
-
{/if}
|
|
421
|
-
|
|
422
|
-
<!-- Actions -->
|
|
423
|
-
<div class="border-t mt-3 pt-3 flex justify-between">
|
|
424
|
-
<button
|
|
425
|
-
type="button"
|
|
426
|
-
onclick={clearValue}
|
|
427
|
-
class="text-sm text-muted-foreground hover:text-foreground"
|
|
428
|
-
>
|
|
429
|
-
Clear
|
|
430
|
-
</button>
|
|
431
|
-
<button
|
|
432
|
-
type="button"
|
|
433
|
-
onclick={() => (open = false)}
|
|
434
|
-
class="text-sm font-medium text-primary hover:underline"
|
|
435
|
-
>
|
|
436
|
-
Done
|
|
437
|
-
</button>
|
|
438
503
|
</div>
|
|
439
|
-
</
|
|
440
|
-
</Popover.
|
|
441
|
-
</Popover.
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
{
|
|
446
|
-
|
|
447
|
-
{/
|
|
448
|
-
|
|
449
|
-
{#if
|
|
450
|
-
<
|
|
451
|
-
{
|
|
452
|
-
|
|
504
|
+
</Popover.Content>
|
|
505
|
+
</Popover.Portal>
|
|
506
|
+
</Popover.Root>
|
|
507
|
+
|
|
508
|
+
<!-- Hidden input for form submission -->
|
|
509
|
+
{#if name && value}
|
|
510
|
+
<input type="hidden" {name} value={value.toISOString()} />
|
|
511
|
+
{/if}
|
|
512
|
+
{/snippet}
|
|
513
|
+
|
|
514
|
+
{#if hasWrapper}
|
|
515
|
+
<div class="space-y-2">
|
|
516
|
+
{#if label}
|
|
517
|
+
<Label for={componentId} {disabled}>
|
|
518
|
+
{label}
|
|
519
|
+
{#if required}
|
|
520
|
+
<span class="text-destructive ml-0.5" aria-hidden="true">*</span>
|
|
521
|
+
<span class="sr-only">(required)</span>
|
|
522
|
+
{/if}
|
|
523
|
+
</Label>
|
|
524
|
+
{/if}
|
|
525
|
+
|
|
526
|
+
{@render pickerContent()}
|
|
527
|
+
|
|
528
|
+
{#if hint && !error}
|
|
529
|
+
<p id={hintId} class="text-sm text-muted-foreground">
|
|
530
|
+
{hint}
|
|
531
|
+
</p>
|
|
532
|
+
{/if}
|
|
533
|
+
|
|
534
|
+
{#if error}
|
|
535
|
+
<p id={errorId} class="text-sm text-destructive" role="alert" aria-live="polite">
|
|
536
|
+
{error}
|
|
537
|
+
</p>
|
|
538
|
+
{/if}
|
|
539
|
+
</div>
|
|
540
|
+
{:else}
|
|
541
|
+
{@render pickerContent()}
|
|
453
542
|
{/if}
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
interface Props {
|
|
2
2
|
/** Current date/time value */
|
|
3
3
|
value?: Date | null;
|
|
4
|
+
/** Current value as ISO string (alternative to value prop for form binding) */
|
|
5
|
+
stringValue?: string | null;
|
|
4
6
|
/** Callback when value changes */
|
|
5
7
|
onValueChange?: (date: Date | null) => void;
|
|
8
|
+
/** Callback when stringValue changes */
|
|
9
|
+
onStringValueChange?: (value: string | null) => void;
|
|
6
10
|
/** Placeholder text */
|
|
7
11
|
placeholder?: string;
|
|
8
12
|
/** Whether the picker is disabled */
|
|
@@ -27,7 +31,11 @@ interface Props {
|
|
|
27
31
|
class?: string;
|
|
28
32
|
/** Show only date picker without time selection */
|
|
29
33
|
dateOnly?: boolean;
|
|
34
|
+
/** Label text for the picker */
|
|
35
|
+
label?: string;
|
|
36
|
+
/** Hint text displayed below the picker */
|
|
37
|
+
hint?: string;
|
|
30
38
|
}
|
|
31
|
-
declare const DateTimePicker: import("svelte").Component<Props, {}, "value">;
|
|
39
|
+
declare const DateTimePicker: import("svelte").Component<Props, {}, "value" | "stringValue">;
|
|
32
40
|
type DateTimePicker = ReturnType<typeof DateTimePicker>;
|
|
33
41
|
export default DateTimePicker;
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
{autocomplete}
|
|
54
54
|
{inputmode}
|
|
55
55
|
class={cn(
|
|
56
|
-
'flex h-
|
|
56
|
+
'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
|
|
57
57
|
className
|
|
58
58
|
)}
|
|
59
59
|
oninput={handleInput}
|