fix: diagram rendering improvements
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 57s
CI / docker (push) Successful in 52s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 37s

- Recursive compound rendering: CompoundNode checks if children are
  themselves compound types (WHEN inside CHOICE) and renders them
  recursively. Added EIP_WHEN, EIP_OTHERWISE, DO_CATCH, DO_FINALLY
  to frontend COMPOUND_TYPES.
- Edge z-ordering: edges are distributed to their containing compound
  and rendered after the background rect, so they're not hidden behind
  compound containers.
- Error section sizing: normalize error handler node coordinates to
  start at (0,0), compute red tint background height from actual
  content with symmetric padding for vertical centering.
- Toolbar as HTML overlay: moved from SVG foreignObject to absolute-
  positioned HTML div so it stays fixed size at any zoom level. Uses
  design system tokens for consistent styling.
- Zoom: replaced viewBox approach with CSS transform on content group.
  Default zoom is 100% anchored top-left. Fit-to-view still available
  via button.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-03-27 16:33:24 +01:00
parent 20d1182259
commit 9b7626f6ff
8 changed files with 326 additions and 176 deletions

View File

@@ -1,13 +1,19 @@
import type { DiagramNode as DiagramNodeType } from '../../api/queries/diagrams';
import type { DiagramNode as DiagramNodeType, DiagramEdge as DiagramEdgeType } from '../../api/queries/diagrams';
import type { NodeConfig } from './types';
import { colorForType } from './node-colors';
import { colorForType, isCompoundType } from './node-colors';
import { DiagramNode } from './DiagramNode';
import { DiagramEdge } from './DiagramEdge';
const HEADER_HEIGHT = 22;
const CORNER_RADIUS = 4;
interface CompoundNodeProps {
node: DiagramNodeType;
/** All edges for this section — compound filters to its own internal edges */
edges: DiagramEdgeType[];
/** Absolute offset of the nearest compound ancestor (for coordinate adjustment) */
parentX?: number;
parentY?: number;
selectedNodeId?: string;
hoveredNodeId: string | null;
nodeConfigs?: Map<string, NodeConfig>;
@@ -17,17 +23,28 @@ interface CompoundNodeProps {
}
export function CompoundNode({
node, selectedNodeId, hoveredNodeId, nodeConfigs,
node, edges, parentX = 0, parentY = 0,
selectedNodeId, hoveredNodeId, nodeConfigs,
onNodeClick, onNodeEnter, onNodeLeave,
}: CompoundNodeProps) {
const x = node.x ?? 0;
const y = node.y ?? 0;
const x = (node.x ?? 0) - parentX;
const y = (node.y ?? 0) - parentY;
const absX = node.x ?? 0;
const absY = node.y ?? 0;
const w = node.width ?? 200;
const h = node.height ?? 100;
const color = colorForType(node.type);
const typeName = node.type?.replace(/^EIP_/, '').replace(/_/g, ' ') ?? '';
const label = node.label ? `${typeName}: ${node.label}` : typeName;
// Collect all descendant node IDs to filter edges that belong inside this compound
const descendantIds = new Set<string>();
collectIds(node.children ?? [], descendantIds);
const internalEdges = edges.filter(
e => descendantIds.has(e.sourceId) && descendantIds.has(e.targetId),
);
return (
<g data-node-id={node.id} transform={`translate(${x}, ${y})`}>
{/* Container body */}
@@ -58,25 +75,62 @@ export function CompoundNode({
{label}
</text>
{/* Children nodes (positioned relative to compound) */}
{node.children?.map(child => (
<DiagramNode
key={child.id}
node={{
...child,
// Children have absolute coordinates from the backend,
// but since we're inside the compound's translate, subtract parent offset
x: (child.x ?? 0) - x,
y: (child.y ?? 0) - y,
}}
isHovered={hoveredNodeId === child.id}
isSelected={selectedNodeId === child.id}
config={child.id ? nodeConfigs?.get(child.id) : undefined}
onClick={() => child.id && onNodeClick(child.id)}
onMouseEnter={() => child.id && onNodeEnter(child.id)}
onMouseLeave={onNodeLeave}
/>
))}
{/* Internal edges (rendered after background, before children) */}
<g className="edges">
{internalEdges.map((edge, i) => (
<DiagramEdge
key={`${edge.sourceId}-${edge.targetId}-${i}`}
edge={{
...edge,
points: edge.points.map(p => [p[0] - absX, p[1] - absY]),
}}
/>
))}
</g>
{/* Children — recurse into compound children, render leaves as DiagramNode */}
{node.children?.map(child => {
if (isCompoundType(child.type) && child.children && child.children.length > 0) {
return (
<CompoundNode
key={child.id}
node={child}
edges={edges}
parentX={absX}
parentY={absY}
selectedNodeId={selectedNodeId}
hoveredNodeId={hoveredNodeId}
nodeConfigs={nodeConfigs}
onNodeClick={onNodeClick}
onNodeEnter={onNodeEnter}
onNodeLeave={onNodeLeave}
/>
);
}
return (
<DiagramNode
key={child.id}
node={{
...child,
x: (child.x ?? 0) - absX,
y: (child.y ?? 0) - absY,
}}
isHovered={hoveredNodeId === child.id}
isSelected={selectedNodeId === child.id}
config={child.id ? nodeConfigs?.get(child.id) : undefined}
onClick={() => child.id && onNodeClick(child.id)}
onMouseEnter={() => child.id && onNodeEnter(child.id)}
onMouseLeave={onNodeLeave}
/>
);
})}
</g>
);
}
function collectIds(nodes: DiagramNodeType[], set: Set<string>) {
for (const n of nodes) {
if (n.id) set.add(n.id);
if (n.children) collectIds(n.children, set);
}
}