@fluixi-ui/carousel 0.1.0-alpha.1 → 0.1.0-alpha.10

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/carousel.js CHANGED
@@ -1,388 +1,470 @@
1
- import { delegateEvents, splitProps, createComponent, createNativeElement, spread, mergeProps, insert, handleRef } from "@fluixi/dom";
2
- import { createEffect, createSignal, onCleanup, createMemo } from "@fluixi/reactive/signal";
1
+ import { createComponent, createNativeElement, delegateEvents, handleRef, holeContent, holeEnd, holeScope, insert, mergeProps, splitProps, spread, templateNode } from "@fluixi/dom";
2
+ import { createEffect, createMemo, createSignal, onCleanup } from "@fluixi/reactive/signal";
3
+ import { fxProps } from "@fluixi-ui/fx";
3
4
  import { For } from "@fluixi/core";
4
- import { createSafeContext, mergeDefaultProps, createId, createControllableSignal, composeId, composeEventHandlers, dataAttr } from "@fluixi-ui/kit";
5
+ import { composeEventHandlers, composeId, createControllableSignal, createId, createSafeContext, dataAttr, mergeDefaultProps } from "@fluixi-ui/kit";
5
6
  import { createCollection } from "@fluixi-ui/primitives";
6
- delegateEvents(["keydown", "click"]);
7
- const [CarouselProvider, useCarouselContext] = createSafeContext("Carousel");
7
+ //#region src/carousel.tsx
8
+ var _fxTmpl$0 = "<div data-carousel-status=\"\" aria-live=\"polite\" aria-atomic=\"true\" style=\"position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0\"><!--fx--><!--fx/--></div>";
9
+ delegateEvents(["click", "keydown"]);
10
+ var [CarouselProvider, useCarouselContext] = createSafeContext("Carousel");
11
+ /**
12
+ * Root of a carousel. Owns the active slide index (controlled `index` or
13
+ * uncontrolled `defaultIndex`), exposes navigation to its parts, and drives
14
+ * optional autoplay. Arrow keys move between slides when the carousel is focused.
15
+ *
16
+ * @example
17
+ * ```tsx
18
+ * import {
19
+ * Carousel, CarouselViewport, CarouselSlide,
20
+ * CarouselPrevious, CarouselNext, CarouselDots,
21
+ * } from '@fluixi-ui/carousel';
22
+ *
23
+ * <Carousel loop autoplay>
24
+ * <CarouselViewport>
25
+ * <CarouselSlide><img src="/1.jpg" alt="" /></CarouselSlide>
26
+ * <CarouselSlide><img src="/2.jpg" alt="" /></CarouselSlide>
27
+ * </CarouselViewport>
28
+ * <CarouselPrevious /><CarouselNext />
29
+ * <CarouselDots />
30
+ * </Carousel>
31
+ * ```
32
+ */
8
33
  function Carousel(props) {
9
- const merged = mergeDefaultProps({
10
- orientation: "horizontal",
11
- loop: false,
12
- pauseOnHover: true,
13
- draggable: true
14
- }, props);
15
- const [local, others] = splitProps(merged, ["index", "defaultIndex", "onIndexChange", "orientation", "loop", "autoplay", "autoplayInterval", "pauseOnHover", "draggable", "ref", "children", "onKeyDown"]);
16
- const baseId = createId("carousel");
17
- const collection = createCollection();
18
- const count = () => collection.items().length;
19
- const loop = () => local.loop ?? false;
20
- const orientation = () => local.orientation ?? "horizontal";
21
- const [rawIndex, setIndex] = createControllableSignal({
22
- value: () => local.index,
23
- defaultValue: () => local.defaultIndex ?? 0,
24
- onChange: (next2) => local.onIndexChange?.(next2)
25
- });
26
- const index = () => rawIndex() ?? 0;
27
- const clamp = (i) => {
28
- const c = count();
29
- if (c <= 0) return 0;
30
- return loop() ? (i % c + c) % c : Math.min(Math.max(i, 0), c - 1);
31
- };
32
- const goTo = (i) => setIndex(clamp(i));
33
- const next = () => goTo(index() + 1);
34
- const previous = () => goTo(index() - 1);
35
- const canPrevious = () => loop() || index() > 0;
36
- const canNext = () => loop() || index() < count() - 1;
37
- createEffect(() => {
38
- const c = count();
39
- if (c > 0 && index() > c - 1) setIndex(c - 1);
40
- });
41
- const [paused, setPaused] = createSignal(false);
42
- const [dragging, setDragging] = createSignal(false);
43
- const [reducedMotion, setReducedMotion] = createSignal(false);
44
- const mq = typeof window !== "undefined" && typeof window.matchMedia === "function" ? window.matchMedia("(prefers-reduced-motion: reduce)") : null;
45
- if (mq) {
46
- setReducedMotion(mq.matches);
47
- const onChange = () => setReducedMotion(mq.matches);
48
- mq.addEventListener?.("change", onChange);
49
- onCleanup(() => mq.removeEventListener?.("change", onChange));
50
- }
51
- const autoplayMs = () => {
52
- const a = local.autoplay;
53
- if (a === true) return local.autoplayInterval ?? 4e3;
54
- if (typeof a === "number") return a;
55
- return 0;
56
- };
57
- createEffect(() => {
58
- const ms = autoplayMs();
59
- if (!ms || paused() || dragging() || reducedMotion() || count() <= 1) return;
60
- const id = setInterval(() => next(), ms);
61
- onCleanup(() => clearInterval(id));
62
- });
63
- const onRootKeyDown = (event) => {
64
- const horizontal = orientation() === "horizontal";
65
- switch (event.key) {
66
- case (horizontal ? "ArrowLeft" : "ArrowUp"):
67
- event.preventDefault();
68
- previous();
69
- break;
70
- case (horizontal ? "ArrowRight" : "ArrowDown"):
71
- event.preventDefault();
72
- next();
73
- break;
74
- case "Home":
75
- event.preventDefault();
76
- goTo(0);
77
- break;
78
- case "End":
79
- event.preventDefault();
80
- goTo(count() - 1);
81
- break;
82
- }
83
- };
84
- const setRootRef = (element) => {
85
- const pause = () => local.pauseOnHover && setPaused(true);
86
- const resume = () => local.pauseOnHover && setPaused(false);
87
- element.addEventListener("pointerenter", pause);
88
- element.addEventListener("pointerleave", resume);
89
- element.addEventListener("focusin", pause);
90
- element.addEventListener("focusout", resume);
91
- onCleanup(() => {
92
- element.removeEventListener("pointerenter", pause);
93
- element.removeEventListener("pointerleave", resume);
94
- element.removeEventListener("focusin", pause);
95
- element.removeEventListener("focusout", resume);
96
- });
97
- handleRef(local.ref, element);
98
- };
99
- const context = {
100
- index,
101
- count,
102
- orientation,
103
- loop,
104
- goTo,
105
- next,
106
- previous,
107
- canPrevious,
108
- canNext,
109
- collection,
110
- draggable: () => local.draggable ?? true,
111
- isDragging: dragging,
112
- setDragging,
113
- ids: {
114
- root: composeId(baseId, "root")
115
- }
116
- };
117
- return createMemo(() => createComponent(CarouselProvider, {
118
- get value() {
119
- return context;
120
- },
121
- get children() {
122
- return (() => {
123
- const _el$ = createNativeElement("div");
124
- spread({
125
- element: _el$,
126
- props: mergeProps(others, {
127
- ref: setRootRef,
128
- id: () => context.ids.root,
129
- "data-carousel": "",
130
- role: "region",
131
- "aria-roledescription": "carousel",
132
- "data-orientation": () => orientation(),
133
- tabindex: 0,
134
- onKeyDown: composeEventHandlers([local.onKeyDown, onRootKeyDown])
135
- })
136
- });
137
- insert(_el$, local.children);
138
- insert(_el$, (() => {
139
- const _el$2 = createNativeElement("div");
140
- spread({
141
- element: _el$2,
142
- props: {
143
- "data-carousel-status": "",
144
- "aria-live": "polite",
145
- "aria-atomic": "true",
146
- style: "position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0"
147
- }
148
- });
149
- insert(_el$2, () => `Slide ${index() + 1} of ${count()}`, null);
150
- return _el$2;
151
- })());
152
- return _el$;
153
- })();
154
- }
155
- }));
34
+ const merged = mergeDefaultProps({
35
+ orientation: "horizontal",
36
+ loop: false,
37
+ pauseOnHover: true,
38
+ draggable: true
39
+ }, props);
40
+ const [local, others] = splitProps(merged, [
41
+ "index",
42
+ "defaultIndex",
43
+ "onIndexChange",
44
+ "orientation",
45
+ "loop",
46
+ "autoplay",
47
+ "autoplayInterval",
48
+ "pauseOnHover",
49
+ "draggable",
50
+ "overlay",
51
+ "ref",
52
+ "children",
53
+ "onKeyDown",
54
+ "fx",
55
+ "style"
56
+ ]);
57
+ const baseId = createId("carousel");
58
+ const collection = createCollection();
59
+ const count = () => collection.items().length;
60
+ const loop = () => local.loop ?? false;
61
+ const orientation = () => local.orientation ?? "horizontal";
62
+ const [rawIndex, setIndex] = createControllableSignal({
63
+ value: () => local.index,
64
+ defaultValue: () => local.defaultIndex ?? 0,
65
+ onChange: (next) => local.onIndexChange?.(next)
66
+ });
67
+ const index = () => rawIndex() ?? 0;
68
+ const clamp = (i) => {
69
+ const c = count();
70
+ if (c <= 0) return 0;
71
+ return loop() ? (i % c + c) % c : Math.min(Math.max(i, 0), c - 1);
72
+ };
73
+ const goTo = (i) => setIndex(clamp(i));
74
+ const next = () => goTo(index() + 1);
75
+ const previous = () => goTo(index() - 1);
76
+ const canPrevious = () => loop() || index() > 0;
77
+ const canNext = () => loop() || index() < count() - 1;
78
+ createEffect(() => {
79
+ const c = count();
80
+ if (c > 0 && index() > c - 1) setIndex(c - 1);
81
+ });
82
+ const [paused, setPaused] = createSignal(false);
83
+ const [dragging, setDragging] = createSignal(false);
84
+ const [reducedMotion, setReducedMotion] = createSignal(false);
85
+ const mq = typeof window !== "undefined" && typeof window.matchMedia === "function" ? window.matchMedia("(prefers-reduced-motion: reduce)") : null;
86
+ if (mq) {
87
+ setReducedMotion(mq.matches);
88
+ const onChange = () => setReducedMotion(mq.matches);
89
+ mq.addEventListener?.("change", onChange);
90
+ onCleanup(() => mq.removeEventListener?.("change", onChange));
91
+ }
92
+ const autoplayMs = () => {
93
+ const a = local.autoplay;
94
+ if (a === true) return local.autoplayInterval ?? 4e3;
95
+ if (typeof a === "number") return a;
96
+ return 0;
97
+ };
98
+ createEffect(() => {
99
+ const ms = autoplayMs();
100
+ if (!ms || paused() || dragging() || reducedMotion() || count() <= 1) return;
101
+ const id = setInterval(() => next(), ms);
102
+ onCleanup(() => clearInterval(id));
103
+ });
104
+ const onRootKeyDown = (event) => {
105
+ const horizontal = orientation() === "horizontal";
106
+ switch (event.key) {
107
+ case horizontal ? "ArrowLeft" : "ArrowUp":
108
+ event.preventDefault();
109
+ previous();
110
+ break;
111
+ case horizontal ? "ArrowRight" : "ArrowDown":
112
+ event.preventDefault();
113
+ next();
114
+ break;
115
+ case "Home":
116
+ event.preventDefault();
117
+ goTo(0);
118
+ break;
119
+ case "End":
120
+ event.preventDefault();
121
+ goTo(count() - 1);
122
+ }
123
+ };
124
+ const setRootRef = (element) => {
125
+ const pause = () => local.pauseOnHover && setPaused(true);
126
+ const resume = () => local.pauseOnHover && setPaused(false);
127
+ element.addEventListener("pointerenter", pause);
128
+ element.addEventListener("pointerleave", resume);
129
+ element.addEventListener("focusin", pause);
130
+ element.addEventListener("focusout", resume);
131
+ onCleanup(() => {
132
+ element.removeEventListener("pointerenter", pause);
133
+ element.removeEventListener("pointerleave", resume);
134
+ element.removeEventListener("focusin", pause);
135
+ element.removeEventListener("focusout", resume);
136
+ });
137
+ handleRef(local.ref, element);
138
+ };
139
+ const context = {
140
+ index,
141
+ count,
142
+ orientation,
143
+ loop,
144
+ goTo,
145
+ next,
146
+ previous,
147
+ canPrevious,
148
+ canNext,
149
+ collection,
150
+ draggable: () => local.draggable ?? true,
151
+ isDragging: dragging,
152
+ setDragging,
153
+ ids: { root: composeId(baseId, "root") }
154
+ };
155
+ return createMemo(() => createComponent(CarouselProvider, {
156
+ get value() {
157
+ return context;
158
+ },
159
+ get children() {
160
+ return (() => {
161
+ const _el$ = createNativeElement("div");
162
+ spread({
163
+ element: _el$,
164
+ props: mergeProps(fxProps(() => local.fx, () => local.style), others, {
165
+ ref: setRootRef,
166
+ id: () => context.ids.root,
167
+ "data-carousel": "",
168
+ "data-overlay": () => local.overlay ? "" : void 0,
169
+ role: "region",
170
+ "aria-roledescription": "carousel",
171
+ "data-orientation": () => orientation(),
172
+ tabindex: 0,
173
+ onKeyDown: composeEventHandlers([local.onKeyDown, onRootKeyDown])
174
+ })
175
+ });
176
+ insert(_el$, local.children, null);
177
+ insert(_el$, (() => {
178
+ const _el$ = templateNode(_fxTmpl$0, "div", true);
179
+ const _n$0 = _el$.firstChild;
180
+ const _n$1 = holeEnd(_n$0);
181
+ insert(_el$, holeScope(_n$0, () => () => `Slide ${index() + 1} of ${count()}`), _n$1, holeContent(_n$0, _n$1));
182
+ return _el$;
183
+ })());
184
+ return _el$;
185
+ })();
186
+ }
187
+ }));
156
188
  }
189
+ /** Clips the slides and translates the track to the active slide. */
157
190
  function CarouselViewport(props) {
158
- const ctx = useCarouselContext();
159
- const [local, others] = splitProps(props, ["ref", "children", "style"]);
160
- const [dragDelta, setDragDelta] = createSignal(0);
161
- const trackTransform = () => {
162
- const base = ctx.index() * -100;
163
- const delta = dragDelta();
164
- const axis = ctx.orientation() === "vertical" ? "Y" : "X";
165
- return delta === 0 ? `transform:translate${axis}(${base}%)` : `transform:translate${axis}(calc(${base}% + ${delta}px))`;
166
- };
167
- const setRef = (element) => {
168
- const vertical = () => ctx.orientation() === "vertical";
169
- let active = false;
170
- let pointerId = -1;
171
- let start = 0;
172
- let size = 1;
173
- let moved = false;
174
- const onDown = (event) => {
175
- if (!ctx.draggable() || event.button !== 0) return;
176
- active = true;
177
- moved = false;
178
- pointerId = event.pointerId;
179
- start = vertical() ? event.clientY : event.clientX;
180
- size = (vertical() ? element.clientHeight : element.clientWidth) || 1;
181
- element.setPointerCapture?.(event.pointerId);
182
- ctx.setDragging(true);
183
- };
184
- const onMove = (event) => {
185
- if (!active || event.pointerId !== pointerId) return;
186
- const delta = (vertical() ? event.clientY : event.clientX) - start;
187
- if (Math.abs(delta) > 3) moved = true;
188
- setDragDelta(delta);
189
- event.preventDefault();
190
- };
191
- const end = (event) => {
192
- if (!active || event.pointerId !== pointerId) return;
193
- active = false;
194
- const delta = dragDelta();
195
- setDragDelta(0);
196
- ctx.setDragging(false);
197
- element.releasePointerCapture?.(event.pointerId);
198
- const threshold = Math.max(size * 0.2, 40);
199
- if (delta <= -threshold) ctx.next();
200
- else if (delta >= threshold) ctx.previous();
201
- };
202
- const onClick = (event) => {
203
- if (moved) {
204
- event.preventDefault();
205
- event.stopPropagation();
206
- moved = false;
207
- }
208
- };
209
- element.addEventListener("pointerdown", onDown);
210
- element.addEventListener("pointermove", onMove);
211
- element.addEventListener("pointerup", end);
212
- element.addEventListener("pointercancel", end);
213
- element.addEventListener("click", onClick, true);
214
- onCleanup(() => {
215
- element.removeEventListener("pointerdown", onDown);
216
- element.removeEventListener("pointermove", onMove);
217
- element.removeEventListener("pointerup", end);
218
- element.removeEventListener("pointercancel", end);
219
- element.removeEventListener("click", onClick, true);
220
- });
221
- handleRef(local.ref, element);
222
- };
223
- return (() => {
224
- const _el$ = createNativeElement("div");
225
- spread({
226
- element: _el$,
227
- props: mergeProps(others, {
228
- ref: setRef,
229
- "data-carousel-viewport": "",
230
- "data-orientation": () => ctx.orientation(),
231
- style: local.style
232
- })
233
- });
234
- insert(_el$, (() => {
235
- const _el$2 = createNativeElement("div");
236
- spread({
237
- element: _el$2,
238
- props: {
239
- "data-carousel-track": "",
240
- "data-orientation": () => ctx.orientation(),
241
- "data-dragging": () => dataAttr(ctx.isDragging()),
242
- style: () => trackTransform()
243
- }
244
- });
245
- insert(_el$2, local.children);
246
- return _el$2;
247
- })());
248
- return _el$;
249
- })();
191
+ const ctx = useCarouselContext();
192
+ const [local, others] = splitProps(props, [
193
+ "ref",
194
+ "children",
195
+ "style",
196
+ "fx",
197
+ "style"
198
+ ]);
199
+ const [dragDelta, setDragDelta] = createSignal(0);
200
+ const trackTransform = () => {
201
+ const base = ctx.index() * -100;
202
+ const delta = dragDelta();
203
+ const axis = ctx.orientation() === "vertical" ? "Y" : "X";
204
+ return delta === 0 ? `transform:translate${axis}(${base}%)` : `transform:translate${axis}(calc(${base}% + ${delta}px))`;
205
+ };
206
+ const setRef = (element) => {
207
+ const vertical = () => ctx.orientation() === "vertical";
208
+ let active = false;
209
+ let pointerId = -1;
210
+ let start = 0;
211
+ let size = 1;
212
+ let moved = false;
213
+ const onDown = (event) => {
214
+ if (!ctx.draggable() || event.button !== 0) return;
215
+ active = true;
216
+ moved = false;
217
+ pointerId = event.pointerId;
218
+ start = vertical() ? event.clientY : event.clientX;
219
+ size = (vertical() ? element.clientHeight : element.clientWidth) || 1;
220
+ element.setPointerCapture?.(event.pointerId);
221
+ ctx.setDragging(true);
222
+ };
223
+ const onMove = (event) => {
224
+ if (!active || event.pointerId !== pointerId) return;
225
+ const delta = (vertical() ? event.clientY : event.clientX) - start;
226
+ if (Math.abs(delta) > 3) moved = true;
227
+ setDragDelta(delta);
228
+ event.preventDefault();
229
+ };
230
+ const end = (event) => {
231
+ if (!active || event.pointerId !== pointerId) return;
232
+ active = false;
233
+ const delta = dragDelta();
234
+ setDragDelta(0);
235
+ ctx.setDragging(false);
236
+ element.releasePointerCapture?.(event.pointerId);
237
+ const threshold = Math.max(size * .2, 40);
238
+ if (delta <= -threshold) ctx.next();
239
+ else if (delta >= threshold) ctx.previous();
240
+ };
241
+ const onClick = (event) => {
242
+ if (moved) {
243
+ event.preventDefault();
244
+ event.stopPropagation();
245
+ moved = false;
246
+ }
247
+ };
248
+ element.addEventListener("pointerdown", onDown);
249
+ element.addEventListener("pointermove", onMove);
250
+ element.addEventListener("pointerup", end);
251
+ element.addEventListener("pointercancel", end);
252
+ element.addEventListener("click", onClick, true);
253
+ onCleanup(() => {
254
+ element.removeEventListener("pointerdown", onDown);
255
+ element.removeEventListener("pointermove", onMove);
256
+ element.removeEventListener("pointerup", end);
257
+ element.removeEventListener("pointercancel", end);
258
+ element.removeEventListener("click", onClick, true);
259
+ });
260
+ handleRef(local.ref, element);
261
+ };
262
+ return (() => {
263
+ const _el$ = createNativeElement("div");
264
+ spread({
265
+ element: _el$,
266
+ props: mergeProps(fxProps(() => local.fx, () => local.style), others, {
267
+ ref: setRef,
268
+ "data-carousel-viewport": "",
269
+ "data-orientation": () => ctx.orientation()
270
+ })
271
+ });
272
+ insert(_el$, (() => {
273
+ const _el$ = createNativeElement("div");
274
+ spread({
275
+ element: _el$,
276
+ props: {
277
+ "data-carousel-track": "",
278
+ "data-orientation": () => ctx.orientation(),
279
+ "data-dragging": () => dataAttr(ctx.isDragging()),
280
+ style: () => trackTransform()
281
+ }
282
+ });
283
+ insert(_el$, local.children, null);
284
+ return _el$;
285
+ })());
286
+ return _el$;
287
+ })();
250
288
  }
289
+ /** A single slide; registers with the carousel so counts and dots stay in sync. */
251
290
  function CarouselSlide(props) {
252
- const ctx = useCarouselContext();
253
- const [local, others] = splitProps(props, ["ref", "children"]);
254
- const [node, setNode] = createSignal(void 0);
255
- const [idx, setIdx] = createSignal(-1);
256
- const setRef = (element) => {
257
- setNode(element);
258
- const remove = ctx.collection.register({
259
- ref: element
260
- });
261
- onCleanup(remove);
262
- handleRef(local.ref, element);
263
- };
264
- createEffect(() => {
265
- ctx.count();
266
- const element = node();
267
- const parent = element?.parentElement;
268
- if (!element || !parent) {
269
- setIdx(-1);
270
- return;
271
- }
272
- setIdx([...parent.querySelectorAll(":scope > [data-carousel-slide]")].indexOf(element));
273
- });
274
- const isActive = () => idx() === ctx.index();
275
- return (() => {
276
- const _el$ = createNativeElement("div");
277
- spread({
278
- element: _el$,
279
- props: mergeProps(others, {
280
- ref: setRef,
281
- "data-carousel-slide": "",
282
- role: "group",
283
- "aria-roledescription": "slide",
284
- "aria-label": () => {
285
- const i = idx();
286
- return i >= 0 ? `${i + 1} of ${ctx.count()}` : void 0;
287
- },
288
- "aria-hidden": () => isActive() ? void 0 : "true",
289
- "data-active": () => dataAttr(isActive())
290
- })
291
- });
292
- insert(_el$, local.children);
293
- return _el$;
294
- })();
291
+ const ctx = useCarouselContext();
292
+ const [local, others] = splitProps(props, [
293
+ "ref",
294
+ "children",
295
+ "fx",
296
+ "style"
297
+ ]);
298
+ const [node, setNode] = createSignal(void 0);
299
+ const [idx, setIdx] = createSignal(-1);
300
+ const setRef = (element) => {
301
+ setNode(element);
302
+ const remove = ctx.collection.register({ ref: element });
303
+ onCleanup(remove);
304
+ handleRef(local.ref, element);
305
+ };
306
+ createEffect(() => {
307
+ ctx.count();
308
+ const element = node();
309
+ const parent = element?.parentElement;
310
+ if (!element || !parent) {
311
+ setIdx(-1);
312
+ return;
313
+ }
314
+ setIdx([...parent.querySelectorAll(":scope > [data-carousel-slide]")].indexOf(element));
315
+ });
316
+ const isActive = () => idx() === ctx.index();
317
+ return (() => {
318
+ const _el$ = createNativeElement("div");
319
+ spread({
320
+ element: _el$,
321
+ props: mergeProps(fxProps(() => local.fx, () => local.style), others, {
322
+ ref: setRef,
323
+ "data-carousel-slide": "",
324
+ role: "group",
325
+ "aria-roledescription": "slide",
326
+ "aria-label": () => {
327
+ const i = idx();
328
+ return i >= 0 ? `${i + 1} of ${ctx.count()}` : void 0;
329
+ },
330
+ "aria-hidden": () => isActive() ? void 0 : "true",
331
+ "data-active": () => dataAttr(isActive())
332
+ })
333
+ });
334
+ insert(_el$, local.children, null);
335
+ return _el$;
336
+ })();
295
337
  }
338
+ /**
339
+ * Lays out the carousel's controls: previous, dots, next.
340
+ *
341
+ * Without it every consumer hand-rolls a flex row, and they tend to end up flush
342
+ * against the carousel's edges. In `overlay` mode this becomes a transparent
343
+ * layer over the slides — it stops reserving height, and only the controls
344
+ * themselves take pointer events, so a drag still starts on the slide beneath.
345
+ *
346
+ * @example
347
+ * ```tsx
348
+ * <Carousel overlay>
349
+ * <CarouselViewport>…</CarouselViewport>
350
+ * <CarouselControls>
351
+ * <CarouselPrevious />
352
+ * <CarouselDots />
353
+ * <CarouselNext />
354
+ * </CarouselControls>
355
+ * </Carousel>
356
+ * ```
357
+ */
358
+ function CarouselControls(props) {
359
+ const [local, others] = splitProps(props, [
360
+ "children",
361
+ "fx",
362
+ "style"
363
+ ]);
364
+ return (() => {
365
+ const _el$ = createNativeElement("div");
366
+ spread({
367
+ element: _el$,
368
+ props: mergeProps(fxProps(() => local.fx, () => local.style), others, { "data-carousel-controls": "" })
369
+ });
370
+ insert(_el$, local.children, null);
371
+ return _el$;
372
+ })();
373
+ }
374
+ /** Moves to the previous slide; disabled at the start unless the carousel loops. */
296
375
  function CarouselPrevious(props) {
297
- const ctx = useCarouselContext();
298
- const merged = mergeDefaultProps({
299
- type: "button",
300
- "aria-label": "Previous slide"
301
- }, props);
302
- const [local, others] = splitProps(merged, ["onClick", "children"]);
303
- return (() => {
304
- const _el$ = createNativeElement("button");
305
- spread({
306
- element: _el$,
307
- props: mergeProps(others, {
308
- "data-carousel-prev": "",
309
- "aria-controls": () => ctx.ids.root,
310
- disabled: () => !ctx.canPrevious(),
311
- onClick: composeEventHandlers([local.onClick, () => ctx.previous()])
312
- })
313
- });
314
- insert(_el$, () => local.children ?? "‹", null);
315
- return _el$;
316
- })();
376
+ const ctx = useCarouselContext();
377
+ const merged = mergeDefaultProps({
378
+ type: "button",
379
+ "aria-label": "Previous slide"
380
+ }, props);
381
+ const [local, others] = splitProps(merged, [
382
+ "onClick",
383
+ "children",
384
+ "fx",
385
+ "style"
386
+ ]);
387
+ return (() => {
388
+ const _el$ = createNativeElement("button");
389
+ spread({
390
+ element: _el$,
391
+ props: mergeProps(fxProps(() => local.fx, () => local.style), others, {
392
+ "data-carousel-prev": "",
393
+ "aria-controls": () => ctx.ids.root,
394
+ disabled: () => !ctx.canPrevious(),
395
+ onClick: composeEventHandlers([local.onClick, () => ctx.previous()])
396
+ })
397
+ });
398
+ insert(_el$, local.children ?? "‹", null);
399
+ return _el$;
400
+ })();
317
401
  }
402
+ /** Moves to the next slide; disabled at the end unless the carousel loops. */
318
403
  function CarouselNext(props) {
319
- const ctx = useCarouselContext();
320
- const merged = mergeDefaultProps({
321
- type: "button",
322
- "aria-label": "Next slide"
323
- }, props);
324
- const [local, others] = splitProps(merged, ["onClick", "children"]);
325
- return (() => {
326
- const _el$ = createNativeElement("button");
327
- spread({
328
- element: _el$,
329
- props: mergeProps(others, {
330
- "data-carousel-next": "",
331
- "aria-controls": () => ctx.ids.root,
332
- disabled: () => !ctx.canNext(),
333
- onClick: composeEventHandlers([local.onClick, () => ctx.next()])
334
- })
335
- });
336
- insert(_el$, () => local.children ?? "›", null);
337
- return _el$;
338
- })();
404
+ const ctx = useCarouselContext();
405
+ const merged = mergeDefaultProps({
406
+ type: "button",
407
+ "aria-label": "Next slide"
408
+ }, props);
409
+ const [local, others] = splitProps(merged, [
410
+ "onClick",
411
+ "children",
412
+ "fx",
413
+ "style"
414
+ ]);
415
+ return (() => {
416
+ const _el$ = createNativeElement("button");
417
+ spread({
418
+ element: _el$,
419
+ props: mergeProps(fxProps(() => local.fx, () => local.style), others, {
420
+ "data-carousel-next": "",
421
+ "aria-controls": () => ctx.ids.root,
422
+ disabled: () => !ctx.canNext(),
423
+ onClick: composeEventHandlers([local.onClick, () => ctx.next()])
424
+ })
425
+ });
426
+ insert(_el$, local.children ?? "›", null);
427
+ return _el$;
428
+ })();
339
429
  }
430
+ /** One clickable dot per slide, reflecting and setting the active index. */
340
431
  function CarouselDots(props) {
341
- const ctx = useCarouselContext();
342
- const dots = () => Array.from({
343
- length: ctx.count()
344
- }, (_, i) => i);
345
- return (() => {
346
- const _el$ = createNativeElement("div");
347
- spread({
348
- element: _el$,
349
- props: mergeProps(props, {
350
- "data-carousel-dots": "",
351
- role: "group",
352
- "aria-label": "Choose slide"
353
- })
354
- });
355
- insert(_el$, createMemo(() => createComponent(For, {
356
- get each() {
357
- return dots;
358
- },
359
- get children() {
360
- return (i) => (() => {
361
- const _el$2 = createNativeElement("button");
362
- spread({
363
- element: _el$2,
364
- props: {
365
- type: "button",
366
- "data-carousel-dot": "",
367
- "data-active": () => dataAttr(i === ctx.index()),
368
- "aria-label": () => `Go to slide ${i + 1}`,
369
- "aria-current": () => i === ctx.index() ? "true" : void 0,
370
- onClick: () => ctx.goTo(i)
371
- }
372
- });
373
- return _el$2;
374
- })();
375
- }
376
- })), null);
377
- return _el$;
378
- })();
432
+ const ctx = useCarouselContext();
433
+ const dots = () => Array.from({ length: ctx.count() }, (_, i) => i);
434
+ return (() => {
435
+ const _el$ = createNativeElement("div");
436
+ spread({
437
+ element: _el$,
438
+ props: mergeProps(props, {
439
+ "data-carousel-dots": "",
440
+ role: "group",
441
+ "aria-label": "Choose slide"
442
+ })
443
+ });
444
+ insert(_el$, createMemo(() => createComponent(For, {
445
+ get each() {
446
+ return dots;
447
+ },
448
+ get children() {
449
+ return (i) => (() => {
450
+ const _el$ = createNativeElement("button");
451
+ spread({
452
+ element: _el$,
453
+ props: {
454
+ type: "button",
455
+ "data-carousel-dot": "",
456
+ "data-active": () => dataAttr(i === ctx.index()),
457
+ "aria-label": `Go to slide ${i + 1}`,
458
+ "aria-current": () => i === ctx.index() ? "true" : void 0,
459
+ onClick: () => ctx.goTo(i)
460
+ }
461
+ });
462
+ return _el$;
463
+ })();
464
+ }
465
+ })), null);
466
+ return _el$;
467
+ })();
379
468
  }
380
- export {
381
- Carousel,
382
- CarouselDots,
383
- CarouselNext,
384
- CarouselPrevious,
385
- CarouselSlide,
386
- CarouselViewport,
387
- useCarouselContext
388
- };
469
+ //#endregion
470
+ export { Carousel, CarouselControls, CarouselDots, CarouselNext, CarouselPrevious, CarouselSlide, CarouselViewport, useCarouselContext };