sprae 13.0.1 → 13.1.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.
package/readme.md CHANGED
@@ -1,15 +1,18 @@
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)
1
+ # [∴](https://dy.github.io/sprae) sprae [![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
- Microhydration for HTML/JSX tree.
3
+ > Microhydration for HTML/JSX tree.
4
4
 
5
- ## usage
5
+ Open & minimal PE framework with signals-based reactivity.
6
+
7
+
8
+ ## Usage
6
9
 
7
10
  ```html
8
11
  <!-- Day/Night switch -->
9
- <div :scope="{ isDark: false }">
12
+ <div id="app" :scope="{ isDark: false }">
10
13
  <button :onclick="isDark = !isDark">
11
14
  <span :text="isDark ? '🌙' : '☀️'"></span>
12
- </button>
15
+ </button>
13
16
  <div :class="isDark ? 'dark' : 'light'">Welcome to Spræ!</div>
14
17
  </div>
15
18
 
@@ -20,87 +23,556 @@ Microhydration for HTML/JSX tree.
20
23
 
21
24
  <!-- default -->
22
25
  <script type="module" src="//unpkg.com/sprae"></script>
26
+ ```
23
27
 
24
- <!-- CSP-safe (no eval, uses jessie compiler) -->
25
- <!-- <script src="//unpkg.com/sprae/dist/sprae-csp.umd.js" data-start></script> -->
28
+ Or with module:
26
29
 
27
- <!-- Preact signals -->
28
- <!-- <script src="//unpkg.com/sprae/dist/sprae-preact.umd.js" data-start></script> -->
30
+ ```js
31
+ import sprae from 'sprae'
32
+
33
+ const state = sprae(document.querySelector('#app'), { count: 0 })
34
+ state.count++ // updates DOM
29
35
  ```
30
36
 
31
- <!--
32
- ## why
37
+ Sprae evaluates `:`-attributes and evaporates them, returning reactive state.
38
+
39
+
40
+ ## Directives
41
+
33
42
 
34
- The easiest harmless way to bring interactions to DOM and connect open state.
43
+ #### `:text`
35
44
 
36
- -->
45
+ Set text content.
37
46
 
38
47
  ```html
39
- <!-- Modifiers: debounce, throttle, key filters, event targets -->
40
- <input :oninput.debounce-300="search()" placeholder="Search..." />
41
- <button :onclick.once="init()">Initialize</button>
42
- <div :onkeydown.window.escape="close()">Press Esc to close</div>
43
- <form :onsubmit.prevent="save()">...</form>
48
+ <span :text="user.name">Guest</span>
49
+ <span :text="count + ' items'"></span>
50
+ <span :text="text => text.toUpperCase()">hello</span> <!-- function form -->
51
+ ```
44
52
 
45
- <!-- Observer directives -->
46
- <img :intersect.once="loadImage()" :src="placeholder" />
47
- <div :resize.throttle-100="({ width }) => narrow = width < 600"></div>
53
+
54
+ #### `:html`
55
+
56
+ Set innerHTML. Initializes directives in inserted content.
57
+
58
+ ```html
59
+ <article :html="marked(content)"></article>
60
+
61
+ <!-- template form -->
62
+ <section :html="document.querySelector('#card')"></section>
63
+
64
+ <!-- function form -->
65
+ <div :html="html => DOMPurify.sanitize(html)"></div>
66
+ ```
67
+
68
+
69
+ #### `:class`
70
+
71
+ Set classes from object, array, or string.
72
+
73
+ ```html
74
+ <div :class="{ active: isActive, disabled }"></div>
75
+ <div :class="['btn', size, variant]"></div>
76
+ <div :class="isError && 'error'"></div>
77
+
78
+ <!-- function form: extend existing -->
79
+ <div :class="cls => [...cls, 'extra']"></div>
80
+ ```
81
+
82
+
83
+ #### `:style`
84
+
85
+ Set inline styles from object or string. Supports CSS variables.
86
+
87
+ ```html
88
+ <div :style="{ color, opacity, '--size': size + 'px' }"></div>
89
+ <div :style="'color:' + color"></div>
90
+
91
+ <!-- function form -->
92
+ <div :style="style => ({ ...style, color })"></div>
93
+ ```
94
+
95
+
96
+ #### `:<attr>`, `:="{ ...attrs }"`
97
+
98
+ Set any attribute. Spread form for multiple.
99
+
100
+ ```html
101
+ <button :disabled="loading" :aria-busy="loading">Save</button>
102
+ <input :id:name="fieldName" />
103
+ <input :="{ type: 'email', required, placeholder }" />
104
+ ```
105
+
106
+
107
+ #### `:if` / `:else`
108
+
109
+ Conditional rendering. Removes element from DOM when false.
110
+
111
+ ```html
112
+ <div :if="loading">Loading...</div>
113
+ <div :else :if="error" :text="error"></div>
114
+ <div :else>Ready!</div>
115
+
116
+ <!-- fragment -->
117
+ <template :if="showDetails">
118
+ <dt>Name</dt>
119
+ <dd :text="name"></dd>
120
+ </template>
121
+ ```
122
+
123
+
124
+ #### `:each`
125
+
126
+ Iterate arrays, objects, numbers.
127
+
128
+ ```html
129
+ <li :each="item in items" :text="item.name"></li>
130
+ <li :each="item, index in items" :text="index + '. ' + item.name"></li>
131
+ <li :each="value, key in object" :text="key + ': ' + value"></li>
132
+ <li :each="n in 5" :text="'Item ' + n"></li>
133
+
134
+ <!-- filter (reactive) -->
135
+ <li :each="item in items.filter(i => i.active)" :text="item.name"></li>
136
+
137
+ <!-- fragment -->
138
+ <template :each="item in items">
139
+ <dt :text="item.term"></dt>
140
+ <dd :text="item.definition"></dd>
141
+ </template>
142
+ ```
143
+
144
+
145
+ #### `:scope`
146
+
147
+ Create local reactive state. Inherits from parent scope.
148
+
149
+ ```html
150
+ <div :scope="{ count: 0, open: false }">
151
+ <button :onclick="count++">Count: <span :text="count"></span></button>
152
+ </div>
153
+
154
+ <!-- inline variables -->
155
+ <span :scope="x = 1, y = 2" :text="x + y"></span>
156
+
157
+ <!-- access parent scope -->
158
+ <div :scope="{ local: parentValue * 2 }">...</div>
159
+
160
+ <!-- function form -->
161
+ <div :scope="scope => ({ double: scope.value * 2 })">...</div>
162
+ ```
163
+
164
+
165
+ #### `:value`
166
+
167
+ Bind state to form input (state → DOM).
168
+
169
+ ```html
170
+ <input :value="query" />
171
+ <textarea :value="content"></textarea>
172
+ <input type="checkbox" :value="agreed" />
173
+ <select :value="country">
174
+ <option :each="c in countries" :value="c.code" :text="c.name"></option>
175
+ </select>
176
+ ```
177
+
178
+
179
+ #### `:change`
180
+
181
+ Write-back from input to state (DOM → state). Handles type coercion.
182
+
183
+ ```html
184
+ <input :value="query" :change="v => query = v" />
185
+ <input type="number" :value="count" :change="v => count = v" />
186
+ <input :value="search" :change.debounce-300="v => search = v" />
187
+ ```
188
+
189
+
190
+ #### `:fx`
191
+
192
+ Run side effect. Return cleanup function for disposal.
193
+
194
+ ```html
195
+ <div :fx="console.log('count changed:', count)"></div>
196
+ <div :fx="() => {
197
+ const id = setInterval(tick, 1000)
198
+ return () => clearInterval(id)
199
+ }"></div>
200
+ ```
201
+
202
+
203
+ #### `:ref`
204
+
205
+ Store element reference in state. Function form calls with element.
206
+
207
+ ```html
208
+ <canvas :ref="canvas" :fx="draw(canvas)"></canvas>
209
+ <input :ref="el => el.focus()" />
210
+
211
+ <!-- path reference -->
212
+ <input :ref="$refs.email" />
213
+ ```
214
+
215
+ > For lifecycle hooks with setup/cleanup, use [`:mount`](#mount).
216
+
217
+
218
+ #### `:on<event>`
219
+
220
+ Attach event listeners. Chain modifiers with `.`.
221
+
222
+ ```html
223
+ <button :onclick="count++">Click</button>
224
+ <form :onsubmit.prevent="handleSubmit()">...</form>
225
+ <input :onkeydown.enter="send()" />
226
+ <input :oninput:onchange="e => validate(e)" />
227
+
228
+ <!-- sequence: setup on first event, cleanup on second -->
229
+ <div :onfocus..onblur="e => (active = true, () => active = false)"></div>
230
+ ```
231
+
232
+
233
+ #### `:hidden`
234
+
235
+ Toggle `hidden` attribute. Unlike `:if`, keeps element in DOM.
236
+
237
+ ```html
238
+ <p :hidden="!ready">Loading...</p>
239
+ ```
240
+
241
+
242
+ #### `:mount`
243
+
244
+ Lifecycle hook — runs once on connect. Not reactive. Can return cleanup.
245
+
246
+ ```html
48
247
  <canvas :mount="el => initChart(el)"></canvas>
248
+ <div :mount="el => {
249
+ const timer = setInterval(tick, 1000)
250
+ return () => clearInterval(timer)
251
+ }"></div>
252
+ ```
253
+
254
+
255
+ #### `:intersect`
256
+
257
+ IntersectionObserver wrapper. Fires on enter, or receive entry for full control.
258
+
259
+ ```html
260
+ <img :intersect.once="loadImage()" :src="placeholder" />
261
+ <div :intersect="entry => visible = entry.isIntersecting"></div>
49
262
  ```
50
263
 
51
- ## [docs](docs.md)
52
264
 
53
- #### directives
54
- [`:text`](docs.md#text) [`:class`](docs.md#class) [`:style`](docs.md#style) [`:value`](docs.md#value) [`:change`](docs.md#change) [`:<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) [`:mount`](docs.md#mount) [`:intersect`](docs.md#intersect) [`:resize`](docs.md#resize) [`:portal`](docs.md#portal) [`:on<event>`](docs.md#onevent)
265
+ #### `:resize`
266
+
267
+ ResizeObserver wrapper.
268
+
269
+ ```html
270
+ <div :resize="({width}) => cols = Math.floor(width / 200)"></div>
271
+ ```
272
+
273
+
274
+ #### `:portal`
275
+
276
+ Move element to another container.
277
+
278
+ ```html
279
+ <div :portal="'#modals'">Modal content</div>
280
+ <dialog :portal="open && '#portal-target'">...</dialog>
281
+ ```
282
+
283
+
284
+
285
+ ## Modifiers
286
+
287
+ Chain with `.` after directive name.
288
+
289
+ #### Timing
290
+
291
+ ```html
292
+ <input :oninput.debounce-300="search()" /> <!-- delay until activity stops -->
293
+ <div :onscroll.throttle-100="update()">...</div> <!-- limit frequency -->
294
+ <div :onmouseenter.delay-500="show = true" /> <!-- delay each call -->
295
+ <button :onclick.once="init()">Initialize</button>
296
+ ```
297
+
298
+ Time formats: `100` (ms), `100ms`, `1s`, `1m`, `raf`, `idle`, `tick`.
299
+ Add `-immediate` to debounce for leading edge.
300
+
301
+ #### Event targets
302
+
303
+ ```html
304
+ <div :onkeydown.window.escape="close()">...</div>
305
+ <div :onclick.self="only direct clicks"></div>
306
+ <div :onclick.away="open = false">Click outside to close</div>
307
+ ```
308
+
309
+ `.window` `.document` `.body` `.root` `.parent` `.self` `.away`
310
+
311
+ #### Event control
312
+
313
+ ```html
314
+ <a :onclick.prevent="navigate()" href="/fallback">Link</a>
315
+ <button :onclick.stop="handleClick()">Don't bubble</button>
316
+ ```
317
+
318
+ `.prevent` `.stop` `.stop-immediate` `.passive` `.capture`
319
+
320
+ #### Key filters
321
+
322
+ Filter keyboard events by key or combination.
323
+
324
+ * `.ctrl`, `.shift`, `.alt`, `.meta` — modifier keys
325
+ * `.enter`, `.esc`, `.tab`, `.space` — common keys
326
+ * `.delete` — delete or backspace
327
+ * `.arrow` — any arrow key
328
+ * `.digit` — 0-9
329
+ * `.letter` — any unicode letter
330
+ * `.char` — any non-space character
331
+ * `.ctrl-<key>`, `.alt-<key>`, `.meta-<key>`, `.shift-<key>` — combinations
332
+
333
+ ```html
334
+ <input :onkeydown.enter="submit()" />
335
+ <input :onkeydown.ctrl-s.prevent="save()" />
336
+ <input :onkeydown.shift-enter="newLine()" />
337
+ <input :onkeydown.meta-x="cut()" />
338
+ ```
339
+
340
+
341
+
342
+ ## Signals
343
+
344
+ Sprae uses signals for reactivity.
345
+
346
+ ```js
347
+ import { signal, computed, effect, batch } from 'sprae'
348
+
349
+ const count = signal(0)
350
+ const doubled = computed(() => count.value * 2)
351
+ effect(() => console.log('Count:', count.value))
352
+ count.value++
353
+ ```
354
+
355
+ ### Store
356
+
357
+ `store()` creates reactive objects from plain data. Getters become computed, `_`-prefixed properties are untracked.
358
+
359
+ ```js
360
+ import sprae, { store } from 'sprae'
361
+
362
+ const state = store({
363
+ count: 0,
364
+ items: [],
365
+ increment() { this.count++ },
366
+ get double() { return this.count * 2 },
367
+ _cache: {} // untracked
368
+ })
369
+
370
+ sprae(element, state)
371
+ state.count++ // reactive
372
+ state._cache.x = 1 // not reactive
373
+ ```
374
+
375
+ ### Alternative signals
376
+
377
+ Replace built-in signals with any preact-signals compatible library:
378
+
379
+ ```html
380
+ <script src="//unpkg.com/sprae/dist/sprae-preact.umd.js" data-start></script>
381
+ ```
382
+
383
+ ```js
384
+ import sprae from 'sprae'
385
+ import * as signals from '@preact/signals-core'
386
+ sprae.use(signals)
387
+ ```
388
+
389
+ | Library | Size | Notes |
390
+ |---------|------|-------|
391
+ | Built-in | ~300b | Default |
392
+ | [@preact/signals-core](https://github.com/preactjs/signals) | 1.5kb | Industry standard, best performance |
393
+ | [ulive](https://github.com/kethan/ulive) | 350b | Minimal |
394
+ | [signal](https://ghub.io/@webreflection/signal) | 633b | Enhanced performance. |
395
+ | [usignal](https://github.com/@webreflection/usignal) | 955b | Async effects support |
396
+
397
+
398
+ ## Configuration
399
+
400
+ ### CSP-safe evaluator
401
+
402
+ Default uses `new Function` (fast, but requires `unsafe-eval` CSP).<br/>
403
+ For strict CSP, use the pre-built variant or wire [jessie](https://github.com/dy/subscript) manually:
404
+
405
+ ```html
406
+ <script src="//unpkg.com/sprae/dist/sprae-csp.umd.js" data-start></script>
407
+ ```
408
+
409
+ ```js
410
+ import sprae from 'sprae'
411
+ import jessie from 'subscript/jessie'
412
+ sprae.use({ compile: jessie })
413
+ ```
414
+
415
+ ### Custom prefix
416
+
417
+ ```js
418
+ sprae.use({ prefix: 'data-' })
419
+ ```
420
+ ```html
421
+ <div data-text="message">...</div>
422
+ ```
423
+
424
+ ### Custom directive
425
+
426
+ ```js
427
+ import { directive, parse } from 'sprae'
428
+
429
+ directive.id = (el, state, expr) => value => el.id = value
430
+
431
+ directive.timer = (el, state, expr) => {
432
+ let id
433
+ return ms => {
434
+ clearInterval(id)
435
+ id = setInterval(() => el.textContent = Date.now(), ms)
436
+ return () => clearInterval(id)
437
+ }
438
+ }
439
+ ```
440
+
441
+ ### Custom modifier
442
+
443
+ ```js
444
+ import { modifier } from 'sprae'
445
+ modifier.log = (fn) => (e) => (console.log(e.type), fn(e))
446
+ ```
447
+
448
+
449
+
450
+ ## Integration
451
+
452
+ ### JSX / Next.js
453
+
454
+ Avoids `'use client'` — keep server components, let sprae handle client-side interactivity:
455
+
456
+ ```jsx
457
+ // layout.jsx
458
+ import Script from 'next/script'
459
+ export default function Layout({ children }) {
460
+ return <>
461
+ {children}
462
+ <Script src="https://unpkg.com/sprae" data-prefix="x-" data-start />
463
+ </>
464
+ }
465
+ ```
466
+
467
+ ```jsx
468
+ // page.jsx — server component, no 'use client' needed
469
+ export default function Page() {
470
+ return <div x-scope="{count: 0}">
471
+ <button x-onclick="count++">
472
+ Clicked <span x-text="count">0</span> times
473
+ </button>
474
+ </div>
475
+ }
476
+ ```
477
+
478
+ ### Markdown / Static Sites
479
+
480
+ Markdown processors strip `:` attributes, so use `data-` prefix:
481
+
482
+ ```html
483
+ <script src="https://unpkg.com/sprae" data-prefix="data-" data-start></script>
484
+ ```
485
+
486
+ ```md
487
+ <div data-scope="{ count: 0 }">
488
+ <button data-onclick="count++">
489
+ Clicked <span data-text="count">0</span> times
490
+ </button>
491
+ </div>
492
+ ```
493
+
494
+ Works with Jekyll, Hugo, Eleventy, Astro. Sprae site itself is built this way.
495
+
496
+
497
+ ### Server Templates
498
+
499
+ Same pattern works with PHP, Django, Rails, Jinja — server renders HTML, sprae handles client interactivity:
500
+
501
+ ```html
502
+ <script src="https://unpkg.com/sprae" data-start></script>
503
+ <div :scope="{ count: <?= $initial ?> }">
504
+ <button :onclick="count++">Count: <span :text="count"></span></button>
505
+ </div>
506
+ ```
507
+
508
+ ### Web Components
509
+
510
+ Sprae treats custom elements as boundaries — directives on the element set props, but sprae does not descend into children. The component owns its DOM.
511
+
512
+ ```html
513
+ <user-card :each="u in users" :name="u.name" :avatar="u.avatar"></user-card>
514
+ ```
515
+
516
+ Works with [define-element](https://github.com/dy/define-element), Lit, or any CE library.
517
+
518
+
519
+
520
+ ## Hints
521
+
522
+ * Prevent [FOUC](https://en.wikipedia.org/wiki/Flash_of_unstyled_content): `<style>[\:each],[\:if],[\:else]{visibility:hidden}</style>`
523
+ * Attribute order matters: `:each` before `:text`, not after.
524
+ * Async expressions work: `<div :text="await fetchData()"></div>`
525
+ * Dispose: `sprae.dispose(el)` or `el[Symbol.dispose]()`
526
+ * No `key` needed — `:each` uses direct list mapping, not DOM diffing.
527
+ * `this` refers to current element, but prefer `:ref` or `:mount` for element access.
528
+ * Properties prefixed with `_` are untracked.
529
+
530
+
531
+ ## FAQ
532
+
533
+ **What is sprae?**
534
+ : ~5kb script that adds reactivity to HTML via `:attribute="expression"`. No build step, no new syntax.
535
+
536
+ **Learning curve?**
537
+ : If you know HTML and JS, you know sprae. Just `:attribute="expression"`.
538
+
539
+ **How does it compare to Alpine?**
540
+ : 3x lighter, pluggable signals, prop modifiers, event chains. Faster in [benchmarks](https://krausest.github.io/js-framework-benchmark/).
541
+
542
+ **How does it compare to React/Vue?**
543
+ : No build step, no virtual DOM. Can inject into [JSX](#jsx--nextjs) for server components without framework overhead.
55
544
 
56
- #### modifiers
57
- [`.debounce`](docs.md#debounce-ms) [`.throttle`](docs.md#throttle-ms) [`.delay`](docs.md#tick) [`.once`](docs.md#once)<br>
58
- [`.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>
59
- [`.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)
545
+ **Why signals?**
546
+ : Signals are the emerging [standard](https://github.com/tc39/proposal-signals) for reactivity. Pluggable — first to support native signals when browsers ship.
60
547
 
61
- #### core
548
+ **Is new Function unsafe?**
549
+ : No more than inline `onclick` handlers. For strict CSP, use the [safe evaluator](#csp-safe-evaluator).
62
550
 
63
- [start](docs.md#start) [store](docs.md#store) [signals](docs.md#signals) [config](docs.md#configuration) [evaluator](docs.md#evaluator) [jsx](docs.md#jsx) [build](docs.md#custom-build) [hints](docs.md#hints)
551
+ **Components?**
552
+ : Use [define-element](https://github.com/dy/define-element) for declarative web components, or any CE library. For simpler cases, [manage duplication](https://tailwindcss.com/docs/styling-with-utility-classes#managing-duplication) with templates/includes.
64
553
 
554
+ **TypeScript?**
555
+ : Full types included.
65
556
 
66
- <!--
67
- ## vs alpine
557
+ **Browser support?**
558
+ : Any browser with [Proxy](https://caniuse.com/proxy) (all modern browsers, no IE).
68
559
 
69
- | | [alpine](alpine.md) | sprae |
70
- |------------------|--------|-------|
71
- | _size_ | ~16kb | ~6kb |
72
- | _performance_ | ~2× slower | 1.00× |
73
- | _CSP_ | limited | full |
74
- | _reactivity_ | custom | [signals](docs.md#signals) |
75
- | _sandboxing_ | no | yes |
76
- | _typescript_ | partial | full |
77
- | _JSX/SSR_ | no | [yes](docs.md#jsx) |
78
- | _prefix_ | `x-`, `:`, `@` | `:` or [custom](docs.md#custom-build) |
560
+ **Does it scale?**
561
+ : State is plain reactive objects — scales as far as your data model does. Use [store](#store) with computed getters and methods for complex apps.
79
562
 
80
- <sup>[benchmark](https://krausest.github.io/js-framework-benchmark/current.html). CSP via [jessie](docs.md#evaluator).</sup>
81
- -->
563
+ **Is it production-ready?**
564
+ : It is used by a few SaaS systems and landing pages of big guys.
82
565
 
83
- ## used by
566
+ **Is it backed by a company?**
567
+ : Indie project. [Support it](https://github.com/sponsors/dy).
84
568
 
85
- [settings-panel](https://dy.github.io/settings-panel), [wavearea](https://dy.github.io/wavearea), [watr](https://dy.github.io/watr/play) and others
86
- <!-- , [maetr]() -->
87
569
 
88
- ## alternatives
570
+ ## Used by
89
571
 
90
- <sup>[alpine](https://github.com/alpinejs/alpine), [petite-vue](https://github.com/vuejs/petite-vue), [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)</sup>
572
+ [settings-panel](https://dy.github.io/settings-panel) · [wavearea](https://dy.github.io/wavearea) · [watr](https://dy.github.io/watr/play)
91
573
 
92
- <br><br>
93
- <!--
94
- ### Drops
574
+ ## Refs
95
575
 
96
- * ToDo MVC: [demo](https://dy.github.io/sprae/examples/todomvc), [code](https://github.com/dy/sprae/blob/main/examples/todomvc.html)
97
- * JS Framework Benchmark: [demo](https://dy.github.io/sprae/examples/js-framework-benchmark), [code](https://github.com/dy/sprae/blob/main/examples/js-framework-benchmark.html)
98
- * Wavearea: [demo](https://dy.github.io/wavearea?src=//cdn.freesound.org/previews/586/586281_2332564-lq.mp3), [code](https://github.com/dy/wavearea)
99
- * Carousel: [demo](https://rwdevelopment.github.io/sprae_js_carousel/), [code](https://github.com/RWDevelopment/sprae_js_carousel)
100
- * Tabs: [demo](https://rwdevelopment.github.io/sprae_js_tabs/), [code](https://github.com/RWDevelopment/sprae_js_tabs?tab=readme-ov-file)
101
- * Prostogreen [demo](https://web-being.org/prostogreen/), [code](https://github.com/web-being/prostogreen/)
102
- -->
576
+ <sup>[alpine](https://github.com/alpinejs/alpine) · [petite-vue](https://github.com/vuejs/petite-vue) · [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)</sup>
103
577
 
104
- <p align='center'>
105
- <a href="https://krishnized.github.io/license">ॐ</a>
106
- </p>
578
+ <p align="center"><a href="https://krishnized.github.io/license">ॐ</a></p>
package/signal.js CHANGED
@@ -41,10 +41,15 @@ export const signal = (v, _s, _obs = new Set, _v = () => _s.value) => (
41
41
  export const effect = (fn, _teardown, _fx, _deps) => (
42
42
  _fx = (prev) => {
43
43
  let tmp = _teardown;
44
- _teardown = null; // we null _teardown to avoid repeated call in case of recursive update
44
+ _teardown = null;
45
45
  tmp?.call?.();
46
46
  prev = current, current = _fx
47
- if (depth++ > 50) throw 'Cycle detected';
47
+ if (depth++ > 50) {
48
+ depth--; current = prev;
49
+ // dispose: unsubscribe from all deps so this effect never fires again
50
+ _teardown = fn = _fx.fn = null; for (let dep of _deps) dep.delete(_fx); _deps.clear()
51
+ console.error('∴ Reactive loop detected'); return
52
+ }
48
53
  try { _teardown = fn() } finally { current = prev; depth-- }
49
54
  },
50
55
  _fx.fn = fn,
@@ -1 +1 @@
1
- {"version":3,"file":"core.d.ts","sourceRoot":"","sources":["../core.js"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"core.d.ts","sourceRoot":"","sources":["../core.js"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"text.d.ts","sourceRoot":"","sources":["../../directive/text.js"],"names":[],"mappings":"AAQe,8BAHJ,OAAO,GAAG,mBAAmB,GAC3B,CAAC,CAAC,EAAE,GAAG,KAAK,IAAI,CA0B5B"}
1
+ {"version":3,"file":"text.d.ts","sourceRoot":"","sources":["../../directive/text.js"],"names":[],"mappings":"AAQe,8BAHJ,OAAO,GAAG,mBAAmB,GAC3B,CAAC,CAAC,EAAE,GAAG,KAAK,IAAI,CA+B5B"}
@@ -1 +1 @@
1
- {"version":3,"file":"signal.d.ts","sourceRoot":"","sources":["../signal.js"],"names":[],"mappings":"AAmBO,uBAJM,CAAC,KACH,CAAC,6CACC,OAAO,WAAW,EAAE,MAAM,CAAC,CAAC,CAAC,CAgBzC;AAOM,2BAHI,MAAM,IAAI,GAAG,CAAC,MAAM,IAAI,CAAC,yCACvB,MAAM,IAAI,CAgBtB;AAQM,yBAJM,CAAC,MACH,MAAM,CAAC,0EACL,OAAO,WAAW,EAAE,MAAM,CAAC,CAAC,CAAC,CAWzC;AAQM,sBAJM,CAAC,MACH,MAAM,CAAC,gCACL,CAAC,CAMb;AAQM,0BAJM,CAAC,MACH,MAAM,CAAC,wBACL,CAAC,CAE+F"}
1
+ {"version":3,"file":"signal.d.ts","sourceRoot":"","sources":["../signal.js"],"names":[],"mappings":"AAmBO,uBAJM,CAAC,KACH,CAAC,6CACC,OAAO,WAAW,EAAE,MAAM,CAAC,CAAC,CAAC,CAgBzC;AAOM,2BAHI,MAAM,IAAI,GAAG,CAAC,MAAM,IAAI,CAAC,yCACvB,MAAM,IAAI,CAqBtB;AAQM,yBAJM,CAAC,MACH,MAAM,CAAC,0EACL,OAAO,WAAW,EAAE,MAAM,CAAC,CAAC,CAAC,CAWzC;AAQM,sBAJM,CAAC,MACH,MAAM,CAAC,gCACL,CAAC,CAMb;AAQM,0BAJM,CAAC,MACH,MAAM,CAAC,wBACL,CAAC,CAE+F"}