@daz4126/helium 0.1.0 → 0.3.0

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.
Files changed (4) hide show
  1. package/LLM_GUIDE.md +138 -0
  2. package/README.md +141 -5
  3. package/helium.js +196 -45
  4. package/package.json +2 -2
package/LLM_GUIDE.md ADDED
@@ -0,0 +1,138 @@
1
+ # Helium LLM Guide
2
+
3
+ This guide explains how to use **Helium** to build interactive UIs.
4
+ It is designed for **AI assistants** and **developers** to have a clear playbook for generating Helium code.
5
+
6
+ ---
7
+
8
+ ## Core Ideas
9
+
10
+ - Helium uses **HTML attributes** to connect state and DOM.
11
+ - Attributes can use either `@` or `data-he-*` prefixes. Both are valid.
12
+ - State lives in a JavaScript object (via `helium({...})`) or in HTML with `@data`.
13
+ - Changes to state automatically update the DOM where it is bound.
14
+
15
+ ---
16
+
17
+ ## Directives
18
+
19
+ ### State
20
+ - `@data` / `data-he` → Initialize state.
21
+ ```html
22
+ <div @data="{ count: 0, open: false }"></div>
23
+ ```
24
+
25
+ ### Reactive values
26
+ @react / data-he-react → Bind text content to state.
27
+
28
+ ```html
29
+ <span @react="count"></span>
30
+ ```
31
+
32
+ ### Two-way binding
33
+ @bind / data-he-bind → Sync input values with state.
34
+
35
+ ```html
36
+ <input @bind="name" placeholder="Enter your name">
37
+ ```
38
+
39
+ ### Visibility
40
+ @hidden / data-he-hidden → Hide element if expression is true.
41
+ @visible / data-he-visible → Show element if expression is true.
42
+
43
+ ```html
44
+ <div @hidden="!open">Only visible if `open` is true</div>
45
+ ```
46
+
47
+ ### Attribute binding
48
+ :attribute → Dynamically bind attributes.
49
+
50
+ ```html
51
+ <p :class="count > 3 ? 'danger' : 'safe'">Hello</p>
52
+ ```
53
+
54
+ ###Events
55
+
56
+ @event / data-he-on* → Attach event listeners.
57
+ .prevent → preventDefault
58
+ .once → run once then remove
59
+ .outside → listen outside the element
60
+
61
+ ```html
62
+ <button @click="count++">Increment</button>
63
+ <button @click.prevent="submitForm()">Save</button>
64
+ ```
65
+
66
+ ### Refs
67
+
68
+ @ref / data-he-ref → Create named references to elements.
69
+
70
+ ```html
71
+ <div @ref="box"></div>
72
+ ```
73
+
74
+
75
+ ### Initialization
76
+
77
+ @init / data-he-init → Run code once on init.
78
+
79
+ ```html
80
+ <div @init="console.log('ready')"></div>
81
+ ```
82
+
83
+ ### Example Patterns
84
+
85
+ #### Counter
86
+
87
+ ```html
88
+ <div @data="{ count: 0 }">
89
+ <h1 @react="count"></h1>
90
+ <button @click="count++">+</button>
91
+ <button @click="count--">-</button>
92
+ </div>
93
+ ```
94
+
95
+ #### Modal
96
+
97
+ ```html
98
+ <div @data="{ open: false }">
99
+ <button @click="open = true">Open Modal</button>
100
+
101
+ <div @visible="open" class="modal">
102
+ <p>Hello Helium!</p>
103
+ <button @click="open = false">Close</button>
104
+ </div>
105
+ </div>
106
+ ```
107
+
108
+ #### Form Handling
109
+
110
+ ```html
111
+ <div @data="{ name: '', email: '' }">
112
+ <input @bind="name" placeholder="Name">
113
+ <input @bind="email" placeholder="Email">
114
+ <button @click.prevent="console.log(name, email)">
115
+ Submit
116
+ </button>
117
+ </div>
118
+ ```
119
+
120
+ ## Tips for LLMs
121
+
122
+ Always start with @data to define state.
123
+
124
+ * Use @react for dynamic text.
125
+ * Use @bind for inputs.
126
+ * Use @click (and modifiers) for actions.
127
+ * Use @hidden/@visible for conditionals.
128
+ * Use :attribute="expression" for dynamic attributes.
129
+ * Use @ref if you need to manipulate elements directly.
130
+ * Use @init for setup logic.
131
+
132
+ When in Doubt ...
133
+
134
+ * Think in terms of state → DOM binding.
135
+ * If something should display, use @react.
136
+ * If it should toggle, use @hidden/@visible.
137
+ * If it should update on input, use @bind.
138
+ * If it should respond to a click, use @click.
package/README.md CHANGED
@@ -1,11 +1,147 @@
1
1
  # 🎈Helium🎈
2
2
 
3
- Light & powerful .... Helium makes HTML interactive!
3
+ The ultra-light library that makes HTML interactive!
4
+
5
+ Here's a simple example of a button that counts clicks and turns red after more than 3 presses:
4
6
 
5
7
  ```html
6
- <main @helium>
7
- <button @click="count++" :style="count > 3 && 'background: red'">
8
+ <button @helium @click="count++" :style="count > 3 && 'background: red'">
8
9
  clicked <span @react="count">0</span> times
9
- </button>
10
- </main>
10
+ </button>
11
+ ```
12
+
13
+ [See more examples here](https://codepen.io/daz4126/pen/YPwwdBK)
14
+
15
+ To use, just import from the CDN then call the `helium` funtion (no install or build step required!):
16
+
17
+ ```javascript
18
+ import helium from "https://cdn.jsdelivr.net/gh/daz-codes/helium/helium.js"
19
+ helium()
20
+ ```
21
+
22
+ Alernatively you can install from NPM:
23
+
24
+ ```bash
25
+ npm install @daz4126/helium
26
+ ```
27
+
28
+ Then include it in your
29
+
30
+ ```javascript
31
+ import helium from "@daz4126/helium"
32
+ helium
33
+ ```
34
+
35
+ ## `@helium`
36
+
37
+ This attribute sets the root element. Helium attributes can only be used on this element and its children. If not set then it defaults to `document`.
38
+
39
+ Alias: `data-helium`
40
+
41
+ ## `@react`
42
+
43
+ Inserts the result of a JavaScript expression into the text-content of the element.
44
+
45
+ This will update the textContent of the element with the value of the `count` variable:
46
+
47
+ ```html
48
+ <b @react="count">0</b>
49
+ ```
50
+
51
+ You can also use expresions. This will update the textContent of the element with the value of the `name` variable but in uppercase:
52
+
53
+ ```html
54
+ <span @react="name.toUpperCase()">0</span>
55
+ ```
56
+
57
+ Alias: `data-he-react`
58
+
59
+ ## `@bind`
60
+
61
+ Creates a 2-way binding between an input or textcontent element's value attribute and a variable.
62
+
63
+ Whatever is entered in the following input field will be stored as a variable called `name`:
64
+
65
+ ```html
66
+ <input @bind="name" placeholder="Enter your name">
67
+ ```
68
+
69
+ Alias: `data-he-bind`
70
+
71
+ ## `@hidden` & `@visible`
72
+
73
+ Makes the element hidden or visible depending on the result of a JavaScript expression.
74
+
75
+ ```html
76
+ <div @hidden="count > 3">Only visible if the count is greater than 3</div>
77
+ ```
78
+
79
+ Alias: `data-he-hidden` & `data-he-visible`
80
+
81
+ ## `@data`
82
+
83
+ Initializes variables that can be used in JacaScript expressions.
84
+
85
+ ```html
86
+ <div @data="{ count: 0, open: false }"></div>
87
+ ```
88
+
89
+ Alias: `data-he-data`
90
+
91
+ ## `@ref`
92
+
93
+ Creates a reference to the element that can be used in JavaScript expressions.
94
+
95
+ For example, this will create a reference called `$list` to this element:
96
+
97
+ ```html
98
+ <ul @ref="list"></ul>
99
+ ```
100
+
101
+ This element can then be accessed in other JavaScript expressions as `$list`, for example:
102
+
103
+ ```html
104
+ <button @click="appendTo($list)">Add Task</button>
11
105
  ```
106
+
107
+ Alias: `data-he-ref`
108
+
109
+ ## `@init`
110
+
111
+ A JavaScript expression that will run once when Helium initializes.
112
+
113
+ ```html
114
+ <div @init="timestamp = Date.now()"></div>
115
+ ```
116
+
117
+ Alias: `data-he-init`
118
+
119
+ ## Event Listeners & Handlers
120
+
121
+ Event listeners and handlers can be created by prepending `@` before the event name, for example `@click="count++"` will run the cound `count++` when the element is clicked on.
122
+
123
+ ```html
124
+ <button @click="count++">Increment</button>
125
+ ```
126
+
127
+ You can add modifiers of `prevent` to prevent the default behaviour, `outside` to only fire when the event happens outside the element and `once` to only run the event handler once.
128
+
129
+ ```html
130
+ <button @click.prevent="submitForm()">Save</button>
131
+ ```
132
+
133
+ Alias: prepend the event name with `data-he-on`, for example `data-he-onclick="count++"`
134
+
135
+ ## Conditional Attributes
136
+
137
+ ## Magic Attributes
138
+
139
+ `$` is an alias for `document.querySelector`
140
+
141
+ `$el` is an alias for the element
142
+
143
+ `$event` is an alias for the event object of an event handler
144
+
145
+ ## Default Variables and Functions
146
+
147
+ The helium function accepts a single JavaScript object as an argument. This can include default variable values and functions that can then be called inside the JavaScript expressions.
package/helium.js CHANGED
@@ -1,60 +1,211 @@
1
1
  export default function helium(data = {}) {
2
- const root = document.querySelector("[\\@helium]") || document.querySelector("[data-helium]") || document.body
3
- const [bindings, refs] = [new Map(), new Map()]
4
- const $ = selector => document.querySelector(selector)
2
+ const root =
3
+ document.querySelector("[\\@helium]") ||
4
+ document.querySelector("[data-helium]") ||
5
+ document.body;
6
+ const [bindings, refs] = [new Map(), new Map()];
7
+ const $ = (selector) => document.querySelector(selector);
5
8
  const state = new Proxy(data, {
6
- get(target, prop) { return target[prop] },
9
+ get(target, prop) {
10
+ return target[prop];
11
+ },
7
12
  set(target, prop, value) {
8
- target[prop] = value
9
- if (bindings.has(prop)) bindings.get(prop).forEach(binding => applyBinding(binding))
10
- return true
11
- }
12
- })
13
+ target[prop] = value;
14
+ if (bindings.has(prop))
15
+ bindings.get(prop).forEach((binding) => applyBinding(binding));
16
+ return true;
17
+ },
18
+ });
13
19
  function applyBinding(binding, event = {}, elCtx = binding.el) {
14
- const { el, prop, fn } = binding
15
- const result = fn($, state, event, elCtx, ...Object.values(data), ...[...refs.values()])
16
- if (prop in el) el[prop] = result
17
- else el.setAttribute(prop, result)
20
+ const { el, prop, fn } = binding;
21
+ const result = fn(
22
+ $,
23
+ state,
24
+ event,
25
+ elCtx,
26
+ ...Object.values(data),
27
+ ...[...refs.values()],
28
+ );
29
+ if (prop in el) el[prop] = result;
30
+ else el.setAttribute(prop, result);
18
31
  }
19
32
  function compileExpression(expr, withReturn = false) {
20
- try {
21
- return new Function("$", "$state", "$event", "$el", ...Object.keys(data), ...[...refs.keys()],`with($state) { ${withReturn ? "return" : ""} (${expr.trim()}) }`)
22
- } catch (err) { return () => expr }
33
+ try {
34
+ return new Function(
35
+ "$",
36
+ "$state",
37
+ "$event",
38
+ "$el",
39
+ ...Object.keys(data),
40
+ ...[...refs.keys()],
41
+ `with($state) { ${withReturn ? "return" : ""} (${expr.trim()}) }`,
42
+ );
43
+ } catch (err) {
44
+ return () => expr;
45
+ }
23
46
  }
24
47
  function processElements(element) {
25
- const heliumElements = [element, ...Array.from(element.querySelectorAll("*")).filter(el => Array.from(el.attributes).some(attr => attr.name.startsWith("data-he-"))),]
26
- const xpath = document.evaluate(".//*[@*[starts-with(name(), '@') or starts-with(name(), ':')]]",element,null,XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,null)
27
- for (let i = 0; i < xpath.snapshotLength; i++) heliumElements.push(xpath.snapshotItem(i))
28
- heliumElements.forEach(el => {
48
+ const heliumElements = [
49
+ element,
50
+ ...Array.from(element.querySelectorAll("*")).filter((el) =>
51
+ Array.from(el.attributes).some((attr) =>
52
+ attr.name.startsWith("data-he-"),
53
+ ),
54
+ ),
55
+ ];
56
+ const xpath = document.evaluate(
57
+ ".//*[@*[starts-with(name(), '@') or starts-with(name(), ':')]]",
58
+ element,
59
+ null,
60
+ XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
61
+ null,
62
+ );
63
+ for (let i = 0; i < xpath.snapshotLength; i++)
64
+ heliumElements.push(xpath.snapshotItem(i));
65
+ heliumElements.forEach((el) => {
66
+ for (const { name, value } of el.attributes) {
67
+ if (
68
+ name == "@react" ||
69
+ name == "data-he-react" ||
70
+ name == "@text" ||
71
+ name == "data-he-text" ||
72
+ name == "@bind" ||
73
+ name == "data-he-bind"
74
+ ) {
75
+ try {
76
+ new Function(`let ${value} = 1`);
77
+ state[value] ||=
78
+ name == "@react" || name == "data-he-react"
79
+ ? el.textContent
80
+ : el.value;
81
+ } catch (e) {}
82
+ }
83
+ }
84
+ });
85
+ heliumElements.forEach((el) => {
29
86
  for (const { name, value } of el.attributes) {
30
- if (name == "@data" || name == "data-he") Object.assign(state, compileExpression(value, true)($, state, {}, el, ...Object.values(data), ...[...refs.values()]))
31
- if (name == "@ref" || name == "data-he-ref") refs.set("$" + value, el)
32
- if (name == "@react" || name == "data-he-react") Object.keys(state).filter(key => value.includes(key)).forEach(val =>
33
- bindings.set(val, [...(bindings.get(val) || []), { el, prop: "textContent", expr: value, fn: compileExpression(value, true) }])
34
- )
87
+ if (name == "@data" || name == "data-he")
88
+ Object.assign(
89
+ state,
90
+ compileExpression(value, true)(
91
+ $,
92
+ state,
93
+ {},
94
+ el,
95
+ ...Object.values(data),
96
+ ...[...refs.values()],
97
+ ),
98
+ );
99
+ if (name == "@ref" || name == "data-he-ref") refs.set("$" + value, el);
100
+ if (
101
+ name == "@react" ||
102
+ name == "data-he-react" ||
103
+ name == "@text" ||
104
+ name == "data-he-text"
105
+ )
106
+ Object.keys(state)
107
+ .filter((key) => value.includes(key))
108
+ .forEach((val) =>
109
+ bindings.set(val, [
110
+ ...(bindings.get(val) || []),
111
+ {
112
+ el,
113
+ prop: "textContent",
114
+ expr: value,
115
+ fn: compileExpression(value, true),
116
+ },
117
+ ]),
118
+ );
35
119
  if (name == "@bind" || name == "data-he-bind") {
36
- el.addEventListener("input", e => (state[value] = e.target.value))
37
- bindings.set(value, [...(bindings.get(value) || []), { el, prop: "value", expr: value, fn: compileExpression(value, true) }])
38
- el.value = state[value]
120
+ el.addEventListener("input", (e) => (state[value] = e.target.value));
121
+ bindings.set(value, [
122
+ ...(bindings.get(value) || []),
123
+ {
124
+ el,
125
+ prop: "value",
126
+ expr: value,
127
+ fn: compileExpression(value, true),
128
+ },
129
+ ]);
130
+ el.value = state[value];
39
131
  }
40
- if (name == "@hidden" || name == "@visible" || name == "data-he-hidden" || name == "data-he-visible") Object.keys(state).filter(key => value.includes(key)).forEach(val => bindings.set(val, [...(bindings.get(val) || []), { el, prop: "hidden", expr: value, fn: compileExpression(`${(name == "@hidden" || name == "data-he-hidden") ? "!" : ""}!(${value})`, true) }]))
41
- if (name.startsWith(":")) Object.keys(state).filter(key => value.includes(key)).forEach(val => bindings.set(val, [...(bindings.get(val) || []), { el, prop: name.split(":")[1], expr: value, fn: compileExpression(value, true) }]))
42
- if (name == "@init" || name == "data-he-init") compileExpression(value, false)($, state, undefined, el, ...Object.values(data), ...[...refs.values()])
132
+ if (
133
+ name == "@hidden" ||
134
+ name == "@visible" ||
135
+ name == "data-he-hidden" ||
136
+ name == "data-he-visible"
137
+ )
138
+ Object.keys(state)
139
+ .filter((key) => value.includes(key))
140
+ .forEach((val) =>
141
+ bindings.set(val, [
142
+ ...(bindings.get(val) || []),
143
+ {
144
+ el,
145
+ prop: "hidden",
146
+ expr: value,
147
+ fn: compileExpression(
148
+ `${name == "@hidden" || name == "data-he-hidden" ? "!" : ""}!(${value})`,
149
+ true,
150
+ ),
151
+ },
152
+ ]),
153
+ );
154
+ if (name.startsWith(":"))
155
+ Object.keys(state)
156
+ .filter((key) => value.includes(key))
157
+ .forEach((val) =>
158
+ bindings.set(val, [
159
+ ...(bindings.get(val) || []),
160
+ {
161
+ el,
162
+ prop: name.split(":")[1],
163
+ expr: value,
164
+ fn: compileExpression(value, true),
165
+ },
166
+ ]),
167
+ );
168
+ if (name == "@init" || name == "data-he-init")
169
+ compileExpression(value, false)(
170
+ $,
171
+ state,
172
+ undefined,
173
+ el,
174
+ ...Object.values(data),
175
+ ...[...refs.values()],
176
+ );
43
177
  else if (name.startsWith("@") || name.startsWith("data-he-on")) {
44
- const [eventName, ...modifiers] = name.slice(name.startsWith("@") ? 1 : 10).split(".")
45
- const receiver = modifiers.includes("outside") ? document : el
178
+ const [eventName, ...modifiers] = name
179
+ .slice(name.startsWith("@") ? 1 : 10)
180
+ .split(".");
181
+ const receiver = modifiers.includes("outside") ? document : el;
46
182
  receiver.addEventListener(eventName, function _handler(e) {
47
- if (modifiers.includes("prevent")) e.preventDefault()
48
- if (!modifiers.includes("outside") || !el.contains(e.target)) compileExpression(value, false)($, state, e, el, ...Object.values(data), ...[...refs.values()])
49
- if (modifiers.includes("once")) el.removeEventListener(eventName, _handler)
50
- })}}})
51
- const observer = new MutationObserver(mutations => {
52
- for (const m of mutations) {
53
- for (const node of m.addedNodes) {
54
- if (node.nodeType === 1) processElements(node)
55
- }}})
56
- observer.observe(element, { childList: true, subtree: true })
57
- for (const [key, items] of bindings.entries()) items.forEach(binding => applyBinding(binding))
183
+ if (modifiers.includes("prevent")) e.preventDefault();
184
+ if (!modifiers.includes("outside") || !el.contains(e.target))
185
+ compileExpression(value, false)(
186
+ $,
187
+ state,
188
+ e,
189
+ el,
190
+ ...Object.values(data),
191
+ ...[...refs.values()],
192
+ );
193
+ if (modifiers.includes("once"))
194
+ el.removeEventListener(eventName, _handler);
195
+ });
196
+ }
197
+ }
198
+ });
199
+ const observer = new MutationObserver((mutations) => {
200
+ for (const m of mutations) {
201
+ for (const node of m.addedNodes) {
202
+ if (node.nodeType === 1) processElements(node);
203
+ }
204
+ }
205
+ });
206
+ observer.observe(element, { childList: true, subtree: true });
207
+ for (const [key, items] of bindings.entries())
208
+ items.forEach((binding) => applyBinding(binding));
58
209
  }
59
- processElements(root)
210
+ processElements(root);
60
211
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@daz4126/helium",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "main": "helium.js",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -12,7 +12,7 @@
12
12
  ],
13
13
  "author": "DAZ",
14
14
  "license": "MIT",
15
- "description": "A light yet powerful library that makes HTML interactive",
15
+ "description": "The ultra-light library that makes HTML interactive",
16
16
  "repository": {
17
17
  "type": "git",
18
18
  "url": "git+https://github.com/daz-codes/helium.git"