kerfjs 1.0.2 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +36 -0
- package/LICENSE +29 -1
- package/README.md +53 -17
- package/ai/cursorrules +21 -6
- package/ai/manifest.json +5 -5
- package/ai/skill.md +23 -6
- package/dist/array-signal.js +1 -1
- package/dist/{chunk-QNYOMGI4.js → chunk-GYRZQCSY.js} +78 -17
- package/dist/chunk-GYRZQCSY.js.map +1 -0
- package/dist/chunk-KFUDM3VP.js +131 -0
- package/dist/chunk-KFUDM3VP.js.map +1 -0
- package/dist/{chunk-4E26PO2C.js → chunk-NU7YHYEV.js} +13 -5
- package/dist/chunk-NU7YHYEV.js.map +1 -0
- package/dist/html.d.ts +50 -0
- package/dist/html.js +156 -0
- package/dist/html.js.map +1 -0
- package/dist/index.d.ts +70 -34
- package/dist/index.js +239 -126
- package/dist/index.js.map +1 -1
- package/dist/jsx-runtime.d.ts +96 -55
- package/dist/jsx-runtime.js +2 -2
- package/dist/testing.js +2 -2
- package/llms.txt +6 -5
- package/package.json +7 -2
- package/dist/chunk-4E26PO2C.js.map +0 -1
- package/dist/chunk-7SKIIA5P.js +0 -70
- package/dist/chunk-7SKIIA5P.js.map +0 -1
- package/dist/chunk-QNYOMGI4.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,42 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [2.0.1] - 2026-07-23
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
- Fixed a `morph()` bug where static text siblings of a fine-grained bound text hole were dropped after a structural re-render (e.g. `<div>{label} / static</div>` collapsing to just the label).
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
- New animated coding-session demo on the getting-started page: watch the canonical counter get typed out, served, clicked in a browser, and live-edited into a todo list.
|
|
17
|
+
- Added an animated architecture diagram and a link to the docs site from the README.
|
|
18
|
+
|
|
19
|
+
## [2.0.0] - 2026-07-23
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
- `delegateCapture()` now uses `closest()`-style walk-up matching like `delegate()`, passing the matched ancestor to the handler; pass `{ match: 'direct' }` (new `DelegateOptions`) to restore exact-element matching.
|
|
24
|
+
- The dangerous-URL screen (`javascript:`, `vbscript:`, script-executing `data:` URLs) now throws an error in development instead of only warning. Production behavior is unchanged: warn and drop the attribute.
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
- New `kerfjs/html` tagged template: author kerf UIs with no build step (CDN / importmap, no JSX transform) with runtime semantics identical to JSX — signal holes become fine-grained bindings, attributes and text are escaped and URL-screened the same way.
|
|
28
|
+
- New no-build example app, **live-poll**, served exactly as authored — an importmap plus one `html`-templated module, with view-source showing the app.
|
|
29
|
+
- Added `<filter>` to the typed JSX intrinsic elements, so SVG filters compile in JSX-authored code.
|
|
30
|
+
- Fully-bound mounts are now a documented, test-pinned guarantee: a render that reads no signal `.value` runs exactly once, forever — every update is a direct per-node write.
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
- Controlled form state now survives user interaction: `checked`, `value`, and `selected` DOM properties are synced when the reconciler mutates those attributes, so a clicked checkbox or typed-into input no longer ignores later updates.
|
|
34
|
+
- Fixed stale fine-grained bindings after `arraySignal.update()`: in-place row updates now re-wire bindings whose signal instance changed, instead of leaving effects reading the old row object forever.
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
- New runtime dev-mode override: set `globalThis.KERF_DEV = false` (or `true`) to control dev mode without a bundler — CDN/importmap apps are no longer stuck in dev mode in production.
|
|
38
|
+
- Two new opt-in dev warnings: `KERF_DEV_WARN_STALE_BINDING` flags bindings that silently go stale on the byte-equal fast path, and `KERF_DEV_WARN_VALUE_ONLY_RERENDER` flags re-renders whose only changes could have been fine-grained bindings.
|
|
39
|
+
- `defineStore`'s dev-mode `get()` snapshot now returns a deep read-only proxy instead of freezing the live state: nested mutations like `get().nested.x = 1` are caught too, and they throw a descriptive `TypeError` rather than failing silently.
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
- Docs repositioned around the "values bind, structure re-renders" idiom as the primary way to render dynamic values, across the overview, reactivity guide, and AI assistant configs.
|
|
43
|
+
- New guide covering the no-build authoring path and example app, plus a documentation-wide accuracy pass (delegate capture semantics, `effect()` cleanup returns, URL-screen behavior, and more).
|
|
44
|
+
|
|
9
45
|
## [1.0.2] - 2026-07-22
|
|
10
46
|
|
|
11
47
|
|
package/LICENSE
CHANGED
|
@@ -25,7 +25,7 @@ SOFTWARE.
|
|
|
25
25
|
|
|
26
26
|
## Acknowledgements
|
|
27
27
|
|
|
28
|
-
`src/
|
|
28
|
+
`src/morph.ts` re-implements the DOM-reconciliation algorithm of
|
|
29
29
|
[morphdom](https://github.com/patrick-steele-idem/morphdom) by Patrick
|
|
30
30
|
Steele-Idem, which is also distributed under the MIT License:
|
|
31
31
|
|
|
@@ -51,3 +51,31 @@ Steele-Idem, which is also distributed under the MIT License:
|
|
|
51
51
|
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
|
52
52
|
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
|
53
53
|
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
`src/attrSelector.ts`'s `cssEscapeIdent` adapts the CSS identifier escaping of
|
|
58
|
+
the [CSS.escape polyfill](https://github.com/mathiasbynens/CSS.escape) by
|
|
59
|
+
Mathias Bynens, which is also distributed under the MIT License:
|
|
60
|
+
|
|
61
|
+
Copyright Mathias Bynens <https://mathiasbynens.be/>
|
|
62
|
+
|
|
63
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
64
|
+
a copy of this software and associated documentation files (the
|
|
65
|
+
"Software"), to deal in the Software without restriction, including
|
|
66
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
67
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
68
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
69
|
+
the following conditions:
|
|
70
|
+
|
|
71
|
+
The above copyright notice and this permission notice shall be
|
|
72
|
+
included in all copies or substantial portions of the Software.
|
|
73
|
+
|
|
74
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
75
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
76
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
77
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
|
78
|
+
BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
|
79
|
+
ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
80
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
81
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -6,6 +6,10 @@
|
|
|
6
6
|
|
|
7
7
|
<p align="center"><em>The smallest cut.</em></p>
|
|
8
8
|
|
|
9
|
+
<p align="center">
|
|
10
|
+
<a href="https://brianwestphal.github.io/kerf/"><strong>brianwestphal.github.io/kerf</strong></a> — docs · examples · live demo
|
|
11
|
+
</p>
|
|
12
|
+
|
|
9
13
|
---
|
|
10
14
|
|
|
11
15
|
> Introducing Kerf.
|
|
@@ -29,23 +33,27 @@ mount(document.getElementById('app')!, () => (
|
|
|
29
33
|
|
|
30
34
|
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.
|
|
31
35
|
|
|
36
|
+
Here's the whole development loop — write a component, run the dev server, click around, edit, watch the browser pick it up:
|
|
37
|
+
|
|
38
|
+
[](https://brianwestphal.github.io/kerf/getting-started/)
|
|
39
|
+
|
|
32
40
|
## Why Kerf
|
|
33
41
|
|
|
34
42
|
1. **Small bundle.** ~11 KB minified + gzipped including `@preact/signals-core` (~12 KB with `arraySignal`). One runtime dependency. No virtual DOM, no scheduler, no concurrent-mode machinery. On the official [krausest js-framework-benchmark](https://krausest.github.io/js-framework-benchmark/current.html) — where kerf is a listed entry, measured on the same reference machine as every competitor ([local mirror](./bench/results.md)) — kerf is in the same cluster as Vue, vanjs, and Lit on most operations; Solid's compiler leads the update-path benchmarks (notably `partial update`), which kerf doesn't try to match by design — no compiler.
|
|
35
43
|
|
|
36
44
|
2. **No virtual DOM, no compiler.** JSX → HTML strings → native diff. DevTools shows the real DOM because it *is* the DOM.
|
|
37
45
|
|
|
38
|
-
3. **
|
|
46
|
+
3. **Values bind, structure re-renders.** Hand a signal *itself* into a JSX hole — `class={selectedId}` or `{status}` — and kerf binds that one node directly: when the signal changes, only that attribute or text node updates, with no render re-run and no list reconcile. A selection flip on a 10,000-row table touches exactly one class. Taken to its logical end: a mount whose render reads no `.value` at all runs **exactly once, forever** — every subsequent update flows through the per-hole bindings. Read `.value` in the render only when the *structure* depends on it (conditionals, list shape).
|
|
39
47
|
|
|
40
48
|
4. **Focus, selection, listeners survive re-renders — even mid-list.** The reconciler morphs instead of rebuilding, so caret position, selection range, IME composition, and delegated listeners survive every re-render. Keyed lists get the same treatment: same-identity rows are updated *in place* rather than recreated, so a row reorder or a single-cell edit no longer blows away focus, scroll, or an in-flight animation the way node replacement does.
|
|
41
49
|
|
|
42
|
-
5. **Safe by default.** Text and attribute values are HTML-escaped automatically, URL attributes are scheme-screened (`javascript:` / script-carrying `data:` dropped), inline `on*` handlers are rejected outright, and the same screening covers the fine-grained bound path — so untrusted data stays inert even when kerf is dropped into someone else's page. `raw()` is the explicit, auditable opt-out.
|
|
50
|
+
5. **Safe by default.** Text and attribute values are HTML-escaped automatically, URL attributes are scheme-screened (`javascript:` / script-carrying `data:` dropped), inline `on*` handlers are rejected outright, and the same screening covers the fine-grained bound path — so untrusted data stays inert even when kerf is dropped into someone else's page. The URL screen fails loudly at your desk (throws in development) and degrades safely in the field (warns and drops in production). `raw()` is the explicit, auditable opt-out.
|
|
43
51
|
|
|
44
|
-
6. **Small public API.** ~17 exports from the main barrel (plus `arraySignal` on
|
|
52
|
+
6. **Small public API.** ~17 exports from the main barrel (plus `arraySignal` and the `html` tagged template on their own subpaths). No hooks, no lifecycle, no per-instance state. Components are plain functions that return JSX.
|
|
45
53
|
|
|
46
|
-
7. **Plain TS, plain JSX, plain ESM.** Drops into anything using esbuild / Vite / tsup. No plugin chain.
|
|
54
|
+
7. **Plain TS, plain JSX, plain ESM.** Drops into anything using esbuild / Vite / tsup. No plugin chain. And with the `html` tagged template (`import { html } from 'kerfjs/html'` — identical runtime semantics to JSX), a CDN / importmap project needs no build step at all.
|
|
47
55
|
|
|
48
|
-
8. **Grown-up tooling around a tiny core.** An [ESLint plugin](https://brianwestphal.github.io/kerf/docs/eslint-plugin/) that enforces the hard rules at edit time, a `create-kerf-component` scaffold for publishable component packages, drop-in AI-assistant configs, and side-by-side migration guides for a dozen-plus frameworks — none of which grows the core runtime past ~11 KB.
|
|
56
|
+
8. **Grown-up tooling around a tiny core.** An [ESLint plugin](https://brianwestphal.github.io/kerf/docs/eslint-plugin/) that enforces the hard rules at edit time, an opt-in family of `KERF_DEV_WARN_*` runtime warnings that catch the classic mistakes in development (with zero production cost), a `create-kerf-component` scaffold for publishable component packages, drop-in AI-assistant configs, and side-by-side migration guides for a dozen-plus frameworks — none of which grows the core runtime past ~11 KB.
|
|
49
57
|
|
|
50
58
|
## When to use Kerf
|
|
51
59
|
|
|
@@ -91,16 +99,12 @@ mount(root, () => (
|
|
|
91
99
|
<div>
|
|
92
100
|
<h1>Cart ({cart.state.value.items.length})</h1>
|
|
93
101
|
<ul>
|
|
94
|
-
{each(
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
<
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
</li>
|
|
101
|
-
),
|
|
102
|
-
(item) => item.id,
|
|
103
|
-
)}
|
|
102
|
+
{each(cart.state.value.items, (item) => (
|
|
103
|
+
<li data-key={item.id}>
|
|
104
|
+
{item.name}
|
|
105
|
+
<button data-action="remove" data-id={item.id}>×</button>
|
|
106
|
+
</li>
|
|
107
|
+
))}
|
|
104
108
|
</ul>
|
|
105
109
|
<p>Doubled count: {doubled.value}</p>
|
|
106
110
|
</div>
|
|
@@ -112,6 +116,17 @@ delegate(root, 'click', '[data-action="remove"]', (_e, btn) => {
|
|
|
112
116
|
});
|
|
113
117
|
```
|
|
114
118
|
|
|
119
|
+
The stringly-typed `'[data-action="remove"]'` pair above can be made rename-safe with the `attr()` helper — declare the attribute once and use it on both sides:
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
import { attr } from 'kerfjs';
|
|
123
|
+
|
|
124
|
+
const REMOVE = attr('data-action', 'remove'); // pre-escaped name/value/selector
|
|
125
|
+
|
|
126
|
+
<button {...REMOVE.attrs} data-id={item.id}>×</button>; // in JSX
|
|
127
|
+
delegate(root, 'click', REMOVE.selector, (_e, btn) => { /* … */ }); // in delegation
|
|
128
|
+
```
|
|
129
|
+
|
|
115
130
|
### Fine-grained updates: bind a signal into a hole
|
|
116
131
|
|
|
117
132
|
Inside a `mount()`, hand a signal *itself* (not its `.value`) into an attribute or text position and kerf wires that hole straight to the signal — the render function never re-runs and the list reconciler never walks:
|
|
@@ -128,7 +143,9 @@ mount(root, () => (
|
|
|
128
143
|
status.value = 'saving'; // updates the class + the text node directly — no re-render
|
|
129
144
|
```
|
|
130
145
|
|
|
131
|
-
The headline use is external state driving one spot: a `selectedId` flipping a single row's class inside a 10,000-row `each()` list touches exactly that one node, no reconcile. Works in static content and inside `each()` rows (a row's binding is torn down with the row); outside a `mount()` (SSR / `SafeHtml.toString()`) a bound signal just snapshots its current value.
|
|
146
|
+
The headline use is external state driving one spot: a `selectedId` flipping a single row's class inside a 10,000-row `each()` list touches exactly that one node, no reconcile. Works in static content and inside `each()` rows (a row's binding is torn down with the row); outside a `mount()` (SSR / `SafeHtml.toString()`) a bound signal just snapshots its current value.
|
|
147
|
+
|
|
148
|
+
This is kerf's guiding idiom — *values bind, structure re-renders*: pass the signal itself wherever a hole is just a value, and read `.value` in the render function only where the JSX structure depends on it. A render that reads no `.value` runs exactly once; from then on every update is a direct write to the node it concerns. See [`docs/2-reactivity.md`](./docs/2-reactivity.md) §2.9.
|
|
132
149
|
|
|
133
150
|
### Long keyed lists: `arraySignal`
|
|
134
151
|
|
|
@@ -164,6 +181,25 @@ morph(liveCard, raw(htmlFromServer)); // SafeHtml
|
|
|
164
181
|
|
|
165
182
|
Same algorithm `mount()` uses internally — `data-morph-skip`, `data-morph-skip-children`, `data-morph-preserve`, focused-input value + selection preservation, the `<details>` / `<dialog>` user-agent-owned `open` rule all carry over. Use it for SSR-fragment hydration, page-refresh diffs, third-party widget remounts. See [`docs/4-render.md`](./docs/4-render.md) §4.4.3.
|
|
166
183
|
|
|
184
|
+
### No build step at all: the `html` tagged template
|
|
185
|
+
|
|
186
|
+
"No compiler" isn't just a JSX story. The `html` tagged template from `kerfjs/html` has **identical runtime semantics to JSX** — escaping, boolean/nullish attribute rules, URL screening, `on*` rejection, fine-grained signal bindings, `each()` composition — with no transform, so a plain `<script type="module">` on a CDN / importmap page is a complete kerf app:
|
|
187
|
+
|
|
188
|
+
```html
|
|
189
|
+
<script type="module">
|
|
190
|
+
import { signal, mount, each } from 'https://esm.sh/kerfjs';
|
|
191
|
+
import { html } from 'https://esm.sh/kerfjs/html';
|
|
192
|
+
|
|
193
|
+
const items = signal([{ id: 1, label: 'no build step' }]);
|
|
194
|
+
|
|
195
|
+
mount(document.getElementById('app'), () => html`
|
|
196
|
+
<ul>${each(items.value, (i) => html`<li id="${i.id}">${i.label}</li>`)}</ul>
|
|
197
|
+
`);
|
|
198
|
+
</script>
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Attribute names are written verbatim (`class`, not `className`), and holes are only legal in text positions or as a complete attribute value — anything ambiguous throws with an actionable message. Static template parts parse once per call site. See [`docs/6-jsx-runtime.md`](./docs/6-jsx-runtime.md) §6.11 — or the [live-poll example](https://brianwestphal.github.io/kerf/examples/complete/live-poll/), a complete app served exactly as authored: no bundler ever touches it.
|
|
202
|
+
|
|
167
203
|
## Install
|
|
168
204
|
|
|
169
205
|
```bash
|
|
@@ -225,7 +261,7 @@ A *kerf* is the narrow strip of material a saw blade removes when cutting — th
|
|
|
225
261
|
|
|
226
262
|
## Status
|
|
227
263
|
|
|
228
|
-
|
|
264
|
+
Stable — the public API follows semver. See [CHANGELOG.md](./CHANGELOG.md) for the current version and what's shipped.
|
|
229
265
|
|
|
230
266
|
## Sponsor
|
|
231
267
|
|
package/ai/cursorrules
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- kerf-skill-version: 1.
|
|
1
|
+
<!-- kerf-skill-version: 1.8.0 -->
|
|
2
2
|
# kerf.cursorrules — rules for building apps with kerf
|
|
3
3
|
#
|
|
4
4
|
# Drop this file into your project as `.cursorrules` (Cursor will pick it
|
|
@@ -42,12 +42,13 @@ import { arraySignal } from 'kerfjs/array-signal';
|
|
|
42
42
|
| `morph(liveRoot, template)` | one-shot reconcile against an already-populated element (SSR hydration, page-refresh diffs). Template can be `Element`, `SafeHtml`, or HTML string |
|
|
43
43
|
| `each(items, render, cacheKey?)` | keyed list iteration; per-row memoization on object identity (+ optional cacheKey — a passive comparator for external state). Distinct from `data-key` on the rendered element |
|
|
44
44
|
| `delegate(root, type, sel, h)` | one listener at the root, walks `closest(selector)` from target |
|
|
45
|
-
| `delegateCapture(root, type, sel, h)` | capture-phase escape hatch
|
|
45
|
+
| `delegateCapture(root, type, sel, h, opts?)` | capture-phase escape hatch; `closest()` walk-up by default (same as `delegate`); pass `{ match: 'direct' }` for strict `target.matches()` |
|
|
46
46
|
| `attr(name, value)` | pre-computed `AttrSpec<N,V>` — `.selector` for `delegate()`, `.attrs` to spread into JSX (rename-safe) |
|
|
47
47
|
| `attr(name)` | dynamic factory — `attr<N,V=string>(name)` returns `(value: V) => { readonly [name]: V }`; both generics off → N inferred, V defaults to string; specify both to constrain values |
|
|
48
48
|
| `toElement(jsx)` | parse JSX into a DOM node (SVG-aware). Single-root → `Element`; multi-root (`<><svg/> label</>`, two icons side by side) → `DocumentFragment` that `appendChild`/`replaceChildren`/`append` inlines into the parent. |
|
|
49
49
|
| `raw(html)` | inject pre-escaped HTML |
|
|
50
50
|
| `arraySignal(initial?)` | granular keyed-list signal at `kerfjs/array-signal` subpath; `each()` reconciles in O(patches) |
|
|
51
|
+
| `` html`…` `` | tagged template at `kerfjs/html` subpath — JSX-identical runtime semantics with NO build step (CDN/importmap projects). Real HTML attribute names (`class`, not `className`); holes only in text positions or as a COMPLETE attribute value (`attr=${v}` / `attr="${v}"`) |
|
|
51
52
|
|
|
52
53
|
## Hard rules — get these right on the first try
|
|
53
54
|
|
|
@@ -58,7 +59,7 @@ import { arraySignal } from 'kerfjs/array-signal';
|
|
|
58
59
|
5. **Capture the `delegate()` / `delegateCapture()` disposer** whenever the registration's scope is shorter than the page. Both helpers return `() => void`; the listener closure pins `rootEl`, `handler`, and everything the handler closes over (stores, signals, app state). Discarding the disposer on a transient root (modal, route view, mount swap, dynamic widget) leaks the listener AND the app graph it captures; re-mount cycles stack listeners linearly. `mount()`'s own disposer does NOT remove delegates for you. Safe to discard only when the registration is truly page-lifetime (root is `document.body` or equivalent, attached once at startup, never torn down).
|
|
59
60
|
6. **One `mount()` per root.** Don't nest. Compose with plain functions that return JSX.
|
|
60
61
|
7. **No `<MyComponent />` semantics with hooks.** Components are plain functions returning JSX. State lives in module-scope signals or stores, never in component closures.
|
|
61
|
-
8. **
|
|
62
|
+
8. **Values bind, structure re-renders.** For a value hole, pass the signal/computed ITSELF (`<span>{count}</span>`, `class={sig}`) — kerf updates that one node directly, no render re-run. Read `.value` only when the JSX *structure* depends on the signal — and then the read must happen INSIDE the render function to be tracked: `const x = count.value; mount(el, () => <span>{x}</span>)` will NOT re-render. Bind a STABLE signal/computed instance per hole (a `computed` that switches internally), never `class={cond ? sigA : sigB}` — switching instances can go silently stale (`KERF_DEV_WARN_STALE_BINDING=1` detects it). Endpoint: a render reading NO `.value` runs exactly once — a fully bound mount never re-renders; `KERF_DEV_WARN_VALUE_ONLY_RERENDER=1` flags re-renders that could have been bindings.
|
|
62
63
|
9. **Store actions receive `(set, get)`, not `(state)`.** `set(next)` replaces state; mutating `get()` does nothing.
|
|
63
64
|
10. **Use `data-action` attributes, not inline `onClick`.** Inline handlers are NOT supported by the JSX → string runtime; delegate from the root instead.
|
|
64
65
|
11. **`arraySignal` is opt-in for long keyed lists** where most updates are pointwise (single-row edits, append-to-end). For short lists / filter+sort pipelines, plain `signal` + `each(items.value, ...)` is simpler and equally fast.
|
|
@@ -73,7 +74,7 @@ When deciding which primitive to reach for, work down the axes:
|
|
|
73
74
|
**Events.**
|
|
74
75
|
- Originates inside the mount tree → `delegate(rootEl, type, sel, handler)`. Originates outside (window-level keyboard, online/offline, beforeunload) → native `window.addEventListener` at module top-level.
|
|
75
76
|
- Gesture that needs to follow an element after press (drag, draw, resize) → at the start event, `el.setPointerCapture(e.pointerId)`. Subsequent `pointermove` / `pointerup` redirect to the captured element and `delegate(rootEl, 'pointermove', '[data-card]', …)` still picks them up. Don't reach for `window.addEventListener` for in-mount-tree gestures.
|
|
76
|
-
- Well-known non-bubbler (`focus`, `blur`, `scroll`, `load`, `error`, `mouseenter`, `mouseleave`) → still `delegate()`; it auto-promotes to capture. Custom non-bubblers or strict element-match
|
|
77
|
+
- Well-known non-bubbler (`focus`, `blur`, `scroll`, `load`, `error`, `mouseenter`, `mouseleave`) → still `delegate()`; it auto-promotes to capture. Custom non-bubblers or capture-phase interception → `delegateCapture()` (also `closest()`-matched by default). Need strict element-match? Add `{ match: 'direct' }` on either helper.
|
|
77
78
|
|
|
78
79
|
**Lists.**
|
|
79
80
|
- Items change across renders (todos, chat messages, table rows) → `each(items, render)`.
|
|
@@ -89,18 +90,22 @@ When deciding which primitive to reach for, work down the axes:
|
|
|
89
90
|
**Raw HTML.**
|
|
90
91
|
- User-controlled HTML → sanitize first (DOMPurify) then `raw(sanitized)`.
|
|
91
92
|
- Author-controlled trusted HTML → `raw(html)` directly.
|
|
93
|
+
- Dangerous URLs (`javascript:`/`vbscript:`/script-executing `data:`) on `href`/`src`/`xlink:href`/`formaction`/`action`/`data` are dropped — kerf THROWS in dev, WARNS + drops in prod. Sanitize user URLs upstream; wrap an intentional trusted one in `raw(url)` to bypass the screen in both modes.
|
|
92
94
|
|
|
93
95
|
## Canonical patterns
|
|
94
96
|
|
|
95
97
|
```tsx
|
|
96
|
-
// Signal + mount
|
|
98
|
+
// Signal + mount. THE core idiom — values bind, structure re-renders:
|
|
99
|
+
// pass the signal ITSELF into a value hole ({count}, class={sig}) so kerf
|
|
100
|
+
// updates that one node directly with no render re-run; read `.value` only
|
|
101
|
+
// when the JSX STRUCTURE depends on the signal (conditionals, list shape).
|
|
97
102
|
const count = signal(0);
|
|
98
103
|
const ACTIONS = { inc: attr('data-action', 'inc') } as const satisfies Record<string, AttrSpec<'data-action'>>;
|
|
99
104
|
|
|
100
105
|
mount(document.getElementById('app')!, () => (
|
|
101
106
|
<div>
|
|
102
107
|
<button {...ACTIONS.inc.attrs}>+</button>
|
|
103
|
-
<span>{count
|
|
108
|
+
<span>{count}</span>
|
|
104
109
|
</div>
|
|
105
110
|
));
|
|
106
111
|
delegate(rootEl, 'click', ACTIONS.inc.selector, () => { count.value += 1; });
|
|
@@ -124,6 +129,15 @@ const cart = defineStore({
|
|
|
124
129
|
// One-shot reconcile against existing DOM (no signals)
|
|
125
130
|
morph(liveCard, '<article class="card">…</article>');
|
|
126
131
|
|
|
132
|
+
// No build step (CDN / importmap): the html tagged template instead of JSX.
|
|
133
|
+
// Same runtime semantics as JSX; write real HTML attribute names; a hole must
|
|
134
|
+
// be a text position or a COMPLETE attribute value (partial values throw).
|
|
135
|
+
import { html } from 'kerfjs/html';
|
|
136
|
+
mount(rootEl, () => html`
|
|
137
|
+
<div class="${cls}">Count: ${count}</div>
|
|
138
|
+
<ul>${each(rows.value, (row) => html`<li data-key="${row.id}">${row.label}</li>`)}</ul>
|
|
139
|
+
`);
|
|
140
|
+
|
|
127
141
|
// Fine-grained binding (opt-in): pass the signal/computed ITSELF into a hole
|
|
128
142
|
// so a change updates ONLY that node (no render re-run, no reconcile). For a
|
|
129
143
|
// hot spot driven by an external signal (selection class) — not everywhere.
|
|
@@ -150,6 +164,7 @@ mount(listEl, () => (
|
|
|
150
164
|
- Drag/drop / state change has no visible effect, only stuff *outside* `each()` updates → you used `each(STATIC_ARRAY, …)` whose row render reads signals. Replace the outer with `STATIC_ARRAY.map(...)`; keep inner `each()` for the dynamic sub-list. See Hard Rule 14.
|
|
151
165
|
- Row-enter CSS animation no longer replays when only a row's *content* changed (kerf ≥ 0.15.0) → 0.15.0+ morphs a same-identity, same-position row *in place* instead of recreating its node, so a mount-keyed `@keyframes` never re-triggers on a content-only update (≤ 0.14.x recreated the node, so it fired; the intentional flip side is that focus, scroll, IME, and in-progress transitions now survive). Key the animation on a state-class toggle, not element creation; to force a remount, churn the row's identity (new object ref / `data-key`).
|
|
152
166
|
- Want a hot spot to update without re-running the whole render → fine-grained binding: pass the signal/`computed` ITSELF into the attr/text hole (`class={computed(() => …)}`), not `.value`. Use `computed()` not a bare `() => …` (memoization keeps a shared-signal flip to ~O(changed nodes)). Opt-in per hole. Limit: a bound hole depending on the row's OWN mutated data goes stale on a granular in-place update — use plain interpolation there.
|
|
167
|
+
- `` html`` ``: partial attribute values are not supported → in `kerfjs/html` templates a hole must be the COMPLETE attribute value. Replace `class="a ${b}"` with a pre-built string (`` class="${`a ${b}`}" ``) or, for a bound attribute, `class="${computed(() => `a ${b.value}`)}"`.
|
|
153
168
|
|
|
154
169
|
## Server / SSR
|
|
155
170
|
|
package/ai/manifest.json
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
{
|
|
2
|
-
"kerfjsVersion": "
|
|
2
|
+
"kerfjsVersion": "2.0.1",
|
|
3
3
|
"files": [
|
|
4
4
|
{
|
|
5
5
|
"name": "skill",
|
|
6
6
|
"source": "kerf.claude-skill.md",
|
|
7
7
|
"bundle": "ai/skill.md",
|
|
8
8
|
"dest": ".claude/skills/kerf-app/SKILL.md",
|
|
9
|
-
"version": "1.
|
|
10
|
-
"sha256": "
|
|
9
|
+
"version": "1.8.0",
|
|
10
|
+
"sha256": "76b8a88bc7aaf9d50e6f3eee7b57a976b3279127f5d4970599aef1f839a06655"
|
|
11
11
|
},
|
|
12
12
|
{
|
|
13
13
|
"name": "cursorrules",
|
|
14
14
|
"source": "kerf.cursorrules",
|
|
15
15
|
"bundle": "ai/cursorrules",
|
|
16
16
|
"dest": ".cursorrules",
|
|
17
|
-
"version": "1.
|
|
18
|
-
"sha256": "
|
|
17
|
+
"version": "1.8.0",
|
|
18
|
+
"sha256": "f91402df009941b4198ea659498f458d2433843d47a3b524b0ca5e2d3d2fc949"
|
|
19
19
|
}
|
|
20
20
|
]
|
|
21
21
|
}
|
package/ai/skill.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: kerf-app
|
|
3
3
|
description: Build UIs in the kerf reactive framework (https://github.com/brianwestphal/kerf). Use this skill whenever the user is writing or modifying code that imports `kerfjs`, asks to add a feature to a kerf app, or asks "how do I do X in kerf?". Use it proactively the moment you spot a kerf import in the file you're editing.
|
|
4
|
-
kerf-skill-version: 1.
|
|
4
|
+
kerf-skill-version: 1.8.0
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Building apps with kerf
|
|
@@ -47,12 +47,13 @@ import { arraySignal } from 'kerfjs/array-signal';
|
|
|
47
47
|
| `morph(liveRoot, template)` | one-shot reconcile against a populated element (SSR hydration, page-refresh diffs). Template = `Element`, `SafeHtml`, or HTML string |
|
|
48
48
|
| `each(items, render, cacheKey?)` | keyed list iteration; per-row memoization on identity (+ optional cacheKey — a passive comparator for external state). Distinct from `data-key` on the rendered element |
|
|
49
49
|
| `delegate(root, type, sel, h)` | one listener at the root; `closest(selector)` walk from target |
|
|
50
|
-
| `delegateCapture(root, type, sel, h)` | capture-phase escape hatch; `target.matches()`
|
|
50
|
+
| `delegateCapture(root, type, sel, h, opts?)` | capture-phase escape hatch; `closest()` walk-up by default (same as `delegate`); pass `{ match: 'direct' }` for strict `target.matches()` |
|
|
51
51
|
| `attr(name, value)` | pre-computed `AttrSpec<N,V>` — `.selector` for `delegate()`, `.attrs` to spread into JSX (rename-safe) |
|
|
52
52
|
| `attr(name)` | dynamic factory — `attr<N,V=string>(name)` returns `(value: V) => { readonly [name]: V }`; both generics off → N inferred, V defaults to string; specify both to constrain values |
|
|
53
53
|
| `toElement(jsx)` | parse JSX into a DOM node (SVG-aware). Single-root → `Element`; multi-root (`<><svg/> label</>`, two icons side by side) → `DocumentFragment` that `appendChild`/`replaceChildren`/`append` inlines into the parent. |
|
|
54
54
|
| `raw(html)` | inject pre-escaped HTML |
|
|
55
55
|
| `arraySignal(initial?)` | granular keyed-list signal (subpath `kerfjs/array-signal`); `each()` reconciles in O(patches) |
|
|
56
|
+
| `` html`…` `` | tagged template (subpath `kerfjs/html`) — JSX-identical runtime semantics with NO build step, for CDN/importmap projects. Real HTML attribute names (`class`, not `className`); holes only in text positions or as a COMPLETE attribute value (`attr=${v}` / `attr="${v}"`) |
|
|
56
57
|
|
|
57
58
|
## Hard rules — every AI assistant gets these wrong at least once
|
|
58
59
|
|
|
@@ -66,7 +67,7 @@ import { arraySignal } from 'kerfjs/array-signal';
|
|
|
66
67
|
5. **Capture the `delegate()` / `delegateCapture()` disposer** whenever the registration's scope is shorter than the page. Both helpers return `() => void`; the listener closure pins `rootEl`, `handler`, and everything the handler closes over (stores, signals, app state). Discarding the disposer on a transient root (modal, route view, mount swap, dynamic widget) leaks the listener AND the app graph it captures; re-mount cycles stack listeners linearly. `mount()`'s own disposer does NOT remove delegates for you. Safe to discard only when the registration is truly page-lifetime (root is `document.body` or equivalent, attached once at startup, never torn down).
|
|
67
68
|
6. **One `mount()` per root.** Don't nest `mount()` calls. Compose with plain functions returning JSX.
|
|
68
69
|
7. **Components are plain functions.** `<MyComponent props />` works — the JSX runtime calls `MyComponent(props)` and uses the returned JSX — but there's no hook system, no lifecycle, and no per-instance state. State lives in module-scope signals or stores, never in component closures.
|
|
69
|
-
8. **
|
|
70
|
+
8. **Values bind, structure re-renders.** For a value hole, pass the signal/computed ITSELF (`<span>{count}</span>`, `class={sig}`) — kerf updates that one node directly, no render re-run. Read `.value` only when the JSX *structure* depends on the signal — and then the read must happen INSIDE the render function to be tracked: `const x = count.value; mount(el, () => <span>{x}</span>)` does NOT re-render. One caveat on bound holes: bind a STABLE signal/computed instance per hole (`class={computed(() => …)}` that switches internally), never `class={cond ? sigA : sigB}` — switching instances can go silently stale (detectable via `KERF_DEV_WARN_STALE_BINDING=1`). The idiom's endpoint: a render that reads NO `.value` runs exactly once — a fully bound mount never re-renders. To find `.value` holes worth migrating, `KERF_DEV_WARN_VALUE_ONLY_RERENDER=1` flags re-renders whose only differences were text/attribute values.
|
|
70
71
|
9. **Store actions take `(set, get)`, not `(state)`.** `set(next)` replaces state; mutating `get()` does nothing.
|
|
71
72
|
10. **Use `data-action` attributes, not inline `onClick`.** Inline handlers are NOT supported by the JSX → string runtime; delegate from the root.
|
|
72
73
|
11. **`arraySignal` is opt-in for long keyed lists** where most updates are pointwise. For short lists / filter+sort pipelines, plain `signal` + `each(items.value, ...)` is simpler and equally fast.
|
|
@@ -81,7 +82,7 @@ When deciding which primitive to reach for, work down the axes:
|
|
|
81
82
|
**Events.**
|
|
82
83
|
- Originates inside the mount tree → `delegate(rootEl, type, sel, handler)`. Originates outside (window-level keyboard, online/offline, beforeunload) → native `window.addEventListener` at module top-level.
|
|
83
84
|
- Gesture that needs to follow an element after press (drag, draw, resize) → at the start event, `el.setPointerCapture(e.pointerId)`. Subsequent `pointermove` / `pointerup` redirect to the captured element and `delegate(rootEl, 'pointermove', '[data-card]', …)` still picks them up. Don't reach for `window.addEventListener` for in-mount-tree gestures.
|
|
84
|
-
- Well-known non-bubbler (`focus`, `blur`, `scroll`, `load`, `error`, `mouseenter`, `mouseleave`) → still `delegate()`; it auto-promotes to capture. Custom non-bubblers or strict element-match
|
|
85
|
+
- Well-known non-bubbler (`focus`, `blur`, `scroll`, `load`, `error`, `mouseenter`, `mouseleave`) → still `delegate()`; it auto-promotes to capture. Custom non-bubblers or capture-phase interception → `delegateCapture()` (also `closest()`-matched by default). Need strict element-match? Add `{ match: 'direct' }` on either helper.
|
|
85
86
|
|
|
86
87
|
**Lists.**
|
|
87
88
|
- Items change across renders (todos, chat messages, table rows) → `each(items, render)`.
|
|
@@ -98,17 +99,23 @@ When deciding which primitive to reach for, work down the axes:
|
|
|
98
99
|
- User-controlled HTML → sanitize first (DOMPurify) then `raw(sanitized)`.
|
|
99
100
|
- Author-controlled trusted HTML → `raw(html)` directly.
|
|
100
101
|
|
|
102
|
+
**Dangerous URLs.** `javascript:`/`vbscript:`/script-executing `data:` values on `href`/`src`/`xlink:href`/`formaction`/`action`/`data` are dropped — kerf **throws in dev**, **warns + drops in prod**. Sanitize user URLs upstream; wrap an intentional trusted one in `raw(url)` (bypasses the screen in both modes).
|
|
103
|
+
|
|
101
104
|
## Canonical patterns
|
|
102
105
|
|
|
103
106
|
```tsx
|
|
104
|
-
// Pattern 1: signal + mount + delegate
|
|
107
|
+
// Pattern 1: signal + mount + delegate.
|
|
108
|
+
// THE core idiom — values bind, structure re-renders: pass the signal ITSELF
|
|
109
|
+
// into a value hole ({count}, class={sig}) so kerf updates that one node
|
|
110
|
+
// directly with no render re-run; read `.value` only when the JSX STRUCTURE
|
|
111
|
+
// depends on the signal (conditionals, list shape).
|
|
105
112
|
const count = signal(0);
|
|
106
113
|
const ACTIONS = { inc: attr('data-action', 'inc') } as const satisfies Record<string, AttrSpec<'data-action'>>;
|
|
107
114
|
|
|
108
115
|
mount(document.getElementById('app')!, () => (
|
|
109
116
|
<div>
|
|
110
117
|
<button {...ACTIONS.inc.attrs}>+</button>
|
|
111
|
-
<span>{count
|
|
118
|
+
<span>{count}</span>
|
|
112
119
|
</div>
|
|
113
120
|
));
|
|
114
121
|
delegate(rootEl, 'click', ACTIONS.inc.selector, () => { count.value += 1; });
|
|
@@ -146,6 +153,15 @@ mount(listEl, () => (
|
|
|
146
153
|
</ul>
|
|
147
154
|
));
|
|
148
155
|
// selectedId.value = 3 → only the ~2 affected <li> class attrs update.
|
|
156
|
+
|
|
157
|
+
// Pattern 6: no build step (CDN / importmap) — the html tagged template
|
|
158
|
+
// instead of JSX. Same runtime semantics; real HTML attribute names; a hole
|
|
159
|
+
// must be a text position or a COMPLETE attribute value (partials throw).
|
|
160
|
+
import { html } from 'kerfjs/html';
|
|
161
|
+
mount(rootEl, () => html`
|
|
162
|
+
<div class="${cls}">Count: ${count}</div>
|
|
163
|
+
<ul>${each(rows.value, (row) => html`<li data-key="${row.id}">${row.label}</li>`)}</ul>
|
|
164
|
+
`);
|
|
149
165
|
```
|
|
150
166
|
|
|
151
167
|
## Diagnosing common errors
|
|
@@ -163,6 +179,7 @@ mount(listEl, () => (
|
|
|
163
179
|
| Drag/drop / state change has no visible effect; only elements *outside* `each()` update | Used `each(STATIC_ARRAY, …)` whose row render reads signals. Items never change identity → cache hits forever → row render never re-invoked → signal reads stop tracking | Replace outer with `STATIC_ARRAY.map(...)`; keep inner `each()` for the dynamic sub-list. See Hard Rule 14 |
|
|
164
180
|
| Row-enter CSS animation no longer replays when only a row's *content* changed (kerf ≥ 0.15.0) | 0.15.0+ morphs a same-identity, same-position row *in place* instead of recreating its node, so a mount-keyed `@keyframes` never re-triggers on a content-only update (≤ 0.14.x recreated the node, so it fired). Intentional flip side: focus, scroll, IME, and in-progress transitions now survive | Key the animation on a state-class toggle, not element creation. To force a remount, churn the row's identity (new object ref / `data-key`) so the reconciler replaces the node |
|
|
165
181
|
| Want a hot spot to update without re-running the whole render | Fine-grained binding: pass the signal/`computed` ITSELF into the attr/text hole (`class={computed(() => …)}`), not `.value`. Use `computed()` not a bare `() => …` (memoization keeps a shared-signal flip to ~O(changed nodes)). Opt-in per hole. Limit: a bound hole depending on the row's OWN mutated data goes stale on a granular in-place update — use plain interpolation there |
|
|
182
|
+
| `` html`` ``: partial attribute values are not supported | In `kerfjs/html` templates a hole must be the COMPLETE attribute value | Build the full string first (`` class="${`a ${b}`}" ``), or bind `class="${computed(() => `a ${b.value}`)}"` for a reactive one |
|
|
166
183
|
|
|
167
184
|
## Workflow guidance
|
|
168
185
|
|
package/dist/array-signal.js
CHANGED
|
@@ -1,4 +1,18 @@
|
|
|
1
|
-
import { effect, isSignal } from './chunk-
|
|
1
|
+
import { effect, isSignal, isDevMode } from './chunk-NU7YHYEV.js';
|
|
2
|
+
|
|
3
|
+
// src/utils/syncFormProp.ts
|
|
4
|
+
function syncFormProp(el, name, value, present) {
|
|
5
|
+
const tag = el.tagName;
|
|
6
|
+
if (name === "checked") {
|
|
7
|
+
if (tag === "INPUT") el.checked = present;
|
|
8
|
+
} else if (name === "value") {
|
|
9
|
+
if (tag === "INPUT" && el !== document.activeElement) {
|
|
10
|
+
el.value = present ? value : "";
|
|
11
|
+
}
|
|
12
|
+
} else if (name === "selected") {
|
|
13
|
+
if (tag === "OPTION") el.selected = present;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
2
16
|
|
|
3
17
|
// src/utils/urlScreen.ts
|
|
4
18
|
var URL_ATTRS = /* @__PURE__ */ new Set(["href", "src", "xlink:href", "formaction", "action", "data"]);
|
|
@@ -30,6 +44,11 @@ function isDangerousUrlValue(name, value) {
|
|
|
30
44
|
function dangerousUrlWarning(name, value) {
|
|
31
45
|
return `dropped dangerous URL value for ${name}=${JSON.stringify(value.slice(0, 80))}. kerf blocks javascript:, vbscript:, and script-executing data: URLs (text/html, image/svg+xml, xml) in href/src/data/formaction/action/xlink:href by default. Wrap in raw() if this is intentional (e.g. bookmarklets), or sanitize upstream.`;
|
|
32
46
|
}
|
|
47
|
+
function reportDangerousUrl(context2, name, value) {
|
|
48
|
+
const message = `${context2}: ${dangerousUrlWarning(name, value)}`;
|
|
49
|
+
if (isDevMode()) throw new Error(message);
|
|
50
|
+
console.warn(message);
|
|
51
|
+
}
|
|
33
52
|
|
|
34
53
|
// src/bindings.ts
|
|
35
54
|
var BIND_ATTR = "data-kfb";
|
|
@@ -39,7 +58,7 @@ var ROW_TEXT_PREFIX = "kfbr:";
|
|
|
39
58
|
var context = null;
|
|
40
59
|
var rowSink = null;
|
|
41
60
|
var rowCounter = 0;
|
|
42
|
-
var NO_DISPOSERS = [];
|
|
61
|
+
var NO_DISPOSERS = Object.freeze([]);
|
|
43
62
|
function newBindingContext() {
|
|
44
63
|
return { counter: 0, list: [] };
|
|
45
64
|
}
|
|
@@ -92,7 +111,7 @@ function wireBindings(rootEl, ctx, prevDisposers) {
|
|
|
92
111
|
for (const d of prevDisposers) d();
|
|
93
112
|
if (ctx.list.length === 0) return NO_DISPOSERS;
|
|
94
113
|
const disposers = [];
|
|
95
|
-
wireInto(rootEl,
|
|
114
|
+
wireInto(rootEl, ctx.list, disposers);
|
|
96
115
|
return disposers;
|
|
97
116
|
}
|
|
98
117
|
function wireRowBindings(rowNode, bindings) {
|
|
@@ -138,10 +157,29 @@ function disposeRowBindings(disposers) {
|
|
|
138
157
|
if (disposers === void 0) return;
|
|
139
158
|
for (const d of disposers) d();
|
|
140
159
|
}
|
|
141
|
-
function
|
|
142
|
-
const
|
|
160
|
+
function carryOrRewireRowBindings(node, oldBindings, oldDisposers, newBindings) {
|
|
161
|
+
const oldLen = oldBindings === void 0 ? 0 : oldBindings.length;
|
|
162
|
+
const newLen = newBindings === void 0 ? 0 : newBindings.length;
|
|
163
|
+
if (oldLen === newLen) {
|
|
164
|
+
let same = true;
|
|
165
|
+
for (let i = 0; i < newLen; i++) {
|
|
166
|
+
if (oldBindings[i].signal !== newBindings[i].signal) {
|
|
167
|
+
same = false;
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (same) return { bindings: oldBindings, bindingDisposers: oldDisposers };
|
|
172
|
+
}
|
|
173
|
+
disposeRowBindings(oldDisposers);
|
|
174
|
+
return {
|
|
175
|
+
bindings: newBindings,
|
|
176
|
+
bindingDisposers: newLen > 0 ? wireRowBindings(node, newBindings) : void 0
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
function wireInto(scope, bindings, disposers) {
|
|
180
|
+
const attrEls = indexAttrEls(scope, BIND_ATTR);
|
|
143
181
|
const textMarkers = /* @__PURE__ */ new Map();
|
|
144
|
-
collectComments(scope,
|
|
182
|
+
collectComments(scope, TEXT_MARKER_PREFIX, textMarkers);
|
|
145
183
|
for (const b of bindings) {
|
|
146
184
|
if (b.kind === "attr") {
|
|
147
185
|
const el = attrEls.get(b.id);
|
|
@@ -164,33 +202,47 @@ function indexAttrEls(scope, attrName) {
|
|
|
164
202
|
function attachAttrEffect(el, attr, signal) {
|
|
165
203
|
return effect(() => setBoundAttr(el, attr, signal.value));
|
|
166
204
|
}
|
|
205
|
+
var insertedTextNodes = /* @__PURE__ */ new WeakMap();
|
|
206
|
+
function boundTextNodeOf(marker) {
|
|
207
|
+
const t = insertedTextNodes.get(marker);
|
|
208
|
+
return t !== void 0 && marker.nextSibling === t ? t : null;
|
|
209
|
+
}
|
|
167
210
|
function attachTextEffect(marker, signal) {
|
|
168
|
-
|
|
169
|
-
|
|
211
|
+
let text = insertedTextNodes.get(marker);
|
|
212
|
+
if (text === void 0 || marker.nextSibling !== text) {
|
|
213
|
+
text = marker.ownerDocument.createTextNode("");
|
|
214
|
+
marker.parentNode.insertBefore(text, marker.nextSibling);
|
|
215
|
+
insertedTextNodes.set(marker, text);
|
|
216
|
+
}
|
|
217
|
+
const node = text;
|
|
170
218
|
return effect(() => {
|
|
171
|
-
|
|
219
|
+
node.data = coerceText(signal.value);
|
|
172
220
|
});
|
|
173
221
|
}
|
|
174
222
|
function setBoundAttr(el, name, value) {
|
|
175
223
|
if (value == null || value === false) {
|
|
176
224
|
el.removeAttribute(name);
|
|
225
|
+
syncFormProp(el, name, "", false);
|
|
177
226
|
return;
|
|
178
227
|
}
|
|
179
228
|
if (value === true) {
|
|
180
229
|
el.setAttribute(name, "");
|
|
230
|
+
syncFormProp(el, name, "", true);
|
|
181
231
|
return;
|
|
182
232
|
}
|
|
183
233
|
if (isSafeHtmlValue(value)) {
|
|
184
234
|
el.setAttribute(name, value.__html);
|
|
235
|
+
syncFormProp(el, name, value.__html, true);
|
|
185
236
|
return;
|
|
186
237
|
}
|
|
187
238
|
const str = String(value);
|
|
188
239
|
if (isDangerousUrlValue(name, str)) {
|
|
189
|
-
|
|
240
|
+
reportDangerousUrl("kerf binding", name, str);
|
|
190
241
|
el.removeAttribute(name);
|
|
191
242
|
return;
|
|
192
243
|
}
|
|
193
244
|
el.setAttribute(name, str);
|
|
245
|
+
syncFormProp(el, name, str, true);
|
|
194
246
|
}
|
|
195
247
|
var SAFE_HTML_BRAND = /* @__PURE__ */ Symbol.for("kerfjs.SafeHtml");
|
|
196
248
|
function isSafeHtmlValue(v) {
|
|
@@ -212,17 +264,18 @@ function collectComments(node, prefix, out) {
|
|
|
212
264
|
}
|
|
213
265
|
|
|
214
266
|
// src/segment.ts
|
|
267
|
+
var LIST_MARKER_PREFIX = "kf-list:";
|
|
215
268
|
function flatten(segment, withMarkers) {
|
|
216
269
|
if (segment.kind === "static") return segment.html;
|
|
217
270
|
if (segment.kind === "list") {
|
|
218
271
|
const items = segment.items.map((i) => i.html).join("");
|
|
219
|
-
return withMarkers ?
|
|
272
|
+
return withMarkers ? `<!--${LIST_MARKER_PREFIX}${segment.id}-->${items}` : items;
|
|
220
273
|
}
|
|
221
274
|
return segment.parts.map((p) => flatten(p, withMarkers)).join("");
|
|
222
275
|
}
|
|
223
276
|
function flattenWithoutListItems(segment) {
|
|
224
277
|
if (segment.kind === "static") return segment.html;
|
|
225
|
-
if (segment.kind === "list") return
|
|
278
|
+
if (segment.kind === "list") return `<!--${LIST_MARKER_PREFIX}${segment.id}-->`;
|
|
226
279
|
return segment.parts.map(flattenWithoutListItems).join("");
|
|
227
280
|
}
|
|
228
281
|
function collectLists(segment, out = /* @__PURE__ */ new Map()) {
|
|
@@ -488,7 +541,9 @@ See docs/5-event-delegation.md for the tier-1/tier-2/tier-3 model.`
|
|
|
488
541
|
}
|
|
489
542
|
}
|
|
490
543
|
function renderAttr(key, value) {
|
|
491
|
-
|
|
544
|
+
return renderAttrNamed(key, ATTR_ALIASES[key] ?? key, value);
|
|
545
|
+
}
|
|
546
|
+
function renderAttrNamed(key, name, value) {
|
|
492
547
|
if (value == null || value === false) return "";
|
|
493
548
|
assertEmittableAttrName(key, name, typeof value === "function");
|
|
494
549
|
if (value === true) return ` ${name}`;
|
|
@@ -499,7 +554,7 @@ function renderAttr(key, value) {
|
|
|
499
554
|
strValue = String(value);
|
|
500
555
|
} else if (typeof value === "string") {
|
|
501
556
|
if (isDangerousUrlValue(name, value)) {
|
|
502
|
-
|
|
557
|
+
reportDangerousUrl("JSX", name, value);
|
|
503
558
|
return "";
|
|
504
559
|
}
|
|
505
560
|
strValue = escapeAttr(value);
|
|
@@ -537,7 +592,13 @@ function jsx(tag, props) {
|
|
|
537
592
|
function Fragment({ children }) {
|
|
538
593
|
return new SafeHtml(children != null ? toSegment(children) : { kind: "static", html: "" });
|
|
539
594
|
}
|
|
595
|
+
function _toSegment(child) {
|
|
596
|
+
return toSegment(child);
|
|
597
|
+
}
|
|
598
|
+
function _renderAttrVerbatim(name, value) {
|
|
599
|
+
return renderAttrNamed(name, name, value);
|
|
600
|
+
}
|
|
540
601
|
|
|
541
|
-
export { Fragment, SafeHtml, _setBindingContext, captureRowBindings, collectLists, disposeRowBindings, flatten, flattenWithoutListItems, granularListSafeHtml, isSafeHtml, jsx, listSafeHtml, newBindingContext, raw, wireBindings, wireRowBindings };
|
|
542
|
-
//# sourceMappingURL=chunk-
|
|
543
|
-
//# sourceMappingURL=chunk-
|
|
602
|
+
export { Fragment, LIST_MARKER_PREFIX, SafeHtml, _renderAttrVerbatim, _setBindingContext, _toSegment, assertEmittableAttrName, bindAttr, bindMarkerAttr, boundTextNodeOf, captureRowBindings, carryOrRewireRowBindings, collectLists, disposeRowBindings, flatten, flattenWithoutListItems, granularListSafeHtml, isSafeHtml, jsx, listSafeHtml, mergeChildSegments, newBindingContext, raw, syncFormProp, wireBindings, wireRowBindings };
|
|
603
|
+
//# sourceMappingURL=chunk-GYRZQCSY.js.map
|
|
604
|
+
//# sourceMappingURL=chunk-GYRZQCSY.js.map
|