kerfjs 4.2.0 → 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 CHANGED
@@ -6,6 +6,11 @@ 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
+
9
14
  ## [4.2.0] - 2026-08-20
10
15
 
11
16
 
@@ -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.insertBefore(matched, fromChild);
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.insertBefore(el, fromChild);
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.insertBefore(node, fromChild);
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.insertBefore(moved.node, anchor);
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.insertBefore(node, nextSibling);
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-LKWAKC2X.js.map
1395
- //# sourceMappingURL=chunk-LKWAKC2X.js.map
1403
+ export { each, morph, mount, moveNode };
1404
+ //# sourceMappingURL=chunk-SRWQKB33.js.map
1405
+ //# sourceMappingURL=chunk-SRWQKB33.js.map