svelte-comp 1.3.3 → 1.3.6

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 (138) hide show
  1. package/LICENSE.md +21 -21
  2. package/README.md +101 -100
  3. package/dist/App.svelte +507 -507
  4. package/dist/Container.svelte +59 -59
  5. package/dist/app.css +234 -235
  6. package/dist/app.d.ts +10 -0
  7. package/dist/lib/Accordion.svelte +155 -155
  8. package/dist/lib/Badge.svelte +44 -44
  9. package/dist/lib/Button.svelte +185 -170
  10. package/dist/lib/Calendar.svelte +384 -384
  11. package/dist/lib/Card.svelte +103 -103
  12. package/dist/lib/Carousel.svelte +293 -293
  13. package/dist/lib/Carousel.svelte.d.ts +1 -1
  14. package/dist/lib/CheckBox.svelte +210 -210
  15. package/dist/lib/CodeView.svelte +308 -307
  16. package/dist/lib/ColorPicker.svelte +159 -159
  17. package/dist/lib/ContextMenu.svelte +328 -322
  18. package/dist/lib/DatePicker.svelte +246 -246
  19. package/dist/lib/Dialog.svelte +233 -233
  20. package/dist/lib/Field.svelte +299 -299
  21. package/dist/lib/FilePicker.svelte +295 -240
  22. package/dist/lib/FilePicker.svelte.d.ts +6 -1
  23. package/dist/lib/Form.svelte +438 -438
  24. package/dist/lib/Hamburger.svelte +217 -217
  25. package/dist/lib/InstallPWA.svelte +94 -94
  26. package/dist/lib/Menu.svelte +623 -623
  27. package/dist/lib/NoticeBase.svelte +140 -140
  28. package/dist/lib/PaginatedCard.svelte +73 -73
  29. package/dist/lib/Pagination.svelte +119 -119
  30. package/dist/lib/PrimaryColorSelect.svelte +111 -111
  31. package/dist/lib/ProgressBar.svelte +141 -141
  32. package/dist/lib/ProgressCircle.svelte +190 -190
  33. package/dist/lib/Radio.svelte +189 -189
  34. package/dist/lib/SearchInput.svelte +104 -104
  35. package/dist/lib/Select.svelte +524 -524
  36. package/dist/lib/Slider.svelte +253 -253
  37. package/dist/lib/Splitter.svelte +159 -150
  38. package/dist/lib/Switch.svelte +168 -167
  39. package/dist/lib/Table.svelte +299 -299
  40. package/dist/lib/Tabs.svelte +213 -213
  41. package/dist/lib/ThemeToggle.svelte +128 -127
  42. package/dist/lib/TimePicker.svelte +312 -312
  43. package/dist/lib/TimePickerNew.svelte +634 -0
  44. package/dist/lib/TimePickerNew.svelte.d.ts +49 -0
  45. package/dist/lib/Toast.svelte +123 -123
  46. package/dist/lib/Tooltip.svelte +110 -110
  47. package/dist/lib/Topbar.svelte +107 -107
  48. package/dist/lib/__tests__/Accordion.test.d.ts +1 -0
  49. package/dist/lib/__tests__/Accordion.test.js +171 -0
  50. package/dist/lib/__tests__/Badge.test.d.ts +1 -0
  51. package/dist/lib/__tests__/Badge.test.js +41 -0
  52. package/dist/lib/__tests__/Button.test.d.ts +1 -0
  53. package/dist/lib/__tests__/Button.test.js +269 -0
  54. package/dist/lib/__tests__/Calendar.test.d.ts +1 -0
  55. package/dist/lib/__tests__/Calendar.test.js +171 -0
  56. package/dist/lib/__tests__/Card.test.d.ts +1 -0
  57. package/dist/lib/__tests__/Card.test.js +148 -0
  58. package/dist/lib/__tests__/Carousel.test.d.ts +1 -0
  59. package/dist/lib/__tests__/Carousel.test.js +439 -0
  60. package/dist/lib/__tests__/CheckBox.test.d.ts +1 -0
  61. package/dist/lib/__tests__/CheckBox.test.js +152 -0
  62. package/dist/lib/__tests__/CodeView.test.d.ts +1 -0
  63. package/dist/lib/__tests__/CodeView.test.js +157 -0
  64. package/dist/lib/__tests__/ColorPicker.test.d.ts +1 -0
  65. package/dist/lib/__tests__/ColorPicker.test.js +93 -0
  66. package/dist/lib/__tests__/ContextMenu.test.d.ts +1 -0
  67. package/dist/lib/__tests__/ContextMenu.test.js +67 -0
  68. package/dist/lib/__tests__/DatePicker.test.d.ts +1 -0
  69. package/dist/lib/__tests__/DatePicker.test.js +108 -0
  70. package/dist/lib/__tests__/Dialog.test.d.ts +1 -0
  71. package/dist/lib/__tests__/Dialog.test.js +183 -0
  72. package/dist/lib/__tests__/Field.test.d.ts +1 -0
  73. package/dist/lib/__tests__/Field.test.js +190 -0
  74. package/dist/lib/__tests__/FilePicker.test.d.ts +1 -0
  75. package/dist/lib/__tests__/FilePicker.test.js +179 -0
  76. package/dist/lib/__tests__/Form.integration.test.d.ts +1 -0
  77. package/dist/lib/__tests__/Form.integration.test.js +158 -0
  78. package/dist/lib/__tests__/Form.test.d.ts +1 -0
  79. package/dist/lib/__tests__/Form.test.js +463 -0
  80. package/dist/lib/__tests__/Hamburger.test.d.ts +1 -0
  81. package/dist/lib/__tests__/Hamburger.test.js +161 -0
  82. package/dist/lib/__tests__/InstallPWA.test.d.ts +1 -0
  83. package/dist/lib/__tests__/InstallPWA.test.js +15 -0
  84. package/dist/lib/__tests__/Menu.test.d.ts +1 -0
  85. package/dist/lib/__tests__/Menu.test.js +285 -0
  86. package/dist/lib/__tests__/NoticeBase.test.d.ts +1 -0
  87. package/dist/lib/__tests__/NoticeBase.test.js +60 -0
  88. package/dist/lib/__tests__/PaginatedCard.test.d.ts +1 -0
  89. package/dist/lib/__tests__/PaginatedCard.test.js +89 -0
  90. package/dist/lib/__tests__/Pagination.test.d.ts +1 -0
  91. package/dist/lib/__tests__/Pagination.test.js +168 -0
  92. package/dist/lib/__tests__/PrimaryColorSelect.test.d.ts +1 -0
  93. package/dist/lib/__tests__/PrimaryColorSelect.test.js +92 -0
  94. package/dist/lib/__tests__/ProgressBar.test.d.ts +1 -0
  95. package/dist/lib/__tests__/ProgressBar.test.js +69 -0
  96. package/dist/lib/__tests__/ProgressCircle.test.d.ts +1 -0
  97. package/dist/lib/__tests__/ProgressCircle.test.js +71 -0
  98. package/dist/lib/__tests__/Radio.test.d.ts +1 -0
  99. package/dist/lib/__tests__/Radio.test.js +127 -0
  100. package/dist/lib/__tests__/SearchInput.test.d.ts +1 -0
  101. package/dist/lib/__tests__/SearchInput.test.js +80 -0
  102. package/dist/lib/__tests__/Select.test.d.ts +1 -0
  103. package/dist/lib/__tests__/Select.test.js +408 -0
  104. package/dist/lib/__tests__/Slider.test.d.ts +1 -0
  105. package/dist/lib/__tests__/Slider.test.js +213 -0
  106. package/dist/lib/__tests__/Splitter.test.d.ts +1 -0
  107. package/dist/lib/__tests__/Splitter.test.js +87 -0
  108. package/dist/lib/__tests__/Switch.test.d.ts +1 -0
  109. package/dist/lib/__tests__/Switch.test.js +97 -0
  110. package/dist/lib/__tests__/Table.test.d.ts +1 -0
  111. package/dist/lib/__tests__/Table.test.js +349 -0
  112. package/dist/lib/__tests__/Tabs.test.d.ts +1 -0
  113. package/dist/lib/__tests__/Tabs.test.js +262 -0
  114. package/dist/lib/__tests__/ThemeToggle.test.d.ts +1 -0
  115. package/dist/lib/__tests__/ThemeToggle.test.js +84 -0
  116. package/dist/lib/__tests__/TimePicker.test.d.ts +1 -0
  117. package/dist/lib/__tests__/TimePicker.test.js +146 -0
  118. package/dist/lib/__tests__/TimePickerNew.test.d.ts +1 -0
  119. package/dist/lib/__tests__/TimePickerNew.test.js +322 -0
  120. package/dist/lib/__tests__/Toast.test.d.ts +1 -0
  121. package/dist/lib/__tests__/Toast.test.js +135 -0
  122. package/dist/lib/__tests__/Tooltip.test.d.ts +1 -0
  123. package/dist/lib/__tests__/Tooltip.test.js +171 -0
  124. package/dist/lib/__tests__/Topbar.test.d.ts +1 -0
  125. package/dist/lib/__tests__/Topbar.test.js +25 -0
  126. package/dist/lib/__tests__/setupLangContext.d.ts +1 -0
  127. package/dist/lib/__tests__/setupLangContext.js +65 -0
  128. package/dist/lib/__tests__/storage.test.d.ts +1 -0
  129. package/dist/lib/__tests__/storage.test.js +124 -0
  130. package/dist/lib/__tests__/utils.test.d.ts +1 -0
  131. package/dist/lib/__tests__/utils.test.js +11 -0
  132. package/dist/lib/index.d.ts +1 -0
  133. package/dist/lib/index.js +1 -0
  134. package/dist/lib/lang.d.ts +4 -0
  135. package/dist/lib/lang.js +4 -0
  136. package/dist/styles.css +234 -232
  137. package/dist/utils/index.js +15 -4
  138. package/package.json +52 -52
@@ -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;