@aspect-ops/exon-ui 0.0.3 → 0.2.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 (79) hide show
  1. package/README.md +929 -54
  2. package/dist/components/Accordion/Accordion.svelte +79 -0
  3. package/dist/components/Accordion/Accordion.svelte.d.ts +10 -0
  4. package/dist/components/Accordion/AccordionItem.svelte +198 -0
  5. package/dist/components/Accordion/AccordionItem.svelte.d.ts +10 -0
  6. package/dist/components/Accordion/index.d.ts +2 -0
  7. package/dist/components/Accordion/index.js +2 -0
  8. package/dist/components/Carousel/Carousel.svelte +454 -0
  9. package/dist/components/Carousel/Carousel.svelte.d.ts +14 -0
  10. package/dist/components/Carousel/CarouselSlide.svelte +22 -0
  11. package/dist/components/Carousel/CarouselSlide.svelte.d.ts +7 -0
  12. package/dist/components/Carousel/index.d.ts +2 -0
  13. package/dist/components/Carousel/index.js +2 -0
  14. package/dist/components/Chip/Chip.svelte +461 -0
  15. package/dist/components/Chip/Chip.svelte.d.ts +17 -0
  16. package/dist/components/Chip/ChipGroup.svelte +76 -0
  17. package/dist/components/Chip/ChipGroup.svelte.d.ts +9 -0
  18. package/dist/components/Chip/index.d.ts +2 -0
  19. package/dist/components/Chip/index.js +2 -0
  20. package/dist/components/DatePicker/DatePicker.svelte +746 -0
  21. package/dist/components/DatePicker/DatePicker.svelte.d.ts +19 -0
  22. package/dist/components/DatePicker/index.d.ts +1 -0
  23. package/dist/components/DatePicker/index.js +1 -0
  24. package/dist/components/FileUpload/FileUpload.svelte +484 -0
  25. package/dist/components/FileUpload/FileUpload.svelte.d.ts +16 -0
  26. package/dist/components/FileUpload/index.d.ts +1 -0
  27. package/dist/components/FileUpload/index.js +1 -0
  28. package/dist/components/Image/Image.svelte +223 -0
  29. package/dist/components/Image/Image.svelte.d.ts +19 -0
  30. package/dist/components/Image/index.d.ts +1 -0
  31. package/dist/components/Image/index.js +1 -0
  32. package/dist/components/NumberInput/NumberInput.svelte +293 -0
  33. package/dist/components/NumberInput/NumberInput.svelte.d.ts +16 -0
  34. package/dist/components/NumberInput/index.d.ts +1 -0
  35. package/dist/components/NumberInput/index.js +1 -0
  36. package/dist/components/OTPInput/OTPInput.svelte +312 -0
  37. package/dist/components/OTPInput/OTPInput.svelte.d.ts +57 -0
  38. package/dist/components/OTPInput/index.d.ts +1 -0
  39. package/dist/components/OTPInput/index.js +1 -0
  40. package/dist/components/Pagination/Pagination.svelte +243 -0
  41. package/dist/components/Pagination/Pagination.svelte.d.ts +10 -0
  42. package/dist/components/Pagination/index.d.ts +1 -0
  43. package/dist/components/Pagination/index.js +1 -0
  44. package/dist/components/Rating/Rating.svelte +316 -0
  45. package/dist/components/Rating/Rating.svelte.d.ts +16 -0
  46. package/dist/components/Rating/index.d.ts +1 -0
  47. package/dist/components/Rating/index.js +1 -0
  48. package/dist/components/SearchInput/SearchInput.svelte +480 -0
  49. package/dist/components/SearchInput/SearchInput.svelte.d.ts +22 -0
  50. package/dist/components/SearchInput/index.d.ts +1 -0
  51. package/dist/components/SearchInput/index.js +1 -0
  52. package/dist/components/Slider/Slider.svelte +324 -0
  53. package/dist/components/Slider/Slider.svelte.d.ts +14 -0
  54. package/dist/components/Slider/index.d.ts +1 -0
  55. package/dist/components/Slider/index.js +1 -0
  56. package/dist/components/Stepper/Stepper.svelte +100 -0
  57. package/dist/components/Stepper/Stepper.svelte.d.ts +11 -0
  58. package/dist/components/Stepper/StepperStep.svelte +391 -0
  59. package/dist/components/Stepper/StepperStep.svelte.d.ts +13 -0
  60. package/dist/components/Stepper/index.d.ts +2 -0
  61. package/dist/components/Stepper/index.js +2 -0
  62. package/dist/components/TimePicker/TimePicker.svelte +803 -0
  63. package/dist/components/TimePicker/TimePicker.svelte.d.ts +17 -0
  64. package/dist/components/TimePicker/index.d.ts +1 -0
  65. package/dist/components/TimePicker/index.js +1 -0
  66. package/dist/components/ToggleGroup/ToggleGroup.svelte +91 -0
  67. package/dist/components/ToggleGroup/ToggleGroup.svelte.d.ts +13 -0
  68. package/dist/components/ToggleGroup/ToggleGroupItem.svelte +158 -0
  69. package/dist/components/ToggleGroup/ToggleGroupItem.svelte.d.ts +9 -0
  70. package/dist/components/ToggleGroup/index.d.ts +3 -0
  71. package/dist/components/ToggleGroup/index.js +2 -0
  72. package/dist/index.d.ts +13 -1
  73. package/dist/index.js +12 -0
  74. package/dist/types/data-display.d.ts +68 -0
  75. package/dist/types/index.d.ts +3 -2
  76. package/dist/types/input.d.ts +82 -0
  77. package/dist/types/input.js +2 -0
  78. package/dist/types/navigation.d.ts +15 -0
  79. package/package.json +1 -1
@@ -0,0 +1,803 @@
1
+ <script lang="ts">
2
+ import type { InputSize } from '../../types/index.js';
3
+ import { onMount } from 'svelte';
4
+
5
+ interface Props {
6
+ value?: string;
7
+ format?: '12h' | '24h';
8
+ minuteStep?: number;
9
+ min?: string;
10
+ max?: string;
11
+ disabled?: boolean;
12
+ error?: boolean;
13
+ size?: InputSize;
14
+ placeholder?: string;
15
+ class?: string;
16
+ onchange?: (value: string) => void;
17
+ }
18
+
19
+ let {
20
+ value = $bindable(''),
21
+ format = '24h',
22
+ minuteStep = 1,
23
+ min = undefined,
24
+ max = undefined,
25
+ disabled = false,
26
+ error = false,
27
+ size = 'md',
28
+ placeholder = 'Select time',
29
+ class: className = '',
30
+ onchange
31
+ }: Props = $props();
32
+
33
+ let isOpen = $state(false);
34
+ let inputElement: HTMLInputElement | undefined;
35
+ let pickerElement = $state<HTMLDivElement | undefined>(undefined);
36
+ let hourScrollElement = $state<HTMLDivElement | undefined>(undefined);
37
+ let minuteScrollElement = $state<HTMLDivElement | undefined>(undefined);
38
+ let periodScrollElement = $state<HTMLDivElement | undefined>(undefined);
39
+ let selectedHour = $state(0);
40
+ let selectedMinute = $state(0);
41
+ let period = $state<'AM' | 'PM'>('AM');
42
+ let focusedColumn: 'hour' | 'minute' | 'period' = $state('hour');
43
+
44
+ // Generate hour/minute options
45
+ const hours = $derived(
46
+ format === '12h'
47
+ ? Array.from({ length: 12 }, (_, i) => i + 1)
48
+ : Array.from({ length: 24 }, (_, i) => i)
49
+ );
50
+ const minutes = $derived(Array.from({ length: 60 / minuteStep }, (_, i) => i * minuteStep));
51
+
52
+ // Parse value to update internal state
53
+ $effect(() => {
54
+ if (value) {
55
+ const parsed = parseTimeValue(value);
56
+ if (parsed) {
57
+ selectedHour = parsed.hour;
58
+ selectedMinute = parsed.minute;
59
+ period = parsed.period;
60
+ }
61
+ }
62
+ });
63
+
64
+ function parseTimeValue(timeStr: string): {
65
+ hour: number;
66
+ minute: number;
67
+ period: 'AM' | 'PM';
68
+ } | null {
69
+ const match = timeStr.match(/^(\d{1,2}):(\d{2})$/);
70
+ if (!match) return null;
71
+
72
+ let hour = parseInt(match[1], 10);
73
+ const minute = parseInt(match[2], 10);
74
+
75
+ if (format === '12h') {
76
+ const isPM = hour >= 12;
77
+ if (hour === 0) hour = 12;
78
+ else if (hour > 12) hour -= 12;
79
+ return {
80
+ hour,
81
+ minute,
82
+ period: isPM ? 'PM' : 'AM'
83
+ };
84
+ } else {
85
+ return {
86
+ hour,
87
+ minute,
88
+ period: hour >= 12 ? 'PM' : 'AM'
89
+ };
90
+ }
91
+ }
92
+
93
+ function formatTimeValue(hour: number, minute: number, per: 'AM' | 'PM'): string {
94
+ if (format === '12h') {
95
+ // Convert 12h format to 24h HH:mm
96
+ let h24 = hour;
97
+ if (per === 'PM' && hour !== 12) {
98
+ h24 = hour + 12;
99
+ } else if (per === 'AM' && hour === 12) {
100
+ h24 = 0;
101
+ }
102
+ return `${String(h24).padStart(2, '0')}:${String(minute).padStart(2, '0')}`;
103
+ } else {
104
+ return `${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}`;
105
+ }
106
+ }
107
+
108
+ function formatDisplayValue(timeStr: string): string {
109
+ if (!timeStr) return '';
110
+ const parsed = parseTimeValue(timeStr);
111
+ if (!parsed) return timeStr;
112
+
113
+ if (format === '12h') {
114
+ return `${parsed.hour}:${String(parsed.minute).padStart(2, '0')} ${parsed.period}`;
115
+ } else {
116
+ return `${String(parsed.hour).padStart(2, '0')}:${String(parsed.minute).padStart(2, '0')}`;
117
+ }
118
+ }
119
+
120
+ const displayValue = $derived(formatDisplayValue(value));
121
+
122
+ function isTimeDisabled(hour: number, minute: number, per: 'AM' | 'PM'): boolean {
123
+ const timeStr = formatTimeValue(hour, minute, per);
124
+ if (min && timeStr < min) return true;
125
+ if (max && timeStr > max) return true;
126
+ return false;
127
+ }
128
+
129
+ function selectTime(hour: number, minute: number, per: 'AM' | 'PM') {
130
+ if (isTimeDisabled(hour, minute, per) || disabled) return;
131
+ selectedHour = hour;
132
+ selectedMinute = minute;
133
+ period = per;
134
+
135
+ const newValue = formatTimeValue(hour, minute, per);
136
+ value = newValue;
137
+ onchange?.(newValue);
138
+ }
139
+
140
+ function confirmSelection() {
141
+ selectTime(selectedHour, selectedMinute, period);
142
+ closePicker();
143
+ }
144
+
145
+ function clearTime() {
146
+ value = '';
147
+ onchange?.('');
148
+ inputElement?.focus();
149
+ }
150
+
151
+ function openPicker() {
152
+ if (disabled) return;
153
+ isOpen = true;
154
+
155
+ // Initialize from current value or set to current time
156
+ if (value) {
157
+ const parsed = parseTimeValue(value);
158
+ if (parsed) {
159
+ selectedHour = parsed.hour;
160
+ selectedMinute = parsed.minute;
161
+ period = parsed.period;
162
+ }
163
+ } else {
164
+ const now = new Date();
165
+ let hour = now.getHours();
166
+ const minute = Math.floor(now.getMinutes() / minuteStep) * minuteStep;
167
+
168
+ if (format === '12h') {
169
+ period = hour >= 12 ? 'PM' : 'AM';
170
+ if (hour === 0) hour = 12;
171
+ else if (hour > 12) hour -= 12;
172
+ } else {
173
+ period = hour >= 12 ? 'PM' : 'AM';
174
+ }
175
+
176
+ selectedHour = hour;
177
+ selectedMinute = minute;
178
+ }
179
+
180
+ // Scroll to selected values after DOM updates
181
+ setTimeout(() => {
182
+ scrollToSelected();
183
+ }, 10);
184
+ }
185
+
186
+ function closePicker() {
187
+ isOpen = false;
188
+ }
189
+
190
+ function scrollToSelected() {
191
+ const scrollToOption = (container: HTMLDivElement | undefined, value: number) => {
192
+ if (!container) return;
193
+ const option = container.querySelector(`[data-value="${value}"]`) as HTMLElement;
194
+ if (option) {
195
+ const containerHeight = container.clientHeight;
196
+ const optionTop = option.offsetTop;
197
+ const optionHeight = option.offsetHeight;
198
+ container.scrollTop = optionTop - containerHeight / 2 + optionHeight / 2;
199
+ }
200
+ };
201
+
202
+ scrollToOption(hourScrollElement, selectedHour);
203
+ scrollToOption(minuteScrollElement, selectedMinute);
204
+ if (format === '12h' && periodScrollElement) {
205
+ const periodValue = period === 'AM' ? 0 : 1;
206
+ scrollToOption(periodScrollElement, periodValue);
207
+ }
208
+ }
209
+
210
+ function handleKeyDown(e: KeyboardEvent) {
211
+ if (!isOpen) return;
212
+
213
+ switch (e.key) {
214
+ case 'ArrowUp':
215
+ e.preventDefault();
216
+ if (focusedColumn === 'hour') {
217
+ const currentIndex = hours.indexOf(selectedHour);
218
+ const newIndex = currentIndex > 0 ? currentIndex - 1 : hours.length - 1;
219
+ selectedHour = hours[newIndex];
220
+ } else if (focusedColumn === 'minute') {
221
+ const currentIndex = minutes.indexOf(selectedMinute);
222
+ const newIndex = currentIndex > 0 ? currentIndex - 1 : minutes.length - 1;
223
+ selectedMinute = minutes[newIndex];
224
+ } else if (focusedColumn === 'period') {
225
+ period = period === 'AM' ? 'PM' : 'AM';
226
+ }
227
+ scrollToSelected();
228
+ break;
229
+ case 'ArrowDown':
230
+ e.preventDefault();
231
+ if (focusedColumn === 'hour') {
232
+ const currentIndex = hours.indexOf(selectedHour);
233
+ const newIndex = currentIndex < hours.length - 1 ? currentIndex + 1 : 0;
234
+ selectedHour = hours[newIndex];
235
+ } else if (focusedColumn === 'minute') {
236
+ const currentIndex = minutes.indexOf(selectedMinute);
237
+ const newIndex = currentIndex < minutes.length - 1 ? currentIndex + 1 : 0;
238
+ selectedMinute = minutes[newIndex];
239
+ } else if (focusedColumn === 'period') {
240
+ period = period === 'AM' ? 'PM' : 'AM';
241
+ }
242
+ scrollToSelected();
243
+ break;
244
+ case 'Tab':
245
+ e.preventDefault();
246
+ if (e.shiftKey) {
247
+ // Shift+Tab - move backward
248
+ if (focusedColumn === 'period') focusedColumn = 'minute';
249
+ else if (focusedColumn === 'minute') focusedColumn = 'hour';
250
+ else if (focusedColumn === 'hour' && format === '12h') focusedColumn = 'period';
251
+ } else {
252
+ // Tab - move forward
253
+ if (focusedColumn === 'hour') focusedColumn = 'minute';
254
+ else if (focusedColumn === 'minute' && format === '12h') focusedColumn = 'period';
255
+ else if (focusedColumn === 'minute') focusedColumn = 'hour';
256
+ else if (focusedColumn === 'period') focusedColumn = 'hour';
257
+ }
258
+ break;
259
+ case 'Enter':
260
+ e.preventDefault();
261
+ confirmSelection();
262
+ break;
263
+ case 'Escape':
264
+ e.preventDefault();
265
+ closePicker();
266
+ inputElement?.focus();
267
+ break;
268
+ }
269
+ }
270
+
271
+ function handleClickOutside(e: MouseEvent) {
272
+ if (!isOpen) return;
273
+ const target = e.target as Node;
274
+ if (
275
+ pickerElement &&
276
+ !pickerElement.contains(target) &&
277
+ inputElement &&
278
+ !inputElement.contains(target)
279
+ ) {
280
+ closePicker();
281
+ }
282
+ }
283
+
284
+ onMount(() => {
285
+ document.addEventListener('click', handleClickOutside);
286
+ return () => {
287
+ document.removeEventListener('click', handleClickOutside);
288
+ };
289
+ });
290
+ </script>
291
+
292
+ <div class="timepicker timepicker--{size} {className}">
293
+ <div class="timepicker__input-wrapper">
294
+ <input
295
+ bind:this={inputElement}
296
+ type="text"
297
+ role="combobox"
298
+ class="timepicker__input"
299
+ class:timepicker__input--error={error}
300
+ class:timepicker__input--disabled={disabled}
301
+ value={displayValue}
302
+ {placeholder}
303
+ {disabled}
304
+ readonly
305
+ aria-invalid={error}
306
+ aria-haspopup="dialog"
307
+ aria-expanded={isOpen}
308
+ aria-controls="timepicker-dropdown"
309
+ onclick={openPicker}
310
+ onkeydown={(e) => {
311
+ if (e.key === 'Enter' || e.key === ' ') {
312
+ e.preventDefault();
313
+ openPicker();
314
+ }
315
+ }}
316
+ />
317
+
318
+ <div class="timepicker__icons">
319
+ {#if value && !disabled}
320
+ <button
321
+ type="button"
322
+ class="timepicker__clear"
323
+ aria-label="Clear time"
324
+ onclick={(e) => {
325
+ e.stopPropagation();
326
+ clearTime();
327
+ }}
328
+ >
329
+ <svg
330
+ width="16"
331
+ height="16"
332
+ viewBox="0 0 16 16"
333
+ fill="none"
334
+ xmlns="http://www.w3.org/2000/svg"
335
+ >
336
+ <path
337
+ d="M12 4L4 12M4 4L12 12"
338
+ stroke="currentColor"
339
+ stroke-width="1.5"
340
+ stroke-linecap="round"
341
+ stroke-linejoin="round"
342
+ />
343
+ </svg>
344
+ </button>
345
+ {/if}
346
+
347
+ <button
348
+ type="button"
349
+ class="timepicker__clock-icon"
350
+ aria-label="Open time picker"
351
+ {disabled}
352
+ onclick={openPicker}
353
+ >
354
+ <svg
355
+ width="16"
356
+ height="16"
357
+ viewBox="0 0 16 16"
358
+ fill="none"
359
+ xmlns="http://www.w3.org/2000/svg"
360
+ >
361
+ <circle
362
+ cx="8"
363
+ cy="8"
364
+ r="6"
365
+ stroke="currentColor"
366
+ stroke-width="1.5"
367
+ stroke-linecap="round"
368
+ stroke-linejoin="round"
369
+ />
370
+ <path
371
+ d="M8 4.66667V8L10 10"
372
+ stroke="currentColor"
373
+ stroke-width="1.5"
374
+ stroke-linecap="round"
375
+ stroke-linejoin="round"
376
+ />
377
+ </svg>
378
+ </button>
379
+ </div>
380
+ </div>
381
+
382
+ {#if isOpen}
383
+ <div
384
+ bind:this={pickerElement}
385
+ id="timepicker-dropdown"
386
+ class="timepicker__picker"
387
+ role="dialog"
388
+ aria-label="Choose a time"
389
+ aria-modal="false"
390
+ tabindex="-1"
391
+ onkeydown={handleKeyDown}
392
+ >
393
+ <div class="timepicker__columns">
394
+ <!-- Hour column -->
395
+ <div class="timepicker__column">
396
+ <div class="timepicker__column-label">Hour</div>
397
+ <div
398
+ bind:this={hourScrollElement}
399
+ class="timepicker__scroll"
400
+ aria-label="Select hour"
401
+ onfocus={() => {
402
+ focusedColumn = 'hour';
403
+ }}
404
+ >
405
+ {#each hours as hour}
406
+ <button
407
+ type="button"
408
+ class="timepicker__option"
409
+ class:timepicker__option--selected={hour === selectedHour}
410
+ class:timepicker__option--focused={focusedColumn === 'hour' &&
411
+ hour === selectedHour}
412
+ data-value={hour}
413
+ disabled={isTimeDisabled(hour, selectedMinute, period)}
414
+ tabindex={focusedColumn === 'hour' && hour === selectedHour ? 0 : -1}
415
+ onclick={() => {
416
+ selectedHour = hour;
417
+ focusedColumn = 'hour';
418
+ }}
419
+ >
420
+ {format === '12h' ? hour : String(hour).padStart(2, '0')}
421
+ </button>
422
+ {/each}
423
+ </div>
424
+ </div>
425
+
426
+ <!-- Minute column -->
427
+ <div class="timepicker__column">
428
+ <div class="timepicker__column-label">Min</div>
429
+ <div
430
+ bind:this={minuteScrollElement}
431
+ class="timepicker__scroll"
432
+ aria-label="Select minute"
433
+ onfocus={() => {
434
+ focusedColumn = 'minute';
435
+ }}
436
+ >
437
+ {#each minutes as minute}
438
+ <button
439
+ type="button"
440
+ class="timepicker__option"
441
+ class:timepicker__option--selected={minute === selectedMinute}
442
+ class:timepicker__option--focused={focusedColumn === 'minute' &&
443
+ minute === selectedMinute}
444
+ data-value={minute}
445
+ disabled={isTimeDisabled(selectedHour, minute, period)}
446
+ tabindex={focusedColumn === 'minute' && minute === selectedMinute ? 0 : -1}
447
+ onclick={() => {
448
+ selectedMinute = minute;
449
+ focusedColumn = 'minute';
450
+ }}
451
+ >
452
+ {String(minute).padStart(2, '0')}
453
+ </button>
454
+ {/each}
455
+ </div>
456
+ </div>
457
+
458
+ <!-- Period column (12h only) -->
459
+ {#if format === '12h'}
460
+ <div class="timepicker__column">
461
+ <div class="timepicker__column-label">&nbsp;</div>
462
+ <div
463
+ bind:this={periodScrollElement}
464
+ class="timepicker__scroll"
465
+ aria-label="Select AM or PM"
466
+ onfocus={() => {
467
+ focusedColumn = 'period';
468
+ }}
469
+ >
470
+ {#each ['AM', 'PM'] as per, idx}
471
+ <button
472
+ type="button"
473
+ class="timepicker__option"
474
+ class:timepicker__option--selected={per === period}
475
+ class:timepicker__option--focused={focusedColumn === 'period' && per === period}
476
+ data-value={idx}
477
+ tabindex={focusedColumn === 'period' && per === period ? 0 : -1}
478
+ onclick={() => {
479
+ period = per as 'AM' | 'PM';
480
+ focusedColumn = 'period';
481
+ }}
482
+ >
483
+ {per}
484
+ </button>
485
+ {/each}
486
+ </div>
487
+ </div>
488
+ {/if}
489
+ </div>
490
+
491
+ <div class="timepicker__actions">
492
+ <button
493
+ type="button"
494
+ class="timepicker__action-button timepicker__action-button--cancel"
495
+ onclick={closePicker}
496
+ >
497
+ Cancel
498
+ </button>
499
+ <button
500
+ type="button"
501
+ class="timepicker__action-button timepicker__action-button--confirm"
502
+ onclick={confirmSelection}
503
+ >
504
+ OK
505
+ </button>
506
+ </div>
507
+ </div>
508
+ {/if}
509
+ </div>
510
+
511
+ <style>
512
+ .timepicker {
513
+ position: relative;
514
+ width: 100%;
515
+ }
516
+
517
+ .timepicker__input-wrapper {
518
+ position: relative;
519
+ display: flex;
520
+ align-items: center;
521
+ width: 100%;
522
+ }
523
+
524
+ .timepicker__input {
525
+ width: 100%;
526
+ min-height: var(--touch-target-min, 44px);
527
+ padding: var(--space-sm, 0.5rem) var(--space-md, 1rem);
528
+ padding-right: 4rem;
529
+ border: 1px solid var(--color-border, #e5e7eb);
530
+ border-radius: var(--radius-md, 0.375rem);
531
+ background: var(--color-bg, #ffffff);
532
+ color: var(--color-text, #1f2937);
533
+ font-family: inherit;
534
+ font-size: var(--text-sm, 0.875rem);
535
+ line-height: 1.5;
536
+ cursor: pointer;
537
+ transition:
538
+ border-color var(--transition-fast, 150ms ease),
539
+ box-shadow var(--transition-fast, 150ms ease);
540
+ -webkit-tap-highlight-color: transparent;
541
+ }
542
+
543
+ .timepicker__input::placeholder {
544
+ color: var(--color-text-muted, #9ca3af);
545
+ }
546
+
547
+ .timepicker__input:focus {
548
+ outline: none;
549
+ border-color: var(--color-primary, #3b82f6);
550
+ box-shadow: 0 0 0 3px var(--color-primary-alpha, rgba(59, 130, 246, 0.1));
551
+ }
552
+
553
+ .timepicker__input--disabled {
554
+ background: var(--color-bg-muted, #f3f4f6);
555
+ cursor: not-allowed;
556
+ opacity: 0.5;
557
+ }
558
+
559
+ .timepicker__input--error {
560
+ border-color: var(--color-error, #ef4444);
561
+ }
562
+
563
+ .timepicker__input--error:focus {
564
+ border-color: var(--color-error, #ef4444);
565
+ box-shadow: 0 0 0 3px var(--color-error-alpha, rgba(239, 68, 68, 0.1));
566
+ }
567
+
568
+ /* Sizes */
569
+ .timepicker--sm .timepicker__input {
570
+ min-height: var(--touch-target-min, 44px);
571
+ padding: var(--space-xs, 0.25rem) var(--space-sm, 0.5rem);
572
+ padding-right: 3.5rem;
573
+ font-size: var(--text-xs, 0.75rem);
574
+ }
575
+
576
+ .timepicker--md .timepicker__input {
577
+ min-height: var(--touch-target-min, 44px);
578
+ padding: var(--space-sm, 0.5rem) var(--space-md, 1rem);
579
+ padding-right: 4rem;
580
+ font-size: var(--text-sm, 0.875rem);
581
+ }
582
+
583
+ .timepicker--lg .timepicker__input {
584
+ min-height: 3.25rem;
585
+ padding: var(--space-md, 1rem) var(--space-lg, 1.5rem);
586
+ padding-right: 4.5rem;
587
+ font-size: var(--text-base, 1rem);
588
+ }
589
+
590
+ /* Icons */
591
+ .timepicker__icons {
592
+ position: absolute;
593
+ right: var(--space-md, 1rem);
594
+ display: flex;
595
+ align-items: center;
596
+ gap: var(--space-xs, 0.25rem);
597
+ }
598
+
599
+ .timepicker__clear,
600
+ .timepicker__clock-icon {
601
+ display: flex;
602
+ align-items: center;
603
+ justify-content: center;
604
+ min-width: var(--touch-target-min, 44px);
605
+ min-height: var(--touch-target-min, 44px);
606
+ padding: var(--space-xs, 0.25rem);
607
+ border: none;
608
+ background: transparent;
609
+ color: var(--color-text-muted, #6b7280);
610
+ cursor: pointer;
611
+ border-radius: var(--radius-sm, 0.25rem);
612
+ transition: background var(--transition-fast, 150ms ease);
613
+ -webkit-tap-highlight-color: transparent;
614
+ }
615
+
616
+ .timepicker__clear:hover,
617
+ .timepicker__clock-icon:hover {
618
+ background: var(--color-bg-muted, #f3f4f6);
619
+ color: var(--color-text, #1f2937);
620
+ }
621
+
622
+ .timepicker__clock-icon:disabled {
623
+ cursor: not-allowed;
624
+ opacity: 0.5;
625
+ }
626
+
627
+ /* Picker */
628
+ .timepicker__picker {
629
+ position: absolute;
630
+ top: calc(100% + 0.5rem);
631
+ left: 0;
632
+ z-index: 50;
633
+ min-width: 16rem;
634
+ padding: var(--space-md, 1rem);
635
+ border: 1px solid var(--color-border, #e5e7eb);
636
+ border-radius: var(--radius-md, 0.375rem);
637
+ background: var(--color-bg, #ffffff);
638
+ box-shadow: var(--shadow-lg, 0 10px 15px rgba(0, 0, 0, 0.1));
639
+ animation: picker-in 150ms ease-out;
640
+ font-family: var(--font-family, system-ui, -apple-system, sans-serif);
641
+ }
642
+
643
+ @keyframes picker-in {
644
+ from {
645
+ opacity: 0;
646
+ transform: translateY(-4px);
647
+ }
648
+ to {
649
+ opacity: 1;
650
+ transform: translateY(0);
651
+ }
652
+ }
653
+
654
+ .timepicker__columns {
655
+ display: flex;
656
+ gap: var(--space-sm, 0.5rem);
657
+ margin-bottom: var(--space-md, 1rem);
658
+ }
659
+
660
+ .timepicker__column {
661
+ flex: 1;
662
+ display: flex;
663
+ flex-direction: column;
664
+ align-items: center;
665
+ }
666
+
667
+ .timepicker__column-label {
668
+ font-size: var(--text-xs, 0.75rem);
669
+ font-weight: 600;
670
+ color: var(--color-text-muted, #6b7280);
671
+ margin-bottom: var(--space-xs, 0.25rem);
672
+ text-align: center;
673
+ }
674
+
675
+ .timepicker__scroll {
676
+ width: 100%;
677
+ max-height: 12rem;
678
+ overflow-y: auto;
679
+ border: 1px solid var(--color-border, #e5e7eb);
680
+ border-radius: var(--radius-sm, 0.25rem);
681
+ scroll-snap-type: y mandatory;
682
+ scroll-behavior: smooth;
683
+ -webkit-overflow-scrolling: touch;
684
+ }
685
+
686
+ .timepicker__option {
687
+ width: 100%;
688
+ min-height: var(--touch-target-min, 44px);
689
+ padding: var(--space-sm, 0.5rem);
690
+ border: none;
691
+ background: transparent;
692
+ color: var(--color-text, #1f2937);
693
+ font-size: var(--text-sm, 0.875rem);
694
+ cursor: pointer;
695
+ transition:
696
+ background var(--transition-fast, 150ms ease),
697
+ color var(--transition-fast, 150ms ease);
698
+ scroll-snap-align: center;
699
+ -webkit-tap-highlight-color: transparent;
700
+ text-align: center;
701
+ }
702
+
703
+ .timepicker__option:hover:not(:disabled) {
704
+ background: var(--color-bg-muted, #f3f4f6);
705
+ }
706
+
707
+ .timepicker__option:focus {
708
+ outline: none;
709
+ }
710
+
711
+ .timepicker__option--focused {
712
+ box-shadow: 0 0 0 2px var(--color-primary-alpha, rgba(59, 130, 246, 0.2));
713
+ }
714
+
715
+ .timepicker__option--selected {
716
+ background: var(--color-primary, #3b82f6);
717
+ color: var(--color-text-inverse, #ffffff);
718
+ font-weight: 600;
719
+ }
720
+
721
+ .timepicker__option--selected:hover {
722
+ background: var(--color-primary, #3b82f6);
723
+ color: var(--color-text-inverse, #ffffff);
724
+ }
725
+
726
+ .timepicker__option:disabled {
727
+ color: var(--color-text-muted, #9ca3af);
728
+ cursor: not-allowed;
729
+ opacity: 0.5;
730
+ }
731
+
732
+ .timepicker__option:disabled:hover {
733
+ background: transparent;
734
+ }
735
+
736
+ /* Actions */
737
+ .timepicker__actions {
738
+ display: flex;
739
+ justify-content: flex-end;
740
+ gap: var(--space-sm, 0.5rem);
741
+ padding-top: var(--space-sm, 0.5rem);
742
+ border-top: 1px solid var(--color-border, #e5e7eb);
743
+ }
744
+
745
+ .timepicker__action-button {
746
+ min-height: var(--touch-target-min, 44px);
747
+ padding: var(--space-sm, 0.5rem) var(--space-md, 1rem);
748
+ border: none;
749
+ border-radius: var(--radius-sm, 0.25rem);
750
+ font-size: var(--text-sm, 0.875rem);
751
+ font-weight: 500;
752
+ cursor: pointer;
753
+ transition:
754
+ background var(--transition-fast, 150ms ease),
755
+ color var(--transition-fast, 150ms ease);
756
+ -webkit-tap-highlight-color: transparent;
757
+ }
758
+
759
+ .timepicker__action-button--cancel {
760
+ background: transparent;
761
+ color: var(--color-text, #1f2937);
762
+ }
763
+
764
+ .timepicker__action-button--cancel:hover {
765
+ background: var(--color-bg-muted, #f3f4f6);
766
+ }
767
+
768
+ .timepicker__action-button--confirm {
769
+ background: var(--color-primary, #3b82f6);
770
+ color: var(--color-text-inverse, #ffffff);
771
+ }
772
+
773
+ .timepicker__action-button--confirm:hover {
774
+ background: var(--color-primary-dark, #2563eb);
775
+ }
776
+
777
+ /* Mobile responsive */
778
+ @media (max-width: 640px) {
779
+ .timepicker__picker {
780
+ position: fixed;
781
+ top: auto;
782
+ bottom: 0;
783
+ left: 0;
784
+ right: 0;
785
+ min-width: 100%;
786
+ max-width: 100%;
787
+ border-radius: 1rem 1rem 0 0;
788
+ animation: picker-slide-up 250ms ease-out;
789
+ padding-bottom: calc(var(--space-md, 1rem) + env(safe-area-inset-bottom, 0));
790
+ }
791
+
792
+ @keyframes picker-slide-up {
793
+ from {
794
+ opacity: 0;
795
+ transform: translateY(100%);
796
+ }
797
+ to {
798
+ opacity: 1;
799
+ transform: translateY(0);
800
+ }
801
+ }
802
+ }
803
+ </style>