lume-js 0.2.1 โ†’ 0.4.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
@@ -1,51 +1,650 @@
1
- # lume-js
1
+ # Lume.js
2
2
 
3
- Minimal reactive state + DOM binding with **zero runtime** overhead.
4
- Inspired by Go-style simplicity.
3
+ **Reactivity that follows web standards.**
5
4
 
6
- ## Philosophy
7
- - โšก Zero runtime, direct DOM updates (no VDOM, no diffing).
8
- - ๐Ÿ”‘ Simple `state()` and `bindDom()` โ€” that's it.
9
- - ๐Ÿงฉ Explicit nesting (`state({ user: state({ name: "Alice" }) })`).
10
- - โœจ Works anywhere โ€” plain JS, or alongside other frameworks.
5
+ Minimal reactive state management using only standard JavaScript and HTML - no custom syntax, no build step required, no framework lock-in.
6
+
7
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
8
+ [![Version](https://img.shields.io/badge/version-0.4.0-green.svg)](package.json)
9
+
10
+ ## Why Lume.js?
11
+
12
+ - ๐ŸŽฏ **Standards-Only** - Uses only `data-*` attributes and standard JavaScript
13
+ - ๐Ÿ“ฆ **Tiny** - <2KB gzipped
14
+ - โšก **Fast** - Direct DOM updates, no virtual DOM overhead
15
+ - ๐Ÿ”ง **No Build Step** - Works directly in the browser
16
+ - ๐ŸŽจ **No Lock-in** - Works with any library (GSAP, jQuery, D3, etc.)
17
+ - โ™ฟ **Accessible** - HTML validators love it, screen readers work perfectly
18
+ - ๐Ÿงน **Clean API** - Cleanup functions prevent memory leaks
19
+
20
+ ### vs Other Libraries
21
+
22
+ | Feature | Lume.js | Alpine.js | Vue | React |
23
+ |---------|---------|-----------|-----|-------|
24
+ | Custom Syntax | โŒ No | โœ… `x-data`, `@click` | โœ… `v-bind`, `v-model` | โœ… JSX |
25
+ | Build Step | โŒ Optional | โŒ Optional | โš ๏ธ Recommended | โœ… Required |
26
+ | Bundle Size | ~2KB | ~15KB | ~35KB | ~45KB |
27
+ | HTML Validation | โœ… Pass | โš ๏ธ Warnings | โš ๏ธ Warnings | โŒ JSX |
28
+ | Cleanup API | โœ… Yes | โš ๏ธ Limited | โœ… Yes | โœ… Yes |
29
+
30
+ **Lume.js is essentially "Modern Knockout.js" - standards-only reactivity for 2025.**
11
31
 
12
- ## Install
32
+ ---
33
+
34
+ ## Installation
35
+
36
+ ### Via npm
13
37
 
14
38
  ```bash
15
39
  npm install lume-js
16
40
  ```
17
41
 
18
- ## Example
42
+ ### Via CDN
19
43
 
20
- **main.js**
21
- ```js
22
- import { state, bindDom } from "lume-js";
44
+ ```html
45
+ <script type="module">
46
+ import { state, bindDom, effect } from 'https://cdn.jsdelivr.net/npm/lume-js/src/index.js';
47
+
48
+ // For addons:
49
+ import { computed, watch } from 'https://cdn.jsdelivr.net/npm/lume-js/src/addons/index.js';
50
+ </script>
51
+ ```
52
+
53
+ ---
23
54
 
55
+ ## Quick Start
56
+
57
+ ### Examples
58
+
59
+ Run the live examples (including the new Todo app) with Vite:
60
+
61
+ ```bash
62
+ npm run dev
63
+ ```
64
+
65
+ Then open the Examples index (Vite will auto-open): `http://localhost:5173/examples/`
66
+
67
+ **HTML:**
68
+ ```html
69
+ <div>
70
+ <p>Count: <span data-bind="count"></span></p>
71
+ <input data-bind="name" placeholder="Enter name">
72
+ <p>Hello, <span data-bind="name"></span>!</p>
73
+
74
+ <button id="increment">+</button>
75
+ </div>
76
+ ```
77
+
78
+ **JavaScript:**
79
+ ```javascript
80
+ import { state, bindDom, effect } from 'lume-js';
81
+
82
+ // Create reactive state
24
83
  const store = state({
25
84
  count: 0,
26
- user: state({ name: "Alice" })
85
+ name: 'World'
27
86
  });
28
87
 
29
- bindDom(document.body, store);
88
+ // Bind to DOM (updates on state changes)
89
+ const cleanup = bindDom(document.body, store);
90
+
91
+ // Auto-update document title when name changes
92
+ const effectCleanup = effect(() => {
93
+ document.title = `Hello, ${store.name}!`;
94
+ });
30
95
 
31
- document.getElementById("inc").addEventListener("click", () => {
96
+ // Update state with standard JavaScript
97
+ document.getElementById('increment').addEventListener('click', () => {
32
98
  store.count++;
33
99
  });
34
100
 
35
- document.getElementById("changeName").addEventListener("click", () => {
36
- store.user.name = store.user.name === "Alice" ? "Bob" : "Alice";
101
+ // Cleanup when done (important!)
102
+ window.addEventListener('beforeunload', () => {
103
+ cleanup();
104
+ effectCleanup();
105
+ });
106
+ ```
107
+
108
+ That's it! No build step, no custom syntax, just HTML and JavaScript.
109
+
110
+ ---
111
+
112
+ ## Core API
113
+
114
+ ### `state(object)`
115
+
116
+ Creates a reactive state object using Proxy with automatic dependency tracking.
117
+
118
+ ```javascript
119
+ const store = state({
120
+ count: 0,
121
+ user: state({ // Nested state must be wrapped
122
+ name: 'Alice',
123
+ email: 'alice@example.com'
124
+ })
125
+ });
126
+
127
+ // Update state
128
+ store.count++;
129
+ store.user.name = 'Bob';
130
+ ```
131
+
132
+ **Features:**
133
+ - โœ… Automatic dependency tracking for effects
134
+ - โœ… Per-state microtask batching for performance
135
+ - โœ… Validates input (must be plain object)
136
+ - โœ… Only triggers updates when value actually changes
137
+ - โœ… Returns cleanup function from `$subscribe`
138
+ - โœ… Deduplicates effect runs per flush cycle
139
+
140
+ ### `effect(fn)`
141
+
142
+ Creates an effect that automatically tracks dependencies and re-runs when they change.
143
+
144
+ ```javascript
145
+ const store = state({ count: 0, name: 'Alice' });
146
+
147
+ const cleanup = effect(() => {
148
+ // Only tracks 'count' (name is not accessed)
149
+ console.log(`Count: ${store.count}`);
150
+ });
151
+
152
+ store.count = 5; // Effect re-runs
153
+ store.name = 'Bob'; // Effect does NOT re-run
154
+
155
+ cleanup(); // Stop the effect
156
+ ```
157
+
158
+ **Features:**
159
+ - โœ… Automatic dependency collection (tracks what you actually access)
160
+ - โœ… Dynamic dependencies (re-tracks on every execution)
161
+ - โœ… Returns cleanup function
162
+ - โœ… Prevents infinite recursion
163
+ - โœ… Integrates with per-state batching (no global scheduler)
164
+
165
+ **How it works:** Effects use `globalThis.__LUME_CURRENT_EFFECT__` to track which state properties are accessed during execution. When any tracked property changes, the effect is queued in that state's pending effects set and runs once in the next microtask.
166
+
167
+ ### `bindDom(root, store)`
168
+
169
+ Binds reactive state to DOM elements with `data-bind` attributes.
170
+
171
+ ```javascript
172
+ const cleanup = bindDom(document.body, store);
173
+
174
+ // Later: cleanup all bindings
175
+ cleanup();
176
+ ```
177
+
178
+ **Supports:**
179
+ - โœ… Text content: `<span data-bind="count"></span>`
180
+ - โœ… Input values: `<input data-bind="name">`
181
+ - โœ… Textareas: `<textarea data-bind="bio"></textarea>`
182
+ - โœ… Selects: `<select data-bind="theme"></select>`
183
+ - โœ… Checkboxes: `<input type="checkbox" data-bind="enabled">`
184
+ - โœ… Numbers: `<input type="number" data-bind="age">`
185
+ - โœ… Radio buttons: `<input type="radio" data-bind="choice">`
186
+ - โœ… Nested paths: `<span data-bind="user.name"></span>`
187
+
188
+ **Features:**
189
+ - โœ… Returns cleanup function
190
+ - โœ… Better error messages with `[Lume.js]` prefix
191
+ - โœ… Handles edge cases (empty bindings, invalid paths)
192
+ - โœ… Two-way binding for form inputs
193
+
194
+ ### `$subscribe(key, callback)`
195
+
196
+ Manually subscribe to state changes. Calls callback immediately with current value, then on every change.
197
+
198
+ ```javascript
199
+ const unsubscribe = store.$subscribe('count', (value) => {
200
+ console.log('Count changed:', value);
201
+
202
+ // Integrate with other libraries
203
+ if (value > 10) {
204
+ showNotification('Count is high!');
205
+ }
206
+ });
207
+
208
+ // Cleanup
209
+ unsubscribe();
210
+ ```
211
+
212
+ **Features:**
213
+ - โœ… Returns unsubscribe function
214
+ - โœ… Validates callback is a function
215
+ - โœ… Calls immediately with current value (not batched)
216
+ - โœ… Only notifies on actual value changes (via batching)
217
+
218
+ ---
219
+
220
+ ## Addons
221
+
222
+ Lume.js provides optional addons for advanced reactivity patterns. Import from `lume-js/addons`.
223
+
224
+ ### `computed(fn)`
225
+
226
+ Creates a computed value that automatically updates when its dependencies change.
227
+
228
+ ```javascript
229
+ import { state, effect } from 'lume-js';
230
+ import { computed } from 'lume-js/addons';
231
+
232
+ const store = state({ count: 5 });
233
+
234
+ const doubled = computed(() => store.count * 2);
235
+ console.log(doubled.value); // 10
236
+
237
+ store.count = 10;
238
+ // After microtask:
239
+ console.log(doubled.value); // 20 (auto-updated)
240
+
241
+ // Subscribe to changes
242
+ const unsub = doubled.subscribe(value => {
243
+ console.log('Doubled:', value);
244
+ });
245
+
246
+ // Cleanup
247
+ doubled.dispose();
248
+ unsub();
249
+ ```
250
+
251
+ **Features:**
252
+ - โœ… Automatic dependency tracking using `effect()`
253
+ - โœ… Cached values (only recomputes when dependencies change)
254
+ - โœ… Subscribe to changes with `.subscribe(callback)`
255
+ - โœ… Cleanup with `.dispose()`
256
+ - โœ… Error handling (sets to undefined on error)
257
+
258
+ ### `watch(store, key, callback)`
259
+
260
+ Alias for `$subscribe` - observes changes to a specific state key.
261
+
262
+ ```javascript
263
+ import { state } from 'lume-js';
264
+ import { watch } from 'lume-js/addons';
265
+
266
+ const store = state({ count: 0 });
267
+
268
+ const unwatch = watch(store, 'count', (value) => {
269
+ console.log('Count is now:', value);
270
+ });
271
+
272
+ // Cleanup
273
+ unwatch();
274
+ ```
275
+
276
+ **Note:** `watch()` is just a convenience wrapper around `store.$subscribe()`. Use whichever feels more natural.
277
+
278
+ ---
279
+
280
+ ## Examples
281
+
282
+ ### Basic Counter
283
+
284
+ ```javascript
285
+ const store = state({ count: 0 });
286
+ const cleanup = bindDom(document.body, store);
287
+
288
+ document.getElementById('inc').addEventListener('click', () => {
289
+ store.count++;
37
290
  });
291
+
292
+ // Cleanup on unmount
293
+ window.addEventListener('beforeunload', () => cleanup());
38
294
  ```
39
295
 
40
- **index.html**
41
296
  ```html
42
297
  <p>Count: <span data-bind="count"></span></p>
43
- <p>User Name: <span data-bind="user.name"></span></p>
44
-
45
298
  <button id="inc">Increment</button>
46
- <button id="changeName">Change Name</button>
47
299
  ```
48
300
 
49
- ## Status
301
+ ### Form Handling with Validation
302
+
303
+ ```javascript
304
+ const form = state({
305
+ email: '',
306
+ age: 25,
307
+ theme: 'light',
308
+ errors: {}
309
+ });
310
+
311
+ const cleanup = bindDom(document.querySelector('form'), form);
312
+
313
+ // Validate on change
314
+ const unsubEmail = form.$subscribe('email', (value) => {
315
+ const isValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
316
+ form.errors = {
317
+ ...form.errors,
318
+ email: isValid ? '' : 'Invalid email'
319
+ };
320
+ });
321
+
322
+ // Cleanup
323
+ window.addEventListener('beforeunload', () => {
324
+ cleanup();
325
+ unsubEmail();
326
+ });
327
+ ```
328
+
329
+ ```html
330
+ <form>
331
+ <input type="email" data-bind="email">
332
+ <span data-bind="errors.email" style="color: red;"></span>
333
+
334
+ <input type="number" data-bind="age">
335
+
336
+ <select data-bind="theme">
337
+ <option value="light">Light</option>
338
+ <option value="dark">Dark</option>
339
+ </select>
340
+ </form>
341
+ ```
342
+
343
+ ### Nested State
344
+
345
+ ```javascript
346
+ const store = state({
347
+ user: state({
348
+ profile: state({
349
+ name: 'Alice',
350
+ bio: 'Developer'
351
+ }),
352
+ settings: state({
353
+ notifications: true
354
+ })
355
+ })
356
+ });
357
+
358
+ const cleanup = bindDom(document.body, store);
359
+
360
+ // Subscribe to nested changes
361
+ const unsub = store.user.profile.$subscribe('name', (name) => {
362
+ console.log('Profile name changed:', name);
363
+ });
364
+
365
+ // Cleanup
366
+ window.addEventListener('beforeunload', () => {
367
+ cleanup();
368
+ unsub();
369
+ });
370
+ ```
371
+
372
+ ```html
373
+ <input data-bind="user.profile.name">
374
+ <textarea data-bind="user.profile.bio"></textarea>
375
+ <input type="checkbox" data-bind="user.settings.notifications">
376
+ ```
377
+
378
+ ### Using Effects for Auto-Updates
379
+
380
+ ```javascript
381
+ import { state, effect } from 'lume-js';
382
+
383
+ const store = state({
384
+ firstName: 'Alice',
385
+ lastName: 'Smith'
386
+ });
387
+
388
+ // Auto-update title when name changes
389
+ effect(() => {
390
+ document.title = `${store.firstName} ${store.lastName}`;
391
+ });
392
+
393
+ store.firstName = 'Bob';
394
+ // Title automatically updates to "Bob Smith" in next microtask
395
+ ```
396
+
397
+ ### Computed Values
398
+
399
+ ```javascript
400
+ import { state } from 'lume-js';
401
+ import { computed } from 'lume-js/addons';
402
+
403
+ const cart = state({
404
+ items: state([
405
+ state({ price: 10, quantity: 2 }),
406
+ state({ price: 15, quantity: 1 })
407
+ ])
408
+ });
409
+
410
+ const total = computed(() => {
411
+ return cart.items.reduce((sum, item) =>
412
+ sum + (item.price * item.quantity), 0
413
+ );
414
+ });
415
+
416
+ console.log(total.value); // 35
417
+
418
+ cart.items[0].quantity = 3;
419
+ // After microtask:
420
+ console.log(total.value); // 45
421
+ ```
422
+
423
+ ### Integration with GSAP
424
+
425
+ ```javascript
426
+ import gsap from 'gsap';
427
+ import { state, effect } from 'lume-js';
428
+
429
+ const ui = state({ x: 0, y: 0 });
430
+
431
+ // Use effect for automatic animation updates
432
+ effect(() => {
433
+ gsap.to('.box', { x: ui.x, y: ui.y, duration: 0.5 });
434
+ });
435
+
436
+ // Or use $subscribe
437
+ const unsubX = ui.$subscribe('x', (value) => {
438
+ gsap.to('.box', { x: value, duration: 0.5 });
439
+ });
440
+
441
+ // Now ui.x = 100 triggers smooth animation
442
+
443
+ // Cleanup
444
+ window.addEventListener('beforeunload', () => unsubX());
445
+ ```
446
+
447
+ ### Cleanup Pattern (Important!)
448
+
449
+ ```javascript
450
+ import { state, effect, bindDom } from 'lume-js';
451
+ import { computed } from 'lume-js/addons';
452
+
453
+ const store = state({ data: [] });
454
+ const cleanup = bindDom(root, store);
455
+
456
+ const unsub1 = store.$subscribe('data', handleData);
457
+ const unsub2 = store.$subscribe('status', handleStatus);
458
+
459
+ const effectCleanup = effect(() => {
460
+ console.log('Data length:', store.data.length);
461
+ });
462
+
463
+ const total = computed(() => store.data.length * 2);
464
+
465
+ // Cleanup when component unmounts
466
+ function destroy() {
467
+ cleanup(); // Remove DOM bindings
468
+ unsub1(); // Remove subscription 1
469
+ unsub2(); // Remove subscription 2
470
+ effectCleanup(); // Stop effect
471
+ total.dispose(); // Stop computed
472
+ }
473
+
474
+ // For SPA frameworks
475
+ onUnmount(destroy);
476
+
477
+ // For vanilla JS
478
+ window.addEventListener('beforeunload', destroy);
479
+ ```
480
+
481
+ ---
482
+
483
+ ## Philosophy
484
+
485
+ ### Standards-Only
486
+ - Uses only `data-*` attributes (HTML5 standard)
487
+ - Uses only standard JavaScript APIs (Proxy, addEventListener)
488
+ - No custom syntax that breaks validators
489
+ - Works with any tool/library
490
+
491
+ ### No Artificial Limitations
492
+ ```javascript
493
+ // โœ… Use with jQuery
494
+ $('.modal').fadeIn();
495
+ store.modalOpen = true;
496
+
497
+ // โœ… Use with GSAP
498
+ gsap.to(el, { x: store.position });
499
+
500
+ // โœ… Use with any router
501
+ router.on('/home', () => store.route = 'home');
502
+
503
+ // โœ… Mix with vanilla JS
504
+ document.addEventListener('click', () => store.clicks++);
505
+ ```
506
+
507
+ **Lume.js doesn't hijack your architecture - it enhances it.**
508
+
509
+ ### Progressive Enhancement
510
+
511
+ ```html
512
+ <!-- Works without JavaScript (server-rendered) -->
513
+ <form action="/submit" method="POST">
514
+ <input name="email" value="alice@example.com">
515
+ <button type="submit">Save</button>
516
+ </form>
517
+
518
+ <script type="module">
519
+ // Enhanced when JS loads
520
+ import { state, bindDom } from 'lume-js';
521
+
522
+ const form = state({ email: 'alice@example.com' });
523
+ const cleanup = bindDom(document.querySelector('form'), form);
524
+
525
+ // Prevent default, use AJAX
526
+ document.querySelector('form').addEventListener('submit', async (e) => {
527
+ e.preventDefault();
528
+ await fetch('/submit', {
529
+ method: 'POST',
530
+ body: JSON.stringify({ email: form.email })
531
+ });
532
+ });
533
+
534
+ window.addEventListener('beforeunload', () => cleanup());
535
+ </script>
536
+ ```
537
+
538
+ ---
539
+
540
+ ## Who Should Use Lume.js?
541
+
542
+ ### โœ… Perfect For:
543
+ - WordPress/Shopify theme developers
544
+ - Accessibility-focused teams (government, healthcare, education)
545
+ - Legacy codebases that can't do full rewrites
546
+ - Developers who hate learning framework-specific syntax
547
+ - Progressive enhancement advocates
548
+ - Projects requiring HTML validation
549
+ - Adding reactivity to server-rendered apps
550
+
551
+ ### โš ๏ธ Consider Alternatives:
552
+ - **Complex SPAs** โ†’ Use React, Vue, or Svelte
553
+ - **Need routing/SSR** โ†’ Use Next.js, Nuxt, or SvelteKit
554
+ - **Prefer terse syntax** โ†’ Use Alpine.js (if custom syntax is okay)
555
+
556
+ ---
557
+
558
+ ## What's New
559
+
560
+ ### Core Features
561
+ - โœ… **Automatic dependency tracking** - `effect()` automatically tracks which state properties are accessed
562
+ - โœ… **Per-state batching** - Each state object maintains its own microtask flush for optimal performance
563
+ - โœ… **Effect deduplication** - Effects only run once per flush cycle, even if multiple dependencies change
564
+ - โœ… **TypeScript support** - Full type definitions in `index.d.ts`
565
+ - โœ… **Cleanup functions** - All reactive APIs return cleanup/unsubscribe functions
566
+
567
+ ### Addons
568
+ - โœ… **`computed()`** - Memoized computed values with automatic dependency tracking
569
+ - โœ… **`watch()`** - Convenience alias for `$subscribe`
570
+
571
+ ### API Design
572
+ - โœ… `state()` - Create reactive state with automatic tracking support
573
+ - โœ… `effect()` - Core reactivity primitive (automatic dependency collection)
574
+ - โœ… `bindDom()` - DOM binding with two-way sync for form inputs
575
+ - โœ… `$subscribe()` - Manual subscriptions (calls immediately with current value)
576
+ - โœ… All functions return cleanup/unsubscribe functions
577
+ - โœ… Better error handling with `[Lume.js]` prefix
578
+ - โœ… Input validation on all public APIs
579
+
580
+ ---
581
+
582
+ ## Browser Support
583
+
584
+ - Chrome/Edge 49+
585
+ - Firefox 18+
586
+ - Safari 10+
587
+ - No IE11 (Proxy can't be polyfilled)
588
+
589
+ **Basically: Modern browsers with ES6 Proxy support.**
590
+
591
+ ---
592
+
593
+ ## Testing
594
+
595
+ Lume.js uses [Vitest](https://vitest.dev) with a jsdom environment. The test suite mirrors the source tree: files under `tests/core/**` map to `src/core/**`, and `tests/addons/**` map to `src/addons/**`.
596
+
597
+ ```bash
598
+ # Run tests
599
+ npm test
600
+
601
+ # Watch mode
602
+ npm run test:watch
603
+
604
+ # Coverage with HTML report in ./coverage
605
+ npm run coverage
606
+ ```
607
+
608
+ **Import alias:** Tests use an alias so you can import from `src/...` without relative `../../` paths.
609
+
610
+ ```js
611
+ // vitest.config.js
612
+ resolve: {
613
+ alias: {
614
+ src: fileURLToPath(new URL('./src', import.meta.url))
615
+ }
616
+ }
617
+ ```
618
+
619
+ **Current coverage:**
620
+ - 100% statements, functions, and lines
621
+ - 100% branches (including edge-case paths)
622
+ - 37 tests covering core behavior, addons, inputs (text/checkbox/radio/number/range/select/textarea), nested state, and cleanup semantics
623
+
624
+ ---
625
+
626
+ ## Contributing
627
+
628
+ We welcome contributions! Please:
629
+
630
+ 1. **Focus on:** Examples, documentation, bug fixes, performance
631
+ 2. **Avoid:** Adding core features without discussion (keep it minimal!)
632
+ 3. **Check:** Project specification for philosophy
633
+
634
+ ---
635
+
636
+ ## License
637
+
638
+ MIT ยฉ Sathvik C
639
+
640
+ ---
641
+
642
+ ## Inspiration
643
+
644
+ Lume.js is inspired by:
645
+ - **Knockout.js** - The original `data-bind` approach
646
+ - **Alpine.js** - Minimal, HTML-first philosophy
647
+ - **Go** - Simplicity and explicit design
648
+ - **The Web Platform** - Standards over abstractions
50
649
 
51
- ๐Ÿšง Early alpha (`0.1.0`). API may change. Feedback welcome!
650
+ **Built for developers who want reactivity without the framework tax.**