@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,1454 @@
1
+ UI.Tabs = (...args) => {
2
+ const { props, children } = JSswift.uiNormalizeArgs(args);
3
+ const slots = props.slots || {};
4
+ const hasOwn = (obj, key) => !!obj && Object.prototype.hasOwnProperty.call(obj, key);
5
+ const normalizeOrientation = (value) => String(uiUnwrap(value) || "horizontal").toLowerCase() === "vertical" ? "vertical" : "horizontal";
6
+ const normalizeVariant = (value) => {
7
+ const raw = String(uiUnwrap(value) || "").toLowerCase();
8
+ if (raw === "pills" || raw === "pill") return "pills";
9
+ if (raw === "soft" || raw === "card") return "soft";
10
+ return "line";
11
+ };
12
+ const resolveAccent = (value) => {
13
+ const raw = uiUnwrap(value);
14
+ if (raw == null || raw === "") return null;
15
+ return JSswift.uiColors?.includes(raw) ? `var(--cms-${raw})` : String(raw);
16
+ };
17
+ const isSameValue = (a, b) => Object.is(a, b) || (a != null && b != null && a == b);
18
+
19
+ const model = resolveModel(props.model, "UI.Tabs:model");
20
+ const orientation = normalizeOrientation(props.orientation || props.orient || props.direction);
21
+ const variant = normalizeVariant(props.variant || (props.pills ? "pills" : (props.soft ? "soft" : "line")));
22
+ const fillTabs = !!uiUnwrap(props.fill ?? props.navFill ?? props.stretch);
23
+ const wrapTabs = !!uiUnwrap(props.wrap);
24
+ const disabledAll = !!uiUnwrap(props.disabled);
25
+ const componentId = props.id || (`cms-tabs-` + Math.random().toString(36).slice(2, 9));
26
+ const rawTabs = uiUnwrap(props.tabs ?? props.items) || [];
27
+ const tabs = (Array.isArray(rawTabs) ? rawTabs : [rawTabs])
28
+ .map((tab, index) => {
29
+ if (tab == null) return null;
30
+ if (typeof tab !== "object") {
31
+ return {
32
+ raw: tab,
33
+ index,
34
+ value: tab,
35
+ labelFallback: tab,
36
+ noteFallback: null,
37
+ iconFallback: null,
38
+ badgeFallback: null,
39
+ disabled: false,
40
+ hidden: false
41
+ };
42
+ }
43
+ const value = tab.value ?? tab.name ?? tab.id ?? tab.key ?? tab.label ?? tab.title ?? `tab-${index}`;
44
+ const labelFallback = tab.label ?? tab.title ?? tab.name ?? tab.value ?? `Tab ${index + 1}`;
45
+ const badgeFallback = hasOwn(tab, "badge")
46
+ ? tab.badge
47
+ : (hasOwn(tab, "counter") ? tab.counter : null);
48
+ return {
49
+ ...tab,
50
+ raw: tab,
51
+ index,
52
+ value,
53
+ labelFallback,
54
+ noteFallback: tab.note ?? tab.subtitle ?? tab.description ?? null,
55
+ iconFallback: tab.icon ?? null,
56
+ badgeFallback
57
+ };
58
+ })
59
+ .filter(Boolean)
60
+ .filter((tab) => tab.hidden !== true && tab.visible !== false);
61
+
62
+ const firstEnabled = tabs.find((tab) => !tab.disabled) || null;
63
+ const findTabIndex = (value) => tabs.findIndex((tab) => isSameValue(tab.value, value));
64
+ const findEnabledIndex = (startIndex, step) => {
65
+ if (!tabs.length) return -1;
66
+ let index = startIndex;
67
+ for (let i = 0; i < tabs.length; i += 1) {
68
+ index += step;
69
+ if (index < 0) index = tabs.length - 1;
70
+ else if (index >= tabs.length) index = 0;
71
+ if (!tabs[index]?.disabled) return index;
72
+ }
73
+ return -1;
74
+ };
75
+ const coerceValue = (value, fallback = false) => {
76
+ const index = findTabIndex(value);
77
+ if (index >= 0 && !tabs[index]?.disabled) return tabs[index].value;
78
+ return fallback ? (firstEnabled?.value ?? null) : (value ?? null);
79
+ };
80
+ const initialValue = coerceValue(
81
+ model
82
+ ? model.get()
83
+ : (hasOwn(props, "value") ? uiUnwrap(props.value) : (props.defaultValue ?? props.default)),
84
+ true
85
+ );
86
+ const [getValue, setValue] = JSswift.reactive.signal(initialValue);
87
+
88
+ const cls = uiClass([
89
+ "cms-clear-set",
90
+ "cms-tabs",
91
+ "cms-singularity",
92
+ orientation,
93
+ `variant-${variant}`,
94
+ uiWhen(props.dense, "dense"),
95
+ uiWhen(fillTabs, "fill"),
96
+ uiWhen(wrapTabs, "wrap"),
97
+ uiWhen(disabledAll, "disabled"),
98
+ props.class
99
+ ]);
100
+ const wrapProps = JSswift.omit(props, [
101
+ "tabs", "items", "value", "defaultValue", "default", "model",
102
+ "orientation", "orient", "direction",
103
+ "variant", "pills", "soft",
104
+ "fill", "navFill", "stretch",
105
+ "wrap", "dense", "disabled",
106
+ "slots", "empty",
107
+ "gap", "tabGap",
108
+ "align", "justify",
109
+ "color", "state",
110
+ "navClass", "navStyle",
111
+ "barClass", "barStyle",
112
+ "tabClass", "tabStyle",
113
+ "itemClass", "itemStyle",
114
+ "class", "style"
115
+ ]);
116
+ wrapProps.class = cls;
117
+ wrapProps.style = {
118
+ ...(props.style || {})
119
+ };
120
+ if (props.gap != null) wrapProps.style["--cms-tabs-gap"] = toCssSize(props.gap);
121
+ if (props.tabGap != null) wrapProps.style["--cms-tabs-tab-gap"] = toCssSize(props.tabGap);
122
+ const accentColor = resolveAccent(props.color ?? props.state);
123
+ if (accentColor) wrapProps.style["--cms-tabs-accent"] = accentColor;
124
+ const wrap = _.div(wrapProps);
125
+
126
+ const bar = _.div({
127
+ class: uiClass(["cms-tabs-bar", props.barClass]),
128
+ style: {
129
+ alignItems: props.align || (orientation === "vertical" ? "stretch" : "center"),
130
+ justifyContent: props.justify || "flex-start",
131
+ ...(props.barStyle || {})
132
+ }
133
+ });
134
+ const nav = _.div({
135
+ class: uiClass(["cms-tabs-nav", props.navClass]),
136
+ style: props.navStyle || null,
137
+ role: "tablist",
138
+ "aria-orientation": orientation
139
+ });
140
+ const tabButtons = [];
141
+
142
+ const getActiveIndex = () => findTabIndex(getValue());
143
+ const getActiveTab = () => {
144
+ const index = getActiveIndex();
145
+ return index >= 0 ? tabs[index] : null;
146
+ };
147
+ const createRootCtx = () => ({
148
+ tabs,
149
+ value: getValue(),
150
+ active: getValue(),
151
+ activeIndex: getActiveIndex(),
152
+ activeTab: getActiveTab(),
153
+ orientation,
154
+ variant,
155
+ select: (value, event) => selectByValue(value, { event, emit: true }),
156
+ next: () => goNext(),
157
+ prev: () => goPrev()
158
+ });
159
+ const createCtx = (tab, index, active) => ({
160
+ ...createRootCtx(),
161
+ tab,
162
+ index,
163
+ active,
164
+ disabled: !!tab.disabled,
165
+ select: (event) => selectByIndex(index, { event, emit: true })
166
+ });
167
+ const focusIndex = (index) => {
168
+ const btn = tabButtons[index]?.btn;
169
+ if (!btn || typeof btn.focus !== "function") return;
170
+ requestAnimationFrame(() => btn.focus());
171
+ };
172
+
173
+ const updateButtons = (nextValue) => {
174
+ const nextIndex = findTabIndex(nextValue);
175
+ wrap.dataset.value = nextValue == null ? "" : String(nextValue);
176
+ wrap.dataset.activeIndex = nextIndex >= 0 ? String(nextIndex) : "";
177
+ tabButtons.forEach(({ btn, item, tab, index }) => {
178
+ const active = index === nextIndex;
179
+ item.classList.toggle("active", active);
180
+ item.classList.toggle("disabled", !!(tab.disabled || disabledAll));
181
+ btn.classList.toggle("active", active);
182
+ btn.setAttribute("aria-selected", active ? "true" : "false");
183
+ btn.setAttribute("tabindex", active ? "0" : "-1");
184
+ });
185
+ };
186
+
187
+ const syncValue = (nextValue) => {
188
+ setValue(nextValue);
189
+ updateButtons(nextValue);
190
+ };
191
+
192
+ const selectByValue = (nextValue, options = {}) => {
193
+ if (disabledAll) return null;
194
+ const index = findTabIndex(nextValue);
195
+ if (index < 0) return null;
196
+ return selectByIndex(index, options);
197
+ };
198
+
199
+ const selectByIndex = (index, options = {}) => {
200
+ const tab = tabs[index];
201
+ if (!tab || tab.disabled || disabledAll) return null;
202
+ const nextValue = tab.value;
203
+ const prevValue = getValue();
204
+ const changed = !isSameValue(prevValue, nextValue);
205
+
206
+ syncValue(nextValue);
207
+ if (model && !isSameValue(model.get(), nextValue)) {
208
+ model.set(nextValue);
209
+ }
210
+ if (options.focus) focusIndex(index);
211
+ if (options.event) {
212
+ tab.onClick?.(nextValue, options.event, tab, index);
213
+ }
214
+ if (changed && options.emit !== false) {
215
+ props.onChange?.(nextValue, tab, options.event || null, index);
216
+ }
217
+ return tab;
218
+ };
219
+
220
+ const goNext = () => {
221
+ const nextIndex = findEnabledIndex(getActiveIndex() < 0 ? -1 : getActiveIndex(), 1);
222
+ if (nextIndex >= 0) selectByIndex(nextIndex, { emit: true, focus: true });
223
+ };
224
+ const goPrev = () => {
225
+ const start = getActiveIndex() < 0 ? 0 : getActiveIndex();
226
+ const prevIndex = findEnabledIndex(start, -1);
227
+ if (prevIndex >= 0) selectByIndex(prevIndex, { emit: true, focus: true });
228
+ };
229
+
230
+ const makeTabNode = (tab, index) => {
231
+ const isActive = index === getActiveIndex();
232
+ const ctx = createCtx(tab, index, isActive);
233
+ const tabId = `${componentId}-tab-${index}`;
234
+ const iconFallback = tab.iconFallback != null
235
+ ? (typeof tab.iconFallback === "string"
236
+ ? UI.Icon({ name: tab.iconFallback, size: tab.iconSize ?? tab.size ?? props.size ?? null })
237
+ : tab.iconFallback)
238
+ : null;
239
+ const iconNodes = renderSlotToArray(slots, "icon", ctx, iconFallback);
240
+ const labelNodes = renderSlotToArray(null, "default", {}, JSswift.ui.renderSlot(slots, "label", ctx, tab.labelFallback));
241
+ const noteNodes = renderSlotToArray(slots, "note", ctx, tab.noteFallback);
242
+ const badgeNodes = renderSlotToArray(slots, "badge", ctx, tab.badgeFallback);
243
+ const fallbackContent = _.span({ class: "cms-tabs-tab-inner" },
244
+ iconNodes.length ? _.span({ class: "cms-tabs-tab-icon" }, ...iconNodes) : null,
245
+ _.span({ class: "cms-tabs-tab-copy" },
246
+ _.span({ class: "cms-tabs-tab-label" }, ...(labelNodes.length ? labelNodes : [""])),
247
+ noteNodes.length ? _.span({ class: "cms-tabs-tab-note" }, ...noteNodes) : null
248
+ ),
249
+ badgeNodes.length ? _.span({ class: "cms-tabs-tab-badge" }, ...badgeNodes) : null
250
+ );
251
+ const contentNodes = renderSlotToArray(slots, "tab", {
252
+ ...ctx,
253
+ tabId,
254
+ label: labelNodes,
255
+ icon: iconNodes,
256
+ note: noteNodes,
257
+ badge: badgeNodes
258
+ }, fallbackContent);
259
+ const onKeydown = (e) => {
260
+ if (tab.disabled || disabledAll) return;
261
+ const key = e.key;
262
+ if (key === "Enter" || key === " ") {
263
+ e.preventDefault();
264
+ selectByIndex(index, { event: e, emit: true });
265
+ return;
266
+ }
267
+ if (key === "Home") {
268
+ e.preventDefault();
269
+ const firstIndex = tabs.findIndex((item) => !item.disabled);
270
+ if (firstIndex >= 0) selectByIndex(firstIndex, { event: e, emit: true, focus: true });
271
+ return;
272
+ }
273
+ if (key === "End") {
274
+ e.preventDefault();
275
+ let lastIndex = -1;
276
+ for (let i = tabs.length - 1; i >= 0; i -= 1) {
277
+ if (!tabs[i]?.disabled) {
278
+ lastIndex = i;
279
+ break;
280
+ }
281
+ }
282
+ if (lastIndex >= 0) selectByIndex(lastIndex, { event: e, emit: true, focus: true });
283
+ return;
284
+ }
285
+ const prevKeys = orientation === "vertical" ? ["ArrowUp"] : ["ArrowLeft"];
286
+ const nextKeys = orientation === "vertical" ? ["ArrowDown"] : ["ArrowRight"];
287
+ if (prevKeys.includes(key)) {
288
+ e.preventDefault();
289
+ const prevIndex = findEnabledIndex(index, -1);
290
+ if (prevIndex >= 0) selectByIndex(prevIndex, { event: e, emit: true, focus: true });
291
+ return;
292
+ }
293
+ if (nextKeys.includes(key)) {
294
+ e.preventDefault();
295
+ const nextIndex = findEnabledIndex(index, 1);
296
+ if (nextIndex >= 0) selectByIndex(nextIndex, { event: e, emit: true, focus: true });
297
+ }
298
+ };
299
+ const itemAccent = resolveAccent(tab.color ?? tab.state);
300
+ const item = _.div({
301
+ class: uiClass([
302
+ "cms-tabs-item",
303
+ uiWhen(isActive, "active"),
304
+ uiWhen(tab.disabled || disabledAll, "disabled"),
305
+ props.itemClass,
306
+ tab.itemClass
307
+ ]),
308
+ style: {
309
+ ...(itemAccent ? { "--cms-tabs-item-accent": itemAccent } : {}),
310
+ ...(props.itemStyle || {}),
311
+ ...(tab.itemStyle || {})
312
+ },
313
+ "data-name": String(tab.value)
314
+ });
315
+ const btn = UI.Btn({
316
+ class: uiClass([
317
+ "cms-tabs-tab",
318
+ uiWhen(isActive, "active"),
319
+ uiWhen(tab.disabled || disabledAll, "disabled"),
320
+ props.tabClass,
321
+ tab.tabClass || tab.class
322
+ ]),
323
+ type: "button",
324
+ id: tabId,
325
+ role: "tab",
326
+ disabled: !!(tab.disabled || disabledAll),
327
+ "aria-selected": isActive ? "true" : "false",
328
+ tabindex: isActive ? "0" : "-1",
329
+ size: tab.size ?? props.size ?? null,
330
+ style: {
331
+ ...(props.tabStyle || {}),
332
+ ...(tab.tabStyle || {})
333
+ },
334
+ onClick: (e) => {
335
+ selectByIndex(index, { event: e, emit: true });
336
+ },
337
+ onKeydown
338
+ }, ...contentNodes);
339
+ item.appendChild(btn);
340
+ item.appendChild(_.span({ class: "tab-indicator" }));
341
+ tabButtons[index] = { tab, index, item, btn };
342
+ return item;
343
+ };
344
+
345
+ if (tabs.length) {
346
+ const defaultNavNodes = tabs.map((tab, index) => makeTabNode(tab, index));
347
+ const navContent = JSswift.ui.renderSlot(slots, "nav", {
348
+ ...createRootCtx(),
349
+ nodes: defaultNavNodes
350
+ }, defaultNavNodes);
351
+ renderSlotToArray(null, "default", {}, navContent).forEach((node) => nav.appendChild(node));
352
+ } else {
353
+ renderSlotToArray(slots, "empty", createRootCtx(), props.empty ?? _.div({ class: "cms-tabs-empty" }, "Nessuna tab disponibile."))
354
+ .forEach((node) => nav.appendChild(node));
355
+ }
356
+
357
+ const extraCtx = createRootCtx();
358
+ const extraNodes = [
359
+ ...renderSlotToArray(null, "default", {}, JSswift.ui.renderSlot(slots, "extra", extraCtx, null)),
360
+ ...renderSlotToArray(slots, "default", extraCtx, children)
361
+ ];
362
+ bar.appendChild(nav);
363
+ if (extraNodes.length) {
364
+ bar.appendChild(_.div({ class: "cms-tabs-extra" }, ...extraNodes));
365
+ }
366
+ wrap.appendChild(bar);
367
+
368
+ syncValue(initialValue);
369
+ if (model) {
370
+ model.watch((value) => {
371
+ syncValue(coerceValue(value, true));
372
+ }, "UI.Tabs:watch");
373
+ } else if (uiIsReactive(props.value)) {
374
+ app.reactive.effect(() => {
375
+ syncValue(coerceValue(uiUnwrap(props.value), true));
376
+ }, "UI.Tabs:value");
377
+ }
378
+
379
+ wrap._getValue = () => getValue();
380
+ wrap._setValue = (value) => selectByValue(value, { emit: true });
381
+ wrap._select = (value) => selectByValue(value, { emit: true });
382
+ wrap._next = () => goNext();
383
+ wrap._prev = () => goPrev();
384
+ wrap._getTabs = () => tabs.slice();
385
+ setPropertyProps(wrap, props);
386
+ return wrap;
387
+ };
388
+ if (JSswift.isDev?.()) {
389
+ UI.meta = UI.meta || {};
390
+ UI.meta.Tabs = {
391
+ signature: "UI.Tabs(props) | UI.Tabs(props, ...children)",
392
+ props: {
393
+ tabs: "Array<{ value|name|id|key, label|title, note|subtitle|description, icon, badge|counter, disabled, hidden, onClick }>",
394
+ items: "Alias of tabs",
395
+ value: "any",
396
+ defaultValue: "any",
397
+ default: "Alias of defaultValue",
398
+ model: "[get,set] signal",
399
+ orientation: "horizontal|vertical",
400
+ variant: "line|pills|soft",
401
+ fill: "boolean",
402
+ wrap: "boolean",
403
+ disabled: "boolean",
404
+ color: "string",
405
+ state: "string",
406
+ gap: "string|number",
407
+ tabGap: "string|number",
408
+ align: "string",
409
+ justify: "string",
410
+ size: "string|number",
411
+ dense: "boolean",
412
+ navClass: "string",
413
+ tabClass: "string",
414
+ itemClass: "string",
415
+ slots: "{ nav?, tab?, label?, icon?, note?, badge?, extra?, empty?, default? }",
416
+ class: "string",
417
+ style: "object"
418
+ },
419
+ slots: {
420
+ nav: "Full nav renderer",
421
+ tab: "Single tab renderer",
422
+ label: "Label renderer",
423
+ icon: "Icon renderer",
424
+ note: "Note renderer",
425
+ badge: "Badge/counter renderer",
426
+ extra: "Extra area next to the nav",
427
+ empty: "Fallback when tabs/items are empty",
428
+ default: "Children / extra content fallback"
429
+ },
430
+ events: {
431
+ onChange: "(value, tab, event, index)"
432
+ },
433
+ keyboard: ["Enter/Space", "ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", "Home", "End"],
434
+ returns: "HTMLDivElement con ._getValue(), ._setValue(value), ._select(value), ._next(), ._prev(), ._getTabs()",
435
+ description: "Standardized tab bar with controlled/uncontrolled support, structured slots, badge/note/icon, and keyboard navigation."
436
+ };
437
+ }
438
+ // Esempio: JSswift.ui.Tabs({ tabs: [{ label: "Overview", value: "overview", icon: "dashboard" }], model: [get,set] })
439
+
440
+ UI.RouteTab = (...args) => {
441
+ const { props, children } = JSswift.uiNormalizeArgs(args);
442
+ const slots = props.slots || {};
443
+ const router = JSswift.router || app?.router || null;
444
+ const hasOwn = (key) => Object.prototype.hasOwnProperty.call(props, key);
445
+ const hasExplicitActive = hasOwn("active") || hasOwn("selected");
446
+ const normalizeVariant = (value) => {
447
+ const raw = String(uiUnwrap(value) || "").toLowerCase();
448
+ if (raw === "pill" || raw === "pills") return "pills";
449
+ if (raw === "soft" || raw === "card") return "soft";
450
+ return "line";
451
+ };
452
+ const resolveAccent = (value) => {
453
+ const raw = uiUnwrap(value);
454
+ if (raw == null || raw === "") return null;
455
+ return JSswift.uiColors?.includes(raw) ? `var(--cms-${raw})` : String(raw);
456
+ };
457
+ const resolvePath = (value) => {
458
+ if (value == null || value === "") return "";
459
+ try {
460
+ const url = new URL(String(value), window.location.origin);
461
+ return url.pathname || "";
462
+ } catch (_) {
463
+ return String(value).split(/[?#]/)[0] || "";
464
+ }
465
+ };
466
+ const isExternalLink = (value) => {
467
+ if (uiUnwrap(props.external) === true) return true;
468
+ if (!value) return false;
469
+ return /^(mailto:|tel:|https?:\/\/|\/\/)/i.test(String(value));
470
+ };
471
+ const getHref = () => uiUnwrap(props.href ?? props.to) || "#";
472
+ const getTo = () => uiUnwrap(props.to ?? props.href) || "";
473
+ const getCurrentPath = () => {
474
+ try {
475
+ if (router?.current) return router.current().pathname || window.location.pathname || "";
476
+ } catch (_) { }
477
+ return window.location.pathname || "";
478
+ };
479
+ const matchCurrentRoute = () => {
480
+ const to = getTo();
481
+ if (!to) return false;
482
+ const matchValue = uiUnwrap(props.match ?? props.activeMatch ?? props.routeMatch);
483
+ const matchMode = String(uiUnwrap(props.matchMode ?? (props.startsWith ? "startsWith" : (props.exact ? "exact" : ""))) || "").toLowerCase();
484
+ const currentPath = getCurrentPath();
485
+ if (typeof matchValue === "function") {
486
+ return !!matchValue({ currentPath, to, href: getHref(), router, props });
487
+ }
488
+ if (matchValue instanceof RegExp) {
489
+ return matchValue.test(currentPath);
490
+ }
491
+ const targetPath = resolvePath(typeof matchValue === "string" && !["exact", "startsWith", "prefix", "section"].includes(matchValue.toLowerCase())
492
+ ? matchValue
493
+ : to);
494
+ if (!targetPath) return false;
495
+ const mode = matchMode || (typeof matchValue === "string" && ["startsWith", "prefix", "section"].includes(matchValue.toLowerCase()) ? "startsWith" : "exact");
496
+ if (mode === "startswith" || mode === "prefix" || mode === "section") {
497
+ return currentPath === targetPath || currentPath.startsWith(`${targetPath}/`);
498
+ }
499
+ if (router?.isActive) return !!router.isActive(to);
500
+ return currentPath === targetPath;
501
+ };
502
+ const getDisabled = () => !!uiUnwrap(props.disabled);
503
+ let manualActive = null;
504
+ const getActive = () => {
505
+ if (manualActive != null) return !!manualActive;
506
+ if (hasExplicitActive) return !!uiUnwrap(props.active ?? props.selected);
507
+ return matchCurrentRoute();
508
+ };
509
+
510
+ const variant = normalizeVariant(props.variant || (props.pills ? "pills" : (props.soft ? "soft" : "line")));
511
+ const normalizedState = !uiUnwrap(props.color) && uiUnwrap(props.state)
512
+ ? normalizeState(uiUnwrap(props.state))
513
+ : "";
514
+ const stateClass = normalizedState ? `cms-state-${normalizedState}` : "";
515
+ const p = JSswift.omit(props, [
516
+ "active", "selected", "label", "title", "text", "to", "icon", "iconRight", "iconSize",
517
+ "note", "subtitle", "caption", "description", "badge", "counter", "count", "aside", "trailing",
518
+ "slots", "state", "color", "dense", "outline", "flat", "glossy", "glow", "glass", "gradient",
519
+ "textGradient", "lightShadow", "shadow", "rounded", "radius", "textColor", "variant", "pills",
520
+ "soft", "block", "fill", "match", "matchMode", "activeMatch", "routeMatch", "exact",
521
+ "startsWith", "external", "onNavigate"
522
+ ]);
523
+ p.href = getHref();
524
+ p.class = uiClass([
525
+ "cms-route-tab",
526
+ "cms-clear-set",
527
+ "cms-singularity",
528
+ `variant-${variant}`,
529
+ uiWhen(props.block ?? props.fill, "block"),
530
+ uiWhen(props.dense, "dense"),
531
+ stateClass,
532
+ props.class
533
+ ]);
534
+ p.style = {
535
+ ...(props.style || {})
536
+ };
537
+
538
+ const accent = resolveAccent(props.color ?? props.state);
539
+ if (accent) p.style["--cms-route-tab-accent"] = accent;
540
+
541
+ const iconSize = props.iconSize ?? props.size ?? null;
542
+ const resolveIconNode = (value, as) => {
543
+ const raw = uiUnwrap(value);
544
+ if (raw == null || raw === false || raw === "") return null;
545
+ if (typeof raw === "string") return UI.Icon({ name: raw, size: iconSize });
546
+ return JSswift.ui.slot(raw, { as });
547
+ };
548
+ const ctx = () => ({
549
+ active: getActive(),
550
+ disabled: getDisabled(),
551
+ href: getHref(),
552
+ to: getTo(),
553
+ external: isExternalLink(getHref()),
554
+ navigate: (event) => {
555
+ if (getDisabled()) return false;
556
+ const nextTo = getTo();
557
+ const external = isExternalLink(getHref());
558
+ if (!external && nextTo && router?.navigate) {
559
+ if (event?.preventDefault) event.preventDefault();
560
+ router.navigate(nextTo);
561
+ props.onNavigate?.(nextTo, event || null);
562
+ return true;
563
+ }
564
+ props.onNavigate?.(nextTo || getHref(), event || null);
565
+ return false;
566
+ }
567
+ });
568
+
569
+ const iconNodes = renderSlotToArray(slots, "icon", ctx(), resolveIconNode(props.icon, "icon"));
570
+ const labelNodes = renderSlotToArray(
571
+ slots,
572
+ "label",
573
+ ctx(),
574
+ props.label ?? props.title ?? props.text ?? children
575
+ );
576
+ const noteNodes = renderSlotToArray(
577
+ slots,
578
+ "note",
579
+ ctx(),
580
+ props.note ?? props.subtitle ?? props.caption ?? props.description
581
+ );
582
+ const badgeNodes = renderSlotToArray(
583
+ slots,
584
+ "badge",
585
+ ctx(),
586
+ props.badge ?? props.counter ?? props.count
587
+ );
588
+ const asideNodes = renderSlotToArray(
589
+ slots,
590
+ "aside",
591
+ ctx(),
592
+ props.aside ?? props.trailing ?? resolveIconNode(props.iconRight, "iconRight")
593
+ );
594
+ const defaultNodes = renderSlotToArray(slots, "default", ctx(), props.content ?? props.body);
595
+
596
+ const content = _.span({ class: "cms-route-tab-inner" },
597
+ iconNodes.length ? _.span({ class: "cms-route-tab-icon" }, ...iconNodes) : null,
598
+ _.span({ class: "cms-route-tab-copy" },
599
+ _.span({ class: "cms-route-tab-label" }, ...(labelNodes.length ? labelNodes : [getTo() || getHref() || ""])),
600
+ noteNodes.length ? _.span({ class: "cms-route-tab-note" }, ...noteNodes) : null,
601
+ defaultNodes.length ? _.span({ class: "cms-route-tab-extra" }, ...defaultNodes) : null
602
+ ),
603
+ badgeNodes.length ? _.span({ class: "cms-route-tab-badge" }, ...badgeNodes) : null,
604
+ asideNodes.length ? _.span({ class: "cms-route-tab-aside" }, ...asideNodes) : null
605
+ );
606
+
607
+ const wrap = _.a(p, content);
608
+ setPropertyProps(wrap, props);
609
+
610
+ const syncState = () => {
611
+ const active = getActive();
612
+ const disabled = getDisabled();
613
+ const href = getHref();
614
+ const external = isExternalLink(href);
615
+ wrap.classList.toggle("active", active);
616
+ wrap.classList.toggle("is-disabled", disabled);
617
+ wrap.classList.toggle("is-external", external);
618
+ if (active) wrap.setAttribute("aria-current", String(uiUnwrap(props.ariaCurrent) || "page"));
619
+ else wrap.removeAttribute("aria-current");
620
+ wrap.setAttribute("href", href || "#");
621
+ if (disabled) {
622
+ wrap.setAttribute("aria-disabled", "true");
623
+ wrap.tabIndex = -1;
624
+ } else {
625
+ wrap.removeAttribute("aria-disabled");
626
+ if (!hasOwn("tabIndex") && !hasOwn("tabindex")) wrap.removeAttribute("tabindex");
627
+ }
628
+ if (getTo()) wrap.dataset.to = String(getTo());
629
+ else delete wrap.dataset.to;
630
+ if (external && wrap.target === "_blank" && !wrap.rel) wrap.rel = "noopener noreferrer";
631
+ };
632
+
633
+ const userOnClick = props.onClick;
634
+ wrap.onclick = (e) => {
635
+ userOnClick?.(e);
636
+ if (e.defaultPrevented) return;
637
+ if (getDisabled()) {
638
+ e.preventDefault();
639
+ return;
640
+ }
641
+ ctx().navigate(e);
642
+ };
643
+
644
+ if (!hasExplicitActive && getTo() && router?.subscribe) {
645
+ router.subscribe(() => syncState());
646
+ }
647
+ JSswift.reactive.effect(() => {
648
+ syncState();
649
+ }, "UI.RouteTab:render");
650
+
651
+ wrap._isActive = () => getActive();
652
+ wrap._setActive = (value) => {
653
+ manualActive = value == null ? null : !!value;
654
+ syncState();
655
+ return wrap;
656
+ };
657
+ wrap._navigate = (event) => ctx().navigate(event);
658
+ return wrap;
659
+ };
660
+ if (JSswift.isDev?.()) {
661
+ UI.meta = UI.meta || {};
662
+ UI.meta.RouteTab = {
663
+ signature: "UI.RouteTab(...children) | UI.RouteTab(props, ...children)",
664
+ props: {
665
+ label: "String|Node|Function|Array",
666
+ to: "string",
667
+ href: "string",
668
+ active: "boolean",
669
+ selected: "Alias of active",
670
+ match: "\"exact\"|\"startsWith\"|RegExp|Function|string",
671
+ matchMode: "\"exact\"|\"startsWith\"",
672
+ exact: "boolean",
673
+ startsWith: "boolean",
674
+ note: "String|Node|Function|Array",
675
+ badge: "String|Node|Function|Array",
676
+ icon: "String|Node|Function|Array",
677
+ iconRight: "String|Node|Function|Array",
678
+ aside: "Node|Function|Array",
679
+ variant: "\"line\"|\"pills\"|\"soft\"",
680
+ disabled: "boolean",
681
+ block: "boolean",
682
+ state: "string",
683
+ color: "string",
684
+ slots: "{ icon?, label?, note?, badge?, aside?, default? }",
685
+ class: "string",
686
+ style: "object"
687
+ },
688
+ slots: {
689
+ icon: "Leading icon content",
690
+ label: "Main label content",
691
+ note: "Secondary note/caption",
692
+ badge: "Badge / counter area",
693
+ aside: "Trailing visual/action area",
694
+ default: "Extra content under the note"
695
+ },
696
+ events: {
697
+ onClick: "MouseEvent",
698
+ onNavigate: "(to, event)"
699
+ },
700
+ returns: "HTMLAnchorElement con ._isActive(), ._setActive(boolean|null), ._navigate(event?)",
701
+ description: "Standardized tab/link for router or href navigation, with structured slots, states, and badges."
702
+ };
703
+ }
704
+ // Esempio: JSswift.ui.RouteTab({ label: "Home", to: "/" })
705
+
706
+ UI.Breadcrumbs = (...args) => {
707
+ const { props, children } = JSswift.uiNormalizeArgs(args);
708
+ const slots = props.slots || {};
709
+ const router = JSswift.router || app?.router || null;
710
+ const model = resolveModel(props.model, "UI.Breadcrumbs:model");
711
+ const sizeClass = uiComputed(props.size, () => {
712
+ const v = uiUnwrap(props.size);
713
+ return (typeof v === "string" && JSswift.uiSizes?.includes(v)) ? `cms-size-${v}` : "";
714
+ });
715
+ const variantClass = uiComputed(props.variant, () => {
716
+ const raw = String(uiUnwrap(props.variant || (props.pills ? "pills" : (props.soft ? "soft" : "line"))) || "").toLowerCase();
717
+ if (raw === "pill" || raw === "pills") return "variant-pills";
718
+ if (raw === "soft" || raw === "card") return "variant-soft";
719
+ return "variant-line";
720
+ });
721
+ const nowrapClass = uiComputed([props.wrap, props.nowrap], () => {
722
+ return (uiUnwrap(props.wrap) === false || !!uiUnwrap(props.nowrap)) ? "no-wrap" : "";
723
+ });
724
+ const wrap = _.nav({
725
+ class: uiClass([
726
+ "cms-breadcrumbs",
727
+ "cms-clear-set",
728
+ sizeClass,
729
+ variantClass,
730
+ nowrapClass,
731
+ uiWhen(props.dense, "dense"),
732
+ props.class
733
+ ]),
734
+ style: { ...(props.style || {}) },
735
+ role: "navigation",
736
+ "aria-label": props.ariaLabel || "Breadcrumb"
737
+ });
738
+ const beforeEl = _.div({ class: "cms-breadcrumbs-before" });
739
+ const listEl = _.ol({ class: "cms-breadcrumbs-list" });
740
+ const afterEl = _.div({ class: "cms-breadcrumbs-after" });
741
+ wrap.appendChild(beforeEl);
742
+ wrap.appendChild(listEl);
743
+ wrap.appendChild(afterEl);
744
+
745
+ const resolveAccent = (value) => {
746
+ const raw = uiUnwrap(value);
747
+ if (raw == null || raw === "") return null;
748
+ return JSswift.uiColors?.includes(raw) ? `var(--cms-${raw})` : String(raw);
749
+ };
750
+ const isExternalLink = (entry) => {
751
+ if (!entry) return false;
752
+ if (uiUnwrap(entry.external) === true) return true;
753
+ const href = uiUnwrap(entry.href ?? entry.to);
754
+ if (!href) return false;
755
+ return /^(mailto:|tel:|https?:\/\/|\/\/)/i.test(String(href));
756
+ };
757
+ const cloneIfNode = (value) => {
758
+ if (value?.nodeType && value.cloneNode) return value.cloneNode(true);
759
+ return value;
760
+ };
761
+ const renderInto = (host, nodes) => {
762
+ host.innerHTML = "";
763
+ nodes.forEach((node) => host.appendChild(node));
764
+ };
765
+ const renderNamedSlot = (slotBag, name, ctx, fallback, alias = null) => {
766
+ const primary = JSswift.ui.getSlot(slotBag, name) != null
767
+ ? renderSlotToArray(slotBag, name, ctx, fallback)
768
+ : [];
769
+ if (primary.length) return primary;
770
+ if (alias && JSswift.ui.getSlot(slotBag, alias) != null) {
771
+ return renderSlotToArray(slotBag, alias, ctx, fallback);
772
+ }
773
+ return renderSlotToArray(null, "default", ctx, fallback);
774
+ };
775
+ const resolveIconNode = (value, size, as) => {
776
+ const raw = uiUnwrap(value);
777
+ if (raw == null || raw === false || raw === "") return null;
778
+ if (typeof raw === "string") return UI.Icon({ name: raw, size });
779
+ return JSswift.ui.slot(raw, { as });
780
+ };
781
+ const normalizeItems = () => {
782
+ const source = model ? model.get() : uiUnwrap(props.items ?? props.value ?? props.breadcrumbs);
783
+ const list = [];
784
+ const pushItem = (entry) => {
785
+ if (entry == null || entry === false) return;
786
+ if (Array.isArray(entry)) {
787
+ entry.forEach(pushItem);
788
+ return;
789
+ }
790
+ if (typeof entry === "string" || typeof entry === "number") {
791
+ list.push({ label: String(entry) });
792
+ return;
793
+ }
794
+ if (entry?.nodeType || typeof entry === "function") {
795
+ list.push({ label: entry });
796
+ return;
797
+ }
798
+ list.push(entry);
799
+ };
800
+
801
+ const home = uiUnwrap(props.home);
802
+ if (home) {
803
+ pushItem(home === true ? { label: "Home", to: "/", icon: "home" } : home);
804
+ }
805
+ pushItem(source);
806
+ if (!list.length && children?.length) {
807
+ children.forEach((child) => pushItem(child));
808
+ }
809
+ return list.filter((item) => !uiUnwrap(item?.hidden));
810
+ };
811
+ const getCurrentIndex = (items) => {
812
+ const explicitIndex = items.findIndex((item) => !!uiUnwrap(item?.current ?? item?.active ?? item?.selected));
813
+ if (explicitIndex > -1) return explicitIndex;
814
+ return uiUnwrap(props.autoCurrent) === false ? -1 : (items.length ? items.length - 1 : -1);
815
+ };
816
+ const collapseItems = (items) => {
817
+ const maxRaw = Number(uiUnwrap(props.max ?? props.maxItems ?? props.collapseAfter ?? props.collapse));
818
+ const max = Number.isFinite(maxRaw) ? Math.max(0, Math.trunc(maxRaw)) : 0;
819
+ if (!max || max < 3 || items.length <= max) {
820
+ return items.map((item, index) => ({ type: "item", item, index, key: `item-${index}` }));
821
+ }
822
+
823
+ let leading = Number(uiUnwrap(props.leadingCount ?? props.keepStart ?? 1));
824
+ let trailing = Number(uiUnwrap(props.trailingCount ?? props.keepEnd ?? Math.max(1, max - 2)));
825
+ leading = Number.isFinite(leading) ? Math.max(1, Math.trunc(leading)) : 1;
826
+ trailing = Number.isFinite(trailing) ? Math.max(1, Math.trunc(trailing)) : Math.max(1, max - 2);
827
+ if (leading + trailing + 1 > max) {
828
+ trailing = Math.max(1, max - leading - 1);
829
+ if (leading + trailing + 1 > max) leading = Math.max(1, max - trailing - 1);
830
+ }
831
+ if (leading + trailing >= items.length) {
832
+ return items.map((item, index) => ({ type: "item", item, index, key: `item-${index}` }));
833
+ }
834
+
835
+ const entries = [];
836
+ for (let i = 0; i < leading; i++) {
837
+ entries.push({ type: "item", item: items[i], index: i, key: `item-${i}` });
838
+ }
839
+ const hiddenItems = items.slice(leading, items.length - trailing);
840
+ entries.push({
841
+ type: "collapsed",
842
+ key: `collapsed-${leading}-${items.length - trailing}`,
843
+ items: hiddenItems,
844
+ from: leading,
845
+ to: items.length - trailing - 1
846
+ });
847
+ for (let i = items.length - trailing; i < items.length; i++) {
848
+ entries.push({ type: "item", item: items[i], index: i, key: `item-${i}` });
849
+ }
850
+ return entries;
851
+ };
852
+ const navigateItem = (item, event = null) => {
853
+ if (!item) return false;
854
+ const href = uiUnwrap(item.href ?? item.to) || "";
855
+ const to = uiUnwrap(item.to) || "";
856
+ if (!isExternalLink(item) && to && router?.navigate) {
857
+ if (event?.preventDefault) event.preventDefault();
858
+ router.navigate(to);
859
+ item.onNavigate?.(to, event || null);
860
+ props.onNavigate?.(to, item, event || null);
861
+ return true;
862
+ }
863
+ item.onNavigate?.(to || href, event || null);
864
+ props.onNavigate?.(to || href, item, event || null);
865
+ return false;
866
+ };
867
+ const createSeparator = (entry, index, total) => {
868
+ const prev = entry;
869
+ const next = total[index + 1] || null;
870
+ const ctx = { index, entry, previous: prev, next };
871
+ const fallback = cloneIfNode(uiUnwrap((entry?.type === "item" ? entry.item?.separator : null) ?? props.separator ?? "/"));
872
+ const nodes = renderNamedSlot(slots, "separator", ctx, fallback);
873
+ return _.li({ class: "cms-breadcrumbs-separator-item", "aria-hidden": "true" },
874
+ _.span({ class: "cms-breadcrumbs-separator" }, ...nodes)
875
+ );
876
+ };
877
+ const createCollapsedEntry = (entry, items, currentIndex) => {
878
+ const ctx = {
879
+ type: "collapsed",
880
+ collapsed: true,
881
+ items,
882
+ hiddenItems: entry.items,
883
+ currentIndex,
884
+ from: entry.from,
885
+ to: entry.to
886
+ };
887
+ const fallback = _.span({ class: "cms-breadcrumb cms-breadcrumb-collapsed" },
888
+ _.span({ class: "cms-breadcrumb-inner" },
889
+ _.span({ class: "cms-breadcrumb-copy" },
890
+ _.span({ class: "cms-breadcrumb-label" }, String(uiUnwrap(props.collapsedLabel) || "…"))
891
+ )
892
+ )
893
+ );
894
+ const nodes = renderNamedSlot(slots, "collapsed", ctx, fallback, "item");
895
+ return _.li({ class: "cms-breadcrumbs-entry is-collapsed" }, ...nodes);
896
+ };
897
+ const createItemEntry = (entry, items, currentIndex, visibleIndex) => {
898
+ const item = entry.item || {};
899
+ const slotBag = item.slots ? { ...slots, ...item.slots } : slots;
900
+ const href = uiUnwrap(item.href ?? item.to) || "";
901
+ const disabled = !!uiUnwrap(item.disabled);
902
+ const current = entry.index === currentIndex;
903
+ const iconSize = item.iconSize ?? props.itemSize ?? props.size ?? null;
904
+ const accent = resolveAccent(item.color ?? item.state ?? props.color ?? props.state);
905
+ const state = !uiUnwrap(item.color) && uiUnwrap(item.state)
906
+ ? normalizeState(uiUnwrap(item.state))
907
+ : "";
908
+ const labelFallback = item.label ?? item.title ?? item.text ?? item.to ?? item.href ?? "";
909
+ const noteFallback = item.note ?? item.subtitle ?? item.caption ?? item.description;
910
+ const badgeFallback = item.badge ?? item.counter ?? item.count;
911
+ const asideFallback = item.aside ?? item.trailing ?? resolveIconNode(item.iconRight, iconSize, "iconRight");
912
+ const defaultFallback = item.content ?? item.body;
913
+ const ctx = {
914
+ item,
915
+ index: entry.index,
916
+ visibleIndex,
917
+ total: items.length,
918
+ current,
919
+ active: current,
920
+ disabled,
921
+ href,
922
+ to: uiUnwrap(item.to) || "",
923
+ external: isExternalLink(item),
924
+ isFirst: entry.index === 0,
925
+ isLast: entry.index === items.length - 1,
926
+ navigate: (event) => navigateItem(item, event)
927
+ };
928
+
929
+ const iconNodes = renderNamedSlot(slotBag, "icon", ctx, resolveIconNode(item.icon, iconSize, "icon"));
930
+ const labelNodes = renderNamedSlot(slotBag, "label", ctx, labelFallback);
931
+ const noteNodes = renderNamedSlot(slotBag, "note", ctx, noteFallback);
932
+ const badgeNodes = renderNamedSlot(slotBag, "badge", ctx, badgeFallback);
933
+ const asideNodes = renderNamedSlot(slotBag, "aside", ctx, asideFallback);
934
+ const defaultNodes = renderNamedSlot(slotBag, "default", ctx, defaultFallback);
935
+
936
+ const content = _.span({ class: "cms-breadcrumb-inner" },
937
+ iconNodes.length ? _.span({ class: "cms-breadcrumb-icon" }, ...iconNodes) : null,
938
+ _.span({ class: "cms-breadcrumb-copy" },
939
+ _.span({ class: "cms-breadcrumb-label" }, ...(labelNodes.length ? labelNodes : [href || ""])),
940
+ noteNodes.length ? _.span({ class: "cms-breadcrumb-note" }, ...noteNodes) : null,
941
+ defaultNodes.length ? _.span({ class: "cms-breadcrumb-extra" }, ...defaultNodes) : null
942
+ ),
943
+ badgeNodes.length ? _.span({ class: "cms-breadcrumb-badge" }, ...badgeNodes) : null,
944
+ asideNodes.length ? _.span({ class: "cms-breadcrumb-aside" }, ...asideNodes) : null
945
+ );
946
+
947
+ const interactive = !disabled && (!current || !!uiUnwrap(item.linkCurrent ?? props.linkCurrent));
948
+ const sharedProps = {
949
+ class: uiClass([
950
+ "cms-breadcrumb",
951
+ "cms-clear-set",
952
+ "cms-singularity",
953
+ uiWhen(current, "is-current"),
954
+ uiWhen(current, "active"),
955
+ uiWhen(disabled, "is-disabled"),
956
+ state ? `cms-state-${state}` : "",
957
+ item.class
958
+ ]),
959
+ style: {
960
+ ...(item.style || {})
961
+ },
962
+ "aria-current": current ? String(uiUnwrap(item.ariaCurrent ?? props.ariaCurrent) || "page") : null
963
+ };
964
+ if (accent) sharedProps.style["--cms-breadcrumb-accent"] = accent;
965
+
966
+ let node = null;
967
+ if (interactive && href) {
968
+ node = _.a({
969
+ ...sharedProps,
970
+ href,
971
+ target: item.target,
972
+ rel: item.rel,
973
+ onClick: (event) => {
974
+ item.onClick?.(event, ctx);
975
+ props.onItemClick?.(item, ctx, event);
976
+ if (event.defaultPrevented) return;
977
+ ctx.navigate(event);
978
+ }
979
+ }, content);
980
+ if (ctx.external && node.target === "_blank" && !node.rel) node.rel = "noopener noreferrer";
981
+ } else if (interactive && typeof item.onClick === "function") {
982
+ node = _.button({
983
+ ...sharedProps,
984
+ type: item.type || "button",
985
+ onClick: (event) => {
986
+ item.onClick?.(event, ctx);
987
+ props.onItemClick?.(item, ctx, event);
988
+ }
989
+ }, content);
990
+ } else {
991
+ node = _.span({
992
+ ...sharedProps,
993
+ "aria-disabled": disabled ? "true" : null
994
+ }, content);
995
+ }
996
+
997
+ setPropertyProps(node, {
998
+ size: item.size ?? props.itemSize ?? props.size,
999
+ radius: item.radius ?? props.radius,
1000
+ gradient: item.gradient ?? props.gradient
1001
+ });
1002
+
1003
+ const fallback = node;
1004
+ const rendered = renderNamedSlot(slotBag, "item", ctx, fallback);
1005
+ return _.li({
1006
+ class: uiClass([
1007
+ "cms-breadcrumbs-entry",
1008
+ uiWhen(current, "is-current"),
1009
+ uiWhen(disabled, "is-disabled")
1010
+ ])
1011
+ }, ...rendered);
1012
+ };
1013
+ const render = () => {
1014
+ const items = normalizeItems();
1015
+ const currentIndex = getCurrentIndex(items);
1016
+ const visibleItems = collapseItems(items);
1017
+ const accent = resolveAccent(props.color ?? props.state);
1018
+ wrap.classList.toggle("is-empty", items.length === 0);
1019
+ if (accent) wrap.style.setProperty("--cms-breadcrumb-accent", accent);
1020
+ else wrap.style.removeProperty("--cms-breadcrumb-accent");
1021
+
1022
+ const rootCtx = {
1023
+ items,
1024
+ visibleItems,
1025
+ currentIndex,
1026
+ count: items.length,
1027
+ collapsed: visibleItems.some((entry) => entry.type === "collapsed")
1028
+ };
1029
+ renderInto(beforeEl, renderNamedSlot(slots, "before", rootCtx, props.before));
1030
+ renderInto(afterEl, renderNamedSlot(slots, "after", rootCtx, props.after));
1031
+
1032
+ listEl.innerHTML = "";
1033
+ if (!items.length) {
1034
+ const emptyNodes = renderNamedSlot(slots, "empty", rootCtx, props.empty);
1035
+ if (emptyNodes.length) listEl.appendChild(_.li({ class: "cms-breadcrumbs-entry is-empty" }, ...emptyNodes));
1036
+ return;
1037
+ }
1038
+
1039
+ visibleItems.forEach((entry, visibleIndex) => {
1040
+ if (visibleIndex > 0) listEl.appendChild(createSeparator(visibleItems[visibleIndex - 1], visibleIndex - 1, visibleItems));
1041
+ listEl.appendChild(
1042
+ entry.type === "collapsed"
1043
+ ? createCollapsedEntry(entry, items, currentIndex)
1044
+ : createItemEntry(entry, items, currentIndex, visibleIndex)
1045
+ );
1046
+ });
1047
+ };
1048
+
1049
+ app.reactive.effect(() => {
1050
+ render();
1051
+ }, "UI.Breadcrumbs:render");
1052
+
1053
+ wrap.getItems = () => normalizeItems();
1054
+ wrap.getVisibleItems = () => collapseItems(normalizeItems());
1055
+ wrap.refresh = () => {
1056
+ render();
1057
+ return wrap;
1058
+ };
1059
+ wrap.setItems = (next) => {
1060
+ if (model) model.set(next);
1061
+ else props.items = next;
1062
+ render();
1063
+ return wrap;
1064
+ };
1065
+ setPropertyProps(wrap, props);
1066
+ return wrap;
1067
+ };
1068
+ if (JSswift.isDev?.()) {
1069
+ UI.meta = UI.meta || {};
1070
+ UI.meta.Breadcrumbs = {
1071
+ signature: "UI.Breadcrumbs(props)",
1072
+ props: {
1073
+ items: "Array<string|number|Node|Function|{ label?, to?, href?, icon?, note?, badge?, current?, disabled?, hidden?, slots? }>",
1074
+ model: "[get,set] signal|Array",
1075
+ home: "boolean|Object",
1076
+ separator: "string|Node|Function",
1077
+ before: "Node|Function|Array",
1078
+ after: "Node|Function|Array",
1079
+ empty: "Node|Function|Array",
1080
+ variant: "\"line\"|\"pills\"|\"soft\"",
1081
+ max: "number",
1082
+ leadingCount: "number",
1083
+ trailingCount: "number",
1084
+ dense: "boolean",
1085
+ wrap: "boolean",
1086
+ nowrap: "boolean",
1087
+ linkCurrent: "boolean",
1088
+ color: "string",
1089
+ state: "string",
1090
+ itemSize: "string|number",
1091
+ ariaLabel: "string",
1092
+ class: "string",
1093
+ style: "object"
1094
+ },
1095
+ slots: {
1096
+ before: "Content before breadcrumb list",
1097
+ after: "Content after breadcrumb list",
1098
+ item: "Full renderer for a breadcrumb entry",
1099
+ icon: "Leading icon for each breadcrumb",
1100
+ label: "Main breadcrumb label",
1101
+ note: "Secondary line for the breadcrumb",
1102
+ badge: "Badge/counter area",
1103
+ aside: "Trailing area",
1104
+ default: "Extra content under note",
1105
+ separator: "Separator renderer",
1106
+ collapsed: "Renderer for collapsed middle items",
1107
+ empty: "Renderer for empty state"
1108
+ },
1109
+ events: {
1110
+ onItemClick: "(item, ctx, event)",
1111
+ onNavigate: "(toOrHref, item, event)"
1112
+ },
1113
+ returns: "HTMLElement <nav> con .getItems(), .getVisibleItems(), .refresh(), .setItems(items)",
1114
+ description: "Breadcrumbs standardizzati con item strutturati, slot completi, collapse automatico e supporto a link/router."
1115
+ };
1116
+ }
1117
+ // Esempio: JSswift.ui.Breadcrumbs({ items: [{ label: "Home", to: "/" }, { label: "Pagina" }] })
1118
+
1119
+ UI.Pagination = (...args) => {
1120
+ const { props } = JSswift.uiNormalizeArgs(args);
1121
+ const slots = props.slots || {};
1122
+ const model = resolveModel(props.model, "UI.Pagination:model");
1123
+ const sizeClass = uiComputed(props.size, () => {
1124
+ const v = uiUnwrap(props.size);
1125
+ return (typeof v === "string" && sizeMap[v]) ? `cms-size-${v}` : "";
1126
+ });
1127
+ const wrap = _.nav({
1128
+ class: uiClass([
1129
+ "cms-pagination",
1130
+ "cms-clear-set",
1131
+ sizeClass,
1132
+ uiWhen(props.dense, "dense"),
1133
+ uiWhen(props.simple, "is-simple"),
1134
+ uiWhen(props.disabled, "is-disabled"),
1135
+ props.class
1136
+ ]),
1137
+ style: { ...(props.style || {}) },
1138
+ role: "navigation",
1139
+ "aria-label": props.ariaLabel || "Pagination"
1140
+ });
1141
+ const controls = _.div({ class: "cms-pagination-controls" });
1142
+ const startGroup = _.div({ class: "cms-pagination-start" });
1143
+ const pagesGroup = _.div({ class: "cms-pagination-pages" });
1144
+ const endGroup = _.div({ class: "cms-pagination-end" });
1145
+ const summary = _.div({ class: "cms-pagination-summary" });
1146
+
1147
+ controls.appendChild(startGroup);
1148
+ controls.appendChild(pagesGroup);
1149
+ controls.appendChild(endGroup);
1150
+ wrap.appendChild(controls);
1151
+ wrap.appendChild(summary);
1152
+
1153
+ let currentPage = 1;
1154
+ let currentPages = 1;
1155
+
1156
+ const toInt = (value, fallback = 0) => {
1157
+ const raw = uiUnwrap(value);
1158
+ if (raw == null || raw === "") return fallback;
1159
+ const n = Number(raw);
1160
+ if (!Number.isFinite(n)) return fallback;
1161
+ return Math.trunc(n);
1162
+ };
1163
+ const clampPage = (page, pages = getPages()) => {
1164
+ const raw = Number(page);
1165
+ const next = Number.isFinite(raw) ? Math.trunc(raw) : 1;
1166
+ return Math.min(Math.max(next || 1, 1), Math.max(1, pages || 1));
1167
+ };
1168
+ const getPages = () => {
1169
+ const explicit = toInt(props.max ?? props.pages ?? props.pageCount ?? props.totalPages, 0);
1170
+ if (explicit > 0) return explicit;
1171
+ const total = toInt(props.total ?? props.totalItems ?? props.count, 0);
1172
+ const pageSize = toInt(props.pageSize ?? props.perPage ?? props.limit, 0);
1173
+ if (total >= 0 && pageSize > 0) return Math.max(1, Math.ceil(total / pageSize));
1174
+ return 1;
1175
+ };
1176
+ const getRequestedPage = () => {
1177
+ if (model) return toInt(model.get(), 1);
1178
+ return toInt(props.page ?? props.value ?? props.current ?? props.currentPage, 1);
1179
+ };
1180
+ const getSiblingCount = () => Math.max(0, toInt(props.siblings ?? props.siblingCount ?? props.window, 1));
1181
+ const getBoundaryCount = () => Math.max(0, toInt(props.boundaryCount ?? props.boundaries ?? props.edges, 1));
1182
+ const isDisabled = () => !!uiUnwrap(props.disabled);
1183
+ const showSummary = () => uiUnwrap(props.showSummary ?? props.showLabel) !== false;
1184
+ const showNumbers = () => uiUnwrap(props.showPages) !== false && !uiUnwrap(props.simple);
1185
+ const showPrev = () => uiUnwrap(props.showPrev) !== false;
1186
+ const showNext = () => uiUnwrap(props.showNext) !== false;
1187
+ const showEdges = () => !!uiUnwrap(props.showEdges);
1188
+ const showFirst = () => {
1189
+ const value = uiUnwrap(props.showFirst);
1190
+ return value == null ? showEdges() : !!value;
1191
+ };
1192
+ const showLast = () => {
1193
+ const value = uiUnwrap(props.showLast);
1194
+ return value == null ? showEdges() : !!value;
1195
+ };
1196
+ const hideOnSinglePage = () => !!uiUnwrap(props.hideOnSinglePage);
1197
+ const getTotal = () => {
1198
+ const total = uiUnwrap(props.total ?? props.totalItems ?? props.count);
1199
+ if (total == null || total === "") return null;
1200
+ const n = Number(total);
1201
+ return Number.isFinite(n) ? Math.max(0, Math.trunc(n)) : null;
1202
+ };
1203
+ const getPageSize = () => {
1204
+ const pageSize = uiUnwrap(props.pageSize ?? props.perPage ?? props.limit);
1205
+ if (pageSize == null || pageSize === "") return null;
1206
+ const n = Number(pageSize);
1207
+ return Number.isFinite(n) && n > 0 ? Math.trunc(n) : null;
1208
+ };
1209
+ const getCtx = (page = currentPage, pages = currentPages) => {
1210
+ const total = getTotal();
1211
+ const pageSize = getPageSize();
1212
+ const from = total != null && pageSize ? (total === 0 ? 0 : ((page - 1) * pageSize) + 1) : null;
1213
+ const to = total != null && pageSize ? Math.min(total, page * pageSize) : null;
1214
+ return {
1215
+ page,
1216
+ value: page,
1217
+ current: page,
1218
+ currentPage: page,
1219
+ pages,
1220
+ max: pages,
1221
+ total,
1222
+ pageSize,
1223
+ from,
1224
+ to,
1225
+ disabled: isDisabled(),
1226
+ isFirst: page <= 1,
1227
+ isLast: page >= pages,
1228
+ canPrev: page > 1,
1229
+ canNext: page < pages,
1230
+ setPage: (nextPage, event) => commitPage(nextPage, event),
1231
+ prev: (event) => commitPage(page - 1, event),
1232
+ next: (event) => commitPage(page + 1, event),
1233
+ first: (event) => commitPage(1, event),
1234
+ last: (event) => commitPage(pages, event)
1235
+ };
1236
+ };
1237
+ const buildItems = (page, pages) => {
1238
+ if (pages <= 0) return [];
1239
+ const items = [];
1240
+ const included = new Set();
1241
+ const boundary = getBoundaryCount();
1242
+ const siblings = getSiblingCount();
1243
+
1244
+ for (let i = 1; i <= Math.min(boundary, pages); i++) included.add(i);
1245
+ for (let i = Math.max(1, page - siblings); i <= Math.min(pages, page + siblings); i++) included.add(i);
1246
+ for (let i = Math.max(1, pages - boundary + 1); i <= pages; i++) included.add(i);
1247
+
1248
+ let last = 0;
1249
+ for (let i = 1; i <= pages; i++) {
1250
+ if (!included.has(i)) continue;
1251
+ if (last && i - last > 1) items.push({ type: "ellipsis", key: `ellipsis-${last}-${i}` });
1252
+ items.push({ type: "page", page: i, key: `page-${i}` });
1253
+ last = i;
1254
+ }
1255
+ if (!items.length) items.push({ type: "page", page: 1, key: "page-1" });
1256
+ return items;
1257
+ };
1258
+ const renderNamedSlot = (name, fallback, ctx, alias = null) => {
1259
+ const primary = JSswift.ui.getSlot(slots, name) != null
1260
+ ? renderSlotToArray(slots, name, ctx, fallback)
1261
+ : [];
1262
+ if (primary.length) return primary;
1263
+ if (alias && JSswift.ui.getSlot(slots, alias) != null) {
1264
+ return renderSlotToArray(slots, alias, ctx, fallback);
1265
+ }
1266
+ return renderSlotToArray(null, "default", ctx, fallback);
1267
+ };
1268
+ const renderInto = (host, nodes) => {
1269
+ host.innerHTML = "";
1270
+ nodes.forEach((node) => host.appendChild(node));
1271
+ };
1272
+ const createButton = (name, fallback, onClick, options = {}) => {
1273
+ const ctx = { ...getCtx(), kind: name, active: !!options.active };
1274
+ return UI.Btn({
1275
+ class: uiClass([
1276
+ "cms-pagination-btn",
1277
+ `cms-pagination-${name}`,
1278
+ uiWhen(options.active, "active"),
1279
+ options.class
1280
+ ]),
1281
+ size: props.itemSize || props.size || (props.dense ? "sm" : "md"),
1282
+ color: options.active ? (props.color || props.state || "primary") : (options.color || null),
1283
+ outline: options.active ? false : (options.outline ?? true),
1284
+ disabled: isDisabled() || !!options.disabled,
1285
+ "aria-current": options.active ? "page" : null,
1286
+ onClick: (event) => {
1287
+ if (isDisabled() || options.disabled) return;
1288
+ onClick?.(event);
1289
+ }
1290
+ }, ...renderNamedSlot(name, fallback, ctx, name === "summary" ? "label" : (name === "page" ? "item" : null)));
1291
+ };
1292
+ const render = (page, pages) => {
1293
+ currentPage = clampPage(page, pages);
1294
+ currentPages = Math.max(1, pages || 1);
1295
+ const ctx = getCtx(currentPage, currentPages);
1296
+
1297
+ wrap.style.display = hideOnSinglePage() && currentPages <= 1 ? "none" : "";
1298
+ summary.style.display = showSummary() ? "" : "none";
1299
+ pagesGroup.style.display = showNumbers() ? "" : "none";
1300
+
1301
+ startGroup.innerHTML = "";
1302
+ pagesGroup.innerHTML = "";
1303
+ endGroup.innerHTML = "";
1304
+
1305
+ const startNodes = renderNamedSlot("start", props.start, ctx);
1306
+ if (startNodes.length) {
1307
+ startGroup.appendChild(_.div({ class: "cms-pagination-extra cms-pagination-extra-start" }, ...startNodes));
1308
+ }
1309
+
1310
+ if (showFirst()) {
1311
+ startGroup.appendChild(createButton("first", "«", (event) => commitPage(1, event), {
1312
+ disabled: currentPage <= 1
1313
+ }));
1314
+ }
1315
+ if (showPrev()) {
1316
+ startGroup.appendChild(createButton("prev", "Prev", (event) => commitPage(currentPage - 1, event), {
1317
+ disabled: currentPage <= 1
1318
+ }));
1319
+ }
1320
+
1321
+ if (showNumbers()) {
1322
+ buildItems(currentPage, currentPages).forEach((item) => {
1323
+ if (item.type === "ellipsis") {
1324
+ const ellipsisCtx = { ...ctx, kind: "ellipsis" };
1325
+ const ellipsisNodes = renderNamedSlot("ellipsis", "…", ellipsisCtx);
1326
+ pagesGroup.appendChild(_.span({ class: "cms-pagination-ellipsis", "aria-hidden": "true" }, ...ellipsisNodes));
1327
+ return;
1328
+ }
1329
+ const itemPage = item.page;
1330
+ const itemCtx = { ...ctx, page: itemPage, value: itemPage, item, active: itemPage === currentPage };
1331
+ pagesGroup.appendChild(UI.Btn({
1332
+ class: uiClass([
1333
+ "cms-pagination-btn",
1334
+ "cms-pagination-page-btn",
1335
+ uiWhen(itemPage === currentPage, "active")
1336
+ ]),
1337
+ size: props.itemSize || props.size || (props.dense ? "sm" : "md"),
1338
+ color: itemPage === currentPage ? (props.color || props.state || "primary") : null,
1339
+ outline: itemPage === currentPage ? false : true,
1340
+ disabled: isDisabled(),
1341
+ "aria-current": itemPage === currentPage ? "page" : null,
1342
+ onClick: (event) => commitPage(itemPage, event)
1343
+ }, ...renderNamedSlot("page", String(itemPage), itemCtx, "item")));
1344
+ });
1345
+ }
1346
+
1347
+ if (showNext()) {
1348
+ endGroup.appendChild(createButton("next", "Next", (event) => commitPage(currentPage + 1, event), {
1349
+ disabled: currentPage >= currentPages
1350
+ }));
1351
+ }
1352
+ if (showLast()) {
1353
+ endGroup.appendChild(createButton("last", "»", (event) => commitPage(currentPages, event), {
1354
+ disabled: currentPage >= currentPages
1355
+ }));
1356
+ }
1357
+
1358
+ const endNodes = renderNamedSlot("end", props.end, ctx);
1359
+ if (endNodes.length) {
1360
+ endGroup.appendChild(_.div({ class: "cms-pagination-extra cms-pagination-extra-end" }, ...endNodes));
1361
+ }
1362
+
1363
+ let summaryText = `Pagina ${currentPage} di ${currentPages}`;
1364
+ if (ctx.total != null && ctx.pageSize) {
1365
+ summaryText = ctx.total === 0
1366
+ ? "0 risultati"
1367
+ : `${ctx.from}-${ctx.to} di ${ctx.total}`;
1368
+ }
1369
+ renderInto(summary, renderNamedSlot("summary", summaryText, ctx, "label"));
1370
+ };
1371
+ const commitPage = (page, event = null) => {
1372
+ const pages = getPages();
1373
+ const nextPage = clampPage(page, pages);
1374
+ if (nextPage === currentPage && pages === currentPages) return nextPage;
1375
+ render(nextPage, pages);
1376
+ if (model && model.get() !== nextPage) model.set(nextPage);
1377
+ props.onChange?.(nextPage, getCtx(nextPage, pages), event);
1378
+ props.onPageChange?.(nextPage, getCtx(nextPage, pages), event);
1379
+ return nextPage;
1380
+ };
1381
+
1382
+ app.reactive.effect(() => {
1383
+ const pages = getPages();
1384
+ const requested = getRequestedPage();
1385
+ const nextPage = clampPage(requested, pages);
1386
+ render(nextPage, pages);
1387
+ if (model && requested !== nextPage) model.set(nextPage);
1388
+ }, "UI.Pagination:sync");
1389
+
1390
+ wrap.getPage = () => currentPage;
1391
+ wrap.getPages = () => currentPages;
1392
+ wrap.setPage = (page) => commitPage(page);
1393
+ wrap.prev = () => commitPage(currentPage - 1);
1394
+ wrap.next = () => commitPage(currentPage + 1);
1395
+ wrap.first = () => commitPage(1);
1396
+ wrap.last = () => commitPage(currentPages);
1397
+ setPropertyProps(wrap, props);
1398
+ return wrap;
1399
+ };
1400
+ if (JSswift.isDev?.()) {
1401
+ UI.meta = UI.meta || {};
1402
+ UI.meta.Pagination = {
1403
+ signature: "UI.Pagination(props)",
1404
+ props: {
1405
+ max: "number",
1406
+ pages: "number",
1407
+ total: "number",
1408
+ pageSize: "number",
1409
+ value: "number",
1410
+ page: "number",
1411
+ model: "[get,set] signal",
1412
+ showPages: "boolean",
1413
+ showSummary: "boolean",
1414
+ showLabel: "boolean",
1415
+ showPrev: "boolean",
1416
+ showNext: "boolean",
1417
+ showFirst: "boolean",
1418
+ showLast: "boolean",
1419
+ showEdges: "boolean",
1420
+ siblings: "number",
1421
+ boundaryCount: "number",
1422
+ hideOnSinglePage: "boolean",
1423
+ size: "xxs|xs|sm|md|lg|xl|xxl|xxxl",
1424
+ dense: "boolean",
1425
+ simple: "boolean",
1426
+ color: "primary|secondary|warning|danger|success|info|light|dark",
1427
+ state: "primary|secondary|warning|danger|success|info|light|dark",
1428
+ slots: "{ start?, end?, first?, prev?, page?, item?, ellipsis?, next?, last?, summary?, label? }",
1429
+ class: "string",
1430
+ style: "object"
1431
+ },
1432
+ slots: {
1433
+ start: "Content before the controls",
1434
+ end: "Content after the controls",
1435
+ first: "First button content",
1436
+ prev: "Prev button content",
1437
+ page: "Page item content (ctx: { page, active, pages })",
1438
+ item: "Alias of page",
1439
+ ellipsis: "Ellipsis content",
1440
+ next: "Next button content",
1441
+ last: "Last button content",
1442
+ summary: "Summary content",
1443
+ label: "Alias legacy di summary"
1444
+ },
1445
+ events: {
1446
+ onChange: "(page, ctx, event)",
1447
+ onPageChange: "(page, ctx, event)"
1448
+ },
1449
+ returns: "HTMLElement <nav>",
1450
+ description: "Paginazione standard con controlli edge, numeri, ellissi, summary e supporto total/pageSize."
1451
+ };
1452
+ }
1453
+ // Esempio: JSswift.ui.Pagination({ total: 120, pageSize: 12, model: [get,set], showEdges: true })
1454
+