refactor(canvas-project): extract refresh shell and create menus into components

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>
This commit is contained in:
HouYunFei
2026-07-17 14:00:53 +08:00
parent cf1ea2646f
commit 187c499ef9
3 changed files with 150 additions and 142 deletions
@@ -0,0 +1,112 @@
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>
);
}