@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.
- 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,44 @@
|
|
|
1
|
+
import { IconButton } from '../primitives/IconButton';
|
|
2
|
+
import { cx } from '../utils/cx';
|
|
3
|
+
import { resolveWidgetTreeLabMode, type WidgetTreeViewMode } from './widget-tree-mode.js';
|
|
4
|
+
|
|
5
|
+
export interface WidgetTreeModeControlsProps {
|
|
6
|
+
readonly className?: string | undefined;
|
|
7
|
+
readonly mode: WidgetTreeViewMode;
|
|
8
|
+
readonly onModeChange: (mode: WidgetTreeViewMode) => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function WidgetTreeModeControls({
|
|
12
|
+
className,
|
|
13
|
+
mode,
|
|
14
|
+
onModeChange,
|
|
15
|
+
}: WidgetTreeModeControlsProps) {
|
|
16
|
+
const resolvedMode = resolveWidgetTreeLabMode(mode);
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<div
|
|
20
|
+
className={cx('ui-workbench-artifact-shell__modes', 'widget-tree-mode-controls', className)}
|
|
21
|
+
>
|
|
22
|
+
<IconButton
|
|
23
|
+
aria-pressed={resolvedMode === 'design'}
|
|
24
|
+
className={cx(
|
|
25
|
+
'ui-workbench-artifact-shell__mode',
|
|
26
|
+
resolvedMode === 'design' && 'ui-workbench-artifact-shell__mode--active',
|
|
27
|
+
)}
|
|
28
|
+
icon="codicon-layout-sidebar-right"
|
|
29
|
+
label="Design"
|
|
30
|
+
onClick={() => onModeChange('design')}
|
|
31
|
+
/>
|
|
32
|
+
<IconButton
|
|
33
|
+
aria-pressed={resolvedMode === 'code'}
|
|
34
|
+
className={cx(
|
|
35
|
+
'ui-workbench-artifact-shell__mode',
|
|
36
|
+
resolvedMode === 'code' && 'ui-workbench-artifact-shell__mode--active',
|
|
37
|
+
)}
|
|
38
|
+
icon="codicon-code"
|
|
39
|
+
label="Code"
|
|
40
|
+
onClick={() => onModeChange('code')}
|
|
41
|
+
/>
|
|
42
|
+
</div>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { useState, type ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
import { SegmentedControl } from '../primitives/WorkbenchEditor.js';
|
|
4
|
+
import { cx } from '../utils/cx.js';
|
|
5
|
+
|
|
6
|
+
export type WidgetTreeSidePanelTab = 'outline' | 'assets' | 'properties';
|
|
7
|
+
|
|
8
|
+
export interface WidgetTreeSidePanelProps {
|
|
9
|
+
readonly outline: ReactNode;
|
|
10
|
+
readonly assets: ReactNode;
|
|
11
|
+
readonly properties: ReactNode;
|
|
12
|
+
readonly defaultTab?: WidgetTreeSidePanelTab | undefined;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function WidgetTreeSidePanel({
|
|
16
|
+
outline,
|
|
17
|
+
assets,
|
|
18
|
+
properties,
|
|
19
|
+
defaultTab = 'outline',
|
|
20
|
+
}: WidgetTreeSidePanelProps) {
|
|
21
|
+
const [tab, setTab] = useState<WidgetTreeSidePanelTab>(defaultTab);
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<section className="widget-tree-side-panel" data-testid="widget-tree-side-panel">
|
|
25
|
+
<header className="widget-tree-side-panel__header">
|
|
26
|
+
<SegmentedControl
|
|
27
|
+
ariaLabel="Widget side panel"
|
|
28
|
+
options={[
|
|
29
|
+
{ label: 'Outline', value: 'outline' },
|
|
30
|
+
{ label: 'Assets', value: 'assets' },
|
|
31
|
+
{ label: 'Props', value: 'properties' },
|
|
32
|
+
]}
|
|
33
|
+
value={tab}
|
|
34
|
+
onChange={setTab}
|
|
35
|
+
/>
|
|
36
|
+
</header>
|
|
37
|
+
<div
|
|
38
|
+
className={cx(
|
|
39
|
+
'widget-tree-side-panel__body',
|
|
40
|
+
tab === 'outline' && 'widget-tree-side-panel__body--outline',
|
|
41
|
+
tab === 'assets' && 'widget-tree-side-panel__body--assets',
|
|
42
|
+
tab === 'properties' && 'widget-tree-side-panel__body--properties',
|
|
43
|
+
)}
|
|
44
|
+
>
|
|
45
|
+
{tab === 'outline' ? outline : tab === 'assets' ? assets : properties}
|
|
46
|
+
</div>
|
|
47
|
+
</section>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
collectWidgetNodes,
|
|
4
|
+
getWidgetDisplayLabel,
|
|
5
|
+
isWidgetPathSelected,
|
|
6
|
+
widgetPathKey,
|
|
7
|
+
type GenericWidget,
|
|
8
|
+
type WidgetPath,
|
|
9
|
+
type WidgetSelectionState,
|
|
10
|
+
} from '@workbench-kit/jdw';
|
|
11
|
+
|
|
12
|
+
import { Panel, PanelBody } from '../layout/Panel';
|
|
13
|
+
import { EmptyState } from '../primitives/EmptyState';
|
|
14
|
+
import { cx } from '../utils/cx';
|
|
15
|
+
import { formatWidgetPlacementMeta } from './widget-tree-layout.js';
|
|
16
|
+
|
|
17
|
+
export interface WidgetTreeViewProps {
|
|
18
|
+
readonly root: GenericWidget | null;
|
|
19
|
+
readonly parseError: string | null;
|
|
20
|
+
readonly selection?: WidgetSelectionState | undefined;
|
|
21
|
+
readonly onSelectPath?: ((path: WidgetPath) => void) | undefined;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function parentTypeForNode(
|
|
25
|
+
nodes: ReturnType<typeof collectWidgetNodes>,
|
|
26
|
+
path: WidgetPath,
|
|
27
|
+
): string | undefined {
|
|
28
|
+
if (path.length === 0) return undefined;
|
|
29
|
+
const parentPath = path.slice(0, -1);
|
|
30
|
+
const parentKey = widgetPathKey(parentPath);
|
|
31
|
+
return nodes.find((node) => widgetPathKey(node.path) === parentKey)?.widget.type;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function WidgetTreeView({ root, parseError, selection, onSelectPath }: WidgetTreeViewProps) {
|
|
35
|
+
const nodes = useMemo(() => (root ? collectWidgetNodes(root) : []), [root]);
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<Panel className="widget-tree-outline" data-testid="widget-tree-outline-panel">
|
|
39
|
+
<PanelBody className="widget-tree-outline__body">
|
|
40
|
+
{parseError !== null ? (
|
|
41
|
+
<EmptyState compact icon="codicon-error">
|
|
42
|
+
{parseError}
|
|
43
|
+
</EmptyState>
|
|
44
|
+
) : root === null ? (
|
|
45
|
+
<EmptyState compact icon="codicon-list-tree">
|
|
46
|
+
No widget root.
|
|
47
|
+
</EmptyState>
|
|
48
|
+
) : (
|
|
49
|
+
<ul aria-label="Widget outline" className="widget-tree-outline__list" role="tree">
|
|
50
|
+
{nodes.map((node) => {
|
|
51
|
+
const pathKey = widgetPathKey(node.path);
|
|
52
|
+
const depth = node.path.length;
|
|
53
|
+
const isSelected = selection ? isWidgetPathSelected(selection, node.path) : false;
|
|
54
|
+
const parentType = parentTypeForNode(nodes, node.path);
|
|
55
|
+
const placementMeta = formatWidgetPlacementMeta(node.widget, parentType);
|
|
56
|
+
const textPreview =
|
|
57
|
+
node.widget.type === 'text' && typeof node.widget.text === 'string'
|
|
58
|
+
? node.widget.text
|
|
59
|
+
: null;
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<li
|
|
63
|
+
key={pathKey}
|
|
64
|
+
aria-level={depth + 1}
|
|
65
|
+
aria-selected={isSelected}
|
|
66
|
+
className={cx(
|
|
67
|
+
'widget-tree-outline__item',
|
|
68
|
+
isSelected && 'widget-tree-outline__item--selected',
|
|
69
|
+
)}
|
|
70
|
+
data-testid={`widget-tree-node-${pathKey}`}
|
|
71
|
+
role="treeitem"
|
|
72
|
+
style={{ paddingLeft: `${depth * 14 + 6}px` }}
|
|
73
|
+
>
|
|
74
|
+
<button
|
|
75
|
+
className="widget-tree-outline__button"
|
|
76
|
+
type="button"
|
|
77
|
+
onClick={() => onSelectPath?.(node.path)}
|
|
78
|
+
>
|
|
79
|
+
<span className="widget-tree-outline__type">
|
|
80
|
+
{getWidgetDisplayLabel(node.widget)}
|
|
81
|
+
</span>
|
|
82
|
+
{placementMeta ? (
|
|
83
|
+
<span className="widget-tree-outline__placement">{placementMeta}</span>
|
|
84
|
+
) : null}
|
|
85
|
+
{textPreview ? (
|
|
86
|
+
<span className="widget-tree-outline__meta">{textPreview}</span>
|
|
87
|
+
) : null}
|
|
88
|
+
</button>
|
|
89
|
+
</li>
|
|
90
|
+
);
|
|
91
|
+
})}
|
|
92
|
+
</ul>
|
|
93
|
+
)}
|
|
94
|
+
</PanelBody>
|
|
95
|
+
</Panel>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { useState, type ReactNode } from 'react';
|
|
2
|
+
import type { WidgetAssetCatalogContract, WidgetRegistryContract } from '@workbench-kit/contracts';
|
|
3
|
+
|
|
4
|
+
import { Panel, PanelBody, PanelHeader } from '../layout/Panel';
|
|
5
|
+
import { Button } from '../primitives/Button';
|
|
6
|
+
import { Toolbar } from '../primitives/Toolbar';
|
|
7
|
+
import type { WorkspaceEditorTheme } from '../workbench/workspace/WorkspaceEditor.js';
|
|
8
|
+
import { fileNameOfPath } from '../workbench/workspace/path';
|
|
9
|
+
import { WidgetTreeLab } from './WidgetTreeLab.js';
|
|
10
|
+
import { WidgetTreeModeControls } from './WidgetTreeModeControls.js';
|
|
11
|
+
import { DEFAULT_WIDGET_TREE_VIEW_MODE, type WidgetTreeViewMode } from './widget-tree-mode.js';
|
|
12
|
+
|
|
13
|
+
export interface WidgetTreeWorkbenchProps {
|
|
14
|
+
readonly path?: string | undefined;
|
|
15
|
+
readonly title?: ReactNode | undefined;
|
|
16
|
+
readonly value: string;
|
|
17
|
+
readonly onChange: (value: string) => void;
|
|
18
|
+
readonly onSave?: (() => void) | undefined;
|
|
19
|
+
readonly onDiscard?: (() => void) | undefined;
|
|
20
|
+
readonly dirty?: boolean | undefined;
|
|
21
|
+
readonly readOnly?: boolean | undefined;
|
|
22
|
+
readonly registry?: WidgetRegistryContract<unknown> | undefined;
|
|
23
|
+
readonly assetCatalog?: WidgetAssetCatalogContract | undefined;
|
|
24
|
+
readonly theme?: WorkspaceEditorTheme | undefined;
|
|
25
|
+
readonly defaultViewMode?: WidgetTreeViewMode | undefined;
|
|
26
|
+
readonly viewMode?: WidgetTreeViewMode | undefined;
|
|
27
|
+
readonly onViewModeChange?: ((mode: WidgetTreeViewMode) => void) | undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function WidgetTreeWorkbench({
|
|
31
|
+
path,
|
|
32
|
+
title,
|
|
33
|
+
value,
|
|
34
|
+
onChange,
|
|
35
|
+
onSave,
|
|
36
|
+
onDiscard,
|
|
37
|
+
dirty = false,
|
|
38
|
+
readOnly = false,
|
|
39
|
+
registry,
|
|
40
|
+
assetCatalog,
|
|
41
|
+
theme = 'dark',
|
|
42
|
+
defaultViewMode = DEFAULT_WIDGET_TREE_VIEW_MODE,
|
|
43
|
+
viewMode,
|
|
44
|
+
onViewModeChange,
|
|
45
|
+
}: WidgetTreeWorkbenchProps) {
|
|
46
|
+
const [uncontrolledViewMode, setUncontrolledViewMode] =
|
|
47
|
+
useState<WidgetTreeViewMode>(defaultViewMode);
|
|
48
|
+
const resolvedViewMode = viewMode ?? uncontrolledViewMode;
|
|
49
|
+
|
|
50
|
+
const setViewMode = (nextMode: WidgetTreeViewMode) => {
|
|
51
|
+
if (viewMode === undefined) {
|
|
52
|
+
setUncontrolledViewMode(nextMode);
|
|
53
|
+
}
|
|
54
|
+
onViewModeChange?.(nextMode);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const resolvedTitle = title ?? (path ? fileNameOfPath(path) : 'Widget document');
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<Panel
|
|
61
|
+
className="widget-tree-workbench"
|
|
62
|
+
data-mode={resolvedViewMode}
|
|
63
|
+
data-testid="widget-tree-workbench"
|
|
64
|
+
>
|
|
65
|
+
<PanelHeader
|
|
66
|
+
actions={
|
|
67
|
+
<Toolbar>
|
|
68
|
+
{dirty && !readOnly ? (
|
|
69
|
+
<>
|
|
70
|
+
{onDiscard ? <Button onClick={onDiscard}>Discard</Button> : null}
|
|
71
|
+
{onSave ? (
|
|
72
|
+
<Button variant="primary" onClick={onSave}>
|
|
73
|
+
Save
|
|
74
|
+
</Button>
|
|
75
|
+
) : null}
|
|
76
|
+
</>
|
|
77
|
+
) : null}
|
|
78
|
+
<WidgetTreeModeControls mode={resolvedViewMode} onModeChange={setViewMode} />
|
|
79
|
+
</Toolbar>
|
|
80
|
+
}
|
|
81
|
+
>
|
|
82
|
+
<span className="widget-tree-workbench__title">
|
|
83
|
+
{resolvedTitle}
|
|
84
|
+
{path ? (
|
|
85
|
+
<span className="widget-tree-workbench__path" title={path}>
|
|
86
|
+
{path}
|
|
87
|
+
</span>
|
|
88
|
+
) : null}
|
|
89
|
+
{dirty ? (
|
|
90
|
+
<span className="widget-tree-workbench__dirty-indicator" title="Unsaved changes">
|
|
91
|
+
●
|
|
92
|
+
</span>
|
|
93
|
+
) : null}
|
|
94
|
+
</span>
|
|
95
|
+
</PanelHeader>
|
|
96
|
+
<PanelBody className="widget-tree-workbench__body">
|
|
97
|
+
<WidgetTreeLab
|
|
98
|
+
assetCatalog={assetCatalog}
|
|
99
|
+
path={path}
|
|
100
|
+
readOnly={readOnly}
|
|
101
|
+
registry={registry}
|
|
102
|
+
theme={theme}
|
|
103
|
+
value={value}
|
|
104
|
+
viewMode={resolvedViewMode}
|
|
105
|
+
onChange={onChange}
|
|
106
|
+
onSave={onSave}
|
|
107
|
+
/>
|
|
108
|
+
</PanelBody>
|
|
109
|
+
</Panel>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { useMemo, useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
integratedShellWorkspaceFiles,
|
|
4
|
+
integratedShellWorkspaceFolders,
|
|
5
|
+
} from '@workbench-kit/adapters';
|
|
6
|
+
|
|
7
|
+
import { SideBarViewFrame } from '../layout/SideBarViewFrame.js';
|
|
8
|
+
import { WorkbenchShell } from '../workbench/WorkbenchShell.js';
|
|
9
|
+
import type { StatusBarSectionModel } from '../workbench/StatusBar.js';
|
|
10
|
+
import {
|
|
11
|
+
useVirtualWorkspace,
|
|
12
|
+
WorkspaceDraftsProvider,
|
|
13
|
+
WorkspaceEditorPanel,
|
|
14
|
+
WorkspaceExplorer,
|
|
15
|
+
type WorkspaceEditorTheme,
|
|
16
|
+
type WorkspaceFile,
|
|
17
|
+
} from '../workbench/workspace/index.js';
|
|
18
|
+
import { createWidgetStudioWorkspaceEditorRenderer } from '../widget-studio/create-widget-studio-workspace-editor.js';
|
|
19
|
+
import { WIDGET_TREE_DEMO_REGISTRY, WIDGET_TREE_WELCOME_DOCUMENT } from './demo-registry.js';
|
|
20
|
+
|
|
21
|
+
const JDW_WORKSPACE_DEFAULT_WIDGET_PATH = 'src/widgets/home.widget.json';
|
|
22
|
+
|
|
23
|
+
function withJdwWelcomeDocument(files: readonly WorkspaceFile[]): WorkspaceFile[] {
|
|
24
|
+
return files.map((file) =>
|
|
25
|
+
file.path === JDW_WORKSPACE_DEFAULT_WIDGET_PATH
|
|
26
|
+
? { ...file, content: WIDGET_TREE_WELCOME_DOCUMENT }
|
|
27
|
+
: file,
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const statusSections: StatusBarSectionModel[] = [
|
|
32
|
+
{
|
|
33
|
+
id: 'main',
|
|
34
|
+
items: [{ id: 'label', label: 'JDW Workspace' }],
|
|
35
|
+
},
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
export interface WidgetTreeWorkspaceShellProps {
|
|
39
|
+
readonly initialSelectedPath?: string | undefined;
|
|
40
|
+
readonly initialTheme?: WorkspaceEditorTheme | undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function WidgetTreeWorkspaceShell({
|
|
44
|
+
initialSelectedPath = JDW_WORKSPACE_DEFAULT_WIDGET_PATH,
|
|
45
|
+
initialTheme = 'dark',
|
|
46
|
+
}: WidgetTreeWorkspaceShellProps = {}) {
|
|
47
|
+
const [sidebarVisible, setSidebarVisible] = useState(true);
|
|
48
|
+
const [sidebarSizePercent, setSidebarSizePercent] = useState(20);
|
|
49
|
+
|
|
50
|
+
const workspaceFiles = useMemo(() => withJdwWelcomeDocument(integratedShellWorkspaceFiles), []);
|
|
51
|
+
|
|
52
|
+
const workspace = useVirtualWorkspace({
|
|
53
|
+
expandedPaths: ['src', 'src/widgets', 'src/widgets/assets'],
|
|
54
|
+
files: workspaceFiles,
|
|
55
|
+
folders: [...integratedShellWorkspaceFolders],
|
|
56
|
+
openPaths: [initialSelectedPath],
|
|
57
|
+
selectedPath: initialSelectedPath,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const {
|
|
61
|
+
closeAll,
|
|
62
|
+
closeOthers,
|
|
63
|
+
closePath,
|
|
64
|
+
deleteFile,
|
|
65
|
+
expandedPaths,
|
|
66
|
+
files,
|
|
67
|
+
openFile,
|
|
68
|
+
openPaths,
|
|
69
|
+
saveFile,
|
|
70
|
+
selectedPath,
|
|
71
|
+
toggleFolder,
|
|
72
|
+
workspaceTree,
|
|
73
|
+
} = workspace;
|
|
74
|
+
|
|
75
|
+
const widgetStudioEditorRenderer = useMemo(
|
|
76
|
+
() =>
|
|
77
|
+
createWidgetStudioWorkspaceEditorRenderer({
|
|
78
|
+
registry: WIDGET_TREE_DEMO_REGISTRY,
|
|
79
|
+
}),
|
|
80
|
+
[],
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<div className="jdw-workspace-shell-host" data-testid="jdw-workspace-shell">
|
|
85
|
+
<WorkspaceDraftsProvider>
|
|
86
|
+
<WorkbenchShell
|
|
87
|
+
activityBar={{
|
|
88
|
+
items: [
|
|
89
|
+
{
|
|
90
|
+
active: sidebarVisible,
|
|
91
|
+
icon: <i className="codicon codicon-files" aria-hidden />,
|
|
92
|
+
id: 'explorer',
|
|
93
|
+
label: 'Explorer',
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
onItemActivate: () => setSidebarVisible((visible) => !visible),
|
|
97
|
+
}}
|
|
98
|
+
compactStatus
|
|
99
|
+
primarySidebar={{
|
|
100
|
+
className: 'ui-workbench-story-shell-split',
|
|
101
|
+
isVisible: sidebarVisible,
|
|
102
|
+
maxPrimarySizePercent: 36,
|
|
103
|
+
minPrimarySizePercent: 16,
|
|
104
|
+
node: (
|
|
105
|
+
<aside aria-label="Explorer sidebar" className="workbench-primary-side-bar">
|
|
106
|
+
<SideBarViewFrame title="Explorer">
|
|
107
|
+
<WorkspaceExplorer
|
|
108
|
+
activePath={selectedPath}
|
|
109
|
+
expandedPaths={expandedPaths}
|
|
110
|
+
nodes={workspaceTree}
|
|
111
|
+
onActivateFile={openFile}
|
|
112
|
+
onToggleFolder={toggleFolder}
|
|
113
|
+
/>
|
|
114
|
+
</SideBarViewFrame>
|
|
115
|
+
</aside>
|
|
116
|
+
),
|
|
117
|
+
onSizePercentChange: setSidebarSizePercent,
|
|
118
|
+
primarySizePercent: sidebarSizePercent,
|
|
119
|
+
}}
|
|
120
|
+
rootClassName="ide-root jdw-workspace-shell"
|
|
121
|
+
rootStyle={{ height: '100%', minHeight: 0 }}
|
|
122
|
+
secondaryArea={
|
|
123
|
+
<main className="workbench-editor-area jdw-workspace-shell__editor">
|
|
124
|
+
<WorkspaceEditorPanel
|
|
125
|
+
files={files}
|
|
126
|
+
openPaths={openPaths}
|
|
127
|
+
renderEditor={(context) =>
|
|
128
|
+
widgetStudioEditorRenderer({ ...context, workspaceFiles: files })
|
|
129
|
+
}
|
|
130
|
+
selectedPath={selectedPath}
|
|
131
|
+
theme={initialTheme}
|
|
132
|
+
onCloseAll={closeAll}
|
|
133
|
+
onCloseOthers={closeOthers}
|
|
134
|
+
onClosePath={closePath}
|
|
135
|
+
onDeletePath={deleteFile}
|
|
136
|
+
onSaveFile={(path, content) => {
|
|
137
|
+
saveFile(path, { content, source: 'user' });
|
|
138
|
+
return {
|
|
139
|
+
file: files.find((file) => file.path === path) ?? { content, path },
|
|
140
|
+
kind: 'save:success' as const,
|
|
141
|
+
outcome: 'unchanged' as const,
|
|
142
|
+
};
|
|
143
|
+
}}
|
|
144
|
+
onSelectedPathChange={openFile}
|
|
145
|
+
/>
|
|
146
|
+
</main>
|
|
147
|
+
}
|
|
148
|
+
statusSections={statusSections}
|
|
149
|
+
theme={initialTheme}
|
|
150
|
+
/>
|
|
151
|
+
</WorkspaceDraftsProvider>
|
|
152
|
+
</div>
|
|
153
|
+
);
|
|
154
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { formatWidgetDocumentJson } from '@workbench-kit/jdw';
|
|
2
|
+
|
|
3
|
+
import { BUILTIN_JDW_REGISTRY } from '../jdw/createBuiltinJdwRegistry.js';
|
|
4
|
+
|
|
5
|
+
export const WIDGET_TREE_DEMO_REGISTRY = BUILTIN_JDW_REGISTRY;
|
|
6
|
+
|
|
7
|
+
export const WIDGET_TREE_WELCOME_DOCUMENT = formatWidgetDocumentJson({
|
|
8
|
+
type: 'column',
|
|
9
|
+
gap: 12,
|
|
10
|
+
padding: 16,
|
|
11
|
+
children: [
|
|
12
|
+
{ type: 'text', text: 'Widget Tree' },
|
|
13
|
+
{
|
|
14
|
+
type: 'grid',
|
|
15
|
+
columns: 2,
|
|
16
|
+
gap: 8,
|
|
17
|
+
children: [
|
|
18
|
+
{ type: 'text', text: 'A', col: 0, row: 0 },
|
|
19
|
+
{ type: 'text', text: 'B', col: 1, row: 0 },
|
|
20
|
+
{ type: 'text', text: 'Wide', col: 0, row: 1, colSpan: 2 },
|
|
21
|
+
],
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
type: 'row',
|
|
25
|
+
gap: 8,
|
|
26
|
+
children: [
|
|
27
|
+
{ type: 'text', text: 'Left', flex: 1 },
|
|
28
|
+
{ type: 'text', text: 'Right', flex: 1 },
|
|
29
|
+
],
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export { WidgetTreeLab } from './WidgetTreeLab.js';
|
|
2
|
+
export type { WidgetTreeLabProps } from './WidgetTreeLab.js';
|
|
3
|
+
export { WidgetTreeWorkbench } from './WidgetTreeWorkbench.js';
|
|
4
|
+
export type { WidgetTreeWorkbenchProps } from './WidgetTreeWorkbench.js';
|
|
5
|
+
export { WidgetTreeWorkspaceShell } from './WidgetTreeWorkspaceShell.js';
|
|
6
|
+
export type { WidgetTreeWorkspaceShellProps } from './WidgetTreeWorkspaceShell.js';
|
|
7
|
+
export { WidgetTreeView } from './WidgetTreeView.js';
|
|
8
|
+
export type { WidgetTreeViewProps } from './WidgetTreeView.js';
|
|
9
|
+
export { WidgetInspectorPanel } from './WidgetInspectorPanel.js';
|
|
10
|
+
export type { WidgetInspectorPanelProps } from './WidgetInspectorPanel.js';
|
|
11
|
+
export { WidgetTreeSidePanel } from './WidgetTreeSidePanel.js';
|
|
12
|
+
export type { WidgetTreeSidePanelProps, WidgetTreeSidePanelTab } from './WidgetTreeSidePanel.js';
|
|
13
|
+
export { WidgetAssetPalette } from './WidgetAssetPalette.js';
|
|
14
|
+
export type { WidgetAssetPaletteProps } from './WidgetAssetPalette.js';
|
|
15
|
+
export {
|
|
16
|
+
DEFAULT_WIDGET_TREE_VIEW_MODE,
|
|
17
|
+
resolveWidgetTreeLabMode,
|
|
18
|
+
type WidgetTreeViewMode,
|
|
19
|
+
} from './widget-tree-mode.js';
|
|
20
|
+
export { WidgetSourceEditor } from './WidgetSourceEditor.js';
|
|
21
|
+
export type { WidgetSourceEditorProps } from './WidgetSourceEditor.js';
|
|
22
|
+
export { WidgetTreeModeControls } from './WidgetTreeModeControls.js';
|
|
23
|
+
export type { WidgetTreeModeControlsProps } from './WidgetTreeModeControls.js';
|
|
24
|
+
export { isWidgetTreeDocument, WIDGET_TREE_DOCUMENT_MIME } from './widget-tree-document.js';
|
|
25
|
+
export type { WidgetTreeDocumentRef } from './widget-tree-document.js';
|
|
26
|
+
export { WIDGET_TREE_DEMO_REGISTRY, WIDGET_TREE_WELCOME_DOCUMENT } from './demo-registry.js';
|
|
27
|
+
export { WIDGET_TREE_DEMO_ASSET_CATALOG } from './demo-widget-assets.js';
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { JDW_DOCUMENT_MIME, isJdwDocumentPath } from '../jdw/document';
|
|
2
|
+
|
|
3
|
+
export const WIDGET_TREE_DOCUMENT_MIME = 'application/vnd.workbench-kit.widget+json';
|
|
4
|
+
export const WIDGET_TREE_DOCUMENT_FILE_EXTENSION = '.widget.json';
|
|
5
|
+
|
|
6
|
+
export interface WidgetTreeDocumentRef {
|
|
7
|
+
readonly path: string;
|
|
8
|
+
readonly mimeType?: string | undefined;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function isWidgetTreeDocument(file: WidgetTreeDocumentRef): boolean {
|
|
12
|
+
if (file.mimeType === WIDGET_TREE_DOCUMENT_MIME || file.mimeType === JDW_DOCUMENT_MIME) {
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const normalizedPath = file.path.toLowerCase();
|
|
17
|
+
return (
|
|
18
|
+
normalizedPath.endsWith(WIDGET_TREE_DOCUMENT_FILE_EXTENSION) || isJdwDocumentPath(file.path)
|
|
19
|
+
);
|
|
20
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { isContainerWidget, type GenericWidget } from '@workbench-kit/jdw';
|
|
2
|
+
|
|
3
|
+
export function canAddChildren(widget: GenericWidget | null): widget is GenericWidget {
|
|
4
|
+
return widget !== null && isContainerWidget(widget) && widget.type !== 'box';
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function formatWidgetPlacementMeta(
|
|
8
|
+
widget: GenericWidget,
|
|
9
|
+
parentType?: string,
|
|
10
|
+
): string | null {
|
|
11
|
+
if (parentType === 'grid') {
|
|
12
|
+
const col = typeof widget.col === 'number' ? widget.col : null;
|
|
13
|
+
const row = typeof widget.row === 'number' ? widget.row : null;
|
|
14
|
+
if (col === null && row === null) return null;
|
|
15
|
+
|
|
16
|
+
const span =
|
|
17
|
+
typeof widget.colSpan === 'number' || typeof widget.rowSpan === 'number'
|
|
18
|
+
? ` span ${widget.colSpan ?? 1}x${widget.rowSpan ?? 1}`
|
|
19
|
+
: '';
|
|
20
|
+
|
|
21
|
+
return `c${col ?? '?'} r${row ?? '?'}${span}`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (parentType === 'row' || parentType === 'column') {
|
|
25
|
+
const flex = typeof widget.flex === 'number' ? `flex ${widget.flex}` : null;
|
|
26
|
+
const align = typeof widget.align === 'string' ? widget.align : null;
|
|
27
|
+
return [flex, align].filter(Boolean).join(' · ') || null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export type WidgetTreeViewMode = 'code' | 'editor' | 'split' | 'design';
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_WIDGET_TREE_VIEW_MODE: WidgetTreeViewMode = 'design';
|
|
4
|
+
|
|
5
|
+
export function resolveWidgetTreeLabMode(mode: WidgetTreeViewMode): 'code' | 'design' {
|
|
6
|
+
return mode === 'code' ? 'code' : 'design';
|
|
7
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { expect, userEvent, waitFor } from 'storybook/test';
|
|
2
|
+
|
|
3
|
+
interface MonacoEditorHandle {
|
|
4
|
+
getDomNode: () => HTMLElement | null;
|
|
5
|
+
getValue: () => string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
interface MonacoGlobal {
|
|
9
|
+
editor: {
|
|
10
|
+
getEditors: () => MonacoEditorHandle[];
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function waitForWidgetTreeSourcePane(canvasElement: HTMLElement): Promise<void> {
|
|
15
|
+
await waitFor(
|
|
16
|
+
() => {
|
|
17
|
+
const pane =
|
|
18
|
+
canvasElement.querySelector('.widget-tree-source .ui-json-code-editor-pane') ??
|
|
19
|
+
canvasElement.querySelector('[data-testid="widget-tree-source"]');
|
|
20
|
+
expect(pane).toBeTruthy();
|
|
21
|
+
},
|
|
22
|
+
{ timeout: 15000 },
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function waitForWidgetTreeMonaco(canvasElement: HTMLElement): Promise<void> {
|
|
27
|
+
await waitForWidgetTreeSourcePane(canvasElement);
|
|
28
|
+
|
|
29
|
+
await waitFor(
|
|
30
|
+
() => {
|
|
31
|
+
const editorRoot = canvasElement.querySelector('.widget-tree-source .monaco-editor');
|
|
32
|
+
expect(editorRoot).toBeTruthy();
|
|
33
|
+
expect(editorRoot?.textContent ?? '').not.toContain('Loading editor');
|
|
34
|
+
},
|
|
35
|
+
{ timeout: 15000 },
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function setWidgetTreeSourceJson(
|
|
40
|
+
canvasElement: HTMLElement,
|
|
41
|
+
value: string,
|
|
42
|
+
): Promise<void> {
|
|
43
|
+
await waitForWidgetTreeMonaco(canvasElement);
|
|
44
|
+
|
|
45
|
+
const editorRoot = canvasElement.querySelector('.widget-tree-source .monaco-editor');
|
|
46
|
+
if (!editorRoot) {
|
|
47
|
+
throw new Error('Monaco editor root was not found.');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const textarea = editorRoot.querySelector('textarea');
|
|
51
|
+
if (textarea) {
|
|
52
|
+
await userEvent.click(textarea);
|
|
53
|
+
await userEvent.clear(textarea);
|
|
54
|
+
await userEvent.paste(value);
|
|
55
|
+
} else {
|
|
56
|
+
await userEvent.click(editorRoot);
|
|
57
|
+
await userEvent.keyboard('{Control>}a{/Control}');
|
|
58
|
+
await userEvent.keyboard('{Delete}');
|
|
59
|
+
await userEvent.paste(value);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
await waitFor(
|
|
63
|
+
() => {
|
|
64
|
+
const monacoGlobal = (window as Window & { monaco?: MonacoGlobal }).monaco;
|
|
65
|
+
const editor = monacoGlobal?.editor.getEditors().find((candidate) => {
|
|
66
|
+
const domNode = candidate.getDomNode();
|
|
67
|
+
return domNode ? editorRoot.contains(domNode) : false;
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
expect(editor?.getValue()).toBe(value);
|
|
71
|
+
},
|
|
72
|
+
{ timeout: 5000 },
|
|
73
|
+
);
|
|
74
|
+
}
|