@ailuracode/alpine-carousel 1.0.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -5,12 +5,12 @@ Headless accessible carousel store for Alpine.js, powered by [Embla Carousel](ht
5
5
  ## Install
6
6
 
7
7
  ```bash
8
- pnpm add @ailuracode/alpine-carousel alpinejs
8
+ pnpm add @ailuracode/alpine-carousel @ailuracode/alpine-core alpinejs
9
9
  ```
10
10
 
11
11
  Peer dependencies: `embla-carousel` and `embla-carousel-autoplay` are bundled as direct dependencies.
12
12
 
13
- ## Setup
13
+ ## Quick start
14
14
 
15
15
  ```ts
16
16
  import Alpine from "alpinejs";
@@ -59,7 +59,6 @@ Embla expects the **viewport** element (overflow hidden) with a **container** ch
59
59
  | `current(id)` / `count(id)` | Current index and total slides |
60
60
  | `canNext(id)` / `canPrevious(id)` | Scroll availability |
61
61
  | `play(id)` / `pause(id)` / `isPlaying(id)` | Autoplay controls |
62
- | `instance(id)` | Raw Embla API (advanced) |
63
62
  | `handleKeydown(id, event)` | Arrow keys, Home, End |
64
63
  | `carouselProps(id, options?)` | ARIA region props |
65
64
  | `viewportProps(id, options?)` | Focusable viewport props; sets `--slide-size` (default `100%`, pass `{ slideSize: false }` to use CSS classes instead) |
@@ -68,12 +67,64 @@ Embla expects the **viewport** element (overflow hidden) with a **container** ch
68
67
 
69
68
  ### Reactive state
70
69
 
71
- Each instance in `$store.carousel.instances[id]` exposes:
70
+ Each entry in `$store.carousel.instances[id]` is a **reactive mirror** of controller state (updated on `change` and `slideChange`). Use store commands to mutate; direct writes to `instances[id]` do not affect the controller.
71
+
72
+ Exposed fields:
72
73
 
73
74
  - `currentIndex`, `totalSlides`, `progress`
74
75
  - `isFirst`, `isLast`, `isPlaying`
75
76
  - `canNext`, `canPrevious`, `slidesInView`
76
77
 
78
+ ## Standalone usage (no Alpine)
79
+
80
+ ```ts
81
+ import {
82
+ createCarouselController,
83
+ createCarouselStore,
84
+ createCarouselStoreFromController,
85
+ } from "@ailuracode/alpine-carousel";
86
+
87
+ const controller = createCarouselController();
88
+ controller.create("gallery", { loop: true });
89
+ controller.current("gallery"); // 0
90
+
91
+ const store = createCarouselStore();
92
+ // or: createCarouselStoreFromController(controller)
93
+ ```
94
+
95
+ | Controller API | Description |
96
+ |----------------|-------------|
97
+ | `hasInstance(id)` | Whether a carousel id is registered |
98
+ | `snapshotInstances()` | Shallow readonly copies for adapter sync |
99
+ | `current(id)` / `count(id)` / `isPlaying(id)` | Query methods |
100
+
101
+ Subscribe to `controller.on("change", …)` and `controller.on("slideChange", …)` for adapter sync.
102
+
103
+ ## Architecture
104
+
105
+ `CarouselController` owns all mutable state and the Embla instances. The Alpine plugin mirrors snapshots into `$store.carousel.instances`.
106
+
107
+ ## Migration
108
+
109
+ | Removed / changed | Replacement |
110
+ |-------------------|-------------|
111
+ | `controller.instances` getter | `snapshotInstances()` or `hasInstance(id)` |
112
+ | `controller.toStore()` | `createCarouselStore()` or `createCarouselStoreFromController(controller)` |
113
+ | `$store.carousel.instance(id)` | `goTo(id, index)`, `next(id)`, `previous(id)`, and other semantic store methods |
114
+ | `CarouselInstance.embla` / `.autoplay` / `.viewport` on snapshots | Use semantic store methods and readonly snapshot fields |
115
+ | `CarouselOptions.align` / `.containScroll` typed via Embla | Toolkit-owned `CarouselAlign` and `CarouselContainScroll` types |
116
+ | (none) | `storeKey` — see [Avoiding name collisions](#avoiding-name-collisions) |
117
+
118
+ ### Avoiding name collisions
119
+
120
+ If your application already owns a `$store.carousel` — or another toolkit plugin registers on that name — rename the integration surface without touching the controller:
121
+
122
+ ```ts
123
+ Alpine.plugin(carouselPlugin({ storeKey: "slider" })); // → $store.slider
124
+ ```
125
+
126
+ The exposed constant `DEFAULT_CAROUSEL_STORE_KEY` keeps the rename discoverable from TypeScript.
127
+
77
128
  ## Options
78
129
 
79
130
  | Option | Default | Maps to Embla |
package/dist/global.d.ts CHANGED
@@ -5,7 +5,9 @@ import type { CarouselStore } from "./types";
5
5
  export { CarouselController, createCarouselController, createCarouselStore } from "./controller";
6
6
  export type { CarouselEvents, CarouselSlideChangeDetail } from "./events";
7
7
  export type {
8
+ CarouselAlign,
8
9
  CarouselAutoplayOptions,
10
+ CarouselContainScroll,
9
11
  CarouselInstance,
10
12
  CarouselOptions,
11
13
  CarouselStore,
package/dist/index.d.ts CHANGED
@@ -1,8 +1,6 @@
1
1
  import { Alpine, PluginCallback, BaseController } from '@ailuracode/alpine-core';
2
2
  export { Unsubscribe } from '@ailuracode/alpine-core';
3
- import { EmblaOptionsType, EmblaCarouselType } from 'embla-carousel';
4
3
  import { Alpine as Alpine$1 } from 'alpinejs';
5
- import { AutoplayType } from 'embla-carousel-autoplay';
6
4
 
7
5
  /**
8
6
  * Strongly-typed event map for the carousel controller.
@@ -15,12 +13,17 @@ interface CarouselSlideChangeDetail {
15
13
  readonly index: number;
16
14
  readonly totalSlides: number;
17
15
  }
16
+ /** Detail payload for the `change` event (adapter sync). */
17
+ interface CarouselChangeDetail {
18
+ readonly carouselId?: string;
19
+ }
18
20
  /**
19
21
  * Event map for carousel state changes. Emits `slideChange` on
20
- * every slide transition so consumers can react programmatically.
22
+ * every slide transition and `change` when instance state updates.
21
23
  */
22
24
  interface CarouselEvents extends Record<string, unknown> {
23
25
  slideChange: CarouselSlideChangeDetail;
26
+ change: CarouselChangeDetail;
24
27
  }
25
28
 
26
29
  /**
@@ -30,6 +33,10 @@ interface CarouselEvents extends Record<string, unknown> {
30
33
  * them without pulling the implementation. The shape IS the contract.
31
34
  */
32
35
 
36
+ /** Slide alignment within the viewport. */
37
+ type CarouselAlign = "start" | "center" | "end" | ((viewSize: number, snapSize: number, index: number) => number);
38
+ /** Scroll containment behavior at the start and end of the carousel. */
39
+ type CarouselContainScroll = "trimSnaps" | "keepSnaps" | false;
33
40
  /** Autoplay configuration options. */
34
41
  type CarouselAutoplayOptions = {
35
42
  delay?: number;
@@ -44,14 +51,14 @@ type CarouselOptions = {
44
51
  autoplay?: boolean;
45
52
  autoplayOptions?: CarouselAutoplayOptions;
46
53
  axis?: "x" | "y";
47
- align?: EmblaOptionsType["align"];
48
- containScroll?: EmblaOptionsType["containScroll"];
54
+ align?: CarouselAlign;
55
+ containScroll?: CarouselContainScroll;
49
56
  dragFree?: boolean;
50
57
  duration?: number;
51
58
  ariaLive?: "off" | "polite" | "assertive";
52
59
  onChange?: (index: number) => void;
53
60
  };
54
- /** Internal representation of a carousel instance. */
61
+ /** Readonly snapshot of carousel state exposed through stores and adapters. */
55
62
  type CarouselInstance = {
56
63
  currentIndex: number;
57
64
  totalSlides: number;
@@ -64,9 +71,6 @@ type CarouselInstance = {
64
71
  slidesInView: number[];
65
72
  options: CarouselOptions;
66
73
  ariaLive: "off" | "polite" | "assertive";
67
- viewport: HTMLElement | null;
68
- embla: EmblaCarouselType | null;
69
- autoplay: AutoplayType | null;
70
74
  };
71
75
  /** Alpine-facing store surface. */
72
76
  type CarouselStore = {
@@ -84,7 +88,6 @@ type CarouselStore = {
84
88
  play(id: string): void;
85
89
  pause(id: string): void;
86
90
  isPlaying(id: string): boolean;
87
- instance(id: string): EmblaCarouselType | null;
88
91
  handleKeydown(id: string, event: KeyboardEvent): void;
89
92
  carouselProps(id: string, options?: {
90
93
  label?: string;
@@ -99,7 +102,17 @@ type CarouselStore = {
99
102
  /** Options accepted by the carousel plugin factory. */
100
103
  interface CreateCarouselOptions {
101
104
  readonly id?: string;
105
+ /**
106
+ * `$store.carousel` store key the Alpine plugin registers under.
107
+ * Defaults to {@link DEFAULT_CAROUSEL_STORE_KEY}. Set when the
108
+ * host already owns a `carousel` store or another toolkit plugin
109
+ * would collide on that name — the rename avoids the collision
110
+ * without touching the controller.
111
+ */
112
+ readonly storeKey?: string;
102
113
  }
114
+ /** Default `$store.carousel` key registered by {@link carouselPlugin}. */
115
+ declare const DEFAULT_CAROUSEL_STORE_KEY = "carousel";
103
116
  /** Typed view of `Alpine` the carousel plugin uses internally. */
104
117
  type CarouselAlpine = Alpine<{
105
118
  carousel: CarouselStore;
@@ -126,7 +139,13 @@ type CarouselPluginCallback = PluginCallback<Alpine$1>;
126
139
  declare class CarouselController extends BaseController<CarouselEvents> {
127
140
  #private;
128
141
  constructor(id?: string);
129
- get instances(): Readonly<Record<string, CarouselInstance>>;
142
+ /** Whether a carousel instance is registered. */
143
+ hasInstance(id: string): boolean;
144
+ /**
145
+ * Returns a shallow snapshot of all instances for adapter sync.
146
+ * Mutating the returned objects does not affect controller state.
147
+ */
148
+ snapshotInstances(): Record<string, CarouselInstance>;
130
149
  create(id: string, options?: CarouselOptions): void;
131
150
  /** Destroy a single carousel instance by id. */
132
151
  destroy(id: string): void;
@@ -143,7 +162,6 @@ declare class CarouselController extends BaseController<CarouselEvents> {
143
162
  play(id: string): void;
144
163
  pause(id: string): void;
145
164
  isPlaying(id: string): boolean;
146
- instance(id: string): EmblaCarouselType | null;
147
165
  handleKeydown(id: string, event: KeyboardEvent): void;
148
166
  carouselProps(id: string, options?: {
149
167
  label?: string;
@@ -154,22 +172,12 @@ declare class CarouselController extends BaseController<CarouselEvents> {
154
172
  slideProps(id: string, index: number): Record<string, string | boolean | undefined>;
155
173
  indicatorProps(id: string, index: number): Record<string, string | boolean | undefined>;
156
174
  destroyAll(): void;
157
- /**
158
- * Returns a store-shaped object for Alpine's `$store.carousel`.
159
- * The store delegates to this controller.
160
- */
161
- toStore(): CarouselStore;
162
175
  }
163
176
  /**
164
177
  * Creates a CarouselController and returns the controller instance.
165
178
  * Convenience for non-Alpine consumers.
166
179
  */
167
180
  declare function createCarouselController(id?: string): CarouselController;
168
- /**
169
- * Creates a CarouselStore (store-shaped object) directly.
170
- * Backward-compatible alias.
171
- */
172
- declare function createCarouselStore(): CarouselStore;
173
181
 
174
182
  /**
175
183
  * Alpine.js integration for `@ailuracode/alpine-carousel`.
@@ -187,4 +195,13 @@ declare function carouselPlugin(options?: CreateCarouselOptions): CarouselPlugin
187
195
  /** Builds typed carousel plugin options. */
188
196
  declare function carouselOptions<const T extends CreateCarouselOptions>(options: T): T;
189
197
 
190
- export { type CarouselAlpine, type CarouselAutoplayOptions, CarouselController, type CarouselEvents, type CarouselInstance, type CarouselOptions, type CarouselPluginCallback, type CarouselSlideChangeDetail, type CarouselStore, type CreateCarouselOptions, carouselOptions, carouselPlugin, createCarouselController, createCarouselStore, carouselPlugin as default };
198
+ /**
199
+ * Store factory for `@ailuracode/alpine-carousel`.
200
+ */
201
+
202
+ /** Builds a {@link CarouselStore} backed by a new {@link CarouselController}. */
203
+ declare function createCarouselStore(): CarouselStore;
204
+ /** Builds a {@link CarouselStore} that mirrors the given controller. */
205
+ declare function createCarouselStoreFromController(controller: CarouselController): CarouselStore;
206
+
207
+ export { type CarouselAlign, type CarouselAlpine, type CarouselAutoplayOptions, type CarouselChangeDetail, type CarouselContainScroll, CarouselController, type CarouselEvents, type CarouselInstance, type CarouselOptions, type CarouselPluginCallback, type CarouselSlideChangeDetail, type CarouselStore, type CreateCarouselOptions, DEFAULT_CAROUSEL_STORE_KEY, carouselOptions, carouselPlugin, createCarouselController, createCarouselStore, createCarouselStoreFromController, carouselPlugin as default };
package/dist/index.js CHANGED
@@ -1,418 +1 @@
1
- // src/controller.ts
2
- import { BaseController, generateId } from "@ailuracode/alpine-core";
3
- import EmblaCarousel from "embla-carousel";
4
- import Autoplay from "embla-carousel-autoplay";
5
- function createInstance(options = {}) {
6
- return {
7
- currentIndex: 0,
8
- totalSlides: 0,
9
- progress: 0,
10
- isFirst: true,
11
- isLast: false,
12
- isPlaying: false,
13
- canNext: false,
14
- canPrevious: false,
15
- slidesInView: [],
16
- options,
17
- ariaLive: options.ariaLive ?? "polite",
18
- viewport: null,
19
- embla: null,
20
- autoplay: null
21
- };
22
- }
23
- function toEmblaOptions(options) {
24
- return {
25
- loop: options.loop ?? false,
26
- axis: options.axis ?? "x",
27
- align: options.align ?? "start",
28
- containScroll: options.containScroll ?? "trimSnaps",
29
- dragFree: options.dragFree ?? false,
30
- duration: options.duration ?? 25
31
- };
32
- }
33
- function createAutoplayPlugin(options) {
34
- if (!options.autoplay) {
35
- return null;
36
- }
37
- const autoplayOptions = options.autoplayOptions ?? {};
38
- const stopOnMouseEnter = autoplayOptions.stopOnMouseEnter ?? false;
39
- const stopOnInteraction = autoplayOptions.stopOnInteraction ?? !stopOnMouseEnter;
40
- return Autoplay({
41
- delay: autoplayOptions.delay ?? 4e3,
42
- stopOnInteraction,
43
- stopOnMouseEnter,
44
- stopOnFocusIn: autoplayOptions.stopOnFocusIn ?? true,
45
- playOnInit: true
46
- });
47
- }
48
- var CarouselController = class extends BaseController {
49
- #instances = {};
50
- #cleanups = /* @__PURE__ */ new Map();
51
- constructor(id) {
52
- super(id ?? generateId("carousel"));
53
- }
54
- get instances() {
55
- return this.#instances;
56
- }
57
- create(id, options = {}) {
58
- if (this.isDestroyed) {
59
- return;
60
- }
61
- this.#getOrCreate(id, options);
62
- }
63
- destroy(id) {
64
- if (id !== void 0) {
65
- if (this.isDestroyed) {
66
- return;
67
- }
68
- const instance = this.#instances[id];
69
- if (!instance) {
70
- return;
71
- }
72
- this.#destroyEmbla(id);
73
- delete this.#instances[id];
74
- } else {
75
- this.destroyAll();
76
- super.destroy();
77
- }
78
- }
79
- bindViewport(id, viewport) {
80
- if (this.isDestroyed) {
81
- return;
82
- }
83
- if (typeof window === "undefined" || typeof document === "undefined") {
84
- return;
85
- }
86
- if (!viewport) {
87
- const instance = this.#instances[id];
88
- if (instance) {
89
- this.#destroyEmbla(id);
90
- }
91
- return;
92
- }
93
- this.#getOrCreate(id);
94
- this.#initEmbla(id, viewport);
95
- }
96
- next(id) {
97
- this.#instances[id]?.embla?.scrollNext();
98
- }
99
- previous(id) {
100
- this.#instances[id]?.embla?.scrollPrev();
101
- }
102
- goTo(id, index) {
103
- this.#instances[id]?.embla?.scrollTo(index);
104
- }
105
- current(id) {
106
- return this.#instances[id]?.currentIndex ?? 0;
107
- }
108
- count(id) {
109
- return this.#instances[id]?.totalSlides ?? 0;
110
- }
111
- canNext(id) {
112
- return this.#instances[id]?.canNext ?? false;
113
- }
114
- canPrevious(id) {
115
- return this.#instances[id]?.canPrevious ?? false;
116
- }
117
- play(id) {
118
- this.#instances[id]?.autoplay?.play();
119
- const instance = this.#instances[id];
120
- if (instance) {
121
- instance.isPlaying = instance.autoplay?.isPlaying() ?? false;
122
- }
123
- }
124
- pause(id) {
125
- this.#instances[id]?.autoplay?.stop();
126
- const instance = this.#instances[id];
127
- if (instance) {
128
- instance.isPlaying = instance.autoplay?.isPlaying() ?? false;
129
- }
130
- }
131
- isPlaying(id) {
132
- return this.#instances[id]?.isPlaying ?? false;
133
- }
134
- instance(id) {
135
- return this.#instances[id]?.embla ?? null;
136
- }
137
- handleKeydown(id, event) {
138
- const instance = this.#instances[id];
139
- if (!instance?.embla) {
140
- return;
141
- }
142
- switch (event.key) {
143
- case "ArrowLeft":
144
- case "ArrowUp":
145
- event.preventDefault();
146
- this.previous(id);
147
- break;
148
- case "ArrowRight":
149
- case "ArrowDown":
150
- event.preventDefault();
151
- this.next(id);
152
- break;
153
- case "Home":
154
- event.preventDefault();
155
- this.goTo(id, 0);
156
- break;
157
- case "End": {
158
- event.preventDefault();
159
- const last = this.count(id) - 1;
160
- if (last >= 0) {
161
- this.goTo(id, last);
162
- }
163
- break;
164
- }
165
- default:
166
- break;
167
- }
168
- }
169
- carouselProps(id, options = {}) {
170
- const instance = this.#instances[id];
171
- return {
172
- role: "region",
173
- "aria-roledescription": "carousel",
174
- "aria-live": instance?.ariaLive ?? "polite",
175
- "aria-label": options.label
176
- };
177
- }
178
- viewportProps(_id, options = {}) {
179
- const props = {
180
- tabindex: 0
181
- };
182
- if (options.slideSize !== false) {
183
- props.style = `--slide-size: ${options.slideSize ?? "100%"}`;
184
- }
185
- return props;
186
- }
187
- slideProps(id, index) {
188
- const current = this.current(id);
189
- const total = this.count(id);
190
- return {
191
- role: "group",
192
- "aria-roledescription": "slide",
193
- "aria-label": `${index + 1} of ${total}`,
194
- "aria-hidden": current !== index ? true : void 0
195
- };
196
- }
197
- indicatorProps(id, index) {
198
- const selected = this.current(id) === index;
199
- return {
200
- type: "button",
201
- "aria-label": `Go to slide ${index + 1}`,
202
- "aria-current": selected ? "true" : void 0
203
- };
204
- }
205
- destroyAll() {
206
- for (const id of Object.keys(this.#instances)) {
207
- this.destroy(id);
208
- }
209
- }
210
- /**
211
- * Returns a store-shaped object for Alpine's `$store.carousel`.
212
- * The store delegates to this controller.
213
- */
214
- toStore() {
215
- return {
216
- instances: this.#instances,
217
- create: (id, opts) => this.create(id, opts),
218
- destroy: (id) => this.destroy(id),
219
- bindViewport: (id, viewport) => this.bindViewport(id, viewport),
220
- next: (id) => this.next(id),
221
- previous: (id) => this.previous(id),
222
- goTo: (id, index) => this.goTo(id, index),
223
- current: (id) => this.current(id),
224
- count: (id) => this.count(id),
225
- canNext: (id) => this.canNext(id),
226
- canPrevious: (id) => this.canPrevious(id),
227
- play: (id) => this.play(id),
228
- pause: (id) => this.pause(id),
229
- isPlaying: (id) => this.isPlaying(id),
230
- instance: (id) => this.instance(id),
231
- handleKeydown: (id, event) => this.handleKeydown(id, event),
232
- carouselProps: (id, opts) => this.carouselProps(id, opts),
233
- viewportProps: (id, opts) => this.viewportProps(id, opts),
234
- slideProps: (id, index) => this.slideProps(id, index),
235
- indicatorProps: (id, index) => this.indicatorProps(id, index),
236
- destroyAll: () => this.destroyAll()
237
- };
238
- }
239
- #getOrCreate(id, options) {
240
- if (!this.#instances[id]) {
241
- this.#instances[id] = createInstance(options);
242
- } else if (options) {
243
- this.#instances[id].options = { ...this.#instances[id].options, ...options };
244
- this.#instances[id].ariaLive = options.ariaLive ?? this.#instances[id].ariaLive;
245
- }
246
- return this.#instances[id];
247
- }
248
- #syncFromEmbla(id) {
249
- const instance = this.#instances[id];
250
- const embla = instance?.embla;
251
- if (!(instance && embla)) {
252
- return;
253
- }
254
- const previousIndex = instance.currentIndex;
255
- instance.currentIndex = embla.selectedScrollSnap();
256
- instance.totalSlides = embla.scrollSnapList().length;
257
- instance.progress = embla.scrollProgress();
258
- instance.canNext = embla.canScrollNext();
259
- instance.canPrevious = embla.canScrollPrev();
260
- instance.slidesInView = embla.slidesInView();
261
- instance.isFirst = instance.currentIndex === 0;
262
- instance.isLast = instance.totalSlides > 0 && instance.currentIndex === instance.totalSlides - 1;
263
- instance.isPlaying = instance.autoplay?.isPlaying() ?? false;
264
- if (previousIndex !== instance.currentIndex) {
265
- this.emit("slideChange", {
266
- carouselId: id,
267
- index: instance.currentIndex,
268
- totalSlides: instance.totalSlides
269
- });
270
- instance.options.onChange?.(instance.currentIndex);
271
- }
272
- }
273
- #destroyEmbla(id) {
274
- this.#cleanups.get(id)?.();
275
- this.#cleanups.delete(id);
276
- const instance = this.#instances[id];
277
- instance?.embla?.destroy();
278
- if (instance) {
279
- instance.embla = null;
280
- instance.autoplay = null;
281
- instance.viewport = null;
282
- }
283
- }
284
- #addCleanup(id, cleanup) {
285
- const previous = this.#cleanups.get(id);
286
- this.#cleanups.set(id, () => {
287
- previous?.();
288
- cleanup();
289
- });
290
- }
291
- #setupVisibilityAutoplay(id, instance) {
292
- const stopWhenHidden = instance.options.autoplayOptions?.stopWhenHidden ?? true;
293
- if (!(instance.options.autoplay && stopWhenHidden)) {
294
- return;
295
- }
296
- const onVisibilityChange = () => {
297
- if (document.hidden) {
298
- instance.autoplay?.stop();
299
- } else {
300
- instance.autoplay?.play();
301
- }
302
- instance.isPlaying = instance.autoplay?.isPlaying() ?? false;
303
- };
304
- document.addEventListener("visibilitychange", onVisibilityChange);
305
- this.#addCleanup(
306
- id,
307
- () => document.removeEventListener("visibilitychange", onVisibilityChange)
308
- );
309
- }
310
- #setupResizeObserver(id, viewport) {
311
- if (typeof ResizeObserver === "undefined") {
312
- return;
313
- }
314
- const observer = new ResizeObserver(() => {
315
- this.#instances[id]?.embla?.reInit();
316
- this.#syncFromEmbla(id);
317
- });
318
- observer.observe(viewport);
319
- this.#addCleanup(id, () => observer.disconnect());
320
- }
321
- #initEmbla(id, viewport) {
322
- const instance = this.#instances[id];
323
- if (!instance) {
324
- return;
325
- }
326
- this.#destroyEmbla(id);
327
- instance.viewport = viewport;
328
- const plugins = [];
329
- const autoplay = createAutoplayPlugin(instance.options);
330
- if (autoplay) {
331
- plugins.push(autoplay);
332
- instance.autoplay = autoplay;
333
- }
334
- const embla = EmblaCarousel(viewport, toEmblaOptions(instance.options), plugins);
335
- instance.embla = embla;
336
- const sync = () => this.#syncFromEmbla(id);
337
- embla.on("select", sync);
338
- embla.on("scroll", sync);
339
- embla.on("reInit", sync);
340
- embla.on("autoplay:play", sync);
341
- embla.on("autoplay:stop", sync);
342
- sync();
343
- this.#setupResizeObserver(id, viewport);
344
- this.#setupVisibilityAutoplay(id, instance);
345
- if (typeof requestAnimationFrame !== "undefined") {
346
- requestAnimationFrame(() => {
347
- instance.embla?.reInit();
348
- this.#syncFromEmbla(id);
349
- });
350
- }
351
- }
352
- };
353
- function createCarouselController(id) {
354
- return new CarouselController(id);
355
- }
356
- function createCarouselStore() {
357
- return new CarouselController().toStore();
358
- }
359
-
360
- // src/plugin.ts
361
- var CAROUSEL_STORE_KEY = "carousel";
362
- function carouselPlugin(options = {}) {
363
- return function registerCarousel(alpine) {
364
- const Alpine = alpine;
365
- const controller = new CarouselController(options.id);
366
- const store = {
367
- instances: controller.instances,
368
- create: (id, opts) => controller.create(id, opts),
369
- destroy: (id) => controller.destroy(id),
370
- bindViewport: (id, viewport) => controller.bindViewport(id, viewport),
371
- next: (id) => controller.next(id),
372
- previous: (id) => controller.previous(id),
373
- goTo: (id, index) => controller.goTo(id, index),
374
- current: (id) => controller.current(id),
375
- count: (id) => controller.count(id),
376
- canNext: (id) => controller.canNext(id),
377
- canPrevious: (id) => controller.canPrevious(id),
378
- play: (id) => controller.play(id),
379
- pause: (id) => controller.pause(id),
380
- isPlaying: (id) => controller.isPlaying(id),
381
- instance: (id) => controller.instance(id),
382
- handleKeydown: (id, event) => controller.handleKeydown(id, event),
383
- carouselProps: (id, opts) => controller.carouselProps(id, opts),
384
- viewportProps: (id, opts) => controller.viewportProps(id, opts),
385
- slideProps: (id, index) => controller.slideProps(id, index),
386
- indicatorProps: (id, index) => controller.indicatorProps(id, index),
387
- destroyAll: () => controller.destroyAll()
388
- };
389
- Alpine.store(CAROUSEL_STORE_KEY, store);
390
- const reactiveStore = Alpine.store(CAROUSEL_STORE_KEY);
391
- controller.on("slideChange", () => {
392
- const controllerInstances = controller.instances;
393
- for (const key of Object.keys(controllerInstances)) {
394
- reactiveStore.instances[key] = controllerInstances[key];
395
- }
396
- for (const key of Object.keys(reactiveStore.instances)) {
397
- if (!(key in controllerInstances)) {
398
- delete reactiveStore.instances[key];
399
- }
400
- }
401
- });
402
- Alpine.magic(CAROUSEL_STORE_KEY, () => reactiveStore);
403
- if (typeof Alpine.cleanup === "function") {
404
- Alpine.cleanup(() => controller.destroy());
405
- }
406
- };
407
- }
408
- function carouselOptions(options) {
409
- return options;
410
- }
411
- export {
412
- CarouselController,
413
- carouselOptions,
414
- carouselPlugin,
415
- createCarouselController,
416
- createCarouselStore,
417
- carouselPlugin as default
418
- };
1
+ import{BaseController as C,generateId as m}from"@ailuracode/alpine-core";import h from"embla-carousel";import v from"embla-carousel-autoplay";function x(s={}){return{currentIndex:0,totalSlides:0,progress:0,isFirst:!0,isLast:!1,isPlaying:!1,canNext:!1,canPrevious:!1,slidesInView:[],options:s,ariaLive:s.ariaLive??"polite",viewport:null,embla:null,autoplay:null}}function I(s){return{loop:s.loop??!1,axis:s.axis??"x",align:s.align??"start",containScroll:s.containScroll??"trimSnaps",dragFree:s.dragFree??!1,duration:s.duration??25}}function P(s){if(!s.autoplay)return null;let e=s.autoplayOptions??{},n=e.stopOnMouseEnter??!1,r=e.stopOnInteraction??!n;return v({delay:e.delay??4e3,stopOnInteraction:r,stopOnMouseEnter:n,stopOnFocusIn:e.stopOnFocusIn??!0,playOnInit:!0})}function O(s){return{currentIndex:s.currentIndex,totalSlides:s.totalSlides,progress:s.progress,isFirst:s.isFirst,isLast:s.isLast,isPlaying:s.isPlaying,canNext:s.canNext,canPrevious:s.canPrevious,slidesInView:[...s.slidesInView],options:{...s.options},ariaLive:s.ariaLive}}var u=class extends C{#e={};#s=new Map;constructor(e){super(e??m("carousel"))}hasInstance(e){return e in this.#e}snapshotInstances(){let e={};for(let[n,r]of Object.entries(this.#e))e[n]=O(r);return e}create(e,n={}){this.isDestroyed||(this.#o(e,n),this.#n(e))}destroy(e){if(e!==void 0){if(this.isDestroyed||!this.#e[e])return;this.#r(e),delete this.#e[e],this.#n(e)}else this.destroyAll(),super.destroy()}bindViewport(e,n){if(!this.isDestroyed&&!(typeof window>"u"||typeof document>"u")){if(!n){this.#e[e]&&this.#r(e);return}this.#o(e),this.#u(e,n),this.#n(e)}}next(e){this.#e[e]?.embla?.scrollNext()}previous(e){this.#e[e]?.embla?.scrollPrev()}goTo(e,n){this.#e[e]?.embla?.scrollTo(n)}current(e){return this.#e[e]?.currentIndex??0}count(e){return this.#e[e]?.totalSlides??0}canNext(e){return this.#e[e]?.canNext??!1}canPrevious(e){return this.#e[e]?.canPrevious??!1}play(e){this.#e[e]?.autoplay?.play();let n=this.#e[e];n&&(n.isPlaying=n.autoplay?.isPlaying()??!1,this.#n(e))}pause(e){this.#e[e]?.autoplay?.stop();let n=this.#e[e];n&&(n.isPlaying=n.autoplay?.isPlaying()??!1,this.#n(e))}isPlaying(e){return this.#e[e]?.isPlaying??!1}handleKeydown(e,n){if(this.#e[e]?.embla)switch(n.key){case"ArrowLeft":case"ArrowUp":n.preventDefault(),this.previous(e);break;case"ArrowRight":case"ArrowDown":n.preventDefault(),this.next(e);break;case"Home":n.preventDefault(),this.goTo(e,0);break;case"End":{n.preventDefault();let t=this.count(e)-1;t>=0&&this.goTo(e,t);break}default:break}}carouselProps(e,n={}){return{role:"region","aria-roledescription":"carousel","aria-live":this.#e[e]?.ariaLive??"polite","aria-label":n.label}}viewportProps(e,n={}){let r={tabindex:0};return n.slideSize!==!1&&(r.style=`--slide-size: ${n.slideSize??"100%"}`),r}slideProps(e,n){let r=this.current(e),t=this.count(e);return{role:"group","aria-roledescription":"slide","aria-label":`${n+1} of ${t}`,"aria-hidden":r!==n?!0:void 0}}indicatorProps(e,n){let r=this.current(e)===n;return{type:"button","aria-label":`Go to slide ${n+1}`,"aria-current":r?"true":void 0}}destroyAll(){for(let e of Object.keys(this.#e))this.destroy(e);this.#n()}#n(e){this.emit("change",{carouselId:e})}#o(e,n){return this.#e[e]?n&&(this.#e[e].options={...this.#e[e].options,...n},this.#e[e].ariaLive=n.ariaLive??this.#e[e].ariaLive):this.#e[e]=x(n),this.#e[e]}#t(e){let n=this.#e[e],r=n?.embla;if(!(n&&r))return;let t=n.currentIndex;n.currentIndex=r.selectedScrollSnap(),n.totalSlides=r.scrollSnapList().length,n.progress=r.scrollProgress(),n.canNext=r.canScrollNext(),n.canPrevious=r.canScrollPrev(),n.slidesInView=r.slidesInView(),n.isFirst=n.currentIndex===0,n.isLast=n.totalSlides>0&&n.currentIndex===n.totalSlides-1,n.isPlaying=n.autoplay?.isPlaying()??!1,t!==n.currentIndex&&(this.emit("slideChange",{carouselId:e,index:n.currentIndex,totalSlides:n.totalSlides}),n.options.onChange?.(n.currentIndex)),this.#n(e)}#r(e){this.#s.get(e)?.(),this.#s.delete(e);let n=this.#e[e];n?.embla?.destroy(),n&&(n.embla=null,n.autoplay=null,n.viewport=null)}#i(e,n){let r=this.#s.get(e);this.#s.set(e,()=>{r?.(),n()})}#a(e,n){let r=n.options.autoplayOptions?.stopWhenHidden??!0;if(!(n.options.autoplay&&r))return;let t=()=>{document.hidden?n.autoplay?.stop():n.autoplay?.play(),n.isPlaying=n.autoplay?.isPlaying()??!1};document.addEventListener("visibilitychange",t),this.#i(e,()=>document.removeEventListener("visibilitychange",t))}#l(e,n){if(typeof ResizeObserver>"u")return;let r=new ResizeObserver(()=>{this.#e[e]?.embla?.reInit(),this.#t(e)});r.observe(n),this.#i(e,()=>r.disconnect())}#u(e,n){let r=this.#e[e];if(!r)return;this.#r(e),r.viewport=n;let t=[],o=P(r.options);o&&(t.push(o),r.autoplay=o);let i=h(n,I(r.options),t);r.embla=i;let l=()=>this.#t(e);i.on("select",l),i.on("scroll",l),i.on("reInit",l),i.on("autoplay:play",l),i.on("autoplay:stop",l),l(),this.#l(e,n),this.#a(e,r),typeof requestAnimationFrame<"u"&&requestAnimationFrame(()=>{r.embla?.reInit(),this.#t(e)})}};function A(s){return new u(s)}import{bridgeControllerStore as R,syncRecordFromSnapshot as T}from"@ailuracode/alpine-core";import{syncRecordFromSnapshot as S}from"@ailuracode/alpine-core";function E(s,e){w(s,e.snapshotInstances())}function w(s,e){S(s,e)}function L(){return c(new u)}function c(s){let e={},n=()=>{E(e,s)},r={instances:e,create:(t,o)=>{s.create(t,o),n()},destroy:t=>{s.destroy(t),n()},bindViewport:(t,o)=>{s.bindViewport(t,o),n()},next:t=>{s.next(t)},previous:t=>{s.previous(t)},goTo:(t,o)=>{s.goTo(t,o)},current:t=>s.current(t),count:t=>s.count(t),canNext:t=>s.canNext(t),canPrevious:t=>s.canPrevious(t),play:t=>{s.play(t),n()},pause:t=>{s.pause(t),n()},isPlaying:t=>s.isPlaying(t),handleKeydown:(t,o)=>s.handleKeydown(t,o),carouselProps:(t,o)=>s.carouselProps(t,o),viewportProps:(t,o)=>s.viewportProps(t,o),slideProps:(t,o)=>s.slideProps(t,o),indicatorProps:(t,o)=>s.indicatorProps(t,o),destroyAll:()=>{s.destroyAll(),n()}};return s.on("change",n),s.on("slideChange",n),r}var g="carousel";function y(s={}){let e=s.storeKey??g;return function(r){let t=r,o=new u(s.id);R({alpine:t,storeKey:e,store:c(o),controller:o,packageName:"carousel",subscribe:i=>{i.current=a=>i.instances[a]?.currentIndex??0,i.count=a=>i.instances[a]?.totalSlides??0,i.canNext=a=>i.instances[a]?.canNext??!1,i.canPrevious=a=>i.instances[a]?.canPrevious??!1,i.isPlaying=a=>i.instances[a]?.isPlaying??!1,i.slideProps=(a,p)=>{let d=i.current(a),b=i.count(a);return{role:"group","aria-roledescription":"slide","aria-label":`${p+1} of ${b}`,"aria-hidden":d!==p?!0:void 0}},i.indicatorProps=(a,p)=>{let d=i.current(a)===p;return{type:"button","aria-label":`Go to slide ${p+1}`,"aria-current":d?"true":void 0}};let l=()=>{T(i.instances,o.snapshotInstances())},f=[o.on("change",l),o.on("slideChange",l)];return()=>{for(let a of f)a()}}})}}function F(s){return s}export{u as CarouselController,g as DEFAULT_CAROUSEL_STORE_KEY,F as carouselOptions,y as carouselPlugin,A as createCarouselController,L as createCarouselStore,c as createCarouselStoreFromController,y as default};
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "@ailuracode/alpine-carousel",
3
- "version": "1.0.0",
3
+ "version": "2.0.0",
4
4
  "description": "Alpine.js headless accessible carousel store powered by Embla Carousel",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "author": "ailuracode",
8
+ "sideEffects": false,
8
9
  "publishConfig": {
9
10
  "access": "public"
10
11
  },
@@ -36,10 +37,12 @@
36
37
  "embla-carousel-autoplay": "^8.6.0"
37
38
  },
38
39
  "devDependencies": {
39
- "@ailuracode/alpine-core": "0.2.0"
40
+ "@types/alpinejs": "^3.13.11",
41
+ "alpinejs": "^3.15.12",
42
+ "@ailuracode/alpine-core": "0.3.0"
40
43
  },
41
44
  "peerDependencies": {
42
- "@ailuracode/alpine-core": "^0.2.0",
45
+ "@ailuracode/alpine-core": "^0.3.0",
43
46
  "alpinejs": "^3.0.0",
44
47
  "@types/alpinejs": "^3.13.11"
45
48
  },
@@ -58,8 +61,30 @@
58
61
  "accessibility",
59
62
  "headless"
60
63
  ],
64
+ "toolkit": {
65
+ "bundleBudget": {
66
+ "category": "complex-feature"
67
+ }
68
+ },
69
+ "size-limit": [
70
+ {
71
+ "name": "full surface",
72
+ "path": "dist/index.js",
73
+ "import": "*",
74
+ "ignore": [
75
+ "alpinejs",
76
+ "@ailuracode/alpine-core",
77
+ "@types/alpinejs"
78
+ ],
79
+ "gzip": true,
80
+ "brotli": true,
81
+ "limit": "12 kB"
82
+ }
83
+ ],
61
84
  "scripts": {
62
- "build": "tsup src/index.ts --format esm --dts --clean --out-dir dist && cp src/global.d.ts dist/global.d.ts",
63
- "test": "vitest run --config ../../vitest.config.ts test"
85
+ "build": "tsup src/index.ts --format esm --dts --clean --out-dir dist --minify && cp src/global.d.ts dist/global.d.ts",
86
+ "size": "size-limit",
87
+ "test": "vitest run --config ../../vitest.config.ts packages/carousel",
88
+ "test:e2e": "playwright test --config playwright.config.ts"
64
89
  }
65
90
  }