kerfjs 5.0.0-beta.8 → 5.0.0-beta.80

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.
Files changed (83) hide show
  1. package/CHANGELOG.md +2577 -117
  2. package/README.md +96 -37
  3. package/ai/cursorrules +110 -61
  4. package/ai/manifest.json +16 -7
  5. package/ai/skill.md +120 -75
  6. package/dist/actions.d.ts +1 -1
  7. package/dist/actions.js +10 -6
  8. package/dist/actions.js.map +1 -1
  9. package/dist/array-signal.js +1 -1
  10. package/dist/async.js +22 -19
  11. package/dist/async.js.map +1 -1
  12. package/dist/attach.js +1 -66
  13. package/dist/attach.js.map +1 -1
  14. package/dist/{chunk-HW7KSM2Y.js → chunk-3C6F6NM4.js} +1 -1
  15. package/dist/{chunk-HW7KSM2Y.js.map → chunk-3C6F6NM4.js.map} +1 -1
  16. package/dist/{chunk-GY4XV2UV.js → chunk-4TX6YJDM.js} +10 -3
  17. package/dist/chunk-4TX6YJDM.js.map +1 -0
  18. package/dist/{chunk-QIP723L4.js → chunk-5WRGJZV6.js} +3 -2
  19. package/dist/chunk-5WRGJZV6.js.map +1 -0
  20. package/dist/{chunk-KZJXHFIB.js → chunk-BI4HGICV.js} +3 -3
  21. package/dist/chunk-BI4HGICV.js.map +1 -0
  22. package/dist/chunk-FM6N2B4N.js +70 -0
  23. package/dist/chunk-FM6N2B4N.js.map +1 -0
  24. package/dist/{chunk-LVH3GC6B.js → chunk-H7WTRM6F.js} +3 -3
  25. package/dist/chunk-H7WTRM6F.js.map +1 -0
  26. package/dist/{chunk-U6FK33SG.js → chunk-IHMDBBUJ.js} +2 -2
  27. package/dist/chunk-IHMDBBUJ.js.map +1 -0
  28. package/dist/chunk-JN7EJM3B.js +86 -0
  29. package/dist/chunk-JN7EJM3B.js.map +1 -0
  30. package/dist/{chunk-UZJ6I4T6.js → chunk-MK42GLPV.js} +1 -1
  31. package/dist/chunk-MK42GLPV.js.map +1 -0
  32. package/dist/{chunk-SVATPF5R.js → chunk-TLL3KXT6.js} +249 -160
  33. package/dist/chunk-TLL3KXT6.js.map +1 -0
  34. package/dist/{chunk-ZDCJZCNO.js → chunk-UGNPDLNI.js} +78 -45
  35. package/dist/chunk-UGNPDLNI.js.map +1 -0
  36. package/dist/{chunk-V46JKE44.js → chunk-WKIPLNVO.js} +1 -1
  37. package/dist/chunk-WKIPLNVO.js.map +1 -0
  38. package/dist/{delegate-CL9VTZFb.d.ts → delegate--CrfgYtC.d.ts} +0 -34
  39. package/dist/dev.d.ts +31 -13
  40. package/dist/dev.js +101 -35
  41. package/dist/dev.js.map +1 -1
  42. package/dist/html.js +5 -4
  43. package/dist/html.js.map +1 -1
  44. package/dist/index.d.ts +14 -9
  45. package/dist/index.js +29 -20
  46. package/dist/index.js.map +1 -1
  47. package/dist/jsx-runtime.d.ts +13 -5
  48. package/dist/jsx-runtime.js +1 -1
  49. package/dist/list.d.ts +7 -3
  50. package/dist/list.js +126 -28
  51. package/dist/list.js.map +1 -1
  52. package/dist/overlay.d.ts +128 -129
  53. package/dist/overlay.js +675 -366
  54. package/dist/overlay.js.map +1 -1
  55. package/dist/remount.js +2 -2
  56. package/dist/router.d.ts +13 -4
  57. package/dist/router.js +37 -10
  58. package/dist/router.js.map +1 -1
  59. package/dist/scope.d.ts +35 -8
  60. package/dist/scope.js +61 -30
  61. package/dist/scope.js.map +1 -1
  62. package/dist/testing.js +1 -1
  63. package/dist/timing.d.ts +3 -2
  64. package/dist/timing.js +1 -1
  65. package/dist/timing.js.map +1 -1
  66. package/llms.txt +12 -5
  67. package/package.json +33 -8
  68. package/setup/cli.mjs +90 -0
  69. package/setup/index.d.mts +37 -0
  70. package/setup/index.mjs +1347 -0
  71. package/setup/jsonc.mjs +201 -0
  72. package/setup/state.schema.json +56 -0
  73. package/dist/chunk-GY4XV2UV.js.map +0 -1
  74. package/dist/chunk-KPXIOG2C.js +0 -54
  75. package/dist/chunk-KPXIOG2C.js.map +0 -1
  76. package/dist/chunk-KZJXHFIB.js.map +0 -1
  77. package/dist/chunk-LVH3GC6B.js.map +0 -1
  78. package/dist/chunk-QIP723L4.js.map +0 -1
  79. package/dist/chunk-SVATPF5R.js.map +0 -1
  80. package/dist/chunk-U6FK33SG.js.map +0 -1
  81. package/dist/chunk-UZJ6I4T6.js.map +0 -1
  82. package/dist/chunk-V46JKE44.js.map +0 -1
  83. package/dist/chunk-ZDCJZCNO.js.map +0 -1
package/CHANGELOG.md CHANGED
@@ -6,8 +6,2547 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ### Removed
10
+
11
+ - **Breaking (`@kerfjs/ui`, 5.0 beta):** remove the deprecated `TabScaffold` component and type aliases, `wireTabScaffold` and `WireTabScaffoldOptions`, and the `tab-scaffold`, `wire-tab-scaffold`, and `tab-scaffold.css` package subpaths. Use `TabNavigator`, `TabNavigatorTab`, `TabNavigatorProps`, `wireTabNavigator`, and their corresponding subpaths. The rendered DOM classes and data attributes remain stable. See `ui/docs/tab-navigator.md` for migration steps (`KF-CMK0W8`).
12
+
13
+ ### Added
14
+
15
+ - `ListHeader` accepts `badge` and `status` together, including explicit `undefined` for either when app state changes; a semantic `count` remains separate.
16
+
17
+ - `Pane`, `SunkenPanel`, plain `TabNavigator` scenes, `Workbench` regions, and static `ContentItem` surfaces can opt into keyboard or programmatic focus; `outlined` holds the standard focus ring for application states such as drop targets. Inactive scenes and collapsed panels suppress their focus stop and outline.
18
+
19
+ - `TabNavigator`, `TabNavigatorTab`, `TabNavigatorProps`, and `wireTabNavigator` name the complete persistent bottom-tab layout. The matching module, wiring, and CSS subpaths are available; former `TabScaffold` names and paths were deprecated aliases in earlier 5.0 betas.
20
+
21
+ - `Pane` exposes its resolved logical inline content insets to child CSS, so a table can paint edge to edge while its cell text stays aligned, with or without `deepInset`.
22
+
23
+ - `Pane` and plain `TabNavigator` scenes accept `deepInset` for a 16px outer content gutter while nested Row, List, and Grid spacing remains at its normal 8px.
24
+
25
+ - The TabNavigator catalog now demonstrates a NavStack with a view-owned top toolbar and Pane above the persistent tab bar, including push, tab switch, and return.
26
+
27
+ - UI Doctor now checks component root class names, stylesheet names, and CSS selectors that reach into another local component.
28
+
29
+ - Configure modal `DialogSurface` viewport gutters and typed maximum heights, including a dynamic viewport cap, without application shadow-part overrides.
30
+
31
+ ### Changed
32
+
33
+ - Rename the internal TabNavigator implementation folder, files, and focused test files from `tab-scaffold` to `tab-navigator`; public imports and rendered DOM hooks stay stable.
34
+
35
+ - `deepInset` now adds the same 8px to the top and bottom of Pane and plain TabNavigator scroll content, including scroll padding; Pane header and footer still own their safe-area edges.
36
+
37
+ - Toolbar composition guidance and catalog metadata now allow direct `KerfUiContent` in every zone; `ToolbarControlGroup` remains the recommended shared chrome for related controls.
38
+
39
+ - The UI composition lint rule checks component parent requirements for direct content passed through a named zone prop.
40
+
41
+ - Toolbar zones center direct content by default while keeping ToolbarText, control groups, and standalone primary actions on their established control-band axis.
42
+
43
+ ### Fixed
44
+
45
+ - The actionable ToolbarText catalog example uses the standard TokenSearchField toolbar presentation for its search control, and an empty TokenSearchField shows its placeholder.
46
+
47
+ - Preserve toolbar visibility and measurement state across app renders to prevent geometry-driven render loops and repeated observer/layout work (`KF-58HBRR`).
48
+
49
+ - Restore accessible native dialog names through `@kerfjs/ui/surface-scaffold/register`, preserving live title slots and headerless labels.
50
+
51
+ - Validate remote CI freshness through newer-run searches and exact-run details before warning about a failed main build (`KF-X4PTHF`).
52
+
53
+ - Select menus now shift their complete visible surface inside narrow viewport edges while preserving `fitMenu` trigger width (`KF-H4JBQP`).
54
+
55
+ - Toolbar visibility probes stop rewriting unchanged styles, allowing catalog geometry overlays and responsive controls to settle after updates (`KF-JVWC92`).
56
+
57
+ - Busy toolbar groups preserve their relocation annotation, so the inert action and live status remain available when a panel closes.
58
+
59
+ - Navigation stacks now visibly slide pushed views over the previous screen and cross-fade both header and footer across repeated or interrupted push/pop transitions.
60
+
61
+ ### Added
62
+
63
+ - Add typed toolbar item `hideBelow`/`showBelow` lengths and `wireToolbarVisibility`, including complementary container-width overflow and dynamic font/token updates (`KF-MXE9YV`).
64
+
65
+ - Expanded grouped search supports `expandedOverflow="visible"` for application-owned surfaces below its positioning anchor.
66
+
67
+ - `@kerfjs/ui` ToolbarControlGroup adds `tileTone` quiet semantic palettes for non-interactive heading icon tiles.
68
+
69
+ - ToolbarText supports `tone="dark"` for filenames, headings, and actionable titles on loud toolbar surfaces.
70
+ - `@kerfjs/ui` Text accepts `controlMargins` for selected exterior control gutters, avoiding a Row wrapper around isolated text.
71
+
72
+ - Make lowered surfaces and decorative neutral fills/borders translucent, and replace the SunkenPanel prototype with direct component compositions.
73
+
74
+ - **UI components share `SemanticTone`.** Badge, Chip, StateBanner, and ContentItem
75
+ use `neutral`, `info`, `pop`, `success`, `warning`, and `danger`. Replace
76
+ Badge/Chip `tone="brand"` with `tone="info"`; the existing brand color tokens
77
+ still supply its palette. `BadgeTone` and `StateBannerTone` are deprecated aliases.
78
+
79
+ - **Catalog sidebars filter by item or heading name.** The shared catalog
80
+ shell now searches primary and ecosystem sections as you type, including
81
+ collapsed groups, and keeps the query when the active preview changes.
82
+
83
+ - **ListHeader has five label sizes.** `size="xs|sm|m|l|xl"` controls visual
84
+ scale separately from the semantic heading level; `sm` preserves the existing
85
+ uppercase treatment and `FieldLabel` remains unchanged.
86
+
87
+ - **The pinned TabBar catalog example scrolls with native momentum.** It no
88
+ longer reserves a long blank strip after the final tab; applications can
89
+ still opt into whole-tab settling with `snapTabs`.
90
+
91
+ - **Doctor reports foreign CSS restyles of co-owned classes.** A stylesheet
92
+ outside every co-owner now receives one `KUI-L019` per subject rule, including
93
+ inferred BEM modifiers, with the co-owner set in the diagnostic evidence.
94
+
95
+ - **Doctor respects explicit ownership groups with implicit ownership enabled.**
96
+ A group's stylesheet remains owned by the group even when one of its source
97
+ modules, or an unrelated module, imports that CSS directly.
98
+
99
+ - **LucideIcon supports filled glyphs.** Pass `appearance="solid"` to use the
100
+ icon's current text color as its fill; outline remains the default.
101
+
102
+ - **Doctor detects borrowed component markup.** Source modules that place a
103
+ foreign public class in JSX, raw HTML, or DOM class writes now receive
104
+ `KUI-L023`; documented placeable classes remain allowed.
105
+
106
+ - **Doctor offers strict same-package selector context checks.**
107
+ `ownershipContext: "any"` reports a sibling component's class even when it is
108
+ only used as selector context; cross-package composition stays available.
109
+
110
+ - **Doctor catches classless descendants of composed components.** Component
111
+ mode uses JSX evidence to identify element selectors below component hooks,
112
+ sibling roots, and direct composed child roots.
113
+
114
+ - **Doctor component ownership recognizes source evidence.** Selection-only
115
+ components and literal BEM classes rendered by a component are protected
116
+ when its stylesheet styles their block. CSS imports can supply the owned
117
+ stylesheet, and uncataloged modules can opt into the same inference.
118
+
119
+ - **`createScope().dispose` is typed as a bound function property.** Consumers
120
+ can store or return it as a teardown without a wrapper or an unbound-method
121
+ lint suppression.
122
+
123
+ - **ToolbarControlGroup grow sizing keeps its basis in wide toolbars.** An
124
+ expanded grow search now contributes its minimum width to the trailing zone's
125
+ intrinsic size, keeping sibling groups beside it instead of wrapping early.
126
+
127
+ - **TabBar can settle overflowing peers at whole-tab starts.** Opt into
128
+ `snapTabs` beside a pinned leading AppTab; selection and scroll gestures
129
+ align peers after the pin in LTR and RTL, with enough end room for the final
130
+ tab instead of leaving a clipped sliver at rest.
131
+
132
+ - **The pre-push check no longer adds timing metadata to Hot Sheet tickets.**
133
+ It still runs the repository gate or skips an identical verified clean tree;
134
+ historical timing notes remain readable through `ticket:timing summary`.
135
+
136
+ - **Wrapping catalog resource groups align with the toolbar control band.** A
137
+ single row centers in the band, while multiple rows start at its top.
138
+
139
+ - **UI browser checks cover the current catalog specimens.** Select, toolbar,
140
+ tab, and control group browser assertions now address the expanded demos;
141
+ a focused toolbar search field restores its default surface color when no
142
+ background override is set.
143
+
144
+ - **Pinned tabs cover the full leading edge of their scroll strip.** The
145
+ backing now includes the strip border as well as its padding, preventing a
146
+ sliver of a scrolling tab label from showing beside the pinned tab.
147
+
148
+ - **Catalog resource links remain readable at phone widths.** The resource
149
+ group wraps whole actions onto another row instead of clipping the Guidance
150
+ label at the right edge; `ToolbarControlGroup overflow="wrap"` is available
151
+ for the same independent-link pattern.
152
+
153
+ - **The UI analyzer resolves application component imports through TypeScript
154
+ path aliases.** `KUI-L022` now catches a styled hook class when an imported
155
+ cataloged component uses an exact or wildcard `paths` mapping; Doctor uses
156
+ the same resolution.
157
+
158
+ - **TabBar accepts standalone Web Awesome actions in `trailing` and `end`.**
159
+ A single `wa-button` can sit beside the scrolling tabs or at the far edge
160
+ without ToolbarControlGroup chrome; the composition rule recognizes the
161
+ cataloged custom element in these zones.
162
+
163
+ - **FieldLabel matches form-control labels over field previews.** The
164
+ `@kerfjs/ui/text` export gives a read-only field the same label typography
165
+ and inset as a themed Web Awesome input, with an id for accessible grouping.
166
+
167
+ - **TokenSearchField form fields match adjacent Web Awesome inputs.** The
168
+ control uses the form-control radius, and its required marker uses the
169
+ same neutral color and offset tokens as `wa-input`.
170
+
171
+ - **Grid supports labeled multi-select ContentItem tiles.** Opt into
172
+ `selectionMode="multiple"` with `ariaLabel`; `wireContentItems` follows the
173
+ rendered columns with four arrow keys and keeps nested controls independent.
174
+
175
+ - **Collapsible TokenSearchField can fill a stretched Toolbar center.** The
176
+ `fill` prop expands the field and its search group to the available width
177
+ while preserving the compact collapsed action.
178
+
179
+ - **TokenSearchField has a typed trailing icon action.** `trailingAction`
180
+ renders an accessible button with Kerf owned chrome and a full 24px wide hit
181
+ target; the existing `trailing` content slot remains for passive content.
182
+
183
+ - **PopupMenu and registered PopupSurface dropdowns preserve fast keyboard
184
+ navigation during opening.** Item focus gained while Web Awesome animates
185
+ the menu is restored after its initial focus handoff, so Enter acts on the
186
+ item the user reached.
187
+
188
+ - **PopupMenu submenu checkmarks stay inside the menu surface.** Checked
189
+ submenu rows align their checkmark beside the label instead of letting the
190
+ Web Awesome negative check margin paint beyond the submenu border.
191
+
192
+ - **Filled SegmentedControl labels keep contrast while switching themes.**
193
+ Web Awesome's native button color transition no longer interpolates the
194
+ label against the already-switched track fill.
195
+
196
+ - **Workbench numeric overlays no longer restyle the full work area on unrelated changes.**
197
+ The overlay wiring mirrors active presentation and open state onto the Workbench
198
+ root for CSS, and avoids layout measurements when no wired panel uses a numeric
199
+ breakpoint.
200
+
201
+ ## [5.0.0] - 2026-09-29
202
+
203
+ - **`ui-public-boundaries` inspects `kerfjs/html` tagged templates.** The
204
+ no-build `html` template writes the same markup as JSX, so its static
205
+ `class` attributes now carry `KUI-L101` (private `kui-*` anatomy) and
206
+ `KUI-L103` (a component's anatomy on your own element):
207
+ ``html`<aside class="kui-pane">` `` is reported with `Pane` to render, and a
208
+ plain ``html`<div class="kui-content-item">` `` with `ContentItem`, while a
209
+ `<ul>` carrier keeps the placeable class. The template's static parts are
210
+ read by the runtime's own rules; a class a hole supplies (`class=${cls}`)
211
+ is unknown and skipped.
212
+
213
+ - **`KUI-L103`'s "render X instead" answer has one implementation.**
214
+ `@kerfjs/ui` ships `ai/component-class-owners.cjs`, the rule that decides
215
+ which cataloged classes are a component's anatomy and which export renders
216
+ each. `eslint-plugin-kerfjs`'s `ui-public-boundaries` loads it beside
217
+ `application-ui-profile-sync.cjs`, and the generated `ai/components/*.md`
218
+ pages use it too, so the lint message and the component reference can no
219
+ longer name different components (a test pins their agreement over the whole
220
+ shipped catalog). A vendored `profileContractPath` directory must now also
221
+ hold `component-class-owners.cjs`; an `@kerfjs/ui` without it reports
222
+ `KUI-L090`.
223
+
224
+ - **A delegated click no longer re-fires on a target the re-render recycled.**
225
+ A delegated handler that writes a signal makes `mount()` re-render
226
+ synchronously, inside the same event dispatch; when the morph recycled the
227
+ clicked element in place into a control with a different `data-action`,
228
+ delegates registered later for the same event matched the recycled element
229
+ and fired too (a `request` click immediately ran `cancel` and undid itself).
230
+ `delegate()`, `delegateCapture()`, and `kerfjs/actions`' `delegateActions()`
231
+ now resolve every delegate's match against the DOM as it stood when the
232
+ event reached the first kerf delegate listener, like per-element listeners:
233
+ a delegate that matched at dispatch start still fires (unless an earlier
234
+ handler removed its element), one that matches only after a re-render does
235
+ not, and `delegateActions()` dispatches by the action value the element
236
+ carried at dispatch start. `mount()` stays synchronous. The
237
+ wrap-it-in-a-distinct-element workaround is no longer needed. Cost: about
238
+ 0.11 KB min+gzip, which moves the advertised core size from ~12 KB to
239
+ ~13 KB.
240
+
241
+ - **Component packages declare `placeableClasses`, and `KUI-L103` covers
242
+ their anatomy classes.** `create-kerf-component`'s `kerf.components.json`
243
+ accepts optional `boundaries.placeableClasses` (a subset of
244
+ `publicClasses`) and `boundaries.rootElement`, validates them, and copies
245
+ them into the generated `component-composition.json`.
246
+ `eslint-plugin-kerfjs`'s `ui-public-boundaries` now checks every class the
247
+ application or third-party catalogs in `.kerf-ui-profile.json` declare, not
248
+ only `kui-*` names: `<div class="acme-meter">` is reported with the
249
+ package's component to render (`Meter`), while its placeable classes stay
250
+ allowed. The scaffold template's README explains the field, and
251
+ `@kerfjs/ui/ai/skill.md` (1.50.0) applies the rule to package entries.
252
+
253
+ - **`eslint-plugin-kerfjs` `ui-public-boundaries` reports a plain
254
+ `<div class="kui-content-item">` as `ContentItem` recreated (`KUI-L103`).**
255
+ The `kui-content-item` classes stay placeable on another carrier (a `<ul>`, a
256
+ `Text`, a control-cluster `<footer>`), but a plain `<div>` carrying them is
257
+ exactly what `ContentItem` renders; the diagnostic now says to render
258
+ `ContentItem`. The `@kerfjs/ui` catalogs gain an additive `rootElement`
259
+ field (selection entry and composition `boundaries.rootElement`) naming the
260
+ element a component renders around its own placeable classes, and
261
+ `check:guidance` derives its repository check from the same fields, so the
262
+ package check and the downstream diagnostic cannot drift. The rule also
263
+ inspects the static class names of a template-literal `class` /
264
+ `className` value.
265
+
266
+ - **`eslint-plugin-kerfjs` `ui-public-boundaries` reports a component's
267
+ anatomy class written onto an application-owned element (`KUI-L103`).**
268
+ `<div class="kui-toolbar">`, `<aside class="kui-pane">`, or
269
+ `<wa-dropdown class="kui-popup-menu">` recreates a component by class and
270
+ inherits (and invites restyling of) its CSS; the diagnostic names the
271
+ component to render instead (`Toolbar`, `Pane`, `PopupMenu`). The
272
+ `@kerfjs/ui` catalogs gain `placeableClasses` (selection entry field and
273
+ composition `boundaries.placeableClasses`) naming the public classes an
274
+ application may place itself — the layout utilities, `kui-app-root`, the
275
+ `kui-content-item` geometry on a non-div carrier, and Web Awesome
276
+ `hide-actions` — and `check:catalog` requires every class-only entry to list
277
+ its classes there. A component's own element (`<Toolbar className="kui-toolbar">`)
278
+ is not reported. The README pane example and `@kerfjs/ui/ai/skill.md`
279
+ (1.47.0) now render `Pane` instead of writing its classes, and each generated
280
+ `ai/components/*.md` page states which classes are placeable and which
281
+ component renders the rest.
282
+
283
+ - **`kerf-ui-analyze` retires `KUI-L005` ("forced component dimension").**
284
+ `KUI-L019` already reports any application rule whose subject is a cataloged
285
+ component, dimensions included, so `.kui-pane { width: 300px }` was reported
286
+ twice (an error and a review finding). L005's only other shape, a public
287
+ class as an ancestor (`.kui-pane .app-sidebar { width: 240px }`), sizes the
288
+ application's own element and is allowed. The analyzer no longer emits it;
289
+ the id stays in the diagnostic registry (now listing it under `retired`) so
290
+ existing profile exceptions that name it still load.
291
+
292
+ - **`KUI-L301` now reports every `Discouraged` Web Awesome element rendered
293
+ directly.** The `@kerfjs/ui` package-default application profile avoids all
294
+ fifteen Discouraged entries (Dropdown/Dropdown Item → `PopupMenu`,
295
+ Select/Option → `Select`, Tab Group/Tab/Tab Panel → `TabBar`, Icon →
296
+ `LucideIcon`, Split Panel → `ResizableRegion`, Button Group →
297
+ `SegmentedControl`, Tree/Tree Item → `List`, and Animated Image, Comparison,
298
+ and Zoomable Frame → `ContentItem`), each with a rationale, and
299
+ `eslint-plugin-kerfjs` `ui-preferences` (and `kerf-ui-doctor`, which runs it)
300
+ now resolves a custom-element JSX tag such as `<wa-dropdown>` to its catalog
301
+ entry, not only an imported component. A declared component's own source
302
+ stays exempt because it is the wrapper that owns the element. A test keeps
303
+ the profile's avoid set equal to the catalog's Discouraged set in both
304
+ directions and lints the UX demo, recipes, and catalog shell clean;
305
+ `@kerfjs/ui/ai/skill.md` (1.46.0) names the diagnostic.
306
+
307
+ - **New (`@kerfjs/ui`): `NavStack` and `TabScaffold` take `chromeDividers`
308
+ (`'scroll' | 'always' | 'none'`), like `Pane`.** `scroll` (the default) keeps
309
+ today's behavior: the stack's top-chrome and bottom-toolbar lines and the
310
+ scaffold's bar line follow the scroll once `wireScrollDividers` is installed.
311
+ `always` draws them permanently without the wiring; `none` never draws them,
312
+ even when wired. The top `Toolbar`'s own `toolbarConfig.dividerSides` is
313
+ unaffected. `SplitView` forwards it to its compact stack through
314
+ `compactStack.chromeDividers`.
315
+
316
+ - **Fix (`@kerfjs/ui`): a `Pane` that is the only content of a `NavStack` view
317
+ or a `TabScaffold` scene fills it.** It used to size to its content, so the
318
+ view scrolled the whole Pane and its header scrolled away. Now the Pane's own
319
+ content scrolls under its pinned header and footer, the Pane's slots take the
320
+ safe-area edges the view or scene still reaches, and scroll dividers key on
321
+ the Pane's content. The rule lives in `pane.css`, as the Workbench-region fill
322
+ does.
323
+
324
+ - **The `@kerfjs/ui` suite-v3 AI-regression protocol test runs in every gate
325
+ again.** `ui/tests/integration/ai-regressions-v3.test.ts` was selected by no
326
+ package script, so neither `npm --prefix ui run check` nor CI executed it. It
327
+ now runs as `test:ai-regressions` inside the package's `npm test`, and a new
328
+ unit guard fails when any file under `ui/tests` is selected by no script
329
+ reachable from `check` or `test:e2e`.
330
+
331
+ - **Importing `@kerfjs/ui/catalog` or `@kerfjs/ui/workbench` now ships every
332
+ stylesheet those composites render.** `catalog`, `workbench`, `nav-stack`,
333
+ `split-view`, `tab-scaffold`, and `collapsible-panel` gain a CSS-aware
334
+ `browser` export condition whose generated wrapper imports the foundation, the
335
+ CSS of each internally rendered component (Pane, Toolbar, ToolbarText, List,
336
+ ListItem, ListInsetText, Row, PopupMenu, …) in dependency order, and the
337
+ composite's own CSS. Apps no longer hand-import `pane.css`, and a Catalog
338
+ sidebar's styling no longer depends on whether the app happens to import
339
+ `ListInsetText` elsewhere. The list is derived from the source import graph
340
+ (now following parent-relative imports), the `import` condition stays
341
+ CSS-free, and the companion `.css` subpaths remain for pipelines without the
342
+ `browser` condition. `@kerfjs/ui/ai/skill.md` is 1.46.0.
343
+
344
+ - **The `@kerfjs/ui` AI guidance now states every maintainer design rule
345
+ explicitly.** An audit of the package's tickets folded the missing rules into
346
+ `@kerfjs/ui/ai/skill.md` (1.45.0), `llms.txt`, and the design docs: prefer a
347
+ Kerf UI component over a Web Awesome component over a raw tag and never render
348
+ a `Discouraged` Web Awesome element directly (only through the Kerf component
349
+ that wraps it); nothing wraps inside a `ToolbarControlGroup` and a
350
+ `ToolbarText` is one ellipsized line unless `wrap`/`maxLines` is set; a
351
+ `Select` or `PopupMenu` trigger drops its focus ring while its popup is open
352
+ and the ring follows the control's shape (never add ring CSS); dividers earn
353
+ their place (`dividerSides=""` over self-spacing content or between a pane's
354
+ header and its list, never padded around); a control sits near where its
355
+ effect appears; and text aligns to text and icons to icons — a border is
356
+ never the alignment anchor. The ToolbarControlGroup catalog entry no longer
357
+ points at a hand-written `wa-dropdown` trigger, and the bundled kerf app
358
+ configs (`ai/skill.md` 1.25.0, `ai/cursorrules` 1.23.0) name the List family
359
+ and carry the "components own their styles; configure, never override" rule.
360
+
361
+ - **Applications and component packages are held to the component-ownership
362
+ rules `@kerfjs/ui` enforces internally: components own their styles and are
363
+ configured, never overridden.** `kerf-ui-analyze` and `kerf-ui-doctor` add
364
+ four error diagnostics (review findings under `--adoption`):
365
+ - `KUI-L019` — a CSS rule whose subject is another package's cataloged
366
+ component: a public class (`.kui-toolbar`, `.acme-meter`), its
367
+ `[data-component]` root, or a Web Awesome tag the package themes
368
+ (`wa-button`). Your own element in a component's context
369
+ (`.kui-toolbar > .my-widget`), keying on its state inside `:has()`, and
370
+ setting its public tokens stay allowed.
371
+ - `KUI-L020` — reading or writing another component's private `--_*`
372
+ variable (`--_kui-list-gap`).
373
+ - `KUI-L021` — overriding a token a typed prop sets (`--kui-list-gap` is
374
+ `<List gap>`); the message quotes the prop's example.
375
+ - `KUI-L022` — a class placed on a component's root (`<Toolbar
376
+ className="header" />`) that the application then styles.
377
+
378
+ Each message names the component's configuration (typed props, public
379
+ tokens, variants) and ends by routing a need with no prop to a component-gap
380
+ report instead of an override. Ownership is per package, from the nearest
381
+ `package.json`, so a component package styling its own components — including
382
+ in a Kerf UI parent's context — passes, and third-party catalogs declared in
383
+ `.kerf-ui-profile.json` get the same protection. `eslint-plugin-kerfjs` adds
384
+ `ui-component-ownership` (in `recommended-ui` and `strict-ui`, `error`),
385
+ reporting `KUI-L020` / `KUI-L021` in style strings, style objects, and
386
+ `style.setProperty()`. The `create-kerf-component` scaffold now ships a
387
+ `counter.css` that styles only Counter (configured by a typed `size` prop and
388
+ a `--kerf-counter-gap` token, with context named after Counter and a
389
+ Toolbar-context rule in its own stylesheet), plus `npm run check:styles`
390
+ (`kerf-ui-analyze`, from a new `@kerfjs/ui` dev dependency) in
391
+ `prepublishOnly`.
392
+
393
+ - **Behavior change (`@kerfjs/ui`): `NavStack` and `TabScaffold` chrome
394
+ dividers follow the scroll.** The `NavStack` bottom toolbar's and the
395
+ `TabScaffold` bar's top borders are no longer permanent. With `wireScrollDividers`
396
+ installed, a `NavStack`'s top chrome draws a line along its bottom edge only
397
+ while the active view's content is scrolled beneath it, its bottom toolbar a
398
+ line along its top edge only while more content lies below, and a
399
+ `TabScaffold` bar a line along its top edge only while the active scene's
400
+ content continues below it — none when the content fits. The line follows
401
+ the active view across a push or pop and the active scene across a tab
402
+ switch, and keys on whichever element actually scrolls: the view or scene
403
+ itself, or, through a sole child with no chrome of its own on that edge, a
404
+ `Pane`'s content or a nested `NavStack`'s active view (a sole `Pane`'s own
405
+ header or footer draws that boundary instead). Each layout draws the line
406
+ from its own stylesheet (an overlaid pseudo-element on the stack's chrome; the
407
+ bottom toolbar and bar keep their 1px top border, transparent until shown),
408
+ so nothing moves; the wiring
409
+ writes only its `data-scroll-overflow` / `data-scroll-divider` state. Without
410
+ the wiring those edges draw no line.
411
+
412
+ - **Behavior change (`@kerfjs/ui`): toolbars no longer draw a divider by
413
+ default; dividers between pinned chrome and scrolling content follow the
414
+ scroll.** `Toolbar`'s `dividerSides` now defaults to `''` (was `'b'`), and
415
+ the `Workbench` (`mainToolbar`, `mainBottomToolbar`, panel toolbars, and the
416
+ `mainHeader` / `mainFooter` lists), `CollapsiblePanel`, and `Catalog`
417
+ toolbars no longer place dividers of their own. Instead a `Pane` draws one
418
+ line under its header and one over its footer, wherever that chrome ends:
419
+ the header's only while content is scrolled beneath it, the footer's only
420
+ while more content lies below, and neither when the content fits. The new
421
+ `@kerfjs/ui/wire-scroll-dividers` subpath's `wireScrollDividers(root, {
422
+ targets? })` reports that state — call it once at the app root and keep its
423
+ disposer (`wireCatalog` installs it for its own root). It also marks each
424
+ side of a `TabBar` strip with tabs scrolled out of view, and lets a
425
+ `Toolbar` or `List` named as chrome in `targets` draw the divider facing an
426
+ app-owned scroller. The wiring only writes the `data-scroll-overflow` /
427
+ `data-scroll-divider` state attributes; each component draws its own line,
428
+ and no state moves the chrome, the content, or a tab. `Pane` gains
429
+ `chromeDividers` (`scroll` default, `always`, `none`; also in `PaneConfig`,
430
+ so `mainPane` and a panel's `pane` forward it) and TabBar the
431
+ `--kui-tab-bar-divider-color` token. To keep a permanent line, set
432
+ `chromeDividers: "always"` on the pane or an explicit `dividerSides`.
433
+
434
+ - **`@kerfjs/ui`: `Pane`, `NavStack`, `SplitView`, and `TabScaffold` fill a
435
+ Workbench region from their own stylesheets.** As a Workbench main area's
436
+ or panel's only child, each sets its own `height: 100%` in that context,
437
+ which the Workbench marks on its regions (`data-workbench-main`, and the new
438
+ `data-workbench-panel-content` on each panel's content); `workbench.css` no
439
+ longer sizes another component's root (it still fills a nested Workbench).
440
+ Rendering is unchanged.
441
+
442
+ - **`@kerfjs/ui`: layouts name the covered-floating-controls context after
443
+ themselves, and no longer write `FloatingToolbar`'s inset token.** Workbench,
444
+ CollapsiblePanel, and ResizableRegion each provide their own private
445
+ `--_kui-<layout>-covered` context in place of the shared
446
+ `--_kui-floating-covered` flag; `FloatingToolbar` and every restore corner
447
+ read all three. A restore corner no longer sets
448
+ `--kui-floating-toolbar-inset: 0px`; `FloatingToolbar` zeroes its own inset
449
+ inside a restore corner (a toolbar's own `inset` still wins). Rendering is
450
+ unchanged. One edge case changes: a floating control inside an open overlay
451
+ of one layout type that is itself covered by another layout type's open
452
+ overlay (for example, a ResizableRegion overlay in a Workbench work area
453
+ under an open rail overlay) now stays hidden.
454
+
455
+ - **`@kerfjs/ui`: `ListHeader`, `NavStack`, `Select`, and `StateBanner` no
456
+ longer restyle a composed child through a class on its root.** Each child
457
+ now styles itself: `Text` sets a `ListHeader` label from `text.css`, `Badge`
458
+ spaces a `Select` count and a `StateBanner` badge from `badge.css`, and a
459
+ `NavStack` title uses `ToolbarText`'s new `fill` option. Rendering is
460
+ unchanged. `ToolbarText` gains `fill` (grow into the free space of its
461
+ toolbar zone and truncate there). The `kui-list-header__label`,
462
+ `kui-nav-stack__title`, `kui-select__count`, and `kui-state-banner__badge`
463
+ classes are removed; select a ListHeader label with
464
+ `.kui-list-header__title > .kui-text` and a NavStack title with
465
+ `.kui-toolbar-text[data-fill="true"]` if a test needs to find them.
466
+
467
+ - **`@kerfjs/ui` ships a markdown component reference for AI assistants.**
468
+ `@kerfjs/ui/ai/components/README.md` indexes every catalog entry in one line,
469
+ and `@kerfjs/ui/ai/components/<id>.md` gives each entry a short page: when to
470
+ use and avoid it, imports and CSS/registration side effects, a link to its
471
+ exact props, legal parents and zones (JSX prop and child count), app- and
472
+ wiring-owned state, geometry, accessibility obligations, its styling boundary
473
+ (configure through props; do not override internals), and its diagnostics.
474
+ `llms.txt` and the shipped skill now send assistants there instead of to the
475
+ 263 KB `component-catalog.json` and 221 KB `component-composition.json`,
476
+ which remain the canonical sources for tools. The pages are generated from
477
+ those two files and a package check fails when they drift.
478
+
479
+ - **Breaking (`@kerfjs/ui` and `create-kerf-component`): the composition
480
+ catalog files are renamed so they no longer read as a "v2" of the component
481
+ catalog.** The composition catalog is a generated composition layer beside
482
+ `component-catalog.json`, not a newer version of it. Update any import,
483
+ `$schema`, `$ref`, or `.kerf-ui-profile.json` catalog location that names an
484
+ old path:
485
+
486
+ | Old path | New path |
487
+ | -------------------------------------------------------------- | --------------------------------------------------------------- |
488
+ | `@kerfjs/ui/ai/component-catalog-v2.json` | `@kerfjs/ui/ai/component-composition.json` |
489
+ | `@kerfjs/ui/ai/component-catalog-v2.schema.json` | `@kerfjs/ui/ai/component-composition.schema.json` |
490
+ | `@kerfjs/ui/ai/component-catalog-v2.d.ts` | `@kerfjs/ui/ai/component-composition.d.ts` |
491
+ | `@kerfjs/ui/ai/component-catalog-v2-overrides.json` | `@kerfjs/ui/ai/component-composition-rules.json` |
492
+ | `@kerfjs/ui/ai/component-catalog-extension-v2.schema.json` | `@kerfjs/ui/ai/component-composition-extension.schema.json` |
493
+ | `@kerfjs/ui/ai/component-catalog-extension-v2.d.ts` | `@kerfjs/ui/ai/component-composition-extension.d.ts` |
494
+ | `@kerfjs/ui/docs/examples/component-catalog-extension-v2.json` | `@kerfjs/ui/docs/examples/component-composition-extension.json` |
495
+ | scaffolded package `component-catalog-v2.json` | scaffolded package `component-composition.json` |
496
+
497
+ The exported types follow: `ComponentCatalogV2` → `ComponentComposition`,
498
+ `CatalogCompositionEntryV2` → `CatalogCompositionEntry`,
499
+ `ConsumerComponentCatalogV2` → `ConsumerComponentComposition`, and
500
+ `ConsumerCatalogEntryV2` → `ConsumerCompositionEntry`. In the repository,
501
+ `npm run catalog:v2:sync` is now `npm run composition:sync`
502
+ (`catalog:sync` still runs both generators). The file contents and their
503
+ `schemaVersion` are unchanged.
504
+
505
+ - **Breaking (`@kerfjs/ui`, `create-kerf-component`, and `kerfjs setup`): the
506
+ composition catalog's own fields no longer read as a "v2" of the component
507
+ catalog.** In `component-composition.json` (and any consumer composition
508
+ catalog), `"schemaVersion": 2` is now `"schemaVersion": 1` — the composition
509
+ format's own first version, independent of `component-catalog.json`'s — and
510
+ `compatibility.v1Catalog` is now `compatibility.componentCatalog`. A
511
+ scaffolded package's `kerf.components.json` renames its `v1Catalog` field to
512
+ `componentCatalog` the same way. Regenerate scaffolded catalogs
513
+ (`npm run catalog:generate`), and change any `.kerf-ui-profile.json` `composition`
514
+ location from `"schemaVersion": 2` to `"schemaVersion": 1`; a catalog still
515
+ carrying the old fields fails validation with a message naming the rename.
516
+ The `ComponentComposition` type follows (`schemaVersion: 1`,
517
+ `compatibility.componentCatalog`).
518
+
519
+ - **`@kerfjs/ui` `NavStack` bars use the standard `Toolbar` padding.** The
520
+ navigation bar dropped its toolbar's block padding to stay 44px tall, so its
521
+ title and controls touched the bar's edges. It now keeps the 8px above and
522
+ below that every other toolbar has (60px plus any top safe-area inset).
523
+
524
+ - **`@kerfjs/ui` multiline list rows break long camelCase names at word
525
+ boundaries.** A string label in a `multiline` `ListItem` or `ListActionRow`
526
+ (such as a Catalog sidebar entry) gets `<wbr>` break opportunities at its
527
+ camelCase and acronym boundaries, so `QuickTicketComposer` wraps as
528
+ `QuickTicket` / `Composer` instead of `QuickTicketComp` / `oser`. Breaking
529
+ mid-word remains the fallback for a single word that is still too wide.
530
+ - **`wbr` is a typed JSX intrinsic element.** The runtime already rendered it
531
+ as a void element; `JSX.IntrinsicElements` now declares it.
532
+
533
+ - **`@kerfjs/ui` `Badge` has a text-free dot, and `TabScaffold` tabs can show
534
+ it.** `Badge` takes `size="dot"`: an 8px solid circle in the badge's tone,
535
+ with no children, appearance, or shape. Because a dot has no text, its props
536
+ require either a `label` (announced as an image, for example
537
+ `label="New activity"`) or `ariaHidden` when the owning component already
538
+ announces it. `TabScaffoldTab` accepts `badge: true` for the iOS tab-bar dot
539
+ (new content without a count), centered on the icon's top-trailing corner;
540
+ its `badgeLabel` is required and joins the tab's accessible name
541
+ ("Settings, Update available"). `BadgeTextProps` and `BadgeDotProps` are
542
+ exported from the root barrel.
543
+
544
+ - **`@kerfjs/ui` `Toolbar` truncates the leading title instead of letting the
545
+ center zone cover it.** When a leading identity, a `center` group, and
546
+ trailing actions no longer fit one row, the center zone keeps its whole
547
+ content width and the leading title ends in an ellipsis. Previously the
548
+ center track could shrink below its group, which then painted over the
549
+ title — a pushed `NavStack` view with a center group did this at 390px. The
550
+ fix holds under every `responsive` policy and both `centerAlign` values, and
551
+ a toolbar that fits lays out exactly as before.
552
+
553
+ - **`@kerfjs/ui` `ValueTableRow` keeps its value readable beside a long
554
+ label.** A long single-word label (such as `ConnectionStateBanner` in a
555
+ 390px-wide table) no longer squeezes the value to 0px wide, where it wrapped
556
+ one character per line while the label ran underneath it. The label and value
557
+ now share the row as two content-sized columns: a short value stays whole on
558
+ one line, the label wraps at its spaces and ends an over-long word in an
559
+ ellipsis before the value, and when both are long they split the row.
560
+
561
+ - **`@kerfjs/ui` pinned chrome can give way in a short pane.** `Pane` takes
562
+ `chromePlacement="auto"`: its header and footer stay pinned while the pane is
563
+ at least 480px (30rem) tall, and below that the whole pane scrolls as one
564
+ column so tall chrome never squeezes the content to nothing.
565
+ `WorkbenchChromePlacement` gains `"auto"` for `mainHeaderPlacement` /
566
+ `mainFooterPlacement`, and `Catalog` now defaults `headerPlacement` /
567
+ `footerPlacement` to `"auto"`. Its entry description is pinned under the
568
+ toolbar, which at 200% text size on a short window had left the preview 0px
569
+ tall.
570
+
571
+ - **`@kerfjs/ui` `TabScaffold` tabs can show a badge.** Give a
572
+ `TabScaffoldTab` a `badge` (a count or short string) to show the iOS tab-bar
573
+ badge — a compact, solid danger `Badge` at the top-trailing corner of the
574
+ tab's icon. The badge is hidden from assistive technology; the tab's
575
+ accessible name becomes `"<label>, <badgeLabel>"` (for example
576
+ "Inbox, 3 unread"), with `badgeLabel` defaulting to the badge text. Tabs
577
+ without a badge are unchanged.
578
+
579
+ - **`@kerfjs/ui` `ListItem` and `ListActionRow` labels stay out of the
580
+ trailing slot.** A long single-word primary label (such as a Catalog sidebar
581
+ entry named `ConnectionStateBanner` in a narrow rail) no longer runs
582
+ underneath trailing status text. Single-line rows now end the primary label
583
+ and description with a real ellipsis (previously the text was cut mid-glyph
584
+ with no "…"), and `multiline` rows break a word wider than the label column
585
+ onto the next line.
586
+
587
+ - **Custom elements keep the `role` and `aria-*` they set on themselves across
588
+ re-renders.** Web components conventionally own their host's ARIA (Web
589
+ Awesome's `wa-option` sets `role="option"` / `aria-selected`, `wa-divider`
590
+ sets `role="separator"`; Shoelace and FAST do the same), but a template never
591
+ mentions them, so the morph and the keyed-list attribute fast path stripped
592
+ them on the next re-render. On any custom element (hyphenated tag), `role`
593
+ and `aria-*` are now element-owned like `open`: never removed just because
594
+ the template omits them. The template can still set or change them, a signal
595
+ binding set to `null` / `false` still removes them, and plain elements are
596
+ unchanged. Trade-off: omitting one from a static template no longer removes
597
+ it from a custom element — render an explicit value (`aria-pressed="false"`)
598
+ instead.
599
+
600
+ - **`@kerfjs/ui` layout regions no longer slow edits to long lists inside
601
+ them.** The rule that clears the safe-area context for the children of a
602
+ Workbench main area or panel, SplitView list or detail, NavStack view,
603
+ TabScaffold scene, or CollapsiblePanel content made each child re-check the
604
+ region in Chromium on every DOM change. A 1000-row list placed directly in
605
+ one of these regions took about 46ms per style recalculation (2ms with the
606
+ list one level down), and now takes about 0.2ms. Layout and insets are
607
+ unchanged.
608
+
609
+ - **`@kerfjs/ui` `CollapsiblePanel` no longer slows insertions into long
610
+ lists.** An expanded inline panel clears its screen edge for its siblings;
611
+ the rules that did so selected the siblings themselves
612
+ (`:has(~ panel)` and `panel ~ *`), which in Chromium made each row inserted
613
+ into or removed from a long list restyle the list's other rows — about
614
+ 14.6ms instead of 0.2ms per insertion beside a 1000-row list. The panel's
615
+ container now carries a flag that a CSS style query reads, so a list pays
616
+ nothing. The edge still clears for the panel's siblings and returns when it
617
+ collapses; the one difference is that a sibling placed before a left panel
618
+ or after a right or bottom panel now loses the edge too.
619
+
620
+ - **`@kerfjs/ui` restore corners and covered floating controls no longer slow
621
+ every style recalculation.** The `CollapsiblePanel` / `ResizableRegion`
622
+ restore-corner anchor scope (`:has(> restore) > * > * > *`) and the static
623
+ side-overlay covered flag (`:has(> open overlay) > :not(overlay)`) put a
624
+ universal selector after a `:has()` container, which in Chromium restyled
625
+ the children of every ancestor of each DOM change — about 85ms instead of
626
+ 1ms per edit beside a 1000-row list. The drawer now publishes its anchor
627
+ only beside a restore control's container (or its work-area column), and
628
+ the covered flag is set on the overlay's container and cleared on its
629
+ overlay children. Restore-corner placement and covered-control hiding are
630
+ unchanged; a new CSS unit test rejects the costly `:has()` shapes.
631
+
632
+ - **`@kerfjs/ui` adds `ContentItem`.** Import it from
633
+ `@kerfjs/ui/content-item` to render one self-contained pane child with the
634
+ shared 8px margin, 1px border, and 8px padding. Framing and shape are typed
635
+ props instead of hand-written modifier classes:
636
+ - `frame="framed"` paints the neutral border in the 1px the item always
637
+ reserves, so its geometry does not change (replacing
638
+ `class="kui-content-item kui-content-item--framed"`);
639
+ - `shape="pill"` selects the 22px radius.
640
+ - `ariaLabel` names a distinct region, and `focusTarget` makes the item a
641
+ programmatic focus target (`tabindex="-1"`), such as a NavStack view's
642
+ `data-nav-focus` target.
643
+
644
+ The `.kui-content-item` classes stay public for elements that must carry the
645
+ geometry themselves; their CSS now lives in `content-item.css`, which
646
+ `layout.css` still imports. There is no filled variant — use `SunkenPanel`
647
+ for a lowered surface. The catalog, demos, and recipes use the component.
648
+
649
+ - **`@kerfjs/ui` icon-only toolbar buttons show the same help tag.** An
650
+ icon-only `<button>` or link inside a `ToolbarControlGroup`, named by
651
+ `aria-label` with no visible text, now shows the help tag icon-only Selects
652
+ and PopupMenus show, with the same 500ms hover delay, keyboard-focus-only
653
+ display, press/Escape dismissal, and quick scanning along a toolbar. The tag
654
+ is `aria-hidden` and never renames the button. A button with a native
655
+ `title`, or with its own Web Awesome tooltip, keeps that and gets no second
656
+ tag. The `select/register` and `popup-menu/register` imports install it; a
657
+ toolbar with icon-only buttons but neither control imports the new
658
+ `@kerfjs/ui/help-tags/register` subpath, which registers only the tooltip.
659
+
660
+ - **`@kerfjs/ui` icon-only Select and PopupMenu triggers show a help tag.**
661
+ An icon-only `Select` (single or `multiple` with `triggerIcon`) and an
662
+ icon-only `PopupMenu` now show a Web Awesome tooltip naming the control on
663
+ hover (after 500ms) and on keyboard focus: a filter reads "Filter by label:
664
+ Bug, Docs", a single Select its name and current choice, a menu its `label`.
665
+ It hides while the popup is open, on a press, and on Escape. The tag repeats
666
+ the accessible name, so it is `aria-hidden` and never joins
667
+ `aria-labelledby`; screen readers still hear the name once. It is installed
668
+ by the existing `select/register` and `popup-menu/register` imports, needs no
669
+ props, and never appears for labeled Selects or text menu triggers.
670
+
671
+ - **`@kerfjs/ui` toolbar groups drop their focus ring while a popup is open.**
672
+ A `ToolbarControlGroup` that owns the ring (`focusRing="outline"` or
673
+ `"halo"`) no longer paints it while a Select listbox or `PopupMenu` inside it
674
+ is open; the open popup shows focus, and the ring returns when the popup
675
+ closes with focus still in the group. This matches the per-control Select
676
+ and PopupMenu triggers.
677
+
678
+ - **`@kerfjs/ui` multiple `Select` can be an icon-only toolbar filter menu.**
679
+ Set `selectedPresentation: 'icon-only'` on a `multiple` Select together with
680
+ a fixed `triggerIcon` (for example a funnel `LucideIcon`); the types require
681
+ `triggerIcon` there and reject it everywhere else. The trigger keeps the
682
+ icon-only caret pill used in a `ToolbarControlGroup`, and:
683
+ - shows the fixed icon whatever is chosen;
684
+ - adds a compact count `Badge` while any choice is chosen;
685
+ - ends the combobox's accessible name with the chosen labels in choice order
686
+ ("Filter by label: Bug, Docs"), leaving the hidden value text empty so the
687
+ list is not announced twice.
688
+
689
+ The popup still stays open while choices toggle.
690
+
691
+ - **`@kerfjs/ui` `Select` options keep their accessibility semantics across
692
+ re-renders.** Re-rendering a Select (for example after its `change` handler
693
+ updates the controlling signal) removed the `role="option"` and
694
+ `aria-selected` Web Awesome sets on each option, so screen readers lost every
695
+ option's role and selected state. They now survive in single and `multiple`
696
+ Selects through the core rule below (custom elements own their `role` /
697
+ `aria-*`), with no Select-specific restore.
698
+
699
+ - **`@kerfjs/ui` `Select` dividers keep their separator role across
700
+ re-renders.** The `wa-divider` a `separatorBefore` choice adds lost the
701
+ `role="separator"` and `aria-orientation` Web Awesome sets on it the first
702
+ time the enclosing mount re-rendered, and never got them back, so assistive
703
+ technology stopped announcing the break between option runs. The Select now
704
+ renders both attributes itself.
705
+
706
+ - **`@kerfjs/ui` `Select` and `PopupMenu` re-renders no longer force Web
707
+ Awesome updates.** Web Awesome reflects its default `size`, `appearance`,
708
+ `placement`, and `orientation` onto `wa-select`, `wa-dropdown`, and
709
+ `wa-divider`; the templates omitted them, so every re-render of the enclosing
710
+ mount stripped them and each host ran a full Lit update to put them back. The
711
+ templates now render those defaults (`wa-select size="m"
712
+ appearance="outlined" placement="bottom"`, `wa-dropdown size="m"`, divider
713
+ `orientation="horizontal"`), so an unchanged re-render leaves the hosts
714
+ alone. No visual change. The `@kerfjs/ui/webawesome` JSX declarations type
715
+ these attributes.
716
+
717
+ - **`@kerfjs/ui` Workbench and CollapsiblePanel panes and chrome lists are
718
+ configurable.** The `Pane`s these layouts compose around app content now
719
+ take the new `PaneConfig` type (`contentElement`, `contentLabel`,
720
+ `separators`, `safeAreaEdges`): a Workbench panel's or `CollapsiblePanel`'s
721
+ `pane`, and the Workbench's `mainPane`. The `List`s holding `mainHeader` /
722
+ `mainFooter` take the new `ListConfig` type (`gap`, `hAlign`, `vAlign`,
723
+ `dividerSides`, `textInsets`, `controlInsets`) through `mainHeaderList` /
724
+ `mainFooterList`. Previously this configuration was hard-coded, so, for
725
+ example, a navigator rail could not render its content as a `nav` landmark
726
+ without restyling. Omitted or `undefined` fields keep today's defaults, and
727
+ the default markup is unchanged.
728
+
729
+ - **`@kerfjs/ui` `SplitView` forwards its `ResizableRegion` and compact
730
+ `NavStack` configuration.** `resizable` now also takes the list region's
731
+ `separator`, `handleIcon`, `contentOverflow`, and collapse options
732
+ (`collapsed`, `transitioning`, `collapseMotion`, `restoreControl`,
733
+ `restorePosition`), so an app can hide the list and float a restore control
734
+ without leaving `SplitView`. The new `compactStack` prop forwards the compact
735
+ stack's `toolbarConfig`, `backIcon`, `backText`, `hideToolbar`, and
736
+ persistent `bottomToolbar`, and gives the list and detail views their own
737
+ toolbar groups (`list` / `detail`: `leading`, `center`, `toolbar`,
738
+ `bottomToolbar`). Omitted options keep today's defaults.
739
+
740
+ - **`@kerfjs/ui` `NavStack` renders its top chrome as a real `Toolbar`.** The
741
+ back control is a borderless `ToolbarControlGroup` and the title a
742
+ `ToolbarText` in the leading zone, and the stack now forwards:
743
+ - `toolbarConfig` — the `Toolbar` configuration (`dividerSides`,
744
+ `centerAlign`, `responsive`, `responsiveAt`, `safeAreaEdges`) plus `label`,
745
+ `titleSize` (default `large`), and `headingLevel`;
746
+ - per-view `leading` and `center` groups beside the existing trailing
747
+ `toolbar`;
748
+ - `backIcon` and visible `backText` for the back control.
749
+
750
+ The defaults keep today's bar: no divider, one 44px band, and the top and
751
+ side safe-area edges claimed. The hand-rolled `kui-nav-stack__lead` and
752
+ `kui-nav-stack__actions` wrappers are gone (their content now sits in the
753
+ toolbar's zones), and `kui-nav-stack__back` / `kui-nav-stack__back-icon`
754
+ are no longer styled by the stack.
755
+
756
+ - **`@kerfjs/ui` configures with props instead of token overrides.** Five
757
+ settings apps previously reached through raw `--kui-*` overrides now have a
758
+ typed prop or modifier; each defaults to today's rendering, and the
759
+ underlying tokens keep working:
760
+ - `FloatingToolbar` `inset` (a `CssLength`, default `--kui-space-m`) moves
761
+ it from the container edges. Omit it inside a restore corner, which owns
762
+ the inset.
763
+ - `DisclosureArrow` `size` (a `CssLength`, default 18px root-scaled).
764
+ - `ListHeader` `headingLevel` (1–6, default 2) sets the label's native
765
+ heading element for passive and trailing-action headers; a toggle header
766
+ has no heading and rejects it at compile time.
767
+ - `TokenSearchField` `clearIcon` replaces the clear action's glyph.
768
+ - `.kui-content-item--framed` paints the standard neutral border on a
769
+ content item that marks a real distinction, with no geometry change.
770
+
771
+ The docs and AI guidance also stop recommending `--kui-edge-inset-*`
772
+ overrides: an app-owned arrangement passes `safeAreaEdges` to its panes and
773
+ edge-claiming toolbars (including the layout toolbar configurations, which
774
+ forward it).
775
+
776
+ - **`@kerfjs/ui` hides a work area's floating controls under an open side
777
+ overlay.** While a side overlay is open — a Workbench rail (static
778
+ `presentation: "overlay"`, or a responsive overlay whose breakpoint applies),
779
+ a `wireSidebar` compact overlay or `presentation="overlay"`
780
+ `CollapsiblePanel`, or a horizontal `presentation="overlay"`
781
+ `ResizableRegion` — the covered work area's `FloatingToolbar`s and the
782
+ layouts' restore corners are now hidden (`visibility: hidden`, so they also
783
+ leave the tab order and the accessibility tree) instead of relying on
784
+ stacking order alone. A phone-width app could otherwise paint the main
785
+ column's floating drawer toggle over its open inspector. They come back as
786
+ the overlay closes; the overlay's own floating controls and a bottom
787
+ drawer's overlay are unaffected.
788
+
789
+ - **`@kerfjs/ui` `Select` supports multiple selection.** Set `multiple: true`
790
+ and pass the chosen values as an array `value`; the props are typed so an
791
+ array value requires `multiple`. Behavior:
792
+ - The popup stays open while the person toggles choices with the pointer,
793
+ Enter, or Space, and closes only on an outside press, Escape, or focus
794
+ leaving.
795
+ - Each chosen option shows a check.
796
+ - The closed control summarizes the chosen labels in choice order as a short
797
+ localized list, or shows `placeholderText` when none are chosen.
798
+ - `change` / `input` report the value array, and a form submits every chosen
799
+ value under `name`.
800
+ - Re-rendering with a different `value` updates the selection even after the
801
+ person has interacted.
802
+
803
+ `renderSelected` and `selectedPresentation="icon-only"` stay single-value
804
+ only.
805
+
806
+ - **`@kerfjs/ui` `PopupMenu` now looks exactly like `Select`.** Both read one
807
+ shared popup contract, so a command menu beside a value list in a toolbar
808
+ draws the same popup. `PopupMenu` changes:
809
+ - its caret turns to face the open menu;
810
+ - its menu fades in without scaling;
811
+ - it takes the Select listbox's 4px inset (also under the default
812
+ `menuInset="standard"`);
813
+ - its rows use the Select option's height, padding, and reserved check
814
+ column, and its icons use the same 16px size and 8px gap;
815
+ - a keyboard-focused item takes the Select's current-option fill instead of
816
+ a focus ring;
817
+ - headings render as the Select's uppercase group titles rather than slotted
818
+ `h3` elements.
819
+
820
+ An open `Select` also no longer paints its trigger focus ring. The open
821
+ listbox shows focus, and the ring returns when it closes.
822
+
823
+ - **`@kerfjs/ui` `Catalog` exposes its sidebar and toolbar configuration.** New
824
+ `sidebar` prop (size, `resizable`, `separator`, `collapseMotion`,
825
+ `presentation`, `responsiveOverlayAt`, `compactOverlay`, and the header
826
+ `toolbar`'s `ToolbarConfig`) forwarded to its Workbench rail, plus
827
+ `mainToolbar` / `footerToolbar` `ToolbarConfig` props. `wireCatalog` takes
828
+ `sidebarSize` / `sidebarStorageKey` to wire a resizable sidebar. Today's
829
+ 288px fixed sidebar and toolbar policies remain the defaults.
830
+
831
+ - **`@kerfjs/ui` composed toolbars are configurable.** `WorkbenchMainToolbar`,
832
+ `WorkbenchMainBottomToolbar`, and the panel `toolbar` of a Workbench panel or
833
+ `CollapsiblePanel` now take the `Toolbar`'s configuration: the new
834
+ `ToolbarConfig` type, covering `dividerSides`, `centerAlign`, `responsive`,
835
+ `responsiveAt`, and `safeAreaEdges`. Previously their dividers were
836
+ hard-coded, so for example dropping the line between a toolbar and its
837
+ content took CSS. Today's dividers remain the defaults. Also:
838
+ - `mainBottomToolbar` and panel toolbars take a `center`;
839
+ - panel toggles take localized `showLabel` / `hideLabel`;
840
+ - a Workbench panel's floating restore controls follow its
841
+ `restorePosition`.
842
+
843
+ - **Breaking (`@kerfjs/ui`): removed the pre-release compatibility surface.**
844
+ - The `DividerSides` type is gone; use `Sides`, from the renamed `sides`
845
+ module.
846
+ - `ListInsetText.horizontalOnly` and its `kui-list-inset-text--horizontal`
847
+ class are gone; pass `sides="rl"` instead.
848
+ - The legacy color tokens are gone, both as `--kui-color-*` properties and
849
+ as `uiColor()` names. Replace them with their canonical tokens:
850
+
851
+ | Removed | Use instead |
852
+ | --------------------------------------------- | ----------------------- |
853
+ | `border` | `neutral-border-normal` |
854
+ | `border-quiet` | `neutral-border-quiet` |
855
+ | `accent` | `brand-fill-normal` |
856
+ | `accent-text` | `brand-on-quiet` |
857
+ | `pop` | `pop-fill-quiet` |
858
+ | `pop-text` | `pop-on-fill` |
859
+ | `success`, `warning`, `danger` | `*-fill-quiet` |
860
+ | `success-text`, `warning-text`, `danger-text` | `*-on-quiet` |
861
+
862
+ - **New `@kerfjs/ui` `PopupMenu` (`@kerfjs/ui/popup-menu`).** It renders a
863
+ trigger and typed commands, headings, and dividers over the Web Awesome
864
+ dropdown. Put it in a `single` `ToolbarControlGroup` with `nestedDropdown`, or
865
+ wrap it in a `PopupSurface`. Register it once with
866
+ `@kerfjs/ui/popup-menu/register`. It replaces hand-written `wa-dropdown`
867
+ markup, which the catalog discourages: the guidance, the toolbar demos, and
868
+ the Catalog's "Components" menu now use it. It also fixes two defects that
869
+ hand-written menus had:
870
+ - Icon-only triggers were unnamed, because a Web Awesome button ignores an
871
+ `aria-label` on its host. PopupMenu renders `label` as visually hidden text
872
+ inside the button.
873
+ - A keyboard choice (Enter or Space) never reached click-delegated
874
+ `data-action` handlers. PopupMenu's registration routes it through the
875
+ item's click.
876
+ - The UX catalog's application-layout demo no longer uses the discouraged
877
+ `wa-button-group`.
878
+
879
+ - **`@kerfjs/ui`'s `TokenSearchField` takes a `revision` prop for programmatic
880
+ text replacement.** The editor's text is DOM-owned, so a new `query` with the
881
+ same tokens was silently ignored, for example when reseeding a persistent
882
+ dialog's field on reopen. Changing `revision` rebuilds the editor from `query`
883
+ and `tokens`, so consumers no longer need to re-key a wrapper element.
884
+
885
+ - **`@kerfjs/ui`'s `TabBar` strip no longer scrolls vertically.** The strip set
886
+ only `overflow-x: auto`, so any vertical excess (consumer padding, focus-ring
887
+ insets, taller tabs under larger text) let it scroll up and down on touch. It
888
+ now scrolls only horizontally, and it takes a `min-height` so it grows to fit
889
+ taller tabs instead of clipping them.
890
+
891
+ - **The `@kerfjs/ui` desktop app-shell recipe is a `Workbench`.** The old
892
+ pane switcher that showed one pane at a time below desktop sizes is gone. The
893
+ navigation and inspector are now resizable Workbench rails with their own
894
+ toolbars, and their show toggles relocate into the work-area toolbar. On a
895
+ narrow container the rails overlay the task list one at a time. The
896
+ collapsible-sidebar recipe composes each panel's `toolbar` and uses
897
+ `CollapsiblePanelRelocated` for its restore controls.
898
+
899
+ - **`@kerfjs/ui`'s `CollapsiblePanel` composes a panel toolbar and relocates its
900
+ controls.** A panel takes the same `toolbar` roles as a `Workbench` panel
901
+ (`title`, `panelOnly`, `constant`, and a standard `toggle`) plus a `footer`,
902
+ and the new `CollapsiblePanelRelocated` renders a collapsed panel's constant
903
+ groups and toggle wherever the app's work-area toolbar holds them.
904
+ `wireSidebar` hands focus to that relocated toggle when the panel closes, even
905
+ when the app renders it a frame late. `collapsiblePanelToggleIcon` is
906
+ unchanged.
907
+
908
+ - **`@kerfjs/ui`'s `Catalog` is built on `Workbench` and dogfoods the package's
909
+ components.** The sidebar is a Workbench left rail with the standard toggle
910
+ (labels "Show/Hide {brand} catalog"), which moves into the entry toolbar while
911
+ collapsed; on a small screen the sidebar overlays the stage instead of the
912
+ former stacked two-column navigation, which is gone. The sidebar, sections,
913
+ footer, and example notes are `List` / `ListHeader` / `ListInsetText` / `Text`
914
+ compositions, and most catalog CSS (and every viewport media query in the
915
+ shell) is removed. New: `Catalog`'s `id`, `headerPlacement`, and
916
+ `footerPlacement`; `wireCatalog`'s `collapsed` and `id` options (the sidebar is
917
+ wired like a Workbench rail and an open overlay closes on a selection);
918
+ `Workbench`'s `mainHeader`, `mainFooter`, `mainHeaderPlacement`,
919
+ `mainFooterPlacement`, and `mainToolbar.responsive` / `responsiveAt`;
920
+ `ListInsetText`'s `rootAttributes`. `wireWorkbench` now also re-checks panel
921
+ breakpoints when the root font size changes, and hands focus to a panel's
922
+ relocated toggle when an unfocused click on its own toggle closes it (WebKit).
923
+
924
+ - **`@kerfjs/ui`'s `Workbench` rails overlay the work area on small screens by
925
+ default.** A rail's `responsiveOverlayAt` now defaults to `"narrow"` (704px
926
+ of Workbench width; the new `"never"` keeps it inline), so a sidebar covers
927
+ the work area and any open drawer instead of squeezing it, one at a time
928
+ with `wireWorkbench`. On a compact (448px) Workbench an open overlay rail
929
+ fills the width less a 44px dismiss strip
930
+ (`--kui-workbench-overlay-dismiss-margin`) on the side away from its edge;
931
+ the new per-rail `compactOverlay: "full"` fills it. The bottom drawer's
932
+ behavior is unchanged. `WorkbenchPanelToolbar` and the other panel-toolbar
933
+ types are now exported from `@kerfjs/ui/workbench`.
934
+
935
+ - **`@kerfjs/ui`'s `Workbench` composes panel toolbars and moves a closed
936
+ panel's controls into the work area.** A panel takes an optional `toolbar`
937
+ (`title`, `panelOnly`, `constant`, and a standard `toggle: { action, name }`
938
+ the Workbench renders) and `footer`; the Workbench takes `mainToolbar` and
939
+ `mainBottomToolbar`. While a panel is closed its constant groups and toggle
940
+ move to the work area — a left rail's lead the main toolbar, a right rail's
941
+ end it, a drawer's trail the bottom toolbar or float in a corner
942
+ `FloatingToolbar` — and its panel-only groups stay behind. `wireWorkbench`
943
+ keeps focus with the toggle as it moves. Panels without a `toolbar` render
944
+ exactly as before, and a region's only-child `Pane` or layout now fills it.
945
+
946
+ - **`@kerfjs/ui`'s list-detail dialog guidance is Workbench-shaped.** On roomy
947
+ devices the list is a full-height sidebar and the detail a full-height main
948
+ column, each a `Pane` with its own top `Toolbar`; the main column carries
949
+ the primary title, the close control (`data-dialog="close"` in a
950
+ `without-header` Web Awesome dialog), and the record actions, instead of a
951
+ dialog header spanning both columns. The recipe, `docs/recipes.md`, and the
952
+ shipped AI skill say so; compact devices keep the full-screen drill-down
953
+ sheet.
954
+
955
+ - **`@kerfjs/ui`'s `ToolbarControlGroup` centers a direct icon tile.** A
956
+ non-interactive icon placed directly in a group — a heading's identity
957
+ glyph — now takes an icon button's 40px slot and 16px visual, so it centers
958
+ in the group. It previously sat at the group's start edge, which pushed
959
+ apps to wrap it in a focusable `<button>` that did nothing.
960
+
961
+ - **`@kerfjs/ui`'s `Toolbar` keeps its controls in a top control band.** A
962
+ toolbar now reserves one 44px band at its top: each zone item no taller than
963
+ the band is centered in it by its own size, and a taller item — a wrapped
964
+ `ToolbarText` title, a multi-line search, a second trailing row — starts at
965
+ the band's top and grows down without moving the others. A wrapped title
966
+ centers its first line in the band, so trailing controls stay beside that
967
+ line instead of dropping to the middle of the whole title.
968
+
969
+ - **`@kerfjs/ui`'s segmented `TabBar` ends its strip concentric with the pill
970
+ track.** The first tab's leading corners, and with `allocation="fill"` the
971
+ last tab's trailing corners, now follow the track's curve instead of keeping
972
+ the small segment radius, and compact tabs are inset from the track's ends
973
+ by the same distance as from its top and bottom. Corners between tabs are
974
+ unchanged.
975
+
976
+ - **`@kerfjs/ui`'s `StateBanner` keeps its title, badge, and detail on one
977
+ baseline in Safari.** The truncating detail was a clipped `inline-block`,
978
+ which WebKit baselines at its bottom edge: the detail rose about 2px above
979
+ the title and pushed the title and badge down, so the banner's contents
980
+ looked vertically misaligned. The detail is now an inline flex wrapper
981
+ around a clipped inner `<span>`, which takes its text baseline in every
982
+ engine; truncation and the narrow-width detail line are unchanged.
983
+
984
+ - **`@kerfjs/ui`'s `wireTabBars` reveals a tab the application selects.** A
985
+ tab added and selected by the app in an overflowing `TabBar` stayed
986
+ off-screen, because the wiring revealed a selection only at wire-up, on
987
+ focus, and after its own keyboard/drag changes. A change of a bar's selected
988
+ tab now scrolls that bar's strip (never the page) to show it; re-renders that
989
+ keep the selection leave a user-scrolled strip alone.
990
+
991
+ - **`eslint-plugin-kerfjs`'s `ui-composition` rule resolves application
992
+ wrappers imported through a barrel.** A cataloged wrapper registered only by
993
+ its defining source file, so `import { DemandSegmentsControl } from
994
+ './components/index.js'` (where `index.ts` re-exports it) was an unknown
995
+ element and reported `KUI-L202` in a toolbar zone despite its `rendersAs`.
996
+ Relative re-exports — `export { X } from`, `export { X as Y } from`,
997
+ `export * from`, and chains of them — are now followed to the cataloged
998
+ source file. Type-only and namespace re-exports and non-cataloged components
999
+ stay unresolved.
1000
+
1001
+ - **A collapsed `@kerfjs/ui` `Workbench` panel or `ResizableRegion` leaves
1002
+ the accessibility tree.** Only its content was `inert`, so the labeled
1003
+ `aside` / `section` stayed behind as an empty complementary or region
1004
+ landmark that a screen-reader user could still land on. The collapsed
1005
+ region itself now renders `inert` and `aria-hidden="true"`, like a
1006
+ collapsed `CollapsiblePanel`; its separator was already hidden, and each
1007
+ `restoreControl` renders outside it and stays reachable.
1008
+
1009
+ - **An expanded `@kerfjs/ui` `CollapsiblePanel` holds its size in a flex
1010
+ layout.** The panel shrank like any flex item beside a work area with wide
1011
+ content, while its content kept the panel's fixed size for the slide-out, so
1012
+ the panel's edge clipped the content — in the collapsible-sidebar recipe at
1013
+ 1280px, the rail showed 211 of its 232px and cut off the selected Inbox
1014
+ row's count. The panel is now `flex-shrink: 0`; the sibling work area gives
1015
+ way instead.
1016
+
1017
+ - **`@kerfjs/ui` overlay panels span their host on the fixed axis.** The
1018
+ 85vw / 85vh overlay maximums (`--kui-resizable-region-overlay-max-*`,
1019
+ `--kui-workbench-overlay-max-*`) applied to both axes, so an overlay bottom
1020
+ drawer stopped about 15% short of its host's width and an overlay side rail
1021
+ in a full-height layout stopped short of the bottom. Each maximum now caps
1022
+ only the panel's resizable axis: side overlays cap their width, drawers
1023
+ their height.
1024
+
1025
+ - **An overlay `@kerfjs/ui` `ResizableRegion` paints the surface.** With
1026
+ `presentation="overlay"` the region floated over the work area with a
1027
+ shadow but no background, so the content beneath — and a collapsed rail's
1028
+ restore control stacked under it — showed through. It now paints
1029
+ `--kui-color-surface`, like an overlay `CollapsiblePanel`; an inline region
1030
+ stays transparent over its host.
1031
+
1032
+ - **A pinned `@kerfjs/ui` resize separator no longer overwrites the
1033
+ remembered size.** When a container squeezes a `Workbench` rail, drawer, or
1034
+ `ResizableRegion` below its `min` (or its reachable range collapses to one
1035
+ size), the separator cannot move it, yet an arrow/Home/End key or a drag
1036
+ still committed the configured `min` to the app's size signal and storage:
1037
+ a rail remembered at 280px silently became 180px and came back at 180px
1038
+ when the window widened. A pinned separator now commits nothing, so the
1039
+ track returns at its remembered size once there is room.
1040
+
1041
+ - **A `pending` `@kerfjs/ui` `AppTab` is selectable and no longer dimmed.**
1042
+ A known tab that is still opening now keeps its select button enabled and
1043
+ in the roving tab order, so a user can switch to it and the application can
1044
+ show placeholder content in its panel until loading completes. With
1045
+ `webawesome.css` loaded, Web Awesome's native `button:disabled` no longer
1046
+ dims the pending pill to 50%; it shows only its quiet name color and
1047
+ trailing spinner, as documented. Close and dragging stay disabled while
1048
+ pending, and it stays `aria-busy`. An unknown `placeholder` tab is
1049
+ unchanged: disabled and out of the tab order.
1050
+
1051
+ - **Closing a `@kerfjs/ui` `Workbench` panel from its own control by mouse
1052
+ returns focus in Safari.** Safari never focuses a clicked button, so
1053
+ pressing an open panel's Hide control blurred the focused control to the
1054
+ page before the click collapsed the panel, and `wireWorkbench` — which
1055
+ returns focus only when it is inside the closing panel — left focus on the
1056
+ page body instead of the toggle that opened it. A press that starts inside
1057
+ the panel while focus is there now counts as focus inside, so the close
1058
+ returns it; focus the user has moved to another element since is left
1059
+ alone.
1060
+ - **A collapsed `@kerfjs/ui` `CollapsiblePanel` or `ResizableRegion` leaves
1061
+ the Tab order.** Both kept collapsed content in the DOM, only slid out and
1062
+ clipped, so Tab still reached its controls; focusing one scrolled the clipped
1063
+ panel, and a `CollapsiblePanel`'s `aria-hidden` put those focusable controls
1064
+ inside a hidden subtree. A collapsed `CollapsiblePanel` now renders `inert`
1065
+ beside its `aria-hidden`, and a collapsed `ResizableRegion` renders its
1066
+ content wrapper `inert`, with no wiring needed. The slide-out still animates,
1067
+ each `restoreControl` stays outside the inert subtree and reachable, and
1068
+ `wireSidebar` still hands focus to the panel's toggle when it collapses from
1069
+ inside.
1070
+
1071
+ - **A collapsed `@kerfjs/ui` `Workbench` rail's restore control no longer
1072
+ covers an expanded bottom drawer.** A `leftRail` / `rightRail`
1073
+ `restoreControl` floats in the Workbench's bottom corner, which the bottom
1074
+ drawer's column reaches once the rail collapses, so with the drawer expanded
1075
+ the control sat on the drawer's content. An expanded inline drawer now
1076
+ publishes its top edge as a CSS anchor scoped to its Workbench, and the
1077
+ rail's control floats the restore inset above it, returning to the corner
1078
+ when the drawer collapses. An overlay drawer (static or `responsiveOverlayAt`)
1079
+ still covers the control as every open overlay does, a nested Workbench's
1080
+ drawer moves only its own rails' controls, and engines without anchor
1081
+ positioning keep the corner.
1082
+
1083
+ - **A collapsed `@kerfjs/ui` `Workbench` panel's content leaves the Tab order.**
1084
+ A collapsed rail or bottom drawer kept its content in the DOM, only slid out
1085
+ and clipped, so Tab still reached its controls; focusing one (at a narrow
1086
+ width, the collapsed inspector's or output drawer's own Hide control)
1087
+ scrolled the clipped panel and slid its hidden content back over the work
1088
+ area. The panel's content wrapper now renders `inert` while `collapsed`, with
1089
+ no wiring needed, so neither Tab nor assistive technology reaches it; the
1090
+ slide-out still animates and the `restoreControl`, which lives outside the
1091
+ content, stays reachable.
1092
+
1093
+ - **A `kerfjs/overlay` popover or tooltip in a transformed or filtered modal
1094
+ `<dialog>` now lands against its anchor.** A surface rendered into a
1095
+ dialog's `[data-kerf-overlay-host][data-morph-skip]` slot is
1096
+ `position: fixed`, and a dialog styled with `transform`, `filter`,
1097
+ `perspective`, `contain`, or `will-change` becomes the containing block for
1098
+ fixed descendants, so the surface used to land offset by the dialog's
1099
+ position (and scale). `positionAnchored` (and so `autoReposition`,
1100
+ `popover()`, and `tooltip()`) now measures the element's fixed-position
1101
+ containing block with a transient 1px probe and maps its viewport
1102
+ coordinates into it, compensating translation and scale. Rotated or skewed
1103
+ containing blocks are not compensated.
1104
+ - **The `@kerfjs/ui` UX catalog demonstrates a standalone restore control.**
1105
+ The CollapsiblePanel route gains a "Restore control" example: a bottom
1106
+ drawer that collapses to a `FloatingToolbar` `restoreControl` in its frame's
1107
+ corner, wired with `wireSidebar` so the control restores the drawer and
1108
+ focus moves between it and the drawer's own hide toggle.
1109
+
1110
+ - **A `@kerfjs/ui` collapsed rail's restore control no longer covers an open
1111
+ bottom drawer beside it.** When a `CollapsiblePanel` or `ResizableRegion`
1112
+ with a `restoreControl` shared its container with an expanded bottom
1113
+ drawer — a bottom `CollapsiblePanel` or bottom `ResizableRegion` — as a
1114
+ sibling or inside the app's work-area column, the control floated in the
1115
+ container's bottom corner, on top of the drawer. It now floats the same
1116
+ inset above the drawer's top edge, follows the drawer as it resizes, and
1117
+ returns to the corner when the drawer collapses. A drawer nested deeper,
1118
+ inside the work area's own content, does not move it. This uses CSS anchor
1119
+ positioning; engines without it keep the container corner.
1120
+
1121
+ - **An open `@kerfjs/ui` `CollapsiblePanel` or `ResizableRegion` overlay now
1122
+ covers other panels' restore controls, as a `Workbench` overlay does.** A
1123
+ collapsed panel's `restoreControl` stacked at z-index 42, above
1124
+ `wireSidebar`'s compact overlay and its backdrop and above an overlay
1125
+ `ResizableRegion`, so it floated over an open overlay and stayed clickable
1126
+ behind the overlay's focus trap. `--kui-collapsible-panel-restore-z` now
1127
+ defaults to two less than `--kui-collapsible-panel-overlay-z` (38),
1128
+ `--kui-resizable-region-restore-z` to two less than
1129
+ `--kui-resizable-region-overlay-z` (39), and the compact backdrop's
1130
+ `--kui-collapsible-panel-backdrop-z` to one less than the overlay (still
1131
+ 39), so an app that moves the overlay z-index keeps the order. When the
1132
+ overlay closes, the control shows and works again.
1133
+ - **An open `@kerfjs/ui` `Workbench` overlay now covers other panels'
1134
+ restore controls.** Restore controls stacked above every overlay, so a
1135
+ collapsed drawer's corner restore control floated over an open right-rail
1136
+ overlay's content. An open overlay is now the top layer, like a sheet over
1137
+ the view beneath it: `--kui-workbench-restore-z` defaults to two less than
1138
+ `--kui-workbench-overlay-z` (39 by default, previously 42), below the
1139
+ drawer and rail overlays. When the overlay closes, the control shows and
1140
+ works again; a collapsed overlay passes pointer events through to it.
1141
+ - **`@kerfjs/ui` `wireWorkbench` overlay panels now take focus and keep Tab
1142
+ inside them,** like `wireSidebar`'s compact overlay (the ARIA dialog
1143
+ pattern). An open overlay covers the work area, yet focus stayed on the
1144
+ control that opened it and Tab walked through the covered editor. A wired
1145
+ panel that opens while it presents as an overlay now focuses its first
1146
+ focusable control (usually the close control in its header); while it is
1147
+ open, Tab and Shift+Tab cycle through its controls, and Escape, its own
1148
+ close control, or an outside press leaves it, returning focus to the
1149
+ opener. Panels opening inline never move focus. Focus returned on close
1150
+ also skips an opener inside another panel that has closed since.
1151
+ `dismissOverlays: false` leaves all of this to the app.
1152
+ - **`@kerfjs/ui` `Workbench` keeps its work-area minimum beside fixed rails
1153
+ and above the bottom drawer.** `mainMinSize` used to apply only beside a
1154
+ resizable rail, so two fixed 280px rails on a ~730px Workbench left the
1155
+ editor 174px; now every inline rail, fixed or resizable, shrinks in
1156
+ proportion (its content following the shown width) to keep the work area
1157
+ `mainMinSize` px wide. A new `mainMinHeight` prop (default 120, `0` turns it
1158
+ off) is the drawer's counterpart: an inline drawer, fixed or resizable,
1159
+ shrinks when the Workbench gets shorter, and `wireWorkbench` stops a
1160
+ resizable drawer where the work area would drop below that height (reported
1161
+ in `aria-valuemax`) instead of only at the drawer's own `max`.
1162
+ - **A `@kerfjs/ui` Workbench rail squeezed below its minimum now reports the
1163
+ width it shows.** When the Workbench was too narrow for a rail's `min`
1164
+ beside the work-area minimum, the rail showed about 172px while its
1165
+ separator still announced `aria-valuenow="180"`. The separator now reports
1166
+ the shown width, with `aria-valuemin` and `aria-valuemax` pinned to it: the
1167
+ WAI-ARIA window splitter's value is the separator's actual position and must
1168
+ sit inside its range, and the separator cannot move a rail the container
1169
+ holds there. Resizing still commits no less than `min`, and the configured
1170
+ range returns once there is room. A `ResizableRegion` a parent clamps below
1171
+ its `min` reports the same way.
1172
+ - **`KUI-L018` (`kerf-ui-analyze` / `kerf-ui-doctor`) now measures contrast and
1173
+ follows the cascade.** When a `--wa-color-{tone}-fill-loud` override and its
1174
+ `on-loud` are both literal colors (including `light-dark()`), a pair below
1175
+ 4.5:1 in either color scheme is reported with its ratio, and an unpaired
1176
+ literal fill that still clears 4.5:1 against the theme's default `on-loud`
1177
+ is no longer reported. An `on-loud` set in an ancestor scope (`:root`, or the
1178
+ leading selector of a descendant chain) or in another stylesheet the same
1179
+ entry loads now counts as the pair.
1180
+ - **`kerf-component-catalog` lets a private application declare components
1181
+ without a fake `exports` map.** In a private package
1182
+ (`"private": true`), a component's `publicExports` items may omit `subpath`; the generator
1183
+ verifies each name against the component's `source` file instead of
1184
+ requiring a `./dist/<path>.js` export the bundled app never emits. Publishable
1185
+ packages still need every subpath in `exports`. The v2 catalog schema makes
1186
+ `subpath` optional to match, and the `kerfjs/ui-composition` rule resolves a
1187
+ subpath-less export only by its source file.
1188
+ - **`kerf-ui-doctor` and the `kerfjs/ui-public-boundaries` ESLint rule no
1189
+ longer flag the documented `class="kui-app-root"` as KUI-L101 private
1190
+ anatomy.** The `@kerfjs/ui` component catalog now has a `document-baseline`
1191
+ composition entry for `@kerfjs/ui/document.css` that lists `kui-app-root`
1192
+ as its public class, with a catalog demo route showing a `SplitView` filling
1193
+ the root's definite height.
1194
+ - The `@kerfjs/ui` heading guidance (`ai/skill.md`, `llms.txt`,
1195
+ `docs/component-selection.md`, `docs/layout.md`) now sizes a panel title to
1196
+ its track: extra-large `ToolbarText` stays the title size for a page, view,
1197
+ dialog, or pane that holds it, while a narrow peripheral rail or drawer
1198
+ names itself with the default size, since extra-large truncated "Inspector"
1199
+ to "Ins…" in a 160px Workbench rail.
1200
+ - **A `@kerfjs/ui` `Workbench` panel open when `wireWorkbench` wired up no
1201
+ longer drops focus to the page when it closes from inside.** Such a panel
1202
+ had no recorded opener, so without a restore control closing it from its
1203
+ own header left focus on `<body>`. Focus now falls back to a control
1204
+ outside the panel whose `aria-controls` names it (or an element inside it).
1205
+ Each panel renders a stable `id` for that — `<id>-left-rail`,
1206
+ `<id>-right-rail`, and `<id>-bottom-drawer` — so a toggle can name the
1207
+ panel it controls.
1208
+ - **`@kerfjs/ui` `wireWorkbench` keeps overlay panels exclusive.** At a narrow
1209
+ width two overlay rails could both be open and, together wider than the
1210
+ Workbench, the right rail covered the left rail's own close control. Opening
1211
+ a panel while it presents as an overlay now closes every other open overlay
1212
+ panel, the bottom drawer included, like `wireSidebar`'s compact overlays;
1213
+ panels presenting inline are never closed by it. New option
1214
+ `exclusiveOverlays` (default `true`); pass `false` to let overlays stay open
1215
+ together.
1216
+ - **A `@kerfjs/ui` `CollapsiblePanel` or `ResizableRegion` restore control
1217
+ stays inside its container.** `restoreControl` was `position: fixed` to the
1218
+ viewport, so a panel or region embedded in a page floated it over unrelated
1219
+ page content. It now floats in the `restorePosition` corner of the element
1220
+ that holds the panel (or region), scrolls with it, and is inset by the
1221
+ component's restore inset plus the unsafe area of the edges that container
1222
+ reaches, so a full-viewport shell still places it clear of them. While the
1223
+ control is shown, that container isolates its stacking and becomes its
1224
+ containing block; an app that positions the container itself keeps its own
1225
+ `position`. As in `Workbench`, the corner owns the inset, so a
1226
+ `FloatingToolbar` hosting the control no longer doubles it.
1227
+ - The `@kerfjs/ui` UX catalog's Workbench page gains a responsive overlay
1228
+ drawer example: an output drawer with `responsiveOverlayAt: "narrow"` that
1229
+ takes its own track beside a wide editor and becomes a transient overlay
1230
+ over the editor's bottom once the Workbench is 704px or narrower, starting
1231
+ hidden there and closing from its own header, on Escape, or on a press
1232
+ outside it. The Workbench guide shows the pattern.
1233
+ - A `@kerfjs/ui` `Workbench` now stacks overlay rails above an overlay bottom
1234
+ drawer. With the drawer and a rail both presented as overlays, their order
1235
+ followed the markup, so the drawer covered the left rail while the right
1236
+ rail covered the drawer. Both rails now stack above the drawer, mirroring
1237
+ the inline layout where rails span the full height beside the drawer's
1238
+ column, and the right rail stacks above the left; restore controls stay
1239
+ above every overlay. The drawer's overlay z-index is now one less than
1240
+ `--kui-workbench-overlay-z`.
1241
+ - **A `@kerfjs/ui` `Workbench` overlay rail can always be closed from inside
1242
+ itself.** At a narrow width, the UX catalog's inspector overlay covered the
1243
+ editor toolbar's "Hide inspector" toggle, leaving only Escape or an outside
1244
+ press to close it. The catalog's resizable rails now carry their own close
1245
+ control in their `Pane` header, and the Workbench guide recommends the same
1246
+ for any panel that may present as an overlay. `wireWorkbench` now returns
1247
+ focus stranded in a panel however the panel closed — including through the
1248
+ app's own control inside it, and for inline panels — to the control that
1249
+ opened it, else to its restore control; before, only an Escape, an outside
1250
+ press, or a breakpoint collapse returned it.
1251
+ - **A `@kerfjs/ui` `Workbench` restore control stays inside its Workbench.**
1252
+ `restoreControl` was `position: fixed` to the viewport, so an embedded
1253
+ Workbench (the UX catalog's, for one) floated it over the page footer. It is
1254
+ now anchored to a corner of the Workbench itself — the drawer's in the
1255
+ corner of the work-area column, so it never covers an expanded rail — inset
1256
+ by `--kui-workbench-restore-inset` plus the unsafe area of the edges that
1257
+ corner reaches, and it scrolls with the Workbench. The Workbench now
1258
+ isolates its own stacking, so its overlays and restore controls never stack
1259
+ against the page around it. A `FloatingToolbar` can host the control
1260
+ without doubling the inset: the corner owns it, and `FloatingToolbar` now
1261
+ honors `--kui-floating-toolbar-inset` set on an ancestor as well as on
1262
+ itself. The catalog's collapsed console restores from such a toolbar.
1263
+ - **`@kerfjs/ui` `wireWorkbench` makes overlay panels transient.** A rail
1264
+ whose `responsiveOverlayAt` breakpoint applied stayed expanded over the
1265
+ editor until the user found its toggle. Pass a panel's app-owned
1266
+ `collapsed` signal (`panels: { leftRail: { collapsed } }`; `size` is now
1267
+ optional, so a panel need not be resizable) and, like `wireSidebar`'s
1268
+ compact overlay, the wiring collapses it when the breakpoint begins to
1269
+ apply (without the collapse motion) and gives back its inline state when
1270
+ the breakpoint stops applying or the wiring is disposed. An open overlay
1271
+ panel, responsive or `presentation: "overlay"`, closes on Escape or on a
1272
+ press that starts and ends outside it, and focus stranded inside returns to
1273
+ the control that opened it, else to its restore control. It is on by
1274
+ default for panels given `collapsed`; `dismissOverlays: false` turns it
1275
+ off. The UX catalog's resizable example wires its rails this way.
1276
+ - A `@kerfjs/ui` `Workbench` rail with `presentation: "overlay"` now renders
1277
+ exactly its size. Out of flow, it sized to its content plus its 1px
1278
+ separator border, so a 280px rail measured 281px; it now takes its size
1279
+ explicitly, as the responsive overlays do. Rails and the drawer are also
1280
+ `border-box` now, so a panel's `size` is its whole track, border included,
1281
+ even in an app without a global `box-sizing` reset, and overlay content
1282
+ fills the panel inside its separator instead of painting over it.
1283
+ - **A `@kerfjs/ui` `Workbench` bottom drawer with `presentation: "overlay"`
1284
+ is visible again.** Its content is positioned against the drawer's bottom
1285
+ edge, so the out-of-flow overlay box had no height of its own and collapsed
1286
+ to its 1px border. An overlay drawer now takes its configured height (220px
1287
+ by default, or its `size`), covers the bottom of the work-area column, and
1288
+ clamps its content to the overlay maximum height. The drawer also accepts
1289
+ `responsiveOverlayAt: "narrow" | "compact"` like the rails: below that
1290
+ Workbench width it presents as the same overlay, without a separator and
1291
+ never resizable, while the work area takes the full height and keeps its
1292
+ bottom safe-area inset beneath it.
1293
+ - The bundled AI assistant configs (`ai/skill.md`, `ai/cursorrules`) now
1294
+ explain the `data-kerf-overlay-host` opt-in next to the one-`mount()`-per-root
1295
+ rule: a modal `<dialog>` of your own that anchors kerf popovers or tooltips
1296
+ needs `<div data-kerf-overlay-host data-morph-skip></div>`, the one allowed
1297
+ nested-mount boundary. The `kerfjs/ai-assistant-configs` lint rule reports
1298
+ installed copies as stale until they are refreshed.
1299
+ - The opt-in `rebuiltListeners` dev warning no longer reports a popover or
1300
+ tooltip closing inside a modal dialog's host slot. Removals inside a
1301
+ `[data-kerf-overlay-host][data-morph-skip]` slot belong to the surface's own
1302
+ mount, so the enclosing mount now ignores them, as its re-renders already
1303
+ did. A listener lost to a re-render inside the surface is still reported,
1304
+ once.
1305
+ - **A popover or tooltip in a modal `<dialog>` no longer lingers after the
1306
+ dialog is removed when its anchor was moved out first.** A surface rendered
1307
+ into a dialog's host slot left with the dialog, but because its anchor was
1308
+ still in the document, kerf kept it registered as open, and a tooltip on
1309
+ that anchor never showed again. A slot-hosted surface now closes itself
1310
+ (without calling `onDismiss`) when it leaves the document, and the tooltip
1311
+ shows again on the next hover or focus.
1312
+ - **A popover inside a modal `<dialog>` is reachable by Tab in Safari on
1313
+ macOS.** A popover or tooltip rendered into a dialog's host slot follows the
1314
+ dialog's own Tab order, and WebKit on macOS skips buttons and links there
1315
+ unless the system keyboard-navigation setting is on. kerf now gives each
1316
+ implicitly tabbable control in a slot-hosted surface an explicit
1317
+ `tabindex="0"` when it opens and again before each Tab keypress, the same
1318
+ way its focus trap already did. Authored tabindexes, disabled controls, and
1319
+ surfaces outside a slot are left alone.
1320
+ - `@kerfjs/ui` `Workbench` rails gain `responsiveOverlayAt: "narrow" |
1321
+ "compact"`, mirroring `ResizableRegion`'s `responsiveFillAt`: below the
1322
+ Workbench's own 704px (`narrow`) or 448px (`compact`) width the rail
1323
+ presents as an overlay over the work area, without a separator and never
1324
+ resizable, and inline above it — the CSS decides, so apps no longer derive
1325
+ `presentation` from a `deviceClass()` check. The bottom drawer ignores it.
1326
+ A collapsed overlay rail (with `presentation: "overlay"` too) now drops its
1327
+ surface and shadow instead of leaving an empty panel-sized box over the work
1328
+ area. The UX catalog's resizable example uses it and now renders at phone
1329
+ widths, and its collapsed console's restore control is a `ToolbarControlGroup`
1330
+ toggle that actually restores the console.
1331
+ - `@kerfjs/ui` `Workbench` keeps a minimum work area beside resizable rails.
1332
+ Two rails dragged near their 480px maximum on a ~1024px Workbench used to
1333
+ leave the editor a sliver; now the work area keeps at least `mainMinSize`
1334
+ px (a new `Workbench` prop, default 320, `0` turns it off). `wireWorkbench`
1335
+ stops drag and keyboard resizing where the work area would drop below it
1336
+ (and reports that reachable maximum in `aria-valuemax`), and when the
1337
+ Workbench narrows, resizable rails shrink in proportion to their sizes, with
1338
+ their content following the shown width, instead of squeezing the work
1339
+ area. Workbenches without a resizable rail render exactly as before. The UX
1340
+ catalog's resizable example adds a resizable inspector to show it.
1341
+ - `@kerfjs/ui`'s static analyzer (`kerf-ui-analyze`, and the analyzer stage of
1342
+ `kerf-ui-doctor`) reports a new review finding, `KUI-L018`, when a
1343
+ stylesheet overrides a loud tone fill (`--wa-color-{tone}-fill-loud` for
1344
+ neutral, brand, success, warning, danger, or pop) without setting the
1345
+ matching `--wa-color-{tone}-on-loud` in the same scope. The theme pairs each
1346
+ loud fill with a foreground that clears WCAG AA, and an unpaired override
1347
+ keeps that foreground on a fill it was never checked against. The pair must
1348
+ share a selector (after resolving nesting and splitting selector lists) and
1349
+ at-rule scope in the same stylesheet, in one rule or separate ones; another
1350
+ tone's `on-loud` does not count. It is a review finding because it checks
1351
+ the pairing, not the contrast; pass `--fail-on-review` to gate it. The
1352
+ `webawesome-theme.md` customization example now sets both halves of each
1353
+ pair.
1354
+ - **Tooltips and popovers also stay visible when your own code opens a modal
1355
+ `<dialog>`.** `kerfjs/overlay` already moved every open tooltip and popover
1356
+ above a modal dialog it opened itself, but a dialog opened with a bare
1357
+ `dialog.showModal()` went unnoticed and left them inert beneath it. While
1358
+ any kerf surface is open, one capture-phase listener on the document now
1359
+ catches the dialog's `toggle` event and re-hosts them the same way, one task
1360
+ after the `showModal()` call. The listener is removed once nothing is open.
1361
+ An engine that fires no `toggle` event for `<dialog>` still misses such a
1362
+ dialog; open it through `overlay()` there.
1363
+ - **A popover inside a modal `<dialog>` is clickable.** Browsers make
1364
+ everything outside an open modal dialog inert, so a popover lifted above the
1365
+ dialog was visible but its buttons could not be clicked or focused, and
1366
+ kerf's one-mount-per-tree rule kept it from rendering inside a kerf-mounted
1367
+ dialog. `popover()` and `tooltip()` whose anchor sits inside an open modal
1368
+ `<dialog>` now render into the dialog's host slot, an element marked
1369
+ `data-kerf-overlay-host data-morph-skip`, so they are part of the modal and
1370
+ fully interactive: Tab reaches them, and Escape closes the popover before the
1371
+ dialog. A dialog `kerfjs/overlay` opened gets the slot automatically (an
1372
+ empty `display: contents` element added on first use). For your own
1373
+ `<dialog>`, add `<div data-kerf-overlay-host data-morph-skip></div>` to its
1374
+ markup, or pass `container`. The slot is the one place a second `mount()`
1375
+ may live inside a mounted tree: the enclosing mount's morph, nesting guard,
1376
+ and binding and `each()` marker scans all stop there. The `kerfjs/dev`
1377
+ `inert` warning now fires only when no slot was used.
1378
+ - `@kerfjs/ui` `Workbench` rails and the bottom drawer can be drag-resized,
1379
+ as an opt-in per panel. `WorkbenchPanel` gains `resizable: true | { min, max }`
1380
+ (off by default, so existing Workbenches render unchanged); a resizable panel
1381
+ gets a separator on its inner edge with the same focusable
1382
+ `role="separator"` contract, grip, and 20px hit target as a
1383
+ `ResizableRegion` handle. The new `@kerfjs/ui/wire-workbench` subpath's
1384
+ `wireWorkbench(root, { id, panels, deviceClass?, storage?, onResize? })`
1385
+ drives it: live pointer drags, arrow / Shift+arrow / Home / End keys,
1386
+ clamping to the limits, and commits to the app-owned size signals, with
1387
+ optional `storageKey` persistence. Collapsing keeps a panel's size, so it
1388
+ expands back to the size it had, and resizing is suspended on compact device
1389
+ classes and for overlay or hidden panels. The UX catalog's Workbench page
1390
+ adds a resizable example. `wireResizableRegions` now drives only handles that
1391
+ are direct children of a `ResizableRegion`, so it never double-drives a
1392
+ Workbench separator under the same root.
1393
+ - `@kerfjs/ui` elevation shadows follow the color scheme, with or without the
1394
+ optional `@kerfjs/ui/webawesome.css` theme. Kerf's theme layer set
1395
+ `--wa-shadow-s` / `-m` / `-l` to fixed light-mode shadows (14%, 11%, and 9%
1396
+ black), which all but vanished against the dark page, and the
1397
+ `--kui-shadow-s` / `--kui-shadow-l` foundation fallbacks copied those fixed
1398
+ values. The theme now pairs each light alpha with a stronger dark one (50%,
1399
+ 40%, 32%) through `light-dark()`, and the foundation fallbacks mirror them, so
1400
+ selected segments and app tabs, compact overlay panels, and Web Awesome
1401
+ surfaces that read `--wa-shadow-*` keep their elevation in dark mode. Light mode is unchanged. The browser parity test
1402
+ now compares every `--kui-shadow-*` token with and without the theme in both
1403
+ schemes.
1404
+ - `@kerfjs/ui` text on loud tone fills clears WCAG AA in both color schemes,
1405
+ with or without the optional `@kerfjs/ui/webawesome.css` theme. White
1406
+ `on-loud` text reached about 2.2:1 on the success fill, 3.5:1 on the light
1407
+ brand fill (`#0088ff`), and 3.6:1 on the danger fill (`#ff383c`) in both
1408
+ schemes. Kerf's theme layer and the foundation fallbacks now pair each
1409
+ fill with a foreground chosen to keep the tone's look: success keeps its
1410
+ bright green with dark text (`#1d1d1f`, 7.6:1), as warning already did; light
1411
+ brand and danger keep white text on a slightly deeper fill of the same hue
1412
+ (`#0075db`, 4.6:1; `#eb0005`, 4.6:1); dark danger keeps its fill with dark
1413
+ text (`#111113`, 5.3:1), as dark brand already did. Web Awesome accent
1414
+ buttons, badges, and checked controls pick this up, and accent `wa-badge`s now
1415
+ use their variant's `on-loud` foreground by default instead of a fixed dark
1416
+ color (`--kui-wa-badge-accent-foreground` still overrides it). The browser
1417
+ contrast test asserts every tone's `on-loud` over `fill-loud` pair, including
1418
+ pop and neutral, in both schemes with and without the theme.
1419
+ - `@kerfjs/ui` brand, success, and warning text clears WCAG AA on the lowered
1420
+ page background in light mode, with or without the optional
1421
+ `@kerfjs/ui/webawesome.css` theme. Their light `on-quiet` values (`#1e6ef4`,
1422
+ `#008932`, `#a16a00`) passed on the white surface but reached only about
1423
+ 4.1:1 on `#f2f2f7`. Kerf's theme layer and the foundation fallbacks now use
1424
+ the same hues a step darker: `#0c62f3` (4.64:1), `#007f2e` (4.62:1), and
1425
+ `#966200` (4.65:1) on the page, about 5.2:1 on white. Links, selected tabs,
1426
+ segmented and toolbar selections, and token-search accents that read
1427
+ `brand-on-quiet` darken with it. Dark mode is unchanged. The browser contrast
1428
+ test no longer skips light text over the page background.
1429
+ - `@kerfjs/ui` surfaces, links, and focus rings match the optional
1430
+ `@kerfjs/ui/webawesome.css` theme without it. Three foundation fallbacks
1431
+ pointed at a different token than the theme: `--kui-color-surface-raised`
1432
+ fell back to the surface color (the theme's dark raised surface is
1433
+ `#2c2c2e`), `--kui-color-text-link` fell back to the brand fill (`#0088ff`,
1434
+ about 3.5:1 on white) instead of the brand on-quiet text color, and the focus
1435
+ ring used the brand fill instead of the theme's focus color (`#007aff`). Each
1436
+ now falls back to the theme's value. The browser parity test now covers every
1437
+ `--kui-color-*` token the foundation declares, plus the focus ring, instead of
1438
+ only the tone tokens.
1439
+ - The `@kerfjs/ui` UX catalog's `StateBanner` route adds a placeholder with a
1440
+ badge for every tone, next to the existing badge-less placeholder, so visual
1441
+ review and captures show the solid badge's loading skeleton on each tone's
1442
+ fill. A browser test checks that each tone's badged placeholder is present.
1443
+ - `@kerfjs/ui` toned text stays readable in dark mode without the optional
1444
+ `@kerfjs/ui/webawesome.css` theme. The foundation's fallbacks for the
1445
+ success, warning, and danger `on-quiet` and `on-normal` tokens were single
1446
+ light-mode colors, so dark danger `Text` rendered dark red on a dark surface
1447
+ (under 3:1). Every tone token now falls back to the same light and dark
1448
+ pair the theme assigns, including `brand-on-loud` (dark text on the pale dark
1449
+ brand fill instead of white) and `neutral-border-quiet`. Light-mode colors
1450
+ are unchanged. A browser test bundles the package with and without the theme
1451
+ and asserts that every tone token resolves identically in both color schemes
1452
+ and that toned text clears WCAG AA in dark mode.
1453
+ - `@kerfjs/ui` danger text and badges are readable in dark mode, and every
1454
+ tone's `StateBanner` detail and quiet or outline `Badge` text now clears
1455
+ WCAG AA. The danger tone was the only one without an on-fill foreground: its
1456
+ solid badge fill and `StateBanner` text reused `danger-on-quiet`, a mid red
1457
+ in dark mode (banner detail 4.0:1, badge placeholder skeleton 2.25:1). A new
1458
+ `--kui-color-danger-on-fill` token (also accepted by `uiColor()`) matches the
1459
+ other tones' pale dark values, lifting the dark danger banner text to 8.2:1.
1460
+ The banner detail no longer dims to 78% opacity, which left the light info,
1461
+ success, warning, and danger details at 3.4 to 4.3:1; it reads as secondary
1462
+ through its smaller type, and the neutral detail uses the quiet text color.
1463
+ Quiet and outline badges use their tone's on-fill text, lifting light brand,
1464
+ success, and warning from about 4.1:1 to at least 5:1.
1465
+
1466
+ - A `@kerfjs/ui` placeholder skeleton inside a solid `Badge`, such as the
1467
+ badge of a `StateBanner` placeholder, is now clearly visible. The generic
1468
+ skeleton tint, 12% of the text color, almost disappeared on the badge's
1469
+ saturated fill in every tone. A solid badge now tints its skeleton from its
1470
+ own foreground color, which contrasts with the fill in every tone in light
1471
+ and dark mode. Quiet and outline badges keep the generic tint, which already
1472
+ reads on their pale fills. Geometry is unchanged.
1473
+ - The `@kerfjs/ui` SVG design templates render badges as filled pills: the
1474
+ `StateBanner` badge and the `ListHeader` and `List` count pills used to show
1475
+ as plain text, because the template build loaded only a hand-written list of
1476
+ stylesheets that omitted `badge.css`. The build now loads each component's
1477
+ full stylesheet set from its generated browser entry, the same set a
1478
+ consumer's browser import loads, so a nested component's CSS can no longer be
1479
+ missed. The `ListHeader` and `List` templates also pick up the label inset
1480
+ from `text.css`, so a header label now lines up with the list items below it,
1481
+ as it does in the live component.
1482
+ - A `@kerfjs/ui` `StateBanner` placeholder's badge sits at the live badge's
1483
+ height on every platform. With a skeleton instead of text, the badge took a
1484
+ baseline from the skeleton's bottom edge and sat about a pixel higher, by an
1485
+ amount that depended on the platform's font metrics.
1486
+ - A `@kerfjs/ui` `StateBanner` with a long, wrapping title keeps its badge
1487
+ one item gap after the title's last word, and its detail uses the full copy
1488
+ width. The wide flex layout shrank the title and detail together, which left
1489
+ a large gap before the badge and truncated the detail early. The title,
1490
+ badge, and detail now flow as inline text: the detail stays beside the title
1491
+ while it fits whole, and otherwise moves to its own line, truncating only at
1492
+ the full width. A wrapped title's lines stay evenly spaced.
1493
+ - A `@kerfjs/ui` `ListHeader` disclosure toggle disabled with `actionDisabled`
1494
+ is dimmed by the component itself, at the same tone as a disabled action
1495
+ button, and shows the `not-allowed` cursor. It used to rely on Web Awesome's
1496
+ native `button:disabled` dimming, so without `webawesome.css` it looked
1497
+ enabled. Disabled toggles and action buttons also no longer show hover
1498
+ feedback without `webawesome.css`.
1499
+ - A `@kerfjs/ui` `ListActionRow` placeholder keeps its `busy` spinner. The
1500
+ whole status line used to collapse into one skeleton, dropping the spinner
1501
+ the component draws from a known prop. Now only the `status` text becomes a
1502
+ skeleton, matching `ListItem`.
1503
+ - **A tooltip or popover already showing stays visible when a modal opens.**
1504
+ Lifting a surface above an open modal `<dialog>` was decided only when the
1505
+ surface opened, so a tooltip or popover already on screen when a modal
1506
+ dialog opened was left inert beneath it. Whenever `kerfjs/overlay` opens a
1507
+ modal `<dialog>` (a `native: true` modal, or one it lifted), it now moves
1508
+ every open tooltip and popover into the top layer above the new dialog. One
1509
+ that is already a `[popover]` is re-shown so it stacks on top. The existing
1510
+ `kerfjs/dev` warnings still report an engine without the Popover API, and a
1511
+ surface with controls the modal keeps inert. A modal `<dialog>` the app
1512
+ opens itself, outside kerf, is not observed.
1513
+ - Moved `mount()`'s list-binding lifecycle (turning list markers into
1514
+ bindings, first-render row-contract validation, orphan pruning) into its own
1515
+ internal module, so `mount.ts` holds only the render orchestration. No
1516
+ behavior or public API change.
1517
+ - A narrow (576px and below) `@kerfjs/ui` `StateBanner` keeps its badge
1518
+ sized to its content, one item gap after the title and on the title's
1519
+ baseline, as in the wide layout. The narrow grid stretched the badge across
1520
+ the free column into a wide pill that sat less than 1px from the title.
1521
+ - Hovering a `@kerfjs/ui` `ListActionRow` placeholder with
1522
+ `trailingActionVisibility="interaction"` no longer reveals its disabled
1523
+ trailing button. A placeholder is inert, so it keeps the live row's resting
1524
+ state, with the trailing action hidden until an interaction it cannot take.
1525
+ - A `@kerfjs/ui` `ListItem` placeholder keeps its `busy` spinner. The whole
1526
+ trailing slot used to collapse into one skeleton, dropping the spinner the
1527
+ component draws from a known prop and shifting the label's width. Now only
1528
+ the values in that slot become skeletons: `status` text in its status line
1529
+ and author-supplied `trailing` content (always a value, since the component
1530
+ cannot tell a chevron from a count) each get their own.
1531
+ - `@kerfjs/ui` placeholders keep the live disabled tone of a control the
1532
+ author disabled. A disabled `ListItem` or `ListActionRow`, a
1533
+ `trailingActionDisabled` button, a disabled `SegmentedControl` choice, or a
1534
+ `ListHeader` with `actionDisabled` rendered at full opacity as a
1535
+ placeholder, because the placeholder reset could not tell its own
1536
+ `disabled` attribute from the author's. Those controls now carry a
1537
+ component-owned `data-kui-disabled="true"` marker whenever their disabled
1538
+ prop is set; the disabled tone keys on it and the placeholder reset skips
1539
+ it. The marker is reserved: the extension-attribute props cannot set it.
1540
+ - `@kerfjs/ui` adds a browser test that renders every placeholder-capable
1541
+ component live and as a placeholder from identical props and diffs each
1542
+ element's geometry and computed chrome, so any future drift outside a value
1543
+ slot fails. It caught one more: a `ListActionRow` placeholder's `status`
1544
+ line was 3.5px shorter than the live line; the status line now always
1545
+ keeps a full line height.
1546
+ - `@kerfjs/ui`'s `Select` placeholder hint now matches the live hint: 14px
1547
+ text at a 1.6 line height, 7px below the box. It was 12px / 1.4 with a 4px
1548
+ gap, so a placeholder with a `hint` was 8.6px shorter than the loaded
1549
+ control and the content below it jumped when loading finished.
1550
+ - `@kerfjs/ui`'s `StateBanner` placeholder adds a detail skeleton only when
1551
+ `detail` is set, matching the live banner. Previously every placeholder
1552
+ showed a detail line, so a banner without one changed shape when loading
1553
+ finished. Pass the banner's real `title` and `detail` with `placeholder`;
1554
+ an empty `detail` now means no detail line.
1555
+ - `@kerfjs/ui`'s `AppTab` placeholder pill is no longer dimmed to 50% with a
1556
+ `not-allowed` cursor when `webawesome.css` is loaded; it keeps the live
1557
+ tab's tone, and only its name is a skeleton.
1558
+ - `@kerfjs/ui`'s `ListHeader` placeholder no longer dims its action button
1559
+ (48%) or, in `toggle` mode, the whole title and label (Web Awesome's native
1560
+ 50%), and its inert action and toggle give no hover feedback. A live
1561
+ `actionDisabled` header still dims its action.
1562
+ - `@kerfjs/ui`'s `SegmentedControl` placeholder now keeps its choices. The
1563
+ choices are known; only the selection is unknown, so a placeholder renders
1564
+ every choice's live label or icon at its live size, selects none, and stays
1565
+ inert. Previously every segment became a 4em text skeleton (widening icon
1566
+ segments from 40px to 60px) and was dimmed to 45%.
1567
+ - `@kerfjs/ui`'s `ListItem` and `ListActionRow` placeholders now match the
1568
+ live row. They are no longer dimmed (the rows' own `:disabled` tone and
1569
+ hover highlight skip placeholders), and a row with a `description` (or a
1570
+ `ListActionRow` `status`) keeps that line as a second, smaller skeleton, so
1571
+ the row keeps its live height instead of collapsing to one line. The
1572
+ `ListActionRow` icon placeholder is an 18px block centered in its icon slot,
1573
+ like `ListItem`'s.
1574
+ - `@kerfjs/ui` placeholders no longer look disabled. A component's
1575
+ `placeholder` keeps its controls `disabled` (inert and out of the keyboard
1576
+ order), but `skeleton.css` now neutralizes generic disabled chrome under
1577
+ `[data-placeholder="true"]`: full opacity and the default cursor instead of
1578
+ Web Awesome's native `button:disabled` dimming to 50% with a `not-allowed`
1579
+ cursor. A component's own opacity (a control hidden until interaction) still
1580
+ applies.
1581
+ - `toElement('<image href="…" />')` now returns an SVG `<image>`. The HTML
1582
+ parser rewrites an orphan `<image>` to an HTML `<img>` before kerf sees the
1583
+ tag, so it previously came back as an XHTML `img` that no `<svg>` would
1584
+ render. kerf now recognizes a leading `<image` tag in the raw input
1585
+ (case-insensitive, after leading whitespace) and parses it through the SVG
1586
+ wrapper. A genuine `<img>` is unchanged.
1587
+ - `delegateActions()` (`kerfjs/actions`) dispatches only the table's own
1588
+ entries. It used to look the action up with a plain property read, so a
1589
+ `data-action` naming an inherited `Object.prototype` member resolved:
1590
+ `"__proto__"` or `"hasOwnProperty"` threw a `TypeError` from the listener,
1591
+ and `"toString"` or `"constructor"` silently ran the inherited function.
1592
+ Those values are now ignored like any other action absent from the table.
1593
+ An own entry with such a name (`{ toString: … }`) still dispatches.
1594
+ - `toElement()` now puts orphan camelCase SVG fragments in the SVG namespace.
1595
+ `toElement('<linearGradient>…</linearGradient>')`, and likewise `clipPath`,
1596
+ `radialGradient`, and `foreignObject`, previously returned an XHTML
1597
+ `LINEARGRADIENT` element that no `<svg>` would render, because the lookup
1598
+ compared the HTML parser's lowercased tag against camelCase names. The
1599
+ fragment is now wrapped in `<svg>` and XML-parsed like `<path>` or `<g>`,
1600
+ and the returned element keeps its camelCase `localName`.
1601
+ - **A tooltip or popover no longer outlives its anchor.** When the anchor left
1602
+ the document, most commonly because the modal containing it closed, the
1603
+ `tooltip()` or `popover()` stayed on screen: a removed element fires no
1604
+ `pointerleave` or `blur`. Both now watch their anchor with the same removal
1605
+ tracking as `kerfjs/attach`. The tooltip hides, and the popover closes
1606
+ without calling `onDismiss`, as soon as the anchor leaves the document.
1607
+ Moving the anchor within the document keeps them open. A tooltip show still
1608
+ pending when its anchor is removed never appears.
1609
+ - **Overlays opened over a modal `<dialog>` are visible and usable.** A browser
1610
+ makes everything outside an open modal dialog inert and paints a plain
1611
+ element beneath it, so a `tooltip()`, `popover()`, or `confirm()` opened
1612
+ from inside a `native: true` dialog without `native` itself was invisible and
1613
+ could not be used. While a modal `<dialog>` is open, `kerfjs/overlay` now
1614
+ hosts such a surface in the top layer anyway: a tooltip or popover becomes a
1615
+ `[popover]`, and a nested modal becomes its own `<dialog>`. The browser's
1616
+ default `[popover]` / `<dialog>` styles then apply, as they do with `native:
1617
+ true`. Browsers keep a popover outside the modal dialog inert even in the top
1618
+ layer, so a lifted popover with buttons is visible but not clickable. A new
1619
+ always-on `kerfjs/dev` warning reports that case, and an engine without the
1620
+ needed API.
1621
+ - **A tooltip shown inside a modal no longer disables the modal.** `kerfjs/overlay`
1622
+ used to treat the most recently opened overlay as the only one that could
1623
+ react to input, so a `tooltip()` (which handles no input) inside an open
1624
+ modal left Escape doing nothing and let Tab leave the dialog. Every overlay,
1625
+ native or fallback, now joins one open-order stack, and each Escape, backdrop
1626
+ click, outside click, or trapped Tab goes to the topmost surface that handles
1627
+ it. A modal still blocks everything beneath it, and a surface that handles
1628
+ nothing never intercepts. A popover that dismisses on Escape gets it before
1629
+ the modal beneath. One click that is both outside a popover and on a modal's
1630
+ backdrop now closes only the popover. A native `<dialog>` is arbitrated with
1631
+ the fallback surfaces: one Escape closes one surface, and a fallback modal
1632
+ beneath an open dialog no longer takes the dialog's Tab. Closing a lower
1633
+ overlay first leaves focus in the overlay that holds it. An overlay opened
1634
+ from inside it then restores focus to the closed overlay's opener, not a
1635
+ detached node.
1636
+ - `kerfjs` now exports the `EachOptions<T>` type (`{ cacheKey?, key? }`), the
1637
+ options-object form of `each()`'s third argument, so a helper that builds or
1638
+ forwards those options can import the shape instead of restating it:
1639
+ `import { type EachOptions } from "kerfjs"`. Type-only; no runtime or bundle
1640
+ change.
1641
+ - Documentation: the companion-utility reference now matches the code. The
1642
+ router's `match()` / `activeClass()` compare a literal path prefix, so
1643
+ `match('/users/:id')` is never true. Link interception still applies to
1644
+ `target="_self"`, the no-match `outlet()` is `null` with `params` `{}`,
1645
+ components receive `(params, route)`, and hash mode keeps `route.hash` at
1646
+ `''`. Invalid `bindList` configuration throws only `RangeError`. `popover()`
1647
+ defaults `className` to `'kerf-popover'`, and the toast region is a reused
1648
+ `.kerf-toasts` element when one exists. A `resource()` `cacheKey` applies
1649
+ only to `run(input, fetcher)`, and throttle's `flush()` leaves the cooldown
1650
+ running. `delegate()` / `delegateCapture()` throw on an invalid selector at
1651
+ registration. The reference also now lists the `kerfjs/jsx-dev-runtime`
1652
+ alias, the `DEV_HOOKS` export of `kerfjs/dev`, and `Signal#peek()`.
1653
+ - Documentation: `morph(liveRoot, template)` is now described as it behaves.
1654
+ It reconciles only the root's children and never touches the root's own
1655
+ attributes, so `template` describes the content inside the root (an
1656
+ `Element` template contributes its child nodes). The examples no longer
1657
+ nest a duplicate `<article>` inside the live one. An `each()` list parent is
1658
+ not a children-only short-circuit: the diff still walks it and skips only
1659
+ the owned rows. The user-agent-owned `open` exception covers custom elements
1660
+ as well as `<details>` / `<dialog>`, and a content-only row change inside an
1661
+ `each()` list keeps its DOM node (and focus) unless the row's top-level tag
1662
+ changes or the same render also inserts, removes, or moves rows.
1663
+ - Every opt-in `kerfjs/dev` warning now ends with a silence hint that leads
1664
+ with the browser-reachable switch, for example "Silence it with
1665
+ enableWarnings({ narrowSet: false }) from kerfjs/dev, or set
1666
+ KERF_DEV_WARN_NARROW_SET=0 (or unset it) under Node." Previously the
1667
+ messages named only the environment variable, which a browser cannot reach.
1668
+ The docs now teach `enableWarnings()` wherever they showed an env-only
1669
+ switch, and name all three creation-time boundaries: `signal()`
1670
+ (`untrackedSignals`), `effect()` (`delegateInEffect`), and `mount()`
1671
+ (`rebuiltListeners`) decide once when created, so anything created before
1672
+ the install or the opt-in stays uncovered.
1673
+ - The `kerfjs/dev` rebuilt-listeners warning (`rebuiltListeners` /
1674
+ `KERF_DEV_WARN_REBUILT_LISTENERS`) now dedups once per `mount()`, as
1675
+ documented. Previously the first warning silenced every other mount for
1676
+ the life of the process, so a second mount with the same bug stayed quiet.
1677
+ - `kerfjs/router`: a second router on the same document no longer diverges
1678
+ from the URL after a link click. The first router's interceptor navigates
1679
+ with `pushState`, which fires no `popstate`, so the second router ignored
1680
+ the already-handled click and kept the old route. An already-handled click
1681
+ now re-syncs the route from the URL; when nothing navigated, it is a no-op.
1682
+ One router per document remains the supported shape: a second router does
1683
+ not follow another router's programmatic `navigate()` (no `popstate` fires
1684
+ and routers do not broadcast to each other) until the next `popstate` or
1685
+ link click. This is now documented and pinned by a test.
1686
+ - `kerfjs/scope`: a disposed `Scope` handle no longer leaks registrations.
1687
+ Previously `add` / `mount` / `effect` / `delegate` on a handle after its
1688
+ `dispose()` (or from a disposer while `dispose()` ran) pushed into a list
1689
+ nothing would ever run, leaving the effect subscribed or the listener
1690
+ attached. Such a registration now tears down at once: `add(fn)` calls `fn`
1691
+ immediately, and the wrappers create and immediately dispose.
1692
+ - `bindList` measured-height virtualization no longer applies a stale
1693
+ scroll-anchor correction. A `setHeight()` report for a row above the viewport
1694
+ is now resolved when the animation frame runs: if the source removed the key,
1695
+ moved it below the fold, or the user scrolled the row into view first,
1696
+ `scrollTop` is left alone, and several reports for one key in a frame net
1697
+ out. Fixed-height windows also clamp to the source length, so a list that
1698
+ shrinks while scrolled deep no longer pads far past its content.
1699
+ - `create-kerf-component` accepts an explicit `boundaries.rootClass: null` as
1700
+ an author decision, as its schema always allowed. A wrapper whose root
1701
+ element belongs to another catalog (a `rendersAs` wrapper) no longer has to
1702
+ invent a class to satisfy the generator. An absent `rootClass` still fails
1703
+ as undecided.
1704
+ - `@kerfjs/ui`'s `Select` placeholder now matches the live control. The label
1705
+ uses the field-label style (uppercase, extra-small, quiet, inset to line up
1706
+ with the value), and the chevron is the same glyph at the same size.
1707
+ Previously the placeholder drew a larger, regular-weight label at a
1708
+ different inset and a different, smaller chevron. Only the value is a
1709
+ skeleton; the chrome no longer changes when loading finishes.
1710
+ - **Wrapper components can declare the cataloged root they render.** A
1711
+ `kerf.components.json` component may set `composition.rendersAs`, for
1712
+ example `["@kerfjs/ui:toolbar-control-group"]`: a list of the roots it
1713
+ renders, any one of them, or nothing. `kerf-component-catalog` copies it
1714
+ into the generated v2 entry and fails unless every key resolves to a
1715
+ cataloged entry. `eslint-plugin-kerfjs`'s `ui-composition` rule, and so
1716
+ `kerf-ui-doctor`, now loads the application and third-party catalogs a
1717
+ `.kerf-ui-profile.json` declares. It resolves their components by package
1718
+ subpath or source file and checks a wrapper as each declared root: a zone
1719
+ must accept every root, each root's parent contract applies where the
1720
+ wrapper is placed, and a wrapper counts as zero or one child. Undeclared
1721
+ wrappers keep reporting `KUI-L202`.
1722
+ - `@kerfjs/ui`'s `AppTab` adds `pending` for a known tab that is still
1723
+ opening. It keeps the name visible in the quiet text color and uses it as
1724
+ the tab's accessible name, so a trailing spinner's label is not folded in.
1725
+ Like `placeholder`, it disables select and close, turns off dragging, and
1726
+ sets `aria-busy`, and it shows the consumer's `trailing` spinner. It keeps
1727
+ the live tab's pill geometry, so it swaps in place without shifting the bar.
1728
+ Dormant tabs (placeholder or pending) no longer reveal their disabled close
1729
+ button on hover.
1730
+ - `@kerfjs/ui`'s `wireSidebar` moves focus into an opening panel without
1731
+ scrolling it. A plain `focus()` scrolled the panel's clipped content to the
1732
+ target while it was still sliding in, then the slide unwound that scroll:
1733
+ a bottom drawer's content jumped up past its top and drifted back. The
1734
+ collapsible-sidebar recipe also floats the drawer's expand toggle in a
1735
+ `FloatingToolbar` at the bottom edge the drawer opens from, and drops the
1736
+ divider under the drawer's toolbar.
1737
+ - `@kerfjs/ui`'s `Skeleton` takes `block` (default `false`). It lays the
1738
+ placeholder out as `display: block` instead of `inline-block`, for a
1739
+ skeleton that stands in for a box rather than text: no text line and no
1740
+ baseline gap of its own. `ListItem`'s placeholder icon uses it.
1741
+ - `@kerfjs/ui/css-values` adds `remify(px)`, the runtime twin of source
1742
+ CSS's `remify(<px>)`. It turns a pixel count into a rem `CssLength` against
1743
+ the fixed 16px baseline, so `remify(18)` is `1.125rem`. `ListItem`'s
1744
+ placeholder icon now uses it at 18px, and `Skeleton` no longer forces a
1745
+ 24px minimum width, so small placeholders keep the size they ask for.
1746
+ - `@kerfjs/ui/catalog`'s application-sized (`height: "app"`) example frame
1747
+ no longer loses taps in Safari/WebKit. The frame became the containing block
1748
+ for screen-fixed chrome through `contain: layout`. Combined with its clip,
1749
+ that made WebKit mis-hit-test any transformed element inside an open compact
1750
+ overlay. Every pressed `ListItem` scales, so taps on an overlay rail's items
1751
+ went to their section instead. The frame now uses an identity transform,
1752
+ which docks the same chrome without the bug and renders identically.
1753
+ - **Breaking (`@kerfjs/ui/catalog`):** `CatalogExample`'s viewport drops
1754
+ `height: "fill"` and `fillChildren`. The catalog lays examples out at their
1755
+ content height, so neither could give a specimen a definite height, and a
1756
+ child sized with `height: 100%` (a horizontal `ResizableRegion`, say)
1757
+ collapsed to its content. Use a fixed `height` (`short` through `app`) when
1758
+ a specimen must fill its frame. `minHeight` remains, documented as a minimum
1759
+ rather than a fill.
1760
+ - `create-kerf-component`'s `kerf.components.json` now accepts an optional
1761
+ `composition.wiring.stateAttributes` declaration (the wiring-owned state
1762
+ attributes `@kerfjs/ui`'s catalog now carries), so third-party
1763
+ component authors can list the `data-*` attributes their wiring helpers write
1764
+ (`{ name, on, helper, meaning }`). `kerf-component-catalog` passes them
1765
+ through to the generated `component-catalog-v2.json` and reports a non-`data-*`
1766
+ name, a duplicate name, an empty `on` or `meaning`, or a helper missing from
1767
+ `wiring.helpers` at its exact path.
1768
+ - `@kerfjs/ui`'s component catalog now declares **wiring-owned state
1769
+ attributes**. A new optional `wiring.stateAttributes` in
1770
+ `component-catalog-v2` (and per wiring item in v1) lists each `data-*`
1771
+ attribute a wiring helper writes at runtime, such as `data-handle-inset` and
1772
+ `data-resizing` from `wireResizableRegions`. For each one it records the
1773
+ element, the helper, and the meaning, so AI consumers and tools can tell
1774
+ them apart from attributes the app writes. Every first-party wiring helper
1775
+ is declared. `check:catalog` fails when a helper's declarations differ from
1776
+ what its source writes.
1777
+ - `@kerfjs/ui`'s `wireSidebar` no longer leaves focus inside a panel it just
1778
+ collapsed. When the collapse was triggered from a toggle inside the panel
1779
+ (the documented placement), focus now moves to that panel's toggle outside
1780
+ it, including one the app renders only once the panel is collapsed. The
1781
+ collapsible-sidebar recipe now shows one control per action: each panel's
1782
+ own collapse toggle while it is open, and the main header's expand toggle
1783
+ only while it is collapsed. Wide widths no longer show two rail toggles.
1784
+ - `@kerfjs/ui`'s `wireSidebar` keeps a compact overlay intact when the app
1785
+ re-renders the wired root without changing a panel. Previously, selecting a
1786
+ destination in an open compact rail morphed the root, which removed the
1787
+ overlay attributes and the backdrop, and the "overlay" rail fell back into
1788
+ the layout. The wire now re-applies its host attributes and backdrop after
1789
+ any such re-render, and the backdrop carries `data-morph-preserve`.
1790
+ - `@kerfjs/ui`'s `wireSidebar` now opens a compact overlay only on a user
1791
+ action. Wiring on a compact device, or crossing from a wide device class to
1792
+ a compact one, collapses every panel. The page is no longer covered by a
1793
+ backdrop and focus trap until dismissed. Crossing back to a wide class, or
1794
+ disposing the wire, restores each panel's remembered inline state.
1795
+ Persistence now stores only that inline choice, never an overlay's
1796
+ open/closed state. A new optional `WireSidebarPanel.inlineCollapsed`
1797
+ declares the inline default when an app seeds its signal from the device
1798
+ class, which avoids an open first frame on a compact device. The
1799
+ collapsible-sidebar recipe no longer opens its rail as a blocking overlay
1800
+ on load at compact widths.
1801
+ - `@kerfjs/ui`'s `ResizableRegion` handle keeps its hit target and focus ring
1802
+ whole at a clipping edge. When a parent clamps the region to its own edge
1803
+ (the catalog's 390px stage, an app shell with a `max-width` container),
1804
+ `wireResizableRegions` marks the region `data-handle-inset` and the handle
1805
+ moves inside it instead of overhanging the edge by 10px, where the frame cut
1806
+ its focus ring in half. The ring is now drawn inside the handle's box
1807
+ (`outline-offset: -3px`), so it is also no longer shaved where the region's
1808
+ ends meet a clipping edge.
1809
+ - `@kerfjs/ui`'s `wireResizableRegions` keeps a clamped separator's
1810
+ `aria-valuenow`/`aria-valuemax` current at rest. Previously, after a size was
1811
+ saved at a wider width, narrowing the parent or any unrelated re-render left
1812
+ the handle reporting the unclamped rendered size until it was next focused or
1813
+ resized. A `ResizeObserver` on each region and its parent and a
1814
+ `MutationObserver` for re-renders now re-clamp the report; the disposer
1815
+ disconnects both.
1816
+ - The `@kerfjs/ui` catalog's `height: "app"` example frame is now the
1817
+ containing block for screen-fixed chrome, so a compact `CollapsiblePanel`
1818
+ overlay, its backdrop, and restore controls dock to the frame's edges. The
1819
+ collapsible-sidebar recipe's compact rail was fixed to the page's left edge,
1820
+ 24px outside the inset frame, so its leading edge looked cut off at 390px.
1821
+ - The `CollapsiblePanel` compact-overlay backdrop now covers its whole
1822
+ containing block. As a `<button>`, it did not stretch between its insets and
1823
+ scrimmed only a 35px strip.
1824
+ - `@kerfjs/ui`'s `wireResizableRegions` now bounds a resize to the space the
1825
+ region's parent actually leaves. When a parent clamps the track (a
1826
+ `max-width` stage, a narrow container), pointer and keyboard resizing stop at
1827
+ the visible size, and the separator reports that size in `aria-valuenow` and
1828
+ its visible maximum in `aria-valuemax`. Previously a region resized to 420
1829
+ inside a 340px stage announced 420 and needed several key presses before the
1830
+ separator moved. Focusing the handle reports a stale larger committed size as
1831
+ the shown size without committing it.
1832
+ - The UI evaluator's `KUI-B050` target-size rule now accepts a 44×36 target
1833
+ inside compact density (`[data-density="compact"]`), matching the 36px
1834
+ compact row height; standard density still requires 44×44, and the width
1835
+ floor is unchanged.
1836
+ - Fixed `kerf-ui-analyze` and the `eslint-plugin-kerfjs` UI rules treating
1837
+ every name in a catalog entry's `publicExports` as that entry's component,
1838
+ so a helper such as `uiColor` or `flex` called with an object whose keys
1839
+ matched a contract path (`uiColor({ choices: [...] })`) was checked as a
1840
+ `Select` or `List` call. Only component exports, whose names start with an
1841
+ uppercase letter as JSX component tags do, now resolve to an entry; the
1842
+ `@kerfjs/ui` catalog check fails if a cataloged `SafeHtml` render function
1843
+ is lowercase or any other runtime export is uppercase.
1844
+ - The `@kerfjs/ui` component catalog's `ToolbarText` entry now lists every
1845
+ token its `xlarge` size reads (`--kui-color-text`, `--kui-font-l`,
1846
+ `--kui-font-2xl`) in `publicTokens`; previously it named only the tokens of
1847
+ the smaller sizes.
1848
+ - The `@kerfjs/ui` browser evaluator's target-size check (`KUI-B050`) now
1849
+ measures the pointer-reachable area instead of only the border box: an
1850
+ undersized control is hit-tested outward from its center, so a transparent
1851
+ `::before`/`::after` hit layer (such as the `ListHeader` action's) counts,
1852
+ while `pointer-events: none` decoration, clipping, and overlapping
1853
+ neighbors do not. Diagnostic evidence now carries the measured size plus
1854
+ the border `box`.
1855
+ - Fixed the `eslint-plugin-kerfjs` `ui-css-values` messages (`KUI-L013`,
1856
+ `KUI-L014`, `KUI-L017`) listing suggested helpers with unbalanced backticks
1857
+ (``use `uiColor() or `foregroundColorVar().``); each helper is now wrapped
1858
+ as `` `uiColor()` ``.
1859
+ - Fixed `kerf-ui-analyze` skipping the CSS-value contracts of `@kerfjs/ui`
1860
+ `Select` and `Skeleton`. Its component lookup was keyed by display name, so
1861
+ the same-named Web Awesome catalog entries (`wa-select`, `wa-skeleton`) —
1862
+ which carry no CSS-value props — shadowed them, and a wrong-grammar helper
1863
+ such as `colorVar()` for `SelectChoice.color` went unreported although the
1864
+ ESLint rule caught it. Entries now resolve by `package:id`, using the
1865
+ selection catalog's public exports and import subpaths.
1866
+ - Fixed `@kerfjs/ui` monospace text rendering in the generic face (Courier on
1867
+ macOS Chromium) whenever Web Awesome was loaded: `--kui-font-mono` deferred to
1868
+ Web Awesome's `ui-monospace, monospace`, which Chromium does not resolve, and
1869
+ the trailing generic `monospace` kept Kerf's named fallbacks from ever
1870
+ applying. `--kui-font-mono` is now Kerf's own stack (`ui-monospace,
1871
+ SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace`),
1872
+ and the Web Awesome theme points `--wa-font-family-code` at it so Kerf `Text
1873
+ font="monospace"` and Web Awesome code text match.
1874
+ - The `@kerfjs/ui` Web Awesome theme now gives default-size single-line
1875
+ controls Kerf's 44px control height, so a plain `wa-input` or `wa-button`
1876
+ sits flush beside a Kerf `Select` instead of about 9px shorter. It sets
1877
+ `--wa-form-control-height` to `2.75em` (44px at the default size) on Button,
1878
+ Color Picker, Input, Known Date, Number Input, Radio Group, Select, Textarea,
1879
+ and Time Input; `size="large"` scales proportionally, `size="small"` keeps Web
1880
+ Awesome's compact height, and buttons inside a `ToolbarControlGroup` keep the
1881
+ group's 40px (32px compact) height. Tags, copy buttons, and toasts are
1882
+ unchanged.
1883
+ - Fixed an `@kerfjs/ui` `Select` menu drawing two separators when a choice with
1884
+ `separatorBefore` opened a separated group: the group's border plus a
1885
+ `wa-divider` left an empty band between the group title and its first
1886
+ choice. `separatorBefore` now only divides a choice from the previous choice
1887
+ in the same list; a group boundary is already the single separator.
1888
+ - Fixed `@kerfjs/ui` compact `ToolbarControlGroup` end highlights not being
1889
+ concentric with the group: controls sat 2px from the sides but 1px from the
1890
+ top and bottom. A compact group now has no padding, so its 32px controls sit
1891
+ on a 1px inset on every side with a radius of the group radius minus 1px
1892
+ (pill and rounded). In compact mixed groups the raised selection still paints
1893
+ over the border with the group's radius, neighboring controls still abut, and
1894
+ the trailing dropdown trigger keeps the 1px end inset instead of running onto
1895
+ the border. Compact groups are 2px narrower as a result.
1896
+ - Added `Toolbar.safeAreaEdges` to `@kerfjs/ui` so an app bar or bottom bar
1897
+ that sits at a screen edge, outside any `Pane` header or footer, can claim
1898
+ those edges for device safe areas. A toolbar there used to take only the
1899
+ inline edges an owner handed it, so the app-shell recipe's top bar never
1900
+ cleared the status area. Each claimed side pads by the routed
1901
+ `--kui-edge-inset-*` context, or the full device inset when nothing routes
1902
+ it, while the bar's surface and divider still paint through; the minimum
1903
+ height grows with the claimed block insets so a shrinking flex column keeps a
1904
+ whole control row clear. A claim inside a `Pane` header or footer adds
1905
+ nothing, since the pane already owns those edges. The app-shell recipe's app
1906
+ bar now claims the top edge and both sides.
1907
+ - Fixed `@kerfjs/ui` slide-motion `ResizableRegion` content overflowing its
1908
+ separator when a parent clamps the region. The expanded content kept the
1909
+ committed size even when a `max-width` or narrow container held the track
1910
+ smaller, so in the catalog at 390px a region resized to 420px showed text
1911
+ running past its handle. Expanded content now follows the region's actual
1912
+ track on both axes; the fixed expanded size still applies while collapsing, so
1913
+ the collapse keeps reading as a slide.
1914
+ - Fixed `@kerfjs/ui` `ResizableRegion` panes stopping at their content's
1915
+ height. The region's content now spans the whole region, like its separator,
1916
+ and a lone child (normally a `Pane`) fills it, so the app-shell recipe's
1917
+ navigation and inspector panes reach the bottom edge and scroll long content
1918
+ inside their own content slot instead of ending partway down the region.
1919
+ Several children still stack at their natural height.
1920
+ - Fixed `@kerfjs/ui` `Toolbar responsive="center-priority"` leaving an
1921
+ expanded center control at roughly half the row on narrow toolbars. The
1922
+ policy set the toolbar's own grid template inside a container query on the
1923
+ toolbar itself, which can never match, so the center kept only the first
1924
+ column track once the leading and trailing zones left. The dead rule is gone
1925
+ and the expanded center now spans every column track, filling the toolbar's
1926
+ content width at phone widths.
1927
+ - Fixed a nested `@kerfjs/ui` `List`, `Row`, or `Grid` inheriting its
1928
+ ancestor's `gap` and `flex`. Those props travel through private custom
1929
+ properties, which inherit, so a nested `List` without its own `gap` took the
1930
+ enclosing `List`'s gap (a heading unit inside a form got a 24px gap between
1931
+ its title and supporting line) and any nested instance without `flex` took the
1932
+ ancestor's flex growth. Every instance now resets its private values, so an
1933
+ omitted prop means that component's own default. The workspace-header recipe's
1934
+ breadcrumb now sits directly above its title and the List demo's section rows
1935
+ sit flush under their header, as intended.
1936
+ - Fixed `@kerfjs/ui` slide-motion `ResizableRegion` content staying at its
1937
+ previous width during a live resize until the app re-rendered the committed
1938
+ size. `wireResizableRegions` now applies the live size to the sliding content
1939
+ as well as the track, for pointer drags and key presses, so the content fills
1940
+ the region (including a safe-area edge extent) throughout the drag and a later
1941
+ collapse still slides it out at the resized width.
1942
+ - Fixed the `@kerfjs/ui` `ListHeader` action's pointer target being only its
1943
+ fitted 36px square although the guidance promises 44px. A transparent hit
1944
+ layer now extends the target to at least 44 × 44 around the square without
1945
+ moving layout or enlarging the painted hover surface. Compact headers are
1946
+ 36px tall like the rest of the compact List family, so their action target is
1947
+ 44px wide and clamped to the header height, never reaching into a neighboring
1948
+ row.
1949
+ - Fixed re-renders closing open Web Awesome popups: the morph (and the keyed-list
1950
+ attribute fast path) no longer removes a live `open` attribute from custom
1951
+ elements (`wa-select`, `wa-dropdown`, `wa-dialog`, …) when the template omits
1952
+ it — the same user-agent-owned treatment `<details>` / `<dialog>` already
1953
+ had. A template can still set `open`; closing stays imperative.
1954
+ - The `@kerfjs/ui` composer-form recipe's supporting line now sits directly
1955
+ under its toolbar title instead of 24px lower, because a nested `List` no
1956
+ longer inherits the form's section gap (see the nested `List`/`Row`/`Grid`
1957
+ fix above). The guidance now
1958
+ also states that `ToolbarText` below `xlarge` is intentionally quiet identity
1959
+ text with no tone option. A normal-color subsection title is `Text` with an
1960
+ `h2`–`h6` variant, or `ListHeader` for a list or menu section.
1961
+ - Fixed `@kerfjs/ui` `ToolbarText size="small"` labels rendering in a
1962
+ monospace fallback face in Chromium. The small size used the code-font token,
1963
+ which resolves through Web Awesome's `ui-monospace, monospace` stack; Chromium
1964
+ does not recognize `ui-monospace`, so labels such as a pane's "Workspace" or
1965
+ "Ready" fell back to the generic monospace font. Every `ToolbarText` size now
1966
+ keeps the inherited UI sans stack.
1967
+ - Fixed `@kerfjs/ui` `ListInsetControl` not stretching its control as
1968
+ documented. Its `justify-content: stretch` is a no-op on a flex row, so a
1969
+ `Select`, `wa-input`, or `wa-button` inside stayed at its natural width. Each
1970
+ direct child now grows to fill the inset row (several children share it with
1971
+ the 8px gap) and may shrink below its intrinsic width instead of overflowing.
1972
+ - `@kerfjs/ui` `List`, `Row`, and `Grid` can now be the layout root of a
1973
+ fixed-height parent. New `fill` takes the parent's full height (use `flex`
1974
+ inside a flex layout as before), and new `rootAttributes` carries safe
1975
+ `data-*` metadata while the component's structural attributes stay
1976
+ protected. The app-shell and collapsible-sidebar recipes now use a filling
1977
+ List or Row root instead of a frame `Pane` whose scroll owner never scrolled;
1978
+ the collapsible sidebar's rail and drawer now reach the real frame edges, so
1979
+ they paint through safe areas as the layout model intends. The UX catalog
1980
+ adds an application-sized `height: "app"` (592px) specimen frame, and both
1981
+ recipes use it instead of the cramped 360px `tall` frame.
1982
+ - Fixed a `@kerfjs/ui` stacked `Toolbar` clipping its last trailing action at
1983
+ phone widths (the compact-toolbar recipe's "More" button at 390px). A
1984
+ toolbar's trailing zone now wraps whole control groups onto another
1985
+ trailing-aligned row under every `responsive` policy, and a
1986
+ `responsive="stack"` toolbar wraps its stacked center and trailing groups the
1987
+ same way, so no action is ever clipped or shrunk. Stacked zones also span the
1988
+ full toolbar width; they previously lost 16px to empty column tracks. New:
1989
+ `responsive="wrap"` keeps a heading on one row while everything fits and
1990
+ otherwise moves the trailing zone below a whole title instead of truncating
1991
+ it. The loading-inspector recipe uses it, so its narrow heading shows the
1992
+ whole record title instead of an ellipsis.
1993
+ - `@kerfjs/ui` layouts and `Pane` now handle device safe areas as one model.
1994
+ Pane and panel backgrounds, separators, and dividers paint through unsafe
1995
+ areas; content is padded only on the edges a region actually reaches, and
1996
+ block insets become scroll padding so content can scroll under the status bar
1997
+ or home indicator but always scroll clear. `Workbench` rails and drawer,
1998
+ `CollapsiblePanel`, and the `SplitView` list grow by the inset they dock to.
1999
+ A center pane picks up the inset when an adjacent rail collapses, and
2000
+ interior or nested edges are never double-inset. A header or footer `Toolbar`
2001
+ keeps its dividers edge to edge. New: `Pane.safeAreaEdges` (default all
2002
+ sides; `[]` opts out), the `--kui-safe-area-*` device-inset tokens (default
2003
+ `env(safe-area-inset-*)`), the inherited `--kui-edge-inset-*` edge context for
2004
+ app-owned layouts, and `--kui-resizable-region-edge-extent`. Nothing changes
2005
+ on devices or pages without safe-area insets.
2006
+ - Fixed a `@kerfjs/ui` `ListHeader` action sitting about 4px outside the
2007
+ trailing toolbar action of the Pane header above it. Its fitted action now
2008
+ centers in the same 44px slot a toolbar control occupies at the 8px inline
2009
+ margin, so trailing icon actions share one axis. The action's size, 18px
2010
+ visual, and hover square are unchanged.
2011
+ - **Breaking (`@kerfjs/ui`, 5.0 beta):** `SelectChoice.color` now accepts only
2012
+ a foreground color, the new `CssForegroundColor` brand from
2013
+ `@kerfjs/ui/css-values`. `uiColor()` returns it for foreground token names
2014
+ (the new `UiForegroundColorName`: the `*-on-*` roles, `text`, `text-quiet`,
2015
+ `text-link`, and the `*-text` aliases), and the new
2016
+ `foregroundColorVar('--app-icon-color', fallback?)` returns it for an
2017
+ application-owned property. The `--kui-color-success` / `warning` / `danger`
2018
+ / `pop` / `accent` compatibility aliases are quiet fills that read like
2019
+ foreground colors; colored with one, an icon rendered at about 1.2:1
2020
+ contrast. `uiColor('success')` and the other fill, border, and surface tokens
2021
+ still return a plain `CssColor` and no longer type-check as an icon color,
2022
+ and neither does a plain `colorVar()` value. Migrate
2023
+ `color: uiColor('success')` to `uiColor('success-on-quiet')` and
2024
+ `color: colorVar('--app-color')` to `foregroundColorVar('--app-color')`;
2025
+ `uiColor('…-on-…')` calls keep compiling unchanged. The catalog's CSS-value
2026
+ contract for `choices[].color` lists `uiColor` and `foregroundColorVar`, so
2027
+ `eslint-plugin-kerfjs` also reports `colorVar()` there (`KUI-L014`), and
2028
+ the source and packed consumer type contracts pin the rejected fills.
2029
+
2030
+ - Fixed an `@kerfjs/ui` icon-only `Select` beside other controls in one
2031
+ `ToolbarControlGroup`. A compact Select no longer eats 1px of the group's
2032
+ 4px gap on each side (the lone-trigger padding overlap now applies only when
2033
+ it is the group's only child), a compact rounded Select uses its siblings'
2034
+ item radius instead of the lone-slot radius, and the Select now shows the
2035
+ same hover pill as its sibling buttons (a lone Select lifts the whole group,
2036
+ like a popup-menu dropdown). In a group with per-control focus rings
2037
+ (`focusRing="control"`, the default) the Select paints the same ring as its
2038
+ siblings even when `focusRingOwner="group"` was set, which previously left it
2039
+ with no visible focus. A compact icon-only `<button>` is now a 32px circle,
2040
+ so a selected compact segment is no longer a 34x32 oval. The
2041
+ ToolbarControlGroup catalog adds a "Select beside actions" example.
2042
+
2043
+ - Changed the `@kerfjs/ui` icon-only `Select`
2044
+ (`selectedPresentation="icon-only"`) to keep its disclosure caret. The
2045
+ selected icon and caret now form one pill with the same geometry as a
2046
+ `ToolbarControlGroup` popup-menu `wa-dropdown` trigger: 8px inline padding on
2047
+ both sides, 12px from the icon to the caret, the same caret glyph size, and a
2048
+ 40px default or 32px compact control height, so the icon sits left of center
2049
+ to balance the caret. In a single `ToolbarControlGroup` the group now sizes to
2050
+ the trigger (a 70x44 default or 68x34 compact pill, wider than it is tall)
2051
+ with an even inset on every side and a concentric radius, for pill and
2052
+ rounded shapes, so the group-owned outline or halo ring follows the pill while
2053
+ focused and while the listbox is open. Before, a default-size trigger was a
2054
+ 32px square left-aligned in its 44px group, and the caret was hidden. The
2055
+ Select catalog adds a default-size "Toolbar icon" example beside the compact
2056
+ one.
2057
+
2058
+ - Fixed the `@kerfjs/ui` icon-only `Select` focus ring in a
2059
+ `ToolbarControlGroup` not matching the control: the compact trigger grew its
2060
+ group into a 36x34 oval and its disclosure caret overflowed the trailing edge.
2061
+ The group-owned ring now stays concentric with the trigger while focused and
2062
+ while the listbox is open. The Select demo's "Quiet" option icon also used the success fill alias
2063
+ as its color and was nearly invisible; it now uses `success-on-quiet`, and the
2064
+ `SelectChoice.color` guidance names foreground tokens.
2065
+
2066
+ - `@kerfjs/ui` UX catalog: the ten production recipes now compose Kerf
2067
+ components, layouts, and Web Awesome surfaces through their public
2068
+ configuration alone, with no route stylesheets, inline styles, or
2069
+ styling-only classes. The list-detail dialog is rebuilt from `DialogSurface`,
2070
+ the dialog's own labeled header and close control, `SplitView` (a full-screen
2071
+ list-to-detail drill-down on compact devices), aligned `ListHeader`s, a
2072
+ `ValueTable`, and footer actions. The desktop application shell switches to one
2073
+ pane at a time below desktop sizes through `deviceClass()`. Page and pane
2074
+ headings stack instead of hiding their titles at narrow widths, and supporting
2075
+ copy, content-item text, and loading skeletons sit on the shared 17px text
2076
+ inset. The catalog frames each recipe in a `CatalogExample` viewport that the
2077
+ recipe declares through an exported `presentation`, and recipe ownership notes
2078
+ render as that example's note. `npm run check:recipes` now fails on a recipe
2079
+ stylesheet, an inline style, a class outside the published layout and Web
2080
+ Awesome vocabulary, a non-public stylesheet import, or recipe-specific rules in
2081
+ the catalog shell stylesheet.
2082
+ - Fixed `@kerfjs/ui/webawesome.css` `wa-dialog` titles given as a plain-string
2083
+ `label` sitting at the bare surface inset; they now inset by one bordered
2084
+ item step (`--kui-layout-item-padding` + 1px) so the header label lines up
2085
+ with body text and icons. Slotted label elements are unaffected.
2086
+ - Fixed icon + label `wa-button`s inside `ToolbarControlGroup` stacking the
2087
+ icon above the label; their label part now lays out as a single nowrap row,
2088
+ matching native mixed buttons.
2089
+ - `prompt()` / `form()` now reject with a `TypeError` when a validator returns
2090
+ a promise (validators are synchronous), instead of resolving the value and
2091
+ leaving an unhandled rejection. `prompt()` also re-reads its input on every
2092
+ OK, so a reactive `render` that replaces the input no longer submits the
2093
+ detached original's stale value, and a removed input rejects the promise.
2094
+ - A local `npm run check` run with `KERF_SKIP_PACKAGE_GATES=1` no longer
2095
+ records a verified tree, so the pre-push hook cannot skip the sibling-package
2096
+ gates that run left out.
2097
+ - `kerfjs/overlay`: a `prompt()` `validate` or `form()` `field.validate` that
2098
+ throws now closes the dialog (full teardown, focus restored) and rejects the
2099
+ returned promise with the original error. Previously the error escaped the
2100
+ OK click or Enter handler, and the dialog stayed open with an OK button that
2101
+ could never succeed. Validators are synchronous; this is the error channel
2102
+ for a validator bug.
2103
+ - `kerfjs/overlay`: when a `form()` field input disappears after the dialog
2104
+ opens (for example a reactive `render` re-render drops it), OK and Enter now
2105
+ close the dialog and reject the promise with an error naming the missing
2106
+ field. Previously `form()` resolved `null`, which
2107
+ reads as a Cancel, and threw from the click handler.
2108
+ - `@kerfjs/ui`: a multiline `ListItem` whose label fits on one line now
2109
+ centers its icon on that line. The row's 44px minimum used to stretch the
2110
+ grid track, which centered the label but left the start-aligned icon about
2111
+ 1.4px above it.
2112
+ - `@kerfjs/ui`: a `ToolbarControlGroup` with `focusRing="outline"` or
2113
+ `"halo"` keeps its group focus ring while a `Select` inside it has its
2114
+ listbox open. Chromium and WebKit did not carry `:focus-within` from the
2115
+ focused option to the group, so the ring disappeared.
2116
+ - UX catalog: the foundation palette's Brand swatch uses the brand on-fill
2117
+ color again, the Spacer demo gives its flexible spacer room to grow again,
2118
+ and the TokenSearchField adoption demo again wires its chip keyboard and
2119
+ `onEdit` readout. The catalog demo rework had broken all three.
2120
+ - Documented and pinned how `tooltip()` surfaces a failed show from its delay
2121
+ timer: the half-built tooltip is rolled back, the original error escapes the
2122
+ timer for the host to report (a window `error` event in browsers, matching
2123
+ kerf's other deferred callbacks), and the tooltip stays armed so the next
2124
+ hover or focus tries again.
2125
+ - Fixed the dialog helpers (`confirm` / `prompt` / `form` / `choice`) leaving
2126
+ wiring behind when a step after the overlay opened failed. Their post-open
2127
+ wiring (required-slot checks, the click-table `delegate()`, the Enter-key
2128
+ listener) is now part of construction: a failure removes the listeners
2129
+ already wired, closes the overlay (including a `native` `<dialog>`), restores
2130
+ focus, and rethrows the original error, so no pending promise is left
2131
+ attached to an on-screen dialog.
2132
+ - Added `ticket:timing summary --all`, a read-only cross-ticket timing report
2133
+ (median/p90 per phase, gate, and check step) that counts one push shared by
2134
+ a batch of tickets once and detects and excludes fan-out backfill, such as
2135
+ the first timed push that attached one check interval to 374 historical
2136
+ tickets.
2137
+ - Ticket timing now records per-step durations of the root `npm run check`
2138
+ chain (printed at the end of every run), interrupted gate attempts, active
2139
+ time through `ticket:timing claim`/`release` wrappers around `hotsheet-cli`,
2140
+ and CI/publication intervals imported idempotently from GitHub Actions with
2141
+ `ticket:timing import-ci`.
2142
+ - The pre-push hook no longer reruns `npm run check` when a local run already
2143
+ passed on the exact clean tree being pushed; the skip is recorded in ticket
2144
+ timing, and any dirty worktree, different tree, Node.js change,
2145
+ failed/interrupted rerun, or `KERF_FORCE_CHECK=1` runs the full gate.
2146
+ - The pre-push hook no longer fans its timing record out to every ticket in a
2147
+ push that names more than 25 of them (a first push of a long history, a
2148
+ force-push). It prints a notice and records only the explicit
2149
+ `KERF_TICKET_TIMING_TICKETS` list, or nothing, so it can no longer write the
2150
+ backfill shape the cross-ticket summary has to exclude.
2151
+ - The pre-push hook's "already verified" skip now also requires the same
2152
+ platform, architecture, and installed dependencies as the passing run: a
2153
+ fingerprint of every `package-lock.json` and `node_modules/.package-lock.json`
2154
+ across the root, `ui/`, `eslint-plugin/`, `create-kerf-component/`, and
2155
+ `site/`. A reinstall, a lockfile change, or a missing `node_modules` now runs
2156
+ the full gate.
2157
+ - `ticket:timing record` accepts `--run-id <n>`, and `import-ci` no longer
2158
+ imports a GitHub run a second time when it was already recorded by hand:
2159
+ an interval with that run id, or one with no run id for the same phase and
2160
+ gate whose span overlaps the run's (within 60 seconds), counts as imported.
2161
+ - `bindList()` now rolls back when its first render throws: rows that pass
2162
+ already created are disposed (content mounts and element-mode `dispose`
2163
+ callbacks) and removed before the original error is rethrown, instead of
2164
+ leaking live row effects with no handle to release them.
2165
+ - `bindList()` virtualization now validates its dimensions: heights (fixed,
2166
+ estimated, callback-returned, and `setHeight` reports) must be finite and
2167
+ non-negative, a fixed window-mode `rowHeight` must be positive, and
2168
+ `overscan` / `minRows` must be non-negative integers. Invalid values throw a
2169
+ descriptive error instead of producing blank windows or a corrupted scroll
2170
+ anchor.
2171
+ - `bindList()` now rejects a source that carries the `arraySignal` brand but
2172
+ has no patch queue with a descriptive error at bind time, instead of failing
2173
+ with a generic `TypeError` on its first render.
2174
+ - Added `wa-dialog.hide-actions` support to `@kerfjs/ui/webawesome.css`, hiding
2175
+ Web Awesome's directly exported `header-actions` shadow part for dialogs that
2176
+ provide their own dismissal affordance.
2177
+ - Fixed `mount()` leaving an element permanently "already mounted" when its
2178
+ first render throws. The first render is now transactional: the original
2179
+ error is rethrown, every binding effect and the dev listener observer
2180
+ acquired so far are released, the element's pre-mount child nodes are
2181
+ restored, and a retry on the same element succeeds.
2182
+ - Fixed `overlay()` construction leaking a live wrapper, mount, dismissal
2183
+ listeners, and concurrent-overlay stack entry when setup failed partway. A
2184
+ malformed `initialFocus` selector now throws a descriptive `overlay()` error
2185
+ before anything is touched, and a throwing first render or `native`
2186
+ `showModal()` / `showPopover()` call rolls back everything already installed
2187
+ (restoring focus) before rethrowing. `popover()` and `tooltip()` roll back the
2188
+ same way when positioning fails; the dialog helpers inherit the guarantee.
2189
+ - Added an opt-in `@kerfjs/ui/document.css` baseline for border-box sizing,
2190
+ token-backed body and link presentation, and a `.kui-app-root` full-height
2191
+ chain for application shells.
2192
+ - Added an inline `Text` span variant for secondary copy inside rows, labels,
2193
+ and table cells, preserving tone, size, and font roles without box geometry.
2194
+ - Reworked every focused UX catalog demo to dogfood Kerf components, Web
2195
+ Awesome surfaces, and semantic HTML without route-local stylesheets, inline
2196
+ styles, styling-only classes, or shared icon/button render helpers. Added
2197
+ catalog-owned specimen viewport constraints and a nonwaivable conformance
2198
+ gate that prevents those styling shortcuts from returning.
2199
+ - Fixed `ToolbarControlGroup` dropdown triggers that were sized to fixed 48px/66px boxes,
2200
+ which clipped popup icons and let compact mixed-control carets overflow in
2201
+ Safari while distorting longer catalog controls.
2202
+ - Added a `TabBar.end` action zone for layouts that need a tab-local adjacent
2203
+ `trailing` action and a separate far-edge action; the tab strip now owns the
2204
+ shrink/scroll geometry while all action regions remain visible.
2205
+ - Moved the UX catalog checkerboard to the preview scroll owner and replaced
2206
+ its four gradients with one repeated SVG tile, so the pattern continues
2207
+ through below-fold content.
2208
+ - Added explicit native `slot` support to every stable single-root
2209
+ `@kerfjs/ui` visual component, including alternate render branches, while
2210
+ keeping multi-root `ResizableRegion` and `CollapsiblePanel` excluded.
2211
+ - Fixed the UX catalog's desktop grid sizing so both the sidebar and component
2212
+ detail can scroll all the way to their final content while narrow layouts
2213
+ retain normal document scrolling.
2214
+ - Fixed single-line `ToolbarText` clipping glyph descenders while retaining its
2215
+ horizontal ellipsis behavior.
2216
+ - Formalized UX catalog demos as components, compositions, or recipes; catalogs
2217
+ now group and order those kinds consistently and label compositions with a
2218
+ visible tag instead of title suffixes.
2219
+ - Removed stale Web Awesome guidance that still advertised the deleted
2220
+ aggregate theme route; focused component routes remain the supported catalog
2221
+ surface.
2222
+ - Aligned Web Awesome Accordion and Details disclosure geometry: plain Details
2223
+ keep a flush body, plain Accordion items use the 8px surface inset throughout,
2224
+ framed Details use 16px on the trigger and 8px on the body, and framed
2225
+ Accordion items use 16px throughout.
2226
+ - Added a first-class `@kerfjs/ui/grid` layout primitive for any positive
2227
+ equal-width column count, with typed gap and flex values and zero-minimum
2228
+ fractional tracks that stay equal when child content has different intrinsic
2229
+ widths.
2230
+ - Made accent and filled-outlined Web Awesome badge variants meet WCAG AA at
2231
+ their compact text size while preserving loud accent fills and outlined
2232
+ borders.
2233
+ - Added independent typed `tone`, `size`, and `font` presentation roles to
2234
+ `@kerfjs/ui/text`, covering quiet/danger copy, compact metadata, and monospace
2235
+ identifiers without global utility classes or appearance-only heading levels.
2236
+ - Made filled Web Awesome badges use a quiet semantic fill with normal text by
2237
+ default, preserving variant tint while meeting WCAG AA for their compact text.
2238
+ - Fixed nested Rows and Lists inheriting inset values for sides that were not
2239
+ selected on the inner layout component.
2240
+ - Fixed migrated `Text` instances in Catalog and passive `ListHeader` labels
2241
+ retaining nested padding or border geometry that offset their visible content.
2242
+ - Added `Row.vAlign="baseline"` for native flex cross-axis baseline alignment;
2243
+ `List.vAlign` remains limited to values supported by vertical main-axis
2244
+ distribution.
2245
+ - Added a first-class `@kerfjs/ui/text` component for native `h1`–`h6` and
2246
+ paragraph semantics with standard transparent-border and content-padding
2247
+ geometry and margin-neutral defaults; it renders `p` when `variant` is omitted,
2248
+ and Kerf UI's headings and prose now use it.
2249
+ - Added the shared physical `Sides` contract to `List` and `Row` text/control
2250
+ insets and to `ListInsetText` / `ListInsetControl`; inset wrappers now default
2251
+ to all four sides, while `ListInsetText.horizontalOnly` remains a deprecated
2252
+ compatibility alias for `sides="rl"`.
2253
+ - Added `ListHeader.inline` for shrink-wrapped section labels that remove the
2254
+ component root's outer margin, border, and padding while preserving title,
2255
+ count, action, and disclosure behavior.
2256
+ - Added a catalog-driven public-component integration validator for
2257
+ `@kerfjs/ui`, including an all-at-once dry-run report across package, build,
2258
+ barrel, CSS, demo, route, signature, and compatibility surfaces.
2259
+ - Added one change-local `@kerfjs/ui` verification command that synchronizes
2260
+ generated projections, runs component/bundle/type coverage, builds the demo,
2261
+ reports exact gzip deltas, and records reviewed budget updates with reasons.
2262
+ - Made every `@kerfjs/ui` Playwright invocation build current catalog source
2263
+ after fast preflights, with a served source digest that rejects stale preview
2264
+ output during focused browser runs.
2265
+ - Added durable Hot Sheet phase timing for active work, local verification,
2266
+ push hooks, CI, and publication, including safe failure aggregation and
2267
+ per-ticket summaries.
2268
+ - Added typed flex participation to `Row`, matching `List`'s boolean, finite
2269
+ keyword, and branded `CssFlex` contract.
2270
+ - Added a first-class horizontal `Row` with typed spacing, physical-axis
2271
+ alignment, and opt-in wrapping; `List` now supports the same alignment
2272
+ vocabulary while retaining its existing defaults.
2273
+ - Refined compact mixed `ToolbarControlGroup` selections to retain standard
2274
+ horizontal padding, remove the internal separator, and raise the selected
2275
+ surface over the outer border; avatar group hover now preserves image fitting.
2276
+ - Moved the comprehensive local `npm run check` gate from pre-commit to
2277
+ pre-push so coherent commit batches pay its cost once; pre-commit now performs
2278
+ only Git's staged whitespace check, and multi-source AI guidance files are
2279
+ excluded from repository-wide Prettier enforcement.
2280
+ - Fixed `create-kerf-component` releases carrying a stale composition-catalog
2281
+ schema by generating the bundled copy from `@kerfjs/ui`'s canonical schema
2282
+ and gating both the root check and interactive release flow against drift.
2283
+ - Added catalog-driven CSS value diagnostics across `eslint-plugin-kerfjs`,
2284
+ `kerf-ui-analyze`, and `kerf-ui-doctor`. Stable `KUI-L013`–`KUI-L017`
2285
+ findings now identify raw or unknown values, wrong-dimension helpers,
2286
+ incomplete expressions, declaration-list escapes, and review-worthy
2287
+ off-scale spacing from first- or third-party component catalogs.
2288
+ - Extended the property-specific `@kerfjs/ui/css-values` contracts: `List.flex`
2289
+ now uses finite keywords or `flex()`, `Skeleton` dimensions use `CssSize` and
2290
+ `CssLength`, and `SelectChoice.color` uses `uiColor()` or `colorVar()`.
2291
+ Removed the unrestricted `style` escape hatches from `ListItem` and
2292
+ `ListActionRow` before the 5.0 stable release; use `className`, public tokens,
2293
+ and cataloged props instead.
2294
+ - Added typed, CSS-free `@kerfjs/ui/css-values` builders and branded complete
2295
+ versus expression length types. `List.gap` now accepts direct spacing-token
2296
+ shorthands or complete `CssLength` values and rejects unrestricted CSS strings.
2297
+ - Added the public recursive `JSXChildren` type from both `kerfjs` and
2298
+ `kerfjs/jsx-runtime`, giving third-party function components a canonical
2299
+ declaration for nullable, signal-backed, and arbitrarily nested readonly
2300
+ child content that matches the runtime.
2301
+ - Added the public recursive `KerfUiContent` type across semantic UI content
2302
+ zones, so nullable conditionals and mutable or readonly nested component
2303
+ arrays compose directly without a `Fragment` while raw strings, numbers, and
2304
+ signals remain rejected outside explicit text positions.
2305
+ - Fixed bottom `Workbench` drawers overshooting and snapping during collapse
2306
+ transitions by anchoring fixed-height content to the shell's stable bottom
2307
+ edge.
2308
+ - Fixed bottom `CollapsiblePanel` drawers overshooting their open position and
2309
+ snapping back by anchoring fixed-height content to the panel's stable bottom
2310
+ edge throughout the transform transition.
2311
+ - Formalized named `SafeHtml` prop slots as explicit `zone.jsx.prop` bindings
2312
+ in the UI composition catalog and generalized `ui-composition` linting beyond
2313
+ Toolbar, including statically visible `children`, cardinality, aliases,
2314
+ namespaces, arrays, fragments, and branches.
2315
+ - Added `ToolbarControlGroup.avatarImage`, painting contained avatar imagery on
2316
+ a lone group or on the pressed highlight when the group has multiple buttons.
2317
+ - Split the AppTab and TabBar catalog routes into focused component specimens,
2318
+ and moved their controlled selection, tabpanel, overflow, add/close, and
2319
+ reorder lifecycle into a separate application-tabs composition demo.
2320
+ - Fixed `wireNavStack` focus ownership across controlled navigation: pushes
2321
+ focus the new top view, pops restore its remembered descendant, and removed
2322
+ targets fall back through `data-nav-focus`, the first enabled control, and
2323
+ the view container.
2324
+ - Fixed compact mixed-content `ToolbarControlGroup` dropdown triggers being
2325
+ forced into an icon-only width, which painted the label across its separator
2326
+ and the Web Awesome caret outside the group.
2327
+ - Made `ListItem` clip label, status, and dormant trailing painting to its
2328
+ rounded row boundary so compact and constrained rows cannot visibly overflow;
2329
+ described rows now grow enough to keep their owned two-line label readable.
2330
+ - Fixed compact toolbar `Select` focus to follow its owning group's pill or
2331
+ rounded geometry and restored consistent spacing between popup icons and labels.
2332
+ - Fixed the compact-toolbar recipe's independent overflow action to use the
2333
+ same full-height pill geometry as its adjacent toolbar controls.
2334
+ - Fixed the catalog footer's `Components` relationship selector to fit its icon
2335
+ and plural label and give popup headings/items consistent border clearance.
2336
+ - Fixed a collapsible `TokenSearchField` inside a rounded
2337
+ `ToolbarControlGroup` retaining its independent pill radius instead of using
2338
+ the group's shared concentric highlight radius.
2339
+ - Updated the header-composition catalog demo's supporting copy to use the
2340
+ horizontal `ListInsetText` geometry, aligning it with the value-table content
2341
+ edge across responsive, RTL, and zoomed layouts.
2342
+ - Updated the `Pane` catalog demo's secondary header copy to use
2343
+ `ListInsetText`, aligning it with the standard pane content gutter at wide and
2344
+ narrow sizes instead of maintaining demo-only padding.
2345
+ - Completed `NavStack` navigation transitions by cross-fading top and bottom
2346
+ chrome alongside content slides, added per-view bottom toolbars with a
2347
+ persistent fallback, and replaced the static focused demo with a working
2348
+ push/pop flow.
2349
+ - Replaced the static compact `SplitView` detail specimen with a controlled,
2350
+ interactive list-to-detail drill-down whose Back action restores the list;
2351
+ the roomy example now demonstrates controlled selection as well.
2352
+ - Documented and demonstrated list-first dialog bodies: use `List` with
2353
+ `bodyInset="none"`, and wrap bare dialog copy in `ListInsetText` so content
2354
+ shares the standard list gutter without double-insetting.
2355
+ - Fixed `@kerfjs/ui`'s collapsible `TokenSearchField` keep-open ownership in
2356
+ WebKit: when `focusout.relatedTarget` is missing during pointer activation of
2357
+ an external `data-token-search-keep-open` surface, the helper now uses the
2358
+ actual pointer target instead of collapsing and removing the target before
2359
+ its click handler runs.
2360
+ - Added a first-class vibrant `pop` semantic color for attractive non-status
2361
+ emphasis, with complete light/dark/increased-contrast token roles and typed
2362
+ StateBanner, ListHeader indicator, and ToolbarControlGroup selected tones.
2363
+ - Fixed canonical `Select` popup animation reversals so an earlier close cannot
2364
+ hide a reopened menu after resizing. Canceled transitions retain the accepted
2365
+ state, and removed controls cannot receive stale completion or deferred focus.
2366
+ - Fixed managed `TokenSearchField` Clear focus before the next input task, so
2367
+ immediate typing stays in the replacement editor instead of triggering page
2368
+ shortcuts; genuine later focus handoffs and selections remain untouched.
2369
+ - Fixed `@kerfjs/ui/tab-scaffold` bottom-tab labels being vertically clipped by
2370
+ flex shrinkage while retaining horizontal ellipsis for long labels.
2371
+ - Made inset selection and hover highlights follow their owning control's pill
2372
+ or rounded shape, with concentric inner radii derived from the full inset.
2373
+ - Fixed animated `ResizableRegion` overlays so remembered desktop dimensions
2374
+ cannot make their content overflow the responsive panel cap on narrow screens.
2375
+ - Added configuration-first application-panel policies across `ResizableRegion`,
2376
+ `Workbench`, and `CollapsiblePanel`: separator visibility, instant-track
2377
+ collapse with optional composited content motion, popup-safe overflow,
2378
+ inline/overlay/hidden responsive presentations, and safe-area restore-control
2379
+ placement. `wireResizableRegions` now suppresses motion during pointer drags
2380
+ and ignores unavailable regions; `wireSidebar` supports hidden compact
2381
+ replacements and exclusive overlays.
2382
+ - Added typed `TabBar` allocation/presentation/action-placement and `AppTab`
2383
+ presentation/size/label-width variants for segmented inspector strips,
2384
+ compact drawer tabs, icon-only accessible tabs, and truncating labels.
2385
+ - Added typed `ToolbarControlGroup` size, density, content anatomy, selected
2386
+ chrome/tone, nested-dropdown, and avatar scrim variants.
2387
+ - Added typed `Select` form, borderless-toolbar, intrinsic-navigation, compact,
2388
+ icon-only-selected, truncation, and composed focus-ring variants.
2389
+ - Added `DialogSurface` and `PopupSurface` composition APIs for standard dialog
2390
+ sizes/presentations, independent body/footer insets, and list-compatible popup
2391
+ menu insets without consumer `::part()` overrides.
2392
+ - **Breaking (`@kerfjs/ui`):** removed `PanelHeader` and its subpath/CSS export.
2393
+ Compose page, panel, and dialog headings directly with `Toolbar`, a direct
2394
+ extra-large `ToolbarText`, and optional `ToolbarControlGroup` icon/action zones;
2395
+ keep supporting copy as app-owned content below the toolbar.
2396
+ - Fixed controlled TokenSearchField Select All + Backspace/Delete collapsing the
2397
+ editor: managed deletion keeps the adopted open signal and restores replacement
2398
+ focus before another keystroke, without a stale animation-frame caret reset.
2399
+ The adoption demo now persists both edited text and remaining tokens.
2400
+ - Fixed `Select` controls using only `ariaLabel` being unnamed to assistive
2401
+ technology. Ordinary and custom selected content retain their geometry while
2402
+ the actual shadow combobox receives its accessible name.
2403
+ - Added first-class `Select` hint text below interactive and loading-placeholder
2404
+ controls while keeping empty-value `placeholderText` semantically distinct.
2405
+
2406
+ - Changed the Web Awesome Dialog theme to use 8px body padding and 16px footer
2407
+ padding, independently of the component's shared `--spacing` value.
2408
+
2409
+ - Fixed `kerf-ui-doctor` so its isolated Kerf ESLint pass preserves applicable
2410
+ consumer core rules and suppression reporting options. Playwright's required
2411
+ `no-empty-pattern` fixture suppression no longer becomes a false unused
2412
+ directive while consumer plugin rules remain outside the doctor boundary.
2413
+
2414
+ - Cataloged `SplitView` and its documented
2415
+ `--kui-split-view-list-width` override so `kerf-ui-doctor --full` accepts the
2416
+ public token in downstream application stylesheets.
2417
+
2418
+ - Added the first-class `List` vertical-layout component with gap, flex, scroll,
2419
+ and multi-edge divider options; `Toolbar` now uses the same typed
2420
+ `dividerSides` contract instead of a boolean divider.
2421
+
2422
+ - Added an optional tone-tinted pill badge beside `StateBanner` titles, including
2423
+ loading skeleton behavior and per-instance badge color overrides.
2424
+
2425
+ - Fixed pending `wireTabBars` and `wireTokenSearchFields` controlled-render
2426
+ callbacks moving focus after their owning wiring had been disposed.
2427
+
2428
+ - Changed the default `ListItem` leading icon visual from 24px to a root-scaled
2429
+ 18px while retaining the row's 44px minimum target and first-line alignment.
2430
+
2431
+ - Fixed managed `TokenSearchField` clear losing focus or collapsing when an
2432
+ application replaces its controlled editor; continued typing now works without
2433
+ an application reopen callback.
2434
+
2435
+ - Fixed `@kerfjs/ui` automatic TabBar keyboard activation losing focus when a
2436
+ controlled selection replaces the strip; navigation restores the same logical tab.
2437
+
2438
+ - Updated the published setup runtime to patched `minimatch` 9.0.9 and `yaml`
2439
+ 2.9.1 releases, resolving the known regular-expression denial-of-service and
2440
+ deeply nested YAML denial-of-service advisories without narrowing Kerf's
2441
+ supported Node.js range.
2442
+ - Fixed `kerf-ui-doctor --full` so its isolated ESLint stage shares the same
2443
+ generated-directory exclusions as TypeScript, static analysis, and cache
2444
+ hashing; populated `dist`, `coverage`, dependency, cache, evidence, and nested
2445
+ worktree directories no longer produce application diagnostics.
2446
+ - Changed `@kerfjs/ui` component-library design templates to compose their
2447
+ individual variant SVGs through HTML and `domotion capture
2448
+ --flatten-nested-svg`, eliminating the hand-built nested-SVG compositor and
2449
+ making top-level template sheets reliable in Sketch.
2450
+ - Added a typed `shape` option to `SunkenPanel`: `rounded` remains the default,
2451
+ while `square` produces a lowered surface with `border-radius: 0` for flush or
2452
+ edge-to-edge application areas. Both shapes now appear in the UX catalog.
2453
+ - Made the Catalog geometry overlay derive both margin bands and border
2454
+ edges/radii from each live specimen's computed CSS. Public example helpers now
2455
+ mark their generated labels and notes for automatic exclusion, and stylesheet,
2456
+ theme, DOM, and resize changes refresh the overlay without duplicated demo
2457
+ measurements.
2458
+ - Updated the `@kerfjs/ui` SVG design-template generator to `domotion-svg`
2459
+ 0.30.1 and enabled `--flatten-nested-svg`, so inline component icons are
2460
+ emitted as Sketch-compatible groups without changing their browser rendering.
2461
+ The offline template gate now rejects variant captures that reintroduce nested
2462
+ `<svg>` elements.
2463
+ - Fixed `kerf-ui-doctor` and `kerf-ui-analyze` recursive discovery so nested
2464
+ `.claude/worktrees` checkouts are excluded from source diagnostics, doctor
2465
+ cache inputs, TypeScript, and ESLint.
2466
+ - Added `npx kerfjs setup` and the `kerfjs/setup` automation API for safe,
2467
+ AI-first initialization and migration of core and UI projects. The command
2468
+ provides bounded dry-run diffs, explicit per-conflict keep/Kerf choices,
2469
+ package-scoped hash-backed upgrades, monorepo selection, transactional
2470
+ rollback, shipped guidance, strict TypeScript/ESLint, and UI catalog/profile
2471
+ and doctor discovery. Empty application component metadata now emits a valid
2472
+ empty v2 catalog. Existing JSONC TypeScript configuration is structurally
2473
+ merged without discarding comments, trailing commas, authored fields, or line
2474
+ endings; malformed roots and compiler-option containers require an explicit
2475
+ keep/Kerf decision. Workspace selection now evaluates ordered globstar,
2476
+ brace, exclusion, and re-inclusion patterns deterministically across npm,
2477
+ Yarn, and pnpm declarations, rejects path/symlink/name ambiguity, and uses
2478
+ manager-correct offline commands with explicit Yarn Classic/Berry detection
2479
+ and network-disabled Berry installs.
2480
+ - Added `@kerfjs/ui/doctor` and the `kerf-ui-doctor` command: a versioned, redacted repair-loop report that merges catalog/profile validation, TypeScript, Kerf UI ESLint, static analysis, and opt-in browser evaluation with changed/full modes, monorepo selection, reasoned suppressions, caching, and deterministic exits.
2481
+ - Added the public Playwright-backed `kerf-ui-evaluate` CLI and `@kerfjs/ui/evaluator` Node API. It resolves application profiles and composition catalogs, exercises wide/intermediate/narrow/200%-zoom, light/dark, and reduced-motion contexts across Chromium, Firefox, and WebKit, and emits versioned repair-oriented `KUI-B###` diagnostics plus focused DOM/style evidence and hashed screenshots. Subjective visual quality remains an explicitly separate unrecorded reviewer rubric.
2482
+ - Added `kerf-ui-analyze`, a profile- and catalog-aware static evaluator for Kerf UI integrations. It resolves directory policy per source, follows project-local CSS import graphs without cross-package class leakage, and evaluates shared stylesheets against every consuming source policy before deduplicating findings. Stable portable text, JSON, or SARIF diagnostics cover private selectors, unknown tokens, competing geometry/scroll owners, repeated insets, forced dimensions, off-scale spacing, and dynamic-class review, with exact scoped profile exceptions, a public report schema, and configurable CI review failure.
2483
+ - Added catalog/profile-aware `recommended-ui` and `strict-ui` flat presets to `eslint-plugin-kerfjs`. Toolbar zone acceptance/cardinality and valid wiring imports come from the shipped catalogs; intrinsic or unknown zone children, root/subpath/namespace wiring, and the non-colliding `KUI-L090` load diagnostic are covered by real packed downstream installs on every supported ESLint major.
2484
+ - **Breaking (`@kerfjs/ui` TypeScript declarations):** strengthened invalid-integration contracts for ListHeader modes, Select naming and literal choice identity, SegmentedControl/TabScaffold literal identity, collapsible TokenSearchField props, protected editor metadata, PanelHeader summary ids, ToolbarText line caps, and token-removal callbacks. Added named public unions for previously anonymous finite variants plus source and packed-tarball positive/negative compilation gates. See `ui/docs/type-contracts.md` for the compatibility review and dynamic-data widening syntax.
2485
+ - `create-kerf-component` now scaffolds explicit `kerf.components.json` AI metadata and a deterministic `kerf-component-catalog` generator/checker. Generated v2 entries carry package identity, purpose, verified named exports, composition and geometry ownership, tokens, accessibility, and source links; publishing rejects missing author decisions, schema-invalid source/output fields, deleted sources, renamed exports, duplicate ids, or output drift, including multi-package workspaces. Syntax-aware TypeScript/TSX export discovery prevents JSX text, nested scopes, comments, and literals from impersonating public exports.
2486
+ - Added a versioned application UI profile for AI tools and static evaluators. Shipped package defaults, schema, TypeScript types, Node discovery/merge/validation APIs, and a workspace example now make catalog locations, preferred components/recipes, theme/density policy, semantic token overrides, responsive/layout conventions, and narrow source-located exceptions explicit. Composition-only generated consumer catalogs load without a fabricated v1 selection artifact, while Kerf retains its required v1 selection catalog. Deterministic package → workspace → parent-to-child directory precedence validates every raw layer before merge, attributes resolved diagnostics through field provenance, and rejects stale catalogs, unknown components/tokens/rules, conflicts, malformed value shapes, and broad exemptions.
2487
+ - Fixed `CatalogExampleStack` accessible labels: the helper now renders a semantic `section`, so a supplied `label` exposes the documented named region to assistive technology while an unlabeled stack remains an ordinary grouping.
2488
+ - Added first-class `Pane` and `SunkenPanel` primitives to `@kerfjs/ui`. `Pane` standardizes semantic header/content/footer columns, explicit logical-edge separators, and one content scroll owner; `SunkenPanel` supplies one lowered surface with an 8px inset and vertical rhythm. The Catalog shell now dogfoods `Pane`, and both components ship focused demos, catalog metadata, documentation, and three-engine browser coverage.
2489
+ - Added the package-qualified component catalog v2 alongside the compatible v1 artifact. Its schema and generated metadata formalize child zones, cardinality, exclusivity, wiring, state, responsive behavior, layout and accessibility boundaries, stable validation diagnostics, provenance, and consumer extension catalogs, so AI tools can join Kerf and application components without guessing composition contracts.
2490
+ - Added an AST-based Catalog demo conformance gate. It checks every first-party demo for public helper usage, package imports, focused specimen structure, geometry-overlay metadata, and reviewed schema-checked exceptions, with adversarial fixtures and authoring guidance that makes the same contract available to downstream AI tools.
2491
+ - Made `ui/docs/catalog.md` the authoritative Catalog demo authoring contract, covering focused versus composition previews, public row/group nesting, exact specimen selection, conditional geometry overlays, skip-marker behavior, metadata ownership, and an explicit overlay legend. A shipped `ai/catalog-authoring.json` discovery artifact now routes AI tools to that contract and exact signatures, with schema, link-integrity, and browser-consumer fixture coverage.
2492
+ - Extended `CatalogExample` and `CatalogExampleStack` with typed, runtime-filtered `rootAttributes` for catalog-authoring `data-*` metadata such as `data-demo` and `data-catalog-geometry-overlay-skip`, while preserving helper-owned structural and alignment markers. The UX catalog now composes its example stacks through these public helpers instead of copying private `kui-catalog-*` markup.
2493
+ - Added `revealCatalogEntry` and opt-in `wireCatalog({ revealSelection })` support to the exported Catalog tool. Sidebar or related-entry selection can now reveal the matching row after the controlled render settles, using a desktop-safe media guard, exact id matching, configurable scroll alignment/behavior, focus-preserving scrolling, and cancellation of stale rapid selections; the Kerf UX demo now uses the public behavior.
2494
+ - Added a reusable consumer component-catalog extension schema and checked app-component example. Downstream packages can now publish the same selection facts and margin/border/padding ownership vocabulary as Kerf, and the README, component contract, Catalog guide, `llms.txt`, and AI skill explain how to combine package-qualified app entries with Kerf's shipped catalog for alignment decisions.
2495
+ - Added an opt-in geometry inspector to the exported `@kerfjs/ui/catalog` tool. `geometryOverlay` plus `wireCatalogGeometryOverlay` now marks transparent component bounds with a dashed outline and positive intrinsic margins with devtools-style bands, excludes example labels/alignment scaffolding, stays synchronized across preview changes, and replaces the Kerf UX demo's private implementation.
2496
+ - Added machine-readable margin, border, and padding ownership to every visual entry in the `@kerfjs/ui` component catalog. The additive schema distinguishes self-, parent-, child-, conditional-, and unowned geometry, and enforces complete metadata plus explanatory notes for conditional cases.
2497
+ - Refreshed the standalone reactivity demo lockfile to Vite 6.4.3, PostCSS 8.5.28, and nanoid 3.3.19, clearing its development-toolchain audit advisories.
2498
+ - Overrode the development toolchain's transitive esbuild dependency to 0.28.2, resolving GHSA-g7r4-m6w7-qqqr while current tsup still constrains its declared range to 0.27.x.
2499
+ - Added repository-wide Prettier formatting for TypeScript, JavaScript, Astro, shell, CSS, HTML, Markdown, YAML, and JSON source and structured-content files. Root and `@kerfjs/ui` lint gates now reject formatting drift, while generated, binary, and parser-incompatible fixtures remain explicitly excluded.
2500
+ - Added reusable sidebar tags to `@kerfjs/ui` Catalog entries and marked the 15 Web Awesome components superseded by preferred Kerf patterns or reserved for exceptional cases as `Discouraged` in the UX catalog. The tag is deterministically projected from the canonical recommendation metadata; Popup remains an available conditional positioning primitive.
2501
+ - Aligned Web Awesome Accordion, Card, Details, Callout, and Include surfaces with Kerf content-item geometry: each complete surface now has an overridable 8px logical inline margin and 8px inner padding, Accordion keeps connected items under one group margin, and Card's header/body/footer share the full inset. Tab Panel intentionally retains the roomier 16px container inset.
2502
+ - Fixed `@kerfjs/ui` Web Awesome Slider geometry: its complete interactive region now receives the shared 8px logical inline outer inset without shifting the already-aligned label.
2503
+ - Styled `@kerfjs/ui` Web Awesome OTP Input like a bordered text field: its label now uses the shared uppercase xs/650 field-label treatment, and both label and hint align at the 9px border-plus-padding inset.
2504
+ - Aligned `@kerfjs/ui` Web Awesome Known Date's Month/Day/Year captions and bordered text-like field hints with their values using the shared 9px border-plus-padding inline inset.
2505
+ - Fixed `@kerfjs/ui` Web Awesome Color Picker geometry: its unbordered trigger now receives the shared 8px inline outer inset, aligning it with Kerf content-item spacing.
2506
+ - Fixed `@kerfjs/ui` Web Awesome Checkbox Group and Radio Group option geometry: each group's unbordered option region now receives the shared 8px inline outer inset, aligning it with Kerf content-item spacing without padding the individual controls.
2507
+ - Aligned the remaining bordered/padded Web Awesome chrome in `@kerfjs/ui/webawesome.css` to explicit Kerf spacing tiers: `--kui-wa-control-inset` (8px) drives tabs, inset tree selection, tags, and dropdown items; `--kui-wa-surface-margin` / `--kui-wa-surface-inset` drive bordered surface geometry; and `--kui-wa-container-inset` (16px) retains roomier Tab Panel content. Buttons already inherit the 8px form-control inset; compact badges, unframed Breadcrumb, and slotted Scroller item chrome deliberately retain their native geometry. The tiers are overridable by scope and pinned by three-engine computed-style coverage plus light/dark wide and narrow visual QA.
2508
+ - Tightened the `@kerfjs/ui` Web Awesome theme's form fields to the Kerf content-item inset — a 1px border with 8px inside it (`--wa-form-control-border-width` / `--wa-form-control-padding-block` / `--wa-form-control-padding-inline` now follow `--kui-layout-item-border-width` / `--kui-layout-item-padding`), replacing Web Awesome's larger default pad, so fields land at the standard ~40px height and 9px value inset. Each field's top label is now inset by that same border + padding (9px) so it aligns with the value inside the field, and is styled exactly like a `ListHeader` label — uppercase, xs, weight 650, quiet foreground (`::part(form-control-label)` across Input/Number Input/Textarea/Select/Checkbox Group/Radio Group/Color Picker/Known Date/Time Input, plus the Slider's `::part(label)`). Inline Checkbox/Switch labels keep their sentence case.
2509
+ - Added overflow controls to `@kerfjs/ui`'s `ToolbarText`: `wrap` (flow onto multiple lines instead of a single line), `ellipsis` (show the trailing … at the truncation, or hard-clip when false), and `maxLines` (cap wrapped text to N lines, ellipsizing past it). The default is unchanged — one line, ellipsized when it does not fit. `maxLines` takes effect only with `wrap`. Driven by `data-wrap` / `data-ellipsis` / `data-max-lines` attributes plus a `--kui-toolbar-text-max-lines` var; no new public classes. The UX catalog gains ellipsis/wrap/capped examples.
2510
+ - Fixed a `@kerfjs/ui` `TokenSearchField` bug where selecting all content and pressing Delete (or Backspace) left a stray newline in the field. A `contenteditable` host inserts a bogus `<br>` when emptied that way in every engine — it renders as a newline and `readTokenSearchField` read it back as a space. `wireTokenSearchFields` now strips that line break after any delete input and, when the field is otherwise empty, restores the canonical empty text span and places the caret in it so typing resumes cleanly. Also clears any leftover chips in the select-all case. Regression-tested in unit (happy-dom) and across all three browser engines.
2511
+ - Hardened that full `TokenSearchField` deletion for platform-specific selection
2512
+ ranges whose endpoints sit inside the first and last text spans instead of at
2513
+ the editor boundary—or whose cloned Linux range omits an atomic chip entirely.
2514
+ Full logical text-and-token selection is recognized from its value, while an
2515
+ explicit Ctrl/Cmd+A intent survives only until the next delete and is
2516
+ consumed by that delete even when the browser omits `beforeinput`, and is
2517
+ retained across a controlled editor replacement before or after
2518
+ `beforeinput`. It is captured before propagation can be stopped and
2519
+ invalidated by selection-moving keys, pointer input, blur, or another edit.
2520
+ The explicit shortcut-to-delete transition is handled deterministically:
2521
+ the helper prevents the unreliable mixed-contenteditable native mutation,
2522
+ canonicalizes the empty editor, and emits the corresponding bubbling input
2523
+ event so controlled application state clears before it can restore a chip.
2524
+ A browser can no longer preserve a controlled chip merely because its range
2525
+ shape or native deletion timing differs.
2526
+ - Added a `horizontalOnly` prop to `@kerfjs/ui`'s `ListInsetText`. When true it keeps the horizontal geometry (8px inline margin, 1px left/right border, 8px left/right padding — so the text edge still lands at the 17px inset) but drops the vertical margin, border, and padding, for tight text layout that still aligns with bordered items. It applies a `kui-list-inset-text--horizontal` modifier class; the default (vertical box space intact) is unchanged. The UX catalog gains a horizontal-only example.
2527
+ - Fixed a WCAG AA color-contrast failure on `@kerfjs/ui`'s selected list rows. The selected `ListItem` and `ListActionRow` used the brand accent (`brand-on-quiet`, `#1e6ef4`) for their text, which resolved to only **3.77:1** over the selected `brand-fill-normal` background (`#d6ecff`) — below the 4.5:1 minimum for normal text (flagged by axe downstream). Both now use the normal neutral foreground over the brand-tinted fill, so selection is carried by the fill and border while the text stays readable in light and dark themes. Computed-ratio browser tests pin both selected rows at ≥4.5:1 in both themes across all three engines.
2528
+ - Fixed the same brand-accent contrast failure on `@kerfjs/ui`'s info `StateBanner` tone and `Select`'s current option, where the blue is a semantic (not a selection cue), so the text stays blue rather than going neutral: a new darker `--kui-color-brand-on-fill` token (`#1a5dcf` light; the compliant `#8acbff` in dark) replaces `brand-on-quiet` for brand text placed directly on a brand fill. The info banner (was 4.15:1) now clears ~5.4:1 and the current Select option (was 3.77:1) ~4.9:1. Computed-ratio browser assertions guard both.
2529
+ - Completed the `@kerfjs/ui` `StateBanner` contrast pass: the `success` (was 4.05:1) and `warning` (was 4.39:1) tones failed WCAG AA the same way via their own `*-on-quiet` tokens over the tinted banner fills. New darker `--kui-color-success-on-fill` (`#00792c`) and `--kui-color-warning-on-fill` (`#8f5e00`) tokens (each keeping its semantic hue, dark values unchanged and already compliant) now back the success/warning banner text, clearing ~5.0:1 and ~5.3:1. Every `StateBanner` tone now clears 4.5:1 in both themes, guarded by an all-tone computed-ratio browser assertion.
2530
+ - Added two structure primitives to `@kerfjs/ui` for aligning bare content inside a `.kui-content` list: `ListInsetControl` (`@kerfjs/ui/list-inset-control` + `.css`) wraps a control that owns its own border and padding but no outer margin (a search input, a `SegmentedControl`) with the standard 8px inline margin plus a stretch flex row and an 8px gap, so it lines up with the bordered items around it without double-insetting; `ListInsetText` (`@kerfjs/ui/list-inset-text` + `.css`) gives a bare string or inline content the content-item geometry (8px inline margin, 1px transparent border, 8px padding) so its text edge lands at the same 17px inset as bordered rows. Both are additive, take an optional `className`, and appear in the UX catalog and the component-selection guidance.
2531
+ - `@kerfjs/ui`'s `wireTokenSearchFields` `onEdit` callback now also receives the originating `InputEvent` (`onEdit({ id, editor, event })`), so a consumer can gate token-commit behavior on `event.inputType` / `event.data` (e.g. commit a chip only on whitespace-terminated input) and drop its own `input` listener entirely — completing the goal `onEdit` was added for. Additive; existing `{ id, editor }` destructuring is unaffected.
2532
+ - Added a CI-only visual-drift gate for the `@kerfjs/ui` SVG design templates (`check:design-templates:drift`): it re-renders every template in a browser and fails if any regenerated SVG differs from the committed one (normalizing run-to-run noise — XML comments and auto-minted ids/font names). It closes the gap the offline `check:design-templates` cannot cover and runs in CI's browser-capable job, not the offline pre-commit `check`.
2533
+ - Added `FloatingToolbar` to `@kerfjs/ui` (`@kerfjs/ui/floating-toolbar` + `.css`): a transparent, forced-dark toolbar that floats over the content of its nearest positioned ancestor — a drawer-restore / floating-controls pattern — without covering dialogs or overlays (it is not top-layer). It exposes `role="toolbar"` with a required `label`, floats to a customizable `position` (`bottom-end` default, plus the other corners/edges), and is inset by `--kui-floating-toolbar-inset` (default 8px past a top toolbar's own inset). The app owns the controls and their visibility. Includes a UX-catalog demo with a show/hide toggle that auto-hides on leaving the demo.
2534
+ - Added a `shape` prop to `@kerfjs/ui`'s `ToolbarControlGroup`: `'pill'` (default, unchanged) or `'rounded'` for a softer rounded-rectangle corner on the group and its items (using the design system's rounded radius). The UX catalog's ToolbarControlGroup section gains a Pill/Rounded toggle that switches every example.
2535
+ - Added a **Design template** link to the UX catalog's detail-footer resources (beside demo source / component source / guidance) for every component that has a committed SVG design template, pointing at its per-component library under `docs/design/templates/`. The link is generated from the design-template manifest during `catalog:sync`, so it appears only where a template exists and stays in step as the template set grows.
2536
+ - Gave the `@kerfjs/ui` `Catalog` detail footer's related-entries popup trigger a visible **"Component"** text label beside its glyph (it was icon-only), so the affordance reads as a labeled control rather than a bare icon. The trigger grows to fit the label instead of the single-group's fixed icon-button width, and the glyph stays beside the label.
2537
+ - Fixed the `@kerfjs/ui` `Catalog` component's `secondarySections` ("ecosystem" group) indentation: the subsection heading sat further left than everything else while its list items sat further right, so the group read as awkwardly stair-stepped. The subsection heading now takes the same 8px content inset the list items and the group's `ListHeader` self-apply, so the group heading, its subsection headings, and their items all share one left edge — matching the primary sidebar.
2538
+ - Extended the `@kerfjs/ui` SVG **design templates** (`ui/docs/design/templates/`) from the initial two components to fourteen — adding `Toolbar`, `ToolbarText`, `ListItem`, `ListHeader`, `ListActionRow`, `ValueTable`, `StateBanner`, `EmptyState`, `Skeleton`, `SegmentedControl`, `TabBar`/`AppTab`, and `TokenSearchField`, each captured (light + dark, system-font text, self-contained inlined libraries) in its common presentation combinations with realistic sample data. Added an offline sync-check gate (`check:design-templates`, part of `npm run check`) that verifies every manifest component/variant has its committed output and flags stray files. `Select` (Web Awesome runtime), the decorative icon primitives, `ResizableRegion`, the whole-screen layouts, and the `Catalog` shell are deliberately not templated (see `ui/docs/design/templates.md`).
2539
+ - Extended `@kerfjs/ui`'s `wireTokenSearchFields` with three opt-in knobs for dropping a real app's hand-rolled token-search plumbing: a **focusout keep-open exception** (`collapsible.keepOpenOn(target)` or a `data-token-search-keep-open` region) so focus moving to a sibling suggestions/date/help surface no longer collapses an empty collapsible field; **opt-in atomic-chip keyboard** (`keyboard`) where, from a collapsed caret with no selection, Backspace/Delete remove the adjacent chip (reported via `onRemoveToken` for the app to apply to its controlled state) and ArrowRight moves the caret past a trailing chip; and an **`onEdit({ id, editor })`** callback fired on every editor `input` so callers can drop their own `input` listener. All are additive and off/unchanged by default (the keyboard block is opt-in; the full `collapsible: false` opt-out and per-behavior opt-outs are unchanged).
2540
+ - Fixed a collapsible `@kerfjs/ui` `TokenSearchField` stretching to its full expanded width **in height** when placed directly in a flex column: the expanded size was carried on the flex `basis`, which a column reads as the cross-axis height (a ~480px-tall field). The field and its enclosing toolbar group now size their inline axis through `width` alone (`flex: 0 1 auto`), so the field stays a single line tall in any container while a toolbar row still shrinks it as before.
2541
+ - Added SVG **design templates** for `@kerfjs/ui` components (`ui/docs/design/templates/`): each component captured from its real rendered HTML/CSS in its common presentation combinations via `domotion-svg`, with a per-component library file that references the individual variant SVGs. A `design-templates:build` script + `ui/docs/design/templates.md` cover regeneration, maintenance, and templating your own app-level components. Initial coverage: PanelHeader and ToolbarControlGroup.
2542
+ - Fixed two `@kerfjs/ui/catalog` presentation bugs: header-action buttons that carry a text label (the built-in theme toggle) now render icon-beside-label instead of the toolbar group's icon-over-label grid, and the detail footer's related-entry selector is a single-bordered popup (a borderless `single` group around a label-less `fitMenu` select) rather than a bordered select nested inside a bordered group.
2543
+ - Dogfooded the UX demo's own shell onto `@kerfjs/ui/catalog` (`Catalog` + `wireCatalog`), which surfaced and fixed three `Catalog` bugs: an active entry inside a `secondarySections` group is now found for the detail header/footer; the brand subtitle's alignment override outranks the toolbar group's default gap (it lost specificity before, dropping the logo→title gap); and the detail footer's resource links stack above the related selector at narrow widths.
2544
+ - Extended `@kerfjs/ui/catalog`: `Catalog` gained a `secondarySections` prop — an optional, quieter "ecosystem" group of sections below the primary sidebar categories, with an optional disclosure toggle (`wireCatalog` gains `onToggleSecondary`) — and the subpath now also exports `CatalogExample` / `CatalogExampleStack`, labeled-example primitives that carry a first-class `align` prop (`'glyph'` = 16px, `'inline-control'` = 8px, `'none'`) to line a specimen's left edge up with its label, publishing the inset as `--kui-catalog-example-align` so a debug overlay can exclude it. This lets a gallery compose its previews from the shipped shell instead of hand-rolled example markup and CSS.
2545
+ - Reworked the `@kerfjs/ui` list-detail dialog recipe so its `PanelHeader` sits at the top of the detail column rather than spanning both columns; the master list and detail pane now each run full height top-to-bottom within the dialog.
2546
+ - **Breaking (`@kerfjs/ui`):** renamed the three menu primitives to their list names — `MenuHeader` → `ListHeader`, `MenuItem` → `ListItem`, `MenuActionRow` → `ListActionRow` — across every surface: the exported components, their subpaths (`@kerfjs/ui/list-header`, `/list-item`, `/list-action-row` and the matching `.css` subpaths), their `kui-menu-*` CSS classes (now `kui-list-*`), their `data-component` values, and the catalog. The sidebar navigation composition demo is likewise renamed from `menu` to `list`. There are no deprecated aliases; update imports, class references, and any `?component=menu` links. The names better reflect that these primitives compose any list or navigation surface, not only menus.
2547
+ - Added a reusable component-catalog shell to `@kerfjs/ui`: an opt-in, subpath-only `Catalog` component (`@kerfjs/ui/catalog`) plus a `wireCatalog` helper (`@kerfjs/ui/wire-catalog`) that render the collapsible category sidebar + titled preview stage + resources/related footer the kerf UI catalog uses, driven by your own `sections`, per-entry preview `content`, and `brand`. Controlled and stateless like the app layouts (the app owns `active`/`collapsed`/`theme`); `wireCatalog` wires selection, the related-entry selector, and the collapse/theme toggles, and can mirror the active id into the URL. See `ui/docs/catalog.md`.
9
2548
  - Gave `@kerfjs/ui`'s `PanelHeader` (and `ToolbarText`) an optional `headingLevel` prop: when set, the title exposes `role="heading"` with a matching `aria-level`, so a page or view heading is a real heading landmark for screen-reader navigation. The default is unchanged — a plain span referenced via `aria-labelledby` for dialog titles — so existing usage is backward-compatible. Restores the page-heading semantics lost when `PageHeader` was consolidated into `PanelHeader`.
10
- - Added a first-class loading-placeholder mode to `@kerfjs/ui`: a new unanimated `Skeleton` block (`@kerfjs/ui/skeleton`) plus a `placeholder` boolean on every value-bearing component (`Select`, `MenuHeader`, `MenuItem`, `MenuActionRow`, `ValueTableRow`, `PanelHeader`, `SegmentedControl`, `StateBanner`, `AppTab`, `ToolbarText`). In placeholder mode a component renders its real chrome with value slots as subtle skeleton blocks and its own controls disabled (`aria-busy`, dropped `data-action`), keeping sizes and shapes identical to the populated component, so a parent composes a faithful loading inspector/detail view without hand-rebuilding markup. Built on a Kerf-owned skeleton (not `wa-skeleton`) so the pure-Kerf primitives stay Web-Awesome-free. `Select`'s existing empty-hint string prop is renamed `placeholderText` to free `placeholder` for the loading boolean.
2549
+ - Added a first-class loading-placeholder mode to `@kerfjs/ui`: a new unanimated `Skeleton` block (`@kerfjs/ui/skeleton`) plus a `placeholder` boolean on every value-bearing component (`Select`, `ListHeader`, `ListItem`, `ListActionRow`, `ValueTableRow`, `PanelHeader`, `SegmentedControl`, `StateBanner`, `AppTab`, `ToolbarText`). In placeholder mode a component renders its real chrome with value slots as subtle skeleton blocks and its own controls disabled (`aria-busy`, dropped `data-action`), keeping sizes and shapes identical to the populated component, so a parent composes a faithful loading inspector/detail view without hand-rebuilding markup. Built on a Kerf-owned skeleton (not `wa-skeleton`) so the pure-Kerf primitives stay Web-Awesome-free. `Select`'s existing empty-hint string prop is renamed `placeholderText` to free `placeholder` for the loading boolean.
11
2550
  - Made `@kerfjs/ui`'s `wireTokenSearchFields` manage the collapsible `TokenSearchField`'s transient expand/collapse/focus by default (activate to reveal and focus, Escape or empty blur to collapse), holding that state in a signal exposed on the returned handle so an app can bind it in render, adopt its own via `collapsible.signals`, drive it through `open`/`close`, or opt any behavior out individually — removing the per-app boilerplate that had caused inconsistent transient-UI variation. `onSubmit` is now optional and the helper returns a `TokenSearchFieldsHandle` (a disposer that also exposes `expanded(id)`/`open`/`close`).
12
2551
  - Made the UX catalog's theme action follow the operating system's initial light/dark appearance, label itself with the appearance it will switch to, and explicitly override either direction when activated.
13
2552
  - Corrected the UX catalog's project-details dialog gutters so its header uses less side inset, its selected title uses the full content gutter, its value table fills the available detail width between the usual margins, and its record actions avoid a doubled left inset.
@@ -19,7 +2558,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
19
2558
  - Aligned the UX catalog's StateBanner tone labels with each banner's leading icon by deriving the shared 8px margin, 1px border, and 8px content inset in demo-only specimen chrome.
20
2559
  - Let the UX catalog's resizable-panel specimen fill its available preview height and moved its committed-width readout into the shared status footer.
21
2560
  - Reworked the UI catalog into collapsible pane chrome with toolbar headers, a full-stage checkerboard preview, compact resource footer, hidden-by-default floating recipe notes, and edge-aligned `PageHeader` actions.
22
- - Aligned multiline `MenuItem` and `MenuActionRow` leading icons with the first text line instead of centering them against the full wrapped label.
2561
+ - Aligned multiline `ListItem` and `ListActionRow` leading icons with the first text line instead of centering them against the full wrapped label.
23
2562
  - Fixed packages generated by `create-kerf-component` failing declaration builds under TypeScript 6 when tsup injects its deprecated `baseUrl` option.
24
2563
  - Fixed the complete kanban example's mobile layout by stacking its columns and header controls without horizontal overflow while preserving the wider board layout.
25
2564
  - Fixed the complete dashboard example's mobile layout so its status header, chart, and table fit the viewport without horizontal overflow.
@@ -67,28 +2606,28 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
67
2606
  - Removed the unsolicited command-palette catalog demo and production recipe while retaining the earlier, independent application-local adapter example for missing-concept guidance.
68
2607
  - Kept the `ResizableRegion` UX specimen readable by moving committed-width status below the controlled pane at narrow widths, preserving its 200% root-scaled split, and retaining local scroll access to the handle at its maximum size.
69
2608
  - Reworked the composer-form recipe into one coherent visible form surface with exactly three transparent 8/1/8 content sections, 24px major rhythm, and a conditional `StateBanner` as its only nested semantic surface.
70
- - Made `MenuHeader` fill its available inline width, align separate actions at the logical end in both LTR and RTL, and use an overridable 18px visible action glyph without shrinking the 44px target.
71
- - Added first-class `MenuHeader` count semantics with a required localized `countLabel`, zero-safe neutral pill presentation, accessible heading/disclosure naming, legacy non-count badge exclusivity, and runtime filtering for invalid counts and protected presence flags.
72
- - Made `MenuHeader` toggle mode supply the production 18px `DisclosureArrow` by default while retaining custom `actionIcon` replacement, removed competing raw SVG sizing/rotation, and made the menu-composition Tools disclosure control real content instead of showing a false Projects chevron.
2609
+ - Made `ListHeader` fill its available inline width, align separate actions at the logical end in both LTR and RTL, and use an overridable 18px visible action glyph without shrinking the 44px target.
2610
+ - Added first-class `ListHeader` count semantics with a required localized `countLabel`, zero-safe neutral pill presentation, accessible heading/disclosure naming, legacy non-count badge exclusivity, and runtime filtering for invalid counts and protected presence flags.
2611
+ - Made `ListHeader` toggle mode supply the production 18px `DisclosureArrow` by default while retaining custom `actionIcon` replacement, removed competing raw SVG sizing/rotation, and made the menu-composition Tools disclosure control real content instead of showing a false Projects chevron.
73
2612
  - Repaired the `DisclosureArrow` UX specimen by replacing its ambiguous sorting glyph with a right-facing arrow, putting both examples in independently controlled native buttons with stable names and `aria-expanded`, and demonstrating custom directions through delegated pointer and keyboard activation across narrow and 200%-zoom layouts.
74
2613
  - Set `DisclosureArrow` to an 18px root-scaled default, removed the catalog-only 24px enlargement, retained `--kui-disclosure-arrow-size` for scoped consumer overrides, and kept Kerf `Select` on its independent `.5` Web Awesome expand-glyph scale.
75
2614
  - Added a site-scoped CI dependency audit that includes the complete static-site build tree and fails on high or critical advisories while keeping low/moderate development-tool findings informational.
76
2615
  - Aligned the UX catalog's header-composition dialog with the shared 8px inline layout gutter while preserving its 16px vertical separation.
77
2616
  - Added visible `View demo source` and `Read guidance` affordances to every UI catalog detail, plus `View component source` for first-party components, backed by deterministic repository-relative paths for the main renderer, individual recipe files, Web Awesome specimens, and canonical browser-import implementations. Web Awesome entries explicitly label local documentation as Kerf integration guidance; all links use deploy-safe GitHub URLs and a responsive accessible layout.
78
2617
  - Added runtime-safe `AppTab.rootAttributes` for domain `data-*` metadata plus decorative `AppTab.closeIcon` and `ResizableRegion.handleIcon` slots, preserving existing tab and resize wiring ownership while blocking case-folded component/action/drag/drop collisions.
79
- - Added `MenuActionRow`, a full-width navigation row with sibling primary and trailing native-button actions, independent names and disabled states, controlled selection semantics, safe metadata/popover extension slots, and responsive 44px targets. `MenuItem.trailing` is now explicitly documented as dormant content.
80
- - Added safe typed MenuItem/MenuHeader extension slots for application `data-*` metadata and native popover trigger relationships while protecting component-owned action, selection, disclosure, accessible-name, disabled, icon, and native-button semantics.
2618
+ - Added `ListActionRow`, a full-width navigation row with sibling primary and trailing native-button actions, independent names and disabled states, controlled selection semantics, safe metadata/popover extension slots, and responsive 44px targets. `ListItem.trailing` is now explicitly documented as dormant content.
2619
+ - Added safe typed ListItem/ListHeader extension slots for application `data-*` metadata and native popover trigger relationships while protecting component-owned action, selection, disclosure, accessible-name, disabled, icon, and native-button semantics.
81
2620
  - Fixed keyboard deletion of a controlled `TokenSearchField` chip so `wireTokenSearchFields` restores focus and the text-relative caret after the application rerenders the editor.
82
2621
  - Aligned `@kerfjs/ui` CSS guidance and AI regression scoring with the catalog's exact public anatomy: documented public-class composition selectors are supported, private tag/id/attribute/unlisted-class descendants remain rejected, and historical measured runs replay under their recorded stricter oracle.
83
2622
  - Added typed `ValueTableRow` composition with a first-class optional icon hook. Value-table separators now align 8px from both edges for iconless rows and 40px from the left plus 8px from the right for rows with a 24px leading icon.
84
2623
  - Added first-class collapsible `TokenSearchField` support: an empty closed field becomes one iconic search action, controlled activation animates to the complete searchbox, and text or tokens keep it expanded after blur. It works standalone or when composed inside `ToolbarControlGroup`; the UX demo demonstrates that common toolbar composition at wide and narrow sizes.
85
2624
  - Restored the public documentation site to its Astro + Starlight implementation while the Kerf UI redesign is reconsidered, and removed the in-progress `@kerfjs/ui` page and promotional links from public navigation.
86
2625
  - Fixed `TokenSearchField` first-line alignment so the leading icon, editable text, clear action, and trailing content share the field's vertical center while remaining pinned when text wraps.
87
- - Replaced sidebar-specific and wrapper-inset UI geometry with one shared pane/content model across sidebars, main areas, inspectors, and dialogs. Unpadded panes now compose optional toolbars, one scrolling content stack, and optional footers; content children own consistent 8px margin, 1px transparent-or-visible border, 8px padding, 12px or 22px radii, and 24px major separation. Toolbar groups retain 44px geometry even with transparent chrome, and `MenuHeader` now supports a badge while separating dormant title content from its action.
2626
+ - Replaced sidebar-specific and wrapper-inset UI geometry with one shared pane/content model across sidebars, main areas, inspectors, and dialogs. Unpadded panes now compose optional toolbars, one scrolling content stack, and optional footers; content children own consistent 8px margin, 1px transparent-or-visible border, 8px padding, 12px or 22px radii, and 24px major separation. Toolbar groups retain 44px geometry even with transparent chrome, and `ListHeader` now supports a badge while separating dormant title content from its action.
88
2627
  - Added pixel-first `remify(<px>)` authoring for `@kerfjs/ui` styles. The build converts values exactly against a 16px baseline into ordinary `rem`, package CSS exports now deliver compiled `dist/styles/` files, and the UX catalog applies the same transform during Vite development so CSS edits remain hot-reloaded.
89
2628
  - Added a side-effect-free `@kerfjs/ui/webawesome` TypeScript declaration subpath for all 70 catalog-supported Web Awesome elements, synchronized against the installed custom-elements manifest and covered by a downstream package-export compile fixture.
90
2629
  - Added a deterministic internal `@kerfjs/ui` AI-regression foundation with seven task-shaped prompts, frozen versioned context conditions, AST-aware structural scoring, generated public declaration signatures, equivalent supported import scoring in suite v2, and an opt-in non-executing TypeScript compile-evidence sidecar with exact response/config/package/declaration hashes. Adversarial fixtures catch component substitution, duplicated primitives, and spacing/layout ownership regressions without invoking a live model in CI.
91
- - Added seven lazy, production-backed `@kerfjs/ui` composition recipes for application shells, sidebars, workspace headers, master-detail dialogs, composer forms, list-state lifecycles, and mixed-control toolbars, with stable catalog routes, canonical AI metadata, drift gates, responsive/keyboard/state coverage, and a copyable stable-root adapter that wires delegated actions, form/dialog events, resize commits, and idempotent disposal outside the catalog.
2630
+ - Added seven lazy, production-backed `@kerfjs/ui` composition recipes for application shells, sidebars, workspace headers, list-detail dialogs, composer forms, list-state lifecycles, and mixed-control toolbars, with stable catalog routes, canonical AI metadata, drift gates, responsive/keyboard/state coverage, and a copyable stable-root adapter that wires delegated actions, form/dialog events, resize commits, and idempotent disposal outside the catalog.
92
2631
  - Added a shipped, schema-described `@kerfjs/ui` machine-readable component catalog with deterministic typed UX projection and drift gates for public exports, package/CSS/registration paths, relationships, Web Awesome manifest coverage, AI guidance, routes, CSS hooks, and documentation links.
93
2632
  - Added a canonical `@kerfjs/ui/layout.css` vocabulary for responsive page, pane, section, control, metadata, surface, dialog, and scroll ownership, with semantic variables, compact/narrow behavior, consumer and AI guidance, normalized catalog compositions, and wide/intermediate/narrow/200%-zoom geometry coverage.
94
2633
  - Added a need-first `@kerfjs/ui` component-selection matrix and automated drift gate so AI tools choose among reuse, composition, thin application adapters, custom semantics, and overlapping Web Awesome components without inferring contracts from implementation CSS. A typed application-local command-palette example now shows how a missing package concept still uses canonical layout, one inset owner, and related-control grouping while keeping semantics honest and proposing recurring behavior upstream.
@@ -111,7 +2650,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
111
2650
  - Made the Web Awesome intersection, mutation, and resize observer UX specimens interactive, with deliberate triggers, visible live-event results, and browser coverage.
112
2651
  - Updated the UI package's Vitest toolchain to 4.1.11 and resolved the remaining development-only npm audit advisories without changing its runtime dependency surface.
113
2652
  - Added a first-class controlled `SegmentedControl` to `@kerfjs/ui`, with Hot Sheet 2-aligned toolbar, rounded-rectangle, pill, equal-width, small, and disabled presentations; native pressed-button semantics; scoped palette overrides; tree-shakeable component CSS; and a complete UX-catalog route.
114
- - Aligned `@kerfjs/ui` defaults with Hot Sheet 2, added overridable semantic state palettes, completed ToolbarControlGroup and StateBanner catalog variants, simplified related-component navigation, corrected MenuHeader action alignment, and added a controlled horizontally scrolling TabBar with pointer and keyboard reordering plus proximity-based edge autoscroll.
2653
+ - Aligned `@kerfjs/ui` defaults with Hot Sheet 2, added overridable semantic state palettes, completed ToolbarControlGroup and StateBanner catalog variants, simplified related-component navigation, corrected ListHeader action alignment, and added a controlled horizontally scrolling TabBar with pointer and keyboard reordering plus proximity-based edge autoscroll.
115
2654
  - Updated `@kerfjs/ui` to develop and test against Lucide 1.43.0 and Web Awesome 3.12.0, with matching peer baselines.
116
2655
  - Added an opt-in `@kerfjs/ui/webawesome.css` theme for Web Awesome's free components. It carries the Hot Sheet 2-aligned semantic palette, form and panel geometry, focus, tooltip, radius, and shadow choices without registering component JavaScript; the UX catalog now lists all 70 free Web Awesome 3.12 components under a collapsible ecosystem section, gives each a focused themed route and dependency links, and retains the aggregate light, dark, narrow, and scoped-override gallery.
117
2656
  - Fixed trapped overlays losing sequential focus in WebKit after the second control. Implicitly focusable descendants now receive `tabindex="0"` inside the trap, while authored tabindex values (including `-1`) remain intact.
@@ -120,9 +2659,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
120
2659
  - Added the lockstep `@kerfjs/ui` package: accessible toolbar, menu, tab, header, resize, select, and feedback primitives; semantic CSS tokens and browser component subpaths that automatically include only reachable component styles; CSS-free root/SSR entries and an optional complete stylesheet; opt-in Web Awesome registration; AI-oriented docs; a production-backed UX catalog; bundle/unit/three-engine browser gates; and a least-privilege release workflow.
121
2660
  - The virtual-list demo animation opens on the full app again (header, the "N in the DOM" badge, and the list) before the scroll; the list now scrolls inside its own box while the header stays put (previously the whole frame was a full-bleed list strip), holds at row 0 before gliding instead of starting a few rows down, and the loop stays in phase on every pass. Captured with domotion-svg 0.28.2.
122
2661
 
123
- ## [4.4.1] - 2026-08-26
124
-
2662
+ - @kerfjs/ui panel toolbars now place groups in leading, center, and trailing zones; relocateOnCollapse keeps chosen groups available when a panel closes.
125
2663
 
2664
+ ## [4.4.1] - 2026-08-26
126
2665
 
127
2666
  - Overhauled the README landing page: new one-line tagline, npm/size/license/TypeScript-types badges, a hoisted "Quick start" (install + `tsconfig`) right under the hook, and a jump nav (Quick start · Why kerf · Quick tour · Docs). The counter example now shows `delegate()` wiring the click handler.
128
2667
  - Reworked the homepage hero to lead with the value proposition ("Reactive UI that touches only the bytes that changed"), with "Introducing Kerf" demoted to an eyebrow, a right-sized logo, tighter vertical spacing, and "Get started" as the single filled primary action ("View examples" now a secondary/outline button).
@@ -131,7 +2670,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
131
2670
  - Rewrote the site's docs pages as hand-authored consumer content (cleaner headings, no internal section numbers) rather than verbatim copies of the internal design docs.
132
2671
  - Renamed the migration guides' "Side-by-side code" heading to "Section by section" (the sections stack code blocks rather than showing true columns), across all framework pages.
133
2672
 
134
-
135
2673
  - The getting-started and Markdown-editor demo animations now open on a non-blank frame — real code and a rendered heading are visible immediately instead of an empty pane.
136
2674
  - Added a horizontal-scroll shadow affordance to wide tables so off-screen columns are discoverable on narrow viewports.
137
2675
  - Shortened the longest demo alt text on the router and virtual-list pages for more concise screen-reader output.
@@ -139,29 +2677,23 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
139
2677
 
140
2678
  ## [4.4.0] - 2026-08-23
141
2679
 
142
-
143
-
144
2680
  - Reworked the router example app to run inside a fake browser window — traffic-light chrome, working Back/Forward buttons wired to `router.back()`/`forward()` via one delegated listener, and a live address bar bound to `router.route` that updates as you navigate — making the URL-driven, no-reload story clearer.
145
2681
 
146
2682
  - **New `kerfjs/router` subpath — a client-side router (the "postcard router").** `createRouter({ routes, mode?, base?, interceptLinks? })` returns a handle over three things kerf already has: a reactive `route` signal (`{ path, params, query, hash }`), `delegate()`-based `<a href>` link interception, and a keyed **outlet** — `router.outlet()` renders the matched route in a `data-key`ed wrapper, so kerf's keyed morph **replaces the page wholesale on a route change** (fresh DOM) and **reconciles in place on a same-route param change** (preserving scroll / focus). Route patterns are static, `:param`, a trailing `*rest` wildcard, and `*` catch-all; the handle also gives `navigate(path, { replace?, state? })`, `back()`/`forward()`, `match(pattern)` / `activeClass(pattern, className)` reactive active-link helpers, hash **or** history mode, an optional base path, and `dispose()`. Link interception is automatic (same-origin, left-click, no modifier/`target`/`download`, opt out per-link with `data-router-ignore` / `rel="external"` or globally with `interceptLinks: false`). **Deliberately scoped** — no nested layouts, data loaders, lazy routes, guards, or SSR matching; compose those with kerf primitives (`resource` for loading, an `effect` on `route` for guards). The kerf **core stays router-free** — this is opt-in and tree-shakeable, adding nothing to the main barrel until imported, and docs/1's "Not a router" is about the runtime. See [`docs/20-router.md`](docs/20-router.md).
147
2683
 
148
2684
  ## [4.3.0] - 2026-08-22
149
2685
 
150
-
151
-
152
2686
  - KF-529: refresh README for the 4.3 cycle (prep-major-release) (`909c82a`)
153
2687
  - KF-530: extract promise-dialog helpers into overlay-dialogs.ts (`5d4ac38`)
154
2688
  - KF-528: sync llms.txt with docs/19 + 4.3 features (`1d9c004`)
155
2689
 
156
2690
  - **`kerfjs/overlay` gains opt-in native top-layer backing (`native: true`).** Every overlay surface (`overlay`, `confirm`, `prompt`, `form`, `choice`, `popover`, `tooltip`) now takes `native?: boolean` (default `false`). When `true` and the engine supports it, a **modal** surface (`trap: true`) is hosted in a `<dialog>` opened with `.showModal()` — real document inerting (pointer + focus + AT) and guaranteed stacking above any `z-index` — and a **non-modal** surface (`trap: false`) uses the **Popover API** (`[popover]` + `showPopover()`). Feature-detected (`HTMLDialogElement.prototype.showModal`, `HTMLElement.prototype.showPopover`), falling back to today's plain `<div>` where unsupported, so `native: true` is always safe to pass. The `render` slot + promise API are unchanged — kerf just hosts your markup in a `<dialog>` / `[popover]`. **Opt-in on purpose:** the native elements carry UA default styles (a `::backdrop`, centering, border, padding) that kerf does **not** reset (a reset would violate the zero-CSS contract) — style the element and its `::backdrop` via `className` (`.kerf-overlay::backdrop { … }`, the stable contract); and `container` becomes a visual no-op in native mode (the top layer ignores DOM position). See [`docs/19-native-overlay-backing.md`](docs/19-native-overlay-backing.md).
157
- - **`bindList` virtualization gains a `content-visibility` mode.** `virtualize: { rowHeight, mode: 'content-visibility' }` is a second virtualization strategy alongside the default `mode: 'window'` (today's JS windowing). It keeps **every** row in the DOM and sets `content-visibility: auto` + `contain-intrinsic-size: 0 <rowHeight>px` on each row, so a supporting engine (Chromium, Safari 18) skips the *layout/paint* of off-screen rows while **all rows stay findable** — find-in-page (Cmd/Ctrl+F), the accessibility tree, and anchor links / `scrollIntoView` all work on any row (the exact guarantee `mode: 'window'` can't give, since it removes off-window rows from the DOM). The `mode` choice is the app's and it's about list size: pick `'content-visibility'` for medium lists where findability beats the node ceiling, keep `'window'` for very large (100k-row) lists. In this mode `rowHeight` is only the `contain-intrinsic-size` placeholder (no windowing math), `setHeight` / `observeRowHeights` are no-ops (the browser owns measurement), `minRows` is ignored (all rows already render), and no scroll listener / `ResizeObserver` is installed — while `handle.container` / `containerClass` / `containerId` still work. There is deliberately **no feature detection**: on an engine without `content-visibility` the CSS is inert, so all rows still render (correct, still findable) — only the off-screen-skip optimization is absent. See [`docs/17-list-virtualization.md`](docs/17-list-virtualization.md) §17.11.
2691
+ - **`bindList` virtualization gains a `content-visibility` mode.** `virtualize: { rowHeight, mode: 'content-visibility' }` is a second virtualization strategy alongside the default `mode: 'window'` (today's JS windowing). It keeps **every** row in the DOM and sets `content-visibility: auto` + `contain-intrinsic-size: 0 <rowHeight>px` on each row, so a supporting engine (Chromium, Safari 18) skips the _layout/paint_ of off-screen rows while **all rows stay findable** — find-in-page (Cmd/Ctrl+F), the accessibility tree, and anchor links / `scrollIntoView` all work on any row (the exact guarantee `mode: 'window'` can't give, since it removes off-window rows from the DOM). The `mode` choice is the app's and it's about list size: pick `'content-visibility'` for medium lists where findability beats the node ceiling, keep `'window'` for very large (100k-row) lists. In this mode `rowHeight` is only the `contain-intrinsic-size` placeholder (no windowing math), `setHeight` / `observeRowHeights` are no-ops (the browser owns measurement), `minRows` is ignored (all rows already render), and no scroll listener / `ResizeObserver` is installed — while `handle.container` / `containerClass` / `containerId` still work. There is deliberately **no feature detection**: on an engine without `content-visibility` the CSS is inert, so all rows still render (correct, still findable) — only the off-screen-skip optimization is absent. See [`docs/17-list-virtualization.md`](docs/17-list-virtualization.md) §17.11.
158
2692
  - **Docs: `bindList` virtualization findability/a11y tradeoff is now a first-class caveat.** Off-window rows are removed from the DOM (not just hidden), so with `virtualize` set, find-in-page (Cmd/Ctrl+F), screen readers / the accessibility tree, and anchor links / `scrollIntoView` reach only the visible window. New `docs/17-list-virtualization.md` §17.10 spells out the consequences and the guidance (don't virtualize, or use `minRows` above the list length, when full findability matters more than the DOM node ceiling), and the `bindList` JSDoc + `docs/8-api-reference.md` §8.11 carry the same note. Behavior unchanged — documentation only.
159
2693
  - **State-preserving row moves via `moveBefore()` (transparent optimization).** When a keyed list reorders — `each()` (snapshot and granular paths), `bindList`, and `morph()`'s keyed / positional / list-marker moves — kerf now relocates an already-connected row with `Node.prototype.moveBefore()` where the engine supports it (Chromium 133+, spreading to other engines), falling back to `insertBefore()` everywhere else. `moveBefore()` is an atomic move: the node is never disconnected, so a moved row keeps its focus, text selection, `<iframe>` document state, playing media, running CSS transitions/animations, and open `popover`/`dialog` state across the reorder — richer state than the existing focus snapshot in the reconciler could ever restore, and it needs no snapshot at all where it runs. No API change and no behavior change on engines without `moveBefore()`; the focus-preservation snapshot stays in place for them. Fresh (not-yet-connected) rows still use `insertBefore()` — only genuine moves of connected rows take the new path. See [`docs/18-state-preserving-moves.md`](docs/18-state-preserving-moves.md).
160
2694
 
161
2695
  ## [4.2.0] - 2026-08-20
162
2696
 
163
-
164
-
165
2697
  - Added a **Virtual list** example app — a 10,000-row virtualized list showcasing the companion subpaths together: `kerfjs/list` viewport virtualization (only a screenful in the DOM), `kerfjs/timing` debounced search, and `kerfjs/overlay` confirm-to-delete with a toast. Includes a live "in the DOM" counter that stays flat as you scroll all 10,000 rows.
166
2698
  - Expanded the README with a companion-subpaths spotlight covering `list`, `overlay`, `async`, `scope`, `timing`, `remount`, `attach`, and `actions`, with `bindList` fixed and measured-height virtualization examples.
167
2699
  - Corrected the complete-apps index to accurately describe the eight standalone showcase apps and note that the cart and counter-store apps live in the migration guides.
@@ -175,86 +2707,70 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
175
2707
  - **New `kerfjs/remount` subpath** — `remountOn(parent, key, render)`, the opposite of kerf's morph-by-default: **replace** a subtree wholesale when `key` changes instead of morphing it. Names the hand-rolled `data-key={`gen-${n}`}` + `data-morph-skip` counter trick, for library-owned subtrees (a highlighted diff, a chart, an editor) that must tear down and re-initialize on fresh DOM. `key` is a signal or a thunk `() => K`; an unchanged key (including a thunk whose inputs moved but whose value stayed equal) leaves the subtree alone, so per-row reactivity inside `render` still updates in place. An optional `onMount(root)` callback runs after each (re)mount with the live subtree — the place to bind an imperative widget (`kerfjs/attach`) to the fresh DOM; returning `attach`'s disposer from it makes teardown synchronous. `remountOn` owns `parent`'s children and returns a disposer. Optional and tree-shakeable.
176
2708
  - **New `kerfjs/timing` subpath** — the `let timer; clearTimeout(timer); timer = setTimeout(…)` pattern every app hand-rolls, blessed with disposer-shaped ergonomics. `debounce(fn, ms)` is trailing-edge (runs once `ms` after calls stop, latest args); `throttle(fn, ms)` is leading-plus-trailing (fires immediately, then at most once per `ms`, collapsing a burst to one trailing call). Both return a callable with `cancel()` / `flush()`. `debouncedSignal(source, ms)` is a read-only signal that trails `source` by `ms` so it composes inside the reactive graph (`computed`/`effect`/`mount`) instead of beside it. `debounce`/`throttle` are dependency-free; `debouncedSignal` pulls in signals only (no render core). Optional and tree-shakeable; tiny.
177
2709
  - **New `kerfjs/scope` subpath** — tie disposers to a DOM element's lifetime, so append-heavy UIs stop leaking detached-but-subscribed effects/listeners. `disposeScope(el)` returns a WeakMap-keyed, accumulating scope whose `add(disposer)` (plus convenience `mount` / `effect` / `delegate` wrappers that register their own disposer) collects teardown; `dispose()` runs it all best-effort and idempotently. `disposeSubtree(root)` sweeps a subtree before removal; `observeRemovals(root)` installs one `MutationObserver` that auto-disposes on removal. No module-level mutable state. Optional and tree-shakeable.
178
- - **New `kerfjs/overlay` subpath** — the blessed modal/overlay + dismiss manager that every real kerf app hand-rolls. `overlay(content, options?)` appends a wrapper, `mount()`s content inside it (owning the disposal), wires dismissals (Escape / backdrop / outside-click, with `outsideIgnore`), a focus trap (`role="dialog"` / `aria-modal`, Tab wrap-around, restore-focus-on-close), and returns `{ el, close(result?), result }`. `confirm(message, options?)` is a promise-based `window.confirm` replacement, and `prompt(message, options?)` → `Promise<string | null>` its `window.prompt` counterpart (both globals are no-ops in Tauri webviews); `form(fields, options?)` → `Promise<Record<string, string> | null>` collects a two-or-three-field dialog. `choice<R>(message, actions, options?)` → `Promise<R | null>` is the **N-way** sibling of `confirm` — one button per action resolves that action's `value` (or `null` on dismissal), and `options.defaultValue` makes **Enter** (anywhere in the dialog) resolve a default action (the "global Enter-to-confirm" model) without holding the overlay handle. `prompt`/`form` submit on Enter and take an inline `validate`; all auto-escape their content. `confirm` / `prompt` / `form` also accept a **`render` slot option** for design-system teams — return your own markup and spread the provided `ok`/`cancel` (+ `input`/`error`) wiring onto it; kerf keeps owning the promise, `validate`, Enter-submit, dismiss, focus-trap, and focus-restore, so you adopt the batteries-included dialogs without a CSS rewrite. `popover(anchor, content, options?)` → `OverlayHandle` is a non-modal **anchored** overlay: it positions the content relative to `anchor` (below by default, flipping above on viewport overflow, clamped horizontally), defaults to dismiss-on-outside with the anchor exempt, and repositions on scroll / resize. `popover`'s placement core is also exported standalone: **`positionAnchored(el, anchor, options?)`** (one-shot) and **`autoReposition(el, anchor, options?)`** (keeps an element positioned on scroll/resize, returns a disposer) position *your own* element with no overlay lifecycle. **`tooltip(anchor, content, options?)`** is a hover/focus-triggered, non-modal, auto-hiding tooltip built on them. `toast(content, options?)` now returns a **`ToastHandle` (`{ el, dismiss }`)** instead of a bare dismiss function, so callers can inspect the node, wire an action button, or run entrance/exit transitions; new options: `mode: 'replace'` (collapse a rapid sequence to the latest) with `collapse: 'fade'` (default — run the prior toast's exit transition, good for a stacking region) or `'instant'` (remove it synchronously, what a single centered slot wants so messages never cross-fade in place), `variant` (`'info'`/`'success'`/`'warning'` → a `${className}--${variant}` accent class), `enterClass` (added on the next animation frame for a CSS entrance) and `exitClass` + `exitDuration` (CSS owns the exit — on dismiss the `enterClass` is **removed** so `exitClass` needn't out-specify it, and a symmetric single-class fade works with just `enterClass` + `exitDuration`; the node is removed after the delay). `dismiss({ instant: true })` removes the toast **synchronously** (skipping the exit transition), and `mode: 'replace'` `collapse: 'instant'` also force-removes a toast that is already mid-fade — so an action button that dismisses itself and shows a replacement in a single centered slot doesn't cross-fade the two. **Breaking (beta):** `toast()`'s return type changed from `() => void` to `{ el, dismiss }` — call `toast(...).dismiss()` or destructure `{ dismiss }`. Structural only — kerf ships no CSS. Optional and tree-shakeable; shares the core with the main barrel via code-splitting.
2710
+ - **New `kerfjs/overlay` subpath** — the blessed modal/overlay + dismiss manager that every real kerf app hand-rolls. `overlay(content, options?)` appends a wrapper, `mount()`s content inside it (owning the disposal), wires dismissals (Escape / backdrop / outside-click, with `outsideIgnore`), a focus trap (`role="dialog"` / `aria-modal`, Tab wrap-around, restore-focus-on-close), and returns `{ el, close(result?), result }`. `confirm(message, options?)` is a promise-based `window.confirm` replacement, and `prompt(message, options?)` → `Promise<string | null>` its `window.prompt` counterpart (both globals are no-ops in Tauri webviews); `form(fields, options?)` → `Promise<Record<string, string> | null>` collects a two-or-three-field dialog. `choice<R>(message, actions, options?)` → `Promise<R | null>` is the **N-way** sibling of `confirm` — one button per action resolves that action's `value` (or `null` on dismissal), and `options.defaultValue` makes **Enter** (anywhere in the dialog) resolve a default action (the "global Enter-to-confirm" model) without holding the overlay handle. `prompt`/`form` submit on Enter and take an inline `validate`; all auto-escape their content. `confirm` / `prompt` / `form` also accept a **`render` slot option** for design-system teams — return your own markup and spread the provided `ok`/`cancel` (+ `input`/`error`) wiring onto it; kerf keeps owning the promise, `validate`, Enter-submit, dismiss, focus-trap, and focus-restore, so you adopt the batteries-included dialogs without a CSS rewrite. `popover(anchor, content, options?)` → `OverlayHandle` is a non-modal **anchored** overlay: it positions the content relative to `anchor` (below by default, flipping above on viewport overflow, clamped horizontally), defaults to dismiss-on-outside with the anchor exempt, and repositions on scroll / resize. `popover`'s placement core is also exported standalone: **`positionAnchored(el, anchor, options?)`** (one-shot) and **`autoReposition(el, anchor, options?)`** (keeps an element positioned on scroll/resize, returns a disposer) position _your own_ element with no overlay lifecycle. **`tooltip(anchor, content, options?)`** is a hover/focus-triggered, non-modal, auto-hiding tooltip built on them. `toast(content, options?)` now returns a **`ToastHandle` (`{ el, dismiss }`)** instead of a bare dismiss function, so callers can inspect the node, wire an action button, or run entrance/exit transitions; new options: `mode: 'replace'` (collapse a rapid sequence to the latest) with `collapse: 'fade'` (default — run the prior toast's exit transition, good for a stacking region) or `'instant'` (remove it synchronously, what a single centered slot wants so messages never cross-fade in place), `variant` (`'info'`/`'success'`/`'warning'` → a `${className}--${variant}` accent class), `enterClass` (added on the next animation frame for a CSS entrance) and `exitClass` + `exitDuration` (CSS owns the exit — on dismiss the `enterClass` is **removed** so `exitClass` needn't out-specify it, and a symmetric single-class fade works with just `enterClass` + `exitDuration`; the node is removed after the delay). `dismiss({ instant: true })` removes the toast **synchronously** (skipping the exit transition), and `mode: 'replace'` `collapse: 'instant'` also force-removes a toast that is already mid-fade — so an action button that dismisses itself and shows a replacement in a single centered slot doesn't cross-fade the two. **Breaking (beta):** `toast()`'s return type changed from `() => void` to `{ el, dismiss }` — call `toast(...).dismiss()` or destructure `{ dismiss }`. Structural only — kerf ships no CSS. Optional and tree-shakeable; shares the core with the main barrel via code-splitting.
179
2711
  - **New `kerfjs/actions` subpath** — the blessed delegated action-table helper. `action(value)` returns a `data-action` `AttrSpec` (a thin specialization of `attr()`); `delegateActions(root, eventType, table, options?)` wires a whole table of `data-action` handlers with one delegated listener (built on `delegate()`) and returns a disposer. Formalizes the most-reinvented idiom in real kerf apps — one `attr('data-action', …)` table as the single source of truth for both the JSX attribute and the delegate dispatch. Optional and tree-shakeable; adds nothing to the main barrel.
180
2712
 
181
2713
  ## [4.1.1] - 2026-08-14
182
2714
 
183
-
184
-
185
2715
  - Added a CDN / importmap quickstart (`docs/6-jsx-runtime.md` §6.11.1) covering three no-install ways to load kerf from an ESM CDN: a direct `esm.sh` import, and jsDelivr / unpkg behind an importmap that also maps `@preact/signals-core`. Explains why a raw `dist/*.js` path fails (its unrewritten bare `@preact/signals-core` import won't resolve in the browser) and recommends pinning to a major version.
186
2716
  - Updated the README no-build example to use version-pinned CDN URLs (`kerfjs@4`) and noted that jsDelivr / unpkg need an importmap while esm.sh works with a direct import.
187
2717
 
188
2718
  ## [4.1.0] - 2026-07-31
189
2719
 
190
-
191
-
192
2720
  - ESLint 10 is now supported: `eslint-plugin-kerfjs` declares `peerDependencies.eslint` as `^9.0.0 || ^10.0.0`, and every named major is exercised by the rule suite in CI.
193
2721
  - The plugin now runs its rule suite against each supported ESLint major via `npm run test:eslint-matrix`, which also fails if the declared peer range, the tested set, and the CI matrix disagree — or if the range is left open-ended.
194
2722
 
195
-
196
2723
  - **ESLint 8 is no longer supported by `eslint-plugin-kerfjs`.** The package is ESM-only and ESLint 8 resolves `.eslintrc` plugins with `require()`, so `extends: ["plugin:kerfjs/…"]` could never load it.
197
2724
  - **Legacy `.eslintrc` configuration is documented as unsupported.** Use flat config (`eslint.config.js`); the `legacy-recommended` export remains in the package but is unreachable through any config system and should be treated as deprecated.
198
2725
 
199
-
200
2726
  - Generated Hot Sheet skill and rule files no longer hardcode a machine's local API port or shared secret. The curl fallbacks read `$HOTSHEET_PORT` / `$HOTSHEET_SECRET` from `.hotsheet/settings.local.json` and `.hotsheet/secret.json` instead, with `.hotsheet/settings.json` as the fallback for older projects.
201
2727
  - A new `npm run check:audit` gate runs `npm audit --omit=dev --audit-level=high` against the published dependency tree and is wired into the pre-push `check:full` gate.
202
2728
 
203
-
204
2729
  - Test tooling moved to vitest 4 and ESLint 10; coverage thresholds were recalibrated to 98.5% branches / 99.5% statements (lines and functions stay at 100%) after vitest 4's sharper AST-based coverage mapping resolved seventeen previously-miscredited defensive branches.
205
2730
  - Removed the `hs-m` marketing-ticket skill and Cursor rule.
206
2731
 
207
2732
  ## [4.0.0] - 2026-07-28
208
2733
 
209
-
210
-
211
2734
  - `draggable`, `spellCheck`, and `contentEditable` no longer accept booleans — they are enumerated HTML attributes, so write the keyword string (`draggable="true"`, `spellCheck="false"`). The boolean forms rendered markup that meant the opposite of what was written; real boolean attributes like `hidden`/`checked`/`disabled` are unchanged.
212
2735
  - `<select value>` / `<textarea value>` (and their `defaultValue` forms) no longer typecheck — neither element has a `value` content attribute, so kerf was emitting markup no browser reads. Use `<option selected>` and `<textarea>{draft}</textarea>`.
213
- - Lowercase `autofocus` no longer accepts `"true"` / `"false"` — it is a real boolean attribute, so `autofocus="false"` turned autofocus *on*. Use `autofocus={false}` or omit it.
214
-
2736
+ - Lowercase `autofocus` no longer accepts `"true"` / `"false"` — it is a real boolean attribute, so `autofocus="false"` turned autofocus _on_. Use `autofocus={false}` or omit it.
215
2737
 
216
2738
  - `href="javascript:void(0)"` and its five sibling spellings are no longer dropped by the URL screen. Dropping the `href` unmade the anchor — it lost `:link` styling, keyboard focus, and its pointer cursor. Matching is against the whole normalized value, so nothing can ride along after an inert body.
217
2739
  - `defaultSelected` rendered as `defaultselected`, an attribute no browser reads, so the option it named was never pre-selected. It now correctly emits `selected`.
218
2740
 
219
-
220
2741
  - Typed JSX gained a wide set of modern attributes: globals `inert`, `popover`, `nonce`, `part`/`exportparts`, `enterKeyHint`, `translate`, `autocorrect`, and the full microdata family; per-element `<button popoverTarget/popoverTargetAction/command>`, `<input popoverTarget/popoverTargetAction>`, `<form rel>`, `<source width/height>`, and `writingsuggestions`.
221
2742
  - `<button command>` is now typed to the spec keywords (`show-modal`, `close`, `request-close`, `toggle-popover`, `show-popover`, `hide-popover`) plus any `--custom` command, catching a custom command written without its required `--` prefix.
222
2743
  - `<a download>` and `<area download>` accept the bare boolean form as well as a string filename, so `<a href="/report.pdf" download>` compiles.
223
2744
 
224
-
225
2745
  - The advertised bundle size is corrected everywhere: ~12 KB min+gzip for a realistic import, ~13 KB with `arraySignal` (previously stated as ~11/~12 KB, and ~6.1 KB in the Cursor rules). Every migration page's delta row was recomputed against the new figure.
226
2746
  - The JSX runtime doc gained a section on enumerated vs. boolean attributes, and the API reference now records where the JSX types come from (WHATWG HTML Living Standard + SVG 2, not another framework's property table).
227
2747
  - The dev-warning sections in the dev-warnings doc were renumbered by family so the `11.2.N` headings run in document order; cross-references elsewhere in the docs were updated to match.
228
2748
 
229
-
230
- - The bundle-size check now gates the *prose* as well as the build: every place the docs advertise a size is matched against the measured figure, and a reworded claim that stops matching fails rather than going silently unchecked.
2749
+ - The bundle-size check now gates the _prose_ as well as the build: every place the docs advertise a size is matched against the measured figure, and a reworded claim that stops matching fails rather than going silently unchecked.
231
2750
  - New repo checks wired into `npm run check`: `check:docs:dev-warns` (the dev-warning doc and `ENV_NAME` must name the same set, with monotonic section numbers), `check:design-rule-5` (every top-level `let` in `src/` must be accounted for by the documented rule), and `check:skills` (a skill file restating a threshold its owner disagrees with fails).
232
2751
 
233
2752
  ## [3.0.0] - 2026-07-27
234
2753
 
235
-
236
-
237
2754
  - **kerf no longer infers development mode — add one line to your entry to keep the dev diagnostics.**
238
2755
 
239
2756
  ```js
240
- if (import.meta.env.DEV) await import('kerfjs/dev'); // Vite
241
- if (process.env.NODE_ENV !== 'production') await import('kerfjs/dev'); // webpack / Node
2757
+ if (import.meta.env.DEV) await import("kerfjs/dev"); // Vite
2758
+ if (process.env.NODE_ENV !== "production") await import("kerfjs/dev"); // webpack / Node
242
2759
  ```
243
2760
 
244
2761
  Without it you get production shape: no dev warnings, no read-only `defineStore` `get()`
245
2762
  snapshot, and a screened dangerous URL warns-and-drops instead of throwing. Nothing else
246
2763
  changes, and if you never used the dev warnings you get a smaller bundle for free.
247
2764
 
248
- **Why it changed.** kerf inferred its own dev/prod mode by reading `globalThis.process?.env?.NODE_ENV`. Bundlers substitute the *bare* `process.env.NODE_ENV` token and never create a `globalThis.process` object for browser targets, so that read was `undefined`, `undefined !== 'production'` was `true`, and every production browser build silently took the development path. Consequences that were shipping: every `defineStore` `get()` returned a deep read-only `Proxy` (an allocation on every store read), a screened `javascript:`/`data:` URL **threw** instead of the documented warn-and-drop, and the always-on list-key warnings printed to production consoles. Server/Node builds, where `process` exists, were unaffected.
2765
+ **Why it changed.** kerf inferred its own dev/prod mode by reading `globalThis.process?.env?.NODE_ENV`. Bundlers substitute the _bare_ `process.env.NODE_ENV` token and never create a `globalThis.process` object for browser targets, so that read was `undefined`, `undefined !== 'production'` was `true`, and every production browser build silently took the development path. Consequences that were shipping: every `defineStore` `get()` returned a deep read-only `Proxy` (an allocation on every store read), a screened `javascript:`/`data:` URL **threw** instead of the documented warn-and-drop, and the always-on list-key warnings printed to production consoles. Server/Node builds, where `process` exists, were unaffected.
249
2766
 
250
2767
  Removing the inference also shrinks production bundles by **~4.7 KB min+gzip (27%)** — a realistic import (`signal`/`computed`/`effect`/`batch`/`mount`/`each`/`delegate`) goes from 16.91 KB to **12.24 KB**. Previously the dev-warning modules were imported unconditionally by `mount()`/`each()` and gated at runtime, so they shipped to production regardless of build mode and no amount of tree-shaking could reclaim them. With the dev entry absent they are unreachable, and the `import()` statement itself is eliminated — the chunk is never emitted, let alone fetched.
251
2768
 
252
2769
  `globalThis.KERF_DEV` is no longer consulted — not importing the dev entry is now the (compile-time) way to opt out. One ordering note: `signal()` picks its constructor at creation time, so put the import first if you rely on the untracked-signal warning.
253
2770
 
254
- - The `KERF_DEV_WARN_*` diagnostics no longer consult `NODE_ENV` or `globalThis.KERF_DEV` at all. Whether they run is decided in exactly one place: whether you imported `kerfjs/dev`. The previous release stopped kerf's core from inferring dev mode but left a second, inherited gate inside each warner, so a Node/SSR consumer who *deliberately* installed the diagnostics under `NODE_ENV=production` got silence — and `globalThis.KERF_DEV = false` still silenced warnings the consumer had explicitly opted into. Both are gone; each warning is now gated only by its own env var. If you were using `globalThis.KERF_DEV` to turn diagnostics off, remove the dev import instead (which is also what sheds the ~4.7 KB from your bundle).
255
-
2771
+ - The `KERF_DEV_WARN_*` diagnostics no longer consult `NODE_ENV` or `globalThis.KERF_DEV` at all. Whether they run is decided in exactly one place: whether you imported `kerfjs/dev`. The previous release stopped kerf's core from inferring dev mode but left a second, inherited gate inside each warner, so a Node/SSR consumer who _deliberately_ installed the diagnostics under `NODE_ENV=production` got silence — and `globalThis.KERF_DEV = false` still silenced warnings the consumer had explicitly opted into. Both are gone; each warning is now gated only by its own env var. If you were using `globalThis.KERF_DEV` to turn diagnostics off, remove the dev import instead (which is also what sheds the ~4.7 KB from your bundle).
256
2772
 
257
- - **New:** `each()` accepts an options object — `each(items, render, { cacheKey, key })` — and `key` gives a list a **stable identity**. Without one a list is identified by its position among the `each()` calls in a render, so adding or removing a conditional list above it made kerf rebuild it from scratch: rows lost their DOM nodes, and with them focus, scroll position and in-progress IME composition. Keying a list removes that dependency; because a keyed list doesn't occupy a positional slot, keying just the *conditional* list usually stabilizes its siblings too. The existing three-argument `each(items, render, cacheKey)` form is unchanged. In development kerf now warns once per list when it detects such a shift and names the fix.
2773
+ - **New:** `each()` accepts an options object — `each(items, render, { cacheKey, key })` — and `key` gives a list a **stable identity**. Without one a list is identified by its position among the `each()` calls in a render, so adding or removing a conditional list above it made kerf rebuild it from scratch: rows lost their DOM nodes, and with them focus, scroll position and in-progress IME composition. Keying a list removes that dependency; because a keyed list doesn't occupy a positional slot, keying just the _conditional_ list usually stabilizes its siblings too. The existing three-argument `each(items, render, cacheKey)` form is unchanged. In development kerf now warns once per list when it detects such a shift and names the fix.
258
2774
 
259
2775
  - **`kerfjs/dev` now exports `enableWarnings()`** — and it fixes a defect: until now, none of the
260
2776
  `KERF_DEV_WARN_*` diagnostics could be switched on in a browser at all. Every one of them read `globalThis.process.env`, which does not exist in a browser realm — and a bundler `define` cannot reach it either, because the read goes through `globalThis.process` into a local binding rather than the substitutable `process.env.X` token. So in a Vite/webpack dev server, the environment where these warnings are most wanted, the entire opt-in family was permanently and silently off. Only Node/SSR (and kerf's own vitest suite, which is why nothing caught it) could turn any of them on.
@@ -263,8 +2779,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
263
2779
 
264
2780
  ```js
265
2781
  if (import.meta.env.DEV) {
266
- const dev = await import('kerfjs/dev');
267
- dev.enableWarnings({ staleBinding: true, narrowSet: true, invariants: 'throw' });
2782
+ const dev = await import("kerfjs/dev");
2783
+ dev.enableWarnings({
2784
+ staleBinding: true,
2785
+ narrowSet: true,
2786
+ invariants: "throw",
2787
+ });
268
2788
  }
269
2789
  ```
270
2790
 
@@ -278,8 +2798,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
278
2798
 
279
2799
  - New opt-in dev warning `KERF_DEV_WARN_LIST_REBIND=1`: fires (once per list) when an `each()` list's container is rebuilt by the morph — an ancestor's tag changed across renders, so the subtree was replaced and the list self-healed by re-binding and repopulating. The recovery is correct but discards row DOM state (focus, scroll, IME, imperative listeners); the warning names the list and points at keeping ancestor tags stable. Follows the standard `KERF_DEV_WARN_*` family rules: off by default, dev-mode only, zero production cost.
280
2800
 
281
-
282
- - `KERF_DEV_WARN_UNTRACKED_SIGNALS=1` now tells you what it can and cannot see. The warning picks its machinery when a signal is *created*, so it only covers signals created after `kerfjs/dev` is installed — and because static imports are hoisted above a top-level `await import('kerfjs/dev')`, the module-scope signals it most wants to catch are usually created first. Previously that failed silently: you set the env var, saw nothing, and concluded your code was clean. Opting in now prints the coverage boundary once, along with the fix (make `import 'kerfjs/dev'` the first static import of a dev-only entry file). The boundary itself can't be removed — `Signal.prototype`'s `value` accessor is non-configurable, so already-created signals can't be retro-fitted without kerf keeping a registry of every signal, which production would pay for.
2801
+ - `KERF_DEV_WARN_UNTRACKED_SIGNALS=1` now tells you what it can and cannot see. The warning picks its machinery when a signal is _created_, so it only covers signals created after `kerfjs/dev` is installed — and because static imports are hoisted above a top-level `await import('kerfjs/dev')`, the module-scope signals it most wants to catch are usually created first. Previously that failed silently: you set the env var, saw nothing, and concluded your code was clean. Opting in now prints the coverage boundary once, along with the fix (make `import 'kerfjs/dev'` the first static import of a dev-only entry file). The boundary itself can't be removed — `Signal.prototype`'s `value` accessor is non-configurable, so already-created signals can't be retro-fitted without kerf keeping a registry of every signal, which production would pay for.
283
2802
 
284
2803
  - An `each()` of `<tr>` written directly inside `<table>` now fails with a clear error instead of silently duplicating rows: the HTML parser inserts a `<tbody>` around the rows, which kerf cannot bind through. The message names both tags and shows the supported shape (`<table><tbody>{each(...)}</tbody></table>`). Previously this also mis-reported the rows as missing `data-key`.
285
2804
 
@@ -287,8 +2806,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
287
2806
 
288
2807
  - Toolchain: the repo now type-checks with the native **TypeScript 7** compiler across every gate (`typecheck`, the dist `.d.ts` typing gates, the docs code-block compile — a full-repo `tsc --noEmit` now takes ~0.3 s), with `typescript@6` (the JS-API bridge release) retained for tsup's `.d.ts` emit and typescript-eslint, which still require the JS compiler API. `@typescript-eslint/*` bumped to 8.65. No shipped-code changes — `dist/` output is unaffected.
289
2808
 
290
-
291
- - Fixed: passing a **function** as an `each()` item through an `arraySignal` insert/update (an unusual mistake, but functions are valid `WeakMap` keys so the per-item cache silently accepted them) rendered the row and then threw `each(): items must be objects…` on a *later, unrelated* re-render — far from the cause. The granular path now enforces the same objects-only item contract the snapshot path does, so the error is thrown on the render the offending mutation triggered, naming the type and index. Primitive items already behaved this way; functions now match.
2809
+ - Fixed: passing a **function** as an `each()` item through an `arraySignal` insert/update (an unusual mistake, but functions are valid `WeakMap` keys so the per-item cache silently accepted them) rendered the row and then threw `each(): items must be objects…` on a _later, unrelated_ re-render — far from the cause. The granular path now enforces the same objects-only item contract the snapshot path does, so the error is thrown on the render the offending mutation triggered, naming the type and index. Primitive items already behaved this way; functions now match.
292
2810
 
293
2811
  - `arraySignal.update(i, fn)` now works when `fn` mutates the row object and returns it (not only when it returns a fresh object). Previously such a same-ref update rendered correctly in one list but left every other view of the same signal — a second list, a second `mount()`, a `filter()`ed plain-array view — permanently stale, and was lost outright when the update was batched with a selection change or a `replace()`. kerf now tracks a per-item content version so the change reaches every consumer. Returning a fresh object remains the idiomatic style; both are supported. (Fixed in the same line of work: the per-item version tracking briefly made `arraySignal.update()` throw on a signal of primitives — `arraySignal<number>` used as a plain signal — because a primitive can't key the internal version map; primitive items are now simply skipped, since only object rows are ever memoized.)
294
2812
 
@@ -300,13 +2818,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
300
2818
 
301
2819
  - Fixed: an `arraySignal.update()` that mutated a row object in place (returning the same reference) could be silently reverted to its old content by the next unrelated re-render. The granular update path now keeps kerf's per-row HTML cache in sync with what it rendered, so the two never disagree. (Immutable updates — returning a fresh object — were unaffected, and remain the recommendation.)
302
2820
 
303
- - Fixed: showing an empty conditionally-rendered `each()` list in the same batch as an update to a *sibling* list could empty the sibling entirely — it rendered zero rows. When the conditional list reappeared, its list-marker comment landed next to the sibling's, and the diff overwrote the sibling marker's internal id with the reappearing one's, so the sibling's binding could no longer find its own marker. Marker comments (kerf's internal list and binding anchors) now pair only with the identical marker, never with a different one that happens to be the same kind. Ordinary comments in your markup are unaffected.
2821
+ - Fixed: showing an empty conditionally-rendered `each()` list in the same batch as an update to a _sibling_ list could empty the sibling entirely — it rendered zero rows. When the conditional list reappeared, its list-marker comment landed next to the sibling's, and the diff overwrote the sibling marker's internal id with the reappearing one's, so the sibling's binding could no longer find its own marker. Marker comments (kerf's internal list and binding anchors) now pair only with the identical marker, never with a different one that happens to be the same kind. Ordinary comments in your markup are unaffected.
304
2822
 
305
2823
  - Fixed: when an element rendered by a condition came back, the diff could pair it with an unrelated sibling that happened to have the same tag, and then whatever protects that sibling's contents kept the wrong contents alive inside it. A `data-morph-skip` widget **swallowed** the reappearing element — its content never rendered — and the widget was duplicated, which for a library-owned subtree (an editor, a chart, a terminal) means a second live instance attached to a node the library has no reference to. A `data-morph-preserve` child ended up under the foreign host and appeared twice. A bound hole's text leaked into the reappearing element and rendered twice. Three rules now gate that pairing: an element with an `id`/`data-key` is only ever matched to a live element with the same key; a `data-morph-skip` element only ever matches another one (so a library-owned subtree is never adopted as a stand-in, whether or not you use keys); and a comment anchoring kerf's own state only matches an anchor of the same kind. Ordinary elements without a key still match positionally exactly as before.
306
2824
 
307
- - Fixed: when the number of `each()` calls in a render changed — a conditional list appearing or disappearing — a *surviving* list could render the departed list's rows, or render its own rows inside the wrong container. Lists without a `key` are identified by their position among the `each()` calls, and both the per-item HTML cache and the live list binding were being read as belonging to whichever list now held that position. Two lists over the same collection hit each other's cache exactly, so nothing could detect it from the data. A shift now discards that state and re-renders, which is what the documentation already described it as costing: a rebuild, never wrong output. Lists given a `key` are unaffected, and a render that doesn't change the call count is unaffected. One related improvement falls out: an unrelated list no longer loses its row nodes when a nested `each()` shifts the count.
2825
+ - Fixed: when the number of `each()` calls in a render changed — a conditional list appearing or disappearing — a _surviving_ list could render the departed list's rows, or render its own rows inside the wrong container. Lists without a `key` are identified by their position among the `each()` calls, and both the per-item HTML cache and the live list binding were being read as belonging to whichever list now held that position. Two lists over the same collection hit each other's cache exactly, so nothing could detect it from the data. A shift now discards that state and re-renders, which is what the documentation already described it as costing: a rebuild, never wrong output. Lists given a `key` are unaffected, and a render that doesn't change the call count is unaffected. One related improvement falls out: an unrelated list no longer loses its row nodes when a nested `each()` shifts the count.
308
2826
 
309
- - Fixed: a row added to an `each()` list could land in the wrong place whenever the list wasn't the last thing inside its parent. A list ends at its last row, but kerf was looking for the next *element* after it and skipping everything else on the way — so static content following the list (a footer row, a totals line, an "add item" control) got jumped, and a new row appeared after it instead of before. The same skip crossed a neighboring list's internal anchor: with two `each()` lists in one parent, rows from the first could be placed inside the second's region, and when both lists started empty their rows came out **in the wrong order** — the second list's rows rendered first. Both were correct on the initial paint and only went wrong on a later update, which made them read as intermittent. Lists now anchor on the next node of any kind, so a list's rows always stay within its own region.
2827
+ - Fixed: a row added to an `each()` list could land in the wrong place whenever the list wasn't the last thing inside its parent. A list ends at its last row, but kerf was looking for the next _element_ after it and skipping everything else on the way — so static content following the list (a footer row, a totals line, an "add item" control) got jumped, and a new row appeared after it instead of before. The same skip crossed a neighboring list's internal anchor: with two `each()` lists in one parent, rows from the first could be placed inside the second's region, and when both lists started empty their rows came out **in the wrong order** — the second list's rows rendered first. Both were correct on the initial paint and only went wrong on a later update, which made them read as intermittent. Lists now anchor on the next node of any kind, so a list's rows always stay within its own region.
310
2828
 
311
2829
  - Fixed: an `each()` list inside an `<svg>` whose row markup contained an apostrophe (or anything else the serializer writes back differently) failed to mount at all, with a self-contradictory error. Also fixed: `each({ key })` now validates the key, so a key containing an HTML comment terminator can no longer break out of the list's internal marker and put markup in the page.
312
2830
 
@@ -318,7 +2836,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
318
2836
 
319
2837
  - Fixed: a controlled `checked` / `value` on an `each()` row's own top-level element could stay visibly stale after the user had interacted with it — the row reconciler's attribute-only fast path wrote the attribute without syncing the live property, so whether the control obeyed your data depended on which internal route the update happened to take.
320
2838
 
321
- - Fixed: when a conditionally-rendered `each()` list is added or removed, a sibling list could render the *other* list's rows — a batched "hide one list and push to another" applied the queued update to the wrong list's DOM. Lists now verify which data a pending update belongs to before applying it, and rebuild from their own items when it doesn't match.
2839
+ - Fixed: when a conditionally-rendered `each()` list is added or removed, a sibling list could render the _other_ list's rows — a batched "hide one list and push to another" applied the queued update to the wrong list's DOM. Lists now verify which data a pending update belongs to before applying it, and rebuild from their own items when it doesn't match.
322
2840
 
323
2841
  - Fixed: `each()` rows inside an `<svg>` root were re-parsed in the HTML namespace on every update, so rows added or structurally changed after the first render were invisible in the browser — the initial picture looked right, which made it read as a rendering flake. Row parsing now follows the list parent's namespace (rows under `<foreignObject>` correctly stay HTML).
324
2842
 
@@ -326,92 +2844,72 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
326
2844
 
327
2845
  - Fixed: anything sitting between a keyed `each()` list's anchor and its rows — a node injected imperatively into the list region — could make a conditional sibling's removal reorder the list, landing a trailing sibling ahead of the rows. A list's row region (anchor through last row) is now treated as one unit by the diff: it moves whole, the diff's cursor steps over it whole, and injected nodes inside it travel along keeping their position relative to the rows.
328
2846
 
329
- - A conditional sibling *inside* a keyed `each()` list's parent — a header row that comes and goes above the list — no longer costs the list's rows their DOM identity. The morph now recognizes the list's marker when a sibling shifts it, moving the marker and its rows up as a single unit instead of rebuilding the list, so row nodes, focus, and the caret survive the toggle. (An ancestor tag change, or a same-tag sibling that positionally takes the container's place, still rebuilds the list — give the list's own container a stable `id`/`data-key` if its rows need to survive that.)
2847
+ - A conditional sibling _inside_ a keyed `each()` list's parent — a header row that comes and goes above the list — no longer costs the list's rows their DOM identity. The morph now recognizes the list's marker when a sibling shifts it, moving the marker and its rows up as a single unit instead of rebuilding the list, so row nodes, focus, and the caret survive the toggle. (An ancestor tag change, or a same-tag sibling that positionally takes the container's place, still rebuilds the list — give the list's own container a stable `id`/`data-key` if its rows need to survive that.)
330
2848
 
331
2849
  - Fixed: a conditional sibling that shared or positionally shadowed a keyed `each()` list's container could strand the list's rows and then render a duplicate copy of them (e.g. a header `<li>` toggled inside the list's `<ul>`, or a same-tag banner `<ul>` before the list container). The morph can separate the list's marker comment from its still-attached rows; the self-heal now removes any such still-live stranded rows before repopulating, so recovery replaces the rows rather than duplicating them.
332
2850
 
333
2851
  ## [2.0.1] - 2026-07-23
334
2852
 
335
-
336
-
337
2853
  - Fixed a `morph()` bug where static text siblings of a fine-grained bound text hole were dropped after a structural re-render (e.g. `<div>{label} / static</div>` collapsing to just the label).
338
2854
 
339
-
340
2855
  - New animated coding-session demo on the getting-started page: watch the canonical counter get typed out, served, clicked in a browser, and live-edited into a todo list.
341
2856
  - Added an animated architecture diagram and a link to the docs site from the README.
342
2857
 
343
2858
  ## [2.0.0] - 2026-07-23
344
2859
 
345
-
346
-
347
2860
  - `delegateCapture()` now uses `closest()`-style walk-up matching like `delegate()`, passing the matched ancestor to the handler; pass `{ match: 'direct' }` (new `DelegateOptions`) to restore exact-element matching.
348
2861
  - The dangerous-URL screen (`javascript:`, `vbscript:`, script-executing `data:` URLs) now throws an error in development instead of only warning. Production behavior is unchanged: warn and drop the attribute.
349
2862
 
350
-
351
2863
  - New `kerfjs/html` tagged template: author kerf UIs with no build step (CDN / importmap, no JSX transform) with runtime semantics identical to JSX — signal holes become fine-grained bindings, attributes and text are escaped and URL-screened the same way.
352
2864
  - New no-build example app, **live-poll**, served exactly as authored — an importmap plus one `html`-templated module, with view-source showing the app.
353
2865
  - Added `<filter>` to the typed JSX intrinsic elements, so SVG filters compile in JSX-authored code.
354
2866
  - Fully-bound mounts are now a documented, test-pinned guarantee: a render that reads no signal `.value` runs exactly once, forever — every update is a direct per-node write.
355
2867
 
356
-
357
2868
  - Controlled form state now survives user interaction: `checked`, `value`, and `selected` DOM properties are synced when the reconciler mutates those attributes, so a clicked checkbox or typed-into input no longer ignores later updates.
358
2869
  - Fixed stale fine-grained bindings after `arraySignal.update()`: in-place row updates now re-wire bindings whose signal instance changed, instead of leaving effects reading the old row object forever.
359
2870
 
360
-
361
2871
  - New runtime dev-mode override: set `globalThis.KERF_DEV = false` (or `true`) to control dev mode without a bundler — CDN/importmap apps are no longer stuck in dev mode in production.
362
2872
  - Two new opt-in dev warnings: `KERF_DEV_WARN_STALE_BINDING` flags bindings that silently go stale on the byte-equal fast path, and `KERF_DEV_WARN_VALUE_ONLY_RERENDER` flags re-renders whose only changes could have been fine-grained bindings.
363
2873
  - `defineStore`'s dev-mode `get()` snapshot now returns a deep read-only proxy instead of freezing the live state: nested mutations like `get().nested.x = 1` are caught too, and they throw a descriptive `TypeError` rather than failing silently.
364
2874
 
365
-
366
2875
  - Docs repositioned around the "values bind, structure re-renders" idiom as the primary way to render dynamic values, across the overview, reactivity guide, and AI assistant configs.
367
2876
  - New guide covering the no-build authoring path and example app, plus a documentation-wide accuracy pass (delegate capture semantics, `effect()` cleanup returns, URL-screen behavior, and more).
368
2877
 
369
2878
  ## [1.0.2] - 2026-07-22
370
2879
 
371
-
372
-
373
2880
  - Fixed the row-selector demo animation 404ing on the published site — the missing SVG capture is now generated and committed.
374
2881
 
375
2882
  ## [1.0.1] - 2026-07-22
376
2883
 
377
-
378
-
379
2884
  - KF-329: pin release-workflow npm upgrade to npm@11 — npm@12 needs Node ≥ 22.22.2, above the 22.21.0 pin (`c54559f`)
380
2885
 
381
2886
  ## [1.0.0] - 2026-07-22
382
2887
 
383
-
384
-
385
2888
  - **Fine-grained signal bindings** — pass a signal or `computed` directly into a JSX attribute or text hole (e.g. `class={computed(...)}` or `{sig}`) inside `mount()`, and that one node updates on signal change without re-running `render()` or walking the list reconciler. Opt-in per hole and non-breaking; works in static content and inside `each()` rows on both the snapshot and `arraySignal` reconcile paths, with binding effects correctly wired, carried, and disposed across row insert/update/remove/move.
386
2889
  - Bound values get the same safety treatment as static ones: URL screening and `SafeHtml`/`raw()` unwrapping now apply to fine-grained bound attributes.
387
2890
  - New interactive benchmark playground (`npm run bench:serve`) to explore kerf by hand in the standard 1k-rows benchmark app.
388
2891
  - New **row-selector** example app and a "Fine-grained bindings" section in the reactivity demo, both showing select-row updates with zero render re-runs and zero list reconciles.
389
2892
 
390
-
391
2893
  - Hardened the dangerous-URL screen: scheme detection now normalizes away C0 control characters, DEL, and leading whitespace, so obfuscations like `java script:` or a NUL before the colon can no longer slip `javascript:`/`vbscript:` URLs past the check.
392
2894
  - Attribute names are now validated — malformed names (e.g. from spreading attacker-controlled keys into JSX) throw instead of breaking out of the open tag, and inline `on*` event handlers are rejected on both the static and signal-bound attribute paths, closing an XSS vector where a bound `onclick` would have installed a live handler.
393
2895
  - Documented kerf's reserved marker namespace (`data-kfb`, `data-kfbrow`, `kfb:`/`kfbr:`/`kf-list:` comments) — consumer content using these can collide with binding wiring, so they're now explicitly reserved.
394
2896
  - Documented the trusted-input bridges (`toElement()`, `morph()` with string/Element templates, `<iframe srcdoc>`) that bypass escaping by design, and the `raw()`/`SafeHtml` trust boundary.
395
2897
  - Hardened the release pipeline: least-privilege OIDC scoping per job, SHA-pinned GitHub Actions with automated Dependabot bumps, and a token-holding publish job that runs with `--ignore-scripts` and no build tools.
396
2898
 
397
-
398
2899
  - Reduced the per-row create cost of fine-grained row bindings from ~1.65× to ~1.15× via lazy row wiring — the common single-root-binding case now needs no subtree walk or extra allocations.
399
2900
  - Published cross-framework benchmark numbers now come from the official upstream krausest js-framework-benchmark run (kerf is a merged upstream entry), replacing stale local-machine measurements.
400
2901
 
401
-
402
2902
  - README refreshed for the 1.0 release: fine-grained updates and safe-by-default escaping promoted to headline features, and the status line flipped from "Pre-1.0 — API may evolve" to stable 1.0.
403
2903
  - Fine-grained bindings documented across all consumer and AI-assistant surfaces (reactivity docs, API reference, usage guide, Cursor rules, Claude skill).
404
2904
 
405
- - **Signposted the raw HTML/SVG → DOM bridges as trusted-input only.** `toElement()`, `morph()` (with a string/Element template), and the `<iframe srcdoc>` attribute bypass kerf's escaping/URL-screening by design — they're the same trust model as `innerHTML` / `raw()`. The docs now call this out loudly, including that the SVG path is *more* dangerous than the HTML path (a top-level `<svg><script>`, SVG event attributes, and `xlink:href="javascript:"` execute once inserted, whereas an HTML-string `<script>` is inert), and that `srcdoc` is HTML a browser re-parses as a document (so `srcdoc={userString}` executes even though the value is escaped as an attribute). No behavior change — these are documentation + regression tests: a real-browser spec (`tests/browser/trusted-html-bridges.spec.ts`) pins the execution boundary across Chromium/Firefox/WebKit, a unit test pins that the granular list fast path keeps the URL screen's guarantee end-to-end, and another pins that SVG input isn't sanitized. See [`docs/7-svg.md`](docs/7-svg.md) § Security, [`docs/8-api-reference.md`](docs/8-api-reference.md), and [`docs/6-jsx-runtime.md`](docs/6-jsx-runtime.md) §6.4.3.
406
- - **Documented kerf's reserved marker namespace.** Fine-grained bindings and `each()` lists coordinate through in-band markers that the wiring pass finds by scanning the mounted subtree and matching by id. A consumer element that carries one of those names can collide with a real binding's id and silently steal its update, so the names are now documented as reserved: the `data-kfb` / `data-kfbrow` attributes and HTML comments beginning `kfb:` / `kfbr:` / `kf-list:`. Don't emit them from your own markup or via `raw()`. (kerf's escaping already prevents a plain text/attribute *value* from forging one — the only ways in are hand-written markup or `raw()`.) See [`docs/2-reactivity.md`](docs/2-reactivity.md) § "Reserved marker names".
407
- - **Attribute names are now validated, and inline event handlers are rejected outright.** The JSX runtime already escaped attribute *values*; it now also validates each attribute *name* against a safe shape (a letter/underscore/colon followed by letters, digits, or `_ . : -`) and **throws** on anything else. This closes a markup-injection vector when an object with attacker-controlled keys is spread into JSX (`<div {...untrustedObj}>`) — previously a key like `'x><img onerror=…>'` broke out of the open tag even though the value was escaped. Separately, any `on*` attribute (a function *or* a string value, in any case — e.g. `onClick={fn}` or `onclick="…"`) now throws and points at `delegate()`; previously only function-valued keys matching `/^on[A-Z]/` were caught, so a string `onclick="alert(1)"` slipped through and became a live inline handler when parsed. Both checks now cover the **fine-grained bound path** too: a signal bound straight into an attribute (`onclick={signal}`) is written with `setAttribute`, and `setAttribute('onclick', …)` installs a live inline handler just as a parsed string would — so an `on*` (or malformed) name bound as a signal is rejected at binding time, closing the same vector on the signal path. See [`docs/6-jsx-runtime.md`](docs/6-jsx-runtime.md) §6.4.2.
2905
+ - **Signposted the raw HTML/SVG → DOM bridges as trusted-input only.** `toElement()`, `morph()` (with a string/Element template), and the `<iframe srcdoc>` attribute bypass kerf's escaping/URL-screening by design — they're the same trust model as `innerHTML` / `raw()`. The docs now call this out loudly, including that the SVG path is _more_ dangerous than the HTML path (a top-level `<svg><script>`, SVG event attributes, and `xlink:href="javascript:"` execute once inserted, whereas an HTML-string `<script>` is inert), and that `srcdoc` is HTML a browser re-parses as a document (so `srcdoc={userString}` executes even though the value is escaped as an attribute). No behavior change — these are documentation + regression tests: a real-browser spec (`tests/browser/trusted-html-bridges.spec.ts`) pins the execution boundary across Chromium/Firefox/WebKit, a unit test pins that the granular list fast path keeps the URL screen's guarantee end-to-end, and another pins that SVG input isn't sanitized. See [`docs/7-svg.md`](docs/7-svg.md) § Security, [`docs/8-api-reference.md`](docs/8-api-reference.md), and [`docs/6-jsx-runtime.md`](docs/6-jsx-runtime.md) §6.4.3.
2906
+ - **Documented kerf's reserved marker namespace.** Fine-grained bindings and `each()` lists coordinate through in-band markers that the wiring pass finds by scanning the mounted subtree and matching by id. A consumer element that carries one of those names can collide with a real binding's id and silently steal its update, so the names are now documented as reserved: the `data-kfb` / `data-kfbrow` attributes and HTML comments beginning `kfb:` / `kfbr:` / `kf-list:`. Don't emit them from your own markup or via `raw()`. (kerf's escaping already prevents a plain text/attribute _value_ from forging one — the only ways in are hand-written markup or `raw()`.) See [`docs/2-reactivity.md`](docs/2-reactivity.md) § "Reserved marker names".
2907
+ - **Attribute names are now validated, and inline event handlers are rejected outright.** The JSX runtime already escaped attribute _values_; it now also validates each attribute _name_ against a safe shape (a letter/underscore/colon followed by letters, digits, or `_ . : -`) and **throws** on anything else. This closes a markup-injection vector when an object with attacker-controlled keys is spread into JSX (`<div {...untrustedObj}>`) — previously a key like `'x><img onerror=…>'` broke out of the open tag even though the value was escaped. Separately, any `on*` attribute (a function _or_ a string value, in any case — e.g. `onClick={fn}` or `onclick="…"`) now throws and points at `delegate()`; previously only function-valued keys matching `/^on[A-Z]/` were caught, so a string `onclick="alert(1)"` slipped through and became a live inline handler when parsed. Both checks now cover the **fine-grained bound path** too: a signal bound straight into an attribute (`onclick={signal}`) is written with `setAttribute`, and `setAttribute('onclick', …)` installs a live inline handler just as a parsed string would — so an `on*` (or malformed) name bound as a signal is rejected at binding time, closing the same vector on the signal path. See [`docs/6-jsx-runtime.md`](docs/6-jsx-runtime.md) §6.4.2.
408
2908
  - **Hardened the dangerous-URL screen.** The URL-attribute filter (on `href`/`src`/`xlink:href`/`formaction`/`action`) now: (1) sees through control-character and whitespace obfuscation of the scheme — a leading ``, an in-scheme `TAB`/`LF`/`CR` (`java script:`), or a `NUL` before the colon are all normalized away before the scheme is read, matching how a browser resolves the URL, so they can no longer slip a `javascript:` past the screen; (2) treats `data:` by subtype instead of only blocking `data:text/html` — script-executing document types (`data:text/html`, `data:image/svg+xml`, XHTML/XML) are dropped while inert media (raster images, fonts, audio, video, plain text/CSS) still pass, and any unknown subtype fails closed; and (3) also screens the `data` attribute on `<object>` (which loads its target as a document). `raw()` remains the opt-out. Both the static serializer and the fine-grained bound-attribute writer share the screen, so both paths are covered.
409
- - **Fine-grained signal bindings.** Hand a `Signal`/`computed` *itself* (not its `.value`) into a JSX attribute (`class={someSignal}`) or a text hole (`{someSignal}`) inside a `mount()`, and kerf binds that hole directly to the signal: when the signal changes, only that attribute/text node updates — the render function does **not** re-run and the list reconciler does **not** walk. This is kerf's fine-grained update tier, sitting below the coarse `mount()` effect, for the "external state drives one spot" pattern (a `selectedId` flipping a row's class, a live status attribute, etc.). Works in static content and inside `each()` rows on both the snapshot and `arraySignal` (granular) paths, and row bindings' lifetimes track their row node (a row reorder is free; a removed row's binding is torn down). Opt-in and non-breaking: passing a raw signal into JSX previously threw, so existing apps are unchanged, and any hole that isn't a signal stringifies exactly as before. Bound URL attributes (`href`/`src`/`formaction`/`action`/`xlink:href`) get the same `javascript:`/`vbscript:`/`data:text/html` screening as static attributes (`raw()` opts out). Outside a `mount()` (SSR / `SafeHtml.toString()`) a bound signal snapshots its current value and emits no markers. See [`docs/2-reactivity.md`](docs/2-reactivity.md) §2.9.
2909
+ - **Fine-grained signal bindings.** Hand a `Signal`/`computed` _itself_ (not its `.value`) into a JSX attribute (`class={someSignal}`) or a text hole (`{someSignal}`) inside a `mount()`, and kerf binds that hole directly to the signal: when the signal changes, only that attribute/text node updates — the render function does **not** re-run and the list reconciler does **not** walk. This is kerf's fine-grained update tier, sitting below the coarse `mount()` effect, for the "external state drives one spot" pattern (a `selectedId` flipping a row's class, a live status attribute, etc.). Works in static content and inside `each()` rows on both the snapshot and `arraySignal` (granular) paths, and row bindings' lifetimes track their row node (a row reorder is free; a removed row's binding is torn down). Opt-in and non-breaking: passing a raw signal into JSX previously threw, so existing apps are unchanged, and any hole that isn't a signal stringifies exactly as before. Bound URL attributes (`href`/`src`/`formaction`/`action`/`xlink:href`) get the same `javascript:`/`vbscript:`/`data:text/html` screening as static attributes (`raw()` opts out). Outside a `mount()` (SSR / `SafeHtml.toString()`) a bound signal snapshots its current value and emits no markers. See [`docs/2-reactivity.md`](docs/2-reactivity.md) §2.9.
410
2910
 
411
2911
  ## [0.16.0] - 2026-07-01
412
2912
 
413
-
414
-
415
2913
  - Fixed keyed-list selection breaking after a row was removed: a signal read only inside `each()`'s `cacheKey` (such as a `selectedId` toggling a row's class) no longer drops out of the reactive dependency set, so later changes re-render correctly.
416
2914
  - Fixed appending items to a list after clearing it rendering nothing.
417
2915
 
@@ -420,115 +2918,90 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
420
2918
 
421
2919
  ## [0.15.5] - 2026-06-30
422
2920
 
423
-
424
-
425
2921
  - Corrected the advertised bundle size in `llms.txt` to ~11 KB minified + gzipped (including the `@preact/signals-core` runtime dependency; ~12 KB with `arraySignal`), matching the README.
426
2922
 
427
2923
  ## [0.15.4] - 2026-06-30
428
2924
 
429
-
430
-
431
2925
  - `llms.txt` is now published at a public docs-site URL and bundled in the npm package, making kerf's AI-assistant documentation index discoverable to llms.txt directories and tooling.
432
2926
 
433
2927
  - The `kerfjs` package now bundles `llms.txt` (the AI-discovery index) at the package root, and the docs site serves it at `https://brianwestphal.github.io/kerf/llms.txt`. Its links are now absolute GitHub URLs so the file is portable across GitHub, the site, and the installed package.
434
2928
 
435
2929
  ## [0.15.3] - 2026-06-30
436
2930
 
437
-
438
-
439
2931
  - `npm create kerf-component` with no directory argument now prompts for the target directory (defaulting to `my-kerf-component`, with a `.` hint for the current directory) instead of printing usage text and exiting with an error.
440
2932
 
441
2933
  ## [0.15.2] - 2026-06-30
442
2934
 
443
-
444
-
445
2935
  - Added the `create-kerf-component` initializer — scaffold a ready-to-publish kerf component package with `npm create kerf-component@latest <dir>`, no need to reverse-engineer the packaging rules.
446
2936
 
447
2937
  ## [0.15.1] - 2026-06-30
448
2938
 
449
-
450
-
451
2939
  - README refresh + eslint rule-count fixes + CHANGELOG hygiene (`cfbaef3`)
452
2940
 
453
2941
  ## [0.15.0] - 2026-06-30
454
2942
 
455
-
456
-
457
2943
  - List updates now morph same-identity rows in place instead of recreating their DOM nodes, avoiding full-table relayout on large lists and preserving DOM identity, focus, and IME composition across re-renders.
458
2944
 
459
-
460
2945
  - New guide on incremental migration: kerf can own a single DOM subtree and coexist with React (or any framework), letting you migrate one island at a time.
461
2946
  - New guide on building and publishing reusable kerf components as npm packages.
462
2947
  - Example app documentation pages now open with an animated SVG preview of the real app in action, plus a gallery on the complete-examples index.
463
2948
 
464
2949
  ## [0.14.0] - 2026-05-27
465
2950
 
466
-
467
2951
  - Fix `toElement()` first-paint divergence in WebKit by adopting its result into the live document
468
2952
  - Fix `mount()` first render in WebKit by adopting an inert `rootEl` into the live document
469
2953
 
470
2954
  ## [0.13.0] - 2026-05-23
471
2955
 
472
-
473
2956
  - Add `KERF_DEV_WARN_DELEGATE_IN_EFFECT` dev warning to catch `delegate()` calls inside reactive effects
474
2957
  - New `require-delegate-disposer` ESLint rule flags `delegate()` calls whose disposer is discarded
475
2958
  - Document `delegate()` disposer gotchas and canonical cleanup patterns
476
2959
 
477
2960
  ## [0.12.1] - 2026-05-22
478
2961
 
479
-
480
2962
  - Add GitHub Sponsors link to README, homepage, and npm `funding` field
481
2963
 
482
2964
  ## [0.12.0] - 2026-05-22
483
2965
 
484
-
485
2966
  - `toElement` now returns `Element | DocumentFragment` to support multi-root inputs
486
2967
 
487
2968
  ## [0.11.1] - 2026-05-21
488
2969
 
489
-
490
2970
  - `attr()` redesign: typed `AttrSpec<N, V>` exposes `.attrs` with dual overloads for cleaner attribute handling
491
2971
 
492
2972
  ## [0.11.0] - 2026-05-21
493
2973
 
494
-
495
2974
  - `attr()` redesigned with `AttrSpec<N,V>` shape, `.attrs` accessor, and dual overloads
496
2975
  - Hardened defensive programming across the runtime for safer edge-case handling
497
2976
  - Refreshed published performance numbers from a fresh cross-framework benchmark run
498
2977
 
499
2978
  ## [0.10.0] - 2026-05-20
500
2979
 
501
-
502
2980
  - Expose `kerfjs/ai/*` subpaths via package `exports` so the bundled skill/cursorrules files are resolvable
503
2981
  - Add a defensive fallback in `kerfjs/ai-assistant-configs` ESLint rule so it fires against installed kerfjs versions whose `exports` block subpath resolution
504
2982
 
505
2983
  ## [0.9.1] - 2026-05-20
506
2984
 
507
-
508
2985
  - Bundle the kerf-app Claude Code skill and Cursor rules inside the npm package at `ai/skill.md`, `ai/cursorrules`, and `ai/manifest.json`
509
2986
  - Add `kerfjs/ai-assistant-configs` rule to `eslint-plugin-kerfjs` (warn in recommended) to flag drift in installed AI assistant configs
510
2987
  - `eslint --fix` now replaces only the canonical section above the `KERF-APP-CANONICAL-END` marker, preserving consumer customizations below it
511
2988
 
512
2989
  ## [0.9.0] - 2026-05-20
513
2990
 
514
-
515
2991
  - Bundle the kerf-app Claude Code skill and Cursor rules inside the npm package at `ai/skill.md`, `ai/cursorrules`, and `ai/manifest.json`
516
2992
  - Add `kerfjs/ai-assistant-configs` rule (warn in recommended) to `eslint-plugin-kerfjs` v0.9.0 to surface AI-config drift on every lint pass
517
2993
  - Canonical-file contract (`kerf-skill-version` + `KERF-APP-CANONICAL-END` marker) lets `eslint --fix` refresh the canonical section while preserving consumer customizations below the marker
518
2994
 
519
2995
  ## [0.8.2] - 2026-05-19
520
2996
 
521
-
522
2997
  - Package `homepage` fields now point to the published docs site, with prominent links in both READMEs
523
2998
 
524
2999
  ## [0.8.1] - 2026-05-19
525
3000
 
526
-
527
3001
  - New `eslint-plugin-kerfjs` with four AST rules enforcing kerf Hard Rules
528
3002
 
529
3003
  ## [0.8.0] - 2026-05-18
530
3004
 
531
-
532
3005
  - Add opt-in dev warning `KERF_DEV_WARN_NARROW_SET=1` that fires when `set()` is called with a partial-state object (replace semantics would silently drop missing keys); names the missing keys and points at the `set({ ...get(), ...next })` merge fix
533
3006
  - Widen `KerfBaseAttrs.contentEditable` to accept `'plaintext-only'` and add the lowercase `contenteditable` alias
534
3007
  - Expand the `/kerf/migrating/` hub to 13 frameworks — adds Vue 3, Svelte 5, Solid, Preact, htmx, Angular, jQuery, Redux, and Astro pages alongside a refreshed 8-framework comparison matrix
@@ -539,7 +3012,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
539
3012
 
540
3013
  ## [0.7.0] - 2026-05-18
541
3014
 
542
-
543
3015
  - Granular list updates now preserve DOM identity, focus, scroll, IME state, `<details open>`/`<dialog open>`, and `data-morph-skip` subtrees across in-place row updates
544
3016
  - Two new fast paths in the granular reconciler cut krausest select-row by 71% (27.8 → 8.2 ms) and partial-update by 28% (46.8 → 33.8 ms)
545
3017
  - `each()`'s third parameter renamed from `key` to `cacheKey` to clarify it's a passive cache-invalidation comparator, not a React-style reconciliation identity; positional callers unaffected
@@ -555,7 +3027,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
555
3027
 
556
3028
  ## [0.6.0] - 2026-05-11
557
3029
 
558
-
559
3030
  - Public `morph(liveRoot, template)` export for standalone DOM reconciliation
560
3031
  - New `data-morph-preserve` attribute to opt elements out of morphing
561
3032
  - New `data-morph-skip-children` attribute — morph host attrs but leave subtree intact
@@ -564,12 +3035,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
564
3035
 
565
3036
  ## [0.5.1] - 2026-05-11
566
3037
 
567
-
568
3038
  - Fix `dist/jsx-runtime.d.ts` IntrinsicElements self-shadow that broke JSX typing in consumer apps
569
3039
 
570
3040
  ## [0.5.0] - 2026-05-10
571
3041
 
572
-
573
3042
  - Add `arraySignal` (`kerfjs/array-signal` subpath) — granular collection signal that drives O(patches) DOM updates for keyed lists
574
3043
  - Faster keyed-list updates: bulk-parse contiguous insert runs and consecutive update patches in the granular reconcile path
575
3044
  - Perf optimizations on the `each()` / `mount()` update path; benchmarks now competitive with Solid/Vue on swap/remove/clear
@@ -583,17 +3052,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
583
3052
 
584
3053
  ## [0.4.2] - 2026-05-09
585
3054
 
586
-
587
3055
  - No user-facing changes in this release.
588
3056
 
589
3057
  ## [0.4.1] - 2026-05-09
590
3058
 
591
-
592
3059
  - Just fixing the build
593
3060
 
594
3061
  ## [0.4.0] - 2026-05-09
595
3062
 
596
-
597
3063
  - `delegate()` now auto-promotes the seven well-known non-bubbling events (`focus`, `blur`, `scroll`, `load`, `error`, `mouseenter`, `mouseleave`) to capture phase, with `closest()`-style selector matching preserved
598
3064
  - Fixed focus and caret position loss when reordering keyed `each()` rows on engines that drop focus on `insertBefore` (older Safari, happy-dom)
599
3065
  - `mount()` now throws a descriptive error when the root element is null/undefined instead of a generic "Cannot set properties of null"
@@ -604,12 +3070,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
604
3070
 
605
3071
  ## [0.3.1] - 2026-05-08
606
3072
 
607
-
608
3073
  - Removed stale `morphdom` references; the bundled native diff is now the only reconciler
609
3074
 
610
3075
  ## [0.3.0] - 2026-05-08
611
3076
 
612
-
613
3077
  - Rebuilt render pipeline with structured segments and a native keyed-list diff, replacing the morphdom dependency
614
3078
  - Added `each()` for keyed list iteration with per-item HTML memoization by object identity
615
3079
  - Renamed the npm package from `kerf` to `kerfjs` (the `kerf` name tripped npm's typo-squatting heuristic); the brand, GitHub repo, and Pages URL are unchanged
@@ -620,24 +3084,20 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
620
3084
 
621
3085
  ## [0.2.1] - 2026-05-07
622
3086
 
623
-
624
3087
  - Add `Fragment` export to the `kerfjs` barrel for explicit JSX fragment usage
625
3088
 
626
3089
  ## [0.2.0] - 2026-05-07
627
3090
 
628
-
629
3091
  - Fix focused contenteditable losing focus/caret during morph
630
3092
  - Fix `SafeHtml` identity mismatch across entry points caused by dist bundling
631
3093
  - Fix `clearStoreRegistry` no-op in built output, restoring test isolation
632
3094
 
633
3095
  ## [0.1.2] - 2026-05-07
634
3096
 
635
-
636
3097
  - No user-facing changes; release tooling fixes only.
637
3098
 
638
3099
  ## [0.1.1] - 2026-05-07
639
3100
 
640
-
641
3101
  - This is just a publication script test
642
3102
 
643
3103
  ## [0.1.0] - 2026-05-07