import { createContext, useContext } from 'react'; import { Handle, NodeResizer, Position, type Node, type NodeProps } from '@xyflow/react'; import type { DiagramNodeData, NodeKind } from './types'; type ShapeNode = Node; /** Ids of nodes that currently have children (i.e. render as a drill-into-able sub-process). */ export const NodesWithChildrenContext = createContext>(new Set()); /** Current editor mode — resize handles and connection points only show in 'edit'. */ export const ModeContext = createContext<'edit' | 'view'>('edit'); // Gradients from palette.css's "ОСНОВНАЯ ПАЛИТРА" section — the one the // palette itself labels as meant for diagrams/flowcharts/semantic highlights. // This is what nodes actually render with until a user picks a custom color. export const DEFAULT_NODE_BACKGROUND: Record = { task: 'var(--gradient-blue)', startEvent: 'var(--gradient-green)', intermediateEvent: 'var(--gradient-orange)', endEvent: 'var(--gradient-red)', exclusiveGateway: 'var(--gradient-purple)', parallelGateway: 'var(--gradient-cyan)', inclusiveGateway: 'var(--gradient-olive)', }; // The color picker only understands flat hex, so it needs a solid fallback // distinct from the gradient rendered on canvas — the gradient's start color. export const DEFAULT_NODE_COLOR: Record = { task: '#0790CB', startEvent: '#2CB587', intermediateEvent: '#FF8E58', endEvent: '#E13042', exclusiveGateway: '#A12C7E', parallelGateway: '#1BD3F7', inclusiveGateway: '#BBBD39', }; const HANDLE_POSITIONS: { id: string; position: Position }[] = [ { id: 'top', position: Position.Top }, { id: 'right', position: Position.Right }, { id: 'bottom', position: Position.Bottom }, { id: 'left', position: Position.Left }, ]; /** * A single handle per side acts as both connection start and end * (React Flow handles are connectable both ways by default) — paired with * connectionMode="loose" on so any point can link to any point. */ function ShapeHandles() { return ( <> {HANDLE_POSITIONS.map(({ id, position }) => ( ))} ); } function SubProcessBadge({ id }: { id: string }) { const withChildren = useContext(NodesWithChildrenContext); if (!withChildren.has(id)) return null; return ( ⊞ ); } function ShapeResizer({ selected, minWidth, minHeight, keepAspectRatio = true, }: { selected: boolean; minWidth: number; minHeight: number; keepAspectRatio?: boolean; }) { const mode = useContext(ModeContext); return ( ); } function TaskNode({ id, data, selected }: NodeProps) { return (
{data.label}
); } const EVENT_BORDER_CLASS: Record<'startEvent' | 'intermediateEvent' | 'endEvent', string> = { startEvent: 'diagram-node--startEvent', intermediateEvent: 'diagram-node--intermediateEvent', endEvent: 'diagram-node--endEvent', }; function EventNode({ id, data, type, selected }: NodeProps) { const kind = (type as 'startEvent' | 'intermediateEvent' | 'endEvent') ?? 'startEvent'; return (
{data.label}
); } const GATEWAY_ICON: Record<'exclusiveGateway' | 'parallelGateway' | 'inclusiveGateway', string> = { exclusiveGateway: '✕', parallelGateway: '+', inclusiveGateway: '○', }; function GatewayNode({ id, data, type, selected }: NodeProps) { const kind = (type as 'exclusiveGateway' | 'parallelGateway' | 'inclusiveGateway') ?? 'exclusiveGateway'; return (
{/* The diamond is drawn with clip-path on a plain (unrotated) box, not `transform: rotate(45deg)` — a rotated element makes React Flow's getBoundingClientRect-based handle measurement pick up sub-pixel rounding from the rotation, which showed up as a visible kink in otherwise-straight horizontal/vertical edges. With clip-path, the standard Position.Top/Right/Bottom/Left handles already land exactly on the diamond's tips (they're at the box's edge midpoints either way), so this reuses the same ShapeHandles as every other shape. Handles/badge/caption are siblings of the clipped div, not children of it, since clip-path would otherwise cut off anything of theirs that renders outside the diamond outline. */}
{GATEWAY_ICON[kind]} {data.label}
); } export const nodeTypes = { task: TaskNode, startEvent: EventNode, intermediateEvent: EventNode, endEvent: EventNode, exclusiveGateway: GatewayNode, parallelGateway: GatewayNode, inclusiveGateway: GatewayNode, };