@eventcatalog/visualiser 4.0.0-beta.0 → 4.0.1

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.
package/dist/index.mjs CHANGED
@@ -19,8 +19,8 @@ import {
19
19
  useNodesState as useNodesState2,
20
20
  useEdgesState as useEdgesState2,
21
21
  useReactFlow as useReactFlow5,
22
+ useStoreApi,
22
23
  getNodesBounds,
23
- getViewportForBounds,
24
24
  MarkerType as MarkerType8
25
25
  } from "@xyflow/react";
26
26
  import {
@@ -9972,6 +9972,106 @@ var NodeContextMenu_default = memo39(function NodeContextMenu({
9972
9972
  ] });
9973
9973
  });
9974
9974
 
9975
+ // src/utils/export-image.ts
9976
+ import { getViewportForBounds } from "@xyflow/react";
9977
+ var MIN_IMAGE_WIDTH = 1024;
9978
+ var MIN_IMAGE_HEIGHT = 768;
9979
+ var EXPORT_PADDING = 40;
9980
+ function getExportImageDimensions(nodesBounds) {
9981
+ const paddedBounds = {
9982
+ x: nodesBounds.x - EXPORT_PADDING,
9983
+ y: nodesBounds.y - EXPORT_PADDING,
9984
+ width: nodesBounds.width + EXPORT_PADDING * 2,
9985
+ height: nodesBounds.height + EXPORT_PADDING * 2
9986
+ };
9987
+ const width = Math.max(MIN_IMAGE_WIDTH, Math.ceil(paddedBounds.width));
9988
+ const height = Math.max(MIN_IMAGE_HEIGHT, Math.ceil(paddedBounds.height));
9989
+ const viewport = getViewportForBounds(paddedBounds, width, height, 0.5, 2, 0);
9990
+ return { width, height, viewport };
9991
+ }
9992
+ function extractEcVarNames(cssText) {
9993
+ return Array.from(new Set(cssText.match(/--ec-[a-z0-9-]+/g) ?? []));
9994
+ }
9995
+ function buildExportCss(themeVars) {
9996
+ const varDeclarations = Object.entries(themeVars).map(([name, value]) => `${name}: ${value};`).join(" ");
9997
+ return [
9998
+ // Applied to every element so the variables reach the cloned SVG subtree
9999
+ `* { ${varDeclarations} }`,
10000
+ // Mirrors .react-flow__edge-path in styles.css, which is lost in the clone
10001
+ `.react-flow__edge-path { stroke: var(--ec-edge-stroke, #6b7280); stroke-width: 1.5; fill: none; }`
10002
+ ].join("\n");
10003
+ }
10004
+ function collectThemeCssVars(scope) {
10005
+ const names = /* @__PURE__ */ new Set();
10006
+ for (const sheet of Array.from(document.styleSheets)) {
10007
+ let rules;
10008
+ try {
10009
+ rules = sheet.cssRules;
10010
+ } catch {
10011
+ continue;
10012
+ }
10013
+ for (const rule of Array.from(rules)) {
10014
+ for (const name of extractEcVarNames(rule.cssText)) {
10015
+ names.add(name);
10016
+ }
10017
+ }
10018
+ }
10019
+ const computed = window.getComputedStyle(scope);
10020
+ const vars = {};
10021
+ for (const name of names) {
10022
+ const value = computed.getPropertyValue(name).trim();
10023
+ if (value) vars[name] = value;
10024
+ }
10025
+ return vars;
10026
+ }
10027
+ var SVG_EXPORT_STYLE_PROPERTIES = [
10028
+ "fill",
10029
+ "fill-opacity",
10030
+ "stroke",
10031
+ "stroke-width",
10032
+ "stroke-dasharray",
10033
+ "stroke-linecap",
10034
+ "stroke-linejoin",
10035
+ "opacity",
10036
+ "font-family",
10037
+ "font-size",
10038
+ "font-weight",
10039
+ "letter-spacing",
10040
+ "text-anchor",
10041
+ "dominant-baseline"
10042
+ ];
10043
+ function inlineSvgComputedStyles(viewport) {
10044
+ const restores = [];
10045
+ const svgElements = viewport.querySelectorAll("svg, svg *");
10046
+ svgElements.forEach((element) => {
10047
+ if (!(element instanceof SVGElement)) return;
10048
+ const computed = window.getComputedStyle(element);
10049
+ const originalStyle = element.getAttribute("style");
10050
+ for (const property of SVG_EXPORT_STYLE_PROPERTIES) {
10051
+ const value = computed.getPropertyValue(property);
10052
+ if (value) element.style.setProperty(property, value);
10053
+ }
10054
+ restores.push(() => {
10055
+ if (originalStyle === null) {
10056
+ element.removeAttribute("style");
10057
+ } else {
10058
+ element.setAttribute("style", originalStyle);
10059
+ }
10060
+ });
10061
+ });
10062
+ return () => restores.forEach((restore) => restore());
10063
+ }
10064
+ function injectExportStyles(viewport) {
10065
+ const styleElement = document.createElement("style");
10066
+ styleElement.textContent = buildExportCss(collectThemeCssVars(viewport));
10067
+ viewport.appendChild(styleElement);
10068
+ const restoreSvgStyles = inlineSvgComputedStyles(viewport);
10069
+ return () => {
10070
+ restoreSvgStyles();
10071
+ styleElement.remove();
10072
+ };
10073
+ }
10074
+
9975
10075
  // src/utils/layout.ts
9976
10076
  import dagre2 from "dagre";
9977
10077
  import { MarkerType as MarkerType7 } from "@xyflow/react";
@@ -11593,6 +11693,7 @@ var NodeGraphBuilder = ({
11593
11693
  const [nodes, setNodes, onNodesChange] = useNodesState2(initialNodes);
11594
11694
  const [edges, setEdges, onEdgesChange] = useEdgesState2(initialEdges);
11595
11695
  const { fitView, getNodes, getIntersectingNodes, getZoom, setCenter } = useReactFlow5();
11696
+ const storeApi = useStoreApi();
11596
11697
  useEffect9(() => {
11597
11698
  setNodes(initialNodes);
11598
11699
  setEdges(initialEdges);
@@ -12639,37 +12740,34 @@ var NodeGraphBuilder = ({
12639
12740
  };
12640
12741
  }, [fitView]);
12641
12742
  const handleExportVisual = useCallback12(() => {
12642
- const imageWidth = 1024;
12643
- const imageHeight = 768;
12644
- const nodesBounds = getNodesBounds(getNodes());
12645
- const width = imageWidth > nodesBounds.width ? imageWidth : nodesBounds.width;
12646
- const height = imageHeight > nodesBounds.height ? imageHeight : nodesBounds.height;
12647
- const viewport = getViewportForBounds(
12648
- nodesBounds,
12649
- width,
12650
- height,
12651
- 0.5,
12652
- 2,
12653
- 0
12654
- );
12743
+ const nodesBounds = getNodesBounds(getNodes(), {
12744
+ nodeLookup: storeApi.getState().nodeLookup
12745
+ });
12746
+ const { width, height, viewport } = getExportImageDimensions(nodesBounds);
12655
12747
  const controls = document.querySelector(
12656
12748
  ".react-flow__controls"
12657
12749
  );
12658
12750
  if (controls) controls.style.display = "none";
12659
- toPng(document.querySelector(".react-flow__viewport"), {
12751
+ const viewportElement = document.querySelector(
12752
+ ".react-flow__viewport"
12753
+ );
12754
+ const removeExportStyles = injectExportStyles(viewportElement);
12755
+ toPng(viewportElement, {
12660
12756
  backgroundColor: "#f1f1f1",
12661
12757
  width,
12662
12758
  height,
12663
12759
  style: {
12664
- width: width.toString(),
12665
- height: height.toString(),
12760
+ width: `${width}px`,
12761
+ height: `${height}px`,
12666
12762
  transform: `translate(${viewport.x}px, ${viewport.y}px) scale(${viewport.zoom})`
12667
12763
  }
12668
12764
  }).then((dataUrl) => {
12669
12765
  downloadImage(dataUrl, title);
12766
+ }).finally(() => {
12767
+ removeExportStyles();
12670
12768
  if (controls) controls.style.display = "block";
12671
12769
  });
12672
- }, [getNodes, downloadImage, title]);
12770
+ }, [getNodes, storeApi, downloadImage, title]);
12673
12771
  const handleLegendClick = useCallback12(
12674
12772
  (collectionType, groupId) => {
12675
12773
  const isLegendTarget = (node) => {