@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
|
@@ -10,39 +10,63 @@
|
|
|
10
10
|
//
|
|
11
11
|
|
|
12
12
|
import { useAtomValue } from '@effect/atom-react/Hooks';
|
|
13
|
+
import type * as Atom from 'effect/reactivity/Atom';
|
|
13
14
|
import type * as Schema from 'effect/Schema';
|
|
14
15
|
import React, { useCallback, useMemo } from 'react';
|
|
15
16
|
|
|
17
|
+
import { type Database } from '@dxos/echo';
|
|
16
18
|
import {
|
|
17
19
|
Form,
|
|
18
20
|
type FormFieldMap,
|
|
19
21
|
type FormFieldOverride,
|
|
20
22
|
type FormFieldRenderer,
|
|
21
23
|
type FormUpdateMeta,
|
|
24
|
+
type RefFieldDataProps,
|
|
22
25
|
} from '@dxos/react-ui-form';
|
|
26
|
+
import * as Button from '@dxos/react-ui/Button';
|
|
23
27
|
import * as Input from '@dxos/react-ui/Input';
|
|
28
|
+
import * as Panel from '@dxos/react-ui/Panel';
|
|
29
|
+
import * as Toolbar from '@dxos/react-ui/Toolbar';
|
|
24
30
|
import type * as Util from '@dxos/react-ui/Util';
|
|
25
|
-
import { mx } from '@dxos/ui-theme';
|
|
26
31
|
|
|
32
|
+
import { useRegistry } from '../../hooks/index.ts';
|
|
33
|
+
import { SCENE_OVERLAY_ATTRIBUTE } from '../../hooks/useWheel.ts';
|
|
27
34
|
import { type SceneViewAtoms } from '../../model/atoms.ts';
|
|
28
35
|
import { nodeDef } from '../../model/node-def.ts';
|
|
29
36
|
import { type Projection } from '../../model/projection.ts';
|
|
30
37
|
import { type NodeRegistry, defaultNodeRegistry } from '../../model/registry.ts';
|
|
38
|
+
import { NO_STYLES } from '../../model/store.ts';
|
|
31
39
|
import {
|
|
40
|
+
BaseNode,
|
|
32
41
|
CurveLink,
|
|
33
42
|
type Element,
|
|
43
|
+
type Layer,
|
|
34
44
|
LineLink,
|
|
35
45
|
type LinkType,
|
|
36
|
-
|
|
46
|
+
type Node,
|
|
47
|
+
type NodeStyle,
|
|
37
48
|
SmartLink,
|
|
38
49
|
SplineLink,
|
|
50
|
+
type StyleClass,
|
|
51
|
+
type StyleMap,
|
|
39
52
|
getElement,
|
|
53
|
+
isFrameNode,
|
|
40
54
|
isLink,
|
|
55
|
+
isNoteNode,
|
|
56
|
+
showsContents,
|
|
41
57
|
} from '../../model/types.ts';
|
|
58
|
+
import { elementLayer, sceneLayers } from '../../utils/layers.ts';
|
|
42
59
|
import { MAX_PORTS_PER_SIDE, portsPerSideOf } from '../../utils/ports.ts';
|
|
43
60
|
import { commonSchema, mergeValues, patchValues } from '../../utils/properties.ts';
|
|
44
|
-
import {
|
|
45
|
-
import {
|
|
61
|
+
import { type SceneOption } from '../../utils/scenes.ts';
|
|
62
|
+
import { flipLink } from '../../utils/shapes.ts';
|
|
63
|
+
import { classedLink, classedNode, resolveStyle, splitClassEdit } from '../../utils/style.ts';
|
|
64
|
+
import { AlignField } from './AlignField.tsx';
|
|
65
|
+
import { StyleClassesContext } from './ClassField.tsx';
|
|
66
|
+
import { FontField } from './FontField.tsx';
|
|
67
|
+
import { LayerClassField, LayersContext } from './LayerField.tsx';
|
|
68
|
+
import { SceneField, SceneOptionsContext } from './SceneField.tsx';
|
|
69
|
+
import { OutlineStyleField, StyleGridField } from './StyleGrid.tsx';
|
|
46
70
|
|
|
47
71
|
/** Identity, ordering and geometry lists are the surface's, not the user's. */
|
|
48
72
|
const HIDDEN = ['id', 'type', 'z', 'ports', 'points', 'source', 'target'];
|
|
@@ -52,7 +76,7 @@ const HIDDEN = ['id', 'type', 'z', 'ports', 'points', 'source', 'target'];
|
|
|
52
76
|
* beats the generic array field's row of inputs. Blank lines survive while typing (they round-trip
|
|
53
77
|
* through split/join) and are dropped on blur, when the form saves.
|
|
54
78
|
*/
|
|
55
|
-
const LinesField: FormFieldRenderer = ({ type, label, jsonPath, readonly, getValue, onValueChange, onBlur }) => {
|
|
79
|
+
export const LinesField: FormFieldRenderer = ({ type, label, jsonPath, readonly, getValue, onValueChange, onBlur }) => {
|
|
56
80
|
const lines: string[] = getValue() ?? [];
|
|
57
81
|
return (
|
|
58
82
|
<Form.Field path={jsonPath} label={label} readonly={readonly}>
|
|
@@ -74,11 +98,13 @@ const LinesField: FormFieldRenderer = ({ type, label, jsonPath, readonly, getVal
|
|
|
74
98
|
);
|
|
75
99
|
};
|
|
76
100
|
|
|
77
|
-
/** Renderers by field path
|
|
101
|
+
/** Renderers by field path the panel always uses (the style grid); node types add their own (`NodeDef.fields`). */
|
|
78
102
|
export const DEFAULT_FIELDS: FormFieldMap = {
|
|
79
|
-
'attributes': LinesField,
|
|
80
|
-
'methods': LinesField,
|
|
81
103
|
'style.hue': StyleGridField,
|
|
104
|
+
'style.alignHorizontal': AlignField,
|
|
105
|
+
'style.fontFamily': FontField,
|
|
106
|
+
'scene': SceneField,
|
|
107
|
+
'layer': LayerClassField,
|
|
82
108
|
};
|
|
83
109
|
|
|
84
110
|
const LINK_SCHEMAS: Record<LinkType, Schema.Codec<any, any>> = {
|
|
@@ -88,18 +114,35 @@ const LINK_SCHEMAS: Record<LinkType, Schema.Codec<any, any>> = {
|
|
|
88
114
|
smart: SmartLink,
|
|
89
115
|
};
|
|
90
116
|
|
|
91
|
-
/** An element's own schema; a
|
|
117
|
+
/** An element's own schema; a type the registry does not know gets the core base's (`BaseNode`, or a line). */
|
|
92
118
|
const schemaOf = (nodes: NodeRegistry, element: Element): Schema.Codec<any, any> =>
|
|
93
|
-
isLink(element) ? LINK_SCHEMAS[element.type] : (nodeDef(nodes, element)?.schema ??
|
|
119
|
+
isLink(element) ? (LINK_SCHEMAS[element.type] ?? LineLink) : (nodeDef(nodes, element)?.schema ?? BaseNode);
|
|
120
|
+
|
|
121
|
+
/** Where a type places its text unless styled otherwise: a note from the top left, a label in the middle. */
|
|
122
|
+
const textAlign = (node: Node): Pick<NodeStyle, 'alignHorizontal' | 'alignVertical'> =>
|
|
123
|
+
isNoteNode(node)
|
|
124
|
+
? { alignHorizontal: 'left', alignVertical: 'top' }
|
|
125
|
+
: { alignHorizontal: 'center', alignVertical: 'middle' };
|
|
94
126
|
|
|
95
127
|
/**
|
|
96
|
-
* What the form shows for an element: what the
|
|
97
|
-
* unset port count as the type's.
|
|
128
|
+
* What the form shows for an element: what the view draws, so its class's look, an unset fill, border or Show
|
|
129
|
+
* contents read as they look and an unset port count as the type's.
|
|
98
130
|
*/
|
|
99
|
-
const formValues = (
|
|
131
|
+
const formValues = (
|
|
132
|
+
nodes: NodeRegistry,
|
|
133
|
+
element: Element,
|
|
134
|
+
styles: StyleMap,
|
|
135
|
+
layers: readonly Layer[],
|
|
136
|
+
): Record<string, unknown> =>
|
|
100
137
|
isLink(element)
|
|
101
|
-
? element
|
|
102
|
-
: {
|
|
138
|
+
? { ...element, layer: elementLayer(element, layers), style: classedLink(element, styles).style }
|
|
139
|
+
: {
|
|
140
|
+
...element,
|
|
141
|
+
layer: elementLayer(element, layers),
|
|
142
|
+
style: { ...textAlign(element), ...resolveStyle(classedNode(element, styles).style) },
|
|
143
|
+
portsPerSide: portsPerSideOf(nodes, element),
|
|
144
|
+
...(isFrameNode(element) ? { contents: showsContents(element) } : {}),
|
|
145
|
+
};
|
|
103
146
|
|
|
104
147
|
/**
|
|
105
148
|
* How the panel presents fields. Ranges are the editor's, not the model's: a check on the stored schema would
|
|
@@ -109,8 +152,11 @@ const formValues = (nodes: NodeRegistry, element: Element): Record<string, unkno
|
|
|
109
152
|
const FIELD_OVERRIDES: Record<string, FormFieldOverride> = {
|
|
110
153
|
'style.hue': { label: 'Style' },
|
|
111
154
|
'style.tone': { hidden: true },
|
|
112
|
-
'style.
|
|
155
|
+
'style.alignVertical': { hidden: true },
|
|
156
|
+
'style.fontSize': { hidden: true },
|
|
113
157
|
'portsPerSide': { min: 1, max: MAX_PORTS_PER_SIDE, step: 1 },
|
|
158
|
+
'scene': { label: 'Scene' },
|
|
159
|
+
'layer': { label: 'Layer' },
|
|
114
160
|
};
|
|
115
161
|
|
|
116
162
|
const plural = (count: number, noun: string) => `${count} ${noun}${count === 1 ? '' : 's'}`;
|
|
@@ -129,6 +175,20 @@ export type PropertiesProps = Util.ThemedClassName<{
|
|
|
129
175
|
fields?: FormFieldMap;
|
|
130
176
|
/** Show the fields without letting them change; also implied by a projection that cannot `update`. */
|
|
131
177
|
readonly?: boolean;
|
|
178
|
+
/** The database a reference field picks from; without it reference fields are read-only. */
|
|
179
|
+
db?: Database.Database;
|
|
180
|
+
/** Narrows a reference field's candidates (e.g. to objects of one kind). */
|
|
181
|
+
getOptions?: RefFieldDataProps['getOptions'];
|
|
182
|
+
/** A host's per-selection field overrides (e.g. a field it allows only in some states), over the panel's own. */
|
|
183
|
+
overrides?: (elements: readonly Element[]) => Record<string, FormFieldOverride>;
|
|
184
|
+
/** The scenes a scene shape may open (`sceneOptions`); without them its scene is not editable here. */
|
|
185
|
+
sceneOptions?: readonly SceneOption[];
|
|
186
|
+
/** The drawing's style classes (`SceneStore.styles`); without them elements take no class. */
|
|
187
|
+
styles?: Atom.Writable<StyleMap>;
|
|
188
|
+
/** Moves the selection into a new scene (`groupIntoScene`); offered for a selection of several elements. */
|
|
189
|
+
onGroup?: () => void;
|
|
190
|
+
/** Whether the panel is docked beside the canvas. */
|
|
191
|
+
docked?: boolean;
|
|
132
192
|
}>;
|
|
133
193
|
|
|
134
194
|
export const Properties = ({
|
|
@@ -136,83 +196,203 @@ export const Properties = ({
|
|
|
136
196
|
projection,
|
|
137
197
|
atoms,
|
|
138
198
|
nodes = defaultNodeRegistry,
|
|
139
|
-
fields
|
|
199
|
+
fields,
|
|
140
200
|
readonly: readonlyProp = false,
|
|
201
|
+
db,
|
|
202
|
+
getOptions,
|
|
203
|
+
overrides,
|
|
204
|
+
sceneOptions,
|
|
205
|
+
styles,
|
|
206
|
+
onGroup,
|
|
207
|
+
docked = false,
|
|
141
208
|
}: PropertiesProps) => {
|
|
209
|
+
const registry = useRegistry();
|
|
142
210
|
const scene = useAtomValue(projection.scene);
|
|
143
211
|
const selection = useAtomValue(atoms.selection);
|
|
144
212
|
const elements = useMemo(() => [...selection].flatMap((id) => getElement(scene, id) ?? []), [scene, selection]);
|
|
145
213
|
const readonly = readonlyProp || !projection.capabilities.update;
|
|
214
|
+
const styleMap = useAtomValue(styles ?? NO_STYLES);
|
|
215
|
+
const layers = useMemo(() => sceneLayers(scene), [scene]);
|
|
146
216
|
|
|
147
217
|
// Keyed by the selected types, so the schema (and the form built on it) holds while values change under it.
|
|
148
218
|
const typesKey = [...new Set(elements.map((element) => element.type))].sort().join();
|
|
149
219
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
150
220
|
const schema = useMemo(() => commonSchema(elements.map((element) => schemaOf(nodes, element))), [typesKey, nodes]);
|
|
151
221
|
const { values, fieldOverrides } = useMemo(() => {
|
|
152
|
-
const shown = elements.map((element) => formValues(nodes, element));
|
|
222
|
+
const shown = elements.map((element) => formValues(nodes, element, styleMap, layers));
|
|
153
223
|
const { values, mixed } = mergeValues(shown, Object.keys(shown[0] ?? {}));
|
|
154
224
|
// A value the elements disagree on shows as indeterminate until it is edited, then applies to all of them.
|
|
155
|
-
const fieldOverrides: Record<string, FormFieldOverride> = {
|
|
225
|
+
const fieldOverrides: Record<string, FormFieldOverride> = {
|
|
226
|
+
...FIELD_OVERRIDES,
|
|
227
|
+
...(sceneOptions ? {} : { scene: { hidden: true } }),
|
|
228
|
+
// The class is edited in the layer's row (`LayerClassField`).
|
|
229
|
+
class: { hidden: true },
|
|
230
|
+
...overrides?.(elements),
|
|
231
|
+
};
|
|
156
232
|
for (const path of mixed) {
|
|
157
233
|
fieldOverrides[path] = { ...fieldOverrides[path], indeterminate: true };
|
|
158
234
|
}
|
|
159
235
|
return { values, fieldOverrides };
|
|
160
|
-
}, [elements, nodes]);
|
|
236
|
+
}, [elements, nodes, overrides, sceneOptions, styles, styleMap, layers]);
|
|
161
237
|
|
|
162
238
|
const onSave = useCallback(
|
|
163
239
|
(values: Record<string, unknown>, { changed }: FormUpdateMeta<Record<string, unknown>>) => {
|
|
164
240
|
const paths = Object.keys(changed).filter((path) => changed[path as keyof typeof changed]);
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
241
|
+
// A classed element's look is its class's: an edit of it restyles the class, and so every element deriving it.
|
|
242
|
+
const classes: Record<string, StyleClass> = {};
|
|
243
|
+
const intents = elements.map((element) => {
|
|
244
|
+
const shown = formValues(nodes, element, styleMap, layers);
|
|
245
|
+
const patch = patchValues(shown, values, paths);
|
|
246
|
+
const styleClass = styles && element.class ? (classes[element.class] ?? styleMap[element.class]) : undefined;
|
|
247
|
+
// Taking a class drops the element's own look, so it derives the class's whole rather than in part.
|
|
248
|
+
if (typeof patch.class === 'string') {
|
|
249
|
+
return { kind: 'update' as const, id: element.id, values: { ...patch, style: undefined } };
|
|
250
|
+
}
|
|
251
|
+
if (!styleClass || !('style' in patch)) {
|
|
252
|
+
return { kind: 'update' as const, id: element.id, values: patch };
|
|
253
|
+
}
|
|
254
|
+
// A link's look is the common base of its class's style; a node's, the whole of it.
|
|
255
|
+
const look = isLink(element)
|
|
256
|
+
? classedLink(element, styleMap).style
|
|
257
|
+
: resolveStyle(classedNode(element, styleMap).style);
|
|
258
|
+
const split = splitClassEdit(look, patch.style, styleClass.style ?? {}, element.style);
|
|
259
|
+
classes[styleClass.id] = { ...styleClass, style: split.classLook };
|
|
260
|
+
return { kind: 'update' as const, id: element.id, values: { ...patch, style: split.own } };
|
|
172
261
|
});
|
|
262
|
+
if (styles && Object.keys(classes).length > 0) {
|
|
263
|
+
registry.set(styles, { ...styleMap, ...classes });
|
|
264
|
+
}
|
|
265
|
+
projection.apply({ kind: 'batch', intents });
|
|
173
266
|
},
|
|
174
|
-
[projection, elements, nodes],
|
|
267
|
+
[projection, elements, nodes, styles, styleMap, layers, registry],
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
// A new class takes the selected element's look, and the element then follows it rather than carrying its own.
|
|
271
|
+
const single = elements.length === 1 ? elements[0] : undefined;
|
|
272
|
+
const onCreateClass = useCallback(() => {
|
|
273
|
+
if (!styles || !single) {
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
const id = `class-${crypto.randomUUID()}`;
|
|
277
|
+
const name = `Class ${Object.keys(styleMap).length + 1}`;
|
|
278
|
+
const style = isLink(single) ? classedLink(single, styleMap).style : classedNode(single, styleMap).style;
|
|
279
|
+
const styleClass: StyleClass = { id, name, ...(style ? { style } : {}) };
|
|
280
|
+
registry.set(styles, { ...styleMap, [id]: styleClass });
|
|
281
|
+
projection.apply({ kind: 'update', id: single.id, values: { class: id, style: undefined } });
|
|
282
|
+
}, [styles, single, styleMap, registry, projection]);
|
|
283
|
+
|
|
284
|
+
// Reverses every selected link (source and target swap; an arrow comes to point the other way), as one batch.
|
|
285
|
+
const links = useMemo(() => elements.filter(isLink), [elements]);
|
|
286
|
+
const onFlip = useCallback(() => {
|
|
287
|
+
projection.apply({
|
|
288
|
+
kind: 'batch',
|
|
289
|
+
intents: links.map((link) => ({ kind: 'update' as const, id: link.id, values: flipLink(link) })),
|
|
290
|
+
});
|
|
291
|
+
// A spline's control points run the other way after a flip, so a selected point's index names another one.
|
|
292
|
+
registry.set(atoms.point, undefined);
|
|
293
|
+
}, [projection, links, registry, atoms.point]);
|
|
294
|
+
|
|
295
|
+
// The selected node types' own renderers over the panel's; a host's `fields` win over both. One style picker for
|
|
296
|
+
// every kind: its fill rows only when every element is a node, since a link has a colour but no fill.
|
|
297
|
+
const fieldMap = useMemo(
|
|
298
|
+
() => ({
|
|
299
|
+
...DEFAULT_FIELDS,
|
|
300
|
+
...(elements.some(isLink) ? { 'style.hue': OutlineStyleField } : {}),
|
|
301
|
+
...Object.assign(
|
|
302
|
+
{},
|
|
303
|
+
...elements.map((element) => (isLink(element) ? {} : (nodeDef(nodes, element)?.fields ?? {}))),
|
|
304
|
+
),
|
|
305
|
+
...fields,
|
|
306
|
+
}),
|
|
307
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
308
|
+
[typesKey, nodes, fields],
|
|
175
309
|
);
|
|
176
310
|
|
|
177
311
|
if (elements.length === 0) {
|
|
178
312
|
return (
|
|
179
|
-
<
|
|
180
|
-
<
|
|
181
|
-
|
|
313
|
+
<Panel.Root classNames={classNames} data-testid='properties' {...{ [SCENE_OVERLAY_ATTRIBUTE]: true }}>
|
|
314
|
+
<Panel.Body>
|
|
315
|
+
<p className='p-2 text-sm text-fg-muted'>Select a node or link to edit its properties.</p>
|
|
316
|
+
</Panel.Body>
|
|
317
|
+
</Panel.Root>
|
|
182
318
|
);
|
|
183
319
|
}
|
|
184
320
|
|
|
185
321
|
const summary = elements.length > 1 && `${describeSelection(elements)}${schema ? '' : ' — no shared properties'}`;
|
|
186
322
|
return (
|
|
187
|
-
<
|
|
188
|
-
|
|
189
|
-
<
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
323
|
+
<Panel.Root classNames={classNames} data-testid='properties' {...{ [SCENE_OVERLAY_ATTRIBUTE]: true }}>
|
|
324
|
+
<Panel.Header asChild>
|
|
325
|
+
<Toolbar.Root data-testid='properties-toolbar'>
|
|
326
|
+
{onGroup && elements.length > 1 && elements.some((element) => !isLink(element)) && (
|
|
327
|
+
<Button.Root
|
|
328
|
+
variant='ghost'
|
|
329
|
+
iconOnly
|
|
330
|
+
icon='ph--frame-corners--regular'
|
|
331
|
+
label='New scene from selection'
|
|
332
|
+
disabled={readonly}
|
|
333
|
+
data-testid='properties-group'
|
|
334
|
+
onClick={onGroup}
|
|
335
|
+
/>
|
|
336
|
+
)}
|
|
337
|
+
{styles && single && (
|
|
338
|
+
<Button.Root
|
|
339
|
+
variant='ghost'
|
|
340
|
+
iconOnly
|
|
341
|
+
icon='ph--swatches--regular'
|
|
342
|
+
label='New class from selection'
|
|
343
|
+
disabled={readonly}
|
|
344
|
+
data-testid='properties-new-class'
|
|
345
|
+
onClick={onCreateClass}
|
|
346
|
+
/>
|
|
347
|
+
)}
|
|
348
|
+
{links.length > 0 && links.length === elements.length && (
|
|
349
|
+
<Button.Root
|
|
350
|
+
variant='ghost'
|
|
351
|
+
iconOnly
|
|
352
|
+
icon='ph--arrows-left-right--regular'
|
|
353
|
+
label='Flip direction'
|
|
354
|
+
disabled={readonly}
|
|
355
|
+
data-testid='properties-flip'
|
|
356
|
+
onClick={onFlip}
|
|
357
|
+
/>
|
|
358
|
+
)}
|
|
359
|
+
</Toolbar.Root>
|
|
360
|
+
</Panel.Header>
|
|
361
|
+
<Panel.Body>
|
|
362
|
+
{schema && (
|
|
363
|
+
<LayersContext.Provider value={layers}>
|
|
364
|
+
<StyleClassesContext.Provider value={styles ? styleMap : undefined}>
|
|
365
|
+
<SceneOptionsContext.Provider value={sceneOptions ?? []}>
|
|
366
|
+
<Form.Root
|
|
367
|
+
key={[...selection].join()}
|
|
368
|
+
schema={schema}
|
|
369
|
+
values={values}
|
|
370
|
+
fieldOverrides={fieldOverrides}
|
|
371
|
+
fieldMap={fieldMap}
|
|
372
|
+
db={db}
|
|
373
|
+
getOptions={getOptions}
|
|
374
|
+
readonly={readonly}
|
|
375
|
+
autoSave
|
|
376
|
+
onSave={onSave}
|
|
377
|
+
>
|
|
378
|
+
{/* Floating, the panel is as tall as its host and a long form scrolls inside it; docked, the dock scrolls
|
|
379
|
+
the panels together, so the form keeps no scroll of its own (it would hold the wheel). */}
|
|
380
|
+
<Form.Viewport scroll={!docked}>
|
|
381
|
+
<Form.Content>
|
|
382
|
+
<Form.Fields exclude={HIDDEN} />
|
|
383
|
+
</Form.Content>
|
|
384
|
+
</Form.Viewport>
|
|
385
|
+
</Form.Root>
|
|
386
|
+
</SceneOptionsContext.Provider>
|
|
387
|
+
</StyleClassesContext.Provider>
|
|
388
|
+
</LayersContext.Provider>
|
|
389
|
+
)}
|
|
390
|
+
</Panel.Body>
|
|
391
|
+
<Panel.Footer classNames='text-center'>
|
|
392
|
+
<span className='text-sm text-fg-muted' data-testid='properties-summary'>
|
|
213
393
|
{summary}
|
|
214
|
-
</
|
|
215
|
-
|
|
216
|
-
</
|
|
394
|
+
</span>
|
|
395
|
+
</Panel.Footer>
|
|
396
|
+
</Panel.Root>
|
|
217
397
|
);
|
|
218
398
|
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import React, { createContext, useContext } from 'react';
|
|
6
|
+
|
|
7
|
+
import { Form, type FormFieldRenderer, SelectField } from '@dxos/react-ui-form';
|
|
8
|
+
|
|
9
|
+
import { type SceneOption } from '../../utils/scenes.ts';
|
|
10
|
+
|
|
11
|
+
/** The scenes a selected scene shape may open, as the panel's host lists them (`sceneOptions`). */
|
|
12
|
+
export const SceneOptionsContext = createContext<readonly SceneOption[]>([]);
|
|
13
|
+
|
|
14
|
+
/** A scene shape's `scene`: which scene of this drawing it opens, so several shapes may open one scene. */
|
|
15
|
+
export const SceneField: FormFieldRenderer = (props) => {
|
|
16
|
+
const options = useContext(SceneOptionsContext);
|
|
17
|
+
return (
|
|
18
|
+
<Form.Field path={props.jsonPath} label={props.label} readonly={props.readonly}>
|
|
19
|
+
<SelectField {...props} options={[...options]} />
|
|
20
|
+
</Form.Field>
|
|
21
|
+
);
|
|
22
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
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
|
+
|
|
8
|
+
import { JsonHighlighter } from '@dxos/react-ui-syntax-highlighter';
|
|
9
|
+
import { withLayout, withTheme } from '@dxos/react-ui/testing';
|
|
10
|
+
|
|
11
|
+
import { type NodeTone, type StyleHue } from '../../model/types.ts';
|
|
12
|
+
import { TONE_NAMES } from '../../utils/style.ts';
|
|
13
|
+
import { StyleGrid, type StyleGridProps } from './StyleGrid.tsx';
|
|
14
|
+
|
|
15
|
+
type StoryArgs = Pick<StyleGridProps, 'indeterminate' | 'readonly'>;
|
|
16
|
+
|
|
17
|
+
/** The grid alone, holding its own pick: each swatch is a hue at a tone, as a node would draw it. */
|
|
18
|
+
const DefaultStory = ({ indeterminate, readonly }: StoryArgs) => {
|
|
19
|
+
const [hue, setHue] = useState<StyleHue>('blue');
|
|
20
|
+
const [tone, setTone] = useState<NodeTone>(2);
|
|
21
|
+
return (
|
|
22
|
+
<div className='grid grid-cols-2 gap-4 w-[48rem]'>
|
|
23
|
+
<StyleGrid
|
|
24
|
+
hue={hue}
|
|
25
|
+
tone={tone}
|
|
26
|
+
indeterminate={indeterminate}
|
|
27
|
+
readonly={readonly}
|
|
28
|
+
onValueChange={(choice) => {
|
|
29
|
+
setHue(choice.hue);
|
|
30
|
+
setTone(choice.tone);
|
|
31
|
+
}}
|
|
32
|
+
/>
|
|
33
|
+
<JsonHighlighter data={{ hue, tone, name: TONE_NAMES[tone] }} />
|
|
34
|
+
</div>
|
|
35
|
+
);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const meta: Meta<StoryArgs> = {
|
|
39
|
+
title: 'ui/react-ui-canvas/StyleGrid',
|
|
40
|
+
render: DefaultStory,
|
|
41
|
+
decorators: [withTheme(), withLayout({ layout: 'centered' })],
|
|
42
|
+
args: {
|
|
43
|
+
indeterminate: false,
|
|
44
|
+
readonly: false,
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export default meta;
|
|
49
|
+
|
|
50
|
+
export const Default: StoryObj<StoryArgs> = {};
|
|
@@ -8,8 +8,8 @@ import { Form, type FormFieldRenderer, useFormFieldState } from '@dxos/react-ui-
|
|
|
8
8
|
import * as ToggleGroup from '@dxos/react-ui/ToggleGroup';
|
|
9
9
|
import { mx } from '@dxos/ui-theme';
|
|
10
10
|
|
|
11
|
-
import { NodeStyle, type NodeTone } from '../../model/types.ts';
|
|
12
|
-
import { DEFAULT_TONE,
|
|
11
|
+
import { NodeStyle, type NodeTone, STYLE_HUES, type StyleHue } from '../../model/types.ts';
|
|
12
|
+
import { DEFAULT_TONE, TONE_NAMES, TONES, hueClasses } from '../../utils/style.ts';
|
|
13
13
|
|
|
14
14
|
export type StyleChoice = { hue: StyleHue; tone: NodeTone };
|
|
15
15
|
|
|
@@ -36,6 +36,8 @@ export type StyleGridProps = {
|
|
|
36
36
|
/** Several nodes disagree: no swatch is selected until one is picked. */
|
|
37
37
|
indeterminate?: boolean;
|
|
38
38
|
readonly?: boolean;
|
|
39
|
+
/** The rows offered; all tones by default, or one row (e.g. a line's outline colour only). */
|
|
40
|
+
tones?: readonly NodeTone[];
|
|
39
41
|
onValueChange: (choice: StyleChoice) => void;
|
|
40
42
|
};
|
|
41
43
|
|
|
@@ -43,7 +45,14 @@ export type StyleGridProps = {
|
|
|
43
45
|
* Each offered hue in a column and its tones down the rows, every swatch drawn with the classes the node
|
|
44
46
|
* itself would take, so a pick is a preview. A hue the grid does not offer, or none, selects nothing.
|
|
45
47
|
*/
|
|
46
|
-
export const StyleGrid = ({
|
|
48
|
+
export const StyleGrid = ({
|
|
49
|
+
hue,
|
|
50
|
+
tone = DEFAULT_TONE,
|
|
51
|
+
indeterminate,
|
|
52
|
+
readonly,
|
|
53
|
+
tones = TONES,
|
|
54
|
+
onValueChange,
|
|
55
|
+
}: StyleGridProps) => {
|
|
47
56
|
const selected = indeterminate || !hue ? '' : `${hue}${SEPARATOR}${tone}`;
|
|
48
57
|
return (
|
|
49
58
|
<ToggleGroup.Root
|
|
@@ -60,7 +69,7 @@ export const StyleGrid = ({ hue, tone = DEFAULT_TONE, indeterminate, readonly, o
|
|
|
60
69
|
}
|
|
61
70
|
}}
|
|
62
71
|
>
|
|
63
|
-
{
|
|
72
|
+
{tones.flatMap((rowTone) =>
|
|
64
73
|
STYLE_HUES.map((columnHue) => {
|
|
65
74
|
const key = choiceKey({ hue: columnHue, tone: rowTone });
|
|
66
75
|
const classes = hueClasses(columnHue, rowTone);
|
|
@@ -91,28 +100,47 @@ export const StyleGrid = ({ hue, tone = DEFAULT_TONE, indeterminate, readonly, o
|
|
|
91
100
|
);
|
|
92
101
|
};
|
|
93
102
|
|
|
103
|
+
/** The outline row alone: a link takes a hue but no fill, so its colour is the hue's border. */
|
|
104
|
+
const OUTLINE_TONES: readonly NodeTone[] = [0];
|
|
105
|
+
|
|
106
|
+
export type StyleFieldOptions = {
|
|
107
|
+
/** Offer the fill rows (tones 1–3) as well as the outline; a link, or a selection with one, has no fill. */
|
|
108
|
+
fills: boolean;
|
|
109
|
+
};
|
|
110
|
+
|
|
94
111
|
/**
|
|
95
|
-
* The properties panel's style
|
|
96
|
-
*
|
|
112
|
+
* The properties panel's style picker, rendered at `style.hue`, for every kind of element: with `fills`, one pick
|
|
113
|
+
* sets the hue and its tone together (a node); without, the outline row sets the hue alone (a link, or a
|
|
114
|
+
* selection mixing kinds, which edits only the style they share).
|
|
97
115
|
*/
|
|
98
|
-
export const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
<
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
116
|
+
export const createStyleField = ({ fills }: StyleFieldOptions): FormFieldRenderer => {
|
|
117
|
+
const StyleField: FormFieldRenderer = ({ label, jsonPath, readonly, indeterminate, getValue, onBlur }) => {
|
|
118
|
+
// Hue and tone change together, so the pick writes their parent once; two field writes would race.
|
|
119
|
+
const stylePath = (jsonPath ?? 'style.hue').split('.').slice(0, -1);
|
|
120
|
+
const styleField = useFormFieldState('StyleField', stylePath);
|
|
121
|
+
const style: NodeStyle = styleField.getValue() ?? {};
|
|
122
|
+
return (
|
|
123
|
+
<Form.Field path={jsonPath} label={label} readonly={readonly}>
|
|
124
|
+
<StyleGrid
|
|
125
|
+
hue={getValue()}
|
|
126
|
+
tone={fills ? style.tone : 0}
|
|
127
|
+
tones={fills ? TONES : OUTLINE_TONES}
|
|
128
|
+
indeterminate={indeterminate}
|
|
129
|
+
readonly={!!readonly}
|
|
130
|
+
onValueChange={({ hue, tone }) => {
|
|
131
|
+
// A pick is a commit: the grid never blurs, so it commits itself.
|
|
132
|
+
styleField.onValueChange(NodeStyle.ast, fills ? { ...style, hue, tone } : { ...style, hue });
|
|
133
|
+
onBlur();
|
|
134
|
+
}}
|
|
135
|
+
/>
|
|
136
|
+
</Form.Field>
|
|
137
|
+
);
|
|
138
|
+
};
|
|
139
|
+
return StyleField;
|
|
118
140
|
};
|
|
141
|
+
|
|
142
|
+
/** The style picker with its fill rows: a node's, or a selection of nodes. */
|
|
143
|
+
export const StyleGridField = createStyleField({ fills: true });
|
|
144
|
+
|
|
145
|
+
/** The style picker's outline row alone: a link's, or a selection with a link in it. */
|
|
146
|
+
export const OutlineStyleField = createStyleField({ fills: false });
|