kerfjs 5.0.0-beta.9 → 5.0.0-beta.91

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 (83) hide show
  1. package/CHANGELOG.md +2592 -107
  2. package/README.md +96 -37
  3. package/ai/cursorrules +110 -61
  4. package/ai/manifest.json +15 -7
  5. package/ai/skill.md +117 -72
  6. package/dist/actions.d.ts +1 -1
  7. package/dist/actions.js +10 -6
  8. package/dist/actions.js.map +1 -1
  9. package/dist/array-signal.js +1 -1
  10. package/dist/async.js +22 -19
  11. package/dist/async.js.map +1 -1
  12. package/dist/attach.js +1 -66
  13. package/dist/attach.js.map +1 -1
  14. package/dist/{chunk-HW7KSM2Y.js → chunk-3C6F6NM4.js} +1 -1
  15. package/dist/{chunk-HW7KSM2Y.js.map → chunk-3C6F6NM4.js.map} +1 -1
  16. package/dist/{chunk-GY4XV2UV.js → chunk-4TX6YJDM.js} +10 -3
  17. package/dist/chunk-4TX6YJDM.js.map +1 -0
  18. package/dist/{chunk-QIP723L4.js → chunk-5WRGJZV6.js} +3 -2
  19. package/dist/chunk-5WRGJZV6.js.map +1 -0
  20. package/dist/{chunk-KZJXHFIB.js → chunk-BI4HGICV.js} +3 -3
  21. package/dist/chunk-BI4HGICV.js.map +1 -0
  22. package/dist/chunk-FM6N2B4N.js +70 -0
  23. package/dist/chunk-FM6N2B4N.js.map +1 -0
  24. package/dist/{chunk-LVH3GC6B.js → chunk-H7WTRM6F.js} +3 -3
  25. package/dist/chunk-H7WTRM6F.js.map +1 -0
  26. package/dist/{chunk-U6FK33SG.js → chunk-IHMDBBUJ.js} +2 -2
  27. package/dist/chunk-IHMDBBUJ.js.map +1 -0
  28. package/dist/chunk-JN7EJM3B.js +86 -0
  29. package/dist/chunk-JN7EJM3B.js.map +1 -0
  30. package/dist/{chunk-UZJ6I4T6.js → chunk-MK42GLPV.js} +1 -1
  31. package/dist/chunk-MK42GLPV.js.map +1 -0
  32. package/dist/{chunk-ZDCJZCNO.js → chunk-TFLNYFDO.js} +88 -48
  33. package/dist/chunk-TFLNYFDO.js.map +1 -0
  34. package/dist/{chunk-V46JKE44.js → chunk-WKIPLNVO.js} +1 -1
  35. package/dist/chunk-WKIPLNVO.js.map +1 -0
  36. package/dist/{chunk-SVATPF5R.js → chunk-XHHGPZRQ.js} +261 -165
  37. package/dist/chunk-XHHGPZRQ.js.map +1 -0
  38. package/dist/{delegate-CL9VTZFb.d.ts → delegate--CrfgYtC.d.ts} +0 -34
  39. package/dist/dev.d.ts +31 -13
  40. package/dist/dev.js +101 -35
  41. package/dist/dev.js.map +1 -1
  42. package/dist/html.js +5 -4
  43. package/dist/html.js.map +1 -1
  44. package/dist/index.d.ts +14 -9
  45. package/dist/index.js +29 -20
  46. package/dist/index.js.map +1 -1
  47. package/dist/jsx-runtime.d.ts +13 -5
  48. package/dist/jsx-runtime.js +1 -1
  49. package/dist/list.d.ts +7 -3
  50. package/dist/list.js +153 -46
  51. package/dist/list.js.map +1 -1
  52. package/dist/overlay.d.ts +128 -129
  53. package/dist/overlay.js +675 -366
  54. package/dist/overlay.js.map +1 -1
  55. package/dist/remount.js +2 -2
  56. package/dist/router.d.ts +13 -4
  57. package/dist/router.js +37 -10
  58. package/dist/router.js.map +1 -1
  59. package/dist/scope.d.ts +35 -8
  60. package/dist/scope.js +61 -30
  61. package/dist/scope.js.map +1 -1
  62. package/dist/testing.js +1 -1
  63. package/dist/timing.d.ts +3 -2
  64. package/dist/timing.js +1 -1
  65. package/dist/timing.js.map +1 -1
  66. package/llms.txt +13 -6
  67. package/package.json +33 -8
  68. package/setup/cli.mjs +90 -0
  69. package/setup/index.d.mts +37 -0
  70. package/setup/index.mjs +1347 -0
  71. package/setup/jsonc.mjs +201 -0
  72. package/setup/state.schema.json +56 -0
  73. package/dist/chunk-GY4XV2UV.js.map +0 -1
  74. package/dist/chunk-KPXIOG2C.js +0 -54
  75. package/dist/chunk-KPXIOG2C.js.map +0 -1
  76. package/dist/chunk-KZJXHFIB.js.map +0 -1
  77. package/dist/chunk-LVH3GC6B.js.map +0 -1
  78. package/dist/chunk-QIP723L4.js.map +0 -1
  79. package/dist/chunk-SVATPF5R.js.map +0 -1
  80. package/dist/chunk-U6FK33SG.js.map +0 -1
  81. package/dist/chunk-UZJ6I4T6.js.map +0 -1
  82. package/dist/chunk-V46JKE44.js.map +0 -1
  83. package/dist/chunk-ZDCJZCNO.js.map +0 -1
@@ -1,37 +1,3 @@
1
- /**
2
- * Tiny event-delegation helpers. Replace per-element `addEventListener` calls
3
- * (which don't survive morph re-renders for nodes the diff creates) with one
4
- * listener at the morph-root that dispatches via `closest()`.
5
- *
6
- * Three-tier listener model:
7
- *
8
- * - Tier 1 (bubbling events) — use `delegate()`.
9
- * click, input, change, submit, mousedown/up, keydown/up, pointerdown/up/move,
10
- * drag*, drop, contextmenu, wheel, copy/paste/cut, focusin/focusout.
11
- *
12
- * `delegate()` also auto-promotes the well-known non-bubbling event
13
- * types (`focus`, `blur`, `scroll`, `load`, `error`, `mouseenter`,
14
- * `mouseleave`) to the capture phase under the hood, so the call site
15
- * looks identical for "interactive thing happens on a descendant"
16
- * regardless of whether that event bubbles. Selector matching stays
17
- * `closest()`-style — the same as for bubbling events — so a wrapper
18
- * selector like `'.field-row'` still matches when the event fires on
19
- * a descendant `<input>`.
20
- *
21
- * - Tier 2 (explicit capture) — use `delegateCapture()`.
22
- * The escape hatch for cases the auto-promotion list doesn't cover
23
- * (custom non-bubbling events) or when you want capture-phase
24
- * interception. Selector matching is `closest()`-style by default —
25
- * the same walk-up as `delegate()`, and it passes the matched ancestor
26
- * (not the raw target) to the handler — so a click on any descendant of
27
- * the selected element climbs to it. Pass `{ match: 'direct' }` to opt
28
- * into strict `matches()`-style matching (fire only when the event lands
29
- * on the exact element the selector identifies).
30
- *
31
- * - Tier 3 (per-element instances / library-owned subtrees) — mark the
32
- * host element with `data-morph-skip` and manage the library's
33
- * lifecycle directly. No delegation helper applies.
34
- */
35
1
  /**
36
2
  * How the selector is matched against the event's target:
37
3
  *
package/dist/dev.d.ts CHANGED
@@ -137,7 +137,9 @@ interface DevHooks {
137
137
  signalFactory?: <T>(value: T) => Signal<T>;
138
138
  /**
139
139
  * Wraps an `effect()` body so `delegate()` can detect that it is running
140
- * inside one. Returns the body to actually run.
140
+ * inside one. Returns the body to actually run. Resolved at effect-CREATION
141
+ * time (the delegate-in-effect switch too), so an effect created before the
142
+ * install or the opt-in is never wrapped.
141
143
  */
142
144
  wrapEffect?: (fn: () => void | (() => void)) => () => void | (() => void);
143
145
  delegateInEffect?: (fn: 'delegate' | 'delegateCapture') => void;
@@ -146,6 +148,12 @@ interface DevHooks {
146
148
  storeReadonly?: <T extends object>(state: T) => T;
147
149
  /** Unwraps a proxy handed back through `set({ ...get() })`. */
148
150
  storeToRaw?: <T>(next: T) => T;
151
+ /**
152
+ * Attaches the rebuilt-listeners observer to a mount root. Called once per
153
+ * `mount()`, at creation — and it returns `null` when the warning is switched
154
+ * off then — so a mount created before the install or the opt-in is never
155
+ * observed.
156
+ */
149
157
  listenerRebuild?: (rootEl: Element) => MutationObserver | null;
150
158
  listIdShift?: (id: string) => void;
151
159
  parserRepair?: (html: string) => void;
@@ -164,6 +172,8 @@ interface DevHooks {
164
172
  duplicateCacheKeys?: (id: string, segItems: readonly {
165
173
  cacheKey: unknown;
166
174
  }[]) => void;
175
+ /** A surface opened over a modal `<dialog>` could not be made fully usable. */
176
+ overlayBlockedByModal?: (reason: 'hidden' | 'inert') => void;
167
177
  /**
168
178
  * When installed, a screened URL throws instead of warning-and-dropping.
169
179
  * A slot rather than a boolean so the check stays uniform with the rest.
@@ -282,17 +292,23 @@ interface DevWarningOptions {
282
292
  *
283
293
  * ## Install ordering
284
294
  *
285
- * Every hook except one is read at CALL time (render, reconcile, `set()`,
286
- * `delegate()`), so installing any time before your first `mount()` is enough.
295
+ * Most hooks are read at CALL time (render, reconcile, `set()`, `delegate()`).
296
+ * Three are decided once, when their host is CREATED, so both installation and
297
+ * the warning's switch must precede it:
298
+ *
299
+ * - `signalFactory` — `signal()` picks its constructor (untracked signals).
300
+ * - `wrapEffect` — `effect()` decides whether to wrap its body with the
301
+ * effect-depth counter (delegate-in-effect).
302
+ * - `listenerRebuild` — `mount()` decides whether to attach its
303
+ * rebuilt-listeners `MutationObserver`.
287
304
  *
288
- * The exception is `signalFactory`: `signal()` picks its constructor when the
289
- * signal is CREATED. Static imports are hoisted above a top-level
290
- * `await import()`, so module-scope signals in imported modules are created
291
- * before this module runs and the untracked-signal warning will not see them.
292
- * To cover those, make `import 'kerfjs/dev'` the FIRST STATIC import of a
293
- * dev-only entry file, then load the rest of your app.
305
+ * Static imports are hoisted above a top-level `await import()`, so
306
+ * module-scope signals and effects in imported modules are created before this
307
+ * module runs. To cover those, make `import 'kerfjs/dev'` the FIRST STATIC
308
+ * import of a dev-only entry file (calling `enableWarnings()` there), then load
309
+ * the rest of your app.
294
310
  *
295
- * Opting into that warning prints this boundary once, so the gap is loud
311
+ * Opting into the untracked-signal warning prints its boundary once, so the gap is loud
296
312
  * rather than silent. It cannot be closed by retro-fitting existing signals:
297
313
  * `Signal.prototype`'s `value` accessor is non-configurable, and reaching live
298
314
  * instances would require a per-signal registry that production would pay for.
@@ -326,9 +342,11 @@ interface DevWarningOptions {
326
342
  * goes through `globalThis.process` into a local binding), so the environment
327
343
  * variables are unreachable there.
328
344
  *
329
- * Call it as early as you can. Every diagnostic reads its switch at call time
330
- * except the untracked-signal warning, which is chosen when a signal is
331
- * CREATED — enabling that one prints its coverage boundary once.
345
+ * Call it as early as you can. Most diagnostics read their switch at call
346
+ * time, but three are decided when their host is CREATED: `untrackedSignals`
347
+ * per `signal()`, `delegateInEffect` per `effect()`, and `rebuiltListeners` per
348
+ * `mount()`. A signal, effect, or mount created before this call stays
349
+ * uncovered. Enabling `untrackedSignals` prints its coverage boundary once.
332
350
  */
333
351
  declare function enableWarnings(options: DevWarningOptions): void;
334
352
  /**
package/dist/dev.js CHANGED
@@ -1,7 +1,7 @@
1
- import { truncateRowHtml } from './chunk-V46JKE44.js';
2
- import { LIST_MARKER_PREFIX } from './chunk-GY4XV2UV.js';
3
- import { installDevHooks } from './chunk-HW7KSM2Y.js';
4
- export { clearDevHooks, devHooks, installDevHooks } from './chunk-HW7KSM2Y.js';
1
+ import { truncateRowHtml } from './chunk-WKIPLNVO.js';
2
+ import { LIST_MARKER_PREFIX, OVERLAY_HOST_SELECTOR, isOverlayHost } from './chunk-4TX6YJDM.js';
3
+ import { installDevHooks } from './chunk-3C6F6NM4.js';
4
+ export { clearDevHooks, devHooks, installDevHooks } from './chunk-3C6F6NM4.js';
5
5
  import { signal, Signal } from '@preact/signals-core';
6
6
 
7
7
  // src/dev-warn-config.ts
@@ -24,7 +24,10 @@ function applyWarningOptions(options) {
24
24
  for (const [key, value] of Object.entries(options)) {
25
25
  const envName = ENV_NAME[key];
26
26
  if (envName === void 0 || value === void 0) continue;
27
- overrides.set(envName, value === false ? false : value === true ? "1" : value);
27
+ overrides.set(
28
+ envName,
29
+ value === false ? false : value === true ? "1" : value
30
+ );
28
31
  }
29
32
  }
30
33
  function devFlag(envName) {
@@ -33,6 +36,9 @@ function devFlag(envName) {
33
36
  const proc = globalThis.process;
34
37
  return proc?.env?.[envName];
35
38
  }
39
+ function silenceHint(key) {
40
+ return `Silence it with enableWarnings({ ${key}: false }) from kerfjs/dev, or set ${ENV_NAME[key]}=0 (or unset it) under Node.`;
41
+ }
36
42
 
37
43
  // src/dev-binding-warn.ts
38
44
  var warnedHoles = /* @__PURE__ */ new Set();
@@ -51,7 +57,7 @@ function maybeWarnStaleBinding(prevWired, current) {
51
57
  if (warnedHoles.has(cur.id)) continue;
52
58
  warnedHoles.add(cur.id);
53
59
  console.warn(
54
- `kerf: fine-grained binding ${describeHole(cur)} switched to a different signal instance on a render whose static-surrounds HTML was byte-for-byte unchanged. On that fast path kerf keeps the original binding effect and does NOT re-bind, so this hole is now stale \u2014 it still tracks the FIRST signal instance and will not reflect the new one. Bind one computed that switches internally (e.g. class={computed(() => cond.value ? sigA.value : sigB.value)}) instead of switching which signal instance you bind (see docs/2-reactivity \xA72.9). Set KERF_DEV_WARN_STALE_BINDING=0 (or unset it) to silence this warning.`
60
+ `kerf: fine-grained binding ${describeHole(cur)} switched to a different signal instance on a render whose static-surrounds HTML was byte-for-byte unchanged. On that fast path kerf keeps the original binding effect and does NOT re-bind, so this hole is now stale \u2014 it still tracks the FIRST signal instance and will not reflect the new one. Bind one computed that switches internally (e.g. class={computed(() => cond.value ? sigA.value : sigB.value)}) instead of switching which signal instance you bind (see docs/2-reactivity \xA72.9). ` + silenceHint("staleBinding")
55
61
  );
56
62
  }
57
63
  }
@@ -77,7 +83,7 @@ function warnIfInsideEffect(fn) {
77
83
  if (warned) return;
78
84
  warned = true;
79
85
  console.warn(
80
- `kerf: ${fn}() was called inside an effect() body. Every effect re-run installs a fresh root listener; the effect disposer cleans up the reactive subscription but not the listeners, so listener count grows linearly with signal churn and each listener pins its handler closure. Register the delegate once at module or setup scope and gate behavior on the signal *inside the handler* where the read is free. See docs/5-event-delegation.md \xA75.3 "When capturing the disposer still isn't enough". Set KERF_DEV_WARN_DELEGATE_IN_EFFECT=0 (or unset it) to silence this warning.`
86
+ `kerf: ${fn}() was called inside an effect() body. Every effect re-run installs a fresh root listener; the effect disposer cleans up the reactive subscription but not the listeners, so listener count grows linearly with signal churn and each listener pins its handler closure. Register the delegate once at module or setup scope and gate behavior on the signal *inside the handler* where the read is free. See docs/5-event-delegation.md \xA75.3 "When capturing the disposer still isn't enough". ` + silenceHint("delegateInEffect")
81
87
  );
82
88
  }
83
89
 
@@ -100,7 +106,7 @@ function maybeWarnEachInMorphSkip(id, liveParent, rootEl) {
100
106
  if (!hasMorphSkipAncestor(liveParent, rootEl)) return;
101
107
  warnedIds.add(id);
102
108
  console.warn(
103
- `kerf: each() list '${id}' is inside a data-morph-skip subtree. The keyed reconciler still updates the list rows, but any static signal-reactive JSX inside the same skipped ancestor (e.g. <p>{count.value}</p>) is frozen \u2014 the morph never visits it. Remove data-morph-skip from any element that contains reactive JSX content and reserve it for truly library-owned hosts. Set KERF_DEV_WARN_EACH_IN_MORPH_SKIP=0 (or unset it) to silence this warning.`
109
+ `kerf: each() list '${id}' is inside a data-morph-skip subtree. The keyed reconciler still updates the list rows, but any static signal-reactive JSX inside the same skipped ancestor (e.g. <p>{count.value}</p>) is frozen \u2014 the morph never visits it. Remove data-morph-skip from any element that contains reactive JSX content and reserve it for truly library-owned hosts. ` + silenceHint("eachInMorphSkip")
104
110
  );
105
111
  }
106
112
  var warnedDupIds = /* @__PURE__ */ new Set();
@@ -115,7 +121,7 @@ function maybeWarnDuplicateCacheKeys(id, segItems) {
115
121
  if (seen.has(si.cacheKey)) {
116
122
  warnedDupIds.add(id);
117
123
  console.warn(
118
- `kerf: each() list '${id}' has duplicate cacheKey values (duplicate: ${String(si.cacheKey)}). The cacheKey function should return a unique value per row so kerf can tell apart items for memoization \u2014 duplicate values cause some rows to return stale cached HTML when external state that affects their render changes. Set KERF_DEV_WARN_DUPLICATE_EACH_KEYS=0 (or unset it) to silence this warning.`
124
+ `kerf: each() list '${id}' has duplicate cacheKey values (duplicate: ${String(si.cacheKey)}). The cacheKey function should return a unique value per row so kerf can tell apart items for memoization \u2014 duplicate values cause some rows to return stale cached HTML when external state that affects their render changes. ` + silenceHint("duplicateEachKeys")
119
125
  );
120
126
  return;
121
127
  }
@@ -157,18 +163,38 @@ function findListInvariantViolations(rootEl, bindings, expectedCounts) {
157
163
  const { marker, liveParent, items } = binding;
158
164
  const expected = expectedCounts?.get(id);
159
165
  if (expected !== void 0 && items.length !== expected) {
160
- problems.push(describe(id, `holds ${items.length} row(s) but its source has ${expected} \u2014 the reconcile dropped or duplicated rows`));
166
+ problems.push(
167
+ describe(
168
+ id,
169
+ `holds ${items.length} row(s) but its source has ${expected} \u2014 the reconcile dropped or duplicated rows`
170
+ )
171
+ );
161
172
  }
162
173
  if (!rootEl.contains(marker)) {
163
- problems.push(describe(id, "its marker comment is no longer inside the mount root, so every future reconcile would mutate a detached tree"));
174
+ problems.push(
175
+ describe(
176
+ id,
177
+ "its marker comment is no longer inside the mount root, so every future reconcile would mutate a detached tree"
178
+ )
179
+ );
164
180
  continue;
165
181
  }
166
182
  if (marker.data !== `${LIST_MARKER_PREFIX}${id}`) {
167
- problems.push(describe(id, `its marker reads '${marker.data}' \u2014 the id is carried by a different marker node, so this binding describes another list`));
183
+ problems.push(
184
+ describe(
185
+ id,
186
+ `its marker reads '${marker.data}' \u2014 the id is carried by a different marker node, so this binding describes another list`
187
+ )
188
+ );
168
189
  }
169
190
  const markerIndex = indexIn(liveParent, marker);
170
191
  if (markerIndex === -1) {
171
- problems.push(describe(id, "its marker is not a child of the parent the binding records"));
192
+ problems.push(
193
+ describe(
194
+ id,
195
+ "its marker is not a child of the parent the binding records"
196
+ )
197
+ );
172
198
  continue;
173
199
  }
174
200
  let previousIndex = markerIndex;
@@ -177,28 +203,46 @@ function findListInvariantViolations(rootEl, bindings, expectedCounts) {
177
203
  const node = items[i].node;
178
204
  const index = indexIn(liveParent, node);
179
205
  if (index === -1) {
180
- problems.push(describe(id, `bound row ${i} is not a child of the list's parent (<${liveParent.tagName.toLowerCase()}>)${node.isConnected ? " \u2014 it is attached elsewhere in the document" : " \u2014 it is detached"}`));
206
+ problems.push(
207
+ describe(
208
+ id,
209
+ `bound row ${i} is not a child of the list's parent (<${liveParent.tagName.toLowerCase()}>)${node.isConnected ? " \u2014 it is attached elsewhere in the document" : " \u2014 it is detached"}`
210
+ )
211
+ );
181
212
  continue;
182
213
  }
183
214
  if (index <= previousIndex) {
184
- problems.push(describe(id, `bound row ${i} appears at child position ${index}, which is not after the previous one (${previousIndex}) \u2014 the rows are out of order or have crossed the marker`));
215
+ problems.push(
216
+ describe(
217
+ id,
218
+ `bound row ${i} appears at child position ${index}, which is not after the previous one (${previousIndex}) \u2014 the rows are out of order or have crossed the marker`
219
+ )
220
+ );
185
221
  }
186
222
  previousIndex = index;
187
223
  if (first === -1) first = index;
188
224
  const owner = owners.get(node);
189
225
  if (owner !== void 0) {
190
- problems.push(describe(id, `bound row ${i} is also claimed by list '${owner}'`));
226
+ problems.push(
227
+ describe(id, `bound row ${i} is also claimed by list '${owner}'`)
228
+ );
191
229
  } else {
192
230
  owners.set(node, id);
193
231
  }
194
232
  }
195
- if (first !== -1) spans.push({ id, parent: liveParent, from: first, to: previousIndex });
233
+ if (first !== -1)
234
+ spans.push({ id, parent: liveParent, from: first, to: previousIndex });
196
235
  }
197
236
  for (let a = 0; a < spans.length; a++) {
198
237
  for (let b = a + 1; b < spans.length; b++) {
199
238
  if (spans[a].parent !== spans[b].parent) continue;
200
239
  if (spans[a].from <= spans[b].to && spans[b].from <= spans[a].to) {
201
- problems.push(describe(spans[a].id, `its rows (child positions ${spans[a].from}-${spans[a].to}) overlap those of list '${spans[b].id}' (${spans[b].from}-${spans[b].to}) in the same parent`));
240
+ problems.push(
241
+ describe(
242
+ spans[a].id,
243
+ `its rows (child positions ${spans[a].from}-${spans[a].to}) overlap those of list '${spans[b].id}' (${spans[b].from}-${spans[b].to}) in the same parent`
244
+ )
245
+ );
202
246
  }
203
247
  }
204
248
  }
@@ -207,7 +251,11 @@ function findListInvariantViolations(rootEl, bindings, expectedCounts) {
207
251
  function maybeCheckListInvariants(rootEl, bindings, expectedCounts) {
208
252
  const level = mode();
209
253
  if (level === "off") return;
210
- const problems = findListInvariantViolations(rootEl, bindings, expectedCounts);
254
+ const problems = findListInvariantViolations(
255
+ rootEl,
256
+ bindings,
257
+ expectedCounts
258
+ );
211
259
  if (problems.length === 0) return;
212
260
  const report = `${problems.join("\n")}
213
261
  This is a kerf bug, not an application one \u2014 please report it with the markup that produced it. Set KERF_DEV_INVARIANTS=1 to warn instead of throw, or unset it to disable these checks.`;
@@ -224,7 +272,7 @@ function maybeWarnStaleIndex(id) {
224
272
  if (warnedIds2.has(id)) return;
225
273
  warnedIds2.add(id);
226
274
  console.warn(
227
- `kerf: each() list '${id}' reused a memoized row at a new index this render, but its render function takes an \`index\` argument. Rows are memoized by object identity, not by position, so a reorder or an insert/remove ahead of a surviving row serves that row's cached HTML \u2014 computed at its OLD index. A numbered list, zebra striping, or an "N of M" label will silently show the wrong value. If the row output depends on the index, fold it into the memo key: each(items, render, { cacheKey: (_, i) => i }) (combine with your own key if you have one). If the index is not used in the output, ignore this. Set KERF_DEV_WARN_STALE_INDEX=0 (or unset it) to silence this warning.`
275
+ `kerf: each() list '${id}' reused a memoized row at a new index this render, but its render function takes an \`index\` argument. Rows are memoized by object identity, not by position, so a reorder or an insert/remove ahead of a surviving row serves that row's cached HTML \u2014 computed at its OLD index. A numbered list, zebra striping, or an "N of M" label will silently show the wrong value. If the row output depends on the index, fold it into the memo key: each(items, render, { cacheKey: (_, i) => i }) (combine with your own key if you have one). If the index is not used in the output, ignore this. ` + silenceHint("staleIndex")
228
276
  );
229
277
  }
230
278
 
@@ -245,14 +293,13 @@ function maybeWarnListRebind(id, liveParent) {
245
293
  if (warnedIds3.has(id)) return;
246
294
  warnedIds3.add(id);
247
295
  console.warn(
248
- `kerf: each() list '${id}' had its container (<${liveParent.tagName.toLowerCase()}>) rebuilt by the morph this render \u2014 an ancestor's tag changed so the subtree was replaced, or a same-tag sibling positionally took the container's place. The list re-binds and repopulates automatically, but its rows were re-created from scratch: focus, scroll positions, in-progress IME composition, and any imperative listeners on the old row nodes are lost. If the rows should survive, give the LIST'S OWN container a stable id/data-key (keying the conditional sibling instead only helps when it is removed, not when it reappears) and keep the tags of the list's ancestors stable across renders. Set KERF_DEV_WARN_LIST_REBIND=0 (or unset it) to silence this warning.`
296
+ `kerf: each() list '${id}' had its container (<${liveParent.tagName.toLowerCase()}>) rebuilt by the morph this render \u2014 an ancestor's tag changed so the subtree was replaced, or a same-tag sibling positionally took the container's place. The list re-binds and repopulates automatically, but its rows were re-created from scratch: focus, scroll positions, in-progress IME composition, and any imperative listeners on the old row nodes are lost. If the rows should survive, give the LIST'S OWN container a stable id/data-key (keying the conditional sibling instead only helps when it is removed, not when it reappears) and keep the tags of the list's ancestors stable across renders. ` + silenceHint("listRebind")
249
297
  );
250
298
  }
251
299
 
252
300
  // src/dev-listener-warn.ts
253
301
  var LISTENER_MARKER = /* @__PURE__ */ Symbol.for("kerfjs.devListener");
254
302
  var patched = false;
255
- var warned2 = false;
256
303
  function isOptedIn5() {
257
304
  return devFlag("KERF_DEV_WARN_REBUILT_LISTENERS") === "1";
258
305
  }
@@ -277,33 +324,38 @@ function patchAddEventListenerOnce() {
277
324
  };
278
325
  }
279
326
  function hasMarkedListener(el) {
280
- if (el[LISTENER_MARKER] === true) return true;
281
- const stack = [];
282
- for (let i = 0; i < el.children.length; i++) stack.push(el.children[i]);
327
+ const stack = [el];
283
328
  while (stack.length > 0) {
284
329
  const cur = stack.pop();
285
- if (cur[LISTENER_MARKER] === true) return true;
330
+ if (cur[LISTENER_MARKER] === true)
331
+ return true;
332
+ if (isOverlayHost(cur)) continue;
286
333
  for (let i = 0; i < cur.children.length; i++) stack.push(cur.children[i]);
287
334
  }
288
335
  return false;
289
336
  }
337
+ function insideHostSlot(target, rootEl) {
338
+ const host = target.closest(OVERLAY_HOST_SELECTOR);
339
+ return host !== null && host !== rootEl && rootEl.contains(host);
340
+ }
290
341
  function emitWarning() {
291
- if (warned2) return;
292
- warned2 = true;
293
342
  console.warn(
294
- "kerf: a node inside a mount()-managed tree was removed/rebuilt while carrying an imperative addEventListener listener. The listener is gone with the old node. Use `delegate(rootEl, 'click', '[data-action=\"...\"]', handler)` so the listener lives on a stable ancestor and survives re-renders, or wrap the host in `data-morph-skip` if the subtree is library-owned (Monaco, xterm, D3 charts). Set KERF_DEV_WARN_REBUILT_LISTENERS=0 (or unset it) to silence this warning."
343
+ "kerf: a node inside a mount()-managed tree was removed/rebuilt while carrying an imperative addEventListener listener. The listener is gone with the old node. Use `delegate(rootEl, 'click', '[data-action=\"...\"]', handler)` so the listener lives on a stable ancestor and survives re-renders, or wrap the host in `data-morph-skip` if the subtree is library-owned (Monaco, xterm, D3 charts). " + silenceHint("rebuiltListeners")
295
344
  );
296
345
  }
297
346
  function installListenerRebuildWarn(rootEl) {
298
347
  if (!isOptedIn5()) return null;
299
348
  patchAddEventListenerOnce();
349
+ let warned3 = false;
300
350
  const observer = new MutationObserver((mutations) => {
301
- if (warned2) return;
351
+ if (warned3) return;
302
352
  for (const m of mutations) {
353
+ if (insideHostSlot(m.target, rootEl)) continue;
303
354
  for (let i = 0; i < m.removedNodes.length; i++) {
304
355
  const removed = m.removedNodes[i];
305
356
  if (!(removed instanceof Element)) continue;
306
357
  if (hasMarkedListener(removed)) {
358
+ warned3 = true;
307
359
  emitWarning();
308
360
  return;
309
361
  }
@@ -314,6 +366,18 @@ function installListenerRebuildWarn(rootEl) {
314
366
  return observer;
315
367
  }
316
368
 
369
+ // src/dev-overlay-warn.ts
370
+ var warned2 = /* @__PURE__ */ new Set();
371
+ var MESSAGES = {
372
+ hidden: "kerf overlay(): a surface was opened while a modal <dialog> is open, and this engine lacks the <dialog>.showModal() / Popover API needed to lift it into the top layer. It renders beneath the dialog and is inert, so the user can neither see nor use it. Render the content inside the dialog's own markup, or open the dialog without `native: true`.",
373
+ inert: "kerf overlay(): a non-modal surface opened while a modal <dialog> is open was lifted into the top layer so it is visible, but browsers keep everything outside a modal dialog inert \u2014 its focusable controls cannot be focused or clicked. Anchor the popover inside the dialog and give the dialog a `data-kerf-overlay-host` element (dialogs kerf opens get one automatically), or pass a `container` inside the dialog."
374
+ };
375
+ function maybeWarnOverlayBlockedByModal(reason) {
376
+ if (warned2.has(reason)) return;
377
+ warned2.add(reason);
378
+ console.warn(MESSAGES[reason]);
379
+ }
380
+
317
381
  // src/dev-parser-repair-warn.ts
318
382
  var BLOCK_TAGS = [
319
383
  "address",
@@ -378,7 +442,7 @@ function maybeWarnParserRepair(html) {
378
442
  if (warnedPairs.has(pair)) return;
379
443
  warnedPairs.add(pair);
380
444
  console.warn(
381
- `kerf: a <${tag}> inside a <p> will not survive parsing. <p> may contain only phrasing content, so the HTML parser closes it before a block-level child \u2014 your <p> ends up EMPTY and the <${tag}> (plus everything after it) becomes its sibling instead of its child. kerf then reconciles that repaired tree correctly, so updates still work; what you lose is the structure you wrote, along with any CSS or querySelector that assumed it. Use a <div> (or a phrasing element like <span>) in place of the <p>, or move the block content outside it. Set KERF_DEV_WARN_PARSER_REPAIR=0 (or unset it) to silence this warning.`
445
+ `kerf: a <${tag}> inside a <p> will not survive parsing. <p> may contain only phrasing content, so the HTML parser closes it before a block-level child \u2014 your <p> ends up EMPTY and the <${tag}> (plus everything after it) becomes its sibling instead of its child. kerf then reconciles that repaired tree correctly, so updates still work; what you lose is the structure you wrote, along with any CSS or querySelector that assumed it. Use a <div> (or a phrasing element like <span>) in place of the <p>, or move the block content outside it. ` + silenceHint("parserRepair")
382
446
  );
383
447
  }
384
448
 
@@ -417,7 +481,7 @@ function maybeWarnValueOnlyRerender(prevHtml, nextHtml, ctx) {
417
481
  if (!_isValueOnlyDiff(a.content, b.content)) return;
418
482
  ctx.warned = true;
419
483
  console.warn(
420
- "kerf: this re-render changed only text content and attribute values \u2014 no structural change \u2014 so every changed hole could be a fine-grained binding instead. Values bind, structure re-renders: pass the signal/computed itself ({count}, class={sig}) rather than reading .value in the hole, and each change updates just that node with no render re-run (a mount whose render reads no .value never re-renders at all). See docs/2-reactivity \xA72.9. Set KERF_DEV_WARN_VALUE_ONLY_RERENDER=0 (or unset it) to silence this warning."
484
+ "kerf: this re-render changed only text content and attribute values \u2014 no structural change \u2014 so every changed hole could be a fine-grained binding instead. Values bind, structure re-renders: pass the signal/computed itself ({count}, class={sig}) rather than reading .value in the hole, and each change updates just that node with no render re-run (a mount whose render reads no .value never re-renders at all). See docs/2-reactivity \xA72.9. " + silenceHint("valueOnlyRerender")
421
485
  );
422
486
  }
423
487
 
@@ -430,7 +494,7 @@ function maybeWarnMissingRowKey(rowEl, rowHtml, binding) {
430
494
  `kerf each(): the first row has no \`id\` or \`data-key\` attribute. Without one, rows match positionally \u2014 an insert/remove at the head shifts every row's identity, so focused inputs jump to the wrong row, mid-edit textareas swap content with their neighbor, and any per-row state silently follows the wrong item. Add \`data-key={item.id}\` (or set \`id\`) to the top-level element returned by the row render. Row HTML: ${JSON.stringify(truncateRowHtml(rowHtml))}`
431
495
  );
432
496
  }
433
- var WARNING_MESSAGE = "kerf: signal was written but has no subscribers. Did you read `.value` outside of a render fn / effect()? Hoisted reads do not subscribe, so subsequent writes will not re-render. Move the read inside mount()'s render fn or effect() callback. Set KERF_DEV_WARN_UNTRACKED_SIGNALS=0 (or unset it) to silence this warning.";
497
+ var WARNING_MESSAGE = "kerf: signal was written but has no subscribers. Did you read `.value` outside of a render fn / effect()? Hoisted reads do not subscribe, so subsequent writes will not re-render. Move the read inside mount()'s render fn or effect() callback. " + silenceHint("untrackedSignals");
434
498
  var DevSignal = class extends Signal {
435
499
  __hasSubscriber = false;
436
500
  __warned = false;
@@ -462,13 +526,13 @@ function noteUntrackedCoverage() {
462
526
  if (coverageNoticeShown) return;
463
527
  coverageNoticeShown = true;
464
528
  console.warn(
465
- "kerf: KERF_DEV_WARN_UNTRACKED_SIGNALS only covers signals created AFTER kerfjs/dev is installed. Static imports are hoisted above `await import('kerfjs/dev')`, so module-scope signals in the modules you import are created first and this warning cannot see them \u2014 you may get no warnings even where the bug exists. To cover them, make `import 'kerfjs/dev'` the FIRST STATIC import of a dev-only entry file (static imports evaluate in order), then load the rest of your app. Set KERF_DEV_WARN_UNTRACKED_SIGNALS=0 (or unset it) to silence this warning."
529
+ "kerf: KERF_DEV_WARN_UNTRACKED_SIGNALS only covers signals created AFTER kerfjs/dev is installed. Static imports are hoisted above `await import('kerfjs/dev')`, so module-scope signals in the modules you import are created first and this warning cannot see them \u2014 you may get no warnings even where the bug exists. To cover them, make `import 'kerfjs/dev'` the FIRST STATIC import of a dev-only entry file (static imports evaluate in order), then load the rest of your app. " + silenceHint("untrackedSignals")
466
530
  );
467
531
  }
468
532
 
469
533
  // src/dev-store-warn.ts
470
534
  var WARNING_PREFIX = "kerf: defineStore.set() called with keys missing from the current state \u2014 ";
471
- var WARNING_SUFFIX = ". set() REPLACES state; the missing keys will be undefined after this call. Use `set({ ...get(), ...next })` to merge instead, or update each call site to pass the full state. Set KERF_DEV_WARN_NARROW_SET=0 (or unset it) to silence this warning.";
535
+ var WARNING_SUFFIX = ". set() REPLACES state; the missing keys will be undefined after this call. Use `set({ ...get(), ...next })` to merge instead, or update each call site to pass the full state. " + silenceHint("narrowSet");
472
536
  function isOptedIn8() {
473
537
  return devFlag("KERF_DEV_WARN_NARROW_SET") === "1";
474
538
  }
@@ -594,6 +658,8 @@ var DEV_HOOKS = {
594
658
  staleIndexEnabled: isOptedInStaleIndex,
595
659
  staleIndex: maybeWarnStaleIndex,
596
660
  duplicateCacheKeys: maybeWarnDuplicateCacheKeys,
661
+ // --- overlay ----------------------------------------------------------
662
+ overlayBlockedByModal: maybeWarnOverlayBlockedByModal,
597
663
  // --- urlScreen --------------------------------------------------------
598
664
  urlScreenThrow: (message) => {
599
665
  throw new Error(message);