Replica LibTV image toolbar menus
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled

This commit is contained in:
Your Name 2026-04-18 22:44:30 +08:00
parent 4acf95490d
commit df9a28d752

View File

@ -3730,24 +3730,60 @@ function SceneToolbarPopover({
onSelect?: (option?: string) => void;
}) {
const [lightingPrompt, setLightingPrompt] = useState("");
const quickOptions: Record<Exclude<SceneToolbarMenu, "lighting" | null>, string[]> = {
crop: ["4 宫格切分", "9 宫格切分", "16 宫格切分", "25 宫格切分"],
grid: ["2 x 2", "3 x 3", "4 x 4", "自定义宫格"],
hd: ["2倍高清", "4倍高清"],
const [customGridOpen, setCustomGridOpen] = useState(false);
const popoverOffset: Record<Exclude<SceneToolbarMenu, null>, number> = {
crop: 170,
grid: -38,
hd: 70,
lighting: -145,
orbit: -245,
panorama: -360,
};
const officialPopoverStyle = {
left: anchor ? anchor.centerX + popoverOffset[menu] : `calc(50% + ${popoverOffset[menu]}px)`,
top: anchor ? anchor.topY - (24 * anchor.zoom + 10) + 8 : 168,
};
const popoverSurfaceStyle = {
background: "rgba(31, 31, 31, 0.92)",
border: "0.5px solid #363636",
borderRadius: 12,
boxShadow: "0 4px 10px rgba(0, 0, 0, 0.2), 0 2px 4px rgba(0, 0, 0, 0.12)",
WebkitBackdropFilter: "blur(16px)",
backdropFilter: "blur(16px)",
};
const quickOptions: Record<"orbit" | "panorama", string[]> = {
orbit: ["正侧背三视角", "八方向参考", "环绕镜头"],
panorama: ["全景", "半身", "近景"],
};
const nineGridOptions = [
"多机位九宫格",
"剧情推演四宫格",
"25宫格连贯分镜",
"电影级光影校正",
"角色三视图生成",
"画面推演 - 3秒后",
"画面推演 - 5秒前",
];
const hdToolOptions: Array<{ key: string; label: string; active?: boolean }> = [
{ key: "upscale", label: "高清", active: true },
{ key: "outpaint", label: "扩图" },
{ key: "repaint", label: "重绘" },
{ key: "erase", label: "擦除" },
{ key: "removeBg", label: "抠图" },
{ key: "crop", label: "裁剪" },
];
const cropOptions = [
{ type: 4, label: "4宫格 (2×2)" },
{ type: 9, label: "9宫格 (3×3)" },
{ type: 16, label: "16宫格 (4×4)" },
{ type: 25, label: "25宫格 (5×5)" },
];
if (menu === "lighting") {
return (
<div
className={cn(
"pointer-events-auto absolute z-40 w-[420px] rounded-[14px] border border-white/10 bg-[#262626]/98 p-4 text-white shadow-[0_24px_80px_rgba(0,0,0,0.46)] backdrop-blur-[10px]",
anchor
? "origin-top -translate-x-1/2 scale-[0.7]"
: "left-1/2 top-[214px] origin-top -translate-x-1/2 scale-[0.7]"
)}
style={anchor ? { left: anchor.centerX, top: anchor.topY + 8 } : undefined}
className="pointer-events-auto absolute z-40 w-[420px] -translate-x-1/2 p-4 text-white"
style={{ ...officialPopoverStyle, ...popoverSurfaceStyle }}
>
<div className="mb-3 flex items-center justify-between">
<div className="font-semibold text-[15px] text-white/86">打光</div>
@ -3770,19 +3806,117 @@ function SceneToolbarPopover({
);
}
if (menu === "grid") {
return (
<div
className="pointer-events-auto absolute z-40 flex w-[180px] -translate-x-1/2 flex-col p-1.5 font-sans"
role="menu"
style={{ ...officialPopoverStyle, ...popoverSurfaceStyle }}
>
{nineGridOptions.map((option) => (
<button
className="flex w-full cursor-pointer items-center gap-3 rounded-[8px] px-2 py-2.5 text-left transition-colors duration-200 hover:bg-white/8 active:bg-white/12"
key={option}
onClick={() => onSelect?.(option)}
onPointerDown={(event) => event.stopPropagation()}
role="menuitem"
type="button"
>
<NineGridPresetGlyph />
<span className="min-w-0 flex-1 truncate text-[14px] font-normal leading-snug text-white/86">
{option}
</span>
</button>
))}
</div>
);
}
if (menu === "hd") {
return (
<div
className="pointer-events-auto absolute z-40 flex w-[120px] -translate-x-1/2 flex-col gap-1 p-1 font-sans"
role="menu"
style={{ ...officialPopoverStyle, ...popoverSurfaceStyle }}
>
{hdToolOptions.map((option) => (
<button
className={cn(
"flex h-8 w-full shrink-0 items-center gap-2 rounded-[8px] px-2 text-left text-[13px] font-normal leading-snug text-white/86 transition-colors",
option.active ? "bg-white/16" : "hover:bg-white/8 active:bg-white/12"
)}
key={option.key}
onClick={() => onSelect?.(option.label)}
onPointerDown={(event) => event.stopPropagation()}
role="menuitem"
type="button"
>
<ImageToolMenuGlyph tool={option.key} />
<span className="min-w-0 flex-1">{option.label}</span>
</button>
))}
</div>
);
}
if (menu === "crop") {
return (
<div
className="pointer-events-auto absolute z-40 flex w-[150px] -translate-x-1/2 flex-col p-1.5 font-sans"
role="menu"
style={{ ...officialPopoverStyle, ...popoverSurfaceStyle }}
>
{cropOptions.map((option) => (
<button
className="flex w-full cursor-pointer items-center rounded-[8px] px-3 py-2.5 text-left transition-colors duration-200 hover:bg-white/8 active:bg-white/12"
key={option.type}
onClick={() => onSelect?.(option.label)}
onPointerDown={(event) => event.stopPropagation()}
role="menuitem"
type="button"
>
<span className="min-w-0 truncate text-[14px] font-normal leading-snug text-white/86">
{option.label}
</span>
</button>
))}
<div className="mx-2 my-0.5 h-px bg-white/12" />
<div onMouseEnter={() => setCustomGridOpen(true)} onMouseLeave={() => setCustomGridOpen(false)}>
<button
className={cn(
"flex w-full cursor-pointer items-center justify-between rounded-[8px] px-3 py-2.5 text-left transition-colors duration-200 hover:bg-white/8 active:bg-white/12",
customGridOpen && "bg-white/8"
)}
onPointerDown={(event) => event.stopPropagation()}
role="menuitem"
type="button"
>
<span className="min-w-0 truncate text-[14px] font-normal leading-snug text-white/86">自定义</span>
<ChevronDown className="h-3 w-3 shrink-0 -rotate-90 text-white/60" />
</button>
{customGridOpen ? (
<div
className="absolute left-full top-0 ml-1.5 p-1.5"
onMouseEnter={() => setCustomGridOpen(true)}
onMouseLeave={() => setCustomGridOpen(false)}
style={popoverSurfaceStyle}
>
<CustomGridPicker onSelect={(rows, columns) => onSelect?.(`${rows} x ${columns}`)} />
</div>
) : null}
</div>
</div>
);
}
return (
<div
className={cn(
"pointer-events-auto absolute z-40 w-[260px] rounded-[14px] border border-white/10 bg-[#262626]/98 p-2 text-white shadow-[0_24px_80px_rgba(0,0,0,0.46)] backdrop-blur-[10px]",
anchor
? "origin-top -translate-x-1/2 scale-[0.7]"
: "left-1/2 top-[214px] origin-top -translate-x-1/2 scale-[0.7]"
)}
style={anchor ? { left: anchor.centerX, top: anchor.topY + 8 } : undefined}
className="pointer-events-auto absolute z-40 w-[180px] -translate-x-1/2 p-1.5 text-white"
style={{ ...officialPopoverStyle, ...popoverSurfaceStyle }}
>
{quickOptions[menu].map((option) => (
<button
className="flex h-10 w-full items-center justify-between rounded-[10px] px-3 text-left text-[13px] text-white/76 hover:bg-white/8 hover:text-white"
className="flex h-9 w-full items-center justify-between rounded-[8px] px-3 text-left text-[13px] text-white/76 hover:bg-white/8 hover:text-white"
key={option}
onClick={() => onSelect?.(option)}
type="button"
@ -3795,6 +3929,100 @@ function SceneToolbarPopover({
);
}
function NineGridPresetGlyph() {
return (
<svg aria-hidden="true" className="h-5 w-5 shrink-0 text-white/46" fill="none" viewBox="0 0 20 20">
<path d="M3 7.2 10 3.4l7 3.8-7 3.8-7-3.8Z" stroke="currentColor" strokeLinejoin="round" strokeWidth="1.5" />
<path d="m3 10.2 7 3.8 7-3.8M3 13.2l7 3.8 7-3.8" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.5" />
</svg>
);
}
function ImageToolMenuGlyph({ tool }: { tool: string }) {
if (tool === "upscale") {
return (
<span className="flex h-[14px] min-w-[21px] items-center justify-center rounded-[3px] border border-white/34 text-[9px] font-bold leading-none text-white/46">
HD
</span>
);
}
if (tool === "outpaint") {
return (
<svg aria-hidden="true" className="h-[14px] w-[14px] shrink-0 text-white/46" fill="none" viewBox="0 0 14 14">
<path d="M1.8 5V1.8H5M9 1.8h3.2V5M12.2 9v3.2H9M5 12.2H1.8V9" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.4" />
</svg>
);
}
if (tool === "repaint") {
return (
<svg aria-hidden="true" className="h-[14px] w-[14px] shrink-0 text-white/46" fill="none" viewBox="0 0 14 14">
<path d="M3 10.8 10.8 3l1.2 1.2-7.8 7.8H3v-1.2Z" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.3" />
</svg>
);
}
if (tool === "erase") {
return (
<svg aria-hidden="true" className="h-[14px] w-[14px] shrink-0 text-white/46" fill="none" viewBox="0 0 14 14">
<path d="m2.5 8.5 5.8-5.8 3 3-5.8 5.8H3.2L2.5 8.5Z" stroke="currentColor" strokeLinejoin="round" strokeWidth="1.3" />
<path d="M6.2 11.5h5" stroke="currentColor" strokeLinecap="round" strokeWidth="1.3" />
</svg>
);
}
if (tool === "removeBg") {
return (
<svg aria-hidden="true" className="h-[14px] w-[14px] shrink-0 text-white/46" fill="none" viewBox="0 0 14 14">
<path d="M2 2h10v10H2V2Z" stroke="currentColor" strokeWidth="1.2" />
<path d="M2 12 12 2M2 8l6-6M6 12l6-6" stroke="currentColor" strokeWidth="1.1" />
</svg>
);
}
return (
<svg aria-hidden="true" className="h-[14px] w-[14px] shrink-0 text-white/46" fill="none" viewBox="0 0 14 14">
<path d="M4 1.8v8.2h8.2M1.8 4h8.2v8.2" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.3" />
</svg>
);
}
function CustomGridPicker({ onSelect }: { onSelect: (rows: number, columns: number) => void }) {
const [hovered, setHovered] = useState({ columns: 0, rows: 0 });
return (
<div className="flex flex-col gap-2.5 p-2">
<div className="flex items-center justify-between px-0.5">
<span className="text-[13px] text-white/52">自定义宫格</span>
<span className="text-[13px] font-medium text-white/86">
{hovered.rows > 0 && hovered.columns > 0 ? `${hovered.columns} x ${hovered.rows}` : ""}
</span>
</div>
<div className="grid grid-cols-5 gap-1" onMouseLeave={() => setHovered({ columns: 0, rows: 0 })}>
{Array.from({ length: 25 }).map((_, index) => {
const rows = Math.floor(index / 5) + 1;
const columns = (index % 5) + 1;
const active = rows <= hovered.rows && columns <= hovered.columns;
return (
<button
className={cn(
"h-8 w-8 rounded border transition-colors duration-75",
active ? "border-blue-400/60 bg-blue-500/40" : "border-white/12 bg-white/8 hover:bg-white/12"
)}
key={index}
onClick={() => onSelect(rows, columns)}
onMouseEnter={() => setHovered({ columns, rows })}
onPointerDown={(event) => event.stopPropagation()}
type="button"
/>
);
})}
</div>
</div>
);
}
function SceneToolIcon({ kind }: { kind: "panorama" | "orbit" | "lighting" | "grid" | "hd" | "crop" }) {
if (kind === "panorama") {
return <span className="font-['Barlow_Condensed',sans-serif] text-[16px] font-bold leading-none text-white/86">720</span>;