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 +409 -0
- package/ai/cursorrules +2 -2
- package/ai/manifest.json +9 -7
- package/ai/skill.md +2 -2
- package/dist/{chunk-BDX3R4OM.js → chunk-4TX6YJDM.js} +8 -2
- package/dist/chunk-4TX6YJDM.js.map +1 -0
- package/dist/{chunk-DU35YGKW.js → chunk-UGNPDLNI.js} +5 -3
- package/dist/chunk-UGNPDLNI.js.map +1 -0
- package/dist/{chunk-HTXQR2KL.js → chunk-WMRFXSLG.js} +7 -5
- package/dist/chunk-WMRFXSLG.js.map +1 -0
- package/dist/dev.js +9 -6
- package/dist/dev.js.map +1 -1
- package/dist/html.js +2 -2
- package/dist/index.js +2 -2
- package/dist/jsx-runtime.js +1 -1
- package/dist/list.js +1 -1
- package/dist/overlay.d.ts +12 -3
- package/dist/overlay.js +85 -20
- package/dist/overlay.js.map +1 -1
- package/dist/remount.js +1 -1
- package/dist/router.js +1 -1
- package/dist/scope.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-BDX3R4OM.js.map +0 -1
- package/dist/chunk-DU35YGKW.js.map +0 -1
- package/dist/chunk-HTXQR2KL.js.map +0 -1
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.
|
|
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.
|
|
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.
|
|
10
|
-
"sha256": "
|
|
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.
|
|
50
|
-
"sha256": "
|
|
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.
|
|
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
|
-
|
|
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-
|
|
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-
|
|
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-
|
|
582
|
+
//# sourceMappingURL=chunk-UGNPDLNI.js.map
|