kerfjs 5.0.0-beta.55 → 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/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.55",
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":"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","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"]}
@@ -294,7 +294,10 @@ function restoreFocus(snap) {
294
294
 
295
295
  // src/utils/isUserAgentOwnedAttr.ts
296
296
  function isUserAgentOwnedAttr(tagNameUpper, name) {
297
- return name === "open" && (tagNameUpper === "DETAILS" || tagNameUpper === "DIALOG" || tagNameUpper.includes("-"));
297
+ if (tagNameUpper.includes("-")) {
298
+ return name === "open" || name === "role" || name.startsWith("aria-");
299
+ }
300
+ return name === "open" && (tagNameUpper === "DETAILS" || tagNameUpper === "DIALOG");
298
301
  }
299
302
 
300
303
  // src/utils/moveNode.ts
@@ -1511,4 +1514,4 @@ function resultToSegment(result) {
1511
1514
 
1512
1515
  export { captureFocus, each, morph, mount, moveNode, restoreFocus };
1513
1516
 
1514
- //# sourceMappingURL=chunk-WMRFXSLG.js.map
1517
+ //# sourceMappingURL=chunk-TLL3KXT6.js.map