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
@@ -1,312 +1,312 @@
1
- <!-- src/lib/TimePicker.svelte -->
2
- <script lang="ts">
3
- /**
4
- * @component TimePicker
5
- * @description Simple time selector that stores values in ISO `HH:MM` format. Supports a fixed step and two display systems.
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 the clear button
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
- import type { HTMLAttributes } from "svelte/elements";
37
- import Button from "./Button.svelte";
38
- import Select from "./Select.svelte";
39
- import { cx } from "../utils";
40
- import { getComponentText, getLangContext, getLangKey } from "./lang-context";
41
-
42
- type Props = HTMLAttributes<HTMLDivElement> & {
43
- value?: string | null;
44
- step?: number;
45
- label?: string;
46
- placeholder?: string;
47
- disabled?: boolean;
48
- clearable?: boolean;
49
- initialSystem?: "iso" | "english";
50
- onChange?: (value: string | null) => void;
51
- class?: string;
52
- };
53
- type Period = "AM" | "PM";
54
-
55
- let {
56
- value = $bindable<string | null>(null),
57
- step = 60,
58
- label,
59
- placeholder,
60
- disabled = false,
61
- clearable = true,
62
- initialSystem = "iso",
63
- onChange,
64
- class: externalClass = "",
65
- ...rest
66
- }: Props = $props();
67
-
68
- const langCtx = getLangContext();
69
- const langKey = $derived(getLangKey(langCtx));
70
- const L = $derived(getComponentText("timePicker", langKey));
71
-
72
- const labelFinal = $derived(label ?? L.text);
73
- const placeholderFinal = $derived(placeholder ?? L.placeholder);
74
-
75
- const pickerClass = $derived(cx("inline-block w-full", externalClass));
76
-
77
- let timeSystem = $state<"iso" | "english">("iso");
78
- let didInitSystem = $state(false);
79
-
80
- let hour = $state("00");
81
- let minute = $state("00");
82
- let period = $state<Period>("AM");
83
-
84
- const hasValue = $derived(value != null);
85
-
86
- const isoHours = Array.from({ length: 24 }, (_, i) => {
87
- const h = i.toString().padStart(2, "0");
88
- return { value: h, label: h };
89
- });
90
-
91
- const englishHours = Array.from({ length: 12 }, (_, i) => {
92
- const h = (i + 1).toString().padStart(2, "0");
93
- return { value: h, label: h };
94
- });
95
-
96
- const periodOptions: Array<{ value: Period; label: Period }> = [
97
- { value: "AM", label: "AM" },
98
- { value: "PM", label: "PM" },
99
- ];
100
-
101
- const minuteIncrement = $derived(
102
- !step || step <= 0 ? 1 : Math.min(60, Math.max(1, Math.round(step / 60)))
103
- );
104
-
105
- const minuteOptions = $derived.by(() => {
106
- const arr = [];
107
- for (let i = 0; i < 60; i += minuteIncrement) {
108
- const m = i.toString().padStart(2, "0");
109
- arr.push({ value: m, label: m });
110
- }
111
- return arr;
112
- });
113
-
114
- function normalize(x: string) {
115
- return x.padStart(2, "0").slice(-2);
116
- }
117
-
118
- function toIsoHour(h: string, p: "AM" | "PM") {
119
- const numeric = Number.parseInt(h, 10);
120
- if (Number.isNaN(numeric)) return "00";
121
- const base = numeric % 12;
122
- const withPeriod = p === "PM" ? base + 12 : base;
123
- return normalize(String(withPeriod));
124
- }
125
-
126
- function toEnglishHour(isoHour: string): { hour: string; period: Period } {
127
- const numeric = Number.parseInt(isoHour, 10);
128
- if (Number.isNaN(numeric)) {
129
- return { hour: "12", period: "AM" as const };
130
- }
131
- const periodValue: Period = numeric >= 12 ? "PM" : "AM";
132
- const normalized = numeric % 12 || 12;
133
- return { hour: normalize(String(normalized)), period: periodValue };
134
- }
135
-
136
- function emit() {
137
- const isoHour =
138
- timeSystem === "english" ? toIsoHour(hour, period) : normalize(hour);
139
- const v = `${isoHour}:${normalize(minute)}`;
140
- value = v;
141
- onChange?.(v);
142
- }
143
-
144
- function handleIsoHour(v: string) {
145
- hour = normalize(v);
146
- emit();
147
- }
148
-
149
- function handleMinute(v: string) {
150
- minute = normalize(v);
151
- emit();
152
- }
153
-
154
- function handleEnglishHour(v: string) {
155
- hour = normalize(v);
156
- emit();
157
- }
158
-
159
- function handlePeriod(v: string) {
160
- period = v === "AM" || v === "PM" ? v : "AM";
161
- emit();
162
- }
163
-
164
- function toggleSystem() {
165
- if (disabled) return;
166
-
167
- if (timeSystem === "iso") {
168
- timeSystem = "english";
169
- const mapped = toEnglishHour(hour);
170
- hour = mapped.hour;
171
- period = mapped.period;
172
- } else {
173
- timeSystem = "iso";
174
- hour = toIsoHour(hour, period);
175
- period = "AM";
176
- }
177
-
178
- emit();
179
- }
180
-
181
- function clearSelection() {
182
- if (!clearable) return;
183
- hour = "00";
184
- minute = "00";
185
- period = "AM";
186
- value = null;
187
- onChange?.(null);
188
- }
189
-
190
- const displayValue = $derived.by(() => {
191
- if (!value) return "";
192
- const [isoH, isoM] = value.split(":");
193
- if (timeSystem === "english") {
194
- const { hour: h, period: p } = toEnglishHour(isoH);
195
- return `${h}:${normalize(isoM)} ${p}`;
196
- }
197
- return `${normalize(isoH)}:${normalize(isoM)}`;
198
- });
199
-
200
- $effect(() => {
201
- if (didInitSystem) return;
202
- didInitSystem = true;
203
- timeSystem = initialSystem;
204
- });
205
-
206
- $effect(() => {
207
- if (value == null) return;
208
-
209
- let raw = value;
210
- let parsedPeriod: Period | null = null;
211
-
212
- if (raw.includes("AM") || raw.includes("PM")) {
213
- parsedPeriod = raw.includes("PM") ? "PM" : "AM";
214
- raw = raw.replace(" AM", "").replace(" PM", "");
215
- }
216
-
217
- const [h, m] = raw.split(":");
218
- const isoHour = normalize(h);
219
- const isoMinute = normalize(m);
220
-
221
- if (timeSystem === "english") {
222
- const mapped = parsedPeriod
223
- ? { hour: normalize(h), period: parsedPeriod }
224
- : toEnglishHour(isoHour);
225
- hour = mapped.hour;
226
- period = mapped.period;
227
- } else {
228
- hour = isoHour;
229
- period = toEnglishHour(isoHour).period;
230
- }
231
- minute = isoMinute;
232
- });
233
- </script>
234
-
235
- <div class={pickerClass} {...rest}>
236
- <div class="text-md font-medium mb-2 [color:var(--color-text-default)]">
237
- {labelFinal}
238
- </div>
239
-
240
- <div class="flex flex-wrap items-start gap-4">
241
- <div class="flex flex-wrap gap-3 min-w-[0] flex-1">
242
- {#if timeSystem === "iso"}
243
- <Select
244
- label={L.hour}
245
- options={isoHours}
246
- value={hour}
247
- onChange={handleIsoHour}
248
- {disabled}
249
- sz="sm"
250
- />
251
- {:else}
252
- <Select
253
- label={L.hour}
254
- options={englishHours}
255
- value={hour}
256
- onChange={handleEnglishHour}
257
- {disabled}
258
- sz="sm"
259
- />
260
-
261
- <Select
262
- label={L.period}
263
- options={periodOptions}
264
- value={period}
265
- onChange={handlePeriod}
266
- {disabled}
267
- sz="sm"
268
- />
269
- {/if}
270
-
271
- <Select
272
- label={L.minute}
273
- options={minuteOptions}
274
- value={minute}
275
- onChange={handleMinute}
276
- {disabled}
277
- sz="sm"
278
- />
279
- </div>
280
-
281
- <div class="flex items-center gap-3 basis-full pt-2">
282
- <Button onClick={toggleSystem} {disabled} sz="xs">
283
- {timeSystem === "iso" ? L.switchTo12h : L.switchTo24h}
284
- </Button>
285
-
286
- {#if clearable}
287
- <Button
288
- onClick={clearSelection}
289
- variant="danger"
290
- disabled={!hasValue || disabled}
291
- sz="xs"
292
- >
293
- {L.clear}
294
- </Button>
295
- {/if}
296
- </div>
297
- </div>
298
-
299
- <div class="mt-4 p-4 bg-[var(--color-bg-surface)] text-center">
300
- <p class="text-xs uppercase tracking-wide [color:var(--color-text-muted)]">
301
- {L.selectedTime}
302
- </p>
303
-
304
- <p class="text-sm font-semibold mt-1 [color:var(--color-text-default)]">
305
- {#if hasValue}
306
- {displayValue}
307
- {:else}
308
- {placeholderFinal}
309
- {/if}
310
- </p>
311
- </div>
312
- </div>
1
+ <!-- src/lib/TimePicker.svelte -->
2
+ <script lang="ts">
3
+ /**
4
+ * @component TimePicker
5
+ * @description Simple time selector that stores values in ISO `HH:MM` format. Supports a fixed step and two display systems.
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 the clear button
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
+ import type { HTMLAttributes } from "svelte/elements";
37
+ import Button from "./Button.svelte";
38
+ import Select from "./Select.svelte";
39
+ import { cx } from "../utils";
40
+ import { getComponentText, getLangContext, getLangKey } from "./lang-context";
41
+
42
+ type Props = HTMLAttributes<HTMLDivElement> & {
43
+ value?: string | null;
44
+ step?: number;
45
+ label?: string;
46
+ placeholder?: string;
47
+ disabled?: boolean;
48
+ clearable?: boolean;
49
+ initialSystem?: "iso" | "english";
50
+ onChange?: (value: string | null) => void;
51
+ class?: string;
52
+ };
53
+ type Period = "AM" | "PM";
54
+
55
+ let {
56
+ value = $bindable<string | null>(null),
57
+ step = 60,
58
+ label,
59
+ placeholder,
60
+ disabled = false,
61
+ clearable = true,
62
+ initialSystem = "iso",
63
+ onChange,
64
+ class: externalClass = "",
65
+ ...rest
66
+ }: Props = $props();
67
+
68
+ const langCtx = getLangContext();
69
+ const langKey = $derived(getLangKey(langCtx));
70
+ const L = $derived(getComponentText("timePicker", langKey));
71
+
72
+ const labelFinal = $derived(label ?? L.text);
73
+ const placeholderFinal = $derived(placeholder ?? L.placeholder);
74
+
75
+ const pickerClass = $derived(cx("inline-block w-full", externalClass));
76
+
77
+ let timeSystem = $state<"iso" | "english">("iso");
78
+ let didInitSystem = $state(false);
79
+
80
+ let hour = $state("00");
81
+ let minute = $state("00");
82
+ let period = $state<Period>("AM");
83
+
84
+ const hasValue = $derived(value != null);
85
+
86
+ const isoHours = Array.from({ length: 24 }, (_, i) => {
87
+ const h = i.toString().padStart(2, "0");
88
+ return { value: h, label: h };
89
+ });
90
+
91
+ const englishHours = Array.from({ length: 12 }, (_, i) => {
92
+ const h = (i + 1).toString().padStart(2, "0");
93
+ return { value: h, label: h };
94
+ });
95
+
96
+ const periodOptions: Array<{ value: Period; label: Period }> = [
97
+ { value: "AM", label: "AM" },
98
+ { value: "PM", label: "PM" },
99
+ ];
100
+
101
+ const minuteIncrement = $derived(
102
+ !step || step <= 0 ? 1 : Math.min(60, Math.max(1, Math.round(step / 60)))
103
+ );
104
+
105
+ const minuteOptions = $derived.by(() => {
106
+ const arr = [];
107
+ for (let i = 0; i < 60; i += minuteIncrement) {
108
+ const m = i.toString().padStart(2, "0");
109
+ arr.push({ value: m, label: m });
110
+ }
111
+ return arr;
112
+ });
113
+
114
+ function normalize(x: string) {
115
+ return x.padStart(2, "0").slice(-2);
116
+ }
117
+
118
+ function toIsoHour(h: string, p: "AM" | "PM") {
119
+ const numeric = Number.parseInt(h, 10);
120
+ if (Number.isNaN(numeric)) return "00";
121
+ const base = numeric % 12;
122
+ const withPeriod = p === "PM" ? base + 12 : base;
123
+ return normalize(String(withPeriod));
124
+ }
125
+
126
+ function toEnglishHour(isoHour: string): { hour: string; period: Period } {
127
+ const numeric = Number.parseInt(isoHour, 10);
128
+ if (Number.isNaN(numeric)) {
129
+ return { hour: "12", period: "AM" as const };
130
+ }
131
+ const periodValue: Period = numeric >= 12 ? "PM" : "AM";
132
+ const normalized = numeric % 12 || 12;
133
+ return { hour: normalize(String(normalized)), period: periodValue };
134
+ }
135
+
136
+ function emit() {
137
+ const isoHour =
138
+ timeSystem === "english" ? toIsoHour(hour, period) : normalize(hour);
139
+ const v = `${isoHour}:${normalize(minute)}`;
140
+ value = v;
141
+ onChange?.(v);
142
+ }
143
+
144
+ function handleIsoHour(v: string) {
145
+ hour = normalize(v);
146
+ emit();
147
+ }
148
+
149
+ function handleMinute(v: string) {
150
+ minute = normalize(v);
151
+ emit();
152
+ }
153
+
154
+ function handleEnglishHour(v: string) {
155
+ hour = normalize(v);
156
+ emit();
157
+ }
158
+
159
+ function handlePeriod(v: string) {
160
+ period = v === "AM" || v === "PM" ? v : "AM";
161
+ emit();
162
+ }
163
+
164
+ function toggleSystem() {
165
+ if (disabled) return;
166
+
167
+ if (timeSystem === "iso") {
168
+ timeSystem = "english";
169
+ const mapped = toEnglishHour(hour);
170
+ hour = mapped.hour;
171
+ period = mapped.period;
172
+ } else {
173
+ timeSystem = "iso";
174
+ hour = toIsoHour(hour, period);
175
+ period = "AM";
176
+ }
177
+
178
+ emit();
179
+ }
180
+
181
+ function clearSelection() {
182
+ if (!clearable) return;
183
+ hour = "00";
184
+ minute = "00";
185
+ period = "AM";
186
+ value = null;
187
+ onChange?.(null);
188
+ }
189
+
190
+ const displayValue = $derived.by(() => {
191
+ if (!value) return "";
192
+ const [isoH, isoM] = value.split(":");
193
+ if (timeSystem === "english") {
194
+ const { hour: h, period: p } = toEnglishHour(isoH);
195
+ return `${h}:${normalize(isoM)} ${p}`;
196
+ }
197
+ return `${normalize(isoH)}:${normalize(isoM)}`;
198
+ });
199
+
200
+ $effect(() => {
201
+ if (didInitSystem) return;
202
+ didInitSystem = true;
203
+ timeSystem = initialSystem;
204
+ });
205
+
206
+ $effect(() => {
207
+ if (value == null) return;
208
+
209
+ let raw = value;
210
+ let parsedPeriod: Period | null = null;
211
+
212
+ if (raw.includes("AM") || raw.includes("PM")) {
213
+ parsedPeriod = raw.includes("PM") ? "PM" : "AM";
214
+ raw = raw.replace(" AM", "").replace(" PM", "");
215
+ }
216
+
217
+ const [h, m] = raw.split(":");
218
+ const isoHour = normalize(h);
219
+ const isoMinute = normalize(m);
220
+
221
+ if (timeSystem === "english") {
222
+ const mapped = parsedPeriod
223
+ ? { hour: normalize(h), period: parsedPeriod }
224
+ : toEnglishHour(isoHour);
225
+ hour = mapped.hour;
226
+ period = mapped.period;
227
+ } else {
228
+ hour = isoHour;
229
+ period = toEnglishHour(isoHour).period;
230
+ }
231
+ minute = isoMinute;
232
+ });
233
+ </script>
234
+
235
+ <div class={pickerClass} {...rest}>
236
+ <div class="text-md font-medium mb-2 [color:var(--color-text-default)]">
237
+ {labelFinal}
238
+ </div>
239
+
240
+ <div class="flex flex-wrap items-start gap-4">
241
+ <div class="flex flex-wrap gap-3 min-w-[0] flex-1">
242
+ {#if timeSystem === "iso"}
243
+ <Select
244
+ label={L.hour}
245
+ options={isoHours}
246
+ value={hour}
247
+ onChange={handleIsoHour}
248
+ {disabled}
249
+ sz="sm"
250
+ />
251
+ {:else}
252
+ <Select
253
+ label={L.hour}
254
+ options={englishHours}
255
+ value={hour}
256
+ onChange={handleEnglishHour}
257
+ {disabled}
258
+ sz="sm"
259
+ />
260
+
261
+ <Select
262
+ label={L.period}
263
+ options={periodOptions}
264
+ value={period}
265
+ onChange={handlePeriod}
266
+ {disabled}
267
+ sz="sm"
268
+ />
269
+ {/if}
270
+
271
+ <Select
272
+ label={L.minute}
273
+ options={minuteOptions}
274
+ value={minute}
275
+ onChange={handleMinute}
276
+ {disabled}
277
+ sz="sm"
278
+ />
279
+ </div>
280
+
281
+ <div class="flex items-center gap-3 basis-full pt-2">
282
+ <Button onClick={toggleSystem} {disabled} sz="xs">
283
+ {timeSystem === "iso" ? L.switchTo12h : L.switchTo24h}
284
+ </Button>
285
+
286
+ {#if clearable}
287
+ <Button
288
+ onClick={clearSelection}
289
+ variant="danger"
290
+ disabled={!hasValue || disabled}
291
+ sz="xs"
292
+ >
293
+ {L.clear}
294
+ </Button>
295
+ {/if}
296
+ </div>
297
+ </div>
298
+
299
+ <div class="mt-4 p-4 bg-[var(--color-bg-surface)] text-center">
300
+ <p class="text-xs uppercase tracking-wide [color:var(--color-text-muted)]">
301
+ {L.selectedTime}
302
+ </p>
303
+
304
+ <p class="text-sm font-semibold mt-1 [color:var(--color-text-default)]">
305
+ {#if hasValue}
306
+ {displayValue}
307
+ {:else}
308
+ {placeholderFinal}
309
+ {/if}
310
+ </p>
311
+ </div>
312
+ </div>