From 2903f3d4975406dcba0f3000eff041a0ceb6829c Mon Sep 17 00:00:00 2001 From: Abby263 Date: Fri, 1 May 2026 22:09:07 -0400 Subject: [PATCH] Render architecture mermaid diagrams --- package-lock.json | 107 ++++---- package.json | 4 +- src/app/projects/[id]/artifacts/page.tsx | 28 +- src/components/MermaidDiagram.tsx | 317 +++++++++++++++++++++++ 4 files changed, 400 insertions(+), 56 deletions(-) create mode 100644 src/components/MermaidDiagram.tsx diff --git a/package-lock.json b/package-lock.json index 7d1cfed..55092ff 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,17 +1,18 @@ { "name": "product-dev-blueprint", - "version": "0.1.0", + "version": "0.5.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "product-dev-blueprint", - "version": "0.1.0", + "version": "0.5.0", + "license": "MIT", "dependencies": { "clsx": "2.1.1", "docx": "^8.5.0", "jszip": "3.10.1", - "next": "14.2.18", + "next": "14.2.35", "react": "18.3.1", "react-dom": "18.3.1", "react-markdown": "9.0.1", @@ -23,9 +24,12 @@ "@types/react": "18.3.12", "@types/react-dom": "18.3.1", "autoprefixer": "10.4.20", - "postcss": "8.4.49", + "postcss": "8.5.10", "tailwindcss": "3.4.14", "typescript": "5.6.3" + }, + "engines": { + "node": ">=20" } }, "node_modules/@alloc/quick-lru": { @@ -81,15 +85,15 @@ } }, "node_modules/@next/env": { - "version": "14.2.18", - "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.18.tgz", - "integrity": "sha512-2vWLOUwIPgoqMJKG6dt35fVXVhgM09tw4tK3/Q34GFXDrfiHlG7iS33VA4ggnjWxjiz9KV5xzfsQzJX6vGAekA==", + "version": "14.2.35", + "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.35.tgz", + "integrity": "sha512-DuhvCtj4t9Gwrx80dmz2F4t/zKQ4ktN8WrMwOuVzkJfBilwAwGr6v16M5eI8yCuZ63H9TTuEU09Iu2HqkzFPVQ==", "license": "MIT" }, "node_modules/@next/swc-darwin-arm64": { - "version": "14.2.18", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.18.tgz", - "integrity": "sha512-tOBlDHCjGdyLf0ube/rDUs6VtwNOajaWV+5FV/ajPgrvHeisllEdymY/oDgv2cx561+gJksfMUtqf8crug7sbA==", + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.33.tgz", + "integrity": "sha512-HqYnb6pxlsshoSTubdXKu15g3iivcbsMXg4bYpjL2iS/V6aQot+iyF4BUc2qA/J/n55YtvE4PHMKWBKGCF/+wA==", "cpu": [ "arm64" ], @@ -103,9 +107,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "14.2.18", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.18.tgz", - "integrity": "sha512-uJCEjutt5VeJ30jjrHV1VIHCsbMYnEqytQgvREx+DjURd/fmKy15NaVK4aR/u98S1LGTnjq35lRTnRyygglxoA==", + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.33.tgz", + "integrity": "sha512-8HGBeAE5rX3jzKvF593XTTFg3gxeU4f+UWnswa6JPhzaR6+zblO5+fjltJWIZc4aUalqTclvN2QtTC37LxvZAA==", "cpu": [ "x64" ], @@ -119,9 +123,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "14.2.18", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.18.tgz", - "integrity": "sha512-IL6rU8vnBB+BAm6YSWZewc+qvdL1EaA+VhLQ6tlUc0xp+kkdxQrVqAnh8Zek1ccKHlTDFRyAft0e60gteYmQ4A==", + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.33.tgz", + "integrity": "sha512-JXMBka6lNNmqbkvcTtaX8Gu5by9547bukHQvPoLe9VRBx1gHwzf5tdt4AaezW85HAB3pikcvyqBToRTDA4DeLw==", "cpu": [ "arm64" ], @@ -135,9 +139,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "14.2.18", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.18.tgz", - "integrity": "sha512-RCaENbIZqKKqTlL8KNd+AZV/yAdCsovblOpYFp0OJ7ZxgLNbV5w23CUU1G5On+0fgafrsGcW+GdMKdFjaRwyYA==", + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.33.tgz", + "integrity": "sha512-Bm+QulsAItD/x6Ih8wGIMfRJy4G73tu1HJsrccPW6AfqdZd0Sfm5Imhgkgq2+kly065rYMnCOxTBvmvFY1BKfg==", "cpu": [ "arm64" ], @@ -151,9 +155,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "14.2.18", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.18.tgz", - "integrity": "sha512-3kmv8DlyhPRCEBM1Vavn8NjyXtMeQ49ID0Olr/Sut7pgzaQTo4h01S7Z8YNE0VtbowyuAL26ibcz0ka6xCTH5g==", + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.33.tgz", + "integrity": "sha512-FnFn+ZBgsVMbGDsTqo8zsnRzydvsGV8vfiWwUo1LD8FTmPTdV+otGSWKc4LJec0oSexFnCYVO4hX8P8qQKaSlg==", "cpu": [ "x64" ], @@ -167,9 +171,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "14.2.18", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.18.tgz", - "integrity": "sha512-mliTfa8seVSpTbVEcKEXGjC18+TDII8ykW4a36au97spm9XMPqQTpdGPNBJ9RySSFw9/hLuaCMByluQIAnkzlw==", + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.33.tgz", + "integrity": "sha512-345tsIWMzoXaQndUTDv1qypDRiebFxGYx9pYkhwY4hBRaOLt8UGfiWKr9FSSHs25dFIf8ZqIFaPdy5MljdoawA==", "cpu": [ "x64" ], @@ -183,9 +187,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "14.2.18", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.18.tgz", - "integrity": "sha512-J5g0UFPbAjKYmqS3Cy7l2fetFmWMY9Oao32eUsBPYohts26BdrMUyfCJnZFQkX9npYaHNDOWqZ6uV9hSDPw9NA==", + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.33.tgz", + "integrity": "sha512-nscpt0G6UCTkrT2ppnJnFsYbPDQwmum4GNXYTeoTIdsmMydSKFz9Iny2jpaRupTb+Wl298+Rh82WKzt9LCcqSQ==", "cpu": [ "arm64" ], @@ -199,9 +203,9 @@ } }, "node_modules/@next/swc-win32-ia32-msvc": { - "version": "14.2.18", - "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.18.tgz", - "integrity": "sha512-Ynxuk4ZgIpdcN7d16ivJdjsDG1+3hTvK24Pp8DiDmIa2+A4CfhJSEHHVndCHok6rnLUzAZD+/UOKESQgTsAZGg==", + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.33.tgz", + "integrity": "sha512-pc9LpGNKhJ0dXQhZ5QMmYxtARwwmWLpeocFmVG5Z0DzWq5Uf0izcI8tLc+qOpqxO1PWqZ5A7J1blrUIKrIFc7Q==", "cpu": [ "ia32" ], @@ -215,9 +219,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "14.2.18", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.18.tgz", - "integrity": "sha512-dtRGMhiU9TN5nyhwzce+7c/4CCeykYS+ipY/4mIrGzJ71+7zNo55ZxCB7cAVuNqdwtYniFNR2c9OFQ6UdFIMcg==", + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.33.tgz", + "integrity": "sha512-nOjfZMy8B94MdisuzZo9/57xuFVLHJaDj5e/xrduJp9CV2/HrfxTRH2fbyLe+K9QT41WBLUd4iXX3R7jBp0EUg==", "cpu": [ "x64" ], @@ -2184,13 +2188,12 @@ } }, "node_modules/next": { - "version": "14.2.18", - "resolved": "https://registry.npmjs.org/next/-/next-14.2.18.tgz", - "integrity": "sha512-H9qbjDuGivUDEnK6wa+p2XKO+iMzgVgyr9Zp/4Iv29lKa+DYaxJGjOeEA+5VOvJh/M7HLiskehInSa0cWxVXUw==", - "deprecated": "This version has a security vulnerability. Please upgrade to a patched version. See https://nextjs.org/blog/security-update-2025-12-11 for more details.", + "version": "14.2.35", + "resolved": "https://registry.npmjs.org/next/-/next-14.2.35.tgz", + "integrity": "sha512-KhYd2Hjt/O1/1aZVX3dCwGXM1QmOV4eNM2UTacK5gipDdPN/oHHK/4oVGy7X8GMfPMsUTUEmGlsy0EY1YGAkig==", "license": "MIT", "dependencies": { - "@next/env": "14.2.18", + "@next/env": "14.2.35", "@swc/helpers": "0.5.5", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", @@ -2205,15 +2208,15 @@ "node": ">=18.17.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "14.2.18", - "@next/swc-darwin-x64": "14.2.18", - "@next/swc-linux-arm64-gnu": "14.2.18", - "@next/swc-linux-arm64-musl": "14.2.18", - "@next/swc-linux-x64-gnu": "14.2.18", - "@next/swc-linux-x64-musl": "14.2.18", - "@next/swc-win32-arm64-msvc": "14.2.18", - "@next/swc-win32-ia32-msvc": "14.2.18", - "@next/swc-win32-x64-msvc": "14.2.18" + "@next/swc-darwin-arm64": "14.2.33", + "@next/swc-darwin-x64": "14.2.33", + "@next/swc-linux-arm64-gnu": "14.2.33", + "@next/swc-linux-arm64-musl": "14.2.33", + "@next/swc-linux-x64-gnu": "14.2.33", + "@next/swc-linux-x64-musl": "14.2.33", + "@next/swc-win32-arm64-msvc": "14.2.33", + "@next/swc-win32-ia32-msvc": "14.2.33", + "@next/swc-win32-x64-msvc": "14.2.33" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", @@ -2387,9 +2390,9 @@ } }, "node_modules/postcss": { - "version": "8.4.49", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", - "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", + "version": "8.5.10", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz", + "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==", "dev": true, "funding": [ { @@ -2407,7 +2410,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.7", + "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, diff --git a/package.json b/package.json index 80606d6..77bc21e 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "clsx": "2.1.1", "docx": "^8.5.0", "jszip": "3.10.1", - "next": "14.2.18", + "next": "14.2.35", "react": "18.3.1", "react-dom": "18.3.1", "react-markdown": "9.0.1", @@ -29,7 +29,7 @@ "@types/react": "18.3.12", "@types/react-dom": "18.3.1", "autoprefixer": "10.4.20", - "postcss": "8.4.49", + "postcss": "8.5.10", "tailwindcss": "3.4.14", "typescript": "5.6.3" } diff --git a/src/app/projects/[id]/artifacts/page.tsx b/src/app/projects/[id]/artifacts/page.tsx index 148a4a8..7045162 100644 --- a/src/app/projects/[id]/artifacts/page.tsx +++ b/src/app/projects/[id]/artifacts/page.tsx @@ -3,10 +3,12 @@ import Link from "next/link"; import ReactMarkdown from "react-markdown"; import remarkGfm from "remark-gfm"; -import { useEffect, useMemo, useState } from "react"; +import { Children, isValidElement, useEffect, useMemo, useState, type ReactNode } from "react"; +import type { Components } from "react-markdown"; import { useParams } from "next/navigation"; import { useStore } from "@/lib/store"; import { Badge, Button, Card } from "@/components/ui"; +import MermaidDiagram from "@/components/MermaidDiagram"; import { generateBundle } from "@/lib/generators"; import { downloadFile, downloadProjectBundle, downloadProjectJSON } from "@/lib/export"; import { downloadDocx } from "@/lib/docx"; @@ -20,6 +22,26 @@ const ARTIFACT_GROUPS = [ { title: "Implementation", keys: ["coding-agent-prompts"] }, ]; +type CodeElementProps = { + className?: string; + children?: ReactNode; +}; + +function mermaidCodeFromPre(children: ReactNode) { + const child = Children.toArray(children)[0]; + if (!isValidElement(child)) return null; + if (!child.props.className?.split(/\s+/).includes("language-mermaid")) return null; + return String(child.props.children || "").replace(/\n$/, ""); +} + +const markdownComponents: Components = { + pre({ node: _node, children, ...props }) { + const chart = mermaidCodeFromPre(children); + if (chart) return ; + return
{children}
; + }, +}; + export default function ArtifactsPage() { const params = useParams<{ id: string }>(); const id = params.id; @@ -153,7 +175,9 @@ export default function ArtifactsPage() {
- {active.body} + + {active.body} +
diff --git a/src/components/MermaidDiagram.tsx b/src/components/MermaidDiagram.tsx new file mode 100644 index 0000000..445a6a4 --- /dev/null +++ b/src/components/MermaidDiagram.tsx @@ -0,0 +1,317 @@ +"use client"; + +import { useId, useMemo } from "react"; + +type Direction = "LR" | "TD"; + +interface ParsedNode { + id: string; + label: string; +} + +interface ParsedEdge { + from: string; + to: string; + label?: string; + dashed: boolean; +} + +interface ParsedDiagram { + direction: Direction; + nodes: ParsedNode[]; + edges: ParsedEdge[]; +} + +interface PositionedNode extends ParsedNode { + x: number; + y: number; +} + +const NODE_WIDTH = 220; +const NODE_HEIGHT = 74; +const PADDING = 28; +const H_GAP = 78; +const V_GAP = 34; + +function normalizeDirection(value: string): Direction { + return value === "TD" || value === "TB" ? "TD" : "LR"; +} + +function parseEndpoint(raw: string) { + const text = raw.trim().replace(/;$/, ""); + const match = text.match(/^([A-Za-z][\w-]*)(?:\["([\s\S]*?)"\])?$/); + if (!match) return null; + return { id: match[1], label: match[2] || match[1] }; +} + +function parseMermaidFlowchart(chart: string): ParsedDiagram | null { + const lines = chart + .split(/\r?\n/) + .map((line) => line.trim()) + .filter(Boolean); + + const header = lines.find((line) => /^flowchart\s+/i.test(line)); + const directionMatch = header?.match(/^flowchart\s+(LR|RL|TD|TB|BT)/i); + if (!directionMatch) return null; + + const direction = normalizeDirection(directionMatch[1].toUpperCase()); + const nodes = new Map(); + const edges: ParsedEdge[] = []; + + function upsert(endpoint: { id: string; label: string }) { + const current = nodes.get(endpoint.id); + if (!current || current.label === endpoint.id) { + nodes.set(endpoint.id, endpoint); + } + } + + for (const line of lines) { + if (/^flowchart\s+/i.test(line) || line.startsWith("%%")) continue; + + let left = ""; + let right = ""; + let label: string | undefined; + let dashed = false; + + if (line.includes("-->")) { + const parts = line.split("-->"); + left = parts[0]; + right = parts.slice(1).join("-->"); + } else { + const dashedMatch = line.match(/-\.\s*([\s\S]*?)\s*\.->/); + if (!dashedMatch) continue; + const parts = line.split(dashedMatch[0]); + left = parts[0]; + right = parts.slice(1).join(dashedMatch[0]); + label = dashedMatch[1].trim() || undefined; + dashed = true; + } + + const from = parseEndpoint(left); + const to = parseEndpoint(right); + if (!from || !to) continue; + + upsert(from); + upsert(to); + edges.push({ from: from.id, to: to.id, label, dashed }); + } + + if (nodes.size === 0) return null; + return { direction, nodes: Array.from(nodes.values()), edges }; +} + +function wrapLabel(label: string, maxChars = 25, maxLines = 3) { + const words = label.split(/\s+/).filter(Boolean); + const lines: string[] = []; + let current = ""; + + for (const word of words) { + const next = current ? `${current} ${word}` : word; + if (next.length <= maxChars) { + current = next; + continue; + } + if (current) lines.push(current); + current = word; + if (lines.length === maxLines - 1) break; + } + + if (current && lines.length < maxLines) lines.push(current); + const usedWords = lines.join(" ").split(/\s+/).filter(Boolean).length; + if (usedWords < words.length && lines.length > 0) { + lines[lines.length - 1] = `${lines[lines.length - 1].replace(/\.*$/, "")}...`; + } + return lines.length > 0 ? lines : [label]; +} + +function layoutDiagram(diagram: ParsedDiagram) { + const depth = new Map(); + const order = new Map(); + diagram.nodes.forEach((node, index) => { + depth.set(node.id, 0); + order.set(node.id, index); + }); + + for (let i = 0; i < diagram.nodes.length; i += 1) { + let changed = false; + for (const edge of diagram.edges) { + const fromDepth = depth.get(edge.from) ?? 0; + const toDepth = depth.get(edge.to) ?? 0; + if (toDepth <= fromDepth) { + depth.set(edge.to, fromDepth + 1); + changed = true; + } + } + if (!changed) break; + } + + const layers = new Map(); + for (const node of diagram.nodes) { + const layer = depth.get(node.id) ?? 0; + layers.set(layer, [...(layers.get(layer) || []), node]); + } + + const orderedLayers = Array.from(layers.entries()) + .sort(([a], [b]) => a - b) + .map(([, nodes]) => nodes.sort((a, b) => (order.get(a.id) ?? 0) - (order.get(b.id) ?? 0))); + + const maxLayerSize = Math.max(...orderedLayers.map((layer) => layer.length), 1); + const positioned = new Map(); + + if (diagram.direction === "LR") { + const width = PADDING * 2 + orderedLayers.length * NODE_WIDTH + Math.max(0, orderedLayers.length - 1) * H_GAP; + const height = PADDING * 2 + maxLayerSize * NODE_HEIGHT + Math.max(0, maxLayerSize - 1) * V_GAP; + + orderedLayers.forEach((layer, layerIndex) => { + const layerHeight = layer.length * NODE_HEIGHT + Math.max(0, layer.length - 1) * V_GAP; + const topOffset = PADDING + Math.max(0, (height - PADDING * 2 - layerHeight) / 2); + layer.forEach((node, nodeIndex) => { + positioned.set(node.id, { + ...node, + x: PADDING + layerIndex * (NODE_WIDTH + H_GAP), + y: topOffset + nodeIndex * (NODE_HEIGHT + V_GAP), + }); + }); + }); + + return { width, height, nodes: positioned }; + } + + const width = PADDING * 2 + maxLayerSize * NODE_WIDTH + Math.max(0, maxLayerSize - 1) * H_GAP; + const height = PADDING * 2 + orderedLayers.length * NODE_HEIGHT + Math.max(0, orderedLayers.length - 1) * V_GAP; + + orderedLayers.forEach((layer, layerIndex) => { + const layerWidth = layer.length * NODE_WIDTH + Math.max(0, layer.length - 1) * H_GAP; + const leftOffset = PADDING + Math.max(0, (width - PADDING * 2 - layerWidth) / 2); + layer.forEach((node, nodeIndex) => { + positioned.set(node.id, { + ...node, + x: leftOffset + nodeIndex * (NODE_WIDTH + H_GAP), + y: PADDING + layerIndex * (NODE_HEIGHT + V_GAP), + }); + }); + }); + + return { width, height, nodes: positioned }; +} + +function edgePath(from: PositionedNode, to: PositionedNode, direction: Direction) { + if (direction === "LR") { + const startX = from.x + NODE_WIDTH; + const startY = from.y + NODE_HEIGHT / 2; + const endX = to.x; + const endY = to.y + NODE_HEIGHT / 2; + const handle = Math.max(36, (endX - startX) / 2); + return `M ${startX} ${startY} C ${startX + handle} ${startY}, ${endX - handle} ${endY}, ${endX} ${endY}`; + } + + const startX = from.x + NODE_WIDTH / 2; + const startY = from.y + NODE_HEIGHT; + const endX = to.x + NODE_WIDTH / 2; + const endY = to.y; + const handle = Math.max(30, (endY - startY) / 2); + return `M ${startX} ${startY} C ${startX} ${startY + handle}, ${endX} ${endY - handle}, ${endX} ${endY}`; +} + +export default function MermaidDiagram({ chart }: { chart: string }) { + const markerId = useId().replace(/:/g, ""); + const diagram = useMemo(() => parseMermaidFlowchart(chart), [chart]); + const layout = useMemo(() => (diagram ? layoutDiagram(diagram) : null), [diagram]); + + if (!diagram || !layout) { + return ( +
+
This Mermaid diagram uses syntax the built-in renderer does not support yet.
+
+          {chart}
+        
+
+ ); + } + + return ( +
+ + + + + + + + {diagram.edges.map((edge, index) => { + const from = layout.nodes.get(edge.from); + const to = layout.nodes.get(edge.to); + if (!from || !to) return null; + const labelX = (from.x + to.x + NODE_WIDTH) / 2; + const labelY = (from.y + to.y + NODE_HEIGHT) / 2 - 8; + return ( + + + {edge.label && ( + + {edge.label} + + )} + + ); + })} + + {Array.from(layout.nodes.values()).map((node) => { + const lines = wrapLabel(node.label); + const firstLineY = node.y + NODE_HEIGHT / 2 - (lines.length - 1) * 8; + return ( + + + {lines.map((line, index) => ( + + {line} + + ))} + + ); + })} + +
+ ); +}