@daz4126/helium 1.0.0-rc.1 → 1.0.0-rc.3
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 +493 -93
- package/dist/helium-csp-sse.min.js +1 -0
- package/dist/helium-csp.min.js +1 -0
- package/dist/helium-lite.min.js +1 -0
- package/dist/helium-sse.min.js +1 -0
- package/dist/helium.min.js +1 -0
- package/helium-csp-sse.js +6 -0
- package/helium-csp.js +111 -26
- package/helium-lite.js +741 -406
- package/helium-sse.js +61 -0
- package/helium.js +974 -244
- package/jexpr.js +14 -0
- package/package.json +40 -4
- package/.claude/settings.local.json +0 -18
- package/CLAUDE.md +0 -135
- package/LLM_GUIDE.md +0 -138
- package/examples/index-lite.html +0 -396
- package/examples/index.html +0 -486
- package/examples/password-utils.js +0 -80
- package/examples/sse-demo.html +0 -125
- package/examples/sse-server.js +0 -138
- package/examples/sse-simple.html +0 -49
- package/examples/test-password.html +0 -251
- package/examples/todo.css +0 -109
- package/examples/todo.html +0 -46
- package/examples/todo.js +0 -36
- package/helium-csp.test.js +0 -776
- package/helium-lite.test.js +0 -120
- package/helium.test.js +0 -870
- package/jexpr.test.js +0 -551
- package/test-csp.html +0 -50
- package/vitest.config.js +0 -8
package/README.md
CHANGED
|
@@ -18,24 +18,24 @@ It's really simple to use - just sprinkle the magic @attributes into your HTML a
|
|
|
18
18
|
|
|
19
19
|
Helium is designed for developers who want:
|
|
20
20
|
|
|
21
|
-
- **Lightweight** -
|
|
21
|
+
- **Lightweight** - About 5.5–13.3KB minified and gzipped, depending on the build
|
|
22
22
|
- **Powerful** - Declarative JavaScript in your HTML
|
|
23
23
|
- **Zero build step** - Works directly in the browser with no compiling
|
|
24
24
|
- **Easy to learn** - If you know HTML and basic JavaScript, you're ready
|
|
25
25
|
|
|
26
26
|
## Versions
|
|
27
27
|
|
|
28
|
-
Helium comes in three versions so you can pick the right balance of features and size for your project:
|
|
28
|
+
Helium comes in three core versions, with SSE available as an optional add-on, so you can pick the right balance of features and size for your project:
|
|
29
29
|
|
|
30
30
|
### Helium (Standard)
|
|
31
31
|
|
|
32
32
|
The full-featured version with everything included.
|
|
33
33
|
|
|
34
|
-
- All directives (`@text`, `@html`, `@bind`, `@data`, `@ref`, `@calculate`, `@effect`, `@init`, etc.)
|
|
34
|
+
- All directives (`@text`, `@html`, `@for`, `@if`, `@bind`, `@data`, `@ref`, `@calculate`, `@effect`, `@init`, etc.)
|
|
35
35
|
- Event handlers with modifiers
|
|
36
36
|
- HTTP requests (`@get`, `@post`, `@put`, `@patch`, `@delete`)
|
|
37
37
|
- Module imports (`@import`)
|
|
38
|
-
- Server-Sent Events (SSE)
|
|
38
|
+
- Optional Server-Sent Events (SSE) add-on
|
|
39
39
|
- Turbo/Hotwire integration
|
|
40
40
|
|
|
41
41
|
**Best for:** Most projects where you want the full power of Helium without worrying about CSP restrictions.
|
|
@@ -50,7 +50,8 @@ A slimmed-down version with just the core reactivity features.
|
|
|
50
50
|
|
|
51
51
|
- Core directives (`@text`, `@html`, `@bind`, `@data`, `@ref`, `@calculate`, `@effect`, `@init`)
|
|
52
52
|
- Event handlers with modifiers
|
|
53
|
-
-
|
|
53
|
+
- One global state namespace
|
|
54
|
+
- No local `@data`, keyed `@for`, HTTP requests, imports, SSE, or Turbo integration
|
|
54
55
|
|
|
55
56
|
**Best for:** Simple interactive pages where you only need reactivity and don't need server communication or module imports.
|
|
56
57
|
|
|
@@ -60,11 +61,38 @@ import helium from "@daz4126/helium/lite"
|
|
|
60
61
|
|
|
61
62
|
### Helium CSP
|
|
62
63
|
|
|
63
|
-
A Content Security Policy safe version that doesn't require `unsafe-eval`. It uses [jexpr](https://github.com/nicolo-ribaudo/jexpr) as its expression engine instead of `new Function()
|
|
64
|
+
A Content Security Policy safe version that doesn't require `unsafe-eval`. It uses [jexpr](https://github.com/nicolo-ribaudo/jexpr) as its expression engine instead of `new Function()`.
|
|
64
65
|
|
|
65
|
-
- All the same features as the standard version
|
|
66
|
+
- All the same directives and HTTP/Turbo/`@import` features as the standard version
|
|
67
|
+
- Optional Server-Sent Events (SSE) add-on
|
|
68
|
+
- `&&` / `||` / `??` short-circuit correctly, and optional chaining (`a?.b`) is supported
|
|
66
69
|
- CSP-compliant — no `unsafe-eval` needed
|
|
67
70
|
|
|
71
|
+
Because expressions run through a custom parser rather than the JS engine, a couple of syntax features differ from the standard build:
|
|
72
|
+
|
|
73
|
+
- No `new` keyword — use the `$Date(...)` and `$FormData(...)` helpers instead
|
|
74
|
+
- Expressions are single expressions, not arbitrary statements
|
|
75
|
+
|
|
76
|
+
Expressions also run in a **restricted scope**: only side-effect-free builtins
|
|
77
|
+
(`Math`, `Date`, `JSON`, `Object`, `Array`, `parseInt`, `URL`, and friends) are
|
|
78
|
+
available. Capability-bearing globals — `window`, `document`, `fetch`,
|
|
79
|
+
`localStorage`, `sessionStorage`, `navigator`, `location`, `history`, timers,
|
|
80
|
+
and dialogs — are not reachable from expressions, so markup injected by an
|
|
81
|
+
attacker can't use them to read cookies or storage. If a page legitimately
|
|
82
|
+
needs one, opt in explicitly by passing the reference:
|
|
83
|
+
|
|
84
|
+
```javascript
|
|
85
|
+
import helium, { allowGlobals } from "@daz4126/helium/csp";
|
|
86
|
+
|
|
87
|
+
allowGlobals({ localStorage, fetch }); // expose to expressions
|
|
88
|
+
allowGlobals({ fetch: undefined }); // revoke again
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
(`helium.allowGlobals(...)` works too when using the global script build.)
|
|
92
|
+
Note this reduces the attack surface but is not a sandbox: the `$get`/`$post`
|
|
93
|
+
helpers and `$`/`$el`/`$html` magics remain available to any expression that
|
|
94
|
+
can run at all.
|
|
95
|
+
|
|
68
96
|
**Best for:** Projects with strict Content Security Policies that prohibit `unsafe-eval`.
|
|
69
97
|
|
|
70
98
|
```javascript
|
|
@@ -76,12 +104,25 @@ import helium from "@daz4126/helium/csp"
|
|
|
76
104
|
| Feature | Standard | Lite | CSP |
|
|
77
105
|
|---------|----------|------|-----|
|
|
78
106
|
| Core directives | ✅ | ✅ | ✅ |
|
|
107
|
+
| Element-local `@data` | ✅ | ❌ | ✅ |
|
|
108
|
+
| Keyed `@for` | ✅ | ❌ | ✅ |
|
|
109
|
+
| Conditional `@if` | ✅ | ❌ | ✅ |
|
|
79
110
|
| Event handlers | ✅ | ✅ | ✅ |
|
|
80
111
|
| HTTP requests | ✅ | ❌ | ✅ |
|
|
81
112
|
| `@import` | ✅ | ❌ | ✅ |
|
|
82
|
-
| SSE |
|
|
113
|
+
| SSE | Add-on | ❌ | Add-on |
|
|
83
114
|
| Turbo integration | ✅ | ❌ | ✅ |
|
|
84
115
|
| CSP safe | ❌ | ❌ | ✅ |
|
|
116
|
+
| Approx. min+gzip size | 8.79KB | 5.52KB | 12.89KB |
|
|
117
|
+
|
|
118
|
+
Sizes are enforced against the production Terser builds with gzip level 9. The
|
|
119
|
+
CSP figure includes its expression parser. Adding SSE produces 9.17KB Standard
|
|
120
|
+
and 13.27KB CSP bundles.
|
|
121
|
+
|
|
122
|
+
Lite is generated from the same global-state reactive core as Standard. It has
|
|
123
|
+
identical deep/shared reactivity, lifecycle, binding, and cleanup behavior for
|
|
124
|
+
root state, but intentionally excludes element-local data and Standard's other
|
|
125
|
+
extensions.
|
|
85
126
|
|
|
86
127
|
## Installation
|
|
87
128
|
|
|
@@ -104,6 +145,11 @@ Just import from the CDN in a script tag directly in your HTML page:
|
|
|
104
145
|
<script type="module">
|
|
105
146
|
import helium from 'https://cdn.jsdelivr.net/gh/daz-codes/helium/helium-csp.js';
|
|
106
147
|
</script>
|
|
148
|
+
|
|
149
|
+
<!-- Standard + SSE -->
|
|
150
|
+
<script type="module">
|
|
151
|
+
import helium from 'https://cdn.jsdelivr.net/gh/daz-codes/helium/helium-sse.js';
|
|
152
|
+
</script>
|
|
107
153
|
```
|
|
108
154
|
|
|
109
155
|
### NPM
|
|
@@ -115,14 +161,57 @@ npm install @daz4126/helium
|
|
|
115
161
|
Then import the version you need:
|
|
116
162
|
|
|
117
163
|
```javascript
|
|
118
|
-
import helium from "@daz4126/helium"
|
|
119
|
-
import helium from "@daz4126/helium/lite"
|
|
120
|
-
import helium from "@daz4126/helium/csp"
|
|
164
|
+
import helium from "@daz4126/helium" // Standard
|
|
165
|
+
import helium from "@daz4126/helium/lite" // Lite
|
|
166
|
+
import helium from "@daz4126/helium/csp" // CSP
|
|
167
|
+
import helium from "@daz4126/helium/sse" // Standard + SSE
|
|
168
|
+
import helium from "@daz4126/helium/csp/sse" // CSP + SSE
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
For a pre-minified, self-contained bundle, append `/min` to an entry point:
|
|
172
|
+
|
|
173
|
+
```javascript
|
|
174
|
+
import helium from "@daz4126/helium/min"
|
|
175
|
+
import helium from "@daz4126/helium/sse/min"
|
|
121
176
|
```
|
|
122
177
|
|
|
178
|
+
The source entry points remain browser-ready ES modules, so consuming Helium
|
|
179
|
+
does not require a build step.
|
|
180
|
+
|
|
123
181
|
### Automatic Initialization
|
|
124
182
|
|
|
125
|
-
Helium automatically initializes on `DOMContentLoaded`, so you typically don't need to call `helium()` manually
|
|
183
|
+
Helium automatically initializes on `DOMContentLoaded`, so you typically don't need to call `helium()` manually. Calling it manually to provide initial values or functions suppresses the pending automatic initialization, so your state is not replaced when the DOM becomes ready.
|
|
184
|
+
|
|
185
|
+
### Explicit Mounting
|
|
186
|
+
|
|
187
|
+
Use `helium.mount()` when a page has multiple independent Helium roots. It
|
|
188
|
+
accepts a selector or an element and returns the root's state plus an idempotent
|
|
189
|
+
`unmount()` function:
|
|
190
|
+
|
|
191
|
+
```html
|
|
192
|
+
<section id="counter-a">
|
|
193
|
+
<button @click="count++">Increment</button>
|
|
194
|
+
<span @text="count"></span>
|
|
195
|
+
</section>
|
|
196
|
+
|
|
197
|
+
<section id="counter-b">
|
|
198
|
+
<button @click="count++">Increment</button>
|
|
199
|
+
<span @text="count"></span>
|
|
200
|
+
</section>
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
```javascript
|
|
204
|
+
const a = await helium.mount("#counter-a", { count: 0 })
|
|
205
|
+
const b = await helium.mount("#counter-b", { count: 10 })
|
|
206
|
+
|
|
207
|
+
a.state.count++ // Updates only #counter-a
|
|
208
|
+
a.unmount() // Safe to call more than once
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Explicit roots have independent state, bindings, observers, listeners, refs,
|
|
212
|
+
and cleanup. Calling `mount()` suppresses the pending automatic initialization;
|
|
213
|
+
existing single-root pages can continue relying on auto-initialization or
|
|
214
|
+
calling `helium(initialState)` directly.
|
|
126
215
|
|
|
127
216
|
## Helium Attributes
|
|
128
217
|
|
|
@@ -130,7 +219,10 @@ Helium uses custom attributes to add interactivity to HTML elements. To identify
|
|
|
130
219
|
|
|
131
220
|
### @helium
|
|
132
221
|
|
|
133
|
-
This attribute sets the root element. Helium
|
|
222
|
+
This attribute sets the automatically initialized root element. Helium
|
|
223
|
+
attributes are processed on that element and its children. If it is omitted,
|
|
224
|
+
automatic initialization defaults to `document.body`. Explicit
|
|
225
|
+
`helium.mount()` roots do not need this attribute.
|
|
134
226
|
|
|
135
227
|
```html
|
|
136
228
|
<div @helium>
|
|
@@ -173,6 +265,69 @@ Similar to `@text`, but inserts HTML content into the element's innerHTML. Suppo
|
|
|
173
265
|
|
|
174
266
|
**Alias:** `data-he-html`
|
|
175
267
|
|
|
268
|
+
### @for with :key
|
|
269
|
+
|
|
270
|
+
Standard and CSP can render arrays and other iterables as keyed DOM rows. Put
|
|
271
|
+
`@for` and `:key` on a `<template>`; Lite deliberately excludes this feature.
|
|
272
|
+
|
|
273
|
+
```html
|
|
274
|
+
<ul>
|
|
275
|
+
<template @for="item, index in items" :key="item.id">
|
|
276
|
+
<li>
|
|
277
|
+
<span @text="index + 1"></span>.
|
|
278
|
+
<span @text="item.name"></span>
|
|
279
|
+
<button @click="item.done = !item.done">Toggle</button>
|
|
280
|
+
</li>
|
|
281
|
+
</template>
|
|
282
|
+
</ul>
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
The optional second alias is the current zero-based index:
|
|
286
|
+
|
|
287
|
+
```html
|
|
288
|
+
<template @for="item in items" :key="item.id">
|
|
289
|
+
<!-- item is available to every Helium expression in this row -->
|
|
290
|
+
</template>
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
Keys must be stable and unique. When the collection changes, Helium moves and
|
|
294
|
+
reuses rows with matching keys, creates new rows, and removes missing rows. This
|
|
295
|
+
preserves element identity, focus, listeners, and `@init` lifecycle state while
|
|
296
|
+
still updating `item`, `index`, and normal root-state dependencies. Cleanup
|
|
297
|
+
functions returned by `@init` run when their keyed row is removed.
|
|
298
|
+
|
|
299
|
+
`@html="items.map(...).join('')"` remains useful for simple generated markup,
|
|
300
|
+
but keyed `@for` avoids HTML-string interpolation and lets each row use normal
|
|
301
|
+
`@text`, dynamic attributes, events, and other directives.
|
|
302
|
+
|
|
303
|
+
**Alias:** `data-he-for` (the key remains `:key`)
|
|
304
|
+
|
|
305
|
+
### @if
|
|
306
|
+
|
|
307
|
+
Standard and CSP can conditionally render a `<template>`'s content. Unlike
|
|
308
|
+
`@hidden`/`@visible`, which only toggle visibility, `@if` adds the content to
|
|
309
|
+
the DOM when the expression is truthy and fully removes it — bindings,
|
|
310
|
+
listeners, and `@init` cleanup included — when it becomes falsy:
|
|
311
|
+
|
|
312
|
+
```html
|
|
313
|
+
<template @if="loggedIn">
|
|
314
|
+
<p>Welcome back, <span @text="user.name"></span>!</p>
|
|
315
|
+
<button @click="logout($data)">Log out</button>
|
|
316
|
+
</template>
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
Content that is switched off costs nothing: its expressions never evaluate and
|
|
320
|
+
its listeners don't exist. Use `@if` for anything conditional-but-expensive
|
|
321
|
+
(modals, tab panels, admin-only markup) and `@hidden`/`@visible` for cheap
|
|
322
|
+
show/hide toggles where you want to preserve element state such as form input.
|
|
323
|
+
|
|
324
|
+
`@if` requires a `<template>` element (like `@for`, and like Alpine's `x-if`)
|
|
325
|
+
because template content is inert until inserted — that's what makes the "off"
|
|
326
|
+
state free. It cannot be combined with `@for` on the same template; nest one
|
|
327
|
+
template inside the other instead. Lite excludes this feature.
|
|
328
|
+
|
|
329
|
+
**Alias:** `data-he-if`
|
|
330
|
+
|
|
176
331
|
### @bind
|
|
177
332
|
|
|
178
333
|
Creates a 2-way binding between an input element's value and a variable. Whatever is entered in the following input field will be stored as a variable called name:
|
|
@@ -187,6 +342,13 @@ Works with:
|
|
|
187
342
|
- Radio buttons (binds to `value`, checking the one that matches)
|
|
188
343
|
- Select elements (binds to `value`)
|
|
189
344
|
|
|
345
|
+
Nested paths work too — the parent object must already exist in state:
|
|
346
|
+
|
|
347
|
+
```html
|
|
348
|
+
<input @bind="user.name">
|
|
349
|
+
<p @text="user.name"></p>
|
|
350
|
+
```
|
|
351
|
+
|
|
190
352
|
**Examples:**
|
|
191
353
|
```html
|
|
192
354
|
<!-- Text input -->
|
|
@@ -209,7 +371,38 @@ Works with:
|
|
|
209
371
|
</select>
|
|
210
372
|
```
|
|
211
373
|
|
|
212
|
-
|
|
374
|
+
**`.number` modifier:** append `.number` to coerce the bound value to a number
|
|
375
|
+
(handy for `type="number"` inputs, which otherwise bind as strings). An empty
|
|
376
|
+
field stays `""`, and non-numeric input is left untouched so partial typing
|
|
377
|
+
still works.
|
|
378
|
+
|
|
379
|
+
```html
|
|
380
|
+
<input type="number" @bind.number="age">
|
|
381
|
+
<p @text="age + 1"></p> <!-- numeric addition, not string concatenation -->
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
**`.trim` modifier:** trim leading and trailing whitespace before storing user
|
|
385
|
+
input in state:
|
|
386
|
+
|
|
387
|
+
```html
|
|
388
|
+
<input @bind.trim="name">
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
**`.lazy` modifier:** update state on the element's `change` event rather than
|
|
392
|
+
on every `input` event. For text fields this normally means when the edit is
|
|
393
|
+
committed or the field loses focus:
|
|
394
|
+
|
|
395
|
+
```html
|
|
396
|
+
<input @bind.lazy="search">
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
Modifiers can be combined in any order:
|
|
400
|
+
|
|
401
|
+
```html
|
|
402
|
+
<input @bind.lazy.trim.number="amount">
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
**Alias:** `data-he-bind` (for example `data-he-bind.lazy.trim`)
|
|
213
406
|
|
|
214
407
|
### @hidden & @visible
|
|
215
408
|
|
|
@@ -224,32 +417,76 @@ Makes the element hidden or visible depending on the result of a JavaScript expr
|
|
|
224
417
|
|
|
225
418
|
### @data
|
|
226
419
|
|
|
227
|
-
Initializes variables that can be used in JavaScript expressions.
|
|
420
|
+
Initializes variables that can be used in JavaScript expressions. In Standard
|
|
421
|
+
and CSP, `@data` behaves like Alpine's `x-data`: root-level `@data` initializes
|
|
422
|
+
the mounted root state, while nested `@data` creates local state for that
|
|
423
|
+
element and its descendants. Lite always uses one global state namespace, so
|
|
424
|
+
every `@data` merges into root state.
|
|
228
425
|
|
|
229
426
|
```html
|
|
230
|
-
<
|
|
427
|
+
<main @helium @data="{ count: 100, name: 'Helium' }">
|
|
428
|
+
<p @text="name + ': ' + count"></p>
|
|
429
|
+
</main>
|
|
231
430
|
```
|
|
232
431
|
|
|
233
|
-
|
|
432
|
+
Nested `@data` is local in Standard/CSP:
|
|
234
433
|
|
|
235
434
|
```html
|
|
236
|
-
<
|
|
237
|
-
<
|
|
238
|
-
|
|
435
|
+
<main @helium @data="{ count: 100 }">
|
|
436
|
+
<section @data="{ count: 0, open: false }">
|
|
437
|
+
<button @click="count++">Increment local count</button>
|
|
438
|
+
<button @click="open = !open">Toggle</button>
|
|
439
|
+
<p @text="count"></p>
|
|
440
|
+
<p @visible="open">Local content</p>
|
|
441
|
+
</section>
|
|
442
|
+
|
|
443
|
+
<p @text="count"></p> <!-- Still 100 -->
|
|
444
|
+
</main>
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
The object is evaluated once in its parent context, so local defaults can use
|
|
448
|
+
outer values. Nested data objects inherit outer names, and assignment updates
|
|
449
|
+
the nearest data object that declares that name:
|
|
450
|
+
|
|
451
|
+
```html
|
|
452
|
+
<div @data="{ count: startingCount, label: 'outer' }">
|
|
453
|
+
<div @data="{ label: 'inner' }">
|
|
454
|
+
<button @click="count++">Updates the outer local count</button>
|
|
455
|
+
<span @text="label"></span> <!-- inner -->
|
|
456
|
+
</div>
|
|
239
457
|
</div>
|
|
240
458
|
```
|
|
241
459
|
|
|
242
|
-
|
|
460
|
+
Declare every name that should remain local. Assigning an undeclared name writes
|
|
461
|
+
to root state. The same applies to `@bind` and `@calculate` targets: declare
|
|
462
|
+
their target names in local `@data` when they should be local. Inside the
|
|
463
|
+
subtree, `$data` is the scoped state view. Local values are not returned from
|
|
464
|
+
`helium()` and are not included in root `@local-storage` persistence.
|
|
465
|
+
|
|
466
|
+
**Alias:** `data-he-data` (the legacy bare `data-he` alias is also accepted)
|
|
243
467
|
|
|
244
468
|
### @ref
|
|
245
469
|
|
|
246
|
-
Creates a reference to
|
|
470
|
+
Creates a scoped reference to an element. The first matching element is
|
|
471
|
+
available through `$name`, and every matching element is available as the
|
|
472
|
+
always-array `$refs.name` in current DOM order:
|
|
247
473
|
|
|
248
474
|
```html
|
|
249
|
-
<
|
|
475
|
+
<section @data="{ items: [] }">
|
|
476
|
+
<ul @ref="list"></ul>
|
|
477
|
+
<div @ref="row"></div>
|
|
478
|
+
<div @ref="row"></div>
|
|
479
|
+
<button @click="console.log($list, $refs.row)">Inspect refs</button>
|
|
480
|
+
</section>
|
|
250
481
|
```
|
|
251
482
|
|
|
252
|
-
|
|
483
|
+
Ref lookup starts in the nearest explicit `@data` scope and falls back through
|
|
484
|
+
parent scopes. A ref with the same name in a nested scope shadows the outer
|
|
485
|
+
name. Lite has one root scope, so all of its refs belong to the mounted root.
|
|
486
|
+
When refs are removed or reordered, singular and plural lookup follows the
|
|
487
|
+
current DOM.
|
|
488
|
+
|
|
489
|
+
This element can be accessed in other JavaScript expressions as `$list`:
|
|
253
490
|
|
|
254
491
|
```html
|
|
255
492
|
<button @click="$list.appendChild($html('<li>New item</li>'))">Add Task</button>
|
|
@@ -266,6 +503,22 @@ A JavaScript expression that will run once when Helium initializes. Useful for s
|
|
|
266
503
|
<div @init="console.log('Helium initialized!')"></div>
|
|
267
504
|
```
|
|
268
505
|
|
|
506
|
+
If the expression returns a function, Helium calls it when the element is removed or when Helium is torn down. This is useful for releasing timers, observers, subscriptions, and other resources:
|
|
507
|
+
|
|
508
|
+
```html
|
|
509
|
+
<div @init="start_clock($el, $data)"></div>
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
```js
|
|
513
|
+
function start_clock(element, data) {
|
|
514
|
+
const timer = setInterval(() => {
|
|
515
|
+
element.textContent = new Date().toLocaleTimeString()
|
|
516
|
+
}, 1000)
|
|
517
|
+
|
|
518
|
+
return () => clearInterval(timer)
|
|
519
|
+
}
|
|
520
|
+
```
|
|
521
|
+
|
|
269
522
|
**Alias:** `data-he-init`
|
|
270
523
|
|
|
271
524
|
### @calculate
|
|
@@ -423,11 +676,22 @@ You can add modifiers by appending them with a dot (`.`) after the event name:
|
|
|
423
676
|
- **once** - Only runs the event handler once, then removes the listener
|
|
424
677
|
- **outside** - Only fires when the event happens outside the element
|
|
425
678
|
- **document** - Attaches the listener to the document instead of the element
|
|
679
|
+
- **window** - Attaches the listener to `window` instead of the element
|
|
680
|
+
- **self** - Only fires when the element itself is the event target
|
|
681
|
+
- **capture** - Handles the event during the capture phase
|
|
682
|
+
- **passive** - Registers a native passive listener
|
|
426
683
|
- **debounce** - Debounces the event handler (default 300ms)
|
|
427
684
|
- **debounce:500** - Debounces with custom delay in milliseconds
|
|
685
|
+
- **throttle** - Runs immediately, then at most once per interval (default 300ms)
|
|
686
|
+
- **throttle:500** - Throttles with a custom interval in milliseconds
|
|
428
687
|
- **shift, ctrl, alt, meta** - Only fires if the modifier key is pressed
|
|
429
688
|
- **Key names** - For keyboard events, specify which key (e.g., `enter`, `esc`, `space`)
|
|
430
689
|
|
|
690
|
+
Throttle uses leading and trailing calls: the first matching event runs
|
|
691
|
+
immediately, and events received during the interval are coalesced into one
|
|
692
|
+
final call using the latest event. `.passive.prevent` is invalid because a
|
|
693
|
+
passive listener cannot cancel an event; Helium warns and does not register it.
|
|
694
|
+
|
|
431
695
|
**Examples:**
|
|
432
696
|
|
|
433
697
|
```html
|
|
@@ -457,9 +721,15 @@ You can add modifiers by appending them with a dot (`.`) after the event name:
|
|
|
457
721
|
|
|
458
722
|
<!-- Listen on document level -->
|
|
459
723
|
<div @keydown.document.esc="closeModal()">Press ESC anywhere</div>
|
|
724
|
+
|
|
725
|
+
<!-- Window listener with throttled updates -->
|
|
726
|
+
<div @resize.window.throttle:100="measureLayout()"></div>
|
|
727
|
+
|
|
728
|
+
<!-- Ignore clicks that originated in descendants -->
|
|
729
|
+
<button @click.self="select()"><span>Select</span></button>
|
|
460
730
|
```
|
|
461
731
|
|
|
462
|
-
**Alias:** Prepend the event name with `data-he
|
|
732
|
+
**Alias:** Prepend the event name with `data-he-`, for example `data-he-click="count++"`
|
|
463
733
|
|
|
464
734
|
## HTTP Requests
|
|
465
735
|
|
|
@@ -643,6 +913,44 @@ Additional fetch options (as an object):
|
|
|
643
913
|
- `data-he-loading`
|
|
644
914
|
- `data-he-options`
|
|
645
915
|
|
|
916
|
+
### Server-Sent Events (optional)
|
|
917
|
+
|
|
918
|
+
SSE parsing is kept out of the core build. Select the add-on entry point when a
|
|
919
|
+
page needs it:
|
|
920
|
+
|
|
921
|
+
```javascript
|
|
922
|
+
import helium from "@daz4126/helium/sse"
|
|
923
|
+
// Strict CSP: import helium from "@daz4126/helium/csp/sse"
|
|
924
|
+
```
|
|
925
|
+
|
|
926
|
+
Use the normal HTTP directives. An explicit `@target` is applied to every SSE
|
|
927
|
+
message:
|
|
928
|
+
|
|
929
|
+
```html
|
|
930
|
+
<button @get="/api/events" @target="#events:append">Connect</button>
|
|
931
|
+
<div id="events"></div>
|
|
932
|
+
```
|
|
933
|
+
|
|
934
|
+
When `@target` is omitted, an SSE `event:` field can name a CSS selector or a
|
|
935
|
+
state property:
|
|
936
|
+
|
|
937
|
+
```text
|
|
938
|
+
event: #status
|
|
939
|
+
data: Connected
|
|
940
|
+
```
|
|
941
|
+
|
|
942
|
+
Set `retryMode` and an optional initial `retry` delay through `@options` to
|
|
943
|
+
reconnect after the stream closes or fails. A server-supplied `retry:` field
|
|
944
|
+
updates the delay and `id:` is sent back as `Last-Event-ID` on reconnection.
|
|
945
|
+
|
|
946
|
+
```html
|
|
947
|
+
<button @get="/api/events"
|
|
948
|
+
@target="#events:append"
|
|
949
|
+
@options="{ retryMode: 'error', retry: 3000 }">
|
|
950
|
+
Connect
|
|
951
|
+
</button>
|
|
952
|
+
```
|
|
953
|
+
|
|
646
954
|
### Complete Example
|
|
647
955
|
|
|
648
956
|
```html
|
|
@@ -737,13 +1045,26 @@ You can also use a string:
|
|
|
737
1045
|
These special variables are available in all JavaScript expressions:
|
|
738
1046
|
|
|
739
1047
|
### $
|
|
740
|
-
|
|
1048
|
+
The `$` query helper preserves `document.querySelector` as its default and adds
|
|
1049
|
+
array-returning plural and scoped forms:
|
|
741
1050
|
|
|
742
1051
|
```html
|
|
743
1052
|
<div @click="$('#header').classList.add('active')">Activate Header!</div>
|
|
744
1053
|
<button @click="$('.sidebar').style.display = 'none'">Hide Sidebar</button>
|
|
745
1054
|
```
|
|
746
1055
|
|
|
1056
|
+
```js
|
|
1057
|
+
$(selector) // first match in document
|
|
1058
|
+
$.all(selector) // all matches in document
|
|
1059
|
+
$.root(selector) // first descendant match in $root
|
|
1060
|
+
$.root.all(selector) // all descendant matches in $root
|
|
1061
|
+
$.scope(selector) // first descendant match in $scope
|
|
1062
|
+
$.scope.all(selector) // all descendant matches in $scope
|
|
1063
|
+
```
|
|
1064
|
+
|
|
1065
|
+
The plural forms return normal arrays. Like `querySelector`, the root and scope
|
|
1066
|
+
forms search descendants and do not include `$root` or `$scope` themselves.
|
|
1067
|
+
|
|
747
1068
|
### $el
|
|
748
1069
|
Reference to the current element:
|
|
749
1070
|
|
|
@@ -753,6 +1074,17 @@ Reference to the current element:
|
|
|
753
1074
|
<input @input="console.log($el.value)">
|
|
754
1075
|
```
|
|
755
1076
|
|
|
1077
|
+
### $root
|
|
1078
|
+
|
|
1079
|
+
The actual root element owned by the current Helium instance. This is the
|
|
1080
|
+
element selected by `@helium`/`data-helium`, the element passed to
|
|
1081
|
+
`helium.mount()`, or `document.body` for the default fallback root.
|
|
1082
|
+
|
|
1083
|
+
### $scope
|
|
1084
|
+
|
|
1085
|
+
The nearest element that declares local `@data` state, falling back to `$root`.
|
|
1086
|
+
Lite has no local data scopes, so `$scope` and `$root` are always the same.
|
|
1087
|
+
|
|
756
1088
|
### $event
|
|
757
1089
|
The event object (available in event handlers):
|
|
758
1090
|
|
|
@@ -762,6 +1094,21 @@ The event object (available in event handlers):
|
|
|
762
1094
|
<form @submit="$event.preventDefault(); handleSubmit()">
|
|
763
1095
|
```
|
|
764
1096
|
|
|
1097
|
+
### $dispatch
|
|
1098
|
+
|
|
1099
|
+
Dispatch a bubbling, cancelable `CustomEvent` from the current element. The
|
|
1100
|
+
event is returned, so callers can inspect `defaultPrevented`:
|
|
1101
|
+
|
|
1102
|
+
```html
|
|
1103
|
+
<section @cart:add="total += $event.detail.price">
|
|
1104
|
+
<button @click="$dispatch('cart:add', { price: 12 })">Add</button>
|
|
1105
|
+
</section>
|
|
1106
|
+
```
|
|
1107
|
+
|
|
1108
|
+
The signature is `$dispatch(name, detail = {}, options = {})`. Options can set
|
|
1109
|
+
`target` (default `$el`), `bubbles` (default `true`), and `cancelable` (default
|
|
1110
|
+
`true`).
|
|
1111
|
+
|
|
765
1112
|
### $data
|
|
766
1113
|
The reactive data object containing all Helium variables:
|
|
767
1114
|
|
|
@@ -795,12 +1142,19 @@ HTTP request functions that can be called programmatically:
|
|
|
795
1142
|
|
|
796
1143
|
The arguments are `url`,`params` (not for `$get`) and `options`. `options` is an object that can include the properties `loading`,`target`, `template`
|
|
797
1144
|
|
|
798
|
-
###
|
|
799
|
-
|
|
1145
|
+
### Named refs
|
|
1146
|
+
|
|
1147
|
+
The first element marked with `@ref="name"` in the nearest defining scope is
|
|
1148
|
+
available as `$name`. `$refs.name` is an array of every matching ref in that
|
|
1149
|
+
scope, in current DOM order:
|
|
800
1150
|
|
|
801
1151
|
```html
|
|
802
1152
|
<input @ref="username">
|
|
803
1153
|
<button @click="console.log($username.value)">Log Username</button>
|
|
1154
|
+
|
|
1155
|
+
<div @ref="item"></div>
|
|
1156
|
+
<div @ref="item"></div>
|
|
1157
|
+
<button @click="console.log($refs.item.length)">Log item count</button>
|
|
804
1158
|
```
|
|
805
1159
|
|
|
806
1160
|
## Functions
|
|
@@ -899,6 +1253,29 @@ This is the recommended approach when you need to update variables:
|
|
|
899
1253
|
|
|
900
1254
|
## Advanced Features
|
|
901
1255
|
|
|
1256
|
+
### Batched Updates
|
|
1257
|
+
|
|
1258
|
+
State changes are batched: a burst of synchronous mutations triggers a single
|
|
1259
|
+
DOM update on the next microtask, and each affected binding runs once no matter
|
|
1260
|
+
how many mutations fed it. This keeps loops over state cheap — updating 100
|
|
1261
|
+
items in a keyed list re-renders the list once, not 100 times:
|
|
1262
|
+
|
|
1263
|
+
```javascript
|
|
1264
|
+
// One DOM update, not 100
|
|
1265
|
+
for (let i = 0; i < 100; i++) state.items[i].done = true;
|
|
1266
|
+
```
|
|
1267
|
+
|
|
1268
|
+
Two practical consequences:
|
|
1269
|
+
|
|
1270
|
+
- **Reading the DOM right after a mutation shows the old value.** Await a
|
|
1271
|
+
microtask first: `state.count = 5; await Promise.resolve();` (in tests, a
|
|
1272
|
+
`setTimeout(0)` tick also covers content inserted by `@if`/`@for`, which is
|
|
1273
|
+
processed asynchronously by the MutationObserver).
|
|
1274
|
+
- **Assigning a value that is already current is a no-op** — it doesn't
|
|
1275
|
+
re-trigger bindings, so effects that write unchanged state settle instead of
|
|
1276
|
+
looping. A binding cascade that *never* stops writing new values (e.g.
|
|
1277
|
+
`@effect:*="count++"`) is detected and reported after 100 passes.
|
|
1278
|
+
|
|
902
1279
|
### DOM Morphing with Idiomorph
|
|
903
1280
|
|
|
904
1281
|
By default, when you update innerHTML with `@html`, Helium replaces the entire content. This can cause issues like losing focus, resetting scroll positions, or interrupting animations.
|
|
@@ -1004,7 +1381,11 @@ The token is automatically included in POST, PUT, PATCH, and DELETE requests to
|
|
|
1004
1381
|
|
|
1005
1382
|
### Content Security Policy
|
|
1006
1383
|
|
|
1007
|
-
|
|
1384
|
+
The standard and lite builds use `new Function()` and therefore require `unsafe-eval`. For a strict Content Security Policy, use the CSP build instead:
|
|
1385
|
+
|
|
1386
|
+
```js
|
|
1387
|
+
import helium from "@daz4126/helium/csp"
|
|
1388
|
+
```
|
|
1008
1389
|
|
|
1009
1390
|
## Best Practices
|
|
1010
1391
|
|
|
@@ -1037,6 +1418,18 @@ If you're using a Content Security Policy, note that Helium uses `new Function()
|
|
|
1037
1418
|
<div @effect:page="analytics.track('page_view', { page })"></div>
|
|
1038
1419
|
```
|
|
1039
1420
|
|
|
1421
|
+
**Benchmarking:**
|
|
1422
|
+
|
|
1423
|
+
The repo includes a head-to-head runtime benchmark against Alpine.js, React,
|
|
1424
|
+
and Datastar (keyed list create/update/swap/clear plus rapid counter updates,
|
|
1425
|
+
run in real Chromium; Datastar only runs the counter test since it renders
|
|
1426
|
+
lists server-side). Run it with:
|
|
1427
|
+
|
|
1428
|
+
```bash
|
|
1429
|
+
npm run bench # defaults: 1000 rows, 7 runs
|
|
1430
|
+
node scripts/benchmark.mjs --rows 5000 --runs 5
|
|
1431
|
+
```
|
|
1432
|
+
|
|
1040
1433
|
### Structuring Larger Apps
|
|
1041
1434
|
|
|
1042
1435
|
**Organize state at the root:**
|
|
@@ -1093,22 +1486,19 @@ If you're using a Content Security Policy, note that Helium uses `new Function()
|
|
|
1093
1486
|
|
|
1094
1487
|
### Common Pitfalls
|
|
1095
1488
|
|
|
1096
|
-
|
|
1489
|
+
**Arrays and objects are reactive:**
|
|
1097
1490
|
|
|
1098
|
-
```
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
items.push(item); // ❌ Won't trigger updates
|
|
1102
|
-
}
|
|
1103
|
-
})
|
|
1491
|
+
```html
|
|
1492
|
+
<button @click="items.push(newItem)">Add item</button>
|
|
1493
|
+
<span @text="items.length"></span>
|
|
1104
1494
|
```
|
|
1105
1495
|
|
|
1106
|
-
|
|
1496
|
+
Nested mutations made through `$data` are reactive too:
|
|
1107
1497
|
|
|
1108
1498
|
```javascript
|
|
1109
1499
|
helium({
|
|
1110
1500
|
addItem(data, item) {
|
|
1111
|
-
data.items.push(item); //
|
|
1501
|
+
data.items.push(item); // Triggers updates
|
|
1112
1502
|
}
|
|
1113
1503
|
})
|
|
1114
1504
|
```
|
|
@@ -1141,58 +1531,15 @@ helium({
|
|
|
1141
1531
|
<button @click="goodFunction($data)">Works!</button>
|
|
1142
1532
|
```
|
|
1143
1533
|
|
|
1144
|
-
## Security Considerations
|
|
1145
|
-
|
|
1146
|
-
### XSS Protection
|
|
1147
|
-
|
|
1148
|
-
**Always sanitize user input when using @html:**
|
|
1149
|
-
|
|
1150
|
-
```html
|
|
1151
|
-
<!-- ❌ Dangerous if userInput contains scripts -->
|
|
1152
|
-
<div @html="userInput"></div>
|
|
1153
|
-
|
|
1154
|
-
<!-- ✅ Sanitize first -->
|
|
1155
|
-
<div @html="sanitize(userInput)"></div>
|
|
1156
|
-
```
|
|
1157
|
-
|
|
1158
|
-
Consider using a sanitization library like [DOMPurify](https://github.com/cure53/DOMPurify):
|
|
1159
|
-
|
|
1160
|
-
```javascript
|
|
1161
|
-
import DOMPurify from 'dompurify';
|
|
1162
|
-
|
|
1163
|
-
helium({
|
|
1164
|
-
sanitize(html) {
|
|
1165
|
-
return DOMPurify.sanitize(html);
|
|
1166
|
-
}
|
|
1167
|
-
});
|
|
1168
|
-
```
|
|
1169
|
-
|
|
1170
|
-
**Use @text for plain text:**
|
|
1171
|
-
|
|
1172
|
-
```html
|
|
1173
|
-
<!-- ✅ Safe - automatically escapes HTML -->
|
|
1174
|
-
<div @text="userInput"></div>
|
|
1175
|
-
```
|
|
1176
|
-
|
|
1177
|
-
### CSRF Protection
|
|
1178
|
-
|
|
1179
|
-
Helium automatically includes CSRF tokens in same-origin requests. Add this meta tag to your HTML:
|
|
1180
|
-
|
|
1181
|
-
```html
|
|
1182
|
-
<meta name="csrf-token" content="your-csrf-token">
|
|
1183
|
-
```
|
|
1184
|
-
|
|
1185
|
-
All POST, PUT, PATCH, and DELETE requests will include the `X-CSRF-Token` header automatically.
|
|
1186
|
-
|
|
1187
|
-
### Content Security Policy
|
|
1188
|
-
|
|
1189
|
-
If you're using a strict CSP, you may need to allow `'unsafe-eval'` since Helium uses `new Function()` to compile expressions, or use a hash/nonce for the script.
|
|
1190
|
-
|
|
1191
1534
|
## Error Handling
|
|
1192
1535
|
|
|
1193
1536
|
### JavaScript Expression Errors
|
|
1194
1537
|
|
|
1195
|
-
If
|
|
1538
|
+
If a binding expression throws at runtime, Helium logs the error and continues
|
|
1539
|
+
without updating that binding. Failed compilation keeps Helium's literal-string
|
|
1540
|
+
ergonomics: plain text such as `@text="Loading..."` stays quiet, while
|
|
1541
|
+
expression-looking failures such as `@text="user.name."` or
|
|
1542
|
+
`@click="count +"` emit a `console.warn` with the attribute and expression.
|
|
1196
1543
|
|
|
1197
1544
|
```html
|
|
1198
1545
|
<!-- If items is undefined, this won't crash the page -->
|
|
@@ -1201,13 +1548,12 @@ If an expression throws an error, Helium catches it silently and continues. Chec
|
|
|
1201
1548
|
|
|
1202
1549
|
### HTTP Request Errors
|
|
1203
1550
|
|
|
1204
|
-
|
|
1551
|
+
HTTP directives log failed requests to the console. Programmatic helpers return
|
|
1552
|
+
promises, so you can handle failures explicitly:
|
|
1205
1553
|
|
|
1206
1554
|
```html
|
|
1207
|
-
<button
|
|
1208
|
-
|
|
1209
|
-
@params="{ data: formData }"
|
|
1210
|
-
@target="#message">
|
|
1555
|
+
<button @click="$post('/api/save', { data: formData }, '#message')
|
|
1556
|
+
.catch(error => saveError = error.message)">
|
|
1211
1557
|
Save
|
|
1212
1558
|
</button>
|
|
1213
1559
|
|
|
@@ -1216,8 +1562,50 @@ Failed requests log errors to the console. Handle them in your expressions:
|
|
|
1216
1562
|
|
|
1217
1563
|
### Invalid Attribute Syntax
|
|
1218
1564
|
|
|
1219
|
-
Helium
|
|
1220
|
-
|
|
1565
|
+
Helium keeps plain malformed value expressions as literal values. If a failed
|
|
1566
|
+
expression looks like broken JavaScript, Helium emits a warning with directive
|
|
1567
|
+
context. Event handlers, HTTP options, `@data`, `@for`, `@if`, `@calculate`,
|
|
1568
|
+
`@effect`, and `@init` are treated strictly and warn on compile failure.
|
|
1569
|
+
|
|
1570
|
+
|
|
1571
|
+
## Roadmap & Known Limitations
|
|
1572
|
+
|
|
1573
|
+
Helium aims to stay tiny, so some conveniences found in larger frameworks are
|
|
1574
|
+
intentionally absent (or still on the roadmap). Current gaps:
|
|
1575
|
+
|
|
1576
|
+
### Not yet supported
|
|
1577
|
+
|
|
1578
|
+
- **No `$watch`/`$nextTick`, and no cross-root shared store** — each mounted
|
|
1579
|
+
instance has its own isolated state namespace.
|
|
1580
|
+
- **No transition helper** — use CSS transitions/classes directly with
|
|
1581
|
+
`@hidden`, `@visible`, or `@if`.
|
|
1582
|
+
|
|
1583
|
+
### Roadmap
|
|
1584
|
+
|
|
1585
|
+
Ordered by recommended implementation sequence:
|
|
1586
|
+
|
|
1587
|
+
1. **Document function and `this` context limitations** alongside the existing
|
|
1588
|
+
magic-variable guidance, with correct patterns for passing `$data`, `$el`,
|
|
1589
|
+
and other context explicitly.
|
|
1590
|
+
2. **Add opt-in required-CSRF validation for POST/PUT/PATCH** — fail before the
|
|
1591
|
+
request when an application declares that a token is mandatory, without
|
|
1592
|
+
breaking tokenless APIs and cross-origin requests.
|
|
1593
|
+
3. **Add request cancellation and explicit success/error hooks** using
|
|
1594
|
+
`AbortController`; define the request lifecycle before expanding loading and
|
|
1595
|
+
error-state APIs.
|
|
1596
|
+
4. **Document a form-validation pattern first**, using native constraint
|
|
1597
|
+
validation and Helium state; add helpers only if examples reveal recurring
|
|
1598
|
+
boilerplate.
|
|
1599
|
+
5. **Create a complete error-state example** covering validation, HTTP failure,
|
|
1600
|
+
retry, accessible messaging, and clearing stale errors.
|
|
1601
|
+
6. **Complete HTTP loading-state lifecycle behavior** — `@loading` already
|
|
1602
|
+
exists, so define consistent success, error, cancellation, and restoration
|
|
1603
|
+
behavior rather than adding a second loading mechanism.
|
|
1604
|
+
7. **Add debounced two-way binding updates if needed** after reproducing a real
|
|
1605
|
+
feedback/caret loop; event-handler debouncing already covers ordinary input
|
|
1606
|
+
throttling.
|
|
1607
|
+
8. **Add a minimal watch/middleware system** together with a clear size budget;
|
|
1608
|
+
avoid adopting Alpine-scale surface area without demonstrated use cases.
|
|
1221
1609
|
|
|
1222
1610
|
## Contributing
|
|
1223
1611
|
|
|
@@ -1227,6 +1615,18 @@ Helium is open source! Contributions, issues, and feature requests are welcome.
|
|
|
1227
1615
|
- Report issues: Create an issue on GitHub
|
|
1228
1616
|
- Suggest features: Open a discussion on GitHub
|
|
1229
1617
|
|
|
1618
|
+
Before submitting a change, run `npm run check`. It executes the full jsdom test
|
|
1619
|
+
suite, creates the production bundles, smoke-tests them, and fails if any gzip
|
|
1620
|
+
size budget is exceeded. `npm run test:browser` runs the Chromium smoke suite
|
|
1621
|
+
used by CI for CSP, Turbo, SSE, keyed focus preservation, late imports, Lite
|
|
1622
|
+
generation, and browser entry imports. `npm run check:ci` runs both paths.
|
|
1623
|
+
`npm run size` also reports raw, gzip, and Brotli sizes.
|
|
1624
|
+
|
|
1625
|
+
`helium.js` contains the shared runtime plus sections marked Standard-only.
|
|
1626
|
+
`npm run generate` derives `helium-lite.js`; do not edit the generated Lite file
|
|
1627
|
+
directly. Standard keeps keyed lists, HTTP, imports, and Turbo, while SSE remains
|
|
1628
|
+
an optional module.
|
|
1629
|
+
|
|
1230
1630
|
## License
|
|
1231
1631
|
|
|
1232
1632
|
MIT License - feel free to use Helium in personal and commercial projects.
|