@pindoba/core-carousel 0.0.0-alpha-20260617191010

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,479 @@
1
+ import { anchorFor, clampIndex, edgeState, effectiveAlign, nearestIndex } from "./snap-math.js";
2
+ import { atom } from "nanostores";
3
+ //#region src/create-carousel.ts
4
+ /**
5
+ * Headless carousel factory. Owns all DOM-side behaviour (scroll-tracking,
6
+ * keyboard, autoplay, programmatic scrollTo) so framework components can
7
+ * remain purely presentational. The carousel never traps the page's vertical
8
+ * scroll: a horizontal viewport lets a vertical wheel chain to the page
9
+ * natively (its CSS `overscroll-behavior` contains only the x axis), and a
10
+ * vertical viewport — whose scroll axis IS the page's — forwards the wheel to
11
+ * the page explicitly (it's navigated via controls / drag / keyboard / touch).
12
+ */
13
+ function createCarousel(config = {}) {
14
+ const $activeIndex = atom(config.initialIndex ?? 0);
15
+ const $slideCount = atom(config.slideCount ?? 0);
16
+ const $orientation = atom(config.orientation ?? "horizontal");
17
+ const $align = atom(config.align ?? "start");
18
+ const $loop = atom(config.loop ?? false);
19
+ const $autoplayMs = atom(typeof config.autoplay === "number" && config.autoplay > 0 ? config.autoplay : 0);
20
+ const $atScrollStart = atom(true);
21
+ const $atScrollEnd = atom(false);
22
+ const inertOffscreen = config.inertOffscreenSlides ?? false;
23
+ let viewportEl = null;
24
+ let rootEl = null;
25
+ let isRtl = false;
26
+ const teardownFns = [];
27
+ let snapRestoreTimer = null;
28
+ let removeSnapScrollEnd = null;
29
+ let resetAutoplay = null;
30
+ let pauseAutoplayForDrag = null;
31
+ let resumeAutoplayForDrag = null;
32
+ const VISIBILITY_THRESHOLD = .5;
33
+ function clamp(i) {
34
+ return clampIndex(i, $slideCount.get(), $loop.get());
35
+ }
36
+ function getItems() {
37
+ const vp = viewportEl;
38
+ if (!vp) return [];
39
+ return Array.from(vp.querySelectorAll("[data-component='carousel-item']"));
40
+ }
41
+ function getAxis() {
42
+ return $orientation.get() === "vertical" ? "y" : "x";
43
+ }
44
+ function computeDirection() {
45
+ isRtl = !!viewportEl && typeof getComputedStyle !== "undefined" && getComputedStyle(viewportEl).direction === "rtl";
46
+ }
47
+ function mainAxisExtent(rect, axis) {
48
+ return axis === "x" ? {
49
+ start: rect.left,
50
+ end: rect.right
51
+ } : {
52
+ start: rect.top,
53
+ end: rect.bottom
54
+ };
55
+ }
56
+ function clearSnapRestore() {
57
+ if (snapRestoreTimer != null) {
58
+ clearTimeout(snapRestoreTimer);
59
+ snapRestoreTimer = null;
60
+ }
61
+ if (removeSnapScrollEnd) {
62
+ removeSnapScrollEnd();
63
+ removeSnapScrollEnd = null;
64
+ }
65
+ }
66
+ function scheduleSnapRestore() {
67
+ const vp = viewportEl;
68
+ if (!vp) return;
69
+ clearSnapRestore();
70
+ const restore = () => {
71
+ if (viewportEl) viewportEl.style.scrollSnapType = "";
72
+ clearSnapRestore();
73
+ };
74
+ if ("onscrollend" in vp) {
75
+ const onScrollEnd = () => restore();
76
+ vp.addEventListener("scrollend", onScrollEnd, { once: true });
77
+ removeSnapScrollEnd = () => vp.removeEventListener("scrollend", onScrollEnd);
78
+ }
79
+ snapRestoreTimer = setTimeout(restore, 500);
80
+ }
81
+ function setActiveIndex(index) {
82
+ const next = clamp(index);
83
+ if ($activeIndex.get() === next) return;
84
+ $activeIndex.set(next);
85
+ config.onActiveIndexChange?.(next);
86
+ }
87
+ function setSlideCount(count) {
88
+ const safe = Math.max(0, count);
89
+ if ($slideCount.get() === safe) return;
90
+ $slideCount.set(safe);
91
+ setActiveIndex($activeIndex.get());
92
+ }
93
+ function setOrientation(orientation) {
94
+ $orientation.set(orientation);
95
+ }
96
+ function setAlign(align) {
97
+ $align.set(align);
98
+ }
99
+ function setLoop(loop) {
100
+ $loop.set(loop);
101
+ setActiveIndex($activeIndex.get());
102
+ }
103
+ function setAutoplay(autoplay) {
104
+ const ms = typeof autoplay === "number" && autoplay > 0 ? autoplay : 0;
105
+ $autoplayMs.set(ms);
106
+ }
107
+ function next() {
108
+ setActiveIndex($activeIndex.get() + 1);
109
+ }
110
+ function prev() {
111
+ setActiveIndex($activeIndex.get() - 1);
112
+ }
113
+ function scrollToIndex(index, options = {}) {
114
+ const vp = viewportEl;
115
+ if (!vp) return;
116
+ if (vp.dataset.dragging === "true") return;
117
+ const items = getItems();
118
+ if (items.length === 0) return;
119
+ const item = items[clampIndex(index, items.length, $loop.get())];
120
+ if (!item) return;
121
+ computeDirection();
122
+ const axis = getAxis();
123
+ const align = effectiveAlign($align.get(), isRtl && axis === "x");
124
+ const vpE = mainAxisExtent(vp.getBoundingClientRect(), axis);
125
+ const itemE = mainAxisExtent(item.getBoundingClientRect(), axis);
126
+ const delta = anchorFor(itemE.start, itemE.end, align) - anchorFor(vpE.start, vpE.end, align);
127
+ vp.style.scrollSnapType = "none";
128
+ const behavior = options.behavior ?? "smooth";
129
+ if (axis === "x") vp.scrollBy({
130
+ left: delta,
131
+ behavior
132
+ });
133
+ else vp.scrollBy({
134
+ top: delta,
135
+ behavior
136
+ });
137
+ scheduleSnapRestore();
138
+ }
139
+ function userGoTo(index) {
140
+ scrollToIndex(index);
141
+ resetAutoplay?.();
142
+ }
143
+ function updateActiveFromScroll() {
144
+ const vp = viewportEl;
145
+ if (!vp) return;
146
+ const items = getItems();
147
+ if (items.length === 0) return;
148
+ computeDirection();
149
+ const axis = getAxis();
150
+ const align = effectiveAlign($align.get(), isRtl && axis === "x");
151
+ const vpExtent = mainAxisExtent(vp.getBoundingClientRect(), axis);
152
+ setActiveIndex(nearestIndex(items.map((it) => mainAxisExtent(it.getBoundingClientRect(), axis)), vpExtent, align));
153
+ }
154
+ function updateEdgeStateAndButtons() {
155
+ const vp = viewportEl;
156
+ if (!vp) return;
157
+ const axis = getAxis();
158
+ const max = (axis === "x" ? vp.scrollWidth : vp.scrollHeight) - (axis === "x" ? vp.clientWidth : vp.clientHeight);
159
+ const { atStart, atEnd } = edgeState(Math.abs(axis === "x" ? vp.scrollLeft : vp.scrollTop), max);
160
+ if ($atScrollStart.get() !== atStart) $atScrollStart.set(atStart);
161
+ if ($atScrollEnd.get() !== atEnd) $atScrollEnd.set(atEnd);
162
+ if (!rootEl) return;
163
+ const prevBtn = rootEl.querySelector("[data-carousel-prev]");
164
+ const nextBtn = rootEl.querySelector("[data-carousel-next]");
165
+ if ($loop.get()) {
166
+ if (prevBtn) prevBtn.toggleAttribute("disabled", false);
167
+ if (nextBtn) nextBtn.toggleAttribute("disabled", false);
168
+ return;
169
+ }
170
+ if (prevBtn) prevBtn.toggleAttribute("disabled", atStart);
171
+ if (nextBtn) nextBtn.toggleAttribute("disabled", atEnd);
172
+ }
173
+ function updateSlideVisibility() {
174
+ if (!inertOffscreen) return;
175
+ const vp = viewportEl;
176
+ if (!vp) return;
177
+ const items = getItems();
178
+ if (items.length === 0) return;
179
+ const axis = getAxis();
180
+ const v = mainAxisExtent(vp.getBoundingClientRect(), axis);
181
+ for (const item of items) {
182
+ const i = mainAxisExtent(item.getBoundingClientRect(), axis);
183
+ const size = i.end - i.start;
184
+ const overlap = Math.max(0, Math.min(i.end, v.end) - Math.max(i.start, v.start));
185
+ if (size > 0 && overlap / size >= VISIBILITY_THRESHOLD) {
186
+ item.removeAttribute("aria-hidden");
187
+ item.removeAttribute("inert");
188
+ } else {
189
+ item.setAttribute("aria-hidden", "true");
190
+ item.setAttribute("inert", "");
191
+ }
192
+ }
193
+ }
194
+ function teardownAll() {
195
+ while (teardownFns.length > 0) {
196
+ const fn = teardownFns.pop();
197
+ try {
198
+ fn?.();
199
+ } catch {}
200
+ }
201
+ clearSnapRestore();
202
+ if (viewportEl) viewportEl.style.scrollSnapType = "";
203
+ viewportEl = null;
204
+ rootEl = null;
205
+ resetAutoplay = null;
206
+ }
207
+ function registerViewport(viewport, root) {
208
+ teardownAll();
209
+ viewportEl = viewport;
210
+ rootEl = root;
211
+ setSlideCount(getItems().length);
212
+ let scrollRaf = 0;
213
+ const onScroll = () => {
214
+ if (scrollRaf) return;
215
+ scrollRaf = requestAnimationFrame(() => {
216
+ scrollRaf = 0;
217
+ updateActiveFromScroll();
218
+ updateEdgeStateAndButtons();
219
+ updateSlideVisibility();
220
+ });
221
+ };
222
+ viewport.addEventListener("scroll", onScroll, { passive: true });
223
+ teardownFns.push(() => {
224
+ viewport.removeEventListener("scroll", onScroll);
225
+ if (scrollRaf) cancelAnimationFrame(scrollRaf);
226
+ });
227
+ if (typeof ResizeObserver !== "undefined") {
228
+ const ro = new ResizeObserver(() => {
229
+ setSlideCount(getItems().length);
230
+ updateActiveFromScroll();
231
+ updateEdgeStateAndButtons();
232
+ updateSlideVisibility();
233
+ });
234
+ ro.observe(viewport);
235
+ teardownFns.push(() => ro.disconnect());
236
+ }
237
+ const onKeydown = (e) => {
238
+ const ke = e;
239
+ const axis = getAxis();
240
+ const forward = axis === "x" ? "ArrowRight" : "ArrowDown";
241
+ const back = axis === "x" ? "ArrowLeft" : "ArrowUp";
242
+ if (ke.key === forward) {
243
+ ke.preventDefault();
244
+ userGoTo($activeIndex.get() + 1);
245
+ } else if (ke.key === back) {
246
+ ke.preventDefault();
247
+ userGoTo($activeIndex.get() - 1);
248
+ } else if (ke.key === "Home") {
249
+ ke.preventDefault();
250
+ userGoTo(0);
251
+ } else if (ke.key === "End") {
252
+ ke.preventDefault();
253
+ userGoTo($slideCount.get() - 1);
254
+ }
255
+ };
256
+ viewport.addEventListener("keydown", onKeydown);
257
+ teardownFns.push(() => viewport.removeEventListener("keydown", onKeydown));
258
+ const onWheel = (e) => {
259
+ if (getAxis() !== "y") return;
260
+ if (e.deltaY === 0) return;
261
+ e.preventDefault();
262
+ (document.scrollingElement ?? document.documentElement).scrollBy({ top: e.deltaY });
263
+ };
264
+ viewport.addEventListener("wheel", onWheel, { passive: false });
265
+ teardownFns.push(() => viewport.removeEventListener("wheel", onWheel));
266
+ const DRAG_THRESHOLD = 6;
267
+ let dragPointerId = null;
268
+ let dragStartX = 0;
269
+ let dragStartY = 0;
270
+ let dragBaselineLeft = 0;
271
+ let dragBaselineTop = 0;
272
+ let dragActive = false;
273
+ let prevScrollBehavior = "";
274
+ const suppressNextClick = () => {
275
+ const onClickCapture = (ev) => {
276
+ ev.stopPropagation();
277
+ ev.preventDefault();
278
+ };
279
+ viewport.addEventListener("click", onClickCapture, { capture: true });
280
+ setTimeout(() => {
281
+ viewport.removeEventListener("click", onClickCapture, { capture: true });
282
+ }, 0);
283
+ };
284
+ const onPointerDown = (e) => {
285
+ if (e.pointerType !== "mouse") return;
286
+ if (e.button !== 0) return;
287
+ clearSnapRestore();
288
+ dragPointerId = e.pointerId;
289
+ dragStartX = e.clientX;
290
+ dragStartY = e.clientY;
291
+ dragBaselineLeft = viewport.scrollLeft;
292
+ dragBaselineTop = viewport.scrollTop;
293
+ dragActive = false;
294
+ };
295
+ const onPointerMove = (e) => {
296
+ if (dragPointerId == null || e.pointerId !== dragPointerId) return;
297
+ const axis = getAxis();
298
+ const dx = e.clientX - dragStartX;
299
+ const dy = e.clientY - dragStartY;
300
+ const primary = axis === "x" ? dx : dy;
301
+ if (!dragActive) {
302
+ if (Math.abs(primary) < DRAG_THRESHOLD) return;
303
+ dragActive = true;
304
+ viewport.dataset.dragging = "true";
305
+ prevScrollBehavior = viewport.style.scrollBehavior;
306
+ viewport.style.scrollSnapType = "none";
307
+ viewport.style.scrollBehavior = "auto";
308
+ try {
309
+ viewport.setPointerCapture(e.pointerId);
310
+ } catch {}
311
+ pauseAutoplayForDrag?.();
312
+ }
313
+ e.preventDefault();
314
+ if (axis === "x") viewport.scrollLeft = dragBaselineLeft - dx;
315
+ else viewport.scrollTop = dragBaselineTop - dy;
316
+ };
317
+ const endDrag = (e) => {
318
+ if (dragPointerId == null || e.pointerId !== dragPointerId) return;
319
+ const wasActive = dragActive;
320
+ dragPointerId = null;
321
+ dragActive = false;
322
+ if (!wasActive) return;
323
+ try {
324
+ viewport.releasePointerCapture(e.pointerId);
325
+ } catch {}
326
+ delete viewport.dataset.dragging;
327
+ viewport.style.scrollSnapType = "";
328
+ viewport.style.scrollBehavior = prevScrollBehavior;
329
+ suppressNextClick();
330
+ const items = getItems();
331
+ if (items.length > 0) {
332
+ computeDirection();
333
+ const axis = getAxis();
334
+ const align = effectiveAlign($align.get(), isRtl && axis === "x");
335
+ const dragDelta = axis === "x" ? viewport.scrollLeft - dragBaselineLeft : viewport.scrollTop - dragBaselineTop;
336
+ const firstItem = items[0];
337
+ const slideSize = axis === "x" ? firstItem.offsetWidth : firstItem.offsetHeight;
338
+ const bias = slideSize > 0 ? Math.sign(dragDelta) * slideSize * .2 : 0;
339
+ const vpExtent = mainAxisExtent(viewport.getBoundingClientRect(), axis);
340
+ scrollToIndex(nearestIndex(items.map((it) => mainAxisExtent(it.getBoundingClientRect(), axis)), vpExtent, align, bias), { behavior: typeof window !== "undefined" && window.matchMedia("(prefers-reduced-motion: reduce)").matches ? "auto" : "smooth" });
341
+ }
342
+ if (!!root.matches(":hover")) resumeAutoplayForDrag?.();
343
+ resetAutoplay?.();
344
+ };
345
+ viewport.addEventListener("pointerdown", onPointerDown);
346
+ viewport.addEventListener("pointermove", onPointerMove);
347
+ viewport.addEventListener("pointerup", endDrag);
348
+ viewport.addEventListener("pointercancel", endDrag);
349
+ teardownFns.push(() => {
350
+ viewport.removeEventListener("pointerdown", onPointerDown);
351
+ viewport.removeEventListener("pointermove", onPointerMove);
352
+ viewport.removeEventListener("pointerup", endDrag);
353
+ viewport.removeEventListener("pointercancel", endDrag);
354
+ delete viewport.dataset.dragging;
355
+ });
356
+ teardownFns.push($align.listen(() => {
357
+ updateActiveFromScroll();
358
+ }));
359
+ teardownFns.push($orientation.listen(() => {
360
+ updateActiveFromScroll();
361
+ updateEdgeStateAndButtons();
362
+ updateSlideVisibility();
363
+ }));
364
+ teardownFns.push($loop.listen(() => {
365
+ updateEdgeStateAndButtons();
366
+ }));
367
+ let autoplayTeardown = null;
368
+ const wireAutoplay = () => {
369
+ autoplayTeardown?.();
370
+ autoplayTeardown = null;
371
+ resetAutoplay = null;
372
+ pauseAutoplayForDrag = null;
373
+ resumeAutoplayForDrag = null;
374
+ const ms = $autoplayMs.get();
375
+ if (ms <= 0) return;
376
+ if (typeof window === "undefined") return;
377
+ if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
378
+ let timer = null;
379
+ let paused = false;
380
+ const tick = () => {
381
+ if (paused) return;
382
+ const cur = $activeIndex.get();
383
+ const count = $slideCount.get();
384
+ const nextIndex = cur + 1;
385
+ scrollToIndex($loop.get() ? nextIndex : nextIndex >= count ? 0 : nextIndex);
386
+ };
387
+ const start = () => {
388
+ if (timer != null) return;
389
+ timer = setInterval(tick, ms);
390
+ };
391
+ const stop = () => {
392
+ if (timer != null) clearInterval(timer);
393
+ timer = null;
394
+ };
395
+ const pause = () => {
396
+ paused = true;
397
+ stop();
398
+ };
399
+ const resume = () => {
400
+ paused = false;
401
+ start();
402
+ };
403
+ const onVis = () => {
404
+ if (document.visibilityState === "hidden") pause();
405
+ else resume();
406
+ };
407
+ root.addEventListener("pointerenter", pause);
408
+ root.addEventListener("pointerleave", resume);
409
+ root.addEventListener("focusin", pause);
410
+ root.addEventListener("focusout", resume);
411
+ document.addEventListener("visibilitychange", onVis);
412
+ start();
413
+ resetAutoplay = () => {
414
+ if (paused) return;
415
+ stop();
416
+ start();
417
+ };
418
+ pauseAutoplayForDrag = pause;
419
+ resumeAutoplayForDrag = resume;
420
+ autoplayTeardown = () => {
421
+ stop();
422
+ root.removeEventListener("pointerenter", pause);
423
+ root.removeEventListener("pointerleave", resume);
424
+ root.removeEventListener("focusin", pause);
425
+ root.removeEventListener("focusout", resume);
426
+ document.removeEventListener("visibilitychange", onVis);
427
+ };
428
+ };
429
+ wireAutoplay();
430
+ teardownFns.push($autoplayMs.listen(() => wireAutoplay()));
431
+ teardownFns.push(() => {
432
+ autoplayTeardown?.();
433
+ autoplayTeardown = null;
434
+ });
435
+ teardownFns.push(() => {
436
+ for (const item of getItems()) {
437
+ item.removeAttribute("aria-hidden");
438
+ item.removeAttribute("inert");
439
+ }
440
+ });
441
+ const initialSync = requestAnimationFrame(() => {
442
+ setSlideCount(getItems().length);
443
+ updateActiveFromScroll();
444
+ updateEdgeStateAndButtons();
445
+ updateSlideVisibility();
446
+ });
447
+ teardownFns.push(() => cancelAnimationFrame(initialSync));
448
+ return () => teardownAll();
449
+ }
450
+ function destroy() {
451
+ teardownAll();
452
+ }
453
+ return {
454
+ $activeIndex,
455
+ $slideCount,
456
+ $orientation,
457
+ $align,
458
+ $loop,
459
+ $autoplayMs,
460
+ $atScrollStart,
461
+ $atScrollEnd,
462
+ setActiveIndex,
463
+ setSlideCount,
464
+ setOrientation,
465
+ setAlign,
466
+ setLoop,
467
+ setAutoplay,
468
+ next,
469
+ prev,
470
+ registerViewport,
471
+ scrollToIndex,
472
+ userGoTo,
473
+ destroy
474
+ };
475
+ }
476
+ //#endregion
477
+ export { createCarousel };
478
+
479
+ //# sourceMappingURL=create-carousel.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"create-carousel.js","names":[],"sources":["../../src/create-carousel.ts"],"sourcesContent":["import { atom } from \"nanostores\";\nimport {\n anchorFor,\n clampIndex,\n edgeState,\n effectiveAlign,\n nearestIndex,\n type Extent,\n} from \"./snap-math.js\";\nimport type {\n HeadlessCarousel,\n ScrollToIndexOptions,\n CarouselAlign,\n CarouselConfig,\n CarouselOrientation,\n} from \"./types.js\";\n\n/**\n * Headless carousel factory. Owns all DOM-side behaviour (scroll-tracking,\n * keyboard, autoplay, programmatic scrollTo) so framework components can\n * remain purely presentational. The carousel never traps the page's vertical\n * scroll: a horizontal viewport lets a vertical wheel chain to the page\n * natively (its CSS `overscroll-behavior` contains only the x axis), and a\n * vertical viewport — whose scroll axis IS the page's — forwards the wheel to\n * the page explicitly (it's navigated via controls / drag / keyboard / touch).\n */\nexport function createCarousel(config: CarouselConfig = {}): HeadlessCarousel {\n // --- State ---\n const $activeIndex = atom<number>(config.initialIndex ?? 0);\n const $slideCount = atom<number>(config.slideCount ?? 0);\n const $orientation = atom<CarouselOrientation>(\n config.orientation ?? \"horizontal\",\n );\n const $align = atom<CarouselAlign>(config.align ?? \"start\");\n const $loop = atom<boolean>(config.loop ?? false);\n const $autoplayMs = atom<number>(\n typeof config.autoplay === \"number\" && config.autoplay > 0\n ? config.autoplay\n : 0,\n );\n const $atScrollStart = atom<boolean>(true);\n const $atScrollEnd = atom<boolean>(false);\n\n // Opt-in: mark off-screen slides aria-hidden + inert (APG carousel pattern).\n const inertOffscreen = config.inertOffscreenSlides ?? false;\n\n // --- DOM refs + teardown registry ---\n let viewportEl: HTMLElement | null = null;\n let rootEl: HTMLElement | null = null;\n let isRtl = false;\n const teardownFns: Array<() => void> = [];\n let snapRestoreTimer: ReturnType<typeof setTimeout> | null = null;\n let removeSnapScrollEnd: (() => void) | null = null;\n let resetAutoplay: (() => void) | null = null;\n let pauseAutoplayForDrag: (() => void) | null = null;\n let resumeAutoplayForDrag: (() => void) | null = null;\n\n const VISIBILITY_THRESHOLD = 0.5;\n\n // --- Helpers ---\n function clamp(i: number) {\n return clampIndex(i, $slideCount.get(), $loop.get());\n }\n\n function getItems(): HTMLElement[] {\n const vp = viewportEl;\n if (!vp) return [];\n return Array.from(\n vp.querySelectorAll<HTMLElement>(\"[data-component='carousel-item']\"),\n );\n }\n\n function getAxis(): \"x\" | \"y\" {\n return $orientation.get() === \"vertical\" ? \"y\" : \"x\";\n }\n\n // RTL only affects the horizontal axis. Cache the direction once per layout\n // pass (scroll rAF / resize / initial sync) rather than reading computed\n // style per item — getComputedStyle forces a reflow.\n function computeDirection() {\n isRtl =\n !!viewportEl &&\n typeof getComputedStyle !== \"undefined\" &&\n getComputedStyle(viewportEl).direction === \"rtl\";\n }\n\n function mainAxisExtent(rect: DOMRect, axis: \"x\" | \"y\"): Extent {\n return axis === \"x\"\n ? { start: rect.left, end: rect.right }\n : { start: rect.top, end: rect.bottom };\n }\n\n // --- Snap-type suspension (restored on scrollend, with a 500ms fallback) ---\n function clearSnapRestore() {\n if (snapRestoreTimer != null) {\n clearTimeout(snapRestoreTimer);\n snapRestoreTimer = null;\n }\n if (removeSnapScrollEnd) {\n removeSnapScrollEnd();\n removeSnapScrollEnd = null;\n }\n }\n\n function scheduleSnapRestore() {\n const vp = viewportEl;\n if (!vp) return;\n clearSnapRestore();\n const restore = () => {\n if (viewportEl) viewportEl.style.scrollSnapType = \"\";\n clearSnapRestore();\n };\n // Prefer the `scrollend` event so a long smooth scroll isn't cut short by\n // snap re-enabling at a fixed 500ms. Keep the timeout as a fallback for\n // engines without `scrollend` and for `behavior: \"auto\"` (where the event\n // may fire synchronously or not at all).\n if (\"onscrollend\" in vp) {\n const onScrollEnd = () => restore();\n vp.addEventListener(\"scrollend\", onScrollEnd, { once: true });\n removeSnapScrollEnd = () =>\n vp.removeEventListener(\"scrollend\", onScrollEnd);\n }\n snapRestoreTimer = setTimeout(restore, 500);\n }\n\n // --- Index navigation ---\n function setActiveIndex(index: number) {\n const next = clamp(index);\n if ($activeIndex.get() === next) return;\n $activeIndex.set(next);\n config.onActiveIndexChange?.(next);\n }\n\n function setSlideCount(count: number) {\n const safe = Math.max(0, count);\n if ($slideCount.get() === safe) return;\n $slideCount.set(safe);\n // Re-clamp active index if now out of range.\n setActiveIndex($activeIndex.get());\n }\n\n function setOrientation(orientation: CarouselOrientation) {\n $orientation.set(orientation);\n }\n\n function setAlign(align: CarouselAlign) {\n $align.set(align);\n }\n\n function setLoop(loop: boolean) {\n $loop.set(loop);\n setActiveIndex($activeIndex.get());\n }\n\n function setAutoplay(autoplay: number | false) {\n const ms = typeof autoplay === \"number\" && autoplay > 0 ? autoplay : 0;\n $autoplayMs.set(ms);\n }\n\n function next() {\n setActiveIndex($activeIndex.get() + 1);\n }\n\n function prev() {\n setActiveIndex($activeIndex.get() - 1);\n }\n\n // --- Programmatic scroll ---\n function scrollToIndex(index: number, options: ScrollToIndexOptions = {}) {\n const vp = viewportEl;\n if (!vp) return;\n // While the user is dragging, programmatic scrolls would fight the drag\n // and visibly jump the slide. Skip — the drag handler will snap on release.\n if (vp.dataset.dragging === \"true\") return;\n const items = getItems();\n if (items.length === 0) return;\n const clamped = clampIndex(index, items.length, $loop.get());\n const item = items[clamped];\n if (!item) return;\n\n computeDirection();\n const axis = getAxis();\n const align = effectiveAlign($align.get(), isRtl && axis === \"x\");\n const vpE = mainAxisExtent(vp.getBoundingClientRect(), axis);\n const itemE = mainAxisExtent(item.getBoundingClientRect(), axis);\n // Relative scroll (scrollBy) sidesteps the cross-browser scrollLeft-sign\n // ambiguity in RTL — the delta is computed from on-screen rects, which are\n // direction-agnostic.\n const delta =\n anchorFor(itemE.start, itemE.end, align) -\n anchorFor(vpE.start, vpE.end, align);\n\n // Mandatory snap can short-circuit a smooth scroll that passes near an\n // unreachable snap target (e.g. first/last slide in center/end align).\n // Disable snap during the animation, restore on scrollend / after 500ms.\n vp.style.scrollSnapType = \"none\";\n const behavior = options.behavior ?? \"smooth\";\n if (axis === \"x\") {\n vp.scrollBy({ left: delta, behavior });\n } else {\n vp.scrollBy({ top: delta, behavior });\n }\n scheduleSnapRestore();\n }\n\n function userGoTo(index: number) {\n scrollToIndex(index);\n resetAutoplay?.();\n }\n\n // --- Scroll-driven sync ---\n function updateActiveFromScroll() {\n const vp = viewportEl;\n if (!vp) return;\n const items = getItems();\n if (items.length === 0) return;\n computeDirection();\n const axis = getAxis();\n const align = effectiveAlign($align.get(), isRtl && axis === \"x\");\n const vpExtent = mainAxisExtent(vp.getBoundingClientRect(), axis);\n const extents = items.map((it) =>\n mainAxisExtent(it.getBoundingClientRect(), axis),\n );\n setActiveIndex(nearestIndex(extents, vpExtent, align));\n }\n\n // Keep the prev/next button disabled state in sync with the real scroll\n // position (not activeIndex — with slidesPerView ≥ 2 the scroll reaches its\n // max well before activeIndex hits items.length - 1).\n function updateEdgeStateAndButtons() {\n const vp = viewportEl;\n if (!vp) return;\n const axis = getAxis();\n const scrollSize = axis === \"x\" ? vp.scrollWidth : vp.scrollHeight;\n const clientSize = axis === \"x\" ? vp.clientWidth : vp.clientHeight;\n const max = scrollSize - clientSize;\n // Math.abs handles RTL, where scrollLeft is negative in modern engines.\n const current = Math.abs(axis === \"x\" ? vp.scrollLeft : vp.scrollTop);\n const { atStart, atEnd } = edgeState(current, max);\n if ($atScrollStart.get() !== atStart) $atScrollStart.set(atStart);\n if ($atScrollEnd.get() !== atEnd) $atScrollEnd.set(atEnd);\n if (!rootEl) return;\n const prevBtn = rootEl.querySelector<HTMLButtonElement>(\n \"[data-carousel-prev]\",\n );\n const nextBtn = rootEl.querySelector<HTMLButtonElement>(\n \"[data-carousel-next]\",\n );\n if ($loop.get()) {\n if (prevBtn) prevBtn.toggleAttribute(\"disabled\", false);\n if (nextBtn) nextBtn.toggleAttribute(\"disabled\", false);\n return;\n }\n if (prevBtn) prevBtn.toggleAttribute(\"disabled\", atStart);\n if (nextBtn) nextBtn.toggleAttribute(\"disabled\", atEnd);\n }\n\n // Mark slides that are mostly off-screen as aria-hidden + inert, so AT and\n // the tab order skip them (APG carousel behaviour). Opt-in via config because\n // `inert` changes focus reachability. Pure rect-overlap → orientation- and\n // direction-agnostic, and handles slidesPerView ≥ 2 (multiple visible slides).\n function updateSlideVisibility() {\n if (!inertOffscreen) return;\n const vp = viewportEl;\n if (!vp) return;\n const items = getItems();\n if (items.length === 0) return;\n const axis = getAxis();\n const v = mainAxisExtent(vp.getBoundingClientRect(), axis);\n for (const item of items) {\n const i = mainAxisExtent(item.getBoundingClientRect(), axis);\n const size = i.end - i.start;\n const overlap = Math.max(\n 0,\n Math.min(i.end, v.end) - Math.max(i.start, v.start),\n );\n const visible = size > 0 && overlap / size >= VISIBILITY_THRESHOLD;\n if (visible) {\n item.removeAttribute(\"aria-hidden\");\n item.removeAttribute(\"inert\");\n } else {\n item.setAttribute(\"aria-hidden\", \"true\");\n item.setAttribute(\"inert\", \"\");\n }\n }\n }\n\n // --- Element binding ---\n function teardownAll() {\n while (teardownFns.length > 0) {\n const fn = teardownFns.pop();\n try {\n fn?.();\n } catch {\n // Swallow teardown errors so subsequent cleanups still run.\n }\n }\n clearSnapRestore();\n if (viewportEl) {\n viewportEl.style.scrollSnapType = \"\";\n }\n viewportEl = null;\n rootEl = null;\n resetAutoplay = null;\n }\n\n function registerViewport(viewport: HTMLElement, root: HTMLElement) {\n teardownAll();\n viewportEl = viewport;\n rootEl = root;\n\n // Sync slide count from DOM at registration time.\n setSlideCount(getItems().length);\n\n let scrollRaf = 0;\n const onScroll = () => {\n if (scrollRaf) return;\n scrollRaf = requestAnimationFrame(() => {\n scrollRaf = 0;\n updateActiveFromScroll();\n updateEdgeStateAndButtons();\n updateSlideVisibility();\n });\n };\n viewport.addEventListener(\"scroll\", onScroll, { passive: true });\n teardownFns.push(() => {\n viewport.removeEventListener(\"scroll\", onScroll);\n if (scrollRaf) cancelAnimationFrame(scrollRaf);\n });\n\n // Layout may settle later (e.g. the carousel lives in a tab panel that\n // starts hidden). Re-run the active / edge sync whenever the viewport\n // resizes — that fires when display: none flips to visible too.\n if (typeof ResizeObserver !== \"undefined\") {\n const ro = new ResizeObserver(() => {\n setSlideCount(getItems().length);\n updateActiveFromScroll();\n updateEdgeStateAndButtons();\n updateSlideVisibility();\n });\n ro.observe(viewport);\n teardownFns.push(() => ro.disconnect());\n }\n\n const onKeydown = (e: Event) => {\n const ke = e as KeyboardEvent;\n const axis = getAxis();\n const forward = axis === \"x\" ? \"ArrowRight\" : \"ArrowDown\";\n const back = axis === \"x\" ? \"ArrowLeft\" : \"ArrowUp\";\n if (ke.key === forward) {\n ke.preventDefault();\n userGoTo($activeIndex.get() + 1);\n } else if (ke.key === back) {\n ke.preventDefault();\n userGoTo($activeIndex.get() - 1);\n } else if (ke.key === \"Home\") {\n ke.preventDefault();\n userGoTo(0);\n } else if (ke.key === \"End\") {\n ke.preventDefault();\n userGoTo($slideCount.get() - 1);\n }\n };\n viewport.addEventListener(\"keydown\", onKeydown);\n teardownFns.push(() => viewport.removeEventListener(\"keydown\", onKeydown));\n\n // Keep the page scrollable when the pointer is over the carousel. A HORIZONTAL\n // viewport can't scroll on y, and its `overscroll-behavior-x: contain` (CSS)\n // leaves the y axis free, so a vertical wheel chains to the page natively —\n // nothing to do here. A VERTICAL viewport's scroll axis is the same as the\n // page's, so a wheel would scroll the carousel and trap the page; forward it\n // to the page instead (the carousel advances via controls / drag / keyboard).\n const onWheel = (e: WheelEvent) => {\n if (getAxis() !== \"y\") return;\n if (e.deltaY === 0) return;\n e.preventDefault();\n const scroller = document.scrollingElement ?? document.documentElement;\n scroller.scrollBy({ top: e.deltaY });\n };\n viewport.addEventListener(\"wheel\", onWheel, { passive: false });\n teardownFns.push(() => viewport.removeEventListener(\"wheel\", onWheel));\n\n // Mouse-drag swipe (desktop). Touch / pen continue to use native\n // scroll-snap — intercepting them would worsen the existing mobile UX.\n const DRAG_THRESHOLD = 6;\n let dragPointerId: number | null = null;\n let dragStartX = 0;\n let dragStartY = 0;\n let dragBaselineLeft = 0;\n let dragBaselineTop = 0;\n let dragActive = false;\n let prevScrollBehavior = \"\";\n\n const suppressNextClick = () => {\n const onClickCapture = (ev: MouseEvent) => {\n ev.stopPropagation();\n ev.preventDefault();\n };\n viewport.addEventListener(\"click\", onClickCapture, { capture: true });\n // Remove on the next tick — by then the synthetic click from this drag\n // has fired (or not). Use setTimeout so it survives the current event\n // loop turn rather than the same microtask as pointerup.\n setTimeout(() => {\n viewport.removeEventListener(\"click\", onClickCapture, {\n capture: true,\n });\n }, 0);\n };\n\n const onPointerDown = (e: PointerEvent) => {\n if (e.pointerType !== \"mouse\") return;\n if (e.button !== 0) return;\n // Cancel any pending snap-restore from a recent scrollToIndex — if it\n // fires mid-drag it re-enables mandatory snap and jumps the slide.\n clearSnapRestore();\n dragPointerId = e.pointerId;\n dragStartX = e.clientX;\n dragStartY = e.clientY;\n dragBaselineLeft = viewport.scrollLeft;\n dragBaselineTop = viewport.scrollTop;\n dragActive = false;\n };\n\n const onPointerMove = (e: PointerEvent) => {\n if (dragPointerId == null || e.pointerId !== dragPointerId) return;\n const axis = getAxis();\n const dx = e.clientX - dragStartX;\n const dy = e.clientY - dragStartY;\n const primary = axis === \"x\" ? dx : dy;\n if (!dragActive) {\n if (Math.abs(primary) < DRAG_THRESHOLD) return;\n dragActive = true;\n viewport.dataset.dragging = \"true\";\n prevScrollBehavior = viewport.style.scrollBehavior;\n viewport.style.scrollSnapType = \"none\";\n // CSS sets scroll-behavior: smooth, which animates every scrollLeft\n // assignment and turns the drag into a laggy mess. Force auto for\n // the duration of the drag.\n viewport.style.scrollBehavior = \"auto\";\n try {\n viewport.setPointerCapture(e.pointerId);\n } catch {\n // Some browsers refuse capture on non-target elements; safe to ignore.\n }\n // Pause autoplay explicitly: the pointerenter/leave handlers are\n // unreliable mid-drag (cursor may leave root while button is held).\n pauseAutoplayForDrag?.();\n }\n e.preventDefault();\n if (axis === \"x\") {\n viewport.scrollLeft = dragBaselineLeft - dx;\n } else {\n viewport.scrollTop = dragBaselineTop - dy;\n }\n };\n\n const endDrag = (e: PointerEvent) => {\n if (dragPointerId == null || e.pointerId !== dragPointerId) return;\n const wasActive = dragActive;\n dragPointerId = null;\n dragActive = false;\n if (!wasActive) return;\n try {\n viewport.releasePointerCapture(e.pointerId);\n } catch {\n // Ignore — capture may have already ended.\n }\n delete viewport.dataset.dragging;\n // Always clear inline snap-type — the CSS variant (`x mandatory` /\n // `y mandatory`) re-applies via class. Restoring `prevSnapType` would\n // leave snap disabled if drag started during a scrollToIndex's\n // snap-suspension window.\n viewport.style.scrollSnapType = \"\";\n viewport.style.scrollBehavior = prevScrollBehavior;\n suppressNextClick();\n // Snap on release: bias the anchor by the drag direction so the user\n // only has to pull ~30% of a slide width to commit to the next slide\n // (rather than the 50% threshold a strict \"nearest\" snap would impose).\n // This matches the feel of native touch carousels.\n const items = getItems();\n if (items.length > 0) {\n computeDirection();\n const axis = getAxis();\n const align = effectiveAlign($align.get(), isRtl && axis === \"x\");\n const dragDelta =\n axis === \"x\"\n ? viewport.scrollLeft - dragBaselineLeft\n : viewport.scrollTop - dragBaselineTop;\n const firstItem = items[0];\n const slideSize =\n axis === \"x\" ? firstItem.offsetWidth : firstItem.offsetHeight;\n // Shift the anchor by ~20% of a slide in the drag direction. With\n // a full-width slide that means commit at ~30% drag instead of 50%.\n const COMMIT_BIAS = 0.2;\n const bias =\n slideSize > 0 ? Math.sign(dragDelta) * slideSize * COMMIT_BIAS : 0;\n const vpExtent = mainAxisExtent(viewport.getBoundingClientRect(), axis);\n const extents = items.map((it) =>\n mainAxisExtent(it.getBoundingClientRect(), axis),\n );\n const bestIdx = nearestIndex(extents, vpExtent, align, bias);\n const reduced =\n typeof window !== \"undefined\" &&\n window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n scrollToIndex(bestIdx, { behavior: reduced ? \"auto\" : \"smooth\" });\n }\n // Resume autoplay (only if pointer is still inside root — otherwise\n // the existing pointerleave handler should keep it paused). Calling\n // resume re-checks pointer position implicitly: if the cursor is\n // outside, the next pointerleave already fired before this code, but\n // pointer-capture suppresses leave during drag, so check now.\n const pointerOutside = !root.matches(\":hover\");\n if (!pointerOutside) {\n resumeAutoplayForDrag?.();\n }\n resetAutoplay?.();\n };\n\n viewport.addEventListener(\"pointerdown\", onPointerDown);\n viewport.addEventListener(\"pointermove\", onPointerMove);\n viewport.addEventListener(\"pointerup\", endDrag);\n viewport.addEventListener(\"pointercancel\", endDrag);\n teardownFns.push(() => {\n viewport.removeEventListener(\"pointerdown\", onPointerDown);\n viewport.removeEventListener(\"pointermove\", onPointerMove);\n viewport.removeEventListener(\"pointerup\", endDrag);\n viewport.removeEventListener(\"pointercancel\", endDrag);\n delete viewport.dataset.dragging;\n });\n\n // Re-evaluate sync state when reactive config changes.\n teardownFns.push(\n $align.listen(() => {\n updateActiveFromScroll();\n }),\n );\n teardownFns.push(\n $orientation.listen(() => {\n updateActiveFromScroll();\n updateEdgeStateAndButtons();\n updateSlideVisibility();\n }),\n );\n teardownFns.push(\n $loop.listen(() => {\n updateEdgeStateAndButtons();\n }),\n );\n\n // --- Autoplay (re-wires whenever $autoplayMs changes) ---\n let autoplayTeardown: (() => void) | null = null;\n const wireAutoplay = () => {\n autoplayTeardown?.();\n autoplayTeardown = null;\n resetAutoplay = null;\n pauseAutoplayForDrag = null;\n resumeAutoplayForDrag = null;\n const ms = $autoplayMs.get();\n if (ms <= 0) return;\n if (typeof window === \"undefined\") return;\n if (window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches) return;\n\n let timer: ReturnType<typeof setInterval> | null = null;\n let paused = false;\n const tick = () => {\n if (paused) return;\n const cur = $activeIndex.get();\n const count = $slideCount.get();\n const nextIndex = cur + 1;\n scrollToIndex(\n $loop.get() ? nextIndex : nextIndex >= count ? 0 : nextIndex,\n );\n };\n const start = () => {\n if (timer != null) return;\n timer = setInterval(tick, ms);\n };\n const stop = () => {\n if (timer != null) clearInterval(timer);\n timer = null;\n };\n const pause = () => {\n paused = true;\n stop();\n };\n const resume = () => {\n paused = false;\n start();\n };\n const onVis = () => {\n if (document.visibilityState === \"hidden\") pause();\n else resume();\n };\n root.addEventListener(\"pointerenter\", pause);\n root.addEventListener(\"pointerleave\", resume);\n root.addEventListener(\"focusin\", pause);\n root.addEventListener(\"focusout\", resume);\n document.addEventListener(\"visibilitychange\", onVis);\n start();\n\n resetAutoplay = () => {\n if (paused) return;\n stop();\n start();\n };\n // Drag-specific pause: bypasses the pointerenter/leave handlers that\n // would otherwise resume autoplay if the cursor leaves the root mid-drag.\n pauseAutoplayForDrag = pause;\n resumeAutoplayForDrag = resume;\n autoplayTeardown = () => {\n stop();\n root.removeEventListener(\"pointerenter\", pause);\n root.removeEventListener(\"pointerleave\", resume);\n root.removeEventListener(\"focusin\", pause);\n root.removeEventListener(\"focusout\", resume);\n document.removeEventListener(\"visibilitychange\", onVis);\n };\n };\n wireAutoplay();\n teardownFns.push($autoplayMs.listen(() => wireAutoplay()));\n teardownFns.push(() => {\n autoplayTeardown?.();\n autoplayTeardown = null;\n });\n\n // Clear the a11y attributes we toggle, so a re-register or unmount leaves\n // the slides' DOM clean.\n teardownFns.push(() => {\n for (const item of getItems()) {\n item.removeAttribute(\"aria-hidden\");\n item.removeAttribute(\"inert\");\n }\n });\n\n // Initial sync. Defer one frame so layout for the just-mounted DOM\n // has settled before we measure scrollWidth / scrollHeight.\n const initialSync = requestAnimationFrame(() => {\n setSlideCount(getItems().length);\n updateActiveFromScroll();\n updateEdgeStateAndButtons();\n updateSlideVisibility();\n });\n teardownFns.push(() => cancelAnimationFrame(initialSync));\n\n return () => teardownAll();\n }\n\n function destroy() {\n teardownAll();\n }\n\n return {\n $activeIndex,\n $slideCount,\n $orientation,\n $align,\n $loop,\n $autoplayMs,\n $atScrollStart,\n $atScrollEnd,\n setActiveIndex,\n setSlideCount,\n setOrientation,\n setAlign,\n setLoop,\n setAutoplay,\n next,\n prev,\n registerViewport,\n scrollToIndex,\n userGoTo,\n destroy,\n };\n}\n"],"mappings":"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"}
@@ -0,0 +1,64 @@
1
+ import { CarouselAlign } from "./types.js";
2
+
3
+ //#region src/snap-math.d.ts
4
+ /**
5
+ * Pure, DOM-free geometry helpers for the carousel engine. Extracted from
6
+ * `create-carousel` so the alignment / wrap / edge math is unit-testable without a
7
+ * layout engine — happy-dom (and any non-browser test env) reports `0` for every
8
+ * `getBoundingClientRect` / `scrollWidth`, which would otherwise strand all of
9
+ * this logic in e2e-only territory. The DOM functions in `create-carousel` read
10
+ * rects, then delegate the actual math here.
11
+ */
12
+ /** A 1-D extent on the main scroll axis (left/right or top/bottom, in px). */
13
+ interface Extent {
14
+ start: number;
15
+ end: number;
16
+ }
17
+ /**
18
+ * The anchor point of an extent for a given snap alignment:
19
+ * - `start` → the leading edge,
20
+ * - `center` → the midpoint,
21
+ * - `end` → the trailing edge.
22
+ *
23
+ * For RTL horizontal carousels the caller passes an `effectiveAlign` with
24
+ * `start`/`end` swapped, so this stays direction-agnostic.
25
+ */
26
+ declare function anchorFor(start: number, end: number, align: CarouselAlign): number;
27
+ /**
28
+ * Index of the item whose anchor is closest to the viewport's anchor, optionally
29
+ * biased by `bias` px in the drag direction. The bias is what lets a drag-release
30
+ * commit to the next slide at ~30% drag instead of the 50% a strict nearest-snap
31
+ * would impose. Ties resolve to the lower index (first wins). Returns 0 for an
32
+ * empty list.
33
+ */
34
+ declare function nearestIndex(items: ReadonlyArray<Extent>, vp: Extent, align: CarouselAlign, bias?: number): number;
35
+ /**
36
+ * Clamp / wrap an index into `[0, count)`. Non-loop clamps to the ends; loop
37
+ * wraps (handling negatives via `((i % count) + count) % count`). Returns 0 when
38
+ * `count <= 0`. This unifies the two near-identical clamp formulas that
39
+ * `create-carousel` previously kept in `clamp()` and `scrollToIndex()`.
40
+ */
41
+ declare function clampIndex(i: number, count: number, loop: boolean): number;
42
+ /**
43
+ * Derive the at-start / at-end edge state from the current scroll offset and the
44
+ * maximum scrollable offset (both in px on the main axis; the caller passes
45
+ * `Math.abs(scrollLeft)` for RTL so this stays direction-agnostic). Tolerates
46
+ * ~1px slop. When there's no overflow (`max <= 0`) it reports at-start but NOT
47
+ * at-end, so a next button stays clickable while async layout settles — the
48
+ * first real scroll event corrects it.
49
+ */
50
+ declare function edgeState(current: number, max: number): {
51
+ atStart: boolean;
52
+ atEnd: boolean;
53
+ };
54
+ /**
55
+ * Swap `start` ↔ `end` alignment for RTL horizontal carousels. In RTL the visual
56
+ * "start" of the track is the viewport's right edge, so an item aligned to
57
+ * `start` should anchor on its right edge — achieved by feeding `anchorFor` the
58
+ * swapped value. `center` is unaffected; vertical/LTR callers pass `rtl=false`
59
+ * and get the alignment back unchanged (zero behavior change).
60
+ */
61
+ declare function effectiveAlign(align: CarouselAlign, rtl: boolean): CarouselAlign;
62
+ //#endregion
63
+ export { Extent, anchorFor, clampIndex, edgeState, effectiveAlign, nearestIndex };
64
+ //# sourceMappingURL=snap-math.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"snap-math.d.ts","names":[],"sources":["../../src/snap-math.ts"],"mappings":";;;;;AAYA;;;;AAEK;AAYL;;UAdiB,MAAA;EACf,KAAA;EACA,GAAG;AAAA;;;;AAeiB;AActB;;;;;iBAjBgB,SAAA,CACd,KAAA,UACA,GAAA,UACA,KAAA,EAAO,aAAa;;;;;;;;iBAcN,YAAA,CACd,KAAA,EAAO,aAAA,CAAc,MAAA,GACrB,EAAA,EAAI,MAAA,EACJ,KAAA,EAAO,aAAA,EACP,IAAA;;;;;AAAQ;AAsBV;iBAAgB,UAAA,CAAW,CAAA,UAAW,KAAA,UAAe,IAAA;;;;;;;AAAa;AAgBlE;iBAAgB,SAAA,CACd,OAAA,UACA,GAAA;EACG,OAAA;EAAkB,KAAA;AAAA;;;;;AAAK;AAc5B;;iBAAgB,cAAA,CACd,KAAA,EAAO,aAAA,EACP,GAAA,YACC,aAAa"}