@kerfjs/ui 5.0.0-beta.17 → 5.0.0-beta.18
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 +108 -79
- package/ai/component-catalog-extension.schema.json +79 -0
- package/ai/component-catalog.json +499 -0
- package/ai/component-catalog.schema.json +15 -0
- package/ai/public-api-signatures-v1.md +67 -31
- package/ai/skill.md +58 -49
- package/ai/webawesome-jsx-signatures-v1.md +1 -1
- package/dist/app-tab.d.ts +1 -1
- package/dist/app-tab.js +1 -1
- package/dist/catalog.d.ts +10 -3
- package/dist/catalog.js +279 -90
- package/dist/catalog.js.map +1 -1
- package/dist/chunk-43FUQH35.js +83 -0
- package/dist/chunk-43FUQH35.js.map +1 -0
- package/dist/{chunk-YGN7OMLT.js → chunk-4TRDV6RL.js} +2 -2
- package/dist/chunk-4TRDV6RL.js.map +1 -0
- package/dist/chunk-6ETZKKAE.js +38 -0
- package/dist/chunk-6ETZKKAE.js.map +1 -0
- package/dist/chunk-6WTETUZ6.js +21 -0
- package/dist/chunk-6WTETUZ6.js.map +1 -0
- package/dist/chunk-7L6EHUMV.js +31 -0
- package/dist/chunk-7L6EHUMV.js.map +1 -0
- package/dist/chunk-BBRXX6PF.js +108 -0
- package/dist/chunk-BBRXX6PF.js.map +1 -0
- package/dist/chunk-DPDBVKNA.js +113 -0
- package/dist/chunk-DPDBVKNA.js.map +1 -0
- package/dist/chunk-ELSJX4W7.js +68 -0
- package/dist/chunk-ELSJX4W7.js.map +1 -0
- package/dist/{chunk-6NLRBSVX.js → chunk-ENVTNLYC.js} +125 -68
- package/dist/chunk-ENVTNLYC.js.map +1 -0
- package/dist/{chunk-AHQI52QO.js → chunk-FEY65TBV.js} +6 -2
- package/dist/chunk-FEY65TBV.js.map +1 -0
- package/dist/chunk-JTHUXDN2.js +58 -0
- package/dist/chunk-JTHUXDN2.js.map +1 -0
- package/dist/chunk-LPOMRC4C.js +25 -0
- package/dist/chunk-LPOMRC4C.js.map +1 -0
- package/dist/chunk-NENBUYX5.js +79 -0
- package/dist/chunk-NENBUYX5.js.map +1 -0
- package/dist/{chunk-XXMOARTH.js → chunk-NVWXKU37.js} +57 -16
- package/dist/chunk-NVWXKU37.js.map +1 -0
- package/dist/chunk-PIDTEQ3U.js +105 -0
- package/dist/chunk-PIDTEQ3U.js.map +1 -0
- package/dist/chunk-PX3YSS5C.js +32 -0
- package/dist/chunk-PX3YSS5C.js.map +1 -0
- package/dist/chunk-PZQVSDLH.js +61 -0
- package/dist/chunk-PZQVSDLH.js.map +1 -0
- package/dist/chunk-QIRXYKCE.js +113 -0
- package/dist/chunk-QIRXYKCE.js.map +1 -0
- package/dist/chunk-QO55FN2Y.js +20 -0
- package/dist/chunk-QO55FN2Y.js.map +1 -0
- package/dist/chunk-QQFJMRIB.js +45 -0
- package/dist/chunk-QQFJMRIB.js.map +1 -0
- package/dist/chunk-S3HKPI7I.js +218 -0
- package/dist/chunk-S3HKPI7I.js.map +1 -0
- package/dist/{chunk-WXMGNX2Y.js → chunk-S3PCZ7NV.js} +2 -2
- package/dist/chunk-S3PCZ7NV.js.map +1 -0
- package/dist/{chunk-7BR3DO2A.js → chunk-SRSJO5QE.js} +13 -4
- package/dist/chunk-SRSJO5QE.js.map +1 -0
- package/dist/chunk-U5IFGQCD.js +117 -0
- package/dist/chunk-U5IFGQCD.js.map +1 -0
- package/dist/{chunk-56RJXPHG.js → chunk-Y6LYKFXY.js} +1 -1
- package/dist/{chunk-56RJXPHG.js.map → chunk-Y6LYKFXY.js.map} +1 -1
- package/dist/chunk-ZK2G3VR5.js +35 -0
- package/dist/chunk-ZK2G3VR5.js.map +1 -0
- package/dist/chunk-ZVI5YST2.js +41 -0
- package/dist/chunk-ZVI5YST2.js.map +1 -0
- package/dist/collapsible-panel.d.ts +2 -2
- package/dist/collapsible-panel.js +47 -6
- package/dist/collapsible-panel.js.map +1 -1
- package/dist/device-class.js +33 -5
- package/dist/device-class.js.map +1 -1
- package/dist/disclosure-arrow.js +1 -1
- package/dist/empty-state.d.ts +1 -1
- package/dist/empty-state.js +1 -1
- package/dist/floating-toolbar.d.ts +1 -1
- package/dist/floating-toolbar.js +1 -1
- package/dist/index.js +25 -25
- package/dist/list-action-row.d.ts +1 -1
- package/dist/list-action-row.js +1 -1
- package/dist/list-header.d.ts +1 -1
- package/dist/list-header.js +1 -1
- package/dist/list-inset-control.d.ts +1 -1
- package/dist/list-inset-control.js +1 -1
- package/dist/list-inset-text.d.ts +1 -1
- package/dist/list-inset-text.js +1 -1
- package/dist/list-item.d.ts +1 -1
- package/dist/list-item.js +1 -1
- package/dist/loading-spinner.js +1 -1
- package/dist/nav-stack.d.ts +1 -1
- package/dist/nav-stack.js +1 -1
- package/dist/panel-header.d.ts +1 -1
- package/dist/panel-header.js +1 -1
- package/dist/resizable-region.d.ts +1 -1
- package/dist/resizable-region.js +1 -1
- package/dist/segmented-control.js +1 -1
- package/dist/select.d.ts +1 -1
- package/dist/select.js +1 -1
- package/dist/skeleton.d.ts +1 -1
- package/dist/skeleton.js +1 -1
- package/dist/split-view.d.ts +1 -1
- package/dist/split-view.js +70 -10
- package/dist/split-view.js.map +1 -1
- package/dist/state-banner.d.ts +1 -1
- package/dist/state-banner.js +1 -1
- package/dist/styles/catalog.css +26 -0
- package/dist/tab-bar.d.ts +1 -1
- package/dist/tab-bar.js +1 -1
- package/dist/tab-scaffold.d.ts +1 -1
- package/dist/tab-scaffold.js +43 -8
- package/dist/tab-scaffold.js.map +1 -1
- package/dist/token-search-field.js +1 -1
- package/dist/toolbar-control-group.d.ts +1 -1
- package/dist/toolbar-control-group.js +1 -1
- package/dist/toolbar-text.js +1 -1
- package/dist/toolbar.d.ts +1 -1
- package/dist/toolbar.js +1 -1
- package/dist/value-table.d.ts +2 -2
- package/dist/value-table.js +1 -1
- package/dist/wire-catalog.d.ts +31 -2
- package/dist/wire-catalog.js +203 -8
- package/dist/wire-catalog.js.map +1 -1
- package/dist/wire-nav-stack.js +22 -7
- package/dist/wire-nav-stack.js.map +1 -1
- package/dist/wire-resizable-regions.d.ts +1 -1
- package/dist/wire-resizable-regions.js +1 -1
- package/dist/wire-sidebar.js +52 -41
- package/dist/wire-sidebar.js.map +1 -1
- package/dist/wire-tab-bars.js +1 -1
- package/dist/wire-tab-scaffold.js +9 -4
- package/dist/wire-tab-scaffold.js.map +1 -1
- package/dist/wire-token-search-fields.d.ts +1 -1
- package/dist/wire-token-search-fields.js +1 -1
- package/dist/workbench.d.ts +1 -1
- package/dist/workbench.js +56 -9
- package/dist/workbench.js.map +1 -1
- package/docs/app-layouts.md +7 -7
- package/docs/catalog.md +108 -24
- package/docs/collapsible-panel.md +38 -11
- package/docs/component-contract.md +30 -2
- package/docs/component-selection.md +71 -63
- package/docs/design/templates.md +1 -1
- package/docs/design-philosophy.md +5 -5
- package/docs/device-class.md +8 -8
- package/docs/examples/command-palette-adapter.tsx +67 -17
- package/docs/examples/component-catalog-extension.json +45 -0
- package/docs/layout.md +19 -19
- package/docs/nav-stack.md +15 -6
- package/docs/recipes.md +4 -4
- package/docs/split-view.md +3 -3
- package/docs/tab-scaffold.md +16 -7
- package/docs/webawesome-theme.md +8 -8
- package/docs/workbench.md +18 -5
- package/llms.txt +20 -14
- package/package.json +6 -4
- package/ux-demo/recipes/app-shell.tsx +206 -10
- package/ux-demo/recipes/collapsible-sidebar.tsx +167 -44
- package/ux-demo/recipes/compact-toolbar.tsx +114 -12
- package/ux-demo/recipes/composer-form.tsx +106 -18
- package/ux-demo/recipes/list-detail-dialog.tsx +144 -8
- package/ux-demo/recipes/list-workspace-states.tsx +165 -8
- package/ux-demo/recipes/loaders.ts +5 -2
- package/ux-demo/recipes/loading-inspector.tsx +124 -41
- package/ux-demo/recipes/mount-recipe.ts +27 -6
- package/ux-demo/recipes/navigation-sidebar.tsx +120 -9
- package/ux-demo/recipes/navigation-stack.tsx +61 -19
- package/ux-demo/recipes/types.ts +3 -1
- package/ux-demo/recipes/workspace-header.tsx +57 -2
- package/dist/chunk-2Z5TWHKQ.js +0 -34
- package/dist/chunk-2Z5TWHKQ.js.map +0 -1
- package/dist/chunk-37T3VNIZ.js +0 -19
- package/dist/chunk-37T3VNIZ.js.map +0 -1
- package/dist/chunk-55DATV3A.js +0 -10
- package/dist/chunk-55DATV3A.js.map +0 -1
- package/dist/chunk-5ATCDSQS.js +0 -42
- package/dist/chunk-5ATCDSQS.js.map +0 -1
- package/dist/chunk-5E2HVJ33.js +0 -10
- package/dist/chunk-5E2HVJ33.js.map +0 -1
- package/dist/chunk-6NLRBSVX.js.map +0 -1
- package/dist/chunk-7BR3DO2A.js.map +0 -1
- package/dist/chunk-7JB7VPRI.js +0 -34
- package/dist/chunk-7JB7VPRI.js.map +0 -1
- package/dist/chunk-7LDCTU7D.js +0 -134
- package/dist/chunk-7LDCTU7D.js.map +0 -1
- package/dist/chunk-AHQI52QO.js.map +0 -1
- package/dist/chunk-AQNKJRFH.js +0 -79
- package/dist/chunk-AQNKJRFH.js.map +0 -1
- package/dist/chunk-AVZHJN6C.js +0 -14
- package/dist/chunk-AVZHJN6C.js.map +0 -1
- package/dist/chunk-B24C4A6T.js +0 -22
- package/dist/chunk-B24C4A6T.js.map +0 -1
- package/dist/chunk-CH4SQ6H5.js +0 -39
- package/dist/chunk-CH4SQ6H5.js.map +0 -1
- package/dist/chunk-DAFFDO33.js +0 -14
- package/dist/chunk-DAFFDO33.js.map +0 -1
- package/dist/chunk-I7INRJO4.js +0 -17
- package/dist/chunk-I7INRJO4.js.map +0 -1
- package/dist/chunk-L4OKFDJY.js +0 -24
- package/dist/chunk-L4OKFDJY.js.map +0 -1
- package/dist/chunk-M5JRI6MN.js +0 -27
- package/dist/chunk-M5JRI6MN.js.map +0 -1
- package/dist/chunk-RNPGFQ33.js +0 -10
- package/dist/chunk-RNPGFQ33.js.map +0 -1
- package/dist/chunk-VPCTCUED.js +0 -10
- package/dist/chunk-VPCTCUED.js.map +0 -1
- package/dist/chunk-WRTAQ7BZ.js +0 -23
- package/dist/chunk-WRTAQ7BZ.js.map +0 -1
- package/dist/chunk-WVPYVDXY.js +0 -15
- package/dist/chunk-WVPYVDXY.js.map +0 -1
- package/dist/chunk-WXMGNX2Y.js.map +0 -1
- package/dist/chunk-XXMOARTH.js.map +0 -1
- package/dist/chunk-YGN7OMLT.js.map +0 -1
- package/dist/chunk-ZQQ6P4SV.js +0 -20
- package/dist/chunk-ZQQ6P4SV.js.map +0 -1
|
@@ -15,9 +15,24 @@ import { FileText } from 'lucide';
|
|
|
15
15
|
import type { RecipeFactory } from './types.js';
|
|
16
16
|
|
|
17
17
|
const records = {
|
|
18
|
-
alpha: {
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
alpha: {
|
|
19
|
+
name: 'Northstar migration',
|
|
20
|
+
owner: 'Mara Chen',
|
|
21
|
+
state: 'In review',
|
|
22
|
+
updated: 'Today, 09:42',
|
|
23
|
+
},
|
|
24
|
+
beta: {
|
|
25
|
+
name: 'Tablet navigation',
|
|
26
|
+
owner: 'Sam Rivera',
|
|
27
|
+
state: 'Ready',
|
|
28
|
+
updated: 'Yesterday, 16:20',
|
|
29
|
+
},
|
|
30
|
+
gamma: {
|
|
31
|
+
name: 'Contrast audit',
|
|
32
|
+
owner: 'Inez Okafor',
|
|
33
|
+
state: 'Blocked',
|
|
34
|
+
updated: 'Monday, 11:05',
|
|
35
|
+
},
|
|
21
36
|
} as const;
|
|
22
37
|
|
|
23
38
|
interface RecipeDialogElement extends HTMLElement {
|
|
@@ -27,15 +42,136 @@ interface RecipeDialogElement extends HTMLElement {
|
|
|
27
42
|
export const createRecipe: RecipeFactory = (announce) => {
|
|
28
43
|
const selected = signal<keyof typeof records>('alpha');
|
|
29
44
|
const open = { value: false };
|
|
30
|
-
const render = () =>
|
|
45
|
+
const render = () => (
|
|
46
|
+
<section
|
|
47
|
+
class="kui-recipe kui-content"
|
|
48
|
+
data-recipe="recipe-list-detail-dialog"
|
|
49
|
+
>
|
|
50
|
+
<button
|
|
51
|
+
class="kui-recipe__button kui-content-item"
|
|
52
|
+
data-primary="true"
|
|
53
|
+
type="button"
|
|
54
|
+
data-action="recipe-action"
|
|
55
|
+
data-recipe-command="open"
|
|
56
|
+
>
|
|
57
|
+
Open project details
|
|
58
|
+
</button>
|
|
59
|
+
<p class="kui-recipe__ownership kui-content-item">
|
|
60
|
+
Web Awesome owns modal focus and dismissal. The recipe owns
|
|
61
|
+
header/body/list-detail anatomy; the app owns open state, selection, and
|
|
62
|
+
policy.
|
|
63
|
+
</p>
|
|
64
|
+
<wa-dialog
|
|
65
|
+
class="recipe-dialog"
|
|
66
|
+
label="Project details"
|
|
67
|
+
without-header
|
|
68
|
+
open={open.value}
|
|
69
|
+
>
|
|
70
|
+
<div class="recipe-dialog__pane kui-pane">
|
|
71
|
+
<div class="recipe-list-detail kui-pane__content">
|
|
72
|
+
<nav
|
|
73
|
+
class="recipe-list-detail__list kui-pane"
|
|
74
|
+
aria-label="Projects"
|
|
75
|
+
>
|
|
76
|
+
<ListHeader label="Recent projects" />
|
|
77
|
+
<div class="kui-pane__content kui-content">
|
|
78
|
+
<section>
|
|
79
|
+
{Object.entries(records).map(([id, record]) => (
|
|
80
|
+
<ListItem
|
|
81
|
+
action="recipe-action"
|
|
82
|
+
itemId={id}
|
|
83
|
+
label={record.name}
|
|
84
|
+
selected={selected.value === id}
|
|
85
|
+
multiline
|
|
86
|
+
/>
|
|
87
|
+
))}
|
|
88
|
+
</section>
|
|
89
|
+
</div>
|
|
90
|
+
</nav>
|
|
91
|
+
<section class="recipe-list-detail__detail kui-pane">
|
|
92
|
+
<PanelHeader
|
|
93
|
+
title="Project details"
|
|
94
|
+
titleId="recipe-dialog-title"
|
|
95
|
+
summary="Compare delivery state without leaving the workspace."
|
|
96
|
+
summaryId="recipe-dialog-summary"
|
|
97
|
+
icon={<LucideIcon icon={FileText} name="file-text" />}
|
|
98
|
+
actions={
|
|
99
|
+
<wa-button
|
|
100
|
+
appearance="plain"
|
|
101
|
+
data-action="recipe-action"
|
|
102
|
+
data-recipe-command="close"
|
|
103
|
+
>
|
|
104
|
+
Close
|
|
105
|
+
</wa-button>
|
|
106
|
+
}
|
|
107
|
+
/>
|
|
108
|
+
<div
|
|
109
|
+
class="recipe-list-detail__body kui-pane__content kui-content"
|
|
110
|
+
aria-live="polite"
|
|
111
|
+
>
|
|
112
|
+
<h3 class="recipe-list-detail__title kui-recipe__pane-title kui-content-item">
|
|
113
|
+
{records[selected.value].name}
|
|
114
|
+
</h3>
|
|
115
|
+
<ValueTable label="Project details">
|
|
116
|
+
<ValueTableRow
|
|
117
|
+
label="Owner"
|
|
118
|
+
value={records[selected.value].owner}
|
|
119
|
+
/>
|
|
120
|
+
<ValueTableRow
|
|
121
|
+
label="Status"
|
|
122
|
+
value={records[selected.value].state}
|
|
123
|
+
/>
|
|
124
|
+
<ValueTableRow
|
|
125
|
+
label="Updated"
|
|
126
|
+
value={records[selected.value].updated}
|
|
127
|
+
/>
|
|
128
|
+
</ValueTable>
|
|
129
|
+
<div class="recipe-list-detail__actions kui-control-cluster">
|
|
130
|
+
<wa-button
|
|
131
|
+
appearance="outlined"
|
|
132
|
+
data-action="recipe-action"
|
|
133
|
+
data-recipe-command="archive"
|
|
134
|
+
>
|
|
135
|
+
Archive
|
|
136
|
+
</wa-button>
|
|
137
|
+
<wa-button
|
|
138
|
+
variant="brand"
|
|
139
|
+
appearance="accent"
|
|
140
|
+
data-action="recipe-action"
|
|
141
|
+
data-recipe-command="open-record"
|
|
142
|
+
>
|
|
143
|
+
Open project
|
|
144
|
+
</wa-button>
|
|
145
|
+
</div>
|
|
146
|
+
</div>
|
|
147
|
+
</section>
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
</wa-dialog>
|
|
151
|
+
</section>
|
|
152
|
+
);
|
|
31
153
|
return {
|
|
32
154
|
render,
|
|
33
155
|
action(command, element) {
|
|
34
156
|
const id = element.dataset.itemId;
|
|
35
|
-
if (id && id in records) {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
157
|
+
if (id && id in records) {
|
|
158
|
+
selected.value = id as keyof typeof records;
|
|
159
|
+
announce(`Selected ${records[selected.value].name}`);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
const dialog = element
|
|
163
|
+
.closest('[data-recipe]')
|
|
164
|
+
?.querySelector<RecipeDialogElement>('wa-dialog');
|
|
165
|
+
if (command === 'open' && dialog) {
|
|
166
|
+
element.focus();
|
|
167
|
+
open.value = true;
|
|
168
|
+
dialog.open = true;
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
if (command === 'close' && dialog) {
|
|
172
|
+
dialog.open = false;
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
39
175
|
announce(`${command} requested`);
|
|
40
176
|
},
|
|
41
177
|
afterHide() {
|
|
@@ -10,17 +10,174 @@ import { signal } from 'kerfjs';
|
|
|
10
10
|
import type { RecipeFactory } from './types.js';
|
|
11
11
|
|
|
12
12
|
type ListState = 'loading' | 'empty' | 'populated' | 'stale' | 'error';
|
|
13
|
-
const items = [
|
|
13
|
+
const items = [
|
|
14
|
+
'Audit keyboard focus order',
|
|
15
|
+
'Verify 200% zoom layout',
|
|
16
|
+
'Publish package migration notes',
|
|
17
|
+
];
|
|
14
18
|
|
|
15
19
|
export const createRecipe: RecipeFactory = (announce) => {
|
|
16
20
|
const state = signal<ListState>('loading');
|
|
17
|
-
const populated = () =>
|
|
21
|
+
const populated = () => (
|
|
22
|
+
<ul class="recipe-list__items" aria-label="Release tasks">
|
|
23
|
+
{items.map((item, index) => (
|
|
24
|
+
<li>
|
|
25
|
+
<strong>{item}</strong>
|
|
26
|
+
<span class="kui-recipe__muted">
|
|
27
|
+
{index === 0
|
|
28
|
+
? 'In review · Mara'
|
|
29
|
+
: index === 1
|
|
30
|
+
? 'Ready · Sam'
|
|
31
|
+
: 'Draft · Inez'}
|
|
32
|
+
</span>
|
|
33
|
+
</li>
|
|
34
|
+
))}
|
|
35
|
+
</ul>
|
|
36
|
+
);
|
|
18
37
|
const renderBody = () => {
|
|
19
|
-
if (state.value === 'loading')
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
38
|
+
if (state.value === 'loading')
|
|
39
|
+
return (
|
|
40
|
+
<EmptyState
|
|
41
|
+
title="Loading release tasks"
|
|
42
|
+
detail="The current workspace will appear when data is ready."
|
|
43
|
+
busy
|
|
44
|
+
action={
|
|
45
|
+
<button
|
|
46
|
+
class="kui-recipe__button"
|
|
47
|
+
type="button"
|
|
48
|
+
data-action="recipe-action"
|
|
49
|
+
data-recipe-command="load"
|
|
50
|
+
>
|
|
51
|
+
Complete load
|
|
52
|
+
</button>
|
|
53
|
+
}
|
|
54
|
+
/>
|
|
55
|
+
);
|
|
56
|
+
if (state.value === 'empty')
|
|
57
|
+
return (
|
|
58
|
+
<EmptyState
|
|
59
|
+
title="No release tasks"
|
|
60
|
+
detail="Create the first task for this milestone."
|
|
61
|
+
action={
|
|
62
|
+
<button
|
|
63
|
+
class="kui-recipe__button"
|
|
64
|
+
data-primary="true"
|
|
65
|
+
type="button"
|
|
66
|
+
data-action="recipe-action"
|
|
67
|
+
data-recipe-command="create"
|
|
68
|
+
>
|
|
69
|
+
Create task
|
|
70
|
+
</button>
|
|
71
|
+
}
|
|
72
|
+
/>
|
|
73
|
+
);
|
|
74
|
+
if (state.value === 'error')
|
|
75
|
+
return (
|
|
76
|
+
<StateBanner
|
|
77
|
+
title="Release tasks could not be refreshed"
|
|
78
|
+
detail="Existing filters are preserved. Try again when the connection recovers."
|
|
79
|
+
tone="danger"
|
|
80
|
+
urgency="alert"
|
|
81
|
+
action={
|
|
82
|
+
<button
|
|
83
|
+
class="kui-recipe__button"
|
|
84
|
+
type="button"
|
|
85
|
+
data-action="recipe-action"
|
|
86
|
+
data-recipe-command="retry"
|
|
87
|
+
>
|
|
88
|
+
Retry
|
|
89
|
+
</button>
|
|
90
|
+
}
|
|
91
|
+
/>
|
|
92
|
+
);
|
|
93
|
+
return (
|
|
94
|
+
<>
|
|
95
|
+
{state.value === 'stale' && (
|
|
96
|
+
<StateBanner
|
|
97
|
+
title="Showing saved results"
|
|
98
|
+
detail="Refreshing in the background."
|
|
99
|
+
tone="warning"
|
|
100
|
+
action={
|
|
101
|
+
<span class="kui-inline-metadata">
|
|
102
|
+
<LoadingSpinner label="Refreshing release tasks" />
|
|
103
|
+
<button
|
|
104
|
+
class="kui-recipe__button"
|
|
105
|
+
type="button"
|
|
106
|
+
data-action="recipe-action"
|
|
107
|
+
data-recipe-command="finish"
|
|
108
|
+
>
|
|
109
|
+
Finish refresh
|
|
110
|
+
</button>
|
|
111
|
+
</span>
|
|
112
|
+
}
|
|
113
|
+
/>
|
|
114
|
+
)}
|
|
115
|
+
{populated()}
|
|
116
|
+
</>
|
|
117
|
+
);
|
|
118
|
+
};
|
|
119
|
+
const render = () => (
|
|
120
|
+
<section
|
|
121
|
+
class="kui-recipe recipe-list kui-recipe__surface kui-pane"
|
|
122
|
+
data-recipe="recipe-list-workspace-states"
|
|
123
|
+
data-list-state={state.value}
|
|
124
|
+
>
|
|
125
|
+
<PanelHeader
|
|
126
|
+
title="Release tasks"
|
|
127
|
+
titleId="recipe-list-title"
|
|
128
|
+
actions={
|
|
129
|
+
<div class="kui-control-cluster">
|
|
130
|
+
<button
|
|
131
|
+
class="kui-recipe__button"
|
|
132
|
+
type="button"
|
|
133
|
+
data-action="recipe-action"
|
|
134
|
+
data-recipe-command="empty"
|
|
135
|
+
>
|
|
136
|
+
Clear
|
|
137
|
+
</button>
|
|
138
|
+
<button
|
|
139
|
+
class="kui-recipe__button"
|
|
140
|
+
type="button"
|
|
141
|
+
data-action="recipe-action"
|
|
142
|
+
data-recipe-command="fail"
|
|
143
|
+
>
|
|
144
|
+
Simulate failure
|
|
145
|
+
</button>
|
|
146
|
+
<button
|
|
147
|
+
class="kui-recipe__button"
|
|
148
|
+
data-primary="true"
|
|
149
|
+
type="button"
|
|
150
|
+
data-action="recipe-action"
|
|
151
|
+
data-recipe-command="refresh"
|
|
152
|
+
>
|
|
153
|
+
Refresh
|
|
154
|
+
</button>
|
|
155
|
+
</div>
|
|
156
|
+
}
|
|
157
|
+
/>
|
|
158
|
+
<div class="recipe-list__body kui-pane__content kui-content">
|
|
159
|
+
{renderBody()}
|
|
160
|
+
<p class="kui-recipe__ownership kui-content-item">
|
|
161
|
+
The recipe owns feedback placement and stable content. The app owns
|
|
162
|
+
fetching, cache age, retry policy, and domain rows.
|
|
163
|
+
</p>
|
|
164
|
+
</div>
|
|
165
|
+
</section>
|
|
166
|
+
);
|
|
167
|
+
return {
|
|
168
|
+
render,
|
|
169
|
+
action(command) {
|
|
170
|
+
const next: Record<string, ListState> = {
|
|
171
|
+
load: 'populated',
|
|
172
|
+
empty: 'empty',
|
|
173
|
+
create: 'populated',
|
|
174
|
+
fail: 'error',
|
|
175
|
+
retry: 'populated',
|
|
176
|
+
refresh: 'stale',
|
|
177
|
+
finish: 'populated',
|
|
178
|
+
};
|
|
179
|
+
if (command in next) state.value = next[command]!;
|
|
180
|
+
announce(`List state: ${state.value}`);
|
|
181
|
+
},
|
|
23
182
|
};
|
|
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
|
-
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
183
|
};
|
|
@@ -13,9 +13,12 @@ export const recipeIds = [
|
|
|
13
13
|
'recipe-collapsible-sidebar',
|
|
14
14
|
] as const;
|
|
15
15
|
|
|
16
|
-
export type RecipeId = typeof recipeIds[number];
|
|
16
|
+
export type RecipeId = (typeof recipeIds)[number];
|
|
17
17
|
|
|
18
|
-
export const recipeLoaders: Record<
|
|
18
|
+
export const recipeLoaders: Record<
|
|
19
|
+
RecipeId,
|
|
20
|
+
() => Promise<{ createRecipe: RecipeFactory }>
|
|
21
|
+
> = {
|
|
19
22
|
'recipe-app-shell': () => import('./app-shell.js'),
|
|
20
23
|
'recipe-navigation-sidebar': () => import('./navigation-sidebar.js'),
|
|
21
24
|
'recipe-workspace-header': () => import('./workspace-header.js'),
|
|
@@ -14,7 +14,9 @@ import { Columns3, FileText, Inbox, List } from 'lucide';
|
|
|
14
14
|
|
|
15
15
|
import type { RecipeFactory } from './types.js';
|
|
16
16
|
|
|
17
|
-
const icon = (node: Parameters<typeof LucideIcon>[0]['icon'], name: string) =>
|
|
17
|
+
const icon = (node: Parameters<typeof LucideIcon>[0]['icon'], name: string) => (
|
|
18
|
+
<LucideIcon icon={node} name={name} />
|
|
19
|
+
);
|
|
18
20
|
|
|
19
21
|
/**
|
|
20
22
|
* A record inspector whose per-record values load asynchronously. Every
|
|
@@ -27,47 +29,128 @@ export const createRecipe: RecipeFactory = (announce) => {
|
|
|
27
29
|
const loading = signal(true);
|
|
28
30
|
const render = () => {
|
|
29
31
|
const p = loading.value;
|
|
30
|
-
return
|
|
31
|
-
<
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
<
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
32
|
+
return (
|
|
33
|
+
<section
|
|
34
|
+
class="kui-recipe recipe-inspector kui-recipe__surface kui-pane"
|
|
35
|
+
data-recipe="recipe-loading-inspector"
|
|
36
|
+
data-inspector-loading={String(p)}
|
|
37
|
+
>
|
|
38
|
+
<PanelHeader
|
|
39
|
+
title="Ticket · KF-2048"
|
|
40
|
+
titleId="recipe-inspector-title"
|
|
41
|
+
summary="Restore keyboard focus after a dialog closes"
|
|
42
|
+
summaryId="recipe-inspector-summary"
|
|
43
|
+
icon={icon(FileText, 'file-text')}
|
|
44
|
+
placeholder={p}
|
|
45
|
+
actions={
|
|
46
|
+
<button
|
|
47
|
+
class="kui-recipe__button"
|
|
48
|
+
data-primary="true"
|
|
49
|
+
type="button"
|
|
50
|
+
data-action="recipe-action"
|
|
51
|
+
data-recipe-command="toggle"
|
|
52
|
+
>
|
|
53
|
+
{p ? 'Show loaded' : 'Show loading'}
|
|
54
|
+
</button>
|
|
55
|
+
}
|
|
56
|
+
/>
|
|
57
|
+
<div class="recipe-inspector__body kui-pane__content kui-content">
|
|
58
|
+
<section>
|
|
59
|
+
<ValueTable label="Ticket details">
|
|
60
|
+
<ValueTableRow
|
|
61
|
+
label="Status"
|
|
62
|
+
value="In review"
|
|
63
|
+
icon={icon(Inbox, 'inbox')}
|
|
64
|
+
placeholder={p}
|
|
65
|
+
/>
|
|
66
|
+
<ValueTableRow label="Owner" value="Mara Lopez" placeholder={p} />
|
|
67
|
+
<ValueTableRow label="Priority" value="High" placeholder={p} />
|
|
68
|
+
<ValueTableRow
|
|
69
|
+
label="Updated"
|
|
70
|
+
value="2 hours ago"
|
|
71
|
+
placeholder={p}
|
|
72
|
+
/>
|
|
73
|
+
</ValueTable>
|
|
74
|
+
</section>
|
|
75
|
+
<div class="recipe-inspector__controls">
|
|
76
|
+
<Select
|
|
77
|
+
name="recipe-inspector-status"
|
|
78
|
+
value="review"
|
|
79
|
+
label="Status"
|
|
80
|
+
placeholder={p}
|
|
81
|
+
choices={[
|
|
82
|
+
{ value: 'review', label: 'In review' },
|
|
83
|
+
{ value: 'ready', label: 'Ready' },
|
|
84
|
+
{ value: 'done', label: 'Done' },
|
|
85
|
+
]}
|
|
86
|
+
/>
|
|
87
|
+
<SegmentedControl
|
|
88
|
+
id="recipe-inspector-view"
|
|
89
|
+
label="Inspector view"
|
|
90
|
+
value="details"
|
|
91
|
+
appearance="toolbar"
|
|
92
|
+
shape="pill"
|
|
93
|
+
size="small"
|
|
94
|
+
placeholder={p}
|
|
95
|
+
choices={[
|
|
96
|
+
{
|
|
97
|
+
value: 'details',
|
|
98
|
+
label: 'Details',
|
|
99
|
+
content: icon(List, 'list'),
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
value: 'activity',
|
|
103
|
+
label: 'Activity',
|
|
104
|
+
content: icon(Columns3, 'columns-3'),
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
value: 'files',
|
|
108
|
+
label: 'Files',
|
|
109
|
+
content: icon(FileText, 'file-text'),
|
|
110
|
+
},
|
|
111
|
+
]}
|
|
112
|
+
/>
|
|
113
|
+
</div>
|
|
114
|
+
<section>
|
|
115
|
+
<ListItem
|
|
116
|
+
action="recipe-action"
|
|
117
|
+
itemId="reassign"
|
|
118
|
+
label="Reassign ticket"
|
|
119
|
+
icon={icon(Inbox, 'inbox')}
|
|
120
|
+
placeholder={p}
|
|
121
|
+
/>
|
|
122
|
+
<ListItem
|
|
123
|
+
action="recipe-action"
|
|
124
|
+
itemId="watch"
|
|
125
|
+
label="Watch for changes"
|
|
126
|
+
placeholder={p}
|
|
127
|
+
/>
|
|
128
|
+
</section>
|
|
129
|
+
{p ? (
|
|
130
|
+
<StateBanner
|
|
131
|
+
tone="info"
|
|
132
|
+
title=""
|
|
133
|
+
detail=""
|
|
134
|
+
icon={icon(FileText, 'file-text')}
|
|
135
|
+
placeholder
|
|
136
|
+
/>
|
|
137
|
+
) : (
|
|
138
|
+
<StateBanner
|
|
139
|
+
tone="success"
|
|
140
|
+
title="Up to date"
|
|
141
|
+
detail="All checks passed on the latest revision."
|
|
142
|
+
icon={icon(FileText, 'file-text')}
|
|
143
|
+
/>
|
|
144
|
+
)}
|
|
145
|
+
<p class="kui-recipe__ownership kui-content-item">
|
|
146
|
+
Each value-bearing component's <code>placeholder</code> prop renders
|
|
147
|
+
skeletons in its value slots while loading, so the recipe composes a
|
|
148
|
+
faithful loading inspector from real chrome. The app owns the
|
|
149
|
+
loading lifecycle and which values are still unknown.
|
|
150
|
+
</p>
|
|
60
151
|
</div>
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
<ListItem action="recipe-action" itemId="watch" label="Watch for changes" placeholder={p} />
|
|
64
|
-
</section>
|
|
65
|
-
{p
|
|
66
|
-
? <StateBanner tone="info" title="" detail="" icon={icon(FileText, 'file-text')} placeholder />
|
|
67
|
-
: <StateBanner tone="success" title="Up to date" detail="All checks passed on the latest revision." icon={icon(FileText, 'file-text')} />}
|
|
68
|
-
<p class="kui-recipe__ownership kui-content-item">Each value-bearing component's <code>placeholder</code> prop renders skeletons in its value slots while loading, so the recipe composes a faithful loading inspector from real chrome. The app owns the loading lifecycle and which values are still unknown.</p>
|
|
69
|
-
</div>
|
|
70
|
-
</section>;
|
|
152
|
+
</section>
|
|
153
|
+
);
|
|
71
154
|
};
|
|
72
155
|
return {
|
|
73
156
|
render,
|
|
@@ -9,7 +9,10 @@ import type { RecipeController } from './types.js';
|
|
|
9
9
|
* Copyable application boundary for a recipe. The caller owns one stable root
|
|
10
10
|
* and retains the returned disposer for that root's lifetime.
|
|
11
11
|
*/
|
|
12
|
-
export function mountRecipe(
|
|
12
|
+
export function mountRecipe(
|
|
13
|
+
root: HTMLElement,
|
|
14
|
+
controller: RecipeController,
|
|
15
|
+
): () => void {
|
|
13
16
|
const stopMount = mount(root, controller.render);
|
|
14
17
|
const stopActions = delegateActions(root, 'click', {
|
|
15
18
|
'recipe-action': (_event, element) => {
|
|
@@ -17,14 +20,32 @@ export function mountRecipe(root: HTMLElement, controller: RecipeController): ()
|
|
|
17
20
|
controller.action(target.dataset.recipeCommand ?? '', target);
|
|
18
21
|
},
|
|
19
22
|
});
|
|
20
|
-
const dispatchChange = (_event: Event, element: Element) =>
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
|
|
23
|
+
const dispatchChange = (_event: Event, element: Element) =>
|
|
24
|
+
controller.change?.(element as HTMLElement);
|
|
25
|
+
const stopChanges = delegate(
|
|
26
|
+
root,
|
|
27
|
+
'change',
|
|
28
|
+
'wa-select, wa-input, wa-textarea',
|
|
29
|
+
dispatchChange,
|
|
30
|
+
);
|
|
31
|
+
const stopInputs = delegate(
|
|
32
|
+
root,
|
|
33
|
+
'input',
|
|
34
|
+
'wa-input, wa-textarea',
|
|
35
|
+
dispatchChange,
|
|
36
|
+
);
|
|
37
|
+
const stopDialogs = delegate(
|
|
38
|
+
root,
|
|
39
|
+
'wa-after-hide',
|
|
40
|
+
'wa-dialog',
|
|
41
|
+
(_event, element) => controller.afterHide?.(element as HTMLElement),
|
|
42
|
+
);
|
|
24
43
|
const stopResize = wireResizableRegions(root, {
|
|
25
44
|
onCommit: ({ id, size }) => controller.resize?.(id, size),
|
|
26
45
|
});
|
|
27
|
-
const stopNav = wireNavStack(root, {
|
|
46
|
+
const stopNav = wireNavStack(root, {
|
|
47
|
+
onBack: () => controller.action('nav-back', root),
|
|
48
|
+
});
|
|
28
49
|
const stopWire = controller.wire?.(root);
|
|
29
50
|
|
|
30
51
|
let disposed = false;
|