@elabs-ai/components-flow 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +79 -0
- package/dist/index.d.ts +634 -0
- package/dist/index.js +1577 -0
- package/dist/index.js.map +1 -0
- package/package.json +68 -0
- package/src/canvas-shell/canvas-shell.stories.tsx +77 -0
- package/src/canvas-shell/canvas-shell.test.tsx +106 -0
- package/src/canvas-shell/canvas-shell.tsx +128 -0
- package/src/canvas-shell/index.ts +1 -0
- package/src/flow-button-edge/flow-button-edge.stories.tsx +151 -0
- package/src/flow-button-edge/flow-button-edge.test.tsx +99 -0
- package/src/flow-button-edge/flow-button-edge.tsx +80 -0
- package/src/flow-button-edge/index.ts +5 -0
- package/src/flow-edge/flow-edge.stories.tsx +79 -0
- package/src/flow-edge/flow-edge.test.tsx +76 -0
- package/src/flow-edge/flow-edge.tsx +34 -0
- package/src/flow-edge/index.ts +1 -0
- package/src/flow-floating-edge/floating-edge-geometry.test.ts +100 -0
- package/src/flow-floating-edge/floating-edge-geometry.ts +128 -0
- package/src/flow-floating-edge/flow-floating-edge.stories.tsx +81 -0
- package/src/flow-floating-edge/flow-floating-edge.tsx +82 -0
- package/src/flow-floating-edge/index.ts +6 -0
- package/src/flow-group-node/flow-group-node.stories.tsx +283 -0
- package/src/flow-group-node/flow-group-node.test.tsx +100 -0
- package/src/flow-group-node/flow-group-node.tsx +134 -0
- package/src/flow-group-node/index.ts +6 -0
- package/src/flow-layout/flow-layout.stories.tsx +215 -0
- package/src/flow-layout/flow-layout.test.tsx +133 -0
- package/src/flow-layout/flow-layout.ts +104 -0
- package/src/flow-layout/index.ts +14 -0
- package/src/flow-layout/layout-graph.test.ts +257 -0
- package/src/flow-layout/layout-graph.ts +302 -0
- package/src/flow-layout/use-auto-layout.ts +26 -0
- package/src/flow-layout/use-flow-layout.ts +60 -0
- package/src/flow-mini-map/flow-mini-map.stories.tsx +74 -0
- package/src/flow-mini-map/flow-mini-map.test.tsx +56 -0
- package/src/flow-mini-map/flow-mini-map.tsx +29 -0
- package/src/flow-mini-map/index.ts +1 -0
- package/src/flow-node/flow-node.stories.tsx +143 -0
- package/src/flow-node/flow-node.test.tsx +156 -0
- package/src/flow-node/flow-node.tsx +190 -0
- package/src/flow-node/index.ts +8 -0
- package/src/flow-placeholder-node/flow-placeholder-node.stories.tsx +260 -0
- package/src/flow-placeholder-node/flow-placeholder-node.test.tsx +101 -0
- package/src/flow-placeholder-node/flow-placeholder-node.tsx +49 -0
- package/src/flow-placeholder-node/index.ts +5 -0
- package/src/flow-smart-edge/flow-smart-edge.stories.tsx +75 -0
- package/src/flow-smart-edge/flow-smart-edge.tsx +75 -0
- package/src/flow-smart-edge/index.ts +9 -0
- package/src/flow-smart-edge/smart-edge-geometry.test.ts +120 -0
- package/src/flow-smart-edge/smart-edge-geometry.ts +116 -0
- package/src/helper-lines/get-helper-lines.ts +98 -0
- package/src/helper-lines/helper-lines.stories.tsx +107 -0
- package/src/helper-lines/helper-lines.test.tsx +78 -0
- package/src/helper-lines/helper-lines.tsx +67 -0
- package/src/helper-lines/index.ts +7 -0
- package/src/helper-lines/use-helper-lines.ts +109 -0
- package/src/index.ts +37 -0
- package/src/inspector-panel/index.ts +1 -0
- package/src/inspector-panel/inspector-panel.stories.tsx +161 -0
- package/src/inspector-panel/inspector-panel.test.tsx +82 -0
- package/src/inspector-panel/inspector-panel.tsx +129 -0
- package/src/legend/index.ts +1 -0
- package/src/legend/legend.stories.tsx +56 -0
- package/src/legend/legend.test.tsx +46 -0
- package/src/legend/legend.tsx +39 -0
- package/src/templates-flow-workspace.stories.tsx +169 -0
- package/src/use-flow-groups/group-operations.test.ts +318 -0
- package/src/use-flow-groups/group-operations.ts +349 -0
- package/src/use-flow-groups/index.ts +13 -0
- package/src/use-flow-groups/use-flow-groups.ts +113 -0
- package/src/zoom-controls/index.ts +1 -0
- package/src/zoom-controls/zoom-controls.stories.tsx +68 -0
- package/src/zoom-controls/zoom-controls.test.tsx +80 -0
- package/src/zoom-controls/zoom-controls.tsx +78 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/canvas-shell/canvas-shell.tsx","../src/helper-lines/helper-lines.tsx","../src/helper-lines/use-helper-lines.ts","../src/helper-lines/get-helper-lines.ts","../src/flow-node/flow-node.tsx","../src/flow-edge/flow-edge.tsx","../src/flow-smart-edge/flow-smart-edge.tsx","../src/flow-smart-edge/smart-edge-geometry.ts","../src/flow-floating-edge/flow-floating-edge.tsx","../src/flow-floating-edge/floating-edge-geometry.ts","../src/flow-placeholder-node/flow-placeholder-node.tsx","../src/flow-button-edge/flow-button-edge.tsx","../src/flow-layout/flow-layout.ts","../src/flow-layout/use-flow-layout.ts","../src/flow-layout/layout-graph.ts","../src/flow-layout/use-auto-layout.ts","../src/flow-mini-map/flow-mini-map.tsx","../src/inspector-panel/inspector-panel.tsx","../src/legend/legend.tsx","../src/zoom-controls/zoom-controls.tsx","../src/flow-group-node/flow-group-node.tsx","../src/use-flow-groups/use-flow-groups.ts","../src/use-flow-groups/group-operations.ts","../src/index.ts"],"sourcesContent":["import { type ReactNode } from \"react\";\nimport {\n Background,\n ReactFlow,\n ReactFlowProvider,\n type AriaLabelConfig,\n type Edge,\n type Node,\n type ReactFlowProps,\n} from \"@xyflow/react\";\nimport { cn } from \"@elabs-ai/components-ui/lib/cn\";\nimport { HelperLines } from \"../helper-lines/helper-lines\";\nimport { useHelperLines } from \"../helper-lines/use-helper-lines\";\n\nexport interface CanvasShellProps<\n NodeType extends Node = Node,\n EdgeType extends Edge = Edge,\n> extends ReactFlowProps<NodeType, EdgeType> {\n /** Render a branded dotted background grid. Defaults to true. */\n background?: boolean;\n /**\n * Enable alignment guides + snapping while a single node is dragged. Off by\n * default; when false, CanvasShell behaves exactly as before. When true,\n * CanvasShell wires `useHelperLines` around your `onNodesChange` and renders\n * the `<HelperLines>` overlay internally — pass `nodes` + `onNodesChange`\n * (the standard controlled setup, e.g. via `useNodesState`). For full control\n * use the exported `useHelperLines` + `<HelperLines>` directly.\n */\n helperLines?: boolean;\n /** Overlays rendered inside the flow (ZoomControls, Legend, Panels). */\n children?: ReactNode;\n className?: string;\n}\n\ntype CanvasShellInnerProps<NodeType extends Node, EdgeType extends Edge> = Omit<\n CanvasShellProps<NodeType, EdgeType>,\n \"helperLines\"\n>;\n\n/**\n * Branded defaults for React Flow's `ariaLabelConfig` (12.7+) — sentence-case\n * labels that match the rest of brand-ui's voice (see `ZoomControls`'\n * \"Zoom in\"/\"Zoom out\"/\"Fit view\"), instead of the library's Title Case\n * defaults. Callers can override any key; unset keys keep these defaults, and\n * React Flow itself fills in any key neither side sets.\n */\nconst DEFAULT_ARIA_LABEL_CONFIG: Partial<AriaLabelConfig> = {\n \"node.a11yDescription.default\":\n \"Press Enter or Space to select this node. Press Delete to remove it, Escape to cancel.\",\n \"node.a11yDescription.keyboardDisabled\":\n \"Press Enter or Space to select this node. Then use the arrow keys to move it. Press Delete to remove it, Escape to cancel.\",\n \"edge.a11yDescription.default\":\n \"Press Enter or Space to select this edge. Press Delete to remove it, Escape to cancel.\",\n \"controls.ariaLabel\": \"Canvas controls\",\n \"controls.zoomIn.ariaLabel\": \"Zoom in\",\n \"controls.zoomOut.ariaLabel\": \"Zoom out\",\n \"controls.fitView.ariaLabel\": \"Fit view\",\n \"controls.interactive.ariaLabel\": \"Toggle interactivity\",\n \"minimap.ariaLabel\": \"Minimap\",\n \"handle.ariaLabel\": \"Connection handle\",\n};\n\n/**\n * Branded React Flow canvas. Sets a token-driven background + sensible\n * defaults; pass nodes/edges/handlers through as normal React Flow props.\n *\n * Consumers MUST import the React Flow stylesheet once at the app root:\n * import \"@xyflow/react/dist/style.css\";\n *\n * Children render inside the flow context, so <ZoomControls /> and React Flow\n * <Panel>s work when placed here.\n */\nexport function CanvasShell<NodeType extends Node = Node, EdgeType extends Edge = Edge>({\n helperLines = false,\n ...props\n}: CanvasShellProps<NodeType, EdgeType>) {\n if (helperLines) {\n // A provider is needed so `useHelperLines` can read the live node store; the\n // ReactFlow rendered below connects to this same provider.\n return (\n <ReactFlowProvider>\n <CanvasShellWithHelperLines<NodeType, EdgeType> {...props} />\n </ReactFlowProvider>\n );\n }\n return <CanvasShellBase<NodeType, EdgeType> {...props} />;\n}\n\nfunction CanvasShellBase<NodeType extends Node, EdgeType extends Edge>({\n background = true,\n children,\n className,\n ariaLabelConfig,\n ...props\n}: CanvasShellInnerProps<NodeType, EdgeType>) {\n return (\n <div className={cn(\"h-full w-full bg-canvas\", className)}>\n <ReactFlow\n fitView\n proOptions={{ hideAttribution: true }}\n ariaLabelConfig={{ ...DEFAULT_ARIA_LABEL_CONFIG, ...ariaLabelConfig }}\n {...props}\n >\n {background ? <Background gap={20} size={1} color=\"var(--canvas-grid)\" /> : null}\n {children}\n </ReactFlow>\n </div>\n );\n}\n\nfunction CanvasShellWithHelperLines<NodeType extends Node, EdgeType extends Edge>({\n onNodesChange,\n children,\n ...props\n}: CanvasShellInnerProps<NodeType, EdgeType>) {\n const {\n onNodesChange: wrappedOnNodesChange,\n helperLineHorizontal,\n helperLineVertical,\n } = useHelperLines<NodeType>(onNodesChange);\n\n return (\n <CanvasShellBase<NodeType, EdgeType> {...props} onNodesChange={wrappedOnNodesChange}>\n {children}\n <HelperLines horizontal={helperLineHorizontal} vertical={helperLineVertical} />\n </CanvasShellBase>\n );\n}\n","\"use client\";\n\nimport { forwardRef, type SVGAttributes } from \"react\";\nimport { useViewport } from \"@xyflow/react\";\nimport { cn } from \"@elabs-ai/components-ui/lib/cn\";\n\nexport interface HelperLinesProps extends SVGAttributes<SVGSVGElement> {\n /** Absolute flow-y of the horizontal guide (from `useHelperLines`). */\n horizontal?: number;\n /** Absolute flow-x of the vertical guide (from `useHelperLines`). */\n vertical?: number;\n}\n\n/**\n * Decorative alignment-guide overlay. Draws a full-width horizontal line at\n * `horizontal` and/or a full-height vertical line at `vertical`, coloured with\n * the `--flow-helper-line` token. It reads the live viewport transform\n * (zoom + pan) so the guides sit on the real alignment position when the canvas\n * is zoomed or panned.\n *\n * Render as a child of `<CanvasShell>` / `<ReactFlow>` (it must be inside the\n * flow context to read the viewport). Purely decorative — hidden from AT.\n */\nexport const HelperLines = forwardRef<SVGSVGElement, HelperLinesProps>(function HelperLines(\n { horizontal, vertical, className, ...props },\n ref,\n) {\n const { x, y, zoom } = useViewport();\n\n if (horizontal == null && vertical == null) return null;\n\n // flow coordinate → pane pixel: coord * zoom + pan offset.\n const screenY = horizontal == null ? null : y + horizontal * zoom;\n const screenX = vertical == null ? null : x + vertical * zoom;\n\n return (\n <svg\n ref={ref}\n aria-hidden=\"true\"\n width=\"100%\"\n height=\"100%\"\n className={cn(\"pointer-events-none absolute inset-0 size-full\", className)}\n {...props}\n >\n {screenY != null ? (\n <line\n x1={0}\n x2=\"100%\"\n y1={screenY}\n y2={screenY}\n stroke=\"var(--flow-helper-line, var(--primary))\"\n strokeWidth={1}\n />\n ) : null}\n {screenX != null ? (\n <line\n x1={screenX}\n x2={screenX}\n y1={0}\n y2=\"100%\"\n stroke=\"var(--flow-helper-line, var(--primary))\"\n strokeWidth={1}\n />\n ) : null}\n </svg>\n );\n});\n","\"use client\";\n\nimport { useCallback, useState } from \"react\";\nimport { useReactFlow, type Node, type NodeChange } from \"@xyflow/react\";\nimport { getHelperLines, type HelperLineRect } from \"./get-helper-lines\";\n\nexport interface UseHelperLinesOptions {\n /** Alignment threshold in flow coordinates. Defaults to 5. */\n threshold?: number;\n}\n\nexport interface UseHelperLinesResult<NodeType extends Node = Node> {\n /**\n * Drop-in replacement for the consumer's `onNodesChange`: it snaps a dragged\n * node onto any near-aligned edge/center, then forwards the (mutated) changes\n * to the wrapped handler.\n */\n onNodesChange: (changes: NodeChange<NodeType>[]) => void;\n /** Absolute flow-y of the active horizontal guide, or `undefined`. */\n helperLineHorizontal: number | undefined;\n /** Absolute flow-x of the active vertical guide, or `undefined`. */\n helperLineVertical: number | undefined;\n}\n\nfunction nodeRect(node: Node): HelperLineRect {\n return {\n x: node.position.x,\n y: node.position.y,\n width: node.measured?.width ?? node.width ?? 0,\n height: node.measured?.height ?? node.height ?? 0,\n };\n}\n\n/**\n * Decorate a consumer's `onNodesChange` with alignment guides + snapping.\n *\n * While a **single** node is dragged, the returned handler compares its\n * left/center/right and top/middle/bottom against every other node; when a pair\n * is within `threshold` flow px it (a) snaps the dragged node's position change\n * onto the guide and (b) exposes the guide coordinates so `<HelperLines>` can\n * draw them. Guides clear when the drag ends or nothing aligns.\n *\n * Must be called inside a React Flow context (a `<ReactFlowProvider>` or a\n * component rendered under `<ReactFlow>`), since it reads the live node store.\n *\n * @example\n * const [nodes, , onNodesChange] = useNodesState(initialNodes);\n * const { onNodesChange: handle, helperLineHorizontal, helperLineVertical } =\n * useHelperLines(onNodesChange);\n * // <ReactFlow onNodesChange={handle}>\n * // <HelperLines horizontal={helperLineHorizontal} vertical={helperLineVertical} />\n */\nexport function useHelperLines<NodeType extends Node = Node>(\n onNodesChange?: (changes: NodeChange<NodeType>[]) => void,\n options: UseHelperLinesOptions = {},\n): UseHelperLinesResult<NodeType> {\n const { threshold = 5 } = options;\n const { getNodes } = useReactFlow<NodeType>();\n const [helperLineHorizontal, setHelperLineHorizontal] = useState<number>();\n const [helperLineVertical, setHelperLineVertical] = useState<number>();\n\n const handleNodesChange = useCallback(\n (changes: NodeChange<NodeType>[]) => {\n let horizontal: number | undefined;\n let vertical: number | undefined;\n\n const dragChanges = changes.filter(\n (c): c is Extract<NodeChange<NodeType>, { type: \"position\" }> =>\n c.type === \"position\" && c.dragging === true && c.position != null,\n );\n\n // Only guide/snap a single dragged node — multi-select drags are ambiguous.\n if (dragChanges.length === 1) {\n const change = dragChanges[0];\n const nodes = getNodes();\n const dragged = change ? nodes.find((n) => n.id === change.id) : undefined;\n\n if (change && change.position && dragged) {\n const draggedRect: HelperLineRect = {\n x: change.position.x,\n y: change.position.y,\n width: dragged.measured?.width ?? dragged.width ?? 0,\n height: dragged.measured?.height ?? dragged.height ?? 0,\n };\n const others = nodes.filter((n) => n.id !== change.id).map(nodeRect);\n const {\n snapX,\n snapY,\n vertical: v,\n horizontal: h,\n } = getHelperLines(draggedRect, others, threshold);\n\n // Mutate the position change so the node lands exactly on the guide.\n if (snapX !== undefined) change.position.x = snapX;\n if (snapY !== undefined) change.position.y = snapY;\n vertical = v;\n horizontal = h;\n }\n }\n\n setHelperLineHorizontal(horizontal);\n setHelperLineVertical(vertical);\n onNodesChange?.(changes);\n },\n [getNodes, onNodesChange, threshold],\n );\n\n return { onNodesChange: handleNodesChange, helperLineHorizontal, helperLineVertical };\n}\n","/**\n * Pure alignment-guide geometry for the helper-lines feature.\n *\n * Implemented fresh from the documented \"helper lines\" behaviour (the React Flow\n * Pro example is paid — only the concept is reused): while a single node drags,\n * compare its left / horizontal-center / right against every other node's\n * left / center / right (and top / middle / bottom on the vertical axis). When a\n * pair falls within `threshold` flow-coordinate pixels, record the guide's\n * absolute coordinate and the snapped top-left position for the dragged node.\n *\n * Everything here is a pure function of plain rectangles so it is unit-testable\n * without a live React Flow instance.\n */\n\n/** A node's bounding box in absolute flow coordinates (top-left origin). */\nexport interface HelperLineRect {\n /** Left edge (top-left x) in flow coordinates. */\n x: number;\n /** Top edge (top-left y) in flow coordinates. */\n y: number;\n width: number;\n height: number;\n}\n\nexport interface HelperLinesResult {\n /** New top-left x for the dragged node once snapped to the vertical guide. */\n snapX?: number;\n /** New top-left y for the dragged node once snapped to the horizontal guide. */\n snapY?: number;\n /** Absolute flow-x of the active vertical guide line, if any. */\n vertical?: number;\n /** Absolute flow-y of the active horizontal guide line, if any. */\n horizontal?: number;\n}\n\n/**\n * Fractional anchor offsets along an axis: 0 = start edge (left/top),\n * 0.5 = center/middle, 1 = end edge (right/bottom). Comparing all three anchors\n * of the dragged node against all three of every other node yields\n * edge-to-edge, edge-to-center and center-to-center alignment.\n */\nconst ANCHORS = [0, 0.5, 1] as const;\n\n/**\n * Compute the closest vertical + horizontal alignment guides (and the snapped\n * position) for a dragged rectangle against the other rectangles.\n *\n * @param dragged the dragged node's proposed bounding box (flow coordinates)\n * @param others every other node's bounding box\n * @param threshold max distance (flow px) at which alignment engages; a pair at\n * exactly `threshold` does NOT match (strict boundary)\n */\nexport function getHelperLines(\n dragged: HelperLineRect,\n others: Iterable<HelperLineRect>,\n threshold = 5,\n): HelperLinesResult {\n const result: HelperLinesResult = {};\n // Track the closest match on each axis; initialising to `threshold` makes the\n // boundary strict (a distance equal to threshold never wins).\n let bestVerticalDist = threshold;\n let bestHorizontalDist = threshold;\n\n // Each anchor as { a: fractional offset, pos: absolute coordinate }.\n const draggedX = ANCHORS.map((a) => ({ a, pos: dragged.x + dragged.width * a }));\n const draggedY = ANCHORS.map((a) => ({ a, pos: dragged.y + dragged.height * a }));\n\n for (const other of others) {\n const otherX = ANCHORS.map((a) => other.x + other.width * a);\n const otherY = ANCHORS.map((a) => other.y + other.height * a);\n\n // Vertical guide (align on the X axis).\n for (const d of draggedX) {\n for (const oPos of otherX) {\n const dx = Math.abs(d.pos - oPos);\n if (dx < bestVerticalDist) {\n bestVerticalDist = dx;\n result.vertical = oPos;\n result.snapX = oPos - dragged.width * d.a;\n }\n }\n }\n\n // Horizontal guide (align on the Y axis).\n for (const d of draggedY) {\n for (const oPos of otherY) {\n const dy = Math.abs(d.pos - oPos);\n if (dy < bestHorizontalDist) {\n bestHorizontalDist = dy;\n result.horizontal = oPos;\n result.snapY = oPos - dragged.height * d.a;\n }\n }\n }\n }\n\n return result;\n}\n","import { type ReactNode } from \"react\";\nimport { Handle, Position, type Node, type NodeProps } from \"@xyflow/react\";\nimport { Star, type LucideIcon } from \"lucide-react\";\nimport { STATUS_TONE_ICONS } from \"@elabs-ai/components-ui\";\nimport { cn } from \"@elabs-ai/components-ui/lib/cn\";\n\n/** A node side that can carry a handle. Doubles as the handle's stable id. */\nexport type FlowHandleSide = \"top\" | \"right\" | \"bottom\" | \"left\";\n\n/**\n * Which sides of a node expose source and/or target handles. When omitted,\n * `FlowNode` keeps its default single top **target** + bottom **source**. When\n * set, a `<Handle>` is rendered on each listed side with a **stable id equal to\n * the side name** (`\"top\" | \"right\" | \"bottom\" | \"left\"`), addressable per\n * handle type — so edges (e.g. `FlowSmartEdge`) can pick a specific anchor.\n */\nexport interface FlowNodeHandles {\n /** Sides exposing a source handle (handle `id` === side). */\n source?: FlowHandleSide[];\n /** Sides exposing a target handle (handle `id` === side). */\n target?: FlowHandleSide[];\n}\n\n/**\n * A `FlowNodeHandles` config exposing source **and** target anchors on all four\n * sides. The recommended setup for `FlowSmartEdge` / `FlowFloatingEdge`: with\n * anchors on every side, an edge connects on whichever side **faces** the other\n * node (left/right when side-by-side, top/bottom when stacked) instead of always\n * top/bottom. Spread onto a node's `data.handles`.\n */\nexport const FLOW_ALL_SIDE_HANDLES: FlowNodeHandles = {\n source: [\"top\", \"right\", \"bottom\", \"left\"],\n target: [\"top\", \"right\", \"bottom\", \"left\"],\n};\n\nexport interface FlowNodeData extends Record<string, unknown> {\n title: string;\n subtitle?: string;\n /** Short type label shown as an eyebrow, e.g. \"Source\", \"Transform\". */\n kind?: string;\n icon?: ReactNode;\n tone?: \"default\" | \"accent\" | \"success\" | \"warning\" | \"destructive\";\n /**\n * Optional multi-side handle configuration. Absent → default top-target /\n * bottom-source (unchanged, backward-compatible).\n */\n handles?: FlowNodeHandles;\n}\n\nexport type BrandFlowNode = Node<FlowNodeData, \"brand\">;\n\nconst toneRing: Record<NonNullable<FlowNodeData[\"tone\"]>, string> = {\n default: \"border-border\",\n accent: \"border-primary\",\n success: \"border-success\",\n warning: \"border-warning\",\n destructive: \"border-destructive\",\n};\n\n/**\n * `tone` used to be encoded in colour ALONE — a 1px border and nothing else\n * (WCAG 1.4.1, #387). Every non-default tone now also gets a leading Lucide\n * glyph (`aria-hidden`, decorative — the sr-only text below carries the\n * meaning) and a distinct accessible name. `success`/`warning`/`destructive`\n * reuse `@elabs-ai/components-ui`'s `STATUS_TONE_ICONS` (the SAME glyph\n * `StatusBadge`/`StatusIcon` already pair with that tone) rather than\n * inventing a second icon vocabulary; `accent` has no canonical `StatusTone`\n * counterpart to reuse (the closed `StatusTone` enum is deliberately\n * CALM-ONLY and excludes a \"primary/highlighted\" bucket — see\n * `status-badge.tsx`'s docblock), so it gets the one net-new pairing.\n * `default` keeps neither — reaching for a glyph/label on every ordinary node\n * would be visual and assistive-tech noise on the common case.\n */\nconst toneIcon: Partial<Record<NonNullable<FlowNodeData[\"tone\"]>, LucideIcon>> = {\n accent: Star,\n success: STATUS_TONE_ICONS.success,\n warning: STATUS_TONE_ICONS.warning,\n destructive: STATUS_TONE_ICONS.destructive,\n};\n\n/** Ink for the tone glyph — the coloured-text rung (`-text`), not the fill rung. */\nconst toneIconColor: Partial<Record<NonNullable<FlowNodeData[\"tone\"]>, string>> = {\n accent: \"text-primary\",\n success: \"text-success-text\",\n warning: \"text-warning-text\",\n destructive: \"text-destructive-text\",\n};\n\n/** The accessible name the tone glyph stands in for (`sr-only`, #387). */\nconst toneLabel: Partial<Record<NonNullable<FlowNodeData[\"tone\"]>, string>> = {\n accent: \"Highlighted\",\n success: \"Success\",\n warning: \"Warning\",\n destructive: \"Destructive\",\n};\n\nconst sidePosition: Record<FlowHandleSide, Position> = {\n top: Position.Top,\n right: Position.Right,\n bottom: Position.Bottom,\n left: Position.Left,\n};\n\nconst handleClassName = \"!size-2 !border-2 !border-flow-edge !bg-flow-node\";\n\n/**\n * Branded custom node. Register it in `nodeTypes={{ brand: FlowNode }}` and\n * create nodes with `type: \"brand\"` and `data: FlowNodeData`.\n */\nexport function FlowNode({\n data,\n selected,\n sourcePosition,\n targetPosition,\n}: NodeProps<BrandFlowNode>) {\n const tone = data.tone ?? \"default\";\n const ToneIcon = toneIcon[tone];\n return (\n <div\n data-tone={tone}\n className={cn(\n \"min-w-44 rounded-lg border bg-flow-node px-3 py-2 text-flow-node-foreground shadow-sm transition-[box-shadow,border-color] duration-fast ease-standard\",\n toneRing[tone],\n selected && \"ring-2 ring-ring\",\n )}\n >\n {data.handles ? (\n <>\n {(data.handles.target ?? []).map((side) => (\n <Handle\n key={`target-${side}`}\n id={side}\n type=\"target\"\n position={sidePosition[side]}\n className={handleClassName}\n />\n ))}\n {(data.handles.source ?? []).map((side) => (\n <Handle\n key={`source-${side}`}\n id={side}\n type=\"source\"\n position={sidePosition[side]}\n className={handleClassName}\n />\n ))}\n </>\n ) : (\n <>\n {/* Default single target + source. Positions follow the layout\n direction: React Flow passes `sourcePosition`/`targetPosition`\n (which `layoutFlow` sets per direction), so an LR layout puts the\n target on the LEFT and the source on the RIGHT. Falls back to\n top-in / bottom-out when unset. Handle placement is by `position`,\n not DOM order. */}\n <Handle\n type=\"target\"\n position={targetPosition ?? Position.Top}\n className={handleClassName}\n />\n <Handle\n type=\"source\"\n position={sourcePosition ?? Position.Bottom}\n className={handleClassName}\n />\n </>\n )}\n <div className=\"flex items-center gap-2\">\n {data.icon ? (\n <span className=\"[&_svg]:size-4 text-muted-foreground\">{data.icon}</span>\n ) : null}\n <div className=\"min-w-0 flex-1\">\n {data.kind ? (\n <div className=\"text-[10px] font-medium uppercase tracking-wide text-muted-foreground\">\n {data.kind}\n </div>\n ) : null}\n <div className=\"truncate text-sm font-medium\">{data.title}</div>\n {data.subtitle ? (\n <div className=\"truncate text-xs text-muted-foreground\">{data.subtitle}</div>\n ) : null}\n </div>\n {ToneIcon ? (\n <ToneIcon aria-hidden=\"true\" className={cn(\"size-3.5 shrink-0\", toneIconColor[tone])} />\n ) : null}\n </div>\n {toneLabel[tone] ? <span className=\"sr-only\">{toneLabel[tone]}</span> : null}\n </div>\n );\n}\n","import { BaseEdge, getBezierPath, type EdgeProps } from \"@xyflow/react\";\n\n/**\n * Branded bezier edge using the `--flow-edge` token. Register it in\n * `edgeTypes={{ brand: FlowEdge }}` and create edges with `type: \"brand\"`.\n */\nexport function FlowEdge({\n id,\n sourceX,\n sourceY,\n targetX,\n targetY,\n sourcePosition,\n targetPosition,\n markerEnd,\n style,\n}: EdgeProps) {\n const [edgePath] = getBezierPath({\n sourceX,\n sourceY,\n targetX,\n targetY,\n sourcePosition,\n targetPosition,\n });\n return (\n <BaseEdge\n id={id}\n path={edgePath}\n markerEnd={markerEnd}\n style={{ stroke: \"var(--flow-edge)\", strokeWidth: 1.5, ...style }}\n />\n );\n}\n","import {\n BaseEdge,\n getBezierPath,\n useInternalNode,\n type EdgeProps,\n type InternalNode,\n} from \"@xyflow/react\";\nimport type { FlowNodeData } from \"../flow-node\";\nimport {\n pickClosestHandles,\n rectCenter,\n sideToPosition,\n slideAnchor,\n type NodeRect,\n} from \"./smart-edge-geometry\";\n\nfunction toRect(node: InternalNode): NodeRect {\n return {\n x: node.internals.positionAbsolute.x,\n y: node.internals.positionAbsolute.y,\n width: node.measured.width ?? 0,\n height: node.measured.height ?? 0,\n };\n}\n\n/**\n * Branded edge that picks the closest source/target handle pair (from a node's\n * multi-side `FlowNodeHandles` config) and routes a bezier between them. Anchors\n * are recomputed every render, so they flip as nodes are dragged. Register it in\n * `edgeTypes={{ smart: FlowSmartEdge }}` and give the connected nodes a\n * `data.handles` config. Uses the `--flow-edge` token, matching `FlowEdge`.\n */\nexport function FlowSmartEdge({ id, source, target, markerEnd, style }: EdgeProps) {\n const sourceNode = useInternalNode(source);\n const targetNode = useInternalNode(target);\n if (!sourceNode || !targetNode) return null;\n\n const sourceData = sourceNode.internals.userNode.data as FlowNodeData | undefined;\n const targetData = targetNode.internals.userNode.data as FlowNodeData | undefined;\n\n const sourceRect = toRect(sourceNode);\n const targetRect = toRect(targetNode);\n\n // Pick which side of each node the edge exits/enters (the facing handle side).\n const { sourceSide, targetSide } = pickClosestHandles(\n sourceRect,\n sourceData?.handles?.source ?? [],\n targetRect,\n targetData?.handles?.target ?? [],\n );\n\n // Then slide each anchor along its chosen side toward the OTHER node, so\n // edges sharing a side fan out to distinct, target-facing points instead of\n // stacking on the side midpoint.\n const sourceAnchor = slideAnchor(sourceRect, sourceSide, rectCenter(targetRect));\n const targetAnchor = slideAnchor(targetRect, targetSide, rectCenter(sourceRect));\n\n const [edgePath] = getBezierPath({\n sourceX: sourceAnchor.x,\n sourceY: sourceAnchor.y,\n sourcePosition: sideToPosition[sourceSide],\n targetX: targetAnchor.x,\n targetY: targetAnchor.y,\n targetPosition: sideToPosition[targetSide],\n });\n\n return (\n <BaseEdge\n id={id}\n path={edgePath}\n markerEnd={markerEnd}\n style={{ stroke: \"var(--flow-edge)\", strokeWidth: 1.5, ...style }}\n />\n );\n}\n","import { Position } from \"@xyflow/react\";\nimport type { FlowHandleSide } from \"../flow-node\";\n\n/** Axis-aligned node rectangle in absolute flow coordinates. */\nexport interface NodeRect {\n x: number;\n y: number;\n width: number;\n height: number;\n}\n\n/** All four candidate handle sides, in a stable order. */\nexport const HANDLE_SIDES: FlowHandleSide[] = [\"top\", \"right\", \"bottom\", \"left\"];\n\n/** Maps a handle side to the React Flow `Position` used for bezier control. */\nexport const sideToPosition: Record<FlowHandleSide, Position> = {\n top: Position.Top,\n right: Position.Right,\n bottom: Position.Bottom,\n left: Position.Left,\n};\n\n/** Center point of a node rectangle. */\nexport function rectCenter(rect: NodeRect): { x: number; y: number } {\n return { x: rect.x + rect.width / 2, y: rect.y + rect.height / 2 };\n}\n\n/**\n * The connection point on `side` of `rect`, slid along that side toward\n * `toward` and clamped `inset` px from the corners. This is what stops two\n * edges that both leave the same side (e.g. both targets sit to the right)\n * from piling onto the side's midpoint: each edge's anchor slides toward its\n * own target, so an up-going edge exits the upper part of the side and a\n * down-going edge the lower part — each visibly meeting the node on the side\n * that faces its target instead of stacking in the middle.\n */\nexport function slideAnchor(\n rect: NodeRect,\n side: FlowHandleSide,\n toward: { x: number; y: number },\n inset = 12,\n): { x: number; y: number } {\n const clamp = (v: number, lo: number, hi: number) => Math.max(lo, Math.min(hi, v));\n // Never let the inset invert the usable range on a small node.\n const ix = Math.min(inset, rect.width / 2);\n const iy = Math.min(inset, rect.height / 2);\n switch (side) {\n case \"top\":\n return { x: clamp(toward.x, rect.x + ix, rect.x + rect.width - ix), y: rect.y };\n case \"bottom\":\n return { x: clamp(toward.x, rect.x + ix, rect.x + rect.width - ix), y: rect.y + rect.height };\n case \"left\":\n return { x: rect.x, y: clamp(toward.y, rect.y + iy, rect.y + rect.height - iy) };\n case \"right\":\n return { x: rect.x + rect.width, y: clamp(toward.y, rect.y + iy, rect.y + rect.height - iy) };\n }\n}\n\n/** Absolute coordinate of a handle on the given side (the side's midpoint). */\nexport function handlePoint(rect: NodeRect, side: FlowHandleSide): { x: number; y: number } {\n switch (side) {\n case \"top\":\n return { x: rect.x + rect.width / 2, y: rect.y };\n case \"bottom\":\n return { x: rect.x + rect.width / 2, y: rect.y + rect.height };\n case \"left\":\n return { x: rect.x, y: rect.y + rect.height / 2 };\n case \"right\":\n return { x: rect.x + rect.width, y: rect.y + rect.height / 2 };\n }\n}\n\n/** The chosen source/target handle pair plus their absolute anchor points. */\nexport interface ClosestHandles {\n sourceSide: FlowHandleSide;\n targetSide: FlowHandleSide;\n sx: number;\n sy: number;\n tx: number;\n ty: number;\n}\n\n/**\n * Picks the source/target handle pair (one handle per node) with the shortest\n * straight-line distance between their anchor points. Empty side lists fall\n * back to all four sides so an edge always resolves to a pair.\n */\nexport function pickClosestHandles(\n source: NodeRect,\n sourceSides: FlowHandleSide[],\n target: NodeRect,\n targetSides: FlowHandleSide[],\n): ClosestHandles {\n const srcSides = sourceSides.length ? sourceSides : HANDLE_SIDES;\n const tgtSides = targetSides.length ? targetSides : HANDLE_SIDES;\n\n let best: ClosestHandles | undefined;\n let bestDist = Infinity;\n\n for (const s of srcSides) {\n const sp = handlePoint(source, s);\n for (const t of tgtSides) {\n const tp = handlePoint(target, t);\n const dx = tp.x - sp.x;\n const dy = tp.y - sp.y;\n const dist = dx * dx + dy * dy;\n if (dist < bestDist) {\n bestDist = dist;\n best = { sourceSide: s, targetSide: t, sx: sp.x, sy: sp.y, tx: tp.x, ty: tp.y };\n }\n }\n }\n\n // srcSides/tgtSides are non-empty (fallback above), so best is always set.\n return best as ClosestHandles;\n}\n","import { BaseEdge, getBezierPath, useInternalNode, type EdgeProps } from \"@xyflow/react\";\nimport { getEdgeParams } from \"./floating-edge-geometry\";\n\n/** Optional per-edge `data` for {@link FlowFloatingEdge}. */\nexport interface FloatingEdgeData {\n /**\n * Show a small anchor dot at each border connection point. On by default so the\n * edge visibly terminates on the node's closest side (rather than a bare line\n * touching the border with no anchor). Set `false` to hide.\n */\n anchors?: boolean;\n [key: string]: unknown;\n}\n\n/** Radius of the connection anchor dot (matches the `FlowNode` handle size). */\nconst ANCHOR_RADIUS = 4;\n\n/**\n * Branded floating edge: it attaches to the node **border** at the point facing\n * the other node (no fixed handle), recomputed as nodes drag. Register it in\n * `edgeTypes={{ floating: FlowFloatingEdge }}`; the connected nodes need no\n * handle config. Uses the `--flow-edge` token, matching `FlowEdge`.\n *\n * A small **anchor dot** is drawn at each connection point (on by default) so the\n * line clearly terminates on the node's closest border side — not at a bare,\n * unanchored spot. Toggle per edge with `data.anchors: false`.\n */\nexport function FlowFloatingEdge({ id, source, target, markerEnd, style, data }: EdgeProps) {\n const sourceNode = useInternalNode(source);\n const targetNode = useInternalNode(target);\n if (!sourceNode || !targetNode) return null;\n\n const { sx, sy, tx, ty, sourcePos, targetPos } = getEdgeParams(sourceNode, targetNode);\n\n const [edgePath] = getBezierPath({\n sourceX: sx,\n sourceY: sy,\n sourcePosition: sourcePos,\n targetX: tx,\n targetY: ty,\n targetPosition: targetPos,\n });\n\n const showAnchors = (data as FloatingEdgeData | undefined)?.anchors !== false;\n\n return (\n <>\n <BaseEdge\n id={id}\n path={edgePath}\n markerEnd={markerEnd}\n style={{ stroke: \"var(--flow-edge)\", strokeWidth: 1.5, ...style }}\n />\n {showAnchors ? (\n <g\n className=\"brand-floating-edge__anchors\"\n fill=\"var(--flow-node)\"\n stroke=\"var(--flow-edge)\"\n >\n {/* Decorative rings matching the FlowNode handle look; never intercept\n pointer events so nodes stay draggable through them. */}\n <circle\n cx={sx}\n cy={sy}\n r={ANCHOR_RADIUS}\n strokeWidth={2}\n style={{ pointerEvents: \"none\" }}\n aria-hidden=\"true\"\n />\n <circle\n cx={tx}\n cy={ty}\n r={ANCHOR_RADIUS}\n strokeWidth={2}\n style={{ pointerEvents: \"none\" }}\n aria-hidden=\"true\"\n />\n </g>\n ) : null}\n </>\n );\n}\n","import { Position } from \"@xyflow/react\";\n\n/**\n * The minimal live node geometry `getEdgeParams` needs. A React Flow\n * `InternalNode` satisfies this structurally (`internals.positionAbsolute` +\n * `measured`), so you can pass one directly; tests can pass a plain fake.\n */\nexport interface FloatingNodeGeometry {\n internals: { positionAbsolute: { x: number; y: number } };\n measured: { width?: number; height?: number };\n}\n\ninterface Point {\n x: number;\n y: number;\n}\n\n/**\n * The point where the line from `intersectionNode`'s center to `targetNode`'s\n * center crosses `intersectionNode`'s rectangle border. Standard React Flow\n * floating-edge math (ellipse-normalized).\n */\nfunction getNodeIntersection(\n intersectionNode: FloatingNodeGeometry,\n targetNode: FloatingNodeGeometry,\n): Point {\n const w = (intersectionNode.measured.width ?? 0) / 2;\n const h = (intersectionNode.measured.height ?? 0) / 2;\n const pos = intersectionNode.internals.positionAbsolute;\n const targetPos = targetNode.internals.positionAbsolute;\n\n const x2 = pos.x + w;\n const y2 = pos.y + h;\n const x1 = targetPos.x + (targetNode.measured.width ?? 0) / 2;\n const y1 = targetPos.y + (targetNode.measured.height ?? 0) / 2;\n\n // A not-yet-measured node has w/h = 0; guard the divisions so we return the\n // node's position (finite) instead of NaN (which would break the edge path and\n // make the line appear to end at a random spot until measurement lands).\n const safeW = w || 1;\n const safeH = h || 1;\n\n const xx1 = (x1 - x2) / (2 * safeW) - (y1 - y2) / (2 * safeH);\n const yy1 = (x1 - x2) / (2 * safeW) + (y1 - y2) / (2 * safeH);\n const a = 1 / (Math.abs(xx1) + Math.abs(yy1) || 1);\n const xx3 = a * xx1;\n const yy3 = a * yy1;\n const x = safeW * (xx3 + yy3) + x2;\n const y = safeH * (-xx3 + yy3) + y2;\n\n return { x, y };\n}\n\n/**\n * Snap `point` exactly onto `node`'s nearest border side. The intersection math\n * already lands on the border for a measured node; this guarantees it (removing\n * any sub-pixel drift that would leave the anchor a hair inside the node) and\n * gives a sensible on-border fallback if a point ever lands inside. Returns the\n * point unchanged when the node has no measured size yet.\n */\nfunction clampToBorder(point: Point, node: FloatingNodeGeometry): Point {\n const x0 = node.internals.positionAbsolute.x;\n const y0 = node.internals.positionAbsolute.y;\n const w = node.measured.width ?? 0;\n const h = node.measured.height ?? 0;\n if (w <= 0 || h <= 0) return point;\n\n const cx = Math.min(Math.max(point.x, x0), x0 + w);\n const cy = Math.min(Math.max(point.y, y0), y0 + h);\n // Distance from the (clamped-inside) point to each side; snap to the nearest.\n const dLeft = cx - x0;\n const dRight = x0 + w - cx;\n const dTop = cy - y0;\n const dBottom = y0 + h - cy;\n const nearest = Math.min(dLeft, dRight, dTop, dBottom);\n if (nearest === dLeft) return { x: x0, y: cy };\n if (nearest === dRight) return { x: x0 + w, y: cy };\n if (nearest === dTop) return { x: cx, y: y0 };\n return { x: cx, y: y0 + h };\n}\n\n/** Which border of `node` the intersection `point` sits on. */\nfunction getEdgeBorderPosition(node: FloatingNodeGeometry, point: Point): Position {\n const nx = Math.round(node.internals.positionAbsolute.x);\n const ny = Math.round(node.internals.positionAbsolute.y);\n const px = Math.round(point.x);\n const py = Math.round(point.y);\n const w = node.measured.width ?? 0;\n const h = node.measured.height ?? 0;\n\n if (px <= nx + 1) return Position.Left;\n if (px >= nx + w - 1) return Position.Right;\n if (py <= ny + 1) return Position.Top;\n if (py >= ny + h - 1) return Position.Bottom;\n return Position.Top;\n}\n\n/** Border anchor points + `Position`s for a floating edge between two nodes. */\nexport interface EdgeParams {\n sx: number;\n sy: number;\n tx: number;\n ty: number;\n sourcePos: Position;\n targetPos: Position;\n}\n\n/**\n * Computes where a floating edge attaches to each node's border — the\n * intersection of the center-to-center line with each rectangle — plus the\n * border `Position`. Recompute per render so anchors track dragging nodes.\n */\nexport function getEdgeParams(\n source: FloatingNodeGeometry,\n target: FloatingNodeGeometry,\n): EdgeParams {\n const sourceIntersection = clampToBorder(getNodeIntersection(source, target), source);\n const targetIntersection = clampToBorder(getNodeIntersection(target, source), target);\n\n return {\n sx: sourceIntersection.x,\n sy: sourceIntersection.y,\n tx: targetIntersection.x,\n ty: targetIntersection.y,\n sourcePos: getEdgeBorderPosition(source, sourceIntersection),\n targetPos: getEdgeBorderPosition(target, targetIntersection),\n };\n}\n","import { Handle, Position, type Node, type NodeProps } from \"@xyflow/react\";\nimport { Plus } from \"lucide-react\";\nimport { cn } from \"@elabs-ai/components-ui/lib/cn\";\n\nexport interface FlowPlaceholderNodeData extends Record<string, unknown> {\n /** Label rendered inside the placeholder. @default \"Add node\" */\n label?: string;\n /** Called when the placeholder is activated (click, Enter, or Space). */\n onActivate?: () => void;\n}\n\nexport type BrandFlowPlaceholderNode = Node<FlowPlaceholderNodeData, \"placeholder\">;\n\n/**\n * Dashed, muted \"add here\" node — a clickable affordance that grows the\n * graph. Register it in `nodeTypes={{ placeholder: FlowPlaceholderNode }}`\n * and create nodes with `type: \"placeholder\"` and `data: FlowPlaceholderNodeData`.\n *\n * Renders a real `<button>` (keyboard-activatable via native Enter/Space) with\n * an `aria-label`, and a single **target** `<Handle>` (top) so an existing\n * edge can point at it. Fires `data.onActivate?.()` on activation — the\n * typical handler converts the placeholder into a real `FlowNode` and grows a\n * fresh placeholder beneath it (see the \"Placeholder tail\" story).\n */\nexport function FlowPlaceholderNode({ data }: NodeProps<BrandFlowPlaceholderNode>) {\n const label = data.label ?? \"Add node\";\n return (\n <div className=\"relative\">\n <Handle\n type=\"target\"\n position={Position.Top}\n className=\"!size-2 !border-2 !border-flow-edge !bg-flow-node\"\n />\n <button\n type=\"button\"\n aria-label={label}\n onClick={() => data.onActivate?.()}\n className={cn(\n \"flex min-w-44 items-center justify-center gap-1.5 rounded-lg border border-dashed border-flow-group-border bg-flow-group px-3 py-2 text-muted-foreground\",\n \"transition-colors duration-fast ease-standard hover:bg-accent hover:text-accent-foreground\",\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\",\n )}\n >\n <Plus className=\"size-4\" aria-hidden=\"true\" />\n <span className=\"text-body\">{label}</span>\n </button>\n </div>\n );\n}\n","import {\n BaseEdge,\n EdgeLabelRenderer,\n getBezierPath,\n type Edge,\n type EdgeProps,\n} from \"@xyflow/react\";\nimport { Plus } from \"lucide-react\";\n\nexport interface FlowButtonEdgeData extends Record<string, unknown> {\n /** aria-label for the insert button. @default \"Insert node on edge\" */\n label?: string;\n /** Called when the edge's \"+\" button is activated (click, Enter, or Space). */\n onInsert?: () => void;\n}\n\nexport type BrandFlowButtonEdge = Edge<FlowButtonEdgeData, \"button\">;\n\n/**\n * Branded bezier edge (matching `FlowEdge`'s `--flow-edge` token) with a\n * centered \"+\" button rendered via `EdgeLabelRenderer` at the edge midpoint.\n * Register it in `edgeTypes={{ button: FlowButtonEdge }}` and create edges\n * with `type: \"button\"` and `data: FlowButtonEdgeData`.\n *\n * The button is a real `<button>` with an `aria-label`, keyboard-activatable\n * via native Enter/Space. Fires `data.onInsert?.()` on activation — the\n * typical handler splits the edge: insert a new node between source and\n * target and rewire the two edges (see the \"Insert between\" story).\n */\nexport function FlowButtonEdge({\n id,\n sourceX,\n sourceY,\n targetX,\n targetY,\n sourcePosition,\n targetPosition,\n markerEnd,\n style,\n data,\n}: EdgeProps<BrandFlowButtonEdge>) {\n const [edgePath, labelX, labelY] = getBezierPath({\n sourceX,\n sourceY,\n sourcePosition,\n targetX,\n targetY,\n targetPosition,\n });\n const label = data?.label ?? \"Insert node on edge\";\n\n return (\n <>\n <BaseEdge\n id={id}\n path={edgePath}\n markerEnd={markerEnd}\n style={{ stroke: \"var(--flow-edge)\", strokeWidth: 1.5, ...style }}\n />\n <EdgeLabelRenderer>\n <div\n style={{\n position: \"absolute\",\n transform: `translate(-50%, -50%) translate(${labelX}px, ${labelY}px)`,\n }}\n className=\"nodrag nopan pointer-events-auto\"\n >\n <button\n type=\"button\"\n aria-label={label}\n onClick={() => data?.onInsert?.()}\n className=\"flex size-5 items-center justify-center rounded-full border border-flow-group-border bg-flow-node text-flow-node-foreground shadow-sm transition-colors duration-fast ease-standard hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n >\n <Plus className=\"size-3\" aria-hidden=\"true\" />\n </button>\n </div>\n </EdgeLabelRenderer>\n </>\n );\n}\n","import dagre from \"@dagrejs/dagre\";\nimport { Position } from \"@xyflow/react\";\nimport type { Edge, Node } from \"@xyflow/react\";\n\n/** Direction dagre lays the graph out in — top-to-bottom, left-to-right, etc. */\nexport type FlowLayoutDirection = \"TB\" | \"LR\" | \"BT\" | \"RL\";\n\n/**\n * The handle sides an edge should attach to for each layout direction. A\n * directional layout must move the anchors with it: a left-to-right flow exits\n * the source's RIGHT and enters the target's LEFT; top-to-bottom uses\n * bottom/top. `layoutFlow` stamps these onto every node so `FlowNode` renders\n * its default handles on the correct sides (and `FlowEdge` routes accordingly)\n * — otherwise every layout is stuck with top/bottom handles.\n */\nexport const HANDLE_BY_DIRECTION: Record<\n FlowLayoutDirection,\n { source: Position; target: Position }\n> = {\n TB: { source: Position.Bottom, target: Position.Top },\n BT: { source: Position.Top, target: Position.Bottom },\n LR: { source: Position.Right, target: Position.Left },\n RL: { source: Position.Left, target: Position.Right },\n};\n\nexport interface FlowLayoutOptions {\n /** Layout direction. @default \"TB\" */\n direction?: FlowLayoutDirection;\n /** Gap between nodes in the same rank (dagre `nodesep`). @default 48 */\n nodeSpacing?: number;\n /** Gap between ranks (dagre `ranksep`). @default 72 */\n rankSpacing?: number;\n}\n\nexport interface FlowLayoutResult<NodeType extends Node = Node, EdgeType extends Edge = Edge> {\n nodes: NodeType[];\n edges: EdgeType[];\n}\n\n/** Fallback size used when a node hasn't been measured yet (React Flow's own default node width). */\nconst DEFAULT_NODE_WIDTH = 172;\nconst DEFAULT_NODE_HEIGHT = 40;\n\nfunction nodeSize(node: Node): { width: number; height: number } {\n return {\n width: node.measured?.width ?? node.width ?? DEFAULT_NODE_WIDTH,\n height: node.measured?.height ?? node.height ?? DEFAULT_NODE_HEIGHT,\n };\n}\n\n/**\n * Pure dagre-powered auto layout. Computes new `position`s for `nodes` given\n * `edges` — no React, no side effects, safe to call anywhere (including\n * outside a component or in a test).\n *\n * Respects each node's **measured** size (`node.measured` — how React Flow\n * v12 reports actual rendered dimensions), falling back to `node.width`/\n * `node.height`, then a sensible default. Node identity and `data` are left\n * untouched — only `position` changes.\n *\n * Pair with `useFlowLayout` to apply the result to a live canvas.\n */\nexport function layoutFlow<NodeType extends Node = Node, EdgeType extends Edge = Edge>(\n nodes: NodeType[],\n edges: EdgeType[],\n options: FlowLayoutOptions = {},\n): FlowLayoutResult<NodeType, EdgeType> {\n const { direction = \"TB\", nodeSpacing = 48, rankSpacing = 72 } = options;\n\n const graph = new dagre.graphlib.Graph();\n graph.setDefaultEdgeLabel(() => ({}));\n graph.setGraph({ rankdir: direction, nodesep: nodeSpacing, ranksep: rankSpacing });\n\n for (const node of nodes) {\n const { width, height } = nodeSize(node);\n graph.setNode(node.id, { width, height });\n }\n\n for (const edge of edges) {\n graph.setEdge(edge.source, edge.target);\n }\n\n dagre.layout(graph);\n\n const handles = HANDLE_BY_DIRECTION[direction];\n const layoutedNodes = nodes.map((node) => {\n const dagreNode = graph.node(node.id);\n const { width, height } = nodeSize(node);\n // dagre positions nodes by their center; React Flow positions by top-left.\n return {\n ...node,\n // Move the anchors with the layout direction so `FlowNode` puts its\n // handles on the sides the edges actually leave/enter.\n sourcePosition: handles.source,\n targetPosition: handles.target,\n position: {\n x: dagreNode.x - width / 2,\n y: dagreNode.y - height / 2,\n },\n };\n });\n\n return { nodes: layoutedNodes, edges };\n}\n","import { useCallback, useRef, useState } from \"react\";\nimport { useNodesInitialized, useReactFlow, type Edge, type Node } from \"@xyflow/react\";\nimport { layoutFlow, type FlowLayoutDirection, type FlowLayoutOptions } from \"./flow-layout\";\n\nexport interface UseFlowLayoutResult {\n /**\n * Auto-lay-out the current nodes/edges with dagre and fit the view. Safe to\n * call repeatedly (e.g. from a toolbar button on every click). No-ops until\n * React Flow has measured every node.\n */\n layout: (direction?: FlowLayoutDirection, options?: Omit<FlowLayoutOptions, \"direction\">) => void;\n /** Whether nodes are measured and ready to be laid out. */\n ready: boolean;\n /** Whether a layout pass is currently running (true only for the duration of `layout()`). */\n layouting: boolean;\n}\n\n/**\n * Hook form of `layoutFlow`. Reads the live nodes/edges from `useReactFlow()`,\n * waits for `useNodesInitialized()` so real (measured) node sizes are used —\n * never lays out against 0×0 unmeasured nodes — applies the computed\n * positions via `setNodes`, then calls `fitView()`.\n *\n * Must be called from a component rendered inside a `<CanvasShell>` /\n * `<ReactFlow>` (React Flow context).\n */\nexport function useFlowLayout<\n NodeType extends Node = Node,\n EdgeType extends Edge = Edge,\n>(): UseFlowLayoutResult {\n const { getNodes, getEdges, setNodes, fitView } = useReactFlow<NodeType, EdgeType>();\n const nodesInitialized = useNodesInitialized();\n const [layouting, setLayouting] = useState(false);\n // Avoid overlapping layout passes if `layout()` is invoked in quick succession.\n const runningRef = useRef(false);\n\n const layout = useCallback(\n (direction?: FlowLayoutDirection, options?: Omit<FlowLayoutOptions, \"direction\">) => {\n if (!nodesInitialized || runningRef.current) return;\n\n runningRef.current = true;\n setLayouting(true);\n try {\n const { nodes: layoutedNodes } = layoutFlow<NodeType, EdgeType>(getNodes(), getEdges(), {\n ...options,\n direction,\n });\n setNodes(layoutedNodes);\n // Defer fitView one frame so React Flow has committed the new positions first.\n requestAnimationFrame(() => fitView());\n } finally {\n runningRef.current = false;\n setLayouting(false);\n }\n },\n [nodesInitialized, getNodes, getEdges, setNodes, fitView],\n );\n\n return { layout, ready: nodesInitialized, layouting };\n}\n","import {\n forceCenter,\n forceCollide,\n forceLink,\n forceManyBody,\n forceSimulation,\n type SimulationLinkDatum,\n type SimulationNodeDatum,\n} from \"d3-force\";\nimport type { Edge, Node } from \"@xyflow/react\";\nimport { HANDLE_BY_DIRECTION, layoutFlow } from \"./flow-layout\";\n\n/** Which generic graph-geometry algorithm `layoutGraph` should run. */\nexport type LayoutAlgorithm = \"concentric\" | \"force\" | \"layered-lr\" | \"grid\";\n\n/** Horizontal/vertical gap used by the `\"grid\"` and `\"layered-lr\"` algorithms. */\nexport interface LayoutSpacing {\n x: number;\n y: number;\n}\n\nexport interface LayoutOptions {\n /** Which layout algorithm to run. */\n algorithm: LayoutAlgorithm;\n /**\n * `\"concentric\"` only — the focal node placed at the origin. Defaults to the\n * highest-degree node when omitted (or when the id doesn't match a node).\n */\n centerId?: string;\n /** `\"concentric\"` only — radius of ring 1; ring N sits at `N × ringRadius`. @default 180 */\n ringRadius?: number;\n /** `\"force\"` only — number of synchronous simulation ticks to run. @default 300 */\n iterations?: number;\n /** `\"grid\"` / `\"layered-lr\"` — gap between nodes. @default {x:200,y:120} for grid. */\n spacing?: LayoutSpacing;\n /** Resolve a node's size by id. Falls back to `measured`/`width`/`height`/a sensible default. */\n nodeSize?: (id: string) => { width: number; height: number };\n}\n\nconst DEFAULT_NODE_WIDTH = 172;\nconst DEFAULT_NODE_HEIGHT = 40;\nconst DEFAULT_RING_RADIUS = 180;\nconst DEFAULT_FORCE_ITERATIONS = 300;\nconst DEFAULT_GRID_SPACING: LayoutSpacing = { x: 200, y: 120 };\n/** Base charge/link/collide tuning — chosen to read well at brand-ui's default node size. */\nconst FORCE_CHARGE_STRENGTH = -300;\nconst FORCE_LINK_DISTANCE = 150;\n\nfunction resolveNodeSize(\n node: Node,\n nodeSize?: (id: string) => { width: number; height: number },\n): { width: number; height: number } {\n if (nodeSize) return nodeSize(node.id);\n return {\n width: node.measured?.width ?? node.width ?? DEFAULT_NODE_WIDTH,\n height: node.measured?.height ?? node.height ?? DEFAULT_NODE_HEIGHT,\n };\n}\n\n/**\n * Pure graph-geometry auto layout. Computes new `position`s for `nodes` given\n * `edges` and a `LayoutAlgorithm` — no React, no DOM, no side effects, safe to\n * call anywhere (including outside a component or in a test). Deterministic:\n * identical `nodes`/`edges`/`options` always produce identical positions.\n *\n * Node identity and `data` are left untouched — only `position` changes.\n * Pair with `useAutoLayout` for a memoized hook form.\n *\n * - `\"layered-lr\"` delegates to the dagre-powered `layoutFlow` (direction `\"LR\"`).\n * - `\"concentric\"` places `centerId` (or the highest-degree node) at the\n * origin, with BFS shells at `ring × ringRadius`; disconnected nodes land in\n * one extra outer ring so positions are never `NaN`.\n * - `\"force\"` runs `d3-force` synchronously for a fixed number of `iterations`,\n * seeded on a circle by index (no randomness) so runs are reproducible.\n * - `\"grid\"` lays nodes out row-major using `spacing`.\n */\nexport function layoutGraph<NodeType extends Node = Node, EdgeType extends Edge = Edge>(\n nodes: NodeType[],\n edges: EdgeType[],\n options: LayoutOptions,\n): NodeType[] {\n if (nodes.length === 0) return [];\n\n switch (options.algorithm) {\n case \"layered-lr\":\n return layoutLayeredLr(nodes, edges, options);\n case \"concentric\":\n return layoutConcentric(nodes, edges, options);\n case \"force\":\n return layoutForce(nodes, edges, options);\n case \"grid\":\n return layoutGrid(nodes, options);\n default: {\n const exhaustiveCheck: never = options.algorithm;\n throw new Error(`layoutGraph: unknown algorithm \"${String(exhaustiveCheck)}\"`);\n }\n }\n}\n\nfunction layoutLayeredLr<NodeType extends Node, EdgeType extends Edge>(\n nodes: NodeType[],\n edges: EdgeType[],\n options: LayoutOptions,\n): NodeType[] {\n const { spacing, nodeSize } = options;\n\n // dagre (via layoutFlow) reads sizes off `measured`/`width`/`height` — feed it\n // resolved sizes on a throwaway copy so a caller-supplied `nodeSize` is honored\n // without mutating (or permanently resizing) the nodes we return.\n const sizedNodes = nodeSize\n ? nodes.map((node) => {\n const { width, height } = nodeSize(node.id);\n return { ...node, width, height, measured: { width, height } };\n })\n : nodes;\n\n const { nodes: laidOut } = layoutFlow(sizedNodes, edges, {\n direction: \"LR\",\n // For an LR flow, ranks progress horizontally (x) and nodes within a rank\n // stack vertically (y) — map spacing accordingly onto dagre's rank/node sep.\n nodeSpacing: spacing?.y,\n rankSpacing: spacing?.x,\n });\n\n // Carry the LR handle sides (right-out / left-in) that layoutFlow stamped on,\n // not just the position — otherwise the layout flips positions but leaves the\n // anchors on top/bottom.\n return nodes.map((node, i) => ({\n ...node,\n sourcePosition: laidOut[i]!.sourcePosition,\n targetPosition: laidOut[i]!.targetPosition,\n position: laidOut[i]!.position,\n }));\n}\n\nfunction resolveCenterId(\n centerId: string | undefined,\n nodeIds: string[],\n adjacency: Map<string, Set<string>>,\n): string {\n if (centerId && nodeIds.includes(centerId)) return centerId;\n\n let best = nodeIds[0]!;\n let bestDegree = -1;\n for (const id of nodeIds) {\n const degree = adjacency.get(id)?.size ?? 0;\n if (degree > bestDegree) {\n bestDegree = degree;\n best = id;\n }\n }\n return best;\n}\n\nfunction layoutConcentric<NodeType extends Node, EdgeType extends Edge>(\n nodes: NodeType[],\n edges: EdgeType[],\n options: LayoutOptions,\n): NodeType[] {\n const ringRadius = options.ringRadius ?? DEFAULT_RING_RADIUS;\n const nodeIds = nodes.map((node) => node.id);\n const idSet = new Set(nodeIds);\n\n const adjacency = new Map<string, Set<string>>();\n for (const id of nodeIds) adjacency.set(id, new Set());\n for (const edge of edges) {\n if (!idSet.has(edge.source) || !idSet.has(edge.target)) continue;\n adjacency.get(edge.source)!.add(edge.target);\n adjacency.get(edge.target)!.add(edge.source);\n }\n\n const centerId = resolveCenterId(options.centerId, nodeIds, adjacency);\n\n // BFS shells from the center — hop distance doubles as the ring index.\n const hopOf = new Map<string, number>();\n hopOf.set(centerId, 0);\n const queue: string[] = [centerId];\n let maxHop = 0;\n while (queue.length > 0) {\n const current = queue.shift()!;\n const currentHop = hopOf.get(current)!;\n for (const neighbor of adjacency.get(current) ?? []) {\n if (hopOf.has(neighbor)) continue;\n const hop = currentHop + 1;\n hopOf.set(neighbor, hop);\n maxHop = Math.max(maxHop, hop);\n queue.push(neighbor);\n }\n }\n\n // Disconnected nodes (unreachable from the center) get one extra ring beyond\n // the farthest reached hop — never left with a NaN/undefined position.\n const leftoverHop = maxHop + 1;\n for (const id of nodeIds) {\n if (!hopOf.has(id)) hopOf.set(id, leftoverHop);\n }\n\n const rings = new Map<number, string[]>();\n for (const id of nodeIds) {\n const hop = hopOf.get(id)!;\n if (hop === 0) continue;\n if (!rings.has(hop)) rings.set(hop, []);\n rings.get(hop)!.push(id);\n }\n\n const positionById = new Map<string, { x: number; y: number }>();\n positionById.set(centerId, { x: 0, y: 0 });\n\n for (const [hop, ids] of rings) {\n const count = ids.length;\n const angleStep = (2 * Math.PI) / count;\n // Stagger successive rings by a growing half-step so radial edges don't\n // trivially line up with the ring behind/ahead of them.\n const ringOffset = (hop - 1) * (angleStep / 2);\n const radius = hop * ringRadius;\n ids.forEach((id, i) => {\n const angle = ringOffset + i * angleStep;\n positionById.set(id, { x: radius * Math.cos(angle), y: radius * Math.sin(angle) });\n });\n }\n\n return nodes.map((node) => ({\n ...node,\n position: positionById.get(node.id) ?? { x: 0, y: 0 },\n }));\n}\n\ninterface ForceSimNode extends SimulationNodeDatum {\n id: string;\n}\n\nfunction layoutForce<NodeType extends Node, EdgeType extends Edge>(\n nodes: NodeType[],\n edges: EdgeType[],\n options: LayoutOptions,\n): NodeType[] {\n const iterations = options.iterations ?? DEFAULT_FORCE_ITERATIONS;\n const nodeIds = new Set(nodes.map((node) => node.id));\n\n // Seed initial positions deterministically on a circle by index — never\n // `Math.random()` — so two runs with the same input converge identically.\n const seedRadius = Math.max(nodes.length * 20, 100);\n const simNodes: ForceSimNode[] = nodes.map((node, i) => {\n const angle = (2 * Math.PI * i) / Math.max(nodes.length, 1);\n return { id: node.id, x: seedRadius * Math.cos(angle), y: seedRadius * Math.sin(angle) };\n });\n\n const simLinks: SimulationLinkDatum<ForceSimNode>[] = edges\n .filter((edge) => nodeIds.has(edge.source) && nodeIds.has(edge.target))\n .map((edge) => ({ source: edge.source, target: edge.target }));\n\n const collisionById = new Map(\n nodes.map((node) => {\n const { width, height } = resolveNodeSize(node, options.nodeSize);\n return [node.id, Math.max(width, height) / 2];\n }),\n );\n\n const simulation = forceSimulation(simNodes)\n .force(\"charge\", forceManyBody().strength(FORCE_CHARGE_STRENGTH))\n .force(\n \"link\",\n forceLink<ForceSimNode, SimulationLinkDatum<ForceSimNode>>(simLinks)\n .id((d) => d.id)\n .distance(FORCE_LINK_DISTANCE),\n )\n .force(\"center\", forceCenter(0, 0))\n .force(\n \"collide\",\n forceCollide<ForceSimNode>((d) => collisionById.get(d.id) ?? DEFAULT_NODE_WIDTH / 2),\n )\n .stop();\n\n for (let i = 0; i < iterations; i++) {\n simulation.tick();\n }\n\n const positionById = new Map(simNodes.map((n) => [n.id, { x: n.x ?? 0, y: n.y ?? 0 }]));\n\n return nodes.map((node) => ({\n ...node,\n position: positionById.get(node.id) ?? { x: 0, y: 0 },\n }));\n}\n\nfunction layoutGrid<NodeType extends Node>(nodes: NodeType[], options: LayoutOptions): NodeType[] {\n const spacing = options.spacing ?? DEFAULT_GRID_SPACING;\n const cols = Math.max(1, Math.ceil(Math.sqrt(nodes.length)));\n // A grid reads top-to-bottom; anchor edges bottom-out / top-in to match.\n const handles = HANDLE_BY_DIRECTION.TB;\n\n return nodes.map((node, i) => {\n const row = Math.floor(i / cols);\n const col = i % cols;\n return {\n ...node,\n sourcePosition: handles.source,\n targetPosition: handles.target,\n position: { x: col * spacing.x, y: row * spacing.y },\n };\n });\n}\n","import { useMemo } from \"react\";\nimport type { Edge, Node } from \"@xyflow/react\";\nimport { layoutGraph, type LayoutOptions } from \"./layout-graph\";\n\n/**\n * Memoized `layoutGraph` — pure graph-geometry layout (no live canvas access,\n * no side effects). Recomputes only when `nodes`, `edges`, or `options`\n * change; pass stable references (e.g. from `useNodesState`/`useEdgesState`\n * and a memoized `options` object) to avoid recomputing every render.\n *\n * Use this when you want laid-out nodes to render with (e.g. feeding\n * `useNodesState`'s initial value, or a derived/read-only view). To apply a\n * layout to a *live* React Flow instance (read current nodes, set them back,\n * fit the view), call `layoutGraph` directly from an event handler instead —\n * see the `LayoutGraph` story.\n */\nexport function useAutoLayout<NodeType extends Node = Node, EdgeType extends Edge = Edge>(\n nodes: NodeType[],\n edges: EdgeType[],\n options: LayoutOptions,\n): NodeType[] {\n return useMemo(\n () => layoutGraph<NodeType, EdgeType>(nodes, edges, options),\n [nodes, edges, options],\n );\n}\n","import { MiniMap, type MiniMapProps, type Node } from \"@xyflow/react\";\nimport { cn } from \"@elabs-ai/components-ui/lib/cn\";\n\nexport type FlowMiniMapProps<NodeType extends Node = Node> = MiniMapProps<NodeType>;\n\n/**\n * Branded preset over React Flow's `<MiniMap>` — sources node/mask colors from\n * the `--flow-minimap-*` tokens so the overview stays theme-aware (the raw\n * `MiniMap` re-export defaults to library colors and renders theme-blind,\n * especially under a low-chroma theme). Every prop can still be overridden; render\n * inside a `<CanvasShell>` (it needs the React Flow context via `useStore`).\n */\nexport function FlowMiniMap<NodeType extends Node = Node>({\n className,\n nodeColor = \"var(--flow-minimap-node)\",\n nodeStrokeColor = \"var(--flow-minimap-node)\",\n maskColor = \"var(--flow-minimap-mask)\",\n ...props\n}: FlowMiniMapProps<NodeType>) {\n return (\n <MiniMap\n className={cn(\"!rounded-lg !bg-surface-elevated !shadow-ring-sm\", className)}\n nodeColor={nodeColor}\n nodeStrokeColor={nodeStrokeColor}\n maskColor={maskColor}\n {...props}\n />\n );\n}\n","import { type CSSProperties, type ReactNode } from \"react\";\nimport { Reveal, useCollapsiblePanel } from \"@elabs-ai/components-ui\";\nimport { cn } from \"@elabs-ai/components-ui/lib/cn\";\n\nconst INSPECTOR_PANEL_WIDTH = \"18rem\";\n\nexport interface InspectorPanelProps {\n title?: ReactNode;\n children: ReactNode;\n onClose?: () => void;\n /** Empty-state shown when nothing is selected. */\n emptyMessage?: ReactNode;\n /** When false, render the empty state instead of children. */\n hasSelection?: boolean;\n /**\n * Stable identity of the current selection (e.g. the selected node's id).\n * When it changes, the body replays a quick fade so it reads as \"details for\n * THIS node\". Pass the selected entity's id (or any stable key derived from\n * the selected data).\n */\n selectionKey?: string | number;\n /**\n * Collapse (research 09 step 3 — converged on the shared `@elabs-ai/components-ui`\n * `useCollapsiblePanel` base; the panel stays single-view, drill-in is\n * ContextPanel-only). Controlled `open` / uncontrolled `defaultOpen`.\n */\n open?: boolean;\n defaultOpen?: boolean;\n onOpenChange?: (open: boolean) => void;\n /** Which edge the panel sits on (collapse slide direction). Default `\"right\"`. */\n side?: \"left\" | \"right\";\n /** Expanded width (any CSS length). Default `\"18rem\"` (the original `w-72`). */\n width?: string;\n className?: string;\n}\n\n/**\n * Properties panel for the selected node/edge. Reusable beside a canvas (e.g.\n * inside a <SplitPanel>) or as a React Flow <Panel>. Always mounted; collapses\n * via the canonical `useCollapsiblePanel` width tween (CSS-gated, reduced-\n * motion safe) when `open`/`defaultOpen` drive it.\n */\nexport function InspectorPanel({\n title = \"Inspector\",\n children,\n onClose,\n emptyMessage = \"Select a node to see its details.\",\n hasSelection = true,\n selectionKey,\n open,\n defaultOpen = true,\n onOpenChange,\n side = \"right\",\n width = INSPECTOR_PANEL_WIDTH,\n className,\n}: InspectorPanelProps) {\n const panel = useCollapsiblePanel({\n side,\n open,\n defaultOpen,\n onOpenChange,\n widthClassName: \"w-(--inspector-panel-width)\",\n spacerCollapsedClassName: \"group-data-[state=collapsed]:w-0\",\n containerSlideClassNames: {\n left: \"left-0 group-data-[state=collapsed]:left-[calc(var(--inspector-panel-width)*-1)]\",\n right: \"right-0 group-data-[state=collapsed]:right-[calc(var(--inspector-panel-width)*-1)]\",\n },\n });\n\n return (\n <div\n className={cn(\"group relative h-full overflow-hidden\", className)}\n style={{ \"--inspector-panel-width\": width } as CSSProperties}\n inert={panel.open ? undefined : true}\n {...panel.attrs}\n >\n <div data-slot=\"inspector-panel-gap\" className={panel.spacerClassName} />\n <div\n data-slot=\"inspector-panel-container\"\n // Bounded to the host (a SplitPanel / canvas edge), not the viewport:\n // absolute within the relative group, full height (research 09 §B.2).\n className={cn(panel.containerClassName, \"absolute inset-y-0 flex h-full border-s\")}\n >\n {/* Detail-tier surface: raised `card` — robust across themes (#193). */}\n <div className=\"flex h-full w-full flex-col bg-card\">\n <div className=\"flex items-center justify-between border-b px-4 py-3\">\n <h3 className=\"text-body font-semibold text-foreground\">{title}</h3>\n {onClose ? (\n <button\n type=\"button\"\n aria-label=\"Close inspector\"\n onClick={onClose}\n className=\"rounded-sm p-1 text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n >\n <svg\n width=\"14\"\n height=\"14\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n aria-hidden=\"true\"\n >\n <path d=\"M18 6 6 18M6 6l12 12\" />\n </svg>\n </button>\n ) : null}\n </div>\n {hasSelection ? (\n <Reveal\n key={selectionKey}\n appear=\"fade\"\n speed=\"fast\"\n className=\"flex-1 overflow-y-auto p-4 text-body\"\n >\n {children}\n </Reveal>\n ) : (\n <div className=\"flex-1 overflow-y-auto p-4 text-body\">\n <p className=\"text-muted-foreground\">{emptyMessage}</p>\n </div>\n )}\n </div>\n </div>\n </div>\n );\n}\n","import { cn } from \"@elabs-ai/components-ui/lib/cn\";\n\nexport interface LegendItem {\n label: string;\n /** Any CSS color or token reference, e.g. \"var(--chart-1)\". */\n color: string;\n}\n\nexport interface LegendProps {\n items: LegendItem[];\n title?: string;\n className?: string;\n}\n\n/** Small legend mapping colors/types to labels for a canvas or chart. */\nexport function Legend({ items, title, className }: LegendProps) {\n return (\n <div\n className={cn(\n \"rounded-lg bg-surface-elevated/90 p-3 text-xs shadow-ring-sm backdrop-blur\",\n className,\n )}\n >\n {title ? <div className=\"mb-1.5 font-medium text-foreground\">{title}</div> : null}\n <ul className=\"space-y-1\">\n {items.map((item) => (\n <li key={item.label} className=\"flex items-center gap-2 text-muted-foreground\">\n <span\n aria-hidden=\"true\"\n className=\"size-2.5 shrink-0 rounded-full\"\n style={{ backgroundColor: item.color }}\n />\n {item.label}\n </li>\n ))}\n </ul>\n </div>\n );\n}\n","import { Panel, useReactFlow, type PanelPosition } from \"@xyflow/react\";\nimport { cn } from \"@elabs-ai/components-ui/lib/cn\";\n\nexport interface ZoomControlsProps {\n position?: PanelPosition;\n className?: string;\n}\n\nfunction ControlButton({\n label,\n onClick,\n children,\n}: {\n label: string;\n onClick: () => void;\n children: React.ReactNode;\n}) {\n return (\n <button\n type=\"button\"\n aria-label={label}\n onClick={onClick}\n className=\"flex size-8 items-center justify-center text-foreground transition-colors duration-fast hover:bg-surface-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring [&_svg]:size-4\"\n >\n {children}\n </button>\n );\n}\n\n/** Branded zoom in / out / fit controls. Render inside <CanvasShell>. */\nexport function ZoomControls({ position = \"bottom-right\", className }: ZoomControlsProps) {\n const { zoomIn, zoomOut, fitView } = useReactFlow();\n return (\n <Panel position={position}>\n <div\n className={cn(\n \"flex divide-x overflow-hidden rounded-lg bg-surface-elevated shadow-ring-sm\",\n className,\n )}\n >\n <ControlButton label=\"Zoom in\" onClick={() => zoomIn()}>\n <svg\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n >\n <path d=\"M12 5v14M5 12h14\" />\n </svg>\n </ControlButton>\n <ControlButton label=\"Zoom out\" onClick={() => zoomOut()}>\n <svg\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n >\n <path d=\"M5 12h14\" />\n </svg>\n </ControlButton>\n <ControlButton label=\"Fit view\" onClick={() => fitView()}>\n <svg\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n >\n <path d=\"M8 3H5a2 2 0 0 0-2 2v3M21 8V5a2 2 0 0 0-2-2h-3M16 21h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3\" />\n </svg>\n </ControlButton>\n </div>\n </Panel>\n );\n}\n","import { type ReactNode } from \"react\";\nimport { Handle, NodeResizer, Position, useNodes, type Node, type NodeProps } from \"@xyflow/react\";\nimport { ChevronDown, ChevronRight } from \"lucide-react\";\nimport { cn } from \"@elabs-ai/components-ui/lib/cn\";\nimport { useFlowGroups } from \"../use-flow-groups\";\n\n/** Visual accent for a group container. */\nexport type FlowGroupTone = \"default\" | \"accent\" | \"success\" | \"warning\" | \"destructive\";\n\nexport interface FlowGroupNodeData extends Record<string, unknown> {\n /** Header label for the group. */\n title: string;\n /** Optional leading glyph (a Lucide icon element, etc.). */\n icon?: ReactNode;\n /**\n * Whether the group is collapsed to an overview chip. Managed by\n * `useFlowGroups().collapseGroup` / `expandGroup`; the header toggle flips it.\n */\n collapsed?: boolean;\n /**\n * Fallback count shown next to the title. When omitted, the node counts its\n * live direct children from the store, so the badge stays accurate as nodes\n * are added or removed.\n */\n childCount?: number;\n /** Accent tone for the header rule + count badge. */\n tone?: FlowGroupTone;\n}\n\nexport type BrandFlowGroupNode = Node<FlowGroupNodeData, \"group\">;\n\nconst toneAccent: Record<FlowGroupTone, string> = {\n default: \"text-muted-foreground\",\n // #399 — the group LABEL is text on the canvas, so it takes the on-surface\n // `-text` rung. (The status rows below stay on their fill rungs; those are a\n // separate, still-open question, not part of #399's enumerated call sites.)\n accent: \"text-primary-text\",\n success: \"text-success\",\n warning: \"text-warning\",\n destructive: \"text-destructive\",\n};\n\nconst handleClassName = \"!size-2 !border-2 !border-flow-group-border !bg-flow-group\";\n\n/**\n * Branded group container node. Register it as `nodeTypes={{ group: FlowGroupNode }}`\n * and create nodes with `type: \"group\"` + typed `data: FlowGroupNodeData`. Children\n * are re-parented onto it (`parentId`, `extent: \"parent\"`) — see `useFlowGroups`.\n *\n * The header carries the title, a live child-count badge and a collapse/expand\n * toggle (a real `<button>` with `aria-expanded`) that drives\n * `useFlowGroups().collapseGroup` / `expandGroup`. When selected (and expanded)\n * a `<NodeResizer>` lets the group be resized.\n *\n * The surface uses the FL-01 tokens `--flow-group` (fill) + `--flow-group-border`\n * (border). Under a theme whose `--flow-group` is near-transparent, the\n * drawn border does the work.\n */\nexport function FlowGroupNode({ id, data, selected }: NodeProps<BrandFlowGroupNode>) {\n const { toggleCollapse } = useFlowGroups();\n const nodes = useNodes();\n const collapsed = data.collapsed ?? false;\n const tone = data.tone ?? \"default\";\n\n // Live count of direct children; fall back to the stored count before the\n // store settles (or in a non-interactive preview).\n const liveChildCount = nodes.filter((n) => n.parentId === id).length;\n const childCount = liveChildCount || data.childCount || 0;\n\n return (\n <div\n className={cn(\n \"flex h-full w-full flex-col rounded-lg border bg-flow-group/60 text-foreground\",\n \"border-flow-group-border transition-[box-shadow,border-color] duration-fast ease-standard motion-reduce:transition-none\",\n collapsed ? \"shadow-sm\" : \"shadow-none\",\n selected && \"ring-2 ring-ring\",\n )}\n >\n {!collapsed && selected ? (\n <NodeResizer\n minWidth={160}\n minHeight={96}\n lineClassName=\"!border-flow-group-border\"\n handleClassName={handleClassName}\n />\n ) : null}\n\n {/* Handles so proxy edges can attach to the group when collapsed. */}\n <Handle type=\"target\" position={Position.Top} className={handleClassName} />\n <Handle type=\"source\" position={Position.Bottom} className={handleClassName} />\n\n <div\n className={cn(\n \"flex items-center gap-2 px-3 py-2\",\n !collapsed && \"border-b border-flow-group-border\",\n )}\n >\n <button\n type=\"button\"\n onClick={() => toggleCollapse(id)}\n aria-label={collapsed ? `Expand group ${data.title}` : `Collapse group ${data.title}`}\n aria-expanded={!collapsed}\n className={cn(\n \"-ml-1 grid size-5 shrink-0 place-items-center rounded\",\n \"text-muted-foreground hover:bg-muted hover:text-foreground\",\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\",\n )}\n >\n {collapsed ? (\n <ChevronRight aria-hidden=\"true\" className=\"size-4\" />\n ) : (\n <ChevronDown aria-hidden=\"true\" className=\"size-4\" />\n )}\n </button>\n\n {data.icon ? (\n <span className={cn(\"[&_svg]:size-4\", toneAccent[tone])}>{data.icon}</span>\n ) : null}\n\n <span className=\"min-w-0 flex-1 truncate text-body font-medium\">{data.title}</span>\n\n <span\n className={cn(\n \"shrink-0 rounded-full bg-muted px-1.5 py-0.5 text-meta font-medium tabular-nums\",\n toneAccent[tone],\n )}\n aria-label={`${childCount} ${childCount === 1 ? \"node\" : \"nodes\"}`}\n >\n {childCount}\n </span>\n </div>\n </div>\n );\n}\n","import { useCallback } from \"react\";\nimport { useReactFlow, type Edge, type Node } from \"@xyflow/react\";\nimport {\n collapseGroup as collapseGroupOp,\n expandGroup as expandGroupOp,\n groupNodes as groupNodesOp,\n toggleGroupCollapsed as toggleGroupCollapsedOp,\n ungroup as ungroupOp,\n type GroupNodesOptions,\n} from \"./group-operations\";\n\n/** Options for `groupNodes`/`groupSelection` with an optional generated id. */\nexport type GroupOptions = Partial<Pick<GroupNodesOptions, \"groupId\">> &\n Omit<GroupNodesOptions, \"groupId\">;\n\nexport interface UseFlowGroupsResult {\n /**\n * Wrap the given (top-level) node ids in a new `FlowGroupNode`. Returns the new\n * group id, or `undefined` if nothing was grouped. Pass `options.groupId` for a\n * deterministic id; otherwise one is generated.\n */\n groupNodes: (nodeIds: string[], options?: GroupOptions) => string | undefined;\n /** Group the currently-selected nodes. Returns the new group id (or `undefined`). */\n groupSelection: (options?: GroupOptions) => string | undefined;\n /** Dissolve a group, restoring its children to absolute positions. */\n ungroup: (groupId: string) => void;\n /** Collapse a group to an overview chip (hides subtree, reroutes boundary edges). */\n collapseGroup: (groupId: string) => void;\n /** Expand a collapsed group (exact inverse of collapse). */\n expandGroup: (groupId: string) => void;\n /** Collapse if expanded, expand if collapsed. */\n toggleCollapse: (groupId: string) => void;\n}\n\nfunction generateGroupId(): string {\n return `flow-group-${Math.random().toString(36).slice(2, 9)}`;\n}\n\n/**\n * Thin hook over the pure grouping operations in `group-operations.ts`. Reads and\n * writes the live graph via `useReactFlow()`; all the logic lives in the pure,\n * unit-tested core. Must be called inside a React Flow context (e.g. from a\n * `<Panel>` child of `<CanvasShell>` or from within a custom node).\n */\nexport function useFlowGroups<\n NodeType extends Node = Node,\n EdgeType extends Edge = Edge,\n>(): UseFlowGroupsResult {\n const { getNodes, getEdges, setNodes, setEdges } = useReactFlow<NodeType, EdgeType>();\n\n const apply = useCallback(\n (result: { nodes: NodeType[]; edges: EdgeType[] }) => {\n setNodes(result.nodes);\n setEdges(result.edges);\n },\n [setNodes, setEdges],\n );\n\n const groupNodes = useCallback(\n (nodeIds: string[], options?: GroupOptions): string | undefined => {\n if (nodeIds.length === 0) return undefined;\n const groupId = options?.groupId ?? generateGroupId();\n const result = groupNodesOp<NodeType, EdgeType>(getNodes(), getEdges(), nodeIds, {\n ...options,\n groupId,\n });\n apply(result);\n return groupId;\n },\n [getNodes, getEdges, apply],\n );\n\n const groupSelection = useCallback(\n (options?: GroupOptions): string | undefined => {\n const selectedIds = getNodes()\n .filter((n) => n.selected && !n.parentId)\n .map((n) => n.id);\n return groupNodes(selectedIds, options);\n },\n [getNodes, groupNodes],\n );\n\n const ungroup = useCallback(\n (groupId: string) => apply(ungroupOp<NodeType, EdgeType>(getNodes(), getEdges(), groupId)),\n [getNodes, getEdges, apply],\n );\n\n const collapseGroup = useCallback(\n (groupId: string) =>\n apply(collapseGroupOp<NodeType, EdgeType>(getNodes(), getEdges(), groupId)),\n [getNodes, getEdges, apply],\n );\n\n const expandGroup = useCallback(\n (groupId: string) => apply(expandGroupOp<NodeType, EdgeType>(getNodes(), getEdges(), groupId)),\n [getNodes, getEdges, apply],\n );\n\n const toggleCollapse = useCallback(\n (groupId: string) =>\n apply(toggleGroupCollapsedOp<NodeType, EdgeType>(getNodes(), getEdges(), groupId)),\n [getNodes, getEdges, apply],\n );\n\n return {\n groupNodes,\n groupSelection,\n ungroup,\n collapseGroup,\n expandGroup,\n toggleCollapse,\n };\n}\n","import { getNodesBounds, type Edge, type Node } from \"@xyflow/react\";\nimport type { FlowGroupNodeData, FlowGroupTone } from \"../flow-group-node\";\n\n/**\n * Pure, framework-agnostic grouping operations over `nodes`/`edges` arrays.\n *\n * Every function is a **pure transform** — it never mutates its inputs and\n * returns a fresh `{ nodes, edges }`. This is the testable core; `useFlowGroups`\n * is a thin hook that wires these to a live canvas via `useReactFlow()`.\n */\n\n/** Node type used for the group container (matches `nodeTypes={{ group: FlowGroupNode }}`). */\nexport const FLOW_GROUP_NODE_TYPE = \"group\";\n\n/** Padding (px) left around the child bounds when a group is created. */\nconst DEFAULT_GROUP_PADDING = 28;\n/** Extra top space reserved for the group header so children don't sit under it. */\nconst GROUP_HEADER_OFFSET = 44;\n/** Fixed size of the collapsed \"overview chip\". */\nconst OVERVIEW_WIDTH = 220;\nconst OVERVIEW_HEIGHT = 48;\n\nexport interface GroupNodesOptions {\n /** Id for the new group node. Required so the operation stays deterministic/testable. */\n groupId: string;\n /** Header title for the group. @default \"Group\" */\n title?: string;\n /** Accent tone for the group header. */\n tone?: FlowGroupTone;\n /** Padding (px) around the child bounds. @default 28 */\n padding?: number;\n}\n\nexport interface FlowGroupOperationResult<\n NodeType extends Node = Node,\n EdgeType extends Edge = Edge,\n> {\n nodes: NodeType[];\n edges: EdgeType[];\n}\n\n/**\n * Snapshot stashed on a collapsed group's `data` so `expandGroup` can restore the\n * exact pre-collapse graph (an exact inverse). Holds original node/edge object\n * references — never mutated — so restoration is object-exact.\n */\ninterface FlowGroupCollapsedState {\n /** The group node exactly as it was before collapse (no collapsed state on it). */\n groupSnapshot: Node;\n /** Every descendant node exactly as it was before collapse. */\n descendantSnapshots: Node[];\n /** The boundary-crossing edges exactly as they were before rerouting. */\n reroutedEdges: Edge[];\n}\n\nconst COLLAPSED_STATE_KEY = \"__flowGroupCollapsedState\";\n\n/** Marker + metadata written onto a proxy edge's `data`. */\nexport interface FlowGroupProxyEdgeData extends Record<string, unknown> {\n __flowGroupProxy: true;\n /** Id of the collapsed group this proxy edge stands in for. */\n groupId: string;\n}\n\n/** True if `edge` is a proxy edge synthesized for a collapsed group. */\nexport function isFlowGroupProxyEdge(edge: Edge): boolean {\n return Boolean((edge.data as Partial<FlowGroupProxyEdgeData> | undefined)?.__flowGroupProxy);\n}\n\nfunction proxyEdgeId(groupId: string, originalEdgeId: string): string {\n return `flow-group-proxy__${groupId}__${originalEdgeId}`;\n}\n\nfunction readCollapsedState(node: Node): FlowGroupCollapsedState | undefined {\n return (node.data as Record<string, unknown> | undefined)?.[COLLAPSED_STATE_KEY] as\n | FlowGroupCollapsedState\n | undefined;\n}\n\n/** All node ids whose parent chain reaches `groupId` (children, grandchildren, …). */\nfunction collectDescendantIds(nodes: Node[], groupId: string): Set<string> {\n const childrenByParent = new Map<string, Node[]>();\n for (const n of nodes) {\n if (n.parentId) {\n const arr = childrenByParent.get(n.parentId);\n if (arr) arr.push(n);\n else childrenByParent.set(n.parentId, [n]);\n }\n }\n\n const out = new Set<string>();\n const stack = [...(childrenByParent.get(groupId) ?? [])];\n while (stack.length > 0) {\n const n = stack.pop()!;\n if (out.has(n.id)) continue;\n out.add(n.id);\n const kids = childrenByParent.get(n.id);\n if (kids) stack.push(...kids);\n }\n return out;\n}\n\n/**\n * Wrap `nodeIds` (assumed top-level) in a new `FlowGroupNode` parent.\n *\n * - Computes the child bounds with `getNodesBounds`.\n * - Inserts the group node **before** its children in the array (React Flow\n * requires a parent to precede its children).\n * - Re-parents each child (`parentId`, `extent: \"parent\"`) and rewrites its\n * position to be **relative to the group origin**.\n */\nexport function groupNodes<NodeType extends Node = Node, EdgeType extends Edge = Edge>(\n nodes: NodeType[],\n edges: EdgeType[],\n nodeIds: string[],\n options: GroupNodesOptions,\n): FlowGroupOperationResult<NodeType, EdgeType> {\n const idSet = new Set(nodeIds);\n const children = nodes.filter((n) => idSet.has(n.id));\n if (children.length === 0) return { nodes, edges };\n\n const bounds = getNodesBounds(children);\n const padding = options.padding ?? DEFAULT_GROUP_PADDING;\n const originX = bounds.x - padding;\n const originY = bounds.y - padding - GROUP_HEADER_OFFSET;\n\n const data: FlowGroupNodeData = {\n title: options.title ?? \"Group\",\n childCount: children.length,\n ...(options.tone ? { tone: options.tone } : {}),\n };\n\n const groupNode = {\n id: options.groupId,\n type: FLOW_GROUP_NODE_TYPE,\n position: { x: originX, y: originY },\n width: bounds.width + padding * 2,\n height: bounds.height + padding * 2 + GROUP_HEADER_OFFSET,\n data,\n } as unknown as NodeType;\n\n const reparented = new Map<string, NodeType>();\n for (const child of children) {\n reparented.set(child.id, {\n ...child,\n parentId: options.groupId,\n extent: \"parent\",\n position: { x: child.position.x - originX, y: child.position.y - originY },\n } as NodeType);\n }\n\n let earliest = nodes.length;\n for (let i = 0; i < nodes.length; i++) {\n if (idSet.has(nodes[i]!.id)) {\n earliest = i;\n break;\n }\n }\n\n const mapped = nodes.map((n) => reparented.get(n.id) ?? n);\n const resultNodes = [...mapped.slice(0, earliest), groupNode, ...mapped.slice(earliest)];\n return { nodes: resultNodes, edges };\n}\n\n/**\n * Dissolve a group: remove the group node and restore its **direct** children to\n * absolute positions (dropping `parentId`/`extent`). If the group is currently\n * collapsed it is expanded first, so ungrouping a collapsed group is safe.\n */\nexport function ungroup<NodeType extends Node = Node, EdgeType extends Edge = Edge>(\n nodes: NodeType[],\n edges: EdgeType[],\n groupId: string,\n): FlowGroupOperationResult<NodeType, EdgeType> {\n const group = nodes.find((n) => n.id === groupId);\n if (!group) return { nodes, edges };\n\n const base = (group.data as FlowGroupNodeData | undefined)?.collapsed\n ? expandGroup(nodes, edges, groupId)\n : { nodes, edges };\n\n const liveGroup = base.nodes.find((n) => n.id === groupId)!;\n const gx = liveGroup.position.x;\n const gy = liveGroup.position.y;\n\n const resultNodes = base.nodes\n .filter((n) => n.id !== groupId)\n .map((n) => {\n if (n.parentId === groupId) {\n const {\n parentId: _parentId,\n extent: _extent,\n ...rest\n } = n as NodeType & {\n parentId?: string;\n extent?: unknown;\n };\n return {\n ...rest,\n position: { x: n.position.x + gx, y: n.position.y + gy },\n } as NodeType;\n }\n return n;\n });\n\n return { nodes: resultNodes, edges: base.edges };\n}\n\n/**\n * Collapse a group to an overview chip.\n *\n * - Hides every descendant (including nested groups and their subtrees).\n * - Shrinks the group to a fixed overview size and marks `data.collapsed`.\n * - Re-routes every edge that **crosses the collapse boundary** (exactly one\n * endpoint inside the collapsed subtree) to a **proxy edge** targeting the\n * group node; the original edge is hidden and stashed for restoration.\n * - Edges fully inside or fully outside the subtree are left untouched.\n *\n * The full pre-collapse graph is stashed on the group's `data` so `expandGroup`\n * is an exact inverse. Nested collapse is handled by snapshotting each\n * descendant's exact prior state (a subgroup that was already collapsed stays\n * collapsed on expand — no double-unhide).\n */\nexport function collapseGroup<NodeType extends Node = Node, EdgeType extends Edge = Edge>(\n nodes: NodeType[],\n edges: EdgeType[],\n groupId: string,\n): FlowGroupOperationResult<NodeType, EdgeType> {\n const group = nodes.find((n) => n.id === groupId);\n if (!group) return { nodes, edges };\n if ((group.data as FlowGroupNodeData | undefined)?.collapsed) return { nodes, edges };\n\n const descendantIds = collectDescendantIds(nodes, groupId);\n\n const descendantSnapshots = nodes.filter((n) => descendantIds.has(n.id));\n const directChildCount = nodes.filter((n) => n.parentId === groupId).length;\n\n const isInside = (id: string) => descendantIds.has(id);\n const reroutedEdges: EdgeType[] = [];\n const proxyEdges: EdgeType[] = [];\n for (const edge of edges) {\n const sourceInside = isInside(edge.source);\n const targetInside = isInside(edge.target);\n if (sourceInside === targetInside) continue; // fully inside or fully outside\n\n reroutedEdges.push(edge);\n const proxyData: FlowGroupProxyEdgeData = {\n ...(edge.data ?? {}),\n __flowGroupProxy: true,\n groupId,\n };\n proxyEdges.push({\n ...edge,\n id: proxyEdgeId(groupId, edge.id),\n source: sourceInside ? groupId : edge.source,\n target: targetInside ? groupId : edge.target,\n sourceHandle: undefined,\n targetHandle: undefined,\n data: proxyData,\n } as EdgeType);\n }\n\n const collapsedState: FlowGroupCollapsedState = {\n groupSnapshot: group,\n descendantSnapshots,\n reroutedEdges,\n };\n\n const resultNodes = nodes.map((n) => {\n if (n.id === groupId) {\n return {\n ...n,\n width: OVERVIEW_WIDTH,\n height: OVERVIEW_HEIGHT,\n data: {\n ...(n.data as Record<string, unknown>),\n collapsed: true,\n childCount: directChildCount,\n [COLLAPSED_STATE_KEY]: collapsedState,\n },\n } as NodeType;\n }\n if (descendantIds.has(n.id)) {\n return { ...n, hidden: true } as NodeType;\n }\n return n;\n });\n\n const reroutedIds = new Set(reroutedEdges.map((e) => e.id));\n const resultEdges = edges.map((e) =>\n reroutedIds.has(e.id) ? ({ ...e, hidden: true } as EdgeType) : e,\n );\n resultEdges.push(...proxyEdges);\n\n return { nodes: resultNodes, edges: resultEdges };\n}\n\n/**\n * Expand a collapsed group — the exact inverse of `collapseGroup`. Restores the\n * group node, un-hides the descendant subtree (to each node's exact prior\n * state), removes this group's proxy edges and restores the original edges.\n * No-op if the group isn't collapsed.\n */\nexport function expandGroup<NodeType extends Node = Node, EdgeType extends Edge = Edge>(\n nodes: NodeType[],\n edges: EdgeType[],\n groupId: string,\n): FlowGroupOperationResult<NodeType, EdgeType> {\n const group = nodes.find((n) => n.id === groupId);\n const state = group ? readCollapsedState(group) : undefined;\n if (!group || !state) return { nodes, edges };\n\n const snapshotById = new Map(state.descendantSnapshots.map((n) => [n.id, n]));\n const reroutedById = new Map(state.reroutedEdges.map((e) => [e.id, e]));\n\n const resultNodes = nodes.map((n) => {\n if (n.id === groupId) return state.groupSnapshot as NodeType;\n const snap = snapshotById.get(n.id);\n return snap ? (snap as NodeType) : n;\n });\n\n const resultEdges = edges\n .filter(\n (e) =>\n !(\n isFlowGroupProxyEdge(e) &&\n (e.data as Partial<FlowGroupProxyEdgeData> | undefined)?.groupId === groupId\n ),\n )\n .map((e) => {\n const original = reroutedById.get(e.id);\n return original ? (original as EdgeType) : e;\n });\n\n return { nodes: resultNodes, edges: resultEdges };\n}\n\n/** Collapse if expanded, expand if collapsed. */\nexport function toggleGroupCollapsed<NodeType extends Node = Node, EdgeType extends Edge = Edge>(\n nodes: NodeType[],\n edges: EdgeType[],\n groupId: string,\n): FlowGroupOperationResult<NodeType, EdgeType> {\n const group = nodes.find((n) => n.id === groupId);\n if (!group) return { nodes, edges };\n return (group.data as FlowGroupNodeData | undefined)?.collapsed\n ? expandGroup(nodes, edges, groupId)\n : collapseGroup(nodes, edges, groupId);\n}\n","/**\n * @elabs-ai/components-flow — branded React Flow (@xyflow/react) building blocks.\n *\n * Import the React Flow stylesheet once at the app root:\n * import \"@xyflow/react/dist/style.css\";\n */\nexport * from \"./canvas-shell\";\nexport * from \"./flow-node\";\nexport * from \"./flow-edge\";\nexport * from \"./flow-smart-edge\";\nexport * from \"./flow-floating-edge\";\nexport * from \"./flow-placeholder-node\";\nexport * from \"./flow-button-edge\";\nexport * from \"./flow-layout\";\nexport * from \"./helper-lines\";\nexport * from \"./flow-mini-map\";\nexport * from \"./inspector-panel\";\nexport * from \"./legend\";\nexport * from \"./zoom-controls\";\nexport * from \"./flow-group-node\";\nexport * from \"./use-flow-groups\";\n\n// Convenience re-exports so consumers can build flows without a direct dep.\nexport {\n Background,\n Controls,\n MiniMap,\n Panel,\n Position,\n ReactFlow,\n ReactFlowProvider,\n useReactFlow,\n useNodesState,\n useEdgesState,\n addEdge,\n} from \"@xyflow/react\";\nexport type { Node, Edge, Connection, NodeProps, EdgeProps } from \"@xyflow/react\";\n"],"mappings":"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","names":["cn","HelperLines","jsx","jsxs","cn","cn","jsx","jsxs","jsx","BaseEdge","getBezierPath","Position","jsx","getBezierPath","BaseEdge","BaseEdge","getBezierPath","useInternalNode","Position","Fragment","jsx","jsxs","useInternalNode","getBezierPath","BaseEdge","Handle","Position","cn","jsx","jsxs","BaseEdge","getBezierPath","Plus","Fragment","jsx","jsxs","Position","useCallback","useState","useReactFlow","useReactFlow","useState","useCallback","DEFAULT_NODE_WIDTH","DEFAULT_NODE_HEIGHT","nodeSize","cn","jsx","cn","jsx","jsxs","cn","jsx","jsxs","useReactFlow","cn","jsx","jsxs","Handle","Position","cn","useCallback","useReactFlow","useReactFlow","useCallback","groupNodes","ungroup","collapseGroup","expandGroup","jsx","jsxs","handleClassName","cn","Handle","Position","Background","MiniMap","Panel","Position","ReactFlow","ReactFlowProvider","useReactFlow"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@elabs-ai/components-flow",
|
|
3
|
+
"version": "4.0.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/mreimitz/elabs-components.git",
|
|
8
|
+
"directory": "packages/flow"
|
|
9
|
+
},
|
|
10
|
+
"type": "module",
|
|
11
|
+
"files": [
|
|
12
|
+
"dist",
|
|
13
|
+
"src"
|
|
14
|
+
],
|
|
15
|
+
"sideEffects": false,
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"default": "./dist/index.js"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"publishConfig": {
|
|
23
|
+
"access": "public",
|
|
24
|
+
"registry": "https://registry.npmjs.org/"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@dagrejs/dagre": "^3.0.0",
|
|
28
|
+
"d3-force": "^3.0.0",
|
|
29
|
+
"lucide-react": "^0.577.0"
|
|
30
|
+
},
|
|
31
|
+
"peerDependencies": {
|
|
32
|
+
"@xyflow/react": "^12.11.1",
|
|
33
|
+
"react": "^18.2.0 || ^19.0.0",
|
|
34
|
+
"react-dom": "^18.2.0 || ^19.0.0",
|
|
35
|
+
"@elabs-ai/components-tokens": "4.0.0",
|
|
36
|
+
"@elabs-ai/components-ui": "4.0.0"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@testing-library/jest-dom": "^6.6.3",
|
|
40
|
+
"@testing-library/react": "^16.1.0",
|
|
41
|
+
"@testing-library/user-event": "^14.5.2",
|
|
42
|
+
"@types/d3-force": "^3.0.10",
|
|
43
|
+
"@types/react": "^19.0.2",
|
|
44
|
+
"@types/react-dom": "^19.0.2",
|
|
45
|
+
"@vitejs/plugin-react": "^4.3.4",
|
|
46
|
+
"@xyflow/react": "^12.11.1",
|
|
47
|
+
"eslint": "^9.17.0",
|
|
48
|
+
"jsdom": "^25.0.1",
|
|
49
|
+
"react": "^19.0.0",
|
|
50
|
+
"react-dom": "^19.0.0",
|
|
51
|
+
"tsup": "^8.3.5",
|
|
52
|
+
"typescript": "^5.7.3",
|
|
53
|
+
"vitest": "^3.0.2",
|
|
54
|
+
"@elabs-ai/components-eslint-config": "0.1.0",
|
|
55
|
+
"@elabs-ai/components-tokens": "4.0.0",
|
|
56
|
+
"@elabs-ai/components-ui": "4.0.0",
|
|
57
|
+
"@elabs-ai/components-icons": "4.0.0",
|
|
58
|
+
"@elabs-ai/components-typescript-config": "0.1.0"
|
|
59
|
+
},
|
|
60
|
+
"scripts": {
|
|
61
|
+
"build": "tsup",
|
|
62
|
+
"typecheck": "tsc --noEmit",
|
|
63
|
+
"test": "vitest run",
|
|
64
|
+
"test:watch": "vitest",
|
|
65
|
+
"lint": "eslint .",
|
|
66
|
+
"clean": "rm -rf dist .turbo coverage"
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import "@xyflow/react/dist/style.css";
|
|
3
|
+
import { CanvasShell } from "./canvas-shell";
|
|
4
|
+
import { FlowNode, FLOW_ALL_SIDE_HANDLES, type BrandFlowNode } from "../flow-node";
|
|
5
|
+
import { FlowSmartEdge } from "../flow-smart-edge";
|
|
6
|
+
import { ZoomControls } from "../zoom-controls";
|
|
7
|
+
import { Legend } from "../legend";
|
|
8
|
+
import { Panel, type Edge } from "@xyflow/react";
|
|
9
|
+
|
|
10
|
+
const nodeTypes = { brand: FlowNode };
|
|
11
|
+
// All-side anchors + the smart edge, so each connection lands on the side facing
|
|
12
|
+
// the other node (this pipeline runs left→right → edges attach right/left).
|
|
13
|
+
const edgeTypes = { smart: FlowSmartEdge };
|
|
14
|
+
|
|
15
|
+
const nodes: BrandFlowNode[] = [
|
|
16
|
+
{
|
|
17
|
+
id: "1",
|
|
18
|
+
type: "brand",
|
|
19
|
+
position: { x: 0, y: 0 },
|
|
20
|
+
data: {
|
|
21
|
+
kind: "Source",
|
|
22
|
+
title: "Postgres",
|
|
23
|
+
subtitle: "orders",
|
|
24
|
+
tone: "accent",
|
|
25
|
+
handles: FLOW_ALL_SIDE_HANDLES,
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
id: "2",
|
|
30
|
+
type: "brand",
|
|
31
|
+
position: { x: 220, y: 120 },
|
|
32
|
+
data: {
|
|
33
|
+
kind: "Transform",
|
|
34
|
+
title: "Clean & join",
|
|
35
|
+
tone: "default",
|
|
36
|
+
handles: FLOW_ALL_SIDE_HANDLES,
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
id: "3",
|
|
41
|
+
type: "brand",
|
|
42
|
+
position: { x: 440, y: 0 },
|
|
43
|
+
data: { kind: "Output", title: "Dashboard", tone: "success", handles: FLOW_ALL_SIDE_HANDLES },
|
|
44
|
+
},
|
|
45
|
+
];
|
|
46
|
+
const edges: Edge[] = [
|
|
47
|
+
{ id: "e1-2", source: "1", target: "2", type: "smart" },
|
|
48
|
+
{ id: "e2-3", source: "2", target: "3", type: "smart" },
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
const meta = {
|
|
52
|
+
title: "Flow/CanvasShell",
|
|
53
|
+
component: CanvasShell,
|
|
54
|
+
parameters: { layout: "fullscreen" },
|
|
55
|
+
} satisfies Meta<typeof CanvasShell>;
|
|
56
|
+
export default meta;
|
|
57
|
+
type Story = StoryObj<typeof meta>;
|
|
58
|
+
|
|
59
|
+
export const Pipeline: Story = {
|
|
60
|
+
render: () => (
|
|
61
|
+
<div className="h-[600px]">
|
|
62
|
+
<CanvasShell nodes={nodes} edges={edges} nodeTypes={nodeTypes} edgeTypes={edgeTypes}>
|
|
63
|
+
<ZoomControls />
|
|
64
|
+
<Panel position="top-left">
|
|
65
|
+
<Legend
|
|
66
|
+
title="Node types"
|
|
67
|
+
items={[
|
|
68
|
+
{ label: "Source", color: "var(--primary)" },
|
|
69
|
+
{ label: "Transform", color: "var(--muted-foreground)" },
|
|
70
|
+
{ label: "Output", color: "var(--success)" },
|
|
71
|
+
]}
|
|
72
|
+
/>
|
|
73
|
+
</Panel>
|
|
74
|
+
</CanvasShell>
|
|
75
|
+
</div>
|
|
76
|
+
),
|
|
77
|
+
};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { cleanup, render } from "@testing-library/react";
|
|
2
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
|
|
4
|
+
// @xyflow/react requires real layout/measurement — mock the engine and assert
|
|
5
|
+
// the brand wrapper's own output. Real rendering + a11y are covered by Storybook
|
|
6
|
+
// interaction tests.
|
|
7
|
+
const reactFlowSpy = vi.fn();
|
|
8
|
+
|
|
9
|
+
vi.mock("@xyflow/react", () => {
|
|
10
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports -- a vi.mock factory is hoisted above imports; a lazy require avoids the TDZ a top-level import would hit
|
|
11
|
+
const React = require("react");
|
|
12
|
+
return {
|
|
13
|
+
ReactFlow: (props: { children?: React.ReactNode; className?: string }) => {
|
|
14
|
+
reactFlowSpy(props);
|
|
15
|
+
const { children, className } = props;
|
|
16
|
+
return React.createElement("div", { "data-testid": "react-flow", className }, children);
|
|
17
|
+
},
|
|
18
|
+
Background: () => React.createElement("div", { "data-testid": "rf-background" }),
|
|
19
|
+
ReactFlowProvider: ({ children }: { children?: React.ReactNode }) =>
|
|
20
|
+
React.createElement("div", {}, children),
|
|
21
|
+
};
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
import { CanvasShell } from "./canvas-shell";
|
|
25
|
+
|
|
26
|
+
afterEach(() => {
|
|
27
|
+
cleanup();
|
|
28
|
+
reactFlowSpy.mockClear();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe("CanvasShell", () => {
|
|
32
|
+
it("renders without throwing", () => {
|
|
33
|
+
const { container } = render(<CanvasShell nodes={[]} edges={[]} />);
|
|
34
|
+
expect(container.firstChild).toBeInTheDocument();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("renders the inner ReactFlow mock", () => {
|
|
38
|
+
const { getByTestId } = render(<CanvasShell nodes={[]} edges={[]} />);
|
|
39
|
+
expect(getByTestId("react-flow")).toBeInTheDocument();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("renders the Background when background prop is true (default)", () => {
|
|
43
|
+
const { getByTestId } = render(<CanvasShell nodes={[]} edges={[]} />);
|
|
44
|
+
expect(getByTestId("rf-background")).toBeInTheDocument();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("omits the Background when background={false}", () => {
|
|
48
|
+
const { queryByTestId } = render(<CanvasShell nodes={[]} edges={[]} background={false} />);
|
|
49
|
+
expect(queryByTestId("rf-background")).not.toBeInTheDocument();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("applies custom className to the wrapper div", () => {
|
|
53
|
+
const { container } = render(<CanvasShell nodes={[]} edges={[]} className="my-canvas" />);
|
|
54
|
+
expect(container.firstChild).toHaveClass("my-canvas");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("renders children inside the flow", () => {
|
|
58
|
+
const { getByTestId } = render(
|
|
59
|
+
<CanvasShell nodes={[]} edges={[]}>
|
|
60
|
+
<div data-testid="child-panel">child</div>
|
|
61
|
+
</CanvasShell>,
|
|
62
|
+
);
|
|
63
|
+
expect(getByTestId("child-panel")).toBeInTheDocument();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("passes branded default ariaLabelConfig to ReactFlow", () => {
|
|
67
|
+
render(<CanvasShell nodes={[]} edges={[]} />);
|
|
68
|
+
const props = reactFlowSpy.mock.calls[0]?.[0];
|
|
69
|
+
expect(props.ariaLabelConfig).toMatchObject({
|
|
70
|
+
"controls.ariaLabel": "Canvas controls",
|
|
71
|
+
"controls.zoomIn.ariaLabel": "Zoom in",
|
|
72
|
+
"minimap.ariaLabel": "Minimap",
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// The badge is hidden by product decision, not by licence: @xyflow/react is MIT
|
|
77
|
+
// (notice in source copies, no rendered badge required). This test exists so a
|
|
78
|
+
// future agent cannot quietly flip it back — see .claude/rules/react-flow-components.md.
|
|
79
|
+
it("hides the React Flow attribution badge", () => {
|
|
80
|
+
render(<CanvasShell nodes={[]} edges={[]} />);
|
|
81
|
+
const props = reactFlowSpy.mock.calls[0]?.[0];
|
|
82
|
+
expect(props.proOptions).toMatchObject({ hideAttribution: true });
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("lets a caller restore the attribution badge via proOptions", () => {
|
|
86
|
+
render(<CanvasShell nodes={[]} edges={[]} proOptions={{ hideAttribution: false }} />);
|
|
87
|
+
const props = reactFlowSpy.mock.calls[0]?.[0];
|
|
88
|
+
expect(props.proOptions).toMatchObject({ hideAttribution: false });
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("lets a caller-supplied ariaLabelConfig override the branded defaults", () => {
|
|
92
|
+
render(
|
|
93
|
+
<CanvasShell
|
|
94
|
+
nodes={[]}
|
|
95
|
+
edges={[]}
|
|
96
|
+
ariaLabelConfig={{ "controls.ariaLabel": "Custom controls" }}
|
|
97
|
+
/>,
|
|
98
|
+
);
|
|
99
|
+
const props = reactFlowSpy.mock.calls[0]?.[0];
|
|
100
|
+
expect(props.ariaLabelConfig).toMatchObject({
|
|
101
|
+
"controls.ariaLabel": "Custom controls",
|
|
102
|
+
// Unset keys still fall back to the branded default, not the library's.
|
|
103
|
+
"controls.zoomIn.ariaLabel": "Zoom in",
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
});
|