@dxos/plugin-explorer 0.8.4-main.72ec0f3 → 0.8.4-main.765dc60934

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 (234) hide show
  1. package/LICENSE +102 -5
  2. package/dist/lib/neutral/ExplorerArticle-EW2MBCRK.mjs +141 -0
  3. package/dist/lib/neutral/ExplorerArticle-EW2MBCRK.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/neutral/chunk-GRJXLL4Z.mjs +25 -0
  10. package/dist/lib/neutral/chunk-GRJXLL4Z.mjs.map +7 -0
  11. package/dist/lib/{browser/chunk-UBHZGWZQ.mjs → neutral/chunk-HPIS2WXY.mjs} +2 -2
  12. package/dist/lib/neutral/chunk-HPIS2WXY.mjs.map +7 -0
  13. package/dist/lib/{browser/chunk-ARBGXQFH.mjs → neutral/components/index.mjs} +817 -288
  14. package/dist/lib/{node-esm/chunk-NPIP4VEH.mjs.map → neutral/components/index.mjs.map} +4 -4
  15. package/dist/lib/neutral/containers/index.mjs +9 -0
  16. package/dist/lib/neutral/containers/index.mjs.map +7 -0
  17. package/dist/lib/neutral/create-object-F6TKVAGV.mjs +39 -0
  18. package/dist/lib/neutral/create-object-F6TKVAGV.mjs.map +7 -0
  19. package/dist/lib/neutral/hooks/index.mjs +45 -0
  20. package/dist/lib/neutral/hooks/index.mjs.map +7 -0
  21. package/dist/lib/neutral/index.mjs +14 -0
  22. package/dist/lib/neutral/meta.json +1 -0
  23. package/dist/lib/{browser → neutral}/meta.mjs +1 -1
  24. package/dist/lib/neutral/plugin.mjs +12 -0
  25. package/dist/lib/neutral/plugin.mjs.map +7 -0
  26. package/dist/lib/neutral/react-surface-APBW2VQG.mjs +26 -0
  27. package/dist/lib/neutral/react-surface-APBW2VQG.mjs.map +7 -0
  28. package/dist/lib/neutral/testing.mjs +8 -0
  29. package/dist/lib/neutral/translations.mjs +33 -0
  30. package/dist/lib/neutral/translations.mjs.map +7 -0
  31. package/dist/lib/{browser → neutral}/types/index.mjs +1 -2
  32. package/dist/types/data/cities.d.ts +4 -4
  33. package/dist/types/data/cities.d.ts.map +1 -1
  34. package/dist/types/data/countries-110m.d.ts +19 -22
  35. package/dist/types/data/countries-110m.d.ts.map +1 -1
  36. package/dist/types/src/ExplorerPlugin.d.ts +3 -1
  37. package/dist/types/src/ExplorerPlugin.d.ts.map +1 -1
  38. package/dist/types/src/ExplorerPlugin.test.d.ts +2 -0
  39. package/dist/types/src/ExplorerPlugin.test.d.ts.map +1 -0
  40. package/dist/types/src/capabilities/create-object.d.ts +11 -0
  41. package/dist/types/src/capabilities/create-object.d.ts.map +1 -0
  42. package/dist/types/src/capabilities/index.d.ts +8 -2
  43. package/dist/types/src/capabilities/index.d.ts.map +1 -1
  44. package/dist/types/src/capabilities/react-surface.d.ts +3 -2
  45. package/dist/types/src/capabilities/react-surface.d.ts.map +1 -1
  46. package/dist/types/src/components/Chart/Chart.d.ts.map +1 -1
  47. package/dist/types/src/components/Chart/Chart.stories.d.ts +4 -1
  48. package/dist/types/src/components/Chart/Chart.stories.d.ts.map +1 -1
  49. package/dist/types/src/components/Globe/Globe.d.ts.map +1 -1
  50. package/dist/types/src/components/Globe/Globe.stories.d.ts +5 -2
  51. package/dist/types/src/components/Globe/Globe.stories.d.ts.map +1 -1
  52. package/dist/types/src/components/Graph/CanvasForceGraph.d.ts +13 -0
  53. package/dist/types/src/components/Graph/CanvasForceGraph.d.ts.map +1 -0
  54. package/dist/types/src/components/Graph/CanvasForceGraph.stories.d.ts +17 -0
  55. package/dist/types/src/components/Graph/CanvasForceGraph.stories.d.ts.map +1 -0
  56. package/dist/types/src/components/Graph/ForceGraph.d.ts +12 -5
  57. package/dist/types/src/components/Graph/ForceGraph.d.ts.map +1 -1
  58. package/dist/types/src/components/Graph/ForceGraph.stories.d.ts +4 -2
  59. package/dist/types/src/components/Graph/ForceGraph.stories.d.ts.map +1 -1
  60. package/dist/types/src/components/Graph/{adapter.d.ts → graph-adapter.d.ts} +2 -2
  61. package/dist/types/src/components/Graph/graph-adapter.d.ts.map +1 -0
  62. package/dist/types/src/components/Graph/index.d.ts +1 -1
  63. package/dist/types/src/components/Graph/index.d.ts.map +1 -1
  64. package/dist/types/src/components/Tree/EdgeBundling.stories.d.ts +21 -0
  65. package/dist/types/src/components/Tree/EdgeBundling.stories.d.ts.map +1 -0
  66. package/dist/types/src/components/Tree/Tree.d.ts +20 -23
  67. package/dist/types/src/components/Tree/Tree.d.ts.map +1 -1
  68. package/dist/types/src/components/Tree/Tree.stories.d.ts +5 -12
  69. package/dist/types/src/components/Tree/Tree.stories.d.ts.map +1 -1
  70. package/dist/types/src/components/Tree/index.d.ts +3 -0
  71. package/dist/types/src/components/Tree/index.d.ts.map +1 -1
  72. package/dist/types/src/components/Tree/layout/HierarchicalEdgeBundling.d.ts +35 -2
  73. package/dist/types/src/components/Tree/layout/HierarchicalEdgeBundling.d.ts.map +1 -1
  74. package/dist/types/src/components/Tree/layout/RadialTree.d.ts +35 -2
  75. package/dist/types/src/components/Tree/layout/RadialTree.d.ts.map +1 -1
  76. package/dist/types/src/components/Tree/layout/TidyTree.d.ts +24 -2
  77. package/dist/types/src/components/Tree/layout/TidyTree.d.ts.map +1 -1
  78. package/dist/types/src/components/Tree/layout/hierarchy.d.ts +17 -0
  79. package/dist/types/src/components/Tree/layout/hierarchy.d.ts.map +1 -0
  80. package/dist/types/src/components/Tree/layout/index.d.ts +5 -4
  81. package/dist/types/src/components/Tree/layout/index.d.ts.map +1 -1
  82. package/dist/types/src/components/Tree/layout/slots.d.ts +7 -0
  83. package/dist/types/src/components/Tree/layout/slots.d.ts.map +1 -0
  84. package/dist/types/src/components/Tree/layout/useContainerSize.d.ts +15 -0
  85. package/dist/types/src/components/Tree/layout/useContainerSize.d.ts.map +1 -0
  86. package/dist/types/src/components/Tree/space-graph-adapter.d.ts +32 -0
  87. package/dist/types/src/components/Tree/space-graph-adapter.d.ts.map +1 -0
  88. package/dist/types/src/components/Tree/testing/generator.d.ts.map +1 -1
  89. package/dist/types/src/components/Tree/testing/index.d.ts +1 -0
  90. package/dist/types/src/components/Tree/testing/index.d.ts.map +1 -1
  91. package/dist/types/src/components/Tree/testing/relations.d.ts +47 -0
  92. package/dist/types/src/components/Tree/testing/relations.d.ts.map +1 -0
  93. package/dist/types/src/components/Tree/types/tree.d.ts +18 -16
  94. package/dist/types/src/components/Tree/types/tree.d.ts.map +1 -1
  95. package/dist/types/src/components/Tree/types/types.d.ts +14 -4
  96. package/dist/types/src/components/Tree/types/types.d.ts.map +1 -1
  97. package/dist/types/src/components/index.d.ts +0 -2
  98. package/dist/types/src/components/index.d.ts.map +1 -1
  99. package/dist/types/src/components/plot.d.ts.map +1 -1
  100. package/dist/types/src/containers/ExplorerArticle/ExplorerArticle.d.ts +8 -0
  101. package/dist/types/src/containers/ExplorerArticle/ExplorerArticle.d.ts.map +1 -0
  102. package/dist/types/src/containers/ExplorerArticle/ExplorerArticle.stories.d.ts +24 -0
  103. package/dist/types/src/containers/ExplorerArticle/ExplorerArticle.stories.d.ts.map +1 -0
  104. package/dist/types/src/containers/ExplorerArticle/index.d.ts +2 -0
  105. package/dist/types/src/containers/ExplorerArticle/index.d.ts.map +1 -0
  106. package/dist/types/src/containers/index.d.ts +3 -0
  107. package/dist/types/src/containers/index.d.ts.map +1 -0
  108. package/dist/types/src/hooks/useGraphModel.d.ts +2 -2
  109. package/dist/types/src/hooks/useGraphModel.d.ts.map +1 -1
  110. package/dist/types/src/index.d.ts +1 -3
  111. package/dist/types/src/index.d.ts.map +1 -1
  112. package/dist/types/src/meta.d.ts +2 -2
  113. package/dist/types/src/meta.d.ts.map +1 -1
  114. package/dist/types/src/plugin.d.ts +3 -0
  115. package/dist/types/src/plugin.d.ts.map +1 -0
  116. package/dist/types/src/testing.d.ts +2 -0
  117. package/dist/types/src/testing.d.ts.map +1 -0
  118. package/dist/types/src/translations.d.ts +31 -22
  119. package/dist/types/src/translations.d.ts.map +1 -1
  120. package/dist/types/src/types/ExplorerAction.d.ts +1 -18
  121. package/dist/types/src/types/ExplorerAction.d.ts.map +1 -1
  122. package/dist/types/src/types/Graph.d.ts +14 -25
  123. package/dist/types/src/types/Graph.d.ts.map +1 -1
  124. package/dist/types/tsconfig.tsbuildinfo +1 -1
  125. package/package.json +113 -62
  126. package/src/ExplorerPlugin.test.ts +26 -0
  127. package/src/ExplorerPlugin.tsx +15 -56
  128. package/src/capabilities/create-object.ts +36 -0
  129. package/src/capabilities/index.ts +3 -3
  130. package/src/capabilities/react-surface.tsx +24 -19
  131. package/src/components/Chart/Chart.stories.tsx +16 -23
  132. package/src/components/Globe/Globe.stories.tsx +19 -22
  133. package/src/components/Graph/CanvasForceGraph.stories.tsx +83 -0
  134. package/src/components/Graph/CanvasForceGraph.tsx +124 -0
  135. package/src/components/Graph/ForceGraph.stories.tsx +79 -42
  136. package/src/components/Graph/ForceGraph.tsx +104 -85
  137. package/src/components/Graph/{adapter.ts → graph-adapter.ts} +14 -8
  138. package/src/components/Graph/index.ts +1 -1
  139. package/src/components/Tree/EdgeBundling.stories.tsx +144 -0
  140. package/src/components/Tree/Tree.stories.tsx +20 -38
  141. package/src/components/Tree/Tree.tsx +69 -95
  142. package/src/components/Tree/index.ts +3 -0
  143. package/src/components/Tree/layout/HierarchicalEdgeBundling.tsx +277 -0
  144. package/src/components/Tree/layout/RadialTree.tsx +237 -0
  145. package/src/components/Tree/layout/TidyTree.tsx +246 -0
  146. package/src/components/Tree/layout/hierarchy.ts +32 -0
  147. package/src/components/Tree/layout/index.ts +5 -5
  148. package/src/components/Tree/layout/slots.ts +19 -0
  149. package/src/components/Tree/layout/useContainerSize.ts +43 -0
  150. package/src/components/Tree/space-graph-adapter.ts +96 -0
  151. package/src/components/Tree/testing/generator.ts +4 -2
  152. package/src/components/Tree/testing/index.ts +1 -0
  153. package/src/components/Tree/testing/relations.ts +182 -0
  154. package/src/components/Tree/types/tree.test.ts +5 -4
  155. package/src/components/Tree/types/tree.ts +41 -20
  156. package/src/components/Tree/types/types.ts +38 -29
  157. package/src/components/index.ts +0 -4
  158. package/src/containers/ExplorerArticle/ExplorerArticle.stories.tsx +119 -0
  159. package/src/containers/ExplorerArticle/ExplorerArticle.tsx +153 -0
  160. package/src/containers/ExplorerArticle/index.ts +5 -0
  161. package/src/containers/index.ts +7 -0
  162. package/src/hooks/useGraphModel.ts +25 -14
  163. package/src/index.ts +1 -4
  164. package/src/meta.ts +3 -3
  165. package/src/plugin.ts +9 -0
  166. package/src/testing.ts +7 -0
  167. package/src/translations.ts +16 -13
  168. package/src/types/ExplorerAction.ts +9 -19
  169. package/src/types/Graph.ts +25 -23
  170. package/src/typings.d.ts +8 -0
  171. package/dist/lib/browser/ExplorerContainer-NOLLVUTE.mjs +0 -50
  172. package/dist/lib/browser/ExplorerContainer-NOLLVUTE.mjs.map +0 -7
  173. package/dist/lib/browser/chunk-2MKBRIUT.mjs +0 -31
  174. package/dist/lib/browser/chunk-2MKBRIUT.mjs.map +0 -7
  175. package/dist/lib/browser/chunk-6BVXZQPP.mjs +0 -188
  176. package/dist/lib/browser/chunk-6BVXZQPP.mjs.map +0 -7
  177. package/dist/lib/browser/chunk-ARBGXQFH.mjs.map +0 -7
  178. package/dist/lib/browser/chunk-JDSUIUNR.mjs +0 -80
  179. package/dist/lib/browser/chunk-JDSUIUNR.mjs.map +0 -7
  180. package/dist/lib/browser/chunk-UBHZGWZQ.mjs.map +0 -7
  181. package/dist/lib/browser/index.mjs +0 -119
  182. package/dist/lib/browser/index.mjs.map +0 -7
  183. package/dist/lib/browser/intent-resolver-YS5LZC3A.mjs +0 -31
  184. package/dist/lib/browser/intent-resolver-YS5LZC3A.mjs.map +0 -7
  185. package/dist/lib/browser/meta.json +0 -1
  186. package/dist/lib/browser/react-surface-BVTCOVLK.mjs +0 -35
  187. package/dist/lib/browser/react-surface-BVTCOVLK.mjs.map +0 -7
  188. package/dist/lib/node-esm/ExplorerContainer-N3S5KSUX.mjs +0 -51
  189. package/dist/lib/node-esm/ExplorerContainer-N3S5KSUX.mjs.map +0 -7
  190. package/dist/lib/node-esm/chunk-3ODK27PU.mjs +0 -33
  191. package/dist/lib/node-esm/chunk-3ODK27PU.mjs.map +0 -7
  192. package/dist/lib/node-esm/chunk-CRSVAZNA.mjs +0 -190
  193. package/dist/lib/node-esm/chunk-CRSVAZNA.mjs.map +0 -7
  194. package/dist/lib/node-esm/chunk-HSLMI22Q.mjs +0 -11
  195. package/dist/lib/node-esm/chunk-MS72BATS.mjs +0 -81
  196. package/dist/lib/node-esm/chunk-MS72BATS.mjs.map +0 -7
  197. package/dist/lib/node-esm/chunk-NPIP4VEH.mjs +0 -11091
  198. package/dist/lib/node-esm/chunk-UXZM5VJB.mjs +0 -26
  199. package/dist/lib/node-esm/chunk-UXZM5VJB.mjs.map +0 -7
  200. package/dist/lib/node-esm/index.mjs +0 -120
  201. package/dist/lib/node-esm/index.mjs.map +0 -7
  202. package/dist/lib/node-esm/intent-resolver-VCEC67WX.mjs +0 -32
  203. package/dist/lib/node-esm/intent-resolver-VCEC67WX.mjs.map +0 -7
  204. package/dist/lib/node-esm/meta.json +0 -1
  205. package/dist/lib/node-esm/meta.mjs +0 -9
  206. package/dist/lib/node-esm/react-surface-4HFEX52O.mjs +0 -36
  207. package/dist/lib/node-esm/react-surface-4HFEX52O.mjs.map +0 -7
  208. package/dist/lib/node-esm/types/index.mjs +0 -12
  209. package/dist/types/src/capabilities/intent-resolver.d.ts +0 -4
  210. package/dist/types/src/capabilities/intent-resolver.d.ts.map +0 -1
  211. package/dist/types/src/components/ExplorerContainer.d.ts +0 -9
  212. package/dist/types/src/components/ExplorerContainer.d.ts.map +0 -1
  213. package/dist/types/src/components/Graph/D3ForceGraph.d.ts +0 -14
  214. package/dist/types/src/components/Graph/D3ForceGraph.d.ts.map +0 -1
  215. package/dist/types/src/components/Graph/D3ForceGraph.stories.d.ts +0 -15
  216. package/dist/types/src/components/Graph/D3ForceGraph.stories.d.ts.map +0 -1
  217. package/dist/types/src/components/Graph/adapter.d.ts.map +0 -1
  218. package/dist/types/src/components/Graph/testing.d.ts +0 -14
  219. package/dist/types/src/components/Graph/testing.d.ts.map +0 -1
  220. package/src/capabilities/intent-resolver.ts +0 -21
  221. package/src/components/ExplorerContainer.tsx +0 -54
  222. package/src/components/Graph/D3ForceGraph.stories.tsx +0 -78
  223. package/src/components/Graph/D3ForceGraph.tsx +0 -101
  224. package/src/components/Graph/testing.ts +0 -55
  225. package/src/components/Tree/layout/HierarchicalEdgeBundling.ts +0 -162
  226. package/src/components/Tree/layout/RadialTree.ts +0 -94
  227. package/src/components/Tree/layout/TidyTree.ts +0 -101
  228. /package/dist/lib/{browser/chunk-J5LGTIGS.mjs.map → neutral/ExplorerPlugin.mjs.map} +0 -0
  229. /package/dist/lib/{browser → neutral}/chunk-J5LGTIGS.mjs +0 -0
  230. /package/dist/lib/{browser/meta.mjs.map → neutral/chunk-J5LGTIGS.mjs.map} +0 -0
  231. /package/dist/lib/{browser/types → neutral}/index.mjs.map +0 -0
  232. /package/dist/lib/{node-esm → neutral}/meta.mjs.map +0 -0
  233. /package/dist/lib/{node-esm/chunk-HSLMI22Q.mjs.map → neutral/testing.mjs.map} +0 -0
  234. /package/dist/lib/{node-esm → neutral}/types/index.mjs.map +0 -0
@@ -2,7 +2,7 @@
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
8
  import { Tree, type TreeNodeType } from '../types';
@@ -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;
@@ -3,3 +3,4 @@
3
3
  //
4
4
 
5
5
  export * from './generator';
6
+ export * from './relations';
@@ -0,0 +1,182 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import { type Space } from '@dxos/client/echo';
6
+ import { Obj, Query, Relation } from '@dxos/echo';
7
+ import { type TypeSpec, type ValueGenerator, createObjectFactory } from '@dxos/schema/testing';
8
+ import { HasConnection, HasRelationship, Organization, Person, Pipeline } from '@dxos/types';
9
+ import { range } from '@dxos/util';
10
+
11
+ import { type BundleEdge } from '../layout';
12
+ import { type TreeNode } from '../types';
13
+
14
+ const SECTORS = ['Technology', 'Finance', 'Research', 'Media'];
15
+ const CONNECTION_KINDS = ['partner', 'investor', 'vendor', 'customer'];
16
+
17
+ const pick = <T>(arr: readonly T[], rng = Math.random): T => arr[Math.floor(rng() * arr.length)];
18
+
19
+ export type ConnectedOrgsResult = {
20
+ organizations: Obj.Any[];
21
+ people: Obj.Any[];
22
+ connections: Obj.Any[];
23
+ };
24
+
25
+ export type ConnectedOrgsOptions = {
26
+ organizationCount?: number;
27
+ personCount?: number;
28
+ connectionCount?: number;
29
+ };
30
+
31
+ /**
32
+ * Populate a space with Organizations, People, and HasConnection relations between organizations.
33
+ * Uses `createObjectFactory` to generate Org/Person properties from their `GeneratorAnnotation`s,
34
+ * then layers manual HasConnection relations on top — the connection schema is fixed
35
+ * (Org→Org) so it isn't a fit for the generator's reference inference.
36
+ */
37
+ export const generateConnectedOrgs = async (
38
+ space: Space,
39
+ generator: ValueGenerator,
40
+ { organizationCount = 12, personCount = 24, connectionCount = 18 }: ConnectedOrgsOptions = {},
41
+ ): Promise<ConnectedOrgsResult> => {
42
+ const specs: TypeSpec[] = [
43
+ { type: Organization.Organization, count: organizationCount },
44
+ // Person has a Ref to Organization — generator fills it from objects already in db.
45
+ { type: Person.Person, count: personCount },
46
+ ];
47
+
48
+ const factory = createObjectFactory(space.db, generator);
49
+ await factory(specs);
50
+
51
+ const organizations = await space.db.query(Query.type(Organization.Organization)).run();
52
+ const people = await space.db.query(Query.type(Person.Person)).run();
53
+
54
+ const connections: Obj.Any[] = [];
55
+ const seen = new Set<string>();
56
+ for (let i = 0; i < connectionCount && organizations.length >= 2; i++) {
57
+ const source = pick(organizations);
58
+ const target = pick(organizations);
59
+ if (source.id === target.id) {
60
+ continue;
61
+ }
62
+ const key = `${source.id}->${target.id}`;
63
+ if (seen.has(key)) {
64
+ continue;
65
+ }
66
+ seen.add(key);
67
+
68
+ const relation = Relation.make(HasConnection.HasConnection, {
69
+ [Relation.Source]: source as any,
70
+ [Relation.Target]: target as any,
71
+ kind: pick(CONNECTION_KINDS),
72
+ });
73
+ connections.push(space.db.add(relation as any));
74
+ }
75
+
76
+ await space.db.flush();
77
+ return { organizations, people, connections };
78
+ };
79
+
80
+ /**
81
+ * Build a TreeNode hierarchy: Root → Sector → Organization (leaf).
82
+ * Organizations are deterministically bucketed into `SECTORS` so the demo has visible groups.
83
+ */
84
+ export const buildOrgHierarchy = (organizations: Obj.Any[], sectors: readonly string[] = SECTORS): TreeNode => {
85
+ // Avoid modulo-by-zero / missing-bucket crashes when the caller passes an empty sectors list.
86
+ const activeSectors = sectors.length > 0 ? sectors : ['Uncategorized'];
87
+ const buckets = new Map<string, TreeNode[]>();
88
+ for (const sector of activeSectors) {
89
+ buckets.set(sector, []);
90
+ }
91
+ for (let i = 0; i < organizations.length; i++) {
92
+ const org = organizations[i] as any;
93
+ const sector = activeSectors[i % activeSectors.length];
94
+ buckets.get(sector)!.push({
95
+ id: org.id,
96
+ label: org.name ?? org.id.slice(0, 6),
97
+ });
98
+ }
99
+
100
+ return {
101
+ id: 'root',
102
+ label: 'Organizations',
103
+ children: activeSectors.map((sector) => ({
104
+ id: `sector:${sector}`,
105
+ label: sector,
106
+ children: buckets.get(sector) ?? [],
107
+ })),
108
+ };
109
+ };
110
+
111
+ const defaultGenerateTypes: TypeSpec[] = [
112
+ { type: Organization.Organization, count: 20 },
113
+ { type: Person.Person, count: 20 },
114
+ { type: Pipeline.Pipeline, count: 20 },
115
+ ];
116
+
117
+ export type GenerateOptions = {
118
+ spec?: TypeSpec[];
119
+ relations?: {
120
+ count: number;
121
+ kind: string;
122
+ };
123
+ };
124
+
125
+ const defaultGenerateRelations: NonNullable<GenerateOptions['relations']> = {
126
+ kind: 'friend',
127
+ count: 10,
128
+ };
129
+
130
+ /**
131
+ * Populate a space with a mixed dataset (Orgs, Pipelines, People) plus
132
+ * `HasRelationship` edges between random pairs of People.
133
+ *
134
+ * Used by the force-directed and canvas-force graph stories that want a
135
+ * heterogeneous typed dataset without caring about the precise shape of relations.
136
+ */
137
+ export const generate = async (
138
+ space: Space,
139
+ generator: ValueGenerator,
140
+ { spec = defaultGenerateTypes, relations = defaultGenerateRelations }: GenerateOptions = {},
141
+ ) => {
142
+ const createObjects = createObjectFactory(space.db, generator);
143
+ await createObjects(spec);
144
+
145
+ const contacts: Obj.Any[] = await space.db.query(Query.type(Person.Person)).run();
146
+ if (contacts.length < 2 || relations.count <= 0) {
147
+ return;
148
+ }
149
+ for (const _ of range(relations.count)) {
150
+ const source = pick(contacts);
151
+ const target = pick(contacts);
152
+ if (source.id !== target.id) {
153
+ space.db.add(
154
+ Relation.make(HasRelationship.HasRelationship, {
155
+ [Relation.Source]: source as any,
156
+ [Relation.Target]: target as any,
157
+ kind: relations.kind,
158
+ }) as any,
159
+ );
160
+ }
161
+ }
162
+ };
163
+
164
+ /**
165
+ * Convert HasConnection relations into bundle edges between organization ids.
166
+ */
167
+ export const connectionsToEdges = (connections: Obj.Any[]): BundleEdge[] => {
168
+ return connections
169
+ .map((relation): BundleEdge | undefined => {
170
+ const source = Relation.getSource(relation as any) as any;
171
+ const target = Relation.getTarget(relation as any) as any;
172
+ if (!source?.id || !target?.id) {
173
+ return undefined;
174
+ }
175
+ return {
176
+ source: source.id,
177
+ target: target.id,
178
+ kind: (relation as any).kind,
179
+ };
180
+ })
181
+ .filter((e): e is BundleEdge => Boolean(e));
182
+ };
@@ -5,14 +5,13 @@
5
5
  import { describe, test } from 'vitest';
6
6
 
7
7
  import { Obj, Ref } from '@dxos/echo';
8
- import { faker } from '@dxos/random';
8
+ import { random } from '@dxos/random';
9
9
  import { Task } from '@dxos/types';
10
10
 
11
11
  import { createTree } from '../testing';
12
-
13
12
  import { type Tree } from './tree';
14
13
 
15
- faker.seed(0);
14
+ random.seed(0);
16
15
 
17
16
  const print = (tree: Tree) => {
18
17
  let count = 0;
@@ -129,6 +128,8 @@ describe('tree', () => {
129
128
 
130
129
  const tree = createTree();
131
130
  const node = tree.addNode(tree.root);
132
- node.ref = Ref.make(task);
131
+ Obj.update(tree.tree, () => {
132
+ node.ref = Ref.make(task);
133
+ });
133
134
  });
134
135
  });
@@ -4,7 +4,8 @@
4
4
 
5
5
  import * as Schema from 'effect/Schema';
6
6
 
7
- import { Key, Obj, Type } from '@dxos/echo';
7
+ import { Key, Obj, Ref, Type } from '@dxos/echo';
8
+ import { TestSchema } from '@dxos/echo/testing';
8
9
  import { invariant } from '@dxos/invariant';
9
10
 
10
11
  // TODO(burdon): Reconcile with @dxos/graph (i.e., common types).
@@ -13,7 +14,7 @@ export const TreeNodeType = Schema.Struct({
13
14
  id: Key.ObjectId,
14
15
  children: Schema.mutable(Schema.Array(Key.ObjectId)),
15
16
  data: Schema.mutable(Schema.Record({ key: Schema.String, value: Schema.Any })),
16
- ref: Schema.optional(Type.Ref(Type.Expando)),
17
+ ref: Schema.optional(Ref.Ref(TestSchema.Expando)),
17
18
  }).pipe(Schema.mutable);
18
19
 
19
20
  export interface TreeNodeType extends Schema.Schema.Type<typeof TreeNodeType> {}
@@ -22,8 +23,8 @@ export const TreeType = Schema.Struct({
22
23
  root: Key.ObjectId,
23
24
  nodes: Schema.mutable(Schema.Record({ key: Key.ObjectId, value: TreeNodeType })),
24
25
  }).pipe(
25
- Type.Obj({
26
- typename: 'dxos.org/type/Tree',
26
+ Type.object({
27
+ typename: 'org.dxos.type.tree',
27
28
  version: '0.1.0',
28
29
  }),
29
30
  );
@@ -184,9 +185,11 @@ export class Tree {
184
185
  clear(): void {
185
186
  const root = this._tree.nodes[this._tree.root];
186
187
  root.children.length = 0;
187
- this._tree.nodes = {
188
- [root.id]: root,
189
- };
188
+ Obj.update(this._tree, (obj) => {
189
+ obj.nodes = {
190
+ [root.id]: root,
191
+ };
192
+ });
190
193
  }
191
194
 
192
195
  /**
@@ -198,8 +201,11 @@ export class Tree {
198
201
  node = { id, children: [], data: { text: '' } }; // TODO(burdon): Generic.
199
202
  }
200
203
 
201
- this._tree.nodes[node.id] = node;
202
- parent.children.splice(index ?? parent.children.length, 0, node.id);
204
+ const nodeToAdd = node;
205
+ Obj.update(this._tree, (obj) => {
206
+ obj.nodes[nodeToAdd.id] = nodeToAdd;
207
+ parent.children.splice(index ?? parent.children.length, 0, nodeToAdd.id);
208
+ });
203
209
  return node;
204
210
  }
205
211
 
@@ -212,10 +218,14 @@ export class Tree {
212
218
  return undefined;
213
219
  }
214
220
 
215
- delete this._tree.nodes[node.id];
221
+ Obj.update(this._tree, (obj) => {
222
+ delete obj.nodes[node.id];
223
+ });
216
224
  const idx = parent.children.findIndex((child) => child === id);
217
225
  if (idx !== -1) {
218
- parent.children.splice(idx, 1);
226
+ Obj.update(this._tree, () => {
227
+ parent.children.splice(idx, 1);
228
+ });
219
229
  }
220
230
 
221
231
  return node;
@@ -232,8 +242,10 @@ export class Tree {
232
242
  }
233
243
 
234
244
  const child = node.children[from];
235
- node.children.splice(from, 1);
236
- node.children.splice(to, 0, child);
245
+ Obj.update(this._tree, () => {
246
+ node.children.splice(from, 1);
247
+ node.children.splice(to, 0, child);
248
+ });
237
249
  return this.getNode(child);
238
250
  }
239
251
 
@@ -252,8 +264,10 @@ export class Tree {
252
264
  }
253
265
 
254
266
  const previous = this.getNode(parent.children[idx - 1]);
255
- parent.children.splice(idx, 1);
256
- previous.children.push(node.id);
267
+ Obj.update(this._tree, () => {
268
+ parent.children.splice(idx, 1);
269
+ previous.children.push(node.id);
270
+ });
257
271
  }
258
272
 
259
273
  /**
@@ -270,16 +284,23 @@ export class Tree {
270
284
  return;
271
285
  }
272
286
 
273
- // Remove node from parent.
287
+ // Remove node from parent and get following siblings.
274
288
  const nodeIdx = parent.children.findIndex((id) => id === node.id);
275
- const [_, ...rest] = parent.children.splice(nodeIdx, parent.children.length - nodeIdx);
276
- parent.children.splice(nodeIdx, parent.children.length - nodeIdx);
289
+ let rest: Key.ObjectId[] = [];
290
+ Obj.update(this._tree, () => {
291
+ const removed = parent.children.splice(nodeIdx, parent.children.length - nodeIdx);
292
+ rest = removed.slice(1); // Skip the node itself.
293
+ });
277
294
 
278
295
  // Add to ancestor.
279
296
  const parentIdx = this.getChildNodes(ancestor).findIndex((n) => n.id === parent.id);
280
- ancestor.children.splice(parentIdx + 1, 0, node.id);
297
+ Obj.update(this._tree, () => {
298
+ ancestor.children.splice(parentIdx + 1, 0, node.id);
299
+ });
281
300
 
282
301
  // Transplant following siblings to current node.
283
- node.children.push(...rest);
302
+ Obj.update(this._tree, () => {
303
+ node.children.push(...rest);
304
+ });
284
305
  }
285
306
  }
@@ -2,40 +2,49 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import { type GraphModel } from '@dxos/graph';
5
+ import { type Key } from '@dxos/echo';
6
6
 
7
- export type TreeNode = {
7
+ import { type TreeType } from './tree';
8
+
9
+ /**
10
+ * In-memory tree shape used by the d3 layouts.
11
+ * `data` carries through to layout callbacks (e.g. hover/inspect) — typically an ECHO object on leaves.
12
+ */
13
+ export type TreeNode<TData = unknown> = {
8
14
  id: string;
9
15
  label?: string;
10
- children?: TreeNode[];
16
+ data?: TData;
17
+ children?: TreeNode<TData>[];
11
18
  };
12
19
 
13
- export const mapGraphToTreeData = (model: GraphModel, maxDepth = 8): TreeNode | undefined => {
14
- // TODO(burdon): Convert to common/graph.
15
- // const mapNode = (node: N, depth = 0): TreeNode => {
16
- // const treeNode: TreeNode = {
17
- // id: model.idAccessor(node),
18
- // label: model.idAccessor(node).slice(0, 8),
19
- // };
20
-
21
- // const links = model.graph.links.filter((link) => link.source === treeNode.id);
22
- // if (depth < maxDepth) {
23
- // treeNode.children = links.map((link) =>
24
- // mapNode(model.graph.nodes.find((node) => model.idAccessor(node) === link.target)!, depth + 1),
25
- // );
26
- // }
20
+ /**
21
+ * Convert an ECHO `TreeType` (id-keyed node map) into a nested `TreeNode` hierarchy.
22
+ * Returns `undefined` if the root id is missing the tree is then incomplete and shouldn't render.
23
+ */
24
+ export const treeTypeToTreeNode = (
25
+ tree: TreeType,
26
+ rootId: Key.ObjectId = tree.root,
27
+ visited: Set<string> = new Set(),
28
+ ): TreeNode | undefined => {
29
+ const node = tree.nodes[rootId];
30
+ if (!node) {
31
+ return undefined;
32
+ }
33
+ if (visited.has(rootId)) {
34
+ return { id: rootId, label: labelOf(node), data: node.data };
35
+ }
36
+ visited.add(rootId);
27
37
 
28
- // return treeNode;
29
- // };
30
-
31
- let data: TreeNode | undefined;
32
- // TODO(burdon): Selection model.
33
- // if (model.selected) {
34
- // const node = model.graph.nodes.find((node) => model.idAccessor(node) === model.selected);
35
- // if (node) {
36
- // data = mapNode(node);
37
- // }
38
- // }
38
+ return {
39
+ id: rootId,
40
+ label: labelOf(node),
41
+ data: node.data,
42
+ children: node.children
43
+ .map((childId) => treeTypeToTreeNode(tree, childId, visited))
44
+ .filter((c): c is TreeNode => Boolean(c)),
45
+ };
46
+ };
39
47
 
40
- return data;
48
+ const labelOf = (node: { data: Record<string, any> }): string | undefined => {
49
+ return typeof node.data?.text === 'string' ? node.data.text : undefined;
41
50
  };
@@ -2,11 +2,7 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import { type ComponentType, lazy } from 'react';
6
-
7
5
  export * from './Chart';
8
6
  export * from './Globe';
9
7
  export * from './Graph';
10
8
  export * from './Tree';
11
-
12
- export const ExplorerContainer: ComponentType<any> = lazy(() => import('./ExplorerContainer'));
@@ -0,0 +1,119 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import { type Meta, type StoryObj } from '@storybook/react-vite';
6
+ import * as Effect from 'effect/Effect';
7
+ import React from 'react';
8
+
9
+ import { withPluginManager } from '@dxos/app-framework/testing';
10
+ import { Filter, Query, Type, View } from '@dxos/echo';
11
+ import { ClientPlugin, initializeIdentity } from '@dxos/plugin-client/testing';
12
+ import { PreviewPlugin } from '@dxos/plugin-preview/testing';
13
+ import { StorybookPlugin, corePlugins } from '@dxos/plugin-testing';
14
+ import { random } from '@dxos/random';
15
+ import { useQuery, useSpaces } from '@dxos/react-client/echo';
16
+ import { Loading, withLayout, withTheme } from '@dxos/react-ui/testing';
17
+ import { ViewModel } from '@dxos/schema';
18
+ import { type ValueGenerator } from '@dxos/schema/testing';
19
+ import { HasRelationship, Organization, Person, Pipeline } from '@dxos/types';
20
+
21
+ import { generate } from '../../components/Tree/testing';
22
+ import { Graph } from '../../types';
23
+ import { ExplorerArticle, type ExplorerArticleVariant } from './ExplorerArticle';
24
+
25
+ const generator = random as any as ValueGenerator;
26
+
27
+ random.seed(7);
28
+
29
+ type StoryArgs = { variant: ExplorerArticleVariant };
30
+
31
+ const DefaultStory = ({ variant }: StoryArgs) => {
32
+ const [space] = useSpaces();
33
+ const [graph] = useQuery(space?.db, Filter.type(Graph.Graph));
34
+ if (!space || !graph) {
35
+ return <Loading data={{ space: !!space, graph: !!graph }} />;
36
+ }
37
+
38
+ return <ExplorerArticle role='article' subject={graph as any} attendableId={graph.id} variant={variant} />;
39
+ };
40
+
41
+ const meta: Meta<StoryArgs> = {
42
+ title: 'plugins/plugin-explorer/containers/ExplorerArticle',
43
+ render: DefaultStory,
44
+ decorators: [
45
+ withTheme(),
46
+ withLayout({ layout: 'fullscreen' }),
47
+ withPluginManager({
48
+ plugins: [
49
+ ...corePlugins(),
50
+ StorybookPlugin({}),
51
+ ClientPlugin({
52
+ types: [
53
+ Graph.Graph,
54
+ View.View,
55
+ HasRelationship.HasRelationship,
56
+ Organization.Organization,
57
+ Pipeline.Pipeline,
58
+ Person.Person,
59
+ ],
60
+ onClientInitialized: ({ client }) =>
61
+ Effect.gen(function* () {
62
+ const { personalSpace } = yield* initializeIdentity(client);
63
+ yield* Effect.promise(() => generate(personalSpace, generator));
64
+ const { view } = yield* Effect.promise(() =>
65
+ ViewModel.makeFromDatabase({ db: personalSpace.db, typename: Type.getTypename(Graph.Graph) }),
66
+ );
67
+ const graph = personalSpace.db.add(
68
+ Graph.make({
69
+ name: 'Test',
70
+ view,
71
+ query: { raw: '', ast: Query.select(Filter.everything()).ast },
72
+ }),
73
+ );
74
+ yield* Effect.promise(() => personalSpace.db.flush({ indexes: true }));
75
+ return graph;
76
+ }),
77
+ }),
78
+ PreviewPlugin(),
79
+ ],
80
+ }),
81
+ ],
82
+ parameters: {
83
+ layout: 'fullscreen',
84
+ },
85
+ };
86
+
87
+ export default meta;
88
+
89
+ type Story = StoryObj<StoryArgs>;
90
+
91
+ /**
92
+ * Default force-directed view (the production layout).
93
+ */
94
+ export const Force: Story = {
95
+ args: {
96
+ variant: 'force',
97
+ },
98
+ };
99
+
100
+ /**
101
+ * Radial cluster: every object on the perimeter, grouped by its schema, all under
102
+ * a single database root. Inspired by https://observablehq.com/@d3/radial-cluster.
103
+ */
104
+ export const Cluster: Story = {
105
+ args: {
106
+ variant: 'cluster',
107
+ },
108
+ };
109
+
110
+ /**
111
+ * Hierarchical edge bundling: same hierarchy as `cluster`, with bundled curves
112
+ * routed through the lowest common ancestor for every relation / ref in the space.
113
+ * Inspired by https://observablehq.com/@d3/hierarchical-edge-bundling.
114
+ */
115
+ export const Bundle: Story = {
116
+ args: {
117
+ variant: 'bundle',
118
+ },
119
+ };