feat: add interactive ProcessDiagram SVG component (sub-project 1/3)
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m0s
CI / docker (push) Successful in 56s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 38s

New interactive route diagram component with SVG rendering using
server-computed ELK layout coordinates. TIBCO BW5-inspired top-bar
card node style with zoom/pan, hover toolbars, config badges, and
error handler sections below the main flow.

Backend: add direction query parameter (LR/TB) to diagram render
endpoints, defaulting to left-to-right layout.

Frontend: 14-file ProcessDiagram component in ui/src/components/
with DiagramNode, CompoundNode, DiagramEdge, ConfigBadge, NodeToolbar,
ErrorSection, ZoomControls, and supporting hooks. Dev test page at
/dev/diagram for validation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-03-27 13:55:29 +01:00
parent 78e12f5cf9
commit ac32396a57
24 changed files with 7264 additions and 18 deletions

View File

@@ -0,0 +1,49 @@
import type { DiagramEdge as DiagramEdgeType } from '../../api/queries/diagrams';
interface DiagramEdgeProps {
edge: DiagramEdgeType;
offsetY?: number;
}
export function DiagramEdge({ edge, offsetY = 0 }: DiagramEdgeProps) {
const pts = edge.points;
if (!pts || pts.length < 2) return null;
// Build SVG path: move to first point, then cubic bezier or line to rest
let d = `M ${pts[0][0]} ${pts[0][1] + offsetY}`;
if (pts.length === 2) {
d += ` L ${pts[1][0]} ${pts[1][1] + offsetY}`;
} else if (pts.length === 4) {
// 4 points: start, control1, control2, end → cubic bezier
d += ` C ${pts[1][0]} ${pts[1][1] + offsetY}, ${pts[2][0]} ${pts[2][1] + offsetY}, ${pts[3][0]} ${pts[3][1] + offsetY}`;
} else {
// Multiple points: connect with line segments through intermediate points
for (let i = 1; i < pts.length; i++) {
d += ` L ${pts[i][0]} ${pts[i][1] + offsetY}`;
}
}
return (
<g className="diagram-edge">
<path
d={d}
fill="none"
stroke="#9CA3AF"
strokeWidth={1.5}
markerEnd="url(#arrowhead)"
/>
{edge.label && pts.length >= 2 && (
<text
x={(pts[0][0] + pts[pts.length - 1][0]) / 2}
y={(pts[0][1] + pts[pts.length - 1][1]) / 2 + offsetY - 6}
fill="#9C9184"
fontSize={9}
textAnchor="middle"
>
{edge.label}
</text>
)}
</g>
);
}