@workbench-kit/jdw-editor 0.0.1-prototype.0
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 +38 -0
- package/src/JdwSampleScreenExplorer.tsx +181 -0
- package/src/ScreenNodeInspector.tsx +203 -0
- package/src/ScreenSpecEditor.tsx +196 -0
- package/src/index.ts +14 -0
- package/src/useScreenSpecPipeline.ts +88 -0
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@workbench-kit/jdw-editor",
|
|
3
|
+
"version": "0.0.1-prototype.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./src/index.ts"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"src",
|
|
11
|
+
"!src/**/*.test.ts",
|
|
12
|
+
"!src/**/*.test.tsx",
|
|
13
|
+
"!src/**/*.stories.ts",
|
|
14
|
+
"!src/**/*.stories.tsx"
|
|
15
|
+
],
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@workbench-kit/jdw": "0.0.1-prototype.0",
|
|
18
|
+
"@workbench-kit/react": "0.0.1-prototype.0"
|
|
19
|
+
},
|
|
20
|
+
"peerDependencies": {
|
|
21
|
+
"react": "^19.0.0"
|
|
22
|
+
},
|
|
23
|
+
"description": "React editor UI for JDW screen specs and live compile preview pipelines.",
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"access": "public",
|
|
26
|
+
"tag": "prototype",
|
|
27
|
+
"provenance": true
|
|
28
|
+
},
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "git+https://github.com/NewChoBo/workbench-kit.git",
|
|
32
|
+
"directory": "packages/jdw-editor"
|
|
33
|
+
},
|
|
34
|
+
"scripts": {
|
|
35
|
+
"test": "pnpm exec vitest run --config ../../vitest.config.ts src",
|
|
36
|
+
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
import { compileScreenSpecText } from '@workbench-kit/jdw';
|
|
4
|
+
import { JdwPreview } from '@workbench-kit/react/jdw/preview';
|
|
5
|
+
import {
|
|
6
|
+
JDW_SAMPLE_SCREENS,
|
|
7
|
+
formatJdwSampleScreenSpec,
|
|
8
|
+
type JdwSampleScreenDefinition,
|
|
9
|
+
} from '@workbench-kit/react/jdw/samples';
|
|
10
|
+
import {
|
|
11
|
+
Field,
|
|
12
|
+
Panel,
|
|
13
|
+
PanelBody,
|
|
14
|
+
PanelHeader,
|
|
15
|
+
Select,
|
|
16
|
+
SegmentedControl,
|
|
17
|
+
WorkbenchParseError,
|
|
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
|
+
export type JdwSampleSourceView = 'editor' | 'jdw';
|
|
25
|
+
|
|
26
|
+
export interface JdwSampleScreenExplorerProps {
|
|
27
|
+
readonly samples?: readonly JdwSampleScreenDefinition[] | undefined;
|
|
28
|
+
readonly initialSampleId?: string | undefined;
|
|
29
|
+
readonly initialSourceView?: JdwSampleSourceView | undefined;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function resolveSample(
|
|
33
|
+
samples: readonly JdwSampleScreenDefinition[],
|
|
34
|
+
sampleId: string,
|
|
35
|
+
): JdwSampleScreenDefinition {
|
|
36
|
+
return samples.find((entry) => entry.id === sampleId) ?? samples[0]!;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const SOURCE_VIEW_OPTIONS = [
|
|
40
|
+
{ label: 'Screen editor', testId: 'jdw-sample-source-editor', value: 'editor' as const },
|
|
41
|
+
{ label: 'JDW JSON', testId: 'jdw-sample-source-jdw', value: 'jdw' as const },
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
export function JdwSampleScreenExplorer({
|
|
45
|
+
samples = JDW_SAMPLE_SCREENS,
|
|
46
|
+
initialSampleId,
|
|
47
|
+
initialSourceView = 'editor',
|
|
48
|
+
}: JdwSampleScreenExplorerProps) {
|
|
49
|
+
const [sampleId, setSampleId] = useState(initialSampleId ?? samples[0]?.id ?? '');
|
|
50
|
+
const [sourceView, setSourceView] = useState<JdwSampleSourceView>(initialSourceView);
|
|
51
|
+
const activeSample = resolveSample(samples, sampleId);
|
|
52
|
+
const pipeline = useScreenSpecPipeline(activeSample);
|
|
53
|
+
const { resetSpec } = pipeline;
|
|
54
|
+
const [editorError, setEditorError] = useState<string | null>(null);
|
|
55
|
+
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
resetSpec(resolveSample(samples, sampleId));
|
|
58
|
+
setEditorError(null);
|
|
59
|
+
}, [resetSpec, sampleId, samples]);
|
|
60
|
+
|
|
61
|
+
if (samples.length === 0) {
|
|
62
|
+
return <div data-testid="jdw-sample-explorer-empty">No sample screens configured.</div>;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const compileError = pipeline.compileError ?? editorError;
|
|
66
|
+
|
|
67
|
+
const handleSourceViewChange = (nextView: JdwSampleSourceView) => {
|
|
68
|
+
if (nextView === 'jdw') {
|
|
69
|
+
const compiled = compileScreenSpecText(formatJdwSampleScreenSpec(pipeline.spec));
|
|
70
|
+
if (compiled.json) {
|
|
71
|
+
pipeline.setJson(compiled.json);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
setSourceView(nextView);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<div className="jdw-sample-explorer" data-testid="jdw-sample-explorer">
|
|
79
|
+
<Panel
|
|
80
|
+
className="jdw-sample-explorer__panel"
|
|
81
|
+
style={{ display: 'flex', flexDirection: 'column', minHeight: 0, flex: 1 }}
|
|
82
|
+
>
|
|
83
|
+
<PanelHeader
|
|
84
|
+
actions={
|
|
85
|
+
<div className="jdw-sample-explorer__header-actions">
|
|
86
|
+
<SegmentedControl
|
|
87
|
+
ariaLabel="Source view"
|
|
88
|
+
options={SOURCE_VIEW_OPTIONS}
|
|
89
|
+
value={sourceView}
|
|
90
|
+
onChange={handleSourceViewChange}
|
|
91
|
+
/>
|
|
92
|
+
<Field label="Sample screen" htmlFor="jdw-sample-screen-select" inline>
|
|
93
|
+
<Select
|
|
94
|
+
id="jdw-sample-screen-select"
|
|
95
|
+
aria-label="Sample screen"
|
|
96
|
+
data-testid="jdw-sample-screen-select"
|
|
97
|
+
controlWidth="wide"
|
|
98
|
+
value={activeSample.id}
|
|
99
|
+
onValueChange={setSampleId}
|
|
100
|
+
>
|
|
101
|
+
{samples.map((sample) => (
|
|
102
|
+
<option key={sample.id} value={sample.id}>
|
|
103
|
+
{sample.title}
|
|
104
|
+
</option>
|
|
105
|
+
))}
|
|
106
|
+
</Select>
|
|
107
|
+
</Field>
|
|
108
|
+
</div>
|
|
109
|
+
}
|
|
110
|
+
>
|
|
111
|
+
JDW Sample Explorer
|
|
112
|
+
</PanelHeader>
|
|
113
|
+
<PanelBody
|
|
114
|
+
className="jdw-sample-explorer__body"
|
|
115
|
+
style={{
|
|
116
|
+
display: 'flex',
|
|
117
|
+
flexDirection: 'column',
|
|
118
|
+
gap: 8,
|
|
119
|
+
minHeight: 0,
|
|
120
|
+
flex: 1,
|
|
121
|
+
paddingTop: 0,
|
|
122
|
+
}}
|
|
123
|
+
>
|
|
124
|
+
<p className="jdw-sample-explorer__description">{activeSample.description}</p>
|
|
125
|
+
{compileError ? (
|
|
126
|
+
<WorkbenchParseError role="alert" data-testid="jdw-sample-explorer-error">
|
|
127
|
+
{compileError}
|
|
128
|
+
</WorkbenchParseError>
|
|
129
|
+
) : null}
|
|
130
|
+
<SplitView
|
|
131
|
+
className="jdw-sample-explorer__split"
|
|
132
|
+
defaultPrimarySizePercent={46}
|
|
133
|
+
minPrimarySizePercent={24}
|
|
134
|
+
maxPrimarySizePercent={72}
|
|
135
|
+
primary={
|
|
136
|
+
<section
|
|
137
|
+
aria-label={sourceView === 'editor' ? 'Screen spec editor' : 'JDW JSON source'}
|
|
138
|
+
className="jdw-sample-explorer__pane"
|
|
139
|
+
data-testid="jdw-sample-explorer-source-pane"
|
|
140
|
+
>
|
|
141
|
+
<header className="jdw-sample-explorer__pane-header">
|
|
142
|
+
{sourceView === 'editor' ? 'Screen editor' : 'JDW JSON'}
|
|
143
|
+
</header>
|
|
144
|
+
<div className="jdw-sample-explorer__pane-body">
|
|
145
|
+
{sourceView === 'editor' ? (
|
|
146
|
+
<ScreenSpecEditor
|
|
147
|
+
value={pipeline.spec}
|
|
148
|
+
onChange={pipeline.setSpec}
|
|
149
|
+
onCompileError={setEditorError}
|
|
150
|
+
/>
|
|
151
|
+
) : (
|
|
152
|
+
<textarea
|
|
153
|
+
aria-label="JDW JSON source"
|
|
154
|
+
className="jdw-sample-explorer__json-source"
|
|
155
|
+
data-testid="jdw-sample-explorer-json"
|
|
156
|
+
value={pipeline.json}
|
|
157
|
+
onChange={(event) => pipeline.setJson(event.target.value)}
|
|
158
|
+
spellCheck={false}
|
|
159
|
+
/>
|
|
160
|
+
)}
|
|
161
|
+
</div>
|
|
162
|
+
</section>
|
|
163
|
+
}
|
|
164
|
+
secondary={
|
|
165
|
+
<section
|
|
166
|
+
aria-label="Rendered preview"
|
|
167
|
+
className="jdw-sample-explorer__pane"
|
|
168
|
+
data-testid="jdw-sample-explorer-preview-pane"
|
|
169
|
+
>
|
|
170
|
+
<header className="jdw-sample-explorer__pane-header">Preview</header>
|
|
171
|
+
<div className="jdw-sample-explorer__pane-body">
|
|
172
|
+
<JdwPreview json={pipeline.json} layoutConstraints={pipeline.layoutConstraints} />
|
|
173
|
+
</div>
|
|
174
|
+
</section>
|
|
175
|
+
}
|
|
176
|
+
/>
|
|
177
|
+
</PanelBody>
|
|
178
|
+
</Panel>
|
|
179
|
+
</div>
|
|
180
|
+
);
|
|
181
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import type { ScreenNode, ScreenTextStyle } from '@workbench-kit/jdw';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
TextInput,
|
|
5
|
+
WorkbenchPropertyHint,
|
|
6
|
+
WorkbenchPropertyNumberRow,
|
|
7
|
+
WorkbenchPropertyPanel,
|
|
8
|
+
WorkbenchPropertyRow,
|
|
9
|
+
WorkbenchPropertyStack,
|
|
10
|
+
WorkbenchPropertyTextRow,
|
|
11
|
+
} from '@workbench-kit/react/primitives';
|
|
12
|
+
|
|
13
|
+
export interface ScreenNodeInspectorProps {
|
|
14
|
+
readonly node: ScreenNode;
|
|
15
|
+
readonly parentKind?: ScreenNode['kind'] | undefined;
|
|
16
|
+
readonly onChange: (node: ScreenNode) => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function readStyle(node: ScreenNode): ScreenTextStyle {
|
|
20
|
+
if (node.kind === 'text' || node.kind === 'panel') {
|
|
21
|
+
return node.style ?? {};
|
|
22
|
+
}
|
|
23
|
+
return {};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function patchStyle(node: ScreenNode, patch: ScreenTextStyle): ScreenNode {
|
|
27
|
+
if (node.kind === 'text' || node.kind === 'panel') {
|
|
28
|
+
return { ...node, style: { ...node.style, ...patch } };
|
|
29
|
+
}
|
|
30
|
+
return node;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function OptionalNumberRow({
|
|
34
|
+
label,
|
|
35
|
+
testId,
|
|
36
|
+
value,
|
|
37
|
+
onChange,
|
|
38
|
+
}: {
|
|
39
|
+
readonly label: string;
|
|
40
|
+
readonly testId: string;
|
|
41
|
+
readonly value: number | undefined;
|
|
42
|
+
readonly onChange: (value: number | undefined) => void;
|
|
43
|
+
}) {
|
|
44
|
+
return (
|
|
45
|
+
<WorkbenchPropertyRow label={label} htmlFor={testId}>
|
|
46
|
+
<TextInput
|
|
47
|
+
id={testId}
|
|
48
|
+
data-testid={testId}
|
|
49
|
+
controlWidth="full"
|
|
50
|
+
type="number"
|
|
51
|
+
value={value ?? ''}
|
|
52
|
+
onChange={(event) => {
|
|
53
|
+
const raw = event.currentTarget.value;
|
|
54
|
+
onChange(raw === '' ? undefined : Number(raw));
|
|
55
|
+
}}
|
|
56
|
+
/>
|
|
57
|
+
</WorkbenchPropertyRow>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function ScreenNodeInspector({ node, parentKind, onChange }: ScreenNodeInspectorProps) {
|
|
62
|
+
if (node.kind === 'text' || node.kind === 'panel') {
|
|
63
|
+
const style = readStyle(node);
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<WorkbenchPropertyPanel
|
|
67
|
+
className="jdw-screen-node-inspector"
|
|
68
|
+
data-testid="screen-spec-node-inspector"
|
|
69
|
+
>
|
|
70
|
+
<WorkbenchPropertyStack>
|
|
71
|
+
<WorkbenchPropertyRow label="Content" htmlFor="screen-spec-field-content">
|
|
72
|
+
<TextInput
|
|
73
|
+
id="screen-spec-field-content"
|
|
74
|
+
data-testid="screen-spec-field-content"
|
|
75
|
+
controlWidth="full"
|
|
76
|
+
value={node.content}
|
|
77
|
+
onValueChange={(content) => onChange({ ...node, content })}
|
|
78
|
+
/>
|
|
79
|
+
</WorkbenchPropertyRow>
|
|
80
|
+
{node.kind === 'panel' ? (
|
|
81
|
+
<WorkbenchPropertyTextRow
|
|
82
|
+
htmlFor="screen-spec-field-panel-background"
|
|
83
|
+
label="Panel background"
|
|
84
|
+
value={node.background ?? ''}
|
|
85
|
+
onValueChange={(background) => onChange({ ...node, background })}
|
|
86
|
+
/>
|
|
87
|
+
) : null}
|
|
88
|
+
<WorkbenchPropertyTextRow
|
|
89
|
+
htmlFor="screen-spec-field-color"
|
|
90
|
+
label="Text color"
|
|
91
|
+
value={style.color ?? ''}
|
|
92
|
+
onValueChange={(color) => onChange(patchStyle(node, { color }))}
|
|
93
|
+
/>
|
|
94
|
+
<OptionalNumberRow
|
|
95
|
+
label="Font size"
|
|
96
|
+
testId="screen-spec-field-font-size"
|
|
97
|
+
value={style.fontSize}
|
|
98
|
+
onChange={(fontSize) => onChange(patchStyle(node, { fontSize }))}
|
|
99
|
+
/>
|
|
100
|
+
<WorkbenchPropertyTextRow
|
|
101
|
+
htmlFor="screen-spec-field-background"
|
|
102
|
+
label="Background"
|
|
103
|
+
value={style.background ?? ''}
|
|
104
|
+
onValueChange={(background) => onChange(patchStyle(node, { background }))}
|
|
105
|
+
/>
|
|
106
|
+
{parentKind === 'grid' ? (
|
|
107
|
+
<>
|
|
108
|
+
<OptionalNumberRow
|
|
109
|
+
label="Column"
|
|
110
|
+
testId="screen-spec-field-col"
|
|
111
|
+
value={node.col}
|
|
112
|
+
onChange={(col) => onChange({ ...node, col })}
|
|
113
|
+
/>
|
|
114
|
+
<OptionalNumberRow
|
|
115
|
+
label="Row"
|
|
116
|
+
testId="screen-spec-field-row"
|
|
117
|
+
value={node.row}
|
|
118
|
+
onChange={(row) => onChange({ ...node, row })}
|
|
119
|
+
/>
|
|
120
|
+
<OptionalNumberRow
|
|
121
|
+
label="Column span"
|
|
122
|
+
testId="screen-spec-field-col-span"
|
|
123
|
+
value={node.colSpan}
|
|
124
|
+
onChange={(colSpan) => onChange({ ...node, colSpan })}
|
|
125
|
+
/>
|
|
126
|
+
<OptionalNumberRow
|
|
127
|
+
label="Row span"
|
|
128
|
+
testId="screen-spec-field-row-span"
|
|
129
|
+
value={node.rowSpan}
|
|
130
|
+
onChange={(rowSpan) => onChange({ ...node, rowSpan })}
|
|
131
|
+
/>
|
|
132
|
+
</>
|
|
133
|
+
) : null}
|
|
134
|
+
</WorkbenchPropertyStack>
|
|
135
|
+
</WorkbenchPropertyPanel>
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (node.kind === 'expanded') {
|
|
140
|
+
return (
|
|
141
|
+
<WorkbenchPropertyPanel
|
|
142
|
+
className="jdw-screen-node-inspector"
|
|
143
|
+
data-testid="screen-spec-node-inspector"
|
|
144
|
+
>
|
|
145
|
+
<WorkbenchPropertyStack>
|
|
146
|
+
<OptionalNumberRow
|
|
147
|
+
label="Flex"
|
|
148
|
+
testId="screen-spec-field-flex"
|
|
149
|
+
value={node.flex}
|
|
150
|
+
onChange={(flex) => onChange({ ...node, flex })}
|
|
151
|
+
/>
|
|
152
|
+
<WorkbenchPropertyHint>Edit the wrapped child from the outline.</WorkbenchPropertyHint>
|
|
153
|
+
</WorkbenchPropertyStack>
|
|
154
|
+
</WorkbenchPropertyPanel>
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (
|
|
159
|
+
node.kind === 'row' ||
|
|
160
|
+
node.kind === 'column' ||
|
|
161
|
+
node.kind === 'grid' ||
|
|
162
|
+
node.kind === 'stack'
|
|
163
|
+
) {
|
|
164
|
+
return (
|
|
165
|
+
<WorkbenchPropertyPanel
|
|
166
|
+
className="jdw-screen-node-inspector"
|
|
167
|
+
data-testid="screen-spec-node-inspector"
|
|
168
|
+
>
|
|
169
|
+
<WorkbenchPropertyStack>
|
|
170
|
+
{node.kind === 'grid' ? (
|
|
171
|
+
<WorkbenchPropertyNumberRow
|
|
172
|
+
htmlFor="screen-spec-field-columns"
|
|
173
|
+
label="Columns"
|
|
174
|
+
min={1}
|
|
175
|
+
value={node.columns}
|
|
176
|
+
onValueChange={(columns) => onChange({ ...node, columns: Math.max(1, columns) })}
|
|
177
|
+
/>
|
|
178
|
+
) : null}
|
|
179
|
+
<OptionalNumberRow
|
|
180
|
+
label="Gap"
|
|
181
|
+
testId="screen-spec-field-gap"
|
|
182
|
+
value={node.gap}
|
|
183
|
+
onChange={(gap) => onChange({ ...node, gap })}
|
|
184
|
+
/>
|
|
185
|
+
<OptionalNumberRow
|
|
186
|
+
label="Padding"
|
|
187
|
+
testId="screen-spec-field-padding"
|
|
188
|
+
value={node.padding}
|
|
189
|
+
onChange={(padding) => onChange({ ...node, padding })}
|
|
190
|
+
/>
|
|
191
|
+
<WorkbenchPropertyTextRow
|
|
192
|
+
htmlFor="screen-spec-field-container-background"
|
|
193
|
+
label="Background"
|
|
194
|
+
value={node.background ?? ''}
|
|
195
|
+
onValueChange={(background) => onChange({ ...node, background })}
|
|
196
|
+
/>
|
|
197
|
+
</WorkbenchPropertyStack>
|
|
198
|
+
</WorkbenchPropertyPanel>
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { useMemo, useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
compileScreenSpecToJson,
|
|
4
|
+
listScreenSpecOutline,
|
|
5
|
+
updateScreenNodeAt,
|
|
6
|
+
updateScreenSpecMetadata,
|
|
7
|
+
type JdwScreenSpec,
|
|
8
|
+
type ScreenNode,
|
|
9
|
+
type ScreenNodePath,
|
|
10
|
+
} from '@workbench-kit/jdw';
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
Panel,
|
|
14
|
+
PanelBody,
|
|
15
|
+
ResizablePanels,
|
|
16
|
+
TextInput,
|
|
17
|
+
WorkbenchPropertyNumberRow,
|
|
18
|
+
WorkbenchPropertyRow,
|
|
19
|
+
WorkbenchPropertySection,
|
|
20
|
+
WorkbenchPropertyStack,
|
|
21
|
+
WorkbenchPropertyTextRow,
|
|
22
|
+
} from '@workbench-kit/react/primitives';
|
|
23
|
+
import { ScreenNodeInspector } from './ScreenNodeInspector.js';
|
|
24
|
+
|
|
25
|
+
function cx(...parts: Array<string | false | undefined | null | undefined>) {
|
|
26
|
+
return parts.filter(Boolean).join(' ');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function pathKey(path: ScreenNodePath): string {
|
|
30
|
+
return path.length === 0 ? 'root' : path.join('.');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface ScreenSpecEditorProps {
|
|
34
|
+
readonly value: JdwScreenSpec;
|
|
35
|
+
readonly onChange: (spec: JdwScreenSpec) => void;
|
|
36
|
+
readonly onCompileError?: ((message: string | null) => void) | undefined;
|
|
37
|
+
readonly className?: string | undefined;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function ScreenSpecEditor({
|
|
41
|
+
value,
|
|
42
|
+
onChange,
|
|
43
|
+
onCompileError,
|
|
44
|
+
className,
|
|
45
|
+
}: ScreenSpecEditorProps) {
|
|
46
|
+
const [selectedPath, setSelectedPath] = useState<ScreenNodePath>([]);
|
|
47
|
+
const outline = useMemo(() => listScreenSpecOutline(value), [value]);
|
|
48
|
+
const selectedEntry =
|
|
49
|
+
outline.find((entry) => pathKey(entry.path) === pathKey(selectedPath)) ?? outline[0];
|
|
50
|
+
const selectedNode = selectedEntry?.node ?? value.root;
|
|
51
|
+
|
|
52
|
+
const commitSpec = (nextSpec: JdwScreenSpec) => {
|
|
53
|
+
onChange(nextSpec);
|
|
54
|
+
try {
|
|
55
|
+
compileScreenSpecToJson(nextSpec);
|
|
56
|
+
onCompileError?.(null);
|
|
57
|
+
} catch (error) {
|
|
58
|
+
onCompileError?.(error instanceof Error ? error.message : String(error));
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const updateNode = (nextNode: ScreenNode) => {
|
|
63
|
+
commitSpec(updateScreenNodeAt(value, selectedEntry?.path ?? [], nextNode));
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const metadataPanel = (
|
|
67
|
+
<WorkbenchPropertySection title="Screen" data-testid="screen-spec-metadata">
|
|
68
|
+
<WorkbenchPropertyStack gap="sm">
|
|
69
|
+
<WorkbenchPropertyRow label="Title" htmlFor="screen-spec-field-title">
|
|
70
|
+
<TextInput
|
|
71
|
+
id="screen-spec-field-title"
|
|
72
|
+
data-testid="screen-spec-field-title"
|
|
73
|
+
controlWidth="full"
|
|
74
|
+
value={value.title}
|
|
75
|
+
onValueChange={(title) => commitSpec(updateScreenSpecMetadata(value, { title }))}
|
|
76
|
+
/>
|
|
77
|
+
</WorkbenchPropertyRow>
|
|
78
|
+
<WorkbenchPropertyTextRow
|
|
79
|
+
htmlFor="screen-spec-field-description"
|
|
80
|
+
label="Description"
|
|
81
|
+
value={value.description}
|
|
82
|
+
onValueChange={(description) =>
|
|
83
|
+
commitSpec(updateScreenSpecMetadata(value, { description }))
|
|
84
|
+
}
|
|
85
|
+
/>
|
|
86
|
+
<WorkbenchPropertyNumberRow
|
|
87
|
+
htmlFor="screen-spec-field-frame-width"
|
|
88
|
+
label="Frame width"
|
|
89
|
+
min={1}
|
|
90
|
+
value={value.frameWidth}
|
|
91
|
+
onValueChange={(frameWidth) =>
|
|
92
|
+
commitSpec(updateScreenSpecMetadata(value, { frameWidth }))
|
|
93
|
+
}
|
|
94
|
+
/>
|
|
95
|
+
<WorkbenchPropertyNumberRow
|
|
96
|
+
htmlFor="screen-spec-field-max-width"
|
|
97
|
+
label="Preview max width"
|
|
98
|
+
min={1}
|
|
99
|
+
value={value.layout.maxWidth}
|
|
100
|
+
onValueChange={(maxWidth) =>
|
|
101
|
+
commitSpec(
|
|
102
|
+
updateScreenSpecMetadata(value, {
|
|
103
|
+
layout: { ...value.layout, maxWidth },
|
|
104
|
+
}),
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
/>
|
|
108
|
+
<WorkbenchPropertyNumberRow
|
|
109
|
+
htmlFor="screen-spec-field-max-height"
|
|
110
|
+
label="Preview max height"
|
|
111
|
+
min={1}
|
|
112
|
+
value={value.layout.maxHeight}
|
|
113
|
+
onValueChange={(maxHeight) =>
|
|
114
|
+
commitSpec(
|
|
115
|
+
updateScreenSpecMetadata(value, {
|
|
116
|
+
layout: { ...value.layout, maxHeight },
|
|
117
|
+
}),
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
/>
|
|
121
|
+
</WorkbenchPropertyStack>
|
|
122
|
+
</WorkbenchPropertySection>
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
const outlinePanel = (
|
|
126
|
+
<Panel
|
|
127
|
+
className="widget-tree-outline jdw-screen-spec-editor__outline"
|
|
128
|
+
data-testid="screen-spec-outline"
|
|
129
|
+
>
|
|
130
|
+
<PanelBody className="widget-tree-outline__body">
|
|
131
|
+
<ul aria-label="Screen node outline" className="widget-tree-outline__list" role="tree">
|
|
132
|
+
{outline.map((entry) => {
|
|
133
|
+
const key = pathKey(entry.path);
|
|
134
|
+
const selected = pathKey(selectedEntry?.path ?? []) === key;
|
|
135
|
+
const depth = entry.depth;
|
|
136
|
+
|
|
137
|
+
return (
|
|
138
|
+
<li
|
|
139
|
+
key={key}
|
|
140
|
+
aria-level={depth + 1}
|
|
141
|
+
aria-selected={selected}
|
|
142
|
+
className={cx(
|
|
143
|
+
'widget-tree-outline__item',
|
|
144
|
+
selected && 'widget-tree-outline__item--selected',
|
|
145
|
+
)}
|
|
146
|
+
role="treeitem"
|
|
147
|
+
style={{ paddingLeft: `${depth * 14 + 6}px` }}
|
|
148
|
+
>
|
|
149
|
+
<button
|
|
150
|
+
className="widget-tree-outline__button"
|
|
151
|
+
data-testid={`screen-spec-outline-${key}`}
|
|
152
|
+
type="button"
|
|
153
|
+
onClick={() => setSelectedPath(entry.path)}
|
|
154
|
+
>
|
|
155
|
+
<span className="widget-tree-outline__type">{entry.label}</span>
|
|
156
|
+
</button>
|
|
157
|
+
</li>
|
|
158
|
+
);
|
|
159
|
+
})}
|
|
160
|
+
</ul>
|
|
161
|
+
</PanelBody>
|
|
162
|
+
</Panel>
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
return (
|
|
166
|
+
<div className={cx('jdw-screen-spec-editor', className)} data-testid="screen-spec-editor">
|
|
167
|
+
<ResizablePanels
|
|
168
|
+
className="jdw-screen-spec-editor__layout"
|
|
169
|
+
defaultFirstSize={280}
|
|
170
|
+
minFirstSize={200}
|
|
171
|
+
minSecondSize={220}
|
|
172
|
+
maxFirstSize={420}
|
|
173
|
+
first={
|
|
174
|
+
<div className="jdw-screen-spec-editor__sidebar">
|
|
175
|
+
{metadataPanel}
|
|
176
|
+
{outlinePanel}
|
|
177
|
+
</div>
|
|
178
|
+
}
|
|
179
|
+
second={
|
|
180
|
+
<WorkbenchPropertySection
|
|
181
|
+
aria-label="Selected node properties"
|
|
182
|
+
className="jdw-screen-spec-editor__inspector"
|
|
183
|
+
data-testid="screen-spec-inspector"
|
|
184
|
+
title={selectedEntry?.label ?? 'Node'}
|
|
185
|
+
>
|
|
186
|
+
<ScreenNodeInspector
|
|
187
|
+
node={selectedNode}
|
|
188
|
+
parentKind={selectedEntry?.parentKind}
|
|
189
|
+
onChange={updateNode}
|
|
190
|
+
/>
|
|
191
|
+
</WorkbenchPropertySection>
|
|
192
|
+
}
|
|
193
|
+
/>
|
|
194
|
+
</div>
|
|
195
|
+
);
|
|
196
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export { JdwSampleScreenExplorer } from './JdwSampleScreenExplorer.js';
|
|
2
|
+
export type {
|
|
3
|
+
JdwSampleScreenExplorerProps,
|
|
4
|
+
JdwSampleSourceView,
|
|
5
|
+
} from './JdwSampleScreenExplorer.js';
|
|
6
|
+
export { ScreenNodeInspector } from './ScreenNodeInspector.js';
|
|
7
|
+
export type { ScreenNodeInspectorProps } from './ScreenNodeInspector.js';
|
|
8
|
+
export { ScreenSpecEditor } from './ScreenSpecEditor.js';
|
|
9
|
+
export type { ScreenSpecEditorProps } from './ScreenSpecEditor.js';
|
|
10
|
+
export { useScreenSpecPipeline } from './useScreenSpecPipeline.js';
|
|
11
|
+
export type {
|
|
12
|
+
ScreenSpecPipelineState,
|
|
13
|
+
UseScreenSpecPipelineResult,
|
|
14
|
+
} from './useScreenSpecPipeline.js';
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { useCallback, useMemo, 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
|
+
export function useScreenSpecPipeline(initialSpec: JdwScreenSpec): UseScreenSpecPipelineResult {
|
|
42
|
+
const initial = useMemo(() => {
|
|
43
|
+
const compiled = compileSpec(initialSpec);
|
|
44
|
+
return {
|
|
45
|
+
spec: initialSpec,
|
|
46
|
+
json: compiled.json,
|
|
47
|
+
compileError: compiled.error,
|
|
48
|
+
layoutConstraints: layoutConstraintsFromSpec(initialSpec),
|
|
49
|
+
};
|
|
50
|
+
}, [initialSpec]);
|
|
51
|
+
|
|
52
|
+
const [spec, setSpecState] = useState(initial.spec);
|
|
53
|
+
const [json, setJsonState] = useState(initial.json);
|
|
54
|
+
const [compileError, setCompileError] = useState<string | null>(initial.compileError);
|
|
55
|
+
const [layoutConstraints, setLayoutConstraints] = useState(initial.layoutConstraints);
|
|
56
|
+
|
|
57
|
+
const setSpec = useCallback((nextSpec: JdwScreenSpec) => {
|
|
58
|
+
const compiled = compileSpec(nextSpec);
|
|
59
|
+
setSpecState(nextSpec);
|
|
60
|
+
setLayoutConstraints(layoutConstraintsFromSpec(nextSpec));
|
|
61
|
+
setCompileError(compiled.error);
|
|
62
|
+
if (compiled.error === null) {
|
|
63
|
+
setJsonState(compiled.json);
|
|
64
|
+
}
|
|
65
|
+
}, []);
|
|
66
|
+
|
|
67
|
+
const resetSpec = useCallback(
|
|
68
|
+
(nextSpec: JdwScreenSpec) => {
|
|
69
|
+
setSpec(nextSpec);
|
|
70
|
+
},
|
|
71
|
+
[setSpec],
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
const setJson = useCallback((nextJson: string) => {
|
|
75
|
+
setJsonState(nextJson);
|
|
76
|
+
setCompileError(null);
|
|
77
|
+
}, []);
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
spec,
|
|
81
|
+
json,
|
|
82
|
+
compileError,
|
|
83
|
+
layoutConstraints,
|
|
84
|
+
setSpec,
|
|
85
|
+
setJson,
|
|
86
|
+
resetSpec,
|
|
87
|
+
};
|
|
88
|
+
}
|