@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
@@ -10,39 +10,63 @@
10
10
  //
11
11
 
12
12
  import { useAtomValue } from '@effect/atom-react/Hooks';
13
+ import type * as Atom from 'effect/reactivity/Atom';
13
14
  import type * as Schema from 'effect/Schema';
14
15
  import React, { useCallback, useMemo } from 'react';
15
16
 
17
+ import { type Database } from '@dxos/echo';
16
18
  import {
17
19
  Form,
18
20
  type FormFieldMap,
19
21
  type FormFieldOverride,
20
22
  type FormFieldRenderer,
21
23
  type FormUpdateMeta,
24
+ type RefFieldDataProps,
22
25
  } from '@dxos/react-ui-form';
26
+ import * as Button from '@dxos/react-ui/Button';
23
27
  import * as Input from '@dxos/react-ui/Input';
28
+ import * as Panel from '@dxos/react-ui/Panel';
29
+ import * as Toolbar from '@dxos/react-ui/Toolbar';
24
30
  import type * as Util from '@dxos/react-ui/Util';
25
- import { mx } from '@dxos/ui-theme';
26
31
 
32
+ import { useRegistry } from '../../hooks/index.ts';
33
+ import { SCENE_OVERLAY_ATTRIBUTE } from '../../hooks/useWheel.ts';
27
34
  import { type SceneViewAtoms } from '../../model/atoms.ts';
28
35
  import { nodeDef } from '../../model/node-def.ts';
29
36
  import { type Projection } from '../../model/projection.ts';
30
37
  import { type NodeRegistry, defaultNodeRegistry } from '../../model/registry.ts';
38
+ import { NO_STYLES } from '../../model/store.ts';
31
39
  import {
40
+ BaseNode,
32
41
  CurveLink,
33
42
  type Element,
43
+ type Layer,
34
44
  LineLink,
35
45
  type LinkType,
36
- NodeBase,
46
+ type Node,
47
+ type NodeStyle,
37
48
  SmartLink,
38
49
  SplineLink,
50
+ type StyleClass,
51
+ type StyleMap,
39
52
  getElement,
53
+ isFrameNode,
40
54
  isLink,
55
+ isNoteNode,
56
+ showsContents,
41
57
  } from '../../model/types.ts';
58
+ import { elementLayer, sceneLayers } from '../../utils/layers.ts';
42
59
  import { MAX_PORTS_PER_SIDE, portsPerSideOf } from '../../utils/ports.ts';
43
60
  import { commonSchema, mergeValues, patchValues } from '../../utils/properties.ts';
44
- import { resolveStyle } from '../../utils/style.ts';
45
- import { StyleGridField } from './StyleGrid.tsx';
61
+ import { type SceneOption } from '../../utils/scenes.ts';
62
+ import { flipLink } from '../../utils/shapes.ts';
63
+ import { classedLink, classedNode, resolveStyle, splitClassEdit } from '../../utils/style.ts';
64
+ import { AlignField } from './AlignField.tsx';
65
+ import { StyleClassesContext } from './ClassField.tsx';
66
+ import { FontField } from './FontField.tsx';
67
+ import { LayerClassField, LayersContext } from './LayerField.tsx';
68
+ import { SceneField, SceneOptionsContext } from './SceneField.tsx';
69
+ import { OutlineStyleField, StyleGridField } from './StyleGrid.tsx';
46
70
 
47
71
  /** Identity, ordering and geometry lists are the surface's, not the user's. */
48
72
  const HIDDEN = ['id', 'type', 'z', 'ports', 'points', 'source', 'target'];
@@ -52,7 +76,7 @@ const HIDDEN = ['id', 'type', 'z', 'ports', 'points', 'source', 'target'];
52
76
  * beats the generic array field's row of inputs. Blank lines survive while typing (they round-trip
53
77
  * through split/join) and are dropped on blur, when the form saves.
54
78
  */
55
- const LinesField: FormFieldRenderer = ({ type, label, jsonPath, readonly, getValue, onValueChange, onBlur }) => {
79
+ export const LinesField: FormFieldRenderer = ({ type, label, jsonPath, readonly, getValue, onValueChange, onBlur }) => {
56
80
  const lines: string[] = getValue() ?? [];
57
81
  return (
58
82
  <Form.Field path={jsonPath} label={label} readonly={readonly}>
@@ -74,11 +98,13 @@ const LinesField: FormFieldRenderer = ({ type, label, jsonPath, readonly, getVal
74
98
  );
75
99
  };
76
100
 
77
- /** Renderers by field path for the built-in types' list fields and the style grid; a host may pass its own. */
101
+ /** Renderers by field path the panel always uses (the style grid); node types add their own (`NodeDef.fields`). */
78
102
  export const DEFAULT_FIELDS: FormFieldMap = {
79
- 'attributes': LinesField,
80
- 'methods': LinesField,
81
103
  'style.hue': StyleGridField,
104
+ 'style.alignHorizontal': AlignField,
105
+ 'style.fontFamily': FontField,
106
+ 'scene': SceneField,
107
+ 'layer': LayerClassField,
82
108
  };
83
109
 
84
110
  const LINK_SCHEMAS: Record<LinkType, Schema.Codec<any, any>> = {
@@ -88,18 +114,35 @@ const LINK_SCHEMAS: Record<LinkType, Schema.Codec<any, any>> = {
88
114
  smart: SmartLink,
89
115
  };
90
116
 
91
- /** An element's own schema; a node type the registry does not know gets the shared fields. */
117
+ /** An element's own schema; a type the registry does not know gets the core base's (`BaseNode`, or a line). */
92
118
  const schemaOf = (nodes: NodeRegistry, element: Element): Schema.Codec<any, any> =>
93
- isLink(element) ? LINK_SCHEMAS[element.type] : (nodeDef(nodes, element)?.schema ?? NodeBase);
119
+ isLink(element) ? (LINK_SCHEMAS[element.type] ?? LineLink) : (nodeDef(nodes, element)?.schema ?? BaseNode);
120
+
121
+ /** Where a type places its text unless styled otherwise: a note from the top left, a label in the middle. */
122
+ const textAlign = (node: Node): Pick<NodeStyle, 'alignHorizontal' | 'alignVertical'> =>
123
+ isNoteNode(node)
124
+ ? { alignHorizontal: 'left', alignVertical: 'top' }
125
+ : { alignHorizontal: 'center', alignVertical: 'middle' };
94
126
 
95
127
  /**
96
- * What the form shows for an element: what the frame draws, so an unset fill or border reads as on and an
97
- * unset port count as the type's.
128
+ * What the form shows for an element: what the view draws, so its class's look, an unset fill, border or Show
129
+ * contents read as they look and an unset port count as the type's.
98
130
  */
99
- const formValues = (nodes: NodeRegistry, element: Element): Record<string, unknown> =>
131
+ const formValues = (
132
+ nodes: NodeRegistry,
133
+ element: Element,
134
+ styles: StyleMap,
135
+ layers: readonly Layer[],
136
+ ): Record<string, unknown> =>
100
137
  isLink(element)
101
- ? element
102
- : { ...element, style: resolveStyle(element.style), portsPerSide: portsPerSideOf(nodes, element) };
138
+ ? { ...element, layer: elementLayer(element, layers), style: classedLink(element, styles).style }
139
+ : {
140
+ ...element,
141
+ layer: elementLayer(element, layers),
142
+ style: { ...textAlign(element), ...resolveStyle(classedNode(element, styles).style) },
143
+ portsPerSide: portsPerSideOf(nodes, element),
144
+ ...(isFrameNode(element) ? { contents: showsContents(element) } : {}),
145
+ };
103
146
 
104
147
  /**
105
148
  * How the panel presents fields. Ranges are the editor's, not the model's: a check on the stored schema would
@@ -109,8 +152,11 @@ const formValues = (nodes: NodeRegistry, element: Element): Record<string, unkno
109
152
  const FIELD_OVERRIDES: Record<string, FormFieldOverride> = {
110
153
  'style.hue': { label: 'Style' },
111
154
  'style.tone': { hidden: true },
112
- 'style.fontSize': { min: 8, max: 80, step: 1 },
155
+ 'style.alignVertical': { hidden: true },
156
+ 'style.fontSize': { hidden: true },
113
157
  'portsPerSide': { min: 1, max: MAX_PORTS_PER_SIDE, step: 1 },
158
+ 'scene': { label: 'Scene' },
159
+ 'layer': { label: 'Layer' },
114
160
  };
115
161
 
116
162
  const plural = (count: number, noun: string) => `${count} ${noun}${count === 1 ? '' : 's'}`;
@@ -129,6 +175,20 @@ export type PropertiesProps = Util.ThemedClassName<{
129
175
  fields?: FormFieldMap;
130
176
  /** Show the fields without letting them change; also implied by a projection that cannot `update`. */
131
177
  readonly?: boolean;
178
+ /** The database a reference field picks from; without it reference fields are read-only. */
179
+ db?: Database.Database;
180
+ /** Narrows a reference field's candidates (e.g. to objects of one kind). */
181
+ getOptions?: RefFieldDataProps['getOptions'];
182
+ /** A host's per-selection field overrides (e.g. a field it allows only in some states), over the panel's own. */
183
+ overrides?: (elements: readonly Element[]) => Record<string, FormFieldOverride>;
184
+ /** The scenes a scene shape may open (`sceneOptions`); without them its scene is not editable here. */
185
+ sceneOptions?: readonly SceneOption[];
186
+ /** The drawing's style classes (`SceneStore.styles`); without them elements take no class. */
187
+ styles?: Atom.Writable<StyleMap>;
188
+ /** Moves the selection into a new scene (`groupIntoScene`); offered for a selection of several elements. */
189
+ onGroup?: () => void;
190
+ /** Whether the panel is docked beside the canvas. */
191
+ docked?: boolean;
132
192
  }>;
133
193
 
134
194
  export const Properties = ({
@@ -136,83 +196,203 @@ export const Properties = ({
136
196
  projection,
137
197
  atoms,
138
198
  nodes = defaultNodeRegistry,
139
- fields = DEFAULT_FIELDS,
199
+ fields,
140
200
  readonly: readonlyProp = false,
201
+ db,
202
+ getOptions,
203
+ overrides,
204
+ sceneOptions,
205
+ styles,
206
+ onGroup,
207
+ docked = false,
141
208
  }: PropertiesProps) => {
209
+ const registry = useRegistry();
142
210
  const scene = useAtomValue(projection.scene);
143
211
  const selection = useAtomValue(atoms.selection);
144
212
  const elements = useMemo(() => [...selection].flatMap((id) => getElement(scene, id) ?? []), [scene, selection]);
145
213
  const readonly = readonlyProp || !projection.capabilities.update;
214
+ const styleMap = useAtomValue(styles ?? NO_STYLES);
215
+ const layers = useMemo(() => sceneLayers(scene), [scene]);
146
216
 
147
217
  // Keyed by the selected types, so the schema (and the form built on it) holds while values change under it.
148
218
  const typesKey = [...new Set(elements.map((element) => element.type))].sort().join();
149
219
  // eslint-disable-next-line react-hooks/exhaustive-deps
150
220
  const schema = useMemo(() => commonSchema(elements.map((element) => schemaOf(nodes, element))), [typesKey, nodes]);
151
221
  const { values, fieldOverrides } = useMemo(() => {
152
- const shown = elements.map((element) => formValues(nodes, element));
222
+ const shown = elements.map((element) => formValues(nodes, element, styleMap, layers));
153
223
  const { values, mixed } = mergeValues(shown, Object.keys(shown[0] ?? {}));
154
224
  // A value the elements disagree on shows as indeterminate until it is edited, then applies to all of them.
155
- const fieldOverrides: Record<string, FormFieldOverride> = { ...FIELD_OVERRIDES };
225
+ const fieldOverrides: Record<string, FormFieldOverride> = {
226
+ ...FIELD_OVERRIDES,
227
+ ...(sceneOptions ? {} : { scene: { hidden: true } }),
228
+ // The class is edited in the layer's row (`LayerClassField`).
229
+ class: { hidden: true },
230
+ ...overrides?.(elements),
231
+ };
156
232
  for (const path of mixed) {
157
233
  fieldOverrides[path] = { ...fieldOverrides[path], indeterminate: true };
158
234
  }
159
235
  return { values, fieldOverrides };
160
- }, [elements, nodes]);
236
+ }, [elements, nodes, overrides, sceneOptions, styles, styleMap, layers]);
161
237
 
162
238
  const onSave = useCallback(
163
239
  (values: Record<string, unknown>, { changed }: FormUpdateMeta<Record<string, unknown>>) => {
164
240
  const paths = Object.keys(changed).filter((path) => changed[path as keyof typeof changed]);
165
- projection.apply({
166
- kind: 'batch',
167
- intents: elements.map((element) => ({
168
- kind: 'update' as const,
169
- id: element.id,
170
- values: patchValues(formValues(nodes, element), values, paths),
171
- })),
241
+ // A classed element's look is its class's: an edit of it restyles the class, and so every element deriving it.
242
+ const classes: Record<string, StyleClass> = {};
243
+ const intents = elements.map((element) => {
244
+ const shown = formValues(nodes, element, styleMap, layers);
245
+ const patch = patchValues(shown, values, paths);
246
+ const styleClass = styles && element.class ? (classes[element.class] ?? styleMap[element.class]) : undefined;
247
+ // Taking a class drops the element's own look, so it derives the class's whole rather than in part.
248
+ if (typeof patch.class === 'string') {
249
+ return { kind: 'update' as const, id: element.id, values: { ...patch, style: undefined } };
250
+ }
251
+ if (!styleClass || !('style' in patch)) {
252
+ return { kind: 'update' as const, id: element.id, values: patch };
253
+ }
254
+ // A link's look is the common base of its class's style; a node's, the whole of it.
255
+ const look = isLink(element)
256
+ ? classedLink(element, styleMap).style
257
+ : resolveStyle(classedNode(element, styleMap).style);
258
+ const split = splitClassEdit(look, patch.style, styleClass.style ?? {}, element.style);
259
+ classes[styleClass.id] = { ...styleClass, style: split.classLook };
260
+ return { kind: 'update' as const, id: element.id, values: { ...patch, style: split.own } };
172
261
  });
262
+ if (styles && Object.keys(classes).length > 0) {
263
+ registry.set(styles, { ...styleMap, ...classes });
264
+ }
265
+ projection.apply({ kind: 'batch', intents });
173
266
  },
174
- [projection, elements, nodes],
267
+ [projection, elements, nodes, styles, styleMap, layers, registry],
268
+ );
269
+
270
+ // A new class takes the selected element's look, and the element then follows it rather than carrying its own.
271
+ const single = elements.length === 1 ? elements[0] : undefined;
272
+ const onCreateClass = useCallback(() => {
273
+ if (!styles || !single) {
274
+ return;
275
+ }
276
+ const id = `class-${crypto.randomUUID()}`;
277
+ const name = `Class ${Object.keys(styleMap).length + 1}`;
278
+ const style = isLink(single) ? classedLink(single, styleMap).style : classedNode(single, styleMap).style;
279
+ const styleClass: StyleClass = { id, name, ...(style ? { style } : {}) };
280
+ registry.set(styles, { ...styleMap, [id]: styleClass });
281
+ projection.apply({ kind: 'update', id: single.id, values: { class: id, style: undefined } });
282
+ }, [styles, single, styleMap, registry, projection]);
283
+
284
+ // Reverses every selected link (source and target swap; an arrow comes to point the other way), as one batch.
285
+ const links = useMemo(() => elements.filter(isLink), [elements]);
286
+ const onFlip = useCallback(() => {
287
+ projection.apply({
288
+ kind: 'batch',
289
+ intents: links.map((link) => ({ kind: 'update' as const, id: link.id, values: flipLink(link) })),
290
+ });
291
+ // A spline's control points run the other way after a flip, so a selected point's index names another one.
292
+ registry.set(atoms.point, undefined);
293
+ }, [projection, links, registry, atoms.point]);
294
+
295
+ // The selected node types' own renderers over the panel's; a host's `fields` win over both. One style picker for
296
+ // every kind: its fill rows only when every element is a node, since a link has a colour but no fill.
297
+ const fieldMap = useMemo(
298
+ () => ({
299
+ ...DEFAULT_FIELDS,
300
+ ...(elements.some(isLink) ? { 'style.hue': OutlineStyleField } : {}),
301
+ ...Object.assign(
302
+ {},
303
+ ...elements.map((element) => (isLink(element) ? {} : (nodeDef(nodes, element)?.fields ?? {}))),
304
+ ),
305
+ ...fields,
306
+ }),
307
+ // eslint-disable-next-line react-hooks/exhaustive-deps
308
+ [typesKey, nodes, fields],
175
309
  );
176
310
 
177
311
  if (elements.length === 0) {
178
312
  return (
179
- <div className={mx('flex flex-col overflow-hidden', classNames)} data-testid='properties'>
180
- <div className='p-2 text-sm text-fg-muted'>Select a node or link to edit its properties.</div>
181
- </div>
313
+ <Panel.Root classNames={classNames} data-testid='properties' {...{ [SCENE_OVERLAY_ATTRIBUTE]: true }}>
314
+ <Panel.Body>
315
+ <p className='p-2 text-sm text-fg-muted'>Select a node or link to edit its properties.</p>
316
+ </Panel.Body>
317
+ </Panel.Root>
182
318
  );
183
319
  }
184
320
 
185
321
  const summary = elements.length > 1 && `${describeSelection(elements)}${schema ? '' : ' — no shared properties'}`;
186
322
  return (
187
- <div className={mx('flex flex-col overflow-hidden', classNames)} data-testid='properties'>
188
- {schema ? (
189
- <Form.Root
190
- key={[...selection].join()}
191
- schema={schema}
192
- values={values}
193
- fieldOverrides={fieldOverrides}
194
- fieldMap={fields}
195
- readonly={readonly}
196
- autoSave
197
- onSave={onSave}
198
- >
199
- {/* Scrolling: the panel is as tall as its host, and a long form (a class with many members) scrolls inside it. */}
200
- <Form.Viewport scroll>
201
- <Form.Content>
202
- <Form.Fields exclude={HIDDEN} />
203
- {summary && (
204
- <p className='text-sm text-fg-muted' data-testid='properties-summary'>
205
- {summary}
206
- </p>
207
- )}
208
- </Form.Content>
209
- </Form.Viewport>
210
- </Form.Root>
211
- ) : (
212
- <p className='p-2 text-sm text-fg-muted' data-testid='properties-summary'>
323
+ <Panel.Root classNames={classNames} data-testid='properties' {...{ [SCENE_OVERLAY_ATTRIBUTE]: true }}>
324
+ <Panel.Header asChild>
325
+ <Toolbar.Root data-testid='properties-toolbar'>
326
+ {onGroup && elements.length > 1 && elements.some((element) => !isLink(element)) && (
327
+ <Button.Root
328
+ variant='ghost'
329
+ iconOnly
330
+ icon='ph--frame-corners--regular'
331
+ label='New scene from selection'
332
+ disabled={readonly}
333
+ data-testid='properties-group'
334
+ onClick={onGroup}
335
+ />
336
+ )}
337
+ {styles && single && (
338
+ <Button.Root
339
+ variant='ghost'
340
+ iconOnly
341
+ icon='ph--swatches--regular'
342
+ label='New class from selection'
343
+ disabled={readonly}
344
+ data-testid='properties-new-class'
345
+ onClick={onCreateClass}
346
+ />
347
+ )}
348
+ {links.length > 0 && links.length === elements.length && (
349
+ <Button.Root
350
+ variant='ghost'
351
+ iconOnly
352
+ icon='ph--arrows-left-right--regular'
353
+ label='Flip direction'
354
+ disabled={readonly}
355
+ data-testid='properties-flip'
356
+ onClick={onFlip}
357
+ />
358
+ )}
359
+ </Toolbar.Root>
360
+ </Panel.Header>
361
+ <Panel.Body>
362
+ {schema && (
363
+ <LayersContext.Provider value={layers}>
364
+ <StyleClassesContext.Provider value={styles ? styleMap : undefined}>
365
+ <SceneOptionsContext.Provider value={sceneOptions ?? []}>
366
+ <Form.Root
367
+ key={[...selection].join()}
368
+ schema={schema}
369
+ values={values}
370
+ fieldOverrides={fieldOverrides}
371
+ fieldMap={fieldMap}
372
+ db={db}
373
+ getOptions={getOptions}
374
+ readonly={readonly}
375
+ autoSave
376
+ onSave={onSave}
377
+ >
378
+ {/* Floating, the panel is as tall as its host and a long form scrolls inside it; docked, the dock scrolls
379
+ the panels together, so the form keeps no scroll of its own (it would hold the wheel). */}
380
+ <Form.Viewport scroll={!docked}>
381
+ <Form.Content>
382
+ <Form.Fields exclude={HIDDEN} />
383
+ </Form.Content>
384
+ </Form.Viewport>
385
+ </Form.Root>
386
+ </SceneOptionsContext.Provider>
387
+ </StyleClassesContext.Provider>
388
+ </LayersContext.Provider>
389
+ )}
390
+ </Panel.Body>
391
+ <Panel.Footer classNames='text-center'>
392
+ <span className='text-sm text-fg-muted' data-testid='properties-summary'>
213
393
  {summary}
214
- </p>
215
- )}
216
- </div>
394
+ </span>
395
+ </Panel.Footer>
396
+ </Panel.Root>
217
397
  );
218
398
  };
@@ -0,0 +1,22 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import React, { createContext, useContext } from 'react';
6
+
7
+ import { Form, type FormFieldRenderer, SelectField } from '@dxos/react-ui-form';
8
+
9
+ import { type SceneOption } from '../../utils/scenes.ts';
10
+
11
+ /** The scenes a selected scene shape may open, as the panel's host lists them (`sceneOptions`). */
12
+ export const SceneOptionsContext = createContext<readonly SceneOption[]>([]);
13
+
14
+ /** A scene shape's `scene`: which scene of this drawing it opens, so several shapes may open one scene. */
15
+ export const SceneField: FormFieldRenderer = (props) => {
16
+ const options = useContext(SceneOptionsContext);
17
+ return (
18
+ <Form.Field path={props.jsonPath} label={props.label} readonly={props.readonly}>
19
+ <SelectField {...props} options={[...options]} />
20
+ </Form.Field>
21
+ );
22
+ };
@@ -0,0 +1,50 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import { type Meta, type StoryObj } from '@storybook/react-vite';
6
+ import React, { useState } from 'react';
7
+
8
+ import { JsonHighlighter } from '@dxos/react-ui-syntax-highlighter';
9
+ import { withLayout, withTheme } from '@dxos/react-ui/testing';
10
+
11
+ import { type NodeTone, type StyleHue } from '../../model/types.ts';
12
+ import { TONE_NAMES } from '../../utils/style.ts';
13
+ import { StyleGrid, type StyleGridProps } from './StyleGrid.tsx';
14
+
15
+ type StoryArgs = Pick<StyleGridProps, 'indeterminate' | 'readonly'>;
16
+
17
+ /** The grid alone, holding its own pick: each swatch is a hue at a tone, as a node would draw it. */
18
+ const DefaultStory = ({ indeterminate, readonly }: StoryArgs) => {
19
+ const [hue, setHue] = useState<StyleHue>('blue');
20
+ const [tone, setTone] = useState<NodeTone>(2);
21
+ return (
22
+ <div className='grid grid-cols-2 gap-4 w-[48rem]'>
23
+ <StyleGrid
24
+ hue={hue}
25
+ tone={tone}
26
+ indeterminate={indeterminate}
27
+ readonly={readonly}
28
+ onValueChange={(choice) => {
29
+ setHue(choice.hue);
30
+ setTone(choice.tone);
31
+ }}
32
+ />
33
+ <JsonHighlighter data={{ hue, tone, name: TONE_NAMES[tone] }} />
34
+ </div>
35
+ );
36
+ };
37
+
38
+ const meta: Meta<StoryArgs> = {
39
+ title: 'ui/react-ui-canvas/StyleGrid',
40
+ render: DefaultStory,
41
+ decorators: [withTheme(), withLayout({ layout: 'centered' })],
42
+ args: {
43
+ indeterminate: false,
44
+ readonly: false,
45
+ },
46
+ };
47
+
48
+ export default meta;
49
+
50
+ export const Default: StoryObj<StoryArgs> = {};
@@ -8,8 +8,8 @@ import { Form, type FormFieldRenderer, useFormFieldState } from '@dxos/react-ui-
8
8
  import * as ToggleGroup from '@dxos/react-ui/ToggleGroup';
9
9
  import { mx } from '@dxos/ui-theme';
10
10
 
11
- import { NodeStyle, type NodeTone } from '../../model/types.ts';
12
- import { DEFAULT_TONE, STYLE_HUES, type StyleHue, TONE_NAMES, TONES, hueClasses } from '../../utils/style.ts';
11
+ import { NodeStyle, type NodeTone, STYLE_HUES, type StyleHue } from '../../model/types.ts';
12
+ import { DEFAULT_TONE, TONE_NAMES, TONES, hueClasses } from '../../utils/style.ts';
13
13
 
14
14
  export type StyleChoice = { hue: StyleHue; tone: NodeTone };
15
15
 
@@ -36,6 +36,8 @@ export type StyleGridProps = {
36
36
  /** Several nodes disagree: no swatch is selected until one is picked. */
37
37
  indeterminate?: boolean;
38
38
  readonly?: boolean;
39
+ /** The rows offered; all tones by default, or one row (e.g. a line's outline colour only). */
40
+ tones?: readonly NodeTone[];
39
41
  onValueChange: (choice: StyleChoice) => void;
40
42
  };
41
43
 
@@ -43,7 +45,14 @@ export type StyleGridProps = {
43
45
  * Each offered hue in a column and its tones down the rows, every swatch drawn with the classes the node
44
46
  * itself would take, so a pick is a preview. A hue the grid does not offer, or none, selects nothing.
45
47
  */
46
- export const StyleGrid = ({ hue, tone = DEFAULT_TONE, indeterminate, readonly, onValueChange }: StyleGridProps) => {
48
+ export const StyleGrid = ({
49
+ hue,
50
+ tone = DEFAULT_TONE,
51
+ indeterminate,
52
+ readonly,
53
+ tones = TONES,
54
+ onValueChange,
55
+ }: StyleGridProps) => {
47
56
  const selected = indeterminate || !hue ? '' : `${hue}${SEPARATOR}${tone}`;
48
57
  return (
49
58
  <ToggleGroup.Root
@@ -60,7 +69,7 @@ export const StyleGrid = ({ hue, tone = DEFAULT_TONE, indeterminate, readonly, o
60
69
  }
61
70
  }}
62
71
  >
63
- {TONES.flatMap((rowTone) =>
72
+ {tones.flatMap((rowTone) =>
64
73
  STYLE_HUES.map((columnHue) => {
65
74
  const key = choiceKey({ hue: columnHue, tone: rowTone });
66
75
  const classes = hueClasses(columnHue, rowTone);
@@ -91,28 +100,47 @@ export const StyleGrid = ({ hue, tone = DEFAULT_TONE, indeterminate, readonly, o
91
100
  );
92
101
  };
93
102
 
103
+ /** The outline row alone: a link takes a hue but no fill, so its colour is the hue's border. */
104
+ const OUTLINE_TONES: readonly NodeTone[] = [0];
105
+
106
+ export type StyleFieldOptions = {
107
+ /** Offer the fill rows (tones 1–3) as well as the outline; a link, or a selection with one, has no fill. */
108
+ fills: boolean;
109
+ };
110
+
94
111
  /**
95
- * The properties panel's style field, rendered at `style.hue`: one pick sets the hue and its tone together,
96
- * so it writes the enclosing `style`.
112
+ * The properties panel's style picker, rendered at `style.hue`, for every kind of element: with `fills`, one pick
113
+ * sets the hue and its tone together (a node); without, the outline row sets the hue alone (a link, or a
114
+ * selection mixing kinds, which edits only the style they share).
97
115
  */
98
- export const StyleGridField: FormFieldRenderer = ({ label, jsonPath, readonly, indeterminate, getValue, onBlur }) => {
99
- // Hue and tone change together, so the pick writes their parent once; two field writes would race.
100
- const stylePath = (jsonPath ?? 'style.hue').split('.').slice(0, -1);
101
- const styleField = useFormFieldState('StyleGridField', stylePath);
102
- const style: NodeStyle = styleField.getValue() ?? {};
103
- return (
104
- <Form.Field path={jsonPath} label={label} readonly={readonly}>
105
- <StyleGrid
106
- hue={getValue()}
107
- tone={style.tone}
108
- indeterminate={indeterminate}
109
- readonly={!!readonly}
110
- onValueChange={({ hue, tone }) => {
111
- // A pick is a commit: the grid never blurs, so it commits itself.
112
- styleField.onValueChange(NodeStyle.ast, { ...style, hue, tone });
113
- onBlur();
114
- }}
115
- />
116
- </Form.Field>
117
- );
116
+ export const createStyleField = ({ fills }: StyleFieldOptions): FormFieldRenderer => {
117
+ const StyleField: FormFieldRenderer = ({ label, jsonPath, readonly, indeterminate, getValue, onBlur }) => {
118
+ // Hue and tone change together, so the pick writes their parent once; two field writes would race.
119
+ const stylePath = (jsonPath ?? 'style.hue').split('.').slice(0, -1);
120
+ const styleField = useFormFieldState('StyleField', stylePath);
121
+ const style: NodeStyle = styleField.getValue() ?? {};
122
+ return (
123
+ <Form.Field path={jsonPath} label={label} readonly={readonly}>
124
+ <StyleGrid
125
+ hue={getValue()}
126
+ tone={fills ? style.tone : 0}
127
+ tones={fills ? TONES : OUTLINE_TONES}
128
+ indeterminate={indeterminate}
129
+ readonly={!!readonly}
130
+ onValueChange={({ hue, tone }) => {
131
+ // A pick is a commit: the grid never blurs, so it commits itself.
132
+ styleField.onValueChange(NodeStyle.ast, fills ? { ...style, hue, tone } : { ...style, hue });
133
+ onBlur();
134
+ }}
135
+ />
136
+ </Form.Field>
137
+ );
138
+ };
139
+ return StyleField;
118
140
  };
141
+
142
+ /** The style picker with its fill rows: a node's, or a selection of nodes. */
143
+ export const StyleGridField = createStyleField({ fills: true });
144
+
145
+ /** The style picker's outline row alone: a link's, or a selection with a link in it. */
146
+ export const OutlineStyleField = createStyleField({ fills: false });