accordionary 1.0.0 → 1.0.2

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
@@ -11,6 +11,7 @@ A lightweight, accessible, vanilla JavaScript accordion with zero dependencies.
11
11
  - Respects `prefers-reduced-motion`
12
12
  - Configurable via HTML attributes
13
13
  - Programmatic API for full control
14
+ - Generate accordions from JSON data (package manager only)
14
15
  - TypeScript support
15
16
  - ~4KB minified
16
17
 
@@ -27,7 +28,7 @@ Download `dist/accordionary.js` and include it in your HTML:
27
28
  Or link directly from GitHub (replace `v1.0.0` with the desired version):
28
29
 
29
30
  ```html
30
- <script src="https://cdn.jsdelivr.net/gh/briantucker/accordionary@v1.0.0/dist/accordionary.js"></script>
31
+ <script src="https://cdn.jsdelivr.net/npm/accordionary@1.0.0/dist/accordionary.js"></script>
31
32
  ```
32
33
 
33
34
  This version auto-initializes all accordions on page load.
@@ -127,6 +128,7 @@ All configuration is done via HTML attributes. No JavaScript required.
127
128
  | `accordionary-multiple` | `true`, `false` | `true` | Allow multiple items open at once |
128
129
  | `accordionary-speed` | number (ms) | `300` | Animation duration in milliseconds |
129
130
  | `accordionary-easing` | CSS easing | `ease` | Animation easing function |
131
+ | `accordionary-link` | `true`, `false` | `false` | Link all items: open/close together |
130
132
 
131
133
  ### Item-Level Attributes
132
134
 
@@ -165,6 +167,16 @@ All configuration is done via HTML attributes. No JavaScript required.
165
167
  </div>
166
168
  ```
167
169
 
170
+ ### Linked Items (Open/Close Together)
171
+
172
+ ```html
173
+ <div accordionary="component" accordionary-link="true">
174
+ <!-- Clicking any item open opens all; clicking any item closed closes all -->
175
+ </div>
176
+ ```
177
+
178
+ Disabled items are excluded from linking — they remain in their current state.
179
+
168
180
  ### Force Specific Item Open
169
181
 
170
182
  ```html
@@ -259,6 +271,162 @@ const accordion: AccordionController | null =
259
271
  Accordionary.init("#my-accordion");
260
272
  ```
261
273
 
274
+ ## Generating Accordions from JSON
275
+
276
+ When installed via package manager, you can generate accordion HTML from structured JSON data using the `generateAccordionary` function.
277
+
278
+ ### Basic Usage
279
+
280
+ ```typescript
281
+ import { generateAccordionary } from "accordionary";
282
+
283
+ const data = {
284
+ items: [
285
+ {
286
+ heading: "Question 1",
287
+ content: "Answer 1",
288
+ },
289
+ {
290
+ heading: "Question 2",
291
+ content: "Answer 2",
292
+ },
293
+ ],
294
+ };
295
+
296
+ // Generate the accordion element
297
+ const element = generateAccordionary(data);
298
+
299
+ // Append to DOM
300
+ document.body.appendChild(element);
301
+
302
+ // Initialize it
303
+ const accordion = Accordionary.init(element);
304
+ ```
305
+
306
+ ### Configuration Options
307
+
308
+ ```typescript
309
+ const element = generateAccordionary(data, {
310
+ icon: "▼", // HTML string for icon (default: "▼")
311
+ openDefault: "none", // "all" | "first" | "none" (default: "none")
312
+ allowMultiple: true, // Allow multiple items open (default: true)
313
+ speed: 300, // Animation duration in ms (default: 300)
314
+ easing: "ease", // CSS easing function (default: "ease")
315
+ linked: false, // Link all items open/close together (default: false)
316
+ classes: {
317
+ component: ["my-accordion"], // Custom classes for component
318
+ item: ["my-item"], // Custom classes for items
319
+ heading: ["my-heading"], // Custom classes for headings
320
+ content: ["my-content"], // Custom classes for content
321
+ icon: ["my-icon"], // Custom classes for icons
322
+ },
323
+ });
324
+ ```
325
+
326
+ ### Custom Icon
327
+
328
+ You can provide any HTML string for the icon - emoji, SVG, or image tag:
329
+
330
+ ```typescript
331
+ // Emoji
332
+ const element = generateAccordionary(data, {
333
+ icon: "👇",
334
+ });
335
+
336
+ // SVG
337
+ const element = generateAccordionary(data, {
338
+ icon: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
339
+ <path fill="currentColor" d="M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6l-6-6z"/>
340
+ </svg>`,
341
+ });
342
+
343
+ // Image
344
+ const element = generateAccordionary(data, {
345
+ icon: '<img src="/chevron.svg" alt="">',
346
+ });
347
+ ```
348
+
349
+ ### Item-Level Configuration
350
+
351
+ Individual items can have their own configuration:
352
+
353
+ ```typescript
354
+ const data = {
355
+ items: [
356
+ {
357
+ heading: "Normal Item",
358
+ content: "This item follows component defaults",
359
+ },
360
+ {
361
+ heading: "Force Open Item",
362
+ content: "This item starts open regardless of component settings",
363
+ config: {
364
+ openOverride: true,
365
+ },
366
+ },
367
+ {
368
+ heading: "Disabled Item",
369
+ content: "This item cannot be toggled and is always visible",
370
+ config: {
371
+ disabled: true,
372
+ },
373
+ },
374
+ ],
375
+ };
376
+ ```
377
+
378
+ ### HTML in Content
379
+
380
+ Both `heading` and `content` accept HTML strings:
381
+
382
+ ```typescript
383
+ const data = {
384
+ items: [
385
+ {
386
+ heading: '<div class="font-bold">Rich <em>Heading</em></div>',
387
+ content: `
388
+ <img src="/image.jpg" alt="Description">
389
+ <p>Paragraph with <strong>bold text</strong></p>
390
+ <ul>
391
+ <li>List item 1</li>
392
+ <li>List item 2</li>
393
+ </ul>
394
+ `,
395
+ },
396
+ ],
397
+ };
398
+ ```
399
+
400
+ ### TypeScript Support
401
+
402
+ Full type definitions are included:
403
+
404
+ ```typescript
405
+ import {
406
+ generateAccordionary,
407
+ type AccordionData,
408
+ type GeneratorConfig,
409
+ } from "accordionary";
410
+
411
+ const data: AccordionData = {
412
+ items: [
413
+ {
414
+ heading: "Question",
415
+ content: "Answer",
416
+ },
417
+ ],
418
+ };
419
+
420
+ const config: GeneratorConfig = {
421
+ openDefault: "first",
422
+ classes: {
423
+ component: ["custom-accordion"],
424
+ },
425
+ };
426
+
427
+ const element = generateAccordionary(data, config);
428
+ ```
429
+
262
430
  ## Accessibility
263
431
 
264
432
  Accordionary is built with accessibility in mind:
@@ -1 +1 @@
1
- var{defineProperty:F,getOwnPropertyNames:I,getOwnPropertyDescriptor:O}=Object,f=Object.prototype.hasOwnProperty;var S=new WeakMap,u=(j)=>{var k=S.get(j),J;if(k)return k;if(k=F({},"__esModule",{value:!0}),j&&typeof j==="object"||typeof j==="function")I(j).map((K)=>!f.call(k,K)&&F(k,K,{get:()=>j[K],enumerable:!(J=O(j,K))||J.enumerable}));return S.set(j,k),k};var A=(j,k)=>{for(var J in k)F(j,J,{get:k[J],enumerable:!0,configurable:!0,set:(K)=>k[J]=()=>K})};var d={};A(d,{initAll:()=>C,init:()=>v,default:()=>g});module.exports=u(d);var h=0;function w(j,k,J,K){let{headingElement:P,contentElement:G,iconElement:W,config:H}=j,_=`accordionary-${h++}`,z=`${_}-header`,q=`${_}-content`,U=!1;if(H.openOverride!==null)U=H.openOverride;else if(k.openDefault==="all")U=!0;else if(k.openDefault==="first"&&J)U=!0;let Q=U,{speed:$,easing:V,reduceMotion:B}=k,R=B?"none":`height ${$}ms ${V}`,x=B?"none":`transform ${$}ms ${V}`;if(G.style.overflow="hidden",G.style.transition=R,G.style.height=Q?"auto":"0px",W.style.transition=x,H.disabled)W.style.visibility="hidden";else if(Q)W.style.transform="rotate(180deg)";if(P.id=z,P.setAttribute("tabindex",H.disabled?"-1":"0"),P.setAttribute("role","button"),P.setAttribute("aria-expanded",Q?"true":"false"),P.setAttribute("aria-controls",q),P.style.cursor=H.disabled?"default":"pointer",H.disabled)P.setAttribute("aria-disabled","true");G.id=q,G.setAttribute("role","region"),G.setAttribute("aria-labelledby",z);function T(){if(Q)return;Q=!0,G.style.height=`${G.scrollHeight}px`,W.style.transform="rotate(180deg)",P.setAttribute("aria-expanded","true")}function y(){if(!Q)return;Q=!1,G.style.height=`${G.scrollHeight}px`,G.offsetHeight,G.style.height="0px",W.style.transform="rotate(0deg)",P.setAttribute("aria-expanded","false")}function b(){if(H.disabled)return;if(Q)y();else{if(!k.allowMultiple){for(let X of K)if(X!==j&&X.close)X.close()}T()}}j.open=T,j.close=y,j.toggle=b,P.addEventListener("click",b),P.addEventListener("keydown",(X)=>{if(X.key==="Enter"||X.key===" ")X.preventDefault(),b()}),G.addEventListener("transitionend",()=>{if(Q)G.style.height="auto"})}function Z(j,k){console.warn(`[Accordionary] ${j}`,k||"")}function L(j,k){console.error(`[Accordionary] ${j}`,k||"")}function Y(j,k){return j.getAttribute(`accordionary-${k}`)}function N(j,k=document){return k.querySelector(`[accordionary="${j}"]`)}function D(j,k=document){return k.querySelectorAll(`[accordionary="${j}"]`)}var E=window.matchMedia("(prefers-reduced-motion: reduce)").matches;function M(j){if(j.hasAttribute("data-accordionary-initialized"))return null;j.setAttribute("data-accordionary-initialized","true");let k=Y(j,"open"),J=Y(j,"multiple"),K=Y(j,"speed"),P=Y(j,"easing");if(k&&!["all","first","none"].includes(k))Z(`Invalid accordionary-open="${k}". Expected "all", "first", or "none". Defaulting to "none".`,j);if(J&&!["true","false"].includes(J))Z(`Invalid accordionary-multiple="${J}". Expected "true" or "false". Defaulting to "true".`,j);let G=K?parseInt(K,10):300;if(K&&(isNaN(G)||G<0))Z(`Invalid accordionary-speed="${K}". Expected a positive number in milliseconds. Defaulting to 300.`,j);let W={openDefault:k==="all"||k==="first"?k:"none",allowMultiple:J!=="false",speed:isNaN(G)||G<0?300:G,easing:P||"ease",reduceMotion:E},H=[],_=D("item",j);if(_.length===0)return Z('No items found. Add elements with accordionary="item" inside your component.',j),null;for(let q of _){let U=N("header",q),Q=N("content",q),$=N("icon",q);if(!U){L('Missing header element. Add accordionary="header" inside this item.',q);continue}if(!Q){L('Missing content element. Add accordionary="content" inside this item.',q);continue}if(!$){L('Missing icon element. Add accordionary="icon" inside your header.',q);continue}let V=Y(q,"open"),B=Y(q,"disable");if(V&&!["true","false"].includes(V))Z(`Invalid accordionary-open="${V}" on item. Expected "true" or "false".`,q);let R={openOverride:V==="true"?!0:V==="false"?!1:null,disabled:B==="true"};H.push({element:q,headingElement:U,contentElement:Q,iconElement:$,config:R})}H.forEach((q,U)=>{w(q,W,U===0,H)});let z=H.map((q)=>({element:q.element,open:()=>q.open?.(),close:()=>q.close?.(),toggle:()=>q.toggle?.()}));return{element:j,openAll:()=>{for(let q of H)q.open?.()},closeAll:()=>{for(let q of H)q.close?.()},open:(q)=>{H[q]?.open?.()},close:(q)=>{H[q]?.close?.()},toggle:(q)=>{H[q]?.toggle?.()},items:z}}function v(j){let k=typeof j==="string"?document.querySelector(j):j;if(!k)return console.error(`[Accordionary] Element not found: ${j}`),null;return M(k)}function C(){let j=D("component"),k=[];for(let J of j){let K=M(J);if(K)k.push(K)}return k}var g={init:v,initAll:C};
1
+ var{defineProperty:k,getOwnPropertyNames:I,getOwnPropertyDescriptor:u}=Object,f=Object.prototype.hasOwnProperty;var w=new WeakMap,A=(j)=>{var q=w.get(j),J;if(q)return q;if(q=k({},"__esModule",{value:!0}),j&&typeof j==="object"||typeof j==="function")I(j).map((K)=>!f.call(q,K)&&k(q,K,{get:()=>j[K],enumerable:!(J=u(j,K))||J.enumerable}));return w.set(j,q),q};var h=(j,q)=>{for(var J in q)k(j,J,{get:q[J],enumerable:!0,configurable:!0,set:(K)=>q[J]=()=>K})};var r={};h(r,{initAll:()=>O,init:()=>C,generateAccordionary:()=>D,default:()=>p});module.exports=A(r);var E=0;function x(j,q,J,K){let{headingElement:V,contentElement:H,iconElement:P,config:W}=j,Q=`accordionary-${E++}`,U=`${Q}-header`,z=`${Q}-content`,G=!1;if(W.openOverride!==null)G=W.openOverride;else if(q.openDefault==="all")G=!0;else if(q.openDefault==="first"&&J)G=!0;let X=G,{speed:B,easing:L,reduceMotion:Z}=q,T=Z?"none":`height ${B}ms ${L}`,b=Z?"none":`transform ${B}ms ${L}`;if(H.style.overflow="hidden",H.style.transition=T,H.style.height=X?"auto":"0px",!X)H.inert=!0;if(P.style.transition=b,W.disabled)P.style.visibility="hidden";else if(X)P.style.transform="rotate(180deg)";if(V.id=U,V.setAttribute("tabindex",W.disabled?"-1":"0"),V.setAttribute("role","button"),V.setAttribute("aria-expanded",X?"true":"false"),V.setAttribute("aria-controls",z),V.style.cursor=W.disabled?"default":"pointer",W.disabled)V.setAttribute("aria-disabled","true");H.id=z,H.setAttribute("role","region"),H.setAttribute("aria-labelledby",U);function y(){if(X)return;X=!0,H.inert=!1,H.style.height=`${H.scrollHeight}px`,P.style.transform="rotate(180deg)",V.setAttribute("aria-expanded","true")}function v(){if(!X)return;X=!1,H.inert=!0,H.style.height=`${H.scrollHeight}px`,H.offsetHeight,H.style.height="0px",P.style.transform="rotate(0deg)",V.setAttribute("aria-expanded","false")}function M(){if(W.disabled)return;if(q.linked){if(X){for(let Y of K)if(!Y.config.disabled)Y.close?.()}else for(let Y of K)if(!Y.config.disabled)Y.open?.()}else if(X)v();else{if(!q.allowMultiple){for(let Y of K)if(Y!==j&&Y.close)Y.close()}y()}}j.open=y,j.close=v,j.toggle=M,V.addEventListener("click",M),V.addEventListener("keydown",(Y)=>{if(Y.key==="Enter"||Y.key===" ")Y.preventDefault(),M()}),H.addEventListener("transitionend",()=>{if(X)H.style.height="auto"})}function $(j,q){console.warn(`[Accordionary] ${j}`,q||"")}function N(j,q){console.error(`[Accordionary] ${j}`,q||"")}function _(j,q){return j.getAttribute(`accordionary-${q}`)}function R(j,q=document){return q.querySelector(`[accordionary="${j}"]`)}function F(j,q=document){return q.querySelectorAll(`[accordionary="${j}"]`)}var g=window.matchMedia("(prefers-reduced-motion: reduce)").matches;function S(j){if(j.hasAttribute("data-accordionary-initialized"))return null;j.setAttribute("data-accordionary-initialized","true");let q=_(j,"open"),J=_(j,"multiple"),K=_(j,"speed"),V=_(j,"easing"),H=_(j,"link");if(q&&!["all","first","none"].includes(q))$(`Invalid accordionary-open="${q}". Expected "all", "first", or "none". Defaulting to "none".`,j);if(J&&!["true","false"].includes(J))$(`Invalid accordionary-multiple="${J}". Expected "true" or "false". Defaulting to "true".`,j);if(H&&!["true","false"].includes(H))$(`Invalid accordionary-link="${H}". Expected "true" or "false". Defaulting to "false".`,j);let P=K?parseInt(K,10):300;if(K&&(isNaN(P)||P<0))$(`Invalid accordionary-speed="${K}". Expected a positive number in milliseconds. Defaulting to 300.`,j);let W={openDefault:q==="all"||q==="first"?q:"none",allowMultiple:J!=="false",speed:isNaN(P)||P<0?300:P,easing:V||"ease",reduceMotion:g,linked:H==="true"},Q=[],U=F("item",j);if(U.length===0)return $('No items found. Add elements with accordionary="item" inside your component.',j),null;for(let G of U){let X=R("header",G),B=R("content",G),L=R("icon",G);if(!X){N('Missing header element. Add accordionary="header" inside this item.',G);continue}if(!B){N('Missing content element. Add accordionary="content" inside this item.',G);continue}if(!L){N('Missing icon element. Add accordionary="icon" inside your header.',G);continue}let Z=_(G,"open"),T=_(G,"disable");if(Z&&!["true","false"].includes(Z))$(`Invalid accordionary-open="${Z}" on item. Expected "true" or "false".`,G);let b={openOverride:Z==="true"?!0:Z==="false"?!1:null,disabled:T==="true"};Q.push({element:G,headingElement:X,contentElement:B,iconElement:L,config:b})}Q.forEach((G,X)=>{x(G,W,X===0,Q)});let z=Q.map((G)=>({element:G.element,open:()=>G.open?.(),close:()=>G.close?.(),toggle:()=>G.toggle?.()}));return{element:j,openAll:()=>{for(let G of Q)G.open?.()},closeAll:()=>{for(let G of Q)G.close?.()},open:(G)=>{Q[G]?.open?.()},close:(G)=>{Q[G]?.close?.()},toggle:(G)=>{Q[G]?.toggle?.()},items:z}}function D(j,q={}){let{icon:J="▼",openDefault:K="none",allowMultiple:V=!0,speed:H=300,easing:P="ease",linked:W=!1,classes:Q={}}=q,U=document.createElement("div");if(U.setAttribute("accordionary","component"),K!=="none")U.setAttribute("accordionary-open",K);if(!V)U.setAttribute("accordionary-multiple","false");if(H!==300)U.setAttribute("accordionary-speed",H.toString());if(P!=="ease")U.setAttribute("accordionary-easing",P);if(W)U.setAttribute("accordionary-link","true");if(Q.component)U.classList.add(...Q.component);for(let z of j.items){let G=d(z,J,Q);U.appendChild(G)}return U}function d(j,q,J){let{heading:K,content:V,config:H={}}=j,P=document.createElement("div");if(P.setAttribute("accordionary","item"),H.openOverride!==void 0)P.setAttribute("accordionary-open",H.openOverride.toString());if(H.disabled)P.setAttribute("accordionary-disable","true");if(J.item)P.classList.add(...J.item);let W=document.createElement("div");if(W.setAttribute("accordionary","header"),W.innerHTML=K,J.heading)W.classList.add(...J.heading);let Q=document.createElement("span");if(Q.setAttribute("accordionary","icon"),Q.innerHTML=q,J.icon)Q.classList.add(...J.icon);W.appendChild(Q);let U=document.createElement("div");if(U.setAttribute("accordionary","content"),U.innerHTML=V,J.content)U.classList.add(...J.content);return P.appendChild(W),P.appendChild(U),P}function C(j){let q=typeof j==="string"?document.querySelector(j):j;if(!q)return console.error(`[Accordionary] Element not found: ${j}`),null;return S(q)}function O(){let j=F("component"),q=[];for(let J of j){let K=S(J);if(K)q.push(K)}return q}var p={init:C,initAll:O,generateAccordionary:D};
@@ -4,8 +4,9 @@
4
4
  * Exports the API for programmatic control of accordions.
5
5
  * Use this when installing via NPM for manual initialization.
6
6
  */
7
- import type { AccordionController, ItemController } from "./types";
8
- export type { AccordionController, ItemController };
7
+ import { generateAccordionary } from "./generate-accordion";
8
+ import type { AccordionController, ItemController, AccordionData, AccordionItemData, AccordionItemConfig, GeneratorConfig, GeneratorClasses } from "./types";
9
+ export type { AccordionController, ItemController, AccordionData, AccordionItemData, AccordionItemConfig, GeneratorConfig, GeneratorClasses, };
9
10
  /**
10
11
  * Initialize a single accordion component.
11
12
  *
@@ -28,8 +29,10 @@ export declare function init(element: HTMLElement | string): AccordionController
28
29
  * accordions[0].openAll();
29
30
  */
30
31
  export declare function initAll(): AccordionController[];
32
+ export { generateAccordionary };
31
33
  declare const _default: {
32
34
  init: typeof init;
33
35
  initAll: typeof initAll;
36
+ generateAccordionary: typeof generateAccordionary;
34
37
  };
35
38
  export default _default;
@@ -1 +1 @@
1
- var w=0;function y(j,q,P,Q){let{headingElement:J,contentElement:G,iconElement:W,config:H}=j,_=`accordionary-${w++}`,z=`${_}-header`,k=`${_}-content`,U=!1;if(H.openOverride!==null)U=H.openOverride;else if(q.openDefault==="all")U=!0;else if(q.openDefault==="first"&&P)U=!0;let K=U,{speed:$,easing:V,reduceMotion:B}=q,R=B?"none":`height ${$}ms ${V}`,S=B?"none":`transform ${$}ms ${V}`;if(G.style.overflow="hidden",G.style.transition=R,G.style.height=K?"auto":"0px",W.style.transition=S,H.disabled)W.style.visibility="hidden";else if(K)W.style.transform="rotate(180deg)";if(J.id=z,J.setAttribute("tabindex",H.disabled?"-1":"0"),J.setAttribute("role","button"),J.setAttribute("aria-expanded",K?"true":"false"),J.setAttribute("aria-controls",k),J.style.cursor=H.disabled?"default":"pointer",H.disabled)J.setAttribute("aria-disabled","true");G.id=k,G.setAttribute("role","region"),G.setAttribute("aria-labelledby",z);function M(){if(K)return;K=!0,G.style.height=`${G.scrollHeight}px`,W.style.transform="rotate(180deg)",J.setAttribute("aria-expanded","true")}function T(){if(!K)return;K=!1,G.style.height=`${G.scrollHeight}px`,G.offsetHeight,G.style.height="0px",W.style.transform="rotate(0deg)",J.setAttribute("aria-expanded","false")}function b(){if(H.disabled)return;if(K)T();else{if(!q.allowMultiple){for(let X of Q)if(X!==j&&X.close)X.close()}M()}}j.open=M,j.close=T,j.toggle=b,J.addEventListener("click",b),J.addEventListener("keydown",(X)=>{if(X.key==="Enter"||X.key===" ")X.preventDefault(),b()}),G.addEventListener("transitionend",()=>{if(K)G.style.height="auto"})}function Z(j,q){console.warn(`[Accordionary] ${j}`,q||"")}function L(j,q){console.error(`[Accordionary] ${j}`,q||"")}function Y(j,q){return j.getAttribute(`accordionary-${q}`)}function N(j,q=document){return q.querySelector(`[accordionary="${j}"]`)}function D(j,q=document){return q.querySelectorAll(`[accordionary="${j}"]`)}var v=window.matchMedia("(prefers-reduced-motion: reduce)").matches;function F(j){if(j.hasAttribute("data-accordionary-initialized"))return null;j.setAttribute("data-accordionary-initialized","true");let q=Y(j,"open"),P=Y(j,"multiple"),Q=Y(j,"speed"),J=Y(j,"easing");if(q&&!["all","first","none"].includes(q))Z(`Invalid accordionary-open="${q}". Expected "all", "first", or "none". Defaulting to "none".`,j);if(P&&!["true","false"].includes(P))Z(`Invalid accordionary-multiple="${P}". Expected "true" or "false". Defaulting to "true".`,j);let G=Q?parseInt(Q,10):300;if(Q&&(isNaN(G)||G<0))Z(`Invalid accordionary-speed="${Q}". Expected a positive number in milliseconds. Defaulting to 300.`,j);let W={openDefault:q==="all"||q==="first"?q:"none",allowMultiple:P!=="false",speed:isNaN(G)||G<0?300:G,easing:J||"ease",reduceMotion:v},H=[],_=D("item",j);if(_.length===0)return Z('No items found. Add elements with accordionary="item" inside your component.',j),null;for(let k of _){let U=N("header",k),K=N("content",k),$=N("icon",k);if(!U){L('Missing header element. Add accordionary="header" inside this item.',k);continue}if(!K){L('Missing content element. Add accordionary="content" inside this item.',k);continue}if(!$){L('Missing icon element. Add accordionary="icon" inside your header.',k);continue}let V=Y(k,"open"),B=Y(k,"disable");if(V&&!["true","false"].includes(V))Z(`Invalid accordionary-open="${V}" on item. Expected "true" or "false".`,k);let R={openOverride:V==="true"?!0:V==="false"?!1:null,disabled:B==="true"};H.push({element:k,headingElement:U,contentElement:K,iconElement:$,config:R})}H.forEach((k,U)=>{y(k,W,U===0,H)});let z=H.map((k)=>({element:k.element,open:()=>k.open?.(),close:()=>k.close?.(),toggle:()=>k.toggle?.()}));return{element:j,openAll:()=>{for(let k of H)k.open?.()},closeAll:()=>{for(let k of H)k.close?.()},open:(k)=>{H[k]?.open?.()},close:(k)=>{H[k]?.close?.()},toggle:(k)=>{H[k]?.toggle?.()},items:z}}function C(j){let q=typeof j==="string"?document.querySelector(j):j;if(!q)return console.error(`[Accordionary] Element not found: ${j}`),null;return F(q)}function x(){let j=D("component"),q=[];for(let P of j){let Q=F(P);if(Q)q.push(Q)}return q}var g={init:C,initAll:x};export{x as initAll,C as init,g as default};
1
+ var w=0;function y(j,G,P,W){let{headingElement:U,contentElement:H,iconElement:J,config:V}=j,K=`accordionary-${w++}`,Q=`${K}-header`,z=`${K}-content`,q=!1;if(V.openOverride!==null)q=V.openOverride;else if(G.openDefault==="all")q=!0;else if(G.openDefault==="first"&&P)q=!0;let X=q,{speed:B,easing:L,reduceMotion:Z}=G,T=Z?"none":`height ${B}ms ${L}`,b=Z?"none":`transform ${B}ms ${L}`;if(H.style.overflow="hidden",H.style.transition=T,H.style.height=X?"auto":"0px",!X)H.inert=!0;if(J.style.transition=b,V.disabled)J.style.visibility="hidden";else if(X)J.style.transform="rotate(180deg)";if(U.id=Q,U.setAttribute("tabindex",V.disabled?"-1":"0"),U.setAttribute("role","button"),U.setAttribute("aria-expanded",X?"true":"false"),U.setAttribute("aria-controls",z),U.style.cursor=V.disabled?"default":"pointer",V.disabled)U.setAttribute("aria-disabled","true");H.id=z,H.setAttribute("role","region"),H.setAttribute("aria-labelledby",Q);function S(){if(X)return;X=!0,H.inert=!1,H.style.height=`${H.scrollHeight}px`,J.style.transform="rotate(180deg)",U.setAttribute("aria-expanded","true")}function D(){if(!X)return;X=!1,H.inert=!0,H.style.height=`${H.scrollHeight}px`,H.offsetHeight,H.style.height="0px",J.style.transform="rotate(0deg)",U.setAttribute("aria-expanded","false")}function M(){if(V.disabled)return;if(G.linked){if(X){for(let Y of W)if(!Y.config.disabled)Y.close?.()}else for(let Y of W)if(!Y.config.disabled)Y.open?.()}else if(X)D();else{if(!G.allowMultiple){for(let Y of W)if(Y!==j&&Y.close)Y.close()}S()}}j.open=S,j.close=D,j.toggle=M,U.addEventListener("click",M),U.addEventListener("keydown",(Y)=>{if(Y.key==="Enter"||Y.key===" ")Y.preventDefault(),M()}),H.addEventListener("transitionend",()=>{if(X)H.style.height="auto"})}function $(j,G){console.warn(`[Accordionary] ${j}`,G||"")}function N(j,G){console.error(`[Accordionary] ${j}`,G||"")}function _(j,G){return j.getAttribute(`accordionary-${G}`)}function R(j,G=document){return G.querySelector(`[accordionary="${j}"]`)}function F(j,G=document){return G.querySelectorAll(`[accordionary="${j}"]`)}var x=window.matchMedia("(prefers-reduced-motion: reduce)").matches;function k(j){if(j.hasAttribute("data-accordionary-initialized"))return null;j.setAttribute("data-accordionary-initialized","true");let G=_(j,"open"),P=_(j,"multiple"),W=_(j,"speed"),U=_(j,"easing"),H=_(j,"link");if(G&&!["all","first","none"].includes(G))$(`Invalid accordionary-open="${G}". Expected "all", "first", or "none". Defaulting to "none".`,j);if(P&&!["true","false"].includes(P))$(`Invalid accordionary-multiple="${P}". Expected "true" or "false". Defaulting to "true".`,j);if(H&&!["true","false"].includes(H))$(`Invalid accordionary-link="${H}". Expected "true" or "false". Defaulting to "false".`,j);let J=W?parseInt(W,10):300;if(W&&(isNaN(J)||J<0))$(`Invalid accordionary-speed="${W}". Expected a positive number in milliseconds. Defaulting to 300.`,j);let V={openDefault:G==="all"||G==="first"?G:"none",allowMultiple:P!=="false",speed:isNaN(J)||J<0?300:J,easing:U||"ease",reduceMotion:x,linked:H==="true"},K=[],Q=F("item",j);if(Q.length===0)return $('No items found. Add elements with accordionary="item" inside your component.',j),null;for(let q of Q){let X=R("header",q),B=R("content",q),L=R("icon",q);if(!X){N('Missing header element. Add accordionary="header" inside this item.',q);continue}if(!B){N('Missing content element. Add accordionary="content" inside this item.',q);continue}if(!L){N('Missing icon element. Add accordionary="icon" inside your header.',q);continue}let Z=_(q,"open"),T=_(q,"disable");if(Z&&!["true","false"].includes(Z))$(`Invalid accordionary-open="${Z}" on item. Expected "true" or "false".`,q);let b={openOverride:Z==="true"?!0:Z==="false"?!1:null,disabled:T==="true"};K.push({element:q,headingElement:X,contentElement:B,iconElement:L,config:b})}K.forEach((q,X)=>{y(q,V,X===0,K)});let z=K.map((q)=>({element:q.element,open:()=>q.open?.(),close:()=>q.close?.(),toggle:()=>q.toggle?.()}));return{element:j,openAll:()=>{for(let q of K)q.open?.()},closeAll:()=>{for(let q of K)q.close?.()},open:(q)=>{K[q]?.open?.()},close:(q)=>{K[q]?.close?.()},toggle:(q)=>{K[q]?.toggle?.()},items:z}}function v(j,G={}){let{icon:P="▼",openDefault:W="none",allowMultiple:U=!0,speed:H=300,easing:J="ease",linked:V=!1,classes:K={}}=G,Q=document.createElement("div");if(Q.setAttribute("accordionary","component"),W!=="none")Q.setAttribute("accordionary-open",W);if(!U)Q.setAttribute("accordionary-multiple","false");if(H!==300)Q.setAttribute("accordionary-speed",H.toString());if(J!=="ease")Q.setAttribute("accordionary-easing",J);if(V)Q.setAttribute("accordionary-link","true");if(K.component)Q.classList.add(...K.component);for(let z of j.items){let q=C(z,P,K);Q.appendChild(q)}return Q}function C(j,G,P){let{heading:W,content:U,config:H={}}=j,J=document.createElement("div");if(J.setAttribute("accordionary","item"),H.openOverride!==void 0)J.setAttribute("accordionary-open",H.openOverride.toString());if(H.disabled)J.setAttribute("accordionary-disable","true");if(P.item)J.classList.add(...P.item);let V=document.createElement("div");if(V.setAttribute("accordionary","header"),V.innerHTML=W,P.heading)V.classList.add(...P.heading);let K=document.createElement("span");if(K.setAttribute("accordionary","icon"),K.innerHTML=G,P.icon)K.classList.add(...P.icon);V.appendChild(K);let Q=document.createElement("div");if(Q.setAttribute("accordionary","content"),Q.innerHTML=U,P.content)Q.classList.add(...P.content);return J.appendChild(V),J.appendChild(Q),J}function O(j){let G=typeof j==="string"?document.querySelector(j):j;if(!G)return console.error(`[Accordionary] Element not found: ${j}`),null;return k(G)}function I(){let j=F("component"),G=[];for(let P of j){let W=k(P);if(W)G.push(W)}return G}var a={init:O,initAll:I,generateAccordionary:v};export{I as initAll,O as init,v as generateAccordionary,a as default};
@@ -1 +1 @@
1
- (()=>{var y=0;function M(j,q,U,Y){let{headingElement:J,contentElement:G,iconElement:V,config:H}=j,_=`accordionary-${y++}`,B=`${_}-header`,k=`${_}-content`,P=!1;if(H.openOverride!==null)P=H.openOverride;else if(q.openDefault==="all")P=!0;else if(q.openDefault==="first"&&U)P=!0;let K=P,{speed:$,easing:Q,reduceMotion:L}=q,R=L?"none":`height ${$}ms ${Q}`,v=L?"none":`transform ${$}ms ${Q}`;if(G.style.overflow="hidden",G.style.transition=R,G.style.height=K?"auto":"0px",V.style.transition=v,H.disabled)V.style.visibility="hidden";else if(K)V.style.transform="rotate(180deg)";if(J.id=B,J.setAttribute("tabindex",H.disabled?"-1":"0"),J.setAttribute("role","button"),J.setAttribute("aria-expanded",K?"true":"false"),J.setAttribute("aria-controls",k),J.style.cursor=H.disabled?"default":"pointer",H.disabled)J.setAttribute("aria-disabled","true");G.id=k,G.setAttribute("role","region"),G.setAttribute("aria-labelledby",B);function F(){if(K)return;K=!0,G.style.height=`${G.scrollHeight}px`,V.style.transform="rotate(180deg)",J.setAttribute("aria-expanded","true")}function T(){if(!K)return;K=!1,G.style.height=`${G.scrollHeight}px`,G.offsetHeight,G.style.height="0px",V.style.transform="rotate(0deg)",J.setAttribute("aria-expanded","false")}function b(){if(H.disabled)return;if(K)T();else{if(!q.allowMultiple){for(let W of Y)if(W!==j&&W.close)W.close()}F()}}j.open=F,j.close=T,j.toggle=b,J.addEventListener("click",b),J.addEventListener("keydown",(W)=>{if(W.key==="Enter"||W.key===" ")W.preventDefault(),b()}),G.addEventListener("transitionend",()=>{if(K)G.style.height="auto"})}function Z(j,q){console.warn(`[Accordionary] ${j}`,q||"")}function N(j,q){console.error(`[Accordionary] ${j}`,q||"")}function X(j,q){return j.getAttribute(`accordionary-${q}`)}function z(j,q=document){return q.querySelector(`[accordionary="${j}"]`)}function D(j,q=document){return q.querySelectorAll(`[accordionary="${j}"]`)}var x=window.matchMedia("(prefers-reduced-motion: reduce)").matches;function S(j){if(j.hasAttribute("data-accordionary-initialized"))return null;j.setAttribute("data-accordionary-initialized","true");let q=X(j,"open"),U=X(j,"multiple"),Y=X(j,"speed"),J=X(j,"easing");if(q&&!["all","first","none"].includes(q))Z(`Invalid accordionary-open="${q}". Expected "all", "first", or "none". Defaulting to "none".`,j);if(U&&!["true","false"].includes(U))Z(`Invalid accordionary-multiple="${U}". Expected "true" or "false". Defaulting to "true".`,j);let G=Y?parseInt(Y,10):300;if(Y&&(isNaN(G)||G<0))Z(`Invalid accordionary-speed="${Y}". Expected a positive number in milliseconds. Defaulting to 300.`,j);let V={openDefault:q==="all"||q==="first"?q:"none",allowMultiple:U!=="false",speed:isNaN(G)||G<0?300:G,easing:J||"ease",reduceMotion:x},H=[],_=D("item",j);if(_.length===0)return Z('No items found. Add elements with accordionary="item" inside your component.',j),null;for(let k of _){let P=z("header",k),K=z("content",k),$=z("icon",k);if(!P){N('Missing header element. Add accordionary="header" inside this item.',k);continue}if(!K){N('Missing content element. Add accordionary="content" inside this item.',k);continue}if(!$){N('Missing icon element. Add accordionary="icon" inside your header.',k);continue}let Q=X(k,"open"),L=X(k,"disable");if(Q&&!["true","false"].includes(Q))Z(`Invalid accordionary-open="${Q}" on item. Expected "true" or "false".`,k);let R={openOverride:Q==="true"?!0:Q==="false"?!1:null,disabled:L==="true"};H.push({element:k,headingElement:P,contentElement:K,iconElement:$,config:R})}H.forEach((k,P)=>{M(k,V,P===0,H)});let B=H.map((k)=>({element:k.element,open:()=>k.open?.(),close:()=>k.close?.(),toggle:()=>k.toggle?.()}));return{element:j,openAll:()=>{for(let k of H)k.open?.()},closeAll:()=>{for(let k of H)k.close?.()},open:(k)=>{H[k]?.open?.()},close:(k)=>{H[k]?.close?.()},toggle:(k)=>{H[k]?.toggle?.()},items:B}}function w(){let j=D("component");for(let q of j)S(q)}if(document.readyState==="loading")document.addEventListener("DOMContentLoaded",w);else w();})();
1
+ (()=>{var y=0;function S(j,G,Z,W){let{headingElement:K,contentElement:H,iconElement:U,config:V}=j,Q=`accordionary-${y++}`,$=`${Q}-header`,N=`${Q}-content`,q=!1;if(V.openOverride!==null)q=V.openOverride;else if(G.openDefault==="all")q=!0;else if(G.openDefault==="first"&&Z)q=!0;let J=q,{speed:B,easing:L,reduceMotion:X}=G,b=X?"none":`height ${B}ms ${L}`,F=X?"none":`transform ${B}ms ${L}`;if(H.style.overflow="hidden",H.style.transition=b,H.style.height=J?"auto":"0px",!J)H.inert=!0;if(U.style.transition=F,V.disabled)U.style.visibility="hidden";else if(J)U.style.transform="rotate(180deg)";if(K.id=$,K.setAttribute("tabindex",V.disabled?"-1":"0"),K.setAttribute("role","button"),K.setAttribute("aria-expanded",J?"true":"false"),K.setAttribute("aria-controls",N),K.style.cursor=V.disabled?"default":"pointer",V.disabled)K.setAttribute("aria-disabled","true");H.id=N,H.setAttribute("role","region"),H.setAttribute("aria-labelledby",$);function k(){if(J)return;J=!0,H.inert=!1,H.style.height=`${H.scrollHeight}px`,U.style.transform="rotate(180deg)",K.setAttribute("aria-expanded","true")}function M(){if(!J)return;J=!1,H.inert=!0,H.style.height=`${H.scrollHeight}px`,H.offsetHeight,H.style.height="0px",U.style.transform="rotate(0deg)",K.setAttribute("aria-expanded","false")}function T(){if(V.disabled)return;if(G.linked){if(J){for(let P of W)if(!P.config.disabled)P.close?.()}else for(let P of W)if(!P.config.disabled)P.open?.()}else if(J)M();else{if(!G.allowMultiple){for(let P of W)if(P!==j&&P.close)P.close()}k()}}j.open=k,j.close=M,j.toggle=T,K.addEventListener("click",T),K.addEventListener("keydown",(P)=>{if(P.key==="Enter"||P.key===" ")P.preventDefault(),T()}),H.addEventListener("transitionend",()=>{if(J)H.style.height="auto"})}function _(j,G){console.warn(`[Accordionary] ${j}`,G||"")}function z(j,G){console.error(`[Accordionary] ${j}`,G||"")}function Y(j,G){return j.getAttribute(`accordionary-${G}`)}function D(j,G=document){return G.querySelector(`[accordionary="${j}"]`)}function R(j,G=document){return G.querySelectorAll(`[accordionary="${j}"]`)}var x=window.matchMedia("(prefers-reduced-motion: reduce)").matches;function v(j){if(j.hasAttribute("data-accordionary-initialized"))return null;j.setAttribute("data-accordionary-initialized","true");let G=Y(j,"open"),Z=Y(j,"multiple"),W=Y(j,"speed"),K=Y(j,"easing"),H=Y(j,"link");if(G&&!["all","first","none"].includes(G))_(`Invalid accordionary-open="${G}". Expected "all", "first", or "none". Defaulting to "none".`,j);if(Z&&!["true","false"].includes(Z))_(`Invalid accordionary-multiple="${Z}". Expected "true" or "false". Defaulting to "true".`,j);if(H&&!["true","false"].includes(H))_(`Invalid accordionary-link="${H}". Expected "true" or "false". Defaulting to "false".`,j);let U=W?parseInt(W,10):300;if(W&&(isNaN(U)||U<0))_(`Invalid accordionary-speed="${W}". Expected a positive number in milliseconds. Defaulting to 300.`,j);let V={openDefault:G==="all"||G==="first"?G:"none",allowMultiple:Z!=="false",speed:isNaN(U)||U<0?300:U,easing:K||"ease",reduceMotion:x,linked:H==="true"},Q=[],$=R("item",j);if($.length===0)return _('No items found. Add elements with accordionary="item" inside your component.',j),null;for(let q of $){let J=D("header",q),B=D("content",q),L=D("icon",q);if(!J){z('Missing header element. Add accordionary="header" inside this item.',q);continue}if(!B){z('Missing content element. Add accordionary="content" inside this item.',q);continue}if(!L){z('Missing icon element. Add accordionary="icon" inside your header.',q);continue}let X=Y(q,"open"),b=Y(q,"disable");if(X&&!["true","false"].includes(X))_(`Invalid accordionary-open="${X}" on item. Expected "true" or "false".`,q);let F={openOverride:X==="true"?!0:X==="false"?!1:null,disabled:b==="true"};Q.push({element:q,headingElement:J,contentElement:B,iconElement:L,config:F})}Q.forEach((q,J)=>{S(q,V,J===0,Q)});let N=Q.map((q)=>({element:q.element,open:()=>q.open?.(),close:()=>q.close?.(),toggle:()=>q.toggle?.()}));return{element:j,openAll:()=>{for(let q of Q)q.open?.()},closeAll:()=>{for(let q of Q)q.close?.()},open:(q)=>{Q[q]?.open?.()},close:(q)=>{Q[q]?.close?.()},toggle:(q)=>{Q[q]?.toggle?.()},items:N}}function w(){let j=R("component");for(let G of j)v(G)}if(document.readyState==="loading")document.addEventListener("DOMContentLoaded",w);else w();})();
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Accordionary - Accordion Generator
3
+ *
4
+ * Generates accordion HTML elements from structured JSON data.
5
+ * For use with package managers only (not browser implementation).
6
+ */
7
+ import type { AccordionData, GeneratorConfig } from "./types";
8
+ /**
9
+ * Generates an accordion element from JSON data.
10
+ *
11
+ * @param data - The accordion data with items array
12
+ * @param config - Optional configuration for the accordion
13
+ * @returns An HTMLElement ready to be appended to the DOM and initialized
14
+ *
15
+ * @example
16
+ * ```typescript
17
+ * const data = {
18
+ * items: [
19
+ * {
20
+ * heading: "Question 1",
21
+ * content: "Answer 1",
22
+ * },
23
+ * {
24
+ * heading: "Question 2",
25
+ * content: "Answer 2",
26
+ * config: { openOverride: true }
27
+ * }
28
+ * ]
29
+ * };
30
+ *
31
+ * const element = generateAccordionary(data, {
32
+ * icon: "▼",
33
+ * openDefault: "none",
34
+ * allowMultiple: true,
35
+ * speed: 300,
36
+ * easing: "ease",
37
+ * classes: {
38
+ * component: ["my-accordion"],
39
+ * item: ["my-item"],
40
+ * }
41
+ * });
42
+ *
43
+ * document.body.appendChild(element);
44
+ * const accordion = Accordionary.init(element);
45
+ * ```
46
+ */
47
+ export declare function generateAccordionary(data: AccordionData, config?: GeneratorConfig): HTMLElement;
package/dist/types.d.ts CHANGED
@@ -15,6 +15,8 @@ export interface AccordionConfig {
15
15
  easing: string;
16
16
  /** Whether to disable animations (respects prefers-reduced-motion) */
17
17
  reduceMotion: boolean;
18
+ /** Whether all items are linked (open/close together) */
19
+ linked: boolean;
18
20
  }
19
21
  /**
20
22
  * Item-level configuration parsed from HTML attributes.
@@ -78,3 +80,64 @@ export interface AccordionController {
78
80
  /** Get all item controllers */
79
81
  items: ItemController[];
80
82
  }
83
+ /**
84
+ * Configuration for a single accordion item in generated data.
85
+ */
86
+ export interface AccordionItemConfig {
87
+ /** Override for initial open state */
88
+ openOverride?: boolean;
89
+ /** Whether the item is non-interactive and always open */
90
+ disabled?: boolean;
91
+ }
92
+ /**
93
+ * Data for a single accordion item.
94
+ */
95
+ export interface AccordionItemData {
96
+ /** HTML content for the heading */
97
+ heading: string;
98
+ /** HTML content for the collapsible content */
99
+ content: string;
100
+ /** Optional item-specific configuration */
101
+ config?: AccordionItemConfig;
102
+ }
103
+ /**
104
+ * Data structure for accordion generation.
105
+ */
106
+ export interface AccordionData {
107
+ /** Array of accordion items */
108
+ items: AccordionItemData[];
109
+ }
110
+ /**
111
+ * CSS class names to apply to generated accordion elements.
112
+ */
113
+ export interface GeneratorClasses {
114
+ /** Classes for the accordion component container */
115
+ component?: string[];
116
+ /** Classes for each accordion item */
117
+ item?: string[];
118
+ /** Classes for each accordion heading */
119
+ heading?: string[];
120
+ /** Classes for each accordion content panel */
121
+ content?: string[];
122
+ /** Classes for each accordion icon */
123
+ icon?: string[];
124
+ }
125
+ /**
126
+ * Configuration options for accordion generation.
127
+ */
128
+ export interface GeneratorConfig {
129
+ /** HTML string for the expand/collapse icon (default: "▼") */
130
+ icon?: string;
131
+ /** Which items to open by default: "all", "first", or "none" (default: "none") */
132
+ openDefault?: "all" | "first" | "none";
133
+ /** Whether multiple items can be open simultaneously (default: false) */
134
+ allowMultiple?: boolean;
135
+ /** Animation duration in milliseconds (default: 300) */
136
+ speed?: number;
137
+ /** CSS easing function for animations (default: "ease") */
138
+ easing?: string;
139
+ /** Whether all items are linked (open/close together) (default: false) */
140
+ linked?: boolean;
141
+ /** Optional CSS class names to apply to elements */
142
+ classes?: GeneratorClasses;
143
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "accordionary",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "A lightweight, accessible, vanilla JavaScript accordion with zero dependencies",
5
5
  "author": "Brian Tucker",
6
6
  "license": "GPL-3.0-or-later",
@@ -53,6 +53,7 @@
53
53
  "url": "https://github.com/briantucker/accordionary/issues"
54
54
  },
55
55
  "devDependencies": {
56
- "@types/bun": "latest"
56
+ "@types/bun": "latest",
57
+ "happy-dom": "^20.4.0"
57
58
  }
58
59
  }