lume-js 0.4.0 → 0.4.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.
package/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  Minimal reactive state management using only standard JavaScript and HTML - no custom syntax, no build step required, no framework lock-in.
6
6
 
7
7
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
8
- [![Version](https://img.shields.io/badge/version-0.4.0-green.svg)](package.json)
8
+ [![Version](https://img.shields.io/badge/version-0.4.1-green.svg)](package.json)
9
9
 
10
10
  ## Why Lume.js?
11
11
 
@@ -29,6 +29,8 @@ Minimal reactive state management using only standard JavaScript and HTML - no c
29
29
 
30
30
  **Lume.js is essentially "Modern Knockout.js" - standards-only reactivity for 2025.**
31
31
 
32
+ 📖 **New to the project?** Read [DESIGN_DECISIONS.md](DESIGN_DECISIONS.md) to understand our design philosophy and why certain choices were made.
33
+
32
34
  ---
33
35
 
34
36
  ## Installation
@@ -164,17 +166,29 @@ cleanup(); // Stop the effect
164
166
 
165
167
  **How it works:** Effects use `globalThis.__LUME_CURRENT_EFFECT__` to track which state properties are accessed during execution. When any tracked property changes, the effect is queued in that state's pending effects set and runs once in the next microtask.
166
168
 
167
- ### `bindDom(root, store)`
169
+ ### `bindDom(root, store, options?)`
168
170
 
169
171
  Binds reactive state to DOM elements with `data-bind` attributes.
170
172
 
173
+ **Automatically waits for DOMContentLoaded** if the document is still loading, making it safe to call from anywhere (even in `<head>`).
174
+
171
175
  ```javascript
176
+ // Default: Auto-waits for DOM (safe anywhere)
172
177
  const cleanup = bindDom(document.body, store);
173
178
 
179
+ // Advanced: Force immediate binding (no auto-wait)
180
+ const cleanup = bindDom(myElement, store, { immediate: true });
181
+
174
182
  // Later: cleanup all bindings
175
183
  cleanup();
176
184
  ```
177
185
 
186
+ **Parameters:**
187
+ - `root` (HTMLElement) - Root element to scan for `[data-bind]` attributes
188
+ - `store` (Object) - Reactive state object
189
+ - `options` (Object, optional)
190
+ - `immediate` (Boolean) - Skip auto-wait, bind immediately. Default: `false`
191
+
178
192
  **Supports:**
179
193
  - ✅ Text content: `<span data-bind="count"></span>`
180
194
  - ✅ Input values: `<input data-bind="name">`
@@ -185,12 +199,108 @@ cleanup();
185
199
  - ✅ Radio buttons: `<input type="radio" data-bind="choice">`
186
200
  - ✅ Nested paths: `<span data-bind="user.name"></span>`
187
201
 
202
+ **Multiple Checkboxes Pattern:**
203
+
204
+ For multiple checkboxes, use nested state instead of arrays:
205
+
206
+ ```javascript
207
+ // ✅ Recommended: Nested state objects
208
+ const store = state({
209
+ tags: state({
210
+ javascript: true,
211
+ python: false,
212
+ go: true
213
+ })
214
+ });
215
+ ```
216
+
217
+ ```html
218
+ <input type="checkbox" data-bind="tags.javascript"> JavaScript
219
+ <input type="checkbox" data-bind="tags.python"> Python
220
+ <input type="checkbox" data-bind="tags.go"> Go
221
+ ```
222
+
223
+ Nested state is **more explicit and easier to validate** than array-based bindings. See [DESIGN_DECISIONS.md](DESIGN_DECISIONS.md#why-nested-state-for-multiple-checkboxes-instead-of-arrays) for the full rationale.
224
+
188
225
  **Features:**
226
+ - ✅ Auto-waits for DOM if needed (no timing issues!)
189
227
  - ✅ Returns cleanup function
190
228
  - ✅ Better error messages with `[Lume.js]` prefix
191
229
  - ✅ Handles edge cases (empty bindings, invalid paths)
192
230
  - ✅ Two-way binding for form inputs
193
231
 
232
+ ### `isReactive(obj)`
233
+
234
+ Checks whether a value is a reactive proxy created by `state()`.
235
+
236
+ ```javascript
237
+ import { state, isReactive } from 'lume-js';
238
+
239
+ const original = { count: 1 };
240
+ const store = state(original);
241
+
242
+ isReactive(store); // true
243
+ isReactive(original); // false
244
+ isReactive(null); // false
245
+ ```
246
+
247
+ **How it works:**
248
+ Lume.js uses an internal `Symbol` checked via the Proxy `get` trap rather than mutating the proxy or storing external WeakSet state. Accessing `obj[REACTIVE_SYMBOL]` returns `true` only for reactive proxies, and the symbol is not enumerable or visible via `Object.getOwnPropertySymbols`.
249
+
250
+ **Characteristics:**
251
+ - ✅ Zero mutation of the proxy
252
+ - ✅ Invisible to enumeration and reflection
253
+ - ✅ Fast: single symbol identity check in the `get` path
254
+ - ✅ Supports nested reactive states naturally
255
+ - ✅ Skips tracking meta `$`-prefixed methods (e.g. `$subscribe`)
256
+
257
+ **When to use:** Utility/debugging, conditional wrapping patterns like:
258
+ ```javascript
259
+ function ensureReactive(val) {
260
+ return isReactive(val) ? val : state(val);
261
+ }
262
+ ```
263
+
264
+ **Why Auto-Ready?**
265
+
266
+ Works seamlessly regardless of script placement:
267
+
268
+ ```html
269
+ <!-- ✅ Works in <head> -->
270
+ <script type="module">
271
+ import { state, bindDom } from 'lume-js';
272
+ const store = state({ count: 0 });
273
+ bindDom(document.body, store); // Auto-waits for DOM!
274
+ </script>
275
+
276
+ <!-- ✅ Works inline in <body> -->
277
+ <body>
278
+ <span data-bind="count"></span>
279
+ <script type="module">
280
+ // bindDom() waits for DOMContentLoaded automatically
281
+ </script>
282
+ </body>
283
+
284
+ <!-- ✅ Works with defer -->
285
+ <script type="module" defer>
286
+ // Already loaded, executes immediately
287
+ </script>
288
+ ```
289
+
290
+ **When to use `immediate: true`:**
291
+
292
+ Rare scenarios where you're dynamically creating DOM or need precise control:
293
+
294
+ ```javascript
295
+ // Dynamic DOM injection
296
+ const container = document.createElement('div');
297
+ container.innerHTML = '<span data-bind="count"></span>';
298
+ document.body.appendChild(container);
299
+
300
+ // Bind immediately (DOM already exists)
301
+ bindDom(container, store, { immediate: true });
302
+ ```
303
+
194
304
  ### `$subscribe(key, callback)`
195
305
 
196
306
  Manually subscribe to state changes. Calls callback immediately with current value, then on every change.
@@ -277,6 +387,70 @@ unwatch();
277
387
 
278
388
  ---
279
389
 
390
+ ## Choosing the Right Reactive Pattern
391
+
392
+ Lume.js provides three ways to react to state changes. Here's when to use each:
393
+
394
+ | Pattern | Use When | Pros | Cons |
395
+ |---------|----------|------|------|
396
+ | **`bindDom()`** | Syncing state ↔ DOM | Zero code, declarative HTML | DOM-only, no custom logic |
397
+ | **`$subscribe()`** | Listening to specific keys | Explicit, immediate, simple | Manual dependency tracking |
398
+ | **`effect()`** | Auto-run code on any state access | Automatic dependencies, concise | Microtask delay, can infinite loop |
399
+ | **`computed()`** | Deriving values from state | Cached, automatic recompute | Addon import, slight overhead |
400
+
401
+ **Quick Decision Tree:**
402
+
403
+ ```
404
+ Need to update DOM?
405
+ ├─ Yes, just sync form/text → Use bindDom()
406
+ └─ No, custom logic needed
407
+ ├─ Watch single key? → Use $subscribe()
408
+ ├─ Watch multiple keys dynamically? → Use effect()
409
+ └─ Derive a value? → Use computed()
410
+ ```
411
+
412
+ **Examples:**
413
+
414
+ ```javascript
415
+ // 1. bindDom - Zero code DOM sync
416
+ <span data-bind="count"></span>
417
+ bindDom(document.body, store);
418
+
419
+ // 2. $subscribe - Specific key, immediate execution
420
+ store.$subscribe('count', (val) => {
421
+ if (val > 10) showNotification('High!');
422
+ });
423
+
424
+ // 3. effect - Multiple keys, automatic tracking
425
+ effect(() => {
426
+ document.title = `${store.user.name}: ${store.count}`;
427
+ // Tracks both user.name and count automatically
428
+ });
429
+
430
+ // 4. computed - Derive cached value
431
+ import { computed } from 'lume-js/addons';
432
+ const total = computed(() => store.items.reduce((sum, i) => sum + i.price, 0));
433
+ console.log(total.value);
434
+ ```
435
+
436
+ **Gotchas:**
437
+
438
+ - ⚠️ **effect()** runs in next microtask (~0.002ms delay). Use `$subscribe()` for immediate execution.
439
+ - ⚠️ **Don't mutate tracked state inside effect** - causes infinite loops:
440
+ ```javascript
441
+ // ❌ BAD - Infinite loop
442
+ effect(() => {
443
+ store.count++; // Writes to what it reads!
444
+ });
445
+
446
+ // ✅ GOOD - Read-only or separate keys
447
+ effect(() => {
448
+ store.displayCount = store.count * 2; // Different keys
449
+ });
450
+ ```
451
+
452
+ ---
453
+
280
454
  ## Examples
281
455
 
282
456
  ### Basic Counter
@@ -619,7 +793,7 @@ resolve: {
619
793
  **Current coverage:**
620
794
  - 100% statements, functions, and lines
621
795
  - 100% branches (including edge-case paths)
622
- - 37 tests covering core behavior, addons, inputs (text/checkbox/radio/number/range/select/textarea), nested state, and cleanup semantics
796
+ - 67 tests covering core behavior, addons, inputs (text/checkbox/radio/number/range/select/textarea), nested state, reactive identity, and cleanup semantics
623
797
 
624
798
  ---
625
799
 
@@ -629,7 +803,10 @@ We welcome contributions! Please:
629
803
 
630
804
  1. **Focus on:** Examples, documentation, bug fixes, performance
631
805
  2. **Avoid:** Adding core features without discussion (keep it minimal!)
632
- 3. **Check:** Project specification for philosophy
806
+ 3. **Read:** [DESIGN_DECISIONS.md](DESIGN_DECISIONS.md) to understand our philosophy and why certain choices were made
807
+ 4. **Propose alternatives:** If you think a design decision should be reconsidered, open an issue with your reasoning
808
+
809
+ Before suggesting new features, check if they align with Lume's core principles: standards-only, minimal API, no build step required.
633
810
 
634
811
  ---
635
812
 
package/package.json CHANGED
@@ -1,10 +1,21 @@
1
1
  {
2
2
  "name": "lume-js",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "Minimal reactive state management using only standard JavaScript and HTML - no custom syntax, no build step required",
5
5
  "main": "src/index.js",
6
6
  "types": "src/index.d.ts",
7
7
  "type": "module",
8
+ "sideEffects": false,
9
+ "exports": {
10
+ ".": {
11
+ "import": "./src/index.js",
12
+ "types": "./src/index.d.ts"
13
+ },
14
+ "./addons": {
15
+ "import": "./src/addons/index.js",
16
+ "types": "./src/addons/index.d.ts"
17
+ }
18
+ },
8
19
  "scripts": {
9
20
  "dev": "vite",
10
21
  "build": "echo 'No build step needed - zero-runtime library!'",
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Lume.js Addons TypeScript Definitions
3
+ *
4
+ * Optional utilities for advanced reactive patterns.
5
+ * Import from "lume-js/addons" for tree-shaking.
6
+ */
7
+
8
+ import type { Unsubscribe, Subscriber, ReactiveState } from '../index.js';
9
+
10
+ /**
11
+ * Computed value container returned by computed().
12
+ * T is the computed result type; when an error occurs the value becomes undefined.
13
+ */
14
+ export interface Computed<T> {
15
+ /** Current computed value (undefined if computation threw). */
16
+ readonly value: T | undefined;
17
+ /** Subscribe to changes; immediate invocation with current value (may be undefined). */
18
+ subscribe(callback: Subscriber<T | undefined>): Unsubscribe;
19
+ /** Dispose computed value and stop tracking dependencies. */
20
+ dispose(): void;
21
+ }
22
+
23
+ /**
24
+ * Create a computed value that automatically re-evaluates when accessed reactive state keys change.
25
+ * Uses effect() internally for dependency tracking.
26
+ *
27
+ * @param fn - Pure function that derives a value from reactive state.
28
+ * @returns Computed value container with .value, .subscribe(), and .dispose()
29
+ * @throws {Error} If fn is not a function
30
+ *
31
+ * @example
32
+ * ```typescript
33
+ * import { state } from 'lume-js';
34
+ * import { computed } from 'lume-js/addons';
35
+ *
36
+ * const store = state({ count: 5 });
37
+ * const doubled = computed(() => store.count * 2);
38
+ *
39
+ * console.log(doubled.value); // 10
40
+ *
41
+ * store.count = 10;
42
+ * // After microtask:
43
+ * console.log(doubled.value); // 20 (auto-updated)
44
+ *
45
+ * // Subscribe to changes
46
+ * const unsub = doubled.subscribe(value => {
47
+ * console.log('Doubled:', value);
48
+ * });
49
+ *
50
+ * // Cleanup
51
+ * doubled.dispose();
52
+ * unsub();
53
+ * ```
54
+ */
55
+ export function computed<T>(fn: () => T): Computed<T>;
56
+
57
+ /**
58
+ * Watch a single key on a reactive state object; convenience wrapper around $subscribe.
59
+ *
60
+ * @param store - Reactive state object created with state().
61
+ * @param key - Property key to observe.
62
+ * @param callback - Invoked immediately and on subsequent changes.
63
+ * @returns Unsubscribe function for cleanup
64
+ * @throws {Error} If store is not a reactive state object
65
+ *
66
+ * @example
67
+ * ```typescript
68
+ * import { state } from 'lume-js';
69
+ * import { watch } from 'lume-js/addons';
70
+ *
71
+ * const store = state({ count: 0 });
72
+ *
73
+ * const unwatch = watch(store, 'count', (value) => {
74
+ * console.log('Count is now:', value);
75
+ * });
76
+ *
77
+ * // Cleanup
78
+ * unwatch();
79
+ * ```
80
+ */
81
+ export function watch<T extends object, K extends keyof T>(
82
+ store: ReactiveState<T>,
83
+ key: K,
84
+ callback: Subscriber<T[K]>
85
+ ): Unsubscribe;
@@ -4,10 +4,19 @@
4
4
  *
5
5
  * Binds reactive state to DOM elements using [data-bind].
6
6
  * Supports two-way binding for INPUT/TEXTAREA/SELECT.
7
+ *
8
+ * Automatically waits for DOMContentLoaded if the document is still loading,
9
+ * ensuring safe binding regardless of when the function is called.
7
10
  *
8
11
  * Usage:
9
12
  * import { bindDom } from "lume-js";
13
+ *
14
+ * // Default: Auto-waits for DOM (safe anywhere)
10
15
  * const cleanup = bindDom(document.body, store);
16
+ *
17
+ * // Advanced: Force immediate binding (no auto-wait)
18
+ * const cleanup = bindDom(myElement, store, { immediate: true });
19
+ *
11
20
  * // Later: cleanup();
12
21
  *
13
22
  * HTML:
@@ -23,9 +32,11 @@ import { resolvePath } from "./utils.js";
23
32
  *
24
33
  * @param {HTMLElement} root - Root element to scan for [data-bind]
25
34
  * @param {object} store - Reactive state object
35
+ * @param {object} [options] - Optional configuration
36
+ * @param {boolean} [options.immediate=false] - Skip auto-wait, bind immediately
26
37
  * @returns {function} Cleanup function to remove all bindings
27
38
  */
28
- export function bindDom(root, store) {
39
+ export function bindDom(root, store, options = {}) {
29
40
  if (!(root instanceof HTMLElement)) {
30
41
  throw new Error('bindDom() requires a valid HTMLElement as root');
31
42
  }
@@ -34,52 +45,79 @@ export function bindDom(root, store) {
34
45
  throw new Error('bindDom() requires a reactive state object');
35
46
  }
36
47
 
37
- const nodes = root.querySelectorAll("[data-bind]");
38
- const cleanups = [];
48
+ const { immediate = false } = options;
39
49
 
40
- nodes.forEach(el => {
41
- const bindPath = el.getAttribute("data-bind");
42
-
43
- if (!bindPath) {
44
- console.warn('[Lume.js] Empty data-bind attribute found', el);
45
- return;
46
- }
50
+ // Core binding logic extracted to separate function
51
+ const performBinding = () => {
52
+ const nodes = root.querySelectorAll("[data-bind]");
53
+ const cleanups = [];
47
54
 
48
- const pathArr = bindPath.split(".");
49
- const lastKey = pathArr.pop();
55
+ nodes.forEach(el => {
56
+ const bindPath = el.getAttribute("data-bind");
57
+
58
+ if (!bindPath) {
59
+ console.warn('[Lume.js] Empty data-bind attribute found', el);
60
+ return;
61
+ }
50
62
 
51
- let target;
52
- try {
53
- target = resolvePath(store, pathArr);
54
- } catch (err) {
55
- console.warn(`[Lume.js] Invalid binding path "${bindPath}":`, err.message);
56
- return;
57
- }
63
+ const pathArr = bindPath.split(".");
64
+ const lastKey = pathArr.pop();
58
65
 
59
- if (!target || typeof target.$subscribe !== 'function') {
60
- console.warn(`[Lume.js] Target for "${bindPath}" is not a reactive state object`);
61
- return;
62
- }
66
+ let target;
67
+ try {
68
+ target = resolvePath(store, pathArr);
69
+ } catch (err) {
70
+ console.warn(`[Lume.js] Invalid binding path "${bindPath}":`, err.message);
71
+ return;
72
+ }
73
+
74
+ if (!target || typeof target.$subscribe !== 'function') {
75
+ console.warn(`[Lume.js] Target for "${bindPath}" is not a reactive state object`);
76
+ return;
77
+ }
63
78
 
64
- // Subscribe to changes - receives already-batched notifications
65
- const unsubscribe = target.$subscribe(lastKey, val => {
66
- updateElement(el, val);
79
+ // Subscribe to changes - receives already-batched notifications
80
+ const unsubscribe = target.$subscribe(lastKey, val => {
81
+ updateElement(el, val);
82
+ });
83
+ cleanups.push(unsubscribe);
84
+
85
+ // Two-way binding for form inputs
86
+ if (isFormInput(el)) {
87
+ const handler = e => {
88
+ target[lastKey] = getInputValue(e.target);
89
+ };
90
+ el.addEventListener("input", handler);
91
+ cleanups.push(() => el.removeEventListener("input", handler));
92
+ }
67
93
  });
68
- cleanups.push(unsubscribe);
69
-
70
- // Two-way binding for form inputs
71
- if (isFormInput(el)) {
72
- const handler = e => {
73
- target[lastKey] = getInputValue(e.target);
74
- };
75
- el.addEventListener("input", handler);
76
- cleanups.push(() => el.removeEventListener("input", handler));
77
- }
78
- });
79
94
 
80
- return () => {
81
- cleanups.forEach(cleanup => cleanup());
95
+ return () => {
96
+ cleanups.forEach(cleanup => cleanup());
97
+ };
82
98
  };
99
+
100
+ // Auto-wait for DOM if needed (unless immediate flag is set)
101
+ if (!immediate && document.readyState === 'loading') {
102
+ let cleanup = null;
103
+ const onReady = () => {
104
+ cleanup = performBinding();
105
+ };
106
+ document.addEventListener('DOMContentLoaded', onReady, { once: true });
107
+
108
+ // Return cleanup function that handles both cases
109
+ return () => {
110
+ if (cleanup) {
111
+ cleanup();
112
+ } else {
113
+ // If cleanup hasn't been created yet, remove the event listener
114
+ document.removeEventListener('DOMContentLoaded', onReady);
115
+ }
116
+ };
117
+ }
118
+
119
+ // Immediate binding (DOM already ready or immediate flag set)
120
+ return performBinding();
83
121
  }
84
122
 
85
123
  /**
@@ -89,8 +127,13 @@ export function bindDom(root, store) {
89
127
  function updateElement(el, val) {
90
128
  if (el.tagName === "INPUT") {
91
129
  if (el.type === "checkbox") {
130
+ // Single checkbox: bind to boolean value
131
+ // For multiple checkboxes, use nested state objects:
132
+ // <input data-bind="tags.javascript"> → state({ tags: state({ javascript: true }) })
92
133
  el.checked = Boolean(val);
93
134
  } else if (el.type === "radio") {
135
+ // Radio: checked when el.value matches state value
136
+ // String() handles null/undefined gracefully (no radio selected)
94
137
  el.checked = el.value === String(val);
95
138
  } else {
96
139
  el.value = val ?? '';
package/src/core/state.js CHANGED
@@ -29,6 +29,9 @@
29
29
  * @param {Object} obj - Initial state object
30
30
  * @returns {Proxy} Reactive proxy with $subscribe method
31
31
  */
32
+ // Internal symbol used to mark reactive proxies (non-enumerable via Proxy trap)
33
+ const REACTIVE_MARKER = Symbol('__LUME_REACTIVE__');
34
+
32
35
  export function state(obj) {
33
36
  // Validate input
34
37
  if (!obj || typeof obj !== 'object' || Array.isArray(obj)) {
@@ -59,9 +62,11 @@ export function state(obj) {
59
62
  flushScheduled = false;
60
63
 
61
64
  // Notify all subscribers of changed keys
65
+ // Snapshot listeners array to handle unsubscribes during iteration
62
66
  for (const [key, value] of pendingNotifications) {
63
67
  if (listeners[key]) {
64
- listeners[key].forEach(fn => fn(value));
68
+ const subscribersSnapshot = Array.from(listeners[key]);
69
+ subscribersSnapshot.forEach(fn => fn(value));
65
70
  }
66
71
  }
67
72
 
@@ -76,6 +81,12 @@ export function state(obj) {
76
81
 
77
82
  const proxy = new Proxy(obj, {
78
83
  get(target, key) {
84
+ // Reactive marker check (avoid tracking for internal symbol)
85
+ if (key === REACTIVE_MARKER) return true;
86
+ // Skip effect tracking for internal meta methods (e.g. $subscribe)
87
+ if (typeof key === 'string' && key.startsWith('$')) {
88
+ return target[key];
89
+ }
79
90
  // Support effect tracking
80
91
  // Check if we're inside an effect context
81
92
  if (typeof globalThis.__LUME_CURRENT_EFFECT__ !== 'undefined') {
@@ -156,4 +167,14 @@ export function state(obj) {
156
167
  };
157
168
 
158
169
  return proxy;
170
+ }
171
+
172
+ /**
173
+ * Determine if an object is a Lume reactive proxy.
174
+ * Defensive: ensures object and marker presence.
175
+ * @param {any} obj
176
+ * @returns {boolean}
177
+ */
178
+ export function isReactive(obj) {
179
+ return !!(obj && typeof obj === 'object' && obj[REACTIVE_MARKER]);
159
180
  }
package/src/index.d.ts CHANGED
@@ -58,20 +58,39 @@ export type ReactiveState<T extends object> = T & {
58
58
  */
59
59
  export function state<T extends object>(obj: T): ReactiveState<T>;
60
60
 
61
+ /**
62
+ * Options for bindDom function
63
+ */
64
+ export interface BindDomOptions {
65
+ /**
66
+ * Skip auto-wait for DOM, bind immediately
67
+ * @default false
68
+ */
69
+ immediate?: boolean;
70
+ }
71
+
61
72
  /**
62
73
  * Bind reactive state to DOM elements
63
74
  *
75
+ * Automatically waits for DOMContentLoaded if the document is still loading,
76
+ * ensuring safe binding regardless of when the function is called.
77
+ *
64
78
  * @param root - Root element to scan for [data-bind] attributes
65
79
  * @param store - Reactive state object
80
+ * @param options - Optional configuration
66
81
  * @returns Cleanup function to remove all bindings
67
82
  * @throws {Error} If root is not an HTMLElement
68
83
  * @throws {Error} If store is not a reactive state object
69
84
  *
70
85
  * @example
71
86
  * ```typescript
87
+ * // Default: Auto-waits for DOM (safe anywhere)
72
88
  * const store = state({ count: 0 });
73
89
  * const cleanup = bindDom(document.body, store);
74
90
  *
91
+ * // Advanced: Force immediate binding (no auto-wait)
92
+ * const cleanup = bindDom(myElement, store, { immediate: true });
93
+ *
75
94
  * // Later: cleanup all bindings
76
95
  * cleanup();
77
96
  * ```
@@ -84,7 +103,8 @@ export function state<T extends object>(obj: T): ReactiveState<T>;
84
103
  */
85
104
  export function bindDom(
86
105
  root: HTMLElement,
87
- store: ReactiveState<any>
106
+ store: ReactiveState<any>,
107
+ options?: BindDomOptions
88
108
  ): Unsubscribe;
89
109
 
90
110
  /**
@@ -112,4 +132,11 @@ export function bindDom(
112
132
  * cleanup(); // Stop the effect
113
133
  * ```
114
134
  */
115
- export function effect(fn: () => void): Unsubscribe;
135
+ export function effect(fn: () => void): Unsubscribe;
136
+
137
+ /**
138
+ * Check if a value is a Lume reactive proxy produced by state().
139
+ * Returns true only for objects created by state().
140
+ * @param obj - Value to check
141
+ */
142
+ export function isReactive(obj: any): boolean;
package/src/index.js CHANGED
@@ -10,6 +10,6 @@
10
10
  * import { state, bindDom, effect } from "lume-js";
11
11
  */
12
12
 
13
- export { state } from "./core/state.js";
13
+ export { state, isReactive } from "./core/state.js";
14
14
  export { bindDom } from "./core/bindDom.js";
15
15
  export { effect } from "./core/effect.js";