@dxos/plugin-explorer 0.8.4-main.422d1c7879 → 0.8.4-main.4668b7de9b

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 (238) hide show
  1. package/LICENSE +102 -5
  2. package/PLUGIN.mdl +340 -0
  3. package/dist/lib/neutral/ExplorerArticle-4I7PNGDC.mjs +459 -0
  4. package/dist/lib/neutral/ExplorerArticle-4I7PNGDC.mjs.map +7 -0
  5. package/dist/lib/neutral/ExplorerPlugin.mjs +10 -0
  6. package/dist/lib/neutral/capabilities/index.mjs +11 -0
  7. package/dist/lib/neutral/capabilities/index.mjs.map +7 -0
  8. package/dist/lib/neutral/chunk-3D7BYXOR.mjs +37 -0
  9. package/dist/lib/neutral/chunk-3D7BYXOR.mjs.map +7 -0
  10. package/dist/lib/neutral/chunk-42BYLQQA.mjs +42 -0
  11. package/dist/lib/neutral/chunk-42BYLQQA.mjs.map +7 -0
  12. package/dist/lib/neutral/chunk-7XUDLV6E.mjs +287 -0
  13. package/dist/lib/neutral/chunk-7XUDLV6E.mjs.map +7 -0
  14. package/dist/lib/{browser/types/index.mjs → neutral/chunk-HI324IB4.mjs} +12 -13
  15. package/dist/lib/neutral/chunk-HI324IB4.mjs.map +7 -0
  16. package/dist/lib/neutral/chunk-IKHJV3Q4.mjs +20 -0
  17. package/dist/lib/neutral/chunk-IKHJV3Q4.mjs.map +7 -0
  18. package/dist/lib/{browser → neutral/components}/index.mjs +730 -437
  19. package/dist/lib/neutral/components/index.mjs.map +7 -0
  20. package/dist/lib/neutral/containers/index.mjs +9 -0
  21. package/dist/lib/neutral/containers/index.mjs.map +7 -0
  22. package/dist/lib/neutral/create-object-F6TKVAGV.mjs +39 -0
  23. package/dist/lib/neutral/create-object-F6TKVAGV.mjs.map +7 -0
  24. package/dist/lib/neutral/hooks/index.mjs +45 -0
  25. package/dist/lib/neutral/hooks/index.mjs.map +7 -0
  26. package/dist/lib/neutral/index.mjs +14 -0
  27. package/dist/lib/neutral/meta.json +1 -0
  28. package/dist/lib/{browser → neutral}/meta.mjs +1 -1
  29. package/dist/lib/neutral/plugin.mjs +12 -0
  30. package/dist/lib/neutral/plugin.mjs.map +7 -0
  31. package/dist/lib/neutral/react-surface-APBW2VQG.mjs +26 -0
  32. package/dist/lib/neutral/react-surface-APBW2VQG.mjs.map +7 -0
  33. package/dist/lib/neutral/testing/index.mjs +139 -0
  34. package/dist/lib/neutral/testing/index.mjs.map +7 -0
  35. package/dist/lib/neutral/translations.mjs +33 -0
  36. package/dist/lib/neutral/translations.mjs.map +7 -0
  37. package/dist/lib/neutral/types/index.mjs +10 -0
  38. package/dist/lib/neutral/types/index.mjs.map +7 -0
  39. package/dist/types/data/cities.d.ts +4 -4
  40. package/dist/types/data/cities.d.ts.map +1 -1
  41. package/dist/types/data/countries-110m.d.ts +19 -22
  42. package/dist/types/data/countries-110m.d.ts.map +1 -1
  43. package/dist/types/src/ExplorerPlugin.d.ts +1 -0
  44. package/dist/types/src/ExplorerPlugin.d.ts.map +1 -1
  45. package/dist/types/src/ExplorerPlugin.test.d.ts +2 -0
  46. package/dist/types/src/ExplorerPlugin.test.d.ts.map +1 -0
  47. package/dist/types/src/capabilities/create-object.d.ts +11 -0
  48. package/dist/types/src/capabilities/create-object.d.ts.map +1 -0
  49. package/dist/types/src/capabilities/index.d.ts +6 -0
  50. package/dist/types/src/capabilities/index.d.ts.map +1 -1
  51. package/dist/types/src/capabilities/react-surface.d.ts.map +1 -1
  52. package/dist/types/src/components/Chart/Chart.d.ts +1 -1
  53. package/dist/types/src/components/Chart/Chart.d.ts.map +1 -1
  54. package/dist/types/src/components/Chart/Chart.stories.d.ts +4 -1
  55. package/dist/types/src/components/Chart/Chart.stories.d.ts.map +1 -1
  56. package/dist/types/src/components/Globe/Globe.d.ts +1 -1
  57. package/dist/types/src/components/Globe/Globe.d.ts.map +1 -1
  58. package/dist/types/src/components/Globe/Globe.stories.d.ts +5 -2
  59. package/dist/types/src/components/Globe/Globe.stories.d.ts.map +1 -1
  60. package/dist/types/src/components/Graph/CanvasForceGraph.d.ts +13 -0
  61. package/dist/types/src/components/Graph/CanvasForceGraph.d.ts.map +1 -0
  62. package/dist/types/src/components/Graph/{D3ForceGraph.stories.d.ts → CanvasForceGraph.stories.d.ts} +3 -3
  63. package/dist/types/src/components/Graph/CanvasForceGraph.stories.d.ts.map +1 -0
  64. package/dist/types/src/components/Graph/ForceGraph.d.ts +12 -5
  65. package/dist/types/src/components/Graph/ForceGraph.d.ts.map +1 -1
  66. package/dist/types/src/components/Graph/ForceGraph.stories.d.ts +3 -1
  67. package/dist/types/src/components/Graph/ForceGraph.stories.d.ts.map +1 -1
  68. package/dist/types/src/components/Graph/{adapter.d.ts → graph-adapter.d.ts} +1 -1
  69. package/dist/types/src/components/Graph/graph-adapter.d.ts.map +1 -0
  70. package/dist/types/src/components/Graph/index.d.ts +1 -1
  71. package/dist/types/src/components/Graph/index.d.ts.map +1 -1
  72. package/dist/types/src/components/Lattice/Lattice.d.ts +20 -0
  73. package/dist/types/src/components/Lattice/Lattice.d.ts.map +1 -0
  74. package/dist/types/src/components/Lattice/Lattice.stories.d.ts +8 -0
  75. package/dist/types/src/components/Lattice/Lattice.stories.d.ts.map +1 -0
  76. package/dist/types/src/components/Lattice/index.d.ts +2 -0
  77. package/dist/types/src/components/Lattice/index.d.ts.map +1 -0
  78. package/dist/types/src/components/Tree/EdgeBundling.stories.d.ts +21 -0
  79. package/dist/types/src/components/Tree/EdgeBundling.stories.d.ts.map +1 -0
  80. package/dist/types/src/components/Tree/Tree.d.ts +20 -23
  81. package/dist/types/src/components/Tree/Tree.d.ts.map +1 -1
  82. package/dist/types/src/components/Tree/Tree.stories.d.ts +5 -12
  83. package/dist/types/src/components/Tree/Tree.stories.d.ts.map +1 -1
  84. package/dist/types/src/components/Tree/index.d.ts +2 -0
  85. package/dist/types/src/components/Tree/index.d.ts.map +1 -1
  86. package/dist/types/src/components/Tree/layout/HierarchicalEdgeBundling.d.ts +37 -2
  87. package/dist/types/src/components/Tree/layout/HierarchicalEdgeBundling.d.ts.map +1 -1
  88. package/dist/types/src/components/Tree/layout/RadialTree.d.ts +35 -2
  89. package/dist/types/src/components/Tree/layout/RadialTree.d.ts.map +1 -1
  90. package/dist/types/src/components/Tree/layout/TidyTree.d.ts +24 -2
  91. package/dist/types/src/components/Tree/layout/TidyTree.d.ts.map +1 -1
  92. package/dist/types/src/components/Tree/layout/hierarchy.d.ts +17 -0
  93. package/dist/types/src/components/Tree/layout/hierarchy.d.ts.map +1 -0
  94. package/dist/types/src/components/Tree/layout/index.d.ts +5 -4
  95. package/dist/types/src/components/Tree/layout/index.d.ts.map +1 -1
  96. package/dist/types/src/components/Tree/layout/slots.d.ts +7 -0
  97. package/dist/types/src/components/Tree/layout/slots.d.ts.map +1 -0
  98. package/dist/types/src/components/Tree/layout/useContainerSize.d.ts +15 -0
  99. package/dist/types/src/components/Tree/layout/useContainerSize.d.ts.map +1 -0
  100. package/dist/types/src/components/Tree/types/tree.d.ts +41 -20
  101. package/dist/types/src/components/Tree/types/tree.d.ts.map +1 -1
  102. package/dist/types/src/components/Tree/types/types.d.ts +14 -4
  103. package/dist/types/src/components/Tree/types/types.d.ts.map +1 -1
  104. package/dist/types/src/components/index.d.ts +1 -0
  105. package/dist/types/src/components/index.d.ts.map +1 -1
  106. package/dist/types/src/containers/ExplorerArticle/ExplorerArticle.d.ts +8 -0
  107. package/dist/types/src/containers/ExplorerArticle/ExplorerArticle.d.ts.map +1 -0
  108. package/dist/types/src/containers/ExplorerArticle/ExplorerArticle.stories.d.ts +15 -0
  109. package/dist/types/src/containers/ExplorerArticle/ExplorerArticle.stories.d.ts.map +1 -0
  110. package/dist/types/src/containers/ExplorerArticle/Visualization.d.ts +18 -0
  111. package/dist/types/src/containers/ExplorerArticle/Visualization.d.ts.map +1 -0
  112. package/dist/types/src/containers/ExplorerArticle/index.d.ts +2 -0
  113. package/dist/types/src/containers/ExplorerArticle/index.d.ts.map +1 -0
  114. package/dist/types/src/containers/ExplorerArticle/variants.d.ts +9 -0
  115. package/dist/types/src/containers/ExplorerArticle/variants.d.ts.map +1 -0
  116. package/dist/types/src/containers/index.d.ts +1 -1
  117. package/dist/types/src/containers/index.d.ts.map +1 -1
  118. package/dist/types/src/hooks/useGraphModel.d.ts +2 -2
  119. package/dist/types/src/hooks/useGraphModel.d.ts.map +1 -1
  120. package/dist/types/src/index.d.ts +1 -3
  121. package/dist/types/src/index.d.ts.map +1 -1
  122. package/dist/types/src/meta.d.ts +1 -1
  123. package/dist/types/src/meta.d.ts.map +1 -1
  124. package/dist/types/src/plugin.d.ts +3 -0
  125. package/dist/types/src/plugin.d.ts.map +1 -0
  126. package/dist/types/src/{components/Tree/testing → testing}/generator.d.ts +1 -1
  127. package/dist/types/src/testing/generator.d.ts.map +1 -0
  128. package/dist/types/src/testing/index.d.ts +4 -0
  129. package/dist/types/src/testing/index.d.ts.map +1 -0
  130. package/dist/types/src/testing/relations.d.ts +32 -0
  131. package/dist/types/src/testing/relations.d.ts.map +1 -0
  132. package/dist/types/src/translations.d.ts +17 -17
  133. package/dist/types/src/translations.d.ts.map +1 -1
  134. package/dist/types/src/types/Graph.d.ts +5 -6
  135. package/dist/types/src/types/Graph.d.ts.map +1 -1
  136. package/dist/types/src/util/index.d.ts +3 -0
  137. package/dist/types/src/util/index.d.ts.map +1 -0
  138. package/dist/types/src/util/node-color.d.ts +13 -0
  139. package/dist/types/src/util/node-color.d.ts.map +1 -0
  140. package/dist/types/src/{components → util}/plot.d.ts +1 -1
  141. package/dist/types/src/util/plot.d.ts.map +1 -0
  142. package/dist/types/tsconfig.tsbuildinfo +1 -1
  143. package/package.json +100 -56
  144. package/src/ExplorerPlugin.test.ts +26 -0
  145. package/src/ExplorerPlugin.tsx +11 -34
  146. package/src/capabilities/create-object.ts +36 -0
  147. package/src/capabilities/index.ts +1 -0
  148. package/src/capabilities/react-surface.tsx +2 -2
  149. package/src/components/Chart/Chart.stories.tsx +14 -20
  150. package/src/components/Chart/Chart.tsx +1 -1
  151. package/src/components/Globe/Globe.stories.tsx +17 -19
  152. package/src/components/Globe/Globe.tsx +1 -1
  153. package/src/components/Graph/CanvasForceGraph.stories.tsx +97 -0
  154. package/src/components/Graph/CanvasForceGraph.tsx +124 -0
  155. package/src/components/Graph/ForceGraph.stories.tsx +88 -38
  156. package/src/components/Graph/ForceGraph.tsx +105 -85
  157. package/src/components/Graph/index.ts +1 -1
  158. package/src/components/Lattice/Lattice.stories.tsx +104 -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 +17 -38
  163. package/src/components/Tree/Tree.tsx +69 -100
  164. package/src/components/Tree/index.ts +2 -0
  165. package/src/components/Tree/layout/HierarchicalEdgeBundling.tsx +335 -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 +2 -2
  173. package/src/components/Tree/types/tree.ts +23 -28
  174. package/src/components/Tree/types/types.ts +38 -29
  175. package/src/components/index.ts +1 -0
  176. package/src/containers/ExplorerArticle/ExplorerArticle.stories.tsx +152 -0
  177. package/src/containers/ExplorerArticle/ExplorerArticle.tsx +120 -0
  178. package/src/containers/ExplorerArticle/Visualization.tsx +523 -0
  179. package/src/containers/ExplorerArticle/index.ts +5 -0
  180. package/src/containers/ExplorerArticle/variants.ts +47 -0
  181. package/src/containers/index.ts +1 -1
  182. package/src/hooks/useGraphModel.ts +10 -6
  183. package/src/index.ts +1 -4
  184. package/src/meta.ts +26 -7
  185. package/src/plugin.ts +9 -0
  186. package/src/{components/Tree/testing → testing}/generator.ts +3 -3
  187. package/src/testing/index.ts +9 -0
  188. package/src/testing/relations.ts +117 -0
  189. package/src/translations.ts +1 -1
  190. package/src/types/ExplorerAction.ts +1 -1
  191. package/src/types/Graph.ts +6 -14
  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/src/vite-env.d.ts +10 -0
  196. package/dist/lib/browser/chunk-LSUP47BZ.mjs +0 -24
  197. package/dist/lib/browser/chunk-LSUP47BZ.mjs.map +0 -7
  198. package/dist/lib/browser/index.mjs.map +0 -7
  199. package/dist/lib/browser/meta.json +0 -1
  200. package/dist/lib/browser/types/index.mjs.map +0 -7
  201. package/dist/lib/node-esm/chunk-EN3JZNEY.mjs +0 -26
  202. package/dist/lib/node-esm/chunk-EN3JZNEY.mjs.map +0 -7
  203. package/dist/lib/node-esm/chunk-HSLMI22Q.mjs +0 -11
  204. package/dist/lib/node-esm/index.mjs +0 -11375
  205. package/dist/lib/node-esm/index.mjs.map +0 -7
  206. package/dist/lib/node-esm/meta.json +0 -1
  207. package/dist/lib/node-esm/meta.mjs +0 -9
  208. package/dist/lib/node-esm/types/index.mjs +0 -71
  209. package/dist/lib/node-esm/types/index.mjs.map +0 -7
  210. package/dist/types/src/components/Graph/D3ForceGraph.d.ts +0 -15
  211. package/dist/types/src/components/Graph/D3ForceGraph.d.ts.map +0 -1
  212. package/dist/types/src/components/Graph/D3ForceGraph.stories.d.ts.map +0 -1
  213. package/dist/types/src/components/Graph/adapter.d.ts.map +0 -1
  214. package/dist/types/src/components/Graph/testing.d.ts +0 -14
  215. package/dist/types/src/components/Graph/testing.d.ts.map +0 -1
  216. package/dist/types/src/components/Tree/testing/generator.d.ts.map +0 -1
  217. package/dist/types/src/components/Tree/testing/index.d.ts +0 -2
  218. package/dist/types/src/components/Tree/testing/index.d.ts.map +0 -1
  219. package/dist/types/src/components/plot.d.ts.map +0 -1
  220. package/dist/types/src/containers/ExplorerContainer/ExplorerContainer.d.ts +0 -6
  221. package/dist/types/src/containers/ExplorerContainer/ExplorerContainer.d.ts.map +0 -1
  222. package/dist/types/src/containers/ExplorerContainer/index.d.ts +0 -2
  223. package/dist/types/src/containers/ExplorerContainer/index.d.ts.map +0 -1
  224. package/src/components/Graph/D3ForceGraph.stories.tsx +0 -83
  225. package/src/components/Graph/D3ForceGraph.tsx +0 -108
  226. package/src/components/Graph/testing.ts +0 -58
  227. package/src/components/Tree/layout/HierarchicalEdgeBundling.ts +0 -162
  228. package/src/components/Tree/layout/RadialTree.ts +0 -94
  229. package/src/components/Tree/layout/TidyTree.ts +0 -101
  230. package/src/components/Tree/testing/index.ts +0 -5
  231. package/src/containers/ExplorerContainer/ExplorerContainer.tsx +0 -53
  232. package/src/containers/ExplorerContainer/index.ts +0 -5
  233. /package/dist/lib/{browser/chunk-J5LGTIGS.mjs.map → neutral/ExplorerPlugin.mjs.map} +0 -0
  234. /package/dist/lib/{browser → neutral}/chunk-J5LGTIGS.mjs +0 -0
  235. /package/dist/lib/{browser/meta.mjs.map → neutral/chunk-J5LGTIGS.mjs.map} +0 -0
  236. /package/dist/lib/{node-esm/chunk-HSLMI22Q.mjs.map → neutral/index.mjs.map} +0 -0
  237. /package/dist/lib/{node-esm → neutral}/meta.mjs.map +0 -0
  238. /package/src/components/Graph/{adapter.ts → graph-adapter.ts} +0 -0
@@ -0,0 +1,523 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import { curveCatmullRom, line as d3Line } from 'd3';
6
+ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
7
+
8
+ import { Obj } from '@dxos/echo';
9
+ import { type ThemedClassName } from '@dxos/react-ui';
10
+ import {
11
+ CLUSTER_NODE_TYPE_LEAF,
12
+ CLUSTER_NODE_TYPE_ROOT,
13
+ GraphBundleProjector,
14
+ GraphClusterProjector,
15
+ GraphForceProjector,
16
+ type GraphLayout,
17
+ type GraphLayoutNode,
18
+ GraphLatticeProjector,
19
+ type GraphLayoutEdge,
20
+ GraphPlexusProjector,
21
+ type GraphProjector,
22
+ GraphSwarmProjector,
23
+ type ModelPoint,
24
+ PLEXUS_NODE_TYPE_FOCUS,
25
+ PLEXUS_NODE_TYPE_RELATION,
26
+ type PlexusRelation,
27
+ type RenderNode,
28
+ SVG,
29
+ type SVGContext,
30
+ type SwarmNode,
31
+ appendRadialGroupLabel,
32
+ appendRadialLeafLabel,
33
+ appendRootLabel,
34
+ } from '@dxos/react-ui-graph';
35
+ import { type SpaceGraphEdge, type SpaceGraphModel, type SpaceGraphNode } from '@dxos/schema';
36
+ import { mx } from '@dxos/ui-theme';
37
+
38
+ import { type TreeNode } from '#components';
39
+
40
+ import { getNodeFillForObject } from '../../util';
41
+ import { type ExplorerArticleVariant } from './variants';
42
+
43
+ export type VisualizationProps = ThemedClassName<{
44
+ debug?: boolean;
45
+ variant: ExplorerArticleVariant;
46
+ model: SpaceGraphModel;
47
+ onNodeHover?: (node: TreeNode | null, event?: MouseEvent) => void;
48
+ /** Called when the user clicks the visualization surface (e.g. to dismiss a node preview). */
49
+ onSurfaceClick?: () => void;
50
+ }>;
51
+
52
+ /**
53
+ * Renders the active visualization variant.
54
+ */
55
+ export const Visualization = ({
56
+ classNames,
57
+ debug = true,
58
+ variant,
59
+ model,
60
+ onNodeHover,
61
+ onSurfaceClick,
62
+ }: VisualizationProps) => {
63
+ const svgRef = useRef<SVGContext>(null);
64
+ const [projector, setProjector] = useState<GraphProjector<SpaceGraphNode> | undefined>();
65
+ const projectorRef = useRef<GraphProjector<SpaceGraphNode> | undefined>(undefined);
66
+ projectorRef.current = projector;
67
+
68
+ // Plexus focus — single source of truth for both the bundle→plexus dispatch and re-focus.
69
+ // `undefined` means "no node focused", which dispatches to the bundle projector.
70
+ const [focusId, setFocusId] = useState<string | undefined>(undefined);
71
+
72
+ // Latest layout we hand to the next SVG projector as `prev`. Captured from the live
73
+ // projector when the variant changes so node x/y survive the swap.
74
+ const lastLayoutRef = useRef<GraphLayout<SpaceGraphNode> | undefined>(undefined);
75
+
76
+ // Subscribe to the graph atom — keeps it alive across child unmounts (effect-atom
77
+ // disposes atoms with no subscribers) and triggers re-renders when query results land.
78
+ useEffect(() => model?.subscribe(() => undefined), [model]);
79
+
80
+ // Clear any focus when leaving plexus so returning to it starts from the bundle layout.
81
+ useEffect(() => {
82
+ if (variant !== 'plexus') {
83
+ setFocusId(undefined);
84
+ }
85
+ }, [variant]);
86
+
87
+ // Recreate the projector when the variant or focus changes; snapshot the live layout
88
+ // first so the next projector animates from where the previous one left off.
89
+ useEffect(() => {
90
+ if (projectorRef.current?.layout) {
91
+ lastLayoutRef.current = projectorRef.current.layout as GraphLayout<SpaceGraphNode>;
92
+ }
93
+ if (!svgRef.current) {
94
+ return;
95
+ }
96
+
97
+ setProjector(createProjector(svgRef.current, variant, focusId, lastLayoutRef.current));
98
+ }, [variant, focusId]);
99
+
100
+ // Plexus: clicking an object node re-focuses on it; clicking the current focus clears it
101
+ // (back to the bundle layout). Relation nodes carry no ECHO object, so their clicks are ignored.
102
+ const handleSelect = useCallback(
103
+ (node: GraphLayoutNode<SpaceGraphNode>) => {
104
+ if (variant !== 'plexus') {
105
+ return;
106
+ }
107
+ if (!node.data?.data?.object) {
108
+ return;
109
+ }
110
+ setFocusId((current) => (current === node.id ? undefined : node.id));
111
+ },
112
+ [variant],
113
+ );
114
+
115
+ const renderNode = useMemo(() => createRenderNode(variant), [variant]);
116
+
117
+ // Per-tick swarm tail update. Receives the dx-node `<g>` after its transform is set,
118
+ // so the polyline points + gradient axis can live in node-local coordinates.
119
+ const applyNode = useMemo(() => (variant === 'swarm' ? applyNodeSwarm : undefined), [variant]);
120
+
121
+ // Cursor avoidance for the SVG swarm. The Graph component hands us pre-transformed
122
+ // SVG model coordinates — same space the boids live in.
123
+ const handlePointerMove = useCallback(
124
+ (point: ModelPoint) => {
125
+ if (projector instanceof GraphSwarmProjector) {
126
+ projector.setCursor(point.x, point.y);
127
+ }
128
+ },
129
+ [projector],
130
+ );
131
+
132
+ const handlePointerLeave = useCallback(() => {
133
+ if (projector instanceof GraphSwarmProjector) {
134
+ projector.setCursor(null);
135
+ }
136
+ }, [projector]);
137
+
138
+ const handleInspect = useCallback(
139
+ (node: GraphLayoutNode<SpaceGraphNode> | null, event: MouseEvent) => {
140
+ if (variant === 'plexus') {
141
+ return;
142
+ }
143
+
144
+ onNodeHover?.(node ? { id: node.id, data: node.data?.data?.object } : null, event);
145
+ },
146
+ [variant === 'plexus' ? undefined : onNodeHover],
147
+ );
148
+
149
+ // Only attach pointer handlers when the SVG swarm is active — other variants don't
150
+ // need them and we want to avoid the per-move CTM math when it'd be a no-op.
151
+ const swarmPointerProps =
152
+ variant === 'swarm' ? { onPointerMove: handlePointerMove, onPointerLeave: handlePointerLeave } : undefined;
153
+
154
+ return (
155
+ <div className={mx('dx-expander relative', classNames)} onClick={onSurfaceClick}>
156
+ <SVG.Root ref={svgRef}>
157
+ <SVG.Zoom extent={[1 / 2, 2]}>
158
+ <SVG.Graph<SpaceGraphNode, SpaceGraphEdge>
159
+ model={model}
160
+ projector={projector}
161
+ renderNode={renderNode}
162
+ applyNode={applyNode}
163
+ edgeOpacity={variant === 'swarm' ? 0.3 : undefined}
164
+ drag={variant === 'force'}
165
+ highlightOnHover={variant === 'bundle'}
166
+ onSelect={handleSelect}
167
+ onInspect={handleInspect}
168
+ {...swarmPointerProps}
169
+ />
170
+ </SVG.Zoom>
171
+ {debug && <SVG.FPS />}
172
+ </SVG.Root>
173
+ </div>
174
+ );
175
+ };
176
+
177
+ /** Cross-variant tween duration. Matches the renderer's edge fade timing so node movement and edge enter/exit complete together. */
178
+ const TWEEN_MS = 500;
179
+
180
+ /** Fade-in duration applied to labels after the layout tween completes. */
181
+ const LABEL_FADE_MS = 200;
182
+
183
+ /** Base radius shared by all plexus leaf + relation nodes; the focus node is double. */
184
+ const PLEXUS_LEAF_RADIUS = 5;
185
+ const PLEXUS_FOCUS_RADIUS = PLEXUS_LEAF_RADIUS * 2;
186
+
187
+ /**
188
+ * Catmull-Rom curve generator (α=0.5, "centripetal") for swarm boid trails.
189
+ * Passes through every point and avoids the looping/overshoot artifacts a plain
190
+ * cardinal spline produces when consecutive history samples land close together.
191
+ */
192
+ const swarmTrailLine = d3Line<[number, number]>().curve(curveCatmullRom.alpha(0.5));
193
+
194
+ /**
195
+ * Per-tick swarm tail update. The dx-node `<g>` transform has just been written, so we work
196
+ * in node-local coordinates: head at (0,0), history deltas trailing behind. The single
197
+ * `<path>` is stroked with a per-boid `<linearGradient>` whose endpoints we sync to the
198
+ * tail axis so the fade tracks the direction of travel.
199
+ */
200
+ const applyNodeSwarm = (group: SVGGElement, node: GraphLayoutNode<SpaceGraphNode>): void => {
201
+ const swarm = node as SwarmNode;
202
+ const path = group.querySelector('path.dx-swarm-tail') as SVGPathElement | null;
203
+ const grad = group.querySelector('linearGradient') as SVGLinearGradientElement | null;
204
+ if (!path || !grad) {
205
+ return;
206
+ }
207
+ const history = swarm.history ?? [];
208
+ if (history.length === 0) {
209
+ path.setAttribute('d', '');
210
+ return;
211
+ }
212
+
213
+ const hx = swarm.x ?? 0;
214
+ const hy = swarm.y ?? 0;
215
+ // Build local-space points head → most-recent → … → oldest (history is push-at-end so
216
+ // we walk it backwards), then run them through a Catmull-Rom curve generator so the
217
+ // trail reads as a smooth arc rather than a polyline. The gradient axis below is still
218
+ // head → oldest, so the fade stays aligned with travel direction.
219
+ const points: Array<[number, number]> = [[0, 0]];
220
+ for (let i = history.length - 1; i >= 0; i--) {
221
+ points.push([history[i].x - hx, history[i].y - hy]);
222
+ }
223
+ path.setAttribute('d', swarmTrailLine(points) ?? '');
224
+ const oldest = history[0];
225
+ grad.setAttribute('x2', String(oldest.x - hx));
226
+ grad.setAttribute('y2', String(oldest.y - hy));
227
+ };
228
+
229
+ const createProjector = (
230
+ ctx: SVGContext,
231
+ variant: ExplorerArticleVariant,
232
+ focusId: string | undefined,
233
+ prev?: GraphLayout<SpaceGraphNode>,
234
+ ): GraphProjector<SpaceGraphNode> => {
235
+ switch (variant) {
236
+ case 'force':
237
+ // Force has no `duration` — its own simulation drives motion via ticks.
238
+ return new GraphForceProjector<SpaceGraphNode>(ctx, undefined, undefined, prev);
239
+
240
+ case 'swarm':
241
+ // Swarm in SVG: a per-tick projector mirroring force's emit-positions pattern.
242
+ return new GraphSwarmProjector<SpaceGraphNode>(ctx, undefined, undefined, prev);
243
+
244
+ case 'lattice':
245
+ return new GraphLatticeProjector<SpaceGraphNode>(
246
+ ctx,
247
+ {
248
+ duration: TWEEN_MS,
249
+ // Plugin-explorer overrides the projector's force-matched default (6)
250
+ // with a smaller node so the lattice reads as a dense matrix.
251
+ radius: 4,
252
+ // Cluster by typename first so same-type rects sit together; break ties by label.
253
+ sortBy: (node: GraphLayoutNode<SpaceGraphNode>) => {
254
+ const obj = node.data?.data?.object;
255
+ const typename = obj ? (Obj.getTypename(obj) ?? '(untyped)') : '(untyped)';
256
+ const label = (obj && Obj.getLabel(obj)) ?? node.data?.data?.label ?? node.id;
257
+ return `${typename} ${label}`;
258
+ },
259
+ },
260
+ undefined,
261
+ prev,
262
+ );
263
+
264
+ case 'cluster':
265
+ return new GraphClusterProjector<SpaceGraphNode>(
266
+ ctx,
267
+ {
268
+ duration: TWEEN_MS,
269
+ groupOf: typenameGroupOf,
270
+ rootLabel: 'Database',
271
+ groupLabel: shortTypename,
272
+ // All three node kinds share the same radius — leaves, groups, and root read
273
+ // as members of the same circle rather than ranked by size.
274
+ rootRadius: 4,
275
+ groupRadius: 4,
276
+ },
277
+ undefined,
278
+ prev,
279
+ );
280
+
281
+ case 'bundle':
282
+ return new GraphBundleProjector<SpaceGraphNode>(
283
+ ctx,
284
+ {
285
+ duration: TWEEN_MS,
286
+ groupOf: typenameGroupOf,
287
+ },
288
+ undefined,
289
+ prev,
290
+ );
291
+
292
+ case 'plexus':
293
+ // No focus → dispatch to the bundle projector (the unfocused overview). Once a node is
294
+ // focused, switch to the focus-centric plexus layout; both receive `prev` so the swap
295
+ // (and every re-focus) animates from the previous node positions.
296
+ if (!focusId) {
297
+ return new GraphBundleProjector<SpaceGraphNode>(
298
+ ctx,
299
+ {
300
+ duration: TWEEN_MS,
301
+ groupOf: typenameGroupOf,
302
+ },
303
+ undefined,
304
+ prev,
305
+ );
306
+ }
307
+ return new GraphPlexusProjector<SpaceGraphNode>(
308
+ ctx,
309
+ {
310
+ duration: TWEEN_MS,
311
+ focus: focusId,
312
+ relationOf: plexusRelationOf,
313
+ leafRadius: PLEXUS_LEAF_RADIUS,
314
+ relationRadius: PLEXUS_LEAF_RADIUS,
315
+ focusRadius: PLEXUS_FOCUS_RADIUS,
316
+ },
317
+ undefined,
318
+ prev,
319
+ );
320
+ }
321
+ };
322
+
323
+ /**
324
+ * Classify an edge incident to the focus into a relation group. Each relation/property gets two
325
+ * possible nodes: one for the outgoing direction (focus is the edge source, arrow `→`) and one
326
+ * for the incoming direction (focus is the edge target, arrow `←`), so both sides of a relation
327
+ * fan out separately. Relations group by relation typename; references group by their top-level
328
+ * property name. Edges not incident to the focus are ignored.
329
+ */
330
+ const plexusRelationOf = (edge: GraphLayoutEdge<SpaceGraphNode>, focusId: string): PlexusRelation | undefined => {
331
+ const outgoing = edge.source.id === focusId;
332
+ const incoming = edge.target.id === focusId;
333
+ if (!outgoing && !incoming) {
334
+ return undefined;
335
+ }
336
+ const direction = outgoing ? 'out' : 'in';
337
+ const arrow = outgoing ? '→' : '←';
338
+
339
+ if (edge.type === 'relation') {
340
+ const relation = (edge.data as any)?.object as Obj.Unknown | undefined;
341
+ const typename = relation ? Obj.getTypename(relation) : undefined;
342
+ const name = typename ? shortTypename(typename) : 'Relation';
343
+ return { key: `relation:${direction}:${typename ?? '?'}`, label: `${name} ${arrow}` };
344
+ }
345
+
346
+ if (edge.type === 'ref') {
347
+ const property = (edge.data as any)?.property as string | undefined;
348
+ const name = property ?? 'References';
349
+ return { key: `ref:${direction}:${property ?? '?'}`, label: `${name} ${arrow}` };
350
+ }
351
+
352
+ return undefined;
353
+ };
354
+
355
+ const createRenderNode = (variant: ExplorerArticleVariant): RenderNode<SpaceGraphNode> | undefined => {
356
+ switch (variant) {
357
+ case 'force':
358
+ return (group, node) => {
359
+ const r = node.r ?? 6;
360
+ group
361
+ .append('circle')
362
+ .attr('r', r)
363
+ .style('cursor', 'pointer')
364
+ .style('fill', getNodeFillForObject(node.data?.data?.object as Obj.Unknown | undefined));
365
+ };
366
+
367
+ case 'swarm':
368
+ // Match the force variant's shape so identity-by-id transitions read continuously.
369
+ // The tail is a SINGLE `<path>` traced through head + history points; its stroke
370
+ // uses a per-boid `<linearGradient>` that fades head → tail. Done this way (rather
371
+ // than as N overlapping `<line>` segments) so coincident segment endpoints don't
372
+ // compound their alpha and read as a striped trail.
373
+ return (group, node) => {
374
+ const fill = getNodeFillForObject(node.data?.data?.object as Obj.Unknown | undefined);
375
+ const r = node.r ?? 6;
376
+ const strokeWidth = Math.max(1, r * 0.6);
377
+ // Gradient id must be unique document-wide. node.id is the DXN, which contains
378
+ // characters (`:`, `/`, etc.) that aren't valid in NCName-style ids, so sanitize.
379
+ const gradId = `dx-swarm-grad-${String(node.id).replace(/[^\w-]/g, '_')}`;
380
+ const grad = group
381
+ .append('defs')
382
+ .append('linearGradient')
383
+ .attr('id', gradId)
384
+ // userSpaceOnUse so x1/y1/x2/y2 are interpreted in the dx-node's local coord space
385
+ // (head at 0,0). The applyNode hook overwrites them per tick to align with the
386
+ // path's head → tail axis.
387
+ .attr('gradientUnits', 'userSpaceOnUse')
388
+ .attr('x1', 0)
389
+ .attr('y1', 0)
390
+ .attr('x2', 0)
391
+ .attr('y2', 0);
392
+ grad.append('stop').attr('offset', 0).attr('stop-color', fill).attr('stop-opacity', 0.7);
393
+ grad.append('stop').attr('offset', 1).attr('stop-color', fill).attr('stop-opacity', 0);
394
+ // Path first so the head circle sits on top.
395
+ group
396
+ .append('path')
397
+ .classed('dx-swarm-tail', true)
398
+ .attr('fill', 'none')
399
+ .attr('stroke', `url(#${gradId})`)
400
+ .attr('stroke-width', strokeWidth)
401
+ .attr('stroke-linecap', 'round')
402
+ .attr('stroke-linejoin', 'round')
403
+ .attr('pointer-events', 'none');
404
+ group.append('circle').attr('r', r).style('cursor', 'pointer').style('fill', fill);
405
+ };
406
+
407
+ case 'lattice':
408
+ return (group, node) => {
409
+ const r = node.r ?? 6;
410
+ const sz = r * 2;
411
+ group
412
+ .append('rect')
413
+ .attr('x', -r)
414
+ .attr('y', -r)
415
+ .attr('width', sz)
416
+ .attr('height', sz)
417
+ .attr('rx', r * 0.3)
418
+ .attr('ry', r * 0.3)
419
+ .style('cursor', 'pointer')
420
+ .style('fill', getNodeFillForObject(node.data?.data?.object as Obj.Unknown | undefined));
421
+ };
422
+
423
+ case 'cluster':
424
+ return (group, node) => {
425
+ const obj = node.data?.data?.object as Obj.Unknown | undefined;
426
+ const r = node.r ?? 4;
427
+ // Synthetic root / group nodes have no underlying ECHO object; render them as
428
+ // smaller, neutral circles so the hierarchy reads as "structure + leaves".
429
+ group
430
+ .append('circle')
431
+ .attr('r', r)
432
+ .style('cursor', 'pointer')
433
+ .style('fill', obj ? getNodeFillForObject(obj) : 'var(--color-neutral-500)');
434
+ const labelOptions = { delay: TWEEN_MS, duration: LABEL_FADE_MS };
435
+ if (node.type === CLUSTER_NODE_TYPE_LEAF) {
436
+ const text = labelForLeaf(node, obj);
437
+ if (text) {
438
+ appendRadialLeafLabel(group, node, text, r, labelOptions);
439
+ }
440
+ } else if (node.type === CLUSTER_NODE_TYPE_ROOT) {
441
+ if (node.label) {
442
+ appendRootLabel(group, node.label, r, labelOptions);
443
+ }
444
+ } else {
445
+ if (node.label) {
446
+ appendRadialGroupLabel(group, node, node.label, r, labelOptions);
447
+ }
448
+ }
449
+ };
450
+
451
+ case 'bundle':
452
+ // Bundle layout renders ONLY leaves (root/group are invisible routing anchors).
453
+ return (group, node) => {
454
+ const obj = node.data?.data?.object as Obj.Unknown | undefined;
455
+ const r = node.r ?? 4;
456
+ group
457
+ .append('circle')
458
+ .attr('r', r)
459
+ .style('cursor', 'pointer')
460
+ .style('fill', obj ? getNodeFillForObject(obj) : 'var(--color-neutral-500)');
461
+ const text = labelForLeaf(node, obj);
462
+ if (text) {
463
+ appendRadialLeafLabel(group, node, text, r, { delay: TWEEN_MS, duration: LABEL_FADE_MS });
464
+ }
465
+ };
466
+
467
+ case 'plexus':
468
+ // Three node kinds: the focus at the centre (larger, object fill), synthetic relation
469
+ // nodes on the inner ring (neutral, labelled by relation/property), and object leaves on
470
+ // the outer ring (object fill). Type tags are set by the projector.
471
+ return (group, node) => {
472
+ const obj = node.data?.data?.object as Obj.Unknown | undefined;
473
+ const labelOptions = { delay: TWEEN_MS, duration: LABEL_FADE_MS };
474
+
475
+ // Size by node type (not node.r): the renderer bakes the circle radius at enter time
476
+ // from the tween's start value, so a node.r fallback would inherit the previous
477
+ // variant's radius. All leaves + relations share the base size; the focus is double.
478
+ if (node.type === PLEXUS_NODE_TYPE_RELATION) {
479
+ const r = PLEXUS_LEAF_RADIUS;
480
+ group.append('circle').attr('r', r).style('cursor', 'default').style('fill', 'var(--color-neutral-500)');
481
+ if (node.label) {
482
+ appendRadialGroupLabel(group, node, node.label, r, labelOptions);
483
+ }
484
+ return;
485
+ }
486
+
487
+ const isFocus = node.type === PLEXUS_NODE_TYPE_FOCUS;
488
+ const r = isFocus ? PLEXUS_FOCUS_RADIUS : PLEXUS_LEAF_RADIUS;
489
+ group
490
+ .append('circle')
491
+ .attr('r', r)
492
+ .style('cursor', 'pointer')
493
+ .style('fill', obj ? getNodeFillForObject(obj) : 'var(--color-neutral-500)');
494
+ const text = labelForLeaf(node, obj);
495
+ if (text) {
496
+ if (isFocus) {
497
+ appendRootLabel(group, text, r, labelOptions);
498
+ } else {
499
+ appendRadialLeafLabel(group, node, text, r, labelOptions);
500
+ }
501
+ }
502
+ };
503
+ }
504
+ };
505
+
506
+ /**
507
+ * Group leaves by typename so same-type leaves cluster together — used by both the
508
+ * cluster (visible structural nodes) and bundle (invisible routing anchors) projectors.
509
+ */
510
+ const typenameGroupOf = (node: GraphLayoutNode<SpaceGraphNode>): string | undefined => {
511
+ const obj = node.data?.data?.object;
512
+ return obj ? (Obj.getTypename(obj) ?? '(untyped)') : undefined;
513
+ };
514
+
515
+ /** Drop the package prefix from a typename for display: `org.dxos.type.Person` → `Person`. */
516
+ const shortTypename = (typename: string): string => {
517
+ const last = typename.split('.').pop() ?? typename;
518
+ return last.charAt(0).toUpperCase() + last.slice(1);
519
+ };
520
+
521
+ /** Resolve a leaf's display label from its ECHO object, falling back to node-level metadata. */
522
+ const labelForLeaf = (node: GraphLayoutNode<SpaceGraphNode>, obj: Obj.Unknown | undefined): string | undefined =>
523
+ (obj && Obj.getLabel(obj)) ?? node.data?.data?.label ?? node.id;
@@ -0,0 +1,5 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ export { ExplorerArticle as default } from './ExplorerArticle';
@@ -0,0 +1,47 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ /** Visualization variants exposed by `ExplorerArticle`. */
6
+ export type ExplorerArticleVariant = 'force' | 'cluster' | 'bundle' | 'lattice' | 'swarm' | 'plexus';
7
+
8
+ export const VARIANTS: { value: ExplorerArticleVariant; icon: string; label: string }[] = [
9
+ {
10
+ value: 'force',
11
+ icon: 'ph--graph--regular',
12
+ label: 'Force-directed',
13
+ },
14
+ {
15
+ value: 'cluster',
16
+ icon: 'ph--asterisk-simple--regular',
17
+ label: 'Radial',
18
+ },
19
+ {
20
+ value: 'bundle',
21
+ icon: 'ph--circles-three-plus--regular',
22
+ label: 'Connections',
23
+ },
24
+ {
25
+ value: 'plexus',
26
+ icon: 'ph--share-network--regular',
27
+ label: 'Plexus',
28
+ },
29
+ {
30
+ value: 'lattice',
31
+ icon: 'ph--grid-four--regular',
32
+ label: 'Lattice',
33
+ },
34
+ {
35
+ value: 'swarm',
36
+ icon: 'ph--microscope--regular',
37
+ label: 'Swarm',
38
+ },
39
+ ];
40
+
41
+ export const isVariant = (value: unknown): value is ExplorerArticleVariant =>
42
+ value === 'force' ||
43
+ value === 'cluster' ||
44
+ value === 'bundle' ||
45
+ value === 'lattice' ||
46
+ value === 'swarm' ||
47
+ value === 'plexus';
@@ -4,4 +4,4 @@
4
4
 
5
5
  import { type ComponentType, lazy } from 'react';
6
6
 
7
- export const ExplorerContainer: ComponentType<any> = lazy(() => import('./ExplorerContainer'));
7
+ export const ExplorerArticle: ComponentType<any> = lazy(() => import('./ExplorerArticle'));
@@ -6,38 +6,42 @@ import { useEffect, useState } from 'react';
6
6
 
7
7
  import { Capabilities } from '@dxos/app-framework';
8
8
  import { useCapability } from '@dxos/app-framework/ui';
9
- import { type Filter, type Queue, type Space } from '@dxos/client/echo';
9
+ import { type Database, type Entity, type Filter } from '@dxos/echo';
10
10
  import { SpaceGraphModel, type SpaceGraphModelOptions } from '@dxos/schema';
11
11
 
12
12
  // TODO(burdon): Factor out.
13
13
  export const useGraphModel = (
14
- space: Space | undefined,
14
+ db: Database.Database | undefined,
15
15
  filter?: Filter.Any | undefined,
16
16
  options?: SpaceGraphModelOptions,
17
- queue?: Queue,
17
+ items?: readonly Entity.Unknown[],
18
18
  ): SpaceGraphModel | undefined => {
19
19
  const registry = useCapability(Capabilities.AtomRegistry);
20
20
  const [model, setModel] = useState<SpaceGraphModel | undefined>(undefined);
21
21
 
22
22
  useEffect(() => {
23
- if (!space) {
23
+ if (!db) {
24
24
  setModel(undefined);
25
25
  return;
26
26
  }
27
27
 
28
28
  const newModel = new SpaceGraphModel(registry);
29
- void newModel.open(space.db, queue);
29
+ void newModel.open(db);
30
30
  setModel(newModel);
31
31
 
32
32
  return () => {
33
33
  setModel(undefined);
34
34
  void newModel.close();
35
35
  };
36
- }, [space, registry, queue]);
36
+ }, [db, registry]);
37
37
 
38
38
  useEffect(() => {
39
39
  model?.setFilter(filter).setOptions(options);
40
40
  }, [model, filter, options]);
41
41
 
42
+ useEffect(() => {
43
+ model?.setItems(items);
44
+ }, [model, items]);
45
+
42
46
  return model;
43
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,19 +2,38 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import { type Plugin } from '@dxos/app-framework';
5
+ import { Plugin } from '@dxos/app-framework';
6
+ import { DXN } from '@dxos/keys';
6
7
  import { trim } from '@dxos/util';
7
8
 
8
- export const meta: Plugin.Meta = {
9
- id: 'org.dxos.plugin.explorer',
9
+ export const meta = Plugin.makeMeta({
10
+ key: DXN.make('org.dxos.plugin.explorer'),
10
11
  name: 'Explorer',
12
+ author: 'DXOS',
11
13
  description: trim`
12
- Interactive hypergraph visualization that reveals relationships between objects in your workspace.
13
- Navigate complex data structures and discover connections through a dynamic network view.
14
+ Explorer is an interactive hypergraph visualization plugin that reveals the relationships
15
+ between objects stored in your DXOS workspace. Each Graph document is backed by a live
16
+ ECHO query and a View that you configure — Explorer keeps the visualization synchronized
17
+ with the database in real time so every peer immediately sees changes made by collaborators.
18
+
19
+ The plugin offers four switchable layout algorithms for the same data set: a physics-based
20
+ force-directed graph for freeform exploration, a radial cluster layout that groups objects
21
+ by type around a central root, an edge-bundling layout that tames visual clutter on dense
22
+ graphs, and a lattice grid that arranges objects in a sorted matrix sorted by type and label.
23
+ Switching between layouts smoothly tweens node positions so spatial context is preserved.
24
+
25
+ Nodes are coloured by their ECHO object type, and hovering any node opens an anchor-card
26
+ preview panel with the object's details without leaving the graph view.
27
+ The toolbar's query editor lets you filter the visible node set on the fly using
28
+ the same query syntax available across Composer.
29
+
30
+ A new Graph document can be created from the object-creation dialog; Explorer automatically
31
+ derives an initial View from the chosen type and persists both to ECHO so the graph is
32
+ immediately shareable and replicable across the space.
14
33
  `,
15
34
  icon: 'ph--graph--regular',
16
35
  iconHue: 'green',
17
36
  source: 'https://github.com/dxos/dxos/tree/main/packages/plugins/plugin-explorer',
18
- tags: ['labs'],
37
+ spec: 'PLUGIN.mdl',
19
38
  screenshots: ['https://dxos.network/plugin-details-explorer-dark.png'],
20
- };
39
+ });