Double-click a DIRECT or SEDA node to navigate into that route's diagram. Breadcrumbs show the route stack and allow clicking back to any level. Escape key goes back one level. Route ID resolution handles camelCase endpoint URIs mapping to kebab-case route IDs (e.g. direct:callGetProduct → call-get-product) using the catalog's known route IDs. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
96 lines
2.7 KiB
TypeScript
96 lines
2.7 KiB
TypeScript
import type { DiagramNode as DiagramNodeType } from '../../api/queries/diagrams';
|
|
import type { NodeConfig } from './types';
|
|
import { colorForType, iconForType } from './node-colors';
|
|
import { ConfigBadge } from './ConfigBadge';
|
|
|
|
const TOP_BAR_HEIGHT = 6;
|
|
const CORNER_RADIUS = 4;
|
|
|
|
interface DiagramNodeProps {
|
|
node: DiagramNodeType;
|
|
isHovered: boolean;
|
|
isSelected: boolean;
|
|
config?: NodeConfig;
|
|
onClick: () => void;
|
|
onDoubleClick?: () => void;
|
|
onMouseEnter: () => void;
|
|
onMouseLeave: () => void;
|
|
}
|
|
|
|
export function DiagramNode({
|
|
node, isHovered, isSelected, config, onClick, onDoubleClick, onMouseEnter, onMouseLeave,
|
|
}: DiagramNodeProps) {
|
|
const x = node.x ?? 0;
|
|
const y = node.y ?? 0;
|
|
const w = node.width ?? 120;
|
|
const h = node.height ?? 40;
|
|
const color = colorForType(node.type);
|
|
const icon = iconForType(node.type);
|
|
|
|
// Extract label parts: type name and detail
|
|
const typeName = node.type?.replace(/^EIP_/, '').replace(/_/g, ' ') ?? '';
|
|
const detail = node.label || '';
|
|
|
|
return (
|
|
<g
|
|
data-node-id={node.id}
|
|
transform={`translate(${x}, ${y})`}
|
|
onClick={(e) => { e.stopPropagation(); onClick(); }}
|
|
onDoubleClick={(e) => { e.stopPropagation(); onDoubleClick?.(); }}
|
|
onMouseEnter={onMouseEnter}
|
|
onMouseLeave={onMouseLeave}
|
|
style={{ cursor: 'pointer' }}
|
|
>
|
|
{/* Selection ring */}
|
|
{isSelected && (
|
|
<rect
|
|
x={-2}
|
|
y={-2}
|
|
width={w + 4}
|
|
height={h + 4}
|
|
rx={CORNER_RADIUS + 2}
|
|
fill="none"
|
|
stroke="#C6820E"
|
|
strokeWidth={2.5}
|
|
/>
|
|
)}
|
|
|
|
{/* Card background */}
|
|
<rect
|
|
x={0}
|
|
y={0}
|
|
width={w}
|
|
height={h}
|
|
rx={CORNER_RADIUS}
|
|
fill={isHovered ? '#F5F0EA' : 'white'}
|
|
stroke={isHovered || isSelected ? color : '#E4DFD8'}
|
|
strokeWidth={isHovered || isSelected ? 1.5 : 1}
|
|
/>
|
|
|
|
{/* Colored top bar */}
|
|
<rect x={0} y={0} width={w} height={TOP_BAR_HEIGHT} rx={CORNER_RADIUS} fill={color} />
|
|
<rect x={CORNER_RADIUS} y={0} width={w - CORNER_RADIUS * 2} height={TOP_BAR_HEIGHT} fill={color} />
|
|
|
|
{/* Icon */}
|
|
<text x={14} y={h / 2 + 6} fill={color} fontSize={14}>
|
|
{icon}
|
|
</text>
|
|
|
|
{/* Type name */}
|
|
<text x={32} y={h / 2 + 1} fill="#1A1612" fontSize={11} fontWeight={600}>
|
|
{typeName}
|
|
</text>
|
|
|
|
{/* Detail label (truncated) */}
|
|
{detail && detail !== typeName && (
|
|
<text x={32} y={h / 2 + 14} fill="#5C5347" fontSize={10}>
|
|
{detail.length > 22 ? detail.slice(0, 20) + '...' : detail}
|
|
</text>
|
|
)}
|
|
|
|
{/* Config badges */}
|
|
{config && <ConfigBadge nodeWidth={w} config={config} />}
|
|
</g>
|
|
);
|
|
}
|