95 lines
2.6 KiB
TypeScript
95 lines
2.6 KiB
TypeScript
"use client";
|
|
|
|
import { Handle, Position } from "@xyflow/react";
|
|
import { NODE_SOCKETS, SOCKET_COLORS, type SocketDef } from "./types";
|
|
|
|
function SocketHandle({
|
|
socket,
|
|
isInput,
|
|
topPercent,
|
|
}: {
|
|
socket: SocketDef;
|
|
isInput: boolean;
|
|
topPercent: number;
|
|
}) {
|
|
const color = SOCKET_COLORS[socket.type];
|
|
const isFlow = socket.type === "flow";
|
|
|
|
return (
|
|
<div
|
|
className="absolute flex items-center gap-1"
|
|
style={{
|
|
[isInput ? "left" : "right"]: -8,
|
|
top: `${topPercent}%`,
|
|
transform: "translateY(-50%)",
|
|
flexDirection: isInput ? "row" : "row-reverse",
|
|
}}
|
|
>
|
|
<Handle
|
|
type={isInput ? "target" : "source"}
|
|
position={isInput ? Position.Left : Position.Right}
|
|
id={socket.id}
|
|
style={{
|
|
background: color,
|
|
width: isFlow ? 8 : 6,
|
|
height: isFlow ? 8 : 6,
|
|
borderRadius: isFlow ? 2 : "50%",
|
|
border: `1.5px solid ${color}80`,
|
|
position: "relative",
|
|
left: "auto",
|
|
right: "auto",
|
|
top: "auto",
|
|
flexShrink: 0,
|
|
}}
|
|
/>
|
|
{socket.label && (
|
|
<span
|
|
className="text-[9px] font-mono font-medium leading-none whitespace-nowrap pointer-events-none"
|
|
style={{ color, opacity: 0.9 }}
|
|
>
|
|
{socket.label}
|
|
</span>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
interface NodeShellProps {
|
|
nodeType: string;
|
|
dynamicInputs?: SocketDef[];
|
|
dynamicOutputs?: SocketDef[];
|
|
children: React.ReactNode;
|
|
className?: string;
|
|
}
|
|
|
|
export function NodeShell({
|
|
nodeType,
|
|
dynamicInputs,
|
|
dynamicOutputs,
|
|
children,
|
|
className = "",
|
|
}: NodeShellProps) {
|
|
const config = NODE_SOCKETS[nodeType];
|
|
if (!config) return <div className={className}>{children}</div>;
|
|
|
|
const inputs = dynamicInputs ?? config.inputs;
|
|
const outputs = dynamicOutputs ?? config.outputs;
|
|
const maxSlots = Math.max(inputs.length, outputs.length, 1);
|
|
|
|
return (
|
|
<div className={`relative ${className}`}>
|
|
{/* Input handles - positioned on left edge */}
|
|
{inputs.map((socket, i) => {
|
|
const top = maxSlots === 1 ? 50 : (i / (maxSlots - 1)) * 80 + 10;
|
|
return <SocketHandle key={socket.id} socket={socket} isInput={true} topPercent={top} />;
|
|
})}
|
|
{/* Output handles - positioned on right edge */}
|
|
{outputs.map((socket, i) => {
|
|
const top = maxSlots === 1 ? 50 : (i / (maxSlots - 1)) * 80 + 10;
|
|
return <SocketHandle key={socket.id} socket={socket} isInput={false} topPercent={top} />;
|
|
})}
|
|
{/* Content area with padding to avoid handle overlap */}
|
|
<div className="px-8">{children}</div>
|
|
</div>
|
|
);
|
|
}
|