kerfjs 5.0.0-beta.52 → 5.0.0-beta.54

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 (60) hide show
  1. package/CHANGELOG.md +738 -0
  2. package/README.md +4 -3
  3. package/ai/cursorrules +7 -6
  4. package/ai/manifest.json +11 -7
  5. package/ai/skill.md +7 -6
  6. package/dist/actions.js +3 -3
  7. package/dist/actions.js.map +1 -1
  8. package/dist/array-signal.js +1 -1
  9. package/dist/async.js +1 -1
  10. package/dist/attach.js +1 -68
  11. package/dist/attach.js.map +1 -1
  12. package/dist/{chunk-CEQMZYLR.js → chunk-3C6F6NM4.js} +1 -1
  13. package/dist/{chunk-CEQMZYLR.js.map → chunk-3C6F6NM4.js.map} +1 -1
  14. package/dist/{chunk-BDX3R4OM.js → chunk-4TX6YJDM.js} +8 -2
  15. package/dist/chunk-4TX6YJDM.js.map +1 -0
  16. package/dist/{chunk-ZOIERTUW.js → chunk-BI4HGICV.js} +3 -3
  17. package/dist/{chunk-ZOIERTUW.js.map → chunk-BI4HGICV.js.map} +1 -1
  18. package/dist/chunk-FM6N2B4N.js +70 -0
  19. package/dist/chunk-FM6N2B4N.js.map +1 -0
  20. package/dist/{chunk-ELXVRKY2.js → chunk-H7WTRM6F.js} +2 -2
  21. package/dist/{chunk-ELXVRKY2.js.map → chunk-H7WTRM6F.js.map} +1 -1
  22. package/dist/{chunk-Y2FOYPBV.js → chunk-IHMDBBUJ.js} +2 -2
  23. package/dist/chunk-IHMDBBUJ.js.map +1 -0
  24. package/dist/{chunk-3GSWKMBZ.js → chunk-UGNPDLNI.js} +7 -5
  25. package/dist/chunk-UGNPDLNI.js.map +1 -0
  26. package/dist/{chunk-H3TTM7PQ.js → chunk-WMRFXSLG.js} +112 -108
  27. package/dist/chunk-WMRFXSLG.js.map +1 -0
  28. package/dist/{chunk-E5R5GNKE.js → chunk-X6OQ2HTR.js} +2 -2
  29. package/dist/{chunk-E5R5GNKE.js.map → chunk-X6OQ2HTR.js.map} +1 -1
  30. package/dist/dev.d.ts +31 -13
  31. package/dist/dev.js +42 -23
  32. package/dist/dev.js.map +1 -1
  33. package/dist/html.js +3 -3
  34. package/dist/index.d.ts +12 -7
  35. package/dist/index.js +11 -11
  36. package/dist/index.js.map +1 -1
  37. package/dist/jsx-runtime.js +1 -1
  38. package/dist/list.d.ts +3 -2
  39. package/dist/list.js +27 -13
  40. package/dist/list.js.map +1 -1
  41. package/dist/overlay.d.ts +12 -25
  42. package/dist/overlay.js +177 -35
  43. package/dist/overlay.js.map +1 -1
  44. package/dist/remount.js +2 -2
  45. package/dist/router.d.ts +13 -4
  46. package/dist/router.js +8 -4
  47. package/dist/router.js.map +1 -1
  48. package/dist/scope.d.ts +10 -2
  49. package/dist/scope.js +22 -28
  50. package/dist/scope.js.map +1 -1
  51. package/dist/testing.js +1 -1
  52. package/dist/timing.d.ts +3 -2
  53. package/dist/timing.js +1 -1
  54. package/dist/timing.js.map +1 -1
  55. package/llms.txt +1 -1
  56. package/package.json +1 -1
  57. package/dist/chunk-3GSWKMBZ.js.map +0 -1
  58. package/dist/chunk-BDX3R4OM.js.map +0 -1
  59. package/dist/chunk-H3TTM7PQ.js.map +0 -1
  60. package/dist/chunk-Y2FOYPBV.js.map +0 -1
package/CHANGELOG.md CHANGED
@@ -6,6 +6,744 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ - **`@kerfjs/ui`'s `wireTabBars` reveals a tab the application selects.** A
10
+ tab added and selected by the app in an overflowing `TabBar` stayed
11
+ off-screen, because the wiring revealed a selection only at wire-up, on
12
+ focus, and after its own keyboard/drag changes. A change of a bar's selected
13
+ tab now scrolls that bar's strip (never the page) to show it; re-renders that
14
+ keep the selection leave a user-scrolled strip alone.
15
+
16
+ - **`eslint-plugin-kerfjs`'s `ui-composition` rule resolves application
17
+ wrappers imported through a barrel.** A cataloged wrapper registered only by
18
+ its defining source file, so `import { DemandSegmentsControl } from
19
+ './components/index.js'` (where `index.ts` re-exports it) was an unknown
20
+ element and reported `KUI-L202` in a toolbar zone despite its `rendersAs`.
21
+ Relative re-exports — `export { X } from`, `export { X as Y } from`,
22
+ `export * from`, and chains of them — are now followed to the cataloged
23
+ source file. Type-only and namespace re-exports and non-cataloged components
24
+ stay unresolved.
25
+
26
+ - **A collapsed `@kerfjs/ui` `Workbench` panel or `ResizableRegion` leaves
27
+ the accessibility tree.** Only its content was `inert`, so the labeled
28
+ `aside` / `section` stayed behind as an empty complementary or region
29
+ landmark that a screen-reader user could still land on. The collapsed
30
+ region itself now renders `inert` and `aria-hidden="true"`, like a
31
+ collapsed `CollapsiblePanel`; its separator was already hidden, and each
32
+ `restoreControl` renders outside it and stays reachable.
33
+
34
+ - **An expanded `@kerfjs/ui` `CollapsiblePanel` holds its size in a flex
35
+ layout.** The panel shrank like any flex item beside a work area with wide
36
+ content, while its content kept the panel's fixed size for the slide-out, so
37
+ the panel's edge clipped the content — in the collapsible-sidebar recipe at
38
+ 1280px, the rail showed 211 of its 232px and cut off the selected Inbox
39
+ row's count. The panel is now `flex-shrink: 0`; the sibling work area gives
40
+ way instead.
41
+
42
+ - **`@kerfjs/ui` overlay panels span their host on the fixed axis.** The
43
+ 85vw / 85vh overlay maximums (`--kui-resizable-region-overlay-max-*`,
44
+ `--kui-workbench-overlay-max-*`) applied to both axes, so an overlay bottom
45
+ drawer stopped about 15% short of its host's width and an overlay side rail
46
+ in a full-height layout stopped short of the bottom. Each maximum now caps
47
+ only the panel's resizable axis: side overlays cap their width, drawers
48
+ their height.
49
+
50
+ - **An overlay `@kerfjs/ui` `ResizableRegion` paints the surface.** With
51
+ `presentation="overlay"` the region floated over the work area with a
52
+ shadow but no background, so the content beneath — and a collapsed rail's
53
+ restore control stacked under it — showed through. It now paints
54
+ `--kui-color-surface`, like an overlay `CollapsiblePanel`; an inline region
55
+ stays transparent over its host.
56
+
57
+ - **A pinned `@kerfjs/ui` resize separator no longer overwrites the
58
+ remembered size.** When a container squeezes a `Workbench` rail, drawer, or
59
+ `ResizableRegion` below its `min` (or its reachable range collapses to one
60
+ size), the separator cannot move it, yet an arrow/Home/End key or a drag
61
+ still committed the configured `min` to the app's size signal and storage:
62
+ a rail remembered at 280px silently became 180px and came back at 180px
63
+ when the window widened. A pinned separator now commits nothing, so the
64
+ track returns at its remembered size once there is room.
65
+
66
+ - **A `pending` `@kerfjs/ui` `AppTab` is selectable and no longer dimmed.**
67
+ A known tab that is still opening now keeps its select button enabled and
68
+ in the roving tab order, so a user can switch to it and the application can
69
+ show placeholder content in its panel until loading completes. With
70
+ `webawesome.css` loaded, Web Awesome's native `button:disabled` no longer
71
+ dims the pending pill to 50%; it shows only its quiet name color and
72
+ trailing spinner, as documented. Close and dragging stay disabled while
73
+ pending, and it stays `aria-busy`. An unknown `placeholder` tab is
74
+ unchanged: disabled and out of the tab order.
75
+
76
+ - **Closing a `@kerfjs/ui` `Workbench` panel from its own control by mouse
77
+ returns focus in Safari.** Safari never focuses a clicked button, so
78
+ pressing an open panel's Hide control blurred the focused control to the
79
+ page before the click collapsed the panel, and `wireWorkbench` — which
80
+ returns focus only when it is inside the closing panel — left focus on the
81
+ page body instead of the toggle that opened it. A press that starts inside
82
+ the panel while focus is there now counts as focus inside, so the close
83
+ returns it; focus the user has moved to another element since is left
84
+ alone.
85
+ - **A collapsed `@kerfjs/ui` `CollapsiblePanel` or `ResizableRegion` leaves
86
+ the Tab order.** Both kept collapsed content in the DOM, only slid out and
87
+ clipped, so Tab still reached its controls; focusing one scrolled the clipped
88
+ panel, and a `CollapsiblePanel`'s `aria-hidden` put those focusable controls
89
+ inside a hidden subtree. A collapsed `CollapsiblePanel` now renders `inert`
90
+ beside its `aria-hidden`, and a collapsed `ResizableRegion` renders its
91
+ content wrapper `inert`, with no wiring needed. The slide-out still animates,
92
+ each `restoreControl` stays outside the inert subtree and reachable, and
93
+ `wireSidebar` still hands focus to the panel's toggle when it collapses from
94
+ inside.
95
+
96
+ - **A collapsed `@kerfjs/ui` `Workbench` rail's restore control no longer
97
+ covers an expanded bottom drawer.** A `leftRail` / `rightRail`
98
+ `restoreControl` floats in the Workbench's bottom corner, which the bottom
99
+ drawer's column reaches once the rail collapses, so with the drawer expanded
100
+ the control sat on the drawer's content. An expanded inline drawer now
101
+ publishes its top edge as a CSS anchor scoped to its Workbench, and the
102
+ rail's control floats the restore inset above it, returning to the corner
103
+ when the drawer collapses. An overlay drawer (static or `responsiveOverlayAt`)
104
+ still covers the control as every open overlay does, a nested Workbench's
105
+ drawer moves only its own rails' controls, and engines without anchor
106
+ positioning keep the corner.
107
+
108
+ - **A collapsed `@kerfjs/ui` `Workbench` panel's content leaves the Tab order.**
109
+ A collapsed rail or bottom drawer kept its content in the DOM, only slid out
110
+ and clipped, so Tab still reached its controls; focusing one (at a narrow
111
+ width, the collapsed inspector's or output drawer's own Hide control)
112
+ scrolled the clipped panel and slid its hidden content back over the work
113
+ area. The panel's content wrapper now renders `inert` while `collapsed`, with
114
+ no wiring needed, so neither Tab nor assistive technology reaches it; the
115
+ slide-out still animates and the `restoreControl`, which lives outside the
116
+ content, stays reachable.
117
+
118
+ - **A `kerfjs/overlay` popover or tooltip in a transformed or filtered modal
119
+ `<dialog>` now lands against its anchor.** A surface rendered into a
120
+ dialog's `[data-kerf-overlay-host][data-morph-skip]` slot is
121
+ `position: fixed`, and a dialog styled with `transform`, `filter`,
122
+ `perspective`, `contain`, or `will-change` becomes the containing block for
123
+ fixed descendants, so the surface used to land offset by the dialog's
124
+ position (and scale). `positionAnchored` (and so `autoReposition`,
125
+ `popover()`, and `tooltip()`) now measures the element's fixed-position
126
+ containing block with a transient 1px probe and maps its viewport
127
+ coordinates into it, compensating translation and scale. Rotated or skewed
128
+ containing blocks are not compensated.
129
+ - **The `@kerfjs/ui` UX catalog demonstrates a standalone restore control.**
130
+ The CollapsiblePanel route gains a "Restore control" example: a bottom
131
+ drawer that collapses to a `FloatingToolbar` `restoreControl` in its frame's
132
+ corner, wired with `wireSidebar` so the control restores the drawer and
133
+ focus moves between it and the drawer's own hide toggle.
134
+
135
+ - **A `@kerfjs/ui` collapsed rail's restore control no longer covers an open
136
+ bottom drawer beside it.** When a `CollapsiblePanel` or `ResizableRegion`
137
+ with a `restoreControl` shared its container with an expanded bottom
138
+ drawer — a bottom `CollapsiblePanel` or bottom `ResizableRegion` — as a
139
+ sibling or inside the app's work-area column, the control floated in the
140
+ container's bottom corner, on top of the drawer. It now floats the same
141
+ inset above the drawer's top edge, follows the drawer as it resizes, and
142
+ returns to the corner when the drawer collapses. A drawer nested deeper,
143
+ inside the work area's own content, does not move it. This uses CSS anchor
144
+ positioning; engines without it keep the container corner.
145
+
146
+ - **An open `@kerfjs/ui` `CollapsiblePanel` or `ResizableRegion` overlay now
147
+ covers other panels' restore controls, as a `Workbench` overlay does.** A
148
+ collapsed panel's `restoreControl` stacked at z-index 42, above
149
+ `wireSidebar`'s compact overlay and its backdrop and above an overlay
150
+ `ResizableRegion`, so it floated over an open overlay and stayed clickable
151
+ behind the overlay's focus trap. `--kui-collapsible-panel-restore-z` now
152
+ defaults to two less than `--kui-collapsible-panel-overlay-z` (38),
153
+ `--kui-resizable-region-restore-z` to two less than
154
+ `--kui-resizable-region-overlay-z` (39), and the compact backdrop's
155
+ `--kui-collapsible-panel-backdrop-z` to one less than the overlay (still
156
+ 39), so an app that moves the overlay z-index keeps the order. When the
157
+ overlay closes, the control shows and works again.
158
+ - **An open `@kerfjs/ui` `Workbench` overlay now covers other panels'
159
+ restore controls.** Restore controls stacked above every overlay, so a
160
+ collapsed drawer's corner restore control floated over an open right-rail
161
+ overlay's content. An open overlay is now the top layer, like a sheet over
162
+ the view beneath it: `--kui-workbench-restore-z` defaults to two less than
163
+ `--kui-workbench-overlay-z` (39 by default, previously 42), below the
164
+ drawer and rail overlays. When the overlay closes, the control shows and
165
+ works again; a collapsed overlay passes pointer events through to it.
166
+ - **`@kerfjs/ui` `wireWorkbench` overlay panels now take focus and keep Tab
167
+ inside them,** like `wireSidebar`'s compact overlay (the ARIA dialog
168
+ pattern). An open overlay covers the work area, yet focus stayed on the
169
+ control that opened it and Tab walked through the covered editor. A wired
170
+ panel that opens while it presents as an overlay now focuses its first
171
+ focusable control (usually the close control in its header); while it is
172
+ open, Tab and Shift+Tab cycle through its controls, and Escape, its own
173
+ close control, or an outside press leaves it, returning focus to the
174
+ opener. Panels opening inline never move focus. Focus returned on close
175
+ also skips an opener inside another panel that has closed since.
176
+ `dismissOverlays: false` leaves all of this to the app.
177
+ - **`@kerfjs/ui` `Workbench` keeps its work-area minimum beside fixed rails
178
+ and above the bottom drawer.** `mainMinSize` used to apply only beside a
179
+ resizable rail, so two fixed 280px rails on a ~730px Workbench left the
180
+ editor 174px; now every inline rail, fixed or resizable, shrinks in
181
+ proportion (its content following the shown width) to keep the work area
182
+ `mainMinSize` px wide. A new `mainMinHeight` prop (default 120, `0` turns it
183
+ off) is the drawer's counterpart: an inline drawer, fixed or resizable,
184
+ shrinks when the Workbench gets shorter, and `wireWorkbench` stops a
185
+ resizable drawer where the work area would drop below that height (reported
186
+ in `aria-valuemax`) instead of only at the drawer's own `max`.
187
+ - **A `@kerfjs/ui` Workbench rail squeezed below its minimum now reports the
188
+ width it shows.** When the Workbench was too narrow for a rail's `min`
189
+ beside the work-area minimum, the rail showed about 172px while its
190
+ separator still announced `aria-valuenow="180"`. The separator now reports
191
+ the shown width, with `aria-valuemin` and `aria-valuemax` pinned to it: the
192
+ WAI-ARIA window splitter's value is the separator's actual position and must
193
+ sit inside its range, and the separator cannot move a rail the container
194
+ holds there. Resizing still commits no less than `min`, and the configured
195
+ range returns once there is room. A `ResizableRegion` a parent clamps below
196
+ its `min` reports the same way.
197
+ - **`KUI-L018` (`kerf-ui-analyze` / `kerf-ui-doctor`) now measures contrast and
198
+ follows the cascade.** When a `--wa-color-{tone}-fill-loud` override and its
199
+ `on-loud` are both literal colors (including `light-dark()`), a pair below
200
+ 4.5:1 in either color scheme is reported with its ratio, and an unpaired
201
+ literal fill that still clears 4.5:1 against the theme's default `on-loud`
202
+ is no longer reported. An `on-loud` set in an ancestor scope (`:root`, or the
203
+ leading selector of a descendant chain) or in another stylesheet the same
204
+ entry loads now counts as the pair.
205
+ - **`kerf-component-catalog` lets a private application declare components
206
+ without a fake `exports` map.** In a private package
207
+ (`"private": true`), a component's `publicExports` items may omit `subpath`; the generator
208
+ verifies each name against the component's `source` file instead of
209
+ requiring a `./dist/<path>.js` export the bundled app never emits. Publishable
210
+ packages still need every subpath in `exports`. The v2 catalog schema makes
211
+ `subpath` optional to match, and the `kerfjs/ui-composition` rule resolves a
212
+ subpath-less export only by its source file.
213
+ - **`kerf-ui-doctor` and the `kerfjs/ui-public-boundaries` ESLint rule no
214
+ longer flag the documented `class="kui-app-root"` as KUI-L101 private
215
+ anatomy.** The `@kerfjs/ui` component catalog now has a `document-baseline`
216
+ composition entry for `@kerfjs/ui/document.css` that lists `kui-app-root`
217
+ as its public class, with a catalog demo route showing a `SplitView` filling
218
+ the root's definite height.
219
+ - The `@kerfjs/ui` heading guidance (`ai/skill.md`, `llms.txt`,
220
+ `docs/component-selection.md`, `docs/layout.md`) now sizes a panel title to
221
+ its track: extra-large `ToolbarText` stays the title size for a page, view,
222
+ dialog, or pane that holds it, while a narrow peripheral rail or drawer
223
+ names itself with the default size, since extra-large truncated "Inspector"
224
+ to "Ins…" in a 160px Workbench rail.
225
+ - **A `@kerfjs/ui` `Workbench` panel open when `wireWorkbench` wired up no
226
+ longer drops focus to the page when it closes from inside.** Such a panel
227
+ had no recorded opener, so without a restore control closing it from its
228
+ own header left focus on `<body>`. Focus now falls back to a control
229
+ outside the panel whose `aria-controls` names it (or an element inside it).
230
+ Each panel renders a stable `id` for that — `<id>-left-rail`,
231
+ `<id>-right-rail`, and `<id>-bottom-drawer` — so a toggle can name the
232
+ panel it controls.
233
+ - **`@kerfjs/ui` `wireWorkbench` keeps overlay panels exclusive.** At a narrow
234
+ width two overlay rails could both be open and, together wider than the
235
+ Workbench, the right rail covered the left rail's own close control. Opening
236
+ a panel while it presents as an overlay now closes every other open overlay
237
+ panel, the bottom drawer included, like `wireSidebar`'s compact overlays;
238
+ panels presenting inline are never closed by it. New option
239
+ `exclusiveOverlays` (default `true`); pass `false` to let overlays stay open
240
+ together.
241
+ - **A `@kerfjs/ui` `CollapsiblePanel` or `ResizableRegion` restore control
242
+ stays inside its container.** `restoreControl` was `position: fixed` to the
243
+ viewport, so a panel or region embedded in a page floated it over unrelated
244
+ page content. It now floats in the `restorePosition` corner of the element
245
+ that holds the panel (or region), scrolls with it, and is inset by the
246
+ component's restore inset plus the unsafe area of the edges that container
247
+ reaches, so a full-viewport shell still places it clear of them. While the
248
+ control is shown, that container isolates its stacking and becomes its
249
+ containing block; an app that positions the container itself keeps its own
250
+ `position`. As in `Workbench`, the corner owns the inset, so a
251
+ `FloatingToolbar` hosting the control no longer doubles it.
252
+ - The `@kerfjs/ui` UX catalog's Workbench page gains a responsive overlay
253
+ drawer example: an output drawer with `responsiveOverlayAt: "narrow"` that
254
+ takes its own track beside a wide editor and becomes a transient overlay
255
+ over the editor's bottom once the Workbench is 704px or narrower, starting
256
+ hidden there and closing from its own header, on Escape, or on a press
257
+ outside it. The Workbench guide shows the pattern.
258
+ - A `@kerfjs/ui` `Workbench` now stacks overlay rails above an overlay bottom
259
+ drawer. With the drawer and a rail both presented as overlays, their order
260
+ followed the markup, so the drawer covered the left rail while the right
261
+ rail covered the drawer. Both rails now stack above the drawer, mirroring
262
+ the inline layout where rails span the full height beside the drawer's
263
+ column, and the right rail stacks above the left; restore controls stay
264
+ above every overlay. The drawer's overlay z-index is now one less than
265
+ `--kui-workbench-overlay-z`.
266
+ - **A `@kerfjs/ui` `Workbench` overlay rail can always be closed from inside
267
+ itself.** At a narrow width, the UX catalog's inspector overlay covered the
268
+ editor toolbar's "Hide inspector" toggle, leaving only Escape or an outside
269
+ press to close it. The catalog's resizable rails now carry their own close
270
+ control in their `Pane` header, and the Workbench guide recommends the same
271
+ for any panel that may present as an overlay. `wireWorkbench` now returns
272
+ focus stranded in a panel however the panel closed — including through the
273
+ app's own control inside it, and for inline panels — to the control that
274
+ opened it, else to its restore control; before, only an Escape, an outside
275
+ press, or a breakpoint collapse returned it.
276
+ - **A `@kerfjs/ui` `Workbench` restore control stays inside its Workbench.**
277
+ `restoreControl` was `position: fixed` to the viewport, so an embedded
278
+ Workbench (the UX catalog's, for one) floated it over the page footer. It is
279
+ now anchored to a corner of the Workbench itself — the drawer's in the
280
+ corner of the work-area column, so it never covers an expanded rail — inset
281
+ by `--kui-workbench-restore-inset` plus the unsafe area of the edges that
282
+ corner reaches, and it scrolls with the Workbench. The Workbench now
283
+ isolates its own stacking, so its overlays and restore controls never stack
284
+ against the page around it. A `FloatingToolbar` can host the control
285
+ without doubling the inset: the corner owns it, and `FloatingToolbar` now
286
+ honors `--kui-floating-toolbar-inset` set on an ancestor as well as on
287
+ itself. The catalog's collapsed console restores from such a toolbar.
288
+ - **`@kerfjs/ui` `wireWorkbench` makes overlay panels transient.** A rail
289
+ whose `responsiveOverlayAt` breakpoint applied stayed expanded over the
290
+ editor until the user found its toggle. Pass a panel's app-owned
291
+ `collapsed` signal (`panels: { leftRail: { collapsed } }`; `size` is now
292
+ optional, so a panel need not be resizable) and, like `wireSidebar`'s
293
+ compact overlay, the wiring collapses it when the breakpoint begins to
294
+ apply (without the collapse motion) and gives back its inline state when
295
+ the breakpoint stops applying or the wiring is disposed. An open overlay
296
+ panel, responsive or `presentation: "overlay"`, closes on Escape or on a
297
+ press that starts and ends outside it, and focus stranded inside returns to
298
+ the control that opened it, else to its restore control. It is on by
299
+ default for panels given `collapsed`; `dismissOverlays: false` turns it
300
+ off. The UX catalog's resizable example wires its rails this way.
301
+ - A `@kerfjs/ui` `Workbench` rail with `presentation: "overlay"` now renders
302
+ exactly its size. Out of flow, it sized to its content plus its 1px
303
+ separator border, so a 280px rail measured 281px; it now takes its size
304
+ explicitly, as the responsive overlays do. Rails and the drawer are also
305
+ `border-box` now, so a panel's `size` is its whole track, border included,
306
+ even in an app without a global `box-sizing` reset, and overlay content
307
+ fills the panel inside its separator instead of painting over it.
308
+ - **A `@kerfjs/ui` `Workbench` bottom drawer with `presentation: "overlay"`
309
+ is visible again.** Its content is positioned against the drawer's bottom
310
+ edge, so the out-of-flow overlay box had no height of its own and collapsed
311
+ to its 1px border. An overlay drawer now takes its configured height (220px
312
+ by default, or its `size`), covers the bottom of the work-area column, and
313
+ clamps its content to the overlay maximum height. The drawer also accepts
314
+ `responsiveOverlayAt: "narrow" | "compact"` like the rails: below that
315
+ Workbench width it presents as the same overlay, without a separator and
316
+ never resizable, while the work area takes the full height and keeps its
317
+ bottom safe-area inset beneath it.
318
+ - The bundled AI assistant configs (`ai/skill.md`, `ai/cursorrules`) now
319
+ explain the `data-kerf-overlay-host` opt-in next to the one-`mount()`-per-root
320
+ rule: a modal `<dialog>` of your own that anchors kerf popovers or tooltips
321
+ needs `<div data-kerf-overlay-host data-morph-skip></div>`, the one allowed
322
+ nested-mount boundary. The `kerfjs/ai-assistant-configs` lint rule reports
323
+ installed copies as stale until they are refreshed.
324
+ - The opt-in `rebuiltListeners` dev warning no longer reports a popover or
325
+ tooltip closing inside a modal dialog's host slot. Removals inside a
326
+ `[data-kerf-overlay-host][data-morph-skip]` slot belong to the surface's own
327
+ mount, so the enclosing mount now ignores them, as its re-renders already
328
+ did. A listener lost to a re-render inside the surface is still reported,
329
+ once.
330
+ - **A popover or tooltip in a modal `<dialog>` no longer lingers after the
331
+ dialog is removed when its anchor was moved out first.** A surface rendered
332
+ into a dialog's host slot left with the dialog, but because its anchor was
333
+ still in the document, kerf kept it registered as open, and a tooltip on
334
+ that anchor never showed again. A slot-hosted surface now closes itself
335
+ (without calling `onDismiss`) when it leaves the document, and the tooltip
336
+ shows again on the next hover or focus.
337
+ - **A popover inside a modal `<dialog>` is reachable by Tab in Safari on
338
+ macOS.** A popover or tooltip rendered into a dialog's host slot follows the
339
+ dialog's own Tab order, and WebKit on macOS skips buttons and links there
340
+ unless the system keyboard-navigation setting is on. kerf now gives each
341
+ implicitly tabbable control in a slot-hosted surface an explicit
342
+ `tabindex="0"` when it opens and again before each Tab keypress, the same
343
+ way its focus trap already did. Authored tabindexes, disabled controls, and
344
+ surfaces outside a slot are left alone.
345
+ - `@kerfjs/ui` `Workbench` rails gain `responsiveOverlayAt: "narrow" |
346
+ "compact"`, mirroring `ResizableRegion`'s `responsiveFillAt`: below the
347
+ Workbench's own 704px (`narrow`) or 448px (`compact`) width the rail
348
+ presents as an overlay over the work area, without a separator and never
349
+ resizable, and inline above it — the CSS decides, so apps no longer derive
350
+ `presentation` from a `deviceClass()` check. The bottom drawer ignores it.
351
+ A collapsed overlay rail (with `presentation: "overlay"` too) now drops its
352
+ surface and shadow instead of leaving an empty panel-sized box over the work
353
+ area. The UX catalog's resizable example uses it and now renders at phone
354
+ widths, and its collapsed console's restore control is a `ToolbarControlGroup`
355
+ toggle that actually restores the console.
356
+ - `@kerfjs/ui` `Workbench` keeps a minimum work area beside resizable rails.
357
+ Two rails dragged near their 480px maximum on a ~1024px Workbench used to
358
+ leave the editor a sliver; now the work area keeps at least `mainMinSize`
359
+ px (a new `Workbench` prop, default 320, `0` turns it off). `wireWorkbench`
360
+ stops drag and keyboard resizing where the work area would drop below it
361
+ (and reports that reachable maximum in `aria-valuemax`), and when the
362
+ Workbench narrows, resizable rails shrink in proportion to their sizes, with
363
+ their content following the shown width, instead of squeezing the work
364
+ area. Workbenches without a resizable rail render exactly as before. The UX
365
+ catalog's resizable example adds a resizable inspector to show it.
366
+ - `@kerfjs/ui`'s static analyzer (`kerf-ui-analyze`, and the analyzer stage of
367
+ `kerf-ui-doctor`) reports a new review finding, `KUI-L018`, when a
368
+ stylesheet overrides a loud tone fill (`--wa-color-{tone}-fill-loud` for
369
+ neutral, brand, success, warning, danger, or pop) without setting the
370
+ matching `--wa-color-{tone}-on-loud` in the same scope. The theme pairs each
371
+ loud fill with a foreground that clears WCAG AA, and an unpaired override
372
+ keeps that foreground on a fill it was never checked against. The pair must
373
+ share a selector (after resolving nesting and splitting selector lists) and
374
+ at-rule scope in the same stylesheet, in one rule or separate ones; another
375
+ tone's `on-loud` does not count. It is a review finding because it checks
376
+ the pairing, not the contrast; pass `--fail-on-review` to gate it. The
377
+ `webawesome-theme.md` customization example now sets both halves of each
378
+ pair.
379
+ - **Tooltips and popovers also stay visible when your own code opens a modal
380
+ `<dialog>`.** `kerfjs/overlay` already moved every open tooltip and popover
381
+ above a modal dialog it opened itself, but a dialog opened with a bare
382
+ `dialog.showModal()` went unnoticed and left them inert beneath it. While
383
+ any kerf surface is open, one capture-phase listener on the document now
384
+ catches the dialog's `toggle` event and re-hosts them the same way, one task
385
+ after the `showModal()` call. The listener is removed once nothing is open.
386
+ An engine that fires no `toggle` event for `<dialog>` still misses such a
387
+ dialog; open it through `overlay()` there.
388
+ - **A popover inside a modal `<dialog>` is clickable.** Browsers make
389
+ everything outside an open modal dialog inert, so a popover lifted above the
390
+ dialog was visible but its buttons could not be clicked or focused, and
391
+ kerf's one-mount-per-tree rule kept it from rendering inside a kerf-mounted
392
+ dialog. `popover()` and `tooltip()` whose anchor sits inside an open modal
393
+ `<dialog>` now render into the dialog's host slot, an element marked
394
+ `data-kerf-overlay-host data-morph-skip`, so they are part of the modal and
395
+ fully interactive: Tab reaches them, and Escape closes the popover before the
396
+ dialog. A dialog `kerfjs/overlay` opened gets the slot automatically (an
397
+ empty `display: contents` element added on first use). For your own
398
+ `<dialog>`, add `<div data-kerf-overlay-host data-morph-skip></div>` to its
399
+ markup, or pass `container`. The slot is the one place a second `mount()`
400
+ may live inside a mounted tree: the enclosing mount's morph, nesting guard,
401
+ and binding and `each()` marker scans all stop there. The `kerfjs/dev`
402
+ `inert` warning now fires only when no slot was used.
403
+ - `@kerfjs/ui` `Workbench` rails and the bottom drawer can be drag-resized,
404
+ as an opt-in per panel. `WorkbenchPanel` gains `resizable: true | { min, max }`
405
+ (off by default, so existing Workbenches render unchanged); a resizable panel
406
+ gets a separator on its inner edge with the same focusable
407
+ `role="separator"` contract, grip, and 20px hit target as a
408
+ `ResizableRegion` handle. The new `@kerfjs/ui/wire-workbench` subpath's
409
+ `wireWorkbench(root, { id, panels, deviceClass?, storage?, onResize? })`
410
+ drives it: live pointer drags, arrow / Shift+arrow / Home / End keys,
411
+ clamping to the limits, and commits to the app-owned size signals, with
412
+ optional `storageKey` persistence. Collapsing keeps a panel's size, so it
413
+ expands back to the size it had, and resizing is suspended on compact device
414
+ classes and for overlay or hidden panels. The UX catalog's Workbench page
415
+ adds a resizable example. `wireResizableRegions` now drives only handles that
416
+ are direct children of a `ResizableRegion`, so it never double-drives a
417
+ Workbench separator under the same root.
418
+ - `@kerfjs/ui` elevation shadows follow the color scheme, with or without the
419
+ optional `@kerfjs/ui/webawesome.css` theme. Kerf's theme layer set
420
+ `--wa-shadow-s` / `-m` / `-l` to fixed light-mode shadows (14%, 11%, and 9%
421
+ black), which all but vanished against the dark page, and the
422
+ `--kui-shadow-s` / `--kui-shadow-l` foundation fallbacks copied those fixed
423
+ values. The theme now pairs each light alpha with a stronger dark one (50%,
424
+ 40%, 32%) through `light-dark()`, and the foundation fallbacks mirror them, so
425
+ selected segments and app tabs, compact overlay panels, and Web Awesome
426
+ surfaces that read `--wa-shadow-*` keep their elevation in dark mode. Light mode is unchanged. The browser parity test
427
+ now compares every `--kui-shadow-*` token with and without the theme in both
428
+ schemes.
429
+ - `@kerfjs/ui` text on loud tone fills clears WCAG AA in both color schemes,
430
+ with or without the optional `@kerfjs/ui/webawesome.css` theme. White
431
+ `on-loud` text reached about 2.2:1 on the success fill, 3.5:1 on the light
432
+ brand fill (`#0088ff`), and 3.6:1 on the danger fill (`#ff383c`) in both
433
+ schemes. Kerf's theme layer and the foundation fallbacks now pair each
434
+ fill with a foreground chosen to keep the tone's look: success keeps its
435
+ bright green with dark text (`#1d1d1f`, 7.6:1), as warning already did; light
436
+ brand and danger keep white text on a slightly deeper fill of the same hue
437
+ (`#0075db`, 4.6:1; `#eb0005`, 4.6:1); dark danger keeps its fill with dark
438
+ text (`#111113`, 5.3:1), as dark brand already did. Web Awesome accent
439
+ buttons, badges, and checked controls pick this up, and accent `wa-badge`s now
440
+ use their variant's `on-loud` foreground by default instead of a fixed dark
441
+ color (`--kui-wa-badge-accent-foreground` still overrides it). The browser
442
+ contrast test asserts every tone's `on-loud` over `fill-loud` pair, including
443
+ pop and neutral, in both schemes with and without the theme.
444
+ - `@kerfjs/ui` brand, success, and warning text clears WCAG AA on the lowered
445
+ page background in light mode, with or without the optional
446
+ `@kerfjs/ui/webawesome.css` theme. Their light `on-quiet` values (`#1e6ef4`,
447
+ `#008932`, `#a16a00`) passed on the white surface but reached only about
448
+ 4.1:1 on `#f2f2f7`. Kerf's theme layer and the foundation fallbacks now use
449
+ the same hues a step darker: `#0c62f3` (4.64:1), `#007f2e` (4.62:1), and
450
+ `#966200` (4.65:1) on the page, about 5.2:1 on white. Links, selected tabs,
451
+ segmented and toolbar selections, and token-search accents that read
452
+ `brand-on-quiet` darken with it. Dark mode is unchanged. The browser contrast
453
+ test no longer skips light text over the page background.
454
+ - `@kerfjs/ui` surfaces, links, and focus rings match the optional
455
+ `@kerfjs/ui/webawesome.css` theme without it. Three foundation fallbacks
456
+ pointed at a different token than the theme: `--kui-color-surface-raised`
457
+ fell back to the surface color (the theme's dark raised surface is
458
+ `#2c2c2e`), `--kui-color-text-link` fell back to the brand fill (`#0088ff`,
459
+ about 3.5:1 on white) instead of the brand on-quiet text color, and the focus
460
+ ring used the brand fill instead of the theme's focus color (`#007aff`). Each
461
+ now falls back to the theme's value. The browser parity test now covers every
462
+ `--kui-color-*` token the foundation declares, plus the focus ring, instead of
463
+ only the tone tokens.
464
+ - The `@kerfjs/ui` UX catalog's `StateBanner` route adds a placeholder with a
465
+ badge for every tone, next to the existing badge-less placeholder, so visual
466
+ review and captures show the solid badge's loading skeleton on each tone's
467
+ fill. A browser test checks that each tone's badged placeholder is present.
468
+ - `@kerfjs/ui` toned text stays readable in dark mode without the optional
469
+ `@kerfjs/ui/webawesome.css` theme. The foundation's fallbacks for the
470
+ success, warning, and danger `on-quiet` and `on-normal` tokens were single
471
+ light-mode colors, so dark danger `Text` rendered dark red on a dark surface
472
+ (under 3:1). Every tone token now falls back to the same light and dark
473
+ pair the theme assigns, including `brand-on-loud` (dark text on the pale dark
474
+ brand fill instead of white) and `neutral-border-quiet`. Light-mode colors
475
+ are unchanged. A browser test bundles the package with and without the theme
476
+ and asserts that every tone token resolves identically in both color schemes
477
+ and that toned text clears WCAG AA in dark mode.
478
+ - `@kerfjs/ui` danger text and badges are readable in dark mode, and every
479
+ tone's `StateBanner` detail and quiet or outline `Badge` text now clears
480
+ WCAG AA. The danger tone was the only one without an on-fill foreground: its
481
+ solid badge fill and `StateBanner` text reused `danger-on-quiet`, a mid red
482
+ in dark mode (banner detail 4.0:1, badge placeholder skeleton 2.25:1). A new
483
+ `--kui-color-danger-on-fill` token (also accepted by `uiColor()`) matches the
484
+ other tones' pale dark values, lifting the dark danger banner text to 8.2:1.
485
+ The banner detail no longer dims to 78% opacity, which left the light info,
486
+ success, warning, and danger details at 3.4 to 4.3:1; it reads as secondary
487
+ through its smaller type, and the neutral detail uses the quiet text color.
488
+ Quiet and outline badges use their tone's on-fill text, lifting light brand,
489
+ success, and warning from about 4.1:1 to at least 5:1.
490
+
491
+ - A `@kerfjs/ui` placeholder skeleton inside a solid `Badge`, such as the
492
+ badge of a `StateBanner` placeholder, is now clearly visible. The generic
493
+ skeleton tint, 12% of the text color, almost disappeared on the badge's
494
+ saturated fill in every tone. A solid badge now tints its skeleton from its
495
+ own foreground color, which contrasts with the fill in every tone in light
496
+ and dark mode. Quiet and outline badges keep the generic tint, which already
497
+ reads on their pale fills. Geometry is unchanged.
498
+ - The `@kerfjs/ui` SVG design templates render badges as filled pills: the
499
+ `StateBanner` badge and the `ListHeader` and `List` count pills used to show
500
+ as plain text, because the template build loaded only a hand-written list of
501
+ stylesheets that omitted `badge.css`. The build now loads each component's
502
+ full stylesheet set from its generated browser entry, the same set a
503
+ consumer's browser import loads, so a nested component's CSS can no longer be
504
+ missed. The `ListHeader` and `List` templates also pick up the label inset
505
+ from `text.css`, so a header label now lines up with the list items below it,
506
+ as it does in the live component.
507
+ - A `@kerfjs/ui` `StateBanner` placeholder's badge sits at the live badge's
508
+ height on every platform. With a skeleton instead of text, the badge took a
509
+ baseline from the skeleton's bottom edge and sat about a pixel higher, by an
510
+ amount that depended on the platform's font metrics.
511
+ - A `@kerfjs/ui` `StateBanner` with a long, wrapping title keeps its badge
512
+ one item gap after the title's last word, and its detail uses the full copy
513
+ width. The wide flex layout shrank the title and detail together, which left
514
+ a large gap before the badge and truncated the detail early. The title,
515
+ badge, and detail now flow as inline text: the detail stays beside the title
516
+ while it fits whole, and otherwise moves to its own line, truncating only at
517
+ the full width. A wrapped title's lines stay evenly spaced.
518
+ - A `@kerfjs/ui` `ListHeader` disclosure toggle disabled with `actionDisabled`
519
+ is dimmed by the component itself, at the same tone as a disabled action
520
+ button, and shows the `not-allowed` cursor. It used to rely on Web Awesome's
521
+ native `button:disabled` dimming, so without `webawesome.css` it looked
522
+ enabled. Disabled toggles and action buttons also no longer show hover
523
+ feedback without `webawesome.css`.
524
+ - A `@kerfjs/ui` `ListActionRow` placeholder keeps its `busy` spinner. The
525
+ whole status line used to collapse into one skeleton, dropping the spinner
526
+ the component draws from a known prop. Now only the `status` text becomes a
527
+ skeleton, matching `ListItem`.
528
+ - **A tooltip or popover already showing stays visible when a modal opens.**
529
+ Lifting a surface above an open modal `<dialog>` was decided only when the
530
+ surface opened, so a tooltip or popover already on screen when a modal
531
+ dialog opened was left inert beneath it. Whenever `kerfjs/overlay` opens a
532
+ modal `<dialog>` (a `native: true` modal, or one it lifted), it now moves
533
+ every open tooltip and popover into the top layer above the new dialog. One
534
+ that is already a `[popover]` is re-shown so it stacks on top. The existing
535
+ `kerfjs/dev` warnings still report an engine without the Popover API, and a
536
+ surface with controls the modal keeps inert. A modal `<dialog>` the app
537
+ opens itself, outside kerf, is not observed.
538
+ - Moved `mount()`'s list-binding lifecycle (turning list markers into
539
+ bindings, first-render row-contract validation, orphan pruning) into its own
540
+ internal module, so `mount.ts` holds only the render orchestration. No
541
+ behavior or public API change.
542
+ - A narrow (576px and below) `@kerfjs/ui` `StateBanner` keeps its badge
543
+ sized to its content, one item gap after the title and on the title's
544
+ baseline, as in the wide layout. The narrow grid stretched the badge across
545
+ the free column into a wide pill that sat less than 1px from the title.
546
+ - Hovering a `@kerfjs/ui` `ListActionRow` placeholder with
547
+ `trailingActionVisibility="interaction"` no longer reveals its disabled
548
+ trailing button. A placeholder is inert, so it keeps the live row's resting
549
+ state, with the trailing action hidden until an interaction it cannot take.
550
+ - A `@kerfjs/ui` `ListItem` placeholder keeps its `busy` spinner. The whole
551
+ trailing slot used to collapse into one skeleton, dropping the spinner the
552
+ component draws from a known prop and shifting the label's width. Now only
553
+ the values in that slot become skeletons: `status` text in its status line
554
+ and author-supplied `trailing` content (always a value, since the component
555
+ cannot tell a chevron from a count) each get their own.
556
+ - `@kerfjs/ui` placeholders keep the live disabled tone of a control the
557
+ author disabled. A disabled `ListItem` or `ListActionRow`, a
558
+ `trailingActionDisabled` button, a disabled `SegmentedControl` choice, or a
559
+ `ListHeader` with `actionDisabled` rendered at full opacity as a
560
+ placeholder, because the placeholder reset could not tell its own
561
+ `disabled` attribute from the author's. Those controls now carry a
562
+ component-owned `data-kui-disabled="true"` marker whenever their disabled
563
+ prop is set; the disabled tone keys on it and the placeholder reset skips
564
+ it. The marker is reserved: the extension-attribute props cannot set it.
565
+ - `@kerfjs/ui` adds a browser test that renders every placeholder-capable
566
+ component live and as a placeholder from identical props and diffs each
567
+ element's geometry and computed chrome, so any future drift outside a value
568
+ slot fails. It caught one more: a `ListActionRow` placeholder's `status`
569
+ line was 3.5px shorter than the live line; the status line now always
570
+ keeps a full line height.
571
+ - `@kerfjs/ui`'s `Select` placeholder hint now matches the live hint: 14px
572
+ text at a 1.6 line height, 7px below the box. It was 12px / 1.4 with a 4px
573
+ gap, so a placeholder with a `hint` was 8.6px shorter than the loaded
574
+ control and the content below it jumped when loading finished.
575
+ - `@kerfjs/ui`'s `StateBanner` placeholder adds a detail skeleton only when
576
+ `detail` is set, matching the live banner. Previously every placeholder
577
+ showed a detail line, so a banner without one changed shape when loading
578
+ finished. Pass the banner's real `title` and `detail` with `placeholder`;
579
+ an empty `detail` now means no detail line.
580
+ - `@kerfjs/ui`'s `AppTab` placeholder pill is no longer dimmed to 50% with a
581
+ `not-allowed` cursor when `webawesome.css` is loaded; it keeps the live
582
+ tab's tone, and only its name is a skeleton.
583
+ - `@kerfjs/ui`'s `ListHeader` placeholder no longer dims its action button
584
+ (48%) or, in `toggle` mode, the whole title and label (Web Awesome's native
585
+ 50%), and its inert action and toggle give no hover feedback. A live
586
+ `actionDisabled` header still dims its action.
587
+ - `@kerfjs/ui`'s `SegmentedControl` placeholder now keeps its choices. The
588
+ choices are known; only the selection is unknown, so a placeholder renders
589
+ every choice's live label or icon at its live size, selects none, and stays
590
+ inert. Previously every segment became a 4em text skeleton (widening icon
591
+ segments from 40px to 60px) and was dimmed to 45%.
592
+ - `@kerfjs/ui`'s `ListItem` and `ListActionRow` placeholders now match the
593
+ live row. They are no longer dimmed (the rows' own `:disabled` tone and
594
+ hover highlight skip placeholders), and a row with a `description` (or a
595
+ `ListActionRow` `status`) keeps that line as a second, smaller skeleton, so
596
+ the row keeps its live height instead of collapsing to one line. The
597
+ `ListActionRow` icon placeholder is an 18px block centered in its icon slot,
598
+ like `ListItem`'s.
599
+ - `@kerfjs/ui` placeholders no longer look disabled. A component's
600
+ `placeholder` keeps its controls `disabled` (inert and out of the keyboard
601
+ order), but `skeleton.css` now neutralizes generic disabled chrome under
602
+ `[data-placeholder="true"]`: full opacity and the default cursor instead of
603
+ Web Awesome's native `button:disabled` dimming to 50% with a `not-allowed`
604
+ cursor. A component's own opacity (a control hidden until interaction) still
605
+ applies.
606
+ - `toElement('<image href="…" />')` now returns an SVG `<image>`. The HTML
607
+ parser rewrites an orphan `<image>` to an HTML `<img>` before kerf sees the
608
+ tag, so it previously came back as an XHTML `img` that no `<svg>` would
609
+ render. kerf now recognizes a leading `<image` tag in the raw input
610
+ (case-insensitive, after leading whitespace) and parses it through the SVG
611
+ wrapper. A genuine `<img>` is unchanged.
612
+ - `delegateActions()` (`kerfjs/actions`) dispatches only the table's own
613
+ entries. It used to look the action up with a plain property read, so a
614
+ `data-action` naming an inherited `Object.prototype` member resolved:
615
+ `"__proto__"` or `"hasOwnProperty"` threw a `TypeError` from the listener,
616
+ and `"toString"` or `"constructor"` silently ran the inherited function.
617
+ Those values are now ignored like any other action absent from the table.
618
+ An own entry with such a name (`{ toString: … }`) still dispatches.
619
+ - `toElement()` now puts orphan camelCase SVG fragments in the SVG namespace.
620
+ `toElement('<linearGradient>…</linearGradient>')`, and likewise `clipPath`,
621
+ `radialGradient`, and `foreignObject`, previously returned an XHTML
622
+ `LINEARGRADIENT` element that no `<svg>` would render, because the lookup
623
+ compared the HTML parser's lowercased tag against camelCase names. The
624
+ fragment is now wrapped in `<svg>` and XML-parsed like `<path>` or `<g>`,
625
+ and the returned element keeps its camelCase `localName`.
626
+ - **A tooltip or popover no longer outlives its anchor.** When the anchor left
627
+ the document, most commonly because the modal containing it closed, the
628
+ `tooltip()` or `popover()` stayed on screen: a removed element fires no
629
+ `pointerleave` or `blur`. Both now watch their anchor with the same removal
630
+ tracking as `kerfjs/attach`. The tooltip hides, and the popover closes
631
+ without calling `onDismiss`, as soon as the anchor leaves the document.
632
+ Moving the anchor within the document keeps them open. A tooltip show still
633
+ pending when its anchor is removed never appears.
634
+ - **Overlays opened over a modal `<dialog>` are visible and usable.** A browser
635
+ makes everything outside an open modal dialog inert and paints a plain
636
+ element beneath it, so a `tooltip()`, `popover()`, or `confirm()` opened
637
+ from inside a `native: true` dialog without `native` itself was invisible and
638
+ could not be used. While a modal `<dialog>` is open, `kerfjs/overlay` now
639
+ hosts such a surface in the top layer anyway: a tooltip or popover becomes a
640
+ `[popover]`, and a nested modal becomes its own `<dialog>`. The browser's
641
+ default `[popover]` / `<dialog>` styles then apply, as they do with `native:
642
+ true`. Browsers keep a popover outside the modal dialog inert even in the top
643
+ layer, so a lifted popover with buttons is visible but not clickable. A new
644
+ always-on `kerfjs/dev` warning reports that case, and an engine without the
645
+ needed API.
646
+ - **A tooltip shown inside a modal no longer disables the modal.** `kerfjs/overlay`
647
+ used to treat the most recently opened overlay as the only one that could
648
+ react to input, so a `tooltip()` (which handles no input) inside an open
649
+ modal left Escape doing nothing and let Tab leave the dialog. Every overlay,
650
+ native or fallback, now joins one open-order stack, and each Escape, backdrop
651
+ click, outside click, or trapped Tab goes to the topmost surface that handles
652
+ it. A modal still blocks everything beneath it, and a surface that handles
653
+ nothing never intercepts. A popover that dismisses on Escape gets it before
654
+ the modal beneath. One click that is both outside a popover and on a modal's
655
+ backdrop now closes only the popover. A native `<dialog>` is arbitrated with
656
+ the fallback surfaces: one Escape closes one surface, and a fallback modal
657
+ beneath an open dialog no longer takes the dialog's Tab. Closing a lower
658
+ overlay first leaves focus in the overlay that holds it. An overlay opened
659
+ from inside it then restores focus to the closed overlay's opener, not a
660
+ detached node.
661
+ - `kerfjs` now exports the `EachOptions<T>` type (`{ cacheKey?, key? }`), the
662
+ options-object form of `each()`'s third argument, so a helper that builds or
663
+ forwards those options can import the shape instead of restating it:
664
+ `import { type EachOptions } from "kerfjs"`. Type-only; no runtime or bundle
665
+ change.
666
+ - Documentation: the companion-utility reference now matches the code. The
667
+ router's `match()` / `activeClass()` compare a literal path prefix, so
668
+ `match('/users/:id')` is never true. Link interception still applies to
669
+ `target="_self"`, the no-match `outlet()` is `null` with `params` `{}`,
670
+ components receive `(params, route)`, and hash mode keeps `route.hash` at
671
+ `''`. Invalid `bindList` configuration throws only `RangeError`. `popover()`
672
+ defaults `className` to `'kerf-popover'`, and the toast region is a reused
673
+ `.kerf-toasts` element when one exists. A `resource()` `cacheKey` applies
674
+ only to `run(input, fetcher)`, and throttle's `flush()` leaves the cooldown
675
+ running. `delegate()` / `delegateCapture()` throw on an invalid selector at
676
+ registration. The reference also now lists the `kerfjs/jsx-dev-runtime`
677
+ alias, the `DEV_HOOKS` export of `kerfjs/dev`, and `Signal#peek()`.
678
+ - Documentation: `morph(liveRoot, template)` is now described as it behaves.
679
+ It reconciles only the root's children and never touches the root's own
680
+ attributes, so `template` describes the content inside the root (an
681
+ `Element` template contributes its child nodes). The examples no longer
682
+ nest a duplicate `<article>` inside the live one. An `each()` list parent is
683
+ not a children-only short-circuit: the diff still walks it and skips only
684
+ the owned rows. The user-agent-owned `open` exception covers custom elements
685
+ as well as `<details>` / `<dialog>`, and a content-only row change inside an
686
+ `each()` list keeps its DOM node (and focus) unless the row's top-level tag
687
+ changes or the same render also inserts, removes, or moves rows.
688
+ - Every opt-in `kerfjs/dev` warning now ends with a silence hint that leads
689
+ with the browser-reachable switch, for example "Silence it with
690
+ enableWarnings({ narrowSet: false }) from kerfjs/dev, or set
691
+ KERF_DEV_WARN_NARROW_SET=0 (or unset it) under Node." Previously the
692
+ messages named only the environment variable, which a browser cannot reach.
693
+ The docs now teach `enableWarnings()` wherever they showed an env-only
694
+ switch, and name all three creation-time boundaries: `signal()`
695
+ (`untrackedSignals`), `effect()` (`delegateInEffect`), and `mount()`
696
+ (`rebuiltListeners`) decide once when created, so anything created before
697
+ the install or the opt-in stays uncovered.
698
+ - The `kerfjs/dev` rebuilt-listeners warning (`rebuiltListeners` /
699
+ `KERF_DEV_WARN_REBUILT_LISTENERS`) now dedups once per `mount()`, as
700
+ documented. Previously the first warning silenced every other mount for
701
+ the life of the process, so a second mount with the same bug stayed quiet.
702
+ - `kerfjs/router`: a second router on the same document no longer diverges
703
+ from the URL after a link click. The first router's interceptor navigates
704
+ with `pushState`, which fires no `popstate`, so the second router ignored
705
+ the already-handled click and kept the old route. An already-handled click
706
+ now re-syncs the route from the URL; when nothing navigated, it is a no-op.
707
+ One router per document remains the supported shape: a second router does
708
+ not follow another router's programmatic `navigate()` (no `popstate` fires
709
+ and routers do not broadcast to each other) until the next `popstate` or
710
+ link click. This is now documented and pinned by a test.
711
+ - `kerfjs/scope`: a disposed `Scope` handle no longer leaks registrations.
712
+ Previously `add` / `mount` / `effect` / `delegate` on a handle after its
713
+ `dispose()` (or from a disposer while `dispose()` ran) pushed into a list
714
+ nothing would ever run, leaving the effect subscribed or the listener
715
+ attached. Such a registration now tears down at once: `add(fn)` calls `fn`
716
+ immediately, and the wrappers create and immediately dispose.
717
+ - `bindList` measured-height virtualization no longer applies a stale
718
+ scroll-anchor correction. A `setHeight()` report for a row above the viewport
719
+ is now resolved when the animation frame runs: if the source removed the key,
720
+ moved it below the fold, or the user scrolled the row into view first,
721
+ `scrollTop` is left alone, and several reports for one key in a frame net
722
+ out. Fixed-height windows also clamp to the source length, so a list that
723
+ shrinks while scrolled deep no longer pads far past its content.
724
+ - `create-kerf-component` accepts an explicit `boundaries.rootClass: null` as
725
+ an author decision, as its schema always allowed. A wrapper whose root
726
+ element belongs to another catalog (a `rendersAs` wrapper) no longer has to
727
+ invent a class to satisfy the generator. An absent `rootClass` still fails
728
+ as undecided.
729
+ - `@kerfjs/ui`'s `Select` placeholder now matches the live control. The label
730
+ uses the field-label style (uppercase, extra-small, quiet, inset to line up
731
+ with the value), and the chevron is the same glyph at the same size.
732
+ Previously the placeholder drew a larger, regular-weight label at a
733
+ different inset and a different, smaller chevron. Only the value is a
734
+ skeleton; the chrome no longer changes when loading finishes.
735
+ - **Wrapper components can declare the cataloged root they render.** A
736
+ `kerf.components.json` component may set `composition.rendersAs`, for
737
+ example `["@kerfjs/ui:toolbar-control-group"]`: a list of the roots it
738
+ renders, any one of them, or nothing. `kerf-component-catalog` copies it
739
+ into the generated v2 entry and fails unless every key resolves to a
740
+ cataloged entry. `eslint-plugin-kerfjs`'s `ui-composition` rule, and so
741
+ `kerf-ui-doctor`, now loads the application and third-party catalogs a
742
+ `.kerf-ui-profile.json` declares. It resolves their components by package
743
+ subpath or source file and checks a wrapper as each declared root: a zone
744
+ must accept every root, each root's parent contract applies where the
745
+ wrapper is placed, and a wrapper counts as zero or one child. Undeclared
746
+ wrappers keep reporting `KUI-L202`.
9
747
  - `@kerfjs/ui`'s `AppTab` adds `pending` for a known tab that is still
10
748
  opening. It keeps the name visible in the quiet text color and uses it as
11
749
  the tab's accessible name, so a trailing spinner's label is not folded in.