kerfjs 0.3.1 → 0.5.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 CHANGED
@@ -4,6 +4,46 @@ All notable changes to **kerf** are documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [0.5.0] - 2026-05-10
8
+
9
+
10
+ - Add `arraySignal` (`kerfjs/array-signal` subpath) — granular collection signal that drives O(patches) DOM updates for keyed lists
11
+ - Faster keyed-list updates: bulk-parse contiguous insert runs and consecutive update patches in the granular reconcile path
12
+ - Perf optimisations on the `each()` / `mount()` update path; benchmarks now competitive with Solid/Vue on swap/remove/clear
13
+ - Preserve uncontrolled `<details open>` and `<dialog open>` state across re-renders
14
+ - `each()` now reconciles correctly when list rows have non-list siblings under the same parent
15
+ - Enforce the "exactly one top-level element per row" contract in `each()` with clearer errors
16
+ - Typed JSX `IntrinsicElements` table; custom elements extend it via declaration merging
17
+ - JSX runtime hardens URL attributes against `javascript:` XSS
18
+ - Widen `mount()` return type for better tooling/typed usage
19
+ - Add `kerfjs/testing` subpath exposing `clearStoreRegistry` for unit-test isolation
20
+
21
+ ## [0.4.2] - 2026-05-09
22
+
23
+
24
+ - No user-facing changes in this release.
25
+
26
+ ## [0.4.1] - 2026-05-09
27
+
28
+
29
+ - Just fixing the build
30
+
31
+ ## [0.4.0] - 2026-05-09
32
+
33
+
34
+ - Auto-promote known non-bubbling events (focus, blur, scroll, etc.) to capture phase in `delegate()`
35
+
36
+ ## [0.4.0] - 2026-05-09
37
+
38
+
39
+ - `delegate()` now auto-promotes the seven well-known non-bubbling events (`focus`, `blur`, `scroll`, `load`, `error`, `mouseenter`, `mouseleave`) to capture phase, with `closest()`-style selector matching preserved
40
+ - Fixed focus and caret position loss when reordering keyed `each()` rows on engines that drop focus on `insertBefore` (older Safari, happy-dom)
41
+ - `mount()` now throws a descriptive error when the root element is null/undefined instead of a generic "Cannot set properties of null"
42
+ - `each()` now throws a descriptive error naming the offending index when an item is a primitive (per-item cache requires objects)
43
+ - Minimum Node version bumped to 22.12+
44
+ - New Starlight-powered docs site at `/kerf/` with inline live examples and runnable complete apps; the reactivity demo moved to `/kerf/demo/`
45
+ - New kerf brand identity: production logo, full favicon set (SVG + PNG + ICO + Apple touch icon), and PWA manifest
46
+
7
47
  ## [0.3.1] - 2026-05-08
8
48
 
9
49
 
@@ -41,11 +81,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
41
81
 
42
82
  ### Fixed
43
83
 
84
+ - **Focus was sometimes lost on `each()` reorders (KF-65).** When the keyed list reconciler moved a row whose descendant held focus, `insertBefore` blurred the element on engines that don't preserve focus across DOM moves (older Safari, happy-dom). The element survived in the live tree, but `document.activeElement` reverted to `<body>` and the user's typing was interrupted. The reconciler now snapshots the active element + its selection range (when applicable) before the move pass and re-applies them after, so focus and caret position survive a reorder uniformly across engines. Engines that already preserve focus across moves see a no-op — the snapshot only takes effect when the active element changed. `docs/4-render.md` §4.4 and `docs/8-api-reference.md` updated. New regression tests in `tests/unit/mount.test.ts` cover reorder, top-insert, focused-row removal, non-text focused elements, selection-API rejection (e.g. `type=number`), and the "active element is outside the list" path.
44
85
  - **`Fragment` was missing from the `kerfjs` barrel (KF-24).** `Fragment` was implemented in `src/jsx-runtime.ts`, exported from `kerfjs/jsx-runtime`, and present in the shared chunk — but the barrel `src/index.ts` didn't re-export it. Importing `Fragment` from `'kerfjs'` resolved to `undefined`, so a manual `<Fragment>...</Fragment>` rendered as `<undefined>...</undefined>`. The `<>...</>` shorthand was unaffected because the JSX transform pulls `Fragment` from `kerfjs/jsx-runtime` directly. Added `Fragment` to the barrel re-export, and pinned the entire public-API contract with a new `tests/dist/barrel-completeness.test.ts` so any future omission fails CI loudly. Docs updated to list `Fragment` in the public API surface (`CLAUDE.md`, `llms.txt`, `docs/ai/usage-guide.md`, `docs/ai/code-summary.md`, `docs/6-jsx-runtime.md`, `docs/8-api-reference.md`).
45
86
  - **Focused contenteditable was being morphed, clobbering in-progress edits (KF-19).** The docs claimed contenteditable elements got focus + selection preservation alongside `<input>` and `<textarea>`, but the implementation only handled the latter two — a focused contenteditable's typed content was overwritten by morphdom on the next re-render. `mount()` now short-circuits the morph entirely when the active element is a contenteditable (same mechanism as `data-morph-skip`), so the user's edit, caret position, and any multi-range selection survive verbatim. Attribute updates are deferred until the next render after blur — that's the explicit trade-off, and matches what you want for in-progress rich-text editing. `docs/4-render.md` §4.4 and `docs/8-api-reference.md` §8.7 updated to describe the per-element-kind behaviour. The check uses the `contenteditable` attribute directly (the spec's source of truth) rather than the derived `isContentEditable` property, so test environments that don't populate the latter still get correct behaviour.
46
87
  - **`clearStoreRegistry` was a no-op in the published bundle (KF-15).** `dist/testing.js` shipped an empty function body. Root cause: `tsup` bundled each entry independently with `splitting: false`, so the testing entry tree-shook the module-level `REGISTRY` array out as unreferenced — leaving `REGISTRY.length = 0` as dead code. Same root cause as KF-14. Fixed by enabling `splitting: true` in `tsup.config.ts`: shared modules now live in chunk files that all entries import, so `defineStore`'s registry and `clearStoreRegistry`'s reference are the same array. Side benefit: the duplicate `SafeHtml` class definition is gone too — there's now exactly one copy across the whole dist. Build output now includes `dist/chunk-*.js` files (covered by the existing `"files": ["dist"]` in `package.json`). New regression test in `tests/dist/store-registry-shared.test.ts` exercises the cross-entry registry from the built bundles.
47
88
  - **`SafeHtml` cross-bundle identity (KF-14).** When a consumer's bundler ended up loading two copies of kerf — for example, the barrel (`kerfjs`) and the JSX-runtime entry (`kerfjs/jsx-runtime`) resolving as separate modules — `instanceof SafeHtml` failed inside the JSX runtime because the two `SafeHtml` classes were structurally identical but referentially distinct. The renderer would then throw `JSX: unsupported child of type object (SafeHtml)` on perfectly valid JSX. `SafeHtml` instances now carry a `Symbol.for('kerfjs.SafeHtml')` brand and the runtime checks for the brand instead of using `instanceof`. New unit tests simulate the duplicate-class scenario, and a new `npm run test:dist` job exercises the actual built bundles in CI.
48
89
 
90
+ ### Changed
91
+
92
+ - **`delegate()` now auto-promotes the well-known non-bubbling event types to capture phase (KF-56).** `focus`, `blur`, `scroll`, `load`, `error`, `mouseenter`, `mouseleave` previously needed `delegateCapture()`; with auto-promotion the call site is identical to bubbling events. Selector matching stays `closest()`-style for every type, including the auto-promoted ones — so `delegate(root, 'focus', '.field-row', ...)` fires when a descendant `<input>` is focused, with the row as the matched element (not the input). `delegateCapture()` remains as the explicit-capture escape hatch with its `target.matches()`-style direct matching. No bundle-size change worth measuring. `docs/5-event-delegation.md` and `docs/8-api-reference.md` §8.4 updated. New unit tests in `tests/unit/delegate.test.ts` cover the `closest()` walk-up on auto-promoted events, the disposer path, and a phase-check for bubbling events to confirm the promotion is type-gated.
93
+
49
94
  ### Added
50
95
 
51
96
  - **`each(items, render, key?)` list primitive** exported from `kerfjs`. Keyed list iteration with per-item memoisation: skips re-running `render` for items whose object identity (and optional `key`) are unchanged since the previous call. Targets the partial-update / select-row / swap-rows perf path, where today's `mount()` re-runs the render for the full list on any signal change. On the js-framework-benchmark suite this drops kerfjs's partial-update from 87 → 64 ms (-27%), select-row from 69 → 42 ms (-38%), swap-rows from 86 → 58 ms (-33%), and remove-row from 49 → 35 ms (-29%); creates and bundle size are unaffected (+0.2 KB gz for the WeakMap memoiser). See `docs/8-api-reference.md` §8.3 and `bench/` for the benchmark harness.
package/README.md CHANGED
@@ -1,10 +1,18 @@
1
- # kerf
1
+ <p align="center">
2
+ <img src="./site/src/assets/logo.svg" alt="Kerf logo" width="96" height="96" />
3
+ </p>
2
4
 
3
- > *kerf* — *noun* — the narrow strip of material a saw blade removes when cutting. The smallest possible cut.
5
+ <h1 align="center">Kerf</h1>
4
6
 
5
- A tiny reactive UI framework. Apply the smallest possible cut to update your DOM.
7
+ <p align="center"><em>The smallest cut.</em></p>
6
8
 
7
- **[Live demo →](https://brianwestphal.github.io/kerf/)** — seven sections exercising every primitive, no install required.
9
+ ---
10
+
11
+ > Introducing Kerf.
12
+ > The smallest cut.
13
+ >
14
+ > 6.1 KB. No virtual DOM. No compiler. No magic.
15
+ > Reactive UI that touches only the bytes that changed.
8
16
 
9
17
  ```ts
10
18
  import { signal, mount } from 'kerfjs';
@@ -19,41 +27,42 @@ mount(document.getElementById('app')!, () => (
19
27
  ));
20
28
  ```
21
29
 
22
- That's it. There's no virtual DOM, no compiler, no template language. Your JSX renders to HTML strings (with structured "list" segments where you use `each(...)`), kerf's native diff applies the minimum DOM mutations to make the live tree match, and signals re-run the render only when something they read actually changed.
30
+ That's it. Your JSX renders to HTML strings, kerf's native diff applies the minimum DOM mutations to make the live tree match, and signals re-run the render only when something they read actually changed.
23
31
 
24
- ## Why
32
+ ## Why Kerf
25
33
 
26
- Most reactive UI frameworks come with a lot of machinery: virtual DOMs, schedulers, reconcilers, compiler plugins, hook stacks, lifecycle hooks. kerf has none of that. You get four things:
34
+ 1. **Built for the AI-assisted era.** Tiny public surface (15 exports), no compiler magic, no hidden lifecycle. An LLM holds the framework in context and predicts behaviour — your AI agent generates code that works the first time. Ships [`llms.txt`](./llms.txt) and a dedicated AI usage guide; the [Built by an AI · Pomodoro](https://brianwestphal.github.io/kerf/examples/complete/built-by-an-ai/) example is a working app one-shotted by Claude with `llms.txt` as its only kerf knowledge.
27
35
 
28
- - **Signals** ([`@preact/signals-core`](https://github.com/preactjs/signals)) for fine-grained reactivity.
29
- - **Stores** built on signals — composable, testable units of state.
30
- - **Render** — a `mount(el, () => jsx)` helper that diffs the new HTML against the live DOM with kerf's native, segment-aware reconciler. Preserves focus, selection, in-flight pointer interactions, and event listeners on identity-preserved nodes. Lists rendered with `each(...)` go through a keyed reconciler that does O(changes) work, not O(rows).
31
- - **Event delegation** — small `delegate` / `delegateCapture` helpers that survive every re-render because they live on the morph root, not on individual nodes.
36
+ 2. **Smallest cut.** 6.1 KB gzipped including signals (6.5 KB with `arraySignal`). Fine-grained reactivity re-runs only what changed; the diff touches only the DOM nodes that differ. On the [krausest js-framework-benchmark](./bench/results.md) kerf is competitive with Solid and Vue on swap-rows, remove-row, and clear — no compiler required.
32
37
 
33
- The whole runtime is roughly 6.6 KB minified + gzipped, including `signals-core`.
38
+ 3. **No virtual DOM, no compiler.** JSX → HTML strings → native diff. DevTools shows the real DOM because it *is* the DOM.
34
39
 
35
- ## Install
40
+ 4. **Focus, selection, listeners survive re-renders.** We morph instead of rebuilding — your caret stays where you put it, your in-progress drag keeps moving, your delegated handlers keep firing.
36
41
 
37
- ```bash
38
- npm install kerfjs
39
- ```
42
+ 5. **Plain TS, plain JSX, plain ESM.** Drops into anything using esbuild / Vite / tsup. No plugin chain.
40
43
 
41
- Configure JSX:
44
+ ## When to use Kerf
42
45
 
43
- ```jsonc
44
- // tsconfig.json
45
- {
46
- "compilerOptions": {
47
- "jsx": "react-jsx",
48
- "jsxImportSource": "kerfjs"
49
- }
50
- }
51
- ```
46
+ - **AI-generated apps** — your LLM/agent holds the framework in context; no hallucinated APIs.
47
+ - **Hybrid desktop apps (Tauri / Electron)** — small bundle, predictable diff, debuggable runtime; ideal for the embedded webview.
48
+ - **Embedded widgets** — chat bubbles, comment boxes, dashboards dropped into someone else's page.
49
+ - **Server-rendered apps with islands** — Rails / Phoenix / Django / Hono. `mount` per island; `delegate` survives turbo-frame swaps.
50
+ - **Admin panels & internal tools** — reactivity without 200 KB of framework + state lib + router.
51
+ - **Replacing jQuery** — incremental migration; same delegation mental model, modern primitives.
52
+ - **Prototyping** — entire mental model on a postcard.
53
+
54
+ ### When to reach for something else
55
+
56
+ - Need a full ecosystem (router + forms + data + SSR streaming) → **Next.js / Remix / SolidStart**.
57
+ - Building a deeply componentised design-system app → **React / Solid / Svelte**.
58
+ - Need React Native / cross-platform mobile → **React** (Kerf + Tauri/Electron also covers many of these cases).
59
+ - Building a static site → **Astro** (we use it for *this* project's site).
60
+ - Already invested in a framework where switching cost outweighs the ~6 KB win.
52
61
 
53
62
  ## Quick tour
54
63
 
55
64
  ```ts
56
- import { signal, computed, effect, defineStore, mount, delegate } from 'kerfjs';
65
+ import { signal, computed, effect, defineStore, mount, each, delegate } from 'kerfjs';
57
66
 
58
67
  // 1. A signal — single piece of reactive state.
59
68
  const count = signal(0);
@@ -77,12 +86,16 @@ mount(root, () => (
77
86
  <div>
78
87
  <h1>Cart ({cart.state.value.items.length})</h1>
79
88
  <ul>
80
- {cart.state.value.items.map((item) => (
81
- <li data-key={item.id}>
82
- {item.name}
83
- <button data-action="remove" data-id={item.id}>×</button>
84
- </li>
85
- ))}
89
+ {each(
90
+ cart.state.value.items,
91
+ (item) => (
92
+ <li>
93
+ {item.name}
94
+ <button data-action="remove" data-id={item.id}>×</button>
95
+ </li>
96
+ ),
97
+ (item) => item.id,
98
+ )}
86
99
  </ul>
87
100
  <p>Doubled count: {doubled.value}</p>
88
101
  </div>
@@ -94,40 +107,59 @@ delegate(root, 'click', '[data-action="remove"]', (_e, btn) => {
94
107
  });
95
108
  ```
96
109
 
97
- ## Documentation
98
-
99
- The numbered docs in [`docs/`](./docs/) cover the design and rationale:
110
+ ### Long keyed lists: `arraySignal`
100
111
 
101
- 1. [Overview](./docs/1-overview.md) — what kerf is, what it isn't, when to use it.
102
- 2. [Reactivity](./docs/2-reactivity.md) — `signal`, `computed`, `effect`, `batch`.
103
- 3. [Stores](./docs/3-stores.md) — `defineStore`, `resetAllStores`.
104
- 4. [Render](./docs/4-render.md) — `mount`, segments, the native diff, and the list reconciler.
105
- 5. [Event delegation](./docs/5-event-delegation.md) — Tier 1 / Tier 2 / Tier 3 patterns.
106
- 6. [JSX runtime](./docs/6-jsx-runtime.md) — `SafeHtml`, `raw`, server-rendering.
107
- 7. [SVG handling](./docs/7-svg.md) — namespace propagation, `toElement`.
108
- 8. [API reference](./docs/8-api-reference.md) — every export, every option.
109
- 9. [Live demo](./docs/9-live-demo.md) — the GitHub Pages deploy of `examples/reactivity-demo`.
112
+ For lists where most updates are pointwise (single-row edits, append-to-end, selection flips on individual rows), reach for `arraySignal` from the `kerfjs/array-signal` subpath. Mutators emit typed patches that `each()` applies in O(patches), not O(N):
110
113
 
111
- ### For AI coding assistants
114
+ ```ts
115
+ import { arraySignal } from 'kerfjs/array-signal';
112
116
 
113
- If you are an AI assistant (Claude, Cursor, Copilot, etc.) helping a user choose or use kerf, start here:
117
+ const rows = arraySignal<{ id: number; label: string }>([]);
114
118
 
115
- - [`docs/ai/usage-guide.md`](./docs/ai/usage-guide.md) — when to recommend kerf, the full public API at a glance, the four core patterns, hard rules, and a common-errors → fixes table. Designed to be read once before writing kerf code.
116
- - [`llms.txt`](./llms.txt) — top-level index of every doc, in the [llmstxt.org](https://llmstxt.org) format.
119
+ mount(root, () => (
120
+ <ul>{each(rows, (r) => <li data-key={r.id}>{r.label}</li>)}</ul>
121
+ ));
117
122
 
118
- ## Examples
123
+ rows.push({ id: 1, label: 'a' }); // 1 insert patch
124
+ rows.update(0, (r) => ({ ...r, label: 'A' })); // 1 update patch
125
+ rows.move(0, 1); // 1 move patch
126
+ ```
119
127
 
120
- [`examples/reactivity-demo/`](./examples/reactivity-demo) is a 7-section live demo exercising every primitive: counter, multi-consumer store, focus survival across re-renders, keyed list with identity preservation, morph-skip for library-owned subtrees, JSX-rendered SVG, and capture-phase event delegation.
128
+ The class lives in its own subpath so apps that don't need it shed ~1 KB. Reads on `rows.value` are tracking, so `computed(() => rows.value.filter(...))` works as expected. See [`docs/2-reactivity.md`](./docs/2-reactivity.md) §2.6.
121
129
 
122
- Play with it live at **[brianwestphal.github.io/kerf](https://brianwestphal.github.io/kerf/)**, or run it locally:
130
+ ## Install
123
131
 
124
132
  ```bash
125
- npm run example:reactivity-demo
133
+ npm install kerfjs
126
134
  ```
127
135
 
136
+ ```jsonc
137
+ // tsconfig.json
138
+ {
139
+ "compilerOptions": {
140
+ "jsx": "react-jsx",
141
+ "jsxImportSource": "kerfjs"
142
+ }
143
+ }
144
+ ```
145
+
146
+ ## Links
147
+
148
+ - **Site:** [brianwestphal.github.io/kerf](https://brianwestphal.github.io/kerf/)
149
+ - **Docs:** [`docs/`](./docs/) — overview · reactivity · stores · render · events · jsx · svg · [API reference](./docs/8-api-reference.md)
150
+ - **AI guide:** [`docs/ai/usage-guide.md`](./docs/ai/usage-guide.md) — read once before writing kerf code with an LLM
151
+ - **Demo:** [live demo](https://brianwestphal.github.io/kerf/demo/) — eight sections exercising every primitive (counter, store-backed cart, focus survival, keyed list, morph-skip, SVG render, Tier-2 capture, `arraySignal` patches)
152
+ - **Repo:** [github.com/brianwestphal/kerf](https://github.com/brianwestphal/kerf)
153
+
154
+ ## Why "kerf"?
155
+
156
+ A *kerf* is the narrow strip of material a saw blade removes when cutting — the smallest possible cut. The framework's job is the same: apply the smallest possible mutation to update your DOM.
157
+
158
+ (And yes, ~~kerformance~~ → *performance* jokes were written. They were also rejected.)
159
+
128
160
  ## Status
129
161
 
130
- v0.3.x — early. API may evolve. See [CHANGELOG.md](./CHANGELOG.md) for what's shipped.
162
+ Pre-1.0 — API may evolve. See [CHANGELOG.md](./CHANGELOG.md) for the current version and what's shipped.
131
163
 
132
164
  ## License
133
165
 
@@ -0,0 +1,86 @@
1
+ /**
2
+ * `arraySignal(initial)` — granular collection signal.
3
+ *
4
+ * A keyed-list-friendly variant of `signal()` that emits typed patch events
5
+ * for every mutation (update / insert / remove / move / replace). When such
6
+ * a signal is bound to `each(...)` inside a `mount()`, the keyed list
7
+ * reconciler applies just the patches against the live DOM — no per-item
8
+ * iteration, no `classifyItems` Map build, no LIS pass over unchanged rows.
9
+ *
10
+ * const rows = arraySignal<Row>([]);
11
+ *
12
+ * rows.update(42, (r) => ({ ...r, label: 'changed' })); // 1 update event
13
+ * rows.insert(0, { id: 'x', ... }); // 1 insert event
14
+ * rows.remove(7); // 1 remove event
15
+ * rows.move(3, 0); // 1 move event
16
+ * rows.replace([...]); // falls back to snapshot reconcile
17
+ *
18
+ * Read-side semantics match a regular signal: `arraySig.value` is a
19
+ * snapshot, and reads inside `effect()` / `computed()` register as
20
+ * dependencies, so derived values keep working.
21
+ */
22
+ /** A single granular mutation event. */
23
+ type ArrayPatch<T> = {
24
+ type: 'update';
25
+ index: number;
26
+ item: T;
27
+ } | {
28
+ type: 'insert';
29
+ index: number;
30
+ item: T;
31
+ } | {
32
+ type: 'remove';
33
+ index: number;
34
+ } | {
35
+ type: 'move';
36
+ from: number;
37
+ to: number;
38
+ } | {
39
+ type: 'replace';
40
+ items: readonly T[];
41
+ };
42
+ /**
43
+ * Cross-bundle brand for `ArraySignal` instances. `each()` and the
44
+ * granular reconciler check for this brand instead of `instanceof
45
+ * ArraySignal`, so the main `kerfjs` barrel can detect arraySignal
46
+ * inputs without importing the class at runtime — the class lives
47
+ * only in the `kerfjs/array-signal` subpath, so apps that don't need
48
+ * granular collections shed ~1 KB.
49
+ *
50
+ * Same `Symbol.for(...)`-based pattern as `SafeHtml` (KF-14): cross-
51
+ * bundle-safe, zero-cost runtime check.
52
+ */
53
+ declare const ARRAY_SIGNAL_BRAND: unique symbol;
54
+ declare class ArraySignal<T> {
55
+ private _items;
56
+ private _version;
57
+ private _patches;
58
+ readonly [ARRAY_SIGNAL_BRAND]: true;
59
+ constructor(initial?: readonly T[]);
60
+ /** Read-only snapshot. Reads inside an effect/computed register a dependency. */
61
+ get value(): readonly T[];
62
+ /** Replace the item at `index` with `fn(currentItem)`. Emits one `update` patch. */
63
+ update(index: number, fn: (item: T) => T): void;
64
+ /** Insert `item` at `index`. Existing items at index..N shift right. Emits one `insert` patch. */
65
+ insert(index: number, item: T): void;
66
+ /** Append `item` at the end. Sugar for `insert(items.length, item)`. */
67
+ push(item: T): void;
68
+ /** Remove and return the item at `index`. Emits one `remove` patch. */
69
+ remove(index: number): T;
70
+ /** Move the item at `from` to position `to`. Emits one `move` patch (no-op when from === to). */
71
+ move(from: number, to: number): void;
72
+ /** Replace every item. Emits one `replace` patch — the granular reconciler falls back to a full keyed diff for this case. */
73
+ replace(items: readonly T[]): void;
74
+ /**
75
+ * @internal Used by `each()` when binding this signal to a list. Returns
76
+ * the queue of granular patches issued since the previous call, then
77
+ * clears the queue. Best paired with a single binding — a second consumer
78
+ * in the same render gets an empty array (which forces the snapshot
79
+ * fall-back path, which is correct but slower).
80
+ */
81
+ _consumePatches(): ArrayPatch<T>[];
82
+ }
83
+ /** Construct an array signal seeded with `initial`. */
84
+ declare function arraySignal<T>(initial?: readonly T[]): ArraySignal<T>;
85
+
86
+ export { ARRAY_SIGNAL_BRAND, type ArrayPatch, ArraySignal, arraySignal };
@@ -0,0 +1,98 @@
1
+ import { signal } from './chunk-FN2ID4QO.js';
2
+
3
+ // src/array-signal.ts
4
+ var ARRAY_SIGNAL_BRAND = /* @__PURE__ */ Symbol.for("kerfjs.ArraySignal");
5
+ var ArraySignal = class {
6
+ _items;
7
+ _version;
8
+ _patches;
9
+ // Branded so `isArraySignal()` recognises instances from any copy of this module.
10
+ [ARRAY_SIGNAL_BRAND] = true;
11
+ constructor(initial = []) {
12
+ this._items = [...initial];
13
+ this._version = signal(0);
14
+ this._patches = [];
15
+ }
16
+ /** Read-only snapshot. Reads inside an effect/computed register a dependency. */
17
+ get value() {
18
+ void this._version.value;
19
+ return this._items;
20
+ }
21
+ /** Replace the item at `index` with `fn(currentItem)`. Emits one `update` patch. */
22
+ update(index, fn) {
23
+ if (index < 0 || index >= this._items.length) {
24
+ throw new Error(
25
+ `arraySignal.update: index ${index} out of bounds [0, ${this._items.length}).`
26
+ );
27
+ }
28
+ const next = fn(this._items[index]);
29
+ this._items[index] = next;
30
+ this._patches.push({ type: "update", index, item: next });
31
+ this._version.value++;
32
+ }
33
+ /** Insert `item` at `index`. Existing items at index..N shift right. Emits one `insert` patch. */
34
+ insert(index, item) {
35
+ if (index < 0 || index > this._items.length) {
36
+ throw new Error(
37
+ `arraySignal.insert: index ${index} out of bounds [0, ${this._items.length}].`
38
+ );
39
+ }
40
+ this._items.splice(index, 0, item);
41
+ this._patches.push({ type: "insert", index, item });
42
+ this._version.value++;
43
+ }
44
+ /** Append `item` at the end. Sugar for `insert(items.length, item)`. */
45
+ push(item) {
46
+ this.insert(this._items.length, item);
47
+ }
48
+ /** Remove and return the item at `index`. Emits one `remove` patch. */
49
+ remove(index) {
50
+ if (index < 0 || index >= this._items.length) {
51
+ throw new Error(
52
+ `arraySignal.remove: index ${index} out of bounds [0, ${this._items.length}).`
53
+ );
54
+ }
55
+ const [removed] = this._items.splice(index, 1);
56
+ this._patches.push({ type: "remove", index });
57
+ this._version.value++;
58
+ return removed;
59
+ }
60
+ /** Move the item at `from` to position `to`. Emits one `move` patch (no-op when from === to). */
61
+ move(from, to) {
62
+ if (from === to) return;
63
+ if (from < 0 || from >= this._items.length || to < 0 || to >= this._items.length) {
64
+ throw new Error(
65
+ `arraySignal.move: indices out of bounds (from=${from}, to=${to}, length=${this._items.length}).`
66
+ );
67
+ }
68
+ const [item] = this._items.splice(from, 1);
69
+ this._items.splice(to, 0, item);
70
+ this._patches.push({ type: "move", from, to });
71
+ this._version.value++;
72
+ }
73
+ /** Replace every item. Emits one `replace` patch — the granular reconciler falls back to a full keyed diff for this case. */
74
+ replace(items) {
75
+ this._items = [...items];
76
+ this._patches.push({ type: "replace", items: this._items });
77
+ this._version.value++;
78
+ }
79
+ /**
80
+ * @internal Used by `each()` when binding this signal to a list. Returns
81
+ * the queue of granular patches issued since the previous call, then
82
+ * clears the queue. Best paired with a single binding — a second consumer
83
+ * in the same render gets an empty array (which forces the snapshot
84
+ * fall-back path, which is correct but slower).
85
+ */
86
+ _consumePatches() {
87
+ const out = this._patches;
88
+ this._patches = [];
89
+ return out;
90
+ }
91
+ };
92
+ function arraySignal(initial = []) {
93
+ return new ArraySignal(initial);
94
+ }
95
+
96
+ export { ARRAY_SIGNAL_BRAND, ArraySignal, arraySignal };
97
+ //# sourceMappingURL=array-signal.js.map
98
+ //# sourceMappingURL=array-signal.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/array-signal.ts"],"names":[],"mappings":"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","file":"array-signal.js","sourcesContent":["/**\n * `arraySignal(initial)` — granular collection signal.\n *\n * A keyed-list-friendly variant of `signal()` that emits typed patch events\n * for every mutation (update / insert / remove / move / replace). When such\n * a signal is bound to `each(...)` inside a `mount()`, the keyed list\n * reconciler applies just the patches against the live DOM — no per-item\n * iteration, no `classifyItems` Map build, no LIS pass over unchanged rows.\n *\n * const rows = arraySignal<Row>([]);\n *\n * rows.update(42, (r) => ({ ...r, label: 'changed' })); // 1 update event\n * rows.insert(0, { id: 'x', ... }); // 1 insert event\n * rows.remove(7); // 1 remove event\n * rows.move(3, 0); // 1 move event\n * rows.replace([...]); // falls back to snapshot reconcile\n *\n * Read-side semantics match a regular signal: `arraySig.value` is a\n * snapshot, and reads inside `effect()` / `computed()` register as\n * dependencies, so derived values keep working.\n */\n\nimport type { Signal } from './reactive.js';\nimport { signal } from './reactive.js';\n\n/** A single granular mutation event. */\nexport type ArrayPatch<T> =\n | { type: 'update'; index: number; item: T }\n | { type: 'insert'; index: number; item: T }\n | { type: 'remove'; index: number }\n | { type: 'move'; from: number; to: number }\n | { type: 'replace'; items: readonly T[] };\n\n/**\n * Cross-bundle brand for `ArraySignal` instances. `each()` and the\n * granular reconciler check for this brand instead of `instanceof\n * ArraySignal`, so the main `kerfjs` barrel can detect arraySignal\n * inputs without importing the class at runtime — the class lives\n * only in the `kerfjs/array-signal` subpath, so apps that don't need\n * granular collections shed ~1 KB.\n *\n * Same `Symbol.for(...)`-based pattern as `SafeHtml` (KF-14): cross-\n * bundle-safe, zero-cost runtime check.\n */\nexport const ARRAY_SIGNAL_BRAND = Symbol.for('kerfjs.ArraySignal');\n\nexport class ArraySignal<T> {\n private _items: T[];\n private _version: Signal<number>;\n private _patches: ArrayPatch<T>[];\n // Branded so `isArraySignal()` recognises instances from any copy of this module.\n readonly [ARRAY_SIGNAL_BRAND] = true as const;\n\n constructor(initial: readonly T[] = []) {\n this._items = [...initial];\n this._version = signal(0);\n this._patches = [];\n }\n\n /** Read-only snapshot. Reads inside an effect/computed register a dependency. */\n get value(): readonly T[] {\n // Touch the version signal so signals-core treats reads as tracked.\n void this._version.value;\n return this._items;\n }\n\n /** Replace the item at `index` with `fn(currentItem)`. Emits one `update` patch. */\n update(index: number, fn: (item: T) => T): void {\n if (index < 0 || index >= this._items.length) {\n throw new Error(\n `arraySignal.update: index ${index} out of bounds [0, ${this._items.length}).`,\n );\n }\n const next = fn(this._items[index]);\n this._items[index] = next;\n this._patches.push({ type: 'update', index, item: next });\n this._version.value++;\n }\n\n /** Insert `item` at `index`. Existing items at index..N shift right. Emits one `insert` patch. */\n insert(index: number, item: T): void {\n if (index < 0 || index > this._items.length) {\n throw new Error(\n `arraySignal.insert: index ${index} out of bounds [0, ${this._items.length}].`,\n );\n }\n this._items.splice(index, 0, item);\n this._patches.push({ type: 'insert', index, item });\n this._version.value++;\n }\n\n /** Append `item` at the end. Sugar for `insert(items.length, item)`. */\n push(item: T): void {\n this.insert(this._items.length, item);\n }\n\n /** Remove and return the item at `index`. Emits one `remove` patch. */\n remove(index: number): T {\n if (index < 0 || index >= this._items.length) {\n throw new Error(\n `arraySignal.remove: index ${index} out of bounds [0, ${this._items.length}).`,\n );\n }\n const [removed] = this._items.splice(index, 1);\n this._patches.push({ type: 'remove', index });\n this._version.value++;\n return removed;\n }\n\n /** Move the item at `from` to position `to`. Emits one `move` patch (no-op when from === to). */\n move(from: number, to: number): void {\n if (from === to) return;\n if (from < 0 || from >= this._items.length || to < 0 || to >= this._items.length) {\n throw new Error(\n `arraySignal.move: indices out of bounds (from=${from}, to=${to}, length=${this._items.length}).`,\n );\n }\n const [item] = this._items.splice(from, 1);\n this._items.splice(to, 0, item);\n this._patches.push({ type: 'move', from, to });\n this._version.value++;\n }\n\n /** Replace every item. Emits one `replace` patch — the granular reconciler falls back to a full keyed diff for this case. */\n replace(items: readonly T[]): void {\n this._items = [...items];\n this._patches.push({ type: 'replace', items: this._items });\n this._version.value++;\n }\n\n /**\n * @internal Used by `each()` when binding this signal to a list. Returns\n * the queue of granular patches issued since the previous call, then\n * clears the queue. Best paired with a single binding — a second consumer\n * in the same render gets an empty array (which forces the snapshot\n * fall-back path, which is correct but slower).\n */\n _consumePatches(): ArrayPatch<T>[] {\n const out = this._patches;\n this._patches = [];\n return out;\n }\n}\n\n/** Construct an array signal seeded with `initial`. */\nexport function arraySignal<T>(initial: readonly T[] = []): ArraySignal<T> {\n return new ArraySignal(initial);\n}\n"]}
@@ -0,0 +1,3 @@
1
+ export { batch, computed, effect, signal } from '@preact/signals-core';
2
+ //# sourceMappingURL=chunk-FN2ID4QO.js.map
3
+ //# sourceMappingURL=chunk-FN2ID4QO.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"chunk-FN2ID4QO.js","sourcesContent":[]}
@@ -1,7 +1,4 @@
1
- import { signal } from '@preact/signals-core';
2
- export { batch, computed, effect, signal } from '@preact/signals-core';
3
-
4
- // src/reactive.ts
1
+ import { signal } from './chunk-FN2ID4QO.js';
5
2
 
6
3
  // src/store.ts
7
4
  var REGISTRY = [];
@@ -30,5 +27,5 @@ function clearStoreRegistry() {
30
27
  }
31
28
 
32
29
  export { clearStoreRegistry, defineStore, resetAllStores };
33
- //# sourceMappingURL=chunk-IZJIKRCE.js.map
34
- //# sourceMappingURL=chunk-IZJIKRCE.js.map
30
+ //# sourceMappingURL=chunk-GQGJFCWL.js.map
31
+ //# sourceMappingURL=chunk-GQGJFCWL.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/store.ts"],"names":[],"mappings":";;;AAqCA,IAAM,WAAyC,EAAC;AAEzC,SAAS,YACd,IAAA,EACyB;AACzB,EAAA,MAAM,QAAA,GAA2B,MAAA,CAAO,IAAA,CAAK,OAAA,EAAS,CAAA;AAEtD,EAAA,MAAM,GAAA,GAAM,CAAC,IAAA,KAAuB;AAClC,IAAA,QAAA,CAAS,KAAA,GAAQ,IAAA;AAAA,EACnB,CAAA;AACA,EAAA,MAAM,GAAA,GAAM,MAAc,QAAA,CAAS,KAAA;AAEnC,EAAA,MAAM,OAAA,GAAU,IAAA,CAAK,OAAA,CAAQ,GAAA,EAAK,GAAG,CAAA;AAErC,EAAA,MAAM,KAAA,GAAiC;AAAA,IACrC,KAAA,EAAO,QAAA;AAAA,IACP,OAAA;AAAA,IACA,KAAA,GAAQ;AACN,MAAA,QAAA,CAAS,KAAA,GAAQ,KAAK,OAAA,EAAQ;AAAA,IAChC;AAAA,GACF;AAEA,EAAA,QAAA,CAAS,KAAK,KAAK,CAAA;AACnB,EAAA,OAAO,KAAA;AACT;AAOO,SAAS,cAAA,GAAuB;AACrC,EAAA,KAAA,MAAW,CAAA,IAAK,QAAA,EAAU,CAAA,CAAE,KAAA,EAAM;AACpC;AAMO,SAAS,kBAAA,GAA2B;AACzC,EAAA,QAAA,CAAS,MAAA,GAAS,CAAA;AACpB","file":"chunk-GQGJFCWL.js","sourcesContent":["/**\n * `defineStore({ initial, actions })` — composable testable stores layered on\n * top of `reactive.ts`'s signals.\n *\n * Three rules:\n * 1. `state` is read-only. Consumers read via `state.value` or subscribe via\n * `effect()`. They cannot write directly.\n * 2. `actions` is the only mutation surface. All writes go through named\n * action functions. This is what makes stores testable — assert against\n * actions, not against arbitrary writes.\n * 3. `reset()` resets to `initial()`. Always defined; tests use it for\n * setup, lifecycle hooks (route change, sign-out, etc.) use it for\n * tear-down.\n *\n * A module-level registry tracks every store created via `defineStore()`;\n * `resetAllStores()` walks the registry and calls each `reset()`. Useful for\n * tests + project-switch / logout / route-reset scenarios where every piece\n * of client state should return to its initial shape.\n */\n\nimport type { ReadonlySignal, Signal } from './reactive.js';\nimport { signal } from './reactive.js';\n\nexport interface Store<TState, TActions> {\n /** Read-only reactive view. Consumers read `state.value` or subscribe via `effect()`. */\n readonly state: ReadonlySignal<TState>;\n /** Named mutators — the only way to change state. */\n readonly actions: TActions;\n /** Reset state to `initial()`. Used by tests and lifecycle hooks. */\n reset(): void;\n}\n\ninterface DefineStoreSpec<TState, TActions> {\n initial: () => TState;\n actions: (set: (next: TState) => void, get: () => TState) => TActions;\n}\n\nconst REGISTRY: Array<{ reset: () => void }> = [];\n\nexport function defineStore<TState, TActions>(\n spec: DefineStoreSpec<TState, TActions>,\n): Store<TState, TActions> {\n const internal: Signal<TState> = signal(spec.initial());\n\n const set = (next: TState): void => {\n internal.value = next;\n };\n const get = (): TState => internal.value;\n\n const actions = spec.actions(set, get);\n\n const store: Store<TState, TActions> = {\n state: internal,\n actions,\n reset() {\n internal.value = spec.initial();\n },\n };\n\n REGISTRY.push(store);\n return store;\n}\n\n/**\n * Reset every store registered via `defineStore()` to its `initial()` value.\n * Used by tests and by application lifecycle hooks (project switch, logout,\n * route reset).\n */\nexport function resetAllStores(): void {\n for (const s of REGISTRY) s.reset();\n}\n\n/**\n * Test helper — clears the registry. Exposed via the `kerfjs/testing` subpath,\n * not the main `kerfjs` entry. Unit tests use it to isolate stores between cases.\n */\nexport function clearStoreRegistry(): void {\n REGISTRY.length = 0;\n}\n"]}
@@ -201,6 +201,9 @@ function raw(html) {
201
201
  function listSafeHtml(id, items) {
202
202
  return new SafeHtml({ kind: "list", id, items });
203
203
  }
204
+ function granularListSafeHtml(id, items, patches) {
205
+ return new SafeHtml({ kind: "list", id, items, patches });
206
+ }
204
207
  var VOID_TAGS = /* @__PURE__ */ new Set([
205
208
  "area",
206
209
  "base",
@@ -242,6 +245,8 @@ function describeValue(v) {
242
245
  }
243
246
  return typeof v;
244
247
  }
248
+ var URL_ATTRS = /* @__PURE__ */ new Set(["href", "src", "xlink:href", "formaction", "action"]);
249
+ var DANGEROUS_URL_RE = /^\s*(?:(?:java|vb)script:|data:text\/html[;,])/i;
245
250
  function renderAttr(key, value) {
246
251
  const name = ATTR_ALIASES[key] ?? key;
247
252
  if (value == null || value === false) return "";
@@ -252,6 +257,12 @@ function renderAttr(key, value) {
252
257
  } else if (typeof value === "number") {
253
258
  strValue = String(value);
254
259
  } else if (typeof value === "string") {
260
+ if (URL_ATTRS.has(name) && DANGEROUS_URL_RE.test(value)) {
261
+ console.warn(
262
+ `JSX: dropped dangerous URL value for ${name}=${JSON.stringify(value.slice(0, 80))}. kerf blocks javascript:, vbscript:, and data:text/html URLs in href/src/formaction/action/xlink:href by default. Wrap in raw() if this is intentional (e.g. bookmarklets), or sanitise upstream.`
263
+ );
264
+ return "";
265
+ }
255
266
  strValue = escapeAttr(value);
256
267
  } else {
257
268
  throw new Error(
@@ -272,6 +283,6 @@ function Fragment({ children }) {
272
283
  return new SafeHtml(children != null ? toSegment(children) : { kind: "static", html: "" });
273
284
  }
274
285
 
275
- export { Fragment, SafeHtml, collectLists, flatten, flattenWithoutListItems, isSafeHtml, jsx, listSafeHtml, raw };
276
- //# sourceMappingURL=chunk-WK5D3OO7.js.map
277
- //# sourceMappingURL=chunk-WK5D3OO7.js.map
286
+ export { Fragment, SafeHtml, collectLists, flatten, flattenWithoutListItems, granularListSafeHtml, isSafeHtml, jsx, listSafeHtml, raw };
287
+ //# sourceMappingURL=chunk-WYJTMERY.js.map
288
+ //# sourceMappingURL=chunk-WYJTMERY.js.map