@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
@@ -0,0 +1,38 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import { describe, test } from 'vitest';
6
+
7
+ import { SceneBuilder } from './builder.ts';
8
+ import { sceneOptions } from './scenes.ts';
9
+
10
+ describe('sceneOptions', () => {
11
+ const box = (x: number) => ({ x, y: 0, width: 200, height: 100 });
12
+ const { scenes: list } = SceneBuilder.scene('root', [
13
+ SceneBuilder.scene('a', [SceneBuilder.scene('x').at(box(0)).properties({ label: 'Inner' })])
14
+ .name('A')
15
+ .at(box(0)),
16
+ SceneBuilder.scene('b').name('B').at(box(300)),
17
+ ]).build();
18
+ const scenes = Object.fromEntries(list.map((scene) => [scene.id, scene]));
19
+
20
+ test('lists every scene a shape may open, named or by the label of a shape that opens it', ({ expect }) => {
21
+ expect(sceneOptions(scenes, ['root'])).toEqual([
22
+ { value: 'a', label: 'A' },
23
+ { value: 'b', label: 'B' },
24
+ { value: 'x', label: 'Inner' },
25
+ ]);
26
+ });
27
+
28
+ test('never offers a scene that would open the path again', ({ expect }) => {
29
+ // Inside `a`: neither `a` nor the root, which opens `a`, may be opened from it; `b` and `x` may.
30
+ expect(sceneOptions(scenes, ['root', 'a']).map((option) => option.value)).toEqual(['b', 'x']);
31
+ // Inside `x`: `a` opens `x`, so it is out too.
32
+ expect(sceneOptions(scenes, ['root', 'a', 'x']).map((option) => option.value)).toEqual(['b']);
33
+ });
34
+
35
+ test('a host narrows the candidates', ({ expect }) => {
36
+ expect(sceneOptions(scenes, ['root'], (id) => id !== 'x').map((option) => option.value)).toEqual(['a', 'b']);
37
+ });
38
+ });
@@ -0,0 +1,54 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import { type SceneMap } from '../model/store.ts';
6
+ import { type Scene, type SceneId, isFrameNode } from '../model/types.ts';
7
+
8
+ /** A scene a scene shape may open, as the properties panel lists it. */
9
+ export type SceneOption = { value: SceneId; label: string };
10
+
11
+ /** The scenes the shapes of `scene` open. */
12
+ const childScenes = (scene: Scene | undefined): SceneId[] =>
13
+ scene ? Object.values(scene.nodes).flatMap((node) => (isFrameNode(node) ? [node.scene] : [])) : [];
14
+
15
+ /** Whether `from`, or any scene it opens however deep, is one of `targets`. */
16
+ const reaches = (scenes: SceneMap, from: SceneId, targets: ReadonlySet<SceneId>): boolean => {
17
+ const seen = new Set<SceneId>();
18
+ const pending = [from];
19
+ for (let id = pending.pop(); id !== undefined; id = pending.pop()) {
20
+ if (targets.has(id)) {
21
+ return true;
22
+ }
23
+ if (!seen.has(id)) {
24
+ seen.add(id);
25
+ pending.push(...childScenes(scenes[id]));
26
+ }
27
+ }
28
+ return false;
29
+ };
30
+
31
+ /**
32
+ * The scenes a shape in the scene at the head of `path` may open: any scene of the store, shared by as many shapes
33
+ * as open it, except one that would open the path again, which would nest the scene in itself without end. An
34
+ * unnamed scene reads as the label of a shape that opens it.
35
+ */
36
+ export const sceneOptions = (
37
+ scenes: SceneMap,
38
+ path: readonly SceneId[],
39
+ filter: (id: SceneId) => boolean = () => true,
40
+ ): SceneOption[] => {
41
+ const ancestors = new Set(path);
42
+ const labels = new Map<SceneId, string>();
43
+ for (const scene of Object.values(scenes)) {
44
+ for (const node of Object.values(scene.nodes)) {
45
+ if (isFrameNode(node) && node.label && !labels.has(node.scene)) {
46
+ labels.set(node.scene, node.label);
47
+ }
48
+ }
49
+ }
50
+ return Object.values(scenes)
51
+ .filter((scene) => filter(scene.id) && !reaches(scenes, scene.id, ancestors))
52
+ .map((scene) => ({ value: scene.id, label: scene.name ?? labels.get(scene.id) ?? scene.id }))
53
+ .sort((left, right) => left.label.localeCompare(right.label));
54
+ };
@@ -4,7 +4,8 @@
4
4
 
5
5
  import { describe, test } from 'vitest';
6
6
 
7
- import { cloneShape, createNode } from './shapes.ts';
7
+ import { type SplineLink } from '../model/types.ts';
8
+ import { cloneShape, createNode, flipLink } from './shapes.ts';
8
9
 
9
10
  describe('shapes', () => {
10
11
  test('a cloned shape keeps the look and size but not the text, identity or place', ({ expect }) => {
@@ -15,7 +16,7 @@ describe('shapes', () => {
15
16
  style: { hue: 'teal', rounded: true },
16
17
  };
17
18
  const fresh = createNode({ type: 'rect', id: 'b', z: 'a1', center: { x: 400, y: 0 }, size: source.size });
18
- expect(cloneShape(source, fresh)).toEqual({
19
+ expect(cloneShape(source, fresh, [{ field: 'label' }])).toEqual({
19
20
  type: 'rect',
20
21
  id: 'b',
21
22
  z: 'a1',
@@ -26,13 +27,37 @@ describe('shapes', () => {
26
27
  });
27
28
  });
28
29
 
29
- test('a cloned portal opens onto its own scene, and a class gets its type text', ({ expect }) => {
30
- const portal = createNode({ type: 'scene', id: 'p', z: 'a0', center: { x: 0, y: 0 }, scene: 'scene:p' });
31
- const copy = cloneShape(portal, createNode({ type: 'scene', id: 'q', z: 'a1', center: { x: 0, y: 0 } }));
30
+ test('a cloned portal opens onto its own scene, and a note gets its type text', ({ expect }) => {
31
+ const portal = createNode({ type: 'frame', id: 'p', z: 'a0', center: { x: 0, y: 0 }, scene: 'scene:p' });
32
+ const copy = cloneShape(portal, createNode({ type: 'frame', id: 'q', z: 'a1', center: { x: 0, y: 0 } }));
32
33
  expect(copy).toMatchObject({ id: 'q', scene: 'q' });
33
34
 
34
- const named = { ...createNode({ type: 'class', id: 'c', z: 'a0', center: { x: 0, y: 0 } }), name: 'Person' };
35
- const fresh = createNode({ type: 'class', id: 'd', z: 'a1', center: { x: 0, y: 0 } });
36
- expect(cloneShape(named, fresh)).toMatchObject({ id: 'd', name: 'Class' });
35
+ const named = { ...createNode({ type: 'note', id: 'c', z: 'a0', center: { x: 0, y: 0 } }), text: 'Mine' };
36
+ const fresh = createNode({ type: 'note', id: 'd', z: 'a1', center: { x: 0, y: 0 } });
37
+ expect(cloneShape(named, fresh, [{ field: 'text', multiline: true }])).toMatchObject({ id: 'd', text: 'Note' });
38
+ });
39
+
40
+ test('flipping a link swaps its ends, keeps its markers and reverses a spline', ({ expect }) => {
41
+ const link: SplineLink = {
42
+ type: 'spline',
43
+ id: 'l',
44
+ z: 'a',
45
+ source: { node: 'a', port: 'e1' },
46
+ target: { point: { x: 100, y: 0 } },
47
+ ends: { end: 'arrow' },
48
+ points: [
49
+ { x: 10, y: 0 },
50
+ { x: 20, y: 5 },
51
+ ],
52
+ };
53
+ const flipped = flipLink(link);
54
+ expect(flipped).toEqual({
55
+ source: { point: { x: 100, y: 0 } },
56
+ target: { node: 'a', port: 'e1' },
57
+ points: [
58
+ { x: 20, y: 5 },
59
+ { x: 10, y: 0 },
60
+ ],
61
+ });
37
62
  });
38
63
  });
@@ -12,19 +12,18 @@ import {
12
12
  type Bounds,
13
13
  type BuiltinNode,
14
14
  type BuiltinNodeType,
15
+ DEFAULT_GRID,
15
16
  type Endpoint,
16
17
  type Link,
17
18
  type LinkEnds,
18
19
  type LinkType,
20
+ type LinkValues,
21
+ MAJOR_GRID_RATIO,
19
22
  type Node,
20
23
  type Point,
21
24
  type Size,
22
- isClassNode,
23
- isEllipseNode,
24
- isNoteNode,
25
- isRectNode,
26
25
  } from '../model/types.ts';
27
- import { partKey } from './parts.ts';
26
+ import { type PartField } from './parts.ts';
28
27
 
29
28
  /** Axis-aligned frame of a node: its size centred on its centre. */
30
29
  export const nodeBounds = (node: Node): Bounds => ({
@@ -43,24 +42,44 @@ export const resizeNode = <N extends Node>(node: N, bounds: Bounds): N => ({
43
42
  size: { width: bounds.width, height: bounds.height },
44
43
  });
45
44
 
46
- /** The bounding box a new basic shape gets: rectangle, ellipse and class share it, so a new circle matches a new square. */
47
- export const DEFAULT_SHAPE_SIZE: Size = { width: 256, height: 256 };
45
+ /** Scene px of one nominal unit (a major grid cell) at the default grid. */
46
+ export const DEFAULT_CELL = DEFAULT_GRID * MAJOR_GRID_RATIO;
48
47
 
48
+ /** A nominal size (in major grid cells) in scene px, for a drawing whose major cell is `cell` px. */
49
+ export const nominalSize = (size: Size, cell: number = DEFAULT_CELL): Size => ({
50
+ width: size.width * cell,
51
+ height: size.height * cell,
52
+ });
53
+
54
+ /**
55
+ * The built-in types' sizes when created, in nominal units: one unit is the drawing's major grid cell, so a
56
+ * new shape scales with the drawing's grid.
57
+ */
58
+ export const NOMINAL_SIZES: Record<BuiltinNodeType, Size> = {
59
+ rect: { width: 2, height: 1 },
60
+ ellipse: { width: 2, height: 2 },
61
+ note: { width: 4, height: 2 },
62
+ frame: { width: 4, height: 2 },
63
+ };
64
+
65
+ /** The built-in types' sizes in scene px, for fixtures and imports, whose layouts are written against them. */
49
66
  export const DEFAULT_SIZES: Record<BuiltinNodeType, Size> = {
50
- rect: DEFAULT_SHAPE_SIZE,
51
- ellipse: DEFAULT_SHAPE_SIZE,
52
- class: DEFAULT_SHAPE_SIZE,
67
+ rect: { width: 256, height: 256 },
68
+ ellipse: { width: 256, height: 256 },
53
69
  note: { width: 256, height: 128 },
54
- scene: { width: 512, height: 256 },
70
+ frame: { width: 512, height: 256 },
55
71
  };
56
72
 
73
+ /** The bounding box a new basic shape gets at the default grid. */
74
+ export const DEFAULT_SHAPE_SIZE: Size = DEFAULT_SIZES.rect;
75
+
57
76
  export type CreateNodeProps = {
58
77
  type: BuiltinNodeType;
59
78
  id: string;
60
79
  z: string;
61
80
  center: Point;
62
81
  size?: Size;
63
- /** A portal's child scene id. */
82
+ /** A frame's child scene id. */
64
83
  scene?: string;
65
84
  };
66
85
 
@@ -79,11 +98,9 @@ export const createNode = ({
79
98
  return { type, id, z, center, size };
80
99
  case 'ellipse':
81
100
  return { type, id, z, center, size };
82
- case 'class':
83
- return { type, id, z, center, size, name: 'Class', attributes: ['id: string'], methods: ['save(): void'] };
84
101
  case 'note':
85
102
  return { type, id, z, center, size, text: 'Note' };
86
- case 'scene':
103
+ case 'frame':
87
104
  return { type, id, z, center, size, scene: scene ?? id };
88
105
  }
89
106
  };
@@ -95,23 +112,12 @@ const OWN_FIELDS = new Set(['id', 'type', 'z', 'center', 'scene']);
95
112
  * A new shape like `source` (its size, look and ports) built on `fresh`, the type's own new node at the
96
113
  * new place: it keeps its own identity and child scene, and the type's text rather than the source's.
97
114
  */
98
- export const cloneShape = (source: Node, fresh: Node): Node => ({
99
- ...fresh,
100
- ...Object.fromEntries(Object.entries(source).filter(([key]) => !OWN_FIELDS.has(key) && !partKey(key))),
101
- });
102
-
103
- /** The node's display text, in the field its type uses for it; a type without one is returned as is. */
104
- export const withLabel = <N extends Node>(node: N, label: string): N => {
105
- if (isRectNode(node) || isEllipseNode(node)) {
106
- return { ...node, label };
107
- }
108
- if (isClassNode(node)) {
109
- return { ...node, name: label };
110
- }
111
- if (isNoteNode(node)) {
112
- return { ...node, text: label };
113
- }
114
- return node;
115
+ export const cloneShape = (source: Node, fresh: Node, parts: readonly PartField[] = []): Node => {
116
+ const text = new Set(parts.map(({ field }) => field));
117
+ return {
118
+ ...fresh,
119
+ ...Object.fromEntries(Object.entries(source).filter(([key]) => !OWN_FIELDS.has(key) && !text.has(key))),
120
+ };
115
121
  };
116
122
 
117
123
  export type CreateLinkProps = {
@@ -122,13 +128,12 @@ export type CreateLinkProps = {
122
128
  target: Endpoint;
123
129
  /** Where a new spline gets its first control point; ignored by the other types. */
124
130
  midpoint?: Point;
125
- directed?: boolean;
126
131
  ends?: LinkEnds;
127
132
  };
128
133
 
129
134
  /** A new link of `type`; a spline starts with one control point so it is editable at once. */
130
- export const createLink = ({ type, id, z, source, target, midpoint, directed, ends }: CreateLinkProps): Link => {
131
- const base = { id, z, source, target, ...(directed ? { directed } : {}), ...(ends ? { ends } : {}) };
135
+ export const createLink = ({ type, id, z, source, target, midpoint, ends }: CreateLinkProps): Link => {
136
+ const base = { id, z, source, target, ...(ends ? { ends } : {}) };
132
137
  switch (type) {
133
138
  case 'line':
134
139
  case 'curve':
@@ -138,3 +143,14 @@ export const createLink = ({ type, id, z, source, target, midpoint, directed, en
138
143
  return { type, ...base, points: midpoint ? [midpoint] : [] };
139
144
  }
140
145
  };
146
+
147
+ /**
148
+ * The `update` values that reverse a link: source and target swap while the markers stay at the start and
149
+ * end, so an arrow comes to point the other way; a spline's control points run the other way, so the drawn
150
+ * path keeps its shape.
151
+ */
152
+ export const flipLink = (link: Link): LinkValues => ({
153
+ source: link.target,
154
+ target: link.source,
155
+ ...(link.type === 'spline' ? { points: [...link.points].reverse() } : {}),
156
+ });
@@ -0,0 +1,54 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import { describe, test } from 'vitest';
6
+
7
+ import { type StyleMap } from '../model/types.ts';
8
+ import { SceneBuilder } from './builder.ts';
9
+ import { classedLink, classedNode, frameClasses, splitClassEdit } from './style.ts';
10
+
11
+ describe('style classes', () => {
12
+ const styles: StyleMap = {
13
+ warn: { id: 'warn', name: 'Warning', style: { hue: 'red', lineStyle: 'dashed', rounded: true } },
14
+ };
15
+ const box = { x: 0, y: 0, width: 200, height: 100 };
16
+ const {
17
+ scenes: [{ nodes, links }],
18
+ } = SceneBuilder.scene('s', [
19
+ SceneBuilder.rect('a', box).properties({ class: 'warn', style: { hue: 'blue' } }),
20
+ SceneBuilder.rect('b', { ...box, x: 400 }).properties({ class: 'gone' }),
21
+ SceneBuilder.link('line', 'a', 'b')
22
+ .id('l')
23
+ .properties({ class: 'warn', style: { lineStyle: 'solid' } }),
24
+ ]).build();
25
+
26
+ test('an element takes its class look under its own', ({ expect }) => {
27
+ expect(classedNode(nodes.a, styles).style).toEqual({ hue: 'blue', lineStyle: 'dashed', rounded: true });
28
+ // A link takes only the common base of the class's style: its colour and line style.
29
+ expect(classedLink(links.l, styles).style).toEqual({ hue: 'red', lineStyle: 'solid' });
30
+ });
31
+
32
+ test("a node's line style draws its border", ({ expect }) => {
33
+ expect(frameClasses(classedNode(nodes.a, styles), false)).toEqual(expect.arrayContaining(['border-dashed']));
34
+ });
35
+
36
+ test('a missing class leaves the element as it is', ({ expect }) => {
37
+ expect(classedNode(nodes.b, styles)).toBe(nodes.b);
38
+ });
39
+
40
+ test('an edit of a classed look goes to the class, and the element stops overriding it', ({ expect }) => {
41
+ // A shows blue over the class's red; the user picks green and turns rounding off.
42
+ const shown = classedNode(nodes.a, styles).style;
43
+ const edited = { hue: 'green', rounded: false };
44
+ expect(splitClassEdit(shown, edited, styles.warn.style ?? {}, nodes.a.style)).toEqual({
45
+ classLook: { hue: 'green', lineStyle: 'dashed', rounded: false },
46
+ own: {},
47
+ });
48
+ // An unchanged field stays where it was: A keeps its own blue, the class its red.
49
+ expect(splitClassEdit(shown, { ...shown, rounded: false }, styles.warn.style ?? {}, nodes.a.style)).toEqual({
50
+ classLook: { hue: 'red', lineStyle: 'dashed', rounded: false },
51
+ own: { hue: 'blue' },
52
+ });
53
+ });
54
+ });
@@ -7,7 +7,20 @@
7
7
  // every hue is spelled out here rather than composed from the hue name.
8
8
  //
9
9
 
10
- import { type Node, type NodeStyle, type NodeTone, isEllipseNode } from '../model/types.ts';
10
+ import {
11
+ type HorizontalAlign,
12
+ type Link,
13
+ type Node,
14
+ type NodeStyle,
15
+ type NodeTone,
16
+ STYLE_HUES,
17
+ type StyleHue,
18
+ type StyleMap,
19
+ type VerticalAlign,
20
+ isEllipseNode,
21
+ isFrameNode,
22
+ showsContents,
23
+ } from '../model/types.ts';
11
24
 
12
25
  export type HueClasses = { surface: string; text: string; border: string };
13
26
 
@@ -32,14 +45,10 @@ const HUES: Record<string, HueClasses> = {
32
45
  rose: { surface: 'bg-rose-surface', text: 'text-rose-fg', border: 'border-rose-border' },
33
46
  };
34
47
 
35
- /** The hues the style picker offers, neutral first; any theme hue still renders, at its `medium` tone. */
36
- export const STYLE_HUES = ['neutral', 'red', 'orange', 'amber', 'green', 'teal', 'blue', 'violet', 'pink'] as const;
37
- export type StyleHue = (typeof STYLE_HUES)[number];
38
-
39
48
  export const TONES: readonly NodeTone[] = [0, 1, 2, 3];
40
49
 
41
50
  /** What each tone is called, for labels. */
42
- export const TONE_NAMES: Record<NodeTone, string> = { 0: 'outline', 1: 'light', 2: 'medium', 3: 'strong' };
51
+ export const TONE_NAMES: Record<NodeTone, string> = { 0: 'outline', 1: 'strong', 2: 'medium', 3: 'light' };
43
52
 
44
53
  /** The tone a hue without one draws at: the look a hue had before tones. */
45
54
  export const DEFAULT_TONE: NodeTone = 2;
@@ -47,56 +56,56 @@ export const DEFAULT_TONE: NodeTone = 2;
47
56
  type ToneClasses = { surface: string; text: string };
48
57
 
49
58
  /**
50
- * The fills lighter and stronger than a hue's `surface` role: the scale's 200 under its darkest text, and the
51
- * hue's solid `bg` role under light text (tones 1 and 3). Tone 2 is the role pair in `HUES`; tone 0 has no fill.
59
+ * The fills either side of a hue's `surface` role (its 400): the scale's 500 under light text (tone 1), and its
60
+ * 300 under its darkest text (tone 3). Tone 2 is the role pair in `HUES`; tone 0 has no fill.
52
61
  */
53
- const TONE_FILLS: Record<StyleHue, Record<1 | 3, ToneClasses>> = {
62
+ const TONE_FILLS: Record<StyleHue, Record<'strong' | 'light', ToneClasses>> = {
54
63
  neutral: {
55
- 1: { surface: 'bg-neutral-200', text: 'text-neutral-900' },
56
- 3: { surface: 'bg-neutral-bg', text: 'text-neutral-50' },
64
+ light: { surface: 'bg-neutral-300', text: 'text-neutral-900' },
65
+ strong: { surface: 'bg-neutral-500', text: 'text-neutral-50' },
57
66
  },
58
67
  red: {
59
- 1: { surface: 'bg-red-200', text: 'text-red-900' },
60
- 3: { surface: 'bg-red-bg', text: 'text-neutral-50' },
68
+ light: { surface: 'bg-red-300', text: 'text-red-900' },
69
+ strong: { surface: 'bg-red-500', text: 'text-neutral-50' },
61
70
  },
62
71
  orange: {
63
- 1: { surface: 'bg-orange-200', text: 'text-orange-900' },
64
- 3: { surface: 'bg-orange-bg', text: 'text-neutral-50' },
72
+ light: { surface: 'bg-orange-300', text: 'text-orange-900' },
73
+ strong: { surface: 'bg-orange-500', text: 'text-neutral-50' },
65
74
  },
66
75
  amber: {
67
- 1: { surface: 'bg-amber-200', text: 'text-amber-900' },
68
- 3: { surface: 'bg-amber-bg', text: 'text-neutral-50' },
76
+ light: { surface: 'bg-amber-300', text: 'text-amber-900' },
77
+ strong: { surface: 'bg-amber-500', text: 'text-neutral-50' },
69
78
  },
70
79
  green: {
71
- 1: { surface: 'bg-green-200', text: 'text-green-900' },
72
- 3: { surface: 'bg-green-bg', text: 'text-neutral-50' },
80
+ light: { surface: 'bg-green-300', text: 'text-green-900' },
81
+ strong: { surface: 'bg-green-500', text: 'text-neutral-50' },
73
82
  },
74
83
  teal: {
75
- 1: { surface: 'bg-teal-200', text: 'text-teal-900' },
76
- 3: { surface: 'bg-teal-bg', text: 'text-neutral-50' },
84
+ light: { surface: 'bg-teal-300', text: 'text-teal-900' },
85
+ strong: { surface: 'bg-teal-500', text: 'text-neutral-50' },
86
+ },
87
+ sky: {
88
+ light: { surface: 'bg-sky-300', text: 'text-sky-900' },
89
+ strong: { surface: 'bg-sky-500', text: 'text-neutral-50' },
77
90
  },
78
91
  blue: {
79
- 1: { surface: 'bg-blue-200', text: 'text-blue-900' },
80
- 3: { surface: 'bg-blue-bg', text: 'text-neutral-50' },
92
+ light: { surface: 'bg-blue-300', text: 'text-blue-900' },
93
+ strong: { surface: 'bg-blue-500', text: 'text-neutral-50' },
81
94
  },
82
95
  violet: {
83
- 1: { surface: 'bg-violet-200', text: 'text-violet-900' },
84
- 3: { surface: 'bg-violet-bg', text: 'text-neutral-50' },
85
- },
86
- pink: {
87
- 1: { surface: 'bg-pink-200', text: 'text-pink-900' },
88
- 3: { surface: 'bg-pink-bg', text: 'text-neutral-50' },
96
+ light: { surface: 'bg-violet-300', text: 'text-violet-900' },
97
+ strong: { surface: 'bg-violet-500', text: 'text-neutral-50' },
89
98
  },
90
99
  };
91
100
 
92
- const isStyleHue = (hue: string): hue is StyleHue => STYLE_HUES.some((candidate) => candidate === hue);
101
+ const isStyleHue = (hue: string | undefined): hue is StyleHue => STYLE_HUES.some((candidate) => candidate === hue);
93
102
 
94
103
  /** The default look: the base surface, the base text and the separator border. */
95
104
  const DEFAULT: HueClasses = { surface: 'bg-base-surface', text: '', border: 'border-separator' };
96
105
 
97
106
  /**
98
107
  * The classes a hue at a tone draws with. Every tone keeps the hue's border; tone 0 drops the fill and
99
- * keeps the default text, and a tone a hue has no fills for draws as `medium`.
108
+ * keeps the default text, and a tone a hue has no fills for draws as `medium` (its role pair).
100
109
  */
101
110
  export const hueClasses = (hue: string | undefined, tone: NodeTone = DEFAULT_TONE): HueClasses => {
102
111
  const base = (hue && HUES[hue]) || DEFAULT;
@@ -107,11 +116,38 @@ export const hueClasses = (hue: string | undefined, tone: NodeTone = DEFAULT_TON
107
116
  return { surface: 'bg-transparent', text: '', border: base.border };
108
117
  }
109
118
  if ((tone === 1 || tone === 3) && isStyleHue(hue)) {
110
- return { ...TONE_FILLS[hue][tone], border: base.border };
119
+ return { ...TONE_FILLS[hue][tone === 1 ? 'strong' : 'light'], border: base.border };
111
120
  }
112
121
  return base;
113
122
  };
114
123
 
124
+ /** `base` with every field `own` sets over it; an `undefined` field of `own` is unset, so it leaves the base's. */
125
+ const overlay = <T extends object>(base: T | undefined, own: T | undefined): T | undefined => {
126
+ if (!base || !own) {
127
+ return own ?? base;
128
+ }
129
+ const merged = { ...base };
130
+ for (const [key, value] of Object.entries(own)) {
131
+ if (value !== undefined) {
132
+ Reflect.set(merged, key, value);
133
+ }
134
+ }
135
+ return merged;
136
+ };
137
+
138
+ /** A node as drawn: its class's style under its own (`StyleClass`). */
139
+ export const classedNode = (node: Node, styles: StyleMap | undefined): Node => {
140
+ const style = node.class ? styles?.[node.class]?.style : undefined;
141
+ return style ? { ...node, style: overlay(style, node.style) } : node;
142
+ };
143
+
144
+ /** A link as drawn: its class's colour and line style under its own. */
145
+ export const classedLink = (link: Link, styles: StyleMap | undefined): Link => {
146
+ const style = link.class ? styles?.[link.class]?.style : undefined;
147
+ // A link draws only the common base (`LineStyle`) of its class's style.
148
+ return style ? { ...link, style: overlay({ hue: style.hue, lineStyle: style.lineStyle }, link.style) } : link;
149
+ };
150
+
115
151
  /**
116
152
  * A node's style with the defaults the frame draws spelled out: an unset `fill` or `border` is drawn, so
117
153
  * the properties panel must show it as on rather than as an unset (off) toggle.
@@ -124,24 +160,101 @@ export const resolveStyle = (style: NodeStyle = {}): NodeStyle => ({
124
160
 
125
161
  /**
126
162
  * Frame classes for a node: fill and text colour, border and corner radius from its style; a guide is
127
- * dashed and unfilled, and the host's `className` comes last so it wins.
163
+ * dashed and unfilled.
128
164
  */
129
165
  export const frameClasses = (node: Node, selected: boolean, hovered = false): string[] => {
130
166
  const style = resolveStyle(node.style);
131
167
  const hue = hueClasses(style.hue, style.tone);
132
168
  const filled = style.fill && !style.guide;
169
+ // A scene shape drawing its contents is a window onto another canvas, so it is opaque even in outline: the
170
+ // grid behind it would read as part of the child scene.
171
+ const opaque = isFrameNode(node) && showsContents(node);
172
+ const clear = !filled || hue.surface === 'bg-transparent';
173
+ const surface = opaque && clear ? 'bg-base-surface' : filled ? hue.surface : '';
133
174
  return [
134
- filled ? hue.surface : '',
135
- hue.text,
175
+ surface,
176
+ // A guide's hue marks its edge (a group's backdrop); its title reads in the default text colour.
177
+ style.guide ? '' : hue.text,
136
178
  selected
137
- ? 'border-primary-500'
179
+ ? 'border-focus'
138
180
  : hovered
139
- ? 'border-primary-500/50'
140
- : style.border === false && !style.guide
181
+ ? 'border-focus/50'
182
+ : // A frame always shows its edge: it bounds a scene or an embedded object, which has none of its own.
183
+ style.border === false && !style.guide && !isFrameNode(node)
141
184
  ? 'border-transparent'
142
185
  : hue.border,
143
- style.guide ? 'border-dashed' : '',
186
+ style.guide || style.lineStyle === 'dashed' ? 'border-dashed' : style.lineStyle === 'dotted' ? 'border-dotted' : '',
144
187
  isEllipseNode(node) ? 'rounded-[50%]' : style.rounded ? 'rounded-2xl' : 'rounded-sm',
145
- style.className ?? '',
146
188
  ];
147
189
  };
190
+
191
+ export type LineClasses = { stroke: string; fill: string };
192
+
193
+ /** A link's stroke and its end markers' fill, in the hue's border colour so a link matches a node of its hue. */
194
+ const LINE_CLASSES: Record<StyleHue, LineClasses> = {
195
+ neutral: { stroke: 'stroke-neutral-border', fill: 'fill-neutral-border' },
196
+ red: { stroke: 'stroke-red-border', fill: 'fill-red-border' },
197
+ orange: { stroke: 'stroke-orange-border', fill: 'fill-orange-border' },
198
+ amber: { stroke: 'stroke-amber-border', fill: 'fill-amber-border' },
199
+ green: { stroke: 'stroke-green-border', fill: 'fill-green-border' },
200
+ teal: { stroke: 'stroke-teal-border', fill: 'fill-teal-border' },
201
+ sky: { stroke: 'stroke-sky-border', fill: 'fill-sky-border' },
202
+ blue: { stroke: 'stroke-blue-border', fill: 'fill-blue-border' },
203
+ violet: { stroke: 'stroke-violet-border', fill: 'fill-violet-border' },
204
+ };
205
+
206
+ /** The default line: the grey every link drew before lines took a hue. */
207
+ const DEFAULT_LINE: LineClasses = { stroke: 'stroke-neutral-500', fill: 'fill-neutral-500' };
208
+
209
+ export const lineClasses = (hue: string | undefined): LineClasses =>
210
+ isStyleHue(hue) ? LINE_CLASSES[hue] : DEFAULT_LINE;
211
+
212
+ /**
213
+ * Splits an edit of a classed element's look (`edited`, against what the panel `shown`): every field that changed
214
+ * goes to the class, so each element of the class derives it, and the element drops its own value for that field,
215
+ * which would otherwise hide the class's.
216
+ */
217
+ export const splitClassEdit = <T extends object>(
218
+ shown: object | undefined,
219
+ edited: unknown,
220
+ classLook: T,
221
+ own: T | undefined,
222
+ ): { classLook: T; own: T | undefined } => {
223
+ const nextClass = { ...classLook };
224
+ const nextOwn = own ? { ...own } : undefined;
225
+ if (typeof edited === 'object' && edited !== null) {
226
+ for (const [key, value] of Object.entries(edited)) {
227
+ if (Reflect.get(shown ?? {}, key) !== value) {
228
+ Reflect.set(nextClass, key, value);
229
+ if (nextOwn) {
230
+ Reflect.deleteProperty(nextOwn, key);
231
+ }
232
+ }
233
+ }
234
+ }
235
+ return { classLook: nextClass, own: nextOwn };
236
+ };
237
+
238
+ const HORIZONTAL_CLASSES: Record<HorizontalAlign, string> = {
239
+ left: 'items-start text-left',
240
+ center: 'items-center text-center',
241
+ right: 'items-end text-right',
242
+ };
243
+
244
+ const VERTICAL_CLASSES: Record<VerticalAlign, string> = {
245
+ top: 'justify-start',
246
+ middle: 'justify-center',
247
+ bottom: 'justify-end',
248
+ };
249
+
250
+ /**
251
+ * Classes placing a text part in its shape by the style's alignment, else by the type's own (`defaults`): the part
252
+ * is a column, so `justify` places it down the shape and `items` across it.
253
+ */
254
+ export const alignClasses = (
255
+ style: NodeStyle | undefined,
256
+ defaults: { horizontal: HorizontalAlign; vertical: VerticalAlign },
257
+ ): string =>
258
+ `flex flex-col ${HORIZONTAL_CLASSES[style?.alignHorizontal ?? defaults.horizontal]} ${
259
+ VERTICAL_CLASSES[style?.alignVertical ?? defaults.vertical]
260
+ }`;
@@ -8,7 +8,7 @@ import { solve } from '../model/projections/constrained.ts';
8
8
  import { layoutGraph } from '../model/projections/dynamic.ts';
9
9
  import { MAJOR_GRID, type Point, type Scene } from '../model/types.ts';
10
10
  import { nodeBounds } from './shapes.ts';
11
- import { createClassSceneTree, createSceneTree } from './testing.ts';
11
+ import { createModelSceneTree, createSceneTree } from './testing.ts';
12
12
 
13
13
  /** Every edge of every node lies on the grid, which is what move and resize snap to. */
14
14
  const offGrid = (scene: Scene): string[] =>
@@ -42,8 +42,8 @@ describe('initial layouts conform to the major grid', () => {
42
42
  }
43
43
  });
44
44
 
45
- test('class scene tree fixture', ({ expect }) => {
46
- const { scenes, root } = createClassSceneTree();
45
+ test('model scene tree fixture', ({ expect }) => {
46
+ const { scenes, root } = createModelSceneTree();
47
47
  expect(scenes.map(({ id }) => id)).toContain(root);
48
48
  // Three levels: the root, its two subsystems, and one leaf under each.
49
49
  expect(scenes).toHaveLength(5);