refactor(canvas): unify node handles with shared wrapper

This commit is contained in:
2026-04-11 08:56:45 +02:00
parent ae76289e41
commit db71b2485a
23 changed files with 266 additions and 68 deletions

View File

@@ -28,6 +28,31 @@ vi.mock("@xyflow/react", () => ({
useStore: (selector: (state: StoreState) => unknown) => selector(storeState),
}));
vi.mock("@/components/canvas/canvas-handle", () => ({
default: ({
id,
type,
nodeId,
nodeType,
style,
}: {
id?: string;
type: "source" | "target";
nodeId: string;
nodeType?: string;
style?: React.CSSProperties;
}) => (
<div
data-canvas-handle="true"
data-handle-id={id ?? ""}
data-handle-type={type}
data-node-id={nodeId}
data-node-type={nodeType ?? ""}
data-top={typeof style?.top === "string" ? style.top : ""}
/>
),
}));
vi.mock("../nodes/base-node-wrapper", () => ({
default: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
}));
@@ -261,4 +286,35 @@ describe("CompareNode render preview inputs", () => {
},
});
});
it("renders compare handles through CanvasHandle with preserved ids and positions", () => {
const markup = renderCompareNode({
id: "compare-1",
data: {},
selected: false,
dragging: false,
zIndex: 0,
isConnectable: true,
type: "compare",
xPos: 0,
yPos: 0,
width: 500,
height: 380,
sourcePosition: undefined,
targetPosition: undefined,
positionAbsoluteX: 0,
positionAbsoluteY: 0,
});
expect(markup).toContain('data-canvas-handle="true"');
expect(markup).toContain('data-node-id="compare-1"');
expect(markup).toContain('data-node-type="compare"');
expect(markup).toContain('data-handle-id="left"');
expect(markup).toContain('data-handle-id="right"');
expect(markup).toContain('data-handle-id="compare-out"');
expect(markup).toContain('data-handle-type="target"');
expect(markup).toContain('data-handle-type="source"');
expect(markup).toContain('data-top="35%"');
expect(markup).toContain('data-top="55%"');
});
});

View File

@@ -17,6 +17,31 @@ vi.mock("@xyflow/react", () => ({
Position: { Left: "left", Right: "right" },
}));
vi.mock("@/components/canvas/canvas-handle", () => ({
default: ({
id,
type,
nodeId,
nodeType,
style,
}: {
id?: string;
type: "source" | "target";
nodeId: string;
nodeType?: string;
style?: React.CSSProperties;
}) => (
<div
data-canvas-handle="true"
data-handle-id={id ?? ""}
data-handle-type={type}
data-node-id={nodeId}
data-node-type={nodeType ?? ""}
data-top={typeof style?.top === "string" ? style.top : ""}
/>
),
}));
vi.mock("@/components/canvas/canvas-sync-context", () => ({
useCanvasSync: () => ({
queueNodeDataUpdate: mocks.queueNodeDataUpdate,
@@ -222,8 +247,20 @@ describe("MixerNode", () => {
it("renders expected mixer handles", async () => {
await renderNode();
expect(container?.querySelector('[data-handle-id="base"][data-handle-type="target"]')).toBeTruthy();
expect(container?.querySelector('[data-handle-id="overlay"][data-handle-type="target"]')).toBeTruthy();
expect(container?.querySelector('[data-handle-id="mixer-out"][data-handle-type="source"]')).toBeTruthy();
expect(
container?.querySelector(
'[data-canvas-handle="true"][data-node-id="mixer-1"][data-node-type="mixer"][data-handle-id="base"][data-handle-type="target"][data-top="35%"]',
),
).toBeTruthy();
expect(
container?.querySelector(
'[data-canvas-handle="true"][data-node-id="mixer-1"][data-node-type="mixer"][data-handle-id="overlay"][data-handle-type="target"][data-top="58%"]',
),
).toBeTruthy();
expect(
container?.querySelector(
'[data-canvas-handle="true"][data-node-id="mixer-1"][data-node-type="mixer"][data-handle-id="mixer-out"][data-handle-type="source"]',
),
).toBeTruthy();
});
});

View File

@@ -2,7 +2,7 @@
import { useCallback, useMemo, useState } from "react";
import { Bot } from "lucide-react";
import { Handle, Position, type Node, type NodeProps } from "@xyflow/react";
import { Position, type Node, type NodeProps } from "@xyflow/react";
import { useAction } from "convex/react";
import type { FunctionReference } from "convex/server";
import { useTranslations } from "next-intl";
@@ -33,6 +33,7 @@ import {
SelectValue,
} from "@/components/ui/select";
import BaseNodeWrapper from "./base-node-wrapper";
import CanvasHandle from "@/components/canvas/canvas-handle";
type AgentNodeData = {
templateId?: string;
@@ -466,13 +467,17 @@ export default function AgentNode({ id, data, selected }: NodeProps<AgentNodeTyp
statusMessage={nodeData._statusMessage}
className="min-w-[300px] border-amber-500/30"
>
<Handle
<CanvasHandle
nodeId={id}
nodeType="agent"
type="target"
position={Position.Left}
id="agent-in"
className="!h-3 !w-3 !bg-amber-500 !border-2 !border-background"
/>
<Handle
<CanvasHandle
nodeId={id}
nodeType="agent"
type="source"
position={Position.Right}
className="!h-3 !w-3 !bg-amber-500 !border-2 !border-background"

View File

@@ -1,9 +1,10 @@
"use client";
import { Handle, Position, type Node, type NodeProps } from "@xyflow/react";
import { Position, type Node, type NodeProps } from "@xyflow/react";
import { useTranslations } from "next-intl";
import BaseNodeWrapper from "./base-node-wrapper";
import CanvasHandle from "@/components/canvas/canvas-handle";
type AgentOutputNodeData = {
isSkeleton?: boolean;
@@ -186,7 +187,7 @@ function partitionSections(
};
}
export default function AgentOutputNode({ data, selected }: NodeProps<AgentOutputNodeType>) {
export default function AgentOutputNode({ id, data, selected }: NodeProps<AgentOutputNodeType>) {
const t = useTranslations("agentOutputNode");
const nodeData = data as AgentOutputNodeData;
const isSkeleton = nodeData.isSkeleton === true;
@@ -240,7 +241,9 @@ export default function AgentOutputNode({ data, selected }: NodeProps<AgentOutpu
statusMessage={nodeData._statusMessage}
className={`min-w-[300px] border-amber-500/30 ${isSkeleton ? "opacity-80" : ""}`}
>
<Handle
<CanvasHandle
nodeId={id}
nodeType="agent-output"
type="target"
position={Position.Left}
id="agent-output-in"

View File

@@ -3,7 +3,7 @@
import { useCallback, useState } from "react";
import { useRouter } from "next/navigation";
import { useTranslations } from "next-intl";
import { Handle, Position, useReactFlow, type NodeProps, type Node } from "@xyflow/react";
import { Position, useReactFlow, type NodeProps, type Node } from "@xyflow/react";
import { useAction } from "convex/react";
import { api } from "@/convex/_generated/api";
import type { Id } from "@/convex/_generated/dataModel";
@@ -30,6 +30,7 @@ import {
DialogContent,
DialogTitle,
} from "@/components/ui/dialog";
import CanvasHandle from "@/components/canvas/canvas-handle";
type AiImageNodeData = {
storageId?: string;
@@ -194,7 +195,9 @@ export default function AiImageNode({
]}
className="flex h-full w-full min-h-0 min-w-0 flex-col"
>
<Handle
<CanvasHandle
nodeId={id}
nodeType="ai-image"
type="target"
position={Position.Left}
id="prompt-in"
@@ -331,7 +334,9 @@ export default function AiImageNode({
</div>
)}
<Handle
<CanvasHandle
nodeId={id}
nodeType="ai-image"
type="source"
position={Position.Right}
id="image-out"

View File

@@ -5,7 +5,7 @@ import { useAction } from "convex/react";
import type { FunctionReference } from "convex/server";
import { useTranslations } from "next-intl";
import { AlertCircle, Download, Loader2, RefreshCw, Video } from "lucide-react";
import { Handle, Position, useReactFlow, type Node, type NodeProps } from "@xyflow/react";
import { Position, useReactFlow, type Node, type NodeProps } from "@xyflow/react";
import { api } from "@/convex/_generated/api";
import type { Id } from "@/convex/_generated/dataModel";
@@ -14,6 +14,7 @@ import { classifyError } from "@/lib/ai-errors";
import { getVideoModel, type VideoModelDurationSeconds } from "@/lib/ai-video-models";
import { toast } from "@/lib/toast";
import BaseNodeWrapper from "./base-node-wrapper";
import CanvasHandle from "@/components/canvas/canvas-handle";
type AiVideoNodeData = {
prompt?: string;
@@ -160,7 +161,9 @@ export default function AiVideoNode({ id, data, selected }: NodeProps<AiVideoNod
statusMessage={nodeData._statusMessage}
className="flex h-full w-full min-h-0 min-w-0 flex-col"
>
<Handle
<CanvasHandle
nodeId={id}
nodeType="ai-video"
type="target"
position={Position.Left}
id="video-in"
@@ -240,7 +243,9 @@ export default function AiVideoNode({ id, data, selected }: NodeProps<AiVideoNod
) : null}
</div>
<Handle
<CanvasHandle
nodeId={id}
nodeType="ai-video"
type="source"
position={Position.Right}
id="video-out"

View File

@@ -8,7 +8,7 @@ import {
useState,
type MouseEvent,
} from "react";
import { Handle, Position, useStore, type Node, type NodeProps } from "@xyflow/react";
import { Position, useStore, type Node, type NodeProps } from "@xyflow/react";
import { ExternalLink, ImageIcon } from "lucide-react";
import BaseNodeWrapper from "./base-node-wrapper";
import {
@@ -21,6 +21,7 @@ import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { resolveMediaAspectRatio } from "@/lib/canvas-utils";
import { useCanvasSync } from "@/components/canvas/canvas-sync-context";
import CanvasHandle from "@/components/canvas/canvas-handle";
type AssetNodeData = {
assetId?: number;
@@ -152,7 +153,9 @@ export default function AssetNode({ id, data, selected, width, height }: NodePro
status={data._status}
statusMessage={data._statusMessage}
>
<Handle
<CanvasHandle
nodeId={id}
nodeType="asset"
type="target"
position={Position.Left}
className="h-3! w-3! border-2! border-background! bg-primary!"
@@ -273,7 +276,9 @@ export default function AssetNode({ id, data, selected, width, height }: NodePro
/>
) : null}
<Handle
<CanvasHandle
nodeId={id}
nodeType="asset"
type="source"
position={Position.Right}
className="h-3! w-3! border-2! border-background! bg-primary!"

View File

@@ -1,7 +1,7 @@
"use client";
import { useCallback, useMemo, useState } from "react";
import { Handle, Position, type Node, type NodeProps } from "@xyflow/react";
import { Position, type Node, type NodeProps } from "@xyflow/react";
import { useTranslations } from "next-intl";
import { Palette } from "lucide-react";
@@ -29,6 +29,7 @@ import { preserveNodeFavorite } from "@/lib/canvas-node-favorite";
import { COLOR_PRESETS } from "@/lib/image-pipeline/presets";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { toast } from "@/lib/toast";
import CanvasHandle from "@/components/canvas/canvas-handle";
type ColorAdjustNodeData = ColorAdjustData & {
_status?: string;
@@ -191,7 +192,9 @@ export default function ColorAdjustNode({ id, data, selected, width }: NodeProps
statusMessage={data._statusMessage}
className="min-w-[300px] border-cyan-500/30"
>
<Handle
<CanvasHandle
nodeId={id}
nodeType="color-adjust"
type="target"
position={Position.Left}
className="!h-3 !w-3 !border-2 !border-background !bg-cyan-500"
@@ -268,7 +271,9 @@ export default function ColorAdjustNode({ id, data, selected, width }: NodeProps
/>
</div>
<Handle
<CanvasHandle
nodeId={id}
nodeType="color-adjust"
type="source"
position={Position.Right}
className="!h-3 !w-3 !border-2 !border-background !bg-cyan-500"

View File

@@ -1,7 +1,7 @@
"use client";
import { useCallback, useMemo, useRef, useState } from "react";
import { Handle, Position, type NodeProps } from "@xyflow/react";
import { Position, type NodeProps } from "@xyflow/react";
import { ImageIcon } from "lucide-react";
import BaseNodeWrapper from "./base-node-wrapper";
import CompareSurface from "./compare-surface";
@@ -15,6 +15,7 @@ import {
resolveMixerPreviewFromGraph,
type MixerPreviewState,
} from "@/lib/canvas-mixer-preview";
import CanvasHandle from "@/components/canvas/canvas-handle";
interface CompareNodeData {
leftUrl?: string;
@@ -242,21 +243,27 @@ export default function CompareNode({ id, data, selected, width }: NodeProps) {
return (
<BaseNodeWrapper nodeType="compare" selected={selected} className="p-0">
<Handle
<CanvasHandle
nodeId={id}
nodeType="compare"
type="target"
position={Position.Left}
id="left"
style={{ top: "35%" }}
className="!h-3 !w-3 !border-2 !border-background !bg-blue-500"
/>
<Handle
<CanvasHandle
nodeId={id}
nodeType="compare"
type="target"
position={Position.Left}
id="right"
style={{ top: "55%" }}
className="!h-3 !w-3 !border-2 !border-background !bg-emerald-500"
/>
<Handle
<CanvasHandle
nodeId={id}
nodeType="compare"
type="source"
position={Position.Right}
id="compare-out"

View File

@@ -1,7 +1,7 @@
"use client";
import { useCallback, useMemo, useRef, type PointerEvent as ReactPointerEvent } from "react";
import { Handle, Position, type Node, type NodeProps } from "@xyflow/react";
import { Position, type Node, type NodeProps } from "@xyflow/react";
import { Crop } from "lucide-react";
import { useTranslations } from "next-intl";
@@ -24,6 +24,7 @@ import {
import { preserveNodeFavorite } from "@/lib/canvas-node-favorite";
import type { Id } from "@/convex/_generated/dataModel";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import CanvasHandle from "@/components/canvas/canvas-handle";
type CropNodeViewData = CropNodeData & {
_status?: string;
@@ -400,7 +401,9 @@ export default function CropNode({ id, data, selected, width }: NodeProps<CropNo
statusMessage={data._statusMessage}
className="min-w-[320px] border-violet-500/30"
>
<Handle
<CanvasHandle
nodeId={id}
nodeType="crop"
type="target"
position={Position.Left}
className="!h-3 !w-3 !border-2 !border-background !bg-violet-500"
@@ -735,7 +738,9 @@ export default function CropNode({ id, data, selected, width }: NodeProps<CropNo
{error ? <p className="text-[11px] text-destructive">{error}</p> : null}
</div>
<Handle
<CanvasHandle
nodeId={id}
nodeType="crop"
type="source"
position={Position.Right}
className="!h-3 !w-3 !border-2 !border-background !bg-violet-500"

View File

@@ -1,7 +1,7 @@
"use client";
import { useCallback, useMemo, useState } from "react";
import { Handle, Position, type Node, type NodeProps } from "@xyflow/react";
import { Position, type Node, type NodeProps } from "@xyflow/react";
import { useTranslations } from "next-intl";
import { TrendingUp } from "lucide-react";
@@ -29,6 +29,7 @@ import { preserveNodeFavorite } from "@/lib/canvas-node-favorite";
import { CURVE_PRESETS } from "@/lib/image-pipeline/presets";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { toast } from "@/lib/toast";
import CanvasHandle from "@/components/canvas/canvas-handle";
type CurvesNodeData = CurvesData & {
_status?: string;
@@ -163,7 +164,9 @@ export default function CurvesNode({ id, data, selected, width }: NodeProps<Curv
statusMessage={data._statusMessage}
className="min-w-[300px] border-emerald-500/30"
>
<Handle
<CanvasHandle
nodeId={id}
nodeType="curves"
type="target"
position={Position.Left}
className="!h-3 !w-3 !border-2 !border-background !bg-emerald-500"
@@ -237,7 +240,9 @@ export default function CurvesNode({ id, data, selected, width }: NodeProps<Curv
/>
</div>
<Handle
<CanvasHandle
nodeId={id}
nodeType="curves"
type="source"
position={Position.Right}
className="!h-3 !w-3 !border-2 !border-background !bg-emerald-500"

View File

@@ -1,7 +1,7 @@
"use client";
import { useCallback, useMemo, useState } from "react";
import { Handle, Position, type Node, type NodeProps } from "@xyflow/react";
import { Position, type Node, type NodeProps } from "@xyflow/react";
import { useTranslations } from "next-intl";
import { Focus } from "lucide-react";
@@ -29,6 +29,7 @@ import { preserveNodeFavorite } from "@/lib/canvas-node-favorite";
import { DETAIL_PRESETS } from "@/lib/image-pipeline/presets";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { toast } from "@/lib/toast";
import CanvasHandle from "@/components/canvas/canvas-handle";
type DetailAdjustNodeData = DetailAdjustData & {
_status?: string;
@@ -202,7 +203,9 @@ export default function DetailAdjustNode({ id, data, selected, width }: NodeProp
statusMessage={data._statusMessage}
className="min-w-[300px] border-indigo-500/30"
>
<Handle
<CanvasHandle
nodeId={id}
nodeType="detail-adjust"
type="target"
position={Position.Left}
className="!h-3 !w-3 !border-2 !border-background !bg-indigo-500"
@@ -286,7 +289,9 @@ export default function DetailAdjustNode({ id, data, selected, width }: NodeProp
/>
</div>
<Handle
<CanvasHandle
nodeId={id}
nodeType="detail-adjust"
type="source"
position={Position.Right}
className="!h-3 !w-3 !border-2 !border-background !bg-indigo-500"

View File

@@ -1,7 +1,7 @@
"use client";
import { useCallback, useState } from "react";
import { Handle, Position, type NodeProps } from "@xyflow/react";
import { Position, type NodeProps } from "@xyflow/react";
import { useAction } from "convex/react";
import { useTranslations } from "next-intl";
import { Download, Loader2 } from "lucide-react";
@@ -11,6 +11,7 @@ import { useDebouncedCallback } from "@/hooks/use-debounced-callback";
import BaseNodeWrapper from "./base-node-wrapper";
import { toast } from "@/lib/toast";
import { useCanvasSync } from "@/components/canvas/canvas-sync-context";
import CanvasHandle from "@/components/canvas/canvas-handle";
interface FrameNodeData {
label?: string;
@@ -125,13 +126,17 @@ export default function FrameNode({ id, data, selected, width, height }: NodePro
<div className="nodrag h-full w-full" />
<Handle
<CanvasHandle
nodeId={id}
nodeType="frame"
type="target"
position={Position.Left}
id="frame-in"
className="!h-3 !w-3 !border-2 !border-background !bg-orange-500"
/>
<Handle
<CanvasHandle
nodeId={id}
nodeType="frame"
type="source"
position={Position.Right}
id="frame-out"

View File

@@ -1,10 +1,11 @@
"use client";
import { useState, useCallback, useEffect } from "react";
import { Handle, Position, type NodeProps, type Node } from "@xyflow/react";
import { Position, type NodeProps, type Node } from "@xyflow/react";
import type { Id } from "@/convex/_generated/dataModel";
import BaseNodeWrapper from "./base-node-wrapper";
import { useCanvasSync } from "@/components/canvas/canvas-sync-context";
import CanvasHandle from "@/components/canvas/canvas-handle";
type GroupNodeData = {
label?: string;
@@ -47,7 +48,9 @@ export default function GroupNode({ id, data, selected }: NodeProps<GroupNode>)
selected={selected}
className="min-w-[200px] min-h-[150px] p-3 border-dashed"
>
<Handle
<CanvasHandle
nodeId={id}
nodeType="group"
type="target"
position={Position.Left}
className="!h-3 !w-3 !bg-muted-foreground !border-2 !border-background"
@@ -71,7 +74,9 @@ export default function GroupNode({ id, data, selected }: NodeProps<GroupNode>)
</div>
)}
<Handle
<CanvasHandle
nodeId={id}
nodeType="group"
type="source"
position={Position.Right}
className="!h-3 !w-3 !bg-muted-foreground !border-2 !border-background"

View File

@@ -8,7 +8,7 @@ import {
type ChangeEvent,
type DragEvent,
} from "react";
import { Handle, Position, type NodeProps, type Node } from "@xyflow/react";
import { Position, type NodeProps, type Node } from "@xyflow/react";
import { Maximize2, X } from "lucide-react";
import { useTranslations } from "next-intl";
import { api } from "@/convex/_generated/api";
@@ -37,6 +37,7 @@ import {
getImageDimensions,
} from "@/components/canvas/canvas-media-utils";
import { preserveNodeFavorite } from "@/lib/canvas-node-favorite";
import CanvasHandle from "@/components/canvas/canvas-handle";
const ALLOWED_IMAGE_TYPES = new Set([
"image/png",
@@ -508,7 +509,9 @@ export default function ImageNode({
},
]}
>
<Handle
<CanvasHandle
nodeId={id}
nodeType="image"
type="target"
position={Position.Left}
className="h-3! w-3! bg-primary! border-2! border-background!"
@@ -609,7 +612,9 @@ export default function ImageNode({
className="hidden"
/>
<Handle
<CanvasHandle
nodeId={id}
nodeType="image"
type="source"
position={Position.Right}
className="h-3! w-3! bg-primary! border-2! border-background!"

View File

@@ -1,7 +1,7 @@
"use client";
import { useCallback, useMemo, useState } from "react";
import { Handle, Position, type Node, type NodeProps } from "@xyflow/react";
import { Position, type Node, type NodeProps } from "@xyflow/react";
import { useTranslations } from "next-intl";
import { Sun } from "lucide-react";
@@ -29,6 +29,7 @@ import { preserveNodeFavorite } from "@/lib/canvas-node-favorite";
import { LIGHT_PRESETS } from "@/lib/image-pipeline/presets";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { toast } from "@/lib/toast";
import CanvasHandle from "@/components/canvas/canvas-handle";
type LightAdjustNodeData = LightAdjustData & {
_status?: string;
@@ -213,7 +214,9 @@ export default function LightAdjustNode({ id, data, selected, width }: NodeProps
statusMessage={data._statusMessage}
className="min-w-[300px] border-amber-500/30"
>
<Handle
<CanvasHandle
nodeId={id}
nodeType="light-adjust"
type="target"
position={Position.Left}
className="!h-3 !w-3 !border-2 !border-background !bg-amber-500"
@@ -292,7 +295,9 @@ export default function LightAdjustNode({ id, data, selected, width }: NodeProps
/>
</div>
<Handle
<CanvasHandle
nodeId={id}
nodeType="light-adjust"
type="source"
position={Position.Right}
className="!h-3 !w-3 !border-2 !border-background !bg-amber-500"

View File

@@ -1,7 +1,7 @@
"use client";
import { useMemo, useState, type ChangeEvent, type FormEvent } from "react";
import { Handle, Position, type NodeProps } from "@xyflow/react";
import { Position, type NodeProps } from "@xyflow/react";
import BaseNodeWrapper from "./base-node-wrapper";
import { useCanvasGraph } from "@/components/canvas/canvas-graph-context";
@@ -12,6 +12,7 @@ import {
type MixerBlendMode,
} from "@/lib/canvas-mixer-preview";
import type { Id } from "@/convex/_generated/dataModel";
import CanvasHandle from "@/components/canvas/canvas-handle";
const BLEND_MODE_OPTIONS: MixerBlendMode[] = ["normal", "multiply", "screen", "overlay"];
@@ -56,21 +57,27 @@ export default function MixerNode({ id, data, selected }: NodeProps) {
return (
<BaseNodeWrapper nodeType="mixer" selected={selected} className="p-0">
<Handle
<CanvasHandle
nodeId={id}
nodeType="mixer"
type="target"
position={Position.Left}
id="base"
style={{ top: "35%" }}
className="!h-3 !w-3 !border-2 !border-background !bg-sky-500"
/>
<Handle
<CanvasHandle
nodeId={id}
nodeType="mixer"
type="target"
position={Position.Left}
id="overlay"
style={{ top: "58%" }}
className="!h-3 !w-3 !border-2 !border-background !bg-pink-500"
/>
<Handle
<CanvasHandle
nodeId={id}
nodeType="mixer"
type="source"
position={Position.Right}
id="mixer-out"

View File

@@ -1,11 +1,12 @@
"use client";
import { useState, useCallback, useEffect } from "react";
import { Handle, Position, type NodeProps, type Node } from "@xyflow/react";
import { Position, type NodeProps, type Node } from "@xyflow/react";
import type { Id } from "@/convex/_generated/dataModel";
import { useDebouncedCallback } from "@/hooks/use-debounced-callback";
import BaseNodeWrapper from "./base-node-wrapper";
import { useCanvasSync } from "@/components/canvas/canvas-sync-context";
import CanvasHandle from "@/components/canvas/canvas-handle";
type NoteNodeData = {
content?: string;
@@ -53,7 +54,9 @@ export default function NoteNode({ id, data, selected }: NodeProps<NoteNode>) {
return (
<BaseNodeWrapper nodeType="note" selected={selected} className="p-3">
<Handle
<CanvasHandle
nodeId={id}
nodeType="note"
type="target"
position={Position.Left}
className="!h-3 !w-3 !bg-primary !border-2 !border-background"
@@ -85,7 +88,9 @@ export default function NoteNode({ id, data, selected }: NodeProps<NoteNode>) {
</div>
)}
<Handle
<CanvasHandle
nodeId={id}
nodeType="note"
type="source"
position={Position.Right}
className="!h-3 !w-3 !bg-primary !border-2 !border-background"

View File

@@ -2,7 +2,6 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import {
Handle,
Position,
useReactFlow,
useStore,
@@ -45,6 +44,7 @@ import { useRouter } from "next/navigation";
import { toast } from "@/lib/toast";
import { classifyError } from "@/lib/ai-errors";
import { normalizePublicTier } from "@/lib/tier-credits";
import CanvasHandle from "@/components/canvas/canvas-handle";
type PromptNodeData = {
prompt?: string;
@@ -353,7 +353,9 @@ export default function PromptNode({
statusMessage={nodeData._statusMessage}
className="min-w-[240px] border-violet-500/30"
>
<Handle
<CanvasHandle
nodeId={id}
nodeType="prompt"
type="target"
position={Position.Left}
id="image-in"
@@ -489,7 +491,9 @@ export default function PromptNode({
</div>
</div>
<Handle
<CanvasHandle
nodeId={id}
nodeType="prompt"
type="source"
position={Position.Right}
id="prompt-out"

View File

@@ -1,7 +1,7 @@
"use client";
import { useEffect, useMemo, useRef, useState } from "react";
import { Handle, Position, type Node, type NodeProps } from "@xyflow/react";
import { Position, type Node, type NodeProps } from "@xyflow/react";
import { AlertCircle, ArrowDown, CheckCircle2, CloudUpload, Loader2, Maximize2, X } from "lucide-react";
import { useMutation } from "convex/react";
@@ -29,6 +29,7 @@ import {
import { preserveNodeFavorite } from "@/lib/canvas-node-favorite";
import type { Id } from "@/convex/_generated/dataModel";
import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog";
import CanvasHandle from "@/components/canvas/canvas-handle";
type RenderResolutionOption = "original" | "2x" | "custom";
type RenderFormatOption = "png" | "jpeg" | "webp";
@@ -978,7 +979,9 @@ export default function RenderNode({ id, data, selected, width, height }: NodePr
]}
className="flex h-full min-w-[280px] flex-col overflow-hidden border-sky-500/30"
>
<Handle
<CanvasHandle
nodeId={id}
nodeType="render"
type="target"
position={Position.Left}
className="!h-3 !w-3 !border-2 !border-background !bg-sky-500"
@@ -1273,7 +1276,9 @@ export default function RenderNode({ id, data, selected, width, height }: NodePr
</div>
</div>
<Handle
<CanvasHandle
nodeId={id}
nodeType="render"
type="source"
position={Position.Right}
className="!h-3 !w-3 !border-2 !border-background !bg-sky-500"

View File

@@ -2,7 +2,6 @@
import { useState, useCallback, useEffect, useRef } from "react";
import {
Handle,
Position,
useReactFlow,
type NodeProps,
@@ -20,6 +19,7 @@ import {
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import CanvasHandle from "@/components/canvas/canvas-handle";
type TextNodeData = {
content?: string;
@@ -155,7 +155,9 @@ export default function TextNode({ id, data, selected }: NodeProps<TextNode>) {
]}
className="relative"
>
<Handle
<CanvasHandle
nodeId={id}
nodeType="text"
type="target"
position={Position.Left}
className="!h-3 !w-3 !bg-primary !border-2 !border-background"
@@ -190,7 +192,9 @@ export default function TextNode({ id, data, selected }: NodeProps<TextNode>) {
</div>
)}
</div>
<Handle
<CanvasHandle
nodeId={id}
nodeType="text"
type="source"
position={Position.Right}
className="!h-3 !w-3 !bg-primary !border-2 !border-background"

View File

@@ -1,7 +1,7 @@
"use client";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { Handle, Position, useStore, type NodeProps } from "@xyflow/react";
import { Position, useStore, type NodeProps } from "@xyflow/react";
import { useAction } from "convex/react";
import { Play } from "lucide-react";
import BaseNodeWrapper from "./base-node-wrapper";
@@ -12,6 +12,7 @@ import {
import { api } from "@/convex/_generated/api";
import type { Id } from "@/convex/_generated/dataModel";
import { useCanvasSync } from "@/components/canvas/canvas-sync-context";
import CanvasHandle from "@/components/canvas/canvas-handle";
type VideoNodeData = {
canvasId?: string;
@@ -150,7 +151,9 @@ export default function VideoNode({
return (
<BaseNodeWrapper nodeType="video" selected={selected}>
<Handle
<CanvasHandle
nodeId={id}
nodeType="video"
type="target"
position={Position.Left}
className="h-3! w-3! border-2! border-background! bg-primary!"
@@ -245,7 +248,9 @@ export default function VideoNode({
/>
) : null}
<Handle
<CanvasHandle
nodeId={id}
nodeType="video"
type="source"
position={Position.Right}
className="h-3! w-3! border-2! border-background! bg-primary!"

View File

@@ -1,7 +1,7 @@
"use client";
import { useCallback, useMemo, useState } from "react";
import { Handle, Position, useReactFlow, useStore, type Node, type NodeProps } from "@xyflow/react";
import { Position, useReactFlow, useStore, type Node, type NodeProps } from "@xyflow/react";
import { useAction } from "convex/react";
import type { FunctionReference } from "convex/server";
import { useRouter } from "next/navigation";
@@ -33,6 +33,7 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import CanvasHandle from "@/components/canvas/canvas-handle";
type VideoPromptNodeData = {
prompt?: string;
@@ -300,7 +301,9 @@ export default function VideoPromptNode({
statusMessage={nodeData._statusMessage}
className="min-w-[260px] border-violet-500/30"
>
<Handle
<CanvasHandle
nodeId={id}
nodeType="video-prompt"
type="target"
position={Position.Left}
id="video-prompt-in"
@@ -407,7 +410,9 @@ export default function VideoPromptNode({
) : null}
</div>
<Handle
<CanvasHandle
nodeId={id}
nodeType="video-prompt"
type="source"
position={Position.Right}
id="video-prompt-out"