kerfjs 5.0.0-beta.17 → 5.0.0-beta.19
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 +60 -107
- package/README.md +43 -23
- package/ai/cursorrules +84 -47
- package/ai/manifest.json +5 -5
- package/ai/skill.md +95 -63
- package/dist/actions.js +2 -2
- 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 +3 -1
- package/dist/attach.js.map +1 -1
- package/dist/{chunk-QIP723L4.js → chunk-5WRGJZV6.js} +3 -2
- package/dist/chunk-5WRGJZV6.js.map +1 -0
- package/dist/{chunk-GY4XV2UV.js → chunk-BDX3R4OM.js} +3 -2
- package/dist/chunk-BDX3R4OM.js.map +1 -0
- package/dist/{chunk-HW7KSM2Y.js → chunk-CEQMZYLR.js} +1 -1
- package/dist/{chunk-HW7KSM2Y.js.map → chunk-CEQMZYLR.js.map} +1 -1
- package/dist/{chunk-KPXIOG2C.js → chunk-E5R5GNKE.js} +14 -4
- package/dist/chunk-E5R5GNKE.js.map +1 -0
- package/dist/{chunk-LVH3GC6B.js → chunk-ELXVRKY2.js} +3 -3
- package/dist/chunk-ELXVRKY2.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-ZDCJZCNO.js → chunk-QFUNWHKH.js} +36 -15
- package/dist/chunk-QFUNWHKH.js.map +1 -0
- package/dist/{chunk-SVATPF5R.js → chunk-V757BT6U.js} +121 -49
- package/dist/chunk-V757BT6U.js.map +1 -0
- package/dist/{chunk-V46JKE44.js → chunk-WKIPLNVO.js} +1 -1
- package/dist/chunk-WKIPLNVO.js.map +1 -0
- package/dist/{chunk-U6FK33SG.js → chunk-Y2FOYPBV.js} +2 -2
- package/dist/{chunk-U6FK33SG.js.map → chunk-Y2FOYPBV.js.map} +1 -1
- package/dist/{chunk-KZJXHFIB.js → chunk-ZOIERTUW.js} +3 -3
- package/dist/chunk-ZOIERTUW.js.map +1 -0
- package/dist/dev.js +64 -17
- package/dist/dev.js.map +1 -1
- package/dist/html.js +5 -4
- package/dist/html.js.map +1 -1
- package/dist/index.js +23 -14
- package/dist/index.js.map +1 -1
- package/dist/jsx-runtime.js +1 -1
- package/dist/list.js +43 -14
- package/dist/list.js.map +1 -1
- package/dist/overlay.js +99 -32
- package/dist/overlay.js.map +1 -1
- package/dist/remount.js +2 -2
- package/dist/router.js +33 -10
- package/dist/router.js.map +1 -1
- package/dist/scope.js +5 -4
- package/dist/scope.js.map +1 -1
- package/dist/testing.js +1 -1
- package/dist/timing.js +1 -1
- package/dist/timing.js.map +1 -1
- package/llms.txt +6 -2
- package/package.json +24 -4
- 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.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-UZJ6I4T6.js.map +0 -1
- package/dist/chunk-V46JKE44.js.map +0 -1
- package/dist/chunk-ZDCJZCNO.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,40 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
- Added `npx kerfjs setup` and the `kerfjs/setup` automation API for safe,
|
|
10
|
+
AI-first initialization and migration of core and UI projects. The command
|
|
11
|
+
provides bounded dry-run diffs, explicit per-conflict keep/Kerf choices,
|
|
12
|
+
package-scoped hash-backed upgrades, monorepo selection, transactional
|
|
13
|
+
rollback, shipped guidance, strict TypeScript/ESLint, and UI catalog/profile
|
|
14
|
+
and doctor discovery. Empty application component metadata now emits a valid
|
|
15
|
+
empty v2 catalog. Existing JSONC TypeScript configuration is structurally
|
|
16
|
+
merged without discarding comments, trailing commas, authored fields, or line
|
|
17
|
+
endings; malformed roots and compiler-option containers require an explicit
|
|
18
|
+
keep/Kerf decision. Workspace selection now evaluates ordered globstar,
|
|
19
|
+
brace, exclusion, and re-inclusion patterns deterministically across npm,
|
|
20
|
+
Yarn, and pnpm declarations, rejects path/symlink/name ambiguity, and uses
|
|
21
|
+
manager-correct offline commands with explicit Yarn Classic/Berry detection
|
|
22
|
+
and network-disabled Berry installs.
|
|
23
|
+
- Added `@kerfjs/ui/doctor` and the `kerf-ui-doctor` command: a versioned, redacted repair-loop report that merges catalog/profile validation, TypeScript, Kerf UI ESLint, static analysis, and opt-in browser evaluation with changed/full modes, monorepo selection, reasoned suppressions, caching, and deterministic exits.
|
|
24
|
+
- Added the public Playwright-backed `kerf-ui-evaluate` CLI and `@kerfjs/ui/evaluator` Node API. It resolves application profiles and composition catalogs, exercises wide/intermediate/narrow/200%-zoom, light/dark, and reduced-motion contexts across Chromium, Firefox, and WebKit, and emits versioned repair-oriented `KUI-B###` diagnostics plus focused DOM/style evidence and hashed screenshots. Subjective visual quality remains an explicitly separate unrecorded reviewer rubric.
|
|
25
|
+
- Added `kerf-ui-analyze`, a profile- and catalog-aware static evaluator for Kerf UI integrations. It resolves directory policy per source, follows project-local CSS import graphs without cross-package class leakage, and evaluates shared stylesheets against every consuming source policy before deduplicating findings. Stable portable text, JSON, or SARIF diagnostics cover private selectors, unknown tokens, competing geometry/scroll owners, repeated insets, forced dimensions, off-scale spacing, and dynamic-class review, with exact scoped profile exceptions, a public report schema, and configurable CI review failure.
|
|
26
|
+
- Added catalog/profile-aware `recommended-ui` and `strict-ui` flat presets to `eslint-plugin-kerfjs`. Toolbar zone acceptance/cardinality and valid wiring imports come from the shipped catalogs; intrinsic or unknown zone children, root/subpath/namespace wiring, and the non-colliding `KUI-L090` load diagnostic are covered by real packed downstream installs on every supported ESLint major.
|
|
27
|
+
- **Breaking (`@kerfjs/ui` TypeScript declarations):** strengthened invalid-integration contracts for ListHeader modes, Select naming and literal choice identity, SegmentedControl/TabScaffold literal identity, collapsible TokenSearchField props, protected editor metadata, PanelHeader summary ids, ToolbarText line caps, and token-removal callbacks. Added named public unions for previously anonymous finite variants plus source and packed-tarball positive/negative compilation gates. See `ui/docs/type-contracts.md` for the compatibility review and dynamic-data widening syntax.
|
|
28
|
+
- `create-kerf-component` now scaffolds explicit `kerf.components.json` AI metadata and a deterministic `kerf-component-catalog` generator/checker. Generated v2 entries carry package identity, purpose, verified named exports, composition and geometry ownership, tokens, accessibility, and source links; publishing rejects missing author decisions, schema-invalid source/output fields, deleted sources, renamed exports, duplicate ids, or output drift, including multi-package workspaces. Syntax-aware TypeScript/TSX export discovery prevents JSX text, nested scopes, comments, and literals from impersonating public exports.
|
|
29
|
+
- Added a versioned application UI profile for AI tools and static evaluators. Shipped package defaults, schema, TypeScript types, Node discovery/merge/validation APIs, and a workspace example now make catalog locations, preferred components/recipes, theme/density policy, semantic token overrides, responsive/layout conventions, and narrow source-located exceptions explicit. Composition-only generated consumer catalogs load without a fabricated v1 selection artifact, while Kerf retains its required v1 selection catalog. Deterministic package → workspace → parent-to-child directory precedence validates every raw layer before merge, attributes resolved diagnostics through field provenance, and rejects stale catalogs, unknown components/tokens/rules, conflicts, malformed value shapes, and broad exemptions.
|
|
30
|
+
- Fixed `CatalogExampleStack` accessible labels: the helper now renders a semantic `section`, so a supplied `label` exposes the documented named region to assistive technology while an unlabeled stack remains an ordinary grouping.
|
|
31
|
+
- Added first-class `Pane` and `SunkenPanel` primitives to `@kerfjs/ui`. `Pane` standardizes semantic header/content/footer columns, explicit logical-edge separators, and one content scroll owner; `SunkenPanel` supplies one lowered surface with an 8px inset and vertical rhythm. The Catalog shell now dogfoods `Pane`, and both components ship focused demos, catalog metadata, documentation, and three-engine browser coverage.
|
|
32
|
+
- Added the package-qualified component catalog v2 alongside the compatible v1 artifact. Its schema and generated metadata formalize child zones, cardinality, exclusivity, wiring, state, responsive behavior, layout and accessibility boundaries, stable validation diagnostics, provenance, and consumer extension catalogs, so AI tools can join Kerf and application components without guessing composition contracts.
|
|
33
|
+
- Added an AST-based Catalog demo conformance gate. It checks every first-party demo for public helper usage, package imports, focused specimen structure, geometry-overlay metadata, and reviewed schema-checked exceptions, with adversarial fixtures and authoring guidance that makes the same contract available to downstream AI tools.
|
|
34
|
+
- Made `ui/docs/catalog.md` the authoritative Catalog demo authoring contract, covering focused versus composition previews, public row/group nesting, exact specimen selection, conditional geometry overlays, skip-marker behavior, metadata ownership, and an explicit overlay legend. A shipped `ai/catalog-authoring.json` discovery artifact now routes AI tools to that contract and exact signatures, with schema, link-integrity, and browser-consumer fixture coverage.
|
|
35
|
+
- Extended `CatalogExample` and `CatalogExampleStack` with typed, runtime-filtered `rootAttributes` for catalog-authoring `data-*` metadata such as `data-demo` and `data-catalog-geometry-overlay-skip`, while preserving helper-owned structural and alignment markers. The UX catalog now composes its example stacks through these public helpers instead of copying private `kui-catalog-*` markup.
|
|
36
|
+
- Added `revealCatalogEntry` and opt-in `wireCatalog({ revealSelection })` support to the exported Catalog tool. Sidebar or related-entry selection can now reveal the matching row after the controlled render settles, using a desktop-safe media guard, exact id matching, configurable scroll alignment/behavior, focus-preserving scrolling, and cancellation of stale rapid selections; the Kerf UX demo now uses the public behavior.
|
|
37
|
+
- Added a reusable consumer component-catalog extension schema and checked app-component example. Downstream packages can now publish the same selection facts and margin/border/padding ownership vocabulary as Kerf, and the README, component contract, Catalog guide, `llms.txt`, and AI skill explain how to combine package-qualified app entries with Kerf's shipped catalog for alignment decisions.
|
|
38
|
+
- Added an opt-in geometry inspector to the exported `@kerfjs/ui/catalog` tool. `geometryOverlay` plus `wireCatalogGeometryOverlay` now marks transparent component bounds with a dashed outline and positive intrinsic margins with devtools-style bands, excludes example labels/alignment scaffolding, stays synchronized across preview changes, and replaces the Kerf UX demo's private implementation.
|
|
39
|
+
- Added machine-readable margin, border, and padding ownership to every visual entry in the `@kerfjs/ui` component catalog. The additive schema distinguishes self-, parent-, child-, conditional-, and unowned geometry, and enforces complete metadata plus explanatory notes for conditional cases.
|
|
40
|
+
- Refreshed the standalone reactivity demo lockfile to Vite 6.4.3, PostCSS 8.5.28, and nanoid 3.3.19, clearing its development-toolchain audit advisories.
|
|
41
|
+
- Overrode the development toolchain's transitive esbuild dependency to 0.28.2, resolving GHSA-g7r4-m6w7-qqqr while current tsup still constrains its declared range to 0.27.x.
|
|
42
|
+
- Added repository-wide Prettier formatting for TypeScript, JavaScript, Astro, shell, CSS, HTML, Markdown, YAML, and JSON source and structured-content files. Root and `@kerfjs/ui` lint gates now reject formatting drift, while generated, binary, and parser-incompatible fixtures remain explicitly excluded.
|
|
9
43
|
- Added reusable sidebar tags to `@kerfjs/ui` Catalog entries and marked the 15 Web Awesome components superseded by preferred Kerf patterns or reserved for exceptional cases as `Discouraged` in the UX catalog. The tag is deterministically projected from the canonical recommendation metadata; Popup remains an available conditional positioning primitive.
|
|
10
44
|
- Aligned Web Awesome Accordion, Card, Details, Callout, and Include surfaces with Kerf content-item geometry: each complete surface now has an overridable 8px logical inline margin and 8px inner padding, Accordion keeps connected items under one group margin, and Card's header/body/footer share the full inset. Tab Panel intentionally retains the roomier 16px container inset.
|
|
11
45
|
- Fixed `@kerfjs/ui` Web Awesome Slider geometry: its complete interactive region now receives the shared 8px logical inline outer inset without shifting the already-aligned label.
|
|
@@ -155,8 +189,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
155
189
|
|
|
156
190
|
## [4.4.1] - 2026-08-26
|
|
157
191
|
|
|
158
|
-
|
|
159
|
-
|
|
160
192
|
- Overhauled the README landing page: new one-line tagline, npm/size/license/TypeScript-types badges, a hoisted "Quick start" (install + `tsconfig`) right under the hook, and a jump nav (Quick start · Why kerf · Quick tour · Docs). The counter example now shows `delegate()` wiring the click handler.
|
|
161
193
|
- Reworked the homepage hero to lead with the value proposition ("Reactive UI that touches only the bytes that changed"), with "Introducing Kerf" demoted to an eyebrow, a right-sized logo, tighter vertical spacing, and "Get started" as the single filled primary action ("View examples" now a secondary/outline button).
|
|
162
194
|
- Redesigned the complete-apps index as a card grid — each app appears once with an animated preview, a one-line summary, and "Run live" / "Source" buttons.
|
|
@@ -164,7 +196,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
164
196
|
- Rewrote the site's docs pages as hand-authored consumer content (cleaner headings, no internal section numbers) rather than verbatim copies of the internal design docs.
|
|
165
197
|
- Renamed the migration guides' "Side-by-side code" heading to "Section by section" (the sections stack code blocks rather than showing true columns), across all framework pages.
|
|
166
198
|
|
|
167
|
-
|
|
168
199
|
- The getting-started and Markdown-editor demo animations now open on a non-blank frame — real code and a rendered heading are visible immediately instead of an empty pane.
|
|
169
200
|
- Added a horizontal-scroll shadow affordance to wide tables so off-screen columns are discoverable on narrow viewports.
|
|
170
201
|
- Shortened the longest demo alt text on the router and virtual-list pages for more concise screen-reader output.
|
|
@@ -172,29 +203,23 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
172
203
|
|
|
173
204
|
## [4.4.0] - 2026-08-23
|
|
174
205
|
|
|
175
|
-
|
|
176
|
-
|
|
177
206
|
- Reworked the router example app to run inside a fake browser window — traffic-light chrome, working Back/Forward buttons wired to `router.back()`/`forward()` via one delegated listener, and a live address bar bound to `router.route` that updates as you navigate — making the URL-driven, no-reload story clearer.
|
|
178
207
|
|
|
179
208
|
- **New `kerfjs/router` subpath — a client-side router (the "postcard router").** `createRouter({ routes, mode?, base?, interceptLinks? })` returns a handle over three things kerf already has: a reactive `route` signal (`{ path, params, query, hash }`), `delegate()`-based `<a href>` link interception, and a keyed **outlet** — `router.outlet()` renders the matched route in a `data-key`ed wrapper, so kerf's keyed morph **replaces the page wholesale on a route change** (fresh DOM) and **reconciles in place on a same-route param change** (preserving scroll / focus). Route patterns are static, `:param`, a trailing `*rest` wildcard, and `*` catch-all; the handle also gives `navigate(path, { replace?, state? })`, `back()`/`forward()`, `match(pattern)` / `activeClass(pattern, className)` reactive active-link helpers, hash **or** history mode, an optional base path, and `dispose()`. Link interception is automatic (same-origin, left-click, no modifier/`target`/`download`, opt out per-link with `data-router-ignore` / `rel="external"` or globally with `interceptLinks: false`). **Deliberately scoped** — no nested layouts, data loaders, lazy routes, guards, or SSR matching; compose those with kerf primitives (`resource` for loading, an `effect` on `route` for guards). The kerf **core stays router-free** — this is opt-in and tree-shakeable, adding nothing to the main barrel until imported, and docs/1's "Not a router" is about the runtime. See [`docs/20-router.md`](docs/20-router.md).
|
|
180
209
|
|
|
181
210
|
## [4.3.0] - 2026-08-22
|
|
182
211
|
|
|
183
|
-
|
|
184
|
-
|
|
185
212
|
- KF-529: refresh README for the 4.3 cycle (prep-major-release) (`909c82a`)
|
|
186
213
|
- KF-530: extract promise-dialog helpers into overlay-dialogs.ts (`5d4ac38`)
|
|
187
214
|
- KF-528: sync llms.txt with docs/19 + 4.3 features (`1d9c004`)
|
|
188
215
|
|
|
189
216
|
- **`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).
|
|
190
|
-
- **`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
|
|
217
|
+
- **`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.
|
|
191
218
|
- **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.
|
|
192
219
|
- **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).
|
|
193
220
|
|
|
194
221
|
## [4.2.0] - 2026-08-20
|
|
195
222
|
|
|
196
|
-
|
|
197
|
-
|
|
198
223
|
- 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.
|
|
199
224
|
- 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.
|
|
200
225
|
- 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.
|
|
@@ -208,86 +233,70 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
208
233
|
- **New `kerfjs/remount` subpath** — `remountOn(parent, key, render)`, the opposite of kerf's morph-by-default: **replace** a subtree wholesale when `key` changes instead of morphing it. Names the hand-rolled `data-key={`gen-${n}`}` + `data-morph-skip` counter trick, for library-owned subtrees (a highlighted diff, a chart, an editor) that must tear down and re-initialize on fresh DOM. `key` is a signal or a thunk `() => K`; an unchanged key (including a thunk whose inputs moved but whose value stayed equal) leaves the subtree alone, so per-row reactivity inside `render` still updates in place. An optional `onMount(root)` callback runs after each (re)mount with the live subtree — the place to bind an imperative widget (`kerfjs/attach`) to the fresh DOM; returning `attach`'s disposer from it makes teardown synchronous. `remountOn` owns `parent`'s children and returns a disposer. Optional and tree-shakeable.
|
|
209
234
|
- **New `kerfjs/timing` subpath** — the `let timer; clearTimeout(timer); timer = setTimeout(…)` pattern every app hand-rolls, blessed with disposer-shaped ergonomics. `debounce(fn, ms)` is trailing-edge (runs once `ms` after calls stop, latest args); `throttle(fn, ms)` is leading-plus-trailing (fires immediately, then at most once per `ms`, collapsing a burst to one trailing call). Both return a callable with `cancel()` / `flush()`. `debouncedSignal(source, ms)` is a read-only signal that trails `source` by `ms` so it composes inside the reactive graph (`computed`/`effect`/`mount`) instead of beside it. `debounce`/`throttle` are dependency-free; `debouncedSignal` pulls in signals only (no render core). Optional and tree-shakeable; tiny.
|
|
210
235
|
- **New `kerfjs/scope` subpath** — tie disposers to a DOM element's lifetime, so append-heavy UIs stop leaking detached-but-subscribed effects/listeners. `disposeScope(el)` returns a WeakMap-keyed, accumulating scope whose `add(disposer)` (plus convenience `mount` / `effect` / `delegate` wrappers that register their own disposer) collects teardown; `dispose()` runs it all best-effort and idempotently. `disposeSubtree(root)` sweeps a subtree before removal; `observeRemovals(root)` installs one `MutationObserver` that auto-disposes on removal. No module-level mutable state. Optional and tree-shakeable.
|
|
211
|
-
- **New `kerfjs/overlay` subpath** — the blessed modal/overlay + dismiss manager that every real kerf app hand-rolls. `overlay(content, options?)` appends a wrapper, `mount()`s content inside it (owning the disposal), wires dismissals (Escape / backdrop / outside-click, with `outsideIgnore`), a focus trap (`role="dialog"` / `aria-modal`, Tab wrap-around, restore-focus-on-close), and returns `{ el, close(result?), result }`. `confirm(message, options?)` is a promise-based `window.confirm` replacement, and `prompt(message, options?)` → `Promise<string | null>` its `window.prompt` counterpart (both globals are no-ops in Tauri webviews); `form(fields, options?)` → `Promise<Record<string, string> | null>` collects a two-or-three-field dialog. `choice<R>(message, actions, options?)` → `Promise<R | null>` is the **N-way** sibling of `confirm` — one button per action resolves that action's `value` (or `null` on dismissal), and `options.defaultValue` makes **Enter** (anywhere in the dialog) resolve a default action (the "global Enter-to-confirm" model) without holding the overlay handle. `prompt`/`form` submit on Enter and take an inline `validate`; all auto-escape their content. `confirm` / `prompt` / `form` also accept a **`render` slot option** for design-system teams — return your own markup and spread the provided `ok`/`cancel` (+ `input`/`error`) wiring onto it; kerf keeps owning the promise, `validate`, Enter-submit, dismiss, focus-trap, and focus-restore, so you adopt the batteries-included dialogs without a CSS rewrite. `popover(anchor, content, options?)` → `OverlayHandle` is a non-modal **anchored** overlay: it positions the content relative to `anchor` (below by default, flipping above on viewport overflow, clamped horizontally), defaults to dismiss-on-outside with the anchor exempt, and repositions on scroll / resize. `popover`'s placement core is also exported standalone: **`positionAnchored(el, anchor, options?)`** (one-shot) and **`autoReposition(el, anchor, options?)`** (keeps an element positioned on scroll/resize, returns a disposer) position
|
|
236
|
+
- **New `kerfjs/overlay` subpath** — the blessed modal/overlay + dismiss manager that every real kerf app hand-rolls. `overlay(content, options?)` appends a wrapper, `mount()`s content inside it (owning the disposal), wires dismissals (Escape / backdrop / outside-click, with `outsideIgnore`), a focus trap (`role="dialog"` / `aria-modal`, Tab wrap-around, restore-focus-on-close), and returns `{ el, close(result?), result }`. `confirm(message, options?)` is a promise-based `window.confirm` replacement, and `prompt(message, options?)` → `Promise<string | null>` its `window.prompt` counterpart (both globals are no-ops in Tauri webviews); `form(fields, options?)` → `Promise<Record<string, string> | null>` collects a two-or-three-field dialog. `choice<R>(message, actions, options?)` → `Promise<R | null>` is the **N-way** sibling of `confirm` — one button per action resolves that action's `value` (or `null` on dismissal), and `options.defaultValue` makes **Enter** (anywhere in the dialog) resolve a default action (the "global Enter-to-confirm" model) without holding the overlay handle. `prompt`/`form` submit on Enter and take an inline `validate`; all auto-escape their content. `confirm` / `prompt` / `form` also accept a **`render` slot option** for design-system teams — return your own markup and spread the provided `ok`/`cancel` (+ `input`/`error`) wiring onto it; kerf keeps owning the promise, `validate`, Enter-submit, dismiss, focus-trap, and focus-restore, so you adopt the batteries-included dialogs without a CSS rewrite. `popover(anchor, content, options?)` → `OverlayHandle` is a non-modal **anchored** overlay: it positions the content relative to `anchor` (below by default, flipping above on viewport overflow, clamped horizontally), defaults to dismiss-on-outside with the anchor exempt, and repositions on scroll / resize. `popover`'s placement core is also exported standalone: **`positionAnchored(el, anchor, options?)`** (one-shot) and **`autoReposition(el, anchor, options?)`** (keeps an element positioned on scroll/resize, returns a disposer) position _your own_ element with no overlay lifecycle. **`tooltip(anchor, content, options?)`** is a hover/focus-triggered, non-modal, auto-hiding tooltip built on them. `toast(content, options?)` now returns a **`ToastHandle` (`{ el, dismiss }`)** instead of a bare dismiss function, so callers can inspect the node, wire an action button, or run entrance/exit transitions; new options: `mode: 'replace'` (collapse a rapid sequence to the latest) with `collapse: 'fade'` (default — run the prior toast's exit transition, good for a stacking region) or `'instant'` (remove it synchronously, what a single centered slot wants so messages never cross-fade in place), `variant` (`'info'`/`'success'`/`'warning'` → a `${className}--${variant}` accent class), `enterClass` (added on the next animation frame for a CSS entrance) and `exitClass` + `exitDuration` (CSS owns the exit — on dismiss the `enterClass` is **removed** so `exitClass` needn't out-specify it, and a symmetric single-class fade works with just `enterClass` + `exitDuration`; the node is removed after the delay). `dismiss({ instant: true })` removes the toast **synchronously** (skipping the exit transition), and `mode: 'replace'` `collapse: 'instant'` also force-removes a toast that is already mid-fade — so an action button that dismisses itself and shows a replacement in a single centered slot doesn't cross-fade the two. **Breaking (beta):** `toast()`'s return type changed from `() => void` to `{ el, dismiss }` — call `toast(...).dismiss()` or destructure `{ dismiss }`. Structural only — kerf ships no CSS. Optional and tree-shakeable; shares the core with the main barrel via code-splitting.
|
|
212
237
|
- **New `kerfjs/actions` subpath** — the blessed delegated action-table helper. `action(value)` returns a `data-action` `AttrSpec` (a thin specialization of `attr()`); `delegateActions(root, eventType, table, options?)` wires a whole table of `data-action` handlers with one delegated listener (built on `delegate()`) and returns a disposer. Formalizes the most-reinvented idiom in real kerf apps — one `attr('data-action', …)` table as the single source of truth for both the JSX attribute and the delegate dispatch. Optional and tree-shakeable; adds nothing to the main barrel.
|
|
213
238
|
|
|
214
239
|
## [4.1.1] - 2026-08-14
|
|
215
240
|
|
|
216
|
-
|
|
217
|
-
|
|
218
241
|
- Added a CDN / importmap quickstart (`docs/6-jsx-runtime.md` §6.11.1) covering three no-install ways to load kerf from an ESM CDN: a direct `esm.sh` import, and jsDelivr / unpkg behind an importmap that also maps `@preact/signals-core`. Explains why a raw `dist/*.js` path fails (its unrewritten bare `@preact/signals-core` import won't resolve in the browser) and recommends pinning to a major version.
|
|
219
242
|
- Updated the README no-build example to use version-pinned CDN URLs (`kerfjs@4`) and noted that jsDelivr / unpkg need an importmap while esm.sh works with a direct import.
|
|
220
243
|
|
|
221
244
|
## [4.1.0] - 2026-07-31
|
|
222
245
|
|
|
223
|
-
|
|
224
|
-
|
|
225
246
|
- ESLint 10 is now supported: `eslint-plugin-kerfjs` declares `peerDependencies.eslint` as `^9.0.0 || ^10.0.0`, and every named major is exercised by the rule suite in CI.
|
|
226
247
|
- The plugin now runs its rule suite against each supported ESLint major via `npm run test:eslint-matrix`, which also fails if the declared peer range, the tested set, and the CI matrix disagree — or if the range is left open-ended.
|
|
227
248
|
|
|
228
|
-
|
|
229
249
|
- **ESLint 8 is no longer supported by `eslint-plugin-kerfjs`.** The package is ESM-only and ESLint 8 resolves `.eslintrc` plugins with `require()`, so `extends: ["plugin:kerfjs/…"]` could never load it.
|
|
230
250
|
- **Legacy `.eslintrc` configuration is documented as unsupported.** Use flat config (`eslint.config.js`); the `legacy-recommended` export remains in the package but is unreachable through any config system and should be treated as deprecated.
|
|
231
251
|
|
|
232
|
-
|
|
233
252
|
- Generated Hot Sheet skill and rule files no longer hardcode a machine's local API port or shared secret. The curl fallbacks read `$HOTSHEET_PORT` / `$HOTSHEET_SECRET` from `.hotsheet/settings.local.json` and `.hotsheet/secret.json` instead, with `.hotsheet/settings.json` as the fallback for older projects.
|
|
234
253
|
- A new `npm run check:audit` gate runs `npm audit --omit=dev --audit-level=high` against the published dependency tree and is wired into the pre-push `check:full` gate.
|
|
235
254
|
|
|
236
|
-
|
|
237
255
|
- Test tooling moved to vitest 4 and ESLint 10; coverage thresholds were recalibrated to 98.5% branches / 99.5% statements (lines and functions stay at 100%) after vitest 4's sharper AST-based coverage mapping resolved seventeen previously-miscredited defensive branches.
|
|
238
256
|
- Removed the `hs-m` marketing-ticket skill and Cursor rule.
|
|
239
257
|
|
|
240
258
|
## [4.0.0] - 2026-07-28
|
|
241
259
|
|
|
242
|
-
|
|
243
|
-
|
|
244
260
|
- `draggable`, `spellCheck`, and `contentEditable` no longer accept booleans — they are enumerated HTML attributes, so write the keyword string (`draggable="true"`, `spellCheck="false"`). The boolean forms rendered markup that meant the opposite of what was written; real boolean attributes like `hidden`/`checked`/`disabled` are unchanged.
|
|
245
261
|
- `<select value>` / `<textarea value>` (and their `defaultValue` forms) no longer typecheck — neither element has a `value` content attribute, so kerf was emitting markup no browser reads. Use `<option selected>` and `<textarea>{draft}</textarea>`.
|
|
246
|
-
- Lowercase `autofocus` no longer accepts `"true"` / `"false"` — it is a real boolean attribute, so `autofocus="false"` turned autofocus
|
|
247
|
-
|
|
262
|
+
- Lowercase `autofocus` no longer accepts `"true"` / `"false"` — it is a real boolean attribute, so `autofocus="false"` turned autofocus _on_. Use `autofocus={false}` or omit it.
|
|
248
263
|
|
|
249
264
|
- `href="javascript:void(0)"` and its five sibling spellings are no longer dropped by the URL screen. Dropping the `href` unmade the anchor — it lost `:link` styling, keyboard focus, and its pointer cursor. Matching is against the whole normalized value, so nothing can ride along after an inert body.
|
|
250
265
|
- `defaultSelected` rendered as `defaultselected`, an attribute no browser reads, so the option it named was never pre-selected. It now correctly emits `selected`.
|
|
251
266
|
|
|
252
|
-
|
|
253
267
|
- Typed JSX gained a wide set of modern attributes: globals `inert`, `popover`, `nonce`, `part`/`exportparts`, `enterKeyHint`, `translate`, `autocorrect`, and the full microdata family; per-element `<button popoverTarget/popoverTargetAction/command>`, `<input popoverTarget/popoverTargetAction>`, `<form rel>`, `<source width/height>`, and `writingsuggestions`.
|
|
254
268
|
- `<button command>` is now typed to the spec keywords (`show-modal`, `close`, `request-close`, `toggle-popover`, `show-popover`, `hide-popover`) plus any `--custom` command, catching a custom command written without its required `--` prefix.
|
|
255
269
|
- `<a download>` and `<area download>` accept the bare boolean form as well as a string filename, so `<a href="/report.pdf" download>` compiles.
|
|
256
270
|
|
|
257
|
-
|
|
258
271
|
- The advertised bundle size is corrected everywhere: ~12 KB min+gzip for a realistic import, ~13 KB with `arraySignal` (previously stated as ~11/~12 KB, and ~6.1 KB in the Cursor rules). Every migration page's delta row was recomputed against the new figure.
|
|
259
272
|
- The JSX runtime doc gained a section on enumerated vs. boolean attributes, and the API reference now records where the JSX types come from (WHATWG HTML Living Standard + SVG 2, not another framework's property table).
|
|
260
273
|
- The dev-warning sections in the dev-warnings doc were renumbered by family so the `11.2.N` headings run in document order; cross-references elsewhere in the docs were updated to match.
|
|
261
274
|
|
|
262
|
-
|
|
263
|
-
- The bundle-size check now gates the *prose* as well as the build: every place the docs advertise a size is matched against the measured figure, and a reworded claim that stops matching fails rather than going silently unchecked.
|
|
275
|
+
- The bundle-size check now gates the _prose_ as well as the build: every place the docs advertise a size is matched against the measured figure, and a reworded claim that stops matching fails rather than going silently unchecked.
|
|
264
276
|
- New repo checks wired into `npm run check`: `check:docs:dev-warns` (the dev-warning doc and `ENV_NAME` must name the same set, with monotonic section numbers), `check:design-rule-5` (every top-level `let` in `src/` must be accounted for by the documented rule), and `check:skills` (a skill file restating a threshold its owner disagrees with fails).
|
|
265
277
|
|
|
266
278
|
## [3.0.0] - 2026-07-27
|
|
267
279
|
|
|
268
|
-
|
|
269
|
-
|
|
270
280
|
- **kerf no longer infers development mode — add one line to your entry to keep the dev diagnostics.**
|
|
271
281
|
|
|
272
282
|
```js
|
|
273
|
-
if (import.meta.env.DEV) await import(
|
|
274
|
-
if (process.env.NODE_ENV !==
|
|
283
|
+
if (import.meta.env.DEV) await import("kerfjs/dev"); // Vite
|
|
284
|
+
if (process.env.NODE_ENV !== "production") await import("kerfjs/dev"); // webpack / Node
|
|
275
285
|
```
|
|
276
286
|
|
|
277
287
|
Without it you get production shape: no dev warnings, no read-only `defineStore` `get()`
|
|
278
288
|
snapshot, and a screened dangerous URL warns-and-drops instead of throwing. Nothing else
|
|
279
289
|
changes, and if you never used the dev warnings you get a smaller bundle for free.
|
|
280
290
|
|
|
281
|
-
**Why it changed.** kerf inferred its own dev/prod mode by reading `globalThis.process?.env?.NODE_ENV`. Bundlers substitute the
|
|
291
|
+
**Why it changed.** kerf inferred its own dev/prod mode by reading `globalThis.process?.env?.NODE_ENV`. Bundlers substitute the _bare_ `process.env.NODE_ENV` token and never create a `globalThis.process` object for browser targets, so that read was `undefined`, `undefined !== 'production'` was `true`, and every production browser build silently took the development path. Consequences that were shipping: every `defineStore` `get()` returned a deep read-only `Proxy` (an allocation on every store read), a screened `javascript:`/`data:` URL **threw** instead of the documented warn-and-drop, and the always-on list-key warnings printed to production consoles. Server/Node builds, where `process` exists, were unaffected.
|
|
282
292
|
|
|
283
293
|
Removing the inference also shrinks production bundles by **~4.7 KB min+gzip (27%)** — a realistic import (`signal`/`computed`/`effect`/`batch`/`mount`/`each`/`delegate`) goes from 16.91 KB to **12.24 KB**. Previously the dev-warning modules were imported unconditionally by `mount()`/`each()` and gated at runtime, so they shipped to production regardless of build mode and no amount of tree-shaking could reclaim them. With the dev entry absent they are unreachable, and the `import()` statement itself is eliminated — the chunk is never emitted, let alone fetched.
|
|
284
294
|
|
|
285
295
|
`globalThis.KERF_DEV` is no longer consulted — not importing the dev entry is now the (compile-time) way to opt out. One ordering note: `signal()` picks its constructor at creation time, so put the import first if you rely on the untracked-signal warning.
|
|
286
296
|
|
|
287
|
-
- The `KERF_DEV_WARN_*` diagnostics no longer consult `NODE_ENV` or `globalThis.KERF_DEV` at all. Whether they run is decided in exactly one place: whether you imported `kerfjs/dev`. The previous release stopped kerf's core from inferring dev mode but left a second, inherited gate inside each warner, so a Node/SSR consumer who
|
|
288
|
-
|
|
297
|
+
- The `KERF_DEV_WARN_*` diagnostics no longer consult `NODE_ENV` or `globalThis.KERF_DEV` at all. Whether they run is decided in exactly one place: whether you imported `kerfjs/dev`. The previous release stopped kerf's core from inferring dev mode but left a second, inherited gate inside each warner, so a Node/SSR consumer who _deliberately_ installed the diagnostics under `NODE_ENV=production` got silence — and `globalThis.KERF_DEV = false` still silenced warnings the consumer had explicitly opted into. Both are gone; each warning is now gated only by its own env var. If you were using `globalThis.KERF_DEV` to turn diagnostics off, remove the dev import instead (which is also what sheds the ~4.7 KB from your bundle).
|
|
289
298
|
|
|
290
|
-
- **New:** `each()` accepts an options object — `each(items, render, { cacheKey, key })` — and `key` gives a list a **stable identity**. Without one a list is identified by its position among the `each()` calls in a render, so adding or removing a conditional list above it made kerf rebuild it from scratch: rows lost their DOM nodes, and with them focus, scroll position and in-progress IME composition. Keying a list removes that dependency; because a keyed list doesn't occupy a positional slot, keying just the
|
|
299
|
+
- **New:** `each()` accepts an options object — `each(items, render, { cacheKey, key })` — and `key` gives a list a **stable identity**. Without one a list is identified by its position among the `each()` calls in a render, so adding or removing a conditional list above it made kerf rebuild it from scratch: rows lost their DOM nodes, and with them focus, scroll position and in-progress IME composition. Keying a list removes that dependency; because a keyed list doesn't occupy a positional slot, keying just the _conditional_ list usually stabilizes its siblings too. The existing three-argument `each(items, render, cacheKey)` form is unchanged. In development kerf now warns once per list when it detects such a shift and names the fix.
|
|
291
300
|
|
|
292
301
|
- **`kerfjs/dev` now exports `enableWarnings()`** — and it fixes a defect: until now, none of the
|
|
293
302
|
`KERF_DEV_WARN_*` diagnostics could be switched on in a browser at all. Every one of them read `globalThis.process.env`, which does not exist in a browser realm — and a bundler `define` cannot reach it either, because the read goes through `globalThis.process` into a local binding rather than the substitutable `process.env.X` token. So in a Vite/webpack dev server, the environment where these warnings are most wanted, the entire opt-in family was permanently and silently off. Only Node/SSR (and kerf's own vitest suite, which is why nothing caught it) could turn any of them on.
|
|
@@ -296,8 +305,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
296
305
|
|
|
297
306
|
```js
|
|
298
307
|
if (import.meta.env.DEV) {
|
|
299
|
-
const dev = await import(
|
|
300
|
-
dev.enableWarnings({
|
|
308
|
+
const dev = await import("kerfjs/dev");
|
|
309
|
+
dev.enableWarnings({
|
|
310
|
+
staleBinding: true,
|
|
311
|
+
narrowSet: true,
|
|
312
|
+
invariants: "throw",
|
|
313
|
+
});
|
|
301
314
|
}
|
|
302
315
|
```
|
|
303
316
|
|
|
@@ -311,8 +324,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
311
324
|
|
|
312
325
|
- New opt-in dev warning `KERF_DEV_WARN_LIST_REBIND=1`: fires (once per list) when an `each()` list's container is rebuilt by the morph — an ancestor's tag changed across renders, so the subtree was replaced and the list self-healed by re-binding and repopulating. The recovery is correct but discards row DOM state (focus, scroll, IME, imperative listeners); the warning names the list and points at keeping ancestor tags stable. Follows the standard `KERF_DEV_WARN_*` family rules: off by default, dev-mode only, zero production cost.
|
|
313
326
|
|
|
314
|
-
|
|
315
|
-
- `KERF_DEV_WARN_UNTRACKED_SIGNALS=1` now tells you what it can and cannot see. The warning picks its machinery when a signal is *created*, so it only covers signals created after `kerfjs/dev` is installed — and because static imports are hoisted above a top-level `await import('kerfjs/dev')`, the module-scope signals it most wants to catch are usually created first. Previously that failed silently: you set the env var, saw nothing, and concluded your code was clean. Opting in now prints the coverage boundary once, along with the fix (make `import 'kerfjs/dev'` the first static import of a dev-only entry file). The boundary itself can't be removed — `Signal.prototype`'s `value` accessor is non-configurable, so already-created signals can't be retro-fitted without kerf keeping a registry of every signal, which production would pay for.
|
|
327
|
+
- `KERF_DEV_WARN_UNTRACKED_SIGNALS=1` now tells you what it can and cannot see. The warning picks its machinery when a signal is _created_, so it only covers signals created after `kerfjs/dev` is installed — and because static imports are hoisted above a top-level `await import('kerfjs/dev')`, the module-scope signals it most wants to catch are usually created first. Previously that failed silently: you set the env var, saw nothing, and concluded your code was clean. Opting in now prints the coverage boundary once, along with the fix (make `import 'kerfjs/dev'` the first static import of a dev-only entry file). The boundary itself can't be removed — `Signal.prototype`'s `value` accessor is non-configurable, so already-created signals can't be retro-fitted without kerf keeping a registry of every signal, which production would pay for.
|
|
316
328
|
|
|
317
329
|
- An `each()` of `<tr>` written directly inside `<table>` now fails with a clear error instead of silently duplicating rows: the HTML parser inserts a `<tbody>` around the rows, which kerf cannot bind through. The message names both tags and shows the supported shape (`<table><tbody>{each(...)}</tbody></table>`). Previously this also mis-reported the rows as missing `data-key`.
|
|
318
330
|
|
|
@@ -320,8 +332,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
320
332
|
|
|
321
333
|
- Toolchain: the repo now type-checks with the native **TypeScript 7** compiler across every gate (`typecheck`, the dist `.d.ts` typing gates, the docs code-block compile — a full-repo `tsc --noEmit` now takes ~0.3 s), with `typescript@6` (the JS-API bridge release) retained for tsup's `.d.ts` emit and typescript-eslint, which still require the JS compiler API. `@typescript-eslint/*` bumped to 8.65. No shipped-code changes — `dist/` output is unaffected.
|
|
322
334
|
|
|
323
|
-
|
|
324
|
-
- Fixed: passing a **function** as an `each()` item through an `arraySignal` insert/update (an unusual mistake, but functions are valid `WeakMap` keys so the per-item cache silently accepted them) rendered the row and then threw `each(): items must be objects…` on a *later, unrelated* re-render — far from the cause. The granular path now enforces the same objects-only item contract the snapshot path does, so the error is thrown on the render the offending mutation triggered, naming the type and index. Primitive items already behaved this way; functions now match.
|
|
335
|
+
- Fixed: passing a **function** as an `each()` item through an `arraySignal` insert/update (an unusual mistake, but functions are valid `WeakMap` keys so the per-item cache silently accepted them) rendered the row and then threw `each(): items must be objects…` on a _later, unrelated_ re-render — far from the cause. The granular path now enforces the same objects-only item contract the snapshot path does, so the error is thrown on the render the offending mutation triggered, naming the type and index. Primitive items already behaved this way; functions now match.
|
|
325
336
|
|
|
326
337
|
- `arraySignal.update(i, fn)` now works when `fn` mutates the row object and returns it (not only when it returns a fresh object). Previously such a same-ref update rendered correctly in one list but left every other view of the same signal — a second list, a second `mount()`, a `filter()`ed plain-array view — permanently stale, and was lost outright when the update was batched with a selection change or a `replace()`. kerf now tracks a per-item content version so the change reaches every consumer. Returning a fresh object remains the idiomatic style; both are supported. (Fixed in the same line of work: the per-item version tracking briefly made `arraySignal.update()` throw on a signal of primitives — `arraySignal<number>` used as a plain signal — because a primitive can't key the internal version map; primitive items are now simply skipped, since only object rows are ever memoized.)
|
|
327
338
|
|
|
@@ -333,13 +344,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
333
344
|
|
|
334
345
|
- Fixed: an `arraySignal.update()` that mutated a row object in place (returning the same reference) could be silently reverted to its old content by the next unrelated re-render. The granular update path now keeps kerf's per-row HTML cache in sync with what it rendered, so the two never disagree. (Immutable updates — returning a fresh object — were unaffected, and remain the recommendation.)
|
|
335
346
|
|
|
336
|
-
- Fixed: showing an empty conditionally-rendered `each()` list in the same batch as an update to a
|
|
347
|
+
- Fixed: showing an empty conditionally-rendered `each()` list in the same batch as an update to a _sibling_ list could empty the sibling entirely — it rendered zero rows. When the conditional list reappeared, its list-marker comment landed next to the sibling's, and the diff overwrote the sibling marker's internal id with the reappearing one's, so the sibling's binding could no longer find its own marker. Marker comments (kerf's internal list and binding anchors) now pair only with the identical marker, never with a different one that happens to be the same kind. Ordinary comments in your markup are unaffected.
|
|
337
348
|
|
|
338
349
|
- Fixed: when an element rendered by a condition came back, the diff could pair it with an unrelated sibling that happened to have the same tag, and then whatever protects that sibling's contents kept the wrong contents alive inside it. A `data-morph-skip` widget **swallowed** the reappearing element — its content never rendered — and the widget was duplicated, which for a library-owned subtree (an editor, a chart, a terminal) means a second live instance attached to a node the library has no reference to. A `data-morph-preserve` child ended up under the foreign host and appeared twice. A bound hole's text leaked into the reappearing element and rendered twice. Three rules now gate that pairing: an element with an `id`/`data-key` is only ever matched to a live element with the same key; a `data-morph-skip` element only ever matches another one (so a library-owned subtree is never adopted as a stand-in, whether or not you use keys); and a comment anchoring kerf's own state only matches an anchor of the same kind. Ordinary elements without a key still match positionally exactly as before.
|
|
339
350
|
|
|
340
|
-
- Fixed: when the number of `each()` calls in a render changed — a conditional list appearing or disappearing — a
|
|
351
|
+
- Fixed: when the number of `each()` calls in a render changed — a conditional list appearing or disappearing — a _surviving_ list could render the departed list's rows, or render its own rows inside the wrong container. Lists without a `key` are identified by their position among the `each()` calls, and both the per-item HTML cache and the live list binding were being read as belonging to whichever list now held that position. Two lists over the same collection hit each other's cache exactly, so nothing could detect it from the data. A shift now discards that state and re-renders, which is what the documentation already described it as costing: a rebuild, never wrong output. Lists given a `key` are unaffected, and a render that doesn't change the call count is unaffected. One related improvement falls out: an unrelated list no longer loses its row nodes when a nested `each()` shifts the count.
|
|
341
352
|
|
|
342
|
-
- Fixed: a row added to an `each()` list could land in the wrong place whenever the list wasn't the last thing inside its parent. A list ends at its last row, but kerf was looking for the next
|
|
353
|
+
- Fixed: a row added to an `each()` list could land in the wrong place whenever the list wasn't the last thing inside its parent. A list ends at its last row, but kerf was looking for the next _element_ after it and skipping everything else on the way — so static content following the list (a footer row, a totals line, an "add item" control) got jumped, and a new row appeared after it instead of before. The same skip crossed a neighboring list's internal anchor: with two `each()` lists in one parent, rows from the first could be placed inside the second's region, and when both lists started empty their rows came out **in the wrong order** — the second list's rows rendered first. Both were correct on the initial paint and only went wrong on a later update, which made them read as intermittent. Lists now anchor on the next node of any kind, so a list's rows always stay within its own region.
|
|
343
354
|
|
|
344
355
|
- Fixed: an `each()` list inside an `<svg>` whose row markup contained an apostrophe (or anything else the serializer writes back differently) failed to mount at all, with a self-contradictory error. Also fixed: `each({ key })` now validates the key, so a key containing an HTML comment terminator can no longer break out of the list's internal marker and put markup in the page.
|
|
345
356
|
|
|
@@ -351,7 +362,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
351
362
|
|
|
352
363
|
- Fixed: a controlled `checked` / `value` on an `each()` row's own top-level element could stay visibly stale after the user had interacted with it — the row reconciler's attribute-only fast path wrote the attribute without syncing the live property, so whether the control obeyed your data depended on which internal route the update happened to take.
|
|
353
364
|
|
|
354
|
-
- Fixed: when a conditionally-rendered `each()` list is added or removed, a sibling list could render the
|
|
365
|
+
- Fixed: when a conditionally-rendered `each()` list is added or removed, a sibling list could render the _other_ list's rows — a batched "hide one list and push to another" applied the queued update to the wrong list's DOM. Lists now verify which data a pending update belongs to before applying it, and rebuild from their own items when it doesn't match.
|
|
355
366
|
|
|
356
367
|
- Fixed: `each()` rows inside an `<svg>` root were re-parsed in the HTML namespace on every update, so rows added or structurally changed after the first render were invisible in the browser — the initial picture looked right, which made it read as a rendering flake. Row parsing now follows the list parent's namespace (rows under `<foreignObject>` correctly stay HTML).
|
|
357
368
|
|
|
@@ -359,92 +370,72 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
359
370
|
|
|
360
371
|
- Fixed: anything sitting between a keyed `each()` list's anchor and its rows — a node injected imperatively into the list region — could make a conditional sibling's removal reorder the list, landing a trailing sibling ahead of the rows. A list's row region (anchor through last row) is now treated as one unit by the diff: it moves whole, the diff's cursor steps over it whole, and injected nodes inside it travel along keeping their position relative to the rows.
|
|
361
372
|
|
|
362
|
-
- A conditional sibling
|
|
373
|
+
- A conditional sibling _inside_ a keyed `each()` list's parent — a header row that comes and goes above the list — no longer costs the list's rows their DOM identity. The morph now recognizes the list's marker when a sibling shifts it, moving the marker and its rows up as a single unit instead of rebuilding the list, so row nodes, focus, and the caret survive the toggle. (An ancestor tag change, or a same-tag sibling that positionally takes the container's place, still rebuilds the list — give the list's own container a stable `id`/`data-key` if its rows need to survive that.)
|
|
363
374
|
|
|
364
375
|
- Fixed: a conditional sibling that shared or positionally shadowed a keyed `each()` list's container could strand the list's rows and then render a duplicate copy of them (e.g. a header `<li>` toggled inside the list's `<ul>`, or a same-tag banner `<ul>` before the list container). The morph can separate the list's marker comment from its still-attached rows; the self-heal now removes any such still-live stranded rows before repopulating, so recovery replaces the rows rather than duplicating them.
|
|
365
376
|
|
|
366
377
|
## [2.0.1] - 2026-07-23
|
|
367
378
|
|
|
368
|
-
|
|
369
|
-
|
|
370
379
|
- 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).
|
|
371
380
|
|
|
372
|
-
|
|
373
381
|
- 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.
|
|
374
382
|
- Added an animated architecture diagram and a link to the docs site from the README.
|
|
375
383
|
|
|
376
384
|
## [2.0.0] - 2026-07-23
|
|
377
385
|
|
|
378
|
-
|
|
379
|
-
|
|
380
386
|
- `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.
|
|
381
387
|
- 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.
|
|
382
388
|
|
|
383
|
-
|
|
384
389
|
- 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.
|
|
385
390
|
- New no-build example app, **live-poll**, served exactly as authored — an importmap plus one `html`-templated module, with view-source showing the app.
|
|
386
391
|
- Added `<filter>` to the typed JSX intrinsic elements, so SVG filters compile in JSX-authored code.
|
|
387
392
|
- 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.
|
|
388
393
|
|
|
389
|
-
|
|
390
394
|
- 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.
|
|
391
395
|
- 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.
|
|
392
396
|
|
|
393
|
-
|
|
394
397
|
- 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.
|
|
395
398
|
- 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.
|
|
396
399
|
- `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.
|
|
397
400
|
|
|
398
|
-
|
|
399
401
|
- 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.
|
|
400
402
|
- 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).
|
|
401
403
|
|
|
402
404
|
## [1.0.2] - 2026-07-22
|
|
403
405
|
|
|
404
|
-
|
|
405
|
-
|
|
406
406
|
- Fixed the row-selector demo animation 404ing on the published site — the missing SVG capture is now generated and committed.
|
|
407
407
|
|
|
408
408
|
## [1.0.1] - 2026-07-22
|
|
409
409
|
|
|
410
|
-
|
|
411
|
-
|
|
412
410
|
- KF-329: pin release-workflow npm upgrade to npm@11 — npm@12 needs Node ≥ 22.22.2, above the 22.21.0 pin (`c54559f`)
|
|
413
411
|
|
|
414
412
|
## [1.0.0] - 2026-07-22
|
|
415
413
|
|
|
416
|
-
|
|
417
|
-
|
|
418
414
|
- **Fine-grained signal bindings** — pass a signal or `computed` directly into a JSX attribute or text hole (e.g. `class={computed(...)}` or `{sig}`) inside `mount()`, and that one node updates on signal change without re-running `render()` or walking the list reconciler. Opt-in per hole and non-breaking; works in static content and inside `each()` rows on both the snapshot and `arraySignal` reconcile paths, with binding effects correctly wired, carried, and disposed across row insert/update/remove/move.
|
|
419
415
|
- Bound values get the same safety treatment as static ones: URL screening and `SafeHtml`/`raw()` unwrapping now apply to fine-grained bound attributes.
|
|
420
416
|
- New interactive benchmark playground (`npm run bench:serve`) to explore kerf by hand in the standard 1k-rows benchmark app.
|
|
421
417
|
- New **row-selector** example app and a "Fine-grained bindings" section in the reactivity demo, both showing select-row updates with zero render re-runs and zero list reconciles.
|
|
422
418
|
|
|
423
|
-
|
|
424
419
|
- Hardened the dangerous-URL screen: scheme detection now normalizes away C0 control characters, DEL, and leading whitespace, so obfuscations like `java script:` or a NUL before the colon can no longer slip `javascript:`/`vbscript:` URLs past the check.
|
|
425
420
|
- Attribute names are now validated — malformed names (e.g. from spreading attacker-controlled keys into JSX) throw instead of breaking out of the open tag, and inline `on*` event handlers are rejected on both the static and signal-bound attribute paths, closing an XSS vector where a bound `onclick` would have installed a live handler.
|
|
426
421
|
- Documented kerf's reserved marker namespace (`data-kfb`, `data-kfbrow`, `kfb:`/`kfbr:`/`kf-list:` comments) — consumer content using these can collide with binding wiring, so they're now explicitly reserved.
|
|
427
422
|
- Documented the trusted-input bridges (`toElement()`, `morph()` with string/Element templates, `<iframe srcdoc>`) that bypass escaping by design, and the `raw()`/`SafeHtml` trust boundary.
|
|
428
423
|
- Hardened the release pipeline: least-privilege OIDC scoping per job, SHA-pinned GitHub Actions with automated Dependabot bumps, and a token-holding publish job that runs with `--ignore-scripts` and no build tools.
|
|
429
424
|
|
|
430
|
-
|
|
431
425
|
- Reduced the per-row create cost of fine-grained row bindings from ~1.65× to ~1.15× via lazy row wiring — the common single-root-binding case now needs no subtree walk or extra allocations.
|
|
432
426
|
- Published cross-framework benchmark numbers now come from the official upstream krausest js-framework-benchmark run (kerf is a merged upstream entry), replacing stale local-machine measurements.
|
|
433
427
|
|
|
434
|
-
|
|
435
428
|
- README refreshed for the 1.0 release: fine-grained updates and safe-by-default escaping promoted to headline features, and the status line flipped from "Pre-1.0 — API may evolve" to stable 1.0.
|
|
436
429
|
- Fine-grained bindings documented across all consumer and AI-assistant surfaces (reactivity docs, API reference, usage guide, Cursor rules, Claude skill).
|
|
437
430
|
|
|
438
|
-
- **Signposted the raw HTML/SVG → DOM bridges as trusted-input only.** `toElement()`, `morph()` (with a string/Element template), and the `<iframe srcdoc>` attribute bypass kerf's escaping/URL-screening by design — they're the same trust model as `innerHTML` / `raw()`. The docs now call this out loudly, including that the SVG path is
|
|
439
|
-
- **Documented kerf's reserved marker namespace.** Fine-grained bindings and `each()` lists coordinate through in-band markers that the wiring pass finds by scanning the mounted subtree and matching by id. A consumer element that carries one of those names can collide with a real binding's id and silently steal its update, so the names are now documented as reserved: the `data-kfb` / `data-kfbrow` attributes and HTML comments beginning `kfb:` / `kfbr:` / `kf-list:`. Don't emit them from your own markup or via `raw()`. (kerf's escaping already prevents a plain text/attribute
|
|
440
|
-
- **Attribute names are now validated, and inline event handlers are rejected outright.** The JSX runtime already escaped attribute
|
|
431
|
+
- **Signposted the raw HTML/SVG → DOM bridges as trusted-input only.** `toElement()`, `morph()` (with a string/Element template), and the `<iframe srcdoc>` attribute bypass kerf's escaping/URL-screening by design — they're the same trust model as `innerHTML` / `raw()`. The docs now call this out loudly, including that the SVG path is _more_ dangerous than the HTML path (a top-level `<svg><script>`, SVG event attributes, and `xlink:href="javascript:"` execute once inserted, whereas an HTML-string `<script>` is inert), and that `srcdoc` is HTML a browser re-parses as a document (so `srcdoc={userString}` executes even though the value is escaped as an attribute). No behavior change — these are documentation + regression tests: a real-browser spec (`tests/browser/trusted-html-bridges.spec.ts`) pins the execution boundary across Chromium/Firefox/WebKit, a unit test pins that the granular list fast path keeps the URL screen's guarantee end-to-end, and another pins that SVG input isn't sanitized. See [`docs/7-svg.md`](docs/7-svg.md) § Security, [`docs/8-api-reference.md`](docs/8-api-reference.md), and [`docs/6-jsx-runtime.md`](docs/6-jsx-runtime.md) §6.4.3.
|
|
432
|
+
- **Documented kerf's reserved marker namespace.** Fine-grained bindings and `each()` lists coordinate through in-band markers that the wiring pass finds by scanning the mounted subtree and matching by id. A consumer element that carries one of those names can collide with a real binding's id and silently steal its update, so the names are now documented as reserved: the `data-kfb` / `data-kfbrow` attributes and HTML comments beginning `kfb:` / `kfbr:` / `kf-list:`. Don't emit them from your own markup or via `raw()`. (kerf's escaping already prevents a plain text/attribute _value_ from forging one — the only ways in are hand-written markup or `raw()`.) See [`docs/2-reactivity.md`](docs/2-reactivity.md) § "Reserved marker names".
|
|
433
|
+
- **Attribute names are now validated, and inline event handlers are rejected outright.** The JSX runtime already escaped attribute _values_; it now also validates each attribute _name_ against a safe shape (a letter/underscore/colon followed by letters, digits, or `_ . : -`) and **throws** on anything else. This closes a markup-injection vector when an object with attacker-controlled keys is spread into JSX (`<div {...untrustedObj}>`) — previously a key like `'x><img onerror=…>'` broke out of the open tag even though the value was escaped. Separately, any `on*` attribute (a function _or_ a string value, in any case — e.g. `onClick={fn}` or `onclick="…"`) now throws and points at `delegate()`; previously only function-valued keys matching `/^on[A-Z]/` were caught, so a string `onclick="alert(1)"` slipped through and became a live inline handler when parsed. Both checks now cover the **fine-grained bound path** too: a signal bound straight into an attribute (`onclick={signal}`) is written with `setAttribute`, and `setAttribute('onclick', …)` installs a live inline handler just as a parsed string would — so an `on*` (or malformed) name bound as a signal is rejected at binding time, closing the same vector on the signal path. See [`docs/6-jsx-runtime.md`](docs/6-jsx-runtime.md) §6.4.2.
|
|
441
434
|
- **Hardened the dangerous-URL screen.** The URL-attribute filter (on `href`/`src`/`xlink:href`/`formaction`/`action`) now: (1) sees through control-character and whitespace obfuscation of the scheme — a leading ``, an in-scheme `TAB`/`LF`/`CR` (`java script:`), or a `NUL` before the colon are all normalized away before the scheme is read, matching how a browser resolves the URL, so they can no longer slip a `javascript:` past the screen; (2) treats `data:` by subtype instead of only blocking `data:text/html` — script-executing document types (`data:text/html`, `data:image/svg+xml`, XHTML/XML) are dropped while inert media (raster images, fonts, audio, video, plain text/CSS) still pass, and any unknown subtype fails closed; and (3) also screens the `data` attribute on `<object>` (which loads its target as a document). `raw()` remains the opt-out. Both the static serializer and the fine-grained bound-attribute writer share the screen, so both paths are covered.
|
|
442
|
-
- **Fine-grained signal bindings.** Hand a `Signal`/`computed`
|
|
435
|
+
- **Fine-grained signal bindings.** Hand a `Signal`/`computed` _itself_ (not its `.value`) into a JSX attribute (`class={someSignal}`) or a text hole (`{someSignal}`) inside a `mount()`, and kerf binds that hole directly to the signal: when the signal changes, only that attribute/text node updates — the render function does **not** re-run and the list reconciler does **not** walk. This is kerf's fine-grained update tier, sitting below the coarse `mount()` effect, for the "external state drives one spot" pattern (a `selectedId` flipping a row's class, a live status attribute, etc.). Works in static content and inside `each()` rows on both the snapshot and `arraySignal` (granular) paths, and row bindings' lifetimes track their row node (a row reorder is free; a removed row's binding is torn down). Opt-in and non-breaking: passing a raw signal into JSX previously threw, so existing apps are unchanged, and any hole that isn't a signal stringifies exactly as before. Bound URL attributes (`href`/`src`/`formaction`/`action`/`xlink:href`) get the same `javascript:`/`vbscript:`/`data:text/html` screening as static attributes (`raw()` opts out). Outside a `mount()` (SSR / `SafeHtml.toString()`) a bound signal snapshots its current value and emits no markers. See [`docs/2-reactivity.md`](docs/2-reactivity.md) §2.9.
|
|
443
436
|
|
|
444
437
|
## [0.16.0] - 2026-07-01
|
|
445
438
|
|
|
446
|
-
|
|
447
|
-
|
|
448
439
|
- Fixed keyed-list selection breaking after a row was removed: a signal read only inside `each()`'s `cacheKey` (such as a `selectedId` toggling a row's class) no longer drops out of the reactive dependency set, so later changes re-render correctly.
|
|
449
440
|
- Fixed appending items to a list after clearing it rendering nothing.
|
|
450
441
|
|
|
@@ -453,115 +444,90 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
453
444
|
|
|
454
445
|
## [0.15.5] - 2026-06-30
|
|
455
446
|
|
|
456
|
-
|
|
457
|
-
|
|
458
447
|
- Corrected the advertised bundle size in `llms.txt` to ~11 KB minified + gzipped (including the `@preact/signals-core` runtime dependency; ~12 KB with `arraySignal`), matching the README.
|
|
459
448
|
|
|
460
449
|
## [0.15.4] - 2026-06-30
|
|
461
450
|
|
|
462
|
-
|
|
463
|
-
|
|
464
451
|
- `llms.txt` is now published at a public docs-site URL and bundled in the npm package, making kerf's AI-assistant documentation index discoverable to llms.txt directories and tooling.
|
|
465
452
|
|
|
466
453
|
- The `kerfjs` package now bundles `llms.txt` (the AI-discovery index) at the package root, and the docs site serves it at `https://brianwestphal.github.io/kerf/llms.txt`. Its links are now absolute GitHub URLs so the file is portable across GitHub, the site, and the installed package.
|
|
467
454
|
|
|
468
455
|
## [0.15.3] - 2026-06-30
|
|
469
456
|
|
|
470
|
-
|
|
471
|
-
|
|
472
457
|
- `npm create kerf-component` with no directory argument now prompts for the target directory (defaulting to `my-kerf-component`, with a `.` hint for the current directory) instead of printing usage text and exiting with an error.
|
|
473
458
|
|
|
474
459
|
## [0.15.2] - 2026-06-30
|
|
475
460
|
|
|
476
|
-
|
|
477
|
-
|
|
478
461
|
- Added the `create-kerf-component` initializer — scaffold a ready-to-publish kerf component package with `npm create kerf-component@latest <dir>`, no need to reverse-engineer the packaging rules.
|
|
479
462
|
|
|
480
463
|
## [0.15.1] - 2026-06-30
|
|
481
464
|
|
|
482
|
-
|
|
483
|
-
|
|
484
465
|
- README refresh + eslint rule-count fixes + CHANGELOG hygiene (`cfbaef3`)
|
|
485
466
|
|
|
486
467
|
## [0.15.0] - 2026-06-30
|
|
487
468
|
|
|
488
|
-
|
|
489
|
-
|
|
490
469
|
- List updates now morph same-identity rows in place instead of recreating their DOM nodes, avoiding full-table relayout on large lists and preserving DOM identity, focus, and IME composition across re-renders.
|
|
491
470
|
|
|
492
|
-
|
|
493
471
|
- New guide on incremental migration: kerf can own a single DOM subtree and coexist with React (or any framework), letting you migrate one island at a time.
|
|
494
472
|
- New guide on building and publishing reusable kerf components as npm packages.
|
|
495
473
|
- Example app documentation pages now open with an animated SVG preview of the real app in action, plus a gallery on the complete-examples index.
|
|
496
474
|
|
|
497
475
|
## [0.14.0] - 2026-05-27
|
|
498
476
|
|
|
499
|
-
|
|
500
477
|
- Fix `toElement()` first-paint divergence in WebKit by adopting its result into the live document
|
|
501
478
|
- Fix `mount()` first render in WebKit by adopting an inert `rootEl` into the live document
|
|
502
479
|
|
|
503
480
|
## [0.13.0] - 2026-05-23
|
|
504
481
|
|
|
505
|
-
|
|
506
482
|
- Add `KERF_DEV_WARN_DELEGATE_IN_EFFECT` dev warning to catch `delegate()` calls inside reactive effects
|
|
507
483
|
- New `require-delegate-disposer` ESLint rule flags `delegate()` calls whose disposer is discarded
|
|
508
484
|
- Document `delegate()` disposer gotchas and canonical cleanup patterns
|
|
509
485
|
|
|
510
486
|
## [0.12.1] - 2026-05-22
|
|
511
487
|
|
|
512
|
-
|
|
513
488
|
- Add GitHub Sponsors link to README, homepage, and npm `funding` field
|
|
514
489
|
|
|
515
490
|
## [0.12.0] - 2026-05-22
|
|
516
491
|
|
|
517
|
-
|
|
518
492
|
- `toElement` now returns `Element | DocumentFragment` to support multi-root inputs
|
|
519
493
|
|
|
520
494
|
## [0.11.1] - 2026-05-21
|
|
521
495
|
|
|
522
|
-
|
|
523
496
|
- `attr()` redesign: typed `AttrSpec<N, V>` exposes `.attrs` with dual overloads for cleaner attribute handling
|
|
524
497
|
|
|
525
498
|
## [0.11.0] - 2026-05-21
|
|
526
499
|
|
|
527
|
-
|
|
528
500
|
- `attr()` redesigned with `AttrSpec<N,V>` shape, `.attrs` accessor, and dual overloads
|
|
529
501
|
- Hardened defensive programming across the runtime for safer edge-case handling
|
|
530
502
|
- Refreshed published performance numbers from a fresh cross-framework benchmark run
|
|
531
503
|
|
|
532
504
|
## [0.10.0] - 2026-05-20
|
|
533
505
|
|
|
534
|
-
|
|
535
506
|
- Expose `kerfjs/ai/*` subpaths via package `exports` so the bundled skill/cursorrules files are resolvable
|
|
536
507
|
- Add a defensive fallback in `kerfjs/ai-assistant-configs` ESLint rule so it fires against installed kerfjs versions whose `exports` block subpath resolution
|
|
537
508
|
|
|
538
509
|
## [0.9.1] - 2026-05-20
|
|
539
510
|
|
|
540
|
-
|
|
541
511
|
- Bundle the kerf-app Claude Code skill and Cursor rules inside the npm package at `ai/skill.md`, `ai/cursorrules`, and `ai/manifest.json`
|
|
542
512
|
- Add `kerfjs/ai-assistant-configs` rule to `eslint-plugin-kerfjs` (warn in recommended) to flag drift in installed AI assistant configs
|
|
543
513
|
- `eslint --fix` now replaces only the canonical section above the `KERF-APP-CANONICAL-END` marker, preserving consumer customizations below it
|
|
544
514
|
|
|
545
515
|
## [0.9.0] - 2026-05-20
|
|
546
516
|
|
|
547
|
-
|
|
548
517
|
- Bundle the kerf-app Claude Code skill and Cursor rules inside the npm package at `ai/skill.md`, `ai/cursorrules`, and `ai/manifest.json`
|
|
549
518
|
- Add `kerfjs/ai-assistant-configs` rule (warn in recommended) to `eslint-plugin-kerfjs` v0.9.0 to surface AI-config drift on every lint pass
|
|
550
519
|
- Canonical-file contract (`kerf-skill-version` + `KERF-APP-CANONICAL-END` marker) lets `eslint --fix` refresh the canonical section while preserving consumer customizations below the marker
|
|
551
520
|
|
|
552
521
|
## [0.8.2] - 2026-05-19
|
|
553
522
|
|
|
554
|
-
|
|
555
523
|
- Package `homepage` fields now point to the published docs site, with prominent links in both READMEs
|
|
556
524
|
|
|
557
525
|
## [0.8.1] - 2026-05-19
|
|
558
526
|
|
|
559
|
-
|
|
560
527
|
- New `eslint-plugin-kerfjs` with four AST rules enforcing kerf Hard Rules
|
|
561
528
|
|
|
562
529
|
## [0.8.0] - 2026-05-18
|
|
563
530
|
|
|
564
|
-
|
|
565
531
|
- Add opt-in dev warning `KERF_DEV_WARN_NARROW_SET=1` that fires when `set()` is called with a partial-state object (replace semantics would silently drop missing keys); names the missing keys and points at the `set({ ...get(), ...next })` merge fix
|
|
566
532
|
- Widen `KerfBaseAttrs.contentEditable` to accept `'plaintext-only'` and add the lowercase `contenteditable` alias
|
|
567
533
|
- Expand the `/kerf/migrating/` hub to 13 frameworks — adds Vue 3, Svelte 5, Solid, Preact, htmx, Angular, jQuery, Redux, and Astro pages alongside a refreshed 8-framework comparison matrix
|
|
@@ -572,7 +538,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
572
538
|
|
|
573
539
|
## [0.7.0] - 2026-05-18
|
|
574
540
|
|
|
575
|
-
|
|
576
541
|
- Granular list updates now preserve DOM identity, focus, scroll, IME state, `<details open>`/`<dialog open>`, and `data-morph-skip` subtrees across in-place row updates
|
|
577
542
|
- Two new fast paths in the granular reconciler cut krausest select-row by 71% (27.8 → 8.2 ms) and partial-update by 28% (46.8 → 33.8 ms)
|
|
578
543
|
- `each()`'s third parameter renamed from `key` to `cacheKey` to clarify it's a passive cache-invalidation comparator, not a React-style reconciliation identity; positional callers unaffected
|
|
@@ -588,7 +553,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
588
553
|
|
|
589
554
|
## [0.6.0] - 2026-05-11
|
|
590
555
|
|
|
591
|
-
|
|
592
556
|
- Public `morph(liveRoot, template)` export for standalone DOM reconciliation
|
|
593
557
|
- New `data-morph-preserve` attribute to opt elements out of morphing
|
|
594
558
|
- New `data-morph-skip-children` attribute — morph host attrs but leave subtree intact
|
|
@@ -597,12 +561,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
597
561
|
|
|
598
562
|
## [0.5.1] - 2026-05-11
|
|
599
563
|
|
|
600
|
-
|
|
601
564
|
- Fix `dist/jsx-runtime.d.ts` IntrinsicElements self-shadow that broke JSX typing in consumer apps
|
|
602
565
|
|
|
603
566
|
## [0.5.0] - 2026-05-10
|
|
604
567
|
|
|
605
|
-
|
|
606
568
|
- Add `arraySignal` (`kerfjs/array-signal` subpath) — granular collection signal that drives O(patches) DOM updates for keyed lists
|
|
607
569
|
- Faster keyed-list updates: bulk-parse contiguous insert runs and consecutive update patches in the granular reconcile path
|
|
608
570
|
- Perf optimizations on the `each()` / `mount()` update path; benchmarks now competitive with Solid/Vue on swap/remove/clear
|
|
@@ -616,17 +578,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
616
578
|
|
|
617
579
|
## [0.4.2] - 2026-05-09
|
|
618
580
|
|
|
619
|
-
|
|
620
581
|
- No user-facing changes in this release.
|
|
621
582
|
|
|
622
583
|
## [0.4.1] - 2026-05-09
|
|
623
584
|
|
|
624
|
-
|
|
625
585
|
- Just fixing the build
|
|
626
586
|
|
|
627
587
|
## [0.4.0] - 2026-05-09
|
|
628
588
|
|
|
629
|
-
|
|
630
589
|
- `delegate()` now auto-promotes the seven well-known non-bubbling events (`focus`, `blur`, `scroll`, `load`, `error`, `mouseenter`, `mouseleave`) to capture phase, with `closest()`-style selector matching preserved
|
|
631
590
|
- Fixed focus and caret position loss when reordering keyed `each()` rows on engines that drop focus on `insertBefore` (older Safari, happy-dom)
|
|
632
591
|
- `mount()` now throws a descriptive error when the root element is null/undefined instead of a generic "Cannot set properties of null"
|
|
@@ -637,12 +596,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
637
596
|
|
|
638
597
|
## [0.3.1] - 2026-05-08
|
|
639
598
|
|
|
640
|
-
|
|
641
599
|
- Removed stale `morphdom` references; the bundled native diff is now the only reconciler
|
|
642
600
|
|
|
643
601
|
## [0.3.0] - 2026-05-08
|
|
644
602
|
|
|
645
|
-
|
|
646
603
|
- Rebuilt render pipeline with structured segments and a native keyed-list diff, replacing the morphdom dependency
|
|
647
604
|
- Added `each()` for keyed list iteration with per-item HTML memoization by object identity
|
|
648
605
|
- Renamed the npm package from `kerf` to `kerfjs` (the `kerf` name tripped npm's typo-squatting heuristic); the brand, GitHub repo, and Pages URL are unchanged
|
|
@@ -653,24 +610,20 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
653
610
|
|
|
654
611
|
## [0.2.1] - 2026-05-07
|
|
655
612
|
|
|
656
|
-
|
|
657
613
|
- Add `Fragment` export to the `kerfjs` barrel for explicit JSX fragment usage
|
|
658
614
|
|
|
659
615
|
## [0.2.0] - 2026-05-07
|
|
660
616
|
|
|
661
|
-
|
|
662
617
|
- Fix focused contenteditable losing focus/caret during morph
|
|
663
618
|
- Fix `SafeHtml` identity mismatch across entry points caused by dist bundling
|
|
664
619
|
- Fix `clearStoreRegistry` no-op in built output, restoring test isolation
|
|
665
620
|
|
|
666
621
|
## [0.1.2] - 2026-05-07
|
|
667
622
|
|
|
668
|
-
|
|
669
623
|
- No user-facing changes; release tooling fixes only.
|
|
670
624
|
|
|
671
625
|
## [0.1.1] - 2026-05-07
|
|
672
626
|
|
|
673
|
-
|
|
674
627
|
- This is just a publication script test
|
|
675
628
|
|
|
676
629
|
## [0.1.0] - 2026-05-07
|