@kerfjs/ui 5.0.0-beta.77 → 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.
- package/README.md +3 -0
- package/ai/application-ui-diagnostic-ids-v1.json +3 -0
- package/ai/component-catalog.json +121 -9
- package/ai/component-composition.json +102 -6
- package/ai/components/nav-stack.md +3 -3
- package/ai/components/surface-scaffold.md +11 -2
- package/ai/components/toolbar-control-group.md +2 -0
- package/ai/components/toolbar-text.md +2 -0
- package/ai/components/toolbar.md +2 -0
- package/ai/public-api-signatures-v1.md +49 -121
- package/ai/skill.md +11 -1
- package/ai/webawesome-jsx-signatures-v1.md +1 -1
- package/dist/browser/nav-stack.js +2 -0
- package/dist/browser/split-view.js +2 -0
- package/dist/catalog-resources.d.ts +3 -3
- package/dist/catalog.d.ts +3 -3
- package/dist/catalog.js +3 -3
- package/dist/{chunk-WJN3V2H5.js → chunk-2GOCOLQL.js} +18 -4
- package/dist/chunk-2GOCOLQL.js.map +1 -0
- package/dist/{chunk-XIZRXI7N.js → chunk-A7DYEERU.js} +13 -1
- package/dist/chunk-A7DYEERU.js.map +1 -0
- package/dist/{chunk-6VQPLV3O.js → chunk-DSLRNR43.js} +9 -8
- package/dist/chunk-DSLRNR43.js.map +1 -0
- package/dist/{chunk-AOAW55JN.js → chunk-DXQZ3BEO.js} +4 -1
- package/dist/chunk-DXQZ3BEO.js.map +1 -0
- package/dist/chunk-IKCUPTR2.js +170 -0
- package/dist/chunk-IKCUPTR2.js.map +1 -0
- package/dist/{chunk-Z6PE5T7N.js → chunk-JL4BBZP4.js} +4 -3
- package/dist/chunk-JL4BBZP4.js.map +1 -0
- package/dist/collapsible-panel.js +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +5 -5
- package/dist/nav-stack-B1Qc2e5q.d.ts +101 -0
- package/dist/nav-stack.d.ts +11 -93
- package/dist/nav-stack.js +5 -1
- package/dist/pane.d.ts +5 -2
- package/dist/pane.js +1 -1
- package/dist/split-view.d.ts +9 -4
- package/dist/split-view.js +7 -3
- package/dist/split-view.js.map +1 -1
- package/dist/styles/components/actions/toolbar-control-group/toolbar-control-group.css +60 -13
- package/dist/styles/components/actions/toolbar-text/toolbar-text.css +7 -1
- package/dist/styles/components/layout/pane/pane.css +7 -0
- package/dist/styles/components/navigation/nav-stack/nav-stack.css +6 -12
- package/dist/styles/components/navigation/tab-scaffold/tab-scaffold.css +17 -0
- package/dist/styles/components/surfaces/surface-scaffold/surface-scaffold.css +55 -0
- package/dist/styles/nav-stack.css +6 -12
- package/dist/styles/pane.css +7 -0
- package/dist/styles/surface-scaffold.css +55 -0
- package/dist/styles/tab-scaffold.css +17 -0
- package/dist/styles/toolbar-control-group.css +60 -13
- package/dist/styles/toolbar-text.css +7 -1
- package/dist/surface-scaffold-register.d.ts +4 -0
- package/dist/surface-scaffold-register.js +59 -0
- package/dist/surface-scaffold-register.js.map +1 -0
- package/dist/surface-scaffold.d.ts +10 -2
- package/dist/surface-scaffold.js +1 -1
- package/dist/tab-scaffold.d.ts +2 -0
- package/dist/tab-scaffold.js +1 -0
- package/dist/tab-scaffold.js.map +1 -1
- package/dist/wire-toolbar-visibility.js +1 -1
- package/dist/workbench.d.ts +5 -13
- package/dist/workbench.js +1 -1
- package/docs/component-contract.md +8 -1
- package/docs/component-selection.md +1 -1
- package/docs/layout.md +20 -0
- package/docs/nav-stack.md +30 -18
- package/docs/recipes.md +5 -1
- package/docs/split-view.md +7 -4
- package/docs/surface-scaffold.md +46 -0
- package/docs/tab-scaffold.md +57 -0
- package/docs/ui-doctor.md +23 -0
- package/docs/workbench.md +3 -2
- package/doctor/component-style-checks.mjs +224 -0
- package/doctor/index.mjs +42 -13
- package/llms.txt +10 -0
- package/package.json +7 -2
- package/ux-demo/recipes/list-detail-dialog.tsx +4 -0
- package/ux-demo/recipes/navigation-stack.tsx +2 -2
- package/dist/chunk-6VQPLV3O.js.map +0 -1
- package/dist/chunk-AOAW55JN.js.map +0 -1
- package/dist/chunk-WJN3V2H5.js.map +0 -1
- package/dist/chunk-XIZRXI7N.js.map +0 -1
- package/dist/chunk-Z4EPMX25.js +0 -123
- package/dist/chunk-Z4EPMX25.js.map +0 -1
- package/dist/chunk-Z6PE5T7N.js.map +0 -1
|
@@ -183,7 +183,7 @@ beside a `ContentItem` value, and connect its id to the preview group's
|
|
|
183
183
|
| One application tab — `AppTab` | A controlled app tab needs selection, close, drag, leading/trailing anatomy, compact/segmented/icon-only presentation, label truncation, safe domain metadata, or a product-specific close glyph. | Do not render it alone or use it for a small settings choice; compose in `TabBar`, or use `SegmentedControl`. Keep `closeIcon` noninteractive. | Compose in `TabBar`; let `wireTabBars` manage interaction. Put only domain `data-*` values in `rootAttributes`; use typed appearance props instead of descendant CSS. | Tab identity, order, selection, close policy, content, domain metadata values, and outer placement. | `@kerfjs/ui/app-tab` | [Tabs contract](./accessibility.md#tabs) |
|
|
184
184
|
| Application tab strip — `TabBar`, `wireTabBars`, `reorderTabs` | Tabs switch page regions and may overflow, close, reorder, fill a segmented inspector strip, sit beside a trailing action, or keep that adjacent action distinct from a far-edge end action. | Do not use it for a compact local view toggle; use `SegmentedControl`. Do not use it for a long choice list; use `Select`. Prefer it over Web Awesome `wa-tab-group`, `wa-tab`, and `wa-tab-panel` for Kerf app tabs. | Call `wireTabBars` once, retain the disposer, and apply `onReorder` synchronously; `reorderTabs` is the default array helper. Combine `trailingPlacement="adjacent"` with `end` for split actions; TabBar owns the shrinking/scrolling tab geometry and keeps both actions visible. Choose typed presentation/allocation props while the parent owns outer placement. | Ordered tabs, selection, panels, routing, closing, persistence, action content, and outer placement. | `@kerfjs/ui/tab-bar` plus `@kerfjs/ui/wire-tab-bars` | [Tabs contract](./accessibility.md#tabs) |
|
|
185
185
|
| Panel, dialog, or page heading — `Toolbar` + `ToolbarText` | A panel, dialog, or page needs an extra-large title, optional icon, grouped trailing actions, and optional supporting copy. | Do not add a private heading wrapper or bespoke layout. The toolbar supplies structure, not modal behavior; use an application overlay or Web Awesome `wa-dialog` for that behavior. In a narrow peripheral rail or drawer, where xlarge would truncate at the panel's minimum size, use the default `ToolbarText` size as pane identity. | Connect title/supporting-copy ids to the host, group controls, and delegate actions. **For a page or view title set `headingLevel` (usually `1`)** on `ToolbarText`; omit it for a dialog title referenced by `aria-labelledby`. | Open state, focus lifecycle, dismissal, controls, ids, copy, and heading level. | `@kerfjs/ui/toolbar` + `@kerfjs/ui/toolbar-text` | [Toolbar headings](./accessibility.md#toolbar-headings) |
|
|
186
|
-
| Dialog or popup geometry — `DialogSurface`, `PopupSurface` | Recurring Web Awesome dialogs need standard sizes/presentations/body/footer insets, or dropdown menus need list-compatible inset ownership.
|
|
186
|
+
| Dialog or popup geometry — `DialogSurface`, `PopupSurface` | Recurring Web Awesome dialogs need standard sizes/presentations/body/footer insets and typed modal viewport bounds, or dropdown menus need list-compatible inset ownership. | Do not use these wrappers as generic layout or to replace native dialog/dropdown behavior. Avoid consumer `::part()` overrides and do not stack a dialog body inset around list-owned child geometry. | Wrap one `wa-dialog` or `wa-dropdown`; choose the typed geometry while retaining native labeling, open state, focus, dismissal, and menu semantics. Compose a dialog body as a `List` by default, use `bodyInset="none"` when its children own list geometry, and wrap bare prose in `ListInsetText`. | State, focus lifecycle, dismissal, labels, content, and outer placement. | `@kerfjs/ui/surface-scaffold` | [Surface scaffolds](./surface-scaffold.md) |
|
|
187
187
|
| Key/value facts — `ValueTable`, `ValueTableRow` | Read-only labels and values form a semantic definition list, optionally with a leading icon. | Do not use it for editable form fields or a row/column data grid; use native form or table semantics. | Compose typed `ValueTableRow` entries; pass `icon` when a 24px leading icon adds useful context. | Values, formatting, icon meaning, and empty/loading policy. | `@kerfjs/ui/value-table` | [Component ownership](./component-contract.md#ownership-boundaries) |
|
|
188
188
|
| Indeterminate activity — `LoadingSpinner` | A Kerf surface needs compact, labeled or decorative indeterminate progress. | Do not use it for known progress; use Web Awesome `wa-progress-bar` or `wa-progress-ring`. Direct Web Awesome UI may use `wa-spinner`; do not mix spinner systems within one surface. | Pass a label when the spinner conveys status; use size for a named icon step or positive pixel size. | Loading lifecycle and adjacent status copy. | `@kerfjs/ui/loading-spinner` | [Accessibility](./accessibility.md#shared-rules) |
|
|
189
189
|
| Loading placeholder — `Skeleton` + a component's `placeholder` prop | A value or a whole component is still loading and should hold its space as a subtle, unanimated block, keeping the layout stable — an inspector or detail view rendering its real chrome with per-record values absent. | Do not use it for known progress (use `LoadingSpinner`), do not animate it, and do not hand-rebuild a component's empty state — set `placeholder` on the component instead. Prefer it over `wa-skeleton`, which the pure-Kerf primitives avoid to stay Web-Awesome-free. | Set `placeholder` on a value-bearing component (`Select`, `ListHeader`, `ListItem`, `ValueTableRow`, `SegmentedControl`, `StateBanner`, `AppTab`, `ToolbarText`, `ListActionRow`) to render skeletons in its value slots with interactivity disabled; use the standalone `Skeleton` for a custom slot. | Loading lifecycle, which slots are unknown, and announcing the loading region. | `@kerfjs/ui/skeleton` | [Accessibility](./accessibility.md#shared-rules) |
|
package/docs/layout.md
CHANGED
|
@@ -36,6 +36,26 @@ size) tall, and below that lets the whole pane scroll as one column, header,
|
|
|
36
36
|
content, and footer together, so the content keeps its natural height. The
|
|
37
37
|
default `fixed` always pins them.
|
|
38
38
|
|
|
39
|
+
### Deep content inset
|
|
40
|
+
|
|
41
|
+
Set `deepInset` on a `Pane` to add one 8px inline gutter to its scrolling
|
|
42
|
+
content. A `ContentItem` or `ListItem` that normally sits 8px from the pane
|
|
43
|
+
edge then sits 16px from it. The default is `false`. This extra gutter adds
|
|
44
|
+
to any device safe-area inset, while the pane's header, footer, background,
|
|
45
|
+
and separators still reach their usual edges.
|
|
46
|
+
|
|
47
|
+
```tsx
|
|
48
|
+
<Pane deepInset>
|
|
49
|
+
<ContentItem>Project Atlas</ContentItem>
|
|
50
|
+
</Pane>
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The pane leaves `--kui-layout-inline-margin` at its normal 8px value. Nested
|
|
54
|
+
`Row`, `List`, and `Grid` therefore keep their usual item spacing; they do
|
|
55
|
+
not inherit a doubled margin. `PaneConfig.deepInset` forwards through
|
|
56
|
+
Workbench `mainPane`, panel `pane`, CollapsiblePanel `pane`, and a
|
|
57
|
+
`NavStackView.pane`.
|
|
58
|
+
|
|
39
59
|
### Lowered work surfaces
|
|
40
60
|
|
|
41
61
|
Set `appearance="sunken"` when the work area itself should use the lowered
|
package/docs/nav-stack.md
CHANGED
|
@@ -15,7 +15,7 @@ import { wireNavStack } from "@kerfjs/ui/wire-nav-stack";
|
|
|
15
15
|
In a browser bundler that honors the `browser` export condition (Vite, esbuild, and
|
|
16
16
|
webpack do by default), the import above also
|
|
17
17
|
loads NavStack's stylesheet and those of the components it renders internally
|
|
18
|
-
(`Toolbar`, `ToolbarControlGroup`, `ToolbarText`, `LucideIcon`). Without that condition, import the manual stylesheets instead:
|
|
18
|
+
(`Toolbar`, `ToolbarControlGroup`, `ToolbarText`, `Pane`, `List`, `LucideIcon`). Without that condition, import the manual stylesheets instead:
|
|
19
19
|
`@kerfjs/ui/nav-stack.css` plus those components' CSS, or `@kerfjs/ui/styles.css`.
|
|
20
20
|
|
|
21
21
|
## State lives in the app
|
|
@@ -28,9 +28,12 @@ stack as a `signal<NavStackView[]>`, `NavStack({ views })` renders it, and
|
|
|
28
28
|
const views = signal<NavStackView[]>([
|
|
29
29
|
{
|
|
30
30
|
key: "inbox",
|
|
31
|
-
title: "Inbox",
|
|
31
|
+
toolbar: { title: "Inbox" },
|
|
32
32
|
content: <InboxView />,
|
|
33
|
-
bottomToolbar:
|
|
33
|
+
bottomToolbar: {
|
|
34
|
+
label: "Inbox status",
|
|
35
|
+
leading: <InboxStatus />,
|
|
36
|
+
},
|
|
34
37
|
},
|
|
35
38
|
]);
|
|
36
39
|
|
|
@@ -47,35 +50,39 @@ const dispose = wireNavStack(root, {
|
|
|
47
50
|
// push / pop by editing the signal:
|
|
48
51
|
views.value = [
|
|
49
52
|
...views.value,
|
|
50
|
-
{ key: id, title: "Message", content: <MessageView id={id} /> },
|
|
53
|
+
{ key: id, toolbar: { title: "Message" }, content: <MessageView id={id} /> },
|
|
51
54
|
];
|
|
52
55
|
```
|
|
53
56
|
|
|
54
57
|
`NavStack` renders every entry stacked, the last one active and the rest kept
|
|
55
58
|
mounted (so their DOM state survives) but hidden. Each entry carries a
|
|
56
|
-
`key` (stable identity), `content`, an optional `title`,
|
|
57
|
-
`leading`, `center`, and trailing
|
|
58
|
-
|
|
59
|
+
`key` (stable identity), `content`, an optional `toolbar` object with `title`,
|
|
60
|
+
`leading`, `center`, and `trailing` groups, and an optional structured
|
|
61
|
+
`bottomToolbar`. The component-level structured
|
|
59
62
|
`bottomToolbar` remains a persistent fallback for views that do not provide one.
|
|
60
|
-
Each view can also provide `header`,
|
|
61
|
-
|
|
62
|
-
|
|
63
|
+
Each view can also provide `header`, `footer`, `headerList`, `footerList`, and
|
|
64
|
+
`pane` configuration, as a `WorkbenchStaticPanel` does. With any of these,
|
|
65
|
+
the view renders a `Pane`: its header and footer stay pinned around the view's
|
|
66
|
+
scrolling content, and slide with that view during navigation. The header and
|
|
67
|
+
footer list configuration forwards to their `List`s; `pane` forwards
|
|
68
|
+
`contentElement`, `contentLabel`, `separators`, `safeAreaEdges`,
|
|
69
|
+
`chromeDividers`, and `appearance` to the Pane. Without those fields, the view
|
|
70
|
+
keeps its simple direct scroll area. Use `pane: { appearance: "sunken" }` for a
|
|
71
|
+
lowered scrolling work surface.
|
|
63
72
|
The back control appears automatically once the stack has more than one entry;
|
|
64
73
|
`wireNavStack`'s `onBack` is where the app pops its own signal.
|
|
65
74
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
view holds a `Pane` as its only child, set the appearance on the Pane instead
|
|
69
|
-
if its content owns scrolling. See [Lowered work surfaces](layout.md#lowered-work-surfaces).
|
|
75
|
+
See [Lowered work surfaces](layout.md#lowered-work-surfaces) for nested Pane
|
|
76
|
+
and layout content.
|
|
70
77
|
|
|
71
78
|
## The top toolbar
|
|
72
79
|
|
|
73
80
|
The top chrome is a real `Toolbar`, so it follows the same zone,
|
|
74
81
|
group, and responsive rules as every other toolbar. Its leading zone holds the
|
|
75
82
|
back control (a borderless `ToolbarControlGroup`, shown once the stack has
|
|
76
|
-
depth), then
|
|
77
|
-
`
|
|
78
|
-
|
|
83
|
+
depth), then `toolbar.leading`, then `toolbar.title` as a `ToolbarText`.
|
|
84
|
+
`toolbar.center` fills the center zone and `toolbar.trailing` fills the
|
|
85
|
+
trailing zone. Give each zone `ToolbarControlGroup`s or `ToolbarText`,
|
|
79
86
|
as in any toolbar.
|
|
80
87
|
|
|
81
88
|
Configure the toolbar with `toolbarConfig` instead of styling it:
|
|
@@ -95,7 +102,12 @@ Configure the toolbar with `toolbarConfig` instead of styling it:
|
|
|
95
102
|
/>
|
|
96
103
|
```
|
|
97
104
|
|
|
98
|
-
- `
|
|
105
|
+
- Each view's `toolbar` can forward the `Toolbar` configuration (`dividerSides`,
|
|
106
|
+
`centerAlign`, `responsive`, `responsiveAt`, `safeAreaEdges`) and its `label`,
|
|
107
|
+
`titleSize`, and `headingLevel`. A view value overrides the corresponding
|
|
108
|
+
`toolbarConfig` default. Its `bottomToolbar` forwards the same `Toolbar`
|
|
109
|
+
configuration and has a required accessible `label`.
|
|
110
|
+
- `toolbarConfig` supplies stack-wide defaults (`dividerSides`,
|
|
99
111
|
`centerAlign`, `responsive`, `responsiveAt`, `safeAreaEdges`) and adds
|
|
100
112
|
`label` (the toolbar's accessible name), `titleSize` (the title's
|
|
101
113
|
`ToolbarText` size, default `large`), and `headingLevel` (expose the title as
|
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.
|
|
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,
|
package/docs/split-view.md
CHANGED
|
@@ -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
|
|
90
|
-
(`leading`, `center`, trailing
|
|
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: {
|
|
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
|
|
package/docs/surface-scaffold.md
CHANGED
|
@@ -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.
|
package/docs/tab-scaffold.md
CHANGED
|
@@ -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
|
|
171
|
-
groups,
|
|
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
|
+
}
|