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.
- package/AGENT_GUIDE.md +224 -0
- package/README.md +97 -16
- package/dist/addons.min.mjs +1 -1
- package/dist/addons.mjs +190 -7
- package/dist/addons.mjs.map +1 -1
- package/dist/handlers.min.mjs +1 -1
- package/dist/handlers.mjs +29 -2
- package/dist/handlers.mjs.map +1 -1
- package/dist/index.min.mjs +1 -1
- package/dist/index.mjs +4 -141
- package/dist/index.mjs.map +1 -1
- package/dist/lume.global.js +1 -1
- package/dist/lume.global.js.map +1 -1
- package/dist/shared-BGg9PbiG.mjs +249 -0
- package/dist/shared-BGg9PbiG.mjs.map +1 -0
- package/dist/shared-DmpHYKx7.mjs +15 -0
- package/dist/shared-DmpHYKx7.mjs.map +1 -0
- package/dist/shared-SUXdsYBx.mjs +233 -0
- package/dist/shared-SUXdsYBx.mjs.map +1 -0
- package/dist/state.min.mjs +1 -0
- package/dist/state.mjs +7 -0
- package/dist/state.mjs.map +1 -0
- package/llms-full.txt +6999 -0
- package/llms.txt +89 -0
- package/package.json +11 -2
- package/src/addons/index.d.ts +99 -7
- package/src/addons/index.js +13 -2
- package/src/addons/persist.js +190 -0
- package/src/addons/repeat.js +159 -10
- package/src/core/batch.js +139 -0
- package/src/core/bindDom.js +7 -3
- package/src/core/effect.js +34 -5
- package/src/core/state.js +118 -73
- package/src/handlers/index.d.ts +24 -0
- package/src/handlers/index.js +1 -0
- package/src/handlers/on.js +60 -0
- package/src/handlers/stringAttr.js +9 -2
- package/src/index.d.ts +14 -200
- package/src/index.js +3 -1
- package/src/state.d.ts +252 -0
- package/src/state.js +25 -0
- package/dist/shared-x2HJmEyO.mjs +0 -260
- 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 · No build step · No framework lock-in
|
|
7
|
+
No custom syntax · No build step · No framework lock-in.<br>
|
|
8
|
+
<strong>1.46 KB universal core</strong> · <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
|
|
|
12
|
-
<a href="package.json"><img src="https://img.shields.io/badge/version-2.
|
|
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
|
|
|
14
|
-
<a href="tests/"><img src="https://img.shields.io/badge/tests-
|
|
15
|
+
<a href="tests/"><img src="https://img.shields.io/badge/tests-439%20passing-brightgreen.svg" alt="439 tests"></a>
|
|
15
16
|
|
|
16
|
-
<a href="scripts/check-size.js"><img src="https://img.shields.io/badge/
|
|
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
|
+
|
|
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 |
|
|
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 |
|
|
62
|
-
| Firefox |
|
|
63
|
-
| Safari |
|
|
64
|
-
| Edge |
|
|
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
|
|
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
|
|
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
|
-
|
|
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/
|
|
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
|
package/dist/addons.min.mjs
CHANGED
|
@@ -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};
|