@classic-homes/theme-svelte 0.1.3 → 0.1.5

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 (41) hide show
  1. package/dist/lib/components/Combobox.svelte +187 -0
  2. package/dist/lib/components/Combobox.svelte.d.ts +38 -0
  3. package/dist/lib/components/DateTimePicker.svelte +415 -0
  4. package/dist/lib/components/DateTimePicker.svelte.d.ts +31 -0
  5. package/dist/lib/components/MultiSelect.svelte +244 -0
  6. package/dist/lib/components/MultiSelect.svelte.d.ts +40 -0
  7. package/dist/lib/components/NumberInput.svelte +205 -0
  8. package/dist/lib/components/NumberInput.svelte.d.ts +33 -0
  9. package/dist/lib/components/OTPInput.svelte +213 -0
  10. package/dist/lib/components/OTPInput.svelte.d.ts +23 -0
  11. package/dist/lib/components/RadioGroup.svelte +124 -0
  12. package/dist/lib/components/RadioGroup.svelte.d.ts +31 -0
  13. package/dist/lib/components/Signature.svelte +1070 -0
  14. package/dist/lib/components/Signature.svelte.d.ts +74 -0
  15. package/dist/lib/components/Slider.svelte +136 -0
  16. package/dist/lib/components/Slider.svelte.d.ts +30 -0
  17. package/dist/lib/components/layout/AppShell.svelte +1 -1
  18. package/dist/lib/components/layout/DashboardLayout.svelte +63 -16
  19. package/dist/lib/components/layout/DashboardLayout.svelte.d.ts +12 -10
  20. package/dist/lib/components/layout/QuickLinks.svelte +49 -29
  21. package/dist/lib/components/layout/QuickLinks.svelte.d.ts +4 -2
  22. package/dist/lib/components/layout/Sidebar.svelte +345 -86
  23. package/dist/lib/components/layout/Sidebar.svelte.d.ts +12 -0
  24. package/dist/lib/components/layout/sidebar/SidebarFlyout.svelte +182 -0
  25. package/dist/lib/components/layout/sidebar/SidebarFlyout.svelte.d.ts +18 -0
  26. package/dist/lib/components/layout/sidebar/SidebarNavItem.svelte +369 -0
  27. package/dist/lib/components/layout/sidebar/SidebarNavItem.svelte.d.ts +25 -0
  28. package/dist/lib/components/layout/sidebar/SidebarSearch.svelte +121 -0
  29. package/dist/lib/components/layout/sidebar/SidebarSearch.svelte.d.ts +17 -0
  30. package/dist/lib/components/layout/sidebar/SidebarSection.svelte +144 -0
  31. package/dist/lib/components/layout/sidebar/SidebarSection.svelte.d.ts +25 -0
  32. package/dist/lib/components/layout/sidebar/index.d.ts +10 -0
  33. package/dist/lib/components/layout/sidebar/index.js +10 -0
  34. package/dist/lib/index.d.ts +9 -1
  35. package/dist/lib/index.js +8 -0
  36. package/dist/lib/schemas/auth.d.ts +6 -6
  37. package/dist/lib/stores/sidebar.svelte.d.ts +54 -0
  38. package/dist/lib/stores/sidebar.svelte.js +171 -1
  39. package/dist/lib/types/components.d.ts +105 -0
  40. package/dist/lib/types/layout.d.ts +32 -2
  41. package/package.json +1 -1
@@ -0,0 +1,187 @@
1
+ <script lang="ts">
2
+ import { Combobox as ComboboxPrimitive } from 'bits-ui';
3
+ import { cn } from '../utils.js';
4
+ import Spinner from './Spinner.svelte';
5
+
6
+ export interface ComboboxOption {
7
+ value: string;
8
+ label: string;
9
+ disabled?: boolean;
10
+ }
11
+
12
+ interface Props {
13
+ /** Current selected value */
14
+ value?: string;
15
+ /** Callback when value changes */
16
+ onValueChange?: (value: string) => void;
17
+ /** Array of selectable options */
18
+ options: ComboboxOption[];
19
+ /** Placeholder text for the input */
20
+ placeholder?: string;
21
+ /** Message to display when no results found */
22
+ emptyMessage?: string;
23
+ /** Whether the combobox is disabled */
24
+ disabled?: boolean;
25
+ /** Whether selection is required */
26
+ required?: boolean;
27
+ /** Name attribute for form submission */
28
+ name?: string;
29
+ /** Element ID */
30
+ id?: string;
31
+ /** Error message to display */
32
+ error?: string;
33
+ /** Whether async data is loading */
34
+ loading?: boolean;
35
+ /** Callback when search query changes (for async loading) */
36
+ onSearch?: (query: string) => void;
37
+ /** Debounce delay in ms for search callback (default: 300) */
38
+ debounceMs?: number;
39
+ /** Additional class for the trigger */
40
+ class?: string;
41
+ }
42
+
43
+ let {
44
+ value = $bindable(''),
45
+ onValueChange,
46
+ options,
47
+ placeholder = 'Select an option...',
48
+ emptyMessage = 'No results found.',
49
+ disabled = false,
50
+ required = false,
51
+ name,
52
+ id,
53
+ error,
54
+ loading = false,
55
+ onSearch,
56
+ debounceMs = 300,
57
+ class: className,
58
+ }: Props = $props();
59
+
60
+ let searchQuery = $state('');
61
+ let debounceTimer: ReturnType<typeof setTimeout> | null = null;
62
+ let open = $state(false);
63
+
64
+ // Find selected option label
65
+ const selectedLabel = $derived(options.find((opt) => opt.value === value)?.label);
66
+
67
+ // Always filter options locally based on search query
68
+ const filteredOptions = $derived.by(() => {
69
+ if (!searchQuery) return options;
70
+ const query = searchQuery.toLowerCase();
71
+ return options.filter((opt) => opt.label.toLowerCase().includes(query));
72
+ });
73
+
74
+ function handleSearchInput(e: Event) {
75
+ const target = e.target as HTMLInputElement;
76
+ searchQuery = target.value;
77
+
78
+ if (onSearch) {
79
+ // Debounce the search callback
80
+ if (debounceTimer) clearTimeout(debounceTimer);
81
+ debounceTimer = setTimeout(() => {
82
+ onSearch(searchQuery);
83
+ }, debounceMs);
84
+ }
85
+ }
86
+
87
+ function handleValueChange(newValue: string | undefined) {
88
+ if (newValue !== undefined) {
89
+ value = newValue;
90
+ onValueChange?.(newValue);
91
+ }
92
+ }
93
+
94
+ function handleOpenChange(isOpen: boolean) {
95
+ open = isOpen;
96
+ if (!isOpen) {
97
+ searchQuery = '';
98
+ }
99
+ }
100
+
101
+ // Cleanup debounce timer on unmount
102
+ $effect(() => {
103
+ return () => {
104
+ if (debounceTimer) clearTimeout(debounceTimer);
105
+ };
106
+ });
107
+ </script>
108
+
109
+ <ComboboxPrimitive.Root
110
+ type="single"
111
+ {disabled}
112
+ {required}
113
+ {name}
114
+ bind:open
115
+ onOpenChange={handleOpenChange}
116
+ onValueChange={handleValueChange}
117
+ {value}
118
+ >
119
+ <div class="relative">
120
+ <ComboboxPrimitive.Input
121
+ {id}
122
+ placeholder={selectedLabel || placeholder}
123
+ oninput={handleSearchInput}
124
+ onfocus={() => (open = true)}
125
+ onclick={() => (open = true)}
126
+ class={cn(
127
+ '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',
128
+ error && 'border-destructive focus:ring-destructive',
129
+ className
130
+ )}
131
+ aria-invalid={error ? 'true' : undefined}
132
+ />
133
+ </div>
134
+
135
+ <ComboboxPrimitive.Content
136
+ class="relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md 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"
137
+ sideOffset={4}
138
+ >
139
+ <div class="p-1 max-h-[300px] overflow-y-auto">
140
+ {#if loading}
141
+ <div class="flex items-center justify-center py-6">
142
+ <Spinner size="sm" />
143
+ <span class="ml-2 text-sm text-muted-foreground">Loading...</span>
144
+ </div>
145
+ {:else if filteredOptions.length === 0}
146
+ <div class="py-6 text-center text-sm text-muted-foreground">
147
+ {emptyMessage}
148
+ </div>
149
+ {:else}
150
+ {#each filteredOptions as option}
151
+ <ComboboxPrimitive.Item
152
+ value={option.value}
153
+ label={option.label}
154
+ disabled={option.disabled}
155
+ 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"
156
+ >
157
+ {#if option.value === value}
158
+ <span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
159
+ <svg
160
+ xmlns="http://www.w3.org/2000/svg"
161
+ width="16"
162
+ height="16"
163
+ viewBox="0 0 24 24"
164
+ fill="none"
165
+ stroke="currentColor"
166
+ stroke-width="2"
167
+ stroke-linecap="round"
168
+ stroke-linejoin="round"
169
+ class="h-4 w-4"
170
+ >
171
+ <polyline points="20 6 9 17 4 12" />
172
+ </svg>
173
+ </span>
174
+ {/if}
175
+ {option.label}
176
+ </ComboboxPrimitive.Item>
177
+ {/each}
178
+ {/if}
179
+ </div>
180
+ </ComboboxPrimitive.Content>
181
+ </ComboboxPrimitive.Root>
182
+
183
+ {#if error}
184
+ <p class="text-sm text-destructive mt-1.5" role="alert">
185
+ {error}
186
+ </p>
187
+ {/if}
@@ -0,0 +1,38 @@
1
+ export interface ComboboxOption {
2
+ value: string;
3
+ label: string;
4
+ disabled?: boolean;
5
+ }
6
+ interface Props {
7
+ /** Current selected value */
8
+ value?: string;
9
+ /** Callback when value changes */
10
+ onValueChange?: (value: string) => void;
11
+ /** Array of selectable options */
12
+ options: ComboboxOption[];
13
+ /** Placeholder text for the input */
14
+ placeholder?: string;
15
+ /** Message to display when no results found */
16
+ emptyMessage?: string;
17
+ /** Whether the combobox is disabled */
18
+ disabled?: boolean;
19
+ /** Whether selection is required */
20
+ required?: boolean;
21
+ /** Name attribute for form submission */
22
+ name?: string;
23
+ /** Element ID */
24
+ id?: string;
25
+ /** Error message to display */
26
+ error?: string;
27
+ /** Whether async data is loading */
28
+ loading?: boolean;
29
+ /** Callback when search query changes (for async loading) */
30
+ onSearch?: (query: string) => void;
31
+ /** Debounce delay in ms for search callback (default: 300) */
32
+ debounceMs?: number;
33
+ /** Additional class for the trigger */
34
+ class?: string;
35
+ }
36
+ declare const Combobox: import("svelte").Component<Props, {}, "value">;
37
+ type Combobox = ReturnType<typeof Combobox>;
38
+ export default Combobox;
@@ -0,0 +1,415 @@
1
+ <script lang="ts">
2
+ import { Popover, Calendar } from 'bits-ui';
3
+ import { cn } from '../utils.js';
4
+
5
+ interface Props {
6
+ /** Current date/time value */
7
+ value?: Date | null;
8
+ /** Callback when value changes */
9
+ onValueChange?: (date: Date | null) => void;
10
+ /** Placeholder text */
11
+ placeholder?: string;
12
+ /** Whether the picker is disabled */
13
+ disabled?: boolean;
14
+ /** Whether selection is required */
15
+ required?: boolean;
16
+ /** Minimum selectable date */
17
+ min?: Date;
18
+ /** Maximum selectable date */
19
+ max?: Date;
20
+ /** Time format (12h or 24h) */
21
+ timeFormat?: '12h' | '24h';
22
+ /** Minute step interval (default: 15) */
23
+ minuteStep?: number;
24
+ /** Name attribute for form submission */
25
+ name?: string;
26
+ /** Element ID */
27
+ id?: string;
28
+ /** Error message to display */
29
+ error?: string;
30
+ /** Additional class for the trigger */
31
+ class?: string;
32
+ }
33
+
34
+ let {
35
+ value = $bindable(null),
36
+ onValueChange,
37
+ placeholder = 'Select date and time...',
38
+ disabled = false,
39
+ required = false,
40
+ min,
41
+ max,
42
+ timeFormat = '12h',
43
+ minuteStep = 15,
44
+ name,
45
+ id,
46
+ error,
47
+ class: className,
48
+ }: Props = $props();
49
+
50
+ let open = $state(false);
51
+
52
+ // Calendar state - initialize with current date
53
+ let viewMonth = $state(new Date().getMonth());
54
+ let viewYear = $state(new Date().getFullYear());
55
+
56
+ // Time state
57
+ let hour = $state(12);
58
+ let minute = $state(0);
59
+ let period = $state<'AM' | 'PM'>('PM');
60
+
61
+ // Sync state when value changes externally
62
+ $effect(() => {
63
+ if (value) {
64
+ hour = value.getHours();
65
+ minute = value.getMinutes();
66
+ period = value.getHours() >= 12 ? 'PM' : 'AM';
67
+ viewMonth = value.getMonth();
68
+ viewYear = value.getFullYear();
69
+ }
70
+ });
71
+
72
+ // Format display value
73
+ const displayValue = $derived.by(() => {
74
+ if (!value) return '';
75
+ const dateStr = value.toLocaleDateString('en-US', {
76
+ month: 'short',
77
+ day: 'numeric',
78
+ year: 'numeric',
79
+ });
80
+ const timeStr = value.toLocaleTimeString('en-US', {
81
+ hour: 'numeric',
82
+ minute: '2-digit',
83
+ hour12: timeFormat === '12h',
84
+ });
85
+ return `${dateStr} at ${timeStr}`;
86
+ });
87
+
88
+ // Generate calendar days for current month
89
+ const calendarDays = $derived.by(() => {
90
+ const firstDay = new Date(viewYear, viewMonth, 1);
91
+ const lastDay = new Date(viewYear, viewMonth + 1, 0);
92
+ const daysInMonth = lastDay.getDate();
93
+ const startDayOfWeek = firstDay.getDay();
94
+
95
+ const days: (number | null)[] = [];
96
+
97
+ // Add empty slots for days before the 1st
98
+ for (let i = 0; i < startDayOfWeek; i++) {
99
+ days.push(null);
100
+ }
101
+
102
+ // Add days of the month
103
+ for (let i = 1; i <= daysInMonth; i++) {
104
+ days.push(i);
105
+ }
106
+
107
+ return days;
108
+ });
109
+
110
+ // Generate time options
111
+ const hourOptions = $derived(
112
+ timeFormat === '12h'
113
+ ? Array.from({ length: 12 }, (_, i) => (i === 0 ? 12 : i))
114
+ : Array.from({ length: 24 }, (_, i) => i)
115
+ );
116
+
117
+ const minuteOptions = $derived(Array.from({ length: 60 / minuteStep }, (_, i) => i * minuteStep));
118
+
119
+ function isDateDisabled(day: number): boolean {
120
+ const date = new Date(viewYear, viewMonth, day);
121
+ if (min && date < new Date(min.getFullYear(), min.getMonth(), min.getDate())) {
122
+ return true;
123
+ }
124
+ if (max && date > new Date(max.getFullYear(), max.getMonth(), max.getDate())) {
125
+ return true;
126
+ }
127
+ return false;
128
+ }
129
+
130
+ function isDateSelected(day: number): boolean {
131
+ if (!value) return false;
132
+ return (
133
+ value.getDate() === day && value.getMonth() === viewMonth && value.getFullYear() === viewYear
134
+ );
135
+ }
136
+
137
+ function selectDate(day: number) {
138
+ if (isDateDisabled(day)) return;
139
+
140
+ const newDate = new Date(viewYear, viewMonth, day);
141
+
142
+ // Preserve or set time
143
+ let h = hour;
144
+ if (timeFormat === '12h') {
145
+ h = period === 'PM' ? (hour === 12 ? 12 : hour + 12) : hour === 12 ? 0 : hour;
146
+ }
147
+ newDate.setHours(h, minute, 0, 0);
148
+
149
+ value = newDate;
150
+ onValueChange?.(newDate);
151
+ }
152
+
153
+ function updateTime() {
154
+ if (!value) {
155
+ // If no date selected, use today
156
+ const today = new Date();
157
+ let h = hour;
158
+ if (timeFormat === '12h') {
159
+ h = period === 'PM' ? (hour === 12 ? 12 : hour + 12) : hour === 12 ? 0 : hour;
160
+ }
161
+ today.setHours(h, minute, 0, 0);
162
+ value = today;
163
+ onValueChange?.(today);
164
+ } else {
165
+ const newDate = new Date(value);
166
+ let h = hour;
167
+ if (timeFormat === '12h') {
168
+ h = period === 'PM' ? (hour === 12 ? 12 : hour + 12) : hour === 12 ? 0 : hour;
169
+ }
170
+ newDate.setHours(h, minute, 0, 0);
171
+ value = newDate;
172
+ onValueChange?.(newDate);
173
+ }
174
+ }
175
+
176
+ function prevMonth() {
177
+ if (viewMonth === 0) {
178
+ viewMonth = 11;
179
+ viewYear--;
180
+ } else {
181
+ viewMonth--;
182
+ }
183
+ }
184
+
185
+ function nextMonth() {
186
+ if (viewMonth === 11) {
187
+ viewMonth = 0;
188
+ viewYear++;
189
+ } else {
190
+ viewMonth++;
191
+ }
192
+ }
193
+
194
+ function clearValue() {
195
+ value = null;
196
+ onValueChange?.(null);
197
+ }
198
+
199
+ const monthNames = [
200
+ 'January',
201
+ 'February',
202
+ 'March',
203
+ 'April',
204
+ 'May',
205
+ 'June',
206
+ 'July',
207
+ 'August',
208
+ 'September',
209
+ 'October',
210
+ 'November',
211
+ 'December',
212
+ ];
213
+
214
+ const dayNames = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'];
215
+ </script>
216
+
217
+ <Popover.Root bind:open>
218
+ <Popover.Trigger
219
+ {id}
220
+ {disabled}
221
+ class={cn(
222
+ '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',
223
+ !value && 'text-muted-foreground',
224
+ error && 'border-destructive focus:ring-destructive',
225
+ className
226
+ )}
227
+ aria-invalid={error ? 'true' : undefined}
228
+ >
229
+ <span class="truncate">
230
+ {displayValue || placeholder}
231
+ </span>
232
+ <svg
233
+ xmlns="http://www.w3.org/2000/svg"
234
+ width="16"
235
+ height="16"
236
+ viewBox="0 0 24 24"
237
+ fill="none"
238
+ stroke="currentColor"
239
+ stroke-width="2"
240
+ stroke-linecap="round"
241
+ stroke-linejoin="round"
242
+ class="h-4 w-4 opacity-50 shrink-0 ml-2"
243
+ >
244
+ <rect width="18" height="18" x="3" y="4" rx="2" ry="2" />
245
+ <line x1="16" x2="16" y1="2" y2="6" />
246
+ <line x1="8" x2="8" y1="2" y2="6" />
247
+ <line x1="3" x2="21" y1="10" y2="10" />
248
+ <path d="M8 14h.01" />
249
+ <path d="M12 14h.01" />
250
+ <path d="M16 14h.01" />
251
+ <path d="M8 18h.01" />
252
+ <path d="M12 18h.01" />
253
+ <path d="M16 18h.01" />
254
+ </svg>
255
+ </Popover.Trigger>
256
+
257
+ <Popover.Content
258
+ class="w-auto p-0 rounded-md border bg-popover text-popover-foreground shadow-md"
259
+ sideOffset={4}
260
+ align="start"
261
+ >
262
+ <div class="p-3">
263
+ <!-- Calendar header -->
264
+ <div class="flex items-center justify-between mb-2">
265
+ <button
266
+ type="button"
267
+ onclick={prevMonth}
268
+ class="h-7 w-7 inline-flex items-center justify-center rounded-md hover:bg-accent hover:text-accent-foreground"
269
+ aria-label="Previous month"
270
+ >
271
+ <svg
272
+ xmlns="http://www.w3.org/2000/svg"
273
+ width="16"
274
+ height="16"
275
+ viewBox="0 0 24 24"
276
+ fill="none"
277
+ stroke="currentColor"
278
+ stroke-width="2"
279
+ stroke-linecap="round"
280
+ stroke-linejoin="round"
281
+ >
282
+ <path d="m15 18-6-6 6-6" />
283
+ </svg>
284
+ </button>
285
+ <span class="text-sm font-medium">
286
+ {monthNames[viewMonth]}
287
+ {viewYear}
288
+ </span>
289
+ <button
290
+ type="button"
291
+ onclick={nextMonth}
292
+ class="h-7 w-7 inline-flex items-center justify-center rounded-md hover:bg-accent hover:text-accent-foreground"
293
+ aria-label="Next month"
294
+ >
295
+ <svg
296
+ xmlns="http://www.w3.org/2000/svg"
297
+ width="16"
298
+ height="16"
299
+ viewBox="0 0 24 24"
300
+ fill="none"
301
+ stroke="currentColor"
302
+ stroke-width="2"
303
+ stroke-linecap="round"
304
+ stroke-linejoin="round"
305
+ >
306
+ <path d="m9 18 6-6-6-6" />
307
+ </svg>
308
+ </button>
309
+ </div>
310
+
311
+ <!-- Day names -->
312
+ <div class="grid grid-cols-7 gap-1 mb-1">
313
+ {#each dayNames as dayName}
314
+ <div
315
+ class="h-8 w-8 flex items-center justify-center text-xs text-muted-foreground font-medium"
316
+ >
317
+ {dayName}
318
+ </div>
319
+ {/each}
320
+ </div>
321
+
322
+ <!-- Calendar days -->
323
+ <div class="grid grid-cols-7 gap-1">
324
+ {#each calendarDays as day}
325
+ {#if day === null}
326
+ <div class="h-8 w-8"></div>
327
+ {:else}
328
+ <button
329
+ type="button"
330
+ onclick={() => selectDate(day)}
331
+ disabled={isDateDisabled(day)}
332
+ class={cn(
333
+ 'h-8 w-8 inline-flex items-center justify-center rounded-md text-sm transition-colors',
334
+ 'hover:bg-accent hover:text-accent-foreground',
335
+ 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
336
+ 'disabled:pointer-events-none disabled:opacity-50',
337
+ isDateSelected(day) &&
338
+ 'bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground'
339
+ )}
340
+ >
341
+ {day}
342
+ </button>
343
+ {/if}
344
+ {/each}
345
+ </div>
346
+
347
+ <!-- Time selector -->
348
+ <div class="border-t mt-3 pt-3">
349
+ <div class="flex items-center gap-2">
350
+ <span class="text-sm text-muted-foreground">Time:</span>
351
+ <select
352
+ bind:value={hour}
353
+ onchange={updateTime}
354
+ class="h-8 rounded-md border border-input bg-background px-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
355
+ >
356
+ {#each hourOptions as h}
357
+ <option value={timeFormat === '12h' ? h : h}>
358
+ {String(h).padStart(2, '0')}
359
+ </option>
360
+ {/each}
361
+ </select>
362
+ <span class="text-muted-foreground">:</span>
363
+ <select
364
+ bind:value={minute}
365
+ onchange={updateTime}
366
+ class="h-8 rounded-md border border-input bg-background px-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
367
+ >
368
+ {#each minuteOptions as m}
369
+ <option value={m}>{String(m).padStart(2, '0')}</option>
370
+ {/each}
371
+ </select>
372
+ {#if timeFormat === '12h'}
373
+ <select
374
+ bind:value={period}
375
+ onchange={updateTime}
376
+ class="h-8 rounded-md border border-input bg-background px-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
377
+ >
378
+ <option value="AM">AM</option>
379
+ <option value="PM">PM</option>
380
+ </select>
381
+ {/if}
382
+ </div>
383
+ </div>
384
+
385
+ <!-- Actions -->
386
+ <div class="border-t mt-3 pt-3 flex justify-between">
387
+ <button
388
+ type="button"
389
+ onclick={clearValue}
390
+ class="text-sm text-muted-foreground hover:text-foreground"
391
+ >
392
+ Clear
393
+ </button>
394
+ <button
395
+ type="button"
396
+ onclick={() => (open = false)}
397
+ class="text-sm font-medium text-primary hover:underline"
398
+ >
399
+ Done
400
+ </button>
401
+ </div>
402
+ </div>
403
+ </Popover.Content>
404
+ </Popover.Root>
405
+
406
+ <!-- Hidden input for form submission -->
407
+ {#if name && value}
408
+ <input type="hidden" {name} value={value.toISOString()} />
409
+ {/if}
410
+
411
+ {#if error}
412
+ <p class="text-sm text-destructive mt-1.5" role="alert">
413
+ {error}
414
+ </p>
415
+ {/if}
@@ -0,0 +1,31 @@
1
+ interface Props {
2
+ /** Current date/time value */
3
+ value?: Date | null;
4
+ /** Callback when value changes */
5
+ onValueChange?: (date: Date | null) => void;
6
+ /** Placeholder text */
7
+ placeholder?: string;
8
+ /** Whether the picker is disabled */
9
+ disabled?: boolean;
10
+ /** Whether selection is required */
11
+ required?: boolean;
12
+ /** Minimum selectable date */
13
+ min?: Date;
14
+ /** Maximum selectable date */
15
+ max?: Date;
16
+ /** Time format (12h or 24h) */
17
+ timeFormat?: '12h' | '24h';
18
+ /** Minute step interval (default: 15) */
19
+ minuteStep?: number;
20
+ /** Name attribute for form submission */
21
+ name?: string;
22
+ /** Element ID */
23
+ id?: string;
24
+ /** Error message to display */
25
+ error?: string;
26
+ /** Additional class for the trigger */
27
+ class?: string;
28
+ }
29
+ declare const DateTimePicker: import("svelte").Component<Props, {}, "value">;
30
+ type DateTimePicker = ReturnType<typeof DateTimePicker>;
31
+ export default DateTimePicker;