@kerfjs/ui 5.0.0-beta.76 → 5.0.0-beta.78

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 (147) hide show
  1. package/README.md +15 -0
  2. package/ai/application-ui-diagnostic-ids-v1.json +3 -0
  3. package/ai/component-catalog.json +333 -28
  4. package/ai/component-composition.json +224 -17
  5. package/ai/components/nav-stack.md +4 -3
  6. package/ai/components/select.md +1 -0
  7. package/ai/components/split-view.md +1 -0
  8. package/ai/components/surface-scaffold.md +11 -2
  9. package/ai/components/text.md +4 -2
  10. package/ai/components/toolbar-control-group.md +22 -3
  11. package/ai/components/toolbar-text.md +21 -4
  12. package/ai/components/toolbar.md +12 -4
  13. package/ai/public-api-signatures-v1.md +99 -127
  14. package/ai/skill.md +37 -3
  15. package/ai/webawesome-jsx-signatures-v1.md +1 -1
  16. package/dist/browser/nav-stack.js +2 -0
  17. package/dist/browser/split-view.js +2 -0
  18. package/dist/catalog-resources.d.ts +3 -3
  19. package/dist/catalog.d.ts +3 -3
  20. package/dist/catalog.js +6 -6
  21. package/dist/chunk-2GOCOLQL.js +270 -0
  22. package/dist/chunk-2GOCOLQL.js.map +1 -0
  23. package/dist/{chunk-ZVYA6TLR.js → chunk-4Y4L5YEI.js} +3 -1
  24. package/dist/chunk-4Y4L5YEI.js.map +1 -0
  25. package/dist/{chunk-XIZRXI7N.js → chunk-A7DYEERU.js} +13 -1
  26. package/dist/chunk-A7DYEERU.js.map +1 -0
  27. package/dist/{chunk-7QWIJEK5.js → chunk-DSLRNR43.js} +9 -8
  28. package/dist/chunk-DSLRNR43.js.map +1 -0
  29. package/dist/{chunk-AOAW55JN.js → chunk-DXQZ3BEO.js} +4 -1
  30. package/dist/chunk-DXQZ3BEO.js.map +1 -0
  31. package/dist/chunk-IKCUPTR2.js +170 -0
  32. package/dist/chunk-IKCUPTR2.js.map +1 -0
  33. package/dist/{chunk-HGSTMZZ6.js → chunk-JL4BBZP4.js} +5 -4
  34. package/dist/chunk-JL4BBZP4.js.map +1 -0
  35. package/dist/{chunk-IVLTEBBT.js → chunk-M6BWQMQY.js} +2 -2
  36. package/dist/{chunk-IVLTEBBT.js.map → chunk-M6BWQMQY.js.map} +1 -1
  37. package/dist/{chunk-72LLWYP2.js → chunk-NVKRZ5DI.js} +1 -1
  38. package/dist/chunk-NVKRZ5DI.js.map +1 -0
  39. package/dist/{chunk-GO2CZX4M.js → chunk-RRCD3A6G.js} +13 -5
  40. package/dist/chunk-RRCD3A6G.js.map +1 -0
  41. package/dist/{chunk-5PAVAR4B.js → chunk-VHXHLZ6R.js} +10 -1
  42. package/dist/chunk-VHXHLZ6R.js.map +1 -0
  43. package/dist/collapsible-panel.js +2 -2
  44. package/dist/index.d.ts +4 -3
  45. package/dist/index.js +9 -8
  46. package/dist/list-header.d.ts +1 -0
  47. package/dist/list-header.js +1 -1
  48. package/dist/nav-stack-B1Qc2e5q.d.ts +101 -0
  49. package/dist/nav-stack.d.ts +12 -93
  50. package/dist/nav-stack.js +5 -1
  51. package/dist/pane.d.ts +5 -2
  52. package/dist/pane.js +1 -1
  53. package/dist/select-register.js +3 -0
  54. package/dist/select-register.js.map +1 -1
  55. package/dist/select.d.ts +1 -0
  56. package/dist/select.js +1 -1
  57. package/dist/split-view.d.ts +9 -4
  58. package/dist/split-view.js +7 -3
  59. package/dist/split-view.js.map +1 -1
  60. package/dist/styles/components/actions/toolbar/toolbar.css +32 -0
  61. package/dist/styles/components/actions/toolbar-control-group/toolbar-control-group.css +105 -7
  62. package/dist/styles/components/actions/toolbar-text/toolbar-text.css +17 -0
  63. package/dist/styles/components/layout/pane/pane.css +7 -0
  64. package/dist/styles/components/navigation/nav-stack/nav-stack.css +17 -12
  65. package/dist/styles/components/navigation/tab-scaffold/tab-scaffold.css +17 -0
  66. package/dist/styles/components/surfaces/surface-scaffold/surface-scaffold.css +55 -0
  67. package/dist/styles/components/typography/text/text.css +19 -0
  68. package/dist/styles/nav-stack.css +17 -12
  69. package/dist/styles/pane.css +7 -0
  70. package/dist/styles/surface-scaffold.css +55 -0
  71. package/dist/styles/tab-scaffold.css +17 -0
  72. package/dist/styles/text.css +19 -0
  73. package/dist/styles/toolbar-control-group.css +105 -7
  74. package/dist/styles/toolbar-text.css +17 -0
  75. package/dist/styles/toolbar.css +32 -0
  76. package/dist/surface-scaffold-register.d.ts +4 -0
  77. package/dist/surface-scaffold-register.js +59 -0
  78. package/dist/surface-scaffold-register.js.map +1 -0
  79. package/dist/surface-scaffold.d.ts +10 -2
  80. package/dist/surface-scaffold.js +1 -1
  81. package/dist/tab-scaffold.d.ts +2 -0
  82. package/dist/tab-scaffold.js +1 -0
  83. package/dist/tab-scaffold.js.map +1 -1
  84. package/dist/text.d.ts +5 -2
  85. package/dist/text.js +1 -1
  86. package/dist/toolbar-control-group.d.ts +14 -2
  87. package/dist/toolbar-control-group.js +1 -1
  88. package/dist/toolbar-text.d.ts +10 -2
  89. package/dist/toolbar-text.js +1 -1
  90. package/dist/wire-nav-stack.js +58 -22
  91. package/dist/wire-nav-stack.js.map +1 -1
  92. package/dist/wire-toolbar-visibility.d.ts +13 -0
  93. package/dist/wire-toolbar-visibility.js +3 -0
  94. package/dist/wire-toolbar-visibility.js.map +1 -0
  95. package/dist/workbench.d.ts +5 -13
  96. package/dist/workbench.js +1 -1
  97. package/docs/accessibility.md +15 -0
  98. package/docs/collapsible-panel.md +4 -0
  99. package/docs/component-contract.md +38 -0
  100. package/docs/component-selection.md +56 -2
  101. package/docs/design/templates/toolbar/width-visibility-320-dark.svg +1 -0
  102. package/docs/design/templates/toolbar/width-visibility-320.svg +1 -0
  103. package/docs/design/templates/toolbar/width-visibility-600-dark.svg +1 -0
  104. package/docs/design/templates/toolbar/width-visibility-600.svg +1 -0
  105. package/docs/design/templates/toolbar-control-group/icon-tile-brand-dark.svg +1 -0
  106. package/docs/design/templates/toolbar-control-group/icon-tile-brand.svg +1 -0
  107. package/docs/design/templates/toolbar-control-group/icon-tile-danger-dark.svg +1 -0
  108. package/docs/design/templates/toolbar-control-group/icon-tile-danger.svg +1 -0
  109. package/docs/design/templates/toolbar-control-group/icon-tile-neutral-dark.svg +1 -0
  110. package/docs/design/templates/toolbar-control-group/icon-tile-neutral.svg +1 -0
  111. package/docs/design/templates/toolbar-control-group/icon-tile-success-dark.svg +1 -0
  112. package/docs/design/templates/toolbar-control-group/icon-tile-success.svg +1 -0
  113. package/docs/design/templates/toolbar-control-group/icon-tile-warning-dark.svg +1 -0
  114. package/docs/design/templates/toolbar-control-group/icon-tile-warning.svg +1 -0
  115. package/docs/design/templates/toolbar-control-group-dark.svg +1 -1
  116. package/docs/design/templates/toolbar-control-group.svg +1 -1
  117. package/docs/design/templates/toolbar-dark.svg +1 -1
  118. package/docs/design/templates/toolbar-text/dark-tone-dark.svg +1 -0
  119. package/docs/design/templates/toolbar-text/dark-tone.svg +1 -0
  120. package/docs/design/templates/toolbar-text-dark.svg +1 -1
  121. package/docs/design/templates/toolbar-text.svg +1 -1
  122. package/docs/design/templates/toolbar.svg +1 -1
  123. package/docs/design/templates.md +2 -0
  124. package/docs/layout.md +32 -1
  125. package/docs/nav-stack.md +35 -19
  126. package/docs/recipes.md +5 -1
  127. package/docs/split-view.md +7 -4
  128. package/docs/surface-scaffold.md +46 -0
  129. package/docs/tab-scaffold.md +57 -0
  130. package/docs/ui-doctor.md +23 -0
  131. package/docs/workbench.md +3 -2
  132. package/doctor/component-style-checks.mjs +224 -0
  133. package/doctor/index.mjs +42 -13
  134. package/llms.txt +34 -0
  135. package/package.json +11 -2
  136. package/ux-demo/recipes/list-detail-dialog.tsx +4 -0
  137. package/ux-demo/recipes/navigation-stack.tsx +2 -2
  138. package/dist/chunk-5PAVAR4B.js.map +0 -1
  139. package/dist/chunk-72LLWYP2.js.map +0 -1
  140. package/dist/chunk-7QWIJEK5.js.map +0 -1
  141. package/dist/chunk-AOAW55JN.js.map +0 -1
  142. package/dist/chunk-GO2CZX4M.js.map +0 -1
  143. package/dist/chunk-HGSTMZZ6.js.map +0 -1
  144. package/dist/chunk-SJY7KC45.js +0 -123
  145. package/dist/chunk-SJY7KC45.js.map +0 -1
  146. package/dist/chunk-XIZRXI7N.js.map +0 -1
  147. package/dist/chunk-ZVYA6TLR.js.map +0 -1
package/docs/recipes.md CHANGED
@@ -88,7 +88,11 @@ its label still names the modal — and the close button uses
88
88
  `data-dialog="close"`. Web Awesome keeps modal focus and Escape, and the thin
89
89
  recipe adapter restores the invoking control after the hide event.
90
90
  `DialogSurface` chooses the large modal with no body inset, because the panes
91
- own their geometry. On compact devices the dialog becomes a full-screen sheet
91
+ own their geometry. Its explicit 16px `viewportGutter` and `maxHeight="viewport"`
92
+ bound the roomy modal to the dynamic viewport; the compact fullscreen
93
+ presentation retains its existing geometry. Application render state chooses
94
+ the presentation and can likewise choose gutter/cap values for a compact modal.
95
+ On compact devices the dialog becomes a full-screen sheet
92
96
  with its own labeled header and footer actions, and `SplitView` becomes a
93
97
  `NavStack` drill-down whose back control the application pops. The
94
98
  application owns open state, selection, the pushed detail, dismissal policy,
@@ -86,15 +86,18 @@ resizable={{
86
86
 
87
87
  `compactStack` configures the compact `NavStack`: `toolbarConfig`, `backIcon`,
88
88
  `backText`, `hideToolbar`, the persistent `bottomToolbar`, and `chromeDividers`
89
- forward to the stack, and `list` / `detail` give each view its own top-toolbar groups
90
- (`leading`, `center`, trailing `toolbar`) and `bottomToolbar`:
89
+ forward to the stack, and `list` / `detail` give each view its own structured
90
+ top `toolbar` (`leading`, `center`, `trailing`) and `bottomToolbar`:
91
91
 
92
92
  ```tsx
93
93
  compactStack={{
94
94
  backText: "Threads",
95
95
  toolbarConfig: { headingLevel: 1 },
96
- list: { toolbar: <ComposeGroup /> },
97
- detail: { toolbar: <ReplyGroup />, bottomToolbar: <MessageActions /> },
96
+ list: { toolbar: { trailing: <ComposeGroup /> } },
97
+ detail: {
98
+ toolbar: { trailing: <ReplyGroup /> },
99
+ bottomToolbar: { label: "Message actions", leading: <MessageActions /> },
100
+ },
98
101
  }}
99
102
  ```
100
103
 
@@ -12,6 +12,50 @@ Wrap one `wa-dialog` with `DialogSurface`. Choose `size` (`small`, `medium`, or
12
12
  `bodyInset` / `footerInset` (`none`, `compact`, or `comfortable`). The defaults
13
13
  match Kerf's medium modal, 8px body, and 16px footer rhythm.
14
14
 
15
+ The modal size presets have preferred widths of 400px, 560px, and 800px,
16
+ respectively, before the viewport width cap. Set `preferredWidth` to a branded
17
+ `CssLength` when the desktop shell needs another width. It takes precedence over
18
+ `size`, while the viewport cap still applies. For example, 832px and 480px
19
+ desktop preferences can share the same responsive dialog without changing its
20
+ resource tracks or preview aspect sizing.
21
+
22
+ ### Modal viewport bounds
23
+
24
+ `viewportGutter` accepts a branded `CssLength` from `@kerfjs/ui/css-values`.
25
+ It sets the minimum horizontal clearance; `preferredWidth` overrides the `size`
26
+ preset when supplied. `maxHeight` accepts a branded length or `"viewport"`; explicit caps are
27
+ clamped to the dynamic viewport height minus twice the gutter (16px per edge
28
+ when the gutter is omitted). Negative gutters/caps clamp to zero. Omitting
29
+ both options preserves Web Awesome's existing width and phone height caps.
30
+ These options apply only to `presentation="modal"`; side sheets and fullscreen
31
+ surfaces keep their own geometry.
32
+
33
+ ```tsx
34
+ import '@kerfjs/ui/surface-scaffold/register';
35
+ import { remify } from '@kerfjs/ui/css-values';
36
+ import { DialogSurface } from '@kerfjs/ui/surface-scaffold';
37
+
38
+ <DialogSurface preferredWidth={remify(832)} viewportGutter={remify(8)} maxHeight="viewport" bodyInset="none">
39
+ <wa-dialog label="Close workspace">{/* Application-owned content and footer */}</wa-dialog>
40
+ </DialogSurface>;
41
+ ```
42
+
43
+ This gives a desktop modal an 832px preferred width and a phone modal an 8px edge gutter with a `calc(100dvh - 16px)` height
44
+ cap without application selectors on Kerf roots or Web Awesome parts. A cap
45
+ is not a fixed height: short content stays naturally sized, and taller content
46
+ scrolls in Web Awesome's body while its header/footer remain outside that
47
+ scroll owner. Choosing a larger cap cannot guarantee that arbitrary content
48
+ fits without scrolling; preview tracks, canvas aspect ratios and content sizing
49
+ remain application-owned.
50
+
51
+ Responsive policy is explicit application state: read `deviceClass()` during
52
+ the render to choose the gutter, cap or presentation. For example use
53
+ `viewportGutter={remify(device.value.handset ? 8 : 16)}` with `maxHeight="viewport"`.
54
+ The scaffold introduces no hidden breakpoint or automatic fullscreen switch.
55
+ An application can configure device-class breakpoints when its phone policy
56
+ differs from the default. Prefer a typed cap such as `maxHeight={remify(640)}`
57
+ when the modal should stop growing before the viewport bound.
58
+
15
59
  Treat a dialog body as a `List` by default. When its rows or sections own their
16
60
  standard list geometry, set `bodyInset="none"` so the dialog does not add a
17
61
  second inset. Bare prose is still a list child: wrap it in `ListInsetText` so its
@@ -26,3 +70,5 @@ padding for a child that already owns row insets, `compact` uses 4px, and
26
70
  preserve `data-morph-skip-children` when its upgraded light-DOM items must retain
27
71
  identity across Kerf rerenders. Import `@kerfjs/ui/popup-menu/register` once for
28
72
  the focus handoff fix when wrapping a raw `wa-dropdown`.
73
+
74
+ Import `@kerfjs/ui/surface-scaffold/register` once when using `DialogSurface`: it registers `wa-dialog` and repairs Web Awesome 3.12 native dialog names before the first modal opening. Headed dialogs reference the rendered title, including live slotted labels; headerless dialogs use the public `label`. Explicit host `aria-label` and pre-existing native names are preserved. Geometry-only markup stays CSS-free and does not register Web Awesome implicitly.
@@ -59,6 +59,63 @@ truncate horizontally with an ellipsis when a destination name exceeds its
59
59
  share of the bar. On larger device classes, promote the tab set to a
60
60
  `Workbench` rail or a persistent sidebar instead of a bottom bar.
61
61
 
62
+ Set `deepInset: true` on a plain tab scene to add an 8px outer inline gutter
63
+ to its scrolling content, giving ordinary content items a 16px edge inset.
64
+ The default is `false`. A scene whose only child is a `Pane` or layout lets
65
+ that child own its content gutter: for a nested `NavStack`, set
66
+ `pane: { deepInset: true }` on its views instead. The scaffold bar and a
67
+ stack's top toolbar remain full width.
68
+
69
+ ## A NavStack with a top toolbar and Pane
70
+
71
+ Put the `NavStack` directly in a tab's `content`. Give each view a structured
72
+ `toolbar` and `pane` configuration. Leave `bottomToolbar` off both the views
73
+ and the stack: the scaffold's tab bar is the only bottom chrome. The view's
74
+ Pane owns scrolling, while the stack keeps its toolbar above it and the
75
+ scaffold keeps the tab bar below it.
76
+
77
+ ```tsx
78
+ import { List } from "@kerfjs/ui/list";
79
+ import { ListItem } from "@kerfjs/ui/list-item";
80
+ import { NavStack, type NavStackView } from "@kerfjs/ui/nav-stack";
81
+
82
+ const projectViews: NavStackView[] = [
83
+ {
84
+ key: "projects",
85
+ toolbar: { title: "Projects" },
86
+ pane: { appearance: "sunken" },
87
+ content: <List><ListItem label="Project Atlas" /></List>,
88
+ },
89
+ ];
90
+
91
+ <TabScaffold
92
+ id="app"
93
+ label="Sections"
94
+ active={active.value}
95
+ tabs={[
96
+ {
97
+ id: "projects",
98
+ label: "Projects",
99
+ content: (
100
+ <NavStack
101
+ id="projects-stack"
102
+ label="Projects navigation"
103
+ views={projectViews}
104
+ />
105
+ ),
106
+ },
107
+ { id: "search", label: "Search", content: <SearchScene /> },
108
+ ]}
109
+ />;
110
+ ```
111
+
112
+ The app owns each stack's view array and the scaffold's active tab. Call
113
+ `wireTabScaffold` for tab selection and `wireNavStack` for back navigation
114
+ and transitions, each on its own component root. Push a detail by appending
115
+ a view with its own `toolbar` and `pane`; switching tabs keeps that view
116
+ mounted. The catalog's **NavStack inside a tab** example demonstrates the
117
+ push, tab switch, and return flow.
118
+
62
119
  Set a tab's `appearance: "sunken"` when its scene owns scrolling and should
63
120
  paint the lowered surface behind short or long content. For a scene whose
64
121
  only child is a `Pane` or `NavStack`, set appearance on that child's scroll
package/docs/ui-doctor.md CHANGED
@@ -58,6 +58,29 @@ without that marker remain app-owned.
58
58
  The analyzer resolves a cataloged application component imported through an
59
59
  exact or wildcard TypeScript `paths` alias from the nearest consumer tsconfig,
60
60
  so the same root hook-class check applies in those files.
61
+
62
+ ## Component style naming
63
+
64
+ The analyzer stage also checks colocated JSX/TSX components and stylesheets.
65
+ When a stylesheet has a same-basename component module, or exactly one
66
+ component module imports it, Doctor checks that the stylesheet filename is
67
+ the component's kebab-case name (`KUI-D031`). A component that renders its own
68
+ HTML element must put that kebab-case class on rendered markup (`KUI-D030`).
69
+ For example, `WorkspaceHeader` pairs with `workspace-header.css` and renders
70
+ `.workspace-header`. Thin components that only compose other components need
71
+ no root class. Shared stylesheets declared in `ownershipGroups` are exempt
72
+ from the naming check.
73
+
74
+ Doctor reports `KUI-D032` when a paired stylesheet selects another local
75
+ component's kebab-case class or its BEM descendants or modifiers. Move that
76
+ rule to the owning component's stylesheet or use the component's public
77
+ configuration. This check uses source declarations and CSS selectors; it does
78
+ not execute component code. In changed mode, it reads other component sources
79
+ to resolve ownership but reports only pairs containing a selected path. For
80
+ broader cross-component ownership, including
81
+ cataloged package components and hook classes, enable `ownership: "component"`
82
+ with `implicitComponentOwnership: true` as described above.
83
+
61
84
  Set `"ownership": "component"` in `.kerf-ui-doctor.json` to enforce the same
62
85
  boundary between a consumer package's own cataloged components. The default is
63
86
  `"package"`. Selection-only entries participate too. A component owns its
package/docs/workbench.md CHANGED
@@ -167,8 +167,9 @@ into `.kui-resizable-region__content`.
167
167
 
168
168
  A panel with `navStack` hosts a controlled `NavStack` in place of the static
169
169
  `content` and `Pane` slots. Give it a `toolbar` with a `toggle`; each
170
- `navStack.views` entry then supplies its own title, leading/center/trailing
171
- groups, fixed `header`, and scrolling `content`. The active view's toolbar is
170
+ `navStack.views` entry then supplies its own structured `toolbar` with a title
171
+ and leading/center/trailing groups, optional `header` / `footer` and `pane`
172
+ configuration, and scrolling `content`. The active view's toolbar is
172
173
  the panel's only toolbar row, with the standard toggle always last. On
173
174
  collapse, marked groups from the active view and the toggle relocate to the
174
175
  work-area toolbar; the view stack stays mounted. The app owns the view array
@@ -0,0 +1,224 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { basename, dirname, extname, relative, resolve } from 'node:path';
3
+
4
+ import postcss from 'postcss';
5
+ import ts from 'typescript';
6
+
7
+ import { isUiTraversalExcluded } from '../traversal-exclusions.mjs';
8
+
9
+ const scriptExtensions = new Set(['.tsx', '.jsx']);
10
+ const kebab = (name) =>
11
+ name.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
12
+ const componentName = (name) => /^[A-Z][A-Za-z0-9]*$/.test(name);
13
+ const classNames = (value) =>
14
+ [...value.matchAll(/(?:^|\s)([a-z][a-z0-9_-]*)(?=\s|$)/g)].map(
15
+ (match) => match[1],
16
+ );
17
+ const block = (name) => name.split(/__|--/)[0];
18
+
19
+ function componentFacts(file, source) {
20
+ const syntax = file.endsWith('.tsx') ? ts.ScriptKind.TSX : ts.ScriptKind.JSX;
21
+ const ast = ts.createSourceFile(
22
+ file,
23
+ source,
24
+ ts.ScriptTarget.Latest,
25
+ true,
26
+ syntax,
27
+ );
28
+ const components = new Map();
29
+ const visit = (node, owner) => {
30
+ if (
31
+ ts.isFunctionDeclaration(node) &&
32
+ node.name &&
33
+ componentName(node.name.text)
34
+ )
35
+ owner = node.name.text;
36
+ if (
37
+ ts.isVariableDeclaration(node) &&
38
+ ts.isIdentifier(node.name) &&
39
+ componentName(node.name.text)
40
+ )
41
+ owner = node.name.text;
42
+ if (
43
+ owner &&
44
+ (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node))
45
+ ) {
46
+ if (ts.isIdentifier(node.tagName) && /^[a-z]/.test(node.tagName.text)) {
47
+ if (components.has(owner)) components.get(owner).intrinsic = true;
48
+ for (const attribute of node.attributes.properties) {
49
+ if (
50
+ !ts.isJsxAttribute(attribute) ||
51
+ !['class', 'className'].includes(attribute.name.text)
52
+ )
53
+ continue;
54
+ const value = attribute.initializer;
55
+ const literal =
56
+ value && ts.isStringLiteral(value)
57
+ ? value.text
58
+ : value &&
59
+ ts.isJsxExpression(value) &&
60
+ value.expression &&
61
+ (ts.isStringLiteral(value.expression) ||
62
+ ts.isNoSubstitutionTemplateLiteral(value.expression))
63
+ ? value.expression.text
64
+ : undefined;
65
+ if (literal)
66
+ for (const name of classNames(literal))
67
+ components.get(owner)?.add(name);
68
+ }
69
+ }
70
+ }
71
+ ts.forEachChild(node, (child) => visit(child, owner));
72
+ };
73
+ const declarations = (node) => {
74
+ if (
75
+ ts.isFunctionDeclaration(node) &&
76
+ node.name &&
77
+ componentName(node.name.text)
78
+ )
79
+ components.set(node.name.text, new Set());
80
+ if (
81
+ ts.isVariableDeclaration(node) &&
82
+ ts.isIdentifier(node.name) &&
83
+ componentName(node.name.text)
84
+ )
85
+ components.set(node.name.text, new Set());
86
+ ts.forEachChild(node, declarations);
87
+ };
88
+ declarations(ast);
89
+ visit(ast);
90
+ return components;
91
+ }
92
+
93
+ function diagnostic(id, file, line, message, action) {
94
+ return {
95
+ id,
96
+ severity: 'error',
97
+ stage: 'analyzer',
98
+ message,
99
+ location: { file, line, column: 1 },
100
+ action,
101
+ documentation: '@kerfjs/ui/docs/ui-doctor.md#component-style-naming',
102
+ };
103
+ }
104
+
105
+ export async function checkComponentStyles(
106
+ packageRoot,
107
+ inputs,
108
+ ownershipGroups = [],
109
+ selectedPaths,
110
+ ) {
111
+ const scripts = new Map();
112
+ const styles = new Map();
113
+ const importedBy = new Map();
114
+ for (const file of inputs) {
115
+ if (isUiTraversalExcluded(packageRoot, file)) continue;
116
+ const extension = extname(file);
117
+ if (scriptExtensions.has(extension)) {
118
+ const source = await readFile(file, 'utf8');
119
+ scripts.set(file, componentFacts(file, source));
120
+ for (const match of source.matchAll(
121
+ /\bimport\s*(?:['"]|[^;]*?\bfrom\s*['"])(\.[^'"]+\.css)['"]/g,
122
+ )) {
123
+ const style = resolve(dirname(file), match[1]);
124
+ const importers = importedBy.get(style) ?? [];
125
+ importers.push(file);
126
+ importedBy.set(style, importers);
127
+ }
128
+ } else if (extension === '.css')
129
+ styles.set(file, await readFile(file, 'utf8'));
130
+ }
131
+ const owners = new Map();
132
+ for (const [file, components] of scripts)
133
+ for (const [name, classes] of components)
134
+ if (classes.has(kebab(name)))
135
+ owners.set(kebab(name), { file, name, classes });
136
+ const diagnostics = [];
137
+ for (const [file, source] of styles) {
138
+ if (
139
+ ownershipGroups.some((group) =>
140
+ group.styleSources?.some(
141
+ (style) => resolve(packageRoot, style) === file,
142
+ ),
143
+ )
144
+ )
145
+ continue;
146
+ const stem = basename(file, '.css');
147
+ const pair =
148
+ [...scriptExtensions]
149
+ .map((extension) => resolve(dirname(file), `${stem}${extension}`))
150
+ .find((candidate) => scripts.has(candidate)) ??
151
+ (importedBy.get(file)?.length === 1
152
+ ? importedBy.get(file)[0]
153
+ : undefined);
154
+ if (!pair) continue;
155
+ const path = relative(packageRoot, file).replaceAll('\\', '/');
156
+ if (
157
+ selectedPaths?.length &&
158
+ !selectedPaths.includes(path) &&
159
+ !selectedPaths.includes(relative(packageRoot, pair).replaceAll('\\', '/'))
160
+ )
161
+ continue;
162
+ const components = scripts.get(pair);
163
+ const matching = [...components].find(([name]) => kebab(name) === stem);
164
+ if (!matching) {
165
+ const names = [...components.keys()];
166
+ if (names.length)
167
+ diagnostics.push(
168
+ diagnostic(
169
+ 'KUI-D031',
170
+ path,
171
+ 1,
172
+ `${basename(file)} is paired with ${basename(pair)}, but its component ${names[0]} calls for ${kebab(names[0])}.css.`,
173
+ `Rename the stylesheet and its import to ${kebab(names[0])}.css, or rename the component and source pair.`,
174
+ ),
175
+ );
176
+ continue;
177
+ }
178
+ const [name, renderedClasses] = matching;
179
+ if (renderedClasses.intrinsic && !renderedClasses.has(stem))
180
+ diagnostics.push(
181
+ diagnostic(
182
+ 'KUI-D030',
183
+ relative(packageRoot, pair).replaceAll('\\', '/'),
184
+ 1,
185
+ `${name} must render its own .${stem} root class.`,
186
+ `Add class="${stem}" to an element rendered by ${name}, then select that class in ${basename(file)}.`,
187
+ ),
188
+ );
189
+ let sheet;
190
+ try {
191
+ sheet = postcss.parse(source, { from: file });
192
+ } catch {
193
+ continue;
194
+ }
195
+ sheet.walkRules((rule) => {
196
+ if (
197
+ rule.parent?.type === 'atrule' &&
198
+ /keyframes$/i.test(rule.parent.name)
199
+ )
200
+ return;
201
+ const selected = [
202
+ ...new Set(
203
+ [...rule.selector.matchAll(/\.([a-z][a-z0-9_-]*)/gi)].map(
204
+ (match) => match[1],
205
+ ),
206
+ ),
207
+ ];
208
+ for (const className of selected) {
209
+ const foreign = owners.get(block(className));
210
+ if (!foreign || foreign.name === name) continue;
211
+ diagnostics.push(
212
+ diagnostic(
213
+ 'KUI-D032',
214
+ path,
215
+ rule.source?.start?.line ?? 1,
216
+ `${basename(file)} selects .${className}, which belongs to ${foreign.name} in ${relative(packageRoot, foreign.file).replaceAll('\\', '/')}.`,
217
+ `Move this rule to ${kebab(foreign.name)}.css or configure ${foreign.name} through its public API.`,
218
+ ),
219
+ );
220
+ }
221
+ });
222
+ }
223
+ return diagnostics;
224
+ }
package/doctor/index.mjs CHANGED
@@ -27,6 +27,7 @@ import {
27
27
  UI_ANALYSIS_RULES,
28
28
  } from '../analyzer/index.mjs';
29
29
  import { evaluateUi, UI_EVALUATION_RULES } from '../evaluator/index.mjs';
30
+ import { checkComponentStyles } from './component-style-checks.mjs';
30
31
  import {
31
32
  isForeignRuleDefinitionDiagnostic,
32
33
  projectConsumerCoreConfig,
@@ -53,6 +54,18 @@ export const UI_DOCTOR_RULES = Object.freeze({
53
54
  severity: 'error',
54
55
  title: 'Local component catalog is stale or invalid',
55
56
  },
57
+ 'KUI-D030': {
58
+ severity: 'error',
59
+ title: 'Component root class does not match its name',
60
+ },
61
+ 'KUI-D031': {
62
+ severity: 'error',
63
+ title: 'Component stylesheet does not match its name',
64
+ },
65
+ 'KUI-D032': {
66
+ severity: 'error',
67
+ title: 'Stylesheet selects another component class',
68
+ },
56
69
  });
57
70
 
58
71
  const sourceExtensions = new Set([
@@ -480,7 +493,7 @@ async function installedPackageVersion(root, name) {
480
493
  }
481
494
 
482
495
  async function cacheKey(root, packageRoot, mode, paths, config) {
483
- const files = await collectInputs(packageRoot, paths);
496
+ const files = await collectInputs(packageRoot);
484
497
  for (const directory of new Set([root, packageRoot])) {
485
498
  for (const name of [
486
499
  'package.json',
@@ -521,6 +534,14 @@ async function cacheKey(root, packageRoot, mode, paths, config) {
521
534
  }),
522
535
  )
523
536
  .update(await readFile(fileURLToPath(import.meta.url)));
537
+ hash.update(
538
+ await readFile(
539
+ resolve(
540
+ dirname(fileURLToPath(import.meta.url)),
541
+ 'component-style-checks.mjs',
542
+ ),
543
+ ),
544
+ );
524
545
  for (const file of files)
525
546
  hash.update(portablePath(root, file)).update(await readFile(file));
526
547
  return hash.digest('hex');
@@ -1215,18 +1236,26 @@ export async function runUiDoctor({
1215
1236
  signal,
1216
1237
  })
1217
1238
  : (async () => ({
1218
- diagnostics: normalizeAnalyzer(
1219
- await analyzeUiProject({
1220
- root: packageRoot,
1221
- paths: selectedPaths,
1222
- ownership: config.ownership ?? 'package',
1223
- ownershipContext: config.ownershipContext ?? 'subject',
1224
- implicitComponentOwnership:
1225
- config.implicitComponentOwnership ?? false,
1226
- ownershipGroups: config.ownershipGroups ?? [],
1227
- knownRules: [...registeredRuleIds, ...eslintRuleIds],
1228
- }),
1229
- ),
1239
+ diagnostics: [
1240
+ ...normalizeAnalyzer(
1241
+ await analyzeUiProject({
1242
+ root: packageRoot,
1243
+ paths: selectedPaths,
1244
+ ownership: config.ownership ?? 'package',
1245
+ ownershipContext: config.ownershipContext ?? 'subject',
1246
+ implicitComponentOwnership:
1247
+ config.implicitComponentOwnership ?? false,
1248
+ ownershipGroups: config.ownershipGroups ?? [],
1249
+ knownRules: [...registeredRuleIds, ...eslintRuleIds],
1250
+ }),
1251
+ ),
1252
+ ...(await checkComponentStyles(
1253
+ packageRoot,
1254
+ await collectInputs(packageRoot),
1255
+ config.ownershipGroups ?? [],
1256
+ selectedPaths,
1257
+ )),
1258
+ ],
1230
1259
  }))(),
1231
1260
  signal,
1232
1261
  ),
package/llms.txt CHANGED
@@ -19,6 +19,10 @@ the folder modules, and CSS only imports their owner stylesheets.
19
19
 
20
20
  ### Shared semantic tones
21
21
 
22
+ Use `ToolbarText tone="dark"` on a loud or photo-backed toolbar surface to
23
+ match dark control groups. It applies to read-only headings and actionable
24
+ titles at every size; default tone retains existing identity and heading colors.
25
+
22
26
  `SemanticTone` is the shared color-role type for `Badge`, `Chip`, `StateBanner`,
23
27
  and semantic `ContentItem` appearances: `neutral`, `info`, `pop`, `success`,
24
28
  `warning`, and `danger`. Import it as a type from `@kerfjs/ui`,
@@ -342,6 +346,11 @@ headingLevel`, `TokenSearchField clearIcon`, `ContentItem frame="framed"` for a
342
346
  compact, prominent, display, and code roles instead of global utility classes. It passes native attributes
343
347
  through; block variants own the standard transparent border plus item padding, while `span` owns none. Keep
344
348
  toolbar titles on `ToolbarText` and list-aligned bare prose on `ListInsetText`.
349
+ Text supports `controlMargins` with canonical physical Sides for an exterior 8px control gutter; it preserves border/padding and avoids an isolated Row wrapper. Avoid stacking it with parent insets. Inline spans retain native inline margin behavior.
350
+
351
+ `ToolbarControlGroup.tileTone` selects `neutral`, `brand`, `success`, `warning`, or `danger` quiet fill, matching quiet border, and on-quiet foreground for a non-interactive icon tile. Compose a contained `single` group whose only child is a direct decorative LucideIcon (`aria-hidden="true"`). Omission preserves the normal group palette. The prop does not recolor interactive, mixed, multi-control, or borderless groups; keep real actions in controls with their existing focus/hover/pressed treatments.
352
+ Expanded search groups accept `expandedOverflow="visible"` to anchor app-owned suggestion/date/help surfaces below their relative root; collapsed/default groups retain existing clipping. Apps own surface placement/dismissal and use `data-token-search-keep-open` or `collapsible.keepOpenOn` for focus.
353
+
345
354
  - **Space by the five-step scale, by relationship not by eye.** `0`
346
355
  `--kui-space-none`, `4px` `--kui-space-2xs`, `8px` `--kui-space-xs` (inside a
347
356
  group), `16px` `--kui-space-m`, `24px` `--kui-space-l` (between major differing
@@ -533,3 +542,28 @@ shell, `@kerfjs/ui/collapsible-panel` (`CollapsiblePanel`, `CollapsiblePanelTogg
533
542
  - [README.md](./README.md): package overview and the component subpath table.
534
543
 
535
544
  Neutral lowered surfaces and decorative neutral fills/borders are translucent theme colors; compose nested public components directly. Default/raised canvases, foregrounds, focus indicators, and loud contrast-bearing fills remain opaque. Do not assign depth-specific component tokens in demo wrappers.
545
+
546
+ Navigation transition continuity: `wireNavStack` slides the incoming view over the preserved previous view on push and the outgoing view off on pop, cross-fades both live chrome surfaces with inert snapshots, and settles/cancels an interrupted transition before starting the next. Reduced motion and duration zero settle immediately.
547
+
548
+ For a tab scene with drill-down, put a `NavStack` directly in `TabScaffoldTab.content`, configure each view's top `toolbar` and `pane`, and omit `bottomToolbar` at both levels so the scaffold bar remains the only bottom chrome. Wire both components on their own roots. The catalog's **NavStack inside a tab** example exercises push and tab switching.
549
+
550
+ `Pane.deepInset` (and `PaneConfig.deepInset`) or `TabScaffoldTab.deepInset` on a plain scene adds an 8px outer gutter to the scroll content, so ordinary 8px item margins land 16px from the edge. Nested Row/List/Grid spacing stays at its default. Set a nested scene's Pane option instead of insetting its entire NavStack or toolbar.
551
+
552
+ `ToolbarText` and `ToolbarControlGroup` accept typed `hideBelow`/`showBelow`
553
+ lengths (for example `px(416)`). Wire `wireToolbarVisibility(root)` from
554
+ `@kerfjs/ui/wire-toolbar-visibility` and dispose on unmount. Thresholds use the
555
+ nearest Toolbar content-box width: hideBelow hides strictly below, showBelow
556
+ shows strictly below, with complementary behavior at equality. Percentages use
557
+ that content width; em and custom properties use the item's own context. Invalid,
558
+ negative, unresolved, or contradictory values leave the item visible under this
559
+ width policy. Keep primary actions visible and offer a complementary overflow
560
+ group. See `docs/component-contract.md` "Toolbar width visibility" for lifecycle
561
+ details.
562
+
563
+ Select menus use native viewport shifting with 10px gutters: default menus grow to content (up to 20rem), while `fitMenu` matches trigger width and long labels wrap. Keep popup sizing and position repairs out of consumer CSS.
564
+
565
+ Modal `DialogSurface` bounds use typed `viewportGutter` and `maxHeight` (`CssLength` or `"viewport"`); explicit caps stay within the dynamic viewport minus both gutters. Omitted props preserve Web Awesome defaults. Application render state chooses responsive values, and preview layout remains application-owned.
566
+
567
+ Import `@kerfjs/ui/surface-scaffold/register` once when using `DialogSurface`: it registers `wa-dialog` and repairs Web Awesome 3.12 native dialog names before the first modal opening. Headed dialogs reference the rendered title, including live slotted labels; headerless dialogs use the public `label`. Explicit host `aria-label` and pre-existing native names are preserved. Geometry-only markup stays CSS-free and does not register Web Awesome implicitly.
568
+
569
+ Toolbar visibility measurement probes and invisible state children are helper-owned and survive Kerf rerenders through `data-morph-preserve`. Actual CSS visibility stays stable while rendering removes transient parent markers, preventing geometry-driven render feedback and repeated probe recreation. Toolbar replacement, explicit internal-node removal and disposal retain their existing behavior.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kerfjs/ui",
3
- "version": "5.0.0-beta.76",
3
+ "version": "5.0.0-beta.78",
4
4
  "description": "Accessible, composable UI primitives for kerf applications.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -40,7 +40,8 @@
40
40
  "./dist/browser/*.js",
41
41
  "./dist/select-register.js",
42
42
  "./dist/popup-menu-register.js",
43
- "./dist/help-tags-register.js"
43
+ "./dist/help-tags-register.js",
44
+ "./dist/surface-scaffold-register.js"
44
45
  ],
45
46
  "exports": {
46
47
  ".": {
@@ -90,6 +91,10 @@
90
91
  "browser": "./dist/browser/surface-scaffold.js",
91
92
  "import": "./dist/surface-scaffold.js"
92
93
  },
94
+ "./surface-scaffold/register": {
95
+ "types": "./dist/surface-scaffold-register.d.ts",
96
+ "import": "./dist/surface-scaffold-register.js"
97
+ },
93
98
  "./floating-toolbar": {
94
99
  "types": "./dist/floating-toolbar.d.ts",
95
100
  "browser": "./dist/browser/floating-toolbar.js",
@@ -215,6 +220,10 @@
215
220
  "types": "./dist/wire-scroll-dividers.d.ts",
216
221
  "import": "./dist/wire-scroll-dividers.js"
217
222
  },
223
+ "./wire-toolbar-visibility": {
224
+ "types": "./dist/wire-toolbar-visibility.d.ts",
225
+ "import": "./dist/wire-toolbar-visibility.js"
226
+ },
218
227
  "./nav-stack": {
219
228
  "types": "./dist/nav-stack.d.ts",
220
229
  "browser": "./dist/browser/nav-stack.js",
@@ -1,3 +1,4 @@
1
+ import '@kerfjs/ui/surface-scaffold/register';
1
2
  import '@kerfjs/ui/layout.css';
2
3
  import '@kerfjs/ui/nav-stack.css';
3
4
  import '@kerfjs/ui/split-view.css';
@@ -5,6 +6,7 @@ import '@kerfjs/ui/webawesome.css';
5
6
  import '@awesome.me/webawesome/dist/components/button/button.js';
6
7
  import '@awesome.me/webawesome/dist/components/dialog/dialog.js';
7
8
 
9
+ import { remify } from '@kerfjs/ui/css-values';
8
10
  import { deviceClass } from '@kerfjs/ui/device-class';
9
11
  import { List } from '@kerfjs/ui/list';
10
12
  import { ListItem } from '@kerfjs/ui/list-item';
@@ -184,6 +186,8 @@ export const createRecipe: RecipeFactory = (announce) => {
184
186
  <DialogSurface
185
187
  size="large"
186
188
  presentation={compact ? 'fullscreen' : 'modal'}
189
+ viewportGutter={remify(16)}
190
+ maxHeight="viewport"
187
191
  bodyInset="none"
188
192
  footerInset="comfortable"
189
193
  >
@@ -58,7 +58,7 @@ const ITEMS: LibraryItem[] = [
58
58
  export const createRecipe: RecipeFactory = (announce) => {
59
59
  const listView = (): NavStackView => ({
60
60
  key: 'library',
61
- title: 'Library',
61
+ toolbar: { title: 'Library' },
62
62
  content: (
63
63
  <List>
64
64
  <section>
@@ -79,7 +79,7 @@ export const createRecipe: RecipeFactory = (announce) => {
79
79
 
80
80
  const detailView = (item: LibraryItem): NavStackView => ({
81
81
  key: item.id,
82
- title: item.label,
82
+ toolbar: { title: item.label },
83
83
  content: (
84
84
  <List controlInsets="t">
85
85
  <ContentItem>
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/components/actions/toolbar-text/toolbar-text.tsx"],"names":[],"mappings":";;;;AA8DO,SAAS,WAAA,CAAY;AAAA,EAC1B,IAAA;AAAA,EACA,IAAA,GAAO,SAAA;AAAA,EACP,SAAA,GAAY,EAAA;AAAA,EACZ,EAAA;AAAA,EACA,YAAA;AAAA,EACA,WAAA,GAAc,KAAA;AAAA,EACd,IAAA,GAAO,KAAA;AAAA,EACP,QAAA,GAAW,IAAA;AAAA,EACX,QAAA,GAAW,IAAA;AAAA,EACX,IAAA,GAAO,KAAA;AAAA,EACP,IAAA;AAAA,EACA;AACF,CAAA,EAAqB;AACnB,EAAA,MAAM,MAAA,GAAS,IAAA,IAAQ,QAAA,IAAY,IAAA,IAAQ,QAAA,GAAW,CAAA;AACtD,EAAA,IAAI,MAAA,KAAW,MAAA;AACb,IAAA,uBACE,GAAA;AAAA,MAAC,QAAA;AAAA,MAAA;AAAA,QACC,IAAA,EAAK,QAAA;AAAA,QACL,KAAA,EAAO,CAAA,iBAAA,EAAoB,SAAS,CAAA,CAAA,CAAG,IAAA,EAAK;AAAA,QAC5C,gBAAA,EAAe,cAAA;AAAA,QACf,WAAA,EAAW,IAAA;AAAA,QACX,WAAA,EAAW,OAAO,MAAA,GAAS,MAAA;AAAA,QAC3B,eAAA,EAAe,WAAW,MAAA,GAAY,OAAA;AAAA,QACtC,aAAA,EAAa,MAAA;AAAA,QACb,EAAA;AAAA,QACA,IAAA;AAAA,QAEA,QAAA,kBAAA,GAAA,CAAC,MAAA,EAAA,EAAK,KAAA,EAAM,wBAAA,EAA0B,QAAA,EAAA,IAAA,EAAK;AAAA;AAAA,KAC7C;AAEJ,EAAA,uBACE,GAAA;AAAA,IAAC,MAAA;AAAA,IAAA;AAAA,MACC,KAAA,EAAO,CAAA,iBAAA,EAAoB,SAAS,CAAA,CAAA,CAAG,IAAA,EAAK;AAAA,MAC5C,gBAAA,EAAe,cAAA;AAAA,MACf,WAAA,EAAW,IAAA;AAAA,MACX,WAAA,EAAW,OAAO,MAAA,GAAS,MAAA;AAAA,MAC3B,WAAA,EAAW,OAAO,MAAA,GAAS,MAAA;AAAA,MAC3B,eAAA,EAAe,WAAW,MAAA,GAAY,OAAA;AAAA,MACtC,gBAAA,EAAgB,MAAA,GAAS,MAAA,CAAO,QAAQ,CAAA,GAAI,MAAA;AAAA,MAC5C,KAAA,EAAO,MAAA,GAAS,CAAA,6BAAA,EAAgC,QAAQ,CAAA,CAAA,GAAK,MAAA;AAAA,MAC7D,kBAAA,EAAkB,cAAc,MAAA,GAAS,MAAA;AAAA,MACzC,EAAA;AAAA,MACA,IAAA,EAAM,eAAe,SAAA,GAAY,MAAA;AAAA,MACjC,YAAA,EAAY,YAAA,GAAe,MAAA,CAAO,YAAY,CAAA,GAAI,MAAA;AAAA,MAClD,WAAA,EAAW,cAAc,MAAA,GAAS,MAAA;AAAA,MAClC,IAAA;AAAA,MAEC,QAAA,EAAA,WAAA,mBACC,GAAA,CAAC,QAAA,EAAA,EAAS,KAAA,EAAO,EAAA,CAAG,CAAC,CAAA,EAAG,CAAA,mBAExB,GAAA,CAAC,MAAA,EAAA,EAAK,KAAA,EAAM,wBAAA,EAA0B,QAAA,EAAA,IAAA,EAAK;AAAA;AAAA,GAE/C;AAEJ","file":"chunk-5PAVAR4B.js","sourcesContent":["import { em } from '../../../shared/styles/css-values.js';\nimport { Skeleton } from '../../feedback/skeleton/skeleton.js';\n\nexport type ToolbarTextSize =\n 'xlarge' | 'xlarge-fixed' | 'large' | 'default' | 'small' | 'xsmall';\n\n/** ARIA heading level for a title exposed as a heading landmark. */\nexport type HeadingLevel = 1 | 2 | 3 | 4 | 5 | 6;\n\ninterface ToolbarTextBaseProps {\n text: string;\n size?: ToolbarTextSize;\n className?: string;\n /** Optional id, e.g. so a dialog can reference the title via aria-labelledby. */\n id?: string;\n /**\n * Show a trailing ellipsis (…) where the text is truncated — on the single line\n * (default), or at the `maxLines` boundary when wrapping. Set false to hard-clip\n * instead. Default true.\n */\n ellipsis?: boolean;\n /**\n * Grow to fill the free space of the flex row it sits in (a Toolbar zone),\n * truncating at the space left by its siblings instead of taking only its\n * text's width. Default false.\n */\n fill?: boolean;\n /** Native named-slot assignment when composed inside a web component. */\n slot?: string;\n /**\n * Cap wrapped text to this many lines, truncating past it. Only takes effect with\n * `wrap`; ignored on a single line. `null`/omitted wraps without a line cap. Default null.\n */\n}\n\ntype ToolbarTextWrappingProps =\n | {\n /** Wrap onto multiple lines; combine with `maxLines` to cap them. */\n wrap: true;\n maxLines?: number | null;\n }\n | { wrap?: false; maxLines?: never };\n\nexport type ToolbarTextProps = ToolbarTextBaseProps &\n (\n | ({\n action?: never;\n /** Expose `role=\"heading\"` and `aria-level` on read-only text. */\n headingLevel?: HeadingLevel;\n /** Render an unanimated loading skeleton instead of the text. */\n placeholder?: boolean;\n } & ToolbarTextWrappingProps)\n | {\n /** Render a native button with this delegated action; the app owns editing. */\n action: string;\n headingLevel?: never;\n placeholder?: never;\n wrap?: never;\n maxLines?: never;\n }\n );\n\nexport function ToolbarText({\n text,\n size = 'default',\n className = '',\n id,\n headingLevel,\n placeholder = false,\n wrap = false,\n ellipsis = true,\n maxLines = null,\n fill = false,\n slot,\n action,\n}: ToolbarTextProps) {\n const capped = wrap && maxLines != null && maxLines > 0;\n if (action !== undefined)\n return (\n <button\n type=\"button\"\n class={`kui-toolbar-text ${className}`.trim()}\n data-component=\"toolbar-text\"\n data-size={size}\n data-fill={fill ? 'true' : undefined}\n data-ellipsis={ellipsis ? undefined : 'false'}\n data-action={action}\n id={id}\n slot={slot}\n >\n <span class=\"kui-toolbar-text__text\">{text}</span>\n </button>\n );\n return (\n <span\n class={`kui-toolbar-text ${className}`.trim()}\n data-component=\"toolbar-text\"\n data-size={size}\n data-fill={fill ? 'true' : undefined}\n data-wrap={wrap ? 'true' : undefined}\n data-ellipsis={ellipsis ? undefined : 'false'}\n data-max-lines={capped ? String(maxLines) : undefined}\n style={capped ? `--kui-toolbar-text-max-lines:${maxLines}` : undefined}\n data-placeholder={placeholder ? 'true' : undefined}\n id={id}\n role={headingLevel ? 'heading' : undefined}\n aria-level={headingLevel ? String(headingLevel) : undefined}\n aria-busy={placeholder ? 'true' : undefined}\n slot={slot}\n >\n {placeholder ? (\n <Skeleton width={em(8)} />\n ) : (\n <span class=\"kui-toolbar-text__text\">{text}</span>\n )}\n </span>\n );\n}\n"]}