@daz4126/helium 0.30.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.
@@ -0,0 +1,17 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "WebFetch(domain:data-star.dev)",
5
+ "WebFetch(domain:github.com)",
6
+ "WebFetch(domain:raw.githubusercontent.com)",
7
+ "WebSearch",
8
+ "WebFetch(domain:cdn.jsdelivr.net)",
9
+ "Bash(npm run test:run:*)",
10
+ "Bash(npm link)",
11
+ "Bash(npm install)",
12
+ "Bash(npm link helium)",
13
+ "Bash(ls:*)",
14
+ "Bash(wc:*)"
15
+ ]
16
+ }
17
+ }
package/CLAUDE.md ADDED
@@ -0,0 +1,137 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## Project Overview
6
+
7
+ Helium is an ultra-light (~5KB minified/gzipped) reactive library that makes HTML interactive using declarative attributes. It requires no build step and works directly in the browser.
8
+
9
+ ## Commands
10
+
11
+ ```bash
12
+ npm test # Run tests in watch mode (Vitest)
13
+ npm run test:ui # Run tests with interactive UI dashboard
14
+ npm run test:run # Run tests once (CI mode)
15
+ ```
16
+
17
+ There is no build step - `helium.js` is the single-file library used directly.
18
+
19
+ ## Architecture
20
+
21
+ ### Core Concepts
22
+
23
+ - **Single-file library**: All code lives in `helium.js` (minified/golfed for size)
24
+ - **Proxy-based reactivity**: Uses JavaScript Proxy to intercept state changes and trigger DOM updates
25
+ - **Attribute-driven**: Interactivity is declared via `@` prefixed HTML attributes (with `data-he-*` aliases for HTML validation)
26
+ - **Expression compilation**: Inline expressions are compiled to functions using `new Function()` with caching
27
+ - **Pluggable expression engine**: Use `createHelium()` to create variants with custom expression engines
28
+
29
+ ### Data Flow
30
+
31
+ ```
32
+ HTML parsed → processElements() registers bindings → expressions compiled (cached)
33
+ → initial applyBinding() updates DOM → state changes via Proxy → triggers bound updates
34
+ → MutationObserver watches for new content → processes dynamically added elements
35
+ ```
36
+
37
+ ### Key Internal Components
38
+
39
+ - **Global state**: `HELIUM.state` - single proxy object holding all reactive state
40
+ - **Bindings registry**: `HELIUM.bindings` - maps state properties to UI update functions
41
+ - **Expression cache**: Compiled functions are cached to avoid recompilation
42
+ - **MutationObserver**: Automatically processes dynamically added elements
43
+ - **WeakMaps/WeakSets**: Used for listener tracking and processed element tracking (memory-efficient)
44
+
45
+ ### File Structure
46
+
47
+ - `helium.js` - Main library implementation
48
+ - `helium.test.js` - Comprehensive test suite
49
+ - `vitest.config.js` - Test configuration (jsdom environment, globals enabled)
50
+
51
+ ### Exports
52
+
53
+ ```javascript
54
+ import helium from 'helium'; // Default helium function
55
+ import { createHelium } from 'helium'; // Factory for custom variants
56
+ ```
57
+
58
+ ## Key Directives
59
+
60
+ | Directive | Purpose |
61
+ |-----------|---------|
62
+ | `@data` | Initialize state: `@data="{ count: 0 }"` |
63
+ | `@text` | Bind text content |
64
+ | `@html` | Bind HTML content (use with caution for XSS) |
65
+ | `@bind` | Two-way binding for inputs |
66
+ | `@hidden/@visible` | Conditional visibility |
67
+ | `:attribute` | Dynamic attribute binding |
68
+ | `@click/@input/etc` | Event handlers (supports modifiers like `.prevent`, `.once`, `.debounce`) |
69
+ | `@ref` | Element reference (accessed as `$refName`) |
70
+ | `@calculate` | Computed properties |
71
+ | `@effect` | Side effects on state changes |
72
+ | `@get/@post/@put/@patch/@delete` | HTTP requests |
73
+ | `@import` | Import ES modules: `@import="utils"` or `@import="https://cdn.example.com/lib.js"` |
74
+
75
+ ## Imports
76
+
77
+ `@import` loads ES modules and adds their named exports to state. Paths are flexible:
78
+ - `utils` → `./utils.js` (same folder, .js added automatically)
79
+ - `modules/helpers` → `./modules/helpers.js` (subfolders work)
80
+ - `https://...` → URLs used as-is (for CDNs, GitHub raw files, etc.)
81
+
82
+ ## Magic Variables
83
+
84
+ Available in all expressions: `$` (querySelector), `$el` (current element), `$event`, `$data` (reactive state), `$html` (create elements), `$refs`, `$get/$post/$put/$patch/$delete`.
85
+
86
+ ## Important Notes
87
+
88
+ - **Functions and reactivity**: Magic variables are NOT available inside functions defined in `@data`. Pass `$data` as an argument to enable reactive updates: `@click="myFunc($data)"`
89
+ - **CSP**: Library uses `new Function()` which requires `unsafe-eval` if using strict Content Security Policy. For CSP-safe environments, see the [Xenon](https://github.com/daz-codes/xenon) variant.
90
+ - **Idiomorph integration**: If Idiomorph is loaded, Helium automatically uses it for efficient DOM morphing
91
+ - **Turbo/Hotwire**: Automatic integration via `turbo:before-render` and `turbo:render` events
92
+
93
+ ## Creating Custom Variants
94
+
95
+ Use `createHelium()` to create variants with custom expression engines:
96
+
97
+ ```javascript
98
+ import { createHelium } from 'helium';
99
+
100
+ const { helium, heliumTeardown } = createHelium({
101
+ engine: {
102
+ compile(expr, withReturn) {
103
+ // Return { execute(scope), getIds() }
104
+ },
105
+ createScope(ctx) {
106
+ // Return scope object for expression execution
107
+ }
108
+ },
109
+ prefix: 'my', // For data-my-* attributes
110
+ rootAttr: 'mylib' // For @mylib / data-mylib root element
111
+ });
112
+ ```
113
+
114
+ ## Testing Patterns
115
+
116
+ ```javascript
117
+ describe('Feature', () => {
118
+ let container;
119
+
120
+ beforeEach(() => {
121
+ container = document.createElement('div');
122
+ container.setAttribute('data-helium', '');
123
+ document.body.appendChild(container);
124
+ });
125
+
126
+ afterEach(() => {
127
+ window.heliumTeardown?.();
128
+ document.body.innerHTML = '';
129
+ });
130
+
131
+ it('should work', async () => {
132
+ container.innerHTML = '<div @text="count"></div>';
133
+ await helium({ count: 5 });
134
+ expect(container.querySelector('div').textContent).toBe('5');
135
+ });
136
+ });
137
+ ```
package/README.md CHANGED
@@ -263,33 +263,61 @@ Runs a side effect whenever specified dependencies change. Use `:*` to run on an
263
263
 
264
264
  ### @import
265
265
 
266
- Imports global functions or variables from the `window` object into Helium's scope, making them available in Helium expressions.
266
+ Imports ES modules into Helium's scope, making their exports available in Helium expressions.
267
267
 
268
268
  ```html
269
- <div @import="myFunction,myVariable">
270
- <button @click="myFunction()">Call Imported Function</button>
271
- <p @text="myVariable"></p>
269
+ <div @import="utils,api">
270
+ <button @click="formatDate(new Date())">Format Date</button>
271
+ <p @text="API_VERSION"></p>
272
272
  </div>
273
273
  ```
274
274
 
275
- This is useful when you have existing global functions and want to use them with Helium without passing them through the `helium()` initialization.
275
+ This dynamically imports modules and adds all their named exports to Helium's state.
276
+
277
+ **Import paths:**
278
+
279
+ | Path | Resolves to |
280
+ |------|-------------|
281
+ | `utils` | `./utils.js` |
282
+ | `modules/helpers` | `./modules/helpers.js` |
283
+ | `./utils.js` | `./utils.js` |
284
+ | `../shared/utils` | `../shared/utils.js` |
285
+ | `https://example.com/lib.js` | `https://example.com/lib.js` |
286
+
287
+ The `.js` extension is added automatically if not provided. URLs are used as-is.
276
288
 
277
289
  **Example:**
290
+
291
+ ```javascript
292
+ // utils.js
293
+ export function formatDate(date) {
294
+ return date.toLocaleDateString();
295
+ }
296
+
297
+ export function capitalize(str) {
298
+ return str.charAt(0).toUpperCase() + str.slice(1);
299
+ }
300
+ ```
301
+
302
+ ```javascript
303
+ // api.js
304
+ export const API_VERSION = '1.0.0';
305
+ export const API_URL = 'https://api.example.com';
306
+ ```
307
+
278
308
  ```html
279
- <script>
280
- function greet(name) {
281
- alert(`Hello, ${name}!`);
282
- }
283
-
284
- window.appConfig = {
285
- version: '1.0.0',
286
- apiUrl: 'https://api.example.com'
287
- };
288
- </script>
309
+ <div @import="utils,api">
310
+ <button @click="alert(capitalize('hello'))">Capitalize</button>
311
+ <p @text="API_VERSION"></p>
312
+ </div>
313
+ ```
289
314
 
290
- <div @import="greet,appConfig">
291
- <button @click="greet('World')">Greet</button>
292
- <p @text="appConfig.version"></p>
315
+ **Importing from URLs:**
316
+
317
+ ```html
318
+ <!-- Import from a CDN or GitHub -->
319
+ <div @import="https://cdn.example.com/helpers.js">
320
+ <button @click="helper()">Use Remote Helper</button>
293
321
  </div>
294
322
  ```
295
323
 
@@ -0,0 +1,387 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Helium Lite Examples</title>
7
+ <style>
8
+ * { box-sizing: border-box; }
9
+
10
+ body {
11
+ font-family: system-ui, -apple-system, sans-serif;
12
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
13
+ min-height: 100vh;
14
+ margin: 0;
15
+ padding: 2rem;
16
+ }
17
+
18
+ .page {
19
+ max-width: 1200px;
20
+ margin: 0 auto;
21
+ }
22
+
23
+ h1 {
24
+ color: white;
25
+ text-align: center;
26
+ margin-bottom: 2rem;
27
+ font-size: 2.5rem;
28
+ }
29
+
30
+ .examples {
31
+ display: grid;
32
+ grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
33
+ gap: 1.5rem;
34
+ }
35
+
36
+ .card {
37
+ background: white;
38
+ border-radius: 12px;
39
+ padding: 1.5rem;
40
+ box-shadow: 0 10px 40px rgba(0,0,0,0.2);
41
+ transition: transform 0.2s, box-shadow 0.2s;
42
+ }
43
+
44
+ .card:hover {
45
+ transform: translateY(-2px);
46
+ box-shadow: 0 15px 50px rgba(0,0,0,0.25);
47
+ }
48
+
49
+ .card.active {
50
+ background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
51
+ border: 2px solid #667eea;
52
+ }
53
+
54
+ .card h2 {
55
+ margin: 0 0 1rem 0;
56
+ color: #333;
57
+ font-size: 1.25rem;
58
+ border-bottom: 2px solid #eee;
59
+ padding-bottom: 0.5rem;
60
+ }
61
+
62
+ button {
63
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
64
+ color: white;
65
+ border: none;
66
+ padding: 0.6rem 1.2rem;
67
+ border-radius: 6px;
68
+ cursor: pointer;
69
+ font-size: 0.95rem;
70
+ transition: transform 0.1s, box-shadow 0.2s;
71
+ }
72
+
73
+ button:hover {
74
+ transform: scale(1.02);
75
+ box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
76
+ }
77
+
78
+ button:active {
79
+ transform: scale(0.98);
80
+ }
81
+
82
+ input, textarea, select {
83
+ width: 100%;
84
+ padding: 0.6rem;
85
+ border: 2px solid #ddd;
86
+ border-radius: 6px;
87
+ font-size: 1rem;
88
+ margin-bottom: 0.5rem;
89
+ transition: border-color 0.2s;
90
+ }
91
+
92
+ input:focus, textarea:focus, select:focus {
93
+ outline: none;
94
+ border-color: #667eea;
95
+ }
96
+
97
+ .button-group {
98
+ display: flex;
99
+ gap: 0.5rem;
100
+ flex-wrap: wrap;
101
+ }
102
+
103
+ .color-preview {
104
+ width: 100%;
105
+ height: 60px;
106
+ border-radius: 6px;
107
+ margin-top: 0.5rem;
108
+ transition: background-color 0.3s;
109
+ }
110
+
111
+ .keyboard-display {
112
+ font-family: monospace;
113
+ background: #1a1a2e;
114
+ color: #0f0;
115
+ padding: 1rem;
116
+ border-radius: 6px;
117
+ min-height: 40px;
118
+ }
119
+
120
+ .progress-bar {
121
+ width: 100%;
122
+ height: 20px;
123
+ background: #eee;
124
+ border-radius: 10px;
125
+ overflow: hidden;
126
+ }
127
+
128
+ .progress-fill {
129
+ height: 100%;
130
+ background: linear-gradient(90deg, #667eea, #764ba2);
131
+ transition: width 0.3s;
132
+ }
133
+
134
+ .tabs {
135
+ display: flex;
136
+ gap: 0;
137
+ margin-bottom: 1rem;
138
+ }
139
+
140
+ .tab {
141
+ padding: 0.5rem 1rem;
142
+ background: #eee;
143
+ border: none;
144
+ cursor: pointer;
145
+ transition: background 0.2s;
146
+ }
147
+
148
+ .tab:first-child { border-radius: 6px 0 0 6px; }
149
+ .tab:last-child { border-radius: 0 6px 6px 0; }
150
+ .tab.active { background: #667eea; color: white; }
151
+
152
+ .tab-content {
153
+ padding: 1rem;
154
+ background: #f9f9f9;
155
+ border-radius: 6px;
156
+ }
157
+
158
+ .emoji-picker {
159
+ display: flex;
160
+ gap: 0.5rem;
161
+ flex-wrap: wrap;
162
+ }
163
+
164
+ .emoji-btn {
165
+ font-size: 1.5rem;
166
+ padding: 0.5rem;
167
+ background: #f0f0f0;
168
+ border-radius: 8px;
169
+ }
170
+
171
+ .emoji-btn:hover {
172
+ background: #e0e0e0;
173
+ }
174
+
175
+ .selected-emoji {
176
+ font-size: 3rem;
177
+ text-align: center;
178
+ padding: 1rem;
179
+ }
180
+ </style>
181
+ </head>
182
+ <body>
183
+
184
+ <div class="page" data-helium @data="{ count: 0 }">
185
+ <h1>Helium Lite Examples</h1>
186
+
187
+ <div class="examples">
188
+
189
+ <!-- Counter -->
190
+ <div class="card">
191
+ <h2>Counter</h2>
192
+ <p>Simple reactive counter with increment/decrement.</p>
193
+ <div class="button-group">
194
+ <button @click="count--">-</button>
195
+ <button @click="count++">+</button>
196
+ </div>
197
+ <p>Count: <b @text="count">0</b></p>
198
+ </div>
199
+
200
+ <!-- Dynamic Classes -->
201
+ <div
202
+ @data="{ active: false }"
203
+ :class="{active: active}"
204
+ class="card"
205
+ >
206
+ <h2>Dynamic Classes</h2>
207
+ <p>Toggle the active state to change card styling.</p>
208
+ <button @click="active = !active">
209
+ Toggle (<span @text="active ? 'ON' : 'OFF'"></span>)
210
+ </button>
211
+ </div>
212
+
213
+ <!-- Two-way Binding -->
214
+ <div class="card">
215
+ <h2>Two-Way Binding</h2>
216
+ <input @bind="firstname" placeholder="First name">
217
+ <input @bind="lastname" placeholder="Last name">
218
+ <span @calculate:fullname="firstname && lastname ? firstname + ' ' + lastname : firstname || lastname || 'stranger'"></span>
219
+ <p>Hello, <b @text="fullname">stranger</b>!</p>
220
+ <button @click="firstname = '', lastname = ''">Clear</button>
221
+ </div>
222
+
223
+ <!-- Visibility Toggle -->
224
+ <div @data="{ show: true }" class="card">
225
+ <h2>Visibility Toggle</h2>
226
+ <button @click="show = !show" @text="show ? 'Hide' : 'Show'">Show</button>
227
+ <p @visible="show">Now you see me!</p>
228
+ <p @hidden="show">Now you don't!</p>
229
+ </div>
230
+
231
+ <!-- Checkbox Binding -->
232
+ <div class="card" @data="{ agreed: false, newsletter: true }">
233
+ <h2>Checkbox Binding</h2>
234
+ <label>
235
+ <input type="checkbox" @bind="agreed"> I agree to the terms
236
+ </label>
237
+ <label>
238
+ <input type="checkbox" @bind="newsletter"> Subscribe to newsletter
239
+ </label>
240
+ <p>
241
+ Terms: <b @text="agreed ? 'Accepted' : 'Not accepted'">Not accepted</b><br>
242
+ Newsletter: <b @text="newsletter ? 'Subscribed' : 'Not subscribed'">Subscribed</b>
243
+ </p>
244
+ <button @click="agreed = true, newsletter = true">Accept All</button>
245
+ </div>
246
+
247
+ <!-- Radio Buttons -->
248
+ <div class="card" @data="{ size: 'medium' }">
249
+ <h2>Radio Buttons</h2>
250
+ <label><input type="radio" name="size" value="small" @bind="size"> Small</label>
251
+ <label><input type="radio" name="size" value="medium" @bind="size"> Medium</label>
252
+ <label><input type="radio" name="size" value="large" @bind="size"> Large</label>
253
+ <p>Selected size: <b @text="size">medium</b></p>
254
+ </div>
255
+
256
+ <!-- Select Dropdown -->
257
+ <div class="card" @data="{ color: 'blue' }">
258
+ <h2>Select Dropdown</h2>
259
+ <select @bind="color">
260
+ <option value="red">Red</option>
261
+ <option value="green">Green</option>
262
+ <option value="blue">Blue</option>
263
+ <option value="purple">Purple</option>
264
+ </select>
265
+ <div class="color-preview" :style="{ backgroundColor: color }"></div>
266
+ </div>
267
+
268
+ <!-- Dynamic Style -->
269
+ <div class="card" @data="{ width: 50 }">
270
+ <h2>Dynamic Style</h2>
271
+ <input type="range" min="0" max="100" @bind="width">
272
+ <p>Width: <b @text="width">50</b>%</p>
273
+ <div class="progress-bar">
274
+ <div class="progress-fill" :style="{ width: width + '%' }"></div>
275
+ </div>
276
+ </div>
277
+
278
+ <!-- Keyboard Events -->
279
+ <div class="card" @data="{ lastKey: 'Press a key...' }">
280
+ <h2>Keyboard Events</h2>
281
+ <input placeholder="Type here..." @keydown="lastKey = $event.key" @keydown.enter="lastKey = 'ENTER pressed!'" @keydown.esc="lastKey = 'Escaped!'">
282
+ <div class="keyboard-display" @text="lastKey">Press a key...</div>
283
+ </div>
284
+
285
+ <!-- Debounced Input -->
286
+ <div class="card" @data="{ search: '', debouncedSearch: '' }">
287
+ <h2>Debounced Input</h2>
288
+ <p>Type and wait 500ms to see the debounced value update.</p>
289
+ <input placeholder="Search..." @bind="search" @input.debounce:500="debouncedSearch = search">
290
+ <p>Immediate: <b @text="search || '(empty)'"></b></p>
291
+ <p>Debounced: <b @text="debouncedSearch || '(empty)'"></b></p>
292
+ </div>
293
+
294
+ <!-- Tabs -->
295
+ <div class="card" @data="{ tab: 'one' }">
296
+ <h2>Tabs</h2>
297
+ <div class="tabs">
298
+ <button class="tab" :class="{ active: tab === 'one' }" @click="tab = 'one'">Tab 1</button>
299
+ <button class="tab" :class="{ active: tab === 'two' }" @click="tab = 'two'">Tab 2</button>
300
+ <button class="tab" :class="{ active: tab === 'three' }" @click="tab = 'three'">Tab 3</button>
301
+ </div>
302
+ <div class="tab-content" @visible="tab === 'one'">Content for Tab 1</div>
303
+ <div class="tab-content" @visible="tab === 'two'">Content for Tab 2</div>
304
+ <div class="tab-content" @visible="tab === 'three'">Content for Tab 3</div>
305
+ </div>
306
+
307
+ <!-- Element References -->
308
+ <div class="card">
309
+ <h2>Element References</h2>
310
+ <input @ref="myInput" placeholder="Focus me!">
311
+ <div class="button-group">
312
+ <button @click="$myInput.focus()">Focus Input</button>
313
+ <button @click="$myInput.value = 'Hello!'">Set Value</button>
314
+ <button @click="$myInput.value = ''">Clear</button>
315
+ </div>
316
+ </div>
317
+
318
+ <!-- Emoji Picker -->
319
+ <div class="card" @data="{ emoji: '' }">
320
+ <h2>Emoji Picker</h2>
321
+ <div class="emoji-picker">
322
+ <button class="emoji-btn" @click="emoji = '😀'">😀</button>
323
+ <button class="emoji-btn" @click="emoji = '🎉'">🎉</button>
324
+ <button class="emoji-btn" @click="emoji = '🚀'">🚀</button>
325
+ <button class="emoji-btn" @click="emoji = '💡'">💡</button>
326
+ <button class="emoji-btn" @click="emoji = '🎈'">🎈</button>
327
+ <button class="emoji-btn" @click="emoji = '❤️'">❤️</button>
328
+ </div>
329
+ <div class="selected-emoji" @text="emoji || 'Pick one!'">Pick one!</div>
330
+ </div>
331
+
332
+ <!-- Calculator -->
333
+ <div class="card" @data="{ a: 10, b: 5, op: '+' }">
334
+ <h2>Calculator</h2>
335
+ <input type="number" @bind="a" placeholder="First number">
336
+ <select @bind="op">
337
+ <option value="+">+</option>
338
+ <option value="-">-</option>
339
+ <option value="*">×</option>
340
+ <option value="/">÷</option>
341
+ </select>
342
+ <input type="number" @bind="b" placeholder="Second number">
343
+ <span @calculate:result="op === '+' ? a + b : op === '-' ? a - b : op === '*' ? a * b : op === '/' ? a / b : 0"></span>
344
+ <p style="font-size: 1.5rem; text-align: center;">
345
+ <span @text="a">10</span>
346
+ <span @text="op">+</span>
347
+ <span @text="b">5</span>
348
+ = <b @text="result">15</b>
349
+ </p>
350
+ </div>
351
+
352
+ <!-- Character Counter -->
353
+ <div class="card" @data="{ text: '', maxLength: 100 }">
354
+ <h2>Character Counter</h2>
355
+ <textarea @bind="text" placeholder="Type something..." rows="3"></textarea>
356
+ <span @calculate:remaining="maxLength - text.length"></span>
357
+ <p :style="{ color: remaining < 20 ? 'red' : 'inherit' }">
358
+ <b @text="remaining">100</b> characters remaining
359
+ </p>
360
+ </div>
361
+
362
+ <!-- Init Example -->
363
+ <div class="card" @data="{ loadTime: '' }">
364
+ <h2>Init Example</h2>
365
+ <p @init="loadTime = new Date().toLocaleTimeString()">
366
+ Page loaded at: <b @text="loadTime"></b>
367
+ </p>
368
+ <button @click="loadTime = new Date().toLocaleTimeString()">Update Time</button>
369
+ </div>
370
+
371
+ <!-- Effect Example -->
372
+ <div class="card" @data="{ clicks: 0, history: [] }">
373
+ <h2>Effect (Side Effects)</h2>
374
+ <p>Click the button and watch the history build up.</p>
375
+ <button @click="clicks++">Click Me</button>
376
+ <span @effect="clicks > 0 ? history.push('Click #' + clicks) : null"></span>
377
+ <p>Clicks: <b @text="clicks">0</b></p>
378
+ <p>History: <span @text="history.join(', ') || 'None yet'">None yet</span></p>
379
+ </div>
380
+
381
+ </div>
382
+ </div>
383
+
384
+ <script type="module" src="../helium-lite.js"></script>
385
+
386
+ </body>
387
+ </html>