kerfjs 5.0.0-beta.8 → 5.0.0-beta.81
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 +2577 -117
- package/README.md +96 -37
- package/ai/cursorrules +110 -61
- package/ai/manifest.json +16 -7
- package/ai/skill.md +120 -75
- package/dist/actions.d.ts +1 -1
- package/dist/actions.js +10 -6
- package/dist/actions.js.map +1 -1
- package/dist/array-signal.js +1 -1
- package/dist/async.js +22 -19
- package/dist/async.js.map +1 -1
- package/dist/attach.js +1 -66
- package/dist/attach.js.map +1 -1
- package/dist/{chunk-HW7KSM2Y.js → chunk-3C6F6NM4.js} +1 -1
- package/dist/{chunk-HW7KSM2Y.js.map → chunk-3C6F6NM4.js.map} +1 -1
- package/dist/{chunk-GY4XV2UV.js → chunk-4TX6YJDM.js} +10 -3
- package/dist/chunk-4TX6YJDM.js.map +1 -0
- package/dist/{chunk-QIP723L4.js → chunk-5WRGJZV6.js} +3 -2
- package/dist/chunk-5WRGJZV6.js.map +1 -0
- package/dist/{chunk-KZJXHFIB.js → chunk-BI4HGICV.js} +3 -3
- package/dist/chunk-BI4HGICV.js.map +1 -0
- package/dist/chunk-FM6N2B4N.js +70 -0
- package/dist/chunk-FM6N2B4N.js.map +1 -0
- package/dist/{chunk-LVH3GC6B.js → chunk-H7WTRM6F.js} +3 -3
- package/dist/chunk-H7WTRM6F.js.map +1 -0
- package/dist/{chunk-U6FK33SG.js → chunk-IHMDBBUJ.js} +2 -2
- package/dist/chunk-IHMDBBUJ.js.map +1 -0
- package/dist/chunk-JN7EJM3B.js +86 -0
- package/dist/chunk-JN7EJM3B.js.map +1 -0
- package/dist/{chunk-UZJ6I4T6.js → chunk-MK42GLPV.js} +1 -1
- package/dist/chunk-MK42GLPV.js.map +1 -0
- package/dist/{chunk-SVATPF5R.js → chunk-TLL3KXT6.js} +249 -160
- package/dist/chunk-TLL3KXT6.js.map +1 -0
- package/dist/{chunk-ZDCJZCNO.js → chunk-UGNPDLNI.js} +78 -45
- package/dist/chunk-UGNPDLNI.js.map +1 -0
- package/dist/{chunk-V46JKE44.js → chunk-WKIPLNVO.js} +1 -1
- package/dist/chunk-WKIPLNVO.js.map +1 -0
- package/dist/{delegate-CL9VTZFb.d.ts → delegate--CrfgYtC.d.ts} +0 -34
- package/dist/dev.d.ts +31 -13
- package/dist/dev.js +101 -35
- package/dist/dev.js.map +1 -1
- package/dist/html.js +5 -4
- package/dist/html.js.map +1 -1
- package/dist/index.d.ts +14 -9
- package/dist/index.js +29 -20
- package/dist/index.js.map +1 -1
- package/dist/jsx-runtime.d.ts +13 -5
- package/dist/jsx-runtime.js +1 -1
- package/dist/list.d.ts +7 -3
- package/dist/list.js +126 -28
- package/dist/list.js.map +1 -1
- package/dist/overlay.d.ts +128 -129
- package/dist/overlay.js +675 -366
- package/dist/overlay.js.map +1 -1
- package/dist/remount.js +2 -2
- package/dist/router.d.ts +13 -4
- package/dist/router.js +37 -10
- package/dist/router.js.map +1 -1
- package/dist/scope.d.ts +35 -8
- package/dist/scope.js +61 -30
- package/dist/scope.js.map +1 -1
- package/dist/testing.js +1 -1
- package/dist/timing.d.ts +3 -2
- package/dist/timing.js +1 -1
- package/dist/timing.js.map +1 -1
- package/llms.txt +12 -5
- package/package.json +33 -8
- package/setup/cli.mjs +90 -0
- package/setup/index.d.mts +37 -0
- package/setup/index.mjs +1347 -0
- package/setup/jsonc.mjs +201 -0
- package/setup/state.schema.json +56 -0
- package/dist/chunk-GY4XV2UV.js.map +0 -1
- package/dist/chunk-KPXIOG2C.js +0 -54
- package/dist/chunk-KPXIOG2C.js.map +0 -1
- package/dist/chunk-KZJXHFIB.js.map +0 -1
- package/dist/chunk-LVH3GC6B.js.map +0 -1
- package/dist/chunk-QIP723L4.js.map +0 -1
- package/dist/chunk-SVATPF5R.js.map +0 -1
- package/dist/chunk-U6FK33SG.js.map +0 -1
- package/dist/chunk-UZJ6I4T6.js.map +0 -1
- package/dist/chunk-V46JKE44.js.map +0 -1
- package/dist/chunk-ZDCJZCNO.js.map +0 -1
package/README.md
CHANGED
|
@@ -12,14 +12,14 @@
|
|
|
12
12
|
|
|
13
13
|
<p align="center">
|
|
14
14
|
<a href="https://www.npmjs.com/package/kerfjs"><img src="https://img.shields.io/npm/v/kerfjs.svg" alt="npm version" /></a>
|
|
15
|
-
<img src="https://img.shields.io/badge/min%2Bgzip-~
|
|
15
|
+
<img src="https://img.shields.io/badge/min%2Bgzip-~13%20KB-brightgreen.svg" alt="~13 KB minified and gzipped" />
|
|
16
16
|
<img src="https://img.shields.io/npm/l/kerfjs.svg" alt="MIT license" />
|
|
17
17
|
<img src="https://img.shields.io/badge/types-included-3178c6.svg" alt="TypeScript types included" />
|
|
18
18
|
</p>
|
|
19
19
|
|
|
20
20
|
---
|
|
21
21
|
|
|
22
|
-
> ~
|
|
22
|
+
> ~13 KB. No virtual DOM. No compiler. No magic.
|
|
23
23
|
> Reactive UI that touches only the bytes that changed.
|
|
24
24
|
|
|
25
25
|
```ts
|
|
@@ -31,14 +31,17 @@ const app = document.getElementById('app')!;
|
|
|
31
31
|
mount(app, () => (
|
|
32
32
|
<div>
|
|
33
33
|
<button data-action="inc">+</button>
|
|
34
|
-
<span>{count
|
|
34
|
+
<span>{count}</span>
|
|
35
35
|
</div>
|
|
36
36
|
));
|
|
37
37
|
|
|
38
38
|
delegate(app, 'click', '[data-action="inc"]', () => count.value++);
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
-
That's it. Your JSX renders to HTML strings,
|
|
41
|
+
That's it. Your JSX renders to HTML strings, and handing the signal itself to
|
|
42
|
+
the text hole binds that one node directly. Structural reads still re-run the
|
|
43
|
+
render, where kerf's native diff applies the minimum DOM mutations needed to
|
|
44
|
+
make the live tree match.
|
|
42
45
|
|
|
43
46
|
Here's the whole development loop — write a component, run the dev server, click around, edit, watch the browser pick it up:
|
|
44
47
|
|
|
@@ -51,25 +54,42 @@ Here's the whole development loop — write a component, run the dev server, cli
|
|
|
51
54
|
npm install kerfjs
|
|
52
55
|
```
|
|
53
56
|
|
|
57
|
+
For an existing project, install the AI guidance, strict TypeScript/ESLint
|
|
58
|
+
feedback, and package scripts with a reviewed dry run:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
npx kerfjs setup # inspect a value-level plan
|
|
62
|
+
npx kerfjs setup --write --yes # apply it
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
The command detects core versus `@kerfjs/ui`, supports deterministic npm,
|
|
66
|
+
pnpm, and Yarn workspace selection plus manager-correct offline installs, and
|
|
67
|
+
never adds UI to a core-only project. See
|
|
68
|
+
[`docs/24-ai-first-setup.md`](./docs/24-ai-first-setup.md).
|
|
69
|
+
|
|
54
70
|
```jsonc
|
|
55
71
|
// tsconfig.json — point JSX at kerf
|
|
56
72
|
{ "compilerOptions": { "jsx": "react-jsx", "jsxImportSource": "kerfjs" } }
|
|
57
73
|
```
|
|
58
74
|
|
|
59
75
|
Write plain `.tsx` and build with your existing esbuild / Vite / tsup — no extra plugin. New here? Read the [5-minute orientation](https://github.com/brianwestphal/kerf/blob/main/docs/orientation.md), or open a [complete example](https://brianwestphal.github.io/kerf/examples/complete/).
|
|
76
|
+
|
|
60
77
|
## Why Kerf
|
|
61
78
|
|
|
62
|
-
1. **~
|
|
79
|
+
1. **~13 KB in the browser.** ~13 KB minified + gzipped including `@preact/signals-core` (~13 KB with `arraySignal`). The optional setup CLI has its own npm dependencies; they stay out of the browser bundle. No virtual DOM, no scheduler, no concurrent-mode machinery. On the official [krausest benchmark](https://krausest.github.io/js-framework-benchmark/current.html) kerf sits in the same cluster as Vue, Lit, and vanjs; Solid's compiler leads the update-path benchmarks, which kerf doesn't try to match by design — no compiler.
|
|
63
80
|
|
|
64
|
-
2. **No virtual DOM, no compiler.** JSX → HTML strings → native diff. DevTools shows the real DOM because it
|
|
81
|
+
2. **No virtual DOM, no compiler.** JSX → HTML strings → native diff. DevTools shows the real DOM because it _is_ the DOM.
|
|
65
82
|
|
|
66
|
-
3. **Values bind, structure re-renders.** Hand a signal
|
|
83
|
+
3. **Values bind, structure re-renders.** Hand a signal _itself_ into a JSX hole — `class={selectedId}` — and kerf binds that one node: on change, only that attribute updates, with no render re-run and no list reconcile. Moving selection between rows in a 10,000-row table touches at most the old and new row classes. ([more →](#fine-grained-updates-bind-a-signal-into-a-hole))
|
|
67
84
|
|
|
68
85
|
4. **Focus, selection, and listeners survive re-renders — even mid-list.** The reconciler morphs instead of rebuilding, so caret position, IME composition, scroll, and delegated listeners survive every update; keyed rows are patched in place rather than recreated.
|
|
69
86
|
|
|
70
|
-
5. **
|
|
87
|
+
5. **A first-party UI layer when you want one.** `@kerfjs/ui` adds accessible components, responsive application layouts, typed CSS values, and explicit wiring helpers without changing kerf's stateless component model. It is separately installable and tree-shakeable, so the core stays the core. ([tour →](#first-party-ui-separately-installable))
|
|
88
|
+
|
|
89
|
+
6. **Safe by default.** Text and attributes are HTML-escaped automatically, URL attributes are scheme-screened (`javascript:` dropped), and inline `on*` handlers are rejected outright — so untrusted data stays inert. `raw()` is the explicit, auditable opt-out.
|
|
90
|
+
|
|
91
|
+
**Plus, nothing you don't ask for:** JSX typed against the HTML standard (not React's props) · a small public API with no hooks, lifecycle, or per-instance state · tree-shakeable companion subpaths (`router`, `list`, `overlay`, `async`, …) that stay out of the core until imported · an [ESLint plugin](https://brianwestphal.github.io/kerf/docs/eslint-plugin/) + opt-in dev warnings + `create-kerf-component` scaffold · plain TS/JSX/ESM that drops into esbuild / Vite / tsup — or **no** build at all via the `html` tagged template.
|
|
71
92
|
|
|
72
|
-
**Plus, nothing you don't ask for:** JSX typed against the HTML standard (not React's props) · a ~18-export API with no hooks, lifecycle, or per-instance state · **nine** tree-shakeable companion subpaths (`router`, `list`, `overlay`, `async`, …) that stay out of the core until imported · an [ESLint plugin](https://brianwestphal.github.io/kerf/docs/eslint-plugin/) + opt-in dev warnings + `create-kerf-component` scaffold · plain TS/JSX/ESM that drops into esbuild / Vite / tsup — or **no** build at all via the `html` tagged template.
|
|
73
93
|
## When to use Kerf
|
|
74
94
|
|
|
75
95
|
- **Hybrid desktop apps (Tauri / Electron)** — small bundle, predictable diff, debuggable runtime; ideal for the embedded webview.
|
|
@@ -82,9 +102,9 @@ Write plain `.tsx` and build with your existing esbuild / Vite / tsup — no ext
|
|
|
82
102
|
### When to reach for something else
|
|
83
103
|
|
|
84
104
|
- Need a full ecosystem (router + forms + data + SSR streaming) → **Next.js / Remix / SolidStart**.
|
|
85
|
-
- Building a deeply
|
|
105
|
+
- Building a deeply componentized design-system app → **React / Solid / Svelte**.
|
|
86
106
|
- Need React Native / cross-platform mobile → **React** (Kerf + Tauri/Electron also covers many of these cases).
|
|
87
|
-
- Building a static site → **Astro** (we use it for
|
|
107
|
+
- Building a static site → **Astro** (we use it for _this_ project's site).
|
|
88
108
|
- Already invested in a framework where switching cost outweighs the bundle size gain.
|
|
89
109
|
|
|
90
110
|
## Quick tour
|
|
@@ -144,7 +164,7 @@ delegate(root, 'click', REMOVE.selector, (_e, btn) => { /* … */ }); // in dele
|
|
|
144
164
|
|
|
145
165
|
### Fine-grained updates: bind a signal into a hole
|
|
146
166
|
|
|
147
|
-
Inside a `mount()`, hand a signal
|
|
167
|
+
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:
|
|
148
168
|
|
|
149
169
|
```ts
|
|
150
170
|
const status = signal('idle');
|
|
@@ -160,7 +180,7 @@ status.value = 'saving'; // updates the class + the text node directly — n
|
|
|
160
180
|
|
|
161
181
|
The headline use is external state driving a hot spot: a `selectedId` moving between rows inside a 10,000-row `each()` list updates at most the old and new row class attributes, with 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.
|
|
162
182
|
|
|
163
|
-
This is kerf's guiding idiom —
|
|
183
|
+
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.
|
|
164
184
|
|
|
165
185
|
### Long keyed lists: `arraySignal`
|
|
166
186
|
|
|
@@ -187,14 +207,15 @@ The class lives in its own subpath so apps that don't need it shed ~1 KB. Reads
|
|
|
187
207
|
`mount()` wraps `effect()` so the render re-runs on signal changes. Sometimes you have a freshly-built template and an already-populated element and you just want to reconcile them once — no subscription, no re-render loop. That's `morph`:
|
|
188
208
|
|
|
189
209
|
```ts
|
|
190
|
-
import { morph, raw } from
|
|
210
|
+
import { morph, raw } from "kerfjs";
|
|
191
211
|
|
|
192
|
-
|
|
193
|
-
morph(liveCard,
|
|
194
|
-
morph(liveCard,
|
|
212
|
+
// `template` describes liveCard's CHILDREN; liveCard's own attributes are untouched
|
|
213
|
+
morph(liveCard, freshlyBuiltCardEl); // Element template: its child nodes are used
|
|
214
|
+
morph(liveCard, '<h2 class="card-title">…</h2><p>…</p>'); // raw HTML string
|
|
215
|
+
morph(liveCard, raw(htmlFromServer)); // SafeHtml
|
|
195
216
|
```
|
|
196
217
|
|
|
197
|
-
Same algorithm `mount()` uses internally — `data-morph-skip`, `data-morph-skip-children`, `data-morph-preserve`, focused-input value + selection preservation, the
|
|
218
|
+
Same algorithm `mount()` uses internally — `data-morph-skip`, `data-morph-skip-children`, `data-morph-preserve`, focused-input value + selection preservation, the user-agent-owned `open` rule (`<details>`, `<dialog>`, custom elements) all carry over. Only the live root's children are reconciled — the root's own attributes are never touched. 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.
|
|
198
219
|
|
|
199
220
|
### No build step at all: the `html` tagged template
|
|
200
221
|
|
|
@@ -202,22 +223,27 @@ Same algorithm `mount()` uses internally — `data-morph-skip`, `data-morph-skip
|
|
|
202
223
|
|
|
203
224
|
```html
|
|
204
225
|
<script type="module">
|
|
205
|
-
import { signal, mount, each } from
|
|
206
|
-
import { html } from
|
|
207
|
-
|
|
208
|
-
const items = signal([{ id: 1, label:
|
|
209
|
-
|
|
210
|
-
mount(
|
|
211
|
-
|
|
212
|
-
|
|
226
|
+
import { signal, mount, each } from "https://esm.sh/kerfjs@5";
|
|
227
|
+
import { html } from "https://esm.sh/kerfjs@5/html";
|
|
228
|
+
|
|
229
|
+
const items = signal([{ id: 1, label: "no build step" }]);
|
|
230
|
+
|
|
231
|
+
mount(
|
|
232
|
+
document.getElementById("app"),
|
|
233
|
+
() => html`
|
|
234
|
+
<ul>
|
|
235
|
+
${each(items.value, (i) => html`<li id="${i.id}">${i.label}</li>`)}
|
|
236
|
+
</ul>
|
|
237
|
+
`,
|
|
238
|
+
);
|
|
213
239
|
</script>
|
|
214
240
|
```
|
|
215
241
|
|
|
216
|
-
Nothing is self-hosted — `kerfjs` is on npm, so every ESM CDN (esm.sh, jsDelivr, unpkg) mirrors it automatically. esm.sh works with a direct import as shown; jsDelivr / unpkg want an importmap so the internal `@preact/signals-core` import resolves. Pin to a major (`@
|
|
242
|
+
Nothing is self-hosted — `kerfjs` is on npm, so every ESM CDN (esm.sh, jsDelivr, unpkg) mirrors it automatically. esm.sh works with a direct import as shown; jsDelivr / unpkg want an importmap so the internal `@preact/signals-core` import resolves. Pin to a major (`@5`, as shown) rather than floating on `latest`, or an exact version (`@5.0.0`) for full reproducibility. 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. See [`docs/6-jsx-runtime.md`](./docs/6-jsx-runtime.md) §6.11 (§6.11.1 for the full CDN / importmap recipes) — 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.
|
|
217
243
|
|
|
218
244
|
### Batteries when you need them: the companion subpaths
|
|
219
245
|
|
|
220
|
-
The core stays tiny because the patterns every real app rebuilds live in optional, tree-shakeable subpaths — a modal you'd otherwise hand-roll (`kerfjs/overlay`), an async-state container with the stale-response race already solved (`kerfjs/async`), a debounce that composes inside the reactive graph (`kerfjs/timing`),
|
|
246
|
+
The core stays tiny because the patterns every real app rebuilds live in optional, tree-shakeable subpaths — a modal you'd otherwise hand-roll (`kerfjs/overlay`), an async-state container with the stale-response race already solved (`kerfjs/async`), a debounce that composes inside the reactive graph (`kerfjs/timing`), and disposer groups with or without a DOM owner (`kerfjs/scope`). The largest is `kerfjs/list` — a keyed list that mounts each row individually (so a signal one row reads updates just that row) and virtualizes a long viewport, with fixed, app-declared, or measured row heights:
|
|
221
247
|
|
|
222
248
|
```ts
|
|
223
249
|
import { bindList, observeRowHeights } from 'kerfjs/list';
|
|
@@ -238,7 +264,7 @@ const list = bindList(scrollEl, messages, {
|
|
|
238
264
|
observeRowHeights(list); // one ResizeObserver → kerf anchor-corrects scroll
|
|
239
265
|
```
|
|
240
266
|
|
|
241
|
-
And a whole client-side router in one call — `kerfjs/router`, the "postcard router." A route table, a keyed `outlet()`, and automatic `<a href>` interception; the
|
|
267
|
+
And a whole client-side router in one call — `kerfjs/router`, the "postcard router." A route table, a keyed `outlet()`, and automatic `<a href>` interception; the _core_ stays router-free (this is opt-in):
|
|
242
268
|
|
|
243
269
|
```ts
|
|
244
270
|
import { createRouter } from 'kerfjs/router';
|
|
@@ -258,13 +284,45 @@ mount(app, () => <div><nav>{/* <a href> links, auto-intercepted */}</nav>{router
|
|
|
258
284
|
|
|
259
285
|
Each subpath adds nothing to the main barrel until it's imported. See [`docs/8-api-reference.md`](./docs/8-api-reference.md) for the full list (`list`, `router`, `overlay`, `scope`, `async`, `timing`, `remount`, `attach`, `actions`).
|
|
260
286
|
|
|
287
|
+
### First-party UI, separately installable
|
|
288
|
+
|
|
289
|
+
`@kerfjs/ui` is the optional component layer for applications that want a
|
|
290
|
+
coherent interface vocabulary without switching frameworks. Components return
|
|
291
|
+
Kerf `SafeHtml`; the application still owns state and wires behavior explicitly.
|
|
292
|
+
Component subpaths pull in only their reachable CSS, while responsive app
|
|
293
|
+
layouts, typed CSS-value builders, Web Awesome adapters, and the checked
|
|
294
|
+
component catalog stay opt-in.
|
|
295
|
+
|
|
296
|
+
```bash
|
|
297
|
+
npm install kerfjs @kerfjs/ui
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
```tsx
|
|
301
|
+
import { mount } from "kerfjs";
|
|
302
|
+
import { ListItem } from "@kerfjs/ui/list-item";
|
|
303
|
+
import { Toolbar } from "@kerfjs/ui/toolbar";
|
|
304
|
+
import { ToolbarText } from "@kerfjs/ui/toolbar-text";
|
|
305
|
+
|
|
306
|
+
const root = document.getElementById("app")!;
|
|
307
|
+
|
|
308
|
+
mount(root, () => (
|
|
309
|
+
<>
|
|
310
|
+
<Toolbar label="Files" leading={<ToolbarText text="Workspace" />} />
|
|
311
|
+
<ListItem action="open-file" label="src/main.ts" selected />
|
|
312
|
+
</>
|
|
313
|
+
));
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
Start with the [component selection matrix](./ui/docs/component-selection.md),
|
|
317
|
+
or read the complete [`@kerfjs/ui` package guide](./ui/README.md).
|
|
318
|
+
|
|
261
319
|
## Optional tooling
|
|
262
320
|
|
|
263
321
|
Install and JSX setup are in [Quick start](#quick-start) above. These companion packages are opt-in.
|
|
264
322
|
|
|
265
323
|
### Optional: `eslint-plugin-kerfjs`
|
|
266
324
|
|
|
267
|
-
A companion ESLint plugin enforces kerf's hard rules at edit time.
|
|
325
|
+
A companion ESLint plugin enforces kerf's hard rules at edit time. Its eight core rules cover inline JSX event handlers, missing `data-key` in `each()`, nested `mount()`, delegate-disposer capture, `attr()` selector rename-safety, `raw()` XSS audit trails, JSX augmentation, and AI-assistant config hygiene. Six additional UI rules check component ownership, composition, CSS values, application preferences, wiring, and public boundaries when you opt into the UI preset. The plugin requires no TypeScript parser service.
|
|
268
326
|
|
|
269
327
|
```bash
|
|
270
328
|
npm install --save-dev eslint-plugin-kerfjs
|
|
@@ -272,15 +330,15 @@ npm install --save-dev eslint-plugin-kerfjs
|
|
|
272
330
|
|
|
273
331
|
```js
|
|
274
332
|
// eslint.config.js (flat config, ESLint v9+)
|
|
275
|
-
import kerfjs from
|
|
333
|
+
import kerfjs from "eslint-plugin-kerfjs";
|
|
276
334
|
export default [kerfjs.configs.recommended];
|
|
277
335
|
```
|
|
278
336
|
|
|
279
|
-
Full docs at [brianwestphal.github.io/kerf/docs/eslint-plugin/](https://brianwestphal.github.io/kerf/docs/eslint-plugin/) —
|
|
337
|
+
Full docs at [brianwestphal.github.io/kerf/docs/eslint-plugin/](https://brianwestphal.github.io/kerf/docs/eslint-plugin/) — flat config, per-rule examples, and the rationale for which violations get lint rules vs. dev-warns vs. strict TS.
|
|
280
338
|
|
|
281
339
|
### Optional: `create-kerf-component`
|
|
282
340
|
|
|
283
|
-
Building a reusable component package? Scaffold one that already follows kerf's hard packaging rules (kerfjs as a peer dependency and `external` in the build, ESM + `.d.ts`, `jsxImportSource: "kerfjs"`, subpath exports)
|
|
341
|
+
Building a reusable component package? Scaffold one that already follows kerf's hard packaging rules (kerfjs as a peer dependency and `external` in the build, ESM + `.d.ts`, `jsxImportSource: "kerfjs"`, subpath exports), includes an example component showing per-instance state via a factory and a `wire(root)` delegation disposer, and generates checked package-qualified AI component metadata from explicit author decisions:
|
|
284
342
|
|
|
285
343
|
```bash
|
|
286
344
|
npm create kerf-component@latest my-widgets
|
|
@@ -294,16 +352,17 @@ See [`docs/13-component-packages.md`](./docs/13-component-packages.md) for the f
|
|
|
294
352
|
- **Docs:** [`docs/`](./docs/) — overview · reactivity · stores · render · events · jsx · svg · [API reference](./docs/8-api-reference.md)
|
|
295
353
|
- **Migrating:** [coming from another framework?](https://brianwestphal.github.io/kerf/migrating/) — side-by-side TodoMVC translations + per-framework gotchas
|
|
296
354
|
- **AI guide:** [`docs/ai/usage-guide.md`](./docs/ai/usage-guide.md) — reference for AI tools fetching kerf docs (linked from `llms.txt`)
|
|
297
|
-
- **
|
|
298
|
-
- **
|
|
355
|
+
- **UI package:** [`@kerfjs/ui`](./ui/README.md) — accessible components, responsive app layouts, typed CSS values, Web Awesome adapters, and checked AI-facing component metadata
|
|
356
|
+
- **ESLint plugin:** [brianwestphal.github.io/kerf/docs/eslint-plugin/](https://brianwestphal.github.io/kerf/docs/eslint-plugin/) — `eslint-plugin-kerfjs`; eight core rules plus six opt-in UI rules at edit time (source: [`eslint-plugin/`](./eslint-plugin/))
|
|
357
|
+
- **Component scaffold:** `npm create kerf-component@latest <dir>` — `create-kerf-component`; generates a publishable component package with packaging rules plus deterministic, drift-checked AI metadata pre-wired (source: [`create-kerf-component/`](./create-kerf-component/))
|
|
299
358
|
- **Demo:** [live demo](https://brianwestphal.github.io/kerf/demo/) — nine sections exercising every primitive (counter, store-backed cart, focus survival, keyed list, morph-skip, SVG render, Tier-2 capture, `arraySignal` patches, fine-grained signal bindings)
|
|
300
359
|
- **Repo:** [github.com/brianwestphal/kerf](https://github.com/brianwestphal/kerf)
|
|
301
360
|
|
|
302
361
|
## Why "kerf"?
|
|
303
362
|
|
|
304
|
-
A
|
|
363
|
+
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.
|
|
305
364
|
|
|
306
|
-
(And yes, ~~kerformance~~ →
|
|
365
|
+
(And yes, ~~kerformance~~ → _performance_ jokes were written. They were also rejected.)
|
|
307
366
|
|
|
308
367
|
## Status
|
|
309
368
|
|
package/ai/cursorrules
CHANGED
|
@@ -1,18 +1,23 @@
|
|
|
1
|
-
<!-- kerf-skill-version: 1.
|
|
1
|
+
<!-- kerf-skill-version: 1.23.0 -->
|
|
2
|
+
|
|
2
3
|
# kerf.cursorrules — rules for building apps with kerf
|
|
4
|
+
|
|
3
5
|
#
|
|
6
|
+
|
|
4
7
|
# Drop this file into your project as `.cursorrules` (Cursor will pick it
|
|
8
|
+
|
|
5
9
|
# up automatically) when you're using kerf (https://github.com/brianwestphal/kerf).
|
|
10
|
+
|
|
6
11
|
# These rules condense `docs/ai/usage-guide.md` into the form Cursor parses.
|
|
7
12
|
|
|
8
|
-
You are writing a UI in kerf — a ~
|
|
13
|
+
You are writing a UI in kerf — a ~13 KB reactive framework (still ~13 KB with `arraySignal`): signals + DOM morphing + JSX → HTML strings. No virtual DOM, no compiler, no scheduler.
|
|
9
14
|
|
|
10
15
|
## Setup
|
|
11
16
|
|
|
12
17
|
- Install with `npm install kerfjs`.
|
|
13
18
|
- `tsconfig.json`: `"jsx": "react-jsx"`, `"jsxImportSource": "kerfjs"`.
|
|
14
19
|
- Vite / esbuild need no extra config.
|
|
15
|
-
- **Dev diagnostics are opt-in by import, and only an APP installs them.** kerf does not infer dev mode. In the app entry add `if (import.meta.env.DEV) await import('kerfjs/dev');` (Vite) or `if (process.env.NODE_ENV !== 'production') await import('kerfjs/dev');` (webpack/Node). That enables the read-only store `get()` snapshot, the throwing dangerous-URL screen, and makes the `KERF_DEV_WARN_*` family available; omitting it is production shape and sheds ~4.7 KB min+gzip because the condition folds away and the chunk is never emitted. Put it FIRST if relying on the untracked-signal
|
|
20
|
+
- **Dev diagnostics are opt-in by import, and only an APP installs them.** kerf does not infer dev mode. In the app entry add `if (import.meta.env.DEV) await import('kerfjs/dev');` (Vite) or `if (process.env.NODE_ENV !== 'production') await import('kerfjs/dev');` (webpack/Node). That enables the read-only store `get()` snapshot, the throwing dangerous-URL screen, and makes the `KERF_DEV_WARN_*` family available; omitting it is production shape and sheds ~4.7 KB min+gzip because the condition folds away and the chunk is never emitted. Put it (and any `enableWarnings()` call) FIRST if relying on the untracked-signal, delegate-in-effect, or rebuilt-listeners warnings — `signal()`, `effect()`, and `mount()` decide those at creation time, so anything created earlier stays uncovered.
|
|
16
21
|
- **Switch individual warnings on with `enableWarnings()`**, which is the only switch that works in a browser (no `process` object there, and a bundler `define` cannot reach the read): `const dev = await import('kerfjs/dev'); dev.enableWarnings({ staleBinding: true, narrowSet: true, invariants: 'throw' });`. The `KERF_DEV_WARN_*` env vars do the same for Node/SSR/CI; an explicit call wins either way.
|
|
17
22
|
- **A component package must NEVER import `kerfjs/dev`.** The hooks are process-global, so installing them is the consuming app's decision — a library that does it forces the diagnostics (and the chunk) on every consumer. Put the import in your demo page or test harness instead.
|
|
18
23
|
- Recommended: also install `eslint-plugin-kerfjs` (`npm install --save-dev eslint-plugin-kerfjs`) and add `kerfjs.configs.recommended` to the project's eslint config. It enforces five of the hard rules below (no inline JSX event handlers, require `data-key` in `each()`, capture `delegate()` disposers, no nested `mount()`, prefer module JSX augmentation) at edit time so violations surface as IDE squiggles before any code runs.
|
|
@@ -28,13 +33,13 @@ the complete layer is intentional. Reuse these primitives before creating
|
|
|
28
33
|
parallel local ones; keep domain state, commands, routing, and tab/menu policy
|
|
29
34
|
in the app.
|
|
30
35
|
|
|
31
|
-
Use
|
|
32
|
-
`data-*` metadata.
|
|
36
|
+
Use ListItem/ListActionRow/ListHeader `rootAttributes` only for application
|
|
37
|
+
`data-*` metadata. ListActionRow `trailingActionAttributes` and ListHeader
|
|
33
38
|
`triggerAttributes` additionally support native popover target/action and
|
|
34
39
|
`aria-controls`/`aria-haspopup`. Do not use these slots to override action,
|
|
35
40
|
item identity, selection, disclosure, naming, disabled, icon, or role
|
|
36
41
|
semantics; one `role="menuitem"` does not make a complete menu widget.
|
|
37
|
-
`
|
|
42
|
+
`ListItem.trailing` is dormant. Use `ListActionRow` for sibling primary and
|
|
38
43
|
trailing native buttons, and keep its `label`, `icon`, and
|
|
39
44
|
`trailingActionIcon` SafeHtml slots free of controls.
|
|
40
45
|
|
|
@@ -43,13 +48,22 @@ Wire emitted `data-action` hooks at the mount root and retain disposers.
|
|
|
43
48
|
disposers. Compose `AppTab` inside controlled `TabBar`; `wireTabBars()` supplies
|
|
44
49
|
horizontal edge autoscroll while dragging. Apply reorder reports
|
|
45
50
|
with `reorderTabs()`, and keep order, selection, close policy, routing, panels,
|
|
46
|
-
and persistence in the host.
|
|
51
|
+
and persistence in the host. Toolbars draw no divider by default; call
|
|
52
|
+
`wireScrollDividers()` once at the app root so panes and tab bars show scroll
|
|
53
|
+
dividers only while content is scrolled beneath or beyond them. Icons/spinners are decorative unless
|
|
47
54
|
labeled, and assertive banners are only for urgent interruption.
|
|
48
55
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
56
|
+
Components own their styles; configure, never override. Set how a component
|
|
57
|
+
looks and sits through its typed props (size, density, inset, presentation,
|
|
58
|
+
`dividerSides`, `ToolbarConfig`, `PaneConfig`, …); never write application
|
|
59
|
+
CSS that selects a `.kui-*` class, a `[data-component]` root, a `wa-*` element
|
|
60
|
+
a Kerf component renders, or its `::part()`s, and never add padding, margin,
|
|
61
|
+
width, or height around a component to size or space it (it owns its 8px
|
|
62
|
+
inset — adding more double-insets). A missing prop is a component gap to
|
|
63
|
+
report upstream, not a reason to override. The opinionated semantic ramps
|
|
64
|
+
match Hot Sheet 2 and Web Awesome: override `--kui-color-*` globally or
|
|
65
|
+
component properties such as `--kui-state-banner-background` at a tone or
|
|
66
|
+
instance boundary, for theming only.
|
|
53
67
|
|
|
54
68
|
For Web Awesome's broader free component set, import the CSS-only
|
|
55
69
|
`@kerfjs/ui/webawesome.css` theme once, then import only each Web Awesome
|
|
@@ -66,41 +80,53 @@ peer and stays out of bundles that use neither Select nor the Web Awesome theme.
|
|
|
66
80
|
|
|
67
81
|
```ts
|
|
68
82
|
import {
|
|
69
|
-
signal,
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
83
|
+
signal,
|
|
84
|
+
computed,
|
|
85
|
+
effect,
|
|
86
|
+
batch, // reactivity
|
|
87
|
+
defineStore,
|
|
88
|
+
resetAllStores, // stores
|
|
89
|
+
mount,
|
|
90
|
+
morph,
|
|
91
|
+
each, // render (reactive + one-shot) + keyed list
|
|
92
|
+
delegate,
|
|
93
|
+
delegateCapture, // events
|
|
94
|
+
toElement, // direct JSX → DOM Element (or DocumentFragment for multi-root)
|
|
95
|
+
SafeHtml,
|
|
96
|
+
isSafeHtml,
|
|
97
|
+
raw,
|
|
98
|
+
Fragment,
|
|
99
|
+
type JSXChildren,
|
|
100
|
+
} from "kerfjs";
|
|
76
101
|
|
|
77
102
|
// Optional, only when you need granular collection updates:
|
|
78
|
-
import { arraySignal } from
|
|
103
|
+
import { arraySignal } from "kerfjs/array-signal";
|
|
79
104
|
|
|
80
105
|
// Development diagnostics — gate with YOUR build's dev flag, in YOUR code.
|
|
81
|
-
if (import.meta.env.DEV) await import(
|
|
106
|
+
if (import.meta.env.DEV) await import("kerfjs/dev");
|
|
82
107
|
```
|
|
83
108
|
|
|
84
|
-
| Export
|
|
85
|
-
|
|
|
86
|
-
| `signal(initial)`
|
|
87
|
-
| `computed(fn)`
|
|
88
|
-
| `effect(fn)`
|
|
89
|
-
| `batch(fn)`
|
|
90
|
-
| `defineStore({initial, actions})`
|
|
91
|
-
| `resetAllStores()`
|
|
92
|
-
| `mount(el, render)`
|
|
93
|
-
| `morph(liveRoot, template)`
|
|
94
|
-
| `each(items, render, cacheKey?)`
|
|
95
|
-
| `each(items, render, { cacheKey, key })`
|
|
96
|
-
| `delegate(root, type, sel, h)`
|
|
97
|
-
| `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()`
|
|
98
|
-
| `attr(name, value)`
|
|
99
|
-
| `attr(name)`
|
|
100
|
-
| `toElement(jsx)`
|
|
101
|
-
| `raw(html)`
|
|
102
|
-
| `
|
|
103
|
-
|
|
|
109
|
+
| Export | Use |
|
|
110
|
+
| -------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
111
|
+
| `signal(initial)` | atomic reactive state; read/write via `.value` |
|
|
112
|
+
| `computed(fn)` | derived value (read-only) |
|
|
113
|
+
| `effect(fn)` | side effect that re-runs on signal change |
|
|
114
|
+
| `batch(fn)` | coalesce multiple writes into one re-run |
|
|
115
|
+
| `defineStore({initial, actions})` | named multi-consumer state |
|
|
116
|
+
| `resetAllStores()` | reset every store (test teardown) |
|
|
117
|
+
| `mount(el, render)` | bind reactive render to a DOM element; returns a disposer |
|
|
118
|
+
| `morph(liveRoot, template)` | one-shot reconcile against an already-populated element (SSR hydration, page-refresh diffs). Template can be `Element`, `SafeHtml`, or HTML string |
|
|
119
|
+
| `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 |
|
|
120
|
+
| `each(items, render, { cacheKey, key })` | same, options form. **`key` gives the list a stable identity** — required whenever a _conditional_ list can render before this one, else kerf rebuilds this list and its rows lose focus/scroll/IME. A keyed list takes no positional slot, so keying the conditional list usually fixes its siblings too |
|
|
121
|
+
| `delegate(root, type, sel, h)` | one listener at the root, walks `closest(selector)` from target |
|
|
122
|
+
| `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()` |
|
|
123
|
+
| `attr(name, value)` | pre-computed `AttrSpec<N,V>` — `.selector` for `delegate()`, `.attrs` to spread into JSX (rename-safe) |
|
|
124
|
+
| `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 |
|
|
125
|
+
| `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. |
|
|
126
|
+
| `raw(html)` | inject pre-escaped HTML |
|
|
127
|
+
| `JSXChildren` | recursive type for general-purpose component `children`: SafeHtml, primitives, read-only signals, nullish/boolean empty values, and readonly arrays at any depth. Use a narrower union for semantically restricted slots |
|
|
128
|
+
| `arraySignal(initial?)` | granular keyed-list signal at `kerfjs/array-signal` subpath; `each()` reconciles in O(patches) |
|
|
129
|
+
| `` 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}"`) |
|
|
104
130
|
|
|
105
131
|
## Hard rules — get these right on the first try
|
|
106
132
|
|
|
@@ -109,9 +135,9 @@ if (import.meta.env.DEV) await import('kerfjs/dev');
|
|
|
109
135
|
3. **Escape hatches:** `data-morph-skip` (element + subtree preserved verbatim — for Monaco / xterm / D3); `data-morph-skip-children` (attrs morph, subtree preserved — for client-hydrated slots whose host classes change); `data-morph-preserve` (element survives the trailing-removal pass — for imperatively-injected nodes like autoplay videos).
|
|
110
136
|
4. **Never `addEventListener` inside a `mount()`-managed tree** unless under `data-morph-skip`. A morph re-render may discard the node. Use `delegate` / `delegateCapture` instead.
|
|
111
137
|
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).
|
|
112
|
-
6. **One `mount()` per root.** Don't nest. Compose with plain functions that return JSX.
|
|
138
|
+
6. **One `mount()` per root.** Don't nest. Compose with plain functions that return JSX. One exception: a modal `<dialog>` of your own that anchors kerf `popover()` / `tooltip()` surfaces must contain `<div data-kerf-overlay-host data-morph-skip></div>` — kerf renders those surfaces into it (inside the modal, so they are not inert) and that slot is the one allowed nested-mount boundary. Dialogs `kerfjs/overlay` opens get a slot automatically.
|
|
113
139
|
7. **No `<MyComponent />` semantics with hooks.** Components are plain functions returning JSX. State lives in module-scope signals or stores, never in component closures.
|
|
114
|
-
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
|
|
140
|
+
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 (`enableWarnings({ staleBinding: true })` (or `KERF_DEV_WARN_STALE_BINDING=1` under Node) detects it). Endpoint: a render reading NO `.value` runs exactly once — a fully bound mount never re-renders; `enableWarnings({ valueOnlyRerender: true })` (or `KERF_DEV_WARN_VALUE_ONLY_RERENDER=1` under Node) flags re-renders that could have been bindings.
|
|
115
141
|
9. **Store actions receive `(set, get)`, not `(state)`.** `set(next)` replaces state; mutating `get()` does nothing.
|
|
116
142
|
10. **Use `data-action` attributes, not inline `onClick`.** Inline handlers are NOT supported by the JSX → string runtime; delegate from the root instead.
|
|
117
143
|
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.
|
|
@@ -124,22 +150,26 @@ if (import.meta.env.DEV) await import('kerfjs/dev');
|
|
|
124
150
|
When deciding which primitive to reach for, work down the axes:
|
|
125
151
|
|
|
126
152
|
**Events.**
|
|
153
|
+
|
|
127
154
|
- 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.
|
|
128
155
|
- 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.
|
|
129
156
|
- 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.
|
|
130
157
|
|
|
131
158
|
**Lists.**
|
|
159
|
+
|
|
132
160
|
- Items change across renders (todos, chat messages, table rows) → `each(items, render)`.
|
|
133
161
|
- Static structural enumeration whose row render reads signals → `STATIC.map(item => <jsx/>)`. Inner `each(item.children, …)` still gets keyed reconcile.
|
|
134
162
|
- Long list with point-wise mutations → `arraySignal` + `each(arraySig, render)` for O(patches) updates.
|
|
135
163
|
|
|
136
164
|
**Side effects / imperative DOM.**
|
|
165
|
+
|
|
137
166
|
- Library-owned subtree survives across renders → `data-morph-skip` on host.
|
|
138
167
|
- Host attributes morph but subtree preserved → `data-morph-skip-children`.
|
|
139
168
|
- Imperatively-injected element survives the trailing-removal pass → `data-morph-preserve`.
|
|
140
169
|
- Focused input / contenteditable caret survives re-renders → automatic; no opt-in.
|
|
141
170
|
|
|
142
171
|
**Raw HTML.**
|
|
172
|
+
|
|
143
173
|
- User-controlled HTML → sanitize first (DOMPurify) then `raw(sanitized)`.
|
|
144
174
|
- Author-controlled trusted HTML → `raw(html)` directly.
|
|
145
175
|
- 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. The `javascript:` no-op placeholders (`javascript:void(0)`, `javascript:;`, …) are allowed — they're the placeholder-link idiom, matched whole so nothing can ride along.
|
|
@@ -152,20 +182,27 @@ When deciding which primitive to reach for, work down the axes:
|
|
|
152
182
|
// updates that one node directly with no render re-run; read `.value` only
|
|
153
183
|
// when the JSX STRUCTURE depends on the signal (conditionals, list shape).
|
|
154
184
|
const count = signal(0);
|
|
155
|
-
const ACTIONS = { inc: attr(
|
|
185
|
+
const ACTIONS = { inc: attr("data-action", "inc") } as const satisfies Record<
|
|
186
|
+
string,
|
|
187
|
+
AttrSpec<"data-action">
|
|
188
|
+
>;
|
|
156
189
|
|
|
157
|
-
mount(document.getElementById(
|
|
190
|
+
mount(document.getElementById("app")!, () => (
|
|
158
191
|
<div>
|
|
159
192
|
<button {...ACTIONS.inc.attrs}>+</button>
|
|
160
193
|
<span>{count}</span>
|
|
161
194
|
</div>
|
|
162
195
|
));
|
|
163
|
-
delegate(rootEl,
|
|
196
|
+
delegate(rootEl, "click", ACTIONS.inc.selector, () => {
|
|
197
|
+
count.value += 1;
|
|
198
|
+
});
|
|
164
199
|
|
|
165
200
|
// Keyed list with per-item memoization
|
|
166
201
|
mount(listEl, () => (
|
|
167
202
|
<ul>
|
|
168
|
-
{each(rows.value, (row) =>
|
|
203
|
+
{each(rows.value, (row) => (
|
|
204
|
+
<li data-key={row.id}>{row.label}</li>
|
|
205
|
+
))}
|
|
169
206
|
</ul>
|
|
170
207
|
));
|
|
171
208
|
|
|
@@ -173,22 +210,28 @@ mount(listEl, () => (
|
|
|
173
210
|
const cart = defineStore({
|
|
174
211
|
initial: () => ({ items: [] as string[] }),
|
|
175
212
|
actions: (set, get) => ({
|
|
176
|
-
add:
|
|
177
|
-
clear: ()
|
|
213
|
+
add: (id: string) => set({ items: [...get().items, id] }),
|
|
214
|
+
clear: () => set({ items: [] }),
|
|
178
215
|
}),
|
|
179
216
|
});
|
|
180
217
|
|
|
181
218
|
// One-shot reconcile against existing DOM (no signals)
|
|
182
|
-
|
|
219
|
+
// the template describes liveCard's CHILDREN; liveCard's own attributes are untouched
|
|
220
|
+
morph(liveCard, '<h2 class="card-title">…</h2><p>…</p>');
|
|
183
221
|
|
|
184
222
|
// No build step (CDN / importmap): the html tagged template instead of JSX.
|
|
185
223
|
// Same runtime semantics as JSX; write real HTML attribute names; a hole must
|
|
186
224
|
// be a text position or a COMPLETE attribute value (partial values throw).
|
|
187
|
-
import { html } from
|
|
188
|
-
mount(
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
225
|
+
import { html } from "kerfjs/html";
|
|
226
|
+
mount(
|
|
227
|
+
rootEl,
|
|
228
|
+
() => html`
|
|
229
|
+
<div class="${cls}">Count: ${count}</div>
|
|
230
|
+
<ul>
|
|
231
|
+
${each(rows.value, (row) => html`<li data-key="${row.id}">${row.label}</li>`)}
|
|
232
|
+
</ul>
|
|
233
|
+
`,
|
|
234
|
+
);
|
|
192
235
|
|
|
193
236
|
// Fine-grained binding (opt-in): pass the signal/computed ITSELF into a hole
|
|
194
237
|
// so a change updates ONLY that node (no render re-run, no reconcile). For a
|
|
@@ -197,9 +240,15 @@ mount(rootEl, () => html`
|
|
|
197
240
|
const selectedId = signal<number | null>(null);
|
|
198
241
|
mount(listEl, () => (
|
|
199
242
|
<ul>
|
|
200
|
-
{each(
|
|
201
|
-
|
|
202
|
-
|
|
243
|
+
{each(
|
|
244
|
+
rows.value,
|
|
245
|
+
(row) => (
|
|
246
|
+
<li class={computed(() => (row.id === selectedId.value ? "sel" : ""))}>
|
|
247
|
+
{row.label}
|
|
248
|
+
</li>
|
|
249
|
+
),
|
|
250
|
+
(row) => row.id,
|
|
251
|
+
)}
|
|
203
252
|
</ul>
|
|
204
253
|
));
|
|
205
254
|
```
|
|
@@ -215,13 +264,13 @@ mount(listEl, () => (
|
|
|
215
264
|
- SVG renders as broken / namespaceless markup → use `mount` (HTML path) or `toElement` (SVG-aware), not `innerHTML`.
|
|
216
265
|
- Library widget destroyed on every render → wrap host in `data-morph-skip`; mount the library imperatively after first render.
|
|
217
266
|
- `each(): row render at index N produced K top-level elements` → wrap multiple roots in one parent.
|
|
218
|
-
- Drag/drop / state change has no visible effect, only stuff
|
|
219
|
-
- Row-enter CSS animation no longer replays when only a row's
|
|
267
|
+
- 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.
|
|
268
|
+
- 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`).
|
|
220
269
|
- 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.
|
|
221
|
-
-
|
|
270
|
+
- ` 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}`)}"`.
|
|
222
271
|
- An `each()` list's rows lose focus / scroll / typing state when an unrelated conditional list above them appears or disappears (kerf warns in dev) → lists without a key are identified by position among the render's `each()` calls, so adding/removing one above shifts this list's identity and kerf rebuilds it. Give the lists stable keys: `each(items, render, { key: 'results' })`; keying just the conditional list is usually enough.
|
|
223
272
|
- Keyed `each()` list suddenly renders zero rows — only its `<!--kf-list:N-->` marker — with no errors, and it never recovers (kerfjs ≤ 2.0.1) → a conditionally-rendered sibling BEFORE the list (possibly higher in the tree, e.g. an error banner) was removed that render; older kerfjs rebuilt the shifted list container from the template, permanently detaching the list's internal binding. Upgrade kerfjs (fixed after 2.0.1 — the morph now moves the shifted container up in place, keeping node identity). On older versions, keep the structure before the list stable: wrap the conditional in an always-present container (`<div class="banners">{cond ? <div/> : ''}</div>`).
|
|
224
|
-
- A numbered / zebra-striped / "N of M" `each()` list shows the wrong number on rows that MOVED (reorder, or non-tail insert/remove), while unmoved rows look right → the render fn's `index` argument is NOT part of the memo key (only item identity + `cacheKey` + content version are), so a row that keeps identity but changes position keeps HTML rendered at its old index. Fold the index into the memo key: `each(items, (it, i) => …, { cacheKey: (_, i) => i })` (add `key` if used). Opt-in dev warn: `KERF_DEV_WARN_STALE_INDEX=1
|
|
273
|
+
- A numbered / zebra-striped / "N of M" `each()` list shows the wrong number on rows that MOVED (reorder, or non-tail insert/remove), while unmoved rows look right → the render fn's `index` argument is NOT part of the memo key (only item identity + `cacheKey` + content version are), so a row that keeps identity but changes position keeps HTML rendered at its old index. Fold the index into the memo key: `each(items, (it, i) => …, { cacheKey: (_, i) => i })` (add `key` if used). Opt-in dev warn: `enableWarnings({ staleIndex: true })` (or `KERF_DEV_WARN_STALE_INDEX=1` under Node).
|
|
225
274
|
|
|
226
275
|
## Server / SSR
|
|
227
276
|
|