jsswift 2.0.0 → 2.1.0

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 (38) hide show
  1. package/AI.md +38 -73
  2. package/README.md +2 -0
  3. package/dist/build/build-configured-runtime.d.mts +29 -0
  4. package/dist/build/build-configured-runtime.mjs +92 -0
  5. package/dist/build/runtime-layout.mjs +13 -0
  6. package/dist/build/select-ui.mjs +167 -0
  7. package/dist/build/vite-jss.d.mts +2 -0
  8. package/dist/build/vite-jss.mjs +73 -0
  9. package/dist/cms.js +3522 -0
  10. package/dist/documentions/AI.md +38 -73
  11. package/dist/documentions/README.md +4 -2
  12. package/dist/documentions/components/table.md +13 -0
  13. package/dist/documentions/configured-runtime.md +82 -0
  14. package/dist/documentions/examples/crud.md +18 -7
  15. package/dist/documentions/getting-started.md +2 -0
  16. package/dist/documentions/knowledge-map.json +20 -1
  17. package/dist/documentions/ui/data-feedback-overlays.md +2 -0
  18. package/dist/documentions/ui/forms.md +2 -0
  19. package/dist/ui-src/00-bootstrap.js +330 -0
  20. package/dist/ui-src/01-foundation-helpers.js +809 -0
  21. package/dist/ui-src/10-primitives-layout.js +5091 -0
  22. package/dist/ui-src/20-display-content.js +780 -0
  23. package/dist/ui-src/21-list-content.js +246 -0
  24. package/dist/ui-src/22-banner-content.js +376 -0
  25. package/dist/ui-src/23-tooltip.js +276 -0
  26. package/dist/ui-src/31-form-advanced.js +2784 -0
  27. package/dist/ui-src/40-navigation.js +1262 -0
  28. package/dist/ui-src/41-tab-panel.js +896 -0
  29. package/dist/ui-src/50-feedback.js +999 -0
  30. package/dist/ui-src/51-feedback-service.js +166 -0
  31. package/dist/ui-src/60-shell-app.js +1223 -0
  32. package/dist/ui-src/61-form-service.js +368 -0
  33. package/dist/ui-src/70-dialog.js +333 -0
  34. package/dist/ui-src/71-menu-overlays.js +1217 -0
  35. package/dist/ui-src/80-data.js +437 -0
  36. package/dist/ui-src/modules.json +212 -0
  37. package/dist/ui.js +12565 -0
  38. package/package.json +26 -2
@@ -0,0 +1,896 @@
1
+ UI.TabPanel = (...args) => {
2
+ const { props, children } = JSswift.uiNormalizeArgs(args);
3
+ const slots = props.slots || {};
4
+ const rawTabs = Array.isArray(props.tabs)
5
+ ? props.tabs
6
+ : (Array.isArray(props.items) ? props.items : []);
7
+ const model = resolveModel(props.model, "UI.TabPanel:model");
8
+ const componentId = props.id || (`jss-tabpanel-` + Math.random().toString(36).slice(2, 9));
9
+ const normalizeOrientation = (value) => String(uiUnwrap(value) || "vertical").toLowerCase() === "horizontal" ? "horizontal" : "vertical";
10
+ const normalizePosition = (value) => String(uiUnwrap(value) || "before").toLowerCase() === "after" ? "after" : "before";
11
+ const normalizeVariant = (value) => {
12
+ const raw = String(uiUnwrap(value) || "").toLowerCase();
13
+ if (raw === "pills" || raw === "pill") return "pills";
14
+ if (raw === "soft" || raw === "card") return "soft";
15
+ return "line";
16
+ };
17
+ const resolveAccent = (value) => {
18
+ const raw = uiUnwrap(value);
19
+ if (raw == null || raw === "") return null;
20
+ return JSswift.uiColors.includes(raw) ? `var(--jss-${raw})` : String(raw);
21
+ };
22
+
23
+ const orientation = normalizeOrientation(props.orientation || props.orient || props.direction);
24
+ const navPosition = normalizePosition(props.navPosition || props.barPosition || props.position);
25
+ const variant = normalizeVariant(props.variant || (props.pills ? "pills" : (props.soft ? "soft" : "line")));
26
+ const wrapTabs = !!props.wrap;
27
+ const swipeable = !!props.swipeable;
28
+ const infinite = !!props.infinite;
29
+ const animated = !!props.animated;
30
+ const navFill = !!(props.navFill ?? props.fill ?? props.stretch);
31
+ const disabledAll = !!props.disabled;
32
+
33
+ const transitionDurationRaw = uiUnwrap(props.transitionDuration ?? props["transition-duration"]);
34
+ const transitionDuration = (() => {
35
+ if (transitionDurationRaw == null) return 220;
36
+ if (typeof transitionDurationRaw === "number") return transitionDurationRaw;
37
+ const parsed = parseFloat(transitionDurationRaw);
38
+ return Number.isFinite(parsed) ? parsed : 220;
39
+ })();
40
+ const transitionEasing = uiUnwrap(props.transitionEasing ?? props["transition-easing"]) || "ease";
41
+ const transitionPrev = props.transitionPrev ?? props["transition-prev"] ?? null;
42
+ const transitionNext = props.transitionNext ?? props["transition-next"] ?? null;
43
+ const accentColor = resolveAccent(props.color ?? props.state);
44
+
45
+ const tabs = rawTabs.map((tab, index) => {
46
+ if (tab == null) return null;
47
+ if (typeof tab !== "object") {
48
+ return {
49
+ name: tab,
50
+ labelFallback: tab,
51
+ panelFallback: null,
52
+ noteFallback: null,
53
+ iconFallback: null,
54
+ badgeFallback: null,
55
+ hidden: false,
56
+ raw: tab,
57
+ index
58
+ };
59
+ }
60
+ const name = tab.name ?? tab.value ?? tab.id ?? tab.key ?? tab.label ?? tab.title ?? `tab-${index}`;
61
+ const hasLabel = tab.label != null || tab.title != null;
62
+ const labelFallback = hasLabel
63
+ ? (tab.label ?? tab.title)
64
+ : (tab.children != null && tab.content == null && tab.panel == null ? tab.children : (tab.name ?? tab.value ?? `Tab ${index + 1}`));
65
+ const panelFallback = tab.content ?? tab.panel ?? tab.body ?? (hasLabel ? tab.children : null);
66
+ const noteFallback = tab.note ?? tab.subtitle ?? tab.description ?? null;
67
+ const iconFallback = tab.icon ?? null;
68
+ const badgeFallback = Object.prototype.hasOwnProperty.call(tab, "badge")
69
+ ? tab.badge
70
+ : (Object.prototype.hasOwnProperty.call(tab, "counter") ? tab.counter : null);
71
+ return {
72
+ ...tab,
73
+ name,
74
+ labelFallback,
75
+ panelFallback,
76
+ noteFallback,
77
+ iconFallback,
78
+ badgeFallback,
79
+ index
80
+ };
81
+ }).filter(Boolean).filter((tab) => tab.hidden !== true && tab.visible !== false);
82
+
83
+ const cls = uiClass([
84
+ "jss-clear-set",
85
+ "jss-tabpanel",
86
+ "jss-singularity",
87
+ orientation,
88
+ `variant-${variant}`,
89
+ uiWhen(wrapTabs, "wrap"),
90
+ uiWhen(animated, "animated"),
91
+ uiWhen(navFill, "nav-fill"),
92
+ uiWhen(disabledAll, "disabled"),
93
+ uiWhen(navPosition === "after", "nav-after"),
94
+ props.class
95
+ ]);
96
+ const wrapProps = JSswift.omit(props, [
97
+ "tabs", "items", "value", "default", "defaultValue", "model",
98
+ "orientation", "orient", "direction",
99
+ "navPosition", "barPosition", "position",
100
+ "variant", "pills", "soft",
101
+ "navFill", "fill", "stretch",
102
+ "wrap", "swipeable", "infinite", "animated",
103
+ "transitionDuration", "transition-duration",
104
+ "transitionEasing", "transition-easing",
105
+ "transitionPrev", "transition-prev",
106
+ "transitionNext", "transition-next",
107
+ "slots", "empty",
108
+ "tabClass", "tabStyle",
109
+ "panelClass", "panelStyle",
110
+ "navClass", "panelsClass",
111
+ "class", "style"
112
+ ]);
113
+ wrapProps.class = cls;
114
+ wrapProps.style = props.style;
115
+ const wrap = _.div(wrapProps);
116
+
117
+ if (animated) {
118
+ wrap.style.setProperty("--jss-tabpanel-duration", `${transitionDuration}ms`);
119
+ wrap.style.setProperty("--jss-tabpanel-easing", transitionEasing);
120
+ }
121
+ if (accentColor) {
122
+ wrap.style.setProperty("--jss-tabpanel-accent", accentColor);
123
+ }
124
+
125
+ const nav = _.div({
126
+ class: uiClass(["jss-tabpanel-nav", props.navClass]),
127
+ role: "tablist",
128
+ "aria-orientation": orientation
129
+ });
130
+ const panelsWrap = _.div({ class: uiClass(["jss-tabpanel-panels", props.panelsClass]) });
131
+
132
+ const tabButtons = [];
133
+ const panelNodes = [];
134
+ let activeIndex = -1;
135
+
136
+ const isHorizontal = () => orientation === "horizontal";
137
+ const createCtx = (tab, index, active) => ({
138
+ tab,
139
+ name: tab.name,
140
+ index,
141
+ active,
142
+ value: tab.name,
143
+ select: () => setActiveByIndex(index),
144
+ next: () => goNext(),
145
+ prev: () => goPrev()
146
+ });
147
+
148
+ const findEnabledIndex = (start, step, allowWrap = false) => {
149
+ if (!tabs.length) return -1;
150
+ let idx = start;
151
+ for (let i = 0; i < tabs.length; i += 1) {
152
+ idx += step;
153
+ if (idx >= tabs.length) {
154
+ if (!allowWrap) return -1;
155
+ idx = 0;
156
+ } else if (idx < 0) {
157
+ if (!allowWrap) return -1;
158
+ idx = tabs.length - 1;
159
+ }
160
+ if (!tabs[idx]?.disabled) return idx;
161
+ }
162
+ return -1;
163
+ };
164
+
165
+ const focusIndex = (index) => {
166
+ const btn = tabButtons[index]?.btn;
167
+ if (!btn || typeof btn.focus !== "function") return;
168
+ requestAnimationFrame(() => btn.focus());
169
+ };
170
+
171
+ const makeLabelNodes = (tab, index, isActive) => {
172
+ const ctx = createCtx(tab, index, isActive);
173
+ const labelNode = JSswift.ui.renderSlot(slots, "label", ctx, tab.labelFallback);
174
+ return renderSlotToArray(null, "default", {}, labelNode);
175
+ };
176
+
177
+ const makeTabNode = (tab, index) => {
178
+ const isActive = index === activeIndex;
179
+ const tabId = `${componentId}-tab-${index}`;
180
+ const panelId = `${componentId}-panel-${index}`;
181
+ const ctx = createCtx(tab, index, isActive);
182
+ const labelNodes = makeLabelNodes(tab, index, isActive);
183
+ const iconFallback = tab.iconFallback != null
184
+ ? (typeof tab.iconFallback === "string"
185
+ ? UI.Icon({ name: tab.iconFallback, size: tab.iconSize ?? tab.size ?? props.size ?? null })
186
+ : tab.iconFallback)
187
+ : null;
188
+ const iconNodes = renderSlotToArray(slots, "icon", ctx, iconFallback);
189
+ const noteNodes = renderSlotToArray(slots, "note", ctx, tab.noteFallback);
190
+ const badgeNodes = renderSlotToArray(slots, "badge", ctx, tab.badgeFallback);
191
+ const fallbackTabContent = _.span({ class: "jss-tabpanel-tab-inner" },
192
+ iconNodes.length ? _.span({ class: "jss-tabpanel-tab-icon" }, ...iconNodes) : null,
193
+ _.span({ class: "jss-tabpanel-tab-copy" },
194
+ _.span({ class: "jss-tabpanel-tab-label" }, ...(labelNodes.length ? labelNodes : [""])),
195
+ noteNodes.length ? _.span({ class: "jss-tabpanel-tab-note" }, ...noteNodes) : null
196
+ ),
197
+ badgeNodes.length ? _.span({ class: "jss-tabpanel-tab-badge" }, ...badgeNodes) : null
198
+ );
199
+ const contentNodes = renderSlotToArray(slots, "tab", {
200
+ ...ctx,
201
+ label: labelNodes,
202
+ icon: iconNodes,
203
+ note: noteNodes,
204
+ badge: badgeNodes,
205
+ tabId,
206
+ panelId
207
+ }, fallbackTabContent);
208
+ const onKeydown = (e) => {
209
+ if (disabledAll || tab.disabled) return;
210
+ const key = e.key;
211
+ if (key === "Enter" || key === " ") {
212
+ e.preventDefault();
213
+ setActiveByIndex(index);
214
+ return;
215
+ }
216
+ if (key === "Home") {
217
+ e.preventDefault();
218
+ const first = tabs.findIndex((item) => !item.disabled);
219
+ if (first >= 0) {
220
+ setActiveByIndex(first);
221
+ focusIndex(first);
222
+ }
223
+ return;
224
+ }
225
+ if (key === "End") {
226
+ e.preventDefault();
227
+ const last = (() => {
228
+ for (let i = tabs.length - 1; i >= 0; i -= 1) {
229
+ if (!tabs[i]?.disabled) return i;
230
+ }
231
+ return -1;
232
+ })();
233
+ if (last >= 0) {
234
+ setActiveByIndex(last);
235
+ focusIndex(last);
236
+ }
237
+ return;
238
+ }
239
+ const prevKeys = isHorizontal() ? ["ArrowLeft"] : ["ArrowUp"];
240
+ const nextKeys = isHorizontal() ? ["ArrowRight"] : ["ArrowDown"];
241
+ if (prevKeys.includes(key)) {
242
+ e.preventDefault();
243
+ const prev = findEnabledIndex(index, -1, infinite);
244
+ if (prev >= 0) {
245
+ setActiveByIndex(prev, { dir: "prev" });
246
+ focusIndex(prev);
247
+ }
248
+ return;
249
+ }
250
+ if (nextKeys.includes(key)) {
251
+ e.preventDefault();
252
+ const next = findEnabledIndex(index, 1, infinite);
253
+ if (next >= 0) {
254
+ setActiveByIndex(next, { dir: "next" });
255
+ focusIndex(next);
256
+ }
257
+ }
258
+ };
259
+ const defaultBtn = UI.Btn({
260
+ class: uiClass([
261
+ "jss-tabpanel-tab",
262
+ uiWhen(isActive, "active"),
263
+ uiWhen(tab.disabled || disabledAll, "disabled"),
264
+ props.tabClass,
265
+ tab.tabClass || tab.class
266
+ ]),
267
+ type: "button",
268
+ id: tabId,
269
+ role: "tab",
270
+ disabled: !!(tab.disabled || disabledAll),
271
+ "aria-selected": isActive ? "true" : "false",
272
+ "aria-controls": panelId,
273
+ tabindex: isActive ? "0" : "-1",
274
+ outline: variant === "pills" && !isActive,
275
+ color: variant === "pills" && isActive ? (tab.color || props.color || props.state || "primary") : null,
276
+ size: tab.size ?? props.size ?? null,
277
+ style: {
278
+ ...(props.tabStyle || {}),
279
+ ...(tab.tabStyle || {})
280
+ },
281
+ onClick: () => {
282
+ if (tab.disabled || disabledAll) return;
283
+ setActiveByIndex(index);
284
+ },
285
+ onKeydown
286
+ }, ...contentNodes);
287
+ const navBtn = _.div({
288
+ class: uiClass([
289
+ "jss-tabpanel-nav-btn",
290
+ uiWhen(isActive, "active"),
291
+ tab.navClass
292
+ ]),
293
+ "data-name": tab.name
294
+ }, defaultBtn, _.span({ class: "tab-indicator" }));
295
+ tabButtons[index] = { btn: defaultBtn, wrap: navBtn, index };
296
+ return navBtn;
297
+ };
298
+
299
+ const makePanelNode = (tab, index) => {
300
+ const isActive = index === activeIndex;
301
+ const tabId = `${componentId}-tab-${index}`;
302
+ const panelId = `${componentId}-panel-${index}`;
303
+ const ctx = createCtx(tab, index, isActive);
304
+ const panelNode = JSswift.ui.renderSlot(slots, "panel", ctx, tab.panelFallback);
305
+ const panel = _.div({
306
+ class: uiClass([
307
+ "jss-tabpanel-panel",
308
+ uiWhen(isActive, "active"),
309
+ props.panelClass,
310
+ tab.panelClass
311
+ ]),
312
+ style: {
313
+ ...(props.panelStyle || {}),
314
+ ...(tab.panelStyle || {})
315
+ },
316
+ "data-name": tab.name,
317
+ role: "tabpanel",
318
+ id: panelId,
319
+ tabindex: 0,
320
+ "aria-labelledby": tabId,
321
+ "aria-hidden": isActive ? "false" : "true"
322
+ }, ...renderSlotToArray(null, "default", {}, panelNode));
323
+ panelNodes[index] = panel;
324
+ return panel;
325
+ };
326
+
327
+ const defaultNavNodes = tabs.map((tab, index) => makeTabNode(tab, index));
328
+ const navContent = JSswift.ui.renderSlot(slots, "nav", {
329
+ tabs,
330
+ active: () => (activeIndex >= 0 ? tabs[activeIndex]?.name : null),
331
+ activeIndex: () => activeIndex,
332
+ activeTab: () => (activeIndex >= 0 ? tabs[activeIndex] : null),
333
+ select: (nameOrIndex) => {
334
+ if (typeof nameOrIndex === "number") setActiveByIndex(nameOrIndex);
335
+ else setActiveByValue(nameOrIndex);
336
+ },
337
+ next: () => goNext(),
338
+ prev: () => goPrev(),
339
+ nodes: defaultNavNodes,
340
+ orientation,
341
+ position: navPosition,
342
+ variant
343
+ }, defaultNavNodes);
344
+
345
+ renderSlotToArray(null, "default", {}, navContent).forEach(n => nav.appendChild(n));
346
+ if (tabs.length) {
347
+ tabs.forEach((tab, index) => panelsWrap.appendChild(makePanelNode(tab, index)));
348
+ } else {
349
+ const emptyNodes = renderSlotToArray(slots, "empty", { tabs }, props.empty ?? _.div({ class: "jss-tabpanel-empty" }, "Nessun contenuto disponibile."));
350
+ emptyNodes.forEach((node) => panelsWrap.appendChild(node));
351
+ }
352
+
353
+ const setDirectionVars = (dir) => {
354
+ if (!animated) return;
355
+ const enter = dir === "next" ? "18px" : "-18px";
356
+ const leave = dir === "next" ? "-18px" : "18px";
357
+ wrap.style.setProperty("--jss-tabpanel-enter", enter);
358
+ wrap.style.setProperty("--jss-tabpanel-leave", leave);
359
+ };
360
+
361
+ const cleanCustomTransitions = () => {
362
+ const classes = [];
363
+ if (transitionPrev) classes.push(...String(transitionPrev).split(/\s+/));
364
+ if (transitionNext) classes.push(...String(transitionNext).split(/\s+/));
365
+ if (classes.length) panelsWrap.classList.remove(...classes.filter(Boolean));
366
+ };
367
+
368
+ const applyCustomTransition = (dir) => {
369
+ cleanCustomTransitions();
370
+ const cls = dir === "prev" ? transitionPrev : transitionNext;
371
+ if (!cls) return;
372
+ const parts = String(cls).split(/\s+/).filter(Boolean);
373
+ if (!parts.length) return;
374
+ panelsWrap.classList.add(...parts);
375
+ setTimeout(() => panelsWrap.classList.remove(...parts), transitionDuration + 40);
376
+ };
377
+
378
+ const updateNavButtons = (nextIndex) => {
379
+ const active = tabButtons.find(({ btn }) => btn.classList.contains("active"));
380
+ const rectPrev = active?.btn.parentNode?.getBoundingClientRect();
381
+ tabButtons.forEach(({ btn, index }) => {
382
+ const isActive = index === nextIndex;
383
+ const parent = btn.parentNode;
384
+ if (!parent) return;
385
+ const rect = parent.getBoundingClientRect();
386
+ if (parent.isConnected && rectPrev) {
387
+ parent.style.setProperty("--nav-pos-x", (rectPrev.left - rect.left) + "px");
388
+ parent.style.setProperty("--nav-pos-y", (rectPrev.top - rect.top) + "px");
389
+ }
390
+ parent.classList.toggle("active", isActive);
391
+ btn.classList.toggle("active", isActive);
392
+ btn.setAttribute("aria-selected", isActive ? "true" : "false");
393
+ btn.setAttribute("tabindex", isActive ? "0" : "-1");
394
+ });
395
+ };
396
+
397
+ const updatePanels = (prevIndex, nextIndex, dir) => {
398
+ setDirectionVars(dir);
399
+ panelNodes.forEach((panel, index) => {
400
+ if (!panel) return;
401
+ const isNext = index === nextIndex;
402
+ const isPrev = index === prevIndex;
403
+ if (isNext) {
404
+ panel.classList.add("active");
405
+ panel.classList.remove("leaving");
406
+ panel.setAttribute("aria-hidden", "false");
407
+ } else {
408
+ panel.classList.remove("active");
409
+ panel.setAttribute("aria-hidden", "true");
410
+ if (animated && isPrev) {
411
+ panel.classList.add("leaving");
412
+ setTimeout(() => panel.classList.remove("leaving"), transitionDuration + 40);
413
+ } else {
414
+ panel.classList.remove("leaving");
415
+ }
416
+ }
417
+ });
418
+ };
419
+
420
+ const computeDir = (prevIndex, nextIndex) => {
421
+ if (prevIndex < 0 || prevIndex === nextIndex) return "next";
422
+ if (infinite && prevIndex === tabs.length - 1 && nextIndex === 0) return "next";
423
+ if (infinite && prevIndex === 0 && nextIndex === tabs.length - 1) return "prev";
424
+ return nextIndex > prevIndex ? "next" : "prev";
425
+ };
426
+
427
+ const setActiveByIndex = (nextIndex, opts = {}) => {
428
+ if (nextIndex == null || nextIndex < 0 || nextIndex >= tabs.length) return;
429
+ if (tabs[nextIndex]?.disabled) return;
430
+ const prevIndex = activeIndex;
431
+ if (prevIndex === nextIndex) return;
432
+ activeIndex = nextIndex;
433
+ const nextTab = tabs[nextIndex];
434
+ const dir = opts.dir || computeDir(prevIndex, nextIndex);
435
+ if (model && !opts.fromModel) model.set(nextTab.name);
436
+ updateNavButtons(nextIndex);
437
+ updatePanels(prevIndex, nextIndex, dir);
438
+ applyCustomTransition(dir);
439
+ wrap.dataset.active = String(nextTab.name);
440
+ props.onChange?.(nextTab.name, nextTab, nextIndex);
441
+ };
442
+
443
+ const setActiveByValue = (value, opts = {}) => {
444
+ if (!tabs.length) return;
445
+ const idx = tabs.findIndex(t => t.name == value);
446
+ if (idx === -1) return;
447
+ setActiveByIndex(idx, opts);
448
+ };
449
+
450
+ const goNext = () => {
451
+ if (!tabs.length) return;
452
+ const nextIndex = activeIndex < 0
453
+ ? tabs.findIndex((tab) => !tab.disabled)
454
+ : findEnabledIndex(activeIndex, 1, infinite);
455
+ if (nextIndex < 0) return;
456
+ setActiveByIndex(nextIndex, { dir: "next" });
457
+ };
458
+
459
+ const goPrev = () => {
460
+ if (!tabs.length) return;
461
+ const nextIndex = activeIndex < 0
462
+ ? (() => {
463
+ for (let i = tabs.length - 1; i >= 0; i -= 1) {
464
+ if (!tabs[i]?.disabled) return i;
465
+ }
466
+ return -1;
467
+ })()
468
+ : findEnabledIndex(activeIndex, -1, infinite);
469
+ if (nextIndex < 0) return;
470
+ setActiveByIndex(nextIndex, { dir: "prev" });
471
+ };
472
+
473
+ if (swipeable) {
474
+ let startX = 0;
475
+ let startY = 0;
476
+ let tracking = false;
477
+ const threshold = 42;
478
+ panelsWrap.addEventListener("pointerdown", (e) => {
479
+ if (e.button != null && e.button !== 0) return;
480
+ tracking = true;
481
+ startX = e.clientX;
482
+ startY = e.clientY;
483
+ });
484
+ panelsWrap.addEventListener("pointerup", (e) => {
485
+ if (!tracking) return;
486
+ tracking = false;
487
+ const dx = e.clientX - startX;
488
+ const dy = e.clientY - startY;
489
+ if (Math.abs(dx) < threshold || Math.abs(dx) < Math.abs(dy)) return;
490
+ if (dx < 0) goNext();
491
+ else goPrev();
492
+ });
493
+ panelsWrap.addEventListener("pointercancel", () => { tracking = false; });
494
+ panelsWrap.addEventListener("pointerleave", () => { tracking = false; });
495
+ }
496
+
497
+ const initialValue = model ? model.get() : (props.value ?? props.defaultValue ?? props.default ?? null);
498
+ if (tabs.length) {
499
+ const initialIndex = tabs.findIndex(t => t.name == initialValue);
500
+ if (initialIndex >= 0) {
501
+ setActiveByIndex(initialIndex, { fromModel: true });
502
+ } else {
503
+ const firstEnabled = tabs.findIndex((tab) => !tab.disabled);
504
+ if (firstEnabled >= 0) {
505
+ setActiveByIndex(firstEnabled, { fromModel: true });
506
+ if (model) model.set(tabs[firstEnabled].name);
507
+ }
508
+ }
509
+ }
510
+
511
+ if (model) {
512
+ model.watch((v) => setActiveByValue(v, { fromModel: true }), "UI.TabPanel:watch");
513
+ }
514
+
515
+ if (navPosition === "after") {
516
+ wrap.appendChild(panelsWrap);
517
+ wrap.appendChild(nav);
518
+ } else {
519
+ wrap.appendChild(nav);
520
+ wrap.appendChild(panelsWrap);
521
+ }
522
+
523
+ const extra = renderSlotToArray(slots, "default", {}, children);
524
+ extra.forEach((n) => wrap.appendChild(n));
525
+ setPropertyProps(wrap, props);
526
+ wrap._tabs = () => tabs.slice();
527
+ wrap._active = () => (activeIndex >= 0 ? tabs[activeIndex] : null);
528
+ wrap._getValue = () => (activeIndex >= 0 ? tabs[activeIndex]?.name : null);
529
+ wrap._setValue = (value) => setActiveByValue(value);
530
+ wrap._select = (value) => {
531
+ if (typeof value === "number") setActiveByIndex(value);
532
+ else setActiveByValue(value);
533
+ };
534
+ wrap._next = goNext;
535
+ wrap._prev = goPrev;
536
+ return wrap;
537
+ };
538
+
539
+
540
+ function closeBackdrop(backdrop) {
541
+ if (backdrop && backdrop.parentNode) backdrop.parentNode.removeChild(backdrop);
542
+ }
543
+
544
+ function renderDialog({ title, message, contentNode, okText, cancelText, showCancel, onOk, onCancel, persistent }) {
545
+ ensureModalRoot();
546
+
547
+ const backdrop = _.div({
548
+ class: "jss-dialog-backdrop", onClick: (e) => {
549
+ if (e.target === backdrop && persistent !== true) onCancel?.();
550
+ }
551
+ });
552
+
553
+ const titleNodes = renderSlotToArray(null, "default", {}, title);
554
+ const messageNodes = renderSlotToArray(null, "default", {}, message);
555
+ const contentNodes = renderSlotToArray(null, "default", {}, contentNode);
556
+
557
+ const dialog = _.div({ class: "jss-dialog jss-panel" },
558
+ titleNodes.length ? _.h3(...titleNodes) : null,
559
+ messageNodes.length ? _.p(...messageNodes) : null,
560
+ ...contentNodes,
561
+ _.div({ class: "jss-dialog-actions" },
562
+ showCancel ? JSswift.ui.Btn({ onClick: onCancel }, cancelText || "Annulla") : null,
563
+ JSswift.ui.Btn({ color: "primary", onClick: onOk }, okText || "OK")
564
+ )
565
+ );
566
+
567
+ backdrop.appendChild(dialog);
568
+ ensureModalRoot().appendChild(backdrop);
569
+
570
+ // escape to close
571
+ const onKey = (e) => {
572
+ if (e.key === "Escape") onCancel?.();
573
+ };
574
+ document.addEventListener("keydown", onKey, { once: true });
575
+
576
+ return { backdrop, close: () => closeBackdrop(backdrop) };
577
+ }
578
+
579
+ app.dialog.alert = function (message, opts = {}) {
580
+ return new Promise((resolve) => {
581
+ const { backdrop, close } = renderDialog({
582
+ title: opts.title || "Info",
583
+ message,
584
+ okText: opts.okText || "OK",
585
+ showCancel: false,
586
+ persistent: opts.persistent,
587
+ onOk: () => { close(); resolve(true); },
588
+ onCancel: () => { close(); resolve(true); }
589
+ });
590
+ });
591
+ };
592
+
593
+ app.dialog.confirm = function (opts = {}) {
594
+ return new Promise((resolve) => {
595
+ const { close } = renderDialog({
596
+ title: opts.title || "Conferma",
597
+ message: opts.message || "",
598
+ okText: opts.okText || "Conferma",
599
+ cancelText: opts.cancelText || "Annulla",
600
+ showCancel: true,
601
+ persistent: opts.persistent,
602
+ onOk: () => { close(); resolve(true); },
603
+ onCancel: () => { close(); resolve(false); }
604
+ });
605
+ });
606
+ };
607
+
608
+ app.dialog.prompt = function (opts = {}) {
609
+ return new Promise((resolve) => {
610
+ const input = JSswift.ui.Input({
611
+ class: "jss-dialog-input",
612
+ type: opts.type || "text",
613
+ placeholder: opts.placeholder || ""
614
+ });
615
+
616
+ // default value
617
+ if (opts.value != null) input.value = String(opts.value);
618
+
619
+ const { close } = renderDialog({
620
+ title: opts.title || "Inserisci valore",
621
+ message: opts.message || "",
622
+ okText: opts.okText || "OK",
623
+ cancelText: opts.cancelText || "Annulla",
624
+ showCancel: true,
625
+ contentNode: _.div(input),
626
+ persistent: opts.persistent,
627
+ onOk: () => { const v = input.value; close(); resolve(v); },
628
+ onCancel: () => { close(); resolve(null); }
629
+ });
630
+
631
+ // focus
632
+ setTimeout(() => input.focus(), 0);
633
+ });
634
+ };
635
+
636
+ // ===============================
637
+ // Loading Service
638
+ // ===============================
639
+
640
+ app.services = app.services || {};
641
+ app.services.loading = app.services.loading || {};
642
+ app.loading = app.services.loading;
643
+
644
+ let overlay = null;
645
+ let countLoading = 0;
646
+
647
+ function ensureOverlay() {
648
+ if (overlay) return overlay;
649
+
650
+ overlay = document.createElement("div");
651
+ overlay.className = "jss-loading-backdrop";
652
+ overlay.style.cssText = `
653
+ position: fixed; inset: 0;
654
+ background: rgba(0,0,0,0.45);
655
+ display: none;
656
+ align-items: center; justify-content: center;
657
+ z-index: 10001;
658
+ padding: 14px;
659
+ `;
660
+ const box = document.createElement("div");
661
+ box.className = "jss-panel jss-loading-card";
662
+ box.style.cssText = `
663
+ padding: 14px 16px;
664
+ border-radius: var(--jss-r-lg);
665
+ min-width: 260px;
666
+ display:flex; gap: 12px; align-items:center;
667
+ flex-direction: column;
668
+ `;
669
+
670
+ const spinner = document.createElement("div");
671
+ spinner.className = "jss-loading-spinner";
672
+ spinner.style.cssText = `
673
+ width: 18px; height: 18px;
674
+ border-radius: 50%;
675
+ border: 2px solid rgba(255,255,255,0.25);
676
+ border-top-color: var(--jss-primary);
677
+ animation: jssSpin 0.9s linear infinite;
678
+ `;
679
+
680
+ const text = document.createElement("div");
681
+ text.className = "jss-muted jss-loading-text";
682
+ text.textContent = "Loading...";
683
+
684
+ const progressWrap = document.createElement("div");
685
+ progressWrap.className = "jss-loading-progress";
686
+ progressWrap.style.cssText = `
687
+ width: 100%;
688
+ height: 6px;
689
+ border-radius: 999px;
690
+ overflow: hidden;
691
+ background: rgba(255,255,255,0.08);
692
+ display: none;
693
+ `;
694
+ const progressBar = document.createElement("div");
695
+ progressBar.className = "jss-loading-progress-bar";
696
+ progressBar.style.cssText = `
697
+ height: 100%;
698
+ width: 0%;
699
+ background: var(--jss-primary);
700
+ transition: width 200ms ease;
701
+ `;
702
+ progressWrap.appendChild(progressBar);
703
+
704
+ const row = document.createElement("div");
705
+ row.style.cssText = "display:flex; gap: 12px; align-items:center; width: 100%;";
706
+ row.appendChild(spinner);
707
+ row.appendChild(text);
708
+
709
+ box.appendChild(row);
710
+ box.appendChild(progressWrap);
711
+ overlay.appendChild(box);
712
+ document.body.appendChild(overlay);
713
+
714
+ const style = document.createElement("style");
715
+ style.textContent = `
716
+ @keyframes jssSpin { to { transform: rotate(360deg); } }
717
+ `;
718
+ document.head.appendChild(style);
719
+
720
+ overlay._textNode = text;
721
+ overlay._spinner = spinner;
722
+ overlay._progressWrap = progressWrap;
723
+ overlay._progressBar = progressBar;
724
+ overlay._progressActive = false;
725
+ return overlay;
726
+ }
727
+
728
+ function normalizeLoadingOptions(message, opts) {
729
+ if (typeof message === "object" && message !== null) return { ...message };
730
+ const base = (opts && typeof opts === "object") ? opts : {};
731
+ if (typeof opts === "string" && (message == null || typeof message === "number")) {
732
+ return { ...base, message: opts };
733
+ }
734
+ return { ...base, message: message };
735
+ }
736
+
737
+ function setLoadingMode(ov, mode) {
738
+ const showProgress = mode === "progress";
739
+ ov._spinner.style.display = showProgress ? "none" : "";
740
+ ov._progressWrap.style.display = showProgress ? "block" : "none";
741
+ ov._mode = mode;
742
+ }
743
+
744
+ app.loading.show = function (message = "Loading...", opts = {}) {
745
+ const ov = ensureOverlay();
746
+ const o = normalizeLoadingOptions(message, opts);
747
+ countLoading++;
748
+ ov._progressActive = false;
749
+ ov._textNode.textContent = o.message || "Loading...";
750
+ setLoadingMode(ov, "spinner");
751
+ if (o.spinner === false) ov._spinner.style.display = "none";
752
+ ov.style.display = "flex";
753
+ };
754
+
755
+ app.loading.hide = function () {
756
+ if (!overlay) return;
757
+ countLoading = Math.max(0, countLoading - 1);
758
+ if (countLoading === 0) {
759
+ overlay.style.display = "none";
760
+ overlay._progressActive = false;
761
+ }
762
+ };
763
+
764
+ app.loading.progress = function (value = 0, opts = {}) {
765
+ const ov = ensureOverlay();
766
+ if (typeof value === "object" && value !== null) {
767
+ opts = value;
768
+ value = value.value ?? 0;
769
+ }
770
+ const o = normalizeLoadingOptions(null, opts);
771
+ if (!ov._progressActive) {
772
+ countLoading++;
773
+ ov._progressActive = true;
774
+ }
775
+ const v = Math.max(0, Math.min(100, Number(value ?? 0)));
776
+ ov._textNode.textContent = o.message || "Loading...";
777
+ ov._progressBar.style.width = v + "%";
778
+ setLoadingMode(ov, "progress");
779
+ ov.style.display = "flex";
780
+ };
781
+
782
+ // helper: wrapper async
783
+ app.loading.wrap = async function (message, fn) {
784
+ app.loading.show(message);
785
+ try { return await fn(); }
786
+ finally { app.loading.hide(); }
787
+ };
788
+
789
+ // ===============================
790
+ // UI.Table
791
+ // ===============================
792
+ function toValue(rows) {
793
+ if (typeof rows === "function") return rows();
794
+ return rows || [];
795
+ }
796
+
797
+ function tableGetByPath(obj, path) {
798
+ if (obj == null || path == null) return undefined;
799
+ if (typeof path !== "string" || path.indexOf(".") < 0) return obj?.[path];
800
+ return path.split(".").reduce((acc, key) => acc == null ? acc : acc[key], obj);
801
+ }
802
+
803
+ function defaultCompare(a, b) {
804
+ if (a == null && b == null) return 0;
805
+ if (a == null) return -1;
806
+ if (b == null) return 1;
807
+ if (typeof a === "number" && typeof b === "number") return a - b;
808
+ return String(a).localeCompare(String(b), undefined, { numeric: true, sensitivity: "base" });
809
+ }
810
+
811
+ function tableToArray(value) {
812
+ const rows = toValue(value);
813
+ return Array.isArray(rows) ? rows : [];
814
+ }
815
+
816
+ function tableColumnLabel(col) {
817
+ return col?.label ?? col?.title ?? col?.header ?? col?.key ?? "";
818
+ }
819
+
820
+ function tableColumnSortKey(col, index) {
821
+ return col?.sortKey ?? col?.key ?? `__col_${index}`;
822
+ }
823
+
824
+ function tableFindColumn(columns, key) {
825
+ return columns.find((col, index) => tableColumnSortKey(col, index) === key || col?.key === key) || null;
826
+ }
827
+
828
+ function tableResolveValue(col, row, rowIndex) {
829
+ if (!col) return row;
830
+ if (typeof col.get === "function") return col.get(row, { row, rowIndex, col });
831
+ if (typeof col.value === "function") return col.value(row, { row, rowIndex, col });
832
+ if (typeof col.key === "string") return tableGetByPath(row, col.key);
833
+ return col.key != null ? row?.[col.key] : row;
834
+ }
835
+
836
+ function tableResolveStyle(style, ctx) {
837
+ if (typeof style === "function") return style(ctx) || {};
838
+ return style || {};
839
+ }
840
+
841
+ function tableTextValue(value) {
842
+ if (value == null) return "";
843
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return String(value);
844
+ if (Array.isArray(value)) return value.map(tableTextValue).filter(Boolean).join(" ");
845
+ if (typeof Node !== "undefined" && value instanceof Node) return value.textContent || "";
846
+ if (typeof value === "object") {
847
+ if (typeof value.textContent === "string") return value.textContent;
848
+ try {
849
+ return JSON.stringify(value);
850
+ } catch {
851
+ return "";
852
+ }
853
+ }
854
+ return String(value);
855
+ }
856
+
857
+ function tableNormalizePageSizes(options, fallback) {
858
+ const list = Array.isArray(options) ? options : fallback;
859
+ const normalized = list
860
+ .map((item) => Number(item))
861
+ .filter((item) => Number.isFinite(item) && item > 0);
862
+ return normalized.length ? Array.from(new Set(normalized)) : fallback;
863
+ }
864
+
865
+ function tableMatchesQuery(row, columns, query, props) {
866
+ const predicate = props.searchBy || props.searchPredicate;
867
+ if (typeof predicate === "function") return !!predicate(row, { query, columns });
868
+
869
+ const terms = String(query || "")
870
+ .trim()
871
+ .toLowerCase()
872
+ .split(/\s+/)
873
+ .filter(Boolean);
874
+ if (!terms.length) return true;
875
+
876
+ const searchKeys = Array.isArray(props.searchKeys) && props.searchKeys.length
877
+ ? props.searchKeys
878
+ : columns.filter((col) => col?.searchable !== false).map((col) => col?.searchKey ?? col?.key).filter(Boolean);
879
+
880
+ const tokens = [];
881
+ if (searchKeys.length) {
882
+ for (const key of searchKeys) {
883
+ if (typeof key === "function") tokens.push(tableTextValue(key(row)));
884
+ else {
885
+ const col = columns.find((item) => item?.key === key || item?.searchKey === key);
886
+ tokens.push(tableTextValue(col ? tableResolveValue(col, row, -1) : tableGetByPath(row, key)));
887
+ }
888
+ }
889
+ } else {
890
+ tokens.push(tableTextValue(row));
891
+ }
892
+
893
+ const haystack = tokens.join(" ").toLowerCase();
894
+ return terms.every((term) => haystack.includes(term));
895
+ }
896
+