lume-js 2.3.0 → 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 +34 -13
- package/dist/addons.min.mjs +1 -1
- package/dist/addons.mjs +48 -10
- package/dist/addons.mjs.map +1 -1
- package/dist/handlers.min.mjs +1 -1
- package/dist/handlers.mjs +3 -2
- package/dist/handlers.mjs.map +1 -1
- package/dist/index.min.mjs +1 -1
- package/dist/index.mjs +2 -2
- package/dist/lume.global.js +1 -1
- package/dist/lume.global.js.map +1 -1
- package/dist/{shared-DNe4ez8V.mjs → shared-BGg9PbiG.mjs} +2 -2
- package/dist/{shared-DNe4ez8V.mjs.map → shared-BGg9PbiG.mjs.map} +1 -1
- package/dist/{shared-Bk_gndPJ.mjs → shared-SUXdsYBx.mjs} +4 -3
- package/dist/shared-SUXdsYBx.mjs.map +1 -0
- package/dist/state.min.mjs +1 -1
- package/dist/state.mjs +1 -1
- package/llms-full.txt +6999 -0
- package/llms.txt +89 -0
- package/package.json +7 -2
- package/src/addons/index.d.ts +29 -5
- package/src/addons/persist.js +40 -2
- package/src/addons/repeat.js +43 -9
- package/src/core/state.js +10 -2
- package/src/handlers/stringAttr.js +9 -2
- package/src/index.d.ts +14 -240
- package/src/state.d.ts +246 -11
- package/dist/shared-Bk_gndPJ.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
|
@@ -5,16 +5,16 @@
|
|
|
5
5
|
<p>
|
|
6
6
|
Minimal reactive state management using only standard JavaScript and HTML.<br>
|
|
7
7
|
No custom syntax · No build step · No framework lock-in.<br>
|
|
8
|
-
<strong>1.
|
|
8
|
+
<strong>1.46 KB universal core</strong> · <strong>2.66 KB with DOM</strong>
|
|
9
9
|
</p>
|
|
10
10
|
<p>
|
|
11
11
|
<a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-blue.svg" alt="License: MIT"></a>
|
|
12
12
|
|
|
13
|
-
<a href="package.json"><img src="https://img.shields.io/badge/version-2.3.
|
|
13
|
+
<a href="package.json"><img src="https://img.shields.io/badge/version-2.3.1-orange.svg" alt="v2.3.1"></a>
|
|
14
14
|
|
|
15
|
-
<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>
|
|
16
16
|
|
|
17
|
-
<a href="scripts/check-size.js"><img src="https://img.shields.io/badge/universal%20core-1.
|
|
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
18
|
|
|
19
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>
|
|
20
20
|
</p>
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
|---------|---------|-----------|-----|-------|
|
|
31
31
|
| Custom Syntax | ❌ No | ✅ `x-data` | ✅ `v-bind` | ✅ JSX |
|
|
32
32
|
| Build Step | ❌ Optional | ❌ Optional | ⚠️ Recommended | ✅ Required |
|
|
33
|
-
| Bundle Size | 1.
|
|
33
|
+
| Bundle Size | 1.46–2.66KB | ~15KB | ~35KB | ~45KB |
|
|
34
34
|
| HTML Validation | ✅ Pass | ⚠️ Warnings | ⚠️ Warnings | ❌ JSX |
|
|
35
35
|
| Extensible Handlers | ✅ | ❌ Built-in only | ❌ Built-in only | N/A |
|
|
36
36
|
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
|
|
45
45
|
| Entry | Size (gz) | Contents | For |
|
|
46
46
|
|-------|-----------|----------|-----|
|
|
47
|
-
| `lume-js/state` | **1.
|
|
47
|
+
| `lume-js/state` | **1.46 KB** | `state`, `batch`, `withReadObserver` | Node, Deno, Bun, workers, CLI — anywhere without a DOM |
|
|
48
48
|
| `lume-js` | **2.66 KB** | + `bindDom`, `effect` | Browsers |
|
|
49
49
|
| `lume-js/addons` | pay per import | `computed`, `watch`, `repeat`, `persist`, … | Optional patterns |
|
|
50
50
|
| `lume-js/handlers` | pay per import | `show`, `classToggle`, `on`, … | Extra reactive attributes |
|
|
@@ -73,7 +73,7 @@ npm install lume-js
|
|
|
73
73
|
|
|
74
74
|
```javascript
|
|
75
75
|
import { state, bindDom } from 'lume-js'; // browser: full core
|
|
76
|
-
import { state, batch } from 'lume-js/state'; // Node/CLI/workers: 1.
|
|
76
|
+
import { state, batch } from 'lume-js/state'; // Node/CLI/workers: 1.46 KB kernel
|
|
77
77
|
```
|
|
78
78
|
|
|
79
79
|
> **→ Using Lume without a DOM?** See the [Universal core guide](docs/guides/universal-core.md).
|
|
@@ -83,13 +83,13 @@ import { state, batch } from 'lume-js/state'; // Node/CLI/workers: 1.45 KB ke
|
|
|
83
83
|
|
|
84
84
|
| Browser | Minimum version |
|
|
85
85
|
|---------|-----------------|
|
|
86
|
-
| Chrome |
|
|
87
|
-
| Firefox |
|
|
88
|
-
| Safari |
|
|
89
|
-
| Edge |
|
|
86
|
+
| Chrome | 80+ |
|
|
87
|
+
| Firefox | 74+ |
|
|
88
|
+
| Safari | 13.1+ |
|
|
89
|
+
| Edge | 80+ |
|
|
90
90
|
| IE11 | ❌ Not supported |
|
|
91
91
|
|
|
92
|
-
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`.
|
|
93
93
|
|
|
94
94
|
---
|
|
95
95
|
|
|
@@ -248,7 +248,7 @@ import { computed, watch, repeat } from 'lume-js/addons';
|
|
|
248
248
|
| `repeat(container, store, key, opts)` | Render a keyed list with element reuse — incl. declarative `template:` mode bound straight from a `<template>` element |
|
|
249
249
|
| `persist(store, key, opts)` | Sync selected keys with localStorage/sessionStorage — hydrate on call, auto-save on change |
|
|
250
250
|
| `createCleanupGroup()` | Collect multiple cleanup/unsubscribe functions and dispose them all at once |
|
|
251
|
-
| `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 |
|
|
252
252
|
|
|
253
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.
|
|
254
254
|
|
|
@@ -286,6 +286,27 @@ watch(store, 'count', (val) => {
|
|
|
286
286
|
|
|
287
287
|
---
|
|
288
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
|
+
|
|
289
310
|
## Documentation
|
|
290
311
|
|
|
291
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:
|
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,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.content.firstElementChild}function a(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 p(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 g(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 d(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 y(o,n,r,c){const{key:s,render:i,create:l,update:u,remove:y,template:h=null,element:b="div",preserveFocus:m=g,preserveScroll:$=d}=c,w="string"==typeof o?document.querySelector(o):o;if(!w)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 j=h?f(h,w):null;if(j&&"function"==typeof i&&e("[Lume.js] repeat(): options.render is ignored when options.template is set — use create/update instead"),!j&&"function"!=typeof i&&"function"!=typeof l)throw Error("[Lume.js] repeat(): options.render or options.create must be a function");const E=new Map,S=new Map,k=new Map,L=new Map,v=new Map,A=new Set;function N(){return j?j.cloneNode(!0):"function"==typeof b?b():document.createElement(b)}function O(){const o=n[r];if(!Array.isArray(o))return void e(`[Lume.js] repeat(): store.${r} is not an array`);let c=!1;if($&&E.size===o.length){c=!0;for(let e=0;e<o.length;e++)if(!E.has(s(o[e]))){c=!1;break}}A.clear();const f=[];for(let n=0;n<o.length;n++){const r=o[n],c=s(r);if(A.has(c)){e(`[Lume.js] repeat(): duplicate key "${c}"`);continue}A.add(c);let g=E.get(c);const d=!g;d&&(g=N(),E.set(c,g),j&&v.set(c,a(g)));try{if(d&&l){const e=l(r,g,n);"function"==typeof e&&L.set(c,e)}const e=S.get(c),t=k.get(c);j?e===r&&t===n||(p(v.get(c),r,n),u&&u(r,g,n,{isFirstRender:d})):u?e===r&&t===n||u(r,g,n,{isFirstRender:d}):i&&i(r,g,n),S.set(c,r),k.set(c,n)}catch(e){t(`[Lume.js] repeat(): error rendering key "${c}":`,e)}f.push(g)}!function(e,o,n){const r=document.body.contains(e),c=r&&m?m(e):null,s=r&&$?$(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}}(w,f),E.size!==A.size)for(const e of E.keys())if(!A.has(e)){const o=E.get(e),n=S.get(e),r=L.get(e);if("function"==typeof r)try{r()}catch(o){t(`[Lume.js] repeat(): cleanup error for key "${e}":`,o)}"function"==typeof y&&o&&y(n,o),E.delete(e),S.delete(e),k.delete(e),L.delete(e),v.delete(e)}})(),c&&c(),s&&s()}(w,0,c)}let T;if("function"==typeof n.$subscribe)T=n.$subscribe(r,O);else{if("function"!=typeof n.subscribe)return O(),e("[Lume.js] repeat(): store is not reactive (no $subscribe or subscribe method)"),()=>{for(const[e,o]of E){const n=S.get(e),r=L.get(e);if("function"==typeof r)try{r()}catch(o){t(`[Lume.js] repeat(): cleanup error for key "${e}":`,o)}"function"==typeof y&&y(n,o)}w.replaceChildren(),E.clear(),S.clear(),k.clear(),L.clear(),v.clear(),A.clear()};{const e=n.subscribe(()=>O());O(),T="function"==typeof e?e:()=>{e?.unsubscribe?.()}}}return()=>{"function"==typeof T&&T();for(const[e,o]of E){const n=S.get(e),r=L.get(e);if("function"==typeof r)try{r()}catch(o){t(`[Lume.js] repeat(): cleanup error for key "${e}":`,o)}"function"==typeof y&&y(n,o)}w.replaceChildren(),E.clear(),S.clear(),k.clear(),L.clear(),v.clear(),A.clear()}}let h=!0,b=null;const m=new Map;function $(e){return null===b||("string"==typeof b?e.includes(b):!(b instanceof RegExp)||b.test(e))}function w(e){return m.has(e)||m.set(e,{gets:new Map,sets:new Map,notifies:new Map}),m.get(e)}function j(e,t,o){const n=w(e)[t];n.set(o,(n.get(o)||0)+1)}const E=100,S=97;function k(e){try{const t=JSON.stringify(e);return t.length>E?t.slice(0,S)+"...":t}catch{return e+""}}function L(e={}){const t=e.label??"store",o=(t,o)=>{const n=e[t];return void 0!==n?n:o};return{name:"debug:"+t,onInit:()=>{h&&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),h&&o("logGet",!1)&&$(e)&&console.log(`%c[${t}]%c GET %c${e}%c = ${k(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),h&&o("logSet",!0)&&$(e)&&(console.log(`%c[${t}]%c SET %c${e}%c: ${k(r)} → ${k(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=>{h&&$(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),h&&o("logNotify",!0)&&$(e)&&console.log(`%c[${t}]%c NOTIFY %c${e}%c = ${k(n)}`,"color: #888; font-weight: bold","color: #E91E63","color: #2196F3; font-weight: bold","color: inherit"))}}}const v={enable(){h=!0,console.log("%c[lume-debug]%c Logging enabled","color: #888; font-weight: bold","color: #4CAF50")},disable(){h=!1,console.log("%c[lume-debug]%c Logging disabled","color: #888; font-weight: bold","color: #F44336")},isEnabled:()=>h,filter(e){b=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:()=>b,stats(){const e={};for(const[t,o]of m)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(){m.clear(),console.log("%c[lume-debug]%c Stats reset","color: #888; font-weight: bold","color: inherit")}};function A(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 N(){const e=[];return{add(t){"function"==typeof t&&e.push(t)},dispose(){for(;e.length;){const t=e.pop();try{t()}catch(e){}}}}}function O(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 T(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 C(e,t){const o={};for(const n of t)o[n]=e[n];return JSON.stringify(o)}function F(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:globalThis.localStorage;if(!r||"function"!=typeof r.getItem)return e("[Lume.js] persist(): no storage available — persistence disabled"),()=>{};const c=Array.isArray(n.keys)&&n.keys.length>0?n.keys.slice():Object.keys(t).filter(e=>!e.startsWith("$")),s=T(r,o);if(s)for(const e of c)Object.prototype.hasOwnProperty.call(s,e)&&(t[e]=s[e]);let i=null;try{i=C(t,c)}catch{}let l=!1,u=!1;const f=()=>{if(l=!1,u)return;let n;try{n=C(t,c)}catch(t){return void e("[Lume.js] persist(): state not serializable — skipping save",t)}if(n!==i)try{r.setItem(o,n),i=n}catch(t){e("[Lume.js] persist(): could not write — storage full or unavailable?",t)}},a=c.map(e=>{let o=!0;return t.$subscribe(e,()=>{o?o=!1:l||(l=!0,queueMicrotask(f))})});return()=>{for(u=!0;a.length;)a.pop()()}}function M(e){return!(!e||"object"!=typeof e||!(n in e)&&"function"!=typeof e.$subscribe)}export{i as computed,N as createCleanupGroup,L as createDebugPlugin,v as debug,g as defaultFocusPreservation,d as defaultScrollPreservation,O as hydrateState,M as isReactive,F as persist,y as repeat,l as watch,A 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};
|
package/dist/addons.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { R as REACTIVE_BRAND } from "./shared-
|
|
2
|
-
import { e as effect, a as applyBindValue } from "./shared-
|
|
1
|
+
import { R as REACTIVE_BRAND } from "./shared-SUXdsYBx.mjs";
|
|
2
|
+
import { e as effect, a as applyBindValue } from "./shared-BGg9PbiG.mjs";
|
|
3
3
|
import { l as logError, a as logWarn } from "./shared-DmpHYKx7.mjs";
|
|
4
4
|
function computed(fn) {
|
|
5
5
|
if (typeof fn !== "function") {
|
|
@@ -94,7 +94,7 @@ function watch(store, key, callback, { immediate = true } = {}) {
|
|
|
94
94
|
}
|
|
95
95
|
return store.$subscribe(key, callback);
|
|
96
96
|
}
|
|
97
|
-
function
|
|
97
|
+
function resolveTemplateEl(template, containerEl) {
|
|
98
98
|
let templateEl = template;
|
|
99
99
|
if (template === true) {
|
|
100
100
|
templateEl = containerEl.querySelector("template");
|
|
@@ -107,7 +107,14 @@ function resolveTemplateRoot(template, containerEl) {
|
|
|
107
107
|
if (templateEl.content.children.length !== 1) {
|
|
108
108
|
throw new Error("[Lume.js] repeat(): template must contain exactly one root element");
|
|
109
109
|
}
|
|
110
|
-
return templateEl
|
|
110
|
+
return templateEl;
|
|
111
|
+
}
|
|
112
|
+
function resolveTemplate(template, containerEl) {
|
|
113
|
+
if (!template) return { templateRoot: null, keepEl: null };
|
|
114
|
+
const templateEl = resolveTemplateEl(template, containerEl);
|
|
115
|
+
let keepEl = templateEl;
|
|
116
|
+
while (keepEl && keepEl.parentNode !== containerEl) keepEl = keepEl.parentNode;
|
|
117
|
+
return { templateRoot: templateEl.content.firstElementChild, keepEl };
|
|
111
118
|
}
|
|
112
119
|
function collectItemBindings(el) {
|
|
113
120
|
const bindings = [];
|
|
@@ -208,7 +215,7 @@ function repeat(container, store, arrayKey, options) {
|
|
|
208
215
|
if (typeof key !== "function") {
|
|
209
216
|
throw new Error("[Lume.js] repeat(): options.key must be a function");
|
|
210
217
|
}
|
|
211
|
-
const templateRoot
|
|
218
|
+
const { templateRoot, keepEl } = resolveTemplate(template, containerEl);
|
|
212
219
|
if (templateRoot && typeof render === "function") {
|
|
213
220
|
logWarn("[Lume.js] repeat(): options.render is ignored when options.template is set — use create/update instead");
|
|
214
221
|
}
|
|
@@ -225,9 +232,18 @@ function repeat(container, store, arrayKey, options) {
|
|
|
225
232
|
if (templateRoot) return templateRoot.cloneNode(true);
|
|
226
233
|
return typeof element === "function" ? element() : document.createElement(element);
|
|
227
234
|
}
|
|
235
|
+
function clearContainer() {
|
|
236
|
+
let node = containerEl.firstChild;
|
|
237
|
+
while (node) {
|
|
238
|
+
const next = node.nextSibling;
|
|
239
|
+
if (node !== keepEl) containerEl.removeChild(node);
|
|
240
|
+
node = next;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
228
243
|
function reconcileDOM(container2, nextEls) {
|
|
229
244
|
let ptr = container2.firstChild;
|
|
230
245
|
for (let i = 0; i < nextEls.length; i++) {
|
|
246
|
+
if (keepEl && ptr === keepEl) ptr = ptr.nextSibling;
|
|
231
247
|
const desired = nextEls[i];
|
|
232
248
|
if (ptr === desired) {
|
|
233
249
|
ptr = ptr.nextSibling;
|
|
@@ -237,7 +253,7 @@ function repeat(container, store, arrayKey, options) {
|
|
|
237
253
|
}
|
|
238
254
|
while (ptr) {
|
|
239
255
|
const next = ptr.nextSibling;
|
|
240
|
-
container2.removeChild(ptr);
|
|
256
|
+
if (ptr !== keepEl) container2.removeChild(ptr);
|
|
241
257
|
ptr = next;
|
|
242
258
|
}
|
|
243
259
|
}
|
|
@@ -367,7 +383,7 @@ function repeat(container, store, arrayKey, options) {
|
|
|
367
383
|
remove(prevItem, el);
|
|
368
384
|
}
|
|
369
385
|
}
|
|
370
|
-
|
|
386
|
+
clearContainer();
|
|
371
387
|
elementsByKey.clear();
|
|
372
388
|
prevItemsByKey.clear();
|
|
373
389
|
prevIndexByKey.clear();
|
|
@@ -394,7 +410,7 @@ function repeat(container, store, arrayKey, options) {
|
|
|
394
410
|
remove(prevItem, el);
|
|
395
411
|
}
|
|
396
412
|
}
|
|
397
|
-
|
|
413
|
+
clearContainer();
|
|
398
414
|
elementsByKey.clear();
|
|
399
415
|
prevItemsByKey.clear();
|
|
400
416
|
prevIndexByKey.clear();
|
|
@@ -759,6 +775,14 @@ function serializeKeys(store, watched) {
|
|
|
759
775
|
for (const k of watched) out[k] = store[k];
|
|
760
776
|
return JSON.stringify(out);
|
|
761
777
|
}
|
|
778
|
+
const activeEntries = /* @__PURE__ */ new WeakMap();
|
|
779
|
+
function defaultStorage() {
|
|
780
|
+
try {
|
|
781
|
+
return typeof localStorage !== "undefined" ? localStorage : void 0;
|
|
782
|
+
} catch {
|
|
783
|
+
return void 0;
|
|
784
|
+
}
|
|
785
|
+
}
|
|
762
786
|
function persist(store, storageKey, options = {}) {
|
|
763
787
|
if (!store || typeof store.$subscribe !== "function") {
|
|
764
788
|
throw new Error("[Lume.js] persist() requires a reactive store from state()");
|
|
@@ -766,13 +790,26 @@ function persist(store, storageKey, options = {}) {
|
|
|
766
790
|
if (typeof storageKey !== "string" || storageKey.length === 0) {
|
|
767
791
|
throw new Error("[Lume.js] persist() requires a non-empty storage key");
|
|
768
792
|
}
|
|
769
|
-
const storage = options.storage !== void 0 ? options.storage :
|
|
793
|
+
const storage = options.storage !== void 0 ? options.storage : defaultStorage();
|
|
770
794
|
if (!storage || typeof storage.getItem !== "function") {
|
|
771
795
|
logWarn("[Lume.js] persist(): no storage available — persistence disabled");
|
|
772
796
|
return () => {
|
|
773
797
|
};
|
|
774
798
|
}
|
|
775
|
-
const watched = Array.isArray(options.keys)
|
|
799
|
+
const watched = Array.isArray(options.keys) ? options.keys.slice() : Object.keys(store).filter((k) => !k.startsWith("$"));
|
|
800
|
+
let entrySet = activeEntries.get(storage);
|
|
801
|
+
if (!entrySet) {
|
|
802
|
+
entrySet = /* @__PURE__ */ new Set();
|
|
803
|
+
activeEntries.set(storage, entrySet);
|
|
804
|
+
}
|
|
805
|
+
const ownsEntry = !entrySet.has(storageKey);
|
|
806
|
+
if (ownsEntry) {
|
|
807
|
+
entrySet.add(storageKey);
|
|
808
|
+
} else {
|
|
809
|
+
logWarn(
|
|
810
|
+
`[Lume.js] persist(): "${storageKey}" is already managed by another persist() on this storage — instances will overwrite each other's data. Use a distinct key per store.`
|
|
811
|
+
);
|
|
812
|
+
}
|
|
776
813
|
const stored = readStored(storage, storageKey);
|
|
777
814
|
if (stored) {
|
|
778
815
|
for (const k of watched) {
|
|
@@ -823,6 +860,7 @@ function persist(store, storageKey, options = {}) {
|
|
|
823
860
|
});
|
|
824
861
|
return () => {
|
|
825
862
|
disposed = true;
|
|
863
|
+
if (ownsEntry) entrySet.delete(storageKey);
|
|
826
864
|
while (unsubs.length) unsubs.pop()();
|
|
827
865
|
};
|
|
828
866
|
}
|