svelte-comp 1.3.3 → 1.3.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 (120) hide show
  1. package/README.md +12 -11
  2. package/dist/App.svelte +540 -540
  3. package/dist/app.css +2 -3
  4. package/dist/app.d.ts +10 -0
  5. package/dist/lib/Accordion.svelte +14 -14
  6. package/dist/lib/Button.svelte +23 -8
  7. package/dist/lib/Card.svelte +6 -6
  8. package/dist/lib/Carousel.svelte +16 -16
  9. package/dist/lib/Carousel.svelte.d.ts +1 -1
  10. package/dist/lib/CheckBox.svelte +2 -2
  11. package/dist/lib/CodeView.svelte +6 -5
  12. package/dist/lib/ContextMenu.svelte +19 -13
  13. package/dist/lib/Dialog.svelte +3 -3
  14. package/dist/lib/Field.svelte +1 -1
  15. package/dist/lib/FilePicker.svelte +66 -11
  16. package/dist/lib/FilePicker.svelte.d.ts +6 -1
  17. package/dist/lib/Hamburger.svelte +12 -12
  18. package/dist/lib/Menu.svelte +18 -18
  19. package/dist/lib/NoticeBase.svelte +5 -5
  20. package/dist/lib/Select.svelte +2 -2
  21. package/dist/lib/Slider.svelte +1 -1
  22. package/dist/lib/Splitter.svelte +15 -6
  23. package/dist/lib/Switch.svelte +5 -4
  24. package/dist/lib/Tabs.svelte +6 -6
  25. package/dist/lib/ThemeToggle.svelte +1 -0
  26. package/dist/lib/TimePickerNew.svelte +634 -0
  27. package/dist/lib/TimePickerNew.svelte.d.ts +49 -0
  28. package/dist/lib/Tooltip.svelte +7 -7
  29. package/dist/lib/Topbar.svelte +6 -6
  30. package/dist/lib/__tests__/Accordion.test.d.ts +1 -0
  31. package/dist/lib/__tests__/Accordion.test.js +171 -0
  32. package/dist/lib/__tests__/Badge.test.d.ts +1 -0
  33. package/dist/lib/__tests__/Badge.test.js +41 -0
  34. package/dist/lib/__tests__/Button.test.d.ts +1 -0
  35. package/dist/lib/__tests__/Button.test.js +269 -0
  36. package/dist/lib/__tests__/Calendar.test.d.ts +1 -0
  37. package/dist/lib/__tests__/Calendar.test.js +171 -0
  38. package/dist/lib/__tests__/Card.test.d.ts +1 -0
  39. package/dist/lib/__tests__/Card.test.js +148 -0
  40. package/dist/lib/__tests__/Carousel.test.d.ts +1 -0
  41. package/dist/lib/__tests__/Carousel.test.js +439 -0
  42. package/dist/lib/__tests__/CheckBox.test.d.ts +1 -0
  43. package/dist/lib/__tests__/CheckBox.test.js +152 -0
  44. package/dist/lib/__tests__/CodeView.test.d.ts +1 -0
  45. package/dist/lib/__tests__/CodeView.test.js +157 -0
  46. package/dist/lib/__tests__/ColorPicker.test.d.ts +1 -0
  47. package/dist/lib/__tests__/ColorPicker.test.js +93 -0
  48. package/dist/lib/__tests__/ContextMenu.test.d.ts +1 -0
  49. package/dist/lib/__tests__/ContextMenu.test.js +67 -0
  50. package/dist/lib/__tests__/DatePicker.test.d.ts +1 -0
  51. package/dist/lib/__tests__/DatePicker.test.js +108 -0
  52. package/dist/lib/__tests__/Dialog.test.d.ts +1 -0
  53. package/dist/lib/__tests__/Dialog.test.js +183 -0
  54. package/dist/lib/__tests__/Field.test.d.ts +1 -0
  55. package/dist/lib/__tests__/Field.test.js +190 -0
  56. package/dist/lib/__tests__/FilePicker.test.d.ts +1 -0
  57. package/dist/lib/__tests__/FilePicker.test.js +179 -0
  58. package/dist/lib/__tests__/Form.integration.test.d.ts +1 -0
  59. package/dist/lib/__tests__/Form.integration.test.js +158 -0
  60. package/dist/lib/__tests__/Form.test.d.ts +1 -0
  61. package/dist/lib/__tests__/Form.test.js +463 -0
  62. package/dist/lib/__tests__/Hamburger.test.d.ts +1 -0
  63. package/dist/lib/__tests__/Hamburger.test.js +161 -0
  64. package/dist/lib/__tests__/InstallPWA.test.d.ts +1 -0
  65. package/dist/lib/__tests__/InstallPWA.test.js +15 -0
  66. package/dist/lib/__tests__/Menu.test.d.ts +1 -0
  67. package/dist/lib/__tests__/Menu.test.js +285 -0
  68. package/dist/lib/__tests__/NoticeBase.test.d.ts +1 -0
  69. package/dist/lib/__tests__/NoticeBase.test.js +60 -0
  70. package/dist/lib/__tests__/PaginatedCard.test.d.ts +1 -0
  71. package/dist/lib/__tests__/PaginatedCard.test.js +89 -0
  72. package/dist/lib/__tests__/Pagination.test.d.ts +1 -0
  73. package/dist/lib/__tests__/Pagination.test.js +168 -0
  74. package/dist/lib/__tests__/PrimaryColorSelect.test.d.ts +1 -0
  75. package/dist/lib/__tests__/PrimaryColorSelect.test.js +92 -0
  76. package/dist/lib/__tests__/ProgressBar.test.d.ts +1 -0
  77. package/dist/lib/__tests__/ProgressBar.test.js +69 -0
  78. package/dist/lib/__tests__/ProgressCircle.test.d.ts +1 -0
  79. package/dist/lib/__tests__/ProgressCircle.test.js +71 -0
  80. package/dist/lib/__tests__/Radio.test.d.ts +1 -0
  81. package/dist/lib/__tests__/Radio.test.js +127 -0
  82. package/dist/lib/__tests__/SearchInput.test.d.ts +1 -0
  83. package/dist/lib/__tests__/SearchInput.test.js +80 -0
  84. package/dist/lib/__tests__/Select.test.d.ts +1 -0
  85. package/dist/lib/__tests__/Select.test.js +408 -0
  86. package/dist/lib/__tests__/Slider.test.d.ts +1 -0
  87. package/dist/lib/__tests__/Slider.test.js +213 -0
  88. package/dist/lib/__tests__/Splitter.test.d.ts +1 -0
  89. package/dist/lib/__tests__/Splitter.test.js +87 -0
  90. package/dist/lib/__tests__/Switch.test.d.ts +1 -0
  91. package/dist/lib/__tests__/Switch.test.js +97 -0
  92. package/dist/lib/__tests__/Table.test.d.ts +1 -0
  93. package/dist/lib/__tests__/Table.test.js +349 -0
  94. package/dist/lib/__tests__/Tabs.test.d.ts +1 -0
  95. package/dist/lib/__tests__/Tabs.test.js +262 -0
  96. package/dist/lib/__tests__/ThemeToggle.test.d.ts +1 -0
  97. package/dist/lib/__tests__/ThemeToggle.test.js +84 -0
  98. package/dist/lib/__tests__/TimePicker.test.d.ts +1 -0
  99. package/dist/lib/__tests__/TimePicker.test.js +146 -0
  100. package/dist/lib/__tests__/TimePickerNew.test.d.ts +1 -0
  101. package/dist/lib/__tests__/TimePickerNew.test.js +322 -0
  102. package/dist/lib/__tests__/Toast.test.d.ts +1 -0
  103. package/dist/lib/__tests__/Toast.test.js +135 -0
  104. package/dist/lib/__tests__/Tooltip.test.d.ts +1 -0
  105. package/dist/lib/__tests__/Tooltip.test.js +171 -0
  106. package/dist/lib/__tests__/Topbar.test.d.ts +1 -0
  107. package/dist/lib/__tests__/Topbar.test.js +25 -0
  108. package/dist/lib/__tests__/setupLangContext.d.ts +1 -0
  109. package/dist/lib/__tests__/setupLangContext.js +65 -0
  110. package/dist/lib/__tests__/storage.test.d.ts +1 -0
  111. package/dist/lib/__tests__/storage.test.js +124 -0
  112. package/dist/lib/__tests__/utils.test.d.ts +1 -0
  113. package/dist/lib/__tests__/utils.test.js +11 -0
  114. package/dist/lib/index.d.ts +1 -0
  115. package/dist/lib/index.js +1 -0
  116. package/dist/lib/lang.d.ts +4 -0
  117. package/dist/lib/lang.js +4 -0
  118. package/dist/styles.css +2 -0
  119. package/dist/utils/index.js +15 -4
  120. package/package.json +12 -12
@@ -0,0 +1,634 @@
1
+ <!-- src/lib/TimePickerNew.svelte -->
2
+ <script lang="ts">
3
+ /**
4
+ * @component TimePickerNew
5
+ * @description Second time picker implementation with improved functionality and UI.
6
+ *
7
+ * @prop value {string | null} - Stored time in ISO `HH:MM` (bindable)
8
+ * @default null
9
+ *
10
+ * @prop step {number} - Step in seconds
11
+ * @default 60
12
+ *
13
+ * @prop label {string} - Label text
14
+ *
15
+ * @prop placeholder {string} - Placeholder when value is null
16
+ *
17
+ * @prop disabled {boolean} - Disable all interactions
18
+ * @default false
19
+ *
20
+ * @prop clearable {boolean} - Show clear action
21
+ * @default true
22
+ *
23
+ * @prop initialSystem {"iso" | "english"} - Picker mode (24h vs 12h)
24
+ * @default "iso"
25
+ *
26
+ * @prop onChange {(value: string | null) => void} - Fired when value changes
27
+ *
28
+ * @prop class {string} - Wrapper classes
29
+ * @default ""
30
+ *
31
+ * @note ISO mode uses 24-hour time; English mode uses 12-hour time with AM/PM
32
+ * @note The stored value is always ISO (`HH:MM`)
33
+ * @note `step` defines the minute grid, derived from seconds
34
+ * @note No locale or date-formatting APIs are used internally
35
+ */
36
+
37
+ import type { HTMLAttributes } from "svelte/elements";
38
+ import Button from "./Button.svelte";
39
+ import { cx } from "../utils";
40
+ import { getComponentText, getLangContext, getLangKey } from "./lang-context";
41
+
42
+ type TimeSystem = "iso" | "english";
43
+ type Period = "AM" | "PM";
44
+
45
+ type Props = HTMLAttributes<HTMLDivElement> & {
46
+ value?: string | null;
47
+ step?: number;
48
+ label?: string;
49
+ placeholder?: string;
50
+ disabled?: boolean;
51
+ clearable?: boolean;
52
+ initialSystem?: TimeSystem;
53
+ onChange?: (value: string | null) => void;
54
+ class?: string;
55
+ };
56
+
57
+ let {
58
+ value = $bindable<string | null>(null),
59
+ step = 60,
60
+ label,
61
+ placeholder,
62
+ disabled = false,
63
+ clearable = true,
64
+ initialSystem = "iso",
65
+ onChange,
66
+ class: externalClass = "",
67
+ ...rest
68
+ }: Props = $props();
69
+
70
+ const langCtx = getLangContext();
71
+ const langKey = $derived(getLangKey(langCtx));
72
+ const L = $derived(getComponentText("timePicker", langKey));
73
+
74
+ const labelFinal = $derived(label ?? L.text);
75
+ const placeholderFinal = $derived(placeholder ?? L.placeholder);
76
+
77
+ let triggerEl = $state<HTMLButtonElement | null>(null);
78
+ let popupEl = $state<HTMLDivElement | null>(null);
79
+ let popupStyle = $state("");
80
+ let open = $state(false);
81
+
82
+ let timeSystem = $state<TimeSystem>("iso");
83
+ let didInitSystem = $state(false);
84
+
85
+ let hour = $state("00");
86
+ let minute = $state("00");
87
+ let period = $state<Period>("AM");
88
+
89
+ const hasValue = $derived(value != null);
90
+ const pickerClass = $derived(
91
+ cx("relative inline-block w-full", externalClass),
92
+ );
93
+
94
+ const isoHours = Array.from({ length: 24 }, (_, index) => {
95
+ const item = index.toString().padStart(2, "0");
96
+ return { value: item, label: item };
97
+ });
98
+
99
+ const englishHours = Array.from({ length: 12 }, (_, index) => {
100
+ const item = (index + 1).toString().padStart(2, "0");
101
+ return { value: item, label: item };
102
+ });
103
+
104
+ const periodOptions: Array<{ value: Period; label: Period }> = [
105
+ { value: "AM", label: "AM" },
106
+ { value: "PM", label: "PM" },
107
+ ];
108
+
109
+ const minuteIncrement = $derived(
110
+ !step || step <= 0 ? 1 : Math.min(60, Math.max(1, Math.round(step / 60))),
111
+ );
112
+
113
+ const minuteOptions = $derived.by(() => {
114
+ const options: Array<{ value: string; label: string }> = [];
115
+
116
+ for (let index = 0; index < 60; index += minuteIncrement) {
117
+ const item = index.toString().padStart(2, "0");
118
+ options.push({ value: item, label: item });
119
+ }
120
+
121
+ if (!options.some((item) => item.value === minute)) {
122
+ options.push({ value: minute, label: minute });
123
+ options.sort((a, b) => Number(a.value) - Number(b.value));
124
+ }
125
+
126
+ return options;
127
+ });
128
+
129
+ const hourOptions = $derived(timeSystem === "iso" ? isoHours : englishHours);
130
+
131
+ const displayValue = $derived.by(() => {
132
+ if (!value) return "";
133
+
134
+ const parsed = parseTimeValue(value);
135
+ if (!parsed) return "";
136
+
137
+ if (timeSystem === "english") {
138
+ const mapped = toEnglishHour(parsed.hour);
139
+ return `${mapped.hour}:${parsed.minute} ${mapped.period}`;
140
+ }
141
+
142
+ return `${parsed.hour}:${parsed.minute}`;
143
+ });
144
+
145
+ const triggerText = $derived(hasValue ? displayValue : placeholderFinal);
146
+
147
+ function normalizeNumberPart(
148
+ raw: string | number | undefined | null,
149
+ fallback: number,
150
+ min: number,
151
+ max: number,
152
+ ) {
153
+ const parsed = Number.parseInt(String(raw ?? ""), 10);
154
+ const safe = Number.isFinite(parsed) ? parsed : fallback;
155
+ const clamped = Math.min(max, Math.max(min, safe));
156
+
157
+ return clamped.toString().padStart(2, "0");
158
+ }
159
+
160
+ function normalizeHour24(raw: string | number | undefined | null) {
161
+ return normalizeNumberPart(raw, 0, 0, 23);
162
+ }
163
+
164
+ function normalizeHour12(raw: string | number | undefined | null) {
165
+ return normalizeNumberPart(raw, 12, 1, 12);
166
+ }
167
+
168
+ function normalizeMinute(raw: string | number | undefined | null) {
169
+ return normalizeNumberPart(raw, 0, 0, 59);
170
+ }
171
+
172
+ function normalizePeriod(raw: string | undefined | null): Period {
173
+ return raw === "PM" ? "PM" : "AM";
174
+ }
175
+
176
+ function toIsoHour(h: string, p: Period) {
177
+ const numeric = Number.parseInt(h, 10);
178
+
179
+ if (!Number.isFinite(numeric)) {
180
+ return "00";
181
+ }
182
+
183
+ const base = numeric % 12;
184
+ const withPeriod = p === "PM" ? base + 12 : base;
185
+
186
+ return normalizeHour24(withPeriod);
187
+ }
188
+
189
+ function toEnglishHour(isoHour: string): { hour: string; period: Period } {
190
+ const numeric = Number.parseInt(isoHour, 10);
191
+
192
+ if (!Number.isFinite(numeric)) {
193
+ return { hour: "12", period: "AM" };
194
+ }
195
+
196
+ const periodValue: Period = numeric >= 12 ? "PM" : "AM";
197
+ const normalized = numeric % 12 || 12;
198
+
199
+ return {
200
+ hour: normalizeHour12(normalized),
201
+ period: periodValue,
202
+ };
203
+ }
204
+
205
+ function parseTimeValue(rawValue: string | null) {
206
+ if (!rawValue) return null;
207
+
208
+ const trimmed = rawValue.trim();
209
+ const parsedPeriod: Period | null = trimmed.includes("PM")
210
+ ? "PM"
211
+ : trimmed.includes("AM")
212
+ ? "AM"
213
+ : null;
214
+
215
+ const withoutPeriod = trimmed.replace(/\s?(AM|PM)$/u, "");
216
+ const [rawHour, rawMinute] = withoutPeriod.split(":");
217
+
218
+ if (parsedPeriod) {
219
+ return {
220
+ hour: toIsoHour(normalizeHour12(rawHour), parsedPeriod),
221
+ minute: normalizeMinute(rawMinute),
222
+ };
223
+ }
224
+
225
+ return {
226
+ hour: normalizeHour24(rawHour),
227
+ minute: normalizeMinute(rawMinute),
228
+ };
229
+ }
230
+
231
+ function emitCurrent() {
232
+ const isoHour =
233
+ timeSystem === "english"
234
+ ? toIsoHour(hour, period)
235
+ : normalizeHour24(hour);
236
+
237
+ const nextValue = `${isoHour}:${normalizeMinute(minute)}`;
238
+
239
+ value = nextValue;
240
+ onChange?.(nextValue);
241
+ }
242
+
243
+ function syncStateFromIso(isoHour: string, isoMinute: string) {
244
+ if (timeSystem === "english") {
245
+ const mapped = toEnglishHour(isoHour);
246
+ hour = mapped.hour;
247
+ period = mapped.period;
248
+ } else {
249
+ hour = normalizeHour24(isoHour);
250
+ period = toEnglishHour(isoHour).period;
251
+ }
252
+
253
+ minute = normalizeMinute(isoMinute);
254
+ }
255
+
256
+ function updatePopupPosition() {
257
+ if (!triggerEl || typeof window === "undefined") return;
258
+
259
+ const rect = triggerEl.getBoundingClientRect();
260
+ const margin = 8;
261
+ const gap = 4;
262
+ const preferredWidth = Math.max(rect.width, 168);
263
+ const left = Math.min(
264
+ Math.max(margin, rect.left),
265
+ Math.max(margin, window.innerWidth - preferredWidth - margin),
266
+ );
267
+ const availableBelow = window.innerHeight - rect.bottom - gap - margin;
268
+ const availableAbove = rect.top - gap - margin;
269
+ const placeAbove = availableBelow < 180 && availableAbove > availableBelow;
270
+ const maxHeight = Math.max(
271
+ 160,
272
+ placeAbove ? availableAbove : availableBelow,
273
+ );
274
+ const top = placeAbove
275
+ ? Math.max(margin, rect.top - gap - Math.min(272, maxHeight))
276
+ : rect.bottom + gap;
277
+
278
+ popupStyle = [
279
+ `position: fixed`,
280
+ `left: ${left}px`,
281
+ `top: ${top}px`,
282
+ `width: ${preferredWidth}px`,
283
+ `max-height: ${maxHeight}px`,
284
+ ].join("; ");
285
+ }
286
+
287
+ function focusSelectedOption() {
288
+ window.requestAnimationFrame(() => {
289
+ updatePopupPosition();
290
+
291
+ const selected = popupEl?.querySelector<HTMLButtonElement>(
292
+ "button[data-selected='true']",
293
+ );
294
+
295
+ selected?.focus();
296
+ });
297
+ }
298
+
299
+ function openPicker() {
300
+ if (disabled) return;
301
+
302
+ open = true;
303
+ focusSelectedOption();
304
+ }
305
+
306
+ function closePicker() {
307
+ open = false;
308
+ triggerEl?.focus();
309
+ }
310
+
311
+ function toggleOpen() {
312
+ if (open) {
313
+ closePicker();
314
+ return;
315
+ }
316
+
317
+ openPicker();
318
+ }
319
+
320
+ function selectHour(nextHour: string) {
321
+ hour =
322
+ timeSystem === "english"
323
+ ? normalizeHour12(nextHour)
324
+ : normalizeHour24(nextHour);
325
+
326
+ emitCurrent();
327
+ }
328
+
329
+ function selectMinute(nextMinute: string) {
330
+ minute = normalizeMinute(nextMinute);
331
+ emitCurrent();
332
+ }
333
+
334
+ function selectPeriod(nextPeriod: string) {
335
+ period = normalizePeriod(nextPeriod);
336
+ emitCurrent();
337
+ }
338
+
339
+ function toggleSystem() {
340
+ if (disabled) return;
341
+
342
+ if (timeSystem === "iso") {
343
+ timeSystem = "english";
344
+
345
+ const mapped = toEnglishHour(hour);
346
+ hour = mapped.hour;
347
+ period = mapped.period;
348
+ } else {
349
+ timeSystem = "iso";
350
+ hour = toIsoHour(hour, period);
351
+ period = "AM";
352
+ }
353
+
354
+ emitCurrent();
355
+ focusSelectedOption();
356
+ }
357
+
358
+ function clearSelection() {
359
+ if (!clearable || disabled) return;
360
+
361
+ hour = timeSystem === "english" ? "12" : "00";
362
+ minute = "00";
363
+ period = "AM";
364
+ value = null;
365
+ onChange?.(null);
366
+ open = false;
367
+ triggerEl?.focus();
368
+ }
369
+
370
+ function snapMinute(rawMinute: number) {
371
+ if (minuteIncrement >= 60) return 0;
372
+
373
+ return Math.floor(rawMinute / minuteIncrement) * minuteIncrement;
374
+ }
375
+
376
+ function selectNow() {
377
+ if (disabled) return;
378
+
379
+ const now = new Date();
380
+ const nextHour = normalizeHour24(now.getHours());
381
+ const nextMinute = normalizeMinute(snapMinute(now.getMinutes()));
382
+
383
+ syncStateFromIso(nextHour, nextMinute);
384
+
385
+ value = `${nextHour}:${nextMinute}`;
386
+ onChange?.(value);
387
+ }
388
+
389
+ function handleWindowClick(event: MouseEvent) {
390
+ if (!open) return;
391
+
392
+ const target = event.target;
393
+
394
+ if (!(target instanceof Node)) return;
395
+ if (triggerEl?.contains(target) || popupEl?.contains(target)) return;
396
+
397
+ open = false;
398
+ }
399
+
400
+ function handleWindowKeydown(event: KeyboardEvent) {
401
+ if (!open) return;
402
+
403
+ if (event.key === "Escape") {
404
+ event.preventDefault();
405
+ closePicker();
406
+ }
407
+ }
408
+
409
+ $effect(() => {
410
+ if (didInitSystem) return;
411
+
412
+ didInitSystem = true;
413
+ timeSystem = initialSystem;
414
+ hour = initialSystem === "english" ? "12" : "00";
415
+ });
416
+
417
+ $effect(() => {
418
+ const parsed = parseTimeValue(value);
419
+
420
+ if (!parsed) return;
421
+
422
+ syncStateFromIso(parsed.hour, parsed.minute);
423
+ });
424
+
425
+ $effect(() => {
426
+ if (!open) return;
427
+
428
+ updatePopupPosition();
429
+
430
+ const handleViewportChange = () => updatePopupPosition();
431
+
432
+ window.addEventListener("resize", handleViewportChange);
433
+ window.addEventListener("scroll", handleViewportChange, true);
434
+
435
+ return () => {
436
+ window.removeEventListener("resize", handleViewportChange);
437
+ window.removeEventListener("scroll", handleViewportChange, true);
438
+ };
439
+ });
440
+ </script>
441
+
442
+ <svelte:window onclick={handleWindowClick} onkeydown={handleWindowKeydown} />
443
+
444
+ <div class={pickerClass} {...rest}>
445
+ <div
446
+ class="mb-[var(--spacing-sm)] text-[length:var(--text-md)] [font-weight:var(--font-weight-medium)] text-[var(--color-text-default)]"
447
+ >
448
+ {labelFinal}
449
+ </div>
450
+
451
+ <button
452
+ bind:this={triggerEl}
453
+ type="button"
454
+ class={cx(
455
+ "flex min-h-11 w-full min-w-0 items-center justify-between gap-[var(--spacing-sm)] rounded-[var(--radius-md)] border border-[var(--border-color-default)] bg-[var(--color-bg-surface)] px-[var(--spacing-sm)] py-[var(--spacing-sm)] text-left text-[length:var(--text-sm)] text-[var(--color-text-default)] shadow-[0_1px_2px_var(--shadow-color)] transition-[border-color,box-shadow,background-color] duration-[var(--transition-fast)] ease-[var(--timing-default)]",
456
+ "hover:bg-[var(--color-bg-hover)] focus:border-[var(--border-color-focus)] focus:outline-none focus:ring-2 focus:ring-[var(--border-color-focus)]/30",
457
+ disabled && "cursor-not-allowed opacity-[var(--opacity-disabled)]",
458
+ !hasValue && "text-[var(--color-text-muted)]",
459
+ )}
460
+ aria-label={labelFinal}
461
+ aria-haspopup="dialog"
462
+ aria-expanded={open}
463
+ {disabled}
464
+ onclick={toggleOpen}
465
+ >
466
+ <span class="min-w-0 truncate">{triggerText}</span>
467
+
468
+ <svg
469
+ class="size-4 shrink-0 text-[var(--color-text-muted)]"
470
+ viewBox="0 0 24 24"
471
+ fill="none"
472
+ aria-hidden="true"
473
+ >
474
+ <circle cx="12" cy="12" r="9" stroke="currentColor" stroke-width="1.8" />
475
+ <path
476
+ d="M12 7v5l3 2"
477
+ stroke="currentColor"
478
+ stroke-width="1.8"
479
+ stroke-linecap="round"
480
+ stroke-linejoin="round"
481
+ />
482
+ </svg>
483
+ </button>
484
+
485
+ {#if open}
486
+ <div
487
+ bind:this={popupEl}
488
+ role="dialog"
489
+ aria-label={labelFinal}
490
+ class="z-[var(--z-dropdown)] overflow-hidden rounded-[var(--radius-lg)] border border-[var(--border-color-default)] bg-[var(--color-bg-surface)] shadow-[0_12px_32px_var(--shadow-color)]"
491
+ style={popupStyle}
492
+ >
493
+ <div
494
+ class={cx(
495
+ "grid min-h-0 overflow-hidden",
496
+ timeSystem === "english" ? "grid-cols-3" : "grid-cols-2",
497
+ )}
498
+ >
499
+ <section class="min-w-0 border-r border-[var(--border-color-default)]">
500
+ <div
501
+ class="sticky top-0 z-[var(--z-base)] border-b border-[var(--border-color-default)] bg-[var(--color-bg-surface)] px-[var(--spacing-sm)] py-[var(--spacing-xs)] text-[length:var(--text-xs)] text-[var(--color-text-muted)]"
502
+ >
503
+ {L.hour}
504
+ </div>
505
+
506
+ <div class="max-h-56 overflow-y-auto py-[var(--spacing-xs)]">
507
+ {#each hourOptions as option (option.value)}
508
+ <button
509
+ type="button"
510
+ class={cx(
511
+ "block min-h-9 w-full px-[var(--spacing-sm)] text-left text-[length:var(--text-sm)] transition-colors duration-[var(--transition-fast)] ease-[var(--timing-default)]",
512
+ option.value === hour
513
+ ? "bg-[var(--color-bg-primary)] text-[var(--color-text-inverse)]"
514
+ : "text-[var(--color-text-default)] hover:bg-[var(--color-bg-hover)]",
515
+ )}
516
+ data-selected={option.value === hour}
517
+ onclick={() => selectHour(option.value)}
518
+ >
519
+ {option.label}
520
+ </button>
521
+ {/each}
522
+ </div>
523
+ </section>
524
+
525
+ <section
526
+ class={cx(
527
+ "min-w-0",
528
+ timeSystem === "english" &&
529
+ "border-r border-[var(--border-color-default)]",
530
+ )}
531
+ >
532
+ <div
533
+ class="sticky top-0 z-[var(--z-base)] border-b border-[var(--border-color-default)] bg-[var(--color-bg-surface)] px-[var(--spacing-sm)] py-[var(--spacing-xs)] text-[length:var(--text-xs)] text-[var(--color-text-muted)]"
534
+ >
535
+ {L.minute}
536
+ </div>
537
+
538
+ <div class="max-h-56 overflow-y-auto py-[var(--spacing-xs)]">
539
+ {#each minuteOptions as option (option.value)}
540
+ <button
541
+ type="button"
542
+ class={cx(
543
+ "block min-h-9 w-full px-[var(--spacing-sm)] text-left text-[length:var(--text-sm)] transition-colors duration-[var(--transition-fast)] ease-[var(--timing-default)]",
544
+ option.value === minute
545
+ ? "bg-[var(--color-bg-primary)] text-[var(--color-text-inverse)]"
546
+ : "text-[var(--color-text-default)] hover:bg-[var(--color-bg-hover)]",
547
+ )}
548
+ data-selected={option.value === minute}
549
+ onclick={() => selectMinute(option.value)}
550
+ >
551
+ {option.label}
552
+ </button>
553
+ {/each}
554
+ </div>
555
+ </section>
556
+
557
+ {#if timeSystem === "english"}
558
+ <section class="min-w-0">
559
+ <div
560
+ class="sticky top-0 z-[var(--z-base)] border-b border-[var(--border-color-default)] bg-[var(--color-bg-surface)] px-[var(--spacing-sm)] py-[var(--spacing-xs)] text-[length:var(--text-xs)] text-[var(--color-text-muted)]"
561
+ >
562
+ {L.period}
563
+ </div>
564
+
565
+ <div class="py-[var(--spacing-xs)]">
566
+ {#each periodOptions as option (option.value)}
567
+ <button
568
+ type="button"
569
+ class={cx(
570
+ "block min-h-9 w-full px-[var(--spacing-sm)] text-left text-[length:var(--text-sm)] transition-colors duration-[var(--transition-fast)] ease-[var(--timing-default)]",
571
+ option.value === period
572
+ ? "bg-[var(--color-bg-primary)] text-[var(--color-text-inverse)]"
573
+ : "text-[var(--color-text-default)] hover:bg-[var(--color-bg-hover)]",
574
+ )}
575
+ data-selected={option.value === period}
576
+ onclick={() => selectPeriod(option.value)}
577
+ >
578
+ {option.label}
579
+ </button>
580
+ {/each}
581
+ </div>
582
+ </section>
583
+ {/if}
584
+ </div>
585
+
586
+ <div
587
+ class="flex flex-wrap items-center justify-between gap-[var(--spacing-xs)] border-t border-[var(--border-color-default)] bg-[var(--color-bg-surface)] p-[var(--spacing-sm)]"
588
+ >
589
+ <div class="flex flex-wrap items-center gap-[var(--spacing-xs)]">
590
+ <Button variant="ghost" sz="xs" onClick={selectNow} {disabled}>
591
+ {L.now}
592
+ </Button>
593
+
594
+ <Button variant="ghost" sz="xs" onClick={toggleSystem} {disabled}>
595
+ {timeSystem === "iso" ? L.switchTo12h : L.switchTo24h}
596
+ </Button>
597
+
598
+ {#if clearable}
599
+ <Button
600
+ variant="danger"
601
+ sz="xs"
602
+ onClick={clearSelection}
603
+ disabled={!hasValue || disabled}
604
+ >
605
+ {L.clear}
606
+ </Button>
607
+ {/if}
608
+ </div>
609
+
610
+ <Button variant="secondary" sz="xs" onClick={closePicker}>{L.ok}</Button>
611
+ </div>
612
+ </div>
613
+ {/if}
614
+
615
+ <div
616
+ class="mt-[var(--spacing-md)] rounded-[var(--radius-lg)] border border-[var(--border-color-default)] bg-[var(--color-bg-surface)] p-[var(--spacing-md)] text-center"
617
+ >
618
+ <p
619
+ class="text-[length:var(--text-xs)] uppercase tracking-[var(--letter-spacing-wide)] text-[var(--color-text-muted)]"
620
+ >
621
+ {L.selectedTime}
622
+ </p>
623
+
624
+ <p
625
+ class="mt-[var(--spacing-xs)] text-[length:var(--text-sm)] [font-weight:var(--font-weight-semibold)] text-[var(--color-text-default)]"
626
+ >
627
+ {#if hasValue}
628
+ {displayValue}
629
+ {:else}
630
+ {placeholderFinal}
631
+ {/if}
632
+ </p>
633
+ </div>
634
+ </div>
@@ -0,0 +1,49 @@
1
+ /**
2
+ * @component TimePickerNew
3
+ * @description Second time picker implementation with improved functionality and UI.
4
+ *
5
+ * @prop value {string | null} - Stored time in ISO `HH:MM` (bindable)
6
+ * @default null
7
+ *
8
+ * @prop step {number} - Step in seconds
9
+ * @default 60
10
+ *
11
+ * @prop label {string} - Label text
12
+ *
13
+ * @prop placeholder {string} - Placeholder when value is null
14
+ *
15
+ * @prop disabled {boolean} - Disable all interactions
16
+ * @default false
17
+ *
18
+ * @prop clearable {boolean} - Show clear action
19
+ * @default true
20
+ *
21
+ * @prop initialSystem {"iso" | "english"} - Picker mode (24h vs 12h)
22
+ * @default "iso"
23
+ *
24
+ * @prop onChange {(value: string | null) => void} - Fired when value changes
25
+ *
26
+ * @prop class {string} - Wrapper classes
27
+ * @default ""
28
+ *
29
+ * @note ISO mode uses 24-hour time; English mode uses 12-hour time with AM/PM
30
+ * @note The stored value is always ISO (`HH:MM`)
31
+ * @note `step` defines the minute grid, derived from seconds
32
+ * @note No locale or date-formatting APIs are used internally
33
+ */
34
+ import type { HTMLAttributes } from "svelte/elements";
35
+ type TimeSystem = "iso" | "english";
36
+ type Props = HTMLAttributes<HTMLDivElement> & {
37
+ value?: string | null;
38
+ step?: number;
39
+ label?: string;
40
+ placeholder?: string;
41
+ disabled?: boolean;
42
+ clearable?: boolean;
43
+ initialSystem?: TimeSystem;
44
+ onChange?: (value: string | null) => void;
45
+ class?: string;
46
+ };
47
+ declare const TimePickerNew: import("svelte").Component<Props, {}, "value">;
48
+ type TimePickerNew = ReturnType<typeof TimePickerNew>;
49
+ export default TimePickerNew;
@@ -74,19 +74,19 @@
74
74
  });
75
75
 
76
76
  const positionClass: Record<Position, string> = {
77
- top: "bottom-full left-1/2 -translate-x-1/2 mb-2",
78
- bottom: "top-full left-1/2 -translate-x-1/2 mt-2",
79
- left: "right-full top-1/2 -translate-y-1/2 mr-2",
80
- right: "left-full top-1/2 -translate-y-1/2 ml-2",
77
+ top: "bottom-full left-1/2 -translate-x-1/2 mb-[var(--spacing-sm)]",
78
+ bottom: "top-full left-1/2 -translate-x-1/2 mt-[var(--spacing-sm)]",
79
+ left: "right-full top-1/2 -translate-y-1/2 mr-[var(--spacing-sm)]",
80
+ right: "left-full top-1/2 -translate-y-1/2 ml-[var(--spacing-sm)]",
81
81
  };
82
82
 
83
83
  const rootClass = $derived(cx("relative inline-block", externalClass));
84
84
 
85
85
  const bubbleClass = $derived(
86
86
  cx(
87
- "absolute z-20 p-0.5 italic text-xs font-medium whitespace-nowrap rounded-[var(--radius-xs)]",
88
- "bg-[var(--color-bg-surface)] text-[var(--color-text-default)] shadow-lg border border-[var(--border-color-default)]",
89
- "transition-opacity duration-150 ease-linear",
87
+ "absolute z-20 p-[calc(var(--spacing-xs)/2)] italic [font-size:var(--text-xs)] font-medium whitespace-nowrap rounded-[var(--radius-sm)]",
88
+ "bg-[var(--color-bg-surface)] text-[var(--color-text-default)] shadow-[0_8px_16px_var(--shadow-color)] border border-[var(--border-color-default)]",
89
+ "transition-opacity duration-[var(--transition-fast)] ease-linear",
90
90
  visible ? "opacity-100" : "opacity-0 pointer-events-none"
91
91
  )
92
92
  );