From 59224880d2125bd9c0a2f1f3f5b0bb7bf1243f53 Mon Sep 17 00:00:00 2001 From: dtoro Date: Sat, 7 Mar 2026 10:43:26 +0100 Subject: [PATCH] resize --- src/App.tsx | 13 +++- src/components/graph/BaseNode.tsx | 61 +++++++++++++-- src/components/graph/ConfigNode.tsx | 17 +++-- src/components/graph/FunctionNode.tsx | 16 ++-- src/components/graph/RenderingNode.tsx | 102 +++++++++++++------------ src/components/graph/VariableNode.tsx | 4 +- src/hooks/useResizeHeight.ts | 27 +++++++ 7 files changed, 164 insertions(+), 76 deletions(-) create mode 100644 src/hooks/useResizeHeight.ts diff --git a/src/App.tsx b/src/App.tsx index a6c2599..15ef3eb 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -39,18 +39,27 @@ const snapToGrid = (x: number, y: number): { x: number; y: number } => ({ y: Math.round(y / SNAP_GRID[1]) * SNAP_GRID[1], }) +const DEFAULT_NODE_STYLE: Record = { + config: { width: 320, height: 320 }, + render: { width: 384, height: 320 }, + variable: { width: 224, height: 180 }, + function: { width: 288, height: 260 }, +} + const initialNodes: Node[] = [ { id: 'config-1', position: { x: 50, y: 50 }, data: { plantuml: '@startuml\nactor User\nparticipant "Render" as R\nUser -> R : config\n@enduml\n' }, type: 'config', + style: DEFAULT_NODE_STYLE.config, }, { id: 'render-1', position: { x: 350, y: 80 }, data: {}, type: 'render', + style: DEFAULT_NODE_STYLE.render, }, ].map((n) => ({ ...n, id: n.id ?? genId() })) @@ -133,11 +142,13 @@ export default function App() { variable: { value: '', valueType: 'string' }, function: { body: '// args[0], args[1], ...\nreturn args[0];' }, } + const nodeType = typeMap[type] ?? 'config' const newNode: Node = { id, - type: typeMap[type] ?? 'config', + type: nodeType, position: { x: position.x, y: position.y }, data: dataMap[type] ?? {}, + style: DEFAULT_NODE_STYLE[nodeType] ?? DEFAULT_NODE_STYLE.config, } setNodes((nds) => nds.concat(newNode)) lastClickRef.current = null diff --git a/src/components/graph/BaseNode.tsx b/src/components/graph/BaseNode.tsx index 613df63..bb4631b 100644 --- a/src/components/graph/BaseNode.tsx +++ b/src/components/graph/BaseNode.tsx @@ -1,8 +1,31 @@ -import type { ComponentProps } from "react"; +import type { ComponentProps, ReactNode } from "react"; +import { NodeResizeControl } from "@xyflow/react"; +import { Expand } from "lucide-react"; import { cn } from "@/lib/utils"; -export function BaseNode({ className, ...props }: ComponentProps<"div">) { +export type BaseNodeProps = ComponentProps<"div"> & { + /** When true, shows a bottom-right resize handle. Requires nodeId when used inside React Flow. */ + resizable?: boolean; + /** Node id for the resize control (required when resizable is true). */ + nodeId?: string; + /** Connection handles (InputHandle, OutputHandle). Rendered outside the overflow layer so they stay visible. */ + handles?: ReactNode; +}; + +export function BaseNode({ + className, + style, + resizable, + nodeId, + handles, + children, + ...props +}: BaseNodeProps) { + const appliedStyle = style + ? { ...style, display: "flex", flexDirection: "column" as const } + : undefined; + return (
) { "[.react-flow\\_\\_node.selected_&]:shadow-lg", className, )} + style={appliedStyle} tabIndex={0} {...props} - /> + > +
{children}
+ {resizable && nodeId && ( +
+ + + + + +
+ )} + {handles} +
); } @@ -35,7 +84,7 @@ export function BaseNodeHeader({
` component. className, @@ -68,7 +117,7 @@ export function BaseNodeContent({ return (
); @@ -79,7 +128,7 @@ export function BaseNodeFooter({ className, ...props }: ComponentProps<"div">) {
(data?.plantuml ?? '@startuml\n\n@enduml\n') const { theme } = useTheme() const ctx = useContext(FlowContext) @@ -114,9 +117,10 @@ export const ConfigNode = memo(function ConfigNode({ id, data }: Props) { ) const extensions = useMemo(() => [plantumlLanguage.extension], []) + const [editorHeight, editorContainerRef] = useResizeHeight(180) return ( - + 0 && height > 0 ? { width, height } : undefined} resizable nodeId={id} handles={<>}> {id}.puml @@ -124,7 +128,7 @@ export const ConfigNode = memo(function ConfigNode({ id, data }: Props) { {hasDependencies && ( -
+
@@ -194,12 +198,12 @@ export const ConfigNode = memo(function ConfigNode({ id, data }: Props) {
)} -
+
{storedPlantuml ? `${storedPlantuml.length} chars` : 'none'}
- - - ) }) diff --git a/src/components/graph/FunctionNode.tsx b/src/components/graph/FunctionNode.tsx index 056e6f5..9cfb7ac 100644 --- a/src/components/graph/FunctionNode.tsx +++ b/src/components/graph/FunctionNode.tsx @@ -2,6 +2,7 @@ import React, { memo, useCallback, useContext, useEffect, useMemo, useRef, useSt import CodeMirror from '@uiw/react-codemirror' import { javascript } from '@codemirror/lang-javascript' import FlowContext from '../../lib/flowContext' +import { useResizeHeight } from '../../hooks/useResizeHeight' import { useTheme } from '../../lib/themeContext' import { BaseNode, @@ -16,13 +17,14 @@ import { Code2 } from 'lucide-react' type Props = { id: string data: { body?: string } + style?: React.CSSProperties } const DEFAULT_BODY = `// args[0], args[1], ... are the variable values (in order) return args[0]; ` -export const FunctionNode = memo(function FunctionNode({ id, data }: Props) { +export const FunctionNode = memo(function FunctionNode({ id, data, width, height }: Props) { const [value, setValue] = useState(data?.body ?? DEFAULT_BODY) const { theme } = useTheme() const ctx = useContext(FlowContext) @@ -52,22 +54,23 @@ export const FunctionNode = memo(function FunctionNode({ id, data }: Props) { const storedBody = ctx?.nodes?.find((n: any) => n.id === id)?.data?.body ?? '' const extensions = useMemo(() => [javascript()], []) + const [editorHeight, editorContainerRef] = useResizeHeight(120) return ( - + 0 && height > 0 ? { width, height } : undefined} resizable nodeId={id} handles={<>}> {id} -

+

Call in config: ${{id}(var1, var2)}

-
+
- - - ) }) diff --git a/src/components/graph/RenderingNode.tsx b/src/components/graph/RenderingNode.tsx index 1f315e9..eb8a900 100644 --- a/src/components/graph/RenderingNode.tsx +++ b/src/components/graph/RenderingNode.tsx @@ -18,8 +18,11 @@ const KROKI_PLANTUML_SVG = '/api/kroki/plantuml/svg' type Props = { id: string data?: any + style?: React.CSSProperties } +const DEFAULT_CONFIG_NODE_STYLE = { width: 320, height: 320 } + const DARK_SKINPARAMS = ` skinparam backgroundColor #1e1e1e skinparam defaultFontColor #e0e0e0 @@ -63,7 +66,7 @@ skinparam class { } ` -export const RenderingNode = memo(function RenderingNode({ id }: Props) { +export const RenderingNode = memo(function RenderingNode({ id, width, height }: Props) { const [svgContent, setSvgContent] = useState(null) const [error, setError] = useState(null) const [loading, setLoading] = useState(false) @@ -264,58 +267,60 @@ export const RenderingNode = memo(function RenderingNode({ id }: Props) { }, [id, theme, plantumlText, configSignature, edgesSignature, variablesSignature, functionsSignature]) return ( - + 0 && height > 0 ? { width, height } : undefined} resizable nodeId={id} handles={<>}> {id} - {incomingIds.length === 0 ? ( - - - - - - No configuration connected - Connect a Configuration node or create one. The renderer will display the PlantUML diagram. - - - - - - ) : error ? ( - srcData?.renderError ? ( - srcData.renderError(error) - ) : srcData?.errorHtml ? ( -
- ) : ( -
{error.message}
- ) - ) : loading ? ( -
Rendering…
- ) : svgContent ? ( -
- ) : null} +
+ {incomingIds.length === 0 ? ( + + + + + + No configuration connected + Connect a Configuration node or create one. The renderer will display the PlantUML diagram. + + + + + + ) : error ? ( + srcData?.renderError ? ( + srcData.renderError(error) + ) : srcData?.errorHtml ? ( +
+ ) : ( +
{error.message}
+ ) + ) : loading ? ( +
Rendering…
+ ) : svgContent ? ( +
+ ) : null} +
@@ -323,9 +328,6 @@ export const RenderingNode = memo(function RenderingNode({ id }: Props) { {svgContent ? 'PlantUML diagram' : error ? 'Error' : '—'}
- - - ) }) diff --git a/src/components/graph/VariableNode.tsx b/src/components/graph/VariableNode.tsx index 146a627..3fc9559 100644 --- a/src/components/graph/VariableNode.tsx +++ b/src/components/graph/VariableNode.tsx @@ -88,7 +88,7 @@ export const VariableNode = memo(function VariableNode({ id, data }: Props) { ) return ( - + }> {id} @@ -130,8 +130,6 @@ export const VariableNode = memo(function VariableNode({ id, data }: Props) { Use in config: prop: ${{id}}

- -
) }) diff --git a/src/hooks/useResizeHeight.ts b/src/hooks/useResizeHeight.ts new file mode 100644 index 0000000..0b4b2ea --- /dev/null +++ b/src/hooks/useResizeHeight.ts @@ -0,0 +1,27 @@ +import { useEffect, useRef, useState } from 'react' + +/** + * Returns the height of the observed element, updating when it resizes (e.g. node resize). + * Used to give CodeMirror and other components an explicit height that tracks their container. + */ +export function useResizeHeight(defaultHeight: number): [number, React.RefObject] { + const ref = useRef(null) + const [height, setHeight] = useState(defaultHeight) + + useEffect(() => { + const el = ref.current + if (!el) return + + const ro = new ResizeObserver((entries) => { + const entry = entries[0] + if (entry?.contentRect.height != null && entry.contentRect.height > 0) { + setHeight(entry.contentRect.height) + } + }) + ro.observe(el) + setHeight(el.getBoundingClientRect().height) + return () => ro.disconnect() + }, []) + + return [height, ref] +}