@ailuracode/alpine-accordion 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
@@ -1,23 +1,184 @@
1
1
  # @ailuracode/alpine-accordion
2
2
 
3
- Headless accessible accordion store for Alpine.js — single or multiple open panels, keyboard focus, and ARIA helpers.
4
-
5
- **[Full documentation →](../../docs/plugins/accordion.md)**
3
+ Headless accordion store with **single** or **multiple** open panels, optional **default open** items, keyboard focus management, and ARIA helpers.
6
4
 
7
5
  ## Install
8
6
 
9
7
  ```bash
10
- pnpm add @ailuracode/alpine-accordion alpinejs
8
+ pnpm add @ailuracode/alpine-accordion @ailuracode/alpine-core alpinejs
9
+ ```
10
+
11
+ Open panel state is backed by an inline lightweight state — no extra dependency.
12
+
13
+ ## Quick start
14
+
15
+ ```js
16
+ import Alpine from "alpinejs";
17
+ import { accordionPlugin } from "@ailuracode/alpine-accordion";
18
+
19
+ Alpine.plugin(accordionPlugin());
20
+ Alpine.start();
11
21
  ```
12
22
 
13
23
  ## Store API
14
24
 
25
+ | Method | Description |
26
+ |--------|-------------|
27
+ | `register(accordionId, options?)` | Create a group (`mode`, `defaultOpen`, `onChange`) |
28
+ | `registerItem(accordionId, itemId, disabled?)` | Register a panel trigger |
29
+ | `open(accordionId, itemId)` / `close` / `toggle` | Panel visibility |
30
+ | `isOpen(accordionId, itemId)` | Whether a panel is expanded |
31
+ | `openIds(accordionId)` | Array of currently open item ids |
32
+ | `activeItem(accordionId)` | Focused trigger id |
33
+ | `handleKeydown(accordionId, event)` | `ArrowUp`/`ArrowDown`/`Home`/`End` |
34
+ | `triggerProps(accordionId, itemId)` | `aria-expanded`, `aria-controls`, `tabindex` |
35
+ | `panelProps(accordionId, itemId)` | `role`, `aria-labelledby`, `aria-hidden` |
36
+
37
+ ## Options
38
+
39
+ | Option | Default | Description |
40
+ |--------|---------|-------------|
41
+ | `mode` | `'single'` | `'single'` closes other panels when one opens; `'multiple'` allows several open |
42
+ | `defaultOpen` | — | Item id or array of ids open after `registerItem` |
43
+ | `onChange` | — | `(openIds: string[]) => void` when open state changes |
44
+ | `storeKey` | `'accordion'` | `$store` key the Alpine plugin registers under |
45
+ | `magicKey` | `'accordion'` (or `storeKey` when renamed) | `$accordion` magic key the Alpine plugin registers under |
46
+
47
+ In **single** mode, only the **first** id in `defaultOpen` is used when an array is passed.
48
+
49
+ ### Avoiding name collisions
50
+
51
+ If your application already owns a `$store.accordion` or another toolkit plugin registers on that name, rename the integration surface without touching the controller:
52
+
15
53
  ```ts
16
- $store.accordion.register("faq", { mode: "single", defaultOpen: "item-1" });
17
- $store.accordion.open("faq", "item-1");
18
- $store.accordion.toggle("faq", "item-1");
19
- $store.accordion.isOpen("faq", "item-1");
20
- $store.accordion.openIds("faq");
54
+ Alpine.plugin(accordionPlugin({
55
+ storeKey: "faq", // → $store.faq
56
+ // magicKey follows storeKey by default → $faq
57
+ magicKey: "disclosure", // explicit override → $disclosure
58
+ }));
59
+ ```
60
+
61
+ `storeKey` is the only argument most hosts need. `magicKey` moves independently only when both names must be freed. The exposed constants `DEFAULT_ACCORDION_STORE_KEY` and `DEFAULT_ACCORDION_MAGIC_KEY` keep the rename discoverable from TypeScript.
62
+
63
+ ## Single mode
64
+
65
+ Only one panel open at a time.
66
+
67
+ ```js
68
+ $store.accordion.register("faq", { mode: "single" });
69
+ ["item-1", "item-2"].forEach((id) => $store.accordion.registerItem("faq", id));
70
+ ```
71
+
72
+ ```html
73
+ <div
74
+ x-data
75
+ x-init="
76
+ $store.accordion.register('faq', { mode: 'single' });
77
+ ['item-1','item-2'].forEach(id => $store.accordion.registerItem('faq', id));
78
+ "
79
+ @keydown="$store.accordion.handleKeydown('faq', $event)"
80
+ >
81
+ <template x-for="id in ['item-1','item-2']" :key="id">
82
+ <div>
83
+ <button
84
+ x-bind="$store.accordion.triggerProps('faq', id)"
85
+ @click="$store.accordion.toggle('faq', id)"
86
+ x-text="id"
87
+ ></button>
88
+ <div
89
+ class="overflow-hidden"
90
+ x-show="$store.accordion.isOpen('faq', id)"
91
+ x-collapse
92
+ x-bind="$store.accordion.panelProps('faq', id)"
93
+ >
94
+ <p class="px-4 py-3">Answer for <span x-text="id"></span></p>
95
+ </div>
96
+ </div>
97
+ </template>
98
+ </div>
99
+ ```
100
+
101
+ ## Multiple mode
102
+
103
+ Several panels can stay open.
104
+
105
+ ```js
106
+ $store.accordion.register("settings", { mode: "multiple" });
107
+ ["notifications", "privacy"].forEach((id) => $store.accordion.registerItem("settings", id));
108
+ ```
109
+
110
+ ```html
111
+ <div
112
+ x-init="
113
+ $store.accordion.register('settings', { mode: 'multiple' });
114
+ ['notifications','privacy'].forEach(id => $store.accordion.registerItem('settings', id));
115
+ "
116
+ >
117
+ <!-- same markup as single mode -->
118
+ </div>
21
119
  ```
22
120
 
23
- Modes: `single` (one panel open) or `multiple` (several panels). Use `defaultOpen` with a string or string array. Animate panels with `@alpinejs/collapse` (`x-show` + `x-collapse` on an outer wrapper; put padding on an inner element).
121
+ ## Default open
122
+
123
+ Pass `defaultOpen` when registering. Panels open automatically when their item is registered.
124
+
125
+ ```js
126
+ // Single — one panel open on init
127
+ $store.accordion.register("faq", {
128
+ mode: "single",
129
+ defaultOpen: "item-2",
130
+ });
131
+
132
+ // Multiple — several panels open on init
133
+ $store.accordion.register("settings", {
134
+ mode: "multiple",
135
+ defaultOpen: ["notifications", "integrations"],
136
+ });
137
+
138
+ ["item-1", "item-2", "item-3"].forEach((id) => $store.accordion.registerItem("faq", id));
139
+ ```
140
+
141
+ Read open ids reactively:
142
+
143
+ ```html
144
+ <span x-text="$store.accordion.openIds('faq').join(', ')"></span>
145
+ ```
146
+
147
+ ## Panel animation
148
+
149
+ `@ailuracode/alpine-accordion` is headless — it does not animate panels. Use the official [`@alpinejs/collapse`](https://alpinejs.dev/plugins/collapse) plugin: `x-collapse` must sit on the **same element** as `x-show`.
150
+
151
+ ```bash
152
+ pnpm add @alpinejs/collapse
153
+ ```
154
+
155
+ ```js
156
+ import collapse from "@alpinejs/collapse";
157
+
158
+ Alpine.plugin(collapse);
159
+ ```
160
+
161
+ ```html
162
+ <div
163
+ x-show="$store.accordion.isOpen('faq', id)"
164
+ x-collapse
165
+ x-bind="$store.accordion.panelProps('faq', id)"
166
+ class="overflow-hidden"
167
+ >
168
+ <div class="px-4 py-3">Panel content</div>
169
+ </div>
170
+ ```
171
+
172
+ Put padding on an **inner** wrapper — vertical padding on the same node as `x-collapse` prevents height from reaching `0` and the close animation can appear stuck.
173
+
174
+ Optional modifiers: `x-collapse.duration.300ms`, `x-collapse.min.50px`. See the [Collapse plugin docs](https://alpinejs.dev/plugins/collapse).
175
+
176
+ `panelProps()` does not set `hidden` — visibility is driven by `x-show` on the client.
177
+
178
+ ## SSR
179
+
180
+ Safe when panels start collapsed; visibility is controlled with `x-show` on the client. Use `defaultOpen` only when markup is hydrated on the client.
181
+
182
+ ## License
183
+
184
+ MIT
package/dist/index.d.ts CHANGED
@@ -62,7 +62,27 @@ interface AccordionStore {
62
62
  /** Options accepted by the accordion plugin factory. */
63
63
  interface CreateAccordionOptions {
64
64
  readonly id?: string;
65
+ /**
66
+ * `$store` key the Alpine plugin registers under. Defaults to
67
+ * {@link DEFAULT_ACCORDION_STORE_KEY}. Set when the host already
68
+ * owns an `accordion` store or another toolkit plugin would
69
+ * collide on that name — the rename avoids the collision without
70
+ * touching the controller. Ignored by the standalone
71
+ * `createAccordionController` factory.
72
+ */
73
+ readonly storeKey?: string;
74
+ /**
75
+ * `$accordion` magic key the Alpine plugin registers under.
76
+ * Defaults to {@link DEFAULT_ACCORDION_MAGIC_KEY}, or to `storeKey`
77
+ * when that is renamed (the magic follows the store so consumers
78
+ * only rename one). Ignored by the standalone factory.
79
+ */
80
+ readonly magicKey?: string;
65
81
  }
82
+ /** Default `$store` key registered by {@link accordionPlugin}. */
83
+ declare const DEFAULT_ACCORDION_STORE_KEY = "accordion";
84
+ /** Default `$accordion` magic key registered by {@link accordionPlugin}. */
85
+ declare const DEFAULT_ACCORDION_MAGIC_KEY = "accordion";
66
86
  /** Typed view of `Alpine` the accordion plugin uses internally. */
67
87
  type AccordionAlpine = Alpine<{
68
88
  accordion: AccordionStore;
@@ -92,6 +112,11 @@ interface AccordionEvents extends Record<string, unknown> {
92
112
  *
93
113
  * Emits a typed `change` event on every open/close transition so
94
114
  * consumers can react programmatically.
115
+ *
116
+ * Selection state is a plain per-group object — no controller class,
117
+ * no event emission. The full `@ailuracode/alpine-selection`
118
+ * dependency was dropped in favour of this lightweight state to keep
119
+ * the bundle slim.
95
120
  */
96
121
 
97
122
  /**
@@ -101,7 +126,13 @@ interface AccordionEvents extends Record<string, unknown> {
101
126
  declare class AccordionController extends BaseController<AccordionEvents> {
102
127
  #private;
103
128
  constructor(id?: string);
104
- get groups(): Readonly<Record<string, AccordionGroup>>;
129
+ /** Whether a group is registered. */
130
+ hasGroup(accordionId: string): boolean;
131
+ /**
132
+ * Returns shallow snapshots of all groups for adapter sync.
133
+ * Mutating the returned objects does not affect controller state.
134
+ */
135
+ snapshotGroups(): Record<string, AccordionGroup>;
105
136
  register(accordionId: string, options?: AccordionGroupOptions): void;
106
137
  unregister(accordionId: string): void;
107
138
  registerItem(accordionId: string, itemId: string, disabled?: boolean): void;
@@ -118,7 +149,9 @@ declare class AccordionController extends BaseController<AccordionEvents> {
118
149
  panelProps(accordionId: string, itemId: string): Record<string, string | boolean | undefined>;
119
150
  /**
120
151
  * Returns a store-shaped object for Alpine's `$store.accordion`.
121
- * The store delegates to this controller.
152
+ * Prefer {@link createAccordionStore} for new code.
153
+ *
154
+ * @deprecated Use `createAccordionStore()` or `createAccordionStoreFromController()`.
122
155
  */
123
156
  toStore(): AccordionStore;
124
157
  }
@@ -127,11 +160,6 @@ declare class AccordionController extends BaseController<AccordionEvents> {
127
160
  * Convenience for non-Alpine consumers.
128
161
  */
129
162
  declare function createAccordionController(id?: string): AccordionController;
130
- /**
131
- * Creates an AccordionStore (store-shaped object) directly.
132
- * Backward-compatible alias.
133
- */
134
- declare function createAccordionStore(): AccordionStore;
135
163
 
136
164
  /**
137
165
  * Alpine.js integration for `@ailuracode/alpine-accordion`.
@@ -149,4 +177,13 @@ declare function accordionPlugin(options?: CreateAccordionOptions): AccordionPlu
149
177
  /** Builds typed accordion plugin options. */
150
178
  declare function accordionOptions<const T extends CreateAccordionOptions>(options: T): T;
151
179
 
152
- export { type AccordionAlpine, type AccordionChangeDetail, type AccordionChangeSource, AccordionController, type AccordionEvents, type AccordionGroup, type AccordionGroupOptions, type AccordionItem, type AccordionMode, type AccordionPluginCallback, type AccordionStore, type CreateAccordionOptions, accordionOptions, accordionPlugin, createAccordionController, createAccordionStore, accordionPlugin as default };
180
+ /**
181
+ * Store factory for `@ailuracode/alpine-accordion`.
182
+ */
183
+
184
+ /** Builds an {@link AccordionStore} backed by a new {@link AccordionController}. */
185
+ declare function createAccordionStore(): AccordionStore;
186
+ /** Builds an {@link AccordionStore} that mirrors the given controller. */
187
+ declare function createAccordionStoreFromController(controller: AccordionController): AccordionStore;
188
+
189
+ export { type AccordionAlpine, type AccordionChangeDetail, type AccordionChangeSource, AccordionController, type AccordionEvents, type AccordionGroup, type AccordionGroupOptions, type AccordionItem, type AccordionMode, type AccordionPluginCallback, type AccordionStore, type CreateAccordionOptions, DEFAULT_ACCORDION_MAGIC_KEY, DEFAULT_ACCORDION_STORE_KEY, accordionOptions, accordionPlugin, createAccordionController, createAccordionStore, createAccordionStoreFromController, accordionPlugin as default };
package/dist/index.js CHANGED
@@ -1,318 +1 @@
1
- // src/controller.ts
2
- import { BaseController, generateId } from "@ailuracode/alpine-core";
3
- function enabledItems(group) {
4
- return group.items.filter((item) => !item.disabled);
5
- }
6
- function itemIndex(group, itemId) {
7
- return enabledItems(group).findIndex((item) => item.id === itemId);
8
- }
9
- function normalizeDefaultOpen(mode, value) {
10
- if (!value) {
11
- return [];
12
- }
13
- const ids = Array.isArray(value) ? value : [value];
14
- return mode === "single" && ids.length > 0 ? [ids[0]] : ids;
15
- }
16
- function createGroup(options = {}) {
17
- const mode = options.mode ?? "single";
18
- return {
19
- mode,
20
- open: {},
21
- activeItemId: null,
22
- items: [],
23
- defaultOpen: normalizeDefaultOpen(mode, options.defaultOpen),
24
- onChange: options.onChange
25
- };
26
- }
27
- var AccordionController = class extends BaseController {
28
- #groups = {};
29
- constructor(id) {
30
- super(id ?? generateId("accordion"));
31
- }
32
- get groups() {
33
- return this.#groups;
34
- }
35
- register(accordionId, options = {}) {
36
- if (this.isDestroyed) {
37
- return;
38
- }
39
- this.#groups[accordionId] = createGroup(options);
40
- }
41
- unregister(accordionId) {
42
- if (this.isDestroyed) {
43
- return;
44
- }
45
- delete this.#groups[accordionId];
46
- }
47
- registerItem(accordionId, itemId, disabled = false) {
48
- if (this.isDestroyed) {
49
- return;
50
- }
51
- const group = this.#getOrCreate(accordionId);
52
- const existing = group.items.find((item) => item.id === itemId);
53
- if (existing) {
54
- existing.disabled = disabled;
55
- return;
56
- }
57
- group.items.push({ id: itemId, disabled });
58
- if (group.defaultOpen.includes(itemId) && !disabled) {
59
- this.open(accordionId, itemId);
60
- group.activeItemId ??= itemId;
61
- }
62
- }
63
- unregisterItem(accordionId, itemId) {
64
- if (this.isDestroyed) {
65
- return;
66
- }
67
- const group = this.#groups[accordionId];
68
- if (!group) {
69
- return;
70
- }
71
- group.items = group.items.filter((item) => item.id !== itemId);
72
- delete group.open[itemId];
73
- }
74
- open(accordionId, itemId) {
75
- if (this.isDestroyed) {
76
- return;
77
- }
78
- const group = this.#groups[accordionId];
79
- const item = group?.items.find((entry) => entry.id === itemId);
80
- if (!(group && item) || item.disabled) {
81
- return;
82
- }
83
- if (group.mode === "single") {
84
- for (const id of Object.keys(group.open)) {
85
- if (group.open[id]) {
86
- group.open[id] = false;
87
- }
88
- }
89
- group.open[itemId] = true;
90
- } else {
91
- group.open[itemId] = true;
92
- }
93
- this.#emitChange(accordionId, "user");
94
- group.onChange?.(this.openIds(accordionId));
95
- }
96
- close(accordionId, itemId) {
97
- if (this.isDestroyed) {
98
- return;
99
- }
100
- const group = this.#groups[accordionId];
101
- if (!group?.open[itemId]) {
102
- return;
103
- }
104
- group.open[itemId] = false;
105
- this.#emitChange(accordionId, "user");
106
- group.onChange?.(this.openIds(accordionId));
107
- }
108
- toggle(accordionId, itemId) {
109
- if (this.isOpen(accordionId, itemId)) {
110
- this.close(accordionId, itemId);
111
- } else {
112
- this.open(accordionId, itemId);
113
- }
114
- }
115
- isOpen(accordionId, itemId) {
116
- return this.#groups[accordionId]?.open[itemId] ?? false;
117
- }
118
- openIds(accordionId) {
119
- const open = this.#groups[accordionId]?.open ?? {};
120
- return Object.entries(open).filter(([, isOpen]) => isOpen).map(([id]) => id);
121
- }
122
- activeItem(accordionId) {
123
- return this.#groups[accordionId]?.activeItemId ?? null;
124
- }
125
- setActiveItem(accordionId, itemId) {
126
- if (this.isDestroyed) {
127
- return;
128
- }
129
- const group = this.#groups[accordionId];
130
- if (!group) {
131
- return;
132
- }
133
- if (itemId === null) {
134
- group.activeItemId = null;
135
- return;
136
- }
137
- const item = group.items.find((entry) => entry.id === itemId);
138
- if (item && !item.disabled) {
139
- group.activeItemId = itemId;
140
- }
141
- }
142
- handleKeydown(accordionId, event) {
143
- const group = this.#groups[accordionId];
144
- if (!group) {
145
- return;
146
- }
147
- const items = enabledItems(group);
148
- if (items.length === 0) {
149
- return;
150
- }
151
- if (!group.activeItemId) {
152
- group.activeItemId = items[0]?.id ?? null;
153
- }
154
- const currentIndex = group.activeItemId ? itemIndex(group, group.activeItemId) : 0;
155
- switch (event.key) {
156
- case "ArrowDown":
157
- event.preventDefault();
158
- group.activeItemId = items[(currentIndex + 1) % items.length]?.id ?? null;
159
- break;
160
- case "ArrowUp":
161
- event.preventDefault();
162
- group.activeItemId = items[(currentIndex - 1 + items.length) % items.length]?.id ?? null;
163
- break;
164
- case "Home":
165
- event.preventDefault();
166
- group.activeItemId = items[0]?.id ?? null;
167
- break;
168
- case "End":
169
- event.preventDefault();
170
- group.activeItemId = items[items.length - 1]?.id ?? null;
171
- break;
172
- default:
173
- break;
174
- }
175
- }
176
- triggerProps(accordionId, itemId) {
177
- const open = this.isOpen(accordionId, itemId);
178
- const active = this.activeItem(accordionId) === itemId;
179
- return {
180
- "aria-expanded": open,
181
- "aria-controls": `${accordionId}-panel-${itemId}`,
182
- id: `${accordionId}-trigger-${itemId}`,
183
- tabindex: active ? 0 : -1
184
- };
185
- }
186
- panelProps(accordionId, itemId) {
187
- const open = this.isOpen(accordionId, itemId);
188
- return {
189
- id: `${accordionId}-panel-${itemId}`,
190
- role: "region",
191
- "aria-labelledby": `${accordionId}-trigger-${itemId}`,
192
- "aria-hidden": open ? void 0 : true
193
- };
194
- }
195
- /**
196
- * Returns a store-shaped object for Alpine's `$store.accordion`.
197
- * The store delegates to this controller.
198
- */
199
- toStore() {
200
- return {
201
- groups: this.#groups,
202
- register: (id, opts) => this.register(id, opts),
203
- unregister: (id) => this.unregister(id),
204
- registerItem: (id, itemId, disabled) => this.registerItem(id, itemId, disabled),
205
- unregisterItem: (id, itemId) => this.unregisterItem(id, itemId),
206
- open: (id, itemId) => this.open(id, itemId),
207
- close: (id, itemId) => this.close(id, itemId),
208
- toggle: (id, itemId) => this.toggle(id, itemId),
209
- isOpen: (id, itemId) => this.isOpen(id, itemId),
210
- openIds: (id) => this.openIds(id),
211
- activeItem: (id) => this.activeItem(id),
212
- setActiveItem: (id, itemId) => this.setActiveItem(id, itemId),
213
- handleKeydown: (id, event) => this.handleKeydown(id, event),
214
- triggerProps: (id, itemId) => this.triggerProps(id, itemId),
215
- panelProps: (id, itemId) => this.panelProps(id, itemId),
216
- destroy: () => this.destroy()
217
- };
218
- }
219
- #getOrCreate(accordionId) {
220
- this.#groups[accordionId] ??= createGroup();
221
- return this.#groups[accordionId];
222
- }
223
- #emitChange(accordionId, source) {
224
- this.emit("change", {
225
- groupId: accordionId,
226
- openIds: this.openIds(accordionId),
227
- source
228
- });
229
- }
230
- };
231
- function createAccordionController(id) {
232
- return new AccordionController(id);
233
- }
234
- function createAccordionStore() {
235
- return new AccordionController().toStore();
236
- }
237
-
238
- // src/plugin.ts
239
- var ACCORDION_STORE_KEY = "accordion";
240
- function accordionPlugin(options = {}) {
241
- return function registerAccordion(alpine) {
242
- const Alpine = alpine;
243
- const controller = new AccordionController(options.id);
244
- const store = {
245
- groups: {},
246
- register: (id, opts) => controller.register(id, opts),
247
- unregister: (id) => controller.unregister(id),
248
- registerItem: (id, itemId, disabled) => controller.registerItem(id, itemId, disabled),
249
- unregisterItem: (id, itemId) => controller.unregisterItem(id, itemId),
250
- open: (id, itemId) => controller.open(id, itemId),
251
- close: (id, itemId) => controller.close(id, itemId),
252
- toggle: (id, itemId) => controller.toggle(id, itemId),
253
- isOpen: () => false,
254
- openIds: () => [],
255
- activeItem: () => null,
256
- setActiveItem: (id, itemId) => controller.setActiveItem(id, itemId),
257
- handleKeydown: (id, event) => controller.handleKeydown(id, event),
258
- triggerProps: () => ({}),
259
- panelProps: () => ({}),
260
- destroy: () => controller.destroy()
261
- };
262
- Alpine.store(ACCORDION_STORE_KEY, store);
263
- const reactive = () => Alpine.store(ACCORDION_STORE_KEY);
264
- store.isOpen = (id, itemId) => reactive().groups[id]?.open[itemId] ?? false;
265
- store.openIds = (id) => {
266
- const open = reactive().groups[id]?.open ?? {};
267
- return Object.entries(open).filter(([, isOpen]) => isOpen).map(([gid]) => gid);
268
- };
269
- store.activeItem = (id) => reactive().groups[id]?.activeItemId ?? null;
270
- store.triggerProps = (id, itemId) => {
271
- const r = reactive();
272
- const open = r.isOpen(id, itemId);
273
- const active = r.activeItem(id) === itemId;
274
- return {
275
- "aria-expanded": open,
276
- "aria-controls": `${id}-panel-${itemId}`,
277
- id: `${id}-trigger-${itemId}`,
278
- tabindex: active ? 0 : -1
279
- };
280
- };
281
- store.panelProps = (id, itemId) => {
282
- const open = reactive().isOpen(id, itemId);
283
- return {
284
- id: `${id}-panel-${itemId}`,
285
- role: "region",
286
- "aria-labelledby": `${id}-trigger-${itemId}`,
287
- "aria-hidden": open ? void 0 : true
288
- };
289
- };
290
- controller.on("change", () => {
291
- const r = reactive();
292
- const controllerGroups = controller.groups;
293
- for (const key of Object.keys(controllerGroups)) {
294
- r.groups[key] = controllerGroups[key];
295
- }
296
- for (const key of Object.keys(r.groups)) {
297
- if (!(key in controllerGroups)) {
298
- delete r.groups[key];
299
- }
300
- }
301
- });
302
- Alpine.magic(ACCORDION_STORE_KEY, () => reactive());
303
- if (typeof Alpine.cleanup === "function") {
304
- Alpine.cleanup(() => controller.destroy());
305
- }
306
- };
307
- }
308
- function accordionOptions(options) {
309
- return options;
310
- }
311
- export {
312
- AccordionController,
313
- accordionOptions,
314
- accordionPlugin,
315
- createAccordionController,
316
- createAccordionStore,
317
- accordionPlugin as default
318
- };
1
+ import{BaseController as C,generateId as I}from"@ailuracode/alpine-core";function K(r,e){return r==="single"?{mode:r,value:typeof e=="string"?e:null,selectedKeys:[],keys:[],disabledKeys:new Set}:{mode:r,value:null,selectedKeys:Array.isArray(e)?[...e]:[],keys:[],disabledKeys:new Set}}function x(r,e,n){r.keys=e,r.disabledKeys=new Set(n),r.value!==null&&(!r.keys.includes(r.value)||r.disabledKeys.has(r.value))&&(r.value=null),r.selectedKeys.some(o=>!r.keys.includes(o)||r.disabledKeys.has(o))&&(r.selectedKeys=r.selectedKeys.filter(o=>r.keys.includes(o)&&!r.disabledKeys.has(o)))}function h(r){return r.items.filter(e=>!e.disabled)}function G(r,e){return h(r).findIndex(n=>n.id===e)}function S(r,e){if(!e)return[];let n=Array.isArray(e)?e:[e];return r==="single"&&n.length>0?[n[0]]:n}function y(r={}){let e=r.mode??"single";return{mode:e,open:{},activeItemId:null,items:[],defaultOpen:S(e,r.defaultOpen),onChange:r.onChange}}function k(r){return{...r,open:{...r.open},items:r.items.map(e=>({...e})),defaultOpen:[...r.defaultOpen]}}function g(r){return r.mode==="single"?r.value===null?[]:[r.value]:r.selectedKeys}function a(r,e){let n=new Set(g(e)),o={};for(let t of r.items)o[t.id]=n.has(t.id);r.open=o}var p=class extends C{#e={};#o={};constructor(e){super(e??I("accordion"))}hasGroup(e){return e in this.#e}snapshotGroups(){let e={};for(let[n,o]of Object.entries(this.#e))e[n]=k(o);return e}register(e,n={}){if(this.isDestroyed)return;let o=y(n);this.#e[e]=o;let t=o.mode==="single"?o.defaultOpen[0]??null:o.defaultOpen,i=K(o.mode,t);this.#o[e]=i,this.#r(e),a(o,i),this.#n(e,"initialization")}unregister(e){this.isDestroyed||(this.#n(e,"initialization"),delete this.#o[e],delete this.#e[e])}registerItem(e,n,o=!1){if(this.isDestroyed)return;let t=this.#t(e),i=t.items.find(d=>d.id===n);if(i){i.disabled=o,this.#n(e,"user");return}if(t.items.push({id:n,disabled:o}),this.#r(e),t.defaultOpen.includes(n)&&!o){this.open(e,n),t.activeItemId??=n;return}this.#n(e,"user")}unregisterItem(e,n){if(this.isDestroyed)return;let o=this.#e[e],t=this.#o[e];o&&t&&(o.items=o.items.filter(i=>i.id!==n),this.#r(e),a(o,t),this.#n(e,"user"))}open(e,n){if(this.isDestroyed)return;let o=this.#e[e],t=this.#o[e],i=o?.items.find(d=>d.id===n);!(o&&t&&i)||i.disabled||(o.mode==="single"?(t.mode="single",t.value=n,t.selectedKeys=[]):g(t).includes(n)||(t.mode="multiple",t.value=null,t.selectedKeys=[...g(t),n]),a(o,t),this.#n(e,"user"),o.onChange?.(this.openIds(e)))}close(e,n){if(this.isDestroyed)return;let o=this.#e[e],t=this.#o[e];o&&t&&o.open[n]&&(o.mode==="single"?(t.value=null,t.selectedKeys=[]):t.selectedKeys=g(t).filter(i=>i!==n),a(o,t),this.#n(e,"user"),o.onChange?.(this.openIds(e)))}toggle(e,n){this.#e[e]&&(this.isOpen(e,n)?this.close(e,n):this.open(e,n))}isOpen(e,n){let o=this.#o[e];return o?o.mode==="single"?o.value===n:o.selectedKeys.includes(n):!1}openIds(e){let n=this.#o[e];return n?[...g(n)]:[]}activeItem(e){return this.#e[e]?.activeItemId??null}setActiveItem(e,n){if(this.isDestroyed)return;let o=this.#e[e];if(!o)return;if(n===null){o.activeItemId=null,this.#n(e,"user");return}let t=o.items.find(i=>i.id===n);t&&!t.disabled&&(o.activeItemId=n,this.#n(e,"user"))}handleKeydown(e,n){let o=this.#e[e];if(!o)return;let t=h(o);if(t.length===0)return;o.activeItemId||(o.activeItemId=t[0]?.id??null);let i=o.activeItemId?G(o,o.activeItemId):0;switch(n.key){case"ArrowDown":n.preventDefault(),o.activeItemId=t[(i+1)%t.length]?.id??null;break;case"ArrowUp":n.preventDefault(),o.activeItemId=t[(i-1+t.length)%t.length]?.id??null;break;case"Home":n.preventDefault(),o.activeItemId=t[0]?.id??null;break;case"End":n.preventDefault(),o.activeItemId=t[t.length-1]?.id??null;break;default:break}this.#n(e,"user")}triggerProps(e,n){let o=this.isOpen(e,n),t=this.activeItem(e)===n;return{"aria-expanded":o,"aria-controls":`${e}-panel-${n}`,id:`${e}-trigger-${n}`,tabindex:t?0:-1}}panelProps(e,n){let o=this.isOpen(e,n);return{id:`${e}-panel-${n}`,role:"region","aria-labelledby":`${e}-trigger-${n}`,"aria-hidden":o?void 0:!0}}toStore(){return{groups:this.#e,register:(e,n)=>this.register(e,n),unregister:e=>this.unregister(e),registerItem:(e,n,o)=>this.registerItem(e,n,o),unregisterItem:(e,n)=>this.unregisterItem(e,n),open:(e,n)=>this.open(e,n),close:(e,n)=>this.close(e,n),toggle:(e,n)=>this.toggle(e,n),isOpen:(e,n)=>this.isOpen(e,n),openIds:e=>this.openIds(e),activeItem:e=>this.activeItem(e),setActiveItem:(e,n)=>this.setActiveItem(e,n),handleKeydown:(e,n)=>this.handleKeydown(e,n),triggerProps:(e,n)=>this.triggerProps(e,n),panelProps:(e,n)=>this.panelProps(e,n),destroy:()=>this.destroy()}}#t(e){return this.#e[e]??=y(),this.#e[e]}#n(e,n){this.emit("change",{groupId:e,openIds:this.openIds(e),source:n})}#r(e){let n=this.#e[e],o=this.#o[e];if(!(n&&o))return;let t=n.items.map(d=>d.id),i=n.items.filter(d=>d.disabled).map(d=>d.id);x(o,t,i)}};function D(r){return new p(r)}import{bridgeControllerStore as R}from"@ailuracode/alpine-core";var A="accordion",f="accordion";function m(r={}){let e=r.storeKey??A,n=r.magicKey??r.storeKey??f;return function(t){let i=t,d=new p(r.id),v={...d.toStore(),groups:{}};R({alpine:i,storeKey:e,magicKey:n,store:v,controller:d,packageName:"accordion",subscribe:l=>(l.isOpen=(s,c)=>l.groups[s]?.open[c]??!1,l.openIds=s=>{let c=l.groups[s]?.open??{};return Object.entries(c).filter(([,u])=>u).map(([u])=>u)},l.activeItem=s=>l.groups[s]?.activeItemId??null,l.triggerProps=(s,c)=>{let u=l.isOpen(s,c),O=l.activeItem(s)===c;return{"aria-expanded":u,"aria-controls":`${s}-panel-${c}`,id:`${s}-trigger-${c}`,tabindex:O?0:-1}},l.panelProps=(s,c)=>{let u=l.isOpen(s,c);return{id:`${s}-panel-${c}`,role:"region","aria-labelledby":`${s}-trigger-${c}`,"aria-hidden":u?void 0:!0}},d.on("change",()=>{let s=d.snapshotGroups();for(let c of Object.keys(s))l.groups[c]=s[c];for(let c of Object.keys(l.groups))c in s||delete l.groups[c]}))})}}function E(r){return r}function P(r,e){for(let n of Object.keys(e))r[n]=e[n];for(let n of Object.keys(r))n in e||delete r[n]}function _(){return b(new p)}function b(r){let e={},n={...r.toStore(),groups:e};return r.on("change",()=>{P(e,r.snapshotGroups())}),n}export{p as AccordionController,f as DEFAULT_ACCORDION_MAGIC_KEY,A as DEFAULT_ACCORDION_STORE_KEY,E as accordionOptions,m as accordionPlugin,D as createAccordionController,_ as createAccordionStore,b as createAccordionStoreFromController,m as default};
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "@ailuracode/alpine-accordion",
3
- "version": "1.0.0",
3
+ "version": "2.0.0",
4
4
  "description": "Alpine.js headless accessible accordion store",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "author": "ailuracode",
8
+ "sideEffects": false,
8
9
  "publishConfig": {
9
10
  "access": "public"
10
11
  },
@@ -32,10 +33,12 @@
32
33
  },
33
34
  "types": "./dist/global.d.ts",
34
35
  "devDependencies": {
35
- "@ailuracode/alpine-core": "0.2.0"
36
+ "@types/alpinejs": "^3.13.11",
37
+ "alpinejs": "^3.15.12",
38
+ "@ailuracode/alpine-core": "0.3.0"
36
39
  },
37
40
  "peerDependencies": {
38
- "@ailuracode/alpine-core": "^0.2.0",
41
+ "@ailuracode/alpine-core": "^0.3.0",
39
42
  "alpinejs": "^3.0.0",
40
43
  "@types/alpinejs": "^3.13.11"
41
44
  },
@@ -52,8 +55,30 @@
52
55
  "accessibility",
53
56
  "headless"
54
57
  ],
58
+ "toolkit": {
59
+ "bundleBudget": {
60
+ "category": "small-feature"
61
+ }
62
+ },
63
+ "size-limit": [
64
+ {
65
+ "name": "full surface",
66
+ "path": "dist/index.js",
67
+ "import": "*",
68
+ "ignore": [
69
+ "alpinejs",
70
+ "@ailuracode/alpine-core",
71
+ "@types/alpinejs"
72
+ ],
73
+ "gzip": true,
74
+ "brotli": true,
75
+ "limit": "2.3 kB"
76
+ }
77
+ ],
55
78
  "scripts": {
56
- "build": "tsup src/index.ts --format esm --dts --clean --out-dir dist && cp src/global.d.ts dist/global.d.ts",
57
- "test": "vitest run --config ../../vitest.config.ts test"
79
+ "build": "tsup src/index.ts --format esm --dts --clean --out-dir dist --minify && cp src/global.d.ts dist/global.d.ts",
80
+ "size": "size-limit",
81
+ "test": "vitest run --config ../../vitest.config.ts packages/accordion",
82
+ "test:e2e": "playwright test --config playwright.config.ts"
58
83
  }
59
84
  }