kerfjs 5.0.0-beta.10 → 5.0.0-beta.101
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 +2826 -107
- package/README.md +96 -37
- package/ai/cursorrules +110 -61
- package/ai/manifest.json +15 -7
- package/ai/skill.md +117 -72
- package/dist/actions.d.ts +1 -1
- package/dist/actions.js +10 -6
- package/dist/actions.js.map +1 -1
- package/dist/array-signal.js +1 -1
- package/dist/async.js +24 -21
- package/dist/async.js.map +1 -1
- package/dist/attach.js +1 -66
- package/dist/attach.js.map +1 -1
- package/dist/{chunk-HW7KSM2Y.js → chunk-3C6F6NM4.js} +1 -1
- package/dist/{chunk-HW7KSM2Y.js.map → chunk-3C6F6NM4.js.map} +1 -1
- package/dist/{chunk-GY4XV2UV.js → chunk-4TX6YJDM.js} +10 -3
- package/dist/chunk-4TX6YJDM.js.map +1 -0
- package/dist/{chunk-QIP723L4.js → chunk-5WRGJZV6.js} +3 -2
- package/dist/chunk-5WRGJZV6.js.map +1 -0
- package/dist/{chunk-V46JKE44.js → chunk-6LXUC4ZY.js} +29 -2
- package/dist/chunk-6LXUC4ZY.js.map +1 -0
- package/dist/{chunk-KZJXHFIB.js → chunk-BI4HGICV.js} +3 -3
- package/dist/chunk-BI4HGICV.js.map +1 -0
- package/dist/chunk-FM6N2B4N.js +70 -0
- package/dist/chunk-FM6N2B4N.js.map +1 -0
- package/dist/{chunk-LVH3GC6B.js → chunk-H7WTRM6F.js} +3 -3
- package/dist/chunk-H7WTRM6F.js.map +1 -0
- package/dist/{chunk-U6FK33SG.js → chunk-IHMDBBUJ.js} +2 -2
- package/dist/chunk-IHMDBBUJ.js.map +1 -0
- package/dist/chunk-JN7EJM3B.js +86 -0
- package/dist/chunk-JN7EJM3B.js.map +1 -0
- package/dist/{chunk-UZJ6I4T6.js → chunk-MK42GLPV.js} +1 -1
- package/dist/chunk-MK42GLPV.js.map +1 -0
- package/dist/{chunk-SVATPF5R.js → chunk-NVNTUHAA.js} +560 -176
- package/dist/chunk-NVNTUHAA.js.map +1 -0
- package/dist/{chunk-ZDCJZCNO.js → chunk-VX3RCRGG.js} +161 -90
- package/dist/chunk-VX3RCRGG.js.map +1 -0
- package/dist/{delegate-CL9VTZFb.d.ts → delegate--CrfgYtC.d.ts} +0 -34
- package/dist/dev.d.ts +31 -13
- package/dist/dev.js +101 -35
- package/dist/dev.js.map +1 -1
- package/dist/html.js +20 -8
- package/dist/html.js.map +1 -1
- package/dist/index.d.ts +14 -9
- package/dist/index.js +29 -20
- package/dist/index.js.map +1 -1
- package/dist/jsx-runtime.d.ts +17 -7
- package/dist/jsx-runtime.js +1 -1
- package/dist/list.d.ts +7 -3
- package/dist/list.js +153 -46
- package/dist/list.js.map +1 -1
- package/dist/overlay.d.ts +272 -133
- package/dist/overlay.js +1011 -368
- package/dist/overlay.js.map +1 -1
- package/dist/remount.js +2 -2
- package/dist/router.d.ts +13 -4
- package/dist/router.js +37 -10
- package/dist/router.js.map +1 -1
- package/dist/scope.d.ts +35 -8
- package/dist/scope.js +61 -30
- package/dist/scope.js.map +1 -1
- package/dist/testing.js +1 -1
- package/dist/timing.d.ts +3 -2
- package/dist/timing.js +1 -1
- package/dist/timing.js.map +1 -1
- package/llms.txt +13 -6
- package/package.json +35 -8
- package/setup/cli.mjs +90 -0
- package/setup/index.d.mts +37 -0
- package/setup/index.mjs +1347 -0
- package/setup/jsonc.mjs +201 -0
- package/setup/state.schema.json +56 -0
- package/dist/chunk-GY4XV2UV.js.map +0 -1
- package/dist/chunk-KPXIOG2C.js +0 -54
- package/dist/chunk-KPXIOG2C.js.map +0 -1
- package/dist/chunk-KZJXHFIB.js.map +0 -1
- package/dist/chunk-LVH3GC6B.js.map +0 -1
- package/dist/chunk-QIP723L4.js.map +0 -1
- package/dist/chunk-SVATPF5R.js.map +0 -1
- package/dist/chunk-U6FK33SG.js.map +0 -1
- package/dist/chunk-UZJ6I4T6.js.map +0 -1
- package/dist/chunk-V46JKE44.js.map +0 -1
- package/dist/chunk-ZDCJZCNO.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,2804 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **`@kerfjs/ui/overlay-sheet.css`.** An opt-in stylesheet for core `kerfjs/overlay` dialogs presented with `presentation: 'bottom-sheet'`, matching `DialogSurface`'s sheet:
|
|
12
|
+
- the bottom edge, full width up to 560px and centered beyond;
|
|
13
|
+
- square bottom corners under the dialog radius;
|
|
14
|
+
- the 8px surface inset, padded by the larger of the safe area and the core's `--kerf-keyboard-inset`;
|
|
15
|
+
- a cap below the top safe area;
|
|
16
|
+
- a slide up as it opens.
|
|
17
|
+
|
|
18
|
+
The `kui-overlay-sheet-exit` class (passed as `exitClass`) slides it back down as it closes. It covers the plain and `native: true` hosts and honors reduced motion, the static-effects switch, and forced colors.
|
|
19
|
+
|
|
20
|
+
- **Resizable core bottom sheets in `@kerfjs/ui`.** `wireOverlaySheets()` (from `@kerfjs/ui/wire-overlay-sheets`) gives a core `kerfjs/overlay` bottom sheet with the `kui-overlay-sheet-resizable` class the grabber of a resizable `DialogSurface` sheet. It resizes the visible height by drag or keyboard (arrows, Page Up/Down, Home, End), between `--kui-overlay-sheet-min-height` (160px) and the cap, and reports the height in its ARIA values. The grabber sticks to the top edge while the content scrolls and stays first across re-renders. Call it once for each container sheets open in. Covered in Chromium, Firefox, and WebKit.
|
|
21
|
+
|
|
22
|
+
- **Tint accents.** A `@kerfjs/ui` `tint` takes an optional `accent`: `{ color, scheme, accent }`. Inside the scope, the brand ramp re-derives from it: loud fills and their on-loud text, brand text and links, text on brand fills, and the focus ring. Lightness is clamped so every pair keeps WCAG AA for any accent; an accent seed sweep checks this in Chromium, Firefox, and WebKit.
|
|
23
|
+
- **`npm run dev:lan` for device checks.** From the repo root or `ui/`, it serves the `@kerfjs/ui` development catalog on every network interface. It prints the LAN and Tailscale URLs and accepts Tailscale MagicDNS (`*.ts.net`) and mDNS (`*.local`) host names, so a phone can test what a desktop browser cannot: the virtual keyboard, safe areas, and touch. Plain `npm run dev` stays on `127.0.0.1`. The catalog page now sets `viewport-fit=cover`, so a notched phone reports its real safe-area insets. The device checks are listed in `ui/docs/manual-test-plan.md`.
|
|
24
|
+
- **Exit motion for `kerfjs/overlay` dialogs.** `overlay()`, `confirm()`, `prompt()`, `form()`, and `choice()` take `exitClass` / `exitDuration`, named and defaulted like `toast()`'s, so CSS can animate an overlay out (for example, slide a bottom sheet down). On `close()` everything else still happens immediately: the result settles, the overlay leaves the stack (an overlay opened during the exit is topmost), a `native` dialog leaves the top layer, the wrapper turns `inert`, and focus returns. Only the node's removal waits, until the exit's CSS animations finish or `exitDuration` passes, whichever is first. `prefers-reduced-motion: reduce` skips the exit. `OverlayHandle.close(result, { instant: true })` (new `OverlayCloseOptions` type) skips the exit or ends one in progress. A second `close()` is still a no-op. Without the new options, behavior is unchanged.
|
|
25
|
+
- **Exit motion for `popover()` and `tooltip()`.** Both take `exitClass` / `exitDuration` and use the same exit as `overlay()`. A popover plays it on `close()` and on an outside-click dismissal, and a tooltip plays it when it hides after `hideDelay`. Teardown when the anchor leaves the document stays instant, and so does the tooltip disposer, which also ends an exit in progress. A tooltip shown again while the previous one is still exiting replaces the leaving node at once, so an anchor never has two tooltips. A popover or tooltip anchored inside an overlay now closes at that overlay's `close()`, instead of lingering until an exiting overlay's node is removed.
|
|
26
|
+
- **Bottom-sheet presentation for `kerfjs/overlay` dialogs.** `overlay()`, `confirm()`, `prompt()`, `form()`, and `choice()` take `presentation: 'bottom-sheet'` (the new `OverlayPresentation` type, named after `DialogSurface`'s prop). The wrapper gets `data-presentation="bottom-sheet"` on both the plain and `native` hosts. While the sheet is open, kerf follows `visualViewport` and publishes the virtual keyboard's overlap as `--kerf-keyboard-inset` on the wrapper. The value is cleared when the keyboard hides, while pinch-zoomed, and on close. kerf still ships no CSS: rest the sheet on the bottom edge and add `max(env(safe-area-inset-bottom), var(--kerf-keyboard-inset, 0px))` to its bottom padding (the API reference has the recipe). The default `'modal'` presentation is unchanged. Covered in Chromium, Firefox, and WebKit.
|
|
27
|
+
- **Media Detail recipe for `@kerfjs/ui`.** `?component=recipe-media-detail` composes an Apple-Music-style album page from components only: `NavStack` clear chrome with the scroll-edge material, a scoped `tint` from the artwork's bottom color, an `underChrome` `BackdropImage` with parallax and progressive blur fading into the tinted surface, and glass control groups. Under clear chrome, a `NavStack`'s own back control now sits in a glass control group, so it stays legible over imagery. The recipe has end-to-end coverage in Chromium, Firefox, and WebKit.
|
|
28
|
+
- **`BackdropImage` runs beneath clear chrome.** `underChrome` pulls the image up beneath the Pane's block-start inset: an overlay or clear header's clearance, NavStack chrome handed to a view's Pane, and the top safe area. Artwork fills the screen behind transparent chrome, as on a media detail page. The visible image keeps its `height` below the chrome and its `overlay` stays clear of it. `Pane` now publishes that inset as `--kui-pane-content-inset-block-start`, beside the inline insets.
|
|
29
|
+
- **Clear chrome for `@kerfjs/ui` layouts.** `Pane` `headerPresentation`, `NavStack` `chromePresentation` and `bottomToolbarPresentation`, and `TabNavigator` `barPresentation` take `clear`: an overlay bar with the same measuring and scroll clearance as `overlay`, but fully transparent, for chrome over imagery whose controls sit in glass `ToolbarControlGroup`s. A clear bar stays clear while content scrolls beneath it. `scrollEdge: 'material' | 'surface'` (on `Pane`, also in `PaneConfig`, and on the two layouts) fades the chrome material or the opaque surface in gradually instead, the iOS scroll-edge effect: fully clear at rest, fully in once content has scrolled `scrollEdgeDistance` pixels (default 60) beneath the bar, and only the backing's opacity changes. `wireScrollDividers` publishes the progress from the scroll it already tracks, so it works in every engine. Clear bars draw no scroll divider, turn opaque under reduced transparency or increased contrast, paint `Canvas` in forced colors, and snap on the scroll state instead of tracking it under the static-effects switch or on a slow-updating display. A `Toolbar`'s leading zone no longer clips the shadow of a glass control group.
|
|
30
|
+
- **Scoped tints for `@kerfjs/ui` layout surfaces.** `Pane` (and `PaneConfig`), `NavStack`, `TabNavigator`, and `TabNavigatorTab` take `tint: { color, scheme }`: an app color, typed with `@kerfjs/ui/css-values`, and the `'light'` or `'dark'` scheme it belongs to. Kerf does no color detection. The region paints the tinted surface, and its surface, raised surface, text, quiet text, link and brand text, quiet fills, chrome material, and popup rows re-derive from the color through `color-mix()` and relative color syntax. Lightness is clamped so text keeps WCAG AA over any color: a seed sweep across hue and lightness holds every text token at 4.5:1 or better in Chromium, Firefox, and WebKit, with and without the Web Awesome theme. The scope sets its `color-scheme` and Web Awesome scheme class, so Web Awesome components inside re-derive too. Under `prefers-contrast: more`, or without relative color syntax, it keeps the plain palette in the declared scheme.
|
|
31
|
+
- `@kerfjs/ui` `DialogSurface` gains `presentation="bottom-sheet"`: a dialog attached to the bottom edge of the screen, with square bottom corners, the top corners on the dialog radius, and a panel that runs behind the home indicator and the virtual keyboard while its content stays above them (the larger of the bottom safe area, the keyboard overlap measured from `visualViewport`, and `env(keyboard-inset-height)`). `height`, `maxHeight`, and the new `minHeight` size its visible height. The new `resizable` prop adds a top grabber, a focusable "Resize sheet" separator you drag or move with the arrow, Page Up/Down, Home, and End keys between `minHeight` and the viewport cap. The sheet slides up when it opens and down when it is dismissed; `enterMotion` / `exitMotion` (`"slide"` or `"standard"`) choose per transition, and reduced motion keeps Web Awesome's own animation. The centered `modal` presentation remains the default. The grabber, keyboard clearance, and slide need `@kerfjs/ui/surface-scaffold/register`.
|
|
32
|
+
|
|
33
|
+
### Fixed
|
|
34
|
+
|
|
35
|
+
- A `kerfjs/overlay` `initialFocus` that targets a custom element that cannot take focus yet no longer aborts the open. Before, a Web Awesome `wa-button` whose `focus()` throws before it has rendered made `overlay()` (and `confirm()`, `prompt()`, `form()`, `choice()`) roll back and rethrow. Now focus holds on the overlay itself, then moves to the element once it is defined and rendered, unless the overlay has closed or the user has moved focus in the meantime. The same applies to `initialFocus: true` when the first tab stop is such an element. Covered in Chromium, Firefox, and WebKit.
|
|
36
|
+
- `wireNavStack` keeps fallback focus on a pushed view with nothing focusable (an inert loading placeholder, say) across re-renders. The temporary `tabindex="-1"` it adds to the view was morphed away by the next render, which made the view unfocusable and dropped focus to the page body. It is now restored while the view holds that focus, and focus moves to the view's first focus target once content with one arrives under the same key. Focus the person moved elsewhere is left alone.
|
|
37
|
+
- Clearing a collapsible `TokenSearchField` with its ✕ button while focus is outside the field now collapses the emptied field and leaves focus where it was. Previously the clear re-expanded the field and claimed the caret, so it stayed open until it was focused and blurred again. A clear started from inside the field still returns the caret to the editor; that is now decided by where focus was when ✕ was pressed, which also covers WebKit, where the press drops focus to the page before the click.
|
|
38
|
+
- A `DialogSurface` bottom sheet with no `height` now opens at its content height, as documented. A modal dialog sits between both block insets, so the sheet's auto height stretched to its cap. The catalog's resizable sheet now relies on content height: its fixed height sat a few pixels short of the form and clipped the textarea's bottom border on a phone.
|
|
39
|
+
- A title over a `BackdropImage` `scrim` is now light in both color schemes. The scrim is dark in both schemes, so the overlay now takes the dark color scheme and default-tone titles and controls resolve their light foregrounds. `ToolbarText tone="dark"` is unchanged: it matches dark control groups on a loud-filled toolbar and inverts with the scheme, so the guidance no longer recommends it for photo-backed toolbars. Under reduced transparency or increased contrast, where the overlay sits on a solid `fadeTo` band, the overlay keeps the page's scheme.
|
|
40
|
+
- `kerf-ui-doctor --full` and `kerf-ui-analyze` no longer report findings in gitignored, generated output. A local Playwright run's `playwright-report/` (its trace viewer ships CSS) produced about a hundred `KUI-L006` review findings in one app, so running the tests changed the doctor result. Inside a Git work tree, full traversal now skips every path the project's ignore rules ignore, across TypeScript, the isolated ESLint pass, analyzer discovery, and cache inputs; Playwright's `playwright-report`, `test-results`, and `blob-report` directories are also skipped by name, which covers projects outside Git.
|
|
41
|
+
- A dark-tone `AnchoredSurface` (`tone: 'dark'`) now follows a live color-scheme switch while it is open. It picked its inverse scheme once, on open, so an OS preference change or an app theme toggled on an ancestor left the popup on the old side until it was reopened. It now flips with the page. `openAnchoredSurfaceAt` also reads the dark tone's scheme from `point.context`, the element under the pointer, instead of from the body or dialog that holds its temporary anchor, so a region with its own forced `color-scheme` is honored.
|
|
42
|
+
- A keyboard move made as a long `PopupMenu` submenu (or menu) opens now leaves the chosen item in view, not just focused. When Web Awesome's late first-item focus landed after the move (say `End` pressed right after `ArrowRight`), it scrolled the menu back to the top, and kerf's restore refocused the chosen item without scrolling, so the focused last choice sat out of view below the clip. The restore now scrolls the item into view within its own menu, leaving the page alone.
|
|
43
|
+
- A default-tone `ToolbarControlGroup` in an overlay `Pane` header (`headerPresentation="overlay"`) or in a `DialogSurface` floating header's `header-actions` (`headerPresentation="floating"`) now keeps its controls at WCAG AA over content passing beneath the translucent chrome material. Its quiet control color over its own translucent fill measured about 3.0:1 (light) and 3.5:1 (dark) over the worst case; its controls now take the normal text color there, as they already did in overlay `NavStack` chrome.
|
|
44
|
+
- A dark `ToolbarControlGroup` (`tone="dark"`) holding one control no longer hides its icon on hover over a light page. A lone control's hover replaces the whole group's fill, and the dark tone's hover was a translucent surface wash meant to lie over that fill, so the white icon sat on a near-white circle (about 1.3:1). The wash now paints over the group's loud fill, as it does on one control of a multi-control dark group, and it is 14% instead of 28%: at 28% even that layered paint measured about 2.7:1. Hovered dark controls now keep about 3.4:1 over a light page and about 5.8:1 in dark.
|
|
45
|
+
- An unframed scrollable `List` now stops a row focused or scrolled into view with its whole focus ring inside the scroll clip. Only a framed List carried ring-room scroll padding, so in an unframed one the ring of a row brought into view ended flush against the clip and was cut off. Every `scrollable` List now has 6px of block scroll padding (the ring's 4px reach plus 2px of slack), which moves nothing at rest. An unframed List's first and last rows still meet the clip at the scroll extremes; `frame`'s 4px inset is the answer when they must show a whole ring.
|
|
46
|
+
- An overlay `Pane` header (`headerPresentation="overlay"`) now clears its forced-colors edge. In forced colors the header gains a 1px `CanvasText` bottom border, but `wireScrollDividers` observed the header's content box, so the border alone reported no resize and `--kui-scroll-clearance-block-start` stayed 1px short: the content's first item rested under the edge. The wiring now observes each overlay header's border box, the same box it publishes.
|
|
47
|
+
- `positionAnchored({ fitHeight: true })` (and so `autoReposition()`, `popover()`, `tooltip()`, and `@kerfjs/ui`'s `openAnchoredSurface()`) now keeps an element placed above its anchor growing upward. It set `top` from the height the element had at the reposition, so an element still growing afterwards — say an expand-from-0 `max-height` entrance placed above mid-expand — slid its bottom edge down over the anchor until the next scroll or resize. An above placement now pins the element's bottom edge at the gap above the anchor (`bottom`, with `top: auto`), so it grows and shrinks upward, away from the anchor, whatever height it goes on to lay out; a below placement sets `top` with `bottom: auto`. Placement without `fitHeight` is unchanged (`KF-MZ03RG`).
|
|
48
|
+
- An overlay `Pane` header (`headerPresentation="overlay"`) no longer covers the top of a classic, space-taking scrollbar (Windows and Linux, or macOS with "always show scrollbars"). Its thumb and arrow buttons ran beneath the translucent header. `wireScrollDividers` now also measures the content's vertical scrollbar and publishes a classic one's inline size on the Pane as `--kui-pane-scrollbar-inline-size`, and the header ends where the scrollbar's track begins. Overlay scrollbars take no space and still pass beneath the header like the content.
|
|
49
|
+
- A `ToolbarText` in an overlay `Pane` header (`headerPresentation="overlay"`) now keeps WCAG AA over the translucent chrome material. Over worst-case content passing beneath, its quiet `description` measured about 3.2:1 (light) and 4.1:1 (dark), and a default-size title about 3.8:1 (light). In that header an untoned `ToolbarText` now takes the normal text color for its title at every size and mixes its description halfway toward it (about 5.9:1 in both themes), so the description stays readable and still reads as secondary.
|
|
50
|
+
- A framed `List` (`frame`) now keeps a focused middle row's ring inside its scroll clip in Chromium. The frame's scroll padding equaled the ring's 4px reach, but Chromium's focus scroll stops 1px short of the padded target and a row's fractional position can cost another fraction of a pixel, so about 1px of the ring was cut. The scroll padding is now the ring reach plus 2px; the frame's 4px visual inset and its first- and last-row resting positions are unchanged.
|
|
51
|
+
- `positionAnchored({ fitHeight: true })` now measures an element whose `max-height` is mid-animation at the cap it is headed to. An app transition already running on `max-height` (say an expand-from-0 entrance to `max-height: 300px`) or a `@keyframes` animation of `max-height` made the cap read report the current frame, so a reposition mid-expand judged the element by a height it was about to outgrow and could open it on a side its settled cap does not fit. kerf now reads the cap from the running effect instead — its last `max-height` keyframe, or its first while the current iteration plays in reverse — and, for that case, never touches the element's style, so the app's animation keeps running uninterrupted. A finished animation that still fills is read from computed style as before, and a keyframe `var()` kerf cannot resolve outside the element counts as no cap (`KF-YW3PRD`).
|
|
52
|
+
- `eslint-plugin-kerfjs`'s `ui-css-values` rule now recognizes CSS-value helpers imported from any `@kerfjs/ui` subpath, including the documented `@kerfjs/ui/css-values`. It only knew helpers imported from the package root, so `iconTile={{ color: foregroundColor('#000') }}` with `foregroundColor` from `@kerfjs/ui/css-values` passed lint while `kerf-ui-analyze` reported `KUI-L014` (and likewise `KUI-L015` for a bare `plus()`). Lint now resolves a helper exactly as the analyzer does: by its imported name, from the component's package or any subpath of it, through a named, aliased, or namespace import. A same-named helper from another package is still ignored (`KF-HKGK2C`).
|
|
53
|
+
- `positionAnchored({ fitHeight: true })` now honors a relative `max-height` cap and is no longer fooled by a `max-height` transition. A cap that stays relative after computing — `max-height: 60%`, `calc(50% - 2rem)`, `min(300px, 60%)`, `clamp()` — was treated as no cap, so an element that fit the preferred side at that cap still opened on the roomier side. kerf now resolves it against the element's fixed-position containing block (normally the viewport) with an unpainted probe beside the element, so the element's scroll position and layout are untouched. And an app `transition` on `max-height` made the cap read report the transition's running value (the room kerf last published) and fire `transitionrun` / `transitionstart` events, so the cap was ignored again; kerf now holds off only the `max-height` transition for that read, and every other transition keeps running (`KF-53GEGN`).
|
|
54
|
+
- A `StateBanner`'s side action no longer squeezes or covers its copy on a narrow banner. The banner laid out as a three-track grid whose action track took the action's full width, so on a phone a pair of buttons (say Retry Save and Discard Changes in a wrapping `Row`) left the title about 65px, and a word wider than that ran underneath the buttons. The banner is now a wrapping row: the default `actionPlacement="trailing"` keeps the action beside the copy while the copy has at least 12rem, and otherwise moves it to its own end-aligned trailing row. The choice follows the banner's own width rather than a breakpoint, so it also works in a narrow sidebar on a wide screen and inside an intrinsically sized `CornerStatus`. `actionPlacement="below"` still always uses the trailing row (`KF-PXCKVZ`).
|
|
55
|
+
- `positionAnchored({ fitHeight: true })` (and so `autoReposition()`, `popover()`, `tooltip()`, and `@kerfjs/ui`'s `openAnchoredSurface()`) now honors the element's own `max-height` when choosing a side. It measured a scrolling element at its full content height, so an element the app caps below that height — say `max-height: min(300px, var(--kerf-anchor-available-height))` with 500px of content — fit neither side and opened on the roomier one, even when its 300px capped height fit the preferred side. kerf now reads that cap with its own published room swapped for an unbounded one, so it never mistakes its last cap for the app's, and the read is style-only, so the element's scroll position is kept (`KF-878W6T`).
|
|
56
|
+
- A keyboard move made just as a `PopupMenu` submenu finishes opening is no longer undone. Web Awesome hands focus to the submenu's first item from a timer it queues when the show animation settles, and a browser may dispatch a keypress before that timer runs. So pressing `End` (or an arrow key) at that moment focused the chosen item, and the late handoff then snapped focus back to the first item. PopupMenu now keeps the keyboard or pointer choice through that handoff; a deliberate `Home` to the first item still stands (`KF-XMDR2Q`).
|
|
57
|
+
- An open `PopupMenu` is now announced with a name. Web Awesome renders the `role="menu"` panel in a shadow root, and the trigger's name never reached it: a `context` menu's `label` sat only on its `aria-hidden` anchor, and no ID reference can cross into or out of the shadow roots involved. So a screen reader announced an unnamed menu, and `getByRole('menu', { name })` found nothing. The panel now takes the trigger's name (the `context` or icon-only `label`, or the visible `text`), and each submenu is named by its parent item's label (`KF-AB4ZVD`).
|
|
58
|
+
- A `TabBar` with `snapTabs` no longer grows its tab strip past its allocation. The snap mode reserves end scroll room so the last tab can settle at the strip's start, and it reserved that room as end padding on the strip. A flex item can't shrink below its own padding, so room measured at one width held the strip open after the bar narrowed. This happened, for example, when a drawer opened or lost space to a neighboring panel. The strip then pushed an adjacent `trailing` action and the `end` zone out of the bar, where they could not be clicked; a twelve-tab drawer strip measured 1584px against a 1088px edge. The room is now scrollable content after the last tab, inside the strip. It lengthens only the scroll range, so the strip shrinks to its allocation and the last tab can still snap to the start. Writing the room no longer resizes the strip, so `wireTabBars` now re-measures it on the next frame until the value holds. This replaces the follow-up measurement that a resize used to trigger (`KF-3Q98FJ`).
|
|
59
|
+
- In Safari, closing a `Select` inside a scrolling dialog body no longer leaves blank scroll space below the content. WebKit kept the closed listbox's last box in the body's scrollable overflow, so the body scrolled into an empty band the height of the listbox. The Select now relayouts its popup once after it closes, which keeps focus and the keyboard on the Select (`KF-C3523R`).
|
|
60
|
+
- In a divided list, `ListItem`, `ListItemLink`, and `ListActionRow` corners now follow the row's position. A corner is flat wherever a divider line meets it, whether the row draws that line or its neighbor does, so with lines between rows the first row rounds only its top, middle rows are flat, and the last row rounds only its bottom. Previously each row squared only the edges it drew itself, so in a `divider="after"` list every hovered or selected row was rounded on top and flat on the bottom, including the last. A single divided row is unchanged (`KF-AQN4H7`).
|
|
61
|
+
|
|
62
|
+
- A collapsible `TokenSearchField` in a `ToolbarControlGroup` with `content="search"` and `sizing="grow"` now opens and closes as one surface. The group snapped straight to its grown width (its `width: auto` cannot transition) while the field animated its own width inside it, so for about 250ms the pill showed an empty band and the query wrapped onto several lines. The group now animates its flex size, and the field fills it on every frame, laid out at least basis-wide while the group clips it. In a toolbar 480px wide or narrower, where the grown group takes its own row, and under reduced motion, both open in one frame (`KF-50K0JM`). A plain collapsible search group (no `sizing="grow"`) had the same defect in a milder form: its field ran a second width transition that lagged the group's by up to 90px, and the query wrapped onto as many as 19 lines for about 100ms. The group now owns that motion too. The field follows it on every frame and keeps the query on one line until the opening ends, then wraps normally again (`KF-CXQB30`).
|
|
63
|
+
|
|
64
|
+
- A tooltip or popover outside a Web Awesome `wa-dialog` / `wa-drawer` is no longer painted beneath it. Those elements keep their modal `<dialog>` in a shadow root, which kerf's modal checks never saw: a surface opened while one was open was not lifted into the top layer, and surfaces already showing were not re-hosted above one that opened later (its `toggle` event stays inside the shadow root). kerf now also checks the open shadow roots when a surface opens, and watches them for `toggle` while any surface is open; a surface hosted inside the dialog stays where it is. A closed shadow root remains invisible (`KF-1FGTX1`).
|
|
65
|
+
|
|
66
|
+
- A `ToolbarControlGroup` now dims a native button marked `aria-disabled="true"` (one kept focusable while unavailable) exactly as it dims a `disabled` one: half opacity, a `not-allowed` cursor, and no hover surface, including a lone group's whole-group hover. It used to look and hover like an enabled control. A disabled raw `wa-button` in a group also no longer paints the group's hover surface (`KF-Y3ZZ10`).
|
|
67
|
+
|
|
68
|
+
- A focused `scrollable` `List` now draws the theme focus ring inset by the ring width and rounded with the standard control radius. Chromium makes a scroller without focusable rows keyboard-focusable, and a dialog may focus it first. It used to show the UA's square outline outside its box, which a rounded or `overflow: hidden` parent clipped (`KF-AJNFTT`).
|
|
69
|
+
|
|
70
|
+
- Typing quickly into a `TokenSearchField` whose app commits typed grammar to chips from `onEdit` (or removes a chip from `keyboard.onRemoveToken`) no longer loses keys. The token change re-keys the editor, so the controlled render replaced the focused element, and keys typed before the app refocused it went nowhere: `site:north site:annex` became `[site:north] ite:annex`. `wireTokenSearchFields` now hands focus and caret to the replacement at the same mutation checkpoint, as it already did for Clear and native deletion. A caret at the end stays at the end, and any other caret keeps its place in the remaining text (`KF-96TEF4`).
|
|
71
|
+
|
|
72
|
+
- `DialogSurface` no longer names a `wa-dialog`'s native panel when the host is itself an explicit, named dialog (`role="dialog"` or `"alertdialog"`). Both used to carry the same name, so the accessibility tree had two dialogs with one name and role queries such as Playwright's `getByRole('dialog', { name })` failed in strict mode. Removing or changing the host role restores the panel name (`KF-A0NF2S`).
|
|
73
|
+
|
|
74
|
+
- A nested `PopupMenu` submenu's pointer "safe triangle" now spans the gap to the placed submenu. Web Awesome measured the submenu for that triangle before its asynchronous placement landed, so the triangle kept the submenu's pre-placement edge (often the viewport origin): its fixed, viewport-sized hit region fanned away from the submenu and could cover the parent menu's rows, swallowing their hover and clicks. PopupMenu now re-measures once the placement lands and again when the submenu's show animation settles, so applications need no `::part(submenu)::before` override (`KF-YAHAZK`).
|
|
75
|
+
|
|
76
|
+
- A `scrollable` `List` bounded by a `max-height` ancestor now scrolls its rows instead of compressing them. Its children kept the default `flex-shrink: 1`, and a `ListItem` clips its own overflow, so multiline rows shrank into one another and their descriptions overlapped the next row. A scrollable List's direct children no longer shrink, and `docs/layout.md` documents bounding one with `flex` inside a max-height flex column (`KF-93GZC1`).
|
|
77
|
+
|
|
78
|
+
- A `popover()`, `tooltip()`, or `openAnchoredSurface()` opened from inside a Web Awesome `wa-dialog` or `wa-drawer` is now interactive. Those elements keep their modal `<dialog>` in a shadow root, so kerf found no dialog above the slotted trigger and appended the surface to `document.body`, where the modal made it inert: it painted above the dialog, but its controls took no focus or input. kerf now resolves the modal context across shadow boundaries: a shadow host whose slot renders the trigger inside an open modal `<dialog>` counts as that dialog, and kerf creates the overlay host slot in the host's light DOM, so the surface is slotted inside the dialog. A surface inside a modal context, through its host slot or an explicit `container`, now also closes when that dialog closes. Escape still closes only the surface. `openAnchoredSurface()` accepts a `container` for any other modal context, and `openAnchoredSurfaceAt()`'s `context` resolves a `wa-dialog` / `wa-drawer` too (`KF-F52T5D`).
|
|
79
|
+
|
|
80
|
+
- Re-renders of a large page whose rows come from a memoized `each()` now scale with the change instead of the row count. The change-scoped morph now also runs on renders that contain `each()` lists: a change beside the list, such as a details pane that follows the selection, is re-parsed and morphed without visiting the rows, and kerf falls back to the full diff when the change sits in the element that holds the rows. After such a render, the scan for list markers skips the bound rows. `SafeHtml` also builds its HTML string on first read instead of at construction, so each element above a list no longer copies every row's markup. A one-row selection on a 300-row memoized keyed page drops from about 3.7 ms to 0.3 ms per render in Chromium (about 29 ms to 3 ms in happy-dom). An attribute set imperatively on an element outside the changed markup now survives such a render, as it already did on list-free pages (`KF-03KWNV`).
|
|
81
|
+
|
|
82
|
+
- On the same kind of page, a mount that also has a fine-grained binding outside the list (for example `class={signal}` on a page wrapper) no longer searches every row for binding markers after a render that changed the page. Rows carry only their own row-scoped markers, so kerf now skips them. A selection flip on the 300-row keyed fixture with one such binding drops from about 3.7 ms to 0.4 ms in Chromium (about 66 ms to 6 ms in happy-dom) (`KF-5S9XPH`).
|
|
83
|
+
|
|
84
|
+
- `wireTabBars` no longer re-measures every tab strip after every DOM mutation in the root it watches. It used to read computed style and layout for each strip whenever anything under the root changed, such as terminal output or a list update. In Hot Sheet's terminal profile that work was the single hottest function. Now only the bars a mutation touches are re-measured: a change inside a bar, a newly inserted bar, or a `dir` change on an element above one. A strip with a pinned tab also re-measures when its own size changes (`KF-8H2MH2`).
|
|
85
|
+
|
|
86
|
+
- `resource().run()` can be called inside an `effect()`. `run()` read the resource's own state through a tracking read, so an effect that re-ran a resource from an input signal subscribed to that state, wrote it, and threw `Cycle detected`. `run()` and its progress and failure paths now read the state untracked; `resource.value` stays a tracking read (`KF-H5PNHK`).
|
|
87
|
+
|
|
88
|
+
- Re-renders of a large list-free page now scale with the change instead of the page. When a `mount()` render has no `each()` list and no fine-grained bindings, kerf compares the previous and next static HTML, finds the innermost element around the change whose markup parses on its own, and re-parses and morphs only that element's changed children; anything it cannot prove identical to a full diff still takes the full diff. A one-row selection on a 300-row, ~1.4 MB page drops from about 23 ms to 13.5 ms per render in Chromium. One documented behavior narrows: an attribute set imperatively on an element whose markup did not change now survives a render that changed something else, as it already did on a no-op render. The core grows by about 2 KB min+gzip (~13 KB to ~15 KB) (`KF-HA8R27`).
|
|
89
|
+
|
|
90
|
+
- The morph no longer strips the defaults a custom element reflects onto its host. Web Awesome reflects `variant`, `appearance`, `size`, `attention`, and similar defaults onto `wa-button`, `wa-tag`, `wa-badge`, `wa-textarea`, and others; a template that omitted them lost them on every re-render that reached the element, and Lit put them straight back with a full update (one row selection on a 300-row page flipped 1,200 attributes). On custom elements the morph now removes an attribute the template omits only when the previous template rendered it; a template that stops rendering an attribute still removes it, and plain elements are unchanged. An attribute set imperatively on a custom element now survives re-renders (`KF-K8S4RX`). Elements kerf creates (a mount's first render, a morph insertion, a new `each()` row) start with their template's record, taken before they connect, so even the first re-render leaves their reflected defaults alone; only an element kerf did not create uses the ordinary rule on its first attribute sync (`KF-G7N4PK`).
|
|
91
|
+
|
|
92
|
+
- `KUI-L017` (exceptional spacing shorthand, such as `<List gap="s" />`) now has one severity everywhere: a review finding. `eslint-plugin-kerfjs` reported it from the `error`-level `ui-css-values` rule, so a deliberate off-scale choice failed lint while `kerf-ui-analyze` queued it for review, and `kerf-ui-doctor` counted every occurrence twice (error plus review) with a `KUI-D003` conflict warning. The finding moves to a new `ui-exceptional-shorthands` rule, `warn` in both `recommended-ui` and `strict-ui`; `ui-css-values` keeps `KUI-L013`–`KUI-L016` at `error`. The doctor reads an ESLint warning for an analyzer review code as that review finding, and the analyzer's `KUI-L014`–`KUI-L017` messages now match ESLint's word for word, so the doctor merges each CSS-value finding into one diagnostic. A new `@kerfjs/ui` contract test fails if any ESLint UI rule's preset severity disagrees with the analyzer's classification of the codes it reports (`KF-P3945Y`).
|
|
93
|
+
|
|
94
|
+
- `mount()` no longer walks the whole live tree for `each()` list markers after a re-render that changed the page when the render has no `each()` lists and no list is bound. A large list-free page (one `mount()` with plain `.map()`) paid a full DOM traversal per render for markers kerf never emitted (`KF-VBQJAX`).
|
|
95
|
+
|
|
96
|
+
- Faster JSX attribute serialization on large pages: the runtime remembers, per attribute name, that the name passed the `on*` / malformed-name check and whether it is URL-bearing, instead of re-running those regexes for every attribute of every render, and the URL screen returns early for values without a `:` (no scheme to screen). Rejected names still throw their exact message on every render, and every URL value is still screened. Building a 300-row, ~1.4 MB list-free page drops from about 13.7 ms to 10.8 ms of CPU (`KF-VBQJAX`).
|
|
97
|
+
|
|
98
|
+
- `wireScrollDividers` no longer forces a synchronous layout after a render. A content mutation used to re-measure the scrollers in the next animation frame, and a structural one at once in the mutation callback, both before the browser had laid out the new tree. Both now re-measure once the frame has laid out and before it paints: the wiring toggles an inert, zero-size probe outside the app tree, and its `ResizeObserver` reports the probe after layout. Without `ResizeObserver` the previous timing remains. In a 300-row work area, `measure` self time drops from about 408 ms to under 1 ms over 16 segment switches and 8 row selections; the layout itself now runs once, in the browser's frame (`KF-4XYQ95`).
|
|
99
|
+
|
|
100
|
+
- `wireWorkbench` no longer forces a document-wide style recalculation after ordinary renders and presses. It caches each responsive panel's computed overlay state until a Workbench resize, a root font-size change, or a presentation attribute change on that panel, and its observer re-syncs only for a new Workbench, an added or removed panel, or an attribute change on a panel rather than any `data-collapsed` or `data-presentation` change below the root. In a 300-row work area with Web Awesome menus, the overlay check's forced style time drops from about 150 ms to 0 over 16 segment switches and 8 row selections (`KF-CRRD70`).
|
|
101
|
+
|
|
102
|
+
- Faster `PopupMenu` and `LucideIcon` re-renders in large lists. `LucideIcon` returns the same `SafeHtml` for the same icon and props instead of rebuilding its `<svg>` every render (up to 64 prop variants per icon), and builds its child markup as a Fragment instead of wrapping joined strings in `raw()` (`KF-XW15D6`). `PopupMenu` hashes icon and details markup of 64 or more characters as a memoized digest of the whole string instead of reading every character every render. A 300-row list with a nine-entry menu per row drops from about 12.7 ms to 2.1 ms per render when the icons are rebuilt each render, and from 3.9 ms to 1.3 ms when they are prebuilt. A menu with such a part now has a different `data-key` value than before; the key is still stable across renders, and every other byte of the rendered HTML is unchanged (`KF-V2ZBHT`).
|
|
103
|
+
|
|
104
|
+
- `PopupMenu` hashes its content key with a streaming `charCodeAt` loop instead of joining every field (including icon markup) into one string and iterating its code points, and caches each menu's trigger and entry markup in a bounded least-recently-used cache keyed by that hash and an independent check hash. An unchanged menu now costs one hash pass instead of rendering every entry again for the morph to skip. Rendered HTML, including the `data-key` digest, is byte-identical (`KF-GVCMC7`).
|
|
105
|
+
|
|
106
|
+
- Faster attribute serialization for large static values: `LucideIcon` caches each icon node's rendered child markup by node identity, and the JSX runtime's HTML/attribute escaping skips its replace passes when a value has nothing to escape. A 138-icon render pass drops from about 0.80 ms to 0.45 ms (`KF-JPP0Z5`). `jsx()` also iterates props without the rest-spread and `Object.entries` allocations, and the URL screen checks lowercase attribute names without allocating a lowercased copy, about 25% faster for an attribute-heavy list row (`KF-YHNV84`).
|
|
107
|
+
|
|
108
|
+
- `wireTokenSearchFields` collapses an empty field blurred by a pointer press in the click's own task, after the click reaches the window, instead of a separate task, so the click's render and the collapse share one style recalculation; a click that stops propagating still falls back to the next task (`KF-AS24QP`).
|
|
109
|
+
|
|
110
|
+
- `wireScrollDividers` no longer re-collects pairings and re-measures every scroller for mutations that cannot change a pairing, such as a row's selection class or a list morph inside scrolled content; it still refreshes when structural elements or target ids are added, removed, re-classed, or renamed (`KF-50PA0Y`). Those content mutations instead re-measure the known scrollers once in the next animation frame, so an absolutely positioned descendant that extends the scroll area still updates its dividers (`KF-E29N3W`).
|
|
111
|
+
|
|
112
|
+
- The UX Catalog filter keeps its placeholder hidden when a controlled state change rerenders the sidebar while filter text remains (`KF-4EA9R6`).
|
|
113
|
+
|
|
114
|
+
- Inspector TabBars use the shared rounded rectangle radius on the strip and inset tabs while ordinary segmented TabBars retain their pill geometry (`KF-K3RDN1`).
|
|
115
|
+
|
|
116
|
+
- **`KUI-L103` no longer flags a component's classes inside its own source.** `ui-public-boundaries` treats a catalog entry's `source` file as the owner of that entry's public classes, so an app component writing its own root class is not reported as recreating itself. Apps no longer need to mark those classes `placeableClasses` to silence it. Any other file, including another component's source, is still checked (`KF-G6FR6V`).
|
|
117
|
+
|
|
118
|
+
### Documentation
|
|
119
|
+
|
|
120
|
+
- New recipe in the `kerfjs/async` API reference: move heavy app compute (fuzzy search, CSV or markdown parsing, aggregation) into a Worker by having a `resource()` fetcher post a message to the worker and resolve with its reply. It covers request/reply ids, dropping superseded results, worker errors, transferring `ArrayBuffer`s, and keeping the render on the main thread (`KF-04KHT9`).
|
|
121
|
+
|
|
122
|
+
- Clarify for AI consumers that sunken/lowered token names resolve to a translucent tint, and explain intentional nested layers versus duplicate surface paint (`KF-9CM6D8`).
|
|
123
|
+
|
|
124
|
+
- **`layout.roles` vocabulary for app components.** The component-packages guide documents the eleven roles `@kerfjs/ui` uses, what each covers, and how to map application dialogs, rows, status indicators, and composed views onto them. The `kerf.components.json` schema description lists them, and the `create-kerf-component` template now uses `controls` instead of a one-off `counter` role (`KF-4E04HF`).
|
|
125
|
+
|
|
126
|
+
### Removed
|
|
127
|
+
|
|
128
|
+
- **Breaking (`@kerfjs/ui`, 5.0 beta):** remove the Catalog geometry highlight overlay, its `geometryOverlay` prop, `wireCatalogGeometryOverlay` helper, and specimen skip marker. Catalog previews retain their component borders, outlines, and focus states (`KF-E3WKA5`).
|
|
129
|
+
|
|
130
|
+
- **Breaking (`@kerfjs/ui`, 5.0 beta):** remove the deprecated `TabScaffold` component and type aliases, `wireTabScaffold` and `WireTabScaffoldOptions`, and the `tab-scaffold`, `wire-tab-scaffold`, and `tab-scaffold.css` package subpaths. Use `TabNavigator`, `TabNavigatorTab`, `TabNavigatorProps`, `wireTabNavigator`, and their corresponding subpaths. The rendered DOM classes and data attributes remain stable. See `ui/docs/tab-navigator.md` for migration steps (`KF-CMK0W8`).
|
|
131
|
+
|
|
132
|
+
### Added
|
|
133
|
+
|
|
134
|
+
- **Typed background colors on `@kerfjs/ui` layout surfaces.** `Pane` takes `background`, a `CssColor` from `@kerfjs/ui/css-values` (`uiColor(...)`, `fillColor(...)`, or `colorVar(...)`). It paints the whole pane and replaces the default or sunken fill, and it is in `PaneConfig`, so Workbench panels, `CollapsiblePanel`, and `NavStack` view panes forward it. `NavStack` and `TabNavigator` take their own `background` for the stack or navigator surface and their docked bars, and each `TabNavigatorTab` takes one for its scene. Overlay chrome keeps the translucent material, and forced colors paint `Canvas`. Omitted, every surface keeps its current color.
|
|
135
|
+
- **Static-effects switch for `@kerfjs/ui`.** Set `data-kui-effects="static"` on the root element (`document.documentElement.dataset.kuiEffects = 'static'`) to turn every backdrop effect opaque and stop every scroll-driven animation. Use it when the app detects a low-power device itself, since CSS has no reliable signal for one. A slow-updating display (`@media (update: slow)`, such as e-ink) gets the same result automatically. The translucent chrome material (overlay `Pane` headers, `NavStack` and `TabNavigator` overlay bars, floating `DialogSurface` headers) resolves to the opaque surface, as it already does under reduced transparency.
|
|
136
|
+
- `ToolbarControlGroup` takes `material: 'solid' | 'glass'` (default `solid`). A `glass` group paints the translucent chrome material (`--kui-chrome-material`, blurred and saturated by `--kui-chrome-material-filter`), replaces its gray border with a specular edge highlight, and floats on a soft shadow. Use it for per-control glass pills over imagery or a clear (transparent) toolbar, for example inside a `FloatingToolbar`. Its controls take the normal text color, and a raised or outline selection paints an opaque surface pill. Rest, hover, and selected controls keep WCAG AA over worst-case content in light and dark: the bare material leaves the brand selected color at about 3.2:1 over black. The fill turns opaque under reduced transparency, increased contrast, static effects, or without `backdrop-filter`, and the edge gives way to the normal border there. A dark-toned glass group's loud fill turns opaque too. Forced colors paints `Canvas`/`CanvasText` with no filter or shadow. A borderless group ignores the material.
|
|
137
|
+
- **`BackdropImage`.** `@kerfjs/ui/backdrop-image` opens a scrolling Pane or NavStack view with a hero or tiled image (`src`/`srcset`/`alt`, `fit`, `position`, `repeat`, typed `height`). `parallax` (`subtle` or a 0 to 1 factor) lags the image on a compositor-only scroll-driven timeline with no scroll listener or `background-attachment: fixed`; a typed `fadeTo` color dissolves it through a mask and keeps painting under its `children`, so the page reads as one surface; `overlay` content sits over its block end with an optional `scrim`. It is static without scroll-driven animation support, under reduced motion, `data-kui-effects="static"`, or on slow-update displays, and ends on a hard edge over solid `fadeTo` under reduced transparency or increased contrast.
|
|
138
|
+
- **`BackdropImage` progressive blur.** `blur={{ radius?, blurredSrc?, dim? }}` stacks a copy of the image blurred once (a CSS `filter` of `radius`, default 24px, overhanging the block so its fringe is clipped) or an app-supplied pre-blurred `blurredSrc`, and fades only its opacity (plus an optional black `dim` layer) in over the first half of the image's scroll exit, so the blur is at full strength while most of the image is still in view; the filter never animates. A Chromium trace across 200 scroll frames recorded no `Paint` events and no per-frame raster. Static and sharp wherever the parallax is static.
|
|
139
|
+
- `TabNavigator` takes `barPresentation: 'docked' | 'overlay'` (default `docked`). With `overlay`, the tab bar lies over the active scene on the translucent chrome material, so content scrolls visibly beneath it, as under an iOS tab bar. `wireTabNavigator` measures the live bar (bottom safe area included), and the scenes hand its height to their content as the block-end edge inset: a plain scene, a sole `Pane`, or a `NavStack` (whose own overlay bottom toolbar then sits just above the bar) rests its last item clear of it. Unselected and selected labels lift to keep AA over the material. The bar is opaque under reduced transparency, increased contrast, or without `backdrop-filter`, and paints `Canvas` with a `CanvasText` edge in forced colors. Unwired, the bar stays docked. Like the NavStack overlay bars, the tab bar ends where the active scene's classic (space-taking) scrollbar begins, so the thumb and buttons stay visible.
|
|
140
|
+
- `@kerfjs/ui/wire-list-focus-scroll`'s `wireListFocusScroll(root)` brings a keyboard-focused row of a `scrollable` List fully into view, focus ring included, in every engine. Firefox's `focus()` leaves a partly hidden row alone until about half of it is hidden and then centers it, and Safari's never scrolls it, so arrow-key or Tab navigation could rest on a row that was mostly clipped. The wiring calls `scrollIntoView({ block: 'nearest' })` only when a `:focus-visible` element's ring crosses the List's clip, honoring its ring-room scroll padding; pointer focus is left alone, so a click never scrolls its row from under the pointer.
|
|
141
|
+
- **`AnchoredSurface` dark tone for popups anchored to media chrome.** `openAnchoredSurface` and `openAnchoredSurfaceAt` take `tone: 'dark'`, the same loud, inverse tone as a `ToolbarControlGroup` or `ToolbarText` `tone="dark"`. A volume popup anchored to a dark media control group over a video preview is dark beside a light page and light beside a dark one, matching the group that opened it. The surface takes the opposite of its anchor's color scheme when it opens and is marked `data-tone="dark"`. Every semantic token is a `light-dark()` pair, so the content inherits the inverse text, border, and focus colors without tone props or overrides: a `ListItem` mute row and a `wa-slider` included. Measured on the catalog demo in Chromium, Firefox, and WebKit, the row label reads 12.8:1 and its focus ring 8.1:1 on a light page, and 16.8:1 and 4.0:1 on a dark one. The default tone is unchanged.
|
|
142
|
+
- A floating `DialogSurface` title bar (`headerPresentation="floating"`) now draws its divider as scroll state, like an overlay `Pane` header: a 1px line along its bottom edge while the body, or a filling child that reaches beneath it (a transcript `List` or `Pane` with block-start `scrollClearance`), is scrolled away from its top, and none at rest. In light mode the title bar over white bubbles had little separation. `@kerfjs/ui/surface-scaffold/register` follows those scrollers; a nested scroller that starts below the title bar never counts.
|
|
143
|
+
- **Analyzer and lint check row icon-tile colors.** Catalog `cssValueProps` paths now reach a member of an object-valued prop (`iconTile.color`) as well as each array item's member (`choices[].swatch`); a prop given a non-object value, such as a tone string `iconTile="brand"`, is skipped. `ListItem`, `ListItemLink`, and `ListActionRow` record `iconTile.color` as a color fill (`fillColor()`, `uiColor()`, `colorVar()`) and `iconTile.foreground` as a foreground (`uiColor()`, `foregroundColorVar()`, `foregroundColor()`), so the Kerf UI analyzer, `kerf-ui-doctor`, and the `kerfjs/ui-css-values` ESLint rule report `foregroundColor()` on a tile fill (`KUI-L014`) and keep accepting it as the glyph color. The catalog and composition schemas' `path` pattern accepts the same grammar, so consumer catalogs can publish object-member paths (`KF-R02RDK`).
|
|
144
|
+
- **Catalog frames can be a named anchor with a centered content column.** `CatalogExample`'s `viewport` takes `anchorName`, a CSS dashed-ident (`"--app-shell-main"`) that becomes the frame's `anchor-name`, so a top-layer specimen (a `popover="manual"` request popup, say) whose own CSS sets `position-anchor` to that name positions against the frame exactly as it does against its production anchor. An invalid name throws a `TypeError`. `minHeight` adds `"app"` (592px, a floor the frame can grow past), and `contentWidth` (`"compact"` 440px, `"medium"` 640px, or `"wide"` 864px, capped at the frame) with `contentAlign` (`"start"` or `"center"`) gives each in-flow child a centered or start-aligned reading column; `[popover]` children keep their own geometry. An app no longer needs a wrapper stage element to name the anchor and center the column. The catalog's Application Layout page shows a request popup docked to the bottom of its frame (`KF-D532S2`).
|
|
145
|
+
- **`fillColor()` for application fill colors.** `@kerfjs/ui/css-values` adds `fillColor(value)`, the fill twin of `foregroundColor()`: it accepts one application-chosen CSS color (`#3b82f6`, `rgb(20 40 60 / 0.8)`, `rebeccapurple`) with the same single-declaration check, but returns a plain `CssColor` rather than a `CssForegroundColor`, so a fill literal is rejected by props that paint text or an icon (`SelectChoice.color`, `LucideIcon.color`, `Text.color`, an `iconTile` `foreground`). Use it for a `ChoiceChips` `swatch` and a list row's `iconTile` `color`; every example, demo, and design template now does. A `CssForegroundColor` is still a `CssColor`, so an existing `foregroundColor()` fill keeps compiling, and the `ChoiceChips` catalog entry now records `choices[].swatch` as a color fill so the Kerf UI analyzer reports `foregroundColor()` there (`KUI-L014`) with `fillColor()` as the fix (`KF-7JA25S`).
|
|
146
|
+
- **Framed result pickers without app CSS.** `List` takes `frame`, `maxHeight`, and `minHeight` (typed `CssLength`s from `@kerfjs/ui/css-values`). `frame` draws a 1px neutral border with the `--kui-radius-m` control radius and a 4px inset on every side, replacing `inset`, `textInsets`, and `controlInsets`. That inset is exactly a row's focus-ring reach, and the frame adds it to its scroll padding, so the first and last rows' rings stay inside the clip. With `scrollable`, the height bounds work without a flex-column ancestor. `<List scrollable frame gap="2xs" minHeight={rem(4.25)} maxHeight={rem(14)}>` is a bounded list of `ListItem` candidates, such as Hot Sheet's duplicate-ticket picker. The catalog's List page shows it as "Result Picker" (`KF-SHW2J9`).
|
|
147
|
+
- **Transcripts scroll beneath a floating composer.** A `scrollable` `List` and a `Pane` take `scrollClearance` (`"block-start"`, `"block-end"`, or `"both"`; the `ScrollClearance` type). It clears chrome laid over the scroller's block edge inside its own scroll area: the scroller adds the inherited `--kui-scroll-clearance-block-start` / `--kui-scroll-clearance-block-end` context that the chrome publishes to its block padding and `scroll-padding`. Content then passes visibly beneath the chrome, while the first and last items, and anything focused or scrolled into view, still rest clear of it. A `DialogSurface` with `footerPresentation="floating"` publishes block-end clearance for a `fill` or `fill-last` child that opts in. That clearance is the footer's measured height plus its offset. The body stops reserving the footer below that child, so a chat transcript reaches the panel's end and scrolls beneath the transparent composer. Without the opt-in, the transcript still ends at the composer's top. Every dialog body clears clearance inherited from the page. An app that overlays its own translucent chrome on a scrolling region publishes the same context, so header clearance works today. The catalog's chat dialog now scrolls its transcript beneath the composer (`KF-ET0EX8`). `PaneConfig` carries `scrollClearance` too, so a composed Pane opts in through a Workbench `mainPane` or panel `pane`, a `CollapsiblePanel` `pane`, or a `NavStack` view's `pane`; the composites now forward every `PaneConfig` field through one exhaustively typed helper, so a new field can no longer be silently dropped (`KF-574HQV`).
|
|
148
|
+
- **Translucent overlay Pane headers.** `Pane` takes `headerPresentation` (`"docked"`, the default, or `"overlay"`; the `PaneHeaderPresentation` type, also in `PaneConfig`). An overlay header is laid over the top of the scrolling content on the new chrome material. The material is `--kui-chrome-material`, the surface at 80% opacity, behind `--kui-chrome-material-filter`, a blur and saturation backdrop filter, so content scrolls visibly beneath the header. `wireScrollDividers` measures each overlay header with a `ResizeObserver` and publishes its height on the Pane as `--kui-scroll-clearance-block-start`. The Pane's content clears that automatically, so the first item rests just below the header and anything scrolled into view stays clear of it. The clearance follows the header as its title wraps or its text grows. Until the wiring runs, and with `chromePlacement="auto"`, the header stays docked. The material turns opaque under `prefers-reduced-transparency: reduce` or `prefers-contrast: more`, and without `backdrop-filter`. In forced colors the header paints `Canvas` with a `CanvasText` edge. Normal text keeps WCAG AA over any content beneath the material. Quiet text does not, so overlay-header text uses the normal text color (`ui/docs/accessibility.md` "Chrome material"). The catalog's Pane page shows an inbox scrolling beneath its header (`KF-DT6C3F`).
|
|
149
|
+
- **Translucent dialog title bars.** `DialogSurface` takes `headerPresentation` (`"docked"`, the default, or `"floating"`; the `DialogSurfaceHeaderPresentation` type). A floating header lies over the body's block start on `--kui-chrome-material-raised`, a new token for chrome on a raised surface: the raised panel color at 80% opacity behind `--kui-chrome-material-filter`. The `surface-scaffold/register` import now measures the header as well as the footer, and the body reserves the header's height before its own inset, with matching scroll padding, so flowing content starts below the title bar and scrolls beneath it. Without the register, the header stays docked. When the body's only child fills it and opts into block-start `scrollClearance`, the body publishes the header's height as `--kui-scroll-clearance-block-start` instead. With a floating composer and `scrollClearance="both"`, a transcript then scrolls beneath both the title bar and the composer while its first and last messages rest clear of them. The material has the same opaque fallbacks as the Pane overlay header, and normal text keeps WCAG AA over any content beneath it. An untoned `ToolbarText` slotted as the dialog's label (or into its header actions) takes the same AA lift it gets in an overlay Pane header: a normal-color title and a description mixed halfway toward it. The catalog's surface scaffold page adds a "Floating Title Bar" thread dialog (`KF-REH2T2`).
|
|
150
|
+
- **Translucent NavStack top bar.** `NavStack` takes `chromePresentation` (`"docked"`, the default, or `"overlay"`; the `NavStackChromePresentation` type). An overlay top chrome lies over the active view on the same chrome material as an overlay Pane header, so content scrolls visibly beneath it, as under an iOS navigation bar. `wireNavStack` measures the live chrome (never a cross-fading copy) with a `ResizeObserver`, publishes its height on the stack root with a `data-chrome-overlay="measured"` gate, and restores both before paint if a re-render strips them. The measured height, top safe area included, becomes the viewport's block-start edge inset, so a plain view, a sole `Pane`, or a nested layout clears the bar automatically: the first item rests just below it, and focused or scrolled-to content stops clear of it. A classic, space-taking scrollbar in the active view stays uncovered: the chrome ends where its track begins. Unwired, or with `hideToolbar`, the chrome stays docked. The material keeps its opaque fallbacks (reduced transparency, increased contrast, no `backdrop-filter`) and its forced-colors edge, and an untoned `ToolbarText` in the chrome lifts its title and description to AA exactly as in an overlay Pane header. `SplitView` forwards the option through `compactStack`, and a Workbench navigation panel through `navStack`. The catalog's NavStack page adds an **Overlay Chrome** example; `ui/tests/browser/overlay-nav-stack.spec.ts` covers it in Chromium, Firefox, and WebKit.
|
|
151
|
+
- **Translucent NavStack bottom toolbar.** `NavStack` takes `bottomToolbarPresentation` (`"docked"`, the default, or `"overlay"`; the `NavStackBottomToolbarPresentation` type), a separate prop from `chromePresentation`, which keeps governing only the top chrome. An overlay bottom toolbar lies over the active view's bottom edge on the same chrome material, as an iOS toolbar does. `wireNavStack` measures the live toolbar (never a cross-fading copy), publishes its height with a `data-bottom-toolbar-overlay="measured"` gate, and the measured height, bottom safe area included, becomes the views' block-end edge inset: a plain view's or a sole `Pane`'s last item (or the Pane's footer) rests just above the toolbar while content scrolls visibly beneath it, and the toolbar's scroll divider keeps working. Unwired, or while no bottom toolbar is shown, it stays docked; inside a `TabNavigator` scene it sits just above the tab bar. It keeps the material's opaque fallbacks, and in forced colors its top border becomes a `CanvasText` edge. A default-tone `ToolbarControlGroup` in either overlaid bar now lifts its controls to the normal text color, so they keep AA over the content beneath (the top chrome's back control measured about 3.0:1 before). A non-scrolling `List` directly in a `Pane`'s content no longer shrinks below its rows, so the content's block-end padding (a bottom safe area or the toolbar's clearance) follows the last row. `SplitView` forwards the option through `compactStack`, and a Workbench navigation panel through `navStack`. The catalog's **Overlay Chrome** example now overlays both bars; `ui/tests/browser/overlay-nav-stack-bottom.spec.ts` covers it in Chromium, Firefox, and WebKit.
|
|
152
|
+
|
|
153
|
+
- **Anchored surfaces fit beside their anchor.** `positionAnchored()` / `autoReposition()` (and so `popover()` and `tooltip()`) take `viewportMargin`, a gutter in px that the horizontal clamp and the vertical fit test keep from the viewport edges (default `0`, the bare edge as before), and `fitHeight` for an element that scrolls its overflow: when neither side has room for its full height it opens on the roomier side, and every placement publishes that room as the `--kerf-anchor-available-height` custom property on the element, so CSS can cap it (`max-height: var(--kerf-anchor-available-height); overflow: auto`). Scrolling inside the element keeps its scroll position. Without the new options placement is unchanged. `@kerfjs/ui`'s `openAnchoredSurface()` uses both: the surface stays 8px inside the viewport (`viewportMargin` overrides it), caps its height to the room beside its anchor, scrolls its content, and exposes that room to its content as `--kui-anchored-surface-available-height`, refreshed on scroll and resize, so a tall help surface fits beside a field on a phone with no application media queries (`KF-K4GE6K`).
|
|
154
|
+
|
|
155
|
+
- **Stateful `TokenSearchField` trailing action.** `trailingAction` now takes `expanded` (a disclosure such as a help toggle; renders `aria-expanded`) or `pressed` (a toggle; renders `aria-pressed`), which are mutually exclusive at compile time, plus `controls` (`aria-controls`), `haspopup` (`aria-haspopup`), and `attributes` for domain `data-*` metadata (the field keeps `data-action`). An open or pressed button keeps its hover fill and reads at full text strength. In the `toolbar-group` presentation it now takes the clear button's control-highlight radius, so the compact 24px help glyph beside the clear button matches it in every presentation with no application CSS. The catalog's Structured Ticket Search help toggle opens syntax help in an `openAnchoredSurface` (`KF-R056AJ`).
|
|
156
|
+
|
|
157
|
+
- **Link `Chip` for downloadable files.** `Chip` takes an `href`, with an optional `download` (`true` or a suggested file name) and `external` (new tab, `rel="noopener noreferrer"`, and an "(opens in new tab)" accessible-name suffix, as on `ListItemLink`). It then renders a native `<a>` with the chip's geometry, icon slot, truncation, focus ring, and hover feedback, so an app no longer styles its own file-link chips. A link chip cannot also carry a remove button, and a disabled one drops its destination and leaves the tab order. The new `inherit` tone (`ChipTone`) follows the surrounding foreground, for a chip inside a `brand-loud` or `neutral` `ContentItem` bubble. It draws a currentColor border and no fill, so the label keeps the bubble's text contrast, and its focus ring follows the foreground too. The Chip catalog shows file chips in both bubble kinds (`KF-H658D6`).
|
|
158
|
+
|
|
159
|
+
- **Multi-select `ChoiceChips`.** Set `multiple` with a `readonly Value[]` `value` and the same wrapping pills, swatches, and icons become native checkboxes in a named `role="group"`: each pill is its own Tab stop, Space toggles it, assistive technology reads its native checked state, and every checked value submits under `name` (`FormData.getAll(name)`). The props are a discriminated union, so a multiple group rejects a single value (and a single group an array), and a multiple group takes no `required`. Every chip color is a theme token, so on dark inverse chrome, such as a media viewer's annotation intents, place the chips in a dark appearance scope (`.wa-dark`) rather than restyling them. The UX catalog adds Multiple Selection and On Dark Chrome examples, and the design templates add a Multiple Selection variant.
|
|
160
|
+
- `ListItem`, `ListItemLink`, and `ListActionRow` take an `iconTile` prop that presents the leading icon in a filled 32px rounded tile holding an 18px glyph, for settings and command rows identified by color. Pass a tone (`neutral`, `brand`, `success`, `warning`, or `danger`, the same quiet palette as `ToolbarControlGroup.tileTone`) or `{ color, foreground? }` with a typed `CssColor` fill (`uiColor()`, `colorVar()`, or `foregroundColor('#e5484d')`) and an optional `CssForegroundColor` glyph; without a foreground, the glyph is black or white, whichever contrasts with the fill. The tile keeps 6px clear of a standard 44px row, a compact row is at least 42px tall, the label moves to the tile's edge, and hover and selection leave the tile's colors alone.
|
|
161
|
+
|
|
162
|
+
- **A chat composer without app chrome.** The new `Composer` (`@kerfjs/ui/composer`) is a raised frame (the standard border, large radius, and `shadow-l`) whose border turns brand while focus is within. It holds a borderless textarea that grows with `field-sizing: content` between `minRows` (default 1) and `maxRows` (default 8) lines and then scrolls. Below the textarea sits an action row with an optional quiet `hint`, which describes the field when you pass an `id`, and the trailing `actions` (a Send button). Engines without `field-sizing` show a fixed three-line field that scrolls. Slot it into a `DialogSurface` floating footer to float it over a transcript; the catalog's chat dialog now does (`KF-S5EG5K`).
|
|
163
|
+
- **Enter-to-send for `Composer`.** Set `submitOnEnter` and call `wireComposers(root)` (`@kerfjs/ui/wire-composers`) once: Enter, or Cmd/Ctrl+Enter, in the field clicks the composer's first trailing action, so a keyboard send runs the same delegated `data-action` handler as a click. Shift+Enter still inserts a newline. An Enter that commits an IME composition (`isComposing`, keyCode 229, or between `compositionstart` and `compositionend`) is left to the input method, and an empty or whitespace-only message, an auto-repeating key, a disabled field, or a disabled or loading action sends nothing. The field announces the shortcut with `aria-keyshortcuts="Enter"`. The catalog's Composer and chat dialog examples use it (`KF-DGD5QS`).
|
|
164
|
+
- `PopoverDialogSurface` takes an optional `describedBy` id, rendered as `aria-describedby` on its `role="dialog"` root. Pair it with a title `ToolbarText`'s `descriptionId` so a non-modal dialog keeps its accessible description; `rootAttributes` accepts only `data-*` attributes, so there was no way to set it before. `DialogSurface` is unchanged, because it wraps a `wa-dialog` that the app authors and does not render the dialog root itself (`KF-SKVEQW`).
|
|
165
|
+
- A compact `ListActionRow` is 36px tall, as documented, instead of 44px. Its label kept the standard row's minimum height and its primary button kept the standard 8px block padding, so `density="compact"` had no effect on a single-line row. Compact rows now use 4px block padding like a compact `ListItem`, so the two row kinds stay level in a dense list; a compact row with an `iconTile` is exactly 42px. The ListActionRow catalog adds a Compact Rows example (`KF-4Q5MJ5`).
|
|
166
|
+
- `ListActionRow` leading icons default to 18px (they were 24px), the same size as `ListItem` icons, so a `List` that mixes the two row kinds starts their labels on one edge, not 6px apart. Both read a new shared `--kui-list-row-icon-size`: set it on a containing `List` to resize both row kinds' untiled icons together. `--kui-list-item-icon-size` still wins for `ListItem` rows that set it, and an `iconTile` keeps its 32px tile and 18px glyph. An app that wants the old 24px `ListActionRow` glyphs sets `--kui-list-row-icon-size: 24px` on the list (`KF-KWFMM1`).
|
|
167
|
+
- `ListItem` and `ListActionRow` share one leading inline inset. Set `--kui-list-row-padding-inline` on a containing `List` and both row kinds move their content and reorder insertion lines together; a `ListActionRow` adds the 1px a `ListItem` border takes, so their leading icons stay on one edge at any value. It defaults to `--kui-layout-item-padding`, so unset lists render as before. Previously only `ListItem` had an inline padding token, and overriding it moved `ListItem` icons away from `ListActionRow` icons in the same list. `--kui-list-item-padding-inline` still wins for `ListItem` rows that set it. The List catalog's new Shared Row Inset example shows mixed rows on a customized inset (`KF-N3TDV3`).
|
|
168
|
+
- `@kerfjs/ui` now has one capitalization rule for interface copy, taken from Apple's guidance and documented in the new `ui/docs/writing.md`. Text that names a command, control, view, or field is title style ("Save Draft", "Back to Inbox", "Workspace Settings"): buttons, menu items, segments, `Select` choices, tabs, titles, `ListHeader` labels, and field labels. Text that describes, explains, or reports is sentence style: descriptions, messages, tooltips, placeholders, option text, and status lines. The AI skill, `llms.txt`, and the design docs carry the rule. `kerf-ui-doctor` reports a lowercase word in a known title-style label as the warning `KUI-D040`, with the repaired label. The catalog demos, recipes, and design templates now follow the rule (`KF-6VWSV4`).
|
|
169
|
+
- **Chat-style dialogs without `::part()` rules.** `DialogSurface` gains `footerPresentation="floating"`, which lays the footer over the body's block end, a typed `footerOffset` from the panel's inline and block-end edges (default the 16px container inset), on a transparent background. The `surface-scaffold/register` import measures the footer, and the body reserves that height plus the offset, so a growing composer never hides the transcript's last message. `bodyLayout="fill-last"` (also on `PopoverDialogSurface`) keeps the body's leading children at their natural height and gives the last child the remaining height and its own scrolling: a notice above a scrolling transcript. The catalog's new chat dialog example composes both (`KF-913AT2`).
|
|
170
|
+
- `@kerfjs/ui`'s title-style rule now reaches the names the doctor could not see. `TokenSearchField`'s default `clearLabel` is `Clear Search` (it was `Clear search`); a test or app that addresses the clear control by its default accessible name needs the new spelling, and `clearLabel` still overrides it. The catalog shell (`Filter Catalog`, `Clear Filter`, `Reduce Motion`, `Vertical Stripes`, `Layout Guide`, `Related Entries`, `Capture Region`, the `Demo Source` / `Component Source` / `Design Template` / `Integration Guidance` resource links, and the `Used By` related group), the catalog entry names that render as page titles (`Foundation Tokens`, `Surface Scaffolds`, `List Workspace States`, …), and the demos' and recipes' accessible region, control, field, dialog, tab, and list-item names are title style. `kerf-ui-doctor`'s `KUI-D040` now also reads the accessible names of icon-only controls: `IconButton` / `IconToggleButton` / `PopupMenu` `label`, `SplitButton` `optionsLabel`, `TokenSearchField` `clearLabel`, and the `aria-label` of a `button` or `wa-button` whose only content is an icon (`KF-ZAD6EJ`).
|
|
171
|
+
- `@kerfjs/ui` catalog example and stack labels are headings, so they now use title style across every UX-demo page ("Heading Identity Stays Whole", "Icon Trigger in a Toolbar Group"); their notes stay sentence style. Demo content item titles ("Build Output", "Work Area") follow, with mirrored `ariaLabel`s. `kerf-ui-doctor`'s `KUI-D040` now reads `CatalogExample` and `CatalogExampleStack` `label`, and keeps a minor word inside a longer hyphenated compound lowercase ("Edge-to-Edge"). A test that addresses a demo specimen by its region name needs the new spelling (`KF-4TXTVV`).
|
|
172
|
+
- A labelled `CatalogExample` now names its `section` with its label, so each specimen is a named region that assistive technology and `getByRole('region', { name })` can address. An unlabeled example stays an ordinary grouping (`KF-ZQMXK1`).
|
|
173
|
+
- `CatalogExampleStack` takes a `width` option: `reading` (default) keeps the 736px reading measure, and `full` lets the stack span the stage so a catalog can show application-width compositions such as boards, app shells, drawers, and split views without clipping them (`KF-HKVT5S`).
|
|
174
|
+
- `CatalogExample` `viewport.width` takes `sidebar`: a 272px production-rail width, capped at the stage, so sidebar-pane specimens render at their real width without a wrapper (`KF-DH5MH8`).
|
|
175
|
+
- In a `PopupMenu` level (the top menu or one submenu) that holds a checkbox choice, plain commands now reserve the check column too, so check, icon, and label columns line up across checked, unchecked, and plain rows; a level without a choice keeps the compact start. Submenu checkbox rows drop an extra 1em label inset, so their icon-to-label gap matches the top level (`KF-31QHPE`).
|
|
176
|
+
- Hovering a selected (`aria-current`) or pressed `ListItem` likewise keeps its selected tint instead of the neutral hover colors (`KF-84AGE5`).
|
|
177
|
+
- Hovering a selected or pressed `ListActionRow` keeps its selected tint instead of switching to the neutral hover colors, so a freshly selected row no longer looks unselected while the pointer stays on it (`KF-Z737D0`).
|
|
178
|
+
- A `width="full"` `CatalogExampleStack` now spans the whole catalog stage on wide screens; the stage's 1120px canvas cap no longer stops it short of the right edge (`KF-WDSFWH`).
|
|
179
|
+
- **Chat-bubble `ContentItem`s.** `bubble="start" | "end"` lays a `ContentItem` out as a transcript bubble: it fits its message up to 80% of the row, sits at the start (other participants) or end (the person's own messages), and wraps long unbroken text. The new `appearance="brand-loud"` paints the solid brand fill with its on-loud foreground for the person's own messages (`KF-M3HJK8`).
|
|
180
|
+
|
|
181
|
+
- **`DialogSurface rootAttributes`.** `DialogSurface` takes safe `data-*` `rootAttributes`, such as a `data-key`, on its root; its own configuration attributes stay protected. Sibling dialogs that render conditionally then morph by identity rather than by position. Before, a re-render could reuse one open dialog's element for another, swapping its accessible name and suppressing `wa-after-hide`. Apps no longer wrap each dialog in a keyed `div` (`KF-D7H0WN`).
|
|
182
|
+
|
|
183
|
+
- **Heading supporting text.** A read-only `ToolbarText` takes `description`: quiet, single-line, truncating copy under the title ("Changes save automatically."). It starts at the title's text whether or not a leading icon group is present, and shares the control band so trailing actions stay aligned. `descriptionId` gives it an id for `aria-describedby`. With a description, `id` and `headingLevel` apply to the title span alone, so `aria-labelledby` names only the title. Apps no longer recompute the title inset from toolbar tokens (`KF-DXVMBN`).
|
|
184
|
+
|
|
185
|
+
- **Drag-to-reorder list rows in `@kerfjs/ui`.** `ListItem`, `ListItemLink`, and `ListActionRow` take `draggable`, `dragging`, and `dropPosition: 'before' | 'after'`: a draggable row is a native HTML drag source (never while it is a loading placeholder), a dragging row dims, and a drop position draws a 2px brand insertion line on that edge of the row (`--kui-list-item-drop-indicator` / `--kui-list-action-row-drop-indicator` and matching width and dragging-opacity tokens; system `Highlight` in forced colors). The new `@kerfjs/ui/wire-list-reorder` subpath adds `wireListReorder(root, { onReorder })`, which paints those states during a pointer drag, moves rows with Alt+Shift+ArrowUp/ArrowDown and restores focus after the re-render, and reports `{ listId, sourceIds, target: { id, position }, source }`; dragging a selected or pressed row moves every selected row of its `List`. `reorderListItems` applies the change to an array. Apps no longer hand-style dragged rows and insertion lines (`KF-HWYE11`).
|
|
186
|
+
|
|
187
|
+
- **Compact `Toolbar` density.** `Toolbar density="compact"` makes a 40px header for a panel embedded in a card or inspector: a 34px control band (the compact `ToolbarControlGroup` size) with 3px block insets, keeping the 8px inline insets and zone gap. Fill it with `size="compact"` groups. `--kui-toolbar-inset` still overrides the block inset Composite toolbars forward `density` from their `ToolbarConfig` (Workbench panel, main, and bottom toolbars, CollapsiblePanel, NavStack, SplitView) and size the groups they compose (panel and relocated toggles, the back button) to match; `CollapsiblePanelRelocated` takes `density` for its destination toolbar (`KF-0QDF0V`, `KF-QPSTTA`).
|
|
188
|
+
|
|
189
|
+
- **`ChoiceChips` for a wrapping exclusive choice.** `@kerfjs/ui/choice-chips` renders one choice from a visible set too large for a `SegmentedControl` track, such as a nine-color palette in a 560px dialog, as separate pills that wrap with the standard 8px gap at any width. Each pill is backed by a native radio in a named `role="radiogroup"`, so the group is one Tab stop, arrow keys move the selection, and the checked radio submits under `name`. A choice may lead with a `swatch` (a `CssColor`, or `'transparent'` for an outlined dot struck through diagonally) or a decorative `icon`; the group takes `label` or `labelledBy`, a controlled `value`, a delegated `action` for `change`, `size="compact"`, `required`, and group or per-choice `disabled`. Use it instead of wrapping a segmented control or button-appearance `wa-radio` set, whose joined borders turn ragged across rows (`KF-EJPYWD`).
|
|
190
|
+
|
|
191
|
+
- **Viewport-locked app root.** In the opt-in `@kerfjs/ui/document.css` baseline, `data-viewport="locked"` on a body-child `.kui-app-root` gives `html`, `body`, and the root `height: 100dvh` and `overflow: clip`. Mobile browser toolbars can then never leave the root taller than the visible area, and the document itself never scrolls; only the shell's panes do. `docs/document-baseline.md` covers the on-screen keyboard (`KF-1XNQYP`).
|
|
192
|
+
|
|
193
|
+
- **Semantic cursors in `document.css`.** The opt-in `@kerfjs/ui/document.css` baseline now gives native controls semantic cursors with zero specificity: `pointer` on enabled buttons, links, `role="button"`, `summary`, selects, checkbox/radio/button-type inputs, and labels around checkboxes or radios; `not-allowed` on disabled controls, `aria-disabled` role buttons, and a disabled `wa-button`'s `base` part (Web Awesome 3.12 leaves a pointer there); and `text` on text-like inputs and textareas. Components and applications keep their own cursors (`KF-C336VV`).
|
|
194
|
+
|
|
195
|
+
- **`Badge maxWidth`.** A text `Badge` takes `maxWidth` (a `CssLength`). Longer text truncates with an ellipsis inside a new `kui-badge__label`, the badge may shrink below the cap in a tight row, and string or number content becomes the native `title` tooltip unless `title` is supplied (`KF-0WKZY2`).
|
|
196
|
+
|
|
197
|
+
- **`Text align`.** `Text` takes `align="start" | "center" | "end"` (default `start`), so centered status copy that wraps in a narrow surface, or an end-aligned value, uses `Text` directly instead of an app rule on `.kui-text`. Start and middle truncation align their single line to match (`KF-MMAWEG`).
|
|
198
|
+
|
|
199
|
+
- **`SummaryRow`.** `@kerfjs/ui/summary-row` is a selectable, focusable summary of one item (`role="option"`) as a list `row` (bottom divider, `listEdge` rounding for rounded sections) or a board `card`. It has a leading glyph, a semantic `rail`, an identity `title` clamped to `titleLines` (two for rows, four for cards) with `trailingMeta` floated at its end, and a wrapping `metadata` line. Kerf owns hover, focus, `selected`, `dimmed`, narrow compaction, and optional `deferRendering` for long lists (`KF-XCFMX8`).
|
|
200
|
+
- **`Combobox`.** `@kerfjs/ui/combobox` is a free-text field with filtered suggestions that can also create a new value (tags, labels): a native input with the themed field chrome and a 16px font floor on touch devices, on the ARIA combobox pattern (`role="combobox"`, `aria-activedescendant`, a listbox of options). `wireComboboxes(root, { onCommit })` from `@kerfjs/ui/wire-comboboxes` filters as people type, moves the active option with the arrow keys, and reports `{ id, value, created, input }` on Enter, a comma, or a click; `createLabel={false}` allows only suggestions (`KF-XDVAMR`).
|
|
201
|
+
- **File picking and dropping.** `@kerfjs/ui/file-picker` adds `FilePickerTrigger` (a compact, link-toned browse control) and `FileDropZone` (a dashed target that also opens the chooser), both around visually hidden native file inputs with visible focus rings. `wireFilePickers(root, { onFiles })` from `@kerfjs/ui/wire-file-pickers` reports browsed and dropped files, filters drops by `accept` (exported as `fileMatchesAccept`), marks a zone `data-dragging` while files are over it, and resets the input so the same file can be chosen again (`KF-RDV6V9`).
|
|
202
|
+
- **`ContentSwap`.** `@kerfjs/ui/content-swap` swaps two contents in one shared cell (a dialog's two steps, a footer's changing actions) with a `push` (forward or backward, mirrored in right-to-left documents), a `crossfade`, or no motion. Kerf owns the keyframes, the reduced-motion fade, and the inactive side's `inert` and `aria-hidden`; `label` makes it a named region and `slot`/`fill` place it in a dialog label or footer (`KF-V7MZY9`).
|
|
203
|
+
- **`ColumnTrack` for boards.** `@kerfjs/ui/column-track` is a horizontal scroll owner for columns that share the width equally but never narrow below `minColumnWidth` (250px), so a narrow board scrolls instead of crushing them. `layout="paged"` shows one near-full-width column per page with a `peek` of the next, mandatory start snapping, a hidden scrollbar, and smooth scrolling unless reduced motion is preferred (`KF-9WEWRF`).
|
|
204
|
+
- **`EdgeActivityIndicator`.** `@kerfjs/ui/edge-activity-indicator` is a layout-free, pointer-inert activity strip pinned below the top safe area (or flush). While `busy` its thin bars ripple in a traveling wave, drawn by one gradient and an animated mask with no per-bar elements or resize script, and an optional `message` hangs from the edge as a polite status pill. It takes a semantic `tone`, scales in from the edge, and only fades under reduced motion (`KF-CS729Y`).
|
|
205
|
+
- **`ValueTable` layout props.** `labelWidth` (a typed length) gives labels a minimum column and the value the rest of the row; `presentation="stacked"` puts each value under its label, starting at the label's text; and `stackBelow="compact" | "narrow"` stacks only while the table itself is narrower than 512px or 576px (a stacking table fills its container's width). Apps no longer override `--kui-value-table-row-columns` and the row gap/padding tokens for this (`KF-BTMDH5`).
|
|
206
|
+
- **`SplitButton`.** `@kerfjs/ui/split-button` renders a primary action and an attached options trigger on one loud semantic fill (`tone`: brand, neutral, success, warning, danger) with `fill` for full width, `busy` (a spinner and `aria-busy`), and separate `disabled`/`optionsDisabled`. Both halves are native buttons, so the options trigger's `aria-expanded`, `aria-haspopup`, and name reach assistive technology, which a `wa-button-group` could not (`KF-KFC7C2`).
|
|
207
|
+
- **`IconToggleButton` for standalone icon toggles.** `@kerfjs/ui/icon-toggle-button` renders a native `aria-pressed` button with a single contained `ToolbarControlGroup`'s 44px geometry and raised pressed face (pill or rounded), for content and pane footers where a `Toolbar` does not belong and `ToolbarControlGroup`'s toolbar-parent rule (`KUI-L201`) applies. Its required `label` names it and, with help tags registered, shows on hover and focus (`KF-HBWDHE`).
|
|
208
|
+
- **Selected-chrome opt-out for icon-state toggles.** `ToolbarControlGroup` `selectedChrome="none"` keeps pressed controls looking like their neighbors, and `data-selected-chrome="none"` on one button opts out just that control, for a toggle whose glyph already shows its state (an empty or filled star). Hover and focus treatment are unchanged (`KF-GGDABA`).
|
|
209
|
+
- **`FramedList` for inset-grouped rows.** `@kerfjs/ui/framed-list` draws `ValueTable`'s rounded, bordered frame (lowered or base surface) around `FramedListRow`s of arbitrary content, with each row owning its 8px inset, a 44px minimum height, the inset divider above it, and an optional `highlighted` tint. Settings, account, and shortcut lists no longer need a hand-rolled `ul` stylesheet (`KF-2HB7RT`).
|
|
210
|
+
- **ARIA root attributes and a document inset for `List`/`Row`.** `rootAttributes` on a `List` or `Row` now also accepts `aria-*` attributes and a structural `role` (`region`, `group`, `list`, `status`, landmarks, …), so a labeled drop-target region or an `aria-busy` stack needs no wrapper; widget roles are dropped, and a multi-select List keeps owning its grid role and name. `List` `inset="document"` adds a reading inset (24px block, 32px inline, 16px inline below 576px) for reader views (`KF-DNZJCG`).
|
|
211
|
+
- **`Badge` icon, title, and identity.** A text `Badge` takes a decorative leading `icon` (4px before its text, like `Chip`), and every Badge takes a `title` tooltip and safe `data-*` `rootAttributes`, so apps no longer wrap a Badge for status identity or nest a `Row` inside it for icon spacing. A text Badge with a `label` is now a labeled image (`role="img"`), so that name is actually exposed in place of its abbreviated visible text (`KF-2YKKK5`).
|
|
212
|
+
- **`IconButton` for icon-only Lucide buttons.** Web Awesome gives a `wa-button` its square icon-button geometry only when its label holds a lone `wa-icon`, so a button whose only child was a `LucideIcon` kept text-button padding and did not match an adjacent field. `IconButton` (`@kerfjs/ui/icon-button`) renders a `wa-button` with a `LucideIcon` as a square one form-control height on each side, takes a required `label` that names it, and keeps that square beside a field in a `ListInsetControl` (`KF-1315VN`).
|
|
213
|
+
- **`List` surface.** `surface="base"` or `"raised"` paints `--kui-color-surface` or `--kui-color-surface-raised` behind a List and its insets, so a banner strip above a sunken work area no longer needs an app wrapper whose only CSS is a background. A lowered region remains a `SunkenPanel` (`KF-ZKYYH1`).
|
|
214
|
+
- **`CornerStatus` for non-dismissible app-level progress.** The new `@kerfjs/ui/corner-status` component hosts one or more `StateBanner`s at a viewport corner (`placement`: `bottom-end` by default, `bottom-start`, `top-end`, or `top-start`, RTL-aware) with a 16px gutter plus device safe-area insets. It is a native `popover="manual"` in the top layer, so it sits above fixed and z-indexed page chrome, and it has no close button, light dismiss, Escape handling, or focus change. `wireCornerStatus` (`@kerfjs/ui/wire-corner-status`) opens each host as renders insert it; the app removes the host when the work ends. Inside it, a banner drops its inline margin, gains elevation, and paints its tone fill over the opaque surface. `presentation="embedded"` renders the same stack in flow. This replaces fixed-position app CSS around a `StateBanner` and hiding a toast's close button (`KF-PZCPHQ`).
|
|
215
|
+
- **`StateBanner` strip presentation.** `presentation="strip"` renders a full-width status band for a layout edge, such as above a workspace: flush (no inline margin), square, with its copy on the shared content-text column and only tone-colored `dividerSides` edges (bottom by default; `''` draws none). Apps no longer zero the card's margin, radius, padding, and border tokens and draw their own per-tone rule. The default `card` presentation is unchanged (`KF-JQ8FB0`).
|
|
216
|
+
- **Dialogs that fill, and a non-modal dialog surface.**
|
|
217
|
+
- `DialogSurface` gains `height`: a definite modal height bounded by the dynamic viewport minus both gutters, like `maxHeight`. It also gains `bodyLayout="fill"`, which gives the body's whole height to one child (a `SplitView` or `Pane`) that owns its scrolling.
|
|
218
|
+
- The new `PopoverDialogSurface` in `@kerfjs/ui/surface-scaffold` is the non-modal counterpart. It is a native `popover="auto"` `role="dialog"` section that light-dismisses on Escape or an outside press and leaves the page interactive.
|
|
219
|
+
- It owns Kerf's dialog chrome and takes the same `size` / `preferredWidth` / `viewportGutter` / `height` / `maxHeight` / `bodyLayout` props, and it centers in the top layer whatever alignment its host sets. `presentation="embedded"` renders it in flow for catalogs and inline previews.
|
|
220
|
+
- Apps no longer hand-build popovers with their own chrome, viewport math, `:popover-open` grid, and embedded resets (`KF-XHECB9`).
|
|
221
|
+
|
|
222
|
+
- **Sunken surfaces have a fine grain texture.** Every sunken surface paints the new `--kui-sunken-surface`: a seeded 100px monochrome Gaussian grain tile at 4% maximum opacity, over the lowered color. That covers `SunkenPanel`, sunken `Pane`, Workbench regions, TabNavigator scenes, Web Awesome `appearance="sunken"`, and the catalog's lowered specimen viewport. The tile is generated once per page at runtime by the first sunken surface that renders, so no asset ships. `ensureSunkenTexture()` (exported from `@kerfjs/ui` and `@kerfjs/ui/sunken-panel`) covers pages with only Web Awesome sunken surfaces. Set `--kui-sunken-texture: none` at `:root` for the previous flat surface. An app's `--kui-sunken-panel-background` / `--kui-wa-sunken-background` override still wins (`KF-GMAK3H`).
|
|
223
|
+
|
|
224
|
+
- **Required wiring can be root-delegated actions.** Composition catalogs and `kerf.components.json` accept `wiring.actions`, the `data-action` values the app must handle with its own root `delegate()` / `delegateActions()` listener. Required wiring now names at least one helper _or_ action, so an app no longer has to mark such wiring `required: false` and weaken the check. `eslint-plugin-kerfjs`'s `ui-wiring` reports each unhandled action as `KUI-L401`. An action counts as handled when a reachable module imports a Kerf delegation API and names it as a string, selector, or table key. Comments and the component's own source do not count (`KF-AF5KD5`).
|
|
225
|
+
|
|
226
|
+
- **`kerf.components.json` declares where app-owned wiring helpers live.** `composition.wiring.sources` lists each application helper's `{ export, source }`. The generator verifies that the helper is in `helpers`, that the file exists, and that the file exports it. `ui-wiring` then resolves the app's relative imports of that helper, so page-root wiring with helpers such as `wireTicketSearchFields` no longer raises `KUI-L401`. `composition.wiring.scope` (`application` or `module`) is now authorable too. Both fields were previously expressible only in a hand-written catalog (`KF-YZE3DP`).
|
|
227
|
+
|
|
228
|
+
- **`kerf.components.json` zones can name their JSX prop.** A zone may declare `jsx: { prop }` (`children` for the default slot). The prop must be unique per component, and the generated composition catalog keeps it, so `ui-composition` maps a prop's value to its zone exactly as it does for `@kerfjs/ui`'s own catalog (`KF-99Z95S`).
|
|
229
|
+
|
|
230
|
+
- **Context `PopupMenu` owns its anchor.** A context-mode menu takes an `anchor: { x, y }` prop in viewport pixels and renders the point into its own template, so the point survives re-renders and a menu rebuilt with changed items. Open it with the new `openPopupMenu(menu)`; `openPopupMenuAt` still works for menus without `anchor`. The menu's root is now out of flow (`position: fixed`, 0×0). Previously its 0×0 `inline-block` added a line box in block layout and an extra gap slot in a flex row. Apps no longer need a fixed, 1px, z-indexed wrapper, and no z-index is needed because the open menu is in the top layer (`KF-FXGW0C`).
|
|
231
|
+
|
|
232
|
+
- **`SplitView` `listWidth`.** Size the fixed list column with a prop instead of a raw token: a number of CSS pixels, or `{ size, maxFraction }` to cap it at that share of the split's own width (`{ size: 432, maxFraction: 0.5 }` is 432px until the split is narrower than 864px). A narrow dialog then always leaves the detail in view. It is validated, and it throws when combined with `resizable`, which already sizes the list. The guide now states, and browser tests pin, that a SplitView fills any container with a definite height: a block box, a fixed-size popover, a flex column below a header, or a grid. Apps do not need a `display: grid` / `minmax(0, 1fr)` wrapper (`KF-7R5NWM`).
|
|
233
|
+
|
|
234
|
+
- **`Text` truncates at the start or in the middle.** With `wrap="truncate"`, the new `truncate` prop places the ellipsis at the `end` (default), the `start` (keeping the tail, for branch names), or the `middle` (keeping a suffix, for file paths). `truncateKeep` chooses that suffix: the last path segment (default) or a number of trailing characters. Start truncation uses an isolated left-to-right run instead of a bare `direction: rtl`, so punctuation stays in place. Both positions keep the whole value in the DOM and show it as the `title` unless the app sets one. Apps no longer need split spans or rtl CSS for this (`KF-XYA6VR`).
|
|
235
|
+
|
|
236
|
+
- **A trailing overflow menu for `ListActionRow`.** Pass `trailingMenu: { label, icon, items, placement?, disabled? }` instead of the `trailingAction*` props and the row renders a real `PopupMenu` (icon-only, no caret, opening `bottom-end` by default) as its sibling trailing control, so a command row with a "…" menu of Edit and Delete no longer needs a control inside dormant content or an override. The types accept exactly one trailing control. `trailingActionVisibility="interaction"` reveals the menu trigger on hover or focus and keeps it visible while the menu is open; a placeholder row disables it. `openPopupMenuAt` now also opens a button-triggered `PopupMenu` at a pointer position: for that opening the menu anchors at the point and opens `bottom-start`, then returns to its trigger and placement once it closes. A row can therefore open its trailing menu from its own `contextmenu` handler (`KF-VWTCYB`).
|
|
237
|
+
|
|
238
|
+
- **UX Review capture for catalogs served without Vite.** `@kerfjs/ui/vite-ux-review` now also exports the endpoint behind the catalog's **Capture region** link in server-neutral forms: `resolveUxReviewProject({ root?, projectDirectory? })`, `uxReviewProjectResponse(request, projectDirectory)` for Fetch API servers (Hono, Bun, Deno), and `uxReviewMiddleware(projectDirectory)` for Node/connect servers (Express, `node:http`). An app that serves its UX catalog from its own backend mounts one on its development server instead of hand-rolling the route. `uxReviewPlugin()` is now built on the same middleware, with unchanged behavior (`KF-PFF721`).
|
|
239
|
+
|
|
240
|
+
- **A per-entry settings pane in `@kerfjs/ui/catalog`.** Pass an entry's variant and state controls as `Catalog`'s new `settings` prop and the Catalog renders a **Settings** pane beside the stage, with the standard collapse toggle: in the pane's header while open, at the end of the entry toolbar while collapsed. Entries without settings render neither the pane nor the toggle. The app owns `settingsCollapsed`; hand the same signal to `wireCatalog`'s new `settingsCollapsed` option and it flips the signal from the toggle (`onToggleSettings` reports it) and wires the pane as a Workbench rail, so below the `narrow` breakpoint it starts collapsed and opens as an overlay that Escape or an outside press closes, exclusive with the sidebar overlay. `settingsPanel` sets the title, width, resizing, and overlay options (`settingsSize` / `settingsStorageKey` remember a resized width). The UX catalog's Badge entry demonstrates it with a playground. Consumers no longer build their own settings aside and toggle (`KF-0AY6AE`).
|
|
241
|
+
|
|
242
|
+
- **`kerf-component-catalog` generates the selection catalog too.** Packages and apps that keep `kerf.components.json` no longer hand-maintain a second selection JSON (`useWhen`, `avoidWhen`, `publicClasses`, `publicTokens`, …) for `@kerfjs/ui`'s component-catalog extension. Set `package.json#kerfComponentCatalog.selectionOutput`, add a top-level `description` and a per-component `selection` block (`useWhen`, `avoidWhen`, optional `uses`, `styleSources`, `demoSource`, `documentation`, and a `geometry` override), and `--write` / `--check` emit and verify the selection catalog beside the composition catalog. `name`, `kind`, `purpose`, `source`, `publicClasses`, `publicTokens`, and `geometry` come from the existing metadata; the composition catalog's `compatibility.componentCatalog` points at the selection file. The output is validated against the canonical selection schema, which `create-kerf-component` now ships. Recipes are not selection entries, `composed` geometry needs an explicit override, and `uses` must name this package's components or installed `@kerfjs/ui` catalog ids (`KF-K1SY5X`).
|
|
243
|
+
|
|
244
|
+
- **UX Review capture in any `@kerfjs/ui/catalog` app.** The catalog's **Capture region** header link, which starts a UX Review region screenshot preset with the project and the active entry, now ships in the package instead of living in Kerf's own demo. Add `uxReviewPlugin()` from the new Node-only `@kerfjs/ui/vite-ux-review` subpath to the Vite config. Report the project with `wireCatalog`'s new `onUxReviewProject` callback, and pass it to `Catalog`'s new `uxReview` prop. The link appears on local dev and preview servers and stays hidden on static hosting. `uxReviewCaptureUrl` and `loadUxReviewProject` are exported from `@kerfjs/ui/catalog` for custom placements (`KF-XB1G3S`).
|
|
245
|
+
|
|
246
|
+
- Deep-inset Pane content supports `--kui-pane-scrollbar-gutter: stable` for opaque edge-to-edge content, and Pane/Workbench focus rings paint above such scrollers without blocking their controls (`KF-9NVNGT`).
|
|
247
|
+
|
|
248
|
+
- NavStack now has `separator="auto" | "hidden"`. Automatic separators frame a sunken active Pane at the top chrome and optional bottom toolbar; hidden suppresses stack-owned lines (`KF-1XRSRW`).
|
|
249
|
+
|
|
250
|
+
- The UX catalog remembers its background, light or dark theme, contrast, and reduced-motion choices across reloads.
|
|
251
|
+
|
|
252
|
+
- The UX catalog can show a layout guide background with three one-pixel vertical rules at 15px, 23px, and 32px.
|
|
253
|
+
|
|
254
|
+
- `Row`, `Grid`, and `List` accept `gap={{ column, row }}` with independent typed spacing values, while their existing single-value gaps continue to apply to both axes (`KF-8HJFWC`).
|
|
255
|
+
|
|
256
|
+
- `ListHeader` accepts `badge` and `status` together, including explicit `undefined` for either when app state changes; a semantic `count` remains separate.
|
|
257
|
+
|
|
258
|
+
- `Pane`, `SunkenPanel`, plain `TabNavigator` scenes, `Workbench` regions, and static `ContentItem` surfaces can opt into keyboard or programmatic focus; `outlined` holds the standard focus ring for application states such as drop targets. Inactive scenes and collapsed panels suppress their focus stop and outline.
|
|
259
|
+
|
|
260
|
+
- `TabNavigator`, `TabNavigatorTab`, `TabNavigatorProps`, and `wireTabNavigator` name the complete persistent bottom-tab layout. The matching module, wiring, and CSS subpaths are available; former `TabScaffold` names and paths were deprecated aliases in earlier 5.0 betas.
|
|
261
|
+
|
|
262
|
+
- `Pane` exposes its resolved logical inline content insets to child CSS, so a table can paint edge to edge while its cell text stays aligned, with or without `deepInset`.
|
|
263
|
+
|
|
264
|
+
- `Pane` and plain `TabNavigator` scenes accept `deepInset` for a 16px outer content gutter while nested Row, List, and Grid spacing remains at its normal 8px.
|
|
265
|
+
|
|
266
|
+
- The TabNavigator catalog now demonstrates a NavStack with a view-owned top toolbar and Pane above the persistent tab bar, including push, tab switch, and return.
|
|
267
|
+
|
|
268
|
+
- UI Doctor now checks component root class names, stylesheet names, and CSS selectors that reach into another local component.
|
|
269
|
+
|
|
270
|
+
- Configure modal `DialogSurface` viewport gutters and typed maximum heights, including a dynamic viewport cap, without application shadow-part overrides.
|
|
271
|
+
|
|
272
|
+
### Changed
|
|
273
|
+
|
|
274
|
+
- **SVG data URLs are allowed where they load as images.** The URL screen now keeps `data:image/svg+xml` in `<img src>` and in SVG `<image href>` / `<image xlink:href>`, where the browser renders the SVG as an image in secure static mode and runs no script, on the JSX, `html` tagged template, and live-binding paths. Every other sink (`<iframe src>`, `<embed src>`, `<object data>`, `<a href>`, …) still drops it, and no other scheme or `data:` rule changes. Generated SVG artwork no longer needs a `blob:` URL or `raw()`.
|
|
275
|
+
- `@kerfjs/ui` dialog corners are now concentric with a pill `ToolbarControlGroup` (22px) resting at the body inset, in place of Web Awesome's 12px panel radius: 30px by default (a bare `wa-dialog`, `PopoverDialogSurface`, and `DialogSurface` with a `compact` or `none` body inset) and 38px for `bodyInset="comfortable"`. A side sheet's leading corners follow the same radius (`KF-1FNWQG`).
|
|
276
|
+
|
|
277
|
+
- A resizable `@kerfjs/ui` `Workbench` panel without a `label` now names its resize handle in title style, as `ui/docs/writing.md` asks for region names: "Resize Left Rail", "Resize Right Rail", and "Resize Bottom Drawer" (previously "Resize left rail" and so on). A test or locator that matched the old accessible name needs the new one (`KF-NWS9GB`).
|
|
278
|
+
|
|
279
|
+
- `kerf-ui-doctor`'s `KUI-D040` now also checks template literals passed to title-style props, judging only their static words: ``label={`Edit ${name} shortcut`}`` is reported with the suggestion `` `Edit ${name} Shortcut` ``. Interpolated values and words joined to an interpolation are never judged. The UX demos' dynamic control names follow it ("Edit … Shortcut", "Open Ticket …", "New Tab …"), and the NavStack demo's detail item is named by its visible title alone (`KF-8B6JES`).
|
|
280
|
+
|
|
281
|
+
- `kerf-ui-doctor`'s `KUI-D040` now also reads region and landmark names, which `ui/docs/writing.md` makes title style, and raw Web Awesome field labels. The new region names are:
|
|
282
|
+
- the `label` of `Pane`, `Workbench`, `Toolbar`, `ToolbarControlGroup`, `TabBar`, `NavStack`, `CollapsiblePanel`, `FloatingToolbar`, and `ResizableRegion`;
|
|
283
|
+
- the `ariaLabel` of `SunkenPanel` and `List`;
|
|
284
|
+
- a `Pane`'s `contentLabel`;
|
|
285
|
+
- the panel, toolbar, and pane labels in Workbench, CollapsiblePanel, and NavStack configuration;
|
|
286
|
+
- the `aria-label` of `section`, `nav`, `aside`, `main`, `header`, `footer`, `form`, `search`, or an element with a landmark role.
|
|
287
|
+
|
|
288
|
+
The field labels it reads are the `label` of a raw `wa-input`, `wa-select`, `wa-textarea`, or `wa-radio-group`. `wa-checkbox`, `wa-switch`, and `wa-radio` option text, and `LoadingSpinner` and `Skeleton` labels, stay unread. Expect new warnings on lowercase names such as `<Pane label="Workspace pages">` ("Workspace Pages"). The package's own fixtures and examples are fixed. The catalog shell's sidebar navigation is now named "Kerf Components" (it was "Kerf components") (`KF-8JR7EC`).
|
|
289
|
+
|
|
290
|
+
- A panel toolbar's `toggle: { action, name }` (Workbench panels, `CollapsiblePanel`) now documents `name` as the panel's title-style name. Pass `name: "Navigator"`, not `"navigator"`: `name` labels the toggle group and completes the default "Show …" / "Hide …" label, which kerf never recapitalizes. The docs, demos, and recipes passed lowercase names, which produced sentence-style accessible names such as "Show output". They now pass title-style names. The catalog shell names its sidebar "Kerf Catalog" and its settings rail toggle reads "Hide Settings" (they read "Kerf catalog" and "Hide settings"). `KUI-D040` now reports a lowercase `name` in a `toggle` object literal that also has an `action` (`KF-EJ32ZH`).
|
|
291
|
+
- A `NavStack` root view's title now defaults to the `xlarge` page-heading size, and steps down to `large` once a back control shares the bar. An explicit `titleSize` still wins. A Workbench panel's stack keeps `large` at every depth, so narrow rails stay quieter than the work-area title (`KF-2XWXFR`).
|
|
292
|
+
- The built-in close control of a `wa-dialog` or `wa-drawer` now matches a borderless single `ToolbarControlGroup` button, the close a toolbar-composed dialog uses: a 40px circle with no border and the neutral hover fill, instead of Web Awesome's 35px rounded square (`KF-5B5C4W`).
|
|
293
|
+
- A capped, scrolling `Select` listbox, `PopupMenu` menu, or phone-width `PopupMenu` submenu now shows that it scrolls: a soft shadow lines each edge with more choices beyond it. With overlay scrollbars (macOS, iOS, Android), a capped submenu whose bottom edge fell between rows looked like a complete short list. The cue is CSS only, the same on every popup in the shared popup contract, and absent when every choice fits (`KF-B6BTHW`).
|
|
294
|
+
- A centered `NavStack` title (and any lone `ToolbarText` in a `centerAlign="balanced"` center) now keeps the full-strength text color the root title has, instead of dimming to the quieter `large`-size color as it centers (`KF-DX3739`).
|
|
295
|
+
- Once a `NavStack` shows a back control, the view title centers on the toolbar like a platform navigation bar. It stays on the midpoint while the sides leave room, moves away from a wider side when they do not, and truncates with an ellipsis before it reaches the back control or a trailing group. At the root, and on a view that supplies `toolbar.center`, the title stays left-aligned. A lone `ToolbarText` in a `centerAlign="balanced"` toolbar gets this navigation-bar layout in any `Toolbar` (`KF-9YD9KV`).
|
|
296
|
+
- The `NavStack` back control is now a contained, bordered `ToolbarControlGroup` (the default pill), so it reads as a real control instead of a bare chevron beside the title (`KF-TBRP2P`).
|
|
297
|
+
- HTML and attribute escaping in the JSX runtime now runs one pass over a value instead of one `replace` pass per special character, about 2x faster on values that need escaping. Output is unchanged (`KF-3SHRTR`).
|
|
298
|
+
|
|
299
|
+
- Rename the internal TabNavigator implementation folder, files, and focused test files from `tab-scaffold` to `tab-navigator`; public imports and rendered DOM hooks stay stable.
|
|
300
|
+
|
|
301
|
+
- `deepInset` now adds the same 8px to the top and bottom of Pane and plain TabNavigator scroll content, including scroll padding; Pane header and footer still own their safe-area edges.
|
|
302
|
+
|
|
303
|
+
- Toolbar composition guidance and catalog metadata now allow direct `KerfUiContent` in every zone; `ToolbarControlGroup` remains the recommended shared chrome for related controls.
|
|
304
|
+
|
|
305
|
+
- The UI composition lint rule checks component parent requirements for direct content passed through a named zone prop.
|
|
306
|
+
|
|
307
|
+
- Toolbar zones center direct content by default while keeping ToolbarText, control groups, and standalone primary actions on their established control-band axis.
|
|
308
|
+
|
|
309
|
+
### Fixed
|
|
310
|
+
|
|
311
|
+
- Catalog Pane's documented `--kui-pane-scrollbar-gutter` token so downstream doctor and ESLint checks accept the supported setting (`KF-W5VXQF`).
|
|
312
|
+
|
|
313
|
+
- The actionable ToolbarText catalog example uses the standard TokenSearchField toolbar presentation for its search control, and an empty TokenSearchField shows its placeholder.
|
|
314
|
+
|
|
315
|
+
- Preserve toolbar visibility and measurement state across app renders to prevent geometry-driven render loops and repeated observer/layout work (`KF-58HBRR`).
|
|
316
|
+
|
|
317
|
+
- Restore accessible native dialog names through `@kerfjs/ui/surface-scaffold/register`, preserving live title slots and headerless labels.
|
|
318
|
+
|
|
319
|
+
- Validate remote CI freshness through newer-run searches and exact-run details before warning about a failed main build (`KF-X4PTHF`).
|
|
320
|
+
|
|
321
|
+
- Select menus now shift their complete visible surface inside narrow viewport edges while preserving `fitMenu` trigger width (`KF-H4JBQP`).
|
|
322
|
+
|
|
323
|
+
- Toolbar visibility probes stop rewriting unchanged styles, allowing catalog geometry overlays and responsive controls to settle after updates (`KF-JVWC92`).
|
|
324
|
+
|
|
325
|
+
- Busy toolbar groups preserve their relocation annotation, so the inert action and live status remain available when a panel closes.
|
|
326
|
+
|
|
327
|
+
- Navigation stacks now visibly slide pushed views over the previous screen and cross-fade both header and footer across repeated or interrupted push/pop transitions.
|
|
328
|
+
|
|
329
|
+
### Added
|
|
330
|
+
|
|
331
|
+
- Add typed toolbar item `hideBelow`/`showBelow` lengths and `wireToolbarVisibility`, including complementary container-width overflow and dynamic font/token updates (`KF-MXE9YV`).
|
|
332
|
+
|
|
333
|
+
- Expanded grouped search supports `expandedOverflow="visible"` for application-owned surfaces below its positioning anchor.
|
|
334
|
+
|
|
335
|
+
- `@kerfjs/ui` ToolbarControlGroup adds `tileTone` quiet semantic palettes for non-interactive heading icon tiles.
|
|
336
|
+
|
|
337
|
+
- ToolbarText supports `tone="dark"` for filenames, headings, and actionable titles on loud toolbar surfaces.
|
|
338
|
+
- `@kerfjs/ui` Text accepts `controlMargins` for selected exterior control gutters, avoiding a Row wrapper around isolated text.
|
|
339
|
+
|
|
340
|
+
- Make lowered surfaces and decorative neutral fills/borders translucent, and replace the SunkenPanel prototype with direct component compositions.
|
|
341
|
+
|
|
342
|
+
- **UI components share `SemanticTone`.** Badge, Chip, StateBanner, and ContentItem
|
|
343
|
+
use `neutral`, `info`, `pop`, `success`, `warning`, and `danger`. Replace
|
|
344
|
+
Badge/Chip `tone="brand"` with `tone="info"`; the existing brand color tokens
|
|
345
|
+
still supply its palette. `BadgeTone` and `StateBannerTone` are deprecated aliases.
|
|
346
|
+
|
|
347
|
+
- **Catalog sidebars filter by item or heading name.** The shared catalog
|
|
348
|
+
shell now searches primary and ecosystem sections as you type, including
|
|
349
|
+
collapsed groups, and keeps the query when the active preview changes.
|
|
350
|
+
|
|
351
|
+
- **ListHeader has five label sizes.** `size="xs|sm|m|l|xl"` controls visual
|
|
352
|
+
scale separately from the semantic heading level; `sm` preserves the existing
|
|
353
|
+
uppercase treatment and `FieldLabel` remains unchanged.
|
|
354
|
+
|
|
355
|
+
- **The pinned TabBar catalog example scrolls with native momentum.** It no
|
|
356
|
+
longer reserves a long blank strip after the final tab; applications can
|
|
357
|
+
still opt into whole-tab settling with `snapTabs`.
|
|
358
|
+
|
|
359
|
+
- **Doctor reports foreign CSS restyles of co-owned classes.** A stylesheet
|
|
360
|
+
outside every co-owner now receives one `KUI-L019` per subject rule, including
|
|
361
|
+
inferred BEM modifiers, with the co-owner set in the diagnostic evidence.
|
|
362
|
+
|
|
363
|
+
- **Doctor respects explicit ownership groups with implicit ownership enabled.**
|
|
364
|
+
A group's stylesheet remains owned by the group even when one of its source
|
|
365
|
+
modules, or an unrelated module, imports that CSS directly.
|
|
366
|
+
|
|
367
|
+
- **LucideIcon supports filled glyphs.** Pass `appearance="solid"` to use the
|
|
368
|
+
icon's current text color as its fill; outline remains the default.
|
|
369
|
+
|
|
370
|
+
- **Doctor detects borrowed component markup.** Source modules that place a
|
|
371
|
+
foreign public class in JSX, raw HTML, or DOM class writes now receive
|
|
372
|
+
`KUI-L023`; documented placeable classes remain allowed.
|
|
373
|
+
|
|
374
|
+
- **Doctor offers strict same-package selector context checks.**
|
|
375
|
+
`ownershipContext: "any"` reports a sibling component's class even when it is
|
|
376
|
+
only used as selector context; cross-package composition stays available.
|
|
377
|
+
|
|
378
|
+
- **Doctor catches classless descendants of composed components.** Component
|
|
379
|
+
mode uses JSX evidence to identify element selectors below component hooks,
|
|
380
|
+
sibling roots, and direct composed child roots.
|
|
381
|
+
|
|
382
|
+
- **Doctor component ownership recognizes source evidence.** Selection-only
|
|
383
|
+
components and literal BEM classes rendered by a component are protected
|
|
384
|
+
when its stylesheet styles their block. CSS imports can supply the owned
|
|
385
|
+
stylesheet, and uncataloged modules can opt into the same inference.
|
|
386
|
+
|
|
387
|
+
- **`createScope().dispose` is typed as a bound function property.** Consumers
|
|
388
|
+
can store or return it as a teardown without a wrapper or an unbound-method
|
|
389
|
+
lint suppression.
|
|
390
|
+
|
|
391
|
+
- **ToolbarControlGroup grow sizing keeps its basis in wide toolbars.** An
|
|
392
|
+
expanded grow search now contributes its minimum width to the trailing zone's
|
|
393
|
+
intrinsic size, keeping sibling groups beside it instead of wrapping early.
|
|
394
|
+
|
|
395
|
+
- **TabBar can settle overflowing peers at whole-tab starts.** Opt into
|
|
396
|
+
`snapTabs` beside a pinned leading AppTab; selection and scroll gestures
|
|
397
|
+
align peers after the pin in LTR and RTL, with enough end room for the final
|
|
398
|
+
tab instead of leaving a clipped sliver at rest.
|
|
399
|
+
|
|
400
|
+
- **The pre-push check no longer adds timing metadata to Hot Sheet tickets.**
|
|
401
|
+
It still runs the repository gate or skips an identical verified clean tree;
|
|
402
|
+
historical timing notes remain readable through `ticket:timing summary`.
|
|
403
|
+
|
|
404
|
+
- **Wrapping catalog resource groups align with the toolbar control band.** A
|
|
405
|
+
single row centers in the band, while multiple rows start at its top.
|
|
406
|
+
|
|
407
|
+
- **UI browser checks cover the current catalog specimens.** Select, toolbar,
|
|
408
|
+
tab, and control group browser assertions now address the expanded demos;
|
|
409
|
+
a focused toolbar search field restores its default surface color when no
|
|
410
|
+
background override is set.
|
|
411
|
+
|
|
412
|
+
- **Pinned tabs cover the full leading edge of their scroll strip.** The
|
|
413
|
+
backing now includes the strip border as well as its padding, preventing a
|
|
414
|
+
sliver of a scrolling tab label from showing beside the pinned tab.
|
|
415
|
+
|
|
416
|
+
- **Catalog resource links remain readable at phone widths.** The resource
|
|
417
|
+
group wraps whole actions onto another row instead of clipping the Guidance
|
|
418
|
+
label at the right edge; `ToolbarControlGroup overflow="wrap"` is available
|
|
419
|
+
for the same independent-link pattern.
|
|
420
|
+
|
|
421
|
+
- **The UI analyzer resolves application component imports through TypeScript
|
|
422
|
+
path aliases.** `KUI-L022` now catches a styled hook class when an imported
|
|
423
|
+
cataloged component uses an exact or wildcard `paths` mapping; Doctor uses
|
|
424
|
+
the same resolution.
|
|
425
|
+
|
|
426
|
+
- **TabBar accepts standalone Web Awesome actions in `trailing` and `end`.**
|
|
427
|
+
A single `wa-button` can sit beside the scrolling tabs or at the far edge
|
|
428
|
+
without ToolbarControlGroup chrome; the composition rule recognizes the
|
|
429
|
+
cataloged custom element in these zones.
|
|
430
|
+
|
|
431
|
+
- **FieldLabel matches form-control labels over field previews.** The
|
|
432
|
+
`@kerfjs/ui/text` export gives a read-only field the same label typography
|
|
433
|
+
and inset as a themed Web Awesome input, with an id for accessible grouping.
|
|
434
|
+
|
|
435
|
+
- **TokenSearchField form fields match adjacent Web Awesome inputs.** The
|
|
436
|
+
control uses the form-control radius, and its required marker uses the
|
|
437
|
+
same neutral color and offset tokens as `wa-input`.
|
|
438
|
+
|
|
439
|
+
- **Grid supports labeled multi-select ContentItem tiles.** Opt into
|
|
440
|
+
`selectionMode="multiple"` with `ariaLabel`; `wireContentItems` follows the
|
|
441
|
+
rendered columns with four arrow keys and keeps nested controls independent.
|
|
442
|
+
|
|
443
|
+
- **Collapsible TokenSearchField can fill a stretched Toolbar center.** The
|
|
444
|
+
`fill` prop expands the field and its search group to the available width
|
|
445
|
+
while preserving the compact collapsed action.
|
|
446
|
+
|
|
447
|
+
- **TokenSearchField has a typed trailing icon action.** `trailingAction`
|
|
448
|
+
renders an accessible button with Kerf owned chrome and a full 24px wide hit
|
|
449
|
+
target; the existing `trailing` content slot remains for passive content.
|
|
450
|
+
|
|
451
|
+
- **PopupMenu and registered PopupSurface dropdowns preserve fast keyboard
|
|
452
|
+
navigation during opening.** Item focus gained while Web Awesome animates
|
|
453
|
+
the menu is restored after its initial focus handoff, so Enter acts on the
|
|
454
|
+
item the user reached.
|
|
455
|
+
|
|
456
|
+
- **PopupMenu submenu checkmarks stay inside the menu surface.** Checked
|
|
457
|
+
submenu rows align their checkmark beside the label instead of letting the
|
|
458
|
+
Web Awesome negative check margin paint beyond the submenu border.
|
|
459
|
+
|
|
460
|
+
- **Filled SegmentedControl labels keep contrast while switching themes.**
|
|
461
|
+
Web Awesome's native button color transition no longer interpolates the
|
|
462
|
+
label against the already-switched track fill.
|
|
463
|
+
|
|
464
|
+
- **Workbench numeric overlays no longer restyle the full work area on unrelated changes.**
|
|
465
|
+
The overlay wiring mirrors active presentation and open state onto the Workbench
|
|
466
|
+
root for CSS, and avoids layout measurements when no wired panel uses a numeric
|
|
467
|
+
breakpoint.
|
|
468
|
+
|
|
469
|
+
## [5.0.0] - 2026-09-29
|
|
470
|
+
|
|
471
|
+
- **`ui-public-boundaries` inspects `kerfjs/html` tagged templates.** The
|
|
472
|
+
no-build `html` template writes the same markup as JSX, so its static
|
|
473
|
+
`class` attributes now carry `KUI-L101` (private `kui-*` anatomy) and
|
|
474
|
+
`KUI-L103` (a component's anatomy on your own element):
|
|
475
|
+
``html`<aside class="kui-pane">` `` is reported with `Pane` to render, and a
|
|
476
|
+
plain ``html`<div class="kui-content-item">` `` with `ContentItem`, while a
|
|
477
|
+
`<ul>` carrier keeps the placeable class. The template's static parts are
|
|
478
|
+
read by the runtime's own rules; a class a hole supplies (`class=${cls}`)
|
|
479
|
+
is unknown and skipped.
|
|
480
|
+
|
|
481
|
+
- **`KUI-L103`'s "render X instead" answer has one implementation.**
|
|
482
|
+
`@kerfjs/ui` ships `ai/component-class-owners.cjs`, the rule that decides
|
|
483
|
+
which cataloged classes are a component's anatomy and which export renders
|
|
484
|
+
each. `eslint-plugin-kerfjs`'s `ui-public-boundaries` loads it beside
|
|
485
|
+
`application-ui-profile-sync.cjs`, and the generated `ai/components/*.md`
|
|
486
|
+
pages use it too, so the lint message and the component reference can no
|
|
487
|
+
longer name different components (a test pins their agreement over the whole
|
|
488
|
+
shipped catalog). A vendored `profileContractPath` directory must now also
|
|
489
|
+
hold `component-class-owners.cjs`; an `@kerfjs/ui` without it reports
|
|
490
|
+
`KUI-L090`.
|
|
491
|
+
|
|
492
|
+
- **A delegated click no longer re-fires on a target the re-render recycled.**
|
|
493
|
+
A delegated handler that writes a signal makes `mount()` re-render
|
|
494
|
+
synchronously, inside the same event dispatch; when the morph recycled the
|
|
495
|
+
clicked element in place into a control with a different `data-action`,
|
|
496
|
+
delegates registered later for the same event matched the recycled element
|
|
497
|
+
and fired too (a `request` click immediately ran `cancel` and undid itself).
|
|
498
|
+
`delegate()`, `delegateCapture()`, and `kerfjs/actions`' `delegateActions()`
|
|
499
|
+
now resolve every delegate's match against the DOM as it stood when the
|
|
500
|
+
event reached the first kerf delegate listener, like per-element listeners:
|
|
501
|
+
a delegate that matched at dispatch start still fires (unless an earlier
|
|
502
|
+
handler removed its element), one that matches only after a re-render does
|
|
503
|
+
not, and `delegateActions()` dispatches by the action value the element
|
|
504
|
+
carried at dispatch start. `mount()` stays synchronous. The
|
|
505
|
+
wrap-it-in-a-distinct-element workaround is no longer needed. Cost: about
|
|
506
|
+
0.11 KB min+gzip, which moves the advertised core size from ~12 KB to
|
|
507
|
+
~13 KB.
|
|
508
|
+
|
|
509
|
+
- **Component packages declare `placeableClasses`, and `KUI-L103` covers
|
|
510
|
+
their anatomy classes.** `create-kerf-component`'s `kerf.components.json`
|
|
511
|
+
accepts optional `boundaries.placeableClasses` (a subset of
|
|
512
|
+
`publicClasses`) and `boundaries.rootElement`, validates them, and copies
|
|
513
|
+
them into the generated `component-composition.json`.
|
|
514
|
+
`eslint-plugin-kerfjs`'s `ui-public-boundaries` now checks every class the
|
|
515
|
+
application or third-party catalogs in `.kerf-ui-profile.json` declare, not
|
|
516
|
+
only `kui-*` names: `<div class="acme-meter">` is reported with the
|
|
517
|
+
package's component to render (`Meter`), while its placeable classes stay
|
|
518
|
+
allowed. The scaffold template's README explains the field, and
|
|
519
|
+
`@kerfjs/ui/ai/skill.md` (1.50.0) applies the rule to package entries.
|
|
520
|
+
|
|
521
|
+
- **`eslint-plugin-kerfjs` `ui-public-boundaries` reports a plain
|
|
522
|
+
`<div class="kui-content-item">` as `ContentItem` recreated (`KUI-L103`).**
|
|
523
|
+
The `kui-content-item` classes stay placeable on another carrier (a `<ul>`, a
|
|
524
|
+
`Text`, a control-cluster `<footer>`), but a plain `<div>` carrying them is
|
|
525
|
+
exactly what `ContentItem` renders; the diagnostic now says to render
|
|
526
|
+
`ContentItem`. The `@kerfjs/ui` catalogs gain an additive `rootElement`
|
|
527
|
+
field (selection entry and composition `boundaries.rootElement`) naming the
|
|
528
|
+
element a component renders around its own placeable classes, and
|
|
529
|
+
`check:guidance` derives its repository check from the same fields, so the
|
|
530
|
+
package check and the downstream diagnostic cannot drift. The rule also
|
|
531
|
+
inspects the static class names of a template-literal `class` /
|
|
532
|
+
`className` value.
|
|
533
|
+
|
|
534
|
+
- **`eslint-plugin-kerfjs` `ui-public-boundaries` reports a component's
|
|
535
|
+
anatomy class written onto an application-owned element (`KUI-L103`).**
|
|
536
|
+
`<div class="kui-toolbar">`, `<aside class="kui-pane">`, or
|
|
537
|
+
`<wa-dropdown class="kui-popup-menu">` recreates a component by class and
|
|
538
|
+
inherits (and invites restyling of) its CSS; the diagnostic names the
|
|
539
|
+
component to render instead (`Toolbar`, `Pane`, `PopupMenu`). The
|
|
540
|
+
`@kerfjs/ui` catalogs gain `placeableClasses` (selection entry field and
|
|
541
|
+
composition `boundaries.placeableClasses`) naming the public classes an
|
|
542
|
+
application may place itself — the layout utilities, `kui-app-root`, the
|
|
543
|
+
`kui-content-item` geometry on a non-div carrier, and Web Awesome
|
|
544
|
+
`hide-actions` — and `check:catalog` requires every class-only entry to list
|
|
545
|
+
its classes there. A component's own element (`<Toolbar className="kui-toolbar">`)
|
|
546
|
+
is not reported. The README pane example and `@kerfjs/ui/ai/skill.md`
|
|
547
|
+
(1.47.0) now render `Pane` instead of writing its classes, and each generated
|
|
548
|
+
`ai/components/*.md` page states which classes are placeable and which
|
|
549
|
+
component renders the rest.
|
|
550
|
+
|
|
551
|
+
- **`kerf-ui-analyze` retires `KUI-L005` ("forced component dimension").**
|
|
552
|
+
`KUI-L019` already reports any application rule whose subject is a cataloged
|
|
553
|
+
component, dimensions included, so `.kui-pane { width: 300px }` was reported
|
|
554
|
+
twice (an error and a review finding). L005's only other shape, a public
|
|
555
|
+
class as an ancestor (`.kui-pane .app-sidebar { width: 240px }`), sizes the
|
|
556
|
+
application's own element and is allowed. The analyzer no longer emits it;
|
|
557
|
+
the id stays in the diagnostic registry (now listing it under `retired`) so
|
|
558
|
+
existing profile exceptions that name it still load.
|
|
559
|
+
|
|
560
|
+
- **`KUI-L301` now reports every `Discouraged` Web Awesome element rendered
|
|
561
|
+
directly.** The `@kerfjs/ui` package-default application profile avoids all
|
|
562
|
+
fifteen Discouraged entries (Dropdown/Dropdown Item → `PopupMenu`,
|
|
563
|
+
Select/Option → `Select`, Tab Group/Tab/Tab Panel → `TabBar`, Icon →
|
|
564
|
+
`LucideIcon`, Split Panel → `ResizableRegion`, Button Group →
|
|
565
|
+
`SegmentedControl`, Tree/Tree Item → `List`, and Animated Image, Comparison,
|
|
566
|
+
and Zoomable Frame → `ContentItem`), each with a rationale, and
|
|
567
|
+
`eslint-plugin-kerfjs` `ui-preferences` (and `kerf-ui-doctor`, which runs it)
|
|
568
|
+
now resolves a custom-element JSX tag such as `<wa-dropdown>` to its catalog
|
|
569
|
+
entry, not only an imported component. A declared component's own source
|
|
570
|
+
stays exempt because it is the wrapper that owns the element. A test keeps
|
|
571
|
+
the profile's avoid set equal to the catalog's Discouraged set in both
|
|
572
|
+
directions and lints the UX demo, recipes, and catalog shell clean;
|
|
573
|
+
`@kerfjs/ui/ai/skill.md` (1.46.0) names the diagnostic.
|
|
574
|
+
|
|
575
|
+
- **New (`@kerfjs/ui`): `NavStack` and `TabScaffold` take `chromeDividers`
|
|
576
|
+
(`'scroll' | 'always' | 'none'`), like `Pane`.** `scroll` (the default) keeps
|
|
577
|
+
today's behavior: the stack's top-chrome and bottom-toolbar lines and the
|
|
578
|
+
scaffold's bar line follow the scroll once `wireScrollDividers` is installed.
|
|
579
|
+
`always` draws them permanently without the wiring; `none` never draws them,
|
|
580
|
+
even when wired. The top `Toolbar`'s own `toolbarConfig.dividerSides` is
|
|
581
|
+
unaffected. `SplitView` forwards it to its compact stack through
|
|
582
|
+
`compactStack.chromeDividers`.
|
|
583
|
+
|
|
584
|
+
- **Fix (`@kerfjs/ui`): a `Pane` that is the only content of a `NavStack` view
|
|
585
|
+
or a `TabScaffold` scene fills it.** It used to size to its content, so the
|
|
586
|
+
view scrolled the whole Pane and its header scrolled away. Now the Pane's own
|
|
587
|
+
content scrolls under its pinned header and footer, the Pane's slots take the
|
|
588
|
+
safe-area edges the view or scene still reaches, and scroll dividers key on
|
|
589
|
+
the Pane's content. The rule lives in `pane.css`, as the Workbench-region fill
|
|
590
|
+
does.
|
|
591
|
+
|
|
592
|
+
- **The `@kerfjs/ui` suite-v3 AI-regression protocol test runs in every gate
|
|
593
|
+
again.** `ui/tests/integration/ai-regressions-v3.test.ts` was selected by no
|
|
594
|
+
package script, so neither `npm --prefix ui run check` nor CI executed it. It
|
|
595
|
+
now runs as `test:ai-regressions` inside the package's `npm test`, and a new
|
|
596
|
+
unit guard fails when any file under `ui/tests` is selected by no script
|
|
597
|
+
reachable from `check` or `test:e2e`.
|
|
598
|
+
|
|
599
|
+
- **Importing `@kerfjs/ui/catalog` or `@kerfjs/ui/workbench` now ships every
|
|
600
|
+
stylesheet those composites render.** `catalog`, `workbench`, `nav-stack`,
|
|
601
|
+
`split-view`, `tab-scaffold`, and `collapsible-panel` gain a CSS-aware
|
|
602
|
+
`browser` export condition whose generated wrapper imports the foundation, the
|
|
603
|
+
CSS of each internally rendered component (Pane, Toolbar, ToolbarText, List,
|
|
604
|
+
ListItem, ListInsetText, Row, PopupMenu, …) in dependency order, and the
|
|
605
|
+
composite's own CSS. Apps no longer hand-import `pane.css`, and a Catalog
|
|
606
|
+
sidebar's styling no longer depends on whether the app happens to import
|
|
607
|
+
`ListInsetText` elsewhere. The list is derived from the source import graph
|
|
608
|
+
(now following parent-relative imports), the `import` condition stays
|
|
609
|
+
CSS-free, and the companion `.css` subpaths remain for pipelines without the
|
|
610
|
+
`browser` condition. `@kerfjs/ui/ai/skill.md` is 1.46.0.
|
|
611
|
+
|
|
612
|
+
- **The `@kerfjs/ui` AI guidance now states every maintainer design rule
|
|
613
|
+
explicitly.** An audit of the package's tickets folded the missing rules into
|
|
614
|
+
`@kerfjs/ui/ai/skill.md` (1.45.0), `llms.txt`, and the design docs: prefer a
|
|
615
|
+
Kerf UI component over a Web Awesome component over a raw tag and never render
|
|
616
|
+
a `Discouraged` Web Awesome element directly (only through the Kerf component
|
|
617
|
+
that wraps it); nothing wraps inside a `ToolbarControlGroup` and a
|
|
618
|
+
`ToolbarText` is one ellipsized line unless `wrap`/`maxLines` is set; a
|
|
619
|
+
`Select` or `PopupMenu` trigger drops its focus ring while its popup is open
|
|
620
|
+
and the ring follows the control's shape (never add ring CSS); dividers earn
|
|
621
|
+
their place (`dividerSides=""` over self-spacing content or between a pane's
|
|
622
|
+
header and its list, never padded around); a control sits near where its
|
|
623
|
+
effect appears; and text aligns to text and icons to icons — a border is
|
|
624
|
+
never the alignment anchor. The ToolbarControlGroup catalog entry no longer
|
|
625
|
+
points at a hand-written `wa-dropdown` trigger, and the bundled kerf app
|
|
626
|
+
configs (`ai/skill.md` 1.25.0, `ai/cursorrules` 1.23.0) name the List family
|
|
627
|
+
and carry the "components own their styles; configure, never override" rule.
|
|
628
|
+
|
|
629
|
+
- **Applications and component packages are held to the component-ownership
|
|
630
|
+
rules `@kerfjs/ui` enforces internally: components own their styles and are
|
|
631
|
+
configured, never overridden.** `kerf-ui-analyze` and `kerf-ui-doctor` add
|
|
632
|
+
four error diagnostics (review findings under `--adoption`):
|
|
633
|
+
- `KUI-L019` — a CSS rule whose subject is another package's cataloged
|
|
634
|
+
component: a public class (`.kui-toolbar`, `.acme-meter`), its
|
|
635
|
+
`[data-component]` root, or a Web Awesome tag the package themes
|
|
636
|
+
(`wa-button`). Your own element in a component's context
|
|
637
|
+
(`.kui-toolbar > .my-widget`), keying on its state inside `:has()`, and
|
|
638
|
+
setting its public tokens stay allowed.
|
|
639
|
+
- `KUI-L020` — reading or writing another component's private `--_*`
|
|
640
|
+
variable (`--_kui-list-gap`).
|
|
641
|
+
- `KUI-L021` — overriding a token a typed prop sets (`--kui-list-gap` is
|
|
642
|
+
`<List gap>`); the message quotes the prop's example.
|
|
643
|
+
- `KUI-L022` — a class placed on a component's root (`<Toolbar
|
|
644
|
+
className="header" />`) that the application then styles.
|
|
645
|
+
|
|
646
|
+
Each message names the component's configuration (typed props, public
|
|
647
|
+
tokens, variants) and ends by routing a need with no prop to a component-gap
|
|
648
|
+
report instead of an override. Ownership is per package, from the nearest
|
|
649
|
+
`package.json`, so a component package styling its own components — including
|
|
650
|
+
in a Kerf UI parent's context — passes, and third-party catalogs declared in
|
|
651
|
+
`.kerf-ui-profile.json` get the same protection. `eslint-plugin-kerfjs` adds
|
|
652
|
+
`ui-component-ownership` (in `recommended-ui` and `strict-ui`, `error`),
|
|
653
|
+
reporting `KUI-L020` / `KUI-L021` in style strings, style objects, and
|
|
654
|
+
`style.setProperty()`. The `create-kerf-component` scaffold now ships a
|
|
655
|
+
`counter.css` that styles only Counter (configured by a typed `size` prop and
|
|
656
|
+
a `--kerf-counter-gap` token, with context named after Counter and a
|
|
657
|
+
Toolbar-context rule in its own stylesheet), plus `npm run check:styles`
|
|
658
|
+
(`kerf-ui-analyze`, from a new `@kerfjs/ui` dev dependency) in
|
|
659
|
+
`prepublishOnly`.
|
|
660
|
+
|
|
661
|
+
- **Behavior change (`@kerfjs/ui`): `NavStack` and `TabScaffold` chrome
|
|
662
|
+
dividers follow the scroll.** The `NavStack` bottom toolbar's and the
|
|
663
|
+
`TabScaffold` bar's top borders are no longer permanent. With `wireScrollDividers`
|
|
664
|
+
installed, a `NavStack`'s top chrome draws a line along its bottom edge only
|
|
665
|
+
while the active view's content is scrolled beneath it, its bottom toolbar a
|
|
666
|
+
line along its top edge only while more content lies below, and a
|
|
667
|
+
`TabScaffold` bar a line along its top edge only while the active scene's
|
|
668
|
+
content continues below it — none when the content fits. The line follows
|
|
669
|
+
the active view across a push or pop and the active scene across a tab
|
|
670
|
+
switch, and keys on whichever element actually scrolls: the view or scene
|
|
671
|
+
itself, or, through a sole child with no chrome of its own on that edge, a
|
|
672
|
+
`Pane`'s content or a nested `NavStack`'s active view (a sole `Pane`'s own
|
|
673
|
+
header or footer draws that boundary instead). Each layout draws the line
|
|
674
|
+
from its own stylesheet (an overlaid pseudo-element on the stack's chrome; the
|
|
675
|
+
bottom toolbar and bar keep their 1px top border, transparent until shown),
|
|
676
|
+
so nothing moves; the wiring
|
|
677
|
+
writes only its `data-scroll-overflow` / `data-scroll-divider` state. Without
|
|
678
|
+
the wiring those edges draw no line.
|
|
679
|
+
|
|
680
|
+
- **Behavior change (`@kerfjs/ui`): toolbars no longer draw a divider by
|
|
681
|
+
default; dividers between pinned chrome and scrolling content follow the
|
|
682
|
+
scroll.** `Toolbar`'s `dividerSides` now defaults to `''` (was `'b'`), and
|
|
683
|
+
the `Workbench` (`mainToolbar`, `mainBottomToolbar`, panel toolbars, and the
|
|
684
|
+
`mainHeader` / `mainFooter` lists), `CollapsiblePanel`, and `Catalog`
|
|
685
|
+
toolbars no longer place dividers of their own. Instead a `Pane` draws one
|
|
686
|
+
line under its header and one over its footer, wherever that chrome ends:
|
|
687
|
+
the header's only while content is scrolled beneath it, the footer's only
|
|
688
|
+
while more content lies below, and neither when the content fits. The new
|
|
689
|
+
`@kerfjs/ui/wire-scroll-dividers` subpath's `wireScrollDividers(root, {
|
|
690
|
+
targets? })` reports that state — call it once at the app root and keep its
|
|
691
|
+
disposer (`wireCatalog` installs it for its own root). It also marks each
|
|
692
|
+
side of a `TabBar` strip with tabs scrolled out of view, and lets a
|
|
693
|
+
`Toolbar` or `List` named as chrome in `targets` draw the divider facing an
|
|
694
|
+
app-owned scroller. The wiring only writes the `data-scroll-overflow` /
|
|
695
|
+
`data-scroll-divider` state attributes; each component draws its own line,
|
|
696
|
+
and no state moves the chrome, the content, or a tab. `Pane` gains
|
|
697
|
+
`chromeDividers` (`scroll` default, `always`, `none`; also in `PaneConfig`,
|
|
698
|
+
so `mainPane` and a panel's `pane` forward it) and TabBar the
|
|
699
|
+
`--kui-tab-bar-divider-color` token. To keep a permanent line, set
|
|
700
|
+
`chromeDividers: "always"` on the pane or an explicit `dividerSides`.
|
|
701
|
+
|
|
702
|
+
- **`@kerfjs/ui`: `Pane`, `NavStack`, `SplitView`, and `TabScaffold` fill a
|
|
703
|
+
Workbench region from their own stylesheets.** As a Workbench main area's
|
|
704
|
+
or panel's only child, each sets its own `height: 100%` in that context,
|
|
705
|
+
which the Workbench marks on its regions (`data-workbench-main`, and the new
|
|
706
|
+
`data-workbench-panel-content` on each panel's content); `workbench.css` no
|
|
707
|
+
longer sizes another component's root (it still fills a nested Workbench).
|
|
708
|
+
Rendering is unchanged.
|
|
709
|
+
|
|
710
|
+
- **`@kerfjs/ui`: layouts name the covered-floating-controls context after
|
|
711
|
+
themselves, and no longer write `FloatingToolbar`'s inset token.** Workbench,
|
|
712
|
+
CollapsiblePanel, and ResizableRegion each provide their own private
|
|
713
|
+
`--_kui-<layout>-covered` context in place of the shared
|
|
714
|
+
`--_kui-floating-covered` flag; `FloatingToolbar` and every restore corner
|
|
715
|
+
read all three. A restore corner no longer sets
|
|
716
|
+
`--kui-floating-toolbar-inset: 0px`; `FloatingToolbar` zeroes its own inset
|
|
717
|
+
inside a restore corner (a toolbar's own `inset` still wins). Rendering is
|
|
718
|
+
unchanged. One edge case changes: a floating control inside an open overlay
|
|
719
|
+
of one layout type that is itself covered by another layout type's open
|
|
720
|
+
overlay (for example, a ResizableRegion overlay in a Workbench work area
|
|
721
|
+
under an open rail overlay) now stays hidden.
|
|
722
|
+
|
|
723
|
+
- **`@kerfjs/ui`: `ListHeader`, `NavStack`, `Select`, and `StateBanner` no
|
|
724
|
+
longer restyle a composed child through a class on its root.** Each child
|
|
725
|
+
now styles itself: `Text` sets a `ListHeader` label from `text.css`, `Badge`
|
|
726
|
+
spaces a `Select` count and a `StateBanner` badge from `badge.css`, and a
|
|
727
|
+
`NavStack` title uses `ToolbarText`'s new `fill` option. Rendering is
|
|
728
|
+
unchanged. `ToolbarText` gains `fill` (grow into the free space of its
|
|
729
|
+
toolbar zone and truncate there). The `kui-list-header__label`,
|
|
730
|
+
`kui-nav-stack__title`, `kui-select__count`, and `kui-state-banner__badge`
|
|
731
|
+
classes are removed; select a ListHeader label with
|
|
732
|
+
`.kui-list-header__title > .kui-text` and a NavStack title with
|
|
733
|
+
`.kui-toolbar-text[data-fill="true"]` if a test needs to find them.
|
|
734
|
+
|
|
735
|
+
- **`@kerfjs/ui` ships a markdown component reference for AI assistants.**
|
|
736
|
+
`@kerfjs/ui/ai/components/README.md` indexes every catalog entry in one line,
|
|
737
|
+
and `@kerfjs/ui/ai/components/<id>.md` gives each entry a short page: when to
|
|
738
|
+
use and avoid it, imports and CSS/registration side effects, a link to its
|
|
739
|
+
exact props, legal parents and zones (JSX prop and child count), app- and
|
|
740
|
+
wiring-owned state, geometry, accessibility obligations, its styling boundary
|
|
741
|
+
(configure through props; do not override internals), and its diagnostics.
|
|
742
|
+
`llms.txt` and the shipped skill now send assistants there instead of to the
|
|
743
|
+
263 KB `component-catalog.json` and 221 KB `component-composition.json`,
|
|
744
|
+
which remain the canonical sources for tools. The pages are generated from
|
|
745
|
+
those two files and a package check fails when they drift.
|
|
746
|
+
|
|
747
|
+
- **Breaking (`@kerfjs/ui` and `create-kerf-component`): the composition
|
|
748
|
+
catalog files are renamed so they no longer read as a "v2" of the component
|
|
749
|
+
catalog.** The composition catalog is a generated composition layer beside
|
|
750
|
+
`component-catalog.json`, not a newer version of it. Update any import,
|
|
751
|
+
`$schema`, `$ref`, or `.kerf-ui-profile.json` catalog location that names an
|
|
752
|
+
old path:
|
|
753
|
+
|
|
754
|
+
| Old path | New path |
|
|
755
|
+
| -------------------------------------------------------------- | --------------------------------------------------------------- |
|
|
756
|
+
| `@kerfjs/ui/ai/component-catalog-v2.json` | `@kerfjs/ui/ai/component-composition.json` |
|
|
757
|
+
| `@kerfjs/ui/ai/component-catalog-v2.schema.json` | `@kerfjs/ui/ai/component-composition.schema.json` |
|
|
758
|
+
| `@kerfjs/ui/ai/component-catalog-v2.d.ts` | `@kerfjs/ui/ai/component-composition.d.ts` |
|
|
759
|
+
| `@kerfjs/ui/ai/component-catalog-v2-overrides.json` | `@kerfjs/ui/ai/component-composition-rules.json` |
|
|
760
|
+
| `@kerfjs/ui/ai/component-catalog-extension-v2.schema.json` | `@kerfjs/ui/ai/component-composition-extension.schema.json` |
|
|
761
|
+
| `@kerfjs/ui/ai/component-catalog-extension-v2.d.ts` | `@kerfjs/ui/ai/component-composition-extension.d.ts` |
|
|
762
|
+
| `@kerfjs/ui/docs/examples/component-catalog-extension-v2.json` | `@kerfjs/ui/docs/examples/component-composition-extension.json` |
|
|
763
|
+
| scaffolded package `component-catalog-v2.json` | scaffolded package `component-composition.json` |
|
|
764
|
+
|
|
765
|
+
The exported types follow: `ComponentCatalogV2` → `ComponentComposition`,
|
|
766
|
+
`CatalogCompositionEntryV2` → `CatalogCompositionEntry`,
|
|
767
|
+
`ConsumerComponentCatalogV2` → `ConsumerComponentComposition`, and
|
|
768
|
+
`ConsumerCatalogEntryV2` → `ConsumerCompositionEntry`. In the repository,
|
|
769
|
+
`npm run catalog:v2:sync` is now `npm run composition:sync`
|
|
770
|
+
(`catalog:sync` still runs both generators). The file contents and their
|
|
771
|
+
`schemaVersion` are unchanged.
|
|
772
|
+
|
|
773
|
+
- **Breaking (`@kerfjs/ui`, `create-kerf-component`, and `kerfjs setup`): the
|
|
774
|
+
composition catalog's own fields no longer read as a "v2" of the component
|
|
775
|
+
catalog.** In `component-composition.json` (and any consumer composition
|
|
776
|
+
catalog), `"schemaVersion": 2` is now `"schemaVersion": 1` — the composition
|
|
777
|
+
format's own first version, independent of `component-catalog.json`'s — and
|
|
778
|
+
`compatibility.v1Catalog` is now `compatibility.componentCatalog`. A
|
|
779
|
+
scaffolded package's `kerf.components.json` renames its `v1Catalog` field to
|
|
780
|
+
`componentCatalog` the same way. Regenerate scaffolded catalogs
|
|
781
|
+
(`npm run catalog:generate`), and change any `.kerf-ui-profile.json` `composition`
|
|
782
|
+
location from `"schemaVersion": 2` to `"schemaVersion": 1`; a catalog still
|
|
783
|
+
carrying the old fields fails validation with a message naming the rename.
|
|
784
|
+
The `ComponentComposition` type follows (`schemaVersion: 1`,
|
|
785
|
+
`compatibility.componentCatalog`).
|
|
786
|
+
|
|
787
|
+
- **`@kerfjs/ui` `NavStack` bars use the standard `Toolbar` padding.** The
|
|
788
|
+
navigation bar dropped its toolbar's block padding to stay 44px tall, so its
|
|
789
|
+
title and controls touched the bar's edges. It now keeps the 8px above and
|
|
790
|
+
below that every other toolbar has (60px plus any top safe-area inset).
|
|
791
|
+
|
|
792
|
+
- **`@kerfjs/ui` multiline list rows break long camelCase names at word
|
|
793
|
+
boundaries.** A string label in a `multiline` `ListItem` or `ListActionRow`
|
|
794
|
+
(such as a Catalog sidebar entry) gets `<wbr>` break opportunities at its
|
|
795
|
+
camelCase and acronym boundaries, so `QuickTicketComposer` wraps as
|
|
796
|
+
`QuickTicket` / `Composer` instead of `QuickTicketComp` / `oser`. Breaking
|
|
797
|
+
mid-word remains the fallback for a single word that is still too wide.
|
|
798
|
+
- **`wbr` is a typed JSX intrinsic element.** The runtime already rendered it
|
|
799
|
+
as a void element; `JSX.IntrinsicElements` now declares it.
|
|
800
|
+
|
|
801
|
+
- **`@kerfjs/ui` `Badge` has a text-free dot, and `TabScaffold` tabs can show
|
|
802
|
+
it.** `Badge` takes `size="dot"`: an 8px solid circle in the badge's tone,
|
|
803
|
+
with no children, appearance, or shape. Because a dot has no text, its props
|
|
804
|
+
require either a `label` (announced as an image, for example
|
|
805
|
+
`label="New activity"`) or `ariaHidden` when the owning component already
|
|
806
|
+
announces it. `TabScaffoldTab` accepts `badge: true` for the iOS tab-bar dot
|
|
807
|
+
(new content without a count), centered on the icon's top-trailing corner;
|
|
808
|
+
its `badgeLabel` is required and joins the tab's accessible name
|
|
809
|
+
("Settings, Update available"). `BadgeTextProps` and `BadgeDotProps` are
|
|
810
|
+
exported from the root barrel.
|
|
811
|
+
|
|
812
|
+
- **`@kerfjs/ui` `Toolbar` truncates the leading title instead of letting the
|
|
813
|
+
center zone cover it.** When a leading identity, a `center` group, and
|
|
814
|
+
trailing actions no longer fit one row, the center zone keeps its whole
|
|
815
|
+
content width and the leading title ends in an ellipsis. Previously the
|
|
816
|
+
center track could shrink below its group, which then painted over the
|
|
817
|
+
title — a pushed `NavStack` view with a center group did this at 390px. The
|
|
818
|
+
fix holds under every `responsive` policy and both `centerAlign` values, and
|
|
819
|
+
a toolbar that fits lays out exactly as before.
|
|
820
|
+
|
|
821
|
+
- **`@kerfjs/ui` `ValueTableRow` keeps its value readable beside a long
|
|
822
|
+
label.** A long single-word label (such as `ConnectionStateBanner` in a
|
|
823
|
+
390px-wide table) no longer squeezes the value to 0px wide, where it wrapped
|
|
824
|
+
one character per line while the label ran underneath it. The label and value
|
|
825
|
+
now share the row as two content-sized columns: a short value stays whole on
|
|
826
|
+
one line, the label wraps at its spaces and ends an over-long word in an
|
|
827
|
+
ellipsis before the value, and when both are long they split the row.
|
|
828
|
+
|
|
829
|
+
- **`@kerfjs/ui` pinned chrome can give way in a short pane.** `Pane` takes
|
|
830
|
+
`chromePlacement="auto"`: its header and footer stay pinned while the pane is
|
|
831
|
+
at least 480px (30rem) tall, and below that the whole pane scrolls as one
|
|
832
|
+
column so tall chrome never squeezes the content to nothing.
|
|
833
|
+
`WorkbenchChromePlacement` gains `"auto"` for `mainHeaderPlacement` /
|
|
834
|
+
`mainFooterPlacement`, and `Catalog` now defaults `headerPlacement` /
|
|
835
|
+
`footerPlacement` to `"auto"`. Its entry description is pinned under the
|
|
836
|
+
toolbar, which at 200% text size on a short window had left the preview 0px
|
|
837
|
+
tall.
|
|
838
|
+
|
|
839
|
+
- **`@kerfjs/ui` `TabScaffold` tabs can show a badge.** Give a
|
|
840
|
+
`TabScaffoldTab` a `badge` (a count or short string) to show the iOS tab-bar
|
|
841
|
+
badge — a compact, solid danger `Badge` at the top-trailing corner of the
|
|
842
|
+
tab's icon. The badge is hidden from assistive technology; the tab's
|
|
843
|
+
accessible name becomes `"<label>, <badgeLabel>"` (for example
|
|
844
|
+
"Inbox, 3 unread"), with `badgeLabel` defaulting to the badge text. Tabs
|
|
845
|
+
without a badge are unchanged.
|
|
846
|
+
|
|
847
|
+
- **`@kerfjs/ui` `ListItem` and `ListActionRow` labels stay out of the
|
|
848
|
+
trailing slot.** A long single-word primary label (such as a Catalog sidebar
|
|
849
|
+
entry named `ConnectionStateBanner` in a narrow rail) no longer runs
|
|
850
|
+
underneath trailing status text. Single-line rows now end the primary label
|
|
851
|
+
and description with a real ellipsis (previously the text was cut mid-glyph
|
|
852
|
+
with no "…"), and `multiline` rows break a word wider than the label column
|
|
853
|
+
onto the next line.
|
|
854
|
+
|
|
855
|
+
- **Custom elements keep the `role` and `aria-*` they set on themselves across
|
|
856
|
+
re-renders.** Web components conventionally own their host's ARIA (Web
|
|
857
|
+
Awesome's `wa-option` sets `role="option"` / `aria-selected`, `wa-divider`
|
|
858
|
+
sets `role="separator"`; Shoelace and FAST do the same), but a template never
|
|
859
|
+
mentions them, so the morph and the keyed-list attribute fast path stripped
|
|
860
|
+
them on the next re-render. On any custom element (hyphenated tag), `role`
|
|
861
|
+
and `aria-*` are now element-owned like `open`: never removed just because
|
|
862
|
+
the template omits them. The template can still set or change them, a signal
|
|
863
|
+
binding set to `null` / `false` still removes them, and plain elements are
|
|
864
|
+
unchanged. Trade-off: omitting one from a static template no longer removes
|
|
865
|
+
it from a custom element — render an explicit value (`aria-pressed="false"`)
|
|
866
|
+
instead.
|
|
867
|
+
|
|
868
|
+
- **`@kerfjs/ui` layout regions no longer slow edits to long lists inside
|
|
869
|
+
them.** The rule that clears the safe-area context for the children of a
|
|
870
|
+
Workbench main area or panel, SplitView list or detail, NavStack view,
|
|
871
|
+
TabScaffold scene, or CollapsiblePanel content made each child re-check the
|
|
872
|
+
region in Chromium on every DOM change. A 1000-row list placed directly in
|
|
873
|
+
one of these regions took about 46ms per style recalculation (2ms with the
|
|
874
|
+
list one level down), and now takes about 0.2ms. Layout and insets are
|
|
875
|
+
unchanged.
|
|
876
|
+
|
|
877
|
+
- **`@kerfjs/ui` `CollapsiblePanel` no longer slows insertions into long
|
|
878
|
+
lists.** An expanded inline panel clears its screen edge for its siblings;
|
|
879
|
+
the rules that did so selected the siblings themselves
|
|
880
|
+
(`:has(~ panel)` and `panel ~ *`), which in Chromium made each row inserted
|
|
881
|
+
into or removed from a long list restyle the list's other rows — about
|
|
882
|
+
14.6ms instead of 0.2ms per insertion beside a 1000-row list. The panel's
|
|
883
|
+
container now carries a flag that a CSS style query reads, so a list pays
|
|
884
|
+
nothing. The edge still clears for the panel's siblings and returns when it
|
|
885
|
+
collapses; the one difference is that a sibling placed before a left panel
|
|
886
|
+
or after a right or bottom panel now loses the edge too.
|
|
887
|
+
|
|
888
|
+
- **`@kerfjs/ui` restore corners and covered floating controls no longer slow
|
|
889
|
+
every style recalculation.** The `CollapsiblePanel` / `ResizableRegion`
|
|
890
|
+
restore-corner anchor scope (`:has(> restore) > * > * > *`) and the static
|
|
891
|
+
side-overlay covered flag (`:has(> open overlay) > :not(overlay)`) put a
|
|
892
|
+
universal selector after a `:has()` container, which in Chromium restyled
|
|
893
|
+
the children of every ancestor of each DOM change — about 85ms instead of
|
|
894
|
+
1ms per edit beside a 1000-row list. The drawer now publishes its anchor
|
|
895
|
+
only beside a restore control's container (or its work-area column), and
|
|
896
|
+
the covered flag is set on the overlay's container and cleared on its
|
|
897
|
+
overlay children. Restore-corner placement and covered-control hiding are
|
|
898
|
+
unchanged; a new CSS unit test rejects the costly `:has()` shapes.
|
|
899
|
+
|
|
900
|
+
- **`@kerfjs/ui` adds `ContentItem`.** Import it from
|
|
901
|
+
`@kerfjs/ui/content-item` to render one self-contained pane child with the
|
|
902
|
+
shared 8px margin, 1px border, and 8px padding. Framing and shape are typed
|
|
903
|
+
props instead of hand-written modifier classes:
|
|
904
|
+
- `frame="framed"` paints the neutral border in the 1px the item always
|
|
905
|
+
reserves, so its geometry does not change (replacing
|
|
906
|
+
`class="kui-content-item kui-content-item--framed"`);
|
|
907
|
+
- `shape="pill"` selects the 22px radius.
|
|
908
|
+
- `ariaLabel` names a distinct region, and `focusTarget` makes the item a
|
|
909
|
+
programmatic focus target (`tabindex="-1"`), such as a NavStack view's
|
|
910
|
+
`data-nav-focus` target.
|
|
911
|
+
|
|
912
|
+
The `.kui-content-item` classes stay public for elements that must carry the
|
|
913
|
+
geometry themselves; their CSS now lives in `content-item.css`, which
|
|
914
|
+
`layout.css` still imports. There is no filled variant — use `SunkenPanel`
|
|
915
|
+
for a lowered surface. The catalog, demos, and recipes use the component.
|
|
916
|
+
|
|
917
|
+
- **`@kerfjs/ui` icon-only toolbar buttons show the same help tag.** An
|
|
918
|
+
icon-only `<button>` or link inside a `ToolbarControlGroup`, named by
|
|
919
|
+
`aria-label` with no visible text, now shows the help tag icon-only Selects
|
|
920
|
+
and PopupMenus show, with the same 500ms hover delay, keyboard-focus-only
|
|
921
|
+
display, press/Escape dismissal, and quick scanning along a toolbar. The tag
|
|
922
|
+
is `aria-hidden` and never renames the button. A button with a native
|
|
923
|
+
`title`, or with its own Web Awesome tooltip, keeps that and gets no second
|
|
924
|
+
tag. The `select/register` and `popup-menu/register` imports install it; a
|
|
925
|
+
toolbar with icon-only buttons but neither control imports the new
|
|
926
|
+
`@kerfjs/ui/help-tags/register` subpath, which registers only the tooltip.
|
|
927
|
+
|
|
928
|
+
- **`@kerfjs/ui` icon-only Select and PopupMenu triggers show a help tag.**
|
|
929
|
+
An icon-only `Select` (single or `multiple` with `triggerIcon`) and an
|
|
930
|
+
icon-only `PopupMenu` now show a Web Awesome tooltip naming the control on
|
|
931
|
+
hover (after 500ms) and on keyboard focus: a filter reads "Filter by label:
|
|
932
|
+
Bug, Docs", a single Select its name and current choice, a menu its `label`.
|
|
933
|
+
It hides while the popup is open, on a press, and on Escape. The tag repeats
|
|
934
|
+
the accessible name, so it is `aria-hidden` and never joins
|
|
935
|
+
`aria-labelledby`; screen readers still hear the name once. It is installed
|
|
936
|
+
by the existing `select/register` and `popup-menu/register` imports, needs no
|
|
937
|
+
props, and never appears for labeled Selects or text menu triggers.
|
|
938
|
+
|
|
939
|
+
- **`@kerfjs/ui` toolbar groups drop their focus ring while a popup is open.**
|
|
940
|
+
A `ToolbarControlGroup` that owns the ring (`focusRing="outline"` or
|
|
941
|
+
`"halo"`) no longer paints it while a Select listbox or `PopupMenu` inside it
|
|
942
|
+
is open; the open popup shows focus, and the ring returns when the popup
|
|
943
|
+
closes with focus still in the group. This matches the per-control Select
|
|
944
|
+
and PopupMenu triggers.
|
|
945
|
+
|
|
946
|
+
- **`@kerfjs/ui` multiple `Select` can be an icon-only toolbar filter menu.**
|
|
947
|
+
Set `selectedPresentation: 'icon-only'` on a `multiple` Select together with
|
|
948
|
+
a fixed `triggerIcon` (for example a funnel `LucideIcon`); the types require
|
|
949
|
+
`triggerIcon` there and reject it everywhere else. The trigger keeps the
|
|
950
|
+
icon-only caret pill used in a `ToolbarControlGroup`, and:
|
|
951
|
+
- shows the fixed icon whatever is chosen;
|
|
952
|
+
- adds a compact count `Badge` while any choice is chosen;
|
|
953
|
+
- ends the combobox's accessible name with the chosen labels in choice order
|
|
954
|
+
("Filter by label: Bug, Docs"), leaving the hidden value text empty so the
|
|
955
|
+
list is not announced twice.
|
|
956
|
+
|
|
957
|
+
The popup still stays open while choices toggle.
|
|
958
|
+
|
|
959
|
+
- **`@kerfjs/ui` `Select` options keep their accessibility semantics across
|
|
960
|
+
re-renders.** Re-rendering a Select (for example after its `change` handler
|
|
961
|
+
updates the controlling signal) removed the `role="option"` and
|
|
962
|
+
`aria-selected` Web Awesome sets on each option, so screen readers lost every
|
|
963
|
+
option's role and selected state. They now survive in single and `multiple`
|
|
964
|
+
Selects through the core rule below (custom elements own their `role` /
|
|
965
|
+
`aria-*`), with no Select-specific restore.
|
|
966
|
+
|
|
967
|
+
- **`@kerfjs/ui` `Select` dividers keep their separator role across
|
|
968
|
+
re-renders.** The `wa-divider` a `separatorBefore` choice adds lost the
|
|
969
|
+
`role="separator"` and `aria-orientation` Web Awesome sets on it the first
|
|
970
|
+
time the enclosing mount re-rendered, and never got them back, so assistive
|
|
971
|
+
technology stopped announcing the break between option runs. The Select now
|
|
972
|
+
renders both attributes itself.
|
|
973
|
+
|
|
974
|
+
- **`@kerfjs/ui` `Select` and `PopupMenu` re-renders no longer force Web
|
|
975
|
+
Awesome updates.** Web Awesome reflects its default `size`, `appearance`,
|
|
976
|
+
`placement`, and `orientation` onto `wa-select`, `wa-dropdown`, and
|
|
977
|
+
`wa-divider`; the templates omitted them, so every re-render of the enclosing
|
|
978
|
+
mount stripped them and each host ran a full Lit update to put them back. The
|
|
979
|
+
templates now render those defaults (`wa-select size="m"
|
|
980
|
+
appearance="outlined" placement="bottom"`, `wa-dropdown size="m"`, divider
|
|
981
|
+
`orientation="horizontal"`), so an unchanged re-render leaves the hosts
|
|
982
|
+
alone. No visual change. The `@kerfjs/ui/webawesome` JSX declarations type
|
|
983
|
+
these attributes.
|
|
984
|
+
|
|
985
|
+
- **`@kerfjs/ui` Workbench and CollapsiblePanel panes and chrome lists are
|
|
986
|
+
configurable.** The `Pane`s these layouts compose around app content now
|
|
987
|
+
take the new `PaneConfig` type (`contentElement`, `contentLabel`,
|
|
988
|
+
`separators`, `safeAreaEdges`): a Workbench panel's or `CollapsiblePanel`'s
|
|
989
|
+
`pane`, and the Workbench's `mainPane`. The `List`s holding `mainHeader` /
|
|
990
|
+
`mainFooter` take the new `ListConfig` type (`gap`, `hAlign`, `vAlign`,
|
|
991
|
+
`dividerSides`, `textInsets`, `controlInsets`) through `mainHeaderList` /
|
|
992
|
+
`mainFooterList`. Previously this configuration was hard-coded, so, for
|
|
993
|
+
example, a navigator rail could not render its content as a `nav` landmark
|
|
994
|
+
without restyling. Omitted or `undefined` fields keep today's defaults, and
|
|
995
|
+
the default markup is unchanged.
|
|
996
|
+
|
|
997
|
+
- **`@kerfjs/ui` `SplitView` forwards its `ResizableRegion` and compact
|
|
998
|
+
`NavStack` configuration.** `resizable` now also takes the list region's
|
|
999
|
+
`separator`, `handleIcon`, `contentOverflow`, and collapse options
|
|
1000
|
+
(`collapsed`, `transitioning`, `collapseMotion`, `restoreControl`,
|
|
1001
|
+
`restorePosition`), so an app can hide the list and float a restore control
|
|
1002
|
+
without leaving `SplitView`. The new `compactStack` prop forwards the compact
|
|
1003
|
+
stack's `toolbarConfig`, `backIcon`, `backText`, `hideToolbar`, and
|
|
1004
|
+
persistent `bottomToolbar`, and gives the list and detail views their own
|
|
1005
|
+
toolbar groups (`list` / `detail`: `leading`, `center`, `toolbar`,
|
|
1006
|
+
`bottomToolbar`). Omitted options keep today's defaults.
|
|
1007
|
+
|
|
1008
|
+
- **`@kerfjs/ui` `NavStack` renders its top chrome as a real `Toolbar`.** The
|
|
1009
|
+
back control is a borderless `ToolbarControlGroup` and the title a
|
|
1010
|
+
`ToolbarText` in the leading zone, and the stack now forwards:
|
|
1011
|
+
- `toolbarConfig` — the `Toolbar` configuration (`dividerSides`,
|
|
1012
|
+
`centerAlign`, `responsive`, `responsiveAt`, `safeAreaEdges`) plus `label`,
|
|
1013
|
+
`titleSize` (default `large`), and `headingLevel`;
|
|
1014
|
+
- per-view `leading` and `center` groups beside the existing trailing
|
|
1015
|
+
`toolbar`;
|
|
1016
|
+
- `backIcon` and visible `backText` for the back control.
|
|
1017
|
+
|
|
1018
|
+
The defaults keep today's bar: no divider, one 44px band, and the top and
|
|
1019
|
+
side safe-area edges claimed. The hand-rolled `kui-nav-stack__lead` and
|
|
1020
|
+
`kui-nav-stack__actions` wrappers are gone (their content now sits in the
|
|
1021
|
+
toolbar's zones), and `kui-nav-stack__back` / `kui-nav-stack__back-icon`
|
|
1022
|
+
are no longer styled by the stack.
|
|
1023
|
+
|
|
1024
|
+
- **`@kerfjs/ui` configures with props instead of token overrides.** Five
|
|
1025
|
+
settings apps previously reached through raw `--kui-*` overrides now have a
|
|
1026
|
+
typed prop or modifier; each defaults to today's rendering, and the
|
|
1027
|
+
underlying tokens keep working:
|
|
1028
|
+
- `FloatingToolbar` `inset` (a `CssLength`, default `--kui-space-m`) moves
|
|
1029
|
+
it from the container edges. Omit it inside a restore corner, which owns
|
|
1030
|
+
the inset.
|
|
1031
|
+
- `DisclosureArrow` `size` (a `CssLength`, default 18px root-scaled).
|
|
1032
|
+
- `ListHeader` `headingLevel` (1–6, default 2) sets the label's native
|
|
1033
|
+
heading element for passive and trailing-action headers; a toggle header
|
|
1034
|
+
has no heading and rejects it at compile time.
|
|
1035
|
+
- `TokenSearchField` `clearIcon` replaces the clear action's glyph.
|
|
1036
|
+
- `.kui-content-item--framed` paints the standard neutral border on a
|
|
1037
|
+
content item that marks a real distinction, with no geometry change.
|
|
1038
|
+
|
|
1039
|
+
The docs and AI guidance also stop recommending `--kui-edge-inset-*`
|
|
1040
|
+
overrides: an app-owned arrangement passes `safeAreaEdges` to its panes and
|
|
1041
|
+
edge-claiming toolbars (including the layout toolbar configurations, which
|
|
1042
|
+
forward it).
|
|
1043
|
+
|
|
1044
|
+
- **`@kerfjs/ui` hides a work area's floating controls under an open side
|
|
1045
|
+
overlay.** While a side overlay is open — a Workbench rail (static
|
|
1046
|
+
`presentation: "overlay"`, or a responsive overlay whose breakpoint applies),
|
|
1047
|
+
a `wireSidebar` compact overlay or `presentation="overlay"`
|
|
1048
|
+
`CollapsiblePanel`, or a horizontal `presentation="overlay"`
|
|
1049
|
+
`ResizableRegion` — the covered work area's `FloatingToolbar`s and the
|
|
1050
|
+
layouts' restore corners are now hidden (`visibility: hidden`, so they also
|
|
1051
|
+
leave the tab order and the accessibility tree) instead of relying on
|
|
1052
|
+
stacking order alone. A phone-width app could otherwise paint the main
|
|
1053
|
+
column's floating drawer toggle over its open inspector. They come back as
|
|
1054
|
+
the overlay closes; the overlay's own floating controls and a bottom
|
|
1055
|
+
drawer's overlay are unaffected.
|
|
1056
|
+
|
|
1057
|
+
- **`@kerfjs/ui` `Select` supports multiple selection.** Set `multiple: true`
|
|
1058
|
+
and pass the chosen values as an array `value`; the props are typed so an
|
|
1059
|
+
array value requires `multiple`. Behavior:
|
|
1060
|
+
- The popup stays open while the person toggles choices with the pointer,
|
|
1061
|
+
Enter, or Space, and closes only on an outside press, Escape, or focus
|
|
1062
|
+
leaving.
|
|
1063
|
+
- Each chosen option shows a check.
|
|
1064
|
+
- The closed control summarizes the chosen labels in choice order as a short
|
|
1065
|
+
localized list, or shows `placeholderText` when none are chosen.
|
|
1066
|
+
- `change` / `input` report the value array, and a form submits every chosen
|
|
1067
|
+
value under `name`.
|
|
1068
|
+
- Re-rendering with a different `value` updates the selection even after the
|
|
1069
|
+
person has interacted.
|
|
1070
|
+
|
|
1071
|
+
`renderSelected` and `selectedPresentation="icon-only"` stay single-value
|
|
1072
|
+
only.
|
|
1073
|
+
|
|
1074
|
+
- **`@kerfjs/ui` `PopupMenu` now looks exactly like `Select`.** Both read one
|
|
1075
|
+
shared popup contract, so a command menu beside a value list in a toolbar
|
|
1076
|
+
draws the same popup. `PopupMenu` changes:
|
|
1077
|
+
- its caret turns to face the open menu;
|
|
1078
|
+
- its menu fades in without scaling;
|
|
1079
|
+
- it takes the Select listbox's 4px inset (also under the default
|
|
1080
|
+
`menuInset="standard"`);
|
|
1081
|
+
- its rows use the Select option's height, padding, and reserved check
|
|
1082
|
+
column, and its icons use the same 16px size and 8px gap;
|
|
1083
|
+
- a keyboard-focused item takes the Select's current-option fill instead of
|
|
1084
|
+
a focus ring;
|
|
1085
|
+
- headings render as the Select's uppercase group titles rather than slotted
|
|
1086
|
+
`h3` elements.
|
|
1087
|
+
|
|
1088
|
+
An open `Select` also no longer paints its trigger focus ring. The open
|
|
1089
|
+
listbox shows focus, and the ring returns when it closes.
|
|
1090
|
+
|
|
1091
|
+
- **`@kerfjs/ui` `Catalog` exposes its sidebar and toolbar configuration.** New
|
|
1092
|
+
`sidebar` prop (size, `resizable`, `separator`, `collapseMotion`,
|
|
1093
|
+
`presentation`, `responsiveOverlayAt`, `compactOverlay`, and the header
|
|
1094
|
+
`toolbar`'s `ToolbarConfig`) forwarded to its Workbench rail, plus
|
|
1095
|
+
`mainToolbar` / `footerToolbar` `ToolbarConfig` props. `wireCatalog` takes
|
|
1096
|
+
`sidebarSize` / `sidebarStorageKey` to wire a resizable sidebar. Today's
|
|
1097
|
+
288px fixed sidebar and toolbar policies remain the defaults.
|
|
1098
|
+
|
|
1099
|
+
- **`@kerfjs/ui` composed toolbars are configurable.** `WorkbenchMainToolbar`,
|
|
1100
|
+
`WorkbenchMainBottomToolbar`, and the panel `toolbar` of a Workbench panel or
|
|
1101
|
+
`CollapsiblePanel` now take the `Toolbar`'s configuration: the new
|
|
1102
|
+
`ToolbarConfig` type, covering `dividerSides`, `centerAlign`, `responsive`,
|
|
1103
|
+
`responsiveAt`, and `safeAreaEdges`. Previously their dividers were
|
|
1104
|
+
hard-coded, so for example dropping the line between a toolbar and its
|
|
1105
|
+
content took CSS. Today's dividers remain the defaults. Also:
|
|
1106
|
+
- `mainBottomToolbar` and panel toolbars take a `center`;
|
|
1107
|
+
- panel toggles take localized `showLabel` / `hideLabel`;
|
|
1108
|
+
- a Workbench panel's floating restore controls follow its
|
|
1109
|
+
`restorePosition`.
|
|
1110
|
+
|
|
1111
|
+
- **Breaking (`@kerfjs/ui`): removed the pre-release compatibility surface.**
|
|
1112
|
+
- The `DividerSides` type is gone; use `Sides`, from the renamed `sides`
|
|
1113
|
+
module.
|
|
1114
|
+
- `ListInsetText.horizontalOnly` and its `kui-list-inset-text--horizontal`
|
|
1115
|
+
class are gone; pass `sides="rl"` instead.
|
|
1116
|
+
- The legacy color tokens are gone, both as `--kui-color-*` properties and
|
|
1117
|
+
as `uiColor()` names. Replace them with their canonical tokens:
|
|
1118
|
+
|
|
1119
|
+
| Removed | Use instead |
|
|
1120
|
+
| --------------------------------------------- | ----------------------- |
|
|
1121
|
+
| `border` | `neutral-border-normal` |
|
|
1122
|
+
| `border-quiet` | `neutral-border-quiet` |
|
|
1123
|
+
| `accent` | `brand-fill-normal` |
|
|
1124
|
+
| `accent-text` | `brand-on-quiet` |
|
|
1125
|
+
| `pop` | `pop-fill-quiet` |
|
|
1126
|
+
| `pop-text` | `pop-on-fill` |
|
|
1127
|
+
| `success`, `warning`, `danger` | `*-fill-quiet` |
|
|
1128
|
+
| `success-text`, `warning-text`, `danger-text` | `*-on-quiet` |
|
|
1129
|
+
|
|
1130
|
+
- **New `@kerfjs/ui` `PopupMenu` (`@kerfjs/ui/popup-menu`).** It renders a
|
|
1131
|
+
trigger and typed commands, headings, and dividers over the Web Awesome
|
|
1132
|
+
dropdown. Put it in a `single` `ToolbarControlGroup` with `nestedDropdown`, or
|
|
1133
|
+
wrap it in a `PopupSurface`. Register it once with
|
|
1134
|
+
`@kerfjs/ui/popup-menu/register`. It replaces hand-written `wa-dropdown`
|
|
1135
|
+
markup, which the catalog discourages: the guidance, the toolbar demos, and
|
|
1136
|
+
the Catalog's "Components" menu now use it. It also fixes two defects that
|
|
1137
|
+
hand-written menus had:
|
|
1138
|
+
- Icon-only triggers were unnamed, because a Web Awesome button ignores an
|
|
1139
|
+
`aria-label` on its host. PopupMenu renders `label` as visually hidden text
|
|
1140
|
+
inside the button.
|
|
1141
|
+
- A keyboard choice (Enter or Space) never reached click-delegated
|
|
1142
|
+
`data-action` handlers. PopupMenu's registration routes it through the
|
|
1143
|
+
item's click.
|
|
1144
|
+
- The UX catalog's application-layout demo no longer uses the discouraged
|
|
1145
|
+
`wa-button-group`.
|
|
1146
|
+
|
|
1147
|
+
- **`@kerfjs/ui`'s `TokenSearchField` takes a `revision` prop for programmatic
|
|
1148
|
+
text replacement.** The editor's text is DOM-owned, so a new `query` with the
|
|
1149
|
+
same tokens was silently ignored, for example when reseeding a persistent
|
|
1150
|
+
dialog's field on reopen. Changing `revision` rebuilds the editor from `query`
|
|
1151
|
+
and `tokens`, so consumers no longer need to re-key a wrapper element.
|
|
1152
|
+
|
|
1153
|
+
- **`@kerfjs/ui`'s `TabBar` strip no longer scrolls vertically.** The strip set
|
|
1154
|
+
only `overflow-x: auto`, so any vertical excess (consumer padding, focus-ring
|
|
1155
|
+
insets, taller tabs under larger text) let it scroll up and down on touch. It
|
|
1156
|
+
now scrolls only horizontally, and it takes a `min-height` so it grows to fit
|
|
1157
|
+
taller tabs instead of clipping them.
|
|
1158
|
+
|
|
1159
|
+
- **The `@kerfjs/ui` desktop app-shell recipe is a `Workbench`.** The old
|
|
1160
|
+
pane switcher that showed one pane at a time below desktop sizes is gone. The
|
|
1161
|
+
navigation and inspector are now resizable Workbench rails with their own
|
|
1162
|
+
toolbars, and their show toggles relocate into the work-area toolbar. On a
|
|
1163
|
+
narrow container the rails overlay the task list one at a time. The
|
|
1164
|
+
collapsible-sidebar recipe composes each panel's `toolbar` and uses
|
|
1165
|
+
`CollapsiblePanelRelocated` for its restore controls.
|
|
1166
|
+
|
|
1167
|
+
- **`@kerfjs/ui`'s `CollapsiblePanel` composes a panel toolbar and relocates its
|
|
1168
|
+
controls.** A panel takes the same `toolbar` roles as a `Workbench` panel
|
|
1169
|
+
(`title`, `panelOnly`, `constant`, and a standard `toggle`) plus a `footer`,
|
|
1170
|
+
and the new `CollapsiblePanelRelocated` renders a collapsed panel's constant
|
|
1171
|
+
groups and toggle wherever the app's work-area toolbar holds them.
|
|
1172
|
+
`wireSidebar` hands focus to that relocated toggle when the panel closes, even
|
|
1173
|
+
when the app renders it a frame late. `collapsiblePanelToggleIcon` is
|
|
1174
|
+
unchanged.
|
|
1175
|
+
|
|
1176
|
+
- **`@kerfjs/ui`'s `Catalog` is built on `Workbench` and dogfoods the package's
|
|
1177
|
+
components.** The sidebar is a Workbench left rail with the standard toggle
|
|
1178
|
+
(labels "Show/Hide {brand} catalog"), which moves into the entry toolbar while
|
|
1179
|
+
collapsed; on a small screen the sidebar overlays the stage instead of the
|
|
1180
|
+
former stacked two-column navigation, which is gone. The sidebar, sections,
|
|
1181
|
+
footer, and example notes are `List` / `ListHeader` / `ListInsetText` / `Text`
|
|
1182
|
+
compositions, and most catalog CSS (and every viewport media query in the
|
|
1183
|
+
shell) is removed. New: `Catalog`'s `id`, `headerPlacement`, and
|
|
1184
|
+
`footerPlacement`; `wireCatalog`'s `collapsed` and `id` options (the sidebar is
|
|
1185
|
+
wired like a Workbench rail and an open overlay closes on a selection);
|
|
1186
|
+
`Workbench`'s `mainHeader`, `mainFooter`, `mainHeaderPlacement`,
|
|
1187
|
+
`mainFooterPlacement`, and `mainToolbar.responsive` / `responsiveAt`;
|
|
1188
|
+
`ListInsetText`'s `rootAttributes`. `wireWorkbench` now also re-checks panel
|
|
1189
|
+
breakpoints when the root font size changes, and hands focus to a panel's
|
|
1190
|
+
relocated toggle when an unfocused click on its own toggle closes it (WebKit).
|
|
1191
|
+
|
|
1192
|
+
- **`@kerfjs/ui`'s `Workbench` rails overlay the work area on small screens by
|
|
1193
|
+
default.** A rail's `responsiveOverlayAt` now defaults to `"narrow"` (704px
|
|
1194
|
+
of Workbench width; the new `"never"` keeps it inline), so a sidebar covers
|
|
1195
|
+
the work area and any open drawer instead of squeezing it, one at a time
|
|
1196
|
+
with `wireWorkbench`. On a compact (448px) Workbench an open overlay rail
|
|
1197
|
+
fills the width less a 44px dismiss strip
|
|
1198
|
+
(`--kui-workbench-overlay-dismiss-margin`) on the side away from its edge;
|
|
1199
|
+
the new per-rail `compactOverlay: "full"` fills it. The bottom drawer's
|
|
1200
|
+
behavior is unchanged. `WorkbenchPanelToolbar` and the other panel-toolbar
|
|
1201
|
+
types are now exported from `@kerfjs/ui/workbench`.
|
|
1202
|
+
|
|
1203
|
+
- **`@kerfjs/ui`'s `Workbench` composes panel toolbars and moves a closed
|
|
1204
|
+
panel's controls into the work area.** A panel takes an optional `toolbar`
|
|
1205
|
+
(`title`, `panelOnly`, `constant`, and a standard `toggle: { action, name }`
|
|
1206
|
+
the Workbench renders) and `footer`; the Workbench takes `mainToolbar` and
|
|
1207
|
+
`mainBottomToolbar`. While a panel is closed its constant groups and toggle
|
|
1208
|
+
move to the work area — a left rail's lead the main toolbar, a right rail's
|
|
1209
|
+
end it, a drawer's trail the bottom toolbar or float in a corner
|
|
1210
|
+
`FloatingToolbar` — and its panel-only groups stay behind. `wireWorkbench`
|
|
1211
|
+
keeps focus with the toggle as it moves. Panels without a `toolbar` render
|
|
1212
|
+
exactly as before, and a region's only-child `Pane` or layout now fills it.
|
|
1213
|
+
|
|
1214
|
+
- **`@kerfjs/ui`'s list-detail dialog guidance is Workbench-shaped.** On roomy
|
|
1215
|
+
devices the list is a full-height sidebar and the detail a full-height main
|
|
1216
|
+
column, each a `Pane` with its own top `Toolbar`; the main column carries
|
|
1217
|
+
the primary title, the close control (`data-dialog="close"` in a
|
|
1218
|
+
`without-header` Web Awesome dialog), and the record actions, instead of a
|
|
1219
|
+
dialog header spanning both columns. The recipe, `docs/recipes.md`, and the
|
|
1220
|
+
shipped AI skill say so; compact devices keep the full-screen drill-down
|
|
1221
|
+
sheet.
|
|
1222
|
+
|
|
1223
|
+
- **`@kerfjs/ui`'s `ToolbarControlGroup` centers a direct icon tile.** A
|
|
1224
|
+
non-interactive icon placed directly in a group — a heading's identity
|
|
1225
|
+
glyph — now takes an icon button's 40px slot and 16px visual, so it centers
|
|
1226
|
+
in the group. It previously sat at the group's start edge, which pushed
|
|
1227
|
+
apps to wrap it in a focusable `<button>` that did nothing.
|
|
1228
|
+
|
|
1229
|
+
- **`@kerfjs/ui`'s `Toolbar` keeps its controls in a top control band.** A
|
|
1230
|
+
toolbar now reserves one 44px band at its top: each zone item no taller than
|
|
1231
|
+
the band is centered in it by its own size, and a taller item — a wrapped
|
|
1232
|
+
`ToolbarText` title, a multi-line search, a second trailing row — starts at
|
|
1233
|
+
the band's top and grows down without moving the others. A wrapped title
|
|
1234
|
+
centers its first line in the band, so trailing controls stay beside that
|
|
1235
|
+
line instead of dropping to the middle of the whole title.
|
|
1236
|
+
|
|
1237
|
+
- **`@kerfjs/ui`'s segmented `TabBar` ends its strip concentric with the pill
|
|
1238
|
+
track.** The first tab's leading corners, and with `allocation="fill"` the
|
|
1239
|
+
last tab's trailing corners, now follow the track's curve instead of keeping
|
|
1240
|
+
the small segment radius, and compact tabs are inset from the track's ends
|
|
1241
|
+
by the same distance as from its top and bottom. Corners between tabs are
|
|
1242
|
+
unchanged.
|
|
1243
|
+
|
|
1244
|
+
- **`@kerfjs/ui`'s `StateBanner` keeps its title, badge, and detail on one
|
|
1245
|
+
baseline in Safari.** The truncating detail was a clipped `inline-block`,
|
|
1246
|
+
which WebKit baselines at its bottom edge: the detail rose about 2px above
|
|
1247
|
+
the title and pushed the title and badge down, so the banner's contents
|
|
1248
|
+
looked vertically misaligned. The detail is now an inline flex wrapper
|
|
1249
|
+
around a clipped inner `<span>`, which takes its text baseline in every
|
|
1250
|
+
engine; truncation and the narrow-width detail line are unchanged.
|
|
1251
|
+
|
|
1252
|
+
- **`@kerfjs/ui`'s `wireTabBars` reveals a tab the application selects.** A
|
|
1253
|
+
tab added and selected by the app in an overflowing `TabBar` stayed
|
|
1254
|
+
off-screen, because the wiring revealed a selection only at wire-up, on
|
|
1255
|
+
focus, and after its own keyboard/drag changes. A change of a bar's selected
|
|
1256
|
+
tab now scrolls that bar's strip (never the page) to show it; re-renders that
|
|
1257
|
+
keep the selection leave a user-scrolled strip alone.
|
|
1258
|
+
|
|
1259
|
+
- **`eslint-plugin-kerfjs`'s `ui-composition` rule resolves application
|
|
1260
|
+
wrappers imported through a barrel.** A cataloged wrapper registered only by
|
|
1261
|
+
its defining source file, so `import { DemandSegmentsControl } from
|
|
1262
|
+
'./components/index.js'` (where `index.ts` re-exports it) was an unknown
|
|
1263
|
+
element and reported `KUI-L202` in a toolbar zone despite its `rendersAs`.
|
|
1264
|
+
Relative re-exports — `export { X } from`, `export { X as Y } from`,
|
|
1265
|
+
`export * from`, and chains of them — are now followed to the cataloged
|
|
1266
|
+
source file. Type-only and namespace re-exports and non-cataloged components
|
|
1267
|
+
stay unresolved.
|
|
1268
|
+
|
|
1269
|
+
- **A collapsed `@kerfjs/ui` `Workbench` panel or `ResizableRegion` leaves
|
|
1270
|
+
the accessibility tree.** Only its content was `inert`, so the labeled
|
|
1271
|
+
`aside` / `section` stayed behind as an empty complementary or region
|
|
1272
|
+
landmark that a screen-reader user could still land on. The collapsed
|
|
1273
|
+
region itself now renders `inert` and `aria-hidden="true"`, like a
|
|
1274
|
+
collapsed `CollapsiblePanel`; its separator was already hidden, and each
|
|
1275
|
+
`restoreControl` renders outside it and stays reachable.
|
|
1276
|
+
|
|
1277
|
+
- **An expanded `@kerfjs/ui` `CollapsiblePanel` holds its size in a flex
|
|
1278
|
+
layout.** The panel shrank like any flex item beside a work area with wide
|
|
1279
|
+
content, while its content kept the panel's fixed size for the slide-out, so
|
|
1280
|
+
the panel's edge clipped the content — in the collapsible-sidebar recipe at
|
|
1281
|
+
1280px, the rail showed 211 of its 232px and cut off the selected Inbox
|
|
1282
|
+
row's count. The panel is now `flex-shrink: 0`; the sibling work area gives
|
|
1283
|
+
way instead.
|
|
1284
|
+
|
|
1285
|
+
- **`@kerfjs/ui` overlay panels span their host on the fixed axis.** The
|
|
1286
|
+
85vw / 85vh overlay maximums (`--kui-resizable-region-overlay-max-*`,
|
|
1287
|
+
`--kui-workbench-overlay-max-*`) applied to both axes, so an overlay bottom
|
|
1288
|
+
drawer stopped about 15% short of its host's width and an overlay side rail
|
|
1289
|
+
in a full-height layout stopped short of the bottom. Each maximum now caps
|
|
1290
|
+
only the panel's resizable axis: side overlays cap their width, drawers
|
|
1291
|
+
their height.
|
|
1292
|
+
|
|
1293
|
+
- **An overlay `@kerfjs/ui` `ResizableRegion` paints the surface.** With
|
|
1294
|
+
`presentation="overlay"` the region floated over the work area with a
|
|
1295
|
+
shadow but no background, so the content beneath — and a collapsed rail's
|
|
1296
|
+
restore control stacked under it — showed through. It now paints
|
|
1297
|
+
`--kui-color-surface`, like an overlay `CollapsiblePanel`; an inline region
|
|
1298
|
+
stays transparent over its host.
|
|
1299
|
+
|
|
1300
|
+
- **A pinned `@kerfjs/ui` resize separator no longer overwrites the
|
|
1301
|
+
remembered size.** When a container squeezes a `Workbench` rail, drawer, or
|
|
1302
|
+
`ResizableRegion` below its `min` (or its reachable range collapses to one
|
|
1303
|
+
size), the separator cannot move it, yet an arrow/Home/End key or a drag
|
|
1304
|
+
still committed the configured `min` to the app's size signal and storage:
|
|
1305
|
+
a rail remembered at 280px silently became 180px and came back at 180px
|
|
1306
|
+
when the window widened. A pinned separator now commits nothing, so the
|
|
1307
|
+
track returns at its remembered size once there is room.
|
|
1308
|
+
|
|
1309
|
+
- **A `pending` `@kerfjs/ui` `AppTab` is selectable and no longer dimmed.**
|
|
1310
|
+
A known tab that is still opening now keeps its select button enabled and
|
|
1311
|
+
in the roving tab order, so a user can switch to it and the application can
|
|
1312
|
+
show placeholder content in its panel until loading completes. With
|
|
1313
|
+
`webawesome.css` loaded, Web Awesome's native `button:disabled` no longer
|
|
1314
|
+
dims the pending pill to 50%; it shows only its quiet name color and
|
|
1315
|
+
trailing spinner, as documented. Close and dragging stay disabled while
|
|
1316
|
+
pending, and it stays `aria-busy`. An unknown `placeholder` tab is
|
|
1317
|
+
unchanged: disabled and out of the tab order.
|
|
1318
|
+
|
|
1319
|
+
- **Closing a `@kerfjs/ui` `Workbench` panel from its own control by mouse
|
|
1320
|
+
returns focus in Safari.** Safari never focuses a clicked button, so
|
|
1321
|
+
pressing an open panel's Hide control blurred the focused control to the
|
|
1322
|
+
page before the click collapsed the panel, and `wireWorkbench` — which
|
|
1323
|
+
returns focus only when it is inside the closing panel — left focus on the
|
|
1324
|
+
page body instead of the toggle that opened it. A press that starts inside
|
|
1325
|
+
the panel while focus is there now counts as focus inside, so the close
|
|
1326
|
+
returns it; focus the user has moved to another element since is left
|
|
1327
|
+
alone.
|
|
1328
|
+
- **A collapsed `@kerfjs/ui` `CollapsiblePanel` or `ResizableRegion` leaves
|
|
1329
|
+
the Tab order.** Both kept collapsed content in the DOM, only slid out and
|
|
1330
|
+
clipped, so Tab still reached its controls; focusing one scrolled the clipped
|
|
1331
|
+
panel, and a `CollapsiblePanel`'s `aria-hidden` put those focusable controls
|
|
1332
|
+
inside a hidden subtree. A collapsed `CollapsiblePanel` now renders `inert`
|
|
1333
|
+
beside its `aria-hidden`, and a collapsed `ResizableRegion` renders its
|
|
1334
|
+
content wrapper `inert`, with no wiring needed. The slide-out still animates,
|
|
1335
|
+
each `restoreControl` stays outside the inert subtree and reachable, and
|
|
1336
|
+
`wireSidebar` still hands focus to the panel's toggle when it collapses from
|
|
1337
|
+
inside.
|
|
1338
|
+
|
|
1339
|
+
- **A collapsed `@kerfjs/ui` `Workbench` rail's restore control no longer
|
|
1340
|
+
covers an expanded bottom drawer.** A `leftRail` / `rightRail`
|
|
1341
|
+
`restoreControl` floats in the Workbench's bottom corner, which the bottom
|
|
1342
|
+
drawer's column reaches once the rail collapses, so with the drawer expanded
|
|
1343
|
+
the control sat on the drawer's content. An expanded inline drawer now
|
|
1344
|
+
publishes its top edge as a CSS anchor scoped to its Workbench, and the
|
|
1345
|
+
rail's control floats the restore inset above it, returning to the corner
|
|
1346
|
+
when the drawer collapses. An overlay drawer (static or `responsiveOverlayAt`)
|
|
1347
|
+
still covers the control as every open overlay does, a nested Workbench's
|
|
1348
|
+
drawer moves only its own rails' controls, and engines without anchor
|
|
1349
|
+
positioning keep the corner.
|
|
1350
|
+
|
|
1351
|
+
- **A collapsed `@kerfjs/ui` `Workbench` panel's content leaves the Tab order.**
|
|
1352
|
+
A collapsed rail or bottom drawer kept its content in the DOM, only slid out
|
|
1353
|
+
and clipped, so Tab still reached its controls; focusing one (at a narrow
|
|
1354
|
+
width, the collapsed inspector's or output drawer's own Hide control)
|
|
1355
|
+
scrolled the clipped panel and slid its hidden content back over the work
|
|
1356
|
+
area. The panel's content wrapper now renders `inert` while `collapsed`, with
|
|
1357
|
+
no wiring needed, so neither Tab nor assistive technology reaches it; the
|
|
1358
|
+
slide-out still animates and the `restoreControl`, which lives outside the
|
|
1359
|
+
content, stays reachable.
|
|
1360
|
+
|
|
1361
|
+
- **A `kerfjs/overlay` popover or tooltip in a transformed or filtered modal
|
|
1362
|
+
`<dialog>` now lands against its anchor.** A surface rendered into a
|
|
1363
|
+
dialog's `[data-kerf-overlay-host][data-morph-skip]` slot is
|
|
1364
|
+
`position: fixed`, and a dialog styled with `transform`, `filter`,
|
|
1365
|
+
`perspective`, `contain`, or `will-change` becomes the containing block for
|
|
1366
|
+
fixed descendants, so the surface used to land offset by the dialog's
|
|
1367
|
+
position (and scale). `positionAnchored` (and so `autoReposition`,
|
|
1368
|
+
`popover()`, and `tooltip()`) now measures the element's fixed-position
|
|
1369
|
+
containing block with a transient 1px probe and maps its viewport
|
|
1370
|
+
coordinates into it, compensating translation and scale. Rotated or skewed
|
|
1371
|
+
containing blocks are not compensated.
|
|
1372
|
+
- **The `@kerfjs/ui` UX catalog demonstrates a standalone restore control.**
|
|
1373
|
+
The CollapsiblePanel route gains a "Restore control" example: a bottom
|
|
1374
|
+
drawer that collapses to a `FloatingToolbar` `restoreControl` in its frame's
|
|
1375
|
+
corner, wired with `wireSidebar` so the control restores the drawer and
|
|
1376
|
+
focus moves between it and the drawer's own hide toggle.
|
|
1377
|
+
|
|
1378
|
+
- **A `@kerfjs/ui` collapsed rail's restore control no longer covers an open
|
|
1379
|
+
bottom drawer beside it.** When a `CollapsiblePanel` or `ResizableRegion`
|
|
1380
|
+
with a `restoreControl` shared its container with an expanded bottom
|
|
1381
|
+
drawer — a bottom `CollapsiblePanel` or bottom `ResizableRegion` — as a
|
|
1382
|
+
sibling or inside the app's work-area column, the control floated in the
|
|
1383
|
+
container's bottom corner, on top of the drawer. It now floats the same
|
|
1384
|
+
inset above the drawer's top edge, follows the drawer as it resizes, and
|
|
1385
|
+
returns to the corner when the drawer collapses. A drawer nested deeper,
|
|
1386
|
+
inside the work area's own content, does not move it. This uses CSS anchor
|
|
1387
|
+
positioning; engines without it keep the container corner.
|
|
1388
|
+
|
|
1389
|
+
- **An open `@kerfjs/ui` `CollapsiblePanel` or `ResizableRegion` overlay now
|
|
1390
|
+
covers other panels' restore controls, as a `Workbench` overlay does.** A
|
|
1391
|
+
collapsed panel's `restoreControl` stacked at z-index 42, above
|
|
1392
|
+
`wireSidebar`'s compact overlay and its backdrop and above an overlay
|
|
1393
|
+
`ResizableRegion`, so it floated over an open overlay and stayed clickable
|
|
1394
|
+
behind the overlay's focus trap. `--kui-collapsible-panel-restore-z` now
|
|
1395
|
+
defaults to two less than `--kui-collapsible-panel-overlay-z` (38),
|
|
1396
|
+
`--kui-resizable-region-restore-z` to two less than
|
|
1397
|
+
`--kui-resizable-region-overlay-z` (39), and the compact backdrop's
|
|
1398
|
+
`--kui-collapsible-panel-backdrop-z` to one less than the overlay (still
|
|
1399
|
+
39), so an app that moves the overlay z-index keeps the order. When the
|
|
1400
|
+
overlay closes, the control shows and works again.
|
|
1401
|
+
- **An open `@kerfjs/ui` `Workbench` overlay now covers other panels'
|
|
1402
|
+
restore controls.** Restore controls stacked above every overlay, so a
|
|
1403
|
+
collapsed drawer's corner restore control floated over an open right-rail
|
|
1404
|
+
overlay's content. An open overlay is now the top layer, like a sheet over
|
|
1405
|
+
the view beneath it: `--kui-workbench-restore-z` defaults to two less than
|
|
1406
|
+
`--kui-workbench-overlay-z` (39 by default, previously 42), below the
|
|
1407
|
+
drawer and rail overlays. When the overlay closes, the control shows and
|
|
1408
|
+
works again; a collapsed overlay passes pointer events through to it.
|
|
1409
|
+
- **`@kerfjs/ui` `wireWorkbench` overlay panels now take focus and keep Tab
|
|
1410
|
+
inside them,** like `wireSidebar`'s compact overlay (the ARIA dialog
|
|
1411
|
+
pattern). An open overlay covers the work area, yet focus stayed on the
|
|
1412
|
+
control that opened it and Tab walked through the covered editor. A wired
|
|
1413
|
+
panel that opens while it presents as an overlay now focuses its first
|
|
1414
|
+
focusable control (usually the close control in its header); while it is
|
|
1415
|
+
open, Tab and Shift+Tab cycle through its controls, and Escape, its own
|
|
1416
|
+
close control, or an outside press leaves it, returning focus to the
|
|
1417
|
+
opener. Panels opening inline never move focus. Focus returned on close
|
|
1418
|
+
also skips an opener inside another panel that has closed since.
|
|
1419
|
+
`dismissOverlays: false` leaves all of this to the app.
|
|
1420
|
+
- **`@kerfjs/ui` `Workbench` keeps its work-area minimum beside fixed rails
|
|
1421
|
+
and above the bottom drawer.** `mainMinSize` used to apply only beside a
|
|
1422
|
+
resizable rail, so two fixed 280px rails on a ~730px Workbench left the
|
|
1423
|
+
editor 174px; now every inline rail, fixed or resizable, shrinks in
|
|
1424
|
+
proportion (its content following the shown width) to keep the work area
|
|
1425
|
+
`mainMinSize` px wide. A new `mainMinHeight` prop (default 120, `0` turns it
|
|
1426
|
+
off) is the drawer's counterpart: an inline drawer, fixed or resizable,
|
|
1427
|
+
shrinks when the Workbench gets shorter, and `wireWorkbench` stops a
|
|
1428
|
+
resizable drawer where the work area would drop below that height (reported
|
|
1429
|
+
in `aria-valuemax`) instead of only at the drawer's own `max`.
|
|
1430
|
+
- **A `@kerfjs/ui` Workbench rail squeezed below its minimum now reports the
|
|
1431
|
+
width it shows.** When the Workbench was too narrow for a rail's `min`
|
|
1432
|
+
beside the work-area minimum, the rail showed about 172px while its
|
|
1433
|
+
separator still announced `aria-valuenow="180"`. The separator now reports
|
|
1434
|
+
the shown width, with `aria-valuemin` and `aria-valuemax` pinned to it: the
|
|
1435
|
+
WAI-ARIA window splitter's value is the separator's actual position and must
|
|
1436
|
+
sit inside its range, and the separator cannot move a rail the container
|
|
1437
|
+
holds there. Resizing still commits no less than `min`, and the configured
|
|
1438
|
+
range returns once there is room. A `ResizableRegion` a parent clamps below
|
|
1439
|
+
its `min` reports the same way.
|
|
1440
|
+
- **`KUI-L018` (`kerf-ui-analyze` / `kerf-ui-doctor`) now measures contrast and
|
|
1441
|
+
follows the cascade.** When a `--wa-color-{tone}-fill-loud` override and its
|
|
1442
|
+
`on-loud` are both literal colors (including `light-dark()`), a pair below
|
|
1443
|
+
4.5:1 in either color scheme is reported with its ratio, and an unpaired
|
|
1444
|
+
literal fill that still clears 4.5:1 against the theme's default `on-loud`
|
|
1445
|
+
is no longer reported. An `on-loud` set in an ancestor scope (`:root`, or the
|
|
1446
|
+
leading selector of a descendant chain) or in another stylesheet the same
|
|
1447
|
+
entry loads now counts as the pair.
|
|
1448
|
+
- **`kerf-component-catalog` lets a private application declare components
|
|
1449
|
+
without a fake `exports` map.** In a private package
|
|
1450
|
+
(`"private": true`), a component's `publicExports` items may omit `subpath`; the generator
|
|
1451
|
+
verifies each name against the component's `source` file instead of
|
|
1452
|
+
requiring a `./dist/<path>.js` export the bundled app never emits. Publishable
|
|
1453
|
+
packages still need every subpath in `exports`. The v2 catalog schema makes
|
|
1454
|
+
`subpath` optional to match, and the `kerfjs/ui-composition` rule resolves a
|
|
1455
|
+
subpath-less export only by its source file.
|
|
1456
|
+
- **`kerf-ui-doctor` and the `kerfjs/ui-public-boundaries` ESLint rule no
|
|
1457
|
+
longer flag the documented `class="kui-app-root"` as KUI-L101 private
|
|
1458
|
+
anatomy.** The `@kerfjs/ui` component catalog now has a `document-baseline`
|
|
1459
|
+
composition entry for `@kerfjs/ui/document.css` that lists `kui-app-root`
|
|
1460
|
+
as its public class, with a catalog demo route showing a `SplitView` filling
|
|
1461
|
+
the root's definite height.
|
|
1462
|
+
- The `@kerfjs/ui` heading guidance (`ai/skill.md`, `llms.txt`,
|
|
1463
|
+
`docs/component-selection.md`, `docs/layout.md`) now sizes a panel title to
|
|
1464
|
+
its track: extra-large `ToolbarText` stays the title size for a page, view,
|
|
1465
|
+
dialog, or pane that holds it, while a narrow peripheral rail or drawer
|
|
1466
|
+
names itself with the default size, since extra-large truncated "Inspector"
|
|
1467
|
+
to "Ins…" in a 160px Workbench rail.
|
|
1468
|
+
- **A `@kerfjs/ui` `Workbench` panel open when `wireWorkbench` wired up no
|
|
1469
|
+
longer drops focus to the page when it closes from inside.** Such a panel
|
|
1470
|
+
had no recorded opener, so without a restore control closing it from its
|
|
1471
|
+
own header left focus on `<body>`. Focus now falls back to a control
|
|
1472
|
+
outside the panel whose `aria-controls` names it (or an element inside it).
|
|
1473
|
+
Each panel renders a stable `id` for that — `<id>-left-rail`,
|
|
1474
|
+
`<id>-right-rail`, and `<id>-bottom-drawer` — so a toggle can name the
|
|
1475
|
+
panel it controls.
|
|
1476
|
+
- **`@kerfjs/ui` `wireWorkbench` keeps overlay panels exclusive.** At a narrow
|
|
1477
|
+
width two overlay rails could both be open and, together wider than the
|
|
1478
|
+
Workbench, the right rail covered the left rail's own close control. Opening
|
|
1479
|
+
a panel while it presents as an overlay now closes every other open overlay
|
|
1480
|
+
panel, the bottom drawer included, like `wireSidebar`'s compact overlays;
|
|
1481
|
+
panels presenting inline are never closed by it. New option
|
|
1482
|
+
`exclusiveOverlays` (default `true`); pass `false` to let overlays stay open
|
|
1483
|
+
together.
|
|
1484
|
+
- **A `@kerfjs/ui` `CollapsiblePanel` or `ResizableRegion` restore control
|
|
1485
|
+
stays inside its container.** `restoreControl` was `position: fixed` to the
|
|
1486
|
+
viewport, so a panel or region embedded in a page floated it over unrelated
|
|
1487
|
+
page content. It now floats in the `restorePosition` corner of the element
|
|
1488
|
+
that holds the panel (or region), scrolls with it, and is inset by the
|
|
1489
|
+
component's restore inset plus the unsafe area of the edges that container
|
|
1490
|
+
reaches, so a full-viewport shell still places it clear of them. While the
|
|
1491
|
+
control is shown, that container isolates its stacking and becomes its
|
|
1492
|
+
containing block; an app that positions the container itself keeps its own
|
|
1493
|
+
`position`. As in `Workbench`, the corner owns the inset, so a
|
|
1494
|
+
`FloatingToolbar` hosting the control no longer doubles it.
|
|
1495
|
+
- The `@kerfjs/ui` UX catalog's Workbench page gains a responsive overlay
|
|
1496
|
+
drawer example: an output drawer with `responsiveOverlayAt: "narrow"` that
|
|
1497
|
+
takes its own track beside a wide editor and becomes a transient overlay
|
|
1498
|
+
over the editor's bottom once the Workbench is 704px or narrower, starting
|
|
1499
|
+
hidden there and closing from its own header, on Escape, or on a press
|
|
1500
|
+
outside it. The Workbench guide shows the pattern.
|
|
1501
|
+
- A `@kerfjs/ui` `Workbench` now stacks overlay rails above an overlay bottom
|
|
1502
|
+
drawer. With the drawer and a rail both presented as overlays, their order
|
|
1503
|
+
followed the markup, so the drawer covered the left rail while the right
|
|
1504
|
+
rail covered the drawer. Both rails now stack above the drawer, mirroring
|
|
1505
|
+
the inline layout where rails span the full height beside the drawer's
|
|
1506
|
+
column, and the right rail stacks above the left; restore controls stay
|
|
1507
|
+
above every overlay. The drawer's overlay z-index is now one less than
|
|
1508
|
+
`--kui-workbench-overlay-z`.
|
|
1509
|
+
- **A `@kerfjs/ui` `Workbench` overlay rail can always be closed from inside
|
|
1510
|
+
itself.** At a narrow width, the UX catalog's inspector overlay covered the
|
|
1511
|
+
editor toolbar's "Hide inspector" toggle, leaving only Escape or an outside
|
|
1512
|
+
press to close it. The catalog's resizable rails now carry their own close
|
|
1513
|
+
control in their `Pane` header, and the Workbench guide recommends the same
|
|
1514
|
+
for any panel that may present as an overlay. `wireWorkbench` now returns
|
|
1515
|
+
focus stranded in a panel however the panel closed — including through the
|
|
1516
|
+
app's own control inside it, and for inline panels — to the control that
|
|
1517
|
+
opened it, else to its restore control; before, only an Escape, an outside
|
|
1518
|
+
press, or a breakpoint collapse returned it.
|
|
1519
|
+
- **A `@kerfjs/ui` `Workbench` restore control stays inside its Workbench.**
|
|
1520
|
+
`restoreControl` was `position: fixed` to the viewport, so an embedded
|
|
1521
|
+
Workbench (the UX catalog's, for one) floated it over the page footer. It is
|
|
1522
|
+
now anchored to a corner of the Workbench itself — the drawer's in the
|
|
1523
|
+
corner of the work-area column, so it never covers an expanded rail — inset
|
|
1524
|
+
by `--kui-workbench-restore-inset` plus the unsafe area of the edges that
|
|
1525
|
+
corner reaches, and it scrolls with the Workbench. The Workbench now
|
|
1526
|
+
isolates its own stacking, so its overlays and restore controls never stack
|
|
1527
|
+
against the page around it. A `FloatingToolbar` can host the control
|
|
1528
|
+
without doubling the inset: the corner owns it, and `FloatingToolbar` now
|
|
1529
|
+
honors `--kui-floating-toolbar-inset` set on an ancestor as well as on
|
|
1530
|
+
itself. The catalog's collapsed console restores from such a toolbar.
|
|
1531
|
+
- **`@kerfjs/ui` `wireWorkbench` makes overlay panels transient.** A rail
|
|
1532
|
+
whose `responsiveOverlayAt` breakpoint applied stayed expanded over the
|
|
1533
|
+
editor until the user found its toggle. Pass a panel's app-owned
|
|
1534
|
+
`collapsed` signal (`panels: { leftRail: { collapsed } }`; `size` is now
|
|
1535
|
+
optional, so a panel need not be resizable) and, like `wireSidebar`'s
|
|
1536
|
+
compact overlay, the wiring collapses it when the breakpoint begins to
|
|
1537
|
+
apply (without the collapse motion) and gives back its inline state when
|
|
1538
|
+
the breakpoint stops applying or the wiring is disposed. An open overlay
|
|
1539
|
+
panel, responsive or `presentation: "overlay"`, closes on Escape or on a
|
|
1540
|
+
press that starts and ends outside it, and focus stranded inside returns to
|
|
1541
|
+
the control that opened it, else to its restore control. It is on by
|
|
1542
|
+
default for panels given `collapsed`; `dismissOverlays: false` turns it
|
|
1543
|
+
off. The UX catalog's resizable example wires its rails this way.
|
|
1544
|
+
- A `@kerfjs/ui` `Workbench` rail with `presentation: "overlay"` now renders
|
|
1545
|
+
exactly its size. Out of flow, it sized to its content plus its 1px
|
|
1546
|
+
separator border, so a 280px rail measured 281px; it now takes its size
|
|
1547
|
+
explicitly, as the responsive overlays do. Rails and the drawer are also
|
|
1548
|
+
`border-box` now, so a panel's `size` is its whole track, border included,
|
|
1549
|
+
even in an app without a global `box-sizing` reset, and overlay content
|
|
1550
|
+
fills the panel inside its separator instead of painting over it.
|
|
1551
|
+
- **A `@kerfjs/ui` `Workbench` bottom drawer with `presentation: "overlay"`
|
|
1552
|
+
is visible again.** Its content is positioned against the drawer's bottom
|
|
1553
|
+
edge, so the out-of-flow overlay box had no height of its own and collapsed
|
|
1554
|
+
to its 1px border. An overlay drawer now takes its configured height (220px
|
|
1555
|
+
by default, or its `size`), covers the bottom of the work-area column, and
|
|
1556
|
+
clamps its content to the overlay maximum height. The drawer also accepts
|
|
1557
|
+
`responsiveOverlayAt: "narrow" | "compact"` like the rails: below that
|
|
1558
|
+
Workbench width it presents as the same overlay, without a separator and
|
|
1559
|
+
never resizable, while the work area takes the full height and keeps its
|
|
1560
|
+
bottom safe-area inset beneath it.
|
|
1561
|
+
- The bundled AI assistant configs (`ai/skill.md`, `ai/cursorrules`) now
|
|
1562
|
+
explain the `data-kerf-overlay-host` opt-in next to the one-`mount()`-per-root
|
|
1563
|
+
rule: a modal `<dialog>` of your own that anchors kerf popovers or tooltips
|
|
1564
|
+
needs `<div data-kerf-overlay-host data-morph-skip></div>`, the one allowed
|
|
1565
|
+
nested-mount boundary. The `kerfjs/ai-assistant-configs` lint rule reports
|
|
1566
|
+
installed copies as stale until they are refreshed.
|
|
1567
|
+
- The opt-in `rebuiltListeners` dev warning no longer reports a popover or
|
|
1568
|
+
tooltip closing inside a modal dialog's host slot. Removals inside a
|
|
1569
|
+
`[data-kerf-overlay-host][data-morph-skip]` slot belong to the surface's own
|
|
1570
|
+
mount, so the enclosing mount now ignores them, as its re-renders already
|
|
1571
|
+
did. A listener lost to a re-render inside the surface is still reported,
|
|
1572
|
+
once.
|
|
1573
|
+
- **A popover or tooltip in a modal `<dialog>` no longer lingers after the
|
|
1574
|
+
dialog is removed when its anchor was moved out first.** A surface rendered
|
|
1575
|
+
into a dialog's host slot left with the dialog, but because its anchor was
|
|
1576
|
+
still in the document, kerf kept it registered as open, and a tooltip on
|
|
1577
|
+
that anchor never showed again. A slot-hosted surface now closes itself
|
|
1578
|
+
(without calling `onDismiss`) when it leaves the document, and the tooltip
|
|
1579
|
+
shows again on the next hover or focus.
|
|
1580
|
+
- **A popover inside a modal `<dialog>` is reachable by Tab in Safari on
|
|
1581
|
+
macOS.** A popover or tooltip rendered into a dialog's host slot follows the
|
|
1582
|
+
dialog's own Tab order, and WebKit on macOS skips buttons and links there
|
|
1583
|
+
unless the system keyboard-navigation setting is on. kerf now gives each
|
|
1584
|
+
implicitly tabbable control in a slot-hosted surface an explicit
|
|
1585
|
+
`tabindex="0"` when it opens and again before each Tab keypress, the same
|
|
1586
|
+
way its focus trap already did. Authored tabindexes, disabled controls, and
|
|
1587
|
+
surfaces outside a slot are left alone.
|
|
1588
|
+
- `@kerfjs/ui` `Workbench` rails gain `responsiveOverlayAt: "narrow" |
|
|
1589
|
+
"compact"`, mirroring `ResizableRegion`'s `responsiveFillAt`: below the
|
|
1590
|
+
Workbench's own 704px (`narrow`) or 448px (`compact`) width the rail
|
|
1591
|
+
presents as an overlay over the work area, without a separator and never
|
|
1592
|
+
resizable, and inline above it — the CSS decides, so apps no longer derive
|
|
1593
|
+
`presentation` from a `deviceClass()` check. The bottom drawer ignores it.
|
|
1594
|
+
A collapsed overlay rail (with `presentation: "overlay"` too) now drops its
|
|
1595
|
+
surface and shadow instead of leaving an empty panel-sized box over the work
|
|
1596
|
+
area. The UX catalog's resizable example uses it and now renders at phone
|
|
1597
|
+
widths, and its collapsed console's restore control is a `ToolbarControlGroup`
|
|
1598
|
+
toggle that actually restores the console.
|
|
1599
|
+
- `@kerfjs/ui` `Workbench` keeps a minimum work area beside resizable rails.
|
|
1600
|
+
Two rails dragged near their 480px maximum on a ~1024px Workbench used to
|
|
1601
|
+
leave the editor a sliver; now the work area keeps at least `mainMinSize`
|
|
1602
|
+
px (a new `Workbench` prop, default 320, `0` turns it off). `wireWorkbench`
|
|
1603
|
+
stops drag and keyboard resizing where the work area would drop below it
|
|
1604
|
+
(and reports that reachable maximum in `aria-valuemax`), and when the
|
|
1605
|
+
Workbench narrows, resizable rails shrink in proportion to their sizes, with
|
|
1606
|
+
their content following the shown width, instead of squeezing the work
|
|
1607
|
+
area. Workbenches without a resizable rail render exactly as before. The UX
|
|
1608
|
+
catalog's resizable example adds a resizable inspector to show it.
|
|
1609
|
+
- `@kerfjs/ui`'s static analyzer (`kerf-ui-analyze`, and the analyzer stage of
|
|
1610
|
+
`kerf-ui-doctor`) reports a new review finding, `KUI-L018`, when a
|
|
1611
|
+
stylesheet overrides a loud tone fill (`--wa-color-{tone}-fill-loud` for
|
|
1612
|
+
neutral, brand, success, warning, danger, or pop) without setting the
|
|
1613
|
+
matching `--wa-color-{tone}-on-loud` in the same scope. The theme pairs each
|
|
1614
|
+
loud fill with a foreground that clears WCAG AA, and an unpaired override
|
|
1615
|
+
keeps that foreground on a fill it was never checked against. The pair must
|
|
1616
|
+
share a selector (after resolving nesting and splitting selector lists) and
|
|
1617
|
+
at-rule scope in the same stylesheet, in one rule or separate ones; another
|
|
1618
|
+
tone's `on-loud` does not count. It is a review finding because it checks
|
|
1619
|
+
the pairing, not the contrast; pass `--fail-on-review` to gate it. The
|
|
1620
|
+
`webawesome-theme.md` customization example now sets both halves of each
|
|
1621
|
+
pair.
|
|
1622
|
+
- **Tooltips and popovers also stay visible when your own code opens a modal
|
|
1623
|
+
`<dialog>`.** `kerfjs/overlay` already moved every open tooltip and popover
|
|
1624
|
+
above a modal dialog it opened itself, but a dialog opened with a bare
|
|
1625
|
+
`dialog.showModal()` went unnoticed and left them inert beneath it. While
|
|
1626
|
+
any kerf surface is open, one capture-phase listener on the document now
|
|
1627
|
+
catches the dialog's `toggle` event and re-hosts them the same way, one task
|
|
1628
|
+
after the `showModal()` call. The listener is removed once nothing is open.
|
|
1629
|
+
An engine that fires no `toggle` event for `<dialog>` still misses such a
|
|
1630
|
+
dialog; open it through `overlay()` there.
|
|
1631
|
+
- **A popover inside a modal `<dialog>` is clickable.** Browsers make
|
|
1632
|
+
everything outside an open modal dialog inert, so a popover lifted above the
|
|
1633
|
+
dialog was visible but its buttons could not be clicked or focused, and
|
|
1634
|
+
kerf's one-mount-per-tree rule kept it from rendering inside a kerf-mounted
|
|
1635
|
+
dialog. `popover()` and `tooltip()` whose anchor sits inside an open modal
|
|
1636
|
+
`<dialog>` now render into the dialog's host slot, an element marked
|
|
1637
|
+
`data-kerf-overlay-host data-morph-skip`, so they are part of the modal and
|
|
1638
|
+
fully interactive: Tab reaches them, and Escape closes the popover before the
|
|
1639
|
+
dialog. A dialog `kerfjs/overlay` opened gets the slot automatically (an
|
|
1640
|
+
empty `display: contents` element added on first use). For your own
|
|
1641
|
+
`<dialog>`, add `<div data-kerf-overlay-host data-morph-skip></div>` to its
|
|
1642
|
+
markup, or pass `container`. The slot is the one place a second `mount()`
|
|
1643
|
+
may live inside a mounted tree: the enclosing mount's morph, nesting guard,
|
|
1644
|
+
and binding and `each()` marker scans all stop there. The `kerfjs/dev`
|
|
1645
|
+
`inert` warning now fires only when no slot was used.
|
|
1646
|
+
- `@kerfjs/ui` `Workbench` rails and the bottom drawer can be drag-resized,
|
|
1647
|
+
as an opt-in per panel. `WorkbenchPanel` gains `resizable: true | { min, max }`
|
|
1648
|
+
(off by default, so existing Workbenches render unchanged); a resizable panel
|
|
1649
|
+
gets a separator on its inner edge with the same focusable
|
|
1650
|
+
`role="separator"` contract, grip, and 20px hit target as a
|
|
1651
|
+
`ResizableRegion` handle. The new `@kerfjs/ui/wire-workbench` subpath's
|
|
1652
|
+
`wireWorkbench(root, { id, panels, deviceClass?, storage?, onResize? })`
|
|
1653
|
+
drives it: live pointer drags, arrow / Shift+arrow / Home / End keys,
|
|
1654
|
+
clamping to the limits, and commits to the app-owned size signals, with
|
|
1655
|
+
optional `storageKey` persistence. Collapsing keeps a panel's size, so it
|
|
1656
|
+
expands back to the size it had, and resizing is suspended on compact device
|
|
1657
|
+
classes and for overlay or hidden panels. The UX catalog's Workbench page
|
|
1658
|
+
adds a resizable example. `wireResizableRegions` now drives only handles that
|
|
1659
|
+
are direct children of a `ResizableRegion`, so it never double-drives a
|
|
1660
|
+
Workbench separator under the same root.
|
|
1661
|
+
- `@kerfjs/ui` elevation shadows follow the color scheme, with or without the
|
|
1662
|
+
optional `@kerfjs/ui/webawesome.css` theme. Kerf's theme layer set
|
|
1663
|
+
`--wa-shadow-s` / `-m` / `-l` to fixed light-mode shadows (14%, 11%, and 9%
|
|
1664
|
+
black), which all but vanished against the dark page, and the
|
|
1665
|
+
`--kui-shadow-s` / `--kui-shadow-l` foundation fallbacks copied those fixed
|
|
1666
|
+
values. The theme now pairs each light alpha with a stronger dark one (50%,
|
|
1667
|
+
40%, 32%) through `light-dark()`, and the foundation fallbacks mirror them, so
|
|
1668
|
+
selected segments and app tabs, compact overlay panels, and Web Awesome
|
|
1669
|
+
surfaces that read `--wa-shadow-*` keep their elevation in dark mode. Light mode is unchanged. The browser parity test
|
|
1670
|
+
now compares every `--kui-shadow-*` token with and without the theme in both
|
|
1671
|
+
schemes.
|
|
1672
|
+
- `@kerfjs/ui` text on loud tone fills clears WCAG AA in both color schemes,
|
|
1673
|
+
with or without the optional `@kerfjs/ui/webawesome.css` theme. White
|
|
1674
|
+
`on-loud` text reached about 2.2:1 on the success fill, 3.5:1 on the light
|
|
1675
|
+
brand fill (`#0088ff`), and 3.6:1 on the danger fill (`#ff383c`) in both
|
|
1676
|
+
schemes. Kerf's theme layer and the foundation fallbacks now pair each
|
|
1677
|
+
fill with a foreground chosen to keep the tone's look: success keeps its
|
|
1678
|
+
bright green with dark text (`#1d1d1f`, 7.6:1), as warning already did; light
|
|
1679
|
+
brand and danger keep white text on a slightly deeper fill of the same hue
|
|
1680
|
+
(`#0075db`, 4.6:1; `#eb0005`, 4.6:1); dark danger keeps its fill with dark
|
|
1681
|
+
text (`#111113`, 5.3:1), as dark brand already did. Web Awesome accent
|
|
1682
|
+
buttons, badges, and checked controls pick this up, and accent `wa-badge`s now
|
|
1683
|
+
use their variant's `on-loud` foreground by default instead of a fixed dark
|
|
1684
|
+
color (`--kui-wa-badge-accent-foreground` still overrides it). The browser
|
|
1685
|
+
contrast test asserts every tone's `on-loud` over `fill-loud` pair, including
|
|
1686
|
+
pop and neutral, in both schemes with and without the theme.
|
|
1687
|
+
- `@kerfjs/ui` brand, success, and warning text clears WCAG AA on the lowered
|
|
1688
|
+
page background in light mode, with or without the optional
|
|
1689
|
+
`@kerfjs/ui/webawesome.css` theme. Their light `on-quiet` values (`#1e6ef4`,
|
|
1690
|
+
`#008932`, `#a16a00`) passed on the white surface but reached only about
|
|
1691
|
+
4.1:1 on `#f2f2f7`. Kerf's theme layer and the foundation fallbacks now use
|
|
1692
|
+
the same hues a step darker: `#0c62f3` (4.64:1), `#007f2e` (4.62:1), and
|
|
1693
|
+
`#966200` (4.65:1) on the page, about 5.2:1 on white. Links, selected tabs,
|
|
1694
|
+
segmented and toolbar selections, and token-search accents that read
|
|
1695
|
+
`brand-on-quiet` darken with it. Dark mode is unchanged. The browser contrast
|
|
1696
|
+
test no longer skips light text over the page background.
|
|
1697
|
+
- `@kerfjs/ui` surfaces, links, and focus rings match the optional
|
|
1698
|
+
`@kerfjs/ui/webawesome.css` theme without it. Three foundation fallbacks
|
|
1699
|
+
pointed at a different token than the theme: `--kui-color-surface-raised`
|
|
1700
|
+
fell back to the surface color (the theme's dark raised surface is
|
|
1701
|
+
`#2c2c2e`), `--kui-color-text-link` fell back to the brand fill (`#0088ff`,
|
|
1702
|
+
about 3.5:1 on white) instead of the brand on-quiet text color, and the focus
|
|
1703
|
+
ring used the brand fill instead of the theme's focus color (`#007aff`). Each
|
|
1704
|
+
now falls back to the theme's value. The browser parity test now covers every
|
|
1705
|
+
`--kui-color-*` token the foundation declares, plus the focus ring, instead of
|
|
1706
|
+
only the tone tokens.
|
|
1707
|
+
- The `@kerfjs/ui` UX catalog's `StateBanner` route adds a placeholder with a
|
|
1708
|
+
badge for every tone, next to the existing badge-less placeholder, so visual
|
|
1709
|
+
review and captures show the solid badge's loading skeleton on each tone's
|
|
1710
|
+
fill. A browser test checks that each tone's badged placeholder is present.
|
|
1711
|
+
- `@kerfjs/ui` toned text stays readable in dark mode without the optional
|
|
1712
|
+
`@kerfjs/ui/webawesome.css` theme. The foundation's fallbacks for the
|
|
1713
|
+
success, warning, and danger `on-quiet` and `on-normal` tokens were single
|
|
1714
|
+
light-mode colors, so dark danger `Text` rendered dark red on a dark surface
|
|
1715
|
+
(under 3:1). Every tone token now falls back to the same light and dark
|
|
1716
|
+
pair the theme assigns, including `brand-on-loud` (dark text on the pale dark
|
|
1717
|
+
brand fill instead of white) and `neutral-border-quiet`. Light-mode colors
|
|
1718
|
+
are unchanged. A browser test bundles the package with and without the theme
|
|
1719
|
+
and asserts that every tone token resolves identically in both color schemes
|
|
1720
|
+
and that toned text clears WCAG AA in dark mode.
|
|
1721
|
+
- `@kerfjs/ui` danger text and badges are readable in dark mode, and every
|
|
1722
|
+
tone's `StateBanner` detail and quiet or outline `Badge` text now clears
|
|
1723
|
+
WCAG AA. The danger tone was the only one without an on-fill foreground: its
|
|
1724
|
+
solid badge fill and `StateBanner` text reused `danger-on-quiet`, a mid red
|
|
1725
|
+
in dark mode (banner detail 4.0:1, badge placeholder skeleton 2.25:1). A new
|
|
1726
|
+
`--kui-color-danger-on-fill` token (also accepted by `uiColor()`) matches the
|
|
1727
|
+
other tones' pale dark values, lifting the dark danger banner text to 8.2:1.
|
|
1728
|
+
The banner detail no longer dims to 78% opacity, which left the light info,
|
|
1729
|
+
success, warning, and danger details at 3.4 to 4.3:1; it reads as secondary
|
|
1730
|
+
through its smaller type, and the neutral detail uses the quiet text color.
|
|
1731
|
+
Quiet and outline badges use their tone's on-fill text, lifting light brand,
|
|
1732
|
+
success, and warning from about 4.1:1 to at least 5:1.
|
|
1733
|
+
|
|
1734
|
+
- A `@kerfjs/ui` placeholder skeleton inside a solid `Badge`, such as the
|
|
1735
|
+
badge of a `StateBanner` placeholder, is now clearly visible. The generic
|
|
1736
|
+
skeleton tint, 12% of the text color, almost disappeared on the badge's
|
|
1737
|
+
saturated fill in every tone. A solid badge now tints its skeleton from its
|
|
1738
|
+
own foreground color, which contrasts with the fill in every tone in light
|
|
1739
|
+
and dark mode. Quiet and outline badges keep the generic tint, which already
|
|
1740
|
+
reads on their pale fills. Geometry is unchanged.
|
|
1741
|
+
- The `@kerfjs/ui` SVG design templates render badges as filled pills: the
|
|
1742
|
+
`StateBanner` badge and the `ListHeader` and `List` count pills used to show
|
|
1743
|
+
as plain text, because the template build loaded only a hand-written list of
|
|
1744
|
+
stylesheets that omitted `badge.css`. The build now loads each component's
|
|
1745
|
+
full stylesheet set from its generated browser entry, the same set a
|
|
1746
|
+
consumer's browser import loads, so a nested component's CSS can no longer be
|
|
1747
|
+
missed. The `ListHeader` and `List` templates also pick up the label inset
|
|
1748
|
+
from `text.css`, so a header label now lines up with the list items below it,
|
|
1749
|
+
as it does in the live component.
|
|
1750
|
+
- A `@kerfjs/ui` `StateBanner` placeholder's badge sits at the live badge's
|
|
1751
|
+
height on every platform. With a skeleton instead of text, the badge took a
|
|
1752
|
+
baseline from the skeleton's bottom edge and sat about a pixel higher, by an
|
|
1753
|
+
amount that depended on the platform's font metrics.
|
|
1754
|
+
- A `@kerfjs/ui` `StateBanner` with a long, wrapping title keeps its badge
|
|
1755
|
+
one item gap after the title's last word, and its detail uses the full copy
|
|
1756
|
+
width. The wide flex layout shrank the title and detail together, which left
|
|
1757
|
+
a large gap before the badge and truncated the detail early. The title,
|
|
1758
|
+
badge, and detail now flow as inline text: the detail stays beside the title
|
|
1759
|
+
while it fits whole, and otherwise moves to its own line, truncating only at
|
|
1760
|
+
the full width. A wrapped title's lines stay evenly spaced.
|
|
1761
|
+
- A `@kerfjs/ui` `ListHeader` disclosure toggle disabled with `actionDisabled`
|
|
1762
|
+
is dimmed by the component itself, at the same tone as a disabled action
|
|
1763
|
+
button, and shows the `not-allowed` cursor. It used to rely on Web Awesome's
|
|
1764
|
+
native `button:disabled` dimming, so without `webawesome.css` it looked
|
|
1765
|
+
enabled. Disabled toggles and action buttons also no longer show hover
|
|
1766
|
+
feedback without `webawesome.css`.
|
|
1767
|
+
- A `@kerfjs/ui` `ListActionRow` placeholder keeps its `busy` spinner. The
|
|
1768
|
+
whole status line used to collapse into one skeleton, dropping the spinner
|
|
1769
|
+
the component draws from a known prop. Now only the `status` text becomes a
|
|
1770
|
+
skeleton, matching `ListItem`.
|
|
1771
|
+
- **A tooltip or popover already showing stays visible when a modal opens.**
|
|
1772
|
+
Lifting a surface above an open modal `<dialog>` was decided only when the
|
|
1773
|
+
surface opened, so a tooltip or popover already on screen when a modal
|
|
1774
|
+
dialog opened was left inert beneath it. Whenever `kerfjs/overlay` opens a
|
|
1775
|
+
modal `<dialog>` (a `native: true` modal, or one it lifted), it now moves
|
|
1776
|
+
every open tooltip and popover into the top layer above the new dialog. One
|
|
1777
|
+
that is already a `[popover]` is re-shown so it stacks on top. The existing
|
|
1778
|
+
`kerfjs/dev` warnings still report an engine without the Popover API, and a
|
|
1779
|
+
surface with controls the modal keeps inert. A modal `<dialog>` the app
|
|
1780
|
+
opens itself, outside kerf, is not observed.
|
|
1781
|
+
- Moved `mount()`'s list-binding lifecycle (turning list markers into
|
|
1782
|
+
bindings, first-render row-contract validation, orphan pruning) into its own
|
|
1783
|
+
internal module, so `mount.ts` holds only the render orchestration. No
|
|
1784
|
+
behavior or public API change.
|
|
1785
|
+
- A narrow (576px and below) `@kerfjs/ui` `StateBanner` keeps its badge
|
|
1786
|
+
sized to its content, one item gap after the title and on the title's
|
|
1787
|
+
baseline, as in the wide layout. The narrow grid stretched the badge across
|
|
1788
|
+
the free column into a wide pill that sat less than 1px from the title.
|
|
1789
|
+
- Hovering a `@kerfjs/ui` `ListActionRow` placeholder with
|
|
1790
|
+
`trailingActionVisibility="interaction"` no longer reveals its disabled
|
|
1791
|
+
trailing button. A placeholder is inert, so it keeps the live row's resting
|
|
1792
|
+
state, with the trailing action hidden until an interaction it cannot take.
|
|
1793
|
+
- A `@kerfjs/ui` `ListItem` placeholder keeps its `busy` spinner. The whole
|
|
1794
|
+
trailing slot used to collapse into one skeleton, dropping the spinner the
|
|
1795
|
+
component draws from a known prop and shifting the label's width. Now only
|
|
1796
|
+
the values in that slot become skeletons: `status` text in its status line
|
|
1797
|
+
and author-supplied `trailing` content (always a value, since the component
|
|
1798
|
+
cannot tell a chevron from a count) each get their own.
|
|
1799
|
+
- `@kerfjs/ui` placeholders keep the live disabled tone of a control the
|
|
1800
|
+
author disabled. A disabled `ListItem` or `ListActionRow`, a
|
|
1801
|
+
`trailingActionDisabled` button, a disabled `SegmentedControl` choice, or a
|
|
1802
|
+
`ListHeader` with `actionDisabled` rendered at full opacity as a
|
|
1803
|
+
placeholder, because the placeholder reset could not tell its own
|
|
1804
|
+
`disabled` attribute from the author's. Those controls now carry a
|
|
1805
|
+
component-owned `data-kui-disabled="true"` marker whenever their disabled
|
|
1806
|
+
prop is set; the disabled tone keys on it and the placeholder reset skips
|
|
1807
|
+
it. The marker is reserved: the extension-attribute props cannot set it.
|
|
1808
|
+
- `@kerfjs/ui` adds a browser test that renders every placeholder-capable
|
|
1809
|
+
component live and as a placeholder from identical props and diffs each
|
|
1810
|
+
element's geometry and computed chrome, so any future drift outside a value
|
|
1811
|
+
slot fails. It caught one more: a `ListActionRow` placeholder's `status`
|
|
1812
|
+
line was 3.5px shorter than the live line; the status line now always
|
|
1813
|
+
keeps a full line height.
|
|
1814
|
+
- `@kerfjs/ui`'s `Select` placeholder hint now matches the live hint: 14px
|
|
1815
|
+
text at a 1.6 line height, 7px below the box. It was 12px / 1.4 with a 4px
|
|
1816
|
+
gap, so a placeholder with a `hint` was 8.6px shorter than the loaded
|
|
1817
|
+
control and the content below it jumped when loading finished.
|
|
1818
|
+
- `@kerfjs/ui`'s `StateBanner` placeholder adds a detail skeleton only when
|
|
1819
|
+
`detail` is set, matching the live banner. Previously every placeholder
|
|
1820
|
+
showed a detail line, so a banner without one changed shape when loading
|
|
1821
|
+
finished. Pass the banner's real `title` and `detail` with `placeholder`;
|
|
1822
|
+
an empty `detail` now means no detail line.
|
|
1823
|
+
- `@kerfjs/ui`'s `AppTab` placeholder pill is no longer dimmed to 50% with a
|
|
1824
|
+
`not-allowed` cursor when `webawesome.css` is loaded; it keeps the live
|
|
1825
|
+
tab's tone, and only its name is a skeleton.
|
|
1826
|
+
- `@kerfjs/ui`'s `ListHeader` placeholder no longer dims its action button
|
|
1827
|
+
(48%) or, in `toggle` mode, the whole title and label (Web Awesome's native
|
|
1828
|
+
50%), and its inert action and toggle give no hover feedback. A live
|
|
1829
|
+
`actionDisabled` header still dims its action.
|
|
1830
|
+
- `@kerfjs/ui`'s `SegmentedControl` placeholder now keeps its choices. The
|
|
1831
|
+
choices are known; only the selection is unknown, so a placeholder renders
|
|
1832
|
+
every choice's live label or icon at its live size, selects none, and stays
|
|
1833
|
+
inert. Previously every segment became a 4em text skeleton (widening icon
|
|
1834
|
+
segments from 40px to 60px) and was dimmed to 45%.
|
|
1835
|
+
- `@kerfjs/ui`'s `ListItem` and `ListActionRow` placeholders now match the
|
|
1836
|
+
live row. They are no longer dimmed (the rows' own `:disabled` tone and
|
|
1837
|
+
hover highlight skip placeholders), and a row with a `description` (or a
|
|
1838
|
+
`ListActionRow` `status`) keeps that line as a second, smaller skeleton, so
|
|
1839
|
+
the row keeps its live height instead of collapsing to one line. The
|
|
1840
|
+
`ListActionRow` icon placeholder is an 18px block centered in its icon slot,
|
|
1841
|
+
like `ListItem`'s.
|
|
1842
|
+
- `@kerfjs/ui` placeholders no longer look disabled. A component's
|
|
1843
|
+
`placeholder` keeps its controls `disabled` (inert and out of the keyboard
|
|
1844
|
+
order), but `skeleton.css` now neutralizes generic disabled chrome under
|
|
1845
|
+
`[data-placeholder="true"]`: full opacity and the default cursor instead of
|
|
1846
|
+
Web Awesome's native `button:disabled` dimming to 50% with a `not-allowed`
|
|
1847
|
+
cursor. A component's own opacity (a control hidden until interaction) still
|
|
1848
|
+
applies.
|
|
1849
|
+
- `toElement('<image href="…" />')` now returns an SVG `<image>`. The HTML
|
|
1850
|
+
parser rewrites an orphan `<image>` to an HTML `<img>` before kerf sees the
|
|
1851
|
+
tag, so it previously came back as an XHTML `img` that no `<svg>` would
|
|
1852
|
+
render. kerf now recognizes a leading `<image` tag in the raw input
|
|
1853
|
+
(case-insensitive, after leading whitespace) and parses it through the SVG
|
|
1854
|
+
wrapper. A genuine `<img>` is unchanged.
|
|
1855
|
+
- `delegateActions()` (`kerfjs/actions`) dispatches only the table's own
|
|
1856
|
+
entries. It used to look the action up with a plain property read, so a
|
|
1857
|
+
`data-action` naming an inherited `Object.prototype` member resolved:
|
|
1858
|
+
`"__proto__"` or `"hasOwnProperty"` threw a `TypeError` from the listener,
|
|
1859
|
+
and `"toString"` or `"constructor"` silently ran the inherited function.
|
|
1860
|
+
Those values are now ignored like any other action absent from the table.
|
|
1861
|
+
An own entry with such a name (`{ toString: … }`) still dispatches.
|
|
1862
|
+
- `toElement()` now puts orphan camelCase SVG fragments in the SVG namespace.
|
|
1863
|
+
`toElement('<linearGradient>…</linearGradient>')`, and likewise `clipPath`,
|
|
1864
|
+
`radialGradient`, and `foreignObject`, previously returned an XHTML
|
|
1865
|
+
`LINEARGRADIENT` element that no `<svg>` would render, because the lookup
|
|
1866
|
+
compared the HTML parser's lowercased tag against camelCase names. The
|
|
1867
|
+
fragment is now wrapped in `<svg>` and XML-parsed like `<path>` or `<g>`,
|
|
1868
|
+
and the returned element keeps its camelCase `localName`.
|
|
1869
|
+
- **A tooltip or popover no longer outlives its anchor.** When the anchor left
|
|
1870
|
+
the document, most commonly because the modal containing it closed, the
|
|
1871
|
+
`tooltip()` or `popover()` stayed on screen: a removed element fires no
|
|
1872
|
+
`pointerleave` or `blur`. Both now watch their anchor with the same removal
|
|
1873
|
+
tracking as `kerfjs/attach`. The tooltip hides, and the popover closes
|
|
1874
|
+
without calling `onDismiss`, as soon as the anchor leaves the document.
|
|
1875
|
+
Moving the anchor within the document keeps them open. A tooltip show still
|
|
1876
|
+
pending when its anchor is removed never appears.
|
|
1877
|
+
- **Overlays opened over a modal `<dialog>` are visible and usable.** A browser
|
|
1878
|
+
makes everything outside an open modal dialog inert and paints a plain
|
|
1879
|
+
element beneath it, so a `tooltip()`, `popover()`, or `confirm()` opened
|
|
1880
|
+
from inside a `native: true` dialog without `native` itself was invisible and
|
|
1881
|
+
could not be used. While a modal `<dialog>` is open, `kerfjs/overlay` now
|
|
1882
|
+
hosts such a surface in the top layer anyway: a tooltip or popover becomes a
|
|
1883
|
+
`[popover]`, and a nested modal becomes its own `<dialog>`. The browser's
|
|
1884
|
+
default `[popover]` / `<dialog>` styles then apply, as they do with `native:
|
|
1885
|
+
true`. Browsers keep a popover outside the modal dialog inert even in the top
|
|
1886
|
+
layer, so a lifted popover with buttons is visible but not clickable. A new
|
|
1887
|
+
always-on `kerfjs/dev` warning reports that case, and an engine without the
|
|
1888
|
+
needed API.
|
|
1889
|
+
- **A tooltip shown inside a modal no longer disables the modal.** `kerfjs/overlay`
|
|
1890
|
+
used to treat the most recently opened overlay as the only one that could
|
|
1891
|
+
react to input, so a `tooltip()` (which handles no input) inside an open
|
|
1892
|
+
modal left Escape doing nothing and let Tab leave the dialog. Every overlay,
|
|
1893
|
+
native or fallback, now joins one open-order stack, and each Escape, backdrop
|
|
1894
|
+
click, outside click, or trapped Tab goes to the topmost surface that handles
|
|
1895
|
+
it. A modal still blocks everything beneath it, and a surface that handles
|
|
1896
|
+
nothing never intercepts. A popover that dismisses on Escape gets it before
|
|
1897
|
+
the modal beneath. One click that is both outside a popover and on a modal's
|
|
1898
|
+
backdrop now closes only the popover. A native `<dialog>` is arbitrated with
|
|
1899
|
+
the fallback surfaces: one Escape closes one surface, and a fallback modal
|
|
1900
|
+
beneath an open dialog no longer takes the dialog's Tab. Closing a lower
|
|
1901
|
+
overlay first leaves focus in the overlay that holds it. An overlay opened
|
|
1902
|
+
from inside it then restores focus to the closed overlay's opener, not a
|
|
1903
|
+
detached node.
|
|
1904
|
+
- `kerfjs` now exports the `EachOptions<T>` type (`{ cacheKey?, key? }`), the
|
|
1905
|
+
options-object form of `each()`'s third argument, so a helper that builds or
|
|
1906
|
+
forwards those options can import the shape instead of restating it:
|
|
1907
|
+
`import { type EachOptions } from "kerfjs"`. Type-only; no runtime or bundle
|
|
1908
|
+
change.
|
|
1909
|
+
- Documentation: the companion-utility reference now matches the code. The
|
|
1910
|
+
router's `match()` / `activeClass()` compare a literal path prefix, so
|
|
1911
|
+
`match('/users/:id')` is never true. Link interception still applies to
|
|
1912
|
+
`target="_self"`, the no-match `outlet()` is `null` with `params` `{}`,
|
|
1913
|
+
components receive `(params, route)`, and hash mode keeps `route.hash` at
|
|
1914
|
+
`''`. Invalid `bindList` configuration throws only `RangeError`. `popover()`
|
|
1915
|
+
defaults `className` to `'kerf-popover'`, and the toast region is a reused
|
|
1916
|
+
`.kerf-toasts` element when one exists. A `resource()` `cacheKey` applies
|
|
1917
|
+
only to `run(input, fetcher)`, and throttle's `flush()` leaves the cooldown
|
|
1918
|
+
running. `delegate()` / `delegateCapture()` throw on an invalid selector at
|
|
1919
|
+
registration. The reference also now lists the `kerfjs/jsx-dev-runtime`
|
|
1920
|
+
alias, the `DEV_HOOKS` export of `kerfjs/dev`, and `Signal#peek()`.
|
|
1921
|
+
- Documentation: `morph(liveRoot, template)` is now described as it behaves.
|
|
1922
|
+
It reconciles only the root's children and never touches the root's own
|
|
1923
|
+
attributes, so `template` describes the content inside the root (an
|
|
1924
|
+
`Element` template contributes its child nodes). The examples no longer
|
|
1925
|
+
nest a duplicate `<article>` inside the live one. An `each()` list parent is
|
|
1926
|
+
not a children-only short-circuit: the diff still walks it and skips only
|
|
1927
|
+
the owned rows. The user-agent-owned `open` exception covers custom elements
|
|
1928
|
+
as well as `<details>` / `<dialog>`, and a content-only row change inside an
|
|
1929
|
+
`each()` list keeps its DOM node (and focus) unless the row's top-level tag
|
|
1930
|
+
changes or the same render also inserts, removes, or moves rows.
|
|
1931
|
+
- Every opt-in `kerfjs/dev` warning now ends with a silence hint that leads
|
|
1932
|
+
with the browser-reachable switch, for example "Silence it with
|
|
1933
|
+
enableWarnings({ narrowSet: false }) from kerfjs/dev, or set
|
|
1934
|
+
KERF_DEV_WARN_NARROW_SET=0 (or unset it) under Node." Previously the
|
|
1935
|
+
messages named only the environment variable, which a browser cannot reach.
|
|
1936
|
+
The docs now teach `enableWarnings()` wherever they showed an env-only
|
|
1937
|
+
switch, and name all three creation-time boundaries: `signal()`
|
|
1938
|
+
(`untrackedSignals`), `effect()` (`delegateInEffect`), and `mount()`
|
|
1939
|
+
(`rebuiltListeners`) decide once when created, so anything created before
|
|
1940
|
+
the install or the opt-in stays uncovered.
|
|
1941
|
+
- The `kerfjs/dev` rebuilt-listeners warning (`rebuiltListeners` /
|
|
1942
|
+
`KERF_DEV_WARN_REBUILT_LISTENERS`) now dedups once per `mount()`, as
|
|
1943
|
+
documented. Previously the first warning silenced every other mount for
|
|
1944
|
+
the life of the process, so a second mount with the same bug stayed quiet.
|
|
1945
|
+
- `kerfjs/router`: a second router on the same document no longer diverges
|
|
1946
|
+
from the URL after a link click. The first router's interceptor navigates
|
|
1947
|
+
with `pushState`, which fires no `popstate`, so the second router ignored
|
|
1948
|
+
the already-handled click and kept the old route. An already-handled click
|
|
1949
|
+
now re-syncs the route from the URL; when nothing navigated, it is a no-op.
|
|
1950
|
+
One router per document remains the supported shape: a second router does
|
|
1951
|
+
not follow another router's programmatic `navigate()` (no `popstate` fires
|
|
1952
|
+
and routers do not broadcast to each other) until the next `popstate` or
|
|
1953
|
+
link click. This is now documented and pinned by a test.
|
|
1954
|
+
- `kerfjs/scope`: a disposed `Scope` handle no longer leaks registrations.
|
|
1955
|
+
Previously `add` / `mount` / `effect` / `delegate` on a handle after its
|
|
1956
|
+
`dispose()` (or from a disposer while `dispose()` ran) pushed into a list
|
|
1957
|
+
nothing would ever run, leaving the effect subscribed or the listener
|
|
1958
|
+
attached. Such a registration now tears down at once: `add(fn)` calls `fn`
|
|
1959
|
+
immediately, and the wrappers create and immediately dispose.
|
|
1960
|
+
- `bindList` measured-height virtualization no longer applies a stale
|
|
1961
|
+
scroll-anchor correction. A `setHeight()` report for a row above the viewport
|
|
1962
|
+
is now resolved when the animation frame runs: if the source removed the key,
|
|
1963
|
+
moved it below the fold, or the user scrolled the row into view first,
|
|
1964
|
+
`scrollTop` is left alone, and several reports for one key in a frame net
|
|
1965
|
+
out. Fixed-height windows also clamp to the source length, so a list that
|
|
1966
|
+
shrinks while scrolled deep no longer pads far past its content.
|
|
1967
|
+
- `create-kerf-component` accepts an explicit `boundaries.rootClass: null` as
|
|
1968
|
+
an author decision, as its schema always allowed. A wrapper whose root
|
|
1969
|
+
element belongs to another catalog (a `rendersAs` wrapper) no longer has to
|
|
1970
|
+
invent a class to satisfy the generator. An absent `rootClass` still fails
|
|
1971
|
+
as undecided.
|
|
1972
|
+
- `@kerfjs/ui`'s `Select` placeholder now matches the live control. The label
|
|
1973
|
+
uses the field-label style (uppercase, extra-small, quiet, inset to line up
|
|
1974
|
+
with the value), and the chevron is the same glyph at the same size.
|
|
1975
|
+
Previously the placeholder drew a larger, regular-weight label at a
|
|
1976
|
+
different inset and a different, smaller chevron. Only the value is a
|
|
1977
|
+
skeleton; the chrome no longer changes when loading finishes.
|
|
1978
|
+
- **Wrapper components can declare the cataloged root they render.** A
|
|
1979
|
+
`kerf.components.json` component may set `composition.rendersAs`, for
|
|
1980
|
+
example `["@kerfjs/ui:toolbar-control-group"]`: a list of the roots it
|
|
1981
|
+
renders, any one of them, or nothing. `kerf-component-catalog` copies it
|
|
1982
|
+
into the generated v2 entry and fails unless every key resolves to a
|
|
1983
|
+
cataloged entry. `eslint-plugin-kerfjs`'s `ui-composition` rule, and so
|
|
1984
|
+
`kerf-ui-doctor`, now loads the application and third-party catalogs a
|
|
1985
|
+
`.kerf-ui-profile.json` declares. It resolves their components by package
|
|
1986
|
+
subpath or source file and checks a wrapper as each declared root: a zone
|
|
1987
|
+
must accept every root, each root's parent contract applies where the
|
|
1988
|
+
wrapper is placed, and a wrapper counts as zero or one child. Undeclared
|
|
1989
|
+
wrappers keep reporting `KUI-L202`.
|
|
1990
|
+
- `@kerfjs/ui`'s `AppTab` adds `pending` for a known tab that is still
|
|
1991
|
+
opening. It keeps the name visible in the quiet text color and uses it as
|
|
1992
|
+
the tab's accessible name, so a trailing spinner's label is not folded in.
|
|
1993
|
+
Like `placeholder`, it disables select and close, turns off dragging, and
|
|
1994
|
+
sets `aria-busy`, and it shows the consumer's `trailing` spinner. It keeps
|
|
1995
|
+
the live tab's pill geometry, so it swaps in place without shifting the bar.
|
|
1996
|
+
Dormant tabs (placeholder or pending) no longer reveal their disabled close
|
|
1997
|
+
button on hover.
|
|
1998
|
+
- `@kerfjs/ui`'s `wireSidebar` moves focus into an opening panel without
|
|
1999
|
+
scrolling it. A plain `focus()` scrolled the panel's clipped content to the
|
|
2000
|
+
target while it was still sliding in, then the slide unwound that scroll:
|
|
2001
|
+
a bottom drawer's content jumped up past its top and drifted back. The
|
|
2002
|
+
collapsible-sidebar recipe also floats the drawer's expand toggle in a
|
|
2003
|
+
`FloatingToolbar` at the bottom edge the drawer opens from, and drops the
|
|
2004
|
+
divider under the drawer's toolbar.
|
|
2005
|
+
- `@kerfjs/ui`'s `Skeleton` takes `block` (default `false`). It lays the
|
|
2006
|
+
placeholder out as `display: block` instead of `inline-block`, for a
|
|
2007
|
+
skeleton that stands in for a box rather than text: no text line and no
|
|
2008
|
+
baseline gap of its own. `ListItem`'s placeholder icon uses it.
|
|
2009
|
+
- `@kerfjs/ui/css-values` adds `remify(px)`, the runtime twin of source
|
|
2010
|
+
CSS's `remify(<px>)`. It turns a pixel count into a rem `CssLength` against
|
|
2011
|
+
the fixed 16px baseline, so `remify(18)` is `1.125rem`. `ListItem`'s
|
|
2012
|
+
placeholder icon now uses it at 18px, and `Skeleton` no longer forces a
|
|
2013
|
+
24px minimum width, so small placeholders keep the size they ask for.
|
|
2014
|
+
- `@kerfjs/ui/catalog`'s application-sized (`height: "app"`) example frame
|
|
2015
|
+
no longer loses taps in Safari/WebKit. The frame became the containing block
|
|
2016
|
+
for screen-fixed chrome through `contain: layout`. Combined with its clip,
|
|
2017
|
+
that made WebKit mis-hit-test any transformed element inside an open compact
|
|
2018
|
+
overlay. Every pressed `ListItem` scales, so taps on an overlay rail's items
|
|
2019
|
+
went to their section instead. The frame now uses an identity transform,
|
|
2020
|
+
which docks the same chrome without the bug and renders identically.
|
|
2021
|
+
- **Breaking (`@kerfjs/ui/catalog`):** `CatalogExample`'s viewport drops
|
|
2022
|
+
`height: "fill"` and `fillChildren`. The catalog lays examples out at their
|
|
2023
|
+
content height, so neither could give a specimen a definite height, and a
|
|
2024
|
+
child sized with `height: 100%` (a horizontal `ResizableRegion`, say)
|
|
2025
|
+
collapsed to its content. Use a fixed `height` (`short` through `app`) when
|
|
2026
|
+
a specimen must fill its frame. `minHeight` remains, documented as a minimum
|
|
2027
|
+
rather than a fill.
|
|
2028
|
+
- `create-kerf-component`'s `kerf.components.json` now accepts an optional
|
|
2029
|
+
`composition.wiring.stateAttributes` declaration (the wiring-owned state
|
|
2030
|
+
attributes `@kerfjs/ui`'s catalog now carries), so third-party
|
|
2031
|
+
component authors can list the `data-*` attributes their wiring helpers write
|
|
2032
|
+
(`{ name, on, helper, meaning }`). `kerf-component-catalog` passes them
|
|
2033
|
+
through to the generated `component-catalog-v2.json` and reports a non-`data-*`
|
|
2034
|
+
name, a duplicate name, an empty `on` or `meaning`, or a helper missing from
|
|
2035
|
+
`wiring.helpers` at its exact path.
|
|
2036
|
+
- `@kerfjs/ui`'s component catalog now declares **wiring-owned state
|
|
2037
|
+
attributes**. A new optional `wiring.stateAttributes` in
|
|
2038
|
+
`component-catalog-v2` (and per wiring item in v1) lists each `data-*`
|
|
2039
|
+
attribute a wiring helper writes at runtime, such as `data-handle-inset` and
|
|
2040
|
+
`data-resizing` from `wireResizableRegions`. For each one it records the
|
|
2041
|
+
element, the helper, and the meaning, so AI consumers and tools can tell
|
|
2042
|
+
them apart from attributes the app writes. Every first-party wiring helper
|
|
2043
|
+
is declared. `check:catalog` fails when a helper's declarations differ from
|
|
2044
|
+
what its source writes.
|
|
2045
|
+
- `@kerfjs/ui`'s `wireSidebar` no longer leaves focus inside a panel it just
|
|
2046
|
+
collapsed. When the collapse was triggered from a toggle inside the panel
|
|
2047
|
+
(the documented placement), focus now moves to that panel's toggle outside
|
|
2048
|
+
it, including one the app renders only once the panel is collapsed. The
|
|
2049
|
+
collapsible-sidebar recipe now shows one control per action: each panel's
|
|
2050
|
+
own collapse toggle while it is open, and the main header's expand toggle
|
|
2051
|
+
only while it is collapsed. Wide widths no longer show two rail toggles.
|
|
2052
|
+
- `@kerfjs/ui`'s `wireSidebar` keeps a compact overlay intact when the app
|
|
2053
|
+
re-renders the wired root without changing a panel. Previously, selecting a
|
|
2054
|
+
destination in an open compact rail morphed the root, which removed the
|
|
2055
|
+
overlay attributes and the backdrop, and the "overlay" rail fell back into
|
|
2056
|
+
the layout. The wire now re-applies its host attributes and backdrop after
|
|
2057
|
+
any such re-render, and the backdrop carries `data-morph-preserve`.
|
|
2058
|
+
- `@kerfjs/ui`'s `wireSidebar` now opens a compact overlay only on a user
|
|
2059
|
+
action. Wiring on a compact device, or crossing from a wide device class to
|
|
2060
|
+
a compact one, collapses every panel. The page is no longer covered by a
|
|
2061
|
+
backdrop and focus trap until dismissed. Crossing back to a wide class, or
|
|
2062
|
+
disposing the wire, restores each panel's remembered inline state.
|
|
2063
|
+
Persistence now stores only that inline choice, never an overlay's
|
|
2064
|
+
open/closed state. A new optional `WireSidebarPanel.inlineCollapsed`
|
|
2065
|
+
declares the inline default when an app seeds its signal from the device
|
|
2066
|
+
class, which avoids an open first frame on a compact device. The
|
|
2067
|
+
collapsible-sidebar recipe no longer opens its rail as a blocking overlay
|
|
2068
|
+
on load at compact widths.
|
|
2069
|
+
- `@kerfjs/ui`'s `ResizableRegion` handle keeps its hit target and focus ring
|
|
2070
|
+
whole at a clipping edge. When a parent clamps the region to its own edge
|
|
2071
|
+
(the catalog's 390px stage, an app shell with a `max-width` container),
|
|
2072
|
+
`wireResizableRegions` marks the region `data-handle-inset` and the handle
|
|
2073
|
+
moves inside it instead of overhanging the edge by 10px, where the frame cut
|
|
2074
|
+
its focus ring in half. The ring is now drawn inside the handle's box
|
|
2075
|
+
(`outline-offset: -3px`), so it is also no longer shaved where the region's
|
|
2076
|
+
ends meet a clipping edge.
|
|
2077
|
+
- `@kerfjs/ui`'s `wireResizableRegions` keeps a clamped separator's
|
|
2078
|
+
`aria-valuenow`/`aria-valuemax` current at rest. Previously, after a size was
|
|
2079
|
+
saved at a wider width, narrowing the parent or any unrelated re-render left
|
|
2080
|
+
the handle reporting the unclamped rendered size until it was next focused or
|
|
2081
|
+
resized. A `ResizeObserver` on each region and its parent and a
|
|
2082
|
+
`MutationObserver` for re-renders now re-clamp the report; the disposer
|
|
2083
|
+
disconnects both.
|
|
2084
|
+
- The `@kerfjs/ui` catalog's `height: "app"` example frame is now the
|
|
2085
|
+
containing block for screen-fixed chrome, so a compact `CollapsiblePanel`
|
|
2086
|
+
overlay, its backdrop, and restore controls dock to the frame's edges. The
|
|
2087
|
+
collapsible-sidebar recipe's compact rail was fixed to the page's left edge,
|
|
2088
|
+
24px outside the inset frame, so its leading edge looked cut off at 390px.
|
|
2089
|
+
- The `CollapsiblePanel` compact-overlay backdrop now covers its whole
|
|
2090
|
+
containing block. As a `<button>`, it did not stretch between its insets and
|
|
2091
|
+
scrimmed only a 35px strip.
|
|
2092
|
+
- `@kerfjs/ui`'s `wireResizableRegions` now bounds a resize to the space the
|
|
2093
|
+
region's parent actually leaves. When a parent clamps the track (a
|
|
2094
|
+
`max-width` stage, a narrow container), pointer and keyboard resizing stop at
|
|
2095
|
+
the visible size, and the separator reports that size in `aria-valuenow` and
|
|
2096
|
+
its visible maximum in `aria-valuemax`. Previously a region resized to 420
|
|
2097
|
+
inside a 340px stage announced 420 and needed several key presses before the
|
|
2098
|
+
separator moved. Focusing the handle reports a stale larger committed size as
|
|
2099
|
+
the shown size without committing it.
|
|
2100
|
+
- The UI evaluator's `KUI-B050` target-size rule now accepts a 44×36 target
|
|
2101
|
+
inside compact density (`[data-density="compact"]`), matching the 36px
|
|
2102
|
+
compact row height; standard density still requires 44×44, and the width
|
|
2103
|
+
floor is unchanged.
|
|
2104
|
+
- Fixed `kerf-ui-analyze` and the `eslint-plugin-kerfjs` UI rules treating
|
|
2105
|
+
every name in a catalog entry's `publicExports` as that entry's component,
|
|
2106
|
+
so a helper such as `uiColor` or `flex` called with an object whose keys
|
|
2107
|
+
matched a contract path (`uiColor({ choices: [...] })`) was checked as a
|
|
2108
|
+
`Select` or `List` call. Only component exports, whose names start with an
|
|
2109
|
+
uppercase letter as JSX component tags do, now resolve to an entry; the
|
|
2110
|
+
`@kerfjs/ui` catalog check fails if a cataloged `SafeHtml` render function
|
|
2111
|
+
is lowercase or any other runtime export is uppercase.
|
|
2112
|
+
- The `@kerfjs/ui` component catalog's `ToolbarText` entry now lists every
|
|
2113
|
+
token its `xlarge` size reads (`--kui-color-text`, `--kui-font-l`,
|
|
2114
|
+
`--kui-font-2xl`) in `publicTokens`; previously it named only the tokens of
|
|
2115
|
+
the smaller sizes.
|
|
2116
|
+
- The `@kerfjs/ui` browser evaluator's target-size check (`KUI-B050`) now
|
|
2117
|
+
measures the pointer-reachable area instead of only the border box: an
|
|
2118
|
+
undersized control is hit-tested outward from its center, so a transparent
|
|
2119
|
+
`::before`/`::after` hit layer (such as the `ListHeader` action's) counts,
|
|
2120
|
+
while `pointer-events: none` decoration, clipping, and overlapping
|
|
2121
|
+
neighbors do not. Diagnostic evidence now carries the measured size plus
|
|
2122
|
+
the border `box`.
|
|
2123
|
+
- Fixed the `eslint-plugin-kerfjs` `ui-css-values` messages (`KUI-L013`,
|
|
2124
|
+
`KUI-L014`, `KUI-L017`) listing suggested helpers with unbalanced backticks
|
|
2125
|
+
(``use `uiColor() or `foregroundColorVar().``); each helper is now wrapped
|
|
2126
|
+
as `` `uiColor()` ``.
|
|
2127
|
+
- Fixed `kerf-ui-analyze` skipping the CSS-value contracts of `@kerfjs/ui`
|
|
2128
|
+
`Select` and `Skeleton`. Its component lookup was keyed by display name, so
|
|
2129
|
+
the same-named Web Awesome catalog entries (`wa-select`, `wa-skeleton`) —
|
|
2130
|
+
which carry no CSS-value props — shadowed them, and a wrong-grammar helper
|
|
2131
|
+
such as `colorVar()` for `SelectChoice.color` went unreported although the
|
|
2132
|
+
ESLint rule caught it. Entries now resolve by `package:id`, using the
|
|
2133
|
+
selection catalog's public exports and import subpaths.
|
|
2134
|
+
- Fixed `@kerfjs/ui` monospace text rendering in the generic face (Courier on
|
|
2135
|
+
macOS Chromium) whenever Web Awesome was loaded: `--kui-font-mono` deferred to
|
|
2136
|
+
Web Awesome's `ui-monospace, monospace`, which Chromium does not resolve, and
|
|
2137
|
+
the trailing generic `monospace` kept Kerf's named fallbacks from ever
|
|
2138
|
+
applying. `--kui-font-mono` is now Kerf's own stack (`ui-monospace,
|
|
2139
|
+
SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace`),
|
|
2140
|
+
and the Web Awesome theme points `--wa-font-family-code` at it so Kerf `Text
|
|
2141
|
+
font="monospace"` and Web Awesome code text match.
|
|
2142
|
+
- The `@kerfjs/ui` Web Awesome theme now gives default-size single-line
|
|
2143
|
+
controls Kerf's 44px control height, so a plain `wa-input` or `wa-button`
|
|
2144
|
+
sits flush beside a Kerf `Select` instead of about 9px shorter. It sets
|
|
2145
|
+
`--wa-form-control-height` to `2.75em` (44px at the default size) on Button,
|
|
2146
|
+
Color Picker, Input, Known Date, Number Input, Radio Group, Select, Textarea,
|
|
2147
|
+
and Time Input; `size="large"` scales proportionally, `size="small"` keeps Web
|
|
2148
|
+
Awesome's compact height, and buttons inside a `ToolbarControlGroup` keep the
|
|
2149
|
+
group's 40px (32px compact) height. Tags, copy buttons, and toasts are
|
|
2150
|
+
unchanged.
|
|
2151
|
+
- Fixed an `@kerfjs/ui` `Select` menu drawing two separators when a choice with
|
|
2152
|
+
`separatorBefore` opened a separated group: the group's border plus a
|
|
2153
|
+
`wa-divider` left an empty band between the group title and its first
|
|
2154
|
+
choice. `separatorBefore` now only divides a choice from the previous choice
|
|
2155
|
+
in the same list; a group boundary is already the single separator.
|
|
2156
|
+
- Fixed `@kerfjs/ui` compact `ToolbarControlGroup` end highlights not being
|
|
2157
|
+
concentric with the group: controls sat 2px from the sides but 1px from the
|
|
2158
|
+
top and bottom. A compact group now has no padding, so its 32px controls sit
|
|
2159
|
+
on a 1px inset on every side with a radius of the group radius minus 1px
|
|
2160
|
+
(pill and rounded). In compact mixed groups the raised selection still paints
|
|
2161
|
+
over the border with the group's radius, neighboring controls still abut, and
|
|
2162
|
+
the trailing dropdown trigger keeps the 1px end inset instead of running onto
|
|
2163
|
+
the border. Compact groups are 2px narrower as a result.
|
|
2164
|
+
- Added `Toolbar.safeAreaEdges` to `@kerfjs/ui` so an app bar or bottom bar
|
|
2165
|
+
that sits at a screen edge, outside any `Pane` header or footer, can claim
|
|
2166
|
+
those edges for device safe areas. A toolbar there used to take only the
|
|
2167
|
+
inline edges an owner handed it, so the app-shell recipe's top bar never
|
|
2168
|
+
cleared the status area. Each claimed side pads by the routed
|
|
2169
|
+
`--kui-edge-inset-*` context, or the full device inset when nothing routes
|
|
2170
|
+
it, while the bar's surface and divider still paint through; the minimum
|
|
2171
|
+
height grows with the claimed block insets so a shrinking flex column keeps a
|
|
2172
|
+
whole control row clear. A claim inside a `Pane` header or footer adds
|
|
2173
|
+
nothing, since the pane already owns those edges. The app-shell recipe's app
|
|
2174
|
+
bar now claims the top edge and both sides.
|
|
2175
|
+
- Fixed `@kerfjs/ui` slide-motion `ResizableRegion` content overflowing its
|
|
2176
|
+
separator when a parent clamps the region. The expanded content kept the
|
|
2177
|
+
committed size even when a `max-width` or narrow container held the track
|
|
2178
|
+
smaller, so in the catalog at 390px a region resized to 420px showed text
|
|
2179
|
+
running past its handle. Expanded content now follows the region's actual
|
|
2180
|
+
track on both axes; the fixed expanded size still applies while collapsing, so
|
|
2181
|
+
the collapse keeps reading as a slide.
|
|
2182
|
+
- Fixed `@kerfjs/ui` `ResizableRegion` panes stopping at their content's
|
|
2183
|
+
height. The region's content now spans the whole region, like its separator,
|
|
2184
|
+
and a lone child (normally a `Pane`) fills it, so the app-shell recipe's
|
|
2185
|
+
navigation and inspector panes reach the bottom edge and scroll long content
|
|
2186
|
+
inside their own content slot instead of ending partway down the region.
|
|
2187
|
+
Several children still stack at their natural height.
|
|
2188
|
+
- Fixed `@kerfjs/ui` `Toolbar responsive="center-priority"` leaving an
|
|
2189
|
+
expanded center control at roughly half the row on narrow toolbars. The
|
|
2190
|
+
policy set the toolbar's own grid template inside a container query on the
|
|
2191
|
+
toolbar itself, which can never match, so the center kept only the first
|
|
2192
|
+
column track once the leading and trailing zones left. The dead rule is gone
|
|
2193
|
+
and the expanded center now spans every column track, filling the toolbar's
|
|
2194
|
+
content width at phone widths.
|
|
2195
|
+
- Fixed a nested `@kerfjs/ui` `List`, `Row`, or `Grid` inheriting its
|
|
2196
|
+
ancestor's `gap` and `flex`. Those props travel through private custom
|
|
2197
|
+
properties, which inherit, so a nested `List` without its own `gap` took the
|
|
2198
|
+
enclosing `List`'s gap (a heading unit inside a form got a 24px gap between
|
|
2199
|
+
its title and supporting line) and any nested instance without `flex` took the
|
|
2200
|
+
ancestor's flex growth. Every instance now resets its private values, so an
|
|
2201
|
+
omitted prop means that component's own default. The workspace-header recipe's
|
|
2202
|
+
breadcrumb now sits directly above its title and the List demo's section rows
|
|
2203
|
+
sit flush under their header, as intended.
|
|
2204
|
+
- Fixed `@kerfjs/ui` slide-motion `ResizableRegion` content staying at its
|
|
2205
|
+
previous width during a live resize until the app re-rendered the committed
|
|
2206
|
+
size. `wireResizableRegions` now applies the live size to the sliding content
|
|
2207
|
+
as well as the track, for pointer drags and key presses, so the content fills
|
|
2208
|
+
the region (including a safe-area edge extent) throughout the drag and a later
|
|
2209
|
+
collapse still slides it out at the resized width.
|
|
2210
|
+
- Fixed the `@kerfjs/ui` `ListHeader` action's pointer target being only its
|
|
2211
|
+
fitted 36px square although the guidance promises 44px. A transparent hit
|
|
2212
|
+
layer now extends the target to at least 44 × 44 around the square without
|
|
2213
|
+
moving layout or enlarging the painted hover surface. Compact headers are
|
|
2214
|
+
36px tall like the rest of the compact List family, so their action target is
|
|
2215
|
+
44px wide and clamped to the header height, never reaching into a neighboring
|
|
2216
|
+
row.
|
|
2217
|
+
- Fixed re-renders closing open Web Awesome popups: the morph (and the keyed-list
|
|
2218
|
+
attribute fast path) no longer removes a live `open` attribute from custom
|
|
2219
|
+
elements (`wa-select`, `wa-dropdown`, `wa-dialog`, …) when the template omits
|
|
2220
|
+
it — the same user-agent-owned treatment `<details>` / `<dialog>` already
|
|
2221
|
+
had. A template can still set `open`; closing stays imperative.
|
|
2222
|
+
- The `@kerfjs/ui` composer-form recipe's supporting line now sits directly
|
|
2223
|
+
under its toolbar title instead of 24px lower, because a nested `List` no
|
|
2224
|
+
longer inherits the form's section gap (see the nested `List`/`Row`/`Grid`
|
|
2225
|
+
fix above). The guidance now
|
|
2226
|
+
also states that `ToolbarText` below `xlarge` is intentionally quiet identity
|
|
2227
|
+
text with no tone option. A normal-color subsection title is `Text` with an
|
|
2228
|
+
`h2`–`h6` variant, or `ListHeader` for a list or menu section.
|
|
2229
|
+
- Fixed `@kerfjs/ui` `ToolbarText size="small"` labels rendering in a
|
|
2230
|
+
monospace fallback face in Chromium. The small size used the code-font token,
|
|
2231
|
+
which resolves through Web Awesome's `ui-monospace, monospace` stack; Chromium
|
|
2232
|
+
does not recognize `ui-monospace`, so labels such as a pane's "Workspace" or
|
|
2233
|
+
"Ready" fell back to the generic monospace font. Every `ToolbarText` size now
|
|
2234
|
+
keeps the inherited UI sans stack.
|
|
2235
|
+
- Fixed `@kerfjs/ui` `ListInsetControl` not stretching its control as
|
|
2236
|
+
documented. Its `justify-content: stretch` is a no-op on a flex row, so a
|
|
2237
|
+
`Select`, `wa-input`, or `wa-button` inside stayed at its natural width. Each
|
|
2238
|
+
direct child now grows to fill the inset row (several children share it with
|
|
2239
|
+
the 8px gap) and may shrink below its intrinsic width instead of overflowing.
|
|
2240
|
+
- `@kerfjs/ui` `List`, `Row`, and `Grid` can now be the layout root of a
|
|
2241
|
+
fixed-height parent. New `fill` takes the parent's full height (use `flex`
|
|
2242
|
+
inside a flex layout as before), and new `rootAttributes` carries safe
|
|
2243
|
+
`data-*` metadata while the component's structural attributes stay
|
|
2244
|
+
protected. The app-shell and collapsible-sidebar recipes now use a filling
|
|
2245
|
+
List or Row root instead of a frame `Pane` whose scroll owner never scrolled;
|
|
2246
|
+
the collapsible sidebar's rail and drawer now reach the real frame edges, so
|
|
2247
|
+
they paint through safe areas as the layout model intends. The UX catalog
|
|
2248
|
+
adds an application-sized `height: "app"` (592px) specimen frame, and both
|
|
2249
|
+
recipes use it instead of the cramped 360px `tall` frame.
|
|
2250
|
+
- Fixed a `@kerfjs/ui` stacked `Toolbar` clipping its last trailing action at
|
|
2251
|
+
phone widths (the compact-toolbar recipe's "More" button at 390px). A
|
|
2252
|
+
toolbar's trailing zone now wraps whole control groups onto another
|
|
2253
|
+
trailing-aligned row under every `responsive` policy, and a
|
|
2254
|
+
`responsive="stack"` toolbar wraps its stacked center and trailing groups the
|
|
2255
|
+
same way, so no action is ever clipped or shrunk. Stacked zones also span the
|
|
2256
|
+
full toolbar width; they previously lost 16px to empty column tracks. New:
|
|
2257
|
+
`responsive="wrap"` keeps a heading on one row while everything fits and
|
|
2258
|
+
otherwise moves the trailing zone below a whole title instead of truncating
|
|
2259
|
+
it. The loading-inspector recipe uses it, so its narrow heading shows the
|
|
2260
|
+
whole record title instead of an ellipsis.
|
|
2261
|
+
- `@kerfjs/ui` layouts and `Pane` now handle device safe areas as one model.
|
|
2262
|
+
Pane and panel backgrounds, separators, and dividers paint through unsafe
|
|
2263
|
+
areas; content is padded only on the edges a region actually reaches, and
|
|
2264
|
+
block insets become scroll padding so content can scroll under the status bar
|
|
2265
|
+
or home indicator but always scroll clear. `Workbench` rails and drawer,
|
|
2266
|
+
`CollapsiblePanel`, and the `SplitView` list grow by the inset they dock to.
|
|
2267
|
+
A center pane picks up the inset when an adjacent rail collapses, and
|
|
2268
|
+
interior or nested edges are never double-inset. A header or footer `Toolbar`
|
|
2269
|
+
keeps its dividers edge to edge. New: `Pane.safeAreaEdges` (default all
|
|
2270
|
+
sides; `[]` opts out), the `--kui-safe-area-*` device-inset tokens (default
|
|
2271
|
+
`env(safe-area-inset-*)`), the inherited `--kui-edge-inset-*` edge context for
|
|
2272
|
+
app-owned layouts, and `--kui-resizable-region-edge-extent`. Nothing changes
|
|
2273
|
+
on devices or pages without safe-area insets.
|
|
2274
|
+
- Fixed a `@kerfjs/ui` `ListHeader` action sitting about 4px outside the
|
|
2275
|
+
trailing toolbar action of the Pane header above it. Its fitted action now
|
|
2276
|
+
centers in the same 44px slot a toolbar control occupies at the 8px inline
|
|
2277
|
+
margin, so trailing icon actions share one axis. The action's size, 18px
|
|
2278
|
+
visual, and hover square are unchanged.
|
|
2279
|
+
- **Breaking (`@kerfjs/ui`, 5.0 beta):** `SelectChoice.color` now accepts only
|
|
2280
|
+
a foreground color, the new `CssForegroundColor` brand from
|
|
2281
|
+
`@kerfjs/ui/css-values`. `uiColor()` returns it for foreground token names
|
|
2282
|
+
(the new `UiForegroundColorName`: the `*-on-*` roles, `text`, `text-quiet`,
|
|
2283
|
+
`text-link`, and the `*-text` aliases), and the new
|
|
2284
|
+
`foregroundColorVar('--app-icon-color', fallback?)` returns it for an
|
|
2285
|
+
application-owned property. The `--kui-color-success` / `warning` / `danger`
|
|
2286
|
+
/ `pop` / `accent` compatibility aliases are quiet fills that read like
|
|
2287
|
+
foreground colors; colored with one, an icon rendered at about 1.2:1
|
|
2288
|
+
contrast. `uiColor('success')` and the other fill, border, and surface tokens
|
|
2289
|
+
still return a plain `CssColor` and no longer type-check as an icon color,
|
|
2290
|
+
and neither does a plain `colorVar()` value. Migrate
|
|
2291
|
+
`color: uiColor('success')` to `uiColor('success-on-quiet')` and
|
|
2292
|
+
`color: colorVar('--app-color')` to `foregroundColorVar('--app-color')`;
|
|
2293
|
+
`uiColor('…-on-…')` calls keep compiling unchanged. The catalog's CSS-value
|
|
2294
|
+
contract for `choices[].color` lists `uiColor` and `foregroundColorVar`, so
|
|
2295
|
+
`eslint-plugin-kerfjs` also reports `colorVar()` there (`KUI-L014`), and
|
|
2296
|
+
the source and packed consumer type contracts pin the rejected fills.
|
|
2297
|
+
|
|
2298
|
+
- Fixed an `@kerfjs/ui` icon-only `Select` beside other controls in one
|
|
2299
|
+
`ToolbarControlGroup`. A compact Select no longer eats 1px of the group's
|
|
2300
|
+
4px gap on each side (the lone-trigger padding overlap now applies only when
|
|
2301
|
+
it is the group's only child), a compact rounded Select uses its siblings'
|
|
2302
|
+
item radius instead of the lone-slot radius, and the Select now shows the
|
|
2303
|
+
same hover pill as its sibling buttons (a lone Select lifts the whole group,
|
|
2304
|
+
like a popup-menu dropdown). In a group with per-control focus rings
|
|
2305
|
+
(`focusRing="control"`, the default) the Select paints the same ring as its
|
|
2306
|
+
siblings even when `focusRingOwner="group"` was set, which previously left it
|
|
2307
|
+
with no visible focus. A compact icon-only `<button>` is now a 32px circle,
|
|
2308
|
+
so a selected compact segment is no longer a 34x32 oval. The
|
|
2309
|
+
ToolbarControlGroup catalog adds a "Select beside actions" example.
|
|
2310
|
+
|
|
2311
|
+
- Changed the `@kerfjs/ui` icon-only `Select`
|
|
2312
|
+
(`selectedPresentation="icon-only"`) to keep its disclosure caret. The
|
|
2313
|
+
selected icon and caret now form one pill with the same geometry as a
|
|
2314
|
+
`ToolbarControlGroup` popup-menu `wa-dropdown` trigger: 8px inline padding on
|
|
2315
|
+
both sides, 12px from the icon to the caret, the same caret glyph size, and a
|
|
2316
|
+
40px default or 32px compact control height, so the icon sits left of center
|
|
2317
|
+
to balance the caret. In a single `ToolbarControlGroup` the group now sizes to
|
|
2318
|
+
the trigger (a 70x44 default or 68x34 compact pill, wider than it is tall)
|
|
2319
|
+
with an even inset on every side and a concentric radius, for pill and
|
|
2320
|
+
rounded shapes, so the group-owned outline or halo ring follows the pill while
|
|
2321
|
+
focused and while the listbox is open. Before, a default-size trigger was a
|
|
2322
|
+
32px square left-aligned in its 44px group, and the caret was hidden. The
|
|
2323
|
+
Select catalog adds a default-size "Toolbar icon" example beside the compact
|
|
2324
|
+
one.
|
|
2325
|
+
|
|
2326
|
+
- Fixed the `@kerfjs/ui` icon-only `Select` focus ring in a
|
|
2327
|
+
`ToolbarControlGroup` not matching the control: the compact trigger grew its
|
|
2328
|
+
group into a 36x34 oval and its disclosure caret overflowed the trailing edge.
|
|
2329
|
+
The group-owned ring now stays concentric with the trigger while focused and
|
|
2330
|
+
while the listbox is open. The Select demo's "Quiet" option icon also used the success fill alias
|
|
2331
|
+
as its color and was nearly invisible; it now uses `success-on-quiet`, and the
|
|
2332
|
+
`SelectChoice.color` guidance names foreground tokens.
|
|
2333
|
+
|
|
2334
|
+
- `@kerfjs/ui` UX catalog: the ten production recipes now compose Kerf
|
|
2335
|
+
components, layouts, and Web Awesome surfaces through their public
|
|
2336
|
+
configuration alone, with no route stylesheets, inline styles, or
|
|
2337
|
+
styling-only classes. The list-detail dialog is rebuilt from `DialogSurface`,
|
|
2338
|
+
the dialog's own labeled header and close control, `SplitView` (a full-screen
|
|
2339
|
+
list-to-detail drill-down on compact devices), aligned `ListHeader`s, a
|
|
2340
|
+
`ValueTable`, and footer actions. The desktop application shell switches to one
|
|
2341
|
+
pane at a time below desktop sizes through `deviceClass()`. Page and pane
|
|
2342
|
+
headings stack instead of hiding their titles at narrow widths, and supporting
|
|
2343
|
+
copy, content-item text, and loading skeletons sit on the shared 17px text
|
|
2344
|
+
inset. The catalog frames each recipe in a `CatalogExample` viewport that the
|
|
2345
|
+
recipe declares through an exported `presentation`, and recipe ownership notes
|
|
2346
|
+
render as that example's note. `npm run check:recipes` now fails on a recipe
|
|
2347
|
+
stylesheet, an inline style, a class outside the published layout and Web
|
|
2348
|
+
Awesome vocabulary, a non-public stylesheet import, or recipe-specific rules in
|
|
2349
|
+
the catalog shell stylesheet.
|
|
2350
|
+
- Fixed `@kerfjs/ui/webawesome.css` `wa-dialog` titles given as a plain-string
|
|
2351
|
+
`label` sitting at the bare surface inset; they now inset by one bordered
|
|
2352
|
+
item step (`--kui-layout-item-padding` + 1px) so the header label lines up
|
|
2353
|
+
with body text and icons. Slotted label elements are unaffected.
|
|
2354
|
+
- Fixed icon + label `wa-button`s inside `ToolbarControlGroup` stacking the
|
|
2355
|
+
icon above the label; their label part now lays out as a single nowrap row,
|
|
2356
|
+
matching native mixed buttons.
|
|
2357
|
+
- `prompt()` / `form()` now reject with a `TypeError` when a validator returns
|
|
2358
|
+
a promise (validators are synchronous), instead of resolving the value and
|
|
2359
|
+
leaving an unhandled rejection. `prompt()` also re-reads its input on every
|
|
2360
|
+
OK, so a reactive `render` that replaces the input no longer submits the
|
|
2361
|
+
detached original's stale value, and a removed input rejects the promise.
|
|
2362
|
+
- A local `npm run check` run with `KERF_SKIP_PACKAGE_GATES=1` no longer
|
|
2363
|
+
records a verified tree, so the pre-push hook cannot skip the sibling-package
|
|
2364
|
+
gates that run left out.
|
|
2365
|
+
- `kerfjs/overlay`: a `prompt()` `validate` or `form()` `field.validate` that
|
|
2366
|
+
throws now closes the dialog (full teardown, focus restored) and rejects the
|
|
2367
|
+
returned promise with the original error. Previously the error escaped the
|
|
2368
|
+
OK click or Enter handler, and the dialog stayed open with an OK button that
|
|
2369
|
+
could never succeed. Validators are synchronous; this is the error channel
|
|
2370
|
+
for a validator bug.
|
|
2371
|
+
- `kerfjs/overlay`: when a `form()` field input disappears after the dialog
|
|
2372
|
+
opens (for example a reactive `render` re-render drops it), OK and Enter now
|
|
2373
|
+
close the dialog and reject the promise with an error naming the missing
|
|
2374
|
+
field. Previously `form()` resolved `null`, which
|
|
2375
|
+
reads as a Cancel, and threw from the click handler.
|
|
2376
|
+
- `@kerfjs/ui`: a multiline `ListItem` whose label fits on one line now
|
|
2377
|
+
centers its icon on that line. The row's 44px minimum used to stretch the
|
|
2378
|
+
grid track, which centered the label but left the start-aligned icon about
|
|
2379
|
+
1.4px above it.
|
|
2380
|
+
- `@kerfjs/ui`: a `ToolbarControlGroup` with `focusRing="outline"` or
|
|
2381
|
+
`"halo"` keeps its group focus ring while a `Select` inside it has its
|
|
2382
|
+
listbox open. Chromium and WebKit did not carry `:focus-within` from the
|
|
2383
|
+
focused option to the group, so the ring disappeared.
|
|
2384
|
+
- UX catalog: the foundation palette's Brand swatch uses the brand on-fill
|
|
2385
|
+
color again, the Spacer demo gives its flexible spacer room to grow again,
|
|
2386
|
+
and the TokenSearchField adoption demo again wires its chip keyboard and
|
|
2387
|
+
`onEdit` readout. The catalog demo rework had broken all three.
|
|
2388
|
+
- Documented and pinned how `tooltip()` surfaces a failed show from its delay
|
|
2389
|
+
timer: the half-built tooltip is rolled back, the original error escapes the
|
|
2390
|
+
timer for the host to report (a window `error` event in browsers, matching
|
|
2391
|
+
kerf's other deferred callbacks), and the tooltip stays armed so the next
|
|
2392
|
+
hover or focus tries again.
|
|
2393
|
+
- Fixed the dialog helpers (`confirm` / `prompt` / `form` / `choice`) leaving
|
|
2394
|
+
wiring behind when a step after the overlay opened failed. Their post-open
|
|
2395
|
+
wiring (required-slot checks, the click-table `delegate()`, the Enter-key
|
|
2396
|
+
listener) is now part of construction: a failure removes the listeners
|
|
2397
|
+
already wired, closes the overlay (including a `native` `<dialog>`), restores
|
|
2398
|
+
focus, and rethrows the original error, so no pending promise is left
|
|
2399
|
+
attached to an on-screen dialog.
|
|
2400
|
+
- Added `ticket:timing summary --all`, a read-only cross-ticket timing report
|
|
2401
|
+
(median/p90 per phase, gate, and check step) that counts one push shared by
|
|
2402
|
+
a batch of tickets once and detects and excludes fan-out backfill, such as
|
|
2403
|
+
the first timed push that attached one check interval to 374 historical
|
|
2404
|
+
tickets.
|
|
2405
|
+
- Ticket timing now records per-step durations of the root `npm run check`
|
|
2406
|
+
chain (printed at the end of every run), interrupted gate attempts, active
|
|
2407
|
+
time through `ticket:timing claim`/`release` wrappers around `hotsheet-cli`,
|
|
2408
|
+
and CI/publication intervals imported idempotently from GitHub Actions with
|
|
2409
|
+
`ticket:timing import-ci`.
|
|
2410
|
+
- The pre-push hook no longer reruns `npm run check` when a local run already
|
|
2411
|
+
passed on the exact clean tree being pushed; the skip is recorded in ticket
|
|
2412
|
+
timing, and any dirty worktree, different tree, Node.js change,
|
|
2413
|
+
failed/interrupted rerun, or `KERF_FORCE_CHECK=1` runs the full gate.
|
|
2414
|
+
- The pre-push hook no longer fans its timing record out to every ticket in a
|
|
2415
|
+
push that names more than 25 of them (a first push of a long history, a
|
|
2416
|
+
force-push). It prints a notice and records only the explicit
|
|
2417
|
+
`KERF_TICKET_TIMING_TICKETS` list, or nothing, so it can no longer write the
|
|
2418
|
+
backfill shape the cross-ticket summary has to exclude.
|
|
2419
|
+
- The pre-push hook's "already verified" skip now also requires the same
|
|
2420
|
+
platform, architecture, and installed dependencies as the passing run: a
|
|
2421
|
+
fingerprint of every `package-lock.json` and `node_modules/.package-lock.json`
|
|
2422
|
+
across the root, `ui/`, `eslint-plugin/`, `create-kerf-component/`, and
|
|
2423
|
+
`site/`. A reinstall, a lockfile change, or a missing `node_modules` now runs
|
|
2424
|
+
the full gate.
|
|
2425
|
+
- `ticket:timing record` accepts `--run-id <n>`, and `import-ci` no longer
|
|
2426
|
+
imports a GitHub run a second time when it was already recorded by hand:
|
|
2427
|
+
an interval with that run id, or one with no run id for the same phase and
|
|
2428
|
+
gate whose span overlaps the run's (within 60 seconds), counts as imported.
|
|
2429
|
+
- `bindList()` now rolls back when its first render throws: rows that pass
|
|
2430
|
+
already created are disposed (content mounts and element-mode `dispose`
|
|
2431
|
+
callbacks) and removed before the original error is rethrown, instead of
|
|
2432
|
+
leaking live row effects with no handle to release them.
|
|
2433
|
+
- `bindList()` virtualization now validates its dimensions: heights (fixed,
|
|
2434
|
+
estimated, callback-returned, and `setHeight` reports) must be finite and
|
|
2435
|
+
non-negative, a fixed window-mode `rowHeight` must be positive, and
|
|
2436
|
+
`overscan` / `minRows` must be non-negative integers. Invalid values throw a
|
|
2437
|
+
descriptive error instead of producing blank windows or a corrupted scroll
|
|
2438
|
+
anchor.
|
|
2439
|
+
- `bindList()` now rejects a source that carries the `arraySignal` brand but
|
|
2440
|
+
has no patch queue with a descriptive error at bind time, instead of failing
|
|
2441
|
+
with a generic `TypeError` on its first render.
|
|
2442
|
+
- Added `wa-dialog.hide-actions` support to `@kerfjs/ui/webawesome.css`, hiding
|
|
2443
|
+
Web Awesome's directly exported `header-actions` shadow part for dialogs that
|
|
2444
|
+
provide their own dismissal affordance.
|
|
2445
|
+
- Fixed `mount()` leaving an element permanently "already mounted" when its
|
|
2446
|
+
first render throws. The first render is now transactional: the original
|
|
2447
|
+
error is rethrown, every binding effect and the dev listener observer
|
|
2448
|
+
acquired so far are released, the element's pre-mount child nodes are
|
|
2449
|
+
restored, and a retry on the same element succeeds.
|
|
2450
|
+
- Fixed `overlay()` construction leaking a live wrapper, mount, dismissal
|
|
2451
|
+
listeners, and concurrent-overlay stack entry when setup failed partway. A
|
|
2452
|
+
malformed `initialFocus` selector now throws a descriptive `overlay()` error
|
|
2453
|
+
before anything is touched, and a throwing first render or `native`
|
|
2454
|
+
`showModal()` / `showPopover()` call rolls back everything already installed
|
|
2455
|
+
(restoring focus) before rethrowing. `popover()` and `tooltip()` roll back the
|
|
2456
|
+
same way when positioning fails; the dialog helpers inherit the guarantee.
|
|
2457
|
+
- Added an opt-in `@kerfjs/ui/document.css` baseline for border-box sizing,
|
|
2458
|
+
token-backed body and link presentation, and a `.kui-app-root` full-height
|
|
2459
|
+
chain for application shells.
|
|
2460
|
+
- Added an inline `Text` span variant for secondary copy inside rows, labels,
|
|
2461
|
+
and table cells, preserving tone, size, and font roles without box geometry.
|
|
2462
|
+
- Reworked every focused UX catalog demo to dogfood Kerf components, Web
|
|
2463
|
+
Awesome surfaces, and semantic HTML without route-local stylesheets, inline
|
|
2464
|
+
styles, styling-only classes, or shared icon/button render helpers. Added
|
|
2465
|
+
catalog-owned specimen viewport constraints and a nonwaivable conformance
|
|
2466
|
+
gate that prevents those styling shortcuts from returning.
|
|
2467
|
+
- Fixed `ToolbarControlGroup` dropdown triggers that were sized to fixed 48px/66px boxes,
|
|
2468
|
+
which clipped popup icons and let compact mixed-control carets overflow in
|
|
2469
|
+
Safari while distorting longer catalog controls.
|
|
2470
|
+
- Added a `TabBar.end` action zone for layouts that need a tab-local adjacent
|
|
2471
|
+
`trailing` action and a separate far-edge action; the tab strip now owns the
|
|
2472
|
+
shrink/scroll geometry while all action regions remain visible.
|
|
2473
|
+
- Moved the UX catalog checkerboard to the preview scroll owner and replaced
|
|
2474
|
+
its four gradients with one repeated SVG tile, so the pattern continues
|
|
2475
|
+
through below-fold content.
|
|
2476
|
+
- Added explicit native `slot` support to every stable single-root
|
|
2477
|
+
`@kerfjs/ui` visual component, including alternate render branches, while
|
|
2478
|
+
keeping multi-root `ResizableRegion` and `CollapsiblePanel` excluded.
|
|
2479
|
+
- Fixed the UX catalog's desktop grid sizing so both the sidebar and component
|
|
2480
|
+
detail can scroll all the way to their final content while narrow layouts
|
|
2481
|
+
retain normal document scrolling.
|
|
2482
|
+
- Fixed single-line `ToolbarText` clipping glyph descenders while retaining its
|
|
2483
|
+
horizontal ellipsis behavior.
|
|
2484
|
+
- Formalized UX catalog demos as components, compositions, or recipes; catalogs
|
|
2485
|
+
now group and order those kinds consistently and label compositions with a
|
|
2486
|
+
visible tag instead of title suffixes.
|
|
2487
|
+
- Removed stale Web Awesome guidance that still advertised the deleted
|
|
2488
|
+
aggregate theme route; focused component routes remain the supported catalog
|
|
2489
|
+
surface.
|
|
2490
|
+
- Aligned Web Awesome Accordion and Details disclosure geometry: plain Details
|
|
2491
|
+
keep a flush body, plain Accordion items use the 8px surface inset throughout,
|
|
2492
|
+
framed Details use 16px on the trigger and 8px on the body, and framed
|
|
2493
|
+
Accordion items use 16px throughout.
|
|
2494
|
+
- Added a first-class `@kerfjs/ui/grid` layout primitive for any positive
|
|
2495
|
+
equal-width column count, with typed gap and flex values and zero-minimum
|
|
2496
|
+
fractional tracks that stay equal when child content has different intrinsic
|
|
2497
|
+
widths.
|
|
2498
|
+
- Made accent and filled-outlined Web Awesome badge variants meet WCAG AA at
|
|
2499
|
+
their compact text size while preserving loud accent fills and outlined
|
|
2500
|
+
borders.
|
|
2501
|
+
- Added independent typed `tone`, `size`, and `font` presentation roles to
|
|
2502
|
+
`@kerfjs/ui/text`, covering quiet/danger copy, compact metadata, and monospace
|
|
2503
|
+
identifiers without global utility classes or appearance-only heading levels.
|
|
2504
|
+
- Made filled Web Awesome badges use a quiet semantic fill with normal text by
|
|
2505
|
+
default, preserving variant tint while meeting WCAG AA for their compact text.
|
|
2506
|
+
- Fixed nested Rows and Lists inheriting inset values for sides that were not
|
|
2507
|
+
selected on the inner layout component.
|
|
2508
|
+
- Fixed migrated `Text` instances in Catalog and passive `ListHeader` labels
|
|
2509
|
+
retaining nested padding or border geometry that offset their visible content.
|
|
2510
|
+
- Added `Row.vAlign="baseline"` for native flex cross-axis baseline alignment;
|
|
2511
|
+
`List.vAlign` remains limited to values supported by vertical main-axis
|
|
2512
|
+
distribution.
|
|
2513
|
+
- Added a first-class `@kerfjs/ui/text` component for native `h1`–`h6` and
|
|
2514
|
+
paragraph semantics with standard transparent-border and content-padding
|
|
2515
|
+
geometry and margin-neutral defaults; it renders `p` when `variant` is omitted,
|
|
2516
|
+
and Kerf UI's headings and prose now use it.
|
|
2517
|
+
- Added the shared physical `Sides` contract to `List` and `Row` text/control
|
|
2518
|
+
insets and to `ListInsetText` / `ListInsetControl`; inset wrappers now default
|
|
2519
|
+
to all four sides, while `ListInsetText.horizontalOnly` remains a deprecated
|
|
2520
|
+
compatibility alias for `sides="rl"`.
|
|
2521
|
+
- Added `ListHeader.inline` for shrink-wrapped section labels that remove the
|
|
2522
|
+
component root's outer margin, border, and padding while preserving title,
|
|
2523
|
+
count, action, and disclosure behavior.
|
|
2524
|
+
- Added a catalog-driven public-component integration validator for
|
|
2525
|
+
`@kerfjs/ui`, including an all-at-once dry-run report across package, build,
|
|
2526
|
+
barrel, CSS, demo, route, signature, and compatibility surfaces.
|
|
2527
|
+
- Added one change-local `@kerfjs/ui` verification command that synchronizes
|
|
2528
|
+
generated projections, runs component/bundle/type coverage, builds the demo,
|
|
2529
|
+
reports exact gzip deltas, and records reviewed budget updates with reasons.
|
|
2530
|
+
- Made every `@kerfjs/ui` Playwright invocation build current catalog source
|
|
2531
|
+
after fast preflights, with a served source digest that rejects stale preview
|
|
2532
|
+
output during focused browser runs.
|
|
2533
|
+
- Added durable Hot Sheet phase timing for active work, local verification,
|
|
2534
|
+
push hooks, CI, and publication, including safe failure aggregation and
|
|
2535
|
+
per-ticket summaries.
|
|
2536
|
+
- Added typed flex participation to `Row`, matching `List`'s boolean, finite
|
|
2537
|
+
keyword, and branded `CssFlex` contract.
|
|
2538
|
+
- Added a first-class horizontal `Row` with typed spacing, physical-axis
|
|
2539
|
+
alignment, and opt-in wrapping; `List` now supports the same alignment
|
|
2540
|
+
vocabulary while retaining its existing defaults.
|
|
2541
|
+
- Refined compact mixed `ToolbarControlGroup` selections to retain standard
|
|
2542
|
+
horizontal padding, remove the internal separator, and raise the selected
|
|
2543
|
+
surface over the outer border; avatar group hover now preserves image fitting.
|
|
2544
|
+
- Moved the comprehensive local `npm run check` gate from pre-commit to
|
|
2545
|
+
pre-push so coherent commit batches pay its cost once; pre-commit now performs
|
|
2546
|
+
only Git's staged whitespace check, and multi-source AI guidance files are
|
|
2547
|
+
excluded from repository-wide Prettier enforcement.
|
|
2548
|
+
- Fixed `create-kerf-component` releases carrying a stale composition-catalog
|
|
2549
|
+
schema by generating the bundled copy from `@kerfjs/ui`'s canonical schema
|
|
2550
|
+
and gating both the root check and interactive release flow against drift.
|
|
2551
|
+
- Added catalog-driven CSS value diagnostics across `eslint-plugin-kerfjs`,
|
|
2552
|
+
`kerf-ui-analyze`, and `kerf-ui-doctor`. Stable `KUI-L013`–`KUI-L017`
|
|
2553
|
+
findings now identify raw or unknown values, wrong-dimension helpers,
|
|
2554
|
+
incomplete expressions, declaration-list escapes, and review-worthy
|
|
2555
|
+
off-scale spacing from first- or third-party component catalogs.
|
|
2556
|
+
- Extended the property-specific `@kerfjs/ui/css-values` contracts: `List.flex`
|
|
2557
|
+
now uses finite keywords or `flex()`, `Skeleton` dimensions use `CssSize` and
|
|
2558
|
+
`CssLength`, and `SelectChoice.color` uses `uiColor()` or `colorVar()`.
|
|
2559
|
+
Removed the unrestricted `style` escape hatches from `ListItem` and
|
|
2560
|
+
`ListActionRow` before the 5.0 stable release; use `className`, public tokens,
|
|
2561
|
+
and cataloged props instead.
|
|
2562
|
+
- Added typed, CSS-free `@kerfjs/ui/css-values` builders and branded complete
|
|
2563
|
+
versus expression length types. `List.gap` now accepts direct spacing-token
|
|
2564
|
+
shorthands or complete `CssLength` values and rejects unrestricted CSS strings.
|
|
2565
|
+
- Added the public recursive `JSXChildren` type from both `kerfjs` and
|
|
2566
|
+
`kerfjs/jsx-runtime`, giving third-party function components a canonical
|
|
2567
|
+
declaration for nullable, signal-backed, and arbitrarily nested readonly
|
|
2568
|
+
child content that matches the runtime.
|
|
2569
|
+
- Added the public recursive `KerfUiContent` type across semantic UI content
|
|
2570
|
+
zones, so nullable conditionals and mutable or readonly nested component
|
|
2571
|
+
arrays compose directly without a `Fragment` while raw strings, numbers, and
|
|
2572
|
+
signals remain rejected outside explicit text positions.
|
|
2573
|
+
- Fixed bottom `Workbench` drawers overshooting and snapping during collapse
|
|
2574
|
+
transitions by anchoring fixed-height content to the shell's stable bottom
|
|
2575
|
+
edge.
|
|
2576
|
+
- Fixed bottom `CollapsiblePanel` drawers overshooting their open position and
|
|
2577
|
+
snapping back by anchoring fixed-height content to the panel's stable bottom
|
|
2578
|
+
edge throughout the transform transition.
|
|
2579
|
+
- Formalized named `SafeHtml` prop slots as explicit `zone.jsx.prop` bindings
|
|
2580
|
+
in the UI composition catalog and generalized `ui-composition` linting beyond
|
|
2581
|
+
Toolbar, including statically visible `children`, cardinality, aliases,
|
|
2582
|
+
namespaces, arrays, fragments, and branches.
|
|
2583
|
+
- Added `ToolbarControlGroup.avatarImage`, painting contained avatar imagery on
|
|
2584
|
+
a lone group or on the pressed highlight when the group has multiple buttons.
|
|
2585
|
+
- Split the AppTab and TabBar catalog routes into focused component specimens,
|
|
2586
|
+
and moved their controlled selection, tabpanel, overflow, add/close, and
|
|
2587
|
+
reorder lifecycle into a separate application-tabs composition demo.
|
|
2588
|
+
- Fixed `wireNavStack` focus ownership across controlled navigation: pushes
|
|
2589
|
+
focus the new top view, pops restore its remembered descendant, and removed
|
|
2590
|
+
targets fall back through `data-nav-focus`, the first enabled control, and
|
|
2591
|
+
the view container.
|
|
2592
|
+
- Fixed compact mixed-content `ToolbarControlGroup` dropdown triggers being
|
|
2593
|
+
forced into an icon-only width, which painted the label across its separator
|
|
2594
|
+
and the Web Awesome caret outside the group.
|
|
2595
|
+
- Made `ListItem` clip label, status, and dormant trailing painting to its
|
|
2596
|
+
rounded row boundary so compact and constrained rows cannot visibly overflow;
|
|
2597
|
+
described rows now grow enough to keep their owned two-line label readable.
|
|
2598
|
+
- Fixed compact toolbar `Select` focus to follow its owning group's pill or
|
|
2599
|
+
rounded geometry and restored consistent spacing between popup icons and labels.
|
|
2600
|
+
- Fixed the compact-toolbar recipe's independent overflow action to use the
|
|
2601
|
+
same full-height pill geometry as its adjacent toolbar controls.
|
|
2602
|
+
- Fixed the catalog footer's `Components` relationship selector to fit its icon
|
|
2603
|
+
and plural label and give popup headings/items consistent border clearance.
|
|
2604
|
+
- Fixed a collapsible `TokenSearchField` inside a rounded
|
|
2605
|
+
`ToolbarControlGroup` retaining its independent pill radius instead of using
|
|
2606
|
+
the group's shared concentric highlight radius.
|
|
2607
|
+
- Updated the header-composition catalog demo's supporting copy to use the
|
|
2608
|
+
horizontal `ListInsetText` geometry, aligning it with the value-table content
|
|
2609
|
+
edge across responsive, RTL, and zoomed layouts.
|
|
2610
|
+
- Updated the `Pane` catalog demo's secondary header copy to use
|
|
2611
|
+
`ListInsetText`, aligning it with the standard pane content gutter at wide and
|
|
2612
|
+
narrow sizes instead of maintaining demo-only padding.
|
|
2613
|
+
- Completed `NavStack` navigation transitions by cross-fading top and bottom
|
|
2614
|
+
chrome alongside content slides, added per-view bottom toolbars with a
|
|
2615
|
+
persistent fallback, and replaced the static focused demo with a working
|
|
2616
|
+
push/pop flow.
|
|
2617
|
+
- Replaced the static compact `SplitView` detail specimen with a controlled,
|
|
2618
|
+
interactive list-to-detail drill-down whose Back action restores the list;
|
|
2619
|
+
the roomy example now demonstrates controlled selection as well.
|
|
2620
|
+
- Documented and demonstrated list-first dialog bodies: use `List` with
|
|
2621
|
+
`bodyInset="none"`, and wrap bare dialog copy in `ListInsetText` so content
|
|
2622
|
+
shares the standard list gutter without double-insetting.
|
|
2623
|
+
- Fixed `@kerfjs/ui`'s collapsible `TokenSearchField` keep-open ownership in
|
|
2624
|
+
WebKit: when `focusout.relatedTarget` is missing during pointer activation of
|
|
2625
|
+
an external `data-token-search-keep-open` surface, the helper now uses the
|
|
2626
|
+
actual pointer target instead of collapsing and removing the target before
|
|
2627
|
+
its click handler runs.
|
|
2628
|
+
- Added a first-class vibrant `pop` semantic color for attractive non-status
|
|
2629
|
+
emphasis, with complete light/dark/increased-contrast token roles and typed
|
|
2630
|
+
StateBanner, ListHeader indicator, and ToolbarControlGroup selected tones.
|
|
2631
|
+
- Fixed canonical `Select` popup animation reversals so an earlier close cannot
|
|
2632
|
+
hide a reopened menu after resizing. Canceled transitions retain the accepted
|
|
2633
|
+
state, and removed controls cannot receive stale completion or deferred focus.
|
|
2634
|
+
- Fixed managed `TokenSearchField` Clear focus before the next input task, so
|
|
2635
|
+
immediate typing stays in the replacement editor instead of triggering page
|
|
2636
|
+
shortcuts; genuine later focus handoffs and selections remain untouched.
|
|
2637
|
+
- Fixed `@kerfjs/ui/tab-scaffold` bottom-tab labels being vertically clipped by
|
|
2638
|
+
flex shrinkage while retaining horizontal ellipsis for long labels.
|
|
2639
|
+
- Made inset selection and hover highlights follow their owning control's pill
|
|
2640
|
+
or rounded shape, with concentric inner radii derived from the full inset.
|
|
2641
|
+
- Fixed animated `ResizableRegion` overlays so remembered desktop dimensions
|
|
2642
|
+
cannot make their content overflow the responsive panel cap on narrow screens.
|
|
2643
|
+
- Added configuration-first application-panel policies across `ResizableRegion`,
|
|
2644
|
+
`Workbench`, and `CollapsiblePanel`: separator visibility, instant-track
|
|
2645
|
+
collapse with optional composited content motion, popup-safe overflow,
|
|
2646
|
+
inline/overlay/hidden responsive presentations, and safe-area restore-control
|
|
2647
|
+
placement. `wireResizableRegions` now suppresses motion during pointer drags
|
|
2648
|
+
and ignores unavailable regions; `wireSidebar` supports hidden compact
|
|
2649
|
+
replacements and exclusive overlays.
|
|
2650
|
+
- Added typed `TabBar` allocation/presentation/action-placement and `AppTab`
|
|
2651
|
+
presentation/size/label-width variants for segmented inspector strips,
|
|
2652
|
+
compact drawer tabs, icon-only accessible tabs, and truncating labels.
|
|
2653
|
+
- Added typed `ToolbarControlGroup` size, density, content anatomy, selected
|
|
2654
|
+
chrome/tone, nested-dropdown, and avatar scrim variants.
|
|
2655
|
+
- Added typed `Select` form, borderless-toolbar, intrinsic-navigation, compact,
|
|
2656
|
+
icon-only-selected, truncation, and composed focus-ring variants.
|
|
2657
|
+
- Added `DialogSurface` and `PopupSurface` composition APIs for standard dialog
|
|
2658
|
+
sizes/presentations, independent body/footer insets, and list-compatible popup
|
|
2659
|
+
menu insets without consumer `::part()` overrides.
|
|
2660
|
+
- **Breaking (`@kerfjs/ui`):** removed `PanelHeader` and its subpath/CSS export.
|
|
2661
|
+
Compose page, panel, and dialog headings directly with `Toolbar`, a direct
|
|
2662
|
+
extra-large `ToolbarText`, and optional `ToolbarControlGroup` icon/action zones;
|
|
2663
|
+
keep supporting copy as app-owned content below the toolbar.
|
|
2664
|
+
- Fixed controlled TokenSearchField Select All + Backspace/Delete collapsing the
|
|
2665
|
+
editor: managed deletion keeps the adopted open signal and restores replacement
|
|
2666
|
+
focus before another keystroke, without a stale animation-frame caret reset.
|
|
2667
|
+
The adoption demo now persists both edited text and remaining tokens.
|
|
2668
|
+
- Fixed `Select` controls using only `ariaLabel` being unnamed to assistive
|
|
2669
|
+
technology. Ordinary and custom selected content retain their geometry while
|
|
2670
|
+
the actual shadow combobox receives its accessible name.
|
|
2671
|
+
- Added first-class `Select` hint text below interactive and loading-placeholder
|
|
2672
|
+
controls while keeping empty-value `placeholderText` semantically distinct.
|
|
2673
|
+
|
|
2674
|
+
- Changed the Web Awesome Dialog theme to use 8px body padding and 16px footer
|
|
2675
|
+
padding, independently of the component's shared `--spacing` value.
|
|
2676
|
+
|
|
2677
|
+
- Fixed `kerf-ui-doctor` so its isolated Kerf ESLint pass preserves applicable
|
|
2678
|
+
consumer core rules and suppression reporting options. Playwright's required
|
|
2679
|
+
`no-empty-pattern` fixture suppression no longer becomes a false unused
|
|
2680
|
+
directive while consumer plugin rules remain outside the doctor boundary.
|
|
2681
|
+
|
|
2682
|
+
- Cataloged `SplitView` and its documented
|
|
2683
|
+
`--kui-split-view-list-width` override so `kerf-ui-doctor --full` accepts the
|
|
2684
|
+
public token in downstream application stylesheets.
|
|
2685
|
+
|
|
2686
|
+
- Added the first-class `List` vertical-layout component with gap, flex, scroll,
|
|
2687
|
+
and multi-edge divider options; `Toolbar` now uses the same typed
|
|
2688
|
+
`dividerSides` contract instead of a boolean divider.
|
|
2689
|
+
|
|
2690
|
+
- Added an optional tone-tinted pill badge beside `StateBanner` titles, including
|
|
2691
|
+
loading skeleton behavior and per-instance badge color overrides.
|
|
2692
|
+
|
|
2693
|
+
- Fixed pending `wireTabBars` and `wireTokenSearchFields` controlled-render
|
|
2694
|
+
callbacks moving focus after their owning wiring had been disposed.
|
|
2695
|
+
|
|
2696
|
+
- Changed the default `ListItem` leading icon visual from 24px to a root-scaled
|
|
2697
|
+
18px while retaining the row's 44px minimum target and first-line alignment.
|
|
2698
|
+
|
|
2699
|
+
- Fixed managed `TokenSearchField` clear losing focus or collapsing when an
|
|
2700
|
+
application replaces its controlled editor; continued typing now works without
|
|
2701
|
+
an application reopen callback.
|
|
2702
|
+
|
|
2703
|
+
- Fixed `@kerfjs/ui` automatic TabBar keyboard activation losing focus when a
|
|
2704
|
+
controlled selection replaces the strip; navigation restores the same logical tab.
|
|
2705
|
+
|
|
2706
|
+
- Updated the published setup runtime to patched `minimatch` 9.0.9 and `yaml`
|
|
2707
|
+
2.9.1 releases, resolving the known regular-expression denial-of-service and
|
|
2708
|
+
deeply nested YAML denial-of-service advisories without narrowing Kerf's
|
|
2709
|
+
supported Node.js range.
|
|
2710
|
+
- Fixed `kerf-ui-doctor --full` so its isolated ESLint stage shares the same
|
|
2711
|
+
generated-directory exclusions as TypeScript, static analysis, and cache
|
|
2712
|
+
hashing; populated `dist`, `coverage`, dependency, cache, evidence, and nested
|
|
2713
|
+
worktree directories no longer produce application diagnostics.
|
|
2714
|
+
- Changed `@kerfjs/ui` component-library design templates to compose their
|
|
2715
|
+
individual variant SVGs through HTML and `domotion capture
|
|
2716
|
+
--flatten-nested-svg`, eliminating the hand-built nested-SVG compositor and
|
|
2717
|
+
making top-level template sheets reliable in Sketch.
|
|
2718
|
+
- Added a typed `shape` option to `SunkenPanel`: `rounded` remains the default,
|
|
2719
|
+
while `square` produces a lowered surface with `border-radius: 0` for flush or
|
|
2720
|
+
edge-to-edge application areas. Both shapes now appear in the UX catalog.
|
|
2721
|
+
- Made the Catalog geometry overlay derive both margin bands and border
|
|
2722
|
+
edges/radii from each live specimen's computed CSS. Public example helpers now
|
|
2723
|
+
mark their generated labels and notes for automatic exclusion, and stylesheet,
|
|
2724
|
+
theme, DOM, and resize changes refresh the overlay without duplicated demo
|
|
2725
|
+
measurements.
|
|
2726
|
+
- Updated the `@kerfjs/ui` SVG design-template generator to `domotion-svg`
|
|
2727
|
+
0.30.1 and enabled `--flatten-nested-svg`, so inline component icons are
|
|
2728
|
+
emitted as Sketch-compatible groups without changing their browser rendering.
|
|
2729
|
+
The offline template gate now rejects variant captures that reintroduce nested
|
|
2730
|
+
`<svg>` elements.
|
|
2731
|
+
- Fixed `kerf-ui-doctor` and `kerf-ui-analyze` recursive discovery so nested
|
|
2732
|
+
`.claude/worktrees` checkouts are excluded from source diagnostics, doctor
|
|
2733
|
+
cache inputs, TypeScript, and ESLint.
|
|
2734
|
+
- Added `npx kerfjs setup` and the `kerfjs/setup` automation API for safe,
|
|
2735
|
+
AI-first initialization and migration of core and UI projects. The command
|
|
2736
|
+
provides bounded dry-run diffs, explicit per-conflict keep/Kerf choices,
|
|
2737
|
+
package-scoped hash-backed upgrades, monorepo selection, transactional
|
|
2738
|
+
rollback, shipped guidance, strict TypeScript/ESLint, and UI catalog/profile
|
|
2739
|
+
and doctor discovery. Empty application component metadata now emits a valid
|
|
2740
|
+
empty v2 catalog. Existing JSONC TypeScript configuration is structurally
|
|
2741
|
+
merged without discarding comments, trailing commas, authored fields, or line
|
|
2742
|
+
endings; malformed roots and compiler-option containers require an explicit
|
|
2743
|
+
keep/Kerf decision. Workspace selection now evaluates ordered globstar,
|
|
2744
|
+
brace, exclusion, and re-inclusion patterns deterministically across npm,
|
|
2745
|
+
Yarn, and pnpm declarations, rejects path/symlink/name ambiguity, and uses
|
|
2746
|
+
manager-correct offline commands with explicit Yarn Classic/Berry detection
|
|
2747
|
+
and network-disabled Berry installs.
|
|
2748
|
+
- Added `@kerfjs/ui/doctor` and the `kerf-ui-doctor` command: a versioned, redacted repair-loop report that merges catalog/profile validation, TypeScript, Kerf UI ESLint, static analysis, and opt-in browser evaluation with changed/full modes, monorepo selection, reasoned suppressions, caching, and deterministic exits.
|
|
2749
|
+
- Added the public Playwright-backed `kerf-ui-evaluate` CLI and `@kerfjs/ui/evaluator` Node API. It resolves application profiles and composition catalogs, exercises wide/intermediate/narrow/200%-zoom, light/dark, and reduced-motion contexts across Chromium, Firefox, and WebKit, and emits versioned repair-oriented `KUI-B###` diagnostics plus focused DOM/style evidence and hashed screenshots. Subjective visual quality remains an explicitly separate unrecorded reviewer rubric.
|
|
2750
|
+
- Added `kerf-ui-analyze`, a profile- and catalog-aware static evaluator for Kerf UI integrations. It resolves directory policy per source, follows project-local CSS import graphs without cross-package class leakage, and evaluates shared stylesheets against every consuming source policy before deduplicating findings. Stable portable text, JSON, or SARIF diagnostics cover private selectors, unknown tokens, competing geometry/scroll owners, repeated insets, forced dimensions, off-scale spacing, and dynamic-class review, with exact scoped profile exceptions, a public report schema, and configurable CI review failure.
|
|
2751
|
+
- Added catalog/profile-aware `recommended-ui` and `strict-ui` flat presets to `eslint-plugin-kerfjs`. Toolbar zone acceptance/cardinality and valid wiring imports come from the shipped catalogs; intrinsic or unknown zone children, root/subpath/namespace wiring, and the non-colliding `KUI-L090` load diagnostic are covered by real packed downstream installs on every supported ESLint major.
|
|
2752
|
+
- **Breaking (`@kerfjs/ui` TypeScript declarations):** strengthened invalid-integration contracts for ListHeader modes, Select naming and literal choice identity, SegmentedControl/TabScaffold literal identity, collapsible TokenSearchField props, protected editor metadata, PanelHeader summary ids, ToolbarText line caps, and token-removal callbacks. Added named public unions for previously anonymous finite variants plus source and packed-tarball positive/negative compilation gates. See `ui/docs/type-contracts.md` for the compatibility review and dynamic-data widening syntax.
|
|
2753
|
+
- `create-kerf-component` now scaffolds explicit `kerf.components.json` AI metadata and a deterministic `kerf-component-catalog` generator/checker. Generated v2 entries carry package identity, purpose, verified named exports, composition and geometry ownership, tokens, accessibility, and source links; publishing rejects missing author decisions, schema-invalid source/output fields, deleted sources, renamed exports, duplicate ids, or output drift, including multi-package workspaces. Syntax-aware TypeScript/TSX export discovery prevents JSX text, nested scopes, comments, and literals from impersonating public exports.
|
|
2754
|
+
- Added a versioned application UI profile for AI tools and static evaluators. Shipped package defaults, schema, TypeScript types, Node discovery/merge/validation APIs, and a workspace example now make catalog locations, preferred components/recipes, theme/density policy, semantic token overrides, responsive/layout conventions, and narrow source-located exceptions explicit. Composition-only generated consumer catalogs load without a fabricated v1 selection artifact, while Kerf retains its required v1 selection catalog. Deterministic package → workspace → parent-to-child directory precedence validates every raw layer before merge, attributes resolved diagnostics through field provenance, and rejects stale catalogs, unknown components/tokens/rules, conflicts, malformed value shapes, and broad exemptions.
|
|
2755
|
+
- Fixed `CatalogExampleStack` accessible labels: the helper now renders a semantic `section`, so a supplied `label` exposes the documented named region to assistive technology while an unlabeled stack remains an ordinary grouping.
|
|
2756
|
+
- Added first-class `Pane` and `SunkenPanel` primitives to `@kerfjs/ui`. `Pane` standardizes semantic header/content/footer columns, explicit logical-edge separators, and one content scroll owner; `SunkenPanel` supplies one lowered surface with an 8px inset and vertical rhythm. The Catalog shell now dogfoods `Pane`, and both components ship focused demos, catalog metadata, documentation, and three-engine browser coverage.
|
|
2757
|
+
- Added the package-qualified component catalog v2 alongside the compatible v1 artifact. Its schema and generated metadata formalize child zones, cardinality, exclusivity, wiring, state, responsive behavior, layout and accessibility boundaries, stable validation diagnostics, provenance, and consumer extension catalogs, so AI tools can join Kerf and application components without guessing composition contracts.
|
|
2758
|
+
- Added an AST-based Catalog demo conformance gate. It checks every first-party demo for public helper usage, package imports, focused specimen structure, geometry-overlay metadata, and reviewed schema-checked exceptions, with adversarial fixtures and authoring guidance that makes the same contract available to downstream AI tools.
|
|
2759
|
+
- Made `ui/docs/catalog.md` the authoritative Catalog demo authoring contract, covering focused versus composition previews, public row/group nesting, exact specimen selection, conditional geometry overlays, skip-marker behavior, metadata ownership, and an explicit overlay legend. A shipped `ai/catalog-authoring.json` discovery artifact now routes AI tools to that contract and exact signatures, with schema, link-integrity, and browser-consumer fixture coverage.
|
|
2760
|
+
- Extended `CatalogExample` and `CatalogExampleStack` with typed, runtime-filtered `rootAttributes` for catalog-authoring `data-*` metadata such as `data-demo` and `data-catalog-geometry-overlay-skip`, while preserving helper-owned structural and alignment markers. The UX catalog now composes its example stacks through these public helpers instead of copying private `kui-catalog-*` markup.
|
|
2761
|
+
- Added `revealCatalogEntry` and opt-in `wireCatalog({ revealSelection })` support to the exported Catalog tool. Sidebar or related-entry selection can now reveal the matching row after the controlled render settles, using a desktop-safe media guard, exact id matching, configurable scroll alignment/behavior, focus-preserving scrolling, and cancellation of stale rapid selections; the Kerf UX demo now uses the public behavior.
|
|
2762
|
+
- Added a reusable consumer component-catalog extension schema and checked app-component example. Downstream packages can now publish the same selection facts and margin/border/padding ownership vocabulary as Kerf, and the README, component contract, Catalog guide, `llms.txt`, and AI skill explain how to combine package-qualified app entries with Kerf's shipped catalog for alignment decisions.
|
|
2763
|
+
- Added an opt-in geometry inspector to the exported `@kerfjs/ui/catalog` tool. `geometryOverlay` plus `wireCatalogGeometryOverlay` now marks transparent component bounds with a dashed outline and positive intrinsic margins with devtools-style bands, excludes example labels/alignment scaffolding, stays synchronized across preview changes, and replaces the Kerf UX demo's private implementation.
|
|
2764
|
+
- Added machine-readable margin, border, and padding ownership to every visual entry in the `@kerfjs/ui` component catalog. The additive schema distinguishes self-, parent-, child-, conditional-, and unowned geometry, and enforces complete metadata plus explanatory notes for conditional cases.
|
|
2765
|
+
- Refreshed the standalone reactivity demo lockfile to Vite 6.4.3, PostCSS 8.5.28, and nanoid 3.3.19, clearing its development-toolchain audit advisories.
|
|
2766
|
+
- Overrode the development toolchain's transitive esbuild dependency to 0.28.2, resolving GHSA-g7r4-m6w7-qqqr while current tsup still constrains its declared range to 0.27.x.
|
|
2767
|
+
- Added repository-wide Prettier formatting for TypeScript, JavaScript, Astro, shell, CSS, HTML, Markdown, YAML, and JSON source and structured-content files. Root and `@kerfjs/ui` lint gates now reject formatting drift, while generated, binary, and parser-incompatible fixtures remain explicitly excluded.
|
|
2768
|
+
- Added reusable sidebar tags to `@kerfjs/ui` Catalog entries and marked the 15 Web Awesome components superseded by preferred Kerf patterns or reserved for exceptional cases as `Discouraged` in the UX catalog. The tag is deterministically projected from the canonical recommendation metadata; Popup remains an available conditional positioning primitive.
|
|
2769
|
+
- Aligned Web Awesome Accordion, Card, Details, Callout, and Include surfaces with Kerf content-item geometry: each complete surface now has an overridable 8px logical inline margin and 8px inner padding, Accordion keeps connected items under one group margin, and Card's header/body/footer share the full inset. Tab Panel intentionally retains the roomier 16px container inset.
|
|
2770
|
+
- Fixed `@kerfjs/ui` Web Awesome Slider geometry: its complete interactive region now receives the shared 8px logical inline outer inset without shifting the already-aligned label.
|
|
2771
|
+
- Styled `@kerfjs/ui` Web Awesome OTP Input like a bordered text field: its label now uses the shared uppercase xs/650 field-label treatment, and both label and hint align at the 9px border-plus-padding inset.
|
|
2772
|
+
- Aligned `@kerfjs/ui` Web Awesome Known Date's Month/Day/Year captions and bordered text-like field hints with their values using the shared 9px border-plus-padding inline inset.
|
|
2773
|
+
- Fixed `@kerfjs/ui` Web Awesome Color Picker geometry: its unbordered trigger now receives the shared 8px inline outer inset, aligning it with Kerf content-item spacing.
|
|
2774
|
+
- Fixed `@kerfjs/ui` Web Awesome Checkbox Group and Radio Group option geometry: each group's unbordered option region now receives the shared 8px inline outer inset, aligning it with Kerf content-item spacing without padding the individual controls.
|
|
2775
|
+
- Aligned the remaining bordered/padded Web Awesome chrome in `@kerfjs/ui/webawesome.css` to explicit Kerf spacing tiers: `--kui-wa-control-inset` (8px) drives tabs, inset tree selection, tags, and dropdown items; `--kui-wa-surface-margin` / `--kui-wa-surface-inset` drive bordered surface geometry; and `--kui-wa-container-inset` (16px) retains roomier Tab Panel content. Buttons already inherit the 8px form-control inset; compact badges, unframed Breadcrumb, and slotted Scroller item chrome deliberately retain their native geometry. The tiers are overridable by scope and pinned by three-engine computed-style coverage plus light/dark wide and narrow visual QA.
|
|
2776
|
+
- Tightened the `@kerfjs/ui` Web Awesome theme's form fields to the Kerf content-item inset — a 1px border with 8px inside it (`--wa-form-control-border-width` / `--wa-form-control-padding-block` / `--wa-form-control-padding-inline` now follow `--kui-layout-item-border-width` / `--kui-layout-item-padding`), replacing Web Awesome's larger default pad, so fields land at the standard ~40px height and 9px value inset. Each field's top label is now inset by that same border + padding (9px) so it aligns with the value inside the field, and is styled exactly like a `ListHeader` label — uppercase, xs, weight 650, quiet foreground (`::part(form-control-label)` across Input/Number Input/Textarea/Select/Checkbox Group/Radio Group/Color Picker/Known Date/Time Input, plus the Slider's `::part(label)`). Inline Checkbox/Switch labels keep their sentence case.
|
|
2777
|
+
- Added overflow controls to `@kerfjs/ui`'s `ToolbarText`: `wrap` (flow onto multiple lines instead of a single line), `ellipsis` (show the trailing … at the truncation, or hard-clip when false), and `maxLines` (cap wrapped text to N lines, ellipsizing past it). The default is unchanged — one line, ellipsized when it does not fit. `maxLines` takes effect only with `wrap`. Driven by `data-wrap` / `data-ellipsis` / `data-max-lines` attributes plus a `--kui-toolbar-text-max-lines` var; no new public classes. The UX catalog gains ellipsis/wrap/capped examples.
|
|
2778
|
+
- Fixed a `@kerfjs/ui` `TokenSearchField` bug where selecting all content and pressing Delete (or Backspace) left a stray newline in the field. A `contenteditable` host inserts a bogus `<br>` when emptied that way in every engine — it renders as a newline and `readTokenSearchField` read it back as a space. `wireTokenSearchFields` now strips that line break after any delete input and, when the field is otherwise empty, restores the canonical empty text span and places the caret in it so typing resumes cleanly. Also clears any leftover chips in the select-all case. Regression-tested in unit (happy-dom) and across all three browser engines.
|
|
2779
|
+
- Hardened that full `TokenSearchField` deletion for platform-specific selection
|
|
2780
|
+
ranges whose endpoints sit inside the first and last text spans instead of at
|
|
2781
|
+
the editor boundary—or whose cloned Linux range omits an atomic chip entirely.
|
|
2782
|
+
Full logical text-and-token selection is recognized from its value, while an
|
|
2783
|
+
explicit Ctrl/Cmd+A intent survives only until the next delete and is
|
|
2784
|
+
consumed by that delete even when the browser omits `beforeinput`, and is
|
|
2785
|
+
retained across a controlled editor replacement before or after
|
|
2786
|
+
`beforeinput`. It is captured before propagation can be stopped and
|
|
2787
|
+
invalidated by selection-moving keys, pointer input, blur, or another edit.
|
|
2788
|
+
The explicit shortcut-to-delete transition is handled deterministically:
|
|
2789
|
+
the helper prevents the unreliable mixed-contenteditable native mutation,
|
|
2790
|
+
canonicalizes the empty editor, and emits the corresponding bubbling input
|
|
2791
|
+
event so controlled application state clears before it can restore a chip.
|
|
2792
|
+
A browser can no longer preserve a controlled chip merely because its range
|
|
2793
|
+
shape or native deletion timing differs.
|
|
2794
|
+
- Added a `horizontalOnly` prop to `@kerfjs/ui`'s `ListInsetText`. When true it keeps the horizontal geometry (8px inline margin, 1px left/right border, 8px left/right padding — so the text edge still lands at the 17px inset) but drops the vertical margin, border, and padding, for tight text layout that still aligns with bordered items. It applies a `kui-list-inset-text--horizontal` modifier class; the default (vertical box space intact) is unchanged. The UX catalog gains a horizontal-only example.
|
|
2795
|
+
- Fixed a WCAG AA color-contrast failure on `@kerfjs/ui`'s selected list rows. The selected `ListItem` and `ListActionRow` used the brand accent (`brand-on-quiet`, `#1e6ef4`) for their text, which resolved to only **3.77:1** over the selected `brand-fill-normal` background (`#d6ecff`) — below the 4.5:1 minimum for normal text (flagged by axe downstream). Both now use the normal neutral foreground over the brand-tinted fill, so selection is carried by the fill and border while the text stays readable in light and dark themes. Computed-ratio browser tests pin both selected rows at ≥4.5:1 in both themes across all three engines.
|
|
2796
|
+
- Fixed the same brand-accent contrast failure on `@kerfjs/ui`'s info `StateBanner` tone and `Select`'s current option, where the blue is a semantic (not a selection cue), so the text stays blue rather than going neutral: a new darker `--kui-color-brand-on-fill` token (`#1a5dcf` light; the compliant `#8acbff` in dark) replaces `brand-on-quiet` for brand text placed directly on a brand fill. The info banner (was 4.15:1) now clears ~5.4:1 and the current Select option (was 3.77:1) ~4.9:1. Computed-ratio browser assertions guard both.
|
|
2797
|
+
- Completed the `@kerfjs/ui` `StateBanner` contrast pass: the `success` (was 4.05:1) and `warning` (was 4.39:1) tones failed WCAG AA the same way via their own `*-on-quiet` tokens over the tinted banner fills. New darker `--kui-color-success-on-fill` (`#00792c`) and `--kui-color-warning-on-fill` (`#8f5e00`) tokens (each keeping its semantic hue, dark values unchanged and already compliant) now back the success/warning banner text, clearing ~5.0:1 and ~5.3:1. Every `StateBanner` tone now clears 4.5:1 in both themes, guarded by an all-tone computed-ratio browser assertion.
|
|
2798
|
+
- Added two structure primitives to `@kerfjs/ui` for aligning bare content inside a `.kui-content` list: `ListInsetControl` (`@kerfjs/ui/list-inset-control` + `.css`) wraps a control that owns its own border and padding but no outer margin (a search input, a `SegmentedControl`) with the standard 8px inline margin plus a stretch flex row and an 8px gap, so it lines up with the bordered items around it without double-insetting; `ListInsetText` (`@kerfjs/ui/list-inset-text` + `.css`) gives a bare string or inline content the content-item geometry (8px inline margin, 1px transparent border, 8px padding) so its text edge lands at the same 17px inset as bordered rows. Both are additive, take an optional `className`, and appear in the UX catalog and the component-selection guidance.
|
|
2799
|
+
- `@kerfjs/ui`'s `wireTokenSearchFields` `onEdit` callback now also receives the originating `InputEvent` (`onEdit({ id, editor, event })`), so a consumer can gate token-commit behavior on `event.inputType` / `event.data` (e.g. commit a chip only on whitespace-terminated input) and drop its own `input` listener entirely — completing the goal `onEdit` was added for. Additive; existing `{ id, editor }` destructuring is unaffected.
|
|
2800
|
+
- Added a CI-only visual-drift gate for the `@kerfjs/ui` SVG design templates (`check:design-templates:drift`): it re-renders every template in a browser and fails if any regenerated SVG differs from the committed one (normalizing run-to-run noise — XML comments and auto-minted ids/font names). It closes the gap the offline `check:design-templates` cannot cover and runs in CI's browser-capable job, not the offline pre-commit `check`.
|
|
2801
|
+
- Added `FloatingToolbar` to `@kerfjs/ui` (`@kerfjs/ui/floating-toolbar` + `.css`): a transparent, forced-dark toolbar that floats over the content of its nearest positioned ancestor — a drawer-restore / floating-controls pattern — without covering dialogs or overlays (it is not top-layer). It exposes `role="toolbar"` with a required `label`, floats to a customizable `position` (`bottom-end` default, plus the other corners/edges), and is inset by `--kui-floating-toolbar-inset` (default 8px past a top toolbar's own inset). The app owns the controls and their visibility. Includes a UX-catalog demo with a show/hide toggle that auto-hides on leaving the demo.
|
|
2802
|
+
- Added a `shape` prop to `@kerfjs/ui`'s `ToolbarControlGroup`: `'pill'` (default, unchanged) or `'rounded'` for a softer rounded-rectangle corner on the group and its items (using the design system's rounded radius). The UX catalog's ToolbarControlGroup section gains a Pill/Rounded toggle that switches every example.
|
|
2803
|
+
- Added a **Design template** link to the UX catalog's detail-footer resources (beside demo source / component source / guidance) for every component that has a committed SVG design template, pointing at its per-component library under `docs/design/templates/`. The link is generated from the design-template manifest during `catalog:sync`, so it appears only where a template exists and stays in step as the template set grows.
|
|
2804
|
+
- Gave the `@kerfjs/ui` `Catalog` detail footer's related-entries popup trigger a visible **"Component"** text label beside its glyph (it was icon-only), so the affordance reads as a labeled control rather than a bare icon. The trigger grows to fit the label instead of the single-group's fixed icon-button width, and the glyph stays beside the label.
|
|
2805
|
+
- Fixed the `@kerfjs/ui` `Catalog` component's `secondarySections` ("ecosystem" group) indentation: the subsection heading sat further left than everything else while its list items sat further right, so the group read as awkwardly stair-stepped. The subsection heading now takes the same 8px content inset the list items and the group's `ListHeader` self-apply, so the group heading, its subsection headings, and their items all share one left edge — matching the primary sidebar.
|
|
2806
|
+
- Extended the `@kerfjs/ui` SVG **design templates** (`ui/docs/design/templates/`) from the initial two components to fourteen — adding `Toolbar`, `ToolbarText`, `ListItem`, `ListHeader`, `ListActionRow`, `ValueTable`, `StateBanner`, `EmptyState`, `Skeleton`, `SegmentedControl`, `TabBar`/`AppTab`, and `TokenSearchField`, each captured (light + dark, system-font text, self-contained inlined libraries) in its common presentation combinations with realistic sample data. Added an offline sync-check gate (`check:design-templates`, part of `npm run check`) that verifies every manifest component/variant has its committed output and flags stray files. `Select` (Web Awesome runtime), the decorative icon primitives, `ResizableRegion`, the whole-screen layouts, and the `Catalog` shell are deliberately not templated (see `ui/docs/design/templates.md`).
|
|
9
2807
|
- Extended `@kerfjs/ui`'s `wireTokenSearchFields` with three opt-in knobs for dropping a real app's hand-rolled token-search plumbing: a **focusout keep-open exception** (`collapsible.keepOpenOn(target)` or a `data-token-search-keep-open` region) so focus moving to a sibling suggestions/date/help surface no longer collapses an empty collapsible field; **opt-in atomic-chip keyboard** (`keyboard`) where, from a collapsed caret with no selection, Backspace/Delete remove the adjacent chip (reported via `onRemoveToken` for the app to apply to its controlled state) and ArrowRight moves the caret past a trailing chip; and an **`onEdit({ id, editor })`** callback fired on every editor `input` so callers can drop their own `input` listener. All are additive and off/unchanged by default (the keyboard block is opt-in; the full `collapsible: false` opt-out and per-behavior opt-outs are unchanged).
|
|
10
2808
|
- Fixed a collapsible `@kerfjs/ui` `TokenSearchField` stretching to its full expanded width **in height** when placed directly in a flex column: the expanded size was carried on the flex `basis`, which a column reads as the cross-axis height (a ~480px-tall field). The field and its enclosing toolbar group now size their inline axis through `width` alone (`flex: 0 1 auto`), so the field stays a single line tall in any container while a toolbar row still shrinks it as before.
|
|
11
2809
|
- Added SVG **design templates** for `@kerfjs/ui` components (`ui/docs/design/templates/`): each component captured from its real rendered HTML/CSS in its common presentation combinations via `domotion-svg`, with a per-component library file that references the individual variant SVGs. A `design-templates:build` script + `ui/docs/design/templates.md` cover regeneration, maintenance, and templating your own app-level components. Initial coverage: PanelHeader and ToolbarControlGroup.
|
|
@@ -129,9 +2927,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
129
2927
|
- Added the lockstep `@kerfjs/ui` package: accessible toolbar, menu, tab, header, resize, select, and feedback primitives; semantic CSS tokens and browser component subpaths that automatically include only reachable component styles; CSS-free root/SSR entries and an optional complete stylesheet; opt-in Web Awesome registration; AI-oriented docs; a production-backed UX catalog; bundle/unit/three-engine browser gates; and a least-privilege release workflow.
|
|
130
2928
|
- The virtual-list demo animation opens on the full app again (header, the "N in the DOM" badge, and the list) before the scroll; the list now scrolls inside its own box while the header stays put (previously the whole frame was a full-bleed list strip), holds at row 0 before gliding instead of starting a few rows down, and the loop stays in phase on every pass. Captured with domotion-svg 0.28.2.
|
|
131
2929
|
|
|
132
|
-
|
|
133
|
-
|
|
2930
|
+
- @kerfjs/ui panel toolbars now place groups in leading, center, and trailing zones; relocateOnCollapse keeps chosen groups available when a panel closes.
|
|
134
2931
|
|
|
2932
|
+
## [4.4.1] - 2026-08-26
|
|
135
2933
|
|
|
136
2934
|
- Overhauled the README landing page: new one-line tagline, npm/size/license/TypeScript-types badges, a hoisted "Quick start" (install + `tsconfig`) right under the hook, and a jump nav (Quick start · Why kerf · Quick tour · Docs). The counter example now shows `delegate()` wiring the click handler.
|
|
137
2935
|
- Reworked the homepage hero to lead with the value proposition ("Reactive UI that touches only the bytes that changed"), with "Introducing Kerf" demoted to an eyebrow, a right-sized logo, tighter vertical spacing, and "Get started" as the single filled primary action ("View examples" now a secondary/outline button).
|
|
@@ -140,7 +2938,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
140
2938
|
- Rewrote the site's docs pages as hand-authored consumer content (cleaner headings, no internal section numbers) rather than verbatim copies of the internal design docs.
|
|
141
2939
|
- Renamed the migration guides' "Side-by-side code" heading to "Section by section" (the sections stack code blocks rather than showing true columns), across all framework pages.
|
|
142
2940
|
|
|
143
|
-
|
|
144
2941
|
- The getting-started and Markdown-editor demo animations now open on a non-blank frame — real code and a rendered heading are visible immediately instead of an empty pane.
|
|
145
2942
|
- Added a horizontal-scroll shadow affordance to wide tables so off-screen columns are discoverable on narrow viewports.
|
|
146
2943
|
- Shortened the longest demo alt text on the router and virtual-list pages for more concise screen-reader output.
|
|
@@ -148,29 +2945,23 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
148
2945
|
|
|
149
2946
|
## [4.4.0] - 2026-08-23
|
|
150
2947
|
|
|
151
|
-
|
|
152
|
-
|
|
153
2948
|
- Reworked the router example app to run inside a fake browser window — traffic-light chrome, working Back/Forward buttons wired to `router.back()`/`forward()` via one delegated listener, and a live address bar bound to `router.route` that updates as you navigate — making the URL-driven, no-reload story clearer.
|
|
154
2949
|
|
|
155
2950
|
- **New `kerfjs/router` subpath — a client-side router (the "postcard router").** `createRouter({ routes, mode?, base?, interceptLinks? })` returns a handle over three things kerf already has: a reactive `route` signal (`{ path, params, query, hash }`), `delegate()`-based `<a href>` link interception, and a keyed **outlet** — `router.outlet()` renders the matched route in a `data-key`ed wrapper, so kerf's keyed morph **replaces the page wholesale on a route change** (fresh DOM) and **reconciles in place on a same-route param change** (preserving scroll / focus). Route patterns are static, `:param`, a trailing `*rest` wildcard, and `*` catch-all; the handle also gives `navigate(path, { replace?, state? })`, `back()`/`forward()`, `match(pattern)` / `activeClass(pattern, className)` reactive active-link helpers, hash **or** history mode, an optional base path, and `dispose()`. Link interception is automatic (same-origin, left-click, no modifier/`target`/`download`, opt out per-link with `data-router-ignore` / `rel="external"` or globally with `interceptLinks: false`). **Deliberately scoped** — no nested layouts, data loaders, lazy routes, guards, or SSR matching; compose those with kerf primitives (`resource` for loading, an `effect` on `route` for guards). The kerf **core stays router-free** — this is opt-in and tree-shakeable, adding nothing to the main barrel until imported, and docs/1's "Not a router" is about the runtime. See [`docs/20-router.md`](docs/20-router.md).
|
|
156
2951
|
|
|
157
2952
|
## [4.3.0] - 2026-08-22
|
|
158
2953
|
|
|
159
|
-
|
|
160
|
-
|
|
161
2954
|
- KF-529: refresh README for the 4.3 cycle (prep-major-release) (`909c82a`)
|
|
162
2955
|
- KF-530: extract promise-dialog helpers into overlay-dialogs.ts (`5d4ac38`)
|
|
163
2956
|
- KF-528: sync llms.txt with docs/19 + 4.3 features (`1d9c004`)
|
|
164
2957
|
|
|
165
2958
|
- **`kerfjs/overlay` gains opt-in native top-layer backing (`native: true`).** Every overlay surface (`overlay`, `confirm`, `prompt`, `form`, `choice`, `popover`, `tooltip`) now takes `native?: boolean` (default `false`). When `true` and the engine supports it, a **modal** surface (`trap: true`) is hosted in a `<dialog>` opened with `.showModal()` — real document inerting (pointer + focus + AT) and guaranteed stacking above any `z-index` — and a **non-modal** surface (`trap: false`) uses the **Popover API** (`[popover]` + `showPopover()`). Feature-detected (`HTMLDialogElement.prototype.showModal`, `HTMLElement.prototype.showPopover`), falling back to today's plain `<div>` where unsupported, so `native: true` is always safe to pass. The `render` slot + promise API are unchanged — kerf just hosts your markup in a `<dialog>` / `[popover]`. **Opt-in on purpose:** the native elements carry UA default styles (a `::backdrop`, centering, border, padding) that kerf does **not** reset (a reset would violate the zero-CSS contract) — style the element and its `::backdrop` via `className` (`.kerf-overlay::backdrop { … }`, the stable contract); and `container` becomes a visual no-op in native mode (the top layer ignores DOM position). See [`docs/19-native-overlay-backing.md`](docs/19-native-overlay-backing.md).
|
|
166
|
-
- **`bindList` virtualization gains a `content-visibility` mode.** `virtualize: { rowHeight, mode: 'content-visibility' }` is a second virtualization strategy alongside the default `mode: 'window'` (today's JS windowing). It keeps **every** row in the DOM and sets `content-visibility: auto` + `contain-intrinsic-size: 0 <rowHeight>px` on each row, so a supporting engine (Chromium, Safari 18) skips the
|
|
2959
|
+
- **`bindList` virtualization gains a `content-visibility` mode.** `virtualize: { rowHeight, mode: 'content-visibility' }` is a second virtualization strategy alongside the default `mode: 'window'` (today's JS windowing). It keeps **every** row in the DOM and sets `content-visibility: auto` + `contain-intrinsic-size: 0 <rowHeight>px` on each row, so a supporting engine (Chromium, Safari 18) skips the _layout/paint_ of off-screen rows while **all rows stay findable** — find-in-page (Cmd/Ctrl+F), the accessibility tree, and anchor links / `scrollIntoView` all work on any row (the exact guarantee `mode: 'window'` can't give, since it removes off-window rows from the DOM). The `mode` choice is the app's and it's about list size: pick `'content-visibility'` for medium lists where findability beats the node ceiling, keep `'window'` for very large (100k-row) lists. In this mode `rowHeight` is only the `contain-intrinsic-size` placeholder (no windowing math), `setHeight` / `observeRowHeights` are no-ops (the browser owns measurement), `minRows` is ignored (all rows already render), and no scroll listener / `ResizeObserver` is installed — while `handle.container` / `containerClass` / `containerId` still work. There is deliberately **no feature detection**: on an engine without `content-visibility` the CSS is inert, so all rows still render (correct, still findable) — only the off-screen-skip optimization is absent. See [`docs/17-list-virtualization.md`](docs/17-list-virtualization.md) §17.11.
|
|
167
2960
|
- **Docs: `bindList` virtualization findability/a11y tradeoff is now a first-class caveat.** Off-window rows are removed from the DOM (not just hidden), so with `virtualize` set, find-in-page (Cmd/Ctrl+F), screen readers / the accessibility tree, and anchor links / `scrollIntoView` reach only the visible window. New `docs/17-list-virtualization.md` §17.10 spells out the consequences and the guidance (don't virtualize, or use `minRows` above the list length, when full findability matters more than the DOM node ceiling), and the `bindList` JSDoc + `docs/8-api-reference.md` §8.11 carry the same note. Behavior unchanged — documentation only.
|
|
168
2961
|
- **State-preserving row moves via `moveBefore()` (transparent optimization).** When a keyed list reorders — `each()` (snapshot and granular paths), `bindList`, and `morph()`'s keyed / positional / list-marker moves — kerf now relocates an already-connected row with `Node.prototype.moveBefore()` where the engine supports it (Chromium 133+, spreading to other engines), falling back to `insertBefore()` everywhere else. `moveBefore()` is an atomic move: the node is never disconnected, so a moved row keeps its focus, text selection, `<iframe>` document state, playing media, running CSS transitions/animations, and open `popover`/`dialog` state across the reorder — richer state than the existing focus snapshot in the reconciler could ever restore, and it needs no snapshot at all where it runs. No API change and no behavior change on engines without `moveBefore()`; the focus-preservation snapshot stays in place for them. Fresh (not-yet-connected) rows still use `insertBefore()` — only genuine moves of connected rows take the new path. See [`docs/18-state-preserving-moves.md`](docs/18-state-preserving-moves.md).
|
|
169
2962
|
|
|
170
2963
|
## [4.2.0] - 2026-08-20
|
|
171
2964
|
|
|
172
|
-
|
|
173
|
-
|
|
174
2965
|
- Added a **Virtual list** example app — a 10,000-row virtualized list showcasing the companion subpaths together: `kerfjs/list` viewport virtualization (only a screenful in the DOM), `kerfjs/timing` debounced search, and `kerfjs/overlay` confirm-to-delete with a toast. Includes a live "in the DOM" counter that stays flat as you scroll all 10,000 rows.
|
|
175
2966
|
- Expanded the README with a companion-subpaths spotlight covering `list`, `overlay`, `async`, `scope`, `timing`, `remount`, `attach`, and `actions`, with `bindList` fixed and measured-height virtualization examples.
|
|
176
2967
|
- Corrected the complete-apps index to accurately describe the eight standalone showcase apps and note that the cart and counter-store apps live in the migration guides.
|
|
@@ -184,86 +2975,70 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
184
2975
|
- **New `kerfjs/remount` subpath** — `remountOn(parent, key, render)`, the opposite of kerf's morph-by-default: **replace** a subtree wholesale when `key` changes instead of morphing it. Names the hand-rolled `data-key={`gen-${n}`}` + `data-morph-skip` counter trick, for library-owned subtrees (a highlighted diff, a chart, an editor) that must tear down and re-initialize on fresh DOM. `key` is a signal or a thunk `() => K`; an unchanged key (including a thunk whose inputs moved but whose value stayed equal) leaves the subtree alone, so per-row reactivity inside `render` still updates in place. An optional `onMount(root)` callback runs after each (re)mount with the live subtree — the place to bind an imperative widget (`kerfjs/attach`) to the fresh DOM; returning `attach`'s disposer from it makes teardown synchronous. `remountOn` owns `parent`'s children and returns a disposer. Optional and tree-shakeable.
|
|
185
2976
|
- **New `kerfjs/timing` subpath** — the `let timer; clearTimeout(timer); timer = setTimeout(…)` pattern every app hand-rolls, blessed with disposer-shaped ergonomics. `debounce(fn, ms)` is trailing-edge (runs once `ms` after calls stop, latest args); `throttle(fn, ms)` is leading-plus-trailing (fires immediately, then at most once per `ms`, collapsing a burst to one trailing call). Both return a callable with `cancel()` / `flush()`. `debouncedSignal(source, ms)` is a read-only signal that trails `source` by `ms` so it composes inside the reactive graph (`computed`/`effect`/`mount`) instead of beside it. `debounce`/`throttle` are dependency-free; `debouncedSignal` pulls in signals only (no render core). Optional and tree-shakeable; tiny.
|
|
186
2977
|
- **New `kerfjs/scope` subpath** — tie disposers to a DOM element's lifetime, so append-heavy UIs stop leaking detached-but-subscribed effects/listeners. `disposeScope(el)` returns a WeakMap-keyed, accumulating scope whose `add(disposer)` (plus convenience `mount` / `effect` / `delegate` wrappers that register their own disposer) collects teardown; `dispose()` runs it all best-effort and idempotently. `disposeSubtree(root)` sweeps a subtree before removal; `observeRemovals(root)` installs one `MutationObserver` that auto-disposes on removal. No module-level mutable state. Optional and tree-shakeable.
|
|
187
|
-
- **New `kerfjs/overlay` subpath** — the blessed modal/overlay + dismiss manager that every real kerf app hand-rolls. `overlay(content, options?)` appends a wrapper, `mount()`s content inside it (owning the disposal), wires dismissals (Escape / backdrop / outside-click, with `outsideIgnore`), a focus trap (`role="dialog"` / `aria-modal`, Tab wrap-around, restore-focus-on-close), and returns `{ el, close(result?), result }`. `confirm(message, options?)` is a promise-based `window.confirm` replacement, and `prompt(message, options?)` → `Promise<string | null>` its `window.prompt` counterpart (both globals are no-ops in Tauri webviews); `form(fields, options?)` → `Promise<Record<string, string> | null>` collects a two-or-three-field dialog. `choice<R>(message, actions, options?)` → `Promise<R | null>` is the **N-way** sibling of `confirm` — one button per action resolves that action's `value` (or `null` on dismissal), and `options.defaultValue` makes **Enter** (anywhere in the dialog) resolve a default action (the "global Enter-to-confirm" model) without holding the overlay handle. `prompt`/`form` submit on Enter and take an inline `validate`; all auto-escape their content. `confirm` / `prompt` / `form` also accept a **`render` slot option** for design-system teams — return your own markup and spread the provided `ok`/`cancel` (+ `input`/`error`) wiring onto it; kerf keeps owning the promise, `validate`, Enter-submit, dismiss, focus-trap, and focus-restore, so you adopt the batteries-included dialogs without a CSS rewrite. `popover(anchor, content, options?)` → `OverlayHandle` is a non-modal **anchored** overlay: it positions the content relative to `anchor` (below by default, flipping above on viewport overflow, clamped horizontally), defaults to dismiss-on-outside with the anchor exempt, and repositions on scroll / resize. `popover`'s placement core is also exported standalone: **`positionAnchored(el, anchor, options?)`** (one-shot) and **`autoReposition(el, anchor, options?)`** (keeps an element positioned on scroll/resize, returns a disposer) position
|
|
2978
|
+
- **New `kerfjs/overlay` subpath** — the blessed modal/overlay + dismiss manager that every real kerf app hand-rolls. `overlay(content, options?)` appends a wrapper, `mount()`s content inside it (owning the disposal), wires dismissals (Escape / backdrop / outside-click, with `outsideIgnore`), a focus trap (`role="dialog"` / `aria-modal`, Tab wrap-around, restore-focus-on-close), and returns `{ el, close(result?), result }`. `confirm(message, options?)` is a promise-based `window.confirm` replacement, and `prompt(message, options?)` → `Promise<string | null>` its `window.prompt` counterpart (both globals are no-ops in Tauri webviews); `form(fields, options?)` → `Promise<Record<string, string> | null>` collects a two-or-three-field dialog. `choice<R>(message, actions, options?)` → `Promise<R | null>` is the **N-way** sibling of `confirm` — one button per action resolves that action's `value` (or `null` on dismissal), and `options.defaultValue` makes **Enter** (anywhere in the dialog) resolve a default action (the "global Enter-to-confirm" model) without holding the overlay handle. `prompt`/`form` submit on Enter and take an inline `validate`; all auto-escape their content. `confirm` / `prompt` / `form` also accept a **`render` slot option** for design-system teams — return your own markup and spread the provided `ok`/`cancel` (+ `input`/`error`) wiring onto it; kerf keeps owning the promise, `validate`, Enter-submit, dismiss, focus-trap, and focus-restore, so you adopt the batteries-included dialogs without a CSS rewrite. `popover(anchor, content, options?)` → `OverlayHandle` is a non-modal **anchored** overlay: it positions the content relative to `anchor` (below by default, flipping above on viewport overflow, clamped horizontally), defaults to dismiss-on-outside with the anchor exempt, and repositions on scroll / resize. `popover`'s placement core is also exported standalone: **`positionAnchored(el, anchor, options?)`** (one-shot) and **`autoReposition(el, anchor, options?)`** (keeps an element positioned on scroll/resize, returns a disposer) position _your own_ element with no overlay lifecycle. **`tooltip(anchor, content, options?)`** is a hover/focus-triggered, non-modal, auto-hiding tooltip built on them. `toast(content, options?)` now returns a **`ToastHandle` (`{ el, dismiss }`)** instead of a bare dismiss function, so callers can inspect the node, wire an action button, or run entrance/exit transitions; new options: `mode: 'replace'` (collapse a rapid sequence to the latest) with `collapse: 'fade'` (default — run the prior toast's exit transition, good for a stacking region) or `'instant'` (remove it synchronously, what a single centered slot wants so messages never cross-fade in place), `variant` (`'info'`/`'success'`/`'warning'` → a `${className}--${variant}` accent class), `enterClass` (added on the next animation frame for a CSS entrance) and `exitClass` + `exitDuration` (CSS owns the exit — on dismiss the `enterClass` is **removed** so `exitClass` needn't out-specify it, and a symmetric single-class fade works with just `enterClass` + `exitDuration`; the node is removed after the delay). `dismiss({ instant: true })` removes the toast **synchronously** (skipping the exit transition), and `mode: 'replace'` `collapse: 'instant'` also force-removes a toast that is already mid-fade — so an action button that dismisses itself and shows a replacement in a single centered slot doesn't cross-fade the two. **Breaking (beta):** `toast()`'s return type changed from `() => void` to `{ el, dismiss }` — call `toast(...).dismiss()` or destructure `{ dismiss }`. Structural only — kerf ships no CSS. Optional and tree-shakeable; shares the core with the main barrel via code-splitting.
|
|
188
2979
|
- **New `kerfjs/actions` subpath** — the blessed delegated action-table helper. `action(value)` returns a `data-action` `AttrSpec` (a thin specialization of `attr()`); `delegateActions(root, eventType, table, options?)` wires a whole table of `data-action` handlers with one delegated listener (built on `delegate()`) and returns a disposer. Formalizes the most-reinvented idiom in real kerf apps — one `attr('data-action', …)` table as the single source of truth for both the JSX attribute and the delegate dispatch. Optional and tree-shakeable; adds nothing to the main barrel.
|
|
189
2980
|
|
|
190
2981
|
## [4.1.1] - 2026-08-14
|
|
191
2982
|
|
|
192
|
-
|
|
193
|
-
|
|
194
2983
|
- Added a CDN / importmap quickstart (`docs/6-jsx-runtime.md` §6.11.1) covering three no-install ways to load kerf from an ESM CDN: a direct `esm.sh` import, and jsDelivr / unpkg behind an importmap that also maps `@preact/signals-core`. Explains why a raw `dist/*.js` path fails (its unrewritten bare `@preact/signals-core` import won't resolve in the browser) and recommends pinning to a major version.
|
|
195
2984
|
- Updated the README no-build example to use version-pinned CDN URLs (`kerfjs@4`) and noted that jsDelivr / unpkg need an importmap while esm.sh works with a direct import.
|
|
196
2985
|
|
|
197
2986
|
## [4.1.0] - 2026-07-31
|
|
198
2987
|
|
|
199
|
-
|
|
200
|
-
|
|
201
2988
|
- ESLint 10 is now supported: `eslint-plugin-kerfjs` declares `peerDependencies.eslint` as `^9.0.0 || ^10.0.0`, and every named major is exercised by the rule suite in CI.
|
|
202
2989
|
- The plugin now runs its rule suite against each supported ESLint major via `npm run test:eslint-matrix`, which also fails if the declared peer range, the tested set, and the CI matrix disagree — or if the range is left open-ended.
|
|
203
2990
|
|
|
204
|
-
|
|
205
2991
|
- **ESLint 8 is no longer supported by `eslint-plugin-kerfjs`.** The package is ESM-only and ESLint 8 resolves `.eslintrc` plugins with `require()`, so `extends: ["plugin:kerfjs/…"]` could never load it.
|
|
206
2992
|
- **Legacy `.eslintrc` configuration is documented as unsupported.** Use flat config (`eslint.config.js`); the `legacy-recommended` export remains in the package but is unreachable through any config system and should be treated as deprecated.
|
|
207
2993
|
|
|
208
|
-
|
|
209
2994
|
- Generated Hot Sheet skill and rule files no longer hardcode a machine's local API port or shared secret. The curl fallbacks read `$HOTSHEET_PORT` / `$HOTSHEET_SECRET` from `.hotsheet/settings.local.json` and `.hotsheet/secret.json` instead, with `.hotsheet/settings.json` as the fallback for older projects.
|
|
210
2995
|
- A new `npm run check:audit` gate runs `npm audit --omit=dev --audit-level=high` against the published dependency tree and is wired into the pre-push `check:full` gate.
|
|
211
2996
|
|
|
212
|
-
|
|
213
2997
|
- Test tooling moved to vitest 4 and ESLint 10; coverage thresholds were recalibrated to 98.5% branches / 99.5% statements (lines and functions stay at 100%) after vitest 4's sharper AST-based coverage mapping resolved seventeen previously-miscredited defensive branches.
|
|
214
2998
|
- Removed the `hs-m` marketing-ticket skill and Cursor rule.
|
|
215
2999
|
|
|
216
3000
|
## [4.0.0] - 2026-07-28
|
|
217
3001
|
|
|
218
|
-
|
|
219
|
-
|
|
220
3002
|
- `draggable`, `spellCheck`, and `contentEditable` no longer accept booleans — they are enumerated HTML attributes, so write the keyword string (`draggable="true"`, `spellCheck="false"`). The boolean forms rendered markup that meant the opposite of what was written; real boolean attributes like `hidden`/`checked`/`disabled` are unchanged.
|
|
221
3003
|
- `<select value>` / `<textarea value>` (and their `defaultValue` forms) no longer typecheck — neither element has a `value` content attribute, so kerf was emitting markup no browser reads. Use `<option selected>` and `<textarea>{draft}</textarea>`.
|
|
222
|
-
- Lowercase `autofocus` no longer accepts `"true"` / `"false"` — it is a real boolean attribute, so `autofocus="false"` turned autofocus
|
|
223
|
-
|
|
3004
|
+
- Lowercase `autofocus` no longer accepts `"true"` / `"false"` — it is a real boolean attribute, so `autofocus="false"` turned autofocus _on_. Use `autofocus={false}` or omit it.
|
|
224
3005
|
|
|
225
3006
|
- `href="javascript:void(0)"` and its five sibling spellings are no longer dropped by the URL screen. Dropping the `href` unmade the anchor — it lost `:link` styling, keyboard focus, and its pointer cursor. Matching is against the whole normalized value, so nothing can ride along after an inert body.
|
|
226
3007
|
- `defaultSelected` rendered as `defaultselected`, an attribute no browser reads, so the option it named was never pre-selected. It now correctly emits `selected`.
|
|
227
3008
|
|
|
228
|
-
|
|
229
3009
|
- Typed JSX gained a wide set of modern attributes: globals `inert`, `popover`, `nonce`, `part`/`exportparts`, `enterKeyHint`, `translate`, `autocorrect`, and the full microdata family; per-element `<button popoverTarget/popoverTargetAction/command>`, `<input popoverTarget/popoverTargetAction>`, `<form rel>`, `<source width/height>`, and `writingsuggestions`.
|
|
230
3010
|
- `<button command>` is now typed to the spec keywords (`show-modal`, `close`, `request-close`, `toggle-popover`, `show-popover`, `hide-popover`) plus any `--custom` command, catching a custom command written without its required `--` prefix.
|
|
231
3011
|
- `<a download>` and `<area download>` accept the bare boolean form as well as a string filename, so `<a href="/report.pdf" download>` compiles.
|
|
232
3012
|
|
|
233
|
-
|
|
234
3013
|
- The advertised bundle size is corrected everywhere: ~12 KB min+gzip for a realistic import, ~13 KB with `arraySignal` (previously stated as ~11/~12 KB, and ~6.1 KB in the Cursor rules). Every migration page's delta row was recomputed against the new figure.
|
|
235
3014
|
- The JSX runtime doc gained a section on enumerated vs. boolean attributes, and the API reference now records where the JSX types come from (WHATWG HTML Living Standard + SVG 2, not another framework's property table).
|
|
236
3015
|
- The dev-warning sections in the dev-warnings doc were renumbered by family so the `11.2.N` headings run in document order; cross-references elsewhere in the docs were updated to match.
|
|
237
3016
|
|
|
238
|
-
|
|
239
|
-
- The bundle-size check now gates the *prose* as well as the build: every place the docs advertise a size is matched against the measured figure, and a reworded claim that stops matching fails rather than going silently unchecked.
|
|
3017
|
+
- The bundle-size check now gates the _prose_ as well as the build: every place the docs advertise a size is matched against the measured figure, and a reworded claim that stops matching fails rather than going silently unchecked.
|
|
240
3018
|
- New repo checks wired into `npm run check`: `check:docs:dev-warns` (the dev-warning doc and `ENV_NAME` must name the same set, with monotonic section numbers), `check:design-rule-5` (every top-level `let` in `src/` must be accounted for by the documented rule), and `check:skills` (a skill file restating a threshold its owner disagrees with fails).
|
|
241
3019
|
|
|
242
3020
|
## [3.0.0] - 2026-07-27
|
|
243
3021
|
|
|
244
|
-
|
|
245
|
-
|
|
246
3022
|
- **kerf no longer infers development mode — add one line to your entry to keep the dev diagnostics.**
|
|
247
3023
|
|
|
248
3024
|
```js
|
|
249
|
-
if (import.meta.env.DEV) await import(
|
|
250
|
-
if (process.env.NODE_ENV !==
|
|
3025
|
+
if (import.meta.env.DEV) await import("kerfjs/dev"); // Vite
|
|
3026
|
+
if (process.env.NODE_ENV !== "production") await import("kerfjs/dev"); // webpack / Node
|
|
251
3027
|
```
|
|
252
3028
|
|
|
253
3029
|
Without it you get production shape: no dev warnings, no read-only `defineStore` `get()`
|
|
254
3030
|
snapshot, and a screened dangerous URL warns-and-drops instead of throwing. Nothing else
|
|
255
3031
|
changes, and if you never used the dev warnings you get a smaller bundle for free.
|
|
256
3032
|
|
|
257
|
-
**Why it changed.** kerf inferred its own dev/prod mode by reading `globalThis.process?.env?.NODE_ENV`. Bundlers substitute the
|
|
3033
|
+
**Why it changed.** kerf inferred its own dev/prod mode by reading `globalThis.process?.env?.NODE_ENV`. Bundlers substitute the _bare_ `process.env.NODE_ENV` token and never create a `globalThis.process` object for browser targets, so that read was `undefined`, `undefined !== 'production'` was `true`, and every production browser build silently took the development path. Consequences that were shipping: every `defineStore` `get()` returned a deep read-only `Proxy` (an allocation on every store read), a screened `javascript:`/`data:` URL **threw** instead of the documented warn-and-drop, and the always-on list-key warnings printed to production consoles. Server/Node builds, where `process` exists, were unaffected.
|
|
258
3034
|
|
|
259
3035
|
Removing the inference also shrinks production bundles by **~4.7 KB min+gzip (27%)** — a realistic import (`signal`/`computed`/`effect`/`batch`/`mount`/`each`/`delegate`) goes from 16.91 KB to **12.24 KB**. Previously the dev-warning modules were imported unconditionally by `mount()`/`each()` and gated at runtime, so they shipped to production regardless of build mode and no amount of tree-shaking could reclaim them. With the dev entry absent they are unreachable, and the `import()` statement itself is eliminated — the chunk is never emitted, let alone fetched.
|
|
260
3036
|
|
|
261
3037
|
`globalThis.KERF_DEV` is no longer consulted — not importing the dev entry is now the (compile-time) way to opt out. One ordering note: `signal()` picks its constructor at creation time, so put the import first if you rely on the untracked-signal warning.
|
|
262
3038
|
|
|
263
|
-
- The `KERF_DEV_WARN_*` diagnostics no longer consult `NODE_ENV` or `globalThis.KERF_DEV` at all. Whether they run is decided in exactly one place: whether you imported `kerfjs/dev`. The previous release stopped kerf's core from inferring dev mode but left a second, inherited gate inside each warner, so a Node/SSR consumer who
|
|
3039
|
+
- The `KERF_DEV_WARN_*` diagnostics no longer consult `NODE_ENV` or `globalThis.KERF_DEV` at all. Whether they run is decided in exactly one place: whether you imported `kerfjs/dev`. The previous release stopped kerf's core from inferring dev mode but left a second, inherited gate inside each warner, so a Node/SSR consumer who _deliberately_ installed the diagnostics under `NODE_ENV=production` got silence — and `globalThis.KERF_DEV = false` still silenced warnings the consumer had explicitly opted into. Both are gone; each warning is now gated only by its own env var. If you were using `globalThis.KERF_DEV` to turn diagnostics off, remove the dev import instead (which is also what sheds the ~4.7 KB from your bundle).
|
|
264
3040
|
|
|
265
|
-
|
|
266
|
-
- **New:** `each()` accepts an options object — `each(items, render, { cacheKey, key })` — and `key` gives a list a **stable identity**. Without one a list is identified by its position among the `each()` calls in a render, so adding or removing a conditional list above it made kerf rebuild it from scratch: rows lost their DOM nodes, and with them focus, scroll position and in-progress IME composition. Keying a list removes that dependency; because a keyed list doesn't occupy a positional slot, keying just the *conditional* list usually stabilizes its siblings too. The existing three-argument `each(items, render, cacheKey)` form is unchanged. In development kerf now warns once per list when it detects such a shift and names the fix.
|
|
3041
|
+
- **New:** `each()` accepts an options object — `each(items, render, { cacheKey, key })` — and `key` gives a list a **stable identity**. Without one a list is identified by its position among the `each()` calls in a render, so adding or removing a conditional list above it made kerf rebuild it from scratch: rows lost their DOM nodes, and with them focus, scroll position and in-progress IME composition. Keying a list removes that dependency; because a keyed list doesn't occupy a positional slot, keying just the _conditional_ list usually stabilizes its siblings too. The existing three-argument `each(items, render, cacheKey)` form is unchanged. In development kerf now warns once per list when it detects such a shift and names the fix.
|
|
267
3042
|
|
|
268
3043
|
- **`kerfjs/dev` now exports `enableWarnings()`** — and it fixes a defect: until now, none of the
|
|
269
3044
|
`KERF_DEV_WARN_*` diagnostics could be switched on in a browser at all. Every one of them read `globalThis.process.env`, which does not exist in a browser realm — and a bundler `define` cannot reach it either, because the read goes through `globalThis.process` into a local binding rather than the substitutable `process.env.X` token. So in a Vite/webpack dev server, the environment where these warnings are most wanted, the entire opt-in family was permanently and silently off. Only Node/SSR (and kerf's own vitest suite, which is why nothing caught it) could turn any of them on.
|
|
@@ -272,8 +3047,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
272
3047
|
|
|
273
3048
|
```js
|
|
274
3049
|
if (import.meta.env.DEV) {
|
|
275
|
-
const dev = await import(
|
|
276
|
-
dev.enableWarnings({
|
|
3050
|
+
const dev = await import("kerfjs/dev");
|
|
3051
|
+
dev.enableWarnings({
|
|
3052
|
+
staleBinding: true,
|
|
3053
|
+
narrowSet: true,
|
|
3054
|
+
invariants: "throw",
|
|
3055
|
+
});
|
|
277
3056
|
}
|
|
278
3057
|
```
|
|
279
3058
|
|
|
@@ -287,8 +3066,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
287
3066
|
|
|
288
3067
|
- New opt-in dev warning `KERF_DEV_WARN_LIST_REBIND=1`: fires (once per list) when an `each()` list's container is rebuilt by the morph — an ancestor's tag changed across renders, so the subtree was replaced and the list self-healed by re-binding and repopulating. The recovery is correct but discards row DOM state (focus, scroll, IME, imperative listeners); the warning names the list and points at keeping ancestor tags stable. Follows the standard `KERF_DEV_WARN_*` family rules: off by default, dev-mode only, zero production cost.
|
|
289
3068
|
|
|
290
|
-
|
|
291
|
-
- `KERF_DEV_WARN_UNTRACKED_SIGNALS=1` now tells you what it can and cannot see. The warning picks its machinery when a signal is *created*, so it only covers signals created after `kerfjs/dev` is installed — and because static imports are hoisted above a top-level `await import('kerfjs/dev')`, the module-scope signals it most wants to catch are usually created first. Previously that failed silently: you set the env var, saw nothing, and concluded your code was clean. Opting in now prints the coverage boundary once, along with the fix (make `import 'kerfjs/dev'` the first static import of a dev-only entry file). The boundary itself can't be removed — `Signal.prototype`'s `value` accessor is non-configurable, so already-created signals can't be retro-fitted without kerf keeping a registry of every signal, which production would pay for.
|
|
3069
|
+
- `KERF_DEV_WARN_UNTRACKED_SIGNALS=1` now tells you what it can and cannot see. The warning picks its machinery when a signal is _created_, so it only covers signals created after `kerfjs/dev` is installed — and because static imports are hoisted above a top-level `await import('kerfjs/dev')`, the module-scope signals it most wants to catch are usually created first. Previously that failed silently: you set the env var, saw nothing, and concluded your code was clean. Opting in now prints the coverage boundary once, along with the fix (make `import 'kerfjs/dev'` the first static import of a dev-only entry file). The boundary itself can't be removed — `Signal.prototype`'s `value` accessor is non-configurable, so already-created signals can't be retro-fitted without kerf keeping a registry of every signal, which production would pay for.
|
|
292
3070
|
|
|
293
3071
|
- An `each()` of `<tr>` written directly inside `<table>` now fails with a clear error instead of silently duplicating rows: the HTML parser inserts a `<tbody>` around the rows, which kerf cannot bind through. The message names both tags and shows the supported shape (`<table><tbody>{each(...)}</tbody></table>`). Previously this also mis-reported the rows as missing `data-key`.
|
|
294
3072
|
|
|
@@ -296,8 +3074,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
296
3074
|
|
|
297
3075
|
- Toolchain: the repo now type-checks with the native **TypeScript 7** compiler across every gate (`typecheck`, the dist `.d.ts` typing gates, the docs code-block compile — a full-repo `tsc --noEmit` now takes ~0.3 s), with `typescript@6` (the JS-API bridge release) retained for tsup's `.d.ts` emit and typescript-eslint, which still require the JS compiler API. `@typescript-eslint/*` bumped to 8.65. No shipped-code changes — `dist/` output is unaffected.
|
|
298
3076
|
|
|
299
|
-
|
|
300
|
-
- Fixed: passing a **function** as an `each()` item through an `arraySignal` insert/update (an unusual mistake, but functions are valid `WeakMap` keys so the per-item cache silently accepted them) rendered the row and then threw `each(): items must be objects…` on a *later, unrelated* re-render — far from the cause. The granular path now enforces the same objects-only item contract the snapshot path does, so the error is thrown on the render the offending mutation triggered, naming the type and index. Primitive items already behaved this way; functions now match.
|
|
3077
|
+
- Fixed: passing a **function** as an `each()` item through an `arraySignal` insert/update (an unusual mistake, but functions are valid `WeakMap` keys so the per-item cache silently accepted them) rendered the row and then threw `each(): items must be objects…` on a _later, unrelated_ re-render — far from the cause. The granular path now enforces the same objects-only item contract the snapshot path does, so the error is thrown on the render the offending mutation triggered, naming the type and index. Primitive items already behaved this way; functions now match.
|
|
301
3078
|
|
|
302
3079
|
- `arraySignal.update(i, fn)` now works when `fn` mutates the row object and returns it (not only when it returns a fresh object). Previously such a same-ref update rendered correctly in one list but left every other view of the same signal — a second list, a second `mount()`, a `filter()`ed plain-array view — permanently stale, and was lost outright when the update was batched with a selection change or a `replace()`. kerf now tracks a per-item content version so the change reaches every consumer. Returning a fresh object remains the idiomatic style; both are supported. (Fixed in the same line of work: the per-item version tracking briefly made `arraySignal.update()` throw on a signal of primitives — `arraySignal<number>` used as a plain signal — because a primitive can't key the internal version map; primitive items are now simply skipped, since only object rows are ever memoized.)
|
|
303
3080
|
|
|
@@ -309,13 +3086,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
309
3086
|
|
|
310
3087
|
- Fixed: an `arraySignal.update()` that mutated a row object in place (returning the same reference) could be silently reverted to its old content by the next unrelated re-render. The granular update path now keeps kerf's per-row HTML cache in sync with what it rendered, so the two never disagree. (Immutable updates — returning a fresh object — were unaffected, and remain the recommendation.)
|
|
311
3088
|
|
|
312
|
-
- Fixed: showing an empty conditionally-rendered `each()` list in the same batch as an update to a
|
|
3089
|
+
- Fixed: showing an empty conditionally-rendered `each()` list in the same batch as an update to a _sibling_ list could empty the sibling entirely — it rendered zero rows. When the conditional list reappeared, its list-marker comment landed next to the sibling's, and the diff overwrote the sibling marker's internal id with the reappearing one's, so the sibling's binding could no longer find its own marker. Marker comments (kerf's internal list and binding anchors) now pair only with the identical marker, never with a different one that happens to be the same kind. Ordinary comments in your markup are unaffected.
|
|
313
3090
|
|
|
314
3091
|
- Fixed: when an element rendered by a condition came back, the diff could pair it with an unrelated sibling that happened to have the same tag, and then whatever protects that sibling's contents kept the wrong contents alive inside it. A `data-morph-skip` widget **swallowed** the reappearing element — its content never rendered — and the widget was duplicated, which for a library-owned subtree (an editor, a chart, a terminal) means a second live instance attached to a node the library has no reference to. A `data-morph-preserve` child ended up under the foreign host and appeared twice. A bound hole's text leaked into the reappearing element and rendered twice. Three rules now gate that pairing: an element with an `id`/`data-key` is only ever matched to a live element with the same key; a `data-morph-skip` element only ever matches another one (so a library-owned subtree is never adopted as a stand-in, whether or not you use keys); and a comment anchoring kerf's own state only matches an anchor of the same kind. Ordinary elements without a key still match positionally exactly as before.
|
|
315
3092
|
|
|
316
|
-
- Fixed: when the number of `each()` calls in a render changed — a conditional list appearing or disappearing — a
|
|
3093
|
+
- Fixed: when the number of `each()` calls in a render changed — a conditional list appearing or disappearing — a _surviving_ list could render the departed list's rows, or render its own rows inside the wrong container. Lists without a `key` are identified by their position among the `each()` calls, and both the per-item HTML cache and the live list binding were being read as belonging to whichever list now held that position. Two lists over the same collection hit each other's cache exactly, so nothing could detect it from the data. A shift now discards that state and re-renders, which is what the documentation already described it as costing: a rebuild, never wrong output. Lists given a `key` are unaffected, and a render that doesn't change the call count is unaffected. One related improvement falls out: an unrelated list no longer loses its row nodes when a nested `each()` shifts the count.
|
|
317
3094
|
|
|
318
|
-
- Fixed: a row added to an `each()` list could land in the wrong place whenever the list wasn't the last thing inside its parent. A list ends at its last row, but kerf was looking for the next
|
|
3095
|
+
- Fixed: a row added to an `each()` list could land in the wrong place whenever the list wasn't the last thing inside its parent. A list ends at its last row, but kerf was looking for the next _element_ after it and skipping everything else on the way — so static content following the list (a footer row, a totals line, an "add item" control) got jumped, and a new row appeared after it instead of before. The same skip crossed a neighboring list's internal anchor: with two `each()` lists in one parent, rows from the first could be placed inside the second's region, and when both lists started empty their rows came out **in the wrong order** — the second list's rows rendered first. Both were correct on the initial paint and only went wrong on a later update, which made them read as intermittent. Lists now anchor on the next node of any kind, so a list's rows always stay within its own region.
|
|
319
3096
|
|
|
320
3097
|
- Fixed: an `each()` list inside an `<svg>` whose row markup contained an apostrophe (or anything else the serializer writes back differently) failed to mount at all, with a self-contradictory error. Also fixed: `each({ key })` now validates the key, so a key containing an HTML comment terminator can no longer break out of the list's internal marker and put markup in the page.
|
|
321
3098
|
|
|
@@ -327,7 +3104,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
327
3104
|
|
|
328
3105
|
- Fixed: a controlled `checked` / `value` on an `each()` row's own top-level element could stay visibly stale after the user had interacted with it — the row reconciler's attribute-only fast path wrote the attribute without syncing the live property, so whether the control obeyed your data depended on which internal route the update happened to take.
|
|
329
3106
|
|
|
330
|
-
- Fixed: when a conditionally-rendered `each()` list is added or removed, a sibling list could render the
|
|
3107
|
+
- Fixed: when a conditionally-rendered `each()` list is added or removed, a sibling list could render the _other_ list's rows — a batched "hide one list and push to another" applied the queued update to the wrong list's DOM. Lists now verify which data a pending update belongs to before applying it, and rebuild from their own items when it doesn't match.
|
|
331
3108
|
|
|
332
3109
|
- Fixed: `each()` rows inside an `<svg>` root were re-parsed in the HTML namespace on every update, so rows added or structurally changed after the first render were invisible in the browser — the initial picture looked right, which made it read as a rendering flake. Row parsing now follows the list parent's namespace (rows under `<foreignObject>` correctly stay HTML).
|
|
333
3110
|
|
|
@@ -335,92 +3112,72 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
335
3112
|
|
|
336
3113
|
- Fixed: anything sitting between a keyed `each()` list's anchor and its rows — a node injected imperatively into the list region — could make a conditional sibling's removal reorder the list, landing a trailing sibling ahead of the rows. A list's row region (anchor through last row) is now treated as one unit by the diff: it moves whole, the diff's cursor steps over it whole, and injected nodes inside it travel along keeping their position relative to the rows.
|
|
337
3114
|
|
|
338
|
-
- A conditional sibling
|
|
3115
|
+
- A conditional sibling _inside_ a keyed `each()` list's parent — a header row that comes and goes above the list — no longer costs the list's rows their DOM identity. The morph now recognizes the list's marker when a sibling shifts it, moving the marker and its rows up as a single unit instead of rebuilding the list, so row nodes, focus, and the caret survive the toggle. (An ancestor tag change, or a same-tag sibling that positionally takes the container's place, still rebuilds the list — give the list's own container a stable `id`/`data-key` if its rows need to survive that.)
|
|
339
3116
|
|
|
340
3117
|
- Fixed: a conditional sibling that shared or positionally shadowed a keyed `each()` list's container could strand the list's rows and then render a duplicate copy of them (e.g. a header `<li>` toggled inside the list's `<ul>`, or a same-tag banner `<ul>` before the list container). The morph can separate the list's marker comment from its still-attached rows; the self-heal now removes any such still-live stranded rows before repopulating, so recovery replaces the rows rather than duplicating them.
|
|
341
3118
|
|
|
342
3119
|
## [2.0.1] - 2026-07-23
|
|
343
3120
|
|
|
344
|
-
|
|
345
|
-
|
|
346
3121
|
- Fixed a `morph()` bug where static text siblings of a fine-grained bound text hole were dropped after a structural re-render (e.g. `<div>{label} / static</div>` collapsing to just the label).
|
|
347
3122
|
|
|
348
|
-
|
|
349
3123
|
- New animated coding-session demo on the getting-started page: watch the canonical counter get typed out, served, clicked in a browser, and live-edited into a todo list.
|
|
350
3124
|
- Added an animated architecture diagram and a link to the docs site from the README.
|
|
351
3125
|
|
|
352
3126
|
## [2.0.0] - 2026-07-23
|
|
353
3127
|
|
|
354
|
-
|
|
355
|
-
|
|
356
3128
|
- `delegateCapture()` now uses `closest()`-style walk-up matching like `delegate()`, passing the matched ancestor to the handler; pass `{ match: 'direct' }` (new `DelegateOptions`) to restore exact-element matching.
|
|
357
3129
|
- The dangerous-URL screen (`javascript:`, `vbscript:`, script-executing `data:` URLs) now throws an error in development instead of only warning. Production behavior is unchanged: warn and drop the attribute.
|
|
358
3130
|
|
|
359
|
-
|
|
360
3131
|
- New `kerfjs/html` tagged template: author kerf UIs with no build step (CDN / importmap, no JSX transform) with runtime semantics identical to JSX — signal holes become fine-grained bindings, attributes and text are escaped and URL-screened the same way.
|
|
361
3132
|
- New no-build example app, **live-poll**, served exactly as authored — an importmap plus one `html`-templated module, with view-source showing the app.
|
|
362
3133
|
- Added `<filter>` to the typed JSX intrinsic elements, so SVG filters compile in JSX-authored code.
|
|
363
3134
|
- Fully-bound mounts are now a documented, test-pinned guarantee: a render that reads no signal `.value` runs exactly once, forever — every update is a direct per-node write.
|
|
364
3135
|
|
|
365
|
-
|
|
366
3136
|
- Controlled form state now survives user interaction: `checked`, `value`, and `selected` DOM properties are synced when the reconciler mutates those attributes, so a clicked checkbox or typed-into input no longer ignores later updates.
|
|
367
3137
|
- Fixed stale fine-grained bindings after `arraySignal.update()`: in-place row updates now re-wire bindings whose signal instance changed, instead of leaving effects reading the old row object forever.
|
|
368
3138
|
|
|
369
|
-
|
|
370
3139
|
- New runtime dev-mode override: set `globalThis.KERF_DEV = false` (or `true`) to control dev mode without a bundler — CDN/importmap apps are no longer stuck in dev mode in production.
|
|
371
3140
|
- Two new opt-in dev warnings: `KERF_DEV_WARN_STALE_BINDING` flags bindings that silently go stale on the byte-equal fast path, and `KERF_DEV_WARN_VALUE_ONLY_RERENDER` flags re-renders whose only changes could have been fine-grained bindings.
|
|
372
3141
|
- `defineStore`'s dev-mode `get()` snapshot now returns a deep read-only proxy instead of freezing the live state: nested mutations like `get().nested.x = 1` are caught too, and they throw a descriptive `TypeError` rather than failing silently.
|
|
373
3142
|
|
|
374
|
-
|
|
375
3143
|
- Docs repositioned around the "values bind, structure re-renders" idiom as the primary way to render dynamic values, across the overview, reactivity guide, and AI assistant configs.
|
|
376
3144
|
- New guide covering the no-build authoring path and example app, plus a documentation-wide accuracy pass (delegate capture semantics, `effect()` cleanup returns, URL-screen behavior, and more).
|
|
377
3145
|
|
|
378
3146
|
## [1.0.2] - 2026-07-22
|
|
379
3147
|
|
|
380
|
-
|
|
381
|
-
|
|
382
3148
|
- Fixed the row-selector demo animation 404ing on the published site — the missing SVG capture is now generated and committed.
|
|
383
3149
|
|
|
384
3150
|
## [1.0.1] - 2026-07-22
|
|
385
3151
|
|
|
386
|
-
|
|
387
|
-
|
|
388
3152
|
- KF-329: pin release-workflow npm upgrade to npm@11 — npm@12 needs Node ≥ 22.22.2, above the 22.21.0 pin (`c54559f`)
|
|
389
3153
|
|
|
390
3154
|
## [1.0.0] - 2026-07-22
|
|
391
3155
|
|
|
392
|
-
|
|
393
|
-
|
|
394
3156
|
- **Fine-grained signal bindings** — pass a signal or `computed` directly into a JSX attribute or text hole (e.g. `class={computed(...)}` or `{sig}`) inside `mount()`, and that one node updates on signal change without re-running `render()` or walking the list reconciler. Opt-in per hole and non-breaking; works in static content and inside `each()` rows on both the snapshot and `arraySignal` reconcile paths, with binding effects correctly wired, carried, and disposed across row insert/update/remove/move.
|
|
395
3157
|
- Bound values get the same safety treatment as static ones: URL screening and `SafeHtml`/`raw()` unwrapping now apply to fine-grained bound attributes.
|
|
396
3158
|
- New interactive benchmark playground (`npm run bench:serve`) to explore kerf by hand in the standard 1k-rows benchmark app.
|
|
397
3159
|
- New **row-selector** example app and a "Fine-grained bindings" section in the reactivity demo, both showing select-row updates with zero render re-runs and zero list reconciles.
|
|
398
3160
|
|
|
399
|
-
|
|
400
3161
|
- Hardened the dangerous-URL screen: scheme detection now normalizes away C0 control characters, DEL, and leading whitespace, so obfuscations like `java script:` or a NUL before the colon can no longer slip `javascript:`/`vbscript:` URLs past the check.
|
|
401
3162
|
- Attribute names are now validated — malformed names (e.g. from spreading attacker-controlled keys into JSX) throw instead of breaking out of the open tag, and inline `on*` event handlers are rejected on both the static and signal-bound attribute paths, closing an XSS vector where a bound `onclick` would have installed a live handler.
|
|
402
3163
|
- Documented kerf's reserved marker namespace (`data-kfb`, `data-kfbrow`, `kfb:`/`kfbr:`/`kf-list:` comments) — consumer content using these can collide with binding wiring, so they're now explicitly reserved.
|
|
403
3164
|
- Documented the trusted-input bridges (`toElement()`, `morph()` with string/Element templates, `<iframe srcdoc>`) that bypass escaping by design, and the `raw()`/`SafeHtml` trust boundary.
|
|
404
3165
|
- Hardened the release pipeline: least-privilege OIDC scoping per job, SHA-pinned GitHub Actions with automated Dependabot bumps, and a token-holding publish job that runs with `--ignore-scripts` and no build tools.
|
|
405
3166
|
|
|
406
|
-
|
|
407
3167
|
- Reduced the per-row create cost of fine-grained row bindings from ~1.65× to ~1.15× via lazy row wiring — the common single-root-binding case now needs no subtree walk or extra allocations.
|
|
408
3168
|
- Published cross-framework benchmark numbers now come from the official upstream krausest js-framework-benchmark run (kerf is a merged upstream entry), replacing stale local-machine measurements.
|
|
409
3169
|
|
|
410
|
-
|
|
411
3170
|
- README refreshed for the 1.0 release: fine-grained updates and safe-by-default escaping promoted to headline features, and the status line flipped from "Pre-1.0 — API may evolve" to stable 1.0.
|
|
412
3171
|
- Fine-grained bindings documented across all consumer and AI-assistant surfaces (reactivity docs, API reference, usage guide, Cursor rules, Claude skill).
|
|
413
3172
|
|
|
414
|
-
- **Signposted the raw HTML/SVG → DOM bridges as trusted-input only.** `toElement()`, `morph()` (with a string/Element template), and the `<iframe srcdoc>` attribute bypass kerf's escaping/URL-screening by design — they're the same trust model as `innerHTML` / `raw()`. The docs now call this out loudly, including that the SVG path is
|
|
415
|
-
- **Documented kerf's reserved marker namespace.** Fine-grained bindings and `each()` lists coordinate through in-band markers that the wiring pass finds by scanning the mounted subtree and matching by id. A consumer element that carries one of those names can collide with a real binding's id and silently steal its update, so the names are now documented as reserved: the `data-kfb` / `data-kfbrow` attributes and HTML comments beginning `kfb:` / `kfbr:` / `kf-list:`. Don't emit them from your own markup or via `raw()`. (kerf's escaping already prevents a plain text/attribute
|
|
416
|
-
- **Attribute names are now validated, and inline event handlers are rejected outright.** The JSX runtime already escaped attribute
|
|
3173
|
+
- **Signposted the raw HTML/SVG → DOM bridges as trusted-input only.** `toElement()`, `morph()` (with a string/Element template), and the `<iframe srcdoc>` attribute bypass kerf's escaping/URL-screening by design — they're the same trust model as `innerHTML` / `raw()`. The docs now call this out loudly, including that the SVG path is _more_ dangerous than the HTML path (a top-level `<svg><script>`, SVG event attributes, and `xlink:href="javascript:"` execute once inserted, whereas an HTML-string `<script>` is inert), and that `srcdoc` is HTML a browser re-parses as a document (so `srcdoc={userString}` executes even though the value is escaped as an attribute). No behavior change — these are documentation + regression tests: a real-browser spec (`tests/browser/trusted-html-bridges.spec.ts`) pins the execution boundary across Chromium/Firefox/WebKit, a unit test pins that the granular list fast path keeps the URL screen's guarantee end-to-end, and another pins that SVG input isn't sanitized. See [`docs/7-svg.md`](docs/7-svg.md) § Security, [`docs/8-api-reference.md`](docs/8-api-reference.md), and [`docs/6-jsx-runtime.md`](docs/6-jsx-runtime.md) §6.4.3.
|
|
3174
|
+
- **Documented kerf's reserved marker namespace.** Fine-grained bindings and `each()` lists coordinate through in-band markers that the wiring pass finds by scanning the mounted subtree and matching by id. A consumer element that carries one of those names can collide with a real binding's id and silently steal its update, so the names are now documented as reserved: the `data-kfb` / `data-kfbrow` attributes and HTML comments beginning `kfb:` / `kfbr:` / `kf-list:`. Don't emit them from your own markup or via `raw()`. (kerf's escaping already prevents a plain text/attribute _value_ from forging one — the only ways in are hand-written markup or `raw()`.) See [`docs/2-reactivity.md`](docs/2-reactivity.md) § "Reserved marker names".
|
|
3175
|
+
- **Attribute names are now validated, and inline event handlers are rejected outright.** The JSX runtime already escaped attribute _values_; it now also validates each attribute _name_ against a safe shape (a letter/underscore/colon followed by letters, digits, or `_ . : -`) and **throws** on anything else. This closes a markup-injection vector when an object with attacker-controlled keys is spread into JSX (`<div {...untrustedObj}>`) — previously a key like `'x><img onerror=…>'` broke out of the open tag even though the value was escaped. Separately, any `on*` attribute (a function _or_ a string value, in any case — e.g. `onClick={fn}` or `onclick="…"`) now throws and points at `delegate()`; previously only function-valued keys matching `/^on[A-Z]/` were caught, so a string `onclick="alert(1)"` slipped through and became a live inline handler when parsed. Both checks now cover the **fine-grained bound path** too: a signal bound straight into an attribute (`onclick={signal}`) is written with `setAttribute`, and `setAttribute('onclick', …)` installs a live inline handler just as a parsed string would — so an `on*` (or malformed) name bound as a signal is rejected at binding time, closing the same vector on the signal path. See [`docs/6-jsx-runtime.md`](docs/6-jsx-runtime.md) §6.4.2.
|
|
417
3176
|
- **Hardened the dangerous-URL screen.** The URL-attribute filter (on `href`/`src`/`xlink:href`/`formaction`/`action`) now: (1) sees through control-character and whitespace obfuscation of the scheme — a leading ``, an in-scheme `TAB`/`LF`/`CR` (`java script:`), or a `NUL` before the colon are all normalized away before the scheme is read, matching how a browser resolves the URL, so they can no longer slip a `javascript:` past the screen; (2) treats `data:` by subtype instead of only blocking `data:text/html` — script-executing document types (`data:text/html`, `data:image/svg+xml`, XHTML/XML) are dropped while inert media (raster images, fonts, audio, video, plain text/CSS) still pass, and any unknown subtype fails closed; and (3) also screens the `data` attribute on `<object>` (which loads its target as a document). `raw()` remains the opt-out. Both the static serializer and the fine-grained bound-attribute writer share the screen, so both paths are covered.
|
|
418
|
-
- **Fine-grained signal bindings.** Hand a `Signal`/`computed`
|
|
3177
|
+
- **Fine-grained signal bindings.** Hand a `Signal`/`computed` _itself_ (not its `.value`) into a JSX attribute (`class={someSignal}`) or a text hole (`{someSignal}`) inside a `mount()`, and kerf binds that hole directly to the signal: when the signal changes, only that attribute/text node updates — the render function does **not** re-run and the list reconciler does **not** walk. This is kerf's fine-grained update tier, sitting below the coarse `mount()` effect, for the "external state drives one spot" pattern (a `selectedId` flipping a row's class, a live status attribute, etc.). Works in static content and inside `each()` rows on both the snapshot and `arraySignal` (granular) paths, and row bindings' lifetimes track their row node (a row reorder is free; a removed row's binding is torn down). Opt-in and non-breaking: passing a raw signal into JSX previously threw, so existing apps are unchanged, and any hole that isn't a signal stringifies exactly as before. Bound URL attributes (`href`/`src`/`formaction`/`action`/`xlink:href`) get the same `javascript:`/`vbscript:`/`data:text/html` screening as static attributes (`raw()` opts out). Outside a `mount()` (SSR / `SafeHtml.toString()`) a bound signal snapshots its current value and emits no markers. See [`docs/2-reactivity.md`](docs/2-reactivity.md) §2.9.
|
|
419
3178
|
|
|
420
3179
|
## [0.16.0] - 2026-07-01
|
|
421
3180
|
|
|
422
|
-
|
|
423
|
-
|
|
424
3181
|
- Fixed keyed-list selection breaking after a row was removed: a signal read only inside `each()`'s `cacheKey` (such as a `selectedId` toggling a row's class) no longer drops out of the reactive dependency set, so later changes re-render correctly.
|
|
425
3182
|
- Fixed appending items to a list after clearing it rendering nothing.
|
|
426
3183
|
|
|
@@ -429,115 +3186,90 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
429
3186
|
|
|
430
3187
|
## [0.15.5] - 2026-06-30
|
|
431
3188
|
|
|
432
|
-
|
|
433
|
-
|
|
434
3189
|
- Corrected the advertised bundle size in `llms.txt` to ~11 KB minified + gzipped (including the `@preact/signals-core` runtime dependency; ~12 KB with `arraySignal`), matching the README.
|
|
435
3190
|
|
|
436
3191
|
## [0.15.4] - 2026-06-30
|
|
437
3192
|
|
|
438
|
-
|
|
439
|
-
|
|
440
3193
|
- `llms.txt` is now published at a public docs-site URL and bundled in the npm package, making kerf's AI-assistant documentation index discoverable to llms.txt directories and tooling.
|
|
441
3194
|
|
|
442
3195
|
- The `kerfjs` package now bundles `llms.txt` (the AI-discovery index) at the package root, and the docs site serves it at `https://brianwestphal.github.io/kerf/llms.txt`. Its links are now absolute GitHub URLs so the file is portable across GitHub, the site, and the installed package.
|
|
443
3196
|
|
|
444
3197
|
## [0.15.3] - 2026-06-30
|
|
445
3198
|
|
|
446
|
-
|
|
447
|
-
|
|
448
3199
|
- `npm create kerf-component` with no directory argument now prompts for the target directory (defaulting to `my-kerf-component`, with a `.` hint for the current directory) instead of printing usage text and exiting with an error.
|
|
449
3200
|
|
|
450
3201
|
## [0.15.2] - 2026-06-30
|
|
451
3202
|
|
|
452
|
-
|
|
453
|
-
|
|
454
3203
|
- Added the `create-kerf-component` initializer — scaffold a ready-to-publish kerf component package with `npm create kerf-component@latest <dir>`, no need to reverse-engineer the packaging rules.
|
|
455
3204
|
|
|
456
3205
|
## [0.15.1] - 2026-06-30
|
|
457
3206
|
|
|
458
|
-
|
|
459
|
-
|
|
460
3207
|
- README refresh + eslint rule-count fixes + CHANGELOG hygiene (`cfbaef3`)
|
|
461
3208
|
|
|
462
3209
|
## [0.15.0] - 2026-06-30
|
|
463
3210
|
|
|
464
|
-
|
|
465
|
-
|
|
466
3211
|
- List updates now morph same-identity rows in place instead of recreating their DOM nodes, avoiding full-table relayout on large lists and preserving DOM identity, focus, and IME composition across re-renders.
|
|
467
3212
|
|
|
468
|
-
|
|
469
3213
|
- New guide on incremental migration: kerf can own a single DOM subtree and coexist with React (or any framework), letting you migrate one island at a time.
|
|
470
3214
|
- New guide on building and publishing reusable kerf components as npm packages.
|
|
471
3215
|
- Example app documentation pages now open with an animated SVG preview of the real app in action, plus a gallery on the complete-examples index.
|
|
472
3216
|
|
|
473
3217
|
## [0.14.0] - 2026-05-27
|
|
474
3218
|
|
|
475
|
-
|
|
476
3219
|
- Fix `toElement()` first-paint divergence in WebKit by adopting its result into the live document
|
|
477
3220
|
- Fix `mount()` first render in WebKit by adopting an inert `rootEl` into the live document
|
|
478
3221
|
|
|
479
3222
|
## [0.13.0] - 2026-05-23
|
|
480
3223
|
|
|
481
|
-
|
|
482
3224
|
- Add `KERF_DEV_WARN_DELEGATE_IN_EFFECT` dev warning to catch `delegate()` calls inside reactive effects
|
|
483
3225
|
- New `require-delegate-disposer` ESLint rule flags `delegate()` calls whose disposer is discarded
|
|
484
3226
|
- Document `delegate()` disposer gotchas and canonical cleanup patterns
|
|
485
3227
|
|
|
486
3228
|
## [0.12.1] - 2026-05-22
|
|
487
3229
|
|
|
488
|
-
|
|
489
3230
|
- Add GitHub Sponsors link to README, homepage, and npm `funding` field
|
|
490
3231
|
|
|
491
3232
|
## [0.12.0] - 2026-05-22
|
|
492
3233
|
|
|
493
|
-
|
|
494
3234
|
- `toElement` now returns `Element | DocumentFragment` to support multi-root inputs
|
|
495
3235
|
|
|
496
3236
|
## [0.11.1] - 2026-05-21
|
|
497
3237
|
|
|
498
|
-
|
|
499
3238
|
- `attr()` redesign: typed `AttrSpec<N, V>` exposes `.attrs` with dual overloads for cleaner attribute handling
|
|
500
3239
|
|
|
501
3240
|
## [0.11.0] - 2026-05-21
|
|
502
3241
|
|
|
503
|
-
|
|
504
3242
|
- `attr()` redesigned with `AttrSpec<N,V>` shape, `.attrs` accessor, and dual overloads
|
|
505
3243
|
- Hardened defensive programming across the runtime for safer edge-case handling
|
|
506
3244
|
- Refreshed published performance numbers from a fresh cross-framework benchmark run
|
|
507
3245
|
|
|
508
3246
|
## [0.10.0] - 2026-05-20
|
|
509
3247
|
|
|
510
|
-
|
|
511
3248
|
- Expose `kerfjs/ai/*` subpaths via package `exports` so the bundled skill/cursorrules files are resolvable
|
|
512
3249
|
- Add a defensive fallback in `kerfjs/ai-assistant-configs` ESLint rule so it fires against installed kerfjs versions whose `exports` block subpath resolution
|
|
513
3250
|
|
|
514
3251
|
## [0.9.1] - 2026-05-20
|
|
515
3252
|
|
|
516
|
-
|
|
517
3253
|
- Bundle the kerf-app Claude Code skill and Cursor rules inside the npm package at `ai/skill.md`, `ai/cursorrules`, and `ai/manifest.json`
|
|
518
3254
|
- Add `kerfjs/ai-assistant-configs` rule to `eslint-plugin-kerfjs` (warn in recommended) to flag drift in installed AI assistant configs
|
|
519
3255
|
- `eslint --fix` now replaces only the canonical section above the `KERF-APP-CANONICAL-END` marker, preserving consumer customizations below it
|
|
520
3256
|
|
|
521
3257
|
## [0.9.0] - 2026-05-20
|
|
522
3258
|
|
|
523
|
-
|
|
524
3259
|
- Bundle the kerf-app Claude Code skill and Cursor rules inside the npm package at `ai/skill.md`, `ai/cursorrules`, and `ai/manifest.json`
|
|
525
3260
|
- Add `kerfjs/ai-assistant-configs` rule (warn in recommended) to `eslint-plugin-kerfjs` v0.9.0 to surface AI-config drift on every lint pass
|
|
526
3261
|
- Canonical-file contract (`kerf-skill-version` + `KERF-APP-CANONICAL-END` marker) lets `eslint --fix` refresh the canonical section while preserving consumer customizations below the marker
|
|
527
3262
|
|
|
528
3263
|
## [0.8.2] - 2026-05-19
|
|
529
3264
|
|
|
530
|
-
|
|
531
3265
|
- Package `homepage` fields now point to the published docs site, with prominent links in both READMEs
|
|
532
3266
|
|
|
533
3267
|
## [0.8.1] - 2026-05-19
|
|
534
3268
|
|
|
535
|
-
|
|
536
3269
|
- New `eslint-plugin-kerfjs` with four AST rules enforcing kerf Hard Rules
|
|
537
3270
|
|
|
538
3271
|
## [0.8.0] - 2026-05-18
|
|
539
3272
|
|
|
540
|
-
|
|
541
3273
|
- Add opt-in dev warning `KERF_DEV_WARN_NARROW_SET=1` that fires when `set()` is called with a partial-state object (replace semantics would silently drop missing keys); names the missing keys and points at the `set({ ...get(), ...next })` merge fix
|
|
542
3274
|
- Widen `KerfBaseAttrs.contentEditable` to accept `'plaintext-only'` and add the lowercase `contenteditable` alias
|
|
543
3275
|
- Expand the `/kerf/migrating/` hub to 13 frameworks — adds Vue 3, Svelte 5, Solid, Preact, htmx, Angular, jQuery, Redux, and Astro pages alongside a refreshed 8-framework comparison matrix
|
|
@@ -548,7 +3280,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
548
3280
|
|
|
549
3281
|
## [0.7.0] - 2026-05-18
|
|
550
3282
|
|
|
551
|
-
|
|
552
3283
|
- Granular list updates now preserve DOM identity, focus, scroll, IME state, `<details open>`/`<dialog open>`, and `data-morph-skip` subtrees across in-place row updates
|
|
553
3284
|
- Two new fast paths in the granular reconciler cut krausest select-row by 71% (27.8 → 8.2 ms) and partial-update by 28% (46.8 → 33.8 ms)
|
|
554
3285
|
- `each()`'s third parameter renamed from `key` to `cacheKey` to clarify it's a passive cache-invalidation comparator, not a React-style reconciliation identity; positional callers unaffected
|
|
@@ -564,7 +3295,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
564
3295
|
|
|
565
3296
|
## [0.6.0] - 2026-05-11
|
|
566
3297
|
|
|
567
|
-
|
|
568
3298
|
- Public `morph(liveRoot, template)` export for standalone DOM reconciliation
|
|
569
3299
|
- New `data-morph-preserve` attribute to opt elements out of morphing
|
|
570
3300
|
- New `data-morph-skip-children` attribute — morph host attrs but leave subtree intact
|
|
@@ -573,12 +3303,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
573
3303
|
|
|
574
3304
|
## [0.5.1] - 2026-05-11
|
|
575
3305
|
|
|
576
|
-
|
|
577
3306
|
- Fix `dist/jsx-runtime.d.ts` IntrinsicElements self-shadow that broke JSX typing in consumer apps
|
|
578
3307
|
|
|
579
3308
|
## [0.5.0] - 2026-05-10
|
|
580
3309
|
|
|
581
|
-
|
|
582
3310
|
- Add `arraySignal` (`kerfjs/array-signal` subpath) — granular collection signal that drives O(patches) DOM updates for keyed lists
|
|
583
3311
|
- Faster keyed-list updates: bulk-parse contiguous insert runs and consecutive update patches in the granular reconcile path
|
|
584
3312
|
- Perf optimizations on the `each()` / `mount()` update path; benchmarks now competitive with Solid/Vue on swap/remove/clear
|
|
@@ -592,17 +3320,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
592
3320
|
|
|
593
3321
|
## [0.4.2] - 2026-05-09
|
|
594
3322
|
|
|
595
|
-
|
|
596
3323
|
- No user-facing changes in this release.
|
|
597
3324
|
|
|
598
3325
|
## [0.4.1] - 2026-05-09
|
|
599
3326
|
|
|
600
|
-
|
|
601
3327
|
- Just fixing the build
|
|
602
3328
|
|
|
603
3329
|
## [0.4.0] - 2026-05-09
|
|
604
3330
|
|
|
605
|
-
|
|
606
3331
|
- `delegate()` now auto-promotes the seven well-known non-bubbling events (`focus`, `blur`, `scroll`, `load`, `error`, `mouseenter`, `mouseleave`) to capture phase, with `closest()`-style selector matching preserved
|
|
607
3332
|
- Fixed focus and caret position loss when reordering keyed `each()` rows on engines that drop focus on `insertBefore` (older Safari, happy-dom)
|
|
608
3333
|
- `mount()` now throws a descriptive error when the root element is null/undefined instead of a generic "Cannot set properties of null"
|
|
@@ -613,12 +3338,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
613
3338
|
|
|
614
3339
|
## [0.3.1] - 2026-05-08
|
|
615
3340
|
|
|
616
|
-
|
|
617
3341
|
- Removed stale `morphdom` references; the bundled native diff is now the only reconciler
|
|
618
3342
|
|
|
619
3343
|
## [0.3.0] - 2026-05-08
|
|
620
3344
|
|
|
621
|
-
|
|
622
3345
|
- Rebuilt render pipeline with structured segments and a native keyed-list diff, replacing the morphdom dependency
|
|
623
3346
|
- Added `each()` for keyed list iteration with per-item HTML memoization by object identity
|
|
624
3347
|
- Renamed the npm package from `kerf` to `kerfjs` (the `kerf` name tripped npm's typo-squatting heuristic); the brand, GitHub repo, and Pages URL are unchanged
|
|
@@ -629,24 +3352,20 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
629
3352
|
|
|
630
3353
|
## [0.2.1] - 2026-05-07
|
|
631
3354
|
|
|
632
|
-
|
|
633
3355
|
- Add `Fragment` export to the `kerfjs` barrel for explicit JSX fragment usage
|
|
634
3356
|
|
|
635
3357
|
## [0.2.0] - 2026-05-07
|
|
636
3358
|
|
|
637
|
-
|
|
638
3359
|
- Fix focused contenteditable losing focus/caret during morph
|
|
639
3360
|
- Fix `SafeHtml` identity mismatch across entry points caused by dist bundling
|
|
640
3361
|
- Fix `clearStoreRegistry` no-op in built output, restoring test isolation
|
|
641
3362
|
|
|
642
3363
|
## [0.1.2] - 2026-05-07
|
|
643
3364
|
|
|
644
|
-
|
|
645
3365
|
- No user-facing changes; release tooling fixes only.
|
|
646
3366
|
|
|
647
3367
|
## [0.1.1] - 2026-05-07
|
|
648
3368
|
|
|
649
|
-
|
|
650
3369
|
- This is just a publication script test
|
|
651
3370
|
|
|
652
3371
|
## [0.1.0] - 2026-05-07
|