@graph-artifact/core 0.1.11 → 0.1.13
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.
|
@@ -11,8 +11,22 @@ interface GraphCanvasProps {
|
|
|
11
11
|
hideAttribution?: boolean;
|
|
12
12
|
/** Optional branding overlays (included in exports). */
|
|
13
13
|
branding?: {
|
|
14
|
+
/**
|
|
15
|
+
* Where branding renders:
|
|
16
|
+
* - `canvas`: on-screen in the ReactFlow viewer
|
|
17
|
+
* - `download`: only during PNG export
|
|
18
|
+
*
|
|
19
|
+
* Defaults: `{ canvas: false, download: true }`
|
|
20
|
+
*/
|
|
21
|
+
visibility?: {
|
|
22
|
+
canvas?: boolean;
|
|
23
|
+
download?: boolean;
|
|
24
|
+
};
|
|
14
25
|
logo?: {
|
|
15
|
-
|
|
26
|
+
/** Image URL to render (e.g. PNG). Prefer `node` for inline SVG via SVGR. */
|
|
27
|
+
src?: string;
|
|
28
|
+
/** Inline logo node (e.g. an SVGR component). */
|
|
29
|
+
node?: ReactNode;
|
|
16
30
|
alt?: string;
|
|
17
31
|
position?: CornerPosition;
|
|
18
32
|
width?: number;
|
|
@@ -34,6 +48,19 @@ interface GraphCanvasProps {
|
|
|
34
48
|
pixelRatio?: number;
|
|
35
49
|
/** Background color used for export. Defaults to `canvas.containerBackground`. */
|
|
36
50
|
backgroundColor?: string;
|
|
51
|
+
/**
|
|
52
|
+
* Whether the exported PNG should include the dotted/grid background.
|
|
53
|
+
* Defaults to false (clean export).
|
|
54
|
+
*/
|
|
55
|
+
includeBackground?: boolean;
|
|
56
|
+
/**
|
|
57
|
+
* Whether to embed webfonts into the exported image.
|
|
58
|
+
* Defaults to false because many apps load fonts from cross-origin
|
|
59
|
+
* stylesheets (e.g. Google Fonts), which causes DOMExceptions during export.
|
|
60
|
+
*/
|
|
61
|
+
embedFonts?: boolean;
|
|
62
|
+
/** Optional error callback for export failures. */
|
|
63
|
+
onError?: (error: Error) => void;
|
|
37
64
|
};
|
|
38
65
|
/**
|
|
39
66
|
* If true, reruns dagre after React Flow measures node DOM sizes so
|
|
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { useMemo, useCallback, useState, useEffect, useRef } from 'react';
|
|
3
3
|
import { ReactFlow, Background, Controls, MiniMap, ReactFlowProvider, useReactFlow, useNodesInitialized, useStore, useStoreApi, Panel, } from '@xyflow/react';
|
|
4
4
|
import '@xyflow/react/dist/style.css';
|
|
5
|
-
import {
|
|
5
|
+
import { toBlob } from 'html-to-image';
|
|
6
6
|
import { layoutNodes, buildLayoutEdges } from '../layout/index.js';
|
|
7
7
|
import { getConfig, getNodeTypes } from '../config.js';
|
|
8
8
|
import { useTheme } from '../ThemeContext.js';
|
|
@@ -273,6 +273,16 @@ function GraphCanvasInner({ parsed, metadata, onNodeClick, canvasOverrides, hide
|
|
|
273
273
|
const miniMapNodeColor = useCallback(() => theme.color.orange1, [theme.color.orange1]);
|
|
274
274
|
const miniMapMaskColor = useMemo(() => `${theme.color.gray1}cc`, [theme.color.gray1]);
|
|
275
275
|
const exportRef = useRef(null);
|
|
276
|
+
const [isExporting, setIsExporting] = useState(false);
|
|
277
|
+
const brandingVisibility = useMemo(() => {
|
|
278
|
+
const v = branding?.visibility ?? {};
|
|
279
|
+
return {
|
|
280
|
+
canvas: v.canvas ?? false,
|
|
281
|
+
download: v.download ?? true,
|
|
282
|
+
};
|
|
283
|
+
}, [branding?.visibility]);
|
|
284
|
+
const showBrandingNow = isExporting ? brandingVisibility.download : brandingVisibility.canvas;
|
|
285
|
+
const showBackgroundNow = isExporting ? Boolean(download?.includeBackground) : Boolean(canvas.showBackground);
|
|
276
286
|
const onDownload = useCallback(async () => {
|
|
277
287
|
if (!exportRef.current)
|
|
278
288
|
return;
|
|
@@ -281,26 +291,57 @@ function GraphCanvasInner({ parsed, metadata, onNodeClick, canvasOverrides, hide
|
|
|
281
291
|
.replace(/[^\w\- ]+/g, '')
|
|
282
292
|
.replace(/\s+/g, '-')
|
|
283
293
|
.slice(0, 80) || 'diagram';
|
|
294
|
+
setIsExporting(true);
|
|
284
295
|
try {
|
|
285
|
-
|
|
296
|
+
// Wait for React to commit export-only visuals (branding/background toggles)
|
|
297
|
+
// before html-to-image snapshots the DOM.
|
|
298
|
+
await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(() => resolve())));
|
|
299
|
+
// Prefer blob + object URL over a data URL. In some browsers, downloads
|
|
300
|
+
// triggered after an async boundary can be blocked when using data URLs.
|
|
301
|
+
const blob = await toBlob(exportRef.current, {
|
|
286
302
|
cacheBust: true,
|
|
287
303
|
pixelRatio: Math.max(1, download?.pixelRatio ?? 2),
|
|
288
304
|
backgroundColor: download?.backgroundColor ?? canvas.containerBackground,
|
|
305
|
+
// Avoid CORS errors when the host app uses cross-origin stylesheets for fonts.
|
|
306
|
+
// If the host wants font embedding, they can explicitly enable it.
|
|
307
|
+
skipFonts: !(download?.embedFonts ?? false),
|
|
289
308
|
filter: (node) => {
|
|
290
309
|
if (!(node instanceof Element))
|
|
291
310
|
return true;
|
|
292
311
|
return !node.classList.contains('ga-no-export');
|
|
293
312
|
},
|
|
294
313
|
});
|
|
314
|
+
if (!blob)
|
|
315
|
+
throw new Error('Export failed: no image data produced.');
|
|
316
|
+
const url = URL.createObjectURL(blob);
|
|
295
317
|
const a = document.createElement('a');
|
|
296
|
-
a.href =
|
|
318
|
+
a.href = url;
|
|
297
319
|
a.download = `${fileName}.png`;
|
|
320
|
+
a.rel = 'noopener';
|
|
321
|
+
document.body.appendChild(a);
|
|
298
322
|
a.click();
|
|
323
|
+
a.remove();
|
|
324
|
+
URL.revokeObjectURL(url);
|
|
299
325
|
}
|
|
300
|
-
catch {
|
|
301
|
-
|
|
326
|
+
catch (err) {
|
|
327
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
328
|
+
// eslint-disable-next-line no-console
|
|
329
|
+
console.error('[graph-artifact] Download export failed:', error);
|
|
330
|
+
download?.onError?.(error);
|
|
331
|
+
}
|
|
332
|
+
finally {
|
|
333
|
+
setIsExporting(false);
|
|
302
334
|
}
|
|
303
|
-
}, [
|
|
335
|
+
}, [
|
|
336
|
+
canvas.containerBackground,
|
|
337
|
+
canvas.showBackground,
|
|
338
|
+
download?.backgroundColor,
|
|
339
|
+
download?.fileName,
|
|
340
|
+
download?.pixelRatio,
|
|
341
|
+
download?.includeBackground,
|
|
342
|
+
download?.onError,
|
|
343
|
+
download?.embedFonts,
|
|
344
|
+
]);
|
|
304
345
|
if (layoutError) {
|
|
305
346
|
return (_jsx("div", { style: {
|
|
306
347
|
width: '100%',
|
|
@@ -349,7 +390,9 @@ function GraphCanvasInner({ parsed, metadata, onNodeClick, canvasOverrides, hide
|
|
|
349
390
|
background: ${theme.mode === 'dark' ? theme.color.darkBg2 : theme.color.white};
|
|
350
391
|
border: 1px solid ${theme.color.gray2};
|
|
351
392
|
}
|
|
352
|
-
|
|
393
|
+
/* Ensure inline SVG logos scale to the requested width. */
|
|
394
|
+
.ga-logo svg { width: 100%; height: auto; display: block; }
|
|
395
|
+
` }), showBrandingNow && (branding?.logo?.src || branding?.logo?.node) && (_jsx("div", { style: {
|
|
353
396
|
position: 'absolute',
|
|
354
397
|
zIndex: 5,
|
|
355
398
|
pointerEvents: 'none',
|
|
@@ -358,7 +401,11 @@ function GraphCanvasInner({ parsed, metadata, onNodeClick, canvasOverrides, hide
|
|
|
358
401
|
...(branding.logo.position === 'bottom-right' ? { bottom: 12, right: 12 } : {}),
|
|
359
402
|
...(branding.logo.position === 'top-left' || !branding.logo.position ? { top: 12, left: 12 } : {}),
|
|
360
403
|
opacity: branding.logo.opacity ?? 1,
|
|
361
|
-
}, children:
|
|
404
|
+
}, children: branding.logo.node ? (_jsx("div", { className: "ga-logo", style: {
|
|
405
|
+
width: branding.logo.width ?? 120,
|
|
406
|
+
height: branding.logo.height,
|
|
407
|
+
display: 'block',
|
|
408
|
+
}, children: branding.logo.node })) : (_jsx("img", { src: branding.logo.src, alt: branding.logo.alt ?? 'logo', width: branding.logo.width ?? 120, height: branding.logo.height, style: { display: 'block' } })) })), showBrandingNow && branding?.watermark?.text && (_jsxs("div", { style: {
|
|
362
409
|
position: 'absolute',
|
|
363
410
|
left: '50%',
|
|
364
411
|
bottom: 14,
|
|
@@ -374,9 +421,9 @@ function GraphCanvasInner({ parsed, metadata, onNodeClick, canvasOverrides, hide
|
|
|
374
421
|
opacity: branding.watermark.opacity ?? 0.9,
|
|
375
422
|
userSelect: 'none',
|
|
376
423
|
whiteSpace: 'nowrap',
|
|
377
|
-
}, children: [branding.watermark.icon ?? null, _jsx("span", { children: branding.watermark.text })] })), _jsxs(ReactFlow, { nodes: nodes, edges: edges, nodeTypes: resolvedNodeTypes, edgeTypes: edgeTypes, onNodeClick: handleNodeClick, fitView: canvas.fitView, minZoom: canvas.minZoom, maxZoom: canvas.maxZoom, defaultEdgeOptions: defaultEdgeOptions, zIndexMode: "manual", colorMode: theme.mode, proOptions: hideAttribution ? { hideAttribution: true } : undefined, children: [
|
|
378
|
-
|
|
379
|
-
|
|
424
|
+
}, children: [branding.watermark.icon ?? null, _jsx("span", { children: branding.watermark.text })] })), _jsxs(ReactFlow, { nodes: nodes, edges: edges, nodeTypes: resolvedNodeTypes, edgeTypes: edgeTypes, onNodeClick: handleNodeClick, fitView: canvas.fitView, minZoom: canvas.minZoom, maxZoom: canvas.maxZoom, defaultEdgeOptions: defaultEdgeOptions, zIndexMode: "manual", colorMode: theme.mode, proOptions: hideAttribution ? { hideAttribution: true } : undefined, children: [showBackgroundNow && (_jsx(Background, { color: canvas.gridColor === 'auto' ? theme.color.gray2 : canvas.gridColor, gap: canvas.backgroundGap, size: canvas.backgroundSize })), canvas.showControls && (_jsx("div", { className: "ga-no-export", children: _jsx(Controls, { position: canvas.controlsPosition,
|
|
425
|
+
// Remove the "lock" (interactive) button; we want pan/zoom always available.
|
|
426
|
+
showInteractive: false }) })), ((download?.enabled ?? canvas.showControls) && download?.enabled !== false) && (_jsx(Panel, { position: cornerToPanelPosition(download?.position ?? 'top-right'), className: "ga-no-export", children: _jsx("button", { type: "button", onClick: onDownload, "aria-label": "Download diagram", title: "Download", style: {
|
|
380
427
|
display: 'inline-flex',
|
|
381
428
|
alignItems: 'center',
|
|
382
429
|
justifyContent: 'center',
|
|
@@ -388,7 +435,7 @@ function GraphCanvasInner({ parsed, metadata, onNodeClick, canvasOverrides, hide
|
|
|
388
435
|
border: `1px solid ${theme.color.gray2}`,
|
|
389
436
|
boxShadow: theme.shadow.card,
|
|
390
437
|
cursor: 'pointer',
|
|
391
|
-
}, children: download?.icon ?? defaultDownloadIcon(theme) }) })), canvas.showMiniMap && (_jsx(MiniMap, { nodeColor: miniMapNodeColor, maskColor: miniMapMaskColor }))] })] }));
|
|
438
|
+
}, children: download?.icon ?? defaultDownloadIcon(theme) }) })), canvas.showMiniMap && (_jsx("div", { className: "ga-no-export", children: _jsx(MiniMap, { nodeColor: miniMapNodeColor, maskColor: miniMapMaskColor }) }))] })] }));
|
|
392
439
|
}
|
|
393
440
|
export function GraphCanvas(props) {
|
|
394
441
|
return (_jsx(ReactFlowProvider, { children: _jsx(GraphCanvasInner, { ...props }) }));
|
package/package.json
CHANGED