kerfjs 4.2.0-beta.8 → 4.3.0-beta.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 +13 -0
- package/README.md +30 -3
- package/ai/manifest.json +1 -1
- package/dist/{chunk-LKWAKC2X.js → chunk-SRWQKB33.js} +18 -8
- package/dist/chunk-SRWQKB33.js.map +1 -0
- package/dist/index.js +1 -1
- package/dist/list.d.ts +35 -0
- package/dist/list.js +26 -13
- package/dist/list.js.map +1 -1
- package/dist/overlay.d.ts +35 -0
- package/dist/overlay.js +63 -15
- package/dist/overlay.js.map +1 -1
- package/dist/remount.js +1 -1
- package/dist/scope.js +1 -1
- package/llms.txt +1 -0
- package/package.json +2 -2
- package/dist/chunk-LKWAKC2X.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,19 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
- **`kerfjs/overlay` gains opt-in native top-layer backing (`native: true`).** Every overlay surface (`overlay`, `confirm`, `prompt`, `form`, `choice`, `popover`, `tooltip`) now takes `native?: boolean` (default `false`). When `true` and the engine supports it, a **modal** surface (`trap: true`) is hosted in a `<dialog>` opened with `.showModal()` — real document inerting (pointer + focus + AT) and guaranteed stacking above any `z-index` — and a **non-modal** surface (`trap: false`) uses the **Popover API** (`[popover]` + `showPopover()`). Feature-detected (`HTMLDialogElement.prototype.showModal`, `HTMLElement.prototype.showPopover`), falling back to today's plain `<div>` where unsupported, so `native: true` is always safe to pass. The `render` slot + promise API are unchanged — kerf just hosts your markup in a `<dialog>` / `[popover]`. **Opt-in on purpose:** the native elements carry UA default styles (a `::backdrop`, centering, border, padding) that kerf does **not** reset (a reset would violate the zero-CSS contract) — style the element and its `::backdrop` via `className` (`.kerf-overlay::backdrop { … }`, the stable contract); and `container` becomes a visual no-op in native mode (the top layer ignores DOM position). See [`docs/19-native-overlay-backing.md`](docs/19-native-overlay-backing.md).
|
|
10
|
+
- **`bindList` virtualization gains a `content-visibility` mode.** `virtualize: { rowHeight, mode: 'content-visibility' }` is a second virtualization strategy alongside the default `mode: 'window'` (today's JS windowing). It keeps **every** row in the DOM and sets `content-visibility: auto` + `contain-intrinsic-size: 0 <rowHeight>px` on each row, so a supporting engine (Chromium, Safari 18) skips the *layout/paint* of off-screen rows while **all rows stay findable** — find-in-page (Cmd/Ctrl+F), the accessibility tree, and anchor links / `scrollIntoView` all work on any row (the exact guarantee `mode: 'window'` can't give, since it removes off-window rows from the DOM). The `mode` choice is the app's and it's about list size: pick `'content-visibility'` for medium lists where findability beats the node ceiling, keep `'window'` for very large (100k-row) lists. In this mode `rowHeight` is only the `contain-intrinsic-size` placeholder (no windowing math), `setHeight` / `observeRowHeights` are no-ops (the browser owns measurement), `minRows` is ignored (all rows already render), and no scroll listener / `ResizeObserver` is installed — while `handle.container` / `containerClass` / `containerId` still work. There is deliberately **no feature detection**: on an engine without `content-visibility` the CSS is inert, so all rows still render (correct, still findable) — only the off-screen-skip optimization is absent. See [`docs/17-list-virtualization.md`](docs/17-list-virtualization.md) §17.11.
|
|
11
|
+
- **Docs: `bindList` virtualization findability/a11y tradeoff is now a first-class caveat.** Off-window rows are removed from the DOM (not just hidden), so with `virtualize` set, find-in-page (Cmd/Ctrl+F), screen readers / the accessibility tree, and anchor links / `scrollIntoView` reach only the visible window. New `docs/17-list-virtualization.md` §17.10 spells out the consequences and the guidance (don't virtualize, or use `minRows` above the list length, when full findability matters more than the DOM node ceiling), and the `bindList` JSDoc + `docs/8-api-reference.md` §8.11 carry the same note. Behavior unchanged — documentation only.
|
|
12
|
+
- **State-preserving row moves via `moveBefore()` (transparent optimization).** When a keyed list reorders — `each()` (snapshot and granular paths), `bindList`, and `morph()`'s keyed / positional / list-marker moves — kerf now relocates an already-connected row with `Node.prototype.moveBefore()` where the engine supports it (Chromium 133+, spreading to other engines), falling back to `insertBefore()` everywhere else. `moveBefore()` is an atomic move: the node is never disconnected, so a moved row keeps its focus, text selection, `<iframe>` document state, playing media, running CSS transitions/animations, and open `popover`/`dialog` state across the reorder — richer state than the existing focus snapshot in the reconciler could ever restore, and it needs no snapshot at all where it runs. No API change and no behavior change on engines without `moveBefore()`; the focus-preservation snapshot stays in place for them. Fresh (not-yet-connected) rows still use `insertBefore()` — only genuine moves of connected rows take the new path. See [`docs/18-state-preserving-moves.md`](docs/18-state-preserving-moves.md).
|
|
13
|
+
|
|
14
|
+
## [4.2.0] - 2026-08-20
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
- Added a **Virtual list** example app — a 10,000-row virtualized list showcasing the companion subpaths together: `kerfjs/list` viewport virtualization (only a screenful in the DOM), `kerfjs/timing` debounced search, and `kerfjs/overlay` confirm-to-delete with a toast. Includes a live "in the DOM" counter that stays flat as you scroll all 10,000 rows.
|
|
19
|
+
- Expanded the README with a companion-subpaths spotlight covering `list`, `overlay`, `async`, `scope`, `timing`, `remount`, `attach`, and `actions`, with `bindList` fixed and measured-height virtualization examples.
|
|
20
|
+
- Corrected the complete-apps index to accurately describe the eight standalone showcase apps and note that the cart and counter-store apps live in the migration guides.
|
|
21
|
+
|
|
9
22
|
- Fixed (beta): `bindList` measured-height virtualization (`virtualize: { rowHeight: { estimate } }`) never pruned reported heights, so a list with key churn (a feed prepending new ids over a long session) grew its internal height map without bound. Reported heights are now pruned to the live key set on each rebuild — no leak, and a key that leaves and later returns is re-measured (uses the estimate again) rather than reusing a stale height. A key that only scrolls out of the window keeps its measurement (it's still in the source).
|
|
10
23
|
- Fixed (beta regression): `popover()` / `tooltip()` (and `positionAnchored` / `autoReposition`) mispositioned horizontally — the anchored element was measured while still `display:block`, so its width read as the full body-content width and the viewport clamp slid it to the body's left edge instead of aligning it to the anchor. `positionAnchored` now sets `position: fixed` before measuring, so it uses the element's real (shrink-to-fit) size.
|
|
11
24
|
- **`renderDocument(node, options?)`** (main barrel) — a tiny SSR helper that prepends the doctype to a rendered document, so server routes stop reinventing `"<!DOCTYPE html>" + page.toString()`. Takes a `SafeHtml` or string; optional `{ doctype }` (default `'html'`). Pure string work, no DOM dependency.
|
package/README.md
CHANGED
|
@@ -51,11 +51,13 @@ Here's the whole development loop — write a component, run the dev server, cli
|
|
|
51
51
|
|
|
52
52
|
6. **JSX typed against HTML, not against React.** Tags and attributes are checked at compile time — `<diiv>` and `<input typo />` don't build. The attribute types are derived from the HTML standard rather than another framework's property table, and that distinction has teeth: `draggable` and `spellcheck` are *enumerated* attributes that take the strings `"true"` / `"false"`, so kerf rejects `draggable={true}` rather than quietly emitting markup that means the opposite. Custom elements and web components slot in with one declaration merge.
|
|
53
53
|
|
|
54
|
-
7. **Small public API.** ~
|
|
54
|
+
7. **Small public API.** ~18 exports from the main barrel (plus `arraySignal`, the `html` tagged template, and the companion-utility subpaths below — each opt-in, none in the core). No hooks, no lifecycle, no per-instance state. Components are plain functions that return JSX.
|
|
55
55
|
|
|
56
|
-
8. **
|
|
56
|
+
8. **Batteries on their own subpaths.** Nine optional, tree-shakeable subpaths cover the patterns every real app otherwise hand-rolls — **`kerfjs/list`** (a keyed list with per-row fine-grained mounts and fixed / app-declared / measured-height viewport **virtualization**), **`kerfjs/overlay`** (modals, `confirm` / `prompt` / `form` / `choice`, anchored popovers + tooltips, toasts), **`kerfjs/async`** (`resource` async-state with a built-in stale-response guard + SWR cache), **`kerfjs/scope`** (dispose-scopes that tie teardown to a DOM node's lifetime), plus `timing`, `remount`, `attach`, and `actions`. None of them grows the ~12 KB core until you import it.
|
|
57
57
|
|
|
58
|
-
9. **
|
|
58
|
+
9. **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.
|
|
59
|
+
|
|
60
|
+
10. **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 ~12 KB.
|
|
59
61
|
|
|
60
62
|
## When to use Kerf
|
|
61
63
|
|
|
@@ -202,6 +204,31 @@ Same algorithm `mount()` uses internally — `data-morph-skip`, `data-morph-skip
|
|
|
202
204
|
|
|
203
205
|
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 (`@4`, as shown — the latest `4.x`) rather than floating on `latest`, or an exact version (`@4.1.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.
|
|
204
206
|
|
|
207
|
+
### Batteries when you need them: the companion subpaths
|
|
208
|
+
|
|
209
|
+
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`), teardown tied to a DOM node's lifetime (`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:
|
|
210
|
+
|
|
211
|
+
```ts
|
|
212
|
+
import { bindList, observeRowHeights } from 'kerfjs/list';
|
|
213
|
+
|
|
214
|
+
// Fixed-height windowing: only the visible rows render.
|
|
215
|
+
bindList(scrollEl, rows, {
|
|
216
|
+
key: (r) => r.id,
|
|
217
|
+
render: (r) => <div class="row">{r.label}</div>,
|
|
218
|
+
virtualize: { rowHeight: 32 },
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// Measured heights (chat, feeds): kerf estimates, you report the real height.
|
|
222
|
+
const list = bindList(scrollEl, messages, {
|
|
223
|
+
key: (m) => m.id,
|
|
224
|
+
render: (m) => <div class="msg">{m.text}</div>,
|
|
225
|
+
virtualize: { rowHeight: { estimate: 64 } },
|
|
226
|
+
});
|
|
227
|
+
observeRowHeights(list); // one ResizeObserver → kerf anchor-corrects scroll
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
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`, `overlay`, `scope`, `async`, `timing`, `remount`, `attach`, `actions`).
|
|
231
|
+
|
|
205
232
|
## Install
|
|
206
233
|
|
|
207
234
|
```bash
|
package/ai/manifest.json
CHANGED
|
@@ -240,6 +240,16 @@ function eachSnapshotById(items, render, cacheKey, id, source) {
|
|
|
240
240
|
return listSafeHtml(id, segItems, source);
|
|
241
241
|
}
|
|
242
242
|
|
|
243
|
+
// src/utils/moveNode.ts
|
|
244
|
+
function moveNode(parent, node, ref) {
|
|
245
|
+
const move = parent.moveBefore;
|
|
246
|
+
if (move !== void 0 && node.isConnected) {
|
|
247
|
+
move.call(parent, node, ref);
|
|
248
|
+
} else {
|
|
249
|
+
parent.insertBefore(node, ref);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
243
253
|
// src/list-reconcile-focus.ts
|
|
244
254
|
function captureFocus(liveParent) {
|
|
245
255
|
const active = document.activeElement;
|
|
@@ -355,7 +365,7 @@ function morphChildren(fromParent, toParent, ownedItems) {
|
|
|
355
365
|
matched = keyed.get(toKey);
|
|
356
366
|
keyed.delete(toKey);
|
|
357
367
|
if (matched !== fromChild) {
|
|
358
|
-
fromParent
|
|
368
|
+
moveNode(fromParent, matched, fromChild);
|
|
359
369
|
} else {
|
|
360
370
|
fromChild = skipOwned(fromChild.nextSibling, ownedItems);
|
|
361
371
|
}
|
|
@@ -382,7 +392,7 @@ function morphChildren(fromParent, toParent, ownedItems) {
|
|
|
382
392
|
if (el.tagName !== toTag || getNodeKey(el) !== void 0) continue;
|
|
383
393
|
if (protectionTag(el) !== protectionTag(toChild)) continue;
|
|
384
394
|
matched = el;
|
|
385
|
-
fromParent
|
|
395
|
+
moveNode(fromParent, el, fromChild);
|
|
386
396
|
break;
|
|
387
397
|
}
|
|
388
398
|
}
|
|
@@ -396,7 +406,7 @@ function morphChildren(fromParent, toParent, ownedItems) {
|
|
|
396
406
|
run.push(r);
|
|
397
407
|
}
|
|
398
408
|
const focusSnap = captureFocus(fromParent);
|
|
399
|
-
for (const node of run) fromParent
|
|
409
|
+
for (const node of run) moveNode(fromParent, node, fromChild);
|
|
400
410
|
if (focusSnap !== null) restoreFocus(focusSnap);
|
|
401
411
|
matched = scan;
|
|
402
412
|
break;
|
|
@@ -755,7 +765,7 @@ function reconcileGranular(binding, patches) {
|
|
|
755
765
|
let anchorIdx = patch.to;
|
|
756
766
|
if (patch.from < patch.to) anchorIdx += 1;
|
|
757
767
|
const anchor = anchorIdx < items.length ? items[anchorIdx].node : endAnchor(binding);
|
|
758
|
-
liveParent
|
|
768
|
+
moveNode(liveParent, moved.node, anchor);
|
|
759
769
|
items.splice(patch.from, 1);
|
|
760
770
|
items.splice(patch.to, 0, moved);
|
|
761
771
|
i += 1;
|
|
@@ -1053,7 +1063,7 @@ function applyMoves(liveParent, newRecord, prevIdx, stable, tailAnchor) {
|
|
|
1053
1063
|
for (let i = newRecord.length - 1; i >= 0; i--) {
|
|
1054
1064
|
const node = newRecord[i].node;
|
|
1055
1065
|
if (prevIdx[i] === -1 || !stable.has(i)) {
|
|
1056
|
-
liveParent
|
|
1066
|
+
moveNode(liveParent, node, nextSibling);
|
|
1057
1067
|
}
|
|
1058
1068
|
nextSibling = node;
|
|
1059
1069
|
}
|
|
@@ -1390,6 +1400,6 @@ function collectComments(node, out) {
|
|
|
1390
1400
|
}
|
|
1391
1401
|
}
|
|
1392
1402
|
|
|
1393
|
-
export { each, morph, mount };
|
|
1394
|
-
//# sourceMappingURL=chunk-
|
|
1395
|
-
//# sourceMappingURL=chunk-
|
|
1403
|
+
export { each, morph, mount, moveNode };
|
|
1404
|
+
//# sourceMappingURL=chunk-SRWQKB33.js.map
|
|
1405
|
+
//# sourceMappingURL=chunk-SRWQKB33.js.map
|