jsswift 2.0.1 → 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.
@@ -0,0 +1,1262 @@
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(--jss-${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 || (`jss-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
+ "jss-clear-set",
90
+ "jss-tabs",
91
+ "jss-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["--jss-tabs-gap"] = toCssSize(props.gap);
121
+ if (props.tabGap != null) wrapProps.style["--jss-tabs-tab-gap"] = toCssSize(props.tabGap);
122
+ const accentColor = resolveAccent(props.color ?? props.state);
123
+ if (accentColor) wrapProps.style["--jss-tabs-accent"] = accentColor;
124
+ const wrap = _.div(wrapProps);
125
+
126
+ const bar = _.div({
127
+ class: uiClass(["jss-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(["jss-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: "jss-tabs-tab-inner" },
244
+ iconNodes.length ? _.span({ class: "jss-tabs-tab-icon" }, ...iconNodes) : null,
245
+ _.span({ class: "jss-tabs-tab-copy" },
246
+ _.span({ class: "jss-tabs-tab-label" }, ...(labelNodes.length ? labelNodes : [""])),
247
+ noteNodes.length ? _.span({ class: "jss-tabs-tab-note" }, ...noteNodes) : null
248
+ ),
249
+ badgeNodes.length ? _.span({ class: "jss-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
+ "jss-tabs-item",
303
+ uiWhen(isActive, "active"),
304
+ uiWhen(tab.disabled || disabledAll, "disabled"),
305
+ props.itemClass,
306
+ tab.itemClass
307
+ ]),
308
+ style: {
309
+ ...(itemAccent ? { "--jss-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
+ "jss-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: "jss-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: "jss-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
+
389
+ // Esempio: JSswift.ui.Tabs({ tabs: [{ label: "Overview", value: "overview", icon: "dashboard" }], model: [get,set] })
390
+
391
+ UI.RouteTab = (...args) => {
392
+ const { props, children } = JSswift.uiNormalizeArgs(args);
393
+ const slots = props.slots || {};
394
+ const router = JSswift.router || app?.router || null;
395
+ const hasOwn = (key) => Object.prototype.hasOwnProperty.call(props, key);
396
+ const hasExplicitActive = hasOwn("active") || hasOwn("selected");
397
+ const normalizeVariant = (value) => {
398
+ const raw = String(uiUnwrap(value) || "").toLowerCase();
399
+ if (raw === "pill" || raw === "pills") return "pills";
400
+ if (raw === "soft" || raw === "card") return "soft";
401
+ return "line";
402
+ };
403
+ const resolveAccent = (value) => {
404
+ const raw = uiUnwrap(value);
405
+ if (raw == null || raw === "") return null;
406
+ return JSswift.uiColors?.includes(raw) ? `var(--jss-${raw})` : String(raw);
407
+ };
408
+ const resolvePath = (value) => {
409
+ if (value == null || value === "") return "";
410
+ try {
411
+ const url = new URL(String(value), window.location.origin);
412
+ return url.pathname || "";
413
+ } catch (_) {
414
+ return String(value).split(/[?#]/)[0] || "";
415
+ }
416
+ };
417
+ const isExternalLink = (value) => {
418
+ if (uiUnwrap(props.external) === true) return true;
419
+ if (!value) return false;
420
+ return /^(mailto:|tel:|https?:\/\/|\/\/)/i.test(String(value));
421
+ };
422
+ const getHref = () => uiUnwrap(props.href ?? props.to) || "#";
423
+ const getTo = () => uiUnwrap(props.to ?? props.href) || "";
424
+ const getCurrentPath = () => {
425
+ try {
426
+ if (router?.current) return router.current().pathname || window.location.pathname || "";
427
+ } catch (_) { }
428
+ return window.location.pathname || "";
429
+ };
430
+ const matchCurrentRoute = () => {
431
+ const to = getTo();
432
+ if (!to) return false;
433
+ const matchValue = uiUnwrap(props.match ?? props.activeMatch ?? props.routeMatch);
434
+ const matchMode = String(uiUnwrap(props.matchMode ?? (props.startsWith ? "startsWith" : (props.exact ? "exact" : ""))) || "").toLowerCase();
435
+ const currentPath = getCurrentPath();
436
+ if (typeof matchValue === "function") {
437
+ return !!matchValue({ currentPath, to, href: getHref(), router, props });
438
+ }
439
+ if (matchValue instanceof RegExp) {
440
+ return matchValue.test(currentPath);
441
+ }
442
+ const targetPath = resolvePath(typeof matchValue === "string" && !["exact", "startsWith", "prefix", "section"].includes(matchValue.toLowerCase())
443
+ ? matchValue
444
+ : to);
445
+ if (!targetPath) return false;
446
+ const mode = matchMode || (typeof matchValue === "string" && ["startsWith", "prefix", "section"].includes(matchValue.toLowerCase()) ? "startsWith" : "exact");
447
+ if (mode === "startswith" || mode === "prefix" || mode === "section") {
448
+ return currentPath === targetPath || currentPath.startsWith(`${targetPath}/`);
449
+ }
450
+ if (router?.isActive) return !!router.isActive(to);
451
+ return currentPath === targetPath;
452
+ };
453
+ const getDisabled = () => !!uiUnwrap(props.disabled);
454
+ let manualActive = null;
455
+ const getActive = () => {
456
+ if (manualActive != null) return !!manualActive;
457
+ if (hasExplicitActive) return !!uiUnwrap(props.active ?? props.selected);
458
+ return matchCurrentRoute();
459
+ };
460
+
461
+ const variant = normalizeVariant(props.variant || (props.pills ? "pills" : (props.soft ? "soft" : "line")));
462
+ const normalizedState = !uiUnwrap(props.color) && uiUnwrap(props.state)
463
+ ? normalizeState(uiUnwrap(props.state))
464
+ : "";
465
+ const stateClass = normalizedState ? `jss-state-${normalizedState}` : "";
466
+ const p = JSswift.omit(props, [
467
+ "active", "selected", "label", "title", "text", "to", "icon", "iconRight", "iconSize",
468
+ "note", "subtitle", "caption", "description", "badge", "counter", "count", "aside", "trailing",
469
+ "slots", "state", "color", "dense", "outline", "flat", "glossy", "glow", "glass", "gradient",
470
+ "textGradient", "lightShadow", "shadow", "rounded", "radius", "textColor", "variant", "pills",
471
+ "soft", "block", "fill", "match", "matchMode", "activeMatch", "routeMatch", "exact",
472
+ "startsWith", "external", "onNavigate"
473
+ ]);
474
+ p.href = getHref();
475
+ p.class = uiClass([
476
+ "jss-route-tab",
477
+ "jss-clear-set",
478
+ "jss-singularity",
479
+ `variant-${variant}`,
480
+ uiWhen(props.block ?? props.fill, "block"),
481
+ uiWhen(props.dense, "dense"),
482
+ stateClass,
483
+ props.class
484
+ ]);
485
+ p.style = {
486
+ ...(props.style || {})
487
+ };
488
+
489
+ const accent = resolveAccent(props.color ?? props.state);
490
+ if (accent) p.style["--jss-route-tab-accent"] = accent;
491
+
492
+ const iconSize = props.iconSize ?? props.size ?? null;
493
+ const resolveIconNode = (value, as) => {
494
+ const raw = uiUnwrap(value);
495
+ if (raw == null || raw === false || raw === "") return null;
496
+ if (typeof raw === "string") return UI.Icon({ name: raw, size: iconSize });
497
+ return JSswift.ui.slot(raw, { as });
498
+ };
499
+ const ctx = () => ({
500
+ active: getActive(),
501
+ disabled: getDisabled(),
502
+ href: getHref(),
503
+ to: getTo(),
504
+ external: isExternalLink(getHref()),
505
+ navigate: (event) => {
506
+ if (getDisabled()) return false;
507
+ const nextTo = getTo();
508
+ const external = isExternalLink(getHref());
509
+ if (!external && nextTo && router?.navigate) {
510
+ if (event?.preventDefault) event.preventDefault();
511
+ router.navigate(nextTo);
512
+ props.onNavigate?.(nextTo, event || null);
513
+ return true;
514
+ }
515
+ props.onNavigate?.(nextTo || getHref(), event || null);
516
+ return false;
517
+ }
518
+ });
519
+
520
+ const iconNodes = renderSlotToArray(slots, "icon", ctx(), resolveIconNode(props.icon, "icon"));
521
+ const labelNodes = renderSlotToArray(
522
+ slots,
523
+ "label",
524
+ ctx(),
525
+ props.label ?? props.title ?? props.text ?? children
526
+ );
527
+ const noteNodes = renderSlotToArray(
528
+ slots,
529
+ "note",
530
+ ctx(),
531
+ props.note ?? props.subtitle ?? props.caption ?? props.description
532
+ );
533
+ const badgeNodes = renderSlotToArray(
534
+ slots,
535
+ "badge",
536
+ ctx(),
537
+ props.badge ?? props.counter ?? props.count
538
+ );
539
+ const asideNodes = renderSlotToArray(
540
+ slots,
541
+ "aside",
542
+ ctx(),
543
+ props.aside ?? props.trailing ?? resolveIconNode(props.iconRight, "iconRight")
544
+ );
545
+ const defaultNodes = renderSlotToArray(slots, "default", ctx(), props.content ?? props.body);
546
+
547
+ const content = _.span({ class: "jss-route-tab-inner" },
548
+ iconNodes.length ? _.span({ class: "jss-route-tab-icon" }, ...iconNodes) : null,
549
+ _.span({ class: "jss-route-tab-copy" },
550
+ _.span({ class: "jss-route-tab-label" }, ...(labelNodes.length ? labelNodes : [getTo() || getHref() || ""])),
551
+ noteNodes.length ? _.span({ class: "jss-route-tab-note" }, ...noteNodes) : null,
552
+ defaultNodes.length ? _.span({ class: "jss-route-tab-extra" }, ...defaultNodes) : null
553
+ ),
554
+ badgeNodes.length ? _.span({ class: "jss-route-tab-badge" }, ...badgeNodes) : null,
555
+ asideNodes.length ? _.span({ class: "jss-route-tab-aside" }, ...asideNodes) : null
556
+ );
557
+
558
+ const wrap = _.a(p, content);
559
+ setPropertyProps(wrap, props);
560
+
561
+ const syncState = () => {
562
+ const active = getActive();
563
+ const disabled = getDisabled();
564
+ const href = getHref();
565
+ const external = isExternalLink(href);
566
+ wrap.classList.toggle("active", active);
567
+ wrap.classList.toggle("is-disabled", disabled);
568
+ wrap.classList.toggle("is-external", external);
569
+ if (active) wrap.setAttribute("aria-current", String(uiUnwrap(props.ariaCurrent) || "page"));
570
+ else wrap.removeAttribute("aria-current");
571
+ wrap.setAttribute("href", href || "#");
572
+ if (disabled) {
573
+ wrap.setAttribute("aria-disabled", "true");
574
+ wrap.tabIndex = -1;
575
+ } else {
576
+ wrap.removeAttribute("aria-disabled");
577
+ if (!hasOwn("tabIndex") && !hasOwn("tabindex")) wrap.removeAttribute("tabindex");
578
+ }
579
+ if (getTo()) wrap.dataset.to = String(getTo());
580
+ else delete wrap.dataset.to;
581
+ if (external && wrap.target === "_blank" && !wrap.rel) wrap.rel = "noopener noreferrer";
582
+ };
583
+
584
+ const userOnClick = props.onClick;
585
+ wrap.onclick = (e) => {
586
+ userOnClick?.(e);
587
+ if (e.defaultPrevented) return;
588
+ if (getDisabled()) {
589
+ e.preventDefault();
590
+ return;
591
+ }
592
+ ctx().navigate(e);
593
+ };
594
+
595
+ if (!hasExplicitActive && getTo() && router?.subscribe) {
596
+ router.subscribe(() => syncState());
597
+ }
598
+ JSswift.reactive.effect(() => {
599
+ syncState();
600
+ }, "UI.RouteTab:render");
601
+
602
+ wrap._isActive = () => getActive();
603
+ wrap._setActive = (value) => {
604
+ manualActive = value == null ? null : !!value;
605
+ syncState();
606
+ return wrap;
607
+ };
608
+ wrap._navigate = (event) => ctx().navigate(event);
609
+ return wrap;
610
+ };
611
+
612
+ // Esempio: JSswift.ui.RouteTab({ label: "Home", to: "/" })
613
+
614
+ UI.Breadcrumbs = (...args) => {
615
+ const { props, children } = JSswift.uiNormalizeArgs(args);
616
+ const slots = props.slots || {};
617
+ const router = JSswift.router || app?.router || null;
618
+ const model = resolveModel(props.model, "UI.Breadcrumbs:model");
619
+ const sizeClass = uiComputed(props.size, () => {
620
+ const v = uiUnwrap(props.size);
621
+ return (typeof v === "string" && JSswift.uiSizes?.includes(v)) ? `jss-size-${v}` : "";
622
+ });
623
+ const variantClass = uiComputed(props.variant, () => {
624
+ const raw = String(uiUnwrap(props.variant || (props.pills ? "pills" : (props.soft ? "soft" : "line"))) || "").toLowerCase();
625
+ if (raw === "pill" || raw === "pills") return "variant-pills";
626
+ if (raw === "soft" || raw === "card") return "variant-soft";
627
+ return "variant-line";
628
+ });
629
+ const nowrapClass = uiComputed([props.wrap, props.nowrap], () => {
630
+ return (uiUnwrap(props.wrap) === false || !!uiUnwrap(props.nowrap)) ? "no-wrap" : "";
631
+ });
632
+ const wrap = _.nav({
633
+ class: uiClass([
634
+ "jss-breadcrumbs",
635
+ "jss-clear-set",
636
+ sizeClass,
637
+ variantClass,
638
+ nowrapClass,
639
+ uiWhen(props.dense, "dense"),
640
+ props.class
641
+ ]),
642
+ style: { ...(props.style || {}) },
643
+ role: "navigation",
644
+ "aria-label": props.ariaLabel || "Breadcrumb"
645
+ });
646
+ const beforeEl = _.div({ class: "jss-breadcrumbs-before" });
647
+ const listEl = _.ol({ class: "jss-breadcrumbs-list" });
648
+ const afterEl = _.div({ class: "jss-breadcrumbs-after" });
649
+ wrap.appendChild(beforeEl);
650
+ wrap.appendChild(listEl);
651
+ wrap.appendChild(afterEl);
652
+
653
+ const resolveAccent = (value) => {
654
+ const raw = uiUnwrap(value);
655
+ if (raw == null || raw === "") return null;
656
+ return JSswift.uiColors?.includes(raw) ? `var(--jss-${raw})` : String(raw);
657
+ };
658
+ const isExternalLink = (entry) => {
659
+ if (!entry) return false;
660
+ if (uiUnwrap(entry.external) === true) return true;
661
+ const href = uiUnwrap(entry.href ?? entry.to);
662
+ if (!href) return false;
663
+ return /^(mailto:|tel:|https?:\/\/|\/\/)/i.test(String(href));
664
+ };
665
+ const cloneIfNode = (value) => {
666
+ if (value?.nodeType && value.cloneNode) return value.cloneNode(true);
667
+ return value;
668
+ };
669
+ const renderInto = (host, nodes) => {
670
+ host.innerHTML = "";
671
+ nodes.forEach((node) => host.appendChild(node));
672
+ };
673
+ const renderNamedSlot = (slotBag, name, ctx, fallback, alias = null) => {
674
+ const primary = JSswift.ui.getSlot(slotBag, name) != null
675
+ ? renderSlotToArray(slotBag, name, ctx, fallback)
676
+ : [];
677
+ if (primary.length) return primary;
678
+ if (alias && JSswift.ui.getSlot(slotBag, alias) != null) {
679
+ return renderSlotToArray(slotBag, alias, ctx, fallback);
680
+ }
681
+ return renderSlotToArray(null, "default", ctx, fallback);
682
+ };
683
+ const resolveIconNode = (value, size, as) => {
684
+ const raw = uiUnwrap(value);
685
+ if (raw == null || raw === false || raw === "") return null;
686
+ if (typeof raw === "string") return UI.Icon({ name: raw, size });
687
+ return JSswift.ui.slot(raw, { as });
688
+ };
689
+ const normalizeItems = () => {
690
+ const source = model ? model.get() : uiUnwrap(props.items ?? props.value ?? props.breadcrumbs);
691
+ const list = [];
692
+ const pushItem = (entry) => {
693
+ if (entry == null || entry === false) return;
694
+ if (Array.isArray(entry)) {
695
+ entry.forEach(pushItem);
696
+ return;
697
+ }
698
+ if (typeof entry === "string" || typeof entry === "number") {
699
+ list.push({ label: String(entry) });
700
+ return;
701
+ }
702
+ if (entry?.nodeType || typeof entry === "function") {
703
+ list.push({ label: entry });
704
+ return;
705
+ }
706
+ list.push(entry);
707
+ };
708
+
709
+ const home = uiUnwrap(props.home);
710
+ if (home) {
711
+ pushItem(home === true ? { label: "Home", to: "/", icon: "home" } : home);
712
+ }
713
+ pushItem(source);
714
+ if (!list.length && children?.length) {
715
+ children.forEach((child) => pushItem(child));
716
+ }
717
+ return list.filter((item) => !uiUnwrap(item?.hidden));
718
+ };
719
+ const getCurrentIndex = (items) => {
720
+ const explicitIndex = items.findIndex((item) => !!uiUnwrap(item?.current ?? item?.active ?? item?.selected));
721
+ if (explicitIndex > -1) return explicitIndex;
722
+ return uiUnwrap(props.autoCurrent) === false ? -1 : (items.length ? items.length - 1 : -1);
723
+ };
724
+ const collapseItems = (items) => {
725
+ const maxRaw = Number(uiUnwrap(props.max ?? props.maxItems ?? props.collapseAfter ?? props.collapse));
726
+ const max = Number.isFinite(maxRaw) ? Math.max(0, Math.trunc(maxRaw)) : 0;
727
+ if (!max || max < 3 || items.length <= max) {
728
+ return items.map((item, index) => ({ type: "item", item, index, key: `item-${index}` }));
729
+ }
730
+
731
+ let leading = Number(uiUnwrap(props.leadingCount ?? props.keepStart ?? 1));
732
+ let trailing = Number(uiUnwrap(props.trailingCount ?? props.keepEnd ?? Math.max(1, max - 2)));
733
+ leading = Number.isFinite(leading) ? Math.max(1, Math.trunc(leading)) : 1;
734
+ trailing = Number.isFinite(trailing) ? Math.max(1, Math.trunc(trailing)) : Math.max(1, max - 2);
735
+ if (leading + trailing + 1 > max) {
736
+ trailing = Math.max(1, max - leading - 1);
737
+ if (leading + trailing + 1 > max) leading = Math.max(1, max - trailing - 1);
738
+ }
739
+ if (leading + trailing >= items.length) {
740
+ return items.map((item, index) => ({ type: "item", item, index, key: `item-${index}` }));
741
+ }
742
+
743
+ const entries = [];
744
+ for (let i = 0; i < leading; i++) {
745
+ entries.push({ type: "item", item: items[i], index: i, key: `item-${i}` });
746
+ }
747
+ const hiddenItems = items.slice(leading, items.length - trailing);
748
+ entries.push({
749
+ type: "collapsed",
750
+ key: `collapsed-${leading}-${items.length - trailing}`,
751
+ items: hiddenItems,
752
+ from: leading,
753
+ to: items.length - trailing - 1
754
+ });
755
+ for (let i = items.length - trailing; i < items.length; i++) {
756
+ entries.push({ type: "item", item: items[i], index: i, key: `item-${i}` });
757
+ }
758
+ return entries;
759
+ };
760
+ const navigateItem = (item, event = null) => {
761
+ if (!item) return false;
762
+ const href = uiUnwrap(item.href ?? item.to) || "";
763
+ const to = uiUnwrap(item.to) || "";
764
+ if (!isExternalLink(item) && to && router?.navigate) {
765
+ if (event?.preventDefault) event.preventDefault();
766
+ router.navigate(to);
767
+ item.onNavigate?.(to, event || null);
768
+ props.onNavigate?.(to, item, event || null);
769
+ return true;
770
+ }
771
+ item.onNavigate?.(to || href, event || null);
772
+ props.onNavigate?.(to || href, item, event || null);
773
+ return false;
774
+ };
775
+ const createSeparator = (entry, index, total) => {
776
+ const prev = entry;
777
+ const next = total[index + 1] || null;
778
+ const ctx = { index, entry, previous: prev, next };
779
+ const fallback = cloneIfNode(uiUnwrap((entry?.type === "item" ? entry.item?.separator : null) ?? props.separator ?? "/"));
780
+ const nodes = renderNamedSlot(slots, "separator", ctx, fallback);
781
+ return _.li({ class: "jss-breadcrumbs-separator-item", "aria-hidden": "true" },
782
+ _.span({ class: "jss-breadcrumbs-separator" }, ...nodes)
783
+ );
784
+ };
785
+ const createCollapsedEntry = (entry, items, currentIndex) => {
786
+ const ctx = {
787
+ type: "collapsed",
788
+ collapsed: true,
789
+ items,
790
+ hiddenItems: entry.items,
791
+ currentIndex,
792
+ from: entry.from,
793
+ to: entry.to
794
+ };
795
+ const fallback = _.span({ class: "jss-breadcrumb jss-breadcrumb-collapsed" },
796
+ _.span({ class: "jss-breadcrumb-inner" },
797
+ _.span({ class: "jss-breadcrumb-copy" },
798
+ _.span({ class: "jss-breadcrumb-label" }, String(uiUnwrap(props.collapsedLabel) || "…"))
799
+ )
800
+ )
801
+ );
802
+ const nodes = renderNamedSlot(slots, "collapsed", ctx, fallback, "item");
803
+ return _.li({ class: "jss-breadcrumbs-entry is-collapsed" }, ...nodes);
804
+ };
805
+ const createItemEntry = (entry, items, currentIndex, visibleIndex) => {
806
+ const item = entry.item || {};
807
+ const slotBag = item.slots ? { ...slots, ...item.slots } : slots;
808
+ const href = uiUnwrap(item.href ?? item.to) || "";
809
+ const disabled = !!uiUnwrap(item.disabled);
810
+ const current = entry.index === currentIndex;
811
+ const iconSize = item.iconSize ?? props.itemSize ?? props.size ?? null;
812
+ const accent = resolveAccent(item.color ?? item.state ?? props.color ?? props.state);
813
+ const state = !uiUnwrap(item.color) && uiUnwrap(item.state)
814
+ ? normalizeState(uiUnwrap(item.state))
815
+ : "";
816
+ const labelFallback = item.label ?? item.title ?? item.text ?? item.to ?? item.href ?? "";
817
+ const noteFallback = item.note ?? item.subtitle ?? item.caption ?? item.description;
818
+ const badgeFallback = item.badge ?? item.counter ?? item.count;
819
+ const asideFallback = item.aside ?? item.trailing ?? resolveIconNode(item.iconRight, iconSize, "iconRight");
820
+ const defaultFallback = item.content ?? item.body;
821
+ const ctx = {
822
+ item,
823
+ index: entry.index,
824
+ visibleIndex,
825
+ total: items.length,
826
+ current,
827
+ active: current,
828
+ disabled,
829
+ href,
830
+ to: uiUnwrap(item.to) || "",
831
+ external: isExternalLink(item),
832
+ isFirst: entry.index === 0,
833
+ isLast: entry.index === items.length - 1,
834
+ navigate: (event) => navigateItem(item, event)
835
+ };
836
+
837
+ const iconNodes = renderNamedSlot(slotBag, "icon", ctx, resolveIconNode(item.icon, iconSize, "icon"));
838
+ const labelNodes = renderNamedSlot(slotBag, "label", ctx, labelFallback);
839
+ const noteNodes = renderNamedSlot(slotBag, "note", ctx, noteFallback);
840
+ const badgeNodes = renderNamedSlot(slotBag, "badge", ctx, badgeFallback);
841
+ const asideNodes = renderNamedSlot(slotBag, "aside", ctx, asideFallback);
842
+ const defaultNodes = renderNamedSlot(slotBag, "default", ctx, defaultFallback);
843
+
844
+ const content = _.span({ class: "jss-breadcrumb-inner" },
845
+ iconNodes.length ? _.span({ class: "jss-breadcrumb-icon" }, ...iconNodes) : null,
846
+ _.span({ class: "jss-breadcrumb-copy" },
847
+ _.span({ class: "jss-breadcrumb-label" }, ...(labelNodes.length ? labelNodes : [href || ""])),
848
+ noteNodes.length ? _.span({ class: "jss-breadcrumb-note" }, ...noteNodes) : null,
849
+ defaultNodes.length ? _.span({ class: "jss-breadcrumb-extra" }, ...defaultNodes) : null
850
+ ),
851
+ badgeNodes.length ? _.span({ class: "jss-breadcrumb-badge" }, ...badgeNodes) : null,
852
+ asideNodes.length ? _.span({ class: "jss-breadcrumb-aside" }, ...asideNodes) : null
853
+ );
854
+
855
+ const interactive = !disabled && (!current || !!uiUnwrap(item.linkCurrent ?? props.linkCurrent));
856
+ const sharedProps = {
857
+ class: uiClass([
858
+ "jss-breadcrumb",
859
+ "jss-clear-set",
860
+ "jss-singularity",
861
+ uiWhen(current, "is-current"),
862
+ uiWhen(current, "active"),
863
+ uiWhen(disabled, "is-disabled"),
864
+ state ? `jss-state-${state}` : "",
865
+ item.class
866
+ ]),
867
+ style: {
868
+ ...(item.style || {})
869
+ },
870
+ "aria-current": current ? String(uiUnwrap(item.ariaCurrent ?? props.ariaCurrent) || "page") : null
871
+ };
872
+ if (accent) sharedProps.style["--jss-breadcrumb-accent"] = accent;
873
+
874
+ let node = null;
875
+ if (interactive && href) {
876
+ node = _.a({
877
+ ...sharedProps,
878
+ href,
879
+ target: item.target,
880
+ rel: item.rel,
881
+ onClick: (event) => {
882
+ item.onClick?.(event, ctx);
883
+ props.onItemClick?.(item, ctx, event);
884
+ if (event.defaultPrevented) return;
885
+ ctx.navigate(event);
886
+ }
887
+ }, content);
888
+ if (ctx.external && node.target === "_blank" && !node.rel) node.rel = "noopener noreferrer";
889
+ } else if (interactive && typeof item.onClick === "function") {
890
+ node = _.button({
891
+ ...sharedProps,
892
+ type: item.type || "button",
893
+ onClick: (event) => {
894
+ item.onClick?.(event, ctx);
895
+ props.onItemClick?.(item, ctx, event);
896
+ }
897
+ }, content);
898
+ } else {
899
+ node = _.span({
900
+ ...sharedProps,
901
+ "aria-disabled": disabled ? "true" : null
902
+ }, content);
903
+ }
904
+
905
+ setPropertyProps(node, {
906
+ size: item.size ?? props.itemSize ?? props.size,
907
+ radius: item.radius ?? props.radius,
908
+ gradient: item.gradient ?? props.gradient
909
+ });
910
+
911
+ const fallback = node;
912
+ const rendered = renderNamedSlot(slotBag, "item", ctx, fallback);
913
+ return _.li({
914
+ class: uiClass([
915
+ "jss-breadcrumbs-entry",
916
+ uiWhen(current, "is-current"),
917
+ uiWhen(disabled, "is-disabled")
918
+ ])
919
+ }, ...rendered);
920
+ };
921
+ const render = () => {
922
+ const items = normalizeItems();
923
+ const currentIndex = getCurrentIndex(items);
924
+ const visibleItems = collapseItems(items);
925
+ const accent = resolveAccent(props.color ?? props.state);
926
+ wrap.classList.toggle("is-empty", items.length === 0);
927
+ if (accent) wrap.style.setProperty("--jss-breadcrumb-accent", accent);
928
+ else wrap.style.removeProperty("--jss-breadcrumb-accent");
929
+
930
+ const rootCtx = {
931
+ items,
932
+ visibleItems,
933
+ currentIndex,
934
+ count: items.length,
935
+ collapsed: visibleItems.some((entry) => entry.type === "collapsed")
936
+ };
937
+ renderInto(beforeEl, renderNamedSlot(slots, "before", rootCtx, props.before));
938
+ renderInto(afterEl, renderNamedSlot(slots, "after", rootCtx, props.after));
939
+
940
+ listEl.innerHTML = "";
941
+ if (!items.length) {
942
+ const emptyNodes = renderNamedSlot(slots, "empty", rootCtx, props.empty);
943
+ if (emptyNodes.length) listEl.appendChild(_.li({ class: "jss-breadcrumbs-entry is-empty" }, ...emptyNodes));
944
+ return;
945
+ }
946
+
947
+ visibleItems.forEach((entry, visibleIndex) => {
948
+ if (visibleIndex > 0) listEl.appendChild(createSeparator(visibleItems[visibleIndex - 1], visibleIndex - 1, visibleItems));
949
+ listEl.appendChild(
950
+ entry.type === "collapsed"
951
+ ? createCollapsedEntry(entry, items, currentIndex)
952
+ : createItemEntry(entry, items, currentIndex, visibleIndex)
953
+ );
954
+ });
955
+ };
956
+
957
+ app.reactive.effect(() => {
958
+ render();
959
+ }, "UI.Breadcrumbs:render");
960
+
961
+ wrap.getItems = () => normalizeItems();
962
+ wrap.getVisibleItems = () => collapseItems(normalizeItems());
963
+ wrap.refresh = () => {
964
+ render();
965
+ return wrap;
966
+ };
967
+ wrap.setItems = (next) => {
968
+ if (model) model.set(next);
969
+ else props.items = next;
970
+ render();
971
+ return wrap;
972
+ };
973
+ setPropertyProps(wrap, props);
974
+ return wrap;
975
+ };
976
+
977
+ // Esempio: JSswift.ui.Breadcrumbs({ items: [{ label: "Home", to: "/" }, { label: "Pagina" }] })
978
+
979
+ UI.Pagination = (...args) => {
980
+ const { props } = JSswift.uiNormalizeArgs(args);
981
+ const slots = props.slots || {};
982
+ const model = resolveModel(props.model, "UI.Pagination:model");
983
+ const sizeClass = uiComputed(props.size, () => {
984
+ const v = uiUnwrap(props.size);
985
+ return (typeof v === "string" && sizeMap[v]) ? `jss-size-${v}` : "";
986
+ });
987
+ const wrap = _.nav({
988
+ class: uiClass([
989
+ "jss-pagination",
990
+ "jss-clear-set",
991
+ sizeClass,
992
+ uiWhen(props.dense, "dense"),
993
+ uiWhen(props.simple, "is-simple"),
994
+ uiWhen(props.disabled, "is-disabled"),
995
+ props.class
996
+ ]),
997
+ style: { ...(props.style || {}) },
998
+ role: "navigation",
999
+ "aria-label": props.ariaLabel || "Pagination"
1000
+ });
1001
+ const controls = _.div({ class: "jss-pagination-controls" });
1002
+ const startGroup = _.div({ class: "jss-pagination-start" });
1003
+ const pagesGroup = _.div({ class: "jss-pagination-pages" });
1004
+ const endGroup = _.div({ class: "jss-pagination-end" });
1005
+ const summary = _.div({ class: "jss-pagination-summary" });
1006
+
1007
+ controls.appendChild(startGroup);
1008
+ controls.appendChild(pagesGroup);
1009
+ controls.appendChild(endGroup);
1010
+ wrap.appendChild(controls);
1011
+ wrap.appendChild(summary);
1012
+
1013
+ let currentPage = 1;
1014
+ let currentPages = 1;
1015
+
1016
+ const toInt = (value, fallback = 0) => {
1017
+ const raw = uiUnwrap(value);
1018
+ if (raw == null || raw === "") return fallback;
1019
+ const n = Number(raw);
1020
+ if (!Number.isFinite(n)) return fallback;
1021
+ return Math.trunc(n);
1022
+ };
1023
+ const clampPage = (page, pages = getPages()) => {
1024
+ const raw = Number(page);
1025
+ const next = Number.isFinite(raw) ? Math.trunc(raw) : 1;
1026
+ return Math.min(Math.max(next || 1, 1), Math.max(1, pages || 1));
1027
+ };
1028
+ const getPages = () => {
1029
+ const explicit = toInt(props.max ?? props.pages ?? props.pageCount ?? props.totalPages, 0);
1030
+ if (explicit > 0) return explicit;
1031
+ const total = toInt(props.total ?? props.totalItems ?? props.count, 0);
1032
+ const pageSize = toInt(props.pageSize ?? props.perPage ?? props.limit, 0);
1033
+ if (total >= 0 && pageSize > 0) return Math.max(1, Math.ceil(total / pageSize));
1034
+ return 1;
1035
+ };
1036
+ const getRequestedPage = () => {
1037
+ if (model) return toInt(model.get(), 1);
1038
+ return toInt(props.page ?? props.value ?? props.current ?? props.currentPage, 1);
1039
+ };
1040
+ const getSiblingCount = () => Math.max(0, toInt(props.siblings ?? props.siblingCount ?? props.window, 1));
1041
+ const getBoundaryCount = () => Math.max(0, toInt(props.boundaryCount ?? props.boundaries ?? props.edges, 1));
1042
+ const isDisabled = () => !!uiUnwrap(props.disabled);
1043
+ const showSummary = () => uiUnwrap(props.showSummary ?? props.showLabel) !== false;
1044
+ const showNumbers = () => uiUnwrap(props.showPages) !== false && !uiUnwrap(props.simple);
1045
+ const showPrev = () => uiUnwrap(props.showPrev) !== false;
1046
+ const showNext = () => uiUnwrap(props.showNext) !== false;
1047
+ const showEdges = () => !!uiUnwrap(props.showEdges);
1048
+ const showFirst = () => {
1049
+ const value = uiUnwrap(props.showFirst);
1050
+ return value == null ? showEdges() : !!value;
1051
+ };
1052
+ const showLast = () => {
1053
+ const value = uiUnwrap(props.showLast);
1054
+ return value == null ? showEdges() : !!value;
1055
+ };
1056
+ const hideOnSinglePage = () => !!uiUnwrap(props.hideOnSinglePage);
1057
+ const getTotal = () => {
1058
+ const total = uiUnwrap(props.total ?? props.totalItems ?? props.count);
1059
+ if (total == null || total === "") return null;
1060
+ const n = Number(total);
1061
+ return Number.isFinite(n) ? Math.max(0, Math.trunc(n)) : null;
1062
+ };
1063
+ const getPageSize = () => {
1064
+ const pageSize = uiUnwrap(props.pageSize ?? props.perPage ?? props.limit);
1065
+ if (pageSize == null || pageSize === "") return null;
1066
+ const n = Number(pageSize);
1067
+ return Number.isFinite(n) && n > 0 ? Math.trunc(n) : null;
1068
+ };
1069
+ const getCtx = (page = currentPage, pages = currentPages) => {
1070
+ const total = getTotal();
1071
+ const pageSize = getPageSize();
1072
+ const from = total != null && pageSize ? (total === 0 ? 0 : ((page - 1) * pageSize) + 1) : null;
1073
+ const to = total != null && pageSize ? Math.min(total, page * pageSize) : null;
1074
+ return {
1075
+ page,
1076
+ value: page,
1077
+ current: page,
1078
+ currentPage: page,
1079
+ pages,
1080
+ max: pages,
1081
+ total,
1082
+ pageSize,
1083
+ from,
1084
+ to,
1085
+ disabled: isDisabled(),
1086
+ isFirst: page <= 1,
1087
+ isLast: page >= pages,
1088
+ canPrev: page > 1,
1089
+ canNext: page < pages,
1090
+ setPage: (nextPage, event) => commitPage(nextPage, event),
1091
+ prev: (event) => commitPage(page - 1, event),
1092
+ next: (event) => commitPage(page + 1, event),
1093
+ first: (event) => commitPage(1, event),
1094
+ last: (event) => commitPage(pages, event)
1095
+ };
1096
+ };
1097
+ const buildItems = (page, pages) => {
1098
+ if (pages <= 0) return [];
1099
+ const items = [];
1100
+ const included = new Set();
1101
+ const boundary = getBoundaryCount();
1102
+ const siblings = getSiblingCount();
1103
+
1104
+ for (let i = 1; i <= Math.min(boundary, pages); i++) included.add(i);
1105
+ for (let i = Math.max(1, page - siblings); i <= Math.min(pages, page + siblings); i++) included.add(i);
1106
+ for (let i = Math.max(1, pages - boundary + 1); i <= pages; i++) included.add(i);
1107
+
1108
+ let last = 0;
1109
+ for (let i = 1; i <= pages; i++) {
1110
+ if (!included.has(i)) continue;
1111
+ if (last && i - last > 1) items.push({ type: "ellipsis", key: `ellipsis-${last}-${i}` });
1112
+ items.push({ type: "page", page: i, key: `page-${i}` });
1113
+ last = i;
1114
+ }
1115
+ if (!items.length) items.push({ type: "page", page: 1, key: "page-1" });
1116
+ return items;
1117
+ };
1118
+ const renderNamedSlot = (name, fallback, ctx, alias = null) => {
1119
+ const primary = JSswift.ui.getSlot(slots, name) != null
1120
+ ? renderSlotToArray(slots, name, ctx, fallback)
1121
+ : [];
1122
+ if (primary.length) return primary;
1123
+ if (alias && JSswift.ui.getSlot(slots, alias) != null) {
1124
+ return renderSlotToArray(slots, alias, ctx, fallback);
1125
+ }
1126
+ return renderSlotToArray(null, "default", ctx, fallback);
1127
+ };
1128
+ const renderInto = (host, nodes) => {
1129
+ host.innerHTML = "";
1130
+ nodes.forEach((node) => host.appendChild(node));
1131
+ };
1132
+ const createButton = (name, fallback, onClick, options = {}) => {
1133
+ const ctx = { ...getCtx(), kind: name, active: !!options.active };
1134
+ return UI.Btn({
1135
+ class: uiClass([
1136
+ "jss-pagination-btn",
1137
+ `jss-pagination-${name}`,
1138
+ uiWhen(options.active, "active"),
1139
+ options.class
1140
+ ]),
1141
+ size: props.itemSize || props.size || (props.dense ? "sm" : "md"),
1142
+ color: options.active ? (props.color || props.state || "primary") : (options.color || null),
1143
+ outline: options.active ? false : (options.outline ?? true),
1144
+ disabled: isDisabled() || !!options.disabled,
1145
+ "aria-current": options.active ? "page" : null,
1146
+ onClick: (event) => {
1147
+ if (isDisabled() || options.disabled) return;
1148
+ onClick?.(event);
1149
+ }
1150
+ }, ...renderNamedSlot(name, fallback, ctx, name === "summary" ? "label" : (name === "page" ? "item" : null)));
1151
+ };
1152
+ const render = (page, pages) => {
1153
+ currentPage = clampPage(page, pages);
1154
+ currentPages = Math.max(1, pages || 1);
1155
+ const ctx = getCtx(currentPage, currentPages);
1156
+
1157
+ wrap.style.display = hideOnSinglePage() && currentPages <= 1 ? "none" : "";
1158
+ summary.style.display = showSummary() ? "" : "none";
1159
+ pagesGroup.style.display = showNumbers() ? "" : "none";
1160
+
1161
+ startGroup.innerHTML = "";
1162
+ pagesGroup.innerHTML = "";
1163
+ endGroup.innerHTML = "";
1164
+
1165
+ const startNodes = renderNamedSlot("start", props.start, ctx);
1166
+ if (startNodes.length) {
1167
+ startGroup.appendChild(_.div({ class: "jss-pagination-extra jss-pagination-extra-start" }, ...startNodes));
1168
+ }
1169
+
1170
+ if (showFirst()) {
1171
+ startGroup.appendChild(createButton("first", "«", (event) => commitPage(1, event), {
1172
+ disabled: currentPage <= 1
1173
+ }));
1174
+ }
1175
+ if (showPrev()) {
1176
+ startGroup.appendChild(createButton("prev", "Prev", (event) => commitPage(currentPage - 1, event), {
1177
+ disabled: currentPage <= 1
1178
+ }));
1179
+ }
1180
+
1181
+ if (showNumbers()) {
1182
+ buildItems(currentPage, currentPages).forEach((item) => {
1183
+ if (item.type === "ellipsis") {
1184
+ const ellipsisCtx = { ...ctx, kind: "ellipsis" };
1185
+ const ellipsisNodes = renderNamedSlot("ellipsis", "…", ellipsisCtx);
1186
+ pagesGroup.appendChild(_.span({ class: "jss-pagination-ellipsis", "aria-hidden": "true" }, ...ellipsisNodes));
1187
+ return;
1188
+ }
1189
+ const itemPage = item.page;
1190
+ const itemCtx = { ...ctx, page: itemPage, value: itemPage, item, active: itemPage === currentPage };
1191
+ pagesGroup.appendChild(UI.Btn({
1192
+ class: uiClass([
1193
+ "jss-pagination-btn",
1194
+ "jss-pagination-page-btn",
1195
+ uiWhen(itemPage === currentPage, "active")
1196
+ ]),
1197
+ size: props.itemSize || props.size || (props.dense ? "sm" : "md"),
1198
+ color: itemPage === currentPage ? (props.color || props.state || "primary") : null,
1199
+ outline: itemPage === currentPage ? false : true,
1200
+ disabled: isDisabled(),
1201
+ "aria-current": itemPage === currentPage ? "page" : null,
1202
+ onClick: (event) => commitPage(itemPage, event)
1203
+ }, ...renderNamedSlot("page", String(itemPage), itemCtx, "item")));
1204
+ });
1205
+ }
1206
+
1207
+ if (showNext()) {
1208
+ endGroup.appendChild(createButton("next", "Next", (event) => commitPage(currentPage + 1, event), {
1209
+ disabled: currentPage >= currentPages
1210
+ }));
1211
+ }
1212
+ if (showLast()) {
1213
+ endGroup.appendChild(createButton("last", "»", (event) => commitPage(currentPages, event), {
1214
+ disabled: currentPage >= currentPages
1215
+ }));
1216
+ }
1217
+
1218
+ const endNodes = renderNamedSlot("end", props.end, ctx);
1219
+ if (endNodes.length) {
1220
+ endGroup.appendChild(_.div({ class: "jss-pagination-extra jss-pagination-extra-end" }, ...endNodes));
1221
+ }
1222
+
1223
+ let summaryText = `Pagina ${currentPage} di ${currentPages}`;
1224
+ if (ctx.total != null && ctx.pageSize) {
1225
+ summaryText = ctx.total === 0
1226
+ ? "0 risultati"
1227
+ : `${ctx.from}-${ctx.to} di ${ctx.total}`;
1228
+ }
1229
+ renderInto(summary, renderNamedSlot("summary", summaryText, ctx, "label"));
1230
+ };
1231
+ const commitPage = (page, event = null) => {
1232
+ const pages = getPages();
1233
+ const nextPage = clampPage(page, pages);
1234
+ if (nextPage === currentPage && pages === currentPages) return nextPage;
1235
+ render(nextPage, pages);
1236
+ if (model && model.get() !== nextPage) model.set(nextPage);
1237
+ props.onChange?.(nextPage, getCtx(nextPage, pages), event);
1238
+ props.onPageChange?.(nextPage, getCtx(nextPage, pages), event);
1239
+ return nextPage;
1240
+ };
1241
+
1242
+ app.reactive.effect(() => {
1243
+ const pages = getPages();
1244
+ const requested = getRequestedPage();
1245
+ const nextPage = clampPage(requested, pages);
1246
+ render(nextPage, pages);
1247
+ if (model && requested !== nextPage) model.set(nextPage);
1248
+ }, "UI.Pagination:sync");
1249
+
1250
+ wrap.getPage = () => currentPage;
1251
+ wrap.getPages = () => currentPages;
1252
+ wrap.setPage = (page) => commitPage(page);
1253
+ wrap.prev = () => commitPage(currentPage - 1);
1254
+ wrap.next = () => commitPage(currentPage + 1);
1255
+ wrap.first = () => commitPage(1);
1256
+ wrap.last = () => commitPage(currentPages);
1257
+ setPropertyProps(wrap, props);
1258
+ return wrap;
1259
+ };
1260
+
1261
+ // Esempio: JSswift.ui.Pagination({ total: 120, pageSize: 12, model: [get,set], showEdges: true })
1262
+