@dxos/react-ui-canvas 0.13.0 → 0.14.0
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/README.md +1 -1
- package/dist/lib/index.mjs +9044 -1135
- package/dist/lib/index.mjs.map +1 -1
- package/dist/types/src/components/About/About.d.ts +13 -0
- package/dist/types/src/components/About/About.d.ts.map +1 -0
- package/dist/types/src/components/About/About.stories.d.ts +7 -0
- package/dist/types/src/components/About/About.stories.d.ts.map +1 -0
- package/dist/types/src/components/About/index.d.ts +2 -0
- package/dist/types/src/components/About/index.d.ts.map +1 -0
- package/dist/types/src/components/Breadcrumbs/Breadcrumbs.d.ts.map +1 -1
- package/dist/types/src/components/ControlFrame/ControlFrame.d.ts +8 -2
- package/dist/types/src/components/ControlFrame/ControlFrame.d.ts.map +1 -1
- package/dist/types/src/components/Layers/Layers.d.ts +30 -0
- package/dist/types/src/components/Layers/Layers.d.ts.map +1 -0
- package/dist/types/src/components/Layers/Layers.stories.d.ts +11 -0
- package/dist/types/src/components/Layers/Layers.stories.d.ts.map +1 -0
- package/dist/types/src/components/Layers/index.d.ts +2 -0
- package/dist/types/src/components/Layers/index.d.ts.map +1 -0
- package/dist/types/src/components/Palette/Palette.d.ts +20 -4
- package/dist/types/src/components/Palette/Palette.d.ts.map +1 -1
- package/dist/types/src/components/Palette/Palette.stories.d.ts +22 -0
- package/dist/types/src/components/Palette/Palette.stories.d.ts.map +1 -0
- package/dist/types/src/components/PartEditor/PartEditor.d.ts.map +1 -1
- package/dist/types/src/components/Properties/AlignField.d.ts +23 -0
- package/dist/types/src/components/Properties/AlignField.d.ts.map +1 -0
- package/dist/types/src/components/Properties/ClassField.d.ts +23 -0
- package/dist/types/src/components/Properties/ClassField.d.ts.map +1 -0
- package/dist/types/src/components/Properties/FontField.d.ts +7 -0
- package/dist/types/src/components/Properties/FontField.d.ts.map +1 -0
- package/dist/types/src/components/Properties/LayerField.d.ts +17 -0
- package/dist/types/src/components/Properties/LayerField.d.ts.map +1 -0
- package/dist/types/src/components/Properties/Properties.d.ts +27 -3
- package/dist/types/src/components/Properties/Properties.d.ts.map +1 -1
- package/dist/types/src/components/Properties/Properties.stories.d.ts +20 -0
- package/dist/types/src/components/Properties/Properties.stories.d.ts.map +1 -0
- package/dist/types/src/components/Properties/SceneField.d.ts +8 -0
- package/dist/types/src/components/Properties/SceneField.d.ts.map +1 -0
- package/dist/types/src/components/Properties/StyleGrid.d.ts +15 -5
- package/dist/types/src/components/Properties/StyleGrid.d.ts.map +1 -1
- package/dist/types/src/components/Properties/StyleGrid.stories.d.ts +7 -0
- package/dist/types/src/components/Properties/StyleGrid.stories.d.ts.map +1 -0
- package/dist/types/src/components/SceneLayer/SceneLayer.d.ts +31 -7
- package/dist/types/src/components/SceneLayer/SceneLayer.d.ts.map +1 -1
- package/dist/types/src/components/SceneView/Dock.d.ts +22 -0
- package/dist/types/src/components/SceneView/Dock.d.ts.map +1 -0
- package/dist/types/src/components/SceneView/Dock.stories.d.ts +15 -0
- package/dist/types/src/components/SceneView/Dock.stories.d.ts.map +1 -0
- package/dist/types/src/components/SceneView/SceneView.d.ts +33 -7
- package/dist/types/src/components/SceneView/SceneView.d.ts.map +1 -1
- package/dist/types/src/components/SceneView/SceneView.stories.d.ts +16 -6
- package/dist/types/src/components/SceneView/SceneView.stories.d.ts.map +1 -1
- package/dist/types/src/components/SceneView/SceneViewContext.d.ts +36 -4
- package/dist/types/src/components/SceneView/SceneViewContext.d.ts.map +1 -1
- package/dist/types/src/components/SceneView/Scored.stories.d.ts +4 -4
- package/dist/types/src/components/SceneView/Scored.stories.d.ts.map +1 -1
- package/dist/types/src/components/SceneView/usePointerMachine.d.ts +3 -1
- package/dist/types/src/components/SceneView/usePointerMachine.d.ts.map +1 -1
- package/dist/types/src/components/SceneView/useSceneCamera.d.ts +2 -2
- package/dist/types/src/components/SceneView/useSceneCamera.d.ts.map +1 -1
- package/dist/types/src/components/SceneView/useSceneClipboard.d.ts.map +1 -1
- package/dist/types/src/components/SceneView/useSceneKeys.d.ts.map +1 -1
- package/dist/types/src/components/SceneView/useSceneNavigation.d.ts +7 -4
- package/dist/types/src/components/SceneView/useSceneNavigation.d.ts.map +1 -1
- package/dist/types/src/components/Toolbar/Toolbar.d.ts +2 -0
- package/dist/types/src/components/Toolbar/Toolbar.d.ts.map +1 -1
- package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts.map +1 -1
- package/dist/types/src/components/index.d.ts +2 -0
- package/dist/types/src/components/index.d.ts.map +1 -1
- package/dist/types/src/hooks/useWheel.d.ts +5 -0
- package/dist/types/src/hooks/useWheel.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +4 -1
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/model/atoms.d.ts +21 -3
- package/dist/types/src/model/atoms.d.ts.map +1 -1
- package/dist/types/src/model/projection.d.ts.map +1 -1
- package/dist/types/src/model/projections/constrained.d.ts +2 -1
- package/dist/types/src/model/projections/constrained.d.ts.map +1 -1
- package/dist/types/src/model/projections/dynamic.d.ts.map +1 -1
- package/dist/types/src/model/projections/lattice.d.ts.map +1 -1
- package/dist/types/src/model/registry.d.ts +21 -3
- package/dist/types/src/model/registry.d.ts.map +1 -1
- package/dist/types/src/model/store.d.ts +10 -2
- package/dist/types/src/model/store.d.ts.map +1 -1
- package/dist/types/src/model/types.d.ts +699 -338
- package/dist/types/src/model/types.d.ts.map +1 -1
- package/dist/types/src/playwright/SceneManager.d.ts +1 -1
- package/dist/types/src/playwright/SceneManager.d.ts.map +1 -1
- package/dist/types/src/utils/builder.d.ts +22 -41
- package/dist/types/src/utils/builder.d.ts.map +1 -1
- package/dist/types/src/utils/camera.d.ts +19 -10
- package/dist/types/src/utils/camera.d.ts.map +1 -1
- package/dist/types/src/utils/clipboard.d.ts +4 -2
- package/dist/types/src/utils/clipboard.d.ts.map +1 -1
- package/dist/types/src/utils/diagram.d.ts.map +1 -1
- package/dist/types/src/utils/group.d.ts +14 -0
- package/dist/types/src/utils/group.d.ts.map +1 -0
- package/dist/types/src/utils/group.test.d.ts +2 -0
- package/dist/types/src/utils/group.test.d.ts.map +1 -0
- package/dist/types/src/utils/gutter-route.d.ts +7 -0
- package/dist/types/src/utils/gutter-route.d.ts.map +1 -1
- package/dist/types/src/utils/hit.d.ts +3 -20
- package/dist/types/src/utils/hit.d.ts.map +1 -1
- package/dist/types/src/utils/index.d.ts +3 -0
- package/dist/types/src/utils/index.d.ts.map +1 -1
- package/dist/types/src/utils/lattice.d.ts +5 -0
- package/dist/types/src/utils/lattice.d.ts.map +1 -1
- package/dist/types/src/utils/layers.d.ts +35 -0
- package/dist/types/src/utils/layers.d.ts.map +1 -0
- package/dist/types/src/utils/layers.test.d.ts +2 -0
- package/dist/types/src/utils/layers.test.d.ts.map +1 -0
- package/dist/types/src/utils/parts.d.ts +25 -9
- package/dist/types/src/utils/parts.d.ts.map +1 -1
- package/dist/types/src/utils/ports.d.ts +2 -0
- package/dist/types/src/utils/ports.d.ts.map +1 -1
- package/dist/types/src/utils/properties.d.ts +4 -3
- package/dist/types/src/utils/properties.d.ts.map +1 -1
- package/dist/types/src/utils/route.d.ts +5 -0
- package/dist/types/src/utils/route.d.ts.map +1 -1
- package/dist/types/src/utils/scenes.d.ts +14 -0
- package/dist/types/src/utils/scenes.d.ts.map +1 -0
- package/dist/types/src/utils/scenes.test.d.ts +2 -0
- package/dist/types/src/utils/scenes.test.d.ts.map +1 -0
- package/dist/types/src/utils/shapes.d.ts +23 -9
- package/dist/types/src/utils/shapes.d.ts.map +1 -1
- package/dist/types/src/utils/style.d.ts +29 -6
- package/dist/types/src/utils/style.d.ts.map +1 -1
- package/dist/types/src/utils/style.test.d.ts +2 -0
- package/dist/types/src/utils/style.test.d.ts.map +1 -0
- package/dist/types/src/utils/testing.d.ts +2 -2
- package/dist/types/src/utils/testing.d.ts.map +1 -1
- package/dist/types/src/utils/undo.d.ts +15 -4
- package/dist/types/src/utils/undo.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +20 -26
- package/src/components/About/About.stories.tsx +32 -0
- package/src/components/About/About.tsx +38 -0
- package/src/components/About/index.ts +5 -0
- package/src/components/Breadcrumbs/Breadcrumbs.tsx +17 -3
- package/src/components/ControlFrame/ControlFrame.tsx +138 -82
- package/src/components/Grid/Grid.stories.tsx +1 -1
- package/src/components/Layers/Layers.stories.tsx +131 -0
- package/src/components/Layers/Layers.tsx +216 -0
- package/src/components/Layers/index.ts +5 -0
- package/src/components/Palette/Palette.stories.tsx +135 -0
- package/src/components/Palette/Palette.tsx +230 -39
- package/src/components/PartEditor/PartEditor.tsx +4 -3
- package/src/components/Properties/AlignField.tsx +117 -0
- package/src/components/Properties/ClassField.tsx +44 -0
- package/src/components/Properties/FontField.tsx +73 -0
- package/src/components/Properties/LayerField.tsx +61 -0
- package/src/components/Properties/Properties.stories.tsx +113 -0
- package/src/components/Properties/Properties.tsx +240 -60
- package/src/components/Properties/SceneField.tsx +22 -0
- package/src/components/Properties/StyleGrid.stories.tsx +50 -0
- package/src/components/Properties/StyleGrid.tsx +54 -26
- package/src/components/SceneLayer/SceneLayer.tsx +497 -188
- package/src/components/SceneView/Constrained.stories.tsx +1 -1
- package/src/components/SceneView/Dock.stories.tsx +95 -0
- package/src/components/SceneView/Dock.tsx +146 -0
- package/src/components/SceneView/Dynamic.stories.tsx +1 -1
- package/src/components/SceneView/SceneView.stories.tsx +170 -18
- package/src/components/SceneView/SceneView.tsx +565 -151
- package/src/components/SceneView/SceneViewContext.ts +23 -2
- package/src/components/SceneView/Scored.stories.tsx +11 -11
- package/src/components/SceneView/usePointerMachine.ts +80 -20
- package/src/components/SceneView/useSceneCamera.ts +15 -8
- package/src/components/SceneView/useSceneClipboard.ts +4 -0
- package/src/components/SceneView/useSceneKeys.ts +6 -4
- package/src/components/SceneView/useSceneNavigation.ts +44 -33
- package/src/components/Toolbar/Toolbar.stories.tsx +14 -15
- package/src/components/Toolbar/Toolbar.tsx +15 -4
- package/src/components/index.ts +2 -0
- package/src/hooks/useWheel.ts +35 -0
- package/src/index.ts +4 -3
- package/src/model/atoms.ts +27 -3
- package/src/model/projection.ts +5 -0
- package/src/model/projections/constrained.test.ts +18 -10
- package/src/model/projections/constrained.ts +19 -8
- package/src/model/projections/dynamic.ts +5 -2
- package/src/model/projections/lattice.ts +15 -3
- package/src/model/registry.test.ts +14 -9
- package/src/model/registry.ts +39 -34
- package/src/model/store.ts +12 -2
- package/src/model/types.ts +124 -36
- package/src/playwright/SceneManager.ts +2 -2
- package/src/playwright/create.spec.ts +5 -3
- package/src/playwright/demo.spec.ts +2 -2
- package/src/playwright/lattice.spec.ts +4 -5
- package/src/playwright/scene.spec.ts +27 -25
- package/src/utils/builder.test.ts +46 -9
- package/src/utils/builder.ts +42 -18
- package/src/utils/camera.test.ts +22 -51
- package/src/utils/camera.ts +39 -32
- package/src/utils/clipboard.test.ts +23 -2
- package/src/utils/clipboard.ts +8 -4
- package/src/utils/diagram.test.ts +3 -1
- package/src/utils/diagram.ts +3 -25
- package/src/utils/group.test.ts +69 -0
- package/src/utils/group.ts +94 -0
- package/src/utils/gutter-route.test.ts +49 -0
- package/src/utils/gutter-route.ts +142 -16
- package/src/utils/hit.test.ts +4 -38
- package/src/utils/hit.ts +4 -26
- package/src/utils/index.ts +3 -0
- package/src/utils/lattice.ts +7 -1
- package/src/utils/layers.test.ts +139 -0
- package/src/utils/layers.ts +175 -0
- package/src/utils/parts.test.ts +66 -32
- package/src/utils/parts.ts +63 -41
- package/src/utils/ports.ts +7 -4
- package/src/utils/properties.test.ts +23 -6
- package/src/utils/properties.ts +66 -16
- package/src/utils/route.test.ts +51 -2
- package/src/utils/route.ts +133 -23
- package/src/utils/scenes.test.ts +38 -0
- package/src/utils/scenes.ts +54 -0
- package/src/utils/shapes.test.ts +33 -8
- package/src/utils/shapes.ts +51 -35
- package/src/utils/style.test.ts +54 -0
- package/src/utils/style.ts +153 -40
- package/src/utils/testing.test.ts +3 -3
- package/src/utils/testing.ts +21 -30
- package/src/utils/undo.test.ts +25 -2
- package/src/utils/undo.ts +50 -13
- package/dist/lib/chunk-Grid.mjs +0 -99
- package/dist/lib/chunk-Grid.mjs.map +0 -1
- package/dist/lib/scene.mjs +0 -6835
- package/dist/lib/scene.mjs.map +0 -1
- package/dist/lib/types.mjs +0 -16
- package/dist/lib/types.mjs.map +0 -1
- package/dist/types/src/archive/components/Canvas/Canvas.d.ts +0 -15
- package/dist/types/src/archive/components/Canvas/Canvas.d.ts.map +0 -1
- package/dist/types/src/archive/components/Canvas/Canvas.stories.d.ts +0 -18
- package/dist/types/src/archive/components/Canvas/Canvas.stories.d.ts.map +0 -1
- package/dist/types/src/archive/components/Canvas/index.d.ts +0 -2
- package/dist/types/src/archive/components/Canvas/index.d.ts.map +0 -1
- package/dist/types/src/archive/components/CellGrid/CellGrid.d.ts +0 -21
- package/dist/types/src/archive/components/CellGrid/CellGrid.d.ts.map +0 -1
- package/dist/types/src/archive/components/CellGrid/CellGrid.stories.d.ts +0 -21
- package/dist/types/src/archive/components/CellGrid/CellGrid.stories.d.ts.map +0 -1
- package/dist/types/src/archive/components/CellGrid/headers/Ruler.d.ts +0 -15
- package/dist/types/src/archive/components/CellGrid/headers/Ruler.d.ts.map +0 -1
- package/dist/types/src/archive/components/CellGrid/headers/TrackHeader.d.ts +0 -19
- package/dist/types/src/archive/components/CellGrid/headers/TrackHeader.d.ts.map +0 -1
- package/dist/types/src/archive/components/CellGrid/headers/index.d.ts +0 -3
- package/dist/types/src/archive/components/CellGrid/headers/index.d.ts.map +0 -1
- package/dist/types/src/archive/components/CellGrid/index.d.ts +0 -6
- package/dist/types/src/archive/components/CellGrid/index.d.ts.map +0 -1
- package/dist/types/src/archive/components/CellGrid/input/index.d.ts +0 -3
- package/dist/types/src/archive/components/CellGrid/input/index.d.ts.map +0 -1
- package/dist/types/src/archive/components/CellGrid/input/pointer.d.ts +0 -33
- package/dist/types/src/archive/components/CellGrid/input/pointer.d.ts.map +0 -1
- package/dist/types/src/archive/components/CellGrid/input/wheel.d.ts +0 -14
- package/dist/types/src/archive/components/CellGrid/input/wheel.d.ts.map +0 -1
- package/dist/types/src/archive/components/CellGrid/render/index.d.ts +0 -3
- package/dist/types/src/archive/components/CellGrid/render/index.d.ts.map +0 -1
- package/dist/types/src/archive/components/CellGrid/render/overlay-layer.d.ts +0 -21
- package/dist/types/src/archive/components/CellGrid/render/overlay-layer.d.ts.map +0 -1
- package/dist/types/src/archive/components/CellGrid/render/static-layer.d.ts +0 -36
- package/dist/types/src/archive/components/CellGrid/render/static-layer.d.ts.map +0 -1
- package/dist/types/src/archive/components/CellGrid/state/atoms.d.ts +0 -23
- package/dist/types/src/archive/components/CellGrid/state/atoms.d.ts.map +0 -1
- package/dist/types/src/archive/components/CellGrid/state/index.d.ts +0 -4
- package/dist/types/src/archive/components/CellGrid/state/index.d.ts.map +0 -1
- package/dist/types/src/archive/components/CellGrid/state/types.d.ts +0 -39
- package/dist/types/src/archive/components/CellGrid/state/types.d.ts.map +0 -1
- package/dist/types/src/archive/components/CellGrid/state/viewport.d.ts +0 -52
- package/dist/types/src/archive/components/CellGrid/state/viewport.d.ts.map +0 -1
- package/dist/types/src/archive/components/CellGrid/state/viewport.test.d.ts +0 -2
- package/dist/types/src/archive/components/CellGrid/state/viewport.test.d.ts.map +0 -1
- package/dist/types/src/archive/components/FPS.d.ts +0 -9
- package/dist/types/src/archive/components/FPS.d.ts.map +0 -1
- package/dist/types/src/archive/components/Grid/Grid.d.ts +0 -6
- package/dist/types/src/archive/components/Grid/Grid.d.ts.map +0 -1
- package/dist/types/src/archive/components/Grid/index.d.ts +0 -2
- package/dist/types/src/archive/components/Grid/index.d.ts.map +0 -1
- package/dist/types/src/archive/components/index.d.ts +0 -5
- package/dist/types/src/archive/components/index.d.ts.map +0 -1
- package/dist/types/src/archive/hooks/index.d.ts +0 -5
- package/dist/types/src/archive/hooks/index.d.ts.map +0 -1
- package/dist/types/src/archive/hooks/projection.d.ts +0 -60
- package/dist/types/src/archive/hooks/projection.d.ts.map +0 -1
- package/dist/types/src/archive/hooks/useCanvasContext.d.ts +0 -13
- package/dist/types/src/archive/hooks/useCanvasContext.d.ts.map +0 -1
- package/dist/types/src/archive/hooks/useDrag.d.ts +0 -6
- package/dist/types/src/archive/hooks/useDrag.d.ts.map +0 -1
- package/dist/types/src/archive/hooks/useWheel.d.ts +0 -8
- package/dist/types/src/archive/hooks/useWheel.d.ts.map +0 -1
- package/dist/types/src/archive/index.d.ts +0 -5
- package/dist/types/src/archive/index.d.ts.map +0 -1
- package/dist/types/src/archive/types.d.ts +0 -19
- package/dist/types/src/archive/types.d.ts.map +0 -1
- package/dist/types/src/archive/util/index.d.ts +0 -4
- package/dist/types/src/archive/util/index.d.ts.map +0 -1
- package/dist/types/src/archive/util/svg-path.d.ts +0 -4
- package/dist/types/src/archive/util/svg-path.d.ts.map +0 -1
- package/dist/types/src/archive/util/svg.d.ts +0 -32
- package/dist/types/src/archive/util/svg.d.ts.map +0 -1
- package/dist/types/src/archive/util/svg.stories.d.ts +0 -15
- package/dist/types/src/archive/util/svg.stories.d.ts.map +0 -1
- package/dist/types/src/archive/util/util.d.ts +0 -17
- package/dist/types/src/archive/util/util.d.ts.map +0 -1
- package/dist/types/src/scene.d.ts +0 -5
- package/dist/types/src/scene.d.ts.map +0 -1
- package/src/archive/components/Canvas/Canvas.stories.tsx +0 -111
- package/src/archive/components/Canvas/Canvas.tsx +0 -87
- package/src/archive/components/Canvas/index.ts +0 -5
- package/src/archive/components/CellGrid/CellGrid.stories.tsx +0 -239
- package/src/archive/components/CellGrid/CellGrid.tsx +0 -270
- package/src/archive/components/CellGrid/headers/Ruler.tsx +0 -71
- package/src/archive/components/CellGrid/headers/TrackHeader.tsx +0 -58
- package/src/archive/components/CellGrid/headers/index.ts +0 -6
- package/src/archive/components/CellGrid/index.ts +0 -9
- package/src/archive/components/CellGrid/input/index.ts +0 -6
- package/src/archive/components/CellGrid/input/pointer.ts +0 -236
- package/src/archive/components/CellGrid/input/wheel.ts +0 -68
- package/src/archive/components/CellGrid/render/index.ts +0 -6
- package/src/archive/components/CellGrid/render/overlay-layer.ts +0 -66
- package/src/archive/components/CellGrid/render/static-layer.ts +0 -112
- package/src/archive/components/CellGrid/state/atoms.ts +0 -43
- package/src/archive/components/CellGrid/state/index.ts +0 -7
- package/src/archive/components/CellGrid/state/types.ts +0 -40
- package/src/archive/components/CellGrid/state/viewport.test.ts +0 -50
- package/src/archive/components/CellGrid/state/viewport.ts +0 -94
- package/src/archive/components/FPS.tsx +0 -98
- package/src/archive/components/Grid/Grid.tsx +0 -17
- package/src/archive/components/Grid/index.ts +0 -5
- package/src/archive/components/index.ts +0 -8
- package/src/archive/hooks/index.ts +0 -8
- package/src/archive/hooks/projection.tsx +0 -156
- package/src/archive/hooks/useCanvasContext.ts +0 -32
- package/src/archive/hooks/useDrag.tsx +0 -96
- package/src/archive/hooks/useWheel.tsx +0 -80
- package/src/archive/index.ts +0 -8
- package/src/archive/types.ts +0 -14
- package/src/archive/util/index.ts +0 -7
- package/src/archive/util/svg-path.ts +0 -13
- package/src/archive/util/svg.stories.tsx +0 -46
- package/src/archive/util/svg.tsx +0 -128
- package/src/archive/util/util.ts +0 -50
- package/src/scene.ts +0 -9
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import React, { useMemo, useState } from 'react';
|
|
6
|
+
|
|
7
|
+
import { OrderedList } from '@dxos/react-ui-list';
|
|
8
|
+
import * as Button from '@dxos/react-ui/Button';
|
|
9
|
+
import * as Editable from '@dxos/react-ui/Editable';
|
|
10
|
+
import * as Menu from '@dxos/react-ui/Menu';
|
|
11
|
+
import * as Panel from '@dxos/react-ui/Panel';
|
|
12
|
+
import * as Toolbar from '@dxos/react-ui/Toolbar';
|
|
13
|
+
import type * as Util from '@dxos/react-ui/Util';
|
|
14
|
+
|
|
15
|
+
import { type Layer, type LayerId } from '../../model/types.ts';
|
|
16
|
+
|
|
17
|
+
export type LayersPanelProps = Util.ThemedClassName<{
|
|
18
|
+
/** The scene's layers, bottom first (`sceneLayers`); the panel lists them top first, as they stack. */
|
|
19
|
+
layers: readonly Layer[];
|
|
20
|
+
/** The selected layers; the top-most of them is the one new shapes go on. */
|
|
21
|
+
selected?: readonly LayerId[];
|
|
22
|
+
readonly?: boolean;
|
|
23
|
+
onSelectedChange?: (ids: LayerId[]) => void;
|
|
24
|
+
/** Shows or hides a layer. */
|
|
25
|
+
onToggle?: (id: LayerId) => void;
|
|
26
|
+
onRename?: (id: LayerId, name: string) => void;
|
|
27
|
+
/** Moves a layer to `index` in the bottom-first order. */
|
|
28
|
+
onMove?: (id: LayerId, index: number) => void;
|
|
29
|
+
/** Adds a layer; the panel opens the name of the one whose id it returns. */
|
|
30
|
+
onCreate?: () => LayerId | void;
|
|
31
|
+
/** Removes the selected layers and their shapes; offered while at least one layer would remain. */
|
|
32
|
+
onDelete?: (ids: LayerId[]) => void;
|
|
33
|
+
/** Merges the selected layers into the top-most of them; offered while two or more are selected. */
|
|
34
|
+
onMerge?: (ids: LayerId[], into: LayerId) => void;
|
|
35
|
+
/** Whether the panel is docked beside the canvas. */
|
|
36
|
+
docked?: boolean;
|
|
37
|
+
}>;
|
|
38
|
+
|
|
39
|
+
type LayerNameProps = {
|
|
40
|
+
layer: Layer;
|
|
41
|
+
readonly?: boolean;
|
|
42
|
+
editing: boolean;
|
|
43
|
+
onEditingChange: (editing: boolean) => void;
|
|
44
|
+
onRename?: (name: string) => void;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* A layer's name, edited in place on a double-click (`Editable`: the input takes the preview's cell, so the row does
|
|
49
|
+
* not move). Enter or leaving it commits, Escape keeps the old name. It is the row's text part, so it carries that
|
|
50
|
+
* part's class for the row's layout.
|
|
51
|
+
*/
|
|
52
|
+
const LayerName = ({ layer, readonly, editing, onEditingChange, onRename }: LayerNameProps) => (
|
|
53
|
+
<Editable.Root
|
|
54
|
+
classNames='dx-listbox-item-text'
|
|
55
|
+
value={layer.name}
|
|
56
|
+
activation='dblclick'
|
|
57
|
+
editing={editing}
|
|
58
|
+
onEditingChange={onEditingChange}
|
|
59
|
+
disabled={readonly || !onRename}
|
|
60
|
+
onValueChange={(next) => {
|
|
61
|
+
const name = next.trim();
|
|
62
|
+
if (name && name !== layer.name) {
|
|
63
|
+
onRename?.(name);
|
|
64
|
+
}
|
|
65
|
+
}}
|
|
66
|
+
>
|
|
67
|
+
<Editable.Preview data-testid={`layer-name-${layer.id}`} />
|
|
68
|
+
<Editable.Input
|
|
69
|
+
aria-label='Layer name'
|
|
70
|
+
data-testid={`layer-input-${layer.id}`}
|
|
71
|
+
// While a name is typed, the list's keys (typeahead, arrows, selection) must not take its keys; otherwise the
|
|
72
|
+
// list keeps them, so the arrows move between rows.
|
|
73
|
+
onKeyDown={(event) => event.stopPropagation()}
|
|
74
|
+
/>
|
|
75
|
+
</Editable.Root>
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* A scene's layers, top first as they stack: the active one is selected, each shows or hides from its eye, renames on
|
|
80
|
+
* a double-click and reorders by its handle. The toolbar adds a layer, deletes the active one, and merges it down.
|
|
81
|
+
*/
|
|
82
|
+
export const LayersPanel = ({
|
|
83
|
+
classNames,
|
|
84
|
+
layers,
|
|
85
|
+
selected: selectedProp,
|
|
86
|
+
readonly,
|
|
87
|
+
onSelectedChange,
|
|
88
|
+
onToggle,
|
|
89
|
+
onRename,
|
|
90
|
+
onMove,
|
|
91
|
+
onCreate,
|
|
92
|
+
onDelete,
|
|
93
|
+
onMerge,
|
|
94
|
+
docked = false,
|
|
95
|
+
}: LayersPanelProps) => {
|
|
96
|
+
const items = useMemo(() => [...layers].reverse(), [layers]);
|
|
97
|
+
// The row whose name is open; a new layer opens its own, so it is named as it is made.
|
|
98
|
+
const [editingId, setEditingId] = useState<LayerId>();
|
|
99
|
+
// Bottom first, as `layers`, so the last is the top-most selected.
|
|
100
|
+
const selected = useMemo(
|
|
101
|
+
() => layers.filter((layer) => selectedProp?.includes(layer.id)).map((layer) => layer.id),
|
|
102
|
+
[layers, selectedProp],
|
|
103
|
+
);
|
|
104
|
+
const top = selected[selected.length - 1];
|
|
105
|
+
return (
|
|
106
|
+
<Panel.Root classNames={classNames} data-testid='layers'>
|
|
107
|
+
<Panel.Header asChild>
|
|
108
|
+
<Toolbar.Root data-testid='layers-toolbar'>
|
|
109
|
+
<Button.Root
|
|
110
|
+
variant='ghost'
|
|
111
|
+
iconOnly
|
|
112
|
+
icon='ph--plus--regular'
|
|
113
|
+
label='Add layer'
|
|
114
|
+
disabled={readonly || !onCreate}
|
|
115
|
+
data-testid='layers-create'
|
|
116
|
+
onClick={() => {
|
|
117
|
+
const id = onCreate?.();
|
|
118
|
+
if (id) {
|
|
119
|
+
setEditingId(id);
|
|
120
|
+
}
|
|
121
|
+
}}
|
|
122
|
+
/>
|
|
123
|
+
{/* The rarer, destructive edits wait in a menu at the toolbar's end. */}
|
|
124
|
+
<Toolbar.Separator variant='gap' />
|
|
125
|
+
<Menu.Root positioning={{ placement: 'bottom-end', gutter: 4 }}>
|
|
126
|
+
<Menu.Trigger asChild>
|
|
127
|
+
<Button.Root
|
|
128
|
+
variant='ghost'
|
|
129
|
+
iconOnly
|
|
130
|
+
icon='ph--dots-three-vertical--regular'
|
|
131
|
+
label='Layer actions'
|
|
132
|
+
disabled={readonly}
|
|
133
|
+
data-testid='layers-menu'
|
|
134
|
+
/>
|
|
135
|
+
</Menu.Trigger>
|
|
136
|
+
<Menu.Content>
|
|
137
|
+
<Menu.Item
|
|
138
|
+
item={{ value: 'merge', label: 'Merge layers', icon: 'ph--arrow-line-down--regular' }}
|
|
139
|
+
disabled={!onMerge || selected.length < 2}
|
|
140
|
+
data-testid='layers-merge'
|
|
141
|
+
onSelect={() => top && onMerge?.(selected, top)}
|
|
142
|
+
/>
|
|
143
|
+
<Menu.Item
|
|
144
|
+
item={{ value: 'delete', label: 'Delete layers', icon: 'ph--trash--regular' }}
|
|
145
|
+
disabled={!onDelete || selected.length === 0 || selected.length >= layers.length}
|
|
146
|
+
data-testid='layers-delete'
|
|
147
|
+
onSelect={() => onDelete?.(selected)}
|
|
148
|
+
/>
|
|
149
|
+
</Menu.Content>
|
|
150
|
+
</Menu.Root>
|
|
151
|
+
</Toolbar.Root>
|
|
152
|
+
</Panel.Header>
|
|
153
|
+
<Panel.Body>
|
|
154
|
+
<OrderedList.Root
|
|
155
|
+
items={items}
|
|
156
|
+
getLabel={(layer) => layer.name}
|
|
157
|
+
readonly={readonly || !onMove}
|
|
158
|
+
multiple
|
|
159
|
+
value={selected}
|
|
160
|
+
onValueChange={(ids) => onSelectedChange?.(ids)}
|
|
161
|
+
// The list is top first; the order the model keeps is bottom first.
|
|
162
|
+
onMove={(from, to) => onMove?.(items[from].id, items.length - 1 - to)}
|
|
163
|
+
>
|
|
164
|
+
{({ items }) => (
|
|
165
|
+
<OrderedList.Content
|
|
166
|
+
// Docked, the dock scrolls the panels together, so the list keeps no scroll of its own (it would hold the wheel).
|
|
167
|
+
scroll={!docked}
|
|
168
|
+
// Rows run edge to edge, with no gutter around them.
|
|
169
|
+
gutter='none'
|
|
170
|
+
aria-label='Layers'
|
|
171
|
+
onKeyDown={(event) => {
|
|
172
|
+
// Enter on the list (as well as selecting the highlighted row) opens that row's name.
|
|
173
|
+
if (event.key !== 'Enter' || event.target !== event.currentTarget || readonly || !onRename) {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
const active = event.currentTarget.getAttribute('aria-activedescendant');
|
|
177
|
+
const id = active ? event.currentTarget.ownerDocument.getElementById(active)?.dataset.value : undefined;
|
|
178
|
+
if (id) {
|
|
179
|
+
setEditingId(id);
|
|
180
|
+
}
|
|
181
|
+
}}
|
|
182
|
+
>
|
|
183
|
+
{items.map((layer) => (
|
|
184
|
+
<OrderedList.Item key={layer.id} id={layer.id} data-testid={`layer-${layer.id}`}>
|
|
185
|
+
<OrderedList.DragHandle />
|
|
186
|
+
<LayerName
|
|
187
|
+
layer={layer}
|
|
188
|
+
readonly={readonly}
|
|
189
|
+
editing={editingId === layer.id}
|
|
190
|
+
onEditingChange={(editing) =>
|
|
191
|
+
setEditingId((current) => (editing ? layer.id : current === layer.id ? undefined : current))
|
|
192
|
+
}
|
|
193
|
+
onRename={(name) => onRename?.(layer.id, name)}
|
|
194
|
+
/>
|
|
195
|
+
<Button.Root
|
|
196
|
+
variant='ghost'
|
|
197
|
+
iconOnly
|
|
198
|
+
icon={layer.hidden ? 'ph--eye-slash--regular' : 'ph--eye--regular'}
|
|
199
|
+
label={layer.hidden ? 'Show layer' : 'Hide layer'}
|
|
200
|
+
disabled={readonly || !onToggle}
|
|
201
|
+
data-testid={`layer-toggle-${layer.id}`}
|
|
202
|
+
onClick={(event) => {
|
|
203
|
+
// The row's own click selects it; showing or hiding a layer leaves the selection alone.
|
|
204
|
+
event.stopPropagation();
|
|
205
|
+
onToggle?.(layer.id);
|
|
206
|
+
}}
|
|
207
|
+
/>
|
|
208
|
+
</OrderedList.Item>
|
|
209
|
+
))}
|
|
210
|
+
</OrderedList.Content>
|
|
211
|
+
)}
|
|
212
|
+
</OrderedList.Root>
|
|
213
|
+
</Panel.Body>
|
|
214
|
+
</Panel.Root>
|
|
215
|
+
);
|
|
216
|
+
};
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
|
6
|
+
import React, { useState } from 'react';
|
|
7
|
+
import { expect, userEvent, waitFor, within } from 'storybook/test';
|
|
8
|
+
|
|
9
|
+
import { withLayout, withTheme } from '@dxos/react-ui/testing';
|
|
10
|
+
|
|
11
|
+
import { freehandCapabilities } from '../../model/projection.ts';
|
|
12
|
+
import { type NodeRegistry, defaultLinkRegistry, defaultNodeRegistry } from '../../model/registry.ts';
|
|
13
|
+
import { type Tool } from '../../model/types.ts';
|
|
14
|
+
import { Palette, type PaletteProps } from './Palette.tsx';
|
|
15
|
+
|
|
16
|
+
const ICONS = [
|
|
17
|
+
'ph--brain--regular',
|
|
18
|
+
'ph--function--regular',
|
|
19
|
+
'ph--database--regular',
|
|
20
|
+
'ph--image--regular',
|
|
21
|
+
'ph--git-fork--regular',
|
|
22
|
+
'ph--intersect--regular',
|
|
23
|
+
'ph--shuffle--regular',
|
|
24
|
+
'ph--code--regular',
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
/** Many node types in four groups, as a host with a large registry (the compute shapes) has. */
|
|
28
|
+
const MANY: NodeRegistry = Object.fromEntries(
|
|
29
|
+
['Inputs', 'Transform', 'Operations', 'Outputs'].flatMap((group, groupIndex) =>
|
|
30
|
+
Array.from({ length: 6 }, (_, index) => {
|
|
31
|
+
const type = `${group.toLowerCase()}-${index}`;
|
|
32
|
+
return [
|
|
33
|
+
type,
|
|
34
|
+
{
|
|
35
|
+
...defaultNodeRegistry.rect,
|
|
36
|
+
type,
|
|
37
|
+
key: undefined,
|
|
38
|
+
name: `${group} ${index + 1}`,
|
|
39
|
+
icon: ICONS[(groupIndex * 6 + index) % ICONS.length],
|
|
40
|
+
group,
|
|
41
|
+
},
|
|
42
|
+
];
|
|
43
|
+
}),
|
|
44
|
+
),
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
type StoryArgs = Pick<PaletteProps, 'collapse' | 'flyout'> & {
|
|
48
|
+
/** Many types in groups, or the built-in few. */
|
|
49
|
+
many: boolean;
|
|
50
|
+
/** The height of the view the rail floats in. */
|
|
51
|
+
height: number;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/** The rail floating in a view of `height`, as `SceneView.Palette` places it. */
|
|
55
|
+
const DefaultStory = ({ collapse, flyout, many, height }: StoryArgs) => {
|
|
56
|
+
const [tool, setTool] = useState<Tool>({ kind: 'select' });
|
|
57
|
+
return (
|
|
58
|
+
<div className='relative w-64 border border-separator rounded-sm' style={{ height }}>
|
|
59
|
+
<div className='absolute top-2 bottom-2 left-2 pointer-events-none'>
|
|
60
|
+
<Palette
|
|
61
|
+
tool={tool}
|
|
62
|
+
nodes={many ? MANY : defaultNodeRegistry}
|
|
63
|
+
links={defaultLinkRegistry}
|
|
64
|
+
capabilities={freehandCapabilities}
|
|
65
|
+
collapse={collapse}
|
|
66
|
+
flyout={flyout}
|
|
67
|
+
onToolChange={setTool}
|
|
68
|
+
/>
|
|
69
|
+
</div>
|
|
70
|
+
<div className='absolute bottom-2 right-2 text-xs text-fg-muted' data-testid='palette-tool'>
|
|
71
|
+
{'type' in tool ? tool.type : tool.kind}
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const meta: Meta<StoryArgs> = {
|
|
78
|
+
title: 'ui/react-ui-canvas/Palette',
|
|
79
|
+
render: DefaultStory,
|
|
80
|
+
decorators: [withTheme(), withLayout({ layout: 'centered' })],
|
|
81
|
+
args: { collapse: 'auto', flyout: 'list', many: false, height: 640 },
|
|
82
|
+
argTypes: {
|
|
83
|
+
collapse: { control: 'select', options: ['auto', true, false] },
|
|
84
|
+
flyout: { control: 'select', options: ['list', 'grid'] },
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export default meta;
|
|
89
|
+
|
|
90
|
+
type Story = StoryObj<StoryArgs>;
|
|
91
|
+
|
|
92
|
+
/** The built-in types: few enough that the rail stays flat. */
|
|
93
|
+
export const Default: Story = {};
|
|
94
|
+
|
|
95
|
+
/** Many types in a short view: the node groups fold into flyouts. */
|
|
96
|
+
export const Many: Story = { args: { many: true } };
|
|
97
|
+
|
|
98
|
+
/** Many types folded, each group's tools as a grid of icons rather than a list. */
|
|
99
|
+
export const ManyGrid: Story = { args: { many: true, flyout: 'grid' } };
|
|
100
|
+
|
|
101
|
+
/** Many types with room to spare: still flat, every tool one click away. */
|
|
102
|
+
export const ManyTall: Story = { args: { many: true, height: 1400 } };
|
|
103
|
+
|
|
104
|
+
/** A folded group picks its face, opens its grid, and shows the tool picked from it. */
|
|
105
|
+
export const Test: Story = {
|
|
106
|
+
args: { many: true },
|
|
107
|
+
play: async ({ canvasElement }) => {
|
|
108
|
+
const canvas = within(canvasElement);
|
|
109
|
+
// 1. The rail folds: one button per node group.
|
|
110
|
+
await waitFor(() => expect(canvas.getByTestId('palette')).toHaveAttribute('data-collapsed'));
|
|
111
|
+
// 2. The group's face picks its first tool.
|
|
112
|
+
await userEvent.click(within(canvas.getByTestId('palette-group-Transform')).getAllByRole('button')[0]);
|
|
113
|
+
await expect(canvas.getByTestId('palette-tool')).toHaveTextContent('transform-0');
|
|
114
|
+
// 3. The chevron opens the group's grid; a pick there selects it and becomes the face.
|
|
115
|
+
await userEvent.click(canvas.getByTestId('palette-group-Transform-open'));
|
|
116
|
+
const tools = await within(canvasElement.ownerDocument.body).findByTestId('palette-group-Transform-tools');
|
|
117
|
+
await userEvent.click(within(tools).getByTestId('palette-transform-3'));
|
|
118
|
+
await expect(canvas.getByTestId('palette-tool')).toHaveTextContent('transform-3');
|
|
119
|
+
await waitFor(() =>
|
|
120
|
+
expect(
|
|
121
|
+
within(canvas.getByTestId('palette-group-Transform')).getByTestId('palette-transform-3'),
|
|
122
|
+
).toBeInTheDocument(),
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
// 4. Resting the pointer on a folded group opens its grid.
|
|
126
|
+
await userEvent.hover(canvas.getByTestId('palette-group-Outputs'));
|
|
127
|
+
await expect(
|
|
128
|
+
await within(canvasElement.ownerDocument.body).findByTestId(
|
|
129
|
+
'palette-group-Outputs-tools',
|
|
130
|
+
{},
|
|
131
|
+
{ timeout: 2_000 },
|
|
132
|
+
),
|
|
133
|
+
).toBeInTheDocument();
|
|
134
|
+
},
|
|
135
|
+
};
|
|
@@ -4,9 +4,11 @@
|
|
|
4
4
|
|
|
5
5
|
import { draggable } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
|
|
6
6
|
import { disableNativeDragPreview } from '@atlaskit/pragmatic-drag-and-drop/element/disable-native-drag-preview';
|
|
7
|
-
import React, { useEffect, useRef } from 'react';
|
|
7
|
+
import React, { type RefObject, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
|
8
8
|
|
|
9
9
|
import * as Button from '@dxos/react-ui/Button';
|
|
10
|
+
import * as Icon from '@dxos/react-ui/Icon';
|
|
11
|
+
import * as Popover from '@dxos/react-ui/Popover';
|
|
10
12
|
import { mx } from '@dxos/ui-theme';
|
|
11
13
|
|
|
12
14
|
import { type LinkRegistry, type NodeRegistry } from '../../model/registry.ts';
|
|
@@ -20,8 +22,11 @@ const BASE: Entry[] = [
|
|
|
20
22
|
{ tool: { kind: 'hand' }, icon: 'ph--hand--regular', label: 'Pan', key: 'H' },
|
|
21
23
|
];
|
|
22
24
|
|
|
25
|
+
/** A run of palette entries; a group of node types may fold into one flyout button when the rail runs out of room. */
|
|
26
|
+
export type PaletteGroup = { name: string; entries: Entry[]; collapsible: boolean };
|
|
27
|
+
|
|
23
28
|
/** Node types by their `group`, in registry order; types without one share the first, unnamed group. */
|
|
24
|
-
const nodeGroups = (nodes: NodeRegistry):
|
|
29
|
+
const nodeGroups = (nodes: NodeRegistry): PaletteGroup[] => {
|
|
25
30
|
const groups = new Map<string, Entry[]>();
|
|
26
31
|
for (const def of Object.values(nodes)) {
|
|
27
32
|
const name = def.group ?? '';
|
|
@@ -30,26 +35,34 @@ const nodeGroups = (nodes: NodeRegistry): Entry[][] => {
|
|
|
30
35
|
{ tool: { kind: 'node' as const, type: def.type }, icon: def.icon, label: def.name, key: def.key },
|
|
31
36
|
]);
|
|
32
37
|
}
|
|
33
|
-
return [...groups.
|
|
38
|
+
return [...groups.entries()].map(([name, entries]) => ({ name, entries, collapsible: entries.length > 1 }));
|
|
34
39
|
};
|
|
35
40
|
|
|
36
41
|
/**
|
|
37
|
-
* Palette
|
|
38
|
-
*
|
|
42
|
+
* Palette groups: the fixed tools, then the node types by group, then a link per link type; a group the projection
|
|
43
|
+
* cannot apply (`create`, `link`) is left out rather than offered and silently dropped. Only node groups fold.
|
|
39
44
|
*/
|
|
40
|
-
export const
|
|
45
|
+
export const paletteGroups = (nodes: NodeRegistry, links: LinkRegistry, capabilities: Capabilities): PaletteGroup[] =>
|
|
41
46
|
[
|
|
42
|
-
BASE,
|
|
47
|
+
{ name: 'Tools', entries: BASE, collapsible: false },
|
|
43
48
|
...(capabilities.create ? nodeGroups(nodes) : []),
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
49
|
+
{
|
|
50
|
+
name: 'Links',
|
|
51
|
+
entries: capabilities.link
|
|
52
|
+
? Object.values(links).map((def) => ({
|
|
53
|
+
tool: { kind: 'link' as const, type: def.type },
|
|
54
|
+
icon: def.icon,
|
|
55
|
+
label: def.name,
|
|
56
|
+
key: def.key,
|
|
57
|
+
}))
|
|
58
|
+
: [],
|
|
59
|
+
collapsible: false,
|
|
60
|
+
},
|
|
61
|
+
].filter((group) => group.entries.length > 0);
|
|
62
|
+
|
|
63
|
+
/** Palette entries by group, flat within each. */
|
|
64
|
+
export const paletteEntries = (nodes: NodeRegistry, links: LinkRegistry, capabilities: Capabilities): Entry[][] =>
|
|
65
|
+
paletteGroups(nodes, links, capabilities).map((group) => group.entries);
|
|
53
66
|
|
|
54
67
|
export const sameTool = (left: Tool, right: Tool): boolean =>
|
|
55
68
|
left.kind === right.kind && ('type' in left ? left.type === ('type' in right ? right.type : undefined) : true);
|
|
@@ -69,40 +82,218 @@ export type PaletteProps = {
|
|
|
69
82
|
nodes: NodeRegistry;
|
|
70
83
|
links: LinkRegistry;
|
|
71
84
|
capabilities: Capabilities;
|
|
85
|
+
/**
|
|
86
|
+
* Folds each group of node types into one flyout button: `auto` (the default) does so only when the flat rail would
|
|
87
|
+
* not fit the space below it, so a small registry keeps every tool one click away.
|
|
88
|
+
*/
|
|
89
|
+
collapse?: 'auto' | boolean;
|
|
90
|
+
/** How a folded group lists its tools: one per row with its name (the default), or a grid of icons. */
|
|
91
|
+
flyout?: 'list' | 'grid';
|
|
72
92
|
onToolChange: (tool: Tool) => void;
|
|
73
93
|
};
|
|
74
94
|
|
|
95
|
+
/** The height the rail would take flat: each button, the gaps between them, and each group's padding and divider. */
|
|
96
|
+
const flatHeight = (groups: readonly PaletteGroup[], button: number): number =>
|
|
97
|
+
groups.reduce((height, group) => height + group.entries.length * (button + 4) - 4 + 9, 0);
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Whether the rail folds its node groups: always, never, or (`auto`) when the flat rail would overflow its parent,
|
|
101
|
+
* which the host sizes to the room the rail may take (`SceneView.Palette` spans from below the navigation to above
|
|
102
|
+
* the bottom bars); re-measured as that resizes.
|
|
103
|
+
*/
|
|
104
|
+
const useCollapsed = (
|
|
105
|
+
ref: RefObject<HTMLDivElement | null>,
|
|
106
|
+
groups: readonly PaletteGroup[],
|
|
107
|
+
collapse: PaletteProps['collapse'],
|
|
108
|
+
) => {
|
|
109
|
+
const [overflows, setOverflows] = useState(false);
|
|
110
|
+
useLayoutEffect(() => {
|
|
111
|
+
const element = ref.current;
|
|
112
|
+
const host = element?.parentElement;
|
|
113
|
+
if (collapse !== 'auto' || !element || !host) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const measure = () => {
|
|
117
|
+
const button = element.querySelector('button')?.getBoundingClientRect().height ?? 32;
|
|
118
|
+
setOverflows(flatHeight(groups, button) > host.clientHeight);
|
|
119
|
+
};
|
|
120
|
+
measure();
|
|
121
|
+
const observer = new ResizeObserver(measure);
|
|
122
|
+
observer.observe(host);
|
|
123
|
+
return () => observer.disconnect();
|
|
124
|
+
}, [ref, groups, collapse]);
|
|
125
|
+
return collapse === 'auto' ? overflows : collapse;
|
|
126
|
+
};
|
|
127
|
+
|
|
75
128
|
/**
|
|
76
129
|
* Tool palette: a click picks the tool; a node type can also be dragged onto the canvas, which creates
|
|
77
|
-
* it where it drops (the canvas is a pragmatic-dnd drop target for `nodeDragData`).
|
|
130
|
+
* it where it drops (the canvas is a pragmatic-dnd drop target for `nodeDragData`). With many types the node
|
|
131
|
+
* groups fold into flyouts (`collapse`), each showing its active or last-used tool.
|
|
132
|
+
*/
|
|
133
|
+
export const Palette = ({
|
|
134
|
+
tool,
|
|
135
|
+
nodes,
|
|
136
|
+
links,
|
|
137
|
+
capabilities,
|
|
138
|
+
collapse = 'auto',
|
|
139
|
+
flyout = 'list',
|
|
140
|
+
onToolChange,
|
|
141
|
+
}: PaletteProps) => {
|
|
142
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
143
|
+
const groups = useMemo(() => paletteGroups(nodes, links, capabilities), [nodes, links, capabilities]);
|
|
144
|
+
const collapsed = useCollapsed(ref, groups, collapse);
|
|
145
|
+
return (
|
|
146
|
+
<div
|
|
147
|
+
ref={ref}
|
|
148
|
+
// The host's frame spans the room the rail may take and lets the canvas below it take the pointer.
|
|
149
|
+
className='flex flex-col w-fit rounded-sm bg-modal-surface border border-separator divide-y divide-separator pointer-events-auto'
|
|
150
|
+
data-testid='palette'
|
|
151
|
+
data-collapsed={collapsed || undefined}
|
|
152
|
+
>
|
|
153
|
+
{groups.map((group) =>
|
|
154
|
+
collapsed && group.collapsible ? (
|
|
155
|
+
<div key={group.name} className='flex flex-col p-1'>
|
|
156
|
+
<PaletteFlyout group={group} tool={tool} layout={flyout} onToolChange={onToolChange} />
|
|
157
|
+
</div>
|
|
158
|
+
) : (
|
|
159
|
+
<div key={group.name} className='flex flex-col gap-1 p-1'>
|
|
160
|
+
{group.entries.map((entry) => (
|
|
161
|
+
<PaletteButton
|
|
162
|
+
key={entry.label}
|
|
163
|
+
entry={entry}
|
|
164
|
+
active={sameTool(tool, entry.tool)}
|
|
165
|
+
onToolChange={onToolChange}
|
|
166
|
+
/>
|
|
167
|
+
))}
|
|
168
|
+
</div>
|
|
169
|
+
),
|
|
170
|
+
)}
|
|
171
|
+
</div>
|
|
172
|
+
);
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
/** How long the pointer rests on a folded group before its tools open. */
|
|
176
|
+
const HOVER_OPEN_MS = 500;
|
|
177
|
+
/** How long the tools stay open after the pointer leaves, so it can cross the gap between the rail and the grid. */
|
|
178
|
+
const HOVER_CLOSE_MS = 250;
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Opens after the pointer rests on the trigger, and closes once it has left both the trigger and the content. `hovered`
|
|
182
|
+
* is how it opened, so the content can leave focus where it is.
|
|
78
183
|
*/
|
|
79
|
-
|
|
80
|
-
<
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
184
|
+
const useHoverOpen = (setOpen: (open: boolean, hovered?: boolean) => void) => {
|
|
185
|
+
const timer = useRef<ReturnType<typeof setTimeout>>(undefined);
|
|
186
|
+
useEffect(() => () => clearTimeout(timer.current), []);
|
|
187
|
+
return useMemo(
|
|
188
|
+
() => ({
|
|
189
|
+
enter: () => {
|
|
190
|
+
clearTimeout(timer.current);
|
|
191
|
+
timer.current = setTimeout(() => setOpen(true, true), HOVER_OPEN_MS);
|
|
192
|
+
},
|
|
193
|
+
keep: () => clearTimeout(timer.current),
|
|
194
|
+
leave: () => {
|
|
195
|
+
clearTimeout(timer.current);
|
|
196
|
+
timer.current = setTimeout(() => setOpen(false), HOVER_CLOSE_MS);
|
|
197
|
+
},
|
|
198
|
+
}),
|
|
199
|
+
[setOpen],
|
|
200
|
+
);
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
type PaletteFlyoutProps = {
|
|
204
|
+
group: PaletteGroup;
|
|
205
|
+
tool: Tool;
|
|
206
|
+
layout: NonNullable<PaletteProps['flyout']>;
|
|
207
|
+
onToolChange: (tool: Tool) => void;
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* A folded group: its active (else last-used, else first) tool as one button, picked with a click, and a corner
|
|
212
|
+
* chevron (or a hover) opening the group's tools beside the rail, as a list or a grid (`layout`).
|
|
213
|
+
*/
|
|
214
|
+
const PaletteFlyout = ({ group, tool, layout, onToolChange }: PaletteFlyoutProps) => {
|
|
215
|
+
const [open, setOpenState] = useState(false);
|
|
216
|
+
// Opened by a hover, the list takes no focus (a pointer user would see a focus ring on the first row); opened from
|
|
217
|
+
// the chevron (a click or the keyboard), it does, so arrows move through it.
|
|
218
|
+
const [hovered, setHovered] = useState(false);
|
|
219
|
+
const setOpen = useCallback((next: boolean, byHover = false) => {
|
|
220
|
+
setHovered(next && byHover);
|
|
221
|
+
setOpenState(next);
|
|
222
|
+
}, []);
|
|
223
|
+
const hover = useHoverOpen(setOpen);
|
|
224
|
+
const [lastUsed, setLastUsed] = useState<Entry>(group.entries[0]);
|
|
225
|
+
const active = group.entries.find((entry) => sameTool(tool, entry.tool));
|
|
226
|
+
// A tool picked from elsewhere (its key, the grid) becomes the group's face, so the rail shows what is in use.
|
|
227
|
+
useEffect(() => {
|
|
228
|
+
if (active) {
|
|
229
|
+
setLastUsed(active);
|
|
230
|
+
}
|
|
231
|
+
}, [active]);
|
|
232
|
+
const shown = active ?? lastUsed;
|
|
233
|
+
return (
|
|
234
|
+
<Popover.Root
|
|
235
|
+
open={open}
|
|
236
|
+
autoFocus={!hovered}
|
|
237
|
+
onOpenChange={({ open }) => setOpen(open)}
|
|
238
|
+
positioning={{ placement: 'right-start' }}
|
|
239
|
+
>
|
|
240
|
+
<Popover.Anchor asChild>
|
|
241
|
+
<div
|
|
242
|
+
className='relative'
|
|
243
|
+
data-testid={`palette-group-${group.name}`}
|
|
244
|
+
onPointerEnter={hover.enter}
|
|
245
|
+
onPointerLeave={hover.leave}
|
|
246
|
+
>
|
|
247
|
+
<PaletteButton entry={shown} active={active !== undefined} onToolChange={onToolChange} />
|
|
248
|
+
<Popover.Trigger asChild>
|
|
249
|
+
<button
|
|
250
|
+
type='button'
|
|
251
|
+
aria-label={`${group.name} tools`}
|
|
252
|
+
className='absolute bottom-0 right-0 flex size-3 items-end justify-end text-fg-subtle hover:text-fg'
|
|
253
|
+
data-testid={`palette-group-${group.name}-open`}
|
|
254
|
+
>
|
|
255
|
+
<Icon.Icon icon='ph--caret-right--fill' size='xs' />
|
|
256
|
+
</button>
|
|
257
|
+
</Popover.Trigger>
|
|
258
|
+
</div>
|
|
259
|
+
</Popover.Anchor>
|
|
260
|
+
<Popover.Content onPointerEnter={hover.keep} onPointerLeave={hover.leave}>
|
|
261
|
+
<div
|
|
262
|
+
className={mx(
|
|
263
|
+
layout === 'list'
|
|
264
|
+
? 'flex flex-col items-stretch gap-1 p-1'
|
|
265
|
+
: 'grid grid-cols-[repeat(3,min-content)] gap-1 p-1',
|
|
266
|
+
)}
|
|
267
|
+
data-testid={`palette-group-${group.name}-tools`}
|
|
268
|
+
>
|
|
269
|
+
{group.entries.map((entry) => (
|
|
270
|
+
<PaletteButton
|
|
271
|
+
key={entry.label}
|
|
272
|
+
entry={entry}
|
|
273
|
+
labelled={layout === 'list'}
|
|
274
|
+
active={sameTool(tool, entry.tool)}
|
|
275
|
+
onToolChange={(next) => {
|
|
276
|
+
onToolChange(next);
|
|
277
|
+
setOpen(false);
|
|
278
|
+
}}
|
|
279
|
+
/>
|
|
280
|
+
))}
|
|
281
|
+
</div>
|
|
282
|
+
</Popover.Content>
|
|
283
|
+
</Popover.Root>
|
|
284
|
+
);
|
|
285
|
+
};
|
|
98
286
|
|
|
99
287
|
const PaletteButton = ({
|
|
100
288
|
entry,
|
|
101
289
|
active,
|
|
290
|
+
labelled,
|
|
102
291
|
onToolChange,
|
|
103
292
|
}: {
|
|
104
293
|
entry: Entry;
|
|
105
294
|
active: boolean;
|
|
295
|
+
/** Shows the tool's name beside its icon (a flyout's list), rather than the icon alone. */
|
|
296
|
+
labelled?: boolean;
|
|
106
297
|
onToolChange: (tool: Tool) => void;
|
|
107
298
|
}) => {
|
|
108
299
|
const ref = useRef<HTMLButtonElement>(null);
|
|
@@ -122,10 +313,10 @@ const PaletteButton = ({
|
|
|
122
313
|
<Button.Root
|
|
123
314
|
ref={ref}
|
|
124
315
|
variant='ghost'
|
|
125
|
-
iconOnly
|
|
316
|
+
iconOnly={!labelled}
|
|
126
317
|
icon={entry.icon}
|
|
127
|
-
label={entry.key ? `${entry.label} (${entry.key})` : entry.label}
|
|
128
|
-
classNames={mx(active && 'bg-primary-500/20')}
|
|
318
|
+
label={labelled ? entry.label : entry.key ? `${entry.label} (${entry.key})` : entry.label}
|
|
319
|
+
classNames={mx(labelled && 'w-full justify-start', active && 'bg-primary-500/20')}
|
|
129
320
|
data-testid={`palette-${entry.key ?? ('type' in entry.tool ? entry.tool.type : entry.tool.kind)}`}
|
|
130
321
|
onClick={() => onToolChange(entry.tool)}
|
|
131
322
|
/>
|