kerfjs 5.0.0-beta.50 → 5.0.0-beta.51

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,15 +1,467 @@
1
1
  # Changelog
2
2
 
3
- - Added `wa-dialog.hide-actions` support to `@kerfjs/ui/webawesome.css`, hiding
4
- Web Awesome's directly exported `header-actions` shadow part for dialogs that
5
- provide their own dismissal affordance.
6
-
7
3
  All notable changes to **kerf** are documented in this file.
8
4
 
9
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
10
6
 
11
7
  ## [Unreleased]
12
8
 
9
+ - `@kerfjs/ui/catalog`'s application-sized (`height: "app"`) example frame
10
+ no longer loses taps in Safari/WebKit. The frame became the containing block
11
+ for screen-fixed chrome through `contain: layout`. Combined with its clip,
12
+ that made WebKit mis-hit-test any transformed element inside an open compact
13
+ overlay. Every pressed `ListItem` scales, so taps on an overlay rail's items
14
+ went to their section instead. The frame now uses an identity transform,
15
+ which docks the same chrome without the bug and renders identically.
16
+ - **Breaking (`@kerfjs/ui/catalog`):** `CatalogExample`'s viewport drops
17
+ `height: "fill"` and `fillChildren`. The catalog lays examples out at their
18
+ content height, so neither could give a specimen a definite height, and a
19
+ child sized with `height: 100%` (a horizontal `ResizableRegion`, say)
20
+ collapsed to its content. Use a fixed `height` (`short` through `app`) when
21
+ a specimen must fill its frame. `minHeight` remains, documented as a minimum
22
+ rather than a fill.
23
+ - `create-kerf-component`'s `kerf.components.json` now accepts an optional
24
+ `composition.wiring.stateAttributes` declaration (the wiring-owned state
25
+ attributes `@kerfjs/ui`'s catalog now carries), so third-party
26
+ component authors can list the `data-*` attributes their wiring helpers write
27
+ (`{ name, on, helper, meaning }`). `kerf-component-catalog` passes them
28
+ through to the generated `component-catalog-v2.json` and reports a non-`data-*`
29
+ name, a duplicate name, an empty `on` or `meaning`, or a helper missing from
30
+ `wiring.helpers` at its exact path.
31
+ - `@kerfjs/ui`'s component catalog now declares **wiring-owned state
32
+ attributes**. A new optional `wiring.stateAttributes` in
33
+ `component-catalog-v2` (and per wiring item in v1) lists each `data-*`
34
+ attribute a wiring helper writes at runtime, such as `data-handle-inset` and
35
+ `data-resizing` from `wireResizableRegions`. For each one it records the
36
+ element, the helper, and the meaning, so AI consumers and tools can tell
37
+ them apart from attributes the app writes. Every first-party wiring helper
38
+ is declared. `check:catalog` fails when a helper's declarations differ from
39
+ what its source writes.
40
+ - `@kerfjs/ui`'s `wireSidebar` no longer leaves focus inside a panel it just
41
+ collapsed. When the collapse was triggered from a toggle inside the panel
42
+ (the documented placement), focus now moves to that panel's toggle outside
43
+ it, including one the app renders only once the panel is collapsed. The
44
+ collapsible-sidebar recipe now shows one control per action: each panel's
45
+ own collapse toggle while it is open, and the main header's expand toggle
46
+ only while it is collapsed. Wide widths no longer show two rail toggles.
47
+ - `@kerfjs/ui`'s `wireSidebar` keeps a compact overlay intact when the app
48
+ re-renders the wired root without changing a panel. Previously, selecting a
49
+ destination in an open compact rail morphed the root, which removed the
50
+ overlay attributes and the backdrop, and the "overlay" rail fell back into
51
+ the layout. The wire now re-applies its host attributes and backdrop after
52
+ any such re-render, and the backdrop carries `data-morph-preserve`.
53
+ - `@kerfjs/ui`'s `wireSidebar` now opens a compact overlay only on a user
54
+ action. Wiring on a compact device, or crossing from a wide device class to
55
+ a compact one, collapses every panel. The page is no longer covered by a
56
+ backdrop and focus trap until dismissed. Crossing back to a wide class, or
57
+ disposing the wire, restores each panel's remembered inline state.
58
+ Persistence now stores only that inline choice, never an overlay's
59
+ open/closed state. A new optional `WireSidebarPanel.inlineCollapsed`
60
+ declares the inline default when an app seeds its signal from the device
61
+ class, which avoids an open first frame on a compact device. The
62
+ collapsible-sidebar recipe no longer opens its rail as a blocking overlay
63
+ on load at compact widths.
64
+ - `@kerfjs/ui`'s `ResizableRegion` handle keeps its hit target and focus ring
65
+ whole at a clipping edge. When a parent clamps the region to its own edge
66
+ (the catalog's 390px stage, an app shell with a `max-width` container),
67
+ `wireResizableRegions` marks the region `data-handle-inset` and the handle
68
+ moves inside it instead of overhanging the edge by 10px, where the frame cut
69
+ its focus ring in half. The ring is now drawn inside the handle's box
70
+ (`outline-offset: -3px`), so it is also no longer shaved where the region's
71
+ ends meet a clipping edge.
72
+ - `@kerfjs/ui`'s `wireResizableRegions` keeps a clamped separator's
73
+ `aria-valuenow`/`aria-valuemax` current at rest. Previously, after a size was
74
+ saved at a wider width, narrowing the parent or any unrelated re-render left
75
+ the handle reporting the unclamped rendered size until it was next focused or
76
+ resized. A `ResizeObserver` on each region and its parent and a
77
+ `MutationObserver` for re-renders now re-clamp the report; the disposer
78
+ disconnects both.
79
+ - The `@kerfjs/ui` catalog's `height: "app"` example frame is now the
80
+ containing block for screen-fixed chrome, so a compact `CollapsiblePanel`
81
+ overlay, its backdrop, and restore controls dock to the frame's edges. The
82
+ collapsible-sidebar recipe's compact rail was fixed to the page's left edge,
83
+ 24px outside the inset frame, so its leading edge looked cut off at 390px.
84
+ - The `CollapsiblePanel` compact-overlay backdrop now covers its whole
85
+ containing block. As a `<button>`, it did not stretch between its insets and
86
+ scrimmed only a 35px strip.
87
+ - `@kerfjs/ui`'s `wireResizableRegions` now bounds a resize to the space the
88
+ region's parent actually leaves. When a parent clamps the track (a
89
+ `max-width` stage, a narrow container), pointer and keyboard resizing stop at
90
+ the visible size, and the separator reports that size in `aria-valuenow` and
91
+ its visible maximum in `aria-valuemax`. Previously a region resized to 420
92
+ inside a 340px stage announced 420 and needed several key presses before the
93
+ separator moved. Focusing the handle reports a stale larger committed size as
94
+ the shown size without committing it.
95
+ - The UI evaluator's `KUI-B050` target-size rule now accepts a 44×36 target
96
+ inside compact density (`[data-density="compact"]`), matching the 36px
97
+ compact row height; standard density still requires 44×44, and the width
98
+ floor is unchanged.
99
+ - Fixed `kerf-ui-analyze` and the `eslint-plugin-kerfjs` UI rules treating
100
+ every name in a catalog entry's `publicExports` as that entry's component,
101
+ so a helper such as `uiColor` or `flex` called with an object whose keys
102
+ matched a contract path (`uiColor({ choices: [...] })`) was checked as a
103
+ `Select` or `List` call. Only component exports, whose names start with an
104
+ uppercase letter as JSX component tags do, now resolve to an entry; the
105
+ `@kerfjs/ui` catalog check fails if a cataloged `SafeHtml` render function
106
+ is lowercase or any other runtime export is uppercase.
107
+ - The `@kerfjs/ui` component catalog's `ToolbarText` entry now lists every
108
+ token its `xlarge` size reads (`--kui-color-text`, `--kui-font-l`,
109
+ `--kui-font-2xl`) in `publicTokens`; previously it named only the tokens of
110
+ the smaller sizes.
111
+ - The `@kerfjs/ui` browser evaluator's target-size check (`KUI-B050`) now
112
+ measures the pointer-reachable area instead of only the border box: an
113
+ undersized control is hit-tested outward from its center, so a transparent
114
+ `::before`/`::after` hit layer (such as the `ListHeader` action's) counts,
115
+ while `pointer-events: none` decoration, clipping, and overlapping
116
+ neighbors do not. Diagnostic evidence now carries the measured size plus
117
+ the border `box`.
118
+ - Fixed the `eslint-plugin-kerfjs` `ui-css-values` messages (`KUI-L013`,
119
+ `KUI-L014`, `KUI-L017`) listing suggested helpers with unbalanced backticks
120
+ (``use `uiColor() or `foregroundColorVar().``); each helper is now wrapped
121
+ as `` `uiColor()` ``.
122
+ - Fixed `kerf-ui-analyze` skipping the CSS-value contracts of `@kerfjs/ui`
123
+ `Select` and `Skeleton`. Its component lookup was keyed by display name, so
124
+ the same-named Web Awesome catalog entries (`wa-select`, `wa-skeleton`) —
125
+ which carry no CSS-value props — shadowed them, and a wrong-grammar helper
126
+ such as `colorVar()` for `SelectChoice.color` went unreported although the
127
+ ESLint rule caught it. Entries now resolve by `package:id`, using the
128
+ selection catalog's public exports and import subpaths.
129
+ - Fixed `@kerfjs/ui` monospace text rendering in the generic face (Courier on
130
+ macOS Chromium) whenever Web Awesome was loaded: `--kui-font-mono` deferred to
131
+ Web Awesome's `ui-monospace, monospace`, which Chromium does not resolve, and
132
+ the trailing generic `monospace` kept Kerf's named fallbacks from ever
133
+ applying. `--kui-font-mono` is now Kerf's own stack (`ui-monospace,
134
+ SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace`),
135
+ and the Web Awesome theme points `--wa-font-family-code` at it so Kerf `Text
136
+ font="monospace"` and Web Awesome code text match.
137
+ - The `@kerfjs/ui` Web Awesome theme now gives default-size single-line
138
+ controls Kerf's 44px control height, so a plain `wa-input` or `wa-button`
139
+ sits flush beside a Kerf `Select` instead of about 9px shorter. It sets
140
+ `--wa-form-control-height` to `2.75em` (44px at the default size) on Button,
141
+ Color Picker, Input, Known Date, Number Input, Radio Group, Select, Textarea,
142
+ and Time Input; `size="large"` scales proportionally, `size="small"` keeps Web
143
+ Awesome's compact height, and buttons inside a `ToolbarControlGroup` keep the
144
+ group's 40px (32px compact) height. Tags, copy buttons, and toasts are
145
+ unchanged.
146
+ - Fixed an `@kerfjs/ui` `Select` menu drawing two separators when a choice with
147
+ `separatorBefore` opened a separated group: the group's border plus a
148
+ `wa-divider` left an empty band between the group title and its first
149
+ choice. `separatorBefore` now only divides a choice from the previous choice
150
+ in the same list; a group boundary is already the single separator.
151
+ - Fixed `@kerfjs/ui` compact `ToolbarControlGroup` end highlights not being
152
+ concentric with the group: controls sat 2px from the sides but 1px from the
153
+ top and bottom. A compact group now has no padding, so its 32px controls sit
154
+ on a 1px inset on every side with a radius of the group radius minus 1px
155
+ (pill and rounded). In compact mixed groups the raised selection still paints
156
+ over the border with the group's radius, neighboring controls still abut, and
157
+ the trailing dropdown trigger keeps the 1px end inset instead of running onto
158
+ the border. Compact groups are 2px narrower as a result.
159
+ - Added `Toolbar.safeAreaEdges` to `@kerfjs/ui` so an app bar or bottom bar
160
+ that sits at a screen edge, outside any `Pane` header or footer, can claim
161
+ those edges for device safe areas. A toolbar there used to take only the
162
+ inline edges an owner handed it, so the app-shell recipe's top bar never
163
+ cleared the status area. Each claimed side pads by the routed
164
+ `--kui-edge-inset-*` context, or the full device inset when nothing routes
165
+ it, while the bar's surface and divider still paint through; the minimum
166
+ height grows with the claimed block insets so a shrinking flex column keeps a
167
+ whole control row clear. A claim inside a `Pane` header or footer adds
168
+ nothing, since the pane already owns those edges. The app-shell recipe's app
169
+ bar now claims the top edge and both sides.
170
+ - Fixed `@kerfjs/ui` slide-motion `ResizableRegion` content overflowing its
171
+ separator when a parent clamps the region. The expanded content kept the
172
+ committed size even when a `max-width` or narrow container held the track
173
+ smaller, so in the catalog at 390px a region resized to 420px showed text
174
+ running past its handle. Expanded content now follows the region's actual
175
+ track on both axes; the fixed expanded size still applies while collapsing, so
176
+ the collapse keeps reading as a slide.
177
+ - Fixed `@kerfjs/ui` `ResizableRegion` panes stopping at their content's
178
+ height. The region's content now spans the whole region, like its separator,
179
+ and a lone child (normally a `Pane`) fills it, so the app-shell recipe's
180
+ navigation and inspector panes reach the bottom edge and scroll long content
181
+ inside their own content slot instead of ending partway down the region.
182
+ Several children still stack at their natural height.
183
+ - Fixed `@kerfjs/ui` `Toolbar responsive="center-priority"` leaving an
184
+ expanded center control at roughly half the row on narrow toolbars. The
185
+ policy set the toolbar's own grid template inside a container query on the
186
+ toolbar itself, which can never match, so the center kept only the first
187
+ column track once the leading and trailing zones left. The dead rule is gone
188
+ and the expanded center now spans every column track, filling the toolbar's
189
+ content width at phone widths.
190
+ - Fixed a nested `@kerfjs/ui` `List`, `Row`, or `Grid` inheriting its
191
+ ancestor's `gap` and `flex`. Those props travel through private custom
192
+ properties, which inherit, so a nested `List` without its own `gap` took the
193
+ enclosing `List`'s gap (a heading unit inside a form got a 24px gap between
194
+ its title and supporting line) and any nested instance without `flex` took the
195
+ ancestor's flex growth. Every instance now resets its private values, so an
196
+ omitted prop means that component's own default. The workspace-header recipe's
197
+ breadcrumb now sits directly above its title and the List demo's section rows
198
+ sit flush under their header, as intended.
199
+ - Fixed `@kerfjs/ui` slide-motion `ResizableRegion` content staying at its
200
+ previous width during a live resize until the app re-rendered the committed
201
+ size. `wireResizableRegions` now applies the live size to the sliding content
202
+ as well as the track, for pointer drags and key presses, so the content fills
203
+ the region (including a safe-area edge extent) throughout the drag and a later
204
+ collapse still slides it out at the resized width.
205
+ - Fixed the `@kerfjs/ui` `ListHeader` action's pointer target being only its
206
+ fitted 36px square although the guidance promises 44px. A transparent hit
207
+ layer now extends the target to at least 44 × 44 around the square without
208
+ moving layout or enlarging the painted hover surface. Compact headers are
209
+ 36px tall like the rest of the compact List family, so their action target is
210
+ 44px wide and clamped to the header height, never reaching into a neighboring
211
+ row.
212
+ - Fixed re-renders closing open Web Awesome popups: the morph (and the keyed-list
213
+ attribute fast path) no longer removes a live `open` attribute from custom
214
+ elements (`wa-select`, `wa-dropdown`, `wa-dialog`, …) when the template omits
215
+ it — the same user-agent-owned treatment `<details>` / `<dialog>` already
216
+ had. A template can still set `open`; closing stays imperative.
217
+ - The `@kerfjs/ui` composer-form recipe's supporting line now sits directly
218
+ under its toolbar title instead of 24px lower, because a nested `List` no
219
+ longer inherits the form's section gap (see the nested `List`/`Row`/`Grid`
220
+ fix above). The guidance now
221
+ also states that `ToolbarText` below `xlarge` is intentionally quiet identity
222
+ text with no tone option. A normal-color subsection title is `Text` with an
223
+ `h2`–`h6` variant, or `ListHeader` for a list or menu section.
224
+ - Fixed `@kerfjs/ui` `ToolbarText size="small"` labels rendering in a
225
+ monospace fallback face in Chromium. The small size used the code-font token,
226
+ which resolves through Web Awesome's `ui-monospace, monospace` stack; Chromium
227
+ does not recognize `ui-monospace`, so labels such as a pane's "Workspace" or
228
+ "Ready" fell back to the generic monospace font. Every `ToolbarText` size now
229
+ keeps the inherited UI sans stack.
230
+ - Fixed `@kerfjs/ui` `ListInsetControl` not stretching its control as
231
+ documented. Its `justify-content: stretch` is a no-op on a flex row, so a
232
+ `Select`, `wa-input`, or `wa-button` inside stayed at its natural width. Each
233
+ direct child now grows to fill the inset row (several children share it with
234
+ the 8px gap) and may shrink below its intrinsic width instead of overflowing.
235
+ - `@kerfjs/ui` `List`, `Row`, and `Grid` can now be the layout root of a
236
+ fixed-height parent. New `fill` takes the parent's full height (use `flex`
237
+ inside a flex layout as before), and new `rootAttributes` carries safe
238
+ `data-*` metadata while the component's structural attributes stay
239
+ protected. The app-shell and collapsible-sidebar recipes now use a filling
240
+ List or Row root instead of a frame `Pane` whose scroll owner never scrolled;
241
+ the collapsible sidebar's rail and drawer now reach the real frame edges, so
242
+ they paint through safe areas as the layout model intends. The UX catalog
243
+ adds an application-sized `height: "app"` (592px) specimen frame, and both
244
+ recipes use it instead of the cramped 360px `tall` frame.
245
+ - Fixed a `@kerfjs/ui` stacked `Toolbar` clipping its last trailing action at
246
+ phone widths (the compact-toolbar recipe's "More" button at 390px). A
247
+ toolbar's trailing zone now wraps whole control groups onto another
248
+ trailing-aligned row under every `responsive` policy, and a
249
+ `responsive="stack"` toolbar wraps its stacked center and trailing groups the
250
+ same way, so no action is ever clipped or shrunk. Stacked zones also span the
251
+ full toolbar width; they previously lost 16px to empty column tracks. New:
252
+ `responsive="wrap"` keeps a heading on one row while everything fits and
253
+ otherwise moves the trailing zone below a whole title instead of truncating
254
+ it. The loading-inspector recipe uses it, so its narrow heading shows the
255
+ whole record title instead of an ellipsis.
256
+ - `@kerfjs/ui` layouts and `Pane` now handle device safe areas as one model.
257
+ Pane and panel backgrounds, separators, and dividers paint through unsafe
258
+ areas; content is padded only on the edges a region actually reaches, and
259
+ block insets become scroll padding so content can scroll under the status bar
260
+ or home indicator but always scroll clear. `Workbench` rails and drawer,
261
+ `CollapsiblePanel`, and the `SplitView` list grow by the inset they dock to.
262
+ A center pane picks up the inset when an adjacent rail collapses, and
263
+ interior or nested edges are never double-inset. A header or footer `Toolbar`
264
+ keeps its dividers edge to edge. New: `Pane.safeAreaEdges` (default all
265
+ sides; `[]` opts out), the `--kui-safe-area-*` device-inset tokens (default
266
+ `env(safe-area-inset-*)`), the inherited `--kui-edge-inset-*` edge context for
267
+ app-owned layouts, and `--kui-resizable-region-edge-extent`. Nothing changes
268
+ on devices or pages without safe-area insets.
269
+ - Fixed a `@kerfjs/ui` `ListHeader` action sitting about 4px outside the
270
+ trailing toolbar action of the Pane header above it. Its fitted action now
271
+ centers in the same 44px slot a toolbar control occupies at the 8px inline
272
+ margin, so trailing icon actions share one axis. The action's size, 18px
273
+ visual, and hover square are unchanged.
274
+ - **Breaking (`@kerfjs/ui`, 5.0 beta):** `SelectChoice.color` now accepts only
275
+ a foreground color, the new `CssForegroundColor` brand from
276
+ `@kerfjs/ui/css-values`. `uiColor()` returns it for foreground token names
277
+ (the new `UiForegroundColorName`: the `*-on-*` roles, `text`, `text-quiet`,
278
+ `text-link`, and the `*-text` aliases), and the new
279
+ `foregroundColorVar('--app-icon-color', fallback?)` returns it for an
280
+ application-owned property. The `--kui-color-success` / `warning` / `danger`
281
+ / `pop` / `accent` compatibility aliases are quiet fills that read like
282
+ foreground colors; colored with one, an icon rendered at about 1.2:1
283
+ contrast. `uiColor('success')` and the other fill, border, and surface tokens
284
+ still return a plain `CssColor` and no longer type-check as an icon color,
285
+ and neither does a plain `colorVar()` value. Migrate
286
+ `color: uiColor('success')` to `uiColor('success-on-quiet')` and
287
+ `color: colorVar('--app-color')` to `foregroundColorVar('--app-color')`;
288
+ `uiColor('…-on-…')` calls keep compiling unchanged. The catalog's CSS-value
289
+ contract for `choices[].color` lists `uiColor` and `foregroundColorVar`, so
290
+ `eslint-plugin-kerfjs` also reports `colorVar()` there (`KUI-L014`), and
291
+ the source and packed consumer type contracts pin the rejected fills.
292
+
293
+ - Fixed an `@kerfjs/ui` icon-only `Select` beside other controls in one
294
+ `ToolbarControlGroup`. A compact Select no longer eats 1px of the group's
295
+ 4px gap on each side (the lone-trigger padding overlap now applies only when
296
+ it is the group's only child), a compact rounded Select uses its siblings'
297
+ item radius instead of the lone-slot radius, and the Select now shows the
298
+ same hover pill as its sibling buttons (a lone Select lifts the whole group,
299
+ like a popup-menu dropdown). In a group with per-control focus rings
300
+ (`focusRing="control"`, the default) the Select paints the same ring as its
301
+ siblings even when `focusRingOwner="group"` was set, which previously left it
302
+ with no visible focus. A compact icon-only `<button>` is now a 32px circle,
303
+ so a selected compact segment is no longer a 34x32 oval. The
304
+ ToolbarControlGroup catalog adds a "Select beside actions" example.
305
+
306
+ - Changed the `@kerfjs/ui` icon-only `Select`
307
+ (`selectedPresentation="icon-only"`) to keep its disclosure caret. The
308
+ selected icon and caret now form one pill with the same geometry as a
309
+ `ToolbarControlGroup` popup-menu `wa-dropdown` trigger: 8px inline padding on
310
+ both sides, 12px from the icon to the caret, the same caret glyph size, and a
311
+ 40px default or 32px compact control height, so the icon sits left of center
312
+ to balance the caret. In a single `ToolbarControlGroup` the group now sizes to
313
+ the trigger (a 70x44 default or 68x34 compact pill, wider than it is tall)
314
+ with an even inset on every side and a concentric radius, for pill and
315
+ rounded shapes, so the group-owned outline or halo ring follows the pill while
316
+ focused and while the listbox is open. Before, a default-size trigger was a
317
+ 32px square left-aligned in its 44px group, and the caret was hidden. The
318
+ Select catalog adds a default-size "Toolbar icon" example beside the compact
319
+ one.
320
+
321
+ - Fixed the `@kerfjs/ui` icon-only `Select` focus ring in a
322
+ `ToolbarControlGroup` not matching the control: the compact trigger grew its
323
+ group into a 36x34 oval and its disclosure caret overflowed the trailing edge.
324
+ The group-owned ring now stays concentric with the trigger while focused and
325
+ while the listbox is open. The Select demo's "Quiet" option icon also used the success fill alias
326
+ as its color and was nearly invisible; it now uses `success-on-quiet`, and the
327
+ `SelectChoice.color` guidance names foreground tokens.
328
+
329
+ - `@kerfjs/ui` UX catalog: the ten production recipes now compose Kerf
330
+ components, layouts, and Web Awesome surfaces through their public
331
+ configuration alone, with no route stylesheets, inline styles, or
332
+ styling-only classes. The list-detail dialog is rebuilt from `DialogSurface`,
333
+ the dialog's own labeled header and close control, `SplitView` (a full-screen
334
+ list-to-detail drill-down on compact devices), aligned `ListHeader`s, a
335
+ `ValueTable`, and footer actions. The desktop application shell switches to one
336
+ pane at a time below desktop sizes through `deviceClass()`. Page and pane
337
+ headings stack instead of hiding their titles at narrow widths, and supporting
338
+ copy, content-item text, and loading skeletons sit on the shared 17px text
339
+ inset. The catalog frames each recipe in a `CatalogExample` viewport that the
340
+ recipe declares through an exported `presentation`, and recipe ownership notes
341
+ render as that example's note. `npm run check:recipes` now fails on a recipe
342
+ stylesheet, an inline style, a class outside the published layout and Web
343
+ Awesome vocabulary, a non-public stylesheet import, or recipe-specific rules in
344
+ the catalog shell stylesheet.
345
+ - Fixed `@kerfjs/ui/webawesome.css` `wa-dialog` titles given as a plain-string
346
+ `label` sitting at the bare surface inset; they now inset by one bordered
347
+ item step (`--kui-layout-item-padding` + 1px) so the header label lines up
348
+ with body text and icons. Slotted label elements are unaffected.
349
+ - Fixed icon + label `wa-button`s inside `ToolbarControlGroup` stacking the
350
+ icon above the label; their label part now lays out as a single nowrap row,
351
+ matching native mixed buttons.
352
+ - `prompt()` / `form()` now reject with a `TypeError` when a validator returns
353
+ a promise (validators are synchronous), instead of resolving the value and
354
+ leaving an unhandled rejection. `prompt()` also re-reads its input on every
355
+ OK, so a reactive `render` that replaces the input no longer submits the
356
+ detached original's stale value, and a removed input rejects the promise.
357
+ - A local `npm run check` run with `KERF_SKIP_PACKAGE_GATES=1` no longer
358
+ records a verified tree, so the pre-push hook cannot skip the sibling-package
359
+ gates that run left out.
360
+ - `kerfjs/overlay`: a `prompt()` `validate` or `form()` `field.validate` that
361
+ throws now closes the dialog (full teardown, focus restored) and rejects the
362
+ returned promise with the original error. Previously the error escaped the
363
+ OK click or Enter handler, and the dialog stayed open with an OK button that
364
+ could never succeed. Validators are synchronous; this is the error channel
365
+ for a validator bug.
366
+ - `kerfjs/overlay`: when a `form()` field input disappears after the dialog
367
+ opens (for example a reactive `render` re-render drops it), OK and Enter now
368
+ close the dialog and reject the promise with an error naming the missing
369
+ field. Previously `form()` resolved `null`, which
370
+ reads as a Cancel, and threw from the click handler.
371
+ - `@kerfjs/ui`: a multiline `ListItem` whose label fits on one line now
372
+ centers its icon on that line. The row's 44px minimum used to stretch the
373
+ grid track, which centered the label but left the start-aligned icon about
374
+ 1.4px above it.
375
+ - `@kerfjs/ui`: a `ToolbarControlGroup` with `focusRing="outline"` or
376
+ `"halo"` keeps its group focus ring while a `Select` inside it has its
377
+ listbox open. Chromium and WebKit did not carry `:focus-within` from the
378
+ focused option to the group, so the ring disappeared.
379
+ - UX catalog: the foundation palette's Brand swatch uses the brand on-fill
380
+ color again, the Spacer demo gives its flexible spacer room to grow again,
381
+ and the TokenSearchField adoption demo again wires its chip keyboard and
382
+ `onEdit` readout. The catalog demo rework had broken all three.
383
+ - Documented and pinned how `tooltip()` surfaces a failed show from its delay
384
+ timer: the half-built tooltip is rolled back, the original error escapes the
385
+ timer for the host to report (a window `error` event in browsers, matching
386
+ kerf's other deferred callbacks), and the tooltip stays armed so the next
387
+ hover or focus tries again.
388
+ - Fixed the dialog helpers (`confirm` / `prompt` / `form` / `choice`) leaving
389
+ wiring behind when a step after the overlay opened failed. Their post-open
390
+ wiring (required-slot checks, the click-table `delegate()`, the Enter-key
391
+ listener) is now part of construction: a failure removes the listeners
392
+ already wired, closes the overlay (including a `native` `<dialog>`), restores
393
+ focus, and rethrows the original error, so no pending promise is left
394
+ attached to an on-screen dialog.
395
+ - Added `ticket:timing summary --all`, a read-only cross-ticket timing report
396
+ (median/p90 per phase, gate, and check step) that counts one push shared by
397
+ a batch of tickets once and detects and excludes fan-out backfill, such as
398
+ the first timed push that attached one check interval to 374 historical
399
+ tickets.
400
+ - Ticket timing now records per-step durations of the root `npm run check`
401
+ chain (printed at the end of every run), interrupted gate attempts, active
402
+ time through `ticket:timing claim`/`release` wrappers around `hotsheet-cli`,
403
+ and CI/publication intervals imported idempotently from GitHub Actions with
404
+ `ticket:timing import-ci`.
405
+ - The pre-push hook no longer reruns `npm run check` when a local run already
406
+ passed on the exact clean tree being pushed; the skip is recorded in ticket
407
+ timing, and any dirty worktree, different tree, Node.js change,
408
+ failed/interrupted rerun, or `KERF_FORCE_CHECK=1` runs the full gate.
409
+ - The pre-push hook no longer fans its timing record out to every ticket in a
410
+ push that names more than 25 of them (a first push of a long history, a
411
+ force-push). It prints a notice and records only the explicit
412
+ `KERF_TICKET_TIMING_TICKETS` list, or nothing, so it can no longer write the
413
+ backfill shape the cross-ticket summary has to exclude.
414
+ - The pre-push hook's "already verified" skip now also requires the same
415
+ platform, architecture, and installed dependencies as the passing run: a
416
+ fingerprint of every `package-lock.json` and `node_modules/.package-lock.json`
417
+ across the root, `ui/`, `eslint-plugin/`, `create-kerf-component/`, and
418
+ `site/`. A reinstall, a lockfile change, or a missing `node_modules` now runs
419
+ the full gate.
420
+ - `ticket:timing record` accepts `--run-id <n>`, and `import-ci` no longer
421
+ imports a GitHub run a second time when it was already recorded by hand:
422
+ an interval with that run id, or one with no run id for the same phase and
423
+ gate whose span overlaps the run's (within 60 seconds), counts as imported.
424
+ - `bindList()` now rolls back when its first render throws: rows that pass
425
+ already created are disposed (content mounts and element-mode `dispose`
426
+ callbacks) and removed before the original error is rethrown, instead of
427
+ leaking live row effects with no handle to release them.
428
+ - `bindList()` virtualization now validates its dimensions: heights (fixed,
429
+ estimated, callback-returned, and `setHeight` reports) must be finite and
430
+ non-negative, a fixed window-mode `rowHeight` must be positive, and
431
+ `overscan` / `minRows` must be non-negative integers. Invalid values throw a
432
+ descriptive error instead of producing blank windows or a corrupted scroll
433
+ anchor.
434
+ - `bindList()` now rejects a source that carries the `arraySignal` brand but
435
+ has no patch queue with a descriptive error at bind time, instead of failing
436
+ with a generic `TypeError` on its first render.
437
+ - Added `wa-dialog.hide-actions` support to `@kerfjs/ui/webawesome.css`, hiding
438
+ Web Awesome's directly exported `header-actions` shadow part for dialogs that
439
+ provide their own dismissal affordance.
440
+ - Fixed `mount()` leaving an element permanently "already mounted" when its
441
+ first render throws. The first render is now transactional: the original
442
+ error is rethrown, every binding effect and the dev listener observer
443
+ acquired so far are released, the element's pre-mount child nodes are
444
+ restored, and a retry on the same element succeeds.
445
+ - Fixed `overlay()` construction leaking a live wrapper, mount, dismissal
446
+ listeners, and concurrent-overlay stack entry when setup failed partway. A
447
+ malformed `initialFocus` selector now throws a descriptive `overlay()` error
448
+ before anything is touched, and a throwing first render or `native`
449
+ `showModal()` / `showPopover()` call rolls back everything already installed
450
+ (restoring focus) before rethrowing. `popover()` and `tooltip()` roll back the
451
+ same way when positioning fails; the dialog helpers inherit the guarantee.
452
+ - Added an opt-in `@kerfjs/ui/document.css` baseline for border-box sizing,
453
+ token-backed body and link presentation, and a `.kui-app-root` full-height
454
+ chain for application shells.
455
+ - Added an inline `Text` span variant for secondary copy inside rows, labels,
456
+ and table cells, preserving tone, size, and font roles without box geometry.
457
+ - Reworked every focused UX catalog demo to dogfood Kerf components, Web
458
+ Awesome surfaces, and semantic HTML without route-local stylesheets, inline
459
+ styles, styling-only classes, or shared icon/button render helpers. Added
460
+ catalog-owned specimen viewport constraints and a nonwaivable conformance
461
+ gate that prevents those styling shortcuts from returning.
462
+ - Fixed `ToolbarControlGroup` dropdown triggers that were sized to fixed 48px/66px boxes,
463
+ which clipped popup icons and let compact mixed-control carets overflow in
464
+ Safari while distorting longer catalog controls.
13
465
  - Added a `TabBar.end` action zone for layouts that need a tab-local adjacent
14
466
  `trailing` action and a separate far-edge action; the tab strip now owns the
15
467
  shrink/scroll geometry while all action regions remain visible.
package/README.md CHANGED
@@ -31,14 +31,17 @@ const app = document.getElementById('app')!;
31
31
  mount(app, () => (
32
32
  <div>
33
33
  <button data-action="inc">+</button>
34
- <span>{count.value}</span>
34
+ <span>{count}</span>
35
35
  </div>
36
36
  ));
37
37
 
38
38
  delegate(app, 'click', '[data-action="inc"]', () => count.value++);
39
39
  ```
40
40
 
41
- That's it. Your JSX renders to HTML strings, kerf's native diff applies the minimum DOM mutations to make the live tree match, and signals re-run the render only when something they read actually changed.
41
+ That's it. Your JSX renders to HTML strings, and handing the signal itself to
42
+ the text hole binds that one node directly. Structural reads still re-run the
43
+ render, where kerf's native diff applies the minimum DOM mutations needed to
44
+ make the live tree match.
42
45
 
43
46
  Here's the whole development loop — write a component, run the dev server, click around, edit, watch the browser pick it up:
44
47
 
@@ -81,7 +84,9 @@ Write plain `.tsx` and build with your existing esbuild / Vite / tsup — no ext
81
84
 
82
85
  4. **Focus, selection, and listeners survive re-renders — even mid-list.** The reconciler morphs instead of rebuilding, so caret position, IME composition, scroll, and delegated listeners survive every update; keyed rows are patched in place rather than recreated.
83
86
 
84
- 5. **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.
87
+ 5. **A first-party UI layer when you want one.** `@kerfjs/ui` adds accessible components, responsive application layouts, typed CSS values, and explicit wiring helpers without changing kerf's stateless component model. It is separately installable and tree-shakeable, so the core stays the core. ([tour →](#first-party-ui-separately-installable))
88
+
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.
85
90
 
86
91
  **Plus, nothing you don't ask for:** JSX typed against the HTML standard (not React's props) · a ~18-export API with no hooks, lifecycle, or per-instance state · **nine** 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.
87
92
 
@@ -97,7 +102,7 @@ Write plain `.tsx` and build with your existing esbuild / Vite / tsup — no ext
97
102
  ### When to reach for something else
98
103
 
99
104
  - Need a full ecosystem (router + forms + data + SSR streaming) → **Next.js / Remix / SolidStart**.
100
- - Building a deeply componentised design-system app → **React / Solid / Svelte**.
105
+ - Building a deeply componentized design-system app → **React / Solid / Svelte**.
101
106
  - Need React Native / cross-platform mobile → **React** (Kerf + Tauri/Electron also covers many of these cases).
102
107
  - Building a static site → **Astro** (we use it for _this_ project's site).
103
108
  - Already invested in a framework where switching cost outweighs the bundle size gain.
@@ -278,6 +283,38 @@ mount(app, () => <div><nav>{/* <a href> links, auto-intercepted */}</nav>{router
278
283
 
279
284
  Each subpath adds nothing to the main barrel until it's imported. See [`docs/8-api-reference.md`](./docs/8-api-reference.md) for the full list (`list`, `router`, `overlay`, `scope`, `async`, `timing`, `remount`, `attach`, `actions`).
280
285
 
286
+ ### First-party UI, separately installable
287
+
288
+ `@kerfjs/ui` is the optional component layer for applications that want a
289
+ coherent interface vocabulary without switching frameworks. Components return
290
+ Kerf `SafeHtml`; the application still owns state and wires behavior explicitly.
291
+ Component subpaths pull in only their reachable CSS, while responsive app
292
+ layouts, typed CSS-value builders, Web Awesome adapters, and the checked
293
+ component catalog stay opt-in.
294
+
295
+ ```bash
296
+ npm install kerfjs @kerfjs/ui
297
+ ```
298
+
299
+ ```tsx
300
+ import { mount } from "kerfjs";
301
+ import { ListItem } from "@kerfjs/ui/list-item";
302
+ import { Toolbar } from "@kerfjs/ui/toolbar";
303
+ import { ToolbarText } from "@kerfjs/ui/toolbar-text";
304
+
305
+ const root = document.getElementById("app")!;
306
+
307
+ mount(root, () => (
308
+ <>
309
+ <Toolbar label="Files" leading={<ToolbarText text="Workspace" />} />
310
+ <ListItem action="open-file" label="src/main.ts" selected />
311
+ </>
312
+ ));
313
+ ```
314
+
315
+ Start with the [component selection matrix](./ui/docs/component-selection.md),
316
+ or read the complete [`@kerfjs/ui` package guide](./ui/README.md).
317
+
281
318
  ## Optional tooling
282
319
 
283
320
  Install and JSX setup are in [Quick start](#quick-start) above. These companion packages are opt-in.
@@ -314,6 +351,7 @@ See [`docs/13-component-packages.md`](./docs/13-component-packages.md) for the f
314
351
  - **Docs:** [`docs/`](./docs/) — overview · reactivity · stores · render · events · jsx · svg · [API reference](./docs/8-api-reference.md)
315
352
  - **Migrating:** [coming from another framework?](https://brianwestphal.github.io/kerf/migrating/) — side-by-side TodoMVC translations + per-framework gotchas
316
353
  - **AI guide:** [`docs/ai/usage-guide.md`](./docs/ai/usage-guide.md) — reference for AI tools fetching kerf docs (linked from `llms.txt`)
354
+ - **UI package:** [`@kerfjs/ui`](./ui/README.md) — accessible components, responsive app layouts, typed CSS values, Web Awesome adapters, and checked AI-facing component metadata
317
355
  - **ESLint plugin:** [brianwestphal.github.io/kerf/docs/eslint-plugin/](https://brianwestphal.github.io/kerf/docs/eslint-plugin/) — `eslint-plugin-kerfjs`; eight rules (four hard-rule errors + four warns: `require-delegate-disposer`, `prefer-attr-selector`, `no-raw-with-dynamic-arg`, `ai-assistant-configs`) at edit time (source: [`eslint-plugin/`](./eslint-plugin/))
318
356
  - **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/))
319
357
  - **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)
package/ai/manifest.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "kerfjsVersion": "5.0.0-beta.50",
2
+ "kerfjsVersion": "5.0.0-beta.51",
3
3
  "files": [
4
4
  {
5
5
  "name": "skill",