@workbench-kit/jdw-editor 0.0.2-prototype.0.2.2 → 0.0.2-prototype.0.2.21
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/package.json +4 -4
- package/src/index.ts +2 -27
- package/src/ScreenNodeInspector.tsx +0 -369
- package/src/ScreenSpecEditor.tsx +0 -431
- package/src/ScreenSpecEditorStoryHost.tsx +0 -103
- package/src/ScreenSpecPalette.tsx +0 -119
- package/src/ScreenSpecWorkbench.tsx +0 -177
- package/src/filterScreenSpecOutline.ts +0 -40
- package/src/useScreenSpecPipeline.ts +0 -82
- /package/src/{JdwSampleScreenExplorer.tsx → screen-spec/JdwSampleScreenExplorer.tsx} +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@workbench-kit/jdw-editor",
|
|
3
|
-
"version": "0.0.2-prototype.0.2.
|
|
3
|
+
"version": "0.0.2-prototype.0.2.21",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -14,13 +14,13 @@
|
|
|
14
14
|
"!src/**/*.stories.tsx"
|
|
15
15
|
],
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@workbench-kit/jdw": "0.0.2-prototype.0.2.
|
|
18
|
-
"@workbench-kit/react": "0.0.2-prototype.0.2.
|
|
17
|
+
"@workbench-kit/jdw": "0.0.2-prototype.0.2.21",
|
|
18
|
+
"@workbench-kit/react": "0.0.2-prototype.0.2.21"
|
|
19
19
|
},
|
|
20
20
|
"peerDependencies": {
|
|
21
21
|
"react": "^19.0.0"
|
|
22
22
|
},
|
|
23
|
-
"description": "JDW template
|
|
23
|
+
"description": "Compile-once JDW template explorer for the canonical WidgetTreeLab authoring surface.",
|
|
24
24
|
"publishConfig": {
|
|
25
25
|
"access": "public",
|
|
26
26
|
"tag": "prototype",
|
package/src/index.ts
CHANGED
|
@@ -1,27 +1,2 @@
|
|
|
1
|
-
export { JdwSampleScreenExplorer } from './JdwSampleScreenExplorer.js';
|
|
2
|
-
export type { JdwSampleScreenExplorerProps } from './JdwSampleScreenExplorer.js';
|
|
3
|
-
export { ScreenNodeInspector } from './ScreenNodeInspector.js';
|
|
4
|
-
export type { ScreenNodeInspectorProps } from './ScreenNodeInspector.js';
|
|
5
|
-
export { ScreenSpecEditor } from './ScreenSpecEditor.js';
|
|
6
|
-
export type {
|
|
7
|
-
ScreenSpecDetailTab,
|
|
8
|
-
ScreenSpecEditorPane,
|
|
9
|
-
ScreenSpecEditorProps,
|
|
10
|
-
ScreenSpecLeftRailView,
|
|
11
|
-
} from './ScreenSpecEditor.js';
|
|
12
|
-
export {
|
|
13
|
-
SCREEN_PALETTE_ITEMS,
|
|
14
|
-
SCREEN_PALETTE_MIME,
|
|
15
|
-
ScreenSpecPalette,
|
|
16
|
-
readScreenPaletteDragData,
|
|
17
|
-
writeScreenPaletteDragData,
|
|
18
|
-
} from './ScreenSpecPalette.js';
|
|
19
|
-
export type { ScreenPaletteItem, ScreenSpecPaletteProps } from './ScreenSpecPalette.js';
|
|
20
|
-
export { filterScreenSpecOutline } from './filterScreenSpecOutline.js';
|
|
21
|
-
export { ScreenSpecWorkbench } from './ScreenSpecWorkbench.js';
|
|
22
|
-
export type { ScreenSpecWorkbenchProps } from './ScreenSpecWorkbench.js';
|
|
23
|
-
export { useScreenSpecPipeline } from './useScreenSpecPipeline.js';
|
|
24
|
-
export type {
|
|
25
|
-
ScreenSpecPipelineState,
|
|
26
|
-
UseScreenSpecPipelineResult,
|
|
27
|
-
} from './useScreenSpecPipeline.js';
|
|
1
|
+
export { JdwSampleScreenExplorer } from './screen-spec/JdwSampleScreenExplorer.js';
|
|
2
|
+
export type { JdwSampleScreenExplorerProps } from './screen-spec/JdwSampleScreenExplorer.js';
|
|
@@ -1,369 +0,0 @@
|
|
|
1
|
-
import { useMemo, useState, type ReactNode } from 'react';
|
|
2
|
-
import type { ScreenNode, ScreenTextStyle } from '@workbench-kit/jdw';
|
|
3
|
-
import {
|
|
4
|
-
NumberInput,
|
|
5
|
-
TextInput,
|
|
6
|
-
WorkbenchPropertyHint,
|
|
7
|
-
WorkbenchPropertyNumberRow,
|
|
8
|
-
WorkbenchPropertyPanel,
|
|
9
|
-
WorkbenchPropertyRow,
|
|
10
|
-
WorkbenchPropertySearch,
|
|
11
|
-
WorkbenchPropertySection,
|
|
12
|
-
WorkbenchPropertyStack,
|
|
13
|
-
WorkbenchPropertyTextRow,
|
|
14
|
-
filterWorkbenchPropertyFields,
|
|
15
|
-
isWorkbenchPropertySearchActive,
|
|
16
|
-
type WorkbenchPropertyFieldManifestEntry,
|
|
17
|
-
} from '@workbench-kit/react/primitives';
|
|
18
|
-
|
|
19
|
-
export interface ScreenNodeInspectorProps {
|
|
20
|
-
readonly node: ScreenNode;
|
|
21
|
-
readonly parentKind?: ScreenNode['kind'] | undefined;
|
|
22
|
-
readonly onChange: (node: ScreenNode) => void;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function readStyle(node: Extract<ScreenNode, { kind: 'text' | 'panel' }>): ScreenTextStyle {
|
|
26
|
-
return node.style ?? {};
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function patchStyle(
|
|
30
|
-
node: Extract<ScreenNode, { kind: 'text' | 'panel' }>,
|
|
31
|
-
patch: ScreenTextStyle,
|
|
32
|
-
): ScreenNode {
|
|
33
|
-
return { ...node, style: { ...node.style, ...patch } };
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function OptionalNumberRow({
|
|
37
|
-
label,
|
|
38
|
-
testId,
|
|
39
|
-
value,
|
|
40
|
-
onChange,
|
|
41
|
-
}: {
|
|
42
|
-
readonly label: string;
|
|
43
|
-
readonly testId: string;
|
|
44
|
-
readonly value: number | undefined;
|
|
45
|
-
readonly onChange: (value: number | undefined) => void;
|
|
46
|
-
}) {
|
|
47
|
-
return (
|
|
48
|
-
<WorkbenchPropertyRow label={label} htmlFor={testId}>
|
|
49
|
-
<NumberInput
|
|
50
|
-
id={testId}
|
|
51
|
-
data-testid={testId}
|
|
52
|
-
controlWidth="full"
|
|
53
|
-
nullable
|
|
54
|
-
value={value}
|
|
55
|
-
onEmptyValue={() => onChange(undefined)}
|
|
56
|
-
onValueChange={(next) => onChange(next)}
|
|
57
|
-
/>
|
|
58
|
-
</WorkbenchPropertyRow>
|
|
59
|
-
);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function usePropertyVisibility(
|
|
63
|
-
fields: readonly WorkbenchPropertyFieldManifestEntry[],
|
|
64
|
-
query: string,
|
|
65
|
-
) {
|
|
66
|
-
const filtered = useMemo(() => filterWorkbenchPropertyFields({ fields, query }), [fields, query]);
|
|
67
|
-
const visible = useMemo(() => new Set(filtered.fieldIds), [filtered.fieldIds]);
|
|
68
|
-
const sections = useMemo(() => new Set(filtered.sectionIds), [filtered.sectionIds]);
|
|
69
|
-
const searching = isWorkbenchPropertySearchActive(query);
|
|
70
|
-
return {
|
|
71
|
-
searching,
|
|
72
|
-
showField: (id: string) => !searching || visible.has(id),
|
|
73
|
-
showSection: (sectionId: string) => !searching || sections.has(sectionId),
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function InspectorShell({
|
|
78
|
-
query,
|
|
79
|
-
onQueryChange,
|
|
80
|
-
children,
|
|
81
|
-
}: {
|
|
82
|
-
readonly query: string;
|
|
83
|
-
readonly onQueryChange: (value: string) => void;
|
|
84
|
-
readonly children: ReactNode;
|
|
85
|
-
}) {
|
|
86
|
-
return (
|
|
87
|
-
<WorkbenchPropertyPanel
|
|
88
|
-
className="jdw-screen-node-inspector"
|
|
89
|
-
data-testid="screen-spec-node-inspector"
|
|
90
|
-
>
|
|
91
|
-
<WorkbenchPropertySearch
|
|
92
|
-
data-testid="screen-spec-props-search"
|
|
93
|
-
value={query}
|
|
94
|
-
onValueChange={onQueryChange}
|
|
95
|
-
/>
|
|
96
|
-
<WorkbenchPropertyStack>{children}</WorkbenchPropertyStack>
|
|
97
|
-
</WorkbenchPropertyPanel>
|
|
98
|
-
);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function ContentNodeInspector({
|
|
102
|
-
node,
|
|
103
|
-
parentKind,
|
|
104
|
-
onChange,
|
|
105
|
-
}: {
|
|
106
|
-
readonly node: Extract<ScreenNode, { kind: 'text' | 'panel' }>;
|
|
107
|
-
readonly parentKind?: ScreenNode['kind'] | undefined;
|
|
108
|
-
readonly onChange: (node: ScreenNode) => void;
|
|
109
|
-
}) {
|
|
110
|
-
const [query, setQuery] = useState('');
|
|
111
|
-
const style = readStyle(node);
|
|
112
|
-
const fields = useMemo((): WorkbenchPropertyFieldManifestEntry[] => {
|
|
113
|
-
const next: WorkbenchPropertyFieldManifestEntry[] = [
|
|
114
|
-
{ id: 'content', label: 'Content', sectionId: 'content', keywords: ['text'] },
|
|
115
|
-
];
|
|
116
|
-
if (node.kind === 'panel') {
|
|
117
|
-
next.push({
|
|
118
|
-
id: 'panel-background',
|
|
119
|
-
label: 'Panel background',
|
|
120
|
-
sectionId: 'content',
|
|
121
|
-
keywords: ['fill'],
|
|
122
|
-
});
|
|
123
|
-
}
|
|
124
|
-
next.push(
|
|
125
|
-
{ id: 'color', label: 'Text color', sectionId: 'style', keywords: ['colour'] },
|
|
126
|
-
{ id: 'font-size', label: 'Font size', sectionId: 'style', keywords: ['typography'] },
|
|
127
|
-
{ id: 'background', label: 'Background', sectionId: 'style' },
|
|
128
|
-
);
|
|
129
|
-
if (parentKind === 'grid') {
|
|
130
|
-
next.push(
|
|
131
|
-
{ id: 'col', label: 'Column', sectionId: 'placement', keywords: ['grid'] },
|
|
132
|
-
{ id: 'row', label: 'Row', sectionId: 'placement', keywords: ['grid'] },
|
|
133
|
-
{ id: 'col-span', label: 'Column span', sectionId: 'placement', keywords: ['grid'] },
|
|
134
|
-
{ id: 'row-span', label: 'Row span', sectionId: 'placement', keywords: ['grid'] },
|
|
135
|
-
);
|
|
136
|
-
}
|
|
137
|
-
return next;
|
|
138
|
-
}, [node.kind, parentKind]);
|
|
139
|
-
const { showField, showSection, searching } = usePropertyVisibility(fields, query);
|
|
140
|
-
|
|
141
|
-
return (
|
|
142
|
-
<InspectorShell query={query} onQueryChange={setQuery}>
|
|
143
|
-
{showSection('content') ? (
|
|
144
|
-
<WorkbenchPropertySection collapsible title="Content">
|
|
145
|
-
{showField('content') ? (
|
|
146
|
-
<WorkbenchPropertyRow label="Content" htmlFor="screen-spec-field-content">
|
|
147
|
-
<TextInput
|
|
148
|
-
id="screen-spec-field-content"
|
|
149
|
-
data-testid="screen-spec-field-content"
|
|
150
|
-
controlWidth="full"
|
|
151
|
-
value={node.content}
|
|
152
|
-
onValueChange={(content) => onChange({ ...node, content })}
|
|
153
|
-
/>
|
|
154
|
-
</WorkbenchPropertyRow>
|
|
155
|
-
) : null}
|
|
156
|
-
{node.kind === 'panel' && showField('panel-background') ? (
|
|
157
|
-
<WorkbenchPropertyTextRow
|
|
158
|
-
htmlFor="screen-spec-field-panel-background"
|
|
159
|
-
label="Panel background"
|
|
160
|
-
value={node.background ?? ''}
|
|
161
|
-
onValueChange={(background) => onChange({ ...node, background })}
|
|
162
|
-
/>
|
|
163
|
-
) : null}
|
|
164
|
-
</WorkbenchPropertySection>
|
|
165
|
-
) : null}
|
|
166
|
-
|
|
167
|
-
{showSection('style') ? (
|
|
168
|
-
<WorkbenchPropertySection collapsible title="Style">
|
|
169
|
-
{showField('color') ? (
|
|
170
|
-
<WorkbenchPropertyTextRow
|
|
171
|
-
htmlFor="screen-spec-field-color"
|
|
172
|
-
label="Text color"
|
|
173
|
-
value={style.color ?? ''}
|
|
174
|
-
onValueChange={(color) => onChange(patchStyle(node, { color }))}
|
|
175
|
-
/>
|
|
176
|
-
) : null}
|
|
177
|
-
{showField('font-size') ? (
|
|
178
|
-
<OptionalNumberRow
|
|
179
|
-
label="Font size"
|
|
180
|
-
testId="screen-spec-field-font-size"
|
|
181
|
-
value={style.fontSize}
|
|
182
|
-
onChange={(fontSize) => onChange(patchStyle(node, { fontSize }))}
|
|
183
|
-
/>
|
|
184
|
-
) : null}
|
|
185
|
-
{showField('background') ? (
|
|
186
|
-
<WorkbenchPropertyTextRow
|
|
187
|
-
htmlFor="screen-spec-field-background"
|
|
188
|
-
label="Background"
|
|
189
|
-
value={style.background ?? ''}
|
|
190
|
-
onValueChange={(background) => onChange(patchStyle(node, { background }))}
|
|
191
|
-
/>
|
|
192
|
-
) : null}
|
|
193
|
-
</WorkbenchPropertySection>
|
|
194
|
-
) : null}
|
|
195
|
-
|
|
196
|
-
{parentKind === 'grid' && showSection('placement') ? (
|
|
197
|
-
<WorkbenchPropertySection collapsible title="Placement">
|
|
198
|
-
{showField('col') ? (
|
|
199
|
-
<OptionalNumberRow
|
|
200
|
-
label="Column"
|
|
201
|
-
testId="screen-spec-field-col"
|
|
202
|
-
value={node.col}
|
|
203
|
-
onChange={(col) => onChange({ ...node, col })}
|
|
204
|
-
/>
|
|
205
|
-
) : null}
|
|
206
|
-
{showField('row') ? (
|
|
207
|
-
<OptionalNumberRow
|
|
208
|
-
label="Row"
|
|
209
|
-
testId="screen-spec-field-row"
|
|
210
|
-
value={node.row}
|
|
211
|
-
onChange={(row) => onChange({ ...node, row })}
|
|
212
|
-
/>
|
|
213
|
-
) : null}
|
|
214
|
-
{showField('col-span') ? (
|
|
215
|
-
<OptionalNumberRow
|
|
216
|
-
label="Column span"
|
|
217
|
-
testId="screen-spec-field-col-span"
|
|
218
|
-
value={node.colSpan}
|
|
219
|
-
onChange={(colSpan) => onChange({ ...node, colSpan })}
|
|
220
|
-
/>
|
|
221
|
-
) : null}
|
|
222
|
-
{showField('row-span') ? (
|
|
223
|
-
<OptionalNumberRow
|
|
224
|
-
label="Row span"
|
|
225
|
-
testId="screen-spec-field-row-span"
|
|
226
|
-
value={node.rowSpan}
|
|
227
|
-
onChange={(rowSpan) => onChange({ ...node, rowSpan })}
|
|
228
|
-
/>
|
|
229
|
-
) : null}
|
|
230
|
-
</WorkbenchPropertySection>
|
|
231
|
-
) : null}
|
|
232
|
-
|
|
233
|
-
{searching &&
|
|
234
|
-
!showSection('content') &&
|
|
235
|
-
!showSection('style') &&
|
|
236
|
-
!showSection('placement') ? (
|
|
237
|
-
<WorkbenchPropertyHint>No properties match.</WorkbenchPropertyHint>
|
|
238
|
-
) : null}
|
|
239
|
-
</InspectorShell>
|
|
240
|
-
);
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
function ExpandedNodeInspector({
|
|
244
|
-
node,
|
|
245
|
-
onChange,
|
|
246
|
-
}: {
|
|
247
|
-
readonly node: Extract<ScreenNode, { kind: 'expanded' }>;
|
|
248
|
-
readonly onChange: (node: ScreenNode) => void;
|
|
249
|
-
}) {
|
|
250
|
-
const [query, setQuery] = useState('');
|
|
251
|
-
const fields = useMemo(
|
|
252
|
-
(): WorkbenchPropertyFieldManifestEntry[] => [
|
|
253
|
-
{ id: 'flex', label: 'Flex', sectionId: 'layout', keywords: ['grow'] },
|
|
254
|
-
],
|
|
255
|
-
[],
|
|
256
|
-
);
|
|
257
|
-
const { showField, showSection, searching } = usePropertyVisibility(fields, query);
|
|
258
|
-
|
|
259
|
-
return (
|
|
260
|
-
<InspectorShell query={query} onQueryChange={setQuery}>
|
|
261
|
-
{showSection('layout') ? (
|
|
262
|
-
<WorkbenchPropertySection collapsible title="Layout">
|
|
263
|
-
{showField('flex') ? (
|
|
264
|
-
<OptionalNumberRow
|
|
265
|
-
label="Flex"
|
|
266
|
-
testId="screen-spec-field-flex"
|
|
267
|
-
value={node.flex}
|
|
268
|
-
onChange={(flex) => onChange({ ...node, flex })}
|
|
269
|
-
/>
|
|
270
|
-
) : null}
|
|
271
|
-
<WorkbenchPropertyHint>Edit the wrapped child from the outline.</WorkbenchPropertyHint>
|
|
272
|
-
</WorkbenchPropertySection>
|
|
273
|
-
) : null}
|
|
274
|
-
{searching && !showSection('layout') ? (
|
|
275
|
-
<WorkbenchPropertyHint>No properties match.</WorkbenchPropertyHint>
|
|
276
|
-
) : null}
|
|
277
|
-
</InspectorShell>
|
|
278
|
-
);
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
function ContainerNodeInspector({
|
|
282
|
-
node,
|
|
283
|
-
onChange,
|
|
284
|
-
}: {
|
|
285
|
-
readonly node: Extract<ScreenNode, { kind: 'row' | 'column' | 'grid' | 'stack' }>;
|
|
286
|
-
readonly onChange: (node: ScreenNode) => void;
|
|
287
|
-
}) {
|
|
288
|
-
const [query, setQuery] = useState('');
|
|
289
|
-
const fields = useMemo((): WorkbenchPropertyFieldManifestEntry[] => {
|
|
290
|
-
const next: WorkbenchPropertyFieldManifestEntry[] = [];
|
|
291
|
-
if (node.kind === 'grid') {
|
|
292
|
-
next.push({ id: 'columns', label: 'Columns', sectionId: 'layout', keywords: ['grid'] });
|
|
293
|
-
}
|
|
294
|
-
next.push(
|
|
295
|
-
{ id: 'gap', label: 'Gap', sectionId: 'layout', keywords: ['spacing'] },
|
|
296
|
-
{ id: 'padding', label: 'Padding', sectionId: 'layout', keywords: ['spacing'] },
|
|
297
|
-
{ id: 'background', label: 'Background', sectionId: 'style' },
|
|
298
|
-
);
|
|
299
|
-
return next;
|
|
300
|
-
}, [node.kind]);
|
|
301
|
-
const { showField, showSection, searching } = usePropertyVisibility(fields, query);
|
|
302
|
-
|
|
303
|
-
return (
|
|
304
|
-
<InspectorShell query={query} onQueryChange={setQuery}>
|
|
305
|
-
{showSection('layout') ? (
|
|
306
|
-
<WorkbenchPropertySection collapsible title="Layout">
|
|
307
|
-
{node.kind === 'grid' && showField('columns') ? (
|
|
308
|
-
<WorkbenchPropertyNumberRow
|
|
309
|
-
htmlFor="screen-spec-field-columns"
|
|
310
|
-
label="Columns"
|
|
311
|
-
min={1}
|
|
312
|
-
value={node.columns}
|
|
313
|
-
onValueChange={(columns) => onChange({ ...node, columns: Math.max(1, columns) })}
|
|
314
|
-
/>
|
|
315
|
-
) : null}
|
|
316
|
-
{showField('gap') ? (
|
|
317
|
-
<OptionalNumberRow
|
|
318
|
-
label="Gap"
|
|
319
|
-
testId="screen-spec-field-gap"
|
|
320
|
-
value={node.gap}
|
|
321
|
-
onChange={(gap) => onChange({ ...node, gap })}
|
|
322
|
-
/>
|
|
323
|
-
) : null}
|
|
324
|
-
{showField('padding') ? (
|
|
325
|
-
<OptionalNumberRow
|
|
326
|
-
label="Padding"
|
|
327
|
-
testId="screen-spec-field-padding"
|
|
328
|
-
value={node.padding}
|
|
329
|
-
onChange={(padding) => onChange({ ...node, padding })}
|
|
330
|
-
/>
|
|
331
|
-
) : null}
|
|
332
|
-
</WorkbenchPropertySection>
|
|
333
|
-
) : null}
|
|
334
|
-
{showSection('style') ? (
|
|
335
|
-
<WorkbenchPropertySection collapsible title="Style">
|
|
336
|
-
{showField('background') ? (
|
|
337
|
-
<WorkbenchPropertyTextRow
|
|
338
|
-
htmlFor="screen-spec-field-container-background"
|
|
339
|
-
label="Background"
|
|
340
|
-
value={node.background ?? ''}
|
|
341
|
-
onValueChange={(background) => onChange({ ...node, background })}
|
|
342
|
-
/>
|
|
343
|
-
) : null}
|
|
344
|
-
</WorkbenchPropertySection>
|
|
345
|
-
) : null}
|
|
346
|
-
{searching && !showSection('layout') && !showSection('style') ? (
|
|
347
|
-
<WorkbenchPropertyHint>No properties match.</WorkbenchPropertyHint>
|
|
348
|
-
) : null}
|
|
349
|
-
</InspectorShell>
|
|
350
|
-
);
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
export function ScreenNodeInspector({ node, parentKind, onChange }: ScreenNodeInspectorProps) {
|
|
354
|
-
if (node.kind === 'text' || node.kind === 'panel') {
|
|
355
|
-
return <ContentNodeInspector node={node} parentKind={parentKind} onChange={onChange} />;
|
|
356
|
-
}
|
|
357
|
-
if (node.kind === 'expanded') {
|
|
358
|
-
return <ExpandedNodeInspector node={node} onChange={onChange} />;
|
|
359
|
-
}
|
|
360
|
-
if (
|
|
361
|
-
node.kind === 'row' ||
|
|
362
|
-
node.kind === 'column' ||
|
|
363
|
-
node.kind === 'grid' ||
|
|
364
|
-
node.kind === 'stack'
|
|
365
|
-
) {
|
|
366
|
-
return <ContainerNodeInspector node={node} onChange={onChange} />;
|
|
367
|
-
}
|
|
368
|
-
return null;
|
|
369
|
-
}
|
package/src/ScreenSpecEditor.tsx
DELETED
|
@@ -1,431 +0,0 @@
|
|
|
1
|
-
import { useMemo, useState, type DragEvent, type KeyboardEvent } from 'react';
|
|
2
|
-
import {
|
|
3
|
-
createDefaultScreenNode,
|
|
4
|
-
getScreenNodeAt,
|
|
5
|
-
insertScreenNodeAt,
|
|
6
|
-
isScreenContainerNode,
|
|
7
|
-
listScreenSpecOutline,
|
|
8
|
-
removeScreenNodeAt,
|
|
9
|
-
resolveScreenInsertParentPath,
|
|
10
|
-
updateScreenNodeAt,
|
|
11
|
-
updateScreenSpecMetadata,
|
|
12
|
-
type JdwScreenSpec,
|
|
13
|
-
type ScreenNode,
|
|
14
|
-
type ScreenNodePath,
|
|
15
|
-
type ScreenPaletteKind,
|
|
16
|
-
} from '@workbench-kit/jdw';
|
|
17
|
-
import {
|
|
18
|
-
Badge,
|
|
19
|
-
ClearableTextInput,
|
|
20
|
-
SegmentedControl,
|
|
21
|
-
TextInput,
|
|
22
|
-
WorkbenchFill,
|
|
23
|
-
WorkbenchLabeledPane,
|
|
24
|
-
WorkbenchPropertyHint,
|
|
25
|
-
WorkbenchPropertyRow,
|
|
26
|
-
WorkbenchPropertyStack,
|
|
27
|
-
} from '@workbench-kit/react/primitives';
|
|
28
|
-
import { SplitView } from '@workbench-kit/react/workbench/shell';
|
|
29
|
-
|
|
30
|
-
import { filterScreenSpecOutline } from './filterScreenSpecOutline.js';
|
|
31
|
-
import { ScreenNodeInspector } from './ScreenNodeInspector.js';
|
|
32
|
-
import {
|
|
33
|
-
readScreenPaletteDragData,
|
|
34
|
-
SCREEN_PALETTE_MIME,
|
|
35
|
-
ScreenSpecPalette,
|
|
36
|
-
} from './ScreenSpecPalette.js';
|
|
37
|
-
|
|
38
|
-
function cx(...parts: Array<string | false | null | undefined>) {
|
|
39
|
-
return parts.filter(Boolean).join(' ');
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function pathKey(path: ScreenNodePath): string {
|
|
43
|
-
return path.length === 0 ? 'root' : path.join('.');
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function screenKindLabel(kind: string): string {
|
|
47
|
-
return kind.length === 0 ? kind : `${kind[0]!.toUpperCase()}${kind.slice(1)}`;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export type ScreenSpecEditorPane = 'all' | 'outline' | 'inspector';
|
|
51
|
-
/** Local left rail inside the Form — not the host Activity Bar. */
|
|
52
|
-
export type ScreenSpecLeftRailView = 'outline' | 'screen';
|
|
53
|
-
/** Right detail tabs — matches WidgetTreeLab Props | Assets. */
|
|
54
|
-
export type ScreenSpecDetailTab = 'properties' | 'assets';
|
|
55
|
-
|
|
56
|
-
const DETAIL_TABS = [
|
|
57
|
-
{ label: 'Props', testId: 'screen-spec-detail-props', value: 'properties' as const },
|
|
58
|
-
{ label: 'Assets', testId: 'screen-spec-detail-assets', value: 'assets' as const },
|
|
59
|
-
];
|
|
60
|
-
|
|
61
|
-
const LEFT_VIEW_TABS = [
|
|
62
|
-
{ label: 'Outline', testId: 'screen-spec-rail-outline', value: 'outline' as const },
|
|
63
|
-
{ label: 'Screen', testId: 'screen-spec-rail-screen', value: 'screen' as const },
|
|
64
|
-
];
|
|
65
|
-
|
|
66
|
-
/** @deprecated Compatibility editor; use `WidgetTreeLab` for JDW authoring. */
|
|
67
|
-
export interface ScreenSpecEditorProps {
|
|
68
|
-
readonly value: JdwScreenSpec;
|
|
69
|
-
readonly onChange: (spec: JdwScreenSpec) => void;
|
|
70
|
-
readonly onCompileError?: ((message: string | null) => void) | undefined;
|
|
71
|
-
readonly className?: string | undefined;
|
|
72
|
-
readonly selectedPath?: ScreenNodePath | undefined;
|
|
73
|
-
readonly onSelectPath?: ((path: ScreenNodePath) => void) | undefined;
|
|
74
|
-
/**
|
|
75
|
-
* `outline` / `inspector` — single inner-sidebar pane for 3-column workbench.
|
|
76
|
-
* `all` — combined Outline|Inspector split (standalone hosts).
|
|
77
|
-
*/
|
|
78
|
-
readonly pane?: ScreenSpecEditorPane | undefined;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function useScreenSpecEditorModel({
|
|
82
|
-
value,
|
|
83
|
-
onChange,
|
|
84
|
-
onCompileError,
|
|
85
|
-
selectedPath: selectedPathProp,
|
|
86
|
-
onSelectPath,
|
|
87
|
-
}: Pick<
|
|
88
|
-
ScreenSpecEditorProps,
|
|
89
|
-
'value' | 'onChange' | 'onCompileError' | 'selectedPath' | 'onSelectPath'
|
|
90
|
-
>) {
|
|
91
|
-
const [uncontrolledPath, setUncontrolledPath] = useState<ScreenNodePath>([]);
|
|
92
|
-
const selectedPath = selectedPathProp ?? uncontrolledPath;
|
|
93
|
-
const setSelectedPath = onSelectPath ?? setUncontrolledPath;
|
|
94
|
-
const outline = useMemo(() => listScreenSpecOutline(value), [value]);
|
|
95
|
-
const selectedPathKey = pathKey(selectedPath);
|
|
96
|
-
const selectedEntry =
|
|
97
|
-
outline.find((entry) => pathKey(entry.path) === selectedPathKey) ?? outline[0];
|
|
98
|
-
const selectedEntryKey = selectedEntry ? pathKey(selectedEntry.path) : selectedPathKey;
|
|
99
|
-
const selectedNode = selectedEntry?.node ?? value.root;
|
|
100
|
-
|
|
101
|
-
const insertParentPath = useMemo(
|
|
102
|
-
() => resolveScreenInsertParentPath(value.root, selectedPath),
|
|
103
|
-
[selectedPath, value.root],
|
|
104
|
-
);
|
|
105
|
-
const insertParent = insertParentPath ? getScreenNodeAt(value.root, insertParentPath) : null;
|
|
106
|
-
const canClickPlace = Boolean(insertParent && isScreenContainerNode(insertParent));
|
|
107
|
-
const insertTargetLabel = insertParent
|
|
108
|
-
? `${insertParent.kind}${insertParentPath?.length === 0 ? ' (root)' : ''}`
|
|
109
|
-
: undefined;
|
|
110
|
-
|
|
111
|
-
const commitSpec = (nextSpec: JdwScreenSpec) => {
|
|
112
|
-
onChange(nextSpec);
|
|
113
|
-
onCompileError?.(null);
|
|
114
|
-
};
|
|
115
|
-
|
|
116
|
-
const updateNode = (nextNode: ScreenNode) => {
|
|
117
|
-
commitSpec(updateScreenNodeAt(value, selectedEntry?.path ?? [], nextNode));
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
const placeKind = (kind: ScreenPaletteKind, parentPath: ScreenNodePath | null) => {
|
|
121
|
-
if (!parentPath) {
|
|
122
|
-
return false;
|
|
123
|
-
}
|
|
124
|
-
const result = insertScreenNodeAt(value, parentPath, createDefaultScreenNode(kind));
|
|
125
|
-
if (!result) {
|
|
126
|
-
return false;
|
|
127
|
-
}
|
|
128
|
-
commitSpec(result.spec);
|
|
129
|
-
setSelectedPath(result.insertedPath);
|
|
130
|
-
return true;
|
|
131
|
-
};
|
|
132
|
-
|
|
133
|
-
const removeSelected = () => {
|
|
134
|
-
const result = removeScreenNodeAt(value, selectedPath);
|
|
135
|
-
if (!result) {
|
|
136
|
-
return;
|
|
137
|
-
}
|
|
138
|
-
commitSpec(result.spec);
|
|
139
|
-
setSelectedPath(result.nextSelectedPath);
|
|
140
|
-
};
|
|
141
|
-
|
|
142
|
-
return {
|
|
143
|
-
canClickPlace,
|
|
144
|
-
commitSpec,
|
|
145
|
-
insertParentPath,
|
|
146
|
-
insertTargetLabel,
|
|
147
|
-
outline,
|
|
148
|
-
placeKind,
|
|
149
|
-
removeSelected,
|
|
150
|
-
selectedEntry,
|
|
151
|
-
selectedEntryKey,
|
|
152
|
-
selectedNode,
|
|
153
|
-
selectedPath,
|
|
154
|
-
setSelectedPath,
|
|
155
|
-
updateNode,
|
|
156
|
-
value,
|
|
157
|
-
};
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
type EditorModel = ReturnType<typeof useScreenSpecEditorModel>;
|
|
161
|
-
|
|
162
|
-
function ScreenSpecOutlineBody({ model }: { readonly model: EditorModel }) {
|
|
163
|
-
const {
|
|
164
|
-
commitSpec,
|
|
165
|
-
outline,
|
|
166
|
-
placeKind,
|
|
167
|
-
removeSelected,
|
|
168
|
-
selectedEntryKey,
|
|
169
|
-
selectedPath,
|
|
170
|
-
setSelectedPath,
|
|
171
|
-
value,
|
|
172
|
-
} = model;
|
|
173
|
-
const [dropTargetKey, setDropTargetKey] = useState<string | null>(null);
|
|
174
|
-
const [leftView, setLeftView] = useState<ScreenSpecLeftRailView>('outline');
|
|
175
|
-
const [outlineQuery, setOutlineQuery] = useState('');
|
|
176
|
-
const visibleOutline = useMemo(
|
|
177
|
-
() => filterScreenSpecOutline(outline, outlineQuery),
|
|
178
|
-
[outline, outlineQuery],
|
|
179
|
-
);
|
|
180
|
-
|
|
181
|
-
const resolveDropParentPath = (entryPath: ScreenNodePath, entryNode: ScreenNode) => {
|
|
182
|
-
if (isScreenContainerNode(entryNode)) {
|
|
183
|
-
return entryPath;
|
|
184
|
-
}
|
|
185
|
-
return resolveScreenInsertParentPath(value.root, entryPath);
|
|
186
|
-
};
|
|
187
|
-
|
|
188
|
-
const handleDragOver = (event: DragEvent<HTMLLIElement>, key: string, canDrop: boolean) => {
|
|
189
|
-
const types = Array.from(event.dataTransfer.types);
|
|
190
|
-
if (!canDrop || (!types.includes(SCREEN_PALETTE_MIME) && !types.includes('text/plain'))) {
|
|
191
|
-
return;
|
|
192
|
-
}
|
|
193
|
-
event.preventDefault();
|
|
194
|
-
event.dataTransfer.dropEffect = 'copy';
|
|
195
|
-
setDropTargetKey(key);
|
|
196
|
-
};
|
|
197
|
-
|
|
198
|
-
const handleOutlineKeyDown = (event: KeyboardEvent<HTMLUListElement>) => {
|
|
199
|
-
if (event.key !== 'Delete' && event.key !== 'Backspace') {
|
|
200
|
-
return;
|
|
201
|
-
}
|
|
202
|
-
if (selectedPath.length === 0) {
|
|
203
|
-
return;
|
|
204
|
-
}
|
|
205
|
-
event.preventDefault();
|
|
206
|
-
removeSelected();
|
|
207
|
-
};
|
|
208
|
-
|
|
209
|
-
return (
|
|
210
|
-
<WorkbenchLabeledPane
|
|
211
|
-
aria-label="Screen structure and metadata"
|
|
212
|
-
chrome="flat"
|
|
213
|
-
data-testid="screen-spec-sidebar"
|
|
214
|
-
header={
|
|
215
|
-
<SegmentedControl
|
|
216
|
-
ariaLabel="Screen editor panel"
|
|
217
|
-
options={LEFT_VIEW_TABS}
|
|
218
|
-
value={leftView}
|
|
219
|
-
onChange={setLeftView}
|
|
220
|
-
/>
|
|
221
|
-
}
|
|
222
|
-
>
|
|
223
|
-
{leftView === 'screen' ? (
|
|
224
|
-
<WorkbenchPropertyStack gap="sm" data-testid="screen-spec-metadata">
|
|
225
|
-
<WorkbenchPropertyRow label="Title" htmlFor="screen-spec-field-title">
|
|
226
|
-
<TextInput
|
|
227
|
-
id="screen-spec-field-title"
|
|
228
|
-
data-testid="screen-spec-field-title"
|
|
229
|
-
controlWidth="full"
|
|
230
|
-
value={value.title}
|
|
231
|
-
onValueChange={(title) => commitSpec(updateScreenSpecMetadata(value, { title }))}
|
|
232
|
-
/>
|
|
233
|
-
</WorkbenchPropertyRow>
|
|
234
|
-
<WorkbenchPropertyRow label="Description" htmlFor="screen-spec-field-description">
|
|
235
|
-
<TextInput
|
|
236
|
-
id="screen-spec-field-description"
|
|
237
|
-
data-testid="screen-spec-field-description"
|
|
238
|
-
controlWidth="full"
|
|
239
|
-
value={value.description}
|
|
240
|
-
onValueChange={(description) =>
|
|
241
|
-
commitSpec(updateScreenSpecMetadata(value, { description }))
|
|
242
|
-
}
|
|
243
|
-
/>
|
|
244
|
-
</WorkbenchPropertyRow>
|
|
245
|
-
</WorkbenchPropertyStack>
|
|
246
|
-
) : (
|
|
247
|
-
<div className="widget-tree-outline" data-testid="screen-spec-outline">
|
|
248
|
-
<div style={{ padding: '6px 8px 4px' }}>
|
|
249
|
-
<ClearableTextInput
|
|
250
|
-
aria-label="Search outline"
|
|
251
|
-
clearLabel="Clear"
|
|
252
|
-
controlWidth="full"
|
|
253
|
-
data-testid="screen-spec-outline-search"
|
|
254
|
-
placeholder="Search outline"
|
|
255
|
-
value={outlineQuery}
|
|
256
|
-
onValueChange={setOutlineQuery}
|
|
257
|
-
/>
|
|
258
|
-
</div>
|
|
259
|
-
{visibleOutline.length === 0 ? (
|
|
260
|
-
<WorkbenchPropertyHint data-testid="screen-spec-outline-empty">
|
|
261
|
-
No outline matches.
|
|
262
|
-
</WorkbenchPropertyHint>
|
|
263
|
-
) : (
|
|
264
|
-
<ul
|
|
265
|
-
aria-label="Screen node outline"
|
|
266
|
-
className="widget-tree-outline__list"
|
|
267
|
-
role="tree"
|
|
268
|
-
tabIndex={0}
|
|
269
|
-
onKeyDown={handleOutlineKeyDown}
|
|
270
|
-
>
|
|
271
|
-
{visibleOutline.map((entry) => {
|
|
272
|
-
const key = pathKey(entry.path);
|
|
273
|
-
const selected = selectedEntryKey === key;
|
|
274
|
-
const depth = entry.depth;
|
|
275
|
-
const dropParentPath = resolveDropParentPath(entry.path, entry.node);
|
|
276
|
-
const canDrop = dropParentPath !== null;
|
|
277
|
-
|
|
278
|
-
return (
|
|
279
|
-
<li
|
|
280
|
-
key={key}
|
|
281
|
-
aria-level={depth + 1}
|
|
282
|
-
aria-selected={selected}
|
|
283
|
-
className={cx(
|
|
284
|
-
'widget-tree-outline__item',
|
|
285
|
-
selected && 'widget-tree-outline__item--selected',
|
|
286
|
-
dropTargetKey === key && 'widget-tree-outline__item--drop-inside',
|
|
287
|
-
)}
|
|
288
|
-
role="treeitem"
|
|
289
|
-
style={{ paddingLeft: `${depth * 14 + 6}px` }}
|
|
290
|
-
onDragLeave={() => {
|
|
291
|
-
setDropTargetKey((current) => (current === key ? null : current));
|
|
292
|
-
}}
|
|
293
|
-
onDragOver={(event) => handleDragOver(event, key, canDrop)}
|
|
294
|
-
onDrop={(event) => {
|
|
295
|
-
event.preventDefault();
|
|
296
|
-
setDropTargetKey(null);
|
|
297
|
-
const kind = readScreenPaletteDragData(event.dataTransfer);
|
|
298
|
-
if (!kind || !dropParentPath) {
|
|
299
|
-
return;
|
|
300
|
-
}
|
|
301
|
-
placeKind(kind, dropParentPath);
|
|
302
|
-
}}
|
|
303
|
-
>
|
|
304
|
-
<button
|
|
305
|
-
className="widget-tree-outline__button"
|
|
306
|
-
data-testid={`screen-spec-outline-${key}`}
|
|
307
|
-
type="button"
|
|
308
|
-
onClick={() => setSelectedPath(entry.path)}
|
|
309
|
-
>
|
|
310
|
-
<span className="widget-tree-outline__type">{entry.label}</span>
|
|
311
|
-
</button>
|
|
312
|
-
</li>
|
|
313
|
-
);
|
|
314
|
-
})}
|
|
315
|
-
</ul>
|
|
316
|
-
)}
|
|
317
|
-
</div>
|
|
318
|
-
)}
|
|
319
|
-
</WorkbenchLabeledPane>
|
|
320
|
-
);
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
function ScreenSpecInspectorBody({ model }: { readonly model: EditorModel }) {
|
|
324
|
-
const {
|
|
325
|
-
canClickPlace,
|
|
326
|
-
insertParentPath,
|
|
327
|
-
insertTargetLabel,
|
|
328
|
-
placeKind,
|
|
329
|
-
selectedNode,
|
|
330
|
-
updateNode,
|
|
331
|
-
selectedEntry,
|
|
332
|
-
} = model;
|
|
333
|
-
const [detailTab, setDetailTab] = useState<ScreenSpecDetailTab>('properties');
|
|
334
|
-
|
|
335
|
-
const placeFromAssets = (kind: ScreenPaletteKind) => {
|
|
336
|
-
if (placeKind(kind, insertParentPath)) {
|
|
337
|
-
setDetailTab('properties');
|
|
338
|
-
}
|
|
339
|
-
};
|
|
340
|
-
|
|
341
|
-
return (
|
|
342
|
-
<WorkbenchLabeledPane
|
|
343
|
-
aria-label="Screen node details"
|
|
344
|
-
chrome="flat"
|
|
345
|
-
data-testid="screen-spec-inspector"
|
|
346
|
-
header={
|
|
347
|
-
<SegmentedControl
|
|
348
|
-
ariaLabel="Screen detail panel"
|
|
349
|
-
options={DETAIL_TABS}
|
|
350
|
-
value={detailTab}
|
|
351
|
-
onChange={setDetailTab}
|
|
352
|
-
/>
|
|
353
|
-
}
|
|
354
|
-
>
|
|
355
|
-
{detailTab === 'assets' ? (
|
|
356
|
-
<div data-testid="screen-spec-assets">
|
|
357
|
-
<ScreenSpecPalette
|
|
358
|
-
canClickPlace={canClickPlace}
|
|
359
|
-
insertTargetLabel={insertTargetLabel}
|
|
360
|
-
onPlaceKind={placeFromAssets}
|
|
361
|
-
/>
|
|
362
|
-
</div>
|
|
363
|
-
) : (
|
|
364
|
-
<div data-testid="screen-spec-props">
|
|
365
|
-
<WorkbenchPropertyStack>
|
|
366
|
-
<div className="widget-tree-inspector__header">
|
|
367
|
-
<Badge data-testid="screen-spec-kind-pill">
|
|
368
|
-
{screenKindLabel(selectedNode.kind)}
|
|
369
|
-
</Badge>
|
|
370
|
-
</div>
|
|
371
|
-
<ScreenNodeInspector
|
|
372
|
-
node={selectedNode}
|
|
373
|
-
parentKind={selectedEntry?.parentKind}
|
|
374
|
-
onChange={updateNode}
|
|
375
|
-
/>
|
|
376
|
-
</WorkbenchPropertyStack>
|
|
377
|
-
</div>
|
|
378
|
-
)}
|
|
379
|
-
</WorkbenchLabeledPane>
|
|
380
|
-
);
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
/**
|
|
384
|
-
* @deprecated Compatibility editor for pre-compile Screen Spec templates.
|
|
385
|
-
* Compile once and continue design/code editing in `WidgetTreeLab`.
|
|
386
|
-
*/
|
|
387
|
-
export function ScreenSpecEditor({
|
|
388
|
-
value,
|
|
389
|
-
onChange,
|
|
390
|
-
onCompileError,
|
|
391
|
-
className,
|
|
392
|
-
selectedPath,
|
|
393
|
-
onSelectPath,
|
|
394
|
-
pane = 'all',
|
|
395
|
-
}: ScreenSpecEditorProps) {
|
|
396
|
-
const model = useScreenSpecEditorModel({
|
|
397
|
-
value,
|
|
398
|
-
onChange,
|
|
399
|
-
onCompileError,
|
|
400
|
-
selectedPath,
|
|
401
|
-
onSelectPath,
|
|
402
|
-
});
|
|
403
|
-
|
|
404
|
-
if (pane === 'outline') {
|
|
405
|
-
return (
|
|
406
|
-
<WorkbenchFill className={className} data-testid="screen-spec-editor">
|
|
407
|
-
<ScreenSpecOutlineBody model={model} />
|
|
408
|
-
</WorkbenchFill>
|
|
409
|
-
);
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
if (pane === 'inspector') {
|
|
413
|
-
return (
|
|
414
|
-
<WorkbenchFill className={className}>
|
|
415
|
-
<ScreenSpecInspectorBody model={model} />
|
|
416
|
-
</WorkbenchFill>
|
|
417
|
-
);
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
return (
|
|
421
|
-
<WorkbenchFill className={className} data-testid="screen-spec-editor">
|
|
422
|
-
<SplitView
|
|
423
|
-
defaultPrimarySizePercent={38}
|
|
424
|
-
minPrimarySizePercent={24}
|
|
425
|
-
maxPrimarySizePercent={55}
|
|
426
|
-
primary={<ScreenSpecOutlineBody model={model} />}
|
|
427
|
-
secondary={<ScreenSpecInspectorBody model={model} />}
|
|
428
|
-
/>
|
|
429
|
-
</WorkbenchFill>
|
|
430
|
-
);
|
|
431
|
-
}
|
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
import { type ReactNode, useState } from 'react';
|
|
2
|
-
import {
|
|
3
|
-
screenColumn,
|
|
4
|
-
screenText,
|
|
5
|
-
type JdwScreenSpec,
|
|
6
|
-
type ScreenNodePath,
|
|
7
|
-
} from '@workbench-kit/jdw';
|
|
8
|
-
import { BUILTIN_JDW_REGISTRY } from '@workbench-kit/react/jdw';
|
|
9
|
-
import { JdwPreview } from '@workbench-kit/react/jdw/preview';
|
|
10
|
-
import {
|
|
11
|
-
WorkbenchFill,
|
|
12
|
-
WorkbenchLabeledPane,
|
|
13
|
-
WorkbenchParseError,
|
|
14
|
-
} from '@workbench-kit/react/primitives';
|
|
15
|
-
import { SplitView } from '@workbench-kit/react/workbench/shell';
|
|
16
|
-
|
|
17
|
-
import { ScreenSpecEditor } from './ScreenSpecEditor.js';
|
|
18
|
-
import { useScreenSpecPipeline } from './useScreenSpecPipeline.js';
|
|
19
|
-
|
|
20
|
-
const DEMO_SCREEN_SPEC: JdwScreenSpec = {
|
|
21
|
-
id: 'demo-screen',
|
|
22
|
-
title: 'Demo Screen',
|
|
23
|
-
description: 'Screen-spec compile smoke sample',
|
|
24
|
-
frameWidth: 360,
|
|
25
|
-
layout: { maxWidth: 360, maxHeight: 240 },
|
|
26
|
-
root: screenColumn([screenText('Hello from ScreenSpec')], { gap: 8, padding: 12 }),
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
export interface ScreenSpecEditorStoryHostProps {
|
|
30
|
-
readonly initialSpec?: JdwScreenSpec | undefined;
|
|
31
|
-
readonly previewLabel?: ReactNode | undefined;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Compatibility smoke shell: Outline | Preview | Inspector.
|
|
36
|
-
*
|
|
37
|
-
* @deprecated Active stories compile templates into JDW and open
|
|
38
|
-
* `WidgetTreeLab`. Keep this host only for compatibility editor tests.
|
|
39
|
-
*/
|
|
40
|
-
export function ScreenSpecEditorStoryHost({
|
|
41
|
-
initialSpec = DEMO_SCREEN_SPEC,
|
|
42
|
-
previewLabel = 'Compiled preview',
|
|
43
|
-
}: ScreenSpecEditorStoryHostProps) {
|
|
44
|
-
const pipeline = useScreenSpecPipeline(initialSpec);
|
|
45
|
-
const [selectedPath, setSelectedPath] = useState<ScreenNodePath>([]);
|
|
46
|
-
|
|
47
|
-
const previewPane = (
|
|
48
|
-
<WorkbenchLabeledPane
|
|
49
|
-
aria-label="Compiled JDW preview"
|
|
50
|
-
chrome="flat"
|
|
51
|
-
data-testid="jdw-screen-spec-story-preview"
|
|
52
|
-
title={previewLabel}
|
|
53
|
-
>
|
|
54
|
-
{pipeline.compileError ? (
|
|
55
|
-
<WorkbenchParseError role="alert" data-testid="jdw-screen-spec-story-error">
|
|
56
|
-
{pipeline.compileError}
|
|
57
|
-
</WorkbenchParseError>
|
|
58
|
-
) : (
|
|
59
|
-
<JdwPreview json={pipeline.json} registry={BUILTIN_JDW_REGISTRY} />
|
|
60
|
-
)}
|
|
61
|
-
</WorkbenchLabeledPane>
|
|
62
|
-
);
|
|
63
|
-
|
|
64
|
-
return (
|
|
65
|
-
<WorkbenchFill
|
|
66
|
-
className="jdw-screen-spec-story-host"
|
|
67
|
-
data-testid="jdw-screen-spec-story-host"
|
|
68
|
-
style={{ minHeight: 360, padding: 12 }}
|
|
69
|
-
>
|
|
70
|
-
<SplitView
|
|
71
|
-
defaultPrimarySizePercent={22}
|
|
72
|
-
minPrimarySizePercent={16}
|
|
73
|
-
maxPrimarySizePercent={36}
|
|
74
|
-
primary={
|
|
75
|
-
<ScreenSpecEditor
|
|
76
|
-
pane="outline"
|
|
77
|
-
selectedPath={selectedPath}
|
|
78
|
-
value={pipeline.spec}
|
|
79
|
-
onChange={pipeline.setSpec}
|
|
80
|
-
onSelectPath={setSelectedPath}
|
|
81
|
-
/>
|
|
82
|
-
}
|
|
83
|
-
secondary={
|
|
84
|
-
<SplitView
|
|
85
|
-
defaultPrimarySizePercent={64}
|
|
86
|
-
minPrimarySizePercent={40}
|
|
87
|
-
maxPrimarySizePercent={78}
|
|
88
|
-
primary={previewPane}
|
|
89
|
-
secondary={
|
|
90
|
-
<ScreenSpecEditor
|
|
91
|
-
pane="inspector"
|
|
92
|
-
selectedPath={selectedPath}
|
|
93
|
-
value={pipeline.spec}
|
|
94
|
-
onChange={pipeline.setSpec}
|
|
95
|
-
onSelectPath={setSelectedPath}
|
|
96
|
-
/>
|
|
97
|
-
}
|
|
98
|
-
/>
|
|
99
|
-
}
|
|
100
|
-
/>
|
|
101
|
-
</WorkbenchFill>
|
|
102
|
-
);
|
|
103
|
-
}
|
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
import type { DragEvent } from 'react';
|
|
2
|
-
import { SCREEN_SPEC_PALETTE_ASSETS, type ScreenPaletteKind } from '@workbench-kit/jdw';
|
|
3
|
-
import { Codicon, WorkbenchPropertyHint } from '@workbench-kit/react/primitives';
|
|
4
|
-
|
|
5
|
-
export const SCREEN_PALETTE_MIME = 'application/x-workbench-kit-screen-palette';
|
|
6
|
-
|
|
7
|
-
export interface ScreenPaletteItem {
|
|
8
|
-
readonly kind: ScreenPaletteKind;
|
|
9
|
-
readonly label: string;
|
|
10
|
-
readonly category: 'content' | 'layout';
|
|
11
|
-
readonly icon: string;
|
|
12
|
-
readonly description: string;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export const SCREEN_PALETTE_ITEMS: readonly ScreenPaletteItem[] = SCREEN_SPEC_PALETTE_ASSETS.map(
|
|
16
|
-
(asset) => ({
|
|
17
|
-
kind: asset.screenKind,
|
|
18
|
-
label: asset.label,
|
|
19
|
-
category: asset.category === 'layout' ? 'layout' : 'content',
|
|
20
|
-
icon: asset.icon ?? 'symbol-misc',
|
|
21
|
-
description: asset.description ?? asset.label,
|
|
22
|
-
}),
|
|
23
|
-
);
|
|
24
|
-
|
|
25
|
-
const CATEGORY_LABELS = {
|
|
26
|
-
content: 'Content',
|
|
27
|
-
layout: 'Layout',
|
|
28
|
-
} as const;
|
|
29
|
-
|
|
30
|
-
function cx(...parts: Array<string | false | null | undefined>) {
|
|
31
|
-
return parts.filter(Boolean).join(' ');
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export function writeScreenPaletteDragData(
|
|
35
|
-
dataTransfer: DataTransfer,
|
|
36
|
-
kind: ScreenPaletteKind,
|
|
37
|
-
): void {
|
|
38
|
-
dataTransfer.setData(SCREEN_PALETTE_MIME, kind);
|
|
39
|
-
dataTransfer.setData('text/plain', kind);
|
|
40
|
-
dataTransfer.effectAllowed = 'copy';
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export function readScreenPaletteDragData(dataTransfer: DataTransfer): ScreenPaletteKind | null {
|
|
44
|
-
const raw = dataTransfer.getData(SCREEN_PALETTE_MIME) || dataTransfer.getData('text/plain');
|
|
45
|
-
if (
|
|
46
|
-
raw === 'text' ||
|
|
47
|
-
raw === 'panel' ||
|
|
48
|
-
raw === 'row' ||
|
|
49
|
-
raw === 'column' ||
|
|
50
|
-
raw === 'grid' ||
|
|
51
|
-
raw === 'stack'
|
|
52
|
-
) {
|
|
53
|
-
return raw;
|
|
54
|
-
}
|
|
55
|
-
return null;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export interface ScreenSpecPaletteProps {
|
|
59
|
-
readonly canClickPlace: boolean;
|
|
60
|
-
readonly insertTargetLabel?: string | undefined;
|
|
61
|
-
readonly onPlaceKind: (kind: ScreenPaletteKind) => void;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export function ScreenSpecPalette({
|
|
65
|
-
canClickPlace,
|
|
66
|
-
insertTargetLabel,
|
|
67
|
-
onPlaceKind,
|
|
68
|
-
}: ScreenSpecPaletteProps) {
|
|
69
|
-
const categories = ['content', 'layout'] as const;
|
|
70
|
-
|
|
71
|
-
const handleDragStart = (event: DragEvent<HTMLButtonElement>, kind: ScreenPaletteKind): void => {
|
|
72
|
-
writeScreenPaletteDragData(event.dataTransfer, kind);
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
return (
|
|
76
|
-
<div className="widget-tree-asset-palette" data-testid="screen-spec-palette">
|
|
77
|
-
{canClickPlace ? (
|
|
78
|
-
<WorkbenchPropertyHint>
|
|
79
|
-
Click to add to <strong>{insertTargetLabel ?? 'container'}</strong>, or drag onto Outline.
|
|
80
|
-
</WorkbenchPropertyHint>
|
|
81
|
-
) : (
|
|
82
|
-
<WorkbenchPropertyHint>
|
|
83
|
-
Select a row/column/grid/stack in Outline to click-add, or drag onto a container.
|
|
84
|
-
</WorkbenchPropertyHint>
|
|
85
|
-
)}
|
|
86
|
-
|
|
87
|
-
{categories.map((category) => (
|
|
88
|
-
<section key={category} className="widget-tree-asset-palette__section">
|
|
89
|
-
<h3 className="widget-tree-asset-palette__title">{CATEGORY_LABELS[category]}</h3>
|
|
90
|
-
<div className="widget-tree-asset-palette__grid">
|
|
91
|
-
{SCREEN_PALETTE_ITEMS.filter((item) => item.category === category).map((item) => (
|
|
92
|
-
<button
|
|
93
|
-
key={item.kind}
|
|
94
|
-
aria-disabled={!canClickPlace}
|
|
95
|
-
className={cx(
|
|
96
|
-
'widget-tree-asset-palette__card',
|
|
97
|
-
'widget-tree-asset-palette__card--draggable',
|
|
98
|
-
!canClickPlace && 'widget-tree-asset-palette__card--drop-only',
|
|
99
|
-
)}
|
|
100
|
-
data-testid={`screen-spec-palette-${item.kind}`}
|
|
101
|
-
draggable
|
|
102
|
-
title={item.description}
|
|
103
|
-
type="button"
|
|
104
|
-
onClick={() => {
|
|
105
|
-
if (!canClickPlace) return;
|
|
106
|
-
onPlaceKind(item.kind);
|
|
107
|
-
}}
|
|
108
|
-
onDragStart={(event) => handleDragStart(event, item.kind)}
|
|
109
|
-
>
|
|
110
|
-
<Codicon icon={item.icon} />
|
|
111
|
-
<span className="widget-tree-asset-palette__label">{item.label}</span>
|
|
112
|
-
</button>
|
|
113
|
-
))}
|
|
114
|
-
</div>
|
|
115
|
-
</section>
|
|
116
|
-
))}
|
|
117
|
-
</div>
|
|
118
|
-
);
|
|
119
|
-
}
|
|
@@ -1,177 +0,0 @@
|
|
|
1
|
-
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
-
import {
|
|
3
|
-
formatScreenSpecJson,
|
|
4
|
-
parseScreenSpecJson,
|
|
5
|
-
screenNodePathToWidgetPath,
|
|
6
|
-
widgetPathToScreenNodePath,
|
|
7
|
-
type JdwScreenSpec,
|
|
8
|
-
type ScreenNodePath,
|
|
9
|
-
type WidgetPath,
|
|
10
|
-
} from '@workbench-kit/jdw';
|
|
11
|
-
import { BUILTIN_JDW_REGISTRY } from '@workbench-kit/react/jdw';
|
|
12
|
-
import { JdwPreviewViewport } from '@workbench-kit/react/jdw/preview-viewport';
|
|
13
|
-
import {
|
|
14
|
-
WorkbenchAuthoringShell,
|
|
15
|
-
WorkbenchLabeledPane,
|
|
16
|
-
WorkbenchParseError,
|
|
17
|
-
WorkbenchSurfaceMeta,
|
|
18
|
-
} from '@workbench-kit/react/primitives';
|
|
19
|
-
import { SplitView } from '@workbench-kit/react/workbench/shell';
|
|
20
|
-
|
|
21
|
-
import { ScreenSpecEditor } from './ScreenSpecEditor.js';
|
|
22
|
-
import { useScreenSpecPipeline } from './useScreenSpecPipeline.js';
|
|
23
|
-
|
|
24
|
-
/** @deprecated Compile Screen Spec templates once, then author the JDW result with `WidgetTreeLab`. */
|
|
25
|
-
export interface ScreenSpecWorkbenchProps {
|
|
26
|
-
readonly value: string;
|
|
27
|
-
readonly onChange?: ((next: string) => void) | undefined;
|
|
28
|
-
readonly className?: string | undefined;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const INVALID_SCREEN_FALLBACK: JdwScreenSpec = {
|
|
32
|
-
id: 'invalid-screen',
|
|
33
|
-
title: 'Invalid screen spec',
|
|
34
|
-
description: '',
|
|
35
|
-
frameWidth: 360,
|
|
36
|
-
layout: { maxWidth: 360, maxHeight: 240 },
|
|
37
|
-
root: { kind: 'text', content: '' },
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
const PREVIEW_HELP = 'Click to select · Middle-drag to pan · Ctrl+Scroll to zoom';
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Compatibility-only 3-pane Screen Spec authoring.
|
|
44
|
-
*
|
|
45
|
-
* @deprecated Compile Screen Spec templates once, then author the resulting JDW
|
|
46
|
-
* document with `WidgetTreeLab`. ScreenNodePath↔WidgetPath synchronization stays
|
|
47
|
-
* here only for compatibility and must not be used by active product entries.
|
|
48
|
-
*/
|
|
49
|
-
export function ScreenSpecWorkbench({ value, onChange, className }: ScreenSpecWorkbenchProps) {
|
|
50
|
-
const parsed = useMemo(() => parseScreenSpecJson(value), [value]);
|
|
51
|
-
const pipeline = useScreenSpecPipeline(parsed.value ?? INVALID_SCREEN_FALLBACK);
|
|
52
|
-
const { resetSpec, setSpec, spec, json, compileError } = pipeline;
|
|
53
|
-
const [selectedPath, setSelectedPath] = useState<ScreenNodePath>([]);
|
|
54
|
-
/** Skips document→pipeline echo after local writes (avoids recompile + selection reset). */
|
|
55
|
-
const lastWrittenValueRef = useRef<string | null>(null);
|
|
56
|
-
/** Mount seed — avoids an immediate resetSpec/compile for the same document. */
|
|
57
|
-
const seededSpecRef = useRef(parsed.value);
|
|
58
|
-
|
|
59
|
-
useEffect(() => {
|
|
60
|
-
if (lastWrittenValueRef.current === value) {
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
lastWrittenValueRef.current = null;
|
|
64
|
-
if (parsed.value) {
|
|
65
|
-
if (parsed.value !== seededSpecRef.current) {
|
|
66
|
-
resetSpec(parsed.value);
|
|
67
|
-
setSelectedPath([]);
|
|
68
|
-
}
|
|
69
|
-
seededSpecRef.current = parsed.value;
|
|
70
|
-
}
|
|
71
|
-
}, [parsed.value, resetSpec, value]);
|
|
72
|
-
|
|
73
|
-
const selectedWidgetPath = useMemo(() => {
|
|
74
|
-
if (!parsed.value) {
|
|
75
|
-
return null;
|
|
76
|
-
}
|
|
77
|
-
return screenNodePathToWidgetPath(spec.root, selectedPath);
|
|
78
|
-
}, [parsed.value, selectedPath, spec.root]);
|
|
79
|
-
|
|
80
|
-
const handleSpecChange = useCallback(
|
|
81
|
-
(nextSpec: JdwScreenSpec) => {
|
|
82
|
-
const nextValue = formatScreenSpecJson(nextSpec);
|
|
83
|
-
lastWrittenValueRef.current = nextValue;
|
|
84
|
-
setSpec(nextSpec);
|
|
85
|
-
onChange?.(nextValue);
|
|
86
|
-
},
|
|
87
|
-
[onChange, setSpec],
|
|
88
|
-
);
|
|
89
|
-
|
|
90
|
-
const handleSelectWidgetPath = useCallback(
|
|
91
|
-
(widgetPath: WidgetPath) => {
|
|
92
|
-
const nextPath = widgetPathToScreenNodePath(spec.root, widgetPath);
|
|
93
|
-
if (nextPath) {
|
|
94
|
-
setSelectedPath(nextPath);
|
|
95
|
-
}
|
|
96
|
-
},
|
|
97
|
-
[spec.root],
|
|
98
|
-
);
|
|
99
|
-
|
|
100
|
-
if (!parsed.value) {
|
|
101
|
-
return (
|
|
102
|
-
<WorkbenchAuthoringShell className={className} data-testid="screen-spec-workbench">
|
|
103
|
-
<WorkbenchParseError role="alert" data-testid="screen-spec-workbench-error">
|
|
104
|
-
{parsed.error ?? 'Invalid screen spec document.'}
|
|
105
|
-
</WorkbenchParseError>
|
|
106
|
-
</WorkbenchAuthoringShell>
|
|
107
|
-
);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
return (
|
|
111
|
-
<WorkbenchAuthoringShell
|
|
112
|
-
className={className}
|
|
113
|
-
data-testid="screen-spec-workbench"
|
|
114
|
-
toolbar={
|
|
115
|
-
spec.title ? (
|
|
116
|
-
<WorkbenchSurfaceMeta>
|
|
117
|
-
{spec.title}
|
|
118
|
-
{spec.description ? ` — ${spec.description}` : ''}
|
|
119
|
-
</WorkbenchSurfaceMeta>
|
|
120
|
-
) : null
|
|
121
|
-
}
|
|
122
|
-
>
|
|
123
|
-
{compileError ? (
|
|
124
|
-
<WorkbenchParseError role="alert" data-testid="screen-spec-workbench-error">
|
|
125
|
-
{compileError}
|
|
126
|
-
</WorkbenchParseError>
|
|
127
|
-
) : null}
|
|
128
|
-
<SplitView
|
|
129
|
-
defaultPrimarySizePercent={22}
|
|
130
|
-
minPrimarySizePercent={16}
|
|
131
|
-
maxPrimarySizePercent={36}
|
|
132
|
-
primary={
|
|
133
|
-
<ScreenSpecEditor
|
|
134
|
-
pane="outline"
|
|
135
|
-
selectedPath={selectedPath}
|
|
136
|
-
value={spec}
|
|
137
|
-
onChange={handleSpecChange}
|
|
138
|
-
onSelectPath={setSelectedPath}
|
|
139
|
-
/>
|
|
140
|
-
}
|
|
141
|
-
secondary={
|
|
142
|
-
<SplitView
|
|
143
|
-
defaultPrimarySizePercent={64}
|
|
144
|
-
minPrimarySizePercent={40}
|
|
145
|
-
maxPrimarySizePercent={78}
|
|
146
|
-
primary={
|
|
147
|
-
<WorkbenchLabeledPane
|
|
148
|
-
aria-label="Rendered preview"
|
|
149
|
-
chrome="flat"
|
|
150
|
-
data-testid="screen-spec-workbench-preview-pane"
|
|
151
|
-
title="Preview"
|
|
152
|
-
>
|
|
153
|
-
<JdwPreviewViewport
|
|
154
|
-
enablePrimaryPointerPan={false}
|
|
155
|
-
help={PREVIEW_HELP}
|
|
156
|
-
json={json}
|
|
157
|
-
registry={BUILTIN_JDW_REGISTRY}
|
|
158
|
-
selectedPath={selectedWidgetPath}
|
|
159
|
-
onSelectPath={handleSelectWidgetPath}
|
|
160
|
-
/>
|
|
161
|
-
</WorkbenchLabeledPane>
|
|
162
|
-
}
|
|
163
|
-
secondary={
|
|
164
|
-
<ScreenSpecEditor
|
|
165
|
-
pane="inspector"
|
|
166
|
-
selectedPath={selectedPath}
|
|
167
|
-
value={spec}
|
|
168
|
-
onChange={handleSpecChange}
|
|
169
|
-
onSelectPath={setSelectedPath}
|
|
170
|
-
/>
|
|
171
|
-
}
|
|
172
|
-
/>
|
|
173
|
-
}
|
|
174
|
-
/>
|
|
175
|
-
</WorkbenchAuthoringShell>
|
|
176
|
-
);
|
|
177
|
-
}
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import type { ScreenNodePath, ScreenSpecOutlineEntry } from '@workbench-kit/jdw';
|
|
2
|
-
|
|
3
|
-
function pathKey(path: ScreenNodePath): string {
|
|
4
|
-
return path.length === 0 ? 'root' : path.join('.');
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
function normalizedTokens(query: string): string[] {
|
|
8
|
-
return query.trim().toLocaleLowerCase().split(/\s+/).filter(Boolean);
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
function entryMatches(entry: ScreenSpecOutlineEntry, tokens: readonly string[]): boolean {
|
|
12
|
-
const haystack = `${entry.label} ${entry.node.kind}`.toLocaleLowerCase();
|
|
13
|
-
return tokens.every((token) => haystack.includes(token));
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Unreal Outliner-style search: keep matches and their ancestors so tree context remains.
|
|
18
|
-
* Multi-term queries use AND (every token must match).
|
|
19
|
-
*/
|
|
20
|
-
export function filterScreenSpecOutline(
|
|
21
|
-
outline: readonly ScreenSpecOutlineEntry[],
|
|
22
|
-
query: string,
|
|
23
|
-
): readonly ScreenSpecOutlineEntry[] {
|
|
24
|
-
const tokens = normalizedTokens(query);
|
|
25
|
-
if (tokens.length === 0) {
|
|
26
|
-
return outline;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const keep = new Set<string>();
|
|
30
|
-
for (const entry of outline) {
|
|
31
|
-
if (!entryMatches(entry, tokens)) {
|
|
32
|
-
continue;
|
|
33
|
-
}
|
|
34
|
-
for (let depth = 0; depth <= entry.path.length; depth += 1) {
|
|
35
|
-
keep.add(pathKey(entry.path.slice(0, depth)));
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
return outline.filter((entry) => keep.has(pathKey(entry.path)));
|
|
40
|
-
}
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
import { useCallback, useState } from 'react';
|
|
2
|
-
import {
|
|
3
|
-
compileScreenSpecToJson,
|
|
4
|
-
type JdwScreenSpec,
|
|
5
|
-
type LayoutConstraints,
|
|
6
|
-
} from '@workbench-kit/jdw';
|
|
7
|
-
|
|
8
|
-
export interface ScreenSpecPipelineState {
|
|
9
|
-
readonly spec: JdwScreenSpec;
|
|
10
|
-
readonly json: string;
|
|
11
|
-
readonly compileError: string | null;
|
|
12
|
-
readonly layoutConstraints: LayoutConstraints;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export interface UseScreenSpecPipelineResult extends ScreenSpecPipelineState {
|
|
16
|
-
readonly setSpec: (spec: JdwScreenSpec) => void;
|
|
17
|
-
readonly setJson: (json: string) => void;
|
|
18
|
-
readonly resetSpec: (spec: JdwScreenSpec) => void;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function layoutConstraintsFromSpec(spec: JdwScreenSpec): LayoutConstraints {
|
|
22
|
-
return {
|
|
23
|
-
minWidth: 0,
|
|
24
|
-
maxWidth: spec.layout.maxWidth,
|
|
25
|
-
minHeight: 0,
|
|
26
|
-
maxHeight: spec.layout.maxHeight,
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function compileSpec(spec: JdwScreenSpec): { json: string; error: string | null } {
|
|
31
|
-
try {
|
|
32
|
-
return { json: compileScreenSpecToJson(spec), error: null };
|
|
33
|
-
} catch (error) {
|
|
34
|
-
return {
|
|
35
|
-
json: '',
|
|
36
|
-
error: error instanceof Error ? error.message : String(error),
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function createPipelineState(spec: JdwScreenSpec): ScreenSpecPipelineState {
|
|
42
|
-
const compiled = compileSpec(spec);
|
|
43
|
-
return {
|
|
44
|
-
spec,
|
|
45
|
-
json: compiled.json,
|
|
46
|
-
compileError: compiled.error,
|
|
47
|
-
layoutConstraints: layoutConstraintsFromSpec(spec),
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Owns Screen Spec → compiled JDW JSON. Compiles once per `setSpec`/`resetSpec`.
|
|
53
|
-
* `initialSpec` is mount-only; external document reloads must call `resetSpec`.
|
|
54
|
-
*/
|
|
55
|
-
export function useScreenSpecPipeline(initialSpec: JdwScreenSpec): UseScreenSpecPipelineResult {
|
|
56
|
-
const [state, setState] = useState(() => createPipelineState(initialSpec));
|
|
57
|
-
|
|
58
|
-
const setSpec = useCallback((nextSpec: JdwScreenSpec) => {
|
|
59
|
-
const compiled = compileSpec(nextSpec);
|
|
60
|
-
setState((prev) => ({
|
|
61
|
-
spec: nextSpec,
|
|
62
|
-
layoutConstraints: layoutConstraintsFromSpec(nextSpec),
|
|
63
|
-
compileError: compiled.error,
|
|
64
|
-
json: compiled.error === null ? compiled.json : prev.json,
|
|
65
|
-
}));
|
|
66
|
-
}, []);
|
|
67
|
-
|
|
68
|
-
const setJson = useCallback((nextJson: string) => {
|
|
69
|
-
setState((prev) => ({
|
|
70
|
-
...prev,
|
|
71
|
-
json: nextJson,
|
|
72
|
-
compileError: null,
|
|
73
|
-
}));
|
|
74
|
-
}, []);
|
|
75
|
-
|
|
76
|
-
return {
|
|
77
|
-
...state,
|
|
78
|
-
setSpec,
|
|
79
|
-
setJson,
|
|
80
|
-
resetSpec: setSpec,
|
|
81
|
-
};
|
|
82
|
-
}
|
|
File without changes
|