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

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,77 @@
1
+ import type { WidgetPlacementAsset } from '@workbench-kit/contracts';
2
+ import type { GenericWidget } from '@workbench-kit/jdw';
3
+
4
+ import { WorkbenchPropertyHint } from '../layout/WorkbenchPropertyPanel';
5
+ import { cxCodicon } from '../utils/codicon';
6
+ import { cx } from '../utils/cx';
7
+ import { canAddChildren } from './widget-tree-layout.js';
8
+
9
+ const CATEGORY_LABELS: Record<string, string> = {
10
+ content: 'Content',
11
+ layout: 'Layout',
12
+ template: 'Templates',
13
+ };
14
+
15
+ export interface WidgetAssetPaletteProps {
16
+ readonly assetsByCategory: Readonly<Record<string, readonly WidgetPlacementAsset[]>>;
17
+ readonly selectedContainer: GenericWidget | null;
18
+ readonly readOnly?: boolean | undefined;
19
+ readonly onPlaceAsset: (asset: WidgetPlacementAsset) => void;
20
+ }
21
+
22
+ export function WidgetAssetPalette({
23
+ assetsByCategory,
24
+ onPlaceAsset,
25
+ readOnly = false,
26
+ selectedContainer,
27
+ }: WidgetAssetPaletteProps) {
28
+ const canPlace = canAddChildren(selectedContainer) && !readOnly;
29
+ const categories = Object.keys(assetsByCategory);
30
+
31
+ return (
32
+ <div className="widget-tree-asset-palette" data-testid="widget-tree-asset-palette">
33
+ {!canPlace ? (
34
+ <WorkbenchPropertyHint>
35
+ Select a container node in Outline to place an asset.
36
+ </WorkbenchPropertyHint>
37
+ ) : (
38
+ <WorkbenchPropertyHint>
39
+ Click an asset to add it to <strong>{selectedContainer?.type ?? 'container'}</strong>.
40
+ </WorkbenchPropertyHint>
41
+ )}
42
+
43
+ {categories.map((category) => (
44
+ <section key={category} className="widget-tree-asset-palette__section">
45
+ <h3 className="widget-tree-asset-palette__title">
46
+ {CATEGORY_LABELS[category] ?? category}
47
+ </h3>
48
+ <div className="widget-tree-asset-palette__grid">
49
+ {assetsByCategory[category]?.map((asset) => (
50
+ <button
51
+ key={asset.id}
52
+ className={cx(
53
+ 'widget-tree-asset-palette__card',
54
+ !canPlace && 'widget-tree-asset-palette__card--disabled',
55
+ )}
56
+ data-testid={`widget-asset-${asset.id}`}
57
+ disabled={!canPlace}
58
+ title={asset.description ?? asset.label}
59
+ type="button"
60
+ onClick={() => onPlaceAsset(asset)}
61
+ >
62
+ {asset.icon ? (
63
+ <i aria-hidden className={cxCodicon(asset.icon)} />
64
+ ) : (
65
+ <span className="widget-tree-asset-palette__fallback-icon" aria-hidden>
66
+ {asset.label.slice(0, 1).toUpperCase()}
67
+ </span>
68
+ )}
69
+ <span className="widget-tree-asset-palette__label">{asset.label}</span>
70
+ </button>
71
+ ))}
72
+ </div>
73
+ </section>
74
+ ))}
75
+ </div>
76
+ );
77
+ }
@@ -0,0 +1,266 @@
1
+ import type {
2
+ WidgetInspectorField,
3
+ WidgetInspectorSection,
4
+ WidgetRegistryContract,
5
+ } from '@workbench-kit/contracts';
6
+ import type { GenericWidget, WidgetPath } from '@workbench-kit/jdw';
7
+
8
+ import { Badge } from '../primitives/Badge';
9
+ import { Button } from '../primitives/Button';
10
+ import {
11
+ WorkbenchPropertyCheckboxRow,
12
+ WorkbenchPropertyColorRow,
13
+ WorkbenchPropertyHint,
14
+ WorkbenchPropertyNumberRow,
15
+ WorkbenchPropertyPanel,
16
+ WorkbenchPropertySection,
17
+ WorkbenchPropertySelectRow,
18
+ WorkbenchPropertyStack,
19
+ WorkbenchPropertyTextRow,
20
+ } from '../layout/WorkbenchPropertyPanel';
21
+
22
+ export interface WidgetInspectorPanelProps {
23
+ readonly widget: GenericWidget | null;
24
+ readonly path: WidgetPath | null;
25
+ readonly parentWidget?: GenericWidget | null | undefined;
26
+ readonly widgetRegistry?: WidgetRegistryContract<unknown> | undefined;
27
+ readonly readOnly?: boolean | undefined;
28
+ readonly onPatch?: ((next: GenericWidget) => void) | undefined;
29
+ readonly onRemove?: (() => void) | undefined;
30
+ }
31
+
32
+ export function WidgetInspectorPanel({
33
+ widget,
34
+ path,
35
+ parentWidget = null,
36
+ widgetRegistry,
37
+ readOnly = false,
38
+ onPatch,
39
+ onRemove,
40
+ }: WidgetInspectorPanelProps) {
41
+ const inspector = widget ? widgetRegistry?.definition(widget.type)?.inspector : undefined;
42
+ const parentType = parentWidget?.type;
43
+ const showPlacement =
44
+ widget &&
45
+ parentType &&
46
+ (parentType === 'grid' || parentType === 'row' || parentType === 'column');
47
+
48
+ const updateProp = (prop: string, value: unknown) => {
49
+ if (!widget || !path || !onPatch || readOnly) return;
50
+ onPatch({ ...widget, [prop]: value });
51
+ };
52
+
53
+ return (
54
+ <WorkbenchPropertyPanel
55
+ className="widget-tree-inspector"
56
+ data-testid="widget-tree-inspector-panel"
57
+ >
58
+ {!widget ? (
59
+ <WorkbenchPropertyHint>Select a node in the outline.</WorkbenchPropertyHint>
60
+ ) : (
61
+ <WorkbenchPropertyStack>
62
+ <div className="widget-tree-inspector__header">
63
+ <Badge>{widgetRegistry?.definition(widget.type)?.displayName ?? widget.type}</Badge>
64
+ {typeof widget.id === 'string' && widget.id.trim().length > 0 ? (
65
+ <WorkbenchPropertyHint title={widget.id}>{widget.id}</WorkbenchPropertyHint>
66
+ ) : null}
67
+ </div>
68
+
69
+ {showPlacement ? (
70
+ <PlacementSection
71
+ parentType={parentType}
72
+ readOnly={readOnly}
73
+ values={widget}
74
+ onValueChange={updateProp}
75
+ />
76
+ ) : null}
77
+
78
+ {inspector && inspector.length > 0 ? (
79
+ inspector.map((section) => (
80
+ <InspectorSection
81
+ key={section.title}
82
+ readOnly={readOnly}
83
+ section={section}
84
+ values={widget}
85
+ onValueChange={updateProp}
86
+ />
87
+ ))
88
+ ) : !showPlacement ? (
89
+ <WorkbenchPropertyHint>
90
+ No property fields registered for this widget type.
91
+ </WorkbenchPropertyHint>
92
+ ) : null}
93
+
94
+ {path && path.length > 0 && onRemove ? (
95
+ <div className="widget-tree-inspector__danger">
96
+ <Button disabled={readOnly} variant="danger" onClick={onRemove}>
97
+ Remove node
98
+ </Button>
99
+ </div>
100
+ ) : null}
101
+ </WorkbenchPropertyStack>
102
+ )}
103
+ </WorkbenchPropertyPanel>
104
+ );
105
+ }
106
+
107
+ function PlacementSection({
108
+ onValueChange,
109
+ parentType,
110
+ readOnly,
111
+ values,
112
+ }: {
113
+ onValueChange: (prop: string, value: unknown) => void;
114
+ parentType: string;
115
+ readOnly: boolean;
116
+ values: GenericWidget;
117
+ }) {
118
+ if (parentType === 'grid') {
119
+ return (
120
+ <WorkbenchPropertySection title="Grid placement">
121
+ <WorkbenchPropertyNumberRow
122
+ label="Column"
123
+ disabled={readOnly}
124
+ min={0}
125
+ value={typeof values.col === 'number' ? values.col : undefined}
126
+ onValueChange={(next) => onValueChange('col', next)}
127
+ />
128
+ <WorkbenchPropertyNumberRow
129
+ label="Row"
130
+ disabled={readOnly}
131
+ min={0}
132
+ value={typeof values.row === 'number' ? values.row : undefined}
133
+ onValueChange={(next) => onValueChange('row', next)}
134
+ />
135
+ <WorkbenchPropertyNumberRow
136
+ label="Column span"
137
+ disabled={readOnly}
138
+ min={1}
139
+ value={typeof values.colSpan === 'number' ? values.colSpan : undefined}
140
+ onValueChange={(next) => onValueChange('colSpan', next)}
141
+ />
142
+ <WorkbenchPropertyNumberRow
143
+ label="Row span"
144
+ disabled={readOnly}
145
+ min={1}
146
+ value={typeof values.rowSpan === 'number' ? values.rowSpan : undefined}
147
+ onValueChange={(next) => onValueChange('rowSpan', next)}
148
+ />
149
+ </WorkbenchPropertySection>
150
+ );
151
+ }
152
+
153
+ return (
154
+ <WorkbenchPropertySection title="Flex placement">
155
+ <WorkbenchPropertyNumberRow
156
+ label="Flex"
157
+ disabled={readOnly}
158
+ min={0}
159
+ step={0.1}
160
+ value={typeof values.flex === 'number' ? values.flex : undefined}
161
+ onValueChange={(next) => onValueChange('flex', next)}
162
+ />
163
+ <WorkbenchPropertySelectRow
164
+ label="Align"
165
+ disabled={readOnly}
166
+ options={[
167
+ { label: 'Auto', value: '' },
168
+ { label: 'Start', value: 'flex-start' },
169
+ { label: 'Center', value: 'center' },
170
+ { label: 'End', value: 'flex-end' },
171
+ { label: 'Stretch', value: 'stretch' },
172
+ ]}
173
+ value={typeof values.align === 'string' ? values.align : ''}
174
+ onValueChange={(next) => onValueChange('align', next.length > 0 ? next : undefined)}
175
+ />
176
+ </WorkbenchPropertySection>
177
+ );
178
+ }
179
+
180
+ function InspectorSection({
181
+ onValueChange,
182
+ readOnly,
183
+ section,
184
+ values,
185
+ }: {
186
+ onValueChange: (prop: string, value: unknown) => void;
187
+ readOnly: boolean;
188
+ section: WidgetInspectorSection;
189
+ values: GenericWidget;
190
+ }) {
191
+ return (
192
+ <WorkbenchPropertySection title={section.title}>
193
+ {section.fields.map((field) => (
194
+ <InspectorField
195
+ key={field.prop}
196
+ field={field}
197
+ readOnly={readOnly}
198
+ value={values[field.prop]}
199
+ onValueChange={(prop, next) => onValueChange(prop, next)}
200
+ />
201
+ ))}
202
+ </WorkbenchPropertySection>
203
+ );
204
+ }
205
+
206
+ function InspectorField({
207
+ field,
208
+ onValueChange,
209
+ readOnly,
210
+ value,
211
+ }: {
212
+ field: WidgetInspectorField;
213
+ onValueChange: (prop: string, value: unknown) => void;
214
+ readOnly: boolean;
215
+ value: unknown;
216
+ }) {
217
+ if (field.kind === 'text' || field.kind === 'color') {
218
+ const Row = field.kind === 'color' ? WorkbenchPropertyColorRow : WorkbenchPropertyTextRow;
219
+ return (
220
+ <Row
221
+ label={field.label}
222
+ readOnly={readOnly}
223
+ value={typeof value === 'string' ? value : ''}
224
+ {...(field.kind === 'text' && field.placeholder
225
+ ? { placeholder: field.placeholder }
226
+ : null)}
227
+ onValueChange={(next) => onValueChange(field.prop, next)}
228
+ />
229
+ );
230
+ }
231
+
232
+ if (field.kind === 'number') {
233
+ return (
234
+ <WorkbenchPropertyNumberRow
235
+ label={field.label}
236
+ disabled={readOnly}
237
+ max={field.max}
238
+ min={field.min}
239
+ step={field.step}
240
+ value={typeof value === 'number' ? value : undefined}
241
+ onValueChange={(next) => onValueChange(field.prop, next)}
242
+ />
243
+ );
244
+ }
245
+
246
+ if (field.kind === 'select') {
247
+ return (
248
+ <WorkbenchPropertySelectRow
249
+ label={field.label}
250
+ disabled={readOnly}
251
+ options={field.options}
252
+ value={typeof value === 'string' ? value : undefined}
253
+ onValueChange={(next) => onValueChange(field.prop, next)}
254
+ />
255
+ );
256
+ }
257
+
258
+ return (
259
+ <WorkbenchPropertyCheckboxRow
260
+ label={field.label}
261
+ disabled={readOnly}
262
+ checked={typeof value === 'boolean' ? value : false}
263
+ onCheckedChange={(next) => onValueChange(field.prop, next)}
264
+ />
265
+ );
266
+ }
@@ -0,0 +1,56 @@
1
+ import { useMemo } from 'react';
2
+ import type { WidgetJsonSchema } from '@workbench-kit/contracts';
3
+
4
+ import { JsonCodeEditorPane } from '../jdw/JsonCodeEditorPane.js';
5
+ import type { WorkspaceEditorTheme } from '../workbench/workspace/WorkspaceEditor.js';
6
+ import type { WorkspaceFile } from '../workbench/workspace/types.js';
7
+ import { WIDGET_TREE_DOCUMENT_MIME } from './widget-tree-document.js';
8
+
9
+ export interface WidgetSourceEditorProps {
10
+ readonly path?: string | undefined;
11
+ readonly value: string;
12
+ readonly onChange: (value: string) => void;
13
+ readonly onSave?: (() => void) | undefined;
14
+ readonly parseError?: string | null | undefined;
15
+ readonly jsonSchema?: WidgetJsonSchema | null | undefined;
16
+ readonly readOnly?: boolean | undefined;
17
+ readonly showProblemsPanel?: boolean | undefined;
18
+ readonly theme?: WorkspaceEditorTheme | undefined;
19
+ }
20
+
21
+ export function WidgetSourceEditor({
22
+ path = 'widget.json',
23
+ value,
24
+ onChange,
25
+ onSave,
26
+ parseError = null,
27
+ jsonSchema = null,
28
+ readOnly = false,
29
+ showProblemsPanel = false,
30
+ theme = 'dark',
31
+ }: WidgetSourceEditorProps) {
32
+ const file = useMemo<WorkspaceFile>(
33
+ () => ({
34
+ content: value,
35
+ mimeType: WIDGET_TREE_DOCUMENT_MIME,
36
+ path,
37
+ }),
38
+ [path, value],
39
+ );
40
+
41
+ return (
42
+ <div className="widget-tree-source" data-testid="widget-tree-source">
43
+ <JsonCodeEditorPane
44
+ documentParseError={parseError}
45
+ file={file}
46
+ jsonSchema={jsonSchema}
47
+ readOnly={readOnly}
48
+ showProblemsPanel={showProblemsPanel}
49
+ theme={theme}
50
+ value={value}
51
+ onChange={onChange}
52
+ onSave={onSave}
53
+ />
54
+ </div>
55
+ );
56
+ }
@@ -0,0 +1,272 @@
1
+ import { useEffect, useMemo, useState } from 'react';
2
+ import type { WidgetRegistryContract } from '@workbench-kit/contracts';
3
+ import type { WidgetAssetCatalogContract, WidgetPlacementAsset } from '@workbench-kit/contracts';
4
+ import {
5
+ applyWidgetDocumentPatch,
6
+ createJdwDocumentJsonSchema,
7
+ createWidgetDocument,
8
+ firstSelectedWidgetPath,
9
+ getWidgetAtPath,
10
+ getWidgetChildren,
11
+ materializeWidgetPlacementAsset,
12
+ normalizeWidgetForParent,
13
+ ROOT_WIDGET_PATH,
14
+ selectWidgetPath,
15
+ validateJsonWidgetData,
16
+ type GenericWidget,
17
+ type WidgetPath,
18
+ type WidgetSelectionState,
19
+ } from '@workbench-kit/jdw';
20
+
21
+ import { ResizablePanels } from '../primitives/WorkbenchEditor.js';
22
+ import type { WorkspaceEditorTheme } from '../workbench/workspace/WorkspaceEditor.js';
23
+ import { JdwPreview } from '../jdw/JdwPreview.js';
24
+ import { WidgetAssetPalette } from './WidgetAssetPalette.js';
25
+ import { WidgetInspectorPanel } from './WidgetInspectorPanel.js';
26
+ import { WidgetSourceEditor } from './WidgetSourceEditor.js';
27
+ import { WidgetTreeSidePanel } from './WidgetTreeSidePanel.js';
28
+ import { WidgetTreeView } from './WidgetTreeView.js';
29
+ import { canAddChildren } from './widget-tree-layout.js';
30
+ import {
31
+ DEFAULT_WIDGET_TREE_VIEW_MODE,
32
+ resolveWidgetTreeLabMode,
33
+ type WidgetTreeViewMode,
34
+ } from './widget-tree-mode.js';
35
+
36
+ export interface WidgetTreeLabProps {
37
+ readonly path?: string | undefined;
38
+ readonly value: string;
39
+ readonly onChange: (value: string) => void;
40
+ readonly onSave?: (() => void) | undefined;
41
+ readonly registry?: WidgetRegistryContract<unknown>;
42
+ readonly assetCatalog?: WidgetAssetCatalogContract | undefined;
43
+ readonly readOnly?: boolean;
44
+ readonly theme?: WorkspaceEditorTheme | undefined;
45
+ readonly viewMode?: WidgetTreeViewMode;
46
+ }
47
+
48
+ export function WidgetTreeLab({
49
+ path,
50
+ value,
51
+ onChange,
52
+ onSave,
53
+ registry,
54
+ assetCatalog,
55
+ readOnly = false,
56
+ theme = 'dark',
57
+ viewMode = DEFAULT_WIDGET_TREE_VIEW_MODE,
58
+ }: WidgetTreeLabProps) {
59
+ const resolvedMode = resolveWidgetTreeLabMode(viewMode);
60
+ const document = useMemo(() => createWidgetDocument(value), [value]);
61
+ const jsonSchema = useMemo(() => createJdwDocumentJsonSchema(), []);
62
+ const registeredTypes = useMemo(
63
+ () => registry?.definitions().map((definition) => definition.type),
64
+ [registry],
65
+ );
66
+ const validation = useMemo(() => {
67
+ if (document.parseError !== null) {
68
+ return null;
69
+ }
70
+
71
+ return validateJsonWidgetData(value, {
72
+ registeredTypes,
73
+ strictKnownTypes: Boolean(registry),
74
+ });
75
+ }, [document.parseError, registry, registeredTypes, value]);
76
+ const sourceValidationError = useMemo(() => {
77
+ if (document.parseError !== null) {
78
+ return document.parseError;
79
+ }
80
+
81
+ if (!validation || validation.valid || validation.issues.length === 0) {
82
+ return null;
83
+ }
84
+
85
+ const [firstIssue] = validation.issues;
86
+ return firstIssue ? `${firstIssue.path}: ${firstIssue.message}` : null;
87
+ }, [document.parseError, validation]);
88
+
89
+ const [selection, setSelection] = useState<WidgetSelectionState>({ pathKeys: new Set() });
90
+
91
+ useEffect(() => {
92
+ if (document.parseError !== null || document.root === null) {
93
+ setSelection({ pathKeys: new Set() });
94
+ return;
95
+ }
96
+
97
+ setSelection((current) => {
98
+ const selectedPath = firstSelectedWidgetPath(current);
99
+ if (selectedPath !== null && getWidgetAtPath(document.root!, selectedPath)) {
100
+ return current;
101
+ }
102
+
103
+ return selectWidgetPath(current, ROOT_WIDGET_PATH);
104
+ });
105
+ }, [document.parseError, document.root, value]);
106
+
107
+ const selectedPath = firstSelectedWidgetPath(selection);
108
+ const selectedWidget = useMemo(() => {
109
+ if (!document.root || selectedPath === null) return null;
110
+ return getWidgetAtPath(document.root, selectedPath);
111
+ }, [document.root, selectedPath]);
112
+
113
+ const parentWidget = useMemo(() => {
114
+ if (!document.root || !selectedPath || selectedPath.length === 0) return null;
115
+ return getWidgetAtPath(document.root, selectedPath.slice(0, -1));
116
+ }, [document.root, selectedPath]);
117
+
118
+ const handleSelectPath = (nextPath: WidgetPath) => {
119
+ setSelection((current) => selectWidgetPath(current, nextPath));
120
+ };
121
+
122
+ const applyPatch = (patch: Parameters<typeof applyWidgetDocumentPatch>[1]) => {
123
+ const nextSource = applyWidgetDocumentPatch(value, patch);
124
+ if (nextSource !== null) {
125
+ onChange(nextSource);
126
+ }
127
+ };
128
+
129
+ const handleInspectorPatch = (nextWidget: GenericWidget) => {
130
+ if (!selectedPath) return;
131
+ const widget =
132
+ parentWidget !== null ? normalizeWidgetForParent(nextWidget, parentWidget) : nextWidget;
133
+ applyPatch({
134
+ type: 'replace-widget',
135
+ path: selectedPath,
136
+ widget,
137
+ });
138
+ };
139
+
140
+ const handleInsertChild = (child: GenericWidget) => {
141
+ if (!selectedPath || !selectedWidget) return;
142
+ const index = getWidgetChildren(selectedWidget).length;
143
+ applyPatch({
144
+ type: 'insert-child',
145
+ parentPath: selectedPath,
146
+ index,
147
+ child,
148
+ });
149
+ };
150
+
151
+ const handlePlaceAsset = (asset: WidgetPlacementAsset) => {
152
+ if (!selectedPath || !selectedWidget || !canAddChildren(selectedWidget)) return;
153
+ handleInsertChild(materializeWidgetPlacementAsset(asset, selectedWidget));
154
+ };
155
+
156
+ const handleRemove = () => {
157
+ if (!selectedPath || selectedPath.length === 0) return;
158
+ applyPatch({
159
+ type: 'remove-widget',
160
+ path: selectedPath,
161
+ });
162
+ setSelection((current) => selectWidgetPath(current, ROOT_WIDGET_PATH));
163
+ };
164
+
165
+ const sourcePane = (
166
+ <WidgetSourceEditor
167
+ jsonSchema={jsonSchema}
168
+ parseError={sourceValidationError}
169
+ path={path}
170
+ readOnly={readOnly}
171
+ showProblemsPanel
172
+ theme={theme}
173
+ value={value}
174
+ onChange={onChange}
175
+ onSave={onSave}
176
+ />
177
+ );
178
+
179
+ const sidePanel = (
180
+ <WidgetTreeSidePanel
181
+ assets={
182
+ assetCatalog ? (
183
+ <WidgetAssetPalette
184
+ assetsByCategory={assetCatalog.assetsByCategory()}
185
+ readOnly={readOnly}
186
+ selectedContainer={canAddChildren(selectedWidget) ? selectedWidget : null}
187
+ onPlaceAsset={handlePlaceAsset}
188
+ />
189
+ ) : (
190
+ <div className="widget-tree-asset-palette widget-tree-asset-palette--empty">
191
+ No asset catalog configured.
192
+ </div>
193
+ )
194
+ }
195
+ outline={
196
+ <WidgetTreeView
197
+ parseError={document.parseError}
198
+ root={document.root}
199
+ selection={selection}
200
+ onSelectPath={handleSelectPath}
201
+ />
202
+ }
203
+ properties={
204
+ <WidgetInspectorPanel
205
+ parentWidget={parentWidget}
206
+ path={selectedPath}
207
+ readOnly={readOnly}
208
+ widget={selectedWidget}
209
+ widgetRegistry={registry}
210
+ onPatch={handleInspectorPatch}
211
+ onRemove={handleRemove}
212
+ />
213
+ }
214
+ />
215
+ );
216
+
217
+ const previewPane = (
218
+ <section aria-label="Widget preview" className="widget-tree-lab__preview">
219
+ <header className="widget-tree-lab__preview-header">Preview</header>
220
+ <div className="widget-tree-lab__preview-body">
221
+ <JdwPreview json={value} registry={registry} />
222
+ </div>
223
+ </section>
224
+ );
225
+
226
+ if (resolvedMode === 'code') {
227
+ return (
228
+ <div
229
+ className="widget-tree-lab widget-tree-lab--code"
230
+ data-mode="code"
231
+ data-testid="widget-tree-lab"
232
+ >
233
+ {sourcePane}
234
+ </div>
235
+ );
236
+ }
237
+
238
+ return (
239
+ <div
240
+ className="widget-tree-lab widget-tree-lab--design"
241
+ data-mode="design"
242
+ data-testid="widget-tree-lab"
243
+ >
244
+ <ResizablePanels
245
+ className="widget-tree-lab__design"
246
+ data-testid="widget-tree-workspace"
247
+ defaultFirstSize={560}
248
+ minFirstSize={360}
249
+ minSecondSize={280}
250
+ style={{ height: '100%', minHeight: 0 }}
251
+ first={
252
+ <ResizablePanels
253
+ className="widget-tree-lab__design-data"
254
+ data-testid="widget-tree-lab-data-pane"
255
+ defaultFirstSize={320}
256
+ direction="vertical"
257
+ minFirstSize={160}
258
+ minSecondSize={180}
259
+ style={{ height: '100%', minHeight: 0 }}
260
+ first={<div className="widget-tree-lab__design-source">{sourcePane}</div>}
261
+ second={<div className="widget-tree-lab__design-tools">{sidePanel}</div>}
262
+ />
263
+ }
264
+ second={
265
+ <div className="widget-tree-lab__design-render" data-testid="widget-tree-lab-render-pane">
266
+ {previewPane}
267
+ </div>
268
+ }
269
+ />
270
+ </div>
271
+ );
272
+ }