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/CHANGELOG.md +659 -0
- package/README.md +3 -3
- package/ai/cursorrules +19 -10
- package/ai/manifest.json +9 -7
- package/ai/skill.md +16 -6
- package/dist/actions.d.ts +1 -1
- package/dist/actions.js +8 -4
- package/dist/actions.js.map +1 -1
- package/dist/chunk-JN7EJM3B.js +86 -0
- package/dist/chunk-JN7EJM3B.js.map +1 -0
- package/dist/{chunk-WMRFXSLG.js → chunk-TLL3KXT6.js} +5 -2
- package/dist/chunk-TLL3KXT6.js.map +1 -0
- package/dist/{delegate-CL9VTZFb.d.ts → delegate--CrfgYtC.d.ts} +0 -34
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -2
- package/dist/jsx-runtime.d.ts +1 -0
- package/dist/list.js +1 -1
- package/dist/overlay.js +2 -2
- package/dist/remount.js +1 -1
- package/dist/router.js +1 -1
- package/dist/scope.d.ts +1 -1
- package/dist/scope.js +2 -2
- package/llms.txt +3 -3
- package/package.json +1 -1
- package/setup/index.mjs +4 -4
- package/dist/chunk-WMRFXSLG.js.map +0 -1
- package/dist/chunk-X6OQ2HTR.js +0 -64
- package/dist/chunk-X6OQ2HTR.js.map +0 -1
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-~
|
|
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
|
-
> ~
|
|
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. **~
|
|
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.
|
|
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 ~
|
|
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
|
|
37
|
-
`data-*` metadata.
|
|
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
|
-
`
|
|
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.
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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.
|
|
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.
|
|
10
|
-
"sha256": "
|
|
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.
|
|
51
|
-
"sha256": "
|
|
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.
|
|
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 ~
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
package/dist/actions.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { attr } from './chunk-MK42GLPV.js';
|
|
2
|
-
import {
|
|
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
|
|
12
|
+
return _delegate(
|
|
13
13
|
root,
|
|
14
14
|
eventType,
|
|
15
15
|
`[${attrName}]`,
|
|
16
16
|
(event, el) => {
|
|
17
17
|
const value = el.getAttribute(attrName);
|
|
18
|
-
|
|
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
|
|
package/dist/actions.js.map
CHANGED
|
@@ -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;
|
|
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"]}
|
|
@@ -294,7 +294,10 @@ function restoreFocus(snap) {
|
|
|
294
294
|
|
|
295
295
|
// src/utils/isUserAgentOwnedAttr.ts
|
|
296
296
|
function isUserAgentOwnedAttr(tagNameUpper, name) {
|
|
297
|
-
|
|
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-
|
|
1517
|
+
//# sourceMappingURL=chunk-TLL3KXT6.js.map
|