@lab206/dom 0.1.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 (69) hide show
  1. package/LICENSE +114 -0
  2. package/README.md +6 -0
  3. package/dist/bind.d.ts +23 -0
  4. package/dist/bind.d.ts.map +1 -0
  5. package/dist/bind.js +170 -0
  6. package/dist/bind.js.map +1 -0
  7. package/dist/component.d.ts +47 -0
  8. package/dist/component.d.ts.map +1 -0
  9. package/dist/component.js +95 -0
  10. package/dist/component.js.map +1 -0
  11. package/dist/dynamic.d.ts +17 -0
  12. package/dist/dynamic.d.ts.map +1 -0
  13. package/dist/dynamic.js +68 -0
  14. package/dist/dynamic.js.map +1 -0
  15. package/dist/fragment.d.ts +13 -0
  16. package/dist/fragment.d.ts.map +1 -0
  17. package/dist/fragment.js +41 -0
  18. package/dist/fragment.js.map +1 -0
  19. package/dist/h.d.ts +26 -0
  20. package/dist/h.d.ts.map +1 -0
  21. package/dist/h.js +164 -0
  22. package/dist/h.js.map +1 -0
  23. package/dist/index.d.ts +29 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +23 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/insert.d.ts +5 -0
  28. package/dist/insert.d.ts.map +1 -0
  29. package/dist/insert.js +10 -0
  30. package/dist/insert.js.map +1 -0
  31. package/dist/jsx-runtime.d.ts +52 -0
  32. package/dist/jsx-runtime.d.ts.map +1 -0
  33. package/dist/jsx-runtime.js +43 -0
  34. package/dist/jsx-runtime.js.map +1 -0
  35. package/dist/list.d.ts +23 -0
  36. package/dist/list.d.ts.map +1 -0
  37. package/dist/list.js +77 -0
  38. package/dist/list.js.map +1 -0
  39. package/dist/mount.d.ts +19 -0
  40. package/dist/mount.d.ts.map +1 -0
  41. package/dist/mount.js +40 -0
  42. package/dist/mount.js.map +1 -0
  43. package/dist/on.d.ts +8 -0
  44. package/dist/on.d.ts.map +1 -0
  45. package/dist/on.js +7 -0
  46. package/dist/on.js.map +1 -0
  47. package/dist/props.d.ts +54 -0
  48. package/dist/props.d.ts.map +1 -0
  49. package/dist/props.js +211 -0
  50. package/dist/props.js.map +1 -0
  51. package/dist/show.d.ts +12 -0
  52. package/dist/show.d.ts.map +1 -0
  53. package/dist/show.js +42 -0
  54. package/dist/show.js.map +1 -0
  55. package/package.json +71 -0
  56. package/src/bind.ts +195 -0
  57. package/src/component.ts +127 -0
  58. package/src/dynamic.ts +93 -0
  59. package/src/fragment.ts +43 -0
  60. package/src/h.ts +236 -0
  61. package/src/index.ts +54 -0
  62. package/src/insert.ts +14 -0
  63. package/src/jsx-runtime.ts +100 -0
  64. package/src/list.ts +111 -0
  65. package/src/mount.ts +46 -0
  66. package/src/on.ts +31 -0
  67. package/src/props.ts +259 -0
  68. package/src/show.ts +52 -0
  69. package/src/test-setup.ts +17 -0
@@ -0,0 +1,54 @@
1
+ import type { Signal } from "@lab206/core";
2
+ /**
3
+ * Reactive component props.
4
+ *
5
+ * Access like normal fields (`props.name`). If the parent passed a signal or
6
+ * a zero-arg accessor `() => …`, the value is unwrapped **on read** and tracked.
7
+ *
8
+ * @example
9
+ * ```tsx
10
+ * // Parent — pass a signal or accessor for live updates:
11
+ * <Hello name={name} />
12
+ * <Hello name={() => user().name} />
13
+ *
14
+ * // Child — read as a plain field inside reactive scopes:
15
+ * <p>{() => props.name}</p>
16
+ * ```
17
+ */
18
+ export type ReactiveProps<P extends object> = {
19
+ readonly [K in keyof P]: P[K];
20
+ };
21
+ /** True if value looks like a Powers signal. */
22
+ export declare function isSignal(value: unknown): value is Signal<unknown>;
23
+ /** True if this object was created by `createProps` / `mergeProps` / `splitProps`. */
24
+ export declare function isReactiveProps(value: unknown): value is object;
25
+ /**
26
+ * Unwrap a prop value for reactive read.
27
+ * - Signals → call (tracked)
28
+ * - Zero-arg functions (accessors) → call (tracked)
29
+ * - Event handlers / multi-arg fns → return as-is
30
+ * - Everything else → as-is
31
+ */
32
+ export declare function unwrapProp(key: string, value: unknown): unknown;
33
+ /**
34
+ * Peek at the raw prop bag value without unwrapping signals/accessors.
35
+ * Useful for dev warnings (e.g. detect snapshot `value={sig()}` vs `value={sig}`).
36
+ */
37
+ export declare function getRawProp(props: object, key: string): unknown;
38
+ /**
39
+ * Wrap a raw props object so property access is reactive.
40
+ * Idempotent — wrapping an existing reactive props object returns it as-is.
41
+ */
42
+ export declare function createProps<P extends object>(raw: P): ReactiveProps<P>;
43
+ /**
44
+ * Merge default props with overrides. Later sources win.
45
+ * Accessors stay lazy — resolved only when the child reads a key.
46
+ */
47
+ export declare function mergeProps<T extends object, U extends object>(defaults: T, props: U): ReactiveProps<T & U>;
48
+ export declare function mergeProps<T extends object, U extends object, V extends object>(a: T, b: U, c: V): ReactiveProps<T & U & V>;
49
+ /**
50
+ * Split props into a picked reactive bag + the rest.
51
+ * Both bags read through the same underlying props (no snapshot).
52
+ */
53
+ export declare function splitProps<P extends object, K extends keyof P>(props: P, keys: readonly K[]): [ReactiveProps<Pick<P, K>>, ReactiveProps<Omit<P, K>>];
54
+ //# sourceMappingURL=props.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"props.d.ts","sourceRoot":"","sources":["../src/props.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAE3C;;;;;;;;;;;;;;;GAeG;AACH,MAAM,MAAM,aAAa,CAAC,CAAC,SAAS,MAAM,IAAI;IAC5C,QAAQ,EAAE,CAAC,IAAI,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;CAC9B,CAAC;AAIF,gDAAgD;AAChD,wBAAgB,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAAC,OAAO,CAAC,CAMjE;AAED,sFAAsF;AACtF,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAE/D;AAQD;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAQ/D;AAID;;;GAGG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAG9D;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,CAAC,SAAS,MAAM,EAAE,GAAG,EAAE,CAAC,GAAG,aAAa,CAAC,CAAC,CAAC,CAsCtE;AAID;;;GAGG;AACH,wBAAgB,UAAU,CAAC,CAAC,SAAS,MAAM,EAAE,CAAC,SAAS,MAAM,EAC3D,QAAQ,EAAE,CAAC,EACX,KAAK,EAAE,CAAC,GACP,aAAa,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;AACxB,wBAAgB,UAAU,CAAC,CAAC,SAAS,MAAM,EAAE,CAAC,SAAS,MAAM,EAAE,CAAC,SAAS,MAAM,EAC7E,CAAC,EAAE,CAAC,EACJ,CAAC,EAAE,CAAC,EACJ,CAAC,EAAE,CAAC,GACH,aAAa,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC;AA2D5B;;;GAGG;AACH,wBAAgB,UAAU,CAAC,CAAC,SAAS,MAAM,EAAE,CAAC,SAAS,MAAM,CAAC,EAC5D,KAAK,EAAE,CAAC,EACR,IAAI,EAAE,SAAS,CAAC,EAAE,GACjB,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,aAAa,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CA8DxD"}
package/dist/props.js ADDED
@@ -0,0 +1,211 @@
1
+ const reactivePropsSet = new WeakSet();
2
+ /** True if value looks like a Powers signal. */
3
+ export function isSignal(value) {
4
+ return (typeof value === "function" &&
5
+ typeof value.peek === "function" &&
6
+ typeof value.set === "function");
7
+ }
8
+ /** True if this object was created by `createProps` / `mergeProps` / `splitProps`. */
9
+ export function isReactiveProps(value) {
10
+ return typeof value === "object" && value !== null && reactivePropsSet.has(value);
11
+ }
12
+ function isEventKey(key) {
13
+ return (key.startsWith("on") && key.length > 2 && key[2] === key[2].toUpperCase());
14
+ }
15
+ /**
16
+ * Unwrap a prop value for reactive read.
17
+ * - Signals → call (tracked)
18
+ * - Zero-arg functions (accessors) → call (tracked)
19
+ * - Event handlers / multi-arg fns → return as-is
20
+ * - Everything else → as-is
21
+ */
22
+ export function unwrapProp(key, value) {
23
+ // children + bind must stay as passed (bind is a signal ref for two-way forms)
24
+ if (key === "children" || key === "bind")
25
+ return value;
26
+ if (typeof value !== "function")
27
+ return value;
28
+ if (isEventKey(key))
29
+ return value;
30
+ if (isSignal(value))
31
+ return value();
32
+ if (value.length === 0)
33
+ return value();
34
+ return value;
35
+ }
36
+ const rawSourceMap = new WeakMap();
37
+ /**
38
+ * Peek at the raw prop bag value without unwrapping signals/accessors.
39
+ * Useful for dev warnings (e.g. detect snapshot `value={sig()}` vs `value={sig}`).
40
+ */
41
+ export function getRawProp(props, key) {
42
+ const src = rawSourceMap.get(props) ?? props;
43
+ return Reflect.get(src, key);
44
+ }
45
+ /**
46
+ * Wrap a raw props object so property access is reactive.
47
+ * Idempotent — wrapping an existing reactive props object returns it as-is.
48
+ */
49
+ export function createProps(raw) {
50
+ if (isReactiveProps(raw)) {
51
+ return raw;
52
+ }
53
+ const source = (raw ?? {});
54
+ const proxy = new Proxy(source, {
55
+ get(target, key, receiver) {
56
+ if (typeof key === "symbol") {
57
+ return Reflect.get(target, key, receiver);
58
+ }
59
+ const value = Reflect.get(target, key, receiver);
60
+ return unwrapProp(key, value);
61
+ },
62
+ set() {
63
+ return false;
64
+ },
65
+ has(target, key) {
66
+ return Reflect.has(target, key);
67
+ },
68
+ ownKeys(target) {
69
+ return Reflect.ownKeys(target);
70
+ },
71
+ getOwnPropertyDescriptor(target, key) {
72
+ if (!Reflect.has(target, key))
73
+ return undefined;
74
+ return {
75
+ enumerable: true,
76
+ configurable: true,
77
+ get: () => unwrapProp(String(key), Reflect.get(target, key)),
78
+ };
79
+ },
80
+ });
81
+ reactivePropsSet.add(proxy);
82
+ rawSourceMap.set(proxy, source);
83
+ return proxy;
84
+ }
85
+ export function mergeProps(...sources) {
86
+ const list = sources.filter(Boolean);
87
+ const proxy = new Proxy({}, {
88
+ get(_target, key) {
89
+ if (typeof key === "symbol")
90
+ return undefined;
91
+ for (let i = list.length - 1; i >= 0; i--) {
92
+ const src = list[i];
93
+ if (key in src) {
94
+ // If source is already reactive props, read through it
95
+ // (already unwrapped). Prefer raw: if reactive, reading is fine.
96
+ const value = src[key];
97
+ // When reading from reactive source, value is already unwrapped.
98
+ // When reading from plain source, unwrap.
99
+ if (isReactiveProps(src))
100
+ return value;
101
+ return unwrapProp(key, value);
102
+ }
103
+ }
104
+ return undefined;
105
+ },
106
+ has(_target, key) {
107
+ if (typeof key === "symbol")
108
+ return false;
109
+ return list.some((src) => key in src);
110
+ },
111
+ ownKeys() {
112
+ const keys = new Set();
113
+ for (const src of list) {
114
+ for (const k of Reflect.ownKeys(src))
115
+ keys.add(k);
116
+ }
117
+ return [...keys];
118
+ },
119
+ getOwnPropertyDescriptor(_target, key) {
120
+ if (typeof key === "symbol")
121
+ return undefined;
122
+ if (!list.some((src) => key in src))
123
+ return undefined;
124
+ return {
125
+ enumerable: true,
126
+ configurable: true,
127
+ get: () => {
128
+ for (let i = list.length - 1; i >= 0; i--) {
129
+ const src = list[i];
130
+ if (key in src) {
131
+ if (isReactiveProps(src))
132
+ return src[key];
133
+ return unwrapProp(String(key), src[key]);
134
+ }
135
+ }
136
+ return undefined;
137
+ },
138
+ };
139
+ },
140
+ });
141
+ reactivePropsSet.add(proxy);
142
+ return proxy;
143
+ }
144
+ /**
145
+ * Split props into a picked reactive bag + the rest.
146
+ * Both bags read through the same underlying props (no snapshot).
147
+ */
148
+ export function splitProps(props, keys) {
149
+ const keySet = new Set(keys);
150
+ const picked = new Proxy({}, {
151
+ get(_t, key) {
152
+ if (typeof key === "symbol")
153
+ return undefined;
154
+ if (!keySet.has(key))
155
+ return undefined;
156
+ return props[key];
157
+ },
158
+ has(_t, key) {
159
+ return typeof key === "string" && keySet.has(key) && key in props;
160
+ },
161
+ ownKeys() {
162
+ return [...keySet].filter((k) => k in props);
163
+ },
164
+ getOwnPropertyDescriptor(_t, key) {
165
+ if (typeof key !== "string" || !keySet.has(key))
166
+ return undefined;
167
+ if (!(key in props))
168
+ return undefined;
169
+ return {
170
+ enumerable: true,
171
+ configurable: true,
172
+ get: () => props[key],
173
+ };
174
+ },
175
+ });
176
+ const rest = new Proxy({}, {
177
+ get(_t, key) {
178
+ if (typeof key === "symbol")
179
+ return undefined;
180
+ if (keySet.has(key))
181
+ return undefined;
182
+ return props[key];
183
+ },
184
+ has(_t, key) {
185
+ return (typeof key === "string" &&
186
+ !keySet.has(key) &&
187
+ key in props);
188
+ },
189
+ ownKeys() {
190
+ return Reflect.ownKeys(props).filter((k) => typeof k !== "string" || !keySet.has(k));
191
+ },
192
+ getOwnPropertyDescriptor(_t, key) {
193
+ if (typeof key !== "string" || keySet.has(key))
194
+ return undefined;
195
+ if (!(key in props))
196
+ return undefined;
197
+ return {
198
+ enumerable: true,
199
+ configurable: true,
200
+ get: () => props[key],
201
+ };
202
+ },
203
+ });
204
+ reactivePropsSet.add(picked);
205
+ reactivePropsSet.add(rest);
206
+ return [
207
+ picked,
208
+ rest,
209
+ ];
210
+ }
211
+ //# sourceMappingURL=props.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"props.js","sourceRoot":"","sources":["../src/props.ts"],"names":[],"mappings":"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"}
package/dist/show.d.ts ADDED
@@ -0,0 +1,12 @@
1
+ import { type Dispose } from "@lab206/core";
2
+ /**
3
+ * Conditionally mount a DOM subtree.
4
+ * When `when()` is false, the subtree is disposed and removed.
5
+ *
6
+ * @example
7
+ * ```ts
8
+ * show(parent, () => open(), () => h("div", null, "Hello"));
9
+ * ```
10
+ */
11
+ export declare function show(parent: ParentNode, when: () => boolean, factory: () => Node, anchor?: Node | null): Dispose;
12
+ //# sourceMappingURL=show.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"show.d.ts","sourceRoot":"","sources":["../src/show.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,OAAO,EACb,MAAM,cAAc,CAAC;AAGtB;;;;;;;;GAQG;AACH,wBAAgB,IAAI,CAClB,MAAM,EAAE,UAAU,EAClB,IAAI,EAAE,MAAM,OAAO,EACnB,OAAO,EAAE,MAAM,IAAI,EACnB,MAAM,GAAE,IAAI,GAAG,IAAW,GACzB,OAAO,CA8BT"}
package/dist/show.js ADDED
@@ -0,0 +1,42 @@
1
+ import { createRoot, effect, } from "@lab206/core";
2
+ import { remove } from "./insert.js";
3
+ /**
4
+ * Conditionally mount a DOM subtree.
5
+ * When `when()` is false, the subtree is disposed and removed.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * show(parent, () => open(), () => h("div", null, "Hello"));
10
+ * ```
11
+ */
12
+ export function show(parent, when, factory, anchor = null) {
13
+ let childDispose;
14
+ let node;
15
+ const stop = effect(() => {
16
+ if (when()) {
17
+ if (!node) {
18
+ createRoot((dispose) => {
19
+ childDispose = dispose;
20
+ node = factory();
21
+ parent.insertBefore(node, anchor);
22
+ });
23
+ }
24
+ }
25
+ else if (node) {
26
+ childDispose?.();
27
+ childDispose = undefined;
28
+ remove(node);
29
+ node = undefined;
30
+ }
31
+ });
32
+ return () => {
33
+ stop();
34
+ childDispose?.();
35
+ childDispose = undefined;
36
+ if (node) {
37
+ remove(node);
38
+ node = undefined;
39
+ }
40
+ };
41
+ }
42
+ //# sourceMappingURL=show.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"show.js","sourceRoot":"","sources":["../src/show.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,UAAU,EACV,MAAM,GAEP,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAErC;;;;;;;;GAQG;AACH,MAAM,UAAU,IAAI,CAClB,MAAkB,EAClB,IAAmB,EACnB,OAAmB,EACnB,SAAsB,IAAI;IAE1B,IAAI,YAAiC,CAAC;IACtC,IAAI,IAAsB,CAAC;IAE3B,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,EAAE;QACvB,IAAI,IAAI,EAAE,EAAE,CAAC;YACX,IAAI,CAAC,IAAI,EAAE,CAAC;gBACV,UAAU,CAAC,CAAC,OAAO,EAAE,EAAE;oBACrB,YAAY,GAAG,OAAO,CAAC;oBACvB,IAAI,GAAG,OAAO,EAAE,CAAC;oBACjB,MAAM,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;gBACpC,CAAC,CAAC,CAAC;YACL,CAAC;QACH,CAAC;aAAM,IAAI,IAAI,EAAE,CAAC;YAChB,YAAY,EAAE,EAAE,CAAC;YACjB,YAAY,GAAG,SAAS,CAAC;YACzB,MAAM,CAAC,IAAI,CAAC,CAAC;YACb,IAAI,GAAG,SAAS,CAAC;QACnB,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,OAAO,GAAG,EAAE;QACV,IAAI,EAAE,CAAC;QACP,YAAY,EAAE,EAAE,CAAC;QACjB,YAAY,GAAG,SAAS,CAAC;QACzB,IAAI,IAAI,EAAE,CAAC;YACT,MAAM,CAAC,IAAI,CAAC,CAAC;YACb,IAAI,GAAG,SAAS,CAAC;QACnB,CAAC;IACH,CAAC,CAAC;AACJ,CAAC"}
package/package.json ADDED
@@ -0,0 +1,71 @@
1
+ {
2
+ "name": "@lab206/dom",
3
+ "version": "0.1.0",
4
+ "description": "Thin DOM bindings + JSX + reactive props for Powers.",
5
+ "type": "module",
6
+ "license": "BUSL-1.1",
7
+ "sideEffects": false,
8
+ "exports": {
9
+ ".": {
10
+ "types": "./src/index.ts",
11
+ "development": "./src/index.ts",
12
+ "import": "./dist/index.js",
13
+ "default": "./dist/index.js"
14
+ },
15
+ "./jsx-runtime": {
16
+ "types": "./src/jsx-runtime.ts",
17
+ "development": "./src/jsx-runtime.ts",
18
+ "import": "./dist/jsx-runtime.js",
19
+ "default": "./dist/jsx-runtime.js"
20
+ },
21
+ "./jsx-dev-runtime": {
22
+ "types": "./src/jsx-runtime.ts",
23
+ "development": "./src/jsx-runtime.ts",
24
+ "import": "./dist/jsx-runtime.js",
25
+ "default": "./dist/jsx-runtime.js"
26
+ }
27
+ },
28
+ "main": "./dist/index.js",
29
+ "types": "./dist/index.d.ts",
30
+ "files": [
31
+ "dist",
32
+ "README.md",
33
+ "src/**/*.ts",
34
+ "src/**/*.tsx",
35
+ "src/**/*.css",
36
+ "!src/**/*.test.ts",
37
+ "!src/**/*.test.tsx"
38
+ ],
39
+ "dependencies": {
40
+ "@lab206/core": "0.1.0"
41
+ },
42
+ "devDependencies": {
43
+ "esbuild": "^0.25.1",
44
+ "happy-dom": "^17.4.4",
45
+ "tsx": "^4.19.3",
46
+ "typescript": "^5.8.2"
47
+ },
48
+ "author": "Scott Powers",
49
+ "private": false,
50
+ "publishConfig": {
51
+ "access": "public"
52
+ },
53
+ "homepage": "https://lab206.com",
54
+ "bugs": {
55
+ "url": "https://github.com/spowers2/powers/issues"
56
+ },
57
+ "engines": {
58
+ "node": ">=20"
59
+ },
60
+ "repository": {
61
+ "type": "git",
62
+ "url": "git+https://github.com/spowers2/powers.git",
63
+ "directory": "packages/dom"
64
+ },
65
+ "scripts": {
66
+ "build": "tsc -p tsconfig.json",
67
+ "typecheck": "tsc -p tsconfig.json --noEmit",
68
+ "test": "node --import tsx --test src/*.test.ts",
69
+ "size": "node --import tsx scripts/size.ts"
70
+ }
71
+ }
package/src/bind.ts ADDED
@@ -0,0 +1,195 @@
1
+ import { effect, type Dispose } from "@lab206/core";
2
+
3
+ /** Reactive text content of an Element or Text node. */
4
+ export function bindText(
5
+ node: Element | Text,
6
+ get: () => string | number | boolean | null | undefined,
7
+ ): Dispose {
8
+ return effect(() => {
9
+ const value = get();
10
+ const text = value == null ? "" : String(value);
11
+ if (node.nodeType === Node.TEXT_NODE) {
12
+ (node as Text).data = text;
13
+ } else {
14
+ (node as Element).textContent = text;
15
+ }
16
+ });
17
+ }
18
+
19
+ /**
20
+ * Reactive attribute. `false` / `null` / `undefined` removes the attribute.
21
+ * `true` sets a boolean attribute to `""`.
22
+ */
23
+ export function bindAttr(
24
+ el: Element,
25
+ name: string,
26
+ get: () => string | number | boolean | null | undefined,
27
+ ): Dispose {
28
+ return effect(() => {
29
+ const value = get();
30
+ if (value === false || value == null) {
31
+ el.removeAttribute(name);
32
+ } else if (value === true) {
33
+ el.setAttribute(name, "");
34
+ } else {
35
+ el.setAttribute(name, String(value));
36
+ }
37
+ });
38
+ }
39
+
40
+ /** Reactive DOM property (e.g. `value`, `checked`, `disabled`). */
41
+ export function bindProp<T>(
42
+ el: Element,
43
+ name: string,
44
+ get: () => T,
45
+ ): Dispose {
46
+ return effect(() => {
47
+ const next = get();
48
+ const rec = el as unknown as Record<string, unknown>;
49
+ // Skip no-ops — assigning input.value every keystroke resets the caret
50
+ if (rec[name] !== next) {
51
+ rec[name] = next;
52
+ }
53
+ });
54
+ }
55
+
56
+ /**
57
+ * Reactive className.
58
+ * - string → full className
59
+ * - record → toggles keys by truthiness (merged onto base if second arg)
60
+ */
61
+ export function bindClass(
62
+ el: Element,
63
+ get: () => string | Record<string, boolean | undefined | null> | null | undefined,
64
+ ): Dispose {
65
+ return effect(() => {
66
+ const value = get();
67
+ if (value == null || value === "") {
68
+ el.className = "";
69
+ return;
70
+ }
71
+ if (typeof value === "string") {
72
+ el.className = value;
73
+ return;
74
+ }
75
+ const parts: string[] = [];
76
+ for (const key of Object.keys(value)) {
77
+ if (value[key]) parts.push(key);
78
+ }
79
+ el.className = parts.join(" ");
80
+ });
81
+ }
82
+
83
+ /**
84
+ * Reactive inline styles.
85
+ * Pass a partial style object; keys are CSS properties (camelCase or kebab-case).
86
+ * `null` / `undefined` values clear that property.
87
+ */
88
+ export function bindStyle(
89
+ el: HTMLElement | SVGElement,
90
+ get: () => Record<string, string | number | null | undefined> | null | undefined,
91
+ ): Dispose {
92
+ let prevKeys: string[] = [];
93
+ return effect(() => {
94
+ const styles = get() ?? {};
95
+ const nextKeys = Object.keys(styles);
96
+
97
+ for (const key of prevKeys) {
98
+ if (!(key in styles)) {
99
+ setStyleProp(el, key, "");
100
+ }
101
+ }
102
+
103
+ for (const key of nextKeys) {
104
+ const value = styles[key];
105
+ if (value == null) {
106
+ setStyleProp(el, key, "");
107
+ } else if (typeof value === "number") {
108
+ setStyleProp(el, key, toCssValue(key.includes("-")
109
+ ? key.replace(/-([a-z])/g, (_, c: string) => c.toUpperCase())
110
+ : key, value));
111
+ } else {
112
+ setStyleProp(el, key, String(value));
113
+ }
114
+ }
115
+
116
+ prevKeys = nextKeys;
117
+ });
118
+ }
119
+
120
+ /** CSS props that accept unitless numbers (like React). Everything else gets `px`. */
121
+ const UNITLESS = new Set([
122
+ "animationIterationCount",
123
+ "aspectRatio",
124
+ "borderImageOutset",
125
+ "borderImageSlice",
126
+ "borderImageWidth",
127
+ "boxFlex",
128
+ "boxFlexGroup",
129
+ "boxOrdinalGroup",
130
+ "columnCount",
131
+ "columns",
132
+ "flex",
133
+ "flexGrow",
134
+ "flexPositive",
135
+ "flexShrink",
136
+ "flexNegative",
137
+ "flexOrder",
138
+ "gridArea",
139
+ "gridRow",
140
+ "gridRowEnd",
141
+ "gridRowSpan",
142
+ "gridRowStart",
143
+ "gridColumn",
144
+ "gridColumnEnd",
145
+ "gridColumnSpan",
146
+ "gridColumnStart",
147
+ "fontWeight",
148
+ "lineClamp",
149
+ "lineHeight",
150
+ "opacity",
151
+ "order",
152
+ "orphans",
153
+ "tabSize",
154
+ "widows",
155
+ "zIndex",
156
+ "zoom",
157
+ "fillOpacity",
158
+ "floodOpacity",
159
+ "stopOpacity",
160
+ "strokeDasharray",
161
+ "strokeDashoffset",
162
+ "strokeMiterlimit",
163
+ "strokeOpacity",
164
+ "strokeWidth",
165
+ ]);
166
+
167
+ function toCssValue(key: string, value: string | number): string {
168
+ if (typeof value === "number") {
169
+ if (value === 0 || UNITLESS.has(key)) return String(value);
170
+ return `${value}px`;
171
+ }
172
+ return value;
173
+ }
174
+
175
+ function setStyleProp(
176
+ el: HTMLElement | SVGElement,
177
+ key: string,
178
+ value: string,
179
+ ): void {
180
+ // Support both camelCase and kebab-case.
181
+ if (key.startsWith("--")) {
182
+ el.style.setProperty(key, value);
183
+ return;
184
+ }
185
+ const camel = key.includes("-")
186
+ ? key.replace(/-([a-z])/g, (_, c: string) => c.toUpperCase())
187
+ : key;
188
+ // value may already be stringified; re-apply unit rule when it's a bare number
189
+ const asNum = Number(value);
190
+ const final =
191
+ value !== "" && Number.isFinite(asNum) && String(asNum) === value.trim()
192
+ ? toCssValue(camel, asNum)
193
+ : value;
194
+ (el.style as unknown as Record<string, string>)[camel] = final;
195
+ }