@kerfjs/ui 5.0.0-beta.3 → 5.0.0-beta.6
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 +12 -9
- package/ai/component-catalog.json +314 -172
- package/ai/public-api-signatures-v1.md +333 -26
- package/ai/skill.md +17 -9
- package/ai/webawesome-jsx-signatures-v1.md +1 -1
- package/dist/browser/{dialog-header.js → panel-header.js} +3 -2
- package/dist/{chunk-GY5WH7TO.js → chunk-24Z2XE6C.js} +3 -3
- package/dist/chunk-24Z2XE6C.js.map +1 -0
- package/dist/chunk-2RJBFNB6.js +172 -0
- package/dist/chunk-2RJBFNB6.js.map +1 -0
- package/dist/chunk-KZKSBUKC.js +19 -0
- package/dist/chunk-KZKSBUKC.js.map +1 -0
- package/dist/chunk-RBTVBGRD.js +23 -0
- package/dist/chunk-RBTVBGRD.js.map +1 -0
- package/dist/device-class.d.ts +62 -0
- package/dist/device-class.js +75 -0
- package/dist/device-class.js.map +1 -0
- package/dist/index.d.ts +2 -3
- package/dist/index.js +8 -9
- package/dist/nav-stack.d.ts +38 -0
- package/dist/nav-stack.js +4 -0
- package/dist/nav-stack.js.map +1 -0
- package/dist/panel-header.d.ts +26 -0
- package/dist/panel-header.js +6 -0
- package/dist/panel-header.js.map +1 -0
- package/dist/split-view.d.ts +42 -0
- package/dist/split-view.js +20 -0
- package/dist/split-view.js.map +1 -0
- package/dist/styles/foundation.css +21 -0
- package/dist/styles/menu-action-row.css +6 -0
- package/dist/styles/menu-header.css +19 -2
- package/dist/styles/menu-item.css +5 -0
- package/dist/styles/nav-stack.css +112 -0
- package/dist/styles/panel-header.css +59 -0
- package/dist/styles/split-view.css +35 -0
- package/dist/styles/styles.css +1 -2
- package/dist/styles/tab-scaffold.css +84 -0
- package/dist/styles/toolbar-control-group.css +52 -2
- package/dist/styles/toolbar-text.css +11 -0
- package/dist/styles/toolbar.css +9 -0
- package/dist/styles/workbench.css +94 -0
- package/dist/tab-scaffold.d.ts +30 -0
- package/dist/tab-scaffold.js +16 -0
- package/dist/tab-scaffold.js.map +1 -0
- package/dist/toolbar-text.d.ts +4 -2
- package/dist/toolbar-text.js +1 -1
- package/dist/wire-nav-stack.d.ts +15 -0
- package/dist/wire-nav-stack.js +88 -0
- package/dist/wire-nav-stack.js.map +1 -0
- package/dist/wire-tab-scaffold.d.ts +11 -0
- package/dist/wire-tab-scaffold.js +16 -0
- package/dist/wire-tab-scaffold.js.map +1 -0
- package/dist/wire-token-search-fields.d.ts +46 -4
- package/dist/wire-token-search-fields.js +1 -1
- package/dist/workbench.d.ts +33 -0
- package/dist/workbench.js +17 -0
- package/dist/workbench.js.map +1 -0
- package/docs/accessibility.md +10 -10
- package/docs/app-layouts.md +57 -0
- package/docs/component-contract.md +37 -9
- package/docs/component-selection.md +57 -8
- package/docs/design-philosophy.md +23 -1
- package/docs/device-class.md +54 -0
- package/docs/layout.md +44 -8
- package/docs/nav-stack.md +47 -0
- package/docs/recipes.md +29 -9
- package/docs/split-view.md +49 -0
- package/docs/tab-scaffold.md +41 -0
- package/docs/ux-demo.md +11 -9
- package/docs/workbench.md +47 -0
- package/llms.txt +107 -43
- package/package.json +37 -11
- package/ux-demo/recipes/app-shell.tsx +3 -3
- package/ux-demo/recipes/composer-form.tsx +5 -4
- package/ux-demo/recipes/list-workspace-states.tsx +2 -2
- package/ux-demo/recipes/loaders.ts +2 -0
- package/ux-demo/recipes/master-detail-dialog.tsx +2 -2
- package/ux-demo/recipes/mount-recipe.ts +3 -0
- package/ux-demo/recipes/navigation-stack.tsx +76 -0
- package/ux-demo/recipes/recipes.css +40 -13
- package/ux-demo/recipes/workspace-header.tsx +2 -2
- package/dist/browser/page-header.js +0 -3
- package/dist/chunk-2PES33HS.js +0 -13
- package/dist/chunk-2PES33HS.js.map +0 -1
- package/dist/chunk-GY5WH7TO.js.map +0 -1
- package/dist/chunk-H5AGGVU5.js +0 -75
- package/dist/chunk-H5AGGVU5.js.map +0 -1
- package/dist/chunk-K3G72I6D.js +0 -24
- package/dist/chunk-K3G72I6D.js.map +0 -1
- package/dist/dialog-header.d.ts +0 -15
- package/dist/dialog-header.js +0 -5
- package/dist/dialog-header.js.map +0 -1
- package/dist/page-header.d.ts +0 -9
- package/dist/page-header.js +0 -3
- package/dist/page-header.js.map +0 -1
- package/dist/styles/dialog-header.css +0 -87
- package/dist/styles/page-header.css +0 -26
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Workbench
|
|
2
|
+
|
|
3
|
+
`@kerfjs/ui/workbench` is the Xcode-like multi-panel workspace: a collapsible
|
|
4
|
+
left rail, right rail, and bottom drawer around a central work area (any absent).
|
|
5
|
+
It generalizes the instant-width / sliding-content collapse used by the catalog
|
|
6
|
+
sidebar. One of the opt-in app layouts (see
|
|
7
|
+
[`../../docs/23-app-layouts.md`](../../docs/23-app-layouts.md)); best on
|
|
8
|
+
desktop-class devices.
|
|
9
|
+
|
|
10
|
+
```ts
|
|
11
|
+
import { Workbench } from '@kerfjs/ui/workbench';
|
|
12
|
+
import '@kerfjs/ui/workbench.css';
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## State lives in the app
|
|
16
|
+
|
|
17
|
+
`Workbench` is declarative and the collapse is **pure CSS** — no wire. The app
|
|
18
|
+
owns each panel's `collapsed` flag (usually a signal) and toggles it; the panel
|
|
19
|
+
animates itself.
|
|
20
|
+
|
|
21
|
+
```tsx
|
|
22
|
+
const navCollapsed = signal(false);
|
|
23
|
+
|
|
24
|
+
<Workbench
|
|
25
|
+
id="studio"
|
|
26
|
+
label="Studio"
|
|
27
|
+
main={<Editor />}
|
|
28
|
+
leftRail={{ content: <Navigator />, label: 'Navigator', collapsed: navCollapsed.value, size: 280 }}
|
|
29
|
+
rightRail={{ content: <Inspector />, label: 'Inspector', collapsed: inspectorCollapsed.value }}
|
|
30
|
+
bottomDrawer={{ content: <Console />, label: 'Console', collapsed: consoleCollapsed.value }}
|
|
31
|
+
/>;
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Each `WorkbenchPanel` takes `content`, an optional `collapsed`, an optional
|
|
35
|
+
`size` (rail width or drawer height in px, overriding the CSS default —
|
|
36
|
+
`--kui-workbench-rail-width` 280px, `--kui-workbench-drawer-height` 220px), and
|
|
37
|
+
an optional `label`.
|
|
38
|
+
|
|
39
|
+
## How the collapse animates
|
|
40
|
+
|
|
41
|
+
Collapsing snaps the panel's flex track to zero in a single reflow (so the work
|
|
42
|
+
area relayouts once, not per frame) while the panel's fixed-size content slides
|
|
43
|
+
out via a composited `transform` — a rail slides horizontally, the drawer
|
|
44
|
+
vertically — clipped by the shell's overflow. It honors `prefers-reduced-motion`
|
|
45
|
+
(the slide collapses to instant). On smaller device classes, present the rails'
|
|
46
|
+
contents through a `NavStack` or overlay drawers rather than shrinking the
|
|
47
|
+
three-panel shell.
|
package/llms.txt
CHANGED
|
@@ -1,45 +1,109 @@
|
|
|
1
1
|
# @kerfjs/ui
|
|
2
2
|
|
|
3
|
-
Accessible, production-backed UI primitives for kerf
|
|
4
|
-
|
|
5
|
-
-
|
|
6
|
-
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
3
|
+
> Accessible, production-backed UI primitives for [kerf](https://github.com/brianwestphal/kerf)
|
|
4
|
+
> applications. Components are plain functions returning `SafeHtml`; the app owns
|
|
5
|
+
> state and wires stable `data-action` hooks once. This file orients an LLM to
|
|
6
|
+
> generate correct, good-looking kerf UI — it is not a catalog to browse.
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
npm install kerfjs @kerfjs/ui # kerfjs is a peer; Web Awesome is an optional peer
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Import each component from its own subpath (`@kerfjs/ui/menu-item`, `@kerfjs/ui/toolbar`,
|
|
13
|
+
…); the browser build pulls that component's reachable CSS automatically, so do not
|
|
14
|
+
maintain a central style list. Configure JSX with `"jsxImportSource": "kerfjs"`.
|
|
15
|
+
|
|
16
|
+
## How to build a screen (retrieval order)
|
|
17
|
+
|
|
18
|
+
1. **Pick the component from the interface need**, not by appearance — read
|
|
19
|
+
[component-selection.md](./docs/component-selection.md) (the problem→component
|
|
20
|
+
matrix, with imports, wiring, ownership, and alternatives).
|
|
21
|
+
2. **Get exact signatures before emitting code** — read
|
|
22
|
+
[public-api-signatures-v1.md](./ai/public-api-signatures-v1.md) (real prop names,
|
|
23
|
+
callback shapes, disposer return types).
|
|
24
|
+
3. **For exhaustive, structured facts** (ids, exports, variants, `publicClasses`,
|
|
25
|
+
relationships, side effects) load the
|
|
26
|
+
[Machine-readable component catalog](./ai/component-catalog.json) — the
|
|
27
|
+
canonical checked-in source; its schema is alongside it.
|
|
28
|
+
4. **Apply the rules below and in** [skill.md](./ai/skill.md).
|
|
29
|
+
|
|
30
|
+
## Core rules (do the right thing)
|
|
31
|
+
|
|
32
|
+
- **Reach for the primitive, not for CSS.** The package is built to look right
|
|
33
|
+
*unstyled*. Before adding `padding`, `margin`, `width`, `height`, `border`,
|
|
34
|
+
`background`, a wrapper card, or a decoration, check whether the component, the
|
|
35
|
+
pane, or the content-item already owns it — it almost always does. Prefer a
|
|
36
|
+
component prop or a `--kui-*` token; write CSS only for genuinely new structure,
|
|
37
|
+
and then only join a component's documented `publicClasses` (never reach in by
|
|
38
|
+
tag/id/attribute or an unlisted class).
|
|
39
|
+
- **Never double-inset.** A `.kui-pane` has no padding; its `.kui-content` children
|
|
40
|
+
own the geometry (8px inline margin + 1px border + 8px padding). Do not add a
|
|
41
|
+
container padding on top of a content-item's own margin — pick one owner. Use
|
|
42
|
+
`.kui-content` for 24px major vertical rhythm.
|
|
43
|
+
- **Trust the defaults.** Render a control at its natural size and color (a
|
|
44
|
+
`LucideIcon` is 24px); if it looks wrong, fix the surrounding layout, not the
|
|
45
|
+
control. Never force a component's width/height to size it — that leaves a halo
|
|
46
|
+
or a stretched oval.
|
|
47
|
+
- **Toolbars hold only `ToolbarText` and `ToolbarControlGroup`.** Never put a bare
|
|
48
|
+
button, input, link, or loose markup in a `leading`/`center`/`trailing` zone.
|
|
49
|
+
Wrap every control — `SegmentedControl`, `Select`, a collapsible
|
|
50
|
+
`TokenSearchField`, Web Awesome controls — inside a `ToolbarControlGroup`. A
|
|
51
|
+
toolbar popup menu is a `single` `ToolbarControlGroup` around a `wa-dropdown`
|
|
52
|
+
(its `slot="trigger"` `wa-button` is the toolbar button, kept under
|
|
53
|
+
`data-morph-skip-children`).
|
|
54
|
+
- **`PanelHeader` is *the* panel/dialog/page heading.** A plain toolbar with an
|
|
55
|
+
optional bordered icon group, an extra-large `ToolbarText` title, the trailing
|
|
56
|
+
controls you pass, and an optional subtitle below — do not hand-build a heading
|
|
57
|
+
row or restyle a toolbar into one.
|
|
58
|
+
- **Space by the five-step scale, by relationship not by eye.** `0`
|
|
59
|
+
`--kui-space-none`, `4px` `--kui-space-2xs`, `8px` `--kui-space-xs` (inside a
|
|
60
|
+
group), `16px` `--kui-space-m`, `24px` `--kui-space-l` (between major differing
|
|
61
|
+
regions). 12px/32px are off-scale exceptions.
|
|
62
|
+
- **Panes share one child-owned geometry.** Sidebars, main areas, inspectors, and
|
|
63
|
+
dialogs all use an unpadded `.kui-pane` (optional `.kui-pane__toolbar`, one
|
|
64
|
+
scrolling `.kui-pane__content`, optional `.kui-pane__footer`). A visible pane owns
|
|
65
|
+
its collapse control; a hidden pane's restore moves to the adjacent main
|
|
66
|
+
toolbar's matching edge — no icon-only rail. Keep one scroll owner per region.
|
|
67
|
+
- **State and behavior live in the app.** Components take semantic `data-action`
|
|
68
|
+
strings and controlled values; wire them once with `delegate()`/`delegateActions()`
|
|
69
|
+
or the disposer-returning `wire…` helpers, and own domain copy, transport, and
|
|
70
|
+
policy in a thin adapter. Every element must earn its place — cut chrome, labels,
|
|
71
|
+
and readouts that do not help a person decide or act.
|
|
72
|
+
- **App state is domain/persisted; helpers own only ephemeral mechanics.** Keep
|
|
73
|
+
navigation stacks, tab selection and order, pane sizes, and `collapsed`/`expanded`
|
|
74
|
+
disclosure flags in the app — they are data or persisted layout, and the app reads
|
|
75
|
+
them to render. The `wire…` helpers own the *ephemeral* parts: `wireNavStack` the
|
|
76
|
+
push/pop animation, `wireTabBars` the overflow autoscroll and drag preview,
|
|
77
|
+
`wireResizableRegions` the live drag preview; they report committed changes via
|
|
78
|
+
callbacks. The one richer case is `wireTokenSearchFields`, which manages the
|
|
79
|
+
collapsible field's expand/collapse/focus by default (on, per-behavior opt-out) and
|
|
80
|
+
exposes the `expanded` signal on its handle — bind it in render or hand in your own
|
|
81
|
+
via `collapsible.signals`. Do not hand-roll open/collapse/focus that helper
|
|
82
|
+
provides; but do keep a plain disclosure toggle (a one-line boolean the app already
|
|
83
|
+
renders from) in the app.
|
|
84
|
+
|
|
85
|
+
## Public exports
|
|
86
|
+
|
|
87
|
+
`LucideIcon`, `DisclosureArrow`, `Toolbar`, `ToolbarControlGroup`, `ToolbarText`,
|
|
88
|
+
`PanelHeader`, `MenuItem`, `MenuActionRow`, `MenuHeader`, `SegmentedControl`,
|
|
89
|
+
`Select`, `TokenSearchField` (+ `readTokenSearchField`, `placeTokenSearchCaret`,
|
|
90
|
+
`wireTokenSearchFields`), `AppTab`, `TabBar` (+ `wireTabBars`, `reorderTabs`),
|
|
91
|
+
`ResizableRegion` (+ `wireResizableRegions`), `StateBanner`, `EmptyState`,
|
|
92
|
+
`LoadingSpinner`, `ValueTable`, `ValueTableRow`. Whole-screen layouts (opt-in,
|
|
93
|
+
tree-shakeable, declarative with `wire…` helpers, each with a companion CSS import):
|
|
94
|
+
`NavStack`, `SplitView`, `Workbench`, `TabScaffold`, driven by `deviceClass()` from
|
|
95
|
+
`@kerfjs/ui/device-class`. `@kerfjs/ui/webawesome` is a type-only boundary for
|
|
96
|
+
`wa-*` JSX; `@kerfjs/ui/select/register` and the CSS are the declared side effects.
|
|
97
|
+
|
|
98
|
+
## Reference
|
|
99
|
+
|
|
100
|
+
- [skill.md](./ai/skill.md): the full rule set + a common-mistakes table (the shipped AI skill).
|
|
101
|
+
- [design-philosophy.md](./docs/design-philosophy.md): why kerf UI is shaped this way — the decision principles behind the rules above.
|
|
102
|
+
- [layout.md](./docs/layout.md): the pane/content-item geometry, public roles/tokens, and the spacing scale.
|
|
103
|
+
- [component-contract.md](./docs/component-contract.md): what a component owns vs. what the app owns.
|
|
104
|
+
- [accessibility.md](./docs/accessibility.md): keyboard and ARIA contracts per component.
|
|
105
|
+
- [recipes.md](./docs/recipes.md): production compositions (app shell, master-detail dialog, composer form, …) as copyable references.
|
|
106
|
+
- [app-layouts.md](./docs/app-layouts.md): choosing `NavStack`/`SplitView`/`Workbench`/`TabScaffold` by data + interaction + device, and dialog presentation.
|
|
107
|
+
- [webawesome-theme.md](./docs/webawesome-theme.md): the optional Web Awesome theme; catalog coverage means themed-and-supported, not preferred (prefer the Kerf primitive when one exists).
|
|
108
|
+
- [component-catalog.json](./ai/component-catalog.json) + [public-api-signatures-v1.md](./ai/public-api-signatures-v1.md) + [webawesome-jsx-signatures-v1.md](./ai/webawesome-jsx-signatures-v1.md): machine-readable facts and exact signatures.
|
|
109
|
+
- [README.md](./README.md): package overview and the component subpath table.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kerfjs/ui",
|
|
3
|
-
"version": "5.0.0-beta.
|
|
3
|
+
"version": "5.0.0-beta.6",
|
|
4
4
|
"description": "Accessible, composable UI primitives for kerf applications.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -88,6 +88,10 @@
|
|
|
88
88
|
"types": "./dist/wire-resizable-regions.d.ts",
|
|
89
89
|
"import": "./dist/wire-resizable-regions.js"
|
|
90
90
|
},
|
|
91
|
+
"./device-class": {
|
|
92
|
+
"types": "./dist/device-class.d.ts",
|
|
93
|
+
"import": "./dist/device-class.js"
|
|
94
|
+
},
|
|
91
95
|
"./app-tab": {
|
|
92
96
|
"types": "./dist/app-tab.d.ts",
|
|
93
97
|
"browser": "./dist/browser/app-tab.js",
|
|
@@ -102,10 +106,29 @@
|
|
|
102
106
|
"types": "./dist/wire-tab-bars.d.ts",
|
|
103
107
|
"import": "./dist/wire-tab-bars.js"
|
|
104
108
|
},
|
|
105
|
-
"./
|
|
106
|
-
"types": "./dist/
|
|
107
|
-
"
|
|
108
|
-
|
|
109
|
+
"./nav-stack": {
|
|
110
|
+
"types": "./dist/nav-stack.d.ts",
|
|
111
|
+
"import": "./dist/nav-stack.js"
|
|
112
|
+
},
|
|
113
|
+
"./wire-nav-stack": {
|
|
114
|
+
"types": "./dist/wire-nav-stack.d.ts",
|
|
115
|
+
"import": "./dist/wire-nav-stack.js"
|
|
116
|
+
},
|
|
117
|
+
"./split-view": {
|
|
118
|
+
"types": "./dist/split-view.d.ts",
|
|
119
|
+
"import": "./dist/split-view.js"
|
|
120
|
+
},
|
|
121
|
+
"./workbench": {
|
|
122
|
+
"types": "./dist/workbench.d.ts",
|
|
123
|
+
"import": "./dist/workbench.js"
|
|
124
|
+
},
|
|
125
|
+
"./tab-scaffold": {
|
|
126
|
+
"types": "./dist/tab-scaffold.d.ts",
|
|
127
|
+
"import": "./dist/tab-scaffold.js"
|
|
128
|
+
},
|
|
129
|
+
"./wire-tab-scaffold": {
|
|
130
|
+
"types": "./dist/wire-tab-scaffold.d.ts",
|
|
131
|
+
"import": "./dist/wire-tab-scaffold.js"
|
|
109
132
|
},
|
|
110
133
|
"./loading-spinner": {
|
|
111
134
|
"types": "./dist/loading-spinner.d.ts",
|
|
@@ -149,10 +172,10 @@
|
|
|
149
172
|
"browser": "./dist/browser/empty-state.js",
|
|
150
173
|
"import": "./dist/empty-state.js"
|
|
151
174
|
},
|
|
152
|
-
"./
|
|
153
|
-
"types": "./dist/
|
|
154
|
-
"browser": "./dist/browser/
|
|
155
|
-
"import": "./dist/
|
|
175
|
+
"./panel-header": {
|
|
176
|
+
"types": "./dist/panel-header.d.ts",
|
|
177
|
+
"browser": "./dist/browser/panel-header.js",
|
|
178
|
+
"import": "./dist/panel-header.js"
|
|
156
179
|
},
|
|
157
180
|
"./value-table": {
|
|
158
181
|
"types": "./dist/value-table.d.ts",
|
|
@@ -178,14 +201,17 @@
|
|
|
178
201
|
"./resizable-region.css": "./dist/styles/resizable-region.css",
|
|
179
202
|
"./app-tab.css": "./dist/styles/app-tab.css",
|
|
180
203
|
"./tab-bar.css": "./dist/styles/tab-bar.css",
|
|
181
|
-
"./
|
|
204
|
+
"./nav-stack.css": "./dist/styles/nav-stack.css",
|
|
205
|
+
"./split-view.css": "./dist/styles/split-view.css",
|
|
206
|
+
"./workbench.css": "./dist/styles/workbench.css",
|
|
207
|
+
"./tab-scaffold.css": "./dist/styles/tab-scaffold.css",
|
|
182
208
|
"./loading-spinner.css": "./dist/styles/loading-spinner.css",
|
|
183
209
|
"./select.css": "./dist/styles/select.css",
|
|
184
210
|
"./segmented-control.css": "./dist/styles/segmented-control.css",
|
|
185
211
|
"./token-search-field.css": "./dist/styles/token-search-field.css",
|
|
186
212
|
"./state-banner.css": "./dist/styles/state-banner.css",
|
|
187
213
|
"./empty-state.css": "./dist/styles/empty-state.css",
|
|
188
|
-
"./
|
|
214
|
+
"./panel-header.css": "./dist/styles/panel-header.css",
|
|
189
215
|
"./value-table.css": "./dist/styles/value-table.css",
|
|
190
216
|
"./ai/*": "./ai/*",
|
|
191
217
|
"./docs/*": "./docs/*"
|
|
@@ -4,7 +4,7 @@ import './recipes.css';
|
|
|
4
4
|
import { LucideIcon } from '@kerfjs/ui/lucide-icon';
|
|
5
5
|
import { MenuHeader } from '@kerfjs/ui/menu-header';
|
|
6
6
|
import { MenuItem } from '@kerfjs/ui/menu-item';
|
|
7
|
-
import {
|
|
7
|
+
import { PanelHeader } from '@kerfjs/ui/panel-header';
|
|
8
8
|
import { ResizableRegion } from '@kerfjs/ui/resizable-region';
|
|
9
9
|
import { Toolbar } from '@kerfjs/ui/toolbar';
|
|
10
10
|
import { ToolbarControlGroup } from '@kerfjs/ui/toolbar-control-group';
|
|
@@ -24,8 +24,8 @@ export const createRecipe: RecipeFactory = (announce) => {
|
|
|
24
24
|
<Toolbar label="Atlas workspace" leading={<ToolbarControlGroup appearance="borderless" single><ToolbarText text="Atlas" /></ToolbarControlGroup>} trailing={<div class="recipe-shell__toolbar-actions"><ToolbarControlGroup className="recipe-shell__responsive-controls" appearance="borderless" label="Workspace panes"><button type="button" aria-label="Show navigation" aria-controls="recipe-shell-navigation" aria-pressed={String(responsivePane.value === 'navigation')} data-action="recipe-action" data-recipe-command="show-navigation"><LucideIcon icon={Folder} name="folder" /></button><button type="button" aria-label="Show content" aria-controls="recipe-shell-content" aria-pressed={String(responsivePane.value === 'content')} data-action="recipe-action" data-recipe-command="show-content"><LucideIcon icon={Inbox} name="inbox" /></button><button type="button" aria-label="Show inspector" aria-controls="recipe-shell-inspector" aria-pressed={String(responsivePane.value === 'inspector')} data-action="recipe-action" data-recipe-command="show-inspector"><LucideIcon icon={Settings} name="settings" /></button></ToolbarControlGroup><ToolbarControlGroup appearance="borderless" label="Workspace controls"><button type="button" aria-label="Notifications" data-action="recipe-action" data-recipe-command="notify"><LucideIcon icon={Bell} name="bell" /></button><button type="button" aria-label="Settings" data-action="recipe-action" data-recipe-command="settings"><LucideIcon icon={Settings} name="settings" /></button></ToolbarControlGroup></div>} />
|
|
25
25
|
<div class="recipe-shell__body">
|
|
26
26
|
<div id="recipe-shell-navigation" class="recipe-shell__nav-region"><ResizableRegion id="recipe-navigation" label="Navigation" size={navigationSize.value} min={180} max={320}><aside class="recipe-shell__nav kui-pane"><MenuHeader label="Workspace" /><nav class="recipe-shell__nav-list kui-pane__content kui-content" aria-label="Workspace"><section><MenuItem action="recipe-action" itemId="inbox" label="Inbox" icon={<LucideIcon icon={Inbox} name="inbox" />} selected={selected.value === 'inbox'} /><MenuItem action="recipe-action" itemId="projects" label="Projects with a deliberately wrapping title" icon={<LucideIcon icon={Folder} name="folder" />} selected={selected.value === 'projects'} multiline /></section></nav></aside></ResizableRegion></div>
|
|
27
|
-
<main id="recipe-shell-content" class="recipe-shell__main kui-pane"><
|
|
28
|
-
<div id="recipe-shell-inspector" class="recipe-shell__inspector-region"><ResizableRegion id="recipe-inspector" label="Inspector" size={inspectorSize.value} min={200} max={360} edge="start"><aside class="recipe-shell__inspector kui-pane"><
|
|
27
|
+
<main id="recipe-shell-content" class="recipe-shell__main kui-pane"><PanelHeader title={selected.value === 'inbox' ? 'Inbox triage' : 'Active projects'} titleId="recipe-shell-main-title" actions={<button class="kui-recipe__button" data-primary="true" type="button" data-action="recipe-action" data-recipe-command="new">New task</button>} /><div class="recipe-shell__main-body kui-pane__content kui-content"><p class="kui-recipe__ownership kui-content-item">Recipe owns pane geometry and one scroll owner per pane. The app owns routing, data, pane visibility, sizes, and persistence.</p><div class="recipe-shell__cards">{['Release accessibility audit', 'Prepare tablet navigation', 'Review stale-data states', 'Confirm package boundaries'].map((title) => <article class="recipe-shell__card kui-content-item"><strong>{title}</strong><p class="kui-recipe__muted">Assigned to the interface systems team · due this week</p></article>)}</div></div></main>
|
|
28
|
+
<div id="recipe-shell-inspector" class="recipe-shell__inspector-region"><ResizableRegion id="recipe-inspector" label="Inspector" size={inspectorSize.value} min={200} max={360} edge="start"><aside class="recipe-shell__inspector kui-pane"><PanelHeader title="Inspector" titleId="recipe-shell-inspector-title" /><div class="recipe-shell__inspector-body kui-pane__content kui-content"><ValueTable label="Selected task"><ValueTableRow label="Status" value="In review" /><ValueTableRow label="Owner" value="Mara Chen" /><ValueTableRow label="Priority" value="High" /></ValueTable></div></aside></ResizableRegion></div>
|
|
29
29
|
</div>
|
|
30
30
|
</section>;
|
|
31
31
|
return { render, action(command, element) { const pane = command.match(/^show-(navigation|content|inspector)$/)?.[1] as typeof responsivePane.value | undefined; if (pane) responsivePane.value = pane; const id = element.dataset.itemId; if (id) selected.value = id; announce(id ? `Opened ${id}` : pane ? `Showing ${pane}` : command === 'new' ? 'New task requested' : `${command} requested`); }, resize(id, size) { if (id === 'recipe-navigation') navigationSize.value = size; if (id === 'recipe-inspector') inspectorSize.value = size; announce(`${id} resized to ${size}px`); } };
|
|
@@ -6,6 +6,7 @@ import '@awesome.me/webawesome/dist/components/input/input.js';
|
|
|
6
6
|
import '@awesome.me/webawesome/dist/components/textarea/textarea.js';
|
|
7
7
|
import './recipes.css';
|
|
8
8
|
|
|
9
|
+
import { PanelHeader } from '@kerfjs/ui/panel-header';
|
|
9
10
|
import { Select } from '@kerfjs/ui/select';
|
|
10
11
|
import { StateBanner } from '@kerfjs/ui/state-banner';
|
|
11
12
|
import { signal } from 'kerfjs';
|
|
@@ -28,16 +29,16 @@ export const createRecipe: RecipeFactory = (announce) => {
|
|
|
28
29
|
const body = signal('The tablet layout now keeps navigation, content, and inspector focus order aligned.');
|
|
29
30
|
const audience = signal('team');
|
|
30
31
|
const status = signal<'idle' | 'error' | 'saved'>('idle');
|
|
31
|
-
const render = () => <form class="kui-recipe recipe-form kui-recipe__surface kui-content" data-recipe="recipe-composer-form" noValidate>
|
|
32
|
-
<
|
|
32
|
+
const render = () => <form class="kui-recipe recipe-form kui-recipe__surface kui-content" data-recipe="recipe-composer-form" aria-labelledby="recipe-composer-title" aria-describedby="recipe-composer-summary" noValidate>
|
|
33
|
+
<PanelHeader title="Publish workspace update" titleId="recipe-composer-title" summary="Share a concise, actionable update with collaborators." summaryId="recipe-composer-summary" />
|
|
33
34
|
{status.value === 'error' && <StateBanner title="Add a title before publishing" detail="The update body and audience are preserved." tone="danger" urgency="alert" />}
|
|
34
35
|
{status.value === 'saved' && <StateBanner title="Update published" detail="The team audience can now read it." tone="success" />}
|
|
35
|
-
<div class="recipe-form__section recipe-form__fields
|
|
36
|
+
<div class="recipe-form__section recipe-form__fields">
|
|
36
37
|
<wa-input name="recipe-title" label="Update title" hint="Summarize the outcome in one line." required value={title.value}></wa-input>
|
|
37
38
|
<wa-textarea name="recipe-body" label="Details" hint="Include decisions, owners, and the next checkpoint." rows="5" maxlength="400" with-count value={body.value}></wa-textarea>
|
|
38
39
|
<Select name="recipe-audience" value={audience.value} label="Audience" choices={[{ value: 'team', label: 'Workspace team' }, { value: 'reviewers', label: 'Reviewers' }, { value: 'organization', label: 'Entire organization' }]} />
|
|
39
40
|
</div>
|
|
40
|
-
<footer class="recipe-form__section recipe-form__footer
|
|
41
|
+
<footer class="recipe-form__section recipe-form__footer"><div class="recipe-form__actions kui-control-cluster"><wa-button appearance="outlined" data-action="recipe-action" data-recipe-command="reset">Reset</wa-button><wa-button variant="brand" appearance="accent" data-action="recipe-action" data-recipe-command="submit">Publish update</wa-button></div><p class="kui-recipe__ownership">The recipe owns field, message, and action rhythm. The app owns validation rules, draft persistence, permissions, and transport.</p></footer>
|
|
41
42
|
</form>;
|
|
42
43
|
return {
|
|
43
44
|
render,
|
|
@@ -3,7 +3,7 @@ import './recipes.css';
|
|
|
3
3
|
|
|
4
4
|
import { EmptyState } from '@kerfjs/ui/empty-state';
|
|
5
5
|
import { LoadingSpinner } from '@kerfjs/ui/loading-spinner';
|
|
6
|
-
import {
|
|
6
|
+
import { PanelHeader } from '@kerfjs/ui/panel-header';
|
|
7
7
|
import { StateBanner } from '@kerfjs/ui/state-banner';
|
|
8
8
|
import { signal } from 'kerfjs';
|
|
9
9
|
|
|
@@ -21,6 +21,6 @@ export const createRecipe: RecipeFactory = (announce) => {
|
|
|
21
21
|
if (state.value === 'error') return <StateBanner title="Release tasks could not be refreshed" detail="Existing filters are preserved. Try again when the connection recovers." tone="danger" urgency="alert" action={<button class="kui-recipe__button" type="button" data-action="recipe-action" data-recipe-command="retry">Retry</button>} />;
|
|
22
22
|
return <>{state.value === 'stale' && <StateBanner title="Showing saved results" detail="Refreshing in the background." tone="warning" action={<span class="kui-inline-metadata"><LoadingSpinner label="Refreshing release tasks" /><button class="kui-recipe__button" type="button" data-action="recipe-action" data-recipe-command="finish">Finish refresh</button></span>} />}{populated()}</>;
|
|
23
23
|
};
|
|
24
|
-
const render = () => <section class="kui-recipe recipe-list kui-recipe__surface kui-pane" data-recipe="recipe-list-workspace-states" data-list-state={state.value}><
|
|
24
|
+
const render = () => <section class="kui-recipe recipe-list kui-recipe__surface kui-pane" data-recipe="recipe-list-workspace-states" data-list-state={state.value}><PanelHeader title="Release tasks" titleId="recipe-list-title" actions={<div class="kui-control-cluster"><button class="kui-recipe__button" type="button" data-action="recipe-action" data-recipe-command="empty">Clear</button><button class="kui-recipe__button" type="button" data-action="recipe-action" data-recipe-command="fail">Simulate failure</button><button class="kui-recipe__button" data-primary="true" type="button" data-action="recipe-action" data-recipe-command="refresh">Refresh</button></div>} /><div class="recipe-list__body kui-pane__content kui-content">{renderBody()}<p class="kui-recipe__ownership kui-content-item">The recipe owns feedback placement and stable content. The app owns fetching, cache age, retry policy, and domain rows.</p></div></section>;
|
|
25
25
|
return { render, action(command) { const next: Record<string, ListState> = { load: 'populated', empty: 'empty', create: 'populated', fail: 'error', retry: 'populated', refresh: 'stale', finish: 'populated' }; if (command in next) state.value = next[command]!; announce(`List state: ${state.value}`); } };
|
|
26
26
|
};
|
|
@@ -8,6 +8,7 @@ export const recipeIds = [
|
|
|
8
8
|
'recipe-composer-form',
|
|
9
9
|
'recipe-list-workspace-states',
|
|
10
10
|
'recipe-compact-toolbar',
|
|
11
|
+
'recipe-navigation-stack',
|
|
11
12
|
] as const;
|
|
12
13
|
|
|
13
14
|
export type RecipeId = typeof recipeIds[number];
|
|
@@ -20,6 +21,7 @@ export const recipeLoaders: Record<RecipeId, () => Promise<{ createRecipe: Recip
|
|
|
20
21
|
'recipe-composer-form': () => import('./composer-form.js'),
|
|
21
22
|
'recipe-list-workspace-states': () => import('./list-workspace-states.js'),
|
|
22
23
|
'recipe-compact-toolbar': () => import('./compact-toolbar.js'),
|
|
24
|
+
'recipe-navigation-stack': () => import('./navigation-stack.js'),
|
|
23
25
|
};
|
|
24
26
|
|
|
25
27
|
export function isRecipeId(value: string): value is RecipeId {
|
|
@@ -4,10 +4,10 @@ import '@awesome.me/webawesome/dist/components/button/button.js';
|
|
|
4
4
|
import '@awesome.me/webawesome/dist/components/dialog/dialog.js';
|
|
5
5
|
import './recipes.css';
|
|
6
6
|
|
|
7
|
-
import { DialogHeader } from '@kerfjs/ui/dialog-header';
|
|
8
7
|
import { LucideIcon } from '@kerfjs/ui/lucide-icon';
|
|
9
8
|
import { MenuHeader } from '@kerfjs/ui/menu-header';
|
|
10
9
|
import { MenuItem } from '@kerfjs/ui/menu-item';
|
|
10
|
+
import { PanelHeader } from '@kerfjs/ui/panel-header';
|
|
11
11
|
import { ValueTable, ValueTableRow } from '@kerfjs/ui/value-table';
|
|
12
12
|
import { signal } from 'kerfjs';
|
|
13
13
|
import { FileText } from 'lucide';
|
|
@@ -27,7 +27,7 @@ interface RecipeDialogElement extends HTMLElement {
|
|
|
27
27
|
export const createRecipe: RecipeFactory = (announce) => {
|
|
28
28
|
const selected = signal<keyof typeof records>('alpha');
|
|
29
29
|
const open = { value: false };
|
|
30
|
-
const render = () => <section class="kui-recipe kui-content" data-recipe="recipe-master-detail-dialog"><button class="kui-recipe__button kui-content-item" data-primary="true" type="button" data-action="recipe-action" data-recipe-command="open">Open project details</button><p class="kui-recipe__ownership kui-content-item">Web Awesome owns modal focus and dismissal. The recipe owns header/body/master-detail anatomy; the app owns open state, selection, and policy.</p><wa-dialog class="recipe-dialog" label="Project details" without-header open={open.value}><div class="recipe-dialog__pane kui-pane"><
|
|
30
|
+
const render = () => <section class="kui-recipe kui-content" data-recipe="recipe-master-detail-dialog"><button class="kui-recipe__button kui-content-item" data-primary="true" type="button" data-action="recipe-action" data-recipe-command="open">Open project details</button><p class="kui-recipe__ownership kui-content-item">Web Awesome owns modal focus and dismissal. The recipe owns header/body/master-detail anatomy; the app owns open state, selection, and policy.</p><wa-dialog class="recipe-dialog" label="Project details" without-header open={open.value}><div class="recipe-dialog__pane kui-pane"><PanelHeader title="Project details" titleId="recipe-dialog-title" summary="Compare delivery state without leaving the workspace." summaryId="recipe-dialog-summary" icon={<LucideIcon icon={FileText} name="file-text" />} actions={<wa-button appearance="plain" data-action="recipe-action" data-recipe-command="close">Close</wa-button>} /><div class="recipe-master-detail kui-pane__content"><nav class="recipe-master-detail__master kui-pane" aria-label="Projects"><MenuHeader label="Recent projects" /><div class="kui-pane__content kui-content"><section>{Object.entries(records).map(([id, record]) => <MenuItem action="recipe-action" itemId={id} label={record.name} selected={selected.value === id} multiline />)}</section></div></nav><section class="recipe-master-detail__detail kui-content" aria-live="polite"><h3 class="recipe-master-detail__title kui-recipe__pane-title kui-content-item">{records[selected.value].name}</h3><ValueTable label="Project details"><ValueTableRow label="Owner" value={records[selected.value].owner} /><ValueTableRow label="Status" value={records[selected.value].state} /><ValueTableRow label="Updated" value={records[selected.value].updated} /></ValueTable><div class="recipe-master-detail__actions kui-control-cluster"><wa-button appearance="outlined" data-action="recipe-action" data-recipe-command="archive">Archive</wa-button><wa-button variant="brand" appearance="accent" data-action="recipe-action" data-recipe-command="open-record">Open project</wa-button></div></section></div></div></wa-dialog></section>;
|
|
31
31
|
return {
|
|
32
32
|
render,
|
|
33
33
|
action(command, element) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { wireNavStack } from '@kerfjs/ui/wire-nav-stack';
|
|
1
2
|
import { wireResizableRegions } from '@kerfjs/ui/wire-resizable-regions';
|
|
2
3
|
import { delegate, mount } from 'kerfjs';
|
|
3
4
|
import { delegateActions } from 'kerfjs/actions';
|
|
@@ -23,11 +24,13 @@ export function mountRecipe(root: HTMLElement, controller: RecipeController): ()
|
|
|
23
24
|
const stopResize = wireResizableRegions(root, {
|
|
24
25
|
onCommit: ({ id, size }) => controller.resize?.(id, size),
|
|
25
26
|
});
|
|
27
|
+
const stopNav = wireNavStack(root, { onBack: () => controller.action('nav-back', root) });
|
|
26
28
|
|
|
27
29
|
let disposed = false;
|
|
28
30
|
return () => {
|
|
29
31
|
if (disposed) return;
|
|
30
32
|
disposed = true;
|
|
33
|
+
stopNav();
|
|
31
34
|
stopResize();
|
|
32
35
|
stopDialogs();
|
|
33
36
|
stopInputs();
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import '@kerfjs/ui/layout.css';
|
|
2
|
+
import '@kerfjs/ui/nav-stack.css';
|
|
3
|
+
import '@kerfjs/ui/toolbar-text.css';
|
|
4
|
+
import './recipes.css';
|
|
5
|
+
|
|
6
|
+
import { LucideIcon } from '@kerfjs/ui/lucide-icon';
|
|
7
|
+
import { MenuHeader } from '@kerfjs/ui/menu-header';
|
|
8
|
+
import { MenuItem } from '@kerfjs/ui/menu-item';
|
|
9
|
+
import { NavStack, type NavStackView } from '@kerfjs/ui/nav-stack';
|
|
10
|
+
import { signal } from 'kerfjs';
|
|
11
|
+
import { ChevronRight, FileText, Folder } from 'lucide';
|
|
12
|
+
|
|
13
|
+
import type { RecipeFactory } from './types.js';
|
|
14
|
+
|
|
15
|
+
interface LibraryItem {
|
|
16
|
+
id: string;
|
|
17
|
+
label: string;
|
|
18
|
+
detail: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const ITEMS: LibraryItem[] = [
|
|
22
|
+
{ id: 'tokens', label: 'Design tokens', detail: 'Semantic color, spacing, and radius scales shared across every surface.' },
|
|
23
|
+
{ id: 'controls', label: 'Toolbar controls', detail: 'Control groups and segmented controls with their hover and pressed contracts.' },
|
|
24
|
+
{ id: 'layouts', label: 'App layouts', detail: 'Navigation stacks, split views, workbench, and bottom-tab scaffolds.' },
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
export const createRecipe: RecipeFactory = (announce) => {
|
|
28
|
+
const listView = (): NavStackView => ({
|
|
29
|
+
key: 'library',
|
|
30
|
+
title: 'Library',
|
|
31
|
+
content: <div class="recipe-navstack__view kui-pane__content kui-content">
|
|
32
|
+
<section>
|
|
33
|
+
<MenuHeader label="Components" />
|
|
34
|
+
{ITEMS.map((item) => <MenuItem action="recipe-action" itemId={item.id} label={item.label} icon={<LucideIcon icon={Folder} name="folder" />} trailing={<LucideIcon icon={ChevronRight} name="chevron-right" />} />)}
|
|
35
|
+
</section>
|
|
36
|
+
<p class="kui-recipe__ownership kui-content-item">The recipe owns the stack as a signal of views and pushes/pops it; `NavStack` renders the stack and `wireNavStack` slides the content and settles the chrome. The app owns selection, data, and routing.</p>
|
|
37
|
+
</div>,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const detailView = (item: LibraryItem): NavStackView => ({
|
|
41
|
+
key: item.id,
|
|
42
|
+
title: item.label,
|
|
43
|
+
content: <div class="recipe-navstack__view kui-pane__content kui-content">
|
|
44
|
+
<div class="kui-content-item">
|
|
45
|
+
<LucideIcon icon={FileText} name="file-text" />
|
|
46
|
+
<strong>{item.label}</strong>
|
|
47
|
+
<p class="kui-recipe__muted">{item.detail}</p>
|
|
48
|
+
</div>
|
|
49
|
+
</div>,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const views = signal<NavStackView[]>([listView()]);
|
|
53
|
+
|
|
54
|
+
const render = () => <section class="kui-recipe recipe-navstack kui-recipe__surface" data-recipe="recipe-navigation-stack">
|
|
55
|
+
<div class="recipe-navstack__stage"><NavStack id="recipe-nav" label="Component library" views={views.value} /></div>
|
|
56
|
+
</section>;
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
render,
|
|
60
|
+
action(command, element) {
|
|
61
|
+
if (command === 'nav-back') {
|
|
62
|
+
if (views.value.length > 1) {
|
|
63
|
+
views.value = views.value.slice(0, -1);
|
|
64
|
+
announce('Navigated back');
|
|
65
|
+
}
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const id = element.dataset.itemId;
|
|
69
|
+
const item = ITEMS.find((candidate) => candidate.id === id);
|
|
70
|
+
if (item) {
|
|
71
|
+
views.value = [...views.value, detailView(item)];
|
|
72
|
+
announce(`Opened ${item.label}`);
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
};
|