@workbench-kit/react 0.0.2-prototype.0.2.4 → 0.0.2-prototype.0.2.6
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 +10 -10
- package/src/brand/TilepaperAppIcon.tsx +181 -181
- package/src/brand/index.ts +2 -2
- package/src/brand/tilepaper-app-icon-geometry.ts +201 -201
- package/src/icons/WorkbenchIcon.tsx +57 -57
- package/src/icons/WorkbenchIconContext.tsx +25 -25
- package/src/icons/file-icon.ts +154 -154
- package/src/icons/index.ts +17 -17
- package/src/icons/resolveWorkbenchIcon.ts +48 -48
- package/src/icons/types.ts +28 -28
- package/src/index.ts +735 -735
- package/src/jdw/JdwPreview.tsx +195 -195
- package/src/jdw/JdwPreviewViewport.tsx +190 -190
- package/src/jdw/JsonCodeEditorPane.tsx +424 -424
- package/src/jdw/builtins/renderBuiltinWidgetLeaf.tsx +130 -130
- package/src/jdw/createBuiltinJdwRegistry.ts +436 -436
- package/src/jdw/cssRenderBackend.tsx +223 -223
- package/src/jdw/document.ts +35 -35
- package/src/jdw/fixtures/jdw-fixtures.ts +52 -52
- package/src/jdw/fixtures/jdw-sample-screens.ts +524 -524
- package/src/jdw/index.ts +50 -50
- package/src/jdw/jdw-preview-viewport.css +41 -41
- package/src/jdw/parse.ts +1 -1
- package/src/jdw/renderJdw.tsx +73 -73
- package/src/json-config/JsonConfigWorkbench.tsx +313 -313
- package/src/json-config/index.ts +6 -6
- package/src/json-config/json-config-editor-state.ts +42 -42
- package/src/layout/ViewEmptyState.tsx +14 -14
- package/src/layout/WorkbenchCanvas.tsx +956 -956
- package/src/layout/WorkbenchFullscreen.tsx +368 -368
- package/src/layout/WorkbenchLayout.tsx +8 -8
- package/src/layout/WorkbenchLayoutBase.tsx +335 -335
- package/src/layout/WorkbenchMediaPreviewViewport.tsx +122 -122
- package/src/layout/WorkbenchPropertyOverrideLabel.tsx +90 -90
- package/src/layout/WorkbenchPropertyPanel.tsx +633 -633
- package/src/layout/WorkbenchPropertySearch.tsx +51 -51
- package/src/layout/WorkbenchTree.tsx +350 -350
- package/src/layout/canvas/canvas-drag-ghost.css +21 -21
- package/src/layout/canvas/canvas-drag-preview-frame.css +15 -15
- package/src/layout/canvas/canvas-drop-indicator.css +16 -16
- package/src/layout/canvas/canvas-frame-handle.css +37 -37
- package/src/layout/canvas/canvas-frame-surface.css +24 -24
- package/src/layout/canvas/canvas-guide-block.css +17 -17
- package/src/layout/canvas/canvas-guide-layer.css +6 -6
- package/src/layout/canvas/canvas-guide-line.css +22 -22
- package/src/layout/canvas/canvas-item-badge.css +23 -23
- package/src/layout/canvas/canvas-item-frame.css +54 -54
- package/src/layout/canvas/canvas-pane-surface.css +9 -9
- package/src/layout/canvas/canvas-placeholder.css +21 -21
- package/src/layout/canvas/canvas-resize-frame.css +9 -9
- package/src/layout/canvas/canvas-resize-handle.css +74 -74
- package/src/layout/canvas/canvas-resize-preview.css +11 -11
- package/src/layout/canvas/canvas-selection-marquee.css +14 -14
- package/src/layout/canvas/canvas-viewport.css +18 -18
- package/src/layout/canvas/index.css +19 -19
- package/src/layout/canvas/preview-canvas.css +144 -144
- package/src/layout/editor/editor-body.css +9 -9
- package/src/layout/editor/editor-bottom-panel.css +44 -44
- package/src/layout/editor/editor-frame.css +14 -14
- package/src/layout/editor/editor-viewport.css +7 -7
- package/src/layout/editor/floating-menu.css +91 -91
- package/src/layout/editor/index.css +10 -10
- package/src/layout/editor/parse-error.css +9 -9
- package/src/layout/editor/problem-item.css +52 -52
- package/src/layout/editor/problem-list.css +7 -7
- package/src/layout/editor/render-surface.css +13 -13
- package/src/layout/fillScrollContract.ts +49 -49
- package/src/layout/fullscreen/fullscreen-backdrop.css +31 -31
- package/src/layout/fullscreen/fullscreen-button.css +44 -44
- package/src/layout/fullscreen/fullscreen-carousel.css +20 -20
- package/src/layout/fullscreen/fullscreen-content.css +8 -8
- package/src/layout/fullscreen/fullscreen-empty.css +16 -16
- package/src/layout/fullscreen/fullscreen-header.css +77 -77
- package/src/layout/fullscreen/fullscreen-hero.css +41 -41
- package/src/layout/fullscreen/fullscreen-nav-button.css +29 -29
- package/src/layout/fullscreen/fullscreen-option.css +83 -83
- package/src/layout/fullscreen/fullscreen-pill-row.css +6 -6
- package/src/layout/fullscreen/fullscreen-pill.css +10 -10
- package/src/layout/fullscreen/fullscreen-root.css +11 -11
- package/src/layout/fullscreen/index.css +13 -13
- package/src/layout/index.ts +156 -156
- package/src/layout/layout-base/banner.css +32 -32
- package/src/layout/layout-base/center.css +7 -7
- package/src/layout/layout-base/column.css +7 -7
- package/src/layout/layout-base/divider.css +5 -5
- package/src/layout/layout-base/empty.css +12 -12
- package/src/layout/layout-base/fill-scroll.css +36 -36
- package/src/layout/layout-base/fill.css +18 -18
- package/src/layout/layout-base/index.css +13 -13
- package/src/layout/layout-base/pane.css +6 -6
- package/src/layout/layout-base/panel-scroll.css +5 -5
- package/src/layout/layout-base/panel-surface.css +14 -14
- package/src/layout/layout-base/root.css +9 -9
- package/src/layout/layout-base/section-title.css +8 -8
- package/src/layout/layout.css +10 -10
- package/src/layout/layoutHelpers.ts +27 -27
- package/src/layout/media/index.css +2 -2
- package/src/layout/media/media-preview-viewport.css +69 -69
- package/src/layout/panel/Panel.tsx +161 -161
- package/src/layout/panel/TemplateGallery.tsx +90 -90
- package/src/layout/panel/WorkbenchAuthoringShell.tsx +57 -57
- package/src/layout/panel/WorkbenchLabeledPane.tsx +81 -81
- package/src/layout/panel/WorkbenchSidebarStack.tsx +15 -15
- package/src/layout/panel/filter-bar.css +59 -59
- package/src/layout/panel/index.css +7 -7
- package/src/layout/panel/index.ts +70 -70
- package/src/layout/panel/preview-pane.css +63 -63
- package/src/layout/panel/template-gallery.css +131 -131
- package/src/layout/panel/workbench-authoring-shell.css +48 -48
- package/src/layout/panel/workbench-labeled-pane.css +69 -69
- package/src/layout/panel/workbench-sidebar-stack.css +18 -18
- package/src/layout/property/color-input.css +8 -8
- package/src/layout/property/color-row.css +6 -6
- package/src/layout/property/index.css +15 -15
- package/src/layout/property/property-card.css +8 -8
- package/src/layout/property/property-grid.css +24 -24
- package/src/layout/property/property-hint.css +4 -4
- package/src/layout/property/property-inline.css +10 -10
- package/src/layout/property/property-key-value.css +29 -29
- package/src/layout/property/property-override-label.css +34 -34
- package/src/layout/property/property-panel.css +20 -20
- package/src/layout/property/property-row.css +17 -17
- package/src/layout/property/property-search.css +43 -43
- package/src/layout/property/property-section.css +97 -97
- package/src/layout/property/property-stack.css +11 -11
- package/src/layout/property/property-toggle.css +14 -14
- package/src/layout/propertyFieldFilter.ts +84 -84
- package/src/layout/sidebar/SideBarViewFrame.tsx +235 -235
- package/src/layout/sidebar/SideBarViewTabStrip.tsx +60 -60
- package/src/layout/sidebar/SideBarViewTitleMenu.tsx +75 -75
- package/src/layout/sidebar/SidebarActionIconBar.tsx +242 -242
- package/src/layout/sidebar/SidebarSectionNestingContext.tsx +21 -21
- package/src/layout/sidebar/SidebarSlotChrome.tsx +32 -32
- package/src/layout/sidebar/SidebarToolbar.tsx +10 -10
- package/src/layout/sidebar/WorkbenchActionSidebar.tsx +73 -73
- package/src/layout/sidebar/WorkbenchSidebarActions.tsx +328 -328
- package/src/layout/sidebar/WorkbenchSidebarSectionStack.tsx +165 -165
- package/src/layout/sidebar/index.css +5 -5
- package/src/layout/sidebar/index.ts +55 -55
- package/src/layout/sidebar/sidebar-chrome.css +95 -95
- package/src/layout/sidebar/sidebar-section-stack.css +25 -25
- package/src/layout/sidebar/sidebar-section.css +124 -124
- package/src/layout/sidebar/sidebar-view.css +290 -290
- package/src/layout/tree/drag-preview.css +25 -25
- package/src/layout/tree/index.css +10 -10
- package/src/layout/tree/template-glyph.css +96 -96
- package/src/layout/tree/tree-action.css +20 -20
- package/src/layout/tree/tree-drag-overlay.css +13 -13
- package/src/layout/tree/tree-drop-line.css +17 -17
- package/src/layout/tree/tree-drop-zone.css +13 -13
- package/src/layout/tree/tree-expander.css +23 -23
- package/src/layout/tree/tree-item.css +93 -93
- package/src/layout/tree/tree.css +49 -49
- package/src/layout/usePreviewViewport.ts +428 -428
- package/src/modal/ConfirmDialog.tsx +61 -61
- package/src/modal/Modal.tsx +213 -213
- package/src/modal/ModalResizeHandles.tsx +40 -40
- package/src/modal/ModalTitlebar.tsx +86 -86
- package/src/modal/modal.css +291 -291
- package/src/modal/modalContainer.ts +36 -36
- package/src/modal/modalPosition.ts +36 -36
- package/src/modal/modalSize.ts +84 -84
- package/src/modal/modalTypes.ts +13 -13
- package/src/modal/useModalContainerViewport.ts +41 -41
- package/src/modal/useModalDrag.ts +118 -118
- package/src/modal/useModalResize.ts +122 -122
- package/src/modal/useModalWindowFrame.ts +122 -122
- package/src/overlay/ContextMenu.tsx +124 -124
- package/src/overlay/clampFixedOverlayPosition.ts +22 -22
- package/src/overlay/context-menu.css +127 -127
- package/src/overlay/index.ts +31 -31
- package/src/overlay/measureAnchoredOverlayPanel.ts +107 -107
- package/src/overlay/overlay.css +2 -2
- package/src/overlay/pointerPassthroughRegion.ts +83 -83
- package/src/overlay/useAnchoredOverlayPanel.ts +222 -222
- package/src/overlay/useClampedFixedOverlayPosition.ts +27 -27
- package/src/overlay/useContextMenuState.ts +59 -59
- package/src/overlay/useFixedOverlayDismiss.ts +49 -49
- package/src/overlay/usePointerPassthroughRegion.ts +87 -87
- package/src/primitives/absolute-box/AbsoluteBox.tsx +46 -46
- package/src/primitives/absolute-box/index.ts +1 -1
- package/src/primitives/badge/Badge.tsx +14 -14
- package/src/primitives/badge/badge.css +20 -20
- package/src/primitives/badge/index.ts +1 -1
- package/src/primitives/button/Button.tsx +45 -45
- package/src/primitives/button/button.css +76 -76
- package/src/primitives/button/index.ts +1 -1
- package/src/primitives/catalog-browse-card/CatalogBrowseCard.tsx +121 -121
- package/src/primitives/catalog-browse-card/catalog-browse-card.css +175 -175
- package/src/primitives/catalog-browse-card/index.ts +1 -1
- package/src/primitives/catalog-browse-pane/CatalogBrowseFacetChips.tsx +59 -59
- package/src/primitives/catalog-browse-pane/CatalogBrowsePane.tsx +360 -360
- package/src/primitives/catalog-browse-pane/catalog-browse-facet-chips.css +8 -8
- package/src/primitives/catalog-browse-pane/catalog-browse-pane.css +84 -84
- package/src/primitives/catalog-browse-pane/filterCatalogBrowseItems.ts +27 -27
- package/src/primitives/catalog-browse-pane/index.ts +3 -3
- package/src/primitives/catalog-filter-overlay/CatalogFilterOverlay.tsx +73 -73
- package/src/primitives/catalog-filter-overlay/catalog-filter-overlay.css +65 -65
- package/src/primitives/catalog-filter-overlay/index.ts +1 -1
- package/src/primitives/checkbox/Checkbox.tsx +40 -40
- package/src/primitives/checkbox/checkbox.css +81 -81
- package/src/primitives/checkbox/index.ts +1 -1
- package/src/primitives/chip/Chip.tsx +43 -43
- package/src/primitives/chip/chip.css +110 -110
- package/src/primitives/chip/index.ts +2 -2
- package/src/primitives/clearable-text-input/ClearableTextInput.tsx +78 -78
- package/src/primitives/clearable-text-input/clearable-text-input.css +15 -15
- package/src/primitives/clearable-text-input/index.ts +1 -1
- package/src/primitives/codicon/Codicon.tsx +20 -20
- package/src/primitives/codicon/index.ts +1 -1
- package/src/primitives/empty-state/EmptyState.tsx +33 -33
- package/src/primitives/empty-state/empty-state.css +20 -20
- package/src/primitives/empty-state/index.ts +1 -1
- package/src/primitives/external-link-button/ExternalLinkButton.tsx +56 -56
- package/src/primitives/external-link-button/index.ts +1 -1
- package/src/primitives/field/Field.tsx +39 -39
- package/src/primitives/field/field.css +51 -51
- package/src/primitives/field/index.ts +1 -1
- package/src/primitives/file-icon/FileIcon.tsx +21 -21
- package/src/primitives/file-icon/index.ts +1 -1
- package/src/primitives/icon-button/IconButton.tsx +37 -37
- package/src/primitives/icon-button/icon-button.css +46 -46
- package/src/primitives/icon-button/index.ts +1 -1
- package/src/primitives/index.ts +265 -265
- package/src/primitives/library-detail-layout/LibraryDetailLayout.tsx +135 -135
- package/src/primitives/library-detail-layout/index.ts +1 -1
- package/src/primitives/library-detail-layout/library-detail-layout.css +169 -169
- package/src/primitives/library-facet-filter-panel/LibraryFacetFilterPanel.tsx +454 -454
- package/src/primitives/library-facet-filter-panel/index.ts +1 -1
- package/src/primitives/library-facet-filter-panel/library-facet-filter-panel.css +205 -205
- package/src/primitives/library-facet-filter-strip/LibraryFacetFilterStrip.tsx +527 -527
- package/src/primitives/library-facet-filter-strip/index.ts +1 -1
- package/src/primitives/library-facet-filter-strip/library-facet-filter-strip.css +31 -31
- package/src/primitives/list/List.tsx +120 -120
- package/src/primitives/list/index.ts +1 -1
- package/src/primitives/list/list-empty-state.css +10 -10
- package/src/primitives/list/list-item.css +93 -93
- package/src/primitives/list/list.css +6 -6
- package/src/primitives/number-input/NumberInput.tsx +235 -235
- package/src/primitives/number-input/index.ts +1 -1
- package/src/primitives/number-input/number-input.css +105 -105
- package/src/primitives/panel-loading/PanelLoading.tsx +29 -29
- package/src/primitives/panel-loading/index.ts +1 -1
- package/src/primitives/panel-loading/panel-loading.css +16 -16
- package/src/primitives/panel-surface/panel-surface.css +77 -77
- package/src/primitives/primitives.css +36 -36
- package/src/primitives/record-media-hero/RecordMediaHero.tsx +81 -81
- package/src/primitives/record-media-hero/index.ts +1 -1
- package/src/primitives/record-media-hero/record-media-hero.css +84 -84
- package/src/primitives/scroll-area/ScrollArea.tsx +200 -200
- package/src/primitives/scroll-area/index.ts +1 -1
- package/src/primitives/scroll-area/scroll-area-overlay.css +69 -69
- package/src/primitives/scroll-area-infinite-load/ScrollAreaInfiniteSentinel.tsx +22 -22
- package/src/primitives/scroll-area-infinite-load/index.ts +2 -2
- package/src/primitives/scroll-area-infinite-load/scroll-area-infinite-sentinel.css +6 -6
- package/src/primitives/scroll-area-infinite-load/useScrollAreaInfiniteLoad.ts +84 -84
- package/src/primitives/searchable-multi-select/SearchableMultiSelect.tsx +378 -378
- package/src/primitives/searchable-multi-select/index.ts +9 -9
- package/src/primitives/searchable-multi-select/overlay.ts +125 -125
- package/src/primitives/searchable-multi-select/searchable-multi-select.css +129 -129
- package/src/primitives/select/Select.tsx +284 -284
- package/src/primitives/select/index.ts +2 -2
- package/src/primitives/select/options.ts +38 -38
- package/src/primitives/select/overlay.ts +82 -82
- package/src/primitives/select/select.app.css +12 -12
- package/src/primitives/select/select.css +177 -177
- package/src/primitives/select/types.ts +18 -18
- package/src/primitives/styles.css +6 -6
- package/src/primitives/tabbed-panels/TabbedPanels.tsx +68 -68
- package/src/primitives/tabbed-panels/index.ts +2 -2
- package/src/primitives/tabbed-panels/tabbed-panels.css +58 -58
- package/src/primitives/text-area/TextArea.tsx +37 -37
- package/src/primitives/text-area/index.ts +1 -1
- package/src/primitives/text-input/TextInput.tsx +34 -34
- package/src/primitives/text-input/index.ts +1 -1
- package/src/primitives/text-input/input.css +54 -54
- package/src/primitives/toolbar/Toolbar.tsx +8 -8
- package/src/primitives/toolbar/index.ts +1 -1
- package/src/primitives/visually-hidden/visually-hidden.css +11 -11
- package/src/primitives/workbench-editor/WorkbenchEditor.tsx +432 -432
- package/src/primitives/workbench-editor/button-group.css +10 -10
- package/src/primitives/workbench-editor/editor-tabs.css +201 -201
- package/src/primitives/workbench-editor/index.ts +1 -1
- package/src/primitives/workbench-editor/resizable-panels.css +53 -53
- package/src/primitives/workbench-editor/segmented-control.css +98 -98
- package/src/primitives/workbench-media-slot/WorkbenchMediaPlaceholder.tsx +25 -25
- package/src/primitives/workbench-media-slot/WorkbenchMediaSlot.tsx +60 -60
- package/src/primitives/workbench-media-slot/index.ts +2 -2
- package/src/primitives/workbench-media-slot/media-slot.css +72 -72
- package/src/primitives/workbench-thumbnail/WorkbenchThumbnail.tsx +38 -38
- package/src/primitives/workbench-thumbnail/index.ts +1 -1
- package/src/primitives/workbench-thumbnail/thumbnail.css +69 -69
- package/src/scrollbars.css +144 -144
- package/src/styles.css +17 -17
- package/src/test-utils/workbenchMonacoMock.tsx +37 -37
- package/src/utils/codicon.ts +17 -17
- package/src/utils/dragMime.ts +89 -89
- package/src/utils/useWorkbenchMediaImage.ts +37 -37
- package/src/widget-asset/WidgetAssetEditor.tsx +142 -142
- package/src/widget-asset/WidgetAssetMetadataForm.tsx +82 -82
- package/src/widget-asset/WidgetAssetModeControls.tsx +42 -42
- package/src/widget-asset/WidgetAssetSourceEditor.tsx +50 -50
- package/src/widget-asset/WidgetAssetWorkbench.tsx +111 -111
- package/src/widget-asset/index.ts +17 -17
- package/src/widget-asset/widget-asset-document.ts +37 -37
- package/src/widget-asset/widget-asset-mode.ts +3 -3
- package/src/widget-studio/builtin-widget-asset-catalog.ts +12 -12
- package/src/widget-studio/create-widget-studio-workspace-editor.tsx +78 -78
- package/src/widget-studio/index.ts +10 -10
- package/src/widget-studio/resolve-widget-studio-asset-catalog.ts +43 -43
- package/src/widget-tree/WidgetAssetPalette.tsx +105 -105
- package/src/widget-tree/WidgetInspectorPanel.tsx +316 -316
- package/src/widget-tree/WidgetSourceEditor.tsx +143 -143
- package/src/widget-tree/WidgetTreeCanvasPreview.tsx +875 -875
- package/src/widget-tree/WidgetTreeLab.tsx +600 -600
- package/src/widget-tree/WidgetTreeModeControls.tsx +73 -73
- package/src/widget-tree/WidgetTreeSidePanel.tsx +39 -39
- package/src/widget-tree/WidgetTreeView.tsx +797 -797
- package/src/widget-tree/WidgetTreeWorkbench.tsx +155 -155
- package/src/widget-tree/WidgetTreeWorkspaceShell.tsx +172 -172
- package/src/widget-tree/demo-registry.ts +32 -32
- package/src/widget-tree/demo-widget-assets.ts +4 -4
- package/src/widget-tree/index.ts +45 -45
- package/src/widget-tree/widget-placement-asset-dnd.ts +35 -35
- package/src/widget-tree/widget-tree-document.ts +14 -14
- package/src/widget-tree/widget-tree-editor-state.ts +40 -40
- package/src/widget-tree/widget-tree-layout.ts +76 -76
- package/src/widget-tree/widget-tree-mode.ts +7 -7
- package/src/widget-tree/widget-tree-play-helpers.ts +85 -85
- package/src/workbench/ActivityBar.tsx +159 -159
- package/src/workbench/ArtifactShell.tsx +397 -397
- package/src/workbench/CommandGroupShell.tsx +131 -131
- package/src/workbench/CommandList.tsx +167 -167
- package/src/workbench/CommandPalette.tsx +417 -417
- package/src/workbench/ConfirmationFlow.tsx +243 -243
- package/src/workbench/MarkdownPreview.tsx +147 -147
- package/src/workbench/ShortcutCommandBridge.tsx +365 -365
- package/src/workbench/SplitView.tsx +485 -485
- package/src/workbench/StatusBar.tsx +180 -180
- package/src/workbench/StructuredArtifactEditor.tsx +205 -205
- package/src/workbench/Timeline.tsx +297 -297
- package/src/workbench/WorkbenchCanvasShell.defaults.ts +106 -106
- package/src/workbench/WorkbenchCanvasShell.model.ts +46 -46
- package/src/workbench/WorkbenchCanvasShell.tsx +827 -827
- package/src/workbench/WorkbenchDesktopTitleBar.tsx +126 -126
- package/src/workbench/WorkbenchEditorTabs.tsx +52 -52
- package/src/workbench/WorkbenchModalPortal.tsx +52 -52
- package/src/workbench/WorkbenchPlatformContext.tsx +31 -31
- package/src/workbench/WorkbenchShell.tsx +192 -192
- package/src/workbench/WorkbenchShellTitleBarLayoutControls.tsx +80 -80
- package/src/workbench/WorkbenchStandaloneShell.tsx +604 -604
- package/src/workbench/WorkbenchThemeProvider.tsx +142 -142
- package/src/workbench/WorkbenchViewEditor.tsx +75 -75
- package/src/workbench/WorkbenchViewSidebar.tsx +85 -85
- package/src/workbench/WorkbenchWindowChromeControls.tsx +290 -290
- package/src/workbench/activityBarOrder.ts +98 -98
- package/src/workbench/appearanceLabels.ts +28 -28
- package/src/workbench/auth/WorkbenchAuthGate.tsx +61 -61
- package/src/workbench/auth/WorkbenchLoginView.tsx +633 -633
- package/src/workbench/auth/auth-flows.css +440 -440
- package/src/workbench/auth/auth.css +3 -3
- package/src/workbench/auth/index.ts +19 -19
- package/src/workbench/auth/profile-modals.css +115 -115
- package/src/workbench/bootstrap/WorkbenchBootstrapGate.tsx +45 -45
- package/src/workbench/bootstrap/WorkbenchBootstrapView.tsx +100 -100
- package/src/workbench/bootstrap/index.ts +12 -12
- package/src/workbench/bootstrap/useWorkbenchBootstrap.ts +130 -130
- package/src/workbench/chat/ChatCommandProposalCard.tsx +115 -115
- package/src/workbench/chat/ChatComposer.tsx +163 -163
- package/src/workbench/chat/ChatConversationBar.tsx +134 -134
- package/src/workbench/chat/ChatHistoryMenu.tsx +171 -171
- package/src/workbench/chat/ChatHistoryMenuRow.tsx +74 -74
- package/src/workbench/chat/ChatMessageCollapsible.tsx +115 -115
- package/src/workbench/chat/ChatMessageItem.tsx +318 -318
- package/src/workbench/chat/ChatMessageList.tsx +134 -134
- package/src/workbench/chat/ChatPanel.tsx +168 -168
- package/src/workbench/chat/ChatPhasedRunProgress.tsx +202 -202
- package/src/workbench/chat/ChatRenameDialog.tsx +77 -77
- package/src/workbench/chat/chat-conversation.css +273 -273
- package/src/workbench/chat/chat-ui.css +1124 -1124
- package/src/workbench/chat/chat.css +3 -3
- package/src/workbench/chat/chatConversation.ts +46 -46
- package/src/workbench/chat/chatHistoryRowMenuItems.ts +29 -29
- package/src/workbench/chat/chatMessageMeta.tsx +252 -252
- package/src/workbench/chat/chatScroll.ts +22 -22
- package/src/workbench/chat/index.ts +64 -64
- package/src/workbench/chat/slashCommand.ts +26 -26
- package/src/workbench/chat/types.ts +45 -45
- package/src/workbench/chat/useCancelRuntimeOnUnmount.ts +60 -60
- package/src/workbench/chat/useChatPrependPagination.ts +233 -233
- package/src/workbench/chat/useChatRuntimeState.ts +64 -64
- package/src/workbench/chat/useSlashCommandSuggest.ts +154 -154
- package/src/workbench/chrome/index.css +6 -6
- package/src/workbench/chrome/workbench-desktop-titlebar.css +60 -60
- package/src/workbench/chrome/workbench-host.css +85 -85
- package/src/workbench/chrome/workbench-layout-regions.css +42 -42
- package/src/workbench/chrome/workbench-platform-chrome.css +142 -142
- package/src/workbench/chrome/workbench-shell-titlebar.css +124 -124
- package/src/workbench/command-execution-policy.ts +61 -61
- package/src/workbench/command-model.ts +275 -275
- package/src/workbench/command-palette/command-palette.css +156 -156
- package/src/workbench/commands.ts +576 -576
- package/src/workbench/editor-tabs/index.ts +22 -22
- package/src/workbench/editor-tabs-dnd.ts +162 -162
- package/src/workbench/editorTabContextMenu.ts +111 -111
- package/src/workbench/hardReset.ts +64 -64
- package/src/workbench/index.ts +675 -675
- package/src/workbench/jdw/jdw-editors.css +4 -4
- package/src/workbench/jdw/jdw.css +2 -2
- package/src/workbench/keyboard.ts +53 -53
- package/src/workbench/management/AccountManagementPanel.tsx +82 -82
- package/src/workbench/management/CommandInspectorPanel.tsx +127 -127
- package/src/workbench/management/CommandManagementPanel.tsx +129 -129
- package/src/workbench/management/CommandManagementSidebar.tsx +318 -318
- package/src/workbench/management/ExtensionManagementPanel.tsx +498 -498
- package/src/workbench/management/ExtensionManagementSidebar.tsx +492 -492
- package/src/workbench/management/IntegrationCommandActions.tsx +78 -78
- package/src/workbench/management/IntegrationSettingsSurface.tsx +290 -290
- package/src/workbench/management/IntegrationsShell.tsx +53 -53
- package/src/workbench/management/KeybindingCaptureField.tsx +142 -142
- package/src/workbench/management/KeybindingManagementPanel.tsx +120 -120
- package/src/workbench/management/LibraryCatalogPickerDialog.tsx +208 -208
- package/src/workbench/management/LibraryFacetFilterDialog.tsx +62 -62
- package/src/workbench/management/ManagementCard.tsx +89 -89
- package/src/workbench/management/ManagementFilterChips.tsx +41 -41
- package/src/workbench/management/ManagementGroup.tsx +56 -56
- package/src/workbench/management/ManagementPanelControls.tsx +43 -43
- package/src/workbench/management/ManagementPanelFrame.tsx +109 -109
- package/src/workbench/management/WorkbenchDialogFrame.tsx +148 -148
- package/src/workbench/management/WorkbenchNotice.tsx +211 -211
- package/src/workbench/management/build-command-management-groups.ts +157 -157
- package/src/workbench/management/command-inspector-uri.ts +26 -26
- package/src/workbench/management/extension-category-display.ts +42 -42
- package/src/workbench/management/format-command-run-state.ts +19 -19
- package/src/workbench/management/index.ts +130 -130
- package/src/workbench/management/integration-command-action.ts +25 -25
- package/src/workbench/management/library-catalog-picker-dialog.css +56 -56
- package/src/workbench/management/library-facet-filter-dialog.css +8 -8
- package/src/workbench/management/management-panel.css +444 -444
- package/src/workbench/management/management.css +4 -4
- package/src/workbench/management/types.ts +126 -126
- package/src/workbench/markdownRemarkPlugins.ts +4 -4
- package/src/workbench/schema/index.ts +47 -47
- package/src/workbench/schema/workbenchDocument.ts +31 -31
- package/src/workbench/schema/workbenchDocumentActions.ts +6 -6
- package/src/workbench/schema/workbenchDocumentAdapter.ts +9 -9
- package/src/workbench/schema/workbenchDocumentPatch.ts +13 -13
- package/src/workbench/schema/workbenchDocumentRenderer.tsx +702 -702
- package/src/workbench/settings/NavigationPanel.tsx +86 -86
- package/src/workbench/settings/SchemaForm.tsx +433 -433
- package/src/workbench/settings/SectionTabPanel.tsx +172 -172
- package/src/workbench/settings/SectionedPanel.tsx +169 -169
- package/src/workbench/settings/StructuredDataForm.tsx +619 -619
- package/src/workbench/settings/StructuredDataSchemaPanel.tsx +811 -811
- package/src/workbench/settings/StructuredDataSchemaPanelEmbed.tsx +21 -21
- package/src/workbench/settings/StructuredDataSchemaPanelFrame.tsx +76 -76
- package/src/workbench/settings/StructuredDataTableView.tsx +127 -127
- package/src/workbench/settings/WorkbenchPanelRegion.tsx +46 -46
- package/src/workbench/settings/WorkbenchSettingsModal.tsx +251 -251
- package/src/workbench/settings/WorkbenchSettingsNav.tsx +57 -57
- package/src/workbench/settings/WorkbenchSettingsSection.tsx +30 -30
- package/src/workbench/settings/extensionSettingsForm.ts +34 -34
- package/src/workbench/settings/index.ts +177 -177
- package/src/workbench/settings/navigation-panel.css +59 -59
- package/src/workbench/settings/panel-region.css +22 -22
- package/src/workbench/settings/schema-form.css +36 -36
- package/src/workbench/settings/schemaFormSettingSpec.ts +177 -177
- package/src/workbench/settings/schemaFormSettingsCategory.tsx +21 -21
- package/src/workbench/settings/section-tab-panel.css +157 -157
- package/src/workbench/settings/sectionScrollSpyDom.ts +139 -139
- package/src/workbench/settings/sectioned-panel.css +162 -162
- package/src/workbench/settings/sectionedPanelScrollSpy.ts +273 -273
- package/src/workbench/settings/settings-responsive.css +58 -58
- package/src/workbench/settings/settings.css +11 -11
- package/src/workbench/settings/settingsCommit.tsx +55 -55
- package/src/workbench/settings/structured-data-form.css +114 -114
- package/src/workbench/settings/structured-data-schema-panel.css +344 -344
- package/src/workbench/settings/structured-data-table-view.css +85 -85
- package/src/workbench/settings/structuredDataFormModel.ts +140 -140
- package/src/workbench/settings/structuredDataSchema.ts +339 -339
- package/src/workbench/settings/structuredDataSchemaDate.ts +197 -197
- package/src/workbench/settings/structuredDataSchemaField.ts +105 -105
- package/src/workbench/settings/structuredDataSchemaSection.ts +74 -74
- package/src/workbench/settings/structuredDataSchemaTable.ts +120 -120
- package/src/workbench/settings/structuredDataSchemaTypes.ts +308 -308
- package/src/workbench/settings/structuredDataSchemaValidation.ts +166 -166
- package/src/workbench/settings/types.ts +16 -16
- package/src/workbench/settings/useSectionScrollSpy.ts +420 -420
- package/src/workbench/settings/workbench-settings-modal.css +273 -273
- package/src/workbench/shell/activity-bar.css +245 -245
- package/src/workbench/shell/confirmation-flow.css +87 -87
- package/src/workbench/shell/index.css +5 -5
- package/src/workbench/shell/split-view.css +188 -188
- package/src/workbench/shell/status-bar.css +141 -141
- package/src/workbench/shell.ts +146 -146
- package/src/workbench/shellPresets.ts +36 -36
- package/src/workbench/shellState.ts +196 -196
- package/src/workbench/shellViewModel.ts +160 -160
- package/src/workbench/sidebarViewPlacementDnd.ts +61 -61
- package/src/workbench/standalone.ts +101 -101
- package/src/workbench/status.ts +133 -133
- package/src/workbench/theme.ts +50 -50
- package/src/workbench/themePresets.ts +191 -191
- package/src/workbench/useDeferredWorkbenchMount.ts +71 -71
- package/src/workbench/useEditorTabsStripDnd.ts +262 -262
- package/src/workbench/useWorkbenchAppearanceDocumentSync.tsx +17 -17
- package/src/workbench/useWorkbenchEditorTabContextMenu.tsx +66 -66
- package/src/workbench/useWorkbenchSidebarActionBarDnd.ts +227 -227
- package/src/workbench/useWorkbenchSidebarLayout.tsx +232 -232
- package/src/workbench/useWorkbenchSidebarViewPlacementDropZone.ts +194 -194
- package/src/workbench/useWorkbenchStandaloneShellStateSync.tsx +112 -112
- package/src/workbench/views/action-list.css +144 -144
- package/src/workbench/views/artifact-preview.css +184 -184
- package/src/workbench/views/command-inspector.css +98 -98
- package/src/workbench/views/command-list.css +356 -356
- package/src/workbench/views/commands-sidebar.css +51 -51
- package/src/workbench/views/extensions-sidebar.css +175 -175
- package/src/workbench/views/multi-provider-explorer.css +182 -182
- package/src/workbench/views/search.css +10 -10
- package/src/workbench/views/timeline.css +239 -239
- package/src/workbench/views/view-hosts.css +37 -37
- package/src/workbench/views/view-sidebar.css +21 -21
- package/src/workbench/views/views.css +12 -12
- package/src/workbench/widgets/widget-tree.css +598 -598
- package/src/workbench/widgets/widgets.css +2 -2
- package/src/workbench/workbench-chrome.css +4 -4
- package/src/workbench/workbenchContextMenu.ts +18 -18
- package/src/workbench/workbenchModalViewState.ts +52 -52
- package/src/workbench/workbenchOverlaysContext.tsx +21 -21
- package/src/workbench/workbenchPlatformChrome.ts +40 -40
- package/src/workbench/workbenchSidebarLayoutContext.ts +46 -46
- package/src/workbench/workbenchStandaloneShellReactContext.tsx +80 -80
- package/src/workbench/workbenchViewRouteState.ts +151 -151
- package/src/workbench/workspace/WorkspaceDraftsContext.tsx +99 -99
- package/src/workbench/workspace/WorkspaceEditor.tsx +165 -165
- package/src/workbench/workspace/WorkspaceEditorPanel.tsx +393 -393
- package/src/workbench/workspace/WorkspaceExplorer.tsx +630 -630
- package/src/workbench/workspace/WorkspaceExplorerPanel.tsx +73 -73
- package/src/workbench/workspace/WorkspaceFileIcon.tsx +23 -23
- package/src/workbench/workspace/WorkspaceHighlightedText.tsx +22 -22
- package/src/workbench/workspace/WorkspacePathLabel.tsx +42 -42
- package/src/workbench/workspace/WorkspaceSearchPanel.tsx +116 -116
- package/src/workbench/workspace/WorkspaceSearchResults.tsx +51 -51
- package/src/workbench/workspace/build-workspace-explorer-nodes.ts +44 -44
- package/src/workbench/workspace/createVirtualWorkspaceExplorerPort.ts +141 -141
- package/src/workbench/workspace/explorer-tree-style.ts +18 -18
- package/src/workbench/workspace/index.ts +169 -169
- package/src/workbench/workspace/json-config-workbench.css +57 -57
- package/src/workbench/workspace/mimeType.ts +45 -45
- package/src/workbench/workspace/monaco-panel.css +71 -71
- package/src/workbench/workspace/path.ts +12 -12
- 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 -111
- package/src/workbench/workspace/useWorkspaceExplorerController.ts +490 -490
- package/src/workbench/workspace/workspace-editor.css +235 -235
- package/src/workbench/workspace/workspace-explorer-panel.css +33 -33
- package/src/workbench/workspace/workspace.css +5 -5
- package/src/workbench/workspace/workspaceExplorerController.ts +193 -193
- package/src/workbench/workspace/workspaceJsonDiagnostics.ts +83 -83
- package/src/workbench-menu-surfaces.css +57 -57
|
@@ -1,797 +1,797 @@
|
|
|
1
|
-
import { useEffect, useMemo, useRef, useState, type DragEvent, type KeyboardEvent } from 'react';
|
|
2
|
-
import type { WidgetPlacementAsset } from '@workbench-kit/contracts';
|
|
3
|
-
import {
|
|
4
|
-
collectWidgetNodes,
|
|
5
|
-
firstSelectedWidgetPath,
|
|
6
|
-
getWidgetDisplayLabel,
|
|
7
|
-
getWidgetChildren,
|
|
8
|
-
isWidgetPathSelected,
|
|
9
|
-
widgetPathEquals,
|
|
10
|
-
widgetPathKey,
|
|
11
|
-
type GenericWidget,
|
|
12
|
-
type WidgetNode,
|
|
13
|
-
type WidgetPath,
|
|
14
|
-
type WidgetSelectionState,
|
|
15
|
-
} from '@workbench-kit/jdw';
|
|
16
|
-
|
|
17
|
-
import { Panel, PanelBody } from '../layout/panel';
|
|
18
|
-
import { EmptyState } from '../primitives/empty-state';
|
|
19
|
-
import { cx } from '../utils/cx';
|
|
20
|
-
import { readWidgetPlacementAssetDragData } from './widget-placement-asset-dnd.js';
|
|
21
|
-
import {
|
|
22
|
-
canAddChildren,
|
|
23
|
-
formatWidgetPlacementMeta,
|
|
24
|
-
insertedWidgetPathForParent,
|
|
25
|
-
} from './widget-tree-layout.js';
|
|
26
|
-
|
|
27
|
-
export interface WidgetTreeViewProps {
|
|
28
|
-
readonly root: GenericWidget | null;
|
|
29
|
-
readonly parseError: string | null;
|
|
30
|
-
readonly selection?: WidgetSelectionState | undefined;
|
|
31
|
-
readonly onSelectPath?: ((path: WidgetPath) => void) | undefined;
|
|
32
|
-
readonly onActivatePath?: ((path: WidgetPath) => void) | undefined;
|
|
33
|
-
readonly onDeletePath?: ((path: WidgetPath) => void) | undefined;
|
|
34
|
-
readonly onMovePath?: ((operation: WidgetTreeMoveOperation) => void) | undefined;
|
|
35
|
-
readonly onPlaceAssetPath?: ((operation: WidgetTreeAssetDropOperation) => void) | undefined;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export function isWidgetTreeActivateKey(key: string): boolean {
|
|
39
|
-
return key === 'Enter';
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
type WidgetTreeNavigationKey = 'ArrowDown' | 'ArrowUp' | 'Home' | 'End';
|
|
43
|
-
type WidgetTreeHorizontalNavigationKey = 'ArrowLeft' | 'ArrowRight';
|
|
44
|
-
type WidgetTreeMoveDirection = 'up' | 'down';
|
|
45
|
-
export type WidgetTreeDropPlacement = 'before' | 'inside' | 'after';
|
|
46
|
-
|
|
47
|
-
export interface WidgetTreeReorderOperation {
|
|
48
|
-
readonly kind: 'reorder';
|
|
49
|
-
readonly path: WidgetPath;
|
|
50
|
-
readonly parentPath: WidgetPath;
|
|
51
|
-
readonly fromIndex: number;
|
|
52
|
-
readonly toIndex: number;
|
|
53
|
-
readonly nextPath: WidgetPath;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export interface WidgetTreeReparentOperation {
|
|
57
|
-
readonly kind: 'reparent';
|
|
58
|
-
readonly fromPath: WidgetPath;
|
|
59
|
-
readonly toParentPath: WidgetPath;
|
|
60
|
-
readonly insertIndex: number;
|
|
61
|
-
readonly nextPath: WidgetPath;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export type WidgetTreeMoveOperation = WidgetTreeReorderOperation | WidgetTreeReparentOperation;
|
|
65
|
-
|
|
66
|
-
export type WidgetTreeHorizontalNavigationAction =
|
|
67
|
-
| { readonly type: 'collapse'; readonly path: WidgetPath }
|
|
68
|
-
| { readonly type: 'expand'; readonly path: WidgetPath }
|
|
69
|
-
| { readonly type: 'select'; readonly path: WidgetPath };
|
|
70
|
-
|
|
71
|
-
export interface WidgetTreeAssetDropOperation {
|
|
72
|
-
readonly asset: WidgetPlacementAsset;
|
|
73
|
-
readonly parentPath: WidgetPath;
|
|
74
|
-
readonly insertIndex: number;
|
|
75
|
-
readonly nextPath: WidgetPath;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
interface WidgetTreeDropTarget {
|
|
79
|
-
readonly pathKey: string;
|
|
80
|
-
readonly placement: WidgetTreeDropPlacement;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export interface WidgetTreeDropPlacementGeometry {
|
|
84
|
-
readonly targetPath: WidgetPath;
|
|
85
|
-
readonly targetWidget: GenericWidget;
|
|
86
|
-
readonly offsetY: number;
|
|
87
|
-
readonly height: number;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function selectedNodeIndex(
|
|
91
|
-
nodes: readonly WidgetNode[],
|
|
92
|
-
selection: WidgetSelectionState | undefined,
|
|
93
|
-
): number {
|
|
94
|
-
if (!selection) return -1;
|
|
95
|
-
return nodes.findIndex((node) => isWidgetPathSelected(selection, node.path));
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
export function resolveWidgetTreeNavigationPath(
|
|
99
|
-
nodes: readonly WidgetNode[],
|
|
100
|
-
selection: WidgetSelectionState | undefined,
|
|
101
|
-
key: WidgetTreeNavigationKey,
|
|
102
|
-
): WidgetPath | null {
|
|
103
|
-
if (nodes.length === 0) return null;
|
|
104
|
-
|
|
105
|
-
const currentIndex = selectedNodeIndex(nodes, selection);
|
|
106
|
-
const fallbackIndex = currentIndex >= 0 ? currentIndex : 0;
|
|
107
|
-
|
|
108
|
-
if (key === 'Home') return nodes[0]?.path ?? null;
|
|
109
|
-
if (key === 'End') return nodes[nodes.length - 1]?.path ?? null;
|
|
110
|
-
if (key === 'ArrowDown')
|
|
111
|
-
return nodes[Math.min(nodes.length - 1, fallbackIndex + 1)]?.path ?? null;
|
|
112
|
-
return nodes[Math.max(0, fallbackIndex - 1)]?.path ?? null;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function directChildPathOf(
|
|
116
|
-
nodes: readonly WidgetNode[],
|
|
117
|
-
parentPath: WidgetPath,
|
|
118
|
-
): WidgetPath | null {
|
|
119
|
-
return (
|
|
120
|
-
nodes.find((node) => {
|
|
121
|
-
if (node.path.length !== parentPath.length + 1) return false;
|
|
122
|
-
return isPathPrefix(parentPath, node.path);
|
|
123
|
-
})?.path ?? null
|
|
124
|
-
);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
export function resolveWidgetTreeHorizontalNavigationAction(
|
|
128
|
-
visibleNodes: readonly WidgetNode[],
|
|
129
|
-
selection: WidgetSelectionState | undefined,
|
|
130
|
-
collapsedPathKeys: ReadonlySet<string>,
|
|
131
|
-
key: WidgetTreeHorizontalNavigationKey,
|
|
132
|
-
): WidgetTreeHorizontalNavigationAction | null {
|
|
133
|
-
const current = visibleNodes[selectedNodeIndex(visibleNodes, selection)];
|
|
134
|
-
if (!current) return null;
|
|
135
|
-
|
|
136
|
-
const currentPathKey = widgetPathKey(current.path);
|
|
137
|
-
const isCollapsed = collapsedPathKeys.has(currentPathKey);
|
|
138
|
-
const hasChildren = hasCollapsibleWidgetChildren(current.widget);
|
|
139
|
-
|
|
140
|
-
if (key === 'ArrowRight') {
|
|
141
|
-
if (!hasChildren) return null;
|
|
142
|
-
if (isCollapsed) return { type: 'expand', path: current.path };
|
|
143
|
-
|
|
144
|
-
const childPath = directChildPathOf(visibleNodes, current.path);
|
|
145
|
-
return childPath ? { type: 'select', path: childPath } : null;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
if (hasChildren && !isCollapsed) {
|
|
149
|
-
return { type: 'collapse', path: current.path };
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
if (current.path.length === 0) return null;
|
|
153
|
-
return { type: 'select', path: current.path.slice(0, -1) };
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
export function isRootWidgetPath(path: WidgetPath): boolean {
|
|
157
|
-
return path.length === 0;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
function arrayChildSegment(
|
|
161
|
-
path: WidgetPath,
|
|
162
|
-
): { readonly kind: 'children'; readonly index: number } | null {
|
|
163
|
-
const segment = path[path.length - 1];
|
|
164
|
-
return segment?.kind === 'children' ? segment : null;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
function pathAtIndex(parentPath: WidgetPath, index: number): WidgetPath {
|
|
168
|
-
return [...parentPath, { kind: 'children', index }];
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function hasNodeAtPath(nodes: readonly WidgetNode[], path: WidgetPath): boolean {
|
|
172
|
-
const key = widgetPathKey(path);
|
|
173
|
-
return nodes.some((node) => widgetPathKey(node.path) === key);
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
function nodeAtPath(nodes: readonly WidgetNode[], path: WidgetPath): WidgetNode | null {
|
|
177
|
-
const key = widgetPathKey(path);
|
|
178
|
-
return nodes.find((node) => widgetPathKey(node.path) === key) ?? null;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
function isPathPrefix(prefix: WidgetPath, path: WidgetPath): boolean {
|
|
182
|
-
if (prefix.length > path.length) return false;
|
|
183
|
-
return prefix.every((segment, index) => {
|
|
184
|
-
const candidate = path[index];
|
|
185
|
-
if (!candidate || segment.kind !== candidate.kind) return false;
|
|
186
|
-
return (
|
|
187
|
-
segment.kind === 'child' ||
|
|
188
|
-
(candidate.kind === 'children' && segment.index === candidate.index)
|
|
189
|
-
);
|
|
190
|
-
});
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
function isGenericWidget(value: unknown): value is GenericWidget {
|
|
194
|
-
return (
|
|
195
|
-
value !== null &&
|
|
196
|
-
!Array.isArray(value) &&
|
|
197
|
-
typeof value === 'object' &&
|
|
198
|
-
typeof (value as GenericWidget).type === 'string'
|
|
199
|
-
);
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
export function hasCollapsibleWidgetChildren(widget: GenericWidget): boolean {
|
|
203
|
-
return getWidgetChildren(widget).length > 0 || isGenericWidget(widget.child);
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
function hasCollapsedAncestor(path: WidgetPath, collapsedPathKeys: ReadonlySet<string>): boolean {
|
|
207
|
-
for (let depth = 0; depth < path.length; depth++) {
|
|
208
|
-
if (collapsedPathKeys.has(widgetPathKey(path.slice(0, depth)))) {
|
|
209
|
-
return true;
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
return false;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
export function filterVisibleWidgetNodes(
|
|
217
|
-
nodes: readonly WidgetNode[],
|
|
218
|
-
collapsedPathKeys: ReadonlySet<string>,
|
|
219
|
-
): WidgetNode[] {
|
|
220
|
-
if (collapsedPathKeys.size === 0) return [...nodes];
|
|
221
|
-
return nodes.filter((node) => !hasCollapsedAncestor(node.path, collapsedPathKeys));
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
function adjustPathAfterRemoval(path: WidgetPath, removedPath: WidgetPath): WidgetPath {
|
|
225
|
-
const removedSegment = removedPath[removedPath.length - 1];
|
|
226
|
-
if (removedSegment?.kind !== 'children') return path;
|
|
227
|
-
|
|
228
|
-
const removedParentPath = removedPath.slice(0, -1);
|
|
229
|
-
return path.map((segment, depth) => {
|
|
230
|
-
if (segment.kind !== 'children' || depth !== removedParentPath.length) return segment;
|
|
231
|
-
|
|
232
|
-
const sameParent = removedParentPath.every((ancestorSegment, index) => {
|
|
233
|
-
const candidate = path[index];
|
|
234
|
-
if (!candidate || ancestorSegment.kind !== candidate.kind) return false;
|
|
235
|
-
return (
|
|
236
|
-
ancestorSegment.kind === 'child' ||
|
|
237
|
-
(candidate.kind === 'children' && ancestorSegment.index === candidate.index)
|
|
238
|
-
);
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
if (sameParent && removedSegment.index < segment.index) {
|
|
242
|
-
return { kind: 'children', index: segment.index - 1 } as const;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
return segment;
|
|
246
|
-
});
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
function createMoveOperation(
|
|
250
|
-
nodes: readonly WidgetNode[],
|
|
251
|
-
path: WidgetPath,
|
|
252
|
-
toIndex: number,
|
|
253
|
-
): WidgetTreeReorderOperation | null {
|
|
254
|
-
const segment = arrayChildSegment(path);
|
|
255
|
-
if (!segment || toIndex < 0 || segment.index === toIndex) return null;
|
|
256
|
-
|
|
257
|
-
const parentPath = path.slice(0, -1);
|
|
258
|
-
const nextPath = pathAtIndex(parentPath, toIndex);
|
|
259
|
-
if (!hasNodeAtPath(nodes, nextPath)) return null;
|
|
260
|
-
|
|
261
|
-
return {
|
|
262
|
-
kind: 'reorder',
|
|
263
|
-
path,
|
|
264
|
-
parentPath,
|
|
265
|
-
fromIndex: segment.index,
|
|
266
|
-
toIndex,
|
|
267
|
-
nextPath,
|
|
268
|
-
};
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
export function resolveWidgetTreeMoveOperation(
|
|
272
|
-
nodes: readonly WidgetNode[],
|
|
273
|
-
selection: WidgetSelectionState | undefined,
|
|
274
|
-
direction: WidgetTreeMoveDirection,
|
|
275
|
-
): WidgetTreeMoveOperation | null {
|
|
276
|
-
const current = nodes[selectedNodeIndex(nodes, selection)];
|
|
277
|
-
if (!current) return null;
|
|
278
|
-
|
|
279
|
-
const segment = arrayChildSegment(current.path);
|
|
280
|
-
if (!segment) return null;
|
|
281
|
-
|
|
282
|
-
const toIndex = direction === 'up' ? segment.index - 1 : segment.index + 1;
|
|
283
|
-
return createMoveOperation(nodes, current.path, toIndex);
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
function createReparentOperation(
|
|
287
|
-
nodes: readonly WidgetNode[],
|
|
288
|
-
sourcePath: WidgetPath,
|
|
289
|
-
targetParentPath: WidgetPath,
|
|
290
|
-
insertIndex: number,
|
|
291
|
-
): WidgetTreeReparentOperation | null {
|
|
292
|
-
if (isRootWidgetPath(sourcePath)) return null;
|
|
293
|
-
if (isPathPrefix(sourcePath, targetParentPath)) return null;
|
|
294
|
-
|
|
295
|
-
const targetNode = nodeAtPath(nodes, targetParentPath);
|
|
296
|
-
if (!targetNode || !canAddChildren(targetNode.widget)) return null;
|
|
297
|
-
|
|
298
|
-
const sourceSegment = arrayChildSegment(sourcePath);
|
|
299
|
-
const targetChildren = getWidgetChildren(targetNode.widget);
|
|
300
|
-
if (insertIndex < 0 || insertIndex > targetChildren.length) return null;
|
|
301
|
-
|
|
302
|
-
const sameParent =
|
|
303
|
-
sourceSegment !== null && widgetPathEquals(sourcePath.slice(0, -1), targetParentPath);
|
|
304
|
-
const targetIndex =
|
|
305
|
-
sameParent && sourceSegment.index < insertIndex ? insertIndex - 1 : insertIndex;
|
|
306
|
-
if (sameParent && sourceSegment?.index === targetIndex) return null;
|
|
307
|
-
|
|
308
|
-
const adjustedParentPath = adjustPathAfterRemoval(targetParentPath, sourcePath);
|
|
309
|
-
const nextPath = insertedWidgetPathForParent(targetNode.widget, adjustedParentPath, targetIndex);
|
|
310
|
-
|
|
311
|
-
return {
|
|
312
|
-
kind: 'reparent',
|
|
313
|
-
fromPath: sourcePath,
|
|
314
|
-
toParentPath: targetParentPath,
|
|
315
|
-
insertIndex,
|
|
316
|
-
nextPath,
|
|
317
|
-
};
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
export function resolveWidgetTreeDropOperation(
|
|
321
|
-
nodes: readonly WidgetNode[],
|
|
322
|
-
sourcePath: WidgetPath,
|
|
323
|
-
targetPath: WidgetPath,
|
|
324
|
-
placement: WidgetTreeDropPlacement = 'inside',
|
|
325
|
-
): WidgetTreeMoveOperation | null {
|
|
326
|
-
const targetNode = nodeAtPath(nodes, targetPath);
|
|
327
|
-
if (!targetNode) return null;
|
|
328
|
-
|
|
329
|
-
if (placement === 'inside') {
|
|
330
|
-
if (!canAddChildren(targetNode.widget)) return null;
|
|
331
|
-
|
|
332
|
-
return createReparentOperation(
|
|
333
|
-
nodes,
|
|
334
|
-
sourcePath,
|
|
335
|
-
targetPath,
|
|
336
|
-
getWidgetChildren(targetNode.widget).length,
|
|
337
|
-
);
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
const target = arrayChildSegment(targetPath);
|
|
341
|
-
if (!target) return null;
|
|
342
|
-
|
|
343
|
-
return createReparentOperation(
|
|
344
|
-
nodes,
|
|
345
|
-
sourcePath,
|
|
346
|
-
targetPath.slice(0, -1),
|
|
347
|
-
target.index + (placement === 'after' ? 1 : 0),
|
|
348
|
-
);
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
export function resolveWidgetTreeAssetDropOperation(
|
|
352
|
-
nodes: readonly WidgetNode[],
|
|
353
|
-
asset: WidgetPlacementAsset,
|
|
354
|
-
targetPath: WidgetPath,
|
|
355
|
-
placement: WidgetTreeDropPlacement = 'inside',
|
|
356
|
-
): WidgetTreeAssetDropOperation | null {
|
|
357
|
-
const targetNode = nodeAtPath(nodes, targetPath);
|
|
358
|
-
if (!targetNode) return null;
|
|
359
|
-
|
|
360
|
-
if (placement === 'inside') {
|
|
361
|
-
if (!canAddChildren(targetNode.widget)) return null;
|
|
362
|
-
|
|
363
|
-
const insertIndex = getWidgetChildren(targetNode.widget).length;
|
|
364
|
-
return {
|
|
365
|
-
asset,
|
|
366
|
-
parentPath: targetPath,
|
|
367
|
-
insertIndex,
|
|
368
|
-
nextPath: insertedWidgetPathForParent(targetNode.widget, targetPath, insertIndex),
|
|
369
|
-
};
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
const target = arrayChildSegment(targetPath);
|
|
373
|
-
if (!target) return null;
|
|
374
|
-
|
|
375
|
-
const parentPath = targetPath.slice(0, -1);
|
|
376
|
-
const parentNode = nodeAtPath(nodes, parentPath);
|
|
377
|
-
if (!parentNode || !canAddChildren(parentNode.widget)) return null;
|
|
378
|
-
|
|
379
|
-
const insertIndex = target.index + (placement === 'after' ? 1 : 0);
|
|
380
|
-
return {
|
|
381
|
-
asset,
|
|
382
|
-
parentPath,
|
|
383
|
-
insertIndex,
|
|
384
|
-
nextPath: insertedWidgetPathForParent(parentNode.widget, parentPath, insertIndex),
|
|
385
|
-
};
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
function parentTypeForNode(
|
|
389
|
-
nodes: ReturnType<typeof collectWidgetNodes>,
|
|
390
|
-
path: WidgetPath,
|
|
391
|
-
): string | undefined {
|
|
392
|
-
if (path.length === 0) return undefined;
|
|
393
|
-
const parentPath = path.slice(0, -1);
|
|
394
|
-
const parentKey = widgetPathKey(parentPath);
|
|
395
|
-
return nodes.find((node) => widgetPathKey(node.path) === parentKey)?.widget.type;
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
export function resolveWidgetTreeDropPlacement({
|
|
399
|
-
targetPath,
|
|
400
|
-
targetWidget,
|
|
401
|
-
offsetY,
|
|
402
|
-
height,
|
|
403
|
-
}: WidgetTreeDropPlacementGeometry): WidgetTreeDropPlacement {
|
|
404
|
-
const resolvedHeight = Math.max(1, height);
|
|
405
|
-
|
|
406
|
-
if (isRootWidgetPath(targetPath) && canAddChildren(targetWidget)) {
|
|
407
|
-
return 'inside';
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
if (canAddChildren(targetWidget)) {
|
|
411
|
-
if (offsetY < resolvedHeight / 3) return 'before';
|
|
412
|
-
if (offsetY > (resolvedHeight * 2) / 3) return 'after';
|
|
413
|
-
return 'inside';
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
return offsetY < resolvedHeight / 2 ? 'before' : 'after';
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
function resolveDropPlacementFromEvent(
|
|
420
|
-
event: DragEvent<HTMLLIElement>,
|
|
421
|
-
targetNode: WidgetNode,
|
|
422
|
-
): WidgetTreeDropPlacement {
|
|
423
|
-
const rect = event.currentTarget.getBoundingClientRect();
|
|
424
|
-
return resolveWidgetTreeDropPlacement({
|
|
425
|
-
targetPath: targetNode.path,
|
|
426
|
-
targetWidget: targetNode.widget,
|
|
427
|
-
offsetY: event.clientY - rect.top,
|
|
428
|
-
height: rect.height,
|
|
429
|
-
});
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
export function WidgetTreeView({
|
|
433
|
-
root,
|
|
434
|
-
parseError,
|
|
435
|
-
selection,
|
|
436
|
-
onSelectPath,
|
|
437
|
-
onActivatePath,
|
|
438
|
-
onDeletePath,
|
|
439
|
-
onMovePath,
|
|
440
|
-
onPlaceAssetPath,
|
|
441
|
-
}: WidgetTreeViewProps) {
|
|
442
|
-
const nodes = useMemo(() => (root ? collectWidgetNodes(root) : []), [root]);
|
|
443
|
-
const selectedPath = selection ? firstSelectedWidgetPath(selection) : null;
|
|
444
|
-
const selectedPathKey = selectedPath ? widgetPathKey(selectedPath) : null;
|
|
445
|
-
const [collapsedPathKeys, setCollapsedPathKeys] = useState<ReadonlySet<string>>(() => new Set());
|
|
446
|
-
const [dragPath, setDragPath] = useState<WidgetPath | null>(null);
|
|
447
|
-
const [dropTarget, setDropTarget] = useState<WidgetTreeDropTarget | null>(null);
|
|
448
|
-
const treeButtonRefs = useRef(new Map<string, HTMLButtonElement>());
|
|
449
|
-
const visibleNodes = useMemo(
|
|
450
|
-
() => filterVisibleWidgetNodes(nodes, collapsedPathKeys),
|
|
451
|
-
[collapsedPathKeys, nodes],
|
|
452
|
-
);
|
|
453
|
-
const selectedIndex = selectedNodeIndex(visibleNodes, selection);
|
|
454
|
-
|
|
455
|
-
const setPathCollapsed = (path: WidgetPath, collapsed: boolean) => {
|
|
456
|
-
const pathKey = widgetPathKey(path);
|
|
457
|
-
setCollapsedPathKeys((current) => {
|
|
458
|
-
const alreadyCollapsed = current.has(pathKey);
|
|
459
|
-
if (alreadyCollapsed === collapsed) return current;
|
|
460
|
-
|
|
461
|
-
const next = new Set(current);
|
|
462
|
-
if (collapsed) {
|
|
463
|
-
next.add(pathKey);
|
|
464
|
-
} else {
|
|
465
|
-
next.delete(pathKey);
|
|
466
|
-
}
|
|
467
|
-
return next;
|
|
468
|
-
});
|
|
469
|
-
};
|
|
470
|
-
|
|
471
|
-
const focusPath = (path: WidgetPath) => {
|
|
472
|
-
treeButtonRefs.current.get(widgetPathKey(path))?.focus();
|
|
473
|
-
};
|
|
474
|
-
|
|
475
|
-
useEffect(() => {
|
|
476
|
-
if (!selectedPath) return;
|
|
477
|
-
|
|
478
|
-
setCollapsedPathKeys((current) => {
|
|
479
|
-
let changed = false;
|
|
480
|
-
const next = new Set(current);
|
|
481
|
-
|
|
482
|
-
for (let depth = 0; depth < selectedPath.length; depth++) {
|
|
483
|
-
const ancestorKey = widgetPathKey(selectedPath.slice(0, depth));
|
|
484
|
-
if (next.delete(ancestorKey)) {
|
|
485
|
-
changed = true;
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
return changed ? next : current;
|
|
490
|
-
});
|
|
491
|
-
}, [selectedPathKey, selectedPath]);
|
|
492
|
-
|
|
493
|
-
const handleHorizontalNavigation = (key: WidgetTreeHorizontalNavigationKey): boolean => {
|
|
494
|
-
const action = resolveWidgetTreeHorizontalNavigationAction(
|
|
495
|
-
visibleNodes,
|
|
496
|
-
selection,
|
|
497
|
-
collapsedPathKeys,
|
|
498
|
-
key,
|
|
499
|
-
);
|
|
500
|
-
if (!action) return false;
|
|
501
|
-
|
|
502
|
-
if (action.type === 'collapse') {
|
|
503
|
-
setPathCollapsed(action.path, true);
|
|
504
|
-
focusPath(action.path);
|
|
505
|
-
return true;
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
if (action.type === 'expand') {
|
|
509
|
-
setPathCollapsed(action.path, false);
|
|
510
|
-
focusPath(action.path);
|
|
511
|
-
return true;
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
onSelectPath?.(action.path);
|
|
515
|
-
focusPath(action.path);
|
|
516
|
-
return true;
|
|
517
|
-
};
|
|
518
|
-
|
|
519
|
-
const handleKeyDown = (event: KeyboardEvent<HTMLUListElement>) => {
|
|
520
|
-
if (event.altKey && (event.key === 'ArrowUp' || event.key === 'ArrowDown')) {
|
|
521
|
-
const operation = resolveWidgetTreeMoveOperation(
|
|
522
|
-
nodes,
|
|
523
|
-
selection,
|
|
524
|
-
event.key === 'ArrowUp' ? 'up' : 'down',
|
|
525
|
-
);
|
|
526
|
-
if (operation && onMovePath) {
|
|
527
|
-
event.preventDefault();
|
|
528
|
-
onMovePath(operation);
|
|
529
|
-
}
|
|
530
|
-
return;
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
if (event.key === 'ArrowLeft' || event.key === 'ArrowRight') {
|
|
534
|
-
if (handleHorizontalNavigation(event.key)) {
|
|
535
|
-
event.preventDefault();
|
|
536
|
-
}
|
|
537
|
-
return;
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
if (
|
|
541
|
-
event.key === 'ArrowDown' ||
|
|
542
|
-
event.key === 'ArrowUp' ||
|
|
543
|
-
event.key === 'Home' ||
|
|
544
|
-
event.key === 'End'
|
|
545
|
-
) {
|
|
546
|
-
const nextPath = resolveWidgetTreeNavigationPath(visibleNodes, selection, event.key);
|
|
547
|
-
if (nextPath) {
|
|
548
|
-
event.preventDefault();
|
|
549
|
-
onSelectPath?.(nextPath);
|
|
550
|
-
focusPath(nextPath);
|
|
551
|
-
}
|
|
552
|
-
return;
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
if (event.key === 'Delete' || event.key === 'Backspace') {
|
|
556
|
-
const current = visibleNodes[selectedIndex];
|
|
557
|
-
if (current && !isRootWidgetPath(current.path)) {
|
|
558
|
-
event.preventDefault();
|
|
559
|
-
onDeletePath?.(current.path);
|
|
560
|
-
}
|
|
561
|
-
return;
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
const current = visibleNodes[selectedIndex];
|
|
565
|
-
if (current && isWidgetTreeActivateKey(event.key) && onActivatePath) {
|
|
566
|
-
event.preventDefault();
|
|
567
|
-
onSelectPath?.(current.path);
|
|
568
|
-
onActivatePath(current.path);
|
|
569
|
-
}
|
|
570
|
-
};
|
|
571
|
-
|
|
572
|
-
const handleToggleCollapse = (path: WidgetPath, isCollapsed: boolean) => {
|
|
573
|
-
setPathCollapsed(path, !isCollapsed);
|
|
574
|
-
|
|
575
|
-
if (
|
|
576
|
-
!isCollapsed &&
|
|
577
|
-
selectedPath &&
|
|
578
|
-
isPathPrefix(path, selectedPath) &&
|
|
579
|
-
!widgetPathEquals(path, selectedPath)
|
|
580
|
-
) {
|
|
581
|
-
onSelectPath?.(path);
|
|
582
|
-
}
|
|
583
|
-
};
|
|
584
|
-
|
|
585
|
-
const handleDragStart = (event: DragEvent<HTMLLIElement>, path: WidgetPath) => {
|
|
586
|
-
if (!onMovePath || isRootWidgetPath(path)) {
|
|
587
|
-
event.preventDefault();
|
|
588
|
-
return;
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
setDragPath(path);
|
|
592
|
-
event.dataTransfer.effectAllowed = 'move';
|
|
593
|
-
event.dataTransfer.setData('application/x-workbench-widget-path', widgetPathKey(path));
|
|
594
|
-
};
|
|
595
|
-
|
|
596
|
-
const handleDragOver = (event: DragEvent<HTMLLIElement>, targetNode: WidgetNode) => {
|
|
597
|
-
const asset = onPlaceAssetPath ? readWidgetPlacementAssetDragData(event.dataTransfer) : null;
|
|
598
|
-
if (asset) {
|
|
599
|
-
const placement = resolveDropPlacementFromEvent(event, targetNode);
|
|
600
|
-
const operation = resolveWidgetTreeAssetDropOperation(
|
|
601
|
-
nodes,
|
|
602
|
-
asset,
|
|
603
|
-
targetNode.path,
|
|
604
|
-
placement,
|
|
605
|
-
);
|
|
606
|
-
const targetKey = widgetPathKey(targetNode.path);
|
|
607
|
-
if (!operation) {
|
|
608
|
-
setDropTarget(null);
|
|
609
|
-
return;
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
event.preventDefault();
|
|
613
|
-
event.dataTransfer.dropEffect = 'copy';
|
|
614
|
-
setDropTarget({ pathKey: targetKey, placement });
|
|
615
|
-
return;
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
if (!dragPath) return;
|
|
619
|
-
|
|
620
|
-
const placement = resolveDropPlacementFromEvent(event, targetNode);
|
|
621
|
-
const operation = resolveWidgetTreeDropOperation(nodes, dragPath, targetNode.path, placement);
|
|
622
|
-
const targetKey = widgetPathKey(targetNode.path);
|
|
623
|
-
if (!operation) {
|
|
624
|
-
setDropTarget(null);
|
|
625
|
-
return;
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
event.preventDefault();
|
|
629
|
-
event.dataTransfer.dropEffect = 'move';
|
|
630
|
-
setDropTarget({ pathKey: targetKey, placement });
|
|
631
|
-
};
|
|
632
|
-
|
|
633
|
-
const handleDragLeave = (event: DragEvent<HTMLLIElement>, path: WidgetPath) => {
|
|
634
|
-
const relatedTarget = event.relatedTarget;
|
|
635
|
-
if (relatedTarget instanceof Node && event.currentTarget.contains(relatedTarget)) return;
|
|
636
|
-
|
|
637
|
-
const targetKey = widgetPathKey(path);
|
|
638
|
-
setDropTarget((current) => (current?.pathKey === targetKey ? null : current));
|
|
639
|
-
};
|
|
640
|
-
|
|
641
|
-
const handleDrop = (event: DragEvent<HTMLLIElement>, targetNode: WidgetNode) => {
|
|
642
|
-
const asset = onPlaceAssetPath ? readWidgetPlacementAssetDragData(event.dataTransfer) : null;
|
|
643
|
-
if (asset) {
|
|
644
|
-
const placement =
|
|
645
|
-
dropTarget?.pathKey === widgetPathKey(targetNode.path)
|
|
646
|
-
? dropTarget.placement
|
|
647
|
-
: resolveDropPlacementFromEvent(event, targetNode);
|
|
648
|
-
const operation = resolveWidgetTreeAssetDropOperation(
|
|
649
|
-
nodes,
|
|
650
|
-
asset,
|
|
651
|
-
targetNode.path,
|
|
652
|
-
placement,
|
|
653
|
-
);
|
|
654
|
-
if (!operation) {
|
|
655
|
-
setDropTarget(null);
|
|
656
|
-
return;
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
event.preventDefault();
|
|
660
|
-
onPlaceAssetPath?.(operation);
|
|
661
|
-
setDragPath(null);
|
|
662
|
-
setDropTarget(null);
|
|
663
|
-
return;
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
if (!dragPath || !onMovePath) {
|
|
667
|
-
setDropTarget(null);
|
|
668
|
-
return;
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
const placement =
|
|
672
|
-
dropTarget?.pathKey === widgetPathKey(targetNode.path)
|
|
673
|
-
? dropTarget.placement
|
|
674
|
-
: resolveDropPlacementFromEvent(event, targetNode);
|
|
675
|
-
const operation = resolveWidgetTreeDropOperation(nodes, dragPath, targetNode.path, placement);
|
|
676
|
-
if (!operation) {
|
|
677
|
-
setDropTarget(null);
|
|
678
|
-
return;
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
event.preventDefault();
|
|
682
|
-
onMovePath(operation);
|
|
683
|
-
setDragPath(null);
|
|
684
|
-
setDropTarget(null);
|
|
685
|
-
};
|
|
686
|
-
|
|
687
|
-
return (
|
|
688
|
-
<Panel className="widget-tree-outline" data-testid="widget-tree-outline-panel">
|
|
689
|
-
<PanelBody className="widget-tree-outline__body">
|
|
690
|
-
{parseError !== null ? (
|
|
691
|
-
<EmptyState compact icon="codicon-error">
|
|
692
|
-
{parseError}
|
|
693
|
-
</EmptyState>
|
|
694
|
-
) : root === null ? (
|
|
695
|
-
<EmptyState compact icon="codicon-list-tree">
|
|
696
|
-
No widget root.
|
|
697
|
-
</EmptyState>
|
|
698
|
-
) : (
|
|
699
|
-
<ul
|
|
700
|
-
aria-label="Widget outline"
|
|
701
|
-
aria-keyshortcuts="ArrowLeft ArrowRight Alt+ArrowUp Alt+ArrowDown Delete Backspace"
|
|
702
|
-
className="widget-tree-outline__list"
|
|
703
|
-
role="tree"
|
|
704
|
-
onKeyDown={handleKeyDown}
|
|
705
|
-
>
|
|
706
|
-
{visibleNodes.map((node) => {
|
|
707
|
-
const pathKey = widgetPathKey(node.path);
|
|
708
|
-
const depth = node.path.length;
|
|
709
|
-
const isSelected = selection
|
|
710
|
-
? isWidgetPathSelected(selection, node.path)
|
|
711
|
-
: isRootWidgetPath(node.path);
|
|
712
|
-
const parentType = parentTypeForNode(nodes, node.path);
|
|
713
|
-
const placementMeta = formatWidgetPlacementMeta(node.widget, parentType);
|
|
714
|
-
const canMove = Boolean(onMovePath && !isRootWidgetPath(node.path));
|
|
715
|
-
const hasChildren = hasCollapsibleWidgetChildren(node.widget);
|
|
716
|
-
const isCollapsed = collapsedPathKeys.has(pathKey);
|
|
717
|
-
const dropPlacement = dropTarget?.pathKey === pathKey ? dropTarget.placement : null;
|
|
718
|
-
const displayLabel = getWidgetDisplayLabel(node.widget);
|
|
719
|
-
const textPreview =
|
|
720
|
-
node.widget.type === 'text' && typeof node.widget.text === 'string'
|
|
721
|
-
? node.widget.text
|
|
722
|
-
: null;
|
|
723
|
-
|
|
724
|
-
return (
|
|
725
|
-
<li
|
|
726
|
-
key={pathKey}
|
|
727
|
-
aria-level={depth + 1}
|
|
728
|
-
aria-expanded={hasChildren ? !isCollapsed : undefined}
|
|
729
|
-
aria-selected={isSelected}
|
|
730
|
-
className={cx(
|
|
731
|
-
'widget-tree-outline__item',
|
|
732
|
-
isSelected && 'widget-tree-outline__item--selected',
|
|
733
|
-
canMove && 'widget-tree-outline__item--movable',
|
|
734
|
-
dropPlacement && `widget-tree-outline__item--drop-${dropPlacement}`,
|
|
735
|
-
)}
|
|
736
|
-
data-drop-position={dropPlacement ?? undefined}
|
|
737
|
-
data-testid={`widget-tree-node-${pathKey}`}
|
|
738
|
-
draggable={canMove}
|
|
739
|
-
role="treeitem"
|
|
740
|
-
style={{ paddingLeft: `${depth * 14 + 6}px` }}
|
|
741
|
-
onDragEnd={() => {
|
|
742
|
-
setDragPath(null);
|
|
743
|
-
setDropTarget(null);
|
|
744
|
-
}}
|
|
745
|
-
onDragLeave={(event) => handleDragLeave(event, node.path)}
|
|
746
|
-
onDragOver={(event) => handleDragOver(event, node)}
|
|
747
|
-
onDragStart={(event) => handleDragStart(event, node.path)}
|
|
748
|
-
onDrop={(event) => handleDrop(event, node)}
|
|
749
|
-
>
|
|
750
|
-
{hasChildren ? (
|
|
751
|
-
<button
|
|
752
|
-
aria-label={`${isCollapsed ? 'Expand' : 'Collapse'} ${displayLabel}`}
|
|
753
|
-
className="widget-tree-outline__twistie"
|
|
754
|
-
type="button"
|
|
755
|
-
onClick={(event) => {
|
|
756
|
-
event.stopPropagation();
|
|
757
|
-
handleToggleCollapse(node.path, isCollapsed);
|
|
758
|
-
}}
|
|
759
|
-
>
|
|
760
|
-
{isCollapsed ? '▸' : '▾'}
|
|
761
|
-
</button>
|
|
762
|
-
) : (
|
|
763
|
-
<span
|
|
764
|
-
aria-hidden="true"
|
|
765
|
-
className="widget-tree-outline__twistie widget-tree-outline__twistie--spacer"
|
|
766
|
-
/>
|
|
767
|
-
)}
|
|
768
|
-
<button
|
|
769
|
-
className="widget-tree-outline__button"
|
|
770
|
-
ref={(button) => {
|
|
771
|
-
if (button) {
|
|
772
|
-
treeButtonRefs.current.set(pathKey, button);
|
|
773
|
-
} else {
|
|
774
|
-
treeButtonRefs.current.delete(pathKey);
|
|
775
|
-
}
|
|
776
|
-
}}
|
|
777
|
-
tabIndex={isSelected ? 0 : -1}
|
|
778
|
-
type="button"
|
|
779
|
-
onClick={() => onSelectPath?.(node.path)}
|
|
780
|
-
>
|
|
781
|
-
<span className="widget-tree-outline__type">{displayLabel}</span>
|
|
782
|
-
{placementMeta ? (
|
|
783
|
-
<span className="widget-tree-outline__placement">{placementMeta}</span>
|
|
784
|
-
) : null}
|
|
785
|
-
{textPreview ? (
|
|
786
|
-
<span className="widget-tree-outline__meta">{textPreview}</span>
|
|
787
|
-
) : null}
|
|
788
|
-
</button>
|
|
789
|
-
</li>
|
|
790
|
-
);
|
|
791
|
-
})}
|
|
792
|
-
</ul>
|
|
793
|
-
)}
|
|
794
|
-
</PanelBody>
|
|
795
|
-
</Panel>
|
|
796
|
-
);
|
|
797
|
-
}
|
|
1
|
+
import { useEffect, useMemo, useRef, useState, type DragEvent, type KeyboardEvent } from 'react';
|
|
2
|
+
import type { WidgetPlacementAsset } from '@workbench-kit/contracts';
|
|
3
|
+
import {
|
|
4
|
+
collectWidgetNodes,
|
|
5
|
+
firstSelectedWidgetPath,
|
|
6
|
+
getWidgetDisplayLabel,
|
|
7
|
+
getWidgetChildren,
|
|
8
|
+
isWidgetPathSelected,
|
|
9
|
+
widgetPathEquals,
|
|
10
|
+
widgetPathKey,
|
|
11
|
+
type GenericWidget,
|
|
12
|
+
type WidgetNode,
|
|
13
|
+
type WidgetPath,
|
|
14
|
+
type WidgetSelectionState,
|
|
15
|
+
} from '@workbench-kit/jdw';
|
|
16
|
+
|
|
17
|
+
import { Panel, PanelBody } from '../layout/panel';
|
|
18
|
+
import { EmptyState } from '../primitives/empty-state';
|
|
19
|
+
import { cx } from '../utils/cx';
|
|
20
|
+
import { readWidgetPlacementAssetDragData } from './widget-placement-asset-dnd.js';
|
|
21
|
+
import {
|
|
22
|
+
canAddChildren,
|
|
23
|
+
formatWidgetPlacementMeta,
|
|
24
|
+
insertedWidgetPathForParent,
|
|
25
|
+
} from './widget-tree-layout.js';
|
|
26
|
+
|
|
27
|
+
export interface WidgetTreeViewProps {
|
|
28
|
+
readonly root: GenericWidget | null;
|
|
29
|
+
readonly parseError: string | null;
|
|
30
|
+
readonly selection?: WidgetSelectionState | undefined;
|
|
31
|
+
readonly onSelectPath?: ((path: WidgetPath) => void) | undefined;
|
|
32
|
+
readonly onActivatePath?: ((path: WidgetPath) => void) | undefined;
|
|
33
|
+
readonly onDeletePath?: ((path: WidgetPath) => void) | undefined;
|
|
34
|
+
readonly onMovePath?: ((operation: WidgetTreeMoveOperation) => void) | undefined;
|
|
35
|
+
readonly onPlaceAssetPath?: ((operation: WidgetTreeAssetDropOperation) => void) | undefined;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function isWidgetTreeActivateKey(key: string): boolean {
|
|
39
|
+
return key === 'Enter';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
type WidgetTreeNavigationKey = 'ArrowDown' | 'ArrowUp' | 'Home' | 'End';
|
|
43
|
+
type WidgetTreeHorizontalNavigationKey = 'ArrowLeft' | 'ArrowRight';
|
|
44
|
+
type WidgetTreeMoveDirection = 'up' | 'down';
|
|
45
|
+
export type WidgetTreeDropPlacement = 'before' | 'inside' | 'after';
|
|
46
|
+
|
|
47
|
+
export interface WidgetTreeReorderOperation {
|
|
48
|
+
readonly kind: 'reorder';
|
|
49
|
+
readonly path: WidgetPath;
|
|
50
|
+
readonly parentPath: WidgetPath;
|
|
51
|
+
readonly fromIndex: number;
|
|
52
|
+
readonly toIndex: number;
|
|
53
|
+
readonly nextPath: WidgetPath;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface WidgetTreeReparentOperation {
|
|
57
|
+
readonly kind: 'reparent';
|
|
58
|
+
readonly fromPath: WidgetPath;
|
|
59
|
+
readonly toParentPath: WidgetPath;
|
|
60
|
+
readonly insertIndex: number;
|
|
61
|
+
readonly nextPath: WidgetPath;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export type WidgetTreeMoveOperation = WidgetTreeReorderOperation | WidgetTreeReparentOperation;
|
|
65
|
+
|
|
66
|
+
export type WidgetTreeHorizontalNavigationAction =
|
|
67
|
+
| { readonly type: 'collapse'; readonly path: WidgetPath }
|
|
68
|
+
| { readonly type: 'expand'; readonly path: WidgetPath }
|
|
69
|
+
| { readonly type: 'select'; readonly path: WidgetPath };
|
|
70
|
+
|
|
71
|
+
export interface WidgetTreeAssetDropOperation {
|
|
72
|
+
readonly asset: WidgetPlacementAsset;
|
|
73
|
+
readonly parentPath: WidgetPath;
|
|
74
|
+
readonly insertIndex: number;
|
|
75
|
+
readonly nextPath: WidgetPath;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
interface WidgetTreeDropTarget {
|
|
79
|
+
readonly pathKey: string;
|
|
80
|
+
readonly placement: WidgetTreeDropPlacement;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface WidgetTreeDropPlacementGeometry {
|
|
84
|
+
readonly targetPath: WidgetPath;
|
|
85
|
+
readonly targetWidget: GenericWidget;
|
|
86
|
+
readonly offsetY: number;
|
|
87
|
+
readonly height: number;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function selectedNodeIndex(
|
|
91
|
+
nodes: readonly WidgetNode[],
|
|
92
|
+
selection: WidgetSelectionState | undefined,
|
|
93
|
+
): number {
|
|
94
|
+
if (!selection) return -1;
|
|
95
|
+
return nodes.findIndex((node) => isWidgetPathSelected(selection, node.path));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function resolveWidgetTreeNavigationPath(
|
|
99
|
+
nodes: readonly WidgetNode[],
|
|
100
|
+
selection: WidgetSelectionState | undefined,
|
|
101
|
+
key: WidgetTreeNavigationKey,
|
|
102
|
+
): WidgetPath | null {
|
|
103
|
+
if (nodes.length === 0) return null;
|
|
104
|
+
|
|
105
|
+
const currentIndex = selectedNodeIndex(nodes, selection);
|
|
106
|
+
const fallbackIndex = currentIndex >= 0 ? currentIndex : 0;
|
|
107
|
+
|
|
108
|
+
if (key === 'Home') return nodes[0]?.path ?? null;
|
|
109
|
+
if (key === 'End') return nodes[nodes.length - 1]?.path ?? null;
|
|
110
|
+
if (key === 'ArrowDown')
|
|
111
|
+
return nodes[Math.min(nodes.length - 1, fallbackIndex + 1)]?.path ?? null;
|
|
112
|
+
return nodes[Math.max(0, fallbackIndex - 1)]?.path ?? null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function directChildPathOf(
|
|
116
|
+
nodes: readonly WidgetNode[],
|
|
117
|
+
parentPath: WidgetPath,
|
|
118
|
+
): WidgetPath | null {
|
|
119
|
+
return (
|
|
120
|
+
nodes.find((node) => {
|
|
121
|
+
if (node.path.length !== parentPath.length + 1) return false;
|
|
122
|
+
return isPathPrefix(parentPath, node.path);
|
|
123
|
+
})?.path ?? null
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function resolveWidgetTreeHorizontalNavigationAction(
|
|
128
|
+
visibleNodes: readonly WidgetNode[],
|
|
129
|
+
selection: WidgetSelectionState | undefined,
|
|
130
|
+
collapsedPathKeys: ReadonlySet<string>,
|
|
131
|
+
key: WidgetTreeHorizontalNavigationKey,
|
|
132
|
+
): WidgetTreeHorizontalNavigationAction | null {
|
|
133
|
+
const current = visibleNodes[selectedNodeIndex(visibleNodes, selection)];
|
|
134
|
+
if (!current) return null;
|
|
135
|
+
|
|
136
|
+
const currentPathKey = widgetPathKey(current.path);
|
|
137
|
+
const isCollapsed = collapsedPathKeys.has(currentPathKey);
|
|
138
|
+
const hasChildren = hasCollapsibleWidgetChildren(current.widget);
|
|
139
|
+
|
|
140
|
+
if (key === 'ArrowRight') {
|
|
141
|
+
if (!hasChildren) return null;
|
|
142
|
+
if (isCollapsed) return { type: 'expand', path: current.path };
|
|
143
|
+
|
|
144
|
+
const childPath = directChildPathOf(visibleNodes, current.path);
|
|
145
|
+
return childPath ? { type: 'select', path: childPath } : null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (hasChildren && !isCollapsed) {
|
|
149
|
+
return { type: 'collapse', path: current.path };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (current.path.length === 0) return null;
|
|
153
|
+
return { type: 'select', path: current.path.slice(0, -1) };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function isRootWidgetPath(path: WidgetPath): boolean {
|
|
157
|
+
return path.length === 0;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function arrayChildSegment(
|
|
161
|
+
path: WidgetPath,
|
|
162
|
+
): { readonly kind: 'children'; readonly index: number } | null {
|
|
163
|
+
const segment = path[path.length - 1];
|
|
164
|
+
return segment?.kind === 'children' ? segment : null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function pathAtIndex(parentPath: WidgetPath, index: number): WidgetPath {
|
|
168
|
+
return [...parentPath, { kind: 'children', index }];
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function hasNodeAtPath(nodes: readonly WidgetNode[], path: WidgetPath): boolean {
|
|
172
|
+
const key = widgetPathKey(path);
|
|
173
|
+
return nodes.some((node) => widgetPathKey(node.path) === key);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function nodeAtPath(nodes: readonly WidgetNode[], path: WidgetPath): WidgetNode | null {
|
|
177
|
+
const key = widgetPathKey(path);
|
|
178
|
+
return nodes.find((node) => widgetPathKey(node.path) === key) ?? null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function isPathPrefix(prefix: WidgetPath, path: WidgetPath): boolean {
|
|
182
|
+
if (prefix.length > path.length) return false;
|
|
183
|
+
return prefix.every((segment, index) => {
|
|
184
|
+
const candidate = path[index];
|
|
185
|
+
if (!candidate || segment.kind !== candidate.kind) return false;
|
|
186
|
+
return (
|
|
187
|
+
segment.kind === 'child' ||
|
|
188
|
+
(candidate.kind === 'children' && segment.index === candidate.index)
|
|
189
|
+
);
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function isGenericWidget(value: unknown): value is GenericWidget {
|
|
194
|
+
return (
|
|
195
|
+
value !== null &&
|
|
196
|
+
!Array.isArray(value) &&
|
|
197
|
+
typeof value === 'object' &&
|
|
198
|
+
typeof (value as GenericWidget).type === 'string'
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function hasCollapsibleWidgetChildren(widget: GenericWidget): boolean {
|
|
203
|
+
return getWidgetChildren(widget).length > 0 || isGenericWidget(widget.child);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function hasCollapsedAncestor(path: WidgetPath, collapsedPathKeys: ReadonlySet<string>): boolean {
|
|
207
|
+
for (let depth = 0; depth < path.length; depth++) {
|
|
208
|
+
if (collapsedPathKeys.has(widgetPathKey(path.slice(0, depth)))) {
|
|
209
|
+
return true;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function filterVisibleWidgetNodes(
|
|
217
|
+
nodes: readonly WidgetNode[],
|
|
218
|
+
collapsedPathKeys: ReadonlySet<string>,
|
|
219
|
+
): WidgetNode[] {
|
|
220
|
+
if (collapsedPathKeys.size === 0) return [...nodes];
|
|
221
|
+
return nodes.filter((node) => !hasCollapsedAncestor(node.path, collapsedPathKeys));
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function adjustPathAfterRemoval(path: WidgetPath, removedPath: WidgetPath): WidgetPath {
|
|
225
|
+
const removedSegment = removedPath[removedPath.length - 1];
|
|
226
|
+
if (removedSegment?.kind !== 'children') return path;
|
|
227
|
+
|
|
228
|
+
const removedParentPath = removedPath.slice(0, -1);
|
|
229
|
+
return path.map((segment, depth) => {
|
|
230
|
+
if (segment.kind !== 'children' || depth !== removedParentPath.length) return segment;
|
|
231
|
+
|
|
232
|
+
const sameParent = removedParentPath.every((ancestorSegment, index) => {
|
|
233
|
+
const candidate = path[index];
|
|
234
|
+
if (!candidate || ancestorSegment.kind !== candidate.kind) return false;
|
|
235
|
+
return (
|
|
236
|
+
ancestorSegment.kind === 'child' ||
|
|
237
|
+
(candidate.kind === 'children' && ancestorSegment.index === candidate.index)
|
|
238
|
+
);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
if (sameParent && removedSegment.index < segment.index) {
|
|
242
|
+
return { kind: 'children', index: segment.index - 1 } as const;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return segment;
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function createMoveOperation(
|
|
250
|
+
nodes: readonly WidgetNode[],
|
|
251
|
+
path: WidgetPath,
|
|
252
|
+
toIndex: number,
|
|
253
|
+
): WidgetTreeReorderOperation | null {
|
|
254
|
+
const segment = arrayChildSegment(path);
|
|
255
|
+
if (!segment || toIndex < 0 || segment.index === toIndex) return null;
|
|
256
|
+
|
|
257
|
+
const parentPath = path.slice(0, -1);
|
|
258
|
+
const nextPath = pathAtIndex(parentPath, toIndex);
|
|
259
|
+
if (!hasNodeAtPath(nodes, nextPath)) return null;
|
|
260
|
+
|
|
261
|
+
return {
|
|
262
|
+
kind: 'reorder',
|
|
263
|
+
path,
|
|
264
|
+
parentPath,
|
|
265
|
+
fromIndex: segment.index,
|
|
266
|
+
toIndex,
|
|
267
|
+
nextPath,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export function resolveWidgetTreeMoveOperation(
|
|
272
|
+
nodes: readonly WidgetNode[],
|
|
273
|
+
selection: WidgetSelectionState | undefined,
|
|
274
|
+
direction: WidgetTreeMoveDirection,
|
|
275
|
+
): WidgetTreeMoveOperation | null {
|
|
276
|
+
const current = nodes[selectedNodeIndex(nodes, selection)];
|
|
277
|
+
if (!current) return null;
|
|
278
|
+
|
|
279
|
+
const segment = arrayChildSegment(current.path);
|
|
280
|
+
if (!segment) return null;
|
|
281
|
+
|
|
282
|
+
const toIndex = direction === 'up' ? segment.index - 1 : segment.index + 1;
|
|
283
|
+
return createMoveOperation(nodes, current.path, toIndex);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function createReparentOperation(
|
|
287
|
+
nodes: readonly WidgetNode[],
|
|
288
|
+
sourcePath: WidgetPath,
|
|
289
|
+
targetParentPath: WidgetPath,
|
|
290
|
+
insertIndex: number,
|
|
291
|
+
): WidgetTreeReparentOperation | null {
|
|
292
|
+
if (isRootWidgetPath(sourcePath)) return null;
|
|
293
|
+
if (isPathPrefix(sourcePath, targetParentPath)) return null;
|
|
294
|
+
|
|
295
|
+
const targetNode = nodeAtPath(nodes, targetParentPath);
|
|
296
|
+
if (!targetNode || !canAddChildren(targetNode.widget)) return null;
|
|
297
|
+
|
|
298
|
+
const sourceSegment = arrayChildSegment(sourcePath);
|
|
299
|
+
const targetChildren = getWidgetChildren(targetNode.widget);
|
|
300
|
+
if (insertIndex < 0 || insertIndex > targetChildren.length) return null;
|
|
301
|
+
|
|
302
|
+
const sameParent =
|
|
303
|
+
sourceSegment !== null && widgetPathEquals(sourcePath.slice(0, -1), targetParentPath);
|
|
304
|
+
const targetIndex =
|
|
305
|
+
sameParent && sourceSegment.index < insertIndex ? insertIndex - 1 : insertIndex;
|
|
306
|
+
if (sameParent && sourceSegment?.index === targetIndex) return null;
|
|
307
|
+
|
|
308
|
+
const adjustedParentPath = adjustPathAfterRemoval(targetParentPath, sourcePath);
|
|
309
|
+
const nextPath = insertedWidgetPathForParent(targetNode.widget, adjustedParentPath, targetIndex);
|
|
310
|
+
|
|
311
|
+
return {
|
|
312
|
+
kind: 'reparent',
|
|
313
|
+
fromPath: sourcePath,
|
|
314
|
+
toParentPath: targetParentPath,
|
|
315
|
+
insertIndex,
|
|
316
|
+
nextPath,
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export function resolveWidgetTreeDropOperation(
|
|
321
|
+
nodes: readonly WidgetNode[],
|
|
322
|
+
sourcePath: WidgetPath,
|
|
323
|
+
targetPath: WidgetPath,
|
|
324
|
+
placement: WidgetTreeDropPlacement = 'inside',
|
|
325
|
+
): WidgetTreeMoveOperation | null {
|
|
326
|
+
const targetNode = nodeAtPath(nodes, targetPath);
|
|
327
|
+
if (!targetNode) return null;
|
|
328
|
+
|
|
329
|
+
if (placement === 'inside') {
|
|
330
|
+
if (!canAddChildren(targetNode.widget)) return null;
|
|
331
|
+
|
|
332
|
+
return createReparentOperation(
|
|
333
|
+
nodes,
|
|
334
|
+
sourcePath,
|
|
335
|
+
targetPath,
|
|
336
|
+
getWidgetChildren(targetNode.widget).length,
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const target = arrayChildSegment(targetPath);
|
|
341
|
+
if (!target) return null;
|
|
342
|
+
|
|
343
|
+
return createReparentOperation(
|
|
344
|
+
nodes,
|
|
345
|
+
sourcePath,
|
|
346
|
+
targetPath.slice(0, -1),
|
|
347
|
+
target.index + (placement === 'after' ? 1 : 0),
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
export function resolveWidgetTreeAssetDropOperation(
|
|
352
|
+
nodes: readonly WidgetNode[],
|
|
353
|
+
asset: WidgetPlacementAsset,
|
|
354
|
+
targetPath: WidgetPath,
|
|
355
|
+
placement: WidgetTreeDropPlacement = 'inside',
|
|
356
|
+
): WidgetTreeAssetDropOperation | null {
|
|
357
|
+
const targetNode = nodeAtPath(nodes, targetPath);
|
|
358
|
+
if (!targetNode) return null;
|
|
359
|
+
|
|
360
|
+
if (placement === 'inside') {
|
|
361
|
+
if (!canAddChildren(targetNode.widget)) return null;
|
|
362
|
+
|
|
363
|
+
const insertIndex = getWidgetChildren(targetNode.widget).length;
|
|
364
|
+
return {
|
|
365
|
+
asset,
|
|
366
|
+
parentPath: targetPath,
|
|
367
|
+
insertIndex,
|
|
368
|
+
nextPath: insertedWidgetPathForParent(targetNode.widget, targetPath, insertIndex),
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const target = arrayChildSegment(targetPath);
|
|
373
|
+
if (!target) return null;
|
|
374
|
+
|
|
375
|
+
const parentPath = targetPath.slice(0, -1);
|
|
376
|
+
const parentNode = nodeAtPath(nodes, parentPath);
|
|
377
|
+
if (!parentNode || !canAddChildren(parentNode.widget)) return null;
|
|
378
|
+
|
|
379
|
+
const insertIndex = target.index + (placement === 'after' ? 1 : 0);
|
|
380
|
+
return {
|
|
381
|
+
asset,
|
|
382
|
+
parentPath,
|
|
383
|
+
insertIndex,
|
|
384
|
+
nextPath: insertedWidgetPathForParent(parentNode.widget, parentPath, insertIndex),
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function parentTypeForNode(
|
|
389
|
+
nodes: ReturnType<typeof collectWidgetNodes>,
|
|
390
|
+
path: WidgetPath,
|
|
391
|
+
): string | undefined {
|
|
392
|
+
if (path.length === 0) return undefined;
|
|
393
|
+
const parentPath = path.slice(0, -1);
|
|
394
|
+
const parentKey = widgetPathKey(parentPath);
|
|
395
|
+
return nodes.find((node) => widgetPathKey(node.path) === parentKey)?.widget.type;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
export function resolveWidgetTreeDropPlacement({
|
|
399
|
+
targetPath,
|
|
400
|
+
targetWidget,
|
|
401
|
+
offsetY,
|
|
402
|
+
height,
|
|
403
|
+
}: WidgetTreeDropPlacementGeometry): WidgetTreeDropPlacement {
|
|
404
|
+
const resolvedHeight = Math.max(1, height);
|
|
405
|
+
|
|
406
|
+
if (isRootWidgetPath(targetPath) && canAddChildren(targetWidget)) {
|
|
407
|
+
return 'inside';
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if (canAddChildren(targetWidget)) {
|
|
411
|
+
if (offsetY < resolvedHeight / 3) return 'before';
|
|
412
|
+
if (offsetY > (resolvedHeight * 2) / 3) return 'after';
|
|
413
|
+
return 'inside';
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
return offsetY < resolvedHeight / 2 ? 'before' : 'after';
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function resolveDropPlacementFromEvent(
|
|
420
|
+
event: DragEvent<HTMLLIElement>,
|
|
421
|
+
targetNode: WidgetNode,
|
|
422
|
+
): WidgetTreeDropPlacement {
|
|
423
|
+
const rect = event.currentTarget.getBoundingClientRect();
|
|
424
|
+
return resolveWidgetTreeDropPlacement({
|
|
425
|
+
targetPath: targetNode.path,
|
|
426
|
+
targetWidget: targetNode.widget,
|
|
427
|
+
offsetY: event.clientY - rect.top,
|
|
428
|
+
height: rect.height,
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
export function WidgetTreeView({
|
|
433
|
+
root,
|
|
434
|
+
parseError,
|
|
435
|
+
selection,
|
|
436
|
+
onSelectPath,
|
|
437
|
+
onActivatePath,
|
|
438
|
+
onDeletePath,
|
|
439
|
+
onMovePath,
|
|
440
|
+
onPlaceAssetPath,
|
|
441
|
+
}: WidgetTreeViewProps) {
|
|
442
|
+
const nodes = useMemo(() => (root ? collectWidgetNodes(root) : []), [root]);
|
|
443
|
+
const selectedPath = selection ? firstSelectedWidgetPath(selection) : null;
|
|
444
|
+
const selectedPathKey = selectedPath ? widgetPathKey(selectedPath) : null;
|
|
445
|
+
const [collapsedPathKeys, setCollapsedPathKeys] = useState<ReadonlySet<string>>(() => new Set());
|
|
446
|
+
const [dragPath, setDragPath] = useState<WidgetPath | null>(null);
|
|
447
|
+
const [dropTarget, setDropTarget] = useState<WidgetTreeDropTarget | null>(null);
|
|
448
|
+
const treeButtonRefs = useRef(new Map<string, HTMLButtonElement>());
|
|
449
|
+
const visibleNodes = useMemo(
|
|
450
|
+
() => filterVisibleWidgetNodes(nodes, collapsedPathKeys),
|
|
451
|
+
[collapsedPathKeys, nodes],
|
|
452
|
+
);
|
|
453
|
+
const selectedIndex = selectedNodeIndex(visibleNodes, selection);
|
|
454
|
+
|
|
455
|
+
const setPathCollapsed = (path: WidgetPath, collapsed: boolean) => {
|
|
456
|
+
const pathKey = widgetPathKey(path);
|
|
457
|
+
setCollapsedPathKeys((current) => {
|
|
458
|
+
const alreadyCollapsed = current.has(pathKey);
|
|
459
|
+
if (alreadyCollapsed === collapsed) return current;
|
|
460
|
+
|
|
461
|
+
const next = new Set(current);
|
|
462
|
+
if (collapsed) {
|
|
463
|
+
next.add(pathKey);
|
|
464
|
+
} else {
|
|
465
|
+
next.delete(pathKey);
|
|
466
|
+
}
|
|
467
|
+
return next;
|
|
468
|
+
});
|
|
469
|
+
};
|
|
470
|
+
|
|
471
|
+
const focusPath = (path: WidgetPath) => {
|
|
472
|
+
treeButtonRefs.current.get(widgetPathKey(path))?.focus();
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
useEffect(() => {
|
|
476
|
+
if (!selectedPath) return;
|
|
477
|
+
|
|
478
|
+
setCollapsedPathKeys((current) => {
|
|
479
|
+
let changed = false;
|
|
480
|
+
const next = new Set(current);
|
|
481
|
+
|
|
482
|
+
for (let depth = 0; depth < selectedPath.length; depth++) {
|
|
483
|
+
const ancestorKey = widgetPathKey(selectedPath.slice(0, depth));
|
|
484
|
+
if (next.delete(ancestorKey)) {
|
|
485
|
+
changed = true;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
return changed ? next : current;
|
|
490
|
+
});
|
|
491
|
+
}, [selectedPathKey, selectedPath]);
|
|
492
|
+
|
|
493
|
+
const handleHorizontalNavigation = (key: WidgetTreeHorizontalNavigationKey): boolean => {
|
|
494
|
+
const action = resolveWidgetTreeHorizontalNavigationAction(
|
|
495
|
+
visibleNodes,
|
|
496
|
+
selection,
|
|
497
|
+
collapsedPathKeys,
|
|
498
|
+
key,
|
|
499
|
+
);
|
|
500
|
+
if (!action) return false;
|
|
501
|
+
|
|
502
|
+
if (action.type === 'collapse') {
|
|
503
|
+
setPathCollapsed(action.path, true);
|
|
504
|
+
focusPath(action.path);
|
|
505
|
+
return true;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
if (action.type === 'expand') {
|
|
509
|
+
setPathCollapsed(action.path, false);
|
|
510
|
+
focusPath(action.path);
|
|
511
|
+
return true;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
onSelectPath?.(action.path);
|
|
515
|
+
focusPath(action.path);
|
|
516
|
+
return true;
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
const handleKeyDown = (event: KeyboardEvent<HTMLUListElement>) => {
|
|
520
|
+
if (event.altKey && (event.key === 'ArrowUp' || event.key === 'ArrowDown')) {
|
|
521
|
+
const operation = resolveWidgetTreeMoveOperation(
|
|
522
|
+
nodes,
|
|
523
|
+
selection,
|
|
524
|
+
event.key === 'ArrowUp' ? 'up' : 'down',
|
|
525
|
+
);
|
|
526
|
+
if (operation && onMovePath) {
|
|
527
|
+
event.preventDefault();
|
|
528
|
+
onMovePath(operation);
|
|
529
|
+
}
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
if (event.key === 'ArrowLeft' || event.key === 'ArrowRight') {
|
|
534
|
+
if (handleHorizontalNavigation(event.key)) {
|
|
535
|
+
event.preventDefault();
|
|
536
|
+
}
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
if (
|
|
541
|
+
event.key === 'ArrowDown' ||
|
|
542
|
+
event.key === 'ArrowUp' ||
|
|
543
|
+
event.key === 'Home' ||
|
|
544
|
+
event.key === 'End'
|
|
545
|
+
) {
|
|
546
|
+
const nextPath = resolveWidgetTreeNavigationPath(visibleNodes, selection, event.key);
|
|
547
|
+
if (nextPath) {
|
|
548
|
+
event.preventDefault();
|
|
549
|
+
onSelectPath?.(nextPath);
|
|
550
|
+
focusPath(nextPath);
|
|
551
|
+
}
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
if (event.key === 'Delete' || event.key === 'Backspace') {
|
|
556
|
+
const current = visibleNodes[selectedIndex];
|
|
557
|
+
if (current && !isRootWidgetPath(current.path)) {
|
|
558
|
+
event.preventDefault();
|
|
559
|
+
onDeletePath?.(current.path);
|
|
560
|
+
}
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
const current = visibleNodes[selectedIndex];
|
|
565
|
+
if (current && isWidgetTreeActivateKey(event.key) && onActivatePath) {
|
|
566
|
+
event.preventDefault();
|
|
567
|
+
onSelectPath?.(current.path);
|
|
568
|
+
onActivatePath(current.path);
|
|
569
|
+
}
|
|
570
|
+
};
|
|
571
|
+
|
|
572
|
+
const handleToggleCollapse = (path: WidgetPath, isCollapsed: boolean) => {
|
|
573
|
+
setPathCollapsed(path, !isCollapsed);
|
|
574
|
+
|
|
575
|
+
if (
|
|
576
|
+
!isCollapsed &&
|
|
577
|
+
selectedPath &&
|
|
578
|
+
isPathPrefix(path, selectedPath) &&
|
|
579
|
+
!widgetPathEquals(path, selectedPath)
|
|
580
|
+
) {
|
|
581
|
+
onSelectPath?.(path);
|
|
582
|
+
}
|
|
583
|
+
};
|
|
584
|
+
|
|
585
|
+
const handleDragStart = (event: DragEvent<HTMLLIElement>, path: WidgetPath) => {
|
|
586
|
+
if (!onMovePath || isRootWidgetPath(path)) {
|
|
587
|
+
event.preventDefault();
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
setDragPath(path);
|
|
592
|
+
event.dataTransfer.effectAllowed = 'move';
|
|
593
|
+
event.dataTransfer.setData('application/x-workbench-widget-path', widgetPathKey(path));
|
|
594
|
+
};
|
|
595
|
+
|
|
596
|
+
const handleDragOver = (event: DragEvent<HTMLLIElement>, targetNode: WidgetNode) => {
|
|
597
|
+
const asset = onPlaceAssetPath ? readWidgetPlacementAssetDragData(event.dataTransfer) : null;
|
|
598
|
+
if (asset) {
|
|
599
|
+
const placement = resolveDropPlacementFromEvent(event, targetNode);
|
|
600
|
+
const operation = resolveWidgetTreeAssetDropOperation(
|
|
601
|
+
nodes,
|
|
602
|
+
asset,
|
|
603
|
+
targetNode.path,
|
|
604
|
+
placement,
|
|
605
|
+
);
|
|
606
|
+
const targetKey = widgetPathKey(targetNode.path);
|
|
607
|
+
if (!operation) {
|
|
608
|
+
setDropTarget(null);
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
event.preventDefault();
|
|
613
|
+
event.dataTransfer.dropEffect = 'copy';
|
|
614
|
+
setDropTarget({ pathKey: targetKey, placement });
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
if (!dragPath) return;
|
|
619
|
+
|
|
620
|
+
const placement = resolveDropPlacementFromEvent(event, targetNode);
|
|
621
|
+
const operation = resolveWidgetTreeDropOperation(nodes, dragPath, targetNode.path, placement);
|
|
622
|
+
const targetKey = widgetPathKey(targetNode.path);
|
|
623
|
+
if (!operation) {
|
|
624
|
+
setDropTarget(null);
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
event.preventDefault();
|
|
629
|
+
event.dataTransfer.dropEffect = 'move';
|
|
630
|
+
setDropTarget({ pathKey: targetKey, placement });
|
|
631
|
+
};
|
|
632
|
+
|
|
633
|
+
const handleDragLeave = (event: DragEvent<HTMLLIElement>, path: WidgetPath) => {
|
|
634
|
+
const relatedTarget = event.relatedTarget;
|
|
635
|
+
if (relatedTarget instanceof Node && event.currentTarget.contains(relatedTarget)) return;
|
|
636
|
+
|
|
637
|
+
const targetKey = widgetPathKey(path);
|
|
638
|
+
setDropTarget((current) => (current?.pathKey === targetKey ? null : current));
|
|
639
|
+
};
|
|
640
|
+
|
|
641
|
+
const handleDrop = (event: DragEvent<HTMLLIElement>, targetNode: WidgetNode) => {
|
|
642
|
+
const asset = onPlaceAssetPath ? readWidgetPlacementAssetDragData(event.dataTransfer) : null;
|
|
643
|
+
if (asset) {
|
|
644
|
+
const placement =
|
|
645
|
+
dropTarget?.pathKey === widgetPathKey(targetNode.path)
|
|
646
|
+
? dropTarget.placement
|
|
647
|
+
: resolveDropPlacementFromEvent(event, targetNode);
|
|
648
|
+
const operation = resolveWidgetTreeAssetDropOperation(
|
|
649
|
+
nodes,
|
|
650
|
+
asset,
|
|
651
|
+
targetNode.path,
|
|
652
|
+
placement,
|
|
653
|
+
);
|
|
654
|
+
if (!operation) {
|
|
655
|
+
setDropTarget(null);
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
event.preventDefault();
|
|
660
|
+
onPlaceAssetPath?.(operation);
|
|
661
|
+
setDragPath(null);
|
|
662
|
+
setDropTarget(null);
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
if (!dragPath || !onMovePath) {
|
|
667
|
+
setDropTarget(null);
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
const placement =
|
|
672
|
+
dropTarget?.pathKey === widgetPathKey(targetNode.path)
|
|
673
|
+
? dropTarget.placement
|
|
674
|
+
: resolveDropPlacementFromEvent(event, targetNode);
|
|
675
|
+
const operation = resolveWidgetTreeDropOperation(nodes, dragPath, targetNode.path, placement);
|
|
676
|
+
if (!operation) {
|
|
677
|
+
setDropTarget(null);
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
event.preventDefault();
|
|
682
|
+
onMovePath(operation);
|
|
683
|
+
setDragPath(null);
|
|
684
|
+
setDropTarget(null);
|
|
685
|
+
};
|
|
686
|
+
|
|
687
|
+
return (
|
|
688
|
+
<Panel className="widget-tree-outline" data-testid="widget-tree-outline-panel">
|
|
689
|
+
<PanelBody className="widget-tree-outline__body">
|
|
690
|
+
{parseError !== null ? (
|
|
691
|
+
<EmptyState compact icon="codicon-error">
|
|
692
|
+
{parseError}
|
|
693
|
+
</EmptyState>
|
|
694
|
+
) : root === null ? (
|
|
695
|
+
<EmptyState compact icon="codicon-list-tree">
|
|
696
|
+
No widget root.
|
|
697
|
+
</EmptyState>
|
|
698
|
+
) : (
|
|
699
|
+
<ul
|
|
700
|
+
aria-label="Widget outline"
|
|
701
|
+
aria-keyshortcuts="ArrowLeft ArrowRight Alt+ArrowUp Alt+ArrowDown Delete Backspace"
|
|
702
|
+
className="widget-tree-outline__list"
|
|
703
|
+
role="tree"
|
|
704
|
+
onKeyDown={handleKeyDown}
|
|
705
|
+
>
|
|
706
|
+
{visibleNodes.map((node) => {
|
|
707
|
+
const pathKey = widgetPathKey(node.path);
|
|
708
|
+
const depth = node.path.length;
|
|
709
|
+
const isSelected = selection
|
|
710
|
+
? isWidgetPathSelected(selection, node.path)
|
|
711
|
+
: isRootWidgetPath(node.path);
|
|
712
|
+
const parentType = parentTypeForNode(nodes, node.path);
|
|
713
|
+
const placementMeta = formatWidgetPlacementMeta(node.widget, parentType);
|
|
714
|
+
const canMove = Boolean(onMovePath && !isRootWidgetPath(node.path));
|
|
715
|
+
const hasChildren = hasCollapsibleWidgetChildren(node.widget);
|
|
716
|
+
const isCollapsed = collapsedPathKeys.has(pathKey);
|
|
717
|
+
const dropPlacement = dropTarget?.pathKey === pathKey ? dropTarget.placement : null;
|
|
718
|
+
const displayLabel = getWidgetDisplayLabel(node.widget);
|
|
719
|
+
const textPreview =
|
|
720
|
+
node.widget.type === 'text' && typeof node.widget.text === 'string'
|
|
721
|
+
? node.widget.text
|
|
722
|
+
: null;
|
|
723
|
+
|
|
724
|
+
return (
|
|
725
|
+
<li
|
|
726
|
+
key={pathKey}
|
|
727
|
+
aria-level={depth + 1}
|
|
728
|
+
aria-expanded={hasChildren ? !isCollapsed : undefined}
|
|
729
|
+
aria-selected={isSelected}
|
|
730
|
+
className={cx(
|
|
731
|
+
'widget-tree-outline__item',
|
|
732
|
+
isSelected && 'widget-tree-outline__item--selected',
|
|
733
|
+
canMove && 'widget-tree-outline__item--movable',
|
|
734
|
+
dropPlacement && `widget-tree-outline__item--drop-${dropPlacement}`,
|
|
735
|
+
)}
|
|
736
|
+
data-drop-position={dropPlacement ?? undefined}
|
|
737
|
+
data-testid={`widget-tree-node-${pathKey}`}
|
|
738
|
+
draggable={canMove}
|
|
739
|
+
role="treeitem"
|
|
740
|
+
style={{ paddingLeft: `${depth * 14 + 6}px` }}
|
|
741
|
+
onDragEnd={() => {
|
|
742
|
+
setDragPath(null);
|
|
743
|
+
setDropTarget(null);
|
|
744
|
+
}}
|
|
745
|
+
onDragLeave={(event) => handleDragLeave(event, node.path)}
|
|
746
|
+
onDragOver={(event) => handleDragOver(event, node)}
|
|
747
|
+
onDragStart={(event) => handleDragStart(event, node.path)}
|
|
748
|
+
onDrop={(event) => handleDrop(event, node)}
|
|
749
|
+
>
|
|
750
|
+
{hasChildren ? (
|
|
751
|
+
<button
|
|
752
|
+
aria-label={`${isCollapsed ? 'Expand' : 'Collapse'} ${displayLabel}`}
|
|
753
|
+
className="widget-tree-outline__twistie"
|
|
754
|
+
type="button"
|
|
755
|
+
onClick={(event) => {
|
|
756
|
+
event.stopPropagation();
|
|
757
|
+
handleToggleCollapse(node.path, isCollapsed);
|
|
758
|
+
}}
|
|
759
|
+
>
|
|
760
|
+
{isCollapsed ? '▸' : '▾'}
|
|
761
|
+
</button>
|
|
762
|
+
) : (
|
|
763
|
+
<span
|
|
764
|
+
aria-hidden="true"
|
|
765
|
+
className="widget-tree-outline__twistie widget-tree-outline__twistie--spacer"
|
|
766
|
+
/>
|
|
767
|
+
)}
|
|
768
|
+
<button
|
|
769
|
+
className="widget-tree-outline__button"
|
|
770
|
+
ref={(button) => {
|
|
771
|
+
if (button) {
|
|
772
|
+
treeButtonRefs.current.set(pathKey, button);
|
|
773
|
+
} else {
|
|
774
|
+
treeButtonRefs.current.delete(pathKey);
|
|
775
|
+
}
|
|
776
|
+
}}
|
|
777
|
+
tabIndex={isSelected ? 0 : -1}
|
|
778
|
+
type="button"
|
|
779
|
+
onClick={() => onSelectPath?.(node.path)}
|
|
780
|
+
>
|
|
781
|
+
<span className="widget-tree-outline__type">{displayLabel}</span>
|
|
782
|
+
{placementMeta ? (
|
|
783
|
+
<span className="widget-tree-outline__placement">{placementMeta}</span>
|
|
784
|
+
) : null}
|
|
785
|
+
{textPreview ? (
|
|
786
|
+
<span className="widget-tree-outline__meta">{textPreview}</span>
|
|
787
|
+
) : null}
|
|
788
|
+
</button>
|
|
789
|
+
</li>
|
|
790
|
+
);
|
|
791
|
+
})}
|
|
792
|
+
</ul>
|
|
793
|
+
)}
|
|
794
|
+
</PanelBody>
|
|
795
|
+
</Panel>
|
|
796
|
+
);
|
|
797
|
+
}
|