mirror of
https://github.com/basketikun/infinite-canvas.git
synced 2026-08-06 09:24:24 +08:00
187c499ef9
Move the module-scope presentational subcomponents out of project.tsx: - canvas-refresh-shell.tsx: CanvasRefreshShell (pre-load placeholder). - canvas-create-menus.tsx: ConnectionCreateMenu, ConnectionCreateOption, NodeCreateMenu, plus the PendingConnectionCreate type they define. They already communicated via explicit props, so the move is mechanical. No behavior change; project.tsx imports them back and drops now-unused icon and node-registry imports. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
113 lines
6.0 KiB
TypeScript
113 lines
6.0 KiB
TypeScript
import { useEffect, useRef } from "react";
|
||
import { ImageIcon, List, Music2, Settings2, Video, X } from "lucide-react";
|
||
|
||
import { canvasThemes } from "@/lib/canvas-theme";
|
||
import { useThemeStore } from "@/stores/use-theme-store";
|
||
import { listNodeDefinitions, useNodeRegistryVersion } from "@/lib/canvas/node-registry";
|
||
import { CanvasNodeType, type ConnectionHandle, type Position } from "@/types/canvas";
|
||
|
||
export type PendingConnectionCreate = {
|
||
connection: ConnectionHandle;
|
||
position: Position;
|
||
};
|
||
|
||
export function ConnectionCreateMenu({
|
||
pending,
|
||
onCreate,
|
||
onClose,
|
||
}: {
|
||
pending: PendingConnectionCreate;
|
||
onCreate: (type: CanvasNodeType.Image | CanvasNodeType.Text | CanvasNodeType.Config | CanvasNodeType.Video | CanvasNodeType.Audio) => void;
|
||
onClose: () => void;
|
||
}) {
|
||
const theme = canvasThemes[useThemeStore((state) => state.theme)];
|
||
return (
|
||
<div
|
||
className="absolute z-[120] w-[300px] rounded-[18px] border p-3 shadow-2xl backdrop-blur"
|
||
data-connection-create-menu
|
||
style={{ left: pending.position.x, top: pending.position.y, background: theme.node.panel, borderColor: theme.node.stroke, color: theme.node.text }}
|
||
onMouseDown={(event) => event.stopPropagation()}
|
||
onPointerDown={(event) => event.stopPropagation()}
|
||
>
|
||
<div className="mb-2 flex items-center justify-between px-1">
|
||
<span className="text-sm font-medium" style={{ color: theme.node.muted }}>
|
||
引用该节点生成
|
||
</span>
|
||
<button type="button" className="grid size-7 place-items-center rounded-lg text-base opacity-55 transition hover:bg-white/10 hover:opacity-100" onClick={onClose} aria-label="关闭">
|
||
×
|
||
</button>
|
||
</div>
|
||
<div className="grid gap-1">
|
||
<ConnectionCreateOption theme={theme} icon={<List className="size-5" />} title="文本生成" description="脚本、广告词、品牌文案" onClick={() => onCreate(CanvasNodeType.Text)} />
|
||
<ConnectionCreateOption theme={theme} icon={<ImageIcon className="size-5" />} title="图片生成" onClick={() => onCreate(CanvasNodeType.Image)} />
|
||
<ConnectionCreateOption theme={theme} icon={<Video className="size-5" />} title="视频生成" onClick={() => onCreate(CanvasNodeType.Video)} />
|
||
<ConnectionCreateOption theme={theme} icon={<Music2 className="size-5" />} title="音频参考" onClick={() => onCreate(CanvasNodeType.Audio)} />
|
||
<ConnectionCreateOption theme={theme} icon={<Settings2 className="size-5" />} title="配置节点" description="模型、尺寸、数量和输入顺序" onClick={() => onCreate(CanvasNodeType.Config)} />
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export function ConnectionCreateOption({ theme, icon, title, description, onClick }: { theme: (typeof canvasThemes)[keyof typeof canvasThemes]; icon: React.ReactNode; title: string; description?: string; onClick?: () => void }) {
|
||
return (
|
||
<button
|
||
type="button"
|
||
className="flex h-16 w-full cursor-pointer items-center gap-3 rounded-2xl px-3 text-left transition"
|
||
style={{ color: theme.node.text }}
|
||
onClick={onClick}
|
||
onMouseEnter={(event) => (event.currentTarget.style.background = theme.node.fill)}
|
||
onMouseLeave={(event) => (event.currentTarget.style.background = "transparent")}
|
||
>
|
||
<span className="grid size-11 shrink-0 place-items-center rounded-xl" style={{ background: theme.node.fill, color: theme.node.muted }}>
|
||
{icon}
|
||
</span>
|
||
<span className="min-w-0 flex-1">
|
||
<span className="flex items-center gap-2 text-base font-semibold leading-5">{title}</span>
|
||
{description ? (
|
||
<span className="mt-1 block truncate text-sm" style={{ color: theme.node.muted }}>
|
||
{description}
|
||
</span>
|
||
) : null}
|
||
</span>
|
||
</button>
|
||
);
|
||
}
|
||
|
||
export function NodeCreateMenu({ position, onCreate, onClose }: { position: Position; onCreate: (type: string) => void; onClose: () => void }) {
|
||
const theme = canvasThemes[useThemeStore((state) => state.theme)];
|
||
useNodeRegistryVersion();
|
||
const menuRef = useRef<HTMLDivElement>(null);
|
||
const definitions = listNodeDefinitions().filter((def) => def.showInCreateMenu !== false);
|
||
// 点击菜单外的空白处自动关闭
|
||
useEffect(() => {
|
||
const handlePointerDown = (event: PointerEvent) => {
|
||
if (menuRef.current && !menuRef.current.contains(event.target as Node)) onClose();
|
||
};
|
||
document.addEventListener("pointerdown", handlePointerDown, true);
|
||
return () => document.removeEventListener("pointerdown", handlePointerDown, true);
|
||
}, [onClose]);
|
||
return (
|
||
<div
|
||
ref={menuRef}
|
||
className="absolute z-[120] max-h-[70vh] w-[300px] overflow-y-auto rounded-[18px] border p-3 shadow-2xl backdrop-blur thin-scrollbar"
|
||
data-canvas-no-zoom
|
||
style={{ left: position.x, top: position.y, background: theme.node.panel, borderColor: theme.node.stroke, color: theme.node.text }}
|
||
onPointerDown={(event) => event.stopPropagation()}
|
||
>
|
||
<div className="mb-2 flex items-center justify-between px-1">
|
||
<span className="text-sm font-medium" style={{ color: theme.node.muted }}>
|
||
选择节点
|
||
</span>
|
||
<button type="button" className="grid size-7 place-items-center rounded-lg opacity-55 transition hover:opacity-100" onClick={onClose} aria-label="关闭">
|
||
<X className="size-4" />
|
||
</button>
|
||
</div>
|
||
<div className="grid gap-1">
|
||
{definitions.map((def) => (
|
||
<ConnectionCreateOption key={def.type} theme={theme} icon={def.icon} title={def.title} description={def.description} onClick={() => onCreate(def.type)} />
|
||
))}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|