@dxos/plugin-explorer 0.8.4-main.7ace549 → 0.8.4-main.8baae0fced

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 (264) hide show
  1. package/LICENSE +102 -5
  2. package/dist/lib/neutral/ExplorerArticle-EAKRB55W.mjs +277 -0
  3. package/dist/lib/neutral/ExplorerArticle-EAKRB55W.mjs.map +7 -0
  4. package/dist/lib/neutral/ExplorerPlugin.mjs +10 -0
  5. package/dist/lib/neutral/capabilities/index.mjs +11 -0
  6. package/dist/lib/neutral/capabilities/index.mjs.map +7 -0
  7. package/dist/lib/neutral/chunk-7SPMPHRS.mjs +72 -0
  8. package/dist/lib/neutral/chunk-7SPMPHRS.mjs.map +7 -0
  9. package/dist/lib/{browser/chunk-UBHZGWZQ.mjs → neutral/chunk-DXIWQFYO.mjs} +3 -5
  10. package/dist/lib/neutral/chunk-DXIWQFYO.mjs.map +7 -0
  11. package/dist/lib/neutral/chunk-EM2BV4PF.mjs +290 -0
  12. package/dist/lib/neutral/chunk-EM2BV4PF.mjs.map +7 -0
  13. package/dist/lib/neutral/chunk-GRJXLL4Z.mjs +25 -0
  14. package/dist/lib/neutral/chunk-GRJXLL4Z.mjs.map +7 -0
  15. package/dist/lib/neutral/chunk-V2OFO6PI.mjs +14 -0
  16. package/dist/lib/neutral/chunk-V2OFO6PI.mjs.map +7 -0
  17. package/dist/lib/{browser/chunk-ARBGXQFH.mjs → neutral/components/index.mjs} +858 -307
  18. package/dist/lib/neutral/components/index.mjs.map +7 -0
  19. package/dist/lib/neutral/containers/index.mjs +9 -0
  20. package/dist/lib/neutral/containers/index.mjs.map +7 -0
  21. package/dist/lib/neutral/create-object-F6TKVAGV.mjs +39 -0
  22. package/dist/lib/neutral/create-object-F6TKVAGV.mjs.map +7 -0
  23. package/dist/lib/neutral/hooks/index.mjs +45 -0
  24. package/dist/lib/neutral/hooks/index.mjs.map +7 -0
  25. package/dist/lib/neutral/index.mjs +14 -0
  26. package/dist/lib/neutral/meta.json +1 -0
  27. package/dist/lib/{browser → neutral}/meta.mjs +1 -1
  28. package/dist/lib/neutral/plugin.mjs +12 -0
  29. package/dist/lib/neutral/plugin.mjs.map +7 -0
  30. package/dist/lib/neutral/react-surface-APBW2VQG.mjs +26 -0
  31. package/dist/lib/neutral/react-surface-APBW2VQG.mjs.map +7 -0
  32. package/dist/lib/neutral/testing/index.mjs +193 -0
  33. package/dist/lib/neutral/testing/index.mjs.map +7 -0
  34. package/dist/lib/neutral/translations.mjs +33 -0
  35. package/dist/lib/neutral/translations.mjs.map +7 -0
  36. package/dist/lib/{browser → neutral}/types/index.mjs +1 -2
  37. package/dist/types/data/cities.d.ts +4 -4
  38. package/dist/types/data/cities.d.ts.map +1 -1
  39. package/dist/types/data/countries-110m.d.ts +19 -22
  40. package/dist/types/data/countries-110m.d.ts.map +1 -1
  41. package/dist/types/src/ExplorerPlugin.d.ts +3 -1
  42. package/dist/types/src/ExplorerPlugin.d.ts.map +1 -1
  43. package/dist/types/src/ExplorerPlugin.test.d.ts +2 -0
  44. package/dist/types/src/ExplorerPlugin.test.d.ts.map +1 -0
  45. package/dist/types/src/capabilities/create-object.d.ts +11 -0
  46. package/dist/types/src/capabilities/create-object.d.ts.map +1 -0
  47. package/dist/types/src/capabilities/index.d.ts +8 -2
  48. package/dist/types/src/capabilities/index.d.ts.map +1 -1
  49. package/dist/types/src/capabilities/react-surface.d.ts +3 -2
  50. package/dist/types/src/capabilities/react-surface.d.ts.map +1 -1
  51. package/dist/types/src/components/Chart/Chart.d.ts +1 -1
  52. package/dist/types/src/components/Chart/Chart.d.ts.map +1 -1
  53. package/dist/types/src/components/Chart/Chart.stories.d.ts +4 -1
  54. package/dist/types/src/components/Chart/Chart.stories.d.ts.map +1 -1
  55. package/dist/types/src/components/Globe/Globe.d.ts +1 -1
  56. package/dist/types/src/components/Globe/Globe.d.ts.map +1 -1
  57. package/dist/types/src/components/Globe/Globe.stories.d.ts +5 -2
  58. package/dist/types/src/components/Globe/Globe.stories.d.ts.map +1 -1
  59. package/dist/types/src/components/Graph/CanvasForceGraph.d.ts +13 -0
  60. package/dist/types/src/components/Graph/CanvasForceGraph.d.ts.map +1 -0
  61. package/dist/types/src/components/Graph/CanvasForceGraph.stories.d.ts +17 -0
  62. package/dist/types/src/components/Graph/CanvasForceGraph.stories.d.ts.map +1 -0
  63. package/dist/types/src/components/Graph/ForceGraph.d.ts +12 -5
  64. package/dist/types/src/components/Graph/ForceGraph.d.ts.map +1 -1
  65. package/dist/types/src/components/Graph/ForceGraph.stories.d.ts +4 -2
  66. package/dist/types/src/components/Graph/ForceGraph.stories.d.ts.map +1 -1
  67. package/dist/types/src/components/Graph/{adapter.d.ts → graph-adapter.d.ts} +2 -2
  68. package/dist/types/src/components/Graph/graph-adapter.d.ts.map +1 -0
  69. package/dist/types/src/components/Graph/index.d.ts +1 -1
  70. package/dist/types/src/components/Graph/index.d.ts.map +1 -1
  71. package/dist/types/src/components/Lattice/Lattice.d.ts +20 -0
  72. package/dist/types/src/components/Lattice/Lattice.d.ts.map +1 -0
  73. package/dist/types/src/components/Lattice/Lattice.stories.d.ts +8 -0
  74. package/dist/types/src/components/Lattice/Lattice.stories.d.ts.map +1 -0
  75. package/dist/types/src/components/Lattice/index.d.ts +2 -0
  76. package/dist/types/src/components/Lattice/index.d.ts.map +1 -0
  77. package/dist/types/src/components/Tree/EdgeBundling.stories.d.ts +21 -0
  78. package/dist/types/src/components/Tree/EdgeBundling.stories.d.ts.map +1 -0
  79. package/dist/types/src/components/Tree/Tree.d.ts +20 -23
  80. package/dist/types/src/components/Tree/Tree.d.ts.map +1 -1
  81. package/dist/types/src/components/Tree/Tree.stories.d.ts +5 -12
  82. package/dist/types/src/components/Tree/Tree.stories.d.ts.map +1 -1
  83. package/dist/types/src/components/Tree/index.d.ts +2 -0
  84. package/dist/types/src/components/Tree/index.d.ts.map +1 -1
  85. package/dist/types/src/components/Tree/layout/HierarchicalEdgeBundling.d.ts +37 -2
  86. package/dist/types/src/components/Tree/layout/HierarchicalEdgeBundling.d.ts.map +1 -1
  87. package/dist/types/src/components/Tree/layout/RadialTree.d.ts +35 -2
  88. package/dist/types/src/components/Tree/layout/RadialTree.d.ts.map +1 -1
  89. package/dist/types/src/components/Tree/layout/TidyTree.d.ts +24 -2
  90. package/dist/types/src/components/Tree/layout/TidyTree.d.ts.map +1 -1
  91. package/dist/types/src/components/Tree/layout/hierarchy.d.ts +17 -0
  92. package/dist/types/src/components/Tree/layout/hierarchy.d.ts.map +1 -0
  93. package/dist/types/src/components/Tree/layout/index.d.ts +5 -4
  94. package/dist/types/src/components/Tree/layout/index.d.ts.map +1 -1
  95. package/dist/types/src/components/Tree/layout/slots.d.ts +7 -0
  96. package/dist/types/src/components/Tree/layout/slots.d.ts.map +1 -0
  97. package/dist/types/src/components/Tree/layout/useContainerSize.d.ts +15 -0
  98. package/dist/types/src/components/Tree/layout/useContainerSize.d.ts.map +1 -0
  99. package/dist/types/src/components/Tree/types/tree.d.ts +18 -16
  100. package/dist/types/src/components/Tree/types/tree.d.ts.map +1 -1
  101. package/dist/types/src/components/Tree/types/types.d.ts +14 -4
  102. package/dist/types/src/components/Tree/types/types.d.ts.map +1 -1
  103. package/dist/types/src/components/index.d.ts +1 -2
  104. package/dist/types/src/components/index.d.ts.map +1 -1
  105. package/dist/types/src/containers/ExplorerArticle/ExplorerArticle.d.ts +9 -0
  106. package/dist/types/src/containers/ExplorerArticle/ExplorerArticle.d.ts.map +1 -0
  107. package/dist/types/src/containers/ExplorerArticle/ExplorerArticle.stories.d.ts +29 -0
  108. package/dist/types/src/containers/ExplorerArticle/ExplorerArticle.stories.d.ts.map +1 -0
  109. package/dist/types/src/containers/ExplorerArticle/experimental.stories.d.ts +7 -0
  110. package/dist/types/src/containers/ExplorerArticle/experimental.stories.d.ts.map +1 -0
  111. package/dist/types/src/containers/ExplorerArticle/index.d.ts +2 -0
  112. package/dist/types/src/containers/ExplorerArticle/index.d.ts.map +1 -0
  113. package/dist/types/src/containers/index.d.ts +3 -0
  114. package/dist/types/src/containers/index.d.ts.map +1 -0
  115. package/dist/types/src/hooks/useGraphModel.d.ts +2 -2
  116. package/dist/types/src/hooks/useGraphModel.d.ts.map +1 -1
  117. package/dist/types/src/index.d.ts +1 -3
  118. package/dist/types/src/index.d.ts.map +1 -1
  119. package/dist/types/src/meta.d.ts +2 -2
  120. package/dist/types/src/meta.d.ts.map +1 -1
  121. package/dist/types/src/plugin.d.ts +3 -0
  122. package/dist/types/src/plugin.d.ts.map +1 -0
  123. package/dist/types/src/{components/Tree/testing → testing}/generator.d.ts +1 -1
  124. package/dist/types/src/testing/generator.d.ts.map +1 -0
  125. package/dist/types/src/testing/index.d.ts +4 -0
  126. package/dist/types/src/testing/index.d.ts.map +1 -0
  127. package/dist/types/src/testing/relations.d.ts +47 -0
  128. package/dist/types/src/testing/relations.d.ts.map +1 -0
  129. package/dist/types/src/translations.d.ts +29 -28
  130. package/dist/types/src/translations.d.ts.map +1 -1
  131. package/dist/types/src/types/ExplorerAction.d.ts +0 -17
  132. package/dist/types/src/types/ExplorerAction.d.ts.map +1 -1
  133. package/dist/types/src/types/Graph.d.ts +10 -20
  134. package/dist/types/src/types/Graph.d.ts.map +1 -1
  135. package/dist/types/src/util/index.d.ts +3 -0
  136. package/dist/types/src/util/index.d.ts.map +1 -0
  137. package/dist/types/src/util/node-color.d.ts +13 -0
  138. package/dist/types/src/util/node-color.d.ts.map +1 -0
  139. package/dist/types/src/{components → util}/plot.d.ts +1 -1
  140. package/dist/types/src/util/plot.d.ts.map +1 -0
  141. package/dist/types/tsconfig.tsbuildinfo +1 -1
  142. package/package.json +113 -62
  143. package/src/ExplorerPlugin.test.ts +26 -0
  144. package/src/ExplorerPlugin.tsx +15 -45
  145. package/src/capabilities/create-object.ts +36 -0
  146. package/src/capabilities/index.ts +3 -3
  147. package/src/capabilities/react-surface.tsx +24 -18
  148. package/src/components/Chart/Chart.stories.tsx +16 -23
  149. package/src/components/Chart/Chart.tsx +1 -1
  150. package/src/components/Globe/Globe.stories.tsx +19 -22
  151. package/src/components/Globe/Globe.tsx +1 -1
  152. package/src/components/Graph/CanvasForceGraph.stories.tsx +83 -0
  153. package/src/components/Graph/CanvasForceGraph.tsx +124 -0
  154. package/src/components/Graph/ForceGraph.stories.tsx +83 -44
  155. package/src/components/Graph/ForceGraph.tsx +104 -85
  156. package/src/components/Graph/{adapter.ts → graph-adapter.ts} +14 -8
  157. package/src/components/Graph/index.ts +1 -1
  158. package/src/components/Lattice/Lattice.stories.tsx +90 -0
  159. package/src/components/Lattice/Lattice.tsx +182 -0
  160. package/src/components/Lattice/index.ts +5 -0
  161. package/src/components/Tree/EdgeBundling.stories.tsx +144 -0
  162. package/src/components/Tree/Tree.stories.tsx +20 -38
  163. package/src/components/Tree/Tree.tsx +69 -95
  164. package/src/components/Tree/index.ts +2 -0
  165. package/src/components/Tree/layout/HierarchicalEdgeBundling.tsx +296 -0
  166. package/src/components/Tree/layout/RadialTree.tsx +242 -0
  167. package/src/components/Tree/layout/TidyTree.tsx +246 -0
  168. package/src/components/Tree/layout/hierarchy.ts +32 -0
  169. package/src/components/Tree/layout/index.ts +5 -5
  170. package/src/components/Tree/layout/slots.ts +19 -0
  171. package/src/components/Tree/layout/useContainerSize.ts +43 -0
  172. package/src/components/Tree/types/tree.test.ts +6 -5
  173. package/src/components/Tree/types/tree.ts +41 -20
  174. package/src/components/Tree/types/types.ts +38 -29
  175. package/src/components/index.ts +1 -4
  176. package/src/containers/ExplorerArticle/ExplorerArticle.stories.tsx +136 -0
  177. package/src/containers/ExplorerArticle/ExplorerArticle.tsx +465 -0
  178. package/src/containers/ExplorerArticle/experimental.stories.tsx +446 -0
  179. package/src/containers/ExplorerArticle/index.ts +5 -0
  180. package/src/containers/index.ts +7 -0
  181. package/src/hooks/useGraphModel.ts +25 -14
  182. package/src/index.ts +1 -4
  183. package/src/meta.ts +4 -4
  184. package/src/plugin.ts +9 -0
  185. package/src/{components/Tree/testing → testing}/generator.ts +5 -3
  186. package/src/testing/index.ts +9 -0
  187. package/src/testing/relations.ts +192 -0
  188. package/src/translations.ts +14 -13
  189. package/src/types/ExplorerAction.ts +1 -18
  190. package/src/types/Graph.ts +13 -28
  191. package/src/typings.d.ts +8 -0
  192. package/src/util/index.ts +6 -0
  193. package/src/util/node-color.ts +23 -0
  194. package/src/{components → util}/plot.ts +16 -4
  195. package/dist/lib/browser/ExplorerContainer-NOLLVUTE.mjs +0 -50
  196. package/dist/lib/browser/ExplorerContainer-NOLLVUTE.mjs.map +0 -7
  197. package/dist/lib/browser/chunk-2MKBRIUT.mjs +0 -31
  198. package/dist/lib/browser/chunk-2MKBRIUT.mjs.map +0 -7
  199. package/dist/lib/browser/chunk-6BVXZQPP.mjs +0 -188
  200. package/dist/lib/browser/chunk-6BVXZQPP.mjs.map +0 -7
  201. package/dist/lib/browser/chunk-ARBGXQFH.mjs.map +0 -7
  202. package/dist/lib/browser/chunk-P6FFFVPM.mjs +0 -100
  203. package/dist/lib/browser/chunk-P6FFFVPM.mjs.map +0 -7
  204. package/dist/lib/browser/chunk-UBHZGWZQ.mjs.map +0 -7
  205. package/dist/lib/browser/index.mjs +0 -112
  206. package/dist/lib/browser/index.mjs.map +0 -7
  207. package/dist/lib/browser/intent-resolver-EWB3H5KH.mjs +0 -35
  208. package/dist/lib/browser/intent-resolver-EWB3H5KH.mjs.map +0 -7
  209. package/dist/lib/browser/meta.json +0 -1
  210. package/dist/lib/browser/react-surface-BY2DYCTH.mjs +0 -34
  211. package/dist/lib/browser/react-surface-BY2DYCTH.mjs.map +0 -7
  212. package/dist/lib/node-esm/ExplorerContainer-N3S5KSUX.mjs +0 -51
  213. package/dist/lib/node-esm/ExplorerContainer-N3S5KSUX.mjs.map +0 -7
  214. package/dist/lib/node-esm/chunk-3ODK27PU.mjs +0 -33
  215. package/dist/lib/node-esm/chunk-3ODK27PU.mjs.map +0 -7
  216. package/dist/lib/node-esm/chunk-4BY2XZET.mjs +0 -101
  217. package/dist/lib/node-esm/chunk-4BY2XZET.mjs.map +0 -7
  218. package/dist/lib/node-esm/chunk-CRSVAZNA.mjs +0 -190
  219. package/dist/lib/node-esm/chunk-CRSVAZNA.mjs.map +0 -7
  220. package/dist/lib/node-esm/chunk-HSLMI22Q.mjs +0 -11
  221. package/dist/lib/node-esm/chunk-HSLMI22Q.mjs.map +0 -7
  222. package/dist/lib/node-esm/chunk-NPIP4VEH.mjs +0 -11091
  223. package/dist/lib/node-esm/chunk-NPIP4VEH.mjs.map +0 -7
  224. package/dist/lib/node-esm/chunk-UXZM5VJB.mjs +0 -26
  225. package/dist/lib/node-esm/chunk-UXZM5VJB.mjs.map +0 -7
  226. package/dist/lib/node-esm/index.mjs +0 -113
  227. package/dist/lib/node-esm/index.mjs.map +0 -7
  228. package/dist/lib/node-esm/intent-resolver-SH6PW7VF.mjs +0 -36
  229. package/dist/lib/node-esm/intent-resolver-SH6PW7VF.mjs.map +0 -7
  230. package/dist/lib/node-esm/meta.json +0 -1
  231. package/dist/lib/node-esm/meta.mjs +0 -9
  232. package/dist/lib/node-esm/react-surface-7AAV7GBG.mjs +0 -35
  233. package/dist/lib/node-esm/react-surface-7AAV7GBG.mjs.map +0 -7
  234. package/dist/lib/node-esm/types/index.mjs +0 -12
  235. package/dist/types/src/capabilities/intent-resolver.d.ts +0 -4
  236. package/dist/types/src/capabilities/intent-resolver.d.ts.map +0 -1
  237. package/dist/types/src/components/ExplorerContainer.d.ts +0 -9
  238. package/dist/types/src/components/ExplorerContainer.d.ts.map +0 -1
  239. package/dist/types/src/components/Graph/D3ForceGraph.d.ts +0 -14
  240. package/dist/types/src/components/Graph/D3ForceGraph.d.ts.map +0 -1
  241. package/dist/types/src/components/Graph/D3ForceGraph.stories.d.ts +0 -15
  242. package/dist/types/src/components/Graph/D3ForceGraph.stories.d.ts.map +0 -1
  243. package/dist/types/src/components/Graph/adapter.d.ts.map +0 -1
  244. package/dist/types/src/components/Graph/testing.d.ts +0 -14
  245. package/dist/types/src/components/Graph/testing.d.ts.map +0 -1
  246. package/dist/types/src/components/Tree/testing/generator.d.ts.map +0 -1
  247. package/dist/types/src/components/Tree/testing/index.d.ts +0 -2
  248. package/dist/types/src/components/Tree/testing/index.d.ts.map +0 -1
  249. package/dist/types/src/components/plot.d.ts.map +0 -1
  250. package/src/capabilities/intent-resolver.ts +0 -23
  251. package/src/components/ExplorerContainer.tsx +0 -54
  252. package/src/components/Graph/D3ForceGraph.stories.tsx +0 -80
  253. package/src/components/Graph/D3ForceGraph.tsx +0 -101
  254. package/src/components/Graph/testing.ts +0 -55
  255. package/src/components/Tree/layout/HierarchicalEdgeBundling.ts +0 -162
  256. package/src/components/Tree/layout/RadialTree.ts +0 -94
  257. package/src/components/Tree/layout/TidyTree.ts +0 -101
  258. package/src/components/Tree/testing/index.ts +0 -5
  259. /package/dist/lib/{browser/chunk-J5LGTIGS.mjs.map → neutral/ExplorerPlugin.mjs.map} +0 -0
  260. /package/dist/lib/{browser → neutral}/chunk-J5LGTIGS.mjs +0 -0
  261. /package/dist/lib/{browser/meta.mjs.map → neutral/chunk-J5LGTIGS.mjs.map} +0 -0
  262. /package/dist/lib/{browser/types → neutral}/index.mjs.map +0 -0
  263. /package/dist/lib/{node-esm → neutral}/meta.mjs.map +0 -0
  264. /package/dist/lib/{node-esm → neutral}/types/index.mjs.map +0 -0
@@ -0,0 +1,446 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import { type Meta, type StoryObj } from '@storybook/react-vite';
6
+ import { linkRadial } from 'd3';
7
+ import React from 'react';
8
+
9
+ import { withLayout, withTheme } from '@dxos/react-ui/testing';
10
+
11
+ const meta: Meta = {
12
+ title: 'plugins/plugin-explorer/containers/ExplorerArticle/experimental',
13
+ decorators: [withTheme(), withLayout({ layout: 'fullscreen' })],
14
+ parameters: {
15
+ layout: 'fullscreen',
16
+ },
17
+ };
18
+
19
+ export default meta;
20
+
21
+ type Story = StoryObj;
22
+
23
+ // ---------------------------------------------------------------------------
24
+ // PathProbe — renders a fixed set of example bezier paths the cluster layout
25
+ // might emit, side-by-side across five columns:
26
+ // 1. Baked path strings (cubic / quadratic) as the cluster projector once
27
+ // emitted them.
28
+ // 2. linkRadial output from polar coordinates — should match column 1.
29
+ // 3. radialQuadratic — Q with control at (target.angle, source.radius).
30
+ // 4. radialElbow — sharp 2-segment elbow through the same knee.
31
+ // 5. radialSmoothElbow — cubic with both controls at the knee.
32
+ //
33
+ // Kept around so future curve experiments can compare candidate generators
34
+ // at a glance without touching the production cluster projector.
35
+ // ---------------------------------------------------------------------------
36
+
37
+ const PROBE_PATHS = [
38
+ // Source group at M(-174.87, -44.729). All three paths share that origin.
39
+ {
40
+ label: 'good (target angle 5.19, clockwise)',
41
+ color: 'lime',
42
+ d: 'M-174.87,-44.729 C-262.305,-67.094, -240.395,-124.563, -320.527,-166.083',
43
+ target: [-320.527, -166.083] as [number, number],
44
+ },
45
+ {
46
+ label: 'good (target angle 5.55, clockwise)',
47
+ color: 'cyan',
48
+ d: 'M-174.87,-44.729 C-262.305,-67.094, -180.249,-202.029, -240.332,-269.372',
49
+ target: [-240.332, -269.372] as [number, number],
50
+ },
51
+ {
52
+ label: 'bad (target angle 4.28, counter-clockwise)',
53
+ color: 'orange',
54
+ d: 'M-174.87,-44.729 C-262.305,-67.094, -245.815,113.492, -327.754,151.323',
55
+ target: [-327.754, 151.323] as [number, number],
56
+ },
57
+ // Live path the user reports as not rendering correctly in the full cluster.
58
+ // Source polar ≈ (1.14, 190.5); target polar ≈ (1.82, 380.9). Clockwise direction.
59
+ {
60
+ label: 'live broken (clockwise, source.x=1.14 → target.x=1.82)',
61
+ color: 'magenta',
62
+ d: 'M172.95585588832455,-79.85312713937451Q184.55815606041466,47.20738323164743 369.1163121208293,94.41476646329485',
63
+ target: [369.1163121208293, 94.41476646329485] as [number, number],
64
+ },
65
+ ];
66
+
67
+ const SOURCE_XY: [number, number] = [-174.87, -44.729];
68
+
69
+ // Reverse-engineered polar coords from the user's reported path strings.
70
+ // Source group: polar (4.96 rad, 181) → cartesian (-174.87, -44.729).
71
+ // We use linkRadial here directly to verify the strings d3 produces match what we expect.
72
+ const SOURCE_POLAR = { x: 4.96, y: 181 } as { x: number; y: number };
73
+ const PROBE_POLAR_TARGETS = [
74
+ { label: 'target 5.19 (clockwise)', color: 'lime', polar: { x: 5.19, y: 360 } },
75
+ { label: 'target 5.55 (clockwise)', color: 'cyan', polar: { x: 5.55, y: 361 } },
76
+ { label: 'target 4.28 (counter-clockwise)', color: 'orange', polar: { x: 4.28, y: 361 } },
77
+ ];
78
+
79
+ /** Convert d3-cluster polar (angle from 12 o'clock, sweeping clockwise) to cartesian. */
80
+ const polarToCartesian = (angle: number, radius: number): [number, number] => [
81
+ Math.cos(angle - Math.PI / 2) * radius,
82
+ Math.sin(angle - Math.PI / 2) * radius,
83
+ ];
84
+
85
+ /**
86
+ * Alternative curve: quadratic bezier from source to target with a single control point at
87
+ * (target.angle, source.radius). This makes the curve emerge from source heading TOWARD
88
+ * the target's angular direction (vs. linkRadial's curveBumpPolar, which emerges along
89
+ * source's angle and may bow far away from the target on the way).
90
+ */
91
+ const radialQuadratic = (source: { x: number; y: number }, target: { x: number; y: number }) => {
92
+ const [sx, sy] = polarToCartesian(source.x, source.y);
93
+ const [tx, ty] = polarToCartesian(target.x, target.y);
94
+ const [cx, cy] = polarToCartesian(target.x, source.y);
95
+ return `M${sx},${sy}Q${cx},${cy} ${tx},${ty}`;
96
+ };
97
+
98
+ /** Sharp elbow: M source L (target.angle, source.radius) L target. Hard-corner radial elbow. */
99
+ const radialElbow = (source: { x: number; y: number }, target: { x: number; y: number }) => {
100
+ const [sx, sy] = polarToCartesian(source.x, source.y);
101
+ const [tx, ty] = polarToCartesian(target.x, target.y);
102
+ const [cx, cy] = polarToCartesian(target.x, source.y);
103
+ return `M${sx},${sy}L${cx},${cy}L${tx},${ty}`;
104
+ };
105
+
106
+ /**
107
+ * Smoothed radial elbow: cubic bezier whose control points sit at the elbow knee. Curve
108
+ * emerges from source TOWARD the target angle (around the source ring), then bends OUT to
109
+ * target's radius. Eliminates the wide-ear from linkRadial AND the missing-stub from the
110
+ * pure quadratic — both halves of the curve are visible and follow the radial layout.
111
+ */
112
+ const radialSmoothElbow = (source: { x: number; y: number }, target: { x: number; y: number }) => {
113
+ const [sx, sy] = polarToCartesian(source.x, source.y);
114
+ const [tx, ty] = polarToCartesian(target.x, target.y);
115
+ const [kx, ky] = polarToCartesian(target.x, source.y); // "knee" — at source radius, target angle
116
+ // Both control points near the knee so the curve hugs the elbow.
117
+ return `M${sx},${sy}C${kx},${ky} ${kx},${ky} ${tx},${ty}`;
118
+ };
119
+
120
+ const PathProbe = () => {
121
+ const radialLink = linkRadial<any, any>()
122
+ .angle((d) => d.x)
123
+ .radius((d) => d.y);
124
+
125
+ return (
126
+ <div style={{ width: '100vw', height: '100vh', background: '#0c0c0c', position: 'relative' }}>
127
+ <svg
128
+ xmlns='http://www.w3.org/2000/svg'
129
+ width='100%'
130
+ height='100%'
131
+ viewBox='-450 -350 4000 700'
132
+ style={{ display: 'block' }}
133
+ >
134
+ {/* Center reference (column 1: baked paths) */}
135
+ <line x1={-450} y1={0} x2={450} y2={0} stroke='#222' />
136
+ <line x1={0} y1={-350} x2={0} y2={350} stroke='#222' />
137
+
138
+ {/* Origin marker */}
139
+ <circle cx={0} cy={0} r={3} fill='#444' />
140
+ <text x={-440} y={-330} fill='#666' fontSize={11}>
141
+ baked path strings (left)
142
+ </text>
143
+
144
+ {/* Source group circle */}
145
+ <circle cx={SOURCE_XY[0]} cy={SOURCE_XY[1]} r={5} fill='#888' />
146
+
147
+ {/* Pre-baked path strings (left column visualization). Handles both cubic (C) and
148
+ quadratic (Q) bezier commands. */}
149
+ {PROBE_PATHS.map((p, i) => {
150
+ const cubic = p.d.match(
151
+ /M\s*([-\d.]+),\s*([-\d.]+)\s*C\s*([-\d.]+),\s*([-\d.]+),\s*([-\d.]+),\s*([-\d.]+),\s*([-\d.]+),\s*([-\d.]+)/,
152
+ );
153
+ const quad = p.d.match(/M\s*([-\d.]+),\s*([-\d.]+)\s*Q\s*([-\d.]+),\s*([-\d.]+)\s+([-\d.]+),\s*([-\d.]+)/);
154
+ const controls: Array<[number, number]> = [];
155
+ let mx = 0,
156
+ my = 0,
157
+ ex = 0,
158
+ ey = 0;
159
+ if (cubic) {
160
+ const v = cubic.map(parseFloat);
161
+ mx = v[1];
162
+ my = v[2];
163
+ controls.push([v[3], v[4]], [v[5], v[6]]);
164
+ ex = v[7];
165
+ ey = v[8];
166
+ } else if (quad) {
167
+ const v = quad.map(parseFloat);
168
+ mx = v[1];
169
+ my = v[2];
170
+ controls.push([v[3], v[4]]);
171
+ ex = v[5];
172
+ ey = v[6];
173
+ }
174
+ return (
175
+ <g key={`baked-${i}`}>
176
+ <path d={p.d} fill='none' stroke={p.color} strokeWidth={2} />
177
+ <polyline
178
+ points={[`${mx},${my}`, ...controls.map(([x, y]) => `${x},${y}`), `${ex},${ey}`].join(' ')}
179
+ fill='none'
180
+ stroke={p.color}
181
+ strokeOpacity={0.3}
182
+ strokeWidth={1}
183
+ strokeDasharray='2 3'
184
+ />
185
+ {controls.map(([x, y], j) => (
186
+ <circle key={j} cx={x} cy={y} r={3} fill={p.color} opacity={0.5} />
187
+ ))}
188
+ <circle cx={p.target[0]} cy={p.target[1]} r={5} fill={p.color} />
189
+ <text x={p.target[0] + 8} y={p.target[1]} fill={p.color} fontSize={11}>
190
+ {p.label}
191
+ </text>
192
+ </g>
193
+ );
194
+ })}
195
+
196
+ {/* Re-generate the same paths via linkRadial from polar coords. Shift right by 700 so they
197
+ sit in a second column for direct visual comparison. */}
198
+ <g transform='translate(750, 0)'>
199
+ <line x1={-450} y1={0} x2={450} y2={0} stroke='#222' />
200
+ <line x1={0} y1={-350} x2={0} y2={350} stroke='#222' />
201
+ <circle cx={0} cy={0} r={3} fill='#444' />
202
+ <circle cx={SOURCE_XY[0]} cy={SOURCE_XY[1]} r={5} fill='#888' />
203
+ <text x={-440} y={-330} fill='#666' fontSize={11}>
204
+ linkRadial output from polar (middle)
205
+ </text>
206
+ {PROBE_POLAR_TARGETS.map((p, i) => {
207
+ const d = radialLink({ source: SOURCE_POLAR, target: p.polar }) ?? '';
208
+ const m = d.match(
209
+ /M\s*([-\d.]+),\s*([-\d.]+)\s*C\s*([-\d.]+),\s*([-\d.]+),\s*([-\d.]+),\s*([-\d.]+),\s*([-\d.]+),\s*([-\d.]+)/,
210
+ );
211
+ if (!m) {
212
+ return null;
213
+ }
214
+ const [, , , c1x, c1y, c2x, c2y, ex, ey] = m.map(parseFloat);
215
+ return (
216
+ <g key={`fresh-${i}`}>
217
+ <path d={d} fill='none' stroke={p.color} strokeWidth={2} />
218
+ <circle cx={c1x} cy={c1y} r={3} fill={p.color} opacity={0.5} />
219
+ <circle cx={c2x} cy={c2y} r={3} fill={p.color} opacity={0.5} />
220
+ <circle cx={ex} cy={ey} r={5} fill={p.color} />
221
+ <text x={ex + 8} y={ey} fill={p.color} fontSize={11}>
222
+ {p.label}
223
+ </text>
224
+ </g>
225
+ );
226
+ })}
227
+ </g>
228
+
229
+ {/* Alternative A: quadratic with control at (target.angle, source.radius). */}
230
+ <g transform='translate(1500, 0)'>
231
+ <line x1={-450} y1={0} x2={450} y2={0} stroke='#222' />
232
+ <line x1={0} y1={-350} x2={0} y2={350} stroke='#222' />
233
+ <circle cx={0} cy={0} r={3} fill='#444' />
234
+ <circle cx={SOURCE_XY[0]} cy={SOURCE_XY[1]} r={5} fill='#888' />
235
+ <text x={-440} y={-330} fill='#666' fontSize={11}>
236
+ radialQuadratic — control at (target.angle, source.radius)
237
+ </text>
238
+ {PROBE_POLAR_TARGETS.map((p, i) => {
239
+ const d = radialQuadratic(SOURCE_POLAR, p.polar);
240
+ const [tx, ty] = polarToCartesian(p.polar.x, p.polar.y);
241
+ const [cx, cy] = polarToCartesian(p.polar.x, SOURCE_POLAR.y);
242
+ return (
243
+ <g key={`alt-${i}`}>
244
+ <path d={d} fill='none' stroke={p.color} strokeWidth={2} />
245
+ <circle cx={cx} cy={cy} r={3} fill={p.color} opacity={0.5} />
246
+ <circle cx={tx} cy={ty} r={5} fill={p.color} />
247
+ <text x={tx + 8} y={ty} fill={p.color} fontSize={11}>
248
+ {p.label}
249
+ </text>
250
+ </g>
251
+ );
252
+ })}
253
+ </g>
254
+
255
+ {/* Alternative B: sharp elbow — two line segments. */}
256
+ <g transform='translate(2250, 0)'>
257
+ <line x1={-450} y1={0} x2={450} y2={0} stroke='#222' />
258
+ <line x1={0} y1={-350} x2={0} y2={350} stroke='#222' />
259
+ <circle cx={0} cy={0} r={3} fill='#444' />
260
+ <circle cx={SOURCE_XY[0]} cy={SOURCE_XY[1]} r={5} fill='#888' />
261
+ <text x={-440} y={-330} fill='#666' fontSize={11}>
262
+ radialElbow — sharp 2-segment elbow
263
+ </text>
264
+ {PROBE_POLAR_TARGETS.map((p, i) => {
265
+ const d = radialElbow(SOURCE_POLAR, p.polar);
266
+ const [tx, ty] = polarToCartesian(p.polar.x, p.polar.y);
267
+ return (
268
+ <g key={`elbow-${i}`}>
269
+ <path d={d} fill='none' stroke={p.color} strokeWidth={2} />
270
+ <circle cx={tx} cy={ty} r={5} fill={p.color} />
271
+ <text x={tx + 8} y={ty} fill={p.color} fontSize={11}>
272
+ {p.label}
273
+ </text>
274
+ </g>
275
+ );
276
+ })}
277
+ </g>
278
+
279
+ {/* Alternative C: smoothed elbow — cubic with both controls at the knee. */}
280
+ <g transform='translate(3000, 0)'>
281
+ <line x1={-450} y1={0} x2={450} y2={0} stroke='#222' />
282
+ <line x1={0} y1={-350} x2={0} y2={350} stroke='#222' />
283
+ <circle cx={0} cy={0} r={3} fill='#444' />
284
+ <circle cx={SOURCE_XY[0]} cy={SOURCE_XY[1]} r={5} fill='#888' />
285
+ <text x={-440} y={-330} fill='#666' fontSize={11}>
286
+ radialSmoothElbow — cubic with both controls at the knee
287
+ </text>
288
+ {PROBE_POLAR_TARGETS.map((p, i) => {
289
+ const d = radialSmoothElbow(SOURCE_POLAR, p.polar);
290
+ const [tx, ty] = polarToCartesian(p.polar.x, p.polar.y);
291
+ return (
292
+ <g key={`smooth-${i}`}>
293
+ <path d={d} fill='none' stroke={p.color} strokeWidth={2} />
294
+ <circle cx={tx} cy={ty} r={5} fill={p.color} />
295
+ <text x={tx + 8} y={ty} fill={p.color} fontSize={11}>
296
+ {p.label}
297
+ </text>
298
+ </g>
299
+ );
300
+ })}
301
+ </g>
302
+ </svg>
303
+ </div>
304
+ );
305
+ };
306
+
307
+ export const PathProbeStory: Story = {
308
+ name: 'PathProbe',
309
+ render: () => <PathProbe />,
310
+ };
311
+
312
+ // ---------------------------------------------------------------------------
313
+ // PathInspector — paste any `d` string, click Diagnose. The path is rendered
314
+ // in an SVG and the same diagnostics (bbox, pathLength, computed CSS) we'd
315
+ // otherwise pull from the console are printed alongside. Loops to avoid the
316
+ // console-snippet ping-pong when debugging "this path doesn't render."
317
+ // ---------------------------------------------------------------------------
318
+
319
+ const PathInspector = () => {
320
+ const [input, setInput] = React.useState(
321
+ 'M172.95585588832455,-79.85312713937451Q184.55815606041466,47.20738323164743 369.1163121208293,94.41476646329485',
322
+ );
323
+ const [info, setInfo] = React.useState<Record<string, unknown> | null>(null);
324
+ const pathRef = React.useRef<SVGPathElement | null>(null);
325
+
326
+ const diagnose = React.useCallback(() => {
327
+ const el = pathRef.current;
328
+ if (!el) {
329
+ setInfo({ error: 'no path element' });
330
+ return;
331
+ }
332
+ const cs = getComputedStyle(el);
333
+ let bbox: any = null;
334
+ try {
335
+ const b = el.getBBox();
336
+ bbox = { x: b.x, y: b.y, width: b.width, height: b.height };
337
+ } catch (e: any) {
338
+ bbox = `error: ${e?.message}`;
339
+ }
340
+ let len: number | string = 0;
341
+ try {
342
+ len = el.getTotalLength();
343
+ } catch (e: any) {
344
+ len = `error: ${e?.message}`;
345
+ }
346
+ setInfo({
347
+ d: el.getAttribute('d'),
348
+ bbox,
349
+ pathLength: len,
350
+ stroke: cs.stroke,
351
+ strokeWidth: cs.strokeWidth,
352
+ strokeOpacity: cs.strokeOpacity,
353
+ opacity: cs.opacity,
354
+ visibility: cs.visibility,
355
+ display: cs.display,
356
+ clipPath: cs.clipPath,
357
+ transform: cs.transform,
358
+ });
359
+ }, []);
360
+
361
+ // Run on mount.
362
+ React.useEffect(() => {
363
+ diagnose();
364
+ // eslint-disable-next-line react-hooks/exhaustive-deps
365
+ }, []);
366
+
367
+ return (
368
+ <div
369
+ style={{
370
+ width: '100vw',
371
+ height: '100vh',
372
+ background: '#0c0c0c',
373
+ color: '#ddd',
374
+ fontFamily: 'monospace',
375
+ padding: 12,
376
+ boxSizing: 'border-box',
377
+ display: 'flex',
378
+ flexDirection: 'column',
379
+ gap: 8,
380
+ }}
381
+ >
382
+ <div style={{ display: 'flex', gap: 8, alignItems: 'flex-start' }}>
383
+ <textarea
384
+ value={input}
385
+ onChange={(e) => setInput(e.target.value)}
386
+ rows={3}
387
+ style={{
388
+ flex: 1,
389
+ background: '#111',
390
+ color: '#ddd',
391
+ border: '1px solid #333',
392
+ padding: 6,
393
+ fontFamily: 'monospace',
394
+ fontSize: 12,
395
+ }}
396
+ placeholder='paste a `d` attribute string (M ... C/Q ...)'
397
+ />
398
+ <button
399
+ onClick={diagnose}
400
+ style={{
401
+ background: '#222',
402
+ color: '#ddd',
403
+ border: '1px solid #444',
404
+ padding: '8px 14px',
405
+ cursor: 'pointer',
406
+ }}
407
+ >
408
+ Diagnose
409
+ </button>
410
+ </div>
411
+
412
+ <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 8, flex: 1, minHeight: 0 }}>
413
+ <svg
414
+ xmlns='http://www.w3.org/2000/svg'
415
+ width='100%'
416
+ height='100%'
417
+ viewBox='-450 -350 900 700'
418
+ style={{ background: '#000', border: '1px solid #222' }}
419
+ >
420
+ <line x1={-450} y1={0} x2={450} y2={0} stroke='#222' />
421
+ <line x1={0} y1={-350} x2={0} y2={350} stroke='#222' />
422
+ <circle cx={0} cy={0} r={3} fill='#444' />
423
+ <path ref={pathRef} d={input} fill='none' stroke='magenta' strokeWidth={2} />
424
+ </svg>
425
+
426
+ <pre
427
+ style={{
428
+ background: '#111',
429
+ padding: 10,
430
+ overflow: 'auto',
431
+ fontSize: 12,
432
+ margin: 0,
433
+ border: '1px solid #222',
434
+ }}
435
+ >
436
+ {info ? JSON.stringify(info, null, 2) : '(click Diagnose)'}
437
+ </pre>
438
+ </div>
439
+ </div>
440
+ );
441
+ };
442
+
443
+ export const PathInspectorStory: Story = {
444
+ name: 'PathInspector',
445
+ render: () => <PathInspector />,
446
+ };
@@ -0,0 +1,5 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ export { ExplorerArticle as default } from './ExplorerArticle';
@@ -0,0 +1,7 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { type ComponentType, lazy } from 'react';
6
+
7
+ export const ExplorerArticle: ComponentType<any> = lazy(() => import('./ExplorerArticle'));
@@ -4,33 +4,44 @@
4
4
 
5
5
  import { useEffect, useState } from 'react';
6
6
 
7
- import { type Filter, type Queue, type Space } from '@dxos/client/echo';
7
+ import { Capabilities } from '@dxos/app-framework';
8
+ import { useCapability } from '@dxos/app-framework/ui';
9
+ import { type Database, type Entity, type Filter } from '@dxos/echo';
8
10
  import { SpaceGraphModel, type SpaceGraphModelOptions } from '@dxos/schema';
9
11
 
10
12
  // TODO(burdon): Factor out.
11
13
  export const useGraphModel = (
12
- space: Space | undefined,
14
+ db: Database.Database | undefined,
13
15
  filter?: Filter.Any | undefined,
14
16
  options?: SpaceGraphModelOptions,
15
- queue?: Queue,
17
+ items?: readonly Entity.Unknown[],
16
18
  ): SpaceGraphModel | undefined => {
19
+ const registry = useCapability(Capabilities.AtomRegistry);
17
20
  const [model, setModel] = useState<SpaceGraphModel | undefined>(undefined);
21
+
18
22
  useEffect(() => {
19
- if (!space) {
20
- void model?.close();
23
+ if (!db) {
21
24
  setModel(undefined);
22
25
  return;
23
26
  }
24
27
 
25
- // TODO(burdon): Does this need to be a dependency?
26
- if (!model || model.queue !== queue) {
27
- const model = new SpaceGraphModel().setFilter(filter).setOptions(options);
28
- void model.open(space, queue);
29
- setModel(model);
30
- } else {
31
- model.setFilter(filter).setOptions(options);
32
- }
33
- }, [space, filter, options, queue]);
28
+ const newModel = new SpaceGraphModel(registry);
29
+ void newModel.open(db);
30
+ setModel(newModel);
31
+
32
+ return () => {
33
+ setModel(undefined);
34
+ void newModel.close();
35
+ };
36
+ }, [db, registry]);
37
+
38
+ useEffect(() => {
39
+ model?.setFilter(filter).setOptions(options);
40
+ }, [model, filter, options]);
41
+
42
+ useEffect(() => {
43
+ model?.setItems(items);
44
+ }, [model, items]);
34
45
 
35
46
  return model;
36
47
  };
package/src/index.ts CHANGED
@@ -2,8 +2,5 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- export * from './components';
6
- export * from './hooks';
7
5
  export * from './meta';
8
-
9
- export * from './ExplorerPlugin';
6
+ export * from './types';
package/src/meta.ts CHANGED
@@ -2,12 +2,13 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import { type PluginMeta } from '@dxos/app-framework';
5
+ import { type Plugin } from '@dxos/app-framework';
6
6
  import { trim } from '@dxos/util';
7
7
 
8
- export const meta: PluginMeta = {
9
- id: 'dxos.org/plugin/explorer',
8
+ export const meta: Plugin.Meta = {
9
+ id: 'org.dxos.plugin.explorer',
10
10
  name: 'Explorer',
11
+ author: 'DXOS',
11
12
  description: trim`
12
13
  Interactive hypergraph visualization that reveals relationships between objects in your workspace.
13
14
  Navigate complex data structures and discover connections through a dynamic network view.
@@ -15,6 +16,5 @@ export const meta: PluginMeta = {
15
16
  icon: 'ph--graph--regular',
16
17
  iconHue: 'green',
17
18
  source: 'https://github.com/dxos/dxos/tree/main/packages/plugins/plugin-explorer',
18
- tags: ['labs'],
19
19
  screenshots: ['https://dxos.network/plugin-details-explorer-dark.png'],
20
20
  };
package/src/plugin.ts ADDED
@@ -0,0 +1,9 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { Plugin } from '@dxos/app-framework';
6
+
7
+ import { meta } from './meta';
8
+
9
+ export const ExplorerPlugin = Plugin.lazy(meta, () => import('#plugin'));
@@ -2,10 +2,10 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import { Key } from '@dxos/echo';
5
+ import { Key, Obj } from '@dxos/echo';
6
6
  import { range } from '@dxos/util';
7
7
 
8
- import { Tree, type TreeNodeType } from '../types';
8
+ import { Tree, type TreeNodeType } from '../components/Tree/types';
9
9
 
10
10
  type NumberOrNumberArray = number | number[];
11
11
 
@@ -16,7 +16,9 @@ const random = (min: number, max: number) => Math.floor(Math.random() * (max - m
16
16
  */
17
17
  export const createTree = (spec: NumberOrNumberArray[] = [], createText?: () => string): Tree => {
18
18
  const tree = new Tree();
19
- tree.root.data = { text: 'root' };
19
+ Obj.update(tree.tree, () => {
20
+ tree.root.data = { text: 'root' };
21
+ });
20
22
 
21
23
  const createNodes = (parent: TreeNodeType, spec: NumberOrNumberArray = 0): TreeNodeType[] => {
22
24
  const count = Array.isArray(spec) ? random(spec[0], spec[1]) : spec;
@@ -0,0 +1,9 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ // Eager re-export of `ExplorerPlugin`. See `@dxos/plugin-testing/src/core.ts` for the rationale.
6
+ export * from '../ExplorerPlugin';
7
+
8
+ export * from './generator';
9
+ export * from './relations';