@warkypublic/svelix 0.1.46 → 0.2.1

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 (129) 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 +492 -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 +489 -614
  13. package/dist/components/CardGrid/CardGrid.svelte.d.ts +4 -0
  14. package/dist/components/CardGrid/CardGridFilterPanel.svelte +111 -90
  15. package/dist/components/CardGrid/DefaultCard.svelte +86 -97
  16. package/dist/components/CardGrid/DefaultCard.svelte.d.ts +1 -0
  17. package/dist/components/CardGrid/ImageCardStory.svelte +87 -36
  18. package/dist/components/CardGrid/cardGrid.d.ts +24 -0
  19. package/dist/components/CardGrid/cardGrid.js +20 -0
  20. package/dist/components/ContentEditor/CollaboraInsertTextDemo.svelte +5 -10
  21. package/dist/components/ContentEditor/ContentEditor.svelte +24 -50
  22. package/dist/components/ContentEditor/subcomponents/AudioPlayer.svelte +17 -17
  23. package/dist/components/ContentEditor/subcomponents/CollaboraEditor.svelte +244 -309
  24. package/dist/components/ContentEditor/subcomponents/EmailViewer.svelte +2 -3
  25. package/dist/components/ContentEditor/subcomponents/ImageViewer.svelte +14 -15
  26. package/dist/components/ContentEditor/subcomponents/MarkdownViewer.svelte +74 -90
  27. package/dist/components/ContentEditor/subcomponents/MonacoEditor.svelte +75 -103
  28. package/dist/components/ContentEditor/subcomponents/Office365Editor.svelte +234 -297
  29. package/dist/components/ContentEditor/subcomponents/PdfViewer.svelte +14 -20
  30. package/dist/components/ContentEditor/subcomponents/TextEditor.svelte +122 -150
  31. package/dist/components/ContentEditor/subcomponents/UnknownFile.svelte +105 -126
  32. package/dist/components/ContentEditor/subcomponents/VideoPlayer.svelte +17 -17
  33. package/dist/components/ContentEditor/subcomponents/ZipViewer.svelte +2 -3
  34. package/dist/components/ErrorBoundary/ErrorBoundary.svelte +41 -58
  35. package/dist/components/ErrorBoundary/ErrorBoundaryPreview.svelte +8 -16
  36. package/dist/components/Former/Former.svelte +306 -424
  37. package/dist/components/Former/FormerButtonArea.svelte +17 -68
  38. package/dist/components/Former/FormerDrawer.svelte +16 -80
  39. package/dist/components/Former/FormerDrawerPreview.svelte +99 -89
  40. package/dist/components/Former/FormerModal.svelte +16 -74
  41. package/dist/components/Former/FormerModalPreview.svelte +99 -89
  42. package/dist/components/Former/FormerPreview.svelte +76 -78
  43. package/dist/components/Former/FormerRestApiPreview.svelte +55 -48
  44. package/dist/components/Former/formerOverlayHelpers.svelte.d.ts +11 -0
  45. package/dist/components/Former/formerOverlayHelpers.svelte.js +38 -0
  46. package/dist/components/Former/index.d.ts +1 -0
  47. package/dist/components/Former/index.js +1 -0
  48. package/dist/components/FormerControllers/ButtonCtrl.svelte +13 -49
  49. package/dist/components/FormerControllers/CtrlFieldShell.svelte +15 -0
  50. package/dist/components/FormerControllers/CtrlFieldShell.svelte.d.ts +12 -0
  51. package/dist/components/FormerControllers/DateTimeCtrl/DateTimeCtrl.svelte +285 -457
  52. package/dist/components/FormerControllers/DateTimeCtrl/DateTimeCtrlCalendar.svelte +95 -136
  53. package/dist/components/FormerControllers/DateTimeCtrl/DateTimeCtrlPickerPanel.svelte +3 -62
  54. package/dist/components/FormerControllers/DateTimeCtrl/DateTimeCtrlTimeFields.svelte +143 -217
  55. package/dist/components/FormerControllers/IconButtonCtrl.svelte +13 -47
  56. package/dist/components/FormerControllers/InlineWrapper.svelte +12 -41
  57. package/dist/components/FormerControllers/InlineWrapperPreview.svelte +4 -26
  58. package/dist/components/FormerControllers/NativeSelectCtrl.svelte +7 -39
  59. package/dist/components/FormerControllers/NumberInputCtrl.svelte +21 -43
  60. package/dist/components/FormerControllers/PasswordInputCtrl.svelte +8 -33
  61. package/dist/components/FormerControllers/SwitchCtrl.svelte +4 -20
  62. package/dist/components/FormerControllers/TextAreaCtrl.svelte +7 -34
  63. package/dist/components/FormerControllers/TextInputCtrl.svelte +7 -34
  64. package/dist/components/FormerControllers/ctrlLoading.svelte.d.ts +8 -0
  65. package/dist/components/FormerControllers/ctrlLoading.svelte.js +43 -0
  66. package/dist/components/FormerControllers/index.d.ts +2 -0
  67. package/dist/components/FormerControllers/index.js +2 -0
  68. package/dist/components/FormerControllers/utils.d.ts +1 -0
  69. package/dist/components/FormerControllers/utils.js +3 -0
  70. package/dist/components/GlobalStateStore/GlobalStateStoreProvider.svelte +73 -120
  71. package/dist/components/Gridler/components/Gridler.svelte +344 -442
  72. package/dist/components/Gridler/components/GridlerCanvas.svelte +720 -1115
  73. package/dist/components/Gridler/components/GridlerEditor.svelte +2 -15
  74. package/dist/components/Gridler/components/GridlerFull.svelte +521 -841
  75. package/dist/components/Gridler/components/GridlerFull.svelte.d.ts +1 -1
  76. package/dist/components/Gridler/components/GridlerFullWithFormerPreview.svelte +194 -147
  77. package/dist/components/Gridler/components/GridlerSearch.svelte +7 -16
  78. package/dist/components/Gridler/components/GridlerSearchToggle.svelte +7 -19
  79. package/dist/components/Gridler/types.d.ts +1 -1
  80. package/dist/components/Gridler/utils/serverPaging.d.ts +1 -0
  81. package/dist/components/Gridler/utils/serverPaging.js +7 -0
  82. package/dist/components/Icons/IconAdd.svelte +1 -3
  83. package/dist/components/Icons/IconAlertCircle.svelte +1 -3
  84. package/dist/components/Icons/IconAlertTriangle.svelte +1 -3
  85. package/dist/components/Icons/IconCamera.svelte +1 -3
  86. package/dist/components/Icons/IconClose.svelte +1 -3
  87. package/dist/components/Icons/IconEdit.svelte +1 -3
  88. package/dist/components/Icons/IconFilter.svelte +1 -3
  89. package/dist/components/Icons/IconSearch.svelte +1 -3
  90. package/dist/components/Icons/IconSort.svelte +1 -3
  91. package/dist/components/Icons/IconTrash.svelte +1 -3
  92. package/dist/components/Portal/Portal.svelte +14 -38
  93. package/dist/components/Screenshot/Screenshot.svelte +7 -13
  94. package/dist/components/Svark/Svark.svelte +680 -852
  95. package/dist/components/Svark/SvarkContextMenu.svelte +44 -63
  96. package/dist/components/Svark/SvarkInfiniteLoader.svelte +2 -6
  97. package/dist/components/Svark/SvarkPager.svelte +2 -28
  98. package/dist/components/Svark/SvarkSelectionDemo.svelte +4 -8
  99. package/dist/components/Svark/SvarkTopBar.svelte +9 -74
  100. package/dist/components/SvarkGrid/SvarkGrid.svelte +877 -1123
  101. package/dist/components/SvarkGrid/components/SvarkColumnFilterForm.svelte +45 -58
  102. package/dist/components/SvarkGrid/components/SvarkContextMenu.svelte +44 -63
  103. package/dist/components/SvarkGrid/components/SvarkHeaderFilterCell.svelte +41 -75
  104. package/dist/components/SvarkGrid/components/SvarkInfiniteLoader.svelte +2 -6
  105. package/dist/components/SvarkGrid/components/SvarkPager.svelte +2 -28
  106. package/dist/components/SvarkGrid/components/SvarkSearchPopover.svelte +8 -16
  107. package/dist/components/SvarkGrid/components/SvarkTopBar.svelte +6 -62
  108. package/dist/components/SvarkGrid/internal/SvarkGridView.svelte +345 -507
  109. package/dist/components/VTree/VTree.svelte +454 -431
  110. package/dist/components/VTree/VTree.utils.d.ts +23 -0
  111. package/dist/components/VTree/VTree.utils.js +73 -0
  112. package/dist/components/VTree/VTreeContextMenu.svelte +21 -33
  113. package/dist/components/VTree/VTreeControlledSelectionDemo.svelte +20 -0
  114. package/dist/components/VTree/VTreeControlledSelectionDemo.svelte.d.ts +18 -0
  115. package/dist/components/VTree/VTreeCustomizationDemo.svelte +23 -0
  116. package/dist/components/VTree/VTreeCustomizationDemo.svelte.d.ts +18 -0
  117. package/dist/components/VTree/VTreeEventsDemo.svelte +48 -58
  118. package/dist/components/VTree/VTreeInteractionDemo.svelte +29 -0
  119. package/dist/components/VTree/VTreeInteractionDemo.svelte.d.ts +18 -0
  120. package/dist/components/VTree/VTreeMultiSelectDemo.svelte +27 -0
  121. package/dist/components/VTree/VTreeMultiSelectDemo.svelte.d.ts +18 -0
  122. package/dist/components/VTree/VTreeRow.svelte +77 -115
  123. package/dist/components/VTree/VTreeRow.svelte.d.ts +34 -18
  124. package/dist/components/VTree/VTreeSearch.svelte +6 -13
  125. package/dist/components/VTree/VTreeVirtualViewport.svelte +54 -143
  126. package/dist/components/VTree/VTreeVirtualViewport.svelte.d.ts +35 -18
  127. package/dist/components/VTree/types.d.ts +39 -1
  128. package/llm/COMPONENT_GUIDE.md +25 -0
  129. package/package.json +51 -50
@@ -1,462 +1,294 @@
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 CtrlFieldShell from "../CtrlFieldShell.svelte";
5
+ import { makeFieldId } from "../utils";
6
+ import DateTimeCtrlPickerPanel from "./DateTimeCtrlPickerPanel.svelte";
7
+ import { formatDateTimeByFormat, getCalendarCells, getNowDateTimeParts, getYearMonthLabel, isDateSelectable, normalizePartsForMode, parseDateTimeByFormat, parsePartialDateTimeByFormat, parseNativeDate, resolveDateTimeFormat, shiftYearMonth, toNativeDate, validateDateTimeParts } from "./DateTimeCtrl.utils";
8
+ 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();
9
+ const id = $derived(makeFieldId(name, sid));
10
+ const pickerDialogId = $derived(`${id}_picker_dialog`);
11
+ const resolvedInputFormat = $derived(resolveDateTimeFormat(inputFormat, mode));
12
+ const resolvedOutputFormat = $derived(resolveDateTimeFormat(outputFormat, mode));
13
+ const showDatePicker = $derived(mode === "date" || mode === "datetime");
14
+ const showTimePicker = $derived(mode === "time" || mode === "datetime");
15
+ const rightSectionCount = $derived((withPicker ? 1 : 0) + (clearable ? 1 : 0));
16
+ const inputRightPaddingClass = $derived(rightSectionCount >= 2 ? "pr-20" : rightSectionCount === 1 ? "pr-12" : "pr-3");
17
+ const resolvedUse12Hour = $derived(use12Hour ?? timePickerProps?.use12Hour ?? false);
18
+ const resolvedWithTimeDropdown = $derived(withTimeDropdown ?? timePickerProps?.withDropdown ?? false);
19
+ const resolvedHoursStep = $derived(hoursStep ?? timePickerProps?.hoursStep ?? 1);
20
+ const resolvedMinutesStep = $derived(minutesStep ?? timePickerProps?.minutesStep ?? 1);
21
+ const resolvedSecondsStep = $derived(secondsStep ?? timePickerProps?.secondsStep ?? 1);
22
+ const resolvedAmLabel = $derived(amLabel ?? timePickerProps?.amLabel ?? "AM");
23
+ const resolvedPmLabel = $derived(pmLabel ?? timePickerProps?.pmLabel ?? "PM");
24
+ const resolvedTimePresets = $derived(timePresets ?? timePickerProps?.presets ?? []);
25
+ const closeOnClickOutside = $derived(popoverProps?.closeOnClickOutside ?? true);
26
+ const closeOnEscape = $derived(popoverProps?.closeOnEscape ?? true);
27
+ const dialogAriaLabel = $derived(popoverProps?.dialogAriaLabel ?? "Date time picker");
28
+ const panelClass = $derived(popoverProps?.panelClass ?? "");
29
+ let displayValue = $state("");
30
+ let pickerOpen = $state(false);
31
+ let pickerHour = $state(0);
32
+ let pickerMinute = $state(0);
33
+ let pickerSecond = $state(0);
34
+ let selectedDateIso = $state("");
35
+ let calendarYear = $state(getNowDateTimeParts().year);
36
+ let calendarMonth = $state(getNowDateTimeParts().month);
37
+ let localError = $state(undefined);
38
+ const effectiveError = $derived(error ?? localError);
39
+ let anchorRef = $state(null);
40
+ let popupTop = $state(0);
41
+ let popupLeft = $state(0);
42
+ let popupWidth = $state(0);
43
+ let pickerZIndex = $state(1100);
44
+ const calendarMonthLabel = $derived(getYearMonthLabel(calendarYear, calendarMonth));
45
+ const calendarCells = $derived(getCalendarCells(calendarYear, calendarMonth));
46
+ function syncPickerFromParts(parts) {
47
+ selectedDateIso = toNativeDate(parts);
48
+ pickerHour = parts.hour;
49
+ pickerMinute = parts.minute;
50
+ pickerSecond = parts.second;
51
+ calendarYear = parts.year;
52
+ calendarMonth = parts.month;
53
+ }
54
+ function getCurrentParts() {
55
+ const trimmed = displayValue.trim();
56
+ if (trimmed === "") {
57
+ const now = normalizePartsForMode(getNowDateTimeParts(), mode);
58
+ return now ?? getNowDateTimeParts();
59
+ }
60
+ const parsed = parseDateTimeByFormat(trimmed, resolvedInputFormat, mode);
61
+ if (parsed) {
62
+ return parsed;
63
+ }
64
+ const now = normalizePartsForMode(getNowDateTimeParts(), mode);
65
+ return now ?? getNowDateTimeParts();
66
+ }
67
+ function emitParts(parts) {
68
+ const normalizedInput = formatDateTimeByFormat(parts, resolvedInputFormat);
69
+ const normalizedOutput = formatDateTimeByFormat(parts, resolvedOutputFormat);
70
+ displayValue = normalizedInput;
71
+ value = normalizedOutput;
72
+ syncPickerFromParts(parts);
73
+ onchange?.(normalizedOutput);
74
+ }
75
+ function applyParts(parts, closeAfterApply = false) {
76
+ const normalized = normalizePartsForMode(parts, mode);
77
+ if (!normalized) {
78
+ localError = "Invalid date/time value";
79
+ return false;
80
+ }
81
+ const constraintError = validateDateTimeParts(normalized, mode, {
82
+ minDate,
83
+ maxDate,
84
+ minTime,
85
+ maxTime
86
+ });
87
+ if (constraintError) {
88
+ localError = constraintError;
89
+ return false;
90
+ }
91
+ localError = undefined;
92
+ emitParts(normalized);
93
+ if (closeAfterApply && closeOnPick) {
94
+ pickerOpen = false;
95
+ }
96
+ return true;
97
+ }
98
+ function syncFromExternalValue() {
99
+ const incoming = value;
100
+ if (!incoming || incoming.trim() === "") {
101
+ displayValue = "";
102
+ selectedDateIso = "";
103
+ localError = undefined;
104
+ return;
105
+ }
106
+ const parsed = parseDateTimeByFormat(incoming, resolvedOutputFormat, mode);
107
+ if (!parsed) {
108
+ displayValue = incoming;
109
+ selectedDateIso = "";
110
+ return;
111
+ }
112
+ displayValue = formatDateTimeByFormat(parsed, resolvedInputFormat);
113
+ syncPickerFromParts(parsed);
114
+ }
115
+ function clearValue() {
116
+ displayValue = "";
117
+ selectedDateIso = "";
118
+ localError = undefined;
119
+ value = null;
120
+ onchange?.(null);
121
+ }
122
+ function commitFromTextInput() {
123
+ const trimmed = displayValue.trim();
124
+ if (trimmed === "") {
125
+ clearValue();
126
+ return;
127
+ }
128
+ const parsed = parseDateTimeByFormat(trimmed, resolvedInputFormat, mode);
129
+ if (parsed) {
130
+ applyParts(parsed);
131
+ return;
132
+ }
133
+ // Fallback: if input is partial, complete missing segments from current date/time
134
+ // and commit without blocking tab/blur flow.
135
+ if (!disableAutocomplete) {
136
+ const partialParsed = parsePartialDateTimeByFormat(trimmed, resolvedInputFormat, mode);
137
+ if (partialParsed) {
138
+ applyParts(partialParsed);
139
+ return;
140
+ }
141
+ }
142
+ // Do not block blur/tab flow on partial or non-matching input.
143
+ // Keep current text and skip hard error here.
144
+ localError = undefined;
145
+ }
146
+ function handleSelectCalendarDate(isoDate) {
147
+ const parsedDate = parseNativeDate(isoDate);
148
+ if (!parsedDate) {
149
+ return;
150
+ }
151
+ const current = getCurrentParts();
152
+ current.year = parsedDate.year;
153
+ current.month = parsedDate.month;
154
+ current.day = parsedDate.day;
155
+ const closeAfterApply = mode === "date";
156
+ applyParts(current, closeAfterApply);
157
+ }
158
+ function handleTimeChange(value) {
159
+ const current = getCurrentParts();
160
+ current.hour = value.hour;
161
+ current.minute = value.minute;
162
+ current.second = value.second;
163
+ const closeAfterApply = mode === "time";
164
+ applyParts(current, closeAfterApply);
165
+ }
166
+ function handleNow() {
167
+ const now = normalizePartsForMode(getNowDateTimeParts(), mode);
168
+ if (!now) {
169
+ return;
170
+ }
171
+ applyParts(now, true);
172
+ }
173
+ function shiftMonth(delta) {
174
+ const shifted = shiftYearMonth(calendarYear, calendarMonth, delta);
175
+ calendarYear = shifted.year;
176
+ calendarMonth = shifted.month;
177
+ }
178
+ function handleTextKeydown(event) {
179
+ if (event.key === "Enter") {
180
+ commitFromTextInput();
181
+ event.currentTarget.blur();
182
+ return;
183
+ }
184
+ if (event.key === "Escape" && closeOnEscape) {
185
+ pickerOpen = false;
186
+ }
187
+ }
188
+ function handleInputBlur() {
189
+ commitFromTextInput();
190
+ onblur?.();
191
+ }
192
+ function updatePortalPosition() {
193
+ if (!anchorRef) {
194
+ return;
195
+ }
196
+ const rect = anchorRef.getBoundingClientRect();
197
+ const viewportWidth = typeof window === "undefined" ? rect.width : window.innerWidth;
198
+ const horizontalPadding = 8;
199
+ const desktopMaxWidth = 560;
200
+ const maxWidth = Math.min(desktopMaxWidth, Math.max(280, viewportWidth - horizontalPadding * 2));
201
+ const nextWidth = Math.min(Math.max(rect.width, 280), maxWidth);
202
+ const maxLeft = viewportWidth - nextWidth - horizontalPadding;
203
+ popupTop = rect.bottom + 8;
204
+ popupWidth = nextWidth;
205
+ popupLeft = Math.min(Math.max(rect.left, horizontalPadding), Math.max(horizontalPadding, maxLeft));
206
+ }
207
+ function handlePickerKeydown(event) {
208
+ if (event.key === "Escape" && closeOnEscape) {
209
+ event.preventDefault();
210
+ pickerOpen = false;
211
+ }
212
+ }
213
+ async function focusPickerFirstControl() {
214
+ if (!pickerOpen || !showDatePicker || typeof document === "undefined") {
215
+ return;
216
+ }
217
+ await tick();
218
+ const dialog = document.getElementById(pickerDialogId);
219
+ if (!dialog) {
220
+ return;
221
+ }
222
+ const selectedDay = dialog.querySelector("button[data-calendar-day=\"true\"][aria-selected=\"true\"]:not([disabled])");
223
+ if (selectedDay) {
224
+ selectedDay.focus();
225
+ return;
226
+ }
227
+ const firstEnabledDay = dialog.querySelector("button[data-calendar-day=\"true\"]:not([disabled])");
228
+ firstEnabledDay?.focus();
229
+ }
230
+ $effect(() => {
231
+ syncFromExternalValue();
232
+ });
233
+ $effect(() => {
234
+ if (!pickerOpen || disabled || !closeOnClickOutside || typeof document === "undefined") {
235
+ return;
236
+ }
237
+ const onDocumentPointerDown = (event) => {
238
+ const target = event.target;
239
+ const dialog = document.getElementById(pickerDialogId);
240
+ const insideAnchor = !!(target && anchorRef?.contains(target));
241
+ const insideDialog = !!(target && dialog?.contains(target));
242
+ if (!insideAnchor && !insideDialog) {
243
+ pickerOpen = false;
244
+ }
245
+ };
246
+ document.addEventListener("mousedown", onDocumentPointerDown);
247
+ return () => {
248
+ document.removeEventListener("mousedown", onDocumentPointerDown);
249
+ };
250
+ });
251
+ $effect(() => {
252
+ if (disablePortal || !pickerOpen || disabled) {
253
+ return;
254
+ }
255
+ updatePortalPosition();
256
+ if (typeof window === "undefined") {
257
+ return;
258
+ }
259
+ const onWindowChange = () => {
260
+ updatePortalPosition();
261
+ };
262
+ window.addEventListener("resize", onWindowChange);
263
+ window.addEventListener("scroll", onWindowChange, true);
264
+ return () => {
265
+ window.removeEventListener("resize", onWindowChange);
266
+ window.removeEventListener("scroll", onWindowChange, true);
267
+ };
268
+ });
269
+ $effect(() => {
270
+ if (!withPicker || !pickerOpen || disabled) {
271
+ return;
272
+ }
273
+ const overlay = registerOverlay({
274
+ kind: "popover",
275
+ mount: disablePortal ? "inline" : "portal"
276
+ });
277
+ pickerZIndex = overlay.layer.content;
278
+ return () => {
279
+ overlay.unregister();
280
+ pickerZIndex = 1100;
281
+ };
282
+ });
446
283
  </script>
447
284
 
448
- <div class="flex flex-col gap-1" title={tooltip}>
449
- {#if label}
450
- <label class="label text-sm font-medium" for={id}>
451
- {label}{#if required}<span class="text-error-500">*</span>{/if}
452
- </label>
453
- {/if}
454
-
285
+ <CtrlFieldShell error={effectiveError} {id} {label} {required} {tooltip}>
455
286
  <div class="relative" bind:this={anchorRef}>
456
287
  <div class="relative">
457
288
  <input
458
289
  {id}
459
290
  {disabled}
291
+ {name}
460
292
  {required}
461
293
  class={`input ${inputRightPaddingClass}`}
462
294
  class:input-error={!!effectiveError}
@@ -644,8 +476,4 @@
644
476
  {/if}
645
477
  </Portal>
646
478
  </div>
647
-
648
- {#if effectiveError}
649
- <p class="text-xs text-error-500">{effectiveError}</p>
650
- {/if}
651
- </div>
479
+ </CtrlFieldShell>