@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
@@ -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,4 @@
1
+ import { createBuiltinWidgetAssetCatalog } from '../widget-studio/builtin-widget-asset-catalog.js';
2
+
3
+ /** Built-in demo catalog for stories/tests without a full workspace fixture. */
4
+ export const WIDGET_TREE_DEMO_ASSET_CATALOG = createBuiltinWidgetAssetCatalog();
@@ -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
+ }