lume-js 2.0.0-alpha.2 → 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,28 +4,26 @@
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-alpha.2
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--alpha.2-orange.svg)](package.json)
13
- [![Tests](https://img.shields.io/badge/tests-193%20passing-brightgreen.svg)](tests/)
14
- [![Size](https://img.shields.io/badge/size-%3C2KB-blue.svg)](dist/)
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
 
18
-
19
- > **Note:** The `repeat` addon is *experimental* in v1.0.0. Its API may evolve in future releases as it is refined to best fit Lume.js philosophy.
20
-
21
18
  | Feature | Lume.js | Alpine.js | Vue | React |
22
19
  |---------|---------|-----------|-----|-------|
23
20
  | Custom Syntax | ❌ No | ✅ `x-data` | ✅ `v-bind` | ✅ JSX |
24
21
  | Build Step | ❌ Optional | ❌ Optional | ⚠️ Recommended | ✅ Required |
25
- | Bundle Size | ~2KB | ~15KB | ~35KB | ~45KB |
22
+ | Bundle Size | ~2.15KB | ~15KB | ~35KB | ~45KB |
26
23
  | HTML Validation | ✅ Pass | ⚠️ Warnings | ⚠️ Warnings | ❌ JSX |
24
+ | Extensible Handlers | ✅ | ❌ Built-in only | ❌ Built-in only | N/A |
27
25
 
28
- **Lume.js is essentially "Modern Knockout.js" - standards-only reactivity for 2025.**
26
+ **Lume.js is "Modern Knockout.js" standards-only reactivity for the modern web.**
29
27
 
30
28
  ---
31
29
 
@@ -35,14 +33,7 @@ Minimal reactive state management using only standard JavaScript and HTML. No cu
35
33
 
36
34
  ```html
37
35
  <script type="module">
38
- import { state, bindDom, effect } from 'https://cdn.jsdelivr.net/npm/lume-js/src/index.js';
39
- </script>
40
- ```
41
-
42
- **Version Pinning:**
43
- ```html
44
- <script type="module">
45
- import { state } from 'https://cdn.jsdelivr.net/npm/lume-js@1.0.0/src/index.js';
36
+ import { state, bindDom, effect } from 'https://cdn.jsdelivr.net/npm/lume-js/dist/index.min.mjs';
46
37
  </script>
47
38
  ```
48
39
 
@@ -57,7 +48,16 @@ import { state, bindDom } from 'lume-js';
57
48
  ```
58
49
 
59
50
  ### Browser Support
60
- 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`.
61
61
 
62
62
  ---
63
63
 
@@ -75,17 +75,106 @@ Works in all modern browsers (Chrome 49+, Firefox 18+, Safari 10+, Edge 79+). **
75
75
  ```javascript
76
76
  import { state, bindDom } from 'lume-js';
77
77
 
78
- // 1. Create state
79
78
  const store = state({ name: 'World' });
80
-
81
- // 2. Bind to DOM
82
79
  bindDom(document.body, store);
83
80
  ```
84
81
 
85
- **What just happened?**
86
- 1. **`state()`** created a reactive object.
87
- 2. **`bindDom()`** scanned the document for `data-bind="name"`.
88
- 3. It set up a two-way binding: typing in the input updates the state, and the state updates the text.
82
+ That's it — two-way binding, no build step, valid HTML.
83
+
84
+ ---
85
+
86
+ ## Built-in Reactive Attributes
87
+
88
+ `bindDom()` supports these `data-*` attributes out of the box:
89
+
90
+ ```html
91
+ <!-- Two-way binding (inputs) / one-way (text elements) -->
92
+ <input data-bind="name">
93
+ <span data-bind="name"></span>
94
+
95
+ <!-- Boolean attributes -->
96
+ <div data-hidden="isLoading">Content</div>
97
+ <button data-disabled="isSubmitting">Submit</button>
98
+ <input data-checked="isAgreed" type="checkbox">
99
+ <input data-required="fieldRequired">
100
+
101
+ <!-- ARIA attributes -->
102
+ <button data-aria-expanded="menuOpen">Menu</button>
103
+ <div data-aria-hidden="isCollapsed">Panel</div>
104
+ ```
105
+
106
+ ---
107
+
108
+ ## Extensible Handler System
109
+
110
+ Need more reactive attributes? Import handlers or create your own — no core modification needed.
111
+
112
+ ```javascript
113
+ import { state, bindDom } from 'lume-js';
114
+ import { show, classToggle, stringAttr } from 'lume-js/handlers';
115
+
116
+ const store = state({
117
+ isVisible: true,
118
+ isActive: false,
119
+ profileUrl: '/user/alice'
120
+ });
121
+
122
+ bindDom(document.body, store, {
123
+ handlers: [show, classToggle('active'), stringAttr('href')]
124
+ });
125
+ ```
126
+
127
+ ```html
128
+ <span data-show="isVisible">Visible when truthy</span>
129
+ <div data-class-active="isActive">Toggles 'active' class</div>
130
+ <a data-href="profileUrl">Profile</a>
131
+ ```
132
+
133
+ ### Available Handlers (`lume-js/handlers`)
134
+
135
+ | Handler | HTML Example | Effect |
136
+ |---------|-------------|--------|
137
+ | `show` | `data-show="key"` | Shows element when truthy (`el.hidden = !val`) |
138
+ | `boolAttr(name)` | `data-readonly="key"` | Toggles any boolean attribute |
139
+ | `ariaAttr(name)` | `data-aria-pressed="key"` | Sets ARIA attribute to "true"/"false" |
140
+ | `classToggle(...names)` | `data-class-active="key"` | Toggles CSS classes |
141
+ | `stringAttr(name)` | `data-href="key"` | Sets string attributes (removes on null) |
142
+
143
+ ### Presets
144
+
145
+ ```javascript
146
+ import { formHandlers, a11yHandlers } from 'lume-js/handlers';
147
+
148
+ // formHandlers: [boolAttr('readonly')]
149
+ // a11yHandlers: [ariaAttr('pressed'), ariaAttr('selected'), ariaAttr('disabled')]
150
+ ```
151
+
152
+ ### Custom Handlers
153
+
154
+ Any plain object with `attr` and `apply` works:
155
+
156
+ ```javascript
157
+ const tooltip = {
158
+ attr: 'data-tooltip',
159
+ apply(el, val) { el.title = val ?? ''; }
160
+ };
161
+
162
+ bindDom(root, store, { handlers: [tooltip] });
163
+ ```
164
+
165
+ ---
166
+
167
+ ## Addons
168
+
169
+ Import only what you need from `lume-js/addons`:
170
+
171
+ ```javascript
172
+ import { computed, watch, repeat } from 'lume-js/addons';
173
+ ```
174
+
175
+ - **`computed(fn)`** — Cached derived values with auto-tracking
176
+ - **`watch(store, key, fn)`** — Subscribe to state changes
177
+ - **`repeat(container, store, key, options)`** — Keyed list rendering with element reuse
89
178
 
90
179
  ---
91
180
 
@@ -93,15 +182,19 @@ bindDom(document.body, store);
93
182
 
94
183
  Full documentation is available in the [docs/](docs/) directory:
95
184
 
96
- - **[Tutorial: Build a Todo App](docs/tutorials/build-todo-app.md)**
97
- - **[Tutorial: Build Tic-Tac-Toe](docs/tutorials/build-tic-tac-toe.md)**
98
- - **[Working with Arrays](docs/tutorials/working-with-arrays.md)**
185
+ - **Tutorials**
186
+ - [Build a Todo App](docs/tutorials/build-todo-app.md)
187
+ - [Build Tic-Tac-Toe](docs/tutorials/build-tic-tac-toe.md)
188
+ - [Working with Arrays](docs/tutorials/working-with-arrays.md)
99
189
  - **API Reference**
100
- - [Core (state, bindDom)](docs/api/core/state.md)
101
- - [Effect System](docs/api/core/effect.md)
102
- - [Plugins (v2.0+)](docs/api/core/plugins.md)
103
- - [Addons (computed, repeat, debug)](docs/api/addons/computed.md)
104
- - **[Design Philosophy](docs/design/design-decisions.md)**
190
+ - [state()](docs/api/core/state.md) — Reactive state
191
+ - [bindDom()](docs/api/core/bindDom.md) — DOM binding
192
+ - [effect()](docs/api/core/effect.md) — Reactive effects
193
+ - [Handlers](docs/api/core/handlers.md) — Extensible attribute handlers
194
+ - [Plugins](docs/api/core/plugins.md) — State extension system
195
+ - [Addons](docs/api/addons/computed.md) — computed, watch, repeat, debug
196
+ - **Design**
197
+ - [Design Decisions](docs/design/design-decisions.md)
105
198
 
106
199
  ---
107
200
 
@@ -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};