@magic-spells/collapsible-content 0.2.0 → 1.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.
@@ -3,6 +3,7 @@
3
3
  */
4
4
  class CollapsibleComponent extends HTMLElement {
5
5
  #handleClick;
6
+ #abortController;
6
7
 
7
8
  /**
8
9
  * Initializes the component and sets up references to child elements
@@ -21,7 +22,7 @@ class CollapsibleComponent extends HTMLElement {
21
22
  // toggle expanded value
22
23
  const expanded = _.button.getAttribute('aria-expanded') !== 'true';
23
24
  _.button.setAttribute('aria-expanded', expanded);
24
- _.content.hidden = !expanded;
25
+ _.content.collapsed = !expanded;
25
26
  };
26
27
  }
27
28
 
@@ -37,7 +38,16 @@ class CollapsibleComponent extends HTMLElement {
37
38
  _.content = _.querySelector('collapsible-content');
38
39
 
39
40
  if (!_.button || !_.content) {
40
- console.error('CollapsibleComponent requires a <button> and a <collapsible-content>.');
41
+ const error = new Error(
42
+ 'CollapsibleComponent requires a <button> and a <collapsible-content>.'
43
+ );
44
+ console.error(error.message);
45
+ _.dispatchEvent(
46
+ new CustomEvent('collapsible-error', {
47
+ bubbles: true,
48
+ detail: { error },
49
+ })
50
+ );
41
51
  return;
42
52
  }
43
53
 
@@ -46,18 +56,20 @@ class CollapsibleComponent extends HTMLElement {
46
56
  _.content.id ||= `collapsible-content-${crypto.randomUUID().slice(0, 8)}`;
47
57
 
48
58
  // set accessibility attributes
59
+ _.button.type ||= 'button';
49
60
  _.button.setAttribute('aria-controls', _.content.id);
50
- _.content.setAttribute('role', 'region');
51
61
  _.content.setAttribute('aria-labelledby', _.button.id);
52
62
 
53
- // set initial state based on aria-expanded attribute
54
- const expanded = _.button.getAttribute('aria-expanded') === 'true';
63
+ // set initial state based on open attribute
64
+ const open = _.content.hasAttribute('open');
65
+ _.button.setAttribute('aria-expanded', open);
66
+ _.content.collapsed = !open;
55
67
 
56
- // make sure content state matches button state
57
- _.content.hidden = !expanded;
58
-
59
- // add event listener
60
- _.button.addEventListener('click', _.#handleClick);
68
+ // use AbortController to prevent duplicate listeners on reconnection
69
+ _.#abortController = new AbortController();
70
+ _.button.addEventListener('click', _.#handleClick, {
71
+ signal: _.#abortController.signal,
72
+ });
61
73
  }
62
74
 
63
75
  /**
@@ -66,8 +78,9 @@ class CollapsibleComponent extends HTMLElement {
66
78
  */
67
79
  disconnectedCallback() {
68
80
  const _ = this;
69
- if (_.button) {
70
- _.button.removeEventListener('click', _.#handleClick);
81
+ if (_.#abortController) {
82
+ _.#abortController.abort();
83
+ _.#abortController = null;
71
84
  }
72
85
  }
73
86
  }
@@ -77,6 +90,8 @@ class CollapsibleComponent extends HTMLElement {
77
90
  */
78
91
  class CollapsibleContent extends HTMLElement {
79
92
  #handleTransitionEnd;
93
+ #abortController;
94
+ #animating = false;
80
95
 
81
96
  /**
82
97
  * Initializes the content element and binds event handlers
@@ -88,35 +103,30 @@ class CollapsibleContent extends HTMLElement {
88
103
  // define event handler using arrow function for proper binding
89
104
  _.#handleTransitionEnd = (event) => {
90
105
  // exit if isn't height property
91
- if (event.propertyName != 'height') return;
106
+ if (event.propertyName !== 'height') return;
107
+
108
+ _.#animating = false;
92
109
 
93
110
  // remove the inline height to allow dynamic content changes
94
- if (!_.hidden) {
111
+ if (!_.collapsed) {
95
112
  _.style.height = 'auto';
96
113
  }
97
114
  };
98
-
99
- // add event listener to remove height after transition
100
- _.addEventListener('transitionend', _.#handleTransitionEnd);
101
115
  }
102
116
 
103
117
  /**
104
118
  * Called when element is added to the DOM
105
- * Sets initial height based on hidden state
119
+ * Sets initial height based on open attribute
106
120
  */
107
121
  connectedCallback() {
108
122
  const _ = this;
123
+ _.style.height = _.hasAttribute('open') ? 'auto' : '0';
109
124
 
110
- // get aria-expanded state from button
111
- const component = _.closest('collapsible-component');
112
- const button = component.querySelector('button');
113
- const expanded = button.getAttribute('aria-expanded') === 'true';
114
-
115
- if (expanded) {
116
- _.style.height = 'auto';
117
- } else {
118
- _.style.height = '0';
119
- }
125
+ // use AbortController to prevent duplicate listeners on reconnection
126
+ _.#abortController = new AbortController();
127
+ _.addEventListener('transitionend', _.#handleTransitionEnd, {
128
+ signal: _.#abortController.signal,
129
+ });
120
130
  }
121
131
 
122
132
  /**
@@ -125,57 +135,74 @@ class CollapsibleContent extends HTMLElement {
125
135
  */
126
136
  disconnectedCallback() {
127
137
  const _ = this;
128
- _.removeEventListener('transitionend', _.#handleTransitionEnd);
138
+ if (_.#abortController) {
139
+ _.#abortController.abort();
140
+ _.#abortController = null;
141
+ }
129
142
  }
130
143
 
131
144
  /**
132
- * Handles setting the hidden attribute with animation
133
- * @param {boolean} value - Whether element should be hidden
145
+ * Handles setting the collapsed state with animation
146
+ * @param {boolean} value - Whether element should be collapsed
134
147
  */
135
- set hidden(value) {
148
+ set collapsed(value) {
136
149
  const _ = this;
137
150
 
138
- // animate to closed
151
+ // prevent rapid clicking during animation
152
+ if (_.#animating) return;
153
+
154
+ // check if transitions are enabled (respects prefers-reduced-motion)
155
+ const hasTransition = getComputedStyle(_).transitionDuration !== '0s';
156
+
139
157
  if (value) {
140
- // reset height to animate from
158
+ if (hasTransition) {
159
+ _.#animating = true;
160
+ }
161
+
162
+ // animate to closed
141
163
  _.style.height = `${_.scrollHeight}px`;
142
164
 
143
- // wait one frame and animate to 0
144
- setTimeout(() => {
145
- _.style.height = '0px';
146
- }, 1);
165
+ // use requestAnimationFrame for reliable frame timing
166
+ requestAnimationFrame(() => {
167
+ requestAnimationFrame(() => {
168
+ _.style.height = '0px';
169
+ });
170
+ });
147
171
 
148
- // set accessibility attributes
172
+ _.removeAttribute('open');
149
173
  _.setAttribute('aria-hidden', 'true');
150
174
  _.setAttribute('inert', '');
151
- _.setAttribute('hidden', '');
152
175
  } else {
153
- // only animate if not set to auto
176
+ // only animate if not already auto
154
177
  if (_.style.height !== 'auto') {
155
- // animate to open
178
+ if (hasTransition) {
179
+ _.#animating = true;
180
+ }
156
181
  _.style.height = `${_.scrollHeight}px`;
157
182
  }
158
183
 
159
- // remove accessibility attributes
184
+ _.setAttribute('open', '');
160
185
  _.removeAttribute('aria-hidden');
161
186
  _.removeAttribute('inert');
162
- _.removeAttribute('hidden');
163
187
  }
164
188
  }
165
189
 
166
190
  /**
167
- * Gets the hidden state from the attribute
168
- * @returns {boolean} Whether element is hidden
191
+ * Gets the collapsed state
192
+ * @returns {boolean} Whether element is collapsed
169
193
  */
170
- get hidden() {
171
- const _ = this;
172
- return _.hasAttribute('hidden');
194
+ get collapsed() {
195
+ return !this.hasAttribute('open');
173
196
  }
174
197
  }
175
198
 
176
- // register custom elements
177
- customElements.define('collapsible-content', CollapsibleContent);
178
- customElements.define('collapsible-component', CollapsibleComponent);
199
+ // register custom elements if not already defined
200
+ if (!customElements.get('collapsible-content')) {
201
+ customElements.define('collapsible-content', CollapsibleContent);
202
+ }
203
+ if (!customElements.get('collapsible-component')) {
204
+ customElements.define('collapsible-component', CollapsibleComponent);
205
+ }
179
206
 
180
207
  export { CollapsibleComponent, CollapsibleContent };
181
208
  //# sourceMappingURL=collapsible-content.esm.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"collapsible-content.esm.js","sources":["../src/collapsible-content.js"],"sourcesContent":["import './index.scss';\n\n/**\n * Custom element that creates a collapsible/expandable component with proper accessibility\n */\nclass CollapsibleComponent extends HTMLElement {\n\t#handleClick;\n\n\t/**\n\t * Initializes the component and sets up references to child elements\n\t */\n\tconstructor() {\n\t\tsuper();\n\t\tconst _ = this;\n\n\t\t// store references to elements once to avoid re-querying\n\t\t_.button = null;\n\t\t_.content = null;\n\n\t\t// define click handler with proper this binding\n\t\t_.#handleClick = (e) => {\n\t\t\te.preventDefault();\n\t\t\t// toggle expanded value\n\t\t\tconst expanded = _.button.getAttribute('aria-expanded') !== 'true';\n\t\t\t_.button.setAttribute('aria-expanded', expanded);\n\t\t\t_.content.hidden = !expanded;\n\t\t};\n\t}\n\n\t/**\n\t * Called when element is added to the DOM\n\t * Sets up accessibility attributes and event listeners\n\t */\n\tconnectedCallback() {\n\t\tconst _ = this;\n\n\t\t// initialize element references once\n\t\t_.button = _.querySelector('button');\n\t\t_.content = _.querySelector('collapsible-content');\n\n\t\tif (!_.button || !_.content) {\n\t\t\tconsole.error('CollapsibleComponent requires a <button> and a <collapsible-content>.');\n\t\t\treturn;\n\t\t}\n\n\t\t// generate ids if not provided\n\t\t_.button.id ||= `collapsible-button-${crypto.randomUUID().slice(0, 8)}`;\n\t\t_.content.id ||= `collapsible-content-${crypto.randomUUID().slice(0, 8)}`;\n\n\t\t// set accessibility attributes\n\t\t_.button.setAttribute('aria-controls', _.content.id);\n\t\t_.content.setAttribute('role', 'region');\n\t\t_.content.setAttribute('aria-labelledby', _.button.id);\n\n\t\t// set initial state based on aria-expanded attribute\n\t\tconst expanded = _.button.getAttribute('aria-expanded') === 'true';\n\n\t\t// make sure content state matches button state\n\t\t_.content.hidden = !expanded;\n\n\t\t// add event listener\n\t\t_.button.addEventListener('click', _.#handleClick);\n\t}\n\n\t/**\n\t * Called when element is removed from the DOM\n\t * Cleans up event listeners\n\t */\n\tdisconnectedCallback() {\n\t\tconst _ = this;\n\t\tif (_.button) {\n\t\t\t_.button.removeEventListener('click', _.#handleClick);\n\t\t}\n\t}\n}\n\n/**\n * Custom element that provides animated collapsible content\n */\nclass CollapsibleContent extends HTMLElement {\n\t#handleTransitionEnd;\n\n\t/**\n\t * Initializes the content element and binds event handlers\n\t */\n\tconstructor() {\n\t\tsuper();\n\t\tconst _ = this;\n\n\t\t// define event handler using arrow function for proper binding\n\t\t_.#handleTransitionEnd = (event) => {\n\t\t\t// exit if isn't height property\n\t\t\tif (event.propertyName != 'height') return;\n\n\t\t\t// remove the inline height to allow dynamic content changes\n\t\t\tif (!_.hidden) {\n\t\t\t\t_.style.height = 'auto';\n\t\t\t}\n\t\t};\n\n\t\t// add event listener to remove height after transition\n\t\t_.addEventListener('transitionend', _.#handleTransitionEnd);\n\t}\n\n\t/**\n\t * Called when element is added to the DOM\n\t * Sets initial height based on hidden state\n\t */\n\tconnectedCallback() {\n\t\tconst _ = this;\n\n\t\t// get aria-expanded state from button\n\t\tconst component = _.closest('collapsible-component');\n\t\tconst button = component.querySelector('button');\n\t\tconst expanded = button.getAttribute('aria-expanded') === 'true';\n\n\t\tif (expanded) {\n\t\t\t_.style.height = 'auto';\n\t\t} else {\n\t\t\t_.style.height = '0';\n\t\t}\n\t}\n\n\t/**\n\t * Called when element is removed from the DOM\n\t * Cleans up event listeners\n\t */\n\tdisconnectedCallback() {\n\t\tconst _ = this;\n\t\t_.removeEventListener('transitionend', _.#handleTransitionEnd);\n\t}\n\n\t/**\n\t * Handles setting the hidden attribute with animation\n\t * @param {boolean} value - Whether element should be hidden\n\t */\n\tset hidden(value) {\n\t\tconst _ = this;\n\n\t\t// animate to closed\n\t\tif (value) {\n\t\t\t// reset height to animate from\n\t\t\t_.style.height = `${_.scrollHeight}px`;\n\n\t\t\t// wait one frame and animate to 0\n\t\t\tsetTimeout(() => {\n\t\t\t\t_.style.height = '0px';\n\t\t\t}, 1);\n\n\t\t\t// set accessibility attributes\n\t\t\t_.setAttribute('aria-hidden', 'true');\n\t\t\t_.setAttribute('inert', '');\n\t\t\t_.setAttribute('hidden', '');\n\t\t} else {\n\t\t\t// only animate if not set to auto\n\t\t\tif (_.style.height !== 'auto') {\n\t\t\t\t// animate to open\n\t\t\t\t_.style.height = `${_.scrollHeight}px`;\n\t\t\t}\n\n\t\t\t// remove accessibility attributes\n\t\t\t_.removeAttribute('aria-hidden');\n\t\t\t_.removeAttribute('inert');\n\t\t\t_.removeAttribute('hidden');\n\t\t}\n\t}\n\n\t/**\n\t * Gets the hidden state from the attribute\n\t * @returns {boolean} Whether element is hidden\n\t */\n\tget hidden() {\n\t\tconst _ = this;\n\t\treturn _.hasAttribute('hidden');\n\t}\n}\n\n// register custom elements\ncustomElements.define('collapsible-content', CollapsibleContent);\ncustomElements.define('collapsible-component', CollapsibleComponent);\n\nexport { CollapsibleContent, CollapsibleComponent };\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"collapsible-content.esm.js","sources":["../src/collapsible-content.js"],"sourcesContent":["import './collapsible-content.css';\n\n/**\n * Custom element that creates a collapsible/expandable component with proper accessibility\n */\nclass CollapsibleComponent extends HTMLElement {\n\t#handleClick;\n\t#abortController;\n\n\t/**\n\t * Initializes the component and sets up references to child elements\n\t */\n\tconstructor() {\n\t\tsuper();\n\t\tconst _ = this;\n\n\t\t// store references to elements once to avoid re-querying\n\t\t_.button = null;\n\t\t_.content = null;\n\n\t\t// define click handler with proper this binding\n\t\t_.#handleClick = (e) => {\n\t\t\te.preventDefault();\n\t\t\t// toggle expanded value\n\t\t\tconst expanded = _.button.getAttribute('aria-expanded') !== 'true';\n\t\t\t_.button.setAttribute('aria-expanded', expanded);\n\t\t\t_.content.collapsed = !expanded;\n\t\t};\n\t}\n\n\t/**\n\t * Called when element is added to the DOM\n\t * Sets up accessibility attributes and event listeners\n\t */\n\tconnectedCallback() {\n\t\tconst _ = this;\n\n\t\t// initialize element references once\n\t\t_.button = _.querySelector('button');\n\t\t_.content = _.querySelector('collapsible-content');\n\n\t\tif (!_.button || !_.content) {\n\t\t\tconst error = new Error(\n\t\t\t\t'CollapsibleComponent requires a <button> and a <collapsible-content>.'\n\t\t\t);\n\t\t\tconsole.error(error.message);\n\t\t\t_.dispatchEvent(\n\t\t\t\tnew CustomEvent('collapsible-error', {\n\t\t\t\t\tbubbles: true,\n\t\t\t\t\tdetail: { error },\n\t\t\t\t})\n\t\t\t);\n\t\t\treturn;\n\t\t}\n\n\t\t// generate ids if not provided\n\t\t_.button.id ||= `collapsible-button-${crypto.randomUUID().slice(0, 8)}`;\n\t\t_.content.id ||= `collapsible-content-${crypto.randomUUID().slice(0, 8)}`;\n\n\t\t// set accessibility attributes\n\t\t_.button.type ||= 'button';\n\t\t_.button.setAttribute('aria-controls', _.content.id);\n\t\t_.content.setAttribute('aria-labelledby', _.button.id);\n\n\t\t// set initial state based on open attribute\n\t\tconst open = _.content.hasAttribute('open');\n\t\t_.button.setAttribute('aria-expanded', open);\n\t\t_.content.collapsed = !open;\n\n\t\t// use AbortController to prevent duplicate listeners on reconnection\n\t\t_.#abortController = new AbortController();\n\t\t_.button.addEventListener('click', _.#handleClick, {\n\t\t\tsignal: _.#abortController.signal,\n\t\t});\n\t}\n\n\t/**\n\t * Called when element is removed from the DOM\n\t * Cleans up event listeners\n\t */\n\tdisconnectedCallback() {\n\t\tconst _ = this;\n\t\tif (_.#abortController) {\n\t\t\t_.#abortController.abort();\n\t\t\t_.#abortController = null;\n\t\t}\n\t}\n}\n\n/**\n * Custom element that provides animated collapsible content\n */\nclass CollapsibleContent extends HTMLElement {\n\t#handleTransitionEnd;\n\t#abortController;\n\t#animating = false;\n\n\t/**\n\t * Initializes the content element and binds event handlers\n\t */\n\tconstructor() {\n\t\tsuper();\n\t\tconst _ = this;\n\n\t\t// define event handler using arrow function for proper binding\n\t\t_.#handleTransitionEnd = (event) => {\n\t\t\t// exit if isn't height property\n\t\t\tif (event.propertyName !== 'height') return;\n\n\t\t\t_.#animating = false;\n\n\t\t\t// remove the inline height to allow dynamic content changes\n\t\t\tif (!_.collapsed) {\n\t\t\t\t_.style.height = 'auto';\n\t\t\t}\n\t\t};\n\t}\n\n\t/**\n\t * Called when element is added to the DOM\n\t * Sets initial height based on open attribute\n\t */\n\tconnectedCallback() {\n\t\tconst _ = this;\n\t\t_.style.height = _.hasAttribute('open') ? 'auto' : '0';\n\n\t\t// use AbortController to prevent duplicate listeners on reconnection\n\t\t_.#abortController = new AbortController();\n\t\t_.addEventListener('transitionend', _.#handleTransitionEnd, {\n\t\t\tsignal: _.#abortController.signal,\n\t\t});\n\t}\n\n\t/**\n\t * Called when element is removed from the DOM\n\t * Cleans up event listeners\n\t */\n\tdisconnectedCallback() {\n\t\tconst _ = this;\n\t\tif (_.#abortController) {\n\t\t\t_.#abortController.abort();\n\t\t\t_.#abortController = null;\n\t\t}\n\t}\n\n\t/**\n\t * Handles setting the collapsed state with animation\n\t * @param {boolean} value - Whether element should be collapsed\n\t */\n\tset collapsed(value) {\n\t\tconst _ = this;\n\n\t\t// prevent rapid clicking during animation\n\t\tif (_.#animating) return;\n\n\t\t// check if transitions are enabled (respects prefers-reduced-motion)\n\t\tconst hasTransition = getComputedStyle(_).transitionDuration !== '0s';\n\n\t\tif (value) {\n\t\t\tif (hasTransition) {\n\t\t\t\t_.#animating = true;\n\t\t\t}\n\n\t\t\t// animate to closed\n\t\t\t_.style.height = `${_.scrollHeight}px`;\n\n\t\t\t// use requestAnimationFrame for reliable frame timing\n\t\t\trequestAnimationFrame(() => {\n\t\t\t\trequestAnimationFrame(() => {\n\t\t\t\t\t_.style.height = '0px';\n\t\t\t\t});\n\t\t\t});\n\n\t\t\t_.removeAttribute('open');\n\t\t\t_.setAttribute('aria-hidden', 'true');\n\t\t\t_.setAttribute('inert', '');\n\t\t} else {\n\t\t\t// only animate if not already auto\n\t\t\tif (_.style.height !== 'auto') {\n\t\t\t\tif (hasTransition) {\n\t\t\t\t\t_.#animating = true;\n\t\t\t\t}\n\t\t\t\t_.style.height = `${_.scrollHeight}px`;\n\t\t\t}\n\n\t\t\t_.setAttribute('open', '');\n\t\t\t_.removeAttribute('aria-hidden');\n\t\t\t_.removeAttribute('inert');\n\t\t}\n\t}\n\n\t/**\n\t * Gets the collapsed state\n\t * @returns {boolean} Whether element is collapsed\n\t */\n\tget collapsed() {\n\t\treturn !this.hasAttribute('open');\n\t}\n}\n\n// register custom elements if not already defined\nif (!customElements.get('collapsible-content')) {\n\tcustomElements.define('collapsible-content', CollapsibleContent);\n}\nif (!customElements.get('collapsible-component')) {\n\tcustomElements.define('collapsible-component', CollapsibleComponent);\n}\n\nexport { CollapsibleContent, CollapsibleComponent };\n"],"names":[],"mappings":"AAEA;AACA;AACA;AACA,MAAM,oBAAoB,SAAS,WAAW,CAAC;AAC/C,CAAC,YAAY,CAAC;AACd,CAAC,gBAAgB,CAAC;AAClB;AACA;AACA;AACA;AACA,CAAC,WAAW,GAAG;AACf,EAAE,KAAK,EAAE,CAAC;AACV,EAAE,MAAM,CAAC,GAAG,IAAI,CAAC;AACjB;AACA;AACA,EAAE,CAAC,CAAC,MAAM,GAAG,IAAI,CAAC;AAClB,EAAE,CAAC,CAAC,OAAO,GAAG,IAAI,CAAC;AACnB;AACA;AACA,EAAE,CAAC,CAAC,YAAY,GAAG,CAAC,CAAC,KAAK;AAC1B,GAAG,CAAC,CAAC,cAAc,EAAE,CAAC;AACtB;AACA,GAAG,MAAM,QAAQ,GAAG,CAAC,CAAC,MAAM,CAAC,YAAY,CAAC,eAAe,CAAC,KAAK,MAAM,CAAC;AACtE,GAAG,CAAC,CAAC,MAAM,CAAC,YAAY,CAAC,eAAe,EAAE,QAAQ,CAAC,CAAC;AACpD,GAAG,CAAC,CAAC,OAAO,CAAC,SAAS,GAAG,CAAC,QAAQ,CAAC;AACnC,GAAG,CAAC;AACJ,EAAE;AACF;AACA;AACA;AACA;AACA;AACA,CAAC,iBAAiB,GAAG;AACrB,EAAE,MAAM,CAAC,GAAG,IAAI,CAAC;AACjB;AACA;AACA,EAAE,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC;AACvC,EAAE,CAAC,CAAC,OAAO,GAAG,CAAC,CAAC,aAAa,CAAC,qBAAqB,CAAC,CAAC;AACrD;AACA,EAAE,IAAI,CAAC,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC,CAAC,OAAO,EAAE;AAC/B,GAAG,MAAM,KAAK,GAAG,IAAI,KAAK;AAC1B,IAAI,uEAAuE;AAC3E,IAAI,CAAC;AACL,GAAG,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;AAChC,GAAG,CAAC,CAAC,aAAa;AAClB,IAAI,IAAI,WAAW,CAAC,mBAAmB,EAAE;AACzC,KAAK,OAAO,EAAE,IAAI;AAClB,KAAK,MAAM,EAAE,EAAE,KAAK,EAAE;AACtB,KAAK,CAAC;AACN,IAAI,CAAC;AACL,GAAG,OAAO;AACV,GAAG;AACH;AACA;AACA,EAAE,CAAC,CAAC,MAAM,CAAC,EAAE,KAAK,CAAC,mBAAmB,EAAE,MAAM,CAAC,UAAU,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;AAC1E,EAAE,CAAC,CAAC,OAAO,CAAC,EAAE,KAAK,CAAC,oBAAoB,EAAE,MAAM,CAAC,UAAU,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;AAC5E;AACA;AACA,EAAE,CAAC,CAAC,MAAM,CAAC,IAAI,KAAK,QAAQ,CAAC;AAC7B,EAAE,CAAC,CAAC,MAAM,CAAC,YAAY,CAAC,eAAe,EAAE,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;AACvD,EAAE,CAAC,CAAC,OAAO,CAAC,YAAY,CAAC,iBAAiB,EAAE,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;AACzD;AACA;AACA,EAAE,MAAM,IAAI,GAAG,CAAC,CAAC,OAAO,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC;AAC9C,EAAE,CAAC,CAAC,MAAM,CAAC,YAAY,CAAC,eAAe,EAAE,IAAI,CAAC,CAAC;AAC/C,EAAE,CAAC,CAAC,OAAO,CAAC,SAAS,GAAG,CAAC,IAAI,CAAC;AAC9B;AACA;AACA,EAAE,CAAC,CAAC,gBAAgB,GAAG,IAAI,eAAe,EAAE,CAAC;AAC7C,EAAE,CAAC,CAAC,MAAM,CAAC,gBAAgB,CAAC,OAAO,EAAE,CAAC,CAAC,YAAY,EAAE;AACrD,GAAG,MAAM,EAAE,CAAC,CAAC,gBAAgB,CAAC,MAAM;AACpC,GAAG,CAAC,CAAC;AACL,EAAE;AACF;AACA;AACA;AACA;AACA;AACA,CAAC,oBAAoB,GAAG;AACxB,EAAE,MAAM,CAAC,GAAG,IAAI,CAAC;AACjB,EAAE,IAAI,CAAC,CAAC,gBAAgB,EAAE;AAC1B,GAAG,CAAC,CAAC,gBAAgB,CAAC,KAAK,EAAE,CAAC;AAC9B,GAAG,CAAC,CAAC,gBAAgB,GAAG,IAAI,CAAC;AAC7B,GAAG;AACH,EAAE;AACF,CAAC;AACD;AACA;AACA;AACA;AACA,MAAM,kBAAkB,SAAS,WAAW,CAAC;AAC7C,CAAC,oBAAoB,CAAC;AACtB,CAAC,gBAAgB,CAAC;AAClB,CAAC,UAAU,GAAG,KAAK,CAAC;AACpB;AACA;AACA;AACA;AACA,CAAC,WAAW,GAAG;AACf,EAAE,KAAK,EAAE,CAAC;AACV,EAAE,MAAM,CAAC,GAAG,IAAI,CAAC;AACjB;AACA;AACA,EAAE,CAAC,CAAC,oBAAoB,GAAG,CAAC,KAAK,KAAK;AACtC;AACA,GAAG,IAAI,KAAK,CAAC,YAAY,KAAK,QAAQ,EAAE,OAAO;AAC/C;AACA,GAAG,CAAC,CAAC,UAAU,GAAG,KAAK,CAAC;AACxB;AACA;AACA,GAAG,IAAI,CAAC,CAAC,CAAC,SAAS,EAAE;AACrB,IAAI,CAAC,CAAC,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC;AAC5B,IAAI;AACJ,GAAG,CAAC;AACJ,EAAE;AACF;AACA;AACA;AACA;AACA;AACA,CAAC,iBAAiB,GAAG;AACrB,EAAE,MAAM,CAAC,GAAG,IAAI,CAAC;AACjB,EAAE,CAAC,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,YAAY,CAAC,MAAM,CAAC,GAAG,MAAM,GAAG,GAAG,CAAC;AACzD;AACA;AACA,EAAE,CAAC,CAAC,gBAAgB,GAAG,IAAI,eAAe,EAAE,CAAC;AAC7C,EAAE,CAAC,CAAC,gBAAgB,CAAC,eAAe,EAAE,CAAC,CAAC,oBAAoB,EAAE;AAC9D,GAAG,MAAM,EAAE,CAAC,CAAC,gBAAgB,CAAC,MAAM;AACpC,GAAG,CAAC,CAAC;AACL,EAAE;AACF;AACA;AACA;AACA;AACA;AACA,CAAC,oBAAoB,GAAG;AACxB,EAAE,MAAM,CAAC,GAAG,IAAI,CAAC;AACjB,EAAE,IAAI,CAAC,CAAC,gBAAgB,EAAE;AAC1B,GAAG,CAAC,CAAC,gBAAgB,CAAC,KAAK,EAAE,CAAC;AAC9B,GAAG,CAAC,CAAC,gBAAgB,GAAG,IAAI,CAAC;AAC7B,GAAG;AACH,EAAE;AACF;AACA;AACA;AACA;AACA;AACA,CAAC,IAAI,SAAS,CAAC,KAAK,EAAE;AACtB,EAAE,MAAM,CAAC,GAAG,IAAI,CAAC;AACjB;AACA;AACA,EAAE,IAAI,CAAC,CAAC,UAAU,EAAE,OAAO;AAC3B;AACA;AACA,EAAE,MAAM,aAAa,GAAG,gBAAgB,CAAC,CAAC,CAAC,CAAC,kBAAkB,KAAK,IAAI,CAAC;AACxE;AACA,EAAE,IAAI,KAAK,EAAE;AACb,GAAG,IAAI,aAAa,EAAE;AACtB,IAAI,CAAC,CAAC,UAAU,GAAG,IAAI,CAAC;AACxB,IAAI;AACJ;AACA;AACA,GAAG,CAAC,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC,CAAC,YAAY,CAAC,EAAE,CAAC,CAAC;AAC1C;AACA;AACA,GAAG,qBAAqB,CAAC,MAAM;AAC/B,IAAI,qBAAqB,CAAC,MAAM;AAChC,KAAK,CAAC,CAAC,KAAK,CAAC,MAAM,GAAG,KAAK,CAAC;AAC5B,KAAK,CAAC,CAAC;AACP,IAAI,CAAC,CAAC;AACN;AACA,GAAG,CAAC,CAAC,eAAe,CAAC,MAAM,CAAC,CAAC;AAC7B,GAAG,CAAC,CAAC,YAAY,CAAC,aAAa,EAAE,MAAM,CAAC,CAAC;AACzC,GAAG,CAAC,CAAC,YAAY,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;AAC/B,GAAG,MAAM;AACT;AACA,GAAG,IAAI,CAAC,CAAC,KAAK,CAAC,MAAM,KAAK,MAAM,EAAE;AAClC,IAAI,IAAI,aAAa,EAAE;AACvB,KAAK,CAAC,CAAC,UAAU,GAAG,IAAI,CAAC;AACzB,KAAK;AACL,IAAI,CAAC,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC,CAAC,YAAY,CAAC,EAAE,CAAC,CAAC;AAC3C,IAAI;AACJ;AACA,GAAG,CAAC,CAAC,YAAY,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;AAC9B,GAAG,CAAC,CAAC,eAAe,CAAC,aAAa,CAAC,CAAC;AACpC,GAAG,CAAC,CAAC,eAAe,CAAC,OAAO,CAAC,CAAC;AAC9B,GAAG;AACH,EAAE;AACF;AACA;AACA;AACA;AACA;AACA,CAAC,IAAI,SAAS,GAAG;AACjB,EAAE,OAAO,CAAC,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC;AACpC,EAAE;AACF,CAAC;AACD;AACA;AACA,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,qBAAqB,CAAC,EAAE;AAChD,CAAC,cAAc,CAAC,MAAM,CAAC,qBAAqB,EAAE,kBAAkB,CAAC,CAAC;AAClE,CAAC;AACD,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,uBAAuB,CAAC,EAAE;AAClD,CAAC,cAAc,CAAC,MAAM,CAAC,uBAAuB,EAAE,oBAAoB,CAAC,CAAC;AACtE;;;;"}
@@ -9,6 +9,7 @@
9
9
  */
10
10
  class CollapsibleComponent extends HTMLElement {
11
11
  #handleClick;
12
+ #abortController;
12
13
 
13
14
  /**
14
15
  * Initializes the component and sets up references to child elements
@@ -27,7 +28,7 @@
27
28
  // toggle expanded value
28
29
  const expanded = _.button.getAttribute('aria-expanded') !== 'true';
29
30
  _.button.setAttribute('aria-expanded', expanded);
30
- _.content.hidden = !expanded;
31
+ _.content.collapsed = !expanded;
31
32
  };
32
33
  }
33
34
 
@@ -43,7 +44,16 @@
43
44
  _.content = _.querySelector('collapsible-content');
44
45
 
45
46
  if (!_.button || !_.content) {
46
- console.error('CollapsibleComponent requires a <button> and a <collapsible-content>.');
47
+ const error = new Error(
48
+ 'CollapsibleComponent requires a <button> and a <collapsible-content>.'
49
+ );
50
+ console.error(error.message);
51
+ _.dispatchEvent(
52
+ new CustomEvent('collapsible-error', {
53
+ bubbles: true,
54
+ detail: { error },
55
+ })
56
+ );
47
57
  return;
48
58
  }
49
59
 
@@ -52,18 +62,20 @@
52
62
  _.content.id ||= `collapsible-content-${crypto.randomUUID().slice(0, 8)}`;
53
63
 
54
64
  // set accessibility attributes
65
+ _.button.type ||= 'button';
55
66
  _.button.setAttribute('aria-controls', _.content.id);
56
- _.content.setAttribute('role', 'region');
57
67
  _.content.setAttribute('aria-labelledby', _.button.id);
58
68
 
59
- // set initial state based on aria-expanded attribute
60
- const expanded = _.button.getAttribute('aria-expanded') === 'true';
69
+ // set initial state based on open attribute
70
+ const open = _.content.hasAttribute('open');
71
+ _.button.setAttribute('aria-expanded', open);
72
+ _.content.collapsed = !open;
61
73
 
62
- // make sure content state matches button state
63
- _.content.hidden = !expanded;
64
-
65
- // add event listener
66
- _.button.addEventListener('click', _.#handleClick);
74
+ // use AbortController to prevent duplicate listeners on reconnection
75
+ _.#abortController = new AbortController();
76
+ _.button.addEventListener('click', _.#handleClick, {
77
+ signal: _.#abortController.signal,
78
+ });
67
79
  }
68
80
 
69
81
  /**
@@ -72,8 +84,9 @@
72
84
  */
73
85
  disconnectedCallback() {
74
86
  const _ = this;
75
- if (_.button) {
76
- _.button.removeEventListener('click', _.#handleClick);
87
+ if (_.#abortController) {
88
+ _.#abortController.abort();
89
+ _.#abortController = null;
77
90
  }
78
91
  }
79
92
  }
@@ -83,6 +96,8 @@
83
96
  */
84
97
  class CollapsibleContent extends HTMLElement {
85
98
  #handleTransitionEnd;
99
+ #abortController;
100
+ #animating = false;
86
101
 
87
102
  /**
88
103
  * Initializes the content element and binds event handlers
@@ -94,35 +109,30 @@
94
109
  // define event handler using arrow function for proper binding
95
110
  _.#handleTransitionEnd = (event) => {
96
111
  // exit if isn't height property
97
- if (event.propertyName != 'height') return;
112
+ if (event.propertyName !== 'height') return;
113
+
114
+ _.#animating = false;
98
115
 
99
116
  // remove the inline height to allow dynamic content changes
100
- if (!_.hidden) {
117
+ if (!_.collapsed) {
101
118
  _.style.height = 'auto';
102
119
  }
103
120
  };
104
-
105
- // add event listener to remove height after transition
106
- _.addEventListener('transitionend', _.#handleTransitionEnd);
107
121
  }
108
122
 
109
123
  /**
110
124
  * Called when element is added to the DOM
111
- * Sets initial height based on hidden state
125
+ * Sets initial height based on open attribute
112
126
  */
113
127
  connectedCallback() {
114
128
  const _ = this;
129
+ _.style.height = _.hasAttribute('open') ? 'auto' : '0';
115
130
 
116
- // get aria-expanded state from button
117
- const component = _.closest('collapsible-component');
118
- const button = component.querySelector('button');
119
- const expanded = button.getAttribute('aria-expanded') === 'true';
120
-
121
- if (expanded) {
122
- _.style.height = 'auto';
123
- } else {
124
- _.style.height = '0';
125
- }
131
+ // use AbortController to prevent duplicate listeners on reconnection
132
+ _.#abortController = new AbortController();
133
+ _.addEventListener('transitionend', _.#handleTransitionEnd, {
134
+ signal: _.#abortController.signal,
135
+ });
126
136
  }
127
137
 
128
138
  /**
@@ -131,57 +141,74 @@
131
141
  */
132
142
  disconnectedCallback() {
133
143
  const _ = this;
134
- _.removeEventListener('transitionend', _.#handleTransitionEnd);
144
+ if (_.#abortController) {
145
+ _.#abortController.abort();
146
+ _.#abortController = null;
147
+ }
135
148
  }
136
149
 
137
150
  /**
138
- * Handles setting the hidden attribute with animation
139
- * @param {boolean} value - Whether element should be hidden
151
+ * Handles setting the collapsed state with animation
152
+ * @param {boolean} value - Whether element should be collapsed
140
153
  */
141
- set hidden(value) {
154
+ set collapsed(value) {
142
155
  const _ = this;
143
156
 
144
- // animate to closed
157
+ // prevent rapid clicking during animation
158
+ if (_.#animating) return;
159
+
160
+ // check if transitions are enabled (respects prefers-reduced-motion)
161
+ const hasTransition = getComputedStyle(_).transitionDuration !== '0s';
162
+
145
163
  if (value) {
146
- // reset height to animate from
164
+ if (hasTransition) {
165
+ _.#animating = true;
166
+ }
167
+
168
+ // animate to closed
147
169
  _.style.height = `${_.scrollHeight}px`;
148
170
 
149
- // wait one frame and animate to 0
150
- setTimeout(() => {
151
- _.style.height = '0px';
152
- }, 1);
171
+ // use requestAnimationFrame for reliable frame timing
172
+ requestAnimationFrame(() => {
173
+ requestAnimationFrame(() => {
174
+ _.style.height = '0px';
175
+ });
176
+ });
153
177
 
154
- // set accessibility attributes
178
+ _.removeAttribute('open');
155
179
  _.setAttribute('aria-hidden', 'true');
156
180
  _.setAttribute('inert', '');
157
- _.setAttribute('hidden', '');
158
181
  } else {
159
- // only animate if not set to auto
182
+ // only animate if not already auto
160
183
  if (_.style.height !== 'auto') {
161
- // animate to open
184
+ if (hasTransition) {
185
+ _.#animating = true;
186
+ }
162
187
  _.style.height = `${_.scrollHeight}px`;
163
188
  }
164
189
 
165
- // remove accessibility attributes
190
+ _.setAttribute('open', '');
166
191
  _.removeAttribute('aria-hidden');
167
192
  _.removeAttribute('inert');
168
- _.removeAttribute('hidden');
169
193
  }
170
194
  }
171
195
 
172
196
  /**
173
- * Gets the hidden state from the attribute
174
- * @returns {boolean} Whether element is hidden
197
+ * Gets the collapsed state
198
+ * @returns {boolean} Whether element is collapsed
175
199
  */
176
- get hidden() {
177
- const _ = this;
178
- return _.hasAttribute('hidden');
200
+ get collapsed() {
201
+ return !this.hasAttribute('open');
179
202
  }
180
203
  }
181
204
 
182
- // register custom elements
183
- customElements.define('collapsible-content', CollapsibleContent);
184
- customElements.define('collapsible-component', CollapsibleComponent);
205
+ // register custom elements if not already defined
206
+ if (!customElements.get('collapsible-content')) {
207
+ customElements.define('collapsible-content', CollapsibleContent);
208
+ }
209
+ if (!customElements.get('collapsible-component')) {
210
+ customElements.define('collapsible-component', CollapsibleComponent);
211
+ }
185
212
 
186
213
  exports.CollapsibleComponent = CollapsibleComponent;
187
214
  exports.CollapsibleContent = CollapsibleContent;
@@ -1 +1 @@
1
- {"version":3,"file":"collapsible-content.js","sources":["../src/collapsible-content.js"],"sourcesContent":["import './index.scss';\n\n/**\n * Custom element that creates a collapsible/expandable component with proper accessibility\n */\nclass CollapsibleComponent extends HTMLElement {\n\t#handleClick;\n\n\t/**\n\t * Initializes the component and sets up references to child elements\n\t */\n\tconstructor() {\n\t\tsuper();\n\t\tconst _ = this;\n\n\t\t// store references to elements once to avoid re-querying\n\t\t_.button = null;\n\t\t_.content = null;\n\n\t\t// define click handler with proper this binding\n\t\t_.#handleClick = (e) => {\n\t\t\te.preventDefault();\n\t\t\t// toggle expanded value\n\t\t\tconst expanded = _.button.getAttribute('aria-expanded') !== 'true';\n\t\t\t_.button.setAttribute('aria-expanded', expanded);\n\t\t\t_.content.hidden = !expanded;\n\t\t};\n\t}\n\n\t/**\n\t * Called when element is added to the DOM\n\t * Sets up accessibility attributes and event listeners\n\t */\n\tconnectedCallback() {\n\t\tconst _ = this;\n\n\t\t// initialize element references once\n\t\t_.button = _.querySelector('button');\n\t\t_.content = _.querySelector('collapsible-content');\n\n\t\tif (!_.button || !_.content) {\n\t\t\tconsole.error('CollapsibleComponent requires a <button> and a <collapsible-content>.');\n\t\t\treturn;\n\t\t}\n\n\t\t// generate ids if not provided\n\t\t_.button.id ||= `collapsible-button-${crypto.randomUUID().slice(0, 8)}`;\n\t\t_.content.id ||= `collapsible-content-${crypto.randomUUID().slice(0, 8)}`;\n\n\t\t// set accessibility attributes\n\t\t_.button.setAttribute('aria-controls', _.content.id);\n\t\t_.content.setAttribute('role', 'region');\n\t\t_.content.setAttribute('aria-labelledby', _.button.id);\n\n\t\t// set initial state based on aria-expanded attribute\n\t\tconst expanded = _.button.getAttribute('aria-expanded') === 'true';\n\n\t\t// make sure content state matches button state\n\t\t_.content.hidden = !expanded;\n\n\t\t// add event listener\n\t\t_.button.addEventListener('click', _.#handleClick);\n\t}\n\n\t/**\n\t * Called when element is removed from the DOM\n\t * Cleans up event listeners\n\t */\n\tdisconnectedCallback() {\n\t\tconst _ = this;\n\t\tif (_.button) {\n\t\t\t_.button.removeEventListener('click', _.#handleClick);\n\t\t}\n\t}\n}\n\n/**\n * Custom element that provides animated collapsible content\n */\nclass CollapsibleContent extends HTMLElement {\n\t#handleTransitionEnd;\n\n\t/**\n\t * Initializes the content element and binds event handlers\n\t */\n\tconstructor() {\n\t\tsuper();\n\t\tconst _ = this;\n\n\t\t// define event handler using arrow function for proper binding\n\t\t_.#handleTransitionEnd = (event) => {\n\t\t\t// exit if isn't height property\n\t\t\tif (event.propertyName != 'height') return;\n\n\t\t\t// remove the inline height to allow dynamic content changes\n\t\t\tif (!_.hidden) {\n\t\t\t\t_.style.height = 'auto';\n\t\t\t}\n\t\t};\n\n\t\t// add event listener to remove height after transition\n\t\t_.addEventListener('transitionend', _.#handleTransitionEnd);\n\t}\n\n\t/**\n\t * Called when element is added to the DOM\n\t * Sets initial height based on hidden state\n\t */\n\tconnectedCallback() {\n\t\tconst _ = this;\n\n\t\t// get aria-expanded state from button\n\t\tconst component = _.closest('collapsible-component');\n\t\tconst button = component.querySelector('button');\n\t\tconst expanded = button.getAttribute('aria-expanded') === 'true';\n\n\t\tif (expanded) {\n\t\t\t_.style.height = 'auto';\n\t\t} else {\n\t\t\t_.style.height = '0';\n\t\t}\n\t}\n\n\t/**\n\t * Called when element is removed from the DOM\n\t * Cleans up event listeners\n\t */\n\tdisconnectedCallback() {\n\t\tconst _ = this;\n\t\t_.removeEventListener('transitionend', _.#handleTransitionEnd);\n\t}\n\n\t/**\n\t * Handles setting the hidden attribute with animation\n\t * @param {boolean} value - Whether element should be hidden\n\t */\n\tset hidden(value) {\n\t\tconst _ = this;\n\n\t\t// animate to closed\n\t\tif (value) {\n\t\t\t// reset height to animate from\n\t\t\t_.style.height = `${_.scrollHeight}px`;\n\n\t\t\t// wait one frame and animate to 0\n\t\t\tsetTimeout(() => {\n\t\t\t\t_.style.height = '0px';\n\t\t\t}, 1);\n\n\t\t\t// set accessibility attributes\n\t\t\t_.setAttribute('aria-hidden', 'true');\n\t\t\t_.setAttribute('inert', '');\n\t\t\t_.setAttribute('hidden', '');\n\t\t} else {\n\t\t\t// only animate if not set to auto\n\t\t\tif (_.style.height !== 'auto') {\n\t\t\t\t// animate to open\n\t\t\t\t_.style.height = `${_.scrollHeight}px`;\n\t\t\t}\n\n\t\t\t// remove accessibility attributes\n\t\t\t_.removeAttribute('aria-hidden');\n\t\t\t_.removeAttribute('inert');\n\t\t\t_.removeAttribute('hidden');\n\t\t}\n\t}\n\n\t/**\n\t * Gets the hidden state from the attribute\n\t * @returns {boolean} Whether element is hidden\n\t */\n\tget hidden() {\n\t\tconst _ = this;\n\t\treturn _.hasAttribute('hidden');\n\t}\n}\n\n// register custom elements\ncustomElements.define('collapsible-content', CollapsibleContent);\ncustomElements.define('collapsible-component', CollapsibleComponent);\n\nexport { CollapsibleContent, CollapsibleComponent };\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"collapsible-content.js","sources":["../src/collapsible-content.js"],"sourcesContent":["import './collapsible-content.css';\n\n/**\n * Custom element that creates a collapsible/expandable component with proper accessibility\n */\nclass CollapsibleComponent extends HTMLElement {\n\t#handleClick;\n\t#abortController;\n\n\t/**\n\t * Initializes the component and sets up references to child elements\n\t */\n\tconstructor() {\n\t\tsuper();\n\t\tconst _ = this;\n\n\t\t// store references to elements once to avoid re-querying\n\t\t_.button = null;\n\t\t_.content = null;\n\n\t\t// define click handler with proper this binding\n\t\t_.#handleClick = (e) => {\n\t\t\te.preventDefault();\n\t\t\t// toggle expanded value\n\t\t\tconst expanded = _.button.getAttribute('aria-expanded') !== 'true';\n\t\t\t_.button.setAttribute('aria-expanded', expanded);\n\t\t\t_.content.collapsed = !expanded;\n\t\t};\n\t}\n\n\t/**\n\t * Called when element is added to the DOM\n\t * Sets up accessibility attributes and event listeners\n\t */\n\tconnectedCallback() {\n\t\tconst _ = this;\n\n\t\t// initialize element references once\n\t\t_.button = _.querySelector('button');\n\t\t_.content = _.querySelector('collapsible-content');\n\n\t\tif (!_.button || !_.content) {\n\t\t\tconst error = new Error(\n\t\t\t\t'CollapsibleComponent requires a <button> and a <collapsible-content>.'\n\t\t\t);\n\t\t\tconsole.error(error.message);\n\t\t\t_.dispatchEvent(\n\t\t\t\tnew CustomEvent('collapsible-error', {\n\t\t\t\t\tbubbles: true,\n\t\t\t\t\tdetail: { error },\n\t\t\t\t})\n\t\t\t);\n\t\t\treturn;\n\t\t}\n\n\t\t// generate ids if not provided\n\t\t_.button.id ||= `collapsible-button-${crypto.randomUUID().slice(0, 8)}`;\n\t\t_.content.id ||= `collapsible-content-${crypto.randomUUID().slice(0, 8)}`;\n\n\t\t// set accessibility attributes\n\t\t_.button.type ||= 'button';\n\t\t_.button.setAttribute('aria-controls', _.content.id);\n\t\t_.content.setAttribute('aria-labelledby', _.button.id);\n\n\t\t// set initial state based on open attribute\n\t\tconst open = _.content.hasAttribute('open');\n\t\t_.button.setAttribute('aria-expanded', open);\n\t\t_.content.collapsed = !open;\n\n\t\t// use AbortController to prevent duplicate listeners on reconnection\n\t\t_.#abortController = new AbortController();\n\t\t_.button.addEventListener('click', _.#handleClick, {\n\t\t\tsignal: _.#abortController.signal,\n\t\t});\n\t}\n\n\t/**\n\t * Called when element is removed from the DOM\n\t * Cleans up event listeners\n\t */\n\tdisconnectedCallback() {\n\t\tconst _ = this;\n\t\tif (_.#abortController) {\n\t\t\t_.#abortController.abort();\n\t\t\t_.#abortController = null;\n\t\t}\n\t}\n}\n\n/**\n * Custom element that provides animated collapsible content\n */\nclass CollapsibleContent extends HTMLElement {\n\t#handleTransitionEnd;\n\t#abortController;\n\t#animating = false;\n\n\t/**\n\t * Initializes the content element and binds event handlers\n\t */\n\tconstructor() {\n\t\tsuper();\n\t\tconst _ = this;\n\n\t\t// define event handler using arrow function for proper binding\n\t\t_.#handleTransitionEnd = (event) => {\n\t\t\t// exit if isn't height property\n\t\t\tif (event.propertyName !== 'height') return;\n\n\t\t\t_.#animating = false;\n\n\t\t\t// remove the inline height to allow dynamic content changes\n\t\t\tif (!_.collapsed) {\n\t\t\t\t_.style.height = 'auto';\n\t\t\t}\n\t\t};\n\t}\n\n\t/**\n\t * Called when element is added to the DOM\n\t * Sets initial height based on open attribute\n\t */\n\tconnectedCallback() {\n\t\tconst _ = this;\n\t\t_.style.height = _.hasAttribute('open') ? 'auto' : '0';\n\n\t\t// use AbortController to prevent duplicate listeners on reconnection\n\t\t_.#abortController = new AbortController();\n\t\t_.addEventListener('transitionend', _.#handleTransitionEnd, {\n\t\t\tsignal: _.#abortController.signal,\n\t\t});\n\t}\n\n\t/**\n\t * Called when element is removed from the DOM\n\t * Cleans up event listeners\n\t */\n\tdisconnectedCallback() {\n\t\tconst _ = this;\n\t\tif (_.#abortController) {\n\t\t\t_.#abortController.abort();\n\t\t\t_.#abortController = null;\n\t\t}\n\t}\n\n\t/**\n\t * Handles setting the collapsed state with animation\n\t * @param {boolean} value - Whether element should be collapsed\n\t */\n\tset collapsed(value) {\n\t\tconst _ = this;\n\n\t\t// prevent rapid clicking during animation\n\t\tif (_.#animating) return;\n\n\t\t// check if transitions are enabled (respects prefers-reduced-motion)\n\t\tconst hasTransition = getComputedStyle(_).transitionDuration !== '0s';\n\n\t\tif (value) {\n\t\t\tif (hasTransition) {\n\t\t\t\t_.#animating = true;\n\t\t\t}\n\n\t\t\t// animate to closed\n\t\t\t_.style.height = `${_.scrollHeight}px`;\n\n\t\t\t// use requestAnimationFrame for reliable frame timing\n\t\t\trequestAnimationFrame(() => {\n\t\t\t\trequestAnimationFrame(() => {\n\t\t\t\t\t_.style.height = '0px';\n\t\t\t\t});\n\t\t\t});\n\n\t\t\t_.removeAttribute('open');\n\t\t\t_.setAttribute('aria-hidden', 'true');\n\t\t\t_.setAttribute('inert', '');\n\t\t} else {\n\t\t\t// only animate if not already auto\n\t\t\tif (_.style.height !== 'auto') {\n\t\t\t\tif (hasTransition) {\n\t\t\t\t\t_.#animating = true;\n\t\t\t\t}\n\t\t\t\t_.style.height = `${_.scrollHeight}px`;\n\t\t\t}\n\n\t\t\t_.setAttribute('open', '');\n\t\t\t_.removeAttribute('aria-hidden');\n\t\t\t_.removeAttribute('inert');\n\t\t}\n\t}\n\n\t/**\n\t * Gets the collapsed state\n\t * @returns {boolean} Whether element is collapsed\n\t */\n\tget collapsed() {\n\t\treturn !this.hasAttribute('open');\n\t}\n}\n\n// register custom elements if not already defined\nif (!customElements.get('collapsible-content')) {\n\tcustomElements.define('collapsible-content', CollapsibleContent);\n}\nif (!customElements.get('collapsible-component')) {\n\tcustomElements.define('collapsible-component', CollapsibleComponent);\n}\n\nexport { CollapsibleContent, CollapsibleComponent };\n"],"names":[],"mappings":";;;;;;CAEA;CACA;CACA;CACA,MAAM,oBAAoB,SAAS,WAAW,CAAC;CAC/C,CAAC,YAAY,CAAC;CACd,CAAC,gBAAgB,CAAC;AAClB;CACA;CACA;CACA;CACA,CAAC,WAAW,GAAG;CACf,EAAE,KAAK,EAAE,CAAC;CACV,EAAE,MAAM,CAAC,GAAG,IAAI,CAAC;AACjB;CACA;CACA,EAAE,CAAC,CAAC,MAAM,GAAG,IAAI,CAAC;CAClB,EAAE,CAAC,CAAC,OAAO,GAAG,IAAI,CAAC;AACnB;CACA;CACA,EAAE,CAAC,CAAC,YAAY,GAAG,CAAC,CAAC,KAAK;CAC1B,GAAG,CAAC,CAAC,cAAc,EAAE,CAAC;CACtB;CACA,GAAG,MAAM,QAAQ,GAAG,CAAC,CAAC,MAAM,CAAC,YAAY,CAAC,eAAe,CAAC,KAAK,MAAM,CAAC;CACtE,GAAG,CAAC,CAAC,MAAM,CAAC,YAAY,CAAC,eAAe,EAAE,QAAQ,CAAC,CAAC;CACpD,GAAG,CAAC,CAAC,OAAO,CAAC,SAAS,GAAG,CAAC,QAAQ,CAAC;CACnC,GAAG,CAAC;CACJ,EAAE;AACF;CACA;CACA;CACA;CACA;CACA,CAAC,iBAAiB,GAAG;CACrB,EAAE,MAAM,CAAC,GAAG,IAAI,CAAC;AACjB;CACA;CACA,EAAE,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC;CACvC,EAAE,CAAC,CAAC,OAAO,GAAG,CAAC,CAAC,aAAa,CAAC,qBAAqB,CAAC,CAAC;AACrD;CACA,EAAE,IAAI,CAAC,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC,CAAC,OAAO,EAAE;CAC/B,GAAG,MAAM,KAAK,GAAG,IAAI,KAAK;CAC1B,IAAI,uEAAuE;CAC3E,IAAI,CAAC;CACL,GAAG,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;CAChC,GAAG,CAAC,CAAC,aAAa;CAClB,IAAI,IAAI,WAAW,CAAC,mBAAmB,EAAE;CACzC,KAAK,OAAO,EAAE,IAAI;CAClB,KAAK,MAAM,EAAE,EAAE,KAAK,EAAE;CACtB,KAAK,CAAC;CACN,IAAI,CAAC;CACL,GAAG,OAAO;CACV,GAAG;AACH;CACA;CACA,EAAE,CAAC,CAAC,MAAM,CAAC,EAAE,KAAK,CAAC,mBAAmB,EAAE,MAAM,CAAC,UAAU,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;CAC1E,EAAE,CAAC,CAAC,OAAO,CAAC,EAAE,KAAK,CAAC,oBAAoB,EAAE,MAAM,CAAC,UAAU,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;AAC5E;CACA;CACA,EAAE,CAAC,CAAC,MAAM,CAAC,IAAI,KAAK,QAAQ,CAAC;CAC7B,EAAE,CAAC,CAAC,MAAM,CAAC,YAAY,CAAC,eAAe,EAAE,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;CACvD,EAAE,CAAC,CAAC,OAAO,CAAC,YAAY,CAAC,iBAAiB,EAAE,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;AACzD;CACA;CACA,EAAE,MAAM,IAAI,GAAG,CAAC,CAAC,OAAO,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC;CAC9C,EAAE,CAAC,CAAC,MAAM,CAAC,YAAY,CAAC,eAAe,EAAE,IAAI,CAAC,CAAC;CAC/C,EAAE,CAAC,CAAC,OAAO,CAAC,SAAS,GAAG,CAAC,IAAI,CAAC;AAC9B;CACA;CACA,EAAE,CAAC,CAAC,gBAAgB,GAAG,IAAI,eAAe,EAAE,CAAC;CAC7C,EAAE,CAAC,CAAC,MAAM,CAAC,gBAAgB,CAAC,OAAO,EAAE,CAAC,CAAC,YAAY,EAAE;CACrD,GAAG,MAAM,EAAE,CAAC,CAAC,gBAAgB,CAAC,MAAM;CACpC,GAAG,CAAC,CAAC;CACL,EAAE;AACF;CACA;CACA;CACA;CACA;CACA,CAAC,oBAAoB,GAAG;CACxB,EAAE,MAAM,CAAC,GAAG,IAAI,CAAC;CACjB,EAAE,IAAI,CAAC,CAAC,gBAAgB,EAAE;CAC1B,GAAG,CAAC,CAAC,gBAAgB,CAAC,KAAK,EAAE,CAAC;CAC9B,GAAG,CAAC,CAAC,gBAAgB,GAAG,IAAI,CAAC;CAC7B,GAAG;CACH,EAAE;CACF,CAAC;AACD;CACA;CACA;CACA;CACA,MAAM,kBAAkB,SAAS,WAAW,CAAC;CAC7C,CAAC,oBAAoB,CAAC;CACtB,CAAC,gBAAgB,CAAC;CAClB,CAAC,UAAU,GAAG,KAAK,CAAC;AACpB;CACA;CACA;CACA;CACA,CAAC,WAAW,GAAG;CACf,EAAE,KAAK,EAAE,CAAC;CACV,EAAE,MAAM,CAAC,GAAG,IAAI,CAAC;AACjB;CACA;CACA,EAAE,CAAC,CAAC,oBAAoB,GAAG,CAAC,KAAK,KAAK;CACtC;CACA,GAAG,IAAI,KAAK,CAAC,YAAY,KAAK,QAAQ,EAAE,OAAO;AAC/C;CACA,GAAG,CAAC,CAAC,UAAU,GAAG,KAAK,CAAC;AACxB;CACA;CACA,GAAG,IAAI,CAAC,CAAC,CAAC,SAAS,EAAE;CACrB,IAAI,CAAC,CAAC,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC;CAC5B,IAAI;CACJ,GAAG,CAAC;CACJ,EAAE;AACF;CACA;CACA;CACA;CACA;CACA,CAAC,iBAAiB,GAAG;CACrB,EAAE,MAAM,CAAC,GAAG,IAAI,CAAC;CACjB,EAAE,CAAC,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,YAAY,CAAC,MAAM,CAAC,GAAG,MAAM,GAAG,GAAG,CAAC;AACzD;CACA;CACA,EAAE,CAAC,CAAC,gBAAgB,GAAG,IAAI,eAAe,EAAE,CAAC;CAC7C,EAAE,CAAC,CAAC,gBAAgB,CAAC,eAAe,EAAE,CAAC,CAAC,oBAAoB,EAAE;CAC9D,GAAG,MAAM,EAAE,CAAC,CAAC,gBAAgB,CAAC,MAAM;CACpC,GAAG,CAAC,CAAC;CACL,EAAE;AACF;CACA;CACA;CACA;CACA;CACA,CAAC,oBAAoB,GAAG;CACxB,EAAE,MAAM,CAAC,GAAG,IAAI,CAAC;CACjB,EAAE,IAAI,CAAC,CAAC,gBAAgB,EAAE;CAC1B,GAAG,CAAC,CAAC,gBAAgB,CAAC,KAAK,EAAE,CAAC;CAC9B,GAAG,CAAC,CAAC,gBAAgB,GAAG,IAAI,CAAC;CAC7B,GAAG;CACH,EAAE;AACF;CACA;CACA;CACA;CACA;CACA,CAAC,IAAI,SAAS,CAAC,KAAK,EAAE;CACtB,EAAE,MAAM,CAAC,GAAG,IAAI,CAAC;AACjB;CACA;CACA,EAAE,IAAI,CAAC,CAAC,UAAU,EAAE,OAAO;AAC3B;CACA;CACA,EAAE,MAAM,aAAa,GAAG,gBAAgB,CAAC,CAAC,CAAC,CAAC,kBAAkB,KAAK,IAAI,CAAC;AACxE;CACA,EAAE,IAAI,KAAK,EAAE;CACb,GAAG,IAAI,aAAa,EAAE;CACtB,IAAI,CAAC,CAAC,UAAU,GAAG,IAAI,CAAC;CACxB,IAAI;AACJ;CACA;CACA,GAAG,CAAC,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC,CAAC,YAAY,CAAC,EAAE,CAAC,CAAC;AAC1C;CACA;CACA,GAAG,qBAAqB,CAAC,MAAM;CAC/B,IAAI,qBAAqB,CAAC,MAAM;CAChC,KAAK,CAAC,CAAC,KAAK,CAAC,MAAM,GAAG,KAAK,CAAC;CAC5B,KAAK,CAAC,CAAC;CACP,IAAI,CAAC,CAAC;AACN;CACA,GAAG,CAAC,CAAC,eAAe,CAAC,MAAM,CAAC,CAAC;CAC7B,GAAG,CAAC,CAAC,YAAY,CAAC,aAAa,EAAE,MAAM,CAAC,CAAC;CACzC,GAAG,CAAC,CAAC,YAAY,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;CAC/B,GAAG,MAAM;CACT;CACA,GAAG,IAAI,CAAC,CAAC,KAAK,CAAC,MAAM,KAAK,MAAM,EAAE;CAClC,IAAI,IAAI,aAAa,EAAE;CACvB,KAAK,CAAC,CAAC,UAAU,GAAG,IAAI,CAAC;CACzB,KAAK;CACL,IAAI,CAAC,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC,CAAC,YAAY,CAAC,EAAE,CAAC,CAAC;CAC3C,IAAI;AACJ;CACA,GAAG,CAAC,CAAC,YAAY,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;CAC9B,GAAG,CAAC,CAAC,eAAe,CAAC,aAAa,CAAC,CAAC;CACpC,GAAG,CAAC,CAAC,eAAe,CAAC,OAAO,CAAC,CAAC;CAC9B,GAAG;CACH,EAAE;AACF;CACA;CACA;CACA;CACA;CACA,CAAC,IAAI,SAAS,GAAG;CACjB,EAAE,OAAO,CAAC,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC;CACpC,EAAE;CACF,CAAC;AACD;CACA;CACA,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,qBAAqB,CAAC,EAAE;CAChD,CAAC,cAAc,CAAC,MAAM,CAAC,qBAAqB,EAAE,kBAAkB,CAAC,CAAC;CAClE,CAAC;CACD,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,uBAAuB,CAAC,EAAE;CAClD,CAAC,cAAc,CAAC,MAAM,CAAC,uBAAuB,EAAE,oBAAoB,CAAC,CAAC;CACtE;;;;;;;;;"}
@@ -1 +1 @@
1
- :root{--cc-component-display:block;--cc-button-width:100%;--cc-button-text-align:left;--cc-button-background:none;--cc-button-border:none;--cc-button-cursor:pointer;--cc-content-padding:0px;--cc-content-display:block;--cc-content-overflow:hidden;--cc-transition-duration:0.35s;--cc-transition-timing:ease-out}collapsible-component{display:var(--cc-component-display,block)}collapsible-component button{background:var(--cc-button-background,none);border:var(--cc-button-border,none);cursor:var(--cc-button-cursor,pointer);text-align:var(--cc-button-text-align,left);width:var(--cc-button-width,100%)}collapsible-component collapsible-content[hidden],collapsible-content{display:var(--cc-content-display,block)}collapsible-content{overflow:var(--cc-content-overflow,hidden);padding:var(--cc-content-padding,0);transition:height var(--cc-transition-duration,.35s) var(--cc-transition-timing,ease-out)}
1
+ collapsible-component{display:block}collapsible-component button{background:none;border:none;cursor:pointer;text-align:left;width:100%}collapsible-component button:focus-visible{outline:2px solid currentColor;outline-offset:2px}collapsible-content{--collapsible-duration:0.35s;--collapsible-easing:ease-out;display:block;overflow:hidden;transition:height var(--collapsible-duration) var(--collapsible-easing)}@media (prefers-reduced-motion:reduce){collapsible-content{transition:none}}
@@ -1 +1 @@
1
- !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).CollapsibleContent={})}(this,(function(t){"use strict";class CollapsibleComponent extends HTMLElement{#t;constructor(){super();const t=this;t.button=null,t.content=null,t.#t=e=>{e.preventDefault();const n="true"!==t.button.getAttribute("aria-expanded");t.button.setAttribute("aria-expanded",n),t.content.hidden=!n}}connectedCallback(){const t=this;if(t.button=t.querySelector("button"),t.content=t.querySelector("collapsible-content"),!t.button||!t.content)return void console.error("CollapsibleComponent requires a <button> and a <collapsible-content>.");t.button.id||=`collapsible-button-${crypto.randomUUID().slice(0,8)}`,t.content.id||=`collapsible-content-${crypto.randomUUID().slice(0,8)}`,t.button.setAttribute("aria-controls",t.content.id),t.content.setAttribute("role","region"),t.content.setAttribute("aria-labelledby",t.button.id);const e="true"===t.button.getAttribute("aria-expanded");t.content.hidden=!e,t.button.addEventListener("click",t.#t)}disconnectedCallback(){const t=this;t.button&&t.button.removeEventListener("click",t.#t)}}class CollapsibleContent extends HTMLElement{#e;constructor(){super();const t=this;t.#e=e=>{"height"==e.propertyName&&(t.hidden||(t.style.height="auto"))},t.addEventListener("transitionend",t.#e)}connectedCallback(){const t=this,e="true"===t.closest("collapsible-component").querySelector("button").getAttribute("aria-expanded");t.style.height=e?"auto":"0"}disconnectedCallback(){this.removeEventListener("transitionend",this.#e)}set hidden(t){const e=this;t?(e.style.height=`${e.scrollHeight}px`,setTimeout((()=>{e.style.height="0px"}),1),e.setAttribute("aria-hidden","true"),e.setAttribute("inert",""),e.setAttribute("hidden","")):("auto"!==e.style.height&&(e.style.height=`${e.scrollHeight}px`),e.removeAttribute("aria-hidden"),e.removeAttribute("inert"),e.removeAttribute("hidden"))}get hidden(){return this.hasAttribute("hidden")}}customElements.define("collapsible-content",CollapsibleContent),customElements.define("collapsible-component",CollapsibleComponent),t.CollapsibleComponent=CollapsibleComponent,t.CollapsibleContent=CollapsibleContent}));
1
+ !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).CollapsibleContent={})}(this,(function(t){"use strict";class CollapsibleComponent extends HTMLElement{#t;#e;constructor(){super();const t=this;t.button=null,t.content=null,t.#t=e=>{e.preventDefault();const n="true"!==t.button.getAttribute("aria-expanded");t.button.setAttribute("aria-expanded",n),t.content.collapsed=!n}}connectedCallback(){const t=this;if(t.button=t.querySelector("button"),t.content=t.querySelector("collapsible-content"),!t.button||!t.content){const e=new Error("CollapsibleComponent requires a <button> and a <collapsible-content>.");return console.error(e.message),void t.dispatchEvent(new CustomEvent("collapsible-error",{bubbles:!0,detail:{error:e}}))}t.button.id||=`collapsible-button-${crypto.randomUUID().slice(0,8)}`,t.content.id||=`collapsible-content-${crypto.randomUUID().slice(0,8)}`,t.button.type||="button",t.button.setAttribute("aria-controls",t.content.id),t.content.setAttribute("aria-labelledby",t.button.id);const e=t.content.hasAttribute("open");t.button.setAttribute("aria-expanded",e),t.content.collapsed=!e,t.#e=new AbortController,t.button.addEventListener("click",t.#t,{signal:t.#e.signal})}disconnectedCallback(){const t=this;t.#e&&(t.#e.abort(),t.#e=null)}}class CollapsibleContent extends HTMLElement{#n;#e;#o=!1;constructor(){super();const t=this;t.#n=e=>{"height"===e.propertyName&&(t.#o=!1,t.collapsed||(t.style.height="auto"))}}connectedCallback(){const t=this;t.style.height=t.hasAttribute("open")?"auto":"0",t.#e=new AbortController,t.addEventListener("transitionend",t.#n,{signal:t.#e.signal})}disconnectedCallback(){const t=this;t.#e&&(t.#e.abort(),t.#e=null)}set collapsed(t){const e=this;if(e.#o)return;const n="0s"!==getComputedStyle(e).transitionDuration;t?(n&&(e.#o=!0),e.style.height=`${e.scrollHeight}px`,requestAnimationFrame((()=>{requestAnimationFrame((()=>{e.style.height="0px"}))})),e.removeAttribute("open"),e.setAttribute("aria-hidden","true"),e.setAttribute("inert","")):("auto"!==e.style.height&&(n&&(e.#o=!0),e.style.height=`${e.scrollHeight}px`),e.setAttribute("open",""),e.removeAttribute("aria-hidden"),e.removeAttribute("inert"))}get collapsed(){return!this.hasAttribute("open")}}customElements.get("collapsible-content")||customElements.define("collapsible-content",CollapsibleContent),customElements.get("collapsible-component")||customElements.define("collapsible-component",CollapsibleComponent),t.CollapsibleComponent=CollapsibleComponent,t.CollapsibleContent=CollapsibleContent}));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@magic-spells/collapsible-content",
3
- "version": "0.2.0",
3
+ "version": "1.0.0",
4
4
  "description": "Collapsible content web component",
5
5
  "author": "Cory Schulz",
6
6
  "license": "MIT",
@@ -9,7 +9,6 @@
9
9
  "module": "dist/collapsible-content.esm.js",
10
10
  "unpkg": "dist/collapsible-content.min.js",
11
11
  "style": "dist/collapsible-content.min.css",
12
- "sass": "dist/collapsible-content.scss",
13
12
  "exports": {
14
13
  ".": {
15
14
  "import": "./dist/collapsible-content.esm.js",
@@ -17,9 +16,7 @@
17
16
  "default": "./dist/collapsible-content.esm.js"
18
17
  },
19
18
  "./css": "./dist/collapsible-content.css",
20
- "./css/min": "./dist/collapsible-content.min.css",
21
- "./scss": "./dist/collapsible-content.scss",
22
- "./scss/*": "./dist/scss/*"
19
+ "./css/min": "./dist/collapsible-content.min.css"
23
20
  },
24
21
  "sideEffects": true,
25
22
  "repository": {
@@ -60,17 +57,16 @@
60
57
  "not ie <= 11"
61
58
  ],
62
59
  "devDependencies": {
63
- "@eslint/js": "^8.57.0",
60
+ "@eslint/js": "^9.38.0",
64
61
  "@rollup/plugin-node-resolve": "^15.2.3",
65
62
  "@rollup/plugin-terser": "^0.4.4",
66
- "eslint": "^8.0.0",
67
- "globals": "^13.24.0",
63
+ "eslint": "^9.38.0",
64
+ "globals": "^15.15.0",
68
65
  "postcss-lightningcss": "^1.0.1",
69
66
  "prettier": "^3.3.3",
70
67
  "rollup": "^3.0.0",
71
68
  "rollup-plugin-copy": "^3.5.0",
72
69
  "rollup-plugin-postcss": "^4.0.2",
73
- "rollup-plugin-serve": "^1.1.1",
74
- "sass": "^1.86.3"
70
+ "rollup-plugin-serve": "^1.1.1"
75
71
  }
76
72
  }