@workbench-kit/react 0.0.1-prototype.0 → 0.0.2-prototype.0.1.2

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.
Files changed (155) hide show
  1. package/package.json +33 -9
  2. package/src/index.ts +545 -43
  3. package/src/jdw/JdwPreview.tsx +38 -0
  4. package/src/jdw/JsonCodeEditorPane.tsx +274 -0
  5. package/src/jdw/builtins/renderBuiltinWidgetLeaf.tsx +29 -0
  6. package/src/jdw/builtins/renderBuiltinWidgetNode.tsx +126 -0
  7. package/src/jdw/createBuiltinJdwRegistry.ts +111 -0
  8. package/src/jdw/cssRenderBackend.tsx +134 -0
  9. package/src/jdw/document.ts +38 -0
  10. package/src/jdw/fixtures/jdw-fixtures.ts +52 -0
  11. package/src/jdw/fixtures/jdw-sample-screens.ts +522 -0
  12. package/src/jdw/index.ts +45 -0
  13. package/src/jdw/parse.ts +1 -0
  14. package/src/jdw/renderJdw.tsx +32 -0
  15. package/src/json-config/JsonConfigWorkbench.tsx +316 -0
  16. package/src/json-config/index.ts +6 -0
  17. package/src/json-config/json-config-editor-state.ts +42 -0
  18. package/src/layout/Panel.tsx +146 -1
  19. package/src/layout/SideBarViewFrame.tsx +367 -328
  20. package/src/layout/WorkbenchCanvas.tsx +849 -0
  21. package/src/layout/WorkbenchFullscreen.tsx +368 -0
  22. package/src/layout/WorkbenchLayout.tsx +5 -0
  23. package/src/layout/WorkbenchLayoutBase.tsx +294 -0
  24. package/src/layout/WorkbenchPropertyPanel.tsx +497 -0
  25. package/src/layout/WorkbenchSidebarActions.tsx +274 -0
  26. package/src/layout/WorkbenchTree.tsx +351 -0
  27. package/src/layout/layoutHelpers.ts +25 -0
  28. package/src/modal/ConfirmDialog.tsx +61 -53
  29. package/src/modal/Modal.tsx +295 -35
  30. package/src/overlay/ContextMenu.tsx +146 -145
  31. package/src/primitives/AbsoluteBox.tsx +46 -0
  32. package/src/primitives/Badge.tsx +12 -12
  33. package/src/primitives/Button.tsx +44 -14
  34. package/src/primitives/Checkbox.tsx +11 -5
  35. package/src/primitives/EmptyState.tsx +27 -26
  36. package/src/primitives/Field.tsx +38 -38
  37. package/src/primitives/IconButton.tsx +5 -2
  38. package/src/primitives/List.tsx +111 -0
  39. package/src/primitives/NumberInput.tsx +38 -0
  40. package/src/primitives/ScrollArea.tsx +42 -0
  41. package/src/primitives/Select.tsx +1 -12
  42. package/src/primitives/StatusBar.tsx +46 -0
  43. package/src/primitives/TextArea.tsx +36 -0
  44. package/src/primitives/TextInput.tsx +11 -2
  45. package/src/primitives/WorkbenchChrome.tsx +424 -0
  46. package/src/primitives/WorkbenchEditor.tsx +394 -0
  47. package/src/primitives/index.ts +105 -0
  48. package/src/primitives/select/Select.tsx +266 -0
  49. package/src/primitives/select/index.ts +2 -0
  50. package/src/primitives/select/options.ts +38 -0
  51. package/src/primitives/select/overlay.ts +63 -0
  52. package/src/primitives/select/select.app.css +9 -0
  53. package/src/primitives/select/select.css +140 -0
  54. package/src/primitives/select/types.ts +18 -0
  55. package/src/primitives/styles.css +955 -0
  56. package/src/scrollbars.css +69 -0
  57. package/src/styles.css +7738 -1762
  58. package/src/utils/codicon.ts +17 -0
  59. package/src/widget-asset/WidgetAssetEditor.tsx +140 -0
  60. package/src/widget-asset/WidgetAssetMetadataForm.tsx +82 -0
  61. package/src/widget-asset/WidgetAssetModeControls.tsx +42 -0
  62. package/src/widget-asset/WidgetAssetSourceEditor.tsx +50 -0
  63. package/src/widget-asset/WidgetAssetWorkbench.tsx +112 -0
  64. package/src/widget-asset/index.ts +17 -0
  65. package/src/widget-asset/widget-asset-document.ts +37 -0
  66. package/src/widget-asset/widget-asset-mode.ts +3 -0
  67. package/src/widget-studio/builtin-widget-asset-catalog.ts +12 -0
  68. package/src/widget-studio/create-widget-studio-workspace-editor.tsx +79 -0
  69. package/src/widget-studio/index.ts +7 -0
  70. package/src/widget-studio/resolve-widget-studio-asset-catalog.ts +26 -0
  71. package/src/widget-tree/WidgetAssetPalette.tsx +77 -0
  72. package/src/widget-tree/WidgetInspectorPanel.tsx +266 -0
  73. package/src/widget-tree/WidgetSourceEditor.tsx +56 -0
  74. package/src/widget-tree/WidgetTreeLab.tsx +272 -0
  75. package/src/widget-tree/WidgetTreeModeControls.tsx +44 -0
  76. package/src/widget-tree/WidgetTreeSidePanel.tsx +49 -0
  77. package/src/widget-tree/WidgetTreeView.tsx +97 -0
  78. package/src/widget-tree/WidgetTreeWorkbench.tsx +111 -0
  79. package/src/widget-tree/WidgetTreeWorkspaceShell.tsx +154 -0
  80. package/src/widget-tree/demo-registry.ts +32 -0
  81. package/src/widget-tree/demo-widget-assets.ts +4 -0
  82. package/src/widget-tree/index.ts +27 -0
  83. package/src/widget-tree/widget-tree-document.ts +20 -0
  84. package/src/widget-tree/widget-tree-layout.ts +31 -0
  85. package/src/widget-tree/widget-tree-mode.ts +7 -0
  86. package/src/widget-tree/widget-tree-play-helpers.ts +74 -0
  87. package/src/workbench/ActivityBar.tsx +52 -52
  88. package/src/workbench/ArtifactShell.tsx +397 -0
  89. package/src/workbench/CommandPalette.tsx +819 -0
  90. package/src/workbench/ConfirmationFlow.tsx +245 -0
  91. package/src/workbench/MarkdownPreview.tsx +151 -0
  92. package/src/workbench/ShortcutCommandBridge.tsx +368 -0
  93. package/src/workbench/SplitView.tsx +136 -136
  94. package/src/workbench/StatusBar.tsx +178 -123
  95. package/src/workbench/StructuredArtifactEditor.tsx +205 -0
  96. package/src/workbench/Timeline.tsx +302 -0
  97. package/src/workbench/WorkbenchCanvasShell.tsx +941 -0
  98. package/src/workbench/WorkbenchShell.tsx +74 -70
  99. package/src/workbench/WorkbenchStandaloneShell.tsx +318 -291
  100. package/src/workbench/auth/WorkbenchAuthGate.tsx +61 -0
  101. package/src/workbench/auth/WorkbenchLoginView.tsx +635 -0
  102. package/src/workbench/auth/index.ts +19 -0
  103. package/src/workbench/chat/ChatComposer.tsx +165 -148
  104. package/src/workbench/chat/ChatMessageItem.tsx +10 -1
  105. package/src/workbench/chat/index.ts +9 -0
  106. package/src/workbench/chat/slashCommand.ts +28 -0
  107. package/src/workbench/chat/useChatRuntimeState.ts +76 -0
  108. package/src/workbench/chat/useSlashCommandSuggest.ts +101 -0
  109. package/src/workbench/commands.ts +520 -490
  110. package/src/workbench/index.ts +443 -98
  111. package/src/workbench/keyboard.ts +53 -0
  112. package/src/workbench/schema/index.ts +48 -0
  113. package/src/workbench/schema/workbenchDocument.ts +31 -0
  114. package/src/workbench/schema/workbenchDocumentActions.ts +6 -0
  115. package/src/workbench/schema/workbenchDocumentAdapter.ts +9 -0
  116. package/src/workbench/schema/workbenchDocumentPatch.ts +13 -0
  117. package/src/workbench/schema/workbenchDocumentRenderer.tsx +702 -0
  118. package/src/workbench/settings/NavigationPanel.tsx +47 -0
  119. package/src/workbench/settings/SchemaForm.tsx +420 -0
  120. package/src/workbench/settings/SectionedPanel.tsx +415 -0
  121. package/src/workbench/settings/StructuredDataForm.tsx +539 -0
  122. package/src/workbench/settings/StructuredDataSchemaPanel.tsx +559 -0
  123. package/src/workbench/settings/StructuredDataSchemaPanelEmbed.tsx +21 -0
  124. package/src/workbench/settings/StructuredDataSchemaPanelFrame.tsx +76 -0
  125. package/src/workbench/settings/StructuredDataTableView.tsx +127 -0
  126. package/src/workbench/settings/WorkbenchPanelRegion.tsx +46 -0
  127. package/src/workbench/settings/WorkbenchSettingsModal.tsx +21 -12
  128. package/src/workbench/settings/WorkbenchSettingsNav.tsx +38 -22
  129. package/src/workbench/settings/index.ts +135 -0
  130. package/src/workbench/settings/panel-region.css +21 -0
  131. package/src/workbench/settings/sectionedPanelScrollSpy.ts +143 -0
  132. package/src/workbench/settings/structured-data-schema-panel.css +330 -0
  133. package/src/workbench/settings/structured-data-table-view.css +85 -0
  134. package/src/workbench/settings/structuredDataSchema.ts +886 -0
  135. package/src/workbench/shell.ts +15 -0
  136. package/src/workbench/shellState.ts +203 -203
  137. package/src/workbench/standalone.ts +101 -94
  138. package/src/workbench/status.ts +129 -0
  139. package/src/workbench/theme.ts +44 -0
  140. package/src/workbench/workspace/MultiProviderExplorer.tsx +625 -0
  141. package/src/workbench/workspace/WorkspaceDraftsContext.tsx +111 -0
  142. package/src/workbench/workspace/WorkspaceEditor.tsx +47 -12
  143. package/src/workbench/workspace/WorkspaceEditorPanel.tsx +334 -353
  144. package/src/workbench/workspace/WorkspaceExplorer.tsx +532 -524
  145. package/src/workbench/workspace/WorkspaceFileIcon.tsx +32 -2
  146. package/src/workbench/workspace/WorkspaceSearchPanel.tsx +113 -113
  147. package/src/workbench/workspace/WorkspaceSearchResults.tsx +47 -47
  148. package/src/workbench/workspace/index.ts +156 -98
  149. package/src/workbench/workspace/mimeType.ts +45 -0
  150. package/src/workbench/workspace/path.ts +10 -10
  151. package/src/workbench/workspace/search.ts +6 -6
  152. package/src/workbench/workspace/tree.ts +1 -1
  153. package/src/workbench/workspace/types.ts +10 -10
  154. package/src/workbench/workspace/useVirtualWorkspace.ts +111 -98
  155. package/src/workbench/workspace/workspaceJsonDiagnostics.ts +83 -0
@@ -0,0 +1,941 @@
1
+ import { type CSSProperties, type ReactElement, useEffect, useMemo, useState } from 'react';
2
+ import { createCommandRegistryFromContributions } from '@workbench-kit/platform';
3
+ import { integratedShellWorkspaceFiles } from '@workbench-kit/adapters';
4
+ import { Button } from '../primitives/Button';
5
+ import { TextInput } from '../primitives/TextInput';
6
+ import { SideBarViewFrame } from '../layout/SideBarViewFrame';
7
+ import { createWorkbenchShellCommands } from './commands';
8
+ import { WorkbenchStandaloneShell } from './WorkbenchStandaloneShell';
9
+ import type { WorkbenchActivityDescriptor, WorkbenchStandaloneBootstrap } from './standalone';
10
+ import {
11
+ WorkbenchDocumentRenderer,
12
+ createPatchFromWorkbenchDocumentAction,
13
+ initializeWorkbenchDocumentPatchHistory,
14
+ isWorkbenchDocumentSupported,
15
+ type WorkbenchDocument,
16
+ type WorkbenchDocumentNode,
17
+ type WorkbenchDocumentPatch,
18
+ type WorkbenchDocumentPatchHistory,
19
+ type WorkbenchDocumentPatchHistoryState,
20
+ type WorkbenchDocumentPatchResult,
21
+ } from './schema';
22
+
23
+ const DEFAULT_CANVAS_ACTIVITY_ID = 'design';
24
+ const DEFAULT_CANVAS_THEME = 'dark';
25
+
26
+ type StudioShellTab = 'source' | 'preview' | 'window' | 'layout' | 'tile' | 'layer';
27
+
28
+ type StudioShellActivityId = 'design';
29
+
30
+ type WorkbenchPatchApplyResult = {
31
+ state: WorkbenchDocumentPatchHistoryState;
32
+ result: WorkbenchDocumentPatchResult;
33
+ };
34
+
35
+ const placeholderImageSrc = `data:image/svg+xml,${encodeURIComponent(
36
+ '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180"><defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1"><stop offset="0%" stop-color="#0f172a"/><stop offset="100%" stop-color="#334155"/></linearGradient></defs><rect width="320" height="180" fill="url(%23g)"/><text x="50%" y="52%" fill="white" font-size="24" text-anchor="middle" font-family="Arial">Image Layer</text></svg>',
37
+ )}`;
38
+
39
+ const defaultDocument: WorkbenchDocument = {
40
+ version: '1.0.0',
41
+ schemaVersion: 1,
42
+ pages: [
43
+ {
44
+ id: 'page-shell-canvas',
45
+ name: 'Design',
46
+ width: 1280,
47
+ height: 760,
48
+ background: '#1f2937',
49
+ nodes: [
50
+ {
51
+ id: 'shell-frame',
52
+ type: 'frame',
53
+ name: 'Window',
54
+ layout: { x: 80, y: 60, width: 980, height: 560 },
55
+ style: {
56
+ backgroundColor: '#0f172a',
57
+ borderColor: '#475569',
58
+ borderWidth: 1,
59
+ borderRadius: 12,
60
+ boxShadow: '0 12px 30px rgba(0, 0, 0, 0.45)',
61
+ },
62
+ children: ['tile-1', 'tile-2', 'tile-3'],
63
+ },
64
+ {
65
+ id: 'tile-1',
66
+ type: 'frame',
67
+ name: 'Tile: Header',
68
+ parentId: 'shell-frame',
69
+ layout: { x: 22, y: 18, width: 300, height: 180 },
70
+ style: {
71
+ backgroundColor: '#0ea5e9',
72
+ borderRadius: 10,
73
+ borderColor: '#0284c7',
74
+ borderWidth: 1,
75
+ },
76
+ children: ['tile-1-image', 'tile-1-text'],
77
+ },
78
+ {
79
+ id: 'tile-1-image',
80
+ type: 'image',
81
+ name: 'Image Layer',
82
+ parentId: 'tile-1',
83
+ layout: { x: 12, y: 12, width: 126, height: 90 },
84
+ src: placeholderImageSrc,
85
+ style: {
86
+ borderRadius: 8,
87
+ borderColor: '#0c4a6e',
88
+ borderWidth: 1,
89
+ },
90
+ },
91
+ {
92
+ id: 'tile-1-text',
93
+ type: 'text',
94
+ name: 'Text Layer',
95
+ parentId: 'tile-1',
96
+ layout: { x: 146, y: 36, width: 140, height: 80 },
97
+ content: 'Tile title',
98
+ style: {
99
+ color: '#f8fafc',
100
+ fontSize: 20,
101
+ fontFamily: 'Inter, "Segoe UI", sans-serif',
102
+ fontWeight: 700,
103
+ },
104
+ },
105
+ {
106
+ id: 'tile-2',
107
+ type: 'rectangle',
108
+ name: 'Action Tile',
109
+ parentId: 'shell-frame',
110
+ layout: { x: 336, y: 18, width: 280, height: 180 },
111
+ style: {
112
+ backgroundColor: '#7c3aed',
113
+ borderRadius: 10,
114
+ borderColor: '#6d28d9',
115
+ borderWidth: 1,
116
+ boxShadow: '0 8px 20px rgba(88, 28, 135, 0.45)',
117
+ },
118
+ },
119
+ {
120
+ id: 'tile-3',
121
+ type: 'rectangle',
122
+ name: 'Action Tile 2',
123
+ parentId: 'shell-frame',
124
+ layout: { x: 632, y: 18, width: 280, height: 180 },
125
+ style: {
126
+ backgroundColor: '#f59e0b',
127
+ borderRadius: 10,
128
+ borderColor: '#d97706',
129
+ borderWidth: 1,
130
+ },
131
+ },
132
+ ],
133
+ },
134
+ ],
135
+ };
136
+
137
+ export interface WorkbenchCanvasShellProps {
138
+ initialDocument?: WorkbenchDocument;
139
+ initialTheme?: 'light' | 'dark' | (string & {});
140
+ rootClassName?: string;
141
+ rootStyle?: CSSProperties;
142
+ compactStatus?: boolean;
143
+ onDocumentChange?: (document: WorkbenchDocument) => void;
144
+ }
145
+
146
+ export function WorkbenchCanvasShell({
147
+ initialDocument = defaultDocument,
148
+ initialTheme = DEFAULT_CANVAS_THEME,
149
+ rootClassName,
150
+ rootStyle,
151
+ compactStatus = true,
152
+ onDocumentChange,
153
+ }: WorkbenchCanvasShellProps) {
154
+ const activities = useMemo<Array<WorkbenchActivityDescriptor<StudioShellActivityId>>>(
155
+ () => [
156
+ {
157
+ id: DEFAULT_CANVAS_ACTIVITY_ID,
158
+ label: 'Design',
159
+ icon: 'codicon-layout',
160
+ },
161
+ ],
162
+ [],
163
+ );
164
+
165
+ const commandRegistry = useMemo(
166
+ () =>
167
+ createCommandRegistryFromContributions([
168
+ {
169
+ commands: createWorkbenchShellCommands<StudioShellActivityId>({
170
+ activities,
171
+ includeSettings: false,
172
+ }),
173
+ },
174
+ ]),
175
+ [activities],
176
+ );
177
+
178
+ const [history] = useState<WorkbenchDocumentPatchHistory>(() =>
179
+ initializeWorkbenchDocumentPatchHistory(initialDocument),
180
+ );
181
+ const [documentJson, setDocumentJson] = useState<WorkbenchDocument>(initialDocument);
182
+ const [selectedNodeId, setSelectedNodeId] = useState<string | undefined>(undefined);
183
+ const [statusText, setStatusText] = useState('ready');
184
+ const [nameInput, setNameInput] = useState('');
185
+ const [contentInput, setContentInput] = useState('');
186
+ const [srcInput, setSrcInput] = useState('');
187
+ const [bgColorInput, setBgColorInput] = useState('');
188
+ const [borderColorInput, setBorderColorInput] = useState('');
189
+ const [textColorInput, setTextColorInput] = useState('');
190
+ const [layoutXInput, setLayoutXInput] = useState('');
191
+ const [layoutYInput, setLayoutYInput] = useState('');
192
+ const [layoutWInput, setLayoutWInput] = useState('');
193
+ const [layoutHInput, setLayoutHInput] = useState('');
194
+ const [sourceText, setSourceText] = useState(() => JSON.stringify(initialDocument, null, 2));
195
+ const [activeTab, setActiveTab] = useState<StudioShellTab>('preview');
196
+
197
+ const activePageId = documentJson.pages?.[0]?.id ?? 'page-shell-canvas';
198
+ const activePage =
199
+ documentJson.pages.find(
200
+ (page: WorkbenchDocument['pages'][number]) => page.id === activePageId,
201
+ ) ?? documentJson.pages[0];
202
+ const selectedNode = activePage?.nodes.find(
203
+ (node: WorkbenchDocumentNode) => node.id === selectedNodeId,
204
+ );
205
+
206
+ const nodeLookup = useMemo(() => {
207
+ const map = new Map<string, WorkbenchDocumentNode>();
208
+ for (const node of activePage?.nodes ?? []) {
209
+ map.set(node.id, node);
210
+ }
211
+ return map;
212
+ }, [activePage?.nodes]);
213
+
214
+ const rootNodeIds = useMemo(() => {
215
+ const childIdSet = new Set(
216
+ activePage?.nodes.flatMap((node: WorkbenchDocumentNode) => {
217
+ const nodeChildren =
218
+ 'children' in node ? (node as { children?: readonly string[] }).children : undefined;
219
+ return nodeChildren ?? [];
220
+ }) ?? [],
221
+ );
222
+ return (
223
+ activePage?.nodes
224
+ .filter((node: WorkbenchDocumentNode) => !node.parentId && !childIdSet.has(node.id))
225
+ .map((node: WorkbenchDocumentNode) => node.id) ?? []
226
+ );
227
+ }, [activePage?.nodes]);
228
+
229
+ useEffect(() => {
230
+ setSourceText(JSON.stringify(documentJson, null, 2));
231
+ }, [documentJson]);
232
+
233
+ useEffect(() => {
234
+ setNameInput(selectedNode?.name ?? '');
235
+ setContentInput(
236
+ selectedNode?.type === 'text'
237
+ ? String((selectedNode as { content?: string }).content ?? '')
238
+ : '',
239
+ );
240
+ setSrcInput(
241
+ selectedNode?.type === 'image' ? String((selectedNode as { src?: string }).src ?? '') : '',
242
+ );
243
+ setBgColorInput(String(selectedNode?.style?.backgroundColor ?? ''));
244
+ setBorderColorInput(String(selectedNode?.style?.borderColor ?? ''));
245
+ setTextColorInput(String(selectedNode?.style?.color ?? ''));
246
+ setLayoutXInput(selectedNode?.layout?.x === undefined ? '' : String(selectedNode.layout.x));
247
+ setLayoutYInput(selectedNode?.layout?.y === undefined ? '' : String(selectedNode.layout.y));
248
+ setLayoutWInput(
249
+ selectedNode?.layout?.width === undefined ? '' : String(selectedNode.layout.width),
250
+ );
251
+ setLayoutHInput(
252
+ selectedNode?.layout?.height === undefined ? '' : String(selectedNode.layout.height),
253
+ );
254
+ }, [
255
+ selectedNode?.id,
256
+ selectedNode?.name,
257
+ selectedNode?.style?.backgroundColor,
258
+ selectedNode?.style?.borderColor,
259
+ selectedNode?.style?.color,
260
+ selectedNode?.layout?.x,
261
+ selectedNode?.layout?.y,
262
+ selectedNode?.layout?.width,
263
+ selectedNode?.layout?.height,
264
+ ]);
265
+
266
+ const applyAction = (action: Parameters<typeof createPatchFromWorkbenchDocumentAction>[0]) => {
267
+ if (!isWorkbenchDocumentSupported(documentJson)) {
268
+ setStatusText('unsupported schema');
269
+ return;
270
+ }
271
+
272
+ let patchResult: WorkbenchPatchApplyResult;
273
+
274
+ try {
275
+ const nextPatch = createPatchFromWorkbenchDocumentAction(action, documentJson);
276
+ patchResult = history.applyPatch(nextPatch.patch);
277
+ } catch (error) {
278
+ setStatusText(error instanceof Error ? error.message : 'patch failed');
279
+ return;
280
+ }
281
+
282
+ setDocumentJson(patchResult.state.present);
283
+ setStatusText(`patch ${patchResult.result.document.version || 'ok'}`);
284
+ onDocumentChange?.(patchResult.state.present);
285
+ };
286
+
287
+ const applyDocumentPatch = (patch: WorkbenchDocumentPatch) => {
288
+ try {
289
+ const patchResult = history.applyPatch(patch);
290
+ setDocumentJson(patchResult.state.present);
291
+ setStatusText(`source patch ${patchResult.state.present.version || 'applied'}`);
292
+ onDocumentChange?.(patchResult.state.present);
293
+ } catch (error) {
294
+ setStatusText(error instanceof Error ? error.message : 'source patch failed');
295
+ }
296
+ };
297
+
298
+ const applyMove = (
299
+ nodeId: string,
300
+ layout: WorkbenchDocument['pages'][number]['nodes'][number]['layout'],
301
+ ) => {
302
+ const targetNode = history.state.present.pages[0]?.nodes.find(
303
+ (node: WorkbenchDocumentNode) => node.id === nodeId,
304
+ );
305
+ const nextLayout = {
306
+ ...(targetNode?.layout ?? {}),
307
+ ...layout,
308
+ };
309
+
310
+ applyAction({
311
+ action: 'replace-layout',
312
+ pageId: activePageId,
313
+ nodeId,
314
+ layout: nextLayout,
315
+ });
316
+ };
317
+
318
+ const createNode = (type: 'text' | 'rectangle' | 'image') => {
319
+ const randomSeed = Math.floor(Math.random() * 10000);
320
+ const id = `${type}-${Date.now()}-${randomSeed}`;
321
+ const baseLayout = {
322
+ x: 110 + Math.random() * 680,
323
+ y: 220 + Math.random() * 220,
324
+ width: 220,
325
+ height: 120,
326
+ };
327
+
328
+ if (type === 'text') {
329
+ applyAction({
330
+ action: 'create',
331
+ pageId: activePageId,
332
+ node: {
333
+ id,
334
+ type: 'text',
335
+ name: `Text ${randomSeed}`,
336
+ layout: baseLayout,
337
+ style: {
338
+ fontSize: 14,
339
+ color: '#f8fafc',
340
+ fontFamily: 'Inter, "Segoe UI", sans-serif',
341
+ },
342
+ content: 'Text node',
343
+ },
344
+ });
345
+ return;
346
+ }
347
+
348
+ if (type === 'image') {
349
+ applyAction({
350
+ action: 'create',
351
+ pageId: activePageId,
352
+ node: {
353
+ id,
354
+ type: 'image',
355
+ name: `Image ${randomSeed}`,
356
+ layout: { ...baseLayout, height: 96 },
357
+ style: {
358
+ borderRadius: 6,
359
+ borderColor: '#94a3b8',
360
+ borderWidth: 1,
361
+ },
362
+ src: placeholderImageSrc,
363
+ },
364
+ });
365
+ return;
366
+ }
367
+
368
+ applyAction({
369
+ action: 'create',
370
+ pageId: activePageId,
371
+ node: {
372
+ id,
373
+ type: 'rectangle',
374
+ name: `Rectangle ${randomSeed}`,
375
+ layout: baseLayout,
376
+ style: {
377
+ backgroundColor: '#facc15',
378
+ borderColor: '#ca8a04',
379
+ borderWidth: 1,
380
+ borderRadius: 6,
381
+ },
382
+ },
383
+ });
384
+ };
385
+
386
+ const renameNode = (nextName: string) => {
387
+ if (!selectedNodeId || !selectedNode) {
388
+ setStatusText('select a node first');
389
+ return;
390
+ }
391
+
392
+ applyAction({
393
+ action: 'rename',
394
+ pageId: activePageId,
395
+ nodeId: selectedNodeId,
396
+ name: nextName || selectedNode.name,
397
+ });
398
+ };
399
+
400
+ const saveText = () => {
401
+ if (!selectedNodeId || !selectedNode || selectedNode.type !== 'text') {
402
+ setStatusText('selected node is not text');
403
+ return;
404
+ }
405
+
406
+ applyAction({
407
+ action: 'replace-content',
408
+ pageId: activePageId,
409
+ nodeId: selectedNodeId,
410
+ content: contentInput,
411
+ });
412
+ };
413
+
414
+ const saveImageSrc = () => {
415
+ if (!selectedNodeId || !selectedNode || selectedNode.type !== 'image') {
416
+ setStatusText('selected node is not image');
417
+ return;
418
+ }
419
+
420
+ applyAction({
421
+ action: 'replace',
422
+ pageId: activePageId,
423
+ nodeId: selectedNodeId,
424
+ node: {
425
+ ...selectedNode,
426
+ src: srcInput,
427
+ },
428
+ });
429
+ };
430
+
431
+ const saveStyle = () => {
432
+ if (!selectedNodeId || !selectedNode) {
433
+ setStatusText('select a node first');
434
+ return;
435
+ }
436
+
437
+ applyAction({
438
+ action: 'replace-style',
439
+ pageId: activePageId,
440
+ nodeId: selectedNodeId,
441
+ style: {
442
+ ...(selectedNode.style ?? {}),
443
+ ...(bgColorInput ? { backgroundColor: bgColorInput } : {}),
444
+ ...(borderColorInput ? { borderColor: borderColorInput } : {}),
445
+ ...(selectedNode.type === 'text' && textColorInput ? { color: textColorInput } : {}),
446
+ },
447
+ });
448
+ };
449
+
450
+ const saveLayout = () => {
451
+ if (!selectedNodeId || !selectedNode) {
452
+ setStatusText('select a node first');
453
+ return;
454
+ }
455
+ const parseNumber = (value: string) => {
456
+ const trimmed = value.trim();
457
+ const n = Number(trimmed);
458
+ if (trimmed === '' || !Number.isFinite(n)) {
459
+ return undefined;
460
+ }
461
+ return n;
462
+ };
463
+
464
+ const nextLayout = {
465
+ ...(selectedNode.layout ?? {}),
466
+ ...(parseNumber(layoutXInput) !== undefined ? { x: parseNumber(layoutXInput) } : {}),
467
+ ...(parseNumber(layoutYInput) !== undefined ? { y: parseNumber(layoutYInput) } : {}),
468
+ ...(parseNumber(layoutWInput) !== undefined ? { width: parseNumber(layoutWInput) } : {}),
469
+ ...(parseNumber(layoutHInput) !== undefined ? { height: parseNumber(layoutHInput) } : {}),
470
+ };
471
+
472
+ applyAction({
473
+ action: 'replace-layout',
474
+ pageId: activePageId,
475
+ nodeId: selectedNodeId,
476
+ layout: nextLayout,
477
+ });
478
+ };
479
+
480
+ const deleteSelected = () => {
481
+ if (!selectedNodeId) {
482
+ setStatusText('select a node first');
483
+ return;
484
+ }
485
+
486
+ applyAction({
487
+ action: 'delete',
488
+ pageId: activePageId,
489
+ nodeId: selectedNodeId,
490
+ });
491
+ setSelectedNodeId(undefined);
492
+ };
493
+
494
+ const undo = () => {
495
+ const nextState = history.undo();
496
+ if (!nextState) {
497
+ setStatusText('nothing to undo');
498
+ return;
499
+ }
500
+
501
+ setDocumentJson(nextState.present);
502
+ onDocumentChange?.(nextState.present);
503
+ setStatusText('undo');
504
+ };
505
+
506
+ const redo = () => {
507
+ const nextState = history.redo();
508
+ if (!nextState) {
509
+ setStatusText('nothing to redo');
510
+ return;
511
+ }
512
+
513
+ setDocumentJson(nextState.present);
514
+ onDocumentChange?.(nextState.present);
515
+ setStatusText('redo');
516
+ };
517
+
518
+ const applySourcePatch = () => {
519
+ let nextDocument: unknown;
520
+ try {
521
+ nextDocument = JSON.parse(sourceText);
522
+ } catch (error) {
523
+ setStatusText(error instanceof Error ? error.message : 'invalid JSON');
524
+ return;
525
+ }
526
+
527
+ if (!isWorkbenchDocumentSupported(nextDocument as WorkbenchDocument)) {
528
+ setStatusText('source patch requires schemaVersion 1');
529
+ return;
530
+ }
531
+
532
+ const patch: WorkbenchDocumentPatch = {
533
+ id: `source-${Date.now()}`,
534
+ schemaVersion: (nextDocument as WorkbenchDocument).schemaVersion,
535
+ timestamp: new Date().toISOString(),
536
+ ops: [
537
+ {
538
+ op: 'replace',
539
+ path: '',
540
+ value: nextDocument as WorkbenchDocument,
541
+ },
542
+ ],
543
+ };
544
+ applyDocumentPatch(patch);
545
+ };
546
+
547
+ const selectNode = (nodeId: string) => {
548
+ setSelectedNodeId(nodeId);
549
+ setActiveTab('layer');
550
+ };
551
+
552
+ const renderNodeRow = (nodeId: string, depth: number): Array<ReactElement | null> => {
553
+ const node = nodeLookup.get(nodeId);
554
+ if (!node) {
555
+ return [];
556
+ }
557
+
558
+ const children =
559
+ 'children' in node ? ((node as { children?: readonly string[] }).children ?? []) : [];
560
+ const isSelected = node.id === selectedNodeId;
561
+ const prefix = `${'\u00a0'.repeat(Math.max(depth - 1, 0) * 2)}${depth > 0 ? '↳ ' : ''}`;
562
+
563
+ const row = (
564
+ <button
565
+ key={node.id}
566
+ onClick={() => selectNode(node.id)}
567
+ style={{
568
+ width: '100%',
569
+ textAlign: 'left',
570
+ border: 'none',
571
+ color: isSelected ? '#ffffff' : 'var(--color-text-secondary)',
572
+ background: isSelected ? 'rgba(59, 130, 246, 0.35)' : 'transparent',
573
+ padding: '6px 8px',
574
+ fontSize: 12,
575
+ display: 'flex',
576
+ alignItems: 'center',
577
+ gap: 8,
578
+ fontWeight: isSelected ? 600 : 400,
579
+ cursor: 'pointer',
580
+ }}
581
+ >
582
+ <span style={{ width: 16, textAlign: 'center', opacity: 0.8 }}>
583
+ {node.type === 'frame'
584
+ ? '▣'
585
+ : node.type === 'text'
586
+ ? '✎'
587
+ : node.type === 'image'
588
+ ? '◻'
589
+ : '▭'}
590
+ </span>
591
+ <span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
592
+ {prefix}
593
+ {node.name}
594
+ </span>
595
+ </button>
596
+ );
597
+
598
+ return [row, ...children.flatMap((childId: string) => renderNodeRow(childId, depth + 1))];
599
+ };
600
+
601
+ const bootstrap = useMemo<WorkbenchStandaloneBootstrap<StudioShellActivityId>>(
602
+ () => ({
603
+ contract: {
604
+ activities,
605
+ commandRegistry,
606
+ statusSections: [],
607
+ initialActivityId: DEFAULT_CANVAS_ACTIVITY_ID,
608
+ initialTheme,
609
+ },
610
+ initialFiles: integratedShellWorkspaceFiles,
611
+ workspace: {
612
+ openFile: async () => undefined,
613
+ saveFile: async () => ({
614
+ file: { content: '', path: 'design.tsx' },
615
+ kind: 'save:success',
616
+ outcome: 'unchanged',
617
+ }),
618
+ deleteFiles: async () => undefined,
619
+ },
620
+ chat: {
621
+ onChatSubmit: async () => undefined,
622
+ onCancelChat: async () => undefined,
623
+ },
624
+ save: {},
625
+ patch: {
626
+ onPatchApply: async (patch) => ({
627
+ patch,
628
+ type: 'patch:applied',
629
+ }),
630
+ },
631
+ status: {},
632
+ }),
633
+ [activities, commandRegistry, initialTheme],
634
+ );
635
+
636
+ const tabItems: Array<{ id: StudioShellTab; label: string }> = [
637
+ { id: 'source', label: 'Source' },
638
+ { id: 'preview', label: 'Preview' },
639
+ { id: 'window', label: 'Window' },
640
+ { id: 'layout', label: 'Layout' },
641
+ { id: 'tile', label: 'Tile' },
642
+ { id: 'layer', label: 'Layer' },
643
+ ];
644
+
645
+ const sectionBlockStyle: CSSProperties = {
646
+ border: '1px solid rgba(148, 163, 184, 0.2)',
647
+ borderRadius: 8,
648
+ padding: 8,
649
+ marginBottom: 8,
650
+ display: 'grid',
651
+ gap: 8,
652
+ background: 'rgba(15, 23, 42, 0.25)',
653
+ };
654
+
655
+ return (
656
+ <WorkbenchStandaloneShell<StudioShellActivityId>
657
+ bootstrap={bootstrap}
658
+ compactStatus={compactStatus}
659
+ maxPrimarySidebarSizePercent={44}
660
+ minPrimarySidebarSizePercent={18}
661
+ getStatusSections={() => [
662
+ {
663
+ id: 'main',
664
+ items: [
665
+ { id: 'status', icon: <span className="workbench-status-dot" />, label: statusText },
666
+ { id: 'document', label: `nodes: ${activePage?.nodes.length ?? 0}` },
667
+ {
668
+ id: 'selected',
669
+ label: selectedNode ? `selected: ${selectedNode.name}` : 'selected: none',
670
+ },
671
+ ],
672
+ },
673
+ {
674
+ id: 'tab',
675
+ items: [{ id: 'tab', label: `tab: ${activeTab}` }],
676
+ },
677
+ ]}
678
+ renderPrimarySidebar={() => (
679
+ <SideBarViewFrame title="Launchpad Explorer">
680
+ <div style={{ padding: 8, display: 'grid', gap: 8 }}>
681
+ <div style={{ display: 'flex', gap: 8 }}>
682
+ <TextInput controlWidth="full" placeholder="Search layer" />
683
+ <Button onClick={() => createNode('text')} compact variant="primary">
684
+ +T
685
+ </Button>
686
+ </div>
687
+ <div style={{ display: 'flex', gap: 8 }}>
688
+ <Button onClick={() => createNode('rectangle')} compact>
689
+ +Rect
690
+ </Button>
691
+ <Button onClick={() => createNode('image')} compact>
692
+ +Img
693
+ </Button>
694
+ </div>
695
+ <div style={sectionBlockStyle}>
696
+ <div style={{ fontSize: 12, opacity: 0.9, fontWeight: 600 }}>Layers</div>
697
+ <div style={{ display: 'grid' }}>
698
+ {rootNodeIds.flatMap((nodeId: string) => renderNodeRow(nodeId, 0))}
699
+ </div>
700
+ </div>
701
+ <div style={sectionBlockStyle}>
702
+ <div style={{ fontSize: 12, opacity: 0.9, fontWeight: 600 }}>Editor</div>
703
+ <div style={{ display: 'grid', gap: 6 }}>
704
+ <Button
705
+ onClick={() => {
706
+ if (selectedNodeId) {
707
+ setActiveTab('layer');
708
+ }
709
+ }}
710
+ compact
711
+ disabled={!selectedNodeId}
712
+ >
713
+ Inspector Focus
714
+ </Button>
715
+ <Button onClick={undo} compact disabled={!history.canUndo}>
716
+ Undo
717
+ </Button>
718
+ <Button onClick={redo} compact disabled={!history.canRedo}>
719
+ Redo
720
+ </Button>
721
+ <Button
722
+ onClick={deleteSelected}
723
+ compact
724
+ disabled={!selectedNodeId}
725
+ variant="danger"
726
+ >
727
+ Delete selected
728
+ </Button>
729
+ </div>
730
+ </div>
731
+ </div>
732
+ </SideBarViewFrame>
733
+ )}
734
+ renderSecondaryArea={() => (
735
+ <div style={{ display: 'grid', gridTemplateRows: '34px 1fr', height: '100%' }}>
736
+ <div
737
+ style={{
738
+ height: 34,
739
+ borderBottom: '1px solid rgba(148, 163, 184, 0.2)',
740
+ display: 'flex',
741
+ gap: 2,
742
+ padding: '4px',
743
+ background: 'rgba(15, 23, 42, 0.24)',
744
+ }}
745
+ >
746
+ {tabItems.map((tab) => (
747
+ <Button
748
+ compact
749
+ key={tab.id}
750
+ variant={activeTab === tab.id ? 'primary' : 'default'}
751
+ onClick={() => setActiveTab(tab.id)}
752
+ style={{ lineHeight: 1 }}
753
+ >
754
+ {tab.label}
755
+ </Button>
756
+ ))}
757
+ </div>
758
+ <div style={{ display: 'flex', minHeight: 0, height: '100%', overflow: 'hidden' }}>
759
+ <main
760
+ style={{
761
+ flex: 1,
762
+ padding: 8,
763
+ minWidth: 0,
764
+ display: 'grid',
765
+ gridTemplateRows: '1fr',
766
+ position: 'relative',
767
+ }}
768
+ >
769
+ {activeTab === 'source' ? (
770
+ <div style={{ display: 'grid', gap: 8 }}>
771
+ <div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
772
+ <Button onClick={applySourcePatch} compact variant="primary">
773
+ Apply JSON
774
+ </Button>
775
+ <span style={{ fontSize: 12, opacity: 0.8 }}>Editing document source</span>
776
+ </div>
777
+ <textarea
778
+ value={sourceText}
779
+ onChange={(event) => setSourceText(event.currentTarget.value)}
780
+ style={{
781
+ width: '100%',
782
+ height: '100%',
783
+ resize: 'none',
784
+ background: '#020617',
785
+ color: '#f8fafc',
786
+ border: '1px solid rgba(148, 163, 184, 0.3)',
787
+ borderRadius: 8,
788
+ padding: 10,
789
+ fontFamily:
790
+ 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace',
791
+ fontSize: 12,
792
+ lineHeight: 1.35,
793
+ }}
794
+ />
795
+ </div>
796
+ ) : (
797
+ <WorkbenchDocumentRenderer
798
+ page={activePage ?? history.state.present.pages[0]}
799
+ selectedNodeId={selectedNodeId}
800
+ onNodeClick={(nodeId) => setSelectedNodeId(nodeId)}
801
+ onNodeMove={(nodeId, layout) => applyMove(nodeId, layout)}
802
+ style={{ height: '100%', width: '100%', background: '#1e293b' }}
803
+ />
804
+ )}
805
+ </main>
806
+ <aside
807
+ style={{
808
+ width: 340,
809
+ borderLeft: '1px solid rgba(148, 163, 184, 0.2)',
810
+ padding: 8,
811
+ display: 'grid',
812
+ gap: 8,
813
+ overflow: 'auto',
814
+ background: 'rgba(15, 23, 42, 0.4)',
815
+ }}
816
+ >
817
+ <div style={sectionBlockStyle}>
818
+ <div style={{ fontWeight: 600 }}>Window / Workspace</div>
819
+ <TextInput
820
+ aria-label="node-name"
821
+ controlWidth="full"
822
+ value={nameInput}
823
+ onChange={(event) => setNameInput(event.currentTarget.value)}
824
+ placeholder="name"
825
+ />
826
+ <Button onClick={() => renameNode(nameInput)} compact>
827
+ Rename
828
+ </Button>
829
+ </div>
830
+ <div style={sectionBlockStyle}>
831
+ <div style={{ fontWeight: 600 }}>Layout</div>
832
+ <div
833
+ style={{
834
+ display: 'grid',
835
+ gridTemplateColumns: '72px 1fr',
836
+ gap: 6,
837
+ alignItems: 'center',
838
+ }}
839
+ >
840
+ <label>X</label>
841
+ <TextInput
842
+ controlWidth="full"
843
+ value={layoutXInput}
844
+ onChange={(event) => setLayoutXInput(event.currentTarget.value)}
845
+ />
846
+ <label>Y</label>
847
+ <TextInput
848
+ controlWidth="full"
849
+ value={layoutYInput}
850
+ onChange={(event) => setLayoutYInput(event.currentTarget.value)}
851
+ />
852
+ <label>W</label>
853
+ <TextInput
854
+ controlWidth="full"
855
+ value={layoutWInput}
856
+ onChange={(event) => setLayoutWInput(event.currentTarget.value)}
857
+ />
858
+ <label>H</label>
859
+ <TextInput
860
+ controlWidth="full"
861
+ value={layoutHInput}
862
+ onChange={(event) => setLayoutHInput(event.currentTarget.value)}
863
+ />
864
+ </div>
865
+ <Button onClick={saveLayout} compact>
866
+ Apply layout
867
+ </Button>
868
+ </div>
869
+ <div style={sectionBlockStyle}>
870
+ <div style={{ fontWeight: 600 }}>Visual</div>
871
+ <TextInput
872
+ controlWidth="full"
873
+ value={bgColorInput}
874
+ onChange={(event) => setBgColorInput(event.currentTarget.value)}
875
+ placeholder="#hex"
876
+ aria-label="background-color"
877
+ />
878
+ <TextInput
879
+ controlWidth="full"
880
+ value={borderColorInput}
881
+ onChange={(event) => setBorderColorInput(event.currentTarget.value)}
882
+ placeholder="#hex"
883
+ aria-label="border-color"
884
+ />
885
+ <Button onClick={saveStyle} compact>
886
+ Apply visual
887
+ </Button>
888
+ </div>
889
+ {selectedNode?.type === 'text' ? (
890
+ <div style={sectionBlockStyle}>
891
+ <div style={{ fontWeight: 600 }}>Text Layer</div>
892
+ <TextInput
893
+ aria-label="node-content"
894
+ controlWidth="full"
895
+ value={contentInput}
896
+ onChange={(event) => setContentInput(event.currentTarget.value)}
897
+ placeholder="text"
898
+ />
899
+ <TextInput
900
+ aria-label="text-color"
901
+ controlWidth="full"
902
+ value={textColorInput}
903
+ onChange={(event) => setTextColorInput(event.currentTarget.value)}
904
+ placeholder="#hex"
905
+ />
906
+ <Button onClick={saveText} compact>
907
+ Update text
908
+ </Button>
909
+ </div>
910
+ ) : null}
911
+ {selectedNode?.type === 'image' ? (
912
+ <div style={sectionBlockStyle}>
913
+ <div style={{ fontWeight: 600 }}>Image Layer</div>
914
+ <TextInput
915
+ aria-label="node-src"
916
+ controlWidth="full"
917
+ value={srcInput}
918
+ onChange={(event) => setSrcInput(event.currentTarget.value)}
919
+ placeholder="image src"
920
+ />
921
+ <Button onClick={saveImageSrc} compact>
922
+ Update image src
923
+ </Button>
924
+ </div>
925
+ ) : null}
926
+ <div style={sectionBlockStyle}>
927
+ <div style={{ fontWeight: 600 }}>Canvas state</div>
928
+ <div style={{ fontSize: 12, opacity: 0.8 }}>
929
+ Tab: {activeTab} / History:{' '}
930
+ {history.state.past.length + history.state.future.length} entries
931
+ </div>
932
+ </div>
933
+ </aside>
934
+ </div>
935
+ </div>
936
+ )}
937
+ rootClassName={rootClassName}
938
+ rootStyle={rootStyle}
939
+ />
940
+ );
941
+ }