@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 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
+ }