@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
|
@@ -8,42 +8,54 @@
|
|
|
8
8
|
//
|
|
9
9
|
|
|
10
10
|
import { useAtomValue } from '@effect/atom-react/Hooks';
|
|
11
|
-
import React, {
|
|
11
|
+
import React, {
|
|
12
|
+
type CSSProperties,
|
|
13
|
+
Fragment,
|
|
14
|
+
createContext,
|
|
15
|
+
memo,
|
|
16
|
+
useContext,
|
|
17
|
+
useId,
|
|
18
|
+
useLayoutEffect,
|
|
19
|
+
useMemo,
|
|
20
|
+
useRef,
|
|
21
|
+
useState,
|
|
22
|
+
} from 'react';
|
|
12
23
|
|
|
13
24
|
import * as Button from '@dxos/react-ui/Button';
|
|
14
25
|
import { mx } from '@dxos/ui-theme';
|
|
15
26
|
|
|
16
27
|
import { nodeDef } from '../../model/node-def.ts';
|
|
17
28
|
import { type NodeRegistry, type NodeViewProps } from '../../model/registry.ts';
|
|
18
|
-
import { type SceneStore } from '../../model/store.ts';
|
|
29
|
+
import { NO_STYLES, type SceneStore } from '../../model/store.ts';
|
|
19
30
|
import {
|
|
20
31
|
type ElementId,
|
|
32
|
+
type LineStyle,
|
|
21
33
|
type Link,
|
|
34
|
+
type LinkId,
|
|
35
|
+
MAJOR_GRID_RATIO,
|
|
22
36
|
type Marker,
|
|
23
37
|
type Node,
|
|
24
38
|
type NodeId,
|
|
25
39
|
type Scene,
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
isEllipseNode,
|
|
40
|
+
type StyleMap,
|
|
41
|
+
isFrameNode,
|
|
29
42
|
isNoteNode,
|
|
30
|
-
isPortalNode,
|
|
31
43
|
linkMarkers,
|
|
32
44
|
showsContents,
|
|
33
45
|
} from '../../model/types.ts';
|
|
34
46
|
import { portalFrame, portalScale, portalTransform } from '../../utils/camera.ts';
|
|
35
47
|
import { contentBounds } from '../../utils/hit.ts';
|
|
36
48
|
import { type LatticeSpec } from '../../utils/lattice.ts';
|
|
49
|
+
import { elementLayer, sceneLayers, visibleScene } from '../../utils/layers.ts';
|
|
37
50
|
import { sortByZ } from '../../utils/order.ts';
|
|
38
|
-
import { type PartEditing, type PartKey } from '../../utils/parts.ts';
|
|
51
|
+
import { type PartEditing, type PartKey, isMultiline, nodeParts } from '../../utils/parts.ts';
|
|
39
52
|
import { sceneLinkGeometry } from '../../utils/route.ts';
|
|
40
|
-
import { nodeBounds } from '../../utils/shapes.ts';
|
|
41
|
-
import { frameClasses } from '../../utils/style.ts';
|
|
53
|
+
import { DEFAULT_CELL, nodeBounds } from '../../utils/shapes.ts';
|
|
54
|
+
import { alignClasses, classedLink, classedNode, frameClasses, lineClasses } from '../../utils/style.ts';
|
|
42
55
|
import { TextPart } from '../PartEditor/PartEditor.tsx';
|
|
43
56
|
|
|
44
|
-
/** Screen px below which a portal
|
|
57
|
+
/** Screen px below which a portal shows only its title; above it a portal showing its contents mounts the child live. */
|
|
45
58
|
export const DOT_PX = 40;
|
|
46
|
-
export const PREVIEW_PX = 260;
|
|
47
59
|
/** Default for `liveDepth`: root plus this many live nested levels (decision 10). */
|
|
48
60
|
export const MAX_LIVE_DEPTH = 1;
|
|
49
61
|
/** Hysteresis at the tier boundaries so a portal does not flicker while zooming across one. */
|
|
@@ -55,14 +67,11 @@ export const tierFor = (node: Node, zoom: number, depth: number, liveDepth: numb
|
|
|
55
67
|
const { width, height } = nodeBounds(node);
|
|
56
68
|
const px = Math.min(width, height) * zoom;
|
|
57
69
|
const dot = previous === 'dot' ? DOT_PX * (1 + TIER_HYSTERESIS) : DOT_PX * (1 - TIER_HYSTERESIS);
|
|
58
|
-
const preview = previous === 'preview' ? PREVIEW_PX * (1 + TIER_HYSTERESIS) : PREVIEW_PX * (1 - TIER_HYSTERESIS);
|
|
59
70
|
if (px < dot) {
|
|
60
71
|
return 'dot';
|
|
61
72
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}
|
|
65
|
-
return 'live';
|
|
73
|
+
// Past `liveDepth` the child is summarised rather than mounted, which bounds how many scenes render live.
|
|
74
|
+
return depth >= liveDepth ? 'preview' : 'live';
|
|
66
75
|
};
|
|
67
76
|
|
|
68
77
|
export type ElementHandlers = {
|
|
@@ -73,6 +82,8 @@ export type ElementHandlers = {
|
|
|
73
82
|
/** The in-place editor of `part` finished with `text` (commit) or was dismissed (cancel). */
|
|
74
83
|
onPartCommit?: (node: Node, part: PartKey, text: string) => void;
|
|
75
84
|
onPartCancel?: () => void;
|
|
85
|
+
/** The pointer entered a link (its id) or left it (`undefined`). */
|
|
86
|
+
onLinkHover?: (id: LinkId | undefined) => void;
|
|
76
87
|
/** A node's own open control was pressed (a portal's zoom-in icon). */
|
|
77
88
|
onNodeOpen?: (node: Node) => void;
|
|
78
89
|
};
|
|
@@ -86,11 +97,19 @@ export type SceneLayerProps = {
|
|
|
86
97
|
depth: number;
|
|
87
98
|
liveDepth: number;
|
|
88
99
|
selected?: ReadonlySet<ElementId>;
|
|
100
|
+
/** The selection is drawn as it looks, without its outline (a move in flight shows the shapes themselves). */
|
|
101
|
+
plain?: boolean;
|
|
89
102
|
hover?: NodeId;
|
|
103
|
+
/** The link under the pointer, outlined as a hovered shape is. */
|
|
104
|
+
hoveredLink?: LinkId;
|
|
90
105
|
/** The portal a drill-in is animating into, while it is. */
|
|
91
106
|
opening?: ElementId;
|
|
107
|
+
/** The portal filling most of the view, and the opacity of everything else on the layer as it does. */
|
|
108
|
+
focus?: { id: ElementId; opacity: number };
|
|
92
109
|
/** The text part being edited in place, if any. */
|
|
93
110
|
editing?: { id: NodeId; part: PartKey };
|
|
111
|
+
/** The node whose embedded content takes input. */
|
|
112
|
+
active?: NodeId;
|
|
94
113
|
/** A node drawn as a preview of what a gesture will create: translucent, dashed, and not pressable. */
|
|
95
114
|
ghost?: NodeId;
|
|
96
115
|
/** Frames show their id, type and geometry. */
|
|
@@ -99,105 +118,236 @@ export type SceneLayerProps = {
|
|
|
99
118
|
handlers?: ElementHandlers;
|
|
100
119
|
/** The scene's lattice, when it has one: smart links route through its gutters. */
|
|
101
120
|
lattice?: LatticeSpec;
|
|
121
|
+
/** Scene px of one nominal unit (the drawing's major grid cell); a nested layer inherits its parent's. */
|
|
122
|
+
cell?: number;
|
|
102
123
|
};
|
|
103
124
|
|
|
104
125
|
export const SceneLayer = memo(
|
|
105
126
|
({
|
|
106
127
|
store,
|
|
107
|
-
scene,
|
|
128
|
+
scene: sceneProp,
|
|
108
129
|
registry,
|
|
109
130
|
zoom,
|
|
110
131
|
depth,
|
|
111
132
|
liveDepth,
|
|
112
133
|
selected,
|
|
134
|
+
plain,
|
|
113
135
|
hover,
|
|
136
|
+
hoveredLink,
|
|
114
137
|
opening,
|
|
138
|
+
focus,
|
|
115
139
|
editing,
|
|
140
|
+
active,
|
|
116
141
|
ghost,
|
|
117
142
|
debug,
|
|
118
143
|
handlers,
|
|
119
|
-
lattice,
|
|
144
|
+
lattice: latticeProp,
|
|
145
|
+
cell: cellProp,
|
|
120
146
|
}: SceneLayerProps) => {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
147
|
+
const inherited = useContext(CellContext);
|
|
148
|
+
const cell = cellProp ?? inherited;
|
|
149
|
+
// A nested scene (a frame's contents, a drill-in in flight) routes on its parent's lattice, so its links do not
|
|
150
|
+
// jump onto the gutters when it settles as the root.
|
|
151
|
+
const inheritedLattice = useContext(LatticeContext);
|
|
152
|
+
const lattice = latticeProp ?? inheritedLattice;
|
|
153
|
+
// A hidden layer's elements are not drawn (nor, for the root, hit: the view hit-tests the same visible scene).
|
|
154
|
+
const scene = useMemo(() => visibleScene(sceneProp), [sceneProp]);
|
|
129
155
|
const links = useMemo(
|
|
130
156
|
() => sceneLinkGeometry(scene, registry, sortByZ(Object.values(scene.links)), lattice),
|
|
131
157
|
[scene, registry, lattice],
|
|
132
158
|
);
|
|
159
|
+
// Paint order is the layers', bottom first: each layer's links, then its nodes, then its links' ends, so an upper
|
|
160
|
+
// layer covers a lower one whole. Within a layer it is z, with the selection on top: a selected node is being
|
|
161
|
+
// worked on and must not hide under a neighbour, while the model's order stays what the user arranged.
|
|
162
|
+
const groups = useMemo(() => {
|
|
163
|
+
const layers = sceneLayers(scene);
|
|
164
|
+
const nodes = sortByZ(Object.values(scene.nodes));
|
|
165
|
+
return layers.map((layer) => {
|
|
166
|
+
const onLayer = nodes.filter((node) => elementLayer(node, layers) === layer.id);
|
|
167
|
+
return {
|
|
168
|
+
layer,
|
|
169
|
+
nodes: selected?.size
|
|
170
|
+
? [...onLayer.filter((node) => !selected.has(node.id)), ...onLayer.filter((node) => selected.has(node.id))]
|
|
171
|
+
: onLayer,
|
|
172
|
+
links: links.filter(({ link }) => elementLayer(link, layers) === layer.id),
|
|
173
|
+
};
|
|
174
|
+
});
|
|
175
|
+
}, [scene, links, selected]);
|
|
176
|
+
// Each element is drawn with its class's look under its own; the handlers still get the element as stored.
|
|
177
|
+
const styles = useAtomValue(store.styles ?? NO_STYLES);
|
|
178
|
+
const lines = useMemo(
|
|
179
|
+
() => new Map(links.map(({ link }) => [link.id, classedLink(link, styles).style])),
|
|
180
|
+
[links, styles],
|
|
181
|
+
);
|
|
133
182
|
const unit = 1 / Math.max(zoom, 0.05);
|
|
134
|
-
|
|
183
|
+
const linkWidth = LINK_WIDTH * lineWeight(depth, zoom);
|
|
184
|
+
// Everything but the portal being zoomed into fades with the zoom (see `layerOpacity`).
|
|
185
|
+
const fadeStyle: CSSProperties | undefined = focus && focus.opacity < 1 ? { opacity: focus.opacity } : undefined;
|
|
186
|
+
// One set of end markers per line colour in use, sized in scene units so they scale with the nodes they join.
|
|
135
187
|
const markerId = useId();
|
|
136
|
-
const
|
|
137
|
-
|
|
188
|
+
const lineHues = useMemo(() => [...new Set([...lines.values()].map((line) => line?.hue))], [lines]);
|
|
189
|
+
const markerUrl = (marker: Marker | undefined, end: 'start' | 'end', hue: string | undefined) =>
|
|
190
|
+
marker ? `url(#${markerId}-${hue ?? 'default'}-${marker}-${end})` : undefined;
|
|
138
191
|
|
|
139
192
|
return (
|
|
140
|
-
|
|
141
|
-
<
|
|
142
|
-
<
|
|
143
|
-
<
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
className='fill-none stroke-transparent pointer-events-auto cursor-pointer'
|
|
152
|
-
style={{ pointerEvents: 'stroke' }}
|
|
153
|
-
strokeWidth={12 * unit}
|
|
154
|
-
onPointerDown={(event) => handlers.onLinkPointerDown?.(link, event)}
|
|
155
|
-
onDoubleClick={(event) => handlers.onLinkDoubleClick?.(link, event)}
|
|
156
|
-
onContextMenu={(event) => handlers.onLinkContextMenu?.(link, event)}
|
|
193
|
+
<CellContext.Provider value={cell}>
|
|
194
|
+
<LatticeContext.Provider value={lattice}>
|
|
195
|
+
<svg className='absolute overflow-visible pointer-events-none' style={fadeStyle} width={1} height={1}>
|
|
196
|
+
<defs>
|
|
197
|
+
{lineHues.map((hue) => (
|
|
198
|
+
<Markers
|
|
199
|
+
key={hue ?? 'default'}
|
|
200
|
+
id={`${markerId}-${hue ?? 'default'}`}
|
|
201
|
+
cell={cell}
|
|
202
|
+
hue={hue}
|
|
203
|
+
width={linkWidth}
|
|
157
204
|
/>
|
|
158
|
-
)}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
205
|
+
))}
|
|
206
|
+
</defs>
|
|
207
|
+
</svg>
|
|
208
|
+
{groups.map((group) => (
|
|
209
|
+
<Fragment key={group.layer.id}>
|
|
210
|
+
<svg className='absolute overflow-visible pointer-events-none' style={fadeStyle} width={1} height={1}>
|
|
211
|
+
{group.links.map(({ link, path }) => (
|
|
212
|
+
<g key={link.id}>
|
|
213
|
+
{/* A wide transparent twin makes the thin stroke easy to press. */}
|
|
214
|
+
{handlers && (
|
|
215
|
+
<path
|
|
216
|
+
d={path}
|
|
217
|
+
className='fill-none stroke-transparent pointer-events-auto cursor-pointer'
|
|
218
|
+
style={{ pointerEvents: 'stroke' }}
|
|
219
|
+
strokeWidth={12 * unit}
|
|
220
|
+
onPointerDown={(event) => handlers.onLinkPointerDown?.(link, event)}
|
|
221
|
+
onPointerEnter={() => handlers.onLinkHover?.(link.id)}
|
|
222
|
+
onPointerLeave={() => handlers.onLinkHover?.(undefined)}
|
|
223
|
+
onDoubleClick={(event) => handlers.onLinkDoubleClick?.(link, event)}
|
|
224
|
+
onContextMenu={(event) => handlers.onLinkContextMenu?.(link, event)}
|
|
225
|
+
/>
|
|
226
|
+
)}
|
|
227
|
+
<path
|
|
228
|
+
d={path}
|
|
229
|
+
className={mx(
|
|
230
|
+
'fill-none',
|
|
231
|
+
plain
|
|
232
|
+
? lineClasses(lines.get(link.id)?.hue).stroke
|
|
233
|
+
: selected?.has(link.id)
|
|
234
|
+
? 'stroke-focus'
|
|
235
|
+
: hoveredLink === link.id
|
|
236
|
+
? 'stroke-focus/50'
|
|
237
|
+
: lineClasses(lines.get(link.id)?.hue).stroke,
|
|
238
|
+
)}
|
|
239
|
+
strokeWidth={linkWidth}
|
|
240
|
+
strokeDasharray={dashArray(lines.get(link.id)?.lineStyle, linkWidth)}
|
|
241
|
+
strokeLinecap={lines.get(link.id)?.lineStyle === 'dotted' ? 'round' : undefined}
|
|
242
|
+
data-link-id={link.id}
|
|
243
|
+
/>
|
|
244
|
+
{link.text && <LinkText path={path} text={link.text} />}
|
|
245
|
+
</g>
|
|
246
|
+
))}
|
|
247
|
+
</svg>
|
|
248
|
+
{group.nodes.map((node) => (
|
|
249
|
+
<NodeFrame
|
|
250
|
+
key={node.id}
|
|
251
|
+
store={store}
|
|
252
|
+
scene={scene}
|
|
253
|
+
registry={registry}
|
|
254
|
+
node={node}
|
|
255
|
+
styles={styles}
|
|
256
|
+
zoom={zoom}
|
|
257
|
+
depth={depth}
|
|
258
|
+
liveDepth={liveDepth}
|
|
259
|
+
selected={!plain && (selected?.has(node.id) ?? false)}
|
|
260
|
+
hovered={hover === node.id}
|
|
261
|
+
opening={opening === node.id}
|
|
262
|
+
fade={focus && focus.id !== node.id ? fadeStyle : undefined}
|
|
263
|
+
chromeFade={focus?.id === node.id ? fadeStyle : undefined}
|
|
264
|
+
editingPart={editing?.id === node.id ? editing.part : undefined}
|
|
265
|
+
active={active === node.id}
|
|
266
|
+
ghost={ghost === node.id}
|
|
267
|
+
debug={debug}
|
|
268
|
+
handlers={handlers}
|
|
269
|
+
/>
|
|
270
|
+
))}
|
|
271
|
+
{/* The ends paint over the layer's nodes, so an end centred on a node's edge shows whole. */}
|
|
272
|
+
<svg className='absolute overflow-visible pointer-events-none' style={fadeStyle} width={1} height={1}>
|
|
273
|
+
{group.links.map(({ link, path }) => (
|
|
274
|
+
<path
|
|
275
|
+
key={link.id}
|
|
276
|
+
d={path}
|
|
277
|
+
className='fill-none stroke-none'
|
|
278
|
+
markerStart={markerUrl(linkMarkers(link).start, 'start', lines.get(link.id)?.hue)}
|
|
279
|
+
markerEnd={markerUrl(linkMarkers(link).end, 'end', lines.get(link.id)?.hue)}
|
|
280
|
+
/>
|
|
281
|
+
))}
|
|
282
|
+
</svg>
|
|
283
|
+
</Fragment>
|
|
168
284
|
))}
|
|
169
|
-
</
|
|
170
|
-
|
|
171
|
-
<NodeFrame
|
|
172
|
-
key={node.id}
|
|
173
|
-
store={store}
|
|
174
|
-
scene={scene}
|
|
175
|
-
registry={registry}
|
|
176
|
-
node={node}
|
|
177
|
-
zoom={zoom}
|
|
178
|
-
depth={depth}
|
|
179
|
-
liveDepth={liveDepth}
|
|
180
|
-
selected={selected?.has(node.id) ?? false}
|
|
181
|
-
hovered={hover === node.id}
|
|
182
|
-
opening={opening === node.id}
|
|
183
|
-
editingPart={editing?.id === node.id ? editing.part : undefined}
|
|
184
|
-
ghost={ghost === node.id}
|
|
185
|
-
debug={debug}
|
|
186
|
-
handlers={handlers}
|
|
187
|
-
/>
|
|
188
|
-
))}
|
|
189
|
-
</>
|
|
285
|
+
</LatticeContext.Provider>
|
|
286
|
+
</CellContext.Provider>
|
|
190
287
|
);
|
|
191
288
|
},
|
|
192
289
|
);
|
|
193
290
|
|
|
194
291
|
SceneLayer.displayName = 'SceneLayer';
|
|
195
292
|
|
|
293
|
+
/** The width of a node frame's border, which a nested scene drawn inside it steps out over. */
|
|
294
|
+
const FRAME_BORDER = '--scene-frame-border' as const;
|
|
295
|
+
|
|
296
|
+
/** A link's stroke, in scene units like a node's 2px border, so a link keeps its weight beside the shapes at any zoom. */
|
|
297
|
+
const LINK_WIDTH = 2;
|
|
298
|
+
|
|
299
|
+
/** The most a nested scene's lines thicken by. */
|
|
300
|
+
const MAX_LINE_WEIGHT = 2;
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* How much thicker a layer draws its lines: a scene seen inside another is small, so its borders and links thicken,
|
|
304
|
+
* up to double, as it shrinks; at its own size (as it is drilled into) it is back to 1, so nothing jumps at the swap.
|
|
305
|
+
*/
|
|
306
|
+
export const lineWeight = (depth: number, zoom: number): number =>
|
|
307
|
+
depth > 0 ? Math.min(MAX_LINE_WEIGHT, Math.max(1, 1 / Math.max(zoom, 0.05))) : 1;
|
|
308
|
+
|
|
309
|
+
/** Scene px of a nominal unit for the layers below a `SceneLayer` given one, so nested scenes draw alike. */
|
|
310
|
+
const CellContext = createContext(DEFAULT_CELL);
|
|
311
|
+
|
|
312
|
+
/** The lattice smart links route on, inherited by nested layers. */
|
|
313
|
+
const LatticeContext = createContext<LatticeSpec | undefined>(undefined);
|
|
314
|
+
|
|
315
|
+
/** The scene's minor grid, in px, on each node frame for the text inside it. */
|
|
316
|
+
const MINOR_GRID = '--dx-minor-grid';
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* A text part covers the frame from its outer edge, border included, so its lines start on the grid the frame's edge is
|
|
320
|
+
* on rather than a border width inside it.
|
|
321
|
+
*/
|
|
322
|
+
// Spelled out, not built from `FRAME_BORDER`: Tailwind only emits classes it can read verbatim.
|
|
323
|
+
// The text's own box is flush with the frame's edge and pads its text a grid unit either side; its lines need no
|
|
324
|
+
// padding above or below, being on the grid already.
|
|
325
|
+
const TEXT_BOX = 'absolute inset-[calc(-1*var(--scene-frame-border,2px))] [&>span]:px-(--dx-minor-grid)';
|
|
326
|
+
|
|
327
|
+
/** A line is a whole number of minor grid units tall, the text centred in it, so lines of text sit on the grid. */
|
|
328
|
+
const LINE_ON_GRID = mx(
|
|
329
|
+
// Important: a size class (`text-lg`) carries its own leading, which would otherwise win.
|
|
330
|
+
'[--dx-line:round(up,1.25em,var(--dx-minor-grid))] leading-(--dx-line)!',
|
|
331
|
+
);
|
|
332
|
+
|
|
333
|
+
/** A line pattern's dashes in scene units, relative to the stroke; a dot is a zero-length dash with a round cap. */
|
|
334
|
+
const dashArray = (dash: LineStyle['lineStyle'], width = LINK_WIDTH): string | undefined =>
|
|
335
|
+
dash === 'dashed' ? `${4 * width} ${3 * width}` : dash === 'dotted' ? `0 ${2.5 * width}` : undefined;
|
|
336
|
+
|
|
337
|
+
/** Bounding box of every end, in nominal units: a quarter of a major grid cell. */
|
|
338
|
+
const END_BOX = 0.25;
|
|
339
|
+
|
|
196
340
|
/** The end markers, one per kind and end: a start marker points back along the path, an end marker along it. */
|
|
197
|
-
const Markers = ({ id,
|
|
198
|
-
const
|
|
199
|
-
//
|
|
200
|
-
|
|
341
|
+
const Markers = ({ id, cell, hue, width }: { id: string; cell: number; hue: string | undefined; width: number }) => {
|
|
342
|
+
const line = lineClasses(hue);
|
|
343
|
+
// Each end fills a box of `END_BOX` nominal units, so it scales with the grid and the shapes it joins: the
|
|
344
|
+
// arrow and the triangle 10 of their 12 view units, the circle 8 of its 10.
|
|
345
|
+
const box = END_BOX * cell;
|
|
346
|
+
const arrow = box;
|
|
347
|
+
const triangle = (box * 12) / 10;
|
|
348
|
+
const circle = (box * 10) / 8;
|
|
349
|
+
// An outline matches the line's width, in its marker's view units.
|
|
350
|
+
const outline = (size: number, view: number) => (width * view) / size;
|
|
201
351
|
const ends = ['start', 'end'] as const;
|
|
202
352
|
return (
|
|
203
353
|
<>
|
|
@@ -205,17 +355,45 @@ const Markers = ({ id, unit }: { id: string; unit: number }) => {
|
|
|
205
355
|
<marker
|
|
206
356
|
key={`arrow-${end}`}
|
|
207
357
|
id={`${id}-arrow-${end}`}
|
|
208
|
-
viewBox='
|
|
209
|
-
refX={
|
|
358
|
+
viewBox='-1 -1 12 12'
|
|
359
|
+
refX={10}
|
|
210
360
|
refY={5}
|
|
211
|
-
markerWidth={arrow}
|
|
212
|
-
markerHeight={arrow}
|
|
361
|
+
markerWidth={(arrow * 12) / 10}
|
|
362
|
+
markerHeight={(arrow * 12) / 10}
|
|
213
363
|
markerUnits='userSpaceOnUse'
|
|
214
364
|
orient={end === 'start' ? 'auto-start-reverse' : 'auto'}
|
|
215
365
|
>
|
|
216
|
-
|
|
366
|
+
{/* An open arrowhead: two strokes, not a filled head. */}
|
|
367
|
+
<path
|
|
368
|
+
d='M 0 0 L 10 5 L 0 10'
|
|
369
|
+
className={mx('fill-none', line.stroke)}
|
|
370
|
+
strokeWidth={outline((arrow * 12) / 10, 12)}
|
|
371
|
+
strokeLinecap='round'
|
|
372
|
+
strokeLinejoin='round'
|
|
373
|
+
/>
|
|
217
374
|
</marker>
|
|
218
375
|
))}
|
|
376
|
+
{/* Inheritance (UML generalization): a triangle filled with the canvas, so the line stops at its base. */}
|
|
377
|
+
{ends.map((end) => (
|
|
378
|
+
<marker
|
|
379
|
+
key={`triangle-${end}`}
|
|
380
|
+
id={`${id}-triangle-${end}`}
|
|
381
|
+
viewBox='-1 -1 12 12'
|
|
382
|
+
refX={10}
|
|
383
|
+
refY={5}
|
|
384
|
+
markerWidth={triangle}
|
|
385
|
+
markerHeight={triangle}
|
|
386
|
+
markerUnits='userSpaceOnUse'
|
|
387
|
+
orient={end === 'start' ? 'auto-start-reverse' : 'auto'}
|
|
388
|
+
>
|
|
389
|
+
<path
|
|
390
|
+
d='M 0 0 L 10 5 L 0 10 z'
|
|
391
|
+
className={mx('fill-base-surface', line.stroke)}
|
|
392
|
+
strokeWidth={outline(triangle, 12)}
|
|
393
|
+
/>
|
|
394
|
+
</marker>
|
|
395
|
+
))}
|
|
396
|
+
{/* A circle centred on the connection point, filled with the canvas so the line stops at its rim. */}
|
|
219
397
|
{ends.map((end) => (
|
|
220
398
|
<marker
|
|
221
399
|
key={`circle-${end}`}
|
|
@@ -223,81 +401,212 @@ const Markers = ({ id, unit }: { id: string; unit: number }) => {
|
|
|
223
401
|
viewBox='0 0 10 10'
|
|
224
402
|
refX={5}
|
|
225
403
|
refY={5}
|
|
226
|
-
markerWidth={
|
|
227
|
-
markerHeight={
|
|
404
|
+
markerWidth={circle}
|
|
405
|
+
markerHeight={circle}
|
|
228
406
|
markerUnits='userSpaceOnUse'
|
|
229
407
|
>
|
|
230
|
-
<circle
|
|
408
|
+
<circle
|
|
409
|
+
cx={5}
|
|
410
|
+
cy={5}
|
|
411
|
+
r={4}
|
|
412
|
+
className={mx('fill-base-surface', line.stroke)}
|
|
413
|
+
strokeWidth={outline(circle, 10)}
|
|
414
|
+
/>
|
|
231
415
|
</marker>
|
|
232
416
|
))}
|
|
233
417
|
</>
|
|
234
418
|
);
|
|
235
419
|
};
|
|
236
420
|
|
|
421
|
+
type LinkTextProps = { path: string; text: string };
|
|
422
|
+
|
|
423
|
+
/** A caption's backdrop margin around its text, in scene units. */
|
|
424
|
+
const LINK_TEXT_PAD = { x: 8, y: 2 };
|
|
425
|
+
|
|
426
|
+
/** A link's caption at the middle of its route, over a halo of the canvas so it reads where it crosses lines. */
|
|
427
|
+
const LinkText = ({ path, text }: LinkTextProps) => {
|
|
428
|
+
const ref = useRef<SVGPathElement>(null);
|
|
429
|
+
const textRef = useRef<SVGTextElement>(null);
|
|
430
|
+
const [at, setAt] = useState<{ x: number; y: number }>();
|
|
431
|
+
const [box, setBox] = useState<{ x: number; y: number; width: number; height: number }>();
|
|
432
|
+
// The route is only known as drawn: its midpoint is measured along the path, not between its ends.
|
|
433
|
+
useLayoutEffect(() => {
|
|
434
|
+
const element = ref.current;
|
|
435
|
+
if (element) {
|
|
436
|
+
const { x, y } = element.getPointAtLength(element.getTotalLength() / 2);
|
|
437
|
+
setAt({ x, y });
|
|
438
|
+
}
|
|
439
|
+
}, [path]);
|
|
440
|
+
// The backdrop fits the text as set, so it is measured once the text is placed.
|
|
441
|
+
useLayoutEffect(() => {
|
|
442
|
+
const element = textRef.current;
|
|
443
|
+
if (element && at) {
|
|
444
|
+
const { x, y, width, height } = element.getBBox();
|
|
445
|
+
setBox({
|
|
446
|
+
x: x - LINK_TEXT_PAD.x,
|
|
447
|
+
y: y - LINK_TEXT_PAD.y,
|
|
448
|
+
width: width + LINK_TEXT_PAD.x * 2,
|
|
449
|
+
height: height + LINK_TEXT_PAD.y * 2,
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
}, [at, text]);
|
|
453
|
+
return (
|
|
454
|
+
<>
|
|
455
|
+
<path ref={ref} d={path} className='fill-none stroke-none' />
|
|
456
|
+
{at && (
|
|
457
|
+
<>
|
|
458
|
+
{/* Opaque, so the caption reads over the lines it sits on. */}
|
|
459
|
+
{box && <rect {...box} rx={box.height / 2} className='fill-base-surface' data-link-text-box />}
|
|
460
|
+
<text
|
|
461
|
+
ref={textRef}
|
|
462
|
+
x={at.x}
|
|
463
|
+
y={at.y}
|
|
464
|
+
textAnchor='middle'
|
|
465
|
+
dominantBaseline='central'
|
|
466
|
+
className='fill-fg-muted text-sm'
|
|
467
|
+
data-link-text
|
|
468
|
+
>
|
|
469
|
+
{text}
|
|
470
|
+
</text>
|
|
471
|
+
</>
|
|
472
|
+
)}
|
|
473
|
+
</>
|
|
474
|
+
);
|
|
475
|
+
};
|
|
476
|
+
|
|
237
477
|
type NodeFrameProps = Omit<NodeViewProps, 'editing'> & {
|
|
238
478
|
hovered?: boolean;
|
|
239
479
|
editingPart?: PartKey;
|
|
240
480
|
ghost?: boolean;
|
|
241
481
|
debug?: boolean;
|
|
242
482
|
handlers?: ElementHandlers;
|
|
483
|
+
/** The fade while another node is zoomed into. */
|
|
484
|
+
fade?: CSSProperties;
|
|
485
|
+
/** The fade of this node's own frame (fill and border) while it is zoomed into; its contents stay. */
|
|
486
|
+
chromeFade?: CSSProperties;
|
|
487
|
+
/** The drawing's style classes, under the node's own style. */
|
|
488
|
+
styles?: StyleMap;
|
|
243
489
|
};
|
|
244
490
|
|
|
245
491
|
/** Positions a node, owns its frame styling and pointer events; the node definition renders the body. */
|
|
246
|
-
const NodeFrame = memo(
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
492
|
+
const NodeFrame = memo(
|
|
493
|
+
({ handlers, hovered, editingPart, ghost, debug, fade, chromeFade, styles, ...props }: NodeFrameProps) => {
|
|
494
|
+
const { node, registry, selected } = props;
|
|
495
|
+
const cell = useContext(CellContext);
|
|
496
|
+
const drawn = useMemo(() => classedNode(node, styles), [node, styles]);
|
|
497
|
+
const bounds = nodeBounds(node);
|
|
498
|
+
const interactive = handlers !== undefined && !ghost;
|
|
499
|
+
// A type the registry does not know is drawn as the core base: a box with its label.
|
|
500
|
+
const def = nodeDef(registry, node);
|
|
501
|
+
const Component = def?.component ?? BoxNodeView;
|
|
502
|
+
const Toolbar = interactive ? def?.toolbar : undefined;
|
|
503
|
+
const editing = useMemo<PartEditing | undefined>(
|
|
504
|
+
() =>
|
|
505
|
+
editingPart && handlers
|
|
506
|
+
? {
|
|
507
|
+
part: editingPart,
|
|
508
|
+
multiline: nodeParts(registry, node).some((part) => part.field === editingPart && isMultiline(part)),
|
|
509
|
+
commit: (text) => handlers.onPartCommit?.(node, editingPart, text),
|
|
510
|
+
cancel: () => handlers.onPartCancel?.(),
|
|
511
|
+
}
|
|
512
|
+
: undefined,
|
|
513
|
+
[editingPart, handlers, node, registry],
|
|
514
|
+
);
|
|
515
|
+
const onOpen = useMemo(
|
|
516
|
+
() => (interactive && handlers.onNodeOpen ? () => handlers.onNodeOpen?.(node) : undefined),
|
|
517
|
+
[interactive, handlers, node],
|
|
518
|
+
);
|
|
519
|
+
// Being zoomed into, the frame becomes the child scene's canvas, so it drops its fill (the first class)
|
|
520
|
+
// at once and keeps only its own border, not the selection's or the hover's.
|
|
521
|
+
// `fontSize` is inherited by every text part, so an override set on the node reaches the label,
|
|
522
|
+
// the class compartments and the editor alike; unset, the parts keep their own theme sizes.
|
|
523
|
+
// The frame's border is 2px, thickened with the layer's lines (`lineWeight`); fading, it is padding instead.
|
|
524
|
+
const border = 2 * lineWeight(props.depth, props.zoom);
|
|
525
|
+
const thick = border !== 2;
|
|
526
|
+
const frameStyle: CSSProperties & Record<`--${string}`, string> = {
|
|
527
|
+
left: bounds.x,
|
|
528
|
+
top: bounds.y,
|
|
529
|
+
width: bounds.width,
|
|
530
|
+
height: bounds.height,
|
|
531
|
+
fontSize: drawn.style?.fontSize,
|
|
532
|
+
[FRAME_BORDER]: chromeFade ? '0px' : `${border}px`,
|
|
533
|
+
[MINOR_GRID]: `${cell / MAJOR_GRID_RATIO}px`,
|
|
534
|
+
...(thick ? (chromeFade ? { padding: border } : { borderWidth: border }) : {}),
|
|
535
|
+
...fade,
|
|
536
|
+
};
|
|
537
|
+
const frameLook = props.opening ? frameClasses(drawn, false).slice(1) : frameClasses(drawn, selected, hovered);
|
|
538
|
+
return (
|
|
539
|
+
<>
|
|
291
540
|
<div
|
|
292
|
-
className=
|
|
293
|
-
|
|
541
|
+
className={mx(
|
|
542
|
+
'absolute box-border overflow-hidden',
|
|
543
|
+
// Fading, the border becomes padding of the same width so the contents stay put.
|
|
544
|
+
...(chromeFade ? ['p-0.5 isolate'] : ['border-2', ...frameLook]),
|
|
545
|
+
// Every text part inherits the face, as it does `fontSize`.
|
|
546
|
+
drawn.style?.fontFamily === 'monospace' && 'font-mono',
|
|
547
|
+
interactive && !node.locked && 'cursor-grab',
|
|
548
|
+
ghost && 'opacity-50 border-dashed pointer-events-none',
|
|
549
|
+
)}
|
|
550
|
+
style={frameStyle}
|
|
551
|
+
data-node-id={node.id}
|
|
552
|
+
data-ghost={ghost || undefined}
|
|
553
|
+
onPointerDown={
|
|
554
|
+
interactive
|
|
555
|
+
? (event) => {
|
|
556
|
+
// React bubbles a portal's events through the tree: a press in a menu the node's content opened is
|
|
557
|
+
// not a press on the node.
|
|
558
|
+
if (event.target instanceof Element && event.currentTarget.contains(event.target)) {
|
|
559
|
+
handlers.onNodePointerDown?.(node, event);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
: undefined
|
|
563
|
+
}
|
|
294
564
|
>
|
|
295
|
-
{
|
|
565
|
+
{/* Fading, the frame's fill and border are drawn behind the contents so they fade without them. */}
|
|
566
|
+
{chromeFade && (
|
|
567
|
+
<div
|
|
568
|
+
aria-hidden
|
|
569
|
+
className={mx('dx-cover -z-10 border-2 pointer-events-none', ...frameLook)}
|
|
570
|
+
style={thick ? { ...chromeFade, borderWidth: border } : chromeFade}
|
|
571
|
+
/>
|
|
572
|
+
)}
|
|
573
|
+
<Component {...props} node={drawn} editing={editing} onOpen={onOpen} />
|
|
574
|
+
{debug && (
|
|
575
|
+
<div
|
|
576
|
+
className='absolute top-0 left-0 px-1 text-[10px] leading-4 font-mono whitespace-nowrap bg-modal-surface text-fg-muted pointer-events-none'
|
|
577
|
+
data-testid='node-debug'
|
|
578
|
+
>
|
|
579
|
+
{node.id} · {node.type} · {bounds.x},{bounds.y} {bounds.width}×{bounds.height} · z {node.z}
|
|
580
|
+
</div>
|
|
581
|
+
)}
|
|
296
582
|
</div>
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
583
|
+
{Toolbar && (
|
|
584
|
+
// Outside the frame, which clips its contents; scaled back to screen size from its bottom-right corner. Its
|
|
585
|
+
// own width (`w-max`): near the layer's edge an absolute box would shrink to the space left. Shown with the
|
|
586
|
+
// frame's hover or selection, and kept while the pointer crosses onto it.
|
|
587
|
+
<div
|
|
588
|
+
className={mx(
|
|
589
|
+
'absolute flex w-max pb-1 transition-opacity',
|
|
590
|
+
hovered || selected ? 'opacity-100' : 'opacity-0 hover:opacity-100',
|
|
591
|
+
)}
|
|
592
|
+
style={{
|
|
593
|
+
left: bounds.x + bounds.width,
|
|
594
|
+
top: bounds.y,
|
|
595
|
+
transform: `translate(-100%, -100%) scale(${1 / Math.max(props.zoom, 0.05)})`,
|
|
596
|
+
transformOrigin: 'bottom right',
|
|
597
|
+
...fade,
|
|
598
|
+
}}
|
|
599
|
+
data-testid='node-toolbar'
|
|
600
|
+
onPointerDown={(event) => event.stopPropagation()}
|
|
601
|
+
onDoubleClick={(event) => event.stopPropagation()}
|
|
602
|
+
>
|
|
603
|
+
<Toolbar {...props} node={drawn} editing={editing} onOpen={onOpen} />
|
|
604
|
+
</div>
|
|
605
|
+
)}
|
|
606
|
+
</>
|
|
607
|
+
);
|
|
608
|
+
},
|
|
609
|
+
);
|
|
301
610
|
|
|
302
611
|
NodeFrame.displayName = 'NodeFrame';
|
|
303
612
|
|
|
@@ -316,16 +625,25 @@ const LabelPart = ({ node, editing, label }: LabelPartProps) => (
|
|
|
316
625
|
text={label}
|
|
317
626
|
editing={editing}
|
|
318
627
|
classNames={mx(
|
|
319
|
-
|
|
320
|
-
|
|
628
|
+
TEXT_BOX,
|
|
629
|
+
'whitespace-pre-wrap',
|
|
630
|
+
LINE_ON_GRID,
|
|
631
|
+
alignClasses(node.style, { horizontal: 'center', vertical: 'middle' }),
|
|
632
|
+
sizeClass(node, 'text-lg'),
|
|
321
633
|
)}
|
|
322
634
|
>
|
|
323
635
|
{label}
|
|
324
636
|
</TextPart>
|
|
325
637
|
);
|
|
326
638
|
|
|
639
|
+
/** A node's core-base `label`, read from any type: a type the registry does not know still carries it. */
|
|
640
|
+
const baseLabel = (node: Node): string => {
|
|
641
|
+
const label: unknown = Reflect.get(node, 'label');
|
|
642
|
+
return typeof label === 'string' ? label : '';
|
|
643
|
+
};
|
|
644
|
+
|
|
327
645
|
const LabelNodeView = ({ node, editing }: NodeViewProps) => (
|
|
328
|
-
<LabelPart node={node} editing={editing} label={
|
|
646
|
+
<LabelPart node={node} editing={editing} label={baseLabel(node)} />
|
|
329
647
|
);
|
|
330
648
|
|
|
331
649
|
/** The `box` prototype's body: its centred label. */
|
|
@@ -333,34 +651,6 @@ export const BoxNodeView = LabelNodeView;
|
|
|
333
651
|
|
|
334
652
|
export const EllipseNodeView = LabelNodeView;
|
|
335
653
|
|
|
336
|
-
export const ClassNodeView = ({ node, editing }: NodeViewProps) => {
|
|
337
|
-
if (!isClassNode(node)) {
|
|
338
|
-
return null;
|
|
339
|
-
}
|
|
340
|
-
return (
|
|
341
|
-
<div className={mx('dx-cover flex flex-col font-mono divide-y divide-separator', sizeClass(node, 'text-sm'))}>
|
|
342
|
-
<TextPart part='name' text={node.name} editing={editing} classNames='px-2 py-1 text-center font-bold'>
|
|
343
|
-
{node.name}
|
|
344
|
-
</TextPart>
|
|
345
|
-
<TextPart
|
|
346
|
-
part='attributes'
|
|
347
|
-
text={node.attributes.join('\n')}
|
|
348
|
-
editing={editing}
|
|
349
|
-
classNames='px-2 py-1 flex-1 min-h-4'
|
|
350
|
-
>
|
|
351
|
-
{node.attributes.map((attribute, index) => (
|
|
352
|
-
<div key={index}>{attribute}</div>
|
|
353
|
-
))}
|
|
354
|
-
</TextPart>
|
|
355
|
-
<TextPart part='methods' text={node.methods.join('\n')} editing={editing} classNames='px-2 py-1 flex-1 min-h-4'>
|
|
356
|
-
{node.methods.map((method, index) => (
|
|
357
|
-
<div key={index}>{method}</div>
|
|
358
|
-
))}
|
|
359
|
-
</TextPart>
|
|
360
|
-
</div>
|
|
361
|
-
);
|
|
362
|
-
};
|
|
363
|
-
|
|
364
654
|
/** A node whose type the registry does not know: its frame and type name, so the scene still reads. */
|
|
365
655
|
export const UnknownNodeView = ({ node }: NodeViewProps) => (
|
|
366
656
|
<div className='dx-cover flex items-center justify-center text-xs text-fg-muted'>{node.type}</div>
|
|
@@ -369,25 +659,36 @@ export const UnknownNodeView = ({ node }: NodeViewProps) => (
|
|
|
369
659
|
export const NoteNodeView = ({ node, editing }: NodeViewProps) => {
|
|
370
660
|
const text = isNoteNode(node) ? node.text : '';
|
|
371
661
|
return (
|
|
372
|
-
<TextPart
|
|
662
|
+
<TextPart
|
|
663
|
+
part='text'
|
|
664
|
+
text={text}
|
|
665
|
+
editing={editing}
|
|
666
|
+
classNames={mx(
|
|
667
|
+
TEXT_BOX,
|
|
668
|
+
'whitespace-pre-wrap',
|
|
669
|
+
LINE_ON_GRID,
|
|
670
|
+
alignClasses(node.style, { horizontal: 'left', vertical: 'top' }),
|
|
671
|
+
)}
|
|
672
|
+
>
|
|
373
673
|
{text}
|
|
374
674
|
</TextPart>
|
|
375
675
|
);
|
|
376
676
|
};
|
|
377
677
|
|
|
378
678
|
/**
|
|
379
|
-
* A
|
|
679
|
+
* A frame over a child scene: the centred label, or with `contents` the child drawn inside the frame (a
|
|
380
680
|
* preview, then the live scene as it grows on screen), and a zoom-in control at the top-right.
|
|
381
681
|
*/
|
|
382
|
-
export const
|
|
682
|
+
export const FrameNodeView = (props: NodeViewProps) => {
|
|
383
683
|
const { node, store, registry, zoom, depth, liveDepth, opening, editing, onOpen } = props;
|
|
384
|
-
const child = useAtomValue(store.scene(
|
|
385
|
-
const contents = opening || (
|
|
386
|
-
// Being entered, the portal is already the child scene on the canvas: live
|
|
684
|
+
const child = useAtomValue(store.scene(isFrameNode(node) ? node.scene : ''));
|
|
685
|
+
const contents = opening || (isFrameNode(node) && showsContents(node));
|
|
686
|
+
// Being entered, the portal is already the child scene on the canvas: live.
|
|
387
687
|
const tier = !child ? 'dot' : opening ? 'live' : tierFor(node, zoom, depth, liveDepth);
|
|
388
688
|
const bounds = useMemo(() => (child ? portalFrame(node, contentBounds(child)) : undefined), [node, child]);
|
|
389
|
-
const title = (
|
|
390
|
-
|
|
689
|
+
const title = (isFrameNode(node) ? node.label : undefined) ?? child?.name ?? child?.id ?? '';
|
|
690
|
+
// Too small to show anything inside (or with no child yet), it reads as a closed scene: frame and title.
|
|
691
|
+
if (!contents || tier === 'dot') {
|
|
391
692
|
return (
|
|
392
693
|
<>
|
|
393
694
|
<LabelPart node={node} editing={editing} label={title} />
|
|
@@ -396,10 +697,10 @@ export const PortalNodeView = (props: NodeViewProps) => {
|
|
|
396
697
|
);
|
|
397
698
|
}
|
|
398
699
|
return (
|
|
399
|
-
<div className=
|
|
700
|
+
<div className='dx-cover'>
|
|
400
701
|
{tier === 'preview' && child && (
|
|
401
702
|
<div className='dx-cover flex flex-col items-center justify-center gap-1 pointer-events-none'>
|
|
402
|
-
<span className='text-
|
|
703
|
+
<span className='text-lg'>{title}</span>
|
|
403
704
|
<span>
|
|
404
705
|
{Object.keys(child.nodes).length} nodes · {Object.keys(child.links).length} links
|
|
405
706
|
</span>
|
|
@@ -411,10 +712,16 @@ export const PortalNodeView = (props: NodeViewProps) => {
|
|
|
411
712
|
// The nested layer is read-only: only the root scene receives handlers.
|
|
412
713
|
// Pulled out by the frame's border, so the child's origin is the node's corner as
|
|
413
714
|
// `portalTransform` and `enterPortal` assume; inside the padding box it sat a border in and
|
|
414
|
-
// the scene jumped by that at the drill-in swap.
|
|
715
|
+
// the scene jumped by that at the drill-in swap. The width is the frame's own (`FRAME_BORDER`),
|
|
716
|
+
// since a fading frame trades its border for padding.
|
|
415
717
|
<div
|
|
416
|
-
className='absolute
|
|
417
|
-
style={{
|
|
718
|
+
className='absolute pointer-events-none'
|
|
719
|
+
style={{
|
|
720
|
+
top: `calc(-1 * var(${FRAME_BORDER}, 2px))`,
|
|
721
|
+
left: `calc(-1 * var(${FRAME_BORDER}, 2px))`,
|
|
722
|
+
transform: portalTransform(node, bounds),
|
|
723
|
+
transformOrigin: '0 0',
|
|
724
|
+
}}
|
|
418
725
|
>
|
|
419
726
|
<SceneLayer
|
|
420
727
|
store={store}
|
|
@@ -435,13 +742,15 @@ export const PortalNodeView = (props: NodeViewProps) => {
|
|
|
435
742
|
);
|
|
436
743
|
};
|
|
437
744
|
|
|
438
|
-
|
|
439
|
-
|
|
745
|
+
export type OpenControlProps = { label?: string; onOpen: () => void };
|
|
746
|
+
|
|
747
|
+
/** The open control at a frame's top-right; it takes the press, so it neither drags nor selects the node. */
|
|
748
|
+
export const OpenControl = ({ label = 'Open scene', onOpen }: OpenControlProps) => (
|
|
440
749
|
<Button.Root
|
|
441
750
|
variant='ghost'
|
|
442
751
|
iconOnly
|
|
443
752
|
icon='ph--arrows-out--regular'
|
|
444
|
-
label=
|
|
753
|
+
label={label}
|
|
445
754
|
classNames='absolute top-1 right-1'
|
|
446
755
|
data-testid='portal-open'
|
|
447
756
|
onPointerDown={(event) => event.stopPropagation()}
|