kerfjs 5.0.0-beta.9 → 5.0.0-beta.90

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 +2586 -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-SVATPF5R.js → chunk-T33U7SRR.js} +261 -165
  33. package/dist/chunk-T33U7SRR.js.map +1 -0
  34. package/dist/{chunk-ZDCJZCNO.js → chunk-UGNPDLNI.js} +78 -45
  35. package/dist/chunk-UGNPDLNI.js.map +1 -0
  36. package/dist/{chunk-V46JKE44.js → chunk-WKIPLNVO.js} +1 -1
  37. package/dist/chunk-WKIPLNVO.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
package/ai/manifest.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
- "kerfjsVersion": "5.0.0-beta.9",
2
+ "kerfjsVersion": "5.0.0-beta.90",
3
3
  "files": [
4
4
  {
5
5
  "name": "skill",
6
6
  "source": "kerf.claude-skill.md",
7
7
  "bundle": "ai/skill.md",
8
8
  "dest": ".claude/skills/kerf-app/SKILL.md",
9
- "version": "1.22.0",
10
- "sha256": "5eb6631671faefeb273c93dee492eecf35dfc567fecba799374ac8af6c9a0dc3",
9
+ "version": "1.25.0",
10
+ "sha256": "4bc2b292bd707639c13b8cafe3fb24f779bfea04ab38fd6a5d40c443eed89c9d",
11
11
  "history": {
12
12
  "1.0.0": "bef982cb743fd4c73e666ee5cf774112e57c93ebb13b24bbacbf08577db0930a",
13
13
  "1.1.0": "f2aadb21e33dd389028951ae1ecbd9f8ba68648b3c5beba43fb4dfa2ab29f195",
@@ -36,7 +36,11 @@
36
36
  "1.19.0": "bbbe87446cc4e1be250d27152adfd385431f4498cde320534f65f10f853ab7f9",
37
37
  "1.20.0": "aa88ccdbf969624bbc6cefadba76ada1f090fae436597a7bab564e24c467287f",
38
38
  "1.21.0": "265b747eeb9c3d445c60d66a1109eec42705075b7cd154d107bd79d543c1e7e5",
39
- "1.22.0": "5eb6631671faefeb273c93dee492eecf35dfc567fecba799374ac8af6c9a0dc3"
39
+ "1.22.0": "dc99b0ca5e911be11cbfa2ae515c0e161a414a5185895069535c41ba56664e70",
40
+ "1.23.0": "28b82b4d31b98814c8fbbe1cb0bcf716192deb494c020b9af41a30830e6a0007",
41
+ "1.23.1": "e21d7db1097ad95c11af9248ad17d514716bebc5646becf57d19a7598d6e382c",
42
+ "1.24.0": "f74616ec20120fa97ec2ff7e92971f5721ee8dcb0b5b7da2dd2604bb87002b69",
43
+ "1.25.0": "4bc2b292bd707639c13b8cafe3fb24f779bfea04ab38fd6a5d40c443eed89c9d"
40
44
  }
41
45
  },
42
46
  {
@@ -44,8 +48,8 @@
44
48
  "source": "kerf.cursorrules",
45
49
  "bundle": "ai/cursorrules",
46
50
  "dest": ".cursorrules",
47
- "version": "1.20.0",
48
- "sha256": "c4dac40630fc456c6c7e6ba9d1b75215bc76bd76e6f31ba36d3bc9ab867d46e0",
51
+ "version": "1.23.0",
52
+ "sha256": "908e6622f597108ca696c8ff6f1081b3d83996dc46771a3515b184884dab4b25",
49
53
  "history": {
50
54
  "1.0.0": "98f7faff2b7585ef9cc511add42ea5d9bf0cbae8307b066af090e9281136d2ab",
51
55
  "1.1.0": "a2ec8c221dc694c98b81009cd2d99f9159a45fda94585de2f54471b227099eda",
@@ -72,7 +76,11 @@
72
76
  "1.17.0": "6ad51fb0eec468f584346d1d7b4d1c2eca2cb52b04891b38ba44e93b57eb9e1c",
73
77
  "1.18.0": "de3eea1bc181da629694d19a0817d252a868e4704437fbdd9e923856b8a0890a",
74
78
  "1.19.0": "c5b91e449f6feccaa36a04cb7f8550d3e414eb2795ea6c5dccdf6071206b3261",
75
- "1.20.0": "c4dac40630fc456c6c7e6ba9d1b75215bc76bd76e6f31ba36d3bc9ab867d46e0"
79
+ "1.20.0": "5e7b8d8ddc41f60220961417c9ae01c193fae045378a10a3e9c96a0e3cf10a03",
80
+ "1.21.0": "a1b663b44650b1a04f89c60b26b002322fc6789650291477e14e13329c3ce25f",
81
+ "1.21.1": "9b3eb825a7bec05bb6f0138f77b45fadf896b83020178c34c5a2f531c38f4dee",
82
+ "1.22.0": "3515c663e908b2d6dc9a2a9cf42f2a5bf9a5b3a10c79d43e30cc8427e2ce209d",
83
+ "1.23.0": "908e6622f597108ca696c8ff6f1081b3d83996dc46771a3515b184884dab4b25"
76
84
  }
77
85
  }
78
86
  ]
package/ai/skill.md CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: kerf-app
3
3
  description: Build UIs in the kerf reactive framework and its @kerfjs/ui component package (https://github.com/brianwestphal/kerf). Use this skill whenever the user is writing or modifying code that imports `kerfjs` or `@kerfjs/ui`, asks to add a feature to a kerf app, or asks "how do I do X in kerf?". Use it proactively the moment you spot a kerf import in the file you're editing.
4
- kerf-skill-version: 1.22.0
4
+ kerf-skill-version: 1.25.0
5
5
  ---
6
6
 
7
7
  # Building apps with kerf
@@ -10,14 +10,14 @@ kerf-skill-version: 1.22.0
10
10
  > project's `.claude/skills/kerf-app/SKILL.md`) so Claude Code activates
11
11
  > it whenever you work on a kerf app.
12
12
 
13
- kerf is a ~12 KB reactive UI framework (~13 KB with `arraySignal`): signals + DOM morphing + JSX → HTML strings. No virtual DOM, no compiler, no scheduler. The whole public surface fits in 17 exports.
13
+ kerf is a ~13 KB reactive UI framework (still ~13 KB with `arraySignal`): signals + DOM morphing + JSX → HTML strings. No virtual DOM, no compiler, no scheduler. The whole public surface fits in 17 exports.
14
14
 
15
15
  ## Setup
16
16
 
17
17
  - Install: `npm install kerfjs`
18
18
  - `tsconfig.json`: `"jsx": "react-jsx"`, `"jsxImportSource": "kerfjs"`
19
19
  - Vite / esbuild need no extra config.
20
- - **Dev diagnostics are opt-in by import, and only an APP installs them.** kerf does not infer dev mode. In the app entry add `if (import.meta.env.DEV) await import('kerfjs/dev');` (Vite) or `if (process.env.NODE_ENV !== 'production') await import('kerfjs/dev');` (webpack/Node). That enables the read-only store `get()` snapshot, the throwing dangerous-URL screen, and makes the `KERF_DEV_WARN_*` family available; omitting it is production shape and sheds ~4.7 KB min+gzip because the condition folds away and the chunk is never emitted. Put it FIRST if relying on the untracked-signal warning — `signal()` picks its constructor at creation time.
20
+ - **Dev diagnostics are opt-in by import, and only an APP installs them.** kerf does not infer dev mode. In the app entry add `if (import.meta.env.DEV) await import('kerfjs/dev');` (Vite) or `if (process.env.NODE_ENV !== 'production') await import('kerfjs/dev');` (webpack/Node). That enables the read-only store `get()` snapshot, the throwing dangerous-URL screen, and makes the `KERF_DEV_WARN_*` family available; omitting it is production shape and sheds ~4.7 KB min+gzip because the condition folds away and the chunk is never emitted. Put it (and any `enableWarnings()` call) FIRST if relying on the untracked-signal, delegate-in-effect, or rebuilt-listeners warnings — `signal()`, `effect()`, and `mount()` decide those at creation time, so anything created earlier stays uncovered.
21
21
  - **Switch individual warnings on with `enableWarnings()`**, which is the only switch that works in a browser (no `process` object there, and a bundler `define` cannot reach the read): `const dev = await import('kerfjs/dev'); dev.enableWarnings({ staleBinding: true, narrowSet: true, invariants: 'throw' });`. The `KERF_DEV_WARN_*` env vars do the same for Node/SSR/CI; an explicit call wins either way.
22
22
  - **A component package must NEVER import `kerfjs/dev`.** The hooks are process-global, so installing them is the consuming app's decision — a library that does it forces the diagnostics (and the chunk) on every consumer. Put the import in your demo page or test harness instead.
23
23
  - Recommended companion: `npm install --save-dev eslint-plugin-kerfjs` and add `kerfjs.configs.recommended` to the project's eslint config. Enforces five of the hard rules below (no inline JSX event handlers, require `data-key` in `each()`, capture `delegate()` disposers, no nested `mount()`, prefer module JSX augmentation) at edit time — useful as a self-correction signal when authoring kerf code.
@@ -53,13 +53,23 @@ explicit behavioral helpers and return disposers. Compose `AppTab` inside a
53
53
  controlled `TabBar`; `wireTabBars()` supplies horizontal edge autoscroll during
54
54
  dragging. Apply reorder reports with `reorderTabs()`, and keep order,
55
55
  selection, close policy, routing, panels, and persistence in the host.
56
+ Toolbars draw no divider by default: call `wireScrollDividers()` once at the
57
+ app root so each `Pane` draws its header/footer divider, and each `TabBar` its
58
+ overflow divider, only while content is scrolled beneath or beyond it.
56
59
  Icons and spinners are decorative unless labeled; use assertive banners only
57
60
  for urgent interruption.
58
61
 
59
- The opinionated semantic ramps match Hot Sheet 2 and Web Awesome. Override
60
- `--kui-color-*` globally or component properties such as
61
- `--kui-state-banner-background` at a tone or instance boundary; do not replace
62
- private descendant selectors.
62
+ Components own their styles; configure, never override. Set how a component
63
+ looks and sits through its typed props (size, density, inset, presentation,
64
+ `dividerSides`, `ToolbarConfig`, `PaneConfig`, …); never write application
65
+ CSS that selects a `.kui-*` class, a `[data-component]` root, a `wa-*` element
66
+ a Kerf component renders, or its `::part()`s, and never add padding, margin,
67
+ width, or height around a component to size or space it (it owns its 8px
68
+ inset — adding more double-insets). A missing prop is a component gap to
69
+ report upstream, not a reason to override. The opinionated semantic ramps
70
+ match Hot Sheet 2 and Web Awesome: override `--kui-color-*` globally or
71
+ component properties such as `--kui-state-banner-background` at a tone or
72
+ instance boundary, for theming only.
63
73
 
64
74
  For Web Awesome's broader free component set, import the CSS-only
65
75
  `@kerfjs/ui/webawesome.css` theme once, then import only each Web Awesome
@@ -77,41 +87,53 @@ full AI contract is at `node_modules/@kerfjs/ui/ai/skill.md`.
77
87
 
78
88
  ```ts
79
89
  import {
80
- signal, computed, effect, batch, // reactivity
81
- defineStore, resetAllStores, // stores
82
- mount, morph, each, // render (reactive + one-shot) + keyed list
83
- delegate, delegateCapture, // events
84
- toElement, // direct JSX → DOM Element (or DocumentFragment for multi-root)
85
- SafeHtml, isSafeHtml, raw, Fragment,
86
- } from 'kerfjs';
90
+ signal,
91
+ computed,
92
+ effect,
93
+ batch, // reactivity
94
+ defineStore,
95
+ resetAllStores, // stores
96
+ mount,
97
+ morph,
98
+ each, // render (reactive + one-shot) + keyed list
99
+ delegate,
100
+ delegateCapture, // events
101
+ toElement, // direct JSX → DOM Element (or DocumentFragment for multi-root)
102
+ SafeHtml,
103
+ isSafeHtml,
104
+ raw,
105
+ Fragment,
106
+ type JSXChildren,
107
+ } from "kerfjs";
87
108
 
88
109
  // Optional, only when you need granular collection updates:
89
- import { arraySignal } from 'kerfjs/array-signal';
110
+ import { arraySignal } from "kerfjs/array-signal";
90
111
 
91
112
  // Development diagnostics — gate with YOUR build's dev flag, in YOUR code.
92
- if (import.meta.env.DEV) await import('kerfjs/dev');
113
+ if (import.meta.env.DEV) await import("kerfjs/dev");
93
114
  ```
94
115
 
95
- | Export | Use |
96
- | --- | --- |
97
- | `signal(initial)` | atomic reactive state; `.value` get/set |
98
- | `computed(fn)` | derived value, read-only |
99
- | `effect(fn)` | side effect on signal change; returns disposer |
100
- | `batch(fn)` | coalesce multiple writes into one re-run |
101
- | `defineStore({initial, actions})` | named multi-consumer state |
102
- | `resetAllStores()` | reset every store (test teardown) |
103
- | `mount(el, render)` | bind reactive render to a DOM element; returns disposer |
104
- | `morph(liveRoot, template)` | one-shot reconcile against a populated element (SSR hydration, page-refresh diffs). Template = `Element`, `SafeHtml`, or HTML string |
105
- | `each(items, render, cacheKey?)` | keyed list iteration; per-row memoization on identity (+ optional cacheKey — a passive comparator for external state). Distinct from `data-key` on the rendered element |
106
- | `each(items, render, { cacheKey, key })` | same, options form. **`key` gives the list a stable identity** — required whenever a *conditional* list can render before this one, else kerf rebuilds this list and its rows lose focus/scroll/IME. A keyed list takes no positional slot, so keying the conditional list usually fixes its siblings too |
107
- | `delegate(root, type, sel, h)` | one listener at the root; `closest(selector)` walk from target |
108
- | `delegateCapture(root, type, sel, h, opts?)` | capture-phase escape hatch; `closest()` walk-up by default (same as `delegate`); pass `{ match: 'direct' }` for strict `target.matches()` |
109
- | `attr(name, value)` | pre-computed `AttrSpec<N,V>` — `.selector` for `delegate()`, `.attrs` to spread into JSX (rename-safe) |
110
- | `attr(name)` | dynamic factory — `attr<N,V=string>(name)` returns `(value: V) => { readonly [name]: V }`; both generics off → N inferred, V defaults to string; specify both to constrain values |
111
- | `toElement(jsx)` | parse JSX into a DOM node (SVG-aware). Single-root → `Element`; multi-root (`<><svg/> label</>`, two icons side by side) → `DocumentFragment` that `appendChild`/`replaceChildren`/`append` inlines into the parent. |
112
- | `raw(html)` | inject pre-escaped HTML |
113
- | `arraySignal(initial?)` | granular keyed-list signal (subpath `kerfjs/array-signal`); `each()` reconciles in O(patches) |
114
- | `` html`…` `` | tagged template (subpath `kerfjs/html`) — JSX-identical runtime semantics with NO build step, for CDN/importmap projects. Real HTML attribute names (`class`, not `className`); holes only in text positions or as a COMPLETE attribute value (`attr=${v}` / `attr="${v}"`) |
116
+ | Export | Use |
117
+ | -------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
118
+ | `signal(initial)` | atomic reactive state; `.value` get/set |
119
+ | `computed(fn)` | derived value, read-only |
120
+ | `effect(fn)` | side effect on signal change; returns disposer |
121
+ | `batch(fn)` | coalesce multiple writes into one re-run |
122
+ | `defineStore({initial, actions})` | named multi-consumer state |
123
+ | `resetAllStores()` | reset every store (test teardown) |
124
+ | `mount(el, render)` | bind reactive render to a DOM element; returns disposer |
125
+ | `morph(liveRoot, template)` | one-shot reconcile against a populated element (SSR hydration, page-refresh diffs). Template = `Element`, `SafeHtml`, or HTML string |
126
+ | `each(items, render, cacheKey?)` | keyed list iteration; per-row memoization on identity (+ optional cacheKey — a passive comparator for external state). Distinct from `data-key` on the rendered element |
127
+ | `each(items, render, { cacheKey, key })` | same, options form. **`key` gives the list a stable identity** — required whenever a _conditional_ list can render before this one, else kerf rebuilds this list and its rows lose focus/scroll/IME. A keyed list takes no positional slot, so keying the conditional list usually fixes its siblings too |
128
+ | `delegate(root, type, sel, h)` | one listener at the root; `closest(selector)` walk from target |
129
+ | `delegateCapture(root, type, sel, h, opts?)` | capture-phase escape hatch; `closest()` walk-up by default (same as `delegate`); pass `{ match: 'direct' }` for strict `target.matches()` |
130
+ | `attr(name, value)` | pre-computed `AttrSpec<N,V>` — `.selector` for `delegate()`, `.attrs` to spread into JSX (rename-safe) |
131
+ | `attr(name)` | dynamic factory — `attr<N,V=string>(name)` returns `(value: V) => { readonly [name]: V }`; both generics off → N inferred, V defaults to string; specify both to constrain values |
132
+ | `toElement(jsx)` | parse JSX into a DOM node (SVG-aware). Single-root → `Element`; multi-root (`<><svg/> label</>`, two icons side by side) → `DocumentFragment` that `appendChild`/`replaceChildren`/`append` inlines into the parent. |
133
+ | `raw(html)` | inject pre-escaped HTML |
134
+ | `JSXChildren` | recursive type for general-purpose component `children`: SafeHtml, primitives, read-only signals, nullish/boolean empty values, and readonly arrays at any depth. Use a narrower union for semantically restricted slots |
135
+ | `arraySignal(initial?)` | granular keyed-list signal (subpath `kerfjs/array-signal`); `each()` reconciles in O(patches) |
136
+ | `` html`…` `` | tagged template (subpath `kerfjs/html`) — JSX-identical runtime semantics with NO build step, for CDN/importmap projects. Real HTML attribute names (`class`, not `className`); holes only in text positions or as a COMPLETE attribute value (`attr=${v}` / `attr="${v}"`) |
115
137
 
116
138
  ## Hard rules — every AI assistant gets these wrong at least once
117
139
 
@@ -123,9 +145,9 @@ if (import.meta.env.DEV) await import('kerfjs/dev');
123
145
  - `data-morph-preserve` — element survives the trailing-removal pass even when the new template doesn't emit it. For imperatively-injected children (autoplay video, tooltip overlay, analytics pixel). Does NOT block a keyed-match move.
124
146
  4. **Never `addEventListener` inside a `mount()`-managed tree** unless under `data-morph-skip`. A morph re-render may discard the node. Use `delegate` / `delegateCapture` instead.
125
147
  5. **Capture the `delegate()` / `delegateCapture()` disposer** whenever the registration's scope is shorter than the page. Both helpers return `() => void`; the listener closure pins `rootEl`, `handler`, and everything the handler closes over (stores, signals, app state). Discarding the disposer on a transient root (modal, route view, mount swap, dynamic widget) leaks the listener AND the app graph it captures; re-mount cycles stack listeners linearly. `mount()`'s own disposer does NOT remove delegates for you. Safe to discard only when the registration is truly page-lifetime (root is `document.body` or equivalent, attached once at startup, never torn down).
126
- 6. **One `mount()` per root.** Don't nest `mount()` calls. Compose with plain functions returning JSX.
148
+ 6. **One `mount()` per root.** Don't nest `mount()` calls. Compose with plain functions returning JSX. One exception: a modal `<dialog>` of your own that anchors kerf `popover()` / `tooltip()` surfaces must contain `<div data-kerf-overlay-host data-morph-skip></div>` — kerf renders those surfaces into it (inside the modal, so they are not inert) and that slot is the one allowed nested-mount boundary. Dialogs `kerfjs/overlay` opens get a slot automatically.
127
149
  7. **Components are plain functions.** `<MyComponent props />` works — the JSX runtime calls `MyComponent(props)` and uses the returned JSX — but there's no hook system, no lifecycle, and no per-instance state. State lives in module-scope signals or stores, never in component closures.
128
- 8. **Values bind, structure re-renders.** For a value hole, pass the signal/computed ITSELF (`<span>{count}</span>`, `class={sig}`) — kerf updates that one node directly, no render re-run. Read `.value` only when the JSX *structure* depends on the signal — and then the read must happen INSIDE the render function to be tracked: `const x = count.value; mount(el, () => <span>{x}</span>)` does NOT re-render. One caveat on bound holes: bind a STABLE signal/computed instance per hole (`class={computed(() => …)}` that switches internally), never `class={cond ? sigA : sigB}` — switching instances can go silently stale (detectable via `KERF_DEV_WARN_STALE_BINDING=1`). The idiom's endpoint: a render that reads NO `.value` runs exactly once — a fully bound mount never re-renders. To find `.value` holes worth migrating, `KERF_DEV_WARN_VALUE_ONLY_RERENDER=1` flags re-renders whose only differences were text/attribute values.
150
+ 8. **Values bind, structure re-renders.** For a value hole, pass the signal/computed ITSELF (`<span>{count}</span>`, `class={sig}`) — kerf updates that one node directly, no render re-run. Read `.value` only when the JSX _structure_ depends on the signal — and then the read must happen INSIDE the render function to be tracked: `const x = count.value; mount(el, () => <span>{x}</span>)` does NOT re-render. One caveat on bound holes: bind a STABLE signal/computed instance per hole (`class={computed(() => …)}` that switches internally), never `class={cond ? sigA : sigB}` — switching instances can go silently stale (detectable via `enableWarnings({ staleBinding: true })` (or `KERF_DEV_WARN_STALE_BINDING=1` under Node)). The idiom's endpoint: a render that reads NO `.value` runs exactly once — a fully bound mount never re-renders. To find `.value` holes worth migrating, `enableWarnings({ valueOnlyRerender: true })` (or `KERF_DEV_WARN_VALUE_ONLY_RERENDER=1` under Node) flags re-renders whose only differences were text/attribute values.
129
151
  9. **Store actions take `(set, get)`, not `(state)`.** `set(next)` replaces state; mutating `get()` does nothing.
130
152
  10. **Use `data-action` attributes, not inline `onClick`.** Inline handlers are NOT supported by the JSX → string runtime; delegate from the root.
131
153
  11. **`arraySignal` is opt-in for long keyed lists** where most updates are pointwise. For short lists / filter+sort pipelines, plain `signal` + `each(items.value, ...)` is simpler and equally fast.
@@ -138,22 +160,26 @@ if (import.meta.env.DEV) await import('kerfjs/dev');
138
160
  When deciding which primitive to reach for, work down the axes:
139
161
 
140
162
  **Events.**
163
+
141
164
  - Originates inside the mount tree → `delegate(rootEl, type, sel, handler)`. Originates outside (window-level keyboard, online/offline, beforeunload) → native `window.addEventListener` at module top-level.
142
165
  - Gesture that needs to follow an element after press (drag, draw, resize) → at the start event, `el.setPointerCapture(e.pointerId)`. Subsequent `pointermove` / `pointerup` redirect to the captured element and `delegate(rootEl, 'pointermove', '[data-card]', …)` still picks them up. Don't reach for `window.addEventListener` for in-mount-tree gestures.
143
166
  - Well-known non-bubbler (`focus`, `blur`, `scroll`, `load`, `error`, `mouseenter`, `mouseleave`) → still `delegate()`; it auto-promotes to capture. Custom non-bubblers or capture-phase interception → `delegateCapture()` (also `closest()`-matched by default). Need strict element-match? Add `{ match: 'direct' }` on either helper.
144
167
 
145
168
  **Lists.**
169
+
146
170
  - Items change across renders (todos, chat messages, table rows) → `each(items, render)`.
147
171
  - Static structural enumeration whose row render reads signals → `STATIC.map(item => <jsx/>)`. Inner `each(item.children, …)` still gets keyed reconcile.
148
172
  - Long list with point-wise mutations → `arraySignal` + `each(arraySig, render)` for O(patches) updates.
149
173
 
150
174
  **Side effects / imperative DOM.**
175
+
151
176
  - Library-owned subtree survives across renders → `data-morph-skip` on host.
152
177
  - Host attributes morph but subtree preserved → `data-morph-skip-children`.
153
178
  - Imperatively-injected element survives the trailing-removal pass → `data-morph-preserve`.
154
179
  - Focused input / contenteditable caret survives re-renders → automatic; no opt-in.
155
180
 
156
181
  **Raw HTML.**
182
+
157
183
  - User-controlled HTML → sanitize first (DOMPurify) then `raw(sanitized)`.
158
184
  - Author-controlled trusted HTML → `raw(html)` directly.
159
185
 
@@ -168,20 +194,27 @@ When deciding which primitive to reach for, work down the axes:
168
194
  // directly with no render re-run; read `.value` only when the JSX STRUCTURE
169
195
  // depends on the signal (conditionals, list shape).
170
196
  const count = signal(0);
171
- const ACTIONS = { inc: attr('data-action', 'inc') } as const satisfies Record<string, AttrSpec<'data-action'>>;
197
+ const ACTIONS = { inc: attr("data-action", "inc") } as const satisfies Record<
198
+ string,
199
+ AttrSpec<"data-action">
200
+ >;
172
201
 
173
- mount(document.getElementById('app')!, () => (
202
+ mount(document.getElementById("app")!, () => (
174
203
  <div>
175
204
  <button {...ACTIONS.inc.attrs}>+</button>
176
205
  <span>{count}</span>
177
206
  </div>
178
207
  ));
179
- delegate(rootEl, 'click', ACTIONS.inc.selector, () => { count.value += 1; });
208
+ delegate(rootEl, "click", ACTIONS.inc.selector, () => {
209
+ count.value += 1;
210
+ });
180
211
 
181
212
  // Pattern 2: keyed list with per-row memoization
182
213
  mount(listEl, () => (
183
214
  <ul>
184
- {each(rows.value, (row) => <li data-key={row.id}>{row.label}</li>)}
215
+ {each(rows.value, (row) => (
216
+ <li data-key={row.id}>{row.label}</li>
217
+ ))}
185
218
  </ul>
186
219
  ));
187
220
 
@@ -189,14 +222,15 @@ mount(listEl, () => (
189
222
  const cart = defineStore({
190
223
  initial: () => ({ items: [] as string[] }),
191
224
  actions: (set, get) => ({
192
- add: (id: string) => set({ items: [...get().items, id] }),
193
- clear: () => set({ items: [] }),
225
+ add: (id: string) => set({ items: [...get().items, id] }),
226
+ clear: () => set({ items: [] }),
194
227
  }),
195
228
  });
196
229
  // access: cart.state.value.items, cart.actions.add('x'), cart.reset()
197
230
 
198
231
  // Pattern 4: one-shot reconcile (no signals, no effect)
199
- morph(liveCard, '<article class="card">…</article>');
232
+ // the template describes liveCard's CHILDREN; liveCard's own attributes are untouched
233
+ morph(liveCard, '<h2 class="card-title">…</h2><p>…</p>');
200
234
 
201
235
  // Pattern 5: fine-grained binding (opt-in) — pass the signal/computed ITSELF
202
236
  // into a hole so a change updates ONLY that node (no render re-run, no
@@ -205,9 +239,15 @@ morph(liveCard, '<article class="card">…</article>');
205
239
  const selectedId = signal<number | null>(null);
206
240
  mount(listEl, () => (
207
241
  <ul>
208
- {each(rows.value, (row) => (
209
- <li class={computed(() => (row.id === selectedId.value ? 'sel' : ''))}>{row.label}</li>
210
- ), (row) => row.id)}
242
+ {each(
243
+ rows.value,
244
+ (row) => (
245
+ <li class={computed(() => (row.id === selectedId.value ? "sel" : ""))}>
246
+ {row.label}
247
+ </li>
248
+ ),
249
+ (row) => row.id,
250
+ )}
211
251
  </ul>
212
252
  ));
213
253
  // selectedId.value = 3 → only the ~2 affected <li> class attrs update.
@@ -215,34 +255,39 @@ mount(listEl, () => (
215
255
  // Pattern 6: no build step (CDN / importmap) — the html tagged template
216
256
  // instead of JSX. Same runtime semantics; real HTML attribute names; a hole
217
257
  // must be a text position or a COMPLETE attribute value (partials throw).
218
- import { html } from 'kerfjs/html';
219
- mount(rootEl, () => html`
220
- <div class="${cls}">Count: ${count}</div>
221
- <ul>${each(rows.value, (row) => html`<li data-key="${row.id}">${row.label}</li>`)}</ul>
222
- `);
258
+ import { html } from "kerfjs/html";
259
+ mount(
260
+ rootEl,
261
+ () => html`
262
+ <div class="${cls}">Count: ${count}</div>
263
+ <ul>
264
+ ${each(rows.value, (row) => html`<li data-key="${row.id}">${row.label}</li>`)}
265
+ </ul>
266
+ `,
267
+ );
223
268
  ```
224
269
 
225
270
  ## Diagnosing common errors
226
271
 
227
- | Error / symptom | Root cause | Fix |
228
- | --- | --- | --- |
229
- | `JSX: DOM elements cannot be passed as children` | passed a `toElement()` result inside JSX | Build the whole tree in JSX; refs via `querySelector` after rendering |
230
- | `draggable={true}` / `spellCheck={false}` / `contentEditable={false}` / `writingsuggestions={false}` / `translate={false}` / `autocorrect={false}` won't typecheck | these are HTML **enumerated** attributes, not boolean ones — they take keyword strings (`"true"`/`"false"`; `"yes"`/`"no"` for `translate`; `"on"`/`"off"` for `autocorrect`), and omitting one selects a third state, so the boolean form rendered the opposite of what was meant | Write the keyword: `draggable="true"`, `spellCheck="false"`, `writingsuggestions="false"`, `translate="no"`, `autocorrect="off"`. Omit for the default state. Real boolean attrs (`hidden`, `checked`, `disabled`, `autofocus`, `required`, `inert`) are unaffected, as is `popover` (bare form = the spec's `auto` state); for a signal use `signal('true')` |
231
- | `<select value={x}>` / `<textarea value={x}>` won't typecheck | neither element has a `value` content attribute — the markup was inert | `<option value="b" selected>`; `<textarea>{draft}</textarea>` |
232
- | Focus / cursor lost on every keystroke | list items lack `data-key` | Add `data-key` (or `id`) to each list item |
233
- | Click handler stops firing after re-render | `el.addEventListener` was used | Replace with `delegate(rootEl, 'click', ACTIONS.foo.selector, ...)` (or a string literal for ad-hoc cases) |
234
- | Render fn never re-runs | signal was read outside the render fn | Move `signal.value` read inside the render fn |
235
- | SVG renders as broken / namespaceless markup | `innerHTML` used directly | Use `mount` or `toElement` (SVG-aware) |
236
- | Library widget destroyed on every render | host reachable by the morph | Wrap host in `data-morph-skip`; mount the library imperatively after first render |
237
- | `<my-tag>` fails to typecheck | declaration merging targeted global JSX | Use `declare module 'kerfjs/jsx-runtime' { namespace JSX { … } }` instead |
238
- | `each(): row render at index N produced K top-level elements` | row returned multiple sibling elements or zero | Wrap them in one parent so the row renders exactly one element |
239
- | Drag/drop / state change has no visible effect; only elements *outside* `each()` update | Used `each(STATIC_ARRAY, …)` whose row render reads signals. Items never change identity → cache hits forever → row render never re-invoked → signal reads stop tracking | Replace outer with `STATIC_ARRAY.map(...)`; keep inner `each()` for the dynamic sub-list. See Hard Rule 14 |
240
- | Row-enter CSS animation no longer replays when only a row's *content* changed (kerf ≥ 0.15.0) | 0.15.0+ morphs a same-identity, same-position row *in place* instead of recreating its node, so a mount-keyed `@keyframes` never re-triggers on a content-only update (≤ 0.14.x recreated the node, so it fired). Intentional flip side: focus, scroll, IME, and in-progress transitions now survive | Key the animation on a state-class toggle, not element creation. To force a remount, churn the row's identity (new object ref / `data-key`) so the reconciler replaces the node |
241
- | Want a hot spot to update without re-running the whole render | Fine-grained binding: pass the signal/`computed` ITSELF into the attr/text hole (`class={computed(() => …)}`), not `.value`. Use `computed()` not a bare `() => …` (memoization keeps a shared-signal flip to ~O(changed nodes)). Opt-in per hole. Limit: a bound hole depending on the row's OWN mutated data goes stale on a granular in-place update — use plain interpolation there |
242
- | `` html`` ``: partial attribute values are not supported | In `kerfjs/html` templates a hole must be the COMPLETE attribute value | Build the full string first (`` class="${`a ${b}`}" ``), or bind `class="${computed(() => `a ${b.value}`)}"` for a reactive one |
243
- | An `each()` list's rows lose focus / scroll / typing state when an unrelated conditional list above them appears or disappears (kerf warns about this in dev) | Lists without a key are identified by their position among the render's `each()` calls, so adding/removing one above shifts this list's identity and kerf rebuilds it | Give the lists stable keys: `each(items, render, { key: 'results' })`. Keying just the conditional list is usually enough |
244
- | Keyed `each()` list suddenly renders zero rows — only its `<!--kf-list:N-->` marker — with no errors, and it never recovers (kerfjs ≤ 2.0.1) | A conditionally-rendered sibling BEFORE the list (possibly higher in the tree, e.g. an error banner) was removed that render; older kerfjs rebuilt the shifted list container from the template, permanently detaching the list's internal binding | Upgrade kerfjs (fixed after 2.0.1 — the morph now moves the shifted container up in place, keeping node identity). On older versions, keep the structure before the list stable: wrap the conditional in an always-present container (`<div class="banners">{cond ? <div/> : ''}</div>`) |
245
- | A numbered / zebra-striped / "N of M" `each()` list shows the wrong number on rows that MOVED (reorder, or non-tail insert/remove), while unmoved rows look right | The render fn's `index` argument is NOT part of the memo key (only item identity + `cacheKey` + content version are), so a row that keeps identity but changes position keeps the HTML it rendered at its old index | Fold the index into the memo key so displaced rows re-render: `each(items, (it, i) => …, { cacheKey: (_, i) => i })` (add `key` if used). Opt-in dev warn: `KERF_DEV_WARN_STALE_INDEX=1` |
272
+ | Error / symptom | Root cause | Fix |
273
+ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
274
+ | `JSX: DOM elements cannot be passed as children` | passed a `toElement()` result inside JSX | Build the whole tree in JSX; refs via `querySelector` after rendering |
275
+ | `draggable={true}` / `spellCheck={false}` / `contentEditable={false}` / `writingsuggestions={false}` / `translate={false}` / `autocorrect={false}` won't typecheck | these are HTML **enumerated** attributes, not boolean ones — they take keyword strings (`"true"`/`"false"`; `"yes"`/`"no"` for `translate`; `"on"`/`"off"` for `autocorrect`), and omitting one selects a third state, so the boolean form rendered the opposite of what was meant | Write the keyword: `draggable="true"`, `spellCheck="false"`, `writingsuggestions="false"`, `translate="no"`, `autocorrect="off"`. Omit for the default state. Real boolean attrs (`hidden`, `checked`, `disabled`, `autofocus`, `required`, `inert`) are unaffected, as is `popover` (bare form = the spec's `auto` state); for a signal use `signal('true')` |
276
+ | `<select value={x}>` / `<textarea value={x}>` won't typecheck | neither element has a `value` content attribute — the markup was inert | `<option value="b" selected>`; `<textarea>{draft}</textarea>` |
277
+ | Focus / cursor lost on every keystroke | list items lack `data-key` | Add `data-key` (or `id`) to each list item |
278
+ | Click handler stops firing after re-render | `el.addEventListener` was used | Replace with `delegate(rootEl, 'click', ACTIONS.foo.selector, ...)` (or a string literal for ad-hoc cases) |
279
+ | Render fn never re-runs | signal was read outside the render fn | Move `signal.value` read inside the render fn |
280
+ | SVG renders as broken / namespaceless markup | `innerHTML` used directly | Use `mount` or `toElement` (SVG-aware) |
281
+ | Library widget destroyed on every render | host reachable by the morph | Wrap host in `data-morph-skip`; mount the library imperatively after first render |
282
+ | `<my-tag>` fails to typecheck | declaration merging targeted global JSX | Use `declare module 'kerfjs/jsx-runtime' { namespace JSX { … } }` instead |
283
+ | `each(): row render at index N produced K top-level elements` | row returned multiple sibling elements or zero | Wrap them in one parent so the row renders exactly one element |
284
+ | Drag/drop / state change has no visible effect; only elements _outside_ `each()` update | Used `each(STATIC_ARRAY, …)` whose row render reads signals. Items never change identity → cache hits forever → row render never re-invoked → signal reads stop tracking | Replace outer with `STATIC_ARRAY.map(...)`; keep inner `each()` for the dynamic sub-list. See Hard Rule 14 |
285
+ | Row-enter CSS animation no longer replays when only a row's _content_ changed (kerf ≥ 0.15.0) | 0.15.0+ morphs a same-identity, same-position row _in place_ instead of recreating its node, so a mount-keyed `@keyframes` never re-triggers on a content-only update (≤ 0.14.x recreated the node, so it fired). Intentional flip side: focus, scroll, IME, and in-progress transitions now survive | Key the animation on a state-class toggle, not element creation. To force a remount, churn the row's identity (new object ref / `data-key`) so the reconciler replaces the node |
286
+ | Want a hot spot to update without re-running the whole render | Fine-grained binding: pass the signal/`computed` ITSELF into the attr/text hole (`class={computed(() => …)}`), not `.value`. Use `computed()` not a bare `() => …` (memoization keeps a shared-signal flip to ~O(changed nodes)). Opt-in per hole. Limit: a bound hole depending on the row's OWN mutated data goes stale on a granular in-place update — use plain interpolation there |
287
+ | ` html` ``: partial attribute values are not supported | In `kerfjs/html` templates a hole must be the COMPLETE attribute value | Build the full string first (``class="${`a ${b}`}"``), or bind `class="${computed(() => `a ${b.value}`)}"` for a reactive one |
288
+ | An `each()` list's rows lose focus / scroll / typing state when an unrelated conditional list above them appears or disappears (kerf warns about this in dev) | Lists without a key are identified by their position among the render's `each()` calls, so adding/removing one above shifts this list's identity and kerf rebuilds it | Give the lists stable keys: `each(items, render, { key: 'results' })`. Keying just the conditional list is usually enough |
289
+ | Keyed `each()` list suddenly renders zero rows — only its `<!--kf-list:N-->` marker — with no errors, and it never recovers (kerfjs ≤ 2.0.1) | A conditionally-rendered sibling BEFORE the list (possibly higher in the tree, e.g. an error banner) was removed that render; older kerfjs rebuilt the shifted list container from the template, permanently detaching the list's internal binding | Upgrade kerfjs (fixed after 2.0.1 — the morph now moves the shifted container up in place, keeping node identity). On older versions, keep the structure before the list stable: wrap the conditional in an always-present container (`<div class="banners">{cond ? <div/> : ''}</div>`) |
290
+ | A numbered / zebra-striped / "N of M" `each()` list shows the wrong number on rows that MOVED (reorder, or non-tail insert/remove), while unmoved rows look right | The render fn's `index` argument is NOT part of the memo key (only item identity + `cacheKey` + content version are), so a row that keeps identity but changes position keeps the HTML it rendered at its old index | Fold the index into the memo key so displaced rows re-render: `each(items, (it, i) => …, { cacheKey: (_, i) => i })` (add `key` if used). Opt-in dev warn: `enableWarnings({ staleIndex: true })` (or `KERF_DEV_WARN_STALE_INDEX=1` under Node) |
246
291
 
247
292
  ## Workflow guidance
248
293
 
package/dist/actions.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { A as AttrSpec } from './attr-Cmu2ZoGO.js';
2
- import { D as DelegateOptions } from './delegate-CL9VTZFb.js';
2
+ import { D as DelegateOptions } from './delegate--CrfgYtC.js';
3
3
 
4
4
  /**
5
5
  * `kerfjs/actions` — the delegated action-table helper.
package/dist/actions.js CHANGED
@@ -1,5 +1,5 @@
1
- import { attr } from './chunk-UZJ6I4T6.js';
2
- import { delegate } from './chunk-KPXIOG2C.js';
1
+ import { attr } from './chunk-MK42GLPV.js';
2
+ import { _delegate, NON_BUBBLING } from './chunk-JN7EJM3B.js';
3
3
 
4
4
 
5
5
  // src/actions.ts
@@ -9,15 +9,19 @@ function action(value) {
9
9
  }
10
10
  function delegateActions(root, eventType, table, options) {
11
11
  const attrName = options?.attr ?? DEFAULT_ACTION_ATTR;
12
- return delegate(
12
+ return _delegate(
13
13
  root,
14
14
  eventType,
15
15
  `[${attrName}]`,
16
16
  (event, el) => {
17
- const handler = table[el.getAttribute(attrName)];
18
- if (handler !== void 0) handler(event, el);
17
+ const value = el.getAttribute(attrName);
18
+ return () => {
19
+ if (Object.hasOwn(table, value)) table[value](event, el);
20
+ };
19
21
  },
20
- options
22
+ options,
23
+ NON_BUBBLING.has(eventType),
24
+ "delegate"
21
25
  );
22
26
  }
23
27
 
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/actions.ts"],"names":[],"mappings":";;;;;AAmCA,IAAM,mBAAA,GAAsB,aAAA;AAerB,SAAS,OAAyB,KAAA,EAAsC;AAC7E,EAAA,OAAO,IAAA,CAAK,qBAAqB,KAAK,CAAA;AACxC;AAwBO,SAAS,eAAA,CACd,IAAA,EACA,SAAA,EACA,KAAA,EACA,OAAA,EACY;AACZ,EAAA,MAAM,QAAA,GAAW,SAAS,IAAA,IAAQ,mBAAA;AAClC,EAAA,OAAO,QAAA;AAAA,IACL,IAAA;AAAA,IACA,SAAA;AAAA,IACA,IAAI,QAAQ,CAAA,CAAA,CAAA;AAAA,IACZ,CAAC,OAAO,EAAA,KAAO;AAEb,MAAA,MAAM,OAAA,GAAU,KAAA,CAAM,EAAA,CAAG,YAAA,CAAa,QAAQ,CAAW,CAAA;AACzD,MAAA,IAAI,OAAA,KAAY,MAAA,EAAW,OAAA,CAAQ,KAAA,EAAO,EAAE,CAAA;AAAA,IAC9C,CAAA;AAAA,IACA;AAAA,GACF;AACF","file":"actions.js","sourcesContent":["/**\n * `kerfjs/actions` — the delegated action-table helper.\n *\n * The most-reinvented idiom across real kerf apps: one table of `data-action`\n * attribute specs used as the single source of truth for BOTH the JSX attribute\n * and the delegate selector, plus a hand-rolled `switch (dataset.action)`\n * dispatcher. This subpath blesses it as two thin helpers over the existing\n * `attr()` + `delegate()` — it does NOT replace them.\n *\n * import { action, delegateActions } from 'kerfjs/actions';\n *\n * const A = {\n * select: action('select-file'),\n * remove: action('remove-file'),\n * };\n *\n * // JSX — spread the attr (rename-safe; no hardcoded attribute name):\n * // <button {...A.select.attrs} data-id={id}>…</button>\n *\n * // Wire the whole table with ONE delegated listener; returns a disposer:\n * const dispose = delegateActions(root, 'click', {\n * [A.select.value]: (_e, el) => selectFile(el.getAttribute('data-id')),\n * [A.remove.value]: (_e, el) => removeFile(el.getAttribute('data-id')),\n * });\n *\n * Contract: `delegateActions` returns a `() => void` disposer and holds no\n * per-instance state — the same shape as `delegate()`, which it builds on (so\n * it inherits the single-listener dispatch and the capture auto-promotion for\n * well-known non-bubbling event types). One event type per call, mirroring\n * `delegate()`; collect the disposers for a root that needs several.\n */\nimport { attr, type AttrSpec } from './attr.js';\nimport { delegate, type DelegateOptions } from './delegate.js';\n\n/** The attribute an action table keys on by default. */\nconst DEFAULT_ACTION_ATTR = 'data-action';\n\n/**\n * A handler in a {@link delegateActions} table. Receives the DOM event and the\n * matched element (walk-up `closest()` match by default) — the same shape as a\n * `delegate()` handler.\n */\nexport type ActionHandler<E extends Element = Element> = (event: Event, el: E) => void;\n\n/**\n * `action(value)` — an {@link AttrSpec} on `data-action`. A thin specialization\n * of `attr('data-action', value)`: spread its `.attrs` in JSX and use its\n * `.value` as the handler-table key, so the action name lives in exactly one\n * place and can't drift between the markup and the dispatcher.\n */\nexport function action<V extends string>(value: V): AttrSpec<'data-action', V> {\n return attr(DEFAULT_ACTION_ATTR, value);\n}\n\n/** Options for {@link delegateActions}. Extends {@link DelegateOptions}. */\nexport interface DelegateActionsOptions extends DelegateOptions {\n /**\n * The attribute the table keys on. Default `'data-action'`. Override it only\n * if you also author the specs with `attr(yourName, …)` instead of `action()`.\n */\n attr?: string;\n}\n\n/**\n * Wire a whole table of action handlers with ONE delegated listener.\n *\n * On `eventType`, the nearest element carrying the action attribute (walk-up\n * `closest()` by default; pass `{ match: 'direct' }` for an exact-element match)\n * is looked up in `table` by its attribute value, and the matching handler\n * runs. An element whose action is absent from the table is ignored — the same\n * behavior as a `switch (dataset.action)` with no matching `case`.\n *\n * Returns a `() => void` disposer. One event type per call (the smallest\n * surface, mirroring `delegate()`); collect the disposers when a root needs\n * several event types.\n */\nexport function delegateActions<E extends Element = Element>(\n root: HTMLElement,\n eventType: string,\n table: Readonly<Record<string, ActionHandler<E>>>,\n options?: DelegateActionsOptions,\n): () => void {\n const attrName = options?.attr ?? DEFAULT_ACTION_ATTR;\n return delegate<E>(\n root,\n eventType,\n `[${attrName}]`,\n (event, el) => {\n // `el` matched `[${attrName}]`, so the attribute is always present.\n const handler = table[el.getAttribute(attrName) as string];\n if (handler !== undefined) handler(event, el);\n },\n options,\n );\n}\n"]}
1
+ {"version":3,"sources":["../src/actions.ts"],"names":[],"mappings":";;;;;AAmCA,IAAM,mBAAA,GAAsB,aAAA;AAkBrB,SAAS,OAAyB,KAAA,EAAsC;AAC7E,EAAA,OAAO,IAAA,CAAK,qBAAqB,KAAK,CAAA;AACxC;AAwBO,SAAS,eAAA,CACd,IAAA,EACA,SAAA,EACA,KAAA,EACA,OAAA,EACY;AACZ,EAAA,MAAM,QAAA,GAAW,SAAS,IAAA,IAAQ,mBAAA;AAKlC,EAAA,OAAO,SAAA;AAAA,IACL,IAAA;AAAA,IACA,SAAA;AAAA,IACA,IAAI,QAAQ,CAAA,CAAA,CAAA;AAAA,IACZ,CAAC,OAAO,EAAA,KAAO;AAEb,MAAA,MAAM,KAAA,GAAQ,EAAA,CAAG,YAAA,CAAa,QAAQ,CAAA;AAKtC,MAAA,OAAO,MAAM;AACX,QAAA,IAAI,MAAA,CAAO,OAAO,KAAA,EAAO,KAAK,GAAG,KAAA,CAAM,KAAK,CAAA,CAAE,KAAA,EAAO,EAAO,CAAA;AAAA,MAC9D,CAAA;AAAA,IACF,CAAA;AAAA,IACA,OAAA;AAAA,IACA,YAAA,CAAa,IAAI,SAAS,CAAA;AAAA,IAC1B;AAAA,GACF;AACF","file":"actions.js","sourcesContent":["/**\n * `kerfjs/actions` — the delegated action-table helper.\n *\n * The most-reinvented idiom across real kerf apps: one table of `data-action`\n * attribute specs used as the single source of truth for BOTH the JSX attribute\n * and the delegate selector, plus a hand-rolled `switch (dataset.action)`\n * dispatcher. This subpath blesses it as two thin helpers over the existing\n * `attr()` + `delegate()` — it does NOT replace them.\n *\n * import { action, delegateActions } from 'kerfjs/actions';\n *\n * const A = {\n * select: action('select-file'),\n * remove: action('remove-file'),\n * };\n *\n * // JSX — spread the attr (rename-safe; no hardcoded attribute name):\n * // <button {...A.select.attrs} data-id={id}>…</button>\n *\n * // Wire the whole table with ONE delegated listener; returns a disposer:\n * const dispose = delegateActions(root, 'click', {\n * [A.select.value]: (_e, el) => selectFile(el.getAttribute('data-id')),\n * [A.remove.value]: (_e, el) => removeFile(el.getAttribute('data-id')),\n * });\n *\n * Contract: `delegateActions` returns a `() => void` disposer and holds no\n * per-instance state — the same shape as `delegate()`, which it builds on (so\n * it inherits the single-listener dispatch and the capture auto-promotion for\n * well-known non-bubbling event types). One event type per call, mirroring\n * `delegate()`; collect the disposers for a root that needs several.\n */\nimport { attr, type AttrSpec } from './attr.js';\nimport { _delegate, type DelegateOptions, NON_BUBBLING } from './delegate.js';\n\n/** The attribute an action table keys on by default. */\nconst DEFAULT_ACTION_ATTR = 'data-action';\n\n/**\n * A handler in a {@link delegateActions} table. Receives the DOM event and the\n * matched element (walk-up `closest()` match by default) — the same shape as a\n * `delegate()` handler.\n */\nexport type ActionHandler<E extends Element = Element> = (\n event: Event,\n el: E,\n) => void;\n\n/**\n * `action(value)` — an {@link AttrSpec} on `data-action`. A thin specialization\n * of `attr('data-action', value)`: spread its `.attrs` in JSX and use its\n * `.value` as the handler-table key, so the action name lives in exactly one\n * place and can't drift between the markup and the dispatcher.\n */\nexport function action<V extends string>(value: V): AttrSpec<'data-action', V> {\n return attr(DEFAULT_ACTION_ATTR, value);\n}\n\n/** Options for {@link delegateActions}. Extends {@link DelegateOptions}. */\nexport interface DelegateActionsOptions extends DelegateOptions {\n /**\n * The attribute the table keys on. Default `'data-action'`. Override it only\n * if you also author the specs with `attr(yourName, …)` instead of `action()`.\n */\n attr?: string;\n}\n\n/**\n * Wire a whole table of action handlers with ONE delegated listener.\n *\n * On `eventType`, the nearest element carrying the action attribute (walk-up\n * `closest()` by default; pass `{ match: 'direct' }` for an exact-element match)\n * is looked up in `table` by its attribute value, and the matching handler\n * runs. An element whose action is absent from the table is ignored — the same\n * behavior as a `switch (dataset.action)` with no matching `case`.\n *\n * Returns a `() => void` disposer. One event type per call (the smallest\n * surface, mirroring `delegate()`); collect the disposers when a root needs\n * several event types.\n */\nexport function delegateActions<E extends Element = Element>(\n root: HTMLElement,\n eventType: string,\n table: Readonly<Record<string, ActionHandler<E>>>,\n options?: DelegateActionsOptions,\n): () => void {\n const attrName = options?.attr ?? DEFAULT_ACTION_ATTR;\n // The action value is read when the event STARTS dispatching (with the\n // match itself — see `_delegate`), not from the live element when the\n // handler runs: an earlier handler's synchronous re-render may have\n // recycled the matched element into a different action by then.\n return _delegate(\n root,\n eventType,\n `[${attrName}]`,\n (event, el) => {\n // `el` matched `[${attrName}]`, so the attribute is always present.\n const value = el.getAttribute(attrName) as string;\n // Own entries only: the table is an ordinary object, so a bare\n // `table[value]` would resolve Object.prototype names (`__proto__`,\n // `constructor`, `toString`, `hasOwnProperty`, …) and dispatch or throw\n // on markup that names no action in the table.\n return () => {\n if (Object.hasOwn(table, value)) table[value](event, el as E);\n };\n },\n options,\n NON_BUBBLING.has(eventType),\n 'delegate',\n );\n}\n"]}
@@ -1,4 +1,4 @@
1
- export { ARRAY_SIGNAL_BRAND, ArraySignal, arraySignal } from './chunk-LVH3GC6B.js';
1
+ export { ARRAY_SIGNAL_BRAND, ArraySignal, arraySignal } from './chunk-H7WTRM6F.js';
2
2
 
3
3
 
4
4
 
package/dist/async.js CHANGED
@@ -1,4 +1,4 @@
1
- import { signal } from './chunk-U6FK33SG.js';
1
+ import { signal } from './chunk-IHMDBBUJ.js';
2
2
 
3
3
 
4
4
  // src/async.ts
@@ -49,7 +49,13 @@ function resource(options = {}) {
49
49
  };
50
50
  const fail = (error) => {
51
51
  if (gen === generation) {
52
- state.value = { ...state.value, status: "failed", error, progress: void 0, input };
52
+ state.value = {
53
+ ...state.value,
54
+ status: "failed",
55
+ error,
56
+ progress: void 0,
57
+ input
58
+ };
53
59
  }
54
60
  return void 0;
55
61
  };
@@ -59,23 +65,20 @@ function resource(options = {}) {
59
65
  } catch (error) {
60
66
  return Promise.resolve(fail(error));
61
67
  }
62
- return pending.then(
63
- (data) => {
64
- if (gen === generation) {
65
- if (key !== void 0) cache.set(key, data);
66
- state.value = {
67
- status: "completed",
68
- data,
69
- error: void 0,
70
- progress: void 0,
71
- input,
72
- revision: commit(data)
73
- };
74
- }
75
- return data;
76
- },
77
- fail
78
- );
68
+ return pending.then((data) => {
69
+ if (gen === generation) {
70
+ if (key !== void 0) cache.set(key, data);
71
+ state.value = {
72
+ status: "completed",
73
+ data,
74
+ error: void 0,
75
+ progress: void 0,
76
+ input,
77
+ revision: commit(data)
78
+ };
79
+ }
80
+ return data;
81
+ }, fail);
79
82
  }
80
83
  function reset() {
81
84
  generation++;