@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
@@ -97,7 +97,6 @@
97
97
 
98
98
  .kui-catalog-example__viewport[data-frame] {
99
99
  border: 1px solid var(--kui-color-neutral-border-normal);
100
- border-radius: var(--kui-radius-m);
101
100
  }
102
101
 
103
102
  .kui-catalog-example__viewport[data-frame="dashed"] {
@@ -150,7 +149,7 @@
150
149
  }
151
150
  }
152
151
 
153
- /* The published inset lets the geometry overlay exclude this authoring aid. */
152
+ /* Bare specimens use a small inset to align with the surrounding examples. */
154
153
  .kui-catalog-example[data-align="glyph"]
155
154
  > :not([data-catalog-example-label]):not([data-catalog-example-note]) {
156
155
  --kui-catalog-example-align: 1rem;
@@ -1,55 +1,53 @@
1
- /* The preview stage fills the work area's scroller — and grows with a taller
2
- specimen, never shrinking under it — behind a transparency checkerboard,
3
- so a specimen's own surface (or lack of one) stays visible. */
1
+ /* The preview stage fills the work area's scroller and grows with a taller
2
+ specimen, never shrinking under it. The default checkerboard shares the
3
+ sunken Pane's translucent surface instead of painting a second layer. */
4
4
  .kui-catalog__stage {
5
5
  flex: 1 0 auto;
6
- background-color: var(--kui-color-surface-lowered);
7
- background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%238e8e93' fill-opacity='.12' d='M0 0h12v12H0zM12 12h12v12H12z'/%3E%3C/svg%3E");
8
- background-size: 1.5rem 1.5rem;
6
+ background-color: transparent;
7
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath fill='%238e8e93' fill-opacity='.12' d='M0 0h8v8H0zM8 8h8v8H8z'/%3E%3C/svg%3E");
8
+ background-size: 1rem 1rem;
9
9
  }
10
10
 
11
- /* The centered specimen canvas, at the catalog's reading measure. */
12
- .kui-catalog__canvas {
13
- position: relative;
14
- display: grid;
15
- width: min(100%, 70rem);
16
- min-width: 0;
17
- min-height: 100%;
18
- margin-inline: auto;
19
- padding: var(--kui-layout-content-gap);
20
- box-sizing: border-box;
21
- place-items: center;
11
+ .kui-catalog__stage[data-background-style="vertical-stripes"] {
12
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath fill='%238e8e93' fill-opacity='.12' d='M0 0h8v16H0z'/%3E%3C/svg%3E");
13
+ background-size: 1rem 1rem;
22
14
  }
23
15
 
24
- /* `wireCatalogGeometryOverlay` appends the bound, border, and margin layers. */
25
- /* css-ownership: allow-root kui-catalog__geometry-bound -- The catalog wire appends measured specimen bound layers at runtime. */
26
- /* css-ownership: allow-root kui-catalog__geometry-border -- The catalog wire appends computed border layers at runtime. */
27
- /* css-ownership: allow-root kui-catalog__geometry-margin -- The catalog wire appends computed margin layers at runtime. */
28
- .kui-catalog__geometry-overlay {
29
- position: absolute;
30
- z-index: 3;
31
- pointer-events: none;
32
- inset: 0;
16
+ .kui-catalog__stage[data-background-style="layout-guide"] {
17
+ background-image: linear-gradient(
18
+ to right,
19
+ transparent 15px,
20
+ rgb(142 142 147 / 12%) 15px 16px,
21
+ transparent 16px 23px,
22
+ rgb(142 142 147 / 12%) 23px 24px,
23
+ transparent 24px 32px,
24
+ rgb(142 142 147 / 12%) 32px 33px,
25
+ transparent 33px
26
+ );
27
+ background-size: auto;
28
+ background-repeat: no-repeat;
33
29
  }
34
30
 
35
- .kui-catalog__geometry-bound,
36
- .kui-catalog__geometry-border,
37
- .kui-catalog__geometry-margin {
38
- position: absolute;
39
- top: 0;
40
- left: 0;
41
- box-sizing: border-box;
31
+ .kui-catalog__stage[data-background-style="surface"] {
32
+ background-color: var(--kui-color-surface);
33
+ background-image: none;
34
+ background-size: auto;
42
35
  }
43
36
 
44
- .kui-catalog__geometry-bound {
45
- border: 1px dashed color-mix(in srgb, var(--kui-color-text) 40%, transparent);
37
+ .kui-catalog__stage[data-background-style="sunken"] {
38
+ background-color: transparent;
39
+ background-image: none;
40
+ background-size: auto;
46
41
  }
47
42
 
48
- .kui-catalog__geometry-border {
49
- border: 0 solid
50
- color-mix(in srgb, var(--kui-color-brand-on-quiet) 72%, transparent);
51
- }
52
-
53
- .kui-catalog__geometry-margin {
54
- background: color-mix(in srgb, #f6a94c 55%, transparent);
43
+ /* Align the specimen canvas with the work area's reading start. */
44
+ .kui-catalog__canvas {
45
+ position: relative;
46
+ display: grid;
47
+ width: min(100%, 70rem);
48
+ min-width: 0;
49
+ min-height: 100%;
50
+ padding: var(--kui-space-m);
51
+ box-sizing: border-box;
52
+ place-items: start;
55
53
  }
@@ -4,17 +4,6 @@
4
4
  height: 100%;
5
5
  }
6
6
 
7
- /* The brand mark, sized to sit inside a toolbar group. */
8
- .kui-catalog__mark {
9
- display: block;
10
- width: 2.125rem;
11
- height: 2.125rem;
12
- }
13
-
14
- .kui-catalog__filter {
15
- border-radius: var(--kui-layout-rounded-radius);
16
- }
17
-
18
7
  .kui-catalog [data-catalog-filter-empty],
19
8
  .kui-catalog [data-catalog-section][hidden],
20
9
  .kui-catalog [data-catalog-entry-name][hidden],
@@ -5,6 +5,8 @@
5
5
  omits `gap` or `flex` never inherits its ancestor's; the component's inline
6
6
  style overrides these defaults for the props it does set. */
7
7
  --_kui-list-gap: 0;
8
+ --_kui-list-column-gap: var(--_kui-list-gap);
9
+ --_kui-list-row-gap: var(--_kui-list-gap);
8
10
  --_kui-list-flex: 0 1 auto;
9
11
  --_kui-list-inset-top: 0;
10
12
  --_kui-list-inset-right: 0;
@@ -26,7 +28,8 @@
26
28
  align-items: stretch;
27
29
  align-content: space-between;
28
30
  justify-content: flex-start;
29
- gap: var(--_kui-list-gap, 0);
31
+ column-gap: var(--_kui-list-column-gap);
32
+ row-gap: var(--_kui-list-row-gap);
30
33
  box-shadow:
31
34
  inset 0 1px var(--_kui-list-divider-top),
32
35
  inset -1px 0 var(--_kui-list-divider-right),
@@ -0,0 +1,15 @@
1
+ .kui-anchored-surface {
2
+ box-sizing: border-box;
3
+ inset: auto;
4
+ width: max-content;
5
+ max-width: calc(100vw - 2 * var(--kui-space-xs, 8px));
6
+ max-height: calc(100dvh - 2 * var(--kui-space-xs, 8px));
7
+ overflow: auto;
8
+ padding: var(--kui-anchored-surface-inset, var(--kui-space-xs, 8px));
9
+ border: 1px solid var(--kui-color-neutral-border-normal);
10
+ border-radius: var(--kui-anchored-surface-radius, var(--kui-radius-m));
11
+ color: var(--kui-color-text, inherit);
12
+ background: var(--kui-color-surface-raised);
13
+ box-shadow: var(--kui-shadow-l);
14
+ font: inherit;
15
+ }
@@ -3,6 +3,8 @@
3
3
  /* Private per-instance values are reset on every Grid so a nested Grid never
4
4
  inherits its ancestor's; the inline style overrides them per prop. */
5
5
  --_kui-grid-gap: var(--kui-grid-gap);
6
+ --_kui-grid-column-gap: var(--_kui-grid-gap);
7
+ --_kui-grid-row-gap: var(--_kui-grid-gap);
6
8
  --_kui-grid-flex: 0 1 auto;
7
9
  --_kui-grid-inset-top: 0;
8
10
  --_kui-grid-inset-right: 0;
@@ -17,7 +19,8 @@
17
19
  var(--_kui-grid-inset-bottom, 0) var(--_kui-grid-inset-left, 0);
18
20
  flex: var(--_kui-grid-flex, initial);
19
21
  grid-template-columns: repeat(var(--_kui-grid-columns), minmax(0, 1fr));
20
- gap: var(--_kui-grid-gap, var(--kui-grid-gap));
22
+ column-gap: var(--_kui-grid-column-gap);
23
+ row-gap: var(--_kui-grid-row-gap);
21
24
  }
22
25
 
23
26
  .kui-grid[data-control-insets*="t"] {
@@ -4,6 +4,7 @@
4
4
  --kui-pane-separator-inline-start-width: 0;
5
5
  --kui-pane-separator-inline-end-width: 0;
6
6
 
7
+ position: relative;
7
8
  display: grid;
8
9
  min-width: 0;
9
10
  min-height: 0;
@@ -27,6 +28,18 @@
27
28
  outline-offset: -3px;
28
29
  }
29
30
 
31
+ /* A bleeding child can establish its own paint layer above the Pane outline.
32
+ Repeat the ring as an overlay after the content, without intercepting input. */
33
+ .kui-pane:is(:focus-visible, [data-outlined="true"])::after {
34
+ position: absolute;
35
+ z-index: 1;
36
+ outline: var(--kui-focus-ring);
37
+ outline-offset: -3px;
38
+ content: "";
39
+ inset: 0;
40
+ pointer-events: none;
41
+ }
42
+
30
43
  /* Paint the active scroll owner once, so alpha lowered colors do not stack.
31
44
  Fixed chrome leaves the content slot scrolling; auto chrome lets the Pane
32
45
  root scroll in its short mode, while its content remains transparent. */
@@ -180,6 +193,8 @@
180
193
  /* Add one outer gutter without changing the inherited item-margin token.
181
194
  Nested Row, List, and Grid therefore keep their own default 8px insets. */
182
195
  .kui-pane[data-deep-inset="true"] > .kui-pane__content {
196
+ /* Reserve a vertical scrollbar gutter when the host uses opaque edge-to-edge content. */
197
+ scrollbar-gutter: var(--kui-pane-scrollbar-gutter, auto);
183
198
  --_kui-pane-deep-block-inset: var(--kui-space-xs);
184
199
  --kui-pane-content-inset-inline-start: calc(
185
200
  var(--_kui-pane-safe-inline-start) + var(--kui-space-xs)
@@ -3,6 +3,8 @@
3
3
  /* Private per-instance values are reset on every Row so a nested Row never
4
4
  inherits its ancestor's; the inline style overrides them per prop. */
5
5
  --_kui-row-gap: var(--kui-row-gap);
6
+ --_kui-row-column-gap: var(--_kui-row-gap);
7
+ --_kui-row-row-gap: var(--_kui-row-gap);
6
8
  --_kui-row-flex: 0 1 auto;
7
9
  --_kui-row-inset-top: 0;
8
10
  --_kui-row-inset-right: 0;
@@ -21,7 +23,8 @@
21
23
  align-items: stretch;
22
24
  align-content: space-between;
23
25
  justify-content: flex-start;
24
- gap: var(--_kui-row-gap, var(--kui-row-gap));
26
+ column-gap: var(--_kui-row-column-gap);
27
+ row-gap: var(--_kui-row-row-gap);
25
28
  }
26
29
 
27
30
  .kui-row[data-control-insets*="t"] {
@@ -100,6 +100,7 @@
100
100
  }
101
101
 
102
102
  .kui-workbench__main {
103
+ position: relative;
103
104
  flex: 1;
104
105
  min-height: 0;
105
106
  overflow: auto;
@@ -113,6 +114,22 @@
113
114
  outline-offset: -3px;
114
115
  }
115
116
 
117
+ /* A sole Pane can host an opaque full-width scroller above the main outline.
118
+ Keep the selected work-area ring in the final paint layer, without blocking
119
+ controls in that Pane. Other main compositions retain their own outline. */
120
+ .kui-workbench__main:has(> [data-component="pane"]:only-child):is(
121
+ :focus-visible,
122
+ [data-outlined="true"]
123
+ )::after {
124
+ position: absolute;
125
+ z-index: 1;
126
+ outline: var(--kui-focus-ring);
127
+ outline-offset: -3px;
128
+ content: "";
129
+ inset: 0;
130
+ pointer-events: none;
131
+ }
132
+
116
133
  /* When no Pane chrome is composed, the Workbench region owns its scroll.
117
134
  A resizable auto-overflow panel instead scrolls its content box so its
118
135
  separator handle remains fixed. Paint only that owner. */
@@ -197,10 +197,19 @@
197
197
  neither, whatever it reports. The chrome's line is an overlaid 1px pseudo-element
198
198
  (whatever the chrome holds never covers it) and the bottom's border is
199
199
  always there, so showing either never moves the chrome, the toolbar, or
200
- the content. A cross-fading chrome copy keeps the line its snapshot had. */
201
- .kui-nav-stack:not([data-chrome-dividers])
200
+ the content. A cross-fading chrome copy keeps the line its snapshot had.
201
+ With `separator="auto"`, an active sunken Pane also colors both available
202
+ edges; `hidden` suppresses the stack-owned lines. */
203
+ .kui-nav-stack[data-separator="auto"]:has(
204
+ > .kui-nav-stack__viewport
205
+ > .kui-nav-stack__view[data-nav-active="true"]
206
+ > [data-component="pane"][data-appearance="sunken"]
207
+ )
208
+ > .kui-nav-stack__chrome::after,
209
+ .kui-nav-stack[data-separator="auto"]:not([data-chrome-dividers])
202
210
  > .kui-nav-stack__chrome[data-scroll-divider*="b"]::after,
203
- .kui-nav-stack[data-chrome-dividers="always"] > .kui-nav-stack__chrome::after {
211
+ .kui-nav-stack[data-separator="auto"][data-chrome-dividers="always"]
212
+ > .kui-nav-stack__chrome::after {
204
213
  position: absolute;
205
214
  block-size: 1px;
206
215
  background: var(--kui-color-neutral-border-normal);
@@ -210,9 +219,16 @@
210
219
  pointer-events: none;
211
220
  }
212
221
 
213
- .kui-nav-stack:not([data-chrome-dividers])
222
+ .kui-nav-stack[data-separator="auto"]:has(
223
+ > .kui-nav-stack__viewport
224
+ > .kui-nav-stack__view[data-nav-active="true"]
225
+ > [data-component="pane"][data-appearance="sunken"]
226
+ )
227
+ > .kui-nav-stack__bottom,
228
+ .kui-nav-stack[data-separator="auto"]:not([data-chrome-dividers])
214
229
  > .kui-nav-stack__bottom[data-scroll-divider*="t"],
215
- .kui-nav-stack[data-chrome-dividers="always"] > .kui-nav-stack__bottom {
230
+ .kui-nav-stack[data-separator="auto"][data-chrome-dividers="always"]
231
+ > .kui-nav-stack__bottom {
216
232
  --_kui-nav-stack-bottom-divider: var(--kui-color-neutral-border-normal);
217
233
  }
218
234
 
@@ -133,21 +133,37 @@
133
133
  );
134
134
  padding-inline: var(--_kui-tab-scaffold-safe-inline-start)
135
135
  var(--_kui-tab-scaffold-safe-inline-end);
136
- /* Chrome divider: with `chromeDividers="scroll"` (the default, no
137
- attribute) the bar's 1px top border shows only while the active scene's
138
- content continues below it — never at the scroll end or when the content
139
- fits. `wireScrollDividers` reports that state through
140
- `data-scroll-divider`; `always` shows it without the wiring, and `none`
141
- never. The border is always there and only its color changes, so
142
- showing it never moves the bar or a tab. */
136
+ /* The reserved 1px edge serves both the sunken-surface separator and the
137
+ scroll divider. Changing its color never moves the bar or a tab. */
143
138
  border-block-start: 1px solid
144
139
  var(--_kui-tab-scaffold-bar-divider, transparent);
145
140
  background: var(--kui-color-surface);
146
141
  }
147
142
 
148
- .kui-tab-scaffold:not([data-chrome-dividers])
143
+ .kui-tab-scaffold[data-separator="auto"]:has(
144
+ > .kui-tab-scaffold__scenes
145
+ > .kui-tab-scaffold__scene[data-active="true"][data-appearance="sunken"]
146
+ )
147
+ > .kui-tab-scaffold__bar,
148
+ .kui-tab-scaffold[data-separator="auto"]:has(
149
+ > .kui-tab-scaffold__scenes
150
+ > .kui-tab-scaffold__scene[data-active="true"]
151
+ > [data-component="pane"][data-appearance="sunken"]:only-child
152
+ )
153
+ > .kui-tab-scaffold__bar,
154
+ .kui-tab-scaffold[data-separator="auto"]:has(
155
+ > .kui-tab-scaffold__scenes
156
+ > .kui-tab-scaffold__scene[data-active="true"]
157
+ > [data-component="nav-stack"]:only-child
158
+ > [data-nav-stack-viewport]
159
+ > [data-nav-active="true"]
160
+ > [data-component="pane"][data-appearance="sunken"]
161
+ )
162
+ > .kui-tab-scaffold__bar,
163
+ .kui-tab-scaffold[data-separator="auto"]:not([data-chrome-dividers])
149
164
  > .kui-tab-scaffold__bar[data-scroll-divider*="t"],
150
- .kui-tab-scaffold[data-chrome-dividers="always"] > .kui-tab-scaffold__bar {
165
+ .kui-tab-scaffold[data-separator="auto"][data-chrome-dividers="always"]
166
+ > .kui-tab-scaffold__bar {
151
167
  --_kui-tab-scaffold-bar-divider: var(--kui-color-neutral-border-normal);
152
168
  }
153
169
 
@@ -3,6 +3,8 @@
3
3
  /* Private per-instance values are reset on every Grid so a nested Grid never
4
4
  inherits its ancestor's; the inline style overrides them per prop. */
5
5
  --_kui-grid-gap: var(--kui-grid-gap);
6
+ --_kui-grid-column-gap: var(--_kui-grid-gap);
7
+ --_kui-grid-row-gap: var(--_kui-grid-gap);
6
8
  --_kui-grid-flex: 0 1 auto;
7
9
  --_kui-grid-inset-top: 0;
8
10
  --_kui-grid-inset-right: 0;
@@ -17,7 +19,8 @@
17
19
  var(--_kui-grid-inset-bottom, 0) var(--_kui-grid-inset-left, 0);
18
20
  flex: var(--_kui-grid-flex, initial);
19
21
  grid-template-columns: repeat(var(--_kui-grid-columns), minmax(0, 1fr));
20
- gap: var(--_kui-grid-gap, var(--kui-grid-gap));
22
+ column-gap: var(--_kui-grid-column-gap);
23
+ row-gap: var(--_kui-grid-row-gap);
21
24
  }
22
25
 
23
26
  .kui-grid[data-control-insets*="t"] {
@@ -5,6 +5,8 @@
5
5
  omits `gap` or `flex` never inherits its ancestor's; the component's inline
6
6
  style overrides these defaults for the props it does set. */
7
7
  --_kui-list-gap: 0;
8
+ --_kui-list-column-gap: var(--_kui-list-gap);
9
+ --_kui-list-row-gap: var(--_kui-list-gap);
8
10
  --_kui-list-flex: 0 1 auto;
9
11
  --_kui-list-inset-top: 0;
10
12
  --_kui-list-inset-right: 0;
@@ -26,7 +28,8 @@
26
28
  align-items: stretch;
27
29
  align-content: space-between;
28
30
  justify-content: flex-start;
29
- gap: var(--_kui-list-gap, 0);
31
+ column-gap: var(--_kui-list-column-gap);
32
+ row-gap: var(--_kui-list-row-gap);
30
33
  box-shadow:
31
34
  inset 0 1px var(--_kui-list-divider-top),
32
35
  inset -1px 0 var(--_kui-list-divider-right),
@@ -197,10 +197,19 @@
197
197
  neither, whatever it reports. The chrome's line is an overlaid 1px pseudo-element
198
198
  (whatever the chrome holds never covers it) and the bottom's border is
199
199
  always there, so showing either never moves the chrome, the toolbar, or
200
- the content. A cross-fading chrome copy keeps the line its snapshot had. */
201
- .kui-nav-stack:not([data-chrome-dividers])
200
+ the content. A cross-fading chrome copy keeps the line its snapshot had.
201
+ With `separator="auto"`, an active sunken Pane also colors both available
202
+ edges; `hidden` suppresses the stack-owned lines. */
203
+ .kui-nav-stack[data-separator="auto"]:has(
204
+ > .kui-nav-stack__viewport
205
+ > .kui-nav-stack__view[data-nav-active="true"]
206
+ > [data-component="pane"][data-appearance="sunken"]
207
+ )
208
+ > .kui-nav-stack__chrome::after,
209
+ .kui-nav-stack[data-separator="auto"]:not([data-chrome-dividers])
202
210
  > .kui-nav-stack__chrome[data-scroll-divider*="b"]::after,
203
- .kui-nav-stack[data-chrome-dividers="always"] > .kui-nav-stack__chrome::after {
211
+ .kui-nav-stack[data-separator="auto"][data-chrome-dividers="always"]
212
+ > .kui-nav-stack__chrome::after {
204
213
  position: absolute;
205
214
  block-size: 1px;
206
215
  background: var(--kui-color-neutral-border-normal);
@@ -210,9 +219,16 @@
210
219
  pointer-events: none;
211
220
  }
212
221
 
213
- .kui-nav-stack:not([data-chrome-dividers])
222
+ .kui-nav-stack[data-separator="auto"]:has(
223
+ > .kui-nav-stack__viewport
224
+ > .kui-nav-stack__view[data-nav-active="true"]
225
+ > [data-component="pane"][data-appearance="sunken"]
226
+ )
227
+ > .kui-nav-stack__bottom,
228
+ .kui-nav-stack[data-separator="auto"]:not([data-chrome-dividers])
214
229
  > .kui-nav-stack__bottom[data-scroll-divider*="t"],
215
- .kui-nav-stack[data-chrome-dividers="always"] > .kui-nav-stack__bottom {
230
+ .kui-nav-stack[data-separator="auto"][data-chrome-dividers="always"]
231
+ > .kui-nav-stack__bottom {
216
232
  --_kui-nav-stack-bottom-divider: var(--kui-color-neutral-border-normal);
217
233
  }
218
234
 
@@ -4,6 +4,7 @@
4
4
  --kui-pane-separator-inline-start-width: 0;
5
5
  --kui-pane-separator-inline-end-width: 0;
6
6
 
7
+ position: relative;
7
8
  display: grid;
8
9
  min-width: 0;
9
10
  min-height: 0;
@@ -27,6 +28,18 @@
27
28
  outline-offset: -3px;
28
29
  }
29
30
 
31
+ /* A bleeding child can establish its own paint layer above the Pane outline.
32
+ Repeat the ring as an overlay after the content, without intercepting input. */
33
+ .kui-pane:is(:focus-visible, [data-outlined="true"])::after {
34
+ position: absolute;
35
+ z-index: 1;
36
+ outline: var(--kui-focus-ring);
37
+ outline-offset: -3px;
38
+ content: "";
39
+ inset: 0;
40
+ pointer-events: none;
41
+ }
42
+
30
43
  /* Paint the active scroll owner once, so alpha lowered colors do not stack.
31
44
  Fixed chrome leaves the content slot scrolling; auto chrome lets the Pane
32
45
  root scroll in its short mode, while its content remains transparent. */
@@ -180,6 +193,8 @@
180
193
  /* Add one outer gutter without changing the inherited item-margin token.
181
194
  Nested Row, List, and Grid therefore keep their own default 8px insets. */
182
195
  .kui-pane[data-deep-inset="true"] > .kui-pane__content {
196
+ /* Reserve a vertical scrollbar gutter when the host uses opaque edge-to-edge content. */
197
+ scrollbar-gutter: var(--kui-pane-scrollbar-gutter, auto);
183
198
  --_kui-pane-deep-block-inset: var(--kui-space-xs);
184
199
  --kui-pane-content-inset-inline-start: calc(
185
200
  var(--_kui-pane-safe-inline-start) + var(--kui-space-xs)
@@ -3,6 +3,8 @@
3
3
  /* Private per-instance values are reset on every Row so a nested Row never
4
4
  inherits its ancestor's; the inline style overrides them per prop. */
5
5
  --_kui-row-gap: var(--kui-row-gap);
6
+ --_kui-row-column-gap: var(--_kui-row-gap);
7
+ --_kui-row-row-gap: var(--_kui-row-gap);
6
8
  --_kui-row-flex: 0 1 auto;
7
9
  --_kui-row-inset-top: 0;
8
10
  --_kui-row-inset-right: 0;
@@ -21,7 +23,8 @@
21
23
  align-items: stretch;
22
24
  align-content: space-between;
23
25
  justify-content: flex-start;
24
- gap: var(--_kui-row-gap, var(--kui-row-gap));
26
+ column-gap: var(--_kui-row-column-gap);
27
+ row-gap: var(--_kui-row-row-gap);
25
28
  }
26
29
 
27
30
  .kui-row[data-control-insets*="t"] {
@@ -24,6 +24,7 @@
24
24
  @import "./skeleton.css";
25
25
  @import "./select.css";
26
26
  @import "./popup-menu.css";
27
+ @import "./anchored-surface.css";
27
28
  @import "./segmented-control.css";
28
29
  @import "./token-search-field.css";
29
30
  @import "./state-banner.css";
@@ -133,21 +133,37 @@
133
133
  );
134
134
  padding-inline: var(--_kui-tab-scaffold-safe-inline-start)
135
135
  var(--_kui-tab-scaffold-safe-inline-end);
136
- /* Chrome divider: with `chromeDividers="scroll"` (the default, no
137
- attribute) the bar's 1px top border shows only while the active scene's
138
- content continues below it — never at the scroll end or when the content
139
- fits. `wireScrollDividers` reports that state through
140
- `data-scroll-divider`; `always` shows it without the wiring, and `none`
141
- never. The border is always there and only its color changes, so
142
- showing it never moves the bar or a tab. */
136
+ /* The reserved 1px edge serves both the sunken-surface separator and the
137
+ scroll divider. Changing its color never moves the bar or a tab. */
143
138
  border-block-start: 1px solid
144
139
  var(--_kui-tab-scaffold-bar-divider, transparent);
145
140
  background: var(--kui-color-surface);
146
141
  }
147
142
 
148
- .kui-tab-scaffold:not([data-chrome-dividers])
143
+ .kui-tab-scaffold[data-separator="auto"]:has(
144
+ > .kui-tab-scaffold__scenes
145
+ > .kui-tab-scaffold__scene[data-active="true"][data-appearance="sunken"]
146
+ )
147
+ > .kui-tab-scaffold__bar,
148
+ .kui-tab-scaffold[data-separator="auto"]:has(
149
+ > .kui-tab-scaffold__scenes
150
+ > .kui-tab-scaffold__scene[data-active="true"]
151
+ > [data-component="pane"][data-appearance="sunken"]:only-child
152
+ )
153
+ > .kui-tab-scaffold__bar,
154
+ .kui-tab-scaffold[data-separator="auto"]:has(
155
+ > .kui-tab-scaffold__scenes
156
+ > .kui-tab-scaffold__scene[data-active="true"]
157
+ > [data-component="nav-stack"]:only-child
158
+ > [data-nav-stack-viewport]
159
+ > [data-nav-active="true"]
160
+ > [data-component="pane"][data-appearance="sunken"]
161
+ )
162
+ > .kui-tab-scaffold__bar,
163
+ .kui-tab-scaffold[data-separator="auto"]:not([data-chrome-dividers])
149
164
  > .kui-tab-scaffold__bar[data-scroll-divider*="t"],
150
- .kui-tab-scaffold[data-chrome-dividers="always"] > .kui-tab-scaffold__bar {
165
+ .kui-tab-scaffold[data-separator="auto"][data-chrome-dividers="always"]
166
+ > .kui-tab-scaffold__bar {
151
167
  --_kui-tab-scaffold-bar-divider: var(--kui-color-neutral-border-normal);
152
168
  }
153
169
 
@@ -100,6 +100,7 @@
100
100
  }
101
101
 
102
102
  .kui-workbench__main {
103
+ position: relative;
103
104
  flex: 1;
104
105
  min-height: 0;
105
106
  overflow: auto;
@@ -113,6 +114,22 @@
113
114
  outline-offset: -3px;
114
115
  }
115
116
 
117
+ /* A sole Pane can host an opaque full-width scroller above the main outline.
118
+ Keep the selected work-area ring in the final paint layer, without blocking
119
+ controls in that Pane. Other main compositions retain their own outline. */
120
+ .kui-workbench__main:has(> [data-component="pane"]:only-child):is(
121
+ :focus-visible,
122
+ [data-outlined="true"]
123
+ )::after {
124
+ position: absolute;
125
+ z-index: 1;
126
+ outline: var(--kui-focus-ring);
127
+ outline-offset: -3px;
128
+ content: "";
129
+ inset: 0;
130
+ pointer-events: none;
131
+ }
132
+
116
133
  /* When no Pane chrome is composed, the Workbench region owns its scroll.
117
134
  A resizable auto-overflow panel instead scrolls its content box so its
118
135
  separator handle remains fixed. Paint only that owner. */
@@ -59,14 +59,21 @@ interface TabNavigatorProps<Id extends string = string> {
59
59
  tabs: readonly TabNavigatorTab<Id>[];
60
60
  /** The controlled active tab id (the app owns selection). */
61
61
  active: NoInfer<Id>;
62
+ /**
63
+ * The bar's top separator. `auto` (default) draws it against a sunken active
64
+ * work surface and otherwise follows `chromeDividers`; `hidden` suppresses
65
+ * the line, including a scroll divider.
66
+ */
67
+ separator?: 'auto' | 'hidden';
62
68
  /**
63
69
  * The line over the tab bar, where it meets the active scene. `scroll`
64
70
  * (default) shows it only while more of the scene's content lies below —
65
71
  * never at the scroll end or when the content fits — once
66
72
  * `wireScrollDividers` (`@kerfjs/ui/wire-scroll-dividers`) is wired above
67
73
  * the scaffold; unwired, it does not show. `always` shows it without the
68
- * wiring; `none` never, even when wired. The line is the bar's own 1px top
69
- * border, so no state moves the bar or a tab.
74
+ * wiring; `none` suppresses that scroll divider. A sunken active work
75
+ * surface still draws the separator unless `separator="hidden"`. The line
76
+ * is the bar's own 1px top border, so no state moves the bar or a tab.
70
77
  */
71
78
  chromeDividers?: 'scroll' | 'always' | 'none';
72
79
  className?: string;
@@ -81,6 +88,6 @@ interface TabNavigatorProps<Id extends string = string> {
81
88
  * On larger classes, promote the tabs to a `Workbench` rail or sidebar instead of
82
89
  * a bottom bar. See `docs/23-app-layouts.md` §3.4.
83
90
  */
84
- declare function TabNavigator<Id extends string>({ id, label, tabs, active, chromeDividers, className, slot, }: TabNavigatorProps<Id>): SafeHtml;
91
+ declare function TabNavigator<Id extends string>({ id, label, tabs, active, separator, chromeDividers, className, slot, }: TabNavigatorProps<Id>): SafeHtml;
85
92
 
86
93
  export { TabNavigator, type TabNavigatorProps, type TabNavigatorTab };
@@ -6,6 +6,7 @@ function TabNavigator({
6
6
  label,
7
7
  tabs,
8
8
  active,
9
+ separator = "auto",
9
10
  chromeDividers = "scroll",
10
11
  className = "",
11
12
  slot
@@ -17,6 +18,7 @@ function TabNavigator({
17
18
  id,
18
19
  "data-component": "tab-scaffold",
19
20
  "data-tab-scaffold-id": id,
21
+ "data-separator": separator,
20
22
  "data-chrome-dividers": chromeDividers === "always" || chromeDividers === "none" ? chromeDividers : void 0,
21
23
  slot,
22
24
  children: [