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,82 +1,50 @@
import { useCallback, useRef, useState } from 'react';
import type { NodeAction } from './types';
import styles from './ProcessDiagram.module.css';
const TOOLBAR_HEIGHT = 28;
const TOOLBAR_WIDTH = 140;
const HIDE_DELAY = 150;
interface NodeToolbarProps {
nodeId: string;
nodeX: number;
nodeY: number;
nodeWidth: number;
/** Screen-space position (already transformed by zoom/pan) */
screenX: number;
screenY: number;
onAction: (nodeId: string, action: NodeAction) => void;
onMouseEnter: () => void;
onMouseLeave: () => void;
}
const ACTIONS: { label: string; icon: string; action: NodeAction; title: string }[] = [
{ label: 'Inspect', icon: '\uD83D\uDD0D', action: 'inspect', title: 'Inspect node' },
{ label: 'Trace', icon: 'T', action: 'toggle-trace', title: 'Toggle tracing' },
{ label: 'Tap', icon: '\u270E', action: 'configure-tap', title: 'Configure tap' },
{ label: 'More', icon: '\u22EF', action: 'copy-id', title: 'Copy processor ID' },
const ACTIONS: { icon: string; action: NodeAction; title: string }[] = [
{ icon: '\uD83D\uDD0D', action: 'inspect', title: 'Inspect' },
{ icon: 'T', action: 'toggle-trace', title: 'Toggle tracing' },
{ icon: '\u270E', action: 'configure-tap', title: 'Configure tap' },
{ icon: '\u22EF', action: 'copy-id', title: 'Copy ID' },
];
export function NodeToolbar({
nodeId, nodeX, nodeY, nodeWidth, onAction, onMouseEnter, onMouseLeave,
nodeId, screenX, screenY, onAction, onMouseEnter, onMouseLeave,
}: NodeToolbarProps) {
const x = nodeX + (nodeWidth - TOOLBAR_WIDTH) / 2;
const y = nodeY - TOOLBAR_HEIGHT - 6;
return (
<foreignObject
x={x}
y={y}
width={TOOLBAR_WIDTH}
height={TOOLBAR_HEIGHT}
<div
className={styles.nodeToolbar}
style={{ left: screenX, top: screenY }}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
>
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: '6px',
height: TOOLBAR_HEIGHT,
background: 'rgba(26, 22, 18, 0.92)',
borderRadius: '6px',
padding: '0 8px',
}}
>
{ACTIONS.map(a => (
<button
key={a.action}
title={a.title}
onClick={(e) => {
e.stopPropagation();
onAction(nodeId, a.action);
}}
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: 22,
height: 22,
borderRadius: '50%',
border: 'none',
background: 'rgba(255, 255, 255, 0.15)',
color: 'white',
fontSize: '11px',
cursor: 'pointer',
padding: 0,
}}
>
{a.icon}
</button>
))}
</div>
</foreignObject>
{ACTIONS.map(a => (
<button
key={a.action}
className={styles.nodeToolbarBtn}
title={a.title}
onClick={(e) => {
e.stopPropagation();
onAction(nodeId, a.action);
}}
>
{a.icon}
</button>
))}
</div>
);
}