kerfjs 4.4.1 → 5.0.0-beta.3

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 (73) hide show
  1. package/CHANGELOG.md +99 -0
  2. package/LICENSE +1 -1
  3. package/README.md +3 -3
  4. package/ai/cursorrules +46 -1
  5. package/ai/manifest.json +62 -5
  6. package/ai/skill.md +53 -2
  7. package/dist/actions.d.ts +1 -1
  8. package/dist/actions.js +4 -4
  9. package/dist/actions.js.map +1 -1
  10. package/dist/array-signal.js +5 -5
  11. package/dist/async.js +17 -10
  12. package/dist/async.js.map +1 -1
  13. package/dist/attach.d.ts +11 -8
  14. package/dist/attach.js +52 -4
  15. package/dist/attach.js.map +1 -1
  16. package/dist/chunk-GY4XV2UV.js +1 -1
  17. package/dist/{chunk-VVDJLWMP.js → chunk-HW7KSM2Y.js} +2 -2
  18. package/dist/chunk-HW7KSM2Y.js.map +1 -0
  19. package/dist/{chunk-KEZTD6H4.js → chunk-KPXIOG2C.js} +3 -3
  20. package/dist/{chunk-KEZTD6H4.js.map → chunk-KPXIOG2C.js.map} +1 -1
  21. package/dist/{chunk-SAYPJ6XR.js → chunk-KZJXHFIB.js} +10 -6
  22. package/dist/chunk-KZJXHFIB.js.map +1 -0
  23. package/dist/{chunk-MRYM3O3V.js → chunk-LVH3GC6B.js} +11 -8
  24. package/dist/chunk-LVH3GC6B.js.map +1 -0
  25. package/dist/chunk-QIP723L4.js +1 -1
  26. package/dist/{chunk-SRWQKB33.js → chunk-SVATPF5R.js} +97 -74
  27. package/dist/chunk-SVATPF5R.js.map +1 -0
  28. package/dist/{chunk-3APBEVHF.js → chunk-U6FK33SG.js} +3 -3
  29. package/dist/{chunk-3APBEVHF.js.map → chunk-U6FK33SG.js.map} +1 -1
  30. package/dist/{chunk-U32TFTGZ.js → chunk-UZJ6I4T6.js} +3 -3
  31. package/dist/chunk-UZJ6I4T6.js.map +1 -0
  32. package/dist/{chunk-YHH7OUFA.js → chunk-V46JKE44.js} +3 -3
  33. package/dist/chunk-V46JKE44.js.map +1 -0
  34. package/dist/{chunk-SUPUPSBE.js → chunk-ZDCJZCNO.js} +9 -8
  35. package/dist/chunk-ZDCJZCNO.js.map +1 -0
  36. package/dist/dev.d.ts +9 -6
  37. package/dist/dev.js +5 -5
  38. package/dist/dev.js.map +1 -1
  39. package/dist/html.d.ts +1 -1
  40. package/dist/html.js +5 -5
  41. package/dist/html.js.map +1 -1
  42. package/dist/index.d.ts +1 -1
  43. package/dist/index.js +11 -11
  44. package/dist/jsx-runtime.js +5 -5
  45. package/dist/list.d.ts +1 -1
  46. package/dist/list.js +273 -211
  47. package/dist/list.js.map +1 -1
  48. package/dist/overlay.d.ts +190 -280
  49. package/dist/overlay.js +401 -361
  50. package/dist/overlay.js.map +1 -1
  51. package/dist/remount.d.ts +5 -3
  52. package/dist/remount.js +29 -10
  53. package/dist/remount.js.map +1 -1
  54. package/dist/router.d.ts +1 -1
  55. package/dist/router.js +44 -22
  56. package/dist/router.js.map +1 -1
  57. package/dist/scope.d.ts +4 -3
  58. package/dist/scope.js +10 -10
  59. package/dist/scope.js.map +1 -1
  60. package/dist/testing.js +4 -4
  61. package/dist/timing.d.ts +3 -2
  62. package/dist/timing.js +5 -5
  63. package/dist/timing.js.map +1 -1
  64. package/llms.txt +7 -4
  65. package/package.json +12 -8
  66. package/dist/chunk-MRYM3O3V.js.map +0 -1
  67. package/dist/chunk-SAYPJ6XR.js.map +0 -1
  68. package/dist/chunk-SRWQKB33.js.map +0 -1
  69. package/dist/chunk-SUPUPSBE.js.map +0 -1
  70. package/dist/chunk-U32TFTGZ.js.map +0 -1
  71. package/dist/chunk-VVDJLWMP.js.map +0 -1
  72. package/dist/chunk-YHH7OUFA.js.map +0 -1
  73. /package/dist/{attrSelector-Cmu2ZoGO.d.ts → attr-Cmu2ZoGO.d.ts} +0 -0
package/dist/attach.js CHANGED
@@ -2,19 +2,67 @@
2
2
  function attach(node, setup) {
3
3
  const teardown = setup(node);
4
4
  let done = false;
5
+ let hasConnected = node.isConnected;
6
+ let observedRoot;
7
+ let connectionFrame = 0;
5
8
  const finish = () => {
6
9
  if (done) return;
7
10
  done = true;
11
+ globalThis.cancelAnimationFrame(connectionFrame);
8
12
  observer.disconnect();
9
13
  if (typeof teardown === "function") teardown();
10
14
  };
11
- const observer = new MutationObserver(() => {
12
- if (!node.isConnected) finish();
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((record) => [...record.addedNodes].some(addedWithin));
29
+ if (!connectedAndRemovedInBatch) return;
30
+ hasConnected = true;
31
+ finish();
32
+ return;
33
+ }
34
+ hasConnected = true;
35
+ globalThis.cancelAnimationFrame(connectionFrame);
36
+ } else if (!node.isConnected) {
37
+ finish();
38
+ return;
39
+ }
40
+ const root = node.getRootNode();
41
+ if (root !== observedRoot) observe(root);
13
42
  });
14
- observer.observe(node.getRootNode(), { childList: true, subtree: true });
43
+ const observe = (root) => {
44
+ observer.disconnect();
45
+ observedRoot = root;
46
+ const options = { childList: true, subtree: true };
47
+ observer.observe(node.ownerDocument, options);
48
+ if (root !== node.ownerDocument) observer.observe(root, options);
49
+ };
50
+ const watchForConnection = () => {
51
+ connectionFrame = globalThis.requestAnimationFrame(() => {
52
+ if (done || hasConnected) return;
53
+ if (node.isConnected) {
54
+ hasConnected = true;
55
+ observe(node.getRootNode());
56
+ return;
57
+ }
58
+ watchForConnection();
59
+ });
60
+ };
61
+ observe(hasConnected ? node.getRootNode() : node.ownerDocument);
62
+ if (!hasConnected) watchForConnection();
15
63
  return finish;
16
64
  }
17
65
 
18
66
  export { attach };
19
- //# sourceMappingURL=attach.js.map
67
+
20
68
  //# sourceMappingURL=attach.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/attach.ts"],"names":[],"mappings":";AA4CO,SAAS,MAAA,CAAO,MAAe,KAAA,EAAgC;AACpE,EAAA,MAAM,QAAA,GAAW,MAAM,IAAI,CAAA;AAC3B,EAAA,IAAI,IAAA,GAAO,KAAA;AAEX,EAAA,MAAM,SAAS,MAAY;AACzB,IAAA,IAAI,IAAA,EAAM;AACV,IAAA,IAAA,GAAO,IAAA;AACP,IAAA,QAAA,CAAS,UAAA,EAAW;AACpB,IAAA,IAAI,OAAO,QAAA,KAAa,UAAA,EAAY,QAAA,EAAS;AAAA,EAC/C,CAAA;AAMA,EAAA,MAAM,QAAA,GAAW,IAAI,gBAAA,CAAiB,MAAM;AAC1C,IAAA,IAAI,CAAC,IAAA,CAAK,WAAA,EAAa,MAAA,EAAO;AAAA,EAChC,CAAC,CAAA;AACD,EAAA,QAAA,CAAS,OAAA,CAAQ,KAAK,WAAA,EAAY,EAAG,EAAE,SAAA,EAAW,IAAA,EAAM,OAAA,EAAS,IAAA,EAAM,CAAA;AAEvE,EAAA,OAAO,MAAA;AACT","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`). The returned teardown\n * runs once whichever comes first when the node leaves the document (detected\n * by a `MutationObserver`, so a morph swap, a `remountOn` replacement, or any\n * removal triggers it) or when the returned disposer is called. Re-creation is\n * NOT 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 — when `node` leaves the\n * document, or when the returned disposer is called, whichever is first. Returns\n * a disposer (idempotent) so a `mount()` / `Scope` can drive teardown explicitly.\n */\nexport function attach(node: Element, setup: AttachSetup): () => void {\n const teardown = setup(node);\n let done = false;\n\n const finish = (): void => {\n if (done) return;\n done = true;\n observer.disconnect();\n if (typeof teardown === 'function') teardown();\n };\n\n // Observe the node's live tree (the document when connected) with subtree, so\n // an ANCESTOR removal not just a direct one is caught. Each mutation just\n // re-checks `node.isConnected`, which is true until the node (or an ancestor)\n // is removed, so a morph swap / remountOn replacement / manual removal all fire.\n const observer = new MutationObserver(() => {\n if (!node.isConnected) finish();\n });\n observer.observe(node.getRootNode(), { childList: true, subtree: true });\n\n return finish;\n}\n"]}
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 = 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 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"]}
@@ -69,5 +69,5 @@ function wrapWithTags(child, openTag, closeTag) {
69
69
  }
70
70
 
71
71
  export { LIST_MARKER_PREFIX, collectLists, flatten, flattenWithoutListItems, mergeChildSegments, wrapWithTags };
72
- //# sourceMappingURL=chunk-GY4XV2UV.js.map
72
+
73
73
  //# sourceMappingURL=chunk-GY4XV2UV.js.map
@@ -10,5 +10,5 @@ function clearDevHooks() {
10
10
  }
11
11
 
12
12
  export { clearDevHooks, devHooks, installDevHooks };
13
- //# sourceMappingURL=chunk-VVDJLWMP.js.map
14
- //# sourceMappingURL=chunk-VVDJLWMP.js.map
13
+
14
+ //# sourceMappingURL=chunk-HW7KSM2Y.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/dev-hooks.ts"],"names":[],"mappings":";AA8IO,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-HW7KSM2Y.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?: (prevWired: readonly Binding[], current: readonly Binding[]) => void;\n listInvariantsEnabled?: () => boolean;\n listInvariants?: (\n rootEl: Element,\n bindings: ReadonlyMap<string, ListBinding>,\n expectedCounts?: ReadonlyMap<string, number>,\n ) => void;\n valueOnlyRerender?: (prevHtml: string, nextHtml: string, ctx: WarnOnceContext) => 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?: (id: string, segItems: readonly { cacheKey: unknown }[]) => 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,4 +1,4 @@
1
- import { devHooks } from './chunk-VVDJLWMP.js';
1
+ import { devHooks } from './chunk-HW7KSM2Y.js';
2
2
 
3
3
  // src/delegate.ts
4
4
  var NON_BUBBLING = /* @__PURE__ */ new Set([
@@ -50,5 +50,5 @@ function delegateCapture(rootEl, type, selector, handler, options) {
50
50
  }
51
51
 
52
52
  export { delegate, delegateCapture };
53
- //# sourceMappingURL=chunk-KEZTD6H4.js.map
54
- //# sourceMappingURL=chunk-KEZTD6H4.js.map
53
+
54
+ //# sourceMappingURL=chunk-KPXIOG2C.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/delegate.ts"],"names":[],"mappings":";;;AA+CA,IAAM,YAAA,uBAAmB,GAAA,CAAY;AAAA,EACnC,OAAA;AAAA,EACA,MAAA;AAAA,EACA,QAAA;AAAA,EACA,MAAA;AAAA,EACA,OAAA;AAAA,EACA,YAAA;AAAA,EACA;AACF,CAAC,CAAA;AAqBD,SAAS,mBAAA,CAAoB,UAAkB,EAAA,EAAkB;AAC/D,EAAA,IAAI;AACF,IAAA,QAAA,CAAS,aAAA,CAAc,KAAK,CAAA,CAAE,OAAA,CAAQ,QAAQ,CAAA;AAAA,EAChD,CAAA,CAAA,MAAQ;AACN,IAAA,MAAM,IAAI,KAAA;AAAA,MACR,CAAA,EAAG,EAAE,CAAA,oBAAA,EAAuB,QAAQ,CAAA,2EAAA;AAAA,KAEtC;AAAA,EACF;AACF;AAQA,SAAS,YAAA,CACP,MAAA,EACA,QAAA,EACA,OAAA,EACA,KAAA,EACwB;AACxB,EAAA,OAAO,CAAC,KAAA,KAAuB;AAC7B,IAAA,MAAM,SAAS,KAAA,CAAM,MAAA;AACrB,IAAA,IAAI,EAAE,kBAAkB,OAAA,CAAA,EAAU;AAClC,IAAA,MAAM,OAAA,GAAU,KAAA,KAAU,QAAA,GACrB,MAAA,CAAO,OAAA,CAAQ,QAAQ,CAAA,GAAI,MAAA,GAAS,IAAA,GACrC,MAAA,CAAO,OAAA,CAAQ,QAAQ,CAAA;AAC3B,IAAA,IAAI,OAAA,KAAY,IAAA,IAAQ,MAAA,CAAO,QAAA,CAAS,OAAO,CAAA,EAAG;AAChD,MAAA,OAAA,CAAQ,OAAO,OAAY,CAAA;AAAA,IAC7B;AAAA,EACF,CAAA;AACF;AAuBO,SAAS,QAAA,CACd,MAAA,EACA,IAAA,EACA,QAAA,EACA,SACA,OAAA,EACY;AACZ,EAAA,mBAAA,CAAoB,UAAU,UAAU,CAAA;AACxC,EAAA,QAAA,CAAS,mBAAmB,UAAU,CAAA;AACtC,EAAA,MAAM,WAAW,YAAA,CAAa,MAAA,EAAQ,UAAU,OAAA,EAAS,OAAA,EAAS,SAAS,SAAS,CAAA;AACpF,EAAA,MAAM,OAAA,GAAU,YAAA,CAAa,GAAA,CAAI,IAAI,CAAA;AACrC,EAAA,MAAA,CAAO,gBAAA,CAAiB,IAAA,EAAM,QAAA,EAAU,OAAO,CAAA;AAC/C,EAAA,OAAO,MAAM;AACX,IAAA,MAAA,CAAO,mBAAA,CAAoB,IAAA,EAAM,QAAA,EAAU,OAAO,CAAA;AAAA,EACpD,CAAA;AACF;AAuBO,SAAS,eAAA,CACd,MAAA,EACA,IAAA,EACA,QAAA,EACA,SACA,OAAA,EACY;AACZ,EAAA,mBAAA,CAAoB,UAAU,iBAAiB,CAAA;AAC/C,EAAA,QAAA,CAAS,mBAAmB,iBAAiB,CAAA;AAC7C,EAAA,MAAM,WAAW,YAAA,CAAa,MAAA,EAAQ,UAAU,OAAA,EAAS,OAAA,EAAS,SAAS,SAAS,CAAA;AACpF,EAAA,MAAA,CAAO,gBAAA,CAAiB,IAAA,EAAM,QAAA,EAAU,IAAI,CAAA;AAC5C,EAAA,OAAO,MAAM;AACX,IAAA,MAAA,CAAO,mBAAA,CAAoB,IAAA,EAAM,QAAA,EAAU,IAAI,CAAA;AAAA,EACjD,CAAA;AACF","file":"chunk-KEZTD6H4.js","sourcesContent":["/**\n * Tiny event-delegation helpers. Replace per-element `addEventListener` calls\n * (which don't survive morph re-renders for nodes the diff creates) with one\n * listener at the morph-root that dispatches via `closest()`.\n *\n * Three-tier listener model:\n *\n * - Tier 1 (bubbling events) — use `delegate()`.\n * click, input, change, submit, mousedown/up, keydown/up, pointerdown/up/move,\n * drag*, drop, contextmenu, wheel, copy/paste/cut, focusin/focusout.\n *\n * `delegate()` also auto-promotes the well-known non-bubbling event\n * types (`focus`, `blur`, `scroll`, `load`, `error`, `mouseenter`,\n * `mouseleave`) to the capture phase under the hood, so the call site\n * looks identical for \"interactive thing happens on a descendant\"\n * regardless of whether that event bubbles. Selector matching stays\n * `closest()`-style — the same as for bubbling events — so a wrapper\n * selector like `'.field-row'` still matches when the event fires on\n * a descendant `<input>`.\n *\n * - Tier 2 (explicit capture) — use `delegateCapture()`.\n * The escape hatch for cases the auto-promotion list doesn't cover\n * (custom non-bubbling events) or when you want capture-phase\n * interception. Selector matching is `closest()`-style by default —\n * the same walk-up as `delegate()`, and it passes the matched ancestor\n * (not the raw target) to the handler — so a click on any descendant of\n * the selected element climbs to it. Pass `{ match: 'direct' }` to opt\n * into strict `matches()`-style matching (fire only when the event lands\n * on the exact element the selector identifies).\n *\n * - Tier 3 (per-element instances / library-owned subtrees) — mark the\n * host element with `data-morph-skip` and manage the library's\n * lifecycle directly. No delegation helper applies.\n */\n\nimport { devHooks } from './dev-hooks.js';\n\n/**\n * Event types that don't bubble and so wouldn't reach a root-level\n * bubble-phase listener. `delegate()` flips to capture for these; the\n * caller doesn't need to know or care.\n *\n * Membership is conservative — it covers the cases that \"should obviously\n * work\" with delegate() but otherwise don't. For exotic non-bubbling events\n * (custom events, less-common DOM events) the explicit `delegateCapture()`\n * remains the escape hatch.\n */\nconst NON_BUBBLING = new Set<string>([\n 'focus',\n 'blur',\n 'scroll',\n 'load',\n 'error',\n 'mouseenter',\n 'mouseleave',\n]);\n\n/**\n * How the selector is matched against the event's target:\n *\n * - `'closest'` (the default for both helpers) — walk UP from `event.target`\n * via `closest(selector)`, firing for the nearest matching ancestor inside\n * `rootEl`. This is the delegation behavior you almost always want: a click\n * on an icon inside a button fires the button's handler.\n * - `'direct'` — strict `matches()` match: fire only when `event.target`\n * itself matches the selector, with no walk-up.\n */\nexport interface DelegateOptions {\n match?: 'closest' | 'direct';\n}\n\n/**\n * Validate a CSS selector at registration time, so a typo throws immediately\n * with the bad selector quoted instead of producing a cryptic DOMException\n * the first time a matching event fires.\n */\nfunction assertValidSelector(selector: string, fn: string): void {\n try {\n document.createElement('div').matches(selector);\n } catch {\n throw new Error(\n `${fn}: invalid selector \"${selector}\". `\n + 'Pass a valid CSS selector (e.g. \\'[data-action=\"add\"]\\', \\'.btn\\', \\'input\\').',\n );\n }\n}\n\n/**\n * Build the shared root-level listener used by both helpers. Resolves the\n * event's target to a matched element (walk-up `closest()` or strict\n * `matches()`, per `match`), requires the match to be inside `rootEl`, then\n * fires `handler(event, matched)`.\n */\nfunction makeListener<T extends Element>(\n rootEl: HTMLElement,\n selector: string,\n handler: (event: Event, target: T) => void,\n match: 'closest' | 'direct',\n): (event: Event) => void {\n return (event: Event): void => {\n const target = event.target;\n if (!(target instanceof Element)) return;\n const matched = match === 'direct'\n ? (target.matches(selector) ? target : null)\n : target.closest(selector);\n if (matched !== null && rootEl.contains(matched)) {\n handler(event, matched as T);\n }\n };\n}\n\n/**\n * Delegation that \"just works\" for both bubbling and the common non-bubbling\n * events. Installs ONE listener on `rootEl`; for known non-bubblers (see\n * `NON_BUBBLING` above) the listener is registered on the capture phase so\n * it actually reaches the target, otherwise on the bubble phase. Either way,\n * matching walks up from `event.target` via `closest(selector)` and fires\n * `handler(event, matched)` if the match is inside `rootEl`.\n *\n * Pass `{ match: 'direct' }` to fire only when `event.target` itself matches\n * the selector (no walk-up); the default is `'closest'`.\n *\n * The generic `T` narrows the second handler argument to the expected element\n * type — `delegate<HTMLButtonElement>(root, 'click', 'button', (e, btn) => btn.value)`\n * — so consumers can avoid casts. Defaults to `Element` for untyped calls.\n *\n * Returns a disposer that removes the listener.\n *\n * Usage (pseudo-code — see examples for live ones):\n * delegate(rootEl, 'click', '[data-action=\"add\"]', handlerFn);\n * delegate(rootEl, 'focus', 'input', handlerFn); // auto-capture\n */\nexport function delegate<T extends Element = Element>(\n rootEl: HTMLElement,\n type: string,\n selector: string,\n handler: (event: Event, target: T) => void,\n options?: DelegateOptions,\n): () => void {\n assertValidSelector(selector, 'delegate');\n devHooks.delegateInEffect?.('delegate');\n const listener = makeListener(rootEl, selector, handler, options?.match ?? 'closest');\n const capture = NON_BUBBLING.has(type);\n rootEl.addEventListener(type, listener, capture);\n return () => {\n rootEl.removeEventListener(type, listener, capture);\n };\n}\n\n/**\n * Capture-phase delegation — the escape hatch for custom non-bubbling events\n * (ones `delegate()`'s auto-promotion list doesn't know about) and for\n * capture-phase interception (run before any descendant's bubble-phase\n * handler). Reaches descendants of `rootEl` that match `selector` regardless\n * of how many times the diff has rebuilt them.\n *\n * Selector matching is `closest()`-style by default — the same walk-up as\n * `delegate()`, and it passes the matched ancestor (not the raw target) to\n * the handler — so a click on any descendant of the selected element climbs\n * to it. Pass `{ match: 'direct' }` to opt into strict `matches()`-style\n * matching (fire only when the event lands on the exact element the selector\n * identifies, with no walk-up).\n *\n * The generic `T` narrows the second handler argument to the expected element\n * type, mirroring `delegate<T>()`. Defaults to `Element` for untyped calls.\n *\n * Usage (pseudo-code — see examples for live ones):\n * delegateCapture(rootEl, 'focus', 'input, textarea', handlerFn);\n * delegateCapture(rootEl, 'click', '.exact', handlerFn, { match: 'direct' });\n */\nexport function delegateCapture<T extends Element = Element>(\n rootEl: HTMLElement,\n type: string,\n selector: string,\n handler: (event: Event, target: T) => void,\n options?: DelegateOptions,\n): () => void {\n assertValidSelector(selector, 'delegateCapture');\n devHooks.delegateInEffect?.('delegateCapture');\n const listener = makeListener(rootEl, selector, handler, options?.match ?? 'closest');\n rootEl.addEventListener(type, listener, true);\n return () => {\n rootEl.removeEventListener(type, listener, true);\n };\n}\n"]}
1
+ {"version":3,"sources":["../src/delegate.ts"],"names":[],"mappings":";;;AA+CA,IAAM,YAAA,uBAAmB,GAAA,CAAY;AAAA,EACnC,OAAA;AAAA,EACA,MAAA;AAAA,EACA,QAAA;AAAA,EACA,MAAA;AAAA,EACA,OAAA;AAAA,EACA,YAAA;AAAA,EACA;AACF,CAAC,CAAA;AAqBD,SAAS,mBAAA,CAAoB,UAAkB,EAAA,EAAkB;AAC/D,EAAA,IAAI;AACF,IAAA,QAAA,CAAS,aAAA,CAAc,KAAK,CAAA,CAAE,OAAA,CAAQ,QAAQ,CAAA;AAAA,EAChD,CAAA,CAAA,MAAQ;AACN,IAAA,MAAM,IAAI,KAAA;AAAA,MACR,CAAA,EAAG,EAAE,CAAA,oBAAA,EAAuB,QAAQ,CAAA,2EAAA;AAAA,KAEtC;AAAA,EACF;AACF;AAQA,SAAS,YAAA,CACP,MAAA,EACA,QAAA,EACA,OAAA,EACA,KAAA,EACwB;AACxB,EAAA,OAAO,CAAC,KAAA,KAAuB;AAC7B,IAAA,MAAM,SAAS,KAAA,CAAM,MAAA;AACrB,IAAA,IAAI,EAAE,kBAAkB,OAAA,CAAA,EAAU;AAClC,IAAA,MAAM,OAAA,GAAU,KAAA,KAAU,QAAA,GACrB,MAAA,CAAO,OAAA,CAAQ,QAAQ,CAAA,GAAI,MAAA,GAAS,IAAA,GACrC,MAAA,CAAO,OAAA,CAAQ,QAAQ,CAAA;AAC3B,IAAA,IAAI,OAAA,KAAY,IAAA,IAAQ,MAAA,CAAO,QAAA,CAAS,OAAO,CAAA,EAAG;AAChD,MAAA,OAAA,CAAQ,OAAO,OAAY,CAAA;AAAA,IAC7B;AAAA,EACF,CAAA;AACF;AAuBO,SAAS,QAAA,CACd,MAAA,EACA,IAAA,EACA,QAAA,EACA,SACA,OAAA,EACY;AACZ,EAAA,mBAAA,CAAoB,UAAU,UAAU,CAAA;AACxC,EAAA,QAAA,CAAS,mBAAmB,UAAU,CAAA;AACtC,EAAA,MAAM,WAAW,YAAA,CAAa,MAAA,EAAQ,UAAU,OAAA,EAAS,OAAA,EAAS,SAAS,SAAS,CAAA;AACpF,EAAA,MAAM,OAAA,GAAU,YAAA,CAAa,GAAA,CAAI,IAAI,CAAA;AACrC,EAAA,MAAA,CAAO,gBAAA,CAAiB,IAAA,EAAM,QAAA,EAAU,OAAO,CAAA;AAC/C,EAAA,OAAO,MAAM;AACX,IAAA,MAAA,CAAO,mBAAA,CAAoB,IAAA,EAAM,QAAA,EAAU,OAAO,CAAA;AAAA,EACpD,CAAA;AACF;AAuBO,SAAS,eAAA,CACd,MAAA,EACA,IAAA,EACA,QAAA,EACA,SACA,OAAA,EACY;AACZ,EAAA,mBAAA,CAAoB,UAAU,iBAAiB,CAAA;AAC/C,EAAA,QAAA,CAAS,mBAAmB,iBAAiB,CAAA;AAC7C,EAAA,MAAM,WAAW,YAAA,CAAa,MAAA,EAAQ,UAAU,OAAA,EAAS,OAAA,EAAS,SAAS,SAAS,CAAA;AACpF,EAAA,MAAA,CAAO,gBAAA,CAAiB,IAAA,EAAM,QAAA,EAAU,IAAI,CAAA;AAC5C,EAAA,OAAO,MAAM;AACX,IAAA,MAAA,CAAO,mBAAA,CAAoB,IAAA,EAAM,QAAA,EAAU,IAAI,CAAA;AAAA,EACjD,CAAA;AACF","file":"chunk-KPXIOG2C.js","sourcesContent":["/**\n * Tiny event-delegation helpers. Replace per-element `addEventListener` calls\n * (which don't survive morph re-renders for nodes the diff creates) with one\n * listener at the morph-root that dispatches via `closest()`.\n *\n * Three-tier listener model:\n *\n * - Tier 1 (bubbling events) — use `delegate()`.\n * click, input, change, submit, mousedown/up, keydown/up, pointerdown/up/move,\n * drag*, drop, contextmenu, wheel, copy/paste/cut, focusin/focusout.\n *\n * `delegate()` also auto-promotes the well-known non-bubbling event\n * types (`focus`, `blur`, `scroll`, `load`, `error`, `mouseenter`,\n * `mouseleave`) to the capture phase under the hood, so the call site\n * looks identical for \"interactive thing happens on a descendant\"\n * regardless of whether that event bubbles. Selector matching stays\n * `closest()`-style — the same as for bubbling events — so a wrapper\n * selector like `'.field-row'` still matches when the event fires on\n * a descendant `<input>`.\n *\n * - Tier 2 (explicit capture) — use `delegateCapture()`.\n * The escape hatch for cases the auto-promotion list doesn't cover\n * (custom non-bubbling events) or when you want capture-phase\n * interception. Selector matching is `closest()`-style by default —\n * the same walk-up as `delegate()`, and it passes the matched ancestor\n * (not the raw target) to the handler — so a click on any descendant of\n * the selected element climbs to it. Pass `{ match: 'direct' }` to opt\n * into strict `matches()`-style matching (fire only when the event lands\n * on the exact element the selector identifies).\n *\n * - Tier 3 (per-element instances / library-owned subtrees) — mark the\n * host element with `data-morph-skip` and manage the library's\n * lifecycle directly. No delegation helper applies.\n */\n\nimport { devHooks } from './dev-hooks.js';\n\n/**\n * Event types that don't bubble and so wouldn't reach a root-level\n * bubble-phase listener. `delegate()` flips to capture for these; the\n * caller doesn't need to know or care.\n *\n * Membership is conservative — it covers the cases that \"should obviously\n * work\" with delegate() but otherwise don't. For exotic non-bubbling events\n * (custom events, less-common DOM events) the explicit `delegateCapture()`\n * remains the escape hatch.\n */\nconst NON_BUBBLING = new Set<string>([\n 'focus',\n 'blur',\n 'scroll',\n 'load',\n 'error',\n 'mouseenter',\n 'mouseleave',\n]);\n\n/**\n * How the selector is matched against the event's target:\n *\n * - `'closest'` (the default for both helpers) — walk UP from `event.target`\n * via `closest(selector)`, firing for the nearest matching ancestor inside\n * `rootEl`. This is the delegation behavior you almost always want: a click\n * on an icon inside a button fires the button's handler.\n * - `'direct'` — strict `matches()` match: fire only when `event.target`\n * itself matches the selector, with no walk-up.\n */\nexport interface DelegateOptions {\n match?: 'closest' | 'direct';\n}\n\n/**\n * Validate a CSS selector at registration time, so a typo throws immediately\n * with the bad selector quoted instead of producing a cryptic DOMException\n * the first time a matching event fires.\n */\nfunction assertValidSelector(selector: string, fn: string): void {\n try {\n document.createElement('div').matches(selector);\n } catch {\n throw new Error(\n `${fn}: invalid selector \"${selector}\". `\n + 'Pass a valid CSS selector (e.g. \\'[data-action=\"add\"]\\', \\'.btn\\', \\'input\\').',\n );\n }\n}\n\n/**\n * Build the shared root-level listener used by both helpers. Resolves the\n * event's target to a matched element (walk-up `closest()` or strict\n * `matches()`, per `match`), requires the match to be inside `rootEl`, then\n * fires `handler(event, matched)`.\n */\nfunction makeListener<T extends Element>(\n rootEl: HTMLElement,\n selector: string,\n handler: (event: Event, target: T) => void,\n match: 'closest' | 'direct',\n): (event: Event) => void {\n return (event: Event): void => {\n const target = event.target;\n if (!(target instanceof Element)) return;\n const matched = match === 'direct'\n ? (target.matches(selector) ? target : null)\n : target.closest(selector);\n if (matched !== null && rootEl.contains(matched)) {\n handler(event, matched as T);\n }\n };\n}\n\n/**\n * Delegation that \"just works\" for both bubbling and the common non-bubbling\n * events. Installs ONE listener on `rootEl`; for known non-bubblers (see\n * `NON_BUBBLING` above) the listener is registered on the capture phase so\n * it actually reaches the target, otherwise on the bubble phase. Either way,\n * matching walks up from `event.target` via `closest(selector)` and fires\n * `handler(event, matched)` if the match is inside `rootEl`.\n *\n * Pass `{ match: 'direct' }` to fire only when `event.target` itself matches\n * the selector (no walk-up); the default is `'closest'`.\n *\n * The generic `T` narrows the second handler argument to the expected element\n * type — `delegate<HTMLButtonElement>(root, 'click', 'button', (e, btn) => btn.value)`\n * — so consumers can avoid casts. Defaults to `Element` for untyped calls.\n *\n * Returns a disposer that removes the listener.\n *\n * Usage (pseudo-code — see examples for live ones):\n * delegate(rootEl, 'click', '[data-action=\"add\"]', handlerFn);\n * delegate(rootEl, 'focus', 'input', handlerFn); // auto-capture\n */\nexport function delegate<T extends Element = Element>(\n rootEl: HTMLElement,\n type: string,\n selector: string,\n handler: (event: Event, target: T) => void,\n options?: DelegateOptions,\n): () => void {\n assertValidSelector(selector, 'delegate');\n devHooks.delegateInEffect?.('delegate');\n const listener = makeListener(rootEl, selector, handler, options?.match ?? 'closest');\n const capture = NON_BUBBLING.has(type);\n rootEl.addEventListener(type, listener, capture);\n return () => {\n rootEl.removeEventListener(type, listener, capture);\n };\n}\n\n/**\n * Capture-phase delegation — the escape hatch for custom non-bubbling events\n * (ones `delegate()`'s auto-promotion list doesn't know about) and for\n * capture-phase interception (run before any descendant's bubble-phase\n * handler). Reaches descendants of `rootEl` that match `selector` regardless\n * of how many times the diff has rebuilt them.\n *\n * Selector matching is `closest()`-style by default — the same walk-up as\n * `delegate()`, and it passes the matched ancestor (not the raw target) to\n * the handler — so a click on any descendant of the selected element climbs\n * to it. Pass `{ match: 'direct' }` to opt into strict `matches()`-style\n * matching (fire only when the event lands on the exact element the selector\n * identifies, with no walk-up).\n *\n * The generic `T` narrows the second handler argument to the expected element\n * type, mirroring `delegate<T>()`. Defaults to `Element` for untyped calls.\n *\n * Usage (pseudo-code — see examples for live ones):\n * delegateCapture(rootEl, 'focus', 'input, textarea', handlerFn);\n * delegateCapture(rootEl, 'click', '.exact', handlerFn, { match: 'direct' });\n */\nexport function delegateCapture<T extends Element = Element>(\n rootEl: HTMLElement,\n type: string,\n selector: string,\n handler: (event: Event, target: T) => void,\n options?: DelegateOptions,\n): () => void {\n assertValidSelector(selector, 'delegateCapture');\n devHooks.delegateInEffect?.('delegateCapture');\n const listener = makeListener(rootEl, selector, handler, options?.match ?? 'closest');\n rootEl.addEventListener(type, listener, true);\n return () => {\n rootEl.removeEventListener(type, listener, true);\n };\n}\n"]}
@@ -1,15 +1,19 @@
1
- import { signal } from './chunk-3APBEVHF.js';
2
- import { devHooks } from './chunk-VVDJLWMP.js';
1
+ import { signal } from './chunk-U6FK33SG.js';
2
+ import { devHooks } from './chunk-HW7KSM2Y.js';
3
3
 
4
4
  // src/store.ts
5
5
  var REGISTRY = [];
6
6
  function defineStore(spec) {
7
7
  const internal = signal(spec.initial());
8
- const warnCtx = devHooks.narrowSet ? { warned: false } : null;
8
+ let warnCtx = null;
9
9
  const set = (next) => {
10
10
  const toRaw = devHooks.storeToRaw;
11
11
  const raw = toRaw ? toRaw(next) : next;
12
- if (warnCtx) devHooks.narrowSet?.(internal.value, raw, warnCtx);
12
+ const narrowSet = devHooks.narrowSet;
13
+ if (narrowSet) {
14
+ warnCtx ??= { warned: false };
15
+ narrowSet(internal.value, raw, warnCtx);
16
+ }
13
17
  internal.value = raw;
14
18
  };
15
19
  const get = () => {
@@ -39,5 +43,5 @@ function clearStoreRegistry() {
39
43
  }
40
44
 
41
45
  export { clearStoreRegistry, defineStore, resetAllStores };
42
- //# sourceMappingURL=chunk-SAYPJ6XR.js.map
43
- //# sourceMappingURL=chunk-SAYPJ6XR.js.map
46
+
47
+ //# sourceMappingURL=chunk-KZJXHFIB.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/store.ts"],"names":[],"mappings":";;;;AAsCA,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-KZJXHFIB.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: (set: (next: TState) => void, get: () => Readonly<TState>) => 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,8 +1,11 @@
1
1
  import { bumpItemVersion } from './chunk-QIP723L4.js';
2
- import { signal } from './chunk-3APBEVHF.js';
2
+ import { signal } from './chunk-U6FK33SG.js';
3
3
 
4
4
  // src/array-signal.ts
5
5
  var ARRAY_SIGNAL_BRAND = /* @__PURE__ */ Symbol.for("kerfjs.ArraySignal");
6
+ function isValidIndex(index, length, allowEnd = false) {
7
+ return Number.isInteger(index) && index >= 0 && (allowEnd ? index <= length : index < length);
8
+ }
6
9
  var ArraySignal = class {
7
10
  _items;
8
11
  _version;
@@ -27,7 +30,7 @@ var ArraySignal = class {
27
30
  * to every consumer's row memo.
28
31
  */
29
32
  update(index, fn) {
30
- if (index < 0 || index >= this._items.length) {
33
+ if (!isValidIndex(index, this._items.length)) {
31
34
  throw new Error(
32
35
  `arraySignal.update: index ${index} out of bounds [0, ${this._items.length}).`
33
36
  );
@@ -40,7 +43,7 @@ var ArraySignal = class {
40
43
  }
41
44
  /** Insert `item` at `index`. Existing items at index..N shift right. Emits one `insert` patch. */
42
45
  insert(index, item) {
43
- if (index < 0 || index > this._items.length) {
46
+ if (!isValidIndex(index, this._items.length, true)) {
44
47
  throw new Error(
45
48
  `arraySignal.insert: index ${index} out of bounds [0, ${this._items.length}].`
46
49
  );
@@ -55,7 +58,7 @@ var ArraySignal = class {
55
58
  }
56
59
  /** Remove and return the item at `index`. Emits one `remove` patch. */
57
60
  remove(index) {
58
- if (index < 0 || index >= this._items.length) {
61
+ if (!isValidIndex(index, this._items.length)) {
59
62
  throw new Error(
60
63
  `arraySignal.remove: index ${index} out of bounds [0, ${this._items.length}).`
61
64
  );
@@ -67,12 +70,12 @@ var ArraySignal = class {
67
70
  }
68
71
  /** Move the item at `from` to position `to`. Emits one `move` patch (no-op when from === to). */
69
72
  move(from, to) {
70
- if (from === to) return;
71
- if (from < 0 || from >= this._items.length || to < 0 || to >= this._items.length) {
73
+ if (!isValidIndex(from, this._items.length) || !isValidIndex(to, this._items.length)) {
72
74
  throw new Error(
73
75
  `arraySignal.move: indices out of bounds (from=${from}, to=${to}, length=${this._items.length}).`
74
76
  );
75
77
  }
78
+ if (from === to) return;
76
79
  const [item] = this._items.splice(from, 1);
77
80
  this._items.splice(to, 0, item);
78
81
  this._patches.push({ type: "move", from, to });
@@ -102,5 +105,5 @@ function arraySignal(initial = []) {
102
105
  }
103
106
 
104
107
  export { ARRAY_SIGNAL_BRAND, ArraySignal, arraySignal };
105
- //# sourceMappingURL=chunk-MRYM3O3V.js.map
106
- //# sourceMappingURL=chunk-MRYM3O3V.js.map
108
+
109
+ //# sourceMappingURL=chunk-LVH3GC6B.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/array-signal.ts"],"names":[],"mappings":"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","file":"chunk-LVH3GC6B.js","sourcesContent":["/**\n * `arraySignal(initial)` — granular collection signal.\n *\n * A keyed-list-friendly variant of `signal()` that emits typed patch events\n * for every mutation (update / insert / remove / move / replace). When such\n * a signal is bound to `each(...)` inside a `mount()`, the keyed list\n * reconciler applies just the patches against the live DOM — no per-item\n * iteration, no `classifyItems` Map build, no LIS pass over unchanged rows.\n *\n * const rows = arraySignal<Row>([]);\n *\n * rows.update(42, (r) => ({ ...r, label: 'changed' })); // 1 update event\n * rows.insert(0, { id: 'x', ... }); // 1 insert event\n * rows.remove(7); // 1 remove event\n * rows.move(3, 0); // 1 move event\n * rows.replace([...]); // falls back to snapshot reconcile\n *\n * Read-side semantics match a regular signal: `arraySig.value` is a\n * snapshot, and reads inside `effect()` / `computed()` register as\n * dependencies, so derived values keep working.\n */\n\nimport { bumpItemVersion } from './item-version.js';\nimport type { Signal } from './reactive.js';\nimport { signal } from './reactive.js';\n\n/** A single granular mutation event. */\nexport type ArrayPatch<T> =\n | { type: 'update'; index: number; item: T }\n | { type: 'insert'; index: number; item: T }\n | { type: 'remove'; index: number }\n | { type: 'move'; from: number; to: number }\n | { type: 'replace'; items: readonly T[] };\n\n/**\n * Cross-bundle brand for `ArraySignal` instances. `each()` and the\n * granular reconciler check for this brand instead of `instanceof\n * ArraySignal`, so the main `kerfjs` barrel can detect arraySignal\n * inputs without importing the class at runtime — the class lives\n * only in the `kerfjs/array-signal` subpath, so apps that don't need\n * granular collections shed ~1 KB.\n *\n * Same `Symbol.for(...)`-based pattern as `SafeHtml` (KF-14): cross-\n * bundle-safe, zero-cost runtime check.\n */\nexport const ARRAY_SIGNAL_BRAND = Symbol.for('kerfjs.ArraySignal');\n\nfunction isValidIndex(index: number, length: number, allowEnd = false): boolean {\n return Number.isInteger(index) && index >= 0 && (allowEnd ? index <= length : index < length);\n}\n\nexport class ArraySignal<T> {\n private _items: T[];\n private _version: Signal<number>;\n private _patches: ArrayPatch<T>[];\n // Branded so `isArraySignal()` recognizes instances from any copy of this module.\n readonly [ARRAY_SIGNAL_BRAND] = true as const;\n\n constructor(initial: readonly T[] = []) {\n this._items = [...initial];\n this._version = signal(0);\n this._patches = [];\n }\n\n /** Read-only snapshot. Reads inside an effect/computed register a dependency. */\n get value(): readonly T[] {\n // Touch the version signal so signals-core treats reads as tracked.\n void this._version.value;\n return this._items;\n }\n\n /**\n * Replace the item at `index` with `fn(currentItem)`. Emits one `update`\n * patch. Both styles work: returning a fresh object (idiomatic) invalidates\n * the row by identity, and mutating `item` in place and returning it works\n * too — a per-item content version (KF-418) makes the same-ref change visible\n * to every consumer's row memo.\n */\n update(index: number, fn: (item: T) => T): void {\n if (!isValidIndex(index, this._items.length)) {\n throw new Error(\n `arraySignal.update: index ${index} out of bounds [0, ${this._items.length}).`,\n );\n }\n const next = fn(this._items[index]);\n this._items[index] = next;\n this._patches.push({ type: 'update', index, item: next });\n // KF-418: a same-ref update (fn mutates and returns the same object) is\n // invisible to the row memo, which is keyed on object identity. Bump the\n // item's content version so every consumer — this list, another list over\n // this signal, a second mount, a plain-array filter() view — re-renders it.\n // Non-object items (an arraySignal<number> used as a plain signal) are\n // skipped by bumpItemVersion — they can't be each() rows (KF-419).\n bumpItemVersion(next);\n this._version.value++;\n }\n\n /** Insert `item` at `index`. Existing items at index..N shift right. Emits one `insert` patch. */\n insert(index: number, item: T): void {\n if (!isValidIndex(index, this._items.length, true)) {\n throw new Error(\n `arraySignal.insert: index ${index} out of bounds [0, ${this._items.length}].`,\n );\n }\n this._items.splice(index, 0, item);\n this._patches.push({ type: 'insert', index, item });\n this._version.value++;\n }\n\n /** Append `item` at the end. Sugar for `insert(items.length, item)`. */\n push(item: T): void {\n this.insert(this._items.length, item);\n }\n\n /** Remove and return the item at `index`. Emits one `remove` patch. */\n remove(index: number): T {\n if (!isValidIndex(index, this._items.length)) {\n throw new Error(\n `arraySignal.remove: index ${index} out of bounds [0, ${this._items.length}).`,\n );\n }\n const [removed] = this._items.splice(index, 1);\n this._patches.push({ type: 'remove', index });\n this._version.value++;\n return removed;\n }\n\n /** Move the item at `from` to position `to`. Emits one `move` patch (no-op when from === to). */\n move(from: number, to: number): void {\n if (!isValidIndex(from, this._items.length) || !isValidIndex(to, this._items.length)) {\n throw new Error(\n `arraySignal.move: indices out of bounds (from=${from}, to=${to}, length=${this._items.length}).`,\n );\n }\n if (from === to) return;\n const [item] = this._items.splice(from, 1);\n this._items.splice(to, 0, item);\n this._patches.push({ type: 'move', from, to });\n this._version.value++;\n }\n\n /** Replace every item. Emits one `replace` patch — the granular reconciler falls back to a full keyed diff for this case. */\n replace(items: readonly T[]): void {\n this._items = [...items];\n this._patches.push({ type: 'replace', items: this._items });\n this._version.value++;\n }\n\n /**\n * @internal Used by `each()` when binding this signal to a list. Returns\n * the queue of granular patches issued since the previous call, then\n * clears the queue. Best paired with a single binding — a second consumer\n * in the same render gets an empty array (which forces the snapshot\n * fall-back path, which is correct but slower).\n */\n _consumePatches(): ArrayPatch<T>[] {\n const out = this._patches;\n this._patches = [];\n return out;\n }\n}\n\n/** Construct an array signal seeded with `initial`. */\nexport function arraySignal<T>(initial: readonly T[] = []): ArraySignal<T> {\n return new ArraySignal(initial);\n}\n"]}
@@ -11,5 +11,5 @@ function itemVersion(item) {
11
11
  }
12
12
 
13
13
  export { bumpItemVersion, itemVersion };
14
- //# sourceMappingURL=chunk-QIP723L4.js.map
14
+
15
15
  //# sourceMappingURL=chunk-QIP723L4.js.map