@dxos/plugin-explorer 0.9.0 → 0.9.1-staging.ee54ba693a

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 (162) hide show
  1. package/dist/lib/neutral/ExplorerArticle-2WPXYTXV.mjs +101 -0
  2. package/dist/lib/neutral/ExplorerArticle-2WPXYTXV.mjs.map +7 -0
  3. package/dist/lib/neutral/ExplorerPlugin.mjs +1 -1
  4. package/dist/lib/neutral/NeighborhoodCompanion-WC6E3YE6.mjs +90 -0
  5. package/dist/lib/neutral/NeighborhoodCompanion-WC6E3YE6.mjs.map +7 -0
  6. package/dist/lib/neutral/app-graph-builder-L2TJ6GNR.mjs +37 -0
  7. package/dist/lib/neutral/app-graph-builder-L2TJ6GNR.mjs.map +7 -0
  8. package/dist/lib/neutral/capabilities/index.mjs +4 -2
  9. package/dist/lib/neutral/capabilities/index.mjs.map +3 -3
  10. package/dist/lib/neutral/{chunk-3D7BYXOR.mjs → chunk-4F6AT7RA.mjs} +6 -4
  11. package/dist/lib/neutral/chunk-4F6AT7RA.mjs.map +7 -0
  12. package/dist/lib/neutral/chunk-C56B464A.mjs +53 -0
  13. package/dist/lib/neutral/chunk-C56B464A.mjs.map +7 -0
  14. package/dist/lib/neutral/{chunk-7XUDLV6E.mjs → chunk-EZ52JY7X.mjs} +34 -31
  15. package/dist/lib/neutral/chunk-EZ52JY7X.mjs.map +7 -0
  16. package/dist/lib/neutral/{chunk-YBCHBVCJ.mjs → chunk-TTFPTHAQ.mjs} +8 -8
  17. package/dist/lib/neutral/chunk-TTFPTHAQ.mjs.map +7 -0
  18. package/dist/lib/neutral/components/index.mjs +430 -506
  19. package/dist/lib/neutral/components/index.mjs.map +4 -4
  20. package/dist/lib/neutral/containers/index.mjs +4 -2
  21. package/dist/lib/neutral/containers/index.mjs.map +3 -3
  22. package/dist/lib/neutral/{create-object-F6TKVAGV.mjs → create-object-WKIN3OB6.mjs} +3 -4
  23. package/dist/lib/neutral/{create-object-F6TKVAGV.mjs.map → create-object-WKIN3OB6.mjs.map} +3 -3
  24. package/dist/lib/neutral/hooks/index.mjs +64 -1
  25. package/dist/lib/neutral/hooks/index.mjs.map +4 -4
  26. package/dist/lib/neutral/index.mjs +2 -2
  27. package/dist/lib/neutral/meta.json +1 -1
  28. package/dist/lib/neutral/meta.mjs +1 -1
  29. package/dist/lib/neutral/plugin.mjs +1 -1
  30. package/dist/lib/neutral/react-surface-EWPG5PUQ.mjs +39 -0
  31. package/dist/lib/neutral/react-surface-EWPG5PUQ.mjs.map +7 -0
  32. package/dist/lib/neutral/testing/index.mjs +2 -2
  33. package/dist/lib/neutral/testing/index.mjs.map +2 -2
  34. package/dist/lib/neutral/translations.mjs +5 -2
  35. package/dist/lib/neutral/translations.mjs.map +3 -3
  36. package/dist/lib/neutral/types/index.mjs +1 -1
  37. package/dist/types/dx.config.d.ts +28 -0
  38. package/dist/types/dx.config.d.ts.map +1 -0
  39. package/dist/types/src/ExplorerPlugin.d.ts.map +1 -1
  40. package/dist/types/src/capabilities/app-graph-builder.d.ts +9 -0
  41. package/dist/types/src/capabilities/app-graph-builder.d.ts.map +1 -0
  42. package/dist/types/src/capabilities/create-object.d.ts +1 -7
  43. package/dist/types/src/capabilities/create-object.d.ts.map +1 -1
  44. package/dist/types/src/capabilities/index.d.ts +3 -7
  45. package/dist/types/src/capabilities/index.d.ts.map +1 -1
  46. package/dist/types/src/capabilities/react-surface.d.ts +2 -2
  47. package/dist/types/src/capabilities/react-surface.d.ts.map +1 -1
  48. package/dist/types/src/components/Lattice/Lattice.d.ts +1 -1
  49. package/dist/types/src/components/Lattice/Lattice.d.ts.map +1 -1
  50. package/dist/types/src/components/Tree/EdgeBundling.stories.d.ts +1 -1
  51. package/dist/types/src/components/Tree/EdgeBundling.stories.d.ts.map +1 -1
  52. package/dist/types/src/components/Tree/Tree.stories.d.ts +1 -1
  53. package/dist/types/src/components/Tree/Tree.stories.d.ts.map +1 -1
  54. package/dist/types/src/components/Tree/index.d.ts +1 -3
  55. package/dist/types/src/components/Tree/index.d.ts.map +1 -1
  56. package/dist/types/src/components/Tree/types/tree.d.ts +10 -27
  57. package/dist/types/src/components/Tree/types/tree.d.ts.map +1 -1
  58. package/dist/types/src/components/Tree/types/types.d.ts +1 -10
  59. package/dist/types/src/components/Tree/types/types.d.ts.map +1 -1
  60. package/dist/types/src/components/Visualization/Visualization.d.ts +39 -0
  61. package/dist/types/src/components/Visualization/Visualization.d.ts.map +1 -0
  62. package/dist/types/src/components/Visualization/Visualization.stories.d.ts +13 -0
  63. package/dist/types/src/components/Visualization/Visualization.stories.d.ts.map +1 -0
  64. package/dist/types/src/components/Visualization/index.d.ts +3 -0
  65. package/dist/types/src/components/Visualization/index.d.ts.map +1 -0
  66. package/dist/types/src/components/Visualization/variants.d.ts +53 -0
  67. package/dist/types/src/components/Visualization/variants.d.ts.map +1 -0
  68. package/dist/types/src/components/index.d.ts +1 -0
  69. package/dist/types/src/components/index.d.ts.map +1 -1
  70. package/dist/types/src/containers/ExplorerArticle/ExplorerArticle.d.ts +2 -2
  71. package/dist/types/src/containers/ExplorerArticle/ExplorerArticle.d.ts.map +1 -1
  72. package/dist/types/src/containers/ExplorerArticle/ExplorerArticle.stories.d.ts +2 -2
  73. package/dist/types/src/containers/ExplorerArticle/ExplorerArticle.stories.d.ts.map +1 -1
  74. package/dist/types/src/containers/NeighborhoodCompanion/NeighborhoodCompanion.d.ts +14 -0
  75. package/dist/types/src/containers/NeighborhoodCompanion/NeighborhoodCompanion.d.ts.map +1 -0
  76. package/dist/types/src/containers/NeighborhoodCompanion/index.d.ts +2 -0
  77. package/dist/types/src/containers/NeighborhoodCompanion/index.d.ts.map +1 -0
  78. package/dist/types/src/containers/index.d.ts +1 -0
  79. package/dist/types/src/containers/index.d.ts.map +1 -1
  80. package/dist/types/src/hooks/index.d.ts +1 -0
  81. package/dist/types/src/hooks/index.d.ts.map +1 -1
  82. package/dist/types/src/hooks/useNeighborhoodModel.d.ts +14 -0
  83. package/dist/types/src/hooks/useNeighborhoodModel.d.ts.map +1 -0
  84. package/dist/types/src/meta.d.ts +32 -2
  85. package/dist/types/src/meta.d.ts.map +1 -1
  86. package/dist/types/src/testing/relations.d.ts +1 -2
  87. package/dist/types/src/testing/relations.d.ts.map +1 -1
  88. package/dist/types/src/translations.d.ts +6 -0
  89. package/dist/types/src/translations.d.ts.map +1 -1
  90. package/dist/types/src/types/Graph.d.ts +6 -8
  91. package/dist/types/src/types/Graph.d.ts.map +1 -1
  92. package/dist/types/tsconfig.tsbuildinfo +1 -1
  93. package/{dist/lib/neutral/chunk-42BYLQQA.mjs → dx.config.ts} +18 -21
  94. package/package.json +42 -37
  95. package/src/ExplorerPlugin.test.ts +1 -1
  96. package/src/ExplorerPlugin.tsx +3 -2
  97. package/src/capabilities/app-graph-builder.ts +40 -0
  98. package/src/capabilities/create-object.ts +3 -2
  99. package/src/capabilities/index.ts +1 -0
  100. package/src/capabilities/react-surface.tsx +14 -4
  101. package/src/components/Lattice/Lattice.tsx +1 -2
  102. package/src/components/Tree/EdgeBundling.stories.tsx +5 -3
  103. package/src/components/Tree/Tree.stories.tsx +7 -2
  104. package/src/components/Tree/index.ts +3 -3
  105. package/src/components/Tree/types/tree.ts +6 -6
  106. package/src/components/Tree/types/types.ts +1 -11
  107. package/src/components/Visualization/Visualization.stories.tsx +104 -0
  108. package/src/components/Visualization/Visualization.tsx +209 -0
  109. package/src/components/Visualization/index.ts +6 -0
  110. package/src/components/Visualization/variants.ts +508 -0
  111. package/src/components/index.ts +1 -0
  112. package/src/containers/ExplorerArticle/ExplorerArticle.stories.tsx +3 -3
  113. package/src/containers/ExplorerArticle/ExplorerArticle.tsx +9 -10
  114. package/src/containers/NeighborhoodCompanion/NeighborhoodCompanion.tsx +102 -0
  115. package/src/containers/NeighborhoodCompanion/index.ts +5 -0
  116. package/src/containers/index.ts +1 -0
  117. package/src/hooks/index.ts +1 -0
  118. package/src/hooks/useNeighborhoodModel.ts +81 -0
  119. package/src/meta.ts +2 -32
  120. package/src/testing/relations.ts +1 -3
  121. package/src/translations.ts +4 -1
  122. package/src/types/Graph.ts +18 -20
  123. package/dist/lib/neutral/ExplorerArticle-4I7PNGDC.mjs +0 -459
  124. package/dist/lib/neutral/ExplorerArticle-4I7PNGDC.mjs.map +0 -7
  125. package/dist/lib/neutral/chunk-3D7BYXOR.mjs.map +0 -7
  126. package/dist/lib/neutral/chunk-42BYLQQA.mjs.map +0 -7
  127. package/dist/lib/neutral/chunk-7XUDLV6E.mjs.map +0 -7
  128. package/dist/lib/neutral/chunk-IKHJV3Q4.mjs +0 -20
  129. package/dist/lib/neutral/chunk-IKHJV3Q4.mjs.map +0 -7
  130. package/dist/lib/neutral/chunk-YBCHBVCJ.mjs.map +0 -7
  131. package/dist/lib/neutral/react-surface-APBW2VQG.mjs +0 -26
  132. package/dist/lib/neutral/react-surface-APBW2VQG.mjs.map +0 -7
  133. package/dist/types/src/components/Tree/Tree.d.ts +0 -24
  134. package/dist/types/src/components/Tree/Tree.d.ts.map +0 -1
  135. package/dist/types/src/components/Tree/layout/HierarchicalEdgeBundling.d.ts +0 -40
  136. package/dist/types/src/components/Tree/layout/HierarchicalEdgeBundling.d.ts.map +0 -1
  137. package/dist/types/src/components/Tree/layout/RadialTree.d.ts +0 -37
  138. package/dist/types/src/components/Tree/layout/RadialTree.d.ts.map +0 -1
  139. package/dist/types/src/components/Tree/layout/TidyTree.d.ts +0 -26
  140. package/dist/types/src/components/Tree/layout/TidyTree.d.ts.map +0 -1
  141. package/dist/types/src/components/Tree/layout/hierarchy.d.ts +0 -17
  142. package/dist/types/src/components/Tree/layout/hierarchy.d.ts.map +0 -1
  143. package/dist/types/src/components/Tree/layout/index.d.ts +0 -6
  144. package/dist/types/src/components/Tree/layout/index.d.ts.map +0 -1
  145. package/dist/types/src/components/Tree/layout/slots.d.ts +0 -7
  146. package/dist/types/src/components/Tree/layout/slots.d.ts.map +0 -1
  147. package/dist/types/src/components/Tree/layout/useContainerSize.d.ts +0 -15
  148. package/dist/types/src/components/Tree/layout/useContainerSize.d.ts.map +0 -1
  149. package/dist/types/src/containers/ExplorerArticle/Visualization.d.ts +0 -18
  150. package/dist/types/src/containers/ExplorerArticle/Visualization.d.ts.map +0 -1
  151. package/dist/types/src/containers/ExplorerArticle/variants.d.ts +0 -9
  152. package/dist/types/src/containers/ExplorerArticle/variants.d.ts.map +0 -1
  153. package/src/components/Tree/Tree.tsx +0 -81
  154. package/src/components/Tree/layout/HierarchicalEdgeBundling.tsx +0 -335
  155. package/src/components/Tree/layout/RadialTree.tsx +0 -242
  156. package/src/components/Tree/layout/TidyTree.tsx +0 -246
  157. package/src/components/Tree/layout/hierarchy.ts +0 -32
  158. package/src/components/Tree/layout/index.ts +0 -9
  159. package/src/components/Tree/layout/slots.ts +0 -19
  160. package/src/components/Tree/layout/useContainerSize.ts +0 -43
  161. package/src/containers/ExplorerArticle/Visualization.tsx +0 -523
  162. package/src/containers/ExplorerArticle/variants.ts +0 -47
@@ -0,0 +1,5 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ export { NeighborhoodCompanion as default } from './NeighborhoodCompanion';
@@ -5,3 +5,4 @@
5
5
  import { type ComponentType, lazy } from 'react';
6
6
 
7
7
  export const ExplorerArticle: ComponentType<any> = lazy(() => import('./ExplorerArticle'));
8
+ export const NeighborhoodCompanion: ComponentType<any> = lazy(() => import('./NeighborhoodCompanion'));
@@ -3,3 +3,4 @@
3
3
  //
4
4
 
5
5
  export * from './useGraphModel';
6
+ export * from './useNeighborhoodModel';
@@ -0,0 +1,81 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import { useEffect, useState } from 'react';
6
+
7
+ import { Obj } from '@dxos/echo';
8
+ import { type SpaceGraphEdge, type SpaceGraphModel, type SpaceGraphNode } from '@dxos/schema';
9
+
10
+ import { useGraphModel } from './useGraphModel';
11
+
12
+ /** Default traversal depth (hops) for the neighborhood graph. */
13
+ export const DEFAULT_NEIGHBORHOOD_DEPTH = 2;
14
+
15
+ /**
16
+ * Builds an ego graph around `object`: the n-hop neighbourhood reached by following refs and
17
+ * relations from the active node. The full space graph is materialised (reactively) and then
18
+ * narrowed to the nodes within `depth` hops, keeping only edges whose endpoints both survive.
19
+ *
20
+ * NOTE: Derives the subgraph from the whole-space model — the same cost the main explorer pays.
21
+ * A direct ECHO traversal that loads only the reachable objects is a future optimisation.
22
+ */
23
+ export const useNeighborhoodModel = (
24
+ object: Obj.Unknown | undefined,
25
+ depth = DEFAULT_NEIGHBORHOOD_DEPTH,
26
+ ): SpaceGraphModel | undefined => {
27
+ const db = object ? Obj.getDatabase(object) : undefined;
28
+ const fullModel = useGraphModel(db);
29
+ const [model, setModel] = useState<SpaceGraphModel | undefined>(undefined);
30
+
31
+ useEffect(() => {
32
+ if (!fullModel || !object) {
33
+ setModel(undefined);
34
+ return;
35
+ }
36
+
37
+ const recompute = () => setModel(buildNeighborhood(fullModel, object.id, depth));
38
+ recompute();
39
+ return fullModel.subscribe(recompute);
40
+ }, [fullModel, object?.id, depth]);
41
+
42
+ return model;
43
+ };
44
+
45
+ /**
46
+ * Narrow the full graph to the BFS neighbourhood of `rootId` within `depth` hops (edges treated as
47
+ * undirected so the neighbourhood reads as graph distance regardless of ref/relation direction).
48
+ */
49
+ const buildNeighborhood = (fullModel: SpaceGraphModel, rootId: string, depth: number): SpaceGraphModel => {
50
+ const { nodes, edges } = fullModel.graph;
51
+
52
+ const adjacency = new Map<string, string[]>();
53
+ const link = (from: string, to: string) => {
54
+ const list = adjacency.get(from) ?? [];
55
+ list.push(to);
56
+ adjacency.set(from, list);
57
+ };
58
+ for (const edge of edges) {
59
+ link(edge.source, edge.target);
60
+ link(edge.target, edge.source);
61
+ }
62
+
63
+ const included = new Set<string>([rootId]);
64
+ let frontier = [rootId];
65
+ for (let hop = 0; hop < depth && frontier.length; hop++) {
66
+ const next: string[] = [];
67
+ for (const id of frontier) {
68
+ for (const neighbour of adjacency.get(id) ?? []) {
69
+ if (!included.has(neighbour)) {
70
+ included.add(neighbour);
71
+ next.push(neighbour);
72
+ }
73
+ }
74
+ }
75
+ frontier = next;
76
+ }
77
+
78
+ const subNodes = nodes.filter((node: SpaceGraphNode) => included.has(node.id));
79
+ const subEdges = edges.filter((edge: SpaceGraphEdge) => included.has(edge.source) && included.has(edge.target));
80
+ return fullModel.copy({ nodes: subNodes, edges: subEdges });
81
+ };
package/src/meta.ts CHANGED
@@ -3,37 +3,7 @@
3
3
  //
4
4
 
5
5
  import { Plugin } from '@dxos/app-framework';
6
- import { DXN } from '@dxos/keys';
7
- import { trim } from '@dxos/util';
8
6
 
9
- export const meta = Plugin.makeMeta({
10
- key: DXN.make('org.dxos.plugin.explorer'),
11
- name: 'Explorer',
12
- author: 'DXOS',
13
- description: trim`
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.
7
+ import config from '../dx.config';
18
8
 
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.
33
- `,
34
- icon: 'ph--graph--regular',
35
- iconHue: 'green',
36
- source: 'https://github.com/dxos/dxos/tree/main/packages/plugins/plugin-explorer',
37
- spec: 'PLUGIN.mdl',
38
- screenshots: ['https://dxos.network/plugin-details-explorer-dark.png'],
39
- });
9
+ export const meta = Plugin.getMetaFromConfig(config);
@@ -4,6 +4,7 @@
4
4
 
5
5
  import { type Space } from '@dxos/client/echo';
6
6
  import { Obj, Query, Relation } from '@dxos/echo';
7
+ import { type BundleEdge, type TreeNode } from '@dxos/react-ui-graph';
7
8
  import {
8
9
  type RelationSpec,
9
10
  type TypeSpec,
@@ -13,9 +14,6 @@ import {
13
14
  } from '@dxos/schema/testing';
14
15
  import { HasConnection, Organization, Person } from '@dxos/types';
15
16
 
16
- import { type BundleEdge } from '../components/Tree/layout';
17
- import { type TreeNode } from '../components/Tree/types';
18
-
19
17
  const SECTORS = ['Technology', 'Finance', 'Research', 'Media'];
20
18
  const CONNECTION_KINDS = ['partner', 'investor', 'vendor', 'customer'];
21
19
 
@@ -23,9 +23,12 @@ export const translations = [
23
23
  'delete-object.label': 'Delete explorer',
24
24
  'object-deleted.label': 'Explorer deleted',
25
25
  },
26
- [meta.id]: {
26
+ [meta.profile.key]: {
27
27
  'plugin.name': 'Explorer',
28
28
  'object-title.label': 'Title',
29
+ 'neighborhood-companion.label': 'Graph',
30
+ 'depth.label_one': '{{count}} hop',
31
+ 'depth.label_other': '{{count}} hops',
29
32
  },
30
33
  },
31
34
  },
@@ -4,29 +4,27 @@
4
4
 
5
5
  import * as Schema from 'effect/Schema';
6
6
 
7
- import { DXN, Annotation, Filter, Obj, Query, QueryAST, Ref, Type, View } from '@dxos/echo';
7
+ import { Annotation, DXN, Filter, Obj, Query, QueryAST, Ref, Type, View } from '@dxos/echo';
8
8
  import { FormInputAnnotation, LabelAnnotation } from '@dxos/echo/Annotation';
9
9
  import { ViewAnnotation } from '@dxos/schema';
10
10
 
11
- const GraphSchema = Schema.Struct({
12
- name: Schema.optional(Schema.String),
13
- view: Ref.Ref(View.View).pipe(FormInputAnnotation.set(false)),
14
- query: Schema.Struct({
15
- raw: Schema.optional(Schema.String),
16
- ast: QueryAST.Query,
17
- }).pipe(FormInputAnnotation.set(false)),
18
- }).pipe(
19
- LabelAnnotation.set(['name']),
20
- ViewAnnotation.set(['view']),
21
- Annotation.IconAnnotation.set({ icon: 'ph--graph--regular', hue: 'green' }),
22
- Type.makeObject(DXN.make('org.dxos.type.graph', '0.1.0')),
23
- );
24
- // TODO(wittjosiah): Try to clean up this type inference.
25
- export interface Graph extends Type.InstanceType<typeof GraphSchema> {}
26
- export const Graph: Type.Obj<Graph> = GraphSchema as any;
11
+ export class Graph extends Type.makeObject<Graph>(DXN.make('org.dxos.type.graph', '0.1.0'))(
12
+ Schema.Struct({
13
+ name: Schema.optional(Schema.String),
14
+ view: Ref.Ref(View.View).pipe(FormInputAnnotation.set(false), Schema.optional),
15
+ query: Schema.Struct({
16
+ raw: Schema.optional(Schema.String),
17
+ ast: QueryAST.Query,
18
+ }).pipe(FormInputAnnotation.set(false)),
19
+ }).pipe(
20
+ LabelAnnotation.set(['name']),
21
+ ViewAnnotation.set(['view']),
22
+ Annotation.IconAnnotation.set({ icon: 'ph--graph--regular', hue: 'green' }),
23
+ ),
24
+ ) {}
27
25
 
28
26
  type MakeProps = Omit<Partial<Obj.MakeProps<typeof Graph>>, 'view'> & {
29
- view: View.View;
27
+ view?: View.View;
30
28
  };
31
29
 
32
30
  /**
@@ -36,6 +34,6 @@ export const make = ({
36
34
  name,
37
35
  query = { raw: '', ast: Query.select(Filter.nothing()).ast },
38
36
  view,
39
- }: MakeProps): Graph => {
40
- return Obj.make(Graph, { name, view: Ref.make(view), query });
37
+ }: MakeProps = {}): Graph => {
38
+ return Obj.make(Graph, { name, view: view && Ref.make(view), query });
41
39
  };
@@ -1,459 +0,0 @@
1
- import {
2
- getNodeFillForObject
3
- } from "./chunk-IKHJV3Q4.mjs";
4
- import "./chunk-J5LGTIGS.mjs";
5
-
6
- // src/containers/ExplorerArticle/ExplorerArticle.tsx
7
- import React2, { useCallback as useCallback2, useEffect as useEffect2, useMemo as useMemo2, useState as useState2 } from "react";
8
- import { Obj as Obj2 } from "@dxos/echo";
9
- import { QueryBuilder } from "@dxos/echo-query";
10
- import { useObject } from "@dxos/react-client/echo";
11
- import { DxAnchorActivate, Icon, Panel, Toolbar } from "@dxos/react-ui";
12
- import { QueryEditor } from "@dxos/react-ui-components";
13
- import "@dxos/react-ui-graph/styles/graph.css";
14
- import { useGraphModel } from "#hooks";
15
-
16
- // src/containers/ExplorerArticle/variants.ts
17
- var VARIANTS = [
18
- {
19
- value: "force",
20
- icon: "ph--graph--regular",
21
- label: "Force-directed"
22
- },
23
- {
24
- value: "cluster",
25
- icon: "ph--asterisk-simple--regular",
26
- label: "Radial"
27
- },
28
- {
29
- value: "bundle",
30
- icon: "ph--circles-three-plus--regular",
31
- label: "Connections"
32
- },
33
- {
34
- value: "plexus",
35
- icon: "ph--share-network--regular",
36
- label: "Plexus"
37
- },
38
- {
39
- value: "lattice",
40
- icon: "ph--grid-four--regular",
41
- label: "Lattice"
42
- },
43
- {
44
- value: "swarm",
45
- icon: "ph--microscope--regular",
46
- label: "Swarm"
47
- }
48
- ];
49
- var isVariant = (value) => value === "force" || value === "cluster" || value === "bundle" || value === "lattice" || value === "swarm" || value === "plexus";
50
-
51
- // src/containers/ExplorerArticle/Visualization.tsx
52
- import { curveCatmullRom, line as d3Line } from "d3";
53
- import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
54
- import { Obj } from "@dxos/echo";
55
- import { CLUSTER_NODE_TYPE_LEAF, CLUSTER_NODE_TYPE_ROOT, GraphBundleProjector, GraphClusterProjector, GraphForceProjector, GraphLatticeProjector, GraphPlexusProjector, GraphSwarmProjector, PLEXUS_NODE_TYPE_FOCUS, PLEXUS_NODE_TYPE_RELATION, SVG, appendRadialGroupLabel, appendRadialLeafLabel, appendRootLabel } from "@dxos/react-ui-graph";
56
- import { mx } from "@dxos/ui-theme";
57
- var Visualization = ({ classNames, debug = true, variant, model, onNodeHover, onSurfaceClick }) => {
58
- const svgRef = useRef(null);
59
- const [projector, setProjector] = useState();
60
- const projectorRef = useRef(void 0);
61
- projectorRef.current = projector;
62
- const [focusId, setFocusId] = useState(void 0);
63
- const lastLayoutRef = useRef(void 0);
64
- useEffect(() => model?.subscribe(() => void 0), [
65
- model
66
- ]);
67
- useEffect(() => {
68
- if (variant !== "plexus") {
69
- setFocusId(void 0);
70
- }
71
- }, [
72
- variant
73
- ]);
74
- useEffect(() => {
75
- if (projectorRef.current?.layout) {
76
- lastLayoutRef.current = projectorRef.current.layout;
77
- }
78
- if (!svgRef.current) {
79
- return;
80
- }
81
- setProjector(createProjector(svgRef.current, variant, focusId, lastLayoutRef.current));
82
- }, [
83
- variant,
84
- focusId
85
- ]);
86
- const handleSelect = useCallback((node) => {
87
- if (variant !== "plexus") {
88
- return;
89
- }
90
- if (!node.data?.data?.object) {
91
- return;
92
- }
93
- setFocusId((current) => current === node.id ? void 0 : node.id);
94
- }, [
95
- variant
96
- ]);
97
- const renderNode = useMemo(() => createRenderNode(variant), [
98
- variant
99
- ]);
100
- const applyNode = useMemo(() => variant === "swarm" ? applyNodeSwarm : void 0, [
101
- variant
102
- ]);
103
- const handlePointerMove = useCallback((point) => {
104
- if (projector instanceof GraphSwarmProjector) {
105
- projector.setCursor(point.x, point.y);
106
- }
107
- }, [
108
- projector
109
- ]);
110
- const handlePointerLeave = useCallback(() => {
111
- if (projector instanceof GraphSwarmProjector) {
112
- projector.setCursor(null);
113
- }
114
- }, [
115
- projector
116
- ]);
117
- const handleInspect = useCallback((node, event) => {
118
- if (variant === "plexus") {
119
- return;
120
- }
121
- onNodeHover?.(node ? {
122
- id: node.id,
123
- data: node.data?.data?.object
124
- } : null, event);
125
- }, [
126
- variant === "plexus" ? void 0 : onNodeHover
127
- ]);
128
- const swarmPointerProps = variant === "swarm" ? {
129
- onPointerMove: handlePointerMove,
130
- onPointerLeave: handlePointerLeave
131
- } : void 0;
132
- return /* @__PURE__ */ React.createElement("div", {
133
- className: mx("dx-expander relative", classNames),
134
- onClick: onSurfaceClick
135
- }, /* @__PURE__ */ React.createElement(SVG.Root, {
136
- ref: svgRef
137
- }, /* @__PURE__ */ React.createElement(SVG.Zoom, {
138
- extent: [
139
- 1 / 2,
140
- 2
141
- ]
142
- }, /* @__PURE__ */ React.createElement(SVG.Graph, {
143
- model,
144
- projector,
145
- renderNode,
146
- applyNode,
147
- edgeOpacity: variant === "swarm" ? 0.3 : void 0,
148
- drag: variant === "force",
149
- highlightOnHover: variant === "bundle",
150
- onSelect: handleSelect,
151
- onInspect: handleInspect,
152
- ...swarmPointerProps
153
- })), debug && /* @__PURE__ */ React.createElement(SVG.FPS, null)));
154
- };
155
- var TWEEN_MS = 500;
156
- var LABEL_FADE_MS = 200;
157
- var PLEXUS_LEAF_RADIUS = 5;
158
- var PLEXUS_FOCUS_RADIUS = PLEXUS_LEAF_RADIUS * 2;
159
- var swarmTrailLine = d3Line().curve(curveCatmullRom.alpha(0.5));
160
- var applyNodeSwarm = (group, node) => {
161
- const swarm = node;
162
- const path = group.querySelector("path.dx-swarm-tail");
163
- const grad = group.querySelector("linearGradient");
164
- if (!path || !grad) {
165
- return;
166
- }
167
- const history = swarm.history ?? [];
168
- if (history.length === 0) {
169
- path.setAttribute("d", "");
170
- return;
171
- }
172
- const hx = swarm.x ?? 0;
173
- const hy = swarm.y ?? 0;
174
- const points = [
175
- [
176
- 0,
177
- 0
178
- ]
179
- ];
180
- for (let i = history.length - 1; i >= 0; i--) {
181
- points.push([
182
- history[i].x - hx,
183
- history[i].y - hy
184
- ]);
185
- }
186
- path.setAttribute("d", swarmTrailLine(points) ?? "");
187
- const oldest = history[0];
188
- grad.setAttribute("x2", String(oldest.x - hx));
189
- grad.setAttribute("y2", String(oldest.y - hy));
190
- };
191
- var createProjector = (ctx, variant, focusId, prev) => {
192
- switch (variant) {
193
- case "force":
194
- return new GraphForceProjector(ctx, void 0, void 0, prev);
195
- case "swarm":
196
- return new GraphSwarmProjector(ctx, void 0, void 0, prev);
197
- case "lattice":
198
- return new GraphLatticeProjector(ctx, {
199
- duration: TWEEN_MS,
200
- // Plugin-explorer overrides the projector's force-matched default (6)
201
- // with a smaller node so the lattice reads as a dense matrix.
202
- radius: 4,
203
- // Cluster by typename first so same-type rects sit together; break ties by label.
204
- sortBy: (node) => {
205
- const obj = node.data?.data?.object;
206
- const typename = obj ? Obj.getTypename(obj) ?? "(untyped)" : "(untyped)";
207
- const label = (obj && Obj.getLabel(obj)) ?? node.data?.data?.label ?? node.id;
208
- return `${typename} ${label}`;
209
- }
210
- }, void 0, prev);
211
- case "cluster":
212
- return new GraphClusterProjector(ctx, {
213
- duration: TWEEN_MS,
214
- groupOf: typenameGroupOf,
215
- rootLabel: "Database",
216
- groupLabel: shortTypename,
217
- // All three node kinds share the same radius — leaves, groups, and root read
218
- // as members of the same circle rather than ranked by size.
219
- rootRadius: 4,
220
- groupRadius: 4
221
- }, void 0, prev);
222
- case "bundle":
223
- return new GraphBundleProjector(ctx, {
224
- duration: TWEEN_MS,
225
- groupOf: typenameGroupOf
226
- }, void 0, prev);
227
- case "plexus":
228
- if (!focusId) {
229
- return new GraphBundleProjector(ctx, {
230
- duration: TWEEN_MS,
231
- groupOf: typenameGroupOf
232
- }, void 0, prev);
233
- }
234
- return new GraphPlexusProjector(ctx, {
235
- duration: TWEEN_MS,
236
- focus: focusId,
237
- relationOf: plexusRelationOf,
238
- leafRadius: PLEXUS_LEAF_RADIUS,
239
- relationRadius: PLEXUS_LEAF_RADIUS,
240
- focusRadius: PLEXUS_FOCUS_RADIUS
241
- }, void 0, prev);
242
- }
243
- };
244
- var plexusRelationOf = (edge, focusId) => {
245
- const outgoing = edge.source.id === focusId;
246
- const incoming = edge.target.id === focusId;
247
- if (!outgoing && !incoming) {
248
- return void 0;
249
- }
250
- const direction = outgoing ? "out" : "in";
251
- const arrow = outgoing ? "\u2192" : "\u2190";
252
- if (edge.type === "relation") {
253
- const relation = edge.data?.object;
254
- const typename = relation ? Obj.getTypename(relation) : void 0;
255
- const name = typename ? shortTypename(typename) : "Relation";
256
- return {
257
- key: `relation:${direction}:${typename ?? "?"}`,
258
- label: `${name} ${arrow}`
259
- };
260
- }
261
- if (edge.type === "ref") {
262
- const property = edge.data?.property;
263
- const name = property ?? "References";
264
- return {
265
- key: `ref:${direction}:${property ?? "?"}`,
266
- label: `${name} ${arrow}`
267
- };
268
- }
269
- return void 0;
270
- };
271
- var createRenderNode = (variant) => {
272
- switch (variant) {
273
- case "force":
274
- return (group, node) => {
275
- const r = node.r ?? 6;
276
- group.append("circle").attr("r", r).style("cursor", "pointer").style("fill", getNodeFillForObject(node.data?.data?.object));
277
- };
278
- case "swarm":
279
- return (group, node) => {
280
- const fill = getNodeFillForObject(node.data?.data?.object);
281
- const r = node.r ?? 6;
282
- const strokeWidth = Math.max(1, r * 0.6);
283
- const gradId = `dx-swarm-grad-${String(node.id).replace(/[^\w-]/g, "_")}`;
284
- const grad = group.append("defs").append("linearGradient").attr("id", gradId).attr("gradientUnits", "userSpaceOnUse").attr("x1", 0).attr("y1", 0).attr("x2", 0).attr("y2", 0);
285
- grad.append("stop").attr("offset", 0).attr("stop-color", fill).attr("stop-opacity", 0.7);
286
- grad.append("stop").attr("offset", 1).attr("stop-color", fill).attr("stop-opacity", 0);
287
- group.append("path").classed("dx-swarm-tail", true).attr("fill", "none").attr("stroke", `url(#${gradId})`).attr("stroke-width", strokeWidth).attr("stroke-linecap", "round").attr("stroke-linejoin", "round").attr("pointer-events", "none");
288
- group.append("circle").attr("r", r).style("cursor", "pointer").style("fill", fill);
289
- };
290
- case "lattice":
291
- return (group, node) => {
292
- const r = node.r ?? 6;
293
- const sz = r * 2;
294
- group.append("rect").attr("x", -r).attr("y", -r).attr("width", sz).attr("height", sz).attr("rx", r * 0.3).attr("ry", r * 0.3).style("cursor", "pointer").style("fill", getNodeFillForObject(node.data?.data?.object));
295
- };
296
- case "cluster":
297
- return (group, node) => {
298
- const obj = node.data?.data?.object;
299
- const r = node.r ?? 4;
300
- group.append("circle").attr("r", r).style("cursor", "pointer").style("fill", obj ? getNodeFillForObject(obj) : "var(--color-neutral-500)");
301
- const labelOptions = {
302
- delay: TWEEN_MS,
303
- duration: LABEL_FADE_MS
304
- };
305
- if (node.type === CLUSTER_NODE_TYPE_LEAF) {
306
- const text = labelForLeaf(node, obj);
307
- if (text) {
308
- appendRadialLeafLabel(group, node, text, r, labelOptions);
309
- }
310
- } else if (node.type === CLUSTER_NODE_TYPE_ROOT) {
311
- if (node.label) {
312
- appendRootLabel(group, node.label, r, labelOptions);
313
- }
314
- } else {
315
- if (node.label) {
316
- appendRadialGroupLabel(group, node, node.label, r, labelOptions);
317
- }
318
- }
319
- };
320
- case "bundle":
321
- return (group, node) => {
322
- const obj = node.data?.data?.object;
323
- const r = node.r ?? 4;
324
- group.append("circle").attr("r", r).style("cursor", "pointer").style("fill", obj ? getNodeFillForObject(obj) : "var(--color-neutral-500)");
325
- const text = labelForLeaf(node, obj);
326
- if (text) {
327
- appendRadialLeafLabel(group, node, text, r, {
328
- delay: TWEEN_MS,
329
- duration: LABEL_FADE_MS
330
- });
331
- }
332
- };
333
- case "plexus":
334
- return (group, node) => {
335
- const obj = node.data?.data?.object;
336
- const labelOptions = {
337
- delay: TWEEN_MS,
338
- duration: LABEL_FADE_MS
339
- };
340
- if (node.type === PLEXUS_NODE_TYPE_RELATION) {
341
- const r2 = PLEXUS_LEAF_RADIUS;
342
- group.append("circle").attr("r", r2).style("cursor", "default").style("fill", "var(--color-neutral-500)");
343
- if (node.label) {
344
- appendRadialGroupLabel(group, node, node.label, r2, labelOptions);
345
- }
346
- return;
347
- }
348
- const isFocus = node.type === PLEXUS_NODE_TYPE_FOCUS;
349
- const r = isFocus ? PLEXUS_FOCUS_RADIUS : PLEXUS_LEAF_RADIUS;
350
- group.append("circle").attr("r", r).style("cursor", "pointer").style("fill", obj ? getNodeFillForObject(obj) : "var(--color-neutral-500)");
351
- const text = labelForLeaf(node, obj);
352
- if (text) {
353
- if (isFocus) {
354
- appendRootLabel(group, text, r, labelOptions);
355
- } else {
356
- appendRadialLeafLabel(group, node, text, r, labelOptions);
357
- }
358
- }
359
- };
360
- }
361
- };
362
- var typenameGroupOf = (node) => {
363
- const obj = node.data?.data?.object;
364
- return obj ? Obj.getTypename(obj) ?? "(untyped)" : void 0;
365
- };
366
- var shortTypename = (typename) => {
367
- const last = typename.split(".").pop() ?? typename;
368
- return last.charAt(0).toUpperCase() + last.slice(1);
369
- };
370
- var labelForLeaf = (node, obj) => (obj && Obj.getLabel(obj)) ?? node.data?.data?.label ?? node.id;
371
-
372
- // src/containers/ExplorerArticle/ExplorerArticle.tsx
373
- var ExplorerArticle = ({ role, subject, variant }) => {
374
- const [view] = useObject(subject);
375
- const [filter, setFilter] = useState2();
376
- const db = view && Obj2.getDatabase(view);
377
- const model = useGraphModel(db, filter);
378
- const builder = useMemo2(() => new QueryBuilder(), []);
379
- const handleChange = useCallback2((value) => {
380
- setFilter(builder.build(value).filter);
381
- }, [
382
- builder
383
- ]);
384
- const [selected, setSelected] = useState2(isVariant(variant) ? variant : "force");
385
- useEffect2(() => {
386
- if (isVariant(variant)) {
387
- setSelected(variant);
388
- }
389
- }, [
390
- variant
391
- ]);
392
- const handleVariantChange = useCallback2((value) => {
393
- if (isVariant(value)) {
394
- setSelected(value);
395
- }
396
- }, []);
397
- const handleDismiss = useCallback2(() => {
398
- document.defaultView?.dispatchEvent(new DxAnchorActivate({
399
- dxn: "",
400
- label: "",
401
- trigger: document.body,
402
- state: false
403
- }));
404
- }, []);
405
- const handleHover = useCallback2((node, event) => {
406
- if (!node || !event) {
407
- return;
408
- }
409
- const obj = node.data;
410
- if (!obj || !Obj2.isObject(obj)) {
411
- return;
412
- }
413
- const dxn = Obj2.getURI(obj);
414
- if (!dxn) {
415
- return;
416
- }
417
- const target = event.target;
418
- target.dispatchEvent(new DxAnchorActivate({
419
- dxn,
420
- kind: "card",
421
- trigger: target,
422
- label: Obj2.getLabel(obj) ?? dxn
423
- }));
424
- }, []);
425
- const showToolbar = role === "article";
426
- if (!db || !model) {
427
- return null;
428
- }
429
- return /* @__PURE__ */ React2.createElement(Panel.Root, {
430
- role
431
- }, showToolbar && /* @__PURE__ */ React2.createElement(Panel.Toolbar, {
432
- asChild: true
433
- }, /* @__PURE__ */ React2.createElement(Toolbar.Root, null, /* @__PURE__ */ React2.createElement(QueryEditor, {
434
- db,
435
- onChange: handleChange
436
- }), /* @__PURE__ */ React2.createElement(Toolbar.ToggleGroup, {
437
- type: "single",
438
- value: selected,
439
- onValueChange: handleVariantChange
440
- }, VARIANTS.map(({ value, icon, label }) => /* @__PURE__ */ React2.createElement(Toolbar.ToggleGroupItem, {
441
- key: value,
442
- value,
443
- "aria-label": label,
444
- title: label
445
- }, /* @__PURE__ */ React2.createElement(Icon, {
446
- icon,
447
- size: 4
448
- })))))), /* @__PURE__ */ React2.createElement(Panel.Content, null, /* @__PURE__ */ React2.createElement(Visualization, {
449
- classNames: "bg-base-surface",
450
- variant: selected,
451
- model,
452
- onNodeHover: handleHover,
453
- onSurfaceClick: handleDismiss
454
- })));
455
- };
456
- export {
457
- ExplorerArticle as default
458
- };
459
- //# sourceMappingURL=ExplorerArticle-4I7PNGDC.mjs.map