@daz4126/helium 0.31.0 → 1.0.0-beta.1

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/helium-lite.js ADDED
@@ -0,0 +1,366 @@
1
+ /**
2
+ * Helium Lite - Lightweight reactive library
3
+ * Core features only: @data, @text, @html, @bind, :attr, @ref, @calculate, @effect, @init, events
4
+ * No AJAX, no @import, no SSE, no Turbo
5
+ */
6
+
7
+ const parseEx = v => { try { return Function(`return(${v})`)() } catch { return v } };
8
+ const tryNum = v => { const t = String(v).trim(), n = +t; return t && !isNaN(n) ? n : v };
9
+ const isValidIdentifier = v => /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(v);
10
+ const RESERVED = new Set(['undefined', 'null', 'true', 'false', 'NaN', 'Infinity', 'this', 'arguments']);
11
+ const KEY_MODS = { shift: "shiftKey", ctrl: "ctrlKey", alt: "altKey", meta: "metaKey" };
12
+ const debounce = (f, d) => { let t; return (...a) => (clearTimeout(t), t = setTimeout(f, d, ...a)) };
13
+
14
+ const ATTR_REGEX = /^(@|:|data-he)/;
15
+
16
+ const match = (name, ...attrs) => {
17
+ const p = name.split(/[.:]/)[0];
18
+ if (p === ":" || p === "") return false;
19
+ return attrs.some(a => p === `@${a}` || p === `data-he-${a}`);
20
+ };
21
+
22
+ let HELIUM = null;
23
+
24
+ async function helium(initialState = {}) {
25
+ const inits = [];
26
+ const ALL = Symbol("all");
27
+ const root = document.querySelector('[\\@helium]') || document.querySelector('[data-helium]') || document.body;
28
+
29
+ if (!HELIUM) {
30
+ HELIUM = {
31
+ observer: null,
32
+ bindings: new Map(),
33
+ refs: new Map(),
34
+ listeners: new WeakMap(),
35
+ processed: new WeakSet(),
36
+ parentKeys: new WeakMap(),
37
+ fnCache: new Map(),
38
+ proxyCache: new WeakMap()
39
+ };
40
+ }
41
+
42
+ const $ = s => document.querySelector(s);
43
+ const html = s => Object.assign(document.createElement("template"), { innerHTML: s.trim() }).content.firstChild;
44
+
45
+ const applyingBindings = new Set();
46
+
47
+ const safeApplyBinding = (b) => {
48
+ if (applyingBindings.has(b)) return;
49
+ applyingBindings.add(b);
50
+ try {
51
+ applyBinding(b);
52
+ } finally {
53
+ applyingBindings.delete(b);
54
+ }
55
+ };
56
+
57
+ const handler = {
58
+ has(t, p) {
59
+ if (typeof p === 'string' && p.startsWith('$')) return false;
60
+ if (Reflect.has(t, p)) return true;
61
+ if (typeof globalThis[p] !== 'undefined') return false;
62
+ return true;
63
+ },
64
+ get(t, p, r) {
65
+ const v = Reflect.get(t, p, r);
66
+ if (v && typeof v === "object") {
67
+ if (HELIUM.proxyCache.has(v)) return HELIUM.proxyCache.get(v);
68
+ const proxy = new Proxy(v, handler);
69
+ HELIUM.proxyCache.set(v, proxy);
70
+ HELIUM.parentKeys.set(v, p);
71
+ return proxy;
72
+ }
73
+ return v;
74
+ },
75
+ set: (t, p, v) => {
76
+ const res = Reflect.set(t, p, v);
77
+ HELIUM.bindings.get(p)?.forEach(safeApplyBinding);
78
+ const parentKey = HELIUM.parentKeys.get(t);
79
+ if (parentKey) HELIUM.bindings.get(parentKey)?.forEach(safeApplyBinding);
80
+ HELIUM.bindings.get(ALL)?.forEach(safeApplyBinding);
81
+ return res;
82
+ }
83
+ };
84
+
85
+ const state = new Proxy({}, handler);
86
+ Object.assign(state, initialState);
87
+
88
+ const getRefsObject = () => {
89
+ const refs = {};
90
+ for (const [name, element] of HELIUM.refs) {
91
+ refs[name] = element;
92
+ }
93
+ return refs;
94
+ };
95
+
96
+ const createScope = (el, event = {}) => {
97
+ return {
98
+ $,
99
+ $data: state,
100
+ $event: event,
101
+ $el: el,
102
+ $html: html,
103
+ ...getRefsObject()
104
+ };
105
+ };
106
+
107
+ const compile = (expr, withReturn = true) => {
108
+ const key = `${withReturn}:${expr}`;
109
+ if (HELIUM.fnCache.has(key)) return HELIUM.fnCache.get(key);
110
+ try {
111
+ const fn = new Function(
112
+ "$scope",
113
+ withReturn
114
+ ? `with($scope){with($scope.$data){return(${expr.trim()})}}`
115
+ : `with($scope){with($scope.$data){${expr.trim()}}}`
116
+ );
117
+ const compiled = { execute: (scope) => fn(scope), getIds: null };
118
+ HELIUM.fnCache.set(key, compiled);
119
+ return compiled;
120
+ } catch {
121
+ const compiled = { execute: () => expr, getIds: null };
122
+ HELIUM.fnCache.set(key, compiled);
123
+ return compiled;
124
+ }
125
+ };
126
+
127
+ function applyBinding(b, e = {}, elCtx = b.el) {
128
+ const { el, prop, compiled, calc, isHiddenAttr, isRadio } = b;
129
+ const scope = createScope(elCtx, e);
130
+ const r = compiled.execute(scope);
131
+ if (calc) state[calc] = r;
132
+
133
+ if (prop === "innerHTML") {
134
+ const content = Array.isArray(r) ? r.join`` : r;
135
+ return typeof Idiomorph === "object"
136
+ ? Idiomorph.morph(el, content, { morphStyle: 'innerHTML' })
137
+ : el.innerHTML = content;
138
+ }
139
+ if (prop === "class" && r && typeof r === "object")
140
+ return Object.entries(r).forEach(([k, v]) =>
141
+ k.split(/\s+/).forEach(c => el.classList.toggle(c, v)));
142
+
143
+ if (prop === "style" && r && typeof r === "object")
144
+ return el.style.cssText = Object.entries(r)
145
+ .filter(([_, v]) => v != null && v !== false)
146
+ .map(([k, v]) => `${k.replace(/[A-Z]/g, m => '-' + m.toLowerCase())}:${v}`)
147
+ .join("; ");
148
+
149
+ if (prop === "hidden") {
150
+ el.hidden = isHiddenAttr ? !!r : !r;
151
+ return;
152
+ }
153
+
154
+ if (prop in el) {
155
+ if (isRadio && prop !== "checked") el.checked = el.value === r;
156
+ else if (el.type === "radio" && prop !== "checked") el.checked = el.value === r;
157
+ else el[prop] = prop === "textContent" ? (r ?? '') : parseEx(r);
158
+ return;
159
+ }
160
+
161
+ el.setAttribute(prop, parseEx(r));
162
+ }
163
+
164
+ const trackDependenciesProxy = (compiled, el, excludeChanged = false) => {
165
+ const accessed = excludeChanged ? new Map() : new Set();
166
+ const trackProxy = new Proxy(state, {
167
+ has(target, prop) {
168
+ return true;
169
+ },
170
+ get(target, prop) {
171
+ if (typeof prop == 'string') {
172
+ excludeChanged ? !accessed.has(prop) && accessed.set(prop, target[prop]) : accessed.add(prop);
173
+ }
174
+ const val = target[prop];
175
+ return typeof val == "object" && val != null ? new Proxy(val, this) : val;
176
+ }
177
+ });
178
+
179
+ try {
180
+ const scope = {
181
+ $,
182
+ $data: trackProxy,
183
+ $event: {},
184
+ $el: el,
185
+ $html: html,
186
+ ...getRefsObject()
187
+ };
188
+ compiled.execute(scope);
189
+ } catch {}
190
+
191
+ return excludeChanged ? [...accessed.keys()].filter(prop => state[prop] === accessed.get(prop)) : [...accessed];
192
+ };
193
+
194
+ const getDependencies = (compiled, el, excludeChanged = false) => {
195
+ return trackDependenciesProxy(compiled, el, excludeChanged);
196
+ };
197
+
198
+ const cleanup = el => {
199
+ [el, ...el.querySelectorAll('*')].forEach(e => {
200
+ HELIUM.listeners.get(e)?.forEach(({ receiver, event, handler }) => receiver.removeEventListener(event, handler));
201
+ HELIUM.listeners.delete(e);
202
+ });
203
+ };
204
+
205
+ async function processElements(element) {
206
+ const newBindings = [];
207
+ const deferredBindings = [];
208
+
209
+ const heElements = [element, ...element.querySelectorAll("*")].filter(e => {
210
+ if (HELIUM.processed.has(e)) return false;
211
+ for (let i = 0; i < e.attributes.length; i++) {
212
+ if (ATTR_REGEX.test(e.attributes[i].name)) return true;
213
+ }
214
+ return false;
215
+ });
216
+
217
+ const addBinding = (val, b) => {
218
+ HELIUM.bindings.set(val, b.calc ? [b, ...(HELIUM.bindings.get(val) || [])] : [...(HELIUM.bindings.get(val) || []), b]);
219
+ b.calc ? newBindings.unshift(b) : newBindings.push(b);
220
+ };
221
+
222
+ heElements.forEach(el => {
223
+ HELIUM.processed.add(el);
224
+
225
+ const attrs = el.attributes;
226
+ const inputType = el.type?.toLowerCase();
227
+ const isCheckbox = inputType == "checkbox", isRadio = inputType == "radio", isSelect = el.tagName == "SELECT";
228
+
229
+ for (let i = 0; i < attrs.length; i++) {
230
+ const { name, value } = attrs[i];
231
+
232
+ if (!name.startsWith('@') && !name.startsWith(':') && !name.startsWith('data-he')) continue;
233
+
234
+ // Initialize state first if needed (skip reserved words, only if not already set)
235
+ if (match(name, "text", "html", "bind") && isValidIdentifier(value) && !RESERVED.has(value) && state[value] == null) {
236
+ state[value] = match(name, "bind") ? (el.type == "checkbox" ? el.checked : tryNum(el.value)) : tryNum(el.textContent);
237
+ }
238
+
239
+ // Process the attribute
240
+ if (name === "@data" || name === "data-he") {
241
+ Object.assign(state, parseEx(value));
242
+ }
243
+ else if (name.startsWith(":") || name.startsWith("data-he-attr:")) {
244
+ const propName = name.startsWith(":") ? name.slice(1) : name.slice(13);
245
+ deferredBindings.push({ el, prop: propName, compiled: compile(value, true) });
246
+ }
247
+ else if (match(name, "ref")) {
248
+ HELIUM.refs.set("$" + value, el);
249
+ }
250
+ else if (match(name, "text", "html")) {
251
+ deferredBindings.push({ el, prop: match(name, "text") ? "textContent" : "innerHTML", compiled: compile(value, true) });
252
+ }
253
+ else if (match(name, "bind")) {
254
+ const event = (isCheckbox || isRadio || isSelect) ? "change" : "input";
255
+ const prop = isCheckbox ? "checked" : "value";
256
+ const inputHandler = e => state[value] = isCheckbox ? e.target.checked : e.target.value;
257
+ el.addEventListener(event, inputHandler);
258
+ if (!HELIUM.listeners.has(el)) HELIUM.listeners.set(el, []);
259
+ HELIUM.listeners.get(el).push({ receiver: el, event, handler: inputHandler });
260
+ deferredBindings.push({ el, prop, compiled: compile(value, true), isRadio });
261
+ if (isCheckbox) el.checked = !!state[value];
262
+ else if (isRadio) el.checked = el.value == state[value];
263
+ else el.value = state[value] ?? "";
264
+ }
265
+ else if (match(name, "hidden", "visible")) {
266
+ const isHidden = match(name, "hidden");
267
+ deferredBindings.push({ el, prop: "hidden", compiled: compile(value, true), isHiddenAttr: isHidden });
268
+ }
269
+ else if (match(name, "calculate")) {
270
+ const calcName = name.split(":")[1];
271
+ if (!(calcName in state)) state[calcName] = undefined;
272
+ deferredBindings.push({ el, calc: calcName, prop: null, compiled: compile(value, true) });
273
+ }
274
+ else if (match(name, "effect")) {
275
+ deferredBindings.push({ el, prop: null, compiled: compile(value, true), keys: name.split(":").slice(1) });
276
+ }
277
+ else if (match(name, "init")) {
278
+ inits.push({ compiled: compile(value, true), el });
279
+ }
280
+ else if (name.startsWith("@") || name.startsWith("data-he")) {
281
+ const fullName = name.startsWith("@") ? name.slice(1) : name.slice(8);
282
+ const [event, ...mods] = fullName.split(".");
283
+ const receiver = mods.includes("outside") || mods.includes("document") ? document : el;
284
+ const debounceMod = mods.find(m => m.startsWith("debounce"));
285
+ const debounceDelay = debounceMod ? (t => t && !isNaN(t) ? Number(t) : 300)(debounceMod.split(":")[1]) : 0;
286
+ const _handler = e => {
287
+ if (mods.includes("prevent")) e.preventDefault();
288
+ if (mods.includes("stop")) e.stopPropagation();
289
+ for (const [mod, prop] of Object.entries(KEY_MODS)) if (mods.includes(mod) && !e[prop]) return;
290
+ if (["keydown", "keyup", "keypress"].includes(event)) {
291
+ const last = mods[mods.length - 1];
292
+ if (last && !["prevent", "once", "outside", "document", "stop"].includes(last) && !last.startsWith("debounce") && !Object.keys(KEY_MODS).includes(last)) {
293
+ const keyName = e.key == " " ? "Space" : e.key == "Escape" ? "Esc" : e.key;
294
+ if (keyName.toLowerCase() !== last.toLowerCase()) return;
295
+ }
296
+ }
297
+ if (!mods.includes("outside") || !el.contains(e.target)) {
298
+ const scope = createScope(el, e);
299
+ const compiled = compile(value, true);
300
+ compiled.execute(scope);
301
+ }
302
+ if (mods.includes("once")) receiver.removeEventListener(event, handler);
303
+ };
304
+ const handler = debounceDelay > 0 ? debounce(_handler, debounceDelay) : _handler;
305
+ const eventOptions = mods.includes("once") ? { once: true } : undefined;
306
+ receiver.addEventListener(event, handler, eventOptions);
307
+ if (!HELIUM.listeners.has(el)) HELIUM.listeners.set(el, []);
308
+ HELIUM.listeners.get(el).push({ receiver, event, handler });
309
+ }
310
+ }
311
+ });
312
+
313
+ deferredBindings.forEach(b => {
314
+ const tracked = b.keys?.includes("*") ? [ALL] : getDependencies(b.compiled, b.el, true).concat(b.keys || []);
315
+ tracked.forEach(key => addBinding(key, b));
316
+ if (tracked.length === 0) {
317
+ newBindings.push(b);
318
+ }
319
+ });
320
+
321
+ return newBindings;
322
+ }
323
+
324
+ HELIUM.observer?.disconnect();
325
+ HELIUM.observer = new MutationObserver(async (ms) => {
326
+ for (const m of ms) {
327
+ m.removedNodes.forEach((n) => n.nodeType === 1 && cleanup(n));
328
+
329
+ for (const n of m.addedNodes) {
330
+ if (n.nodeType === 1 && !HELIUM.processed.has(n)) {
331
+ const bindings = await processElements(n);
332
+ bindings?.forEach(applyBinding);
333
+ }
334
+ }
335
+ }
336
+ });
337
+ HELIUM.observer.observe(root, { childList: true, subtree: true });
338
+
339
+ const initialBindings = await processElements(root);
340
+ initialBindings.forEach(applyBinding);
341
+ inits.forEach(({ compiled, el }) => {
342
+ const scope = createScope(el);
343
+ compiled.execute(scope);
344
+ });
345
+
346
+ return state;
347
+ }
348
+
349
+ function heliumTeardown() {
350
+ if (HELIUM?.observer) HELIUM.observer.disconnect();
351
+ HELIUM = null;
352
+ }
353
+
354
+ // Browser globals
355
+ if (typeof window !== 'undefined') {
356
+ window.helium = helium;
357
+ window.heliumTeardown = heliumTeardown;
358
+ }
359
+
360
+ // Auto-initialize
361
+ if (typeof document !== 'undefined') {
362
+ document.addEventListener("DOMContentLoaded", () => helium());
363
+ }
364
+
365
+ export { helium, heliumTeardown };
366
+ export default helium;
@@ -0,0 +1,120 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import { helium, heliumTeardown } from './helium-lite.js';
3
+
4
+ describe('Helium Lite', () => {
5
+ let container;
6
+
7
+ beforeEach(() => {
8
+ container = document.createElement('div');
9
+ container.setAttribute('data-helium', '');
10
+ document.body.appendChild(container);
11
+ });
12
+
13
+ afterEach(() => {
14
+ heliumTeardown();
15
+ document.body.innerHTML = '';
16
+ });
17
+
18
+ describe('Default State Values', () => {
19
+ it('should initialize state from @text element content', async () => {
20
+ container.innerHTML = '<span @text="name">John</span>';
21
+ const state = await helium();
22
+ expect(state.name).toBe('John');
23
+ });
24
+
25
+ it('should initialize state from @bind input value', async () => {
26
+ container.innerHTML = '<input @bind="email" value="test@example.com" />';
27
+ const state = await helium();
28
+ expect(state.email).toBe('test@example.com');
29
+ });
30
+
31
+ it('should initialize numeric state from @text', async () => {
32
+ container.innerHTML = '<span @text="count">42</span>';
33
+ const state = await helium();
34
+ expect(state.count).toBe(42);
35
+ });
36
+
37
+ it('should not override initial state with defaults', async () => {
38
+ container.innerHTML = '<span @text="name">Default</span>';
39
+ const state = await helium({ name: 'Custom' });
40
+ expect(state.name).toBe('Custom');
41
+ });
42
+
43
+ it('should initialize checkbox state from checked attribute', async () => {
44
+ container.innerHTML = '<input type="checkbox" @bind="agreed" checked />';
45
+ const state = await helium();
46
+ expect(state.agreed).toBe(true);
47
+ });
48
+ });
49
+
50
+ describe('Basic Reactivity', () => {
51
+ it('should update text on state change', async () => {
52
+ container.innerHTML = '<div @text="message"></div>';
53
+ const state = await helium({ message: 'Hello' });
54
+ expect(container.querySelector('div').textContent).toBe('Hello');
55
+
56
+ state.message = 'World';
57
+ await new Promise(r => setTimeout(r, 0));
58
+ expect(container.querySelector('div').textContent).toBe('World');
59
+ });
60
+
61
+ it('should handle @bind two-way binding', async () => {
62
+ container.innerHTML = '<input @bind="name" /><span @text="name"></span>';
63
+ const state = await helium({ name: 'John' });
64
+
65
+ const input = container.querySelector('input');
66
+ expect(input.value).toBe('John');
67
+
68
+ input.value = 'Jane';
69
+ input.dispatchEvent(new Event('input'));
70
+ await new Promise(r => setTimeout(r, 0));
71
+
72
+ expect(state.name).toBe('Jane');
73
+ expect(container.querySelector('span').textContent).toBe('Jane');
74
+ });
75
+ });
76
+
77
+ describe('Events', () => {
78
+ it('should handle click events', async () => {
79
+ container.innerHTML = '<button @click="count++">Click</button><span @text="count"></span>';
80
+ const state = await helium({ count: 0 });
81
+
82
+ container.querySelector('button').click();
83
+ await new Promise(r => setTimeout(r, 0));
84
+ expect(state.count).toBe(1);
85
+ });
86
+ });
87
+
88
+ describe('@calculate', () => {
89
+ it('should create computed properties', async () => {
90
+ container.innerHTML = '<div @calculate:double="count * 2"></div><span @text="double"></span>';
91
+ const state = await helium({ count: 5 });
92
+ expect(container.querySelector('span').textContent).toBe('10');
93
+ });
94
+ });
95
+
96
+ describe('@ref', () => {
97
+ it('should create element references', async () => {
98
+ container.innerHTML = '<div @ref="myDiv"></div><button @click="$myDiv.textContent = \'Changed\'">Change</button>';
99
+ await helium({});
100
+
101
+ container.querySelector('button').click();
102
+ await new Promise(r => setTimeout(r, 0));
103
+ expect(container.querySelector('div').textContent).toBe('Changed');
104
+ });
105
+ });
106
+
107
+ describe('Dynamic Attributes', () => {
108
+ it('should bind :class', async () => {
109
+ container.innerHTML = '<div :class="className"></div>';
110
+ await helium({ className: 'active' });
111
+ expect(container.querySelector('div').getAttribute('class')).toBe('active');
112
+ });
113
+
114
+ it('should handle :class with object syntax', async () => {
115
+ container.innerHTML = '<div :class="{ active: isActive }"></div>';
116
+ await helium({ isActive: true });
117
+ expect(container.querySelector('div').classList.contains('active')).toBe(true);
118
+ });
119
+ });
120
+ });
package/helium.js CHANGED
@@ -389,11 +389,14 @@ export function createHelium(options = {}) {
389
389
  const importAttr = el.getAttribute("@import") || el.getAttribute(`data-${prefix}-import`);
390
390
  if (importAttr) {
391
391
  importAttr.split(",").map((m) => m.trim()).forEach((moduleName) => {
392
- // Build path: use as-is for URLs, otherwise add ./ prefix and .js suffix as needed
392
+ // Build path: use as-is for URLs, otherwise resolve relative to document location
393
393
  const isUrl = moduleName.startsWith("http://") || moduleName.startsWith("https://");
394
394
  const hasExtension = moduleName.endsWith(".js");
395
395
  const hasPathPrefix = moduleName.startsWith("/") || moduleName.startsWith("./") || moduleName.startsWith("../");
396
- const path = (isUrl || hasPathPrefix ? "" : "./") + moduleName + (isUrl || hasExtension ? "" : ".js");
396
+ const relativePath = (isUrl || hasPathPrefix ? "" : "./") + moduleName + (isUrl || hasExtension ? "" : ".js");
397
+ // Ensure base URL ends with / so relative paths resolve correctly
398
+ const baseUrl = location.href.endsWith('/') || location.href.includes('.html') ? location.href : location.href + '/';
399
+ const path = isUrl ? relativePath : new URL(relativePath, baseUrl).href;
397
400
  importPromises.push(
398
401
  import(path)
399
402
  .then((module) => {
@@ -434,7 +437,7 @@ export function createHelium(options = {}) {
434
437
  if (!name.startsWith('@') && !name.startsWith(':') && !name.startsWith(`data-${prefix}`)) continue;
435
438
 
436
439
  // Initialize state first if needed (skip reserved words, only if not already set)
437
- if (match(name, "text", "html", "bind") && isValidIdentifier(value) && !RESERVED.has(value) && !(value in state)) {
440
+ if (match(name, "text", "html", "bind") && isValidIdentifier(value) && !RESERVED.has(value) && state[value] == null) {
438
441
  state[value] = match(name, "bind") ? (el.type == "checkbox" ? el.checked : tryNum(el.value)) : tryNum(el.textContent);
439
442
  }
440
443
 
@@ -587,7 +590,16 @@ export function createHelium(options = {}) {
587
590
  }
588
591
 
589
592
  // Create default instance for backwards compatibility
590
- const { helium: defaultHelium, heliumTeardown: defaultTeardown } = createHelium();
593
+ const { helium: defaultHelium, heliumTeardown: _defaultTeardown } = createHelium();
594
+
595
+ // Flag to disable default auto-init (used by helium-csp.js and other variants)
596
+ let defaultDisabled = false;
597
+
598
+ // Wrapped teardown that also prevents future auto-init
599
+ function defaultTeardown() {
600
+ defaultDisabled = true;
601
+ _defaultTeardown();
602
+ }
591
603
 
592
604
  // Setup browser globals and auto-initialization
593
605
  if (typeof window !== 'undefined') {
@@ -597,11 +609,11 @@ if (typeof window !== 'undefined') {
597
609
 
598
610
  // Initialize on load
599
611
  if (typeof document !== 'undefined') {
600
- document.addEventListener("DOMContentLoaded",_ => defaultHelium());
612
+ document.addEventListener("DOMContentLoaded",_ => !defaultDisabled && defaultHelium());
601
613
  // Turbo integration
602
614
  document.addEventListener("turbo:before-render",_ => defaultTeardown());
603
- document.addEventListener("turbo:render",_ => defaultHelium());
615
+ document.addEventListener("turbo:render",_ => !defaultDisabled && defaultHelium());
604
616
  }
605
617
 
606
- export { createHelium };
618
+ export { defaultTeardown };
607
619
  export default defaultHelium;