laghu 0.0.0-stage → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,771 @@
1
- # Temporary Holding Version
1
+ # laghu.js
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ laghu.js is a tiny front-end helper for small pages. It adds reactivity,
4
+ reusable components, and declarative data fetching to plain HTML, using
5
+ only the browser. There is no build step, no JSX, and no TypeScript
6
+ compiler. You add one script tag and write normal HTML.
7
+
8
+ The name "laghu" is a Sanskrit word for light and small. That is the
9
+ whole idea: a light tool for pages that do not need a heavy framework.
10
+
11
+ Contents:
12
+
13
+ - [1. When to use laghu](#1-when-to-use-laghu)
14
+ - [2. Quick start](#2-quick-start)
15
+ - [3. Loading from a CDN](#3-loading-from-a-cdn)
16
+ - [4. Core concepts](#4-core-concepts)
17
+ - [5. HTML attribute reference](#5-html-attribute-reference)
18
+ - [6. JavaScript API reference](#6-javascript-api-reference)
19
+ - [7. Events reference](#7-events-reference)
20
+ - [8. Fetching data (JSON and HTML)](#8-fetching-data-json-and-html)
21
+ - [9. The morph plugin](#9-the-morph-plugin)
22
+ - [10. Type safety without TypeScript](#10-type-safety-without-typescript)
23
+ - [11. Content Security Policy notes](#11-content-security-policy-notes)
24
+ - [12. Loading flash and SEO notes](#12-loading-flash-and-seo-notes)
25
+ - [13. File sizes](#13-file-sizes)
26
+ - [14. Worked examples](#14-worked-examples)
27
+ - [15. Checking your setup](#15-checking-your-setup)
28
+ - [16. Troubleshooting](#16-troubleshooting)
29
+ - [17. Project files](#17-project-files)
30
+
31
+ ## 1. When to use laghu
32
+
33
+ Use laghu when your page is mostly static HTML with a few interactive
34
+ parts: counters, toggles, tabs, a list rendered from JSON, a form that
35
+ posts and shows a result, a search box with live results.
36
+
37
+ Do not use laghu when you need client-side routing with auth guards,
38
+ offline sync, huge editable tables, canvas editors, or a large team
39
+ hiring for React or Vue skills. At that size, graduate to Svelte,
40
+ Solid, Astro islands, React, or Vue.
41
+
42
+ ## 2. Quick start
43
+
44
+ Step 1. Save this as index.html and open it by double-clicking
45
+ (no server needed):
46
+
47
+ ```html
48
+ <!doctype html>
49
+ <meta charset="utf-8">
50
+ <title>counter</title>
51
+ <style>
52
+ counter-widget:not(:defined) { visibility: hidden; }
53
+ </style>
54
+
55
+ <counter-widget>
56
+ <p>Count: <strong data-bind="count">0</strong></p>
57
+ <button data-click="increment">Add one</button>
58
+ </counter-widget>
59
+
60
+ <script src="laghu.min.js"></script>
61
+ <script>
62
+ Laghu.define('counter-widget', function () {
63
+ return {
64
+ state: { count: 0 },
65
+ actions: {
66
+ increment: function () { this.count++; }
67
+ }
68
+ };
69
+ });
70
+ </script>
71
+ ```
72
+
73
+ Step 2. Open the file in a browser. The button adds one per click.
74
+
75
+ What happened: `Laghu.define` registered a real custom element.
76
+ When the element connected, laghu made `{ count: 0 }` reactive,
77
+ mapped `data-bind="count"` to the `strong` node, and attached the
78
+ click handler. When state changes, only that `strong` node updates.
79
+
80
+ ## 3. Loading from a CDN
81
+
82
+ A direct GitHub link does not work as a script source. A page address
83
+ like `github.com/.../blob/...` serves HTML, and
84
+ `raw.githubusercontent.com` serves plain text, which browsers refuse
85
+ to run as a script.
86
+
87
+ Use jsDelivr instead. It serves GitHub files with the correct
88
+ JavaScript content type:
89
+
90
+ ```html
91
+ <script src="https://cdn.jsdelivr.net/gh/prosubodh/laghu@v1.0.0/dist/laghu.min.js"></script>
92
+ <script src="https://cdn.jsdelivr.net/gh/prosubodh/laghu@v1.0.0/dist/morph.min.js"></script>
93
+ ```
94
+
95
+ Rules for production pages:
96
+
97
+ - Always pin a version (`@v1.0.0`). Never use `@master`, because
98
+ branch content moves and caches shift under you.
99
+ - When a new laghu release comes out, move the version number
100
+ forward deliberately after testing.
101
+ - For extra safety, add a Subresource Integrity hash to the tag so
102
+ the browser rejects a changed file.
103
+ - For local development with no internet, download the two files
104
+ next to your HTML and reference them relatively:
105
+ `<script src="laghu.min.js"></script>`.
106
+
107
+ Long term, publishing the `laghu` package to npm (the name is free
108
+ at the time of writing) unlocks versioned CDN paths such as
109
+ `cdn.jsdelivr.net/npm/laghu@1.0.0/...` plus automated update tools.
110
+
111
+ ## 4. Core concepts
112
+
113
+ State is a plain object. Actions are plain functions that run with
114
+ the state as `this`. Bindings are HTML attributes that connect state
115
+ to the DOM. Scopes decide which state a binding reads.
116
+
117
+ There are two ways to use laghu, sharing one engine:
118
+
119
+ - Islands: any element with `data-scope='{"count": 0}'` becomes an
120
+ independent reactive area, hydrated automatically on page load.
121
+ Good for sprinkling behavior onto server-rendered pages.
122
+ - Components: `Laghu.define('my-tag', setup)` registers a custom
123
+ element. Each instance gets fresh state from `setup()`. Good for
124
+ repeated widgets.
125
+
126
+ Effects are the glue. Each binding creates one effect that re-runs
127
+ when state it read changes. Writes batch in a microtask, so ten
128
+ synchronous writes cause one DOM update.
129
+
130
+ Cleanup is automatic. Every binding registers its teardown on its
131
+ element. Removing nodes, swapping HTML, or disconnecting a component
132
+ stops effects, detaches listeners, clears timers and observers, and
133
+ aborts in-flight fetches.
134
+
135
+ ## 5. HTML attribute reference
136
+
137
+ ### data-scope
138
+
139
+ Marks an island and carries its initial state as JSON (double quotes
140
+ required, because JSON keeps the parser small and CSP-safe).
141
+
142
+ ```html
143
+ <div data-scope='{"count": 0, "user": {"name": "Ada"}}'>
144
+ <span data-bind="count"></span>
145
+ </div>
146
+ ```
147
+
148
+ Islands hydrate automatically when the document is ready. To hydrate
149
+ manually and keep the state handle:
150
+
151
+ ```js
152
+ var root = document.querySelector('[data-scope]');
153
+ var state = Laghu.hydrate(root, { count: 0 });
154
+ state.count = 5; // bound nodes update shortly after
155
+ ```
156
+
157
+ ### data-bind and data-text
158
+
159
+ One-way text binding. Same behavior, two spellings. Sets
160
+ `textContent`, so values are never parsed as HTML (safe by default).
161
+
162
+ ```html
163
+ <span data-bind="user.name"></span>
164
+ <span data-text="count"></span>
165
+ ```
166
+
167
+ ### data-html
168
+
169
+ 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.
172
+
173
+ ```html
174
+ <div data-html="post.body"></div>
175
+ ```
176
+
177
+ ### data-bind:attr
178
+
179
+ Binds any attribute to an expression. Removing values (`false`,
180
+ `null`, `undefined`) remove the attribute.
181
+
182
+ ```html
183
+ <a data-bind:href="user.url">profile</a>
184
+ <img data-bind:src="photo.src" data-bind:alt="photo.alt">
185
+ <button data-bind:disabled="!form.valid">Save</button>
186
+ ```
187
+
188
+ ### data-class:name
189
+
190
+ Toggles one CSS class by truthiness.
191
+
192
+ ```html
193
+ <button data-class:active="tab === 'home'">Home</button>
194
+ ```
195
+
196
+ ### data-style:prop
197
+
198
+ Sets one inline style property. Falsey values clear it.
199
+
200
+ ```html
201
+ <div data-style:color="error ? 'red' : ''">message</div>
202
+ ```
203
+
204
+ ### data-model
205
+
206
+ Two-way form binding. Supported: text, number, checkbox (boolean or
207
+ string array for groups sharing one path), radio, single select,
208
+ multi select, textarea. State writes skip a focused text field whose
209
+ value already matches, so typing never loses the caret.
210
+
211
+ ```html
212
+ <input data-model="form.q" placeholder="search">
213
+ <input type="checkbox" data-model="form.agree">
214
+ <input type="checkbox" value="a" data-model="form.tags">
215
+ <input type="checkbox" value="b" data-model="form.tags">
216
+ <input type="radio" value="r" data-model="form.color">
217
+ <select data-model="form.color"><option value="r">red</option></select>
218
+ <select multiple data-model="form.colors">...</select>
219
+ <input type="number" data-model="form.qty">
220
+ ```
221
+
222
+ ### data-show
223
+
224
+ Toggles `hidden` by truthiness. State and focus are kept because the
225
+ node stays in the document.
226
+
227
+ ```html
228
+ <p data-show="loading">Loading...</p>
229
+ <p data-show="todos.length === 0">Nothing here yet.</p>
230
+ ```
231
+
232
+ ### data-if on an element
233
+
234
+ Same hidden toggle as `data-show`, for template-style conditions on
235
+ plain elements.
236
+
237
+ ```html
238
+ <p data-if="user.isAdmin">Admin panel link</p>
239
+ ```
240
+
241
+ ### template data-if
242
+
243
+ Real attach and detach for a block. Content after the anchor inserts
244
+ when true and removes when false, running cleanups on removal.
245
+
246
+ ```html
247
+ <template data-if="cart.open">
248
+ <aside>Your cart...</aside>
249
+ </template>
250
+ ```
251
+
252
+ ### template data-for and data-key
253
+
254
+ Renders a list by cloning the template per item. Syntax:
255
+ `data-for="item in list"` or `data-for="item, i in list"`. Each row
256
+ gets a child scope where `item` (and `i`) are visible alongside the
257
+ parent scope.
258
+
259
+ ```html
260
+ <ul>
261
+ <template data-for="todo in todos" data-key="id">
262
+ <li>
263
+ <input type="checkbox" data-model="todo.done">
264
+ <span data-bind="todo.title"></span>
265
+ </li>
266
+ </template>
267
+ </ul>
268
+ ```
269
+
270
+ Always add `data-key` when rows hold inputs or components. Keyed rows
271
+ reuse DOM nodes across reorder, so focus and typed text survive.
272
+ Unkeyed lists patch by index and are fine for read-only rows under
273
+ about a hundred items.
274
+
275
+ ### data-click
276
+
277
+ Runs an action on click. Forms, in order: a state helper
278
+ (`count++`, `count--`, `path = expr`), a named action
279
+ (`increment` or `increment()`), or any expression resolving to a
280
+ function. Accepts `data-trigger` modifiers (see section 5).
281
+
282
+ ```html
283
+ <button data-click="increment">Add</button>
284
+ <button data-click="count++">Add (no action needed)</button>
285
+ <button data-click="q = ''">Clear</button>
286
+ ```
287
+
288
+ Actions run with state as `this`:
289
+
290
+ ```js
291
+ Laghu.define('x-y', function () {
292
+ return {
293
+ state: { count: 0 },
294
+ actions: {
295
+ increment: function () { this.count++; }
296
+ }
297
+ };
298
+ });
299
+ ```
300
+
301
+ Unknown names warn in the console during development and dispatch
302
+ `laghu:unknown-action` on document, so typos fail loudly.
303
+
304
+ ### data-on:event
305
+
306
+ Any DOM event, with optional `.prevent`, `.stop`, `.once` suffixes.
307
+ The event is visible inside as `$event`.
308
+
309
+ ```html
310
+ <form data-on:submit.prevent="save">...</form>
311
+ <input data-on:input="q = $event.target.value">
312
+ <div data-on:keydown.enter="send">...</div>
313
+ ```
314
+
315
+ Note: `.enter` style key filters are not built in. Branch on
316
+ `$event.key` inside the action instead.
317
+
318
+ ### data-init
319
+
320
+ Runs once per hydration as an action string. Useful for seeding a
321
+ row scope or kicking off a load.
322
+
323
+ ```html
324
+ <div data-scope='{}' data-init="load">...</div>
325
+ ```
326
+
327
+ ### data-get, data-post, data-put, data-patch, data-delete
328
+
329
+ Declarative fetch. The attribute value is the URL. `data-method`
330
+ overrides the method. See section 8 for the full fetch guide.
331
+
332
+ ```html
333
+ <button data-get="/api/user" data-target="#out">Load</button>
334
+ <form data-post="/api/save" data-target="#msg" data-swap="inner">
335
+ ```
336
+
337
+ ### data-target, data-swap, data-select, data-vals, data-headers
338
+
339
+ Fetch companions:
340
+
341
+ - `data-target="#id"`: where the HTML goes. Default is the element.
342
+ - `data-swap`: how it goes in. One of `inner`, `outer`, `text`,
343
+ `append` (also `beforeend`), `prepend` (also `afterbegin`),
344
+ `beforebegin`, `afterend`, `delete`, `none`, `morph:inner`,
345
+ `morph:outer`. Default is `inner`.
346
+ - `data-select="#part"`: picks one selector out of an HTML response
347
+ before swapping, for endpoints returning full pages.
348
+ - `data-vals`: JSON params (`'{"q": "x"}'`), or the word `form`
349
+ to serialize the enclosing form. GET appends a query string,
350
+ other methods send a JSON body. A surrounding form also merges
351
+ for non-GET requests.
352
+ - `data-headers`: extra headers as JSON.
353
+ - `data-indicator="#spin"`: element shown while the request flies.
354
+ - `data-disable`: disables the element while its request flies.
355
+
356
+ ### data-trigger
357
+
358
+ Controls when clicks and fetches fire. Grammar (any order, first bare
359
+ word is the event):
360
+
361
+ ```html
362
+ <button data-get="/s" data-trigger="click delay:500ms">search</button>
363
+ <input data-get="/s" data-trigger="change changed throttle:1s">
364
+ <div data-get="/t" data-trigger="every 5s">
365
+ <div data-get="/b" data-trigger="load">
366
+ <div data-get="/b" data-trigger="revealed">
367
+ ```
368
+
369
+ Modifiers:
370
+
371
+ - `delay:500ms`: wait before firing; retrigger restarts the wait.
372
+ Good for search-as-you-type (with `changed`).
373
+ - `throttle:1s`: ignore repeats inside the window.
374
+ - `once`: fire a single time, then detach.
375
+ - `changed`: inputs only; skip when the value is unchanged.
376
+ - `queue:last` (default): a new fetch aborts the previous one.
377
+ `queue:first` and `queue:none`: skip while one is in flight.
378
+ `queue:all`: allow parallel requests.
379
+ - `from:document`, `from:body`, `from:.selector`: listen on another
380
+ root instead of the element.
381
+ - `every 5s`: poll on an interval (cleaned up on removal).
382
+ - `load`: fire once the document is ready.
383
+ - `revealed`: fire when scrolled into view (needs
384
+ IntersectionObserver, fires at once without it).
385
+
386
+ Default event by tag: forms use `submit`, inputs, selects, and
387
+ textareas use `change`, everything else uses `click`.
388
+
389
+ ### data-preserve
390
+
391
+ Marks a node (with an `id`) that survives HTML swaps by identity.
392
+ The live node replaces the fresh copy, keeping video playback,
393
+ third-party widgets, and local state.
394
+
395
+ ```html
396
+ <video id="v" data-preserve src="..."></video>
397
+ ```
398
+
399
+ ### data-morph-skip
400
+
401
+ Morph plugin only. Freezes a subtree during morph swaps.
402
+
403
+ ## 6. JavaScript API reference
404
+
405
+ All methods live on `window.Laghu`.
406
+
407
+ ### Laghu.define(tag, setup)
408
+
409
+ Registers a Light-DOM custom component. Tag must contain a hyphen.
410
+ `setup()` runs fresh per instance and returns state, actions,
411
+ observed attribute names, and an optional onMount hook.
412
+
413
+ - Observed attributes default to primitive state keys. Attribute
414
+ text coerces to the state type (numbers stay numbers).
415
+ - Properties set before registration upgrade into state.
416
+ - Attributes set before connect apply on connect.
417
+ - Sync is one way (attribute into state). Send objects through
418
+ properties or `element.state` instead of attributes.
419
+ - Removal destroys all bindings. `moveBefore` moves keep state.
420
+
421
+ ```js
422
+ Laghu.define('user-card', function () {
423
+ return {
424
+ state: { name: 'Ada', likes: 0 },
425
+ actions: {
426
+ like: function () { this.likes++; }
427
+ },
428
+ observed: ['name'],
429
+ onMount: function () { /* this is the state */ }
430
+ };
431
+ });
432
+ ```
433
+
434
+ ```html
435
+ <user-card name="Grace">
436
+ <span data-bind="name"></span>
437
+ <button data-click="like">like</button>
438
+ </userhtml>
439
+ ```
440
+
441
+ ### Laghu.hydrate(root, state, actions)
442
+
443
+ Scans `root` and wires all bindings. Returns the reactive state.
444
+ Islands with `data-scope` call this automatically on page load.
445
+
446
+ ### Laghu.reactive(obj)
447
+
448
+ Makes a plain object or array observable. Reads track, writes
449
+ trigger. Nested objects wrap on first read.
450
+
451
+ ### Laghu.effect(fn)
452
+
453
+ Runs `fn` now and again whenever state it read changes. Returns a
454
+ stop function. One effect per binding is the normal pattern.
455
+
456
+ ### Laghu.eval(scope, expr)
457
+
458
+ Reads one expression without `new Function` (CSP-safe). Supports
459
+ paths, literals, `!`, comparisons, `&&`, `||`, and no-argument
460
+ calls like `reset()`.
461
+
462
+ ### Laghu.get(scope, path) and Laghu.set(scope, path, val)
463
+
464
+ Dotted path access with scope-chain lookup. `$store.name.key`
465
+ reaches global stores.
466
+
467
+ ### Laghu.parseTrigger(spec, el)
468
+
469
+ Parses `data-trigger` text into a modifier object. Useful for
470
+ testing and custom wiring.
471
+
472
+ ### Laghu.swap(target, html, mode, scope)
473
+
474
+ Swaps HTML and hydrates the result. See modes in section 5.
475
+ Dispatches `laghu:before-swap` and `laghu:after-swap`, preserves
476
+ `data-preserve` nodes, restores focus and caret.
477
+
478
+ ### Laghu.sanitize(fn)
479
+
480
+ Replaces the HTML pass-through used by `data-html` and swaps.
481
+ Install a real sanitizer when rendering server HTML:
482
+
483
+ ```js
484
+ Laghu.sanitize(function (h) { return DOMPurify.sanitize(h); });
485
+ ```
486
+
487
+ ### Laghu.store(name, init)
488
+
489
+ Creates or fetches a named global reactive store. Read it in any
490
+ binding as `$store.cart.n`.
491
+
492
+ ```js
493
+ Laghu.store('cart', { n: 0 });
494
+ Laghu.store('cart').n++;
495
+ ```
496
+
497
+ ```html
498
+ <span data-bind="$store.cart.n"></span>
499
+ ```
500
+
501
+ ### Laghu.directive(name, fn)
502
+
503
+ Registers `data-name`. The function receives `{ el, get, effect,
504
+ scope }` and may return a cleanup function.
505
+
506
+ ```js
507
+ Laghu.directive('focus', function (ctx) {
508
+ var stop = ctx.effect(function () {
509
+ if (ctx.get()) ctx.el.focus();
510
+ });
511
+ return stop;
512
+ });
513
+ ```
514
+
515
+ ```html
516
+ <input data-focus="shouldFocus">
517
+ ```
518
+
519
+ ### Laghu.destroyTree(root)
520
+
521
+ Stops everything under `root`: effects, listeners, timers,
522
+ observers, in-flight fetches. Runs automatically on removal and
523
+ before swaps. Call it manually before wiping DOM yourself.
524
+
525
+ ### Laghu.css and Laghu.version
526
+
527
+ `Laghu.css` is the FOUC guard string (hide unregistered tags, show
528
+ them with `is-defined`). `Laghu.version` is the release string.
529
+
530
+ ### LaghuMorph.morph(target, html, opts)
531
+
532
+ From morph.js (load after laghu.js). Reconciles instead of
533
+ replacing. `opts.mode` is `inner` (default) or `outer`.
534
+ `LaghuMorph.morphEl(a, b)` reconciles two elements directly.
535
+
536
+ ## 7. Events reference
537
+
538
+ All names are CustomEvents. Listen with `addEventListener`:
539
+
540
+ - `laghu:before-request`: cancelable, `detail` has `url`, `method`.
541
+ Call `preventDefault()` to stop the fetch (for confirm dialogs).
542
+ - `laghu:after-request`: `detail` has `status`, `url`.
543
+ - `laghu:request-error`: network failure or non-OK status.
544
+ `detail` has `error` or `status` plus `url`.
545
+ - `laghu:before-swap`: before HTML changes, `detail` has `swap`.
546
+ - `laghu:after-swap`: after hydrate, `detail` has `swap`.
547
+ - `laghu:unknown-action`: `detail` has `name`. Fires on typos.
548
+ - `laghu:mount` and `laghu:unmount`: on component elements.
549
+ - `laghu:poll`, `laghu:load`, `laghu:revealed`: synthetic trigger
550
+ events passed to interval, load, and reveal handlers.
551
+
552
+ ## 8. Fetching data (JSON and HTML)
553
+
554
+ laghu has one rule: JSON merges into state, HTML swaps into the DOM.
555
+
556
+ JSON example. The response object merges with `Object.assign`, so
557
+ bound nodes update through normal reactivity:
558
+
559
+ ```html
560
+ <button data-get="/api/user" data-trigger="click queue:last">
561
+ Load user
562
+ </button>
563
+ <p>Hello, <span data-bind="user.name">stranger</span></p>
564
+ ```
565
+
566
+ HTML example. The fragment swaps into the target and hydrates, so
567
+ new `data-bind` and `data-click` attributes work at once:
568
+
569
+ ```html
570
+ <button data-get="/rows?page=2" data-target="#list" data-swap="append">
571
+ More
572
+ </button>
573
+ <ul id="list">...</ul>
574
+ ```
575
+
576
+ Form example. The enclosing form serializes to JSON automatically:
577
+
578
+ ```html
579
+ <form data-post="/api/save" data-target="#msg">
580
+ <input name="email">
581
+ <button>Save</button>
582
+ <p id="msg" data-bind="result"></p>
583
+ </form>
584
+ ```
585
+
586
+ Confirm example. Cancel in `before-request`:
587
+
588
+ ```js
589
+ document.querySelector('#del').addEventListener('laghu:before-request', function (e) {
590
+ if (!confirm('Delete?')) e.preventDefault();
591
+ });
592
+ ```
593
+
594
+ ## 9. The morph plugin
595
+
596
+ Default swaps replace nodes. Replacement loses input focus, caret,
597
+ scroll position, video playback, and component instances inside the
598
+ target. The morph plugin updates nodes in place instead.
599
+
600
+ Load it after the core:
601
+
602
+ ```html
603
+ <script src="laghu.min.js"></script>
604
+ <script src="morph.min.js"></script>
605
+ ```
606
+
607
+ Then use `morph:inner` (reconcile children) or `morph:outer`
608
+ (reconcile the element itself):
609
+
610
+ ```html
611
+ <ul id="list" data-get="/rows" data-target="#list" data-swap="morph:inner">
612
+ <li id="r1">one</li>
613
+ </ul>
614
+ ```
615
+
616
+ Rules: stable rows need an `id` or `data-bkey`. Unkeyed same-tag
617
+ children reuse positionally. `data-morph-skip` freezes a subtree.
618
+ Focused inputs keep typed values. Removed nodes run their laghu
619
+ cleanups. Without morph.js loaded, `morph:*` warns in development
620
+ and falls back to a plain inner swap.
621
+
622
+ Use morph for server-fed lists and tables. Keep destructive swaps
623
+ for one-shot content where replacement is cheaper and simpler.
624
+
625
+ ## 10. Type safety without TypeScript
626
+
627
+ laghu ships type checking with zero build output. Every source file
628
+ starts with `// @ts-check`, and editors run the TypeScript language
629
+ server on plain `.js` files. JSDoc comments carry the types.
630
+
631
+ To check the whole project:
632
+
633
+ ```sh
634
+ npx tsc --noEmit -p jsconfig.json
635
+ ```
636
+
637
+ This typechecks only and emits nothing. The browser runs the same
638
+ `.js` files untouched.
639
+
640
+ One blind spot: HTML attribute strings are invisible to the
641
+ checker. Renaming `increment` to `add` breaks
642
+ `data-click="increment"` silently at the type level. laghu covers
643
+ this three ways: unknown actions warn in development, every unknown
644
+ action dispatches `laghu:unknown-action`, and the test suite boots
645
+ fixture pages and asserts no unknown-action warnings. Keep action
646
+ names in one registry object and test the fixtures.
647
+
648
+ Library consumers get `dist/laghu.d.ts` with full signatures.
649
+
650
+ ## 11. Content Security Policy notes
651
+
652
+ laghu runs under strict CSP with no `unsafe-eval` and no
653
+ `unsafe-inline`:
654
+
655
+ - No `new Function`, no `eval`, no string `setTimeout`. Expressions
656
+ use a small built-in reader (paths, literals, `!`, comparisons,
657
+ `&&`, `||`, no-argument calls).
658
+ - No inline handlers. All listeners attach with
659
+ `addEventListener` from `data-*` attributes.
660
+ - Ship laghu as an external file with a Subresource Integrity hash.
661
+ - `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.
664
+
665
+ ## 12. Loading flash and SEO notes
666
+
667
+ Empty custom tags render as unknown inline elements until the script
668
+ registers them. Two fixes, best used together:
669
+
670
+ 1. Ship real HTML inside the tag (progressive enhancement). Content
671
+ shows before JS, works without JS, and stays indexable.
672
+ 2. Hide only the unknown state with CSS:
673
+
674
+ ```css
675
+ my-card:not(:defined) { visibility: hidden; }
676
+ ```
677
+
678
+ `visibility` (not `display`) avoids layout shift. `Laghu.css`
679
+ carries a ready-made version of this rule. Pre-rendered Light DOM
680
+ also solves SEO: crawlers and readers see content even when scripts
681
+ never run.
682
+
683
+ ## 13. File sizes
684
+
685
+ Measured after minification:
686
+
687
+ - Core `dist/laghu.min.js`: about 21kB raw, 6.8kB gzip, 6.1kB brotli.
688
+ - Morph `dist/morph.min.js`: about 2.3kB raw, 1kB gzip.
689
+ - Budgets enforced by `node scripts/size.mjs`: core at or under
690
+ 7.5kB gzip, morph at or under 3kB gzip.
691
+
692
+ For scale: htmx alone is about 16kB gzip, Alpine with a few plugins
693
+ is 12 to 15kB. laghu core plus morph stays under half of either.
694
+
695
+ ## 14. Worked examples
696
+
697
+ See the `examples/` folder. Each file opens directly from disk.
698
+
699
+ - `counter.html`: one component, one binding, one action.
700
+ - `todo.html`: island with `data-model` input, keyed `data-for`
701
+ list, checkbox models, `data-show` counter.
702
+ - `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).
704
+
705
+ ## 15. Checking your setup
706
+
707
+ ```sh
708
+ npm install
709
+ npx tsc --noEmit -p jsconfig.json
710
+ node scripts/size.mjs
711
+ node test/run.mjs
712
+ ```
713
+
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.
718
+
719
+ Releasing a new version (publishes to npm through CI):
720
+
721
+ 1. Bump `version` in `package.json` and commit.
722
+ 2. Create a GitHub Release with tag `vX.Y.Z` matching that version
723
+ (example: tag `v1.0.1` for version `1.0.1`).
724
+ 3. The `Publish` workflow verifies types, tests, sizes, tarball
725
+ contents, and a packed install smoke test, then publishes with
726
+ provenance. It refuses when the tree is dirty or the tag and
727
+ version disagree.
728
+ 4. Requirements: an `NPM_TOKEN` secret with publish rights on the
729
+ `laghu` package. To check without uploading, run the workflow
730
+ manually from the Actions tab (dry run is the default).
731
+
732
+ ## 16. Troubleshooting
733
+
734
+ Unknown action warning: the `data-click` or `data-on` name matches
735
+ no action and no state function. Check spelling, or add the action
736
+ to the component setup return value.
737
+
738
+ `data-scope must be JSON` warning: attribute text is not valid
739
+ JSON. Use double quotes inside single quotes:
740
+ `data-scope='{"count": 0}'`.
741
+
742
+ Binding shows nothing: the path is missing, so the effect wrote an
743
+ empty string. Check the path against state, including row variable
744
+ names inside `data-for`.
745
+
746
+ Click does nothing on a fetched button: content swapped without
747
+ hydrate cannot happen through `Laghu.swap` (it hydrates), so this
748
+ means the HTML was inserted manually. Call
749
+ `Laghu.hydrate(parent, state, actions)` or route inserts through
750
+ `Laghu.swap`.
751
+
752
+ Focus jumps on list update: rows need `data-key`, or switch the
753
+ swap to `morph:inner` with row ids.
754
+
755
+ Request never fires with `changed`: the input value never differed.
756
+ `changed` compares against the last fired value per element.
757
+
758
+ morph warns and falls back: `morph.min.js` is not loaded. Add the
759
+ script tag after the core.
760
+
761
+ ## 17. Project files
762
+
763
+ - `src/laghu.js`: core source with full JSDoc, loaded directly in
764
+ development.
765
+ - `src/morph.js`: opt-in morph plugin source with full JSDoc.
766
+ - `dist/laghu.min.js`, `dist/morph.min.js`: minified builds.
767
+ - `dist/laghu.d.ts`: TypeScript declarations for consumers.
768
+ - `test/run.mjs`: jsdom suite, 37 assertions.
769
+ - `scripts/size.mjs`: minifies and enforces gzip budgets.
770
+ - `examples/`: three pages opening from disk.
771
+ - `jsconfig.json`: strict `checkJs` config used by editors and CI.