@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dxos/react-ui-canvas",
3
- "version": "0.13.0",
3
+ "version": "0.14.0",
4
4
  "description": "A canvas component.",
5
5
  "homepage": "https://dxos.org",
6
6
  "bugs": "https://github.com/dxos/dxos/issues",
@@ -19,14 +19,9 @@
19
19
  "import": "./dist/lib/index.mjs"
20
20
  },
21
21
  "./scene": {
22
- "source": "./src/scene.ts",
23
- "types": "./dist/types/src/scene.d.ts",
24
- "import": "./dist/lib/scene.mjs"
25
- },
26
- "./types": {
27
- "source": "./src/archive/types.ts",
28
- "types": "./dist/types/src/archive/types.d.ts",
29
- "import": "./dist/lib/types.mjs"
22
+ "source": "./src/index.ts",
23
+ "types": "./dist/types/src/index.d.ts",
24
+ "import": "./dist/lib/index.mjs"
30
25
  }
31
26
  },
32
27
  "types": "dist/types/src/index.d.ts",
@@ -39,19 +34,16 @@
39
34
  "@codemirror/state": "^6.7.6",
40
35
  "@codemirror/view": "^6.43.13",
41
36
  "@effect/atom-react": "4.0.0",
42
- "bind-event-listener": "^3.0.0",
43
37
  "d3": "^7.9.0",
44
- "react-resize-detector": "^11.0.1",
45
- "transformation-matrix": "^2.16.1",
46
- "@dxos/debug": "0.13.0",
47
- "@dxos/diagram": "0.13.0",
48
- "@dxos/echo": "0.13.0",
49
- "@dxos/effect": "0.13.0",
50
- "@dxos/react-hooks": "0.13.0",
51
- "@dxos/react-ui-editor": "0.13.0",
52
- "@dxos/react-ui-form": "0.13.0",
53
- "@dxos/ui-editor": "0.13.0",
54
- "@dxos/ui-types": "0.13.0"
38
+ "@dxos/diagram": "0.14.0",
39
+ "@dxos/echo": "0.14.0",
40
+ "@dxos/effect": "0.14.0",
41
+ "@dxos/react-hooks": "0.14.0",
42
+ "@dxos/react-ui-editor": "0.14.0",
43
+ "@dxos/react-ui-form": "0.14.0",
44
+ "@dxos/react-ui-list": "0.14.0",
45
+ "@dxos/ui-editor": "0.14.0",
46
+ "@dxos/ui-types": "0.14.0"
55
47
  },
56
48
  "devDependencies": {
57
49
  "@playwright/test": "1.63.0",
@@ -63,16 +55,18 @@
63
55
  "react": "~19.2.7",
64
56
  "react-dom": "~19.2.7",
65
57
  "vite": "^8.2.2",
66
- "@dxos/react-ui": "0.13.0",
67
- "@dxos/test-utils": "0.13.0",
68
- "@dxos/ui-theme": "0.13.0"
58
+ "@dxos/log": "0.14.0",
59
+ "@dxos/react-ui": "0.14.0",
60
+ "@dxos/react-ui-syntax-highlighter": "0.14.0",
61
+ "@dxos/test-utils": "0.14.0",
62
+ "@dxos/ui-theme": "0.14.0"
69
63
  },
70
64
  "peerDependencies": {
71
65
  "effect": "4.0.0",
72
66
  "react": "~19.2.7",
73
67
  "react-dom": "~19.2.7",
74
- "@dxos/react-ui": "^0.13.0",
75
- "@dxos/ui-theme": "^0.13.0"
68
+ "@dxos/react-ui": "^0.14.0",
69
+ "@dxos/ui-theme": "^0.14.0"
76
70
  },
77
71
  "publishConfig": {
78
72
  "access": "public"
@@ -0,0 +1,32 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import { type Meta, type StoryObj } from '@storybook/react-vite';
6
+
7
+ import { withLayout, withTheme } from '@dxos/react-ui/testing';
8
+ import { translations as uiTranslations } from '@dxos/react-ui/translations';
9
+
10
+ import { About } from './About.tsx';
11
+
12
+ const meta: Meta<typeof About> = {
13
+ title: 'ui/react-ui-canvas/About',
14
+ component: About,
15
+ decorators: [withTheme(), withLayout({ layout: 'centered' })],
16
+ parameters: { translations: uiTranslations },
17
+ args: {
18
+ classNames: 'w-80',
19
+ stats: [
20
+ { id: 'nodes', label: 'Nodes', value: 12 },
21
+ { id: 'links', label: 'Links', value: 9 },
22
+ { id: 'layers', label: 'Layers', value: 2 },
23
+ { id: 'zoom', label: 'Zoom', value: '100%' },
24
+ ],
25
+ },
26
+ };
27
+
28
+ export default meta;
29
+
30
+ type Story = StoryObj<typeof meta>;
31
+
32
+ export const Default: Story = {};
@@ -0,0 +1,38 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import React, { Fragment, type ReactNode } from 'react';
6
+
7
+ import * as Layout from '@dxos/react-ui/Layout';
8
+ import * as Panel from '@dxos/react-ui/Panel';
9
+ import * as Typography from '@dxos/react-ui/Typography';
10
+ import type * as Util from '@dxos/react-ui/Util';
11
+
12
+ export type AboutStat = { id: string; label: string; value: ReactNode };
13
+
14
+ export type AboutProps = Util.ThemedClassName<{
15
+ stats: readonly AboutStat[];
16
+ }>;
17
+
18
+ /** A two-column table of the drawing's figures, one per list-height row: a label and its value. */
19
+ export const About = ({ classNames, stats }: AboutProps) => (
20
+ <Panel.Root classNames={classNames} data-testid='about'>
21
+ <Panel.Body>
22
+ <Layout.Container>
23
+ <Layout.Grid cols={['fill', 'auto']}>
24
+ {stats.map(({ id, label, value }) => (
25
+ <Fragment key={id}>
26
+ <Typography.Text tone='muted' truncate>
27
+ {label}
28
+ </Typography.Text>
29
+ <Typography.Text classNames='text-end tabular-nums' data-testid={`about-${id}`}>
30
+ {value}
31
+ </Typography.Text>
32
+ </Fragment>
33
+ ))}
34
+ </Layout.Grid>
35
+ </Layout.Container>
36
+ </Panel.Body>
37
+ </Panel.Root>
38
+ );
@@ -0,0 +1,5 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ export * from './About.tsx';
@@ -21,9 +21,23 @@ export const Breadcrumbs = ({ path, nameOf, onSelect }: BreadcrumbsProps) => (
21
21
  {path.map((id, index) => (
22
22
  <Fragment key={`${index}:${id}`}>
23
23
  {index > 0 && <span className='text-fg-subtle'>›</span>}
24
- <Button.Root variant='ghost' size='sm' disabled={index === path.length - 1} onClick={() => onSelect(index)}>
25
- {nameOf(id)}
26
- </Button.Root>
24
+ {/* The root is home whatever its scene is called. */}
25
+ {index === 0 ? (
26
+ <Button.Root
27
+ variant='ghost'
28
+ size='sm'
29
+ iconOnly
30
+ icon='ph--house--regular'
31
+ label={nameOf(id)}
32
+ disabled={index === path.length - 1}
33
+ data-testid='breadcrumb-root'
34
+ onClick={() => onSelect(index)}
35
+ />
36
+ ) : (
37
+ <Button.Root variant='ghost' size='sm' disabled={index === path.length - 1} onClick={() => onSelect(index)}>
38
+ {nameOf(id)}
39
+ </Button.Root>
40
+ )}
27
41
  </Fragment>
28
42
  ))}
29
43
  </nav>
@@ -12,7 +12,7 @@ import React, { memo } from 'react';
12
12
 
13
13
  import { mx } from '@dxos/ui-theme';
14
14
 
15
- import { type ControlPointRef, type Drag, type Handle } from '../../model/atoms.ts';
15
+ import { type ControlPointRef, type Drag, type Handle, isMoving } from '../../model/atoms.ts';
16
16
  import { nodeDef } from '../../model/node-def.ts';
17
17
  import { type NodeRegistry } from '../../model/registry.ts';
18
18
  import {
@@ -21,6 +21,7 @@ import {
21
21
  type ElementId,
22
22
  type Endpoint,
23
23
  type Link,
24
+ type LinkId,
24
25
  type Node,
25
26
  type Point,
26
27
  type Port,
@@ -63,11 +64,17 @@ export type ControlFrameProps = {
63
64
  capabilities: Capabilities;
64
65
  selection: ReadonlySet<ElementId>;
65
66
  hover?: ElementId;
67
+ /** The link under the pointer: it shows its end handles, as a selected link does. */
68
+ hoveredLink?: LinkId;
69
+ /** The pointer entered or left a hovered link's handles, which keeps the link hovered while on them. */
70
+ onLinkHover?: (id: LinkId | undefined) => void;
66
71
  selectedPoint?: ControlPointRef;
67
72
  zoom: number;
68
73
  drag?: Drag;
69
74
  /** The bounds a create gesture in flight would land, drawn as a provisional frame. */
70
75
  createFrame?: Bounds;
76
+ /** Where the nodes of a move in flight will land, drawn under them as they follow the pointer. */
77
+ landing?: readonly Bounds[];
71
78
  /** The gesture in flight would be refused (an overlap on the lattice): its outlines turn red. */
72
79
  blocked?: boolean;
73
80
  /** The scene's lattice, so a selected smart link's handles sit on its gutter route. */
@@ -97,10 +104,13 @@ export const ControlFrame = memo(
97
104
  capabilities,
98
105
  selection,
99
106
  hover,
107
+ hoveredLink,
108
+ onLinkHover,
100
109
  selectedPoint,
101
110
  zoom,
102
111
  drag,
103
112
  createFrame,
113
+ landing,
104
114
  blocked,
105
115
  lattice,
106
116
  onHandlePointerDown,
@@ -114,10 +124,14 @@ export const ControlFrame = memo(
114
124
  const handleSize = 8 * unit;
115
125
  const portRadius = 5 * unit;
116
126
  const midpointRadius = 4 * unit;
117
- const selectedNodes = [...selection].map((id) => scene.nodes[id]).filter((node) => node !== undefined);
118
- const selectedLinks = [...selection].map((id) => scene.links[id]).filter((link) => link !== undefined);
127
+ // A move in flight shows the shapes themselves: no outlines or handles, just where they will land.
128
+ const chrome = isMoving(drag) ? [] : [...selection];
129
+ const selectedNodes = chrome.map((id) => scene.nodes[id]).filter((node) => node !== undefined);
130
+ const selectedLinks = chrome.map((id) => scene.links[id]).filter((link) => link !== undefined);
131
+ const linkUnder = hoveredLink && !selection.has(hoveredLink) ? scene.links[hoveredLink] : undefined;
132
+ // A hovered link shows the handles of a selected one; only its ends, its control points stay a selection's.
119
133
  const lanes =
120
- lattice && selectedLinks.length > 0
134
+ lattice && (selectedLinks.length > 0 || linkUnder !== undefined)
121
135
  ? new Map(
122
136
  sceneLinkGeometry(scene, registry, Object.values(scene.links), lattice).map((geometry) => [
123
137
  geometry.link.id,
@@ -141,6 +155,21 @@ export const ControlFrame = memo(
141
155
  if (dropTarget) {
142
156
  portNodes.add(dropTarget);
143
157
  }
158
+ // The end a link drag holds still shows its ports too, a new link's source as an existing link's far end.
159
+ const draggedLink = drag?.kind === 'end' ? scene.links[drag.id] : undefined;
160
+ const fixedEnd =
161
+ drag?.kind === 'link'
162
+ ? drag.source
163
+ : draggedLink && drag?.kind === 'end'
164
+ ? drag.end === 'source'
165
+ ? draggedLink.target
166
+ : draggedLink.source
167
+ : undefined;
168
+ const fixedNode = fixedEnd && endpointNode(fixedEnd);
169
+ const anchor = fixedNode ? scene.nodes[fixedNode] : undefined;
170
+ if (anchor) {
171
+ portNodes.add(anchor);
172
+ }
144
173
  const marquee = drag?.kind === 'marquee' ? boundsFromPoints(drag.from, drag.to) : undefined;
145
174
  // The layer draws every provisional link that will land (over a target, or free-ended); the band is only
146
175
  // a port drag over free space, where dropping would create a node rather than a free end.
@@ -149,6 +178,74 @@ export const ControlFrame = memo(
149
178
  ? { from: { point: drag.from, side: drag.fromSide }, to: drag.to }
150
179
  : undefined;
151
180
 
181
+ /** A link's end handles, and a selected spline's control points; a hovered link shows only its ends. */
182
+ const linkHandles = (link: Link, hoverOnly: boolean) => {
183
+ // On a lattice the handles sit on the link's lane, which depends on the links around it.
184
+ const geometry = capabilities.update
185
+ ? (lanes?.get(link.id) ?? linkGeometry(scene, registry, link, lattice))
186
+ : undefined;
187
+ if (!geometry) {
188
+ return null;
189
+ }
190
+ const points =
191
+ link.type === 'spline' ? (drag?.kind === 'point' && drag.id === link.id ? drag.points : link.points) : [];
192
+ const ends: [LinkEnd, Point][] = [
193
+ ['source', geometry.source.point],
194
+ ['target', geometry.target.point],
195
+ ];
196
+ return (
197
+ <g key={link.id}>
198
+ {ends.map(([end, point]) => (
199
+ <circle
200
+ key={end}
201
+ cx={point.x}
202
+ cy={point.y}
203
+ r={portRadius}
204
+ className={mx(
205
+ 'stroke-focus pointer-events-auto cursor-move hover:fill-focus',
206
+ drag?.kind === 'end' && drag.id === link.id && drag.end === end ? 'fill-focus' : 'fill-base-surface',
207
+ )}
208
+ strokeWidth={unit}
209
+ onPointerDown={(event) => onEndPointerDown?.(link, end, event)}
210
+ onPointerEnter={hoverOnly ? () => onLinkHover?.(link.id) : undefined}
211
+ onPointerLeave={hoverOnly ? () => onLinkHover?.(undefined) : undefined}
212
+ />
213
+ ))}
214
+ {/* Midpoints first, so a control point wins wherever the two land on each other. */}
215
+ {!hoverOnly &&
216
+ link.type === 'spline' &&
217
+ midpoints(geometry.source.point, points, geometry.target.point).map(({ index, point }) => (
218
+ <circle
219
+ key={`midpoint-${index}`}
220
+ cx={point.x}
221
+ cy={point.y}
222
+ r={midpointRadius}
223
+ className='fill-focus/40 pointer-events-auto cursor-copy hover:fill-focus'
224
+ onPointerDown={(event) => onMidpointPointerDown?.(link, index, point, event)}
225
+ />
226
+ ))}
227
+ {link.type === 'spline' &&
228
+ points.map((point, index) => (
229
+ <rect
230
+ key={index}
231
+ x={point.x - handleSize / 2}
232
+ y={point.y - handleSize / 2}
233
+ width={handleSize}
234
+ height={handleSize}
235
+ transform={`rotate(45 ${point.x} ${point.y})`}
236
+ className={mx(
237
+ 'stroke-focus pointer-events-auto cursor-move hover:fill-focus',
238
+ selectedPoint?.link === link.id && selectedPoint.index === index ? 'fill-focus' : 'fill-base-surface',
239
+ )}
240
+ strokeWidth={unit}
241
+ onPointerDown={(event) => onPointPointerDown?.(link, index, event)}
242
+ onContextMenu={(event) => onPointContextMenu?.(link, index, event)}
243
+ />
244
+ ))}
245
+ </g>
246
+ );
247
+ };
248
+
152
249
  return (
153
250
  <svg className='absolute overflow-visible pointer-events-none' width={1} height={1}>
154
251
  {selectedNodes.map((node) => {
@@ -160,7 +257,7 @@ export const ControlFrame = memo(
160
257
  y={bounds.y}
161
258
  width={bounds.width}
162
259
  height={bounds.height}
163
- className={mx('fill-none', blocked ? 'stroke-error-border' : 'stroke-primary-500')}
260
+ className={mx('fill-none', blocked ? 'stroke-error-border' : 'stroke-focus')}
164
261
  strokeWidth={unit}
165
262
  data-blocked={blocked || undefined}
166
263
  />
@@ -173,9 +270,7 @@ export const ControlFrame = memo(
173
270
  // Filled while it is an end of the drag in progress: the source, or the port it would drop on.
174
271
  const isEnd = (end: Endpoint | undefined) =>
175
272
  end !== undefined && !isPointEndpoint(end) && end.node === node.id && end.port === port.id;
176
- const active =
177
- (drag?.kind === 'link' && isEnd(drag.source)) ||
178
- ((drag?.kind === 'link' || drag?.kind === 'end') && isEnd(drag.target));
273
+ const active = isEnd(fixedEnd) || ((drag?.kind === 'link' || drag?.kind === 'end') && isEnd(drag.target));
179
274
  return (
180
275
  <circle
181
276
  key={`${node.id}/${port.id}`}
@@ -183,8 +278,8 @@ export const ControlFrame = memo(
183
278
  cy={point.y}
184
279
  r={portRadius}
185
280
  className={mx(
186
- 'stroke-primary-500 pointer-events-auto cursor-crosshair hover:fill-primary-500',
187
- active ? 'fill-primary-500' : 'fill-base-surface',
281
+ 'stroke-focus pointer-events-auto cursor-crosshair hover:fill-focus',
282
+ active ? 'fill-focus' : 'fill-base-surface',
188
283
  )}
189
284
  strokeWidth={unit}
190
285
  onPointerDown={(event) => onPortPointerDown?.(node, port, event)}
@@ -192,6 +287,8 @@ export const ControlFrame = memo(
192
287
  );
193
288
  });
194
289
  })}
290
+ {/* A hovered link's ends go under a node's handles, so a resize handle on the same point still wins. */}
291
+ {linkUnder && linkHandles(linkUnder, true)}
195
292
  {/* Handles after the ports so a handle wins where a port sits on the same point (a side centre). */}
196
293
  {single && capabilities.resize && nodeDef(registry, single)?.resizable && !single.locked && (
197
294
  <g>
@@ -204,7 +301,7 @@ export const ControlFrame = memo(
204
301
  y={point.y - handleSize / 2}
205
302
  width={handleSize}
206
303
  height={handleSize}
207
- className='fill-base-surface stroke-primary-500 pointer-events-auto'
304
+ className='fill-base-surface stroke-focus pointer-events-auto'
208
305
  strokeWidth={unit}
209
306
  style={{ cursor: cursorFor(handle) }}
210
307
  onPointerDown={(event) => onHandlePointerDown?.(single, handle, event)}
@@ -214,91 +311,50 @@ export const ControlFrame = memo(
214
311
  </g>
215
312
  )}
216
313
  {/* A link's end and control-point handles all move it, so they follow the `update` capability together. */}
217
- {selectedLinks.map((link) => {
218
- // On a lattice the handles sit on the link's lane, which depends on the links around it.
219
- const geometry = capabilities.update
220
- ? (lanes?.get(link.id) ?? linkGeometry(scene, registry, link, lattice))
221
- : undefined;
222
- if (!geometry) {
223
- return null;
224
- }
225
- const points =
226
- link.type === 'spline' ? (drag?.kind === 'point' && drag.id === link.id ? drag.points : link.points) : [];
227
- const ends: [LinkEnd, Point][] = [
228
- ['source', geometry.source.point],
229
- ['target', geometry.target.point],
230
- ];
231
- return (
232
- <g key={link.id}>
233
- {ends.map(([end, point]) => (
234
- <circle
235
- key={end}
236
- cx={point.x}
237
- cy={point.y}
238
- r={portRadius}
239
- className={mx(
240
- 'stroke-primary-500 pointer-events-auto cursor-move hover:fill-primary-500',
241
- drag?.kind === 'end' && drag.id === link.id && drag.end === end
242
- ? 'fill-primary-500'
243
- : 'fill-base-surface',
244
- )}
245
- strokeWidth={unit}
246
- onPointerDown={(event) => onEndPointerDown?.(link, end, event)}
247
- />
248
- ))}
249
- {/* Midpoints first, so a control point wins wherever the two land on each other. */}
250
- {link.type === 'spline' &&
251
- midpoints(geometry.source.point, points, geometry.target.point).map(({ index, point }) => (
252
- <circle
253
- key={`midpoint-${index}`}
254
- cx={point.x}
255
- cy={point.y}
256
- r={midpointRadius}
257
- className='fill-primary-500/40 pointer-events-auto cursor-copy hover:fill-primary-500'
258
- onPointerDown={(event) => onMidpointPointerDown?.(link, index, point, event)}
259
- />
260
- ))}
261
- {link.type === 'spline' &&
262
- points.map((point, index) => (
263
- <rect
264
- key={index}
265
- x={point.x - handleSize / 2}
266
- y={point.y - handleSize / 2}
267
- width={handleSize}
268
- height={handleSize}
269
- transform={`rotate(45 ${point.x} ${point.y})`}
270
- className={mx(
271
- 'stroke-primary-500 pointer-events-auto cursor-move hover:fill-primary-500',
272
- selectedPoint?.link === link.id && selectedPoint.index === index
273
- ? 'fill-primary-500'
274
- : 'fill-base-surface',
275
- )}
276
- strokeWidth={unit}
277
- onPointerDown={(event) => onPointPointerDown?.(link, index, event)}
278
- onContextMenu={(event) => onPointContextMenu?.(link, index, event)}
279
- />
280
- ))}
281
- </g>
282
- );
283
- })}
314
+ {selectedLinks.map((link) => linkHandles(link, false))}
284
315
  {band && (
285
316
  <path
286
- d={curvePath(band.from, { point: band.to, side: oppositeSide(band.from.side) })}
287
- className='fill-none stroke-primary-500'
317
+ d={curvePath(band.from, { point: band.to, side: oppositeSide(band.from.side), free: true })}
318
+ className='fill-none stroke-focus'
288
319
  strokeWidth={2 * unit}
289
320
  strokeDasharray={`${6 * unit} ${4 * unit}`}
290
321
  />
291
322
  )}
323
+ {/* A link end over empty canvas carries a connection circle, as an end on a port does. */}
324
+ {(drag?.kind === 'link' || drag?.kind === 'end') && !drag.target && (
325
+ <circle
326
+ data-testid='free-end'
327
+ cx={drag.to.x}
328
+ cy={drag.to.y}
329
+ r={portRadius}
330
+ className='fill-focus stroke-focus pointer-events-none'
331
+ strokeWidth={unit}
332
+ />
333
+ )}
292
334
  {marquee && (
293
335
  <rect
294
336
  x={marquee.x}
295
337
  y={marquee.y}
296
338
  width={marquee.width}
297
339
  height={marquee.height}
298
- className='fill-primary-500/10 stroke-primary-500'
340
+ className='fill-focus/10 stroke-focus'
299
341
  strokeWidth={unit}
300
342
  />
301
343
  )}
344
+ {landing?.map((frame, index) => (
345
+ <rect
346
+ key={index}
347
+ data-testid='landing-frame'
348
+ x={frame.x}
349
+ y={frame.y}
350
+ width={frame.width}
351
+ height={frame.height}
352
+ className={mx('fill-none', blocked ? 'stroke-error-border' : 'stroke-focus')}
353
+ data-blocked={blocked || undefined}
354
+ strokeWidth={unit}
355
+ strokeDasharray={`${4 * unit} ${4 * unit}`}
356
+ />
357
+ ))}
302
358
  {createFrame && (
303
359
  <rect
304
360
  data-testid='create-frame'
@@ -306,7 +362,7 @@ export const ControlFrame = memo(
306
362
  y={createFrame.y}
307
363
  width={createFrame.width}
308
364
  height={createFrame.height}
309
- className={blocked ? 'fill-error-surface stroke-error-border' : 'fill-primary-500/10 stroke-primary-500'}
365
+ className={blocked ? 'fill-error-surface stroke-error-border' : 'fill-focus/10 stroke-focus'}
310
366
  data-blocked={blocked || undefined}
311
367
  strokeWidth={unit}
312
368
  />
@@ -120,7 +120,7 @@ const DefaultStory = ({ size, showAxes }: GridProps) => {
120
120
  };
121
121
 
122
122
  const meta: Meta<GridProps> = {
123
- title: 'ui/react-ui-canvas/scene/Grid',
123
+ title: 'ui/react-ui-canvas/Grid',
124
124
  component: GridComponent,
125
125
  render: DefaultStory,
126
126
  decorators: [withTheme(), withLayout({ layout: 'fullscreen' })],
@@ -0,0 +1,131 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import { type Meta, type StoryObj } from '@storybook/react-vite';
6
+ import React, { useState } from 'react';
7
+ import { expect, userEvent, waitFor, within } from 'storybook/test';
8
+
9
+ import { translations as formTranslations } from '@dxos/react-ui-form/translations';
10
+ import { withLayout, withTheme } from '@dxos/react-ui/testing';
11
+ import { translations as uiTranslations } from '@dxos/react-ui/translations';
12
+
13
+ import { type Layer, type LayerId } from '../../model/types.ts';
14
+ import { between } from '../../utils/order.ts';
15
+ import { LayersPanel } from './Layers.tsx';
16
+
17
+ /** Three layers, bottom first, the middle one hidden. */
18
+ const LAYERS: Layer[] = (() => {
19
+ const background: Layer = { id: 'background', name: 'Background', z: between() };
20
+ const notes: Layer = { id: 'notes', name: 'Notes', z: between(background.z), hidden: true };
21
+ const diagram: Layer = { id: 'diagram', name: 'Diagram', z: between(notes.z) };
22
+ return [background, notes, diagram];
23
+ })();
24
+
25
+ /** The panel over local state, as a host keeps the scene's layers. */
26
+ const DefaultStory = () => {
27
+ const [layers, setLayers] = useState(LAYERS);
28
+ const [selected, setSelected] = useState<LayerId[]>(['diagram']);
29
+ const update = (id: LayerId, values: Partial<Layer>) =>
30
+ setLayers((layers) => layers.map((layer) => (layer.id === id ? { ...layer, ...values } : layer)));
31
+ const remove = (ids: LayerId[]) => setLayers((layers) => layers.filter((layer) => !ids.includes(layer.id)));
32
+ return (
33
+ <LayersPanel
34
+ classNames='w-72 border border-separator rounded-sm'
35
+ layers={layers}
36
+ selected={selected}
37
+ onSelectedChange={setSelected}
38
+ onToggle={(id) => update(id, { hidden: !layers.find((layer) => layer.id === id)?.hidden })}
39
+ onRename={(id, name) => update(id, { name })}
40
+ onMove={(id, index) =>
41
+ setLayers((layers) => {
42
+ const moved = layers.find((layer) => layer.id === id);
43
+ const others = layers.filter((layer) => layer.id !== id);
44
+ return moved ? [...others.slice(0, index), moved, ...others.slice(index)] : layers;
45
+ })
46
+ }
47
+ onCreate={() => {
48
+ const id = `layer-${layers.length + 1}`;
49
+ setLayers((layers) => [
50
+ { id, name: `Layer ${layers.length + 1}`, z: between(undefined, layers[0]?.z) },
51
+ ...layers,
52
+ ]);
53
+ setSelected([id]);
54
+ return id;
55
+ }}
56
+ // In a scene, deleting takes the layer's shapes and merging moves them down; here there are none.
57
+ onDelete={(ids) => {
58
+ remove(ids);
59
+ setSelected([]);
60
+ }}
61
+ onMerge={(ids, into) => {
62
+ remove(ids.filter((id) => id !== into));
63
+ setSelected([into]);
64
+ }}
65
+ />
66
+ );
67
+ };
68
+
69
+ const meta: Meta<typeof DefaultStory> = {
70
+ title: 'ui/react-ui-canvas/Layers',
71
+ render: DefaultStory,
72
+ decorators: [withTheme(), withLayout({ layout: 'centered' })],
73
+ parameters: { translations: [...uiTranslations, ...formTranslations] },
74
+ };
75
+
76
+ export default meta;
77
+
78
+ type Story = StoryObj<typeof meta>;
79
+
80
+ export const Default: Story = {};
81
+
82
+ /** Every toolbar action and an inline rename, against the panel's local state. */
83
+ export const Test: Story = {
84
+ play: async ({ canvasElement }) => {
85
+ const canvas = within(canvasElement);
86
+ // 1. The layers list top first, as they stack.
87
+ const names = () => canvas.getAllByTestId(/^layer-name-/).map((element) => element.textContent);
88
+ await expect(names()).toEqual(['Diagram', 'Notes', 'Background']);
89
+
90
+ // 2. A new layer goes at the end of the list (the bottom of the stack).
91
+ await userEvent.click(canvas.getByTestId('layers-create'));
92
+ await waitFor(() => expect(names().at(-1)).toBe('Layer 4'));
93
+
94
+ // 3. The new layer's name opens for editing; the green check saves it.
95
+ const input = await canvas.findByTestId('layer-input-layer-4');
96
+ await waitFor(() => expect(input).toHaveFocus());
97
+ await userEvent.clear(input);
98
+ await userEvent.type(input, 'Sketch');
99
+ await userEvent.click(within(canvas.getByTestId('layer-layer-4')).getByTestId('editable.save'));
100
+ await waitFor(() => expect(names().at(-1)).toBe('Sketch'));
101
+
102
+ // 4. The eye shows a hidden layer.
103
+ await userEvent.click(canvas.getByTestId('layer-toggle-notes'));
104
+ await expect(canvas.getByTestId('layer-toggle-notes')).toHaveAccessibleName('Hide layer');
105
+
106
+ // 5. Merge, in the toolbar's menu, needs two layers: a Shift-click extends the selection to the next one, and
107
+ // merging keeps the top-most. Shift rather than Cmd/Ctrl, whose key the listbox picks by platform. The menu is
108
+ // portalled, so its items are found in the document body.
109
+ const body = within(canvasElement.ownerDocument.body);
110
+ await userEvent.click(canvas.getByTestId('layers-menu'));
111
+ await expect(await body.findByTestId('layers-merge')).toHaveAttribute('data-disabled');
112
+ await userEvent.keyboard('{Escape}');
113
+ await userEvent.click(canvas.getByTestId('layer-name-diagram'));
114
+ // One session, so the held key is still down for the click.
115
+ const user = userEvent.setup();
116
+ await user.keyboard('{Shift>}');
117
+ await user.click(canvas.getByTestId('layer-name-notes'));
118
+ await user.keyboard('{/Shift}');
119
+ await userEvent.click(canvas.getByTestId('layers-menu'));
120
+ const merge = await body.findByTestId('layers-merge');
121
+ await waitFor(() => expect(merge).not.toHaveAttribute('data-disabled'));
122
+ // Picked from the keyboard, as the react-ui Menu stories do: a synthetic click does not select a menu item.
123
+ await userEvent.keyboard('{Home}{Enter}');
124
+ await waitFor(() => expect(names()).toEqual(['Diagram', 'Background', 'Sketch']));
125
+
126
+ // 6. The arrows move along the list, and Enter opens the current row's name.
127
+ await userEvent.click(canvas.getByTestId('layer-name-diagram'));
128
+ await userEvent.keyboard('{ArrowDown}{Enter}');
129
+ await waitFor(() => expect(canvas.getByTestId('layer-input-background')).toHaveFocus());
130
+ },
131
+ };