@salmexio/ui 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/dist/feedback/Alert/Alert.svelte +4 -1
  2. package/dist/feedback/Alert/Alert.svelte.d.ts +1 -0
  3. package/dist/feedback/Alert/Alert.svelte.d.ts.map +1 -1
  4. package/dist/feedback/Spinner/Spinner.svelte +4 -1
  5. package/dist/feedback/Spinner/Spinner.svelte.d.ts +1 -0
  6. package/dist/feedback/Spinner/Spinner.svelte.d.ts.map +1 -1
  7. package/dist/forms/DatePicker/DatePicker.svelte +725 -0
  8. package/dist/forms/DatePicker/DatePicker.svelte.d.ts +48 -0
  9. package/dist/forms/DatePicker/DatePicker.svelte.d.ts.map +1 -0
  10. package/dist/forms/DatePicker/index.d.ts +2 -0
  11. package/dist/forms/DatePicker/index.d.ts.map +1 -0
  12. package/dist/forms/DatePicker/index.js +1 -0
  13. package/dist/forms/FormField/FormField.svelte +173 -0
  14. package/dist/forms/FormField/FormField.svelte.d.ts +46 -0
  15. package/dist/forms/FormField/FormField.svelte.d.ts.map +1 -0
  16. package/dist/forms/FormField/index.d.ts +2 -0
  17. package/dist/forms/FormField/index.d.ts.map +1 -0
  18. package/dist/forms/FormField/index.js +1 -0
  19. package/dist/forms/MultiSelect/MultiSelect.svelte +820 -0
  20. package/dist/forms/MultiSelect/MultiSelect.svelte.d.ts +69 -0
  21. package/dist/forms/MultiSelect/MultiSelect.svelte.d.ts.map +1 -0
  22. package/dist/forms/MultiSelect/index.d.ts +3 -0
  23. package/dist/forms/MultiSelect/index.d.ts.map +1 -0
  24. package/dist/forms/MultiSelect/index.js +1 -0
  25. package/dist/forms/PhoneInput/PhoneInput.svelte +591 -0
  26. package/dist/forms/PhoneInput/PhoneInput.svelte.d.ts +57 -0
  27. package/dist/forms/PhoneInput/PhoneInput.svelte.d.ts.map +1 -0
  28. package/dist/forms/PhoneInput/index.d.ts +4 -0
  29. package/dist/forms/PhoneInput/index.d.ts.map +1 -0
  30. package/dist/forms/PhoneInput/index.js +2 -0
  31. package/dist/forms/RadioGroup/RadioGroup.svelte +417 -0
  32. package/dist/forms/RadioGroup/RadioGroup.svelte.d.ts +62 -0
  33. package/dist/forms/RadioGroup/RadioGroup.svelte.d.ts.map +1 -0
  34. package/dist/forms/RadioGroup/index.d.ts +3 -0
  35. package/dist/forms/RadioGroup/index.d.ts.map +1 -0
  36. package/dist/forms/RadioGroup/index.js +1 -0
  37. package/dist/forms/SearchInput/SearchInput.svelte +788 -0
  38. package/dist/forms/SearchInput/SearchInput.svelte.d.ts +79 -0
  39. package/dist/forms/SearchInput/SearchInput.svelte.d.ts.map +1 -0
  40. package/dist/forms/SearchInput/index.d.ts +3 -0
  41. package/dist/forms/SearchInput/index.d.ts.map +1 -0
  42. package/dist/forms/SearchInput/index.js +1 -0
  43. package/dist/forms/Select/Select.svelte +14 -8
  44. package/dist/forms/Select/Select.svelte.d.ts +2 -0
  45. package/dist/forms/Select/Select.svelte.d.ts.map +1 -1
  46. package/dist/forms/TextInput/TextInput.svelte +38 -16
  47. package/dist/forms/TextInput/TextInput.svelte.d.ts +6 -0
  48. package/dist/forms/TextInput/TextInput.svelte.d.ts.map +1 -1
  49. package/dist/forms/Textarea/Textarea.svelte +7 -1
  50. package/dist/forms/Textarea/Textarea.svelte.d.ts +2 -0
  51. package/dist/forms/Textarea/Textarea.svelte.d.ts.map +1 -1
  52. package/dist/forms/TimePicker/TimePicker.svelte +417 -0
  53. package/dist/forms/TimePicker/TimePicker.svelte.d.ts +53 -0
  54. package/dist/forms/TimePicker/TimePicker.svelte.d.ts.map +1 -0
  55. package/dist/forms/TimePicker/index.d.ts +2 -0
  56. package/dist/forms/TimePicker/index.d.ts.map +1 -0
  57. package/dist/forms/TimePicker/index.js +1 -0
  58. package/dist/forms/index.d.ts +12 -0
  59. package/dist/forms/index.d.ts.map +1 -1
  60. package/dist/forms/index.js +8 -0
  61. package/dist/layout/Container/Container.svelte +3 -0
  62. package/dist/layout/Container/Container.svelte.d.ts +1 -0
  63. package/dist/layout/Container/Container.svelte.d.ts.map +1 -1
  64. package/dist/primitives/Badge/Badge.svelte +5 -1
  65. package/dist/primitives/Badge/Badge.svelte.d.ts +1 -0
  66. package/dist/primitives/Badge/Badge.svelte.d.ts.map +1 -1
  67. package/dist/primitives/Tooltip/Tooltip.svelte +30 -0
  68. package/dist/primitives/Tooltip/Tooltip.svelte.d.ts.map +1 -1
  69. package/dist/utils/accessibility.d.ts +16 -0
  70. package/dist/utils/accessibility.d.ts.map +1 -0
  71. package/dist/utils/accessibility.js +80 -0
  72. package/dist/utils/index.d.ts +2 -1
  73. package/dist/utils/index.d.ts.map +1 -1
  74. package/dist/utils/index.js +2 -1
  75. package/dist/utils/keyboard.d.ts +6 -0
  76. package/dist/utils/keyboard.d.ts.map +1 -1
  77. package/dist/utils/keyboard.js +15 -9
  78. package/package.json +21 -1
@@ -0,0 +1,725 @@
1
+ <!--
2
+ @component DatePicker
3
+
4
+ INFRARED — Date input with calendar popup, month/year navigation,
5
+ today button, keyboard navigation, and full ARIA compliance.
6
+ Zero dependencies.
7
+
8
+ @example
9
+ <DatePicker label="Launch date" bind:value={date} />
10
+ <DatePicker label="Start" min="2024-01-01" max="2025-12-31" />
11
+ -->
12
+ <script lang="ts">
13
+ import { cn } from '../../utils/cn.js';
14
+ import { Keys, generateId } from '../../utils/keyboard.js';
15
+ import { onMount, tick } from 'svelte';
16
+
17
+ type DatePickerSize = 'sm' | 'md' | 'lg';
18
+
19
+ interface Props {
20
+ /** Visible label. */
21
+ label: string;
22
+ /** Selected date in YYYY-MM-DD format. */
23
+ value?: string;
24
+ /** Placeholder text. */
25
+ placeholder?: string;
26
+ /** Minimum allowed date (YYYY-MM-DD). */
27
+ min?: string;
28
+ /** Maximum allowed date (YYYY-MM-DD). */
29
+ max?: string;
30
+ /** Size variant. */
31
+ size?: DatePickerSize;
32
+ /** Required field. */
33
+ required?: boolean;
34
+ /** Disabled state. */
35
+ disabled?: boolean;
36
+ /** Error message. */
37
+ error?: string;
38
+ /** Hint text. */
39
+ hint?: string;
40
+ /** Hide the visible label. */
41
+ hideLabel?: boolean;
42
+ /** Reserve footer space even when empty. */
43
+ alwaysShowFooter?: boolean;
44
+ /** Additional CSS class. */
45
+ class?: string;
46
+ /** Called when date changes. */
47
+ onchange?: (value: string) => void;
48
+ /** Test ID. */
49
+ testId?: string;
50
+ }
51
+
52
+ let {
53
+ label,
54
+ value = $bindable(''),
55
+ placeholder = 'YYYY-MM-DD',
56
+ min,
57
+ max,
58
+ size = 'md',
59
+ required = false,
60
+ disabled = false,
61
+ error = '',
62
+ hint = '',
63
+ hideLabel = false,
64
+ alwaysShowFooter = true,
65
+ class: className = '',
66
+ onchange,
67
+ testId
68
+ }: Props = $props();
69
+
70
+ const id = generateId('datepicker');
71
+ const errorId = `${id}-error`;
72
+ const hintId = `${id}-hint`;
73
+ const calendarId = `${id}-calendar`;
74
+
75
+ let triggerEl = $state<HTMLElement | null>(null);
76
+ let calendarEl = $state<HTMLElement | null>(null);
77
+ let isOpen = $state(false);
78
+ let hasInteracted = $state(false);
79
+
80
+ // Calendar navigation state
81
+ let viewYear = $state(new Date().getFullYear());
82
+ let viewMonth = $state(new Date().getMonth());
83
+ let focusedDay = $state(new Date().getDate());
84
+
85
+ const DAYS = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'];
86
+ const MONTHS = ['January', 'February', 'March', 'April', 'May', 'June',
87
+ 'July', 'August', 'September', 'October', 'November', 'December'];
88
+
89
+ const showError = $derived(hasInteracted && !!error);
90
+ const ariaDescribedBy = $derived(
91
+ [showError && errorId, hint && hintId].filter(Boolean).join(' ') || undefined
92
+ );
93
+ const hasFooterContent = $derived(showError || !!hint);
94
+
95
+ // Parse the selected value into a Date
96
+ const selectedDate = $derived(value ? parseDate(value) : null);
97
+
98
+ // Display text
99
+ const displayText = $derived(
100
+ selectedDate
101
+ ? `${MONTHS[selectedDate.getMonth()]} ${selectedDate.getDate()}, ${selectedDate.getFullYear()}`
102
+ : ''
103
+ );
104
+
105
+ // Calendar grid
106
+ const firstDayOfMonth = $derived(new Date(viewYear, viewMonth, 1).getDay());
107
+ const daysInMonth = $derived(new Date(viewYear, viewMonth + 1, 0).getDate());
108
+ const daysInPrevMonth = $derived(new Date(viewYear, viewMonth, 0).getDate());
109
+
110
+ // Panel positioning
111
+ let panelTop = $state(0);
112
+ let panelLeft = $state(0);
113
+ let placeAbove = $state(false);
114
+
115
+ function parseDate(str: string): Date | null {
116
+ const parts = str.split('-');
117
+ if (parts.length !== 3) return null;
118
+ const d = new Date(Number(parts[0]), Number(parts[1]) - 1, Number(parts[2]));
119
+ return isNaN(d.getTime()) ? null : d;
120
+ }
121
+
122
+ function padYear(y: number): string {
123
+ return String(y).padStart(4, '0');
124
+ }
125
+
126
+ function formatDate(d: Date): string {
127
+ const y = padYear(d.getFullYear());
128
+ const m = String(d.getMonth() + 1).padStart(2, '0');
129
+ const day = String(d.getDate()).padStart(2, '0');
130
+ return `${y}-${m}-${day}`;
131
+ }
132
+
133
+ function isDateDisabled(year: number, month: number, day: number): boolean {
134
+ const dateStr = `${padYear(year)}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
135
+ if (min && dateStr < min) return true;
136
+ if (max && dateStr > max) return true;
137
+ return false;
138
+ }
139
+
140
+ function isToday(year: number, month: number, day: number): boolean {
141
+ const now = new Date();
142
+ return year === now.getFullYear() && month === now.getMonth() && day === now.getDate();
143
+ }
144
+
145
+ function isSelected(year: number, month: number, day: number): boolean {
146
+ if (!selectedDate) return false;
147
+ return year === selectedDate.getFullYear() && month === selectedDate.getMonth() && day === selectedDate.getDate();
148
+ }
149
+
150
+ function positionCalendar() {
151
+ if (!triggerEl) return;
152
+ const rect = triggerEl.getBoundingClientRect();
153
+ const viewportH = window.innerHeight;
154
+ const calH = 340;
155
+ const spaceBelow = viewportH - rect.bottom;
156
+ const spaceAbove = rect.top;
157
+
158
+ placeAbove = spaceBelow < calH && spaceAbove > spaceBelow;
159
+ panelLeft = rect.left;
160
+ panelTop = placeAbove ? rect.top - calH - 4 : rect.bottom + 4;
161
+ }
162
+
163
+ function open() {
164
+ if (disabled) return;
165
+ if (selectedDate) {
166
+ viewYear = selectedDate.getFullYear();
167
+ viewMonth = selectedDate.getMonth();
168
+ focusedDay = selectedDate.getDate();
169
+ } else {
170
+ const now = new Date();
171
+ viewYear = now.getFullYear();
172
+ viewMonth = now.getMonth();
173
+ focusedDay = now.getDate();
174
+ }
175
+ isOpen = true;
176
+ positionCalendar();
177
+ }
178
+
179
+ function close() {
180
+ isOpen = false;
181
+ }
182
+
183
+ function selectDay(day: number) {
184
+ if (isDateDisabled(viewYear, viewMonth, day)) return;
185
+ value = formatDate(new Date(viewYear, viewMonth, day));
186
+ hasInteracted = true;
187
+ onchange?.(value);
188
+ close();
189
+ triggerEl?.focus();
190
+ }
191
+
192
+ function selectToday() {
193
+ const now = new Date();
194
+ if (isDateDisabled(now.getFullYear(), now.getMonth(), now.getDate())) return;
195
+ value = formatDate(now);
196
+ viewYear = now.getFullYear();
197
+ viewMonth = now.getMonth();
198
+ hasInteracted = true;
199
+ onchange?.(value);
200
+ close();
201
+ triggerEl?.focus();
202
+ }
203
+
204
+ function prevMonth() {
205
+ if (viewMonth === 0) {
206
+ viewMonth = 11;
207
+ viewYear--;
208
+ } else {
209
+ viewMonth--;
210
+ }
211
+ focusedDay = Math.min(focusedDay, new Date(viewYear, viewMonth + 1, 0).getDate());
212
+ }
213
+
214
+ function nextMonth() {
215
+ if (viewMonth === 11) {
216
+ viewMonth = 0;
217
+ viewYear++;
218
+ } else {
219
+ viewMonth++;
220
+ }
221
+ focusedDay = Math.min(focusedDay, new Date(viewYear, viewMonth + 1, 0).getDate());
222
+ }
223
+
224
+ function prevYear() {
225
+ viewYear--;
226
+ focusedDay = Math.min(focusedDay, new Date(viewYear, viewMonth + 1, 0).getDate());
227
+ }
228
+
229
+ function nextYear() {
230
+ viewYear++;
231
+ focusedDay = Math.min(focusedDay, new Date(viewYear, viewMonth + 1, 0).getDate());
232
+ }
233
+
234
+ function handleTriggerKeydown(e: KeyboardEvent) {
235
+ if (e.key === Keys.Enter || e.key === Keys.Space || e.key === Keys.ArrowDown) {
236
+ e.preventDefault();
237
+ if (!isOpen) open();
238
+ }
239
+ }
240
+
241
+ function handleCalendarKeydown(e: KeyboardEvent) {
242
+ switch (e.key) {
243
+ case Keys.ArrowRight:
244
+ e.preventDefault();
245
+ if (focusedDay < daysInMonth) focusedDay++;
246
+ else { nextMonth(); focusedDay = 1; }
247
+ break;
248
+ case Keys.ArrowLeft:
249
+ e.preventDefault();
250
+ if (focusedDay > 1) focusedDay--;
251
+ else { prevMonth(); focusedDay = daysInMonth; }
252
+ break;
253
+ case Keys.ArrowDown:
254
+ e.preventDefault();
255
+ if (focusedDay + 7 <= daysInMonth) focusedDay += 7;
256
+ else { const overflow = focusedDay + 7 - daysInMonth; nextMonth(); focusedDay = overflow; }
257
+ break;
258
+ case Keys.ArrowUp:
259
+ e.preventDefault();
260
+ if (focusedDay - 7 >= 1) focusedDay -= 7;
261
+ else { const newDay = daysInPrevMonth + (focusedDay - 7); prevMonth(); focusedDay = newDay; }
262
+ break;
263
+ case Keys.Home:
264
+ e.preventDefault();
265
+ focusedDay = 1;
266
+ break;
267
+ case Keys.End:
268
+ e.preventDefault();
269
+ focusedDay = daysInMonth;
270
+ break;
271
+ case Keys.Enter:
272
+ case Keys.Space:
273
+ e.preventDefault();
274
+ selectDay(focusedDay);
275
+ break;
276
+ case Keys.Escape:
277
+ e.preventDefault();
278
+ close();
279
+ triggerEl?.focus();
280
+ break;
281
+ }
282
+ }
283
+
284
+ function handleClickOutside(e: MouseEvent) {
285
+ const target = e.target as Node;
286
+ if (triggerEl?.contains(target)) return;
287
+ if (calendarEl?.contains(target)) return;
288
+ close();
289
+ }
290
+
291
+ function handleReposition() {
292
+ if (isOpen) positionCalendar();
293
+ }
294
+
295
+ // Portal calendar to body
296
+ $effect(() => {
297
+ if (calendarEl && isOpen) {
298
+ document.body.appendChild(calendarEl);
299
+ return () => {
300
+ if (calendarEl?.parentNode === document.body) {
301
+ document.body.removeChild(calendarEl);
302
+ }
303
+ };
304
+ }
305
+ });
306
+
307
+ onMount(() => {
308
+ document.addEventListener('mousedown', handleClickOutside);
309
+ window.addEventListener('scroll', handleReposition, true);
310
+ window.addEventListener('resize', handleReposition);
311
+ return () => {
312
+ document.removeEventListener('mousedown', handleClickOutside);
313
+ window.removeEventListener('scroll', handleReposition, true);
314
+ window.removeEventListener('resize', handleReposition);
315
+ if (calendarEl?.parentNode === document.body) {
316
+ document.body.removeChild(calendarEl);
317
+ }
318
+ };
319
+ });
320
+ </script>
321
+
322
+ <div
323
+ class={cn('sx-datepicker', `sx-datepicker-${size}`, disabled && 'sx-datepicker-disabled', className)}
324
+ data-testid={testId}
325
+ >
326
+ <label for={id} class={cn('sx-datepicker-label', hideLabel && 'sx-sr-only')}>
327
+ {label}
328
+ {#if required}
329
+ <span class="sx-datepicker-required" aria-hidden="true">*</span>
330
+ {/if}
331
+ </label>
332
+
333
+ <button
334
+ bind:this={triggerEl}
335
+ {id}
336
+ type="button"
337
+ class={cn(
338
+ 'sx-datepicker-trigger',
339
+ isOpen && 'sx-datepicker-trigger-open',
340
+ showError && 'sx-datepicker-trigger-error',
341
+ disabled && 'sx-datepicker-trigger-disabled'
342
+ )}
343
+ {disabled}
344
+ aria-haspopup="dialog"
345
+ aria-expanded={isOpen}
346
+ aria-controls={isOpen ? calendarId : undefined}
347
+ aria-describedby={ariaDescribedBy}
348
+ onclick={() => { if (isOpen) close(); else open(); }}
349
+ onkeydown={handleTriggerKeydown}
350
+ >
351
+ <span class="sx-datepicker-icon" aria-hidden="true">
352
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
353
+ <rect x="3" y="4" width="18" height="18" rx="2" ry="2" /><line x1="16" y1="2" x2="16" y2="6" /><line x1="8" y1="2" x2="8" y2="6" /><line x1="3" y1="10" x2="21" y2="10" />
354
+ </svg>
355
+ </span>
356
+ <span class={cn('sx-datepicker-value', !displayText && 'sx-datepicker-placeholder')}>
357
+ {displayText || placeholder}
358
+ </span>
359
+ </button>
360
+
361
+ {#if alwaysShowFooter || hasFooterContent}
362
+ <div class="sx-datepicker-footer">
363
+ {#if showError}
364
+ <p id={errorId} class="sx-datepicker-error" role="alert" aria-live="assertive">{error}</p>
365
+ {:else if hint}
366
+ <p id={hintId} class="sx-datepicker-hint">{hint}</p>
367
+ {/if}
368
+ </div>
369
+ {/if}
370
+ </div>
371
+
372
+ <!-- Calendar popup -->
373
+ {#if isOpen}
374
+ <!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
375
+ <div
376
+ bind:this={calendarEl}
377
+ id={calendarId}
378
+ class="sx-calendar"
379
+ style="position:fixed;left:{panelLeft}px;top:{panelTop}px;"
380
+ role="dialog"
381
+ aria-modal="false"
382
+ aria-label="{MONTHS[viewMonth]} {viewYear}"
383
+ tabindex="-1"
384
+ onkeydown={handleCalendarKeydown}
385
+ >
386
+ <!-- Header: month/year navigation -->
387
+ <div class="sx-calendar-header">
388
+ <button type="button" class="sx-calendar-nav" aria-label="Previous year" onclick={prevYear}>
389
+ <svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="2"><path d="M7 2L3 6L7 10" /><path d="M10 2L6 6L10 10" /></svg>
390
+ </button>
391
+ <button type="button" class="sx-calendar-nav" aria-label="Previous month" onclick={prevMonth}>
392
+ <svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="2"><path d="M8 2L4 6L8 10" /></svg>
393
+ </button>
394
+ <span class="sx-calendar-title" aria-live="polite">
395
+ {MONTHS[viewMonth]} {viewYear}
396
+ </span>
397
+ <button type="button" class="sx-calendar-nav" aria-label="Next month" onclick={nextMonth}>
398
+ <svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 2L8 6L4 10" /></svg>
399
+ </button>
400
+ <button type="button" class="sx-calendar-nav" aria-label="Next year" onclick={nextYear}>
401
+ <svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 2L9 6L5 10" /><path d="M2 2L6 6L2 10" /></svg>
402
+ </button>
403
+ </div>
404
+
405
+ <!-- Day labels -->
406
+ <div class="sx-calendar-weekdays" role="row">
407
+ {#each DAYS as day}
408
+ <span class="sx-calendar-weekday" role="columnheader" aria-label={day}>{day}</span>
409
+ {/each}
410
+ </div>
411
+
412
+ <!-- Day grid -->
413
+ <div class="sx-calendar-grid" role="grid" aria-label="Calendar">
414
+ <!-- Leading blanks -->
415
+ {#each Array(firstDayOfMonth) as _, i}
416
+ <span class="sx-calendar-day sx-calendar-day-outside" aria-hidden="true">
417
+ {daysInPrevMonth - firstDayOfMonth + 1 + i}
418
+ </span>
419
+ {/each}
420
+
421
+ <!-- Current month days -->
422
+ {#each Array(daysInMonth) as _, i}
423
+ {@const day = i + 1}
424
+ {@const dayDisabled = isDateDisabled(viewYear, viewMonth, day)}
425
+ {@const daySelected = isSelected(viewYear, viewMonth, day)}
426
+ {@const dayToday = isToday(viewYear, viewMonth, day)}
427
+ {@const dayFocused = day === focusedDay}
428
+ <button
429
+ type="button"
430
+ class={cn(
431
+ 'sx-calendar-day',
432
+ daySelected && 'sx-calendar-day-selected',
433
+ dayToday && !daySelected && 'sx-calendar-day-today',
434
+ dayFocused && 'sx-calendar-day-focused',
435
+ dayDisabled && 'sx-calendar-day-disabled'
436
+ )}
437
+ disabled={dayDisabled}
438
+ tabindex={dayFocused ? 0 : -1}
439
+ aria-current={dayToday ? 'date' : undefined}
440
+ onclick={() => selectDay(day)}
441
+ >
442
+ {day}
443
+ </button>
444
+ {/each}
445
+ </div>
446
+
447
+ <!-- Footer -->
448
+ <div class="sx-calendar-footer">
449
+ <button type="button" class="sx-calendar-today-btn" onclick={selectToday}>
450
+ Today
451
+ </button>
452
+ {#if value}
453
+ <button type="button" class="sx-calendar-clear-btn" onclick={() => { value = ''; hasInteracted = true; onchange?.(''); close(); triggerEl?.focus(); }}>
454
+ Clear
455
+ </button>
456
+ {/if}
457
+ </div>
458
+ </div>
459
+ {/if}
460
+
461
+ <style>
462
+ .sx-datepicker {
463
+ display: flex;
464
+ flex-direction: column;
465
+ gap: var(--sx-space-1);
466
+ font-family: var(--sx-font-body);
467
+ }
468
+
469
+ .sx-datepicker-disabled { opacity: 0.5; }
470
+
471
+ .sx-datepicker-label {
472
+ font-size: var(--sx-text-sm);
473
+ font-weight: 500;
474
+ color: var(--sx-color-text-secondary);
475
+ }
476
+
477
+ .sx-datepicker-required { color: var(--sx-color-red); margin-left: 2px; }
478
+
479
+ .sx-sr-only {
480
+ position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
481
+ overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
482
+ }
483
+
484
+ /* Trigger */
485
+ .sx-datepicker-trigger {
486
+ display: flex;
487
+ align-items: center;
488
+ gap: var(--sx-space-2);
489
+ width: 100%;
490
+ border: 1px solid var(--sx-color-border-strong);
491
+ border-radius: var(--sx-radius-md);
492
+ background: var(--sx-color-surface);
493
+ color: var(--sx-color-text);
494
+ font-family: var(--sx-font-body);
495
+ font-weight: 500;
496
+ cursor: pointer;
497
+ text-align: left;
498
+ transition: border-color var(--sx-transition-fast), box-shadow var(--sx-transition-fast);
499
+ box-shadow:
500
+ inset 0 1px 3px rgba(0, 0, 0, 0.3),
501
+ inset 0 0 0 1px rgba(0, 0, 0, 0.06);
502
+ }
503
+
504
+ .sx-datepicker-trigger:hover:not(:disabled) { border-color: var(--sx-color-border-hover); }
505
+
506
+ .sx-datepicker-trigger:focus-visible {
507
+ outline: none;
508
+ border-color: var(--sx-color-primary);
509
+ box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.2), 0 0 0 3px var(--sx-color-primary-ring);
510
+ }
511
+
512
+ .sx-datepicker-trigger-open {
513
+ border-color: var(--sx-color-primary);
514
+ box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.2), 0 0 0 3px var(--sx-color-primary-ring);
515
+ }
516
+
517
+ .sx-datepicker-trigger-error {
518
+ border-color: var(--sx-color-red);
519
+ box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.2), 0 0 0 3px var(--sx-color-red-ring);
520
+ }
521
+
522
+ .sx-datepicker-trigger-disabled { cursor: not-allowed; box-shadow: none; }
523
+
524
+ .sx-datepicker-sm .sx-datepicker-trigger { min-height: 32px; padding: 0 var(--sx-space-3); font-size: var(--sx-text-xs); }
525
+ .sx-datepicker-md .sx-datepicker-trigger { min-height: 40px; padding: 0 var(--sx-space-4); font-size: var(--sx-text-sm); }
526
+ .sx-datepicker-lg .sx-datepicker-trigger { min-height: 48px; padding: 0 var(--sx-space-5); font-size: var(--sx-text-base); }
527
+
528
+ .sx-datepicker-icon {
529
+ flex-shrink: 0;
530
+ display: flex;
531
+ align-items: center;
532
+ color: var(--sx-color-text-secondary);
533
+ }
534
+
535
+ .sx-datepicker-value { flex: 1; }
536
+ .sx-datepicker-placeholder { color: var(--sx-color-text-disabled); }
537
+
538
+ /* Footer */
539
+ .sx-datepicker-footer { min-height: 1.25rem; }
540
+ .sx-datepicker-error { font-size: var(--sx-text-xs); font-weight: 500; color: var(--sx-color-red); margin: 0; }
541
+ .sx-datepicker-hint { font-size: var(--sx-text-xs); color: var(--sx-color-text-secondary); margin: 0; }
542
+
543
+ /* Calendar popup */
544
+ .sx-calendar {
545
+ z-index: var(--sx-z-dropdown);
546
+ background: var(--sx-color-surface-2);
547
+ border: 1px solid var(--sx-color-border-strong);
548
+ border-radius: var(--sx-radius-md);
549
+ box-shadow: var(--sx-shadow-lg);
550
+ backdrop-filter: var(--sx-glass-blur);
551
+ -webkit-backdrop-filter: var(--sx-glass-blur);
552
+ padding: var(--sx-space-3);
553
+ font-family: var(--sx-font-body);
554
+ width: 280px;
555
+ outline: none;
556
+ }
557
+
558
+ .sx-calendar-header {
559
+ display: flex;
560
+ align-items: center;
561
+ justify-content: space-between;
562
+ gap: var(--sx-space-1);
563
+ margin-bottom: var(--sx-space-2);
564
+ }
565
+
566
+ .sx-calendar-nav {
567
+ display: flex;
568
+ align-items: center;
569
+ justify-content: center;
570
+ width: 28px;
571
+ height: 28px;
572
+ border: none;
573
+ border-radius: var(--sx-radius-sm);
574
+ background: transparent;
575
+ color: var(--sx-color-text-secondary);
576
+ cursor: pointer;
577
+ transition: background var(--sx-transition-fast), color var(--sx-transition-fast);
578
+ }
579
+
580
+ .sx-calendar-nav:hover {
581
+ background: var(--sx-color-surface-3);
582
+ color: var(--sx-color-text);
583
+ }
584
+
585
+ .sx-calendar-nav:focus-visible {
586
+ outline: 2px solid var(--sx-color-primary);
587
+ outline-offset: 1px;
588
+ }
589
+
590
+ .sx-calendar-title {
591
+ flex: 1;
592
+ text-align: center;
593
+ font-size: var(--sx-text-sm);
594
+ font-weight: 600;
595
+ color: var(--sx-color-text);
596
+ }
597
+
598
+ .sx-calendar-weekdays {
599
+ display: grid;
600
+ grid-template-columns: repeat(7, 1fr);
601
+ margin-bottom: var(--sx-space-1);
602
+ }
603
+
604
+ .sx-calendar-weekday {
605
+ text-align: center;
606
+ font-size: var(--sx-text-xs);
607
+ font-weight: 600;
608
+ color: var(--sx-color-text-disabled);
609
+ padding: var(--sx-space-1) 0;
610
+ }
611
+
612
+ .sx-calendar-grid {
613
+ display: grid;
614
+ grid-template-columns: repeat(7, 1fr);
615
+ gap: 2px;
616
+ }
617
+
618
+ .sx-calendar-day {
619
+ position: relative;
620
+ display: flex;
621
+ align-items: center;
622
+ justify-content: center;
623
+ width: 34px;
624
+ height: 34px;
625
+ border: none;
626
+ border-radius: var(--sx-radius-sm);
627
+ background: transparent;
628
+ color: var(--sx-color-text);
629
+ font-size: var(--sx-text-sm);
630
+ font-weight: 500;
631
+ cursor: pointer;
632
+ transition: background var(--sx-transition-fast), color var(--sx-transition-fast);
633
+ }
634
+
635
+ .sx-calendar-day:hover:not(:disabled) {
636
+ background: var(--sx-color-surface-3);
637
+ }
638
+
639
+ .sx-calendar-day:focus-visible {
640
+ outline: 2px solid var(--sx-color-primary);
641
+ outline-offset: -2px;
642
+ }
643
+
644
+ .sx-calendar-day-focused:not(.sx-calendar-day-selected) {
645
+ box-shadow: inset 0 0 0 2px var(--sx-color-border-hover);
646
+ }
647
+
648
+ .sx-calendar-day-today {
649
+ color: var(--sx-color-primary);
650
+ font-weight: 700;
651
+ }
652
+
653
+ .sx-calendar-day-today::after {
654
+ content: '';
655
+ position: absolute;
656
+ bottom: 4px;
657
+ width: 4px;
658
+ height: 4px;
659
+ border-radius: 50%;
660
+ background: var(--sx-color-primary);
661
+ }
662
+
663
+ .sx-calendar-day-selected {
664
+ background: var(--sx-gradient-brand) !important;
665
+ color: #fff;
666
+ font-weight: 700;
667
+ box-shadow: 0 0 8px -2px rgba(255, 107, 53, 0.4);
668
+ }
669
+
670
+ .sx-calendar-day-disabled {
671
+ opacity: 0.3;
672
+ cursor: not-allowed;
673
+ }
674
+
675
+ .sx-calendar-day-outside {
676
+ color: var(--sx-color-text-disabled);
677
+ cursor: default;
678
+ pointer-events: none;
679
+ }
680
+
681
+ .sx-calendar-footer {
682
+ display: flex;
683
+ justify-content: center;
684
+ gap: var(--sx-space-2);
685
+ margin-top: var(--sx-space-2);
686
+ padding-top: var(--sx-space-2);
687
+ border-top: 1px solid var(--sx-color-border);
688
+ }
689
+
690
+ .sx-calendar-today-btn,
691
+ .sx-calendar-clear-btn {
692
+ border: none;
693
+ background: none;
694
+ font-family: var(--sx-font-body);
695
+ font-size: var(--sx-text-xs);
696
+ font-weight: 600;
697
+ color: var(--sx-color-primary);
698
+ cursor: pointer;
699
+ padding: var(--sx-space-1) var(--sx-space-3);
700
+ border-radius: var(--sx-radius-sm);
701
+ }
702
+
703
+ .sx-calendar-today-btn:hover,
704
+ .sx-calendar-clear-btn:hover {
705
+ background: var(--sx-color-primary-subtle);
706
+ }
707
+
708
+ .sx-calendar-today-btn:focus-visible,
709
+ .sx-calendar-clear-btn:focus-visible {
710
+ outline: 2px solid var(--sx-color-primary);
711
+ outline-offset: 1px;
712
+ }
713
+
714
+ .sx-calendar-clear-btn {
715
+ color: var(--sx-color-text-secondary);
716
+ }
717
+
718
+ .sx-calendar-clear-btn:hover {
719
+ background: var(--sx-color-surface-3);
720
+ }
721
+
722
+ @media (prefers-reduced-motion: reduce) {
723
+ .sx-datepicker-trigger, .sx-calendar-nav, .sx-calendar-day { transition: none; }
724
+ }
725
+ </style>