@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
@@ -5,6 +5,7 @@
5
5
  import { type Meta, type StoryObj } from '@storybook/react-vite';
6
6
  import React, { useState } from 'react';
7
7
 
8
+ import { log } from '@dxos/log';
8
9
  import { withLayout, withTheme } from '@dxos/react-ui/testing';
9
10
 
10
11
  import { freehandCapabilities } from '../../model/projection.ts';
@@ -14,17 +15,16 @@ import { ActionToolbar, CameraToolbar, NavigationToolbar, type ToolbarActions }
14
15
 
15
16
  /** The bar over a fake view: every action appends to a log so each button is seen to fire. */
16
17
  const DefaultStory = () => {
17
- const [log, setLog] = useState<string[]>([]);
18
18
  const [snap, setSnap] = useState(true);
19
19
  const [guides, setGuides] = useState(true);
20
20
  const [debug, setDebug] = useState(false);
21
21
  const [zoom, setZoom] = useState(1);
22
- const note = (entry: string) => setLog((entries) => [entry, ...entries].slice(0, 12));
23
22
  const actions: ToolbarActions = {
24
23
  path: ['scene:root', 'scene:root/L'],
25
24
  nameOf: (id) => id.split(':')[1] ?? id,
26
- onPath: (index) => note(`path → ${index}`),
27
- fit: () => note('fit'),
25
+ onPath: (index) => log(`path → ${index}`),
26
+ fit: () => log('fit'),
27
+ zoomReset: () => setZoom(1),
28
28
  zoomIn: () => setZoom((value) => value * 1.25),
29
29
  zoomOut: () => setZoom((value) => value / 1.25),
30
30
  snap,
@@ -35,34 +35,33 @@ const DefaultStory = () => {
35
35
  toggleDebug: () => setDebug((value) => !value),
36
36
  canUndo: true,
37
37
  canRedo: false,
38
- undo: () => note('undo'),
39
- redo: () => note('redo'),
38
+ undo: () => log('undo'),
39
+ redo: () => log('redo'),
40
40
  hasSelection: true,
41
41
  hasClipboard: true,
42
- cut: () => note('cut'),
43
- copy: () => note('copy'),
44
- paste: () => note('paste'),
45
- delete: () => note('delete'),
46
- create: (type: NodeType) => note(`create ${type}`),
42
+ cut: () => log('cut'),
43
+ copy: () => log('copy'),
44
+ paste: () => log('paste'),
45
+ delete: () => log('delete'),
46
+ create: (type: NodeType) => log(`create ${type}`),
47
47
  };
48
48
  return (
49
49
  // `items-start`: each bar fills its box, as SceneView floats it, so the column must not stretch them.
50
50
  <div className='flex flex-col items-start gap-2 p-2'>
51
- <NavigationToolbar actions={actions}>depth {actions.path.length - 1}</NavigationToolbar>
51
+ <NavigationToolbar actions={actions} />
52
52
  <ActionToolbar actions={actions} nodes={defaultNodeRegistry} capabilities={freehandCapabilities} />
53
53
  <CameraToolbar actions={actions}>
54
54
  {Math.round(zoom * 100)}% · snap {snap ? 'on' : 'off'} · guides {guides ? 'on' : 'off'} · debug{' '}
55
55
  {debug ? 'on' : 'off'}
56
56
  </CameraToolbar>
57
- <pre className='text-xs text-fg-muted'>{log.join('\n')}</pre>
58
57
  </div>
59
58
  );
60
59
  };
61
60
 
62
61
  const meta: Meta = {
63
- title: 'ui/react-ui-canvas/scene/Toolbar',
62
+ title: 'ui/react-ui-canvas/Toolbar',
64
63
  render: DefaultStory,
65
- decorators: [withTheme(), withLayout({ layout: 'fullscreen' })],
64
+ decorators: [withTheme(), withLayout({ layout: 'centered', classNames: 'w-[30rem]' })],
66
65
  };
67
66
 
68
67
  export default meta;
@@ -29,6 +29,8 @@ export type ToolbarActions = {
29
29
  nameOf: (id: SceneId) => string;
30
30
  onPath: (index: number) => void;
31
31
  fit: () => void;
32
+ /** Back to true size (100%), about the view's centre. */
33
+ zoomReset: () => void;
32
34
  zoomIn: () => void;
33
35
  zoomOut: () => void;
34
36
  snap: boolean;
@@ -75,12 +77,13 @@ export const NavigationToolbar = ({ classNames, actions, children }: NavigationT
75
77
  <Toolbar.Root size='sm' classNames={mx(barClasses, classNames)} data-testid='canvas-toolbar'>
76
78
  <Button.Root
77
79
  variant='ghost'
80
+ iconOnly
81
+ icon='ph--arrow-up--regular'
82
+ label='Up'
78
83
  disabled={path.length < 2}
79
84
  data-testid='toolbar-up'
80
85
  onClick={() => actions.onPath(path.length - 2)}
81
- >
82
- Up
83
- </Button.Root>
86
+ />
84
87
  <Breadcrumbs path={path} nameOf={actions.nameOf} onSelect={actions.onPath} />
85
88
  {children && (
86
89
  <>
@@ -106,6 +109,14 @@ export const CameraToolbar = ({ classNames, actions, children }: CameraToolbarPr
106
109
  data-testid='toolbar-fit'
107
110
  onClick={actions.fit}
108
111
  />
112
+ <Button.Root
113
+ variant='ghost'
114
+ iconOnly
115
+ icon='ph--number-square-one--regular'
116
+ label={`Actual size (${shortcutFor('zoomReset')})`}
117
+ data-testid='toolbar-zoom-reset'
118
+ onClick={actions.zoomReset}
119
+ />
109
120
  <Button.Root
110
121
  variant='ghost'
111
122
  iconOnly
@@ -151,7 +162,7 @@ export const ActionToolbar = ({ classNames, actions, nodes, capabilities }: Acti
151
162
  variant='ghost'
152
163
  iconOnly
153
164
  icon='ph--frame-corners--regular'
154
- label={`Guides (${shortcutFor('guides')}): show the page frame and the lattice cells`}
165
+ label={`Guides (${shortcutFor('guides')}): show the lattice cells`}
155
166
  classNames={mx(actions.guides && 'bg-primary-500/20')}
156
167
  data-testid='toolbar-guides'
157
168
  onClick={actions.toggleGuides}
@@ -2,10 +2,12 @@
2
2
  // Copyright 2026 DXOS.org
3
3
  //
4
4
 
5
+ export * from './About/index.ts';
5
6
  export * from './Breadcrumbs/index.ts';
6
7
  export * from './ControlFrame/index.ts';
7
8
  export * from './Grid/index.ts';
8
9
  export * from './LatticeGrid/index.ts';
10
+ export * from './Layers/index.ts';
9
11
  export * from './Palette/index.ts';
10
12
  export * from './PartEditor/index.ts';
11
13
  export * from './Properties/index.ts';
@@ -6,6 +6,31 @@ import { type RefObject, useEffect } from 'react';
6
6
 
7
7
  export type WheelHandler = (event: WheelEvent, pointer: { x: number; y: number }) => void;
8
8
 
9
+ /**
10
+ * Marks content whose own scrolling takes the wheel rather than the canvas panning: a floating panel over the canvas
11
+ * (the properties form), or a host's content embedded in a node (an object's surface in a frame).
12
+ */
13
+ export const SCENE_OVERLAY_ATTRIBUTE = 'data-scene-overlay';
14
+
15
+ const SCROLLS = /(auto|scroll)/;
16
+
17
+ /** Whether an element from `target` up to `overlay` scrolls, so the wheel is its own rather than the canvas's. */
18
+ const scrollsWithin = (target: Element, overlay: Element): boolean => {
19
+ for (let element: Element | null = target; element; element = element.parentElement) {
20
+ const { overflowX, overflowY } = getComputedStyle(element);
21
+ if (
22
+ (SCROLLS.test(overflowY) && element.scrollHeight > element.clientHeight) ||
23
+ (SCROLLS.test(overflowX) && element.scrollWidth > element.clientWidth)
24
+ ) {
25
+ return true;
26
+ }
27
+ if (element === overlay) {
28
+ return false;
29
+ }
30
+ }
31
+ return false;
32
+ };
33
+
9
34
  /** Non-passive wheel listener with the pointer in element coordinates, so the page never scrolls. */
10
35
  export const useWheel = (ref: RefObject<HTMLElement | null>, handler: WheelHandler) => {
11
36
  useEffect(() => {
@@ -14,6 +39,16 @@ export const useWheel = (ref: RefObject<HTMLElement | null>, handler: WheelHandl
14
39
  return;
15
40
  }
16
41
  const onWheel = (event: WheelEvent) => {
42
+ const target = event.target instanceof Element ? event.target : null;
43
+ const overlay = target?.closest(`[${SCENE_OVERLAY_ATTRIBUTE}]`);
44
+ // Content embedded in a node with nothing to scroll (a card that fits its frame) still lets the canvas pan.
45
+ if (target && overlay && (!overlay.closest('[data-node-id]') || scrollsWithin(target, overlay))) {
46
+ // A pinch over a panel still must not zoom the page; a scroll is the panel's own.
47
+ if (event.ctrlKey || event.metaKey) {
48
+ event.preventDefault();
49
+ }
50
+ return;
51
+ }
17
52
  event.preventDefault();
18
53
  const rect = element.getBoundingClientRect();
19
54
  handler(event, { x: event.clientX - rect.left, y: event.clientY - rect.top });
package/src/index.ts CHANGED
@@ -2,6 +2,7 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- // The pre-engine canvas, kept for canvas-editor, canvas-compute and the sequencer until the scene
6
- // engine replaces them (DESIGN.md phase 4).
7
- export * from './archive/index.ts';
5
+ export * from './components/index.ts';
6
+ export * from './hooks/index.ts';
7
+ export * from './model/index.ts';
8
+ export * from './utils/index.ts';
@@ -17,6 +17,7 @@ import {
17
17
  type Camera,
18
18
  type ElementId,
19
19
  type Endpoint,
20
+ type LayerId,
20
21
  type LinkId,
21
22
  type LinkType,
22
23
  type NodeId,
@@ -41,9 +42,10 @@ export type Drag =
41
42
  /**
42
43
  * Moving the selection; `anchor` is the pressed node's top-left, which is what snaps to the grid,
43
44
  * and `delta` the resulting scene-space offset applied transiently to every selected node. With
44
- * `copy` (⌘ held) the originals stay and copies land at `delta` instead.
45
+ * `copy` (⌘ held) the originals stay and copies land at `delta` instead. `raw` is the pointer's own offset, which
46
+ * the nodes follow while the drag is in flight; `delta` is where they land.
45
47
  */
46
- | { kind: 'move'; ids: NodeId[]; origin: Point; anchor: Point; delta: Point; copy?: boolean }
48
+ | { kind: 'move'; ids: NodeId[]; origin: Point; anchor: Point; delta: Point; raw?: Point; copy?: boolean }
47
49
  /** Resizing one node by a handle; `bounds` is the transient result. */
48
50
  | { kind: 'resize'; id: NodeId; handle: Handle; start: Bounds; bounds: Bounds }
49
51
  /**
@@ -61,6 +63,13 @@ export type Drag =
61
63
  /** Re-attaching one end of a link; `fixed` is the other end's resolved port for the rubber band. */
62
64
  | { kind: 'end'; id: LinkId; end: 'source' | 'target'; fixed: Point; fixedSide: Side; to: Point; target?: Endpoint };
63
65
 
66
+ /**
67
+ * Whether a move has left where it was pressed: until it does, a press on a selected node is a click, and the
68
+ * selection keeps its outline and handles rather than flickering off and on.
69
+ */
70
+ export const isMoving = (drag: Drag | undefined): boolean =>
71
+ drag?.kind === 'move' && drag.raw !== undefined && (drag.raw.x !== 0 || drag.raw.y !== 0);
72
+
64
73
  export type HistoryEntry = { path: SceneId[]; camera: Camera };
65
74
 
66
75
  export type ControlPointRef = { link: LinkId; index: number };
@@ -68,11 +77,18 @@ export type ControlPointRef = { link: LinkId; index: number };
68
77
  /** The text part being edited in place (`parts.ts`). */
69
78
  export type EditingPart = { id: NodeId; part: PartKey };
70
79
 
80
+ /** Where the properties and layers panels sit: floating over the canvas, or docked in a column beside it. */
81
+ export type PanelMode = 'floating' | 'docked';
82
+
71
83
  export type SceneViewAtoms = {
72
84
  camera: Atom.Writable<Camera>;
73
85
  path: Atom.Writable<SceneId[]>;
74
86
  selection: Atom.Writable<ReadonlySet<ElementId>>;
75
87
  hover: Atom.Writable<NodeId | undefined>;
88
+ /** The link under the pointer, which shows its end handles as a selected one does. */
89
+ linkHover: Atom.Writable<LinkId | undefined>;
90
+ /** The layer new shapes and links go on; unset (or a layer the scene does not have), the top one. */
91
+ layer: Atom.Writable<LayerId | undefined>;
76
92
  /** The selected control point of a selected spline, if any. */
77
93
  point: Atom.Writable<ControlPointRef | undefined>;
78
94
  tool: Atom.Writable<Tool>;
@@ -80,7 +96,7 @@ export type SceneViewAtoms = {
80
96
  linkType: Atom.Writable<LinkType>;
81
97
  /** Grid shown and moves/resizes snapped to it. */
82
98
  snap: Atom.Writable<boolean>;
83
- /** Guides shown: the scene's page frame and, on a lattice, its cells. */
99
+ /** Guides shown: on a lattice, its cells. */
84
100
  guides: Atom.Writable<boolean>;
85
101
  /** On a lattice scene, snap lands on the lattice's cells rather than the basic grid. */
86
102
  lattice: Atom.Writable<boolean>;
@@ -91,6 +107,11 @@ export type SceneViewAtoms = {
91
107
  /** The last cut or copied fragment (`clipboard.ts`). */
92
108
  clipboard: Atom.Writable<Clipboard | undefined>;
93
109
  editing: Atom.Writable<EditingPart | undefined>;
110
+ /**
111
+ * The node whose embedded content takes input (an editor in a frame), entered by clicking the node; any other
112
+ * node's embedded content is inert, so a press anywhere on it selects and moves it.
113
+ */
114
+ active: Atom.Writable<NodeId | undefined>;
94
115
  /** Frames show their id, type and geometry. */
95
116
  debug: Atom.Writable<boolean>;
96
117
  };
@@ -104,6 +125,8 @@ export const createSceneViewAtoms = (root: SceneId): SceneViewAtoms => ({
104
125
  path: Atom.keepAlive(Atom.make<SceneId[]>([root])),
105
126
  selection: Atom.keepAlive(Atom.make<ReadonlySet<ElementId>>(new Set<ElementId>())),
106
127
  hover: Atom.keepAlive(Atom.make<NodeId | undefined>(undefined)),
128
+ linkHover: Atom.keepAlive(Atom.make<LinkId | undefined>(undefined)),
129
+ layer: Atom.keepAlive(Atom.make<LayerId | undefined>(undefined)),
107
130
  point: Atom.keepAlive(Atom.make<ControlPointRef | undefined>(undefined)),
108
131
  tool: Atom.keepAlive(Atom.make<Tool>({ kind: 'select' })),
109
132
  linkType: Atom.keepAlive(Atom.make<LinkType>('curve')),
@@ -115,5 +138,6 @@ export const createSceneViewAtoms = (root: SceneId): SceneViewAtoms => ({
115
138
  undo: Atom.keepAlive(Atom.make<UndoState>(emptyUndo())),
116
139
  clipboard: Atom.keepAlive(Atom.make<Clipboard | undefined>(undefined)),
117
140
  editing: Atom.keepAlive(Atom.make<EditingPart | undefined>(undefined)),
141
+ active: Atom.keepAlive(Atom.make<NodeId | undefined>(undefined)),
118
142
  debug: Atom.keepAlive(Atom.make<boolean>(false)),
119
143
  });
@@ -13,6 +13,7 @@ import type * as Registry from 'effect/reactivity/AtomRegistry';
13
13
  import * as Schema from 'effect/Schema';
14
14
 
15
15
  import { type LatticeSpec } from '../utils/lattice.ts';
16
+ import { reduceLayerIntent } from '../utils/layers.ts';
16
17
  import { layoutScene } from '../utils/layout.ts';
17
18
  import { resizeNode } from '../utils/shapes.ts';
18
19
  import { type SceneStore, putScene, updateScene } from './store.ts';
@@ -136,6 +137,10 @@ export const reduceIntent = (scene: Scene, intent: Intent): Scene => {
136
137
  case 'batch':
137
138
  return intent.intents.reduce(reduceIntent, scene);
138
139
 
140
+ case 'layer':
141
+ case 'removeLayer':
142
+ return reduceLayerIntent(scene, intent);
143
+
139
144
  case 'update': {
140
145
  const node = scene.nodes[intent.id];
141
146
  if (node) {
@@ -6,7 +6,7 @@ import * as Atom from 'effect/reactivity/Atom';
6
6
  import * as Registry from 'effect/reactivity/AtomRegistry';
7
7
  import { describe, test } from 'vitest';
8
8
 
9
- import { type ClassNode, type EllipseNode, type Scene, isClassNode, isEllipseNode } from '../types.ts';
9
+ import { type EllipseNode, type NoteNode, type Scene, isEllipseNode, isNoteNode } from '../types.ts';
10
10
  import { type ConstrainedModel, createConstrainedProjection, rewriteForDrop, solve } from './constrained.ts';
11
11
 
12
12
  const model: ConstrainedModel = {
@@ -115,24 +115,32 @@ describe('constrained projection', () => {
115
115
  size: { width: 128, height: 64 },
116
116
  label: 'Round',
117
117
  };
118
- const klass: ClassNode = {
119
- type: 'class',
118
+ const note: NoteNode = {
119
+ type: 'note',
120
120
  id: 'K',
121
121
  z: 'z',
122
122
  center: { x: 0, y: 400 },
123
123
  size: { width: 1, height: 1 },
124
- name: 'Klass',
125
- attributes: [],
126
- methods: [],
124
+ text: 'Klass',
127
125
  };
128
126
  projection.apply({ kind: 'create', node: ellipse });
129
- projection.apply({ kind: 'create', node: klass });
127
+ projection.apply({ kind: 'create', node: note });
130
128
  const scene = registry.get(projection.scene);
131
129
  expect(scene.nodes.E.type).toBe('ellipse');
132
130
  expect(isEllipseNode(scene.nodes.E) && scene.nodes.E.label).toBe('Round');
133
- expect(isClassNode(scene.nodes.K) && scene.nodes.K.name).toBe('Klass');
134
- projection.apply({ kind: 'update', id: 'K', values: { name: 'Renamed' } });
131
+ expect(isNoteNode(scene.nodes.K) && scene.nodes.K.text).toBe('Klass');
132
+ projection.apply({ kind: 'update', id: 'K', values: { text: 'Renamed' } });
135
133
  const after = registry.get(projection.scene).nodes.K;
136
- expect(isClassNode(after) && after.name).toBe('Renamed');
134
+ expect(isNoteNode(after) && after.text).toBe('Renamed');
135
+ });
136
+
137
+ test('update records style alongside the label', ({ expect }) => {
138
+ const registry = Registry.make();
139
+ const atom = Atom.keepAlive(Atom.make<ConstrainedModel>(model));
140
+ const projection = createConstrainedProjection({ registry, model: atom });
141
+ projection.apply({ kind: 'update', id: 'A', values: { style: { hue: 'teal', rounded: true } } });
142
+ expect(registry.get(projection.scene).nodes.A.style).toEqual({ hue: 'teal', rounded: true });
143
+ projection.apply({ kind: 'update', id: 'A', values: { label: 'Alpha' } });
144
+ expect(registry.get(projection.scene).nodes.A.style).toEqual({ hue: 'teal', rounded: true });
137
145
  });
138
146
  });
@@ -15,18 +15,21 @@ import { Layout } from '@dxos/diagram';
15
15
 
16
16
  import { centeredOrigin } from '../../utils/layout.ts';
17
17
  import { initialKeys } from '../../utils/order.ts';
18
- import { createNode, withLabel } from '../../utils/shapes.ts';
18
+ import { withTitle } from '../../utils/parts.ts';
19
+ import { createNode } from '../../utils/shapes.ts';
19
20
  import { type Projection } from '../projection.ts';
21
+ import { defaultNodeRegistry } from '../registry.ts';
20
22
  import {
21
23
  type BuiltinNodeType,
22
24
  type Capabilities,
23
25
  type Intent,
24
26
  type Node,
27
+ type NodeStyle,
25
28
  type Point,
26
29
  type Scene,
27
30
  type Size,
28
31
  isBuiltinNode,
29
- isPortalNode,
32
+ isFrameNode,
30
33
  } from '../types.ts';
31
34
 
32
35
  /** `subject <relation> object`: "A east of B", "A aligned with B" (same row). */
@@ -35,7 +38,7 @@ export type Relation = 'east' | 'west' | 'north' | 'south' | 'aligned';
35
38
  export type Constraint = { subject: string; relation: Relation; object: string };
36
39
 
37
40
  /** `type` is the node's shape; the solver places every type on the same grid. */
38
- export type ConstrainedNode = { id: string; label?: string; type?: BuiltinNodeType };
41
+ export type ConstrainedNode = { id: string; label?: string; type?: BuiltinNodeType; style?: NodeStyle };
39
42
 
40
43
  export type ConstrainedModel = {
41
44
  nodes: ConstrainedNode[];
@@ -192,10 +195,12 @@ export const solve = (model: ConstrainedModel, options: ConstrainedOptions = {})
192
195
  x: origin.x + (columns.get(node.id) ?? 0) * pitch.width,
193
196
  y: origin.y + (rows.get(node.id) ?? 0) * pitch.height,
194
197
  };
195
- nodes[node.id] = withLabel(
198
+ const solved = withTitle(
199
+ defaultNodeRegistry,
196
200
  createNode({ type: node.type ?? 'rect', id: node.id, z: keys[index], center, size }),
197
201
  node.label ?? node.id,
198
202
  );
203
+ nodes[node.id] = node.style ? { ...solved, style: node.style } : solved;
199
204
  });
200
205
  return { id: CONSTRAINED_SCENE_ID, name: 'Constrained', nodes, links: {} };
201
206
  };
@@ -281,13 +286,14 @@ export const createConstrainedProjection = ({ registry, model, options }: Constr
281
286
  }
282
287
  case 'create': {
283
288
  // The model records built-in types only; a portal or a host type has no place in it and is refused.
284
- if (!isBuiltinNode(intent.node) || isPortalNode(intent.node)) {
289
+ if (!isBuiltinNode(intent.node) || isFrameNode(intent.node)) {
285
290
  return;
286
291
  }
287
292
  const node: ConstrainedNode = {
288
293
  id: intent.node.id,
289
294
  type: intent.node.type,
290
295
  label: labelOf(intent.node),
296
+ style: intent.node.style,
291
297
  };
292
298
  const added = { ...current, nodes: [...current.nodes, node] };
293
299
  // Constrain the new node as if it had been dropped where it was drawn.
@@ -308,12 +314,17 @@ export const createConstrainedProjection = ({ registry, model, options }: Constr
308
314
  intent.intents.forEach(apply);
309
315
  break;
310
316
  case 'update': {
311
- // Only the label lives in the model; geometry is solved, so those edits are dropped.
317
+ // Only the label and style live in the model; geometry is solved, so those edits are dropped.
312
318
  const label = labelOf(intent.values);
313
- if (label !== undefined) {
319
+ const style = 'style' in intent.values ? intent.values.style : undefined;
320
+ if (label !== undefined || style !== undefined) {
314
321
  registry.set(model, {
315
322
  ...current,
316
- nodes: current.nodes.map((node) => (node.id === intent.id ? { ...node, label } : node)),
323
+ nodes: current.nodes.map((node) =>
324
+ node.id === intent.id
325
+ ? { ...node, ...(label !== undefined && { label }), ...(style !== undefined && { style }) }
326
+ : node,
327
+ ),
317
328
  });
318
329
  }
319
330
  break;
@@ -16,8 +16,10 @@ import { Layout } from '@dxos/diagram';
16
16
 
17
17
  import { centeredOrigin } from '../../utils/layout.ts';
18
18
  import { initialKeys } from '../../utils/order.ts';
19
- import { createNode, withLabel } from '../../utils/shapes.ts';
19
+ import { withTitle } from '../../utils/parts.ts';
20
+ import { createNode } from '../../utils/shapes.ts';
20
21
  import { type Projection } from '../projection.ts';
22
+ import { defaultNodeRegistry } from '../registry.ts';
21
23
  import {
22
24
  type BuiltinNodeType,
23
25
  type Capabilities,
@@ -94,7 +96,8 @@ export const layoutGraph = (graph: GraphModel, overlay: Overlay, options: Dynami
94
96
  x: origin.x + (columns.get(node.id) ?? 0) * pitch.width,
95
97
  y: origin.y + (ranks.get(node.id) ?? 0) * pitch.height,
96
98
  };
97
- nodes[node.id] = withLabel(
99
+ nodes[node.id] = withTitle(
100
+ defaultNodeRegistry,
98
101
  createNode({ type: node.type ?? 'rect', id: node.id, z: keys[graph.edges.length + index], center, size }),
99
102
  node.label ?? node.id,
100
103
  );
@@ -15,6 +15,7 @@ import {
15
15
  collides,
16
16
  coveredCells,
17
17
  occupancy,
18
+ onLattice,
18
19
  quantize,
19
20
  resizeCell,
20
21
  toCell,
@@ -60,6 +61,9 @@ const blocked = (scene: Scene, spec: LatticeSpec, bounds: Bounds, except: Readon
60
61
  export const constrainIntent = (scene: Scene, intent: Intent, spec: LatticeSpec): Intent | undefined => {
61
62
  switch (intent.kind) {
62
63
  case 'create': {
64
+ if (!onLattice(intent.node)) {
65
+ return intent;
66
+ }
63
67
  const bounds = quantize(nodeBounds(intent.node), spec);
64
68
  return blocked(scene, spec, bounds, new Set())
65
69
  ? undefined
@@ -69,6 +73,9 @@ export const constrainIntent = (scene: Scene, intent: Intent, spec: LatticeSpec)
69
73
  case 'resize': {
70
74
  // The dragged edge snaps to a cell edge and the opposite edge stays, so a face steps one cell at a time.
71
75
  const node = scene.nodes[intent.id];
76
+ if (node && !onLattice(node)) {
77
+ return intent;
78
+ }
72
79
  const from = node && nodeBounds(node);
73
80
  const bounds = from
74
81
  ? cellBounds(resizeCell(toCell(from, spec), from, intent.bounds, spec), spec)
@@ -81,10 +88,15 @@ export const constrainIntent = (scene: Scene, intent: Intent, spec: LatticeSpec)
81
88
  const moving = new Set(intent.ids);
82
89
  const others = occupancy(Object.values(scene.nodes), spec, moving);
83
90
  const taken = new Map<string, ElementId>();
84
- const intents: Intent[] = [];
91
+ // A node off the lattice (a group's backdrop) moves by the drag as is, beside the snapped ones.
92
+ const free = intent.ids.filter((id) => {
93
+ const node = scene.nodes[id];
94
+ return node !== undefined && !node.locked && !onLattice(node);
95
+ });
96
+ const intents: Intent[] = free.length > 0 ? [{ kind: 'move', ids: free, delta: intent.delta }] : [];
85
97
  for (const id of intent.ids) {
86
98
  const node = scene.nodes[id];
87
- if (!node || node.locked) {
99
+ if (!node || node.locked || !onLattice(node)) {
88
100
  continue;
89
101
  }
90
102
  const from = nodeBounds(node);
@@ -103,7 +115,7 @@ export const constrainIntent = (scene: Scene, intent: Intent, spec: LatticeSpec)
103
115
 
104
116
  case 'update': {
105
117
  const node = scene.nodes[intent.id];
106
- const bounds = node && updatedBounds(node, intent.values);
118
+ const bounds = node && onLattice(node) ? updatedBounds(node, intent.values) : undefined;
107
119
  if (!node || !bounds) {
108
120
  return intent;
109
121
  }
@@ -7,7 +7,7 @@ import { describe, test } from 'vitest';
7
7
 
8
8
  import { UnknownNodeView } from '../components/SceneLayer/SceneLayer.tsx';
9
9
  import { nodePorts } from '../utils/ports.ts';
10
- import { createNode, nodeBounds } from '../utils/shapes.ts';
10
+ import { createNode, nodeBounds, nominalSize } from '../utils/shapes.ts';
11
11
  import { nodeDef } from './node-def.ts';
12
12
  import {
13
13
  type CreateProps,
@@ -26,12 +26,12 @@ const functionDef: NodeDef = {
26
26
  type: 'function',
27
27
  name: 'Function',
28
28
  icon: 'ph--function--regular',
29
- key: 'F',
29
+ key: 'U',
30
30
  group: 'compute',
31
31
  schema: FunctionNode,
32
32
  component: UnknownNodeView,
33
33
  create: ({ id, z, center, size }) => ({ type: 'function', id, z, center, size, fn: 'identity' }),
34
- defaultSize: { width: 128, height: 64 },
34
+ defaultSize: { width: 2, height: 1 },
35
35
  ports: () => [
36
36
  { id: 'in', side: 'w', offset: 0.5, accepts: 'in' },
37
37
  { id: 'out', side: 'e', offset: 0.5, accepts: 'out' },
@@ -43,7 +43,12 @@ const registry: NodeRegistry = { ...defaultNodeRegistry, function: functionDef }
43
43
  describe('registry', () => {
44
44
  test('a host composes the scene schema from its registry', ({ expect }) => {
45
45
  const HostScene = createSceneSchema(Object.values(registry).map((def) => def.schema));
46
- const fn = functionDef.create({ id: 'f', z: 'a', center: { x: 64, y: 64 }, size: functionDef.defaultSize });
46
+ const fn = functionDef.create({
47
+ id: 'f',
48
+ z: 'a',
49
+ center: { x: 64, y: 64 },
50
+ size: nominalSize(functionDef.defaultSize),
51
+ });
47
52
  const scene = { id: 's', nodes: { f: fn }, links: {} };
48
53
  expect(Schema.is(HostScene)(scene)).toBe(true);
49
54
  // The built-in schema rejects the host type; the engine still handles the node as a `NodeBase`.
@@ -73,13 +78,13 @@ describe('registry', () => {
73
78
  });
74
79
 
75
80
  describe('node registry', () => {
76
- test('rectangle and scene share the box prototype and differ only where they say so', ({ expect }) => {
77
- const { rect, scene } = defaultNodeRegistry;
81
+ test('rectangle and frame share the box prototype and differ only where they say so', ({ expect }) => {
82
+ const { rect, frame } = defaultNodeRegistry;
78
83
  expect(rect.component).toBe(boxPrototype.component);
79
84
  expect(rect.resizable).toBe(true);
80
- expect(scene.resizable).toBe(rect.resizable);
81
- expect(scene.portsPerSide).toBe(rect.portsPerSide);
82
- expect(scene.openable).toBe(true);
85
+ expect(frame.resizable).toBe(rect.resizable);
86
+ expect(frame.portsPerSide).toBe(rect.portsPerSide);
87
+ expect(frame.openable).toBe(true);
83
88
  expect(rect.openable).toBeUndefined();
84
89
  // A prototype is not a type: the palette and the scene schema never see it.
85
90
  expect(Object.keys(defaultNodeRegistry)).not.toContain('box');