kerfjs 5.0.0-beta.56 → 5.0.0-beta.58
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 +334 -0
- package/README.md +10 -10
- 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/{delegate-CL9VTZFb.d.ts → delegate--CrfgYtC.d.ts} +0 -34
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/jsx-runtime.d.ts +1 -0
- package/dist/overlay.js +1 -1
- package/dist/router.js +1 -1
- package/dist/scope.d.ts +1 -1
- package/dist/scope.js +1 -1
- package/llms.txt +3 -3
- package/package.json +1 -1
- package/setup/index.mjs +4 -4
- package/dist/chunk-X6OQ2HTR.js +0 -64
- package/dist/chunk-X6OQ2HTR.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,338 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [5.0.0] - 2026-09-29
|
|
10
|
+
|
|
11
|
+
- **`ui-public-boundaries` inspects `kerfjs/html` tagged templates.** The
|
|
12
|
+
no-build `html` template writes the same markup as JSX, so its static
|
|
13
|
+
`class` attributes now carry `KUI-L101` (private `kui-*` anatomy) and
|
|
14
|
+
`KUI-L103` (a component's anatomy on your own element):
|
|
15
|
+
``html`<aside class="kui-pane">` `` is reported with `Pane` to render, and a
|
|
16
|
+
plain ``html`<div class="kui-content-item">` `` with `ContentItem`, while a
|
|
17
|
+
`<ul>` carrier keeps the placeable class. The template's static parts are
|
|
18
|
+
read by the runtime's own rules; a class a hole supplies (`class=${cls}`)
|
|
19
|
+
is unknown and skipped.
|
|
20
|
+
|
|
21
|
+
- **`KUI-L103`'s "render X instead" answer has one implementation.**
|
|
22
|
+
`@kerfjs/ui` ships `ai/component-class-owners.cjs`, the rule that decides
|
|
23
|
+
which cataloged classes are a component's anatomy and which export renders
|
|
24
|
+
each. `eslint-plugin-kerfjs`'s `ui-public-boundaries` loads it beside
|
|
25
|
+
`application-ui-profile-sync.cjs`, and the generated `ai/components/*.md`
|
|
26
|
+
pages use it too, so the lint message and the component reference can no
|
|
27
|
+
longer name different components (a test pins their agreement over the whole
|
|
28
|
+
shipped catalog). A vendored `profileContractPath` directory must now also
|
|
29
|
+
hold `component-class-owners.cjs`; an `@kerfjs/ui` without it reports
|
|
30
|
+
`KUI-L090`.
|
|
31
|
+
|
|
32
|
+
- **A delegated click no longer re-fires on a target the re-render recycled.**
|
|
33
|
+
A delegated handler that writes a signal makes `mount()` re-render
|
|
34
|
+
synchronously, inside the same event dispatch; when the morph recycled the
|
|
35
|
+
clicked element in place into a control with a different `data-action`,
|
|
36
|
+
delegates registered later for the same event matched the recycled element
|
|
37
|
+
and fired too (a `request` click immediately ran `cancel` and undid itself).
|
|
38
|
+
`delegate()`, `delegateCapture()`, and `kerfjs/actions`' `delegateActions()`
|
|
39
|
+
now resolve every delegate's match against the DOM as it stood when the
|
|
40
|
+
event reached the first kerf delegate listener, like per-element listeners:
|
|
41
|
+
a delegate that matched at dispatch start still fires (unless an earlier
|
|
42
|
+
handler removed its element), one that matches only after a re-render does
|
|
43
|
+
not, and `delegateActions()` dispatches by the action value the element
|
|
44
|
+
carried at dispatch start. `mount()` stays synchronous. The
|
|
45
|
+
wrap-it-in-a-distinct-element workaround is no longer needed. Cost: about
|
|
46
|
+
0.11 KB min+gzip, which moves the advertised core size from ~12 KB to
|
|
47
|
+
~13 KB.
|
|
48
|
+
|
|
49
|
+
- **Component packages declare `placeableClasses`, and `KUI-L103` covers
|
|
50
|
+
their anatomy classes.** `create-kerf-component`'s `kerf.components.json`
|
|
51
|
+
accepts optional `boundaries.placeableClasses` (a subset of
|
|
52
|
+
`publicClasses`) and `boundaries.rootElement`, validates them, and copies
|
|
53
|
+
them into the generated `component-composition.json`.
|
|
54
|
+
`eslint-plugin-kerfjs`'s `ui-public-boundaries` now checks every class the
|
|
55
|
+
application or third-party catalogs in `.kerf-ui-profile.json` declare, not
|
|
56
|
+
only `kui-*` names: `<div class="acme-meter">` is reported with the
|
|
57
|
+
package's component to render (`Meter`), while its placeable classes stay
|
|
58
|
+
allowed. The scaffold template's README explains the field, and
|
|
59
|
+
`@kerfjs/ui/ai/skill.md` (1.50.0) applies the rule to package entries.
|
|
60
|
+
|
|
61
|
+
- **`eslint-plugin-kerfjs` `ui-public-boundaries` reports a plain
|
|
62
|
+
`<div class="kui-content-item">` as `ContentItem` recreated (`KUI-L103`).**
|
|
63
|
+
The `kui-content-item` classes stay placeable on another carrier (a `<ul>`, a
|
|
64
|
+
`Text`, a control-cluster `<footer>`), but a plain `<div>` carrying them is
|
|
65
|
+
exactly what `ContentItem` renders; the diagnostic now says to render
|
|
66
|
+
`ContentItem`. The `@kerfjs/ui` catalogs gain an additive `rootElement`
|
|
67
|
+
field (selection entry and composition `boundaries.rootElement`) naming the
|
|
68
|
+
element a component renders around its own placeable classes, and
|
|
69
|
+
`check:guidance` derives its repository check from the same fields, so the
|
|
70
|
+
package check and the downstream diagnostic cannot drift. The rule also
|
|
71
|
+
inspects the static class names of a template-literal `class` /
|
|
72
|
+
`className` value.
|
|
73
|
+
|
|
74
|
+
- **`eslint-plugin-kerfjs` `ui-public-boundaries` reports a component's
|
|
75
|
+
anatomy class written onto an application-owned element (`KUI-L103`).**
|
|
76
|
+
`<div class="kui-toolbar">`, `<aside class="kui-pane">`, or
|
|
77
|
+
`<wa-dropdown class="kui-popup-menu">` recreates a component by class and
|
|
78
|
+
inherits (and invites restyling of) its CSS; the diagnostic names the
|
|
79
|
+
component to render instead (`Toolbar`, `Pane`, `PopupMenu`). The
|
|
80
|
+
`@kerfjs/ui` catalogs gain `placeableClasses` (selection entry field and
|
|
81
|
+
composition `boundaries.placeableClasses`) naming the public classes an
|
|
82
|
+
application may place itself — the layout utilities, `kui-app-root`, the
|
|
83
|
+
`kui-content-item` geometry on a non-div carrier, and Web Awesome
|
|
84
|
+
`hide-actions` — and `check:catalog` requires every class-only entry to list
|
|
85
|
+
its classes there. A component's own element (`<Toolbar className="kui-toolbar">`)
|
|
86
|
+
is not reported. The README pane example and `@kerfjs/ui/ai/skill.md`
|
|
87
|
+
(1.47.0) now render `Pane` instead of writing its classes, and each generated
|
|
88
|
+
`ai/components/*.md` page states which classes are placeable and which
|
|
89
|
+
component renders the rest.
|
|
90
|
+
|
|
91
|
+
- **`kerf-ui-analyze` retires `KUI-L005` ("forced component dimension").**
|
|
92
|
+
`KUI-L019` already reports any application rule whose subject is a cataloged
|
|
93
|
+
component, dimensions included, so `.kui-pane { width: 300px }` was reported
|
|
94
|
+
twice (an error and a review finding). L005's only other shape, a public
|
|
95
|
+
class as an ancestor (`.kui-pane .app-sidebar { width: 240px }`), sizes the
|
|
96
|
+
application's own element and is allowed. The analyzer no longer emits it;
|
|
97
|
+
the id stays in the diagnostic registry (now listing it under `retired`) so
|
|
98
|
+
existing profile exceptions that name it still load.
|
|
99
|
+
|
|
100
|
+
- **`KUI-L301` now reports every `Discouraged` Web Awesome element rendered
|
|
101
|
+
directly.** The `@kerfjs/ui` package-default application profile avoids all
|
|
102
|
+
fifteen Discouraged entries (Dropdown/Dropdown Item → `PopupMenu`,
|
|
103
|
+
Select/Option → `Select`, Tab Group/Tab/Tab Panel → `TabBar`, Icon →
|
|
104
|
+
`LucideIcon`, Split Panel → `ResizableRegion`, Button Group →
|
|
105
|
+
`SegmentedControl`, Tree/Tree Item → `List`, and Animated Image, Comparison,
|
|
106
|
+
and Zoomable Frame → `ContentItem`), each with a rationale, and
|
|
107
|
+
`eslint-plugin-kerfjs` `ui-preferences` (and `kerf-ui-doctor`, which runs it)
|
|
108
|
+
now resolves a custom-element JSX tag such as `<wa-dropdown>` to its catalog
|
|
109
|
+
entry, not only an imported component. A declared component's own source
|
|
110
|
+
stays exempt because it is the wrapper that owns the element. A test keeps
|
|
111
|
+
the profile's avoid set equal to the catalog's Discouraged set in both
|
|
112
|
+
directions and lints the UX demo, recipes, and catalog shell clean;
|
|
113
|
+
`@kerfjs/ui/ai/skill.md` (1.46.0) names the diagnostic.
|
|
114
|
+
|
|
115
|
+
- **New (`@kerfjs/ui`): `NavStack` and `TabScaffold` take `chromeDividers`
|
|
116
|
+
(`'scroll' | 'always' | 'none'`), like `Pane`.** `scroll` (the default) keeps
|
|
117
|
+
today's behavior: the stack's top-chrome and bottom-toolbar lines and the
|
|
118
|
+
scaffold's bar line follow the scroll once `wireScrollDividers` is installed.
|
|
119
|
+
`always` draws them permanently without the wiring; `none` never draws them,
|
|
120
|
+
even when wired. The top `Toolbar`'s own `toolbarConfig.dividerSides` is
|
|
121
|
+
unaffected. `SplitView` forwards it to its compact stack through
|
|
122
|
+
`compactStack.chromeDividers`.
|
|
123
|
+
|
|
124
|
+
- **Fix (`@kerfjs/ui`): a `Pane` that is the only content of a `NavStack` view
|
|
125
|
+
or a `TabScaffold` scene fills it.** It used to size to its content, so the
|
|
126
|
+
view scrolled the whole Pane and its header scrolled away. Now the Pane's own
|
|
127
|
+
content scrolls under its pinned header and footer, the Pane's slots take the
|
|
128
|
+
safe-area edges the view or scene still reaches, and scroll dividers key on
|
|
129
|
+
the Pane's content. The rule lives in `pane.css`, as the Workbench-region fill
|
|
130
|
+
does.
|
|
131
|
+
|
|
132
|
+
- **The `@kerfjs/ui` suite-v3 AI-regression protocol test runs in every gate
|
|
133
|
+
again.** `ui/tests/integration/ai-regressions-v3.test.ts` was selected by no
|
|
134
|
+
package script, so neither `npm --prefix ui run check` nor CI executed it. It
|
|
135
|
+
now runs as `test:ai-regressions` inside the package's `npm test`, and a new
|
|
136
|
+
unit guard fails when any file under `ui/tests` is selected by no script
|
|
137
|
+
reachable from `check` or `test:e2e`.
|
|
138
|
+
|
|
139
|
+
- **Importing `@kerfjs/ui/catalog` or `@kerfjs/ui/workbench` now ships every
|
|
140
|
+
stylesheet those composites render.** `catalog`, `workbench`, `nav-stack`,
|
|
141
|
+
`split-view`, `tab-scaffold`, and `collapsible-panel` gain a CSS-aware
|
|
142
|
+
`browser` export condition whose generated wrapper imports the foundation, the
|
|
143
|
+
CSS of each internally rendered component (Pane, Toolbar, ToolbarText, List,
|
|
144
|
+
ListItem, ListInsetText, Row, PopupMenu, …) in dependency order, and the
|
|
145
|
+
composite's own CSS. Apps no longer hand-import `pane.css`, and a Catalog
|
|
146
|
+
sidebar's styling no longer depends on whether the app happens to import
|
|
147
|
+
`ListInsetText` elsewhere. The list is derived from the source import graph
|
|
148
|
+
(now following parent-relative imports), the `import` condition stays
|
|
149
|
+
CSS-free, and the companion `.css` subpaths remain for pipelines without the
|
|
150
|
+
`browser` condition. `@kerfjs/ui/ai/skill.md` is 1.46.0.
|
|
151
|
+
|
|
152
|
+
- **The `@kerfjs/ui` AI guidance now states every maintainer design rule
|
|
153
|
+
explicitly.** An audit of the package's tickets folded the missing rules into
|
|
154
|
+
`@kerfjs/ui/ai/skill.md` (1.45.0), `llms.txt`, and the design docs: prefer a
|
|
155
|
+
Kerf UI component over a Web Awesome component over a raw tag and never render
|
|
156
|
+
a `Discouraged` Web Awesome element directly (only through the Kerf component
|
|
157
|
+
that wraps it); nothing wraps inside a `ToolbarControlGroup` and a
|
|
158
|
+
`ToolbarText` is one ellipsized line unless `wrap`/`maxLines` is set; a
|
|
159
|
+
`Select` or `PopupMenu` trigger drops its focus ring while its popup is open
|
|
160
|
+
and the ring follows the control's shape (never add ring CSS); dividers earn
|
|
161
|
+
their place (`dividerSides=""` over self-spacing content or between a pane's
|
|
162
|
+
header and its list, never padded around); a control sits near where its
|
|
163
|
+
effect appears; and text aligns to text and icons to icons — a border is
|
|
164
|
+
never the alignment anchor. The ToolbarControlGroup catalog entry no longer
|
|
165
|
+
points at a hand-written `wa-dropdown` trigger, and the bundled kerf app
|
|
166
|
+
configs (`ai/skill.md` 1.25.0, `ai/cursorrules` 1.23.0) name the List family
|
|
167
|
+
and carry the "components own their styles; configure, never override" rule.
|
|
168
|
+
|
|
169
|
+
- **Applications and component packages are held to the component-ownership
|
|
170
|
+
rules `@kerfjs/ui` enforces internally: components own their styles and are
|
|
171
|
+
configured, never overridden.** `kerf-ui-analyze` and `kerf-ui-doctor` add
|
|
172
|
+
four error diagnostics (review findings under `--adoption`):
|
|
173
|
+
- `KUI-L019` — a CSS rule whose subject is another package's cataloged
|
|
174
|
+
component: a public class (`.kui-toolbar`, `.acme-meter`), its
|
|
175
|
+
`[data-component]` root, or a Web Awesome tag the package themes
|
|
176
|
+
(`wa-button`). Your own element in a component's context
|
|
177
|
+
(`.kui-toolbar > .my-widget`), keying on its state inside `:has()`, and
|
|
178
|
+
setting its public tokens stay allowed.
|
|
179
|
+
- `KUI-L020` — reading or writing another component's private `--_*`
|
|
180
|
+
variable (`--_kui-list-gap`).
|
|
181
|
+
- `KUI-L021` — overriding a token a typed prop sets (`--kui-list-gap` is
|
|
182
|
+
`<List gap>`); the message quotes the prop's example.
|
|
183
|
+
- `KUI-L022` — a class placed on a component's root (`<Toolbar
|
|
184
|
+
className="header" />`) that the application then styles.
|
|
185
|
+
|
|
186
|
+
Each message names the component's configuration (typed props, public
|
|
187
|
+
tokens, variants) and ends by routing a need with no prop to a component-gap
|
|
188
|
+
report instead of an override. Ownership is per package, from the nearest
|
|
189
|
+
`package.json`, so a component package styling its own components — including
|
|
190
|
+
in a Kerf UI parent's context — passes, and third-party catalogs declared in
|
|
191
|
+
`.kerf-ui-profile.json` get the same protection. `eslint-plugin-kerfjs` adds
|
|
192
|
+
`ui-component-ownership` (in `recommended-ui` and `strict-ui`, `error`),
|
|
193
|
+
reporting `KUI-L020` / `KUI-L021` in style strings, style objects, and
|
|
194
|
+
`style.setProperty()`. The `create-kerf-component` scaffold now ships a
|
|
195
|
+
`counter.css` that styles only Counter (configured by a typed `size` prop and
|
|
196
|
+
a `--kerf-counter-gap` token, with context named after Counter and a
|
|
197
|
+
Toolbar-context rule in its own stylesheet), plus `npm run check:styles`
|
|
198
|
+
(`kerf-ui-analyze`, from a new `@kerfjs/ui` dev dependency) in
|
|
199
|
+
`prepublishOnly`.
|
|
200
|
+
|
|
201
|
+
- **Behavior change (`@kerfjs/ui`): `NavStack` and `TabScaffold` chrome
|
|
202
|
+
dividers follow the scroll.** The `NavStack` bottom toolbar's and the
|
|
203
|
+
`TabScaffold` bar's top borders are no longer permanent. With `wireScrollDividers`
|
|
204
|
+
installed, a `NavStack`'s top chrome draws a line along its bottom edge only
|
|
205
|
+
while the active view's content is scrolled beneath it, its bottom toolbar a
|
|
206
|
+
line along its top edge only while more content lies below, and a
|
|
207
|
+
`TabScaffold` bar a line along its top edge only while the active scene's
|
|
208
|
+
content continues below it — none when the content fits. The line follows
|
|
209
|
+
the active view across a push or pop and the active scene across a tab
|
|
210
|
+
switch, and keys on whichever element actually scrolls: the view or scene
|
|
211
|
+
itself, or, through a sole child with no chrome of its own on that edge, a
|
|
212
|
+
`Pane`'s content or a nested `NavStack`'s active view (a sole `Pane`'s own
|
|
213
|
+
header or footer draws that boundary instead). Each layout draws the line
|
|
214
|
+
from its own stylesheet (an overlaid pseudo-element on the stack's chrome; the
|
|
215
|
+
bottom toolbar and bar keep their 1px top border, transparent until shown),
|
|
216
|
+
so nothing moves; the wiring
|
|
217
|
+
writes only its `data-scroll-overflow` / `data-scroll-divider` state. Without
|
|
218
|
+
the wiring those edges draw no line.
|
|
219
|
+
|
|
220
|
+
- **Behavior change (`@kerfjs/ui`): toolbars no longer draw a divider by
|
|
221
|
+
default; dividers between pinned chrome and scrolling content follow the
|
|
222
|
+
scroll.** `Toolbar`'s `dividerSides` now defaults to `''` (was `'b'`), and
|
|
223
|
+
the `Workbench` (`mainToolbar`, `mainBottomToolbar`, panel toolbars, and the
|
|
224
|
+
`mainHeader` / `mainFooter` lists), `CollapsiblePanel`, and `Catalog`
|
|
225
|
+
toolbars no longer place dividers of their own. Instead a `Pane` draws one
|
|
226
|
+
line under its header and one over its footer, wherever that chrome ends:
|
|
227
|
+
the header's only while content is scrolled beneath it, the footer's only
|
|
228
|
+
while more content lies below, and neither when the content fits. The new
|
|
229
|
+
`@kerfjs/ui/wire-scroll-dividers` subpath's `wireScrollDividers(root, {
|
|
230
|
+
targets? })` reports that state — call it once at the app root and keep its
|
|
231
|
+
disposer (`wireCatalog` installs it for its own root). It also marks each
|
|
232
|
+
side of a `TabBar` strip with tabs scrolled out of view, and lets a
|
|
233
|
+
`Toolbar` or `List` named as chrome in `targets` draw the divider facing an
|
|
234
|
+
app-owned scroller. The wiring only writes the `data-scroll-overflow` /
|
|
235
|
+
`data-scroll-divider` state attributes; each component draws its own line,
|
|
236
|
+
and no state moves the chrome, the content, or a tab. `Pane` gains
|
|
237
|
+
`chromeDividers` (`scroll` default, `always`, `none`; also in `PaneConfig`,
|
|
238
|
+
so `mainPane` and a panel's `pane` forward it) and TabBar the
|
|
239
|
+
`--kui-tab-bar-divider-color` token. To keep a permanent line, set
|
|
240
|
+
`chromeDividers: "always"` on the pane or an explicit `dividerSides`.
|
|
241
|
+
|
|
242
|
+
- **`@kerfjs/ui`: `Pane`, `NavStack`, `SplitView`, and `TabScaffold` fill a
|
|
243
|
+
Workbench region from their own stylesheets.** As a Workbench main area's
|
|
244
|
+
or panel's only child, each sets its own `height: 100%` in that context,
|
|
245
|
+
which the Workbench marks on its regions (`data-workbench-main`, and the new
|
|
246
|
+
`data-workbench-panel-content` on each panel's content); `workbench.css` no
|
|
247
|
+
longer sizes another component's root (it still fills a nested Workbench).
|
|
248
|
+
Rendering is unchanged.
|
|
249
|
+
|
|
250
|
+
- **`@kerfjs/ui`: layouts name the covered-floating-controls context after
|
|
251
|
+
themselves, and no longer write `FloatingToolbar`'s inset token.** Workbench,
|
|
252
|
+
CollapsiblePanel, and ResizableRegion each provide their own private
|
|
253
|
+
`--_kui-<layout>-covered` context in place of the shared
|
|
254
|
+
`--_kui-floating-covered` flag; `FloatingToolbar` and every restore corner
|
|
255
|
+
read all three. A restore corner no longer sets
|
|
256
|
+
`--kui-floating-toolbar-inset: 0px`; `FloatingToolbar` zeroes its own inset
|
|
257
|
+
inside a restore corner (a toolbar's own `inset` still wins). Rendering is
|
|
258
|
+
unchanged. One edge case changes: a floating control inside an open overlay
|
|
259
|
+
of one layout type that is itself covered by another layout type's open
|
|
260
|
+
overlay (for example, a ResizableRegion overlay in a Workbench work area
|
|
261
|
+
under an open rail overlay) now stays hidden.
|
|
262
|
+
|
|
263
|
+
- **`@kerfjs/ui`: `ListHeader`, `NavStack`, `Select`, and `StateBanner` no
|
|
264
|
+
longer restyle a composed child through a class on its root.** Each child
|
|
265
|
+
now styles itself: `Text` sets a `ListHeader` label from `text.css`, `Badge`
|
|
266
|
+
spaces a `Select` count and a `StateBanner` badge from `badge.css`, and a
|
|
267
|
+
`NavStack` title uses `ToolbarText`'s new `fill` option. Rendering is
|
|
268
|
+
unchanged. `ToolbarText` gains `fill` (grow into the free space of its
|
|
269
|
+
toolbar zone and truncate there). The `kui-list-header__label`,
|
|
270
|
+
`kui-nav-stack__title`, `kui-select__count`, and `kui-state-banner__badge`
|
|
271
|
+
classes are removed; select a ListHeader label with
|
|
272
|
+
`.kui-list-header__title > .kui-text` and a NavStack title with
|
|
273
|
+
`.kui-toolbar-text[data-fill="true"]` if a test needs to find them.
|
|
274
|
+
|
|
275
|
+
- **`@kerfjs/ui` ships a markdown component reference for AI assistants.**
|
|
276
|
+
`@kerfjs/ui/ai/components/README.md` indexes every catalog entry in one line,
|
|
277
|
+
and `@kerfjs/ui/ai/components/<id>.md` gives each entry a short page: when to
|
|
278
|
+
use and avoid it, imports and CSS/registration side effects, a link to its
|
|
279
|
+
exact props, legal parents and zones (JSX prop and child count), app- and
|
|
280
|
+
wiring-owned state, geometry, accessibility obligations, its styling boundary
|
|
281
|
+
(configure through props; do not override internals), and its diagnostics.
|
|
282
|
+
`llms.txt` and the shipped skill now send assistants there instead of to the
|
|
283
|
+
263 KB `component-catalog.json` and 221 KB `component-composition.json`,
|
|
284
|
+
which remain the canonical sources for tools. The pages are generated from
|
|
285
|
+
those two files and a package check fails when they drift.
|
|
286
|
+
|
|
287
|
+
- **Breaking (`@kerfjs/ui` and `create-kerf-component`): the composition
|
|
288
|
+
catalog files are renamed so they no longer read as a "v2" of the component
|
|
289
|
+
catalog.** The composition catalog is a generated composition layer beside
|
|
290
|
+
`component-catalog.json`, not a newer version of it. Update any import,
|
|
291
|
+
`$schema`, `$ref`, or `.kerf-ui-profile.json` catalog location that names an
|
|
292
|
+
old path:
|
|
293
|
+
|
|
294
|
+
| Old path | New path |
|
|
295
|
+
| -------------------------------------------------------------- | --------------------------------------------------------------- |
|
|
296
|
+
| `@kerfjs/ui/ai/component-catalog-v2.json` | `@kerfjs/ui/ai/component-composition.json` |
|
|
297
|
+
| `@kerfjs/ui/ai/component-catalog-v2.schema.json` | `@kerfjs/ui/ai/component-composition.schema.json` |
|
|
298
|
+
| `@kerfjs/ui/ai/component-catalog-v2.d.ts` | `@kerfjs/ui/ai/component-composition.d.ts` |
|
|
299
|
+
| `@kerfjs/ui/ai/component-catalog-v2-overrides.json` | `@kerfjs/ui/ai/component-composition-rules.json` |
|
|
300
|
+
| `@kerfjs/ui/ai/component-catalog-extension-v2.schema.json` | `@kerfjs/ui/ai/component-composition-extension.schema.json` |
|
|
301
|
+
| `@kerfjs/ui/ai/component-catalog-extension-v2.d.ts` | `@kerfjs/ui/ai/component-composition-extension.d.ts` |
|
|
302
|
+
| `@kerfjs/ui/docs/examples/component-catalog-extension-v2.json` | `@kerfjs/ui/docs/examples/component-composition-extension.json` |
|
|
303
|
+
| scaffolded package `component-catalog-v2.json` | scaffolded package `component-composition.json` |
|
|
304
|
+
|
|
305
|
+
The exported types follow: `ComponentCatalogV2` → `ComponentComposition`,
|
|
306
|
+
`CatalogCompositionEntryV2` → `CatalogCompositionEntry`,
|
|
307
|
+
`ConsumerComponentCatalogV2` → `ConsumerComponentComposition`, and
|
|
308
|
+
`ConsumerCatalogEntryV2` → `ConsumerCompositionEntry`. In the repository,
|
|
309
|
+
`npm run catalog:v2:sync` is now `npm run composition:sync`
|
|
310
|
+
(`catalog:sync` still runs both generators). The file contents and their
|
|
311
|
+
`schemaVersion` are unchanged.
|
|
312
|
+
|
|
313
|
+
- **Breaking (`@kerfjs/ui`, `create-kerf-component`, and `kerfjs setup`): the
|
|
314
|
+
composition catalog's own fields no longer read as a "v2" of the component
|
|
315
|
+
catalog.** In `component-composition.json` (and any consumer composition
|
|
316
|
+
catalog), `"schemaVersion": 2` is now `"schemaVersion": 1` — the composition
|
|
317
|
+
format's own first version, independent of `component-catalog.json`'s — and
|
|
318
|
+
`compatibility.v1Catalog` is now `compatibility.componentCatalog`. A
|
|
319
|
+
scaffolded package's `kerf.components.json` renames its `v1Catalog` field to
|
|
320
|
+
`componentCatalog` the same way. Regenerate scaffolded catalogs
|
|
321
|
+
(`npm run catalog:generate`), and change any `.kerf-ui-profile.json` `composition`
|
|
322
|
+
location from `"schemaVersion": 2` to `"schemaVersion": 1`; a catalog still
|
|
323
|
+
carrying the old fields fails validation with a message naming the rename.
|
|
324
|
+
The `ComponentComposition` type follows (`schemaVersion: 1`,
|
|
325
|
+
`compatibility.componentCatalog`).
|
|
326
|
+
|
|
327
|
+
- **`@kerfjs/ui` `NavStack` bars use the standard `Toolbar` padding.** The
|
|
328
|
+
navigation bar dropped its toolbar's block padding to stay 44px tall, so its
|
|
329
|
+
title and controls touched the bar's edges. It now keeps the 8px above and
|
|
330
|
+
below that every other toolbar has (60px plus any top safe-area inset).
|
|
331
|
+
|
|
332
|
+
- **`@kerfjs/ui` multiline list rows break long camelCase names at word
|
|
333
|
+
boundaries.** A string label in a `multiline` `ListItem` or `ListActionRow`
|
|
334
|
+
(such as a Catalog sidebar entry) gets `<wbr>` break opportunities at its
|
|
335
|
+
camelCase and acronym boundaries, so `QuickTicketComposer` wraps as
|
|
336
|
+
`QuickTicket` / `Composer` instead of `QuickTicketComp` / `oser`. Breaking
|
|
337
|
+
mid-word remains the fallback for a single word that is still too wide.
|
|
338
|
+
- **`wbr` is a typed JSX intrinsic element.** The runtime already rendered it
|
|
339
|
+
as a void element; `JSX.IntrinsicElements` now declares it.
|
|
340
|
+
|
|
9
341
|
- **`@kerfjs/ui` `Badge` has a text-free dot, and `TabScaffold` tabs can show
|
|
10
342
|
it.** `Badge` takes `size="dot"`: an 8px solid circle in the badge's tone,
|
|
11
343
|
with no children, appearance, or shape. Because a dot has no text, its props
|
|
@@ -2135,6 +2467,8 @@ font="monospace"` and Web Awesome code text match.
|
|
|
2135
2467
|
- Added the lockstep `@kerfjs/ui` package: accessible toolbar, menu, tab, header, resize, select, and feedback primitives; semantic CSS tokens and browser component subpaths that automatically include only reachable component styles; CSS-free root/SSR entries and an optional complete stylesheet; opt-in Web Awesome registration; AI-oriented docs; a production-backed UX catalog; bundle/unit/three-engine browser gates; and a least-privilege release workflow.
|
|
2136
2468
|
- The virtual-list demo animation opens on the full app again (header, the "N in the DOM" badge, and the list) before the scroll; the list now scrolls inside its own box while the header stays put (previously the whole frame was a full-bleed list strip), holds at row 0 before gliding instead of starting a few rows down, and the loop stays in phase on every pass. Captured with domotion-svg 0.28.2.
|
|
2137
2469
|
|
|
2470
|
+
- @kerfjs/ui panel toolbars now place groups in leading, center, and trailing zones; relocateOnCollapse keeps chosen groups available when a panel closes.
|
|
2471
|
+
|
|
2138
2472
|
## [4.4.1] - 2026-08-26
|
|
2139
2473
|
|
|
2140
2474
|
- Overhauled the README landing page: new one-line tagline, npm/size/license/TypeScript-types badges, a hoisted "Quick start" (install + `tsconfig`) right under the hook, and a jump nav (Quick start · Why kerf · Quick tour · Docs). The counter example now shows `delegate()` wiring the click handler.
|
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 in the browser.** ~13 KB minified + gzipped including `@preact/signals-core` (~13 KB with `arraySignal`). The optional setup CLI has its own npm dependencies; they stay out of the browser bundle. 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
|
|
|
@@ -88,7 +88,7 @@ Write plain `.tsx` and build with your existing esbuild / Vite / tsup — no ext
|
|
|
88
88
|
|
|
89
89
|
6. **Safe by default.** Text and attributes are HTML-escaped automatically, URL attributes are scheme-screened (`javascript:` dropped), and inline `on*` handlers are rejected outright — so untrusted data stays inert. `raw()` is the explicit, auditable opt-out.
|
|
90
90
|
|
|
91
|
-
**Plus, nothing you don't ask for:** JSX typed against the HTML standard (not React's props) · a
|
|
91
|
+
**Plus, nothing you don't ask for:** JSX typed against the HTML standard (not React's props) · a small public API with no hooks, lifecycle, or per-instance state · tree-shakeable companion subpaths (`router`, `list`, `overlay`, `async`, …) that stay out of the core until imported · an [ESLint plugin](https://brianwestphal.github.io/kerf/docs/eslint-plugin/) + opt-in dev warnings + `create-kerf-component` scaffold · plain TS/JSX/ESM that drops into esbuild / Vite / tsup — or **no** build at all via the `html` tagged template.
|
|
92
92
|
|
|
93
93
|
## When to use Kerf
|
|
94
94
|
|
|
@@ -223,8 +223,8 @@ Same algorithm `mount()` uses internally — `data-morph-skip`, `data-morph-skip
|
|
|
223
223
|
|
|
224
224
|
```html
|
|
225
225
|
<script type="module">
|
|
226
|
-
import { signal, mount, each } from "https://esm.sh/kerfjs@
|
|
227
|
-
import { html } from "https://esm.sh/kerfjs@
|
|
226
|
+
import { signal, mount, each } from "https://esm.sh/kerfjs@5";
|
|
227
|
+
import { html } from "https://esm.sh/kerfjs@5/html";
|
|
228
228
|
|
|
229
229
|
const items = signal([{ id: 1, label: "no build step" }]);
|
|
230
230
|
|
|
@@ -239,7 +239,7 @@ Same algorithm `mount()` uses internally — `data-morph-skip`, `data-morph-skip
|
|
|
239
239
|
</script>
|
|
240
240
|
```
|
|
241
241
|
|
|
242
|
-
Nothing is self-hosted — `kerfjs` is on npm, so every ESM CDN (esm.sh, jsDelivr, unpkg) mirrors it automatically. esm.sh works with a direct import as shown; jsDelivr / unpkg want an importmap so the internal `@preact/signals-core` import resolves. Pin to a major (`@
|
|
242
|
+
Nothing is self-hosted — `kerfjs` is on npm, so every ESM CDN (esm.sh, jsDelivr, unpkg) mirrors it automatically. esm.sh works with a direct import as shown; jsDelivr / unpkg want an importmap so the internal `@preact/signals-core` import resolves. Pin to a major (`@5`, as shown) rather than floating on `latest`, or an exact version (`@5.0.0`) for full reproducibility. Attribute names are written verbatim (`class`, not `className`), and holes are only legal in text positions or as a complete attribute value — anything ambiguous throws with an actionable message. See [`docs/6-jsx-runtime.md`](./docs/6-jsx-runtime.md) §6.11 (§6.11.1 for the full CDN / importmap recipes) — or the [live-poll example](https://brianwestphal.github.io/kerf/examples/complete/live-poll/), a complete app served exactly as authored: no bundler ever touches it.
|
|
243
243
|
|
|
244
244
|
### Batteries when you need them: the companion subpaths
|
|
245
245
|
|
|
@@ -322,7 +322,7 @@ Install and JSX setup are in [Quick start](#quick-start) above. These companion
|
|
|
322
322
|
|
|
323
323
|
### Optional: `eslint-plugin-kerfjs`
|
|
324
324
|
|
|
325
|
-
A companion ESLint plugin enforces kerf's hard rules at edit time.
|
|
325
|
+
A companion ESLint plugin enforces kerf's hard rules at edit time. Its eight core rules cover inline JSX event handlers, missing `data-key` in `each()`, nested `mount()`, delegate-disposer capture, `attr()` selector rename-safety, `raw()` XSS audit trails, JSX augmentation, and AI-assistant config hygiene. Six additional UI rules check component ownership, composition, CSS values, application preferences, wiring, and public boundaries when you opt into the UI preset. The plugin requires no TypeScript parser service.
|
|
326
326
|
|
|
327
327
|
```bash
|
|
328
328
|
npm install --save-dev eslint-plugin-kerfjs
|
|
@@ -334,7 +334,7 @@ import kerfjs from "eslint-plugin-kerfjs";
|
|
|
334
334
|
export default [kerfjs.configs.recommended];
|
|
335
335
|
```
|
|
336
336
|
|
|
337
|
-
Full docs at [brianwestphal.github.io/kerf/docs/eslint-plugin/](https://brianwestphal.github.io/kerf/docs/eslint-plugin/) —
|
|
337
|
+
Full docs at [brianwestphal.github.io/kerf/docs/eslint-plugin/](https://brianwestphal.github.io/kerf/docs/eslint-plugin/) — flat config, per-rule examples, and the rationale for which violations get lint rules vs. dev-warns vs. strict TS.
|
|
338
338
|
|
|
339
339
|
### Optional: `create-kerf-component`
|
|
340
340
|
|
|
@@ -353,7 +353,7 @@ See [`docs/13-component-packages.md`](./docs/13-component-packages.md) for the f
|
|
|
353
353
|
- **Migrating:** [coming from another framework?](https://brianwestphal.github.io/kerf/migrating/) — side-by-side TodoMVC translations + per-framework gotchas
|
|
354
354
|
- **AI guide:** [`docs/ai/usage-guide.md`](./docs/ai/usage-guide.md) — reference for AI tools fetching kerf docs (linked from `llms.txt`)
|
|
355
355
|
- **UI package:** [`@kerfjs/ui`](./ui/README.md) — accessible components, responsive app layouts, typed CSS values, Web Awesome adapters, and checked AI-facing component metadata
|
|
356
|
-
- **ESLint plugin:** [brianwestphal.github.io/kerf/docs/eslint-plugin/](https://brianwestphal.github.io/kerf/docs/eslint-plugin/) — `eslint-plugin-kerfjs`; eight rules
|
|
356
|
+
- **ESLint plugin:** [brianwestphal.github.io/kerf/docs/eslint-plugin/](https://brianwestphal.github.io/kerf/docs/eslint-plugin/) — `eslint-plugin-kerfjs`; eight core rules plus six opt-in UI rules at edit time (source: [`eslint-plugin/`](./eslint-plugin/))
|
|
357
357
|
- **Component scaffold:** `npm create kerf-component@latest <dir>` — `create-kerf-component`; generates a publishable component package with packaging rules plus deterministic, drift-checked AI metadata pre-wired (source: [`create-kerf-component/`](./create-kerf-component/))
|
|
358
358
|
- **Demo:** [live demo](https://brianwestphal.github.io/kerf/demo/) — nine sections exercising every primitive (counter, store-backed cart, focus survival, keyed list, morph-skip, SVG render, Tier-2 capture, `arraySignal` patches, fine-grained signal bindings)
|
|
359
359
|
- **Repo:** [github.com/brianwestphal/kerf](https://github.com/brianwestphal/kerf)
|
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.58",
|
|
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"]}
|
|
@@ -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
|
|
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-
|
|
4
|
+
export { delegate, delegateCapture } from './chunk-JN7EJM3B.js';
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
export { Fragment, SafeHtml, isSafeHtml, raw } from './chunk-UGNPDLNI.js';
|
package/dist/jsx-runtime.d.ts
CHANGED
package/dist/overlay.js
CHANGED
package/dist/router.js
CHANGED
package/dist/scope.d.ts
CHANGED
package/dist/scope.js
CHANGED
package/llms.txt
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# kerf
|
|
2
2
|
|
|
3
|
-
> A tiny (~
|
|
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
|
|
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-
|
|
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
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:
|
|
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
|
-
|
|
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-
|
|
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-
|
|
1014
|
+
resolve(selected.directory, 'component-composition.json'),
|
|
1015
1015
|
);
|
|
1016
1016
|
const profile = mergeProfile(
|
|
1017
1017
|
currentProfile,
|
package/dist/chunk-X6OQ2HTR.js
DELETED
|
@@ -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"]}
|