@kerfjs/ui 5.0.0-beta.82 → 5.0.0-beta.84

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.
Files changed (153) hide show
  1. package/README.md +1 -0
  2. package/ai/catalog-authoring.json +1 -3
  3. package/ai/catalog-authoring.schema.json +3 -7
  4. package/ai/component-catalog.json +73 -2
  5. package/ai/component-composition.json +64 -1
  6. package/ai/components/README.md +1 -0
  7. package/ai/components/anchored-surface.md +86 -0
  8. package/ai/components/tab-navigator.md +3 -2
  9. package/ai/public-api-signatures-v1.md +79 -35
  10. package/ai/skill.md +2 -2
  11. package/ai/webawesome-jsx-signatures-v1.md +1 -1
  12. package/dist/anchored-surface.d.ts +30 -0
  13. package/dist/anchored-surface.js +3 -0
  14. package/dist/anchored-surface.js.map +1 -0
  15. package/dist/app-tab.js +1 -1
  16. package/dist/browser/anchored-surface.js +3 -0
  17. package/dist/browser/catalog.js +1 -1
  18. package/dist/catalog-resources.d.ts +2 -2
  19. package/dist/catalog.d.ts +4 -4
  20. package/dist/catalog.js +34 -31
  21. package/dist/catalog.js.map +1 -1
  22. package/dist/{chunk-I4SDIMFA.js → chunk-2YPPNFYT.js} +2 -2
  23. package/dist/{chunk-I4SDIMFA.js.map → chunk-2YPPNFYT.js.map} +1 -1
  24. package/dist/{chunk-VHXHLZ6R.js → chunk-3DXHNLME.js} +3 -3
  25. package/dist/{chunk-VHXHLZ6R.js.map → chunk-3DXHNLME.js.map} +1 -1
  26. package/dist/{chunk-HFQDSSED.js → chunk-3PO7JEK7.js} +4 -4
  27. package/dist/{chunk-HFQDSSED.js.map → chunk-3PO7JEK7.js.map} +1 -1
  28. package/dist/chunk-6IVNMC6R.js +19 -0
  29. package/dist/chunk-6IVNMC6R.js.map +1 -0
  30. package/dist/{chunk-VR7MWD2X.js → chunk-CD5TOO22.js} +4 -13
  31. package/dist/chunk-CD5TOO22.js.map +1 -0
  32. package/dist/{chunk-H7ZJEI3H.js → chunk-DL55BSQX.js} +3 -3
  33. package/dist/{chunk-H7ZJEI3H.js.map → chunk-DL55BSQX.js.map} +1 -1
  34. package/dist/{chunk-VIGVNDXZ.js → chunk-GPCAXYMT.js} +4 -4
  35. package/dist/{chunk-VIGVNDXZ.js.map → chunk-GPCAXYMT.js.map} +1 -1
  36. package/dist/chunk-IOS2NXW7.js +53 -0
  37. package/dist/chunk-IOS2NXW7.js.map +1 -0
  38. package/dist/{chunk-HFLGWMG3.js → chunk-KAJVVN7Q.js} +3 -3
  39. package/dist/{chunk-HFLGWMG3.js.map → chunk-KAJVVN7Q.js.map} +1 -1
  40. package/dist/{chunk-LQKY5S4F.js → chunk-KL3W6FUN.js} +4 -4
  41. package/dist/{chunk-LQKY5S4F.js.map → chunk-KL3W6FUN.js.map} +1 -1
  42. package/dist/{chunk-NVKRZ5DI.js → chunk-LFPW4YC6.js} +4 -4
  43. package/dist/{chunk-NVKRZ5DI.js.map → chunk-LFPW4YC6.js.map} +1 -1
  44. package/dist/{chunk-75I3SNUJ.js → chunk-MN3POJFN.js} +2 -2
  45. package/dist/{chunk-75I3SNUJ.js.map → chunk-MN3POJFN.js.map} +1 -1
  46. package/dist/{chunk-5D3WDMIK.js → chunk-MORVLMDG.js} +4 -16
  47. package/dist/chunk-MORVLMDG.js.map +1 -0
  48. package/dist/{chunk-RRCD3A6G.js → chunk-MTCTX25G.js} +2 -2
  49. package/dist/{chunk-RRCD3A6G.js.map → chunk-MTCTX25G.js.map} +1 -1
  50. package/dist/{chunk-PQQQXJGV.js → chunk-NP3FSLNX.js} +2 -2
  51. package/dist/{chunk-PQQQXJGV.js.map → chunk-NP3FSLNX.js.map} +1 -1
  52. package/dist/{chunk-ARZ4RSQO.js → chunk-PGL6TXFW.js} +3 -3
  53. package/dist/{chunk-ARZ4RSQO.js.map → chunk-PGL6TXFW.js.map} +1 -1
  54. package/dist/{chunk-NVRJ7C3W.js → chunk-QQICWWNX.js} +4 -16
  55. package/dist/chunk-QQICWWNX.js.map +1 -0
  56. package/dist/{chunk-CVKIADIS.js → chunk-SJW5BCEQ.js} +7 -5
  57. package/dist/chunk-SJW5BCEQ.js.map +1 -0
  58. package/dist/{chunk-JZM3G6RT.js → chunk-TTGIHMDR.js} +4 -4
  59. package/dist/{chunk-JZM3G6RT.js.map → chunk-TTGIHMDR.js.map} +1 -1
  60. package/dist/{chunk-LRAIS3YB.js → chunk-U657IHFB.js} +1 -1
  61. package/dist/chunk-U657IHFB.js.map +1 -0
  62. package/dist/{chunk-3SXWV2IR.js → chunk-VLGQIYZ5.js} +2 -2
  63. package/dist/{chunk-3SXWV2IR.js.map → chunk-VLGQIYZ5.js.map} +1 -1
  64. package/dist/{chunk-YLJ6X3AC.js → chunk-XK4E225T.js} +2 -2
  65. package/dist/{chunk-YLJ6X3AC.js.map → chunk-XK4E225T.js.map} +1 -1
  66. package/dist/{chunk-BJLXE56E.js → chunk-XKZPBRU7.js} +4 -4
  67. package/dist/{chunk-BJLXE56E.js.map → chunk-XKZPBRU7.js.map} +1 -1
  68. package/dist/{chunk-62VMNBV2.js → chunk-XNXKIPU5.js} +2 -2
  69. package/dist/{chunk-62VMNBV2.js.map → chunk-XNXKIPU5.js.map} +1 -1
  70. package/dist/{chunk-EFDCALR3.js → chunk-Y43B6DS3.js} +2 -2
  71. package/dist/{chunk-EFDCALR3.js.map → chunk-Y43B6DS3.js.map} +1 -1
  72. package/dist/{chunk-3RE2JMZU.js → chunk-YOKEL75M.js} +2 -2
  73. package/dist/{chunk-3RE2JMZU.js.map → chunk-YOKEL75M.js.map} +1 -1
  74. package/dist/collapsible-panel.js +4 -3
  75. package/dist/collapsible-panel.js.map +1 -1
  76. package/dist/css-values.d.ts +8 -1
  77. package/dist/css-values.js +1 -1
  78. package/dist/disclosure-arrow.js +1 -1
  79. package/dist/empty-state.js +1 -1
  80. package/dist/grid.d.ts +5 -4
  81. package/dist/grid.js +2 -1
  82. package/dist/index.d.ts +2 -0
  83. package/dist/index.js +26 -24
  84. package/dist/list-action-row.js +1 -1
  85. package/dist/list-header.js +1 -1
  86. package/dist/list-item.js +1 -1
  87. package/dist/list.d.ts +5 -4
  88. package/dist/list.js +2 -1
  89. package/dist/loading-spinner.js +1 -1
  90. package/dist/lucide-icon.js +1 -1
  91. package/dist/{nav-stack-B1Qc2e5q.d.ts → nav-stack-BvSKY448.d.ts} +10 -2
  92. package/dist/nav-stack.d.ts +1 -1
  93. package/dist/nav-stack.js +2 -1
  94. package/dist/row.d.ts +5 -4
  95. package/dist/row.js +2 -1
  96. package/dist/select.js +1 -1
  97. package/dist/skeleton.js +1 -1
  98. package/dist/spacer.js +1 -1
  99. package/dist/split-view.d.ts +1 -1
  100. package/dist/split-view.js +2 -1
  101. package/dist/split-view.js.map +1 -1
  102. package/dist/state-banner.js +1 -1
  103. package/dist/styles/anchored-surface.css +15 -0
  104. package/dist/styles/catalog/components/catalog-example.css +1 -2
  105. package/dist/styles/catalog/components/catalog-stage.css +40 -42
  106. package/dist/styles/catalog/components/catalog.css +0 -11
  107. package/dist/styles/components/collections/list/list.css +4 -1
  108. package/dist/styles/components/feedback/anchored-surface/anchored-surface.css +15 -0
  109. package/dist/styles/components/layout/grid/grid.css +4 -1
  110. package/dist/styles/components/layout/pane/pane.css +15 -0
  111. package/dist/styles/components/layout/row/row.css +4 -1
  112. package/dist/styles/components/layout/workbench/workbench.css +17 -0
  113. package/dist/styles/components/navigation/nav-stack/nav-stack.css +21 -5
  114. package/dist/styles/components/navigation/tab-navigator/tab-navigator.css +25 -9
  115. package/dist/styles/grid.css +4 -1
  116. package/dist/styles/list.css +4 -1
  117. package/dist/styles/nav-stack.css +21 -5
  118. package/dist/styles/pane.css +15 -0
  119. package/dist/styles/row.css +4 -1
  120. package/dist/styles/styles.css +1 -0
  121. package/dist/styles/tab-navigator.css +25 -9
  122. package/dist/styles/workbench.css +17 -0
  123. package/dist/tab-navigator.d.ts +10 -3
  124. package/dist/tab-navigator.js +2 -0
  125. package/dist/tab-navigator.js.map +1 -1
  126. package/dist/token-search-field.js +1 -1
  127. package/dist/toolbar-control-group.js +1 -1
  128. package/dist/toolbar-text.js +1 -1
  129. package/dist/{types-C9BubXfz.d.ts → types-Bn-dXLcs.d.ts} +6 -2
  130. package/dist/value-table.js +1 -1
  131. package/dist/wire-catalog.d.ts +1 -9
  132. package/dist/wire-catalog.js +2 -200
  133. package/dist/wire-catalog.js.map +1 -1
  134. package/dist/wire-token-search-fields.js +1 -1
  135. package/dist/workbench.d.ts +1 -1
  136. package/dist/workbench.js +2 -1
  137. package/docs/anchored-surface.md +51 -0
  138. package/docs/catalog.md +38 -82
  139. package/docs/component-contract.md +3 -1
  140. package/docs/component-selection.md +19 -1
  141. package/docs/layout.md +19 -2
  142. package/docs/nav-stack.md +13 -4
  143. package/docs/tab-navigator.md +12 -4
  144. package/docs/type-contracts.md +2 -0
  145. package/docs/ux-demo.md +4 -4
  146. package/docs/workbench.md +2 -0
  147. package/llms.txt +8 -3
  148. package/package.json +7 -1
  149. package/dist/chunk-5D3WDMIK.js.map +0 -1
  150. package/dist/chunk-CVKIADIS.js.map +0 -1
  151. package/dist/chunk-LRAIS3YB.js.map +0 -1
  152. package/dist/chunk-NVRJ7C3W.js.map +0 -1
  153. package/dist/chunk-VR7MWD2X.js.map +0 -1
package/docs/catalog.md CHANGED
@@ -37,7 +37,17 @@ description?, tags?, resources?, related? }] }`. Each entry becomes a sidebar
37
37
  the row's quiet status treatment for decision metadata such as `Discouraged`.
38
38
  - **`content`** — the rendered preview for the active entry. Keep a map of `id →
39
39
  () => SafeHtml` in your app and call `renderers[active]()` in your render.
40
- - **`brand`** — `{ title, subtitle?, logoUrl? }` for the sidebar header.
40
+ - **`backgroundStyle`** — the preview stage backdrop: `checkerboard` (default,
41
+ 8px squares),
42
+ `vertical-stripes` (alternating 8px bands), `layout-guide` (single-pixel
43
+ vertical rules at 15px, 23px, and 32px from the stage edge), `surface`, or `sunken`. The
44
+ patterned choices share the main Pane's sunken surface and reveal transparent
45
+ specimen edges; `surface` paints an
46
+ opaque backdrop. `sunken` leaves the stage
47
+ transparent so the main Pane's semitransparent sunken surface paints only once.
48
+ A controlled app may pass its current choice on each render.
49
+ - **`brand`** — `{ title, subtitle?, logoUrl? }` for the sidebar header. The
50
+ optional logo paints through `ToolbarControlGroup.avatarImage`.
41
51
  - **`secondarySections`** — an optional secondary "ecosystem" group shown below the
42
52
  primary sections with a quieter treatment: `{ label, sections, collapsible?,
43
53
  expanded? }`. When `collapsible`, the label is a disclosure toggle controlling
@@ -74,11 +84,11 @@ chosen component.
74
84
 
75
85
  Classify every entry before rendering it:
76
86
 
77
- | Entry kind | Preview purpose | Geometry overlay |
78
- | --------------- | ------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
79
- | **Component** | Show one public component, its meaningful variants, and adverse states. | `true`; the overlay inspects each selected specimen. |
80
- | **Composition** | Show components commonly used together in one focused container or surface. | `false`; child geometry remains unmarked so the composition can be read as a whole. |
81
- | **Recipe** | Teach the recommended production approach to a specific task, including ownership and application glue. | `false`; child geometry remains unmarked so the complete approach can be read. |
87
+ | Entry kind | Preview purpose |
88
+ | --------------- | ------------------------------------------------------------------------------------------------------- |
89
+ | **Component** | Show one public component, its meaningful variants, and adverse states. |
90
+ | **Composition** | Show components commonly used together in one focused container or surface. |
91
+ | **Recipe** | Teach the recommended production approach to a specific task, including ownership and application glue. |
82
92
 
83
93
  These three kinds are exhaustive. Theme/foundation showcases use a functional
84
94
  category, third-party specimens use source metadata, and state galleries retain
@@ -94,10 +104,6 @@ field exists. Show a `Composition` tag rather than repeating the word in a
94
104
  composition's display name; the generic `Catalog` accepts that through the
95
105
  entry's `tags` array.
96
106
 
97
- Keep `geometryOverlay` present and compute it from the active entry. Do not make
98
- it a permanent catalog-wide `true`, and do not give individual specimens their
99
- own overlay implementation.
100
-
101
107
  ### Required structure
102
108
 
103
109
  - `Catalog` is the one shell. The app owns active-entry state and passes one
@@ -149,29 +155,17 @@ const buttonPreview = (
149
155
  <CatalogExample label="Control" align="inline-control">
150
156
  <SegmentedControl id="view" label="View" value="list" choices={choices} />
151
157
  </CatalogExample>
152
- <CatalogExample
153
- label="Authoring note"
154
- note="Explanatory chrome is not a specimen."
155
- rootAttributes={{ "data-catalog-geometry-overlay-skip": "" }}
156
- >
158
+ <CatalogExample label="Authoring note" note="Explain the specimen below.">
157
159
  <p>Use the public helper contract.</p>
158
160
  </CatalogExample>
159
161
  </CatalogExampleStack>
160
162
  );
161
163
  ```
162
164
 
163
- The overlay selects every immediate child of a `CatalogExample`; the helper
164
- marks its generated label and note so they are excluded automatically. It does
165
- not recursively promote a nested child to be the specimen. Outside an example
166
- row, it selects only top-level
167
- `[data-component]` roots in the canvas and ignores nested component descendants.
168
- These rules keep a row's label/group scaffolding out of the measurement and make
169
- the authored nesting determine exactly what is inspected.
170
-
171
165
  ### Metadata ownership
172
166
 
173
- Use `rootAttributes` on either helper for authoring metadata such as `data-demo`
174
- or `data-catalog-geometry-overlay-skip`; the metadata lands on that helper's
167
+ Use `rootAttributes` on either helper for authoring metadata such as `data-demo`;
168
+ the metadata lands on that helper's
175
169
  rendered root. The slot accepts only `data-*` strings. Structural
176
170
  `data-catalog-example`, `data-catalog-example-stack`, and `data-align` semantics
177
171
  remain helper-owned and are rejected case-insensitively at runtime, including
@@ -182,50 +176,21 @@ The app owns entry ids, `kind`, routing, sources, relationships, and test hooks.
182
176
  `Catalog.stageRootAttributes` places app-owned `data-*` state on the
183
177
  catalog-owned preview stage without an extra wrapper; `data-catalog-stage`
184
178
  remains protected.
179
+ The stage is the main Pane's direct scrolling content, owns an 8px inset, and
180
+ grows with tall specimens. The main Pane keeps its sunken appearance without a
181
+ deep inset.
185
182
  The helpers own their structural markers, label/note anatomy, alignment marker,
186
183
  and private classes. Component metadata such as margin/border/padding ownership
187
- lives in `component-catalog.json`; do not infer or overwrite it from overlay
188
- pixels.
189
-
190
- ### Geometry overlay and legend
184
+ lives in `component-catalog.json`.
191
185
 
192
- Pass the conditional `geometryOverlay` boolean to `Catalog`, then call
193
- `wireCatalogGeometryOverlay(root)` once after the first render and retain its
194
- disposer alongside `wireCatalog`'s.
186
+ ### Component metadata
195
187
 
196
- | Overlay mark | Meaning | It is not |
197
- | ----------------------- | ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
198
- | Orange translucent band | A positive computed margin on the selected specimen, after subtracting the helper's alignment inset. | Padding, gap, or empty content. Zero and negative margins are not drawn. |
199
- | Accent solid edge | The selected specimen's computed border widths and radius, except `none`/`hidden` styles. | A focus ring, outline, child border, or metadata ownership claim. |
200
- | Quiet dashed outline | The border-box bound of a transparent selected specimen with no visible border. | A real CSS border, padding edge, or proof that the specimen owns size. |
201
-
202
- The overlay recomputes from live rendered styles when specimens resize, preview
203
- markup changes, theme/root attributes change, or loaded/inline stylesheets
204
- change. Component CSS is therefore the source of truth; do not duplicate margin
205
- or border values in demo data. The overlay does not visualize padding, gaps,
206
- negative/zero margins, scroll overflow, hit targets, or nested descendants.
207
- Inspect computed styles and the machine-readable `geometry` metadata for those
208
- facts. Opaque borderless specimens do not receive the dashed bound.
209
-
210
- ```tsx
211
- <Catalog
212
- {...props}
213
- geometryOverlay={activeEntry.kind === "component"}
214
- content={renderers[active.value]()}
215
- />
216
- ```
188
+ Machine-readable geometry ownership metadata identifies whether the component,
189
+ its parent, or its children are responsible for margin, border, and padding.
190
+ Keep that metadata in `component-catalog.json` and use the rendered specimen to
191
+ review the actual appearance.
217
192
 
218
- Put `rootAttributes={{ "data-catalog-geometry-overlay-skip": "" }}` on a
219
- `CatalogExample` or `CatalogExampleStack` only when that whole subtree is
220
- explanatory chrome rather than a specimen. The marker excludes the marked root
221
- and every descendant from selection; it does not merely hide one band. It is
222
- normally unnecessary in a composition because the active entry already sets the
223
- global overlay to `false`.
224
-
225
- Use the overlay together with machine-readable geometry ownership metadata; the
226
- overlay verifies what is rendered, while metadata tells people and AI tools
227
- whether the component, its parent, or its children are responsible for margin,
228
- border, and padding. Catalogs for downstream components should conform to the
193
+ Catalogs for downstream components should conform to the
229
194
  [`component-catalog-extension.schema.json`](../ai/component-catalog-extension.schema.json)
230
195
  contract and can start from the checked
231
196
  [`component-catalog-extension.json`](./examples/component-catalog-extension.json)
@@ -275,8 +240,8 @@ facts that source can prove without guessing at rendered intent:
275
240
 
276
241
  - focused component routes import and use `CatalogExampleStack` and
277
242
  `CatalogExample` from the public package;
278
- - focused route metadata uses the helpers' `rootAttributes` slot, example rows
279
- are not empty, and composition routes do not add redundant skip markers;
243
+ - focused route metadata uses the helpers' `rootAttributes` slot and example rows
244
+ are not empty;
280
245
  - every `@kerfjs/ui` import is a published package export and relative imports
281
246
  do not reach into `ui/src`;
282
247
  - focused demos have no relative stylesheet imports, inline style attributes,
@@ -286,20 +251,19 @@ facts that source can prove without guessing at rendered intent:
286
251
  analyzer) import no stylesheet other than public `@kerfjs/ui` CSS, set no
287
252
  inline style, and name only published layout and Web Awesome classes; each
288
253
  recipe declares its catalog frame through an exported `presentation`; and
289
- - the shell derives both geometry-overlay enablement and documented demo mode
290
- from the active entry's source and kind.
254
+ - the shell derives the documented demo mode from the active entry's kind.
291
255
 
292
256
  The gate deliberately does not infer component ownership from arbitrary class
293
- names, margins, borders, or nested descendants. Runtime selection and geometry
294
- remain the browser suite's job.
257
+ names, margins, borders, or nested descendants. Runtime appearance remains the
258
+ browser suite's job.
295
259
 
296
260
  A focused route may bypass the two public layout helpers only when the route's
297
261
  stage geometry is itself the reviewed specimen. Add the narrow waiver to
298
262
  [`catalog-conformance-exceptions.json`](../ux-demo/catalog-conformance-exceptions.json)
299
263
  with the exact route, source file, stable diagnostic ids, a substantive reason,
300
264
  and the reviewing `KF-*` ticket. Only helper/metadata rules are waivable;
301
- private imports, local styling, private markup, empty examples, composition overlay drift, and
302
- shell-mode drift always fail. The gate rejects duplicate, malformed, unused,
265
+ private imports, local styling, private markup, empty examples, and shell-mode
266
+ drift always fail. The gate rejects duplicate, malformed, unused,
303
267
  and stale exceptions, so delete a waiver when its route adopts the standard
304
268
  helpers.
305
269
 
@@ -330,7 +294,6 @@ import {
330
294
  import {
331
295
  revealCatalogEntry,
332
296
  wireCatalog,
333
- wireCatalogGeometryOverlay,
334
297
  } from "@kerfjs/ui/wire-catalog";
335
298
  // A CSS-aware (browser-condition) bundler loads the Catalog's CSS with its import.
336
299
 
@@ -375,11 +338,7 @@ const renderers: Record<string, () => SafeHtml> = {
375
338
  <CatalogExample label="Default" align="inline-control">
376
339
  <Button label="Save" />
377
340
  </CatalogExample>
378
- <CatalogExample
379
- label="Authoring note"
380
- note="This explanatory row is deliberately excluded from inspection."
381
- rootAttributes={{ "data-catalog-geometry-overlay-skip": "" }}
382
- >
341
+ <CatalogExample label="Authoring note" note="Explain the specimen below.">
383
342
  <p>The application owns product copy and actions.</p>
384
343
  </CatalogExample>
385
344
  </CatalogExampleStack>
@@ -402,7 +361,6 @@ const initial =
402
361
  const active = signal(initial);
403
362
  const collapsed = signal(false);
404
363
  const theme = signal<"light" | "dark">("light");
405
- const activeEntry = () => entries.find(({ id }) => id === active.value) ?? entries[0];
406
364
 
407
365
  const app = document.getElementById("app")!;
408
366
  mount(app, () => (
@@ -413,7 +371,6 @@ mount(app, () => (
413
371
  content={renderers[active.value]?.() ?? <></>}
414
372
  collapsed={collapsed.value}
415
373
  theme={theme.value}
416
- geometryOverlay={activeEntry().kind === "component"}
417
374
  />
418
375
  ));
419
376
 
@@ -431,7 +388,6 @@ wireCatalog(app, {
431
388
  urlParam: "c", // mirror the active id into ?c=<id>
432
389
  revealSelection: true, // reveal long desktop sidebars without moving focus
433
390
  });
434
- wireCatalogGeometryOverlay(app);
435
391
 
436
392
  // Optional for an initial deep link whose row may start outside the viewport.
437
393
  revealCatalogEntry(app, initial, { block: "center" });
@@ -484,7 +440,7 @@ matching options on `wireCatalog`) if they collide with your own action table.
484
440
 
485
441
  `Catalog` composes public components (`Workbench`, `Pane`, `Toolbar`, `List`,
486
442
  `ListHeader`, `ListItem`, `ListInsetText`, `Text`, …) and owns only the preview stage
487
- (the checkerboard and centered canvas), the geometry overlay, the example viewport
443
+ (the checkerboard and left-aligned canvas), the example viewport
488
444
  options, and the brand mark's size. A bundler that honors the `browser` export
489
445
  condition loads the Catalog's stylesheets and those of every component it renders
490
446
  internally with the `@kerfjs/ui/catalog` import alone — no dependence on which other
@@ -502,7 +502,9 @@ spacing shorthand such as `gap="2xs"`, or pass a `CssLength` created by the
502
502
  CSS-free `@kerfjs/ui/css-values` helpers: `space('m')`, `rem(0.25)`,
503
503
  `lengthVar('--app-gap')`, or `calc(plus(rem(0.25), pct(10)))`. `plus` returns a
504
504
  non-standalone `CssLengthExpression`; only `calc` turns it into a complete
505
- length. `remify(18)` is the runtime twin of source CSS's `remify(18px)`: it
505
+ length. `gap={{ column: 'none', row: 'm' }}` sets the two CSS gap axes
506
+ independently; each axis keeps the same typed spacing contract. `remify(18)` is
507
+ the runtime twin of source CSS's `remify(18px)`: it
506
508
  takes a pixel count and returns a `CssLength` in rem (`1.125rem`) against the
507
509
  same fixed 16px baseline. Raw CSS strings, and the source-only `remify(4px)`
508
510
  string form, are not valid runtime props. The `CssLength` name intentionally includes percentages for UI dimension
@@ -61,6 +61,8 @@ Use `Row`, `Grid`, or `List` spacing shorthands for repeated sibling spacing: `g
61
61
  `gap="m"`, and the other `UiSpaceName` values resolve to their matching
62
62
  `--kui-space-*` token. Use `Spacer` for one deliberate empty width or height, or
63
63
  pass `flex` to consume the remaining space along a flex parent's main axis.
64
+ Pass `gap={{ column: 'none', row: 'm' }}` when the axes need different values;
65
+ each axis accepts the same token or complete typed length as the scalar form.
64
66
  The canonical rhythm is `none`, `2xs`, `xs`, `m`, and `l`; use `s` and `xl`
65
67
  only as deliberate exceptions.
66
68
 
@@ -78,6 +80,7 @@ import { Row } from "@kerfjs/ui/row";
78
80
 
79
81
  <List gap="xs">...</List>;
80
82
  <Grid columns={3} gap="m">...</Grid>;
83
+ <Grid columns={3} gap={{ column: 'none', row: 'm' }}>...</Grid>;
81
84
  <Row hAlign="full" vAlign="middle" gap="m">...</Row>;
82
85
  <List gap={space("m")}>...</List>;
83
86
  <List gap={calc(plus(rem(0.25), pct(10)))}>...</List>;
@@ -130,11 +133,17 @@ copyable reference compositions, not new monolithic components.
130
133
 
131
134
  When presenting a component or recipe in a Catalog, use the authoritative
132
135
  [Catalog demo authoring contract](./catalog.md#catalog-demo-authoring-contract)
133
- rather than deriving preview structure or geometry-overlay behavior from these
136
+ rather than deriving preview structure from these
134
137
  selection entries.
135
138
 
136
139
  ## Missing recurring concepts
137
140
 
141
+ For arbitrary interactive help or popup content beside a control, use
142
+ [`openAnchoredSurface`](anchored-surface.md) with an accessible label; use
143
+ `openAnchoredSurfaceAt` for pointer coordinates. It owns the Kerf UI surface,
144
+ placement, focus, dismissal, and modal-dialog hosting while the app owns its
145
+ content and action policy. Use `PopupMenu` for a typed command list.
146
+
138
147
  Kerf UI does not export a command-palette component. Do not invent a package
139
148
  command-palette import.
140
149
  The typed [application adapter example](./examples/command-palette-adapter.tsx)
@@ -558,6 +567,15 @@ menu. Call `closePopupMenu` for programmatic dismissal. The pointer anchor
558
567
  retains Web Awesome's collision placement, focus management, submenu keyboard
559
568
  navigation, and outside/Escape dismissal.
560
569
 
570
+ The dropdown's popup flips and shifts at viewport edges, limits its height to
571
+ the available space, and uses the browser top layer where supported. The app
572
+ supplies pointer coordinates and command policy; it does not need to calculate
573
+ menu offsets. For other anchored content, use Web Awesome `wa-tooltip` or
574
+ `wa-popover` when its interaction fits, and `wa-popup` when only placement is
575
+ needed. `kerfjs/overlay` provides a structural popover or tooltip with its own
576
+ lifecycle for applications that do not use Web Awesome. These are distinct
577
+ interaction contracts: a command menu still uses `PopupMenu`.
578
+
561
579
  ## Configuring recurring list rows
562
580
 
563
581
  Configure the List family instead of selecting its descendant classes. Use
package/docs/layout.md CHANGED
@@ -44,6 +44,10 @@ inline edge then sits 16px from it. The default is `false`. This extra padding
44
44
  adds to any device safe-area inset; a header or footer still owns the safe-area
45
45
  padding at its edge while the content keeps its extra 8px. The pane's header,
46
46
  footer, background, and separators still reach their usual edges.
47
+ For opaque edge-to-edge content, set `--kui-pane-scrollbar-gutter: stable` on the
48
+ Pane or an ancestor. This reserves a vertical scrollbar gutter so a child that
49
+ bleeds through the inline padding stops before the scroll indicator. The default
50
+ is `auto`; the optional gutter is outside the published content inset values.
47
51
 
48
52
  ```tsx
49
53
  <Pane deepInset>
@@ -65,6 +69,8 @@ draws the standard focus ring. Set `outlined` to keep that ring visible as an
65
69
  application-controlled state, such as a drop target, whether or not the Pane is
66
70
  focused. `PaneConfig` forwards both options through layouts that render a Pane.
67
71
  Give a focusable Pane a useful `label` when it represents a distinct region.
72
+ The ring paints above edge-to-edge child surfaces without intercepting their
73
+ buttons, selects, or nested scrolling.
68
74
 
69
75
  ### Lowered work surfaces
70
76
 
@@ -376,8 +382,12 @@ The two relationships that get confused most are 8px vs 24px: 8px is _inside_ a
376
382
  group, 24px is _between_ major, differing regions. 16px sits between them for
377
383
  same-kind groups.
378
384
 
379
- For `Row.gap`, `Grid.gap`, and `List.gap`, pass these token names directly (`gap="xs"`, `gap="m"`) or use
380
- `space('xs')` from `@kerfjs/ui/css-values` when composing a value in JavaScript.
385
+ For `Row.gap`, `Grid.gap`, and `List.gap`, pass one token name (`gap="xs"`,
386
+ `gap="m"`) to use it on both axes, or pass
387
+ `gap={{ column: 'none', row: 'm' }}` to set each axis independently. Each axis
388
+ also accepts a complete typed `CssLength`, including `space('xs')` from
389
+ `@kerfjs/ui/css-values`. A List is a single vertical column, so its row gap
390
+ separates items; its column gap is exposed for the same CSS axis contract.
381
391
  Use `rem`, `em`, `px`, `pct`, `lengthVar`, and `calc(plus(...))` only when a
382
392
  named spacing relationship does not express the requirement; do not pass raw
383
393
  CSS strings.
@@ -523,6 +533,13 @@ import { px } from "@kerfjs/ui/css-values";
523
533
  <label>Unit <input /></label>
524
534
  </Grid>;
525
535
 
536
+ <Grid columns={2} gap={{ column: 'none', row: 'm' }}>
537
+ <DocumentTile />
538
+ <DocumentTile />
539
+ <DocumentTile />
540
+ <DocumentTile />
541
+ </Grid>;
542
+
526
543
  <Grid minColumnWidth={px(376)} gap="m">
527
544
  <label>Provider <input /></label>
528
545
  <label>Endpoint <input /></label>
package/docs/nav-stack.md CHANGED
@@ -138,9 +138,17 @@ When a remembered control was removed during the controlled rerender, the same
138
138
  fallback order applies. Disposing the helper stops future transitions and
139
139
  cleans up any temporary fallback `tabindex` it added.
140
140
 
141
- ## Scroll dividers
141
+ ## Separators and scroll dividers
142
142
 
143
- The stack draws no permanent line between its chrome and the view. With
143
+ `separator="auto"` (the default) draws a line under the top chrome and above
144
+ the optional bottom toolbar when the active view's Pane has
145
+ `appearance="sunken"`, even if its content fits and no scroll wiring runs. The
146
+ line follows the active view on push and pop. Set `separator="hidden"` to hide
147
+ both stack-owned edges, including lines requested by `chromeDividers`. The
148
+ existing 1px edges change color without moving the content. A view's own Pane
149
+ header and footer separators remain controlled by that Pane.
150
+
151
+ For a flat active view, the stack draws no permanent line. With
144
152
  `wireScrollDividers(appRoot)` installed (see
145
153
  [Layout › Scroll dividers](layout.md#scroll-dividers)), the top chrome shows a
146
154
  line along its bottom edge only while the active view's content is scrolled
@@ -153,10 +161,11 @@ the line keys on whichever actually scrolls — the view, or the Pane's content
153
161
  and a Pane header or footer draws its own boundary instead. An explicit
154
162
  `toolbarConfig.dividerSides` still draws a permanent Toolbar edge.
155
163
 
156
- `chromeDividers` configures both lines, with the same values as a `Pane`'s:
164
+ `chromeDividers` configures both lines for flat views, with the same values as a `Pane`'s:
157
165
  `scroll` (the default) follows the scroll state above and draws nothing until
158
166
  the wiring runs; `always` keeps both lines, with or without the wiring; `none`
159
- drops them even when the wiring reports scroll state. It does not change the
167
+ drops them even when the wiring reports scroll state. A sunken active Pane still
168
+ shows the separator unless `separator="hidden"`. This option does not change the
160
169
  top Toolbar's own `dividerSides`.
161
170
 
162
171
  ```tsx
@@ -144,9 +144,16 @@ paint the lowered surface behind short or long content. For a scene whose
144
144
  only child is a `Pane` or `NavStack`, set appearance on that child's scroll
145
145
  owner. See [Lowered work surfaces](layout.md#lowered-work-surfaces).
146
146
 
147
- ## Scroll divider
147
+ ## Bar separator and scroll divider
148
148
 
149
- The bar has no permanent top border. With `wireScrollDividers(appRoot)`
149
+ `separator="auto"` (the default) draws the bar's top edge when the active scene
150
+ has `appearance: "sunken"`. It also follows the sunken appearance of a sole
151
+ `Pane`, or the active `Pane` view of a sole `NavStack`, inside that scene. The
152
+ edge stays visible when the content fits and as the active tab or nested view
153
+ changes. `separator="hidden"` suppresses the top line, including any scroll
154
+ divider or `chromeDividers="always"` line.
155
+
156
+ For a flat active scene, the bar has no permanent top line. With `wireScrollDividers(appRoot)`
150
157
  installed (see [Layout › Scroll dividers](layout.md#scroll-dividers)), it shows
151
158
  a line along its top edge only while the active scene's content continues below
152
159
  it — not at the scroll end and not when the content fits. The line keys on
@@ -156,10 +163,11 @@ own bottom toolbar sits between them, so that toolbar's line applies instead.
156
163
  The line is the bar's always-present 1px top border, colored only while shown,
157
164
  so it never moves the bar or a tab.
158
165
 
159
- `chromeDividers` configures it, with the same values as a `Pane`'s: `scroll`
166
+ `chromeDividers` configures the scroll divider, with the same values as a `Pane`'s: `scroll`
160
167
  (the default) follows the scroll state and draws nothing until the wiring runs;
161
168
  `always` keeps the line, with or without the wiring; `none` drops it even when
162
- the wiring reports content below.
169
+ the wiring reports content below. It does not suppress the sunken-surface
170
+ separator; use `separator="hidden"` for that.
163
171
 
164
172
  ## Tab badges
165
173
 
@@ -121,6 +121,8 @@ move errors into casts without making the integration safer.
121
121
  - Replace raw `List.gap` strings with a direct spacing shorthand (`"xs"`,
122
122
  `"m"`) or a complete value from `@kerfjs/ui/css-values`. Replace
123
123
  `gap="0.25rem"` with `gap={rem(0.25)}` and wrap sums with `calc(plus(...))`.
124
+ `Row`, `Grid`, and `List` also accept `gap={{ column: 'none', row: 'm' }}`;
125
+ each axis uses the same typed value contract.
124
126
  - Replace raw `List.flex` shorthands with `flex(grow, shrink, basis)` or a finite
125
127
  keyword; replace raw Skeleton dimensions with `px`/`rem`/`em`/`pct` builders;
126
128
  and replace raw choice colors with a foreground token such as
package/docs/ux-demo.md CHANGED
@@ -22,20 +22,20 @@ deterministic state, and semantic layout owners from the [recipe guide](./recipe
22
22
 
23
23
  - It imports public component subpaths, exercising their browser-selected reachable CSS rather than a catalog-only style path.
24
24
  - The `ListActionRow` route demonstrates sibling primary/trailing native controls, independent disabled states, controlled selection, first-line leading-icon alignment for wrapped labels, and an application-owned native popover relationship without nesting controls. The `ListItem` route demonstrates the same multiline leading-icon anchor alongside its single-control navigation contract.
25
- - The `PopupMenu` route's context example opens one app-owned menu either at the row's right-click point or below its adjacent, keyboard-operable Demand actions button. Its interactive row/button composition opts out of the catalog geometry overlay so the overlay's border and margin highlights do not masquerade as a row inset during activation. The Nested decisions example shows the automatic phone-width submenu placement above or below its parent list, with native ArrowLeft return, Escape dismissal, and child selection.
25
+ - The `PopupMenu` route's context example opens one app-owned menu either at the row's right-click point or below its adjacent, keyboard-operable Demand actions button. The Nested decisions example shows the automatic phone-width submenu placement above or below its parent list, with native ArrowLeft return, Escape dismissal, and child selection.
26
26
  - The `AppTab` and `TabBar` routes are focused component specimens: AppTab shows its selected, compact, segmented, icon-only, truncation, placeholder, pending, metadata, and close-icon axes inside the required tablist context; TabBar shows its rail, segmented, inspector, allocation, placement, and accessory-zone axes, including an adjacent action plus a separately pinned end action whose tabs overflow at narrow widths, without application readouts. The pinned leading-tab specimen uses native strip scrolling so momentum and the final tab's end position remain natural; `snapTabs` stays an opt-in for applications that want whole-tab settling. The separate `Application tabs` composition route demonstrates controlled selection, matching panel content, overflow, adding (a new tab opens `pending` with a spinner and stays selectable while its panel shows a `Skeleton` heading until a simulated load completes), closing, keyboard/pointer reordering, and application-owned ordering. The `ResizableRegion` route likewise replaces only the decorative handle glyph while retaining separator semantics and shared wiring.
27
27
  - Every first-party visual component and every free Web Awesome 3.12 component has its own stable `?component=<id>` URL and focused demo. Composite header, menu, and feedback scenarios remain addressable alongside their subcomponents; the root opens the first Kerf component. The former aggregate Web Awesome theme specimen is intentionally omitted because its component families are covered by those focused routes.
28
28
  - `ai/component-catalog.json` is the canonical catalog metadata. A deterministic checked-in projection supplies the demo's unique id, category, kind (`component`, `composition`, or `recipe`), source (`kerf` or `webawesome`), name, description, recommendation, direct `uses` dependencies, existing documentation path, and demo-source path; first-party, recipe, and Web Awesome renderer maps are exhaustively typed against its generated id unions. Functional groups follow their authored importance order; components precede compositions inside each group, equal-importance entries use alphabetical order, and the Recipes group comes last. Composition names stay concise and project a visible `Composition` sidebar tag. Non-supported Web Awesome recommendations project to a visible `Discouraged` tag, with Popup as the deliberate conditional exception. Demo paths resolve to the matching `ui/ux-demo/demos/<id>.tsx` or `ui/ux-demo/recipes/*.tsx` file, or to `ui/ux-demo/webawesome-demos.tsx` for Web Awesome entries. `ui/ux-demo/main.tsx` owns only the catalog shell, shared state, routing, and interaction wiring. First-party component entries additionally derive `ui/src/<public-subpath>.tsx` from their canonical `delivery.browserImport` or opt-in `delivery.moduleImport`.
29
29
  - Every detail footer groups compact `Demo source` and `Guidance` actions; first-party component details also expose `Component source`. Web Awesome details say `Integration guidance` to distinguish the local integration contract from the upstream component implementation. Each button retains its repository-relative path in the DOM for verification and uses a deploy-safe absolute GitHub blob URL with a contextual new-tab name; the demo does not depend on a local source server or add a package runtime export. At phone widths, the resource group and related-component selector stack as full-width rows, with any resource overflow contained locally so labels cannot overlap the selector or widen the document.
30
30
  - The master/detail shell groups first-party routes by category with production `ListHeader` and `ListItem` components. Catalog entries can supply short reusable trailing tags; the Web Awesome projection uses them to label exactly the 15 discouraged choices. The 70 ecosystem components live in a separate `Web Awesome` disclosure with seven subgroups, so they remain discoverable without being presented as first-class `@kerfjs/ui` exports. Direct Web Awesome routes open the disclosure and reveal the selected row at wide sizes; narrow layouts keep the expanded list in document flow rather than introducing a competing nested scroller.
31
- - The shell itself uses `@kerfjs/ui/layout.css`: sidebars, details, previews, dialogs, and resize specimens share unpadded panes plus self-contained content items. Its sidebar identity uses the Kerf logo beside a vertically centered title, with supporting copy in its own aligned row; the current-view heading is also a toolbar. The shared Catalog sidebar includes a filter for entry and heading names that works across first-party and ecosystem sections, including collapsed sections. Collapsing the catalog removes the pane completely and moves its restore action to the main toolbar's leading edge instead of leaving an icon-only rail. The selected view description remains inset below the page toolbar. The checkerboard belongs to the preview scroll owner rather than a viewport-height stage or nested card, and one explicitly repeated SVG tile keeps the pattern continuous through below-fold content. A lightly separated footer keeps live status above compact source, guidance, and relationship groups. The focused `ResizableRegion` specimen stretches its surface through the available stage height and reports committed width in that shared status footer; narrow layouts retain local horizontal access when the pane is resized wider than the stage, and the split remains readable at 200% root scaling. Geometry coverage checks wide, intermediate, narrow, and 200% zoom layouts.
31
+ - The shell itself uses `@kerfjs/ui/layout.css`: sidebars, details, previews, dialogs, and resize specimens share unpadded panes plus self-contained content items. Its sidebar identity uses the Kerf logo beside a vertically centered title, with supporting copy in its own aligned row; the current-view heading is also a toolbar. The shared Catalog sidebar includes a filter for entry and heading names that works across first-party and ecosystem sections, including collapsed sections. Collapsing the catalog removes the pane completely and moves its restore action to the main toolbar's leading edge instead of leaving an icon-only rail. The selected view description remains inset below the page toolbar. The preview stage sits directly in the sunken main Pane, with its canvas padded by the medium spacing token. The checkerboard belongs to that stage, rather than a viewport-height wrapper or nested card, and one explicitly repeated SVG tile keeps the pattern continuous through below-fold content. A lightly separated footer keeps live status above compact source, guidance, and relationship groups. The focused `ResizableRegion` specimen stretches its surface through the available stage height and reports committed width in that shared status footer; narrow layouts retain local horizontal access when the pane is resized wider than the stage, and the split remains readable at 200% root scaling. Geometry coverage checks wide, intermediate, narrow, and 200% zoom layouts.
32
32
  - The composer-form recipe demonstrates one coherent visible form surface with toolbar title/supporting-copy hierarchy, fields and actions on the shared 8px control gutter, and 24px major rhythm. Its conditional error or success `StateBanner` is the only nested semantic surface. Reset synchronizes the upgraded Web Awesome fields' live value properties with their empty controlled attributes and announces `Draft reset`; browser coverage preserves behavior and verifies wide, narrow, dark, validation, success, reset, 200% zoom, and forced-colors states.
33
33
  - The list-detail dialog recipe is laid out like a `Workbench` on roomy devices: a headerless `DialogSurface` modal whose `SplitView` holds a full-height list sidebar `Pane` with its own toolbar beside a full-height detail `Pane` that carries the primary xl title, the `data-dialog="close"` control, and the record actions in its footer. Compact devices get a full-screen sheet with its own labeled header, footer actions, and a list-to-detail drill-down. Browser geometry coverage protects those relationships at wide and narrow widths.
34
34
  - Catalog-only brand marks, preview dimensions, checkerboard tiles, and the compound display-settings control retain local geometry because they are specimen chrome or intrinsic control anatomy rather than reusable application spacing roles.
35
35
  - The menu composition route demonstrates toolbar/content/footer anatomy, 24px major gaps, 8/1/8 child geometry, the full-width `ListHeader` count-or-badge/action split, logical-end 44px action targets with 18px visuals, and 44px row/header/toolbar targets. Its one chevron is a production 18px `DisclosureArrow` inside a controlled Tools header that actually reveals and hides content; ordinary Projects navigation has no false disclosure affordance. Browser assertions cover stable naming, expanded/content transitions, visual rotation, node identity, reduced motion, RTL alignment and containment, and wide, narrow, dark, and 200% layouts. The focused ListHeader route separately shows the shared neutral count pill for Attachments, Notes (including zero), and Duplicates plus a non-count legacy badge, full-width logical-end actions, and no disclosure without content.
36
36
  - The `DisclosureArrow` route renders the production 18px root-scaled default without a catalog-only size override. Its two native buttons independently control the default right-to-down chevron and a recognizable right-facing replacement arrow with left-to-up custom directions; both use shortest-path rotation and their names stay stable while `aria-expanded` communicates state. Browser coverage verifies the custom clockwise path, pointer and native keyboard activation, focus, narrow and 200%-zoom stacking with contained labels, consumer sizing, and the independent `.5` Web Awesome glyph scale retained by Kerf `Select`.
37
37
  - A detail with relationships renders one content-fitted `Components` footer selector: `Uses` links to direct dependencies and `Used by` links to reverse dependents. Its popup owns the standard surface inset so headings and even long component names stay clear of the menu border. Choosing either navigates to that component's demo. The relationship group is absent when both sets are empty; no duplicate summary is rendered.
38
- - Focused demos ship no local stylesheets, inline styles, or styling-only class names. They compose Kerf components first, Web Awesome surfaces where Kerf has no equivalent, and ordinary semantic HTML for the remainder. The catalog-owned `CatalogExample.viewport` contract supplies the few specimen constraints needed to demonstrate layout behavior. Recipes follow the same rule: they ship no stylesheets, inline styles, or styling-only classes, and the catalog frames each one in the `CatalogExample` viewport its exported `presentation` declares (full-width application arrangements span the canvas measure; narrower recipes sit in the centered example column). Demo-only CSS is limited to the document-shell `style.css`, which carries no recipe rules. Decorative chrome and Web Awesome controls share the same semantic system palette instead of carrying a separate demo identity.
38
+ - Focused demos ship no local stylesheets, inline styles, or styling-only class names. They compose Kerf components first, Web Awesome surfaces where Kerf has no equivalent, and ordinary semantic HTML for the remainder. The catalog-owned `CatalogExample.viewport` contract supplies the few specimen constraints needed to demonstrate layout behavior. Recipes follow the same rule: they ship no stylesheets, inline styles, or styling-only classes, and the catalog frames each one in the `CatalogExample` viewport its exported `presentation` declares (full-width application arrangements span the canvas measure; narrower recipes sit in the left-aligned example column). Demo-only CSS is limited to the document-shell `style.css`, which carries no recipe rules. Decorative chrome and Web Awesome controls share the same semantic system palette instead of carrying a separate demo identity.
39
39
  - Scenarios use deterministic data and cover real variants, long content, disabled state, selection, progress, feedback, overlays, media, helpers, and stateful interactions. Individual Web Awesome action, form, layout, navigation, feedback, media, and helper routes provide focused theme specimens without a redundant aggregate route; ListItem and ListHeader demonstrate typed domain metadata, protected native button/disclosure semantics, and a native popover trigger relationship without product props; the Headers composition demonstrates direct toolbar headings with an extra-large title, optional supporting copy, and grouped icon/actions; the Select route verifies that option icons survive unrelated Kerf rerenders and selected slot content follows value changes; the Animation route exposes preset, easing, duration, playback-rate, and transport controls with live lifecycle status; observer routes provide user-driven intersection, mutation, and resize events with live results; ToolbarControlGroup exposes all eight Hot Sheet 2 variants, loads the Web Awesome registrations needed by its popup specimen, and preserves the upgraded dropdown's managed light-DOM children across Kerf rerenders; the Toolbar route composes a collapsible TokenSearchField inside a group, animates between the empty iconic and complete field states, stays open while populated, and focuses the revealed editor; SegmentedControl covers toolbar, rounded-rectangle, pill, equal-width, and disabled presentations; TokenSearchField covers editable ordered chips, pointer and keyboard removal, caret-preserving controlled deletion, editing, clearing, free text, disabled state, and fixed first-line alignment while content wraps; StateBanner exposes every built-in tone with tone-tinted badges, a scoped override, and a placeholder with and without a badge, the badged placeholder in every tone so captures cover the solid badge skeleton; the Application tabs composition uses a Web Awesome card with semantic header/footer slots and Kerf `Text` while demonstrating overflow, edge autoscroll, and controlled pointer/keyboard reordering.
40
40
  - The ToolbarControlGroup route also demonstrates the public compact/tight,
41
41
  mixed-content, filled-selection, nested-dropdown, and avatar/scrim contracts
@@ -48,7 +48,7 @@ deterministic state, and semantic layout owners from the [recipe guide](./recipe
48
48
  collapsible-search specimen follows the selected group shape with a
49
49
  concentric inner radius in both collapsed and expanded states.
50
50
  - Recipe ownership notes are hidden by default. A page-toolbar toggle reveals each recipe's note as its `CatalogExample` note above the framed specimen, so instructional copy never covers the composition.
51
- - Settings exercise light/dark, increased contrast, and reduced motion without changing component code. The theme action starts from the operating system's effective color scheme, always names the appearance it will switch to, and installs an explicit light or dark override when activated; contrast and reduced motion expose their independent preferences through `aria-pressed`.
51
+ - Settings exercise light/dark, increased contrast, and reduced motion without changing component code. The theme action starts from the operating system's effective color scheme, always names the appearance it will switch to, and installs an explicit light or dark override when activated; contrast and reduced motion expose their independent preferences through `aria-pressed`. The display-control group wraps whole independent controls at narrow widths so all stay reachable. The adjacent Background menu chooses an 8px-square checkerboard (default), 8px vertical stripes, a layout guide with single-pixel rules at 15px, 23px, and 32px, surface, or sunken for the preview stage. The demo saves all four display choices in browser storage and restores them across entries and reloads. A saved light or dark choice remains explicit when the operating-system appearance changes; without one, the theme follows the system. Invalid or unavailable storage falls back to defaults while the controls remain usable.
52
52
  - The footer action log is an `aria-live` region, making interactions visible to both people and browser tests.
53
53
  - The focused Toast route calls Web Awesome's documented programmatic stack API and notes that Hot Sheet 2 still uses its own app-level toast implementation; the catalog demonstrates ecosystem support without implying current product adoption.
54
54
  - Browser coverage walks every Kerf, composition, and Web Awesome route, checks every listed Web Awesome custom element is registered in its focused specimen, exercises the ecosystem disclosure and both relationship directions, validates offline source/guidance hrefs and accessible wide/narrow/200%-zoom wrapping, and covers tab selection/reordering/overflow, semantic palettes and scoped overrides, light/dark rendering, toolbar popup and responsive find behavior, single/multiline search alignment, menu alignment, banner urgency, keyboard resizing, live custom-element values, and representative wide/narrow captures.
package/docs/workbench.md CHANGED
@@ -82,6 +82,8 @@ outline is hidden until it expands. `mainPane` and a panel's `pane` forward the
82
82
  same options to an inner Pane when that is the intended focus owner. Give
83
83
  the central region `mainLabel` when it needs its own named landmark; panel
84
84
  regions use their `label`.
85
+ When a sole Pane fills the main region, the Workbench ring paints above opaque
86
+ edge-to-edge Pane content without blocking pointer input in that content.
85
87
 
86
88
  Each static `WorkbenchPanel` takes `content`, an optional `toolbar` and `footer` (see
87
89
  [Panel toolbars](#panel-toolbars)), an optional `collapsed`, an optional
package/llms.txt CHANGED
@@ -12,6 +12,9 @@ npm install kerfjs @kerfjs/ui # kerfjs is a peer; Web Awesome is an optional pee
12
12
  Import each component from its own subpath (`@kerfjs/ui/list-item`, `@kerfjs/ui/toolbar`,
13
13
  …); the browser build pulls that component's reachable CSS automatically, so do not
14
14
  maintain a central style list. Configure JSX with `"jsxImportSource": "kerfjs"`.
15
+ For arbitrary interactive help or popup content, use `openAnchoredSurface` or
16
+ `openAnchoredSurfaceAt` from `@kerfjs/ui/anchored-surface`; see
17
+ [AnchoredSurface](./docs/anchored-surface.md). The app owns content and actions.
15
18
  For a public subpath containing several visual components, use a folder with one
16
19
  component per source file and one owned stylesheet per visual component. Keep its
17
20
  compatibility entrypoints implementation-free: JavaScript/TypeScript only re-exports
@@ -395,6 +398,9 @@ headingLevel`, `TokenSearchField clearIcon`, `ContentItem frame="framed"` for a
395
398
  or explicit `scroll`, `always`, `none`), and a `NavStack` or `TabNavigator`
396
399
  with its own `chromeDividers` (same values; a `SplitView` forwards it through
397
400
  `compactStack`); name app-owned chrome in `targets`.
401
+ A `TabNavigator` also takes `separator="auto" | "hidden"`: auto draws the
402
+ bar's top edge beside a sunken active scene or nested Pane even when content
403
+ fits; hidden suppresses that edge, including scroll dividers.
398
404
  Never add a border, or a permanent `dividerSides`, to fake one.
399
405
  While a side overlay is open (a Workbench rail overlay, a `wireSidebar` compact
400
406
  overlay, an overlay `CollapsiblePanel` or horizontal `ResizableRegion`), the
@@ -524,9 +530,8 @@ shell, `@kerfjs/ui/collapsible-panel` (`CollapsiblePanel`, `CollapsiblePanelTogg
524
530
  - `@kerfjs/ui/wire-sidebar` (`wireSidebar`: toggle, focus, compact overlay, persistence; a compact overlay starts closed and opens only on a user action, and `inlineCollapsed` declares a panel's inline default).
525
531
  `@kerfjs/ui/catalog` is another opt-in shell — a
526
532
  `Catalog` component + `wireCatalog` helper for building a component-gallery catalog
527
- (collapsible category sidebar + preview stage + resources footer, with an opt-in
528
- transparent-bound and margin geometry overlay plus focus-preserving active-row
529
- reveal for long desktop sidebars) from your own
533
+ (collapsible category sidebar + preview stage + resources footer, with
534
+ focus-preserving active-row reveal for long desktop sidebars) from your own
530
535
  entries; controlled/stateless like the layouts. For any Catalog demo, follow
531
536
  the [authoritative authoring contract](./docs/catalog.md#catalog-demo-authoring-contract)
532
537
  rather than inferring structure from the shell or private CSS.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kerfjs/ui",
3
- "version": "5.0.0-beta.82",
3
+ "version": "5.0.0-beta.84",
4
4
  "description": "Accessible, composable UI primitives for kerf applications.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -317,6 +317,11 @@
317
317
  "browser": "./dist/browser/popup-menu.js",
318
318
  "import": "./dist/popup-menu.js"
319
319
  },
320
+ "./anchored-surface": {
321
+ "types": "./dist/anchored-surface.d.ts",
322
+ "browser": "./dist/browser/anchored-surface.js",
323
+ "import": "./dist/anchored-surface.js"
324
+ },
320
325
  "./popup-menu/register": {
321
326
  "types": "./dist/popup-menu-register.d.ts",
322
327
  "import": "./dist/popup-menu-register.js"
@@ -390,6 +395,7 @@
390
395
  "./loading-spinner.css": "./dist/styles/loading-spinner.css",
391
396
  "./select.css": "./dist/styles/select.css",
392
397
  "./popup-menu.css": "./dist/styles/popup-menu.css",
398
+ "./anchored-surface.css": "./dist/styles/anchored-surface.css",
393
399
  "./skeleton.css": "./dist/styles/skeleton.css",
394
400
  "./sunken-panel.css": "./dist/styles/sunken-panel.css",
395
401
  "./segmented-control.css": "./dist/styles/segmented-control.css",
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/components/layout/row/row.tsx"],"names":[],"mappings":";;;;;AAiBA,IAAM,sBAAA,uBAA6B,GAAA,CAAI;AAAA,EACrC,gBAAA;AAAA,EACA,cAAA;AAAA,EACA,cAAA;AAAA,EACA,WAAA;AAAA,EACA,WAAA;AAAA,EACA,WAAA;AAAA,EACA,kBAAA;AAAA,EACA;AACF,CAAC,CAAA;AAeD,IAAM,UAAA,GAAqC;AAAA,EACzC,MAAA;AAAA,EACA,KAAA;AAAA,EACA,IAAA;AAAA,EACA,GAAA;AAAA,EACA,GAAA;AAAA,EACA,GAAA;AAAA,EACA;AACF,CAAA;AAgCO,SAAS,GAAA,CAAI;AAAA,EAClB,QAAA;AAAA,EACA,MAAA,GAAS,MAAA;AAAA,EACT,MAAA,GAAS,MAAA;AAAA,EACT,GAAA,GAAM,IAAA;AAAA,EACN,IAAA,GAAO,KAAA;AAAA,EACP,IAAA,GAAO,KAAA;AAAA,EACP,IAAA,GAAO,KAAA;AAAA,EACP,UAAA,GAAa,EAAA;AAAA,EACb,aAAA,GAAgB,EAAA;AAAA,EAChB,SAAA,GAAY,EAAA;AAAA,EACZ,iBAAiB,EAAC;AAAA,EAClB;AACF,CAAA,EAAa;AACX,EAAA,MAAM,kBAAA,GAAqB,oBAAA;AAAA,IACzB,cAAA;AAAA,IACA;AAAA,GACF;AACA,EAAA,MAAM,WAAW,UAAA,CAAW,QAAA,CAAS,GAAkB,CAAA,GACnD,KAAA,CAAM,GAAkB,CAAA,GACxB,GAAA;AACJ,EAAA,MAAM,SAAA,GAAY,IAAA,KAAS,IAAA,GAAO,UAAA,GAAa,IAAA,IAAQ,MAAA;AACvD,EAAA,MAAM,KAAA,GAAQ;AAAA,IACZ,kBAAkB,QAAQ,CAAA,CAAA;AAAA,IAC1B,SAAA,GAAY,CAAA,gBAAA,EAAmB,SAAS,CAAA,CAAA,GAAK;AAAA,GAC/C,CACG,MAAA,CAAO,OAAO,CAAA,CACd,KAAK,GAAG,CAAA;AAEX,EAAA,uBACE,GAAA;AAAA,IAAC,KAAA;AAAA,IAAA;AAAA,MACE,GAAG,kBAAA;AAAA,MACJ,KAAA,EAAO,CAAA,QAAA,EAAW,SAAS,CAAA,CAAA,CAAG,IAAA,EAAK;AAAA,MACnC,gBAAA,EAAe,KAAA;AAAA,MACf,cAAA,EAAc,oBAAoB,MAAM,CAAA;AAAA,MACxC,cAAA,EAAc,kBAAkB,MAAM,CAAA;AAAA,MACtC,WAAA,EAAW,MAAA,CAAO,OAAA,CAAQ,IAAI,CAAC,CAAA;AAAA,MAC/B,WAAA,EAAW,OAAO,MAAA,GAAS,MAAA;AAAA,MAC3B,WAAA,EAAW,OAAO,IAAI,CAAA;AAAA,MACtB,oBAAkB,UAAA,IAAc,MAAA;AAAA,MAChC,uBAAqB,aAAA,IAAiB,MAAA;AAAA,MACtC,KAAA;AAAA,MACA,IAAA;AAAA,MAEC;AAAA;AAAA,GACH;AAEJ","file":"chunk-5D3WDMIK.js","sourcesContent":["import type { KerfUiContent } from '../../../shared/content/semantic-content.js';\nimport { filterDataAttributes } from '../../../shared/dom/extension-attributes.js';\nimport {\n type HorizontalAlignment,\n horizontalAlignment,\n type VerticalAlignment,\n verticalAlignment,\n} from '../../../shared/layout/flex-alignment.js';\nimport type { Sides } from '../../../shared/layout/sides.js';\nimport {\n type CssFlex,\n type CssFlexKeyword,\n type CssLength,\n space,\n type UiSpaceName,\n} from '../../../shared/styles/css-values.js';\n\nconst rowProtectedAttributes = new Set([\n 'data-component',\n 'data-h-align',\n 'data-v-align',\n 'data-flex',\n 'data-fill',\n 'data-wrap',\n 'data-text-insets',\n 'data-control-insets',\n]);\n\ntype RowRootAttributes = Readonly<\n Record<`data-${string}`, string | undefined> & {\n 'data-component'?: never;\n 'data-h-align'?: never;\n 'data-v-align'?: never;\n 'data-flex'?: never;\n 'data-fill'?: never;\n 'data-wrap'?: never;\n 'data-text-insets'?: never;\n 'data-control-insets'?: never;\n }\n>;\n\nconst spaceNames: readonly UiSpaceName[] = [\n 'none',\n '2xs',\n 'xs',\n 's',\n 'm',\n 'l',\n 'xl',\n];\n\nexport interface RowProps {\n children?: KerfUiContent;\n /** Horizontal distribution. Defaults to left. */\n hAlign?: HorizontalAlignment;\n /** Vertical alignment and wrapped-line distribution. Defaults to full. */\n vAlign?: VerticalAlignment;\n /** A named UI spacing token or typed CSS length. Defaults to xs. */\n gap?: UiSpaceName | CssLength;\n /** Allow this row to grow/shrink, use a keyword, or supply a typed CSS flex shorthand. */\n flex?: boolean | CssFlexKeyword | CssFlex;\n /**\n * Fill the height of a parent with a definite height, such as an app root or\n * a fixed-height frame, when this row is that parent's layout root. Inside a\n * flex layout use `flex` instead. Defaults to false.\n */\n fill?: boolean;\n /** Allow children to wrap onto additional lines. */\n wrap?: boolean;\n /** Physical sides that receive the standard 17px text inset. */\n textInsets?: Sides;\n /** Physical sides that receive the standard 8px control inset. Text insets win on overlap. */\n controlInsets?: Sides;\n className?: string;\n /** Safe `data-*` metadata; Row-owned structural attributes remain protected. */\n rootAttributes?: RowRootAttributes;\n /** Native named-slot assignment when composed inside a web component. */\n slot?: string;\n}\n\n/** A horizontal flex row with explicit physical-axis alignment and spacing. */\nexport function Row({\n children,\n hAlign = 'left',\n vAlign = 'full',\n gap = 'xs',\n flex = false,\n fill = false,\n wrap = false,\n textInsets = '',\n controlInsets = '',\n className = '',\n rootAttributes = {},\n slot,\n}: RowProps) {\n const safeRootAttributes = filterDataAttributes(\n rootAttributes,\n rowProtectedAttributes,\n );\n const gapValue = spaceNames.includes(gap as UiSpaceName)\n ? space(gap as UiSpaceName)\n : gap;\n const flexValue = flex === true ? '1 1 auto' : flex || undefined;\n const style = [\n `--_kui-row-gap:${gapValue}`,\n flexValue ? `--_kui-row-flex:${flexValue}` : '',\n ]\n .filter(Boolean)\n .join(';');\n\n return (\n <div\n {...safeRootAttributes}\n class={`kui-row ${className}`.trim()}\n data-component=\"row\"\n data-h-align={horizontalAlignment(hAlign)}\n data-v-align={verticalAlignment(vAlign)}\n data-flex={String(Boolean(flex))}\n data-fill={fill ? 'true' : undefined}\n data-wrap={String(wrap)}\n data-text-insets={textInsets || undefined}\n data-control-insets={controlInsets || undefined}\n style={style}\n slot={slot}\n >\n {children}\n </div>\n );\n}\n\nexport type {\n HorizontalAlignment,\n VerticalAlignment,\n} from '../../../shared/layout/flex-alignment.js';\nexport type { Sides } from '../../../shared/layout/sides.js';\nexport type {\n CssFlex,\n CssFlexKeyword,\n CssLength,\n UiSpaceName,\n} from '../../../shared/styles/css-values.js';\n"]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/components/navigation/nav-stack/nav-stack.tsx"],"names":[],"mappings":"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","file":"chunk-CVKIADIS.js","sourcesContent":["import { ChevronLeft } from 'lucide';\n\nimport type { KerfUiContent } from '../../../shared/content/semantic-content.js';\nimport type { NavPane } from '../../../shared/panels/nav-pane.js';\nimport type { PanelBottomToolbar } from '../../../shared/panels/panel-toolbar.js';\nimport { Toolbar, type ToolbarConfig } from '../../actions/toolbar/toolbar.js';\nimport { ToolbarControlGroup } from '../../actions/toolbar-control-group/toolbar-control-group.js';\nimport {\n type HeadingLevel,\n ToolbarText,\n type ToolbarTextSize,\n} from '../../actions/toolbar-text/toolbar-text.js';\nimport { List, type ListConfig } from '../../collections/list/list.js';\nimport { Pane } from '../../layout/pane/pane.js';\nimport { LucideIcon } from '../../media/lucide-icon/lucide-icon.js';\n\n/**\n * One entry in a {@link NavStack}. The app owns the stack as an array (usually a\n * signal); `NavStack` renders it and `wireNavStack` animates the transitions.\n */\nexport interface NavStackView extends NavPane<\n NavStackViewToolbar,\n PanelBottomToolbar\n> {\n /** Stable identity for keyed reconcile and transition direction. */\n key: string;\n}\n\n/** Per-view top toolbar. The stack supplies its back control before `leading`. */\nexport interface NavStackViewToolbar extends NavStackToolbarConfig {\n title?: string;\n leading?: KerfUiContent;\n center?: KerfUiContent;\n trailing?: KerfUiContent;\n}\n\n/** A view's structured bottom toolbar; it cross-fades on navigation. */\nexport type NavStackViewBottomToolbar = PanelBottomToolbar;\n\n/**\n * The top toolbar's configuration. Its `ToolbarConfig` forwards to the real\n * `Toolbar` the stack renders; by default that toolbar draws no divider and\n * claims the top and side safe-area edges the stack still touches.\n */\nexport interface NavStackToolbarConfig extends ToolbarConfig {\n /** Accessible name of the top toolbar (default: none). */\n label?: string;\n /** Size of the view title's `ToolbarText` (default `large`). */\n titleSize?: ToolbarTextSize;\n /** Expose the view title as a heading at this level (default: a plain span). */\n headingLevel?: HeadingLevel;\n}\n\nexport interface NavStackProps {\n id: string;\n /** Accessible name for the stack region. */\n label: string;\n /** The stack, root first; the last entry is the active top view. */\n views: NavStackView[];\n /** Accessible label for the icon-only back control (default \"Back\"). */\n backLabel?: string;\n /** The back control's icon (default a chevron-left `LucideIcon`). */\n backIcon?: KerfUiContent;\n /**\n * Visible text beside the back icon, such as the previous view's title\n * (default: icon only). When set, the text names the control and\n * `backLabel` is not used.\n */\n backText?: string;\n /** The top toolbar's configuration, forwarded to its `Toolbar`. */\n toolbarConfig?: NavStackToolbarConfig;\n /** Persistent last group in every view's top toolbar, supplied by a hosting panel. */\n panelToggle?: KerfUiContent;\n /** Hide the top toolbar entirely (rare — a fully custom-chrome view). */\n hideToolbar?: boolean;\n /** Optional persistent bottom toolbar used when the active view does not provide one. */\n bottomToolbar?: NavStackViewBottomToolbar;\n /**\n * The line under the top chrome and over the bottom toolbar, where they\n * meet the active view. `scroll` (default) shows the chrome's line only\n * while the view's content is scrolled beneath it, and the bottom\n * toolbar's only while more content lies below — never when the content\n * fits — once `wireScrollDividers` (`@kerfjs/ui/wire-scroll-dividers`) is\n * wired above the stack; unwired, neither shows. `always` shows both\n * without the wiring; `none` neither, even when wired. The line is drawn\n * inside the chrome, so no state moves the chrome or the content. A\n * `toolbarConfig.dividerSides` edge is the Toolbar's own and is unaffected.\n */\n chromeDividers?: 'scroll' | 'always' | 'none';\n className?: string;\n /** Native named-slot assignment when composed inside a web component. */\n slot?: string;\n}\n\nconst DEFAULT_SAFE_AREA_EDGES = [\n 'block-start',\n 'inline-start',\n 'inline-end',\n] as const;\n\nconst DEFAULT_BOTTOM_SAFE_AREA_EDGES = [\n 'block-end',\n 'inline-start',\n 'inline-end',\n] as const;\n\nfunction chromeList(content: KerfUiContent, config?: ListConfig) {\n return (\n <List\n dividerSides={config?.dividerSides}\n gap={config?.gap}\n hAlign={config?.hAlign}\n vAlign={config?.vAlign}\n textInsets={config?.textInsets}\n controlInsets={config?.controlInsets}\n >\n {content}\n </List>\n );\n}\n\n/**\n * A navigation stack (iOS-style push/pop). Renders every entry stacked, the last\n * one active; `@kerfjs/ui/wire-nav-stack`'s `wireNavStack` slides the content and\n * cross-fades the chrome across a change. Its top chrome is a real `Toolbar`: the\n * back control and title lead, the active view's `toolbar` fills the zones,\n * and `toolbarConfig` supplies defaults. A single-pane\n * layout is a `NavStack` with one entry. See `docs/23-app-layouts.md` §3.1.\n */\nexport function NavStack({\n id,\n label,\n views,\n backLabel = 'Back',\n backIcon,\n backText,\n toolbarConfig = {},\n panelToggle,\n hideToolbar = false,\n bottomToolbar,\n chromeDividers = 'scroll',\n className = '',\n slot,\n}: NavStackProps) {\n const topIndex = views.length - 1;\n const top = views[topIndex];\n const topToolbar = top?.toolbar;\n const bottom = top?.bottomToolbar ?? bottomToolbar;\n const canPop = views.length > 1;\n return (\n <section\n class={`kui-nav-stack ${className}`.trim()}\n id={id}\n data-component=\"nav-stack\"\n data-nav-stack-id={id}\n data-depth={String(views.length)}\n data-chrome-dividers={\n chromeDividers === 'always' || chromeDividers === 'none'\n ? chromeDividers\n : undefined\n }\n aria-label={label}\n slot={slot}\n >\n {!hideToolbar && (\n <div class=\"kui-nav-stack__chrome\" data-nav-stack-chrome>\n <Toolbar\n label={topToolbar?.label ?? toolbarConfig.label}\n dividerSides={\n topToolbar?.dividerSides ?? toolbarConfig.dividerSides\n }\n centerAlign={topToolbar?.centerAlign ?? toolbarConfig.centerAlign}\n responsive={topToolbar?.responsive ?? toolbarConfig.responsive}\n responsiveAt={\n topToolbar?.responsiveAt ?? toolbarConfig.responsiveAt\n }\n safeAreaEdges={\n topToolbar?.safeAreaEdges ??\n toolbarConfig.safeAreaEdges ??\n DEFAULT_SAFE_AREA_EDGES\n }\n leading={\n <>\n {canPop && (\n <ToolbarControlGroup\n appearance=\"borderless\"\n shape=\"rounded\"\n content={backText ? 'mixed' : 'icon'}\n single\n >\n <button\n type=\"button\"\n class=\"kui-nav-stack__back\"\n data-nav-back\n aria-label={backText ? undefined : backLabel}\n >\n {backIcon ?? (\n <LucideIcon\n icon={ChevronLeft}\n name=\"chevron-left\"\n className=\"kui-nav-stack__back-icon\"\n />\n )}\n {backText ? <span>{backText}</span> : null}\n </button>\n </ToolbarControlGroup>\n )}\n {topToolbar?.leading}\n <ToolbarText\n text={topToolbar?.title ?? ''}\n size={\n topToolbar?.titleSize ?? toolbarConfig.titleSize ?? 'large'\n }\n headingLevel={\n topToolbar?.headingLevel ?? toolbarConfig.headingLevel\n }\n fill\n />\n </>\n }\n center={topToolbar?.center}\n trailing={\n <>\n {topToolbar?.trailing}\n {panelToggle}\n </>\n }\n />\n </div>\n )}\n <div class=\"kui-nav-stack__viewport\" data-nav-stack-viewport>\n {views.map((view, index) => (\n <article\n class=\"kui-nav-stack__view\"\n data-key={view.key}\n data-nav-key={view.key}\n data-nav-active={String(index === topIndex)}\n aria-hidden={String(index !== topIndex)}\n >\n {view.pane ||\n view.header !== undefined ||\n view.footer !== undefined ? (\n <Pane\n appearance={view.pane?.appearance}\n deepInset={view.pane?.deepInset}\n tabIndex={view.pane?.tabIndex}\n outlined={view.pane?.outlined}\n contentElement={view.pane?.contentElement}\n contentLabel={view.pane?.contentLabel}\n separators={view.pane?.separators}\n safeAreaEdges={view.pane?.safeAreaEdges}\n chromeDividers={view.pane?.chromeDividers}\n header={\n view.header === undefined ? undefined : (\n <div data-nav-stack-header>\n {chromeList(view.header, view.headerList)}\n </div>\n )\n }\n footer={\n view.footer === undefined ? undefined : (\n <div data-nav-stack-footer>\n {chromeList(view.footer, view.footerList)}\n </div>\n )\n }\n >\n {view.content}\n </Pane>\n ) : (\n view.content\n )}\n </article>\n ))}\n </div>\n {bottom && (\n <footer class=\"kui-nav-stack__bottom\" data-nav-stack-bottom>\n <Toolbar\n position=\"footer\"\n label={bottom.label}\n dividerSides={bottom.dividerSides}\n centerAlign={bottom.centerAlign}\n responsive={bottom.responsive}\n responsiveAt={bottom.responsiveAt}\n safeAreaEdges={\n bottom.safeAreaEdges ?? DEFAULT_BOTTOM_SAFE_AREA_EDGES\n }\n leading={bottom.leading}\n center={bottom.center}\n trailing={bottom.trailing}\n />\n </footer>\n )}\n </section>\n );\n}\n"]}