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