fix: respect canvas toolbar toggles
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-05-08 12:56:19 +08:00
parent 1912bcd25b
commit b78a2701bb
5 changed files with 231 additions and 39 deletions

View File

@ -3257,7 +3257,7 @@ export function AdminSettingsClient() {
const updateCanvasToolbarItem = ( const updateCanvasToolbarItem = (
key: CanvasToolbarItemKey, key: CanvasToolbarItemKey,
updates: Partial<{ label: string; iconUrl: string }> updates: Partial<{ label: string; iconUrl: string; enabled: boolean }>
) => { ) => {
setSiteSettings((prev) => { setSiteSettings((prev) => {
const normalized = normalizeCanvasToolbarConfig(prev.canvasToolbarConfig); const normalized = normalizeCanvasToolbarConfig(prev.canvasToolbarConfig);
@ -11824,12 +11824,29 @@ export function AdminSettingsClient() {
updateCanvasToolbarItem(item.key, { updateCanvasToolbarItem(item.key, {
label: defaultItem.label, label: defaultItem.label,
iconUrl: "", iconUrl: "",
enabled: true,
}) })
} }
> >
默认 默认
</Button> </Button>
</div> </div>
<div className="flex items-center justify-between rounded-lg border bg-background/70 px-3 py-2">
<div className="space-y-0.5">
<div className="text-xs font-medium">启用此功能</div>
<div className="text-[11px] text-muted-foreground">
关闭后前端不再显示这个工具按钮
</div>
</div>
<Switch
checked={itemConfig.enabled !== false}
onCheckedChange={(checked) =>
updateCanvasToolbarItem(item.key, {
enabled: checked,
})
}
/>
</div>
<div className="space-y-1.5"> <div className="space-y-1.5">
<Label <Label
className="text-xs text-muted-foreground" className="text-xs text-muted-foreground"

View File

@ -108,6 +108,7 @@ import {
} from "@/lib/canvas/node-content-scale"; } from "@/lib/canvas/node-content-scale";
import { import {
getCanvasToolbarItem, getCanvasToolbarItem,
isCanvasToolbarItemEnabled,
type CanvasToolbarConfig, type CanvasToolbarConfig,
} from "@/lib/canvas-toolbar-config"; } from "@/lib/canvas-toolbar-config";
import { cn, fetcher } from "@/lib/utils"; import { cn, fetcher } from "@/lib/utils";
@ -225,6 +226,7 @@ interface BoxCutoutPoint {
const BOX_CUTOUT_DEFAULT_PADDING_RATIO = 0.08; const BOX_CUTOUT_DEFAULT_PADDING_RATIO = 0.08;
const BOX_CUTOUT_MIN_EDGE_PX = 24; const BOX_CUTOUT_MIN_EDGE_PX = 24;
const FLOATING_TOOLBAR_SCALE = 0.8; const FLOATING_TOOLBAR_SCALE = 0.8;
const IMAGE_EDIT_PANEL_SCALE = 0.5;
const DEFAULT_CROP_PRESET_ID = "1:1"; const DEFAULT_CROP_PRESET_ID = "1:1";
const IMAGE_ADJUST_NOISE_DATA_URI = `data:image/svg+xml;utf8,${encodeURIComponent( const IMAGE_ADJUST_NOISE_DATA_URI = `data:image/svg+xml;utf8,${encodeURIComponent(
"<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 160 160'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='1.18' numOctaves='2' stitchTiles='stitch'/></filter><rect width='160' height='160' filter='url(#n)' opacity='1'/></svg>" "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 160 160'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='1.18' numOctaves='2' stitchTiles='stitch'/></filter><rect width='160' height='160' filter='url(#n)' opacity='1'/></svg>"
@ -1310,6 +1312,98 @@ export function ImageNode({
canvasToolbarConfig, canvasToolbarConfig,
"imageCancelCutout" "imageCancelCutout"
); );
const imageNodeMoreEnabled = isCanvasToolbarItemEnabled(
canvasToolbarConfig,
"imageNodeMore"
);
const imageRemoveBackgroundEnabled = isCanvasToolbarItemEnabled(
canvasToolbarConfig,
"imageRemoveBackground"
);
const imageRemoveWatermarkEnabled = isCanvasToolbarItemEnabled(
canvasToolbarConfig,
"imageRemoveWatermark"
);
const imageEnhanceEnabled = isCanvasToolbarItemEnabled(
canvasToolbarConfig,
"imageEnhance"
);
const imageNoteEnabled = isCanvasToolbarItemEnabled(canvasToolbarConfig, "imageNote");
const imageLayerSplitEnabledByToolbar = isCanvasToolbarItemEnabled(
canvasToolbarConfig,
"imageLayerSplit"
);
const imageResizeEnabled = isCanvasToolbarItemEnabled(
canvasToolbarConfig,
"imageResize"
);
const imageMultiAngleEnabled = isCanvasToolbarItemEnabled(
canvasToolbarConfig,
"imageMultiAngle"
);
const imagePromptRegenerateEnabled = isCanvasToolbarItemEnabled(
canvasToolbarConfig,
"imagePromptRegenerate"
);
const imageTextEditEnabled = isCanvasToolbarItemEnabled(
canvasToolbarConfig,
"imageTextEdit"
);
const imageExtendEnabled = isCanvasToolbarItemEnabled(
canvasToolbarConfig,
"imageExtend"
);
const imageCropEnabled = isCanvasToolbarItemEnabled(canvasToolbarConfig, "imageCrop");
const imageAdjustEnabled = isCanvasToolbarItemEnabled(
canvasToolbarConfig,
"imageAdjust"
);
const imageFlipRotateEnabled = isCanvasToolbarItemEnabled(
canvasToolbarConfig,
"imageFlipRotate"
);
const imageRenameEnabled = isCanvasToolbarItemEnabled(
canvasToolbarConfig,
"imageRename"
);
const imageDownloadEnabled = isCanvasToolbarItemEnabled(
canvasToolbarConfig,
"imageDownload"
);
const imageDeleteEnabled = isCanvasToolbarItemEnabled(
canvasToolbarConfig,
"imageDelete"
);
const hasHoverImageMorePrimaryActions =
imageRemoveBackgroundEnabled ||
imageRemoveWatermarkEnabled ||
imageEnhanceEnabled ||
imageNoteEnabled ||
(isLayerSplitEnabled && imageLayerSplitEnabledByToolbar) ||
imageResizeEnabled ||
imageMultiAngleEnabled ||
imagePromptRegenerateEnabled ||
imageTextEditEnabled ||
imageExtendEnabled ||
imageCropEnabled ||
imageAdjustEnabled ||
imageFlipRotateEnabled ||
imageRenameEnabled;
const hasHoverImageMoreSecondaryActions =
imageDownloadEnabled || imageDeleteEnabled;
const hasHoverImageMoreMenu =
imageNodeMoreEnabled &&
(hasHoverImageMorePrimaryActions || hasHoverImageMoreSecondaryActions);
const hasSelectedImageMorePrimaryActions =
imageExtendEnabled ||
imageCropEnabled ||
imageAdjustEnabled ||
imageFlipRotateEnabled ||
imagePromptRegenerateEnabled ||
imageNoteEnabled;
const hasSelectedImageMoreMenu =
imageNodeMoreEnabled &&
(hasSelectedImageMorePrimaryActions || imageDeleteEnabled);
const contentScale = useMemo( const contentScale = useMemo(
() => () =>
getNodeContentScale(node.size, { getNodeContentScale(node.size, {
@ -3181,8 +3275,8 @@ export function ImageNode({
} }
const rect = containerRef.current.getBoundingClientRect(); const rect = containerRef.current.getBoundingClientRect();
const panelWidth = 520; const panelWidth = 520 * IMAGE_EDIT_PANEL_SCALE;
const panelHeight = 760; const panelHeight = 760 * IMAGE_EDIT_PANEL_SCALE;
const spacing = 18; const spacing = 18;
const maxTop = Math.max(12, window.innerHeight - panelHeight); const maxTop = Math.max(12, window.innerHeight - panelHeight);
@ -3201,8 +3295,8 @@ export function ImageNode({
} }
const rect = containerRef.current.getBoundingClientRect(); const rect = containerRef.current.getBoundingClientRect();
const panelWidth = 500; const panelWidth = 474 * IMAGE_EDIT_PANEL_SCALE;
const panelHeight = 980; const panelHeight = 980 * IMAGE_EDIT_PANEL_SCALE;
const spacing = 16; const spacing = 16;
const maxTop = Math.max(12, window.innerHeight - panelHeight); const maxTop = Math.max(12, window.innerHeight - panelHeight);
@ -3217,12 +3311,12 @@ export function ImageNode({
const getFlipRotatePanelPosition = () => { const getFlipRotatePanelPosition = () => {
if (!containerRef.current || typeof window === "undefined") { if (!containerRef.current || typeof window === "undefined") {
return { top: 0, left: 0, maxWidth: 660 }; return { top: 0, left: 0, maxWidth: 660 * IMAGE_EDIT_PANEL_SCALE };
} }
const rect = containerRef.current.getBoundingClientRect(); const rect = containerRef.current.getBoundingClientRect();
const panelWidth = 640; const panelWidth = 640 * IMAGE_EDIT_PANEL_SCALE;
const panelHeight = 84; const panelHeight = 84 * IMAGE_EDIT_PANEL_SCALE;
const gap = 18; const gap = 18;
const titleSafeHeight = 34; const titleSafeHeight = 34;
const { viewportPadding, safeLeft, safeRight, maxTop } = const { viewportPadding, safeLeft, safeRight, maxTop } =
@ -3275,8 +3369,8 @@ export function ImageNode({
} }
const rect = containerRef.current.getBoundingClientRect(); const rect = containerRef.current.getBoundingClientRect();
const panelWidth = 380; const panelWidth = 372 * IMAGE_EDIT_PANEL_SCALE;
const panelHeight = 760; const panelHeight = 760 * IMAGE_EDIT_PANEL_SCALE;
const spacing = 16; const spacing = 16;
const maxTop = Math.max(12, window.innerHeight - panelHeight); const maxTop = Math.max(12, window.innerHeight - panelHeight);
@ -6533,7 +6627,11 @@ export function ImageNode({
{/* 工具栏使用 Portal 渲染到 body,避免被裁剪 */} {/* 工具栏使用 Portal 渲染到 body,避免被裁剪 */}
{/* 悬浮时的更多操作按钮 (右上角) */} {/* 悬浮时的更多操作按钮 (右上角) */}
{isHovered && data.src && !isProcessing && !isSelected && ( {isHovered &&
data.src &&
!isProcessing &&
!isSelected &&
hasHoverImageMoreMenu ? (
<div className="absolute top-2 right-2 flex items-center gap-1"> <div className="absolute top-2 right-2 flex items-center gap-1">
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
@ -6551,6 +6649,7 @@ export function ImageNode({
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end"> <DropdownMenuContent align="end">
{imageRemoveBackgroundEnabled ? (
<DropdownMenuItem onClick={() => handleAction("rmbg")}> <DropdownMenuItem onClick={() => handleAction("rmbg")}>
<CanvasToolbarIcon <CanvasToolbarIcon
className="mr-2 h-4 w-4" className="mr-2 h-4 w-4"
@ -6559,6 +6658,8 @@ export function ImageNode({
/> />
{imageRemoveBackgroundItem.label} {imageRemoveBackgroundItem.label}
</DropdownMenuItem> </DropdownMenuItem>
) : null}
{imageRemoveWatermarkEnabled ? (
<DropdownMenuItem onClick={() => handleAction("watermark")}> <DropdownMenuItem onClick={() => handleAction("watermark")}>
<CanvasToolbarIcon <CanvasToolbarIcon
className="mr-2 h-4 w-4" className="mr-2 h-4 w-4"
@ -6567,6 +6668,8 @@ export function ImageNode({
/> />
{imageRemoveWatermarkItem.label} {imageRemoveWatermarkItem.label}
</DropdownMenuItem> </DropdownMenuItem>
) : null}
{imageEnhanceEnabled ? (
<DropdownMenuItem onClick={() => handleAction("enhance")}> <DropdownMenuItem onClick={() => handleAction("enhance")}>
<CanvasToolbarIcon <CanvasToolbarIcon
className="mr-2 h-4 w-4" className="mr-2 h-4 w-4"
@ -6575,6 +6678,8 @@ export function ImageNode({
/> />
{imageEnhanceItem.label} {imageEnhanceItem.label}
</DropdownMenuItem> </DropdownMenuItem>
) : null}
{imageNoteEnabled ? (
<DropdownMenuItem onClick={() => handleAction("note")}> <DropdownMenuItem onClick={() => handleAction("note")}>
<CanvasToolbarIcon <CanvasToolbarIcon
className="mr-2 h-4 w-4" className="mr-2 h-4 w-4"
@ -6583,7 +6688,8 @@ export function ImageNode({
/> />
{imageNoteItem.label} {imageNoteItem.label}
</DropdownMenuItem> </DropdownMenuItem>
{isLayerSplitEnabled ? ( ) : null}
{isLayerSplitEnabled && imageLayerSplitEnabledByToolbar ? (
<DropdownMenuItem onClick={() => handleAction("layer-split")}> <DropdownMenuItem onClick={() => handleAction("layer-split")}>
<CanvasToolbarIcon <CanvasToolbarIcon
className="mr-2 h-4 w-4" className="mr-2 h-4 w-4"
@ -6593,6 +6699,7 @@ export function ImageNode({
{imageLayerSplitItem.label} {imageLayerSplitItem.label}
</DropdownMenuItem> </DropdownMenuItem>
) : null} ) : null}
{imageResizeEnabled ? (
<DropdownMenuItem onClick={() => handleAction("resize")}> <DropdownMenuItem onClick={() => handleAction("resize")}>
<CanvasToolbarIcon <CanvasToolbarIcon
className="mr-2 h-4 w-4" className="mr-2 h-4 w-4"
@ -6601,6 +6708,8 @@ export function ImageNode({
/> />
{imageResizeItem.label} {imageResizeItem.label}
</DropdownMenuItem> </DropdownMenuItem>
) : null}
{imageMultiAngleEnabled ? (
<DropdownMenuItem onClick={() => handleAction("angle")}> <DropdownMenuItem onClick={() => handleAction("angle")}>
<CanvasToolbarIcon <CanvasToolbarIcon
className="mr-2 h-4 w-4" className="mr-2 h-4 w-4"
@ -6609,6 +6718,8 @@ export function ImageNode({
/> />
{imageMultiAngleItem.label} {imageMultiAngleItem.label}
</DropdownMenuItem> </DropdownMenuItem>
) : null}
{imagePromptRegenerateEnabled ? (
<DropdownMenuItem <DropdownMenuItem
onClick={() => handleAction("prompt-regenerate")} onClick={() => handleAction("prompt-regenerate")}
> >
@ -6619,6 +6730,8 @@ export function ImageNode({
/> />
{imagePromptRegenerateItem.label} {imagePromptRegenerateItem.label}
</DropdownMenuItem> </DropdownMenuItem>
) : null}
{imageTextEditEnabled ? (
<DropdownMenuItem onClick={() => handleAction("text-edit")}> <DropdownMenuItem onClick={() => handleAction("text-edit")}>
<CanvasToolbarIcon <CanvasToolbarIcon
className="mr-2 h-4 w-4" className="mr-2 h-4 w-4"
@ -6627,6 +6740,8 @@ export function ImageNode({
/> />
{imageTextEditItem.label} {imageTextEditItem.label}
</DropdownMenuItem> </DropdownMenuItem>
) : null}
{imageExtendEnabled ? (
<DropdownMenuItem onClick={() => handleAction("extend")}> <DropdownMenuItem onClick={() => handleAction("extend")}>
<CanvasToolbarIcon <CanvasToolbarIcon
className="mr-2 h-4 w-4" className="mr-2 h-4 w-4"
@ -6635,6 +6750,8 @@ export function ImageNode({
/> />
{imageExtendItem.label} {imageExtendItem.label}
</DropdownMenuItem> </DropdownMenuItem>
) : null}
{imageCropEnabled ? (
<DropdownMenuItem onClick={() => handleAction("crop")}> <DropdownMenuItem onClick={() => handleAction("crop")}>
<CanvasToolbarIcon <CanvasToolbarIcon
className="mr-2 h-4 w-4" className="mr-2 h-4 w-4"
@ -6643,6 +6760,8 @@ export function ImageNode({
/> />
{imageCropItem.label} {imageCropItem.label}
</DropdownMenuItem> </DropdownMenuItem>
) : null}
{imageAdjustEnabled ? (
<DropdownMenuItem onClick={() => handleAction("adjust")}> <DropdownMenuItem onClick={() => handleAction("adjust")}>
<CanvasToolbarIcon <CanvasToolbarIcon
className="mr-2 h-4 w-4" className="mr-2 h-4 w-4"
@ -6651,6 +6770,8 @@ export function ImageNode({
/> />
{imageAdjustItem.label} {imageAdjustItem.label}
</DropdownMenuItem> </DropdownMenuItem>
) : null}
{imageFlipRotateEnabled ? (
<DropdownMenuItem onClick={() => handleAction("flip-rotate")}> <DropdownMenuItem onClick={() => handleAction("flip-rotate")}>
<CanvasToolbarIcon <CanvasToolbarIcon
className="mr-2 h-4 w-4" className="mr-2 h-4 w-4"
@ -6659,6 +6780,8 @@ export function ImageNode({
/> />
{imageFlipRotateItem.label} {imageFlipRotateItem.label}
</DropdownMenuItem> </DropdownMenuItem>
) : null}
{imageRenameEnabled ? (
<DropdownMenuItem <DropdownMenuItem
onClick={() => { onClick={() => {
setDraftTitle(displayTitle); setDraftTitle(displayTitle);
@ -6672,7 +6795,12 @@ export function ImageNode({
/> />
{imageRenameItem.label} {imageRenameItem.label}
</DropdownMenuItem> </DropdownMenuItem>
) : null}
{hasHoverImageMorePrimaryActions &&
hasHoverImageMoreSecondaryActions ? (
<DropdownMenuSeparator /> <DropdownMenuSeparator />
) : null}
{imageDownloadEnabled ? (
<DropdownMenuItem onClick={handleDownload}> <DropdownMenuItem onClick={handleDownload}>
<CanvasToolbarIcon <CanvasToolbarIcon
className="mr-2 h-4 w-4" className="mr-2 h-4 w-4"
@ -6681,6 +6809,8 @@ export function ImageNode({
/> />
{imageDownloadItem.label} {imageDownloadItem.label}
</DropdownMenuItem> </DropdownMenuItem>
) : null}
{imageDeleteEnabled ? (
<DropdownMenuItem <DropdownMenuItem
className="text-destructive" className="text-destructive"
onClick={() => deleteNode(node.id)} onClick={() => deleteNode(node.id)}
@ -6692,10 +6822,11 @@ export function ImageNode({
/> />
{imageDeleteItem.label} {imageDeleteItem.label}
</DropdownMenuItem> </DropdownMenuItem>
) : null}
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
</div> </div>
)} ) : null}
{/* 提示词显示 */} {/* 提示词显示 */}
{data.prompt && isHovered && !isProcessing && ( {data.prompt && isHovered && !isProcessing && (
@ -7131,6 +7262,7 @@ export function ImageNode({
<div className="mx-1 h-6 w-px bg-slate-200 dark:bg-slate-700" /> <div className="mx-1 h-6 w-px bg-slate-200 dark:bg-slate-700" />
{hasSelectedImageMoreMenu ? (
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button <Button
@ -7152,6 +7284,7 @@ export function ImageNode({
className="bg-white opacity-100 backdrop-blur-none dark:bg-slate-900 dark:text-slate-100" className="bg-white opacity-100 backdrop-blur-none dark:bg-slate-900 dark:text-slate-100"
side="bottom" side="bottom"
> >
{imageExtendEnabled ? (
<DropdownMenuItem onClick={() => handleAction("extend")}> <DropdownMenuItem onClick={() => handleAction("extend")}>
<CanvasToolbarIcon <CanvasToolbarIcon
className="mr-2 h-4 w-4" className="mr-2 h-4 w-4"
@ -7160,6 +7293,8 @@ export function ImageNode({
/> />
{imageExtendItem.label} {imageExtendItem.label}
</DropdownMenuItem> </DropdownMenuItem>
) : null}
{imageCropEnabled ? (
<DropdownMenuItem onClick={() => handleAction("crop")}> <DropdownMenuItem onClick={() => handleAction("crop")}>
<CanvasToolbarIcon <CanvasToolbarIcon
className="mr-2 h-4 w-4" className="mr-2 h-4 w-4"
@ -7168,6 +7303,8 @@ export function ImageNode({
/> />
{imageCropItem.label} {imageCropItem.label}
</DropdownMenuItem> </DropdownMenuItem>
) : null}
{imageAdjustEnabled ? (
<DropdownMenuItem onClick={() => handleAction("adjust")}> <DropdownMenuItem onClick={() => handleAction("adjust")}>
<CanvasToolbarIcon <CanvasToolbarIcon
className="mr-2 h-4 w-4" className="mr-2 h-4 w-4"
@ -7176,6 +7313,8 @@ export function ImageNode({
/> />
{imageAdjustItem.label} {imageAdjustItem.label}
</DropdownMenuItem> </DropdownMenuItem>
) : null}
{imageFlipRotateEnabled ? (
<DropdownMenuItem onClick={() => handleAction("flip-rotate")}> <DropdownMenuItem onClick={() => handleAction("flip-rotate")}>
<CanvasToolbarIcon <CanvasToolbarIcon
className="mr-2 h-4 w-4" className="mr-2 h-4 w-4"
@ -7184,6 +7323,8 @@ export function ImageNode({
/> />
{imageFlipRotateItem.label} {imageFlipRotateItem.label}
</DropdownMenuItem> </DropdownMenuItem>
) : null}
{imagePromptRegenerateEnabled ? (
<DropdownMenuItem <DropdownMenuItem
onClick={() => handleAction("prompt-regenerate")} onClick={() => handleAction("prompt-regenerate")}
> >
@ -7194,6 +7335,8 @@ export function ImageNode({
/> />
{imagePromptRegenerateItem.label} {imagePromptRegenerateItem.label}
</DropdownMenuItem> </DropdownMenuItem>
) : null}
{imageNoteEnabled ? (
<DropdownMenuItem onClick={() => handleAction("note")}> <DropdownMenuItem onClick={() => handleAction("note")}>
<CanvasToolbarIcon <CanvasToolbarIcon
className="mr-2 h-4 w-4" className="mr-2 h-4 w-4"
@ -7202,7 +7345,11 @@ export function ImageNode({
/> />
{imageNoteItem.label} {imageNoteItem.label}
</DropdownMenuItem> </DropdownMenuItem>
) : null}
{hasSelectedImageMorePrimaryActions && imageDeleteEnabled ? (
<DropdownMenuSeparator /> <DropdownMenuSeparator />
) : null}
{imageDeleteEnabled ? (
<DropdownMenuItem <DropdownMenuItem
className="text-red-600 focus:text-red-700" className="text-red-600 focus:text-red-700"
onClick={() => deleteNode(node.id)} onClick={() => deleteNode(node.id)}
@ -7214,8 +7361,10 @@ export function ImageNode({
/> />
{imageDeleteItem.label} {imageDeleteItem.label}
</DropdownMenuItem> </DropdownMenuItem>
) : null}
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
) : null}
<Button <Button
className="h-10 w-10 rounded-xl text-slate-700 hover:bg-slate-100 hover:text-slate-900 dark:text-slate-100 dark:hover:bg-zinc-800" className="h-10 w-10 rounded-xl text-slate-700 hover:bg-slate-100 hover:text-slate-900 dark:text-slate-100 dark:hover:bg-zinc-800"
@ -7358,6 +7507,8 @@ export function ImageNode({
style={{ style={{
top: cropPanelPosition.top, top: cropPanelPosition.top,
left: cropPanelPosition.left, left: cropPanelPosition.left,
transform: `scale(${IMAGE_EDIT_PANEL_SCALE})`,
transformOrigin: "top left",
}} }}
> >
<div className="px-8 pt-8 pb-6"> <div className="px-8 pt-8 pb-6">
@ -7513,6 +7664,8 @@ export function ImageNode({
style={{ style={{
top: imageAdjustPanelPosition.top, top: imageAdjustPanelPosition.top,
left: imageAdjustPanelPosition.left, left: imageAdjustPanelPosition.left,
transform: `scale(${IMAGE_EDIT_PANEL_SCALE})`,
transformOrigin: "top left",
}} }}
> >
<div className="flex items-center justify-between px-8 pt-8 pb-5"> <div className="flex items-center justify-between px-8 pt-8 pb-5">
@ -7604,7 +7757,11 @@ export function ImageNode({
style={{ style={{
top: flipRotatePanelPosition.top, top: flipRotatePanelPosition.top,
left: flipRotatePanelPosition.left, left: flipRotatePanelPosition.left,
maxWidth: `${flipRotatePanelPosition.maxWidth}px`, maxWidth: `${Math.floor(
flipRotatePanelPosition.maxWidth / IMAGE_EDIT_PANEL_SCALE
)}px`,
transform: `scale(${IMAGE_EDIT_PANEL_SCALE})`,
transformOrigin: "top left",
}} }}
> >
<div className="flex h-[82px] shrink-0 items-center gap-3 px-7"> <div className="flex h-[82px] shrink-0 items-center gap-3 px-7">
@ -7712,6 +7869,8 @@ export function ImageNode({
style={{ style={{
top: anglePanelPosition.top, top: anglePanelPosition.top,
left: anglePanelPosition.left, left: anglePanelPosition.left,
transform: `scale(${IMAGE_EDIT_PANEL_SCALE})`,
transformOrigin: "top left",
}} }}
> >
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">

View File

@ -102,18 +102,24 @@ type FloatingSidebarRailProps = {
children: ReactNode; children: ReactNode;
className?: string; className?: string;
orientation?: "vertical" | "horizontal"; orientation?: "vertical" | "horizontal";
tone?: "dark" | "light";
}; };
export function FloatingSidebarRail({ export function FloatingSidebarRail({
children, children,
className, className,
orientation = "vertical", orientation = "vertical",
tone = "dark",
}: FloatingSidebarRailProps) { }: FloatingSidebarRailProps) {
const isHorizontal = orientation === "horizontal"; const isHorizontal = orientation === "horizontal";
const isLight = tone === "light";
return ( return (
<div <div
className={cn( className={cn(
"pointer-events-auto flex items-center gap-1 border border-white/8 bg-[#17181d]/94 text-white shadow-[0_22px_60px_rgba(6,8,14,0.34),inset_0_1px_0_rgba(255,255,255,0.05)] backdrop-blur-2xl", "pointer-events-auto flex items-center gap-1 backdrop-blur-2xl",
isLight
? "border border-black/8 bg-white/96 text-[#111827] shadow-[0_20px_46px_rgba(15,23,42,0.12)]"
: "border border-white/8 bg-[#17181d]/94 text-white shadow-[0_22px_60px_rgba(6,8,14,0.34),inset_0_1px_0_rgba(255,255,255,0.05)]",
isHorizontal isHorizontal
? "h-[56px] w-auto flex-row overflow-hidden rounded-[22px] px-3 py-2" ? "h-[56px] w-auto flex-row overflow-hidden rounded-[22px] px-3 py-2"
: "w-[60px] flex-col overflow-y-auto rounded-[28px] px-2 py-2.5", : "w-[60px] flex-col overflow-y-auto rounded-[28px] px-2 py-2.5",

View File

@ -443,12 +443,12 @@ export function LovartHome({
!siteLogoDark?.trim() && !siteLogoDark?.trim() &&
Boolean(siteLogo?.trim()); Boolean(siteLogo?.trim());
const sidebarPopupBaseClass = const sidebarPopupBaseClass =
"rounded-[24px] border border-white/10 bg-[#17181d]/96 p-2.5 text-white shadow-[0_24px_64px_rgba(6,8,14,0.4),inset_0_1px_0_rgba(255,255,255,0.04)] backdrop-blur-2xl"; "rounded-[24px] border border-black/8 bg-white/96 p-2.5 text-[#111827] shadow-[0_24px_64px_rgba(15,23,42,0.12)] backdrop-blur-xl";
const lovartSidebarButtonClass = (active = false) => const lovartSidebarButtonClass = (active = false) =>
cn( cn(
"group flex h-11 w-11 items-center justify-center rounded-[16px] text-white/52 transition-all duration-200 hover:bg-white/[0.07] hover:text-white/88", "group flex h-11 w-11 items-center justify-center rounded-[16px] text-[#6b7280] transition-all duration-200 hover:bg-black/[0.04] hover:text-[#111827]",
active && active &&
"bg-white/[0.1] text-white shadow-[inset_0_1px_0_rgba(255,255,255,0.08),0_10px_22px_rgba(0,0,0,0.22)]" "bg-[#111827] text-white shadow-[0_10px_22px_rgba(15,23,42,0.18)]"
); );
const [prompt, setPrompt] = useState(""); const [prompt, setPrompt] = useState("");
const [files, setFiles] = useState<UploadFileItem[]>([]); const [files, setFiles] = useState<UploadFileItem[]>([]);
@ -1381,8 +1381,8 @@ export function LovartHome({
<SidebarSvgIcon <SidebarSvgIcon
iconUrl={item.iconUrl} iconUrl={item.iconUrl}
fallback={FallbackIcon} fallback={FallbackIcon}
imgClassName={cn(sizeClassName, "object-contain", "brightness-0")} imgClassName={cn(sizeClassName, "object-contain", "brightness-0 invert")}
fallbackClassName={cn(sizeClassName, "text-black")} fallbackClassName={cn(sizeClassName, "text-white")}
/> />
); );
} }
@ -1394,12 +1394,11 @@ export function LovartHome({
imgClassName={cn( imgClassName={cn(
sizeClassName, sizeClassName,
"object-contain", "object-contain",
"brightness-0 invert", options?.active ? "brightness-0 invert opacity-100" : "brightness-0 opacity-60"
options?.active ? "opacity-100" : "opacity-56"
)} )}
fallbackClassName={cn( fallbackClassName={cn(
sizeClassName, sizeClassName,
options?.active ? "text-white" : "text-white/60" options?.active ? "text-white" : "text-[#6b7280]"
)} )}
/> />
); );
@ -1718,10 +1717,10 @@ export function LovartHome({
<button <button
type="button" type="button"
onClick={openNewCanvas} onClick={openNewCanvas}
className="flex h-[60px] w-[60px] shrink-0 items-center justify-center rounded-[22px] border border-white/8 bg-[#17181d]/96 shadow-[0_22px_60px_rgba(6,8,14,0.34),inset_0_1px_0_rgba(255,255,255,0.05)] backdrop-blur-2xl transition-transform duration-200 hover:-translate-y-0.5 hover:bg-[#1b1d23]" className="flex h-[60px] w-[60px] shrink-0 items-center justify-center rounded-[22px] border border-black/8 bg-white/96 shadow-[0_20px_46px_rgba(15,23,42,0.12)] backdrop-blur-xl transition-transform duration-200 hover:-translate-y-0.5 hover:bg-white"
title={getLovartSidebarItem("newProject").label} title={getLovartSidebarItem("newProject").label}
> >
<div className="flex h-10 w-10 items-center justify-center rounded-[14px] bg-white text-black shadow-[0_10px_18px_rgba(255,255,255,0.15)]"> <div className="flex h-10 w-10 items-center justify-center rounded-[14px] bg-[#111827] text-white shadow-[0_10px_20px_rgba(15,23,42,0.18)]">
{renderLovartSidebarIcon("newProject", { {renderLovartSidebarIcon("newProject", {
filled: true, filled: true,
sizeClassName: "h-[18px] w-[18px]", sizeClassName: "h-[18px] w-[18px]",
@ -1734,7 +1733,7 @@ export function LovartHome({
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
) : null} ) : null}
<FloatingSidebarRail className="max-h-[calc(100vh-172px)]"> <FloatingSidebarRail className="max-h-[calc(100vh-172px)]" tone="light">
{isLovartSidebarItemEnabled("home") ? ( {isLovartSidebarItemEnabled("home") ? (
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
@ -1827,7 +1826,7 @@ export function LovartHome({
align="start" align="start"
className={`w-[300px] ${sidebarPopupBaseClass}`} className={`w-[300px] ${sidebarPopupBaseClass}`}
> >
<div className="px-2 pb-1 pt-1 text-xs font-medium text-white/42"> <div className="px-2 pb-1 pt-1 text-xs font-medium text-black/40">
{getLovartSidebarItem("tutorial").label} {getLovartSidebarItem("tutorial").label}
</div> </div>
<div className="max-h-[380px] space-y-1 overflow-y-auto"> <div className="max-h-[380px] space-y-1 overflow-y-auto">
@ -1836,10 +1835,10 @@ export function LovartHome({
<button <button
key={video.id} key={video.id}
type="button" type="button"
className="flex w-full items-center gap-2.5 rounded-2xl p-2.5 text-left transition hover:bg-white/[0.06]" className="flex w-full items-center gap-2.5 rounded-2xl p-2.5 text-left transition hover:bg-black/[0.04]"
onClick={() => handleOpenTutorial(video)} onClick={() => handleOpenTutorial(video)}
> >
<div className="h-10 w-16 flex-shrink-0 overflow-hidden rounded-xl bg-white/[0.06]"> <div className="h-10 w-16 flex-shrink-0 overflow-hidden rounded-xl bg-black/[0.04]">
{video.coverUrl ? ( {video.coverUrl ? (
<img <img
src={video.coverUrl} src={video.coverUrl}
@ -1848,16 +1847,16 @@ export function LovartHome({
/> />
) : ( ) : (
<div className="flex h-full w-full items-center justify-center"> <div className="flex h-full w-full items-center justify-center">
<PlayCircle className="h-4 w-4 text-white/40" /> <PlayCircle className="h-4 w-4 text-black/35" />
</div> </div>
)} )}
</div> </div>
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<div className="truncate text-[13px] font-medium text-white/88"> <div className="truncate text-[13px] font-medium text-black/85">
{video.title} {video.title}
</div> </div>
{video.description ? ( {video.description ? (
<div className="truncate text-[11px] text-white/42"> <div className="truncate text-[11px] text-black/42">
{video.description} {video.description}
</div> </div>
) : null} ) : null}
@ -1865,7 +1864,7 @@ export function LovartHome({
</button> </button>
)) ))
) : ( ) : (
<div className="px-2 py-3 text-[12px] text-white/42">暂无教程视频</div> <div className="px-2 py-3 text-[12px] text-black/42">暂无教程视频</div>
)} )}
</div> </div>
</HoverCardContent> </HoverCardContent>
@ -1994,7 +1993,7 @@ export function LovartHome({
align="start" align="start"
className={`w-[248px] ${sidebarPopupBaseClass}`} className={`w-[248px] ${sidebarPopupBaseClass}`}
> >
<div className="px-2 pb-1 pt-1 text-xs font-medium text-white/42"> <div className="px-2 pb-1 pt-1 text-xs font-medium text-black/40">
{getLovartSidebarItem("toolbox1").label} {getLovartSidebarItem("toolbox1").label}
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
@ -2002,11 +2001,11 @@ export function LovartHome({
<button <button
key={link.id} key={link.id}
type="button" type="button"
className="flex w-full items-center justify-between rounded-2xl px-3 py-2.5 text-left text-[13px] text-white/82 transition hover:bg-white/[0.06]" className="flex w-full items-center justify-between rounded-2xl px-3 py-2.5 text-left text-[13px] text-black/80 transition hover:bg-black/[0.04]"
onClick={() => openNavLink(link.url)} onClick={() => openNavLink(link.url)}
> >
<span className="line-clamp-1">{link.name}</span> <span className="line-clamp-1">{link.name}</span>
<ArrowRight className="h-3.5 w-3.5 text-white/30" /> <ArrowRight className="h-3.5 w-3.5 text-black/30" />
</button> </button>
))} ))}
</div> </div>
@ -2031,7 +2030,7 @@ export function LovartHome({
align="start" align="start"
className={`w-[248px] ${sidebarPopupBaseClass}`} className={`w-[248px] ${sidebarPopupBaseClass}`}
> >
<div className="px-2 pb-1 pt-1 text-xs font-medium text-white/42"> <div className="px-2 pb-1 pt-1 text-xs font-medium text-black/40">
{getLovartSidebarItem("toolbox2").label} {getLovartSidebarItem("toolbox2").label}
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
@ -2039,11 +2038,11 @@ export function LovartHome({
<button <button
key={link.id} key={link.id}
type="button" type="button"
className="flex w-full items-center justify-between rounded-2xl px-3 py-2.5 text-left text-[13px] text-white/82 transition hover:bg-white/[0.06]" className="flex w-full items-center justify-between rounded-2xl px-3 py-2.5 text-left text-[13px] text-black/80 transition hover:bg-black/[0.04]"
onClick={() => openNavLink(link.url)} onClick={() => openNavLink(link.url)}
> >
<span className="line-clamp-1">{link.name}</span> <span className="line-clamp-1">{link.name}</span>
<ArrowRight className="h-3.5 w-3.5 text-white/30" /> <ArrowRight className="h-3.5 w-3.5 text-black/30" />
</button> </button>
))} ))}
</div> </div>

View File

@ -699,6 +699,7 @@ export type CanvasToolbarItemKey =
export type CanvasToolbarItemConfig = { export type CanvasToolbarItemConfig = {
label: string; label: string;
iconUrl: string; iconUrl: string;
enabled: boolean;
}; };
export type CanvasToolbarConfig = { export type CanvasToolbarConfig = {
@ -710,6 +711,7 @@ const DEFAULT_ITEM_BY_KEY = DEFAULT_CANVAS_TOOLBAR_ITEMS.reduce(
acc[item.key] = { acc[item.key] = {
label: item.label, label: item.label,
iconUrl: "", iconUrl: "",
enabled: true,
}; };
return acc; return acc;
}, },
@ -750,6 +752,8 @@ export function normalizeCanvasToolbarConfig(
items[defaultItem.key] = { items[defaultItem.key] = {
label, label,
iconUrl, iconUrl,
enabled:
typeof rawItem.enabled === "boolean" ? rawItem.enabled : true,
}; };
}); });
@ -768,3 +772,10 @@ export function getCanvasToolbarItem(
): CanvasToolbarItemConfig { ): CanvasToolbarItemConfig {
return config?.items?.[key] || DEFAULT_ITEM_BY_KEY[key]; return config?.items?.[key] || DEFAULT_ITEM_BY_KEY[key];
} }
export function isCanvasToolbarItemEnabled(
config: CanvasToolbarConfig | null | undefined,
key: CanvasToolbarItemKey
): boolean {
return getCanvasToolbarItem(config, key).enabled !== false;
}