kerfjs 5.0.0-beta.52 → 5.0.0-beta.53
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 +329 -0
- package/README.md +4 -3
- package/ai/cursorrules +6 -5
- package/ai/manifest.json +9 -7
- package/ai/skill.md +6 -5
- package/dist/actions.js +3 -3
- package/dist/actions.js.map +1 -1
- package/dist/array-signal.js +1 -1
- package/dist/async.js +1 -1
- package/dist/attach.js +1 -68
- package/dist/attach.js.map +1 -1
- package/dist/{chunk-CEQMZYLR.js → chunk-3C6F6NM4.js} +1 -1
- package/dist/{chunk-CEQMZYLR.js.map → chunk-3C6F6NM4.js.map} +1 -1
- package/dist/{chunk-ZOIERTUW.js → chunk-BI4HGICV.js} +3 -3
- package/dist/{chunk-ZOIERTUW.js.map → chunk-BI4HGICV.js.map} +1 -1
- package/dist/{chunk-3GSWKMBZ.js → chunk-DU35YGKW.js} +3 -3
- package/dist/{chunk-3GSWKMBZ.js.map → chunk-DU35YGKW.js.map} +1 -1
- package/dist/chunk-FM6N2B4N.js +70 -0
- package/dist/chunk-FM6N2B4N.js.map +1 -0
- package/dist/{chunk-ELXVRKY2.js → chunk-H7WTRM6F.js} +2 -2
- package/dist/{chunk-ELXVRKY2.js.map → chunk-H7WTRM6F.js.map} +1 -1
- package/dist/{chunk-H3TTM7PQ.js → chunk-HTXQR2KL.js} +108 -106
- package/dist/chunk-HTXQR2KL.js.map +1 -0
- package/dist/{chunk-Y2FOYPBV.js → chunk-IHMDBBUJ.js} +2 -2
- package/dist/chunk-IHMDBBUJ.js.map +1 -0
- package/dist/{chunk-E5R5GNKE.js → chunk-X6OQ2HTR.js} +2 -2
- package/dist/{chunk-E5R5GNKE.js.map → chunk-X6OQ2HTR.js.map} +1 -1
- package/dist/dev.d.ts +31 -13
- package/dist/dev.js +34 -18
- package/dist/dev.js.map +1 -1
- package/dist/html.js +2 -2
- package/dist/index.d.ts +12 -7
- package/dist/index.js +11 -11
- package/dist/index.js.map +1 -1
- package/dist/jsx-runtime.js +1 -1
- package/dist/list.d.ts +3 -2
- package/dist/list.js +27 -13
- package/dist/list.js.map +1 -1
- package/dist/overlay.d.ts +0 -22
- package/dist/overlay.js +107 -30
- package/dist/overlay.js.map +1 -1
- package/dist/remount.js +2 -2
- package/dist/router.d.ts +13 -4
- package/dist/router.js +8 -4
- package/dist/router.js.map +1 -1
- package/dist/scope.d.ts +10 -2
- package/dist/scope.js +22 -28
- package/dist/scope.js.map +1 -1
- package/dist/testing.js +1 -1
- package/dist/timing.d.ts +3 -2
- package/dist/timing.js +1 -1
- package/dist/timing.js.map +1 -1
- package/llms.txt +1 -1
- package/package.json +1 -1
- package/dist/chunk-H3TTM7PQ.js.map +0 -1
- package/dist/chunk-Y2FOYPBV.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,335 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
- `@kerfjs/ui` elevation shadows follow the color scheme, with or without the
|
|
10
|
+
optional `@kerfjs/ui/webawesome.css` theme. Kerf's theme layer set
|
|
11
|
+
`--wa-shadow-s` / `-m` / `-l` to fixed light-mode shadows (14%, 11%, and 9%
|
|
12
|
+
black), which all but vanished against the dark page, and the
|
|
13
|
+
`--kui-shadow-s` / `--kui-shadow-l` foundation fallbacks copied those fixed
|
|
14
|
+
values. The theme now pairs each light alpha with a stronger dark one (50%,
|
|
15
|
+
40%, 32%) through `light-dark()`, and the foundation fallbacks mirror them, so
|
|
16
|
+
selected segments and app tabs, compact overlay panels, and Web Awesome
|
|
17
|
+
surfaces that read `--wa-shadow-*` keep their elevation in dark mode. Light mode is unchanged. The browser parity test
|
|
18
|
+
now compares every `--kui-shadow-*` token with and without the theme in both
|
|
19
|
+
schemes.
|
|
20
|
+
- `@kerfjs/ui` text on loud tone fills clears WCAG AA in both color schemes,
|
|
21
|
+
with or without the optional `@kerfjs/ui/webawesome.css` theme. White
|
|
22
|
+
`on-loud` text reached about 2.2:1 on the success fill, 3.5:1 on the light
|
|
23
|
+
brand fill (`#0088ff`), and 3.6:1 on the danger fill (`#ff383c`) in both
|
|
24
|
+
schemes. Kerf's theme layer and the foundation fallbacks now pair each
|
|
25
|
+
fill with a foreground chosen to keep the tone's look: success keeps its
|
|
26
|
+
bright green with dark text (`#1d1d1f`, 7.6:1), as warning already did; light
|
|
27
|
+
brand and danger keep white text on a slightly deeper fill of the same hue
|
|
28
|
+
(`#0075db`, 4.6:1; `#eb0005`, 4.6:1); dark danger keeps its fill with dark
|
|
29
|
+
text (`#111113`, 5.3:1), as dark brand already did. Web Awesome accent
|
|
30
|
+
buttons, badges, and checked controls pick this up, and accent `wa-badge`s now
|
|
31
|
+
use their variant's `on-loud` foreground by default instead of a fixed dark
|
|
32
|
+
color (`--kui-wa-badge-accent-foreground` still overrides it). The browser
|
|
33
|
+
contrast test asserts every tone's `on-loud` over `fill-loud` pair, including
|
|
34
|
+
pop and neutral, in both schemes with and without the theme.
|
|
35
|
+
- `@kerfjs/ui` brand, success, and warning text clears WCAG AA on the lowered
|
|
36
|
+
page background in light mode, with or without the optional
|
|
37
|
+
`@kerfjs/ui/webawesome.css` theme. Their light `on-quiet` values (`#1e6ef4`,
|
|
38
|
+
`#008932`, `#a16a00`) passed on the white surface but reached only about
|
|
39
|
+
4.1:1 on `#f2f2f7`. Kerf's theme layer and the foundation fallbacks now use
|
|
40
|
+
the same hues a step darker: `#0c62f3` (4.64:1), `#007f2e` (4.62:1), and
|
|
41
|
+
`#966200` (4.65:1) on the page, about 5.2:1 on white. Links, selected tabs,
|
|
42
|
+
segmented and toolbar selections, and token-search accents that read
|
|
43
|
+
`brand-on-quiet` darken with it. Dark mode is unchanged. The browser contrast
|
|
44
|
+
test no longer skips light text over the page background.
|
|
45
|
+
- `@kerfjs/ui` surfaces, links, and focus rings match the optional
|
|
46
|
+
`@kerfjs/ui/webawesome.css` theme without it. Three foundation fallbacks
|
|
47
|
+
pointed at a different token than the theme: `--kui-color-surface-raised`
|
|
48
|
+
fell back to the surface color (the theme's dark raised surface is
|
|
49
|
+
`#2c2c2e`), `--kui-color-text-link` fell back to the brand fill (`#0088ff`,
|
|
50
|
+
about 3.5:1 on white) instead of the brand on-quiet text color, and the focus
|
|
51
|
+
ring used the brand fill instead of the theme's focus color (`#007aff`). Each
|
|
52
|
+
now falls back to the theme's value. The browser parity test now covers every
|
|
53
|
+
`--kui-color-*` token the foundation declares, plus the focus ring, instead of
|
|
54
|
+
only the tone tokens.
|
|
55
|
+
- The `@kerfjs/ui` UX catalog's `StateBanner` route adds a placeholder with a
|
|
56
|
+
badge for every tone, next to the existing badge-less placeholder, so visual
|
|
57
|
+
review and captures show the solid badge's loading skeleton on each tone's
|
|
58
|
+
fill. A browser test checks that each tone's badged placeholder is present.
|
|
59
|
+
- `@kerfjs/ui` toned text stays readable in dark mode without the optional
|
|
60
|
+
`@kerfjs/ui/webawesome.css` theme. The foundation's fallbacks for the
|
|
61
|
+
success, warning, and danger `on-quiet` and `on-normal` tokens were single
|
|
62
|
+
light-mode colors, so dark danger `Text` rendered dark red on a dark surface
|
|
63
|
+
(under 3:1). Every tone token now falls back to the same light and dark
|
|
64
|
+
pair the theme assigns, including `brand-on-loud` (dark text on the pale dark
|
|
65
|
+
brand fill instead of white) and `neutral-border-quiet`. Light-mode colors
|
|
66
|
+
are unchanged. A browser test bundles the package with and without the theme
|
|
67
|
+
and asserts that every tone token resolves identically in both color schemes
|
|
68
|
+
and that toned text clears WCAG AA in dark mode.
|
|
69
|
+
- `@kerfjs/ui` danger text and badges are readable in dark mode, and every
|
|
70
|
+
tone's `StateBanner` detail and quiet or outline `Badge` text now clears
|
|
71
|
+
WCAG AA. The danger tone was the only one without an on-fill foreground: its
|
|
72
|
+
solid badge fill and `StateBanner` text reused `danger-on-quiet`, a mid red
|
|
73
|
+
in dark mode (banner detail 4.0:1, badge placeholder skeleton 2.25:1). A new
|
|
74
|
+
`--kui-color-danger-on-fill` token (also accepted by `uiColor()`) matches the
|
|
75
|
+
other tones' pale dark values, lifting the dark danger banner text to 8.2:1.
|
|
76
|
+
The banner detail no longer dims to 78% opacity, which left the light info,
|
|
77
|
+
success, warning, and danger details at 3.4 to 4.3:1; it reads as secondary
|
|
78
|
+
through its smaller type, and the neutral detail uses the quiet text color.
|
|
79
|
+
Quiet and outline badges use their tone's on-fill text, lifting light brand,
|
|
80
|
+
success, and warning from about 4.1:1 to at least 5:1.
|
|
81
|
+
|
|
82
|
+
- A `@kerfjs/ui` placeholder skeleton inside a solid `Badge`, such as the
|
|
83
|
+
badge of a `StateBanner` placeholder, is now clearly visible. The generic
|
|
84
|
+
skeleton tint, 12% of the text color, almost disappeared on the badge's
|
|
85
|
+
saturated fill in every tone. A solid badge now tints its skeleton from its
|
|
86
|
+
own foreground color, which contrasts with the fill in every tone in light
|
|
87
|
+
and dark mode. Quiet and outline badges keep the generic tint, which already
|
|
88
|
+
reads on their pale fills. Geometry is unchanged.
|
|
89
|
+
- The `@kerfjs/ui` SVG design templates render badges as filled pills: the
|
|
90
|
+
`StateBanner` badge and the `ListHeader` and `List` count pills used to show
|
|
91
|
+
as plain text, because the template build loaded only a hand-written list of
|
|
92
|
+
stylesheets that omitted `badge.css`. The build now loads each component's
|
|
93
|
+
full stylesheet set from its generated browser entry, the same set a
|
|
94
|
+
consumer's browser import loads, so a nested component's CSS can no longer be
|
|
95
|
+
missed. The `ListHeader` and `List` templates also pick up the label inset
|
|
96
|
+
from `text.css`, so a header label now lines up with the list items below it,
|
|
97
|
+
as it does in the live component.
|
|
98
|
+
- A `@kerfjs/ui` `StateBanner` placeholder's badge sits at the live badge's
|
|
99
|
+
height on every platform. With a skeleton instead of text, the badge took a
|
|
100
|
+
baseline from the skeleton's bottom edge and sat about a pixel higher, by an
|
|
101
|
+
amount that depended on the platform's font metrics.
|
|
102
|
+
- A `@kerfjs/ui` `StateBanner` with a long, wrapping title keeps its badge
|
|
103
|
+
one item gap after the title's last word, and its detail uses the full copy
|
|
104
|
+
width. The wide flex layout shrank the title and detail together, which left
|
|
105
|
+
a large gap before the badge and truncated the detail early. The title,
|
|
106
|
+
badge, and detail now flow as inline text: the detail stays beside the title
|
|
107
|
+
while it fits whole, and otherwise moves to its own line, truncating only at
|
|
108
|
+
the full width. A wrapped title's lines stay evenly spaced.
|
|
109
|
+
- A `@kerfjs/ui` `ListHeader` disclosure toggle disabled with `actionDisabled`
|
|
110
|
+
is dimmed by the component itself, at the same tone as a disabled action
|
|
111
|
+
button, and shows the `not-allowed` cursor. It used to rely on Web Awesome's
|
|
112
|
+
native `button:disabled` dimming, so without `webawesome.css` it looked
|
|
113
|
+
enabled. Disabled toggles and action buttons also no longer show hover
|
|
114
|
+
feedback without `webawesome.css`.
|
|
115
|
+
- A `@kerfjs/ui` `ListActionRow` placeholder keeps its `busy` spinner. The
|
|
116
|
+
whole status line used to collapse into one skeleton, dropping the spinner
|
|
117
|
+
the component draws from a known prop. Now only the `status` text becomes a
|
|
118
|
+
skeleton, matching `ListItem`.
|
|
119
|
+
- **A tooltip or popover already showing stays visible when a modal opens.**
|
|
120
|
+
Lifting a surface above an open modal `<dialog>` was decided only when the
|
|
121
|
+
surface opened, so a tooltip or popover already on screen when a modal
|
|
122
|
+
dialog opened was left inert beneath it. Whenever `kerfjs/overlay` opens a
|
|
123
|
+
modal `<dialog>` (a `native: true` modal, or one it lifted), it now moves
|
|
124
|
+
every open tooltip and popover into the top layer above the new dialog. One
|
|
125
|
+
that is already a `[popover]` is re-shown so it stacks on top. The existing
|
|
126
|
+
`kerfjs/dev` warnings still report an engine without the Popover API, and a
|
|
127
|
+
surface with controls the modal keeps inert. A modal `<dialog>` the app
|
|
128
|
+
opens itself, outside kerf, is not observed.
|
|
129
|
+
- Moved `mount()`'s list-binding lifecycle (turning list markers into
|
|
130
|
+
bindings, first-render row-contract validation, orphan pruning) into its own
|
|
131
|
+
internal module, so `mount.ts` holds only the render orchestration. No
|
|
132
|
+
behavior or public API change.
|
|
133
|
+
- A narrow (576px and below) `@kerfjs/ui` `StateBanner` keeps its badge
|
|
134
|
+
sized to its content, one item gap after the title and on the title's
|
|
135
|
+
baseline, as in the wide layout. The narrow grid stretched the badge across
|
|
136
|
+
the free column into a wide pill that sat less than 1px from the title.
|
|
137
|
+
- Hovering a `@kerfjs/ui` `ListActionRow` placeholder with
|
|
138
|
+
`trailingActionVisibility="interaction"` no longer reveals its disabled
|
|
139
|
+
trailing button. A placeholder is inert, so it keeps the live row's resting
|
|
140
|
+
state, with the trailing action hidden until an interaction it cannot take.
|
|
141
|
+
- A `@kerfjs/ui` `ListItem` placeholder keeps its `busy` spinner. The whole
|
|
142
|
+
trailing slot used to collapse into one skeleton, dropping the spinner the
|
|
143
|
+
component draws from a known prop and shifting the label's width. Now only
|
|
144
|
+
the values in that slot become skeletons: `status` text in its status line
|
|
145
|
+
and author-supplied `trailing` content (always a value, since the component
|
|
146
|
+
cannot tell a chevron from a count) each get their own.
|
|
147
|
+
- `@kerfjs/ui` placeholders keep the live disabled tone of a control the
|
|
148
|
+
author disabled. A disabled `ListItem` or `ListActionRow`, a
|
|
149
|
+
`trailingActionDisabled` button, a disabled `SegmentedControl` choice, or a
|
|
150
|
+
`ListHeader` with `actionDisabled` rendered at full opacity as a
|
|
151
|
+
placeholder, because the placeholder reset could not tell its own
|
|
152
|
+
`disabled` attribute from the author's. Those controls now carry a
|
|
153
|
+
component-owned `data-kui-disabled="true"` marker whenever their disabled
|
|
154
|
+
prop is set; the disabled tone keys on it and the placeholder reset skips
|
|
155
|
+
it. The marker is reserved: the extension-attribute props cannot set it.
|
|
156
|
+
- `@kerfjs/ui` adds a browser test that renders every placeholder-capable
|
|
157
|
+
component live and as a placeholder from identical props and diffs each
|
|
158
|
+
element's geometry and computed chrome, so any future drift outside a value
|
|
159
|
+
slot fails. It caught one more: a `ListActionRow` placeholder's `status`
|
|
160
|
+
line was 3.5px shorter than the live line; the status line now always
|
|
161
|
+
keeps a full line height.
|
|
162
|
+
- `@kerfjs/ui`'s `Select` placeholder hint now matches the live hint: 14px
|
|
163
|
+
text at a 1.6 line height, 7px below the box. It was 12px / 1.4 with a 4px
|
|
164
|
+
gap, so a placeholder with a `hint` was 8.6px shorter than the loaded
|
|
165
|
+
control and the content below it jumped when loading finished.
|
|
166
|
+
- `@kerfjs/ui`'s `StateBanner` placeholder adds a detail skeleton only when
|
|
167
|
+
`detail` is set, matching the live banner. Previously every placeholder
|
|
168
|
+
showed a detail line, so a banner without one changed shape when loading
|
|
169
|
+
finished. Pass the banner's real `title` and `detail` with `placeholder`;
|
|
170
|
+
an empty `detail` now means no detail line.
|
|
171
|
+
- `@kerfjs/ui`'s `AppTab` placeholder pill is no longer dimmed to 50% with a
|
|
172
|
+
`not-allowed` cursor when `webawesome.css` is loaded; it keeps the live
|
|
173
|
+
tab's tone, and only its name is a skeleton.
|
|
174
|
+
- `@kerfjs/ui`'s `ListHeader` placeholder no longer dims its action button
|
|
175
|
+
(48%) or, in `toggle` mode, the whole title and label (Web Awesome's native
|
|
176
|
+
50%), and its inert action and toggle give no hover feedback. A live
|
|
177
|
+
`actionDisabled` header still dims its action.
|
|
178
|
+
- `@kerfjs/ui`'s `SegmentedControl` placeholder now keeps its choices. The
|
|
179
|
+
choices are known; only the selection is unknown, so a placeholder renders
|
|
180
|
+
every choice's live label or icon at its live size, selects none, and stays
|
|
181
|
+
inert. Previously every segment became a 4em text skeleton (widening icon
|
|
182
|
+
segments from 40px to 60px) and was dimmed to 45%.
|
|
183
|
+
- `@kerfjs/ui`'s `ListItem` and `ListActionRow` placeholders now match the
|
|
184
|
+
live row. They are no longer dimmed (the rows' own `:disabled` tone and
|
|
185
|
+
hover highlight skip placeholders), and a row with a `description` (or a
|
|
186
|
+
`ListActionRow` `status`) keeps that line as a second, smaller skeleton, so
|
|
187
|
+
the row keeps its live height instead of collapsing to one line. The
|
|
188
|
+
`ListActionRow` icon placeholder is an 18px block centered in its icon slot,
|
|
189
|
+
like `ListItem`'s.
|
|
190
|
+
- `@kerfjs/ui` placeholders no longer look disabled. A component's
|
|
191
|
+
`placeholder` keeps its controls `disabled` (inert and out of the keyboard
|
|
192
|
+
order), but `skeleton.css` now neutralizes generic disabled chrome under
|
|
193
|
+
`[data-placeholder="true"]`: full opacity and the default cursor instead of
|
|
194
|
+
Web Awesome's native `button:disabled` dimming to 50% with a `not-allowed`
|
|
195
|
+
cursor. A component's own opacity (a control hidden until interaction) still
|
|
196
|
+
applies.
|
|
197
|
+
- `toElement('<image href="…" />')` now returns an SVG `<image>`. The HTML
|
|
198
|
+
parser rewrites an orphan `<image>` to an HTML `<img>` before kerf sees the
|
|
199
|
+
tag, so it previously came back as an XHTML `img` that no `<svg>` would
|
|
200
|
+
render. kerf now recognizes a leading `<image` tag in the raw input
|
|
201
|
+
(case-insensitive, after leading whitespace) and parses it through the SVG
|
|
202
|
+
wrapper. A genuine `<img>` is unchanged.
|
|
203
|
+
- `delegateActions()` (`kerfjs/actions`) dispatches only the table's own
|
|
204
|
+
entries. It used to look the action up with a plain property read, so a
|
|
205
|
+
`data-action` naming an inherited `Object.prototype` member resolved:
|
|
206
|
+
`"__proto__"` or `"hasOwnProperty"` threw a `TypeError` from the listener,
|
|
207
|
+
and `"toString"` or `"constructor"` silently ran the inherited function.
|
|
208
|
+
Those values are now ignored like any other action absent from the table.
|
|
209
|
+
An own entry with such a name (`{ toString: … }`) still dispatches.
|
|
210
|
+
- `toElement()` now puts orphan camelCase SVG fragments in the SVG namespace.
|
|
211
|
+
`toElement('<linearGradient>…</linearGradient>')`, and likewise `clipPath`,
|
|
212
|
+
`radialGradient`, and `foreignObject`, previously returned an XHTML
|
|
213
|
+
`LINEARGRADIENT` element that no `<svg>` would render, because the lookup
|
|
214
|
+
compared the HTML parser's lowercased tag against camelCase names. The
|
|
215
|
+
fragment is now wrapped in `<svg>` and XML-parsed like `<path>` or `<g>`,
|
|
216
|
+
and the returned element keeps its camelCase `localName`.
|
|
217
|
+
- **A tooltip or popover no longer outlives its anchor.** When the anchor left
|
|
218
|
+
the document, most commonly because the modal containing it closed, the
|
|
219
|
+
`tooltip()` or `popover()` stayed on screen: a removed element fires no
|
|
220
|
+
`pointerleave` or `blur`. Both now watch their anchor with the same removal
|
|
221
|
+
tracking as `kerfjs/attach`. The tooltip hides, and the popover closes
|
|
222
|
+
without calling `onDismiss`, as soon as the anchor leaves the document.
|
|
223
|
+
Moving the anchor within the document keeps them open. A tooltip show still
|
|
224
|
+
pending when its anchor is removed never appears.
|
|
225
|
+
- **Overlays opened over a modal `<dialog>` are visible and usable.** A browser
|
|
226
|
+
makes everything outside an open modal dialog inert and paints a plain
|
|
227
|
+
element beneath it, so a `tooltip()`, `popover()`, or `confirm()` opened
|
|
228
|
+
from inside a `native: true` dialog without `native` itself was invisible and
|
|
229
|
+
could not be used. While a modal `<dialog>` is open, `kerfjs/overlay` now
|
|
230
|
+
hosts such a surface in the top layer anyway: a tooltip or popover becomes a
|
|
231
|
+
`[popover]`, and a nested modal becomes its own `<dialog>`. The browser's
|
|
232
|
+
default `[popover]` / `<dialog>` styles then apply, as they do with `native:
|
|
233
|
+
true`. Browsers keep a popover outside the modal dialog inert even in the top
|
|
234
|
+
layer, so a lifted popover with buttons is visible but not clickable. A new
|
|
235
|
+
always-on `kerfjs/dev` warning reports that case, and an engine without the
|
|
236
|
+
needed API.
|
|
237
|
+
- **A tooltip shown inside a modal no longer disables the modal.** `kerfjs/overlay`
|
|
238
|
+
used to treat the most recently opened overlay as the only one that could
|
|
239
|
+
react to input, so a `tooltip()` (which handles no input) inside an open
|
|
240
|
+
modal left Escape doing nothing and let Tab leave the dialog. Every overlay,
|
|
241
|
+
native or fallback, now joins one open-order stack, and each Escape, backdrop
|
|
242
|
+
click, outside click, or trapped Tab goes to the topmost surface that handles
|
|
243
|
+
it. A modal still blocks everything beneath it, and a surface that handles
|
|
244
|
+
nothing never intercepts. A popover that dismisses on Escape gets it before
|
|
245
|
+
the modal beneath. One click that is both outside a popover and on a modal's
|
|
246
|
+
backdrop now closes only the popover. A native `<dialog>` is arbitrated with
|
|
247
|
+
the fallback surfaces: one Escape closes one surface, and a fallback modal
|
|
248
|
+
beneath an open dialog no longer takes the dialog's Tab. Closing a lower
|
|
249
|
+
overlay first leaves focus in the overlay that holds it. An overlay opened
|
|
250
|
+
from inside it then restores focus to the closed overlay's opener, not a
|
|
251
|
+
detached node.
|
|
252
|
+
- `kerfjs` now exports the `EachOptions<T>` type (`{ cacheKey?, key? }`), the
|
|
253
|
+
options-object form of `each()`'s third argument, so a helper that builds or
|
|
254
|
+
forwards those options can import the shape instead of restating it:
|
|
255
|
+
`import { type EachOptions } from "kerfjs"`. Type-only; no runtime or bundle
|
|
256
|
+
change.
|
|
257
|
+
- Documentation: the companion-utility reference now matches the code. The
|
|
258
|
+
router's `match()` / `activeClass()` compare a literal path prefix, so
|
|
259
|
+
`match('/users/:id')` is never true. Link interception still applies to
|
|
260
|
+
`target="_self"`, the no-match `outlet()` is `null` with `params` `{}`,
|
|
261
|
+
components receive `(params, route)`, and hash mode keeps `route.hash` at
|
|
262
|
+
`''`. Invalid `bindList` configuration throws only `RangeError`. `popover()`
|
|
263
|
+
defaults `className` to `'kerf-popover'`, and the toast region is a reused
|
|
264
|
+
`.kerf-toasts` element when one exists. A `resource()` `cacheKey` applies
|
|
265
|
+
only to `run(input, fetcher)`, and throttle's `flush()` leaves the cooldown
|
|
266
|
+
running. `delegate()` / `delegateCapture()` throw on an invalid selector at
|
|
267
|
+
registration. The reference also now lists the `kerfjs/jsx-dev-runtime`
|
|
268
|
+
alias, the `DEV_HOOKS` export of `kerfjs/dev`, and `Signal#peek()`.
|
|
269
|
+
- Documentation: `morph(liveRoot, template)` is now described as it behaves.
|
|
270
|
+
It reconciles only the root's children and never touches the root's own
|
|
271
|
+
attributes, so `template` describes the content inside the root (an
|
|
272
|
+
`Element` template contributes its child nodes). The examples no longer
|
|
273
|
+
nest a duplicate `<article>` inside the live one. An `each()` list parent is
|
|
274
|
+
not a children-only short-circuit: the diff still walks it and skips only
|
|
275
|
+
the owned rows. The user-agent-owned `open` exception covers custom elements
|
|
276
|
+
as well as `<details>` / `<dialog>`, and a content-only row change inside an
|
|
277
|
+
`each()` list keeps its DOM node (and focus) unless the row's top-level tag
|
|
278
|
+
changes or the same render also inserts, removes, or moves rows.
|
|
279
|
+
- Every opt-in `kerfjs/dev` warning now ends with a silence hint that leads
|
|
280
|
+
with the browser-reachable switch, for example "Silence it with
|
|
281
|
+
enableWarnings({ narrowSet: false }) from kerfjs/dev, or set
|
|
282
|
+
KERF_DEV_WARN_NARROW_SET=0 (or unset it) under Node." Previously the
|
|
283
|
+
messages named only the environment variable, which a browser cannot reach.
|
|
284
|
+
The docs now teach `enableWarnings()` wherever they showed an env-only
|
|
285
|
+
switch, and name all three creation-time boundaries: `signal()`
|
|
286
|
+
(`untrackedSignals`), `effect()` (`delegateInEffect`), and `mount()`
|
|
287
|
+
(`rebuiltListeners`) decide once when created, so anything created before
|
|
288
|
+
the install or the opt-in stays uncovered.
|
|
289
|
+
- The `kerfjs/dev` rebuilt-listeners warning (`rebuiltListeners` /
|
|
290
|
+
`KERF_DEV_WARN_REBUILT_LISTENERS`) now dedups once per `mount()`, as
|
|
291
|
+
documented. Previously the first warning silenced every other mount for
|
|
292
|
+
the life of the process, so a second mount with the same bug stayed quiet.
|
|
293
|
+
- `kerfjs/router`: a second router on the same document no longer diverges
|
|
294
|
+
from the URL after a link click. The first router's interceptor navigates
|
|
295
|
+
with `pushState`, which fires no `popstate`, so the second router ignored
|
|
296
|
+
the already-handled click and kept the old route. An already-handled click
|
|
297
|
+
now re-syncs the route from the URL; when nothing navigated, it is a no-op.
|
|
298
|
+
One router per document remains the supported shape: a second router does
|
|
299
|
+
not follow another router's programmatic `navigate()` (no `popstate` fires
|
|
300
|
+
and routers do not broadcast to each other) until the next `popstate` or
|
|
301
|
+
link click. This is now documented and pinned by a test.
|
|
302
|
+
- `kerfjs/scope`: a disposed `Scope` handle no longer leaks registrations.
|
|
303
|
+
Previously `add` / `mount` / `effect` / `delegate` on a handle after its
|
|
304
|
+
`dispose()` (or from a disposer while `dispose()` ran) pushed into a list
|
|
305
|
+
nothing would ever run, leaving the effect subscribed or the listener
|
|
306
|
+
attached. Such a registration now tears down at once: `add(fn)` calls `fn`
|
|
307
|
+
immediately, and the wrappers create and immediately dispose.
|
|
308
|
+
- `bindList` measured-height virtualization no longer applies a stale
|
|
309
|
+
scroll-anchor correction. A `setHeight()` report for a row above the viewport
|
|
310
|
+
is now resolved when the animation frame runs: if the source removed the key,
|
|
311
|
+
moved it below the fold, or the user scrolled the row into view first,
|
|
312
|
+
`scrollTop` is left alone, and several reports for one key in a frame net
|
|
313
|
+
out. Fixed-height windows also clamp to the source length, so a list that
|
|
314
|
+
shrinks while scrolled deep no longer pads far past its content.
|
|
315
|
+
- `create-kerf-component` accepts an explicit `boundaries.rootClass: null` as
|
|
316
|
+
an author decision, as its schema always allowed. A wrapper whose root
|
|
317
|
+
element belongs to another catalog (a `rendersAs` wrapper) no longer has to
|
|
318
|
+
invent a class to satisfy the generator. An absent `rootClass` still fails
|
|
319
|
+
as undecided.
|
|
320
|
+
- `@kerfjs/ui`'s `Select` placeholder now matches the live control. The label
|
|
321
|
+
uses the field-label style (uppercase, extra-small, quiet, inset to line up
|
|
322
|
+
with the value), and the chevron is the same glyph at the same size.
|
|
323
|
+
Previously the placeholder drew a larger, regular-weight label at a
|
|
324
|
+
different inset and a different, smaller chevron. Only the value is a
|
|
325
|
+
skeleton; the chrome no longer changes when loading finishes.
|
|
326
|
+
- **Wrapper components can declare the cataloged root they render.** A
|
|
327
|
+
`kerf.components.json` component may set `composition.rendersAs`, for
|
|
328
|
+
example `["@kerfjs/ui:toolbar-control-group"]`: a list of the roots it
|
|
329
|
+
renders, any one of them, or nothing. `kerf-component-catalog` copies it
|
|
330
|
+
into the generated v2 entry and fails unless every key resolves to a
|
|
331
|
+
cataloged entry. `eslint-plugin-kerfjs`'s `ui-composition` rule, and so
|
|
332
|
+
`kerf-ui-doctor`, now loads the application and third-party catalogs a
|
|
333
|
+
`.kerf-ui-profile.json` declares. It resolves their components by package
|
|
334
|
+
subpath or source file and checks a wrapper as each declared root: a zone
|
|
335
|
+
must accept every root, each root's parent contract applies where the
|
|
336
|
+
wrapper is placed, and a wrapper counts as zero or one child. Undeclared
|
|
337
|
+
wrappers keep reporting `KUI-L202`.
|
|
9
338
|
- `@kerfjs/ui`'s `AppTab` adds `pending` for a known tab that is still
|
|
10
339
|
opening. It keeps the name visible in the quiet text color and uses it as
|
|
11
340
|
the tab's accessible name, so a trailing spinner's label is not folded in.
|
package/README.md
CHANGED
|
@@ -209,12 +209,13 @@ The class lives in its own subpath so apps that don't need it shed ~1 KB. Reads
|
|
|
209
209
|
```ts
|
|
210
210
|
import { morph, raw } from "kerfjs";
|
|
211
211
|
|
|
212
|
-
|
|
213
|
-
morph(liveCard,
|
|
212
|
+
// `template` describes liveCard's CHILDREN; liveCard's own attributes are untouched
|
|
213
|
+
morph(liveCard, freshlyBuiltCardEl); // Element template: its child nodes are used
|
|
214
|
+
morph(liveCard, '<h2 class="card-title">…</h2><p>…</p>'); // raw HTML string
|
|
214
215
|
morph(liveCard, raw(htmlFromServer)); // SafeHtml
|
|
215
216
|
```
|
|
216
217
|
|
|
217
|
-
Same algorithm `mount()` uses internally — `data-morph-skip`, `data-morph-skip-children`, `data-morph-preserve`, focused-input value + selection preservation, the
|
|
218
|
+
Same algorithm `mount()` uses internally — `data-morph-skip`, `data-morph-skip-children`, `data-morph-preserve`, focused-input value + selection preservation, the user-agent-owned `open` rule (`<details>`, `<dialog>`, custom elements) all carry over. Only the live root's children are reconciled — the root's own attributes are never touched. Use it for SSR-fragment hydration, page-refresh diffs, third-party widget remounts. See [`docs/4-render.md`](./docs/4-render.md) §4.4.3.
|
|
218
219
|
|
|
219
220
|
### No build step at all: the `html` tagged template
|
|
220
221
|
|
package/ai/cursorrules
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- kerf-skill-version: 1.21.
|
|
1
|
+
<!-- kerf-skill-version: 1.21.1 -->
|
|
2
2
|
|
|
3
3
|
# kerf.cursorrules — rules for building apps with kerf
|
|
4
4
|
|
|
@@ -17,7 +17,7 @@ You are writing a UI in kerf — a ~12 KB reactive framework (~13 KB with `array
|
|
|
17
17
|
- Install with `npm install kerfjs`.
|
|
18
18
|
- `tsconfig.json`: `"jsx": "react-jsx"`, `"jsxImportSource": "kerfjs"`.
|
|
19
19
|
- Vite / esbuild need no extra config.
|
|
20
|
-
- **Dev diagnostics are opt-in by import, and only an APP installs them.** kerf does not infer dev mode. In the app entry add `if (import.meta.env.DEV) await import('kerfjs/dev');` (Vite) or `if (process.env.NODE_ENV !== 'production') await import('kerfjs/dev');` (webpack/Node). That enables the read-only store `get()` snapshot, the throwing dangerous-URL screen, and makes the `KERF_DEV_WARN_*` family available; omitting it is production shape and sheds ~4.7 KB min+gzip because the condition folds away and the chunk is never emitted. Put it FIRST if relying on the untracked-signal
|
|
20
|
+
- **Dev diagnostics are opt-in by import, and only an APP installs them.** kerf does not infer dev mode. In the app entry add `if (import.meta.env.DEV) await import('kerfjs/dev');` (Vite) or `if (process.env.NODE_ENV !== 'production') await import('kerfjs/dev');` (webpack/Node). That enables the read-only store `get()` snapshot, the throwing dangerous-URL screen, and makes the `KERF_DEV_WARN_*` family available; omitting it is production shape and sheds ~4.7 KB min+gzip because the condition folds away and the chunk is never emitted. Put it (and any `enableWarnings()` call) FIRST if relying on the untracked-signal, delegate-in-effect, or rebuilt-listeners warnings — `signal()`, `effect()`, and `mount()` decide those at creation time, so anything created earlier stays uncovered.
|
|
21
21
|
- **Switch individual warnings on with `enableWarnings()`**, which is the only switch that works in a browser (no `process` object there, and a bundler `define` cannot reach the read): `const dev = await import('kerfjs/dev'); dev.enableWarnings({ staleBinding: true, narrowSet: true, invariants: 'throw' });`. The `KERF_DEV_WARN_*` env vars do the same for Node/SSR/CI; an explicit call wins either way.
|
|
22
22
|
- **A component package must NEVER import `kerfjs/dev`.** The hooks are process-global, so installing them is the consuming app's decision — a library that does it forces the diagnostics (and the chunk) on every consumer. Put the import in your demo page or test harness instead.
|
|
23
23
|
- Recommended: also install `eslint-plugin-kerfjs` (`npm install --save-dev eslint-plugin-kerfjs`) and add `kerfjs.configs.recommended` to the project's eslint config. It enforces five of the hard rules below (no inline JSX event handlers, require `data-key` in `each()`, capture `delegate()` disposers, no nested `mount()`, prefer module JSX augmentation) at edit time so violations surface as IDE squiggles before any code runs.
|
|
@@ -128,7 +128,7 @@ if (import.meta.env.DEV) await import("kerfjs/dev");
|
|
|
128
128
|
5. **Capture the `delegate()` / `delegateCapture()` disposer** whenever the registration's scope is shorter than the page. Both helpers return `() => void`; the listener closure pins `rootEl`, `handler`, and everything the handler closes over (stores, signals, app state). Discarding the disposer on a transient root (modal, route view, mount swap, dynamic widget) leaks the listener AND the app graph it captures; re-mount cycles stack listeners linearly. `mount()`'s own disposer does NOT remove delegates for you. Safe to discard only when the registration is truly page-lifetime (root is `document.body` or equivalent, attached once at startup, never torn down).
|
|
129
129
|
6. **One `mount()` per root.** Don't nest. Compose with plain functions that return JSX.
|
|
130
130
|
7. **No `<MyComponent />` semantics with hooks.** Components are plain functions returning JSX. State lives in module-scope signals or stores, never in component closures.
|
|
131
|
-
8. **Values bind, structure re-renders.** For a value hole, pass the signal/computed ITSELF (`<span>{count}</span>`, `class={sig}`) — kerf updates that one node directly, no render re-run. Read `.value` only when the JSX _structure_ depends on the signal — and then the read must happen INSIDE the render function to be tracked: `const x = count.value; mount(el, () => <span>{x}</span>)` will NOT re-render. Bind a STABLE signal/computed instance per hole (a `computed` that switches internally), never `class={cond ? sigA : sigB}` — switching instances can go silently stale (`KERF_DEV_WARN_STALE_BINDING=1` detects it). Endpoint: a render reading NO `.value` runs exactly once — a fully bound mount never re-renders; `KERF_DEV_WARN_VALUE_ONLY_RERENDER=1` flags re-renders that could have been bindings.
|
|
131
|
+
8. **Values bind, structure re-renders.** For a value hole, pass the signal/computed ITSELF (`<span>{count}</span>`, `class={sig}`) — kerf updates that one node directly, no render re-run. Read `.value` only when the JSX _structure_ depends on the signal — and then the read must happen INSIDE the render function to be tracked: `const x = count.value; mount(el, () => <span>{x}</span>)` will NOT re-render. Bind a STABLE signal/computed instance per hole (a `computed` that switches internally), never `class={cond ? sigA : sigB}` — switching instances can go silently stale (`enableWarnings({ staleBinding: true })` (or `KERF_DEV_WARN_STALE_BINDING=1` under Node) detects it). Endpoint: a render reading NO `.value` runs exactly once — a fully bound mount never re-renders; `enableWarnings({ valueOnlyRerender: true })` (or `KERF_DEV_WARN_VALUE_ONLY_RERENDER=1` under Node) flags re-renders that could have been bindings.
|
|
132
132
|
9. **Store actions receive `(set, get)`, not `(state)`.** `set(next)` replaces state; mutating `get()` does nothing.
|
|
133
133
|
10. **Use `data-action` attributes, not inline `onClick`.** Inline handlers are NOT supported by the JSX → string runtime; delegate from the root instead.
|
|
134
134
|
11. **`arraySignal` is opt-in for long keyed lists** where most updates are pointwise (single-row edits, append-to-end). For short lists / filter+sort pipelines, plain `signal` + `each(items.value, ...)` is simpler and equally fast.
|
|
@@ -207,7 +207,8 @@ const cart = defineStore({
|
|
|
207
207
|
});
|
|
208
208
|
|
|
209
209
|
// One-shot reconcile against existing DOM (no signals)
|
|
210
|
-
|
|
210
|
+
// the template describes liveCard's CHILDREN; liveCard's own attributes are untouched
|
|
211
|
+
morph(liveCard, '<h2 class="card-title">…</h2><p>…</p>');
|
|
211
212
|
|
|
212
213
|
// No build step (CDN / importmap): the html tagged template instead of JSX.
|
|
213
214
|
// Same runtime semantics as JSX; write real HTML attribute names; a hole must
|
|
@@ -260,7 +261,7 @@ mount(listEl, () => (
|
|
|
260
261
|
- ` html` ``: partial attribute values are not supported → in `kerfjs/html` templates a hole must be the COMPLETE attribute value. Replace `class="a ${b}"` with a pre-built string (`` class="${`a ${b}`}" ``) or, for a bound attribute, `class="${computed(() => `a ${b.value}`)}"`.
|
|
261
262
|
- An `each()` list's rows lose focus / scroll / typing state when an unrelated conditional list above them appears or disappears (kerf warns in dev) → lists without a key are identified by position among the render's `each()` calls, so adding/removing one above shifts this list's identity and kerf rebuilds it. Give the lists stable keys: `each(items, render, { key: 'results' })`; keying just the conditional list is usually enough.
|
|
262
263
|
- Keyed `each()` list suddenly renders zero rows — only its `<!--kf-list:N-->` marker — with no errors, and it never recovers (kerfjs ≤ 2.0.1) → a conditionally-rendered sibling BEFORE the list (possibly higher in the tree, e.g. an error banner) was removed that render; older kerfjs rebuilt the shifted list container from the template, permanently detaching the list's internal binding. Upgrade kerfjs (fixed after 2.0.1 — the morph now moves the shifted container up in place, keeping node identity). On older versions, keep the structure before the list stable: wrap the conditional in an always-present container (`<div class="banners">{cond ? <div/> : ''}</div>`).
|
|
263
|
-
- A numbered / zebra-striped / "N of M" `each()` list shows the wrong number on rows that MOVED (reorder, or non-tail insert/remove), while unmoved rows look right → the render fn's `index` argument is NOT part of the memo key (only item identity + `cacheKey` + content version are), so a row that keeps identity but changes position keeps HTML rendered at its old index. Fold the index into the memo key: `each(items, (it, i) => …, { cacheKey: (_, i) => i })` (add `key` if used). Opt-in dev warn: `KERF_DEV_WARN_STALE_INDEX=1
|
|
264
|
+
- A numbered / zebra-striped / "N of M" `each()` list shows the wrong number on rows that MOVED (reorder, or non-tail insert/remove), while unmoved rows look right → the render fn's `index` argument is NOT part of the memo key (only item identity + `cacheKey` + content version are), so a row that keeps identity but changes position keeps HTML rendered at its old index. Fold the index into the memo key: `each(items, (it, i) => …, { cacheKey: (_, i) => i })` (add `key` if used). Opt-in dev warn: `enableWarnings({ staleIndex: true })` (or `KERF_DEV_WARN_STALE_INDEX=1` under Node).
|
|
264
265
|
|
|
265
266
|
## Server / SSR
|
|
266
267
|
|
package/ai/manifest.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
|
-
"kerfjsVersion": "5.0.0-beta.
|
|
2
|
+
"kerfjsVersion": "5.0.0-beta.53",
|
|
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.23.
|
|
10
|
-
"sha256": "
|
|
9
|
+
"version": "1.23.1",
|
|
10
|
+
"sha256": "e21d7db1097ad95c11af9248ad17d514716bebc5646becf57d19a7598d6e382c",
|
|
11
11
|
"history": {
|
|
12
12
|
"1.0.0": "bef982cb743fd4c73e666ee5cf774112e57c93ebb13b24bbacbf08577db0930a",
|
|
13
13
|
"1.1.0": "f2aadb21e33dd389028951ae1ecbd9f8ba68648b3c5beba43fb4dfa2ab29f195",
|
|
@@ -37,7 +37,8 @@
|
|
|
37
37
|
"1.20.0": "aa88ccdbf969624bbc6cefadba76ada1f090fae436597a7bab564e24c467287f",
|
|
38
38
|
"1.21.0": "265b747eeb9c3d445c60d66a1109eec42705075b7cd154d107bd79d543c1e7e5",
|
|
39
39
|
"1.22.0": "dc99b0ca5e911be11cbfa2ae515c0e161a414a5185895069535c41ba56664e70",
|
|
40
|
-
"1.23.0": "28b82b4d31b98814c8fbbe1cb0bcf716192deb494c020b9af41a30830e6a0007"
|
|
40
|
+
"1.23.0": "28b82b4d31b98814c8fbbe1cb0bcf716192deb494c020b9af41a30830e6a0007",
|
|
41
|
+
"1.23.1": "e21d7db1097ad95c11af9248ad17d514716bebc5646becf57d19a7598d6e382c"
|
|
41
42
|
}
|
|
42
43
|
},
|
|
43
44
|
{
|
|
@@ -45,8 +46,8 @@
|
|
|
45
46
|
"source": "kerf.cursorrules",
|
|
46
47
|
"bundle": "ai/cursorrules",
|
|
47
48
|
"dest": ".cursorrules",
|
|
48
|
-
"version": "1.21.
|
|
49
|
-
"sha256": "
|
|
49
|
+
"version": "1.21.1",
|
|
50
|
+
"sha256": "9b3eb825a7bec05bb6f0138f77b45fadf896b83020178c34c5a2f531c38f4dee",
|
|
50
51
|
"history": {
|
|
51
52
|
"1.0.0": "98f7faff2b7585ef9cc511add42ea5d9bf0cbae8307b066af090e9281136d2ab",
|
|
52
53
|
"1.1.0": "a2ec8c221dc694c98b81009cd2d99f9159a45fda94585de2f54471b227099eda",
|
|
@@ -74,7 +75,8 @@
|
|
|
74
75
|
"1.18.0": "de3eea1bc181da629694d19a0817d252a868e4704437fbdd9e923856b8a0890a",
|
|
75
76
|
"1.19.0": "c5b91e449f6feccaa36a04cb7f8550d3e414eb2795ea6c5dccdf6071206b3261",
|
|
76
77
|
"1.20.0": "5e7b8d8ddc41f60220961417c9ae01c193fae045378a10a3e9c96a0e3cf10a03",
|
|
77
|
-
"1.21.0": "a1b663b44650b1a04f89c60b26b002322fc6789650291477e14e13329c3ce25f"
|
|
78
|
+
"1.21.0": "a1b663b44650b1a04f89c60b26b002322fc6789650291477e14e13329c3ce25f",
|
|
79
|
+
"1.21.1": "9b3eb825a7bec05bb6f0138f77b45fadf896b83020178c34c5a2f531c38f4dee"
|
|
78
80
|
}
|
|
79
81
|
}
|
|
80
82
|
]
|
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.23.
|
|
4
|
+
kerf-skill-version: 1.23.1
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Building apps with kerf
|
|
@@ -17,7 +17,7 @@ kerf is a ~12 KB reactive UI framework (~13 KB with `arraySignal`): signals + DO
|
|
|
17
17
|
- Install: `npm install kerfjs`
|
|
18
18
|
- `tsconfig.json`: `"jsx": "react-jsx"`, `"jsxImportSource": "kerfjs"`
|
|
19
19
|
- Vite / esbuild need no extra config.
|
|
20
|
-
- **Dev diagnostics are opt-in by import, and only an APP installs them.** kerf does not infer dev mode. In the app entry add `if (import.meta.env.DEV) await import('kerfjs/dev');` (Vite) or `if (process.env.NODE_ENV !== 'production') await import('kerfjs/dev');` (webpack/Node). That enables the read-only store `get()` snapshot, the throwing dangerous-URL screen, and makes the `KERF_DEV_WARN_*` family available; omitting it is production shape and sheds ~4.7 KB min+gzip because the condition folds away and the chunk is never emitted. Put it FIRST if relying on the untracked-signal
|
|
20
|
+
- **Dev diagnostics are opt-in by import, and only an APP installs them.** kerf does not infer dev mode. In the app entry add `if (import.meta.env.DEV) await import('kerfjs/dev');` (Vite) or `if (process.env.NODE_ENV !== 'production') await import('kerfjs/dev');` (webpack/Node). That enables the read-only store `get()` snapshot, the throwing dangerous-URL screen, and makes the `KERF_DEV_WARN_*` family available; omitting it is production shape and sheds ~4.7 KB min+gzip because the condition folds away and the chunk is never emitted. Put it (and any `enableWarnings()` call) FIRST if relying on the untracked-signal, delegate-in-effect, or rebuilt-listeners warnings — `signal()`, `effect()`, and `mount()` decide those at creation time, so anything created earlier stays uncovered.
|
|
21
21
|
- **Switch individual warnings on with `enableWarnings()`**, which is the only switch that works in a browser (no `process` object there, and a bundler `define` cannot reach the read): `const dev = await import('kerfjs/dev'); dev.enableWarnings({ staleBinding: true, narrowSet: true, invariants: 'throw' });`. The `KERF_DEV_WARN_*` env vars do the same for Node/SSR/CI; an explicit call wins either way.
|
|
22
22
|
- **A component package must NEVER import `kerfjs/dev`.** The hooks are process-global, so installing them is the consuming app's decision — a library that does it forces the diagnostics (and the chunk) on every consumer. Put the import in your demo page or test harness instead.
|
|
23
23
|
- Recommended companion: `npm install --save-dev eslint-plugin-kerfjs` and add `kerfjs.configs.recommended` to the project's eslint config. Enforces five of the hard rules below (no inline JSX event handlers, require `data-key` in `each()`, capture `delegate()` disposers, no nested `mount()`, prefer module JSX augmentation) at edit time — useful as a self-correction signal when authoring kerf code.
|
|
@@ -137,7 +137,7 @@ if (import.meta.env.DEV) await import("kerfjs/dev");
|
|
|
137
137
|
5. **Capture the `delegate()` / `delegateCapture()` disposer** whenever the registration's scope is shorter than the page. Both helpers return `() => void`; the listener closure pins `rootEl`, `handler`, and everything the handler closes over (stores, signals, app state). Discarding the disposer on a transient root (modal, route view, mount swap, dynamic widget) leaks the listener AND the app graph it captures; re-mount cycles stack listeners linearly. `mount()`'s own disposer does NOT remove delegates for you. Safe to discard only when the registration is truly page-lifetime (root is `document.body` or equivalent, attached once at startup, never torn down).
|
|
138
138
|
6. **One `mount()` per root.** Don't nest `mount()` calls. Compose with plain functions returning JSX.
|
|
139
139
|
7. **Components are plain functions.** `<MyComponent props />` works — the JSX runtime calls `MyComponent(props)` and uses the returned JSX — but there's no hook system, no lifecycle, and no per-instance state. State lives in module-scope signals or stores, never in component closures.
|
|
140
|
-
8. **Values bind, structure re-renders.** For a value hole, pass the signal/computed ITSELF (`<span>{count}</span>`, `class={sig}`) — kerf updates that one node directly, no render re-run. Read `.value` only when the JSX _structure_ depends on the signal — and then the read must happen INSIDE the render function to be tracked: `const x = count.value; mount(el, () => <span>{x}</span>)` does NOT re-render. One caveat on bound holes: bind a STABLE signal/computed instance per hole (`class={computed(() => …)}` that switches internally), never `class={cond ? sigA : sigB}` — switching instances can go silently stale (detectable via `KERF_DEV_WARN_STALE_BINDING=1`). The idiom's endpoint: a render that reads NO `.value` runs exactly once — a fully bound mount never re-renders. To find `.value` holes worth migrating, `KERF_DEV_WARN_VALUE_ONLY_RERENDER=1` flags re-renders whose only differences were text/attribute values.
|
|
140
|
+
8. **Values bind, structure re-renders.** For a value hole, pass the signal/computed ITSELF (`<span>{count}</span>`, `class={sig}`) — kerf updates that one node directly, no render re-run. Read `.value` only when the JSX _structure_ depends on the signal — and then the read must happen INSIDE the render function to be tracked: `const x = count.value; mount(el, () => <span>{x}</span>)` does NOT re-render. One caveat on bound holes: bind a STABLE signal/computed instance per hole (`class={computed(() => …)}` that switches internally), never `class={cond ? sigA : sigB}` — switching instances can go silently stale (detectable via `enableWarnings({ staleBinding: true })` (or `KERF_DEV_WARN_STALE_BINDING=1` under Node)). The idiom's endpoint: a render that reads NO `.value` runs exactly once — a fully bound mount never re-renders. To find `.value` holes worth migrating, `enableWarnings({ valueOnlyRerender: true })` (or `KERF_DEV_WARN_VALUE_ONLY_RERENDER=1` under Node) flags re-renders whose only differences were text/attribute values.
|
|
141
141
|
9. **Store actions take `(set, get)`, not `(state)`.** `set(next)` replaces state; mutating `get()` does nothing.
|
|
142
142
|
10. **Use `data-action` attributes, not inline `onClick`.** Inline handlers are NOT supported by the JSX → string runtime; delegate from the root.
|
|
143
143
|
11. **`arraySignal` is opt-in for long keyed lists** where most updates are pointwise. For short lists / filter+sort pipelines, plain `signal` + `each(items.value, ...)` is simpler and equally fast.
|
|
@@ -219,7 +219,8 @@ const cart = defineStore({
|
|
|
219
219
|
// access: cart.state.value.items, cart.actions.add('x'), cart.reset()
|
|
220
220
|
|
|
221
221
|
// Pattern 4: one-shot reconcile (no signals, no effect)
|
|
222
|
-
|
|
222
|
+
// the template describes liveCard's CHILDREN; liveCard's own attributes are untouched
|
|
223
|
+
morph(liveCard, '<h2 class="card-title">…</h2><p>…</p>');
|
|
223
224
|
|
|
224
225
|
// Pattern 5: fine-grained binding (opt-in) — pass the signal/computed ITSELF
|
|
225
226
|
// into a hole so a change updates ONLY that node (no render re-run, no
|
|
@@ -276,7 +277,7 @@ mount(
|
|
|
276
277
|
| ` html` ``: partial attribute values are not supported | In `kerfjs/html` templates a hole must be the COMPLETE attribute value | Build the full string first (``class="${`a ${b}`}"``), or bind `class="${computed(() => `a ${b.value}`)}"` for a reactive one |
|
|
277
278
|
| An `each()` list's rows lose focus / scroll / typing state when an unrelated conditional list above them appears or disappears (kerf warns about this in dev) | Lists without a key are identified by their position among the render's `each()` calls, so adding/removing one above shifts this list's identity and kerf rebuilds it | Give the lists stable keys: `each(items, render, { key: 'results' })`. Keying just the conditional list is usually enough |
|
|
278
279
|
| Keyed `each()` list suddenly renders zero rows — only its `<!--kf-list:N-->` marker — with no errors, and it never recovers (kerfjs ≤ 2.0.1) | A conditionally-rendered sibling BEFORE the list (possibly higher in the tree, e.g. an error banner) was removed that render; older kerfjs rebuilt the shifted list container from the template, permanently detaching the list's internal binding | Upgrade kerfjs (fixed after 2.0.1 — the morph now moves the shifted container up in place, keeping node identity). On older versions, keep the structure before the list stable: wrap the conditional in an always-present container (`<div class="banners">{cond ? <div/> : ''}</div>`) |
|
|
279
|
-
| A numbered / zebra-striped / "N of M" `each()` list shows the wrong number on rows that MOVED (reorder, or non-tail insert/remove), while unmoved rows look right | The render fn's `index` argument is NOT part of the memo key (only item identity + `cacheKey` + content version are), so a row that keeps identity but changes position keeps the HTML it rendered at its old index | Fold the index into the memo key so displaced rows re-render: `each(items, (it, i) => …, { cacheKey: (_, i) => i })` (add `key` if used). Opt-in dev warn: `KERF_DEV_WARN_STALE_INDEX=1`
|
|
280
|
+
| A numbered / zebra-striped / "N of M" `each()` list shows the wrong number on rows that MOVED (reorder, or non-tail insert/remove), while unmoved rows look right | The render fn's `index` argument is NOT part of the memo key (only item identity + `cacheKey` + content version are), so a row that keeps identity but changes position keeps the HTML it rendered at its old index | Fold the index into the memo key so displaced rows re-render: `each(items, (it, i) => …, { cacheKey: (_, i) => i })` (add `key` if used). Opt-in dev warn: `enableWarnings({ staleIndex: true })` (or `KERF_DEV_WARN_STALE_INDEX=1` under Node) |
|
|
280
281
|
|
|
281
282
|
## Workflow guidance
|
|
282
283
|
|
package/dist/actions.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { attr } from './chunk-MK42GLPV.js';
|
|
2
|
-
import { delegate } from './chunk-
|
|
2
|
+
import { delegate } from './chunk-X6OQ2HTR.js';
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
// src/actions.ts
|
|
@@ -14,8 +14,8 @@ function delegateActions(root, eventType, table, options) {
|
|
|
14
14
|
eventType,
|
|
15
15
|
`[${attrName}]`,
|
|
16
16
|
(event, el) => {
|
|
17
|
-
const
|
|
18
|
-
if (
|
|
17
|
+
const value = el.getAttribute(attrName);
|
|
18
|
+
if (Object.hasOwn(table, value)) table[value](event, el);
|
|
19
19
|
},
|
|
20
20
|
options
|
|
21
21
|
);
|
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;AAClC,EAAA,OAAO,QAAA;AAAA,IACL,IAAA;AAAA,IACA,SAAA;AAAA,IACA,IAAI,QAAQ,CAAA,CAAA,CAAA;AAAA,IACZ,CAAC,OAAO,EAAA,KAAO;AAEb,MAAA,MAAM,
|
|
1
|
+
{"version":3,"sources":["../src/actions.ts"],"names":[],"mappings":";;;;;AAmCA,IAAM,mBAAA,GAAsB,aAAA;AAkBrB,SAAS,OAAyB,KAAA,EAAsC;AAC7E,EAAA,OAAO,IAAA,CAAK,qBAAqB,KAAK,CAAA;AACxC;AAwBO,SAAS,eAAA,CACd,IAAA,EACA,SAAA,EACA,KAAA,EACA,OAAA,EACY;AACZ,EAAA,MAAM,QAAA,GAAW,SAAS,IAAA,IAAQ,mBAAA;AAClC,EAAA,OAAO,QAAA;AAAA,IACL,IAAA;AAAA,IACA,SAAA;AAAA,IACA,IAAI,QAAQ,CAAA,CAAA,CAAA;AAAA,IACZ,CAAC,OAAO,EAAA,KAAO;AAEb,MAAA,MAAM,KAAA,GAAQ,EAAA,CAAG,YAAA,CAAa,QAAQ,CAAA;AAKtC,MAAA,IAAI,MAAA,CAAO,OAAO,KAAA,EAAO,KAAK,GAAG,KAAA,CAAM,KAAK,CAAA,CAAE,KAAA,EAAO,EAAE,CAAA;AAAA,IACzD,CAAA;AAAA,IACA;AAAA,GACF;AACF","file":"actions.js","sourcesContent":["/**\n * `kerfjs/actions` — the delegated action-table helper.\n *\n * The most-reinvented idiom across real kerf apps: one table of `data-action`\n * attribute specs used as the single source of truth for BOTH the JSX attribute\n * and the delegate selector, plus a hand-rolled `switch (dataset.action)`\n * dispatcher. This subpath blesses it as two thin helpers over the existing\n * `attr()` + `delegate()` — it does NOT replace them.\n *\n * import { action, delegateActions } from 'kerfjs/actions';\n *\n * const A = {\n * select: action('select-file'),\n * remove: action('remove-file'),\n * };\n *\n * // JSX — spread the attr (rename-safe; no hardcoded attribute name):\n * // <button {...A.select.attrs} data-id={id}>…</button>\n *\n * // Wire the whole table with ONE delegated listener; returns a disposer:\n * const dispose = delegateActions(root, 'click', {\n * [A.select.value]: (_e, el) => selectFile(el.getAttribute('data-id')),\n * [A.remove.value]: (_e, el) => removeFile(el.getAttribute('data-id')),\n * });\n *\n * Contract: `delegateActions` returns a `() => void` disposer and holds no\n * per-instance state — the same shape as `delegate()`, which it builds on (so\n * it inherits the single-listener dispatch and the capture auto-promotion for\n * well-known non-bubbling event types). One event type per call, mirroring\n * `delegate()`; collect the disposers for a root that needs several.\n */\nimport { attr, type AttrSpec } from './attr.js';\nimport { delegate, type DelegateOptions } from './delegate.js';\n\n/** The attribute an action table keys on by default. */\nconst DEFAULT_ACTION_ATTR = 'data-action';\n\n/**\n * A handler in a {@link delegateActions} table. Receives the DOM event and the\n * matched element (walk-up `closest()` match by default) — the same shape as a\n * `delegate()` handler.\n */\nexport type ActionHandler<E extends Element = Element> = (\n event: Event,\n el: E,\n) => void;\n\n/**\n * `action(value)` — an {@link AttrSpec} on `data-action`. A thin specialization\n * of `attr('data-action', value)`: spread its `.attrs` in JSX and use its\n * `.value` as the handler-table key, so the action name lives in exactly one\n * place and can't drift between the markup and the dispatcher.\n */\nexport function action<V extends string>(value: V): AttrSpec<'data-action', V> {\n return attr(DEFAULT_ACTION_ATTR, value);\n}\n\n/** Options for {@link delegateActions}. Extends {@link DelegateOptions}. */\nexport interface DelegateActionsOptions extends DelegateOptions {\n /**\n * The attribute the table keys on. Default `'data-action'`. Override it only\n * if you also author the specs with `attr(yourName, …)` instead of `action()`.\n */\n attr?: string;\n}\n\n/**\n * Wire a whole table of action handlers with ONE delegated listener.\n *\n * On `eventType`, the nearest element carrying the action attribute (walk-up\n * `closest()` by default; pass `{ match: 'direct' }` for an exact-element match)\n * is looked up in `table` by its attribute value, and the matching handler\n * runs. An element whose action is absent from the table is ignored — the same\n * behavior as a `switch (dataset.action)` with no matching `case`.\n *\n * Returns a `() => void` disposer. One event type per call (the smallest\n * surface, mirroring `delegate()`); collect the disposers when a root needs\n * several event types.\n */\nexport function delegateActions<E extends Element = Element>(\n root: HTMLElement,\n eventType: string,\n table: Readonly<Record<string, ActionHandler<E>>>,\n options?: DelegateActionsOptions,\n): () => void {\n const attrName = options?.attr ?? DEFAULT_ACTION_ATTR;\n return delegate<E>(\n root,\n eventType,\n `[${attrName}]`,\n (event, el) => {\n // `el` matched `[${attrName}]`, so the attribute is always present.\n const value = el.getAttribute(attrName) as string;\n // Own entries only: the table is an ordinary object, so a bare\n // `table[value]` would resolve Object.prototype names (`__proto__`,\n // `constructor`, `toString`, `hasOwnProperty`, …) and dispatch or throw\n // on markup that names no action in the table.\n if (Object.hasOwn(table, value)) table[value](event, el);\n },\n options,\n );\n}\n"]}
|
package/dist/array-signal.js
CHANGED
package/dist/async.js
CHANGED