kerfjs 4.2.0 → 4.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/README.md +1 -1
- 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 +180 -122
- package/dist/overlay.js +252 -202
- package/dist/overlay.js.map +1 -1
- package/dist/remount.js +1 -1
- package/dist/scope.js +1 -1
- package/llms.txt +4 -2
- package/package.json +1 -1
- 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
|
+
## [4.3.0] - 2026-08-22
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
- KF-529: refresh README for the 4.3 cycle (prep-major-release) (`909c82a`)
|
|
14
|
+
- KF-530: extract promise-dialog helpers into overlay-dialogs.ts (`5d4ac38`)
|
|
15
|
+
- KF-528: sync llms.txt with docs/19 + 4.3 features (`1d9c004`)
|
|
16
|
+
|
|
17
|
+
- **`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).
|
|
18
|
+
- **`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.
|
|
19
|
+
- **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.
|
|
20
|
+
- **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).
|
|
21
|
+
|
|
9
22
|
## [4.2.0] - 2026-08-20
|
|
10
23
|
|
|
11
24
|
|
package/README.md
CHANGED
|
@@ -53,7 +53,7 @@ Here's the whole development loop — write a component, run the dev server, cli
|
|
|
53
53
|
|
|
54
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. **Batteries on their own subpaths.**
|
|
56
|
+
8. **Batteries on their own subpaths.** Eight 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**, plus a `content-visibility` mode that keeps every row find-in-page-able), **`kerfjs/overlay`** (modals, `confirm` / `prompt` / `form` / `choice`, anchored popovers + tooltips, toasts — with opt-in native **top-layer** backing that stacks above any `z-index`), **`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
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
59
|
|
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
|