@dxos/plugin-explorer 0.8.4-main.abd8ff62ef → 0.8.4-main.bc2380dfbc

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 (181) 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/{browser/types/index.mjs → neutral/chunk-7SPMPHRS.mjs} +7 -7
  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 → neutral}/components/index.mjs +661 -297
  12. package/dist/lib/{browser → neutral}/components/index.mjs.map +4 -4
  13. package/dist/lib/neutral/containers/index.mjs +9 -0
  14. package/dist/lib/neutral/containers/index.mjs.map +7 -0
  15. package/dist/lib/neutral/create-object-F6TKVAGV.mjs +39 -0
  16. package/dist/lib/neutral/create-object-F6TKVAGV.mjs.map +7 -0
  17. package/dist/lib/{browser → neutral}/hooks/index.mjs +11 -6
  18. package/dist/lib/neutral/hooks/index.mjs.map +7 -0
  19. package/dist/lib/neutral/index.mjs +14 -0
  20. package/dist/lib/neutral/meta.json +1 -0
  21. package/dist/lib/{browser/index.mjs → neutral/plugin.mjs} +3 -4
  22. package/dist/lib/{browser/index.mjs.map → neutral/plugin.mjs.map} +3 -3
  23. package/dist/lib/neutral/react-surface-APBW2VQG.mjs +26 -0
  24. package/dist/lib/neutral/react-surface-APBW2VQG.mjs.map +7 -0
  25. package/dist/lib/neutral/testing.mjs +8 -0
  26. package/dist/lib/neutral/testing.mjs.map +7 -0
  27. package/dist/lib/neutral/translations.mjs +33 -0
  28. package/dist/lib/neutral/translations.mjs.map +7 -0
  29. package/dist/lib/neutral/types/index.mjs +10 -0
  30. package/dist/lib/neutral/types/index.mjs.map +7 -0
  31. package/dist/types/src/ExplorerPlugin.d.ts.map +1 -1
  32. package/dist/types/src/capabilities/create-object.d.ts +11 -0
  33. package/dist/types/src/capabilities/create-object.d.ts.map +1 -0
  34. package/dist/types/src/capabilities/index.d.ts +6 -0
  35. package/dist/types/src/capabilities/index.d.ts.map +1 -1
  36. package/dist/types/src/components/Chart/Chart.stories.d.ts +4 -1
  37. package/dist/types/src/components/Chart/Chart.stories.d.ts.map +1 -1
  38. package/dist/types/src/components/Globe/Globe.stories.d.ts +5 -2
  39. package/dist/types/src/components/Globe/Globe.stories.d.ts.map +1 -1
  40. package/dist/types/src/components/Graph/CanvasForceGraph.d.ts +13 -0
  41. package/dist/types/src/components/Graph/CanvasForceGraph.d.ts.map +1 -0
  42. package/dist/types/src/components/Graph/{D3ForceGraph.stories.d.ts → CanvasForceGraph.stories.d.ts} +2 -2
  43. package/dist/types/src/components/Graph/CanvasForceGraph.stories.d.ts.map +1 -0
  44. package/dist/types/src/components/Graph/ForceGraph.d.ts +12 -5
  45. package/dist/types/src/components/Graph/ForceGraph.d.ts.map +1 -1
  46. package/dist/types/src/components/Graph/ForceGraph.stories.d.ts +3 -1
  47. package/dist/types/src/components/Graph/ForceGraph.stories.d.ts.map +1 -1
  48. package/dist/types/src/components/Graph/{adapter.d.ts → graph-adapter.d.ts} +1 -1
  49. package/dist/types/src/components/Graph/graph-adapter.d.ts.map +1 -0
  50. package/dist/types/src/components/Graph/index.d.ts +1 -1
  51. package/dist/types/src/components/Graph/index.d.ts.map +1 -1
  52. package/dist/types/src/components/Tree/EdgeBundling.stories.d.ts +21 -0
  53. package/dist/types/src/components/Tree/EdgeBundling.stories.d.ts.map +1 -0
  54. package/dist/types/src/components/Tree/Tree.d.ts +20 -23
  55. package/dist/types/src/components/Tree/Tree.d.ts.map +1 -1
  56. package/dist/types/src/components/Tree/Tree.stories.d.ts +5 -12
  57. package/dist/types/src/components/Tree/Tree.stories.d.ts.map +1 -1
  58. package/dist/types/src/components/Tree/index.d.ts +3 -0
  59. package/dist/types/src/components/Tree/index.d.ts.map +1 -1
  60. package/dist/types/src/components/Tree/layout/HierarchicalEdgeBundling.d.ts +35 -2
  61. package/dist/types/src/components/Tree/layout/HierarchicalEdgeBundling.d.ts.map +1 -1
  62. package/dist/types/src/components/Tree/layout/RadialTree.d.ts +35 -2
  63. package/dist/types/src/components/Tree/layout/RadialTree.d.ts.map +1 -1
  64. package/dist/types/src/components/Tree/layout/TidyTree.d.ts +24 -2
  65. package/dist/types/src/components/Tree/layout/TidyTree.d.ts.map +1 -1
  66. package/dist/types/src/components/Tree/layout/hierarchy.d.ts +17 -0
  67. package/dist/types/src/components/Tree/layout/hierarchy.d.ts.map +1 -0
  68. package/dist/types/src/components/Tree/layout/index.d.ts +5 -4
  69. package/dist/types/src/components/Tree/layout/index.d.ts.map +1 -1
  70. package/dist/types/src/components/Tree/layout/slots.d.ts +7 -0
  71. package/dist/types/src/components/Tree/layout/slots.d.ts.map +1 -0
  72. package/dist/types/src/components/Tree/layout/useContainerSize.d.ts +15 -0
  73. package/dist/types/src/components/Tree/layout/useContainerSize.d.ts.map +1 -0
  74. package/dist/types/src/components/Tree/space-graph-adapter.d.ts +32 -0
  75. package/dist/types/src/components/Tree/space-graph-adapter.d.ts.map +1 -0
  76. package/dist/types/src/components/Tree/testing/index.d.ts +1 -0
  77. package/dist/types/src/components/Tree/testing/index.d.ts.map +1 -1
  78. package/dist/types/src/components/Tree/testing/relations.d.ts +47 -0
  79. package/dist/types/src/components/Tree/testing/relations.d.ts.map +1 -0
  80. package/dist/types/src/components/Tree/types/types.d.ts +14 -4
  81. package/dist/types/src/components/Tree/types/types.d.ts.map +1 -1
  82. package/dist/types/src/containers/ExplorerArticle/ExplorerArticle.d.ts +8 -0
  83. package/dist/types/src/containers/ExplorerArticle/ExplorerArticle.d.ts.map +1 -0
  84. package/dist/types/src/containers/ExplorerArticle/ExplorerArticle.stories.d.ts +24 -0
  85. package/dist/types/src/containers/ExplorerArticle/ExplorerArticle.stories.d.ts.map +1 -0
  86. package/dist/types/src/containers/ExplorerArticle/index.d.ts +2 -0
  87. package/dist/types/src/containers/ExplorerArticle/index.d.ts.map +1 -0
  88. package/dist/types/src/containers/index.d.ts +1 -1
  89. package/dist/types/src/containers/index.d.ts.map +1 -1
  90. package/dist/types/src/hooks/useGraphModel.d.ts +2 -2
  91. package/dist/types/src/hooks/useGraphModel.d.ts.map +1 -1
  92. package/dist/types/src/index.d.ts +1 -2
  93. package/dist/types/src/index.d.ts.map +1 -1
  94. package/dist/types/src/plugin.d.ts +3 -0
  95. package/dist/types/src/plugin.d.ts.map +1 -0
  96. package/dist/types/src/testing.d.ts +2 -0
  97. package/dist/types/src/testing.d.ts.map +1 -0
  98. package/dist/types/src/types/Graph.d.ts +1 -2
  99. package/dist/types/src/types/Graph.d.ts.map +1 -1
  100. package/dist/types/tsconfig.tsbuildinfo +1 -1
  101. package/package.json +94 -67
  102. package/src/ExplorerPlugin.test.ts +2 -2
  103. package/src/ExplorerPlugin.tsx +3 -33
  104. package/src/capabilities/create-object.ts +36 -0
  105. package/src/capabilities/index.ts +1 -0
  106. package/src/capabilities/react-surface.tsx +2 -2
  107. package/src/components/Chart/Chart.stories.tsx +14 -20
  108. package/src/components/Globe/Globe.stories.tsx +17 -19
  109. package/src/components/Graph/CanvasForceGraph.stories.tsx +83 -0
  110. package/src/components/Graph/CanvasForceGraph.tsx +124 -0
  111. package/src/components/Graph/ForceGraph.stories.tsx +69 -37
  112. package/src/components/Graph/ForceGraph.tsx +104 -85
  113. package/src/components/Graph/index.ts +1 -1
  114. package/src/components/Tree/EdgeBundling.stories.tsx +144 -0
  115. package/src/components/Tree/Tree.stories.tsx +17 -38
  116. package/src/components/Tree/Tree.tsx +69 -100
  117. package/src/components/Tree/index.ts +3 -0
  118. package/src/components/Tree/layout/HierarchicalEdgeBundling.tsx +277 -0
  119. package/src/components/Tree/layout/RadialTree.tsx +237 -0
  120. package/src/components/Tree/layout/TidyTree.tsx +246 -0
  121. package/src/components/Tree/layout/hierarchy.ts +32 -0
  122. package/src/components/Tree/layout/index.ts +5 -5
  123. package/src/components/Tree/layout/slots.ts +19 -0
  124. package/src/components/Tree/layout/useContainerSize.ts +43 -0
  125. package/src/components/Tree/space-graph-adapter.ts +96 -0
  126. package/src/components/Tree/testing/index.ts +1 -0
  127. package/src/components/Tree/testing/relations.ts +182 -0
  128. package/src/components/Tree/types/types.ts +38 -29
  129. package/src/containers/ExplorerArticle/ExplorerArticle.stories.tsx +119 -0
  130. package/src/containers/ExplorerArticle/ExplorerArticle.tsx +153 -0
  131. package/src/containers/ExplorerArticle/index.ts +5 -0
  132. package/src/containers/index.ts +1 -1
  133. package/src/hooks/useGraphModel.ts +10 -6
  134. package/src/index.ts +1 -6
  135. package/src/plugin.ts +9 -0
  136. package/src/testing.ts +7 -0
  137. package/src/types/ExplorerAction.ts +1 -1
  138. package/src/types/Graph.ts +1 -2
  139. package/dist/lib/browser/hooks/index.mjs.map +0 -7
  140. package/dist/lib/browser/meta.json +0 -1
  141. package/dist/lib/browser/types/index.mjs.map +0 -7
  142. package/dist/lib/node-esm/chunk-6EUBRHHX.mjs +0 -26
  143. package/dist/lib/node-esm/chunk-6EUBRHHX.mjs.map +0 -7
  144. package/dist/lib/node-esm/chunk-HSLMI22Q.mjs +0 -11
  145. package/dist/lib/node-esm/components/index.mjs +0 -11255
  146. package/dist/lib/node-esm/components/index.mjs.map +0 -7
  147. package/dist/lib/node-esm/hooks/index.mjs +0 -41
  148. package/dist/lib/node-esm/hooks/index.mjs.map +0 -7
  149. package/dist/lib/node-esm/index.mjs +0 -14
  150. package/dist/lib/node-esm/index.mjs.map +0 -7
  151. package/dist/lib/node-esm/meta.json +0 -1
  152. package/dist/lib/node-esm/meta.mjs +0 -9
  153. package/dist/lib/node-esm/types/index.mjs +0 -73
  154. package/dist/lib/node-esm/types/index.mjs.map +0 -7
  155. package/dist/types/src/components/Graph/D3ForceGraph.d.ts +0 -15
  156. package/dist/types/src/components/Graph/D3ForceGraph.d.ts.map +0 -1
  157. package/dist/types/src/components/Graph/D3ForceGraph.stories.d.ts.map +0 -1
  158. package/dist/types/src/components/Graph/adapter.d.ts.map +0 -1
  159. package/dist/types/src/components/Graph/testing.d.ts +0 -14
  160. package/dist/types/src/components/Graph/testing.d.ts.map +0 -1
  161. package/dist/types/src/containers/ExplorerContainer/ExplorerContainer.d.ts +0 -6
  162. package/dist/types/src/containers/ExplorerContainer/ExplorerContainer.d.ts.map +0 -1
  163. package/dist/types/src/containers/ExplorerContainer/index.d.ts +0 -2
  164. package/dist/types/src/containers/ExplorerContainer/index.d.ts.map +0 -1
  165. package/src/components/Graph/D3ForceGraph.stories.tsx +0 -83
  166. package/src/components/Graph/D3ForceGraph.tsx +0 -108
  167. package/src/components/Graph/testing.ts +0 -58
  168. package/src/components/Tree/layout/HierarchicalEdgeBundling.ts +0 -162
  169. package/src/components/Tree/layout/RadialTree.ts +0 -94
  170. package/src/components/Tree/layout/TidyTree.ts +0 -101
  171. package/src/containers/ExplorerContainer/ExplorerContainer.tsx +0 -53
  172. package/src/containers/ExplorerContainer/index.ts +0 -5
  173. /package/dist/lib/{browser/chunk-J5LGTIGS.mjs.map → neutral/ExplorerPlugin.mjs.map} +0 -0
  174. /package/dist/lib/{browser → neutral}/chunk-HPIS2WXY.mjs +0 -0
  175. /package/dist/lib/{browser → neutral}/chunk-HPIS2WXY.mjs.map +0 -0
  176. /package/dist/lib/{browser → neutral}/chunk-J5LGTIGS.mjs +0 -0
  177. /package/dist/lib/{browser/meta.mjs.map → neutral/chunk-J5LGTIGS.mjs.map} +0 -0
  178. /package/dist/lib/{node-esm/chunk-HSLMI22Q.mjs.map → neutral/index.mjs.map} +0 -0
  179. /package/dist/lib/{browser → neutral}/meta.mjs +0 -0
  180. /package/dist/lib/{node-esm → neutral}/meta.mjs.map +0 -0
  181. /package/src/components/Graph/{adapter.ts → graph-adapter.ts} +0 -0
@@ -10822,45 +10822,158 @@ var Globe = ({ items = [], accessor, projection = "orthographic", options = defa
10822
10822
  });
10823
10823
  };
10824
10824
 
10825
- // src/components/Graph/D3ForceGraph.tsx
10825
+ // src/components/Graph/CanvasForceGraph.tsx
10826
+ import { forceLink, forceManyBody } from "d3";
10827
+ import NativeForceGraph from "force-graph";
10828
+ import React3, { useCallback, useEffect as useEffect3, useRef, useState } from "react";
10829
+ import { useResizeDetector as useResizeDetector3 } from "react-resize-detector";
10830
+ import { composable, composableProps } from "@dxos/ui-theme";
10831
+
10832
+ // src/components/Graph/graph-adapter.ts
10833
+ var GraphAdapter = class {
10834
+ graph;
10835
+ _nodes = [];
10836
+ _links = [];
10837
+ constructor(graph) {
10838
+ this.graph = graph;
10839
+ this._nodes = graph.nodes.map((node) => ({
10840
+ id: node.id,
10841
+ type: node.type,
10842
+ data: node.data
10843
+ }));
10844
+ const nodeIds = new Set(this._nodes.map((node) => node.id));
10845
+ this._links = graph.edges.filter((edge) => nodeIds.has(edge.source) && nodeIds.has(edge.target)).map((edge) => ({
10846
+ type: edge.type,
10847
+ source: edge.source,
10848
+ target: edge.target,
10849
+ data: edge.data
10850
+ }));
10851
+ }
10852
+ get nodes() {
10853
+ return this._nodes;
10854
+ }
10855
+ get links() {
10856
+ return this._links;
10857
+ }
10858
+ };
10859
+
10860
+ // src/components/Graph/CanvasForceGraph.tsx
10861
+ var CanvasForceGraph = composable(({ model, match, onClick, ...props }, forwardedRef) => {
10862
+ const { ref: resizeRef, width, height } = useResizeDetector3({
10863
+ refreshRate: 200
10864
+ });
10865
+ const setRef = useCallback((node) => {
10866
+ resizeRef(node);
10867
+ assignRef(forwardedRef, node);
10868
+ }, [
10869
+ resizeRef,
10870
+ forwardedRef
10871
+ ]);
10872
+ const rootRef = useRef(null);
10873
+ const forceGraph = useRef(null);
10874
+ const [data, setData] = useState();
10875
+ useEffect3(() => {
10876
+ return model?.subscribe((model2) => setData(new GraphAdapter(model2.graph)));
10877
+ }, [
10878
+ model
10879
+ ]);
10880
+ useEffect3(() => {
10881
+ if (rootRef.current) {
10882
+ forceGraph.current = new NativeForceGraph(rootRef.current).nodeRelSize(6).nodeLabel((node) => node.type === "schema" ? node.data.typename : node.data.label ?? node.id).nodeAutoColorBy((node) => node.type === "schema" ? "schema" : node.data.typename).linkAutoColorBy((link) => link.type);
10883
+ }
10884
+ return () => {
10885
+ forceGraph.current?.pauseAnimation().graphData({
10886
+ nodes: [],
10887
+ links: []
10888
+ });
10889
+ forceGraph.current = null;
10890
+ };
10891
+ }, []);
10892
+ useEffect3(() => {
10893
+ if (!data || !width || !height || !forceGraph.current) {
10894
+ return;
10895
+ }
10896
+ forceGraph.current.pauseAnimation().width(width).height(height).onEngineStop(() => handleZoomToFit()).onNodeClick((node) => {
10897
+ forceGraph.current?.emitParticle(node);
10898
+ }).d3Force("link", forceLink().distance(160).strength(0.5)).d3Force("charge", forceManyBody().strength(-30)).graphData(data).warmupTicks(100).cooldownTime(1e3).resumeAnimation();
10899
+ }, [
10900
+ data,
10901
+ width,
10902
+ height
10903
+ ]);
10904
+ const handleZoomToFit = () => {
10905
+ forceGraph.current?.zoomToFit(400, 40);
10906
+ };
10907
+ const handleClick = useCallback((event) => {
10908
+ onClick?.(event);
10909
+ if (!event.defaultPrevented) {
10910
+ handleZoomToFit();
10911
+ }
10912
+ }, [
10913
+ onClick
10914
+ ]);
10915
+ return /* @__PURE__ */ React3.createElement("div", {
10916
+ ...composableProps(props, {
10917
+ classNames: "relative grow"
10918
+ }),
10919
+ onClick: handleClick,
10920
+ ref: setRef
10921
+ }, /* @__PURE__ */ React3.createElement("div", {
10922
+ ref: rootRef,
10923
+ className: "absolute inset-0"
10924
+ }));
10925
+ });
10926
+ var assignRef = (ref, value) => {
10927
+ if (typeof ref === "function") {
10928
+ ref(value);
10929
+ } else if (ref) {
10930
+ ref.current = value;
10931
+ }
10932
+ };
10933
+
10934
+ // src/components/Graph/ForceGraph.tsx
10826
10935
  import { Atom, useAtomValue } from "@effect-atom/atom-react";
10827
- import React3, { useCallback, useEffect as useEffect3, useMemo, useRef } from "react";
10936
+ import React4, { useCallback as useCallback2, useEffect as useEffect4, useMemo, useRef as useRef2, useState as useState2 } from "react";
10828
10937
  import { Obj } from "@dxos/echo";
10829
10938
  import { SelectionModel } from "@dxos/graph";
10830
10939
  import { GraphForceProjector, SVG } from "@dxos/react-ui-graph";
10831
- import { composable, composableProps, getHashStyles } from "@dxos/ui-theme";
10940
+ import { composable as composable2, composableProps as composableProps2, getHashStyles } from "@dxos/ui-theme";
10832
10941
  import "@dxos/react-ui-graph/styles/graph.css";
10833
10942
  var EMPTY_ATOM = Atom.make({
10834
10943
  nodes: [],
10835
10944
  edges: []
10836
10945
  });
10837
- var D3ForceGraph = composable(({ model, selection: _selection, grid, drag, ...props }, forwardedRef) => {
10946
+ var ForceGraph = composable2(({ model, selection: selectionProp, grid, drag, onInspect, ...props }, forwardedRef) => {
10838
10947
  useAtomValue(model?.graphAtom ?? EMPTY_ATOM);
10839
- const svgRef = useRef(null);
10840
- const projector = useMemo(() => {
10948
+ const graph = useRef2(null);
10949
+ const selection = useMemo(() => selectionProp ?? new SelectionModel(), [
10950
+ selectionProp
10951
+ ]);
10952
+ useEffect4(() => {
10953
+ const unsubscribe = selection.subscribe(() => graph.current?.repaint());
10954
+ return unsubscribe;
10955
+ }, [
10956
+ selection
10957
+ ]);
10958
+ const svgRef = useRef2(null);
10959
+ const [projector, setProjector] = useState2();
10960
+ useEffect4(() => {
10841
10961
  if (svgRef.current) {
10842
- return new GraphForceProjector(svgRef.current, {
10962
+ setProjector(new GraphForceProjector(svgRef.current, {
10843
10963
  attributes: {
10844
- linkForce: (edge) => {
10845
- return edge.data?.object?.active !== false;
10846
- }
10964
+ // TODO(burdon): Check type (currently assumes Employee property).
10965
+ // Edge shouldn't contribute to force if it's not active.
10966
+ linkForce: (edge) => edge.data?.object?.active !== false
10847
10967
  },
10848
10968
  forces: {
10849
10969
  point: {
10850
10970
  strength: 0.01
10851
10971
  }
10852
10972
  }
10853
- });
10973
+ }));
10854
10974
  }
10855
10975
  }, []);
10856
- const graph = useRef(null);
10857
- const selection = useMemo(() => _selection ?? new SelectionModel(), [
10858
- _selection
10859
- ]);
10860
- useEffect3(() => selection.subscribe(() => graph.current?.repaint()), [
10861
- selection
10862
- ]);
10863
- const handleSelect = useCallback((node) => {
10976
+ const handleSelect = useCallback2((node) => {
10864
10977
  if (selection.contains(node.id)) {
10865
10978
  selection.remove(node.id);
10866
10979
  } else {
@@ -10869,29 +10982,27 @@ var D3ForceGraph = composable(({ model, selection: _selection, grid, drag, ...pr
10869
10982
  }, [
10870
10983
  selection
10871
10984
  ]);
10872
- return /* @__PURE__ */ React3.createElement("div", {
10873
- ...composableProps(props, {
10985
+ return /* @__PURE__ */ React4.createElement("div", {
10986
+ ...composableProps2(props, {
10874
10987
  classNames: "dx-container"
10875
10988
  }),
10876
10989
  ref: forwardedRef
10877
- }, /* @__PURE__ */ React3.createElement(SVG.Root, {
10990
+ }, /* @__PURE__ */ React4.createElement(SVG.Root, {
10878
10991
  ref: svgRef
10879
- }, /* @__PURE__ */ React3.createElement(SVG.Markers, null), grid && /* @__PURE__ */ React3.createElement(SVG.Grid, {
10992
+ }, /* @__PURE__ */ React4.createElement(SVG.Markers, null), grid && /* @__PURE__ */ React4.createElement(SVG.Grid, {
10880
10993
  axis: true
10881
- }), /* @__PURE__ */ React3.createElement(SVG.Zoom, {
10994
+ }), /* @__PURE__ */ React4.createElement(SVG.Zoom, {
10882
10995
  extent: [
10883
10996
  1 / 2,
10884
10997
  2
10885
10998
  ]
10886
- }, /* @__PURE__ */ React3.createElement(SVG.Graph, {
10887
- drag,
10999
+ }, /* @__PURE__ */ React4.createElement(SVG.Graph, {
10888
11000
  ref: graph,
11001
+ drag,
10889
11002
  model,
10890
11003
  projector,
10891
11004
  labels: {
10892
- text: (node) => {
10893
- return node.data?.data.label ?? node.id;
10894
- }
11005
+ text: (node) => node.data?.data.label ?? node.id
10895
11006
  },
10896
11007
  attributes: {
10897
11008
  node: (node) => {
@@ -10906,217 +11017,402 @@ var D3ForceGraph = composable(({ model, selection: _selection, grid, drag, ...pr
10906
11017
  };
10907
11018
  }
10908
11019
  },
10909
- onSelect: handleSelect
11020
+ onSelect: handleSelect,
11021
+ onInspect
10910
11022
  }))));
10911
11023
  });
10912
11024
 
10913
- // src/components/Graph/ForceGraph.tsx
10914
- import { forceLink, forceManyBody } from "d3";
10915
- import NativeForceGraph from "force-graph";
10916
- import React4, { useEffect as useEffect4, useRef as useRef2, useState } from "react";
10917
- import { useResizeDetector as useResizeDetector3 } from "react-resize-detector";
10918
- import { filterObjectsSync } from "@dxos/plugin-search";
11025
+ // src/components/Tree/Tree.tsx
11026
+ import React8, { useMemo as useMemo5 } from "react";
10919
11027
 
10920
- // src/components/Graph/adapter.ts
10921
- var GraphAdapter = class {
10922
- graph;
10923
- _nodes = [];
10924
- _links = [];
10925
- constructor(graph) {
10926
- this.graph = graph;
10927
- this._nodes = graph.nodes.map((node) => ({
10928
- id: node.id,
10929
- type: node.type,
10930
- data: node.data
10931
- }));
10932
- const nodeIds = new Set(this._nodes.map((node) => node.id));
10933
- this._links = graph.edges.filter((edge) => nodeIds.has(edge.source) && nodeIds.has(edge.target)).map((edge) => ({
10934
- type: edge.type,
10935
- source: edge.source,
10936
- target: edge.target,
10937
- data: edge.data
10938
- }));
10939
- }
10940
- get nodes() {
10941
- return this._nodes;
10942
- }
10943
- get links() {
10944
- return this._links;
10945
- }
11028
+ // src/components/Tree/layout/HierarchicalEdgeBundling.tsx
11029
+ import { cluster, curveBundle, hierarchy, lineRadial, select } from "d3";
11030
+ import React5, { useEffect as useEffect6, useMemo as useMemo2, useRef as useRef3 } from "react";
11031
+ import { mx } from "@dxos/ui-theme";
11032
+
11033
+ // src/components/Tree/layout/slots.ts
11034
+ var defaultTreeLayoutSlots = {
11035
+ // Cursor + transition so the hover swap reads clearly; SVG circles support the `:hover` pseudo-class
11036
+ // via Tailwind variants exactly like HTML elements.
11037
+ node: "fill-blue-600 hover:fill-orange-500 cursor-pointer transition-colors",
11038
+ // 0.5px is fine on a white background, but on a dark Storybook background the lines disappear.
11039
+ // Use stroke-1 with opacity 50% so they read in both themes; dx-bundle-dim/out/in further tune on hover.
11040
+ path: "fill-none stroke-blue-500/50 stroke-[1px] dark:stroke-blue-400/60",
11041
+ text: "fill-neutral-700 dark:fill-neutral-300 text-xs hover:fill-orange-500 cursor-pointer transition-colors"
10946
11042
  };
10947
11043
 
10948
- // src/components/Graph/ForceGraph.tsx
10949
- var ForceGraph = ({ model, match }) => {
10950
- const { ref, width, height } = useResizeDetector3({
10951
- refreshRate: 200
11044
+ // src/components/Tree/layout/useContainerSize.ts
11045
+ import { useEffect as useEffect5, useState as useState3 } from "react";
11046
+ var useContainerSize = () => {
11047
+ const [el, setEl] = useState3(null);
11048
+ const [size, setSize] = useState3({
11049
+ width: 0,
11050
+ height: 0
10952
11051
  });
10953
- const rootRef = useRef2(null);
10954
- const forceGraph = useRef2(null);
10955
- const filteredRef = useRef2([]);
10956
- filteredRef.current = filterObjectsSync(model?.objects ?? [], match);
10957
- const [data, setData] = useState();
10958
- useEffect4(() => {
10959
- return model?.subscribe((model2) => {
10960
- setData(new GraphAdapter(model2.graph));
11052
+ useEffect5(() => {
11053
+ if (!el) {
11054
+ return;
11055
+ }
11056
+ const rect = el.getBoundingClientRect();
11057
+ setSize({
11058
+ width: rect.width,
11059
+ height: rect.height
11060
+ });
11061
+ const observer = new ResizeObserver((entries) => {
11062
+ const entry = entries[0];
11063
+ if (!entry) {
11064
+ return;
11065
+ }
11066
+ const { width, height } = entry.contentRect;
11067
+ setSize((prev) => prev.width === width && prev.height === height ? prev : {
11068
+ width,
11069
+ height
11070
+ });
10961
11071
  });
11072
+ observer.observe(el);
11073
+ return () => observer.disconnect();
10962
11074
  }, [
10963
- model
11075
+ el
10964
11076
  ]);
10965
- useEffect4(() => {
10966
- if (rootRef.current) {
10967
- forceGraph.current = new NativeForceGraph(rootRef.current).nodeRelSize(6).nodeLabel((node) => node.type === "schema" ? node.data.typename : node.data.label ?? node.id).nodeAutoColorBy((node) => node.type === "schema" ? "schema" : node.data.typename).linkAutoColorBy((link2) => link2.type);
10968
- }
10969
- return () => {
10970
- forceGraph.current?.pauseAnimation().graphData({
10971
- nodes: [],
10972
- links: []
10973
- });
10974
- forceGraph.current = null;
10975
- };
10976
- }, []);
10977
- useEffect4(() => {
10978
- if (!data || !width || !height || !forceGraph.current) {
11077
+ return {
11078
+ setRef: setEl,
11079
+ width: size.width,
11080
+ height: size.height
11081
+ };
11082
+ };
11083
+
11084
+ // src/components/Tree/layout/HierarchicalEdgeBundling.tsx
11085
+ var TRANSITION_MS = 350;
11086
+ var HierarchicalEdgeBundling = ({ classNames, data, edges = [], label = (d) => d.label ?? d.id, padding = 120, tension = 0.85, slots = defaultTreeLayoutSlots, onNodeHover }) => {
11087
+ const svgRef = useRef3(null);
11088
+ const { setRef, width, height } = useContainerSize();
11089
+ const root = useMemo2(() => buildBundleHierarchy(data, edges), [
11090
+ data,
11091
+ edges
11092
+ ]);
11093
+ const handleHoverRef = useRef3(() => {
11094
+ });
11095
+ handleHoverRef.current = (node, event) => onNodeHover?.(node, event);
11096
+ useEffect6(() => {
11097
+ if (!svgRef.current || !width || !height) {
10979
11098
  return;
10980
11099
  }
10981
- forceGraph.current.pauseAnimation().width(width).height(height).onEngineStop(() => {
10982
- handleZoomToFit();
10983
- }).onNodeClick((node) => {
10984
- forceGraph.current?.emitParticle(node);
10985
- }).d3Force("link", forceLink().distance(160).strength(0.5)).d3Force("charge", forceManyBody().strength(-30)).graphData(data).warmupTicks(100).cooldownTime(1e3).resumeAnimation();
11100
+ const radius = Math.max(0, Math.min(width, height) / 2 - padding);
11101
+ renderBundling(svgRef.current, root, {
11102
+ radius,
11103
+ label,
11104
+ slots,
11105
+ tension,
11106
+ onNodeHover: (n, e) => handleHoverRef.current(n, e)
11107
+ });
10986
11108
  }, [
10987
- data,
11109
+ root,
10988
11110
  width,
10989
11111
  height,
10990
- forceGraph.current
11112
+ padding,
11113
+ tension,
11114
+ label,
11115
+ slots
10991
11116
  ]);
10992
- const handleZoomToFit = () => {
10993
- forceGraph.current?.zoomToFit(400, 40);
10994
- };
10995
- return /* @__PURE__ */ React4.createElement("div", {
10996
- ref,
10997
- className: "relative grow",
10998
- onClick: handleZoomToFit
10999
- }, /* @__PURE__ */ React4.createElement("div", {
11000
- ref: rootRef,
11001
- className: "absolute inset-0"
11117
+ return /* @__PURE__ */ React5.createElement("div", {
11118
+ ref: setRef,
11119
+ className: mx("dx-expander relative", classNames)
11120
+ }, width > 0 && height > 0 && /* @__PURE__ */ React5.createElement("svg", {
11121
+ ref: svgRef,
11122
+ xmlns: "http://www.w3.org/2000/svg",
11123
+ width,
11124
+ height,
11125
+ viewBox: `${-width / 2} ${-height / 2} ${width} ${height}`
11002
11126
  }));
11003
11127
  };
11004
-
11005
- // src/components/Tree/Tree.tsx
11006
- import { RegistryContext } from "@effect-atom/atom-react";
11007
- import React5, { useContext, useEffect as useEffect5, useRef as useRef3, useState as useState2 } from "react";
11008
- import { useAsyncState } from "@dxos/react-ui";
11009
- import { SVG as SVG2 } from "@dxos/react-ui-graph";
11010
- import { SpaceGraphModel } from "@dxos/schema";
11011
-
11012
- // src/components/Tree/layout/HierarchicalEdgeBundling.ts
11013
- import { cluster, curveBundle, hierarchy, lineRadial, select } from "d3";
11014
- var HierarchicalEdgeBundling = (s, data, options) => {
11015
- const svg = select(s);
11016
- svg.selectAll("*").remove();
11017
- const { radius = 600, padding = 100, slots } = options;
11018
- const root = hierarchy(flatten(data));
11019
- const tree3 = cluster().size([
11128
+ var buildBundleHierarchy = (data, edges) => {
11129
+ const root = hierarchy(data);
11130
+ const byId = /* @__PURE__ */ new Map();
11131
+ for (const node of root.descendants()) {
11132
+ byId.set(node.data.id, node);
11133
+ node.outgoing = [];
11134
+ node.incoming = [];
11135
+ }
11136
+ for (const edge of edges) {
11137
+ const source = byId.get(edge.source);
11138
+ const target = byId.get(edge.target);
11139
+ if (!source || !target || source === target) {
11140
+ continue;
11141
+ }
11142
+ source.outgoing.push([
11143
+ source,
11144
+ target,
11145
+ edge
11146
+ ]);
11147
+ target.incoming.push([
11148
+ source,
11149
+ target,
11150
+ edge
11151
+ ]);
11152
+ }
11153
+ return root;
11154
+ };
11155
+ var renderBundling = (svgElement, root, options) => {
11156
+ const { radius, tension, label, slots, onNodeHover } = options;
11157
+ const svg = select(svgElement);
11158
+ if (!root.children?.length) {
11159
+ svg.selectAll("g.dx-bundle-root").remove();
11160
+ return;
11161
+ }
11162
+ cluster().size([
11020
11163
  2 * Math.PI,
11021
- radius - padding
11022
- ]);
11023
- const layout = tree3(addLinks(root));
11024
- const node = svg.append("g").selectAll().data(layout.leaves()).join("g").attr("transform", (d) => `rotate(${d.x * (180 / Math.PI) - 90}) translate(${d.y},0)`).append("text").attr("class", slots?.text ?? "").attr("dy", "0.31em").attr("x", (d) => d.x < Math.PI ? 6 : -6).attr("text-anchor", (d) => d.x < Math.PI ? "start" : "end").attr("transform", (d) => d.x >= Math.PI ? "rotate(180)" : null).call((text) => text.text((d) => d.data.id.slice(0, 8)));
11025
- const line = lineRadial().curve(curveBundle.beta(0.85)).radius((d) => d.y).angle((d) => d.x);
11026
- const links = svg.append("g").selectAll().data(layout.leaves().flatMap((leaf) => leaf.outgoing)).join("path").style("mix-blend-mode", "multiply").attr("class", slots?.path ?? "").attr("d", ([i, o]) => {
11027
- return line(i.path(o));
11028
- }).each(function(d) {
11029
- d.path = this;
11164
+ radius
11165
+ ])(root);
11166
+ const g = svg.selectAll("g.dx-bundle-root").data([
11167
+ null
11168
+ ]).join("g").classed("dx-bundle-root", true);
11169
+ const linksLayer = g.selectAll("g.dx-bundle-links").data([
11170
+ null
11171
+ ]).join("g").classed("dx-bundle-links", true);
11172
+ const nodesLayer = g.selectAll("g.dx-bundle-nodes").data([
11173
+ null
11174
+ ]).join("g").classed("dx-bundle-nodes", true);
11175
+ const line = lineRadial().curve(curveBundle.beta(tension)).radius((d) => d.y).angle((d) => d.x);
11176
+ const leaves = root.leaves();
11177
+ const flatEdges = leaves.flatMap((leaf) => leaf.outgoing ?? []);
11178
+ const paths = linksLayer.selectAll("path").data(flatEdges, (d) => `${d[0].data.id}->${d[1].data.id}`).join((enter) => enter.append("path").attr("class", slots.path ?? "").attr("fill", "none").attr("opacity", 0), (update) => update, (exit) => exit.each(function() {
11179
+ select(this).interrupt();
11180
+ }).transition().duration(TRANSITION_MS).attr("opacity", 0).remove());
11181
+ paths.each(function(d) {
11182
+ d[0].pathEl = this;
11183
+ }).transition().duration(TRANSITION_MS).attr("opacity", 1).attr("d", ([s, t]) => line(s.path(t)));
11184
+ const labels = nodesLayer.selectAll("g.dx-bundle-leaf").data(leaves, (d) => d.data.id).join((enter) => {
11185
+ const ge = enter.append("g").classed("dx-bundle-leaf", true).attr("opacity", 0);
11186
+ ge.append("text").attr("dy", "0.32em").attr("paint-order", "stroke").style("cursor", "pointer");
11187
+ return ge;
11188
+ }, (update) => update, (exit) => exit.each(function() {
11189
+ select(this).interrupt();
11190
+ }).transition().duration(TRANSITION_MS).attr("opacity", 0).remove());
11191
+ labels.transition().duration(TRANSITION_MS).attr("opacity", 1).attr("transform", (d) => `rotate(${d.x * 180 / Math.PI - 90}) translate(${d.y},0)`);
11192
+ labels.select("text").attr("class", slots.text ?? "").attr("x", (d) => d.x < Math.PI ? 6 : -6).attr("text-anchor", (d) => d.x < Math.PI ? "start" : "end").attr("transform", (d) => d.x >= Math.PI ? "rotate(180)" : null).each(function(d) {
11193
+ d.text = this;
11194
+ }).text((d) => label(d.data)).on("pointerenter", function(event, d) {
11195
+ onNodeHover(d.data, event);
11196
+ hover(linksLayer, leaves, d, true);
11197
+ }).on("pointerleave", function(event, d) {
11198
+ onNodeHover(null);
11199
+ hover(linksLayer, leaves, d, false);
11030
11200
  });
11031
11201
  };
11032
- var addLinks = (root) => {
11033
- const nodes = new Map(root.descendants().map((d) => [
11034
- d.data.id,
11035
- d
11036
- ]));
11037
- const parents = root.descendants().reduce((map, d) => {
11038
- if (d.children?.length) {
11039
- map.set(d.data.id, d);
11040
- }
11041
- return map;
11042
- }, /* @__PURE__ */ new Map());
11043
- for (const d of root.leaves()) {
11044
- const parent = parents.get(d.data.id);
11045
- if (parent) {
11046
- d.outgoing = parent.data.children?.slice(1).map((child) => {
11047
- return [
11048
- d,
11049
- nodes.get(child.id)
11050
- ];
11051
- }) ?? [];
11052
- } else {
11053
- d.outgoing = [];
11202
+ var hover = (linksLayer, leaves, focused, on) => {
11203
+ const outgoing = new Set((focused.outgoing ?? []).map(([, t]) => t));
11204
+ const incoming = new Set((focused.incoming ?? []).map(([s]) => s));
11205
+ linksLayer.selectAll("path").classed("dx-bundle-out", (d) => on && d[0] === focused).classed("dx-bundle-in", (d) => on && d[1] === focused).classed("dx-bundle-dim", (d) => on && d[0] !== focused && d[1] !== focused);
11206
+ for (const leaf of leaves) {
11207
+ if (!leaf.text) {
11208
+ continue;
11054
11209
  }
11210
+ select(leaf.text).classed("dx-bundle-focused", on && leaf === focused).classed("dx-bundle-out-text", on && outgoing.has(leaf)).classed("dx-bundle-in-text", on && incoming.has(leaf));
11055
11211
  }
11056
- return root;
11057
11212
  };
11058
- var flatten = (node) => {
11059
- const clone = {
11060
- id: node.id
11061
- };
11062
- if (node.children?.length) {
11063
- const children = node.children.map((child) => flatten(child));
11064
- clone.children = [
11065
- {
11066
- id: node.id
11067
- },
11068
- ...children
11069
- ];
11070
- }
11071
- return clone;
11213
+
11214
+ // src/components/Tree/layout/RadialTree.tsx
11215
+ import { cluster as d3Cluster, linkRadial, select as select2, tree as d3Tree } from "d3";
11216
+ import React6, { useCallback as useCallback3, useEffect as useEffect7, useMemo as useMemo3, useRef as useRef4, useState as useState4 } from "react";
11217
+ import { mx as mx2 } from "@dxos/ui-theme";
11218
+
11219
+ // src/components/Tree/layout/hierarchy.ts
11220
+ import { hierarchy as d3Hierarchy } from "d3";
11221
+ var buildHierarchy = (data, collapsed = /* @__PURE__ */ new Set()) => {
11222
+ return d3Hierarchy(data, (d) => {
11223
+ if (!d.children?.length) {
11224
+ return void 0;
11225
+ }
11226
+ return collapsed.has(d.id) ? void 0 : d.children;
11227
+ });
11072
11228
  };
11073
- var HierarchicalEdgeBundling_default = HierarchicalEdgeBundling;
11229
+ var isCollapsed = (data, collapsed) => Boolean(data.children?.length) && collapsed.has(data.id);
11230
+ var isLeaf = (data) => !data.children?.length;
11074
11231
 
11075
- // src/components/Tree/layout/RadialTree.ts
11076
- import { hierarchy as hierarchy2, linkRadial, select as select2, tree } from "d3";
11077
- var RadialTree = (s, data, options) => {
11078
- const svg = select2(s);
11079
- svg.selectAll("*").remove();
11080
- const { label, radius = 400, r = 4, slots } = options;
11081
- const arc = 2 * Math.PI;
11082
- const root = hierarchy2(data);
11083
- const descendants = root.descendants();
11084
- const getLabel = label === null ? null : descendants.map((d) => label(d.data));
11085
- const layout = tree().size([
11086
- arc,
11232
+ // src/components/Tree/layout/RadialTree.tsx
11233
+ var TRANSITION_MS2 = 350;
11234
+ var RadialTree = ({ classNames, data, label = (d) => d.label ?? d.id, slots = defaultTreeLayoutSlots, r = 4, padding = 80, initialCollapsed, cluster: cluster2 = false, onNodeClick, onNodeHover }) => {
11235
+ const svgRef = useRef4(null);
11236
+ const { setRef, width, height } = useContainerSize();
11237
+ const [collapsed, setCollapsed] = useState4(() => new Set(initialCollapsed ?? []));
11238
+ const toggle = useCallback3((id) => {
11239
+ setCollapsed((prev) => {
11240
+ const next = new Set(prev);
11241
+ if (next.has(id)) {
11242
+ next.delete(id);
11243
+ } else {
11244
+ next.add(id);
11245
+ }
11246
+ return next;
11247
+ });
11248
+ }, []);
11249
+ const handleClickRef = useRef4(() => {
11250
+ });
11251
+ handleClickRef.current = (node) => {
11252
+ onNodeClick?.(node);
11253
+ if (node.children?.length) {
11254
+ toggle(node.id);
11255
+ }
11256
+ };
11257
+ const handleHoverRef = useRef4(() => {
11258
+ });
11259
+ handleHoverRef.current = (node, event) => onNodeHover?.(node, event);
11260
+ const root = useMemo3(() => buildHierarchy(data, collapsed), [
11261
+ data,
11262
+ collapsed
11263
+ ]);
11264
+ useEffect7(() => {
11265
+ if (!svgRef.current || !width || !height) {
11266
+ return;
11267
+ }
11268
+ const radius = Math.max(0, Math.min(width, height) / 2 - padding);
11269
+ renderRadialTree(svgRef.current, root, {
11270
+ radius,
11271
+ r,
11272
+ label,
11273
+ slots,
11274
+ collapsed,
11275
+ cluster: cluster2,
11276
+ onNodeClick: (n) => handleClickRef.current(n),
11277
+ onNodeHover: (n, e) => handleHoverRef.current(n, e)
11278
+ });
11279
+ }, [
11280
+ root,
11281
+ width,
11282
+ height,
11283
+ r,
11284
+ padding,
11285
+ label,
11286
+ slots,
11287
+ collapsed,
11288
+ cluster2
11289
+ ]);
11290
+ return /* @__PURE__ */ React6.createElement("div", {
11291
+ ref: setRef,
11292
+ className: mx2("dx-expander relative", classNames)
11293
+ }, width > 0 && height > 0 && /* @__PURE__ */ React6.createElement("svg", {
11294
+ ref: svgRef,
11295
+ xmlns: "http://www.w3.org/2000/svg",
11296
+ width,
11297
+ height,
11298
+ viewBox: `${-width / 2} ${-height / 2} ${width} ${height}`
11299
+ }));
11300
+ };
11301
+ var renderRadialTree = (svgElement, root, options) => {
11302
+ const { radius, r, label, slots, collapsed, cluster: cluster2, onNodeClick, onNodeHover } = options;
11303
+ const svg = select2(svgElement);
11304
+ const layout = cluster2 ? d3Cluster() : d3Tree();
11305
+ layout.size([
11306
+ 2 * Math.PI,
11087
11307
  radius
11088
- ]).separation((a, b) => (a.parent === b.parent ? 1 : 2) / a.depth);
11089
- layout(root);
11090
- svg.append("g").selectAll("path").data(root.links()).join("path").attr("class", slots?.path ?? "").attr("d", linkRadial().angle((d) => d.x + Math.PI / 2).radius((d) => d.y));
11091
- const node = svg.append("g").selectAll("a").data(root.descendants()).join("a").attr("transform", (d) => `rotate(${d.x * 180 / Math.PI}) translate(${d.y},0)`);
11092
- node.append("circle").attr("class", slots?.node ?? "").attr("r", r);
11093
- if (getLabel) {
11094
- node.append("text").attr("transform", (d) => `rotate(${d.x >= Math.PI ? 180 : 0})`).attr("dy", "0.32em").attr("x", (d) => d.x < Math.PI === !d.children ? 6 : -6).attr("text-anchor", (d) => d.x < Math.PI === !d.children ? "start" : "end").attr("class", slots?.text ?? "").text((d, i) => getLabel[i]);
11095
- }
11096
- return svg.node();
11308
+ ]).separation((a, b) => (a.parent === b.parent ? 1 : 2) / Math.max(1, a.depth))(root);
11309
+ const g = svg.selectAll("g.dx-radial-root").data([
11310
+ null
11311
+ ]).join("g").classed("dx-radial-root", true);
11312
+ const linksLayer = g.selectAll("g.dx-radial-links").data([
11313
+ null
11314
+ ]).join("g").classed("dx-radial-links", true);
11315
+ const nodesLayer = g.selectAll("g.dx-radial-nodes").data([
11316
+ null
11317
+ ]).join("g").classed("dx-radial-nodes", true);
11318
+ const linkPath = linkRadial().angle((d) => d.x).radius((d) => d.y);
11319
+ linksLayer.selectAll("path").data(root.links(), (d) => `${d.source.data.id}->${d.target.data.id}`).join((enter) => enter.append("path").attr("class", slots.path ?? "").attr("fill", "none").attr("opacity", 0), (update) => update, (exit) => exit.transition().duration(TRANSITION_MS2).attr("opacity", 0).remove()).transition().duration(TRANSITION_MS2).attr("opacity", 1).attr("d", linkPath);
11320
+ const node = nodesLayer.selectAll("g.dx-radial-node").data(root.descendants(), (d) => d.data.id);
11321
+ const nodeEnter = node.enter().append("g").classed("dx-radial-node", true).attr("opacity", 0).attr("transform", (d) => `rotate(${d.x * 180 / Math.PI - 90}) translate(${d.y},0)`).style("cursor", (d) => d.data.children?.length ? "pointer" : "default").on("click", (_, d) => onNodeClick(d.data));
11322
+ nodeEnter.append("circle").attr("r", r).on("pointerenter", (event, d) => onNodeHover(d.data, event)).on("pointerleave", (event) => onNodeHover(null, event));
11323
+ nodeEnter.append("text").attr("dy", "0.32em").attr("paint-order", "stroke").text((d) => label(d.data));
11324
+ const nodeMerge = nodeEnter.merge(node);
11325
+ nodeMerge.transition().duration(TRANSITION_MS2).attr("opacity", 1).attr("transform", (d) => `rotate(${d.x * 180 / Math.PI - 90}) translate(${d.y},0)`);
11326
+ nodeMerge.select("circle").attr("class", (d) => {
11327
+ const collapsedHere = isCollapsed(d.data, collapsed);
11328
+ const leaf = isLeaf(d.data);
11329
+ return [
11330
+ slots.node ?? "",
11331
+ collapsedHere ? "dx-collapsed" : leaf ? "dx-leaf" : "dx-branch"
11332
+ ].filter(Boolean).join(" ");
11333
+ }).attr("r", r);
11334
+ nodeMerge.select("text").attr("class", slots.text ?? "").attr("transform", (d) => d.x >= Math.PI ? "rotate(180)" : null).attr("x", (d) => d.x < Math.PI === !d.children ? r + 4 : -(r + 4)).attr("text-anchor", (d) => d.x < Math.PI === !d.children ? "start" : "end").text((d) => label(d.data));
11335
+ node.exit().transition().duration(TRANSITION_MS2).attr("opacity", 0).remove();
11097
11336
  };
11098
- var RadialTree_default = RadialTree;
11099
11337
 
11100
- // src/components/Tree/layout/TidyTree.ts
11101
- import { curveBumpX, hierarchy as hierarchy3, link, select as select3, tree as tree2 } from "d3";
11102
- var TidyTree = (s, data, options) => {
11103
- const svg = select3(s);
11104
- svg.selectAll("*").remove();
11105
- const { label, width, height, r = 4, padding = 4, margin = 60, slots } = options;
11106
- const root = hierarchy3(data);
11107
- const descendants = root.descendants();
11108
- const getLabel = label == null ? null : descendants.map((d) => label(d.data));
11109
- const dx = 16;
11110
- const dy = width / (root.height + padding);
11111
- const layout = tree2().nodeSize([
11338
+ // src/components/Tree/layout/TidyTree.tsx
11339
+ import { curveBumpX, link as d3Link, select as select3, tree as d3Tree2 } from "d3";
11340
+ import React7, { useCallback as useCallback4, useEffect as useEffect8, useMemo as useMemo4, useRef as useRef5, useState as useState5 } from "react";
11341
+ import { mx as mx3 } from "@dxos/ui-theme";
11342
+ var TRANSITION_MS3 = 350;
11343
+ var TidyTree = ({ classNames, data, label = (d) => d.label ?? d.id, slots = defaultTreeLayoutSlots, r = 4, margin = 24, initialCollapsed, onNodeClick }) => {
11344
+ const svgRef = useRef5(null);
11345
+ const { setRef, width, height } = useContainerSize();
11346
+ const [collapsed, setCollapsed] = useState5(() => new Set(initialCollapsed ?? []));
11347
+ const toggle = useCallback4((id) => {
11348
+ setCollapsed((prev) => {
11349
+ const next = new Set(prev);
11350
+ if (next.has(id)) {
11351
+ next.delete(id);
11352
+ } else {
11353
+ next.add(id);
11354
+ }
11355
+ return next;
11356
+ });
11357
+ }, []);
11358
+ const handleClickRef = useRef5(() => {
11359
+ });
11360
+ handleClickRef.current = (node) => {
11361
+ onNodeClick?.(node);
11362
+ if (node.children?.length) {
11363
+ toggle(node.id);
11364
+ }
11365
+ };
11366
+ const root = useMemo4(() => buildHierarchy(data, collapsed), [
11367
+ data,
11368
+ collapsed
11369
+ ]);
11370
+ useEffect8(() => {
11371
+ if (!svgRef.current || !width || !height) {
11372
+ return;
11373
+ }
11374
+ renderTidyTree(svgRef.current, root, {
11375
+ width,
11376
+ height,
11377
+ r,
11378
+ margin,
11379
+ label,
11380
+ slots,
11381
+ collapsed,
11382
+ onNodeClick: (n) => handleClickRef.current(n)
11383
+ });
11384
+ }, [
11385
+ root,
11386
+ width,
11387
+ height,
11388
+ r,
11389
+ margin,
11390
+ label,
11391
+ slots,
11392
+ collapsed
11393
+ ]);
11394
+ return /* @__PURE__ */ React7.createElement("div", {
11395
+ ref: setRef,
11396
+ className: mx3("dx-expander relative", classNames)
11397
+ }, width > 0 && height > 0 && /* @__PURE__ */ React7.createElement("svg", {
11398
+ ref: svgRef,
11399
+ xmlns: "http://www.w3.org/2000/svg",
11400
+ width,
11401
+ height,
11402
+ viewBox: `${-width / 2} ${-height / 2} ${width} ${height}`
11403
+ }));
11404
+ };
11405
+ var renderTidyTree = (svgElement, root, options) => {
11406
+ const { width, height, r, margin, label, slots, collapsed, onNodeClick } = options;
11407
+ const svg = select3(svgElement);
11408
+ const dx = 18;
11409
+ const dy = Math.max(60, (width - margin * 2) / Math.max(1, root.height + 1));
11410
+ d3Tree2().nodeSize([
11112
11411
  dx,
11113
11412
  dy
11114
- ]);
11115
- layout(root);
11413
+ ])(root);
11116
11414
  let x0 = Infinity;
11117
11415
  let x1 = -x0;
11118
- let y0 = Infinity;
11119
- let y1 = -y0;
11120
11416
  root.each((d) => {
11121
11417
  if (d.x > x1) {
11122
11418
  x1 = d.x;
@@ -11124,27 +11420,139 @@ var TidyTree = (s, data, options) => {
11124
11420
  if (d.x < x0) {
11125
11421
  x0 = d.x;
11126
11422
  }
11127
- if (d.y > y1) {
11128
- y1 = d.y;
11129
- }
11130
- if (d.y < y0) {
11131
- y0 = d.y;
11132
- }
11133
11423
  });
11134
- const sx = Math.min(2, Math.max(1, (height - margin * 2) / (x1 - x0)));
11135
- const oy = -(width - (y1 - y0)) / 2;
11136
- svg.append("g").selectAll("path").data(root.links()).join("path").attr("class", slots?.path ?? "").attr("d", link(curveBumpX).x((d) => d.y + oy).y((d) => d.x * sx));
11137
- const node = svg.append("g").selectAll("a").data(root.descendants()).join("a").attr("transform", (d) => `translate(${d.y + oy},${d.x * sx})`);
11138
- node.append("circle").attr("class", slots?.node ?? "").attr("r", r);
11139
- if (getLabel) {
11140
- node.append("text").attr("dy", "0.32em").attr("x", (d) => d.children ? -6 : 6).attr("text-anchor", (d) => d.children ? "end" : "start").attr("class", slots?.text ?? "").text((d, i) => getLabel[i]);
11424
+ const treeWidth = width - margin * 2;
11425
+ const treeHeight = x1 - x0;
11426
+ const scaleY = treeHeight > 0 ? Math.min(1, (height - margin * 2) / treeHeight) : 1;
11427
+ const offsetX = -treeWidth / 2;
11428
+ const offsetY = -(x0 + x1) / 2;
11429
+ const g = svg.selectAll("g.dx-tidy-root").data([
11430
+ null
11431
+ ]).join("g").classed("dx-tidy-root", true);
11432
+ const linksLayer = g.selectAll("g.dx-tidy-links").data([
11433
+ null
11434
+ ]).join("g").classed("dx-tidy-links", true);
11435
+ const nodesLayer = g.selectAll("g.dx-tidy-nodes").data([
11436
+ null
11437
+ ]).join("g").classed("dx-tidy-nodes", true);
11438
+ const linkPath = d3Link(curveBumpX).x((d) => offsetX + d.y).y((d) => (d.x + offsetY) * scaleY);
11439
+ linksLayer.selectAll("path").data(root.links(), (d) => `${d.source.data.id}->${d.target.data.id}`).join((enter) => enter.append("path").attr("class", slots.path ?? "").attr("fill", "none").attr("opacity", 0), (update) => update, (exit) => exit.transition().duration(TRANSITION_MS3).attr("opacity", 0).remove()).transition().duration(TRANSITION_MS3).attr("opacity", 1).attr("d", linkPath);
11440
+ const node = nodesLayer.selectAll("g.dx-tidy-node").data(root.descendants(), (d) => d.data.id);
11441
+ const nodeEnter = node.enter().append("g").classed("dx-tidy-node", true).attr("transform", (d) => `translate(${offsetX + d.y},${(d.x + offsetY) * scaleY})`).attr("opacity", 0).style("cursor", (d) => d.data.children?.length ? "pointer" : "default").on("click", (_, d) => onNodeClick(d.data));
11442
+ nodeEnter.append("circle").attr("r", r);
11443
+ nodeEnter.append("text").attr("dy", "0.32em").attr("x", (d) => d.children ? -(r + 4) : r + 4).attr("text-anchor", (d) => d.children ? "end" : "start").text((d) => label(d.data));
11444
+ const nodeMerge = nodeEnter.merge(node);
11445
+ nodeMerge.transition().duration(TRANSITION_MS3).attr("opacity", 1).attr("transform", (d) => `translate(${offsetX + d.y},${(d.x + offsetY) * scaleY})`);
11446
+ nodeMerge.select("circle").attr("class", (d) => {
11447
+ const collapsedHere = isCollapsed(d.data, collapsed);
11448
+ const leaf = isLeaf(d.data);
11449
+ return [
11450
+ slots.node ?? "",
11451
+ collapsedHere ? "dx-collapsed" : leaf ? "dx-leaf" : "dx-branch"
11452
+ ].filter(Boolean).join(" ");
11453
+ }).attr("r", r);
11454
+ nodeMerge.select("text").attr("class", slots.text ?? "").attr("x", (d) => d.children ? -(r + 4) : r + 4).attr("text-anchor", (d) => d.children ? "end" : "start").text((d) => label(d.data));
11455
+ node.exit().each(function() {
11456
+ select3(this).interrupt();
11457
+ }).transition().duration(TRANSITION_MS3).attr("opacity", 0).remove();
11458
+ };
11459
+
11460
+ // src/components/Tree/Tree.tsx
11461
+ var Tree = ({ classNames, data, edges, variant = "tidy", label, slots, initialCollapsed, onNodeClick, onNodeHover }) => {
11462
+ return useMemo5(() => {
11463
+ switch (variant) {
11464
+ case "tidy":
11465
+ return /* @__PURE__ */ React8.createElement(TidyTree, {
11466
+ classNames,
11467
+ data,
11468
+ label,
11469
+ slots,
11470
+ initialCollapsed,
11471
+ onNodeClick
11472
+ });
11473
+ case "radial":
11474
+ return /* @__PURE__ */ React8.createElement(RadialTree, {
11475
+ classNames,
11476
+ data,
11477
+ label,
11478
+ slots,
11479
+ initialCollapsed,
11480
+ onNodeClick,
11481
+ onNodeHover
11482
+ });
11483
+ case "edge":
11484
+ return /* @__PURE__ */ React8.createElement(HierarchicalEdgeBundling, {
11485
+ classNames,
11486
+ data,
11487
+ edges: edges ?? [],
11488
+ label,
11489
+ slots,
11490
+ onNodeHover
11491
+ });
11492
+ }
11493
+ }, [
11494
+ variant,
11495
+ classNames,
11496
+ data,
11497
+ edges,
11498
+ label,
11499
+ slots,
11500
+ initialCollapsed,
11501
+ onNodeClick,
11502
+ onNodeHover
11503
+ ]);
11504
+ };
11505
+
11506
+ // src/components/Tree/space-graph-adapter.ts
11507
+ import { Obj as Obj2 } from "@dxos/echo";
11508
+ var ROOT_ID = "db:root";
11509
+ var SCHEMA_PREFIX = "schema:";
11510
+ var truncate = (id) => `${id.slice(0, 4)}\u2026${id.slice(-4)}`;
11511
+ var labelOf = (node) => node.data?.label ?? truncate(node.id);
11512
+ var spaceGraphToHierarchy = (model, { rootLabel = "Database", rootId = ROOT_ID } = {}) => {
11513
+ const graph = model.graph;
11514
+ const objectNodes = graph.nodes.filter((node) => node.type === "object");
11515
+ const byTypename = /* @__PURE__ */ new Map();
11516
+ for (const node of objectNodes) {
11517
+ const obj = node.data?.object;
11518
+ const typename = (obj && Obj2.getTypename(obj)) ?? "(untyped)";
11519
+ const bucket = byTypename.get(typename) ?? [];
11520
+ bucket.push(node);
11521
+ byTypename.set(typename, bucket);
11141
11522
  }
11523
+ const tree = {
11524
+ id: rootId,
11525
+ label: rootLabel,
11526
+ children: Array.from(byTypename.entries()).map(([typename, nodes]) => ({
11527
+ id: `${SCHEMA_PREFIX}${typename}`,
11528
+ label: shortTypename(typename),
11529
+ children: nodes.map((node) => ({
11530
+ id: node.id,
11531
+ label: labelOf(node),
11532
+ // Preserve the ECHO object on the leaf so layouts can fire hover/inspect callbacks with it.
11533
+ data: node.data?.object
11534
+ }))
11535
+ }))
11536
+ };
11537
+ const objectIds = new Set(objectNodes.map((n) => n.id));
11538
+ const edges = graph.edges.filter((edge) => objectIds.has(edge.source) && objectIds.has(edge.target)).filter((edge) => edge.source !== edge.target).map((edge) => ({
11539
+ source: edge.source,
11540
+ target: edge.target,
11541
+ kind: edge.type
11542
+ }));
11543
+ return {
11544
+ tree,
11545
+ edges
11546
+ };
11547
+ };
11548
+ var shortTypename = (typename) => {
11549
+ const last = typename.split(".").pop() ?? typename;
11550
+ return last.charAt(0).toUpperCase() + last.slice(1);
11142
11551
  };
11143
- var TidyTree_default = TidyTree;
11144
11552
 
11145
11553
  // src/components/Tree/types/tree.ts
11146
11554
  import * as Schema from "effect/Schema";
11147
- import { Key, Obj as Obj2, Ref, Type } from "@dxos/echo";
11555
+ import { Key, Obj as Obj3, Ref, Type } from "@dxos/echo";
11148
11556
  import { TestSchema } from "@dxos/echo/testing";
11149
11557
  import { invariant } from "@dxos/invariant";
11150
11558
  var TreeNodeType = Schema.Struct({
@@ -11168,87 +11576,43 @@ var TreeType = Schema.Struct({
11168
11576
  }));
11169
11577
 
11170
11578
  // src/components/Tree/types/types.ts
11171
- var mapGraphToTreeData = (model, maxDepth = 8) => {
11172
- let data;
11173
- return data;
11174
- };
11175
-
11176
- // src/components/Tree/Tree.tsx
11177
- var defaultTreeLayoutSlots = {
11178
- node: "fill-blue-600",
11179
- path: "fill-none stroke-blue-400 stroke-[0.5px]",
11180
- text: "stroke-[0.5px] stroke-neutral-700 text-xs"
11579
+ var treeTypeToTreeNode = (tree, rootId = tree.root, visited = /* @__PURE__ */ new Set()) => {
11580
+ const node = tree.nodes[rootId];
11581
+ if (!node) {
11582
+ return void 0;
11583
+ }
11584
+ if (visited.has(rootId)) {
11585
+ return {
11586
+ id: rootId,
11587
+ label: labelOf2(node),
11588
+ data: node.data
11589
+ };
11590
+ }
11591
+ visited.add(rootId);
11592
+ return {
11593
+ id: rootId,
11594
+ label: labelOf2(node),
11595
+ data: node.data,
11596
+ children: node.children.map((childId) => treeTypeToTreeNode(tree, childId, visited)).filter((c) => Boolean(c))
11597
+ };
11181
11598
  };
11182
- var renderers = /* @__PURE__ */ new Map([
11183
- [
11184
- "tidy",
11185
- TidyTree_default
11186
- ],
11187
- [
11188
- "radial",
11189
- RadialTree_default
11190
- ],
11191
- [
11192
- "edge",
11193
- HierarchicalEdgeBundling_default
11194
- ]
11195
- ]);
11196
- var Tree = ({ space, selected, variant = "tidy", onNodeClick }) => {
11197
- const registry = useContext(RegistryContext);
11198
- const [model] = useAsyncState(async () => space ? new SpaceGraphModel(registry).open(space.db) : void 0, [
11199
- space,
11200
- selected,
11201
- registry
11202
- ]);
11203
- const [tree3, setTree] = useState2();
11204
- useEffect5(() => {
11205
- return model?.subscribe(() => {
11206
- const tree4 = mapGraphToTreeData(model);
11207
- setTree(tree4);
11208
- }, true);
11209
- }, [
11210
- model
11211
- ]);
11212
- const context = useRef3(null);
11213
- useEffect5(() => {
11214
- if (context.current?.size) {
11215
- const { width, height } = context.current.size;
11216
- const size = Math.min(width, height);
11217
- const radius = size * 0.4;
11218
- const options = {
11219
- // TODO(burdon): Type.
11220
- label: (d) => d.label ?? d.id,
11221
- width,
11222
- height,
11223
- radius,
11224
- marginLeft: (width - radius * 2) / 2,
11225
- marginRight: (width - radius * 2) / 2,
11226
- marginTop: (height - radius * 2) / 2,
11227
- marginBottom: (height - radius * 2) / 2,
11228
- slots: defaultTreeLayoutSlots
11229
- };
11230
- if (tree3) {
11231
- const renderer = renderers.get(variant);
11232
- renderer?.(context.current.svg, tree3, options);
11233
- }
11234
- }
11235
- }, [
11236
- context.current,
11237
- tree3
11238
- ]);
11239
- return /* @__PURE__ */ React5.createElement("div", {
11240
- className: "grow",
11241
- onClick: () => onNodeClick?.()
11242
- }, /* @__PURE__ */ React5.createElement(SVG2.Root, {
11243
- ref: context
11244
- }));
11599
+ var labelOf2 = (node) => {
11600
+ return typeof node.data?.text === "string" ? node.data.text : void 0;
11245
11601
  };
11246
11602
  export {
11603
+ CanvasForceGraph,
11247
11604
  Chart,
11248
- D3ForceGraph,
11249
11605
  ForceGraph,
11250
11606
  Globe,
11607
+ HierarchicalEdgeBundling,
11608
+ RadialTree,
11609
+ TidyTree,
11251
11610
  Tree,
11252
- defaultTreeLayoutSlots
11611
+ buildHierarchy,
11612
+ defaultTreeLayoutSlots,
11613
+ isCollapsed,
11614
+ isLeaf,
11615
+ spaceGraphToHierarchy,
11616
+ treeTypeToTreeNode
11253
11617
  };
11254
11618
  //# sourceMappingURL=index.mjs.map