elements-kit 0.0.1 → 0.0.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 CHANGED
@@ -1,22 +1,332 @@
1
- # Web Elements
1
+ # ElementsKit
2
2
 
3
- Web Elements is a lightweight reactive UI library that transforms native HTMLElements into reactive elements. Perfect for framework-agnostic applications and web component development.
3
+ **Universal reactive primitives for the web.** Signals, JSX, and custom elements that work anywhere standalone, inside React, Vue, or any framework, or as the foundation of your own component model.
4
4
 
5
5
  ```tsx
6
- const externalSignal = signal("something")
6
+ import { signal, computed, reactive } from "elements-kit/signals";
7
+ import { attributes, ATTRIBUTES as attr } from "elements-kit/attributes";
7
8
 
8
- function Elements(property1, property2, children): ReactiveHTMLElement{
9
- const value = signal(0)
10
- return div()
11
- .style.color("green")
12
- .title("tooltip")(
13
- header("This is my header"),
14
- main("Second children")
15
- b(value),
16
- span(externalSignal),
17
- ...children
18
- )
9
+ @attributes
10
+ class CounterElement extends HTMLElement {
11
+ static [attr] = {
12
+ count(this: CounterElement, value: string | null) {
13
+ this.count = Number(value ?? 0);
14
+ },
15
+ };
16
+
17
+ @reactive() count = 0;
18
+ doubled = computed(() => this.count * 2);
19
+
20
+ connectedCallback() {
21
+ this.appendChild(
22
+ <section>
23
+ <p>Count: <strong>{() => this.count}</strong> — Doubled: <strong>{this.doubled}</strong></p>
24
+ <button onClick={() => this.count++}>+1</button>{" "}
25
+ <button onClick={() => this.count--}>−1</button>
26
+ </section> as Element,
27
+ );
28
+ }
29
+ }
30
+
31
+ customElements.define("x-counter", CounterElement);
32
+ ```
33
+
34
+ ---
35
+
36
+ ## Why ElementsKit
37
+
38
+ Modern UI frameworks solve reactivity and rendering together — you adopt the whole system or none of it. ElementsKit separates the two:
39
+
40
+ - **Signals** are the reactive core — fine-grained, framework-agnostic, composable with any rendering model.
41
+ - **JSX** compiles to real `document.createElement` calls — no virtual DOM, no runtime overhead.
42
+ - **Custom elements** are standard browser components — ElementsKit enhances them with signals and JSX without wrapping or abstracting the platform.
43
+
44
+ Use one piece, or all three. Integrate with React for complex UIs. Build web components that work anywhere HTML does.
45
+
46
+ ---
47
+
48
+ ## Installation
49
+
50
+ ```sh
51
+ npm install elements-kit
52
+ ```
53
+
54
+ Configure JSX in your `tsconfig.json`:
55
+
56
+ ```json
57
+ {
58
+ "compilerOptions": {
59
+ "jsx": "react-jsx",
60
+ "jsxImportSource": "elements-kit"
61
+ }
62
+ }
63
+ ```
64
+
65
+ ---
66
+
67
+ ## Signals
68
+
69
+ Fine-grained reactive state. Signals track their dependencies automatically — only the exact computeds and effects that depend on a changed signal are re-evaluated.
70
+
71
+ ```ts
72
+ import { signal, computed, effect, batch, untracked, onCleanup } from "elements-kit/signals";
73
+
74
+ const count = signal(0);
75
+ const doubled = computed(() => count() * 2);
76
+
77
+ const stop = effect(() => {
78
+ console.log("count:", count()); // runs on every change
79
+ });
80
+
81
+ count(1); // → count: 1
82
+ count(2); // → count: 2
83
+ stop(); // unsubscribe
84
+
85
+ batch(() => { count(10); count(20); }); // single notification
86
+
87
+ const raw = untracked(() => count()); // read without subscribing
88
+
89
+ effect(() => {
90
+ const id = setInterval(() => count(count() + 1), 1000);
91
+ onCleanup(() => clearInterval(id)); // runs before re-run or on stop
92
+ });
93
+ ```
94
+
95
+ ### Store
96
+
97
+ A **store** is a class whose fields are made reactive with `@reactive`. It holds shared state — no `render()`, no DOM — and any subscriber updates automatically.
98
+
99
+ ```ts
100
+ import { reactive, computed } from "elements-kit/signals";
101
+
102
+ export class CartStore {
103
+ @reactive() items: { name: string; price: number }[] = [];
104
+ @reactive() discount = 0;
105
+
106
+ total = computed(() =>
107
+ this.items.reduce((s, i) => s + i.price, 0) * (1 - this.discount),
108
+ );
109
+
110
+ add(item: { name: string; price: number }) {
111
+ this.items = [...this.items, item];
112
+ }
19
113
  }
20
114
 
21
- document.body.appendChild(render(Elements))
115
+ export const cart = new CartStore();
22
116
  ```
117
+
118
+ Stores are **framework-agnostic** — the same instance drives a custom element, a React component, and a plain effect in sync.
119
+
120
+ ---
121
+
122
+ ## JSX → DOM
123
+
124
+ JSX compiles directly to `document.createElement`. No virtual DOM, no diffing.
125
+
126
+ ```tsx
127
+ // This:
128
+ const el = <button onClick={() => count(count() + 1)}>{count}</button>;
129
+
130
+ // Is equivalent to:
131
+ const el = document.createElement("button");
132
+ el.addEventListener("click", () => count(count() + 1));
133
+ // `count` signal creates a live text node — updates in place on change
134
+ ```
135
+
136
+ Passing a signal or `() => T` as a child or prop creates a **live binding** — the DOM updates in place, never re-rendering the surrounding tree.
137
+
138
+ ```tsx
139
+ const name = signal("Alice");
140
+
141
+ <p>Hello, {name}!</p> // live text node
142
+ <input value={name} /> // live attribute
143
+ <div class:active={computed(() => name() !== "")} /> // reactive class
144
+ <span style:color={signal("red")} /> // reactive style
145
+ ```
146
+
147
+ ### Prop namespaces
148
+
149
+ | Syntax | Effect |
150
+ |--------|--------|
151
+ | `{signal}` / `{() => fn()}` | Live-bound reactive child |
152
+ | `onClick={fn}` | Event listener (camelCase → `onclick`) |
153
+ | `on:click={fn}` | Explicit event namespace |
154
+ | `class:active={bool}` | Reactive `classList.toggle` |
155
+ | `style:color={value}` | Reactive inline style property |
156
+ | `prop:foo={val}` | Force property assignment (skips `setAttribute`) |
157
+
158
+ ---
159
+
160
+ ## Class Components
161
+
162
+ Any class with a `render()` method returning an `Element` is a component. Components own their state and produce elements.
163
+
164
+ ```tsx
165
+ import { reactive, computed } from "elements-kit/signals";
166
+
167
+ class Counter {
168
+ @reactive() count = 0;
169
+ doubled = computed(() => this.count * 2);
170
+
171
+ render() {
172
+ return (
173
+ <section>
174
+ <p>{() => this.count} × 2 = {this.doubled}</p>
175
+ <button onClick={() => this.count++}>+1</button>
176
+ </section>
177
+ ) as Element;
178
+ }
179
+ }
180
+
181
+ document.getElementById("app")!.appendChild(new Counter().render());
182
+ ```
183
+
184
+ ---
185
+
186
+ ## Custom Elements
187
+
188
+ ElementsKit enhances native `HTMLElement` subclasses — start with the platform, add only what you need.
189
+
190
+ ```ts
191
+ import { reactive, computed } from "elements-kit/signals";
192
+ import { attributes, ATTRIBUTES as attr } from "elements-kit/attributes";
193
+
194
+ @attributes
195
+ class CounterElement extends HTMLElement {
196
+ static [attr] = {
197
+ count(this: CounterElement, value: string | null) {
198
+ this.count = Number(value ?? 0);
199
+ },
200
+ };
201
+
202
+ @reactive() count = 0;
203
+ doubled = computed(() => this.count * 2);
204
+
205
+ connectedCallback() {
206
+ this.appendChild(
207
+ <section>
208
+ <p>{() => this.count} × 2 = {this.doubled}</p>
209
+ <button onClick={() => this.count++}>+1</button>
210
+ </section> as Element,
211
+ );
212
+ }
213
+ }
214
+
215
+ customElements.define("x-counter", CounterElement);
216
+ ```
217
+
218
+ `<x-counter count="5" />` — attribute bound, reactive, works in any HTML context.
219
+
220
+ ---
221
+
222
+ ## React Integration
223
+
224
+ Connect signals and stores to React components via `useSyncExternalStore`:
225
+
226
+ ```tsx
227
+ import { useSignal, useScope } from "elements-kit/signals/react";
228
+ import { cart } from "./cart-store";
229
+
230
+ function CartSummary() {
231
+ // Reads a @reactive field — re-renders only when cart.items changes
232
+ const items = useSignal(() => cart.items);
233
+ const total = useSignal(cart.total); // Computed<T> works directly
234
+
235
+ // Effects tied to this component's lifetime
236
+ useScope(() => {
237
+ effect(() => console.log("cart updated:", items));
238
+ });
239
+
240
+ return <p>{items.length} items — ${total.toFixed(2)}</p>;
241
+ }
242
+ ```
243
+
244
+ The same `cart` store drives custom elements, React trees, and plain scripts — all in sync.
245
+
246
+ ---
247
+
248
+ ## Signal Helpers
249
+
250
+ Pre-built signal factories for common browser APIs:
251
+
252
+ ```ts
253
+ import { createMediaSignal } from "elements-kit/signals/media";
254
+
255
+ const isDark = createMediaSignal("(prefers-color-scheme: dark)");
256
+ const isMobile = createMediaSignal("(max-width: 640px)");
257
+
258
+ effect(() => document.documentElement.classList.toggle("dark", isDark()));
259
+ ```
260
+
261
+ ---
262
+
263
+ ## `For` — Keyed List Rendering
264
+
265
+ Reconciles a reactive array into the DOM. Each item renders once per key — no full re-renders on reorder, add, or remove.
266
+
267
+ ```tsx
268
+ import { For } from "elements-kit";
269
+
270
+ <ul>
271
+ <For each={todos} by={(todo) => todo.id}>
272
+ {(todo) => (
273
+ <li>
274
+ <input type="checkbox" checked={computed(() => todo.done)} on:change={() => (todo.done = !todo.done)} />
275
+ {todo.text}
276
+ </li>
277
+ )}
278
+ </For>
279
+ </ul>
280
+ ```
281
+
282
+ ---
283
+
284
+ ## `@reactive()` Decorator
285
+
286
+ Makes any class field reactive — reads subscribe, writes trigger updates.
287
+
288
+ ```ts
289
+ import { reactive, computed } from "elements-kit/signals";
290
+
291
+ class TodoApp {
292
+ @reactive() todos: Todo[] = [];
293
+ @reactive() showDone = true;
294
+
295
+ visible = computed(() =>
296
+ this.showDone ? this.todos : this.todos.filter((t) => !t.done),
297
+ );
298
+ }
299
+ ```
300
+
301
+ ---
302
+
303
+ ## `@attributes` Decorator
304
+
305
+ Wires `observedAttributes` and `attributeChangedCallback` from a static map:
306
+
307
+ ```ts
308
+ import { attributes, ATTRIBUTES as attr } from "elements-kit/attributes";
309
+
310
+ @attributes
311
+ class MyElement extends HTMLElement {
312
+ static [attr] = {
313
+ value(this: MyElement, v: string | null) {
314
+ this.value = v ?? "";
315
+ },
316
+ };
317
+
318
+ @reactive() value = "";
319
+ }
320
+ ```
321
+
322
+ ---
323
+
324
+ ## Roadmap
325
+
326
+ - [ ] More signal helpers (`localStorage`, `IntersectionObserver`, `ResizeObserver`, …)
327
+ - [ ] Context — share state across a subtree without prop drilling
328
+ - [ ] Async signal — `signal.from(promise)`, `signal.from(observable)`
329
+ - [ ] UI library — pre-built reactive components built on ElementsKit primitives
330
+ - [ ] More framework integrations (Vue, Solid, Angular, …)
331
+ - [ ] Tutorial — building a full app from scratch
332
+ - [ ] Complete TypeScript strict-mode coverage
@@ -0,0 +1,90 @@
1
+ //#region src/attributes.d.ts
2
+ interface AttrChangeHandler<T> {
3
+ (this: T, value: string | null, oldValue?: string | null): void;
4
+ }
5
+ declare const ATTRIBUTES: unique symbol;
6
+ /**
7
+ * Dispatches an attribute change to the matching handler in the static `attributes` map,
8
+ * walking the prototype chain for inherited handlers.
9
+ * @example
10
+ * ```ts
11
+ * class MyElement extends HTMLElement {
12
+ * static [ATTRIBUTES]: Attributes<MyElement> = {
13
+ * count(value) {
14
+ * this.#count = Number(value);
15
+ * },
16
+ * };
17
+ * static observedAttributes: string[] = observedAttributes(MyElement);
18
+ *
19
+ * attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null) {
20
+ * dispatchAttrChange.call(this, name, oldValue, newValue);
21
+ * }
22
+ * }
23
+ * ```
24
+ */
25
+ declare function dispatchAttrChange<T extends {
26
+ constructor: {
27
+ [ATTRIBUTES]: Record<string, AttrChangeHandler<T>>;
28
+ };
29
+ }>(this: T, name: string, oldValue: string | null, newValue: string | null): void;
30
+ type Attributes<T> = Record<string, AttrChangeHandler<T>>;
31
+ /**
32
+ * Returns a deduplicated array of all observed attribute names for a custom element class and its ancestors.
33
+ *
34
+ * Call after defining static `[ATTRIBUTES]`, and assign to static `observedAttributes`.
35
+ *
36
+ * Example:
37
+ * ```ts
38
+ * class MyElement extends HTMLElement {
39
+ * static [ATTRIBUTES]: Attributes<MyElement> = {
40
+ * count(value) {
41
+ * this.#count = Number(value);
42
+ * },
43
+ * };
44
+ * static observedAttributes: string[] = observedAttributes(MyElement);
45
+ * }
46
+ *
47
+ * class ChildElement extends MyElement {
48
+ * static [ATTRIBUTES]: Attributes<ChildElement> = {
49
+ * bar(value) {
50
+ * // ...
51
+ * },
52
+ * };
53
+ * static observedAttributes: string[] = observedAttributes(ChildElement);
54
+ * }
55
+ * // ChildElement.observedAttributes will include both 'count' and 'bar', deduplicated.
56
+ * ```
57
+ *
58
+ * @param cls The custom element class constructor
59
+ * @returns Array of unique attribute names to observe
60
+ */
61
+ declare function observedAttributes(cls: any): string[];
62
+ /**
63
+ * A class decorator that automatically wires up `observedAttributes` and `attributeChangedCallback`
64
+ * from a static `[ATTRIBUTES]` map.
65
+ *
66
+ * The `this` type inside attribute handlers is automatically inferred from the decorated class.
67
+ *
68
+ * @example
69
+ * ```ts
70
+ * @attributes
71
+ * class MyElement extends HTMLElement {
72
+ * static [ATTRIBUTES] = {
73
+ * count(this: MyElement, value: string | null) {
74
+ * this.count = Number(value);
75
+ * },
76
+ * };
77
+ * }
78
+ * ```
79
+ */
80
+ type AttributeTarget<T extends abstract new (...args: any[]) => HTMLElement> = T & {
81
+ [ATTRIBUTES]: Record<string, AttrChangeHandler<InstanceType<T>>>;
82
+ };
83
+ type AttributeDecorated<T extends abstract new (...args: any[]) => HTMLElement> = T & (new (...args: any[]) => InstanceType<T> & {
84
+ attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void;
85
+ }) & {
86
+ observedAttributes: string[];
87
+ };
88
+ declare function attributes<T extends abstract new (...args: any[]) => HTMLElement>(target: AttributeTarget<T>, context: ClassDecoratorContext<T>): AttributeDecorated<T>;
89
+ //#endregion
90
+ export { ATTRIBUTES, AttrChangeHandler, AttributeDecorated, AttributeTarget, Attributes, attributes, dispatchAttrChange, observedAttributes };
@@ -0,0 +1,81 @@
1
+ //#region src/attributes.ts
2
+ const ATTRIBUTES = Symbol("attributes");
3
+ /**
4
+ * Dispatches an attribute change to the matching handler in the static `attributes` map,
5
+ * walking the prototype chain for inherited handlers.
6
+ * @example
7
+ * ```ts
8
+ * class MyElement extends HTMLElement {
9
+ * static [ATTRIBUTES]: Attributes<MyElement> = {
10
+ * count(value) {
11
+ * this.#count = Number(value);
12
+ * },
13
+ * };
14
+ * static observedAttributes: string[] = observedAttributes(MyElement);
15
+ *
16
+ * attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null) {
17
+ * dispatchAttrChange.call(this, name, oldValue, newValue);
18
+ * }
19
+ * }
20
+ * ```
21
+ */
22
+ function dispatchAttrChange(name, oldValue, newValue) {
23
+ let cls = this.constructor;
24
+ while (cls) {
25
+ if (cls[ATTRIBUTES] && name in cls[ATTRIBUTES]) {
26
+ cls[ATTRIBUTES][name].call(this, newValue, oldValue);
27
+ return;
28
+ }
29
+ cls = Object.getPrototypeOf(cls);
30
+ }
31
+ }
32
+ /**
33
+ * Returns a deduplicated array of all observed attribute names for a custom element class and its ancestors.
34
+ *
35
+ * Call after defining static `[ATTRIBUTES]`, and assign to static `observedAttributes`.
36
+ *
37
+ * Example:
38
+ * ```ts
39
+ * class MyElement extends HTMLElement {
40
+ * static [ATTRIBUTES]: Attributes<MyElement> = {
41
+ * count(value) {
42
+ * this.#count = Number(value);
43
+ * },
44
+ * };
45
+ * static observedAttributes: string[] = observedAttributes(MyElement);
46
+ * }
47
+ *
48
+ * class ChildElement extends MyElement {
49
+ * static [ATTRIBUTES]: Attributes<ChildElement> = {
50
+ * bar(value) {
51
+ * // ...
52
+ * },
53
+ * };
54
+ * static observedAttributes: string[] = observedAttributes(ChildElement);
55
+ * }
56
+ * // ChildElement.observedAttributes will include both 'count' and 'bar', deduplicated.
57
+ * ```
58
+ *
59
+ * @param cls The custom element class constructor
60
+ * @returns Array of unique attribute names to observe
61
+ */
62
+ function observedAttributes(cls) {
63
+ const s = new Set(Object.keys(cls[ATTRIBUTES] || {}));
64
+ let _cls = Object.getPrototypeOf(cls);
65
+ while (_cls) {
66
+ if (_cls.observedAttributes) _cls.observedAttributes.forEach((attr) => s.add(attr));
67
+ _cls = Object.getPrototypeOf(_cls);
68
+ }
69
+ return Array.from(s);
70
+ }
71
+ function attributes(target, context) {
72
+ context.addInitializer(function() {
73
+ target.observedAttributes = observedAttributes(target);
74
+ });
75
+ target.prototype.attributeChangedCallback = function(name, oldValue, newValue) {
76
+ dispatchAttrChange.call(this, name, oldValue, newValue);
77
+ };
78
+ return target;
79
+ }
80
+ //#endregion
81
+ export { ATTRIBUTES, attributes, dispatchAttrChange, observedAttributes };