@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.
Files changed (341) hide show
  1. package/README.md +1 -1
  2. package/dist/lib/index.mjs +9044 -1135
  3. package/dist/lib/index.mjs.map +1 -1
  4. package/dist/types/src/components/About/About.d.ts +13 -0
  5. package/dist/types/src/components/About/About.d.ts.map +1 -0
  6. package/dist/types/src/components/About/About.stories.d.ts +7 -0
  7. package/dist/types/src/components/About/About.stories.d.ts.map +1 -0
  8. package/dist/types/src/components/About/index.d.ts +2 -0
  9. package/dist/types/src/components/About/index.d.ts.map +1 -0
  10. package/dist/types/src/components/Breadcrumbs/Breadcrumbs.d.ts.map +1 -1
  11. package/dist/types/src/components/ControlFrame/ControlFrame.d.ts +8 -2
  12. package/dist/types/src/components/ControlFrame/ControlFrame.d.ts.map +1 -1
  13. package/dist/types/src/components/Layers/Layers.d.ts +30 -0
  14. package/dist/types/src/components/Layers/Layers.d.ts.map +1 -0
  15. package/dist/types/src/components/Layers/Layers.stories.d.ts +11 -0
  16. package/dist/types/src/components/Layers/Layers.stories.d.ts.map +1 -0
  17. package/dist/types/src/components/Layers/index.d.ts +2 -0
  18. package/dist/types/src/components/Layers/index.d.ts.map +1 -0
  19. package/dist/types/src/components/Palette/Palette.d.ts +20 -4
  20. package/dist/types/src/components/Palette/Palette.d.ts.map +1 -1
  21. package/dist/types/src/components/Palette/Palette.stories.d.ts +22 -0
  22. package/dist/types/src/components/Palette/Palette.stories.d.ts.map +1 -0
  23. package/dist/types/src/components/PartEditor/PartEditor.d.ts.map +1 -1
  24. package/dist/types/src/components/Properties/AlignField.d.ts +23 -0
  25. package/dist/types/src/components/Properties/AlignField.d.ts.map +1 -0
  26. package/dist/types/src/components/Properties/ClassField.d.ts +23 -0
  27. package/dist/types/src/components/Properties/ClassField.d.ts.map +1 -0
  28. package/dist/types/src/components/Properties/FontField.d.ts +7 -0
  29. package/dist/types/src/components/Properties/FontField.d.ts.map +1 -0
  30. package/dist/types/src/components/Properties/LayerField.d.ts +17 -0
  31. package/dist/types/src/components/Properties/LayerField.d.ts.map +1 -0
  32. package/dist/types/src/components/Properties/Properties.d.ts +27 -3
  33. package/dist/types/src/components/Properties/Properties.d.ts.map +1 -1
  34. package/dist/types/src/components/Properties/Properties.stories.d.ts +20 -0
  35. package/dist/types/src/components/Properties/Properties.stories.d.ts.map +1 -0
  36. package/dist/types/src/components/Properties/SceneField.d.ts +8 -0
  37. package/dist/types/src/components/Properties/SceneField.d.ts.map +1 -0
  38. package/dist/types/src/components/Properties/StyleGrid.d.ts +15 -5
  39. package/dist/types/src/components/Properties/StyleGrid.d.ts.map +1 -1
  40. package/dist/types/src/components/Properties/StyleGrid.stories.d.ts +7 -0
  41. package/dist/types/src/components/Properties/StyleGrid.stories.d.ts.map +1 -0
  42. package/dist/types/src/components/SceneLayer/SceneLayer.d.ts +31 -7
  43. package/dist/types/src/components/SceneLayer/SceneLayer.d.ts.map +1 -1
  44. package/dist/types/src/components/SceneView/Dock.d.ts +22 -0
  45. package/dist/types/src/components/SceneView/Dock.d.ts.map +1 -0
  46. package/dist/types/src/components/SceneView/Dock.stories.d.ts +15 -0
  47. package/dist/types/src/components/SceneView/Dock.stories.d.ts.map +1 -0
  48. package/dist/types/src/components/SceneView/SceneView.d.ts +33 -7
  49. package/dist/types/src/components/SceneView/SceneView.d.ts.map +1 -1
  50. package/dist/types/src/components/SceneView/SceneView.stories.d.ts +16 -6
  51. package/dist/types/src/components/SceneView/SceneView.stories.d.ts.map +1 -1
  52. package/dist/types/src/components/SceneView/SceneViewContext.d.ts +36 -4
  53. package/dist/types/src/components/SceneView/SceneViewContext.d.ts.map +1 -1
  54. package/dist/types/src/components/SceneView/Scored.stories.d.ts +4 -4
  55. package/dist/types/src/components/SceneView/Scored.stories.d.ts.map +1 -1
  56. package/dist/types/src/components/SceneView/usePointerMachine.d.ts +3 -1
  57. package/dist/types/src/components/SceneView/usePointerMachine.d.ts.map +1 -1
  58. package/dist/types/src/components/SceneView/useSceneCamera.d.ts +2 -2
  59. package/dist/types/src/components/SceneView/useSceneCamera.d.ts.map +1 -1
  60. package/dist/types/src/components/SceneView/useSceneClipboard.d.ts.map +1 -1
  61. package/dist/types/src/components/SceneView/useSceneKeys.d.ts.map +1 -1
  62. package/dist/types/src/components/SceneView/useSceneNavigation.d.ts +7 -4
  63. package/dist/types/src/components/SceneView/useSceneNavigation.d.ts.map +1 -1
  64. package/dist/types/src/components/Toolbar/Toolbar.d.ts +2 -0
  65. package/dist/types/src/components/Toolbar/Toolbar.d.ts.map +1 -1
  66. package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts.map +1 -1
  67. package/dist/types/src/components/index.d.ts +2 -0
  68. package/dist/types/src/components/index.d.ts.map +1 -1
  69. package/dist/types/src/hooks/useWheel.d.ts +5 -0
  70. package/dist/types/src/hooks/useWheel.d.ts.map +1 -1
  71. package/dist/types/src/index.d.ts +4 -1
  72. package/dist/types/src/index.d.ts.map +1 -1
  73. package/dist/types/src/model/atoms.d.ts +21 -3
  74. package/dist/types/src/model/atoms.d.ts.map +1 -1
  75. package/dist/types/src/model/projection.d.ts.map +1 -1
  76. package/dist/types/src/model/projections/constrained.d.ts +2 -1
  77. package/dist/types/src/model/projections/constrained.d.ts.map +1 -1
  78. package/dist/types/src/model/projections/dynamic.d.ts.map +1 -1
  79. package/dist/types/src/model/projections/lattice.d.ts.map +1 -1
  80. package/dist/types/src/model/registry.d.ts +21 -3
  81. package/dist/types/src/model/registry.d.ts.map +1 -1
  82. package/dist/types/src/model/store.d.ts +10 -2
  83. package/dist/types/src/model/store.d.ts.map +1 -1
  84. package/dist/types/src/model/types.d.ts +699 -338
  85. package/dist/types/src/model/types.d.ts.map +1 -1
  86. package/dist/types/src/playwright/SceneManager.d.ts +1 -1
  87. package/dist/types/src/playwright/SceneManager.d.ts.map +1 -1
  88. package/dist/types/src/utils/builder.d.ts +22 -41
  89. package/dist/types/src/utils/builder.d.ts.map +1 -1
  90. package/dist/types/src/utils/camera.d.ts +19 -10
  91. package/dist/types/src/utils/camera.d.ts.map +1 -1
  92. package/dist/types/src/utils/clipboard.d.ts +4 -2
  93. package/dist/types/src/utils/clipboard.d.ts.map +1 -1
  94. package/dist/types/src/utils/diagram.d.ts.map +1 -1
  95. package/dist/types/src/utils/group.d.ts +14 -0
  96. package/dist/types/src/utils/group.d.ts.map +1 -0
  97. package/dist/types/src/utils/group.test.d.ts +2 -0
  98. package/dist/types/src/utils/group.test.d.ts.map +1 -0
  99. package/dist/types/src/utils/gutter-route.d.ts +7 -0
  100. package/dist/types/src/utils/gutter-route.d.ts.map +1 -1
  101. package/dist/types/src/utils/hit.d.ts +3 -20
  102. package/dist/types/src/utils/hit.d.ts.map +1 -1
  103. package/dist/types/src/utils/index.d.ts +3 -0
  104. package/dist/types/src/utils/index.d.ts.map +1 -1
  105. package/dist/types/src/utils/lattice.d.ts +5 -0
  106. package/dist/types/src/utils/lattice.d.ts.map +1 -1
  107. package/dist/types/src/utils/layers.d.ts +35 -0
  108. package/dist/types/src/utils/layers.d.ts.map +1 -0
  109. package/dist/types/src/utils/layers.test.d.ts +2 -0
  110. package/dist/types/src/utils/layers.test.d.ts.map +1 -0
  111. package/dist/types/src/utils/parts.d.ts +25 -9
  112. package/dist/types/src/utils/parts.d.ts.map +1 -1
  113. package/dist/types/src/utils/ports.d.ts +2 -0
  114. package/dist/types/src/utils/ports.d.ts.map +1 -1
  115. package/dist/types/src/utils/properties.d.ts +4 -3
  116. package/dist/types/src/utils/properties.d.ts.map +1 -1
  117. package/dist/types/src/utils/route.d.ts +5 -0
  118. package/dist/types/src/utils/route.d.ts.map +1 -1
  119. package/dist/types/src/utils/scenes.d.ts +14 -0
  120. package/dist/types/src/utils/scenes.d.ts.map +1 -0
  121. package/dist/types/src/utils/scenes.test.d.ts +2 -0
  122. package/dist/types/src/utils/scenes.test.d.ts.map +1 -0
  123. package/dist/types/src/utils/shapes.d.ts +23 -9
  124. package/dist/types/src/utils/shapes.d.ts.map +1 -1
  125. package/dist/types/src/utils/style.d.ts +29 -6
  126. package/dist/types/src/utils/style.d.ts.map +1 -1
  127. package/dist/types/src/utils/style.test.d.ts +2 -0
  128. package/dist/types/src/utils/style.test.d.ts.map +1 -0
  129. package/dist/types/src/utils/testing.d.ts +2 -2
  130. package/dist/types/src/utils/testing.d.ts.map +1 -1
  131. package/dist/types/src/utils/undo.d.ts +15 -4
  132. package/dist/types/src/utils/undo.d.ts.map +1 -1
  133. package/dist/types/tsconfig.tsbuildinfo +1 -1
  134. package/package.json +20 -26
  135. package/src/components/About/About.stories.tsx +32 -0
  136. package/src/components/About/About.tsx +38 -0
  137. package/src/components/About/index.ts +5 -0
  138. package/src/components/Breadcrumbs/Breadcrumbs.tsx +17 -3
  139. package/src/components/ControlFrame/ControlFrame.tsx +138 -82
  140. package/src/components/Grid/Grid.stories.tsx +1 -1
  141. package/src/components/Layers/Layers.stories.tsx +131 -0
  142. package/src/components/Layers/Layers.tsx +216 -0
  143. package/src/components/Layers/index.ts +5 -0
  144. package/src/components/Palette/Palette.stories.tsx +135 -0
  145. package/src/components/Palette/Palette.tsx +230 -39
  146. package/src/components/PartEditor/PartEditor.tsx +4 -3
  147. package/src/components/Properties/AlignField.tsx +117 -0
  148. package/src/components/Properties/ClassField.tsx +44 -0
  149. package/src/components/Properties/FontField.tsx +73 -0
  150. package/src/components/Properties/LayerField.tsx +61 -0
  151. package/src/components/Properties/Properties.stories.tsx +113 -0
  152. package/src/components/Properties/Properties.tsx +240 -60
  153. package/src/components/Properties/SceneField.tsx +22 -0
  154. package/src/components/Properties/StyleGrid.stories.tsx +50 -0
  155. package/src/components/Properties/StyleGrid.tsx +54 -26
  156. package/src/components/SceneLayer/SceneLayer.tsx +497 -188
  157. package/src/components/SceneView/Constrained.stories.tsx +1 -1
  158. package/src/components/SceneView/Dock.stories.tsx +95 -0
  159. package/src/components/SceneView/Dock.tsx +146 -0
  160. package/src/components/SceneView/Dynamic.stories.tsx +1 -1
  161. package/src/components/SceneView/SceneView.stories.tsx +170 -18
  162. package/src/components/SceneView/SceneView.tsx +565 -151
  163. package/src/components/SceneView/SceneViewContext.ts +23 -2
  164. package/src/components/SceneView/Scored.stories.tsx +11 -11
  165. package/src/components/SceneView/usePointerMachine.ts +80 -20
  166. package/src/components/SceneView/useSceneCamera.ts +15 -8
  167. package/src/components/SceneView/useSceneClipboard.ts +4 -0
  168. package/src/components/SceneView/useSceneKeys.ts +6 -4
  169. package/src/components/SceneView/useSceneNavigation.ts +44 -33
  170. package/src/components/Toolbar/Toolbar.stories.tsx +14 -15
  171. package/src/components/Toolbar/Toolbar.tsx +15 -4
  172. package/src/components/index.ts +2 -0
  173. package/src/hooks/useWheel.ts +35 -0
  174. package/src/index.ts +4 -3
  175. package/src/model/atoms.ts +27 -3
  176. package/src/model/projection.ts +5 -0
  177. package/src/model/projections/constrained.test.ts +18 -10
  178. package/src/model/projections/constrained.ts +19 -8
  179. package/src/model/projections/dynamic.ts +5 -2
  180. package/src/model/projections/lattice.ts +15 -3
  181. package/src/model/registry.test.ts +14 -9
  182. package/src/model/registry.ts +39 -34
  183. package/src/model/store.ts +12 -2
  184. package/src/model/types.ts +124 -36
  185. package/src/playwright/SceneManager.ts +2 -2
  186. package/src/playwright/create.spec.ts +5 -3
  187. package/src/playwright/demo.spec.ts +2 -2
  188. package/src/playwright/lattice.spec.ts +4 -5
  189. package/src/playwright/scene.spec.ts +27 -25
  190. package/src/utils/builder.test.ts +46 -9
  191. package/src/utils/builder.ts +42 -18
  192. package/src/utils/camera.test.ts +22 -51
  193. package/src/utils/camera.ts +39 -32
  194. package/src/utils/clipboard.test.ts +23 -2
  195. package/src/utils/clipboard.ts +8 -4
  196. package/src/utils/diagram.test.ts +3 -1
  197. package/src/utils/diagram.ts +3 -25
  198. package/src/utils/group.test.ts +69 -0
  199. package/src/utils/group.ts +94 -0
  200. package/src/utils/gutter-route.test.ts +49 -0
  201. package/src/utils/gutter-route.ts +142 -16
  202. package/src/utils/hit.test.ts +4 -38
  203. package/src/utils/hit.ts +4 -26
  204. package/src/utils/index.ts +3 -0
  205. package/src/utils/lattice.ts +7 -1
  206. package/src/utils/layers.test.ts +139 -0
  207. package/src/utils/layers.ts +175 -0
  208. package/src/utils/parts.test.ts +66 -32
  209. package/src/utils/parts.ts +63 -41
  210. package/src/utils/ports.ts +7 -4
  211. package/src/utils/properties.test.ts +23 -6
  212. package/src/utils/properties.ts +66 -16
  213. package/src/utils/route.test.ts +51 -2
  214. package/src/utils/route.ts +133 -23
  215. package/src/utils/scenes.test.ts +38 -0
  216. package/src/utils/scenes.ts +54 -0
  217. package/src/utils/shapes.test.ts +33 -8
  218. package/src/utils/shapes.ts +51 -35
  219. package/src/utils/style.test.ts +54 -0
  220. package/src/utils/style.ts +153 -40
  221. package/src/utils/testing.test.ts +3 -3
  222. package/src/utils/testing.ts +21 -30
  223. package/src/utils/undo.test.ts +25 -2
  224. package/src/utils/undo.ts +50 -13
  225. package/dist/lib/chunk-Grid.mjs +0 -99
  226. package/dist/lib/chunk-Grid.mjs.map +0 -1
  227. package/dist/lib/scene.mjs +0 -6835
  228. package/dist/lib/scene.mjs.map +0 -1
  229. package/dist/lib/types.mjs +0 -16
  230. package/dist/lib/types.mjs.map +0 -1
  231. package/dist/types/src/archive/components/Canvas/Canvas.d.ts +0 -15
  232. package/dist/types/src/archive/components/Canvas/Canvas.d.ts.map +0 -1
  233. package/dist/types/src/archive/components/Canvas/Canvas.stories.d.ts +0 -18
  234. package/dist/types/src/archive/components/Canvas/Canvas.stories.d.ts.map +0 -1
  235. package/dist/types/src/archive/components/Canvas/index.d.ts +0 -2
  236. package/dist/types/src/archive/components/Canvas/index.d.ts.map +0 -1
  237. package/dist/types/src/archive/components/CellGrid/CellGrid.d.ts +0 -21
  238. package/dist/types/src/archive/components/CellGrid/CellGrid.d.ts.map +0 -1
  239. package/dist/types/src/archive/components/CellGrid/CellGrid.stories.d.ts +0 -21
  240. package/dist/types/src/archive/components/CellGrid/CellGrid.stories.d.ts.map +0 -1
  241. package/dist/types/src/archive/components/CellGrid/headers/Ruler.d.ts +0 -15
  242. package/dist/types/src/archive/components/CellGrid/headers/Ruler.d.ts.map +0 -1
  243. package/dist/types/src/archive/components/CellGrid/headers/TrackHeader.d.ts +0 -19
  244. package/dist/types/src/archive/components/CellGrid/headers/TrackHeader.d.ts.map +0 -1
  245. package/dist/types/src/archive/components/CellGrid/headers/index.d.ts +0 -3
  246. package/dist/types/src/archive/components/CellGrid/headers/index.d.ts.map +0 -1
  247. package/dist/types/src/archive/components/CellGrid/index.d.ts +0 -6
  248. package/dist/types/src/archive/components/CellGrid/index.d.ts.map +0 -1
  249. package/dist/types/src/archive/components/CellGrid/input/index.d.ts +0 -3
  250. package/dist/types/src/archive/components/CellGrid/input/index.d.ts.map +0 -1
  251. package/dist/types/src/archive/components/CellGrid/input/pointer.d.ts +0 -33
  252. package/dist/types/src/archive/components/CellGrid/input/pointer.d.ts.map +0 -1
  253. package/dist/types/src/archive/components/CellGrid/input/wheel.d.ts +0 -14
  254. package/dist/types/src/archive/components/CellGrid/input/wheel.d.ts.map +0 -1
  255. package/dist/types/src/archive/components/CellGrid/render/index.d.ts +0 -3
  256. package/dist/types/src/archive/components/CellGrid/render/index.d.ts.map +0 -1
  257. package/dist/types/src/archive/components/CellGrid/render/overlay-layer.d.ts +0 -21
  258. package/dist/types/src/archive/components/CellGrid/render/overlay-layer.d.ts.map +0 -1
  259. package/dist/types/src/archive/components/CellGrid/render/static-layer.d.ts +0 -36
  260. package/dist/types/src/archive/components/CellGrid/render/static-layer.d.ts.map +0 -1
  261. package/dist/types/src/archive/components/CellGrid/state/atoms.d.ts +0 -23
  262. package/dist/types/src/archive/components/CellGrid/state/atoms.d.ts.map +0 -1
  263. package/dist/types/src/archive/components/CellGrid/state/index.d.ts +0 -4
  264. package/dist/types/src/archive/components/CellGrid/state/index.d.ts.map +0 -1
  265. package/dist/types/src/archive/components/CellGrid/state/types.d.ts +0 -39
  266. package/dist/types/src/archive/components/CellGrid/state/types.d.ts.map +0 -1
  267. package/dist/types/src/archive/components/CellGrid/state/viewport.d.ts +0 -52
  268. package/dist/types/src/archive/components/CellGrid/state/viewport.d.ts.map +0 -1
  269. package/dist/types/src/archive/components/CellGrid/state/viewport.test.d.ts +0 -2
  270. package/dist/types/src/archive/components/CellGrid/state/viewport.test.d.ts.map +0 -1
  271. package/dist/types/src/archive/components/FPS.d.ts +0 -9
  272. package/dist/types/src/archive/components/FPS.d.ts.map +0 -1
  273. package/dist/types/src/archive/components/Grid/Grid.d.ts +0 -6
  274. package/dist/types/src/archive/components/Grid/Grid.d.ts.map +0 -1
  275. package/dist/types/src/archive/components/Grid/index.d.ts +0 -2
  276. package/dist/types/src/archive/components/Grid/index.d.ts.map +0 -1
  277. package/dist/types/src/archive/components/index.d.ts +0 -5
  278. package/dist/types/src/archive/components/index.d.ts.map +0 -1
  279. package/dist/types/src/archive/hooks/index.d.ts +0 -5
  280. package/dist/types/src/archive/hooks/index.d.ts.map +0 -1
  281. package/dist/types/src/archive/hooks/projection.d.ts +0 -60
  282. package/dist/types/src/archive/hooks/projection.d.ts.map +0 -1
  283. package/dist/types/src/archive/hooks/useCanvasContext.d.ts +0 -13
  284. package/dist/types/src/archive/hooks/useCanvasContext.d.ts.map +0 -1
  285. package/dist/types/src/archive/hooks/useDrag.d.ts +0 -6
  286. package/dist/types/src/archive/hooks/useDrag.d.ts.map +0 -1
  287. package/dist/types/src/archive/hooks/useWheel.d.ts +0 -8
  288. package/dist/types/src/archive/hooks/useWheel.d.ts.map +0 -1
  289. package/dist/types/src/archive/index.d.ts +0 -5
  290. package/dist/types/src/archive/index.d.ts.map +0 -1
  291. package/dist/types/src/archive/types.d.ts +0 -19
  292. package/dist/types/src/archive/types.d.ts.map +0 -1
  293. package/dist/types/src/archive/util/index.d.ts +0 -4
  294. package/dist/types/src/archive/util/index.d.ts.map +0 -1
  295. package/dist/types/src/archive/util/svg-path.d.ts +0 -4
  296. package/dist/types/src/archive/util/svg-path.d.ts.map +0 -1
  297. package/dist/types/src/archive/util/svg.d.ts +0 -32
  298. package/dist/types/src/archive/util/svg.d.ts.map +0 -1
  299. package/dist/types/src/archive/util/svg.stories.d.ts +0 -15
  300. package/dist/types/src/archive/util/svg.stories.d.ts.map +0 -1
  301. package/dist/types/src/archive/util/util.d.ts +0 -17
  302. package/dist/types/src/archive/util/util.d.ts.map +0 -1
  303. package/dist/types/src/scene.d.ts +0 -5
  304. package/dist/types/src/scene.d.ts.map +0 -1
  305. package/src/archive/components/Canvas/Canvas.stories.tsx +0 -111
  306. package/src/archive/components/Canvas/Canvas.tsx +0 -87
  307. package/src/archive/components/Canvas/index.ts +0 -5
  308. package/src/archive/components/CellGrid/CellGrid.stories.tsx +0 -239
  309. package/src/archive/components/CellGrid/CellGrid.tsx +0 -270
  310. package/src/archive/components/CellGrid/headers/Ruler.tsx +0 -71
  311. package/src/archive/components/CellGrid/headers/TrackHeader.tsx +0 -58
  312. package/src/archive/components/CellGrid/headers/index.ts +0 -6
  313. package/src/archive/components/CellGrid/index.ts +0 -9
  314. package/src/archive/components/CellGrid/input/index.ts +0 -6
  315. package/src/archive/components/CellGrid/input/pointer.ts +0 -236
  316. package/src/archive/components/CellGrid/input/wheel.ts +0 -68
  317. package/src/archive/components/CellGrid/render/index.ts +0 -6
  318. package/src/archive/components/CellGrid/render/overlay-layer.ts +0 -66
  319. package/src/archive/components/CellGrid/render/static-layer.ts +0 -112
  320. package/src/archive/components/CellGrid/state/atoms.ts +0 -43
  321. package/src/archive/components/CellGrid/state/index.ts +0 -7
  322. package/src/archive/components/CellGrid/state/types.ts +0 -40
  323. package/src/archive/components/CellGrid/state/viewport.test.ts +0 -50
  324. package/src/archive/components/CellGrid/state/viewport.ts +0 -94
  325. package/src/archive/components/FPS.tsx +0 -98
  326. package/src/archive/components/Grid/Grid.tsx +0 -17
  327. package/src/archive/components/Grid/index.ts +0 -5
  328. package/src/archive/components/index.ts +0 -8
  329. package/src/archive/hooks/index.ts +0 -8
  330. package/src/archive/hooks/projection.tsx +0 -156
  331. package/src/archive/hooks/useCanvasContext.ts +0 -32
  332. package/src/archive/hooks/useDrag.tsx +0 -96
  333. package/src/archive/hooks/useWheel.tsx +0 -80
  334. package/src/archive/index.ts +0 -8
  335. package/src/archive/types.ts +0 -14
  336. package/src/archive/util/index.ts +0 -7
  337. package/src/archive/util/svg-path.ts +0 -13
  338. package/src/archive/util/svg.stories.tsx +0 -46
  339. package/src/archive/util/svg.tsx +0 -128
  340. package/src/archive/util/util.ts +0 -50
  341. package/src/scene.ts +0 -9
@@ -11,15 +11,16 @@
11
11
 
12
12
  import { dropTargetForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
13
13
  import { useAtomValue } from '@effect/atom-react/Hooks';
14
- import React, { type ReactNode, useCallback, useEffect, useLayoutEffect, useMemo, useRef } from 'react';
14
+ import React, { type ReactNode, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
15
15
 
16
16
  import * as Menu from '@dxos/react-ui/Menu';
17
17
  import type * as Util from '@dxos/react-ui/Util';
18
18
  import * as VirtualAnchor from '@dxos/react-ui/VirtualAnchor';
19
19
  import { mx } from '@dxos/ui-theme';
20
20
 
21
- import { useRegistry, useSceneProjection, useViewport, useWheel } from '../../hooks/index.ts';
22
- import { type Drag, type SceneViewAtoms, createSceneViewAtoms } from '../../model/atoms.ts';
21
+ import { SCENE_OVERLAY_ATTRIBUTE, useRegistry, useSceneProjection, useViewport, useWheel } from '../../hooks/index.ts';
22
+ import { type Drag, type PanelMode, type SceneViewAtoms, createSceneViewAtoms, isMoving } from '../../model/atoms.ts';
23
+ import { keyAction } from '../../model/keys.ts';
23
24
  import { nodeDef } from '../../model/node-def.ts';
24
25
  import {
25
26
  type FreehandProjectionOptions,
@@ -35,32 +36,62 @@ import {
35
36
  } from '../../model/registry.ts';
36
37
  import { type SceneStore } from '../../model/store.ts';
37
38
  import {
39
+ type Bounds,
40
+ type Camera,
38
41
  DEFAULT_GRID,
39
42
  type ElementId,
40
43
  type Endpoint,
41
44
  type Intent,
45
+ type Layer,
46
+ type LayerId,
47
+ type LinkId,
42
48
  MAJOR_GRID_RATIO,
43
49
  type Node,
44
50
  type NodeType,
51
+ type Point,
45
52
  type Scene,
46
53
  type SceneId,
54
+ isFrameNode,
47
55
  isPointEndpoint,
48
56
  } from '../../model/types.ts';
49
- import { MIN_ZOOM, cameraTransform, fitBounds, panBy, screenToScene, zoomAt } from '../../utils/camera.ts';
57
+ import {
58
+ MIN_ZOOM,
59
+ NOMINAL_ZOOM,
60
+ cameraTransform,
61
+ fitBounds,
62
+ layerOpacity,
63
+ panBy,
64
+ sceneToScreen,
65
+ screenToScene,
66
+ zoomAt,
67
+ } from '../../utils/camera.ts';
50
68
  import { duplicateSelection } from '../../utils/clipboard.ts';
51
69
  import { nodeDragType } from '../../utils/dnd.ts';
52
- import { hitTest } from '../../utils/hit.ts';
70
+ import { groupIntoScene } from '../../utils/group.ts';
71
+ import { boundsFromPoints, hitTest } from '../../utils/hit.ts';
72
+ import {
73
+ activeLayer,
74
+ createLayer,
75
+ mergeLayersIntent,
76
+ moveLayer,
77
+ sceneLayers,
78
+ visibleScene,
79
+ } from '../../utils/layers.ts';
53
80
  import { topZ } from '../../utils/order.ts';
54
- import { type PartKey, partKey, partText, partValues } from '../../utils/parts.ts';
55
- import { createLink, nodeBounds } from '../../utils/shapes.ts';
56
- import { redo, undo } from '../../utils/undo.ts';
81
+ import { type PartKey, partText, partValues } from '../../utils/parts.ts';
82
+ import { sceneOptions } from '../../utils/scenes.ts';
83
+ import { createLink, nodeBounds, nominalSize } from '../../utils/shapes.ts';
84
+ import { recordScenes, redo, undo } from '../../utils/undo.ts';
85
+ import { About, type AboutStat } from '../About/About.tsx';
57
86
  import { ControlFrame } from '../ControlFrame/ControlFrame.tsx';
58
87
  import { GridComponent } from '../Grid/index.ts';
59
88
  import { LatticeGrid } from '../LatticeGrid/index.ts';
89
+ import { LayersPanel } from '../Layers/Layers.tsx';
60
90
  import { Palette } from '../Palette/Palette.tsx';
61
91
  import { Properties, type PropertiesProps } from '../Properties/Properties.tsx';
62
92
  import { type ElementHandlers, MAX_LIVE_DEPTH, SceneLayer } from '../SceneLayer/SceneLayer.tsx';
63
93
  import { ActionToolbar, CameraToolbar, NavigationToolbar, type ToolbarActions } from '../Toolbar/Toolbar.tsx';
94
+ import { Dock, DockProvider, type DockSection, useDockSection } from './Dock.tsx';
64
95
  import { SceneViewProvider, useSceneViewContext } from './SceneViewContext.ts';
65
96
  import { PREVIEW_NODE_ID, createId, isLinkDrawn, usePointerMachine } from './usePointerMachine.ts';
66
97
  import { useSceneCamera } from './useSceneCamera.ts';
@@ -70,11 +101,20 @@ import { useSceneNavigation } from './useSceneNavigation.ts';
70
101
  import { GRID_LEVELS, GRID_RANGE, useSceneSnap } from './useSceneSnap.ts';
71
102
 
72
103
  /** Major cells between the scene's frame and the viewport edge when fitting; `margin` overrides it. */
73
- const DEFAULT_MARGIN = 1;
104
+ const DEFAULT_MARGIN = 2;
105
+ /** Quiet time after the camera's last move before `onCameraChange` reports it. */
106
+ const CAMERA_SETTLE_MS = 300;
74
107
  /** Zoom factor of one toolbar step. */
75
108
  const ZOOM_STEP = 1.25;
76
- /** Length of a dash of the scene's frame, in screen px. */
77
- const FRAME_DASH = 4;
109
+ /** How long a link stays hovered after the pointer leaves it, so the pointer can reach its end handles. */
110
+ const LINK_HOVER_GRACE_MS = 150;
111
+
112
+ /** How long a move's pointer rests before the shapes snap to where they will land. */
113
+ const SETTLE_MS = 200;
114
+
115
+ /** Where the properties and layers panels float: the top right, one at a time (properties with a selection). */
116
+ const PANEL_CLASSES = 'absolute top-2 right-2 w-80 h-auto max-h-[calc(100%-1rem)]';
117
+
78
118
  /** The link drawn as a preview during a drag; it never reaches the model. */
79
119
  const PREVIEW_LINK_ID = 'preview-link';
80
120
 
@@ -89,15 +129,21 @@ export type SceneViewRootProps = Util.ThemedClassName<{
89
129
  projection?: Projection;
90
130
  /** Externally owned view state, e.g. to drive two views or persist the camera. */
91
131
  atoms?: SceneViewAtoms;
132
+ /** Where the camera starts on the root scene, e.g. as last left; the scene is fitted when unset. */
133
+ initialCamera?: Camera;
134
+ /** Called once the camera settles on the root scene, so a host can persist it. */
135
+ onCameraChange?: (camera: Camera) => void;
92
136
  /** Minor grid spacing in scene px; moves snap to it, creation and resizing to the major grid, `MAJOR_GRID_RATIO` times it. */
93
137
  grid?: number;
94
- /** Least gap between the scene's frame and each viewport edge when fitting, in whole major cells. */
138
+ /** Least gap between the scene's frame and each viewport edge when fitting, in major cells. */
95
139
  margin?: number;
96
140
  /**
97
141
  * Look, select and navigate only: no gesture or key reaches the model, and no handle or port is drawn,
98
142
  * whatever the projection would allow.
99
143
  */
100
144
  readonly?: boolean;
145
+ /** Where the properties and layers panels sit: docked in a column beside the canvas, or floating over it. */
146
+ panels?: PanelMode;
101
147
  children?: ReactNode;
102
148
  }>;
103
149
 
@@ -110,9 +156,12 @@ const SceneViewRoot = ({
110
156
  createProjection,
111
157
  projection: projectionProp,
112
158
  atoms: atomsProp,
159
+ initialCamera,
160
+ onCameraChange,
113
161
  grid = DEFAULT_GRID,
114
162
  margin = DEFAULT_MARGIN,
115
163
  readonly = false,
164
+ panels = 'docked',
116
165
  children,
117
166
  }: SceneViewRootProps) => {
118
167
  const registry = useRegistry();
@@ -123,6 +172,7 @@ const SceneViewRoot = ({
123
172
  const path = useAtomValue(atoms.path);
124
173
  const projection = useSceneProjection({ store, atoms, createProjection, projection: projectionProp });
125
174
  const scene = useAtomValue(projection.scene);
175
+ const visible = useMemo(() => visibleScene(scene), [scene]);
126
176
  const scenes = useAtomValue(store.scenes);
127
177
  const camera = useAtomValue(atoms.camera);
128
178
  const selection = useAtomValue(atoms.selection);
@@ -133,11 +183,20 @@ const SceneViewRoot = ({
133
183
  // The margin is in major cells, taken from the model's grid rather than the level currently drawn,
134
184
  // so a fit puts the same gap around the scene whatever the zoom.
135
185
  const inset = margin * grid * MAJOR_GRID_RATIO;
186
+ // A new node's default size is nominal: major cells of the model's grid, not of the level drawn at this zoom.
187
+ const cell = grid * MAJOR_GRID_RATIO;
136
188
 
137
189
  const drag = useAtomValue(atoms.drag);
138
190
  const undoState = useAtomValue(atoms.undo);
139
191
  const clipboard = useAtomValue(atoms.clipboard);
140
192
  const editing = useAtomValue(atoms.editing);
193
+ const active = useAtomValue(atoms.active);
194
+ // A node's content stays live only while the node is the selection; selecting anything else makes it inert again.
195
+ useEffect(() => {
196
+ if (active !== undefined && (selection.size !== 1 || !selection.has(active))) {
197
+ registry.set(atoms.active, undefined);
198
+ }
199
+ }, [active, selection, registry, atoms.active]);
141
200
  const debug = useAtomValue(atoms.debug);
142
201
  const guides = useAtomValue(atoms.guides);
143
202
  const latticeOn = useAtomValue(atoms.lattice);
@@ -167,42 +226,66 @@ const SceneViewRoot = ({
167
226
 
168
227
  // Keep the scene fitted while the viewport settles, until the user takes the camera over. A layout
169
228
  // effect, so the fit lands before the first paint instead of one frame after it.
170
- const interactedRef = useRef(false);
229
+ // A restored camera counts as taken over, so the fit leaves it where it was.
230
+ const interactedRef = useRef(initialCamera !== undefined);
171
231
 
232
+ // Read-only, nothing is selected: a selection only exists to be edited.
172
233
  const select = useCallback(
173
234
  (ids: Iterable<ElementId>) => {
174
- registry.set(atoms.selection, new Set(ids));
235
+ registry.set(atoms.selection, new Set(readonly ? [] : ids));
175
236
  registry.set(atoms.point, undefined);
176
237
  },
177
- [registry, atoms.selection, atoms.point],
238
+ [registry, atoms.selection, atoms.point, readonly],
178
239
  );
240
+ useEffect(() => {
241
+ if (readonly) {
242
+ select([]);
243
+ }
244
+ }, [readonly, select]);
179
245
 
180
- const { nameOf, portalTo, frameOf, bounds, nominalZoom, pushHistory, drillIn, drillOut, goHistory } =
181
- useSceneNavigation({
182
- registry,
183
- atoms,
184
- store,
185
- scenes,
186
- scene,
187
- path,
188
- camera,
189
- viewport,
190
- inset,
191
- drag,
192
- interactedRef,
193
- select,
194
- animateTo,
195
- setCamera,
196
- setOpening,
197
- isAnimating,
198
- });
246
+ const { nameOf, portalTo, bounds, fitTarget, pushHistory, drillIn, drillOut, goHistory } = useSceneNavigation({
247
+ registry,
248
+ atoms,
249
+ nodeRegistry,
250
+ store,
251
+ scenes,
252
+ scene,
253
+ path,
254
+ camera,
255
+ viewport,
256
+ inset,
257
+ drag,
258
+ interactedRef,
259
+ select,
260
+ animateTo,
261
+ setCamera,
262
+ setOpening,
263
+ isAnimating,
264
+ });
199
265
 
200
266
  const measured = viewport.width > 0 && viewport.height > 0;
267
+ useLayoutEffect(() => {
268
+ if (initialCamera) {
269
+ setCamera(initialCamera);
270
+ }
271
+ // Only the camera the view opened with is restored; later values are the host echoing ours back.
272
+ // eslint-disable-next-line react-hooks/exhaustive-deps
273
+ }, [setCamera]);
201
274
  useLayoutEffect(() => {
202
275
  if (!interactedRef.current && measured) {
203
- setCamera(fitBounds(bounds, viewport, inset));
276
+ setCamera(fitBounds(fitTarget, viewport, inset, NOMINAL_ZOOM));
277
+ }
278
+ }, [measured, viewport, fitTarget, inset, setCamera]);
279
+
280
+ // Reported after a quiet beat, so a wheel gesture or an animation is persisted once, where it ends.
281
+ const atRoot = path.length === 1;
282
+ useEffect(() => {
283
+ if (!onCameraChange || !atRoot || !interactedRef.current) {
284
+ return;
204
285
  }
205
- }, [measured, viewport, bounds, inset, setCamera]);
286
+ const timeout = setTimeout(() => onCameraChange(camera), CAMERA_SETTLE_MS);
287
+ return () => clearTimeout(timeout);
288
+ }, [onCameraChange, atRoot, camera]);
206
289
 
207
290
  useWheel(
208
291
  rootRef,
@@ -227,15 +310,15 @@ const SceneViewRoot = ({
227
310
 
228
311
  // Undo restores a whole model snapshot, so the selection may name elements that no longer exist.
229
312
  const onUndo = useCallback(() => {
230
- if (!readonly && undo(projection, registry, atoms.undo, sceneId)) {
313
+ if (!readonly && undo(projection, registry, atoms.undo, sceneId, store)) {
231
314
  select([]);
232
315
  }
233
- }, [readonly, projection, registry, atoms.undo, sceneId, select]);
316
+ }, [readonly, projection, registry, atoms.undo, sceneId, store, select]);
234
317
  const onRedo = useCallback(() => {
235
- if (!readonly && redo(projection, registry, atoms.undo, sceneId)) {
318
+ if (!readonly && redo(projection, registry, atoms.undo, sceneId, store)) {
236
319
  select([]);
237
320
  }
238
- }, [readonly, projection, registry, atoms.undo, sceneId, select]);
321
+ }, [readonly, projection, registry, atoms.undo, sceneId, store, select]);
239
322
 
240
323
  //
241
324
  // Pointer state machine.
@@ -282,7 +365,8 @@ const SceneViewRoot = ({
282
365
  registry,
283
366
  atoms,
284
367
  store,
285
- scene,
368
+ // A hidden layer's elements are not hit: gestures see the scene as drawn.
369
+ scene: visible,
286
370
  nodeRegistry,
287
371
  projection,
288
372
  capabilities,
@@ -293,12 +377,20 @@ const SceneViewRoot = ({
293
377
  setCamera,
294
378
  cancelAnimation,
295
379
  isNavigating,
380
+ cell,
296
381
  minor,
297
382
  major,
298
383
  snap,
299
384
  snapMinor,
300
385
  });
301
386
 
387
+ // A gesture begun before the view turned read-only is dropped, not committed on release.
388
+ useEffect(() => {
389
+ if (readonly) {
390
+ cancelDrag();
391
+ }
392
+ }, [readonly, cancelDrag]);
393
+
302
394
  //
303
395
  // Clipboard.
304
396
  //
@@ -324,7 +416,7 @@ const SceneViewRoot = ({
324
416
  projection,
325
417
  capabilities,
326
418
  viewport,
327
- bounds,
419
+ bounds: fitTarget,
328
420
  inset,
329
421
  grid,
330
422
  select,
@@ -353,9 +445,13 @@ const SceneViewRoot = ({
353
445
  (event) => {
354
446
  if (event.target === event.currentTarget) {
355
447
  onSceneKey(event);
448
+ } else if (keyAction(event) === 'cancel' && registry.get(atoms.active) !== undefined) {
449
+ // Escape out of a node's live content leaves it inert and the node selected, as a click outside would not.
450
+ registry.set(atoms.active, undefined);
451
+ rootRef.current?.focus();
356
452
  }
357
453
  },
358
- [onSceneKey],
454
+ [onSceneKey, registry, atoms.active],
359
455
  );
360
456
 
361
457
  //
@@ -372,18 +468,51 @@ const SceneViewRoot = ({
372
468
  // a link being drawn or re-attached over a drop target looks exactly as it will once dropped. Geometry
373
469
  // previews pass through the projection's `constrain`, so a drag shows where the drop will land, and
374
470
  // `blocked` says when the drop would be refused (drawn as is, outlined in red).
375
- const { displayScene, blocked } = useMemo<{ displayScene: Scene; blocked: boolean }>(() => {
471
+ // A move follows the pointer while it moves; once it rests for `SETTLE_MS`, the shapes snap to where they will land.
472
+ const moveRaw = drag?.kind === 'move' ? drag.raw : undefined;
473
+ const [settled, setSettled] = useState(false);
474
+ useEffect(() => {
475
+ setSettled(false);
476
+ if (!moveRaw) {
477
+ return;
478
+ }
479
+ const timer = setTimeout(() => setSettled(true), SETTLE_MS);
480
+ return () => clearTimeout(timer);
481
+ }, [moveRaw?.x, moveRaw?.y]);
482
+
483
+ const { displayScene, blocked, landing } = useMemo<{
484
+ displayScene: Scene;
485
+ blocked: boolean;
486
+ /** Where a move in flight will land (snapped and constrained), while the nodes follow the pointer. */
487
+ landing?: Bounds[];
488
+ }>(() => {
376
489
  const preview = (intent: Intent) => {
377
490
  const constrained = projection.constrain ? projection.constrain(intent) : intent;
378
491
  return { displayScene: reduceIntent(scene, constrained ?? intent), blocked: constrained === undefined };
379
492
  };
380
493
  if (drag?.kind === 'move') {
381
494
  // A copy previews beside the originals, which stay; the drop mints the copies' real ids.
382
- let next = 0;
383
- const copy = drag.copy
384
- ? duplicateSelection(scene, drag.ids, drag.delta, (prefix) => `${PREVIEW_NODE_ID}-${prefix}-${next++}`)
385
- : undefined;
386
- return preview(copy ? copy.intent : { kind: 'move', ids: drag.ids, delta: drag.delta });
495
+ const { ids, copy } = drag;
496
+ const moveBy = (delta: Point): Intent => {
497
+ let next = 0;
498
+ const duplicate = copy
499
+ ? duplicateSelection(scene, ids, delta, (prefix) => `${PREVIEW_NODE_ID}-${prefix}-${next++}`)
500
+ : undefined;
501
+ return duplicate ? duplicate.intent : { kind: 'move', ids, delta };
502
+ };
503
+ // The nodes follow the pointer smoothly; the frames they will snap to on the drop are drawn under them.
504
+ const landed = preview(moveBy(drag.delta));
505
+ const moved = copy
506
+ ? Object.values(landed.displayScene.nodes).filter((node) => !scene.nodes[node.id])
507
+ : ids.flatMap((id) => landed.displayScene.nodes[id] ?? []);
508
+ return settled
509
+ ? { displayScene: landed.displayScene, blocked: landed.blocked }
510
+ : {
511
+ displayScene: reduceIntent(scene, moveBy(drag.raw ?? drag.delta)),
512
+ blocked: landed.blocked,
513
+ // The landing is drawn only when the drop would be refused, the one thing following the pointer hides.
514
+ landing: landed.blocked ? moved.map(nodeBounds) : undefined,
515
+ };
387
516
  }
388
517
  if (drag?.kind === 'resize') {
389
518
  return preview({ kind: 'resize', id: drag.id, bounds: drag.bounds });
@@ -421,7 +550,7 @@ const SceneViewRoot = ({
421
550
  };
422
551
  }
423
552
  return { displayScene: scene, blocked: false };
424
- }, [scene, drag, createPreview, projection, minor]);
553
+ }, [scene, drag, settled, createPreview, projection, minor]);
425
554
 
426
555
  /** The bounds a create gesture would land, drawn as a frame whether or not the node itself previews. */
427
556
  const createFrame = useMemo(() => {
@@ -432,18 +561,35 @@ const SceneViewRoot = ({
432
561
  const onPartCommit = useCallback(
433
562
  (node: Node, part: PartKey, text: string) => {
434
563
  registry.set(atoms.editing, undefined);
435
- const values = partValues(node, part, text);
436
- if (values && capabilities.update && text !== partText(node, part)) {
564
+ const values = partValues(nodeRegistry, node, part, text);
565
+ if (values && capabilities.update && text !== partText(nodeRegistry, node, part)) {
437
566
  projection.apply({ kind: 'update', id: node.id, values });
438
567
  }
439
568
  },
440
- [registry, atoms.editing, capabilities.update, projection],
569
+ [registry, atoms.editing, capabilities.update, projection, nodeRegistry],
441
570
  );
442
571
  const onPartCancel = useCallback(() => registry.set(atoms.editing, undefined), [registry, atoms.editing]);
443
572
 
573
+ // Leaving a link waits a moment before it clears, so the pointer can reach the end handles drawn over it.
574
+ const linkHover = useAtomValue(atoms.linkHover);
575
+ const linkHoverTimer = useRef<ReturnType<typeof setTimeout>>(undefined);
576
+ const onLinkHover = useCallback(
577
+ (id: LinkId | undefined) => {
578
+ clearTimeout(linkHoverTimer.current);
579
+ if (id) {
580
+ registry.set(atoms.linkHover, id);
581
+ } else {
582
+ linkHoverTimer.current = setTimeout(() => registry.set(atoms.linkHover, undefined), LINK_HOVER_GRACE_MS);
583
+ }
584
+ },
585
+ [registry, atoms.linkHover],
586
+ );
587
+ useEffect(() => () => clearTimeout(linkHoverTimer.current), []);
588
+
444
589
  const handlers = useMemo<ElementHandlers>(
445
590
  () => ({
446
591
  onNodePointerDown,
592
+ onLinkHover,
447
593
  onLinkPointerDown,
448
594
  onLinkDoubleClick,
449
595
  onLinkContextMenu,
@@ -451,7 +597,16 @@ const SceneViewRoot = ({
451
597
  onPartCancel,
452
598
  onNodeOpen: drillIn,
453
599
  }),
454
- [onNodePointerDown, onLinkPointerDown, onLinkDoubleClick, onLinkContextMenu, onPartCommit, onPartCancel, drillIn],
600
+ [
601
+ onNodePointerDown,
602
+ onLinkHover,
603
+ onLinkPointerDown,
604
+ onLinkDoubleClick,
605
+ onLinkContextMenu,
606
+ onPartCommit,
607
+ onPartCancel,
608
+ drillIn,
609
+ ],
455
610
  );
456
611
 
457
612
  // Resolved at the root from the model: pointer capture during a drag retargets the click, so a
@@ -465,16 +620,21 @@ const SceneViewRoot = ({
465
620
  return;
466
621
  }
467
622
  const target = document.elementFromPoint(event.clientX, event.clientY);
468
- // A floating panel over the node took the clicks, so the node beneath is not the one meant.
469
- if (!(target instanceof Element) || !target.closest('[data-node-id]')) {
623
+ // A floating panel over the node took the clicks, so the node beneath is not the one meant; content embedded in
624
+ // the node (an editor) keeps its own double-click (selecting a word).
625
+ if (
626
+ !(target instanceof Element) ||
627
+ !target.closest('[data-node-id]') ||
628
+ target.closest(`[${SCENE_OVERLAY_ATTRIBUTE}]`)
629
+ ) {
470
630
  return;
471
631
  }
472
632
  const partElement = target.closest('[data-part]');
473
633
  const part =
474
634
  partElement?.closest('[data-node-id]')?.getAttribute('data-node-id') === node.id
475
- ? partKey(partElement?.getAttribute('data-part'))
635
+ ? (partElement?.getAttribute('data-part') ?? undefined)
476
636
  : undefined;
477
- if (part && capabilities.update && partText(node, part) !== undefined) {
637
+ if (part && capabilities.update && partText(nodeRegistry, node, part) !== undefined) {
478
638
  select([node.id]);
479
639
  registry.set(atoms.editing, { id: node.id, part });
480
640
  } else if (nodeDef(nodeRegistry, node)?.openable) {
@@ -493,11 +653,13 @@ const SceneViewRoot = ({
493
653
  if (!element) {
494
654
  return;
495
655
  }
496
- // The pointer is the shape's centre; its top-left is what snaps, so the edges land on the grid.
656
+ // The pointer is the shape's centre; its top-left snaps to the minor grid, as a move does, so the shape
657
+ // lands where it was dropped rather than up to half a major cell away.
497
658
  const dragAt = (type: NodeType, input: { clientX: number; clientY: number }): Drag => {
498
659
  const point = toScene(input);
499
- const size = nodeRegistry[type]?.defaultSize ?? { width: 0, height: 0 };
500
- const from = { x: snap(point.x - size.width / 2), y: snap(point.y - size.height / 2) };
660
+ const def = nodeRegistry[type];
661
+ const size = def ? nominalSize(def.defaultSize, cell) : { width: 0, height: 0 };
662
+ const from = { x: snapMinor(point.x - size.width / 2), y: snapMinor(point.y - size.height / 2) };
501
663
  return { kind: 'create', type, from, to: from, dropped: true };
502
664
  };
503
665
  return dropTargetForElements({
@@ -516,23 +678,45 @@ const SceneViewRoot = ({
516
678
  }
517
679
  },
518
680
  onDragLeave: cancelDrag,
519
- onDrop: () => onPointerUpRef.current(),
681
+ // Placed where it is released: the last drag-over can lag the pointer by a step.
682
+ onDrop: ({ source, location }) => {
683
+ const type = nodeDragType(source.data);
684
+ if (type !== undefined) {
685
+ setDrag(dragAt(type, location.current.input));
686
+ }
687
+ onPointerUpRef.current();
688
+ },
520
689
  });
521
- }, [capabilities.create, nodeRegistry, toScene, snap, setDrag, cancelDrag]);
690
+ }, [capabilities.create, nodeRegistry, cell, toScene, snapMinor, setDrag, cancelDrag]);
522
691
 
523
692
  const pointer = useMemo(
524
693
  () => screenToScene(camera, { x: viewport.width / 2, y: viewport.height / 2 }),
525
694
  [camera, viewport],
526
695
  );
527
696
 
528
- const zoomBy = useCallback(
529
- (factor: number) => {
697
+ // Shapes may land on free cells beyond the scene's frame, so the cells cover what is in view; only the view,
698
+ // since a union with a distant frame would exceed the grid's cell budget and hide the lattice.
699
+ const latticeBounds = useMemo(
700
+ () =>
701
+ boundsFromPoints(
702
+ screenToScene(camera, { x: 0, y: 0 }),
703
+ screenToScene(camera, { x: viewport.width, y: viewport.height }),
704
+ ),
705
+ [camera, viewport],
706
+ );
707
+
708
+ const zoomTo = useCallback(
709
+ (zoom: number) => {
530
710
  interactedRef.current = true;
531
711
  const centre = { x: viewport.width / 2, y: viewport.height / 2 };
532
- animateTo(zoomAt(registry.get(atoms.camera), centre, registry.get(atoms.camera).zoom * factor));
712
+ animateTo(zoomAt(registry.get(atoms.camera), centre, zoom));
533
713
  },
534
714
  [viewport, animateTo, registry, atoms.camera],
535
715
  );
716
+ const zoomBy = useCallback(
717
+ (factor: number) => zoomTo(registry.get(atoms.camera).zoom * factor),
718
+ [zoomTo, registry, atoms.camera],
719
+ );
536
720
 
537
721
  const deleteSelection = useCallback(() => {
538
722
  const ids = [...registry.get(atoms.selection)];
@@ -549,15 +733,15 @@ const SceneViewRoot = ({
549
733
  if (!def || !capabilities.create) {
550
734
  return;
551
735
  }
552
- const size = def.defaultSize;
553
- const from = { x: snap(pointer.x - size.width / 2), y: snap(pointer.y - size.height / 2) };
736
+ const size = nominalSize(def.defaultSize, cell);
737
+ const from = { x: snapMinor(pointer.x - size.width / 2), y: snapMinor(pointer.y - size.height / 2) };
554
738
  // `dropped`: there is no drawn box, so the type's default size applies, as for a palette drop.
555
739
  const node = createdNode({ kind: 'create', type, from, to: from, dropped: true }, createId(type));
556
740
  if (node) {
557
741
  commitCreated(node);
558
742
  }
559
743
  },
560
- [nodeRegistry, capabilities.create, snap, pointer, createdNode, commitCreated],
744
+ [nodeRegistry, capabilities.create, cell, snapMinor, pointer, createdNode, commitCreated],
561
745
  );
562
746
 
563
747
  const toolbarActions = useMemo<ToolbarActions>(
@@ -565,7 +749,8 @@ const SceneViewRoot = ({
565
749
  path,
566
750
  nameOf,
567
751
  onPath: (index) => drillOut(path.length - 1 - index),
568
- fit: () => animateTo(fitBounds(bounds, viewport, inset)),
752
+ fit: () => animateTo(fitBounds(fitTarget, viewport, inset, NOMINAL_ZOOM)),
753
+ zoomReset: () => zoomTo(NOMINAL_ZOOM),
569
754
  zoomIn: () => zoomBy(ZOOM_STEP),
570
755
  zoomOut: () => zoomBy(1 / ZOOM_STEP),
571
756
  snap: snapEnabled,
@@ -597,10 +782,11 @@ const SceneViewRoot = ({
597
782
  nameOf,
598
783
  drillOut,
599
784
  animateTo,
600
- bounds,
785
+ fitTarget,
601
786
  viewport,
602
787
  inset,
603
788
  zoomBy,
789
+ zoomTo,
604
790
  snapEnabled,
605
791
  toggleSnap,
606
792
  guides,
@@ -625,6 +811,28 @@ const SceneViewRoot = ({
625
811
  ],
626
812
  );
627
813
 
814
+ // The portal on screen that fills most of the view; the rest of the layer fades as it grows, so zooming into
815
+ // a scene (by wheel or drill-in) fades out what surrounds it and zooming out fades it back in.
816
+ const focus = useMemo(() => {
817
+ let best: { id: ElementId; opacity: number } | undefined;
818
+ for (const node of Object.values(displayScene.nodes)) {
819
+ // A frame the host opens itself (one showing an object) is no scene to zoom into, so it keeps its frame.
820
+ if (!isFrameNode(node) || nodeDef(nodeRegistry, node)?.hostOpen?.(node)) {
821
+ continue;
822
+ }
823
+ const bounds = nodeBounds(node);
824
+ const centre = sceneToScreen(camera, { x: bounds.x + bounds.width / 2, y: bounds.y + bounds.height / 2 });
825
+ if (centre.x < 0 || centre.y < 0 || centre.x > viewport.width || centre.y > viewport.height) {
826
+ continue;
827
+ }
828
+ const opacity = layerOpacity(camera, bounds, viewport);
829
+ if (!best || opacity < best.opacity) {
830
+ best = { id: node.id, opacity };
831
+ }
832
+ }
833
+ return best;
834
+ }, [displayScene.nodes, nodeRegistry, camera, viewport]);
835
+
628
836
  /** One screen pixel in scene units, for chrome that should not grow with the camera. */
629
837
  const frameUnit = 1 / Math.max(camera.zoom, MIN_ZOOM);
630
838
 
@@ -635,18 +843,20 @@ const SceneViewRoot = ({
635
843
  store={store}
636
844
  projection={projection}
637
845
  capabilities={capabilities}
846
+ readonly={readonly}
638
847
  nodeRegistry={nodeRegistry}
639
848
  linkRegistry={linkRegistry}
640
849
  scene={scene}
641
850
  displayScene={displayScene}
642
851
  blocked={blocked}
643
852
  bounds={bounds}
853
+ latticeBounds={latticeBounds}
644
854
  path={path}
645
855
  camera={camera}
646
- nominalZoom={nominalZoom}
647
856
  pointer={pointer}
648
857
  measured={measured}
649
858
  frameUnit={frameUnit}
859
+ focus={focus}
650
860
  grid={grid}
651
861
  snapEnabled={snapEnabled}
652
862
  guides={guides}
@@ -655,12 +865,16 @@ const SceneViewRoot = ({
655
865
  hover={hover}
656
866
  selectedPoint={selectedPoint}
657
867
  editing={editing}
868
+ active={active}
658
869
  clipboard={clipboard}
659
870
  drag={drag}
660
871
  tool={tool}
661
872
  debug={debug}
873
+ panels={panels}
662
874
  createFrame={createFrame}
875
+ landing={landing}
663
876
  handlers={handlers}
877
+ linkHover={linkHover}
664
878
  select={select}
665
879
  toolbarActions={toolbarActions}
666
880
  navigating={navigating}
@@ -690,29 +904,36 @@ const SceneViewRoot = ({
690
904
  onKeyDown={onKeyDown}
691
905
  rootRef={rootRef}
692
906
  >
693
- <div
694
- ref={rootRef}
695
- tabIndex={0}
696
- className={mx(
697
- 'relative dx-fill overflow-hidden bg-base-surface outline-none touch-none select-none',
698
- tool.kind === 'hand' && 'cursor-grab',
699
- tool.kind === 'node' && 'cursor-crosshair',
700
- classNames,
701
- )}
702
- style={{ contain: 'strict' }}
703
- data-testid='scene-view'
704
- onPointerDown={onBackgroundPointerDown}
705
- onPointerMove={onPointerMove}
706
- onPointerUp={onPointerUp}
707
- // A cancelled pointer (a touch the browser took over) abandons the gesture rather than landing it.
708
- onPointerCancel={cancelDrag}
709
- onPointerLeave={() => updateHover(undefined)}
710
- onDoubleClick={onDoubleClick}
711
- onContextMenu={onContextMenu}
712
- onKeyDown={onKeyDown}
713
- >
714
- {children}
715
- </div>
907
+ <DockProvider>
908
+ {/* The canvas beside the dock, which docked panels move into; with none docked, it takes no space.
909
+ Clipped rather than hidden: a hidden box still scrolls, so focusing content embedded in a node (an editor's
910
+ caret) would scroll the whole canvas to reveal it, out from under the camera. */}
911
+ <div className={mx('flex dx-fill overflow-clip', classNames)}>
912
+ <div
913
+ ref={rootRef}
914
+ tabIndex={0}
915
+ className={mx(
916
+ 'relative grow h-full overflow-clip bg-base-surface outline-none touch-none select-none',
917
+ tool.kind === 'hand' && 'cursor-grab',
918
+ tool.kind === 'node' && 'cursor-crosshair',
919
+ )}
920
+ style={{ contain: 'strict' }}
921
+ data-testid='scene-view'
922
+ onPointerDown={onBackgroundPointerDown}
923
+ onPointerMove={onPointerMove}
924
+ onPointerUp={onPointerUp}
925
+ // A cancelled pointer (a touch the browser took over) abandons the gesture rather than landing it.
926
+ onPointerCancel={cancelDrag}
927
+ onPointerLeave={() => updateHover(undefined)}
928
+ onDoubleClick={onDoubleClick}
929
+ onContextMenu={onContextMenu}
930
+ onKeyDown={onKeyDown}
931
+ >
932
+ {children}
933
+ </div>
934
+ <Dock />
935
+ </div>
936
+ </DockProvider>
716
937
  </SceneViewProvider>
717
938
  );
718
939
  };
@@ -737,14 +958,17 @@ const SceneViewCanvas = ({ liveDepth = MAX_LIVE_DEPTH, overlay }: SceneViewCanva
737
958
  atoms,
738
959
  store,
739
960
  capabilities,
961
+ readonly,
740
962
  projection,
741
963
  nodeRegistry,
742
964
  displayScene,
743
965
  blocked,
744
966
  bounds,
967
+ latticeBounds,
745
968
  camera,
746
969
  measured,
747
970
  frameUnit,
971
+ focus,
748
972
  grid,
749
973
  snapEnabled,
750
974
  guides,
@@ -753,11 +977,14 @@ const SceneViewCanvas = ({ liveDepth = MAX_LIVE_DEPTH, overlay }: SceneViewCanva
753
977
  hover,
754
978
  selectedPoint,
755
979
  editing,
980
+ active,
756
981
  clipboard,
757
982
  drag,
758
983
  debug,
759
984
  createFrame,
985
+ landing,
760
986
  handlers,
987
+ linkHover,
761
988
  select,
762
989
  navigating,
763
990
  opening,
@@ -776,12 +1003,15 @@ const SceneViewCanvas = ({ liveDepth = MAX_LIVE_DEPTH, overlay }: SceneViewCanva
776
1003
  menuAnchorRef,
777
1004
  } = useSceneViewContext('SceneView.Canvas');
778
1005
 
1006
+ // Handles and ports belong to what is drawn: a hidden layer's elements show none.
1007
+ const shownScene = useMemo(() => visibleScene(displayScene), [displayScene]);
1008
+
779
1009
  return (
780
1010
  <>
781
1011
  {/* Only while snapping: the lines are what a gesture lands on, so drawing them when nothing snaps
782
1012
  states a constraint the canvas is not applying. The minor level goes when its cells get too
783
- small to read. */}
784
- {snapEnabled && (
1013
+ small to read. Read-only, nothing snaps, so neither grid nor guides are drawn. */}
1014
+ {snapEnabled && !readonly && (
785
1015
  <GridComponent
786
1016
  size={grid}
787
1017
  scale={camera.zoom}
@@ -795,27 +1025,9 @@ const SceneViewCanvas = ({ liveDepth = MAX_LIVE_DEPTH, overlay }: SceneViewCanva
795
1025
  className={mx('absolute pointer-events-none', !measured && 'invisible')}
796
1026
  style={{ transform: cameraTransform(camera), transformOrigin: '0 0' }}
797
1027
  >
798
- {/* The frame is chrome rather than content, so its stroke and dashes are divided by the zoom
799
- the parent applies, the way the control frame's are. It is drawn as a stroke rather than a
800
- CSS border because a border's width is rounded to whole local pixels, which puts a floor of
801
- one scene unit under it — exactly the thickening that zooming in would cause. */}
802
1028
  {/* A lattice scene shows its cells: the places a shape may land, separated by the gutters. */}
803
- {guides && latticeOn && projection.lattice && (
804
- <LatticeGrid spec={projection.lattice} bounds={bounds} unit={frameUnit} />
805
- )}
806
- {guides && (
807
- <svg className='absolute overflow-visible pointer-events-none' width={1} height={1}>
808
- <rect
809
- data-testid='scene-frame'
810
- x={bounds.x}
811
- y={bounds.y}
812
- width={bounds.width}
813
- height={bounds.height}
814
- className='fill-none stroke-orange-border opacity-50'
815
- strokeWidth={frameUnit}
816
- strokeDasharray={`${FRAME_DASH * frameUnit} ${FRAME_DASH * frameUnit}`}
817
- />
818
- </svg>
1029
+ {guides && !readonly && latticeOn && projection.lattice && (
1030
+ <LatticeGrid spec={projection.lattice} bounds={latticeBounds} unit={frameUnit} />
819
1031
  )}
820
1032
  <div className='pointer-events-auto'>
821
1033
  <SceneLayer
@@ -826,35 +1038,46 @@ const SceneViewCanvas = ({ liveDepth = MAX_LIVE_DEPTH, overlay }: SceneViewCanva
826
1038
  depth={0}
827
1039
  liveDepth={liveDepth}
828
1040
  selected={selection}
1041
+ plain={isMoving(drag)}
829
1042
  hover={hover}
1043
+ hoveredLink={linkHover}
830
1044
  opening={opening}
1045
+ focus={focus}
831
1046
  editing={editing}
1047
+ active={active}
832
1048
  ghost={drag?.kind === 'create' ? PREVIEW_NODE_ID : undefined}
833
1049
  debug={debug}
834
1050
  handlers={handlers}
835
1051
  // Routes follow the gutters in lattice mode, whether or not snap is on.
836
1052
  lattice={latticeOn ? projection.lattice : undefined}
1053
+ cell={grid * MAJOR_GRID_RATIO}
837
1054
  />
838
1055
  </div>
839
- <ControlFrame
840
- scene={displayScene}
841
- registry={nodeRegistry}
842
- selection={selection}
843
- hover={hover}
844
- selectedPoint={selectedPoint}
845
- zoom={camera.zoom}
846
- drag={drag}
847
- capabilities={capabilities}
848
- createFrame={createFrame}
849
- blocked={blocked}
850
- lattice={latticeOn ? projection.lattice : undefined}
851
- onHandlePointerDown={onHandlePointerDown}
852
- onPortPointerDown={onPortPointerDown}
853
- onEndPointerDown={onEndPointerDown}
854
- onPointPointerDown={onPointPointerDown}
855
- onMidpointPointerDown={onMidpointPointerDown}
856
- onPointContextMenu={onPointContextMenu}
857
- />
1056
+ {/* Read-only, nothing is edited, so no selection frame, handle or port is drawn. */}
1057
+ {!readonly && (
1058
+ <ControlFrame
1059
+ scene={shownScene}
1060
+ registry={nodeRegistry}
1061
+ selection={selection}
1062
+ hover={hover}
1063
+ hoveredLink={drag ? undefined : linkHover}
1064
+ onLinkHover={handlers.onLinkHover}
1065
+ selectedPoint={selectedPoint}
1066
+ zoom={camera.zoom}
1067
+ drag={drag}
1068
+ capabilities={capabilities}
1069
+ createFrame={createFrame}
1070
+ landing={landing}
1071
+ blocked={blocked}
1072
+ lattice={latticeOn ? projection.lattice : undefined}
1073
+ onHandlePointerDown={onHandlePointerDown}
1074
+ onPortPointerDown={onPortPointerDown}
1075
+ onEndPointerDown={onEndPointerDown}
1076
+ onPointPointerDown={onPointPointerDown}
1077
+ onMidpointPointerDown={onMidpointPointerDown}
1078
+ onPointContextMenu={onPointContextMenu}
1079
+ />
1080
+ )}
858
1081
  {overlay}
859
1082
  </div>
860
1083
  {/* Wheel events still bubble to the root through the shield, so a zoom keeps zooming. */}
@@ -937,10 +1160,10 @@ const barFrame = 'absolute w-max max-w-[50%]';
937
1160
 
938
1161
  /** Where the view is in the scene tree. */
939
1162
  const SceneViewNavigation = ({ classNames = 'top-2 left-2' }: SceneViewBarProps) => {
940
- const { toolbarActions, path } = useSceneViewContext('SceneView.Navigation');
1163
+ const { toolbarActions } = useSceneViewContext('SceneView.Navigation');
941
1164
  return (
942
1165
  <div className={mx(barFrame, classNames)}>
943
- <NavigationToolbar actions={toolbarActions}>depth {path.length - 1}</NavigationToolbar>
1166
+ <NavigationToolbar actions={toolbarActions} />
944
1167
  </div>
945
1168
  );
946
1169
  };
@@ -949,7 +1172,11 @@ SceneViewNavigation.displayName = 'SceneView.Navigation';
949
1172
 
950
1173
  /** Everything that changes the view or the scene. */
951
1174
  const SceneViewActions = ({ classNames = 'bottom-2 left-1/2 -translate-x-1/2' }: SceneViewBarProps) => {
952
- const { toolbarActions, nodeRegistry, capabilities } = useSceneViewContext('SceneView.Actions');
1175
+ const { toolbarActions, nodeRegistry, capabilities, readonly } = useSceneViewContext('SceneView.Actions');
1176
+ // Every action here edits the scene or how edits snap.
1177
+ if (readonly) {
1178
+ return null;
1179
+ }
953
1180
  return (
954
1181
  <div className={mx(barFrame, classNames)}>
955
1182
  <ActionToolbar actions={toolbarActions} nodes={nodeRegistry} capabilities={capabilities} />
@@ -961,11 +1188,11 @@ SceneViewActions.displayName = 'SceneView.Actions';
961
1188
 
962
1189
  /** The camera's controls and numbers; nothing here changes the scene. */
963
1190
  const SceneViewDebug = ({ classNames = 'bottom-2 left-2' }: SceneViewBarProps) => {
964
- const { toolbarActions, nominalZoom, pointer } = useSceneViewContext('SceneView.Debug');
1191
+ const { toolbarActions, camera, pointer } = useSceneViewContext('SceneView.Debug');
965
1192
  return (
966
1193
  <div className={mx(barFrame, classNames)}>
967
1194
  <CameraToolbar actions={toolbarActions}>
968
- {Math.round(nominalZoom * 100)}% · ({Math.round(pointer.x)}, {Math.round(pointer.y)})
1195
+ {Math.round(camera.zoom * 100)}% · ({Math.round(pointer.x)}, {Math.round(pointer.y)})
969
1196
  </CameraToolbar>
970
1197
  </div>
971
1198
  );
@@ -978,10 +1205,11 @@ SceneViewDebug.displayName = 'SceneView.Debug';
978
1205
  //
979
1206
 
980
1207
  /** The tool rail: what the next gesture will draw. */
981
- const SceneViewPalette = ({ classNames = 'absolute top-14 left-2' }: SceneViewBarProps) => {
1208
+ const SceneViewPalette = ({ classNames = 'absolute top-14 bottom-14 left-2' }: SceneViewBarProps) => {
982
1209
  const { tool, nodeRegistry, linkRegistry, capabilities, setTool } = useSceneViewContext('SceneView.Palette');
983
1210
  return (
984
- <div className={mx(classNames)}>
1211
+ // Spans the room the rail may take, so it can fold its groups to fit; the canvas below the rail keeps the pointer.
1212
+ <div className={mx('pointer-events-none', classNames)}>
985
1213
  <Palette
986
1214
  tool={tool}
987
1215
  nodes={nodeRegistry}
@@ -999,32 +1227,216 @@ SceneViewPalette.displayName = 'SceneView.Palette';
999
1227
  // Properties
1000
1228
  //
1001
1229
 
1002
- export type SceneViewPropertiesProps = Util.ThemedClassName<Pick<PropertiesProps, 'fields'>>;
1230
+ export type SceneViewPropertiesProps = Util.ThemedClassName<
1231
+ Pick<PropertiesProps, 'fields' | 'db' | 'getOptions' | 'overrides'> & {
1232
+ /** Narrows the scenes a scene shape may open (e.g. to the host's own, not those it shows from elsewhere). */
1233
+ sceneFilter?: (id: SceneId) => boolean;
1234
+ }
1235
+ >;
1003
1236
 
1004
1237
  /** The selected element's properties as a floating panel; absent while nothing is selected. */
1005
1238
  const SceneViewProperties = ({
1006
- classNames = 'absolute top-2 right-2 w-80 max-h-[calc(100%-1rem)]',
1239
+ classNames = PANEL_CLASSES,
1007
1240
  fields,
1241
+ db,
1242
+ getOptions,
1243
+ overrides,
1244
+ sceneFilter,
1008
1245
  }: SceneViewPropertiesProps) => {
1009
- const { projection, atoms, nodeRegistry, capabilities, selection } = useSceneViewContext('SceneView.Properties');
1010
- if (selection.size === 0) {
1011
- return null;
1012
- }
1246
+ const { projection, atoms, nodeRegistry, capabilities, readonly, selection, store, path, panels } =
1247
+ useSceneViewContext('SceneView.Properties');
1248
+ const docked = panels === 'docked' && !readonly;
1249
+ const registry = useRegistry();
1250
+ const scenes = useAtomValue(store.scenes);
1251
+ const options = useMemo(() => sceneOptions(scenes, path, sceneFilter), [scenes, path, sceneFilter]);
1252
+
1253
+ // The selection moves into a new scene, opened by a shape where it was; the shape is then the selection.
1254
+ const onGroup = useCallback(() => {
1255
+ const id = createId('scene');
1256
+ const current = registry.get(projection.scene);
1257
+ const group = groupIntoScene(current, selection, id, activeLayer(current, registry.get(atoms.layer)));
1258
+ if (!group) {
1259
+ return;
1260
+ }
1261
+ const before = registry.get(store.scenes);
1262
+ registry.set(store.scenes, { ...before, [id]: { ...group.child, name: 'Untitled' } });
1263
+ projection.apply({ kind: 'batch', intents: group.intents });
1264
+ // A projection may refuse the batch (`apply` reports nothing), so the shape's presence is the result: without
1265
+ // it the new scene would be an orphan, so it goes and the selection stays.
1266
+ if (!registry.get(projection.scene).nodes[id]) {
1267
+ const { [id]: _, ...scenes } = registry.get(store.scenes);
1268
+ registry.set(store.scenes, scenes);
1269
+ return;
1270
+ }
1271
+ // One undo step takes the new scene away with the shape that opens it.
1272
+ recordScenes(registry, atoms.undo, path[path.length - 1], before);
1273
+ registry.set(atoms.selection, new Set([id]));
1274
+ registry.set(atoms.point, undefined);
1275
+ }, [registry, projection, selection, store.scenes, atoms.layer, atoms.selection, atoms.point, atoms.undo, path]);
1013
1276
 
1014
- return (
1277
+ const panel = (
1015
1278
  <Properties
1016
- classNames={mx('rounded-sm bg-modal-surface border border-separator', classNames)}
1279
+ classNames={docked ? 'h-auto' : mx('rounded-sm bg-modal-surface border border-separator', classNames)}
1280
+ docked={docked}
1017
1281
  projection={projection}
1018
1282
  atoms={atoms}
1019
1283
  nodes={nodeRegistry}
1020
1284
  fields={fields}
1285
+ db={db}
1286
+ getOptions={getOptions}
1287
+ overrides={overrides}
1288
+ sceneOptions={options}
1289
+ styles={store.styles}
1290
+ onGroup={capabilities.create && capabilities.delete ? onGroup : undefined}
1021
1291
  readonly={!capabilities.update}
1022
1292
  />
1023
1293
  );
1294
+ // Docked, the panel stays in its section whatever is selected (it says when nothing is); floating, it shows only
1295
+ // with a selection.
1296
+ const dockedPanel = useDockSection(PROPERTIES_SECTION, docked, panel);
1297
+ if (docked || readonly) {
1298
+ return dockedPanel;
1299
+ }
1300
+ return selection.size > 0 ? panel : null;
1024
1301
  };
1025
1302
 
1026
1303
  SceneViewProperties.displayName = 'SceneView.Properties';
1027
1304
 
1305
+ //
1306
+ // Dock
1307
+ //
1308
+
1309
+ const PROPERTIES_SECTION: DockSection = {
1310
+ id: 'properties',
1311
+ title: 'Properties',
1312
+ icon: 'ph--sliders-horizontal--regular',
1313
+ order: 0,
1314
+ };
1315
+
1316
+ const LAYERS_SECTION: DockSection = { id: 'layers', title: 'Layers', icon: 'ph--stack--regular', order: 1 };
1317
+
1318
+ //
1319
+ // Layers
1320
+ //
1321
+
1322
+ export type SceneViewLayersProps = Util.ThemedClassName<{}>;
1323
+
1324
+ /**
1325
+ * The current scene's layers as a floating panel where the properties panel goes, shown while nothing is selected
1326
+ * (the two take turns). Each edit is one intent, so one undo step.
1327
+ */
1328
+ const SceneViewLayers = ({ classNames = PANEL_CLASSES }: SceneViewLayersProps) => {
1329
+ const {
1330
+ projection,
1331
+ atoms,
1332
+ capabilities,
1333
+ readonly: viewReadonly,
1334
+ selection,
1335
+ panels,
1336
+ } = useSceneViewContext('SceneView.Layers');
1337
+ const docked = panels === 'docked' && !viewReadonly;
1338
+ const registry = useRegistry();
1339
+ const scene = useAtomValue(projection.scene);
1340
+ const active = useAtomValue(atoms.layer);
1341
+ const layers = useMemo(() => sceneLayers(scene), [scene]);
1342
+ const readonly = !capabilities.update;
1343
+ const setLayer = useCallback((layer: Layer) => projection.apply({ kind: 'layer', layer }), [projection]);
1344
+ const byId = useCallback((id: LayerId) => layers.find((layer) => layer.id === id), [layers]);
1345
+ // The layers picked in the panel; the active layer (the top-most of them) is the one new shapes go on.
1346
+ const [picked, setPicked] = useState<LayerId[]>([]);
1347
+ const top = activeLayer(scene, active);
1348
+ const selected = useMemo(() => {
1349
+ const ids = picked.filter((id) => byId(id));
1350
+ return ids.includes(top) ? ids : [top];
1351
+ }, [picked, byId, top]);
1352
+ const select = useCallback(
1353
+ (ids: LayerId[]) => {
1354
+ setPicked(ids);
1355
+ const topMost = [...layers].reverse().find((layer) => ids.includes(layer.id));
1356
+ topMost && registry.set(atoms.layer, topMost.id);
1357
+ },
1358
+ [layers, registry, atoms],
1359
+ );
1360
+ const panel = (
1361
+ <LayersPanel
1362
+ classNames={docked ? 'h-auto' : mx('rounded-sm bg-modal-surface border border-separator', classNames)}
1363
+ docked={docked}
1364
+ layers={layers}
1365
+ selected={selected}
1366
+ readonly={readonly}
1367
+ onSelectedChange={select}
1368
+ onToggle={(id) => {
1369
+ const layer = byId(id);
1370
+ layer && setLayer({ ...layer, hidden: !layer.hidden });
1371
+ }}
1372
+ onRename={(id, name) => {
1373
+ const layer = byId(id);
1374
+ layer && setLayer({ ...layer, name });
1375
+ }}
1376
+ onMove={(id, index) => {
1377
+ const layer = moveLayer(scene, id, index);
1378
+ layer && setLayer(layer);
1379
+ }}
1380
+ onCreate={() => {
1381
+ const layer = createLayer(scene, createId('layer'));
1382
+ setLayer(layer);
1383
+ select([layer.id]);
1384
+ return layer.id;
1385
+ }}
1386
+ onDelete={
1387
+ capabilities.delete
1388
+ ? (ids) => {
1389
+ projection.apply({ kind: 'batch', intents: ids.map((id): Intent => ({ kind: 'removeLayer', id })) });
1390
+ setPicked([]);
1391
+ }
1392
+ : undefined
1393
+ }
1394
+ onMerge={(ids, into) => {
1395
+ const merge = mergeLayersIntent(scene, ids, into);
1396
+ if (merge) {
1397
+ projection.apply(merge);
1398
+ select([into]);
1399
+ }
1400
+ }}
1401
+ />
1402
+ );
1403
+ const dockedPanel = useDockSection(LAYERS_SECTION, docked, panel);
1404
+ if (docked || viewReadonly) {
1405
+ return dockedPanel;
1406
+ }
1407
+ // Floating, the layers take the properties panel's place while nothing is selected.
1408
+ return selection.size > 0 ? null : panel;
1409
+ };
1410
+
1411
+ SceneViewLayers.displayName = 'SceneView.Layers';
1412
+
1413
+ //
1414
+ // About
1415
+ //
1416
+
1417
+ const ABOUT_SECTION: DockSection = { id: 'about', title: 'About', icon: 'ph--info--regular', order: 2 };
1418
+
1419
+ export type SceneViewAboutProps = Util.ThemedClassName<{}>;
1420
+
1421
+ /** Counts of the scene's objects and the drawing's scenes; a dock section only, since it has no place over the canvas. */
1422
+ const SceneViewAbout = ({ classNames }: SceneViewAboutProps) => {
1423
+ const { scene, store, panels } = useSceneViewContext('SceneView.About');
1424
+ const scenes = useAtomValue(store.scenes);
1425
+ const docked = panels === 'docked';
1426
+ const stats = useMemo<AboutStat[]>(
1427
+ () => [
1428
+ { id: 'nodes', label: 'Nodes', value: Object.keys(scene.nodes).length },
1429
+ { id: 'links', label: 'Links', value: Object.keys(scene.links).length },
1430
+ { id: 'layers', label: 'Layers', value: Object.keys(scene.layers ?? {}).length },
1431
+ { id: 'scenes', label: 'Scenes', value: Object.keys(scenes).length },
1432
+ ],
1433
+ [scene, scenes],
1434
+ );
1435
+ return useDockSection(ABOUT_SECTION, docked, <About classNames={mx('h-auto', classNames)} stats={stats} />);
1436
+ };
1437
+
1438
+ SceneViewAbout.displayName = 'SceneView.About';
1439
+
1028
1440
  export const SceneView = {
1029
1441
  Root: SceneViewRoot,
1030
1442
  Canvas: SceneViewCanvas,
@@ -1033,4 +1445,6 @@ export const SceneView = {
1033
1445
  Debug: SceneViewDebug,
1034
1446
  Palette: SceneViewPalette,
1035
1447
  Properties: SceneViewProperties,
1448
+ Layers: SceneViewLayers,
1449
+ About: SceneViewAbout,
1036
1450
  };