@daz4126/helium 0.31.0 → 1.0.0-beta.2
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/.claude/settings.local.json +3 -1
- package/CLAUDE.md +1 -1
- package/examples/index-lite.html +396 -0
- package/examples/index.html +27 -11
- package/examples/password-utils.js +80 -0
- package/helium-csp.js +252 -0
- package/helium-csp.test.js +776 -0
- package/helium-lite.js +369 -0
- package/helium-lite.test.js +120 -0
- package/helium.js +35 -9
- package/jexpr.js +666 -0
- package/jexpr.test.js +551 -0
- package/package.json +6 -1
- package/test-csp.html +50 -0
package/CLAUDE.md
CHANGED
|
@@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|
|
4
4
|
|
|
5
5
|
## Project Overview
|
|
6
6
|
|
|
7
|
-
Helium is an ultra-light (~
|
|
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
8
|
|
|
9
9
|
## Commands
|
|
10
10
|
|
|
@@ -0,0 +1,396 @@
|
|
|
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
|
+
<!-- Clipboard Copy -->
|
|
382
|
+
<div class="card" @data="{ copied: false, textToCopy: 'Hello from Helium!' }">
|
|
383
|
+
<h2>Copy to Clipboard</h2>
|
|
384
|
+
<input @bind="textToCopy" placeholder="Text to copy">
|
|
385
|
+
<button @click="navigator.clipboard.writeText(textToCopy).then(() => (copied = true, setTimeout(() => copied = false, 2000)))">
|
|
386
|
+
<span @text="copied ? 'Copied!' : 'Copy'"></span>
|
|
387
|
+
</button>
|
|
388
|
+
</div>
|
|
389
|
+
|
|
390
|
+
</div>
|
|
391
|
+
</div>
|
|
392
|
+
|
|
393
|
+
<script type="module" src="../helium-lite.js"></script>
|
|
394
|
+
|
|
395
|
+
</body>
|
|
396
|
+
</html>
|
package/examples/index.html
CHANGED
|
@@ -225,6 +225,12 @@
|
|
|
225
225
|
|
|
226
226
|
<div class="examples">
|
|
227
227
|
|
|
228
|
+
<!-- @data Test -->
|
|
229
|
+
<div class="card" data-he="{ word: 'Hello',number: 42 }">
|
|
230
|
+
<h2 data-he-text="word"></h2>
|
|
231
|
+
<p>Number: <b @text="number"></b></p>
|
|
232
|
+
</div>
|
|
233
|
+
|
|
228
234
|
<!-- Counter -->
|
|
229
235
|
<div class="card">
|
|
230
236
|
<h2>Counter</h2>
|
|
@@ -245,12 +251,12 @@
|
|
|
245
251
|
<h2>Dynamic Classes</h2>
|
|
246
252
|
<p>Toggle the active state to change card styling.</p>
|
|
247
253
|
<button @click="active = !active">
|
|
248
|
-
Toggle (<span @text="active ? 'ON' : 'OFF'"
|
|
254
|
+
Toggle (<span @text="active ? 'ON' : 'OFF'"></span>)
|
|
249
255
|
</button>
|
|
250
256
|
</div>
|
|
251
257
|
|
|
252
258
|
<!-- Two-way Binding -->
|
|
253
|
-
<div class="card"
|
|
259
|
+
<div class="card">
|
|
254
260
|
<h2>Two-Way Binding</h2>
|
|
255
261
|
<input @bind="firstname" placeholder="First name">
|
|
256
262
|
<input @bind="lastname" placeholder="Last name">
|
|
@@ -398,16 +404,17 @@
|
|
|
398
404
|
</p>
|
|
399
405
|
</div>
|
|
400
406
|
|
|
401
|
-
<!-- Password Strength -->
|
|
402
|
-
<div class="card" @data="{ password: '' }">
|
|
407
|
+
<!-- Password Strength (uses @import) -->
|
|
408
|
+
<div class="card" @data="{ password: '' }" @import="password-utils">
|
|
403
409
|
<h2>Password Strength</h2>
|
|
404
410
|
<input type="password" @bind="password" placeholder="Enter password">
|
|
405
|
-
<span @calculate:
|
|
406
|
-
<span @calculate:
|
|
411
|
+
<span @calculate:result="checkStrength(password)"></span>
|
|
412
|
+
<span @calculate:tips="getPasswordTips(password)"></span>
|
|
407
413
|
<div class="progress-bar">
|
|
408
|
-
<div class="progress-fill" :style="{ width:
|
|
414
|
+
<div class="progress-fill" :style="{ width: result.percent + '%', background: strengthColor(result.label) }"></div>
|
|
409
415
|
</div>
|
|
410
|
-
<p>Strength: <b @text="
|
|
416
|
+
<p>Strength: <b @text="result.label"></b></p>
|
|
417
|
+
<p style="font-size: 0.85rem; color: #666;" @text="tips.join(' • ')"></p>
|
|
411
418
|
</div>
|
|
412
419
|
|
|
413
420
|
<!-- Init Example -->
|
|
@@ -429,12 +436,21 @@
|
|
|
429
436
|
<p>History: <span @text="history.join(', ') || 'None yet'">None yet</span></p>
|
|
430
437
|
</div>
|
|
431
438
|
|
|
439
|
+
<!-- Clipboard Copy -->
|
|
440
|
+
<div class="card" @data="{ copied: false, textToCopy: 'Hello from Helium!' }">
|
|
441
|
+
<h2>Copy to Clipboard</h2>
|
|
442
|
+
<input @bind="textToCopy" placeholder="Text to copy">
|
|
443
|
+
<button @click="navigator.clipboard.writeText(textToCopy).then(() => { copied = true; setTimeout(() => copied = false, 2000) })">
|
|
444
|
+
<span @text="copied ? 'Copied!' : 'Copy'"></span>
|
|
445
|
+
</button>
|
|
446
|
+
</div>
|
|
447
|
+
|
|
432
448
|
</div>
|
|
433
449
|
|
|
434
450
|
<!-- AJAX Examples Section -->
|
|
435
451
|
<div class="page" style="margin-top: 2rem;" @data="{
|
|
436
452
|
templates: {
|
|
437
|
-
post: data => `<section class='post'><h1>${data
|
|
453
|
+
post: data => `<section class='post'><h1>${data && data.title || 'Loading...'}</h1><p>${data && data.body || ''}</p></section>`
|
|
438
454
|
}
|
|
439
455
|
}">
|
|
440
456
|
<h1>AJAX Examples</h1>
|
|
@@ -449,7 +465,7 @@
|
|
|
449
465
|
</section>
|
|
450
466
|
|
|
451
467
|
<!-- Request Methods -->
|
|
452
|
-
<section class="card"
|
|
468
|
+
<section class="card">
|
|
453
469
|
<h2>Request Methods</h2>
|
|
454
470
|
<p>PUT adds to account, DELETE (shift+click) removes.</p>
|
|
455
471
|
<div class="button-group">
|
|
@@ -479,7 +495,7 @@
|
|
|
479
495
|
</div>
|
|
480
496
|
</div>
|
|
481
497
|
|
|
482
|
-
<script type="module" src="../helium.js"></script>
|
|
498
|
+
<script type="module" src="../helium-csp.js"></script>
|
|
483
499
|
|
|
484
500
|
</body>
|
|
485
501
|
</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
|
+
}
|