@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,353 +1,334 @@
1
- import { useMemo, useState, type MouseEvent } from 'react';
2
- import {
3
- canExecuteCommand,
4
- createCommandRegistry,
5
- executeCommand,
6
- resolveCommandMenuItems,
7
- } from '@workbench-kit/core';
8
- import { isSaveSuccess, type SaveResult } from '@workbench-kit/contracts';
9
- import {
10
- discardWorkspaceFileDraft,
11
- resolveWorkspaceFileDraft,
12
- saveWorkspaceFileDraft,
13
- updateWorkspaceFileDraft,
14
- type WorkspaceFileDraftMap,
15
- } from '@workbench-kit/workspace';
16
- import { ConfirmDialog } from '../../modal/ConfirmDialog';
17
- import { ContextMenu, type ContextMenuItem } from '../../overlay/ContextMenu';
18
- import { EmptyState } from '../../primitives/EmptyState';
19
- import { IconButton } from '../../primitives/IconButton';
20
- import { Toolbar } from '../../primitives/Toolbar';
21
- import { Panel, PanelBody } from '../../layout/Panel';
22
- import {
23
- WORKBENCH_EDITOR_DISCARD_CHANGES_COMMAND_ID,
24
- WORKBENCH_EDITOR_SAVE_COMMAND_ID,
25
- WORKBENCH_COMMAND_SURFACE_EDITOR,
26
- commandMenuItemsToContextMenuItems,
27
- createWorkbenchEditorCommands,
28
- createWorkbenchEditorTabListMenuEntries,
29
- createWorkbenchEditorTabMenuEntries,
30
- type WorkbenchEditorCommandContext,
31
- } from '../commands';
32
- import { fileNameOfPath } from './path';
33
- import { WorkspaceEditor, type WorkspaceEditorTheme } from './WorkspaceEditor';
34
- import { WorkspaceFileIcon } from './WorkspaceFileIcon';
35
- import type { WorkspaceFile } from './types';
36
-
37
- const editorCommandRegistry = createCommandRegistry(createWorkbenchEditorCommands());
38
- const editorTabListMenuEntries = createWorkbenchEditorTabListMenuEntries();
39
- const editorTabMenuEntries = createWorkbenchEditorTabMenuEntries();
40
-
41
- export interface WorkspaceEditorPanelProps {
42
- emptyLabel?: string;
43
- files: WorkspaceFile[];
44
- onCloseAll?: () => void;
45
- onCloseOthers?: (path: string) => void;
46
- onClosePath?: (path: string) => void;
47
- onCopyPath?: (path: string) => void;
48
- onDeletePath?: (path: string) => void;
49
- onSaveFile?: (
50
- path: string,
51
- content: string,
52
- previousUpdatedAt?: string,
53
- ) => SaveResult | Promise<SaveResult | undefined> | undefined;
54
- onSelectedPathChange: (path: string) => void;
55
- openPaths: string[];
56
- selectedPath?: string;
57
- theme?: WorkspaceEditorTheme;
58
- }
59
-
60
- export function WorkspaceEditorPanel({
61
- emptyLabel = 'Open a file from Explorer or Search.',
62
- files,
63
- onCloseAll,
64
- onCloseOthers,
65
- onClosePath,
66
- onCopyPath,
67
- onDeletePath,
68
- onSaveFile,
69
- onSelectedPathChange,
70
- openPaths,
71
- selectedPath,
72
- theme,
73
- }: WorkspaceEditorPanelProps) {
74
- const filesByPath = useMemo(() => new Map(files.map((file) => [file.path, file])), [files]);
75
- const openFiles = openPaths
76
- .map((path) => filesByPath.get(path))
77
- .filter((file): file is WorkspaceFile => Boolean(file));
78
- const selectedFile = selectedPath ? filesByPath.get(selectedPath) : openFiles[0];
79
- const [deletePath, setDeletePath] = useState<string | null>(null);
80
- const [draftsByPath, setDraftsByPath] = useState<WorkspaceFileDraftMap>({});
81
- const [tabContextMenu, setTabContextMenu] = useState<{
82
- path: string | null;
83
- x: number;
84
- y: number;
85
- } | null>(null);
86
- const selectedDraft = selectedFile
87
- ? resolveWorkspaceFileDraft({
88
- draft: draftsByPath[selectedFile.path],
89
- file: selectedFile,
90
- })
91
- : null;
92
- const selectedContent = selectedDraft?.content ?? '';
93
-
94
- const updateDraft = (path: string, content: string) => {
95
- setDraftsByPath((currentDrafts) =>
96
- updateWorkspaceFileDraft({
97
- content,
98
- drafts: currentDrafts,
99
- fileContent: filesByPath.get(path)?.content ?? '',
100
- path,
101
- }),
102
- );
103
- };
104
-
105
- const saveFile = (path: string, content: string) => {
106
- Promise.resolve(onSaveFile?.(path, content, filesByPath.get(path)?.updatedAt))
107
- .then((result) => {
108
- if (!result || isSaveSuccess(result)) {
109
- setDraftsByPath((currentDrafts) =>
110
- saveWorkspaceFileDraft({
111
- content,
112
- drafts: currentDrafts,
113
- path,
114
- }),
115
- );
116
- }
117
- })
118
- .catch(() => {
119
- // keep draft intact when persistence fails
120
- });
121
- };
122
-
123
- const discardFile = (file: WorkspaceFile) => {
124
- setDraftsByPath((currentDrafts) =>
125
- discardWorkspaceFileDraft({
126
- drafts: currentDrafts,
127
- file,
128
- }),
129
- );
130
- };
131
-
132
- const handleTabContextMenu = (event: MouseEvent<HTMLElement>, path: string | null) => {
133
- event.preventDefault();
134
- event.stopPropagation();
135
- setTabContextMenu({ path, x: event.clientX, y: event.clientY });
136
- };
137
-
138
- const createEditorCommandContext = (
139
- file: WorkspaceFile | undefined,
140
- ): WorkbenchEditorCommandContext => {
141
- const filePath = file?.path;
142
- const draft = file
143
- ? resolveWorkspaceFileDraft({
144
- draft: draftsByPath[file.path],
145
- file,
146
- })
147
- : undefined;
148
- const content = draft?.content ?? '';
149
-
150
- return {
151
- canCloseAll: Boolean(onCloseAll),
152
- canCloseOthers: Boolean(onCloseOthers),
153
- canClosePath: Boolean(onClosePath),
154
- canCopyPath: Boolean(onCopyPath),
155
- canDeletePath: Boolean(onDeletePath),
156
- canDiscardFile: Boolean(filePath),
157
- canSaveFile: Boolean(filePath),
158
- closeAll: () => onCloseAll?.(),
159
- closeOthers: () => {
160
- if (filePath) onCloseOthers?.(filePath);
161
- },
162
- closePath: () => {
163
- if (filePath) onClosePath?.(filePath);
164
- },
165
- copyPath: () => {
166
- if (filePath) onCopyPath?.(filePath);
167
- },
168
- deletePath: () => {
169
- if (filePath) setDeletePath(filePath);
170
- },
171
- discardFile: () => {
172
- if (file) discardFile(file);
173
- },
174
- filePath,
175
- hasMultipleOpenFiles: openFiles.length > 1,
176
- hasOpenFiles: openFiles.length > 0,
177
- hasUnsavedChanges: Boolean(file && content !== file.content),
178
- saveFile: () => {
179
- if (filePath) saveFile(filePath, content);
180
- },
181
- };
182
- };
183
-
184
- const createTabContextItems = (path: string | null): ContextMenuItem[] => {
185
- const file = path ? filesByPath.get(path) : undefined;
186
- const context = createEditorCommandContext(file);
187
-
188
- return commandMenuItemsToContextMenuItems(
189
- resolveCommandMenuItems({
190
- context,
191
- surface: WORKBENCH_COMMAND_SURFACE_EDITOR,
192
- entries: file ? editorTabMenuEntries : editorTabListMenuEntries,
193
- registry: editorCommandRegistry,
194
- }),
195
- (commandId) => executeCommand(editorCommandRegistry, commandId, context),
196
- );
197
- };
198
-
199
- const handleConfirmDelete = () => {
200
- if (deletePath) {
201
- onDeletePath?.(deletePath);
202
- }
203
- setDeletePath(null);
204
- };
205
- const selectedCommandContext = createEditorCommandContext(selectedFile);
206
-
207
- return (
208
- <Panel className="workspace-panel">
209
- <PanelBody className="workspace-panel__body">
210
- {openFiles.length > 0 ? (
211
- <div className="workspace-editor">
212
- <div className="workspace-editor__tabbar">
213
- <div
214
- aria-label="Open workspace files"
215
- className="workspace-editor__tabs"
216
- role="tablist"
217
- onContextMenu={(event) => handleTabContextMenu(event, null)}
218
- >
219
- {openFiles.map((file) => {
220
- const isActive = file.path === selectedFile?.path;
221
- const draft = resolveWorkspaceFileDraft({
222
- draft: draftsByPath[file.path],
223
- file,
224
- });
225
- const isDirty = draft.content !== file.content;
226
-
227
- return (
228
- <div
229
- key={file.path}
230
- className={`workspace-editor__tab${isActive ? ' workspace-editor__tab--active' : ''}`}
231
- title={file.path}
232
- onContextMenu={(event) => handleTabContextMenu(event, file.path)}
233
- >
234
- <button
235
- aria-selected={isActive}
236
- className="workspace-editor__tab-button"
237
- role="tab"
238
- type="button"
239
- onClick={() => onSelectedPathChange(file.path)}
240
- >
241
- <WorkspaceFileIcon mimeType={file.mimeType} path={file.path} />
242
- <span className="workspace-editor__path">{fileNameOfPath(file.path)}</span>
243
- {isDirty ? (
244
- <span className="workspace-editor__dirty" aria-label="Unsaved changes" />
245
- ) : null}
246
- </button>
247
- <IconButton
248
- className="workspace-editor__tab-close"
249
- icon="codicon-close"
250
- label={`Close ${fileNameOfPath(file.path)}`}
251
- onClick={() => onClosePath?.(file.path)}
252
- />
253
- </div>
254
- );
255
- })}
256
- </div>
257
-
258
- {selectedFile ? (
259
- <Toolbar className="workspace-editor__actions">
260
- <IconButton
261
- disabled={
262
- !canExecuteCommand(
263
- editorCommandRegistry,
264
- WORKBENCH_EDITOR_SAVE_COMMAND_ID,
265
- selectedCommandContext,
266
- )
267
- }
268
- icon="codicon-save"
269
- label="Save"
270
- onClick={() =>
271
- executeCommand(
272
- editorCommandRegistry,
273
- WORKBENCH_EDITOR_SAVE_COMMAND_ID,
274
- selectedCommandContext,
275
- )
276
- }
277
- />
278
- <IconButton
279
- disabled={
280
- !canExecuteCommand(
281
- editorCommandRegistry,
282
- WORKBENCH_EDITOR_DISCARD_CHANGES_COMMAND_ID,
283
- selectedCommandContext,
284
- )
285
- }
286
- icon="codicon-discard"
287
- label="Discard changes"
288
- onClick={() =>
289
- executeCommand(
290
- editorCommandRegistry,
291
- WORKBENCH_EDITOR_DISCARD_CHANGES_COMMAND_ID,
292
- selectedCommandContext,
293
- )
294
- }
295
- />
296
- <IconButton
297
- disabled={!onDeletePath}
298
- icon="codicon-trash"
299
- label="Delete"
300
- variant="danger"
301
- onClick={() => setDeletePath(selectedFile.path)}
302
- />
303
- </Toolbar>
304
- ) : null}
305
- </div>
306
-
307
- {selectedFile ? (
308
- <WorkspaceEditor
309
- key={selectedFile.path}
310
- file={selectedFile}
311
- readOnly={false}
312
- showHeader={false}
313
- theme={theme}
314
- value={selectedContent}
315
- onChange={(content) => updateDraft(selectedFile.path, content)}
316
- onSave={(content) => saveFile(selectedFile.path, content)}
317
- />
318
- ) : (
319
- <EmptyState icon="codicon-open-preview">{emptyLabel}</EmptyState>
320
- )}
321
- </div>
322
- ) : files.length === 0 ? (
323
- <EmptyState icon="codicon-folder">No workspace files</EmptyState>
324
- ) : (
325
- <EmptyState icon="codicon-open-preview">{emptyLabel}</EmptyState>
326
- )}
327
- </PanelBody>
328
-
329
- {deletePath ? (
330
- <ConfirmDialog
331
- closeLabel="Close confirmation"
332
- confirmLabel="Delete"
333
- detail={<code>{deletePath}</code>}
334
- message="Delete this workspace file?"
335
- title="Delete File"
336
- variant="danger"
337
- onCancel={() => setDeletePath(null)}
338
- onConfirm={handleConfirmDelete}
339
- />
340
- ) : null}
341
-
342
- {tabContextMenu ? (
343
- <ContextMenu
344
- ariaLabel="Editor tab menu"
345
- items={createTabContextItems(tabContextMenu.path)}
346
- x={tabContextMenu.x}
347
- y={tabContextMenu.y}
348
- onClose={() => setTabContextMenu(null)}
349
- />
350
- ) : null}
351
- </Panel>
352
- );
353
- }
1
+ import { useMemo, useState, type MouseEvent, type ReactNode } from 'react';
2
+ import {
3
+ createCommandRegistry,
4
+ executeCommand,
5
+ resolveCommandMenuItems,
6
+ } from '@workbench-kit/platform';
7
+ import { isSaveSuccess, type SaveResult } from '@workbench-kit/contracts';
8
+ import { useWorkspaceDrafts } from './WorkspaceDraftsContext';
9
+ import { ConfirmDialog } from '../../modal/ConfirmDialog';
10
+ import { ContextMenu, type ContextMenuItem } from '../../overlay/ContextMenu';
11
+ import { EmptyState } from '../../primitives/EmptyState';
12
+ import { IconButton } from '../../primitives/IconButton';
13
+ import { Panel, PanelBody } from '../../layout/Panel';
14
+ import {
15
+ WORKBENCH_COMMAND_SURFACE_EDITOR,
16
+ commandMenuItemsToContextMenuItems,
17
+ createWorkbenchEditorCommands,
18
+ createWorkbenchEditorTabListMenuEntries,
19
+ createWorkbenchEditorTabMenuEntries,
20
+ type WorkbenchEditorCommandContext,
21
+ } from '../commands';
22
+ import { fileNameOfPath } from './path';
23
+ import { WorkspaceEditor, type WorkspaceEditorTheme } from './WorkspaceEditor';
24
+ import { WorkspaceFileIcon } from './WorkspaceFileIcon';
25
+ import type { WorkspaceFile } from './types';
26
+
27
+ const editorCommandRegistry = createCommandRegistry(createWorkbenchEditorCommands());
28
+ const editorTabListMenuEntries = createWorkbenchEditorTabListMenuEntries();
29
+ const editorTabMenuEntries = createWorkbenchEditorTabMenuEntries();
30
+
31
+ export interface WorkspaceEditorPanelRenderEditorContext {
32
+ content: string;
33
+ file: WorkspaceFile;
34
+ isDirty: boolean;
35
+ onChange: (content: string) => void;
36
+ onDiscard: () => void;
37
+ onSave: (content: string) => void;
38
+ theme?: WorkspaceEditorTheme | undefined;
39
+ }
40
+
41
+ export type WorkspaceEditorPanelRenderEditor = (
42
+ context: WorkspaceEditorPanelRenderEditorContext,
43
+ ) => ReactNode;
44
+
45
+ export interface WorkspaceEditorPanelRenderTabActionsContext {
46
+ content: string;
47
+ file: WorkspaceFile;
48
+ isDirty: boolean;
49
+ onDelete: () => void;
50
+ onDiscard: () => void;
51
+ onSave: () => void;
52
+ theme?: WorkspaceEditorTheme | undefined;
53
+ }
54
+
55
+ export type WorkspaceEditorPanelRenderTabActions = (
56
+ context: WorkspaceEditorPanelRenderTabActionsContext,
57
+ ) => ReactNode;
58
+
59
+ export interface WorkspaceEditorPanelProps {
60
+ emptyLabel?: string;
61
+ files: WorkspaceFile[];
62
+ onCloseAll?: () => void;
63
+ onCloseOthers?: (path: string) => void;
64
+ onClosePath?: (path: string) => void;
65
+ onCopyPath?: (path: string) => void;
66
+ onDeletePath?: (path: string) => void;
67
+ onSaveFile?: (
68
+ path: string,
69
+ content: string,
70
+ previousUpdatedAt?: string,
71
+ ) => SaveResult | Promise<SaveResult | undefined> | undefined;
72
+ onSelectedPathChange: (path: string) => void;
73
+ openPaths: string[];
74
+ renderEditor?: WorkspaceEditorPanelRenderEditor | undefined;
75
+ renderTabActions?: WorkspaceEditorPanelRenderTabActions | undefined;
76
+ selectedPath?: string;
77
+ theme?: WorkspaceEditorTheme;
78
+ }
79
+
80
+ export function WorkspaceEditorPanel({
81
+ emptyLabel = 'Open a file from Explorer or Search.',
82
+ files,
83
+ onCloseAll,
84
+ onCloseOthers,
85
+ onClosePath,
86
+ onCopyPath,
87
+ onDeletePath,
88
+ onSaveFile,
89
+ onSelectedPathChange,
90
+ openPaths,
91
+ renderEditor,
92
+ renderTabActions,
93
+ selectedPath,
94
+ theme,
95
+ }: WorkspaceEditorPanelProps) {
96
+ const filesByPath = useMemo(() => new Map(files.map((file) => [file.path, file])), [files]);
97
+ const openFiles = openPaths
98
+ .map((path) => filesByPath.get(path))
99
+ .filter((file): file is WorkspaceFile => Boolean(file));
100
+ const selectedFile = selectedPath ? filesByPath.get(selectedPath) : openFiles[0];
101
+ const [deletePath, setDeletePath] = useState<string | null>(null);
102
+ const {
103
+ getDraft,
104
+ isDirty,
105
+ updateDraft: updateDraftContext,
106
+ saveDraft: saveDraftContext,
107
+ discardDraft: discardDraftContext,
108
+ } = useWorkspaceDrafts();
109
+ const [tabContextMenu, setTabContextMenu] = useState<{
110
+ path: string | null;
111
+ x: number;
112
+ y: number;
113
+ } | null>(null);
114
+ const selectedContent = selectedFile ? getDraft(selectedFile.path, selectedFile.content) : '';
115
+
116
+ const updateDraft = (path: string, content: string) => {
117
+ updateDraftContext(path, content, filesByPath.get(path)?.content ?? '');
118
+ };
119
+
120
+ const saveFile = (path: string, content: string) => {
121
+ Promise.resolve(onSaveFile?.(path, content, filesByPath.get(path)?.updatedAt))
122
+ .then((result) => {
123
+ if (!result || isSaveSuccess(result)) {
124
+ saveDraftContext(path, content);
125
+ }
126
+ })
127
+ .catch(() => {
128
+ // keep draft intact when persistence fails
129
+ });
130
+ };
131
+
132
+ const discardFile = (file: WorkspaceFile) => {
133
+ discardDraftContext(file.path, file.content);
134
+ };
135
+
136
+ const handleTabContextMenu = (event: MouseEvent<HTMLElement>, path: string | null) => {
137
+ event.preventDefault();
138
+ event.stopPropagation();
139
+ setTabContextMenu({ path, x: event.clientX, y: event.clientY });
140
+ };
141
+
142
+ const createEditorCommandContext = (
143
+ file: WorkspaceFile | undefined,
144
+ ): WorkbenchEditorCommandContext => {
145
+ const filePath = file?.path;
146
+ const content = file ? getDraft(file.path, file.content) : '';
147
+
148
+ return {
149
+ canCloseAll: Boolean(onCloseAll),
150
+ canCloseOthers: Boolean(onCloseOthers),
151
+ canClosePath: Boolean(onClosePath),
152
+ canCopyPath: Boolean(onCopyPath),
153
+ canDeletePath: Boolean(onDeletePath),
154
+ canDiscardFile: Boolean(filePath),
155
+ canSaveFile: Boolean(filePath),
156
+ closeAll: () => onCloseAll?.(),
157
+ closeOthers: () => {
158
+ if (filePath) onCloseOthers?.(filePath);
159
+ },
160
+ closePath: () => {
161
+ if (filePath) onClosePath?.(filePath);
162
+ },
163
+ copyPath: () => {
164
+ if (filePath) onCopyPath?.(filePath);
165
+ },
166
+ deletePath: () => {
167
+ if (filePath) setDeletePath(filePath);
168
+ },
169
+ discardFile: () => {
170
+ if (file) discardFile(file);
171
+ },
172
+ filePath,
173
+ hasMultipleOpenFiles: openFiles.length > 1,
174
+ hasOpenFiles: openFiles.length > 0,
175
+ hasUnsavedChanges: Boolean(file && isDirty(file.path, file.content)),
176
+ saveFile: () => {
177
+ if (filePath) saveFile(filePath, content);
178
+ },
179
+ };
180
+ };
181
+
182
+ const createTabContextItems = (path: string | null): ContextMenuItem[] => {
183
+ const file = path ? filesByPath.get(path) : undefined;
184
+ const context = createEditorCommandContext(file);
185
+
186
+ return commandMenuItemsToContextMenuItems(
187
+ resolveCommandMenuItems({
188
+ context,
189
+ surface: WORKBENCH_COMMAND_SURFACE_EDITOR,
190
+ entries: file ? editorTabMenuEntries : editorTabListMenuEntries,
191
+ registry: editorCommandRegistry,
192
+ }),
193
+ (commandId) => executeCommand(editorCommandRegistry, commandId, context),
194
+ );
195
+ };
196
+
197
+ const handleConfirmDelete = () => {
198
+ if (deletePath) {
199
+ onDeletePath?.(deletePath);
200
+ }
201
+ setDeletePath(null);
202
+ };
203
+ const selectedIsDirty = Boolean(selectedFile && isDirty(selectedFile.path, selectedFile.content));
204
+
205
+ return (
206
+ <Panel className="workspace-panel">
207
+ <PanelBody className="workspace-panel__body">
208
+ {openFiles.length > 0 ? (
209
+ <div
210
+ className="workspace-editor"
211
+ onKeyDown={(event) => {
212
+ if (!selectedFile) return;
213
+ if (event.key.toLowerCase() !== 's' || (!event.ctrlKey && !event.metaKey)) return;
214
+
215
+ event.preventDefault();
216
+ saveFile(selectedFile.path, selectedContent);
217
+ }}
218
+ >
219
+ <div className="workspace-editor__tabbar">
220
+ <div
221
+ aria-label="Open workspace files"
222
+ className="workspace-editor__tabs ui-workbench-scrollbar--hidden"
223
+ role="tablist"
224
+ onContextMenu={(event) => handleTabContextMenu(event, null)}
225
+ >
226
+ {openFiles.map((file) => {
227
+ const isActive = file.path === selectedFile?.path;
228
+ const isDirtyValue = isDirty(file.path, file.content);
229
+
230
+ return (
231
+ <div
232
+ key={file.path}
233
+ className={`workspace-editor__tab${isActive ? ' workspace-editor__tab--active' : ''}`}
234
+ title={file.path}
235
+ onContextMenu={(event) => handleTabContextMenu(event, file.path)}
236
+ >
237
+ <button
238
+ aria-selected={isActive}
239
+ className="workspace-editor__tab-button"
240
+ role="tab"
241
+ type="button"
242
+ onClick={() => onSelectedPathChange(file.path)}
243
+ >
244
+ <WorkspaceFileIcon mimeType={file.mimeType} path={file.path} />
245
+ <span className="workspace-editor__path">{fileNameOfPath(file.path)}</span>
246
+ {isDirtyValue ? (
247
+ <span className="workspace-editor__dirty" aria-label="Unsaved changes" />
248
+ ) : null}
249
+ </button>
250
+ <IconButton
251
+ className="workspace-editor__tab-close"
252
+ icon="codicon-close"
253
+ label={`Close ${fileNameOfPath(file.path)}`}
254
+ onClick={() => onClosePath?.(file.path)}
255
+ />
256
+ </div>
257
+ );
258
+ })}
259
+ </div>
260
+
261
+ {selectedFile && renderTabActions ? (
262
+ <div className="workspace-editor__tab-actions">
263
+ {renderTabActions({
264
+ content: selectedContent,
265
+ file: selectedFile,
266
+ isDirty: selectedIsDirty,
267
+ onDelete: () => setDeletePath(selectedFile.path),
268
+ onDiscard: () => discardFile(selectedFile),
269
+ onSave: () => saveFile(selectedFile.path, selectedContent),
270
+ theme,
271
+ })}
272
+ </div>
273
+ ) : null}
274
+ </div>
275
+
276
+ {selectedFile ? (
277
+ renderEditor ? (
278
+ renderEditor({
279
+ content: selectedContent,
280
+ file: selectedFile,
281
+ isDirty: selectedIsDirty,
282
+ onChange: (content) => updateDraft(selectedFile.path, content),
283
+ onDiscard: () => discardFile(selectedFile),
284
+ onSave: (content) => saveFile(selectedFile.path, content),
285
+ theme,
286
+ })
287
+ ) : (
288
+ <WorkspaceEditor
289
+ key={selectedFile.path}
290
+ file={selectedFile}
291
+ readOnly={false}
292
+ showHeader={false}
293
+ theme={theme}
294
+ value={selectedContent}
295
+ onChange={(content) => updateDraft(selectedFile.path, content)}
296
+ onSave={(content) => saveFile(selectedFile.path, content)}
297
+ />
298
+ )
299
+ ) : (
300
+ <EmptyState icon="codicon-open-preview">{emptyLabel}</EmptyState>
301
+ )}
302
+ </div>
303
+ ) : files.length === 0 ? (
304
+ <EmptyState icon="codicon-folder">No workspace files</EmptyState>
305
+ ) : (
306
+ <EmptyState icon="codicon-open-preview">{emptyLabel}</EmptyState>
307
+ )}
308
+ </PanelBody>
309
+
310
+ {deletePath ? (
311
+ <ConfirmDialog
312
+ closeLabel="Close confirmation"
313
+ confirmLabel="Delete"
314
+ detail={<code>{deletePath}</code>}
315
+ message="Delete this workspace file?"
316
+ title="Delete File"
317
+ variant="danger"
318
+ onCancel={() => setDeletePath(null)}
319
+ onConfirm={handleConfirmDelete}
320
+ />
321
+ ) : null}
322
+
323
+ {tabContextMenu ? (
324
+ <ContextMenu
325
+ ariaLabel="Editor tab menu"
326
+ items={createTabContextItems(tabContextMenu.path)}
327
+ x={tabContextMenu.x}
328
+ y={tabContextMenu.y}
329
+ onClose={() => setTabContextMenu(null)}
330
+ />
331
+ ) : null}
332
+ </Panel>
333
+ );
334
+ }