laghu 1.0.0 → 1.1.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/CHANGELOG.md ADDED
@@ -0,0 +1,66 @@
1
+ # Changelog
2
+
3
+ All notable changes to laghu are recorded here. Versioning follows
4
+ semver: patch for fixes, minor for backwards-compatible additions,
5
+ major for breaking changes.
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [1.1.0]
10
+
11
+ Security baseline (breaking only for attack patterns):
12
+
13
+ - Default HTML sanitizer: `data-html` and swaps strip scripts,
14
+ frames, event-handler attributes, and scriptable URLs out of the
15
+ box. Override with `Laghu.sanitize` (DOMPurify recommended for
16
+ untrusted content).
17
+ - URL policy: `data-bind:href/src/action/...` refuses
18
+ `javascript:`, `data:text/html`, and `vbscript:` values.
19
+ - Prototype pollution guards in path reads, writes, and action
20
+ strings (`__proto__`, `constructor`, `prototype` blocked).
21
+ - `DEV` now defaults off (`__LAGHU_DEV__ === true` enables). Error
22
+ visibility moves to events that always fire: `laghu:error`,
23
+ `laghu:request-error`, `laghu:unknown-action`.
24
+
25
+ Reliability:
26
+
27
+ - One throwing binding no longer kills the page; failures report
28
+ `laghu:error` and the scan continues.
29
+ - Effects unsubscribe on stop and prune stale deps on re-run;
30
+ nested effects compose through a stack.
31
+ - Swapped content keeps the requesting element's actions;
32
+ `morph:outer` forwards its mode.
33
+ - Fetch overlap uses a controller set (correct `queue:all`),
34
+ `data-timeout` aborts hung requests, GET arrays repeat params,
35
+ `+json` content types merge, failures record `state.$error`
36
+ with `data-pending` on the element.
37
+ - `onUnmount` setup hooks now run on disconnect.
38
+ - Row scopes own their variables; keyed lists use item-relative
39
+ `data-key` and rebind only on identity change; the observer
40
+ treats intra-batch moves as moves, not removals.
41
+
42
+ Expressions (still no `new Function`, still CSP-safe):
43
+
44
+ - Ternary (`a ? b : c`), arithmetic (`+ - * / %` with precedence),
45
+ parenthesized groups, calls with evaluated arguments,
46
+ bracket indexes (`list[0]`, `map["key"]`).
47
+ - Plain kebab-case action names stay literal; hyphens mean
48
+ subtraction only inside larger expressions.
49
+
50
+ New API:
51
+
52
+ - `Laghu.computed(fn)`, `Laghu.watch(scope, expr, cb)`,
53
+ `Laghu.persist(name, opts)` (localStorage, versioned).
54
+ - `LaghuRouter` opt-in plugin (`dist/router.min.js`): hash routes
55
+ with params, guards, notfound, programmatic navigate.
56
+ - Late-added state keys wake readers (miss tracking); `$error`
57
+ seeded at hydrate.
58
+
59
+ Docs: full JSDoc with examples on all public API, TSDoc
60
+ declarations, 17-section README with linked contents.
61
+
62
+ ## [1.0.0]
63
+
64
+ Initial release: zero-build IIFE core, Light-DOM components, Proxy
65
+ reactivity, HTML-first templates, JSON/HTML fetch, morph plugin,
66
+ 9 examples, 2 demo apps, 5-way benchmark.
package/README.md CHANGED
@@ -27,6 +27,7 @@ Contents:
27
27
  - [15. Checking your setup](#15-checking-your-setup)
28
28
  - [16. Troubleshooting](#16-troubleshooting)
29
29
  - [17. Project files](#17-project-files)
30
+ - [18. Roadmap (explicit non-goals included)](#18-roadmap-explicit-non-goals-included)
30
31
 
31
32
  ## 1. When to use laghu
32
33
 
@@ -84,14 +85,29 @@ like `github.com/.../blob/...` serves HTML, and
84
85
  `raw.githubusercontent.com` serves plain text, which browsers refuse
85
86
  to run as a script.
86
87
 
87
- Use jsDelivr instead. It serves GitHub files with the correct
88
- JavaScript content type:
88
+ Use jsDelivr instead. It serves files with the correct
89
+ JavaScript content type. Two flavors, pick one per page:
90
+
91
+ npm package (preferred, versioned by the registry):
92
+
93
+ ```html
94
+ <script src="https://cdn.jsdelivr.net/npm/laghu@1.0.0/dist/laghu.min.js"></script>
95
+ <script src="https://cdn.jsdelivr.net/npm/laghu@1.0.0/dist/morph.min.js"></script>
96
+ ```
97
+
98
+ GitHub release files (works without any registry):
89
99
 
90
100
  ```html
91
101
  <script src="https://cdn.jsdelivr.net/gh/prosubodh/laghu@v1.0.0/dist/laghu.min.js"></script>
92
102
  <script src="https://cdn.jsdelivr.net/gh/prosubodh/laghu@v1.0.0/dist/morph.min.js"></script>
93
103
  ```
94
104
 
105
+ With a bundler or Node tooling:
106
+
107
+ ```sh
108
+ npm install laghu
109
+ ```
110
+
95
111
  Rules for production pages:
96
112
 
97
113
  - Always pin a version (`@v1.0.0`). Never use `@master`, because
@@ -167,8 +183,9 @@ One-way text binding. Same behavior, two spellings. Sets
167
183
  ### data-html
168
184
 
169
185
  Renders a value as HTML. Explicit opt-in because HTML parsing is an
170
- XSS risk. Content passes through the sanitize hook (see
171
- `Laghu.sanitize`). Prefer `data-bind` unless you trust the source.
186
+ XSS risk. Content passes through the baseline sanitizer, replaceable
187
+ with `Laghu.sanitize` (DOMPurify for untrusted sources). Prefer
188
+ `data-bind` unless you trust the source.
172
189
 
173
190
  ```html
174
191
  <div data-html="post.body"></div>
@@ -352,6 +369,43 @@ Fetch companions:
352
369
  - `data-headers`: extra headers as JSON.
353
370
  - `data-indicator="#spin"`: element shown while the request flies.
354
371
  - `data-disable`: disables the element while its request flies.
372
+ - `data-timeout="5000"`: aborts hung requests (milliseconds);
373
+ timeouts report `laghu:request-error` with `timeout: true`.
374
+ - `data-pending`: set on the element during flight (style hooks
375
+ like `[data-pending] { opacity: .5; }`).
376
+ - `state.$error`:fetch failures record `{ status }` or `{ message,
377
+ timeout }` here (null when healthy). Bind it directly:
378
+ `<span data-bind="$error.status"></span>`.
379
+
380
+ ### Hash routing (router plugin)
381
+
382
+ For pages with a handful of views, load `router.min.js` after the
383
+ core and declare hash routes. Past about five guarded routes, use
384
+ a real router; this one stays small on purpose.
385
+
386
+ ```html
387
+ <script src="laghu.min.js"></script>
388
+ <script src="router.min.js"></script>
389
+ <div id="view"></div>
390
+ <script>
391
+ LaghuRouter.route({
392
+ target: '#view',
393
+ scope: state,
394
+ routes: [
395
+ { path: '/', render: function () { return '<h1>Home</h1>'; } },
396
+ { path: '/user/:id', render: function (p) { return '<h1>User ' + p.id + '</h1>'; } }
397
+ ],
398
+ notfound: function () { return '<h1>Missing</h1>'; },
399
+ beforeEach: function (to) { return true; }
400
+ });
401
+ LaghuRouter.navigate('/user/42');
402
+ </script>
403
+ ```
404
+
405
+ Links are plain anchors (`href="#/user/42"`); hash changes drive
406
+ renders, params decode automatically, guards cancel by returning
407
+ false, and rendered fragments hydrate with the scope and actions
408
+ you pass in.
355
409
 
356
410
  ### data-trigger
357
411
 
@@ -451,34 +505,51 @@ trigger. Nested objects wrap on first read.
451
505
  ### Laghu.effect(fn)
452
506
 
453
507
  Runs `fn` now and again whenever state it read changes. Returns a
454
- stop function. One effect per binding is the normal pattern.
508
+ stop function that fully unsubscribes. Subscriptions refresh every
509
+ run, so untaken branches release. Nested effects compose; a throwing
510
+ run reports `laghu:error` and the page survives. One effect per
511
+ binding is the normal pattern.
455
512
 
456
513
  ### Laghu.eval(scope, expr)
457
514
 
458
515
  Reads one expression without `new Function` (CSP-safe). Supports
459
- paths, literals, `!`, comparisons, `&&`, `||`, and no-argument
460
- calls like `reset()`.
516
+ dotted paths, bracket indexes (`list[0]`, `map["k"]`), literals,
517
+ `!` and unary `-`, arithmetic (`+ - * / %`), comparisons, `&&`,
518
+ `||`, ternary (`a ? b : c`), parenthesized groups, and calls with
519
+ evaluated arguments like `total(qty)`. Anything else is undefined.
520
+
521
+ State keys must be valid identifiers (camelCase): a hyphen means
522
+ subtraction inside expressions. Action names are exempt; exact
523
+ action lookup runs before expression parsing, so
524
+ `data-click="my-action"` stays literal.
461
525
 
462
526
  ### Laghu.get(scope, path) and Laghu.set(scope, path, val)
463
527
 
464
528
  Dotted path access with scope-chain lookup. `$store.name.key`
465
- reaches global stores.
529
+ reaches global stores. Segments `__proto__`, `constructor`, and
530
+ `prototype` are refused on read and write. Late-added keys wake
531
+ readers: bindings on keys that do not exist yet update when the
532
+ keys appear. Writes follow the chain to the owning scope.
466
533
 
467
534
  ### Laghu.parseTrigger(spec, el)
468
535
 
469
536
  Parses `data-trigger` text into a modifier object. Useful for
470
537
  testing and custom wiring.
471
538
 
472
- ### Laghu.swap(target, html, mode, scope)
539
+ ### Laghu.swap(target, html, mode, scope, actions)
473
540
 
474
541
  Swaps HTML and hydrates the result. See modes in section 5.
475
542
  Dispatches `laghu:before-swap` and `laghu:after-swap`, preserves
476
- `data-preserve` nodes, restores focus and caret.
543
+ `data-preserve` nodes, restores focus and caret. The optional
544
+ `actions` map wires handlers in the new content; fetch swaps reuse
545
+ the requesting element's actions automatically.
477
546
 
478
547
  ### Laghu.sanitize(fn)
479
548
 
480
- Replaces the HTML pass-through used by `data-html` and swaps.
481
- Install a real sanitizer when rendering server HTML:
549
+ Replaces the HTML pass-through used by `data-html` and swaps. The
550
+ default is a baseline cleaner (scripts, frames, event handlers,
551
+ scriptable URLs stripped), not a policy. Install a real sanitizer
552
+ when rendering untrusted HTML:
482
553
 
483
554
  ```js
484
555
  Laghu.sanitize(function (h) { return DOMPurify.sanitize(h); });
@@ -498,6 +569,32 @@ Laghu.store('cart').n++;
498
569
  <span data-bind="$store.cart.n"></span>
499
570
  ```
500
571
 
572
+ ### Laghu.computed(fn)
573
+
574
+ Derived value kept fresh by an effect. Call the getter inside
575
+ bindings or other effects to subscribe.
576
+
577
+ ```js
578
+ var total = Laghu.computed(function () { return state.price * state.qty; });
579
+ Laghu.effect(function () { el.textContent = total(); });
580
+ ```
581
+
582
+ ### Laghu.watch(scope, expr, cb)
583
+
584
+ Calls back with `(next, prev)` whenever an expression's value
585
+ changes. Reads without subscribing the caller's own effect.
586
+ Returns the stop function.
587
+
588
+ ### Laghu.persist(name, opts)
589
+
590
+ Persists a named store to localStorage (debounced) with versioned
591
+ restore. Mismatched versions reseed instead of loading.
592
+
593
+ ```js
594
+ Laghu.store('cart', { n: 0 });
595
+ Laghu.persist('cart', { key: 'cart:v1', version: 1 });
596
+ ```
597
+
501
598
  ### Laghu.directive(name, fn)
502
599
 
503
600
  Registers `data-name`. The function receives `{ el, get, effect,
@@ -535,7 +632,9 @@ replacing. `opts.mode` is `inner` (default) or `outer`.
535
632
 
536
633
  ## 7. Events reference
537
634
 
538
- All names are CustomEvents. Listen with `addEventListener`:
635
+ All names are CustomEvents, and all of them bubble, so one listener
636
+ on a section or on document observes every child. Listen with
637
+ `addEventListener`:
539
638
 
540
639
  - `laghu:before-request`: cancelable, `detail` has `url`, `method`.
541
640
  Call `preventDefault()` to stop the fetch (for confirm dialogs).
@@ -653,14 +752,30 @@ laghu runs under strict CSP with no `unsafe-eval` and no
653
752
  `unsafe-inline`:
654
753
 
655
754
  - No `new Function`, no `eval`, no string `setTimeout`. Expressions
656
- use a small built-in reader (paths, literals, `!`, comparisons,
657
- `&&`, `||`, no-argument calls).
755
+ use a small built-in reader (paths, indexes, literals, `!`,
756
+ arithmetic, comparisons, `&&`, `||`, ternary, calls with args).
658
757
  - No inline handlers. All listeners attach with
659
758
  `addEventListener` from `data-*` attributes.
660
- - Ship laghu as an external file with a Subresource Integrity hash.
759
+ - Ship laghu as an external file with a Subresource Integrity hash
760
+ (`node scripts/sri.mjs` prints them for release notes).
661
761
  - `data-scope` takes JSON only, for the same no-eval reason.
662
- - `data-html` and swaps accept HTML by design. Point
663
- `Laghu.sanitize` at DOMPurify when content comes from a server.
762
+
763
+ No-eval is not the whole of web security. The rest of the posture:
764
+
765
+ - `data-html` and swaps pass a baseline sanitizer (scripts, frames,
766
+ event-handler attributes, scriptable URLs stripped). It is a
767
+ floor: point `Laghu.sanitize` at DOMPurify for untrusted content,
768
+ and set `script-src-attr 'none'` in your policy for defense in
769
+ depth.
770
+ - Attribute bindings refuse `javascript:`, `data:text/html`, and
771
+ `vbscript:` URLs on navigation and loading attributes.
772
+ - State paths refuse `__proto__`, `constructor`, and `prototype`
773
+ segments on read and write, including inside action strings.
774
+ - Runtime failures dispatch `laghu:error` (bindings, directives,
775
+ init, unmount) and `laghu:request-error` (fetch) in every
776
+ environment; consoles stay quiet unless you enable dev mode.
777
+ - See `SECURITY.md` for reporting, supported versions, and the
778
+ audit-backed regression list.
664
779
 
665
780
  ## 12. Loading flash and SEO notes
666
781
 
@@ -684,13 +799,28 @@ never run.
684
799
 
685
800
  Measured after minification:
686
801
 
687
- - Core `dist/laghu.min.js`: about 21kB raw, 6.8kB gzip, 6.1kB brotli.
802
+ - Core `dist/laghu.min.js`: about 27.5kB raw, 9.2kB gzip, 8.2kB brotli.
688
803
  - Morph `dist/morph.min.js`: about 2.3kB raw, 1kB gzip.
804
+ - Router `dist/router.min.js`: about 1.7kB raw, 0.9kB gzip.
689
805
  - Budgets enforced by `node scripts/size.mjs`: core at or under
690
- 7.5kB gzip, morph at or under 3kB gzip.
806
+ 10kB gzip, morph at or under 3kB gzip, router at or under
807
+ 2.5kB gzip.
808
+
809
+ The core grew from 7.1kB to 9.2kB for two consensus items: the
810
+ security baseline (default sanitizer, URL policy, pollution
811
+ guards, error isolation, effect unsubscribe) and the expression
812
+ grammar (ternary, arithmetic, argued calls, indexing). Both are
813
+ audited-in requirements, not creep: the budget moved openly, and
814
+ the total stays under half of htmx alone (~16kB gzip).
691
815
 
692
816
  For scale: htmx alone is about 16kB gzip, Alpine with a few plugins
693
817
  is 12 to 15kB. laghu core plus morph stays under half of either.
818
+ End-to-end proof lives in `bench/RESULTS.md`: the same TicTacToe in
819
+ laghu, vanilla, React, Vue, and Angular, measured in headless
820
+ Chromium. laghu ships 3 to 10 times less JavaScript than the
821
+ frameworks, holds the lowest heap of the four, and ties them on
822
+ interaction latency (all sub-millisecond); vanilla remains the
823
+ floor, as zero abstraction must.
694
824
 
695
825
  ## 14. Worked examples
696
826
 
@@ -699,8 +829,23 @@ See the `examples/` folder. Each file opens directly from disk.
699
829
  - `counter.html`: one component, one binding, one action.
700
830
  - `todo.html`: island with `data-model` input, keyed `data-for`
701
831
  list, checkbox models, `data-show` counter.
832
+ - `model.html`: every two-way input type (text, number, single
833
+ checkbox, checkbox group, radio, single select, multi select,
834
+ textarea) with live mirrors and reset.
835
+ - `triggers.html`: throttle, once, interval, load, init, change
836
+ write-through, and prevented form submit.
837
+ - `swaps.html`: every swap mode, the swap event log,
838
+ `data-preserve`, and a morph swap that keeps typing and focus.
839
+ - `components.html`: two independent instances, attribute sync,
840
+ mount and unmount observation, removal and restore.
841
+ - `store.html`: two islands sharing one `$store` counter and name.
842
+ - `directives.html`: custom ticker and autofocus directives,
843
+ template removal running directive cleanup, class binding.
702
844
  - `fetch.html`: JSON button plus morph list swap (needs a server
703
- for the URLs, or swap in the stub URLs for local JSON files).
845
+ for the URLs, or swap in local JSON files).
846
+
847
+ `test/examples.mjs` boots every file above and drives it: 48
848
+ assertions, all passing alongside the suites below.
704
849
 
705
850
  ## 15. Checking your setup
706
851
 
@@ -708,13 +853,18 @@ See the `examples/` folder. Each file opens directly from disk.
708
853
  npm install
709
854
  npx tsc --noEmit -p jsconfig.json
710
855
  node scripts/size.mjs
711
- node test/run.mjs
856
+ node scripts/sri.mjs
857
+ npm test
712
858
  ```
713
859
 
714
- 37 tests cover: counter increments, text and attribute bindings,
715
- conditionals, keyed reorder reuse, two-way inputs, attribute sync,
716
- triggers, JSON merge, all swap modes, morph focus preservation,
717
- stores, directives with cleanup, and unknown-action warnings.
860
+ 154 tests cover (71 in `test/run.mjs` plus 48 in
861
+ `test/examples.mjs`, 23 in `test/apps.mjs`, 12 in
862
+ `test/router.mjs`): counter increments, text and attribute
863
+ bindings, conditionals, keyed reorder reuse, two-way inputs,
864
+ attribute sync, triggers, JSON merge, all swap modes, morph focus
865
+ preservation, stores, directives with cleanup, unknown-action
866
+ warnings, every shipped example page driven end to end, both demo
867
+ apps played to completion, and the router contract.
718
868
 
719
869
  Releasing a new version (publishes to npm through CI):
720
870
 
@@ -758,14 +908,46 @@ Request never fires with `changed`: the input value never differed.
758
908
  morph warns and falls back: `morph.min.js` is not loaded. Add the
759
909
  script tag after the core.
760
910
 
911
+ `data-click="my-action"` does nothing: check the actions map first;
912
+ exact names win before expression parsing. `data-bind="a-b"` on the
913
+ other hand always subtracts; rename the key to camelCase.
914
+
915
+ New key renders blank: bindings subscribe to keys that appear later,
916
+ but prefer declaring the full shape up front so first paint is
917
+ complete.
918
+
761
919
  ## 17. Project files
762
920
 
763
921
  - `src/laghu.js`: core source with full JSDoc, loaded directly in
764
922
  development.
765
923
  - `src/morph.js`: opt-in morph plugin source with full JSDoc.
766
- - `dist/laghu.min.js`, `dist/morph.min.js`: minified builds.
924
+ - `src/router.js`: opt-in hash router plugin with full JSDoc.
925
+ - `dist/laghu.min.js`, `dist/morph.min.js`, `dist/router.min.js`:
926
+ minified builds.
767
927
  - `dist/laghu.d.ts`: TypeScript declarations for consumers.
768
- - `test/run.mjs`: jsdom suite, 37 assertions.
928
+ - `test/run.mjs`: jsdom suite, 71 assertions including security
929
+ regressions.
930
+ - `test/examples.mjs`: boots every example page and drives it, 48 assertions.
931
+ - `test/apps.mjs`: plays both demo apps to completion, 23 assertions.
932
+ - `test/router.mjs`: router contract, 12 assertions.
769
933
  - `scripts/size.mjs`: minifies and enforces gzip budgets.
770
- - `examples/`: three pages opening from disk.
934
+ - `scripts/sri.mjs`: prints SRI hashes for release notes.
935
+ - `examples/`: nine pages opening from disk.
936
+ - `apps/`: full demo projects with the prescribed structure.
937
+ - `bench/`: five-way benchmark harness and results.
938
+ - `CHANGELOG.md`, `SECURITY.md`, `CONTRIBUTING.md`: release,
939
+ security, and contribution contracts.
771
940
  - `jsconfig.json`: strict `checkJs` config used by editors and CI.
941
+
942
+ ## 18. Roadmap (explicit non-goals included)
943
+
944
+ Next, in order: declarative validation (`data-validate` with
945
+ schemas), transitions (`data-transition`), async fallback templates
946
+ for fetch, icon and Tailwind interop recipes (`data-icon`,
947
+ class merging), then docs site, real-browser matrix, and devtools.
948
+
949
+ Deliberately out of scope: JSX or any compiler toolchain, VDOM
950
+ diffing in core, built-in auth/i18n/charts/offline-sync, a Node
951
+ SSR server, full SPA parity. laghu owns small interactive pages
952
+ and embeds; it graduates users to bigger tools with documented
953
+ boundaries instead of cloning them badly.
package/SECURITY.md ADDED
@@ -0,0 +1,45 @@
1
+ # Security policy
2
+
3
+ ## Supported versions
4
+
5
+ Only the latest minor release receives security fixes. The
6
+ published provenance attests every release build on GitHub Actions;
7
+ prefer registry installs over copying files.
8
+
9
+ | Version | Supported |
10
+ | ------- | --------- |
11
+ | 1.1.x | yes |
12
+ | 1.0.x | no |
13
+
14
+ ## Reporting a vulnerability
15
+
16
+ Open a private security advisory on GitHub, or email the
17
+ maintainer through the profile listed on the repo. Include a
18
+ minimal reproduction (HTML plus state or response payload). Expect
19
+ an acknowledgement within 72 hours and a fix or mitigation plan
20
+ within 14 days. Do not open public issues for unpatched holes.
21
+
22
+ ## Threat model and defaults
23
+
24
+ - `data-html` and HTML swaps pass a baseline sanitizer (scripts,
25
+ frames, event handlers, scriptable URLs stripped). It is a
26
+ floor: render untrusted content only behind DOMPurify via
27
+ `Laghu.sanitize`, and serve a Content Security Policy that also
28
+ sets `script-src-attr 'none'`.
29
+ - Attribute bindings refuse `javascript:`, `data:text/html`, and
30
+ `vbscript:` URLs on navigation and loading attributes.
31
+ - State paths reject `__proto__`, `constructor`, and `prototype`
32
+ segments on read and write, including inside action strings.
33
+ - Expressions never use `new Function` or `eval`, so
34
+ `script-src 'self'` holds without `unsafe-eval`. That covers
35
+ code injection through expressions, not XSS through HTML: those
36
+ are separate layers, configure both.
37
+ - Fetch sends JSON with same-origin default credentials; set
38
+ `data-headers` and CORS server-side for cross-origin APIs.
39
+
40
+ ## Audits
41
+
42
+ Dependency and secret scanning run in CI. XSS and pollution
43
+ vectors from past audits live as regression tests in
44
+ `test/run.mjs` (block 7). Re-run `npm test` before every release;
45
+ the publish workflow refuses dirty trees and mismatched tags.
package/dist/laghu.d.ts CHANGED
@@ -44,7 +44,7 @@ export interface DirectiveContext {
44
44
 
45
45
  /** Full public surface available as window.Laghu. */
46
46
  export interface LaghuAPI {
47
- /** Release string, for example "1.0.0". */
47
+ /** Release string, for example "1.1.0". */
48
48
  version: string;
49
49
  /**
50
50
  * Register a Light-DOM custom element.
@@ -52,18 +52,19 @@ export interface LaghuAPI {
52
52
  * @param setup Factory returning fresh state, actions, and options.
53
53
  */
54
54
  define(tag: string, setup: () => SetupResult): any;
55
- /** Make a plain object or array reactive. Primitives pass through. */
55
+ /** Make a plain object or array reactive. Primitives pass through. Re-wraps return the same proxy. */
56
56
  reactive<T extends object>(obj: T): T;
57
57
  /**
58
- * Run fn now and again on dependency change.
58
+ * Run fn now and again on dependency change. Subscriptions refresh
59
+ * per run; stop() unsubscribes fully. Never throws out.
59
60
  * @returns Stop function that unsubscribes the effect.
60
61
  */
61
62
  effect(fn: () => void): () => void;
62
- /** Evaluate a small CSP-safe expression against a scope. */
63
+ /** Evaluate a CSP-safe expression: paths, indexes, literals, arithmetic, comparisons, ternary, calls. */
63
64
  eval(scope: any, expr: string): any;
64
- /** Read a dotted path (or $store.name.key) without throwing. */
65
+ /** Read a dotted path (or $store.name.key) without throwing. Prototype segments blocked. */
65
66
  get(scope: any, path: string): any;
66
- /** Write a dotted path, creating intermediate objects. */
67
+ /** Write a dotted path, creating intermediate objects. Prototype segments refused. */
67
68
  set(scope: any, path: string, val: any): void;
68
69
  /** Parse data-trigger text into a TriggerSpec. */
69
70
  parseTrigger(spec: string, el: Element): TriggerSpec;
@@ -71,11 +72,12 @@ export interface LaghuAPI {
71
72
  * Swap HTML into target, hydrate the result, preserve focus.
72
73
  * @param swap One of inner, outer, text, append, prepend, beforebegin,
73
74
  * afterend, delete, none, morph:inner, morph:outer.
75
+ * @param actions Named actions for handlers in the new content.
74
76
  */
75
- swap(target: Element, html: string, swap: string, scope: any): void;
77
+ swap(target: Element, html: string, swap: string, scope: any, actions?: Record<string, Function>): void;
76
78
  /** FOUC guard CSS string. Inject it or copy the rule. */
77
79
  css: string;
78
- /** Replace the HTML pass-through used by data-html and swaps. */
80
+ /** Replace the HTML pass-through used by data-html and swaps. Defaults to a baseline sanitizer. */
79
81
  sanitize(fn: (html: string) => string): void;
80
82
  /**
81
83
  * Scan root, wire all bindings, return the reactive state.
@@ -86,6 +88,12 @@ export interface LaghuAPI {
86
88
  destroyTree(root: Element): void;
87
89
  /** Create (with init) or fetch (without) a named global reactive store. */
88
90
  store(name: string, init?: any): any;
91
+ /** Derived value kept fresh; call the getter inside bindings or effects to subscribe. */
92
+ computed(fn: () => any): () => any;
93
+ /** Call back with (next, prev) when an expression's value changes. Returns the stop function. */
94
+ watch(scope: any, expr: string, cb: (next: any, prev: any) => void): () => void;
95
+ /** Persist a named store to localStorage (debounced) with versioned restore. */
96
+ persist(name: string, opts?: { key?: string; version?: number; debounce?: number }): any;
89
97
  /**
90
98
  * Register a data-name directive. Return a cleanup function optionally.
91
99
  * @example
@@ -104,6 +112,22 @@ declare global {
104
112
  morphEl(a: Element, b: Element): void;
105
113
  version: string;
106
114
  };
115
+ LaghuRouter?: {
116
+ route(opts: {
117
+ target?: string;
118
+ scope?: any;
119
+ actions?: Record<string, Function>;
120
+ routes?: Array<{ path: string; render: (params: Record<string, string>, to: any) => string }>;
121
+ notfound?: (to: any) => string;
122
+ beforeEach?: (to: any, from: any) => boolean | void;
123
+ afterEach?: (to: any, from: any) => void;
124
+ }): () => void;
125
+ compile(pattern: string): (path: string) => Record<string, string> | null;
126
+ current(): { path: string; params: Record<string, string> };
127
+ navigate(path: string): void;
128
+ afterEach(fn: (to: any, from: any) => void): void;
129
+ version: string;
130
+ };
107
131
  __LAGHU_DEV__?: boolean;
108
132
  }
109
133
  }