kerfjs 5.0.0-beta.53 → 5.0.0-beta.55

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -6,6 +6,537 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ - **New `@kerfjs/ui` `PopupMenu` (`@kerfjs/ui/popup-menu`).** It renders a
10
+ trigger and typed commands, headings, and dividers over the Web Awesome
11
+ dropdown. Put it in a `single` `ToolbarControlGroup` with `nestedDropdown`, or
12
+ wrap it in a `PopupSurface`. Register it once with
13
+ `@kerfjs/ui/popup-menu/register`. It replaces hand-written `wa-dropdown`
14
+ markup, which the catalog discourages: the guidance, the toolbar demos, and
15
+ the Catalog's "Components" menu now use it. It also fixes two defects that
16
+ hand-written menus had:
17
+ - Icon-only triggers were unnamed, because a Web Awesome button ignores an
18
+ `aria-label` on its host. PopupMenu renders `label` as visually hidden text
19
+ inside the button.
20
+ - A keyboard choice (Enter or Space) never reached click-delegated
21
+ `data-action` handlers. PopupMenu's registration routes it through the
22
+ item's click.
23
+ - The UX catalog's application-layout demo no longer uses the discouraged
24
+ `wa-button-group`.
25
+
26
+ - **`@kerfjs/ui`'s `TokenSearchField` takes a `revision` prop for programmatic
27
+ text replacement.** The editor's text is DOM-owned, so a new `query` with the
28
+ same tokens was silently ignored, for example when reseeding a persistent
29
+ dialog's field on reopen. Changing `revision` rebuilds the editor from `query`
30
+ and `tokens`, so consumers no longer need to re-key a wrapper element.
31
+
32
+ - **`@kerfjs/ui`'s `TabBar` strip no longer scrolls vertically.** The strip set
33
+ only `overflow-x: auto`, so any vertical excess (consumer padding, focus-ring
34
+ insets, taller tabs under larger text) let it scroll up and down on touch. It
35
+ now scrolls only horizontally, and it takes a `min-height` so it grows to fit
36
+ taller tabs instead of clipping them.
37
+
38
+ - **The `@kerfjs/ui` desktop app-shell recipe is a `Workbench`.** The old
39
+ pane switcher that showed one pane at a time below desktop sizes is gone. The
40
+ navigation and inspector are now resizable Workbench rails with their own
41
+ toolbars, and their show toggles relocate into the work-area toolbar. On a
42
+ narrow container the rails overlay the task list one at a time. The
43
+ collapsible-sidebar recipe composes each panel's `toolbar` and uses
44
+ `CollapsiblePanelRelocated` for its restore controls.
45
+
46
+ - **`@kerfjs/ui`'s `CollapsiblePanel` composes a panel toolbar and relocates its
47
+ controls.** A panel takes the same `toolbar` roles as a `Workbench` panel
48
+ (`title`, `panelOnly`, `constant`, and a standard `toggle`) plus a `footer`,
49
+ and the new `CollapsiblePanelRelocated` renders a collapsed panel's constant
50
+ groups and toggle wherever the app's work-area toolbar holds them.
51
+ `wireSidebar` hands focus to that relocated toggle when the panel closes, even
52
+ when the app renders it a frame late. `collapsiblePanelToggleIcon` is
53
+ unchanged.
54
+
55
+ - **`@kerfjs/ui`'s `Catalog` is built on `Workbench` and dogfoods the package's
56
+ components.** The sidebar is a Workbench left rail with the standard toggle
57
+ (labels "Show/Hide {brand} catalog"), which moves into the entry toolbar while
58
+ collapsed; on a small screen the sidebar overlays the stage instead of the
59
+ former stacked two-column navigation, which is gone. The sidebar, sections,
60
+ footer, and example notes are `List` / `ListHeader` / `ListInsetText` / `Text`
61
+ compositions, and most catalog CSS (and every viewport media query in the
62
+ shell) is removed. New: `Catalog`'s `id`, `headerPlacement`, and
63
+ `footerPlacement`; `wireCatalog`'s `collapsed` and `id` options (the sidebar is
64
+ wired like a Workbench rail and an open overlay closes on a selection);
65
+ `Workbench`'s `mainHeader`, `mainFooter`, `mainHeaderPlacement`,
66
+ `mainFooterPlacement`, and `mainToolbar.responsive` / `responsiveAt`;
67
+ `ListInsetText`'s `rootAttributes`. `wireWorkbench` now also re-checks panel
68
+ breakpoints when the root font size changes, and hands focus to a panel's
69
+ relocated toggle when an unfocused click on its own toggle closes it (WebKit).
70
+
71
+ - **`@kerfjs/ui`'s `Workbench` rails overlay the work area on small screens by
72
+ default.** A rail's `responsiveOverlayAt` now defaults to `"narrow"` (704px
73
+ of Workbench width; the new `"never"` keeps it inline), so a sidebar covers
74
+ the work area and any open drawer instead of squeezing it, one at a time
75
+ with `wireWorkbench`. On a compact (448px) Workbench an open overlay rail
76
+ fills the width less a 44px dismiss strip
77
+ (`--kui-workbench-overlay-dismiss-margin`) on the side away from its edge;
78
+ the new per-rail `compactOverlay: "full"` fills it. The bottom drawer's
79
+ behavior is unchanged. `WorkbenchPanelToolbar` and the other panel-toolbar
80
+ types are now exported from `@kerfjs/ui/workbench`.
81
+
82
+ - **`@kerfjs/ui`'s `Workbench` composes panel toolbars and moves a closed
83
+ panel's controls into the work area.** A panel takes an optional `toolbar`
84
+ (`title`, `panelOnly`, `constant`, and a standard `toggle: { action, name }`
85
+ the Workbench renders) and `footer`; the Workbench takes `mainToolbar` and
86
+ `mainBottomToolbar`. While a panel is closed its constant groups and toggle
87
+ move to the work area — a left rail's lead the main toolbar, a right rail's
88
+ end it, a drawer's trail the bottom toolbar or float in a corner
89
+ `FloatingToolbar` — and its panel-only groups stay behind. `wireWorkbench`
90
+ keeps focus with the toggle as it moves. Panels without a `toolbar` render
91
+ exactly as before, and a region's only-child `Pane` or layout now fills it.
92
+
93
+ - **`@kerfjs/ui`'s list-detail dialog guidance is Workbench-shaped.** On roomy
94
+ devices the list is a full-height sidebar and the detail a full-height main
95
+ column, each a `Pane` with its own top `Toolbar`; the main column carries
96
+ the primary title, the close control (`data-dialog="close"` in a
97
+ `without-header` Web Awesome dialog), and the record actions, instead of a
98
+ dialog header spanning both columns. The recipe, `docs/recipes.md`, and the
99
+ shipped AI skill say so; compact devices keep the full-screen drill-down
100
+ sheet.
101
+
102
+ - **`@kerfjs/ui`'s `ToolbarControlGroup` centers a direct icon tile.** A
103
+ non-interactive icon placed directly in a group — a heading's identity
104
+ glyph — now takes an icon button's 40px slot and 16px visual, so it centers
105
+ in the group. It previously sat at the group's start edge, which pushed
106
+ apps to wrap it in a focusable `<button>` that did nothing.
107
+
108
+ - **`@kerfjs/ui`'s `Toolbar` keeps its controls in a top control band.** A
109
+ toolbar now reserves one 44px band at its top: each zone item no taller than
110
+ the band is centered in it by its own size, and a taller item — a wrapped
111
+ `ToolbarText` title, a multi-line search, a second trailing row — starts at
112
+ the band's top and grows down without moving the others. A wrapped title
113
+ centers its first line in the band, so trailing controls stay beside that
114
+ line instead of dropping to the middle of the whole title.
115
+
116
+ - **`@kerfjs/ui`'s segmented `TabBar` ends its strip concentric with the pill
117
+ track.** The first tab's leading corners, and with `allocation="fill"` the
118
+ last tab's trailing corners, now follow the track's curve instead of keeping
119
+ the small segment radius, and compact tabs are inset from the track's ends
120
+ by the same distance as from its top and bottom. Corners between tabs are
121
+ unchanged.
122
+
123
+ - **`@kerfjs/ui`'s `StateBanner` keeps its title, badge, and detail on one
124
+ baseline in Safari.** The truncating detail was a clipped `inline-block`,
125
+ which WebKit baselines at its bottom edge: the detail rose about 2px above
126
+ the title and pushed the title and badge down, so the banner's contents
127
+ looked vertically misaligned. The detail is now an inline flex wrapper
128
+ around a clipped inner `<span>`, which takes its text baseline in every
129
+ engine; truncation and the narrow-width detail line are unchanged.
130
+
131
+ - **`@kerfjs/ui`'s `wireTabBars` reveals a tab the application selects.** A
132
+ tab added and selected by the app in an overflowing `TabBar` stayed
133
+ off-screen, because the wiring revealed a selection only at wire-up, on
134
+ focus, and after its own keyboard/drag changes. A change of a bar's selected
135
+ tab now scrolls that bar's strip (never the page) to show it; re-renders that
136
+ keep the selection leave a user-scrolled strip alone.
137
+
138
+ - **`eslint-plugin-kerfjs`'s `ui-composition` rule resolves application
139
+ wrappers imported through a barrel.** A cataloged wrapper registered only by
140
+ its defining source file, so `import { DemandSegmentsControl } from
141
+ './components/index.js'` (where `index.ts` re-exports it) was an unknown
142
+ element and reported `KUI-L202` in a toolbar zone despite its `rendersAs`.
143
+ Relative re-exports — `export { X } from`, `export { X as Y } from`,
144
+ `export * from`, and chains of them — are now followed to the cataloged
145
+ source file. Type-only and namespace re-exports and non-cataloged components
146
+ stay unresolved.
147
+
148
+ - **A collapsed `@kerfjs/ui` `Workbench` panel or `ResizableRegion` leaves
149
+ the accessibility tree.** Only its content was `inert`, so the labeled
150
+ `aside` / `section` stayed behind as an empty complementary or region
151
+ landmark that a screen-reader user could still land on. The collapsed
152
+ region itself now renders `inert` and `aria-hidden="true"`, like a
153
+ collapsed `CollapsiblePanel`; its separator was already hidden, and each
154
+ `restoreControl` renders outside it and stays reachable.
155
+
156
+ - **An expanded `@kerfjs/ui` `CollapsiblePanel` holds its size in a flex
157
+ layout.** The panel shrank like any flex item beside a work area with wide
158
+ content, while its content kept the panel's fixed size for the slide-out, so
159
+ the panel's edge clipped the content — in the collapsible-sidebar recipe at
160
+ 1280px, the rail showed 211 of its 232px and cut off the selected Inbox
161
+ row's count. The panel is now `flex-shrink: 0`; the sibling work area gives
162
+ way instead.
163
+
164
+ - **`@kerfjs/ui` overlay panels span their host on the fixed axis.** The
165
+ 85vw / 85vh overlay maximums (`--kui-resizable-region-overlay-max-*`,
166
+ `--kui-workbench-overlay-max-*`) applied to both axes, so an overlay bottom
167
+ drawer stopped about 15% short of its host's width and an overlay side rail
168
+ in a full-height layout stopped short of the bottom. Each maximum now caps
169
+ only the panel's resizable axis: side overlays cap their width, drawers
170
+ their height.
171
+
172
+ - **An overlay `@kerfjs/ui` `ResizableRegion` paints the surface.** With
173
+ `presentation="overlay"` the region floated over the work area with a
174
+ shadow but no background, so the content beneath — and a collapsed rail's
175
+ restore control stacked under it — showed through. It now paints
176
+ `--kui-color-surface`, like an overlay `CollapsiblePanel`; an inline region
177
+ stays transparent over its host.
178
+
179
+ - **A pinned `@kerfjs/ui` resize separator no longer overwrites the
180
+ remembered size.** When a container squeezes a `Workbench` rail, drawer, or
181
+ `ResizableRegion` below its `min` (or its reachable range collapses to one
182
+ size), the separator cannot move it, yet an arrow/Home/End key or a drag
183
+ still committed the configured `min` to the app's size signal and storage:
184
+ a rail remembered at 280px silently became 180px and came back at 180px
185
+ when the window widened. A pinned separator now commits nothing, so the
186
+ track returns at its remembered size once there is room.
187
+
188
+ - **A `pending` `@kerfjs/ui` `AppTab` is selectable and no longer dimmed.**
189
+ A known tab that is still opening now keeps its select button enabled and
190
+ in the roving tab order, so a user can switch to it and the application can
191
+ show placeholder content in its panel until loading completes. With
192
+ `webawesome.css` loaded, Web Awesome's native `button:disabled` no longer
193
+ dims the pending pill to 50%; it shows only its quiet name color and
194
+ trailing spinner, as documented. Close and dragging stay disabled while
195
+ pending, and it stays `aria-busy`. An unknown `placeholder` tab is
196
+ unchanged: disabled and out of the tab order.
197
+
198
+ - **Closing a `@kerfjs/ui` `Workbench` panel from its own control by mouse
199
+ returns focus in Safari.** Safari never focuses a clicked button, so
200
+ pressing an open panel's Hide control blurred the focused control to the
201
+ page before the click collapsed the panel, and `wireWorkbench` — which
202
+ returns focus only when it is inside the closing panel — left focus on the
203
+ page body instead of the toggle that opened it. A press that starts inside
204
+ the panel while focus is there now counts as focus inside, so the close
205
+ returns it; focus the user has moved to another element since is left
206
+ alone.
207
+ - **A collapsed `@kerfjs/ui` `CollapsiblePanel` or `ResizableRegion` leaves
208
+ the Tab order.** Both kept collapsed content in the DOM, only slid out and
209
+ clipped, so Tab still reached its controls; focusing one scrolled the clipped
210
+ panel, and a `CollapsiblePanel`'s `aria-hidden` put those focusable controls
211
+ inside a hidden subtree. A collapsed `CollapsiblePanel` now renders `inert`
212
+ beside its `aria-hidden`, and a collapsed `ResizableRegion` renders its
213
+ content wrapper `inert`, with no wiring needed. The slide-out still animates,
214
+ each `restoreControl` stays outside the inert subtree and reachable, and
215
+ `wireSidebar` still hands focus to the panel's toggle when it collapses from
216
+ inside.
217
+
218
+ - **A collapsed `@kerfjs/ui` `Workbench` rail's restore control no longer
219
+ covers an expanded bottom drawer.** A `leftRail` / `rightRail`
220
+ `restoreControl` floats in the Workbench's bottom corner, which the bottom
221
+ drawer's column reaches once the rail collapses, so with the drawer expanded
222
+ the control sat on the drawer's content. An expanded inline drawer now
223
+ publishes its top edge as a CSS anchor scoped to its Workbench, and the
224
+ rail's control floats the restore inset above it, returning to the corner
225
+ when the drawer collapses. An overlay drawer (static or `responsiveOverlayAt`)
226
+ still covers the control as every open overlay does, a nested Workbench's
227
+ drawer moves only its own rails' controls, and engines without anchor
228
+ positioning keep the corner.
229
+
230
+ - **A collapsed `@kerfjs/ui` `Workbench` panel's content leaves the Tab order.**
231
+ A collapsed rail or bottom drawer kept its content in the DOM, only slid out
232
+ and clipped, so Tab still reached its controls; focusing one (at a narrow
233
+ width, the collapsed inspector's or output drawer's own Hide control)
234
+ scrolled the clipped panel and slid its hidden content back over the work
235
+ area. The panel's content wrapper now renders `inert` while `collapsed`, with
236
+ no wiring needed, so neither Tab nor assistive technology reaches it; the
237
+ slide-out still animates and the `restoreControl`, which lives outside the
238
+ content, stays reachable.
239
+
240
+ - **A `kerfjs/overlay` popover or tooltip in a transformed or filtered modal
241
+ `<dialog>` now lands against its anchor.** A surface rendered into a
242
+ dialog's `[data-kerf-overlay-host][data-morph-skip]` slot is
243
+ `position: fixed`, and a dialog styled with `transform`, `filter`,
244
+ `perspective`, `contain`, or `will-change` becomes the containing block for
245
+ fixed descendants, so the surface used to land offset by the dialog's
246
+ position (and scale). `positionAnchored` (and so `autoReposition`,
247
+ `popover()`, and `tooltip()`) now measures the element's fixed-position
248
+ containing block with a transient 1px probe and maps its viewport
249
+ coordinates into it, compensating translation and scale. Rotated or skewed
250
+ containing blocks are not compensated.
251
+ - **The `@kerfjs/ui` UX catalog demonstrates a standalone restore control.**
252
+ The CollapsiblePanel route gains a "Restore control" example: a bottom
253
+ drawer that collapses to a `FloatingToolbar` `restoreControl` in its frame's
254
+ corner, wired with `wireSidebar` so the control restores the drawer and
255
+ focus moves between it and the drawer's own hide toggle.
256
+
257
+ - **A `@kerfjs/ui` collapsed rail's restore control no longer covers an open
258
+ bottom drawer beside it.** When a `CollapsiblePanel` or `ResizableRegion`
259
+ with a `restoreControl` shared its container with an expanded bottom
260
+ drawer — a bottom `CollapsiblePanel` or bottom `ResizableRegion` — as a
261
+ sibling or inside the app's work-area column, the control floated in the
262
+ container's bottom corner, on top of the drawer. It now floats the same
263
+ inset above the drawer's top edge, follows the drawer as it resizes, and
264
+ returns to the corner when the drawer collapses. A drawer nested deeper,
265
+ inside the work area's own content, does not move it. This uses CSS anchor
266
+ positioning; engines without it keep the container corner.
267
+
268
+ - **An open `@kerfjs/ui` `CollapsiblePanel` or `ResizableRegion` overlay now
269
+ covers other panels' restore controls, as a `Workbench` overlay does.** A
270
+ collapsed panel's `restoreControl` stacked at z-index 42, above
271
+ `wireSidebar`'s compact overlay and its backdrop and above an overlay
272
+ `ResizableRegion`, so it floated over an open overlay and stayed clickable
273
+ behind the overlay's focus trap. `--kui-collapsible-panel-restore-z` now
274
+ defaults to two less than `--kui-collapsible-panel-overlay-z` (38),
275
+ `--kui-resizable-region-restore-z` to two less than
276
+ `--kui-resizable-region-overlay-z` (39), and the compact backdrop's
277
+ `--kui-collapsible-panel-backdrop-z` to one less than the overlay (still
278
+ 39), so an app that moves the overlay z-index keeps the order. When the
279
+ overlay closes, the control shows and works again.
280
+ - **An open `@kerfjs/ui` `Workbench` overlay now covers other panels'
281
+ restore controls.** Restore controls stacked above every overlay, so a
282
+ collapsed drawer's corner restore control floated over an open right-rail
283
+ overlay's content. An open overlay is now the top layer, like a sheet over
284
+ the view beneath it: `--kui-workbench-restore-z` defaults to two less than
285
+ `--kui-workbench-overlay-z` (39 by default, previously 42), below the
286
+ drawer and rail overlays. When the overlay closes, the control shows and
287
+ works again; a collapsed overlay passes pointer events through to it.
288
+ - **`@kerfjs/ui` `wireWorkbench` overlay panels now take focus and keep Tab
289
+ inside them,** like `wireSidebar`'s compact overlay (the ARIA dialog
290
+ pattern). An open overlay covers the work area, yet focus stayed on the
291
+ control that opened it and Tab walked through the covered editor. A wired
292
+ panel that opens while it presents as an overlay now focuses its first
293
+ focusable control (usually the close control in its header); while it is
294
+ open, Tab and Shift+Tab cycle through its controls, and Escape, its own
295
+ close control, or an outside press leaves it, returning focus to the
296
+ opener. Panels opening inline never move focus. Focus returned on close
297
+ also skips an opener inside another panel that has closed since.
298
+ `dismissOverlays: false` leaves all of this to the app.
299
+ - **`@kerfjs/ui` `Workbench` keeps its work-area minimum beside fixed rails
300
+ and above the bottom drawer.** `mainMinSize` used to apply only beside a
301
+ resizable rail, so two fixed 280px rails on a ~730px Workbench left the
302
+ editor 174px; now every inline rail, fixed or resizable, shrinks in
303
+ proportion (its content following the shown width) to keep the work area
304
+ `mainMinSize` px wide. A new `mainMinHeight` prop (default 120, `0` turns it
305
+ off) is the drawer's counterpart: an inline drawer, fixed or resizable,
306
+ shrinks when the Workbench gets shorter, and `wireWorkbench` stops a
307
+ resizable drawer where the work area would drop below that height (reported
308
+ in `aria-valuemax`) instead of only at the drawer's own `max`.
309
+ - **A `@kerfjs/ui` Workbench rail squeezed below its minimum now reports the
310
+ width it shows.** When the Workbench was too narrow for a rail's `min`
311
+ beside the work-area minimum, the rail showed about 172px while its
312
+ separator still announced `aria-valuenow="180"`. The separator now reports
313
+ the shown width, with `aria-valuemin` and `aria-valuemax` pinned to it: the
314
+ WAI-ARIA window splitter's value is the separator's actual position and must
315
+ sit inside its range, and the separator cannot move a rail the container
316
+ holds there. Resizing still commits no less than `min`, and the configured
317
+ range returns once there is room. A `ResizableRegion` a parent clamps below
318
+ its `min` reports the same way.
319
+ - **`KUI-L018` (`kerf-ui-analyze` / `kerf-ui-doctor`) now measures contrast and
320
+ follows the cascade.** When a `--wa-color-{tone}-fill-loud` override and its
321
+ `on-loud` are both literal colors (including `light-dark()`), a pair below
322
+ 4.5:1 in either color scheme is reported with its ratio, and an unpaired
323
+ literal fill that still clears 4.5:1 against the theme's default `on-loud`
324
+ is no longer reported. An `on-loud` set in an ancestor scope (`:root`, or the
325
+ leading selector of a descendant chain) or in another stylesheet the same
326
+ entry loads now counts as the pair.
327
+ - **`kerf-component-catalog` lets a private application declare components
328
+ without a fake `exports` map.** In a private package
329
+ (`"private": true`), a component's `publicExports` items may omit `subpath`; the generator
330
+ verifies each name against the component's `source` file instead of
331
+ requiring a `./dist/<path>.js` export the bundled app never emits. Publishable
332
+ packages still need every subpath in `exports`. The v2 catalog schema makes
333
+ `subpath` optional to match, and the `kerfjs/ui-composition` rule resolves a
334
+ subpath-less export only by its source file.
335
+ - **`kerf-ui-doctor` and the `kerfjs/ui-public-boundaries` ESLint rule no
336
+ longer flag the documented `class="kui-app-root"` as KUI-L101 private
337
+ anatomy.** The `@kerfjs/ui` component catalog now has a `document-baseline`
338
+ composition entry for `@kerfjs/ui/document.css` that lists `kui-app-root`
339
+ as its public class, with a catalog demo route showing a `SplitView` filling
340
+ the root's definite height.
341
+ - The `@kerfjs/ui` heading guidance (`ai/skill.md`, `llms.txt`,
342
+ `docs/component-selection.md`, `docs/layout.md`) now sizes a panel title to
343
+ its track: extra-large `ToolbarText` stays the title size for a page, view,
344
+ dialog, or pane that holds it, while a narrow peripheral rail or drawer
345
+ names itself with the default size, since extra-large truncated "Inspector"
346
+ to "Ins…" in a 160px Workbench rail.
347
+ - **A `@kerfjs/ui` `Workbench` panel open when `wireWorkbench` wired up no
348
+ longer drops focus to the page when it closes from inside.** Such a panel
349
+ had no recorded opener, so without a restore control closing it from its
350
+ own header left focus on `<body>`. Focus now falls back to a control
351
+ outside the panel whose `aria-controls` names it (or an element inside it).
352
+ Each panel renders a stable `id` for that — `<id>-left-rail`,
353
+ `<id>-right-rail`, and `<id>-bottom-drawer` — so a toggle can name the
354
+ panel it controls.
355
+ - **`@kerfjs/ui` `wireWorkbench` keeps overlay panels exclusive.** At a narrow
356
+ width two overlay rails could both be open and, together wider than the
357
+ Workbench, the right rail covered the left rail's own close control. Opening
358
+ a panel while it presents as an overlay now closes every other open overlay
359
+ panel, the bottom drawer included, like `wireSidebar`'s compact overlays;
360
+ panels presenting inline are never closed by it. New option
361
+ `exclusiveOverlays` (default `true`); pass `false` to let overlays stay open
362
+ together.
363
+ - **A `@kerfjs/ui` `CollapsiblePanel` or `ResizableRegion` restore control
364
+ stays inside its container.** `restoreControl` was `position: fixed` to the
365
+ viewport, so a panel or region embedded in a page floated it over unrelated
366
+ page content. It now floats in the `restorePosition` corner of the element
367
+ that holds the panel (or region), scrolls with it, and is inset by the
368
+ component's restore inset plus the unsafe area of the edges that container
369
+ reaches, so a full-viewport shell still places it clear of them. While the
370
+ control is shown, that container isolates its stacking and becomes its
371
+ containing block; an app that positions the container itself keeps its own
372
+ `position`. As in `Workbench`, the corner owns the inset, so a
373
+ `FloatingToolbar` hosting the control no longer doubles it.
374
+ - The `@kerfjs/ui` UX catalog's Workbench page gains a responsive overlay
375
+ drawer example: an output drawer with `responsiveOverlayAt: "narrow"` that
376
+ takes its own track beside a wide editor and becomes a transient overlay
377
+ over the editor's bottom once the Workbench is 704px or narrower, starting
378
+ hidden there and closing from its own header, on Escape, or on a press
379
+ outside it. The Workbench guide shows the pattern.
380
+ - A `@kerfjs/ui` `Workbench` now stacks overlay rails above an overlay bottom
381
+ drawer. With the drawer and a rail both presented as overlays, their order
382
+ followed the markup, so the drawer covered the left rail while the right
383
+ rail covered the drawer. Both rails now stack above the drawer, mirroring
384
+ the inline layout where rails span the full height beside the drawer's
385
+ column, and the right rail stacks above the left; restore controls stay
386
+ above every overlay. The drawer's overlay z-index is now one less than
387
+ `--kui-workbench-overlay-z`.
388
+ - **A `@kerfjs/ui` `Workbench` overlay rail can always be closed from inside
389
+ itself.** At a narrow width, the UX catalog's inspector overlay covered the
390
+ editor toolbar's "Hide inspector" toggle, leaving only Escape or an outside
391
+ press to close it. The catalog's resizable rails now carry their own close
392
+ control in their `Pane` header, and the Workbench guide recommends the same
393
+ for any panel that may present as an overlay. `wireWorkbench` now returns
394
+ focus stranded in a panel however the panel closed — including through the
395
+ app's own control inside it, and for inline panels — to the control that
396
+ opened it, else to its restore control; before, only an Escape, an outside
397
+ press, or a breakpoint collapse returned it.
398
+ - **A `@kerfjs/ui` `Workbench` restore control stays inside its Workbench.**
399
+ `restoreControl` was `position: fixed` to the viewport, so an embedded
400
+ Workbench (the UX catalog's, for one) floated it over the page footer. It is
401
+ now anchored to a corner of the Workbench itself — the drawer's in the
402
+ corner of the work-area column, so it never covers an expanded rail — inset
403
+ by `--kui-workbench-restore-inset` plus the unsafe area of the edges that
404
+ corner reaches, and it scrolls with the Workbench. The Workbench now
405
+ isolates its own stacking, so its overlays and restore controls never stack
406
+ against the page around it. A `FloatingToolbar` can host the control
407
+ without doubling the inset: the corner owns it, and `FloatingToolbar` now
408
+ honors `--kui-floating-toolbar-inset` set on an ancestor as well as on
409
+ itself. The catalog's collapsed console restores from such a toolbar.
410
+ - **`@kerfjs/ui` `wireWorkbench` makes overlay panels transient.** A rail
411
+ whose `responsiveOverlayAt` breakpoint applied stayed expanded over the
412
+ editor until the user found its toggle. Pass a panel's app-owned
413
+ `collapsed` signal (`panels: { leftRail: { collapsed } }`; `size` is now
414
+ optional, so a panel need not be resizable) and, like `wireSidebar`'s
415
+ compact overlay, the wiring collapses it when the breakpoint begins to
416
+ apply (without the collapse motion) and gives back its inline state when
417
+ the breakpoint stops applying or the wiring is disposed. An open overlay
418
+ panel, responsive or `presentation: "overlay"`, closes on Escape or on a
419
+ press that starts and ends outside it, and focus stranded inside returns to
420
+ the control that opened it, else to its restore control. It is on by
421
+ default for panels given `collapsed`; `dismissOverlays: false` turns it
422
+ off. The UX catalog's resizable example wires its rails this way.
423
+ - A `@kerfjs/ui` `Workbench` rail with `presentation: "overlay"` now renders
424
+ exactly its size. Out of flow, it sized to its content plus its 1px
425
+ separator border, so a 280px rail measured 281px; it now takes its size
426
+ explicitly, as the responsive overlays do. Rails and the drawer are also
427
+ `border-box` now, so a panel's `size` is its whole track, border included,
428
+ even in an app without a global `box-sizing` reset, and overlay content
429
+ fills the panel inside its separator instead of painting over it.
430
+ - **A `@kerfjs/ui` `Workbench` bottom drawer with `presentation: "overlay"`
431
+ is visible again.** Its content is positioned against the drawer's bottom
432
+ edge, so the out-of-flow overlay box had no height of its own and collapsed
433
+ to its 1px border. An overlay drawer now takes its configured height (220px
434
+ by default, or its `size`), covers the bottom of the work-area column, and
435
+ clamps its content to the overlay maximum height. The drawer also accepts
436
+ `responsiveOverlayAt: "narrow" | "compact"` like the rails: below that
437
+ Workbench width it presents as the same overlay, without a separator and
438
+ never resizable, while the work area takes the full height and keeps its
439
+ bottom safe-area inset beneath it.
440
+ - The bundled AI assistant configs (`ai/skill.md`, `ai/cursorrules`) now
441
+ explain the `data-kerf-overlay-host` opt-in next to the one-`mount()`-per-root
442
+ rule: a modal `<dialog>` of your own that anchors kerf popovers or tooltips
443
+ needs `<div data-kerf-overlay-host data-morph-skip></div>`, the one allowed
444
+ nested-mount boundary. The `kerfjs/ai-assistant-configs` lint rule reports
445
+ installed copies as stale until they are refreshed.
446
+ - The opt-in `rebuiltListeners` dev warning no longer reports a popover or
447
+ tooltip closing inside a modal dialog's host slot. Removals inside a
448
+ `[data-kerf-overlay-host][data-morph-skip]` slot belong to the surface's own
449
+ mount, so the enclosing mount now ignores them, as its re-renders already
450
+ did. A listener lost to a re-render inside the surface is still reported,
451
+ once.
452
+ - **A popover or tooltip in a modal `<dialog>` no longer lingers after the
453
+ dialog is removed when its anchor was moved out first.** A surface rendered
454
+ into a dialog's host slot left with the dialog, but because its anchor was
455
+ still in the document, kerf kept it registered as open, and a tooltip on
456
+ that anchor never showed again. A slot-hosted surface now closes itself
457
+ (without calling `onDismiss`) when it leaves the document, and the tooltip
458
+ shows again on the next hover or focus.
459
+ - **A popover inside a modal `<dialog>` is reachable by Tab in Safari on
460
+ macOS.** A popover or tooltip rendered into a dialog's host slot follows the
461
+ dialog's own Tab order, and WebKit on macOS skips buttons and links there
462
+ unless the system keyboard-navigation setting is on. kerf now gives each
463
+ implicitly tabbable control in a slot-hosted surface an explicit
464
+ `tabindex="0"` when it opens and again before each Tab keypress, the same
465
+ way its focus trap already did. Authored tabindexes, disabled controls, and
466
+ surfaces outside a slot are left alone.
467
+ - `@kerfjs/ui` `Workbench` rails gain `responsiveOverlayAt: "narrow" |
468
+ "compact"`, mirroring `ResizableRegion`'s `responsiveFillAt`: below the
469
+ Workbench's own 704px (`narrow`) or 448px (`compact`) width the rail
470
+ presents as an overlay over the work area, without a separator and never
471
+ resizable, and inline above it — the CSS decides, so apps no longer derive
472
+ `presentation` from a `deviceClass()` check. The bottom drawer ignores it.
473
+ A collapsed overlay rail (with `presentation: "overlay"` too) now drops its
474
+ surface and shadow instead of leaving an empty panel-sized box over the work
475
+ area. The UX catalog's resizable example uses it and now renders at phone
476
+ widths, and its collapsed console's restore control is a `ToolbarControlGroup`
477
+ toggle that actually restores the console.
478
+ - `@kerfjs/ui` `Workbench` keeps a minimum work area beside resizable rails.
479
+ Two rails dragged near their 480px maximum on a ~1024px Workbench used to
480
+ leave the editor a sliver; now the work area keeps at least `mainMinSize`
481
+ px (a new `Workbench` prop, default 320, `0` turns it off). `wireWorkbench`
482
+ stops drag and keyboard resizing where the work area would drop below it
483
+ (and reports that reachable maximum in `aria-valuemax`), and when the
484
+ Workbench narrows, resizable rails shrink in proportion to their sizes, with
485
+ their content following the shown width, instead of squeezing the work
486
+ area. Workbenches without a resizable rail render exactly as before. The UX
487
+ catalog's resizable example adds a resizable inspector to show it.
488
+ - `@kerfjs/ui`'s static analyzer (`kerf-ui-analyze`, and the analyzer stage of
489
+ `kerf-ui-doctor`) reports a new review finding, `KUI-L018`, when a
490
+ stylesheet overrides a loud tone fill (`--wa-color-{tone}-fill-loud` for
491
+ neutral, brand, success, warning, danger, or pop) without setting the
492
+ matching `--wa-color-{tone}-on-loud` in the same scope. The theme pairs each
493
+ loud fill with a foreground that clears WCAG AA, and an unpaired override
494
+ keeps that foreground on a fill it was never checked against. The pair must
495
+ share a selector (after resolving nesting and splitting selector lists) and
496
+ at-rule scope in the same stylesheet, in one rule or separate ones; another
497
+ tone's `on-loud` does not count. It is a review finding because it checks
498
+ the pairing, not the contrast; pass `--fail-on-review` to gate it. The
499
+ `webawesome-theme.md` customization example now sets both halves of each
500
+ pair.
501
+ - **Tooltips and popovers also stay visible when your own code opens a modal
502
+ `<dialog>`.** `kerfjs/overlay` already moved every open tooltip and popover
503
+ above a modal dialog it opened itself, but a dialog opened with a bare
504
+ `dialog.showModal()` went unnoticed and left them inert beneath it. While
505
+ any kerf surface is open, one capture-phase listener on the document now
506
+ catches the dialog's `toggle` event and re-hosts them the same way, one task
507
+ after the `showModal()` call. The listener is removed once nothing is open.
508
+ An engine that fires no `toggle` event for `<dialog>` still misses such a
509
+ dialog; open it through `overlay()` there.
510
+ - **A popover inside a modal `<dialog>` is clickable.** Browsers make
511
+ everything outside an open modal dialog inert, so a popover lifted above the
512
+ dialog was visible but its buttons could not be clicked or focused, and
513
+ kerf's one-mount-per-tree rule kept it from rendering inside a kerf-mounted
514
+ dialog. `popover()` and `tooltip()` whose anchor sits inside an open modal
515
+ `<dialog>` now render into the dialog's host slot, an element marked
516
+ `data-kerf-overlay-host data-morph-skip`, so they are part of the modal and
517
+ fully interactive: Tab reaches them, and Escape closes the popover before the
518
+ dialog. A dialog `kerfjs/overlay` opened gets the slot automatically (an
519
+ empty `display: contents` element added on first use). For your own
520
+ `<dialog>`, add `<div data-kerf-overlay-host data-morph-skip></div>` to its
521
+ markup, or pass `container`. The slot is the one place a second `mount()`
522
+ may live inside a mounted tree: the enclosing mount's morph, nesting guard,
523
+ and binding and `each()` marker scans all stop there. The `kerfjs/dev`
524
+ `inert` warning now fires only when no slot was used.
525
+ - `@kerfjs/ui` `Workbench` rails and the bottom drawer can be drag-resized,
526
+ as an opt-in per panel. `WorkbenchPanel` gains `resizable: true | { min, max }`
527
+ (off by default, so existing Workbenches render unchanged); a resizable panel
528
+ gets a separator on its inner edge with the same focusable
529
+ `role="separator"` contract, grip, and 20px hit target as a
530
+ `ResizableRegion` handle. The new `@kerfjs/ui/wire-workbench` subpath's
531
+ `wireWorkbench(root, { id, panels, deviceClass?, storage?, onResize? })`
532
+ drives it: live pointer drags, arrow / Shift+arrow / Home / End keys,
533
+ clamping to the limits, and commits to the app-owned size signals, with
534
+ optional `storageKey` persistence. Collapsing keeps a panel's size, so it
535
+ expands back to the size it had, and resizing is suspended on compact device
536
+ classes and for overlay or hidden panels. The UX catalog's Workbench page
537
+ adds a resizable example. `wireResizableRegions` now drives only handles that
538
+ are direct children of a `ResizableRegion`, so it never double-drives a
539
+ Workbench separator under the same root.
9
540
  - `@kerfjs/ui` elevation shadows follow the color scheme, with or without the
10
541
  optional `@kerfjs/ui/webawesome.css` theme. Kerf's theme layer set
11
542
  `--wa-shadow-s` / `-m` / `-l` to fixed light-mode shadows (14%, 11%, and 9%
package/ai/cursorrules CHANGED
@@ -1,4 +1,4 @@
1
- <!-- kerf-skill-version: 1.21.1 -->
1
+ <!-- kerf-skill-version: 1.22.0 -->
2
2
 
3
3
  # kerf.cursorrules — rules for building apps with kerf
4
4
 
@@ -126,7 +126,7 @@ if (import.meta.env.DEV) await import("kerfjs/dev");
126
126
  3. **Escape hatches:** `data-morph-skip` (element + subtree preserved verbatim — for Monaco / xterm / D3); `data-morph-skip-children` (attrs morph, subtree preserved — for client-hydrated slots whose host classes change); `data-morph-preserve` (element survives the trailing-removal pass — for imperatively-injected nodes like autoplay videos).
127
127
  4. **Never `addEventListener` inside a `mount()`-managed tree** unless under `data-morph-skip`. A morph re-render may discard the node. Use `delegate` / `delegateCapture` instead.
128
128
  5. **Capture the `delegate()` / `delegateCapture()` disposer** whenever the registration's scope is shorter than the page. Both helpers return `() => void`; the listener closure pins `rootEl`, `handler`, and everything the handler closes over (stores, signals, app state). Discarding the disposer on a transient root (modal, route view, mount swap, dynamic widget) leaks the listener AND the app graph it captures; re-mount cycles stack listeners linearly. `mount()`'s own disposer does NOT remove delegates for you. Safe to discard only when the registration is truly page-lifetime (root is `document.body` or equivalent, attached once at startup, never torn down).
129
- 6. **One `mount()` per root.** Don't nest. Compose with plain functions that return JSX.
129
+ 6. **One `mount()` per root.** Don't nest. Compose with plain functions that return JSX. One exception: a modal `<dialog>` of your own that anchors kerf `popover()` / `tooltip()` surfaces must contain `<div data-kerf-overlay-host data-morph-skip></div>` — kerf renders those surfaces into it (inside the modal, so they are not inert) and that slot is the one allowed nested-mount boundary. Dialogs `kerfjs/overlay` opens get a slot automatically.
130
130
  7. **No `<MyComponent />` semantics with hooks.** Components are plain functions returning JSX. State lives in module-scope signals or stores, never in component closures.
131
131
  8. **Values bind, structure re-renders.** For a value hole, pass the signal/computed ITSELF (`<span>{count}</span>`, `class={sig}`) — kerf updates that one node directly, no render re-run. Read `.value` only when the JSX _structure_ depends on the signal — and then the read must happen INSIDE the render function to be tracked: `const x = count.value; mount(el, () => <span>{x}</span>)` will NOT re-render. Bind a STABLE signal/computed instance per hole (a `computed` that switches internally), never `class={cond ? sigA : sigB}` — switching instances can go silently stale (`enableWarnings({ staleBinding: true })` (or `KERF_DEV_WARN_STALE_BINDING=1` under Node) detects it). Endpoint: a render reading NO `.value` runs exactly once — a fully bound mount never re-renders; `enableWarnings({ valueOnlyRerender: true })` (or `KERF_DEV_WARN_VALUE_ONLY_RERENDER=1` under Node) flags re-renders that could have been bindings.
132
132
  9. **Store actions receive `(set, get)`, not `(state)`.** `set(next)` replaces state; mutating `get()` does nothing.
package/ai/manifest.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
- "kerfjsVersion": "5.0.0-beta.53",
2
+ "kerfjsVersion": "5.0.0-beta.55",
3
3
  "files": [
4
4
  {
5
5
  "name": "skill",
6
6
  "source": "kerf.claude-skill.md",
7
7
  "bundle": "ai/skill.md",
8
8
  "dest": ".claude/skills/kerf-app/SKILL.md",
9
- "version": "1.23.1",
10
- "sha256": "e21d7db1097ad95c11af9248ad17d514716bebc5646becf57d19a7598d6e382c",
9
+ "version": "1.24.0",
10
+ "sha256": "f74616ec20120fa97ec2ff7e92971f5721ee8dcb0b5b7da2dd2604bb87002b69",
11
11
  "history": {
12
12
  "1.0.0": "bef982cb743fd4c73e666ee5cf774112e57c93ebb13b24bbacbf08577db0930a",
13
13
  "1.1.0": "f2aadb21e33dd389028951ae1ecbd9f8ba68648b3c5beba43fb4dfa2ab29f195",
@@ -38,7 +38,8 @@
38
38
  "1.21.0": "265b747eeb9c3d445c60d66a1109eec42705075b7cd154d107bd79d543c1e7e5",
39
39
  "1.22.0": "dc99b0ca5e911be11cbfa2ae515c0e161a414a5185895069535c41ba56664e70",
40
40
  "1.23.0": "28b82b4d31b98814c8fbbe1cb0bcf716192deb494c020b9af41a30830e6a0007",
41
- "1.23.1": "e21d7db1097ad95c11af9248ad17d514716bebc5646becf57d19a7598d6e382c"
41
+ "1.23.1": "e21d7db1097ad95c11af9248ad17d514716bebc5646becf57d19a7598d6e382c",
42
+ "1.24.0": "f74616ec20120fa97ec2ff7e92971f5721ee8dcb0b5b7da2dd2604bb87002b69"
42
43
  }
43
44
  },
44
45
  {
@@ -46,8 +47,8 @@
46
47
  "source": "kerf.cursorrules",
47
48
  "bundle": "ai/cursorrules",
48
49
  "dest": ".cursorrules",
49
- "version": "1.21.1",
50
- "sha256": "9b3eb825a7bec05bb6f0138f77b45fadf896b83020178c34c5a2f531c38f4dee",
50
+ "version": "1.22.0",
51
+ "sha256": "3515c663e908b2d6dc9a2a9cf42f2a5bf9a5b3a10c79d43e30cc8427e2ce209d",
51
52
  "history": {
52
53
  "1.0.0": "98f7faff2b7585ef9cc511add42ea5d9bf0cbae8307b066af090e9281136d2ab",
53
54
  "1.1.0": "a2ec8c221dc694c98b81009cd2d99f9159a45fda94585de2f54471b227099eda",
@@ -76,7 +77,8 @@
76
77
  "1.19.0": "c5b91e449f6feccaa36a04cb7f8550d3e414eb2795ea6c5dccdf6071206b3261",
77
78
  "1.20.0": "5e7b8d8ddc41f60220961417c9ae01c193fae045378a10a3e9c96a0e3cf10a03",
78
79
  "1.21.0": "a1b663b44650b1a04f89c60b26b002322fc6789650291477e14e13329c3ce25f",
79
- "1.21.1": "9b3eb825a7bec05bb6f0138f77b45fadf896b83020178c34c5a2f531c38f4dee"
80
+ "1.21.1": "9b3eb825a7bec05bb6f0138f77b45fadf896b83020178c34c5a2f531c38f4dee",
81
+ "1.22.0": "3515c663e908b2d6dc9a2a9cf42f2a5bf9a5b3a10c79d43e30cc8427e2ce209d"
80
82
  }
81
83
  }
82
84
  ]
package/ai/skill.md CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: kerf-app
3
3
  description: Build UIs in the kerf reactive framework and its @kerfjs/ui component package (https://github.com/brianwestphal/kerf). Use this skill whenever the user is writing or modifying code that imports `kerfjs` or `@kerfjs/ui`, asks to add a feature to a kerf app, or asks "how do I do X in kerf?". Use it proactively the moment you spot a kerf import in the file you're editing.
4
- kerf-skill-version: 1.23.1
4
+ kerf-skill-version: 1.24.0
5
5
  ---
6
6
 
7
7
  # Building apps with kerf
@@ -135,7 +135,7 @@ if (import.meta.env.DEV) await import("kerfjs/dev");
135
135
  - `data-morph-preserve` — element survives the trailing-removal pass even when the new template doesn't emit it. For imperatively-injected children (autoplay video, tooltip overlay, analytics pixel). Does NOT block a keyed-match move.
136
136
  4. **Never `addEventListener` inside a `mount()`-managed tree** unless under `data-morph-skip`. A morph re-render may discard the node. Use `delegate` / `delegateCapture` instead.
137
137
  5. **Capture the `delegate()` / `delegateCapture()` disposer** whenever the registration's scope is shorter than the page. Both helpers return `() => void`; the listener closure pins `rootEl`, `handler`, and everything the handler closes over (stores, signals, app state). Discarding the disposer on a transient root (modal, route view, mount swap, dynamic widget) leaks the listener AND the app graph it captures; re-mount cycles stack listeners linearly. `mount()`'s own disposer does NOT remove delegates for you. Safe to discard only when the registration is truly page-lifetime (root is `document.body` or equivalent, attached once at startup, never torn down).
138
- 6. **One `mount()` per root.** Don't nest `mount()` calls. Compose with plain functions returning JSX.
138
+ 6. **One `mount()` per root.** Don't nest `mount()` calls. Compose with plain functions returning JSX. One exception: a modal `<dialog>` of your own that anchors kerf `popover()` / `tooltip()` surfaces must contain `<div data-kerf-overlay-host data-morph-skip></div>` — kerf renders those surfaces into it (inside the modal, so they are not inert) and that slot is the one allowed nested-mount boundary. Dialogs `kerfjs/overlay` opens get a slot automatically.
139
139
  7. **Components are plain functions.** `<MyComponent props />` works — the JSX runtime calls `MyComponent(props)` and uses the returned JSX — but there's no hook system, no lifecycle, and no per-instance state. State lives in module-scope signals or stores, never in component closures.
140
140
  8. **Values bind, structure re-renders.** For a value hole, pass the signal/computed ITSELF (`<span>{count}</span>`, `class={sig}`) — kerf updates that one node directly, no render re-run. Read `.value` only when the JSX _structure_ depends on the signal — and then the read must happen INSIDE the render function to be tracked: `const x = count.value; mount(el, () => <span>{x}</span>)` does NOT re-render. One caveat on bound holes: bind a STABLE signal/computed instance per hole (`class={computed(() => …)}` that switches internally), never `class={cond ? sigA : sigB}` — switching instances can go silently stale (detectable via `enableWarnings({ staleBinding: true })` (or `KERF_DEV_WARN_STALE_BINDING=1` under Node)). The idiom's endpoint: a render that reads NO `.value` runs exactly once — a fully bound mount never re-renders. To find `.value` holes worth migrating, `enableWarnings({ valueOnlyRerender: true })` (or `KERF_DEV_WARN_VALUE_ONLY_RERENDER=1` under Node) flags re-renders whose only differences were text/attribute values.
141
141
  9. **Store actions take `(set, get)`, not `(state)`.** `set(next)` replaces state; mutating `get()` does nothing.
@@ -69,6 +69,12 @@ function wrapWithTags(child, openTag, closeTag) {
69
69
  };
70
70
  }
71
71
 
72
- export { LIST_MARKER_PREFIX, collectLists, flatten, flattenWithoutListItems, mergeChildSegments, wrapWithTags };
72
+ // src/utils/overlay-host.ts
73
+ var OVERLAY_HOST_SELECTOR = "[data-kerf-overlay-host][data-morph-skip]";
74
+ function isOverlayHost(el) {
75
+ return el.matches(OVERLAY_HOST_SELECTOR);
76
+ }
77
+
78
+ export { LIST_MARKER_PREFIX, OVERLAY_HOST_SELECTOR, collectLists, flatten, flattenWithoutListItems, isOverlayHost, mergeChildSegments, wrapWithTags };
73
79
 
74
- //# sourceMappingURL=chunk-BDX3R4OM.js.map
80
+ //# sourceMappingURL=chunk-4TX6YJDM.js.map