@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,486 @@
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 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
+ .response {
159
+ padding: 1rem;
160
+ border-radius: 6px;
161
+ margin-top: 0.5rem;
162
+ }
163
+
164
+ .response.blue { background: #e3f2fd; }
165
+ .response.red { background: #ffebee; }
166
+
167
+ .form label {
168
+ display: block;
169
+ margin-bottom: 0.5rem;
170
+ }
171
+
172
+ .form label span {
173
+ display: block;
174
+ font-weight: 500;
175
+ margin-bottom: 0.25rem;
176
+ }
177
+
178
+ .post {
179
+ background: #f0f0f0;
180
+ padding: 1rem;
181
+ border-radius: 6px;
182
+ margin-top: 0.5rem;
183
+ }
184
+
185
+ .post h1 {
186
+ font-size: 1.1rem;
187
+ color: #333;
188
+ margin: 0 0 0.5rem 0;
189
+ text-align: left;
190
+ }
191
+
192
+ .post p {
193
+ margin: 0;
194
+ color: #666;
195
+ }
196
+
197
+ .emoji-picker {
198
+ display: flex;
199
+ gap: 0.5rem;
200
+ flex-wrap: wrap;
201
+ }
202
+
203
+ .emoji-btn {
204
+ font-size: 1.5rem;
205
+ padding: 0.5rem;
206
+ background: #f0f0f0;
207
+ border-radius: 8px;
208
+ }
209
+
210
+ .emoji-btn:hover {
211
+ background: #e0e0e0;
212
+ }
213
+
214
+ .selected-emoji {
215
+ font-size: 3rem;
216
+ text-align: center;
217
+ padding: 1rem;
218
+ }
219
+ </style>
220
+ </head>
221
+ <body>
222
+
223
+ <div class="page" data-helium @data="{ count: 0 }">
224
+ <h1>Helium Examples</h1>
225
+
226
+ <div class="examples">
227
+
228
+ <!-- Counter -->
229
+ <div class="card">
230
+ <h2>Counter</h2>
231
+ <p>Simple reactive counter with increment/decrement.</p>
232
+ <div class="button-group">
233
+ <button @click="count--">-</button>
234
+ <button @click="count++">+</button>
235
+ </div>
236
+ <p>Count: <b @text="count">0</b></p>
237
+ </div>
238
+
239
+ <!-- Dynamic Classes -->
240
+ <div
241
+ @data="{ active: false }"
242
+ :class="{active: active}"
243
+ class="card"
244
+ >
245
+ <h2>Dynamic Classes</h2>
246
+ <p>Toggle the active state to change card styling.</p>
247
+ <button @click="active = !active">
248
+ Toggle (<span @text="active ? 'ON' : 'OFF'"></span>)
249
+ </button>
250
+ </div>
251
+
252
+ <!-- Two-way Binding -->
253
+ <div class="card">
254
+ <h2>Two-Way Binding</h2>
255
+ <input @bind="firstname" placeholder="First name">
256
+ <input @bind="lastname" placeholder="Last name">
257
+ <span @calculate:fullname="firstname && lastname ? firstname + ' ' + lastname : firstname || lastname || 'stranger'"></span>
258
+ <p>Hello, <b @text="fullname">stranger</b>!</p>
259
+ <button @click="firstname = '', lastname = ''">Clear</button>
260
+ </div>
261
+
262
+ <!-- Visibility Toggle -->
263
+ <div @data="{ show: true }" class="card">
264
+ <h2>Visibility Toggle</h2>
265
+ <button @click="show = !show" @text="show ? 'Hide' : 'Show'">Show</button>
266
+ <p @visible="show">Now you see me!</p>
267
+ <p @hidden="show">Now you don't!</p>
268
+ </div>
269
+
270
+ <!-- Checkbox Binding -->
271
+ <div class="card" @data="{ agreed: false, newsletter: true }">
272
+ <h2>Checkbox Binding</h2>
273
+ <label>
274
+ <input type="checkbox" @bind="agreed"> I agree to the terms
275
+ </label>
276
+ <label>
277
+ <input type="checkbox" @bind="newsletter"> Subscribe to newsletter
278
+ </label>
279
+ <p>
280
+ Terms: <b @text="agreed ? 'Accepted' : 'Not accepted'">Not accepted</b><br>
281
+ Newsletter: <b @text="newsletter ? 'Subscribed' : 'Not subscribed'">Subscribed</b>
282
+ </p>
283
+ <button @click="agreed = true, newsletter = true">Accept All</button>
284
+ </div>
285
+
286
+ <!-- Radio Buttons -->
287
+ <div class="card" @data="{ size: 'medium' }">
288
+ <h2>Radio Buttons</h2>
289
+ <label><input type="radio" name="size" value="small" @bind="size"> Small</label>
290
+ <label><input type="radio" name="size" value="medium" @bind="size"> Medium</label>
291
+ <label><input type="radio" name="size" value="large" @bind="size"> Large</label>
292
+ <p>Selected size: <b @text="size">medium</b></p>
293
+ </div>
294
+
295
+ <!-- Select Dropdown -->
296
+ <div class="card" @data="{ color: 'blue' }">
297
+ <h2>Select Dropdown</h2>
298
+ <select @bind="color">
299
+ <option value="red">Red</option>
300
+ <option value="green">Green</option>
301
+ <option value="blue">Blue</option>
302
+ <option value="purple">Purple</option>
303
+ </select>
304
+ <div class="color-preview" :style="{ backgroundColor: color }"></div>
305
+ </div>
306
+
307
+ <!-- Dynamic Style -->
308
+ <div class="card" @data="{ width: 50 }">
309
+ <h2>Dynamic Style</h2>
310
+ <input type="range" min="0" max="100" @bind="width">
311
+ <p>Width: <b @text="width">50</b>%</p>
312
+ <div class="progress-bar">
313
+ <div class="progress-fill" :style="{ width: width + '%' }"></div>
314
+ </div>
315
+ </div>
316
+
317
+ <!-- Keyboard Events -->
318
+ <div class="card" @data="{ lastKey: 'Press a key...' }">
319
+ <h2>Keyboard Events</h2>
320
+ <input placeholder="Type here..." @keydown="lastKey = $event.key" @keydown.enter="lastKey = 'ENTER pressed!'" @keydown.esc="lastKey = 'Escaped!'">
321
+ <div class="keyboard-display" @text="lastKey">Press a key...</div>
322
+ </div>
323
+
324
+ <!-- Debounced Input -->
325
+ <div class="card" @data="{ search: '', debouncedSearch: '' }">
326
+ <h2>Debounced Input</h2>
327
+ <p>Type and wait 500ms to see the debounced value update.</p>
328
+ <input placeholder="Search..." @bind="search" @input.debounce:500="debouncedSearch = search">
329
+ <p>Immediate: <b @text="search || '(empty)'"></b></p>
330
+ <p>Debounced: <b @text="debouncedSearch || '(empty)'"></b></p>
331
+ </div>
332
+
333
+ <!-- Tabs -->
334
+ <div class="card" @data="{ tab: 'one' }">
335
+ <h2>Tabs</h2>
336
+ <div class="tabs">
337
+ <button class="tab" :class="{ active: tab === 'one' }" @click="tab = 'one'">Tab 1</button>
338
+ <button class="tab" :class="{ active: tab === 'two' }" @click="tab = 'two'">Tab 2</button>
339
+ <button class="tab" :class="{ active: tab === 'three' }" @click="tab = 'three'">Tab 3</button>
340
+ </div>
341
+ <div class="tab-content" @visible="tab === 'one'">Content for Tab 1</div>
342
+ <div class="tab-content" @visible="tab === 'two'">Content for Tab 2</div>
343
+ <div class="tab-content" @visible="tab === 'three'">Content for Tab 3</div>
344
+ </div>
345
+
346
+ <!-- Element References -->
347
+ <div class="card">
348
+ <h2>Element References</h2>
349
+ <input @ref="myInput" placeholder="Focus me!">
350
+ <div class="button-group">
351
+ <button @click="$myInput.focus()">Focus Input</button>
352
+ <button @click="$myInput.value = 'Hello!'">Set Value</button>
353
+ <button @click="$myInput.value = ''">Clear</button>
354
+ </div>
355
+ </div>
356
+
357
+ <!-- Emoji Picker -->
358
+ <div class="card" @data="{ emoji: '' }">
359
+ <h2>Emoji Picker</h2>
360
+ <div class="emoji-picker">
361
+ <button class="emoji-btn" @click="emoji = '😀'">😀</button>
362
+ <button class="emoji-btn" @click="emoji = '🎉'">🎉</button>
363
+ <button class="emoji-btn" @click="emoji = '🚀'">🚀</button>
364
+ <button class="emoji-btn" @click="emoji = '💡'">💡</button>
365
+ <button class="emoji-btn" @click="emoji = '🎈'">🎈</button>
366
+ <button class="emoji-btn" @click="emoji = '❤️'">❤️</button>
367
+ </div>
368
+ <div class="selected-emoji" @text="emoji || 'Pick one!'">Pick one!</div>
369
+ </div>
370
+
371
+ <!-- Calculator -->
372
+ <div class="card" @data="{ a: 10, b: 5, op: '+' }">
373
+ <h2>Calculator</h2>
374
+ <input type="number" @bind="a" placeholder="First number">
375
+ <select @bind="op">
376
+ <option value="+">+</option>
377
+ <option value="-">-</option>
378
+ <option value="*">×</option>
379
+ <option value="/">÷</option>
380
+ </select>
381
+ <input type="number" @bind="b" placeholder="Second number">
382
+ <span @calculate:result="op === '+' ? a + b : op === '-' ? a - b : op === '*' ? a * b : op === '/' ? a / b : 0"></span>
383
+ <p style="font-size: 1.5rem; text-align: center;">
384
+ <span @text="a">10</span>
385
+ <span @text="op">+</span>
386
+ <span @text="b">5</span>
387
+ = <b @text="result">15</b>
388
+ </p>
389
+ </div>
390
+
391
+ <!-- Character Counter -->
392
+ <div class="card" @data="{ text: '', maxLength: 100 }">
393
+ <h2>Character Counter</h2>
394
+ <textarea @bind="text" placeholder="Type something..." rows="3"></textarea>
395
+ <span @calculate:remaining="maxLength - text.length"></span>
396
+ <p :style="{ color: remaining < 20 ? 'red' : 'inherit' }">
397
+ <b @text="remaining">100</b> characters remaining
398
+ </p>
399
+ </div>
400
+
401
+ <!-- Password Strength (uses @import) -->
402
+ <div class="card" @data="{ password: '' }" @import="password-utils">
403
+ <h2>Password Strength</h2>
404
+ <input type="password" @bind="password" placeholder="Enter password">
405
+ <span @calculate:result="checkStrength(password)"></span>
406
+ <span @calculate:tips="getPasswordTips(password)"></span>
407
+ <div class="progress-bar">
408
+ <div class="progress-fill" :style="{ width: result.percent + '%', background: strengthColor(result.label) }"></div>
409
+ </div>
410
+ <p>Strength: <b @text="result.label"></b></p>
411
+ <p style="font-size: 0.85rem; color: #666;" @text="tips.join(' • ')"></p>
412
+ </div>
413
+
414
+ <!-- Init Example -->
415
+ <div class="card" @data="{ loadTime: '' }">
416
+ <h2>Init Example</h2>
417
+ <p @init="loadTime = new Date().toLocaleTimeString()">
418
+ Page loaded at: <b @text="loadTime"></b>
419
+ </p>
420
+ <button @click="loadTime = new Date().toLocaleTimeString()">Update Time</button>
421
+ </div>
422
+
423
+ <!-- Effect Example -->
424
+ <div class="card" @data="{ clicks: 0, history: [] }">
425
+ <h2>Effect (Side Effects)</h2>
426
+ <p>Click the button and watch the history build up.</p>
427
+ <button @click="clicks++">Click Me</button>
428
+ <span @effect="clicks > 0 ? history.push('Click #' + clicks) : null"></span>
429
+ <p>Clicks: <b @text="clicks">0</b></p>
430
+ <p>History: <span @text="history.join(', ') || 'None yet'">None yet</span></p>
431
+ </div>
432
+
433
+ </div>
434
+
435
+ <!-- AJAX Examples Section -->
436
+ <div class="page" style="margin-top: 2rem;" @data="{
437
+ templates: {
438
+ post: data => `<section class='post'><h1>${data?.title || 'Loading...'}</h1><p>${data?.body || ''}</p></section>`
439
+ }
440
+ }">
441
+ <h1>AJAX Examples</h1>
442
+
443
+ <div class="examples">
444
+
445
+ <!-- HTML Response -->
446
+ <section class="card">
447
+ <h2>HTML Response</h2>
448
+ <div id="table"><p>Click to fetch an HTML table from the server.</p></div>
449
+ <button @get.once="https://html-mock.fly.dev/tag/table" @target="#table" @loading="<p>Loading table...</p>">Get HTML Table</button>
450
+ </section>
451
+
452
+ <!-- Request Methods -->
453
+ <section class="card">
454
+ <h2>Request Methods</h2>
455
+ <p>PUT adds to account, DELETE (shift+click) removes.</p>
456
+ <div class="button-group">
457
+ <button @put="https://html-mock.fly.dev/tag/paragraph" @loading="<span>Adding...</span>" @params="{cash: 100}" @target="put_response">PUT - Add Funds</button>
458
+ <button @delete.shift="https://html-mock.fly.dev/tag/paragraph" @params="{account: 1}" @target="delete_response">DELETE (Shift+Click)</button>
459
+ </div>
460
+ <div @html="put_response" class="response blue">PUT response will appear here</div>
461
+ <div @html="delete_response" class="response red">DELETE response will appear here</div>
462
+ </section>
463
+
464
+ <!-- Posts with JSON -->
465
+ <section @ref="posts" class="card">
466
+ <h2>Posts (JSON + Template)</h2>
467
+ <p>Fetch posts and render them using a template function.</p>
468
+ <form @ref="form" class="form">
469
+ <label><span>Title:</span><input type="hidden" name="userId" value="5"><input name="title" placeholder="Post title"></label>
470
+ <label><span>Body:</span><textarea name="body" rows="2" placeholder="Post content"></textarea></label>
471
+ <button @post.prevent="https://dummyjson.com/posts/add" @params="new FormData($form)" @target="$posts:append" @template="templates.post" @loading="templates.post({title: 'Saving...', body: 'Please wait'})">Create Post</button>
472
+ </form>
473
+ <div class="button-group" style="margin-top: 1rem;">
474
+ <button @get.debounce:500="https://dummyjson.com/posts/1" @target="$posts:append" @template="templates.post" @loading="<p>Loading post...</p>">Get Post #1</button>
475
+ <button @get.debounce:500="https://dummyjson.com/posts/2" @target="$posts:append" @template="templates.post" @loading="<p>Loading post...</p>">Get Post #2</button>
476
+ </div>
477
+ </section>
478
+
479
+ </div>
480
+ </div>
481
+ </div>
482
+
483
+ <script type="module" src="../helium-csp.js"></script>
484
+
485
+ </body>
486
+ </html>
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Password strength utilities for Helium examples
3
+ * Demonstrates @import functionality
4
+ */
5
+
6
+ /**
7
+ * Check password strength based on multiple criteria
8
+ * @param {string} password - The password to check
9
+ * @returns {object} - { score: 0-4, label: string, percent: number }
10
+ */
11
+ export function checkStrength(password) {
12
+ if (!password) {
13
+ return { score: 0, label: 'Enter a password', percent: 0 };
14
+ }
15
+
16
+ let score = 0;
17
+
18
+ // Length check
19
+ if (password.length >= 8) score++;
20
+ if (password.length >= 12) score++;
21
+
22
+ // Has lowercase letter
23
+ if (/[a-z]/.test(password)) score++;
24
+
25
+ // Has uppercase letter
26
+ if (/[A-Z]/.test(password)) score++;
27
+
28
+ // Has digit
29
+ if (/[0-9]/.test(password)) score++;
30
+
31
+ // Has symbol
32
+ if (/[^a-zA-Z0-9]/.test(password)) score++;
33
+
34
+ // Map score to label (max score is 6)
35
+ const labels = ['Weak', 'Weak', 'Fair', 'Good', 'Strong', 'Strong', 'Excellent'];
36
+ const label = labels[Math.min(score, 6)];
37
+
38
+ // Percent for progress bar
39
+ const percent = Math.min(100, Math.round((score / 6) * 100));
40
+
41
+ return { score, label, percent };
42
+ }
43
+
44
+ /**
45
+ * Get color for strength level
46
+ * @param {string} label - The strength label
47
+ * @returns {string} - CSS color
48
+ */
49
+ export function strengthColor(label) {
50
+ const colors = {
51
+ 'Enter a password': '#ccc',
52
+ 'Weak': '#e53935',
53
+ 'Fair': '#ff9800',
54
+ 'Good': '#4caf50',
55
+ 'Strong': '#2e7d32',
56
+ 'Excellent': '#1b5e20'
57
+ };
58
+ return colors[label] || '#ccc';
59
+ }
60
+
61
+ /**
62
+ * Get tips for improving password
63
+ * @param {string} password - The password to check
64
+ * @returns {string[]} - Array of improvement tips
65
+ */
66
+ export function getPasswordTips(password) {
67
+ if (!password) return ['Enter a password to see tips'];
68
+
69
+ const tips = [];
70
+
71
+ if (password.length < 8) tips.push('Use at least 8 characters');
72
+ else if (password.length < 12) tips.push('Use 12+ characters for better security');
73
+
74
+ if (!/[a-z]/.test(password)) tips.push('Add lowercase letters');
75
+ if (!/[A-Z]/.test(password)) tips.push('Add uppercase letters');
76
+ if (!/[0-9]/.test(password)) tips.push('Add numbers');
77
+ if (!/[^a-zA-Z0-9]/.test(password)) tips.push('Add symbols (!@#$%...)');
78
+
79
+ return tips.length ? tips : ['Great password!'];
80
+ }
@@ -0,0 +1,125 @@
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 SSE Demo</title>
7
+ <style>
8
+ body {
9
+ font-family: system-ui, sans-serif;
10
+ max-width: 800px;
11
+ margin: 2rem auto;
12
+ padding: 0 1rem;
13
+ line-height: 1.6;
14
+ }
15
+ button {
16
+ padding: 0.5rem 1rem;
17
+ font-size: 1rem;
18
+ cursor: pointer;
19
+ margin-right: 0.5rem;
20
+ background: #4a90d9;
21
+ color: white;
22
+ border: none;
23
+ border-radius: 4px;
24
+ }
25
+ button:hover { background: #357abd; }
26
+ button:active { background: #2a5f8f; }
27
+ .output {
28
+ margin-top: 1rem;
29
+ padding: 1rem;
30
+ background: #f5f5f5;
31
+ border-radius: 4px;
32
+ min-height: 2rem;
33
+ border: 1px solid #ddd;
34
+ }
35
+ #events {
36
+ margin-top: 1rem;
37
+ padding: 1rem;
38
+ background: #f0fff0;
39
+ border-radius: 4px;
40
+ min-height: 100px;
41
+ border: 1px solid #9d9;
42
+ }
43
+ .event-item {
44
+ padding: 0.25rem 0;
45
+ border-bottom: 1px solid #ccc;
46
+ font-family: monospace;
47
+ }
48
+ h2 { margin-top: 2rem; color: #333; }
49
+ code { background: #eee; padding: 0.2rem 0.4rem; border-radius: 3px; }
50
+ .grid {
51
+ display: grid;
52
+ grid-template-columns: 1fr 1fr;
53
+ gap: 1rem;
54
+ margin-top: 1rem;
55
+ }
56
+ .card {
57
+ background: #fff;
58
+ padding: 0.75rem;
59
+ border-radius: 4px;
60
+ border: 1px solid #ddd;
61
+ }
62
+ .card strong { display: block; margin-bottom: 0.5rem; color: #666; }
63
+ </style>
64
+ </head>
65
+ <body data-helium>
66
+ <h1>Helium SSE Demo</h1>
67
+ <p>This demo shows Helium's Server-Sent Events support. Run the server with: <code>node examples/sse-server.js</code></p>
68
+
69
+ <h2>1. Hello World (like Datastar's /api/hello-world)</h2>
70
+ <p>Streams a greeting character by character:</p>
71
+ <button @get="/api/hello-world" @target="#message">Start</button>
72
+ <div id="message" class="output">&nbsp;</div>
73
+
74
+ <h2>2. Streaming Counter</h2>
75
+ <p>Receives multiple SSE events counting from 1 to 10:</p>
76
+ <button @get="/api/count" @target="#counter">Start Counting</button>
77
+ <div id="counter" class="output" style="font-size: 2rem; font-weight: bold; text-align: center;">&nbsp;</div>
78
+
79
+ <h2>3. Multi-target Updates</h2>
80
+ <p>Single request updates multiple elements using the SSE <code>event:</code> field as target selector:</p>
81
+ <button @get="/api/dashboard">Load Dashboard</button>
82
+ <div class="grid">
83
+ <div class="card">
84
+ <strong>Header (#header)</strong>
85
+ <div id="header">&nbsp;</div>
86
+ </div>
87
+ <div class="card">
88
+ <strong>Stats (#stats)</strong>
89
+ <div id="stats">&nbsp;</div>
90
+ </div>
91
+ </div>
92
+
93
+ <h2>4. State Updates</h2>
94
+ <p>When target isn't a DOM selector, SSE updates reactive state directly:</p>
95
+ <button @get="/api/time" @target="currentTime">Get Server Time</button>
96
+ <div class="output">
97
+ Server time: <strong @text="currentTime || '(click button)'"></strong>
98
+ </div>
99
+
100
+ <h2>5. Appending Events</h2>
101
+ <p>Events appended as they arrive (simulated activity log):</p>
102
+ <button @get="/api/events" @target="#events:append">Start Stream</button>
103
+ <button @click="$('#events').innerHTML = ''" style="background: #888;">Clear</button>
104
+ <div id="events"></div>
105
+
106
+ <hr style="margin-top: 2rem;">
107
+ <h2>SSE Format</h2>
108
+ <p>Helium's SSE format is simple:</p>
109
+ <pre style="background: #f5f5f5; padding: 1rem; border-radius: 4px; overflow-x: auto;">
110
+ event: #target-selector (or state property name)
111
+ data: &lt;div&gt;HTML content&lt;/div&gt;
112
+
113
+ event: #another-target
114
+ data: More content here
115
+ </pre>
116
+
117
+ <script type="module">
118
+ import helium from '../helium.js';
119
+
120
+ helium({
121
+ currentTime: null
122
+ });
123
+ </script>
124
+ </body>
125
+ </html>