sprae 12.3.9 → 12.4.1

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 (79) hide show
  1. package/core.js +240 -59
  2. package/directive/_.js +8 -0
  3. package/directive/class.js +9 -0
  4. package/directive/each.js +41 -17
  5. package/directive/else.js +6 -2
  6. package/directive/event.js +9 -0
  7. package/directive/fx.js +5 -0
  8. package/directive/hidden.js +6 -0
  9. package/directive/html.js +29 -6
  10. package/directive/if.js +7 -2
  11. package/directive/portal.js +38 -0
  12. package/directive/ref.js +8 -0
  13. package/directive/scope.js +7 -0
  14. package/directive/sequence.js +9 -1
  15. package/directive/spread.js +6 -0
  16. package/directive/style.js +9 -0
  17. package/directive/text.js +5 -0
  18. package/directive/value.js +13 -3
  19. package/dist/sprae.js +3 -6
  20. package/dist/sprae.js.map +4 -4
  21. package/dist/sprae.umd.js +3 -6
  22. package/dist/sprae.umd.js.map +4 -4
  23. package/package.json +36 -8
  24. package/readme.md +39 -54
  25. package/signal.js +41 -3
  26. package/sprae.js +114 -25
  27. package/store.js +54 -13
  28. package/types/core.d.ts +222 -0
  29. package/types/core.d.ts.map +1 -0
  30. package/types/directive/_.d.ts +3 -0
  31. package/types/directive/_.d.ts.map +1 -0
  32. package/types/directive/aria.d.ts.map +1 -0
  33. package/types/directive/class.d.ts +3 -0
  34. package/types/directive/class.d.ts.map +1 -0
  35. package/types/directive/data.d.ts.map +1 -0
  36. package/types/directive/each.d.ts +6 -0
  37. package/types/directive/each.d.ts.map +1 -0
  38. package/types/directive/else.d.ts +3 -0
  39. package/types/directive/else.d.ts.map +1 -0
  40. package/types/directive/event.d.ts +5 -0
  41. package/types/directive/event.d.ts.map +1 -0
  42. package/types/directive/fx.d.ts +3 -0
  43. package/types/directive/fx.d.ts.map +1 -0
  44. package/types/directive/hidden.d.ts +3 -0
  45. package/types/directive/hidden.d.ts.map +1 -0
  46. package/types/directive/html.d.ts +3 -0
  47. package/types/directive/html.d.ts.map +1 -0
  48. package/types/directive/if.d.ts +3 -0
  49. package/types/directive/if.d.ts.map +1 -0
  50. package/types/directive/item.d.ts.map +1 -0
  51. package/types/directive/portal.d.ts +3 -0
  52. package/types/directive/portal.d.ts.map +1 -0
  53. package/types/directive/ref.d.ts +5 -0
  54. package/types/directive/ref.d.ts.map +1 -0
  55. package/types/directive/resize.d.ts.map +1 -0
  56. package/types/directive/scope.d.ts +3 -0
  57. package/types/directive/scope.d.ts.map +1 -0
  58. package/types/directive/sequence.d.ts +5 -0
  59. package/types/directive/sequence.d.ts.map +1 -0
  60. package/types/directive/spread.d.ts +3 -0
  61. package/types/directive/spread.d.ts.map +1 -0
  62. package/types/directive/style.d.ts +3 -0
  63. package/types/directive/style.d.ts.map +1 -0
  64. package/types/directive/text.d.ts +3 -0
  65. package/types/directive/text.d.ts.map +1 -0
  66. package/types/directive/value.d.ts +4 -0
  67. package/types/directive/value.d.ts.map +1 -0
  68. package/types/signal.d.ts +6 -0
  69. package/types/signal.d.ts.map +1 -0
  70. package/types/sprae.d.ts +15 -0
  71. package/types/sprae.d.ts.map +1 -0
  72. package/types/store.d.ts +15 -0
  73. package/types/store.d.ts.map +1 -0
  74. package/directive/aria.js +0 -0
  75. package/directive/data.js +0 -0
  76. package/directive/item.js +0 -0
  77. package/dist/sprae.micro.js +0 -6
  78. package/dist/sprae.micro.js.map +0 -7
  79. package/micro.js +0 -56
package/readme.md CHANGED
@@ -1,82 +1,63 @@
1
- # [∴](https://dy.github.io/sprae) spræ [![tests](https://github.com/dy/sprae/actions/workflows/node.js.yml/badge.svg)](https://github.com/dy/sprae/actions/workflows/node.js.yml) [![npm bundle size](https://img.shields.io/bundlephobia/minzip/sprae?color=white)](https://bundlephobia.com/package/sprae) [![npm](https://img.shields.io/npm/v/sprae?color=white)](https://www.npmjs.com/package/sprae) [![ॐ](https://img.shields.io/badge/MIT-%E0%A5%90-white)](https://krishnized.github.io/license)
1
+ # [∴](https://dy.github.io/sprae) spræ [![tests](https://github.com/dy/sprae/actions/workflows/node.js.yml/badge.svg)](https://github.com/dy/sprae/actions/workflows/node.js.yml) ![size](https://img.shields.io/badge/size-~6kb-white) [![npm](https://img.shields.io/npm/v/sprae?color=white)](https://www.npmjs.com/package/sprae)
2
2
 
3
- Ræctive sprinkles for HTML/JSX tree
3
+ Ræctive sprinkles for HTML/JSX
4
4
 
5
5
  ## usage
6
6
 
7
7
  ```html
8
- <div id="counter" :scope="{count: 1}">
9
- <p :text="`Clicked ${count} times`"></p>
10
- <button :onclick="count++">Click me</button>
11
- </div>
12
-
13
- <script type="module">
14
- import sprae from '//unpkg.com/sprae?module'
15
-
16
- const state = sprae(document.getElementById('counter'), { count: 0 })
17
- state.count++
18
- </script>
8
+ <!-- Tabs -->
9
+ <nav :scope="{tab: 'A'}">
10
+ <button :class="{active: tab=='A'}" :onclick="tab='A'">A</button>
11
+ <button :class="{active: tab=='B'}" :onclick="tab='B'">B</button>
12
+ <section :if="tab=='A'">Content A</section>
13
+ <section :if="tab=='B'">Content B</section>
14
+ </nav>
15
+
16
+ <!-- Filter -->
17
+ <input :scope="{q: ''}" :value="q" :oninput="q=e.target.value" placeholder="Search...">
18
+ <ul :each="item in items.filter(i => i.includes(q))">
19
+ <li :text="item"></li>
20
+ </ul>
21
+
22
+ <script type="module" src="//unpkg.com/sprae"></script>
19
23
  ```
20
24
 
21
25
  ## [docs](docs.md)
22
26
 
23
27
  <!-- [start](docs.md#start)  [store](docs.md#store)  [signals](docs.md#signals)  [evaluator](docs.md#evaluator)  [jsx](docs.md#jsx)  [build](docs.md#custom-build)  [hints](docs.md#hints) -->
24
28
 
25
- [`:text`](docs.md#text) [`:class`](docs.md#class) [`:style`](docs.md#style) [`:value`](docs.md#value) [`:<attr>`](docs.md#attr-) [`:if :else`](docs.md#if-else) [`:each`](docs.md#each) [`:scope`](docs.md#scope) [`:fx`](docs.md#fx) [`:ref`](docs.md#ref) [`:on<event>`](docs.md#onevent)
29
+ [`:text`](docs.md#text) [`:class`](docs.md#class) [`:style`](docs.md#style) [`:value`](docs.md#value) [`:<attr>`](docs.md#attr-) [`:if :else`](docs.md#if-else) [`:each`](docs.md#each) [`:scope`](docs.md#scope) [`:fx`](docs.md#fx) [`:ref`](docs.md#ref) [`:hidden`](docs.md#hidden) [`:portal`](docs.md#portal) [`:on<event>`](docs.md#onevent)
26
30
 
27
31
  [`.debounce`](docs.md#debounce-ms) [`.throttle`](docs.md#throttle-ms) [`.delay`](docs.md#tick) [`.once`](docs.md#once)<br>
28
- [`.window`](docs.md#window-document-body-root-parent-outside-self) [`.document`](docs.md#window-document-body-root-parent-outside-self) [`.root`](docs.md#window-document-body-root-parent-outside-self) [`.body`](docs.md#window-document-body-root-parent-outside-self) [`.parent`](docs.md#window-document-body-root-parent-outside-self) [`.self`](docs.md#window-document-body-root-parent-outside-self) [`.outside`](docs.md#window-document-body-root-parent-outside-self)<br>
32
+ [`.window`](docs.md#window-document-body-root-parent-away-self) [`.document`](docs.md#window-document-body-root-parent-away-self) [`.root`](docs.md#window-document-body-root-parent-away-self) [`.body`](docs.md#window-document-body-root-parent-away-self) [`.parent`](docs.md#window-document-body-root-parent-away-self) [`.self`](docs.md#window-document-body-root-parent-away-self) [`.away`](docs.md#window-document-body-root-parent-away-self)<br>
29
33
  [`.passive`](docs.md#passive-captureevents-only) [`.capture`](docs.md#passive-captureevents-only) [`.prevent`](docs.md#prevent-stop-immediateevents-only) [`.stop`](docs.md#prevent-stop-immediateevents-only) [`.<key>`](docs.md#key-filters)
30
34
 
31
35
 
32
- <!--
33
- ## Micro
34
-
35
- Micro sprae version is 2.5kb bundle with essentials:
36
+ ## vs alpine
36
37
 
37
- * no multieffects `:a:b`
38
- * no modifiers `:a.x.y`
39
- * no sequences `:ona..onb`
40
- * no `:each`, `:if`, `:value`
41
- -->
38
+ | | [alpine](alpine.md) | sprae |
39
+ |------------------|--------|-------|
40
+ | _size_ | ~16kb | ~6kb |
41
+ | _performance_ | ~2× slower | 1.00× |
42
+ | _CSP_ | limited | full |
43
+ | _reactivity_ | custom | [signals](docs.md#signals) |
44
+ | _sandboxing_ | no | yes |
45
+ | _typescript_ | partial | full |
46
+ | _JSX/SSR_ | no | [yes](docs.md#jsx) |
47
+ | _prefix_ | `x-`, `:`, `@` | `:` or [custom](docs.md#custom-build) |
42
48
 
43
- <!-- ## used by
49
+ <sup>[benchmark](https://krausest.github.io/js-framework-benchmark/current.html). CSP via [jessie](docs.md#evaluator).</sup>
44
50
 
45
- [wavearea](), [maetr](), [settings-panel]() -->
46
51
 
47
- ## why
52
+ ## used by
48
53
 
49
- Wire UI in markup for cleaner app logic.<br>
50
- <!--Perfect for SPA, PWA, static sites, prototypes, micro-frontends, lightweight UI.<br> -->
51
- <!--Inspired by [preact-signals](https://github.com/preactjs/signals), [alpine](https://github.com/alpinejs/alpine), [lodash](https://lodash.com) and <span title="petite-vue, lucia, nuejs, hmpl, unpoly, dagger">others</span>. <!--[petite-vue](https://github.com/vuejs/petite-vue) and others. -->
52
- <!-- [lucia](https://github.com/aidenybai/lucia), [nuejs](https://github.com/nuejs/nuejs), [hmpl](https://github.com/hmpl-language/hmpl), [unpoly](https://unpoly.com/up.link), [dagger](https://github.com/dagger8224/dagger.js) -->
53
-
54
- <!-- Made with 🫰 for better DX. -->
55
- <!-- – for those who tired of complexity. -->
54
+ [watr](https://dy.github.io/watr/play), [wavearea](https://dy.github.io/wavearea)
55
+ <!-- , [maetr](), [settings-panel]() -->
56
56
 
57
57
 
58
58
  <!--
59
- | | [AlpineJS](https://github.com/alpinejs/alpine) | [Petite-Vue](https://github.com/vuejs/petite-vue) | Sprae |
60
- |-----------------------|-------------------|-------------------|------------------|
61
- | _Size_ | ~10KB | ~6KB | ~5KB |
62
- | _Memory_ | 5.05 | 3.16 | 2.78 |
63
- | _Performance_ | 2.64 | 2.43 | 1.76 |
64
- | _CSP_ | Limited | No | Yes |
65
- | _SSR_ | No | No | No |
66
- | _Evaluation_ | [`new AsyncFunction`](https://github.com/alpinejs/alpine/blob/main/packages/alpinejs/src/evaluator.js#L81) | [`new Function`](https://github.com/vuejs/petite-vue/blob/main/src/eval.ts#L20) | [`new Function`]() / [justin](https://github.com/dy/subscript) |
67
- | _Reactivity_ | `Alpine.store` | _@vue/reactivity_ | _signals_ |
68
- | _Sandboxing_ | No | No | Yes |
69
- | _Directives_ | `:`, `x-`, `{}` | `:`, `v-`, `@`, `{}` | `:` |
70
- | _Magic_ | `$data` | `$app` | - |
71
- | _Fragments_ | Yes | No | Yes |
72
- | _Plugins_ | Yes | No | Yes |
73
- | _Modifiers_ | Yes | No | Yes |
74
-
75
- _Nested directives_ Yes
76
- _Inline directives_ Yes
77
- -->
59
+ [lucia](https://github.com/aidenybai/lucia), [nuejs](https://github.com/nuejs/nuejs), [hmpl](https://github.com/hmpl-language/hmpl), [unpoly](https://unpoly.com/up.link), [dagger](https://github.com/dagger8224/dagger.js), [petite-vue](https://github.com/vuejs/petite-vue)
78
60
 
79
- <!--
80
61
  ### Drops
81
62
 
82
63
  * ToDo MVC: [demo](https://dy.github.io/sprae/examples/todomvc), [code](https://github.com/dy/sprae/blob/main/examples/todomvc.html)
@@ -85,3 +66,7 @@ _Inline directives_ Yes
85
66
  * Carousel: [demo](https://rwdevelopment.github.io/sprae_js_carousel/), [code](https://github.com/RWDevelopment/sprae_js_carousel)
86
67
  * Tabs: [demo](https://rwdevelopment.github.io/sprae_js_tabs/), [code](https://github.com/RWDevelopment/sprae_js_tabs?tab=readme-ov-file)-->
87
68
  <!-- * Prostogreen [demo](https://web-being.org/prostogreen/), [code](https://github.com/web-being/prostogreen/) -->
69
+
70
+ <p align='center'>
71
+ <a href="https://krishnized.github.io/license">ॐ</a>
72
+ </p>
package/signal.js CHANGED
@@ -1,8 +1,22 @@
1
- // preact-signals minimal implementation
2
- let current, depth = 0, batched;
1
+ /**
2
+ * @fileoverview Minimal signals implementation (preact-signals compatible)
3
+ * @module sprae/signal
4
+ */
3
5
 
4
- // default signals impl
6
+ /** @type {import('./core.js').EffectFn | null} */
7
+ let current
5
8
 
9
+ let depth = 0
10
+
11
+ /** @type {Set<import('./core.js').EffectFn> | null} */
12
+ let batched;
13
+
14
+ /**
15
+ * Creates a reactive signal.
16
+ * @template T
17
+ * @param {T} v - Initial value
18
+ * @returns {import('./core.js').Signal<T>}
19
+ */
6
20
  export const signal = (v, _s, _obs = new Set, _v = () => _s.value) => (
7
21
  _s = {
8
22
  get value() {
@@ -19,6 +33,11 @@ export const signal = (v, _s, _obs = new Set, _v = () => _s.value) => (
19
33
  }
20
34
  )
21
35
 
36
+ /**
37
+ * Creates a reactive effect that re-runs when dependencies change.
38
+ * @param {() => void | (() => void)} fn - Effect function, may return cleanup
39
+ * @returns {() => void} Dispose function
40
+ */
22
41
  export const effect = (fn, _teardown, _fx, _deps) => (
23
42
  _fx = (prev) => {
24
43
  let tmp = _teardown;
@@ -35,6 +54,12 @@ export const effect = (fn, _teardown, _fx, _deps) => (
35
54
  (dep) => { _teardown?.call?.(); for (dep of _deps) dep.delete(_fx); _deps.clear() }
36
55
  )
37
56
 
57
+ /**
58
+ * Creates a computed signal derived from other signals.
59
+ * @template T
60
+ * @param {() => T} fn - Computation function
61
+ * @returns {import('./core.js').Signal<T>}
62
+ */
38
63
  export const computed = (fn, _s = signal(), _c, _e, _v = () => _c.value) => (
39
64
  _c = {
40
65
  get value() {
@@ -46,10 +71,23 @@ export const computed = (fn, _s = signal(), _c, _e, _v = () => _c.value) => (
46
71
  }
47
72
  )
48
73
 
74
+ /**
75
+ * Batches multiple signal updates into a single notification.
76
+ * @template T
77
+ * @param {() => T} fn - Function containing updates
78
+ * @returns {T}
79
+ */
49
80
  export const batch = (fn, _first = !batched, _list) => {
50
81
  batched ??= new Set;
51
82
  try { fn(); }
52
83
  finally { if (_first) { [batched, _list] = [null, batched]; for (const fx of _list) fx(); } }
53
84
  }
54
85
 
86
+ /**
87
+ * Runs a function without tracking dependencies.
88
+ * @template T
89
+ * @param {() => T} fn - Function to run untracked
90
+ * @returns {T}
91
+ */
55
92
  export const untracked = (fn, _prev, _v) => (_prev = current, current = null, _v = fn(), current = _prev, _v)
93
+
package/sprae.js CHANGED
@@ -1,3 +1,8 @@
1
+ /**
2
+ * @fileoverview Sprae - lightweight reactive HTML templating library
3
+ * @module sprae
4
+ */
5
+
1
6
  import store from "./store.js";
2
7
  import { batch, computed, effect, signal, untracked } from './core.js';
3
8
  import * as signals from './signal.js';
@@ -17,6 +22,9 @@ import _default from "./directive/_.js";
17
22
  import _spread from "./directive/spread.js";
18
23
  import _event from "./directive/event.js";
19
24
  import _seq from "./directive/sequence.js";
25
+ import _html from "./directive/html.js";
26
+ import _portal from "./directive/portal.js";
27
+ import _hidden from "./directive/hidden.js";
20
28
 
21
29
 
22
30
  Object.assign(directive, {
@@ -24,6 +32,7 @@ Object.assign(directive, {
24
32
  '': _spread,
25
33
  class: _class,
26
34
  text: _text,
35
+ html: _html,
27
36
  style: _style,
28
37
  fx: _fx,
29
38
  value: _value,
@@ -31,14 +40,20 @@ Object.assign(directive, {
31
40
  scope: _scope,
32
41
  if: _if,
33
42
  else: _else,
34
- each: _each
43
+ each: _each,
44
+ portal: _portal,
45
+ hidden: _hidden
35
46
  })
36
47
 
37
48
 
38
49
  /**
39
- * Directive initializer (with modifiers support)
40
- * @type {(el: HTMLElement, name:string, value:string, state:Object) => Function}
41
- * */
50
+ * Directive initializer with modifiers support.
51
+ * @param {Element} target - Target element
52
+ * @param {string} name - Directive name with modifiers (e.g., 'onclick.throttle-500')
53
+ * @param {string} expr - Expression string
54
+ * @param {Object} state - Reactive state object
55
+ * @returns {() => (() => void) | void} Initializer function that returns a disposer
56
+ */
42
57
  const dir = (target, name, expr, state) => {
43
58
  let [dirName, ...mods] = name.split('.'), create = directive[dirName] || directive._
44
59
 
@@ -64,37 +79,68 @@ const dir = (target, name, expr, state) => {
64
79
  }
65
80
  }
66
81
 
67
- Object.assign(modifier, {
68
- // timing (lodash-like)
69
- // FIXME: add immediate param
70
- debounce: (fn, _how) => debounce(fn, (_how ||= 0, !_how ? undefined : _how === 'raf' ? requestAnimationFrame : (fn) => setTimeout(fn, _how))),
71
- throttle: (fn, _how) => throttle(fn, (_how ||= 0, !_how ? undefined : _how === 'raf' ? requestAnimationFrame : (fn) => setTimeout(fn, _how))),
72
- delay: (fn, ms) => !ms ? (e) => (queueMicrotask(() => fn(e))) : (e) => setTimeout(() => fn(e), ms),
82
+ // Parses time string to ms: 100, 100ms, 1s, 1m
83
+ const parseTime = (t) => !t ? 0 : typeof t === 'number' ? t :
84
+ (([, n, u] = t.match(/^(\d+)(ms|s|m)?$/) || []) => (n = +n, u === 's' ? n * 1000 : u === 'm' ? n * 60000 : n))()
73
85
 
74
- tick: (fn) => (console.warn('Deprecated'), (e) => (queueMicrotask(() => fn(e)))),
75
- raf: (fn) => (console.warn('Deprecated'), (e) => requestAnimationFrame(() => fn(e))),
86
+ // Creates scheduler from time/keyword (idle, raf, tick, or ms)
87
+ const scheduler = (t) =>
88
+ t === 'idle' ? requestIdleCallback :
89
+ t === 'raf' ? requestAnimationFrame :
90
+ !t || t === 'tick' ? queueMicrotask :
91
+ (fn) => setTimeout(fn, parseTime(t))
76
92
 
93
+ // Built-in modifiers for timing, targeting, and event handling
94
+ Object.assign(modifier, {
95
+ /**
96
+ * Delays callback by interval since last call (trailing edge).
97
+ * Supports: tick (default), raf, idle, N, Nms, Ns, Nm. Add -immediate for leading edge.
98
+ * Examples: .debounce, .debounce-100, .debounce-1s, .debounce-raf, .debounce-idle, .debounce-100-immediate
99
+ */
100
+ debounce: (fn, a, b) => debounce(fn, scheduler(a === 'immediate' ? b : a), a === 'immediate' || b === 'immediate'),
101
+ /**
102
+ * Limits callback rate to interval (leading + trailing edges).
103
+ * Supports: tick (default), raf, idle, N, Nms, Ns, Nm.
104
+ * Examples: .throttle, .throttle-100, .throttle-1s, .throttle-raf, .throttle-idle
105
+ */
106
+ throttle: (fn, a) => throttle(fn, scheduler(a)),
107
+ /** Runs callback after delay. Supports: tick (default), raf, idle, N, Nms, Ns, Nm. */
108
+ delay: (fn, a) => ((sched = scheduler(a)) => (e) => sched(() => fn(e)))(),
109
+
110
+ /** Calls handler only once. */
77
111
  once: (fn, _done, _fn) => (_fn = (e) => !_done && (_done = 1, fn(e)), _fn.once = true, _fn),
78
112
 
79
- // target
113
+ /** Attaches event listener to window. */
80
114
  window: fn => (fn.target = fn.target.ownerDocument.defaultView, fn),
115
+ /** Attaches event listener to document. */
81
116
  document: fn => (fn.target = fn.target.ownerDocument, fn),
117
+ /** Attaches event listener to document root element (<html>). */
82
118
  root: fn => (fn.target = fn.target.ownerDocument.documentElement, fn),
119
+ /** Attaches event listener to body. */
83
120
  body: fn => (fn.target = fn.target.ownerDocument.body, fn),
121
+ /** Attaches event listener to parent element. */
84
122
  parent: fn => (fn.target = fn.target.parentNode, fn),
123
+ /** Triggers only when event target is the element itself. */
85
124
  self: (fn) => (e) => (e.target === fn.target && fn(e)),
125
+ /** Triggers when click is outside the element. */
86
126
  away: (fn) => Object.assign((e) => (!fn.target.contains(e.target) && e.target.isConnected && fn(e)), {target: fn.target.ownerDocument}),
87
127
 
88
- // events
128
+ /** Calls preventDefault() before handler. */
89
129
  prevent: (fn) => (e) => (e?.preventDefault(), fn(e)),
130
+ /** Calls stopPropagation() or stopImmediatePropagation() (with -immediate). */
90
131
  stop: (fn, _how) => (e) => (_how?.[0] === 'i' ? e?.stopImmediatePropagation() : e?.stopPropagation(), fn(e)),
91
- immediate: (fn) => (console.warn('Deprecated'), (e) => (e?.stopImmediatePropagation(), fn(e))),
132
+ /** Sets passive option for event listener. */
92
133
  passive: fn => (fn.passive = true, fn),
134
+ /** Sets capture option for event listener. */
93
135
  capture: fn => (fn.capture = true, fn),
94
136
  })
137
+ /** Alias for .away modifier */
95
138
  modifier.outside = modifier.away
96
139
 
97
- // key testers
140
+ /**
141
+ * Key testers for keyboard event modifiers.
142
+ * @type {Record<string, (e: KeyboardEvent) => boolean>}
143
+ */
98
144
  const keys = {
99
145
  ctrl: e => e.ctrlKey || e.key === "Control" || e.key === "Ctrl",
100
146
  shift: e => e.shiftKey || e.key === "Shift",
@@ -112,12 +158,37 @@ const keys = {
112
158
  char: e => /^\S$/.test(e.key),
113
159
  };
114
160
 
115
- // augment modifiers with key testers
116
- for (let k in keys) modifier[k] = (fn, a, b) => (e) => keys[k](e) && (!a || keys[a]?.(e)) && (!b || keys[b]?.(e)) && fn(e)
161
+ // match key by name, or by e.key (case-insensitive), or by keyCode (digits)
162
+ const keyMatch = (k, e) => keys[k]?.(e) || e.key.toLowerCase() === k || e.keyCode == k
163
+
164
+ // Augment modifiers with key testers (e.g., .enter, .ctrl, .ctrl-a, .ctrl-65)
165
+ for (let k in keys) modifier[k] = (fn, a, b) => (e) => keys[k](e) && (!a || keyMatch(a, e)) && (!b || keyMatch(b, e)) && fn(e)
117
166
 
118
167
 
168
+ // Checks for first-level semicolons (statement vs expression)
169
+ const hasSemi = s => {
170
+ for (let d=0,i=0;i<s.length;i++) {
171
+ if (s[i]=='{') d++
172
+ else if (s[i]=='}') d--
173
+ else if (s[i]==';' && !d) return true
174
+ }
175
+ return false
176
+ }
177
+
178
+ // Configure sprae with default compiler and signals
119
179
  use({
120
- compile: expr => sprae.constructor(`with(arguments[0]){${expr}}`),
180
+
181
+ // Default compiler wraps expression for new Function
182
+ compile: expr => {
183
+ // if, const, let - no return
184
+ if (/^(if|let|const)\b/.test(expr));
185
+ // first-level semicolons - no return
186
+ else if (hasSemi(expr));
187
+ else expr = `return ${expr}`
188
+ // async expression
189
+ if (/\bawait\s/.test(expr)) expr = `return (async()=>{${expr}})()`
190
+ return sprae.constructor(`with(arguments[0]){${expr}}`)
191
+ },
121
192
  dir: (el, name, expr, state) => {
122
193
  // sequences shortcut
123
194
  if (name.includes('..')) return () => _seq(el, state, expr, name)[_dispose]
@@ -130,15 +201,32 @@ use({
130
201
  })
131
202
 
132
203
 
133
- // expose for runtime config
204
+ // Expose for runtime configuration
134
205
  sprae.use = use
135
206
  sprae.store = store
136
207
  sprae.directive = directive
137
208
  sprae.modifier = modifier
138
209
 
210
+ /**
211
+ * Disposes a spraed element, cleaning up all effects and state.
212
+ * @param {Element} el - Element to dispose
213
+ */
214
+ sprae.dispose = (el) => el[_dispose]?.()
215
+
139
216
 
140
217
  /**
141
- * Lifecycle hanger: spraes automatically any new nodes
218
+ * Auto-initializes sprae on dynamically added elements.
219
+ * Uses MutationObserver to detect new DOM nodes and apply directives.
220
+ *
221
+ * @param {Element} [root=document.body] - Root element to observe
222
+ * @param {Object} [values] - Initial state values
223
+ * @returns {Object} The reactive state object
224
+ *
225
+ * @example
226
+ * ```js
227
+ * // Auto-init on page load
228
+ * sprae.start(document.body, { count: 0 })
229
+ * ```
142
230
  */
143
231
  const start = sprae.start = (root = document.body, values) => {
144
232
  const state = store(values)
@@ -150,10 +238,9 @@ const start = sprae.start = (root = document.body, values) => {
150
238
  if (el.nodeType === 1 && el[_state] === undefined && root.contains(el)) {
151
239
  // even if element has no spraeable attrs, some of its children can have
152
240
  root[_add](el)
153
- // sprae(el, state, root);
154
241
  }
155
242
  }
156
- // for (const el of m.removedNodes) el[Symbol.dispose]?.()
243
+ for (const el of m.removedNodes) el.nodeType === 1 && el[_dispose]?.()
157
244
  }
158
245
  });
159
246
  mo.observe(root, { childList: true, subtree: true });
@@ -161,8 +248,10 @@ const start = sprae.start = (root = document.body, values) => {
161
248
  }
162
249
 
163
250
 
164
- // version placeholder for bundler
251
+ /** Package version (injected by bundler) */
165
252
  sprae.version = "[VI]{{inject}}[/VI]"
166
253
 
254
+ const dispose = sprae.dispose
255
+
167
256
  export default sprae
168
- export { sprae, store, signal, effect, computed, batch, untracked, start, use }
257
+ export { sprae, store, signal, effect, computed, batch, untracked, start, use, throttle, debounce, dispose }
package/store.js CHANGED
@@ -1,19 +1,43 @@
1
- // signals-based proxy
1
+ /**
2
+ * @fileoverview Signals-powered reactive proxy store
3
+ * @module sprae/store
4
+ */
5
+
2
6
  import { signal, computed, batch, untracked } from './core.js'
3
7
 
8
+ /** Symbol for accessing the internal signals map */
9
+ export const _signals = Symbol('signals')
10
+
11
+ /** Symbol for the change signal that tracks object keys or array length */
12
+ export const _change = Symbol('change')
4
13
 
5
- // _signals allows both storing signals and checking instance, which would be difficult with WeakMap
6
- export const _signals = Symbol('signals'),
7
- // _change is a signal that tracks changes to the object keys or array length
8
- _change = Symbol('change'),
9
- // _set is stashed setter for computed values
10
- _set = Symbol('set')
14
+ /** Symbol for stashed setter on computed values */
15
+ export const _set = Symbol('set')
11
16
 
12
17
  // a hack to simulate sandbox for `with` in evaluator
13
18
  let sandbox = true
14
19
 
15
- // object store is not lazy
16
- // parent defines parent scope or sandbox
20
+ /**
21
+ * Reactive store with signals backing.
22
+ * @template T
23
+ * @typedef {T & { [_signals]: Record<string | symbol, import('./core.js').Signal<any>> }} ReactiveStore
24
+ */
25
+
26
+ /**
27
+ * Creates a reactive proxy store from an object or array.
28
+ * Properties become signals for fine-grained reactivity.
29
+ * Supports nested objects, arrays, computed getters, and methods.
30
+ *
31
+ * @template {Object} T
32
+ * @param {T} values - Initial values object
33
+ * @param {Object} [parent] - Parent scope for inheritance
34
+ * @returns {ReactiveStore<T>} Reactive proxy store
35
+ *
36
+ * @example
37
+ * const state = store({ count: 0, get doubled() { return this.count * 2 } })
38
+ * state.count = 5 // triggers updates
39
+ * state.doubled // 10 (computed)
40
+ */
17
41
  export const store = (values, parent) => {
18
42
  if (!values) return values
19
43
 
@@ -41,7 +65,7 @@ export const store = (values, parent) => {
41
65
  return parent ? parent[k] : (typeof globalThis[k] === 'function' && !globalThis[k].prototype ? globalThis[k].bind(globalThis) : globalThis[k])
42
66
  },
43
67
 
44
- set: (_, k, v, _s) => {
68
+ set: (_, k, v) => {
45
69
  // console.group('SET', k, v)
46
70
  if (k in signals) return set(signals, k, v), 1
47
71
 
@@ -99,7 +123,13 @@ export const store = (values, parent) => {
99
123
  return state
100
124
  }
101
125
 
102
- // array store - signals are lazy since arrays can be very large & expensive
126
+ /**
127
+ * Creates a reactive array store with lazy signal initialization.
128
+ * Arrays can be large, so signals are created on-demand.
129
+ * @param {any[]} values - Initial array values
130
+ * @param {Object} [parent=globalThis] - Parent scope
131
+ * @returns {ReactiveStore<any[]>} Reactive array proxy
132
+ */
103
133
  const list = (values, parent = globalThis) => {
104
134
 
105
135
  // gotta fill with null since proto methods like .reduce may fail
@@ -168,10 +198,21 @@ const list = (values, parent = globalThis) => {
168
198
  return state
169
199
  }
170
200
 
171
- // create signal value, skip untracked
201
+ /**
202
+ * Creates a signal for a property value.
203
+ * Skips wrapping for untracked props (underscore prefix) and existing signals.
204
+ * @param {Object} signals - Signals storage object
205
+ * @param {string} k - Property key
206
+ * @param {any} v - Property value
207
+ */
172
208
  const create = (signals, k, v) => (signals[k] = (k[0] == '_' || v?.peek) ? v : signal(store(v)))
173
209
 
174
- // set/update signal value
210
+ /**
211
+ * Updates a signal value, handling arrays specially for efficient patching.
212
+ * @param {Object} signals - Signals storage object
213
+ * @param {string} k - Property key
214
+ * @param {any} v - New value
215
+ */
175
216
  const set = (signals, k, v, _s, _v) => {
176
217
  // skip unchanged (although can be handled by last condition - we skip a few checks this way)
177
218
  return k[0] === '_' ? (signals[k] = v) :