@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
@@ -1,123 +1,178 @@
1
- import type { ComponentPropsWithRef, ReactNode } from 'react';
2
- import { cx } from '../utils/cx';
3
-
4
- export type StatusBarSectionAlign = 'end' | 'start';
5
-
6
- export interface StatusBarItemModel {
7
- active?: boolean;
8
- ariaLabel?: string;
9
- disabled?: boolean;
10
- hidden?: boolean;
11
- icon?: ReactNode;
12
- id: string;
13
- label: ReactNode;
14
- title?: string;
15
- }
16
-
17
- export interface StatusBarSectionModel {
18
- align?: StatusBarSectionAlign;
19
- hidden?: boolean;
20
- id: string;
21
- items: StatusBarItemModel[];
22
- }
23
-
24
- export interface StatusBarProps extends ComponentPropsWithRef<'footer'> {
25
- compact?: boolean;
26
- onItemActivate?: (item: StatusBarItemModel) => void;
27
- sections?: StatusBarSectionModel[];
28
- }
29
-
30
- export function StatusBar({
31
- 'aria-label': ariaLabel = 'Status bar',
32
- children,
33
- className,
34
- compact,
35
- onItemActivate,
36
- sections,
37
- ...props
38
- }: StatusBarProps) {
39
- const content =
40
- children ??
41
- sections
42
- ?.filter((section) => !section.hidden)
43
- .map((section) => (
44
- <StatusBarSection key={section.id} align={section.align}>
45
- {section.items
46
- .filter((item) => !item.hidden)
47
- .map((item) => (
48
- <StatusBarItem
49
- key={item.id}
50
- active={item.active}
51
- aria-label={item.ariaLabel}
52
- disabled={item.disabled}
53
- icon={item.icon}
54
- title={item.title}
55
- onClick={() => onItemActivate?.(item)}
56
- >
57
- {item.label}
58
- </StatusBarItem>
59
- ))}
60
- </StatusBarSection>
61
- ));
62
-
63
- return (
64
- <footer
65
- aria-label={ariaLabel}
66
- className={cx(
67
- 'ui-workbench-status-bar',
68
- compact && 'ui-workbench-status-bar--compact',
69
- className,
70
- )}
71
- {...props}
72
- >
73
- {content}
74
- </footer>
75
- );
76
- }
77
-
78
- export interface StatusBarSectionProps extends ComponentPropsWithRef<'div'> {
79
- align?: StatusBarSectionAlign;
80
- }
81
-
82
- export function StatusBarSection({ align = 'start', className, ...props }: StatusBarSectionProps) {
83
- return (
84
- <div
85
- className={cx(
86
- 'ui-workbench-status-bar__section',
87
- align === 'end' && 'ui-workbench-status-bar__section--end',
88
- className,
89
- )}
90
- {...props}
91
- />
92
- );
93
- }
94
-
95
- export interface StatusBarItemProps extends ComponentPropsWithRef<'button'> {
96
- active?: boolean;
97
- icon?: ReactNode;
98
- }
99
-
100
- export function StatusBarItem({
101
- active,
102
- children,
103
- className,
104
- icon,
105
- type = 'button',
106
- ...props
107
- }: StatusBarItemProps) {
108
- return (
109
- <button
110
- type={type}
111
- className={cx(
112
- 'ui-workbench-status-bar__item',
113
- active && 'ui-workbench-status-bar__item--active',
114
- className,
115
- )}
116
- data-active={active ? 'true' : undefined}
117
- {...props}
118
- >
119
- {icon ? <span className="ui-workbench-status-bar__icon">{icon}</span> : null}
120
- <span className="ui-workbench-status-bar__label">{children}</span>
121
- </button>
122
- );
123
- }
1
+ import type { ComponentPropsWithRef, ReactNode } from 'react';
2
+ import { cxCodicon } from '../utils/codicon';
3
+ import { cx } from '../utils/cx';
4
+ import {
5
+ getWorkbenchStatusLabel,
6
+ isWorkbenchStatusBusy,
7
+ isWorkbenchStatusDisabled,
8
+ type WorkbenchStatus,
9
+ } from './status';
10
+
11
+ export type StatusBarSectionAlign = 'end' | 'start';
12
+
13
+ export interface StatusBarItemModel {
14
+ active?: boolean;
15
+ ariaLabel?: string;
16
+ disabled?: boolean;
17
+ order?: number;
18
+ hidden?: boolean;
19
+ icon?: ReactNode | string;
20
+ id: string;
21
+ label: ReactNode;
22
+ status?: WorkbenchStatus;
23
+ title?: string;
24
+ }
25
+
26
+ export interface StatusBarSectionModel {
27
+ order?: number;
28
+ align?: StatusBarSectionAlign;
29
+ hidden?: boolean;
30
+ id: string;
31
+ items: StatusBarItemModel[];
32
+ }
33
+
34
+ export interface StatusBarProps extends ComponentPropsWithRef<'footer'> {
35
+ compact?: boolean;
36
+ onItemActivate?: (item: StatusBarItemModel) => void;
37
+ sections?: StatusBarSectionModel[];
38
+ }
39
+
40
+ export function StatusBar({
41
+ 'aria-label': ariaLabel = 'Status bar',
42
+ children,
43
+ className,
44
+ compact,
45
+ onItemActivate,
46
+ sections,
47
+ ...props
48
+ }: StatusBarProps) {
49
+ const content =
50
+ children ??
51
+ sortByOrderPriority(sections ?? [])
52
+ ?.filter((section) => !section.hidden)
53
+ .map((section) => (
54
+ <StatusBarSection key={section.id} align={section.align}>
55
+ {sortByOrderPriority(section.items)
56
+ .filter((item) => !item.hidden)
57
+ .map((item) => (
58
+ <StatusBarItem
59
+ key={item.id}
60
+ active={item.active}
61
+ aria-label={item.ariaLabel}
62
+ disabled={item.disabled}
63
+ icon={item.icon}
64
+ status={item.status}
65
+ title={item.title}
66
+ onClick={() => onItemActivate?.(item)}
67
+ >
68
+ {item.label}
69
+ </StatusBarItem>
70
+ ))}
71
+ </StatusBarSection>
72
+ ));
73
+
74
+ return (
75
+ <footer
76
+ aria-label={ariaLabel}
77
+ className={cx(
78
+ 'ui-workbench-status-bar',
79
+ compact && 'ui-workbench-status-bar--compact',
80
+ className,
81
+ )}
82
+ {...props}
83
+ >
84
+ {content}
85
+ </footer>
86
+ );
87
+ }
88
+
89
+ function sortByOrderPriority<T extends { order?: number }>(values: readonly T[]): T[] {
90
+ return [...values]
91
+ .map((value, sourceIndex) => ({ value, sourceIndex }))
92
+ .sort((left, right) => {
93
+ const leftOrder = left.value.order;
94
+ const rightOrder = right.value.order;
95
+
96
+ if (leftOrder !== rightOrder) {
97
+ if (leftOrder === undefined) return 1;
98
+ if (rightOrder === undefined) return -1;
99
+ return leftOrder - rightOrder;
100
+ }
101
+ return left.sourceIndex - right.sourceIndex;
102
+ })
103
+ .map(({ value }) => value);
104
+ }
105
+
106
+ export interface StatusBarSectionProps extends ComponentPropsWithRef<'div'> {
107
+ align?: StatusBarSectionAlign | undefined;
108
+ }
109
+
110
+ export function StatusBarSection({ align = 'start', className, ...props }: StatusBarSectionProps) {
111
+ return (
112
+ <div
113
+ className={cx(
114
+ 'ui-workbench-status-bar__section',
115
+ align === 'end' && 'ui-workbench-status-bar__section--end',
116
+ className,
117
+ )}
118
+ {...props}
119
+ />
120
+ );
121
+ }
122
+
123
+ export interface StatusBarItemProps extends ComponentPropsWithRef<'button'> {
124
+ active?: boolean | undefined;
125
+ icon?: ReactNode | string | undefined;
126
+ status?: WorkbenchStatus | undefined;
127
+ }
128
+
129
+ function resolveStatusBarIcon(icon: ReactNode | string | undefined) {
130
+ if (typeof icon !== 'string') return icon;
131
+
132
+ return <i className={cxCodicon(icon)} aria-hidden="true" />;
133
+ }
134
+
135
+ export function StatusBarItem({
136
+ active,
137
+ children,
138
+ className,
139
+ disabled,
140
+ icon,
141
+ status,
142
+ title,
143
+ type = 'button',
144
+ ...props
145
+ }: StatusBarItemProps) {
146
+ const statusLabel = status ? getWorkbenchStatusLabel(status) : undefined;
147
+ const resolvedDisabled = disabled || Boolean(status && isWorkbenchStatusDisabled(status));
148
+ const resolvedIcon = resolveStatusBarIcon(icon);
149
+
150
+ return (
151
+ <button
152
+ type={type}
153
+ className={cx(
154
+ 'ui-workbench-status-bar__item',
155
+ active && 'ui-workbench-status-bar__item--active',
156
+ className,
157
+ )}
158
+ data-active={active ? 'true' : undefined}
159
+ data-status={status}
160
+ aria-busy={status && isWorkbenchStatusBusy(status) ? true : undefined}
161
+ disabled={resolvedDisabled}
162
+ title={title ?? statusLabel}
163
+ {...props}
164
+ >
165
+ {resolvedIcon ? <span className="ui-workbench-status-bar__icon">{resolvedIcon}</span> : null}
166
+ <span className="ui-workbench-status-bar__label">{children}</span>
167
+ {status ? (
168
+ <span
169
+ aria-label={statusLabel}
170
+ className="ui-workbench-status-bar__status"
171
+ title={statusLabel}
172
+ >
173
+ <span aria-hidden="true" className="ui-workbench-status-bar__status-dot" />
174
+ </span>
175
+ ) : null}
176
+ </button>
177
+ );
178
+ }
@@ -0,0 +1,205 @@
1
+ import { useState, useMemo, type ReactNode } from 'react';
2
+ import { useWorkspaceDrafts } from './workspace/WorkspaceDraftsContext';
3
+ import { WorkspaceEditor, type WorkspaceEditorTheme } from './workspace/WorkspaceEditor';
4
+ import { WorkbenchStructuredDataSchemaPanelEmbed } from './settings/StructuredDataSchemaPanelEmbed';
5
+ import { type WorkbenchStructuredDataSchemaDocument } from './settings/StructuredDataForm';
6
+ import { type WorkbenchArtifactMode, WorkbenchArtifactModeControls } from './ArtifactShell';
7
+ import { Button } from '../primitives/Button';
8
+ import { Panel, PanelBody, PanelHeader } from '../layout/Panel';
9
+ import { Toolbar } from '../primitives/Toolbar';
10
+ import { SplitView } from './SplitView';
11
+ import { type WorkspaceFile } from './workspace/types';
12
+ import { fileNameOfPath } from './workspace/path';
13
+
14
+ export interface StructuredArtifactEditorProps {
15
+ activePattern?: string;
16
+ defaultMode?: WorkbenchArtifactMode;
17
+ emptyLabel?: ReactNode;
18
+ file: WorkspaceFile | null;
19
+ headerActions?: ReactNode;
20
+ mode?: WorkbenchArtifactMode;
21
+ onModeChange?: (mode: WorkbenchArtifactMode) => void;
22
+ onSaveFile?: (path: string, content: string) => void | Promise<void>;
23
+ preferredTableColumns?: readonly string[];
24
+ readOnly?: boolean;
25
+ schema?: WorkbenchStructuredDataSchemaDocument | null;
26
+ sectionValueAliases?: Record<string, readonly (string | number)[][]>;
27
+ theme?: WorkspaceEditorTheme;
28
+ titleFallback?: string;
29
+ serialize?: (data: unknown) => string;
30
+ deserialize?: (content: string) => unknown;
31
+ }
32
+
33
+ const defaultSerialize = (data: unknown) => JSON.stringify(data, null, 2);
34
+ const defaultDeserialize = (content: string) => {
35
+ if (!content.trim()) return null;
36
+ return JSON.parse(content);
37
+ };
38
+
39
+ export function StructuredArtifactEditor({
40
+ activePattern,
41
+ defaultMode = 'preview',
42
+ emptyLabel = 'No artifact selected',
43
+ file,
44
+ headerActions,
45
+ mode,
46
+ onModeChange,
47
+ onSaveFile,
48
+ preferredTableColumns,
49
+ readOnly = false,
50
+ schema,
51
+ sectionValueAliases,
52
+ theme = 'dark',
53
+ titleFallback,
54
+ serialize = defaultSerialize,
55
+ deserialize = defaultDeserialize,
56
+ }: StructuredArtifactEditorProps) {
57
+ const [uncontrolledMode, setUncontrolledMode] = useState<WorkbenchArtifactMode>(defaultMode);
58
+ const resolvedMode = mode ?? uncontrolledMode;
59
+
60
+ const { getDraft, isDirty, updateDraft, saveDraft, discardDraft } = useWorkspaceDrafts();
61
+
62
+ const setMode = (nextMode: WorkbenchArtifactMode) => {
63
+ if (mode === undefined) {
64
+ setUncontrolledMode(nextMode);
65
+ }
66
+ onModeChange?.(nextMode);
67
+ };
68
+
69
+ const currentContent = file ? getDraft(file.path, file.content) : '';
70
+ const dirty = file ? isDirty(file.path, file.content) : false;
71
+
72
+ const structuredData = useMemo(() => {
73
+ if (!currentContent) return null;
74
+ try {
75
+ return deserialize(currentContent);
76
+ } catch {
77
+ return null;
78
+ }
79
+ }, [currentContent, deserialize]);
80
+
81
+ if (!file) {
82
+ return (
83
+ <Panel className="ui-structured-artifact-editor ui-structured-artifact-editor--empty">
84
+ <PanelBody>
85
+ <div className="ui-workbench-artifact-shell__empty">{emptyLabel}</div>
86
+ </PanelBody>
87
+ </Panel>
88
+ );
89
+ }
90
+
91
+ const handleTextChange = (text: string) => {
92
+ updateDraft(file.path, text, file.content);
93
+ };
94
+
95
+ const handleDataChange = (nextData: unknown) => {
96
+ try {
97
+ const nextText = serialize(nextData);
98
+ updateDraft(file.path, nextText, file.content);
99
+ } catch {
100
+ // ignore serialization failures during active input
101
+ }
102
+ };
103
+
104
+ const handleSave = () => {
105
+ if (!dirty) return;
106
+ Promise.resolve(onSaveFile?.(file.path, currentContent))
107
+ .then(() => {
108
+ saveDraft(file.path, currentContent);
109
+ })
110
+ .catch(() => {
111
+ // keep draft intact on failure
112
+ });
113
+ };
114
+
115
+ const handleDiscard = () => {
116
+ discardDraft(file.path, file.content);
117
+ };
118
+
119
+ const codePane = (
120
+ <WorkspaceEditor
121
+ file={file}
122
+ readOnly={readOnly}
123
+ showHeader={false}
124
+ theme={theme}
125
+ value={currentContent}
126
+ onChange={handleTextChange}
127
+ />
128
+ );
129
+
130
+ const previewPane = (
131
+ <WorkbenchStructuredDataSchemaPanelEmbed
132
+ activePattern={activePattern}
133
+ ariaLabel="Schema sections"
134
+ data={structuredData}
135
+ preferredTableColumns={preferredTableColumns}
136
+ readOnly={readOnly}
137
+ schema={schema}
138
+ sectionValueAliases={sectionValueAliases}
139
+ titleFallback={titleFallback ?? fileNameOfPath(file.path)}
140
+ onDataChange={handleDataChange}
141
+ />
142
+ );
143
+
144
+ return (
145
+ <Panel className="ui-structured-artifact-editor" data-theme={theme} data-mode={resolvedMode}>
146
+ <PanelHeader
147
+ actions={
148
+ <Toolbar>
149
+ {dirty && !readOnly ? (
150
+ <>
151
+ <Button onClick={handleDiscard}>Discard</Button>
152
+ <Button variant="primary" onClick={handleSave}>
153
+ Save
154
+ </Button>
155
+ </>
156
+ ) : null}
157
+ {headerActions}
158
+ <WorkbenchArtifactModeControls mode={resolvedMode} onModeChange={setMode} />
159
+ </Toolbar>
160
+ }
161
+ >
162
+ <span className="ui-structured-artifact-editor__title">
163
+ {titleFallback ?? fileNameOfPath(file.path)}
164
+ {dirty ? (
165
+ <span
166
+ className="ui-structured-artifact-editor__dirty-indicator"
167
+ title="Unsaved changes"
168
+ >
169
+
170
+ </span>
171
+ ) : null}
172
+ </span>
173
+ </PanelHeader>
174
+ <PanelBody className="ui-structured-artifact-editor__body">
175
+ {resolvedMode === 'code' ? (
176
+ <section className="ui-structured-artifact-editor__pane" aria-label="Code">
177
+ {codePane}
178
+ </section>
179
+ ) : null}
180
+ {resolvedMode === 'preview' ? (
181
+ <section className="ui-structured-artifact-editor__pane" aria-label="Preview">
182
+ {previewPane}
183
+ </section>
184
+ ) : null}
185
+ {resolvedMode === 'split' ? (
186
+ <SplitView
187
+ className="ui-structured-artifact-editor__split"
188
+ defaultPrimarySizePercent={50}
189
+ minPrimarySizePercent={20}
190
+ primary={
191
+ <section className="ui-structured-artifact-editor__pane" aria-label="Code">
192
+ {codePane}
193
+ </section>
194
+ }
195
+ secondary={
196
+ <section className="ui-structured-artifact-editor__pane" aria-label="Preview">
197
+ {previewPane}
198
+ </section>
199
+ }
200
+ />
201
+ ) : null}
202
+ </PanelBody>
203
+ </Panel>
204
+ );
205
+ }