@workbench-kit/react 0.0.1-prototype.0 → 0.0.2-prototype.0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +33 -9
- package/src/index.ts +545 -43
- package/src/jdw/JdwPreview.tsx +38 -0
- package/src/jdw/JsonCodeEditorPane.tsx +274 -0
- package/src/jdw/builtins/renderBuiltinWidgetLeaf.tsx +29 -0
- package/src/jdw/builtins/renderBuiltinWidgetNode.tsx +126 -0
- package/src/jdw/createBuiltinJdwRegistry.ts +111 -0
- package/src/jdw/cssRenderBackend.tsx +134 -0
- package/src/jdw/document.ts +38 -0
- package/src/jdw/fixtures/jdw-fixtures.ts +52 -0
- package/src/jdw/fixtures/jdw-sample-screens.ts +522 -0
- package/src/jdw/index.ts +45 -0
- package/src/jdw/parse.ts +1 -0
- package/src/jdw/renderJdw.tsx +32 -0
- package/src/json-config/JsonConfigWorkbench.tsx +316 -0
- package/src/json-config/index.ts +6 -0
- package/src/json-config/json-config-editor-state.ts +42 -0
- package/src/layout/Panel.tsx +146 -1
- package/src/layout/SideBarViewFrame.tsx +367 -328
- package/src/layout/WorkbenchCanvas.tsx +849 -0
- package/src/layout/WorkbenchFullscreen.tsx +368 -0
- package/src/layout/WorkbenchLayout.tsx +5 -0
- package/src/layout/WorkbenchLayoutBase.tsx +294 -0
- package/src/layout/WorkbenchPropertyPanel.tsx +497 -0
- package/src/layout/WorkbenchSidebarActions.tsx +274 -0
- package/src/layout/WorkbenchTree.tsx +351 -0
- package/src/layout/layoutHelpers.ts +25 -0
- package/src/modal/ConfirmDialog.tsx +61 -53
- package/src/modal/Modal.tsx +295 -35
- package/src/overlay/ContextMenu.tsx +146 -145
- package/src/primitives/AbsoluteBox.tsx +46 -0
- package/src/primitives/Badge.tsx +12 -12
- package/src/primitives/Button.tsx +44 -14
- package/src/primitives/Checkbox.tsx +11 -5
- package/src/primitives/EmptyState.tsx +27 -26
- package/src/primitives/Field.tsx +38 -38
- package/src/primitives/IconButton.tsx +5 -2
- package/src/primitives/List.tsx +111 -0
- package/src/primitives/NumberInput.tsx +38 -0
- package/src/primitives/ScrollArea.tsx +42 -0
- package/src/primitives/Select.tsx +1 -12
- package/src/primitives/StatusBar.tsx +46 -0
- package/src/primitives/TextArea.tsx +36 -0
- package/src/primitives/TextInput.tsx +11 -2
- package/src/primitives/WorkbenchChrome.tsx +424 -0
- package/src/primitives/WorkbenchEditor.tsx +394 -0
- package/src/primitives/index.ts +105 -0
- package/src/primitives/select/Select.tsx +266 -0
- package/src/primitives/select/index.ts +2 -0
- package/src/primitives/select/options.ts +38 -0
- package/src/primitives/select/overlay.ts +63 -0
- package/src/primitives/select/select.app.css +9 -0
- package/src/primitives/select/select.css +140 -0
- package/src/primitives/select/types.ts +18 -0
- package/src/primitives/styles.css +955 -0
- package/src/scrollbars.css +69 -0
- package/src/styles.css +7738 -1762
- package/src/utils/codicon.ts +17 -0
- package/src/widget-asset/WidgetAssetEditor.tsx +140 -0
- package/src/widget-asset/WidgetAssetMetadataForm.tsx +82 -0
- package/src/widget-asset/WidgetAssetModeControls.tsx +42 -0
- package/src/widget-asset/WidgetAssetSourceEditor.tsx +50 -0
- package/src/widget-asset/WidgetAssetWorkbench.tsx +112 -0
- package/src/widget-asset/index.ts +17 -0
- package/src/widget-asset/widget-asset-document.ts +37 -0
- package/src/widget-asset/widget-asset-mode.ts +3 -0
- package/src/widget-studio/builtin-widget-asset-catalog.ts +12 -0
- package/src/widget-studio/create-widget-studio-workspace-editor.tsx +79 -0
- package/src/widget-studio/index.ts +7 -0
- package/src/widget-studio/resolve-widget-studio-asset-catalog.ts +26 -0
- package/src/widget-tree/WidgetAssetPalette.tsx +77 -0
- package/src/widget-tree/WidgetInspectorPanel.tsx +266 -0
- package/src/widget-tree/WidgetSourceEditor.tsx +56 -0
- package/src/widget-tree/WidgetTreeLab.tsx +272 -0
- package/src/widget-tree/WidgetTreeModeControls.tsx +44 -0
- package/src/widget-tree/WidgetTreeSidePanel.tsx +49 -0
- package/src/widget-tree/WidgetTreeView.tsx +97 -0
- package/src/widget-tree/WidgetTreeWorkbench.tsx +111 -0
- package/src/widget-tree/WidgetTreeWorkspaceShell.tsx +154 -0
- package/src/widget-tree/demo-registry.ts +32 -0
- package/src/widget-tree/demo-widget-assets.ts +4 -0
- package/src/widget-tree/index.ts +27 -0
- package/src/widget-tree/widget-tree-document.ts +20 -0
- package/src/widget-tree/widget-tree-layout.ts +31 -0
- package/src/widget-tree/widget-tree-mode.ts +7 -0
- package/src/widget-tree/widget-tree-play-helpers.ts +74 -0
- package/src/workbench/ActivityBar.tsx +52 -52
- package/src/workbench/ArtifactShell.tsx +397 -0
- package/src/workbench/CommandPalette.tsx +819 -0
- package/src/workbench/ConfirmationFlow.tsx +245 -0
- package/src/workbench/MarkdownPreview.tsx +151 -0
- package/src/workbench/ShortcutCommandBridge.tsx +368 -0
- package/src/workbench/SplitView.tsx +136 -136
- package/src/workbench/StatusBar.tsx +178 -123
- package/src/workbench/StructuredArtifactEditor.tsx +205 -0
- package/src/workbench/Timeline.tsx +302 -0
- package/src/workbench/WorkbenchCanvasShell.tsx +941 -0
- package/src/workbench/WorkbenchShell.tsx +74 -70
- package/src/workbench/WorkbenchStandaloneShell.tsx +318 -291
- package/src/workbench/auth/WorkbenchAuthGate.tsx +61 -0
- package/src/workbench/auth/WorkbenchLoginView.tsx +635 -0
- package/src/workbench/auth/index.ts +19 -0
- package/src/workbench/chat/ChatComposer.tsx +165 -148
- package/src/workbench/chat/ChatMessageItem.tsx +10 -1
- package/src/workbench/chat/index.ts +9 -0
- package/src/workbench/chat/slashCommand.ts +28 -0
- package/src/workbench/chat/useChatRuntimeState.ts +76 -0
- package/src/workbench/chat/useSlashCommandSuggest.ts +101 -0
- package/src/workbench/commands.ts +520 -490
- package/src/workbench/index.ts +443 -98
- package/src/workbench/keyboard.ts +53 -0
- package/src/workbench/schema/index.ts +48 -0
- package/src/workbench/schema/workbenchDocument.ts +31 -0
- package/src/workbench/schema/workbenchDocumentActions.ts +6 -0
- package/src/workbench/schema/workbenchDocumentAdapter.ts +9 -0
- package/src/workbench/schema/workbenchDocumentPatch.ts +13 -0
- package/src/workbench/schema/workbenchDocumentRenderer.tsx +702 -0
- package/src/workbench/settings/NavigationPanel.tsx +47 -0
- package/src/workbench/settings/SchemaForm.tsx +420 -0
- package/src/workbench/settings/SectionedPanel.tsx +415 -0
- package/src/workbench/settings/StructuredDataForm.tsx +539 -0
- package/src/workbench/settings/StructuredDataSchemaPanel.tsx +559 -0
- package/src/workbench/settings/StructuredDataSchemaPanelEmbed.tsx +21 -0
- package/src/workbench/settings/StructuredDataSchemaPanelFrame.tsx +76 -0
- package/src/workbench/settings/StructuredDataTableView.tsx +127 -0
- package/src/workbench/settings/WorkbenchPanelRegion.tsx +46 -0
- package/src/workbench/settings/WorkbenchSettingsModal.tsx +21 -12
- package/src/workbench/settings/WorkbenchSettingsNav.tsx +38 -22
- package/src/workbench/settings/index.ts +135 -0
- package/src/workbench/settings/panel-region.css +21 -0
- package/src/workbench/settings/sectionedPanelScrollSpy.ts +143 -0
- package/src/workbench/settings/structured-data-schema-panel.css +330 -0
- package/src/workbench/settings/structured-data-table-view.css +85 -0
- package/src/workbench/settings/structuredDataSchema.ts +886 -0
- package/src/workbench/shell.ts +15 -0
- package/src/workbench/shellState.ts +203 -203
- package/src/workbench/standalone.ts +101 -94
- package/src/workbench/status.ts +129 -0
- package/src/workbench/theme.ts +44 -0
- package/src/workbench/workspace/MultiProviderExplorer.tsx +625 -0
- package/src/workbench/workspace/WorkspaceDraftsContext.tsx +111 -0
- package/src/workbench/workspace/WorkspaceEditor.tsx +47 -12
- package/src/workbench/workspace/WorkspaceEditorPanel.tsx +334 -353
- package/src/workbench/workspace/WorkspaceExplorer.tsx +532 -524
- package/src/workbench/workspace/WorkspaceFileIcon.tsx +32 -2
- package/src/workbench/workspace/WorkspaceSearchPanel.tsx +113 -113
- package/src/workbench/workspace/WorkspaceSearchResults.tsx +47 -47
- package/src/workbench/workspace/index.ts +156 -98
- package/src/workbench/workspace/mimeType.ts +45 -0
- package/src/workbench/workspace/path.ts +10 -10
- package/src/workbench/workspace/search.ts +6 -6
- package/src/workbench/workspace/tree.ts +1 -1
- package/src/workbench/workspace/types.ts +10 -10
- package/src/workbench/workspace/useVirtualWorkspace.ts +111 -98
- package/src/workbench/workspace/workspaceJsonDiagnostics.ts +83 -0
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { useState, type FormEvent, type ReactNode } from 'react';
|
|
2
|
+
import { Modal } from '../modal/Modal';
|
|
3
|
+
import { Button } from '../primitives/Button';
|
|
4
|
+
import { cx } from '../utils/cx';
|
|
5
|
+
import {
|
|
6
|
+
getWorkbenchStatusLabel,
|
|
7
|
+
isWorkbenchStatusBusy,
|
|
8
|
+
isWorkbenchStatusDisabled,
|
|
9
|
+
type WorkbenchStatus,
|
|
10
|
+
} from './status';
|
|
11
|
+
|
|
12
|
+
export type WorkbenchConfirmationVariant = 'default' | 'danger';
|
|
13
|
+
|
|
14
|
+
export type WorkbenchConfirmationSideEffect =
|
|
15
|
+
| 'external-write'
|
|
16
|
+
| 'none'
|
|
17
|
+
| 'workspace-write'
|
|
18
|
+
| (string & {});
|
|
19
|
+
|
|
20
|
+
export type WorkbenchConfirmationReason = 'cancel' | 'close' | 'confirm';
|
|
21
|
+
|
|
22
|
+
export interface WorkbenchConfirmationAction {
|
|
23
|
+
cancelLabel?: ReactNode;
|
|
24
|
+
closeLabel?: string;
|
|
25
|
+
confirmLabel?: ReactNode;
|
|
26
|
+
danger?: boolean;
|
|
27
|
+
detail?: ReactNode;
|
|
28
|
+
disabled?: boolean;
|
|
29
|
+
disabledReason?: ReactNode;
|
|
30
|
+
id: string;
|
|
31
|
+
message: ReactNode;
|
|
32
|
+
metadata?: Record<string, unknown>;
|
|
33
|
+
pendingLabel?: ReactNode;
|
|
34
|
+
sideEffect?: WorkbenchConfirmationSideEffect;
|
|
35
|
+
status?: WorkbenchStatus;
|
|
36
|
+
title: ReactNode;
|
|
37
|
+
variant?: WorkbenchConfirmationVariant;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface WorkbenchConfirmationContext {
|
|
41
|
+
action: WorkbenchConfirmationAction;
|
|
42
|
+
actionId: string;
|
|
43
|
+
pending: boolean;
|
|
44
|
+
reason: WorkbenchConfirmationReason;
|
|
45
|
+
sideEffect: WorkbenchConfirmationSideEffect;
|
|
46
|
+
status: WorkbenchStatus;
|
|
47
|
+
variant: WorkbenchConfirmationVariant;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface WorkbenchConfirmationConfirmContext extends WorkbenchConfirmationContext {
|
|
51
|
+
event: FormEvent<HTMLFormElement>;
|
|
52
|
+
reason: 'confirm';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface WorkbenchConfirmationCancelContext extends WorkbenchConfirmationContext {
|
|
56
|
+
reason: 'cancel';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface WorkbenchConfirmationCloseContext extends WorkbenchConfirmationContext {
|
|
60
|
+
reason: 'close';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface WorkbenchConfirmationFlowProps {
|
|
64
|
+
action?: WorkbenchConfirmationAction | null;
|
|
65
|
+
allowCancelWhilePending?: boolean;
|
|
66
|
+
className?: string;
|
|
67
|
+
onCancel?: (
|
|
68
|
+
action: WorkbenchConfirmationAction,
|
|
69
|
+
context: WorkbenchConfirmationCancelContext,
|
|
70
|
+
) => void;
|
|
71
|
+
onClose?: (
|
|
72
|
+
action: WorkbenchConfirmationAction,
|
|
73
|
+
context: WorkbenchConfirmationCloseContext,
|
|
74
|
+
) => void;
|
|
75
|
+
onConfirm?: (
|
|
76
|
+
action: WorkbenchConfirmationAction,
|
|
77
|
+
context: WorkbenchConfirmationConfirmContext,
|
|
78
|
+
) => Promise<void> | void;
|
|
79
|
+
onConfirmError?: (
|
|
80
|
+
error: unknown,
|
|
81
|
+
action: WorkbenchConfirmationAction,
|
|
82
|
+
context: WorkbenchConfirmationConfirmContext,
|
|
83
|
+
) => void;
|
|
84
|
+
open?: boolean;
|
|
85
|
+
pending?: boolean;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function getWorkbenchConfirmationVariant(
|
|
89
|
+
action: WorkbenchConfirmationAction,
|
|
90
|
+
): WorkbenchConfirmationVariant {
|
|
91
|
+
return action.variant ?? (action.danger ? 'danger' : 'default');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function getWorkbenchConfirmationStatus({
|
|
95
|
+
action,
|
|
96
|
+
pending = false,
|
|
97
|
+
}: {
|
|
98
|
+
action: WorkbenchConfirmationAction;
|
|
99
|
+
pending?: boolean;
|
|
100
|
+
}): WorkbenchStatus {
|
|
101
|
+
if (pending) return 'running';
|
|
102
|
+
if (action.status) return action.status;
|
|
103
|
+
if (action.disabled) return 'disabled';
|
|
104
|
+
return 'idle';
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function isWorkbenchConfirmationActionDisabled(action: WorkbenchConfirmationAction) {
|
|
108
|
+
return Boolean(action.disabled || (action.status && isWorkbenchStatusDisabled(action.status)));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function getWorkbenchConfirmationConfirmLabel({
|
|
112
|
+
action,
|
|
113
|
+
pending = false,
|
|
114
|
+
}: {
|
|
115
|
+
action: WorkbenchConfirmationAction;
|
|
116
|
+
pending?: boolean;
|
|
117
|
+
}) {
|
|
118
|
+
if (pending) return action.pendingLabel ?? 'Working...';
|
|
119
|
+
return action.confirmLabel ?? 'Confirm';
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function getWorkbenchConfirmationButtonVariant(
|
|
123
|
+
action: WorkbenchConfirmationAction,
|
|
124
|
+
): 'danger' | 'primary' {
|
|
125
|
+
return getWorkbenchConfirmationVariant(action) === 'danger' ? 'danger' : 'primary';
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function getWorkbenchConfirmationSideEffect(
|
|
129
|
+
action: WorkbenchConfirmationAction,
|
|
130
|
+
): WorkbenchConfirmationSideEffect {
|
|
131
|
+
return action.sideEffect ?? 'none';
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function WorkbenchConfirmationFlow({
|
|
135
|
+
action,
|
|
136
|
+
allowCancelWhilePending = false,
|
|
137
|
+
className,
|
|
138
|
+
onCancel,
|
|
139
|
+
onClose,
|
|
140
|
+
onConfirm,
|
|
141
|
+
onConfirmError,
|
|
142
|
+
open,
|
|
143
|
+
pending,
|
|
144
|
+
}: WorkbenchConfirmationFlowProps) {
|
|
145
|
+
const [internalPending, setInternalPending] = useState(false);
|
|
146
|
+
|
|
147
|
+
if (!action || open === false) return null;
|
|
148
|
+
|
|
149
|
+
const resolvedPending = pending ?? internalPending;
|
|
150
|
+
const status = getWorkbenchConfirmationStatus({ action, pending: resolvedPending });
|
|
151
|
+
const variant = getWorkbenchConfirmationVariant(action);
|
|
152
|
+
const sideEffect = getWorkbenchConfirmationSideEffect(action);
|
|
153
|
+
const confirmDisabled = resolvedPending || isWorkbenchConfirmationActionDisabled(action);
|
|
154
|
+
const cancelDisabled = resolvedPending && !allowCancelWhilePending;
|
|
155
|
+
const statusLabel = getWorkbenchStatusLabel(status);
|
|
156
|
+
|
|
157
|
+
const createContext = (reason: WorkbenchConfirmationReason): WorkbenchConfirmationContext => ({
|
|
158
|
+
action,
|
|
159
|
+
actionId: action.id,
|
|
160
|
+
pending: resolvedPending,
|
|
161
|
+
reason,
|
|
162
|
+
sideEffect,
|
|
163
|
+
status,
|
|
164
|
+
variant,
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
const handleClose = () => {
|
|
168
|
+
if (cancelDisabled) return;
|
|
169
|
+
onClose?.(action, { ...createContext('close'), reason: 'close' });
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const handleCancel = () => {
|
|
173
|
+
if (cancelDisabled) return;
|
|
174
|
+
onCancel?.(action, { ...createContext('cancel'), reason: 'cancel' });
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
|
|
178
|
+
event.preventDefault();
|
|
179
|
+
if (confirmDisabled) return;
|
|
180
|
+
|
|
181
|
+
const context: WorkbenchConfirmationConfirmContext = {
|
|
182
|
+
...createContext('confirm'),
|
|
183
|
+
event,
|
|
184
|
+
reason: 'confirm',
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const result = onConfirm?.(action, context);
|
|
188
|
+
if (!result || typeof result.then !== 'function') return;
|
|
189
|
+
|
|
190
|
+
setInternalPending(true);
|
|
191
|
+
try {
|
|
192
|
+
await result;
|
|
193
|
+
} catch (error) {
|
|
194
|
+
onConfirmError?.(error, action, context);
|
|
195
|
+
} finally {
|
|
196
|
+
setInternalPending(false);
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
return (
|
|
201
|
+
<Modal
|
|
202
|
+
bodyClassName="ui-workbench-confirmation-flow__body"
|
|
203
|
+
className={cx('ui-workbench-confirmation-flow', className)}
|
|
204
|
+
closeLabel={action.closeLabel ?? 'Close confirmation'}
|
|
205
|
+
footer={
|
|
206
|
+
<>
|
|
207
|
+
<Button disabled={cancelDisabled} onClick={handleCancel}>
|
|
208
|
+
{action.cancelLabel ?? 'Cancel'}
|
|
209
|
+
</Button>
|
|
210
|
+
<Button
|
|
211
|
+
aria-busy={isWorkbenchStatusBusy(status) ? true : undefined}
|
|
212
|
+
className="ui-workbench-confirmation-flow__confirm"
|
|
213
|
+
data-status={status}
|
|
214
|
+
disabled={confirmDisabled}
|
|
215
|
+
type="submit"
|
|
216
|
+
variant={getWorkbenchConfirmationButtonVariant(action)}
|
|
217
|
+
>
|
|
218
|
+
{getWorkbenchConfirmationConfirmLabel({ action, pending: resolvedPending })}
|
|
219
|
+
</Button>
|
|
220
|
+
</>
|
|
221
|
+
}
|
|
222
|
+
title={action.title}
|
|
223
|
+
titleSuffix={
|
|
224
|
+
sideEffect !== 'none' ? (
|
|
225
|
+
<span className="ui-workbench-confirmation-flow__side-effect">{sideEffect}</span>
|
|
226
|
+
) : null
|
|
227
|
+
}
|
|
228
|
+
onClose={handleClose}
|
|
229
|
+
onSubmit={handleSubmit}
|
|
230
|
+
>
|
|
231
|
+
<p className="ui-workbench-confirmation-flow__message">{action.message}</p>
|
|
232
|
+
{action.detail ? (
|
|
233
|
+
<div className="ui-workbench-confirmation-flow__detail">{action.detail}</div>
|
|
234
|
+
) : null}
|
|
235
|
+
{action.disabledReason ? (
|
|
236
|
+
<div className="ui-workbench-confirmation-flow__disabled-reason">
|
|
237
|
+
{action.disabledReason}
|
|
238
|
+
</div>
|
|
239
|
+
) : null}
|
|
240
|
+
<span className="ui-visually-hidden" aria-live="polite">
|
|
241
|
+
{statusLabel}
|
|
242
|
+
</span>
|
|
243
|
+
</Modal>
|
|
244
|
+
);
|
|
245
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import Markdown from 'react-markdown';
|
|
2
|
+
import { Children, isValidElement, type ComponentPropsWithoutRef, type ReactNode } from 'react';
|
|
3
|
+
import { cx } from '../utils/cx';
|
|
4
|
+
|
|
5
|
+
export interface WorkbenchMarkdownPreviewProps extends ComponentPropsWithoutRef<'article'> {
|
|
6
|
+
source: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function WorkbenchMarkdownPreview({
|
|
10
|
+
className,
|
|
11
|
+
source,
|
|
12
|
+
...props
|
|
13
|
+
}: WorkbenchMarkdownPreviewProps) {
|
|
14
|
+
return (
|
|
15
|
+
<article className={cx('ui-workbench-markdown-preview', className)} {...props}>
|
|
16
|
+
<Markdown
|
|
17
|
+
components={{
|
|
18
|
+
a: ({ children, href, ...anchorProps }) => (
|
|
19
|
+
<a
|
|
20
|
+
href={href}
|
|
21
|
+
rel="noreferrer"
|
|
22
|
+
target={href?.startsWith('#') ? undefined : '_blank'}
|
|
23
|
+
{...anchorProps}
|
|
24
|
+
>
|
|
25
|
+
{children}
|
|
26
|
+
</a>
|
|
27
|
+
),
|
|
28
|
+
code: ({ className: codeClassName, ...codeProps }) => (
|
|
29
|
+
<code
|
|
30
|
+
className={cx('ui-workbench-markdown-preview__code', codeClassName)}
|
|
31
|
+
{...codeProps}
|
|
32
|
+
/>
|
|
33
|
+
),
|
|
34
|
+
pre: ({ children }) => {
|
|
35
|
+
const codeBlock = getMarkdownCodeBlock(children);
|
|
36
|
+
|
|
37
|
+
if (codeBlock?.language === 'mermaid') {
|
|
38
|
+
return <WorkbenchMermaidPreview source={codeBlock.source} />;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return <pre>{children}</pre>;
|
|
42
|
+
},
|
|
43
|
+
}}
|
|
44
|
+
>
|
|
45
|
+
{source}
|
|
46
|
+
</Markdown>
|
|
47
|
+
</article>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function WorkbenchMermaidPreview({ source }: { source: string }) {
|
|
52
|
+
const summary = summarizeMermaidSource(source);
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<figure
|
|
56
|
+
className="ui-workbench-markdown-preview__mermaid"
|
|
57
|
+
data-testid="markdown-mermaid-preview"
|
|
58
|
+
>
|
|
59
|
+
<figcaption className="ui-workbench-markdown-preview__mermaid-header">
|
|
60
|
+
<span className="ui-workbench-markdown-preview__mermaid-title">
|
|
61
|
+
<i aria-hidden className="codicon codicon-git-branch" />
|
|
62
|
+
Mermaid
|
|
63
|
+
</span>
|
|
64
|
+
<span className="ui-workbench-markdown-preview__mermaid-kind">{summary.kind}</span>
|
|
65
|
+
</figcaption>
|
|
66
|
+
<div
|
|
67
|
+
className="ui-workbench-markdown-preview__mermaid-canvas"
|
|
68
|
+
aria-label="Mermaid diagram preview"
|
|
69
|
+
>
|
|
70
|
+
{summary.nodes.length > 0 ? (
|
|
71
|
+
<div className="ui-workbench-markdown-preview__mermaid-flow">
|
|
72
|
+
{summary.nodes.map((node, index) => (
|
|
73
|
+
<span
|
|
74
|
+
key={`${node}-${index}`}
|
|
75
|
+
className="ui-workbench-markdown-preview__mermaid-node"
|
|
76
|
+
>
|
|
77
|
+
{node}
|
|
78
|
+
</span>
|
|
79
|
+
))}
|
|
80
|
+
</div>
|
|
81
|
+
) : (
|
|
82
|
+
<pre className="ui-workbench-markdown-preview__mermaid-source">{source.trim()}</pre>
|
|
83
|
+
)}
|
|
84
|
+
</div>
|
|
85
|
+
</figure>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function getMarkdownCodeBlock(children: ReactNode): { language: string; source: string } | null {
|
|
90
|
+
const [child] = Children.toArray(children);
|
|
91
|
+
if (!isValidElement<{ className?: string | undefined; children?: ReactNode }>(child)) {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const className = child.props.className;
|
|
96
|
+
if (typeof className !== 'string') {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const languageMatch = /\blanguage-([a-z0-9_-]+)\b/i.exec(className);
|
|
101
|
+
if (!languageMatch) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
language: languageMatch[1]?.toLowerCase() ?? '',
|
|
107
|
+
source: extractTextContent(child.props.children).replace(/\n$/, ''),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function extractTextContent(value: ReactNode): string {
|
|
112
|
+
return Children.toArray(value)
|
|
113
|
+
.map((child) => {
|
|
114
|
+
if (typeof child === 'string' || typeof child === 'number') {
|
|
115
|
+
return String(child);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (isValidElement<{ children?: ReactNode }>(child)) {
|
|
119
|
+
return extractTextContent(child.props.children);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return '';
|
|
123
|
+
})
|
|
124
|
+
.join('');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function summarizeMermaidSource(source: string): { kind: string; nodes: string[] } {
|
|
128
|
+
const lines = source
|
|
129
|
+
.split(/\r?\n/)
|
|
130
|
+
.map((line) => line.trim())
|
|
131
|
+
.filter(Boolean);
|
|
132
|
+
const declaration = lines[0] ?? 'mermaid';
|
|
133
|
+
const kind = declaration.split(/\s+/)[0] ?? 'mermaid';
|
|
134
|
+
const nodes = lines
|
|
135
|
+
.slice(1)
|
|
136
|
+
.flatMap((line) => line.split(/-->|---|==>|-.->|->/))
|
|
137
|
+
.map((segment) => cleanMermaidNodeLabel(segment))
|
|
138
|
+
.filter((segment) => segment.length > 0)
|
|
139
|
+
.filter((segment, index, all) => all.indexOf(segment) === index)
|
|
140
|
+
.slice(0, 6);
|
|
141
|
+
|
|
142
|
+
return { kind, nodes };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function cleanMermaidNodeLabel(value: string): string {
|
|
146
|
+
return value
|
|
147
|
+
.replace(/^[A-Za-z0-9_]+(?=\[|\(|\{)/, '')
|
|
148
|
+
.replace(/^[A-Za-z0-9_]+$/, '')
|
|
149
|
+
.replace(/[[\]{}()"]/g, '')
|
|
150
|
+
.trim();
|
|
151
|
+
}
|