@jsswift/ui 1.0.41

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 (55) hide show
  1. package/dist/css/animation.css +121 -0
  2. package/dist/css/base.css +621 -0
  3. package/dist/css/docs.css +216 -0
  4. package/dist/css/min-ui.css +1 -0
  5. package/dist/css/responsive.css +3259 -0
  6. package/dist/css/ui-components.css +8757 -0
  7. package/dist/css/ui.css +12765 -0
  8. package/dist/css/ui.min.css +1 -0
  9. package/dist/fonts/Nunito-Black.ttf +0 -0
  10. package/dist/fonts/Nunito-BlackItalic.ttf +0 -0
  11. package/dist/fonts/Nunito-Bold.ttf +0 -0
  12. package/dist/fonts/Nunito-BoldItalic.ttf +0 -0
  13. package/dist/fonts/Nunito-ExtraBold.ttf +0 -0
  14. package/dist/fonts/Nunito-ExtraBoldItalic.ttf +0 -0
  15. package/dist/fonts/Nunito-ExtraLight.ttf +0 -0
  16. package/dist/fonts/Nunito-ExtraLightItalic.ttf +0 -0
  17. package/dist/fonts/Nunito-Italic.ttf +0 -0
  18. package/dist/fonts/Nunito-Light.ttf +0 -0
  19. package/dist/fonts/Nunito-LightItalic.ttf +0 -0
  20. package/dist/fonts/Nunito-Medium.ttf +0 -0
  21. package/dist/fonts/Nunito-MediumItalic.ttf +0 -0
  22. package/dist/fonts/Nunito-Regular.ttf +0 -0
  23. package/dist/fonts/Nunito-SemiBold.ttf +0 -0
  24. package/dist/fonts/Nunito-SemiBoldItalic.ttf +0 -0
  25. package/dist/img/logo.svg +131 -0
  26. package/dist/img/svg/tabler-icons-sprite.svg +39534 -0
  27. package/dist/min-ui.js +34 -0
  28. package/dist/ui.js +21027 -0
  29. package/dist/ui.min.js +34 -0
  30. package/package.json +40 -0
  31. package/src/00-bootstrap.js +330 -0
  32. package/src/01-foundation-helpers.js +1208 -0
  33. package/src/10-primitives-layout.js +5972 -0
  34. package/src/20-display-content.js +1014 -0
  35. package/src/21-list-content.js +341 -0
  36. package/src/22-banner-content.js +489 -0
  37. package/src/23-tooltip.js +326 -0
  38. package/src/31-form-advanced.js +3149 -0
  39. package/src/40-navigation.js +1454 -0
  40. package/src/41-tab-panel.js +1090 -0
  41. package/src/50-feedback.js +1209 -0
  42. package/src/51-feedback-service.js +166 -0
  43. package/src/60-shell-app.js +1483 -0
  44. package/src/61-form-service.js +450 -0
  45. package/src/70-dialog.js +401 -0
  46. package/src/71-menu-overlays.js +1442 -0
  47. package/src/80-data.js +503 -0
  48. package/src/README.md +159 -0
  49. package/src/css/README.md +17 -0
  50. package/src/css/animation.css +121 -0
  51. package/src/css/base.css +621 -0
  52. package/src/css/docs.css +216 -0
  53. package/src/css/responsive.css +3259 -0
  54. package/src/css/ui-components.css +8757 -0
  55. package/src/modules.json +176 -0
@@ -0,0 +1,3149 @@
1
+ const buildChoiceControl = (type, args, options = {}) => {
2
+ const { props, children } = JSswift.uiNormalizeArgs(args);
3
+ const slots = props.slots || {};
4
+ const isRadio = type === "radio";
5
+ const isToggle = options.appearance === "toggle";
6
+ const supportsStandby = !isRadio;
7
+ const id = props.id || (`cms-${type}-` + Math.random().toString(36).slice(2));
8
+ const model = resolveModel(
9
+ props.model,
10
+ options.modelName || (isRadio ? "UI.Radio:model" : "UI.Checkbox:model")
11
+ );
12
+
13
+ const inputProps = JSswift.omit(props, [
14
+ "model", "label", "checked", "class", "style", "dense", "onChange", "onInput", "slots",
15
+ "icon", "iconOn", "iconOff", "iconStandby", "checkedIcon", "uncheckedIcon", "standbyIcon",
16
+ "indeterminateIcon", "inputClass", "iconSize", "color", "size", "outline", "behavior", "mode",
17
+ "flat", "glossy", "glow", "glass", "gradient", "textGradient", "lightShadow", "shadow",
18
+ "rounded", "radius", "textColor", "clickable", "border",
19
+ "shortcode", "shortcut", "hotkey", "showShortcode", "showShortcut"
20
+ ]);
21
+ inputProps.type = type;
22
+ inputProps.id = id;
23
+ if (isRadio && props.name != null) inputProps.name = props.name;
24
+ inputProps.class = uiClass([`cms-${type}`, "cms-choice-input", props.inputClass]);
25
+ const input = _.input(inputProps);
26
+
27
+ const labelNodes = renderSlotToArray(slots, "label", {}, props.label);
28
+ const labelContent = labelNodes.length ? labelNodes : renderSlotToArray(slots, "default", {}, children);
29
+ const shortcodeHint = uiCreateShortcodeHint(props, { className: "cms-shortcode cms-choice-shortcode" });
30
+ const finalLabelContent = shortcodeHint ? [...labelContent, shortcodeHint] : labelContent;
31
+
32
+ const wrapProps = JSswift.omit(props, [
33
+ "model", "label", "checked", "onChange", "onInput", "value", "name", "id", "type", "dense",
34
+ "inputClass", "slots", "icon", "iconOn", "iconOff", "iconStandby", "checkedIcon",
35
+ "uncheckedIcon", "standbyIcon", "indeterminateIcon", "iconSize", "color", "size", "behavior",
36
+ "mode",
37
+ "outline", "flat", "glossy", "glow", "glass", "gradient", "textGradient", "lightShadow",
38
+ "shadow", "rounded", "radius", "textColor", "clickable", "border",
39
+ "shortcode", "shortcut", "hotkey", "showShortcode", "showShortcut"
40
+ ]);
41
+ wrapProps.class = uiClass([
42
+ "cms-clear-set",
43
+ "cms-singularity-check",
44
+ "cms-choice-wrap",
45
+ `cms-${type}-wrap`,
46
+ uiWhen(isToggle, "cms-toggle-wrap"),
47
+ uiWhen(isToggle && isRadio, "cms-toggle-radio-wrap"),
48
+ uiWhen(isToggle && !isRadio, "cms-toggle-checkbox-wrap"),
49
+ uiWhen(props.dense, "dense"),
50
+ props.class
51
+ ]);
52
+ wrapProps.style = { ...(props.style || {}) };
53
+
54
+ const sizeValue = uiUnwrap(props.size);
55
+ if (sizeValue != null && !(typeof sizeValue === "string" && JSswift.uiSizes?.includes(sizeValue))) {
56
+ wrapProps.style["--cms-choice-size"] = toCssSize(sizeValue);
57
+ }
58
+
59
+ const marker = _.span({
60
+ class: uiClass([
61
+ "cms-choice-mark",
62
+ isRadio ? "cms-choice-radio-mark" : "cms-choice-checkbox-mark",
63
+ uiWhen(isToggle, "cms-toggle-mark")
64
+ ])
65
+ });
66
+ const indicatorHost = isToggle ? _.span({ class: "cms-toggle-thumb" }) : marker;
67
+ if (isToggle) marker.appendChild(indicatorHost);
68
+ const labelNode = finalLabelContent.length ? _.span({ class: "cms-choice-label" }, ...finalLabelContent) : null;
69
+
70
+ const wrap = _.label(
71
+ wrapProps,
72
+ input,
73
+ marker,
74
+ labelNode
75
+ );
76
+ setPropertyProps(wrap, props);
77
+
78
+ const iconSize = props.iconSize ? props.iconSize : props.size;
79
+ const defaultCheckedIcon = isToggle ? null : (isRadio ? "radio_button_checked" : "check");
80
+ const defaultUncheckedIcon = isToggle ? null : (isRadio ? "radio_button_unchecked" : null);
81
+ const defaultStandbyIcon = isToggle || isRadio ? null : "indeterminate_check_box";
82
+ const setInputState = (value) => {
83
+ if (isRadio) {
84
+ input.checked = value == props.value;
85
+ return input.checked;
86
+ }
87
+ const normalized = value == null ? null : !!value;
88
+ input.checked = normalized === true;
89
+ input.indeterminate = supportsStandby && normalized == null;
90
+ return normalized;
91
+ };
92
+ const getInputState = () => {
93
+ if (!isRadio && supportsStandby && input.indeterminate) return null;
94
+ return !!input.checked;
95
+ };
96
+ const resolveIconSource = (state) => {
97
+ if (state === true) {
98
+ if (props.checkedIcon != null) return props.checkedIcon;
99
+ if (props.icon != null) return props.icon;
100
+ return defaultCheckedIcon;
101
+ }
102
+ if (state === false) {
103
+ if (props.uncheckedIcon != null) return props.uncheckedIcon;
104
+ return defaultUncheckedIcon;
105
+ }
106
+ if (props.indeterminateIcon != null) return props.indeterminateIcon;
107
+ if (props.standbyIcon != null) return props.standbyIcon;
108
+ if (props.iconStandby != null) return props.iconStandby;
109
+ return defaultStandbyIcon;
110
+ };
111
+
112
+ const syncIndicator = () => {
113
+ const state = getInputState();
114
+ const checked = state === true;
115
+ while (indicatorHost.firstChild) indicatorHost.removeChild(indicatorHost.firstChild);
116
+ wrap.classList.toggle("is-checked", checked);
117
+ wrap.classList.toggle("is-indeterminate", state == null);
118
+ wrap.classList.toggle("is-disabled", !!input.disabled);
119
+ if (checked && (props.checkedIcon || props.icon)) {
120
+ wrap.classList.toggle("toggle-default", false);
121
+ } else if (checked && !props.checkedIcon) {
122
+ wrap.classList.toggle("toggle-default", true);
123
+ } else if (state == null && !props.standbyIcon) {
124
+ wrap.classList.toggle("toggle-default", true);
125
+ } else if (state == null && props.standbyIcon) {
126
+ wrap.classList.toggle("toggle-default", false);
127
+ } else if (input.disabled === false && !props.uncheckedIcon) {
128
+ wrap.classList.toggle("toggle-default", true);
129
+ } else {
130
+ wrap.classList.toggle("toggle-default", false);
131
+ }
132
+
133
+ if (isToggle) {
134
+ if (state == null) {
135
+ indicatorHost.style.transform = "translateX(calc((var(--cms-toggle-width) - var(--cms-toggle-thumb-size) - 1px) / 2))";
136
+ } else {
137
+ indicatorHost.style.removeProperty("transform");
138
+ }
139
+ }
140
+
141
+ const ctx = { checked, state, indeterminate: state == null, value: props.value, id, type };
142
+ let iconNode = null;
143
+ if (state === true) {
144
+ iconNode = JSswift.ui.renderSlot(slots, "checkedIcon", ctx, null);
145
+ if (iconNode == null) iconNode = JSswift.ui.renderSlot(slots, "iconOn", ctx, null);
146
+ } else if (state === false) {
147
+ iconNode = JSswift.ui.renderSlot(slots, "uncheckedIcon", ctx, null);
148
+ if (iconNode == null) iconNode = JSswift.ui.renderSlot(slots, "iconOff", ctx, null);
149
+ } else {
150
+ iconNode = JSswift.ui.renderSlot(slots, "indeterminateIcon", ctx, null);
151
+ if (iconNode == null) iconNode = JSswift.ui.renderSlot(slots, "standbyIcon", ctx, null);
152
+ if (iconNode == null) iconNode = JSswift.ui.renderSlot(slots, "iconStandby", ctx, null);
153
+ }
154
+ if (iconNode == null) iconNode = JSswift.ui.renderSlot(slots, "icon", ctx, null);
155
+ if (iconNode == null) {
156
+ const source = resolveIconSource(state);
157
+ if (typeof source === "string") iconNode = UI.Icon({ name: source, size: iconSize, ...(isToggle ? { textColor: props.color, outline: true } : {}) });
158
+ else if (source != null) {
159
+ iconNode = JSswift.ui.slot(source, {
160
+ checked,
161
+ state,
162
+ indeterminate: state == null,
163
+ as: state === true ? "checkedIcon" : (state === false ? "uncheckedIcon" : "indeterminateIcon")
164
+ });
165
+ }
166
+ }
167
+ renderSlotToArray(null, "default", {}, iconNode).forEach((n) => indicatorHost.appendChild(n));
168
+ };
169
+
170
+ if (model) {
171
+ setInputState(model.get());
172
+ model.watch((v) => {
173
+ setInputState(v);
174
+ syncIndicator();
175
+ }, isRadio ? "UI.Radio:watch" : "UI.Checkbox:watch");
176
+ input.addEventListener("change", (e) => {
177
+ if (isRadio) {
178
+ if (input.checked) {
179
+ model.set(props.value);
180
+ props.onChange?.(props.value, e);
181
+ }
182
+ } else {
183
+ const nextState = getInputState();
184
+ model.set(nextState);
185
+ props.onChange?.(nextState, e);
186
+ }
187
+ syncIndicator();
188
+ });
189
+ } else {
190
+ setInputState(isRadio ? (props.checked === true ? props.value : undefined) : props.checked);
191
+ input.addEventListener("change", (e) => {
192
+ if (isRadio) props.onChange?.(props.value, e);
193
+ else props.onChange?.(getInputState(), e);
194
+ syncIndicator();
195
+ });
196
+ }
197
+ if (props.onInput) {
198
+ input.addEventListener("input", (e) => {
199
+ if (isRadio) props.onInput?.(props.value, e);
200
+ else props.onInput?.(getInputState(), e);
201
+ });
202
+ }
203
+
204
+ syncIndicator();
205
+ uiRegisterShortcode(wrap, props, {
206
+ isEnabled: () => !input.disabled,
207
+ action: () => {
208
+ if (input.disabled) return false;
209
+ uiFocusShortcutTarget(input);
210
+ input.click();
211
+ }
212
+ });
213
+ return wrap;
214
+ };
215
+
216
+ UI.Checkbox = (...args) => buildChoiceControl("checkbox", args);
217
+ if (JSswift.isDev?.()) {
218
+ UI.meta = UI.meta || {};
219
+ UI.meta.Checkbox = {
220
+ signature: "UI.Checkbox(...children) | UI.Checkbox(props, ...children)",
221
+ props: {
222
+ label: "String|Node|Function|Array",
223
+ checked: "boolean|null",
224
+ model: "[get,set] signal",
225
+ icon: "String|Node|Function|Array",
226
+ iconOn: "Alias of icon/checkedIcon",
227
+ iconOff: "Alias of uncheckedIcon",
228
+ iconStandby: "Icon for null/indeterminate state",
229
+ checkedIcon: "String|Node|Function|Array",
230
+ uncheckedIcon: "String|Node|Function|Array",
231
+ color: "string",
232
+ size: "string|number",
233
+ outline: "boolean",
234
+ dense: "boolean",
235
+ shortcode: "string|Array<string>|object",
236
+ showShortcode: "boolean",
237
+ slots: "{ label?, default? }",
238
+ class: "string",
239
+ style: "object"
240
+ },
241
+ slots: {
242
+ label: "Checkbox label",
243
+ icon: "Base icon content",
244
+ iconStandby: "Slot for null/indeterminate icon",
245
+ checkedIcon: "Icon when checked",
246
+ uncheckedIcon: "Icon when unchecked",
247
+ default: "Fallback label content"
248
+ },
249
+ events: {
250
+ onChange: "(checked|null, event)",
251
+ onInput: "(checked|null, event)"
252
+ },
253
+ returns: "HTMLLabelElement",
254
+ description: "Checkbox con label e supporto model."
255
+ };
256
+ }
257
+ // Esempio: JSswift.ui.Checkbox({ label: "Accetto", model: [get,set] })
258
+
259
+ UI.Radio = (...args) => buildChoiceControl("radio", args);
260
+ if (JSswift.isDev?.()) {
261
+ UI.meta = UI.meta || {};
262
+ UI.meta.Radio = {
263
+ signature: "UI.Radio(...children) | UI.Radio(props, ...children)",
264
+ props: {
265
+ label: "String|Node|Function|Array",
266
+ value: "any",
267
+ name: "string",
268
+ checked: "boolean|null",
269
+ model: "[get,set] signal",
270
+ icon: "String|Node|Function|Array",
271
+ iconOn: "Alias of icon/checkedIcon",
272
+ iconOff: "Alias of uncheckedIcon",
273
+ checkedIcon: "String|Node|Function|Array",
274
+ uncheckedIcon: "String|Node|Function|Array",
275
+ color: "string",
276
+ size: "string|number",
277
+ outline: "boolean",
278
+ dense: "boolean",
279
+ shortcode: "string|Array<string>|object",
280
+ showShortcode: "boolean",
281
+ slots: "{ label?, default? }",
282
+ class: "string",
283
+ style: "object"
284
+ },
285
+ slots: {
286
+ label: "Radio label",
287
+ icon: "Base icon content",
288
+ iconOn: "Alias slot for checked icon",
289
+ iconOff: "Alias slot for unchecked icon",
290
+ checkedIcon: "Icon when checked",
291
+ uncheckedIcon: "Icon when unchecked",
292
+ default: "Fallback label content"
293
+ },
294
+ events: {
295
+ onChange: "(value, event)",
296
+ onInput: "(value, event)"
297
+ },
298
+ returns: "HTMLLabelElement",
299
+ description: "Radio con label e supporto model."
300
+ };
301
+ }
302
+ // Esempio: JSswift.ui.Radio({ name: "r1", value: "a", label: "A", model: [get,set] })
303
+
304
+ UI.Toggle = (...args) => {
305
+ const { props } = JSswift.uiNormalizeArgs(args);
306
+ const behavior = String(props.behavior ?? props.mode ?? props.type ?? "checkbox").toLowerCase() === "radio"
307
+ ? "radio"
308
+ : "checkbox";
309
+ return buildChoiceControl(behavior, args, {
310
+ appearance: "toggle",
311
+ modelName: "UI.Toggle:model"
312
+ });
313
+ };
314
+ if (JSswift.isDev?.()) {
315
+ UI.meta = UI.meta || {};
316
+ UI.meta.Toggle = {
317
+ signature: "UI.Toggle(...children) | UI.Toggle(props, ...children)",
318
+ props: {
319
+ label: "String|Node|Function|Array",
320
+ behavior: "\"checkbox\"|\"radio\"",
321
+ mode: "Alias of behavior",
322
+ value: "any",
323
+ name: "string",
324
+ checked: "boolean",
325
+ model: "[get,set] signal",
326
+ icon: "String|Node|Function|Array",
327
+ iconOn: "Alias of icon/checkedIcon",
328
+ iconOff: "Alias of uncheckedIcon",
329
+ iconStandby: "Icon for null/indeterminate state",
330
+ checkedIcon: "String|Node|Function|Array",
331
+ uncheckedIcon: "String|Node|Function|Array",
332
+ color: "string",
333
+ size: "string|number",
334
+ dense: "boolean",
335
+ shortcode: "string|Array<string>|object",
336
+ showShortcode: "boolean",
337
+ slots: "{ label?, default? }",
338
+ class: "string",
339
+ style: "object"
340
+ },
341
+ slots: {
342
+ label: "Toggle label",
343
+ icon: "Base icon content",
344
+ iconOn: "Alias slot for checked icon",
345
+ iconOff: "Alias slot for unchecked icon",
346
+ iconStandby: "Slot for null/indeterminate icon",
347
+ checkedIcon: "Icon when checked",
348
+ uncheckedIcon: "Icon when unchecked",
349
+ default: "Fallback label content"
350
+ },
351
+ events: {
352
+ onChange: "(checked|value, event)",
353
+ onInput: "(checked|value, event)"
354
+ },
355
+ returns: "HTMLLabelElement",
356
+ description: "Toggle switch con supporto model e comportamento checkbox/radio."
357
+ };
358
+ }
359
+ // Esempio: JSswift.ui.Toggle({ label: "Attivo", model: [get,set] })
360
+
361
+ UI.Slider = (...args) => {
362
+ const { props, children } = JSswift.uiNormalizeArgs(args);
363
+ const slots = props.slots || {};
364
+ const boundValue = props.model || ((uiIsSignal(props.value) || uiIsRod(props.value)) ? props.value : null);
365
+ const model = resolveModel(boundValue, "UI.Slider:model");
366
+
367
+ const inputProps = JSswift.omit(props, [
368
+ "model", "value", "class", "style", "onChange", "onInput", "slots",
369
+ "label", "icon", "iconRight", "thumbIcon", "iconThumb", "pointIcon",
370
+ "markers", "markerLabels", "labelMarks", "leftLabel", "rightLabel",
371
+ "startLabel", "endLabel", "minLabel", "maxLabel", "withQItem", "qitem",
372
+ "item", "itemClass", "itemStyle", "showValue", "thumbLabel", "labelValue",
373
+ "selectionColor", "trackColor", "thumbColor", "inputClass", "readonly", "size"
374
+ ]);
375
+ inputProps.type = "range";
376
+ inputProps.class = uiClass(["cms-slider-input", props.inputClass]);
377
+ const input = _.input(inputProps);
378
+
379
+ const sliderStyle = { ...(props.style || {}) };
380
+ const sizeValue = uiUnwrap(props.size);
381
+ if (sizeValue != null) {
382
+ const sliderSize = (typeof sizeValue === "string" && JSswift.uiSizes?.includes(sizeValue))
383
+ ? `var(--cms-size-${sizeValue})`
384
+ : toCssSize(sizeValue);
385
+ sliderStyle["--cms-slider-size"] = sliderSize;
386
+ sliderStyle["--cms-slider-track-size"] = `max(4px, calc(${sliderSize} / 4))`;
387
+ sliderStyle["--cms-slider-box-height"] = `calc(${sliderSize} * 1.5)`;
388
+ sliderStyle["--cms-slider-marker-size"] = `max(6px, calc(${sliderSize} / 3))`;
389
+ }
390
+
391
+ const wrap = _.label({
392
+ class: uiClass([
393
+ "cms-clear-set",
394
+ "cms-singularity-check",
395
+ "cms-slider-wrap",
396
+ uiWhen(props.dense, "dense"),
397
+ props.class
398
+ ]),
399
+ style: sliderStyle
400
+ });
401
+ setPropertyProps(wrap, props);
402
+
403
+ const header = _.div({
404
+ class: "cms-slider-header"
405
+ });
406
+ const labelHost = _.span({
407
+ class: "cms-slider-label"
408
+ });
409
+ const valueHost = _.span({
410
+ class: "cms-slider-value"
411
+ });
412
+ header.append(labelHost, valueHost);
413
+
414
+ const body = _.div({
415
+ class: "cms-slider-body"
416
+ });
417
+ const startIconHost = _.span({
418
+ class: "cms-slider-icon cms-slider-icon-left"
419
+ });
420
+ const startLabelHost = _.span({
421
+ class: "cms-slider-edge-label cms-slider-edge-label-left"
422
+ });
423
+ const main = _.div({
424
+ class: "cms-slider-main"
425
+ });
426
+ const sliderBox = _.div({
427
+ class: "cms-slider-box",
428
+ style: {
429
+ minHeight: "var(--cms-slider-box-height, 36px)"
430
+ }
431
+ });
432
+ const rail = _.span({
433
+ class: "cms-slider-rail",
434
+ style: {
435
+ height: "var(--cms-slider-track-size, 6px)"
436
+ }
437
+ });
438
+ const selection = _.span({
439
+ class: "cms-slider-selection",
440
+ style: {
441
+ height: "var(--cms-slider-track-size, 6px)"
442
+ }
443
+ });
444
+ const thumb = _.span({
445
+ class: "cms-slider-thumb",
446
+ style: {
447
+ width: "var(--cms-slider-size, 24px)",
448
+ height: "var(--cms-slider-size, 24px)"
449
+ }
450
+ });
451
+ const thumbIconHost = _.span({
452
+ class: "cms-slider-thumb-icon"
453
+ });
454
+ const thumbLabelHost = _.span({
455
+ class: "cms-slider-thumb-label"
456
+ });
457
+ thumb.append(thumbLabelHost, thumbIconHost);
458
+
459
+ const markersHost = _.div({
460
+ class: "cms-slider-markers"
461
+ });
462
+ const endLabelHost = _.span({
463
+ class: "cms-slider-edge-label cms-slider-edge-label-right"
464
+ });
465
+ const endIconHost = _.span({
466
+ class: "cms-slider-icon cms-slider-icon-right"
467
+ });
468
+
469
+ sliderBox.append(rail, selection, thumb, input);
470
+ main.append(sliderBox, markersHost);
471
+ body.append(startIconHost, startLabelHost, main, endLabelHost, endIconHost);
472
+ wrap.append(header, body);
473
+
474
+ const clearHost = (host) => {
475
+ while (host.firstChild) host.removeChild(host.firstChild);
476
+ };
477
+ const renderInto = (host, nodes, display = "") => {
478
+ clearHost(host);
479
+ (nodes || []).forEach((n) => host.appendChild(n));
480
+ host.style.display = host.childNodes.length ? display : "none";
481
+ };
482
+ const unwrapSlotValue = (value) => (uiIsSignal(value) || uiIsRod(value) ? uiUnwrap(value) : value);
483
+ const asArray = (value, ctx = {}) => slotToArray(unwrapSlotValue(value), ctx);
484
+ const asIconArray = (value, as, ctx = {}) => {
485
+ const resolved = unwrapSlotValue(value);
486
+ if (resolved == null) return [];
487
+ if (typeof resolved === "string") return [UI.Icon({ name: resolved, size: props.iconSize ?? props.size ?? 16 })];
488
+ return asArray(resolved, { ...ctx, as });
489
+ };
490
+ const getNumber = (value, fallback) => {
491
+ const next = Number(value);
492
+ return Number.isFinite(next) ? next : fallback;
493
+ };
494
+ const getMin = () => getNumber(uiUnwrap(props.min), 0);
495
+ const getMax = () => {
496
+ const min = getMin();
497
+ const max = getNumber(uiUnwrap(props.max), 100);
498
+ return max < min ? min : max;
499
+ };
500
+ const getStep = () => {
501
+ const raw = uiUnwrap(props.step);
502
+ if (raw === "any") return "any";
503
+ const step = getNumber(raw, 1);
504
+ return step > 0 ? step : 1;
505
+ };
506
+ const getPrecision = (value) => {
507
+ if (value === "any") return 0;
508
+ const str = String(value);
509
+ if (str.includes("e-")) return Number(str.split("e-")[1] || 0);
510
+ const idx = str.indexOf(".");
511
+ return idx === -1 ? 0 : str.length - idx - 1;
512
+ };
513
+ const normalizeValue = (value) => {
514
+ const min = getMin();
515
+ const max = getMax();
516
+ const step = getStep();
517
+ let next = getNumber(value, min);
518
+ if (step !== "any") {
519
+ next = min + Math.round((next - min) / step) * step;
520
+ const precision = getPrecision(step);
521
+ if (precision > 0) next = Number(next.toFixed(precision));
522
+ }
523
+ return Math.min(max, Math.max(min, next));
524
+ };
525
+ const ratioFromValue = (value) => {
526
+ const min = getMin();
527
+ const max = getMax();
528
+ const span = max - min;
529
+ if (!span) return 0;
530
+ const raw = (normalizeValue(value) - min) / span;
531
+ const ratio = uiUnwrap(props.reverse) ? (1 - raw) : raw;
532
+ return Math.max(0, Math.min(1, ratio));
533
+ };
534
+ const [getValue, setValue] = JSswift.reactive.signal(normalizeValue(
535
+ model ? model.get() : (uiUnwrap(props.value) ?? uiUnwrap(props.min) ?? 0)
536
+ ));
537
+
538
+ const setSliderValue = (raw, opts = {}) => {
539
+ const next = normalizeValue(raw);
540
+ if (getValue() !== next) setValue(next);
541
+ if (String(input.value) !== String(next)) input.value = String(next);
542
+ if (model && opts.fromModel !== true) model.set(next);
543
+ return next;
544
+ };
545
+
546
+ const getMarkerItems = () => {
547
+ const raw = uiUnwrap(props.markers);
548
+ const showLabels = !!uiUnwrap(props.markerLabels ?? props.labelMarks);
549
+ if (!raw) return [];
550
+ const min = getMin();
551
+ const max = getMax();
552
+ const normalizeMarker = (entry, index) => {
553
+ if (entry == null) return null;
554
+ if (typeof entry === "object" && !Array.isArray(entry)) {
555
+ const value = normalizeValue(entry.value ?? entry.position ?? entry.at ?? min);
556
+ return {
557
+ key: entry.key ?? `marker-${index}-${value}`,
558
+ value,
559
+ label: entry.label ?? (showLabels ? String(value) : null),
560
+ icon: entry.icon ?? null,
561
+ className: entry.class ?? entry.className ?? ""
562
+ };
563
+ }
564
+ const value = normalizeValue(entry);
565
+ return {
566
+ key: `marker-${index}-${value}`,
567
+ value,
568
+ label: showLabels ? String(entry) : null,
569
+ icon: null,
570
+ className: ""
571
+ };
572
+ };
573
+
574
+ if (Array.isArray(raw)) {
575
+ return raw.map((entry, index) => normalizeMarker(entry, index)).filter(Boolean);
576
+ }
577
+ if (typeof raw === "number" && raw > 1) {
578
+ const count = Math.floor(raw);
579
+ const stepValue = count > 1 ? (max - min) / (count - 1) : 0;
580
+ return Array.from({ length: count }, (_, index) => normalizeMarker(min + (stepValue * index), index)).filter(Boolean);
581
+ }
582
+ if (raw === true) {
583
+ const step = getStep();
584
+ if (step === "any") {
585
+ return [normalizeMarker(min, 0), normalizeMarker(max, 1)].filter(Boolean);
586
+ }
587
+ const count = Math.floor((max - min) / step) + 1;
588
+ if (count > 24) {
589
+ return [normalizeMarker(min, 0), normalizeMarker((min + max) / 2, 1), normalizeMarker(max, 2)].filter(Boolean);
590
+ }
591
+ return Array.from({ length: count }, (_, index) => normalizeMarker(min + (step * index), index)).filter(Boolean);
592
+ }
593
+ if (typeof raw === "object") {
594
+ return Object.keys(raw).map((key, index) => normalizeMarker({
595
+ value: Number(key),
596
+ label: raw[key]
597
+ }, index)).filter(Boolean);
598
+ }
599
+ return [];
600
+ };
601
+
602
+ const renderMarkers = () => {
603
+ clearHost(markersHost);
604
+ const markers = getMarkerItems();
605
+ markersHost.style.display = markers.length ? "block" : "none";
606
+ markersHost.style.minHeight = markers.some((marker) => marker.label != null && marker.label !== "") ? "30px" : "10px";
607
+ markers.forEach((marker) => {
608
+ const ratio = ratioFromValue(marker.value);
609
+ const active = uiUnwrap(props.reverse) ? getValue() <= marker.value : getValue() >= marker.value;
610
+ const item = _.button({
611
+ type: "button",
612
+ class: uiClass(["cms-slider-marker", marker.className, uiWhen(() => active, "active")]),
613
+ style: {
614
+ left: `${ratio * 100}%`,
615
+ color: active ? (uiUnwrap(props.color) || "var(--cms-primary)") : "var(--cms-muted)",
616
+ cursor: (uiUnwrap(props.disabled) || uiUnwrap(props.readonly)) ? "default" : "pointer"
617
+ },
618
+ disabled: !!uiUnwrap(props.disabled) || !!uiUnwrap(props.readonly),
619
+ onClick: () => {
620
+ if (uiUnwrap(props.disabled) || uiUnwrap(props.readonly)) return;
621
+ const next = setSliderValue(marker.value);
622
+ props.onInput?.(next);
623
+ props.onChange?.(next);
624
+ }
625
+ });
626
+ const markerCtx = {
627
+ marker,
628
+ active,
629
+ value: marker.value,
630
+ current: getValue(),
631
+ input
632
+ };
633
+ let markerNode = JSswift.ui.renderSlot(slots, "marker", markerCtx, null);
634
+ if (markerNode == null && marker.icon != null) {
635
+ markerNode = typeof marker.icon === "string"
636
+ ? UI.Icon({ name: marker.icon, size: 12 })
637
+ : JSswift.ui.slot(marker.icon, { ...markerCtx, as: "marker" });
638
+ }
639
+ const markerTick = _.span({
640
+ class: "cms-slider-marker-tick",
641
+ style: {
642
+ width: "var(--cms-slider-marker-size, 8px)",
643
+ height: "var(--cms-slider-marker-size, 8px)",
644
+ background: active ? (uiUnwrap(props.color) || "var(--cms-primary)") : "var(--cms-border-color)"
645
+ }
646
+ });
647
+ const markerNodes = markerNode == null
648
+ ? [markerTick]
649
+ : renderSlotToArray(null, "default", markerCtx, markerNode);
650
+ const labelNodes = marker.label == null
651
+ ? renderSlotToArray(slots, "markerLabel", markerCtx, null)
652
+ : renderSlotToArray(slots, "markerLabel", markerCtx, marker.label);
653
+ markerNodes.forEach((node) => item.appendChild(node));
654
+ if (labelNodes.length) {
655
+ item.appendChild(_.span({
656
+ class: "cms-slider-marker-label"
657
+ }, ...labelNodes));
658
+ }
659
+ markersHost.appendChild(item);
660
+ });
661
+ };
662
+
663
+ const renderHeader = () => {
664
+ const ctx = { value: getValue(), input, props };
665
+ let labelNodes = renderSlotToArray(slots, "label", ctx, unwrapSlotValue(props.label));
666
+ if (!labelNodes.length) labelNodes = renderSlotToArray(slots, "default", ctx, children);
667
+ renderInto(labelHost, labelNodes, "inline-flex");
668
+
669
+ const showValue = uiUnwrap(props.showValue);
670
+ const rawValueLabel = showValue === false
671
+ ? null
672
+ : (unwrapSlotValue(props.labelValue) ?? getValue());
673
+ const valueNodes = showValue || unwrapSlotValue(props.labelValue) != null
674
+ ? renderSlotToArray(slots, "value", ctx, rawValueLabel)
675
+ : [];
676
+ renderInto(valueHost, valueNodes, "inline-flex");
677
+ header.style.display = (labelHost.childNodes.length || valueHost.childNodes.length) ? "flex" : "none";
678
+ };
679
+
680
+ const renderAddons = () => {
681
+ const ctx = { value: getValue(), input, props };
682
+ const leftIcon = renderSlotToArray(slots, "icon", ctx, null);
683
+ const leftIconNodes = leftIcon.length ? leftIcon : asIconArray(props.icon, "icon", ctx);
684
+ renderInto(startIconHost, leftIconNodes, "inline-flex");
685
+
686
+ const rightIcon = renderSlotToArray(slots, "iconRight", ctx, null);
687
+ const rightIconNodes = rightIcon.length ? rightIcon : asIconArray(props.iconRight, "iconRight", ctx);
688
+ renderInto(endIconHost, rightIconNodes, "inline-flex");
689
+
690
+ const leftLabelSource = unwrapSlotValue(props.startLabel ?? props.leftLabel ?? props.minLabel);
691
+ const rightLabelSource = unwrapSlotValue(props.endLabel ?? props.rightLabel ?? props.maxLabel);
692
+ renderInto(startLabelHost, renderSlotToArray(slots, "startLabel", ctx, leftLabelSource), "inline-flex");
693
+ renderInto(endLabelHost, renderSlotToArray(slots, "endLabel", ctx, rightLabelSource), "inline-flex");
694
+ };
695
+
696
+ const renderThumb = () => {
697
+ const ctx = { value: getValue(), input, props };
698
+ const thumbIconNodes = renderSlotToArray(slots, "thumbIcon", ctx, null);
699
+ const thumbIconFallback = props.thumbIcon ?? props.iconThumb ?? props.pointIcon;
700
+ renderInto(
701
+ thumbIconHost,
702
+ thumbIconNodes.length ? thumbIconNodes : asIconArray(thumbIconFallback, "thumbIcon", ctx),
703
+ "inline-flex"
704
+ );
705
+
706
+ const rawThumbLabel = unwrapSlotValue(props.thumbLabel)
707
+ ?? unwrapSlotValue(props.labelValue)
708
+ ?? (uiUnwrap(props.showValue) ? String(getValue()) : null);
709
+ const thumbLabelNodes = renderSlotToArray(slots, "thumbLabel", ctx, rawThumbLabel);
710
+ renderInto(thumbLabelHost, thumbLabelNodes, "inline-flex");
711
+ };
712
+
713
+ const syncVisualState = () => {
714
+ const value = normalizeValue(getValue());
715
+ const min = getMin();
716
+ const max = getMax();
717
+ const step = getStep();
718
+ const ratio = ratioFromValue(value);
719
+ const percent = `${ratio * 100}%`;
720
+ const color = uiUnwrap(props.color) || uiUnwrap(props.selectionColor) || "var(--cms-primary)";
721
+ const trackColor = uiUnwrap(props.trackColor) || "var(--cms-border-color)";
722
+ const thumbColor = uiUnwrap(props.thumbColor) || color;
723
+ const readonly = !!uiUnwrap(props.readonly);
724
+ const disabled = !!uiUnwrap(props.disabled);
725
+
726
+ input.min = String(min);
727
+ input.max = String(max);
728
+ input.step = step === "any" ? "any" : String(step);
729
+ input.disabled = disabled;
730
+ input.value = String(value);
731
+
732
+ rail.style.background = trackColor;
733
+ selection.style.background = color;
734
+ selection.style.width = percent;
735
+ thumb.style.left = percent;
736
+ thumb.style.borderColor = thumbColor;
737
+ thumb.style.color = thumbColor;
738
+ thumbLabelHost.style.background = color;
739
+ wrap.classList.toggle("is-disabled", disabled);
740
+ wrap.classList.toggle("is-readonly", readonly);
741
+ wrap.classList.toggle("has-markers", markersHost.childNodes.length > 0);
742
+ };
743
+
744
+ if (model) {
745
+ setSliderValue(model.get(), { fromModel: true });
746
+ model.watch((v) => { setSliderValue(v, { fromModel: true }); }, "UI.Slider:watch");
747
+ } else if (uiIsReactive(props.value)) {
748
+ JSswift.reactive.effect(() => {
749
+ setSliderValue(uiUnwrap(props.value), { fromModel: true });
750
+ }, "UI.Slider:value");
751
+ } else {
752
+ setSliderValue(props.value ?? props.min ?? 0, { fromModel: true });
753
+ }
754
+
755
+ input.addEventListener("input", (e) => {
756
+ if (uiUnwrap(props.disabled) || uiUnwrap(props.readonly)) {
757
+ input.value = String(getValue());
758
+ return;
759
+ }
760
+ const next = setSliderValue(input.value);
761
+ props.onInput?.(next, e);
762
+ });
763
+ input.addEventListener("change", (e) => {
764
+ if (uiUnwrap(props.disabled) || uiUnwrap(props.readonly)) {
765
+ input.value = String(getValue());
766
+ return;
767
+ }
768
+ const next = setSliderValue(input.value);
769
+ props.onChange?.(next, e);
770
+ });
771
+
772
+ JSswift.reactive.effect(() => {
773
+ renderHeader();
774
+ renderAddons();
775
+ renderThumb();
776
+ renderMarkers();
777
+ syncVisualState();
778
+ }, "UI.Slider:render");
779
+
780
+ wrap._input = input;
781
+ wrap._getValue = getValue;
782
+ wrap._setValue = (value) => setSliderValue(value);
783
+
784
+ if (props.withQItem || props.qitem || props.item === true) {
785
+ const item = UI.Item({
786
+ class: uiClass(["cms-slider-item", props.itemClass]),
787
+ style: props.itemStyle
788
+ }, wrap);
789
+ item._input = input;
790
+ item._slider = wrap;
791
+ item._getValue = getValue;
792
+ item._setValue = wrap._setValue;
793
+ return item;
794
+ }
795
+
796
+ return wrap;
797
+ };
798
+ if (JSswift.isDev?.()) {
799
+ UI.meta = UI.meta || {};
800
+ UI.meta.Slider = {
801
+ signature: "UI.Slider(...children) | UI.Slider(props, ...children)",
802
+ props: {
803
+ min: "number",
804
+ max: "number",
805
+ step: "number|\"any\"",
806
+ value: "number | rod | [get,set] signal",
807
+ model: "rod | [get,set] signal",
808
+ label: "String|Node|Function|Array",
809
+ icon: "String|Node|Function|Array",
810
+ iconRight: "String|Node|Function|Array",
811
+ thumbIcon: "String|Node|Function|Array",
812
+ iconThumb: "Alias of thumbIcon",
813
+ pointIcon: "Alias of thumbIcon",
814
+ thumbLabel: "String|Node|Function|Array",
815
+ showValue: "boolean",
816
+ labelValue: "String|Node|Function|Array",
817
+ markers: "boolean|number|Array|Object",
818
+ markerLabels: "boolean",
819
+ labelMarks: "Alias of markerLabels",
820
+ startLabel: "String|Node|Function|Array",
821
+ endLabel: "String|Node|Function|Array",
822
+ leftLabel: "Alias of startLabel",
823
+ rightLabel: "Alias of endLabel",
824
+ minLabel: "Alias of startLabel",
825
+ maxLabel: "Alias of endLabel",
826
+ withQItem: "boolean",
827
+ qitem: "Alias of withQItem",
828
+ item: "boolean",
829
+ itemClass: "string",
830
+ itemStyle: "object",
831
+ selectionColor: "string",
832
+ trackColor: "string",
833
+ thumbColor: "string",
834
+ size: "string|number",
835
+ readonly: "boolean",
836
+ inputClass: "string",
837
+ slots: "{ label?, default?, value?, icon?, iconRight?, thumbIcon?, thumbLabel?, marker?, markerLabel?, startLabel?, endLabel? }",
838
+ class: "string",
839
+ style: "object"
840
+ },
841
+ slots: {
842
+ label: "Label content",
843
+ default: "Fallback label content",
844
+ value: "Header value content",
845
+ icon: "Left icon content",
846
+ iconRight: "Right icon content",
847
+ thumbIcon: "Thumb icon content",
848
+ thumbLabel: "Thumb label content",
849
+ marker: "Marker content",
850
+ markerLabel: "Marker label content",
851
+ startLabel: "Label on the left/start of the track",
852
+ endLabel: "Label on the right/end of the track"
853
+ },
854
+ events: {
855
+ onChange: "(value, event)",
856
+ onInput: "(value, event)"
857
+ },
858
+ returns: "HTMLLabelElement | HTMLLIElement (with ._input = HTMLInputElement)",
859
+ description: "Reactive slider with label, icons, custom thumb, markers, and model/QItem support."
860
+ };
861
+ }
862
+ // Esempio: JSswift.ui.Slider({ min: 0, max: 10, model: [get,set] })
863
+
864
+ UI.Rating = (...args) => {
865
+ const { props, children } = JSswift.uiNormalizeArgs(args);
866
+ const slots = props.slots || {};
867
+ const boundValue = props.model || ((uiIsSignal(props.value) || uiIsRod(props.value)) ? props.value : null);
868
+ const model = resolveModel(boundValue, "UI.Rating:model");
869
+ const id = props.id || (`cms-rating-` + Math.random().toString(36).slice(2));
870
+ const inputProps = JSswift.omit(props, [
871
+ "model", "value", "max", "class", "style", "dense", "readonly", "disabled", "clearable",
872
+ "half", "allowHalf", "noDimming", "label", "slots", "onChange", "onInput", "onHover",
873
+ "icon", "checkedIcon", "uncheckedIcon", "halfIcon", "hoveredIcon", "iconSelected", "iconHalf",
874
+ "iconHovered", "iconOn", "iconOff", "iconSize", "color", "colorSelected", "colorHalf",
875
+ "colorHovered", "colorInactive", "activeColor", "halfColor", "hoverColor", "size", "gap",
876
+ "tabindex", "tabIndex", "inputClass"
877
+ ]);
878
+ inputProps.type = "hidden";
879
+ inputProps.id = id;
880
+ if (props.name != null) inputProps.name = props.name;
881
+ inputProps.class = uiClass(["cms-rating-input", "cms-choice-input", props.inputClass]);
882
+ const input = _.input(inputProps);
883
+
884
+ const wrapProps = JSswift.omit(props, [
885
+ "model", "value", "max", "id", "name", "type", "class", "style", "dense", "readonly",
886
+ "disabled", "clearable", "half", "allowHalf", "noDimming", "label", "slots", "onChange",
887
+ "onInput", "onHover", "icon", "checkedIcon", "uncheckedIcon", "halfIcon", "hoveredIcon",
888
+ "iconSelected", "iconHalf", "iconHovered", "iconOn", "iconOff", "iconSize", "color",
889
+ "colorSelected", "colorHalf", "colorHovered", "colorInactive", "activeColor", "halfColor",
890
+ "hoverColor", "size", "gap", "tabindex", "tabIndex", "inputClass"
891
+ ]);
892
+ wrapProps.class = uiClass([
893
+ "cms-clear-set",
894
+ "cms-singularity-check",
895
+ "cms-choice-wrap",
896
+ "cms-rating",
897
+ "cms-rating-wrap",
898
+ uiWhen(props.dense, "dense"),
899
+ props.class
900
+ ]);
901
+ wrapProps.style = { ...(props.style || {}) };
902
+ const sizeValue = uiUnwrap(props.size);
903
+ if (sizeValue != null && !(typeof sizeValue === "string" && JSswift.uiSizes?.includes(sizeValue))) {
904
+ wrapProps.style["--cms-rating-size"] = toCssSize(sizeValue);
905
+ }
906
+ const gapValue = uiUnwrap(props.gap);
907
+ if (gapValue != null) {
908
+ wrapProps.style["--cms-rating-gap"] = toCssSize(gapValue);
909
+ }
910
+
911
+ const control = _.span({
912
+ class: "cms-rating-control",
913
+ onMouseleave: (e) => {
914
+ if (hoverValue == null) return;
915
+ hoverValue = null;
916
+ syncVisualState();
917
+ props.onHover?.(getValue(), e);
918
+ },
919
+ onKeydown: (e) => {
920
+ if (!isInteractive()) return;
921
+ const step = hasHalf() ? 0.5 : 1;
922
+ const current = getValue();
923
+ let next = current;
924
+ switch (e.key) {
925
+ case "ArrowRight":
926
+ case "ArrowUp":
927
+ next = current + step;
928
+ break;
929
+ case "ArrowLeft":
930
+ case "ArrowDown":
931
+ next = current - step;
932
+ break;
933
+ case "Home":
934
+ next = uiUnwrap(props.clearable) ? 0 : step;
935
+ break;
936
+ case "End":
937
+ next = getMax();
938
+ break;
939
+ case "Delete":
940
+ case "Backspace":
941
+ case "0":
942
+ if (!uiUnwrap(props.clearable)) return;
943
+ next = 0;
944
+ break;
945
+ case " ":
946
+ case "Enter":
947
+ next = current || (uiUnwrap(props.clearable) ? step : Math.max(step, 1));
948
+ break;
949
+ default:
950
+ return;
951
+ }
952
+ e.preventDefault();
953
+ setRatingValue(next, e, { emit: true });
954
+ }
955
+ });
956
+ const labelHost = _.span({ class: "cms-choice-label cms-rating-label" });
957
+ const wrap = _.label(wrapProps, input, control, labelHost);
958
+ setPropertyProps(wrap, props);
959
+
960
+ const clearHost = (host) => {
961
+ while (host.firstChild) host.removeChild(host.firstChild);
962
+ };
963
+ const renderInto = (host, nodes, display = "") => {
964
+ clearHost(host);
965
+ (nodes || []).forEach((n) => host.appendChild(n));
966
+ host.style.display = host.childNodes.length ? display : "none";
967
+ };
968
+ const unwrapSlotValue = (value) => (uiIsSignal(value) || uiIsRod(value) ? uiUnwrap(value) : value);
969
+ const asArray = (value, ctx = {}) => slotToArray(unwrapSlotValue(value), ctx);
970
+ const asIconArray = (value, as, ctx = {}) => {
971
+ const resolved = unwrapSlotValue(value);
972
+ if (resolved == null) return [];
973
+ if (typeof resolved === "string") return [UI.Icon({ name: resolved, color: props.color, size: props.iconSize ?? props.size ?? 16 })];
974
+ return asArray(resolved, { ...ctx, as });
975
+ };
976
+
977
+ const getMax = () => {
978
+ const value = Number(uiUnwrap(props.max) ?? 5);
979
+ return Number.isFinite(value) && value > 0 ? Math.max(1, Math.round(value)) : 5;
980
+ };
981
+ const hasHalf = () => !!uiUnwrap(props.half ?? props.allowHalf);
982
+ const normalizeValue = (value, max = getMax()) => {
983
+ let next = Number(uiUnwrap(value));
984
+ if (!Number.isFinite(next)) next = 0;
985
+ next = Math.min(max, Math.max(0, next));
986
+ const step = hasHalf() ? 0.5 : 1;
987
+ return Math.round(next / step) * step;
988
+ };
989
+ const isDisabled = () => !!uiUnwrap(props.disabled);
990
+ const isReadonly = () => !!uiUnwrap(props.readonly);
991
+ const isInteractive = () => !isDisabled() && !isReadonly();
992
+ const getValue = () => normalizeValue(model ? model.get() : localValue);
993
+ const updateInputValue = (value) => {
994
+ const stringValue = value > 0 ? String(value) : "";
995
+ input.value = stringValue;
996
+ input.setAttribute("value", stringValue);
997
+ input.disabled = isDisabled();
998
+ };
999
+
1000
+ let localValue = normalizeValue(model ? model.get() : uiUnwrap(props.value));
1001
+ let hoverValue = null;
1002
+ let renderedMax = 0;
1003
+ let items = [];
1004
+
1005
+ const getPointerValue = (index, event) => {
1006
+ if (!hasHalf()) return normalizeValue(index);
1007
+ const rect = event.currentTarget.getBoundingClientRect();
1008
+ const half = (event.clientX - rect.left) <= (rect.width / 2);
1009
+ return normalizeValue(half ? index - 0.5 : index);
1010
+ };
1011
+ const getItemState = (displayValue, index) => {
1012
+ if (displayValue >= index) return "full";
1013
+ if (hasHalf() && displayValue >= (index - 0.5)) return "half";
1014
+ return "empty";
1015
+ };
1016
+ const getStateColor = (state, hovered) => {
1017
+ const selectedColor = uiUnwrap(props.colorSelected ?? props.activeColor ?? props.color ?? props.textColor) || "var(--cms-warning, #f0b429)";
1018
+ const halfColor = uiUnwrap(props.colorHalf ?? props.halfColor ?? props.colorSelected ?? props.activeColor ?? props.color ?? props.textColor) || selectedColor;
1019
+ const hoveredColor = uiUnwrap(props.colorHovered ?? props.hoverColor ?? props.colorSelected ?? props.activeColor ?? props.color ?? props.textColor) || selectedColor;
1020
+ const inactiveColor = uiUnwrap(props.colorInactive ?? props.colorOff ?? props.color) || "var(--cms-muted)";
1021
+ if (hovered) return hoveredColor;
1022
+ if (state === "full") return selectedColor;
1023
+ if (state === "half") return halfColor;
1024
+ return inactiveColor;
1025
+ };
1026
+ const resolveItemNodes = (ctx) => {
1027
+ const slotCandidates = [];
1028
+ if (ctx.hovered && ctx.state !== "empty") slotCandidates.push("hoveredIcon", "iconHovered");
1029
+ if (ctx.state === "full") slotCandidates.push("checkedIcon", "selectedIcon", "iconSelected", "iconOn");
1030
+ else if (ctx.state === "half") slotCandidates.push("halfIcon", "iconHalf");
1031
+ else slotCandidates.push("uncheckedIcon", "iconOff");
1032
+ slotCandidates.push("icon", "item", "star");
1033
+ for (const name of slotCandidates) {
1034
+ const nodes = renderSlotToArray(slots, name, ctx, null);
1035
+ if (nodes.length) return nodes;
1036
+ }
1037
+
1038
+ let source = null;
1039
+ let as = "icon";
1040
+ if (ctx.hovered && ctx.state !== "empty") {
1041
+ source = props.hoveredIcon ?? props.iconHovered;
1042
+ as = "hoveredIcon";
1043
+ }
1044
+ if (source == null && ctx.state === "full") {
1045
+ source = props.checkedIcon ?? props.iconSelected ?? props.iconOn ?? props.icon ?? "star";
1046
+ as = "checkedIcon";
1047
+ } else if (source == null && ctx.state === "half") {
1048
+ source = props.halfIcon ?? props.iconHalf ?? props.checkedIcon ?? props.iconSelected ?? props.icon ?? "star_half";
1049
+ as = "halfIcon";
1050
+ } else if (source == null) {
1051
+ source = props.uncheckedIcon ?? props.iconOff ?? (uiUnwrap(props.noDimming) ? (props.icon ?? props.checkedIcon ?? props.iconSelected ?? "star") : "star_border");
1052
+ as = "uncheckedIcon";
1053
+ }
1054
+ return asIconArray(source, as, ctx);
1055
+ };
1056
+ const ensureItems = () => {
1057
+ const max = getMax();
1058
+ if (renderedMax === max) return;
1059
+ renderedMax = max;
1060
+ items = [];
1061
+ clearHost(control);
1062
+ for (let index = 1; index <= max; index++) {
1063
+ const iconHost = _.span({ class: "cms-rating-item-icon" });
1064
+ const item = _.span({
1065
+ class: "cms-rating-item",
1066
+ role: "radio",
1067
+ tabIndex: -1,
1068
+ onMousemove: (e) => {
1069
+ if (!isInteractive()) return;
1070
+ const nextHover = getPointerValue(index, e);
1071
+ if (hoverValue === nextHover) return;
1072
+ hoverValue = nextHover;
1073
+ syncVisualState();
1074
+ props.onHover?.(nextHover, e);
1075
+ },
1076
+ onClick: (e) => {
1077
+ if (!isInteractive()) return;
1078
+ let nextValue = getPointerValue(index, e);
1079
+ if (uiUnwrap(props.clearable) && nextValue === getValue()) nextValue = 0;
1080
+ setRatingValue(nextValue, e, { emit: true });
1081
+ }
1082
+ }, iconHost);
1083
+ items.push({ item, iconHost, index });
1084
+ control.appendChild(item);
1085
+ }
1086
+ };
1087
+ const syncVisualState = () => {
1088
+ ensureItems();
1089
+ const max = getMax();
1090
+ const value = getValue();
1091
+ const displayValue = hoverValue == null ? value : normalizeValue(hoverValue, max);
1092
+ const disabled = isDisabled();
1093
+ const readonly = isReadonly();
1094
+ const clearable = !!uiUnwrap(props.clearable);
1095
+ const noDimming = !!uiUnwrap(props.noDimming);
1096
+
1097
+ wrap.classList.toggle("is-disabled", disabled);
1098
+ wrap.classList.toggle("is-readonly", readonly);
1099
+ wrap.classList.toggle("is-hovering", hoverValue != null);
1100
+ wrap.classList.toggle("is-clearable", clearable);
1101
+ wrap.classList.toggle("is-half", hasHalf());
1102
+ wrap.classList.toggle("no-dimming", noDimming);
1103
+
1104
+ const tabindex = Number(uiUnwrap(props.tabindex ?? props.tabIndex) ?? 0);
1105
+ control.tabIndex = disabled ? -1 : tabindex;
1106
+ control.setAttribute("role", "slider");
1107
+ control.setAttribute("aria-valuemin", "0");
1108
+ control.setAttribute("aria-valuemax", String(max));
1109
+ control.setAttribute("aria-valuenow", String(value));
1110
+ control.setAttribute("aria-disabled", disabled ? "true" : "false");
1111
+ control.setAttribute("aria-readonly", readonly ? "true" : "false");
1112
+
1113
+ updateInputValue(value);
1114
+
1115
+ const labelNodes = renderSlotToArray(
1116
+ slots,
1117
+ "label",
1118
+ { value, max, disabled, readonly, clearable },
1119
+ props.label != null ? unwrapSlotValue(props.label) : children
1120
+ );
1121
+ renderInto(labelHost, labelNodes, "inline-flex");
1122
+
1123
+ items.forEach(({ item, iconHost, index }) => {
1124
+ const state = getItemState(displayValue, index);
1125
+ const hovered = hoverValue != null && state !== "empty";
1126
+ const ctx = {
1127
+ index,
1128
+ max,
1129
+ value,
1130
+ displayValue,
1131
+ state,
1132
+ active: state !== "empty",
1133
+ checked: state === "full",
1134
+ half: state === "half",
1135
+ hovered,
1136
+ disabled,
1137
+ readonly,
1138
+ clearable,
1139
+ setValue: (next) => setRatingValue(next, null, { emit: false })
1140
+ };
1141
+ item.className = uiClassStatic([
1142
+ "cms-rating-item",
1143
+ `is-${state}`,
1144
+ uiWhen(hovered, "is-hovered")
1145
+ ]);
1146
+ item.style.color = getStateColor(state, hovered);
1147
+ item.style.opacity = state === "empty" && !noDimming ? "0.48" : "1";
1148
+ item.setAttribute("aria-checked", String(value === index));
1149
+ renderInto(iconHost, resolveItemNodes(ctx), "inline-flex");
1150
+ });
1151
+ };
1152
+ const setRatingValue = (value, event, options = {}) => {
1153
+ const normalized = normalizeValue(value);
1154
+ const current = getValue();
1155
+ localValue = normalized;
1156
+ hoverValue = null;
1157
+ if (model && options.fromModel !== true) model.set(normalized);
1158
+ updateInputValue(normalized);
1159
+ syncVisualState();
1160
+ if (options.emit !== false && normalized !== current) {
1161
+ props.onInput?.(normalized, event);
1162
+ props.onChange?.(normalized, event);
1163
+ }
1164
+ return normalized;
1165
+ };
1166
+
1167
+ wrap._input = input;
1168
+ wrap._rating = control;
1169
+ wrap._getValue = getValue;
1170
+ wrap._setValue = (value) => setRatingValue(value, null, { emit: false });
1171
+
1172
+ if (model) {
1173
+ model.watch((next) => {
1174
+ const normalized = normalizeValue(next);
1175
+ if (serializeValue(normalized) === serializeValue(localValue)) return;
1176
+ localValue = normalized;
1177
+ hoverValue = null;
1178
+ updateInputValue(localValue);
1179
+ syncVisualState();
1180
+ }, "UI.Rating:watch");
1181
+ }
1182
+
1183
+ JSswift.reactive.effect(() => {
1184
+ if (!model && props.value != null) {
1185
+ localValue = normalizeValue(uiUnwrap(props.value));
1186
+ }
1187
+ syncVisualState();
1188
+ }, "UI.Rating:render");
1189
+
1190
+ return wrap;
1191
+ };
1192
+ if (JSswift.isDev?.()) {
1193
+ UI.meta = UI.meta || {};
1194
+ UI.meta.Rating = {
1195
+ signature: "UI.Rating(...children) | UI.Rating(props, ...children)",
1196
+ props: {
1197
+ max: "number",
1198
+ value: "number | rod | [get,set] signal",
1199
+ model: "rod | [get,set] signal",
1200
+ name: "string",
1201
+ label: "String|Node|Function|Array",
1202
+ clearable: "boolean",
1203
+ half: "boolean",
1204
+ allowHalf: "Alias of half",
1205
+ noDimming: "boolean",
1206
+ readonly: "boolean",
1207
+ disabled: "boolean",
1208
+ icon: "String|Node|Function|Array",
1209
+ iconSelected: "Alias of checkedIcon",
1210
+ checkedIcon: "String|Node|Function|Array",
1211
+ uncheckedIcon: "String|Node|Function|Array",
1212
+ iconHalf: "Alias of halfIcon",
1213
+ halfIcon: "String|Node|Function|Array",
1214
+ iconHovered: "Alias of hoveredIcon",
1215
+ hoveredIcon: "String|Node|Function|Array",
1216
+ color: "string",
1217
+ colorSelected: "string",
1218
+ colorHalf: "string",
1219
+ colorHovered: "string",
1220
+ colorInactive: "string",
1221
+ iconSize: "string|number",
1222
+ size: "string|number",
1223
+ gap: "string|number",
1224
+ slots: "{ label?, icon?, checkedIcon?, uncheckedIcon?, halfIcon?, hoveredIcon?, item?, star? }",
1225
+ class: "string",
1226
+ style: "object"
1227
+ },
1228
+ slots: {
1229
+ label: "Label content",
1230
+ icon: "Base icon content per item",
1231
+ checkedIcon: "Icon when item is selected",
1232
+ uncheckedIcon: "Icon when item is empty",
1233
+ halfIcon: "Icon when item is half-selected",
1234
+ hoveredIcon: "Icon while hovering selected items",
1235
+ item: "Custom item renderer",
1236
+ star: "Alias of item/icon"
1237
+ },
1238
+ events: {
1239
+ onChange: "(value, event)",
1240
+ onInput: "(value, event)",
1241
+ onHover: "(value, event)"
1242
+ },
1243
+ keyboard: ["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", "Home", "End", "Enter", "Space", "Delete", "Backspace", "0"],
1244
+ returns: "HTMLLabelElement (with ._input, ._rating, ._getValue(), ._setValue(value))",
1245
+ description: "Reactive rating with label, custom icons, half rating, clearable behavior, and model support."
1246
+ };
1247
+ }
1248
+ // Esempio: JSswift.ui.Rating({ max: 5, model: [get,set] })
1249
+
1250
+ const uiCloneTimeParts = (value) => {
1251
+ if (!value) return null;
1252
+ return {
1253
+ hour: Number(value.hour) || 0,
1254
+ minute: Number(value.minute) || 0,
1255
+ second: Number(value.second) || 0
1256
+ };
1257
+ };
1258
+ const uiTimePad = (value) => String(Math.max(0, Number(value) || 0)).padStart(2, "0");
1259
+ const uiNormalizeTimeParts = (raw, options = {}) => {
1260
+ if (raw == null || raw === "") return null;
1261
+ let hour = null;
1262
+ let minute = null;
1263
+ let second = 0;
1264
+ let meridiem = null;
1265
+
1266
+ if (raw instanceof Date && !Number.isNaN(raw.getTime())) {
1267
+ hour = raw.getHours();
1268
+ minute = raw.getMinutes();
1269
+ second = raw.getSeconds();
1270
+ } else if (typeof raw === "number" && Number.isFinite(raw)) {
1271
+ const total = Math.max(0, Math.floor(raw));
1272
+ hour = Math.floor(total / 3600) % 24;
1273
+ minute = Math.floor(total / 60) % 60;
1274
+ second = total % 60;
1275
+ } else if (typeof raw === "object") {
1276
+ if (raw.time != null || raw.value != null) return uiNormalizeTimeParts(raw.time ?? raw.value, options);
1277
+ hour = Number(raw.hour ?? raw.hours ?? raw.h ?? raw.hh);
1278
+ minute = Number(raw.minute ?? raw.minutes ?? raw.min ?? raw.mm ?? 0);
1279
+ second = Number(raw.second ?? raw.seconds ?? raw.sec ?? raw.ss ?? 0);
1280
+ meridiem = raw.meridiem ?? raw.ampm ?? raw.period ?? null;
1281
+ } else if (typeof raw === "string") {
1282
+ const value = raw.trim();
1283
+ if (!value) return null;
1284
+ const compact = value.match(/^(\d{1,2})(\d{2})(\d{2})?$/);
1285
+ const match = value.match(/(\d{1,2}):(\d{1,2})(?::(\d{1,2}))?\s*([ap]m)?/i);
1286
+ if (match) {
1287
+ hour = Number(match[1]);
1288
+ minute = Number(match[2]);
1289
+ second = match[3] != null ? Number(match[3]) : 0;
1290
+ meridiem = match[4] || null;
1291
+ } else if (compact) {
1292
+ hour = Number(compact[1]);
1293
+ minute = Number(compact[2]);
1294
+ second = compact[3] != null ? Number(compact[3]) : 0;
1295
+ } else {
1296
+ return null;
1297
+ }
1298
+ } else {
1299
+ return null;
1300
+ }
1301
+
1302
+ if (!Number.isFinite(hour) || !Number.isFinite(minute) || !Number.isFinite(second)) return null;
1303
+ if (meridiem) {
1304
+ const lower = String(meridiem).toLowerCase();
1305
+ if (lower === "pm" && hour < 12) hour += 12;
1306
+ if (lower === "am" && hour === 12) hour = 0;
1307
+ }
1308
+ if (hour < 0 || hour > 23 || minute < 0 || minute > 59 || second < 0 || second > 59) return null;
1309
+
1310
+ const normalized = {
1311
+ hour: Math.floor(hour),
1312
+ minute: Math.floor(minute),
1313
+ second: Math.floor(second)
1314
+ };
1315
+ return uiConstrainTimeParts(normalized, options);
1316
+ };
1317
+ const uiExportTimeValue = (value, options = {}) => {
1318
+ const parts = value && typeof value === "object" && value.hour != null
1319
+ ? uiCloneTimeParts(value)
1320
+ : uiNormalizeTimeParts(value, options);
1321
+ if (!parts) return "";
1322
+ const withSeconds = options.withSeconds === true || Number(parts.second || 0) > 0;
1323
+ return `${uiTimePad(parts.hour)}:${uiTimePad(parts.minute)}${withSeconds ? `:${uiTimePad(parts.second)}` : ""}`;
1324
+ };
1325
+ const uiCompareTimeValue = (a, b) => {
1326
+ const av = uiExportTimeValue(a, { withSeconds: true });
1327
+ const bv = uiExportTimeValue(b, { withSeconds: true });
1328
+ if (!av && !bv) return 0;
1329
+ if (!av) return -1;
1330
+ if (!bv) return 1;
1331
+ return av < bv ? -1 : (av > bv ? 1 : 0);
1332
+ };
1333
+ function uiConstrainTimeParts(value, options = {}) {
1334
+ if (!value) return null;
1335
+ const out = uiCloneTimeParts(value);
1336
+ const min = options.min != null ? uiNormalizeTimeParts(options.min, { withSeconds: true }) : null;
1337
+ const max = options.max != null ? uiNormalizeTimeParts(options.max, { withSeconds: true }) : null;
1338
+ if (min && uiCompareTimeValue(out, min) < 0) return uiCloneTimeParts(min);
1339
+ if (max && uiCompareTimeValue(out, max) > 0) return uiCloneTimeParts(max);
1340
+ return out;
1341
+ }
1342
+ const uiExtractTimeFromValue = (raw, options = {}) => {
1343
+ if (raw == null || raw === "") return null;
1344
+ if (raw instanceof Date) return uiNormalizeTimeParts(raw, options);
1345
+ if (typeof raw === "object") return uiNormalizeTimeParts(raw.time != null ? raw.time : raw, options);
1346
+ if (typeof raw === "string" || typeof raw === "number") return uiNormalizeTimeParts(raw, options);
1347
+ return null;
1348
+ };
1349
+ const uiFormatTimeDisplay = (value, options = {}) => {
1350
+ const parts = uiNormalizeTimeParts(value, options);
1351
+ if (!parts) return "";
1352
+ const withSeconds = options.withSeconds === true || Number(parts.second || 0) > 0;
1353
+ const use12h = !!uiUnwrap(options.use12h ?? options.ampm);
1354
+ if (!use12h) {
1355
+ return `${uiTimePad(parts.hour)}:${uiTimePad(parts.minute)}${withSeconds ? `:${uiTimePad(parts.second)}` : ""}`;
1356
+ }
1357
+ const meridiem = parts.hour >= 12 ? "PM" : "AM";
1358
+ const hour = parts.hour % 12 || 12;
1359
+ return `${uiTimePad(hour)}:${uiTimePad(parts.minute)}${withSeconds ? `:${uiTimePad(parts.second)}` : ""} ${meridiem}`;
1360
+ };
1361
+ const uiMergeDateAndTime = (dateValue, timeValue, options = {}) => {
1362
+ if (!dateValue) return "";
1363
+ const time = uiExportTimeValue(timeValue, options);
1364
+ return time ? `${dateValue}T${time}` : dateValue;
1365
+ };
1366
+ const uiBuildTimeSteps = (step, max) => {
1367
+ const safeStep = Math.max(1, Math.min(max + 1, Math.round(Number(step) || 1)));
1368
+ const out = [];
1369
+ for (let value = 0; value <= max; value += safeStep) out.push(value);
1370
+ return out;
1371
+ };
1372
+ const uiCreateTimePickerSection = (config = {}) => {
1373
+ const {
1374
+ props = {},
1375
+ slots = {},
1376
+ value = null,
1377
+ withSeconds = false,
1378
+ minuteStep = 5,
1379
+ secondStep = 5,
1380
+ disabled = false,
1381
+ readonly = false,
1382
+ embedded = false,
1383
+ className = "",
1384
+ slotPrefix = "",
1385
+ shortcuts = null,
1386
+ onSelect = null
1387
+ } = config;
1388
+ const selectedValue = uiNormalizeTimeParts(value, { withSeconds, min: props.min, max: props.max });
1389
+ const baseParts = selectedValue
1390
+ || uiNormalizeTimeParts(new Date(), { withSeconds, min: props.min, max: props.max })
1391
+ || { hour: 0, minute: 0, second: 0 };
1392
+ const selectedParts = selectedValue ? uiCloneTimeParts(selectedValue) : null;
1393
+ const prefixed = (name) => slotPrefix ? `${slotPrefix}${name[0].toUpperCase()}${name.slice(1)}` : null;
1394
+ const renderNamedSlot = (name, ctx, fallback) => {
1395
+ const candidates = [prefixed(name), name].filter(Boolean);
1396
+ for (const candidate of candidates) {
1397
+ const slotValue = JSswift.ui.getSlot(slots, candidate);
1398
+ if (slotValue == null) continue;
1399
+ const rendered = JSswift.ui.renderSlot(slots, candidate, ctx, fallback);
1400
+ if (rendered != null) return rendered;
1401
+ }
1402
+ return JSswift.ui.slot(fallback, ctx);
1403
+ };
1404
+ const renderPointNodes = (ctx) => {
1405
+ let pointNode = renderNamedSlot("point", ctx, null);
1406
+ if (pointNode == null && config.pointIcon != null && ctx.selected) {
1407
+ pointNode = typeof config.pointIcon === "string"
1408
+ ? UI.Icon({ name: config.pointIcon, size: embedded ? 10 : 12 })
1409
+ : JSswift.ui.slot(config.pointIcon, ctx);
1410
+ }
1411
+ return renderSlotToArray(null, "default", ctx, pointNode);
1412
+ };
1413
+ const makeRequestedValue = (part, optionValue) => {
1414
+ const next = {
1415
+ hour: baseParts.hour,
1416
+ minute: baseParts.minute,
1417
+ second: withSeconds ? baseParts.second : 0
1418
+ };
1419
+ next[part] = optionValue;
1420
+ return next;
1421
+ };
1422
+ const section = _.div({
1423
+ class: uiClass([
1424
+ "cms-time-section",
1425
+ uiWhen(embedded, "is-embedded"),
1426
+ uiWhen(withSeconds, "has-seconds"),
1427
+ className
1428
+ ])
1429
+ });
1430
+ const shortcutList = uiUnwrap(shortcuts) || [];
1431
+ if (Array.isArray(shortcutList) && shortcutList.length) {
1432
+ const shortcutWrap = _.div({ class: "cms-time-shortcuts" });
1433
+ shortcutList.forEach((item, index) => {
1434
+ if (!item) return;
1435
+ const rawValue = typeof item.value === "function"
1436
+ ? item.value({ now: uiExportTimeValue(new Date(), { withSeconds }) })
1437
+ : item.value;
1438
+ const normalizedValue = uiNormalizeTimeParts(rawValue, { withSeconds, min: props.min, max: props.max });
1439
+ if (!normalizedValue) return;
1440
+ const ctx = {
1441
+ item,
1442
+ index,
1443
+ value: uiExportTimeValue(normalizedValue, { withSeconds }),
1444
+ displayValue: uiFormatTimeDisplay(normalizedValue, { withSeconds, use12h: props.use12h ?? props.ampm })
1445
+ };
1446
+ const labelNode = renderNamedSlot("shortcut", ctx, item.label ?? item.text ?? ctx.displayValue);
1447
+ shortcutWrap.appendChild(_.button({
1448
+ type: "button",
1449
+ class: uiClass(["cms-time-shortcut", uiWhen(!!selectedParts && uiCompareTimeValue(normalizedValue, selectedParts) === 0, "is-selected")]),
1450
+ disabled: disabled || readonly,
1451
+ onClick: () => onSelect?.(normalizedValue, { shortcut: item, index })
1452
+ }, ...renderSlotToArray(null, "default", ctx, labelNode)));
1453
+ });
1454
+ if (shortcutWrap.childNodes.length) section.appendChild(shortcutWrap);
1455
+ }
1456
+ const columns = _.div({ class: "cms-time-columns" });
1457
+ const createColumn = (title, part, values) => {
1458
+ const column = _.div({ class: "cms-time-column", "data-time-part": part });
1459
+ const optionsWrap = _.div({ class: "cms-time-options" });
1460
+ values.forEach((optionValue) => {
1461
+ const requested = makeRequestedValue(part, optionValue);
1462
+ const normalized = uiConstrainTimeParts(requested, { withSeconds, min: props.min, max: props.max });
1463
+ const optionDisabled = disabled || readonly || uiCompareTimeValue(normalized, requested) !== 0;
1464
+ const selected = !!selectedParts && selectedParts[part] === optionValue;
1465
+ const ctx = {
1466
+ part,
1467
+ selected,
1468
+ disabled: optionDisabled,
1469
+ value: optionValue,
1470
+ label: uiTimePad(optionValue),
1471
+ timeValue: uiExportTimeValue(normalized, { withSeconds }),
1472
+ displayValue: uiFormatTimeDisplay(normalized, { withSeconds, use12h: props.use12h ?? props.ampm })
1473
+ };
1474
+ const optionNode = renderNamedSlot("option", ctx, ctx.label);
1475
+ const pointNodes = selected ? renderPointNodes(ctx) : [];
1476
+ optionsWrap.appendChild(_.button({
1477
+ type: "button",
1478
+ class: uiClass(["cms-time-option", uiWhen(selected, "is-selected"), uiWhen(optionDisabled, "is-disabled")]),
1479
+ disabled: optionDisabled,
1480
+ onClick: () => onSelect?.(normalized, { part, optionValue })
1481
+ },
1482
+ _.span({ class: "cms-time-option-label" }, ...renderSlotToArray(null, "default", ctx, optionNode)),
1483
+ pointNodes.length ? _.span({ class: "cms-time-option-point" }, ...pointNodes) : null
1484
+ ));
1485
+ });
1486
+ column.appendChild(optionsWrap);
1487
+ return column;
1488
+ };
1489
+ columns.appendChild(createColumn(uiUnwrap(props.hoursLabel) || "Ore", "hour", uiBuildTimeSteps(1, 23)));
1490
+ columns.appendChild(createColumn(uiUnwrap(props.minutesLabel) || "Min", "minute", uiBuildTimeSteps(minuteStep, 59)));
1491
+ if (withSeconds) {
1492
+ columns.appendChild(createColumn(uiUnwrap(props.secondsLabel) || "Sec", "second", uiBuildTimeSteps(secondStep, 59)));
1493
+ }
1494
+ section.appendChild(columns);
1495
+ return section;
1496
+ };
1497
+
1498
+ const uiCaptureTimeColumnScrollState = (root) => {
1499
+ if (!root) return null;
1500
+ const state = {};
1501
+ root.querySelectorAll(".cms-time-column").forEach((column, index) => {
1502
+ const key = column.getAttribute("data-time-part") || String(index);
1503
+ state[key] = column.scrollTop;
1504
+ });
1505
+ return state;
1506
+ };
1507
+
1508
+ const uiCenterTimeColumnsToSelection = (root, options = {}) => {
1509
+ if (!root) return;
1510
+ const scrollState = options.scrollState || null;
1511
+ const behavior = options.behavior ?? "smooth";
1512
+ root.querySelectorAll(".cms-time-column").forEach((column, index) => {
1513
+ const key = column.getAttribute("data-time-part") || String(index);
1514
+ const previousTop = scrollState && Number.isFinite(scrollState[key]) ? scrollState[key] : null;
1515
+ if (previousTop != null) column.scrollTop = previousTop;
1516
+ const selectedOption = column.querySelector(".cms-time-option.is-selected");
1517
+ if (!selectedOption) return;
1518
+ const maxTop = Math.max(0, column.scrollHeight - column.clientHeight);
1519
+ const targetTop = selectedOption.offsetTop - ((column.clientHeight - selectedOption.offsetHeight) / 2);
1520
+ const nextTop = Math.max(0, Math.min(maxTop, targetTop));
1521
+ if (Math.abs(column.scrollTop - nextTop) < 1) {
1522
+ column.scrollTop = nextTop;
1523
+ return;
1524
+ }
1525
+ if (behavior && typeof column.scrollTo === "function") {
1526
+ column.scrollTo({ top: nextTop, behavior });
1527
+ return;
1528
+ }
1529
+ column.scrollTop = nextTop;
1530
+ });
1531
+ };
1532
+
1533
+ const buildDateControl = (args, options = {}) => {
1534
+ const { props, children } = JSswift.uiNormalizeArgs(args);
1535
+ const isCalendar = options.calendar === true;
1536
+ const slots = props.slots || {};
1537
+ const sizeValue = uiComputed(props.size, () => {
1538
+ const value = String(uiUnwrap(props.size) || "").toLowerCase();
1539
+ return ["xs", "sm", "md", "lg", "xl"].includes(value) ? `cms-size-${value}` : "";
1540
+ });
1541
+ const requestedMode = String(
1542
+ uiUnwrap(
1543
+ props.mode
1544
+ ?? (uiUnwrap(props.rangeMultiple ?? props.multipleRange ?? props.multiRange) ? "range-multiple" : null)
1545
+ ?? ((props.range && props.multiple) ? "range-multiple" : (props.range ? "range" : (props.multiple ? "multiple" : "single")))
1546
+ )
1547
+ ).toLowerCase();
1548
+ const mode = ["range-multiple", "multiple-range", "rangemultiple", "multiplerange", "range_multiple", "multiple_range", "multi-range", "multi_range", "ranges"]
1549
+ .includes(requestedMode)
1550
+ ? "range-multiple"
1551
+ : (requestedMode === "range"
1552
+ ? "range"
1553
+ : (requestedMode === "multiple" ? "multiple" : "single"));
1554
+ const valueBinding = props.model || ((uiIsSignal(props.value) || uiIsRod(props.value)) ? props.value : null);
1555
+ const model = resolveModel(valueBinding, isCalendar ? "UI.Calendar:model" : "UI.Datepicker:model");
1556
+ const initialRawValue = model ? model.get() : uiUnwrap(props.value);
1557
+ const rangeAsArray = uiUnwrap(props.rangeAs ?? props.rangeModel) === "array" || Array.isArray(initialRawValue);
1558
+ const rangeMultipleAsArray = uiUnwrap(props.rangeMultipleAs ?? props.multipleRangeAs) === "array"
1559
+ || (Array.isArray(initialRawValue) && Array.isArray(initialRawValue[0]));
1560
+ const isTimeEnabled = () => mode === "single" && !!uiUnwrap(props.withTime ?? props.time ?? props.timePicker ?? props.enableTime);
1561
+ const getTimeOptions = () => {
1562
+ const detectedTime = uiExportTimeValue(
1563
+ uiExtractTimeFromValue(uiUnwrap(props.timeValue) ?? (model ? model.get() : uiUnwrap(props.value)) ?? initialRawValue, { withSeconds: true }),
1564
+ { withSeconds: true }
1565
+ );
1566
+ return {
1567
+ withSeconds: !!uiUnwrap(props.timeWithSeconds ?? props.withSeconds ?? props.showSeconds)
1568
+ || /:\d{2}:\d{2}$/.test(detectedTime)
1569
+ || /:\d{2}:\d{2}$/.test(uiUnwrap(props.timeFormat) || ""),
1570
+ min: uiUnwrap(props.timeMin ?? props.minTime),
1571
+ max: uiUnwrap(props.timeMax ?? props.maxTime),
1572
+ use12h: uiUnwrap(props.time12h ?? props.use12h ?? props.ampm)
1573
+ };
1574
+ };
1575
+ const getTimeMinuteStep = () => {
1576
+ const raw = Number(uiUnwrap(props.timeMinuteStep ?? props.minuteStep ?? 5));
1577
+ return Math.max(1, Math.min(30, Number.isFinite(raw) ? raw : 5));
1578
+ };
1579
+ const getTimeSecondStep = () => {
1580
+ const raw = Number(uiUnwrap(props.timeSecondStep ?? props.secondStep ?? 5));
1581
+ return Math.max(1, Math.min(30, Number.isFinite(raw) ? raw : 5));
1582
+ };
1583
+
1584
+ const pad = (n) => String(n).padStart(2, "0");
1585
+ const createDate = (year, month, day) => {
1586
+ const date = new Date(year, month, day);
1587
+ if (date.getFullYear() !== year || date.getMonth() !== month || date.getDate() !== day) return null;
1588
+ return date;
1589
+ };
1590
+ const toIsoDate = (date) => {
1591
+ if (!(date instanceof Date) || Number.isNaN(date.getTime())) return null;
1592
+ return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
1593
+ };
1594
+ const fromIsoDate = (iso) => {
1595
+ if (typeof iso !== "string") return null;
1596
+ const match = iso.match(/^(\d{4})-(\d{2})-(\d{2})$/);
1597
+ if (!match) return null;
1598
+ const year = Number(match[1]);
1599
+ const month = Number(match[2]) - 1;
1600
+ const day = Number(match[3]);
1601
+ const date = new Date(year, month, day);
1602
+ if (date.getFullYear() !== year || date.getMonth() !== month || date.getDate() !== day) return null;
1603
+ return date;
1604
+ };
1605
+ const shiftDays = (iso, amount) => {
1606
+ const date = fromIsoDate(iso);
1607
+ if (!date) return null;
1608
+ date.setDate(date.getDate() + amount);
1609
+ return toIsoDate(date);
1610
+ };
1611
+ const shiftMonths = (date, amount) => new Date(date.getFullYear(), date.getMonth() + amount, 1);
1612
+ const monthStart = (value) => {
1613
+ if (value instanceof Date && !Number.isNaN(value.getTime())) return new Date(value.getFullYear(), value.getMonth(), 1);
1614
+ const date = normalizeDateOnly(value);
1615
+ const parsed = fromIsoDate(date);
1616
+ return parsed ? new Date(parsed.getFullYear(), parsed.getMonth(), 1) : new Date();
1617
+ };
1618
+ const todayIso = () => toIsoDate(new Date());
1619
+ const isSameIso = (a, b) => !!a && !!b && a === b;
1620
+ const compareIso = (a, b) => {
1621
+ if (a == null && b == null) return 0;
1622
+ if (a == null) return -1;
1623
+ if (b == null) return 1;
1624
+ return a < b ? -1 : (a > b ? 1 : 0);
1625
+ };
1626
+ const diffDays = (a, b) => {
1627
+ const da = fromIsoDate(a);
1628
+ const db = fromIsoDate(b);
1629
+ if (!da || !db) return 0;
1630
+ return Math.round((db.getTime() - da.getTime()) / 86400000);
1631
+ };
1632
+ const ensureArray = (value) => Array.isArray(value) ? value.slice() : (value == null || value === "" ? [] : [value]);
1633
+ const cloneRangeValue = (value) => ({ from: value?.from || null, to: value?.to || null });
1634
+ const cloneRangeList = (value) => Array.isArray(value) ? value.map(cloneRangeValue) : [];
1635
+ const exportRangeValue = (value) => {
1636
+ const out = cloneRangeValue(value);
1637
+ return rangeAsArray ? [out.from, out.to] : out;
1638
+ };
1639
+ const exportRangeMultipleValue = (value) => cloneRangeList(value).map((item) => (
1640
+ rangeMultipleAsArray ? [item.from, item.to] : item
1641
+ ));
1642
+ const cloneValue = (value) => {
1643
+ if (mode === "multiple") return Array.isArray(value) ? value.slice() : [];
1644
+ if (mode === "range") return cloneRangeValue(value);
1645
+ if (mode === "range-multiple") return cloneRangeList(value);
1646
+ return value || null;
1647
+ };
1648
+ const exportValue = (value) => {
1649
+ if (mode === "multiple") return Array.isArray(value) ? value.slice() : [];
1650
+ if (mode === "range") return exportRangeValue(value);
1651
+ if (mode === "range-multiple") return exportRangeMultipleValue(value);
1652
+ return value || "";
1653
+ };
1654
+ const serializeValue = (value) => JSON.stringify(exportValue(value));
1655
+ const normalizeDateOnly = (raw) => {
1656
+ if (raw == null || raw === "") return null;
1657
+ if (raw instanceof Date) return toIsoDate(raw);
1658
+ if (typeof raw === "number") return toIsoDate(new Date(raw));
1659
+ if (typeof raw !== "string") {
1660
+ if (typeof raw === "object" && raw.date != null) return normalizeDateOnly(raw.date);
1661
+ return null;
1662
+ }
1663
+ const value = raw.trim();
1664
+ if (!value) return null;
1665
+ const isoLike = value.match(/^(\d{4})-(\d{1,2})-(\d{1,2})(?:$|[T\s])/);
1666
+ if (isoLike) {
1667
+ const date = createDate(Number(isoLike[1]), Number(isoLike[2]) - 1, Number(isoLike[3]));
1668
+ return toIsoDate(date);
1669
+ }
1670
+ const euroLike = value.match(/^(\d{1,2})[\/.\-](\d{1,2})[\/.\-](\d{2,4})$/);
1671
+ if (euroLike) {
1672
+ const year = euroLike[3].length === 2 ? (2000 + Number(euroLike[3])) : Number(euroLike[3]);
1673
+ const date = createDate(year, Number(euroLike[2]) - 1, Number(euroLike[1]));
1674
+ return toIsoDate(date);
1675
+ }
1676
+ if (/^\d{1,4}$/.test(value)) return null;
1677
+ const parsed = new Date(value);
1678
+ if (Number.isNaN(parsed.getTime())) return null;
1679
+ const explicitYear = value.match(/(?:^|[^\d])(\d{4})(?!\d)/);
1680
+ if (explicitYear && parsed.getFullYear() !== Number(explicitYear[1])) return null;
1681
+ return toIsoDate(parsed);
1682
+ };
1683
+ const normalizeRangeValue = (raw) => {
1684
+ if (raw == null || raw === "") return { from: null, to: null };
1685
+ let from = null;
1686
+ let to = null;
1687
+ if (Array.isArray(raw)) {
1688
+ from = normalizeDateOnly(raw[0]);
1689
+ to = normalizeDateOnly(raw[1]);
1690
+ } else if (typeof raw === "object") {
1691
+ from = normalizeDateOnly(raw.from ?? raw.start ?? raw.dateFrom ?? raw.departure);
1692
+ to = normalizeDateOnly(raw.to ?? raw.end ?? raw.dateTo ?? raw.return);
1693
+ } else {
1694
+ const found = String(raw).match(/\d{4}-\d{1,2}-\d{1,2}|\d{1,2}[\/.\-]\d{1,2}[\/.\-]\d{2,4}/g) || [];
1695
+ from = normalizeDateOnly(found[0]);
1696
+ to = normalizeDateOnly(found[1]);
1697
+ }
1698
+ if (from && to && from > to) [from, to] = [to, from];
1699
+ return { from, to };
1700
+ };
1701
+ const normalizeRangeMultipleValue = (raw) => {
1702
+ const out = [];
1703
+ const seen = new Set();
1704
+ const pushRange = (value) => {
1705
+ const normalized = normalizeRangeValue(value);
1706
+ if (!normalized.from) return;
1707
+ const key = `${normalized.from || ""}|${normalized.to || ""}`;
1708
+ if (seen.has(key)) return;
1709
+ seen.add(key);
1710
+ out.push(normalized);
1711
+ };
1712
+ if (raw == null || raw === "") return out;
1713
+ if (Array.isArray(raw)) {
1714
+ const isFlatDateList = raw.every((item) => (
1715
+ item == null
1716
+ || typeof item === "string"
1717
+ || typeof item === "number"
1718
+ || item instanceof Date
1719
+ ));
1720
+ if (isFlatDateList) {
1721
+ const dates = raw.map(normalizeDateOnly).filter(Boolean);
1722
+ for (let index = 0; index < dates.length; index += 2) {
1723
+ pushRange({ from: dates[index], to: dates[index + 1] || null });
1724
+ }
1725
+ } else {
1726
+ raw.forEach((item) => pushRange(item));
1727
+ }
1728
+ } else if (typeof raw === "object" && Array.isArray(raw.ranges)) {
1729
+ raw.ranges.forEach((item) => pushRange(item));
1730
+ } else if (typeof raw === "object") {
1731
+ pushRange(raw);
1732
+ } else {
1733
+ const found = String(raw).match(/\d{4}-\d{1,2}-\d{1,2}|\d{1,2}[\/.\-]\d{1,2}[\/.\-]\d{2,4}/g) || [];
1734
+ const dates = found.map(normalizeDateOnly).filter(Boolean);
1735
+ for (let index = 0; index < dates.length; index += 2) {
1736
+ pushRange({ from: dates[index], to: dates[index + 1] || null });
1737
+ }
1738
+ }
1739
+ return out.sort((a, b) => compareIso(a.from, b.from) || compareIso(a.to, b.to));
1740
+ };
1741
+ const normalizeMultipleValue = (raw) => {
1742
+ const out = [];
1743
+ ensureArray(raw).forEach((item) => {
1744
+ const normalized = normalizeDateOnly(item);
1745
+ if (normalized && !out.includes(normalized)) out.push(normalized);
1746
+ });
1747
+ return out.sort();
1748
+ };
1749
+ const normalizeValue = (raw) => {
1750
+ if (mode === "range") return normalizeRangeValue(raw);
1751
+ if (mode === "range-multiple") return normalizeRangeMultipleValue(raw);
1752
+ if (mode === "multiple") return normalizeMultipleValue(raw);
1753
+ return normalizeDateOnly(raw);
1754
+ };
1755
+ const getMin = () => normalizeDateOnly(uiUnwrap(props.min ?? props.minDate));
1756
+ const getMax = () => normalizeDateOnly(uiUnwrap(props.max ?? props.maxDate));
1757
+ const getLocale = () => uiUnwrap(props.locale) || undefined;
1758
+ const getFirstDayOfWeek = () => {
1759
+ const raw = Number(uiUnwrap(props.firstDayOfWeek ?? props.weekStart ?? 1));
1760
+ return Number.isFinite(raw) ? ((raw % 7) + 7) % 7 : 1;
1761
+ };
1762
+ const getMonthsToShow = () => {
1763
+ const fallback = (mode === "range" || mode === "range-multiple") ? 2 : 1;
1764
+ const raw = Number(uiUnwrap(props.monthsToShow) ?? fallback);
1765
+ return Math.max(1, Math.min(4, Number.isFinite(raw) ? raw : fallback));
1766
+ };
1767
+ const getDefaultViewValue = () => {
1768
+ const min = getMin();
1769
+ const max = getMax();
1770
+ let fallback = normalizeDateOnly(uiUnwrap(props.defaultMonth)) || todayIso();
1771
+ if (min && fallback < min) fallback = min;
1772
+ if (max && fallback > max) fallback = max;
1773
+ return fallback || min || max || todayIso();
1774
+ };
1775
+ const getCurrentValue = () => uiUnwrap(props.confirm) ? workingValue : localValue;
1776
+ const getDateContext = (iso) => {
1777
+ const date = fromIsoDate(iso);
1778
+ if (!date) return { date: iso, value: iso };
1779
+ return {
1780
+ date: iso,
1781
+ value: iso,
1782
+ year: date.getFullYear(),
1783
+ month: date.getMonth() + 1,
1784
+ day: date.getDate(),
1785
+ weekday: date.getDay(),
1786
+ today: isSameIso(iso, todayIso())
1787
+ };
1788
+ };
1789
+ const matchesDateSpec = (spec, iso) => {
1790
+ if (spec == null) return false;
1791
+ if (Array.isArray(spec)) return spec.some((item) => normalizeDateOnly(item) === iso);
1792
+ if (spec instanceof Set) return spec.has(iso);
1793
+ if (typeof spec === "function") return !!spec(iso, getDateContext(iso));
1794
+ if (typeof spec === "string" || spec instanceof Date || typeof spec === "number") {
1795
+ return normalizeDateOnly(spec) === iso;
1796
+ }
1797
+ return false;
1798
+ };
1799
+ const isDateAllowed = (iso) => {
1800
+ if (!iso) return false;
1801
+ const min = getMin();
1802
+ const max = getMax();
1803
+ if (min && iso < min) return false;
1804
+ if (max && iso > max) return false;
1805
+ const allowSpec = uiUnwrap(props.options ?? props.enableDates ?? props.allowedDates);
1806
+ if (allowSpec != null && !matchesDateSpec(allowSpec, iso)) return false;
1807
+ const disableSpec = uiUnwrap(props.disableDates ?? props.disabledDates ?? props.notAllowedDates);
1808
+ if (disableSpec != null && matchesDateSpec(disableSpec, iso)) return false;
1809
+ const weekday = fromIsoDate(iso)?.getDay();
1810
+ if (Array.isArray(uiUnwrap(props.allowedWeekdays ?? props.weekdays)) && weekday != null) {
1811
+ const allowedWeekdays = uiUnwrap(props.allowedWeekdays ?? props.weekdays).map((x) => Number(x));
1812
+ if (!allowedWeekdays.includes(weekday)) return false;
1813
+ }
1814
+ if (uiUnwrap(props.weekdaysOnly) && (weekday === 0 || weekday === 6)) return false;
1815
+ if (uiUnwrap(props.weekendsOnly) && weekday != null && weekday !== 0 && weekday !== 6) return false;
1816
+ const current = getCurrentValue();
1817
+ const pendingRange = mode === "range"
1818
+ ? (current?.from && !current?.to ? current : null)
1819
+ : (mode === "range-multiple"
1820
+ ? ((Array.isArray(current) && current.length && current[current.length - 1]?.from && !current[current.length - 1]?.to)
1821
+ ? current[current.length - 1]
1822
+ : null)
1823
+ : null);
1824
+ if (pendingRange?.from) {
1825
+ const nights = Math.abs(diffDays(pendingRange.from, iso));
1826
+ const minRange = Number(uiUnwrap(props.minRange ?? props.minDays ?? props.minNights));
1827
+ const maxRange = Number(uiUnwrap(props.maxRange ?? props.maxDays ?? props.maxNights));
1828
+ if (Number.isFinite(minRange) && nights < minRange) return false;
1829
+ if (Number.isFinite(maxRange) && nights > maxRange) return false;
1830
+ }
1831
+ return true;
1832
+ };
1833
+ const formatSingleDisplay = (iso) => {
1834
+ if (!iso) return "";
1835
+ const date = fromIsoDate(iso);
1836
+ if (!date) return iso;
1837
+ const displayMask = String(uiUnwrap(props.displayMask ?? props.mask ?? "") || "").toLowerCase();
1838
+ if (displayMask === "iso" || uiUnwrap(props.isoDisplay) === true) return iso;
1839
+ return new Intl.DateTimeFormat(getLocale(), {
1840
+ year: "numeric",
1841
+ month: "short",
1842
+ day: "2-digit"
1843
+ }).format(date);
1844
+ };
1845
+ const formatDisplayValue = (value, timeValue = localTimeValue) => {
1846
+ if (mode === "range-multiple") {
1847
+ if (!Array.isArray(value) || !value.length) return "";
1848
+ if (value.length > 2 && uiUnwrap(props.compactMultiple) !== false) {
1849
+ return `${value.length} ${uiUnwrap(props.multipleRangeLabel) || "intervalli selezionati"}`;
1850
+ }
1851
+ return value.map((range) => {
1852
+ const from = range?.from ? formatSingleDisplay(range.from) : "";
1853
+ const to = range?.to ? formatSingleDisplay(range.to) : "";
1854
+ return from && to ? `${from} -> ${to}` : (from || to || "");
1855
+ }).filter(Boolean).join("; ");
1856
+ }
1857
+ if (mode === "multiple") {
1858
+ if (!Array.isArray(value) || !value.length) return "";
1859
+ if (value.length > 3 && uiUnwrap(props.compactMultiple) !== false) {
1860
+ return `${value.length} ${uiUnwrap(props.multipleLabel) || "date selezionate"}`;
1861
+ }
1862
+ return value.map(formatSingleDisplay).join(", ");
1863
+ }
1864
+ if (mode === "range") {
1865
+ const from = value?.from ? formatSingleDisplay(value.from) : "";
1866
+ const to = value?.to ? formatSingleDisplay(value.to) : "";
1867
+ if (from && to) return `${from} -> ${to}`;
1868
+ return from || to || "";
1869
+ }
1870
+ const dateLabel = formatSingleDisplay(value);
1871
+ if (!isTimeEnabled()) return dateLabel;
1872
+ const timeLabel = formatTimeDisplayValue(timeValue);
1873
+ return [dateLabel, timeLabel].filter(Boolean).join(" • ");
1874
+ };
1875
+ const extractTypedDates = (raw) => {
1876
+ const found = String(raw || "").match(/\d{4}-\d{1,2}-\d{1,2}|\d{1,2}[\/.\-]\d{1,2}[\/.\-]\d{2,4}/g) || [];
1877
+ return found.map(normalizeDateOnly).filter(Boolean);
1878
+ };
1879
+ const parseTypedValue = (raw) => {
1880
+ if (raw == null || raw === "") {
1881
+ if (mode === "multiple" || mode === "range-multiple") return [];
1882
+ if (mode === "range") return { from: null, to: null };
1883
+ return null;
1884
+ }
1885
+ const dates = extractTypedDates(raw);
1886
+ if (mode === "range-multiple") {
1887
+ const ranges = [];
1888
+ for (let index = 0; index < dates.length; index += 2) {
1889
+ if (!dates[index]) continue;
1890
+ ranges.push({ from: dates[index], to: dates[index + 1] || null });
1891
+ }
1892
+ return ranges;
1893
+ }
1894
+ if (mode === "multiple") return dates;
1895
+ if (mode === "range") return { from: dates[0] || null, to: dates[1] || null };
1896
+ return dates[0] || normalizeDateOnly(raw);
1897
+ };
1898
+ const emptyValue = () => ((mode === "multiple" || mode === "range-multiple") ? [] : (mode === "range" ? { from: null, to: null } : null));
1899
+ const resolveDateTimeTimeValue = (raw, fallback = null) => {
1900
+ if (!isTimeEnabled()) return null;
1901
+ const options = getTimeOptions();
1902
+ const explicit = uiUnwrap(props.timeValue);
1903
+ const source = raw != null ? raw : explicit;
1904
+ const resolved = uiExtractTimeFromValue(source, options);
1905
+ return uiCloneTimeParts(resolved ?? fallback ?? null);
1906
+ };
1907
+ const getCurrentTimeValue = () => uiUnwrap(props.confirm) ? workingTimeValue : localTimeValue;
1908
+ const exportCurrentValue = (dateValue = localValue, timeValue = localTimeValue) => {
1909
+ if (mode === "single" && isTimeEnabled()) return uiMergeDateAndTime(dateValue || null, timeValue, getTimeOptions());
1910
+ return exportValue(dateValue);
1911
+ };
1912
+ const serializeCurrentValue = (dateValue = localValue, timeValue = localTimeValue) => JSON.stringify(exportCurrentValue(dateValue, timeValue));
1913
+ const formatTimeDisplayValue = (value) => uiFormatTimeDisplay(value, getTimeOptions());
1914
+
1915
+ let localValue = normalizeValue(initialRawValue);
1916
+ let workingValue = cloneValue(localValue);
1917
+ let localTimeValue = resolveDateTimeTimeValue(initialRawValue, uiExtractTimeFromValue(uiUnwrap(props.timeValue), getTimeOptions()));
1918
+ let workingTimeValue = uiCloneTimeParts(localTimeValue);
1919
+ let hoverDate = null;
1920
+ let mouseSelectedDate = null;
1921
+ let viewMonth = monthStart(
1922
+ mode === "range"
1923
+ ? (localValue?.from || localValue?.to || getDefaultViewValue())
1924
+ : (mode === "range-multiple"
1925
+ ? (localValue?.[localValue.length - 1]?.from || localValue?.[localValue.length - 1]?.to || getDefaultViewValue())
1926
+ : (mode === "multiple"
1927
+ ? (localValue[0] || getDefaultViewValue())
1928
+ : (localValue || getDefaultViewValue())))
1929
+ );
1930
+ let entry = null;
1931
+ let panelRoot = null;
1932
+
1933
+ const getVisibleMonthOffset = (value) => {
1934
+ const date = value instanceof Date ? value : fromIsoDate(normalizeDateOnly(value));
1935
+ if (!date) return -1;
1936
+ const year = date.getFullYear();
1937
+ const month = date.getMonth();
1938
+ for (let index = 0; index < getMonthsToShow(); index += 1) {
1939
+ const visibleMonth = shiftMonths(viewMonth, index);
1940
+ if (visibleMonth.getFullYear() === year && visibleMonth.getMonth() === month) return index;
1941
+ }
1942
+ return -1;
1943
+ };
1944
+
1945
+ const fieldProps = {
1946
+ ...props,
1947
+ label: props.label ?? props.placeholder
1948
+ };
1949
+ const hasFloatingLabel = fieldProps.label != null || JSswift.ui.getSlot(slots, "label") != null;
1950
+ const defaultPlaceholder = mode === "range"
1951
+ ? "Seleziona andata e ritorno"
1952
+ : (mode === "range-multiple"
1953
+ ? "Seleziona intervalli"
1954
+ : (mode === "multiple" ? "Seleziona date" : "Seleziona data"));
1955
+
1956
+ const displayInput = isCalendar ? null : _.input({
1957
+ class: uiClass(["cms-input", "cms-date-display", sizeValue, uiWhen(props.manualInput, "is-manual"), props.inputClass]),
1958
+ type: "text",
1959
+ autocomplete: "off",
1960
+ placeholder: hasFloatingLabel ? "" : (props.placeholder ?? defaultPlaceholder),
1961
+ readOnly: !uiUnwrap(props.manualInput),
1962
+ disabled: !!uiUnwrap(props.disabled),
1963
+ value: formatDisplayValue(localValue, localTimeValue)
1964
+ });
1965
+ const hiddenHost = _.div({ style: { display: "contents" } });
1966
+ const controlNode = isCalendar ? null : _.div({ class: "cms-date-control", style: { display: "contents" } }, displayInput, hiddenHost);
1967
+
1968
+ const field = isCalendar ? null : UI.FormField({
1969
+ ...fieldProps,
1970
+ iconRight: props.iconRight ?? "calendar_month",
1971
+ control: controlNode,
1972
+ getValue: () => displayInput.value,
1973
+ onClear: () => {
1974
+ if (uiUnwrap(props.disabled) || uiUnwrap(props.readonly)) return;
1975
+ setDateValue(emptyValue(), null);
1976
+ if (entry) closePanel();
1977
+ },
1978
+ onFocus: () => displayInput.focus()
1979
+ });
1980
+
1981
+ const syncViewMonth = (value, options = {}) => {
1982
+ const current = options.focusIso || (
1983
+ mode === "range"
1984
+ ? (value?.from || value?.to || getDefaultViewValue())
1985
+ : (mode === "range-multiple"
1986
+ ? (value?.[value.length - 1]?.from || value?.[value.length - 1]?.to || getDefaultViewValue())
1987
+ : (mode === "multiple"
1988
+ ? (value?.[0] || getDefaultViewValue())
1989
+ : (value || getDefaultViewValue())))
1990
+ );
1991
+ let nextViewMonth = monthStart(current);
1992
+ if (options.preserveMonthOffset) {
1993
+ const monthOffset = Number.isInteger(options.visibleMonthOffset)
1994
+ ? options.visibleMonthOffset
1995
+ : getVisibleMonthOffset(options.anchorIso || options.focusIso || current);
1996
+ if (monthOffset > 0) nextViewMonth = shiftMonths(nextViewMonth, -monthOffset);
1997
+ }
1998
+ viewMonth = nextViewMonth;
1999
+ };
2000
+
2001
+ const syncHiddenInputs = () => {
2002
+ hiddenHost.replaceChildren();
2003
+ const baseName = uiUnwrap(props.name);
2004
+ if (!baseName) return;
2005
+ const appendHidden = (name, value) => {
2006
+ hiddenHost.appendChild(_.input({ type: "hidden", name, value: value ?? "" }));
2007
+ };
2008
+ const value = localValue;
2009
+ if (mode === "single") {
2010
+ appendHidden(baseName, exportCurrentValue(value, localTimeValue));
2011
+ return;
2012
+ }
2013
+ if (mode === "range") {
2014
+ appendHidden(uiUnwrap(props.nameFrom) || `${baseName}_from`, value?.from || "");
2015
+ appendHidden(uiUnwrap(props.nameTo) || `${baseName}_to`, value?.to || "");
2016
+ return;
2017
+ }
2018
+ if (mode === "range-multiple") {
2019
+ const rangeList = Array.isArray(value) ? value : [];
2020
+ const fromName = /\[\]$/.test(uiUnwrap(props.nameFrom) || "") ? uiUnwrap(props.nameFrom) : `${uiUnwrap(props.nameFrom) || `${baseName}_from`}[]`;
2021
+ const toName = /\[\]$/.test(uiUnwrap(props.nameTo) || "") ? uiUnwrap(props.nameTo) : `${uiUnwrap(props.nameTo) || `${baseName}_to`}[]`;
2022
+ rangeList.forEach((item) => {
2023
+ appendHidden(fromName, item?.from || "");
2024
+ appendHidden(toName, item?.to || "");
2025
+ });
2026
+ return;
2027
+ }
2028
+ const listName = /\[\]$/.test(baseName) ? baseName : `${baseName}[]`;
2029
+ (Array.isArray(value) ? value : []).forEach((item) => appendHidden(listName, item));
2030
+ };
2031
+
2032
+ const syncDisplay = () => {
2033
+ if (isCalendar) return;
2034
+ displayInput.readOnly = !uiUnwrap(props.manualInput);
2035
+ displayInput.disabled = !!uiUnwrap(props.disabled);
2036
+ displayInput.setAttribute("aria-expanded", entry ? "true" : "false");
2037
+ displayInput.value = formatDisplayValue(localValue, localTimeValue);
2038
+ syncHiddenInputs();
2039
+ field?._refresh?.();
2040
+ };
2041
+
2042
+ const setDateValue = (nextValue, event, options = {}) => {
2043
+ const normalized = normalizeValue(nextValue);
2044
+ const nextTime = isTimeEnabled()
2045
+ ? uiCloneTimeParts(
2046
+ uiExtractTimeFromValue(
2047
+ options.timeValue != null
2048
+ ? options.timeValue
2049
+ : (options.preserveTime ? getCurrentTimeValue() : nextValue),
2050
+ getTimeOptions()
2051
+ ) ?? (options.preserveTime ? getCurrentTimeValue() : null)
2052
+ )
2053
+ : null;
2054
+ const prev = serializeCurrentValue(localValue, localTimeValue);
2055
+ localValue = cloneValue(normalized);
2056
+ workingValue = cloneValue(normalized);
2057
+ if (isTimeEnabled()) {
2058
+ localTimeValue = uiCloneTimeParts(nextTime);
2059
+ workingTimeValue = uiCloneTimeParts(nextTime);
2060
+ }
2061
+ hoverDate = null;
2062
+ syncViewMonth(normalized, options);
2063
+ syncDisplay();
2064
+ renderPanel();
2065
+ if (model && options.fromModel !== true) model.set(exportCurrentValue(normalized, nextTime));
2066
+ const nextSerialized = serializeCurrentValue(normalized, nextTime);
2067
+ if (options.emit !== false && nextSerialized !== prev) {
2068
+ const emitted = exportCurrentValue(normalized, nextTime);
2069
+ props.onInput?.(emitted, event);
2070
+ props.onChange?.(emitted, event);
2071
+ }
2072
+ return normalized;
2073
+ };
2074
+ const setTimeValue = (nextValue, event, options = {}) => {
2075
+ if (!isTimeEnabled()) return null;
2076
+ const normalizedTime = uiCloneTimeParts(uiExtractTimeFromValue(nextValue, getTimeOptions()));
2077
+ if (uiUnwrap(props.confirm) && options.commit !== true) {
2078
+ workingTimeValue = normalizedTime;
2079
+ renderPanel();
2080
+ return normalizedTime;
2081
+ }
2082
+ const prev = serializeCurrentValue(localValue, localTimeValue);
2083
+ localTimeValue = uiCloneTimeParts(normalizedTime);
2084
+ workingTimeValue = uiCloneTimeParts(normalizedTime);
2085
+ syncDisplay();
2086
+ renderPanel();
2087
+ if (model && options.fromModel !== true) model.set(exportCurrentValue(localValue, normalizedTime));
2088
+ const nextSerialized = serializeCurrentValue(localValue, normalizedTime);
2089
+ if (options.emit !== false && nextSerialized !== prev) {
2090
+ const emitted = exportCurrentValue(localValue, normalizedTime);
2091
+ props.onInput?.(emitted, event);
2092
+ props.onChange?.(emitted, event);
2093
+ }
2094
+ return normalizedTime;
2095
+ };
2096
+ const setWorkingOrCommit = (nextValue, event, options = {}) => {
2097
+ if (uiUnwrap(props.confirm)) {
2098
+ workingValue = cloneValue(normalizeValue(nextValue));
2099
+ if (isTimeEnabled() && (options.timeValue != null || options.preserveTime)) {
2100
+ workingTimeValue = uiCloneTimeParts(uiExtractTimeFromValue(
2101
+ options.timeValue != null ? options.timeValue : getCurrentTimeValue(),
2102
+ getTimeOptions()
2103
+ ));
2104
+ }
2105
+ hoverDate = null;
2106
+ syncViewMonth(workingValue, options);
2107
+ renderPanel();
2108
+ return;
2109
+ }
2110
+ setDateValue(nextValue, event, options);
2111
+ };
2112
+ const selectionPreview = () => {
2113
+ const current = getCurrentValue();
2114
+ if (mode === "range") {
2115
+ if (!current?.from || current?.to || !hoverDate) return current;
2116
+ return compareIso(current.from, hoverDate) <= 0
2117
+ ? { from: current.from, to: hoverDate }
2118
+ : { from: hoverDate, to: current.from };
2119
+ }
2120
+ if (mode === "range-multiple") {
2121
+ const last = Array.isArray(current) && current.length ? current[current.length - 1] : null;
2122
+ if (!last?.from || last?.to || !hoverDate) return last;
2123
+ return compareIso(last.from, hoverDate) <= 0
2124
+ ? { from: last.from, to: hoverDate }
2125
+ : { from: hoverDate, to: last.from };
2126
+ }
2127
+ return current;
2128
+ };
2129
+ const renderedRanges = () => {
2130
+ if (mode === "range") {
2131
+ const preview = selectionPreview();
2132
+ return preview?.from ? [preview] : [];
2133
+ }
2134
+ if (mode === "range-multiple") {
2135
+ const current = Array.isArray(getCurrentValue()) ? getCurrentValue() : [];
2136
+ if (!current.length) return [];
2137
+ const preview = selectionPreview();
2138
+ if (!preview?.from) return current;
2139
+ return [...current.slice(0, -1), preview];
2140
+ }
2141
+ return [];
2142
+ };
2143
+ const isSelectedDate = (iso) => {
2144
+ const current = getCurrentValue();
2145
+ if (mode === "multiple") return current.includes(iso);
2146
+ if (mode === "range-multiple") {
2147
+ return Array.isArray(current) && current.some((range) => isSameIso(range?.from, iso) || isSameIso(range?.to, iso));
2148
+ }
2149
+ if (mode === "range") return isSameIso(current?.from, iso) || isSameIso(current?.to, iso);
2150
+ return isSameIso(current, iso);
2151
+ };
2152
+ const isInRange = (iso) => {
2153
+ if (mode !== "range" && mode !== "range-multiple") return false;
2154
+ return renderedRanges().some((range) => range?.from && range?.to && iso >= range.from && iso <= range.to);
2155
+ };
2156
+ const updateRangeHover = (iso, options = {}) => {
2157
+ const current = getCurrentValue();
2158
+ const pendingRange = mode === "range"
2159
+ ? current
2160
+ : (mode === "range-multiple"
2161
+ ? (Array.isArray(current) && current.length ? current[current.length - 1] : null)
2162
+ : null);
2163
+ if (!pendingRange?.from || pendingRange?.to || hoverDate === iso) return;
2164
+ hoverDate = iso;
2165
+ // Re-rendering on focus replaces the clicked day button before the click event fires.
2166
+ if (options.render !== false) renderPanel();
2167
+ };
2168
+ const shouldCloseOnSelect = () => {
2169
+ if (isCalendar) return false;
2170
+ if (uiUnwrap(props.confirm)) return false;
2171
+ if (props.closeOnSelect === false) return false;
2172
+ if (mode === "multiple" || mode === "range-multiple") return !!props.closeOnSelect;
2173
+ return true;
2174
+ };
2175
+ const selectDate = (iso, event, selectionOptions = {}) => {
2176
+ if (!isDateAllowed(iso) || uiUnwrap(props.disabled) || uiUnwrap(props.readonly)) return;
2177
+ const syncOptions = {
2178
+ focusIso: iso,
2179
+ anchorIso: iso,
2180
+ preserveTime: isTimeEnabled(),
2181
+ timeValue: getCurrentTimeValue(),
2182
+ preserveMonthOffset: !!entry && getMonthsToShow() > 1,
2183
+ visibleMonthOffset: Number.isInteger(selectionOptions.visibleMonthOffset) ? selectionOptions.visibleMonthOffset : null
2184
+ };
2185
+ if (mode === "multiple") {
2186
+ const list = normalizeMultipleValue(getCurrentValue());
2187
+ const next = list.includes(iso) ? list.filter((item) => item !== iso) : [...list, iso];
2188
+ setWorkingOrCommit(next.sort(), event, syncOptions);
2189
+ if (shouldCloseOnSelect()) closePanel();
2190
+ return;
2191
+ }
2192
+ if (mode === "range") {
2193
+ const current = cloneValue(getCurrentValue());
2194
+ let next;
2195
+ if (!current.from || (current.from && current.to)) {
2196
+ next = { from: iso, to: null };
2197
+ } else if (compareIso(iso, current.from) < 0) {
2198
+ next = { from: iso, to: current.from };
2199
+ } else {
2200
+ next = { from: current.from, to: iso };
2201
+ }
2202
+ setWorkingOrCommit(next, event, syncOptions);
2203
+ if (next.to && shouldCloseOnSelect()) closePanel();
2204
+ return;
2205
+ }
2206
+ if (mode === "range-multiple") {
2207
+ const list = normalizeRangeMultipleValue(getCurrentValue());
2208
+ const last = list.length ? list[list.length - 1] : null;
2209
+ let next;
2210
+ if (!last?.from || last?.to) {
2211
+ next = [...list, { from: iso, to: null }];
2212
+ } else if (compareIso(iso, last.from) < 0) {
2213
+ next = [...list.slice(0, -1), { from: iso, to: last.from }];
2214
+ } else {
2215
+ next = [...list.slice(0, -1), { from: last.from, to: iso }];
2216
+ }
2217
+ setWorkingOrCommit(next, event, syncOptions);
2218
+ if (next[next.length - 1]?.to && shouldCloseOnSelect()) closePanel();
2219
+ return;
2220
+ }
2221
+ setWorkingOrCommit(iso, event, syncOptions);
2222
+ if (shouldCloseOnSelect()) closePanel();
2223
+ };
2224
+ const clearValue = () => {
2225
+ mouseSelectedDate = null;
2226
+ setDateValue(emptyValue(), null, { timeValue: null });
2227
+ };
2228
+ const jumpToToday = () => {
2229
+ const today = todayIso();
2230
+ if (!isDateAllowed(today)) {
2231
+ syncViewMonth(today);
2232
+ renderPanel();
2233
+ return;
2234
+ }
2235
+ if (mode === "range") {
2236
+ const tomorrow = shiftDays(today, 1);
2237
+ const next = tomorrow && isDateAllowed(tomorrow) ? { from: today, to: tomorrow } : { from: today, to: null };
2238
+ setWorkingOrCommit(next, null);
2239
+ return;
2240
+ }
2241
+ if (mode === "range-multiple") {
2242
+ const tomorrow = shiftDays(today, 1);
2243
+ const next = tomorrow && isDateAllowed(tomorrow)
2244
+ ? [{ from: today, to: tomorrow }]
2245
+ : [{ from: today, to: null }];
2246
+ setWorkingOrCommit(next, null);
2247
+ return;
2248
+ }
2249
+ if (mode === "multiple") {
2250
+ setWorkingOrCommit([today], null);
2251
+ return;
2252
+ }
2253
+ setWorkingOrCommit(today, null, { preserveTime: isTimeEnabled(), timeValue: getCurrentTimeValue() });
2254
+ };
2255
+ const scrollTimeColumnsToSelection = (options = {}) => {
2256
+ uiCenterTimeColumnsToSelection(panelRoot, options);
2257
+ };
2258
+ const jumpToNow = () => {
2259
+ if (!isTimeEnabled()) return;
2260
+ const scrollState = uiCaptureTimeColumnScrollState(panelRoot);
2261
+ setTimeValue(new Date(), null, { commit: !uiUnwrap(props.confirm) });
2262
+ scrollTimeColumnsToSelection({ scrollState });
2263
+ if (!uiUnwrap(props.confirm) && props.closeOnSelect === true) closePanel();
2264
+ };
2265
+ const applyWorkingValue = () => {
2266
+ if (!uiUnwrap(props.confirm)) return;
2267
+ setDateValue(workingValue, null, { timeValue: workingTimeValue, preserveTime: true });
2268
+ if (props.closeOnSelect !== false) closePanel();
2269
+ };
2270
+
2271
+ const renderPanel = () => {
2272
+ if (!panelRoot) return;
2273
+ const locale = getLocale();
2274
+ const monthLabelFormatter = new Intl.DateTimeFormat(locale, { month: "long", year: "numeric" });
2275
+ const weekdayFormatter = new Intl.DateTimeFormat(locale, { weekday: "short" });
2276
+ const titleDates = Array.from({ length: 7 }, (_, index) => {
2277
+ const day = (getFirstDayOfWeek() + index) % 7;
2278
+ return weekdayFormatter.format(new Date(2024, 0, 7 + day));
2279
+ });
2280
+ const pickerValue = getCurrentValue();
2281
+
2282
+ const buildDayPoint = (ctx) => {
2283
+ let pointNode = JSswift.ui.renderSlot(slots, "point", ctx, null);
2284
+ if (pointNode == null) pointNode = JSswift.ui.renderSlot(slots, "dayPoint", ctx, null);
2285
+ if (pointNode == null && props.pointIcon != null && (ctx.selected || ctx.inRange || ctx.today)) {
2286
+ pointNode = typeof props.pointIcon === "string"
2287
+ ? UI.Icon({ name: props.pointIcon, size: 10 })
2288
+ : JSswift.ui.slot(props.pointIcon, ctx);
2289
+ }
2290
+ return renderSlotToArray(null, "default", ctx, pointNode);
2291
+ };
2292
+
2293
+ const renderMonth = (baseDate, monthOffset) => {
2294
+ const year = baseDate.getFullYear();
2295
+ const month = baseDate.getMonth();
2296
+ const start = new Date(year, month, 1);
2297
+ const offset = (start.getDay() - getFirstDayOfWeek() + 7) % 7;
2298
+ const gridStart = new Date(year, month, 1 - offset);
2299
+ const monthBox = _.div({ class: "cms-date-month" });
2300
+ monthBox.appendChild(_.div({ class: "cms-date-month-title" }, monthLabelFormatter.format(start)));
2301
+
2302
+ const weekdays = _.div({ class: "cms-date-weekdays" });
2303
+ titleDates.forEach((label) => weekdays.appendChild(_.div({ class: "cms-date-weekday" }, label)));
2304
+ monthBox.appendChild(weekdays);
2305
+
2306
+ const grid = _.div({ class: "cms-date-grid" });
2307
+ for (let index = 0; index < 42; index += 1) {
2308
+ const date = new Date(gridStart.getFullYear(), gridStart.getMonth(), gridStart.getDate() + index);
2309
+ const iso = toIsoDate(date);
2310
+ const inMonth = date.getMonth() === month;
2311
+ const selected = isSelectedDate(iso);
2312
+ const inRange = isInRange(iso);
2313
+ const rangeStart = (mode === "range" || mode === "range-multiple") && renderedRanges().some((range) => isSameIso(range?.from, iso));
2314
+ const rangeEnd = (mode === "range" || mode === "range-multiple") && renderedRanges().some((range) => isSameIso(range?.to, iso));
2315
+ const disabled = !isDateAllowed(iso);
2316
+ const ctx = {
2317
+ ...getDateContext(iso),
2318
+ selected,
2319
+ inRange,
2320
+ rangeStart,
2321
+ rangeEnd,
2322
+ disabled,
2323
+ outside: !inMonth,
2324
+ value: pickerValue,
2325
+ select: () => selectDate(iso, null, { visibleMonthOffset: monthOffset })
2326
+ };
2327
+ const pointNodes = buildDayPoint(ctx);
2328
+ const labelNode = JSswift.ui.renderSlot(slots, "day", ctx, String(date.getDate()));
2329
+ const labelNodes = renderSlotToArray(null, "default", ctx, labelNode);
2330
+ const dayBtn = _.button({
2331
+ type: "button",
2332
+ class: uiClass([
2333
+ "cms-date-day",
2334
+ uiWhen(!inMonth, "is-outside"),
2335
+ uiWhen(selected, "is-selected"),
2336
+ uiWhen(inRange, "is-in-range"),
2337
+ uiWhen(rangeStart, "is-range-start"),
2338
+ uiWhen(rangeEnd, "is-range-end"),
2339
+ uiWhen(disabled, "is-disabled"),
2340
+ uiWhen(isSameIso(iso, todayIso()), "is-today"),
2341
+ uiWhen(pointNodes.length, "has-point")
2342
+ ]),
2343
+ disabled,
2344
+ onMouseDown: (event) => {
2345
+ if (event.button !== 0) return;
2346
+ mouseSelectedDate = iso;
2347
+ event.preventDefault();
2348
+ selectDate(iso, event, { visibleMonthOffset: monthOffset });
2349
+ },
2350
+ onClick: (event) => {
2351
+ if (mouseSelectedDate != null && event.detail !== 0) {
2352
+ mouseSelectedDate = null;
2353
+ return;
2354
+ }
2355
+ mouseSelectedDate = null;
2356
+ selectDate(iso, event, { visibleMonthOffset: monthOffset });
2357
+ },
2358
+ onMouseEnter: () => updateRangeHover(iso),
2359
+ onFocus: () => updateRangeHover(iso, { render: false })
2360
+ },
2361
+ _.span({ class: "cms-date-day-label" }, ...labelNodes),
2362
+ pointNodes.length ? _.span({ class: "cms-date-day-point" }, ...pointNodes) : null
2363
+ );
2364
+ grid.appendChild(dayBtn);
2365
+ }
2366
+ monthBox.appendChild(grid);
2367
+ return monthBox;
2368
+ };
2369
+
2370
+ const min = getMin();
2371
+ const max = getMax();
2372
+ const candidateYears = [
2373
+ viewMonth.getFullYear(),
2374
+ fromIsoDate(todayIso())?.getFullYear(),
2375
+ fromIsoDate(min)?.getFullYear(),
2376
+ fromIsoDate(max)?.getFullYear(),
2377
+ fromIsoDate(uiUnwrap(props.defaultMonth))?.getFullYear()
2378
+ ];
2379
+ if (mode === "range") {
2380
+ candidateYears.push(fromIsoDate(pickerValue?.from)?.getFullYear(), fromIsoDate(pickerValue?.to)?.getFullYear());
2381
+ } else if (mode === "range-multiple") {
2382
+ pickerValue.forEach((item) => {
2383
+ candidateYears.push(fromIsoDate(item?.from)?.getFullYear(), fromIsoDate(item?.to)?.getFullYear());
2384
+ });
2385
+ } else if (mode === "multiple") {
2386
+ pickerValue.forEach((item) => candidateYears.push(fromIsoDate(item)?.getFullYear()));
2387
+ } else {
2388
+ candidateYears.push(fromIsoDate(pickerValue)?.getFullYear());
2389
+ }
2390
+ const validYears = candidateYears.filter((year) => Number.isFinite(year));
2391
+ const inferredYearStart = (validYears.length ? Math.min(...validYears) : viewMonth.getFullYear()) - 100;
2392
+ const inferredYearEnd = (validYears.length ? Math.max(...validYears) : viewMonth.getFullYear()) + 50;
2393
+ const yearStart = Number(uiUnwrap(props.yearStart ?? (min ? fromIsoDate(min)?.getFullYear() : null) ?? inferredYearStart));
2394
+ const yearEnd = Number(uiUnwrap(props.yearEnd ?? (max ? fromIsoDate(max)?.getFullYear() : null) ?? inferredYearEnd));
2395
+ const monthSelect = _.select({
2396
+ class: "cms-date-select",
2397
+ value: String(viewMonth.getMonth()),
2398
+ onChange: (event) => {
2399
+ viewMonth = new Date(viewMonth.getFullYear(), Number(event.currentTarget.value), 1);
2400
+ props.onNavigate?.({ month: viewMonth.getMonth() + 1, year: viewMonth.getFullYear() });
2401
+ renderPanel();
2402
+ }
2403
+ },
2404
+ ...Array.from({ length: 12 }, (_, index) => uiOptionNode({ value: String(index) }, new Intl.DateTimeFormat(locale, { month: "long" }).format(new Date(2024, index, 1))))
2405
+ );
2406
+ const yearSelect = _.select({
2407
+ class: "cms-date-select cms-date-select-year",
2408
+ value: String(viewMonth.getFullYear()),
2409
+ onChange: (event) => {
2410
+ viewMonth = new Date(Number(event.currentTarget.value), viewMonth.getMonth(), 1);
2411
+ props.onNavigate?.({ month: viewMonth.getMonth() + 1, year: viewMonth.getFullYear() });
2412
+ renderPanel();
2413
+ }
2414
+ },
2415
+ ...Array.from({ length: Math.max(1, yearEnd - yearStart + 1) }, (_, index) => {
2416
+ const year = yearStart + index;
2417
+ return uiOptionNode({ value: String(year) }, String(year));
2418
+ })
2419
+ );
2420
+
2421
+ const header = _.div({ class: "cms-date-header" },
2422
+ _.button({
2423
+ type: "button",
2424
+ class: "cms-date-nav",
2425
+ onClick: () => {
2426
+ viewMonth = shiftMonths(viewMonth, -1);
2427
+ props.onNavigate?.({ month: viewMonth.getMonth() + 1, year: viewMonth.getFullYear() });
2428
+ renderPanel();
2429
+ }
2430
+ }, UI.Icon({ name: "chevron_left", size: 16 })),
2431
+ _.div({ class: "cms-date-header-center" }, monthSelect, yearSelect),
2432
+ _.button({
2433
+ type: "button",
2434
+ class: "cms-date-nav",
2435
+ onClick: () => {
2436
+ viewMonth = shiftMonths(viewMonth, 1);
2437
+ props.onNavigate?.({ month: viewMonth.getMonth() + 1, year: viewMonth.getFullYear() });
2438
+ renderPanel();
2439
+ }
2440
+ }, UI.Icon({ name: "chevron_right", size: 16 }))
2441
+ );
2442
+
2443
+ const shortcuts = _.div({ class: "cms-date-shortcuts" });
2444
+ const shortcutList = uiUnwrap(props.shortcuts ?? props.presets) || [];
2445
+ shortcutList.forEach((item, index) => {
2446
+ if (!item) return;
2447
+ const rawValue = typeof item.value === "function" ? item.value({ today: todayIso() }) : item.value;
2448
+ const label = item.label ?? item.text ?? `Preset ${index + 1}`;
2449
+ shortcuts.appendChild(_.button({
2450
+ type: "button",
2451
+ class: "cms-date-shortcut",
2452
+ onClick: () => setWorkingOrCommit(rawValue, null, { preserveTime: isTimeEnabled(), timeValue: getCurrentTimeValue() })
2453
+ }, label));
2454
+ });
2455
+
2456
+ const months = _.div({ class: "cms-date-months" });
2457
+ for (let index = 0; index < getMonthsToShow(); index += 1) {
2458
+ months.appendChild(renderMonth(shiftMonths(viewMonth, index), index));
2459
+ }
2460
+ const footerDisplayValue = formatDisplayValue(pickerValue, getCurrentTimeValue());
2461
+ const footerValue = _.div({ class: "cms-date-value" },
2462
+ ...renderSlotToArray(
2463
+ slots,
2464
+ "value",
2465
+ { value: pickerValue, displayValue: footerDisplayValue, mode, timeValue: getCurrentTimeValue() },
2466
+ footerDisplayValue || renderSlotToArray(slots, "default", {}, children)
2467
+ )
2468
+ );
2469
+ const headerTime = _.div({ class: "cms-time-header" });
2470
+ const columnsTime = _.div({ class: "cms-time-columns-2" },
2471
+ _.div({ class: "cms-time-column-title" }, uiUnwrap(props.hoursLabel) || "Ore"),
2472
+ _.div({ class: "cms-time-column-title" }, uiUnwrap(props.minutesLabel) || "Min")
2473
+ );
2474
+ headerTime.appendChild(columnsTime);
2475
+ const bodyTime = isTimeEnabled()
2476
+ ? uiCreateTimePickerSection({
2477
+ props: {
2478
+ min: uiUnwrap(props.timeMin ?? props.minTime),
2479
+ max: uiUnwrap(props.timeMax ?? props.maxTime),
2480
+ use12h: uiUnwrap(props.time12h ?? props.use12h ?? props.ampm),
2481
+ hoursLabel: uiUnwrap(props.timeHoursLabel) || "Ore",
2482
+ minutesLabel: uiUnwrap(props.timeMinutesLabel) || "Min",
2483
+ secondsLabel: uiUnwrap(props.timeSecondsLabel) || "Sec"
2484
+ },
2485
+ slots,
2486
+ slotPrefix: "time",
2487
+ value: getCurrentTimeValue(),
2488
+ withSeconds: false,
2489
+ minuteStep: 1,
2490
+ secondStep: getTimeSecondStep(),
2491
+ disabled: !!uiUnwrap(props.disabled),
2492
+ readonly: !!uiUnwrap(props.readonly),
2493
+ embedded: true,
2494
+ pointIcon: uiUnwrap(props.timePointIcon),
2495
+ shortcuts: uiUnwrap(props.timeShortcuts ?? props.timePresets),
2496
+ onSelect: (nextTime) => {
2497
+ const scrollState = uiCaptureTimeColumnScrollState(panelRoot);
2498
+ setTimeValue(nextTime, null);
2499
+ scrollTimeColumnsToSelection({ scrollState });
2500
+ }
2501
+ })
2502
+ : null;
2503
+ const footerInfo = _.div({ class: "cms-date-footer-info" },
2504
+ footerValue
2505
+ );
2506
+
2507
+ const footer = _.div({ class: uiClass(["cms-date-footer", uiWhen(isTimeEnabled(), "has-time")]) },
2508
+ footerInfo,
2509
+ _.div({ class: "cms-date-actions" },
2510
+ _.button({ type: "button", class: "cms-date-action", onClick: jumpToToday }, uiUnwrap(props.todayLabel) || "Oggi"),
2511
+ uiUnwrap(props.clearable) !== false
2512
+ ? _.button({ type: "button", class: "cms-date-action", onClick: clearValue }, uiUnwrap(props.clearLabel) || "Reset")
2513
+ : null,
2514
+ uiUnwrap(props.confirm)
2515
+ ? _.button({ type: "button", class: "cms-date-action is-primary", onClick: applyWorkingValue }, uiUnwrap(props.applyLabel) || "Applica")
2516
+ : null
2517
+ )
2518
+ );
2519
+
2520
+ const footerTimer = _.div(_.button({ type: "button", class: "cms-date-action cms-w-lg", onClick: jumpToNow }, uiUnwrap(props.nowLabel) || "Ora"));
2521
+
2522
+ const contentTimer = isTimeEnabled() ? _.div({ class: "cms-date-time" }, headerTime, bodyTime, footerTimer) : null;
2523
+
2524
+ panelRoot.replaceChildren(
2525
+ _.div({
2526
+ class: "cms-date-panel-root",
2527
+ onKeydown: (event) => {
2528
+ if (event.key === "Escape") {
2529
+ event.preventDefault();
2530
+ if (isCalendar) props.onEscape?.(event);
2531
+ else closePanel();
2532
+ }
2533
+ }
2534
+ },
2535
+ header,
2536
+ shortcutList.length ? shortcuts : null,
2537
+ _.div({ class: 'cms-data-time' }, months, contentTimer),
2538
+ footer
2539
+ )
2540
+ );
2541
+ };
2542
+
2543
+ function openPanel() {
2544
+ if (entry || uiUnwrap(props.disabled) || uiUnwrap(props.readonly)) return entry;
2545
+ workingValue = cloneValue(localValue);
2546
+ workingTimeValue = uiCloneTimeParts(localTimeValue);
2547
+ hoverDate = null;
2548
+ mouseSelectedDate = null;
2549
+ syncViewMonth(workingValue);
2550
+ entry = JSswift.overlay.open(({ close }) => {
2551
+ const fallback = (mode === "range" || mode === "range-multiple") ? 2 : 1;
2552
+ const raw = Number(uiUnwrap(props.monthsToShow) ?? fallback);
2553
+ panelRoot = _.div({ class: uiClassStatic(["cms-date-panel", uiUnwrap(sizeValue), uiWhen(raw > 1, "multi-month"), props.panelClass]) });
2554
+ renderPanel();
2555
+ return panelRoot;
2556
+ }, {
2557
+ type: "date",
2558
+ anchorEl: field._control || displayInput,
2559
+ placement: props.placement || "bottom-start",
2560
+ offsetX: props.offsetX ?? 0,
2561
+ offsetY: props.offsetY ?? props.offset ?? 8,
2562
+ backdrop: false,
2563
+ lockScroll: false,
2564
+ trapFocus: false,
2565
+ closeOnOutside: props.closeOnOutside !== false,
2566
+ closeOnBackdrop: false,
2567
+ closeOnEsc: true,
2568
+ autoFocus: false,
2569
+ className: uiClassStatic(["cms-date-overlay", props.panelClass, uiWhen(isTimeEnabled(), "has-time")]),
2570
+ onClose: () => {
2571
+ entry = null;
2572
+ panelRoot = null;
2573
+ hoverDate = null;
2574
+ syncDisplay();
2575
+ props.onClose?.();
2576
+ }
2577
+ });
2578
+ if (props.panelStyle) Object.assign(entry.panel.style, props.panelStyle);
2579
+ overlayEnter(entry);
2580
+ syncDisplay();
2581
+ props.onOpen?.();
2582
+ return entry;
2583
+ }
2584
+
2585
+ function closePanel() {
2586
+ if (!entry) return;
2587
+ mouseSelectedDate = null;
2588
+ const toClose = entry;
2589
+ overlayLeave(toClose, () => JSswift.overlay.close(toClose.id));
2590
+ }
2591
+
2592
+ if (isCalendar) {
2593
+ const fallback = (mode === "range" || mode === "range-multiple") ? 2 : 1;
2594
+ const raw = Number(uiUnwrap(props.monthsToShow) ?? fallback);
2595
+ panelRoot = _.div({
2596
+ class: uiClassStatic([
2597
+ "cms-date-panel",
2598
+ "cms-date-calendar",
2599
+ uiUnwrap(sizeValue),
2600
+ uiWhen(raw > 1, "multi-month"),
2601
+ props.panelClass,
2602
+ props.class
2603
+ ]),
2604
+ style: props.style
2605
+ });
2606
+ }
2607
+
2608
+ if (!isCalendar) displayInput.addEventListener("focus", (event) => {
2609
+ props.onFocus?.(event);
2610
+ if (props.openOnFocus !== false) openPanel();
2611
+ });
2612
+ if (!isCalendar) displayInput.addEventListener("click", (event) => {
2613
+ props.onClick?.(event);
2614
+ openPanel();
2615
+ });
2616
+ if (!isCalendar) displayInput.addEventListener("keydown", (event) => {
2617
+ if (event.key === "Escape") {
2618
+ if (entry) {
2619
+ event.preventDefault();
2620
+ closePanel();
2621
+ }
2622
+ return;
2623
+ }
2624
+ if (event.key === "ArrowDown" || event.key === "Enter" || event.key === " ") {
2625
+ event.preventDefault();
2626
+ openPanel();
2627
+ }
2628
+ if ((event.key === "Backspace" || event.key === "Delete") && !uiUnwrap(props.manualInput) && uiUnwrap(props.clearable) !== false) {
2629
+ event.preventDefault();
2630
+ clearValue();
2631
+ }
2632
+ });
2633
+ if (!isCalendar && uiUnwrap(props.manualInput)) {
2634
+ displayInput.addEventListener("input", (event) => {
2635
+ props.onTyping?.(displayInput.value, event);
2636
+ });
2637
+ displayInput.addEventListener("change", (event) => {
2638
+ const parsed = parseTypedValue(displayInput.value);
2639
+ setDateValue(parsed, event, { timeValue: uiExtractTimeFromValue(displayInput.value, getTimeOptions()), preserveTime: !uiExtractTimeFromValue(displayInput.value, getTimeOptions()) });
2640
+ });
2641
+ displayInput.addEventListener("blur", (event) => {
2642
+ props.onBlur?.(event);
2643
+ const parsed = parseTypedValue(displayInput.value);
2644
+ if (displayInput.value === "") clearValue();
2645
+ else setDateValue(parsed, event, { timeValue: uiExtractTimeFromValue(displayInput.value, getTimeOptions()), preserveTime: !uiExtractTimeFromValue(displayInput.value, getTimeOptions()) });
2646
+ });
2647
+ } else if (!isCalendar) {
2648
+ displayInput.addEventListener("blur", (event) => props.onBlur?.(event));
2649
+ }
2650
+
2651
+ if (model) {
2652
+ model.watch((next) => {
2653
+ const normalized = normalizeValue(next);
2654
+ const nextTime = resolveDateTimeTimeValue(next);
2655
+ if (serializeCurrentValue(normalized, nextTime) === serializeCurrentValue(localValue, localTimeValue)) return;
2656
+ localValue = normalized;
2657
+ workingValue = cloneValue(localValue);
2658
+ localTimeValue = uiCloneTimeParts(nextTime);
2659
+ workingTimeValue = uiCloneTimeParts(nextTime);
2660
+ hoverDate = null;
2661
+ syncViewMonth(localValue);
2662
+ syncDisplay();
2663
+ renderPanel();
2664
+ }, isCalendar ? "UI.Calendar:watch" : "UI.Datepicker:watch");
2665
+ }
2666
+
2667
+ JSswift.reactive.effect(() => {
2668
+ if (!model && props.value != null) {
2669
+ localValue = normalizeValue(uiUnwrap(props.value));
2670
+ workingValue = cloneValue(localValue);
2671
+ localTimeValue = resolveDateTimeTimeValue(uiUnwrap(props.value));
2672
+ workingTimeValue = uiCloneTimeParts(localTimeValue);
2673
+ } else if (isTimeEnabled() && props.timeValue != null) {
2674
+ localTimeValue = resolveDateTimeTimeValue(uiUnwrap(props.timeValue));
2675
+ workingTimeValue = uiCloneTimeParts(localTimeValue);
2676
+ }
2677
+ syncDisplay();
2678
+ renderPanel();
2679
+ if (entry && uiUnwrap(props.disabled)) closePanel();
2680
+ }, isCalendar ? "UI.Calendar:render" : "UI.Datepicker:render");
2681
+
2682
+ if (isCalendar) {
2683
+ panelRoot._calendar = panelRoot;
2684
+ panelRoot._date = panelRoot;
2685
+ panelRoot._getValue = () => exportCurrentValue(localValue, localTimeValue);
2686
+ panelRoot._setValue = (value) => setDateValue(value, null, { emit: false, timeValue: resolveDateTimeTimeValue(value) });
2687
+ panelRoot._clear = clearValue;
2688
+ panelRoot._select = (value) => {
2689
+ const iso = normalizeDateOnly(value);
2690
+ if (iso) selectDate(iso, null);
2691
+ };
2692
+ panelRoot._time = () => uiCloneTimeParts(localTimeValue);
2693
+ return panelRoot;
2694
+ }
2695
+
2696
+ field._input = displayInput;
2697
+ field._date = displayInput;
2698
+ field._open = openPanel;
2699
+ field._close = closePanel;
2700
+ field._getValue = () => exportCurrentValue(localValue, localTimeValue);
2701
+ field._setValue = (value) => setDateValue(value, null, { emit: false, timeValue: resolveDateTimeTimeValue(value) });
2702
+ field._time = () => uiCloneTimeParts(localTimeValue);
2703
+ field._panel = () => entry?.panel || null;
2704
+
2705
+ return field;
2706
+ };
2707
+ UI.Datepicker = (...args) => buildDateControl(args);
2708
+ UI.Calendar = (...args) => buildDateControl(args, { calendar: true });
2709
+ UI.Date = UI.Datepicker;
2710
+ JSswift.ui.Datepicker = UI.Datepicker;
2711
+ JSswift.ui.Calendar = UI.Calendar;
2712
+ JSswift.ui.Date = UI.Date;
2713
+ if (JSswift.isDev?.()) {
2714
+ UI.meta = UI.meta || {};
2715
+ UI.meta.Datepicker = {
2716
+ signature: "UI.Datepicker(props)",
2717
+ props: {
2718
+ value: "string | { from, to } | string[] | Array<{ from, to }> | Array<[from, to]>",
2719
+ model: "rod | [get,set] signal",
2720
+ mode: "\"single\"|\"range\"|\"multiple\"|\"range-multiple\"",
2721
+ range: "boolean",
2722
+ multiple: "boolean",
2723
+ rangeMultiple: "boolean",
2724
+ multipleRange: "Alias of rangeMultiple",
2725
+ min: "string",
2726
+ max: "string",
2727
+ minDate: "Alias of min",
2728
+ maxDate: "Alias of max",
2729
+ minRange: "number",
2730
+ maxRange: "number",
2731
+ manualInput: "boolean",
2732
+ firstDayOfWeek: "number",
2733
+ monthsToShow: "number",
2734
+ locale: "string",
2735
+ shortcuts: "Array<{ label, value }>",
2736
+ options: "Array|string|Function",
2737
+ enableDates: "Alias of options",
2738
+ disableDates: "Array|string|Function",
2739
+ label: "String|Node|Function|Array",
2740
+ icon: "String|Node|Function|Array",
2741
+ iconRight: "String|Node|Function|Array",
2742
+ pointIcon: "String|Node|Function|Array",
2743
+ withTime: "boolean",
2744
+ timeValue: "string|Date",
2745
+ timeMin: "string",
2746
+ timeMax: "string",
2747
+ timeMinuteStep: "number",
2748
+ timeSecondStep: "number",
2749
+ timeWithSeconds: "boolean",
2750
+ timeShortcuts: "Array<{ label, value }>",
2751
+ timePointIcon: "String|Node|Function|Array",
2752
+ clearable: "boolean",
2753
+ confirm: "boolean",
2754
+ name: "string",
2755
+ nameFrom: "string",
2756
+ nameTo: "string",
2757
+ size: "\"xs\"|\"sm\"|\"md\"|\"lg\"|\"xl\"",
2758
+ class: "string",
2759
+ style: "object"
2760
+ },
2761
+ slots: {
2762
+ day: "Day content ({ date, selected, inRange, disabled, outside })",
2763
+ point: "Point/icon in the day",
2764
+ dayPoint: "Alias of point",
2765
+ value: "Footer value renderer ({ value, displayValue, mode })",
2766
+ timeOption: "Renderer for a time option in the integrated footer",
2767
+ timePoint: "Point/icon in the footer time option",
2768
+ timeShortcut: "Integrated time shortcut renderer",
2769
+ label: "Floating label",
2770
+ topLabel: "Top label",
2771
+ icon: "Left icon",
2772
+ iconRight: "Right icon",
2773
+ default: "Fallback value content"
2774
+ },
2775
+ events: {
2776
+ onChange: "(value, event)",
2777
+ onInput: "(value, event)",
2778
+ onOpen: "void",
2779
+ onClose: "void",
2780
+ onNavigate: "({ month, year })"
2781
+ },
2782
+ returns: "HTMLDivElement (field wrapper) con ._input, ._open(), ._close(), ._getValue(), ._setValue(value)",
2783
+ description: "Reactive datepicker input with fixed overlay, single/range/multiple/multi-range modes, model, min/max, presets, xs-xl sizes, and optional time support in a unified interface."
2784
+ };
2785
+ UI.meta.Calendar = {
2786
+ ...UI.meta.Datepicker,
2787
+ signature: "UI.Calendar(props)",
2788
+ returns: "HTMLDivElement calendar panel con ._getValue(), ._setValue(value), ._clear(), ._select(value)",
2789
+ description: "Standalone interactive calendar panel using the same engine and styling as UI.Datepicker, with direct model persistence."
2790
+ };
2791
+ UI.meta.Date = UI.meta.Datepicker;
2792
+ }
2793
+ // Esempio: JSswift.ui.Datepicker({ value: "2024-01-01" })
2794
+
2795
+ UI.Time = (...args) => {
2796
+ const { props, children } = JSswift.uiNormalizeArgs(args);
2797
+ const slots = props.slots || {};
2798
+ const sizeValue = uiComputed(props.size, () => {
2799
+ const value = String(uiUnwrap(props.size) || "").toLowerCase();
2800
+ return ["xs", "sm", "md", "lg", "xl"].includes(value) ? `cms-size-${value}` : "";
2801
+ });
2802
+ const getTimeOptions = () => {
2803
+ const detectedTime = uiExportTimeValue(
2804
+ uiExtractTimeFromValue(model ? model.get() : uiUnwrap(props.value) ?? initialRawValue, { withSeconds: true }),
2805
+ { withSeconds: true }
2806
+ );
2807
+ return {
2808
+ withSeconds: !!uiUnwrap(props.withSeconds ?? props.showSeconds) || /:\d{2}:\d{2}$/.test(detectedTime),
2809
+ min: uiUnwrap(props.min ?? props.minTime),
2810
+ max: uiUnwrap(props.max ?? props.maxTime),
2811
+ use12h: uiUnwrap(props.use12h ?? props.ampm)
2812
+ };
2813
+ };
2814
+ const getMinuteStep = () => {
2815
+ const raw = Number(uiUnwrap(props.minuteStep ?? 1));
2816
+ return Math.max(1, Math.min(30, Number.isFinite(raw) ? raw : 5));
2817
+ };
2818
+ const getSecondStep = () => {
2819
+ const raw = Number(uiUnwrap(props.secondStep ?? 1));
2820
+ return Math.max(1, Math.min(30, Number.isFinite(raw) ? raw : 5));
2821
+ };
2822
+ const valueBinding = props.model || ((uiIsSignal(props.value) || uiIsRod(props.value)) ? props.value : null);
2823
+ const model = resolveModel(valueBinding, "UI.Time:model");
2824
+ const initialRawValue = model ? model.get() : uiUnwrap(props.value);
2825
+ const parseTypedValue = (raw) => uiExtractTimeFromValue(raw, getTimeOptions());
2826
+ const formatDisplayValue = (value) => uiFormatTimeDisplay(value, getTimeOptions());
2827
+ const exportValue = (value) => uiExportTimeValue(value, getTimeOptions());
2828
+ const serializeValue = (value) => JSON.stringify(exportValue(value));
2829
+
2830
+ let localValue = uiCloneTimeParts(parseTypedValue(initialRawValue));
2831
+ let workingValue = uiCloneTimeParts(localValue);
2832
+ let entry = null;
2833
+ let panelRoot = null;
2834
+
2835
+ const fieldProps = {
2836
+ ...props,
2837
+ label: props.label ?? props.placeholder
2838
+ };
2839
+ const hasFloatingLabel = fieldProps.label != null || JSswift.ui.getSlot(slots, "label") != null;
2840
+
2841
+ const displayInput = _.input({
2842
+ class: uiClass(["cms-input", "cms-time-display", sizeValue, uiWhen(props.manualInput, "is-manual"), props.inputClass]),
2843
+ type: "text",
2844
+ autocomplete: "off",
2845
+ placeholder: hasFloatingLabel ? "" : (props.placeholder ?? "Seleziona orario"),
2846
+ readOnly: !uiUnwrap(props.manualInput),
2847
+ disabled: !!uiUnwrap(props.disabled),
2848
+ value: formatDisplayValue(localValue)
2849
+ });
2850
+ displayInput.setAttribute("aria-haspopup", "dialog");
2851
+ const hiddenHost = _.div({ style: { display: "contents" } });
2852
+ const controlNode = _.div({ class: "cms-time-control", style: { display: "contents" } }, displayInput, hiddenHost);
2853
+
2854
+ const field = UI.FormField({
2855
+ ...fieldProps,
2856
+ iconRight: props.iconRight ?? "schedule",
2857
+ control: controlNode,
2858
+ getValue: () => displayInput.value,
2859
+ onClear: () => {
2860
+ if (uiUnwrap(props.disabled) || uiUnwrap(props.readonly)) return;
2861
+ setTimeState(null, null, { commit: true });
2862
+ if (entry) closePanel();
2863
+ },
2864
+ onFocus: () => displayInput.focus()
2865
+ });
2866
+
2867
+ const getCurrentValue = () => uiUnwrap(props.confirm) ? workingValue : localValue;
2868
+ const syncHiddenInputs = () => {
2869
+ hiddenHost.replaceChildren();
2870
+ const baseName = uiUnwrap(props.name);
2871
+ if (!baseName) return;
2872
+ hiddenHost.appendChild(_.input({ type: "hidden", name: baseName, value: exportValue(localValue) }));
2873
+ };
2874
+ const syncDisplay = () => {
2875
+ displayInput.readOnly = !uiUnwrap(props.manualInput);
2876
+ displayInput.disabled = !!uiUnwrap(props.disabled);
2877
+ displayInput.setAttribute("aria-expanded", entry ? "true" : "false");
2878
+ displayInput.value = formatDisplayValue(localValue);
2879
+ syncHiddenInputs();
2880
+ field._refresh?.();
2881
+ };
2882
+ const setTimeState = (nextValue, event, options = {}) => {
2883
+ const normalized = uiCloneTimeParts(parseTypedValue(nextValue));
2884
+ if (uiUnwrap(props.confirm) && options.commit !== true) {
2885
+ workingValue = uiCloneTimeParts(normalized);
2886
+ renderPanel();
2887
+ return normalized;
2888
+ }
2889
+ const prev = serializeValue(localValue);
2890
+ localValue = uiCloneTimeParts(normalized);
2891
+ workingValue = uiCloneTimeParts(normalized);
2892
+ syncDisplay();
2893
+ renderPanel();
2894
+ if (model && options.fromModel !== true) model.set(exportValue(normalized));
2895
+ const nextSerialized = serializeValue(normalized);
2896
+ if (options.emit !== false && nextSerialized !== prev) {
2897
+ const emitted = exportValue(normalized);
2898
+ props.onInput?.(emitted, event);
2899
+ props.onChange?.(emitted, event);
2900
+ }
2901
+ return normalized;
2902
+ };
2903
+ const shouldCloseOnSelect = (meta) => {
2904
+ if (uiUnwrap(props.confirm)) return false;
2905
+ if (props.closeOnSelect !== true) return false;
2906
+ if (getTimeOptions().withSeconds) return meta?.part === "second";
2907
+ return meta?.part === "minute";
2908
+ };
2909
+ const scrollTimeColumnsToSelection = (options = {}) => {
2910
+ uiCenterTimeColumnsToSelection(panelRoot, options);
2911
+ };
2912
+ const jumpToNow = () => {
2913
+ const scrollState = uiCaptureTimeColumnScrollState(panelRoot);
2914
+ setTimeState(new Date(), null, { commit: !uiUnwrap(props.confirm) });
2915
+ scrollTimeColumnsToSelection({ scrollState });
2916
+ if (!uiUnwrap(props.confirm) && props.closeOnSelect === true) closePanel();
2917
+ };
2918
+ const applyWorkingValue = () => {
2919
+ if (!uiUnwrap(props.confirm)) return;
2920
+ setTimeState(workingValue, null, { commit: true });
2921
+ if (props.closeOnSelect !== false) closePanel();
2922
+ };
2923
+
2924
+ const renderPanel = () => {
2925
+ if (!panelRoot) return;
2926
+ const currentValue = getCurrentValue();
2927
+ const footerDisplayValue = formatDisplayValue(currentValue);
2928
+ const header = _.div({ class: "cms-time-header" });
2929
+ const columns = _.div({ class: "cms-time-columns" },
2930
+ _.div({ class: "cms-time-column-title" }, uiUnwrap(props.hoursLabel) || "Ore"),
2931
+ _.div({ class: "cms-time-column-title" }, uiUnwrap(props.minutesLabel) || "Min")
2932
+ );
2933
+ const withSeconds = getTimeOptions().withSeconds;
2934
+ if (withSeconds) {
2935
+ columns.appendChild(_.div({ class: "cms-time-column-title" }, uiUnwrap(props.secondsLabel) || "Sec"));
2936
+ }
2937
+ header.appendChild(columns);
2938
+ const body = uiCreateTimePickerSection({
2939
+ props: {
2940
+ min: uiUnwrap(props.min ?? props.minTime),
2941
+ max: uiUnwrap(props.max ?? props.maxTime),
2942
+ use12h: uiUnwrap(props.use12h ?? props.ampm),
2943
+ hoursLabel: uiUnwrap(props.hoursLabel) || "Ore",
2944
+ minutesLabel: uiUnwrap(props.minutesLabel) || "Min",
2945
+ secondsLabel: uiUnwrap(props.secondsLabel) || "Sec"
2946
+ },
2947
+ slots,
2948
+ value: currentValue,
2949
+ withSeconds: withSeconds,
2950
+ minuteStep: getMinuteStep(),
2951
+ secondStep: getSecondStep(),
2952
+ disabled: !!uiUnwrap(props.disabled),
2953
+ readonly: !!uiUnwrap(props.readonly),
2954
+ pointIcon: uiUnwrap(props.pointIcon),
2955
+ shortcuts: uiUnwrap(props.shortcuts ?? props.presets),
2956
+ onSelect: (nextTime, meta) => {
2957
+ const scrollState = uiCaptureTimeColumnScrollState(panelRoot);
2958
+ setTimeState(nextTime, null);
2959
+ scrollTimeColumnsToSelection({ scrollState });
2960
+ if (shouldCloseOnSelect(meta)) closePanel();
2961
+ }
2962
+ });
2963
+ const footer = _.div({ class: "cms-time-footer" },
2964
+ _.div({ class: "cms-time-value" },
2965
+ ...renderSlotToArray(
2966
+ slots,
2967
+ "value",
2968
+ { value: currentValue, displayValue: footerDisplayValue },
2969
+ footerDisplayValue || renderSlotToArray(slots, "default", {}, children)
2970
+ )
2971
+ ),
2972
+ _.div({ class: "cms-time-actions" },
2973
+ _.button({ type: "button", class: "cms-time-action", onClick: jumpToNow }, uiUnwrap(props.nowLabel) || "Ora"),
2974
+ uiUnwrap(props.clearable) !== false
2975
+ ? _.button({ type: "button", class: "cms-time-action", onClick: () => setTimeState(null, null, { commit: !uiUnwrap(props.confirm) }) }, uiUnwrap(props.clearLabel) || "Reset")
2976
+ : null,
2977
+ uiUnwrap(props.confirm)
2978
+ ? _.button({ type: "button", class: "cms-time-action is-primary", onClick: applyWorkingValue }, uiUnwrap(props.applyLabel) || "Applica")
2979
+ : null
2980
+ )
2981
+ );
2982
+ panelRoot.replaceChildren(_.div({
2983
+ class: "cms-time-panel-root",
2984
+ onKeydown: (event) => {
2985
+ if (event.key === "Escape") {
2986
+ event.preventDefault();
2987
+ closePanel();
2988
+ }
2989
+ }
2990
+ }, header, body, footer));
2991
+ };
2992
+
2993
+ function openPanel() {
2994
+ if (entry || uiUnwrap(props.disabled) || uiUnwrap(props.readonly)) return entry;
2995
+ workingValue = uiCloneTimeParts(localValue);
2996
+ entry = JSswift.overlay.open(() => {
2997
+ panelRoot = _.div({ class: uiClassStatic(["cms-time-panel", uiUnwrap(sizeValue), props.panelClass]) });
2998
+ renderPanel();
2999
+ return panelRoot;
3000
+ }, {
3001
+ type: "time",
3002
+ anchorEl: field._control || displayInput,
3003
+ placement: props.placement || "bottom-start",
3004
+ offsetX: props.offsetX ?? 0,
3005
+ offsetY: props.offsetY ?? props.offset ?? 8,
3006
+ backdrop: false,
3007
+ lockScroll: false,
3008
+ trapFocus: false,
3009
+ closeOnOutside: props.closeOnOutside !== false,
3010
+ closeOnBackdrop: false,
3011
+ closeOnEsc: true,
3012
+ autoFocus: false,
3013
+ className: uiClassStatic(["cms-time-overlay", props.panelClass]),
3014
+ onClose: () => {
3015
+ entry = null;
3016
+ panelRoot = null;
3017
+ syncDisplay();
3018
+ props.onClose?.();
3019
+ }
3020
+ });
3021
+ if (props.panelStyle) Object.assign(entry.panel.style, props.panelStyle);
3022
+ overlayEnter(entry);
3023
+ scrollTimeColumnsToSelection();
3024
+ syncDisplay();
3025
+ props.onOpen?.();
3026
+ return entry;
3027
+ }
3028
+
3029
+ function closePanel() {
3030
+ if (!entry) return;
3031
+ const toClose = entry;
3032
+ overlayLeave(toClose, () => JSswift.overlay.close(toClose.id));
3033
+ }
3034
+
3035
+ displayInput.addEventListener("focus", (event) => {
3036
+ props.onFocus?.(event);
3037
+ if (props.openOnFocus !== false) openPanel();
3038
+ });
3039
+ displayInput.addEventListener("click", (event) => {
3040
+ props.onClick?.(event);
3041
+ openPanel();
3042
+ });
3043
+ displayInput.addEventListener("keydown", (event) => {
3044
+ if (event.key === "Escape") {
3045
+ if (entry) {
3046
+ event.preventDefault();
3047
+ closePanel();
3048
+ }
3049
+ return;
3050
+ }
3051
+ if (event.key === "ArrowDown" || event.key === "Enter" || event.key === " ") {
3052
+ event.preventDefault();
3053
+ openPanel();
3054
+ }
3055
+ if ((event.key === "Backspace" || event.key === "Delete") && !uiUnwrap(props.manualInput) && uiUnwrap(props.clearable) !== false) {
3056
+ event.preventDefault();
3057
+ setTimeState(null, event, { commit: true });
3058
+ }
3059
+ });
3060
+ if (uiUnwrap(props.manualInput)) {
3061
+ displayInput.addEventListener("input", (event) => props.onTyping?.(displayInput.value, event));
3062
+ displayInput.addEventListener("change", (event) => setTimeState(displayInput.value, event));
3063
+ displayInput.addEventListener("blur", (event) => {
3064
+ props.onBlur?.(event);
3065
+ if (displayInput.value === "") setTimeState(null, event);
3066
+ else setTimeState(displayInput.value, event);
3067
+ });
3068
+ } else {
3069
+ displayInput.addEventListener("blur", (event) => props.onBlur?.(event));
3070
+ }
3071
+
3072
+ if (model) {
3073
+ model.watch((next) => {
3074
+ const normalized = uiCloneTimeParts(parseTypedValue(next));
3075
+ if (serializeValue(normalized) === serializeValue(localValue)) return;
3076
+ localValue = uiCloneTimeParts(normalized);
3077
+ workingValue = uiCloneTimeParts(normalized);
3078
+ syncDisplay();
3079
+ renderPanel();
3080
+ }, "UI.Time:watch");
3081
+ }
3082
+
3083
+ JSswift.reactive.effect(() => {
3084
+ if (!model && props.value != null) {
3085
+ localValue = uiCloneTimeParts(parseTypedValue(uiUnwrap(props.value)));
3086
+ workingValue = uiCloneTimeParts(localValue);
3087
+ }
3088
+ syncDisplay();
3089
+ renderPanel();
3090
+ if (entry && uiUnwrap(props.disabled)) closePanel();
3091
+ }, "UI.Time:render");
3092
+
3093
+ field._input = displayInput;
3094
+ field._time = displayInput;
3095
+ field._open = openPanel;
3096
+ field._close = closePanel;
3097
+ field._getValue = () => exportValue(localValue);
3098
+ field._setValue = (value) => setTimeState(value, null, { emit: false, commit: true });
3099
+ field._panel = () => entry?.panel || null;
3100
+
3101
+ return field;
3102
+ };
3103
+ if (JSswift.isDev?.()) {
3104
+ UI.meta = UI.meta || {};
3105
+ UI.meta.Time = {
3106
+ signature: "UI.Time(props)",
3107
+ props: {
3108
+ value: "string|Date",
3109
+ model: "rod | [get,set] signal",
3110
+ min: "string",
3111
+ max: "string",
3112
+ minuteStep: "number",
3113
+ secondStep: "number",
3114
+ withSeconds: "boolean",
3115
+ manualInput: "boolean",
3116
+ clearable: "boolean",
3117
+ confirm: "boolean",
3118
+ label: "String|Node|Function|Array",
3119
+ icon: "String|Node|Function|Array",
3120
+ iconRight: "String|Node|Function|Array",
3121
+ pointIcon: "String|Node|Function|Array",
3122
+ shortcuts: "Array<{ label, value }>",
3123
+ name: "string",
3124
+ class: "string",
3125
+ style: "object"
3126
+ },
3127
+ slots: {
3128
+ option: "Renderer for a time option ({ part, value, label, selected, timeValue })",
3129
+ point: "Point/icon in the selected time option",
3130
+ shortcut: "Shortcut renderer",
3131
+ value: "Footer value renderer ({ value, displayValue })",
3132
+ label: "Floating label",
3133
+ topLabel: "Top label",
3134
+ icon: "Left icon",
3135
+ iconRight: "Right icon",
3136
+ default: "Fallback value content"
3137
+ },
3138
+ events: {
3139
+ onChange: "(value, event)",
3140
+ onInput: "(value, event)",
3141
+ onOpen: "void",
3142
+ onClose: "void"
3143
+ },
3144
+ returns: "HTMLDivElement (field wrapper) con ._input, ._open(), ._close(), ._getValue(), ._setValue(value)",
3145
+ description: "Reactive time picker with fixed overlay, label/icon slots, point icon, shortcuts, confirm, and model."
3146
+ };
3147
+ }
3148
+ // Esempio: JSswift.ui.Time({ value: "09:30" })
3149
+