@principal-ai/principal-view-react 0.13.10 → 0.13.11

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.
@@ -0,0 +1,75 @@
1
+ /// <reference types="react" />
2
+ import type { ExtendedCanvas } from '@principal-ai/principal-view-core';
3
+ import type { GraphRendererProps, GraphRendererHandle } from './GraphRenderer';
4
+ /**
5
+ * A canvas placement with its position in the combined coordinate space
6
+ */
7
+ export interface CanvasPlacement {
8
+ /** Unique identifier for this canvas */
9
+ canvasId: string;
10
+ /** The canvas to include */
11
+ canvas: ExtendedCanvas;
12
+ /** Position offset - all nodes in this canvas will be translated by this amount */
13
+ position: {
14
+ x: number;
15
+ y: number;
16
+ };
17
+ /** Optional label for the canvas (for future visual grouping) */
18
+ label?: string;
19
+ }
20
+ /**
21
+ * Layout configuration for multiple canvases on a single graph
22
+ */
23
+ export interface MultiCanvasLayout {
24
+ /** Array of canvas placements with positions */
25
+ placements: CanvasPlacement[];
26
+ }
27
+ /**
28
+ * Props for the MultiCanvasRenderer component
29
+ */
30
+ export interface MultiCanvasRendererProps extends Omit<GraphRendererProps, 'canvas' | 'editable'> {
31
+ /** Layout configuration containing canvas placements */
32
+ layout: MultiCanvasLayout;
33
+ /** Callback when layout changes (canvas positions updated) */
34
+ onLayoutChange?: (layout: MultiCanvasLayout) => void;
35
+ /** Whether to show group borders around each canvas (default: true) */
36
+ showGroups?: boolean;
37
+ }
38
+ /**
39
+ * Merge multiple canvases into a single canvas with position offsets applied.
40
+ * Each canvas gets a group node border with its name as a label.
41
+ * Node IDs are prefixed with canvasId to avoid collisions (format: "canvasId:nodeId").
42
+ * Original IDs can be recovered using parseNodeId().
43
+ */
44
+ export declare function mergeCanvases(placements: CanvasPlacement[], options?: {
45
+ showGroups?: boolean;
46
+ }): ExtendedCanvas;
47
+ /**
48
+ * Extract the original canvas ID and node ID from a merged node ID
49
+ */
50
+ export declare function parseNodeId(mergedNodeId: string): {
51
+ canvasId: string;
52
+ nodeId: string;
53
+ } | null;
54
+ /**
55
+ * Renders multiple canvases merged into a single graph view.
56
+ *
57
+ * Each canvas is positioned at its specified offset, and all nodes/edges
58
+ * are combined into a unified viewport. Node IDs are prefixed with their
59
+ * source canvas ID to avoid collisions (format: "canvasId:nodeId").
60
+ *
61
+ * @example
62
+ * ```tsx
63
+ * <MultiCanvasRenderer
64
+ * layout={{
65
+ * placements: [
66
+ * { canvasId: 'auth', canvas: authCanvas, position: { x: 0, y: 0 } },
67
+ * { canvasId: 'data', canvas: dataCanvas, position: { x: 600, y: 0 } },
68
+ * ],
69
+ * }}
70
+ * showControls
71
+ * />
72
+ * ```
73
+ */
74
+ export declare const MultiCanvasRenderer: import("react").ForwardRefExoticComponent<MultiCanvasRendererProps & import("react").RefAttributes<GraphRendererHandle>>;
75
+ //# sourceMappingURL=MultiCanvasRenderer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MultiCanvasRenderer.d.ts","sourceRoot":"","sources":["../../src/components/MultiCanvasRenderer.tsx"],"names":[],"mappings":";AACA,OAAO,KAAK,EAAE,cAAc,EAA0C,MAAM,mCAAmC,CAAC;AAEhH,OAAO,KAAK,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AAM/E;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,wCAAwC;IACxC,QAAQ,EAAE,MAAM,CAAC;IACjB,4BAA4B;IAC5B,MAAM,EAAE,cAAc,CAAC;IACvB,mFAAmF;IACnF,QAAQ,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACnC,iEAAiE;IACjE,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,gDAAgD;IAChD,UAAU,EAAE,eAAe,EAAE,CAAC;CAC/B;AAED;;GAEG;AACH,MAAM,WAAW,wBACf,SAAQ,IAAI,CAAC,kBAAkB,EAAE,QAAQ,GAAG,UAAU,CAAC;IACvD,wDAAwD;IACxD,MAAM,EAAE,iBAAiB,CAAC;IAC1B,8DAA8D;IAC9D,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,iBAAiB,KAAK,IAAI,CAAC;IACrD,uEAAuE;IACvE,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAkDD;;;;;GAKG;AACH,wBAAgB,aAAa,CAC3B,UAAU,EAAE,eAAe,EAAE,EAC7B,OAAO,GAAE;IAAE,UAAU,CAAC,EAAE,OAAO,CAAA;CAAO,GACrC,cAAc,CA8DhB;AAED;;GAEG;AACH,wBAAgB,WAAW,CACzB,YAAY,EAAE,MAAM,GACnB;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAO7C;AAMD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,mBAAmB,0HAqB9B,CAAC"}
@@ -0,0 +1,142 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useMemo, forwardRef } from 'react';
3
+ import { GraphRenderer } from './GraphRenderer';
4
+ // ============================================================================
5
+ // Canvas Merging Utilities
6
+ // ============================================================================
7
+ const DEFAULT_NODE_WIDTH = 200;
8
+ const DEFAULT_NODE_HEIGHT = 100;
9
+ const GROUP_PADDING = 40;
10
+ /**
11
+ * Create a prefixed node ID to avoid collisions between canvases
12
+ */
13
+ function prefixNodeId(canvasId, nodeId) {
14
+ return `${canvasId}:${nodeId}`;
15
+ }
16
+ /**
17
+ * Calculate the bounding box of nodes in a canvas (relative to canvas origin)
18
+ */
19
+ function calculateCanvasBounds(canvas) {
20
+ if (!canvas.nodes || canvas.nodes.length === 0) {
21
+ return { minX: 0, minY: 0, maxX: DEFAULT_NODE_WIDTH, maxY: DEFAULT_NODE_HEIGHT };
22
+ }
23
+ let minX = Infinity;
24
+ let minY = Infinity;
25
+ let maxX = -Infinity;
26
+ let maxY = -Infinity;
27
+ for (const node of canvas.nodes) {
28
+ const x = node.x ?? 0;
29
+ const y = node.y ?? 0;
30
+ const width = node.width ?? DEFAULT_NODE_WIDTH;
31
+ const height = node.height ?? DEFAULT_NODE_HEIGHT;
32
+ minX = Math.min(minX, x);
33
+ minY = Math.min(minY, y);
34
+ maxX = Math.max(maxX, x + width);
35
+ maxY = Math.max(maxY, y + height);
36
+ }
37
+ return { minX, minY, maxX, maxY };
38
+ }
39
+ /**
40
+ * Merge multiple canvases into a single canvas with position offsets applied.
41
+ * Each canvas gets a group node border with its name as a label.
42
+ * Node IDs are prefixed with canvasId to avoid collisions (format: "canvasId:nodeId").
43
+ * Original IDs can be recovered using parseNodeId().
44
+ */
45
+ export function mergeCanvases(placements, options = {}) {
46
+ const { showGroups = true } = options;
47
+ const groupNodes = [];
48
+ const mergedNodes = [];
49
+ const mergedEdges = [];
50
+ for (const placement of placements) {
51
+ const { canvasId, canvas, position, label } = placement;
52
+ // Calculate bounds for this canvas to create a group node
53
+ if (showGroups && canvas.nodes && canvas.nodes.length > 0) {
54
+ const bounds = calculateCanvasBounds(canvas);
55
+ const groupNode = {
56
+ id: `${canvasId}:__group__`,
57
+ type: 'group',
58
+ label: label ?? canvas.pv?.name ?? canvasId,
59
+ x: position.x + bounds.minX - GROUP_PADDING,
60
+ y: position.y + bounds.minY - GROUP_PADDING,
61
+ width: bounds.maxX - bounds.minX + GROUP_PADDING * 2,
62
+ height: bounds.maxY - bounds.minY + GROUP_PADDING * 2,
63
+ };
64
+ groupNodes.push(groupNode);
65
+ }
66
+ // Process nodes - apply position offset and prefix IDs
67
+ if (canvas.nodes) {
68
+ for (const node of canvas.nodes) {
69
+ const mergedNode = {
70
+ ...node,
71
+ id: prefixNodeId(canvasId, node.id),
72
+ x: (node.x ?? 0) + position.x,
73
+ y: (node.y ?? 0) + position.y,
74
+ };
75
+ mergedNodes.push(mergedNode);
76
+ }
77
+ }
78
+ // Process edges - update node references with prefixed IDs
79
+ if (canvas.edges) {
80
+ for (const edge of canvas.edges) {
81
+ const mergedEdge = {
82
+ ...edge,
83
+ id: prefixNodeId(canvasId, edge.id),
84
+ fromNode: prefixNodeId(canvasId, edge.fromNode),
85
+ toNode: prefixNodeId(canvasId, edge.toNode),
86
+ };
87
+ mergedEdges.push(mergedEdge);
88
+ }
89
+ }
90
+ }
91
+ // Create merged canvas - group nodes first so they render behind
92
+ const mergedCanvas = {
93
+ nodes: [...groupNodes, ...mergedNodes],
94
+ edges: mergedEdges,
95
+ pv: {
96
+ version: '1.0.0',
97
+ name: 'Multi-Canvas View',
98
+ },
99
+ };
100
+ return mergedCanvas;
101
+ }
102
+ /**
103
+ * Extract the original canvas ID and node ID from a merged node ID
104
+ */
105
+ export function parseNodeId(mergedNodeId) {
106
+ const colonIndex = mergedNodeId.indexOf(':');
107
+ if (colonIndex === -1)
108
+ return null;
109
+ return {
110
+ canvasId: mergedNodeId.substring(0, colonIndex),
111
+ nodeId: mergedNodeId.substring(colonIndex + 1),
112
+ };
113
+ }
114
+ // ============================================================================
115
+ // MultiCanvasRenderer Component
116
+ // ============================================================================
117
+ /**
118
+ * Renders multiple canvases merged into a single graph view.
119
+ *
120
+ * Each canvas is positioned at its specified offset, and all nodes/edges
121
+ * are combined into a unified viewport. Node IDs are prefixed with their
122
+ * source canvas ID to avoid collisions (format: "canvasId:nodeId").
123
+ *
124
+ * @example
125
+ * ```tsx
126
+ * <MultiCanvasRenderer
127
+ * layout={{
128
+ * placements: [
129
+ * { canvasId: 'auth', canvas: authCanvas, position: { x: 0, y: 0 } },
130
+ * { canvasId: 'data', canvas: dataCanvas, position: { x: 600, y: 0 } },
131
+ * ],
132
+ * }}
133
+ * showControls
134
+ * />
135
+ * ```
136
+ */
137
+ export const MultiCanvasRenderer = forwardRef(function MultiCanvasRenderer({ layout, onLayoutChange: _onLayoutChange, showGroups = true, ...graphRendererProps }, ref) {
138
+ // Merge all canvases into a single canvas
139
+ const mergedCanvas = useMemo(() => mergeCanvases(layout.placements, { showGroups }), [layout.placements, showGroups]);
140
+ return (_jsx(GraphRenderer, { ref: ref, canvas: mergedCanvas, editable: false, ...graphRendererProps }));
141
+ });
142
+ //# sourceMappingURL=MultiCanvasRenderer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MultiCanvasRenderer.js","sourceRoot":"","sources":["../../src/components/MultiCanvasRenderer.tsx"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -8,6 +8,8 @@
8
8
  export type { GraphConfiguration, GraphEvent, GraphMetrics, Violation, Warning, ValidationResult, EventStream, NodeTypeDefinition, EdgeTypeDefinition, ConnectionRule, NodeState, EdgeState, ConfigurationFile, ConfigurationLoadResult, ComponentLibrary, LibraryNodeComponent, LibraryEdgeComponent, } from '@principal-ai/principal-view-core';
9
9
  export { GraphRenderer } from './components/GraphRenderer';
10
10
  export type { GraphRendererProps, GraphRendererHandle, NodePositionChange, PendingChanges, } from './components/GraphRenderer';
11
+ export { MultiCanvasRenderer, mergeCanvases, parseNodeId } from './components/MultiCanvasRenderer';
12
+ export type { MultiCanvasRendererProps, MultiCanvasLayout, CanvasPlacement, } from './components/MultiCanvasRenderer';
11
13
  export { ConfigurationSelector } from './components/ConfigurationSelector';
12
14
  export type { ConfigurationSelectorProps } from './components/ConfigurationSelector';
13
15
  export { GenericNode } from './nodes/GenericNode';
@@ -25,4 +27,6 @@ export type { EdgeStateWithHandles } from './utils/graphConverter';
25
27
  export { Icon, resolveIcon } from './utils/iconResolver';
26
28
  export type { IconProps } from './utils/iconResolver';
27
29
  export { swapGraphOrientation, swapNodePositions, swapEdgeSides, } from './utils/orientationUtils';
30
+ export { getCanvasBounds, getCanvasDisplaySize } from './utils/canvasBounds';
31
+ export type { CanvasBounds } from './utils/canvasBounds';
28
32
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,YAAY,EACV,kBAAkB,EAClB,UAAU,EACV,YAAY,EACZ,SAAS,EACT,OAAO,EACP,gBAAgB,EAChB,WAAW,EACX,kBAAkB,EAClB,kBAAkB,EAClB,cAAc,EACd,SAAS,EACT,SAAS,EACT,iBAAiB,EACjB,uBAAuB,EAEvB,gBAAgB,EAChB,oBAAoB,EACpB,oBAAoB,GACrB,MAAM,mCAAmC,CAAC;AAG3C,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAC3D,YAAY,EACV,kBAAkB,EAClB,mBAAmB,EACnB,kBAAkB,EAClB,cAAc,GACf,MAAM,4BAA4B,CAAC;AAEpC,OAAO,EAAE,qBAAqB,EAAE,MAAM,oCAAoC,CAAC;AAC3E,YAAY,EAAE,0BAA0B,EAAE,MAAM,oCAAoC,CAAC;AAGrF,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAClD,YAAY,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAE5D,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAChD,YAAY,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAEzD,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAClD,YAAY,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAE5D,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAChD,YAAY,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAGzD,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AACvD,YAAY,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AAG3E,OAAO,EACL,oBAAoB,EACpB,oBAAoB,GACrB,MAAM,wBAAwB,CAAC;AAChC,YAAY,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AACnE,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACzD,YAAY,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EACL,oBAAoB,EACpB,iBAAiB,EACjB,aAAa,GACd,MAAM,0BAA0B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,YAAY,EACV,kBAAkB,EAClB,UAAU,EACV,YAAY,EACZ,SAAS,EACT,OAAO,EACP,gBAAgB,EAChB,WAAW,EACX,kBAAkB,EAClB,kBAAkB,EAClB,cAAc,EACd,SAAS,EACT,SAAS,EACT,iBAAiB,EACjB,uBAAuB,EAEvB,gBAAgB,EAChB,oBAAoB,EACpB,oBAAoB,GACrB,MAAM,mCAAmC,CAAC;AAG3C,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAC3D,YAAY,EACV,kBAAkB,EAClB,mBAAmB,EACnB,kBAAkB,EAClB,cAAc,GACf,MAAM,4BAA4B,CAAC;AAEpC,OAAO,EAAE,mBAAmB,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAC;AACnG,YAAY,EACV,wBAAwB,EACxB,iBAAiB,EACjB,eAAe,GAChB,MAAM,kCAAkC,CAAC;AAE1C,OAAO,EAAE,qBAAqB,EAAE,MAAM,oCAAoC,CAAC;AAC3E,YAAY,EAAE,0BAA0B,EAAE,MAAM,oCAAoC,CAAC;AAGrF,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAClD,YAAY,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAE5D,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAChD,YAAY,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAEzD,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAClD,YAAY,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAE5D,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAChD,YAAY,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAGzD,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AACvD,YAAY,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AAG3E,OAAO,EACL,oBAAoB,EACpB,oBAAoB,GACrB,MAAM,wBAAwB,CAAC;AAChC,YAAY,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AACnE,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACzD,YAAY,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EACL,oBAAoB,EACpB,iBAAiB,EACjB,aAAa,GACd,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,eAAe,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAC7E,YAAY,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC"}
package/dist/index.js CHANGED
@@ -7,6 +7,7 @@
7
7
  */
8
8
  // Export components
9
9
  export { GraphRenderer } from './components/GraphRenderer';
10
+ export { MultiCanvasRenderer, mergeCanvases, parseNodeId } from './components/MultiCanvasRenderer';
10
11
  export { ConfigurationSelector } from './components/ConfigurationSelector';
11
12
  // Export node/edge renderers
12
13
  export { GenericNode } from './nodes/GenericNode';
@@ -19,4 +20,5 @@ export { NodeTooltip } from './components/NodeTooltip';
19
20
  export { convertToXYFlowNodes, convertToXYFlowEdges, } from './utils/graphConverter';
20
21
  export { Icon, resolveIcon } from './utils/iconResolver';
21
22
  export { swapGraphOrientation, swapNodePositions, swapEdgeSides, } from './utils/orientationUtils';
23
+ export { getCanvasBounds, getCanvasDisplaySize } from './utils/canvasBounds';
22
24
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAwBH,oBAAoB;AACpB,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAQ3D,OAAO,EAAE,qBAAqB,EAAE,MAAM,oCAAoC,CAAC;AAG3E,6BAA6B;AAC7B,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAGlD,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAGhD,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAGlD,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAGhD,2BAA2B;AAC3B,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAGvD,mBAAmB;AACnB,OAAO,EACL,oBAAoB,EACpB,oBAAoB,GACrB,MAAM,wBAAwB,CAAC;AAEhC,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAEzD,OAAO,EACL,oBAAoB,EACpB,iBAAiB,EACjB,aAAa,GACd,MAAM,0BAA0B,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAwBH,oBAAoB;AACpB,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAQ3D,OAAO,EAAE,mBAAmB,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAC;AAOnG,OAAO,EAAE,qBAAqB,EAAE,MAAM,oCAAoC,CAAC;AAG3E,6BAA6B;AAC7B,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAGlD,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAGhD,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAGlD,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAGhD,2BAA2B;AAC3B,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAGvD,mBAAmB;AACnB,OAAO,EACL,oBAAoB,EACpB,oBAAoB,GACrB,MAAM,wBAAwB,CAAC;AAEhC,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAEzD,OAAO,EACL,oBAAoB,EACpB,iBAAiB,EACjB,aAAa,GACd,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,eAAe,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC"}
@@ -0,0 +1,29 @@
1
+ import type { ExtendedCanvas } from '@principal-ai/principal-view-core';
2
+ export interface CanvasBounds {
3
+ minX: number;
4
+ minY: number;
5
+ maxX: number;
6
+ maxY: number;
7
+ width: number;
8
+ height: number;
9
+ }
10
+ /**
11
+ * Calculate the bounding box of all nodes in a canvas.
12
+ * Returns the min/max coordinates and total dimensions.
13
+ */
14
+ export declare function getCanvasBounds(canvas: ExtendedCanvas): CanvasBounds;
15
+ /**
16
+ * Calculate a recommended display size for a canvas cell.
17
+ * Adds padding and enforces minimum dimensions.
18
+ */
19
+ export declare function getCanvasDisplaySize(canvas: ExtendedCanvas, options?: {
20
+ padding?: number;
21
+ minWidth?: number;
22
+ minHeight?: number;
23
+ maxWidth?: number;
24
+ maxHeight?: number;
25
+ }): {
26
+ width: number;
27
+ height: number;
28
+ };
29
+ //# sourceMappingURL=canvasBounds.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"canvasBounds.d.ts","sourceRoot":"","sources":["../../src/utils/canvasBounds.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mCAAmC,CAAC;AAExE,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAKD;;;GAGG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,cAAc,GAAG,YAAY,CAqCpE;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,cAAc,EACtB,OAAO,GAAE;IACP,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;CACf,GACL;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAoBnC"}
@@ -0,0 +1,56 @@
1
+ const DEFAULT_NODE_WIDTH = 200;
2
+ const DEFAULT_NODE_HEIGHT = 100;
3
+ /**
4
+ * Calculate the bounding box of all nodes in a canvas.
5
+ * Returns the min/max coordinates and total dimensions.
6
+ */
7
+ export function getCanvasBounds(canvas) {
8
+ if (!canvas.nodes || canvas.nodes.length === 0) {
9
+ return {
10
+ minX: 0,
11
+ minY: 0,
12
+ maxX: DEFAULT_NODE_WIDTH,
13
+ maxY: DEFAULT_NODE_HEIGHT,
14
+ width: DEFAULT_NODE_WIDTH,
15
+ height: DEFAULT_NODE_HEIGHT,
16
+ };
17
+ }
18
+ let minX = Infinity;
19
+ let minY = Infinity;
20
+ let maxX = -Infinity;
21
+ let maxY = -Infinity;
22
+ for (const node of canvas.nodes) {
23
+ const x = node.x ?? 0;
24
+ const y = node.y ?? 0;
25
+ const width = node.width ?? DEFAULT_NODE_WIDTH;
26
+ const height = node.height ?? DEFAULT_NODE_HEIGHT;
27
+ minX = Math.min(minX, x);
28
+ minY = Math.min(minY, y);
29
+ maxX = Math.max(maxX, x + width);
30
+ maxY = Math.max(maxY, y + height);
31
+ }
32
+ return {
33
+ minX,
34
+ minY,
35
+ maxX,
36
+ maxY,
37
+ width: maxX - minX,
38
+ height: maxY - minY,
39
+ };
40
+ }
41
+ /**
42
+ * Calculate a recommended display size for a canvas cell.
43
+ * Adds padding and enforces minimum dimensions.
44
+ */
45
+ export function getCanvasDisplaySize(canvas, options = {}) {
46
+ const { padding = 100, minWidth = 300, minHeight = 200, maxWidth = 1200, maxHeight = 800, } = options;
47
+ const bounds = getCanvasBounds(canvas);
48
+ // Add padding to content dimensions
49
+ const contentWidth = bounds.width + padding * 2;
50
+ const contentHeight = bounds.height + padding * 2;
51
+ // Clamp to min/max
52
+ const width = Math.min(maxWidth, Math.max(minWidth, contentWidth));
53
+ const height = Math.min(maxHeight, Math.max(minHeight, contentHeight));
54
+ return { width, height };
55
+ }
56
+ //# sourceMappingURL=canvasBounds.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"canvasBounds.js","sourceRoot":"","sources":["../../src/utils/canvasBounds.ts"],"names":[],"mappings":"AAWA,MAAM,kBAAkB,GAAG,GAAG,CAAC;AAC/B,MAAM,mBAAmB,GAAG,GAAG,CAAC;AAEhC;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,MAAsB;IACpD,IAAI,CAAC,MAAM,CAAC,KAAK,IAAI,MAAM,CAAC,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE;QAC9C,OAAO;YACL,IAAI,EAAE,CAAC;YACP,IAAI,EAAE,CAAC;YACP,IAAI,EAAE,kBAAkB;YACxB,IAAI,EAAE,mBAAmB;YACzB,KAAK,EAAE,kBAAkB;YACzB,MAAM,EAAE,mBAAmB;SAC5B,CAAC;KACH;IAED,IAAI,IAAI,GAAG,QAAQ,CAAC;IACpB,IAAI,IAAI,GAAG,QAAQ,CAAC;IACpB,IAAI,IAAI,GAAG,CAAC,QAAQ,CAAC;IACrB,IAAI,IAAI,GAAG,CAAC,QAAQ,CAAC;IAErB,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,KAAK,EAAE;QAC/B,MAAM,CAAC,GAAG,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC;QACtB,MAAM,CAAC,GAAG,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC;QACtB,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,IAAI,kBAAkB,CAAC;QAC/C,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,IAAI,mBAAmB,CAAC;QAElD,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;QACzB,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;QACzB,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,GAAG,KAAK,CAAC,CAAC;QACjC,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,GAAG,MAAM,CAAC,CAAC;KACnC;IAED,OAAO;QACL,IAAI;QACJ,IAAI;QACJ,IAAI;QACJ,IAAI;QACJ,KAAK,EAAE,IAAI,GAAG,IAAI;QAClB,MAAM,EAAE,IAAI,GAAG,IAAI;KACpB,CAAC;AACJ,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,oBAAoB,CAClC,MAAsB,EACtB,UAMI,EAAE;IAEN,MAAM,EACJ,OAAO,GAAG,GAAG,EACb,QAAQ,GAAG,GAAG,EACd,SAAS,GAAG,GAAG,EACf,QAAQ,GAAG,IAAI,EACf,SAAS,GAAG,GAAG,GAChB,GAAG,OAAO,CAAC;IAEZ,MAAM,MAAM,GAAG,eAAe,CAAC,MAAM,CAAC,CAAC;IAEvC,oCAAoC;IACpC,MAAM,YAAY,GAAG,MAAM,CAAC,KAAK,GAAG,OAAO,GAAG,CAAC,CAAC;IAChD,MAAM,aAAa,GAAG,MAAM,CAAC,MAAM,GAAG,OAAO,GAAG,CAAC,CAAC;IAElD,mBAAmB;IACnB,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,QAAQ,EAAE,IAAI,CAAC,GAAG,CAAC,QAAQ,EAAE,YAAY,CAAC,CAAC,CAAC;IACnE,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,SAAS,EAAE,IAAI,CAAC,GAAG,CAAC,SAAS,EAAE,aAAa,CAAC,CAAC,CAAC;IAEvE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC;AAC3B,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@principal-ai/principal-view-react",
3
- "version": "0.13.10",
3
+ "version": "0.13.11",
4
4
  "description": "React components for graph-based principal view framework",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -0,0 +1,225 @@
1
+ import { useMemo, forwardRef } from 'react';
2
+ import type { ExtendedCanvas, ExtendedCanvasNode, ExtendedCanvasEdge } from '@principal-ai/principal-view-core';
3
+ import { GraphRenderer } from './GraphRenderer';
4
+ import type { GraphRendererProps, GraphRendererHandle } from './GraphRenderer';
5
+
6
+ // ============================================================================
7
+ // Types
8
+ // ============================================================================
9
+
10
+ /**
11
+ * A canvas placement with its position in the combined coordinate space
12
+ */
13
+ export interface CanvasPlacement {
14
+ /** Unique identifier for this canvas */
15
+ canvasId: string;
16
+ /** The canvas to include */
17
+ canvas: ExtendedCanvas;
18
+ /** Position offset - all nodes in this canvas will be translated by this amount */
19
+ position: { x: number; y: number };
20
+ /** Optional label for the canvas (for future visual grouping) */
21
+ label?: string;
22
+ }
23
+
24
+ /**
25
+ * Layout configuration for multiple canvases on a single graph
26
+ */
27
+ export interface MultiCanvasLayout {
28
+ /** Array of canvas placements with positions */
29
+ placements: CanvasPlacement[];
30
+ }
31
+
32
+ /**
33
+ * Props for the MultiCanvasRenderer component
34
+ */
35
+ export interface MultiCanvasRendererProps
36
+ extends Omit<GraphRendererProps, 'canvas' | 'editable'> {
37
+ /** Layout configuration containing canvas placements */
38
+ layout: MultiCanvasLayout;
39
+ /** Callback when layout changes (canvas positions updated) */
40
+ onLayoutChange?: (layout: MultiCanvasLayout) => void;
41
+ /** Whether to show group borders around each canvas (default: true) */
42
+ showGroups?: boolean;
43
+ }
44
+
45
+ // ============================================================================
46
+ // Canvas Merging Utilities
47
+ // ============================================================================
48
+
49
+ const DEFAULT_NODE_WIDTH = 200;
50
+ const DEFAULT_NODE_HEIGHT = 100;
51
+ const GROUP_PADDING = 40;
52
+
53
+ /**
54
+ * Create a prefixed node ID to avoid collisions between canvases
55
+ */
56
+ function prefixNodeId(canvasId: string, nodeId: string): string {
57
+ return `${canvasId}:${nodeId}`;
58
+ }
59
+
60
+ /**
61
+ * Calculate the bounding box of nodes in a canvas (relative to canvas origin)
62
+ */
63
+ function calculateCanvasBounds(canvas: ExtendedCanvas): {
64
+ minX: number;
65
+ minY: number;
66
+ maxX: number;
67
+ maxY: number;
68
+ } {
69
+ if (!canvas.nodes || canvas.nodes.length === 0) {
70
+ return { minX: 0, minY: 0, maxX: DEFAULT_NODE_WIDTH, maxY: DEFAULT_NODE_HEIGHT };
71
+ }
72
+
73
+ let minX = Infinity;
74
+ let minY = Infinity;
75
+ let maxX = -Infinity;
76
+ let maxY = -Infinity;
77
+
78
+ for (const node of canvas.nodes) {
79
+ const x = node.x ?? 0;
80
+ const y = node.y ?? 0;
81
+ const width = node.width ?? DEFAULT_NODE_WIDTH;
82
+ const height = node.height ?? DEFAULT_NODE_HEIGHT;
83
+
84
+ minX = Math.min(minX, x);
85
+ minY = Math.min(minY, y);
86
+ maxX = Math.max(maxX, x + width);
87
+ maxY = Math.max(maxY, y + height);
88
+ }
89
+
90
+ return { minX, minY, maxX, maxY };
91
+ }
92
+
93
+ /**
94
+ * Merge multiple canvases into a single canvas with position offsets applied.
95
+ * Each canvas gets a group node border with its name as a label.
96
+ * Node IDs are prefixed with canvasId to avoid collisions (format: "canvasId:nodeId").
97
+ * Original IDs can be recovered using parseNodeId().
98
+ */
99
+ export function mergeCanvases(
100
+ placements: CanvasPlacement[],
101
+ options: { showGroups?: boolean } = {}
102
+ ): ExtendedCanvas {
103
+ const { showGroups = true } = options;
104
+ const groupNodes: ExtendedCanvasNode[] = [];
105
+ const mergedNodes: ExtendedCanvasNode[] = [];
106
+ const mergedEdges: ExtendedCanvasEdge[] = [];
107
+
108
+ for (const placement of placements) {
109
+ const { canvasId, canvas, position, label } = placement;
110
+
111
+ // Calculate bounds for this canvas to create a group node
112
+ if (showGroups && canvas.nodes && canvas.nodes.length > 0) {
113
+ const bounds = calculateCanvasBounds(canvas);
114
+ const groupNode: ExtendedCanvasNode = {
115
+ id: `${canvasId}:__group__`,
116
+ type: 'group',
117
+ label: label ?? canvas.pv?.name ?? canvasId,
118
+ x: position.x + bounds.minX - GROUP_PADDING,
119
+ y: position.y + bounds.minY - GROUP_PADDING,
120
+ width: bounds.maxX - bounds.minX + GROUP_PADDING * 2,
121
+ height: bounds.maxY - bounds.minY + GROUP_PADDING * 2,
122
+ };
123
+ groupNodes.push(groupNode);
124
+ }
125
+
126
+ // Process nodes - apply position offset and prefix IDs
127
+ if (canvas.nodes) {
128
+ for (const node of canvas.nodes) {
129
+ const mergedNode: ExtendedCanvasNode = {
130
+ ...node,
131
+ id: prefixNodeId(canvasId, node.id),
132
+ x: (node.x ?? 0) + position.x,
133
+ y: (node.y ?? 0) + position.y,
134
+ };
135
+ mergedNodes.push(mergedNode);
136
+ }
137
+ }
138
+
139
+ // Process edges - update node references with prefixed IDs
140
+ if (canvas.edges) {
141
+ for (const edge of canvas.edges) {
142
+ const mergedEdge: ExtendedCanvasEdge = {
143
+ ...edge,
144
+ id: prefixNodeId(canvasId, edge.id),
145
+ fromNode: prefixNodeId(canvasId, edge.fromNode),
146
+ toNode: prefixNodeId(canvasId, edge.toNode),
147
+ };
148
+ mergedEdges.push(mergedEdge);
149
+ }
150
+ }
151
+ }
152
+
153
+ // Create merged canvas - group nodes first so they render behind
154
+ const mergedCanvas: ExtendedCanvas = {
155
+ nodes: [...groupNodes, ...mergedNodes],
156
+ edges: mergedEdges,
157
+ pv: {
158
+ version: '1.0.0',
159
+ name: 'Multi-Canvas View',
160
+ },
161
+ };
162
+
163
+ return mergedCanvas;
164
+ }
165
+
166
+ /**
167
+ * Extract the original canvas ID and node ID from a merged node ID
168
+ */
169
+ export function parseNodeId(
170
+ mergedNodeId: string
171
+ ): { canvasId: string; nodeId: string } | null {
172
+ const colonIndex = mergedNodeId.indexOf(':');
173
+ if (colonIndex === -1) return null;
174
+ return {
175
+ canvasId: mergedNodeId.substring(0, colonIndex),
176
+ nodeId: mergedNodeId.substring(colonIndex + 1),
177
+ };
178
+ }
179
+
180
+ // ============================================================================
181
+ // MultiCanvasRenderer Component
182
+ // ============================================================================
183
+
184
+ /**
185
+ * Renders multiple canvases merged into a single graph view.
186
+ *
187
+ * Each canvas is positioned at its specified offset, and all nodes/edges
188
+ * are combined into a unified viewport. Node IDs are prefixed with their
189
+ * source canvas ID to avoid collisions (format: "canvasId:nodeId").
190
+ *
191
+ * @example
192
+ * ```tsx
193
+ * <MultiCanvasRenderer
194
+ * layout={{
195
+ * placements: [
196
+ * { canvasId: 'auth', canvas: authCanvas, position: { x: 0, y: 0 } },
197
+ * { canvasId: 'data', canvas: dataCanvas, position: { x: 600, y: 0 } },
198
+ * ],
199
+ * }}
200
+ * showControls
201
+ * />
202
+ * ```
203
+ */
204
+ export const MultiCanvasRenderer = forwardRef<
205
+ GraphRendererHandle,
206
+ MultiCanvasRendererProps
207
+ >(function MultiCanvasRenderer(
208
+ { layout, onLayoutChange: _onLayoutChange, showGroups = true, ...graphRendererProps },
209
+ ref
210
+ ) {
211
+ // Merge all canvases into a single canvas
212
+ const mergedCanvas = useMemo(
213
+ () => mergeCanvases(layout.placements, { showGroups }),
214
+ [layout.placements, showGroups]
215
+ );
216
+
217
+ return (
218
+ <GraphRenderer
219
+ ref={ref}
220
+ canvas={mergedCanvas}
221
+ editable={false}
222
+ {...graphRendererProps}
223
+ />
224
+ );
225
+ });
package/src/index.ts CHANGED
@@ -37,6 +37,13 @@ export type {
37
37
  PendingChanges,
38
38
  } from './components/GraphRenderer';
39
39
 
40
+ export { MultiCanvasRenderer, mergeCanvases, parseNodeId } from './components/MultiCanvasRenderer';
41
+ export type {
42
+ MultiCanvasRendererProps,
43
+ MultiCanvasLayout,
44
+ CanvasPlacement,
45
+ } from './components/MultiCanvasRenderer';
46
+
40
47
  export { ConfigurationSelector } from './components/ConfigurationSelector';
41
48
  export type { ConfigurationSelectorProps } from './components/ConfigurationSelector';
42
49
 
@@ -70,3 +77,5 @@ export {
70
77
  swapNodePositions,
71
78
  swapEdgeSides,
72
79
  } from './utils/orientationUtils';
80
+ export { getCanvasBounds, getCanvasDisplaySize } from './utils/canvasBounds';
81
+ export type { CanvasBounds } from './utils/canvasBounds';
@@ -0,0 +1,329 @@
1
+ import React from 'react';
2
+ import type { Meta, StoryObj } from '@storybook/react';
3
+ import { MultiCanvasRenderer } from '../components/MultiCanvasRenderer';
4
+ import type { MultiCanvasLayout } from '../components/MultiCanvasRenderer';
5
+ import type { ExtendedCanvas } from '@principal-ai/principal-view-core';
6
+ import { ThemeProvider, defaultEditorTheme } from '@principal-ade/industry-theme';
7
+
8
+ const meta = {
9
+ title: 'Components/MultiCanvasRenderer',
10
+ component: MultiCanvasRenderer,
11
+ parameters: {
12
+ layout: 'fullscreen',
13
+ },
14
+ tags: ['autodocs'],
15
+ decorators: [
16
+ (Story) => (
17
+ <ThemeProvider theme={defaultEditorTheme}>
18
+ <div style={{ width: '100vw', height: '100vh' }}>
19
+ <Story />
20
+ </div>
21
+ </ThemeProvider>
22
+ ),
23
+ ],
24
+ } satisfies Meta<typeof MultiCanvasRenderer>;
25
+
26
+ export default meta;
27
+ type Story = StoryObj<typeof meta>;
28
+
29
+ // ============================================================================
30
+ // Sample Canvases
31
+ // ============================================================================
32
+
33
+ const authFlowCanvas: ExtendedCanvas = {
34
+ nodes: [
35
+ {
36
+ id: 'login',
37
+ type: 'text',
38
+ x: 0,
39
+ y: 0,
40
+ width: 120,
41
+ height: 60,
42
+ text: 'Login',
43
+ color: '#4A90E2',
44
+ pv: { nodeType: 'process', shape: 'rectangle', icon: 'User' },
45
+ },
46
+ {
47
+ id: 'validate',
48
+ type: 'text',
49
+ x: 170,
50
+ y: 0,
51
+ width: 120,
52
+ height: 60,
53
+ text: 'Validate',
54
+ color: '#F5A623',
55
+ pv: { nodeType: 'process', shape: 'rectangle', icon: 'Shield' },
56
+ },
57
+ {
58
+ id: 'session',
59
+ type: 'text',
60
+ x: 340,
61
+ y: 0,
62
+ width: 120,
63
+ height: 60,
64
+ text: 'Session',
65
+ color: '#7ED321',
66
+ pv: { nodeType: 'data', shape: 'rectangle', icon: 'Key' },
67
+ },
68
+ ],
69
+ edges: [
70
+ { id: 'e1', fromNode: 'login', toNode: 'validate', pv: { edgeType: 'flow' } },
71
+ { id: 'e2', fromNode: 'validate', toNode: 'session', pv: { edgeType: 'flow' } },
72
+ ],
73
+ pv: {
74
+ version: '1.0.0',
75
+ name: 'Authentication Flow',
76
+ },
77
+ };
78
+
79
+ const dataProcessingCanvas: ExtendedCanvas = {
80
+ nodes: [
81
+ {
82
+ id: 'input',
83
+ type: 'text',
84
+ x: 0,
85
+ y: 0,
86
+ width: 100,
87
+ height: 100,
88
+ text: 'Input',
89
+ color: '#7B68EE',
90
+ pv: { nodeType: 'data', shape: 'circle', icon: 'Download' },
91
+ },
92
+ {
93
+ id: 'transform',
94
+ type: 'text',
95
+ x: 150,
96
+ y: 15,
97
+ width: 140,
98
+ height: 70,
99
+ text: 'Transform',
100
+ color: '#4A90E2',
101
+ pv: { nodeType: 'process', shape: 'rectangle', icon: 'Settings' },
102
+ },
103
+ {
104
+ id: 'output',
105
+ type: 'text',
106
+ x: 340,
107
+ y: 0,
108
+ width: 100,
109
+ height: 100,
110
+ text: 'Output',
111
+ color: '#7B68EE',
112
+ pv: { nodeType: 'data', shape: 'circle', icon: 'Upload' },
113
+ },
114
+ ],
115
+ edges: [
116
+ { id: 'e1', fromNode: 'input', toNode: 'transform', pv: { edgeType: 'dataflow' } },
117
+ { id: 'e2', fromNode: 'transform', toNode: 'output', pv: { edgeType: 'dataflow' } },
118
+ ],
119
+ pv: {
120
+ version: '1.0.0',
121
+ name: 'Data Processing Pipeline',
122
+ },
123
+ };
124
+
125
+ const notificationCanvas: ExtendedCanvas = {
126
+ nodes: [
127
+ {
128
+ id: 'event',
129
+ type: 'text',
130
+ x: 0,
131
+ y: 30,
132
+ width: 100,
133
+ height: 60,
134
+ text: 'Event',
135
+ color: '#F5A623',
136
+ pv: { nodeType: 'trigger', shape: 'rectangle', icon: 'Zap' },
137
+ },
138
+ {
139
+ id: 'router',
140
+ type: 'text',
141
+ x: 150,
142
+ y: 0,
143
+ width: 100,
144
+ height: 100,
145
+ text: 'Router',
146
+ color: '#4A90E2',
147
+ pv: { nodeType: 'process', shape: 'diamond', icon: 'GitBranch' },
148
+ },
149
+ {
150
+ id: 'email',
151
+ type: 'text',
152
+ x: 300,
153
+ y: -30,
154
+ width: 100,
155
+ height: 50,
156
+ text: 'Email',
157
+ color: '#7ED321',
158
+ pv: { nodeType: 'action', shape: 'rectangle', icon: 'Mail' },
159
+ },
160
+ {
161
+ id: 'sms',
162
+ type: 'text',
163
+ x: 300,
164
+ y: 40,
165
+ width: 100,
166
+ height: 50,
167
+ text: 'SMS',
168
+ color: '#7ED321',
169
+ pv: { nodeType: 'action', shape: 'rectangle', icon: 'MessageSquare' },
170
+ },
171
+ {
172
+ id: 'push',
173
+ type: 'text',
174
+ x: 300,
175
+ y: 110,
176
+ width: 100,
177
+ height: 50,
178
+ text: 'Push',
179
+ color: '#7ED321',
180
+ pv: { nodeType: 'action', shape: 'rectangle', icon: 'Bell' },
181
+ },
182
+ ],
183
+ edges: [
184
+ { id: 'e1', fromNode: 'event', toNode: 'router', pv: { edgeType: 'flow' } },
185
+ { id: 'e2', fromNode: 'router', toNode: 'email', pv: { edgeType: 'flow' } },
186
+ { id: 'e3', fromNode: 'router', toNode: 'sms', pv: { edgeType: 'flow' } },
187
+ { id: 'e4', fromNode: 'router', toNode: 'push', pv: { edgeType: 'flow' } },
188
+ ],
189
+ pv: {
190
+ version: '1.0.0',
191
+ name: 'Notification System',
192
+ },
193
+ };
194
+
195
+ const monitoringCanvas: ExtendedCanvas = {
196
+ nodes: [
197
+ {
198
+ id: 'metrics',
199
+ type: 'text',
200
+ x: 0,
201
+ y: 0,
202
+ width: 100,
203
+ height: 60,
204
+ text: 'Metrics',
205
+ color: '#4A90E2',
206
+ pv: { nodeType: 'data', shape: 'rectangle', icon: 'BarChart2' },
207
+ },
208
+ {
209
+ id: 'alert',
210
+ type: 'text',
211
+ x: 150,
212
+ y: 0,
213
+ width: 100,
214
+ height: 60,
215
+ text: 'Alert',
216
+ color: '#D0021B',
217
+ pv: { nodeType: 'process', shape: 'rectangle', icon: 'AlertTriangle' },
218
+ },
219
+ ],
220
+ edges: [
221
+ { id: 'e1', fromNode: 'metrics', toNode: 'alert', pv: { edgeType: 'flow' } },
222
+ ],
223
+ pv: {
224
+ version: '1.0.0',
225
+ name: 'Monitoring',
226
+ },
227
+ };
228
+
229
+ // ============================================================================
230
+ // Stories
231
+ // ============================================================================
232
+
233
+ /**
234
+ * Two canvases side by side on the same graph
235
+ */
236
+ const twoCanvasLayout: MultiCanvasLayout = {
237
+ placements: [
238
+ { canvasId: 'auth', canvas: authFlowCanvas, position: { x: 0, y: 0 } },
239
+ { canvasId: 'data', canvas: dataProcessingCanvas, position: { x: 600, y: 0 } },
240
+ ],
241
+ };
242
+
243
+ export const TwoCanvasesSideBySide: Story = {
244
+ args: {
245
+ layout: twoCanvasLayout,
246
+ showControls: true,
247
+ showBackground: true,
248
+ },
249
+ };
250
+
251
+ /**
252
+ * Four canvases arranged in a 2x2 grid pattern
253
+ */
254
+ const fourCanvasLayout: MultiCanvasLayout = {
255
+ placements: [
256
+ { canvasId: 'auth', canvas: authFlowCanvas, position: { x: 0, y: 0 } },
257
+ { canvasId: 'data', canvas: dataProcessingCanvas, position: { x: 600, y: 0 } },
258
+ { canvasId: 'notify', canvas: notificationCanvas, position: { x: 0, y: 200 } },
259
+ { canvasId: 'monitor', canvas: monitoringCanvas, position: { x: 600, y: 200 } },
260
+ ],
261
+ };
262
+
263
+ export const FourCanvasesGrid: Story = {
264
+ args: {
265
+ layout: fourCanvasLayout,
266
+ showControls: true,
267
+ showBackground: true,
268
+ },
269
+ };
270
+
271
+ /**
272
+ * Canvases stacked vertically
273
+ */
274
+ const verticalLayout: MultiCanvasLayout = {
275
+ placements: [
276
+ { canvasId: 'auth', canvas: authFlowCanvas, position: { x: 0, y: 0 } },
277
+ { canvasId: 'data', canvas: dataProcessingCanvas, position: { x: 0, y: 150 } },
278
+ { canvasId: 'notify', canvas: notificationCanvas, position: { x: 0, y: 300 } },
279
+ ],
280
+ };
281
+
282
+ export const VerticalStack: Story = {
283
+ args: {
284
+ layout: verticalLayout,
285
+ showControls: true,
286
+ showBackground: true,
287
+ },
288
+ };
289
+
290
+ /**
291
+ * Single canvas (baseline comparison)
292
+ */
293
+ const singleCanvasLayout: MultiCanvasLayout = {
294
+ placements: [
295
+ { canvasId: 'notify', canvas: notificationCanvas, position: { x: 0, y: 0 } },
296
+ ],
297
+ };
298
+
299
+ export const SingleCanvas: Story = {
300
+ args: {
301
+ layout: singleCanvasLayout,
302
+ showControls: true,
303
+ showBackground: true,
304
+ },
305
+ };
306
+
307
+ /**
308
+ * With minimap enabled to navigate the combined view
309
+ */
310
+ export const WithMinimap: Story = {
311
+ args: {
312
+ layout: fourCanvasLayout,
313
+ showControls: true,
314
+ showMinimap: true,
315
+ showBackground: true,
316
+ },
317
+ };
318
+
319
+ /**
320
+ * Without group borders - just the nodes merged together
321
+ */
322
+ export const WithoutGroups: Story = {
323
+ args: {
324
+ layout: fourCanvasLayout,
325
+ showControls: true,
326
+ showBackground: true,
327
+ showGroups: false,
328
+ },
329
+ };
@@ -0,0 +1,91 @@
1
+ import type { ExtendedCanvas } from '@principal-ai/principal-view-core';
2
+
3
+ export interface CanvasBounds {
4
+ minX: number;
5
+ minY: number;
6
+ maxX: number;
7
+ maxY: number;
8
+ width: number;
9
+ height: number;
10
+ }
11
+
12
+ const DEFAULT_NODE_WIDTH = 200;
13
+ const DEFAULT_NODE_HEIGHT = 100;
14
+
15
+ /**
16
+ * Calculate the bounding box of all nodes in a canvas.
17
+ * Returns the min/max coordinates and total dimensions.
18
+ */
19
+ export function getCanvasBounds(canvas: ExtendedCanvas): CanvasBounds {
20
+ if (!canvas.nodes || canvas.nodes.length === 0) {
21
+ return {
22
+ minX: 0,
23
+ minY: 0,
24
+ maxX: DEFAULT_NODE_WIDTH,
25
+ maxY: DEFAULT_NODE_HEIGHT,
26
+ width: DEFAULT_NODE_WIDTH,
27
+ height: DEFAULT_NODE_HEIGHT,
28
+ };
29
+ }
30
+
31
+ let minX = Infinity;
32
+ let minY = Infinity;
33
+ let maxX = -Infinity;
34
+ let maxY = -Infinity;
35
+
36
+ for (const node of canvas.nodes) {
37
+ const x = node.x ?? 0;
38
+ const y = node.y ?? 0;
39
+ const width = node.width ?? DEFAULT_NODE_WIDTH;
40
+ const height = node.height ?? DEFAULT_NODE_HEIGHT;
41
+
42
+ minX = Math.min(minX, x);
43
+ minY = Math.min(minY, y);
44
+ maxX = Math.max(maxX, x + width);
45
+ maxY = Math.max(maxY, y + height);
46
+ }
47
+
48
+ return {
49
+ minX,
50
+ minY,
51
+ maxX,
52
+ maxY,
53
+ width: maxX - minX,
54
+ height: maxY - minY,
55
+ };
56
+ }
57
+
58
+ /**
59
+ * Calculate a recommended display size for a canvas cell.
60
+ * Adds padding and enforces minimum dimensions.
61
+ */
62
+ export function getCanvasDisplaySize(
63
+ canvas: ExtendedCanvas,
64
+ options: {
65
+ padding?: number;
66
+ minWidth?: number;
67
+ minHeight?: number;
68
+ maxWidth?: number;
69
+ maxHeight?: number;
70
+ } = {}
71
+ ): { width: number; height: number } {
72
+ const {
73
+ padding = 100,
74
+ minWidth = 300,
75
+ minHeight = 200,
76
+ maxWidth = 1200,
77
+ maxHeight = 800,
78
+ } = options;
79
+
80
+ const bounds = getCanvasBounds(canvas);
81
+
82
+ // Add padding to content dimensions
83
+ const contentWidth = bounds.width + padding * 2;
84
+ const contentHeight = bounds.height + padding * 2;
85
+
86
+ // Clamp to min/max
87
+ const width = Math.min(maxWidth, Math.max(minWidth, contentWidth));
88
+ const height = Math.min(maxHeight, Math.max(minHeight, contentHeight));
89
+
90
+ return { width, height };
91
+ }