kerfjs 5.0.0-beta.56 → 5.0.0-beta.57

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -6,6 +6,336 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ - **`ui-public-boundaries` inspects `kerfjs/html` tagged templates.** The
10
+ no-build `html` template writes the same markup as JSX, so its static
11
+ `class` attributes now carry `KUI-L101` (private `kui-*` anatomy) and
12
+ `KUI-L103` (a component's anatomy on your own element):
13
+ ``html`<aside class="kui-pane">` `` is reported with `Pane` to render, and a
14
+ plain ``html`<div class="kui-content-item">` `` with `ContentItem`, while a
15
+ `<ul>` carrier keeps the placeable class. The template's static parts are
16
+ read by the runtime's own rules; a class a hole supplies (`class=${cls}`)
17
+ is unknown and skipped.
18
+
19
+ - **`KUI-L103`'s "render X instead" answer has one implementation.**
20
+ `@kerfjs/ui` ships `ai/component-class-owners.cjs`, the rule that decides
21
+ which cataloged classes are a component's anatomy and which export renders
22
+ each. `eslint-plugin-kerfjs`'s `ui-public-boundaries` loads it beside
23
+ `application-ui-profile-sync.cjs`, and the generated `ai/components/*.md`
24
+ pages use it too, so the lint message and the component reference can no
25
+ longer name different components (a test pins their agreement over the whole
26
+ shipped catalog). A vendored `profileContractPath` directory must now also
27
+ hold `component-class-owners.cjs`; an `@kerfjs/ui` without it reports
28
+ `KUI-L090`.
29
+
30
+ - **A delegated click no longer re-fires on a target the re-render recycled.**
31
+ A delegated handler that writes a signal makes `mount()` re-render
32
+ synchronously, inside the same event dispatch; when the morph recycled the
33
+ clicked element in place into a control with a different `data-action`,
34
+ delegates registered later for the same event matched the recycled element
35
+ and fired too (a `request` click immediately ran `cancel` and undid itself).
36
+ `delegate()`, `delegateCapture()`, and `kerfjs/actions`' `delegateActions()`
37
+ now resolve every delegate's match against the DOM as it stood when the
38
+ event reached the first kerf delegate listener, like per-element listeners:
39
+ a delegate that matched at dispatch start still fires (unless an earlier
40
+ handler removed its element), one that matches only after a re-render does
41
+ not, and `delegateActions()` dispatches by the action value the element
42
+ carried at dispatch start. `mount()` stays synchronous. The
43
+ wrap-it-in-a-distinct-element workaround is no longer needed. Cost: about
44
+ 0.11 KB min+gzip, which moves the advertised core size from ~12 KB to
45
+ ~13 KB.
46
+
47
+ - **Component packages declare `placeableClasses`, and `KUI-L103` covers
48
+ their anatomy classes.** `create-kerf-component`'s `kerf.components.json`
49
+ accepts optional `boundaries.placeableClasses` (a subset of
50
+ `publicClasses`) and `boundaries.rootElement`, validates them, and copies
51
+ them into the generated `component-composition.json`.
52
+ `eslint-plugin-kerfjs`'s `ui-public-boundaries` now checks every class the
53
+ application or third-party catalogs in `.kerf-ui-profile.json` declare, not
54
+ only `kui-*` names: `<div class="acme-meter">` is reported with the
55
+ package's component to render (`Meter`), while its placeable classes stay
56
+ allowed. The scaffold template's README explains the field, and
57
+ `@kerfjs/ui/ai/skill.md` (1.50.0) applies the rule to package entries.
58
+
59
+ - **`eslint-plugin-kerfjs` `ui-public-boundaries` reports a plain
60
+ `<div class="kui-content-item">` as `ContentItem` recreated (`KUI-L103`).**
61
+ The `kui-content-item` classes stay placeable on another carrier (a `<ul>`, a
62
+ `Text`, a control-cluster `<footer>`), but a plain `<div>` carrying them is
63
+ exactly what `ContentItem` renders; the diagnostic now says to render
64
+ `ContentItem`. The `@kerfjs/ui` catalogs gain an additive `rootElement`
65
+ field (selection entry and composition `boundaries.rootElement`) naming the
66
+ element a component renders around its own placeable classes, and
67
+ `check:guidance` derives its repository check from the same fields, so the
68
+ package check and the downstream diagnostic cannot drift. The rule also
69
+ inspects the static class names of a template-literal `class` /
70
+ `className` value.
71
+
72
+ - **`eslint-plugin-kerfjs` `ui-public-boundaries` reports a component's
73
+ anatomy class written onto an application-owned element (`KUI-L103`).**
74
+ `<div class="kui-toolbar">`, `<aside class="kui-pane">`, or
75
+ `<wa-dropdown class="kui-popup-menu">` recreates a component by class and
76
+ inherits (and invites restyling of) its CSS; the diagnostic names the
77
+ component to render instead (`Toolbar`, `Pane`, `PopupMenu`). The
78
+ `@kerfjs/ui` catalogs gain `placeableClasses` (selection entry field and
79
+ composition `boundaries.placeableClasses`) naming the public classes an
80
+ application may place itself — the layout utilities, `kui-app-root`, the
81
+ `kui-content-item` geometry on a non-div carrier, and Web Awesome
82
+ `hide-actions` — and `check:catalog` requires every class-only entry to list
83
+ its classes there. A component's own element (`<Toolbar className="kui-toolbar">`)
84
+ is not reported. The README pane example and `@kerfjs/ui/ai/skill.md`
85
+ (1.47.0) now render `Pane` instead of writing its classes, and each generated
86
+ `ai/components/*.md` page states which classes are placeable and which
87
+ component renders the rest.
88
+
89
+ - **`kerf-ui-analyze` retires `KUI-L005` ("forced component dimension").**
90
+ `KUI-L019` already reports any application rule whose subject is a cataloged
91
+ component, dimensions included, so `.kui-pane { width: 300px }` was reported
92
+ twice (an error and a review finding). L005's only other shape, a public
93
+ class as an ancestor (`.kui-pane .app-sidebar { width: 240px }`), sizes the
94
+ application's own element and is allowed. The analyzer no longer emits it;
95
+ the id stays in the diagnostic registry (now listing it under `retired`) so
96
+ existing profile exceptions that name it still load.
97
+
98
+ - **`KUI-L301` now reports every `Discouraged` Web Awesome element rendered
99
+ directly.** The `@kerfjs/ui` package-default application profile avoids all
100
+ fifteen Discouraged entries (Dropdown/Dropdown Item → `PopupMenu`,
101
+ Select/Option → `Select`, Tab Group/Tab/Tab Panel → `TabBar`, Icon →
102
+ `LucideIcon`, Split Panel → `ResizableRegion`, Button Group →
103
+ `SegmentedControl`, Tree/Tree Item → `List`, and Animated Image, Comparison,
104
+ and Zoomable Frame → `ContentItem`), each with a rationale, and
105
+ `eslint-plugin-kerfjs` `ui-preferences` (and `kerf-ui-doctor`, which runs it)
106
+ now resolves a custom-element JSX tag such as `<wa-dropdown>` to its catalog
107
+ entry, not only an imported component. A declared component's own source
108
+ stays exempt because it is the wrapper that owns the element. A test keeps
109
+ the profile's avoid set equal to the catalog's Discouraged set in both
110
+ directions and lints the UX demo, recipes, and catalog shell clean;
111
+ `@kerfjs/ui/ai/skill.md` (1.46.0) names the diagnostic.
112
+
113
+ - **New (`@kerfjs/ui`): `NavStack` and `TabScaffold` take `chromeDividers`
114
+ (`'scroll' | 'always' | 'none'`), like `Pane`.** `scroll` (the default) keeps
115
+ today's behavior: the stack's top-chrome and bottom-toolbar lines and the
116
+ scaffold's bar line follow the scroll once `wireScrollDividers` is installed.
117
+ `always` draws them permanently without the wiring; `none` never draws them,
118
+ even when wired. The top `Toolbar`'s own `toolbarConfig.dividerSides` is
119
+ unaffected. `SplitView` forwards it to its compact stack through
120
+ `compactStack.chromeDividers`.
121
+
122
+ - **Fix (`@kerfjs/ui`): a `Pane` that is the only content of a `NavStack` view
123
+ or a `TabScaffold` scene fills it.** It used to size to its content, so the
124
+ view scrolled the whole Pane and its header scrolled away. Now the Pane's own
125
+ content scrolls under its pinned header and footer, the Pane's slots take the
126
+ safe-area edges the view or scene still reaches, and scroll dividers key on
127
+ the Pane's content. The rule lives in `pane.css`, as the Workbench-region fill
128
+ does.
129
+
130
+ - **The `@kerfjs/ui` suite-v3 AI-regression protocol test runs in every gate
131
+ again.** `ui/tests/integration/ai-regressions-v3.test.ts` was selected by no
132
+ package script, so neither `npm --prefix ui run check` nor CI executed it. It
133
+ now runs as `test:ai-regressions` inside the package's `npm test`, and a new
134
+ unit guard fails when any file under `ui/tests` is selected by no script
135
+ reachable from `check` or `test:e2e`.
136
+
137
+ - **Importing `@kerfjs/ui/catalog` or `@kerfjs/ui/workbench` now ships every
138
+ stylesheet those composites render.** `catalog`, `workbench`, `nav-stack`,
139
+ `split-view`, `tab-scaffold`, and `collapsible-panel` gain a CSS-aware
140
+ `browser` export condition whose generated wrapper imports the foundation, the
141
+ CSS of each internally rendered component (Pane, Toolbar, ToolbarText, List,
142
+ ListItem, ListInsetText, Row, PopupMenu, …) in dependency order, and the
143
+ composite's own CSS. Apps no longer hand-import `pane.css`, and a Catalog
144
+ sidebar's styling no longer depends on whether the app happens to import
145
+ `ListInsetText` elsewhere. The list is derived from the source import graph
146
+ (now following parent-relative imports), the `import` condition stays
147
+ CSS-free, and the companion `.css` subpaths remain for pipelines without the
148
+ `browser` condition. `@kerfjs/ui/ai/skill.md` is 1.46.0.
149
+
150
+ - **The `@kerfjs/ui` AI guidance now states every maintainer design rule
151
+ explicitly.** An audit of the package's tickets folded the missing rules into
152
+ `@kerfjs/ui/ai/skill.md` (1.45.0), `llms.txt`, and the design docs: prefer a
153
+ Kerf UI component over a Web Awesome component over a raw tag and never render
154
+ a `Discouraged` Web Awesome element directly (only through the Kerf component
155
+ that wraps it); nothing wraps inside a `ToolbarControlGroup` and a
156
+ `ToolbarText` is one ellipsized line unless `wrap`/`maxLines` is set; a
157
+ `Select` or `PopupMenu` trigger drops its focus ring while its popup is open
158
+ and the ring follows the control's shape (never add ring CSS); dividers earn
159
+ their place (`dividerSides=""` over self-spacing content or between a pane's
160
+ header and its list, never padded around); a control sits near where its
161
+ effect appears; and text aligns to text and icons to icons — a border is
162
+ never the alignment anchor. The ToolbarControlGroup catalog entry no longer
163
+ points at a hand-written `wa-dropdown` trigger, and the bundled kerf app
164
+ configs (`ai/skill.md` 1.25.0, `ai/cursorrules` 1.23.0) name the List family
165
+ and carry the "components own their styles; configure, never override" rule.
166
+
167
+ - **Applications and component packages are held to the component-ownership
168
+ rules `@kerfjs/ui` enforces internally: components own their styles and are
169
+ configured, never overridden.** `kerf-ui-analyze` and `kerf-ui-doctor` add
170
+ four error diagnostics (review findings under `--adoption`):
171
+ - `KUI-L019` — a CSS rule whose subject is another package's cataloged
172
+ component: a public class (`.kui-toolbar`, `.acme-meter`), its
173
+ `[data-component]` root, or a Web Awesome tag the package themes
174
+ (`wa-button`). Your own element in a component's context
175
+ (`.kui-toolbar > .my-widget`), keying on its state inside `:has()`, and
176
+ setting its public tokens stay allowed.
177
+ - `KUI-L020` — reading or writing another component's private `--_*`
178
+ variable (`--_kui-list-gap`).
179
+ - `KUI-L021` — overriding a token a typed prop sets (`--kui-list-gap` is
180
+ `<List gap>`); the message quotes the prop's example.
181
+ - `KUI-L022` — a class placed on a component's root (`<Toolbar
182
+ className="header" />`) that the application then styles.
183
+
184
+ Each message names the component's configuration (typed props, public
185
+ tokens, variants) and ends by routing a need with no prop to a component-gap
186
+ report instead of an override. Ownership is per package, from the nearest
187
+ `package.json`, so a component package styling its own components — including
188
+ in a Kerf UI parent's context — passes, and third-party catalogs declared in
189
+ `.kerf-ui-profile.json` get the same protection. `eslint-plugin-kerfjs` adds
190
+ `ui-component-ownership` (in `recommended-ui` and `strict-ui`, `error`),
191
+ reporting `KUI-L020` / `KUI-L021` in style strings, style objects, and
192
+ `style.setProperty()`. The `create-kerf-component` scaffold now ships a
193
+ `counter.css` that styles only Counter (configured by a typed `size` prop and
194
+ a `--kerf-counter-gap` token, with context named after Counter and a
195
+ Toolbar-context rule in its own stylesheet), plus `npm run check:styles`
196
+ (`kerf-ui-analyze`, from a new `@kerfjs/ui` dev dependency) in
197
+ `prepublishOnly`.
198
+
199
+ - **Behavior change (`@kerfjs/ui`): `NavStack` and `TabScaffold` chrome
200
+ dividers follow the scroll.** The `NavStack` bottom toolbar's and the
201
+ `TabScaffold` bar's top borders are no longer permanent. With `wireScrollDividers`
202
+ installed, a `NavStack`'s top chrome draws a line along its bottom edge only
203
+ while the active view's content is scrolled beneath it, its bottom toolbar a
204
+ line along its top edge only while more content lies below, and a
205
+ `TabScaffold` bar a line along its top edge only while the active scene's
206
+ content continues below it — none when the content fits. The line follows
207
+ the active view across a push or pop and the active scene across a tab
208
+ switch, and keys on whichever element actually scrolls: the view or scene
209
+ itself, or, through a sole child with no chrome of its own on that edge, a
210
+ `Pane`'s content or a nested `NavStack`'s active view (a sole `Pane`'s own
211
+ header or footer draws that boundary instead). Each layout draws the line
212
+ from its own stylesheet (an overlaid pseudo-element on the stack's chrome; the
213
+ bottom toolbar and bar keep their 1px top border, transparent until shown),
214
+ so nothing moves; the wiring
215
+ writes only its `data-scroll-overflow` / `data-scroll-divider` state. Without
216
+ the wiring those edges draw no line.
217
+
218
+ - **Behavior change (`@kerfjs/ui`): toolbars no longer draw a divider by
219
+ default; dividers between pinned chrome and scrolling content follow the
220
+ scroll.** `Toolbar`'s `dividerSides` now defaults to `''` (was `'b'`), and
221
+ the `Workbench` (`mainToolbar`, `mainBottomToolbar`, panel toolbars, and the
222
+ `mainHeader` / `mainFooter` lists), `CollapsiblePanel`, and `Catalog`
223
+ toolbars no longer place dividers of their own. Instead a `Pane` draws one
224
+ line under its header and one over its footer, wherever that chrome ends:
225
+ the header's only while content is scrolled beneath it, the footer's only
226
+ while more content lies below, and neither when the content fits. The new
227
+ `@kerfjs/ui/wire-scroll-dividers` subpath's `wireScrollDividers(root, {
228
+ targets? })` reports that state — call it once at the app root and keep its
229
+ disposer (`wireCatalog` installs it for its own root). It also marks each
230
+ side of a `TabBar` strip with tabs scrolled out of view, and lets a
231
+ `Toolbar` or `List` named as chrome in `targets` draw the divider facing an
232
+ app-owned scroller. The wiring only writes the `data-scroll-overflow` /
233
+ `data-scroll-divider` state attributes; each component draws its own line,
234
+ and no state moves the chrome, the content, or a tab. `Pane` gains
235
+ `chromeDividers` (`scroll` default, `always`, `none`; also in `PaneConfig`,
236
+ so `mainPane` and a panel's `pane` forward it) and TabBar the
237
+ `--kui-tab-bar-divider-color` token. To keep a permanent line, set
238
+ `chromeDividers: "always"` on the pane or an explicit `dividerSides`.
239
+
240
+ - **`@kerfjs/ui`: `Pane`, `NavStack`, `SplitView`, and `TabScaffold` fill a
241
+ Workbench region from their own stylesheets.** As a Workbench main area's
242
+ or panel's only child, each sets its own `height: 100%` in that context,
243
+ which the Workbench marks on its regions (`data-workbench-main`, and the new
244
+ `data-workbench-panel-content` on each panel's content); `workbench.css` no
245
+ longer sizes another component's root (it still fills a nested Workbench).
246
+ Rendering is unchanged.
247
+
248
+ - **`@kerfjs/ui`: layouts name the covered-floating-controls context after
249
+ themselves, and no longer write `FloatingToolbar`'s inset token.** Workbench,
250
+ CollapsiblePanel, and ResizableRegion each provide their own private
251
+ `--_kui-<layout>-covered` context in place of the shared
252
+ `--_kui-floating-covered` flag; `FloatingToolbar` and every restore corner
253
+ read all three. A restore corner no longer sets
254
+ `--kui-floating-toolbar-inset: 0px`; `FloatingToolbar` zeroes its own inset
255
+ inside a restore corner (a toolbar's own `inset` still wins). Rendering is
256
+ unchanged. One edge case changes: a floating control inside an open overlay
257
+ of one layout type that is itself covered by another layout type's open
258
+ overlay (for example, a ResizableRegion overlay in a Workbench work area
259
+ under an open rail overlay) now stays hidden.
260
+
261
+ - **`@kerfjs/ui`: `ListHeader`, `NavStack`, `Select`, and `StateBanner` no
262
+ longer restyle a composed child through a class on its root.** Each child
263
+ now styles itself: `Text` sets a `ListHeader` label from `text.css`, `Badge`
264
+ spaces a `Select` count and a `StateBanner` badge from `badge.css`, and a
265
+ `NavStack` title uses `ToolbarText`'s new `fill` option. Rendering is
266
+ unchanged. `ToolbarText` gains `fill` (grow into the free space of its
267
+ toolbar zone and truncate there). The `kui-list-header__label`,
268
+ `kui-nav-stack__title`, `kui-select__count`, and `kui-state-banner__badge`
269
+ classes are removed; select a ListHeader label with
270
+ `.kui-list-header__title > .kui-text` and a NavStack title with
271
+ `.kui-toolbar-text[data-fill="true"]` if a test needs to find them.
272
+
273
+ - **`@kerfjs/ui` ships a markdown component reference for AI assistants.**
274
+ `@kerfjs/ui/ai/components/README.md` indexes every catalog entry in one line,
275
+ and `@kerfjs/ui/ai/components/<id>.md` gives each entry a short page: when to
276
+ use and avoid it, imports and CSS/registration side effects, a link to its
277
+ exact props, legal parents and zones (JSX prop and child count), app- and
278
+ wiring-owned state, geometry, accessibility obligations, its styling boundary
279
+ (configure through props; do not override internals), and its diagnostics.
280
+ `llms.txt` and the shipped skill now send assistants there instead of to the
281
+ 263 KB `component-catalog.json` and 221 KB `component-composition.json`,
282
+ which remain the canonical sources for tools. The pages are generated from
283
+ those two files and a package check fails when they drift.
284
+
285
+ - **Breaking (`@kerfjs/ui` and `create-kerf-component`): the composition
286
+ catalog files are renamed so they no longer read as a "v2" of the component
287
+ catalog.** The composition catalog is a generated composition layer beside
288
+ `component-catalog.json`, not a newer version of it. Update any import,
289
+ `$schema`, `$ref`, or `.kerf-ui-profile.json` catalog location that names an
290
+ old path:
291
+
292
+ | Old path | New path |
293
+ | -------------------------------------------------------------- | --------------------------------------------------------------- |
294
+ | `@kerfjs/ui/ai/component-catalog-v2.json` | `@kerfjs/ui/ai/component-composition.json` |
295
+ | `@kerfjs/ui/ai/component-catalog-v2.schema.json` | `@kerfjs/ui/ai/component-composition.schema.json` |
296
+ | `@kerfjs/ui/ai/component-catalog-v2.d.ts` | `@kerfjs/ui/ai/component-composition.d.ts` |
297
+ | `@kerfjs/ui/ai/component-catalog-v2-overrides.json` | `@kerfjs/ui/ai/component-composition-rules.json` |
298
+ | `@kerfjs/ui/ai/component-catalog-extension-v2.schema.json` | `@kerfjs/ui/ai/component-composition-extension.schema.json` |
299
+ | `@kerfjs/ui/ai/component-catalog-extension-v2.d.ts` | `@kerfjs/ui/ai/component-composition-extension.d.ts` |
300
+ | `@kerfjs/ui/docs/examples/component-catalog-extension-v2.json` | `@kerfjs/ui/docs/examples/component-composition-extension.json` |
301
+ | scaffolded package `component-catalog-v2.json` | scaffolded package `component-composition.json` |
302
+
303
+ The exported types follow: `ComponentCatalogV2` → `ComponentComposition`,
304
+ `CatalogCompositionEntryV2` → `CatalogCompositionEntry`,
305
+ `ConsumerComponentCatalogV2` → `ConsumerComponentComposition`, and
306
+ `ConsumerCatalogEntryV2` → `ConsumerCompositionEntry`. In the repository,
307
+ `npm run catalog:v2:sync` is now `npm run composition:sync`
308
+ (`catalog:sync` still runs both generators). The file contents and their
309
+ `schemaVersion` are unchanged.
310
+
311
+ - **Breaking (`@kerfjs/ui`, `create-kerf-component`, and `kerfjs setup`): the
312
+ composition catalog's own fields no longer read as a "v2" of the component
313
+ catalog.** In `component-composition.json` (and any consumer composition
314
+ catalog), `"schemaVersion": 2` is now `"schemaVersion": 1` — the composition
315
+ format's own first version, independent of `component-catalog.json`'s — and
316
+ `compatibility.v1Catalog` is now `compatibility.componentCatalog`. A
317
+ scaffolded package's `kerf.components.json` renames its `v1Catalog` field to
318
+ `componentCatalog` the same way. Regenerate scaffolded catalogs
319
+ (`npm run catalog:generate`), and change any `.kerf-ui-profile.json` `composition`
320
+ location from `"schemaVersion": 2` to `"schemaVersion": 1`; a catalog still
321
+ carrying the old fields fails validation with a message naming the rename.
322
+ The `ComponentComposition` type follows (`schemaVersion: 1`,
323
+ `compatibility.componentCatalog`).
324
+
325
+ - **`@kerfjs/ui` `NavStack` bars use the standard `Toolbar` padding.** The
326
+ navigation bar dropped its toolbar's block padding to stay 44px tall, so its
327
+ title and controls touched the bar's edges. It now keeps the 8px above and
328
+ below that every other toolbar has (60px plus any top safe-area inset).
329
+
330
+ - **`@kerfjs/ui` multiline list rows break long camelCase names at word
331
+ boundaries.** A string label in a `multiline` `ListItem` or `ListActionRow`
332
+ (such as a Catalog sidebar entry) gets `<wbr>` break opportunities at its
333
+ camelCase and acronym boundaries, so `QuickTicketComposer` wraps as
334
+ `QuickTicket` / `Composer` instead of `QuickTicketComp` / `oser`. Breaking
335
+ mid-word remains the fallback for a single word that is still too wide.
336
+ - **`wbr` is a typed JSX intrinsic element.** The runtime already rendered it
337
+ as a void element; `JSX.IntrinsicElements` now declares it.
338
+
9
339
  - **`@kerfjs/ui` `Badge` has a text-free dot, and `TabScaffold` tabs can show
10
340
  it.** `Badge` takes `size="dot"`: an 8px solid circle in the badge's tone,
11
341
  with no children, appearance, or shape. Because a dot has no text, its props
package/README.md CHANGED
@@ -12,14 +12,14 @@
12
12
 
13
13
  <p align="center">
14
14
  <a href="https://www.npmjs.com/package/kerfjs"><img src="https://img.shields.io/npm/v/kerfjs.svg" alt="npm version" /></a>
15
- <img src="https://img.shields.io/badge/min%2Bgzip-~12%20KB-brightgreen.svg" alt="~12 KB minified and gzipped" />
15
+ <img src="https://img.shields.io/badge/min%2Bgzip-~13%20KB-brightgreen.svg" alt="~13 KB minified and gzipped" />
16
16
  <img src="https://img.shields.io/npm/l/kerfjs.svg" alt="MIT license" />
17
17
  <img src="https://img.shields.io/badge/types-included-3178c6.svg" alt="TypeScript types included" />
18
18
  </p>
19
19
 
20
20
  ---
21
21
 
22
- > ~12 KB. No virtual DOM. No compiler. No magic.
22
+ > ~13 KB. No virtual DOM. No compiler. No magic.
23
23
  > Reactive UI that touches only the bytes that changed.
24
24
 
25
25
  ```ts
@@ -76,7 +76,7 @@ Write plain `.tsx` and build with your existing esbuild / Vite / tsup — no ext
76
76
 
77
77
  ## Why Kerf
78
78
 
79
- 1. **~12 KB, one dependency.** ~12 KB minified + gzipped including `@preact/signals-core` (~13 KB with `arraySignal`). No virtual DOM, no scheduler, no concurrent-mode machinery. On the official [krausest benchmark](https://krausest.github.io/js-framework-benchmark/current.html) kerf sits in the same cluster as Vue, Lit, and vanjs; Solid's compiler leads the update-path benchmarks, which kerf doesn't try to match by design — no compiler.
79
+ 1. **~13 KB, one dependency.** ~13 KB minified + gzipped including `@preact/signals-core` (~13 KB with `arraySignal`). No virtual DOM, no scheduler, no concurrent-mode machinery. On the official [krausest benchmark](https://krausest.github.io/js-framework-benchmark/current.html) kerf sits in the same cluster as Vue, Lit, and vanjs; Solid's compiler leads the update-path benchmarks, which kerf doesn't try to match by design — no compiler.
80
80
 
81
81
  2. **No virtual DOM, no compiler.** JSX → HTML strings → native diff. DevTools shows the real DOM because it _is_ the DOM.
82
82
 
package/ai/cursorrules CHANGED
@@ -1,4 +1,4 @@
1
- <!-- kerf-skill-version: 1.22.0 -->
1
+ <!-- kerf-skill-version: 1.23.0 -->
2
2
 
3
3
  # kerf.cursorrules — rules for building apps with kerf
4
4
 
@@ -10,7 +10,7 @@
10
10
 
11
11
  # These rules condense `docs/ai/usage-guide.md` into the form Cursor parses.
12
12
 
13
- You are writing a UI in kerf — a ~12 KB reactive framework (~13 KB with `arraySignal`): signals + DOM morphing + JSX → HTML strings. No virtual DOM, no compiler, no scheduler.
13
+ You are writing a UI in kerf — a ~13 KB reactive framework (still ~13 KB with `arraySignal`): signals + DOM morphing + JSX → HTML strings. No virtual DOM, no compiler, no scheduler.
14
14
 
15
15
  ## Setup
16
16
 
@@ -33,13 +33,13 @@ the complete layer is intentional. Reuse these primitives before creating
33
33
  parallel local ones; keep domain state, commands, routing, and tab/menu policy
34
34
  in the app.
35
35
 
36
- Use MenuItem/MenuActionRow/MenuHeader `rootAttributes` only for application
37
- `data-*` metadata. MenuActionRow `trailingActionAttributes` and MenuHeader
36
+ Use ListItem/ListActionRow/ListHeader `rootAttributes` only for application
37
+ `data-*` metadata. ListActionRow `trailingActionAttributes` and ListHeader
38
38
  `triggerAttributes` additionally support native popover target/action and
39
39
  `aria-controls`/`aria-haspopup`. Do not use these slots to override action,
40
40
  item identity, selection, disclosure, naming, disabled, icon, or role
41
41
  semantics; one `role="menuitem"` does not make a complete menu widget.
42
- `MenuItem.trailing` is dormant. Use `MenuActionRow` for sibling primary and
42
+ `ListItem.trailing` is dormant. Use `ListActionRow` for sibling primary and
43
43
  trailing native buttons, and keep its `label`, `icon`, and
44
44
  `trailingActionIcon` SafeHtml slots free of controls.
45
45
 
@@ -48,13 +48,22 @@ Wire emitted `data-action` hooks at the mount root and retain disposers.
48
48
  disposers. Compose `AppTab` inside controlled `TabBar`; `wireTabBars()` supplies
49
49
  horizontal edge autoscroll while dragging. Apply reorder reports
50
50
  with `reorderTabs()`, and keep order, selection, close policy, routing, panels,
51
- and persistence in the host. Icons/spinners are decorative unless
51
+ and persistence in the host. Toolbars draw no divider by default; call
52
+ `wireScrollDividers()` once at the app root so panes and tab bars show scroll
53
+ dividers only while content is scrolled beneath or beyond them. Icons/spinners are decorative unless
52
54
  labeled, and assertive banners are only for urgent interruption.
53
55
 
54
- The opinionated semantic ramps match Hot Sheet 2 and Web Awesome. Override
55
- `--kui-color-*` globally or component properties such as
56
- `--kui-state-banner-background` at a tone or instance boundary; do not replace
57
- private descendant selectors.
56
+ Components own their styles; configure, never override. Set how a component
57
+ looks and sits through its typed props (size, density, inset, presentation,
58
+ `dividerSides`, `ToolbarConfig`, `PaneConfig`, …); never write application
59
+ CSS that selects a `.kui-*` class, a `[data-component]` root, a `wa-*` element
60
+ a Kerf component renders, or its `::part()`s, and never add padding, margin,
61
+ width, or height around a component to size or space it (it owns its 8px
62
+ inset — adding more double-insets). A missing prop is a component gap to
63
+ report upstream, not a reason to override. The opinionated semantic ramps
64
+ match Hot Sheet 2 and Web Awesome: override `--kui-color-*` globally or
65
+ component properties such as `--kui-state-banner-background` at a tone or
66
+ instance boundary, for theming only.
58
67
 
59
68
  For Web Awesome's broader free component set, import the CSS-only
60
69
  `@kerfjs/ui/webawesome.css` theme once, then import only each Web Awesome
package/ai/manifest.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
- "kerfjsVersion": "5.0.0-beta.56",
2
+ "kerfjsVersion": "5.0.0-beta.57",
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.24.0",
10
- "sha256": "f74616ec20120fa97ec2ff7e92971f5721ee8dcb0b5b7da2dd2604bb87002b69",
9
+ "version": "1.25.0",
10
+ "sha256": "4bc2b292bd707639c13b8cafe3fb24f779bfea04ab38fd6a5d40c443eed89c9d",
11
11
  "history": {
12
12
  "1.0.0": "bef982cb743fd4c73e666ee5cf774112e57c93ebb13b24bbacbf08577db0930a",
13
13
  "1.1.0": "f2aadb21e33dd389028951ae1ecbd9f8ba68648b3c5beba43fb4dfa2ab29f195",
@@ -39,7 +39,8 @@
39
39
  "1.22.0": "dc99b0ca5e911be11cbfa2ae515c0e161a414a5185895069535c41ba56664e70",
40
40
  "1.23.0": "28b82b4d31b98814c8fbbe1cb0bcf716192deb494c020b9af41a30830e6a0007",
41
41
  "1.23.1": "e21d7db1097ad95c11af9248ad17d514716bebc5646becf57d19a7598d6e382c",
42
- "1.24.0": "f74616ec20120fa97ec2ff7e92971f5721ee8dcb0b5b7da2dd2604bb87002b69"
42
+ "1.24.0": "f74616ec20120fa97ec2ff7e92971f5721ee8dcb0b5b7da2dd2604bb87002b69",
43
+ "1.25.0": "4bc2b292bd707639c13b8cafe3fb24f779bfea04ab38fd6a5d40c443eed89c9d"
43
44
  }
44
45
  },
45
46
  {
@@ -47,8 +48,8 @@
47
48
  "source": "kerf.cursorrules",
48
49
  "bundle": "ai/cursorrules",
49
50
  "dest": ".cursorrules",
50
- "version": "1.22.0",
51
- "sha256": "3515c663e908b2d6dc9a2a9cf42f2a5bf9a5b3a10c79d43e30cc8427e2ce209d",
51
+ "version": "1.23.0",
52
+ "sha256": "908e6622f597108ca696c8ff6f1081b3d83996dc46771a3515b184884dab4b25",
52
53
  "history": {
53
54
  "1.0.0": "98f7faff2b7585ef9cc511add42ea5d9bf0cbae8307b066af090e9281136d2ab",
54
55
  "1.1.0": "a2ec8c221dc694c98b81009cd2d99f9159a45fda94585de2f54471b227099eda",
@@ -78,7 +79,8 @@
78
79
  "1.20.0": "5e7b8d8ddc41f60220961417c9ae01c193fae045378a10a3e9c96a0e3cf10a03",
79
80
  "1.21.0": "a1b663b44650b1a04f89c60b26b002322fc6789650291477e14e13329c3ce25f",
80
81
  "1.21.1": "9b3eb825a7bec05bb6f0138f77b45fadf896b83020178c34c5a2f531c38f4dee",
81
- "1.22.0": "3515c663e908b2d6dc9a2a9cf42f2a5bf9a5b3a10c79d43e30cc8427e2ce209d"
82
+ "1.22.0": "3515c663e908b2d6dc9a2a9cf42f2a5bf9a5b3a10c79d43e30cc8427e2ce209d",
83
+ "1.23.0": "908e6622f597108ca696c8ff6f1081b3d83996dc46771a3515b184884dab4b25"
82
84
  }
83
85
  }
84
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.24.0
4
+ kerf-skill-version: 1.25.0
5
5
  ---
6
6
 
7
7
  # Building apps with kerf
@@ -10,7 +10,7 @@ kerf-skill-version: 1.24.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
 
@@ -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
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
1
  import { attr } from './chunk-MK42GLPV.js';
2
- import { delegate } from './chunk-X6OQ2HTR.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
17
  const value = el.getAttribute(attrName);
18
- if (Object.hasOwn(table, value)) table[value](event, el);
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;AAkBrB,SAAS,OAAyB,KAAA,EAAsC;AAC7E,EAAA,OAAO,IAAA,CAAK,qBAAqB,KAAK,CAAA;AACxC;AAwBO,SAAS,eAAA,CACd,IAAA,EACA,SAAA,EACA,KAAA,EACA,OAAA,EACY;AACZ,EAAA,MAAM,QAAA,GAAW,SAAS,IAAA,IAAQ,mBAAA;AAClC,EAAA,OAAO,QAAA;AAAA,IACL,IAAA;AAAA,IACA,SAAA;AAAA,IACA,IAAI,QAAQ,CAAA,CAAA,CAAA;AAAA,IACZ,CAAC,OAAO,EAAA,KAAO;AAEb,MAAA,MAAM,KAAA,GAAQ,EAAA,CAAG,YAAA,CAAa,QAAQ,CAAA;AAKtC,MAAA,IAAI,MAAA,CAAO,OAAO,KAAA,EAAO,KAAK,GAAG,KAAA,CAAM,KAAK,CAAA,CAAE,KAAA,EAAO,EAAE,CAAA;AAAA,IACzD,CAAA;AAAA,IACA;AAAA,GACF;AACF","file":"actions.js","sourcesContent":["/**\n * `kerfjs/actions` — the delegated action-table helper.\n *\n * The most-reinvented idiom across real kerf apps: one table of `data-action`\n * attribute specs used as the single source of truth for BOTH the JSX attribute\n * and the delegate selector, plus a hand-rolled `switch (dataset.action)`\n * dispatcher. This subpath blesses it as two thin helpers over the existing\n * `attr()` + `delegate()` — it does NOT replace them.\n *\n * import { action, delegateActions } from 'kerfjs/actions';\n *\n * const A = {\n * select: action('select-file'),\n * remove: action('remove-file'),\n * };\n *\n * // JSX — spread the attr (rename-safe; no hardcoded attribute name):\n * // <button {...A.select.attrs} data-id={id}>…</button>\n *\n * // Wire the whole table with ONE delegated listener; returns a disposer:\n * const dispose = delegateActions(root, 'click', {\n * [A.select.value]: (_e, el) => selectFile(el.getAttribute('data-id')),\n * [A.remove.value]: (_e, el) => removeFile(el.getAttribute('data-id')),\n * });\n *\n * Contract: `delegateActions` returns a `() => void` disposer and holds no\n * per-instance state — the same shape as `delegate()`, which it builds on (so\n * it inherits the single-listener dispatch and the capture auto-promotion for\n * well-known non-bubbling event types). One event type per call, mirroring\n * `delegate()`; collect the disposers for a root that needs several.\n */\nimport { attr, type AttrSpec } from './attr.js';\nimport { delegate, type DelegateOptions } from './delegate.js';\n\n/** The attribute an action table keys on by default. */\nconst DEFAULT_ACTION_ATTR = 'data-action';\n\n/**\n * A handler in a {@link delegateActions} table. Receives the DOM event and the\n * matched element (walk-up `closest()` match by default) — the same shape as a\n * `delegate()` handler.\n */\nexport type ActionHandler<E extends Element = Element> = (\n event: Event,\n el: E,\n) => void;\n\n/**\n * `action(value)` — an {@link AttrSpec} on `data-action`. A thin specialization\n * of `attr('data-action', value)`: spread its `.attrs` in JSX and use its\n * `.value` as the handler-table key, so the action name lives in exactly one\n * place and can't drift between the markup and the dispatcher.\n */\nexport function action<V extends string>(value: V): AttrSpec<'data-action', V> {\n return attr(DEFAULT_ACTION_ATTR, value);\n}\n\n/** Options for {@link delegateActions}. Extends {@link DelegateOptions}. */\nexport interface DelegateActionsOptions extends DelegateOptions {\n /**\n * The attribute the table keys on. Default `'data-action'`. Override it only\n * if you also author the specs with `attr(yourName, …)` instead of `action()`.\n */\n attr?: string;\n}\n\n/**\n * Wire a whole table of action handlers with ONE delegated listener.\n *\n * On `eventType`, the nearest element carrying the action attribute (walk-up\n * `closest()` by default; pass `{ match: 'direct' }` for an exact-element match)\n * is looked up in `table` by its attribute value, and the matching handler\n * runs. An element whose action is absent from the table is ignored — the same\n * behavior as a `switch (dataset.action)` with no matching `case`.\n *\n * Returns a `() => void` disposer. One event type per call (the smallest\n * surface, mirroring `delegate()`); collect the disposers when a root needs\n * several event types.\n */\nexport function delegateActions<E extends Element = Element>(\n root: HTMLElement,\n eventType: string,\n table: Readonly<Record<string, ActionHandler<E>>>,\n options?: DelegateActionsOptions,\n): () => void {\n const attrName = options?.attr ?? DEFAULT_ACTION_ATTR;\n return delegate<E>(\n root,\n eventType,\n `[${attrName}]`,\n (event, el) => {\n // `el` matched `[${attrName}]`, so the attribute is always present.\n const value = el.getAttribute(attrName) as string;\n // Own entries only: the table is an ordinary object, so a bare\n // `table[value]` would resolve Object.prototype names (`__proto__`,\n // `constructor`, `toString`, `hasOwnProperty`, …) and dispatch or throw\n // on markup that names no action in the table.\n if (Object.hasOwn(table, value)) table[value](event, el);\n },\n options,\n );\n}\n"]}
1
+ {"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"]}
@@ -0,0 +1,86 @@
1
+ import { devHooks } from './chunk-3C6F6NM4.js';
2
+
3
+ // src/delegate.ts
4
+ var NON_BUBBLING = /* @__PURE__ */ new Set([
5
+ "focus",
6
+ "blur",
7
+ "scroll",
8
+ "load",
9
+ "error",
10
+ "mouseenter",
11
+ "mouseleave"
12
+ ]);
13
+ function assertValidSelector(selector, fn) {
14
+ try {
15
+ document.createElement("div").matches(selector);
16
+ } catch {
17
+ throw new Error(
18
+ `${fn}: invalid selector "${selector}". Pass a valid CSS selector (e.g. '[data-action="add"]', '.btn', 'input').`
19
+ );
20
+ }
21
+ }
22
+ var KEY = /* @__PURE__ */ Symbol();
23
+ function _delegate(rootEl, type, selector, bind, options, capture, fn) {
24
+ assertValidSelector(selector, fn);
25
+ devHooks.delegateInEffect?.(fn);
26
+ const direct = options?.match === "direct";
27
+ const resolve = (event) => {
28
+ const target = event.target;
29
+ if (event.type === type && target instanceof Element) {
30
+ const matched = direct ? target.matches(selector) ? target : null : target.closest(selector);
31
+ if (matched !== null) {
32
+ const run = bind(event, matched);
33
+ return () => {
34
+ if (rootEl.contains(matched)) run();
35
+ };
36
+ }
37
+ }
38
+ return void 0;
39
+ };
40
+ const listener = (event) => {
41
+ let snap = event[KEY];
42
+ if (!snap?.has(resolve)) {
43
+ snap = event[KEY] = /* @__PURE__ */ new Map();
44
+ for (const node of event.composedPath()) {
45
+ for (const other of node[KEY] ?? []) {
46
+ snap.set(other, other(event));
47
+ }
48
+ }
49
+ }
50
+ const run = snap.get(resolve);
51
+ snap.delete(resolve);
52
+ run?.();
53
+ };
54
+ const registry = (rootEl[KEY] ??= /* @__PURE__ */ new Set()).add(resolve);
55
+ rootEl.addEventListener(type, listener, capture);
56
+ return () => {
57
+ registry.delete(resolve);
58
+ rootEl.removeEventListener(type, listener, capture);
59
+ };
60
+ }
61
+ function delegate(rootEl, type, selector, handler, options) {
62
+ return _delegate(
63
+ rootEl,
64
+ type,
65
+ selector,
66
+ (event, matched) => () => handler(event, matched),
67
+ options,
68
+ NON_BUBBLING.has(type),
69
+ "delegate"
70
+ );
71
+ }
72
+ function delegateCapture(rootEl, type, selector, handler, options) {
73
+ return _delegate(
74
+ rootEl,
75
+ type,
76
+ selector,
77
+ (event, matched) => () => handler(event, matched),
78
+ options,
79
+ true,
80
+ "delegateCapture"
81
+ );
82
+ }
83
+
84
+ export { NON_BUBBLING, _delegate, delegate, delegateCapture };
85
+
86
+ //# sourceMappingURL=chunk-JN7EJM3B.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/delegate.ts"],"names":[],"mappings":";;;AA+CO,IAAM,YAAA,uBAAmB,GAAA,CAAY;AAAA,EAC1C,OAAA;AAAA,EACA,MAAA;AAAA,EACA,QAAA;AAAA,EACA,MAAA;AAAA,EACA,OAAA;AAAA,EACA,YAAA;AAAA,EACA;AACF,CAAC;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;AASA,IAAM,sBAAM,MAAA,EAAO;AAiDZ,SAAS,UACd,MAAA,EACA,IAAA,EACA,UACA,IAAA,EACA,OAAA,EACA,SACA,EAAA,EACY;AACZ,EAAA,mBAAA,CAAoB,UAAU,EAAE,CAAA;AAChC,EAAA,QAAA,CAAS,mBAAmB,EAAE,CAAA;AAC9B,EAAA,MAAM,MAAA,GAAS,SAAS,KAAA,KAAU,QAAA;AAClC,EAAA,MAAM,OAAA,GAAmB,CAAC,KAAA,KAAU;AAClC,IAAA,MAAM,SAAS,KAAA,CAAM,MAAA;AACrB,IAAA,IAAI,KAAA,CAAM,IAAA,KAAS,IAAA,IAAQ,MAAA,YAAkB,OAAA,EAAS;AACpD,MAAA,MAAM,OAAA,GAAU,MAAA,GACZ,MAAA,CAAO,OAAA,CAAQ,QAAQ,IACrB,MAAA,GACA,IAAA,GACF,MAAA,CAAO,OAAA,CAAQ,QAAQ,CAAA;AAC3B,MAAA,IAAI,YAAY,IAAA,EAAM;AACpB,QAAA,MAAM,GAAA,GAAM,IAAA,CAAK,KAAA,EAAO,OAAO,CAAA;AAI/B,QAAA,OAAO,MAAM;AACX,UAAA,IAAI,MAAA,CAAO,QAAA,CAAS,OAAO,CAAA,EAAG,GAAA,EAAI;AAAA,QACpC,CAAA;AAAA,MACF;AAAA,IACF;AACA,IAAA,OAAO,MAAA;AAAA,EACT,CAAA;AACA,EAAA,MAAM,QAAA,GAAW,CAAC,KAAA,KAAuB;AACvC,IAAA,IAAI,IAAA,GAAQ,MAAsB,GAAG,CAAA;AACrC,IAAA,IAAI,CAAC,IAAA,EAAM,GAAA,CAAI,OAAO,CAAA,EAAG;AACvB,MAAA,IAAA,GAAQ,KAAA,CAAsB,GAAG,CAAA,mBAAI,IAAI,GAAA,EAAI;AAC7C,MAAA,KAAA,MAAW,IAAA,IAAQ,KAAA,CAAM,YAAA,EAAa,EAAG;AACvC,QAAA,KAAA,MAAW,KAAA,IAAU,IAAA,CAAoB,GAAG,CAAA,IAAK,EAAC,EAAG;AACnD,UAAA,IAAA,CAAK,GAAA,CAAI,KAAA,EAAO,KAAA,CAAM,KAAK,CAAC,CAAA;AAAA,QAC9B;AAAA,MACF;AAAA,IACF;AACA,IAAA,MAAM,GAAA,GAAM,IAAA,CAAK,GAAA,CAAI,OAAO,CAAA;AAC5B,IAAA,IAAA,CAAK,OAAO,OAAO,CAAA;AACnB,IAAA,GAAA,IAAM;AAAA,EACR,CAAA;AACA,EAAA,MAAM,QAAA,GAAA,CAAa,OAAsB,GAAG,CAAA,yBAAU,GAAA,EAAI,EAAG,IAAI,OAAO,CAAA;AACxE,EAAA,MAAA,CAAO,gBAAA,CAAiB,IAAA,EAAM,QAAA,EAAU,OAAO,CAAA;AAC/C,EAAA,OAAO,MAAM;AACX,IAAA,QAAA,CAAS,OAAO,OAAO,CAAA;AACvB,IAAA,MAAA,CAAO,mBAAA,CAAoB,IAAA,EAAM,QAAA,EAAU,OAAO,CAAA;AAAA,EACpD,CAAA;AACF;AAuBO,SAAS,QAAA,CACd,MAAA,EACA,IAAA,EACA,QAAA,EACA,SACA,OAAA,EACY;AACZ,EAAA,OAAO,SAAA;AAAA,IACL,MAAA;AAAA,IACA,IAAA;AAAA,IACA,QAAA;AAAA,IACA,CAAC,KAAA,EAAO,OAAA,KAAY,MAAM,OAAA,CAAQ,OAAO,OAAY,CAAA;AAAA,IACrD,OAAA;AAAA,IACA,YAAA,CAAa,IAAI,IAAI,CAAA;AAAA,IACrB;AAAA,GACF;AACF;AAuBO,SAAS,eAAA,CACd,MAAA,EACA,IAAA,EACA,QAAA,EACA,SACA,OAAA,EACY;AACZ,EAAA,OAAO,SAAA;AAAA,IACL,MAAA;AAAA,IACA,IAAA;AAAA,IACA,QAAA;AAAA,IACA,CAAC,KAAA,EAAO,OAAA,KAAY,MAAM,OAAA,CAAQ,OAAO,OAAY,CAAA;AAAA,IACrD,OAAA;AAAA,IACA,IAAA;AAAA,IACA;AAAA,GACF;AACF","file":"chunk-JN7EJM3B.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 */\nexport const 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 * Per-root registry of the delegates installed on that root, and the per-event\n * dispatch snapshot. Both live ON the DOM objects, under one symbol key (a\n * root and an event are never the same object), rather than in module-level\n * maps — so they are collected with the root / event and add no module-level\n * mutable state (Design rule 5).\n */\nconst KEY = Symbol();\n\n/**\n * Resolves one delegate's selector against an event's target: the bound\n * handler call for a match, `undefined` otherwise.\n */\ntype Resolve = (event: Event) => (() => void) | undefined;\n\ninterface Registered {\n [KEY]?: Set<Resolve>;\n}\n\ninterface Snapshotted {\n [KEY]?: Map<Resolve, (() => void) | undefined>;\n}\n\n/**\n * Install one root-level delegate — the shared core of `delegate()`,\n * `delegateCapture()`, and `kerfjs/actions`' `delegateActions()`.\n *\n * The listener resolves the event's target to a matched element (walk-up\n * `closest()` or strict `matches()`, per `options.match`) that must lie inside\n * `rootEl`, then runs the call `bind(event, matched)` prepared. `bind` runs at\n * RESOLUTION time, so a caller can read matched-element state there (such as\n * `delegateActions`' action attribute) before any handler has mutated it.\n *\n * **Dispatch snapshot.** A handler that writes a signal makes `mount()` morph\n * synchronously, inside the same dispatch, and the morph can recycle the\n * clicked element in place into a control with a different selector identity\n * (same tag, same position, new `data-action`). Resolving later delegates\n * against that live DOM would fire one the user never clicked — typically one\n * that immediately undoes the first handler's state change. So the FIRST kerf\n * delegate listener to see an event resolves every kerf delegate on the\n * event's propagation path (fixed at dispatch start, per the DOM spec, and\n * listed by `composedPath()`) before any delegated handler runs, and stores\n * the results on the event; each listener then consumes its own entry. The\n * set of delegates an event reaches is fixed when it starts dispatching, like\n * per-element listeners. The matched element must also still be inside\n * `rootEl` when the listener runs: a handler that removed the target still\n * suppresses the later delegates, as before.\n *\n * A missing entry (a re-dispatch of the same event object, a delegate added\n * mid-dispatch, or a root hidden from the first listener by a closed shadow\n * boundary) takes a fresh snapshot. Accepted residuals: a same-path\n * re-dispatch after a stopped dispatch can reuse an unconsumed entry if all\n * earlier kerf listeners were disposed, and a closed shadow boundary can\n * hide delegates from the first listener's path view. See docs/5 §5.2.1.\n * Internal — exported for `kerfjs/actions`.\n */\nexport function _delegate(\n rootEl: HTMLElement,\n type: string,\n selector: string,\n bind: (event: Event, matched: Element) => () => void,\n options: DelegateOptions | undefined,\n capture: boolean,\n fn: 'delegate' | 'delegateCapture',\n): () => void {\n assertValidSelector(selector, fn);\n devHooks.delegateInEffect?.(fn);\n const direct = options?.match === 'direct';\n const resolve: Resolve = (event) => {\n const target = event.target;\n if (event.type === type && target instanceof Element) {\n const matched = direct\n ? target.matches(selector)\n ? target\n : null\n : target.closest(selector);\n if (matched !== null) {\n const run = bind(event, matched);\n // Checked when the handler would run, not at resolution: a match\n // outside `rootEl` never fires, and neither does one an earlier\n // handler removed from `rootEl`.\n return () => {\n if (rootEl.contains(matched)) run();\n };\n }\n }\n return undefined;\n };\n const listener = (event: Event): void => {\n let snap = (event as Snapshotted)[KEY];\n if (!snap?.has(resolve)) {\n snap = (event as Snapshotted)[KEY] = new Map();\n for (const node of event.composedPath()) {\n for (const other of (node as Registered)[KEY] ?? []) {\n snap.set(other, other(event));\n }\n }\n }\n const run = snap.get(resolve);\n snap.delete(resolve);\n run?.();\n };\n const registry = ((rootEl as Registered)[KEY] ??= new Set()).add(resolve);\n rootEl.addEventListener(type, listener, capture);\n return () => {\n registry.delete(resolve);\n rootEl.removeEventListener(type, listener, capture);\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 return _delegate(\n rootEl,\n type,\n selector,\n (event, matched) => () => handler(event, matched as T),\n options,\n NON_BUBBLING.has(type),\n 'delegate',\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 return _delegate(\n rootEl,\n type,\n selector,\n (event, matched) => () => handler(event, matched as T),\n options,\n true,\n 'delegateCapture',\n );\n}\n"]}
@@ -1,37 +1,3 @@
1
- /**
2
- * Tiny event-delegation helpers. Replace per-element `addEventListener` calls
3
- * (which don't survive morph re-renders for nodes the diff creates) with one
4
- * listener at the morph-root that dispatches via `closest()`.
5
- *
6
- * Three-tier listener model:
7
- *
8
- * - Tier 1 (bubbling events) — use `delegate()`.
9
- * click, input, change, submit, mousedown/up, keydown/up, pointerdown/up/move,
10
- * drag*, drop, contextmenu, wheel, copy/paste/cut, focusin/focusout.
11
- *
12
- * `delegate()` also auto-promotes the well-known non-bubbling event
13
- * types (`focus`, `blur`, `scroll`, `load`, `error`, `mouseenter`,
14
- * `mouseleave`) to the capture phase under the hood, so the call site
15
- * looks identical for "interactive thing happens on a descendant"
16
- * regardless of whether that event bubbles. Selector matching stays
17
- * `closest()`-style — the same as for bubbling events — so a wrapper
18
- * selector like `'.field-row'` still matches when the event fires on
19
- * a descendant `<input>`.
20
- *
21
- * - Tier 2 (explicit capture) — use `delegateCapture()`.
22
- * The escape hatch for cases the auto-promotion list doesn't cover
23
- * (custom non-bubbling events) or when you want capture-phase
24
- * interception. Selector matching is `closest()`-style by default —
25
- * the same walk-up as `delegate()`, and it passes the matched ancestor
26
- * (not the raw target) to the handler — so a click on any descendant of
27
- * the selected element climbs to it. Pass `{ match: 'direct' }` to opt
28
- * into strict `matches()`-style matching (fire only when the event lands
29
- * on the exact element the selector identifies).
30
- *
31
- * - Tier 3 (per-element instances / library-owned subtrees) — mark the
32
- * host element with `data-morph-skip` and manage the library's
33
- * lifecycle directly. No delegation helper applies.
34
- */
35
1
  /**
36
2
  * How the selector is matched against the event's target:
37
3
  *
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export { A as AttrSpec, a as attr } from './attr-Cmu2ZoGO.js';
2
- export { D as DelegateOptions, d as delegate, a as delegateCapture } from './delegate-CL9VTZFb.js';
2
+ export { D as DelegateOptions, d as delegate, a as delegateCapture } from './delegate--CrfgYtC.js';
3
3
  import { ArraySignal } from './array-signal.js';
4
4
  import { SafeHtml } from './jsx-runtime.js';
5
5
  export { Fragment, JSXChildren, isSafeHtml, raw } from './jsx-runtime.js';
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  export { each, morph, mount } from './chunk-TLL3KXT6.js';
2
2
  export { defineStore, resetAllStores } from './chunk-BI4HGICV.js';
3
3
  export { attr } from './chunk-MK42GLPV.js';
4
- export { delegate, delegateCapture } from './chunk-X6OQ2HTR.js';
4
+ export { delegate, delegateCapture } from './chunk-JN7EJM3B.js';
5
5
 
6
6
 
7
7
  export { Fragment, SafeHtml, isSafeHtml, raw } from './chunk-UGNPDLNI.js';
@@ -865,6 +865,7 @@ interface KerfBuiltinIntrinsicElements {
865
865
  p: KerfBaseAttrs;
866
866
  hr: KerfBaseAttrs;
867
867
  br: KerfBaseAttrs;
868
+ wbr: KerfBaseAttrs;
868
869
  pre: KerfBaseAttrs;
869
870
  blockquote: HTMLBlockquoteAttrs;
870
871
  q: HTMLQAttrs;
package/dist/overlay.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { attach } from './chunk-FM6N2B4N.js';
2
2
  import { mount } from './chunk-TLL3KXT6.js';
3
- import { delegate } from './chunk-X6OQ2HTR.js';
3
+ import { delegate } from './chunk-JN7EJM3B.js';
4
4
 
5
5
 
6
6
  import { jsx, escapeHtml } from './chunk-UGNPDLNI.js';
package/dist/router.js CHANGED
@@ -1,4 +1,4 @@
1
- import { delegate } from './chunk-X6OQ2HTR.js';
1
+ import { delegate } from './chunk-JN7EJM3B.js';
2
2
  import { jsx } from './chunk-UGNPDLNI.js';
3
3
  import { signal, computed } from './chunk-IHMDBBUJ.js';
4
4
 
package/dist/scope.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { D as DelegateOptions } from './delegate-CL9VTZFb.js';
1
+ import { D as DelegateOptions } from './delegate--CrfgYtC.js';
2
2
  import { M as MountResult } from './mount-Bo2qOx25.js';
3
3
  import './jsx-runtime.js';
4
4
  import '@preact/signals-core';
package/dist/scope.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { mount } from './chunk-TLL3KXT6.js';
2
- import { delegate } from './chunk-X6OQ2HTR.js';
2
+ import { delegate } from './chunk-JN7EJM3B.js';
3
3
 
4
4
 
5
5
 
package/llms.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  # kerf
2
2
 
3
- > A tiny (~12 KB minified + gzipped including its one runtime dependency `@preact/signals-core`; ~13 KB with `arraySignal`) reactive UI framework — fine-grained signals + DOM morphing + JSX. No virtual DOM, no compiler. Apply the smallest possible cut to update your DOM.
3
+ > A tiny (~13 KB minified + gzipped including its one runtime dependency `@preact/signals-core`; still ~13 KB with `arraySignal`) reactive UI framework — fine-grained signals + DOM morphing + JSX. No virtual DOM, no compiler. Apply the smallest possible cut to update your DOM.
4
4
 
5
5
  kerf renders JSX to a structured `SafeHtml` (string for static content; tagged "list"/"mixed" segments where `each(...)` was used) and reconciles it against the live tree with a custom segment-aware morph. Static surrounds go through a general-purpose tree-morph; list contents go through a keyed reconciler that operates directly on live children. Snapshot reconciliation scans O(rows) but limits rendering and DOM mutations to cache misses and structural changes; eligible `arraySignal` patch delivery runs in O(patches). Reactivity is provided by [@preact/signals-core](https://github.com/preactjs/signals). It pairs well with server-rendered HTML, embedded widgets, and any UI where preserving focus / selection across re-renders matters. Public API is one import: `signal`, `computed`, `effect`, `batch`, `defineStore`, `resetAllStores`, `mount`, `morph`, `each`, `attr`, `delegate`, `delegateCapture`, `toElement`, `renderDocument`, `SafeHtml`, `isSafeHtml`, `raw`, `Fragment`. (Two more subpaths: `kerfjs/testing` exposes `clearStoreRegistry` for unit-test isolation; `kerfjs/jsx-runtime` exposes the typed JSX building blocks for declaration-merging custom-element types.) An optional subpath at `kerfjs/array-signal` adds `arraySignal()` — a granular keyed-list signal whose patch events let `each()` reconcile in O(patches) instead of O(N). An optional subpath at `kerfjs/dev` installs the development diagnostics — kerf does NOT infer dev mode, so you import it behind your own build's dev flag (`if (import.meta.env.DEV) await import('kerfjs/dev');`); omitting it is production and sheds ~4.7 KB min+gzip. Another optional subpath at `kerfjs/html` adds the `html` tagged template — JSX-identical runtime semantics with no JSX transform, so CDN/importmap projects can author kerf UIs with literally no build step. A family of optional, tree-shakeable **companion-utility** subpaths cover patterns real apps hand-roll: `kerfjs/list` (`bindList` — a keyed list with per-row fine-grained mounts and fixed / declared / measured-height viewport virtualization, plus `observeRowHeights`), `kerfjs/router` (`createRouter` — the opt-in "postcard router": route matching + `navigate` + `<a>` link interception + a keyed outlet, core stays router-free), `kerfjs/overlay` (`overlay` / `confirm` / `prompt` / `form` / `choice` / `popover` / `tooltip` / `toast` + `positionAnchored` / `autoReposition`; `native: true` top-layer backing via `<dialog>` / the Popover API applies to the seven overlay/dialog/popover/tooltip surfaces), `kerfjs/scope` (`disposeScope` / `disposeSubtree` / `observeRemovals`), `kerfjs/async` (`resource` async-state with a stale-response guard + SWR cache), `kerfjs/timing` (`debounce` / `throttle` / `debouncedSignal`), `kerfjs/remount` (`remountOn` — key-driven wholesale subtree replacement), `kerfjs/attach` (`attach` — bind a non-kerf widget's lifecycle to one node), and `kerfjs/actions` (`action` / `delegateActions` — the delegated `data-action` table idiom).
6
6
 
@@ -19,8 +19,8 @@ kerf renders JSX to a structured `SafeHtml` (string for static content; tagged "
19
19
  - [Requirements summary](https://github.com/brianwestphal/kerf/blob/main/docs/ai/requirements-summary.md): synthesized view of the numbered design docs.
20
20
  - [`kerf.cursorrules`](https://github.com/brianwestphal/kerf/blob/main/kerf.cursorrules): drop-in [Cursor](https://cursor.sh) rules for projects that import `kerfjs`. Copy into your project as `.cursorrules` — or use the bundled mirror at `node_modules/kerfjs/ai/cursorrules` once you've `npm install`ed kerfjs.
21
21
  - [`kerf.claude-skill.md`](https://github.com/brianwestphal/kerf/blob/main/kerf.claude-skill.md): drop-in [Claude Code](https://claude.com/claude-code) skill. Copy into `~/.claude/skills/kerf-app/SKILL.md` (or your project's `.claude/skills/kerf-app/SKILL.md`) — or use the bundled mirror at `node_modules/kerfjs/ai/skill.md` once you've `npm install`ed kerfjs.
22
- - [`eslint-plugin-kerfjs`](https://github.com/brianwestphal/kerf/blob/main/eslint-plugin/README.md): companion ESLint plugin enforcing the hard rules at edit time — eight AST-only core rules in `kerfjs.configs.recommended`: `no-inline-jsx-event-handlers`, `require-data-key-in-each`, `no-nested-mount`, `prefer-module-jsx-augmentation` (error) plus `require-delegate-disposer`, `prefer-attr-selector`, `no-raw-with-dynamic-arg`, `ai-assistant-configs` (warn), with no `parserServices` dependency; plus five `@kerfjs/ui` catalog-contract rules (`ui-composition`, `ui-css-values`, `ui-preferences`, `ui-public-boundaries`, `ui-wiring`) in the `recommended-ui` / `strict-ui` configs. Install with `npm install --save-dev eslint-plugin-kerfjs` and add `kerfjs.configs.recommended` to your eslint config. Recommended when authoring kerf code with an AI assistant — eslint feedback surfaces in the IDE before `tsc` or runtime warns ever run.
23
- - [`create-kerf-component`](https://github.com/brianwestphal/kerf/blob/main/create-kerf-component/README.md): companion initializer that scaffolds a publishable kerf component package with the hard packaging rules, an example component, and explicit `kerf.components.json` source metadata plus deterministic `component-catalog-v2.json` generation/checking. Run `npm create kerf-component@latest <dir>`.
22
+ - [`eslint-plugin-kerfjs`](https://github.com/brianwestphal/kerf/blob/main/eslint-plugin/README.md): companion ESLint plugin enforcing the hard rules at edit time — eight AST-only core rules in `kerfjs.configs.recommended`: `no-inline-jsx-event-handlers`, `require-data-key-in-each`, `no-nested-mount`, `prefer-module-jsx-augmentation` (error) plus `require-delegate-disposer`, `prefer-attr-selector`, `no-raw-with-dynamic-arg`, `ai-assistant-configs` (warn), with no `parserServices` dependency; plus six `@kerfjs/ui` catalog-contract rules (`ui-component-ownership`, `ui-composition`, `ui-css-values`, `ui-preferences`, `ui-public-boundaries`, `ui-wiring`) in the `recommended-ui` / `strict-ui` configs. Install with `npm install --save-dev eslint-plugin-kerfjs` and add `kerfjs.configs.recommended` to your eslint config. Recommended when authoring kerf code with an AI assistant — eslint feedback surfaces in the IDE before `tsc` or runtime warns ever run.
23
+ - [`create-kerf-component`](https://github.com/brianwestphal/kerf/blob/main/create-kerf-component/README.md): companion initializer that scaffolds a publishable kerf component package with the hard packaging rules, an example component, and explicit `kerf.components.json` source metadata plus deterministic `component-composition.json` generation/checking. Run `npm create kerf-component@latest <dir>`.
24
24
  - [`@kerfjs/ui` AI guide](https://github.com/brianwestphal/kerf/blob/main/ui/ai/skill.md): component-selection, styling, accessibility, and event-wiring contract for the optional first-party UI package.
25
25
 
26
26
  ## Reference docs
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kerfjs",
3
- "version": "5.0.0-beta.56",
3
+ "version": "5.0.0-beta.57",
4
4
  "description": "Tiny reactive UI framework — fine-grained signals + DOM morphing + JSX. Apply the smallest possible cut to update your DOM.",
5
5
  "type": "module",
6
6
  "sideEffects": [
package/setup/index.mjs CHANGED
@@ -491,7 +491,7 @@ function profileTemplate(packageName, catalogPath) {
491
491
  catalogs: [
492
492
  {
493
493
  package: packageName,
494
- composition: { path: catalogPath, schemaVersion: 2 },
494
+ composition: { path: catalogPath, schemaVersion: 1 },
495
495
  },
496
496
  ],
497
497
  };
@@ -502,7 +502,7 @@ function metadataTemplate() {
502
502
  $schema:
503
503
  './node_modules/create-kerf-component/component-metadata.schema.json',
504
504
  schemaVersion: 1,
505
- v1Catalog: 'not-applicable',
505
+ componentCatalog: 'not-applicable',
506
506
  components: [],
507
507
  };
508
508
  }
@@ -820,7 +820,7 @@ export async function planKerfSetup({
820
820
  key: 'kerfComponentCatalog',
821
821
  value: {
822
822
  source: './kerf.components.json',
823
- output: './component-catalog-v2.json',
823
+ output: './component-composition.json',
824
824
  },
825
825
  id: 'package.json#kerfComponentCatalog',
826
826
  ...context,
@@ -1011,7 +1011,7 @@ export async function planKerfSetup({
1011
1011
  : JSON.parse(currentProfileSource);
1012
1012
  const catalogRelative = portable(
1013
1013
  root,
1014
- resolve(selected.directory, 'component-catalog-v2.json'),
1014
+ resolve(selected.directory, 'component-composition.json'),
1015
1015
  );
1016
1016
  const profile = mergeProfile(
1017
1017
  currentProfile,
@@ -1,64 +0,0 @@
1
- import { devHooks } from './chunk-3C6F6NM4.js';
2
-
3
- // src/delegate.ts
4
- var NON_BUBBLING = /* @__PURE__ */ new Set([
5
- "focus",
6
- "blur",
7
- "scroll",
8
- "load",
9
- "error",
10
- "mouseenter",
11
- "mouseleave"
12
- ]);
13
- function assertValidSelector(selector, fn) {
14
- try {
15
- document.createElement("div").matches(selector);
16
- } catch {
17
- throw new Error(
18
- `${fn}: invalid selector "${selector}". Pass a valid CSS selector (e.g. '[data-action="add"]', '.btn', 'input').`
19
- );
20
- }
21
- }
22
- function makeListener(rootEl, selector, handler, match) {
23
- return (event) => {
24
- const target = event.target;
25
- if (!(target instanceof Element)) return;
26
- const matched = match === "direct" ? target.matches(selector) ? target : null : target.closest(selector);
27
- if (matched !== null && rootEl.contains(matched)) {
28
- handler(event, matched);
29
- }
30
- };
31
- }
32
- function delegate(rootEl, type, selector, handler, options) {
33
- assertValidSelector(selector, "delegate");
34
- devHooks.delegateInEffect?.("delegate");
35
- const listener = makeListener(
36
- rootEl,
37
- selector,
38
- handler,
39
- options?.match ?? "closest"
40
- );
41
- const capture = NON_BUBBLING.has(type);
42
- rootEl.addEventListener(type, listener, capture);
43
- return () => {
44
- rootEl.removeEventListener(type, listener, capture);
45
- };
46
- }
47
- function delegateCapture(rootEl, type, selector, handler, options) {
48
- assertValidSelector(selector, "delegateCapture");
49
- devHooks.delegateInEffect?.("delegateCapture");
50
- const listener = makeListener(
51
- rootEl,
52
- selector,
53
- handler,
54
- options?.match ?? "closest"
55
- );
56
- rootEl.addEventListener(type, listener, true);
57
- return () => {
58
- rootEl.removeEventListener(type, listener, true);
59
- };
60
- }
61
-
62
- export { delegate, delegateCapture };
63
-
64
- //# sourceMappingURL=chunk-X6OQ2HTR.js.map
@@ -1 +0,0 @@
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,GACJ,KAAA,KAAU,QAAA,GACN,MAAA,CAAO,OAAA,CAAQ,QAAQ,CAAA,GACrB,MAAA,GACA,IAAA,GACF,MAAA,CAAO,OAAA,CAAQ,QAAQ,CAAA;AAC7B,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,QAAA,GAAW,YAAA;AAAA,IACf,MAAA;AAAA,IACA,QAAA;AAAA,IACA,OAAA;AAAA,IACA,SAAS,KAAA,IAAS;AAAA,GACpB;AACA,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,QAAA,GAAW,YAAA;AAAA,IACf,MAAA;AAAA,IACA,QAAA;AAAA,IACA,OAAA;AAAA,IACA,SAAS,KAAA,IAAS;AAAA,GACpB;AACA,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-X6OQ2HTR.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 =\n match === 'direct'\n ? target.matches(selector)\n ? target\n : 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(\n rootEl,\n selector,\n handler,\n options?.match ?? 'closest',\n );\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(\n rootEl,\n selector,\n handler,\n options?.match ?? 'closest',\n );\n rootEl.addEventListener(type, listener, true);\n return () => {\n rootEl.removeEventListener(type, listener, true);\n };\n}\n"]}