@warkypublic/svelix 0.1.46 → 0.1.47

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 (97) hide show
  1. package/README.md +93 -33
  2. package/dist/components/BetterMenu/BetterMenu.svelte +21 -37
  3. package/dist/components/BetterMenu/BetterMenuAsyncButton.svelte +21 -31
  4. package/dist/components/BetterMenu/BetterMenuPreview.svelte +27 -29
  5. package/dist/components/BetterMenu/MenuRenderer.svelte +40 -51
  6. package/dist/components/Boxer/Boxer.svelte +488 -602
  7. package/dist/components/Boxer/BoxerTarget.svelte +6 -46
  8. package/dist/components/Boxer/BoxerTarget.svelte.d.ts +1 -0
  9. package/dist/components/Boxer/types.d.ts +0 -1
  10. package/dist/components/Button.svelte +19 -43
  11. package/dist/components/ButtonPreview.svelte +2 -11
  12. package/dist/components/CardGrid/CardGrid.svelte +443 -614
  13. package/dist/components/CardGrid/CardGridFilterPanel.svelte +111 -90
  14. package/dist/components/CardGrid/DefaultCard.svelte +80 -96
  15. package/dist/components/CardGrid/ImageCardStory.svelte +87 -36
  16. package/dist/components/ContentEditor/CollaboraInsertTextDemo.svelte +5 -10
  17. package/dist/components/ContentEditor/ContentEditor.svelte +24 -50
  18. package/dist/components/ContentEditor/subcomponents/AudioPlayer.svelte +17 -17
  19. package/dist/components/ContentEditor/subcomponents/CollaboraEditor.svelte +244 -309
  20. package/dist/components/ContentEditor/subcomponents/EmailViewer.svelte +2 -3
  21. package/dist/components/ContentEditor/subcomponents/ImageViewer.svelte +14 -15
  22. package/dist/components/ContentEditor/subcomponents/MarkdownViewer.svelte +74 -90
  23. package/dist/components/ContentEditor/subcomponents/MonacoEditor.svelte +75 -103
  24. package/dist/components/ContentEditor/subcomponents/Office365Editor.svelte +234 -297
  25. package/dist/components/ContentEditor/subcomponents/PdfViewer.svelte +14 -20
  26. package/dist/components/ContentEditor/subcomponents/TextEditor.svelte +122 -150
  27. package/dist/components/ContentEditor/subcomponents/UnknownFile.svelte +105 -126
  28. package/dist/components/ContentEditor/subcomponents/VideoPlayer.svelte +17 -17
  29. package/dist/components/ContentEditor/subcomponents/ZipViewer.svelte +2 -3
  30. package/dist/components/ErrorBoundary/ErrorBoundary.svelte +41 -58
  31. package/dist/components/ErrorBoundary/ErrorBoundaryPreview.svelte +8 -16
  32. package/dist/components/Former/Former.svelte +306 -424
  33. package/dist/components/Former/FormerButtonArea.svelte +17 -68
  34. package/dist/components/Former/FormerDrawer.svelte +26 -78
  35. package/dist/components/Former/FormerDrawerPreview.svelte +99 -89
  36. package/dist/components/Former/FormerModal.svelte +24 -70
  37. package/dist/components/Former/FormerModalPreview.svelte +99 -89
  38. package/dist/components/Former/FormerPreview.svelte +76 -78
  39. package/dist/components/Former/FormerRestApiPreview.svelte +55 -48
  40. package/dist/components/FormerControllers/ButtonCtrl.svelte +13 -44
  41. package/dist/components/FormerControllers/DateTimeCtrl/DateTimeCtrl.svelte +280 -445
  42. package/dist/components/FormerControllers/DateTimeCtrl/DateTimeCtrlCalendar.svelte +95 -136
  43. package/dist/components/FormerControllers/DateTimeCtrl/DateTimeCtrlPickerPanel.svelte +3 -62
  44. package/dist/components/FormerControllers/DateTimeCtrl/DateTimeCtrlTimeFields.svelte +143 -217
  45. package/dist/components/FormerControllers/IconButtonCtrl.svelte +13 -42
  46. package/dist/components/FormerControllers/InlineWrapper.svelte +12 -41
  47. package/dist/components/FormerControllers/InlineWrapperPreview.svelte +4 -26
  48. package/dist/components/FormerControllers/NativeSelectCtrl.svelte +3 -29
  49. package/dist/components/FormerControllers/NumberInputCtrl.svelte +9 -35
  50. package/dist/components/FormerControllers/PasswordInputCtrl.svelte +4 -23
  51. package/dist/components/FormerControllers/SwitchCtrl.svelte +3 -20
  52. package/dist/components/FormerControllers/TextAreaCtrl.svelte +3 -24
  53. package/dist/components/FormerControllers/TextInputCtrl.svelte +3 -24
  54. package/dist/components/GlobalStateStore/GlobalStateStoreProvider.svelte +73 -120
  55. package/dist/components/Gridler/components/Gridler.svelte +338 -442
  56. package/dist/components/Gridler/components/GridlerCanvas.svelte +720 -1115
  57. package/dist/components/Gridler/components/GridlerEditor.svelte +2 -15
  58. package/dist/components/Gridler/components/GridlerFull.svelte +520 -841
  59. package/dist/components/Gridler/components/GridlerFull.svelte.d.ts +1 -1
  60. package/dist/components/Gridler/components/GridlerFullWithFormerPreview.svelte +194 -147
  61. package/dist/components/Gridler/components/GridlerSearch.svelte +7 -16
  62. package/dist/components/Gridler/components/GridlerSearchToggle.svelte +7 -19
  63. package/dist/components/Gridler/types.d.ts +1 -1
  64. package/dist/components/Icons/IconAdd.svelte +1 -3
  65. package/dist/components/Icons/IconAlertCircle.svelte +1 -3
  66. package/dist/components/Icons/IconAlertTriangle.svelte +1 -3
  67. package/dist/components/Icons/IconCamera.svelte +1 -3
  68. package/dist/components/Icons/IconClose.svelte +1 -3
  69. package/dist/components/Icons/IconEdit.svelte +1 -3
  70. package/dist/components/Icons/IconFilter.svelte +1 -3
  71. package/dist/components/Icons/IconSearch.svelte +1 -3
  72. package/dist/components/Icons/IconSort.svelte +1 -3
  73. package/dist/components/Icons/IconTrash.svelte +1 -3
  74. package/dist/components/Portal/Portal.svelte +14 -38
  75. package/dist/components/Screenshot/Screenshot.svelte +7 -13
  76. package/dist/components/Svark/Svark.svelte +680 -852
  77. package/dist/components/Svark/SvarkContextMenu.svelte +44 -63
  78. package/dist/components/Svark/SvarkInfiniteLoader.svelte +2 -6
  79. package/dist/components/Svark/SvarkPager.svelte +2 -28
  80. package/dist/components/Svark/SvarkSelectionDemo.svelte +4 -8
  81. package/dist/components/Svark/SvarkTopBar.svelte +9 -74
  82. package/dist/components/SvarkGrid/SvarkGrid.svelte +877 -1123
  83. package/dist/components/SvarkGrid/components/SvarkColumnFilterForm.svelte +45 -58
  84. package/dist/components/SvarkGrid/components/SvarkContextMenu.svelte +44 -63
  85. package/dist/components/SvarkGrid/components/SvarkHeaderFilterCell.svelte +41 -75
  86. package/dist/components/SvarkGrid/components/SvarkInfiniteLoader.svelte +2 -6
  87. package/dist/components/SvarkGrid/components/SvarkPager.svelte +2 -28
  88. package/dist/components/SvarkGrid/components/SvarkSearchPopover.svelte +8 -16
  89. package/dist/components/SvarkGrid/components/SvarkTopBar.svelte +6 -62
  90. package/dist/components/SvarkGrid/internal/SvarkGridView.svelte +345 -507
  91. package/dist/components/VTree/VTree.svelte +349 -431
  92. package/dist/components/VTree/VTreeContextMenu.svelte +21 -33
  93. package/dist/components/VTree/VTreeEventsDemo.svelte +48 -58
  94. package/dist/components/VTree/VTreeRow.svelte +13 -65
  95. package/dist/components/VTree/VTreeSearch.svelte +6 -13
  96. package/dist/components/VTree/VTreeVirtualViewport.svelte +37 -106
  97. package/package.json +30 -29
@@ -1,448 +1,283 @@
1
- <script lang="ts">
2
- import { tick } from 'svelte';
3
- import Portal from '../../Portal/Portal.svelte';
4
- import { registerOverlay } from '../../OverlayStack';
5
- import type { CtrlBaseProps } from '../types';
6
- import DateTimeCtrlPickerPanel from './DateTimeCtrlPickerPanel.svelte';
7
- import {
8
- formatDateTimeByFormat,
9
- getCalendarCells,
10
- getNowDateTimeParts,
11
- getYearMonthLabel,
12
- isDateSelectable,
13
- normalizePartsForMode,
14
- parseDateTimeByFormat,
15
- parsePartialDateTimeByFormat,
16
- parseNativeDate,
17
- resolveDateTimeFormat,
18
- shiftYearMonth,
19
- toNativeDate,
20
- validateDateTimeParts,
21
- type DateTimeMode,
22
- type DateTimeParts,
23
- } from './DateTimeCtrl.utils';
24
-
25
- interface TimePickerProps {
26
- amLabel?: string;
27
- hoursStep?: number;
28
- minutesStep?: number;
29
- pmLabel?: string;
30
- presets?: Array<string | { label: string; value: string }>;
31
- secondsStep?: number;
32
- use12Hour?: boolean;
33
- withDropdown?: boolean;
34
- }
35
-
36
- interface PopoverProps {
37
- closeOnClickOutside?: boolean;
38
- closeOnEscape?: boolean;
39
- dialogAriaLabel?: string;
40
- panelClass?: string;
41
- }
42
-
43
- export interface Props extends CtrlBaseProps {
44
- amLabel?: string;
45
- closeOnPick?: boolean;
46
- clearable?: boolean;
47
- disableAutocomplete?: boolean;
48
- disablePortal?: boolean;
49
- hoursStep?: number;
50
- inputFormat?: string;
51
- maxDate?: string;
52
- maxTime?: string;
53
- minDate?: string;
54
- minTime?: string;
55
- minutesStep?: number;
56
- mode?: DateTimeMode;
57
- onblur?: () => void;
58
- onchange?: (value: string | null) => void;
59
- outputFormat?: string;
60
- pmLabel?: string;
61
- popoverProps?: PopoverProps;
62
- secondsStep?: number;
63
- timePickerProps?: TimePickerProps;
64
- timePresets?: Array<string | { label: string; value: string }>;
65
- use12Hour?: boolean;
66
- withPicker?: boolean;
67
- withTimeDropdown?: boolean;
68
- value?: string | null;
69
- }
70
-
71
- let {
72
- amLabel,
73
- closeOnPick = true,
74
- clearable = true,
75
- disableAutocomplete = false,
76
- disablePortal = false,
77
- disabled,
78
- error,
79
- hoursStep,
80
- inputFormat = 'ISO',
81
- label,
82
- maxDate,
83
- maxTime,
84
- minDate,
85
- minTime,
86
- minutesStep,
87
- mode = 'datetime',
88
- name,
89
- onblur,
90
- onchange,
91
- outputFormat = 'ISO',
92
- pmLabel,
93
- popoverProps,
94
- placeholder,
95
- required,
96
- sid,
97
- tooltip,
98
- secondsStep,
99
- timePickerProps,
100
- timePresets,
101
- use12Hour,
102
- value = $bindable(null),
103
- withPicker = true,
104
- withTimeDropdown,
105
- }: Props = $props();
106
-
107
- const id = $derived(`field_${name ?? ''}_${sid ?? ''}`);
108
- const pickerDialogId = $derived(`${id}_picker_dialog`);
109
- const resolvedInputFormat = $derived(resolveDateTimeFormat(inputFormat, mode));
110
- const resolvedOutputFormat = $derived(resolveDateTimeFormat(outputFormat, mode));
111
- const showDatePicker = $derived(mode === 'date' || mode === 'datetime');
112
- const showTimePicker = $derived(mode === 'time' || mode === 'datetime');
113
- const rightSectionCount = $derived((withPicker ? 1 : 0) + (clearable ? 1 : 0));
114
- const inputRightPaddingClass = $derived(rightSectionCount >= 2 ? 'pr-20' : rightSectionCount === 1 ? 'pr-12' : 'pr-3');
115
- const resolvedUse12Hour = $derived(use12Hour ?? timePickerProps?.use12Hour ?? false);
116
- const resolvedWithTimeDropdown = $derived(withTimeDropdown ?? timePickerProps?.withDropdown ?? false);
117
- const resolvedHoursStep = $derived(hoursStep ?? timePickerProps?.hoursStep ?? 1);
118
- const resolvedMinutesStep = $derived(minutesStep ?? timePickerProps?.minutesStep ?? 1);
119
- const resolvedSecondsStep = $derived(secondsStep ?? timePickerProps?.secondsStep ?? 1);
120
- const resolvedAmLabel = $derived(amLabel ?? timePickerProps?.amLabel ?? 'AM');
121
- const resolvedPmLabel = $derived(pmLabel ?? timePickerProps?.pmLabel ?? 'PM');
122
- const resolvedTimePresets = $derived(timePresets ?? timePickerProps?.presets ?? []);
123
- const closeOnClickOutside = $derived(popoverProps?.closeOnClickOutside ?? true);
124
- const closeOnEscape = $derived(popoverProps?.closeOnEscape ?? true);
125
- const dialogAriaLabel = $derived(popoverProps?.dialogAriaLabel ?? 'Date time picker');
126
- const panelClass = $derived(popoverProps?.panelClass ?? '');
127
-
128
- let displayValue = $state('');
129
- let pickerOpen = $state(false);
130
- let pickerHour = $state(0);
131
- let pickerMinute = $state(0);
132
- let pickerSecond = $state(0);
133
- let selectedDateIso = $state('');
134
- let calendarYear = $state(getNowDateTimeParts().year);
135
- let calendarMonth = $state(getNowDateTimeParts().month);
136
-
137
- let localError = $state<string | undefined>(undefined);
138
- const effectiveError = $derived(error ?? localError);
139
-
140
- let anchorRef = $state<HTMLDivElement | null>(null);
141
- let popupTop = $state(0);
142
- let popupLeft = $state(0);
143
- let popupWidth = $state(0);
144
- let pickerZIndex = $state(1100);
145
-
146
- const calendarMonthLabel = $derived(getYearMonthLabel(calendarYear, calendarMonth));
147
- const calendarCells = $derived(getCalendarCells(calendarYear, calendarMonth));
148
-
149
- function syncPickerFromParts(parts: DateTimeParts): void {
150
- selectedDateIso = toNativeDate(parts);
151
- pickerHour = parts.hour;
152
- pickerMinute = parts.minute;
153
- pickerSecond = parts.second;
154
- calendarYear = parts.year;
155
- calendarMonth = parts.month;
156
- }
157
-
158
- function getCurrentParts(): DateTimeParts {
159
- const trimmed = displayValue.trim();
160
- if (trimmed === '') {
161
- const now = normalizePartsForMode(getNowDateTimeParts(), mode);
162
- return now ?? getNowDateTimeParts();
163
- }
164
-
165
- const parsed = parseDateTimeByFormat(trimmed, resolvedInputFormat, mode);
166
- if (parsed) {
167
- return parsed;
168
- }
169
-
170
- const now = normalizePartsForMode(getNowDateTimeParts(), mode);
171
- return now ?? getNowDateTimeParts();
172
- }
173
-
174
- function emitParts(parts: DateTimeParts): void {
175
- const normalizedInput = formatDateTimeByFormat(parts, resolvedInputFormat);
176
- const normalizedOutput = formatDateTimeByFormat(parts, resolvedOutputFormat);
177
-
178
- displayValue = normalizedInput;
179
- value = normalizedOutput;
180
- syncPickerFromParts(parts);
181
- onchange?.(normalizedOutput);
182
- }
183
-
184
- function applyParts(parts: DateTimeParts, closeAfterApply = false): boolean {
185
- const normalized = normalizePartsForMode(parts, mode);
186
- if (!normalized) {
187
- localError = 'Invalid date/time value';
188
- return false;
189
- }
190
-
191
- const constraintError = validateDateTimeParts(normalized, mode, {
192
- minDate,
193
- maxDate,
194
- minTime,
195
- maxTime,
196
- });
197
-
198
- if (constraintError) {
199
- localError = constraintError;
200
- return false;
201
- }
202
-
203
- localError = undefined;
204
- emitParts(normalized);
205
-
206
- if (closeAfterApply && closeOnPick) {
207
- pickerOpen = false;
208
- }
209
-
210
- return true;
211
- }
212
-
213
- function syncFromExternalValue(): void {
214
- const incoming = value;
215
-
216
- if (!incoming || incoming.trim() === '') {
217
- displayValue = '';
218
- selectedDateIso = '';
219
- localError = undefined;
220
- return;
221
- }
222
-
223
- const parsed = parseDateTimeByFormat(incoming, resolvedOutputFormat, mode);
224
- if (!parsed) {
225
- displayValue = incoming;
226
- selectedDateIso = '';
227
- return;
228
- }
229
-
230
- displayValue = formatDateTimeByFormat(parsed, resolvedInputFormat);
231
- syncPickerFromParts(parsed);
232
- }
233
-
234
- function clearValue(): void {
235
- displayValue = '';
236
- selectedDateIso = '';
237
- localError = undefined;
238
- value = null;
239
- onchange?.(null);
240
- }
241
-
242
- function commitFromTextInput(): void {
243
- const trimmed = displayValue.trim();
244
-
245
- if (trimmed === '') {
246
- clearValue();
247
- return;
248
- }
249
-
250
- const parsed = parseDateTimeByFormat(trimmed, resolvedInputFormat, mode);
251
- if (parsed) {
252
- applyParts(parsed);
253
- return;
254
- }
255
-
256
- // Fallback: if input is partial, complete missing segments from current date/time
257
- // and commit without blocking tab/blur flow.
258
- if (!disableAutocomplete) {
259
- const partialParsed = parsePartialDateTimeByFormat(trimmed, resolvedInputFormat, mode);
260
- if (partialParsed) {
261
- applyParts(partialParsed);
262
- return;
263
- }
264
- }
265
-
266
- // Do not block blur/tab flow on partial or non-matching input.
267
- // Keep current text and skip hard error here.
268
- localError = undefined;
269
- }
270
-
271
- function handleSelectCalendarDate(isoDate: string): void {
272
- const parsedDate = parseNativeDate(isoDate);
273
- if (!parsedDate) {
274
- return;
275
- }
276
-
277
- const current = getCurrentParts();
278
- current.year = parsedDate.year;
279
- current.month = parsedDate.month;
280
- current.day = parsedDate.day;
281
-
282
- const closeAfterApply = mode === 'date';
283
- applyParts(current, closeAfterApply);
284
- }
285
-
286
- function handleTimeChange(value: { hour: number; minute: number; second: number }): void {
287
- const current = getCurrentParts();
288
- current.hour = value.hour;
289
- current.minute = value.minute;
290
- current.second = value.second;
291
-
292
- const closeAfterApply = mode === 'time';
293
- applyParts(current, closeAfterApply);
294
- }
295
-
296
- function handleNow(): void {
297
- const now = normalizePartsForMode(getNowDateTimeParts(), mode);
298
- if (!now) {
299
- return;
300
- }
301
-
302
- applyParts(now, true);
303
- }
304
-
305
- function shiftMonth(delta: number): void {
306
- const shifted = shiftYearMonth(calendarYear, calendarMonth, delta);
307
- calendarYear = shifted.year;
308
- calendarMonth = shifted.month;
309
- }
310
-
311
- function handleTextKeydown(event: KeyboardEvent): void {
312
- if (event.key === 'Enter') {
313
- commitFromTextInput();
314
- (event.currentTarget as HTMLInputElement).blur();
315
- return;
316
- }
317
-
318
- if (event.key === 'Escape' && closeOnEscape) {
319
- pickerOpen = false;
320
- }
321
- }
322
-
323
- function handleInputBlur(): void {
324
- commitFromTextInput();
325
- onblur?.();
326
- }
327
-
328
- function updatePortalPosition(): void {
329
- if (!anchorRef) {
330
- return;
331
- }
332
-
333
- const rect = anchorRef.getBoundingClientRect();
334
- const viewportWidth = typeof window === 'undefined' ? rect.width : window.innerWidth;
335
- const horizontalPadding = 8;
336
- const desktopMaxWidth = 560;
337
- const maxWidth = Math.min(desktopMaxWidth, Math.max(280, viewportWidth - horizontalPadding * 2));
338
- const nextWidth = Math.min(Math.max(rect.width, 280), maxWidth);
339
- const maxLeft = viewportWidth - nextWidth - horizontalPadding;
340
-
341
- popupTop = rect.bottom + 8;
342
- popupWidth = nextWidth;
343
- popupLeft = Math.min(Math.max(rect.left, horizontalPadding), Math.max(horizontalPadding, maxLeft));
344
- }
345
-
346
- function handlePickerKeydown(event: KeyboardEvent): void {
347
- if (event.key === 'Escape' && closeOnEscape) {
348
- event.preventDefault();
349
- pickerOpen = false;
350
- }
351
- }
352
-
353
- async function focusPickerFirstControl(): Promise<void> {
354
- if (!pickerOpen || !showDatePicker || typeof document === 'undefined') {
355
- return;
356
- }
357
-
358
- await tick();
359
-
360
- const dialog = document.getElementById(pickerDialogId);
361
- if (!dialog) {
362
- return;
363
- }
364
-
365
- const selectedDay = dialog.querySelector<HTMLButtonElement>(
366
- 'button[data-calendar-day="true"][aria-selected="true"]:not([disabled])',
367
- );
368
- if (selectedDay) {
369
- selectedDay.focus();
370
- return;
371
- }
372
-
373
- const firstEnabledDay = dialog.querySelector<HTMLButtonElement>(
374
- 'button[data-calendar-day="true"]:not([disabled])',
375
- );
376
- firstEnabledDay?.focus();
377
- }
378
-
379
- $effect(() => {
380
- syncFromExternalValue();
381
- });
382
-
383
- $effect(() => {
384
- if (!pickerOpen || disabled || !closeOnClickOutside || typeof document === 'undefined') {
385
- return;
386
- }
387
-
388
- const onDocumentPointerDown = (event: MouseEvent) => {
389
- const target = event.target as Node | null;
390
- const dialog = document.getElementById(pickerDialogId);
391
- const insideAnchor = !!(target && anchorRef?.contains(target));
392
- const insideDialog = !!(target && dialog?.contains(target));
393
-
394
- if (!insideAnchor && !insideDialog) {
395
- pickerOpen = false;
396
- }
397
- };
398
-
399
- document.addEventListener('mousedown', onDocumentPointerDown);
400
-
401
- return () => {
402
- document.removeEventListener('mousedown', onDocumentPointerDown);
403
- };
404
- });
405
-
406
- $effect(() => {
407
- if (disablePortal || !pickerOpen || disabled) {
408
- return;
409
- }
410
-
411
- updatePortalPosition();
412
-
413
- if (typeof window === 'undefined') {
414
- return;
415
- }
416
-
417
- const onWindowChange = () => {
418
- updatePortalPosition();
419
- };
420
-
421
- window.addEventListener('resize', onWindowChange);
422
- window.addEventListener('scroll', onWindowChange, true);
423
-
424
- return () => {
425
- window.removeEventListener('resize', onWindowChange);
426
- window.removeEventListener('scroll', onWindowChange, true);
427
- };
428
- });
429
-
430
- $effect(() => {
431
- if (!withPicker || !pickerOpen || disabled) {
432
- return;
433
- }
434
-
435
- const overlay = registerOverlay({
436
- kind: 'popover',
437
- mount: disablePortal ? 'inline' : 'portal',
438
- });
439
- pickerZIndex = overlay.layer.content;
440
-
441
- return () => {
442
- overlay.unregister();
443
- pickerZIndex = 1100;
444
- };
445
- });
1
+ <script lang="ts">import { tick } from "svelte";
2
+ import Portal from "../../Portal/Portal.svelte";
3
+ import { registerOverlay } from "../../OverlayStack";
4
+ import DateTimeCtrlPickerPanel from "./DateTimeCtrlPickerPanel.svelte";
5
+ import { formatDateTimeByFormat, getCalendarCells, getNowDateTimeParts, getYearMonthLabel, isDateSelectable, normalizePartsForMode, parseDateTimeByFormat, parsePartialDateTimeByFormat, parseNativeDate, resolveDateTimeFormat, shiftYearMonth, toNativeDate, validateDateTimeParts } from "./DateTimeCtrl.utils";
6
+ let { amLabel, closeOnPick = true, clearable = true, disableAutocomplete = false, disablePortal = false, disabled, error, hoursStep, inputFormat = "ISO", label, maxDate, maxTime, minDate, minTime, minutesStep, mode = "datetime", name, onblur, onchange, outputFormat = "ISO", pmLabel, popoverProps, placeholder, required, sid, tooltip, secondsStep, timePickerProps, timePresets, use12Hour, value = $bindable(null), withPicker = true, withTimeDropdown } = $props();
7
+ const id = $derived(`field_${name ?? ""}_${sid ?? ""}`);
8
+ const pickerDialogId = $derived(`${id}_picker_dialog`);
9
+ const resolvedInputFormat = $derived(resolveDateTimeFormat(inputFormat, mode));
10
+ const resolvedOutputFormat = $derived(resolveDateTimeFormat(outputFormat, mode));
11
+ const showDatePicker = $derived(mode === "date" || mode === "datetime");
12
+ const showTimePicker = $derived(mode === "time" || mode === "datetime");
13
+ const rightSectionCount = $derived((withPicker ? 1 : 0) + (clearable ? 1 : 0));
14
+ const inputRightPaddingClass = $derived(rightSectionCount >= 2 ? "pr-20" : rightSectionCount === 1 ? "pr-12" : "pr-3");
15
+ const resolvedUse12Hour = $derived(use12Hour ?? timePickerProps?.use12Hour ?? false);
16
+ const resolvedWithTimeDropdown = $derived(withTimeDropdown ?? timePickerProps?.withDropdown ?? false);
17
+ const resolvedHoursStep = $derived(hoursStep ?? timePickerProps?.hoursStep ?? 1);
18
+ const resolvedMinutesStep = $derived(minutesStep ?? timePickerProps?.minutesStep ?? 1);
19
+ const resolvedSecondsStep = $derived(secondsStep ?? timePickerProps?.secondsStep ?? 1);
20
+ const resolvedAmLabel = $derived(amLabel ?? timePickerProps?.amLabel ?? "AM");
21
+ const resolvedPmLabel = $derived(pmLabel ?? timePickerProps?.pmLabel ?? "PM");
22
+ const resolvedTimePresets = $derived(timePresets ?? timePickerProps?.presets ?? []);
23
+ const closeOnClickOutside = $derived(popoverProps?.closeOnClickOutside ?? true);
24
+ const closeOnEscape = $derived(popoverProps?.closeOnEscape ?? true);
25
+ const dialogAriaLabel = $derived(popoverProps?.dialogAriaLabel ?? "Date time picker");
26
+ const panelClass = $derived(popoverProps?.panelClass ?? "");
27
+ let displayValue = $state("");
28
+ let pickerOpen = $state(false);
29
+ let pickerHour = $state(0);
30
+ let pickerMinute = $state(0);
31
+ let pickerSecond = $state(0);
32
+ let selectedDateIso = $state("");
33
+ let calendarYear = $state(getNowDateTimeParts().year);
34
+ let calendarMonth = $state(getNowDateTimeParts().month);
35
+ let localError = $state(undefined);
36
+ const effectiveError = $derived(error ?? localError);
37
+ let anchorRef = $state(null);
38
+ let popupTop = $state(0);
39
+ let popupLeft = $state(0);
40
+ let popupWidth = $state(0);
41
+ let pickerZIndex = $state(1100);
42
+ const calendarMonthLabel = $derived(getYearMonthLabel(calendarYear, calendarMonth));
43
+ const calendarCells = $derived(getCalendarCells(calendarYear, calendarMonth));
44
+ function syncPickerFromParts(parts) {
45
+ selectedDateIso = toNativeDate(parts);
46
+ pickerHour = parts.hour;
47
+ pickerMinute = parts.minute;
48
+ pickerSecond = parts.second;
49
+ calendarYear = parts.year;
50
+ calendarMonth = parts.month;
51
+ }
52
+ function getCurrentParts() {
53
+ const trimmed = displayValue.trim();
54
+ if (trimmed === "") {
55
+ const now = normalizePartsForMode(getNowDateTimeParts(), mode);
56
+ return now ?? getNowDateTimeParts();
57
+ }
58
+ const parsed = parseDateTimeByFormat(trimmed, resolvedInputFormat, mode);
59
+ if (parsed) {
60
+ return parsed;
61
+ }
62
+ const now = normalizePartsForMode(getNowDateTimeParts(), mode);
63
+ return now ?? getNowDateTimeParts();
64
+ }
65
+ function emitParts(parts) {
66
+ const normalizedInput = formatDateTimeByFormat(parts, resolvedInputFormat);
67
+ const normalizedOutput = formatDateTimeByFormat(parts, resolvedOutputFormat);
68
+ displayValue = normalizedInput;
69
+ value = normalizedOutput;
70
+ syncPickerFromParts(parts);
71
+ onchange?.(normalizedOutput);
72
+ }
73
+ function applyParts(parts, closeAfterApply = false) {
74
+ const normalized = normalizePartsForMode(parts, mode);
75
+ if (!normalized) {
76
+ localError = "Invalid date/time value";
77
+ return false;
78
+ }
79
+ const constraintError = validateDateTimeParts(normalized, mode, {
80
+ minDate,
81
+ maxDate,
82
+ minTime,
83
+ maxTime
84
+ });
85
+ if (constraintError) {
86
+ localError = constraintError;
87
+ return false;
88
+ }
89
+ localError = undefined;
90
+ emitParts(normalized);
91
+ if (closeAfterApply && closeOnPick) {
92
+ pickerOpen = false;
93
+ }
94
+ return true;
95
+ }
96
+ function syncFromExternalValue() {
97
+ const incoming = value;
98
+ if (!incoming || incoming.trim() === "") {
99
+ displayValue = "";
100
+ selectedDateIso = "";
101
+ localError = undefined;
102
+ return;
103
+ }
104
+ const parsed = parseDateTimeByFormat(incoming, resolvedOutputFormat, mode);
105
+ if (!parsed) {
106
+ displayValue = incoming;
107
+ selectedDateIso = "";
108
+ return;
109
+ }
110
+ displayValue = formatDateTimeByFormat(parsed, resolvedInputFormat);
111
+ syncPickerFromParts(parsed);
112
+ }
113
+ function clearValue() {
114
+ displayValue = "";
115
+ selectedDateIso = "";
116
+ localError = undefined;
117
+ value = null;
118
+ onchange?.(null);
119
+ }
120
+ function commitFromTextInput() {
121
+ const trimmed = displayValue.trim();
122
+ if (trimmed === "") {
123
+ clearValue();
124
+ return;
125
+ }
126
+ const parsed = parseDateTimeByFormat(trimmed, resolvedInputFormat, mode);
127
+ if (parsed) {
128
+ applyParts(parsed);
129
+ return;
130
+ }
131
+ // Fallback: if input is partial, complete missing segments from current date/time
132
+ // and commit without blocking tab/blur flow.
133
+ if (!disableAutocomplete) {
134
+ const partialParsed = parsePartialDateTimeByFormat(trimmed, resolvedInputFormat, mode);
135
+ if (partialParsed) {
136
+ applyParts(partialParsed);
137
+ return;
138
+ }
139
+ }
140
+ // Do not block blur/tab flow on partial or non-matching input.
141
+ // Keep current text and skip hard error here.
142
+ localError = undefined;
143
+ }
144
+ function handleSelectCalendarDate(isoDate) {
145
+ const parsedDate = parseNativeDate(isoDate);
146
+ if (!parsedDate) {
147
+ return;
148
+ }
149
+ const current = getCurrentParts();
150
+ current.year = parsedDate.year;
151
+ current.month = parsedDate.month;
152
+ current.day = parsedDate.day;
153
+ const closeAfterApply = mode === "date";
154
+ applyParts(current, closeAfterApply);
155
+ }
156
+ function handleTimeChange(value) {
157
+ const current = getCurrentParts();
158
+ current.hour = value.hour;
159
+ current.minute = value.minute;
160
+ current.second = value.second;
161
+ const closeAfterApply = mode === "time";
162
+ applyParts(current, closeAfterApply);
163
+ }
164
+ function handleNow() {
165
+ const now = normalizePartsForMode(getNowDateTimeParts(), mode);
166
+ if (!now) {
167
+ return;
168
+ }
169
+ applyParts(now, true);
170
+ }
171
+ function shiftMonth(delta) {
172
+ const shifted = shiftYearMonth(calendarYear, calendarMonth, delta);
173
+ calendarYear = shifted.year;
174
+ calendarMonth = shifted.month;
175
+ }
176
+ function handleTextKeydown(event) {
177
+ if (event.key === "Enter") {
178
+ commitFromTextInput();
179
+ event.currentTarget.blur();
180
+ return;
181
+ }
182
+ if (event.key === "Escape" && closeOnEscape) {
183
+ pickerOpen = false;
184
+ }
185
+ }
186
+ function handleInputBlur() {
187
+ commitFromTextInput();
188
+ onblur?.();
189
+ }
190
+ function updatePortalPosition() {
191
+ if (!anchorRef) {
192
+ return;
193
+ }
194
+ const rect = anchorRef.getBoundingClientRect();
195
+ const viewportWidth = typeof window === "undefined" ? rect.width : window.innerWidth;
196
+ const horizontalPadding = 8;
197
+ const desktopMaxWidth = 560;
198
+ const maxWidth = Math.min(desktopMaxWidth, Math.max(280, viewportWidth - horizontalPadding * 2));
199
+ const nextWidth = Math.min(Math.max(rect.width, 280), maxWidth);
200
+ const maxLeft = viewportWidth - nextWidth - horizontalPadding;
201
+ popupTop = rect.bottom + 8;
202
+ popupWidth = nextWidth;
203
+ popupLeft = Math.min(Math.max(rect.left, horizontalPadding), Math.max(horizontalPadding, maxLeft));
204
+ }
205
+ function handlePickerKeydown(event) {
206
+ if (event.key === "Escape" && closeOnEscape) {
207
+ event.preventDefault();
208
+ pickerOpen = false;
209
+ }
210
+ }
211
+ async function focusPickerFirstControl() {
212
+ if (!pickerOpen || !showDatePicker || typeof document === "undefined") {
213
+ return;
214
+ }
215
+ await tick();
216
+ const dialog = document.getElementById(pickerDialogId);
217
+ if (!dialog) {
218
+ return;
219
+ }
220
+ const selectedDay = dialog.querySelector("button[data-calendar-day=\"true\"][aria-selected=\"true\"]:not([disabled])");
221
+ if (selectedDay) {
222
+ selectedDay.focus();
223
+ return;
224
+ }
225
+ const firstEnabledDay = dialog.querySelector("button[data-calendar-day=\"true\"]:not([disabled])");
226
+ firstEnabledDay?.focus();
227
+ }
228
+ $effect(() => {
229
+ syncFromExternalValue();
230
+ });
231
+ $effect(() => {
232
+ if (!pickerOpen || disabled || !closeOnClickOutside || typeof document === "undefined") {
233
+ return;
234
+ }
235
+ const onDocumentPointerDown = (event) => {
236
+ const target = event.target;
237
+ const dialog = document.getElementById(pickerDialogId);
238
+ const insideAnchor = !!(target && anchorRef?.contains(target));
239
+ const insideDialog = !!(target && dialog?.contains(target));
240
+ if (!insideAnchor && !insideDialog) {
241
+ pickerOpen = false;
242
+ }
243
+ };
244
+ document.addEventListener("mousedown", onDocumentPointerDown);
245
+ return () => {
246
+ document.removeEventListener("mousedown", onDocumentPointerDown);
247
+ };
248
+ });
249
+ $effect(() => {
250
+ if (disablePortal || !pickerOpen || disabled) {
251
+ return;
252
+ }
253
+ updatePortalPosition();
254
+ if (typeof window === "undefined") {
255
+ return;
256
+ }
257
+ const onWindowChange = () => {
258
+ updatePortalPosition();
259
+ };
260
+ window.addEventListener("resize", onWindowChange);
261
+ window.addEventListener("scroll", onWindowChange, true);
262
+ return () => {
263
+ window.removeEventListener("resize", onWindowChange);
264
+ window.removeEventListener("scroll", onWindowChange, true);
265
+ };
266
+ });
267
+ $effect(() => {
268
+ if (!withPicker || !pickerOpen || disabled) {
269
+ return;
270
+ }
271
+ const overlay = registerOverlay({
272
+ kind: "popover",
273
+ mount: disablePortal ? "inline" : "portal"
274
+ });
275
+ pickerZIndex = overlay.layer.content;
276
+ return () => {
277
+ overlay.unregister();
278
+ pickerZIndex = 1100;
279
+ };
280
+ });
446
281
  </script>
447
282
 
448
283
  <div class="flex flex-col gap-1" title={tooltip}>