diff --git a/src/components/graph/ConfigNode.tsx b/src/components/graph/ConfigNode.tsx index e4bcce0..125c85a 100644 --- a/src/components/graph/ConfigNode.tsx +++ b/src/components/graph/ConfigNode.tsx @@ -10,7 +10,6 @@ import { BaseNode, BaseNodeContent, BaseNodeFooter, - BaseNodeFooterText, BaseNodeHeaderRow, } from './BaseNode' import { ScrollText } from 'lucide-react' @@ -21,6 +20,7 @@ import { MenubarSubTrigger, } from '../ui/menubar' import { InputHandle, OutputHandle } from './NodeHandles' +import { NodeFooterEdgeIndicators } from './NodeFooterEdgeIndicators' import { NodeHeaderTitle } from './NodeHeaderTitle' import { NodeMenubar } from './NodeMenubar' @@ -264,7 +264,9 @@ export const ConfigNode = memo(function ConfigNode({ id, data, width, height }: - {plantumlValue ? `${plantumlValue.length} chars` : 'none'} + + {plantumlValue ? `${plantumlValue.length} chars` : 'none'} + ) diff --git a/src/components/graph/FunctionNode.tsx b/src/components/graph/FunctionNode.tsx index 3639a8f..ff0cc60 100644 --- a/src/components/graph/FunctionNode.tsx +++ b/src/components/graph/FunctionNode.tsx @@ -8,10 +8,10 @@ import { BaseNode, BaseNodeContent, BaseNodeFooter, - BaseNodeFooterText, BaseNodeHeaderRow, } from './BaseNode' import { InputHandle, OutputHandle } from './NodeHandles' +import { NodeFooterEdgeIndicators } from './NodeFooterEdgeIndicators' import { NodeHeaderTitle } from './NodeHeaderTitle' import { NodeMenubar } from './NodeMenubar' import { Code2 } from 'lucide-react' @@ -69,7 +69,9 @@ export const FunctionNode = memo(function FunctionNode({ id, data, width, height - {bodyValue ? `${bodyValue.length} chars` : 'none'} + + {bodyValue ? `${bodyValue.length} chars` : 'none'} + ) diff --git a/src/components/graph/NodeFooterEdgeIndicators.tsx b/src/components/graph/NodeFooterEdgeIndicators.tsx new file mode 100644 index 0000000..d2c3648 --- /dev/null +++ b/src/components/graph/NodeFooterEdgeIndicators.tsx @@ -0,0 +1,63 @@ +import React, { useContext, useMemo } from 'react' +import FlowContext from '../../lib/flowContext' +import { ArrowDownLeft, ArrowUpRight } from 'lucide-react' + +const NODE_HAS_INPUT: Record = { + config: true, + function: true, + render: true, + variable: false, +} +const NODE_HAS_OUTPUT: Record = { + config: true, + function: true, + render: true, + variable: true, +} + +type Props = { + nodeId: string + nodeType: 'config' | 'function' | 'render' | 'variable' + children?: React.ReactNode +} + +export function NodeFooterEdgeIndicators({ nodeId, nodeType, children }: Props) { + const ctx = useContext(FlowContext) + const edges = ctx?.edges ?? [] + + const { inputs, outputs } = useMemo(() => { + let inputs = 0 + let outputs = 0 + for (const e of edges) { + if (e.target === nodeId) inputs += 1 + if (e.source === nodeId) outputs += 1 + } + return { inputs, outputs } + }, [edges, nodeId]) + + const showInput = NODE_HAS_INPUT[nodeType] ?? false + const showOutput = NODE_HAS_OUTPUT[nodeType] ?? false + + return ( +
+ {showInput && ( + + + {inputs} + + )} + {showOutput && ( + + + {outputs} + + )} + {children != null && ( + <> + {(showInput || showOutput) && |} + {children} + + )} +
+ ) +} diff --git a/src/components/graph/NodeMenubar.tsx b/src/components/graph/NodeMenubar.tsx index 2fec532..a3d81ba 100644 --- a/src/components/graph/NodeMenubar.tsx +++ b/src/components/graph/NodeMenubar.tsx @@ -24,9 +24,11 @@ type Props = { editInputsContent?: React.ReactNode /** Content for Insert → Tags (e.g. Nunjucks tag snippets, config nodes) */ insertTagsContent?: React.ReactNode + /** Extra content in Node menu (e.g. Export submenu for render nodes), before the separator */ + nodeMenuExtraContent?: React.ReactNode } -export function NodeMenubar({ nodeId, nodeType, editInputsContent, insertTagsContent }: Props) { +export function NodeMenubar({ nodeId, nodeType, editInputsContent, insertTagsContent, nodeMenuExtraContent }: Props) { const ctx = useContext(FlowContext) const nodes = ctx?.nodes ?? [] const setNodes = ctx?.setNodes @@ -79,7 +81,7 @@ export function NodeMenubar({ nodeId, nodeType, editInputsContent, insertTagsCon }, [nodeId, ctx]) return ( - + Node @@ -97,6 +99,7 @@ export function NodeMenubar({ nodeId, nodeType, editInputsContent, insertTagsCon Reset + {nodeMenuExtraContent} Delete diff --git a/src/components/graph/RenderingNode.tsx b/src/components/graph/RenderingNode.tsx index d731c9d..99c22b0 100644 --- a/src/components/graph/RenderingNode.tsx +++ b/src/components/graph/RenderingNode.tsx @@ -1,4 +1,4 @@ -import { memo, useContext, useEffect, useMemo, useRef, useState } from 'react' +import { memo, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react' import nunjucks from 'nunjucks' import FlowContext from '../../lib/flowContext' import { useTheme } from '../../lib/themeContext' @@ -7,12 +7,13 @@ import { BaseNode, BaseNodeContent, BaseNodeFooter, - BaseNodeFooterText, BaseNodeHeaderRow, } from './BaseNode' import { DEFAULT_NODE_STYLE, getDefaultDataForType, getNextNodeId } from '../../lib/flowUtils' +import { NodeFooterEdgeIndicators } from './NodeFooterEdgeIndicators' import { NodeHeaderTitle } from './NodeHeaderTitle' import { NodeMenubar } from './NodeMenubar' +import { MenubarItem, MenubarSub, MenubarSubContent, MenubarSubTrigger } from '../ui/menubar' import { Sparkles } from 'lucide-react' import { InputHandle, OutputHandle } from './NodeHandles' @@ -355,13 +356,63 @@ export const RenderingNode = memo(function RenderingNode({ id, width, height }: ? { width, height } : undefined + const downloadSvg = useCallback(() => { + if (!svgContent) return + const blob = new Blob([svgContent], { type: 'image/svg+xml' }) + const url = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = `${id}.svg` + a.click() + URL.revokeObjectURL(url) + }, [id, svgContent]) + + const downloadPng = useCallback(() => { + if (!svgContent) return + const dataUrl = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svgContent) + const img = new Image() + img.onload = () => { + const canvas = document.createElement('canvas') + canvas.width = img.naturalWidth + canvas.height = img.naturalHeight + const ctx = canvas.getContext('2d') + if (!ctx) return + ctx.drawImage(img, 0, 0) + const pngUrl = canvas.toDataURL('image/png') + const a = document.createElement('a') + a.href = pngUrl + a.download = `${id}.png` + a.click() + } + img.onerror = () => {} + img.src = dataUrl + }, [id, svgContent]) + return ( }> } title={} />
- + + + Export + + + + PNG + + + SVG + + + + } + />
{incomingIds.length === 0 ? ( @@ -411,9 +462,9 @@ export const RenderingNode = memo(function RenderingNode({ id, width, height }: - + {svgContent ? 'PlantUML diagram' : error ? 'Error' : '—'} - + ) diff --git a/src/components/graph/VariableNode.tsx b/src/components/graph/VariableNode.tsx index aac2d33..4bdce50 100644 --- a/src/components/graph/VariableNode.tsx +++ b/src/components/graph/VariableNode.tsx @@ -4,13 +4,13 @@ import { BaseNode, BaseNodeContent, BaseNodeFooter, - BaseNodeFooterText, BaseNodeHeaderRow, } from './BaseNode' import { NodeMenubar } from './NodeMenubar' import { Input } from '../ui/input' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select' import { Switch } from '../ui/switch' +import { NodeFooterEdgeIndicators } from './NodeFooterEdgeIndicators' import { NodeHeaderTitle } from './NodeHeaderTitle' import { OutputHandle } from './NodeHandles' import { Variable } from 'lucide-react' @@ -134,8 +134,7 @@ export const VariableNode = memo(function VariableNode({ id, data }: Props) { - - + )