@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,52 +1,52 @@
1
- import type { ComponentPropsWithoutRef, ReactNode } from 'react';
2
- import { cx } from '../utils/cx';
3
-
4
- export interface ActivityBarItem {
5
- active?: boolean;
6
- disabled?: boolean;
7
- icon: ReactNode;
8
- id: string;
9
- label: string;
10
- title?: string;
11
- }
12
-
13
- export interface ActivityBarProps extends Omit<ComponentPropsWithoutRef<'nav'>, 'children'> {
14
- items: ActivityBarItem[];
15
- onItemActivate?: (item: ActivityBarItem) => void;
16
- secondaryItems?: ActivityBarItem[];
17
- }
18
-
19
- export function ActivityBar({
20
- 'aria-label': ariaLabel = 'Activity bar',
21
- className,
22
- items,
23
- onItemActivate,
24
- secondaryItems = [],
25
- ...props
26
- }: ActivityBarProps) {
27
- const renderItem = (item: ActivityBarItem) => (
28
- <button
29
- key={item.id}
30
- type="button"
31
- aria-label={item.label}
32
- aria-pressed={item.active}
33
- className={cx(
34
- 'ui-workbench-activity-bar__item',
35
- item.active && 'ui-workbench-activity-bar__item--active',
36
- )}
37
- disabled={item.disabled}
38
- title={item.title ?? item.label}
39
- onClick={() => onItemActivate?.(item)}
40
- >
41
- <span className="ui-workbench-activity-bar__icon">{item.icon}</span>
42
- </button>
43
- );
44
-
45
- return (
46
- <nav aria-label={ariaLabel} className={cx('ui-workbench-activity-bar', className)} {...props}>
47
- {items.map(renderItem)}
48
- {secondaryItems.length ? <span className="ui-workbench-activity-bar__spacer" /> : null}
49
- {secondaryItems.map(renderItem)}
50
- </nav>
51
- );
52
- }
1
+ import type { ComponentPropsWithoutRef, ReactNode } from 'react';
2
+ import { cx } from '../utils/cx';
3
+
4
+ export interface ActivityBarItem {
5
+ active?: boolean;
6
+ disabled?: boolean;
7
+ icon: ReactNode;
8
+ id: string;
9
+ label: string;
10
+ title?: string;
11
+ }
12
+
13
+ export interface ActivityBarProps extends Omit<ComponentPropsWithoutRef<'nav'>, 'children'> {
14
+ items: ActivityBarItem[];
15
+ onItemActivate?: (item: ActivityBarItem) => void;
16
+ secondaryItems?: ActivityBarItem[];
17
+ }
18
+
19
+ export function ActivityBar({
20
+ 'aria-label': ariaLabel = 'Activity bar',
21
+ className,
22
+ items,
23
+ onItemActivate,
24
+ secondaryItems = [],
25
+ ...props
26
+ }: ActivityBarProps) {
27
+ const renderItem = (item: ActivityBarItem) => (
28
+ <button
29
+ key={item.id}
30
+ type="button"
31
+ aria-label={item.label}
32
+ aria-pressed={item.active}
33
+ className={cx(
34
+ 'ui-workbench-activity-bar__item',
35
+ item.active && 'ui-workbench-activity-bar__item--active',
36
+ )}
37
+ disabled={item.disabled}
38
+ title={item.title ?? item.label}
39
+ onClick={() => onItemActivate?.(item)}
40
+ >
41
+ <span className="ui-workbench-activity-bar__icon">{item.icon}</span>
42
+ </button>
43
+ );
44
+
45
+ return (
46
+ <nav aria-label={ariaLabel} className={cx('ui-workbench-activity-bar', className)} {...props}>
47
+ {items.map(renderItem)}
48
+ {secondaryItems.length ? <span className="ui-workbench-activity-bar__spacer" /> : null}
49
+ {secondaryItems.map(renderItem)}
50
+ </nav>
51
+ );
52
+ }
@@ -0,0 +1,397 @@
1
+ import { useState, type ComponentPropsWithRef, type ReactNode } from 'react';
2
+ import { EmptyState } from '../primitives/EmptyState';
3
+ import { IconButton } from '../primitives/IconButton';
4
+ import { cx } from '../utils/cx';
5
+ import { SplitView } from './SplitView';
6
+
7
+ export type WorkbenchArtifactMode = 'code' | 'preview' | 'split';
8
+
9
+ export interface WorkbenchArtifactDescriptor {
10
+ artifactKind?: string;
11
+ content?: unknown;
12
+ extension?: string;
13
+ id: string;
14
+ metadata?: Record<string, unknown>;
15
+ mimeType?: string;
16
+ name?: string;
17
+ path?: string;
18
+ title?: ReactNode;
19
+ }
20
+
21
+ export type WorkbenchPreviewRendererMatchReason =
22
+ | 'artifact-kind'
23
+ | 'custom'
24
+ | 'extension'
25
+ | 'fallback'
26
+ | 'mime-type';
27
+
28
+ export interface WorkbenchPreviewRendererMatch {
29
+ priority: number;
30
+ reason: WorkbenchPreviewRendererMatchReason;
31
+ renderer: WorkbenchPreviewRenderer;
32
+ score: number;
33
+ }
34
+
35
+ export interface WorkbenchPreviewRendererContext {
36
+ artifact: WorkbenchArtifactDescriptor;
37
+ match?: WorkbenchPreviewRendererMatch | undefined;
38
+ renderer?: WorkbenchPreviewRenderer | undefined;
39
+ }
40
+
41
+ export interface WorkbenchPreviewRenderer {
42
+ artifactKinds?: readonly string[];
43
+ canRender?: (artifact: WorkbenchArtifactDescriptor) => boolean;
44
+ extensions?: readonly string[];
45
+ fallback?: boolean;
46
+ id: string;
47
+ label?: string;
48
+ mimeTypes?: readonly string[];
49
+ priority?: number;
50
+ render: (
51
+ artifact: WorkbenchArtifactDescriptor,
52
+ context: WorkbenchPreviewRendererContext,
53
+ ) => ReactNode;
54
+ }
55
+
56
+ export type WorkbenchArtifactShellRenderCode = (artifact: WorkbenchArtifactDescriptor) => ReactNode;
57
+
58
+ export type WorkbenchArtifactShellRenderPreview = (
59
+ artifact: WorkbenchArtifactDescriptor,
60
+ context: WorkbenchPreviewRendererContext,
61
+ ) => ReactNode;
62
+
63
+ export interface WorkbenchArtifactModeControlsProps extends ComponentPropsWithRef<'div'> {
64
+ codeLabel?: string | undefined;
65
+ mode: WorkbenchArtifactMode;
66
+ onModeChange: (mode: WorkbenchArtifactMode) => void;
67
+ previewLabel?: string | undefined;
68
+ splitLabel?: string | undefined;
69
+ }
70
+
71
+ function normalizeToken(value: string | undefined) {
72
+ return value?.trim().toLocaleLowerCase() ?? '';
73
+ }
74
+
75
+ function normalizeExtension(value: string | undefined) {
76
+ const normalized = normalizeToken(value);
77
+ return normalized.startsWith('.') ? normalized.slice(1) : normalized;
78
+ }
79
+
80
+ function extensionFromPath(path: string | undefined) {
81
+ const fileName = path?.split(/[\\/]/).pop() ?? '';
82
+ const dotIndex = fileName.lastIndexOf('.');
83
+ return dotIndex > 0 ? fileName.slice(dotIndex + 1) : '';
84
+ }
85
+
86
+ function matchesMimeType(candidate: string, mimeType: string) {
87
+ const normalizedCandidate = normalizeToken(candidate);
88
+ const normalizedMimeType = normalizeToken(mimeType);
89
+ if (!normalizedCandidate || !normalizedMimeType) return false;
90
+ if (normalizedCandidate === normalizedMimeType) return true;
91
+ if (!normalizedCandidate.endsWith('/*')) return false;
92
+
93
+ return normalizedMimeType.startsWith(normalizedCandidate.slice(0, -1));
94
+ }
95
+
96
+ export function getWorkbenchArtifactExtension(artifact: WorkbenchArtifactDescriptor) {
97
+ return (
98
+ normalizeExtension(artifact.extension) || normalizeExtension(extensionFromPath(artifact.path))
99
+ );
100
+ }
101
+
102
+ export function getWorkbenchArtifactTitle(artifact: WorkbenchArtifactDescriptor): ReactNode {
103
+ return artifact.title ?? artifact.name ?? artifact.path ?? artifact.id;
104
+ }
105
+
106
+ export function formatWorkbenchArtifactContent(content: unknown): string {
107
+ if (content === undefined || content === null) return '';
108
+ if (typeof content === 'string') return content;
109
+
110
+ try {
111
+ return JSON.stringify(content, null, 2);
112
+ } catch {
113
+ return String(content);
114
+ }
115
+ }
116
+
117
+ export function getWorkbenchPreviewRendererMatch(
118
+ artifact: WorkbenchArtifactDescriptor,
119
+ renderer: WorkbenchPreviewRenderer,
120
+ ): WorkbenchPreviewRendererMatch | undefined {
121
+ const customMatch = renderer.canRender?.(artifact);
122
+ if (customMatch === false) return undefined;
123
+
124
+ const priority = renderer.priority ?? 0;
125
+ const artifactKind = normalizeToken(artifact.artifactKind);
126
+ const mimeType = normalizeToken(artifact.mimeType);
127
+ const extension = getWorkbenchArtifactExtension(artifact);
128
+
129
+ if (customMatch) {
130
+ return { priority, reason: 'custom', renderer, score: 100 };
131
+ }
132
+
133
+ if (
134
+ artifactKind &&
135
+ renderer.artifactKinds?.some((candidate) => normalizeToken(candidate) === artifactKind)
136
+ ) {
137
+ return { priority, reason: 'artifact-kind', renderer, score: 80 };
138
+ }
139
+
140
+ if (mimeType && renderer.mimeTypes?.some((candidate) => matchesMimeType(candidate, mimeType))) {
141
+ return { priority, reason: 'mime-type', renderer, score: 70 };
142
+ }
143
+
144
+ if (
145
+ extension &&
146
+ renderer.extensions?.some((candidate) => normalizeExtension(candidate) === extension)
147
+ ) {
148
+ return { priority, reason: 'extension', renderer, score: 60 };
149
+ }
150
+
151
+ if (renderer.fallback) {
152
+ return { priority, reason: 'fallback', renderer, score: 1 };
153
+ }
154
+
155
+ return undefined;
156
+ }
157
+
158
+ export function selectWorkbenchPreviewRenderer(
159
+ artifact: WorkbenchArtifactDescriptor,
160
+ renderers: readonly WorkbenchPreviewRenderer[],
161
+ ): WorkbenchPreviewRendererMatch | undefined {
162
+ return renderers
163
+ .map((renderer) => getWorkbenchPreviewRendererMatch(artifact, renderer))
164
+ .filter((match): match is WorkbenchPreviewRendererMatch => Boolean(match))
165
+ .sort(
166
+ (left, right) =>
167
+ right.score - left.score ||
168
+ right.priority - left.priority ||
169
+ left.renderer.id.localeCompare(right.renderer.id),
170
+ )[0];
171
+ }
172
+
173
+ export function getWorkbenchPreviewRenderer(
174
+ artifact: WorkbenchArtifactDescriptor,
175
+ renderers: readonly WorkbenchPreviewRenderer[],
176
+ ) {
177
+ return selectWorkbenchPreviewRenderer(artifact, renderers)?.renderer;
178
+ }
179
+
180
+ function defaultCodePane(artifact: WorkbenchArtifactDescriptor) {
181
+ return (
182
+ <pre className="ui-workbench-artifact-shell__code ui-workbench-scrollbar">
183
+ <code>{formatWorkbenchArtifactContent(artifact.content)}</code>
184
+ </pre>
185
+ );
186
+ }
187
+
188
+ export function WorkbenchArtifactModeControls({
189
+ className,
190
+ codeLabel = 'Code',
191
+ mode,
192
+ onModeChange,
193
+ previewLabel = 'Preview',
194
+ splitLabel = 'Split',
195
+ ...props
196
+ }: WorkbenchArtifactModeControlsProps) {
197
+ return (
198
+ <div className={cx('ui-workbench-artifact-shell__modes', className)} {...props}>
199
+ <IconButton
200
+ aria-pressed={mode === 'code'}
201
+ className={cx(
202
+ 'ui-workbench-artifact-shell__mode',
203
+ mode === 'code' && 'ui-workbench-artifact-shell__mode--active',
204
+ )}
205
+ icon="codicon-code"
206
+ label={codeLabel}
207
+ onClick={() => onModeChange('code')}
208
+ />
209
+ <IconButton
210
+ aria-pressed={mode === 'preview'}
211
+ className={cx(
212
+ 'ui-workbench-artifact-shell__mode',
213
+ mode === 'preview' && 'ui-workbench-artifact-shell__mode--active',
214
+ )}
215
+ icon="codicon-open-preview"
216
+ label={previewLabel}
217
+ onClick={() => onModeChange('preview')}
218
+ />
219
+ <IconButton
220
+ aria-pressed={mode === 'split'}
221
+ className={cx(
222
+ 'ui-workbench-artifact-shell__mode',
223
+ mode === 'split' && 'ui-workbench-artifact-shell__mode--active',
224
+ )}
225
+ icon="codicon-split-horizontal"
226
+ label={splitLabel}
227
+ onClick={() => onModeChange('split')}
228
+ />
229
+ </div>
230
+ );
231
+ }
232
+
233
+ export interface WorkbenchArtifactPreviewProps extends ComponentPropsWithRef<'div'> {
234
+ artifact: WorkbenchArtifactDescriptor;
235
+ renderPreview?: WorkbenchArtifactShellRenderPreview | undefined;
236
+ renderers?: readonly WorkbenchPreviewRenderer[] | undefined;
237
+ unsupportedLabel?: ReactNode | undefined;
238
+ }
239
+
240
+ export function WorkbenchArtifactPreview({
241
+ artifact,
242
+ className,
243
+ renderPreview,
244
+ renderers = [],
245
+ unsupportedLabel = 'Preview is not available for this artifact',
246
+ ...props
247
+ }: WorkbenchArtifactPreviewProps) {
248
+ const match = selectWorkbenchPreviewRenderer(artifact, renderers);
249
+ const context: WorkbenchPreviewRendererContext = {
250
+ artifact,
251
+ match,
252
+ renderer: match?.renderer,
253
+ };
254
+ const preview =
255
+ renderPreview !== undefined
256
+ ? renderPreview(artifact, context)
257
+ : match?.renderer.render(artifact, context);
258
+
259
+ return (
260
+ <div className={cx('ui-workbench-artifact-preview', className)} {...props}>
261
+ {preview !== undefined && preview !== null ? (
262
+ preview
263
+ ) : (
264
+ <EmptyState compact icon="codicon-open-preview">
265
+ {unsupportedLabel}
266
+ </EmptyState>
267
+ )}
268
+ </div>
269
+ );
270
+ }
271
+
272
+ export interface WorkbenchArtifactShellProps extends Omit<
273
+ ComponentPropsWithRef<'div'>,
274
+ 'children'
275
+ > {
276
+ artifact?: WorkbenchArtifactDescriptor | undefined;
277
+ codeLabel?: string | undefined;
278
+ defaultMode?: WorkbenchArtifactMode | undefined;
279
+ emptyLabel?: ReactNode | undefined;
280
+ mode?: WorkbenchArtifactMode | undefined;
281
+ onModeChange?: ((mode: WorkbenchArtifactMode) => void) | undefined;
282
+ previewLabel?: string | undefined;
283
+ previewRenderers?: readonly WorkbenchPreviewRenderer[] | undefined;
284
+ renderCode?: WorkbenchArtifactShellRenderCode | undefined;
285
+ renderPreview?: WorkbenchArtifactShellRenderPreview | undefined;
286
+ showHeader?: boolean | undefined;
287
+ splitLabel?: string | undefined;
288
+ unsupportedPreviewLabel?: ReactNode | undefined;
289
+ }
290
+
291
+ export function WorkbenchArtifactShell({
292
+ artifact,
293
+ className,
294
+ codeLabel = 'Code',
295
+ defaultMode = 'code',
296
+ emptyLabel = 'No artifact selected',
297
+ mode,
298
+ onModeChange,
299
+ previewLabel = 'Preview',
300
+ previewRenderers,
301
+ renderCode,
302
+ renderPreview,
303
+ showHeader = true,
304
+ splitLabel = 'Split',
305
+ unsupportedPreviewLabel,
306
+ ...props
307
+ }: WorkbenchArtifactShellProps) {
308
+ const [uncontrolledMode, setUncontrolledMode] = useState(defaultMode);
309
+ const resolvedMode = mode ?? uncontrolledMode;
310
+
311
+ const setMode = (nextMode: WorkbenchArtifactMode) => {
312
+ if (mode === undefined) {
313
+ setUncontrolledMode(nextMode);
314
+ }
315
+ onModeChange?.(nextMode);
316
+ };
317
+
318
+ if (!artifact) {
319
+ return (
320
+ <div className={cx('ui-workbench-artifact-shell', className)} {...props}>
321
+ <EmptyState icon="codicon-open-preview">{emptyLabel}</EmptyState>
322
+ </div>
323
+ );
324
+ }
325
+
326
+ const codePane = renderCode ? renderCode(artifact) : defaultCodePane(artifact);
327
+ const previewPane = (
328
+ <WorkbenchArtifactPreview
329
+ artifact={artifact}
330
+ renderPreview={renderPreview}
331
+ renderers={previewRenderers}
332
+ unsupportedLabel={unsupportedPreviewLabel}
333
+ />
334
+ );
335
+ const title = getWorkbenchArtifactTitle(artifact);
336
+ const meta =
337
+ artifact.mimeType ?? artifact.artifactKind ?? getWorkbenchArtifactExtension(artifact);
338
+
339
+ return (
340
+ <div
341
+ className={cx('ui-workbench-artifact-shell', className)}
342
+ data-header={showHeader ? 'visible' : 'hidden'}
343
+ data-mode={resolvedMode}
344
+ {...props}
345
+ >
346
+ {showHeader ? (
347
+ <div className="ui-workbench-artifact-shell__header">
348
+ <div className="ui-workbench-artifact-shell__title-group">
349
+ <span className="ui-workbench-artifact-shell__title">{title}</span>
350
+ {artifact.path ? (
351
+ <span className="ui-workbench-artifact-shell__path">{artifact.path}</span>
352
+ ) : null}
353
+ </div>
354
+ <div className="ui-workbench-artifact-shell__meta">
355
+ {meta ? <span className="ui-workbench-artifact-shell__chip">{meta}</span> : null}
356
+ <WorkbenchArtifactModeControls
357
+ codeLabel={codeLabel}
358
+ mode={resolvedMode}
359
+ previewLabel={previewLabel}
360
+ splitLabel={splitLabel}
361
+ onModeChange={setMode}
362
+ />
363
+ </div>
364
+ </div>
365
+ ) : null}
366
+ <div className="ui-workbench-artifact-shell__body">
367
+ {resolvedMode === 'code' ? (
368
+ <section className="ui-workbench-artifact-shell__pane" aria-label={codeLabel}>
369
+ {codePane}
370
+ </section>
371
+ ) : null}
372
+ {resolvedMode === 'preview' ? (
373
+ <section className="ui-workbench-artifact-shell__pane" aria-label={previewLabel}>
374
+ {previewPane}
375
+ </section>
376
+ ) : null}
377
+ {resolvedMode === 'split' ? (
378
+ <SplitView
379
+ className="ui-workbench-artifact-shell__split"
380
+ defaultPrimarySizePercent={50}
381
+ minPrimarySizePercent={20}
382
+ primary={
383
+ <section className="ui-workbench-artifact-shell__pane" aria-label={codeLabel}>
384
+ {codePane}
385
+ </section>
386
+ }
387
+ secondary={
388
+ <section className="ui-workbench-artifact-shell__pane" aria-label={previewLabel}>
389
+ {previewPane}
390
+ </section>
391
+ }
392
+ />
393
+ ) : null}
394
+ </div>
395
+ </div>
396
+ );
397
+ }