kerfjs 5.0.0-beta.55 → 5.0.0-beta.57

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -6,6 +6,665 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ - **`ui-public-boundaries` inspects `kerfjs/html` tagged templates.** The
10
+ no-build `html` template writes the same markup as JSX, so its static
11
+ `class` attributes now carry `KUI-L101` (private `kui-*` anatomy) and
12
+ `KUI-L103` (a component's anatomy on your own element):
13
+ ``html`<aside class="kui-pane">` `` is reported with `Pane` to render, and a
14
+ plain ``html`<div class="kui-content-item">` `` with `ContentItem`, while a
15
+ `<ul>` carrier keeps the placeable class. The template's static parts are
16
+ read by the runtime's own rules; a class a hole supplies (`class=${cls}`)
17
+ is unknown and skipped.
18
+
19
+ - **`KUI-L103`'s "render X instead" answer has one implementation.**
20
+ `@kerfjs/ui` ships `ai/component-class-owners.cjs`, the rule that decides
21
+ which cataloged classes are a component's anatomy and which export renders
22
+ each. `eslint-plugin-kerfjs`'s `ui-public-boundaries` loads it beside
23
+ `application-ui-profile-sync.cjs`, and the generated `ai/components/*.md`
24
+ pages use it too, so the lint message and the component reference can no
25
+ longer name different components (a test pins their agreement over the whole
26
+ shipped catalog). A vendored `profileContractPath` directory must now also
27
+ hold `component-class-owners.cjs`; an `@kerfjs/ui` without it reports
28
+ `KUI-L090`.
29
+
30
+ - **A delegated click no longer re-fires on a target the re-render recycled.**
31
+ A delegated handler that writes a signal makes `mount()` re-render
32
+ synchronously, inside the same event dispatch; when the morph recycled the
33
+ clicked element in place into a control with a different `data-action`,
34
+ delegates registered later for the same event matched the recycled element
35
+ and fired too (a `request` click immediately ran `cancel` and undid itself).
36
+ `delegate()`, `delegateCapture()`, and `kerfjs/actions`' `delegateActions()`
37
+ now resolve every delegate's match against the DOM as it stood when the
38
+ event reached the first kerf delegate listener, like per-element listeners:
39
+ a delegate that matched at dispatch start still fires (unless an earlier
40
+ handler removed its element), one that matches only after a re-render does
41
+ not, and `delegateActions()` dispatches by the action value the element
42
+ carried at dispatch start. `mount()` stays synchronous. The
43
+ wrap-it-in-a-distinct-element workaround is no longer needed. Cost: about
44
+ 0.11 KB min+gzip, which moves the advertised core size from ~12 KB to
45
+ ~13 KB.
46
+
47
+ - **Component packages declare `placeableClasses`, and `KUI-L103` covers
48
+ their anatomy classes.** `create-kerf-component`'s `kerf.components.json`
49
+ accepts optional `boundaries.placeableClasses` (a subset of
50
+ `publicClasses`) and `boundaries.rootElement`, validates them, and copies
51
+ them into the generated `component-composition.json`.
52
+ `eslint-plugin-kerfjs`'s `ui-public-boundaries` now checks every class the
53
+ application or third-party catalogs in `.kerf-ui-profile.json` declare, not
54
+ only `kui-*` names: `<div class="acme-meter">` is reported with the
55
+ package's component to render (`Meter`), while its placeable classes stay
56
+ allowed. The scaffold template's README explains the field, and
57
+ `@kerfjs/ui/ai/skill.md` (1.50.0) applies the rule to package entries.
58
+
59
+ - **`eslint-plugin-kerfjs` `ui-public-boundaries` reports a plain
60
+ `<div class="kui-content-item">` as `ContentItem` recreated (`KUI-L103`).**
61
+ The `kui-content-item` classes stay placeable on another carrier (a `<ul>`, a
62
+ `Text`, a control-cluster `<footer>`), but a plain `<div>` carrying them is
63
+ exactly what `ContentItem` renders; the diagnostic now says to render
64
+ `ContentItem`. The `@kerfjs/ui` catalogs gain an additive `rootElement`
65
+ field (selection entry and composition `boundaries.rootElement`) naming the
66
+ element a component renders around its own placeable classes, and
67
+ `check:guidance` derives its repository check from the same fields, so the
68
+ package check and the downstream diagnostic cannot drift. The rule also
69
+ inspects the static class names of a template-literal `class` /
70
+ `className` value.
71
+
72
+ - **`eslint-plugin-kerfjs` `ui-public-boundaries` reports a component's
73
+ anatomy class written onto an application-owned element (`KUI-L103`).**
74
+ `<div class="kui-toolbar">`, `<aside class="kui-pane">`, or
75
+ `<wa-dropdown class="kui-popup-menu">` recreates a component by class and
76
+ inherits (and invites restyling of) its CSS; the diagnostic names the
77
+ component to render instead (`Toolbar`, `Pane`, `PopupMenu`). The
78
+ `@kerfjs/ui` catalogs gain `placeableClasses` (selection entry field and
79
+ composition `boundaries.placeableClasses`) naming the public classes an
80
+ application may place itself — the layout utilities, `kui-app-root`, the
81
+ `kui-content-item` geometry on a non-div carrier, and Web Awesome
82
+ `hide-actions` — and `check:catalog` requires every class-only entry to list
83
+ its classes there. A component's own element (`<Toolbar className="kui-toolbar">`)
84
+ is not reported. The README pane example and `@kerfjs/ui/ai/skill.md`
85
+ (1.47.0) now render `Pane` instead of writing its classes, and each generated
86
+ `ai/components/*.md` page states which classes are placeable and which
87
+ component renders the rest.
88
+
89
+ - **`kerf-ui-analyze` retires `KUI-L005` ("forced component dimension").**
90
+ `KUI-L019` already reports any application rule whose subject is a cataloged
91
+ component, dimensions included, so `.kui-pane { width: 300px }` was reported
92
+ twice (an error and a review finding). L005's only other shape, a public
93
+ class as an ancestor (`.kui-pane .app-sidebar { width: 240px }`), sizes the
94
+ application's own element and is allowed. The analyzer no longer emits it;
95
+ the id stays in the diagnostic registry (now listing it under `retired`) so
96
+ existing profile exceptions that name it still load.
97
+
98
+ - **`KUI-L301` now reports every `Discouraged` Web Awesome element rendered
99
+ directly.** The `@kerfjs/ui` package-default application profile avoids all
100
+ fifteen Discouraged entries (Dropdown/Dropdown Item → `PopupMenu`,
101
+ Select/Option → `Select`, Tab Group/Tab/Tab Panel → `TabBar`, Icon →
102
+ `LucideIcon`, Split Panel → `ResizableRegion`, Button Group →
103
+ `SegmentedControl`, Tree/Tree Item → `List`, and Animated Image, Comparison,
104
+ and Zoomable Frame → `ContentItem`), each with a rationale, and
105
+ `eslint-plugin-kerfjs` `ui-preferences` (and `kerf-ui-doctor`, which runs it)
106
+ now resolves a custom-element JSX tag such as `<wa-dropdown>` to its catalog
107
+ entry, not only an imported component. A declared component's own source
108
+ stays exempt because it is the wrapper that owns the element. A test keeps
109
+ the profile's avoid set equal to the catalog's Discouraged set in both
110
+ directions and lints the UX demo, recipes, and catalog shell clean;
111
+ `@kerfjs/ui/ai/skill.md` (1.46.0) names the diagnostic.
112
+
113
+ - **New (`@kerfjs/ui`): `NavStack` and `TabScaffold` take `chromeDividers`
114
+ (`'scroll' | 'always' | 'none'`), like `Pane`.** `scroll` (the default) keeps
115
+ today's behavior: the stack's top-chrome and bottom-toolbar lines and the
116
+ scaffold's bar line follow the scroll once `wireScrollDividers` is installed.
117
+ `always` draws them permanently without the wiring; `none` never draws them,
118
+ even when wired. The top `Toolbar`'s own `toolbarConfig.dividerSides` is
119
+ unaffected. `SplitView` forwards it to its compact stack through
120
+ `compactStack.chromeDividers`.
121
+
122
+ - **Fix (`@kerfjs/ui`): a `Pane` that is the only content of a `NavStack` view
123
+ or a `TabScaffold` scene fills it.** It used to size to its content, so the
124
+ view scrolled the whole Pane and its header scrolled away. Now the Pane's own
125
+ content scrolls under its pinned header and footer, the Pane's slots take the
126
+ safe-area edges the view or scene still reaches, and scroll dividers key on
127
+ the Pane's content. The rule lives in `pane.css`, as the Workbench-region fill
128
+ does.
129
+
130
+ - **The `@kerfjs/ui` suite-v3 AI-regression protocol test runs in every gate
131
+ again.** `ui/tests/integration/ai-regressions-v3.test.ts` was selected by no
132
+ package script, so neither `npm --prefix ui run check` nor CI executed it. It
133
+ now runs as `test:ai-regressions` inside the package's `npm test`, and a new
134
+ unit guard fails when any file under `ui/tests` is selected by no script
135
+ reachable from `check` or `test:e2e`.
136
+
137
+ - **Importing `@kerfjs/ui/catalog` or `@kerfjs/ui/workbench` now ships every
138
+ stylesheet those composites render.** `catalog`, `workbench`, `nav-stack`,
139
+ `split-view`, `tab-scaffold`, and `collapsible-panel` gain a CSS-aware
140
+ `browser` export condition whose generated wrapper imports the foundation, the
141
+ CSS of each internally rendered component (Pane, Toolbar, ToolbarText, List,
142
+ ListItem, ListInsetText, Row, PopupMenu, …) in dependency order, and the
143
+ composite's own CSS. Apps no longer hand-import `pane.css`, and a Catalog
144
+ sidebar's styling no longer depends on whether the app happens to import
145
+ `ListInsetText` elsewhere. The list is derived from the source import graph
146
+ (now following parent-relative imports), the `import` condition stays
147
+ CSS-free, and the companion `.css` subpaths remain for pipelines without the
148
+ `browser` condition. `@kerfjs/ui/ai/skill.md` is 1.46.0.
149
+
150
+ - **The `@kerfjs/ui` AI guidance now states every maintainer design rule
151
+ explicitly.** An audit of the package's tickets folded the missing rules into
152
+ `@kerfjs/ui/ai/skill.md` (1.45.0), `llms.txt`, and the design docs: prefer a
153
+ Kerf UI component over a Web Awesome component over a raw tag and never render
154
+ a `Discouraged` Web Awesome element directly (only through the Kerf component
155
+ that wraps it); nothing wraps inside a `ToolbarControlGroup` and a
156
+ `ToolbarText` is one ellipsized line unless `wrap`/`maxLines` is set; a
157
+ `Select` or `PopupMenu` trigger drops its focus ring while its popup is open
158
+ and the ring follows the control's shape (never add ring CSS); dividers earn
159
+ their place (`dividerSides=""` over self-spacing content or between a pane's
160
+ header and its list, never padded around); a control sits near where its
161
+ effect appears; and text aligns to text and icons to icons — a border is
162
+ never the alignment anchor. The ToolbarControlGroup catalog entry no longer
163
+ points at a hand-written `wa-dropdown` trigger, and the bundled kerf app
164
+ configs (`ai/skill.md` 1.25.0, `ai/cursorrules` 1.23.0) name the List family
165
+ and carry the "components own their styles; configure, never override" rule.
166
+
167
+ - **Applications and component packages are held to the component-ownership
168
+ rules `@kerfjs/ui` enforces internally: components own their styles and are
169
+ configured, never overridden.** `kerf-ui-analyze` and `kerf-ui-doctor` add
170
+ four error diagnostics (review findings under `--adoption`):
171
+ - `KUI-L019` — a CSS rule whose subject is another package's cataloged
172
+ component: a public class (`.kui-toolbar`, `.acme-meter`), its
173
+ `[data-component]` root, or a Web Awesome tag the package themes
174
+ (`wa-button`). Your own element in a component's context
175
+ (`.kui-toolbar > .my-widget`), keying on its state inside `:has()`, and
176
+ setting its public tokens stay allowed.
177
+ - `KUI-L020` — reading or writing another component's private `--_*`
178
+ variable (`--_kui-list-gap`).
179
+ - `KUI-L021` — overriding a token a typed prop sets (`--kui-list-gap` is
180
+ `<List gap>`); the message quotes the prop's example.
181
+ - `KUI-L022` — a class placed on a component's root (`<Toolbar
182
+ className="header" />`) that the application then styles.
183
+
184
+ Each message names the component's configuration (typed props, public
185
+ tokens, variants) and ends by routing a need with no prop to a component-gap
186
+ report instead of an override. Ownership is per package, from the nearest
187
+ `package.json`, so a component package styling its own components — including
188
+ in a Kerf UI parent's context — passes, and third-party catalogs declared in
189
+ `.kerf-ui-profile.json` get the same protection. `eslint-plugin-kerfjs` adds
190
+ `ui-component-ownership` (in `recommended-ui` and `strict-ui`, `error`),
191
+ reporting `KUI-L020` / `KUI-L021` in style strings, style objects, and
192
+ `style.setProperty()`. The `create-kerf-component` scaffold now ships a
193
+ `counter.css` that styles only Counter (configured by a typed `size` prop and
194
+ a `--kerf-counter-gap` token, with context named after Counter and a
195
+ Toolbar-context rule in its own stylesheet), plus `npm run check:styles`
196
+ (`kerf-ui-analyze`, from a new `@kerfjs/ui` dev dependency) in
197
+ `prepublishOnly`.
198
+
199
+ - **Behavior change (`@kerfjs/ui`): `NavStack` and `TabScaffold` chrome
200
+ dividers follow the scroll.** The `NavStack` bottom toolbar's and the
201
+ `TabScaffold` bar's top borders are no longer permanent. With `wireScrollDividers`
202
+ installed, a `NavStack`'s top chrome draws a line along its bottom edge only
203
+ while the active view's content is scrolled beneath it, its bottom toolbar a
204
+ line along its top edge only while more content lies below, and a
205
+ `TabScaffold` bar a line along its top edge only while the active scene's
206
+ content continues below it — none when the content fits. The line follows
207
+ the active view across a push or pop and the active scene across a tab
208
+ switch, and keys on whichever element actually scrolls: the view or scene
209
+ itself, or, through a sole child with no chrome of its own on that edge, a
210
+ `Pane`'s content or a nested `NavStack`'s active view (a sole `Pane`'s own
211
+ header or footer draws that boundary instead). Each layout draws the line
212
+ from its own stylesheet (an overlaid pseudo-element on the stack's chrome; the
213
+ bottom toolbar and bar keep their 1px top border, transparent until shown),
214
+ so nothing moves; the wiring
215
+ writes only its `data-scroll-overflow` / `data-scroll-divider` state. Without
216
+ the wiring those edges draw no line.
217
+
218
+ - **Behavior change (`@kerfjs/ui`): toolbars no longer draw a divider by
219
+ default; dividers between pinned chrome and scrolling content follow the
220
+ scroll.** `Toolbar`'s `dividerSides` now defaults to `''` (was `'b'`), and
221
+ the `Workbench` (`mainToolbar`, `mainBottomToolbar`, panel toolbars, and the
222
+ `mainHeader` / `mainFooter` lists), `CollapsiblePanel`, and `Catalog`
223
+ toolbars no longer place dividers of their own. Instead a `Pane` draws one
224
+ line under its header and one over its footer, wherever that chrome ends:
225
+ the header's only while content is scrolled beneath it, the footer's only
226
+ while more content lies below, and neither when the content fits. The new
227
+ `@kerfjs/ui/wire-scroll-dividers` subpath's `wireScrollDividers(root, {
228
+ targets? })` reports that state — call it once at the app root and keep its
229
+ disposer (`wireCatalog` installs it for its own root). It also marks each
230
+ side of a `TabBar` strip with tabs scrolled out of view, and lets a
231
+ `Toolbar` or `List` named as chrome in `targets` draw the divider facing an
232
+ app-owned scroller. The wiring only writes the `data-scroll-overflow` /
233
+ `data-scroll-divider` state attributes; each component draws its own line,
234
+ and no state moves the chrome, the content, or a tab. `Pane` gains
235
+ `chromeDividers` (`scroll` default, `always`, `none`; also in `PaneConfig`,
236
+ so `mainPane` and a panel's `pane` forward it) and TabBar the
237
+ `--kui-tab-bar-divider-color` token. To keep a permanent line, set
238
+ `chromeDividers: "always"` on the pane or an explicit `dividerSides`.
239
+
240
+ - **`@kerfjs/ui`: `Pane`, `NavStack`, `SplitView`, and `TabScaffold` fill a
241
+ Workbench region from their own stylesheets.** As a Workbench main area's
242
+ or panel's only child, each sets its own `height: 100%` in that context,
243
+ which the Workbench marks on its regions (`data-workbench-main`, and the new
244
+ `data-workbench-panel-content` on each panel's content); `workbench.css` no
245
+ longer sizes another component's root (it still fills a nested Workbench).
246
+ Rendering is unchanged.
247
+
248
+ - **`@kerfjs/ui`: layouts name the covered-floating-controls context after
249
+ themselves, and no longer write `FloatingToolbar`'s inset token.** Workbench,
250
+ CollapsiblePanel, and ResizableRegion each provide their own private
251
+ `--_kui-<layout>-covered` context in place of the shared
252
+ `--_kui-floating-covered` flag; `FloatingToolbar` and every restore corner
253
+ read all three. A restore corner no longer sets
254
+ `--kui-floating-toolbar-inset: 0px`; `FloatingToolbar` zeroes its own inset
255
+ inside a restore corner (a toolbar's own `inset` still wins). Rendering is
256
+ unchanged. One edge case changes: a floating control inside an open overlay
257
+ of one layout type that is itself covered by another layout type's open
258
+ overlay (for example, a ResizableRegion overlay in a Workbench work area
259
+ under an open rail overlay) now stays hidden.
260
+
261
+ - **`@kerfjs/ui`: `ListHeader`, `NavStack`, `Select`, and `StateBanner` no
262
+ longer restyle a composed child through a class on its root.** Each child
263
+ now styles itself: `Text` sets a `ListHeader` label from `text.css`, `Badge`
264
+ spaces a `Select` count and a `StateBanner` badge from `badge.css`, and a
265
+ `NavStack` title uses `ToolbarText`'s new `fill` option. Rendering is
266
+ unchanged. `ToolbarText` gains `fill` (grow into the free space of its
267
+ toolbar zone and truncate there). The `kui-list-header__label`,
268
+ `kui-nav-stack__title`, `kui-select__count`, and `kui-state-banner__badge`
269
+ classes are removed; select a ListHeader label with
270
+ `.kui-list-header__title > .kui-text` and a NavStack title with
271
+ `.kui-toolbar-text[data-fill="true"]` if a test needs to find them.
272
+
273
+ - **`@kerfjs/ui` ships a markdown component reference for AI assistants.**
274
+ `@kerfjs/ui/ai/components/README.md` indexes every catalog entry in one line,
275
+ and `@kerfjs/ui/ai/components/<id>.md` gives each entry a short page: when to
276
+ use and avoid it, imports and CSS/registration side effects, a link to its
277
+ exact props, legal parents and zones (JSX prop and child count), app- and
278
+ wiring-owned state, geometry, accessibility obligations, its styling boundary
279
+ (configure through props; do not override internals), and its diagnostics.
280
+ `llms.txt` and the shipped skill now send assistants there instead of to the
281
+ 263 KB `component-catalog.json` and 221 KB `component-composition.json`,
282
+ which remain the canonical sources for tools. The pages are generated from
283
+ those two files and a package check fails when they drift.
284
+
285
+ - **Breaking (`@kerfjs/ui` and `create-kerf-component`): the composition
286
+ catalog files are renamed so they no longer read as a "v2" of the component
287
+ catalog.** The composition catalog is a generated composition layer beside
288
+ `component-catalog.json`, not a newer version of it. Update any import,
289
+ `$schema`, `$ref`, or `.kerf-ui-profile.json` catalog location that names an
290
+ old path:
291
+
292
+ | Old path | New path |
293
+ | -------------------------------------------------------------- | --------------------------------------------------------------- |
294
+ | `@kerfjs/ui/ai/component-catalog-v2.json` | `@kerfjs/ui/ai/component-composition.json` |
295
+ | `@kerfjs/ui/ai/component-catalog-v2.schema.json` | `@kerfjs/ui/ai/component-composition.schema.json` |
296
+ | `@kerfjs/ui/ai/component-catalog-v2.d.ts` | `@kerfjs/ui/ai/component-composition.d.ts` |
297
+ | `@kerfjs/ui/ai/component-catalog-v2-overrides.json` | `@kerfjs/ui/ai/component-composition-rules.json` |
298
+ | `@kerfjs/ui/ai/component-catalog-extension-v2.schema.json` | `@kerfjs/ui/ai/component-composition-extension.schema.json` |
299
+ | `@kerfjs/ui/ai/component-catalog-extension-v2.d.ts` | `@kerfjs/ui/ai/component-composition-extension.d.ts` |
300
+ | `@kerfjs/ui/docs/examples/component-catalog-extension-v2.json` | `@kerfjs/ui/docs/examples/component-composition-extension.json` |
301
+ | scaffolded package `component-catalog-v2.json` | scaffolded package `component-composition.json` |
302
+
303
+ The exported types follow: `ComponentCatalogV2` → `ComponentComposition`,
304
+ `CatalogCompositionEntryV2` → `CatalogCompositionEntry`,
305
+ `ConsumerComponentCatalogV2` → `ConsumerComponentComposition`, and
306
+ `ConsumerCatalogEntryV2` → `ConsumerCompositionEntry`. In the repository,
307
+ `npm run catalog:v2:sync` is now `npm run composition:sync`
308
+ (`catalog:sync` still runs both generators). The file contents and their
309
+ `schemaVersion` are unchanged.
310
+
311
+ - **Breaking (`@kerfjs/ui`, `create-kerf-component`, and `kerfjs setup`): the
312
+ composition catalog's own fields no longer read as a "v2" of the component
313
+ catalog.** In `component-composition.json` (and any consumer composition
314
+ catalog), `"schemaVersion": 2` is now `"schemaVersion": 1` — the composition
315
+ format's own first version, independent of `component-catalog.json`'s — and
316
+ `compatibility.v1Catalog` is now `compatibility.componentCatalog`. A
317
+ scaffolded package's `kerf.components.json` renames its `v1Catalog` field to
318
+ `componentCatalog` the same way. Regenerate scaffolded catalogs
319
+ (`npm run catalog:generate`), and change any `.kerf-ui-profile.json` `composition`
320
+ location from `"schemaVersion": 2` to `"schemaVersion": 1`; a catalog still
321
+ carrying the old fields fails validation with a message naming the rename.
322
+ The `ComponentComposition` type follows (`schemaVersion: 1`,
323
+ `compatibility.componentCatalog`).
324
+
325
+ - **`@kerfjs/ui` `NavStack` bars use the standard `Toolbar` padding.** The
326
+ navigation bar dropped its toolbar's block padding to stay 44px tall, so its
327
+ title and controls touched the bar's edges. It now keeps the 8px above and
328
+ below that every other toolbar has (60px plus any top safe-area inset).
329
+
330
+ - **`@kerfjs/ui` multiline list rows break long camelCase names at word
331
+ boundaries.** A string label in a `multiline` `ListItem` or `ListActionRow`
332
+ (such as a Catalog sidebar entry) gets `<wbr>` break opportunities at its
333
+ camelCase and acronym boundaries, so `QuickTicketComposer` wraps as
334
+ `QuickTicket` / `Composer` instead of `QuickTicketComp` / `oser`. Breaking
335
+ mid-word remains the fallback for a single word that is still too wide.
336
+ - **`wbr` is a typed JSX intrinsic element.** The runtime already rendered it
337
+ as a void element; `JSX.IntrinsicElements` now declares it.
338
+
339
+ - **`@kerfjs/ui` `Badge` has a text-free dot, and `TabScaffold` tabs can show
340
+ it.** `Badge` takes `size="dot"`: an 8px solid circle in the badge's tone,
341
+ with no children, appearance, or shape. Because a dot has no text, its props
342
+ require either a `label` (announced as an image, for example
343
+ `label="New activity"`) or `ariaHidden` when the owning component already
344
+ announces it. `TabScaffoldTab` accepts `badge: true` for the iOS tab-bar dot
345
+ (new content without a count), centered on the icon's top-trailing corner;
346
+ its `badgeLabel` is required and joins the tab's accessible name
347
+ ("Settings, Update available"). `BadgeTextProps` and `BadgeDotProps` are
348
+ exported from the root barrel.
349
+
350
+ - **`@kerfjs/ui` `Toolbar` truncates the leading title instead of letting the
351
+ center zone cover it.** When a leading identity, a `center` group, and
352
+ trailing actions no longer fit one row, the center zone keeps its whole
353
+ content width and the leading title ends in an ellipsis. Previously the
354
+ center track could shrink below its group, which then painted over the
355
+ title — a pushed `NavStack` view with a center group did this at 390px. The
356
+ fix holds under every `responsive` policy and both `centerAlign` values, and
357
+ a toolbar that fits lays out exactly as before.
358
+
359
+ - **`@kerfjs/ui` `ValueTableRow` keeps its value readable beside a long
360
+ label.** A long single-word label (such as `ConnectionStateBanner` in a
361
+ 390px-wide table) no longer squeezes the value to 0px wide, where it wrapped
362
+ one character per line while the label ran underneath it. The label and value
363
+ now share the row as two content-sized columns: a short value stays whole on
364
+ one line, the label wraps at its spaces and ends an over-long word in an
365
+ ellipsis before the value, and when both are long they split the row.
366
+
367
+ - **`@kerfjs/ui` pinned chrome can give way in a short pane.** `Pane` takes
368
+ `chromePlacement="auto"`: its header and footer stay pinned while the pane is
369
+ at least 480px (30rem) tall, and below that the whole pane scrolls as one
370
+ column so tall chrome never squeezes the content to nothing.
371
+ `WorkbenchChromePlacement` gains `"auto"` for `mainHeaderPlacement` /
372
+ `mainFooterPlacement`, and `Catalog` now defaults `headerPlacement` /
373
+ `footerPlacement` to `"auto"`. Its entry description is pinned under the
374
+ toolbar, which at 200% text size on a short window had left the preview 0px
375
+ tall.
376
+
377
+ - **`@kerfjs/ui` `TabScaffold` tabs can show a badge.** Give a
378
+ `TabScaffoldTab` a `badge` (a count or short string) to show the iOS tab-bar
379
+ badge — a compact, solid danger `Badge` at the top-trailing corner of the
380
+ tab's icon. The badge is hidden from assistive technology; the tab's
381
+ accessible name becomes `"<label>, <badgeLabel>"` (for example
382
+ "Inbox, 3 unread"), with `badgeLabel` defaulting to the badge text. Tabs
383
+ without a badge are unchanged.
384
+
385
+ - **`@kerfjs/ui` `ListItem` and `ListActionRow` labels stay out of the
386
+ trailing slot.** A long single-word primary label (such as a Catalog sidebar
387
+ entry named `ConnectionStateBanner` in a narrow rail) no longer runs
388
+ underneath trailing status text. Single-line rows now end the primary label
389
+ and description with a real ellipsis (previously the text was cut mid-glyph
390
+ with no "…"), and `multiline` rows break a word wider than the label column
391
+ onto the next line.
392
+
393
+ - **Custom elements keep the `role` and `aria-*` they set on themselves across
394
+ re-renders.** Web components conventionally own their host's ARIA (Web
395
+ Awesome's `wa-option` sets `role="option"` / `aria-selected`, `wa-divider`
396
+ sets `role="separator"`; Shoelace and FAST do the same), but a template never
397
+ mentions them, so the morph and the keyed-list attribute fast path stripped
398
+ them on the next re-render. On any custom element (hyphenated tag), `role`
399
+ and `aria-*` are now element-owned like `open`: never removed just because
400
+ the template omits them. The template can still set or change them, a signal
401
+ binding set to `null` / `false` still removes them, and plain elements are
402
+ unchanged. Trade-off: omitting one from a static template no longer removes
403
+ it from a custom element — render an explicit value (`aria-pressed="false"`)
404
+ instead.
405
+
406
+ - **`@kerfjs/ui` layout regions no longer slow edits to long lists inside
407
+ them.** The rule that clears the safe-area context for the children of a
408
+ Workbench main area or panel, SplitView list or detail, NavStack view,
409
+ TabScaffold scene, or CollapsiblePanel content made each child re-check the
410
+ region in Chromium on every DOM change. A 1000-row list placed directly in
411
+ one of these regions took about 46ms per style recalculation (2ms with the
412
+ list one level down), and now takes about 0.2ms. Layout and insets are
413
+ unchanged.
414
+
415
+ - **`@kerfjs/ui` `CollapsiblePanel` no longer slows insertions into long
416
+ lists.** An expanded inline panel clears its screen edge for its siblings;
417
+ the rules that did so selected the siblings themselves
418
+ (`:has(~ panel)` and `panel ~ *`), which in Chromium made each row inserted
419
+ into or removed from a long list restyle the list's other rows — about
420
+ 14.6ms instead of 0.2ms per insertion beside a 1000-row list. The panel's
421
+ container now carries a flag that a CSS style query reads, so a list pays
422
+ nothing. The edge still clears for the panel's siblings and returns when it
423
+ collapses; the one difference is that a sibling placed before a left panel
424
+ or after a right or bottom panel now loses the edge too.
425
+
426
+ - **`@kerfjs/ui` restore corners and covered floating controls no longer slow
427
+ every style recalculation.** The `CollapsiblePanel` / `ResizableRegion`
428
+ restore-corner anchor scope (`:has(> restore) > * > * > *`) and the static
429
+ side-overlay covered flag (`:has(> open overlay) > :not(overlay)`) put a
430
+ universal selector after a `:has()` container, which in Chromium restyled
431
+ the children of every ancestor of each DOM change — about 85ms instead of
432
+ 1ms per edit beside a 1000-row list. The drawer now publishes its anchor
433
+ only beside a restore control's container (or its work-area column), and
434
+ the covered flag is set on the overlay's container and cleared on its
435
+ overlay children. Restore-corner placement and covered-control hiding are
436
+ unchanged; a new CSS unit test rejects the costly `:has()` shapes.
437
+
438
+ - **`@kerfjs/ui` adds `ContentItem`.** Import it from
439
+ `@kerfjs/ui/content-item` to render one self-contained pane child with the
440
+ shared 8px margin, 1px border, and 8px padding. Framing and shape are typed
441
+ props instead of hand-written modifier classes:
442
+ - `frame="framed"` paints the neutral border in the 1px the item always
443
+ reserves, so its geometry does not change (replacing
444
+ `class="kui-content-item kui-content-item--framed"`);
445
+ - `shape="pill"` selects the 22px radius.
446
+ - `ariaLabel` names a distinct region, and `focusTarget` makes the item a
447
+ programmatic focus target (`tabindex="-1"`), such as a NavStack view's
448
+ `data-nav-focus` target.
449
+
450
+ The `.kui-content-item` classes stay public for elements that must carry the
451
+ geometry themselves; their CSS now lives in `content-item.css`, which
452
+ `layout.css` still imports. There is no filled variant — use `SunkenPanel`
453
+ for a lowered surface. The catalog, demos, and recipes use the component.
454
+
455
+ - **`@kerfjs/ui` icon-only toolbar buttons show the same help tag.** An
456
+ icon-only `<button>` or link inside a `ToolbarControlGroup`, named by
457
+ `aria-label` with no visible text, now shows the help tag icon-only Selects
458
+ and PopupMenus show, with the same 500ms hover delay, keyboard-focus-only
459
+ display, press/Escape dismissal, and quick scanning along a toolbar. The tag
460
+ is `aria-hidden` and never renames the button. A button with a native
461
+ `title`, or with its own Web Awesome tooltip, keeps that and gets no second
462
+ tag. The `select/register` and `popup-menu/register` imports install it; a
463
+ toolbar with icon-only buttons but neither control imports the new
464
+ `@kerfjs/ui/help-tags/register` subpath, which registers only the tooltip.
465
+
466
+ - **`@kerfjs/ui` icon-only Select and PopupMenu triggers show a help tag.**
467
+ An icon-only `Select` (single or `multiple` with `triggerIcon`) and an
468
+ icon-only `PopupMenu` now show a Web Awesome tooltip naming the control on
469
+ hover (after 500ms) and on keyboard focus: a filter reads "Filter by label:
470
+ Bug, Docs", a single Select its name and current choice, a menu its `label`.
471
+ It hides while the popup is open, on a press, and on Escape. The tag repeats
472
+ the accessible name, so it is `aria-hidden` and never joins
473
+ `aria-labelledby`; screen readers still hear the name once. It is installed
474
+ by the existing `select/register` and `popup-menu/register` imports, needs no
475
+ props, and never appears for labeled Selects or text menu triggers.
476
+
477
+ - **`@kerfjs/ui` toolbar groups drop their focus ring while a popup is open.**
478
+ A `ToolbarControlGroup` that owns the ring (`focusRing="outline"` or
479
+ `"halo"`) no longer paints it while a Select listbox or `PopupMenu` inside it
480
+ is open; the open popup shows focus, and the ring returns when the popup
481
+ closes with focus still in the group. This matches the per-control Select
482
+ and PopupMenu triggers.
483
+
484
+ - **`@kerfjs/ui` multiple `Select` can be an icon-only toolbar filter menu.**
485
+ Set `selectedPresentation: 'icon-only'` on a `multiple` Select together with
486
+ a fixed `triggerIcon` (for example a funnel `LucideIcon`); the types require
487
+ `triggerIcon` there and reject it everywhere else. The trigger keeps the
488
+ icon-only caret pill used in a `ToolbarControlGroup`, and:
489
+ - shows the fixed icon whatever is chosen;
490
+ - adds a compact count `Badge` while any choice is chosen;
491
+ - ends the combobox's accessible name with the chosen labels in choice order
492
+ ("Filter by label: Bug, Docs"), leaving the hidden value text empty so the
493
+ list is not announced twice.
494
+
495
+ The popup still stays open while choices toggle.
496
+
497
+ - **`@kerfjs/ui` `Select` options keep their accessibility semantics across
498
+ re-renders.** Re-rendering a Select (for example after its `change` handler
499
+ updates the controlling signal) removed the `role="option"` and
500
+ `aria-selected` Web Awesome sets on each option, so screen readers lost every
501
+ option's role and selected state. They now survive in single and `multiple`
502
+ Selects through the core rule below (custom elements own their `role` /
503
+ `aria-*`), with no Select-specific restore.
504
+
505
+ - **`@kerfjs/ui` `Select` dividers keep their separator role across
506
+ re-renders.** The `wa-divider` a `separatorBefore` choice adds lost the
507
+ `role="separator"` and `aria-orientation` Web Awesome sets on it the first
508
+ time the enclosing mount re-rendered, and never got them back, so assistive
509
+ technology stopped announcing the break between option runs. The Select now
510
+ renders both attributes itself.
511
+
512
+ - **`@kerfjs/ui` `Select` and `PopupMenu` re-renders no longer force Web
513
+ Awesome updates.** Web Awesome reflects its default `size`, `appearance`,
514
+ `placement`, and `orientation` onto `wa-select`, `wa-dropdown`, and
515
+ `wa-divider`; the templates omitted them, so every re-render of the enclosing
516
+ mount stripped them and each host ran a full Lit update to put them back. The
517
+ templates now render those defaults (`wa-select size="m"
518
+ appearance="outlined" placement="bottom"`, `wa-dropdown size="m"`, divider
519
+ `orientation="horizontal"`), so an unchanged re-render leaves the hosts
520
+ alone. No visual change. The `@kerfjs/ui/webawesome` JSX declarations type
521
+ these attributes.
522
+
523
+ - **`@kerfjs/ui` Workbench and CollapsiblePanel panes and chrome lists are
524
+ configurable.** The `Pane`s these layouts compose around app content now
525
+ take the new `PaneConfig` type (`contentElement`, `contentLabel`,
526
+ `separators`, `safeAreaEdges`): a Workbench panel's or `CollapsiblePanel`'s
527
+ `pane`, and the Workbench's `mainPane`. The `List`s holding `mainHeader` /
528
+ `mainFooter` take the new `ListConfig` type (`gap`, `hAlign`, `vAlign`,
529
+ `dividerSides`, `textInsets`, `controlInsets`) through `mainHeaderList` /
530
+ `mainFooterList`. Previously this configuration was hard-coded, so, for
531
+ example, a navigator rail could not render its content as a `nav` landmark
532
+ without restyling. Omitted or `undefined` fields keep today's defaults, and
533
+ the default markup is unchanged.
534
+
535
+ - **`@kerfjs/ui` `SplitView` forwards its `ResizableRegion` and compact
536
+ `NavStack` configuration.** `resizable` now also takes the list region's
537
+ `separator`, `handleIcon`, `contentOverflow`, and collapse options
538
+ (`collapsed`, `transitioning`, `collapseMotion`, `restoreControl`,
539
+ `restorePosition`), so an app can hide the list and float a restore control
540
+ without leaving `SplitView`. The new `compactStack` prop forwards the compact
541
+ stack's `toolbarConfig`, `backIcon`, `backText`, `hideToolbar`, and
542
+ persistent `bottomToolbar`, and gives the list and detail views their own
543
+ toolbar groups (`list` / `detail`: `leading`, `center`, `toolbar`,
544
+ `bottomToolbar`). Omitted options keep today's defaults.
545
+
546
+ - **`@kerfjs/ui` `NavStack` renders its top chrome as a real `Toolbar`.** The
547
+ back control is a borderless `ToolbarControlGroup` and the title a
548
+ `ToolbarText` in the leading zone, and the stack now forwards:
549
+ - `toolbarConfig` — the `Toolbar` configuration (`dividerSides`,
550
+ `centerAlign`, `responsive`, `responsiveAt`, `safeAreaEdges`) plus `label`,
551
+ `titleSize` (default `large`), and `headingLevel`;
552
+ - per-view `leading` and `center` groups beside the existing trailing
553
+ `toolbar`;
554
+ - `backIcon` and visible `backText` for the back control.
555
+
556
+ The defaults keep today's bar: no divider, one 44px band, and the top and
557
+ side safe-area edges claimed. The hand-rolled `kui-nav-stack__lead` and
558
+ `kui-nav-stack__actions` wrappers are gone (their content now sits in the
559
+ toolbar's zones), and `kui-nav-stack__back` / `kui-nav-stack__back-icon`
560
+ are no longer styled by the stack.
561
+
562
+ - **`@kerfjs/ui` configures with props instead of token overrides.** Five
563
+ settings apps previously reached through raw `--kui-*` overrides now have a
564
+ typed prop or modifier; each defaults to today's rendering, and the
565
+ underlying tokens keep working:
566
+ - `FloatingToolbar` `inset` (a `CssLength`, default `--kui-space-m`) moves
567
+ it from the container edges. Omit it inside a restore corner, which owns
568
+ the inset.
569
+ - `DisclosureArrow` `size` (a `CssLength`, default 18px root-scaled).
570
+ - `ListHeader` `headingLevel` (1–6, default 2) sets the label's native
571
+ heading element for passive and trailing-action headers; a toggle header
572
+ has no heading and rejects it at compile time.
573
+ - `TokenSearchField` `clearIcon` replaces the clear action's glyph.
574
+ - `.kui-content-item--framed` paints the standard neutral border on a
575
+ content item that marks a real distinction, with no geometry change.
576
+
577
+ The docs and AI guidance also stop recommending `--kui-edge-inset-*`
578
+ overrides: an app-owned arrangement passes `safeAreaEdges` to its panes and
579
+ edge-claiming toolbars (including the layout toolbar configurations, which
580
+ forward it).
581
+
582
+ - **`@kerfjs/ui` hides a work area's floating controls under an open side
583
+ overlay.** While a side overlay is open — a Workbench rail (static
584
+ `presentation: "overlay"`, or a responsive overlay whose breakpoint applies),
585
+ a `wireSidebar` compact overlay or `presentation="overlay"`
586
+ `CollapsiblePanel`, or a horizontal `presentation="overlay"`
587
+ `ResizableRegion` — the covered work area's `FloatingToolbar`s and the
588
+ layouts' restore corners are now hidden (`visibility: hidden`, so they also
589
+ leave the tab order and the accessibility tree) instead of relying on
590
+ stacking order alone. A phone-width app could otherwise paint the main
591
+ column's floating drawer toggle over its open inspector. They come back as
592
+ the overlay closes; the overlay's own floating controls and a bottom
593
+ drawer's overlay are unaffected.
594
+
595
+ - **`@kerfjs/ui` `Select` supports multiple selection.** Set `multiple: true`
596
+ and pass the chosen values as an array `value`; the props are typed so an
597
+ array value requires `multiple`. Behavior:
598
+ - The popup stays open while the person toggles choices with the pointer,
599
+ Enter, or Space, and closes only on an outside press, Escape, or focus
600
+ leaving.
601
+ - Each chosen option shows a check.
602
+ - The closed control summarizes the chosen labels in choice order as a short
603
+ localized list, or shows `placeholderText` when none are chosen.
604
+ - `change` / `input` report the value array, and a form submits every chosen
605
+ value under `name`.
606
+ - Re-rendering with a different `value` updates the selection even after the
607
+ person has interacted.
608
+
609
+ `renderSelected` and `selectedPresentation="icon-only"` stay single-value
610
+ only.
611
+
612
+ - **`@kerfjs/ui` `PopupMenu` now looks exactly like `Select`.** Both read one
613
+ shared popup contract, so a command menu beside a value list in a toolbar
614
+ draws the same popup. `PopupMenu` changes:
615
+ - its caret turns to face the open menu;
616
+ - its menu fades in without scaling;
617
+ - it takes the Select listbox's 4px inset (also under the default
618
+ `menuInset="standard"`);
619
+ - its rows use the Select option's height, padding, and reserved check
620
+ column, and its icons use the same 16px size and 8px gap;
621
+ - a keyboard-focused item takes the Select's current-option fill instead of
622
+ a focus ring;
623
+ - headings render as the Select's uppercase group titles rather than slotted
624
+ `h3` elements.
625
+
626
+ An open `Select` also no longer paints its trigger focus ring. The open
627
+ listbox shows focus, and the ring returns when it closes.
628
+
629
+ - **`@kerfjs/ui` `Catalog` exposes its sidebar and toolbar configuration.** New
630
+ `sidebar` prop (size, `resizable`, `separator`, `collapseMotion`,
631
+ `presentation`, `responsiveOverlayAt`, `compactOverlay`, and the header
632
+ `toolbar`'s `ToolbarConfig`) forwarded to its Workbench rail, plus
633
+ `mainToolbar` / `footerToolbar` `ToolbarConfig` props. `wireCatalog` takes
634
+ `sidebarSize` / `sidebarStorageKey` to wire a resizable sidebar. Today's
635
+ 288px fixed sidebar and toolbar policies remain the defaults.
636
+
637
+ - **`@kerfjs/ui` composed toolbars are configurable.** `WorkbenchMainToolbar`,
638
+ `WorkbenchMainBottomToolbar`, and the panel `toolbar` of a Workbench panel or
639
+ `CollapsiblePanel` now take the `Toolbar`'s configuration: the new
640
+ `ToolbarConfig` type, covering `dividerSides`, `centerAlign`, `responsive`,
641
+ `responsiveAt`, and `safeAreaEdges`. Previously their dividers were
642
+ hard-coded, so for example dropping the line between a toolbar and its
643
+ content took CSS. Today's dividers remain the defaults. Also:
644
+ - `mainBottomToolbar` and panel toolbars take a `center`;
645
+ - panel toggles take localized `showLabel` / `hideLabel`;
646
+ - a Workbench panel's floating restore controls follow its
647
+ `restorePosition`.
648
+
649
+ - **Breaking (`@kerfjs/ui`): removed the pre-release compatibility surface.**
650
+ - The `DividerSides` type is gone; use `Sides`, from the renamed `sides`
651
+ module.
652
+ - `ListInsetText.horizontalOnly` and its `kui-list-inset-text--horizontal`
653
+ class are gone; pass `sides="rl"` instead.
654
+ - The legacy color tokens are gone, both as `--kui-color-*` properties and
655
+ as `uiColor()` names. Replace them with their canonical tokens:
656
+
657
+ | Removed | Use instead |
658
+ | --------------------------------------------- | ----------------------- |
659
+ | `border` | `neutral-border-normal` |
660
+ | `border-quiet` | `neutral-border-quiet` |
661
+ | `accent` | `brand-fill-normal` |
662
+ | `accent-text` | `brand-on-quiet` |
663
+ | `pop` | `pop-fill-quiet` |
664
+ | `pop-text` | `pop-on-fill` |
665
+ | `success`, `warning`, `danger` | `*-fill-quiet` |
666
+ | `success-text`, `warning-text`, `danger-text` | `*-on-quiet` |
667
+
9
668
  - **New `@kerfjs/ui` `PopupMenu` (`@kerfjs/ui/popup-menu`).** It renders a
10
669
  trigger and typed commands, headings, and dividers over the Web Awesome
11
670
  dropdown. Put it in a `single` `ToolbarControlGroup` with `nestedDropdown`, or