lume-js 2.2.0 → 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.
Files changed (43) hide show
  1. package/README.md +90 -19
  2. package/dist/addons.min.mjs +1 -1
  3. package/dist/addons.mjs +160 -6
  4. package/dist/addons.mjs.map +1 -1
  5. package/dist/handlers.min.mjs +1 -1
  6. package/dist/handlers.mjs +38 -2
  7. package/dist/handlers.mjs.map +1 -1
  8. package/dist/index.min.mjs +1 -1
  9. package/dist/index.mjs +4 -141
  10. package/dist/index.mjs.map +1 -1
  11. package/dist/lume.global.js +1 -1
  12. package/dist/lume.global.js.map +1 -1
  13. package/dist/shared-Bk_gndPJ.mjs +232 -0
  14. package/dist/shared-Bk_gndPJ.mjs.map +1 -0
  15. package/dist/shared-DNe4ez8V.mjs +249 -0
  16. package/dist/shared-DNe4ez8V.mjs.map +1 -0
  17. package/dist/shared-DmpHYKx7.mjs +15 -0
  18. package/dist/shared-DmpHYKx7.mjs.map +1 -0
  19. package/dist/state.min.mjs +1 -0
  20. package/dist/state.mjs +7 -0
  21. package/dist/state.mjs.map +1 -0
  22. package/package.json +5 -1
  23. package/src/addons/computed.js +5 -0
  24. package/src/addons/hydrateState.js +32 -4
  25. package/src/addons/index.d.ts +70 -2
  26. package/src/addons/index.js +13 -2
  27. package/src/addons/persist.js +152 -0
  28. package/src/addons/repeat.js +120 -5
  29. package/src/addons/withPlugins.js +7 -1
  30. package/src/core/batch.js +139 -0
  31. package/src/core/bindDom.js +12 -3
  32. package/src/core/effect.js +34 -5
  33. package/src/core/state.js +124 -68
  34. package/src/handlers/index.d.ts +24 -0
  35. package/src/handlers/index.js +1 -0
  36. package/src/handlers/on.js +60 -0
  37. package/src/handlers/stringAttr.js +14 -2
  38. package/src/index.d.ts +40 -0
  39. package/src/index.js +3 -1
  40. package/src/state.d.ts +17 -0
  41. package/src/state.js +25 -0
  42. package/dist/shared-Dcokqj5a.mjs +0 -249
  43. package/dist/shared-Dcokqj5a.mjs.map +0 -1
package/README.md CHANGED
@@ -1,17 +1,28 @@
1
- # Lume.js
2
-
3
- **Reactivity that follows web standards.**
4
-
5
- Minimal reactive state management using only standard JavaScript and HTML. No custom syntax, no build step required, no framework lock-in.
6
-
7
- > **Current Release:** v2.2.0
8
- > Install: `npm install lume-js`
9
- > Bundle size: ~2.09KB gzipped | 348 tests passing
1
+ <div align="center">
2
+ <img src="lume-logo.png" alt="Lume.js" width="128"><br><br>
3
+ <h3>Lume.js</h3>
4
+ <p><strong>Reactivity that follows web standards.</strong></p>
5
+ <p>
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.<br>
8
+ <strong>1.45 KB universal core</strong> &nbsp;·&nbsp; <strong>2.66 KB with DOM</strong>
9
+ </p>
10
+ <p>
11
+ <a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-blue.svg" alt="License: MIT"></a>
12
+ &nbsp;
13
+ <a href="package.json"><img src="https://img.shields.io/badge/version-2.3.0-orange.svg" alt="v2.3.0"></a>
14
+ &nbsp;
15
+ <a href="tests/"><img src="https://img.shields.io/badge/tests-425%20passing-brightgreen.svg" alt="425 tests"></a>
16
+ &nbsp;
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>
20
+ </p>
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>
23
+ </div>
10
24
 
11
- [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
12
- [![Version](https://img.shields.io/badge/version-2.2.0-orange.svg)](package.json)
13
- [![Tests](https://img.shields.io/badge/tests-348%20passing-brightgreen.svg)](tests/)
14
- [![Size](https://img.shields.io/badge/core-2.09KB-blue.svg)](scripts/check-size.js)
25
+ ---
15
26
 
16
27
  ## Why Lume.js?
17
28
 
@@ -19,7 +30,7 @@ Minimal reactive state management using only standard JavaScript and HTML. No cu
19
30
  |---------|---------|-----------|-----|-------|
20
31
  | Custom Syntax | ❌ No | ✅ `x-data` | ✅ `v-bind` | ✅ JSX |
21
32
  | Build Step | ❌ Optional | ❌ Optional | ⚠️ Recommended | ✅ Required |
22
- | Bundle Size | ~2.09KB | ~15KB | ~35KB | ~45KB |
33
+ | Bundle Size | 1.45–2.66KB | ~15KB | ~35KB | ~45KB |
23
34
  | HTML Validation | ✅ Pass | ⚠️ Warnings | ⚠️ Warnings | ❌ JSX |
24
35
  | Extensible Handlers | ✅ | ❌ Built-in only | ❌ Built-in only | N/A |
25
36
 
@@ -29,6 +40,15 @@ Minimal reactive state management using only standard JavaScript and HTML. No cu
29
40
 
30
41
  ## Installation
31
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
+
32
52
  ### Via CDN (Recommended for simple projects)
33
53
 
34
54
  ```html
@@ -37,6 +57,14 @@ Minimal reactive state management using only standard JavaScript and HTML. No cu
37
57
  </script>
38
58
  ```
39
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
+
40
68
  ### Via NPM (Recommended for bundlers)
41
69
 
42
70
  ```bash
@@ -44,9 +72,13 @@ npm install lume-js
44
72
  ```
45
73
 
46
74
  ```javascript
47
- 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
48
77
  ```
49
78
 
79
+ > **→ Using Lume without a DOM?** See the [Universal core guide](docs/guides/universal-core.md).
80
+
81
+
50
82
  ### Browser Support
51
83
 
52
84
  | Browser | Minimum version |
@@ -81,6 +113,37 @@ bindDom(document.body, store);
81
113
 
82
114
  That's it — two-way binding, no build step, valid HTML.
83
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
+
84
147
  ---
85
148
 
86
149
  ## Built-in Reactive Attributes
@@ -142,6 +205,7 @@ bindDom(document.body, store, {
142
205
  | `ariaAttr(name)` | `data-aria-pressed="key"` | Sets ARIA attribute to "true"/"false" |
143
206
  | `classToggle(...names)` | `data-class-active="key"` | Toggles individual CSS classes |
144
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 |
145
209
  | `htmlAttrs()` | *(all of the above)* | One-import preset — all standard HTML + ARIA attrs |
146
210
 
147
211
  ### Presets
@@ -181,7 +245,10 @@ import { computed, watch, repeat } from 'lume-js/addons';
181
245
  | `effect(fn)` *(core)* | Write derived values back into the store, or trigger side effects on state change |
182
246
  | `computed(fn)` | Derive a read-only value from state to consume *outside* the store (templates, display logic) |
183
247
  | `watch(store, key, fn)` | React to a *specific* key changing — DOM updates, analytics, syncing external state |
184
- | `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 |
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) |
185
252
 
186
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.
187
254
 
@@ -221,7 +288,7 @@ watch(store, 'count', (val) => {
221
288
 
222
289
  ## Documentation
223
290
 
224
- 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:
225
292
 
226
293
  - **Tutorials**
227
294
  - [Build a Todo App](docs/tutorials/build-todo-app.md)
@@ -231,11 +298,15 @@ Full documentation is available in the [docs/](docs/) directory:
231
298
  - [state()](docs/api/core/state.md) — Reactive state
232
299
  - [bindDom()](docs/api/core/bindDom.md) — DOM binding
233
300
  - [effect()](docs/api/core/effect.md) — Reactive effects
301
+ - [batch()](docs/api/core/batch.md) — Cross-store write batching
234
302
  - [Handlers](docs/api/core/handlers.md) — Extensible attribute handlers
235
- - [Plugins](docs/api/core/plugins.md) — State extension system
236
- - [Addons](docs/api/addons/computed.md) — computed, watch, repeat, debug
303
+ - [Plugins](docs/api/addons/withPlugins.md) — State extension system
304
+ - [Addons](docs/api/addons/computed.md) — computed, watch, repeat, persist, createCleanupGroup, hydrateState
237
305
  - **Guides**
306
+ - [Universal core (Node, CLI, workers)](docs/guides/universal-core.md) — using `lume-js/state` without a DOM
238
307
  - [Choosing reactive primitives](docs/guides/choosing-reactive-primitives.md) — when to use effect vs computed vs watch
308
+ - [Cleanup & Disposal](docs/guides/cleanup-and-dispose.md) — tearing down effects, bindings, and subscriptions
309
+ - [SSR & Hydration](docs/guides/ssr-hydration.md) — server-rendered HTML with reactive hydration
239
310
  - **Design**
240
311
  - [Design Decisions](docs/design/design-decisions.md)
241
312
 
@@ -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 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)){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:i()});c.push(n)}}i()}else{const o=()=>{if(s)return;const i=c.splice(0),l={fn:e,cleanups:c,execute:o,tracking:{}},f=r;r=l,s=!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(...i),t("[Lume.js effect] Error in effect:",e),e}finally{r=f,s=!1}if(c.length>0)for(const e of i)e();else c.push(...i)};o()}return()=>{for(;c.length;)c.pop()()}}function s(e){if("function"!=typeof e)throw Error("computed() requires a function");let o,n=!1,r=!1,s=!1;const i=[],l=c(()=>{if(!r&&!s){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(()=>{s||(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(){s=!0,l(),i.length=0,n=!1,r=!1}}}function i(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:s,render:i,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 s)throw Error("[Lume.js] repeat(): options.key must be a function");if("function"!=typeof i&&"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(s(o[e]))){c=!1;break}}E.clear();const l=[];for(let n=0;n<o.length;n++){const r=o[n],c=s(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}):i&&i(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,s=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(),s&&s()}(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)}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 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__"){const t="undefined"!=typeof document?document.querySelector(e):null;if(!t)return{};try{return JSON.parse(t.textContent)}catch{return{}}}function k(e){return!(!e||"object"!=typeof e||"function"!=typeof e.$subscribe)}export{s 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,i 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-Dcokqj5a.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
  }
@@ -566,6 +627,7 @@ function withPlugins(store, plugins = []) {
566
627
  } catch (e) {
567
628
  logError(`[Lume.js] Plugin "${p.name}" error in onInit:`, e);
568
629
  }
630
+ Object.freeze(p);
569
631
  }
570
632
  const pendingNotifications = /* @__PURE__ */ new Map();
571
633
  function runNotifyHooks() {
@@ -664,17 +726,108 @@ function createCleanupGroup() {
664
726
  }
665
727
  };
666
728
  }
667
- function hydrateState(selector = "#__LUME_DATA__") {
729
+ function hydrateState(selector = "#__LUME_DATA__", validate) {
668
730
  const el = typeof document !== "undefined" ? document.querySelector(selector) : null;
669
731
  if (!el) return {};
732
+ if (el.tagName !== "SCRIPT" || el.type !== "application/json") {
733
+ return {};
734
+ }
735
+ let data;
670
736
  try {
671
- return JSON.parse(el.textContent);
737
+ data = JSON.parse(el.textContent);
672
738
  } catch {
673
739
  return {};
674
740
  }
741
+ if (typeof validate === "function" && !validate(data)) {
742
+ return {};
743
+ }
744
+ return data;
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
+ };
675
828
  }
676
829
  function isReactive(obj) {
677
- return !!(obj && typeof obj === "object" && typeof obj.$subscribe === "function");
830
+ return !!(obj && typeof obj === "object" && (REACTIVE_BRAND in obj || typeof obj.$subscribe === "function"));
678
831
  }
679
832
  export {
680
833
  computed,
@@ -685,6 +838,7 @@ export {
685
838
  defaultScrollPreservation,
686
839
  hydrateState,
687
840
  isReactive,
841
+ persist,
688
842
  repeat,
689
843
  watch,
690
844
  withPlugins