fix(canvas): 画布浮层 - 修复小地图定位并弱化卡片边界

This commit is contained in:
ddCat
2026-08-22 13:51:24 +08:00
parent 8f8b0f108d
commit c02db54635
4 changed files with 27 additions and 20 deletions

View File

@@ -187,17 +187,16 @@ export function CanvasAssetTray({ assetImages, canvasImages, showLibrary = true,
exit={{ opacity: 0, y: 14, scale: 0.95, rotateX: 3 }}
transition={aceternityMotion.spring.panel}
ref={panelRef}
className="canvas-asset-tray-panel aceternity-floating-panel absolute bottom-[var(--canvas-dock-popover-offset)] left-0 flex w-[min(88vw,312px)] origin-bottom-left flex-col overflow-hidden rounded-[var(--r-2xl)] border p-2.5 backdrop-blur-2xl"
style={{ background: theme.spatial.elevated, borderColor: theme.toolbar.border, color: theme.node.text, height: safeTrayHeight, minHeight: Math.min(TRAY_MIN_HEIGHT, getMaxTrayHeight()), maxHeight: "calc(100vh - 6rem)", boxShadow: `0 32px 100px ${theme.spatial.shadow}` }}
className="canvas-asset-tray-panel aceternity-floating-panel absolute bottom-[var(--canvas-dock-popover-offset)] left-0 flex w-[min(88vw,312px)] origin-bottom-left flex-col overflow-hidden rounded-[var(--r-2xl)] p-2.5 backdrop-blur-2xl"
style={{ background: theme.spatial.elevated, color: theme.node.text, height: safeTrayHeight, minHeight: Math.min(TRAY_MIN_HEIGHT, getMaxTrayHeight()), maxHeight: "calc(100vh - 6rem)", boxShadow: `0 32px 100px ${theme.spatial.shadow}` }}
>
<div className="absolute inset-x-10 top-0 h-px" style={{ background: `linear-gradient(90deg, transparent, ${theme.spatial.glowStrong}, transparent)` }} />
<button type="button" className="canvas-asset-tray-resize-handle absolute left-1/2 top-1 z-10 flex h-5 w-28 -translate-x-1/2 items-center justify-center rounded-full opacity-35 transition-opacity hover:opacity-75" onPointerDown={startResize} aria-label="从顶部调整素材托盘高度" title="拖动调整高度">
<span className="h-1 w-12 rounded-full bg-current" />
</button>
<div className="flex items-center justify-between gap-2 px-1 pb-2.5 pt-1.5">
<div className="flex min-w-0 items-center gap-2">
<span className="grid size-8 shrink-0 place-items-center rounded-[var(--dock-item-radius)] border" style={{ background: theme.spatial.surface, borderColor: theme.toolbar.border, color: theme.accent.primary }}>
<span className="grid size-8 shrink-0 place-items-center rounded-[var(--dock-item-radius)]" style={{ background: theme.spatial.surface, color: theme.accent.primary }}>
<FolderOpen className="size-3.5" />
</span>
<span className="min-w-0">
@@ -205,17 +204,17 @@ export function CanvasAssetTray({ assetImages, canvasImages, showLibrary = true,
<span className="mt-0.5 block truncate text-[var(--fs-micro)]" style={{ color: theme.node.muted }}>拖入画布,或定位已经使用的图片</span>
</span>
</div>
<motion.button type="button" whileHover={motionEnabled ? { rotate: -5, scale: 1.05 } : undefined} whileTap={motionEnabled ? { scale: 0.92 } : undefined} className="grid size-7 shrink-0 place-items-center rounded-full border outline-none focus-visible:ring-2" style={{ background: theme.spatial.surface, borderColor: theme.toolbar.border, color: theme.node.muted }} onClick={() => setOpen(false)} aria-label="收起素材空间">
<motion.button type="button" whileHover={motionEnabled ? { rotate: -5, scale: 1.05 } : undefined} whileTap={motionEnabled ? { scale: 0.92 } : undefined} className="grid size-7 shrink-0 place-items-center rounded-full outline-none focus-visible:ring-2" style={{ background: theme.spatial.surface, color: theme.node.muted }} onClick={() => setOpen(false)} aria-label="收起素材空间">
<PanelLeftClose className="size-3" />
</motion.button>
</div>
<div className={cn("relative grid gap-1 rounded-[var(--r-lg)] border p-0.5", showLibrary ? "grid-cols-2" : "grid-cols-1")} style={{ background: theme.spatial.surface, borderColor: theme.toolbar.border }}>
<div className={cn("relative grid gap-1 rounded-[var(--r-lg)] p-0.5", showLibrary ? "grid-cols-2" : "grid-cols-1")} style={{ background: theme.spatial.surface }}>
{showLibrary ? <TrayTabButton active={tab === "library"} label={`素材库 ${assetImages.length}`} theme={theme} onClick={() => setTab("library")} /> : null}
<TrayTabButton active={tab === "canvas"} label={`当前画布 ${canvasImages.length}`} theme={theme} onClick={() => setTab("canvas")} />
</div>
<label className="mt-2 flex h-8 items-center gap-1.5 rounded-[11px] border px-2.5 focus-within:ring-2" style={{ background: theme.spatial.surface, borderColor: theme.toolbar.border }}>
<label className="mt-2 flex h-8 items-center gap-1.5 rounded-[11px] px-2.5 focus-within:ring-2" style={{ background: theme.spatial.surface }}>
<Search className="size-3.5 shrink-0" style={{ color: theme.node.muted }} />
<input type="search" value={keyword} onChange={(event) => setKeyword(event.target.value)} placeholder="搜索图片素材..." className="min-w-0 flex-1 bg-transparent text-[var(--fs-tiny)] outline-none placeholder:opacity-55" aria-label="搜索图片素材" />
{keyword ? <button type="button" className="grid size-6 shrink-0 place-items-center rounded-full opacity-55 hover:opacity-100" onClick={() => setKeyword("")} aria-label="清空搜索"><X className="size-3" /></button> : null}
@@ -245,7 +244,7 @@ export function CanvasAssetTray({ assetImages, canvasImages, showLibrary = true,
<div className="flex items-center justify-between px-1 pt-2.5 text-[var(--fs-tiny)]" style={{ color: theme.node.muted }}>
<span>{showLibrary && tab === "library" ? "点击插入 · 拖拽定位" : "点击回到节点"}</span>
<span className="rounded-full border px-2 py-0.5 tabular-nums" style={{ background: theme.spatial.surface, borderColor: theme.toolbar.border }}>{activeItems.length} 项</span>
<span className="rounded-full px-2 py-0.5 tabular-nums" style={{ background: theme.spatial.surface }}>{activeItems.length} 项</span>
</div>
</motion.aside>
) : null}
@@ -261,7 +260,7 @@ type CanvasTheme = (typeof canvasThemes)[keyof typeof canvasThemes];
function TrayTabButton({ active, label, theme, onClick }: { active: boolean; label: string; theme: CanvasTheme; onClick: () => void }) {
return (
<button type="button" className={cn("relative z-10 h-7 rounded-[var(--dock-item-radius)] px-2 text-[var(--fs-tiny)] font-semibold outline-none transition-colors focus-visible:ring-2", active ? "" : "opacity-55 hover:opacity-90")} style={{ color: active ? theme.node.text : theme.node.muted }} onClick={onClick}>
{active ? <motion.span layoutId="canvas-asset-tray-active-tab" className="absolute inset-0 -z-10 rounded-[var(--dock-item-radius)] border" style={{ background: theme.node.panel, borderColor: theme.toolbar.border, boxShadow: `0 6px 16px ${theme.spatial.shadow}` }} transition={aceternityMotion.spring.dock} /> : null}
{active ? <motion.span layoutId="canvas-asset-tray-active-tab" className="absolute inset-0 -z-10 rounded-[var(--dock-item-radius)]" style={{ background: theme.node.panel, boxShadow: `0 6px 16px ${theme.spatial.shadow}` }} transition={aceternityMotion.spring.dock} /> : null}
{label}
</button>
);
@@ -277,12 +276,12 @@ function AssetTrayRow({ title, imageUrl, storageKey, icon, active = false, dragg
whileHover={motionEnabled ? { x: 4, scale: 1.008 } : undefined}
whileTap={motionEnabled ? { scale: 0.985 } : undefined}
transition={aceternityMotion.spring.dock}
className="group grid h-12 w-full grid-cols-[36px_minmax(0,1fr)_auto_24px] items-center gap-1.5 rounded-[var(--r-lg)] border px-1.5 text-left outline-none focus-visible:ring-2"
style={{ background: active ? theme.accent.primarySoft : theme.spatial.surface, borderColor: active ? theme.spatial.glowStrong : theme.toolbar.border, color: active ? theme.accent.primary : theme.node.text }}
className="group grid h-12 w-full grid-cols-[36px_minmax(0,1fr)_auto_24px] items-center gap-1.5 rounded-[var(--r-lg)] px-1.5 text-left outline-none focus-visible:ring-2"
style={{ background: active ? theme.accent.primarySoft : theme.spatial.surface, color: active ? theme.accent.primary : theme.node.text, boxShadow: active ? `0 6px 18px ${theme.spatial.shadow}` : undefined }}
onClick={onClick}
onDragStartCapture={onDragStart}
>
<span className="grid size-9 shrink-0 place-items-center overflow-hidden rounded-[var(--dock-item-radius)] border" style={{ background: theme.node.fill, borderColor: theme.toolbar.border }}>
<span className="grid size-9 shrink-0 place-items-center overflow-hidden rounded-[var(--dock-item-radius)]" style={{ background: theme.node.fill }}>
{imageUrl || storageKey ? <CachedResourceImage storageKey={storageKey} src={imageUrl} alt="" width={36} height={36} className="size-full object-cover" draggable={false} fallback={<ImageIcon className="size-3.5 opacity-55" />} /> : <ImageIcon className="size-3.5 opacity-55" />}
</span>
<span className="min-w-0">
@@ -292,14 +291,14 @@ function AssetTrayRow({ title, imageUrl, storageKey, icon, active = false, dragg
<span className={cn("rounded-full px-1.5 py-0.5 text-[var(--fs-micro)] font-semibold", location === "oss" ? "bg-emerald-500/12 text-emerald-700 dark:text-emerald-300" : location === "local" ? "bg-amber-500/12 text-amber-700 dark:text-amber-300" : "bg-black/5 text-stone-400 dark:bg-white/8 dark:text-stone-500")} title={resourceStorageTitle(storageKey)}>
{resourceStorageLabel(storageKey)}
</span>
<span className="grid size-6 place-items-center rounded-full border opacity-45 transition-opacity group-hover:opacity-90" style={{ background: theme.node.panel, borderColor: theme.toolbar.border }}>{icon}</span>
<span className="grid size-6 place-items-center rounded-full opacity-45 transition-opacity group-hover:opacity-90" style={{ background: theme.node.panel }}>{icon}</span>
</motion.button>
);
}
function TrayEmpty({ text, theme }: { text: string; theme: CanvasTheme }) {
return (
<div className="grid h-full min-h-[200px] place-items-center rounded-[var(--dock-radius)] border border-dashed text-center" style={{ background: theme.spatial.surface, borderColor: theme.toolbar.border, color: theme.node.muted }}>
<div className="grid h-full min-h-[200px] place-items-center rounded-[var(--dock-radius)] text-center" style={{ background: theme.spatial.surface, color: theme.node.muted }}>
<span><Images className="mx-auto size-5 opacity-35" /><span className="mt-2 block text-xs opacity-55">{text}</span></span>
</div>
);

View File

@@ -125,7 +125,7 @@ export function Minimap({ nodes, viewport, viewportSize, canvasContainerRef, onV
};
return (
<div className="absolute bottom-24 left-6 z-[var(--z-panel)] overflow-hidden rounded-lg border shadow-2xl backdrop-blur-sm" style={{ width, height, background: theme.toolbar.panel, borderColor: theme.toolbar.border }}>
<div className="absolute bottom-[calc(var(--canvas-inset-y)+var(--space-16)+var(--space-10))] left-6 z-[var(--z-panel)] overflow-hidden rounded-lg shadow-2xl backdrop-blur-sm lg:bottom-[calc(var(--canvas-inset-y)+var(--space-12))]" style={{ width, height, background: theme.toolbar.panel }}>
<div
ref={containerRef}
className="relative h-full w-full cursor-crosshair"
@@ -167,7 +167,7 @@ export function Minimap({ nodes, viewport, viewportSize, canvasContainerRef, onV
/>
);
})}
<div ref={viewportRectRef} className="pointer-events-none absolute border" style={{ left: viewportRect.x, top: viewportRect.y, width: viewportRect.w, height: viewportRect.h, borderColor: theme.node.activeStroke, background: `${theme.node.activeStroke}18` }} />
<div ref={viewportRectRef} className="pointer-events-none absolute rounded-[var(--r-xs)]" style={{ left: viewportRect.x, top: viewportRect.y, width: viewportRect.w, height: viewportRect.h, background: `${theme.node.activeStroke}12`, boxShadow: `inset 0 0 0 1px ${theme.node.activeStroke}66` }} />
</div>
</div>
);

View File

@@ -291,7 +291,8 @@ export const CanvasNode = React.memo(function CanvasNode({
data-state={data.metadata?.status || (isActive ? "active" : isRelated ? "related" : "idle")}
style={{
background: hasImageContent || hasVideoContent ? "transparent" : theme.node.fill,
border: isComposerNode || isSelected ? "0" : `1px solid ${theme.node.edge}`,
// 固定占位但不绘制描边,避免聚焦切换时边框宽度变化造成白边跳动。
border: isComposerNode ? "0" : "1px solid transparent",
boxShadow: isComposerNode ? "none" : isSelected || hovered ? theme.node.hoverShadow : theme.node.shadow,
}}
onMouseDown={(event) => onMouseDown(event, data.id)}

View File

@@ -708,7 +708,7 @@
--dock-label-fs: var(--fs-label); /* 11px */
--dock-blur: 24px;
--dock-surface: var(--cn-spatial-surface);
--dock-border: var(--cn-spatial-toolbar-border);
--dock-border: transparent;
--dock-item-radius: var(--r-md); /* 8px */
--dock-item-radius-labeled: var(--r-lg); /* 12px */
--dock-radius-tight: var(--r-tight);
@@ -1402,7 +1402,7 @@
.canvas-topbar-cluster {
min-height: var(--space-10);
border: 1px solid var(--dock-border);
border: 0;
border-radius: var(--r-lg);
background: var(--dock-surface);
padding: var(--space-half) var(--space-2);
@@ -5480,7 +5480,7 @@
}
.aceternity-dock-danger-group {
background: color-mix(in srgb, var(--dock-command-danger) 8%, transparent);
background: transparent;
}
.aceternity-dock-command:focus-visible {
@@ -6252,6 +6252,7 @@
}
.canvas-floating-dock {
border: 0 !important;
box-shadow:
0 1px 2px rgba(15, 23, 42, 0.06),
0 10px 26px rgba(15, 23, 42, 0.08);
@@ -6264,6 +6265,7 @@
}
.canvas-floating-panel {
border: 0 !important;
box-shadow:
0 1px 2px rgba(15, 23, 42, 0.06),
0 14px 34px rgba(15, 23, 42, 0.1);
@@ -6281,6 +6283,11 @@
0 14px 34px rgba(0, 0, 0, 0.34);
}
.canvas-mode-switch-trigger {
border-color: transparent !important;
box-shadow: var(--elevation-overlay);
}
.canvas-tool-button,
.canvas-menu-item,
.canvas-node-tool-button {