lume-js 2.2.1 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,18 +4,22 @@
4
4
  <p><strong>Reactivity that follows web standards.</strong></p>
5
5
  <p>
6
6
  Minimal reactive state management using only standard JavaScript and HTML.<br>
7
- No custom syntax &nbsp;·&nbsp; No build step &nbsp;·&nbsp; No framework lock-in.
7
+ No custom syntax &nbsp;·&nbsp; No build step &nbsp;·&nbsp; No framework lock-in.<br>
8
+ <strong>1.45 KB universal core</strong> &nbsp;·&nbsp; <strong>2.66 KB with DOM</strong>
8
9
  </p>
9
10
  <p>
10
11
  <a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-blue.svg" alt="License: MIT"></a>
11
12
  &nbsp;
12
- <a href="package.json"><img src="https://img.shields.io/badge/version-2.2.1-orange.svg" alt="v2.2.1"></a>
13
+ <a href="package.json"><img src="https://img.shields.io/badge/version-2.3.0-orange.svg" alt="v2.3.0"></a>
13
14
  &nbsp;
14
- <a href="tests/"><img src="https://img.shields.io/badge/tests-355%20passing-brightgreen.svg" alt="355 tests"></a>
15
+ <a href="tests/"><img src="https://img.shields.io/badge/tests-425%20passing-brightgreen.svg" alt="425 tests"></a>
15
16
  &nbsp;
16
- <a href="scripts/check-size.js"><img src="https://img.shields.io/badge/core-2.23KB%20gzipped-blue.svg" alt="2.23KB"></a>
17
+ <a href="scripts/check-size.js"><img src="https://img.shields.io/badge/universal%20core-1.45KB-blue.svg" alt="universal core 1.45KB"></a>
18
+ &nbsp;
19
+ <a href="scripts/check-size.js"><img src="https://img.shields.io/badge/core%20%2B%20DOM-2.66KB-blue.svg" alt="core + DOM 2.66KB"></a>
17
20
  </p>
18
21
  <p><code>npm install lume-js</code></p>
22
+ <p><a href="https://sathvikc.github.io/lume-js/"><strong>Docs & live examples →</strong></a></p>
19
23
  </div>
20
24
 
21
25
  ---
@@ -26,7 +30,7 @@
26
30
  |---------|---------|-----------|-----|-------|
27
31
  | Custom Syntax | ❌ No | ✅ `x-data` | ✅ `v-bind` | ✅ JSX |
28
32
  | Build Step | ❌ Optional | ❌ Optional | ⚠️ Recommended | ✅ Required |
29
- | Bundle Size | ~2.23KB | ~15KB | ~35KB | ~45KB |
33
+ | Bundle Size | 1.45–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.45 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,9 +72,13 @@ 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.45 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 |
@@ -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,7 +245,8 @@ import { computed, watch, repeat } from 'lume-js/addons';
188
245
  | `effect(fn)` *(core)* | Write derived values back into the store, or trigger side effects on state change |
189
246
  | `computed(fn)` | Derive a read-only value from state to consume *outside* the store (templates, display logic) |
190
247
  | `watch(store, key, fn)` | React to a *specific* key changing — DOM updates, analytics, syncing external state |
191
- | `repeat(container, store, key, opts)` | Render a keyed list with element reuse (no full re-render on change) |
248
+ | `repeat(container, store, key, opts)` | Render a keyed list with element reuse incl. declarative `template:` mode bound straight from a `<template>` element |
249
+ | `persist(store, key, opts)` | Sync selected keys with localStorage/sessionStorage — hydrate on call, auto-save on change |
192
250
  | `createCleanupGroup()` | Collect multiple cleanup/unsubscribe functions and dispose them all at once |
193
251
  | `hydrateState(selector?)` | Read initial state from a `<script type="application/json">` tag (SSR hydration) |
194
252
 
@@ -230,7 +288,7 @@ watch(store, 'count', (val) => {
230
288
 
231
289
  ## Documentation
232
290
 
233
- Full documentation is available in the [docs/](docs/) directory:
291
+ 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
292
 
235
293
  - **Tutorials**
236
294
  - [Build a Todo App](docs/tutorials/build-todo-app.md)
@@ -240,10 +298,12 @@ Full documentation is available in the [docs/](docs/) directory:
240
298
  - [state()](docs/api/core/state.md) — Reactive state
241
299
  - [bindDom()](docs/api/core/bindDom.md) — DOM binding
242
300
  - [effect()](docs/api/core/effect.md) — Reactive effects
301
+ - [batch()](docs/api/core/batch.md) — Cross-store write batching
243
302
  - [Handlers](docs/api/core/handlers.md) — Extensible attribute handlers
244
- - [Plugins](docs/api/core/plugins.md) — State extension system
245
- - [Addons](docs/api/addons/computed.md) — computed, watch, repeat, createCleanupGroup, hydrateState
303
+ - [Plugins](docs/api/addons/withPlugins.md) — State extension system
304
+ - [Addons](docs/api/addons/computed.md) — computed, watch, repeat, persist, createCleanupGroup, hydrateState
246
305
  - **Guides**
306
+ - [Universal core (Node, CLI, workers)](docs/guides/universal-core.md) — using `lume-js/state` without a DOM
247
307
  - [Choosing reactive primitives](docs/guides/choosing-reactive-primitives.md) — when to use effect vs computed vs watch
248
308
  - [Cleanup & Disposal](docs/guides/cleanup-and-dispose.md) — tearing down effects, bindings, and subscriptions
249
309
  - [SSR & Hydration](docs/guides/ssr-hydration.md) — server-rendered HTML with reactive hydration
@@ -1 +1 @@
1
- function e(e,...t){void 0!==console&&"function"==typeof console.warn&&console.warn(e,...t)}function t(e,...t){void 0!==console&&"function"==typeof console.error&&console.error(e,...t)}const o=new Set;function n(e,t){o.add(e);try{return t()}finally{o.delete(e)}}let r=null;function c(e,o){if("function"!=typeof e)throw Error("effect() requires a function");const c=[];let i=!1;const s=()=>{if(!i){i=!0;try{e()}catch(e){throw t("[Lume.js effect] Error in effect:",e),e}finally{i=!1}}};if(Array.isArray(o)){for(const e of o)if(Array.isArray(e)&&e.length>=2){const[t,...o]=e;if(t&&"function"==typeof t.$subscribe)for(const e of o){let o=!0;const n=t.$subscribe(e,()=>{o?o=!1:s()});c.push(n)}}s()}else{const o=()=>{if(i)return;const s=c.splice(0),l={fn:e,cleanups:c,execute:o,tracking:{}},f=r;r=l,i=!0;try{n((e,t,o)=>{r===l&&(l.tracking[t]||(l.tracking[t]=!0,l.cleanups.push(o(t,l.execute))))},e)}catch(e){throw c.length=0,c.push(...s),t("[Lume.js effect] Error in effect:",e),e}finally{r=f,i=!1}if(c.length>0)for(const e of s)e();else c.push(...s)};o()}return()=>{for(;c.length;)c.pop()()}}function i(e){if("function"!=typeof e)throw Error("computed() requires a function");let o,n=!1,r=!1,i=!1;const s=[],l=c(()=>{if(!r&&!i){r=!0;try{const t=e();n&&Object.is(t,o)||(o=t,n=!0,s.forEach(e=>e(o)))}catch(e){t("[Lume.js computed] Error in computation:",e),n&&void 0===o||(o=void 0,n=!0,s.forEach(e=>e(o)))}finally{queueMicrotask(()=>{i||(r=!1)})}}});return{get value(){if(!n)throw Error("Computed value accessed before initialization");return o},subscribe(e){if("function"!=typeof e)throw Error("subscribe() requires a function");return s.push(e),n&&e(o),()=>{const t=s.indexOf(e);t>-1&&s.splice(t,1)}},dispose(){i=!0,l(),s.length=0,n=!1,r=!1}}}function s(e,t,o,{immediate:n=!0}={}){if(!e.$subscribe)throw Error("store must be created with state()");if(!n){let n=!1;return e.$subscribe(t,e=>{n?o(e):n=!0})}return e.$subscribe(t,o)}function l(e){const t=document.activeElement;if(!e.contains(t))return null;let o=null,n=null;return"INPUT"!==t.tagName&&"TEXTAREA"!==t.tagName||(o=t.selectionStart,n=t.selectionEnd),()=>{document.body.contains(t)&&(t.focus(),null!==o&&null!==n&&t.setSelectionRange(o,n))}}function f(e,t={}){const{isReorder:o=!1}=t,n=e.scrollTop;if(0===n)return()=>{e.scrollTop=0};let r=null,c=0;if(!o){const t=e.getBoundingClientRect();for(let o=e.firstElementChild;o;o=o.nextElementSibling){const e=o.getBoundingClientRect();if(e.bottom>t.top){r=o,c=e.top-t.top;break}}}return()=>{if(r&&document.body.contains(r)){const t=r.getBoundingClientRect(),o=e.getBoundingClientRect(),n=t.top-o.top-c;e.scrollTop=e.scrollTop+n}else e.scrollTop=n}}function u(o,n,r,c){const{key:i,render:s,create:u,update:a,remove:g,element:p="div",preserveFocus:d=l,preserveScroll:b=f}=c,h="string"==typeof o?document.querySelector(o):o;if(!h)return e(`[Lume.js] repeat(): container "${o}" not found`),()=>{};if("function"!=typeof i)throw Error("[Lume.js] repeat(): options.key must be a function");if("function"!=typeof s&&"function"!=typeof u)throw Error("[Lume.js] repeat(): options.render or options.create must be a function");const y=new Map,m=new Map,$=new Map,w=new Map,E=new Set;function j(){return"function"==typeof p?p():document.createElement(p)}function S(){const o=n[r];if(!Array.isArray(o))return void e(`[Lume.js] repeat(): store.${r} is not an array`);let c=!1;if(b&&y.size===o.length){c=!0;for(let e=0;e<o.length;e++)if(!y.has(i(o[e]))){c=!1;break}}E.clear();const l=[];for(let n=0;n<o.length;n++){const r=o[n],c=i(r);if(E.has(c)){e(`[Lume.js] repeat(): duplicate key "${c}"`);continue}E.add(c);let f=y.get(c);const g=!f;g&&(f=j(),y.set(c,f));try{if(g&&u){const e=u(r,f,n);"function"==typeof e&&w.set(c,e)}const e=m.get(c),t=$.get(c);a?e===r&&t===n||a(r,f,n,{isFirstRender:g}):s&&s(r,f,n),m.set(c,r),$.set(c,n)}catch(e){t(`[Lume.js] repeat(): error rendering key "${c}":`,e)}l.push(f)}!function(e,o,n){const r=document.body.contains(e),c=r&&d?d(e):null,i=r&&b?b(e,{isReorder:n}):null;(()=>{if(function(e,t){let o=e.firstChild;for(let n=0;n<t.length;n++){const r=t[n];o!==r?e.insertBefore(r,o):o=o.nextSibling}for(;o;){const t=o.nextSibling;e.removeChild(o),o=t}}(h,l),y.size!==E.size)for(const e of y.keys())if(!E.has(e)){const o=y.get(e),n=m.get(e),r=w.get(e);if("function"==typeof r)try{r()}catch(o){t(`[Lume.js] repeat(): cleanup error for key "${e}":`,o)}"function"==typeof g&&o&&g(n,o),y.delete(e),m.delete(e),$.delete(e),w.delete(e)}})(),c&&c(),i&&i()}(h,0,c)}let L;if("function"==typeof n.$subscribe)L=n.$subscribe(r,S);else{if("function"!=typeof n.subscribe)return S(),e("[Lume.js] repeat(): store is not reactive (no $subscribe or subscribe method)"),()=>{for(const[e,o]of y){const n=m.get(e),r=w.get(e);if("function"==typeof r)try{r()}catch(o){t(`[Lume.js] repeat(): cleanup error for key "${e}":`,o)}"function"==typeof g&&g(n,o)}h.replaceChildren(),y.clear(),m.clear(),$.clear(),w.clear(),E.clear()};{const e=n.subscribe(()=>S());S(),L="function"==typeof e?e:()=>{e?.unsubscribe?.()}}}return()=>{"function"==typeof L&&L();for(const[e,o]of y){const n=m.get(e),r=w.get(e);if("function"==typeof r)try{r()}catch(o){t(`[Lume.js] repeat(): cleanup error for key "${e}":`,o)}"function"==typeof g&&g(n,o)}h.replaceChildren(),y.clear(),m.clear(),$.clear(),w.clear(),E.clear()}}let a=!0,g=null;const p=new Map;function d(e){return null===g||("string"==typeof g?e.includes(g):!(g instanceof RegExp)||g.test(e))}function b(e){return p.has(e)||p.set(e,{gets:new Map,sets:new Map,notifies:new Map}),p.get(e)}function h(e,t,o){const n=b(e)[t];n.set(o,(n.get(o)||0)+1)}const y=100,m=97;function $(e){try{const t=JSON.stringify(e);return t.length>y?t.slice(0,m)+"...":t}catch{return e+""}}function w(e={}){const t=e.label??"store",o=(t,o)=>{const n=e[t];return void 0!==n?n:o};return{name:"debug:"+t,onInit:()=>{a&&console.log(`%c[${t}]%c initialized`,"color: #888; font-weight: bold","color: inherit")},onGet:(e,n)=>("string"==typeof e&&e.startsWith("$")||(h(t,"gets",e),a&&o("logGet",!1)&&d(e)&&console.log(`%c[${t}]%c GET %c${e}%c = ${$(n)}`,"color: #888; font-weight: bold","color: #4CAF50","color: #2196F3; font-weight: bold","color: inherit")),n),onSet:(e,n,r)=>("string"==typeof e&&e.startsWith("$")||(h(t,"sets",e),a&&o("logSet",!0)&&d(e)&&(console.log(`%c[${t}]%c SET %c${e}%c: ${$(r)} → ${$(n)}`,"color: #888; font-weight: bold","color: #FF9800","color: #2196F3; font-weight: bold","color: inherit"),o("trace",!1)&&console.trace(`%c[${t}] Stack trace for ${e}`,"color: #888"))),n),onSubscribe:e=>{a&&d(e)&&console.log(`%c[${t}]%c SUBSCRIBE %c${e}`,"color: #888; font-weight: bold","color: #9C27B0","color: #2196F3; font-weight: bold")},onNotify:(e,n)=>{"string"==typeof e&&e.startsWith("$")||(h(t,"notifies",e),a&&o("logNotify",!0)&&d(e)&&console.log(`%c[${t}]%c NOTIFY %c${e}%c = ${$(n)}`,"color: #888; font-weight: bold","color: #E91E63","color: #2196F3; font-weight: bold","color: inherit"))}}}const E={enable(){a=!0,console.log("%c[lume-debug]%c Logging enabled","color: #888; font-weight: bold","color: #4CAF50")},disable(){a=!1,console.log("%c[lume-debug]%c Logging disabled","color: #888; font-weight: bold","color: #F44336")},isEnabled:()=>a,filter(e){g=e,console.log(null===e?"%c[lume-debug]%c Filter cleared":"%c[lume-debug]%c Filter set: "+e,"color: #888; font-weight: bold","color: inherit")},getFilter:()=>g,stats(){const e={};for(const[t,o]of p)e[t]={gets:Object.fromEntries(o.gets),sets:Object.fromEntries(o.sets),notifies:Object.fromEntries(o.notifies)};return e},logStats(){const e=this.stats();if(0===Object.keys(e).length)return console.log("%c[lume-debug]%c No stats collected yet","color: #888; font-weight: bold","color: inherit"),e;console.group("%c[lume-debug] Statistics","color: #888; font-weight: bold");for(const[t,o]of Object.entries(e)){console.group("%c"+t,"color: #2196F3; font-weight: bold");const e=[],n=new Set([...Object.keys(o.gets),...Object.keys(o.sets),...Object.keys(o.notifies)]);for(const t of n)e.push({key:t,gets:o.gets[t]||0,sets:o.sets[t]||0,notifies:o.notifies[t]||0});e.length>0&&console.table(e),console.groupEnd()}return console.groupEnd(),e},resetStats(){p.clear(),console.log("%c[lume-debug]%c Stats reset","color: #888; font-weight: bold","color: inherit")}};function j(e,o=[]){if(!o.length)return e;for(const e of o){try{e.onInit?.()}catch(o){t(`[Lume.js] Plugin "${e.name}" error in onInit:`,o)}Object.freeze(e)}const n=new Map;let r;return"function"==typeof e.$beforeFlush&&(r=e.$beforeFlush(function(){for(const[e,r]of n)for(const n of o)try{n.onNotify?.(e,r)}catch(e){t(`[Lume.js] Plugin "${n.name}" error in onNotify:`,e)}n.clear()})),new Proxy(e,{get(e,c){if("$dispose"===c)return()=>{r&&r(),n.clear()};if("string"==typeof c&&c.startsWith("$")){const n=e[c];return"$subscribe"===c&&"function"==typeof n?(e,r)=>{for(const n of o)try{n.onSubscribe?.(e)}catch(e){t(`[Lume.js] Plugin "${n.name}" error in onSubscribe:`,e)}return n(e,r)}:n}let i=e[c];for(const e of o)try{const t=e.onGet?.(c,i);void 0!==t&&(i=t)}catch(o){t(`[Lume.js] Plugin "${e.name}" error in onGet:`,o)}return i},set(e,r,c){const i=e[r];let s=c;for(const e of o)try{const t=e.onSet?.(r,s,i);void 0!==t&&(s=t)}catch(o){t(`[Lume.js] Plugin "${e.name}" error in onSet:`,o)}return Object.is(s,i)||n.set(r,s),e[r]=s,!0}})}function S(){const e=[];return{add(t){"function"==typeof t&&e.push(t)},dispose(){for(;e.length;){const t=e.pop();try{t()}catch(e){}}}}}function L(e="#__LUME_DATA__",t){const o="undefined"!=typeof document?document.querySelector(e):null;if(!o)return{};if("SCRIPT"!==o.tagName||"application/json"!==o.type)return{};let n;try{n=JSON.parse(o.textContent)}catch{return{}}return"function"!=typeof t||t(n)?n:{}}function k(e){return!(!e||"object"!=typeof e||"function"!=typeof e.$subscribe)}export{i as computed,S as createCleanupGroup,w as createDebugPlugin,E as debug,l as defaultFocusPreservation,f as defaultScrollPreservation,L as hydrateState,k as isReactive,u as repeat,s as watch,j as withPlugins};
1
+ function e(e,...t){void 0!==console&&"function"==typeof console.warn&&console.warn(e,...t)}function t(e,...t){void 0!==console&&"function"==typeof console.error&&console.error(e,...t)}const o=new Set,n=Symbol.for("lume.reactive");function r(e,t){o.add(e);try{return t()}finally{o.delete(e)}}let c=null;function s(e,o){if("function"!=typeof e)throw Error("effect() requires a function");const n=[];let s=!1;const i=()=>{if(!s){s=!0;try{e()}catch(e){throw t("[Lume.js effect] Error in effect:",e),e}finally{s=!1}}};if(Array.isArray(o)){let e=!1,t=!1;n.push(()=>{t=!0});const r=()=>{e||(e=!0,queueMicrotask(()=>{if(e=!1,!t)try{i()}catch{}}))};for(const e of o)if(Array.isArray(e)&&e.length>=2){const[t,...o]=e;if(t&&"function"==typeof t.$subscribe)for(const e of o){let o=!0;const c=t.$subscribe(e,()=>{o?o=!1:r()});n.push(c)}}i()}else{const o=()=>{if(s)return;const i=n.splice(0),l={fn:e,cleanups:n,execute:o,tracking:new WeakMap},u=c;c=l,s=!0;try{r((e,t,o)=>{if(c!==l)return;let n=l.tracking.get(e);n||(n=new Set,l.tracking.set(e,n)),n.has(t)||(n.add(t),l.cleanups.push(o(t,l.execute)))},e)}catch(e){throw n.length=0,n.push(...i),t("[Lume.js effect] Error in effect:",e),e}finally{c=u,s=!1}if(n.length>0)for(const e of i)e();else n.push(...i)};o()}return()=>{for(;n.length;)n.pop()()}}function i(e){if("function"!=typeof e)throw Error("computed() requires a function");let o,n=!1,r=!1,c=!1;const i=[],l=s(()=>{if(!r&&!c){r=!0;try{const t=e();n&&Object.is(t,o)||(o=t,n=!0,i.forEach(e=>e(o)))}catch(e){t("[Lume.js computed] Error in computation:",e),n&&void 0===o||(o=void 0,n=!0,i.forEach(e=>e(o)))}finally{queueMicrotask(()=>{c||(r=!1)})}}});return{get value(){if(!n)throw Error("Computed value accessed before initialization");return o},subscribe(e){if("function"!=typeof e)throw Error("subscribe() requires a function");return i.push(e),n&&e(o),()=>{const t=i.indexOf(e);t>-1&&i.splice(t,1)}},dispose(){c=!0,l(),i.length=0,n=!1,r=!1}}}function l(e,t,o,{immediate:n=!0}={}){if(!e.$subscribe)throw Error("store must be created with state()");if(!n){let n=!1;return e.$subscribe(t,e=>{n?o(e):n=!0})}return e.$subscribe(t,o)}function u(e,t){"INPUT"===e.tagName?"checkbox"===e.type?e.checked=!!t:"radio"===e.type?e.checked=e.value===t+"":e.value=t??"":"TEXTAREA"===e.tagName||"SELECT"===e.tagName?e.value=t??"":e.textContent=t??""}function f(e,t){let o=e;if(!0===e?o=t.querySelector("template"):"string"==typeof e&&(o=document.querySelector(e)),!o||"TEMPLATE"!==o.tagName)throw Error("[Lume.js] repeat(): template not found or not a <template> element");if(1!==o.content.children.length)throw Error("[Lume.js] repeat(): template must contain exactly one root element");return o.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};
package/dist/addons.mjs CHANGED
@@ -1,4 +1,6 @@
1
- import { e as effect, a as logError, l as logWarn } from "./shared-x2HJmEyO.mjs";
1
+ import { R as REACTIVE_BRAND } from "./shared-Bk_gndPJ.mjs";
2
+ import { e as effect, a as applyBindValue } from "./shared-DNe4ez8V.mjs";
3
+ import { l as logError, a as logWarn } from "./shared-DmpHYKx7.mjs";
2
4
  function computed(fn) {
3
5
  if (typeof fn !== "function") {
4
6
  throw new Error("computed() requires a function");
@@ -92,6 +94,47 @@ function watch(store, key, callback, { immediate = true } = {}) {
92
94
  }
93
95
  return store.$subscribe(key, callback);
94
96
  }
97
+ function resolveTemplateRoot(template, containerEl) {
98
+ let templateEl = template;
99
+ if (template === true) {
100
+ templateEl = containerEl.querySelector("template");
101
+ } else if (typeof template === "string") {
102
+ templateEl = document.querySelector(template);
103
+ }
104
+ if (!templateEl || templateEl.tagName !== "TEMPLATE") {
105
+ throw new Error("[Lume.js] repeat(): template not found or not a <template> element");
106
+ }
107
+ if (templateEl.content.children.length !== 1) {
108
+ throw new Error("[Lume.js] repeat(): template must contain exactly one root element");
109
+ }
110
+ return templateEl.content.firstElementChild;
111
+ }
112
+ function collectItemBindings(el) {
113
+ const bindings = [];
114
+ const add = (node) => {
115
+ const path = node.getAttribute("data-bind");
116
+ bindings.push({ node, path, keys: path === "$item" || path === "$index" ? null : path.split(".") });
117
+ };
118
+ if (el.hasAttribute("data-bind")) add(el);
119
+ for (const node of el.querySelectorAll("[data-bind]")) add(node);
120
+ return bindings;
121
+ }
122
+ function applyItemBindings(bindings, item, index) {
123
+ for (const b of bindings) {
124
+ let val;
125
+ if (b.path === "$index") {
126
+ val = index;
127
+ } else if (b.path === "$item") {
128
+ val = item;
129
+ } else {
130
+ val = item;
131
+ for (let i = 0; i < b.keys.length && val != null; i++) {
132
+ val = val[b.keys[i]];
133
+ }
134
+ }
135
+ applyBindValue(b.node, val);
136
+ }
137
+ }
95
138
  function defaultFocusPreservation(container) {
96
139
  const activeEl = document.activeElement;
97
140
  const shouldRestore = container.contains(activeEl);
@@ -151,6 +194,7 @@ function repeat(container, store, arrayKey, options) {
151
194
  create,
152
195
  update,
153
196
  remove,
197
+ template = null,
154
198
  element = "div",
155
199
  preserveFocus = defaultFocusPreservation,
156
200
  preserveScroll = defaultScrollPreservation
@@ -164,15 +208,21 @@ function repeat(container, store, arrayKey, options) {
164
208
  if (typeof key !== "function") {
165
209
  throw new Error("[Lume.js] repeat(): options.key must be a function");
166
210
  }
167
- if (typeof render !== "function" && typeof create !== "function") {
211
+ const templateRoot = template ? resolveTemplateRoot(template, containerEl) : null;
212
+ if (templateRoot && typeof render === "function") {
213
+ logWarn("[Lume.js] repeat(): options.render is ignored when options.template is set — use create/update instead");
214
+ }
215
+ if (!templateRoot && typeof render !== "function" && typeof create !== "function") {
168
216
  throw new Error("[Lume.js] repeat(): options.render or options.create must be a function");
169
217
  }
170
218
  const elementsByKey = /* @__PURE__ */ new Map();
171
219
  const prevItemsByKey = /* @__PURE__ */ new Map();
172
220
  const prevIndexByKey = /* @__PURE__ */ new Map();
173
221
  const cleanupByKey = /* @__PURE__ */ new Map();
222
+ const bindingsByKey = /* @__PURE__ */ new Map();
174
223
  const seenKeys = /* @__PURE__ */ new Set();
175
224
  function createElement() {
225
+ if (templateRoot) return templateRoot.cloneNode(true);
176
226
  return typeof element === "function" ? element() : document.createElement(element);
177
227
  }
178
228
  function reconcileDOM(container2, nextEls) {
@@ -230,6 +280,9 @@ function repeat(container, store, arrayKey, options) {
230
280
  if (isFirstRender) {
231
281
  el = createElement();
232
282
  elementsByKey.set(k, el);
283
+ if (templateRoot) {
284
+ bindingsByKey.set(k, collectItemBindings(el));
285
+ }
233
286
  }
234
287
  try {
235
288
  if (isFirstRender && create) {
@@ -240,7 +293,12 @@ function repeat(container, store, arrayKey, options) {
240
293
  }
241
294
  const prevItem = prevItemsByKey.get(k);
242
295
  const prevIndex = prevIndexByKey.get(k);
243
- if (update) {
296
+ if (templateRoot) {
297
+ if (prevItem !== item || prevIndex !== i) {
298
+ applyItemBindings(bindingsByKey.get(k), item, i);
299
+ if (update) update(item, el, i, { isFirstRender });
300
+ }
301
+ } else if (update) {
244
302
  if (prevItem !== item || prevIndex !== i) {
245
303
  update(item, el, i, { isFirstRender });
246
304
  }
@@ -276,6 +334,7 @@ function repeat(container, store, arrayKey, options) {
276
334
  prevItemsByKey.delete(k);
277
335
  prevIndexByKey.delete(k);
278
336
  cleanupByKey.delete(k);
337
+ bindingsByKey.delete(k);
279
338
  }
280
339
  }
281
340
  }
@@ -313,6 +372,7 @@ function repeat(container, store, arrayKey, options) {
313
372
  prevItemsByKey.clear();
314
373
  prevIndexByKey.clear();
315
374
  cleanupByKey.clear();
375
+ bindingsByKey.clear();
316
376
  seenKeys.clear();
317
377
  };
318
378
  }
@@ -339,6 +399,7 @@ function repeat(container, store, arrayKey, options) {
339
399
  prevItemsByKey.clear();
340
400
  prevIndexByKey.clear();
341
401
  cleanupByKey.clear();
402
+ bindingsByKey.clear();
342
403
  seenKeys.clear();
343
404
  };
344
405
  }
@@ -682,8 +743,91 @@ function hydrateState(selector = "#__LUME_DATA__", validate) {
682
743
  }
683
744
  return data;
684
745
  }
746
+ function readStored(storage, storageKey) {
747
+ try {
748
+ const raw = storage.getItem(storageKey);
749
+ if (!raw) return null;
750
+ const data = JSON.parse(raw);
751
+ return data && typeof data === "object" && !Array.isArray(data) ? data : null;
752
+ } catch {
753
+ logWarn(`[Lume.js] persist(): could not read "${storageKey}" — starting fresh`);
754
+ return null;
755
+ }
756
+ }
757
+ function serializeKeys(store, watched) {
758
+ const out = {};
759
+ for (const k of watched) out[k] = store[k];
760
+ return JSON.stringify(out);
761
+ }
762
+ function persist(store, storageKey, options = {}) {
763
+ if (!store || typeof store.$subscribe !== "function") {
764
+ throw new Error("[Lume.js] persist() requires a reactive store from state()");
765
+ }
766
+ if (typeof storageKey !== "string" || storageKey.length === 0) {
767
+ throw new Error("[Lume.js] persist() requires a non-empty storage key");
768
+ }
769
+ const storage = options.storage !== void 0 ? options.storage : globalThis.localStorage;
770
+ if (!storage || typeof storage.getItem !== "function") {
771
+ logWarn("[Lume.js] persist(): no storage available — persistence disabled");
772
+ return () => {
773
+ };
774
+ }
775
+ const watched = Array.isArray(options.keys) && options.keys.length > 0 ? options.keys.slice() : Object.keys(store).filter((k) => !k.startsWith("$"));
776
+ const stored = readStored(storage, storageKey);
777
+ if (stored) {
778
+ for (const k of watched) {
779
+ if (Object.prototype.hasOwnProperty.call(stored, k)) {
780
+ store[k] = stored[k];
781
+ }
782
+ }
783
+ }
784
+ let lastWritten = null;
785
+ try {
786
+ lastWritten = serializeKeys(store, watched);
787
+ } catch {
788
+ }
789
+ let scheduled = false;
790
+ let disposed = false;
791
+ const flushSave = () => {
792
+ scheduled = false;
793
+ if (disposed) return;
794
+ let json;
795
+ try {
796
+ json = serializeKeys(store, watched);
797
+ } catch (err) {
798
+ logWarn("[Lume.js] persist(): state not serializable — skipping save", err);
799
+ return;
800
+ }
801
+ if (json === lastWritten) return;
802
+ try {
803
+ storage.setItem(storageKey, json);
804
+ lastWritten = json;
805
+ } catch (err) {
806
+ logWarn("[Lume.js] persist(): could not write — storage full or unavailable?", err);
807
+ }
808
+ };
809
+ const save = () => {
810
+ if (scheduled) return;
811
+ scheduled = true;
812
+ queueMicrotask(flushSave);
813
+ };
814
+ const unsubs = watched.map((k) => {
815
+ let first = true;
816
+ return store.$subscribe(k, () => {
817
+ if (first) {
818
+ first = false;
819
+ return;
820
+ }
821
+ save();
822
+ });
823
+ });
824
+ return () => {
825
+ disposed = true;
826
+ while (unsubs.length) unsubs.pop()();
827
+ };
828
+ }
685
829
  function isReactive(obj) {
686
- return !!(obj && typeof obj === "object" && typeof obj.$subscribe === "function");
830
+ return !!(obj && typeof obj === "object" && (REACTIVE_BRAND in obj || typeof obj.$subscribe === "function"));
687
831
  }
688
832
  export {
689
833
  computed,
@@ -694,6 +838,7 @@ export {
694
838
  defaultScrollPreservation,
695
839
  hydrateState,
696
840
  isReactive,
841
+ persist,
697
842
  repeat,
698
843
  watch,
699
844
  withPlugins