@agent-ui-kit/components 0.0.3
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.
- package/dist/base-styles.css +8 -0
- package/dist/component-styles.css +147 -0
- package/dist/controls/_base/index.d.ts +3 -0
- package/dist/controls/_base/index.js +9 -0
- package/dist/controls/_base/indicator-element.d.ts +132 -0
- package/dist/controls/_base/indicator-element.js +158 -0
- package/dist/controls/_base/listbox-element.d.ts +70 -0
- package/dist/controls/_base/listbox-element.js +74 -0
- package/dist/controls/_base/range-element.d.ts +120 -0
- package/dist/controls/_base/range-element.js +158 -0
- package/dist/controls/_surface/container-box.css +301 -0
- package/dist/controls/_surface/container.css +156 -0
- package/dist/controls/_token-surface/token-surface.d.ts +35 -0
- package/dist/controls/_token-surface/token-surface.js +97 -0
- package/dist/controls/attachment/attachment-meta.d.ts +8 -0
- package/dist/controls/attachment/attachment-meta.js +84 -0
- package/dist/controls/attachment/attachment.css +91 -0
- package/dist/controls/attachment/attachment.d.ts +40 -0
- package/dist/controls/attachment/attachment.js +70 -0
- package/dist/controls/avatar/avatar-initials.d.ts +4 -0
- package/dist/controls/avatar/avatar-initials.js +30 -0
- package/dist/controls/avatar/avatar.css +87 -0
- package/dist/controls/avatar/avatar.d.ts +40 -0
- package/dist/controls/avatar/avatar.js +77 -0
- package/dist/controls/badge/badge.css +175 -0
- package/dist/controls/badge/badge.d.ts +36 -0
- package/dist/controls/badge/badge.js +71 -0
- package/dist/controls/bar-chart/bar-chart.css +110 -0
- package/dist/controls/bar-chart/bar-chart.d.ts +26 -0
- package/dist/controls/bar-chart/bar-chart.js +80 -0
- package/dist/controls/bar-chart/bar-math.d.ts +31 -0
- package/dist/controls/bar-chart/bar-math.js +106 -0
- package/dist/controls/button/button.css +263 -0
- package/dist/controls/button/button.d.ts +61 -0
- package/dist/controls/button/button.js +115 -0
- package/dist/controls/calendar/calendar.css +495 -0
- package/dist/controls/calendar/calendar.d.ts +161 -0
- package/dist/controls/calendar/calendar.js +1004 -0
- package/dist/controls/card/card-content.d.ts +24 -0
- package/dist/controls/card/card-content.js +133 -0
- package/dist/controls/card/card-footer.d.ts +3 -0
- package/dist/controls/card/card-footer.js +16 -0
- package/dist/controls/card/card-header.d.ts +3 -0
- package/dist/controls/card/card-header.js +18 -0
- package/dist/controls/card/card.css +445 -0
- package/dist/controls/card/card.d.ts +51 -0
- package/dist/controls/card/card.js +61 -0
- package/dist/controls/checkbox/checkbox.css +204 -0
- package/dist/controls/checkbox/checkbox.d.ts +114 -0
- package/dist/controls/checkbox/checkbox.js +130 -0
- package/dist/controls/code/code.css +72 -0
- package/dist/controls/code/code.d.ts +23 -0
- package/dist/controls/code/code.js +40 -0
- package/dist/controls/color-picker/color-picker.css +209 -0
- package/dist/controls/color-picker/color-picker.d.ts +84 -0
- package/dist/controls/color-picker/color-picker.js +545 -0
- package/dist/controls/color-picker/color.d.ts +40 -0
- package/dist/controls/color-picker/color.js +159 -0
- package/dist/controls/column/column.css +151 -0
- package/dist/controls/column/column.d.ts +107 -0
- package/dist/controls/column/column.js +57 -0
- package/dist/controls/combo-box/combo-box.css +397 -0
- package/dist/controls/combo-box/combo-box.d.ts +133 -0
- package/dist/controls/combo-box/combo-box.js +680 -0
- package/dist/controls/command-modal/command-modal.css +226 -0
- package/dist/controls/command-modal/command-modal.d.ts +42 -0
- package/dist/controls/command-modal/command-modal.js +315 -0
- package/dist/controls/disclosure/disclosure.css +162 -0
- package/dist/controls/disclosure/disclosure.d.ts +37 -0
- package/dist/controls/disclosure/disclosure.js +207 -0
- package/dist/controls/field/field.css +140 -0
- package/dist/controls/field/field.d.ts +27 -0
- package/dist/controls/field/field.js +228 -0
- package/dist/controls/form-provider/form-provider.css +40 -0
- package/dist/controls/form-provider/form-provider.d.ts +42 -0
- package/dist/controls/form-provider/form-provider.js +105 -0
- package/dist/controls/grid/grid.css +72 -0
- package/dist/controls/grid/grid.d.ts +60 -0
- package/dist/controls/grid/grid.js +56 -0
- package/dist/controls/icon/icon.css +37 -0
- package/dist/controls/icon/icon.d.ts +25 -0
- package/dist/controls/icon/icon.js +55 -0
- package/dist/controls/index.d.ts +57 -0
- package/dist/controls/index.js +136 -0
- package/dist/controls/ladder/ladder.css +89 -0
- package/dist/controls/ladder/ladder.d.ts +27 -0
- package/dist/controls/ladder/ladder.js +78 -0
- package/dist/controls/list/list.css +118 -0
- package/dist/controls/list/list.d.ts +84 -0
- package/dist/controls/list/list.js +48 -0
- package/dist/controls/menu/menu.css +195 -0
- package/dist/controls/menu/menu.d.ts +44 -0
- package/dist/controls/menu/menu.js +248 -0
- package/dist/controls/modal/modal.css +99 -0
- package/dist/controls/modal/modal.d.ts +61 -0
- package/dist/controls/modal/modal.js +155 -0
- package/dist/controls/popover/popover.css +83 -0
- package/dist/controls/popover/popover.d.ts +44 -0
- package/dist/controls/popover/popover.js +164 -0
- package/dist/controls/progress/progress.css +106 -0
- package/dist/controls/progress/progress.d.ts +28 -0
- package/dist/controls/progress/progress.js +80 -0
- package/dist/controls/radio/radio-group.css +84 -0
- package/dist/controls/radio/radio-group.d.ts +152 -0
- package/dist/controls/radio/radio-group.js +350 -0
- package/dist/controls/radio/radio.css +219 -0
- package/dist/controls/radio/radio.d.ts +19 -0
- package/dist/controls/radio/radio.js +75 -0
- package/dist/controls/ramp/ramp.css +75 -0
- package/dist/controls/ramp/ramp.d.ts +29 -0
- package/dist/controls/ramp/ramp.js +72 -0
- package/dist/controls/row/row.css +152 -0
- package/dist/controls/row/row.d.ts +95 -0
- package/dist/controls/row/row.js +45 -0
- package/dist/controls/segment/segment.css +66 -0
- package/dist/controls/segment/segment.d.ts +3 -0
- package/dist/controls/segment/segment.js +26 -0
- package/dist/controls/segmented-control/segmented-control.css +189 -0
- package/dist/controls/segmented-control/segmented-control.d.ts +20 -0
- package/dist/controls/segmented-control/segmented-control.js +64 -0
- package/dist/controls/select/select.css +467 -0
- package/dist/controls/select/select.d.ts +147 -0
- package/dist/controls/select/select.js +538 -0
- package/dist/controls/slider/slider.css +231 -0
- package/dist/controls/slider/slider.d.ts +111 -0
- package/dist/controls/slider/slider.js +60 -0
- package/dist/controls/slider-multi/slider-multi.css +197 -0
- package/dist/controls/slider-multi/slider-multi.d.ts +149 -0
- package/dist/controls/slider-multi/slider-multi.js +380 -0
- package/dist/controls/sparkline/sparkline-math.d.ts +23 -0
- package/dist/controls/sparkline/sparkline-math.js +79 -0
- package/dist/controls/sparkline/sparkline.css +61 -0
- package/dist/controls/sparkline/sparkline.d.ts +27 -0
- package/dist/controls/sparkline/sparkline.js +74 -0
- package/dist/controls/split/constrain.d.ts +44 -0
- package/dist/controls/split/constrain.js +126 -0
- package/dist/controls/split/split-pane.css +63 -0
- package/dist/controls/split/split-pane.d.ts +49 -0
- package/dist/controls/split/split-pane.js +42 -0
- package/dist/controls/split/split.css +127 -0
- package/dist/controls/split/split.d.ts +42 -0
- package/dist/controls/split/split.js +364 -0
- package/dist/controls/stat/stat-model.d.ts +22 -0
- package/dist/controls/stat/stat-model.js +69 -0
- package/dist/controls/stat/stat.css +101 -0
- package/dist/controls/stat/stat.d.ts +30 -0
- package/dist/controls/stat/stat.js +73 -0
- package/dist/controls/status-stream/status-stream.css +151 -0
- package/dist/controls/status-stream/status-stream.d.ts +108 -0
- package/dist/controls/status-stream/status-stream.js +417 -0
- package/dist/controls/swatch/swatch.css +66 -0
- package/dist/controls/swatch/swatch.d.ts +28 -0
- package/dist/controls/swatch/swatch.js +74 -0
- package/dist/controls/swiper/swiper-item.css +17 -0
- package/dist/controls/swiper/swiper-item.d.ts +31 -0
- package/dist/controls/swiper/swiper-item.js +47 -0
- package/dist/controls/swiper/swiper-label.css +10 -0
- package/dist/controls/swiper/swiper-label.d.ts +4 -0
- package/dist/controls/swiper/swiper-label.js +20 -0
- package/dist/controls/swiper/swiper-paddles.css +26 -0
- package/dist/controls/swiper/swiper-paddles.d.ts +10 -0
- package/dist/controls/swiper/swiper-paddles.js +62 -0
- package/dist/controls/swiper/swiper-pagination.css +60 -0
- package/dist/controls/swiper/swiper-pagination.d.ts +31 -0
- package/dist/controls/swiper/swiper-pagination.js +78 -0
- package/dist/controls/swiper/swiper.css +153 -0
- package/dist/controls/swiper/swiper.d.ts +158 -0
- package/dist/controls/swiper/swiper.js +659 -0
- package/dist/controls/switch/switch.css +194 -0
- package/dist/controls/switch/switch.d.ts +5 -0
- package/dist/controls/switch/switch.js +18 -0
- package/dist/controls/table/table-model.d.ts +43 -0
- package/dist/controls/table/table-model.js +124 -0
- package/dist/controls/table/table.css +90 -0
- package/dist/controls/table/table.d.ts +29 -0
- package/dist/controls/table/table.js +178 -0
- package/dist/controls/tabs/tab-panel.d.ts +10 -0
- package/dist/controls/tabs/tab-panel.js +89 -0
- package/dist/controls/tabs/tab.d.ts +26 -0
- package/dist/controls/tabs/tab.js +67 -0
- package/dist/controls/tabs/tabs.css +181 -0
- package/dist/controls/tabs/tabs.d.ts +61 -0
- package/dist/controls/tabs/tabs.js +176 -0
- package/dist/controls/text/href.d.ts +26 -0
- package/dist/controls/text/href.js +48 -0
- package/dist/controls/text/text.css +328 -0
- package/dist/controls/text/text.d.ts +85 -0
- package/dist/controls/text/text.js +273 -0
- package/dist/controls/text-field/text-field.css +498 -0
- package/dist/controls/text-field/text-field.d.ts +226 -0
- package/dist/controls/text-field/text-field.js +905 -0
- package/dist/controls/textarea/textarea.css +251 -0
- package/dist/controls/textarea/textarea.d.ts +136 -0
- package/dist/controls/textarea/textarea.js +312 -0
- package/dist/controls/theme-provider/theme-provider.css +48 -0
- package/dist/controls/theme-provider/theme-provider.d.ts +59 -0
- package/dist/controls/theme-provider/theme-provider.js +40 -0
- package/dist/controls/timeline/timeline.css +33 -0
- package/dist/controls/timeline/timeline.d.ts +39 -0
- package/dist/controls/timeline/timeline.js +52 -0
- package/dist/controls/timeline-item/timeline-item.css +495 -0
- package/dist/controls/timeline-item/timeline-item.d.ts +79 -0
- package/dist/controls/timeline-item/timeline-item.js +360 -0
- package/dist/controls/toast/toast-region.css +56 -0
- package/dist/controls/toast/toast-region.d.ts +20 -0
- package/dist/controls/toast/toast-region.js +113 -0
- package/dist/controls/toast/toast.css +96 -0
- package/dist/controls/toast/toast.d.ts +28 -0
- package/dist/controls/toast/toast.js +191 -0
- package/dist/controls/toolbar/toolbar.css +125 -0
- package/dist/controls/toolbar/toolbar.d.ts +108 -0
- package/dist/controls/toolbar/toolbar.js +68 -0
- package/dist/controls/tooltip/tooltip.css +80 -0
- package/dist/controls/tooltip/tooltip.d.ts +57 -0
- package/dist/controls/tooltip/tooltip.js +205 -0
- package/dist/descriptor/component-descriptor.d.ts +112 -0
- package/dist/descriptor/component-descriptor.js +492 -0
- package/dist/descriptor/index.d.ts +1 -0
- package/dist/descriptor/index.js +5 -0
- package/dist/dom/container.d.ts +45 -0
- package/dist/dom/container.js +60 -0
- package/dist/dom/element.d.ts +98 -0
- package/dist/dom/element.js +221 -0
- package/dist/dom/form.d.ts +240 -0
- package/dist/dom/form.js +399 -0
- package/dist/dom/index.d.ts +10 -0
- package/dist/dom/index.js +31 -0
- package/dist/dom/props.d.ts +56 -0
- package/dist/dom/props.js +231 -0
- package/dist/dom/repeat.d.ts +10 -0
- package/dist/dom/repeat.js +202 -0
- package/dist/dom/template.d.ts +161 -0
- package/dist/dom/template.js +634 -0
- package/dist/dom/watch.d.ts +11 -0
- package/dist/dom/watch.js +82 -0
- package/dist/foundation-styles.css +9 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3 -0
- package/dist/reactive/graph.d.ts +65 -0
- package/dist/reactive/graph.js +310 -0
- package/dist/reactive/index.d.ts +3 -0
- package/dist/reactive/index.js +4 -0
- package/dist/reactive/scheduler.d.ts +13 -0
- package/dist/reactive/scheduler.js +49 -0
- package/dist/traits/area-drag.d.ts +12 -0
- package/dist/traits/area-drag.js +70 -0
- package/dist/traits/form-registry.d.ts +36 -0
- package/dist/traits/form-registry.js +108 -0
- package/dist/traits/index.d.ts +24 -0
- package/dist/traits/index.js +19 -0
- package/dist/traits/overlay.d.ts +50 -0
- package/dist/traits/overlay.js +277 -0
- package/dist/traits/pane-resize.d.ts +26 -0
- package/dist/traits/pane-resize.js +103 -0
- package/dist/traits/press-activation.d.ts +11 -0
- package/dist/traits/press-activation.js +55 -0
- package/dist/traits/roving-focus.d.ts +52 -0
- package/dist/traits/roving-focus.js +214 -0
- package/dist/traits/scroll-fade.d.ts +28 -0
- package/dist/traits/scroll-fade.js +120 -0
- package/dist/traits/selection-commit.d.ts +30 -0
- package/dist/traits/selection-commit.js +146 -0
- package/dist/traits/tabbable.d.ts +12 -0
- package/dist/traits/tabbable.js +59 -0
- package/dist/traits/track-user-invalid.d.ts +36 -0
- package/dist/traits/track-user-invalid.js +52 -0
- package/dist/traits/value-codec.d.ts +120 -0
- package/dist/traits/value-codec.js +497 -0
- package/dist/traits/value-drag.d.ts +18 -0
- package/dist/traits/value-drag.js +84 -0
- package/package.json +263 -0
|
@@ -0,0 +1,659 @@
|
|
|
1
|
+
// swiper.ts — UISwiperElement, the ui-swiper coordinator (swiper-family.lld.md LLD-C1/C2/C3/C5/C6/C7 ·
|
|
2
|
+
// swiper-family.spec.md SPEC-R1…R8/R10/R11/R14/R15 · ADR-0124). BEHAVIOUR + props + the owned scroll track +
|
|
3
|
+
// the clone-teleport infinite loop + keyboard + the bindable `active`/`select` commit + chrome drive + self-
|
|
4
|
+
// define ONLY; geometry/tokens live in swiper.css (the family's single sheet), the public contract in
|
|
5
|
+
// swiper.md. Importing this module registers all FIVE family tags (it imports the four leaf modules).
|
|
6
|
+
//
|
|
7
|
+
// The container (`extends UIContainerElement` for the surface axes + the reused protected `internals` — the
|
|
8
|
+
// `ui-tabs` base; NOT form-associated). It owns TWO control-created parts (idempotent across reconnect, the
|
|
9
|
+
// `ui-tabs` tablist precedent): the scrolling `[data-part=track]` (a single owned scroll region, SPEC-R3) and
|
|
10
|
+
// a visually-hidden polite `[data-part=live]` region. It REPARENTS its `ui-swiper-item` children into the
|
|
11
|
+
// track (the chrome anchors — `ui-swiper-pagination`/`-paddles`/`-label` — stay as track SIBLINGS, driven in
|
|
12
|
+
// place); it drives the whole widget from ONE place (a sibling cannot set another element's protected
|
|
13
|
+
// internals): the region's own `role='region'`/`ariaRoleDescription='carousel'`/accessible-name, each real
|
|
14
|
+
// item's `role=group`/`aria-roledescription='slide'`/position label (via `labelAs`), the clone-teleport loop,
|
|
15
|
+
// the bindable `active` + the one `select` commit event (the `ui-tabs` `selected`/`select` pattern, ADR-0019),
|
|
16
|
+
// and the chrome anchors' render/wiring.
|
|
17
|
+
//
|
|
18
|
+
// `controls → dom` is the allowed import direction; importing this module registers all five family tags (it
|
|
19
|
+
// imports the four leaf modules), so the barrel needs only `export * from './swiper/swiper.ts'`.
|
|
20
|
+
import { UIContainerElement } from "../../dom/container.js";
|
|
21
|
+
import { prop } from "../../dom/index.js";
|
|
22
|
+
import { UISwiperItemElement } from "./swiper-item.js";
|
|
23
|
+
import { UISwiperPaginationElement } from "./swiper-pagination.js";
|
|
24
|
+
import { UISwiperPaddlesElement } from "./swiper-paddles.js";
|
|
25
|
+
import { UISwiperLabelElement } from "./swiper-label.js";
|
|
26
|
+
// A per-instance id seed so each swiper's label/track get unique IDREFs (the `ui-tabs` `tabsSeq` precedent).
|
|
27
|
+
let swiperSeq = 0;
|
|
28
|
+
const ORIENTATIONS = ['horizontal', 'vertical'];
|
|
29
|
+
const ALIGNMENTS = ['start', 'center', 'end'];
|
|
30
|
+
const props = {
|
|
31
|
+
...UIContainerElement.surfaceProps, // elevation/brightness (ADR-0015)
|
|
32
|
+
orientation: { ...prop.enum(ORIENTATIONS, 'horizontal'), reflect: true },
|
|
33
|
+
'slides-in-view': { ...prop.string(), reflect: true }, // '' ⇒ responsive-auto; a numeric string pins columns
|
|
34
|
+
align: { ...prop.enum(ALIGNMENTS, 'start'), reflect: true },
|
|
35
|
+
loop: { ...prop.boolean(), reflect: true },
|
|
36
|
+
duration: { ...prop.string(), reflect: true }, // '' ⇒ token default; CSS <time>, programmatic advances only
|
|
37
|
+
easing: { ...prop.string(), reflect: true }, // '' ⇒ token default; CSS easing, programmatic advances only
|
|
38
|
+
pagination: { ...prop.boolean(), reflect: true }, // stamp default dots anchor if none present
|
|
39
|
+
paddles: { ...prop.boolean(), reflect: true }, // stamp default paddles anchor if none present
|
|
40
|
+
active: { ...prop.string(), reflect: true }, // bindable active-slide identity (ADR-0019; commit = LLD-C7)
|
|
41
|
+
};
|
|
42
|
+
// The ARIA element-reflection helper — a MODULE-PRIVATE 3-line peer copy (tab.ts:24 / tab-panel.ts:19 /
|
|
43
|
+
// form.ts:120 are the other three folder copies; this is the family's 4th, not a shared export).
|
|
44
|
+
function reflectAriaElements(internals, name, elements) {
|
|
45
|
+
if (name in internals)
|
|
46
|
+
internals[name] = elements;
|
|
47
|
+
}
|
|
48
|
+
// ── easing (F1's JS scroll-animation engine — CSS scroll-behavior:smooth ignores custom properties) ────────
|
|
49
|
+
// A minimal, real cubic-bezier evaluator for the standard keyword set + `cubic-bezier(x1,y1,x2,y2)` — the
|
|
50
|
+
// vast majority of authored eases. An unrecognised curve (e.g. `steps()`) falls back to ease-in-out — a
|
|
51
|
+
// documented, graceful degradation, not a silent wrong answer.
|
|
52
|
+
const EASING_PRESETS = {
|
|
53
|
+
linear: [0, 0, 1, 1],
|
|
54
|
+
ease: [0.25, 0.1, 0.25, 1],
|
|
55
|
+
'ease-in': [0.42, 0, 1, 1],
|
|
56
|
+
'ease-out': [0, 0, 0.58, 1],
|
|
57
|
+
'ease-in-out': [0.42, 0, 0.58, 1],
|
|
58
|
+
};
|
|
59
|
+
function parseCubicBezier(spec) {
|
|
60
|
+
const preset = EASING_PRESETS[spec.trim()];
|
|
61
|
+
if (preset)
|
|
62
|
+
return preset;
|
|
63
|
+
const m = /cubic-bezier\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*\)/.exec(spec);
|
|
64
|
+
if (m)
|
|
65
|
+
return [Number(m[1]), Number(m[2]), Number(m[3]), Number(m[4])];
|
|
66
|
+
return EASING_PRESETS['ease-in-out'];
|
|
67
|
+
}
|
|
68
|
+
function bezierEase(points) {
|
|
69
|
+
const [x1, y1, x2, y2] = points;
|
|
70
|
+
const bx = (t) => 3 * (1 - t) ** 2 * t * x1 + 3 * (1 - t) * t ** 2 * x2 + t ** 3;
|
|
71
|
+
const by = (t) => 3 * (1 - t) ** 2 * t * y1 + 3 * (1 - t) * t ** 2 * y2 + t ** 3;
|
|
72
|
+
return (x) => {
|
|
73
|
+
let lo = 0;
|
|
74
|
+
let hi = 1;
|
|
75
|
+
for (let i = 0; i < 20; i++) {
|
|
76
|
+
const mid = (lo + hi) / 2;
|
|
77
|
+
if (bx(mid) < x)
|
|
78
|
+
lo = mid;
|
|
79
|
+
else
|
|
80
|
+
hi = mid;
|
|
81
|
+
}
|
|
82
|
+
return by((lo + hi) / 2);
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function parseCssTime(raw, fallbackMs) {
|
|
86
|
+
const s = raw.trim();
|
|
87
|
+
const ms = /^(-?[\d.]+)ms$/.exec(s);
|
|
88
|
+
if (ms)
|
|
89
|
+
return Number(ms[1]);
|
|
90
|
+
const sec = /^(-?[\d.]+)s$/.exec(s);
|
|
91
|
+
if (sec)
|
|
92
|
+
return Number(sec[1]) * 1000;
|
|
93
|
+
return fallbackMs;
|
|
94
|
+
}
|
|
95
|
+
export class UISwiperElement extends UIContainerElement {
|
|
96
|
+
static props = props;
|
|
97
|
+
// Control-created parts (persist across reconnect — the ui-tabs precedent).
|
|
98
|
+
#track = null;
|
|
99
|
+
#live = null;
|
|
100
|
+
#baseId = '';
|
|
101
|
+
// Real-slide + clone bookkeeping.
|
|
102
|
+
#slides = [];
|
|
103
|
+
#leadingClones = [];
|
|
104
|
+
#trailingClones = [];
|
|
105
|
+
#lastCloneBand = 0;
|
|
106
|
+
// Selection / commit state.
|
|
107
|
+
#activeIndex = -1;
|
|
108
|
+
#activeApplied = false;
|
|
109
|
+
// Loop re-entrancy + settle detection.
|
|
110
|
+
#teleporting = false;
|
|
111
|
+
#scrollSettleTimer = null;
|
|
112
|
+
#animFrame = null;
|
|
113
|
+
// Observers.
|
|
114
|
+
#mutationObserver = null;
|
|
115
|
+
#resizeObserver = null;
|
|
116
|
+
#mutationScheduled = false;
|
|
117
|
+
/** REAL items only (clones excluded), DOM order. */
|
|
118
|
+
get slides() {
|
|
119
|
+
return this.#slides;
|
|
120
|
+
}
|
|
121
|
+
/** The resolved real index — always freshly derived from `this.active` (the `#resolveIndex` result). */
|
|
122
|
+
get activeIndex() {
|
|
123
|
+
return this.#resolveIndex();
|
|
124
|
+
}
|
|
125
|
+
/** Advance one slide (wraps into the trailing clone of slide 0 in loop mode — never a visible rewind). */
|
|
126
|
+
next() {
|
|
127
|
+
const target = this.#advanceTarget(1);
|
|
128
|
+
if (target)
|
|
129
|
+
this.#animateTo(target);
|
|
130
|
+
}
|
|
131
|
+
/** Retreat one slide (wraps into the leading clone of the last slide in loop mode). */
|
|
132
|
+
prev() {
|
|
133
|
+
const target = this.#advanceTarget(-1);
|
|
134
|
+
if (target)
|
|
135
|
+
this.#animateTo(target);
|
|
136
|
+
}
|
|
137
|
+
/** Scroll a REAL index into the align position (a rAF scroll animation over duration/easing; instant under
|
|
138
|
+
* reduced-motion or while the loop teleport is in flight). */
|
|
139
|
+
goTo(index) {
|
|
140
|
+
const target = this.#slides[index];
|
|
141
|
+
if (target)
|
|
142
|
+
this.#animateTo(target);
|
|
143
|
+
}
|
|
144
|
+
connected() {
|
|
145
|
+
if (!this.#baseId)
|
|
146
|
+
this.#baseId = `ui-swiper-${++swiperSeq}`;
|
|
147
|
+
this.#ensureParts();
|
|
148
|
+
this.#captureSlides();
|
|
149
|
+
// The region's own ARIA (SPEC-R15) — static facts, set once via internals, never a host attribute.
|
|
150
|
+
this.internals.role = 'region';
|
|
151
|
+
this.internals.ariaRoleDescription = 'carousel';
|
|
152
|
+
this.#applyRegionLabel();
|
|
153
|
+
this.#rebuildLoop(); // clones, if loop
|
|
154
|
+
this.#lastCloneBand = this.loop ? this.#cloneBandSize() : 0;
|
|
155
|
+
this.#labelSlides();
|
|
156
|
+
this.#driveChrome();
|
|
157
|
+
this.listen(this.#track, 'scroll', this.#onScroll, { passive: true });
|
|
158
|
+
this.listen(this, 'keydown', this.#onKeydown);
|
|
159
|
+
// duration/easing → inline custom-property overrides the JS scroll animation reads (LLD §7 Consequences).
|
|
160
|
+
this.effect(() => {
|
|
161
|
+
if (this.duration !== '')
|
|
162
|
+
this.style.setProperty('--ui-swiper-duration', this.duration);
|
|
163
|
+
else
|
|
164
|
+
this.style.removeProperty('--ui-swiper-duration');
|
|
165
|
+
});
|
|
166
|
+
this.effect(() => {
|
|
167
|
+
if (this.easing !== '')
|
|
168
|
+
this.style.setProperty('--ui-swiper-easing', this.easing);
|
|
169
|
+
else
|
|
170
|
+
this.style.removeProperty('--ui-swiper-easing');
|
|
171
|
+
});
|
|
172
|
+
// The active effect + the chrome re-drive — ONE scope-owned effect: applying `active` (LLD-C7) and
|
|
173
|
+
// re-driving pagination/paddles/label (LLD-C9/C10/C11) share the same reactive dependencies
|
|
174
|
+
// (active/pagination/paddles/orientation/loop), so one effect keeps them in lock-step, exactly the
|
|
175
|
+
// way `ui-tabs`'s single selection effect covers roving + panel visibility together.
|
|
176
|
+
this.effect(() => {
|
|
177
|
+
this.#applyActive();
|
|
178
|
+
this.#driveChrome();
|
|
179
|
+
});
|
|
180
|
+
// Motion gate (interaction-states standard) — arm `ready` one frame past first paint (the `ui-tabs`
|
|
181
|
+
// precedent): the synchronous initial position/selection snaps; only later changes animate.
|
|
182
|
+
requestAnimationFrame(() => this.internals.states?.add('ready'));
|
|
183
|
+
// Rebuild clones + re-label + re-drive chrome when the author adds/removes slides.
|
|
184
|
+
this.#mutationObserver = new MutationObserver(() => this.#scheduleMutationSync());
|
|
185
|
+
this.#mutationObserver.observe(this, { childList: true });
|
|
186
|
+
// Recompute columns → rebuild the clone band when a `@container` breakpoint crosses under
|
|
187
|
+
// `slides-in-view=''` (responsive columns) — a real-browser-only mechanism; a no-op guard keeps this
|
|
188
|
+
// jsdom-safe (ResizeObserver is absent there).
|
|
189
|
+
if (typeof ResizeObserver !== 'undefined') {
|
|
190
|
+
this.#resizeObserver = new ResizeObserver(() => this.#onResize());
|
|
191
|
+
this.#resizeObserver.observe(this);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
disconnected() {
|
|
195
|
+
this.#mutationObserver?.disconnect();
|
|
196
|
+
this.#mutationObserver = null;
|
|
197
|
+
this.#resizeObserver?.disconnect();
|
|
198
|
+
this.#resizeObserver = null;
|
|
199
|
+
if (this.#animFrame !== null) {
|
|
200
|
+
cancelAnimationFrame(this.#animFrame);
|
|
201
|
+
this.#animFrame = null;
|
|
202
|
+
}
|
|
203
|
+
if (this.#scrollSettleTimer !== null) {
|
|
204
|
+
clearTimeout(this.#scrollSettleTimer);
|
|
205
|
+
this.#scrollSettleTimer = null;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
// ── parts + slide capture ──────────────────────────────────────────────────────────────────────────────
|
|
209
|
+
#ensureParts() {
|
|
210
|
+
let track = this.#track;
|
|
211
|
+
if (!track) {
|
|
212
|
+
track = document.createElement('div');
|
|
213
|
+
track.setAttribute('data-part', 'track');
|
|
214
|
+
track.setAttribute('role', 'group'); // rides the PART div (the ui-tabs tablist-strip precedent)
|
|
215
|
+
track.tabIndex = 0;
|
|
216
|
+
this.#track = track;
|
|
217
|
+
}
|
|
218
|
+
if (track.parentNode !== this)
|
|
219
|
+
this.insertBefore(track, this.firstChild);
|
|
220
|
+
let live = this.#live;
|
|
221
|
+
if (!live) {
|
|
222
|
+
live = document.createElement('div');
|
|
223
|
+
live.setAttribute('data-part', 'live');
|
|
224
|
+
live.setAttribute('aria-live', 'polite');
|
|
225
|
+
this.#live = live;
|
|
226
|
+
}
|
|
227
|
+
if (live.parentNode !== this)
|
|
228
|
+
this.appendChild(live);
|
|
229
|
+
}
|
|
230
|
+
/** Reparent `ui-swiper-item` children into the track (idempotent); chrome anchors stay host siblings. */
|
|
231
|
+
#captureSlides() {
|
|
232
|
+
const track = this.#track;
|
|
233
|
+
for (const child of [...this.children]) {
|
|
234
|
+
if (child instanceof UISwiperItemElement && child.parentNode !== track)
|
|
235
|
+
track.appendChild(child);
|
|
236
|
+
}
|
|
237
|
+
this.#slides = [...track.children].filter((c) => c instanceof UISwiperItemElement && c.dataset.swiperClone === undefined);
|
|
238
|
+
}
|
|
239
|
+
#scheduleMutationSync() {
|
|
240
|
+
if (this.#mutationScheduled)
|
|
241
|
+
return;
|
|
242
|
+
this.#mutationScheduled = true;
|
|
243
|
+
queueMicrotask(() => {
|
|
244
|
+
this.#mutationScheduled = false;
|
|
245
|
+
if (!this.isConnected)
|
|
246
|
+
return;
|
|
247
|
+
this.#captureSlides();
|
|
248
|
+
this.#rebuildLoop();
|
|
249
|
+
this.#lastCloneBand = this.loop ? this.#cloneBandSize() : 0;
|
|
250
|
+
this.#labelSlides();
|
|
251
|
+
this.#driveChrome();
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
// ── region + slide labelling ────────────────────────────────────────────────────────────────────────────
|
|
255
|
+
#applyRegionLabel() {
|
|
256
|
+
const label = this.querySelector(':scope > ui-swiper-label');
|
|
257
|
+
if (label instanceof UISwiperLabelElement) {
|
|
258
|
+
if (!label.id)
|
|
259
|
+
label.id = `${this.#baseId}-label`;
|
|
260
|
+
reflectAriaElements(this.internals, 'ariaLabelledByElements', [label]);
|
|
261
|
+
this.internals.ariaLabel = null;
|
|
262
|
+
this.#track?.setAttribute('aria-label', (label.textContent ?? '').trim() || 'Carousel');
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
this.internals.ariaLabel = 'Carousel';
|
|
266
|
+
this.#track?.setAttribute('aria-label', 'Carousel');
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
#labelSlides() {
|
|
270
|
+
const count = this.#slides.length;
|
|
271
|
+
this.#slides.forEach((slide, i) => slide.labelAs(`${i + 1} of ${count}`));
|
|
272
|
+
}
|
|
273
|
+
#announce(index) {
|
|
274
|
+
if (this.#live)
|
|
275
|
+
this.#live.textContent = `Slide ${index + 1} of ${this.#slides.length}`;
|
|
276
|
+
}
|
|
277
|
+
// ── bindable active + the select commit (LLD-C7, SPEC-R7/R14) ─────────────────────────────────────────────
|
|
278
|
+
/** `''` ⇒ 0; a real item whose `value` equals `active` wins; else a numeric in-range index; else 0
|
|
279
|
+
* (the `ui-tabs` `#resolveIndex` precedent, SPEC-R14). */
|
|
280
|
+
#resolveIndex() {
|
|
281
|
+
const slides = this.#slides;
|
|
282
|
+
if (slides.length === 0)
|
|
283
|
+
return 0;
|
|
284
|
+
const sel = this.active;
|
|
285
|
+
if (sel === '')
|
|
286
|
+
return 0;
|
|
287
|
+
const byValue = slides.findIndex((s) => s.key !== '' && s.key === sel);
|
|
288
|
+
if (byValue !== -1)
|
|
289
|
+
return byValue;
|
|
290
|
+
if (/^\d+$/.test(sel)) {
|
|
291
|
+
const n = Number(sel);
|
|
292
|
+
if (n >= 0 && n < slides.length)
|
|
293
|
+
return n;
|
|
294
|
+
}
|
|
295
|
+
return 0;
|
|
296
|
+
}
|
|
297
|
+
/** Reads `this.active` (tracked); resolves the real index and `goTo`s it if the track is not already
|
|
298
|
+
* there. The FIRST run snaps instantly (no animation) — the `ui-tabs` first-paint-snaps precedent. */
|
|
299
|
+
#applyActive() {
|
|
300
|
+
const index = this.#resolveIndex();
|
|
301
|
+
const first = !this.#activeApplied;
|
|
302
|
+
this.#activeApplied = true;
|
|
303
|
+
if (index === this.#activeIndex && !first)
|
|
304
|
+
return;
|
|
305
|
+
this.#activeIndex = index;
|
|
306
|
+
this.#announce(index);
|
|
307
|
+
if (!this.#slides[index])
|
|
308
|
+
return;
|
|
309
|
+
if (first) {
|
|
310
|
+
const target = this.#slides[index];
|
|
311
|
+
if (target)
|
|
312
|
+
this.#setScrollPos(this.#alignedOffset(target), 'auto');
|
|
313
|
+
}
|
|
314
|
+
else {
|
|
315
|
+
this.goTo(index); // the PUBLIC seam (LLD §4) — also what a spy observes
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
/** Commit a user-driven selection (scroll settle / paddle / dot / key): compare against the PRE-settle
|
|
319
|
+
* `#activeIndex` (so a genuine wrap, e.g. the last real slide → the first, still emits), eager-set the new
|
|
320
|
+
* index, write `this.active` (reflects + wakes the active effect — a no-op there since the index already
|
|
321
|
+
* matches), and emit `select` ONLY when the real index CHANGED — so the renderer's own two-way write never
|
|
322
|
+
* echoes (ADR-0019). The loop's own teleport never produces a SECOND settle to double-count (`#onScroll`'s
|
|
323
|
+
* `#teleporting` guard suppresses the jump's own scroll events structurally) — one settle, one commit,
|
|
324
|
+
* one changed-index test (LLD §5). */
|
|
325
|
+
#commit(index, moveFocus) {
|
|
326
|
+
const item = this.#slides[index];
|
|
327
|
+
if (!item)
|
|
328
|
+
return;
|
|
329
|
+
const changed = index !== this.#activeIndex;
|
|
330
|
+
const identity = item.key !== '' ? item.key : String(index);
|
|
331
|
+
this.#activeIndex = index;
|
|
332
|
+
this.active = identity;
|
|
333
|
+
this.#announce(index);
|
|
334
|
+
if (moveFocus)
|
|
335
|
+
item.focus();
|
|
336
|
+
if (changed)
|
|
337
|
+
this.emit('select', { value: identity, index });
|
|
338
|
+
}
|
|
339
|
+
// ── keyboard (LLD-C6, SPEC-R4/R5) ──────────────────────────────────────────────────────────────────────
|
|
340
|
+
#onKeydown = (evt) => {
|
|
341
|
+
const event = evt;
|
|
342
|
+
const horizontal = this.orientation === 'horizontal';
|
|
343
|
+
const isNext = horizontal ? event.key === 'ArrowRight' : event.key === 'ArrowDown';
|
|
344
|
+
const isPrev = horizontal ? event.key === 'ArrowLeft' : event.key === 'ArrowUp';
|
|
345
|
+
if (isNext) {
|
|
346
|
+
event.preventDefault();
|
|
347
|
+
this.next();
|
|
348
|
+
}
|
|
349
|
+
else if (isPrev) {
|
|
350
|
+
event.preventDefault();
|
|
351
|
+
this.prev();
|
|
352
|
+
}
|
|
353
|
+
else if (event.key === 'Home') {
|
|
354
|
+
event.preventDefault();
|
|
355
|
+
this.goTo(0);
|
|
356
|
+
}
|
|
357
|
+
else if (event.key === 'End') {
|
|
358
|
+
event.preventDefault();
|
|
359
|
+
if (this.#slides.length > 0)
|
|
360
|
+
this.goTo(this.#slides.length - 1);
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
// ── the infinite loop — clone-teleport (LLD-C5, SPEC-R10/R11) ──────────────────────────────────────────
|
|
364
|
+
/** `k = ceil(slidesInView) + 1`. A pinned numeric `slides-in-view` resolves purely in JS; the `''`
|
|
365
|
+
* responsive-auto sentinel reads the CSS-resolved `--ui-swiper-columns` (real-browser-only — jsdom/no-CSS
|
|
366
|
+
* falls back to 1, the token default). */
|
|
367
|
+
#slidesInViewCount() {
|
|
368
|
+
const raw = this['slides-in-view'];
|
|
369
|
+
if (raw !== '') {
|
|
370
|
+
const n = Number.parseFloat(raw);
|
|
371
|
+
if (Number.isFinite(n) && n > 0)
|
|
372
|
+
return n;
|
|
373
|
+
}
|
|
374
|
+
if (!this.#track)
|
|
375
|
+
return 1;
|
|
376
|
+
const computed = getComputedStyle(this.#track).getPropertyValue('--ui-swiper-columns').trim();
|
|
377
|
+
const n = Number.parseFloat(computed);
|
|
378
|
+
return Number.isFinite(n) && n > 0 ? n : 1;
|
|
379
|
+
}
|
|
380
|
+
#cloneBandSize() {
|
|
381
|
+
return Math.ceil(this.#slidesInViewCount()) + 1;
|
|
382
|
+
}
|
|
383
|
+
#cloneSlide(src) {
|
|
384
|
+
const clone = src.cloneNode(true);
|
|
385
|
+
clone.removeAttribute('id');
|
|
386
|
+
for (const el of clone.querySelectorAll('[id]'))
|
|
387
|
+
el.removeAttribute('id');
|
|
388
|
+
clone.setAttribute('aria-hidden', 'true');
|
|
389
|
+
clone.inert = true;
|
|
390
|
+
clone.dataset.swiperClone = '';
|
|
391
|
+
return clone;
|
|
392
|
+
}
|
|
393
|
+
#clearClones() {
|
|
394
|
+
for (const c of this.#leadingClones)
|
|
395
|
+
c.remove();
|
|
396
|
+
for (const c of this.#trailingClones)
|
|
397
|
+
c.remove();
|
|
398
|
+
this.#leadingClones = [];
|
|
399
|
+
this.#trailingClones = [];
|
|
400
|
+
}
|
|
401
|
+
/** (Re)build the clone band. Non-loop: clears clones and returns (real slides only). Loop: deep-clones the
|
|
402
|
+
* last `k` real slides → prepended (leading), the first `k` → appended (trailing); each clone is
|
|
403
|
+
* id-stripped + `aria-hidden` + `inert` + marked `data-swiper-clone` (excluded from `get slides`). Scrolls
|
|
404
|
+
* the first real slide to the align position WITHOUT animation so the leading clones sit off-viewport. */
|
|
405
|
+
#rebuildLoop() {
|
|
406
|
+
this.#clearClones();
|
|
407
|
+
const track = this.#track;
|
|
408
|
+
if (!track || !this.loop || this.#slides.length === 0)
|
|
409
|
+
return;
|
|
410
|
+
const real = this.#slides;
|
|
411
|
+
const k = Math.min(this.#cloneBandSize(), real.length);
|
|
412
|
+
const lastK = real.slice(real.length - k);
|
|
413
|
+
const firstK = real.slice(0, k);
|
|
414
|
+
this.#leadingClones = lastK.map((src) => this.#cloneSlide(src));
|
|
415
|
+
this.#trailingClones = firstK.map((src) => this.#cloneSlide(src));
|
|
416
|
+
for (const clone of this.#leadingClones)
|
|
417
|
+
track.insertBefore(clone, real[0]);
|
|
418
|
+
for (const clone of this.#trailingClones)
|
|
419
|
+
track.appendChild(clone);
|
|
420
|
+
this.#setScrollPos(this.#alignedOffset(real[0]), 'auto');
|
|
421
|
+
}
|
|
422
|
+
/** The pixel extent of the whole real slide set (the distance from the first-real to the first-trailing-
|
|
423
|
+
* clone snap target) — the exact jump distance a clone-band settle teleports by. */
|
|
424
|
+
#realSetExtent() {
|
|
425
|
+
if (this.#trailingClones.length === 0 || this.#slides.length === 0)
|
|
426
|
+
return 0;
|
|
427
|
+
return this.#alignedOffset(this.#trailingClones[0]) - this.#alignedOffset(this.#slides[0]);
|
|
428
|
+
}
|
|
429
|
+
/** The element to advance to (next()/prev()): the adjacent real slide, or — at a loop-mode boundary — the
|
|
430
|
+
* clone that represents the wrap target (never a `goTo(0)` rewind across the whole track). */
|
|
431
|
+
#advanceTarget(dir) {
|
|
432
|
+
const count = this.#slides.length;
|
|
433
|
+
if (count === 0)
|
|
434
|
+
return null;
|
|
435
|
+
const idx = this.activeIndex + dir;
|
|
436
|
+
if (idx >= 0 && idx < count)
|
|
437
|
+
return this.#slides[idx];
|
|
438
|
+
if (!this.loop)
|
|
439
|
+
return null;
|
|
440
|
+
if (dir > 0)
|
|
441
|
+
return this.#trailingClones[0] ?? this.#slides[0];
|
|
442
|
+
return this.#leadingClones[this.#leadingClones.length - 1] ?? this.#slides[count - 1];
|
|
443
|
+
}
|
|
444
|
+
#onScroll = () => {
|
|
445
|
+
if (this.#teleporting)
|
|
446
|
+
return; // suppress interim scroll-event handling during our own jump
|
|
447
|
+
if (this.#scrollSettleTimer !== null)
|
|
448
|
+
clearTimeout(this.#scrollSettleTimer);
|
|
449
|
+
this.#scrollSettleTimer = setTimeout(() => {
|
|
450
|
+
this.#scrollSettleTimer = null;
|
|
451
|
+
this.#onSettle();
|
|
452
|
+
}, 120);
|
|
453
|
+
};
|
|
454
|
+
/** One settle → one #commit call, structurally (the `#teleporting` guard in `#onScroll` suppresses the
|
|
455
|
+
* jump's own scroll events, so a clone-band settle never produces a SECOND settle cycle to double-count).
|
|
456
|
+
* `#commit`'s changed-index test (against the PRE-settle `#activeIndex`) is what makes the emit correct —
|
|
457
|
+
* a real index change (e.g. the last real slide wrapping to the first) must still emit exactly once. */
|
|
458
|
+
#onSettle() {
|
|
459
|
+
if (!this.isConnected)
|
|
460
|
+
return;
|
|
461
|
+
const node = this.#nearestSlide();
|
|
462
|
+
if (!node)
|
|
463
|
+
return;
|
|
464
|
+
const mapped = this.#mapToReal(node);
|
|
465
|
+
if (!mapped)
|
|
466
|
+
return;
|
|
467
|
+
const { index, band } = mapped;
|
|
468
|
+
if (band)
|
|
469
|
+
this.#teleport(band);
|
|
470
|
+
this.#commit(index, false);
|
|
471
|
+
}
|
|
472
|
+
/** The track child (real or clone) whose aligned snap target is nearest the current scroll position — the
|
|
473
|
+
* same box-alignment geometry native `scroll-snap-align` uses to settle. jsdom has no scroll layout (every
|
|
474
|
+
* rect is zero), so this resolves vacuously there — the real-engine browser leg carries this proof. */
|
|
475
|
+
#nearestSlide() {
|
|
476
|
+
const track = this.#track;
|
|
477
|
+
if (!track)
|
|
478
|
+
return null;
|
|
479
|
+
const candidates = [...track.children];
|
|
480
|
+
if (candidates.length === 0)
|
|
481
|
+
return null;
|
|
482
|
+
const pos = this.#scrollPos();
|
|
483
|
+
let best = null;
|
|
484
|
+
let bestDelta = Infinity;
|
|
485
|
+
for (const el of candidates) {
|
|
486
|
+
const delta = Math.abs(this.#alignedOffset(el) - pos);
|
|
487
|
+
if (delta < bestDelta) {
|
|
488
|
+
bestDelta = delta;
|
|
489
|
+
best = el;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
return best;
|
|
493
|
+
}
|
|
494
|
+
#mapToReal(node) {
|
|
495
|
+
const realIdx = this.#slides.indexOf(node);
|
|
496
|
+
if (realIdx !== -1)
|
|
497
|
+
return { index: realIdx, band: null };
|
|
498
|
+
const leadIdx = this.#leadingClones.indexOf(node);
|
|
499
|
+
if (leadIdx !== -1) {
|
|
500
|
+
const k = this.#leadingClones.length;
|
|
501
|
+
return { index: this.#slides.length - k + leadIdx, band: 'leading' };
|
|
502
|
+
}
|
|
503
|
+
const trailIdx = this.#trailingClones.indexOf(node);
|
|
504
|
+
if (trailIdx !== -1)
|
|
505
|
+
return { index: trailIdx, band: 'trailing' };
|
|
506
|
+
return null;
|
|
507
|
+
}
|
|
508
|
+
/** Jump the scroll offset by exactly the real-set extent, `scroll-behavior: auto` (instant, sub-frame) —
|
|
509
|
+
* pixel-seamless because the clone is pixel-identical to its real twin. `#teleporting` suppresses the
|
|
510
|
+
* jump's OWN scroll events; it is a secondary guard only (the changed-index test in `#commit` is primary,
|
|
511
|
+
* LLD §5). */
|
|
512
|
+
#teleport(band) {
|
|
513
|
+
const extent = this.#realSetExtent();
|
|
514
|
+
if (extent <= 0)
|
|
515
|
+
return;
|
|
516
|
+
this.#teleporting = true;
|
|
517
|
+
const current = this.#scrollPos();
|
|
518
|
+
const next = band === 'leading' ? current + extent : current - extent;
|
|
519
|
+
this.#setScrollPos(next, 'auto');
|
|
520
|
+
requestAnimationFrame(() => {
|
|
521
|
+
this.#teleporting = false;
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
#onResize() {
|
|
525
|
+
if (!this.loop)
|
|
526
|
+
return;
|
|
527
|
+
const k = this.#cloneBandSize();
|
|
528
|
+
if (k === this.#lastCloneBand)
|
|
529
|
+
return; // a resize that does not change the column count is a no-op
|
|
530
|
+
this.#lastCloneBand = k;
|
|
531
|
+
this.#scheduleMutationSync();
|
|
532
|
+
}
|
|
533
|
+
// ── the scroll-position + alignment maths (shared by goTo/teleport/settle-detection) ──────────────────────
|
|
534
|
+
#prefersReducedMotion() {
|
|
535
|
+
return typeof matchMedia === 'function' && matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
536
|
+
}
|
|
537
|
+
#scrollPos() {
|
|
538
|
+
const track = this.#track;
|
|
539
|
+
if (!track)
|
|
540
|
+
return 0;
|
|
541
|
+
return this.orientation === 'horizontal' ? track.scrollLeft : track.scrollTop;
|
|
542
|
+
}
|
|
543
|
+
#setScrollPos(value, behavior) {
|
|
544
|
+
const track = this.#track;
|
|
545
|
+
if (!track)
|
|
546
|
+
return;
|
|
547
|
+
if (typeof track.scrollTo === 'function') {
|
|
548
|
+
if (this.orientation === 'horizontal')
|
|
549
|
+
track.scrollTo({ left: value, behavior });
|
|
550
|
+
else
|
|
551
|
+
track.scrollTo({ top: value, behavior });
|
|
552
|
+
}
|
|
553
|
+
else if (this.orientation === 'horizontal') {
|
|
554
|
+
track.scrollLeft = value;
|
|
555
|
+
}
|
|
556
|
+
else {
|
|
557
|
+
track.scrollTop = value;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
/** The scrollLeft/scrollTop that aligns `target`'s start/center/end edge with the track's, per `this.align`
|
|
561
|
+
* (the same geometry `scroll-snap-align` expresses in CSS). jsdom returns zero rects for every element, so
|
|
562
|
+
* this resolves to 0 there — a real-engine-only proof (the browser leg's seam-jump assertion). */
|
|
563
|
+
#alignedOffset(target) {
|
|
564
|
+
const track = this.#track;
|
|
565
|
+
if (!track)
|
|
566
|
+
return 0;
|
|
567
|
+
const trackRect = track.getBoundingClientRect();
|
|
568
|
+
const targetRect = target.getBoundingClientRect();
|
|
569
|
+
const horizontal = this.orientation === 'horizontal';
|
|
570
|
+
const trackStart = horizontal ? trackRect.left : trackRect.top;
|
|
571
|
+
const trackSize = horizontal ? trackRect.width : trackRect.height;
|
|
572
|
+
const targetStart = horizontal ? targetRect.left : targetRect.top;
|
|
573
|
+
const targetSize = horizontal ? targetRect.width : targetRect.height;
|
|
574
|
+
const base = this.#scrollPos() + (targetStart - trackStart);
|
|
575
|
+
if (this.align === 'center')
|
|
576
|
+
return base - (trackSize - targetSize) / 2;
|
|
577
|
+
if (this.align === 'end')
|
|
578
|
+
return base - (trackSize - targetSize);
|
|
579
|
+
return base;
|
|
580
|
+
}
|
|
581
|
+
/** Scroll (or jump) to `target`: instant under reduced-motion or while `#teleporting`, else a JS `rAF` scroll
|
|
582
|
+
* animation over `--ui-swiper-duration`/`--ui-swiper-easing` (F1 — native `scroll-behavior: smooth` ignores
|
|
583
|
+
* author custom properties, so this is NOT that). */
|
|
584
|
+
#animateTo(target) {
|
|
585
|
+
const to = this.#alignedOffset(target);
|
|
586
|
+
const from = this.#scrollPos();
|
|
587
|
+
if (this.#prefersReducedMotion() || this.#teleporting || Math.abs(to - from) < 0.5) {
|
|
588
|
+
this.#setScrollPos(to, 'auto');
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
this.#runScrollAnimation(from, to);
|
|
592
|
+
}
|
|
593
|
+
#runScrollAnimation(from, to) {
|
|
594
|
+
if (this.#animFrame !== null)
|
|
595
|
+
cancelAnimationFrame(this.#animFrame);
|
|
596
|
+
const track = this.#track;
|
|
597
|
+
const durationRaw = track ? getComputedStyle(track).getPropertyValue('--ui-swiper-duration') : '';
|
|
598
|
+
const easingRaw = track ? getComputedStyle(track).getPropertyValue('--ui-swiper-easing') : '';
|
|
599
|
+
const durationMs = parseCssTime(durationRaw, 250);
|
|
600
|
+
const ease = bezierEase(parseCubicBezier(easingRaw || 'ease-in-out'));
|
|
601
|
+
const start = performance.now();
|
|
602
|
+
const step = (now) => {
|
|
603
|
+
const elapsed = now - start;
|
|
604
|
+
const t = durationMs <= 0 ? 1 : Math.min(1, elapsed / durationMs);
|
|
605
|
+
this.#setScrollPos(from + (to - from) * ease(t), 'auto');
|
|
606
|
+
if (t < 1)
|
|
607
|
+
this.#animFrame = requestAnimationFrame(step);
|
|
608
|
+
else
|
|
609
|
+
this.#animFrame = null;
|
|
610
|
+
};
|
|
611
|
+
this.#animFrame = requestAnimationFrame(step);
|
|
612
|
+
}
|
|
613
|
+
// ── chrome drive (LLD-C9/C10/C11, SPEC-R12) ────────────────────────────────────────────────────────────
|
|
614
|
+
#findPagination() {
|
|
615
|
+
const existing = this.querySelector(':scope > ui-swiper-pagination');
|
|
616
|
+
if (existing instanceof UISwiperPaginationElement)
|
|
617
|
+
return existing;
|
|
618
|
+
if (!this.pagination)
|
|
619
|
+
return null;
|
|
620
|
+
const stamped = document.createElement('ui-swiper-pagination');
|
|
621
|
+
stamped.setAttribute('data-default', ''); // default placement below the track (swiper.css)
|
|
622
|
+
this.appendChild(stamped);
|
|
623
|
+
return stamped;
|
|
624
|
+
}
|
|
625
|
+
#findPaddles() {
|
|
626
|
+
const existing = this.querySelector(':scope > ui-swiper-paddles');
|
|
627
|
+
if (existing instanceof UISwiperPaddlesElement)
|
|
628
|
+
return existing;
|
|
629
|
+
if (!this.paddles)
|
|
630
|
+
return null;
|
|
631
|
+
const stamped = document.createElement('ui-swiper-paddles');
|
|
632
|
+
stamped.setAttribute('data-default', ''); // default placement overlaid on the track (swiper.css)
|
|
633
|
+
this.insertBefore(stamped, this.#track);
|
|
634
|
+
return stamped;
|
|
635
|
+
}
|
|
636
|
+
/** Find a descendant pagination/paddles/label anchor (present wins over the boolean stamp); drive each in
|
|
637
|
+
* place. Re-run on child mutation + on active/pagination/paddles/orientation change (the single connected()
|
|
638
|
+
* effect above). */
|
|
639
|
+
#driveChrome() {
|
|
640
|
+
const pagination = this.#findPagination();
|
|
641
|
+
const paddles = this.#findPaddles();
|
|
642
|
+
if (pagination)
|
|
643
|
+
pagination.renderInto(this.#slides.length, this.activeIndex, (i) => this.goTo(i));
|
|
644
|
+
if (paddles) {
|
|
645
|
+
paddles.fill(() => this.prev(), () => this.next(), this.orientation);
|
|
646
|
+
const prevBtn = paddles.querySelector('[data-part="prev"]');
|
|
647
|
+
const nextBtn = paddles.querySelector('[data-part="next"]');
|
|
648
|
+
const atStart = this.activeIndex <= 0;
|
|
649
|
+
const atEnd = this.activeIndex >= this.#slides.length - 1;
|
|
650
|
+
if (prevBtn)
|
|
651
|
+
prevBtn.disabled = !this.loop && atStart; // paddles never disable in loop mode
|
|
652
|
+
if (nextBtn)
|
|
653
|
+
nextBtn.disabled = !this.loop && atEnd;
|
|
654
|
+
}
|
|
655
|
+
this.#applyRegionLabel();
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
if (!customElements.get('ui-swiper'))
|
|
659
|
+
customElements.define('ui-swiper', UISwiperElement);
|