lume-js 2.0.0-beta.1 → 2.0.0-beta.2

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,14 +4,14 @@
4
4
 
5
5
  Minimal reactive state management using only standard JavaScript and HTML. No custom syntax, no build step required, no framework lock-in.
6
6
 
7
- > **Current Release:** v1.0.0 (stable) | **Next Release:** v2.0.0-beta.1
7
+ > **Current Release:** v1.0.0 (stable) | **Next Release:** v2.0.0-beta.2
8
8
  > Install stable: `npm install lume-js@1.0.0`
9
9
  > Install next: `npm install lume-js@next`
10
10
 
11
11
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
12
- [![Version](https://img.shields.io/badge/version-2.0.0--beta.1-orange.svg)](package.json)
13
- [![Tests](https://img.shields.io/badge/tests-231%20passing-brightgreen.svg)](tests/)
14
- [![Size](https://img.shields.io/badge/core-2.39KB-blue.svg)](scripts/check-size.js)
12
+ [![Version](https://img.shields.io/badge/version-2.0.0--beta.2-orange.svg)](package.json)
13
+ [![Tests](https://img.shields.io/badge/tests-294%20passing-brightgreen.svg)](tests/)
14
+ [![Size](https://img.shields.io/badge/core-2.15KB-blue.svg)](scripts/check-size.js)
15
15
 
16
16
  ## Why Lume.js?
17
17
 
@@ -19,7 +19,7 @@ Minimal reactive state management using only standard JavaScript and HTML. No cu
19
19
  |---------|---------|-----------|-----|-------|
20
20
  | Custom Syntax | ❌ No | ✅ `x-data` | ✅ `v-bind` | ✅ JSX |
21
21
  | Build Step | ❌ Optional | ❌ Optional | ⚠️ Recommended | ✅ Required |
22
- | Bundle Size | ~2.4KB | ~15KB | ~35KB | ~45KB |
22
+ | Bundle Size | ~2.15KB | ~15KB | ~35KB | ~45KB |
23
23
  | HTML Validation | ✅ Pass | ⚠️ Warnings | ⚠️ Warnings | ❌ JSX |
24
24
  | Extensible Handlers | ✅ | ❌ Built-in only | ❌ Built-in only | N/A |
25
25
 
@@ -33,7 +33,7 @@ Minimal reactive state management using only standard JavaScript and HTML. No cu
33
33
 
34
34
  ```html
35
35
  <script type="module">
36
- import { state, bindDom, effect } from 'https://cdn.jsdelivr.net/npm/lume-js/src/index.js';
36
+ import { state, bindDom, effect } from 'https://cdn.jsdelivr.net/npm/lume-js/dist/index.min.mjs';
37
37
  </script>
38
38
  ```
39
39
 
@@ -48,7 +48,16 @@ import { state, bindDom } from 'lume-js';
48
48
  ```
49
49
 
50
50
  ### Browser Support
51
- Works in all modern browsers (Chrome 49+, Firefox 18+, Safari 10+, Edge 79+). **IE11 is NOT supported.**
51
+
52
+ | Browser | Minimum version |
53
+ |---------|-----------------|
54
+ | Chrome | 49+ |
55
+ | Firefox | 18+ |
56
+ | Safari | 10+ |
57
+ | Edge | 79+ |
58
+ | IE11 | ❌ Not supported |
59
+
60
+ IE11 cannot be polyfilled — Lume uses `Proxy`.
52
61
 
53
62
  ---
54
63
 
@@ -167,8 +176,6 @@ import { computed, watch, repeat } from 'lume-js/addons';
167
176
  - **`watch(store, key, fn)`** — Subscribe to state changes
168
177
  - **`repeat(container, store, key, options)`** — Keyed list rendering with element reuse
169
178
 
170
- > **Note:** The `repeat` addon is *experimental*. Its API may evolve in future releases.
171
-
172
179
  ---
173
180
 
174
181
  ## Documentation
@@ -0,0 +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];c.length=0;const l={fn:e,cleanups:c,execute:o,tracking:{}},u=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=u,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){if(!e.$subscribe)throw Error("store must be created with state()");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 u(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 f(o,n,r,c){const{key:s,render:i,create:f,update:a,element:g="div",preserveFocus:b=l,preserveScroll:d=u}=c,p="string"==typeof o?document.querySelector(o):o;if(!p)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 f)throw Error("[Lume.js] repeat(): options.render or options.create must be a function");const h=new Map,y=new Map,m=new Map,w=new Set;function $(){return"function"==typeof g?g():document.createElement(g)}function E(){const o=n[r];if(!Array.isArray(o))return void e(`[Lume.js] repeat(): store.${r} is not an array`);let c=!1;if(d){const e=new Set(h.keys()),t=new Set(o.map(e=>s(e)));c=e.size===t.size&&[...e].every(e=>t.has(e))}w.clear();const l=new Set,u=[];for(let n=0;n<o.length;n++){const r=o[n],c=s(r);if(w.has(c)){e(`[Lume.js] repeat(): duplicate key "${c}"`);continue}w.add(c),l.add(c);let g=h.get(c);const b=!g;b&&(g=$(),h.set(c,g));try{b&&f&&f(r,g,n);const e=y.get(c),t=m.get(c);a?e===r&&t===n||a(r,g,n,{isFirstRender:b}):i&&i(r,g,n),y.set(c,r),m.set(c,n)}catch(e){t(`[Lume.js] repeat(): error rendering key "${c}":`,e)}u.push(g)}!function(e,t,o){const n=document.body.contains(e),r=n&&b?b(e):null,c=n&&d?d(e,{isReorder:o}):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}}(p,u),h.size!==l.size)for(const e of h.keys())l.has(e)||(h.delete(e),y.delete(e),m.delete(e))})(),r&&r(),c&&c()}(p,0,c)}let S;if("function"==typeof n.$subscribe)S=n.$subscribe(r,E);else{if("function"!=typeof n.subscribe)return E(),e("[Lume.js] repeat(): store is not reactive (no $subscribe or subscribe method)"),()=>{p.replaceChildren(),h.clear(),y.clear(),m.clear(),w.clear()};{const e=n.subscribe(()=>E());E(),S="function"==typeof e?e:()=>{e?.unsubscribe?.()}}}return()=>{"function"==typeof S&&S(),p.replaceChildren(),h.clear(),y.clear(),m.clear(),w.clear()}}let a=!0,g=null;const b=new Map;function d(e){return null===g||("string"==typeof g?e.includes(g):!(g instanceof RegExp)||g.test(e))}function p(e){return b.has(e)||b.set(e,{gets:new Map,sets:new Map,notifies:new Map}),b.get(e)}function h(e,t,o){const n=p(e)[t];n.set(o,(n.get(o)||0)+1)}const y=100,m=97;function w(e){try{const t=JSON.stringify(e);return t.length>y?t.slice(0,m)+"...":t}catch{return e+""}}function $(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 = ${w(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: ${w(r)} → ${w(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 = ${w(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 b)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(){b.clear(),console.log("%c[lume-debug]%c Stats reset","color: #888; font-weight: bold","color: inherit")}};function S(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;return"function"==typeof e.$beforeFlush&&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,n){if("string"==typeof n&&n.startsWith("$")){const r=e[n];return"$subscribe"===n&&"function"==typeof r?(e,n)=>{for(const n of o)try{n.onSubscribe?.(e)}catch(e){t(`[Lume.js] Plugin "${n.name}" error in onSubscribe:`,e)}return r(e,n)}:r}let r=e[n];for(const e of o)try{const t=e.onGet?.(n,r);void 0!==t&&(r=t)}catch(o){t(`[Lume.js] Plugin "${e.name}" error in onGet:`,o)}return r},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 j(e){return!(!e||"object"!=typeof e||"function"!=typeof e.$subscribe)}export{s as computed,$ as createDebugPlugin,E as debug,l as defaultFocusPreservation,u as defaultScrollPreservation,j as isReactive,f as repeat,i as watch,S as withPlugins};
@@ -0,0 +1,587 @@
1
+ import { e as effect, a as logError, l as logWarn } from "./shared-DMnPGlYI.mjs";
2
+ function computed(fn) {
3
+ if (typeof fn !== "function") {
4
+ throw new Error("computed() requires a function");
5
+ }
6
+ let cachedValue;
7
+ let isInitialized = false;
8
+ let isInComputation = false;
9
+ let disposed = false;
10
+ const subscribers = [];
11
+ const cleanupEffect = effect(() => {
12
+ if (isInComputation || disposed) return;
13
+ isInComputation = true;
14
+ try {
15
+ const newValue = fn();
16
+ if (!isInitialized || !Object.is(newValue, cachedValue)) {
17
+ cachedValue = newValue;
18
+ isInitialized = true;
19
+ subscribers.forEach((callback) => callback(cachedValue));
20
+ }
21
+ } catch (error) {
22
+ logError("[Lume.js computed] Error in computation:", error);
23
+ if (!isInitialized || cachedValue !== void 0) {
24
+ cachedValue = void 0;
25
+ isInitialized = true;
26
+ subscribers.forEach((callback) => callback(cachedValue));
27
+ }
28
+ } finally {
29
+ queueMicrotask(() => {
30
+ if (!disposed) {
31
+ isInComputation = false;
32
+ }
33
+ });
34
+ }
35
+ });
36
+ return {
37
+ /**
38
+ * Get the current computed value
39
+ */
40
+ get value() {
41
+ if (!isInitialized) {
42
+ throw new Error("Computed value accessed before initialization");
43
+ }
44
+ return cachedValue;
45
+ },
46
+ /**
47
+ * Subscribe to changes in computed value
48
+ *
49
+ * @param {function} callback - Called when value changes
50
+ * @returns {function} Unsubscribe function
51
+ */
52
+ subscribe(callback) {
53
+ if (typeof callback !== "function") {
54
+ throw new Error("subscribe() requires a function");
55
+ }
56
+ subscribers.push(callback);
57
+ if (isInitialized) {
58
+ callback(cachedValue);
59
+ }
60
+ return () => {
61
+ const index = subscribers.indexOf(callback);
62
+ if (index > -1) {
63
+ subscribers.splice(index, 1);
64
+ }
65
+ };
66
+ },
67
+ /**
68
+ * Clean up computed value and stop tracking
69
+ */
70
+ dispose() {
71
+ disposed = true;
72
+ cleanupEffect();
73
+ subscribers.length = 0;
74
+ isInitialized = false;
75
+ isInComputation = false;
76
+ }
77
+ };
78
+ }
79
+ function watch(store, key, callback) {
80
+ if (!store.$subscribe) {
81
+ throw new Error("store must be created with state()");
82
+ }
83
+ return store.$subscribe(key, callback);
84
+ }
85
+ function defaultFocusPreservation(container) {
86
+ const activeEl = document.activeElement;
87
+ const shouldRestore = container.contains(activeEl);
88
+ if (!shouldRestore) return null;
89
+ let selectionStart = null;
90
+ let selectionEnd = null;
91
+ if (activeEl.tagName === "INPUT" || activeEl.tagName === "TEXTAREA") {
92
+ selectionStart = activeEl.selectionStart;
93
+ selectionEnd = activeEl.selectionEnd;
94
+ }
95
+ return () => {
96
+ if (document.body.contains(activeEl)) {
97
+ activeEl.focus();
98
+ if (selectionStart !== null && selectionEnd !== null) {
99
+ activeEl.setSelectionRange(selectionStart, selectionEnd);
100
+ }
101
+ }
102
+ };
103
+ }
104
+ function defaultScrollPreservation(container, context = {}) {
105
+ const { isReorder = false } = context;
106
+ const scrollTop = container.scrollTop;
107
+ if (scrollTop === 0) {
108
+ return () => {
109
+ container.scrollTop = 0;
110
+ };
111
+ }
112
+ let anchorElement = null;
113
+ let anchorOffset = 0;
114
+ if (!isReorder) {
115
+ const containerRect = container.getBoundingClientRect();
116
+ for (let child = container.firstElementChild; child; child = child.nextElementSibling) {
117
+ const rect = child.getBoundingClientRect();
118
+ if (rect.bottom > containerRect.top) {
119
+ anchorElement = child;
120
+ anchorOffset = rect.top - containerRect.top;
121
+ break;
122
+ }
123
+ }
124
+ }
125
+ return () => {
126
+ if (anchorElement && document.body.contains(anchorElement)) {
127
+ const newRect = anchorElement.getBoundingClientRect();
128
+ const containerRect = container.getBoundingClientRect();
129
+ const currentOffset = newRect.top - containerRect.top;
130
+ const scrollAdjustment = currentOffset - anchorOffset;
131
+ container.scrollTop = container.scrollTop + scrollAdjustment;
132
+ } else {
133
+ container.scrollTop = scrollTop;
134
+ }
135
+ };
136
+ }
137
+ function repeat(container, store, arrayKey, options) {
138
+ const {
139
+ key,
140
+ render,
141
+ create,
142
+ update,
143
+ element = "div",
144
+ preserveFocus = defaultFocusPreservation,
145
+ preserveScroll = defaultScrollPreservation
146
+ } = options;
147
+ const containerEl = typeof container === "string" ? document.querySelector(container) : container;
148
+ if (!containerEl) {
149
+ logWarn(`[Lume.js] repeat(): container "${container}" not found`);
150
+ return () => {
151
+ };
152
+ }
153
+ if (typeof key !== "function") {
154
+ throw new Error("[Lume.js] repeat(): options.key must be a function");
155
+ }
156
+ if (typeof render !== "function" && typeof create !== "function") {
157
+ throw new Error("[Lume.js] repeat(): options.render or options.create must be a function");
158
+ }
159
+ const elementsByKey = /* @__PURE__ */ new Map();
160
+ const prevItemsByKey = /* @__PURE__ */ new Map();
161
+ const prevIndexByKey = /* @__PURE__ */ new Map();
162
+ const seenKeys = /* @__PURE__ */ new Set();
163
+ function createElement() {
164
+ return typeof element === "function" ? element() : document.createElement(element);
165
+ }
166
+ function reconcileDOM(container2, nextEls) {
167
+ let ptr = container2.firstChild;
168
+ for (let i = 0; i < nextEls.length; i++) {
169
+ const desired = nextEls[i];
170
+ if (ptr === desired) {
171
+ ptr = ptr.nextSibling;
172
+ continue;
173
+ }
174
+ container2.insertBefore(desired, ptr);
175
+ }
176
+ while (ptr) {
177
+ const next = ptr.nextSibling;
178
+ container2.removeChild(ptr);
179
+ ptr = next;
180
+ }
181
+ }
182
+ function applyPreservation(container2, fn, isReorder) {
183
+ const shouldPreserve = document.body.contains(container2);
184
+ const restoreFocus = shouldPreserve && preserveFocus ? preserveFocus(container2) : null;
185
+ const restoreScroll = shouldPreserve && preserveScroll ? preserveScroll(container2, { isReorder }) : null;
186
+ fn();
187
+ if (restoreFocus) restoreFocus();
188
+ if (restoreScroll) restoreScroll();
189
+ }
190
+ function updateList() {
191
+ const items = store[arrayKey];
192
+ if (!Array.isArray(items)) {
193
+ logWarn(`[Lume.js] repeat(): store.${arrayKey} is not an array`);
194
+ return;
195
+ }
196
+ let isReorder = false;
197
+ if (preserveScroll) {
198
+ const previousKeys = new Set(elementsByKey.keys());
199
+ const currentKeys = new Set(items.map((item) => key(item)));
200
+ isReorder = previousKeys.size === currentKeys.size && [...previousKeys].every((k) => currentKeys.has(k));
201
+ }
202
+ seenKeys.clear();
203
+ const nextKeys = /* @__PURE__ */ new Set();
204
+ const nextEls = [];
205
+ for (let i = 0; i < items.length; i++) {
206
+ const item = items[i];
207
+ const k = key(item);
208
+ if (seenKeys.has(k)) {
209
+ logWarn(`[Lume.js] repeat(): duplicate key "${k}"`);
210
+ continue;
211
+ }
212
+ seenKeys.add(k);
213
+ nextKeys.add(k);
214
+ let el = elementsByKey.get(k);
215
+ const isFirstRender = !el;
216
+ if (isFirstRender) {
217
+ el = createElement();
218
+ elementsByKey.set(k, el);
219
+ }
220
+ try {
221
+ if (isFirstRender && create) {
222
+ create(item, el, i);
223
+ }
224
+ const prevItem = prevItemsByKey.get(k);
225
+ const prevIndex = prevIndexByKey.get(k);
226
+ if (update) {
227
+ if (prevItem !== item || prevIndex !== i) {
228
+ update(item, el, i, { isFirstRender });
229
+ }
230
+ } else if (render) {
231
+ render(item, el, i);
232
+ }
233
+ prevItemsByKey.set(k, item);
234
+ prevIndexByKey.set(k, i);
235
+ } catch (err) {
236
+ logError(`[Lume.js] repeat(): error rendering key "${k}":`, err);
237
+ }
238
+ nextEls.push(el);
239
+ }
240
+ applyPreservation(containerEl, () => {
241
+ reconcileDOM(containerEl, nextEls);
242
+ if (elementsByKey.size !== nextKeys.size) {
243
+ for (const k of elementsByKey.keys()) {
244
+ if (!nextKeys.has(k)) {
245
+ elementsByKey.delete(k);
246
+ prevItemsByKey.delete(k);
247
+ prevIndexByKey.delete(k);
248
+ }
249
+ }
250
+ }
251
+ }, isReorder);
252
+ }
253
+ let unsubscribe;
254
+ if (typeof store.$subscribe === "function") {
255
+ unsubscribe = store.$subscribe(arrayKey, updateList);
256
+ } else if (typeof store.subscribe === "function") {
257
+ const subResult = store.subscribe(() => updateList());
258
+ updateList();
259
+ unsubscribe = typeof subResult === "function" ? subResult : () => {
260
+ subResult?.unsubscribe?.();
261
+ };
262
+ } else {
263
+ updateList();
264
+ logWarn("[Lume.js] repeat(): store is not reactive (no $subscribe or subscribe method)");
265
+ return () => {
266
+ containerEl.replaceChildren();
267
+ elementsByKey.clear();
268
+ prevItemsByKey.clear();
269
+ prevIndexByKey.clear();
270
+ seenKeys.clear();
271
+ };
272
+ }
273
+ return () => {
274
+ if (typeof unsubscribe === "function") {
275
+ unsubscribe();
276
+ }
277
+ containerEl.replaceChildren();
278
+ elementsByKey.clear();
279
+ prevItemsByKey.clear();
280
+ prevIndexByKey.clear();
281
+ seenKeys.clear();
282
+ };
283
+ }
284
+ let globalEnabled = true;
285
+ let globalFilter = null;
286
+ const stats = /* @__PURE__ */ new Map();
287
+ function matchesFilter(key) {
288
+ if (globalFilter === null) return true;
289
+ if (typeof globalFilter === "string") {
290
+ return key.includes(globalFilter);
291
+ }
292
+ if (globalFilter instanceof RegExp) {
293
+ return globalFilter.test(key);
294
+ }
295
+ return true;
296
+ }
297
+ function getStats(label) {
298
+ if (!stats.has(label)) {
299
+ stats.set(label, {
300
+ gets: /* @__PURE__ */ new Map(),
301
+ sets: /* @__PURE__ */ new Map(),
302
+ notifies: /* @__PURE__ */ new Map()
303
+ });
304
+ }
305
+ return stats.get(label);
306
+ }
307
+ function incrementStat(label, type, key) {
308
+ const s = getStats(label);
309
+ const map = s[type];
310
+ map.set(key, (map.get(key) || 0) + 1);
311
+ }
312
+ const MAX_LOG_LEN = 100;
313
+ const TRUNCATED_LEN = MAX_LOG_LEN - 3;
314
+ function formatValue(value) {
315
+ try {
316
+ const json = JSON.stringify(value);
317
+ if (json.length > MAX_LOG_LEN) {
318
+ return json.slice(0, TRUNCATED_LEN) + "...";
319
+ }
320
+ return json;
321
+ } catch {
322
+ return String(value);
323
+ }
324
+ }
325
+ function createDebugPlugin(options = {}) {
326
+ const label = options.label ?? "store";
327
+ const getOpt = (name, defaultVal) => {
328
+ const val = options[name];
329
+ return val !== void 0 ? val : defaultVal;
330
+ };
331
+ return {
332
+ name: `debug:${label}`,
333
+ onInit: () => {
334
+ if (globalEnabled) {
335
+ console.log(`%c[${label}]%c initialized`, "color: #888; font-weight: bold", "color: inherit");
336
+ }
337
+ },
338
+ onGet: (key, value) => {
339
+ if (typeof key === "string" && key.startsWith("$")) {
340
+ return value;
341
+ }
342
+ incrementStat(label, "gets", key);
343
+ if (globalEnabled && getOpt("logGet", false) && matchesFilter(key)) {
344
+ console.log(
345
+ `%c[${label}]%c GET %c${key}%c = ${formatValue(value)}`,
346
+ "color: #888; font-weight: bold",
347
+ "color: #4CAF50",
348
+ "color: #2196F3; font-weight: bold",
349
+ "color: inherit"
350
+ );
351
+ }
352
+ return value;
353
+ },
354
+ onSet: (key, newValue, oldValue) => {
355
+ if (typeof key === "string" && key.startsWith("$")) {
356
+ return newValue;
357
+ }
358
+ incrementStat(label, "sets", key);
359
+ if (globalEnabled && getOpt("logSet", true) && matchesFilter(key)) {
360
+ console.log(
361
+ `%c[${label}]%c SET %c${key}%c: ${formatValue(oldValue)} → ${formatValue(newValue)}`,
362
+ "color: #888; font-weight: bold",
363
+ "color: #FF9800",
364
+ "color: #2196F3; font-weight: bold",
365
+ "color: inherit"
366
+ );
367
+ if (getOpt("trace", false)) {
368
+ console.trace(`%c[${label}] Stack trace for ${key}`, "color: #888");
369
+ }
370
+ }
371
+ return newValue;
372
+ },
373
+ onSubscribe: (key) => {
374
+ if (globalEnabled && matchesFilter(key)) {
375
+ console.log(
376
+ `%c[${label}]%c SUBSCRIBE %c${key}`,
377
+ "color: #888; font-weight: bold",
378
+ "color: #9C27B0",
379
+ "color: #2196F3; font-weight: bold"
380
+ );
381
+ }
382
+ },
383
+ onNotify: (key, value) => {
384
+ if (typeof key === "string" && key.startsWith("$")) {
385
+ return;
386
+ }
387
+ incrementStat(label, "notifies", key);
388
+ if (globalEnabled && getOpt("logNotify", true) && matchesFilter(key)) {
389
+ console.log(
390
+ `%c[${label}]%c NOTIFY %c${key}%c = ${formatValue(value)}`,
391
+ "color: #888; font-weight: bold",
392
+ "color: #E91E63",
393
+ "color: #2196F3; font-weight: bold",
394
+ "color: inherit"
395
+ );
396
+ }
397
+ }
398
+ };
399
+ }
400
+ const debug = {
401
+ /**
402
+ * Enable debug logging globally
403
+ */
404
+ enable() {
405
+ globalEnabled = true;
406
+ console.log("%c[lume-debug]%c Logging enabled", "color: #888; font-weight: bold", "color: #4CAF50");
407
+ },
408
+ /**
409
+ * Disable debug logging globally
410
+ */
411
+ disable() {
412
+ globalEnabled = false;
413
+ console.log("%c[lume-debug]%c Logging disabled", "color: #888; font-weight: bold", "color: #F44336");
414
+ },
415
+ /**
416
+ * Check if debug logging is currently enabled
417
+ * @returns {boolean}
418
+ */
419
+ isEnabled() {
420
+ return globalEnabled;
421
+ },
422
+ /**
423
+ * Filter logs by key pattern
424
+ * @param {string|RegExp|null} pattern - Pattern to match, or null to clear filter
425
+ */
426
+ filter(pattern) {
427
+ globalFilter = pattern;
428
+ if (pattern === null) {
429
+ console.log("%c[lume-debug]%c Filter cleared", "color: #888; font-weight: bold", "color: inherit");
430
+ } else {
431
+ console.log(`%c[lume-debug]%c Filter set: ${pattern}`, "color: #888; font-weight: bold", "color: inherit");
432
+ }
433
+ },
434
+ /**
435
+ * Get current filter pattern
436
+ * @returns {string|RegExp|null}
437
+ */
438
+ getFilter() {
439
+ return globalFilter;
440
+ },
441
+ /**
442
+ * Get statistics data (silent - no console output)
443
+ * Use logStats() if you want to see stats in console.
444
+ * @returns {object} Stats object for programmatic access
445
+ */
446
+ stats() {
447
+ const result = {};
448
+ for (const [label, data] of stats) {
449
+ result[label] = {
450
+ gets: Object.fromEntries(data.gets),
451
+ sets: Object.fromEntries(data.sets),
452
+ notifies: Object.fromEntries(data.notifies)
453
+ };
454
+ }
455
+ return result;
456
+ },
457
+ /**
458
+ * Log statistics summary to console (with formatting)
459
+ * @returns {object} Stats object for programmatic access
460
+ */
461
+ logStats() {
462
+ const result = this.stats();
463
+ if (Object.keys(result).length === 0) {
464
+ console.log("%c[lume-debug]%c No stats collected yet", "color: #888; font-weight: bold", "color: inherit");
465
+ return result;
466
+ }
467
+ console.group("%c[lume-debug] Statistics", "color: #888; font-weight: bold");
468
+ for (const [label, data] of Object.entries(result)) {
469
+ console.group(`%c${label}`, "color: #2196F3; font-weight: bold");
470
+ const tableData = [];
471
+ const allKeys = /* @__PURE__ */ new Set([
472
+ ...Object.keys(data.gets),
473
+ ...Object.keys(data.sets),
474
+ ...Object.keys(data.notifies)
475
+ ]);
476
+ for (const key of allKeys) {
477
+ tableData.push({
478
+ key,
479
+ gets: data.gets[key] || 0,
480
+ sets: data.sets[key] || 0,
481
+ notifies: data.notifies[key] || 0
482
+ });
483
+ }
484
+ if (tableData.length > 0) {
485
+ console.table(tableData);
486
+ }
487
+ console.groupEnd();
488
+ }
489
+ console.groupEnd();
490
+ return result;
491
+ },
492
+ /**
493
+ * Reset all collected statistics
494
+ */
495
+ resetStats() {
496
+ stats.clear();
497
+ console.log("%c[lume-debug]%c Stats reset", "color: #888; font-weight: bold", "color: inherit");
498
+ }
499
+ };
500
+ function withPlugins(store, plugins = []) {
501
+ if (!plugins.length) return store;
502
+ for (const p of plugins) {
503
+ try {
504
+ p.onInit?.();
505
+ } catch (e) {
506
+ logError(`[Lume.js] Plugin "${p.name}" error in onInit:`, e);
507
+ }
508
+ }
509
+ const pendingNotifications = /* @__PURE__ */ new Map();
510
+ function runNotifyHooks() {
511
+ for (const [key, value] of pendingNotifications) {
512
+ for (const p of plugins) {
513
+ try {
514
+ p.onNotify?.(key, value);
515
+ } catch (e) {
516
+ logError(`[Lume.js] Plugin "${p.name}" error in onNotify:`, e);
517
+ }
518
+ }
519
+ }
520
+ pendingNotifications.clear();
521
+ }
522
+ if (typeof store.$beforeFlush === "function") {
523
+ store.$beforeFlush(runNotifyHooks);
524
+ }
525
+ return new Proxy(store, {
526
+ get(target, key) {
527
+ if (typeof key === "string" && key.startsWith("$")) {
528
+ const method = target[key];
529
+ if (key === "$subscribe" && typeof method === "function") {
530
+ return (subKey, fn) => {
531
+ for (const p of plugins) {
532
+ try {
533
+ p.onSubscribe?.(subKey);
534
+ } catch (e) {
535
+ logError(`[Lume.js] Plugin "${p.name}" error in onSubscribe:`, e);
536
+ }
537
+ }
538
+ return method(subKey, fn);
539
+ };
540
+ }
541
+ return method;
542
+ }
543
+ let value = target[key];
544
+ for (const p of plugins) {
545
+ try {
546
+ const r = p.onGet?.(key, value);
547
+ if (r !== void 0) value = r;
548
+ } catch (e) {
549
+ logError(`[Lume.js] Plugin "${p.name}" error in onGet:`, e);
550
+ }
551
+ }
552
+ return value;
553
+ },
554
+ set(target, key, value) {
555
+ const oldValue = target[key];
556
+ let newValue = value;
557
+ for (const p of plugins) {
558
+ try {
559
+ const r = p.onSet?.(key, newValue, oldValue);
560
+ if (r !== void 0) newValue = r;
561
+ } catch (e) {
562
+ logError(`[Lume.js] Plugin "${p.name}" error in onSet:`, e);
563
+ }
564
+ }
565
+ if (!Object.is(newValue, oldValue)) {
566
+ pendingNotifications.set(key, newValue);
567
+ }
568
+ target[key] = newValue;
569
+ return true;
570
+ }
571
+ });
572
+ }
573
+ function isReactive(obj) {
574
+ return !!(obj && typeof obj === "object" && typeof obj.$subscribe === "function");
575
+ }
576
+ export {
577
+ computed,
578
+ createDebugPlugin,
579
+ debug,
580
+ defaultFocusPreservation,
581
+ defaultScrollPreservation,
582
+ isReactive,
583
+ repeat,
584
+ watch,
585
+ withPlugins
586
+ };
587
+ //# sourceMappingURL=addons.mjs.map