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.
- package/LICENSE.md +21 -21
- package/README.md +101 -100
- package/dist/App.svelte +507 -507
- package/dist/Container.svelte +59 -59
- package/dist/app.css +234 -235
- package/dist/app.d.ts +10 -0
- package/dist/lib/Accordion.svelte +155 -155
- package/dist/lib/Badge.svelte +44 -44
- package/dist/lib/Button.svelte +185 -170
- package/dist/lib/Calendar.svelte +384 -384
- package/dist/lib/Card.svelte +103 -103
- package/dist/lib/Carousel.svelte +293 -293
- package/dist/lib/Carousel.svelte.d.ts +1 -1
- package/dist/lib/CheckBox.svelte +210 -210
- package/dist/lib/CodeView.svelte +308 -307
- package/dist/lib/ColorPicker.svelte +159 -159
- package/dist/lib/ContextMenu.svelte +328 -322
- package/dist/lib/DatePicker.svelte +246 -246
- package/dist/lib/Dialog.svelte +233 -233
- package/dist/lib/Field.svelte +299 -299
- package/dist/lib/FilePicker.svelte +295 -240
- package/dist/lib/FilePicker.svelte.d.ts +6 -1
- package/dist/lib/Form.svelte +438 -438
- package/dist/lib/Hamburger.svelte +217 -217
- package/dist/lib/InstallPWA.svelte +94 -94
- package/dist/lib/Menu.svelte +623 -623
- package/dist/lib/NoticeBase.svelte +140 -140
- package/dist/lib/PaginatedCard.svelte +73 -73
- package/dist/lib/Pagination.svelte +119 -119
- package/dist/lib/PrimaryColorSelect.svelte +111 -111
- package/dist/lib/ProgressBar.svelte +141 -141
- package/dist/lib/ProgressCircle.svelte +190 -190
- package/dist/lib/Radio.svelte +189 -189
- package/dist/lib/SearchInput.svelte +104 -104
- package/dist/lib/Select.svelte +524 -524
- package/dist/lib/Slider.svelte +253 -253
- package/dist/lib/Splitter.svelte +159 -150
- package/dist/lib/Switch.svelte +168 -167
- package/dist/lib/Table.svelte +299 -299
- package/dist/lib/Tabs.svelte +213 -213
- package/dist/lib/ThemeToggle.svelte +128 -127
- package/dist/lib/TimePicker.svelte +312 -312
- package/dist/lib/TimePickerNew.svelte +634 -0
- package/dist/lib/TimePickerNew.svelte.d.ts +49 -0
- package/dist/lib/Toast.svelte +123 -123
- package/dist/lib/Tooltip.svelte +110 -110
- package/dist/lib/Topbar.svelte +107 -107
- package/dist/lib/__tests__/Accordion.test.d.ts +1 -0
- package/dist/lib/__tests__/Accordion.test.js +171 -0
- package/dist/lib/__tests__/Badge.test.d.ts +1 -0
- package/dist/lib/__tests__/Badge.test.js +41 -0
- package/dist/lib/__tests__/Button.test.d.ts +1 -0
- package/dist/lib/__tests__/Button.test.js +269 -0
- package/dist/lib/__tests__/Calendar.test.d.ts +1 -0
- package/dist/lib/__tests__/Calendar.test.js +171 -0
- package/dist/lib/__tests__/Card.test.d.ts +1 -0
- package/dist/lib/__tests__/Card.test.js +148 -0
- package/dist/lib/__tests__/Carousel.test.d.ts +1 -0
- package/dist/lib/__tests__/Carousel.test.js +439 -0
- package/dist/lib/__tests__/CheckBox.test.d.ts +1 -0
- package/dist/lib/__tests__/CheckBox.test.js +152 -0
- package/dist/lib/__tests__/CodeView.test.d.ts +1 -0
- package/dist/lib/__tests__/CodeView.test.js +157 -0
- package/dist/lib/__tests__/ColorPicker.test.d.ts +1 -0
- package/dist/lib/__tests__/ColorPicker.test.js +93 -0
- package/dist/lib/__tests__/ContextMenu.test.d.ts +1 -0
- package/dist/lib/__tests__/ContextMenu.test.js +67 -0
- package/dist/lib/__tests__/DatePicker.test.d.ts +1 -0
- package/dist/lib/__tests__/DatePicker.test.js +108 -0
- package/dist/lib/__tests__/Dialog.test.d.ts +1 -0
- package/dist/lib/__tests__/Dialog.test.js +183 -0
- package/dist/lib/__tests__/Field.test.d.ts +1 -0
- package/dist/lib/__tests__/Field.test.js +190 -0
- package/dist/lib/__tests__/FilePicker.test.d.ts +1 -0
- package/dist/lib/__tests__/FilePicker.test.js +179 -0
- package/dist/lib/__tests__/Form.integration.test.d.ts +1 -0
- package/dist/lib/__tests__/Form.integration.test.js +158 -0
- package/dist/lib/__tests__/Form.test.d.ts +1 -0
- package/dist/lib/__tests__/Form.test.js +463 -0
- package/dist/lib/__tests__/Hamburger.test.d.ts +1 -0
- package/dist/lib/__tests__/Hamburger.test.js +161 -0
- package/dist/lib/__tests__/InstallPWA.test.d.ts +1 -0
- package/dist/lib/__tests__/InstallPWA.test.js +15 -0
- package/dist/lib/__tests__/Menu.test.d.ts +1 -0
- package/dist/lib/__tests__/Menu.test.js +285 -0
- package/dist/lib/__tests__/NoticeBase.test.d.ts +1 -0
- package/dist/lib/__tests__/NoticeBase.test.js +60 -0
- package/dist/lib/__tests__/PaginatedCard.test.d.ts +1 -0
- package/dist/lib/__tests__/PaginatedCard.test.js +89 -0
- package/dist/lib/__tests__/Pagination.test.d.ts +1 -0
- package/dist/lib/__tests__/Pagination.test.js +168 -0
- package/dist/lib/__tests__/PrimaryColorSelect.test.d.ts +1 -0
- package/dist/lib/__tests__/PrimaryColorSelect.test.js +92 -0
- package/dist/lib/__tests__/ProgressBar.test.d.ts +1 -0
- package/dist/lib/__tests__/ProgressBar.test.js +69 -0
- package/dist/lib/__tests__/ProgressCircle.test.d.ts +1 -0
- package/dist/lib/__tests__/ProgressCircle.test.js +71 -0
- package/dist/lib/__tests__/Radio.test.d.ts +1 -0
- package/dist/lib/__tests__/Radio.test.js +127 -0
- package/dist/lib/__tests__/SearchInput.test.d.ts +1 -0
- package/dist/lib/__tests__/SearchInput.test.js +80 -0
- package/dist/lib/__tests__/Select.test.d.ts +1 -0
- package/dist/lib/__tests__/Select.test.js +408 -0
- package/dist/lib/__tests__/Slider.test.d.ts +1 -0
- package/dist/lib/__tests__/Slider.test.js +213 -0
- package/dist/lib/__tests__/Splitter.test.d.ts +1 -0
- package/dist/lib/__tests__/Splitter.test.js +87 -0
- package/dist/lib/__tests__/Switch.test.d.ts +1 -0
- package/dist/lib/__tests__/Switch.test.js +97 -0
- package/dist/lib/__tests__/Table.test.d.ts +1 -0
- package/dist/lib/__tests__/Table.test.js +349 -0
- package/dist/lib/__tests__/Tabs.test.d.ts +1 -0
- package/dist/lib/__tests__/Tabs.test.js +262 -0
- package/dist/lib/__tests__/ThemeToggle.test.d.ts +1 -0
- package/dist/lib/__tests__/ThemeToggle.test.js +84 -0
- package/dist/lib/__tests__/TimePicker.test.d.ts +1 -0
- package/dist/lib/__tests__/TimePicker.test.js +146 -0
- package/dist/lib/__tests__/TimePickerNew.test.d.ts +1 -0
- package/dist/lib/__tests__/TimePickerNew.test.js +322 -0
- package/dist/lib/__tests__/Toast.test.d.ts +1 -0
- package/dist/lib/__tests__/Toast.test.js +135 -0
- package/dist/lib/__tests__/Tooltip.test.d.ts +1 -0
- package/dist/lib/__tests__/Tooltip.test.js +171 -0
- package/dist/lib/__tests__/Topbar.test.d.ts +1 -0
- package/dist/lib/__tests__/Topbar.test.js +25 -0
- package/dist/lib/__tests__/setupLangContext.d.ts +1 -0
- package/dist/lib/__tests__/setupLangContext.js +65 -0
- package/dist/lib/__tests__/storage.test.d.ts +1 -0
- package/dist/lib/__tests__/storage.test.js +124 -0
- package/dist/lib/__tests__/utils.test.d.ts +1 -0
- package/dist/lib/__tests__/utils.test.js +11 -0
- package/dist/lib/index.d.ts +1 -0
- package/dist/lib/index.js +1 -0
- package/dist/lib/lang.d.ts +4 -0
- package/dist/lib/lang.js +4 -0
- package/dist/styles.css +234 -232
- package/dist/utils/index.js +15 -4
- 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;
|