kerfjs 5.0.0-beta.53 → 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.
package/CHANGELOG.md CHANGED
@@ -6,6 +6,415 @@ 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.
9
418
  - `@kerfjs/ui` elevation shadows follow the color scheme, with or without the
10
419
  optional `@kerfjs/ui/webawesome.css` theme. Kerf's theme layer set
11
420
  `--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.54",
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
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/segment.ts","../src/utils/overlay-host.ts"],"names":[],"mappings":"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","file":"chunk-4TX6YJDM.js","sourcesContent":["/**\n * `Segment` — kerf's structured render output.\n *\n * The JSX runtime emits a `SafeHtml` wrapping a `Segment`. Most renders\n * produce a single static segment (just an HTML string), which behaves\n * exactly like a string for backward compatibility. When the tree\n * contains a list (`each()`) or a parent whose children include a list,\n * the runtime emits a structured segment that `mount()` can dispatch\n * on — running its native keyed reconciler for the list parts and\n * leaving the static surrounds to the general-purpose diff.\n *\n * Why have a structured form at all: the perf bottleneck for huge\n * keyed lists isn't the per-row JSX work (which `each()` already\n * memoizes). It's that flattening every render's whole tree to one\n * big HTML string forces a full `innerHTML` parse and a tree walk\n * over rows we know are unchanged. The segment shape lets mount()\n * skip both for the list parts.\n */\n\nimport type { Binding } from './bindings.js';\n\nexport type Segment = StaticSegment | ListSegment | MixedSegment;\n\nexport interface StaticSegment {\n kind: 'static';\n html: string;\n}\n\nexport interface ListItem {\n /**\n * The row's object identity. Used by the reconciler to match new items\n * against live DOM nodes across renders. Unchanged ref → reuse the\n * existing live node; replaced ref → build a fresh node.\n */\n ref: object;\n /**\n * KF-294: the row's fine-grained binding specs (signals in row attrs/text).\n * Undefined for granular-path rows (which snapshot in this spike). The\n * snapshot reconciler wires these to the fresh row node and disposes them\n * when the row is removed.\n */\n bindings?: Binding[];\n /**\n * Optional cache-invalidation key that captures external state affecting\n * this row's render (e.g. selection class). Different cacheKey on the\n * same `ref` triggers a cache miss for that row. `undefined` when the\n * user didn't pass a `key` callback to `each()`.\n */\n cacheKey: unknown;\n html: string;\n}\n\nexport interface ListSegment {\n kind: 'list';\n id: string;\n items: ListItem[];\n /**\n * Optional granular patches (KF-92). When present, the list reconciler\n * applies these directly to the existing binding instead of doing a\n * full classify+reconcile pass. Emitted by `each()` when bound to an\n * `arraySignal`. Mutually exclusive with the `items` snapshot in the\n * sense that the snapshot is treated as informational/fall-back when\n * patches are present.\n */\n patches?: ArrayPatchInternal[];\n /**\n * KF-388: the identity of the data this list renders — the `arraySignal`\n * instance, or `undefined` for a plain array.\n *\n * A list's `id` is its call-order index, so a render that changes how many\n * `each()` calls precede this one hands this segment a DIFFERENT list's\n * binding. Patches are only meaningful against the binding they were queued\n * for, so the reconciler compares this against the binding's recorded source\n * before trusting the patch queue. It is an identity check, not a value\n * check — the instance is never read.\n */\n source?: object;\n}\n\n/**\n * Internal patch shape used inside list segments. Mirrors `ArrayPatch<T>`\n * from `array-signal.ts` but typed against `object` so the segment layer\n * doesn't need to be generic. `update` / `insert` patches carry the row's\n * pre-rendered HTML — `each()` renders them at JSX-evaluation time inside a\n * try/catch so a throwing render falls back to the snapshot path (KF-99)\n * instead of leaving the signal and DOM divergent.\n */\nexport type ArrayPatchInternal =\n | {\n type: 'update';\n index: number;\n item: object;\n html: string;\n bindings?: Binding[];\n }\n | {\n type: 'insert';\n index: number;\n item: object;\n html: string;\n bindings?: Binding[];\n }\n | { type: 'remove'; index: number }\n | { type: 'move'; from: number; to: number }\n | { type: 'replace'; items: readonly object[] };\n\n/**\n * Narrowed patch aliases. Array indexing loses the union discriminant, so\n * the granular reconciler casts through these instead of restating the full\n * object type at every site — a field rename is then a one-place edit.\n */\nexport type UpdatePatch = Extract<ArrayPatchInternal, { type: 'update' }>;\nexport type InsertPatch = Extract<ArrayPatchInternal, { type: 'insert' }>;\n\nexport interface MixedSegment {\n kind: 'mixed';\n parts: Segment[];\n}\n\n/**\n * Flatten a segment to a complete HTML string. Used for first render\n * (bulk innerHTML), for SSR-style consumption via `toString()`, and\n * for diagnostics.\n *\n * If `withMarkers` is set, list segments are wrapped in\n * `<!--kf-list:{id}-->` comments so the post-parse walk can find each\n * list's live parent. Plain (non-marker) flatten is what JSX consumers\n * see when they call `.toString()` on the SafeHtml.\n */\n/**\n * Comment-marker prefix emitted before each list (`<!--kf-list:{id}-->`).\n * `mount()` consumes it when binding lists from the live DOM — the emitter\n * (here) and the consumer must agree byte-for-byte, so both import this one\n * constant. Part of the reserved marker namespace (see `bindings.ts`).\n */\nexport const LIST_MARKER_PREFIX = 'kf-list:';\n\nexport function flatten(segment: Segment, withMarkers: boolean): string {\n if (segment.kind === 'static') return segment.html;\n if (segment.kind === 'list') {\n const items = segment.items.map((i) => i.html).join('');\n return withMarkers\n ? `<!--${LIST_MARKER_PREFIX}${segment.id}-->${items}`\n : items;\n }\n return segment.parts.map((p) => flatten(p, withMarkers)).join('');\n}\n\n/**\n * Variant of `flatten` for the static-only diff path on subsequent\n * renders. Lists are reduced to a single marker comment with no items\n * inside — the actual list children stay in the live DOM and are\n * reconciled separately. Keeping list items out of this string is\n * what makes the morph cheap on huge lists where most rows are\n * unchanged.\n */\nexport function flattenWithoutListItems(segment: Segment): string {\n if (segment.kind === 'static') return segment.html;\n if (segment.kind === 'list')\n return `<!--${LIST_MARKER_PREFIX}${segment.id}-->`;\n return segment.parts.map(flattenWithoutListItems).join('');\n}\n\n/** Collect every `ListSegment` in the tree, keyed by its id. */\nexport function collectLists(\n segment: Segment,\n out: Map<string, ListSegment> = new Map(),\n): Map<string, ListSegment> {\n if (segment.kind === 'list') out.set(segment.id, segment);\n else if (segment.kind === 'mixed') {\n for (const part of segment.parts) collectLists(part, out);\n }\n return out;\n}\n\n/**\n * Combine a list of child segments into the smallest equivalent\n * representation: collapses adjacent statics into one static, returns\n * a single static if everything is static, otherwise a mixed segment\n * with statics coalesced.\n */\nexport function mergeChildSegments(parts: Segment[]): Segment {\n if (parts.length === 0) return { kind: 'static', html: '' };\n if (parts.every((p) => p.kind === 'static')) {\n return {\n kind: 'static',\n html: parts.map((p) => (p as StaticSegment).html).join(''),\n };\n }\n const merged: Segment[] = [];\n let coalesced = '';\n for (const p of parts) {\n if (p.kind === 'static') {\n coalesced += p.html;\n } else {\n if (coalesced !== '') {\n merged.push({ kind: 'static', html: coalesced });\n coalesced = '';\n }\n merged.push(p);\n }\n }\n if (coalesced !== '') merged.push({ kind: 'static', html: coalesced });\n return { kind: 'mixed', parts: merged };\n}\n\n/**\n * Wrap a child segment with surrounding open/close tags from the\n * parent JSX element. Used by the JSX runtime when constructing\n * `_jsx(tag, ...)` output.\n */\nexport function wrapWithTags(\n child: Segment,\n openTag: string,\n closeTag: string,\n): Segment {\n if (child.kind === 'static') {\n return { kind: 'static', html: openTag + child.html + closeTag };\n }\n if (child.kind === 'mixed') {\n return {\n kind: 'mixed',\n parts: [\n { kind: 'static', html: openTag },\n ...child.parts,\n { kind: 'static', html: closeTag },\n ],\n };\n }\n return {\n kind: 'mixed',\n parts: [\n { kind: 'static', html: openTag },\n child,\n { kind: 'static', html: closeTag },\n ],\n };\n}\n","/**\n * The overlay host slot — the one sanctioned nested-mount boundary\n * (KF-FBHQEP: a popover opened over a modal `<dialog>` must render INSIDE the\n * dialog to be interactive, because every engine inerts content outside the\n * modal, yet the dialog is usually already a `mount()` root).\n *\n * An element carrying both `data-kerf-overlay-host` and `data-morph-skip` is a\n * boundary between two mounts: `kerfjs/overlay` mounts its surfaces inside it,\n * and the enclosing mount leaves it alone — its morph skips the subtree\n * (`data-morph-skip`), its nested-mount check stops there, and its binding and\n * list-marker scans never descend into it, so the inner mounts' markers can\n * never be mistaken for the outer mount's own. The opt-in rebuilt-listeners\n * dev observer stops there too: a removal inside the slot is the inner\n * mount's to report.\n */\nexport const OVERLAY_HOST_SELECTOR =\n '[data-kerf-overlay-host][data-morph-skip]';\n\n/** True when `el` is an overlay host slot (see the module comment). */\nexport function isOverlayHost(el: Element): boolean {\n return el.matches(OVERLAY_HOST_SELECTOR);\n}\n"]}
@@ -1,5 +1,5 @@
1
1
  import { effect, isSignal } from './chunk-IHMDBBUJ.js';
2
- import { flatten, wrapWithTags, mergeChildSegments } from './chunk-BDX3R4OM.js';
2
+ import { OVERLAY_HOST_SELECTOR, isOverlayHost, flatten, wrapWithTags, mergeChildSegments } from './chunk-4TX6YJDM.js';
3
3
  import { devHooks } from './chunk-3C6F6NM4.js';
4
4
 
5
5
  // src/utils/syncFormProp.ts
@@ -227,7 +227,9 @@ function wireInto(scope, bindings, disposers) {
227
227
  }
228
228
  function indexAttrEls(scope, attrName) {
229
229
  const map = /* @__PURE__ */ new Map();
230
+ const hasHost = scope.querySelector(OVERLAY_HOST_SELECTOR);
230
231
  for (const el of scope.querySelectorAll(`[${attrName}]`)) {
232
+ if (hasHost && scope.contains(el.closest(OVERLAY_HOST_SELECTOR))) continue;
231
233
  for (const id of el.getAttribute(attrName).split(","))
232
234
  map.set(id, el);
233
235
  }
@@ -292,7 +294,7 @@ function collectComments(node, prefix, out) {
292
294
  const data = c.data;
293
295
  if (data.startsWith(prefix))
294
296
  out.set(data.slice(prefix.length), c);
295
- } else if (c.nodeType === Node.ELEMENT_NODE) {
297
+ } else if (c.nodeType === Node.ELEMENT_NODE && !isOverlayHost(c)) {
296
298
  collectComments(c, prefix, out);
297
299
  }
298
300
  }
@@ -577,4 +579,4 @@ function _renderAttrVerbatim(name, value) {
577
579
 
578
580
  export { Fragment, ROW_TEXT_PREFIX, SafeHtml, TEXT_MARKER_PREFIX, _renderAttrVerbatim, _setBindingContext, _toSegment, assertEmittableAttrName, bindAttr, bindMarkerAttr, boundTextNodeOf, captureRowBindings, carryOrRewireRowBindings, disposeRowBindings, escapeHtml, granularListSafeHtml, isSafeHtml, jsx, listSafeHtml, newBindingContext, raw, syncFormProp, wireBindings, wireRowBindings };
579
581
 
580
- //# sourceMappingURL=chunk-DU35YGKW.js.map
582
+ //# sourceMappingURL=chunk-UGNPDLNI.js.map