kerfjs 5.0.0-beta.50 → 5.0.0-beta.52

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