sprae 12.3.9 → 12.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.
Files changed (79) hide show
  1. package/core.js +236 -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 +35 -8
  24. package/readme.md +39 -54
  25. package/signal.js +41 -3
  26. package/sprae.js +117 -21
  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-~5kb-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 | ~5kb |
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,75 @@ const dir = (target, name, expr, state) => {
64
79
  }
65
80
  }
66
81
 
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))()
85
+
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))
92
+
93
+ // Built-in modifiers for timing, targeting, and event handling
67
94
  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),
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)))(),
73
109
 
110
+ /** @deprecated Use .delay instead */
74
111
  tick: (fn) => (console.warn('Deprecated'), (e) => (queueMicrotask(() => fn(e)))),
112
+ /** @deprecated Use .throttle-raf instead */
75
113
  raf: (fn) => (console.warn('Deprecated'), (e) => requestAnimationFrame(() => fn(e))),
76
114
 
115
+ /** Calls handler only once. */
77
116
  once: (fn, _done, _fn) => (_fn = (e) => !_done && (_done = 1, fn(e)), _fn.once = true, _fn),
78
117
 
79
- // target
118
+ /** Attaches event listener to window. */
80
119
  window: fn => (fn.target = fn.target.ownerDocument.defaultView, fn),
120
+ /** Attaches event listener to document. */
81
121
  document: fn => (fn.target = fn.target.ownerDocument, fn),
122
+ /** Attaches event listener to document root element (<html>). */
82
123
  root: fn => (fn.target = fn.target.ownerDocument.documentElement, fn),
124
+ /** Attaches event listener to body. */
83
125
  body: fn => (fn.target = fn.target.ownerDocument.body, fn),
126
+ /** Attaches event listener to parent element. */
84
127
  parent: fn => (fn.target = fn.target.parentNode, fn),
128
+ /** Triggers only when event target is the element itself. */
85
129
  self: (fn) => (e) => (e.target === fn.target && fn(e)),
130
+ /** Triggers when click is outside the element. */
86
131
  away: (fn) => Object.assign((e) => (!fn.target.contains(e.target) && e.target.isConnected && fn(e)), {target: fn.target.ownerDocument}),
87
132
 
88
- // events
133
+ /** Calls preventDefault() before handler. */
89
134
  prevent: (fn) => (e) => (e?.preventDefault(), fn(e)),
135
+ /** Calls stopPropagation() or stopImmediatePropagation() (with -immediate). */
90
136
  stop: (fn, _how) => (e) => (_how?.[0] === 'i' ? e?.stopImmediatePropagation() : e?.stopPropagation(), fn(e)),
137
+ /** @deprecated Use .stop-immediate instead */
91
138
  immediate: (fn) => (console.warn('Deprecated'), (e) => (e?.stopImmediatePropagation(), fn(e))),
139
+ /** Sets passive option for event listener. */
92
140
  passive: fn => (fn.passive = true, fn),
141
+ /** Sets capture option for event listener. */
93
142
  capture: fn => (fn.capture = true, fn),
94
143
  })
144
+ /** Alias for .away modifier */
95
145
  modifier.outside = modifier.away
96
146
 
97
- // key testers
147
+ /**
148
+ * Key testers for keyboard event modifiers.
149
+ * @type {Record<string, (e: KeyboardEvent) => boolean>}
150
+ */
98
151
  const keys = {
99
152
  ctrl: e => e.ctrlKey || e.key === "Control" || e.key === "Ctrl",
100
153
  shift: e => e.shiftKey || e.key === "Shift",
@@ -112,12 +165,37 @@ const keys = {
112
165
  char: e => /^\S$/.test(e.key),
113
166
  };
114
167
 
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)
168
+ // match key by name, or by e.key (case-insensitive), or by keyCode (digits)
169
+ const keyMatch = (k, e) => keys[k]?.(e) || e.key.toLowerCase() === k || e.keyCode == k
170
+
171
+ // Augment modifiers with key testers (e.g., .enter, .ctrl, .ctrl-a, .ctrl-65)
172
+ 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
173
 
118
174
 
175
+ // Checks for first-level semicolons (statement vs expression)
176
+ const hasSemi = s => {
177
+ for (let d=0,i=0;i<s.length;i++) {
178
+ if (s[i]=='{') d++
179
+ else if (s[i]=='}') d--
180
+ else if (s[i]==';' && !d) return true
181
+ }
182
+ return false
183
+ }
184
+
185
+ // Configure sprae with default compiler and signals
119
186
  use({
120
- compile: expr => sprae.constructor(`with(arguments[0]){${expr}}`),
187
+
188
+ // Default compiler wraps expression for new Function
189
+ compile: expr => {
190
+ // if, const, let - no return
191
+ if (/^(if|let|const)\b/.test(expr));
192
+ // first-level semicolons - no return
193
+ else if (hasSemi(expr));
194
+ else expr = `return ${expr}`
195
+ // async expression
196
+ if (/\bawait\s/.test(expr)) expr = `return (async()=>{${expr}})()`
197
+ return sprae.constructor(`with(arguments[0]){${expr}}`)
198
+ },
121
199
  dir: (el, name, expr, state) => {
122
200
  // sequences shortcut
123
201
  if (name.includes('..')) return () => _seq(el, state, expr, name)[_dispose]
@@ -130,15 +208,32 @@ use({
130
208
  })
131
209
 
132
210
 
133
- // expose for runtime config
211
+ // Expose for runtime configuration
134
212
  sprae.use = use
135
213
  sprae.store = store
136
214
  sprae.directive = directive
137
215
  sprae.modifier = modifier
138
216
 
217
+ /**
218
+ * Disposes a spraed element, cleaning up all effects and state.
219
+ * @param {Element} el - Element to dispose
220
+ */
221
+ sprae.dispose = (el) => el[_dispose]?.()
222
+
139
223
 
140
224
  /**
141
- * Lifecycle hanger: spraes automatically any new nodes
225
+ * Auto-initializes sprae on dynamically added elements.
226
+ * Uses MutationObserver to detect new DOM nodes and apply directives.
227
+ *
228
+ * @param {Element} [root=document.body] - Root element to observe
229
+ * @param {Object} [values] - Initial state values
230
+ * @returns {Object} The reactive state object
231
+ *
232
+ * @example
233
+ * ```js
234
+ * // Auto-init on page load
235
+ * sprae.start(document.body, { count: 0 })
236
+ * ```
142
237
  */
143
238
  const start = sprae.start = (root = document.body, values) => {
144
239
  const state = store(values)
@@ -150,10 +245,9 @@ const start = sprae.start = (root = document.body, values) => {
150
245
  if (el.nodeType === 1 && el[_state] === undefined && root.contains(el)) {
151
246
  // even if element has no spraeable attrs, some of its children can have
152
247
  root[_add](el)
153
- // sprae(el, state, root);
154
248
  }
155
249
  }
156
- // for (const el of m.removedNodes) el[Symbol.dispose]?.()
250
+ for (const el of m.removedNodes) el.nodeType === 1 && el[_dispose]?.()
157
251
  }
158
252
  });
159
253
  mo.observe(root, { childList: true, subtree: true });
@@ -161,8 +255,10 @@ const start = sprae.start = (root = document.body, values) => {
161
255
  }
162
256
 
163
257
 
164
- // version placeholder for bundler
258
+ /** Package version (injected by bundler) */
165
259
  sprae.version = "[VI]{{inject}}[/VI]"
166
260
 
261
+ const dispose = sprae.dispose
262
+
167
263
  export default sprae
168
- export { sprae, store, signal, effect, computed, batch, untracked, start, use }
264
+ 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) :