kerfjs 5.0.0-beta.52 → 5.0.0-beta.54

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.
Files changed (60) hide show
  1. package/CHANGELOG.md +738 -0
  2. package/README.md +4 -3
  3. package/ai/cursorrules +7 -6
  4. package/ai/manifest.json +11 -7
  5. package/ai/skill.md +7 -6
  6. package/dist/actions.js +3 -3
  7. package/dist/actions.js.map +1 -1
  8. package/dist/array-signal.js +1 -1
  9. package/dist/async.js +1 -1
  10. package/dist/attach.js +1 -68
  11. package/dist/attach.js.map +1 -1
  12. package/dist/{chunk-CEQMZYLR.js → chunk-3C6F6NM4.js} +1 -1
  13. package/dist/{chunk-CEQMZYLR.js.map → chunk-3C6F6NM4.js.map} +1 -1
  14. package/dist/{chunk-BDX3R4OM.js → chunk-4TX6YJDM.js} +8 -2
  15. package/dist/chunk-4TX6YJDM.js.map +1 -0
  16. package/dist/{chunk-ZOIERTUW.js → chunk-BI4HGICV.js} +3 -3
  17. package/dist/{chunk-ZOIERTUW.js.map → chunk-BI4HGICV.js.map} +1 -1
  18. package/dist/chunk-FM6N2B4N.js +70 -0
  19. package/dist/chunk-FM6N2B4N.js.map +1 -0
  20. package/dist/{chunk-ELXVRKY2.js → chunk-H7WTRM6F.js} +2 -2
  21. package/dist/{chunk-ELXVRKY2.js.map → chunk-H7WTRM6F.js.map} +1 -1
  22. package/dist/{chunk-Y2FOYPBV.js → chunk-IHMDBBUJ.js} +2 -2
  23. package/dist/chunk-IHMDBBUJ.js.map +1 -0
  24. package/dist/{chunk-3GSWKMBZ.js → chunk-UGNPDLNI.js} +7 -5
  25. package/dist/chunk-UGNPDLNI.js.map +1 -0
  26. package/dist/{chunk-H3TTM7PQ.js → chunk-WMRFXSLG.js} +112 -108
  27. package/dist/chunk-WMRFXSLG.js.map +1 -0
  28. package/dist/{chunk-E5R5GNKE.js → chunk-X6OQ2HTR.js} +2 -2
  29. package/dist/{chunk-E5R5GNKE.js.map → chunk-X6OQ2HTR.js.map} +1 -1
  30. package/dist/dev.d.ts +31 -13
  31. package/dist/dev.js +42 -23
  32. package/dist/dev.js.map +1 -1
  33. package/dist/html.js +3 -3
  34. package/dist/index.d.ts +12 -7
  35. package/dist/index.js +11 -11
  36. package/dist/index.js.map +1 -1
  37. package/dist/jsx-runtime.js +1 -1
  38. package/dist/list.d.ts +3 -2
  39. package/dist/list.js +27 -13
  40. package/dist/list.js.map +1 -1
  41. package/dist/overlay.d.ts +12 -25
  42. package/dist/overlay.js +177 -35
  43. package/dist/overlay.js.map +1 -1
  44. package/dist/remount.js +2 -2
  45. package/dist/router.d.ts +13 -4
  46. package/dist/router.js +8 -4
  47. package/dist/router.js.map +1 -1
  48. package/dist/scope.d.ts +10 -2
  49. package/dist/scope.js +22 -28
  50. package/dist/scope.js.map +1 -1
  51. package/dist/testing.js +1 -1
  52. package/dist/timing.d.ts +3 -2
  53. package/dist/timing.js +1 -1
  54. package/dist/timing.js.map +1 -1
  55. package/llms.txt +1 -1
  56. package/package.json +1 -1
  57. package/dist/chunk-3GSWKMBZ.js.map +0 -1
  58. package/dist/chunk-BDX3R4OM.js.map +0 -1
  59. package/dist/chunk-H3TTM7PQ.js.map +0 -1
  60. package/dist/chunk-Y2FOYPBV.js.map +0 -1
package/README.md CHANGED
@@ -209,12 +209,13 @@ The class lives in its own subpath so apps that don't need it shed ~1 KB. Reads
209
209
  ```ts
210
210
  import { morph, raw } from "kerfjs";
211
211
 
212
- morph(liveCard, freshlyBuiltCardEl); // Element template
213
- morph(liveCard, '<article class="card">…</article>'); // raw HTML string
212
+ // `template` describes liveCard's CHILDREN; liveCard's own attributes are untouched
213
+ morph(liveCard, freshlyBuiltCardEl); // Element template: its child nodes are used
214
+ morph(liveCard, '<h2 class="card-title">…</h2><p>…</p>'); // raw HTML string
214
215
  morph(liveCard, raw(htmlFromServer)); // SafeHtml
215
216
  ```
216
217
 
217
- Same algorithm `mount()` uses internally — `data-morph-skip`, `data-morph-skip-children`, `data-morph-preserve`, focused-input value + selection preservation, the `<details>` / `<dialog>` user-agent-owned `open` rule all carry over. Use it for SSR-fragment hydration, page-refresh diffs, third-party widget remounts. See [`docs/4-render.md`](./docs/4-render.md) §4.4.3.
218
+ Same algorithm `mount()` uses internally — `data-morph-skip`, `data-morph-skip-children`, `data-morph-preserve`, focused-input value + selection preservation, the user-agent-owned `open` rule (`<details>`, `<dialog>`, custom elements) all carry over. Only the live root's children are reconciled — the root's own attributes are never touched. Use it for SSR-fragment hydration, page-refresh diffs, third-party widget remounts. See [`docs/4-render.md`](./docs/4-render.md) §4.4.3.
218
219
 
219
220
  ### No build step at all: the `html` tagged template
220
221
 
package/ai/cursorrules CHANGED
@@ -1,4 +1,4 @@
1
- <!-- kerf-skill-version: 1.21.0 -->
1
+ <!-- kerf-skill-version: 1.22.0 -->
2
2
 
3
3
  # kerf.cursorrules — rules for building apps with kerf
4
4
 
@@ -17,7 +17,7 @@ You are writing a UI in kerf — a ~12 KB reactive framework (~13 KB with `array
17
17
  - Install with `npm install kerfjs`.
18
18
  - `tsconfig.json`: `"jsx": "react-jsx"`, `"jsxImportSource": "kerfjs"`.
19
19
  - Vite / esbuild need no extra config.
20
- - **Dev diagnostics are opt-in by import, and only an APP installs them.** kerf does not infer dev mode. In the app entry add `if (import.meta.env.DEV) await import('kerfjs/dev');` (Vite) or `if (process.env.NODE_ENV !== 'production') await import('kerfjs/dev');` (webpack/Node). That enables the read-only store `get()` snapshot, the throwing dangerous-URL screen, and makes the `KERF_DEV_WARN_*` family available; omitting it is production shape and sheds ~4.7 KB min+gzip because the condition folds away and the chunk is never emitted. Put it FIRST if relying on the untracked-signal warning — `signal()` picks its constructor at creation time.
20
+ - **Dev diagnostics are opt-in by import, and only an APP installs them.** kerf does not infer dev mode. In the app entry add `if (import.meta.env.DEV) await import('kerfjs/dev');` (Vite) or `if (process.env.NODE_ENV !== 'production') await import('kerfjs/dev');` (webpack/Node). That enables the read-only store `get()` snapshot, the throwing dangerous-URL screen, and makes the `KERF_DEV_WARN_*` family available; omitting it is production shape and sheds ~4.7 KB min+gzip because the condition folds away and the chunk is never emitted. Put it (and any `enableWarnings()` call) FIRST if relying on the untracked-signal, delegate-in-effect, or rebuilt-listeners warnings — `signal()`, `effect()`, and `mount()` decide those at creation time, so anything created earlier stays uncovered.
21
21
  - **Switch individual warnings on with `enableWarnings()`**, which is the only switch that works in a browser (no `process` object there, and a bundler `define` cannot reach the read): `const dev = await import('kerfjs/dev'); dev.enableWarnings({ staleBinding: true, narrowSet: true, invariants: 'throw' });`. The `KERF_DEV_WARN_*` env vars do the same for Node/SSR/CI; an explicit call wins either way.
22
22
  - **A component package must NEVER import `kerfjs/dev`.** The hooks are process-global, so installing them is the consuming app's decision — a library that does it forces the diagnostics (and the chunk) on every consumer. Put the import in your demo page or test harness instead.
23
23
  - Recommended: also install `eslint-plugin-kerfjs` (`npm install --save-dev eslint-plugin-kerfjs`) and add `kerfjs.configs.recommended` to the project's eslint config. It enforces five of the hard rules below (no inline JSX event handlers, require `data-key` in `each()`, capture `delegate()` disposers, no nested `mount()`, prefer module JSX augmentation) at edit time so violations surface as IDE squiggles before any code runs.
@@ -126,9 +126,9 @@ if (import.meta.env.DEV) await import("kerfjs/dev");
126
126
  3. **Escape hatches:** `data-morph-skip` (element + subtree preserved verbatim — for Monaco / xterm / D3); `data-morph-skip-children` (attrs morph, subtree preserved — for client-hydrated slots whose host classes change); `data-morph-preserve` (element survives the trailing-removal pass — for imperatively-injected nodes like autoplay videos).
127
127
  4. **Never `addEventListener` inside a `mount()`-managed tree** unless under `data-morph-skip`. A morph re-render may discard the node. Use `delegate` / `delegateCapture` instead.
128
128
  5. **Capture the `delegate()` / `delegateCapture()` disposer** whenever the registration's scope is shorter than the page. Both helpers return `() => void`; the listener closure pins `rootEl`, `handler`, and everything the handler closes over (stores, signals, app state). Discarding the disposer on a transient root (modal, route view, mount swap, dynamic widget) leaks the listener AND the app graph it captures; re-mount cycles stack listeners linearly. `mount()`'s own disposer does NOT remove delegates for you. Safe to discard only when the registration is truly page-lifetime (root is `document.body` or equivalent, attached once at startup, never torn down).
129
- 6. **One `mount()` per root.** Don't nest. Compose with plain functions that return JSX.
129
+ 6. **One `mount()` per root.** Don't nest. Compose with plain functions that return JSX. One exception: a modal `<dialog>` of your own that anchors kerf `popover()` / `tooltip()` surfaces must contain `<div data-kerf-overlay-host data-morph-skip></div>` — kerf renders those surfaces into it (inside the modal, so they are not inert) and that slot is the one allowed nested-mount boundary. Dialogs `kerfjs/overlay` opens get a slot automatically.
130
130
  7. **No `<MyComponent />` semantics with hooks.** Components are plain functions returning JSX. State lives in module-scope signals or stores, never in component closures.
131
- 8. **Values bind, structure re-renders.** For a value hole, pass the signal/computed ITSELF (`<span>{count}</span>`, `class={sig}`) — kerf updates that one node directly, no render re-run. Read `.value` only when the JSX _structure_ depends on the signal — and then the read must happen INSIDE the render function to be tracked: `const x = count.value; mount(el, () => <span>{x}</span>)` will NOT re-render. Bind a STABLE signal/computed instance per hole (a `computed` that switches internally), never `class={cond ? sigA : sigB}` — switching instances can go silently stale (`KERF_DEV_WARN_STALE_BINDING=1` detects it). Endpoint: a render reading NO `.value` runs exactly once — a fully bound mount never re-renders; `KERF_DEV_WARN_VALUE_ONLY_RERENDER=1` flags re-renders that could have been bindings.
131
+ 8. **Values bind, structure re-renders.** For a value hole, pass the signal/computed ITSELF (`<span>{count}</span>`, `class={sig}`) — kerf updates that one node directly, no render re-run. Read `.value` only when the JSX _structure_ depends on the signal — and then the read must happen INSIDE the render function to be tracked: `const x = count.value; mount(el, () => <span>{x}</span>)` will NOT re-render. Bind a STABLE signal/computed instance per hole (a `computed` that switches internally), never `class={cond ? sigA : sigB}` — switching instances can go silently stale (`enableWarnings({ staleBinding: true })` (or `KERF_DEV_WARN_STALE_BINDING=1` under Node) detects it). Endpoint: a render reading NO `.value` runs exactly once — a fully bound mount never re-renders; `enableWarnings({ valueOnlyRerender: true })` (or `KERF_DEV_WARN_VALUE_ONLY_RERENDER=1` under Node) flags re-renders that could have been bindings.
132
132
  9. **Store actions receive `(set, get)`, not `(state)`.** `set(next)` replaces state; mutating `get()` does nothing.
133
133
  10. **Use `data-action` attributes, not inline `onClick`.** Inline handlers are NOT supported by the JSX → string runtime; delegate from the root instead.
134
134
  11. **`arraySignal` is opt-in for long keyed lists** where most updates are pointwise (single-row edits, append-to-end). For short lists / filter+sort pipelines, plain `signal` + `each(items.value, ...)` is simpler and equally fast.
@@ -207,7 +207,8 @@ const cart = defineStore({
207
207
  });
208
208
 
209
209
  // One-shot reconcile against existing DOM (no signals)
210
- morph(liveCard, '<article class="card">…</article>');
210
+ // the template describes liveCard's CHILDREN; liveCard's own attributes are untouched
211
+ morph(liveCard, '<h2 class="card-title">…</h2><p>…</p>');
211
212
 
212
213
  // No build step (CDN / importmap): the html tagged template instead of JSX.
213
214
  // Same runtime semantics as JSX; write real HTML attribute names; a hole must
@@ -260,7 +261,7 @@ mount(listEl, () => (
260
261
  - ` html` ``: partial attribute values are not supported → in `kerfjs/html` templates a hole must be the COMPLETE attribute value. Replace `class="a ${b}"` with a pre-built string (`` class="${`a ${b}`}" ``) or, for a bound attribute, `class="${computed(() => `a ${b.value}`)}"`.
261
262
  - An `each()` list's rows lose focus / scroll / typing state when an unrelated conditional list above them appears or disappears (kerf warns in dev) → lists without a key are identified by position among the render's `each()` calls, so adding/removing one above shifts this list's identity and kerf rebuilds it. Give the lists stable keys: `each(items, render, { key: 'results' })`; keying just the conditional list is usually enough.
262
263
  - Keyed `each()` list suddenly renders zero rows — only its `<!--kf-list:N-->` marker — with no errors, and it never recovers (kerfjs ≤ 2.0.1) → a conditionally-rendered sibling BEFORE the list (possibly higher in the tree, e.g. an error banner) was removed that render; older kerfjs rebuilt the shifted list container from the template, permanently detaching the list's internal binding. Upgrade kerfjs (fixed after 2.0.1 — the morph now moves the shifted container up in place, keeping node identity). On older versions, keep the structure before the list stable: wrap the conditional in an always-present container (`<div class="banners">{cond ? <div/> : ''}</div>`).
263
- - A numbered / zebra-striped / "N of M" `each()` list shows the wrong number on rows that MOVED (reorder, or non-tail insert/remove), while unmoved rows look right → the render fn's `index` argument is NOT part of the memo key (only item identity + `cacheKey` + content version are), so a row that keeps identity but changes position keeps HTML rendered at its old index. Fold the index into the memo key: `each(items, (it, i) => …, { cacheKey: (_, i) => i })` (add `key` if used). Opt-in dev warn: `KERF_DEV_WARN_STALE_INDEX=1`.
264
+ - A numbered / zebra-striped / "N of M" `each()` list shows the wrong number on rows that MOVED (reorder, or non-tail insert/remove), while unmoved rows look right → the render fn's `index` argument is NOT part of the memo key (only item identity + `cacheKey` + content version are), so a row that keeps identity but changes position keeps HTML rendered at its old index. Fold the index into the memo key: `each(items, (it, i) => …, { cacheKey: (_, i) => i })` (add `key` if used). Opt-in dev warn: `enableWarnings({ staleIndex: true })` (or `KERF_DEV_WARN_STALE_INDEX=1` under Node).
264
265
 
265
266
  ## Server / SSR
266
267
 
package/ai/manifest.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
- "kerfjsVersion": "5.0.0-beta.52",
2
+ "kerfjsVersion": "5.0.0-beta.54",
3
3
  "files": [
4
4
  {
5
5
  "name": "skill",
6
6
  "source": "kerf.claude-skill.md",
7
7
  "bundle": "ai/skill.md",
8
8
  "dest": ".claude/skills/kerf-app/SKILL.md",
9
- "version": "1.23.0",
10
- "sha256": "28b82b4d31b98814c8fbbe1cb0bcf716192deb494c020b9af41a30830e6a0007",
9
+ "version": "1.24.0",
10
+ "sha256": "f74616ec20120fa97ec2ff7e92971f5721ee8dcb0b5b7da2dd2604bb87002b69",
11
11
  "history": {
12
12
  "1.0.0": "bef982cb743fd4c73e666ee5cf774112e57c93ebb13b24bbacbf08577db0930a",
13
13
  "1.1.0": "f2aadb21e33dd389028951ae1ecbd9f8ba68648b3c5beba43fb4dfa2ab29f195",
@@ -37,7 +37,9 @@
37
37
  "1.20.0": "aa88ccdbf969624bbc6cefadba76ada1f090fae436597a7bab564e24c467287f",
38
38
  "1.21.0": "265b747eeb9c3d445c60d66a1109eec42705075b7cd154d107bd79d543c1e7e5",
39
39
  "1.22.0": "dc99b0ca5e911be11cbfa2ae515c0e161a414a5185895069535c41ba56664e70",
40
- "1.23.0": "28b82b4d31b98814c8fbbe1cb0bcf716192deb494c020b9af41a30830e6a0007"
40
+ "1.23.0": "28b82b4d31b98814c8fbbe1cb0bcf716192deb494c020b9af41a30830e6a0007",
41
+ "1.23.1": "e21d7db1097ad95c11af9248ad17d514716bebc5646becf57d19a7598d6e382c",
42
+ "1.24.0": "f74616ec20120fa97ec2ff7e92971f5721ee8dcb0b5b7da2dd2604bb87002b69"
41
43
  }
42
44
  },
43
45
  {
@@ -45,8 +47,8 @@
45
47
  "source": "kerf.cursorrules",
46
48
  "bundle": "ai/cursorrules",
47
49
  "dest": ".cursorrules",
48
- "version": "1.21.0",
49
- "sha256": "a1b663b44650b1a04f89c60b26b002322fc6789650291477e14e13329c3ce25f",
50
+ "version": "1.22.0",
51
+ "sha256": "3515c663e908b2d6dc9a2a9cf42f2a5bf9a5b3a10c79d43e30cc8427e2ce209d",
50
52
  "history": {
51
53
  "1.0.0": "98f7faff2b7585ef9cc511add42ea5d9bf0cbae8307b066af090e9281136d2ab",
52
54
  "1.1.0": "a2ec8c221dc694c98b81009cd2d99f9159a45fda94585de2f54471b227099eda",
@@ -74,7 +76,9 @@
74
76
  "1.18.0": "de3eea1bc181da629694d19a0817d252a868e4704437fbdd9e923856b8a0890a",
75
77
  "1.19.0": "c5b91e449f6feccaa36a04cb7f8550d3e414eb2795ea6c5dccdf6071206b3261",
76
78
  "1.20.0": "5e7b8d8ddc41f60220961417c9ae01c193fae045378a10a3e9c96a0e3cf10a03",
77
- "1.21.0": "a1b663b44650b1a04f89c60b26b002322fc6789650291477e14e13329c3ce25f"
79
+ "1.21.0": "a1b663b44650b1a04f89c60b26b002322fc6789650291477e14e13329c3ce25f",
80
+ "1.21.1": "9b3eb825a7bec05bb6f0138f77b45fadf896b83020178c34c5a2f531c38f4dee",
81
+ "1.22.0": "3515c663e908b2d6dc9a2a9cf42f2a5bf9a5b3a10c79d43e30cc8427e2ce209d"
78
82
  }
79
83
  }
80
84
  ]
package/ai/skill.md CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: kerf-app
3
3
  description: Build UIs in the kerf reactive framework and its @kerfjs/ui component package (https://github.com/brianwestphal/kerf). Use this skill whenever the user is writing or modifying code that imports `kerfjs` or `@kerfjs/ui`, asks to add a feature to a kerf app, or asks "how do I do X in kerf?". Use it proactively the moment you spot a kerf import in the file you're editing.
4
- kerf-skill-version: 1.23.0
4
+ kerf-skill-version: 1.24.0
5
5
  ---
6
6
 
7
7
  # Building apps with kerf
@@ -17,7 +17,7 @@ kerf is a ~12 KB reactive UI framework (~13 KB with `arraySignal`): signals + DO
17
17
  - Install: `npm install kerfjs`
18
18
  - `tsconfig.json`: `"jsx": "react-jsx"`, `"jsxImportSource": "kerfjs"`
19
19
  - Vite / esbuild need no extra config.
20
- - **Dev diagnostics are opt-in by import, and only an APP installs them.** kerf does not infer dev mode. In the app entry add `if (import.meta.env.DEV) await import('kerfjs/dev');` (Vite) or `if (process.env.NODE_ENV !== 'production') await import('kerfjs/dev');` (webpack/Node). That enables the read-only store `get()` snapshot, the throwing dangerous-URL screen, and makes the `KERF_DEV_WARN_*` family available; omitting it is production shape and sheds ~4.7 KB min+gzip because the condition folds away and the chunk is never emitted. Put it FIRST if relying on the untracked-signal warning — `signal()` picks its constructor at creation time.
20
+ - **Dev diagnostics are opt-in by import, and only an APP installs them.** kerf does not infer dev mode. In the app entry add `if (import.meta.env.DEV) await import('kerfjs/dev');` (Vite) or `if (process.env.NODE_ENV !== 'production') await import('kerfjs/dev');` (webpack/Node). That enables the read-only store `get()` snapshot, the throwing dangerous-URL screen, and makes the `KERF_DEV_WARN_*` family available; omitting it is production shape and sheds ~4.7 KB min+gzip because the condition folds away and the chunk is never emitted. Put it (and any `enableWarnings()` call) FIRST if relying on the untracked-signal, delegate-in-effect, or rebuilt-listeners warnings — `signal()`, `effect()`, and `mount()` decide those at creation time, so anything created earlier stays uncovered.
21
21
  - **Switch individual warnings on with `enableWarnings()`**, which is the only switch that works in a browser (no `process` object there, and a bundler `define` cannot reach the read): `const dev = await import('kerfjs/dev'); dev.enableWarnings({ staleBinding: true, narrowSet: true, invariants: 'throw' });`. The `KERF_DEV_WARN_*` env vars do the same for Node/SSR/CI; an explicit call wins either way.
22
22
  - **A component package must NEVER import `kerfjs/dev`.** The hooks are process-global, so installing them is the consuming app's decision — a library that does it forces the diagnostics (and the chunk) on every consumer. Put the import in your demo page or test harness instead.
23
23
  - Recommended companion: `npm install --save-dev eslint-plugin-kerfjs` and add `kerfjs.configs.recommended` to the project's eslint config. Enforces five of the hard rules below (no inline JSX event handlers, require `data-key` in `each()`, capture `delegate()` disposers, no nested `mount()`, prefer module JSX augmentation) at edit time — useful as a self-correction signal when authoring kerf code.
@@ -135,9 +135,9 @@ if (import.meta.env.DEV) await import("kerfjs/dev");
135
135
  - `data-morph-preserve` — element survives the trailing-removal pass even when the new template doesn't emit it. For imperatively-injected children (autoplay video, tooltip overlay, analytics pixel). Does NOT block a keyed-match move.
136
136
  4. **Never `addEventListener` inside a `mount()`-managed tree** unless under `data-morph-skip`. A morph re-render may discard the node. Use `delegate` / `delegateCapture` instead.
137
137
  5. **Capture the `delegate()` / `delegateCapture()` disposer** whenever the registration's scope is shorter than the page. Both helpers return `() => void`; the listener closure pins `rootEl`, `handler`, and everything the handler closes over (stores, signals, app state). Discarding the disposer on a transient root (modal, route view, mount swap, dynamic widget) leaks the listener AND the app graph it captures; re-mount cycles stack listeners linearly. `mount()`'s own disposer does NOT remove delegates for you. Safe to discard only when the registration is truly page-lifetime (root is `document.body` or equivalent, attached once at startup, never torn down).
138
- 6. **One `mount()` per root.** Don't nest `mount()` calls. Compose with plain functions returning JSX.
138
+ 6. **One `mount()` per root.** Don't nest `mount()` calls. Compose with plain functions returning JSX. One exception: a modal `<dialog>` of your own that anchors kerf `popover()` / `tooltip()` surfaces must contain `<div data-kerf-overlay-host data-morph-skip></div>` — kerf renders those surfaces into it (inside the modal, so they are not inert) and that slot is the one allowed nested-mount boundary. Dialogs `kerfjs/overlay` opens get a slot automatically.
139
139
  7. **Components are plain functions.** `<MyComponent props />` works — the JSX runtime calls `MyComponent(props)` and uses the returned JSX — but there's no hook system, no lifecycle, and no per-instance state. State lives in module-scope signals or stores, never in component closures.
140
- 8. **Values bind, structure re-renders.** For a value hole, pass the signal/computed ITSELF (`<span>{count}</span>`, `class={sig}`) — kerf updates that one node directly, no render re-run. Read `.value` only when the JSX _structure_ depends on the signal — and then the read must happen INSIDE the render function to be tracked: `const x = count.value; mount(el, () => <span>{x}</span>)` does NOT re-render. One caveat on bound holes: bind a STABLE signal/computed instance per hole (`class={computed(() => …)}` that switches internally), never `class={cond ? sigA : sigB}` — switching instances can go silently stale (detectable via `KERF_DEV_WARN_STALE_BINDING=1`). The idiom's endpoint: a render that reads NO `.value` runs exactly once — a fully bound mount never re-renders. To find `.value` holes worth migrating, `KERF_DEV_WARN_VALUE_ONLY_RERENDER=1` flags re-renders whose only differences were text/attribute values.
140
+ 8. **Values bind, structure re-renders.** For a value hole, pass the signal/computed ITSELF (`<span>{count}</span>`, `class={sig}`) — kerf updates that one node directly, no render re-run. Read `.value` only when the JSX _structure_ depends on the signal — and then the read must happen INSIDE the render function to be tracked: `const x = count.value; mount(el, () => <span>{x}</span>)` does NOT re-render. One caveat on bound holes: bind a STABLE signal/computed instance per hole (`class={computed(() => …)}` that switches internally), never `class={cond ? sigA : sigB}` — switching instances can go silently stale (detectable via `enableWarnings({ staleBinding: true })` (or `KERF_DEV_WARN_STALE_BINDING=1` under Node)). The idiom's endpoint: a render that reads NO `.value` runs exactly once — a fully bound mount never re-renders. To find `.value` holes worth migrating, `enableWarnings({ valueOnlyRerender: true })` (or `KERF_DEV_WARN_VALUE_ONLY_RERENDER=1` under Node) flags re-renders whose only differences were text/attribute values.
141
141
  9. **Store actions take `(set, get)`, not `(state)`.** `set(next)` replaces state; mutating `get()` does nothing.
142
142
  10. **Use `data-action` attributes, not inline `onClick`.** Inline handlers are NOT supported by the JSX → string runtime; delegate from the root.
143
143
  11. **`arraySignal` is opt-in for long keyed lists** where most updates are pointwise. For short lists / filter+sort pipelines, plain `signal` + `each(items.value, ...)` is simpler and equally fast.
@@ -219,7 +219,8 @@ const cart = defineStore({
219
219
  // access: cart.state.value.items, cart.actions.add('x'), cart.reset()
220
220
 
221
221
  // Pattern 4: one-shot reconcile (no signals, no effect)
222
- morph(liveCard, '<article class="card">…</article>');
222
+ // the template describes liveCard's CHILDREN; liveCard's own attributes are untouched
223
+ morph(liveCard, '<h2 class="card-title">…</h2><p>…</p>');
223
224
 
224
225
  // Pattern 5: fine-grained binding (opt-in) — pass the signal/computed ITSELF
225
226
  // into a hole so a change updates ONLY that node (no render re-run, no
@@ -276,7 +277,7 @@ mount(
276
277
  | ` html` ``: partial attribute values are not supported | In `kerfjs/html` templates a hole must be the COMPLETE attribute value | Build the full string first (``class="${`a ${b}`}"``), or bind `class="${computed(() => `a ${b.value}`)}"` for a reactive one |
277
278
  | An `each()` list's rows lose focus / scroll / typing state when an unrelated conditional list above them appears or disappears (kerf warns about this in dev) | Lists without a key are identified by their position among the render's `each()` calls, so adding/removing one above shifts this list's identity and kerf rebuilds it | Give the lists stable keys: `each(items, render, { key: 'results' })`. Keying just the conditional list is usually enough |
278
279
  | Keyed `each()` list suddenly renders zero rows — only its `<!--kf-list:N-->` marker — with no errors, and it never recovers (kerfjs ≤ 2.0.1) | A conditionally-rendered sibling BEFORE the list (possibly higher in the tree, e.g. an error banner) was removed that render; older kerfjs rebuilt the shifted list container from the template, permanently detaching the list's internal binding | Upgrade kerfjs (fixed after 2.0.1 — the morph now moves the shifted container up in place, keeping node identity). On older versions, keep the structure before the list stable: wrap the conditional in an always-present container (`<div class="banners">{cond ? <div/> : ''}</div>`) |
279
- | A numbered / zebra-striped / "N of M" `each()` list shows the wrong number on rows that MOVED (reorder, or non-tail insert/remove), while unmoved rows look right | The render fn's `index` argument is NOT part of the memo key (only item identity + `cacheKey` + content version are), so a row that keeps identity but changes position keeps the HTML it rendered at its old index | Fold the index into the memo key so displaced rows re-render: `each(items, (it, i) => …, { cacheKey: (_, i) => i })` (add `key` if used). Opt-in dev warn: `KERF_DEV_WARN_STALE_INDEX=1` |
280
+ | A numbered / zebra-striped / "N of M" `each()` list shows the wrong number on rows that MOVED (reorder, or non-tail insert/remove), while unmoved rows look right | The render fn's `index` argument is NOT part of the memo key (only item identity + `cacheKey` + content version are), so a row that keeps identity but changes position keeps the HTML it rendered at its old index | Fold the index into the memo key so displaced rows re-render: `each(items, (it, i) => …, { cacheKey: (_, i) => i })` (add `key` if used). Opt-in dev warn: `enableWarnings({ staleIndex: true })` (or `KERF_DEV_WARN_STALE_INDEX=1` under Node) |
280
281
 
281
282
  ## Workflow guidance
282
283
 
package/dist/actions.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { attr } from './chunk-MK42GLPV.js';
2
- import { delegate } from './chunk-E5R5GNKE.js';
2
+ import { delegate } from './chunk-X6OQ2HTR.js';
3
3
 
4
4
 
5
5
  // src/actions.ts
@@ -14,8 +14,8 @@ function delegateActions(root, eventType, table, options) {
14
14
  eventType,
15
15
  `[${attrName}]`,
16
16
  (event, el) => {
17
- const handler = table[el.getAttribute(attrName)];
18
- if (handler !== void 0) handler(event, el);
17
+ const value = el.getAttribute(attrName);
18
+ if (Object.hasOwn(table, value)) table[value](event, el);
19
19
  },
20
20
  options
21
21
  );
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/actions.ts"],"names":[],"mappings":";;;;;AAmCA,IAAM,mBAAA,GAAsB,aAAA;AAkBrB,SAAS,OAAyB,KAAA,EAAsC;AAC7E,EAAA,OAAO,IAAA,CAAK,qBAAqB,KAAK,CAAA;AACxC;AAwBO,SAAS,eAAA,CACd,IAAA,EACA,SAAA,EACA,KAAA,EACA,OAAA,EACY;AACZ,EAAA,MAAM,QAAA,GAAW,SAAS,IAAA,IAAQ,mBAAA;AAClC,EAAA,OAAO,QAAA;AAAA,IACL,IAAA;AAAA,IACA,SAAA;AAAA,IACA,IAAI,QAAQ,CAAA,CAAA,CAAA;AAAA,IACZ,CAAC,OAAO,EAAA,KAAO;AAEb,MAAA,MAAM,OAAA,GAAU,KAAA,CAAM,EAAA,CAAG,YAAA,CAAa,QAAQ,CAAW,CAAA;AACzD,MAAA,IAAI,OAAA,KAAY,MAAA,EAAW,OAAA,CAAQ,KAAA,EAAO,EAAE,CAAA;AAAA,IAC9C,CAAA;AAAA,IACA;AAAA,GACF;AACF","file":"actions.js","sourcesContent":["/**\n * `kerfjs/actions` — the delegated action-table helper.\n *\n * The most-reinvented idiom across real kerf apps: one table of `data-action`\n * attribute specs used as the single source of truth for BOTH the JSX attribute\n * and the delegate selector, plus a hand-rolled `switch (dataset.action)`\n * dispatcher. This subpath blesses it as two thin helpers over the existing\n * `attr()` + `delegate()` — it does NOT replace them.\n *\n * import { action, delegateActions } from 'kerfjs/actions';\n *\n * const A = {\n * select: action('select-file'),\n * remove: action('remove-file'),\n * };\n *\n * // JSX — spread the attr (rename-safe; no hardcoded attribute name):\n * // <button {...A.select.attrs} data-id={id}>…</button>\n *\n * // Wire the whole table with ONE delegated listener; returns a disposer:\n * const dispose = delegateActions(root, 'click', {\n * [A.select.value]: (_e, el) => selectFile(el.getAttribute('data-id')),\n * [A.remove.value]: (_e, el) => removeFile(el.getAttribute('data-id')),\n * });\n *\n * Contract: `delegateActions` returns a `() => void` disposer and holds no\n * per-instance state — the same shape as `delegate()`, which it builds on (so\n * it inherits the single-listener dispatch and the capture auto-promotion for\n * well-known non-bubbling event types). One event type per call, mirroring\n * `delegate()`; collect the disposers for a root that needs several.\n */\nimport { attr, type AttrSpec } from './attr.js';\nimport { delegate, type DelegateOptions } from './delegate.js';\n\n/** The attribute an action table keys on by default. */\nconst DEFAULT_ACTION_ATTR = 'data-action';\n\n/**\n * A handler in a {@link delegateActions} table. Receives the DOM event and the\n * matched element (walk-up `closest()` match by default) — the same shape as a\n * `delegate()` handler.\n */\nexport type ActionHandler<E extends Element = Element> = (\n event: Event,\n el: E,\n) => void;\n\n/**\n * `action(value)` — an {@link AttrSpec} on `data-action`. A thin specialization\n * of `attr('data-action', value)`: spread its `.attrs` in JSX and use its\n * `.value` as the handler-table key, so the action name lives in exactly one\n * place and can't drift between the markup and the dispatcher.\n */\nexport function action<V extends string>(value: V): AttrSpec<'data-action', V> {\n return attr(DEFAULT_ACTION_ATTR, value);\n}\n\n/** Options for {@link delegateActions}. Extends {@link DelegateOptions}. */\nexport interface DelegateActionsOptions extends DelegateOptions {\n /**\n * The attribute the table keys on. Default `'data-action'`. Override it only\n * if you also author the specs with `attr(yourName, …)` instead of `action()`.\n */\n attr?: string;\n}\n\n/**\n * Wire a whole table of action handlers with ONE delegated listener.\n *\n * On `eventType`, the nearest element carrying the action attribute (walk-up\n * `closest()` by default; pass `{ match: 'direct' }` for an exact-element match)\n * is looked up in `table` by its attribute value, and the matching handler\n * runs. An element whose action is absent from the table is ignored — the same\n * behavior as a `switch (dataset.action)` with no matching `case`.\n *\n * Returns a `() => void` disposer. One event type per call (the smallest\n * surface, mirroring `delegate()`); collect the disposers when a root needs\n * several event types.\n */\nexport function delegateActions<E extends Element = Element>(\n root: HTMLElement,\n eventType: string,\n table: Readonly<Record<string, ActionHandler<E>>>,\n options?: DelegateActionsOptions,\n): () => void {\n const attrName = options?.attr ?? DEFAULT_ACTION_ATTR;\n return delegate<E>(\n root,\n eventType,\n `[${attrName}]`,\n (event, el) => {\n // `el` matched `[${attrName}]`, so the attribute is always present.\n const handler = table[el.getAttribute(attrName) as string];\n if (handler !== undefined) handler(event, el);\n },\n options,\n );\n}\n"]}
1
+ {"version":3,"sources":["../src/actions.ts"],"names":[],"mappings":";;;;;AAmCA,IAAM,mBAAA,GAAsB,aAAA;AAkBrB,SAAS,OAAyB,KAAA,EAAsC;AAC7E,EAAA,OAAO,IAAA,CAAK,qBAAqB,KAAK,CAAA;AACxC;AAwBO,SAAS,eAAA,CACd,IAAA,EACA,SAAA,EACA,KAAA,EACA,OAAA,EACY;AACZ,EAAA,MAAM,QAAA,GAAW,SAAS,IAAA,IAAQ,mBAAA;AAClC,EAAA,OAAO,QAAA;AAAA,IACL,IAAA;AAAA,IACA,SAAA;AAAA,IACA,IAAI,QAAQ,CAAA,CAAA,CAAA;AAAA,IACZ,CAAC,OAAO,EAAA,KAAO;AAEb,MAAA,MAAM,KAAA,GAAQ,EAAA,CAAG,YAAA,CAAa,QAAQ,CAAA;AAKtC,MAAA,IAAI,MAAA,CAAO,OAAO,KAAA,EAAO,KAAK,GAAG,KAAA,CAAM,KAAK,CAAA,CAAE,KAAA,EAAO,EAAE,CAAA;AAAA,IACzD,CAAA;AAAA,IACA;AAAA,GACF;AACF","file":"actions.js","sourcesContent":["/**\n * `kerfjs/actions` — the delegated action-table helper.\n *\n * The most-reinvented idiom across real kerf apps: one table of `data-action`\n * attribute specs used as the single source of truth for BOTH the JSX attribute\n * and the delegate selector, plus a hand-rolled `switch (dataset.action)`\n * dispatcher. This subpath blesses it as two thin helpers over the existing\n * `attr()` + `delegate()` — it does NOT replace them.\n *\n * import { action, delegateActions } from 'kerfjs/actions';\n *\n * const A = {\n * select: action('select-file'),\n * remove: action('remove-file'),\n * };\n *\n * // JSX — spread the attr (rename-safe; no hardcoded attribute name):\n * // <button {...A.select.attrs} data-id={id}>…</button>\n *\n * // Wire the whole table with ONE delegated listener; returns a disposer:\n * const dispose = delegateActions(root, 'click', {\n * [A.select.value]: (_e, el) => selectFile(el.getAttribute('data-id')),\n * [A.remove.value]: (_e, el) => removeFile(el.getAttribute('data-id')),\n * });\n *\n * Contract: `delegateActions` returns a `() => void` disposer and holds no\n * per-instance state — the same shape as `delegate()`, which it builds on (so\n * it inherits the single-listener dispatch and the capture auto-promotion for\n * well-known non-bubbling event types). One event type per call, mirroring\n * `delegate()`; collect the disposers for a root that needs several.\n */\nimport { attr, type AttrSpec } from './attr.js';\nimport { delegate, type DelegateOptions } from './delegate.js';\n\n/** The attribute an action table keys on by default. */\nconst DEFAULT_ACTION_ATTR = 'data-action';\n\n/**\n * A handler in a {@link delegateActions} table. Receives the DOM event and the\n * matched element (walk-up `closest()` match by default) — the same shape as a\n * `delegate()` handler.\n */\nexport type ActionHandler<E extends Element = Element> = (\n event: Event,\n el: E,\n) => void;\n\n/**\n * `action(value)` — an {@link AttrSpec} on `data-action`. A thin specialization\n * of `attr('data-action', value)`: spread its `.attrs` in JSX and use its\n * `.value` as the handler-table key, so the action name lives in exactly one\n * place and can't drift between the markup and the dispatcher.\n */\nexport function action<V extends string>(value: V): AttrSpec<'data-action', V> {\n return attr(DEFAULT_ACTION_ATTR, value);\n}\n\n/** Options for {@link delegateActions}. Extends {@link DelegateOptions}. */\nexport interface DelegateActionsOptions extends DelegateOptions {\n /**\n * The attribute the table keys on. Default `'data-action'`. Override it only\n * if you also author the specs with `attr(yourName, …)` instead of `action()`.\n */\n attr?: string;\n}\n\n/**\n * Wire a whole table of action handlers with ONE delegated listener.\n *\n * On `eventType`, the nearest element carrying the action attribute (walk-up\n * `closest()` by default; pass `{ match: 'direct' }` for an exact-element match)\n * is looked up in `table` by its attribute value, and the matching handler\n * runs. An element whose action is absent from the table is ignored — the same\n * behavior as a `switch (dataset.action)` with no matching `case`.\n *\n * Returns a `() => void` disposer. One event type per call (the smallest\n * surface, mirroring `delegate()`); collect the disposers when a root needs\n * several event types.\n */\nexport function delegateActions<E extends Element = Element>(\n root: HTMLElement,\n eventType: string,\n table: Readonly<Record<string, ActionHandler<E>>>,\n options?: DelegateActionsOptions,\n): () => void {\n const attrName = options?.attr ?? DEFAULT_ACTION_ATTR;\n return delegate<E>(\n root,\n eventType,\n `[${attrName}]`,\n (event, el) => {\n // `el` matched `[${attrName}]`, so the attribute is always present.\n const value = el.getAttribute(attrName) as string;\n // Own entries only: the table is an ordinary object, so a bare\n // `table[value]` would resolve Object.prototype names (`__proto__`,\n // `constructor`, `toString`, `hasOwnProperty`, …) and dispatch or throw\n // on markup that names no action in the table.\n if (Object.hasOwn(table, value)) table[value](event, el);\n },\n options,\n );\n}\n"]}
@@ -1,4 +1,4 @@
1
- export { ARRAY_SIGNAL_BRAND, ArraySignal, arraySignal } from './chunk-ELXVRKY2.js';
1
+ export { ARRAY_SIGNAL_BRAND, ArraySignal, arraySignal } from './chunk-H7WTRM6F.js';
2
2
 
3
3
 
4
4
 
package/dist/async.js CHANGED
@@ -1,4 +1,4 @@
1
- import { signal } from './chunk-Y2FOYPBV.js';
1
+ import { signal } from './chunk-IHMDBBUJ.js';
2
2
 
3
3
 
4
4
  // src/async.ts
package/dist/attach.js CHANGED
@@ -1,70 +1,3 @@
1
- // src/attach.ts
2
- function attach(node, setup) {
3
- const teardown = setup(node);
4
- let done = false;
5
- let hasConnected = node.isConnected;
6
- let observedRoot;
7
- let connectionFrame = 0;
8
- const finish = () => {
9
- if (done) return;
10
- done = true;
11
- globalThis.cancelAnimationFrame(connectionFrame);
12
- observer.disconnect();
13
- if (typeof teardown === "function") teardown();
14
- };
15
- const addedWithin = (candidate) => {
16
- let current = node;
17
- while (current !== null) {
18
- if (current === candidate) return true;
19
- const root = current.getRootNode();
20
- current = current.parentNode ?? (root instanceof ShadowRoot ? root.host : null);
21
- }
22
- return false;
23
- };
24
- const observer = new MutationObserver((records) => {
25
- if (done) return;
26
- if (!hasConnected) {
27
- if (!node.isConnected) {
28
- const connectedAndRemovedInBatch = records.some(
29
- (record) => [...record.addedNodes].some(addedWithin)
30
- );
31
- if (!connectedAndRemovedInBatch) return;
32
- hasConnected = true;
33
- finish();
34
- return;
35
- }
36
- hasConnected = true;
37
- globalThis.cancelAnimationFrame(connectionFrame);
38
- } else if (!node.isConnected) {
39
- finish();
40
- return;
41
- }
42
- const root = node.getRootNode();
43
- if (root !== observedRoot) observe(root);
44
- });
45
- const observe = (root) => {
46
- observer.disconnect();
47
- observedRoot = root;
48
- const options = { childList: true, subtree: true };
49
- observer.observe(node.ownerDocument, options);
50
- if (root !== node.ownerDocument) observer.observe(root, options);
51
- };
52
- const watchForConnection = () => {
53
- connectionFrame = globalThis.requestAnimationFrame(() => {
54
- if (done || hasConnected) return;
55
- if (node.isConnected) {
56
- hasConnected = true;
57
- observe(node.getRootNode());
58
- return;
59
- }
60
- watchForConnection();
61
- });
62
- };
63
- observe(hasConnected ? node.getRootNode() : node.ownerDocument);
64
- if (!hasConnected) watchForConnection();
65
- return finish;
66
- }
67
-
68
- export { attach };
1
+ export { attach } from './chunk-FM6N2B4N.js';
69
2
 
70
3
  //# sourceMappingURL=attach.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/attach.ts"],"names":[],"mappings":"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","file":"attach.js","sourcesContent":["/**\n * `kerfjs/attach` — bind a non-kerf widget's lifecycle to a single DOM node.\n *\n * `data-morph-skip` lets a library own a subtree so kerf won't touch it — but\n * nothing manages that widget's LIFECYCLE. You set it up imperatively after\n * render and must remember to tear it down when the node is replaced/removed\n * (dropping document-level listeners the widget added, etc.). `attach` closes\n * that seam: run a setup against one **existing** DOM node and auto-run its\n * teardown when that node leaves the document.\n *\n * import { attach } from 'kerfjs/attach';\n *\n * attach(canvasEl, (el) => {\n * const chart = D3.mount(el);\n * return () => chart.destroy(); // runs when el leaves the DOM (or on dispose)\n * });\n *\n * `setup(node)` runs immediately — the node already exists, so there is nothing\n * to wait for (this is NOT React's `useEffect`: no dependency array, no re-run,\n * no render-phase or hook-order scoping; it is closer to a Web Component's\n * `connectedCallback`/`disconnectedCallback` pair, Svelte's\n * `onMount(() => () => cleanup)`, or Solid's `onCleanup`). A node may start\n * disconnected: setup still runs now, and teardown waits until it has first\n * connected and subsequently leaves the document. The returned teardown runs\n * once — whichever comes first — on that removal (detected by a\n * `MutationObserver`, so a morph swap, a `remountOn` replacement, or any removal\n * triggers it) or when the returned disposer is called. Re-creation is NOT\n * handled here: a fresh node is a fresh `attach()` call — pair it with\n * `kerfjs/remount`, which replaces the node and re-runs your render (and thus\n * this call) on the new one.\n *\n * Related: `kerfjs/scope`'s `observeRemovals` also auto-disposes on removal via a\n * `MutationObserver`, but scoped to a whole subtree's registered disposers rather\n * than one node's setup/teardown pair — reach for that when you're collecting\n * many disposers under an element, and for `attach` when you're binding one\n * widget's lifecycle to one node.\n */\n\n/** The setup callback for {@link attach}: run against `node`, optionally return a teardown. */\nexport type AttachSetup = (node: Element) => (() => void) | void;\n\n/**\n * Run `setup(node)` now, and its returned teardown once — after `node` has been\n * connected and then leaves the document, or when the returned disposer is\n * called, whichever is first. Returns an idempotent disposer so a `mount()` /\n * `Scope` can drive teardown explicitly.\n */\nexport function attach(node: Element, setup: AttachSetup): () => void {\n const teardown = setup(node);\n let done = false;\n let hasConnected = node.isConnected;\n let observedRoot: Node | undefined;\n let connectionFrame = 0;\n\n const finish = (): void => {\n if (done) return;\n done = true;\n globalThis.cancelAnimationFrame(connectionFrame);\n observer.disconnect();\n if (typeof teardown === 'function') teardown();\n };\n\n const addedWithin = (candidate: Node): boolean => {\n let current: Node | null = node;\n while (current !== null) {\n if (current === candidate) return true;\n const root = current.getRootNode();\n current =\n current.parentNode ?? (root instanceof ShadowRoot ? root.host : null);\n }\n return false;\n };\n\n const observer = new MutationObserver((records) => {\n if (done) return;\n\n if (!hasConnected) {\n // A node may be prepared before insertion. Its own detached root cannot\n // observe becoming someone else's child, so wait on ownerDocument until\n // it becomes connected; only a later disconnection is a teardown event.\n if (!node.isConnected) {\n const connectedAndRemovedInBatch = records.some((record) =>\n [...record.addedNodes].some(addedWithin),\n );\n if (!connectedAndRemovedInBatch) return;\n hasConnected = true;\n finish();\n return;\n }\n hasConnected = true;\n globalThis.cancelAnimationFrame(connectionFrame);\n } else if (!node.isConnected) {\n finish();\n return;\n }\n\n // A connected node can move between documents or shadow roots without\n // ending its lifecycle. Follow its live root so a later removal remains\n // observable after that move.\n const root = node.getRootNode();\n if (root !== observedRoot) observe(root);\n });\n\n const observe = (root: Node): void => {\n observer.disconnect();\n observedRoot = root;\n const options = { childList: true, subtree: true } as const;\n observer.observe(node.ownerDocument, options);\n // Document observation sees a shadow host leave the document, while shadow\n // observation sees the node (or one of its in-shadow ancestors) removed.\n // Both are required to cover the node's composed lifecycle.\n if (root !== node.ownerDocument) observer.observe(root, options);\n };\n\n const watchForConnection = (): void => {\n connectionFrame = globalThis.requestAnimationFrame(() => {\n if (done || hasConnected) return;\n if (node.isConnected) {\n hasConnected = true;\n observe(node.getRootNode());\n return;\n }\n watchForConnection();\n });\n };\n\n // Connected nodes are watched from their live document/shadow root. For a\n // disconnected node, watch ownerDocument plus animation frames: the document\n // catches ordinary insertion, while the frame check also catches direct\n // insertion across a shadow boundary that document observation cannot see.\n observe(hasConnected ? node.getRootNode() : node.ownerDocument);\n if (!hasConnected) watchForConnection();\n\n return finish;\n}\n"]}
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"attach.js"}
@@ -11,4 +11,4 @@ function clearDevHooks() {
11
11
 
12
12
  export { clearDevHooks, devHooks, installDevHooks };
13
13
 
14
- //# sourceMappingURL=chunk-CEQMZYLR.js.map
14
+ //# sourceMappingURL=chunk-3C6F6NM4.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/dev-hooks.ts"],"names":[],"mappings":";AAwJO,IAAM,WAAqB;AAS3B,SAAS,gBAAgB,KAAA,EAAuB;AACrD,EAAA,MAAA,CAAO,MAAA,CAAO,UAAU,KAAK,CAAA;AAC/B;AAOO,SAAS,aAAA,GAAsB;AACpC,EAAA,KAAA,MAAW,GAAA,IAAO,MAAA,CAAO,IAAA,CAAK,QAAQ,CAAA,EAAG;AACvC,IAAA,OAAQ,SAAqC,GAAG,CAAA;AAAA,EAClD;AACF","file":"chunk-CEQMZYLR.js","sourcesContent":["/**\n * The dev-hook registry — kerf's single seam between production code and the\n * opt-in development diagnostics.\n *\n * ## Why this exists\n *\n * kerf used to INFER whether it was running in development, by reading\n * `globalThis.process?.env?.NODE_ENV` through `utils/devMode.ts`. That\n * inference cannot be made correct, and it was wrong in the most common case:\n * bundlers substitute the BARE `process.env.NODE_ENV` token and never create a\n * `globalThis.process` object for browser targets, so the read returned\n * `undefined` and `undefined !== 'production'` resolved to DEVELOPMENT inside\n * production browser bundles.\n *\n * It also could not be fixed by rewriting the expression. Only the\n * *production* answer can be made static: `X && false` folds to a constant for\n * any side-effect-free `X`, while `X && true` does not. So any form that a\n * bundler can eliminate is also a form that treats \"no `process` binding\" as\n * production — which silently disables every warning in the no-build/CDN path\n * and in browser dev bundles.\n *\n * The fix is to stop guessing. Every environment already has a correct,\n * statically-foldable dev flag; what none of them offers is a way to hand that\n * flag to a *library*. So the consumer writes the conditional, in their own\n * code, with their own flag:\n *\n * ```js\n * if (import.meta.env.DEV) await import('kerfjs/dev'); // Vite\n * if (process.env.NODE_ENV !== 'production') await import('kerfjs/dev');\n * ```\n *\n * Because that condition folds to `false` in the consumer's production build,\n * the entire statement is eliminated and the dev chunk is never emitted or\n * fetched. Installation IS the development signal — there is nothing left to\n * detect, and no environment kerf can be wrong about.\n *\n * ## The contract\n *\n * Core modules never import a `dev-*` module. They read a nullable slot off\n * `devHooks` and call through it:\n *\n * ```ts\n * devHooks.listRebind?.(id, marker.parentElement as Element);\n * ```\n *\n * When nothing is installed every slot is `undefined`, so the cost is one\n * property read per call site and the `dev-*` modules are unreachable from the\n * main entry — which is what lets a bundler drop them. This is why the gate\n * lives at the CALL SITE rather than inside each warner: an unconditional call\n * into a self-gating warner keeps the module reachable no matter how the gate\n * is written, so no amount of dead-code elimination can reclaim it.\n *\n * Slots ending in `Enabled` are predicates rather than warnings. They exist for\n * the handful of call sites that must decide whether to do *expensive\n * preparatory work* — capturing the previous render's binding list, allocating\n * a per-render `Map` — before there is anything to warn about. Core must check\n * those before paying the cost, exactly as it checked the old `isOptedIn()`\n * exports.\n *\n * Each opt-in warner keeps its own internal switch check through `devFlag()`;\n * an `enableWarnings()` override wins over the matching `KERF_DEV_WARN_*`\n * environment variable. Installation decides whether the diagnostics are\n * *present*; the opt-in warner decides whether it is *switched on*. Always-on\n * hooks skip that second gate.\n *\n * @see docs/11-dev-warnings.md\n */\n\nimport type { Binding } from './bindings.js';\nimport type { ListBinding } from './list-binding.js';\nimport type { Signal } from './reactive.js';\n\n/** Per-mount / per-store one-shot dedup flag, owned by the caller in core. */\nexport interface WarnOnceContext {\n warned: boolean;\n}\n\nexport interface DevHooks {\n // --- reactive.ts -------------------------------------------------------\n /**\n * Replaces `signal()`'s constructor so writes to never-subscribed signals can\n * warn. Resolved at signal-CREATION time, so signals created before the dev\n * entry is installed stay plain — see `signalsCreatedBeforeInstall`.\n */\n signalFactory?: <T>(value: T) => Signal<T>;\n /**\n * Wraps an `effect()` body so `delegate()` can detect that it is running\n * inside one. Returns the body to actually run.\n */\n wrapEffect?: (fn: () => void | (() => void)) => () => void | (() => void);\n\n // --- delegate.ts -------------------------------------------------------\n delegateInEffect?: (fn: 'delegate' | 'delegateCapture') => void;\n\n // --- store.ts ----------------------------------------------------------\n narrowSet?: (prev: unknown, next: unknown, ctx: WarnOnceContext) => void;\n /** Deep read-only proxy for the `get()` snapshot, so stray writes throw. */\n storeReadonly?: <T extends object>(state: T) => T;\n /** Unwraps a proxy handed back through `set({ ...get() })`. */\n storeToRaw?: <T>(next: T) => T;\n\n // --- mount.ts ----------------------------------------------------------\n listenerRebuild?: (rootEl: Element) => MutationObserver | null;\n listIdShift?: (id: string) => void;\n parserRepair?: (html: string) => void;\n staleBindingEnabled?: () => boolean;\n staleBinding?: (\n prevWired: readonly Binding[],\n current: readonly Binding[],\n ) => void;\n listInvariantsEnabled?: () => boolean;\n listInvariants?: (\n rootEl: Element,\n bindings: ReadonlyMap<string, ListBinding>,\n expectedCounts?: ReadonlyMap<string, number>,\n ) => void;\n valueOnlyRerender?: (\n prevHtml: string,\n nextHtml: string,\n ctx: WarnOnceContext,\n ) => void;\n listRebind?: (id: string, liveParent: Element) => void;\n eachInMorphSkip?: (id: string, liveParent: Element, rootEl: Element) => void;\n missingRowKey?: (\n rowEl: Element,\n rowHtml: string,\n binding: { warnedMissingKey?: boolean },\n ) => void;\n\n // --- each.ts -----------------------------------------------------------\n staleIndexEnabled?: () => boolean;\n staleIndex?: (id: string) => void;\n duplicateCacheKeys?: (\n id: string,\n segItems: readonly { cacheKey: unknown }[],\n ) => void;\n\n // --- utils/url-screen.ts -----------------------------------------------\n /**\n * When installed, a screened URL throws instead of warning-and-dropping.\n * A slot rather than a boolean so the check stays uniform with the rest.\n */\n urlScreenThrow?: (message: string) => never;\n}\n\n/**\n * The live slot table. Mutable by design — this is the fourth sanctioned\n * module-level mutable location (Design rule 5), and like `store.ts:REGISTRY`\n * it depends on there being exactly ONE copy at runtime. `tsup`'s\n * `splitting: true` guarantees that: shared modules are promoted into a single\n * chunk that both the main entry and the `kerfjs/dev` entry import.\n */\nexport const devHooks: DevHooks = {};\n\n/**\n * Install (or extend) the dev hooks. Called by the `kerfjs/dev` entry; not part\n * of the public API surface.\n *\n * Merges rather than replaces, so a consumer can install the standard bundle\n * and then override a single slot in a test.\n */\nexport function installDevHooks(hooks: DevHooks): void {\n Object.assign(devHooks, hooks);\n}\n\n/**\n * Remove every installed hook. Exists for test isolation — a suite that asserts\n * the not-installed (production-shaped) path needs to get back to a clean slate\n * without reloading modules.\n */\nexport function clearDevHooks(): void {\n for (const key of Object.keys(devHooks)) {\n delete (devHooks as Record<string, unknown>)[key];\n }\n}\n"]}
1
+ {"version":3,"sources":["../src/dev-hooks.ts"],"names":[],"mappings":";AAoKO,IAAM,WAAqB;AAS3B,SAAS,gBAAgB,KAAA,EAAuB;AACrD,EAAA,MAAA,CAAO,MAAA,CAAO,UAAU,KAAK,CAAA;AAC/B;AAOO,SAAS,aAAA,GAAsB;AACpC,EAAA,KAAA,MAAW,GAAA,IAAO,MAAA,CAAO,IAAA,CAAK,QAAQ,CAAA,EAAG;AACvC,IAAA,OAAQ,SAAqC,GAAG,CAAA;AAAA,EAClD;AACF","file":"chunk-3C6F6NM4.js","sourcesContent":["/**\n * The dev-hook registry — kerf's single seam between production code and the\n * opt-in development diagnostics.\n *\n * ## Why this exists\n *\n * kerf used to INFER whether it was running in development, by reading\n * `globalThis.process?.env?.NODE_ENV` through `utils/devMode.ts`. That\n * inference cannot be made correct, and it was wrong in the most common case:\n * bundlers substitute the BARE `process.env.NODE_ENV` token and never create a\n * `globalThis.process` object for browser targets, so the read returned\n * `undefined` and `undefined !== 'production'` resolved to DEVELOPMENT inside\n * production browser bundles.\n *\n * It also could not be fixed by rewriting the expression. Only the\n * *production* answer can be made static: `X && false` folds to a constant for\n * any side-effect-free `X`, while `X && true` does not. So any form that a\n * bundler can eliminate is also a form that treats \"no `process` binding\" as\n * production — which silently disables every warning in the no-build/CDN path\n * and in browser dev bundles.\n *\n * The fix is to stop guessing. Every environment already has a correct,\n * statically-foldable dev flag; what none of them offers is a way to hand that\n * flag to a *library*. So the consumer writes the conditional, in their own\n * code, with their own flag:\n *\n * ```js\n * if (import.meta.env.DEV) await import('kerfjs/dev'); // Vite\n * if (process.env.NODE_ENV !== 'production') await import('kerfjs/dev');\n * ```\n *\n * Because that condition folds to `false` in the consumer's production build,\n * the entire statement is eliminated and the dev chunk is never emitted or\n * fetched. Installation IS the development signal — there is nothing left to\n * detect, and no environment kerf can be wrong about.\n *\n * ## The contract\n *\n * Core modules never import a `dev-*` module. They read a nullable slot off\n * `devHooks` and call through it:\n *\n * ```ts\n * devHooks.listRebind?.(id, marker.parentElement as Element);\n * ```\n *\n * When nothing is installed every slot is `undefined`, so the cost is one\n * property read per call site and the `dev-*` modules are unreachable from the\n * main entry — which is what lets a bundler drop them. This is why the gate\n * lives at the CALL SITE rather than inside each warner: an unconditional call\n * into a self-gating warner keeps the module reachable no matter how the gate\n * is written, so no amount of dead-code elimination can reclaim it.\n *\n * Slots ending in `Enabled` are predicates rather than warnings. They exist for\n * the handful of call sites that must decide whether to do *expensive\n * preparatory work* — capturing the previous render's binding list, allocating\n * a per-render `Map` — before there is anything to warn about. Core must check\n * those before paying the cost, exactly as it checked the old `isOptedIn()`\n * exports.\n *\n * Each opt-in warner keeps its own internal switch check through `devFlag()`;\n * an `enableWarnings()` override wins over the matching `KERF_DEV_WARN_*`\n * environment variable. Installation decides whether the diagnostics are\n * *present*; the opt-in warner decides whether it is *switched on*. Always-on\n * hooks skip that second gate.\n *\n * @see docs/11-dev-warnings.md\n */\n\nimport type { Binding } from './bindings.js';\nimport type { ListBinding } from './list-binding.js';\nimport type { Signal } from './reactive.js';\n\n/** Per-mount / per-store one-shot dedup flag, owned by the caller in core. */\nexport interface WarnOnceContext {\n warned: boolean;\n}\n\nexport interface DevHooks {\n // --- reactive.ts -------------------------------------------------------\n /**\n * Replaces `signal()`'s constructor so writes to never-subscribed signals can\n * warn. Resolved at signal-CREATION time, so signals created before the dev\n * entry is installed stay plain — see `signalsCreatedBeforeInstall`.\n */\n signalFactory?: <T>(value: T) => Signal<T>;\n /**\n * Wraps an `effect()` body so `delegate()` can detect that it is running\n * inside one. Returns the body to actually run. Resolved at effect-CREATION\n * time (the delegate-in-effect switch too), so an effect created before the\n * install or the opt-in is never wrapped.\n */\n wrapEffect?: (fn: () => void | (() => void)) => () => void | (() => void);\n\n // --- delegate.ts -------------------------------------------------------\n delegateInEffect?: (fn: 'delegate' | 'delegateCapture') => void;\n\n // --- store.ts ----------------------------------------------------------\n narrowSet?: (prev: unknown, next: unknown, ctx: WarnOnceContext) => void;\n /** Deep read-only proxy for the `get()` snapshot, so stray writes throw. */\n storeReadonly?: <T extends object>(state: T) => T;\n /** Unwraps a proxy handed back through `set({ ...get() })`. */\n storeToRaw?: <T>(next: T) => T;\n\n // --- mount.ts ----------------------------------------------------------\n /**\n * Attaches the rebuilt-listeners observer to a mount root. Called once per\n * `mount()`, at creation — and it returns `null` when the warning is switched\n * off then — so a mount created before the install or the opt-in is never\n * observed.\n */\n listenerRebuild?: (rootEl: Element) => MutationObserver | null;\n listIdShift?: (id: string) => void;\n parserRepair?: (html: string) => void;\n staleBindingEnabled?: () => boolean;\n staleBinding?: (\n prevWired: readonly Binding[],\n current: readonly Binding[],\n ) => void;\n listInvariantsEnabled?: () => boolean;\n listInvariants?: (\n rootEl: Element,\n bindings: ReadonlyMap<string, ListBinding>,\n expectedCounts?: ReadonlyMap<string, number>,\n ) => void;\n valueOnlyRerender?: (\n prevHtml: string,\n nextHtml: string,\n ctx: WarnOnceContext,\n ) => void;\n listRebind?: (id: string, liveParent: Element) => void;\n eachInMorphSkip?: (id: string, liveParent: Element, rootEl: Element) => void;\n missingRowKey?: (\n rowEl: Element,\n rowHtml: string,\n binding: { warnedMissingKey?: boolean },\n ) => void;\n\n // --- each.ts -----------------------------------------------------------\n staleIndexEnabled?: () => boolean;\n staleIndex?: (id: string) => void;\n duplicateCacheKeys?: (\n id: string,\n segItems: readonly { cacheKey: unknown }[],\n ) => void;\n\n // --- overlay-core.ts ---------------------------------------------------\n /** A surface opened over a modal `<dialog>` could not be made fully usable. */\n overlayBlockedByModal?: (reason: 'hidden' | 'inert') => void;\n\n // --- utils/url-screen.ts -----------------------------------------------\n /**\n * When installed, a screened URL throws instead of warning-and-dropping.\n * A slot rather than a boolean so the check stays uniform with the rest.\n */\n urlScreenThrow?: (message: string) => never;\n}\n\n/**\n * The live slot table. Mutable by design — this is the fourth sanctioned\n * module-level mutable location (Design rule 5), and like `store.ts:REGISTRY`\n * it depends on there being exactly ONE copy at runtime. `tsup`'s\n * `splitting: true` guarantees that: shared modules are promoted into a single\n * chunk that both the main entry and the `kerfjs/dev` entry import.\n */\nexport const devHooks: DevHooks = {};\n\n/**\n * Install (or extend) the dev hooks. Called by the `kerfjs/dev` entry; not part\n * of the public API surface.\n *\n * Merges rather than replaces, so a consumer can install the standard bundle\n * and then override a single slot in a test.\n */\nexport function installDevHooks(hooks: DevHooks): void {\n Object.assign(devHooks, hooks);\n}\n\n/**\n * Remove every installed hook. Exists for test isolation — a suite that asserts\n * the not-installed (production-shaped) path needs to get back to a clean slate\n * without reloading modules.\n */\nexport function clearDevHooks(): void {\n for (const key of Object.keys(devHooks)) {\n delete (devHooks as Record<string, unknown>)[key];\n }\n}\n"]}
@@ -69,6 +69,12 @@ function wrapWithTags(child, openTag, closeTag) {
69
69
  };
70
70
  }
71
71
 
72
- export { LIST_MARKER_PREFIX, collectLists, flatten, flattenWithoutListItems, mergeChildSegments, wrapWithTags };
72
+ // src/utils/overlay-host.ts
73
+ var OVERLAY_HOST_SELECTOR = "[data-kerf-overlay-host][data-morph-skip]";
74
+ function isOverlayHost(el) {
75
+ return el.matches(OVERLAY_HOST_SELECTOR);
76
+ }
77
+
78
+ export { LIST_MARKER_PREFIX, OVERLAY_HOST_SELECTOR, collectLists, flatten, flattenWithoutListItems, isOverlayHost, mergeChildSegments, wrapWithTags };
73
79
 
74
- //# sourceMappingURL=chunk-BDX3R4OM.js.map
80
+ //# sourceMappingURL=chunk-4TX6YJDM.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/segment.ts","../src/utils/overlay-host.ts"],"names":[],"mappings":"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","file":"chunk-4TX6YJDM.js","sourcesContent":["/**\n * `Segment` — kerf's structured render output.\n *\n * The JSX runtime emits a `SafeHtml` wrapping a `Segment`. Most renders\n * produce a single static segment (just an HTML string), which behaves\n * exactly like a string for backward compatibility. When the tree\n * contains a list (`each()`) or a parent whose children include a list,\n * the runtime emits a structured segment that `mount()` can dispatch\n * on — running its native keyed reconciler for the list parts and\n * leaving the static surrounds to the general-purpose diff.\n *\n * Why have a structured form at all: the perf bottleneck for huge\n * keyed lists isn't the per-row JSX work (which `each()` already\n * memoizes). It's that flattening every render's whole tree to one\n * big HTML string forces a full `innerHTML` parse and a tree walk\n * over rows we know are unchanged. The segment shape lets mount()\n * skip both for the list parts.\n */\n\nimport type { Binding } from './bindings.js';\n\nexport type Segment = StaticSegment | ListSegment | MixedSegment;\n\nexport interface StaticSegment {\n kind: 'static';\n html: string;\n}\n\nexport interface ListItem {\n /**\n * The row's object identity. Used by the reconciler to match new items\n * against live DOM nodes across renders. Unchanged ref → reuse the\n * existing live node; replaced ref → build a fresh node.\n */\n ref: object;\n /**\n * KF-294: the row's fine-grained binding specs (signals in row attrs/text).\n * Undefined for granular-path rows (which snapshot in this spike). The\n * snapshot reconciler wires these to the fresh row node and disposes them\n * when the row is removed.\n */\n bindings?: Binding[];\n /**\n * Optional cache-invalidation key that captures external state affecting\n * this row's render (e.g. selection class). Different cacheKey on the\n * same `ref` triggers a cache miss for that row. `undefined` when the\n * user didn't pass a `key` callback to `each()`.\n */\n cacheKey: unknown;\n html: string;\n}\n\nexport interface ListSegment {\n kind: 'list';\n id: string;\n items: ListItem[];\n /**\n * Optional granular patches (KF-92). When present, the list reconciler\n * applies these directly to the existing binding instead of doing a\n * full classify+reconcile pass. Emitted by `each()` when bound to an\n * `arraySignal`. Mutually exclusive with the `items` snapshot in the\n * sense that the snapshot is treated as informational/fall-back when\n * patches are present.\n */\n patches?: ArrayPatchInternal[];\n /**\n * KF-388: the identity of the data this list renders — the `arraySignal`\n * instance, or `undefined` for a plain array.\n *\n * A list's `id` is its call-order index, so a render that changes how many\n * `each()` calls precede this one hands this segment a DIFFERENT list's\n * binding. Patches are only meaningful against the binding they were queued\n * for, so the reconciler compares this against the binding's recorded source\n * before trusting the patch queue. It is an identity check, not a value\n * check — the instance is never read.\n */\n source?: object;\n}\n\n/**\n * Internal patch shape used inside list segments. Mirrors `ArrayPatch<T>`\n * from `array-signal.ts` but typed against `object` so the segment layer\n * doesn't need to be generic. `update` / `insert` patches carry the row's\n * pre-rendered HTML — `each()` renders them at JSX-evaluation time inside a\n * try/catch so a throwing render falls back to the snapshot path (KF-99)\n * instead of leaving the signal and DOM divergent.\n */\nexport type ArrayPatchInternal =\n | {\n type: 'update';\n index: number;\n item: object;\n html: string;\n bindings?: Binding[];\n }\n | {\n type: 'insert';\n index: number;\n item: object;\n html: string;\n bindings?: Binding[];\n }\n | { type: 'remove'; index: number }\n | { type: 'move'; from: number; to: number }\n | { type: 'replace'; items: readonly object[] };\n\n/**\n * Narrowed patch aliases. Array indexing loses the union discriminant, so\n * the granular reconciler casts through these instead of restating the full\n * object type at every site — a field rename is then a one-place edit.\n */\nexport type UpdatePatch = Extract<ArrayPatchInternal, { type: 'update' }>;\nexport type InsertPatch = Extract<ArrayPatchInternal, { type: 'insert' }>;\n\nexport interface MixedSegment {\n kind: 'mixed';\n parts: Segment[];\n}\n\n/**\n * Flatten a segment to a complete HTML string. Used for first render\n * (bulk innerHTML), for SSR-style consumption via `toString()`, and\n * for diagnostics.\n *\n * If `withMarkers` is set, list segments are wrapped in\n * `<!--kf-list:{id}-->` comments so the post-parse walk can find each\n * list's live parent. Plain (non-marker) flatten is what JSX consumers\n * see when they call `.toString()` on the SafeHtml.\n */\n/**\n * Comment-marker prefix emitted before each list (`<!--kf-list:{id}-->`).\n * `mount()` consumes it when binding lists from the live DOM — the emitter\n * (here) and the consumer must agree byte-for-byte, so both import this one\n * constant. Part of the reserved marker namespace (see `bindings.ts`).\n */\nexport const LIST_MARKER_PREFIX = 'kf-list:';\n\nexport function flatten(segment: Segment, withMarkers: boolean): string {\n if (segment.kind === 'static') return segment.html;\n if (segment.kind === 'list') {\n const items = segment.items.map((i) => i.html).join('');\n return withMarkers\n ? `<!--${LIST_MARKER_PREFIX}${segment.id}-->${items}`\n : items;\n }\n return segment.parts.map((p) => flatten(p, withMarkers)).join('');\n}\n\n/**\n * Variant of `flatten` for the static-only diff path on subsequent\n * renders. Lists are reduced to a single marker comment with no items\n * inside — the actual list children stay in the live DOM and are\n * reconciled separately. Keeping list items out of this string is\n * what makes the morph cheap on huge lists where most rows are\n * unchanged.\n */\nexport function flattenWithoutListItems(segment: Segment): string {\n if (segment.kind === 'static') return segment.html;\n if (segment.kind === 'list')\n return `<!--${LIST_MARKER_PREFIX}${segment.id}-->`;\n return segment.parts.map(flattenWithoutListItems).join('');\n}\n\n/** Collect every `ListSegment` in the tree, keyed by its id. */\nexport function collectLists(\n segment: Segment,\n out: Map<string, ListSegment> = new Map(),\n): Map<string, ListSegment> {\n if (segment.kind === 'list') out.set(segment.id, segment);\n else if (segment.kind === 'mixed') {\n for (const part of segment.parts) collectLists(part, out);\n }\n return out;\n}\n\n/**\n * Combine a list of child segments into the smallest equivalent\n * representation: collapses adjacent statics into one static, returns\n * a single static if everything is static, otherwise a mixed segment\n * with statics coalesced.\n */\nexport function mergeChildSegments(parts: Segment[]): Segment {\n if (parts.length === 0) return { kind: 'static', html: '' };\n if (parts.every((p) => p.kind === 'static')) {\n return {\n kind: 'static',\n html: parts.map((p) => (p as StaticSegment).html).join(''),\n };\n }\n const merged: Segment[] = [];\n let coalesced = '';\n for (const p of parts) {\n if (p.kind === 'static') {\n coalesced += p.html;\n } else {\n if (coalesced !== '') {\n merged.push({ kind: 'static', html: coalesced });\n coalesced = '';\n }\n merged.push(p);\n }\n }\n if (coalesced !== '') merged.push({ kind: 'static', html: coalesced });\n return { kind: 'mixed', parts: merged };\n}\n\n/**\n * Wrap a child segment with surrounding open/close tags from the\n * parent JSX element. Used by the JSX runtime when constructing\n * `_jsx(tag, ...)` output.\n */\nexport function wrapWithTags(\n child: Segment,\n openTag: string,\n closeTag: string,\n): Segment {\n if (child.kind === 'static') {\n return { kind: 'static', html: openTag + child.html + closeTag };\n }\n if (child.kind === 'mixed') {\n return {\n kind: 'mixed',\n parts: [\n { kind: 'static', html: openTag },\n ...child.parts,\n { kind: 'static', html: closeTag },\n ],\n };\n }\n return {\n kind: 'mixed',\n parts: [\n { kind: 'static', html: openTag },\n child,\n { kind: 'static', html: closeTag },\n ],\n };\n}\n","/**\n * The overlay host slot — the one sanctioned nested-mount boundary\n * (KF-FBHQEP: a popover opened over a modal `<dialog>` must render INSIDE the\n * dialog to be interactive, because every engine inerts content outside the\n * modal, yet the dialog is usually already a `mount()` root).\n *\n * An element carrying both `data-kerf-overlay-host` and `data-morph-skip` is a\n * boundary between two mounts: `kerfjs/overlay` mounts its surfaces inside it,\n * and the enclosing mount leaves it alone — its morph skips the subtree\n * (`data-morph-skip`), its nested-mount check stops there, and its binding and\n * list-marker scans never descend into it, so the inner mounts' markers can\n * never be mistaken for the outer mount's own. The opt-in rebuilt-listeners\n * dev observer stops there too: a removal inside the slot is the inner\n * mount's to report.\n */\nexport const OVERLAY_HOST_SELECTOR =\n '[data-kerf-overlay-host][data-morph-skip]';\n\n/** True when `el` is an overlay host slot (see the module comment). */\nexport function isOverlayHost(el: Element): boolean {\n return el.matches(OVERLAY_HOST_SELECTOR);\n}\n"]}
@@ -1,5 +1,5 @@
1
- import { signal } from './chunk-Y2FOYPBV.js';
2
- import { devHooks } from './chunk-CEQMZYLR.js';
1
+ import { signal } from './chunk-IHMDBBUJ.js';
2
+ import { devHooks } from './chunk-3C6F6NM4.js';
3
3
 
4
4
  // src/store.ts
5
5
  var REGISTRY = [];
@@ -44,4 +44,4 @@ function clearStoreRegistry() {
44
44
 
45
45
  export { clearStoreRegistry, defineStore, resetAllStores };
46
46
 
47
- //# sourceMappingURL=chunk-ZOIERTUW.js.map
47
+ //# sourceMappingURL=chunk-BI4HGICV.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/store.ts"],"names":[],"mappings":";;;;AAyCA,IAAM,WAAyC,EAAC;AAEzC,SAAS,YACd,IAAA,EACyB;AACzB,EAAA,MAAM,QAAA,GAA2B,MAAA,CAAO,IAAA,CAAK,OAAA,EAAS,CAAA;AAOtD,EAAA,IAAI,OAAA,GAAkC,IAAA;AAEtC,EAAA,MAAM,GAAA,GAAM,CAAC,IAAA,KAAuB;AAMlC,IAAA,MAAM,QAAQ,QAAA,CAAS,UAAA;AACvB,IAAA,MAAM,GAAA,GAAM,KAAA,GAAQ,KAAA,CAAM,IAAI,CAAA,GAAI,IAAA;AAClC,IAAA,MAAM,YAAY,QAAA,CAAS,SAAA;AAC3B,IAAA,IAAI,SAAA,EAAW;AACb,MAAA,OAAA,KAAY,EAAE,QAAQ,KAAA,EAAM;AAC5B,MAAA,SAAA,CAAU,QAAA,CAAS,KAAA,EAAO,GAAA,EAAK,OAAO,CAAA;AAAA,IACxC;AACA,IAAA,QAAA,CAAS,KAAA,GAAQ,GAAA;AAAA,EACnB,CAAA;AAOA,EAAA,MAAM,MAAM,MAAwB;AAClC,IAAA,MAAM,IAAI,QAAA,CAAS,KAAA;AACnB,IAAA,MAAM,WAAW,QAAA,CAAS,aAAA;AAC1B,IAAA,IAAI,QAAA,IAAY,CAAA,KAAM,IAAA,IAAQ,OAAO,MAAM,QAAA,EAAU;AACnD,MAAA,OAAO,SAAS,CAAoB,CAAA;AAAA,IACtC;AACA,IAAA,OAAO,CAAA;AAAA,EACT,CAAA;AAEA,EAAA,MAAM,OAAA,GAAU,IAAA,CAAK,OAAA,CAAQ,GAAA,EAAK,GAAG,CAAA;AAErC,EAAA,MAAM,KAAA,GAAiC;AAAA,IACrC,KAAA,EAAO,QAAA;AAAA,IACP,OAAA;AAAA,IACA,KAAA,GAAQ;AACN,MAAA,QAAA,CAAS,KAAA,GAAQ,KAAK,OAAA,EAAQ;AAAA,IAChC;AAAA,GACF;AAEA,EAAA,QAAA,CAAS,KAAK,KAAK,CAAA;AACnB,EAAA,OAAO,KAAA;AACT;AAOO,SAAS,cAAA,GAAuB;AACrC,EAAA,KAAA,MAAW,CAAA,IAAK,QAAA,EAAU,CAAA,CAAE,KAAA,EAAM;AACpC;AAMO,SAAS,kBAAA,GAA2B;AACzC,EAAA,QAAA,CAAS,MAAA,GAAS,CAAA;AACpB","file":"chunk-ZOIERTUW.js","sourcesContent":["/**\n * `defineStore({ initial, actions })` — composable testable stores layered on\n * top of `reactive.ts`'s signals.\n *\n * Three rules:\n * 1. `state` is read-only. Consumers read via `state.value` or subscribe via\n * `effect()`. They cannot write directly.\n * 2. `actions` is the only mutation surface. All writes go through named\n * action functions. This is what makes stores testable — assert against\n * actions, not against arbitrary writes.\n * 3. `reset()` resets to `initial()`. Always defined; tests use it for\n * setup, lifecycle hooks (route change, sign-out, etc.) use it for\n * tear-down.\n *\n * A module-level registry tracks every store created via `defineStore()`;\n * `resetAllStores()` walks the registry and calls each `reset()`. Useful for\n * tests + project-switch / logout / route-reset scenarios where every piece\n * of client state should return to its initial shape.\n */\n\nimport { devHooks, type WarnOnceContext } from './dev-hooks.js';\nimport type { ReadonlySignal, Signal } from './reactive.js';\nimport { signal } from './reactive.js';\n\nexport interface Store<TState, TActions> {\n /** Read-only reactive view. Consumers read `state.value` or subscribe via `effect()`. */\n readonly state: ReadonlySignal<TState>;\n /** Named mutators — the only way to change state. */\n readonly actions: TActions;\n /** Reset state to `initial()`. Used by tests and lifecycle hooks. */\n reset(): void;\n}\n\ninterface DefineStoreSpec<TState, TActions> {\n initial: () => TState;\n actions: (\n set: (next: TState) => void,\n get: () => Readonly<TState>,\n ) => TActions;\n}\n\nconst REGISTRY: Array<{ reset: () => void }> = [];\n\nexport function defineStore<TState, TActions>(\n spec: DefineStoreSpec<TState, TActions>,\n): Store<TState, TActions> {\n const internal: Signal<TState> = signal(spec.initial());\n // KF-212: per-store one-shot dedup for the opt-in narrow-set warning.\n // Default off; consumers opt in via `KERF_DEV_WARN_NARROW_SET=1` in dev.\n // Resolve the hook on each set(), so a store created before `kerfjs/dev` is\n // installed starts warning as soon as the diagnostics become available.\n // Allocate the context lazily — production never pays for an object it will\n // never read.\n let warnCtx: WarnOnceContext | null = null;\n\n const set = (next: TState): void => {\n // With `kerfjs/dev` installed, `next` may carry proxies handed back by the\n // `get()` trap (e.g. `set({ ...get(), count: 1 })`). Unwrap them so the\n // internal signal only ever holds a plain object — the narrow-set warning\n // and every consumer read see raw state, never a Proxy. Production stores\n // the bare reference.\n const toRaw = devHooks.storeToRaw;\n const raw = toRaw ? toRaw(next) : next;\n const narrowSet = devHooks.narrowSet;\n if (narrowSet) {\n warnCtx ??= { warned: false };\n narrowSet(internal.value, raw, warnCtx);\n }\n internal.value = raw;\n };\n // With the diagnostics installed, wrap the reference returned to actions in a\n // deep read-only Proxy so that `get().count = 42` / `get().nested.x = 1`\n // (documented Rule 8 violations) throw a `TypeError` instead of silently\n // landing on the underlying state without notifying subscribers. The live\n // state object is never frozen or mutated, so external references to it stay\n // writable. Production returns the bare reference — no proxy, no wrapping.\n const get = (): Readonly<TState> => {\n const v = internal.value;\n const readonly = devHooks.storeReadonly;\n if (readonly && v !== null && typeof v === 'object') {\n return readonly(v as TState & object);\n }\n return v;\n };\n\n const actions = spec.actions(set, get);\n\n const store: Store<TState, TActions> = {\n state: internal,\n actions,\n reset() {\n internal.value = spec.initial();\n },\n };\n\n REGISTRY.push(store);\n return store;\n}\n\n/**\n * Reset every store registered via `defineStore()` to its `initial()` value.\n * Used by tests and by application lifecycle hooks (project switch, logout,\n * route reset).\n */\nexport function resetAllStores(): void {\n for (const s of REGISTRY) s.reset();\n}\n\n/**\n * Test helper — clears the registry. Exposed via the `kerfjs/testing` subpath,\n * not the main `kerfjs` entry. Unit tests use it to isolate stores between cases.\n */\nexport function clearStoreRegistry(): void {\n REGISTRY.length = 0;\n}\n"]}
1
+ {"version":3,"sources":["../src/store.ts"],"names":[],"mappings":";;;;AAyCA,IAAM,WAAyC,EAAC;AAEzC,SAAS,YACd,IAAA,EACyB;AACzB,EAAA,MAAM,QAAA,GAA2B,MAAA,CAAO,IAAA,CAAK,OAAA,EAAS,CAAA;AAOtD,EAAA,IAAI,OAAA,GAAkC,IAAA;AAEtC,EAAA,MAAM,GAAA,GAAM,CAAC,IAAA,KAAuB;AAMlC,IAAA,MAAM,QAAQ,QAAA,CAAS,UAAA;AACvB,IAAA,MAAM,GAAA,GAAM,KAAA,GAAQ,KAAA,CAAM,IAAI,CAAA,GAAI,IAAA;AAClC,IAAA,MAAM,YAAY,QAAA,CAAS,SAAA;AAC3B,IAAA,IAAI,SAAA,EAAW;AACb,MAAA,OAAA,KAAY,EAAE,QAAQ,KAAA,EAAM;AAC5B,MAAA,SAAA,CAAU,QAAA,CAAS,KAAA,EAAO,GAAA,EAAK,OAAO,CAAA;AAAA,IACxC;AACA,IAAA,QAAA,CAAS,KAAA,GAAQ,GAAA;AAAA,EACnB,CAAA;AAOA,EAAA,MAAM,MAAM,MAAwB;AAClC,IAAA,MAAM,IAAI,QAAA,CAAS,KAAA;AACnB,IAAA,MAAM,WAAW,QAAA,CAAS,aAAA;AAC1B,IAAA,IAAI,QAAA,IAAY,CAAA,KAAM,IAAA,IAAQ,OAAO,MAAM,QAAA,EAAU;AACnD,MAAA,OAAO,SAAS,CAAoB,CAAA;AAAA,IACtC;AACA,IAAA,OAAO,CAAA;AAAA,EACT,CAAA;AAEA,EAAA,MAAM,OAAA,GAAU,IAAA,CAAK,OAAA,CAAQ,GAAA,EAAK,GAAG,CAAA;AAErC,EAAA,MAAM,KAAA,GAAiC;AAAA,IACrC,KAAA,EAAO,QAAA;AAAA,IACP,OAAA;AAAA,IACA,KAAA,GAAQ;AACN,MAAA,QAAA,CAAS,KAAA,GAAQ,KAAK,OAAA,EAAQ;AAAA,IAChC;AAAA,GACF;AAEA,EAAA,QAAA,CAAS,KAAK,KAAK,CAAA;AACnB,EAAA,OAAO,KAAA;AACT;AAOO,SAAS,cAAA,GAAuB;AACrC,EAAA,KAAA,MAAW,CAAA,IAAK,QAAA,EAAU,CAAA,CAAE,KAAA,EAAM;AACpC;AAMO,SAAS,kBAAA,GAA2B;AACzC,EAAA,QAAA,CAAS,MAAA,GAAS,CAAA;AACpB","file":"chunk-BI4HGICV.js","sourcesContent":["/**\n * `defineStore({ initial, actions })` — composable testable stores layered on\n * top of `reactive.ts`'s signals.\n *\n * Three rules:\n * 1. `state` is read-only. Consumers read via `state.value` or subscribe via\n * `effect()`. They cannot write directly.\n * 2. `actions` is the only mutation surface. All writes go through named\n * action functions. This is what makes stores testable — assert against\n * actions, not against arbitrary writes.\n * 3. `reset()` resets to `initial()`. Always defined; tests use it for\n * setup, lifecycle hooks (route change, sign-out, etc.) use it for\n * tear-down.\n *\n * A module-level registry tracks every store created via `defineStore()`;\n * `resetAllStores()` walks the registry and calls each `reset()`. Useful for\n * tests + project-switch / logout / route-reset scenarios where every piece\n * of client state should return to its initial shape.\n */\n\nimport { devHooks, type WarnOnceContext } from './dev-hooks.js';\nimport type { ReadonlySignal, Signal } from './reactive.js';\nimport { signal } from './reactive.js';\n\nexport interface Store<TState, TActions> {\n /** Read-only reactive view. Consumers read `state.value` or subscribe via `effect()`. */\n readonly state: ReadonlySignal<TState>;\n /** Named mutators — the only way to change state. */\n readonly actions: TActions;\n /** Reset state to `initial()`. Used by tests and lifecycle hooks. */\n reset(): void;\n}\n\ninterface DefineStoreSpec<TState, TActions> {\n initial: () => TState;\n actions: (\n set: (next: TState) => void,\n get: () => Readonly<TState>,\n ) => TActions;\n}\n\nconst REGISTRY: Array<{ reset: () => void }> = [];\n\nexport function defineStore<TState, TActions>(\n spec: DefineStoreSpec<TState, TActions>,\n): Store<TState, TActions> {\n const internal: Signal<TState> = signal(spec.initial());\n // KF-212: per-store one-shot dedup for the opt-in narrow-set warning.\n // Default off; consumers opt in via `KERF_DEV_WARN_NARROW_SET=1` in dev.\n // Resolve the hook on each set(), so a store created before `kerfjs/dev` is\n // installed starts warning as soon as the diagnostics become available.\n // Allocate the context lazily — production never pays for an object it will\n // never read.\n let warnCtx: WarnOnceContext | null = null;\n\n const set = (next: TState): void => {\n // With `kerfjs/dev` installed, `next` may carry proxies handed back by the\n // `get()` trap (e.g. `set({ ...get(), count: 1 })`). Unwrap them so the\n // internal signal only ever holds a plain object — the narrow-set warning\n // and every consumer read see raw state, never a Proxy. Production stores\n // the bare reference.\n const toRaw = devHooks.storeToRaw;\n const raw = toRaw ? toRaw(next) : next;\n const narrowSet = devHooks.narrowSet;\n if (narrowSet) {\n warnCtx ??= { warned: false };\n narrowSet(internal.value, raw, warnCtx);\n }\n internal.value = raw;\n };\n // With the diagnostics installed, wrap the reference returned to actions in a\n // deep read-only Proxy so that `get().count = 42` / `get().nested.x = 1`\n // (documented Rule 8 violations) throw a `TypeError` instead of silently\n // landing on the underlying state without notifying subscribers. The live\n // state object is never frozen or mutated, so external references to it stay\n // writable. Production returns the bare reference — no proxy, no wrapping.\n const get = (): Readonly<TState> => {\n const v = internal.value;\n const readonly = devHooks.storeReadonly;\n if (readonly && v !== null && typeof v === 'object') {\n return readonly(v as TState & object);\n }\n return v;\n };\n\n const actions = spec.actions(set, get);\n\n const store: Store<TState, TActions> = {\n state: internal,\n actions,\n reset() {\n internal.value = spec.initial();\n },\n };\n\n REGISTRY.push(store);\n return store;\n}\n\n/**\n * Reset every store registered via `defineStore()` to its `initial()` value.\n * Used by tests and by application lifecycle hooks (project switch, logout,\n * route reset).\n */\nexport function resetAllStores(): void {\n for (const s of REGISTRY) s.reset();\n}\n\n/**\n * Test helper — clears the registry. Exposed via the `kerfjs/testing` subpath,\n * not the main `kerfjs` entry. Unit tests use it to isolate stores between cases.\n */\nexport function clearStoreRegistry(): void {\n REGISTRY.length = 0;\n}\n"]}
@@ -0,0 +1,70 @@
1
+ // src/attach.ts
2
+ function attach(node, setup) {
3
+ const teardown = setup(node);
4
+ let done = false;
5
+ let hasConnected = node.isConnected;
6
+ let observedRoot;
7
+ let connectionFrame = 0;
8
+ const finish = () => {
9
+ if (done) return;
10
+ done = true;
11
+ globalThis.cancelAnimationFrame(connectionFrame);
12
+ observer.disconnect();
13
+ if (typeof teardown === "function") teardown();
14
+ };
15
+ const addedWithin = (candidate) => {
16
+ let current = node;
17
+ while (current !== null) {
18
+ if (current === candidate) return true;
19
+ const root = current.getRootNode();
20
+ current = current.parentNode ?? (root instanceof ShadowRoot ? root.host : null);
21
+ }
22
+ return false;
23
+ };
24
+ const observer = new MutationObserver((records) => {
25
+ if (done) return;
26
+ if (!hasConnected) {
27
+ if (!node.isConnected) {
28
+ const connectedAndRemovedInBatch = records.some(
29
+ (record) => [...record.addedNodes].some(addedWithin)
30
+ );
31
+ if (!connectedAndRemovedInBatch) return;
32
+ hasConnected = true;
33
+ finish();
34
+ return;
35
+ }
36
+ hasConnected = true;
37
+ globalThis.cancelAnimationFrame(connectionFrame);
38
+ } else if (!node.isConnected) {
39
+ finish();
40
+ return;
41
+ }
42
+ const root = node.getRootNode();
43
+ if (root !== observedRoot) observe(root);
44
+ });
45
+ const observe = (root) => {
46
+ observer.disconnect();
47
+ observedRoot = root;
48
+ const options = { childList: true, subtree: true };
49
+ observer.observe(node.ownerDocument, options);
50
+ if (root !== node.ownerDocument) observer.observe(root, options);
51
+ };
52
+ const watchForConnection = () => {
53
+ connectionFrame = globalThis.requestAnimationFrame(() => {
54
+ if (done || hasConnected) return;
55
+ if (node.isConnected) {
56
+ hasConnected = true;
57
+ observe(node.getRootNode());
58
+ return;
59
+ }
60
+ watchForConnection();
61
+ });
62
+ };
63
+ observe(hasConnected ? node.getRootNode() : node.ownerDocument);
64
+ if (!hasConnected) watchForConnection();
65
+ return finish;
66
+ }
67
+
68
+ export { attach };
69
+
70
+ //# sourceMappingURL=chunk-FM6N2B4N.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/attach.ts"],"names":[],"mappings":"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","file":"chunk-FM6N2B4N.js","sourcesContent":["/**\n * `kerfjs/attach` — bind a non-kerf widget's lifecycle to a single DOM node.\n *\n * `data-morph-skip` lets a library own a subtree so kerf won't touch it — but\n * nothing manages that widget's LIFECYCLE. You set it up imperatively after\n * render and must remember to tear it down when the node is replaced/removed\n * (dropping document-level listeners the widget added, etc.). `attach` closes\n * that seam: run a setup against one **existing** DOM node and auto-run its\n * teardown when that node leaves the document.\n *\n * import { attach } from 'kerfjs/attach';\n *\n * attach(canvasEl, (el) => {\n * const chart = D3.mount(el);\n * return () => chart.destroy(); // runs when el leaves the DOM (or on dispose)\n * });\n *\n * `setup(node)` runs immediately — the node already exists, so there is nothing\n * to wait for (this is NOT React's `useEffect`: no dependency array, no re-run,\n * no render-phase or hook-order scoping; it is closer to a Web Component's\n * `connectedCallback`/`disconnectedCallback` pair, Svelte's\n * `onMount(() => () => cleanup)`, or Solid's `onCleanup`). A node may start\n * disconnected: setup still runs now, and teardown waits until it has first\n * connected and subsequently leaves the document. The returned teardown runs\n * once — whichever comes first — on that removal (detected by a\n * `MutationObserver`, so a morph swap, a `remountOn` replacement, or any removal\n * triggers it) or when the returned disposer is called. Re-creation is NOT\n * handled here: a fresh node is a fresh `attach()` call — pair it with\n * `kerfjs/remount`, which replaces the node and re-runs your render (and thus\n * this call) on the new one.\n *\n * Related: `kerfjs/scope`'s `observeRemovals` also auto-disposes on removal via a\n * `MutationObserver`, but scoped to a whole subtree's registered disposers rather\n * than one node's setup/teardown pair — reach for that when you're collecting\n * many disposers under an element, and for `attach` when you're binding one\n * widget's lifecycle to one node.\n */\n\n/** The setup callback for {@link attach}: run against `node`, optionally return a teardown. */\nexport type AttachSetup = (node: Element) => (() => void) | void;\n\n/**\n * Run `setup(node)` now, and its returned teardown once — after `node` has been\n * connected and then leaves the document, or when the returned disposer is\n * called, whichever is first. Returns an idempotent disposer so a `mount()` /\n * `Scope` can drive teardown explicitly.\n */\nexport function attach(node: Element, setup: AttachSetup): () => void {\n const teardown = setup(node);\n let done = false;\n let hasConnected = node.isConnected;\n let observedRoot: Node | undefined;\n let connectionFrame = 0;\n\n const finish = (): void => {\n if (done) return;\n done = true;\n globalThis.cancelAnimationFrame(connectionFrame);\n observer.disconnect();\n if (typeof teardown === 'function') teardown();\n };\n\n const addedWithin = (candidate: Node): boolean => {\n let current: Node | null = node;\n while (current !== null) {\n if (current === candidate) return true;\n const root = current.getRootNode();\n current =\n current.parentNode ?? (root instanceof ShadowRoot ? root.host : null);\n }\n return false;\n };\n\n const observer = new MutationObserver((records) => {\n if (done) return;\n\n if (!hasConnected) {\n // A node may be prepared before insertion. Its own detached root cannot\n // observe becoming someone else's child, so wait on ownerDocument until\n // it becomes connected; only a later disconnection is a teardown event.\n if (!node.isConnected) {\n const connectedAndRemovedInBatch = records.some((record) =>\n [...record.addedNodes].some(addedWithin),\n );\n if (!connectedAndRemovedInBatch) return;\n hasConnected = true;\n finish();\n return;\n }\n hasConnected = true;\n globalThis.cancelAnimationFrame(connectionFrame);\n } else if (!node.isConnected) {\n finish();\n return;\n }\n\n // A connected node can move between documents or shadow roots without\n // ending its lifecycle. Follow its live root so a later removal remains\n // observable after that move.\n const root = node.getRootNode();\n if (root !== observedRoot) observe(root);\n });\n\n const observe = (root: Node): void => {\n observer.disconnect();\n observedRoot = root;\n const options = { childList: true, subtree: true } as const;\n observer.observe(node.ownerDocument, options);\n // Document observation sees a shadow host leave the document, while shadow\n // observation sees the node (or one of its in-shadow ancestors) removed.\n // Both are required to cover the node's composed lifecycle.\n if (root !== node.ownerDocument) observer.observe(root, options);\n };\n\n const watchForConnection = (): void => {\n connectionFrame = globalThis.requestAnimationFrame(() => {\n if (done || hasConnected) return;\n if (node.isConnected) {\n hasConnected = true;\n observe(node.getRootNode());\n return;\n }\n watchForConnection();\n });\n };\n\n // Connected nodes are watched from their live document/shadow root. For a\n // disconnected node, watch ownerDocument plus animation frames: the document\n // catches ordinary insertion, while the frame check also catches direct\n // insertion across a shadow boundary that document observation cannot see.\n observe(hasConnected ? node.getRootNode() : node.ownerDocument);\n if (!hasConnected) watchForConnection();\n\n return finish;\n}\n"]}
@@ -1,5 +1,5 @@
1
1
  import { bumpItemVersion } from './chunk-5WRGJZV6.js';
2
- import { signal } from './chunk-Y2FOYPBV.js';
2
+ import { signal } from './chunk-IHMDBBUJ.js';
3
3
 
4
4
  // src/array-signal.ts
5
5
  var ARRAY_SIGNAL_BRAND = /* @__PURE__ */ Symbol.for("kerfjs.ArraySignal");
@@ -106,4 +106,4 @@ function arraySignal(initial = []) {
106
106
 
107
107
  export { ARRAY_SIGNAL_BRAND, ArraySignal, arraySignal };
108
108
 
109
- //# sourceMappingURL=chunk-ELXVRKY2.js.map
109
+ //# sourceMappingURL=chunk-H7WTRM6F.js.map