feat: integrate centered flow node positioning in canvas components

- Added useCenteredFlowNodePosition hook to improve node placement in the CanvasCommandPalette and CanvasToolbar.
- Updated node creation logic to utilize centered positioning, enhancing the visual layout and user experience during node interactions.
- Refactored offset calculations to stagger node positions based on the current node count, ensuring a more organized canvas layout.
This commit is contained in:
Matthias
2026-03-27 23:56:06 +01:00
parent 83c0073d51
commit 5dd5dcf55b
3 changed files with 52 additions and 4 deletions

View File

@@ -15,6 +15,7 @@ import {
} from "lucide-react"; } from "lucide-react";
import { useCanvasPlacement } from "@/components/canvas/canvas-placement-context"; import { useCanvasPlacement } from "@/components/canvas/canvas-placement-context";
import { useCenteredFlowNodePosition } from "@/hooks/use-centered-flow-node-position";
import { import {
Command, Command,
CommandDialog, CommandDialog,
@@ -53,6 +54,7 @@ const NODE_SEARCH_KEYWORDS: Partial<
export function CanvasCommandPalette() { export function CanvasCommandPalette() {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const { createNodeWithIntersection } = useCanvasPlacement(); const { createNodeWithIntersection } = useCanvasPlacement();
const getCenteredPosition = useCenteredFlowNodePosition();
const { setTheme } = useTheme(); const { setTheme } = useTheme();
const nodeCountRef = useRef(0); const nodeCountRef = useRef(0);
@@ -73,12 +75,12 @@ export function CanvasCommandPalette() {
width: number, width: number,
height: number, height: number,
) => { ) => {
const offset = (nodeCountRef.current % 8) * 24; const stagger = (nodeCountRef.current % 8) * 24;
nodeCountRef.current += 1; nodeCountRef.current += 1;
setOpen(false); setOpen(false);
void createNodeWithIntersection({ void createNodeWithIntersection({
type, type,
position: { x: 100 + offset, y: 100 + offset }, position: getCenteredPosition(width, height, stagger),
width, width,
height, height,
data, data,

View File

@@ -5,6 +5,7 @@ import { useRef } from "react";
import { CreditDisplay } from "@/components/canvas/credit-display"; import { CreditDisplay } from "@/components/canvas/credit-display";
import { ExportButton } from "@/components/canvas/export-button"; import { ExportButton } from "@/components/canvas/export-button";
import { useCanvasPlacement } from "@/components/canvas/canvas-placement-context"; import { useCanvasPlacement } from "@/components/canvas/canvas-placement-context";
import { useCenteredFlowNodePosition } from "@/hooks/use-centered-flow-node-position";
import { import {
CANVAS_NODE_TEMPLATES, CANVAS_NODE_TEMPLATES,
type CanvasNodeTemplate, type CanvasNodeTemplate,
@@ -18,6 +19,7 @@ export default function CanvasToolbar({
canvasName, canvasName,
}: CanvasToolbarProps) { }: CanvasToolbarProps) {
const { createNodeWithIntersection } = useCanvasPlacement(); const { createNodeWithIntersection } = useCanvasPlacement();
const getCenteredPosition = useCenteredFlowNodePosition();
const nodeCountRef = useRef(0); const nodeCountRef = useRef(0);
const handleAddNode = async ( const handleAddNode = async (
@@ -26,11 +28,11 @@ export default function CanvasToolbar({
width: number, width: number,
height: number, height: number,
) => { ) => {
const offset = (nodeCountRef.current % 8) * 24; const stagger = (nodeCountRef.current % 8) * 24;
nodeCountRef.current += 1; nodeCountRef.current += 1;
await createNodeWithIntersection({ await createNodeWithIntersection({
type, type,
position: { x: 100 + offset, y: 100 + offset }, position: getCenteredPosition(width, height, stagger),
width, width,
height, height,
data, data,

View File

@@ -0,0 +1,44 @@
"use client";
import { useCallback } from "react";
import { useReactFlow } from "@xyflow/react";
const SNAP = 16;
function snapToGrid(x: number, y: number): { x: number; y: number } {
return {
x: Math.round(x / SNAP) * SNAP,
y: Math.round(y / SNAP) * SNAP,
};
}
/**
* Top-left flow position for a node centered in the visible React Flow pane
* (viewport), with optional stagger and 16px grid snap to match the canvas.
*/
export function useCenteredFlowNodePosition() {
const { screenToFlowPosition } = useReactFlow();
return useCallback(
(width: number, height: number, stagger: number) => {
const pane = document.querySelector(".react-flow__pane");
const rect =
pane?.getBoundingClientRect() ??
document.querySelector(".react-flow")?.getBoundingClientRect();
if (!rect || rect.width === 0 || rect.height === 0) {
return snapToGrid(100 + stagger, 100 + stagger);
}
const center = screenToFlowPosition({
x: rect.left + rect.width / 2,
y: rect.top + rect.height / 2,
});
const x = center.x - width / 2 + stagger;
const y = center.y - height / 2 + stagger;
return snapToGrid(x, y);
},
[screenToFlowPosition],
);
}