lume-js 2.2.1 → 2.3.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.
Files changed (43) hide show
  1. package/AGENT_GUIDE.md +224 -0
  2. package/README.md +97 -16
  3. package/dist/addons.min.mjs +1 -1
  4. package/dist/addons.mjs +190 -7
  5. package/dist/addons.mjs.map +1 -1
  6. package/dist/handlers.min.mjs +1 -1
  7. package/dist/handlers.mjs +29 -2
  8. package/dist/handlers.mjs.map +1 -1
  9. package/dist/index.min.mjs +1 -1
  10. package/dist/index.mjs +4 -141
  11. package/dist/index.mjs.map +1 -1
  12. package/dist/lume.global.js +1 -1
  13. package/dist/lume.global.js.map +1 -1
  14. package/dist/shared-BGg9PbiG.mjs +249 -0
  15. package/dist/shared-BGg9PbiG.mjs.map +1 -0
  16. package/dist/shared-DmpHYKx7.mjs +15 -0
  17. package/dist/shared-DmpHYKx7.mjs.map +1 -0
  18. package/dist/shared-SUXdsYBx.mjs +233 -0
  19. package/dist/shared-SUXdsYBx.mjs.map +1 -0
  20. package/dist/state.min.mjs +1 -0
  21. package/dist/state.mjs +7 -0
  22. package/dist/state.mjs.map +1 -0
  23. package/llms-full.txt +6999 -0
  24. package/llms.txt +89 -0
  25. package/package.json +11 -2
  26. package/src/addons/index.d.ts +99 -7
  27. package/src/addons/index.js +13 -2
  28. package/src/addons/persist.js +190 -0
  29. package/src/addons/repeat.js +159 -10
  30. package/src/core/batch.js +139 -0
  31. package/src/core/bindDom.js +7 -3
  32. package/src/core/effect.js +34 -5
  33. package/src/core/state.js +118 -73
  34. package/src/handlers/index.d.ts +24 -0
  35. package/src/handlers/index.js +1 -0
  36. package/src/handlers/on.js +60 -0
  37. package/src/handlers/stringAttr.js +9 -2
  38. package/src/index.d.ts +14 -200
  39. package/src/index.js +3 -1
  40. package/src/state.d.ts +252 -0
  41. package/src/state.js +25 -0
  42. package/dist/shared-x2HJmEyO.mjs +0 -260
  43. package/dist/shared-x2HJmEyO.mjs.map +0 -1
package/AGENT_GUIDE.md ADDED
@@ -0,0 +1,224 @@
1
+ # Lume.js — Agent Guide
2
+
3
+ Instructions for AI coding agents (and fast-moving humans) **using Lume.js in an application**. Read this before writing any Lume code. For developing Lume.js itself, see `AGENTS.md` instead.
4
+
5
+ - Machine-readable doc bundle: `llms.txt` (index) and `llms-full.txt` (this guide + every guide, tutorial, and API page in one file) ship with the npm package and live at the repo root.
6
+ - Full human docs: <https://github.com/sathvikc/lume-js/tree/main/docs>
7
+
8
+ ## Mental model (30 seconds)
9
+
10
+ Lume is **standards-only reactivity**: a `Proxy`-based store (`state`), auto-tracking side effects (`effect`), and declarative DOM binding through plain `data-*` attributes (`bindDom`). No components, no virtual DOM, no compiler, no build step. HTML stays valid HTML; JavaScript stays standard JavaScript. You own the DOM structure; Lume keeps it in sync with state.
11
+
12
+ ```javascript
13
+ import { state, effect, bindDom, batch } from 'lume-js';
14
+
15
+ const store = state({ name: 'World', count: 0 });
16
+ bindDom(document.body, store); // wires all data-* attrs under body
17
+ effect(() => { // auto-tracks store.count
18
+ document.title = `Count: ${store.count}`;
19
+ });
20
+ store.count++; // DOM + title update on next microtask
21
+ ```
22
+
23
+ ```html
24
+ <h1>Hello, <span data-bind="name"></span>!</h1>
25
+ <input data-bind="name"> <!-- two-way on inputs, one-way on text elements -->
26
+ ```
27
+
28
+ ## Package entries — import from the right place
29
+
30
+ | Import | Size (gz) | Gives you | Use in |
31
+ |---|---|---|---|
32
+ | `lume-js` | 2.66 KB | `state`, `effect`, `bindDom`, `batch`, `withReadObserver` | browsers |
33
+ | `lume-js/state` | 1.46 KB | `state`, `batch`, `withReadObserver` (no DOM code) | Node, workers, CLI, SSR |
34
+ | `lume-js/addons` | pay per import | `computed`, `watch`, `repeat`, `persist`, `hydrateState`, `createCleanupGroup`, `debug` | optional patterns |
35
+ | `lume-js/handlers` | pay per import | `show`, `classToggle`, `boolAttr`, `ariaAttr`, `stringAttr`, `className`, `on`, `htmlAttrs`, presets | extra `data-*` attributes |
36
+
37
+ CDN (no build): `https://cdn.jsdelivr.net/npm/lume-js/dist/index.min.mjs` (also `/dist/state.min.mjs`, `/dist/addons.min.mjs`, `/dist/handlers.min.mjs`).
38
+
39
+ ## The rules — violating any of these produces silently broken code
40
+
41
+ 1. **Never mutate arrays or nested objects in place. Replace them.** Lume detects changes with reference equality (`Object.is`) on top-level keys only.
42
+ ```javascript
43
+ store.items.push(x); // ❌ silent — no update ever fires
44
+ store.items = [...store.items, x]; // ✅
45
+ store.items = store.items.filter(t => t.id !== id); // ✅ remove
46
+ store.items = store.items.map(t => t.id === id ? { ...t, done: true } : t); // ✅ update
47
+ store.items = [...store.items].sort(cmp); // ✅ sort (copy first!)
48
+ ```
49
+
50
+ 2. **Nested objects are not reactive unless explicitly wrapped.**
51
+ ```javascript
52
+ const s = state({ user: { name: 'Ada' } });
53
+ s.user.name = 'Z'; // ❌ silent
54
+ const s2 = state({ user: state({ name: 'Ada' }) });
55
+ s2.user.name = 'Z'; // ✅ notifies
56
+ // or replace the whole object:
57
+ s.user = { ...s.user, name: 'Z' }; // ✅ notifies subscribers of 'user'
58
+ ```
59
+
60
+ 3. **Updates are asynchronous (microtask-batched per store).** After a write, the DOM updates on the next microtask, not synchronously. In tests, flush before asserting:
61
+ ```javascript
62
+ store.count = 5;
63
+ await Promise.resolve(); // or await new Promise(r => setTimeout(r))
64
+ expect(el.textContent).toBe('5');
65
+ ```
66
+ Multiple writes to the same store in one tick coalesce into one flush, and subscribers see only the final value of each key.
67
+
68
+ 4. **Effects track only what they read synchronously.** Reads after an `await`, inside `setTimeout`, or inside event callbacks are NOT tracked. Read every dependency at the top of the effect body. Conditional reads re-track on every run (only the branch actually taken is tracked). Writes inside an effect do not create subscriptions — only reads do.
69
+
70
+ 5. **Keep and call the dispose functions.** `effect()`, `bindDom()`, `watch()`, `repeat()`, `persist()`, `$subscribe()`, and `computed().subscribe()` all return a cleanup/unsubscribe function. Call it when the UI section goes away, or collect them:
71
+ ```javascript
72
+ import { createCleanupGroup } from 'lume-js/addons';
73
+ const group = createCleanupGroup();
74
+ group.add(effect(() => { /* … */ }));
75
+ group.add(bindDom(panel, store));
76
+ group.dispose(); // dispose everything at once
77
+ ```
78
+ Creating effects/subscriptions in a loop without cleanup eventually hits the per-key subscriber cap (1000) and logs a console error.
79
+
80
+ 6. **Writing several stores from one event? Wrap it in `batch()`.** Per-store microtask batching already dedupes same-store writes; an effect reading N stores mutated in the same tick still runs once per store. `batch(fn)` flushes synchronously when the outermost batch ends, running cross-store effects exactly once. `fn` must be synchronous — writes after an `await` inside it are NOT batched.
81
+ ```javascript
82
+ batch(() => {
83
+ cart.items = [...cart.items, item];
84
+ totals.sum += item.price;
85
+ ui.message = 'Added!';
86
+ });
87
+ ```
88
+
89
+ 7. **Stores hold plain objects.** `state()` throws on non-objects, arrays, and frozen/sealed objects at the top level. `Map`/`Set` are not reactive (use arrays/objects). Class instances mostly work but private fields bypass the proxy. Functions stored on state stay plain (usable with the `on()` handler). Keys starting with `$` are reserved meta API (`$subscribe`, `$beforeFlush`); writes to `__proto__`/`constructor`/ `prototype` are blocked.
90
+
91
+ 8. **`data-*` attribute values are state keys, never expressions.** `data-bind="user.name"`, `data-show="count > 0"` — ❌ not supported. Derive a flat key in JS instead:
92
+ ```javascript
93
+ effect(() => { store.hasItems = store.items.length > 0; });
94
+ ```
95
+ ```html
96
+ <div data-show="hasItems">…</div>
97
+ ```
98
+
99
+ ## Choosing the right primitive
100
+
101
+ | You want to… | Use |
102
+ |---|---|
103
+ | Derive a value **into the store** (for `data-bind`) | `effect(() => { store.full = store.a + store.b; })` |
104
+ | Derive a read-only value consumed **outside** the store | `computed(() => store.a * 2)` → `.value`, `.subscribe(fn)`, `.dispose()` |
105
+ | React to **one specific key** (analytics, localStorage, manual DOM) | `watch(store, 'key', fn)` — options `{ immediate: false }` to skip the initial call |
106
+ | Run side effects when **anything read** changes | `effect(fn)` (auto-tracking) |
107
+ | Side effect with **no magic tracking** | `effect(fn, [[store, 'key1', 'key2'], [other, 'k']])` (explicit deps, coalesced to one run per microtask) |
108
+ | Render an **array** as DOM elements | `repeat(container, store, 'items', { key: it => it.id, … })` |
109
+ | Low-level single-key subscription (no tracking) | `store.$subscribe('key', fn)` — calls immediately with current value |
110
+
111
+ Anti-pattern: calling `$subscribe` for a key inside an `effect` that also reads that key — the logic runs twice. Pick one mechanism.
112
+
113
+ ## Built-in `data-*` attributes (core `bindDom`)
114
+
115
+ ```html
116
+ <input data-bind="name"> <!-- two-way: input/checkbox/radio/select/textarea -->
117
+ <span data-bind="name"></span> <!-- one-way textContent -->
118
+ <div data-hidden="isLoading">…</div> <!-- boolean attrs: hidden/disabled/checked/required -->
119
+ <button data-disabled="isSubmitting">Go</button>
120
+ <button data-aria-expanded="menuOpen">Menu</button> <!-- ARIA: "true"/"false" strings -->
121
+ ```
122
+
123
+ More attributes are opt-in handlers, passed to `bindDom`:
124
+
125
+ ```javascript
126
+ import { show, classToggle, stringAttr, on } from 'lume-js/handlers';
127
+ bindDom(root, store, { handlers: [show, classToggle('active'), stringAttr('href'), on('click')] });
128
+ ```
129
+
130
+ ```html
131
+ <span data-show="isVisible">…</span> <!-- el.hidden = !truthy -->
132
+ <div data-class-active="isActive">…</div> <!-- toggles the 'active' class -->
133
+ <a data-href="profileUrl">Profile</a> <!-- string attr, removed on null -->
134
+ <button data-onclick="addItem">Add</button> <!-- store.addItem wired as listener -->
135
+ ```
136
+
137
+ Custom handlers are plain objects: `{ attr: 'data-tooltip', apply(el, val) { el.title = val ?? ''; } }`. `htmlAttrs()` is the one-import preset covering standard HTML + ARIA attrs.
138
+
139
+ ## Lists with `repeat()` (keyed, element-reusing)
140
+
141
+ ```html
142
+ <ul id="todos">
143
+ <template> <!-- row structure is a standard <template>; $index is provided -->
144
+ <li><span data-bind="title"></span> <em data-bind="$index"></em></li>
145
+ </template>
146
+ </ul>
147
+ ```
148
+
149
+ ```javascript
150
+ import { repeat } from 'lume-js/addons';
151
+ repeat('#todos', store, 'todos', { key: t => t.id, template: true });
152
+ ```
153
+
154
+ Imperative variant when you need custom DOM per row:
155
+
156
+ ```javascript
157
+ repeat(listEl, store, 'items', {
158
+ element: 'li',
159
+ key: it => it.id,
160
+ create: (it, el) => { /* build row structure once */ },
161
+ update: (it, el) => { /* sync data on every change */ },
162
+ });
163
+ ```
164
+
165
+ `repeat` diffs by `key` against the previous array — which is why rule #1 (immutable array updates) is non-negotiable.
166
+
167
+ ## Canonical app skeleton
168
+
169
+ ```html
170
+ <!DOCTYPE html>
171
+ <html lang="en">
172
+ <body>
173
+ <input data-bind="query" placeholder="Search">
174
+ <p data-show="loading">Loading…</p>
175
+ <ul id="results"><template><li data-bind="title"></li></template></ul>
176
+ <script type="module" src="./app.js"></script>
177
+ </body>
178
+ </html>
179
+ ```
180
+
181
+ ```javascript
182
+ import { state, effect, bindDom, batch } from 'lume-js';
183
+ import { repeat, watch, persist, createCleanupGroup } from 'lume-js/addons';
184
+ import { show } from 'lume-js/handlers';
185
+
186
+ const store = state({ query: '', loading: false, results: [] });
187
+ const group = createCleanupGroup();
188
+
189
+ group.add(bindDom(document.body, store, { handlers: [show] }));
190
+ group.add(repeat('#results', store, 'results', { key: r => r.id, template: true }));
191
+ group.add(persist(store, 'my-app', { keys: ['query'] })); // localStorage sync
192
+
193
+ group.add(watch(store, 'query', async (q) => {
194
+ store.loading = true;
195
+ const results = await fetchResults(q);
196
+ batch(() => { // one flush for both writes
197
+ store.results = results; // replace, never mutate
198
+ store.loading = false;
199
+ });
200
+ }, { immediate: false }));
201
+ ```
202
+
203
+ ## Debugging & testing
204
+
205
+ - Write/read logging is a plugin:
206
+ ```javascript
207
+ import { withPlugins, createDebugPlugin, debug } from 'lume-js/addons';
208
+ const store = withPlugins(state({ count: 0 }), [createDebugPlugin({ label: 'cart' })]);
209
+ debug.enable(); // global on/off switch; also debug.filter('key'), debug.stats()
210
+ ```
211
+ - Nothing updates? Check, in order: (1) mutated an array/nested object in place (rule 1/2), (2) asserted synchronously before the microtask flush (rule 3), (3) the read happened after an `await` inside an effect (rule 4), (4) `bindDom` was called before the element existed, or on a root that doesn't contain it.
212
+ - Effect runs too often? Reading many stores in one effect while writing them separately — wrap the writes in `batch()` (rule 6).
213
+ - Console error about subscriber limit (1000/key) or max flush iterations (100) = subscription leak in a loop, or an effect writing a key it also reads (infinite cascade).
214
+ - Tests: jsdom + vitest work fine. Write state → `await Promise.resolve()` → assert DOM. See `docs/guides/testing.md`.
215
+
216
+ ## SSR / no-DOM environments
217
+
218
+ Import `state`/`batch` from `lume-js/state` (1.46 KB, zero DOM references) in Node/workers/CLI. For server-rendered pages, inline initial state as `<script type="application/json">` and hydrate with `hydrateState()` (`lume-js/addons`), then call `bindDom` as usual.
219
+
220
+ ## Keeping agents up to date
221
+
222
+ `llms.txt` and `llms-full.txt` are regenerated from this guide plus the docs tree by `npm run llms`, and CI fails when they drift — whatever version of `lume-js` is installed, the bundled files match its actual behavior. In a consuming project, add one line to your agent config (`CLAUDE.md`, `.cursorrules`, `AGENTS.md`, …):
223
+
224
+ > Before writing any code that uses lume-js, read `node_modules/lume-js/AGENT_GUIDE.md`. For API details beyond the guide, consult `node_modules/lume-js/llms-full.txt`.
package/README.md CHANGED
@@ -4,18 +4,22 @@
4
4
  <p><strong>Reactivity that follows web standards.</strong></p>
5
5
  <p>
6
6
  Minimal reactive state management using only standard JavaScript and HTML.<br>
7
- No custom syntax &nbsp;·&nbsp; No build step &nbsp;·&nbsp; No framework lock-in.
7
+ No custom syntax &nbsp;·&nbsp; No build step &nbsp;·&nbsp; No framework lock-in.<br>
8
+ <strong>1.46 KB universal core</strong> &nbsp;·&nbsp; <strong>2.66 KB with DOM</strong>
8
9
  </p>
9
10
  <p>
10
11
  <a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-blue.svg" alt="License: MIT"></a>
11
12
  &nbsp;
12
- <a href="package.json"><img src="https://img.shields.io/badge/version-2.2.1-orange.svg" alt="v2.2.1"></a>
13
+ <a href="package.json"><img src="https://img.shields.io/badge/version-2.3.1-orange.svg" alt="v2.3.1"></a>
13
14
  &nbsp;
14
- <a href="tests/"><img src="https://img.shields.io/badge/tests-355%20passing-brightgreen.svg" alt="355 tests"></a>
15
+ <a href="tests/"><img src="https://img.shields.io/badge/tests-439%20passing-brightgreen.svg" alt="439 tests"></a>
15
16
  &nbsp;
16
- <a href="scripts/check-size.js"><img src="https://img.shields.io/badge/core-2.23KB%20gzipped-blue.svg" alt="2.23KB"></a>
17
+ <a href="scripts/check-size.js"><img src="https://img.shields.io/badge/universal%20core-1.46KB-blue.svg" alt="universal core 1.46KB"></a>
18
+ &nbsp;
19
+ <a href="scripts/check-size.js"><img src="https://img.shields.io/badge/core%20%2B%20DOM-2.66KB-blue.svg" alt="core + DOM 2.66KB"></a>
17
20
  </p>
18
21
  <p><code>npm install lume-js</code></p>
22
+ <p><a href="https://sathvikc.github.io/lume-js/"><strong>Docs & live examples →</strong></a></p>
19
23
  </div>
20
24
 
21
25
  ---
@@ -26,7 +30,7 @@
26
30
  |---------|---------|-----------|-----|-------|
27
31
  | Custom Syntax | ❌ No | ✅ `x-data` | ✅ `v-bind` | ✅ JSX |
28
32
  | Build Step | ❌ Optional | ❌ Optional | ⚠️ Recommended | ✅ Required |
29
- | Bundle Size | ~2.23KB | ~15KB | ~35KB | ~45KB |
33
+ | Bundle Size | 1.46–2.66KB | ~15KB | ~35KB | ~45KB |
30
34
  | HTML Validation | ✅ Pass | ⚠️ Warnings | ⚠️ Warnings | ❌ JSX |
31
35
  | Extensible Handlers | ✅ | ❌ Built-in only | ❌ Built-in only | N/A |
32
36
 
@@ -36,6 +40,15 @@
36
40
 
37
41
  ## Installation
38
42
 
43
+ ### Pick your entry
44
+
45
+ | Entry | Size (gz) | Contents | For |
46
+ |-------|-----------|----------|-----|
47
+ | `lume-js/state` | **1.46 KB** | `state`, `batch`, `withReadObserver` | Node, Deno, Bun, workers, CLI — anywhere without a DOM |
48
+ | `lume-js` | **2.66 KB** | + `bindDom`, `effect` | Browsers |
49
+ | `lume-js/addons` | pay per import | `computed`, `watch`, `repeat`, `persist`, … | Optional patterns |
50
+ | `lume-js/handlers` | pay per import | `show`, `classToggle`, `on`, … | Extra reactive attributes |
51
+
39
52
  ### Via CDN (Recommended for simple projects)
40
53
 
41
54
  ```html
@@ -44,6 +57,14 @@
44
57
  </script>
45
58
  ```
46
59
 
60
+ DOM-free kernel only (servers, workers, embedded):
61
+
62
+ ```html
63
+ <script type="module">
64
+ import { state, batch } from 'https://cdn.jsdelivr.net/npm/lume-js/dist/state.min.mjs';
65
+ </script>
66
+ ```
67
+
47
68
  ### Via NPM (Recommended for bundlers)
48
69
 
49
70
  ```bash
@@ -51,20 +72,24 @@ npm install lume-js
51
72
  ```
52
73
 
53
74
  ```javascript
54
- import { state, bindDom } from 'lume-js';
75
+ import { state, bindDom } from 'lume-js'; // browser: full core
76
+ import { state, batch } from 'lume-js/state'; // Node/CLI/workers: 1.46 KB kernel
55
77
  ```
56
78
 
79
+ > **→ Using Lume without a DOM?** See the [Universal core guide](docs/guides/universal-core.md).
80
+
81
+
57
82
  ### Browser Support
58
83
 
59
84
  | Browser | Minimum version |
60
85
  |---------|-----------------|
61
- | Chrome | 49+ |
62
- | Firefox | 18+ |
63
- | Safari | 10+ |
64
- | Edge | 79+ |
86
+ | Chrome | 80+ |
87
+ | Firefox | 74+ |
88
+ | Safari | 13.1+ |
89
+ | Edge | 80+ |
65
90
  | IE11 | ❌ Not supported |
66
91
 
67
- IE11 cannot be polyfilled Lume uses `Proxy`.
92
+ The floor comes from optional chaining / nullish coalescing (ES2020) used in the source — shipped un-transpiled, true to no-build. IE11 cannot be polyfilled regardless: Lume uses `Proxy`.
68
93
 
69
94
  ---
70
95
 
@@ -88,6 +113,37 @@ bindDom(document.body, store);
88
113
 
89
114
  That's it — two-way binding, no build step, valid HTML.
90
115
 
116
+ ### Lists? Also just HTML.
117
+
118
+ ```html
119
+ <ul id="todos">
120
+ <template>
121
+ <li><span data-bind="title"></span> <em data-bind="$index"></em></li>
122
+ </template>
123
+ </ul>
124
+ ```
125
+
126
+ ```javascript
127
+ import { repeat } from 'lume-js/addons';
128
+
129
+ repeat('#todos', store, 'todos', { key: t => t.id, template: true });
130
+ ```
131
+
132
+ The row structure is a standard `<template>` element — no `createElement`, no JSX, no custom syntax.
133
+
134
+ ### Multiple stores? Batch them.
135
+
136
+ ```javascript
137
+ import { batch } from 'lume-js';
138
+
139
+ batch(() => {
140
+ cart.items = [...cart.items, item];
141
+ totals.sum += item.price;
142
+ ui.message = 'Added!';
143
+ });
144
+ // effects depending on several stores ran exactly ONCE, synchronously
145
+ ```
146
+
91
147
  ---
92
148
 
93
149
  ## Built-in Reactive Attributes
@@ -149,6 +205,7 @@ bindDom(document.body, store, {
149
205
  | `ariaAttr(name)` | `data-aria-pressed="key"` | Sets ARIA attribute to "true"/"false" |
150
206
  | `classToggle(...names)` | `data-class-active="key"` | Toggles individual CSS classes |
151
207
  | `stringAttr(name)` | `data-href="key"` | Sets string attributes (removes on null) |
208
+ | `on(...types)` | `data-onclick="key"` | Wires the function at that state key as an event listener |
152
209
  | `htmlAttrs()` | *(all of the above)* | One-import preset — all standard HTML + ARIA attrs |
153
210
 
154
211
  ### Presets
@@ -188,9 +245,10 @@ import { computed, watch, repeat } from 'lume-js/addons';
188
245
  | `effect(fn)` *(core)* | Write derived values back into the store, or trigger side effects on state change |
189
246
  | `computed(fn)` | Derive a read-only value from state to consume *outside* the store (templates, display logic) |
190
247
  | `watch(store, key, fn)` | React to a *specific* key changing — DOM updates, analytics, syncing external state |
191
- | `repeat(container, store, key, opts)` | Render a keyed list with element reuse (no full re-render on change) |
248
+ | `repeat(container, store, key, opts)` | Render a keyed list with element reuse incl. declarative `template:` mode bound straight from a `<template>` element |
249
+ | `persist(store, key, opts)` | Sync selected keys with localStorage/sessionStorage — hydrate on call, auto-save on change |
192
250
  | `createCleanupGroup()` | Collect multiple cleanup/unsubscribe functions and dispose them all at once |
193
- | `hydrateState(selector?)` | Read initial state from a `<script type="application/json">` tag (SSR hydration) |
251
+ | `hydrateState(selector?, validate?)` | Read initial state from a `<script type="application/json">` tag (SSR hydration), with optional schema validation |
194
252
 
195
253
  **Quick rule:** `effect` for writing back into state → `computed` for reading outside state → `watch` for observing a single key → `repeat` for arrays in the DOM.
196
254
 
@@ -228,9 +286,30 @@ watch(store, 'count', (val) => {
228
286
 
229
287
  ---
230
288
 
289
+ ## Using Lume.js with AI agents
290
+
291
+ Lume ships agent-ready documentation **inside the npm package**, regenerated from the docs on every change (CI-enforced), so it always matches the installed version:
292
+
293
+ | File | What it is |
294
+ |------|-----------|
295
+ | [`AGENT_GUIDE.md`](AGENT_GUIDE.md) | Distilled rules, pitfalls, and canonical patterns — one read teaches an agent to write correct Lume code |
296
+ | [`llms.txt`](llms.txt) | [llmstxt.org](https://llmstxt.org) index of all docs, for web-browsing agents |
297
+ | [`llms-full.txt`](llms-full.txt) | Every guide, tutorial, and API page in one file |
298
+
299
+ In a project that uses Lume, add one line to your agent config (`CLAUDE.md`, `AGENTS.md`, `.cursorrules`, …):
300
+
301
+ ```markdown
302
+ Before writing any code that uses lume-js, read node_modules/lume-js/AGENT_GUIDE.md.
303
+ For API details beyond the guide, consult node_modules/lume-js/llms-full.txt.
304
+ ```
305
+
306
+ No install? Point the agent at the hosted copies: `https://raw.githubusercontent.com/sathvikc/lume-js/main/AGENT_GUIDE.md` and `…/llms-full.txt`.
307
+
308
+ ---
309
+
231
310
  ## Documentation
232
311
 
233
- Full documentation is available in the [docs/](docs/) directory:
312
+ Browse the full docs at **[sathvikc.github.io/lume-js](https://sathvikc.github.io/lume-js/)**, or read the source markdown in the [docs/](docs/) directory:
234
313
 
235
314
  - **Tutorials**
236
315
  - [Build a Todo App](docs/tutorials/build-todo-app.md)
@@ -240,10 +319,12 @@ Full documentation is available in the [docs/](docs/) directory:
240
319
  - [state()](docs/api/core/state.md) — Reactive state
241
320
  - [bindDom()](docs/api/core/bindDom.md) — DOM binding
242
321
  - [effect()](docs/api/core/effect.md) — Reactive effects
322
+ - [batch()](docs/api/core/batch.md) — Cross-store write batching
243
323
  - [Handlers](docs/api/core/handlers.md) — Extensible attribute handlers
244
- - [Plugins](docs/api/core/plugins.md) — State extension system
245
- - [Addons](docs/api/addons/computed.md) — computed, watch, repeat, createCleanupGroup, hydrateState
324
+ - [Plugins](docs/api/addons/withPlugins.md) — State extension system
325
+ - [Addons](docs/api/addons/computed.md) — computed, watch, repeat, persist, createCleanupGroup, hydrateState
246
326
  - **Guides**
327
+ - [Universal core (Node, CLI, workers)](docs/guides/universal-core.md) — using `lume-js/state` without a DOM
247
328
  - [Choosing reactive primitives](docs/guides/choosing-reactive-primitives.md) — when to use effect vs computed vs watch
248
329
  - [Cleanup & Disposal](docs/guides/cleanup-and-dispose.md) — tearing down effects, bindings, and subscriptions
249
330
  - [SSR & Hydration](docs/guides/ssr-hydration.md) — server-rendered HTML with reactive hydration
@@ -1 +1 @@
1
- function e(e,...t){void 0!==console&&"function"==typeof console.warn&&console.warn(e,...t)}function t(e,...t){void 0!==console&&"function"==typeof console.error&&console.error(e,...t)}const o=new Set;function n(e,t){o.add(e);try{return t()}finally{o.delete(e)}}let r=null;function c(e,o){if("function"!=typeof e)throw Error("effect() requires a function");const c=[];let i=!1;const s=()=>{if(!i){i=!0;try{e()}catch(e){throw t("[Lume.js effect] Error in effect:",e),e}finally{i=!1}}};if(Array.isArray(o)){for(const e of o)if(Array.isArray(e)&&e.length>=2){const[t,...o]=e;if(t&&"function"==typeof t.$subscribe)for(const e of o){let o=!0;const n=t.$subscribe(e,()=>{o?o=!1:s()});c.push(n)}}s()}else{const o=()=>{if(i)return;const s=c.splice(0),l={fn:e,cleanups:c,execute:o,tracking:{}},f=r;r=l,i=!0;try{n((e,t,o)=>{r===l&&(l.tracking[t]||(l.tracking[t]=!0,l.cleanups.push(o(t,l.execute))))},e)}catch(e){throw c.length=0,c.push(...s),t("[Lume.js effect] Error in effect:",e),e}finally{r=f,i=!1}if(c.length>0)for(const e of s)e();else c.push(...s)};o()}return()=>{for(;c.length;)c.pop()()}}function i(e){if("function"!=typeof e)throw Error("computed() requires a function");let o,n=!1,r=!1,i=!1;const s=[],l=c(()=>{if(!r&&!i){r=!0;try{const t=e();n&&Object.is(t,o)||(o=t,n=!0,s.forEach(e=>e(o)))}catch(e){t("[Lume.js computed] Error in computation:",e),n&&void 0===o||(o=void 0,n=!0,s.forEach(e=>e(o)))}finally{queueMicrotask(()=>{i||(r=!1)})}}});return{get value(){if(!n)throw Error("Computed value accessed before initialization");return o},subscribe(e){if("function"!=typeof e)throw Error("subscribe() requires a function");return s.push(e),n&&e(o),()=>{const t=s.indexOf(e);t>-1&&s.splice(t,1)}},dispose(){i=!0,l(),s.length=0,n=!1,r=!1}}}function s(e,t,o,{immediate:n=!0}={}){if(!e.$subscribe)throw Error("store must be created with state()");if(!n){let n=!1;return e.$subscribe(t,e=>{n?o(e):n=!0})}return e.$subscribe(t,o)}function l(e){const t=document.activeElement;if(!e.contains(t))return null;let o=null,n=null;return"INPUT"!==t.tagName&&"TEXTAREA"!==t.tagName||(o=t.selectionStart,n=t.selectionEnd),()=>{document.body.contains(t)&&(t.focus(),null!==o&&null!==n&&t.setSelectionRange(o,n))}}function f(e,t={}){const{isReorder:o=!1}=t,n=e.scrollTop;if(0===n)return()=>{e.scrollTop=0};let r=null,c=0;if(!o){const t=e.getBoundingClientRect();for(let o=e.firstElementChild;o;o=o.nextElementSibling){const e=o.getBoundingClientRect();if(e.bottom>t.top){r=o,c=e.top-t.top;break}}}return()=>{if(r&&document.body.contains(r)){const t=r.getBoundingClientRect(),o=e.getBoundingClientRect(),n=t.top-o.top-c;e.scrollTop=e.scrollTop+n}else e.scrollTop=n}}function u(o,n,r,c){const{key:i,render:s,create:u,update:a,remove:g,element:p="div",preserveFocus:d=l,preserveScroll:b=f}=c,h="string"==typeof o?document.querySelector(o):o;if(!h)return e(`[Lume.js] repeat(): container "${o}" not found`),()=>{};if("function"!=typeof i)throw Error("[Lume.js] repeat(): options.key must be a function");if("function"!=typeof s&&"function"!=typeof u)throw Error("[Lume.js] repeat(): options.render or options.create must be a function");const y=new Map,m=new Map,$=new Map,w=new Map,E=new Set;function j(){return"function"==typeof p?p():document.createElement(p)}function S(){const o=n[r];if(!Array.isArray(o))return void e(`[Lume.js] repeat(): store.${r} is not an array`);let c=!1;if(b&&y.size===o.length){c=!0;for(let e=0;e<o.length;e++)if(!y.has(i(o[e]))){c=!1;break}}E.clear();const l=[];for(let n=0;n<o.length;n++){const r=o[n],c=i(r);if(E.has(c)){e(`[Lume.js] repeat(): duplicate key "${c}"`);continue}E.add(c);let f=y.get(c);const g=!f;g&&(f=j(),y.set(c,f));try{if(g&&u){const e=u(r,f,n);"function"==typeof e&&w.set(c,e)}const e=m.get(c),t=$.get(c);a?e===r&&t===n||a(r,f,n,{isFirstRender:g}):s&&s(r,f,n),m.set(c,r),$.set(c,n)}catch(e){t(`[Lume.js] repeat(): error rendering key "${c}":`,e)}l.push(f)}!function(e,o,n){const r=document.body.contains(e),c=r&&d?d(e):null,i=r&&b?b(e,{isReorder:n}):null;(()=>{if(function(e,t){let o=e.firstChild;for(let n=0;n<t.length;n++){const r=t[n];o!==r?e.insertBefore(r,o):o=o.nextSibling}for(;o;){const t=o.nextSibling;e.removeChild(o),o=t}}(h,l),y.size!==E.size)for(const e of y.keys())if(!E.has(e)){const o=y.get(e),n=m.get(e),r=w.get(e);if("function"==typeof r)try{r()}catch(o){t(`[Lume.js] repeat(): cleanup error for key "${e}":`,o)}"function"==typeof g&&o&&g(n,o),y.delete(e),m.delete(e),$.delete(e),w.delete(e)}})(),c&&c(),i&&i()}(h,0,c)}let L;if("function"==typeof n.$subscribe)L=n.$subscribe(r,S);else{if("function"!=typeof n.subscribe)return S(),e("[Lume.js] repeat(): store is not reactive (no $subscribe or subscribe method)"),()=>{for(const[e,o]of y){const n=m.get(e),r=w.get(e);if("function"==typeof r)try{r()}catch(o){t(`[Lume.js] repeat(): cleanup error for key "${e}":`,o)}"function"==typeof g&&g(n,o)}h.replaceChildren(),y.clear(),m.clear(),$.clear(),w.clear(),E.clear()};{const e=n.subscribe(()=>S());S(),L="function"==typeof e?e:()=>{e?.unsubscribe?.()}}}return()=>{"function"==typeof L&&L();for(const[e,o]of y){const n=m.get(e),r=w.get(e);if("function"==typeof r)try{r()}catch(o){t(`[Lume.js] repeat(): cleanup error for key "${e}":`,o)}"function"==typeof g&&g(n,o)}h.replaceChildren(),y.clear(),m.clear(),$.clear(),w.clear(),E.clear()}}let a=!0,g=null;const p=new Map;function d(e){return null===g||("string"==typeof g?e.includes(g):!(g instanceof RegExp)||g.test(e))}function b(e){return p.has(e)||p.set(e,{gets:new Map,sets:new Map,notifies:new Map}),p.get(e)}function h(e,t,o){const n=b(e)[t];n.set(o,(n.get(o)||0)+1)}const y=100,m=97;function $(e){try{const t=JSON.stringify(e);return t.length>y?t.slice(0,m)+"...":t}catch{return e+""}}function w(e={}){const t=e.label??"store",o=(t,o)=>{const n=e[t];return void 0!==n?n:o};return{name:"debug:"+t,onInit:()=>{a&&console.log(`%c[${t}]%c initialized`,"color: #888; font-weight: bold","color: inherit")},onGet:(e,n)=>("string"==typeof e&&e.startsWith("$")||(h(t,"gets",e),a&&o("logGet",!1)&&d(e)&&console.log(`%c[${t}]%c GET %c${e}%c = ${$(n)}`,"color: #888; font-weight: bold","color: #4CAF50","color: #2196F3; font-weight: bold","color: inherit")),n),onSet:(e,n,r)=>("string"==typeof e&&e.startsWith("$")||(h(t,"sets",e),a&&o("logSet",!0)&&d(e)&&(console.log(`%c[${t}]%c SET %c${e}%c: ${$(r)} → ${$(n)}`,"color: #888; font-weight: bold","color: #FF9800","color: #2196F3; font-weight: bold","color: inherit"),o("trace",!1)&&console.trace(`%c[${t}] Stack trace for ${e}`,"color: #888"))),n),onSubscribe:e=>{a&&d(e)&&console.log(`%c[${t}]%c SUBSCRIBE %c${e}`,"color: #888; font-weight: bold","color: #9C27B0","color: #2196F3; font-weight: bold")},onNotify:(e,n)=>{"string"==typeof e&&e.startsWith("$")||(h(t,"notifies",e),a&&o("logNotify",!0)&&d(e)&&console.log(`%c[${t}]%c NOTIFY %c${e}%c = ${$(n)}`,"color: #888; font-weight: bold","color: #E91E63","color: #2196F3; font-weight: bold","color: inherit"))}}}const E={enable(){a=!0,console.log("%c[lume-debug]%c Logging enabled","color: #888; font-weight: bold","color: #4CAF50")},disable(){a=!1,console.log("%c[lume-debug]%c Logging disabled","color: #888; font-weight: bold","color: #F44336")},isEnabled:()=>a,filter(e){g=e,console.log(null===e?"%c[lume-debug]%c Filter cleared":"%c[lume-debug]%c Filter set: "+e,"color: #888; font-weight: bold","color: inherit")},getFilter:()=>g,stats(){const e={};for(const[t,o]of p)e[t]={gets:Object.fromEntries(o.gets),sets:Object.fromEntries(o.sets),notifies:Object.fromEntries(o.notifies)};return e},logStats(){const e=this.stats();if(0===Object.keys(e).length)return console.log("%c[lume-debug]%c No stats collected yet","color: #888; font-weight: bold","color: inherit"),e;console.group("%c[lume-debug] Statistics","color: #888; font-weight: bold");for(const[t,o]of Object.entries(e)){console.group("%c"+t,"color: #2196F3; font-weight: bold");const e=[],n=new Set([...Object.keys(o.gets),...Object.keys(o.sets),...Object.keys(o.notifies)]);for(const t of n)e.push({key:t,gets:o.gets[t]||0,sets:o.sets[t]||0,notifies:o.notifies[t]||0});e.length>0&&console.table(e),console.groupEnd()}return console.groupEnd(),e},resetStats(){p.clear(),console.log("%c[lume-debug]%c Stats reset","color: #888; font-weight: bold","color: inherit")}};function j(e,o=[]){if(!o.length)return e;for(const e of o){try{e.onInit?.()}catch(o){t(`[Lume.js] Plugin "${e.name}" error in onInit:`,o)}Object.freeze(e)}const n=new Map;let r;return"function"==typeof e.$beforeFlush&&(r=e.$beforeFlush(function(){for(const[e,r]of n)for(const n of o)try{n.onNotify?.(e,r)}catch(e){t(`[Lume.js] Plugin "${n.name}" error in onNotify:`,e)}n.clear()})),new Proxy(e,{get(e,c){if("$dispose"===c)return()=>{r&&r(),n.clear()};if("string"==typeof c&&c.startsWith("$")){const n=e[c];return"$subscribe"===c&&"function"==typeof n?(e,r)=>{for(const n of o)try{n.onSubscribe?.(e)}catch(e){t(`[Lume.js] Plugin "${n.name}" error in onSubscribe:`,e)}return n(e,r)}:n}let i=e[c];for(const e of o)try{const t=e.onGet?.(c,i);void 0!==t&&(i=t)}catch(o){t(`[Lume.js] Plugin "${e.name}" error in onGet:`,o)}return i},set(e,r,c){const i=e[r];let s=c;for(const e of o)try{const t=e.onSet?.(r,s,i);void 0!==t&&(s=t)}catch(o){t(`[Lume.js] Plugin "${e.name}" error in onSet:`,o)}return Object.is(s,i)||n.set(r,s),e[r]=s,!0}})}function S(){const e=[];return{add(t){"function"==typeof t&&e.push(t)},dispose(){for(;e.length;){const t=e.pop();try{t()}catch(e){}}}}}function L(e="#__LUME_DATA__",t){const o="undefined"!=typeof document?document.querySelector(e):null;if(!o)return{};if("SCRIPT"!==o.tagName||"application/json"!==o.type)return{};let n;try{n=JSON.parse(o.textContent)}catch{return{}}return"function"!=typeof t||t(n)?n:{}}function k(e){return!(!e||"object"!=typeof e||"function"!=typeof e.$subscribe)}export{i as computed,S as createCleanupGroup,w as createDebugPlugin,E as debug,l as defaultFocusPreservation,f as defaultScrollPreservation,L as hydrateState,k as isReactive,u as repeat,s as watch,j as withPlugins};
1
+ function e(e,...t){void 0!==console&&"function"==typeof console.warn&&console.warn(e,...t)}function t(e,...t){void 0!==console&&"function"==typeof console.error&&console.error(e,...t)}const o=new Set,n=Symbol.for("lume.reactive");function r(e,t){o.add(e);try{return t()}finally{o.delete(e)}}let c=null;function s(e,o){if("function"!=typeof e)throw Error("effect() requires a function");const n=[];let s=!1;const i=()=>{if(!s){s=!0;try{e()}catch(e){throw t("[Lume.js effect] Error in effect:",e),e}finally{s=!1}}};if(Array.isArray(o)){let e=!1,t=!1;n.push(()=>{t=!0});const r=()=>{e||(e=!0,queueMicrotask(()=>{if(e=!1,!t)try{i()}catch{}}))};for(const e of o)if(Array.isArray(e)&&e.length>=2){const[t,...o]=e;if(t&&"function"==typeof t.$subscribe)for(const e of o){let o=!0;const c=t.$subscribe(e,()=>{o?o=!1:r()});n.push(c)}}i()}else{const o=()=>{if(s)return;const i=n.splice(0),l={fn:e,cleanups:n,execute:o,tracking:new WeakMap},u=c;c=l,s=!0;try{r((e,t,o)=>{if(c!==l)return;let n=l.tracking.get(e);n||(n=new Set,l.tracking.set(e,n)),n.has(t)||(n.add(t),l.cleanups.push(o(t,l.execute)))},e)}catch(e){throw n.length=0,n.push(...i),t("[Lume.js effect] Error in effect:",e),e}finally{c=u,s=!1}if(n.length>0)for(const e of i)e();else n.push(...i)};o()}return()=>{for(;n.length;)n.pop()()}}function i(e){if("function"!=typeof e)throw Error("computed() requires a function");let o,n=!1,r=!1,c=!1;const i=[],l=s(()=>{if(!r&&!c){r=!0;try{const t=e();n&&Object.is(t,o)||(o=t,n=!0,i.forEach(e=>e(o)))}catch(e){t("[Lume.js computed] Error in computation:",e),n&&void 0===o||(o=void 0,n=!0,i.forEach(e=>e(o)))}finally{queueMicrotask(()=>{c||(r=!1)})}}});return{get value(){if(!n)throw Error("Computed value accessed before initialization");return o},subscribe(e){if("function"!=typeof e)throw Error("subscribe() requires a function");return i.push(e),n&&e(o),()=>{const t=i.indexOf(e);t>-1&&i.splice(t,1)}},dispose(){c=!0,l(),i.length=0,n=!1,r=!1}}}function l(e,t,o,{immediate:n=!0}={}){if(!e.$subscribe)throw Error("store must be created with state()");if(!n){let n=!1;return e.$subscribe(t,e=>{n?o(e):n=!0})}return e.$subscribe(t,o)}function u(e,t){"INPUT"===e.tagName?"checkbox"===e.type?e.checked=!!t:"radio"===e.type?e.checked=e.value===t+"":e.value=t??"":"TEXTAREA"===e.tagName||"SELECT"===e.tagName?e.value=t??"":e.textContent=t??""}function f(e,t){let o=e;if(!0===e?o=t.querySelector("template"):"string"==typeof e&&(o=document.querySelector(e)),!o||"TEMPLATE"!==o.tagName)throw Error("[Lume.js] repeat(): template not found or not a <template> element");if(1!==o.content.children.length)throw Error("[Lume.js] repeat(): template must contain exactly one root element");return o}function a(e,t){if(!e)return{templateRoot:null,keepEl:null};const o=f(e,t);let n=o;for(;n&&n.parentNode!==t;)n=n.parentNode;return{templateRoot:o.content.firstElementChild,keepEl:n}}function p(e){const t=[],o=e=>{const o=e.getAttribute("data-bind");t.push({node:e,path:o,keys:"$item"===o||"$index"===o?null:o.split(".")})};e.hasAttribute("data-bind")&&o(e);for(const t of e.querySelectorAll("[data-bind]"))o(t);return t}function g(e,t,o){for(const n of e){let e;if("$index"===n.path)e=o;else if("$item"===n.path)e=t;else{e=t;for(let t=0;t<n.keys.length&&null!=e;t++)e=e[n.keys[t]]}u(n.node,e)}}function d(e){const t=document.activeElement;if(!e.contains(t))return null;let o=null,n=null;return"INPUT"!==t.tagName&&"TEXTAREA"!==t.tagName||(o=t.selectionStart,n=t.selectionEnd),()=>{document.body.contains(t)&&(t.focus(),null!==o&&null!==n&&t.setSelectionRange(o,n))}}function y(e,t={}){const{isReorder:o=!1}=t,n=e.scrollTop;if(0===n)return()=>{e.scrollTop=0};let r=null,c=0;if(!o){const t=e.getBoundingClientRect();for(let o=e.firstElementChild;o;o=o.nextElementSibling){const e=o.getBoundingClientRect();if(e.bottom>t.top){r=o,c=e.top-t.top;break}}}return()=>{if(r&&document.body.contains(r)){const t=r.getBoundingClientRect(),o=e.getBoundingClientRect(),n=t.top-o.top-c;e.scrollTop=e.scrollTop+n}else e.scrollTop=n}}function h(o,n,r,c){const{key:s,render:i,create:l,update:u,remove:f,template:h=null,element:b="div",preserveFocus:m=d,preserveScroll:w=y}=c,$="string"==typeof o?document.querySelector(o):o;if(!$)return e(`[Lume.js] repeat(): container "${o}" not found`),()=>{};if("function"!=typeof s)throw Error("[Lume.js] repeat(): options.key must be a function");const{templateRoot:E,keepEl:j}=a(h,$);if(E&&"function"==typeof i&&e("[Lume.js] repeat(): options.render is ignored when options.template is set — use create/update instead"),!E&&"function"!=typeof i&&"function"!=typeof l)throw Error("[Lume.js] repeat(): options.render or options.create must be a function");const S=new Map,k=new Map,L=new Map,v=new Map,A=new Map,N=new Set;function O(){return E?E.cloneNode(!0):"function"==typeof b?b():document.createElement(b)}function C(){let e=$.firstChild;for(;e;){const t=e.nextSibling;e!==j&&$.removeChild(e),e=t}}function F(){const o=n[r];if(!Array.isArray(o))return void e(`[Lume.js] repeat(): store.${r} is not an array`);let c=!1;if(w&&S.size===o.length){c=!0;for(let e=0;e<o.length;e++)if(!S.has(s(o[e]))){c=!1;break}}N.clear();const a=[];for(let n=0;n<o.length;n++){const r=o[n],c=s(r);if(N.has(c)){e(`[Lume.js] repeat(): duplicate key "${c}"`);continue}N.add(c);let f=S.get(c);const d=!f;d&&(f=O(),S.set(c,f),E&&A.set(c,p(f)));try{if(d&&l){const e=l(r,f,n);"function"==typeof e&&v.set(c,e)}const e=k.get(c),t=L.get(c);E?e===r&&t===n||(g(A.get(c),r,n),u&&u(r,f,n,{isFirstRender:d})):u?e===r&&t===n||u(r,f,n,{isFirstRender:d}):i&&i(r,f,n),k.set(c,r),L.set(c,n)}catch(e){t(`[Lume.js] repeat(): error rendering key "${c}":`,e)}a.push(f)}!function(e,o,n){const r=document.body.contains(e),c=r&&m?m(e):null,s=r&&w?w(e,{isReorder:n}):null;(()=>{if(function(e,t){let o=e.firstChild;for(let n=0;n<t.length;n++){j&&o===j&&(o=o.nextSibling);const r=t[n];o!==r?e.insertBefore(r,o):o=o.nextSibling}for(;o;){const t=o.nextSibling;o!==j&&e.removeChild(o),o=t}}($,a),S.size!==N.size)for(const e of S.keys())if(!N.has(e)){const o=S.get(e),n=k.get(e),r=v.get(e);if("function"==typeof r)try{r()}catch(o){t(`[Lume.js] repeat(): cleanup error for key "${e}":`,o)}"function"==typeof f&&o&&f(n,o),S.delete(e),k.delete(e),L.delete(e),v.delete(e),A.delete(e)}})(),c&&c(),s&&s()}($,0,c)}let T;if("function"==typeof n.$subscribe)T=n.$subscribe(r,F);else{if("function"!=typeof n.subscribe)return F(),e("[Lume.js] repeat(): store is not reactive (no $subscribe or subscribe method)"),()=>{for(const[e,o]of S){const n=k.get(e),r=v.get(e);if("function"==typeof r)try{r()}catch(o){t(`[Lume.js] repeat(): cleanup error for key "${e}":`,o)}"function"==typeof f&&f(n,o)}C(),S.clear(),k.clear(),L.clear(),v.clear(),A.clear(),N.clear()};{const e=n.subscribe(()=>F());F(),T="function"==typeof e?e:()=>{e?.unsubscribe?.()}}}return()=>{"function"==typeof T&&T();for(const[e,o]of S){const n=k.get(e),r=v.get(e);if("function"==typeof r)try{r()}catch(o){t(`[Lume.js] repeat(): cleanup error for key "${e}":`,o)}"function"==typeof f&&f(n,o)}C(),S.clear(),k.clear(),L.clear(),v.clear(),A.clear(),N.clear()}}let b=!0,m=null;const w=new Map;function $(e){return null===m||("string"==typeof m?e.includes(m):!(m instanceof RegExp)||m.test(e))}function E(e){return w.has(e)||w.set(e,{gets:new Map,sets:new Map,notifies:new Map}),w.get(e)}function j(e,t,o){const n=E(e)[t];n.set(o,(n.get(o)||0)+1)}const S=100,k=97;function L(e){try{const t=JSON.stringify(e);return t.length>S?t.slice(0,k)+"...":t}catch{return e+""}}function v(e={}){const t=e.label??"store",o=(t,o)=>{const n=e[t];return void 0!==n?n:o};return{name:"debug:"+t,onInit:()=>{b&&console.log(`%c[${t}]%c initialized`,"color: #888; font-weight: bold","color: inherit")},onGet:(e,n)=>("string"==typeof e&&e.startsWith("$")||(j(t,"gets",e),b&&o("logGet",!1)&&$(e)&&console.log(`%c[${t}]%c GET %c${e}%c = ${L(n)}`,"color: #888; font-weight: bold","color: #4CAF50","color: #2196F3; font-weight: bold","color: inherit")),n),onSet:(e,n,r)=>("string"==typeof e&&e.startsWith("$")||(j(t,"sets",e),b&&o("logSet",!0)&&$(e)&&(console.log(`%c[${t}]%c SET %c${e}%c: ${L(r)} → ${L(n)}`,"color: #888; font-weight: bold","color: #FF9800","color: #2196F3; font-weight: bold","color: inherit"),o("trace",!1)&&console.trace(`%c[${t}] Stack trace for ${e}`,"color: #888"))),n),onSubscribe:e=>{b&&$(e)&&console.log(`%c[${t}]%c SUBSCRIBE %c${e}`,"color: #888; font-weight: bold","color: #9C27B0","color: #2196F3; font-weight: bold")},onNotify:(e,n)=>{"string"==typeof e&&e.startsWith("$")||(j(t,"notifies",e),b&&o("logNotify",!0)&&$(e)&&console.log(`%c[${t}]%c NOTIFY %c${e}%c = ${L(n)}`,"color: #888; font-weight: bold","color: #E91E63","color: #2196F3; font-weight: bold","color: inherit"))}}}const A={enable(){b=!0,console.log("%c[lume-debug]%c Logging enabled","color: #888; font-weight: bold","color: #4CAF50")},disable(){b=!1,console.log("%c[lume-debug]%c Logging disabled","color: #888; font-weight: bold","color: #F44336")},isEnabled:()=>b,filter(e){m=e,console.log(null===e?"%c[lume-debug]%c Filter cleared":"%c[lume-debug]%c Filter set: "+e,"color: #888; font-weight: bold","color: inherit")},getFilter:()=>m,stats(){const e={};for(const[t,o]of w)e[t]={gets:Object.fromEntries(o.gets),sets:Object.fromEntries(o.sets),notifies:Object.fromEntries(o.notifies)};return e},logStats(){const e=this.stats();if(0===Object.keys(e).length)return console.log("%c[lume-debug]%c No stats collected yet","color: #888; font-weight: bold","color: inherit"),e;console.group("%c[lume-debug] Statistics","color: #888; font-weight: bold");for(const[t,o]of Object.entries(e)){console.group("%c"+t,"color: #2196F3; font-weight: bold");const e=[],n=new Set([...Object.keys(o.gets),...Object.keys(o.sets),...Object.keys(o.notifies)]);for(const t of n)e.push({key:t,gets:o.gets[t]||0,sets:o.sets[t]||0,notifies:o.notifies[t]||0});e.length>0&&console.table(e),console.groupEnd()}return console.groupEnd(),e},resetStats(){w.clear(),console.log("%c[lume-debug]%c Stats reset","color: #888; font-weight: bold","color: inherit")}};function N(e,o=[]){if(!o.length)return e;for(const e of o){try{e.onInit?.()}catch(o){t(`[Lume.js] Plugin "${e.name}" error in onInit:`,o)}Object.freeze(e)}const n=new Map;let r;return"function"==typeof e.$beforeFlush&&(r=e.$beforeFlush(function(){for(const[e,r]of n)for(const n of o)try{n.onNotify?.(e,r)}catch(e){t(`[Lume.js] Plugin "${n.name}" error in onNotify:`,e)}n.clear()})),new Proxy(e,{get(e,c){if("$dispose"===c)return()=>{r&&r(),n.clear()};if("string"==typeof c&&c.startsWith("$")){const n=e[c];return"$subscribe"===c&&"function"==typeof n?(e,r)=>{for(const n of o)try{n.onSubscribe?.(e)}catch(e){t(`[Lume.js] Plugin "${n.name}" error in onSubscribe:`,e)}return n(e,r)}:n}let s=e[c];for(const e of o)try{const t=e.onGet?.(c,s);void 0!==t&&(s=t)}catch(o){t(`[Lume.js] Plugin "${e.name}" error in onGet:`,o)}return s},set(e,r,c){const s=e[r];let i=c;for(const e of o)try{const t=e.onSet?.(r,i,s);void 0!==t&&(i=t)}catch(o){t(`[Lume.js] Plugin "${e.name}" error in onSet:`,o)}return Object.is(i,s)||n.set(r,i),e[r]=i,!0}})}function O(){const e=[];return{add(t){"function"==typeof t&&e.push(t)},dispose(){for(;e.length;){const t=e.pop();try{t()}catch(e){}}}}}function C(e="#__LUME_DATA__",t){const o="undefined"!=typeof document?document.querySelector(e):null;if(!o)return{};if("SCRIPT"!==o.tagName||"application/json"!==o.type)return{};let n;try{n=JSON.parse(o.textContent)}catch{return{}}return"function"!=typeof t||t(n)?n:{}}function F(t,o){try{const e=t.getItem(o);if(!e)return null;const n=JSON.parse(e);return n&&"object"==typeof n&&!Array.isArray(n)?n:null}catch{return e(`[Lume.js] persist(): could not read "${o}" — starting fresh`),null}}function T(e,t){const o={};for(const n of t)o[n]=e[n];return JSON.stringify(o)}const x=new WeakMap;function M(){try{return"undefined"!=typeof localStorage?localStorage:void 0}catch{return}}function R(t,o,n={}){if(!t||"function"!=typeof t.$subscribe)throw Error("[Lume.js] persist() requires a reactive store from state()");if("string"!=typeof o||0===o.length)throw Error("[Lume.js] persist() requires a non-empty storage key");const r=void 0!==n.storage?n.storage:M();if(!r||"function"!=typeof r.getItem)return e("[Lume.js] persist(): no storage available — persistence disabled"),()=>{};const c=Array.isArray(n.keys)?n.keys.slice():Object.keys(t).filter(e=>!e.startsWith("$"));let s=x.get(r);s||(s=new Set,x.set(r,s));const i=!s.has(o);i?s.add(o):e(`[Lume.js] persist(): "${o}" is already managed by another persist() on this storage — instances will overwrite each other's data. Use a distinct key per store.`);const l=F(r,o);if(l)for(const e of c)Object.prototype.hasOwnProperty.call(l,e)&&(t[e]=l[e]);let u=null;try{u=T(t,c)}catch{}let f=!1,a=!1;const p=()=>{if(f=!1,a)return;let n;try{n=T(t,c)}catch(t){return void e("[Lume.js] persist(): state not serializable — skipping save",t)}if(n!==u)try{r.setItem(o,n),u=n}catch(t){e("[Lume.js] persist(): could not write — storage full or unavailable?",t)}},g=c.map(e=>{let o=!0;return t.$subscribe(e,()=>{o?o=!1:f||(f=!0,queueMicrotask(p))})});return()=>{for(a=!0,i&&s.delete(o);g.length;)g.pop()()}}function q(e){return!(!e||"object"!=typeof e||!(n in e)&&"function"!=typeof e.$subscribe)}export{i as computed,O as createCleanupGroup,v as createDebugPlugin,A as debug,d as defaultFocusPreservation,y as defaultScrollPreservation,C as hydrateState,q as isReactive,R as persist,h as repeat,l as watch,N as withPlugins};