refactor(canvas): 浮层体系重构 — z-index 14档+专注模式+工具栏click触发 #126 #127 #128 (#129)

按视觉层重设计 z-index(12→14 档),新增节点层/激活节点层/节点工具栏层等
语义 token,并下沉节点内部 stacking context(--node-z-content/overlay/handle)
与 dock 子面板(--dock-z-popover),消除硬编码 z 值。

新增专注模式(避让分屏可切换):use-focus-mode 管理 localStorage 偏好与小屏
(<1024px)强制避让;助手面板从 flex 改为 absolute + 垂直分槽(顶部 288px
让位活动任务面板),画布区按面板宽度动态 padding;顶栏新增 PanelRight
切换按钮并带强制态反馈。

节点工具栏由 hover+select 触发放为 click-only(选中即显),组件由
canvas-node-hover-toolbar 重命名为 canvas-node-toolbar;移除 project 与
render-model 中冗余的 toolbarNodeId 状态。

活动任务面板 z-index 由 --z-modal-overlay(110) 降至 --z-panel-floating(80),
归位浮动面板层;AGENTS.md Elevation 同步至 14 档。

Co-authored-by: sikongyue <3155157671@qq.com>
This commit is contained in:
爱笑的毛毛虫
2026-08-04 20:46:25 +08:00
committed by GitHub
parent 850375158c
commit d6a42d53a1
17 changed files with 199 additions and 90 deletions

View File

@@ -61,7 +61,7 @@
### 三层结构
- **Primitive 层**:纯值,无语义,不随主题变。包含 9 个维度:色彩(neutral 色板 + 节点类型色板 + 状态色)、间距(4px 栅格 `--space-0.5` ~ `--space-24`)、字号(10 档 `--fs-micro` ~ `--fs-display`)、圆角(10 档 `--r-xs` ~ `--r-full`)、阴影(`--shadow-xs` ~ `--shadow-2xl` + glow)、Elevation(12 档 `--z-canvas` ~ `--z-max`)、动效(duration 5 档 + easing 5 曲线 + delay 5 档 + motion-scale)、描边(4 档)、不透明度(6 档)。
- **Primitive 层**:纯值,无语义,不随主题变。包含 9 个维度:色彩(neutral 色板 + 节点类型色板 + 状态色)、间距(4px 栅格 `--space-0.5` ~ `--space-24`)、字号(10 档 `--fs-micro` ~ `--fs-display`)、圆角(10 档 `--r-xs` ~ `--r-full`)、阴影(`--shadow-xs` ~ `--shadow-2xl` + glow)、Elevation(14 档 `--z-canvas` ~ `--z-max`)、动效(duration 5 档 + easing 5 曲线 + delay 5 档 + motion-scale)、描边(4 档)、不透明度(6 档)。
- **Semantic 层**:引用 Primitive,随主题切换。包含通用语义色(`--bg`/`--fg`/`--border-semantic`)、Canvas 语义(`--cn-text`/`--cn-muted`/`--cn-stroke` 等)、Aceternity spatial 语义(`--cn-spatial-*`)、节点类型语义(`--cn-type-*`)、状态语义(`--status-*`)。
- **Component 层**:引用 Semantic,某组件专属。包含 Dock(`--dock-*`)、Node(`--node-*`)、Modal(`--modal-*`)、Panel(`--panel-*`)、Prompt 面板(`--prompt-panel-*`)、进度条(`--progress-bar-*`)、缩放控制(`--zoom-control-*`)。

View File

@@ -41,7 +41,7 @@ export function CanvasActiveTaskPanel({ tasks }: { tasks: GenerationTask[] }) {
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: -8, scale: 0.98 }}
transition={motionTransition}
className="pointer-events-none absolute right-3 top-[72px] z-[var(--z-modal-overlay)] w-[min(332px,calc(100vw-24px))]"
className="pointer-events-none absolute right-3 top-[72px] z-[var(--z-panel-floating)] w-[min(332px,calc(100vw-24px))]"
>
<LayoutGroup id="canvas-active-tasks">
<motion.section

View File

@@ -146,9 +146,12 @@ type CanvasAssistantPanelProps = {
onCollapse: () => void;
cinematicEntry?: boolean;
onCinematicEntryConsumed?: () => void;
width: number;
onWidthChange: (width: number) => void;
focusMode: boolean;
};
export function CanvasAssistantPanel({ nodes, selectedNodeIds, snapshot, projectId, sessions, activeSessionId, onSelectNodeIds, onSessionsChange, onApplyOps, canUndoOps, undoOpsCount, onUndoOps, onPasteImage, agentMode, onAgentModeChange, autoConnectLocal, closing, onCollapse, cinematicEntry = false, onCinematicEntryConsumed }: CanvasAssistantPanelProps) {
export function CanvasAssistantPanel({ nodes, selectedNodeIds, snapshot, projectId, sessions, activeSessionId, onSelectNodeIds, onSessionsChange, onApplyOps, canUndoOps, undoOpsCount, onUndoOps, onPasteImage, agentMode, onAgentModeChange, autoConnectLocal, closing, onCollapse, cinematicEntry = false, onCinematicEntryConsumed, width, onWidthChange, focusMode }: CanvasAssistantPanelProps) {
const theme = canvasThemes[useThemeStore((state) => state.theme)];
const user = useUserStore((state) => state.user);
const effectiveConfig = useEffectiveConfig();
@@ -157,7 +160,6 @@ export function CanvasAssistantPanel({ nodes, selectedNodeIds, snapshot, project
const updateConfig = useConfigStore((state) => state.updateConfig);
const confirmTools = useCanvasAgentStore((state) => state.confirmTools);
const setAgentState = useCanvasAgentStore((state) => state.setAgentState);
const [width, setWidth] = useState(520);
const [view, setView] = useState<OnlineAgentTab>("chat");
const [prompt, setPrompt] = useState("");
const [cinematicEntryActive, setCinematicEntryActive] = useState(cinematicEntry);
@@ -683,7 +685,7 @@ export function CanvasAssistantPanel({ nodes, selectedNodeIds, snapshot, project
};
const startResize = () => {
const move = (event: MouseEvent) => setWidth(Math.min(760, Math.max(320, window.innerWidth - event.clientX)));
const move = (event: MouseEvent) => onWidthChange(Math.min(760, Math.max(320, window.innerWidth - event.clientX)));
const stop = () => {
setResizing(false);
document.body.style.cursor = "";
@@ -832,22 +834,42 @@ export function CanvasAssistantPanel({ nodes, selectedNodeIds, snapshot, project
</>
);
// 垂直分槽:顶部 72px(顶栏)+ 200px(活动任务最大预估高)+ 16px(间距)= 288px,留给活动任务面板
const PANEL_SLOT_TOP_OFFSET = 288;
const panelOffsetRight = focusMode ? 0 : 12;
const panelOffsetBottom = focusMode ? 0 : 16;
return (
<motion.div
className="flex shrink-0"
initial={{ width: 0, opacity: 0 }}
animate={{ width: closing ? 0 : width + 9, opacity: closing ? 0 : 1 }}
className="pointer-events-none absolute z-[var(--z-panel-floating)]"
initial={{ opacity: 0 }}
animate={{ opacity: closing ? 0 : 1 }}
exit={{ opacity: 0 }}
transition={{ duration: resizing ? 0 : PANEL_MOTION_SECONDS, ease: [0.22, 1, 0.36, 1] }}
style={{ overflow: "clip", pointerEvents: closing ? "none" : undefined }}
style={{
top: PANEL_SLOT_TOP_OFFSET,
right: panelOffsetRight,
bottom: panelOffsetBottom,
width: closing ? 0 : width,
overflow: "clip",
pointerEvents: closing ? "none" : undefined,
}}
>
<motion.aside
className="relative my-2 mr-2 flex shrink-0 flex-col overflow-hidden rounded-lg"
initial={{ x: 48 }}
animate={{ x: closing ? 28 : 0 }}
className="relative flex shrink-0 flex-col overflow-hidden rounded-[var(--panel-radius)] border"
initial={{ x: 48, opacity: 0 }}
animate={{ x: closing ? 28 : 0, opacity: closing ? 0 : 1 }}
transition={{ duration: resizing ? 0 : PANEL_MOTION_SECONDS, ease: [0.22, 1, 0.36, 1] }}
style={{ width, background: theme.spatial.elevated, color: theme.node.text, boxShadow: `0 24px 72px ${theme.spatial.shadow}` }}
style={{
width,
height: "100%",
borderColor: theme.toolbar.border,
background: theme.spatial.elevated,
color: theme.node.text,
boxShadow: focusMode ? "none" : `0 24px 72px ${theme.spatial.shadow}`,
}}
>
<button type="button" className="absolute inset-y-0 left-0 z-40 w-4 -translate-x-1/2 cursor-col-resize" onMouseDown={startResize} aria-label="调整右侧面板宽度" />
<button type="button" className="absolute inset-y-0 left-0 z-[var(--node-z-overlay)] w-4 -translate-x-1/2 cursor-col-resize" onMouseDown={startResize} aria-label="调整右侧面板宽度" />
<AgentPanelChrome
theme={theme}
mode={agentMode}

View File

@@ -125,7 +125,7 @@ export function Minimap({ nodes, viewport, viewportSize, canvasContainerRef, onV
};
return (
<div className="absolute bottom-24 left-6 z-50 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-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
ref={containerRef}
className="relative h-full w-full cursor-crosshair"

View File

@@ -16,7 +16,7 @@ import { CanvasNodeType, type CanvasNodeData, type CanvasNodeMetadata, type Canv
import { ImageToolSettingsModal } from "./canvas-image-toolbar-settings-modal";
import { IMAGE_QUICK_TOOLS_STORAGE_KEY, buildImageToolbarTools, defaultImageQuickToolIds, isImageQuickToolId, readImageQuickToolsConfig, type ImageQuickToolId } from "./canvas-image-toolbar-tools";
type CanvasNodeHoverToolbarProps = {
type CanvasNodeToolbarProps = {
node: CanvasNodeData | null;
viewport: ViewportTransform;
containerRef: RefObject<HTMLDivElement | null>;
@@ -77,7 +77,7 @@ type ToolbarTool = {
const MAX_IMAGE_QUICK_TOOLS = 7;
const NODE_DOCK_LABELS_STORAGE_KEY = "canvas-node-dock-show-labels-v1";
export function CanvasNodeHoverToolbar({
export function CanvasNodeToolbar({
node,
viewport,
containerRef,
@@ -110,7 +110,7 @@ export function CanvasNodeHoverToolbar({
onToggleLocked,
onDelete,
workspaceMode = "professional",
}: CanvasNodeHoverToolbarProps) {
}: CanvasNodeToolbarProps) {
const [quickImageToolIds, setQuickImageToolIds] = useState<ImageQuickToolId[]>(defaultImageQuickToolIds);
const [draftImageToolIds, setDraftImageToolIds] = useState<ImageQuickToolId[]>(defaultImageQuickToolIds);
const [showDockLabels, setShowDockLabels] = useState(true);
@@ -342,7 +342,7 @@ export function CanvasNodeHoverToolbar({
<>
<div
ref={toolbarRef}
className="canvas-node-toolbar absolute z-[var(--z-panel-floating)] flex -translate-x-1/2 -translate-y-full items-end justify-center overflow-visible"
className="canvas-node-toolbar absolute z-[var(--z-node-toolbar)] flex -translate-x-1/2 -translate-y-full items-end justify-center overflow-visible"
style={{ left: anchor.left, top: anchor.top, width: "max-content", maxWidth: `min(calc(100% - 20px), ${showDockLabels ? 840 : 560}px)`, color: theme.node.text }}
onMouseEnter={() => onKeep(node.id)}
onMouseLeave={() => {

View File

@@ -288,7 +288,7 @@ export const CanvasNode = React.memo(function CanvasNode({
return (
<div
data-node-id={data.id}
className={`node-element absolute flex select-none flex-col ${dragOffset ? "cursor-grabbing" : data.type === CanvasNodeType.Drawing ? "cursor-pointer" : "cursor-default"} ${isSelected ? "z-50" : "z-10"}`}
className={`node-element absolute flex select-none flex-col ${dragOffset ? "cursor-grabbing" : data.type === CanvasNodeType.Drawing ? "cursor-pointer" : "cursor-default"} ${isSelected ? "z-[var(--z-node-active)]" : "z-[var(--z-node)]"}`}
style={{
transform: `translate(${data.position.x + (dragOffset?.x || 0)}px, ${data.position.y + (dragOffset?.y || 0)}px)`,
width: data.width,
@@ -416,14 +416,14 @@ export const CanvasNode = React.memo(function CanvasNode({
{flushMediaContent ? (
<div
aria-hidden
className="pointer-events-none absolute inset-0 z-30 rounded-[inherit]"
className="pointer-events-none absolute inset-0 z-[var(--node-z-content)] rounded-[inherit]"
style={{ boxShadow: `inset 0 0 0 1px ${mediaBorderColor}` }}
/>
) : null}
{(hasImageContent || hasVideoContent) && !readOnly ? (
<div
className={`absolute bottom-[10%] left-1/2 z-40 -translate-x-1/2 motion-safe:transition motion-safe:duration-200 ${hovered || isSelected ? "translate-y-0 opacity-100" : "pointer-events-none translate-y-3 opacity-0"}`}
className={`absolute bottom-[10%] left-1/2 z-[var(--node-z-overlay)] -translate-x-1/2 motion-safe:transition motion-safe:duration-200 ${isSelected ? "translate-y-0 opacity-100" : "pointer-events-none translate-y-3 opacity-0"}`}
onMouseDown={(event) => event.stopPropagation()}
onPointerDown={(event) => event.stopPropagation()}
>
@@ -442,7 +442,7 @@ export const CanvasNode = React.memo(function CanvasNode({
{data.type === CanvasNodeType.Text && data.metadata?.workflowKind !== "character" && !readOnly ? (
<div
className={`absolute bottom-[10%] left-1/2 z-40 -translate-x-1/2 motion-safe:transition motion-safe:duration-200 ${hovered || isSelected ? "translate-y-0 opacity-100" : "pointer-events-none translate-y-3 opacity-0"}`}
className={`absolute bottom-[10%] left-1/2 z-[var(--node-z-overlay)] -translate-x-1/2 motion-safe:transition motion-safe:duration-200 ${isSelected ? "translate-y-0 opacity-100" : "pointer-events-none translate-y-3 opacity-0"}`}
onMouseDown={(event) => event.stopPropagation()}
onPointerDown={(event) => event.stopPropagation()}
>
@@ -462,7 +462,7 @@ export const CanvasNode = React.memo(function CanvasNode({
{data.metadata?.versionLabel ? (
<button
type="button"
className="absolute left-3 top-3 z-40 inline-flex h-7 items-center gap-1 rounded-md border px-2 text-[var(--fs-tiny)] font-semibold backdrop-blur transition hover:brightness-110"
className="absolute left-3 top-3 z-[var(--node-z-overlay)] inline-flex h-7 items-center gap-1 rounded-md border px-2 text-[var(--fs-tiny)] font-semibold backdrop-blur transition hover:brightness-110"
style={{ background: theme.toolbar.panel, borderColor: data.metadata.versionPrimary ? theme.node.activeStroke : theme.toolbar.border, color: data.metadata.versionPrimary ? theme.node.activeStroke : theme.node.text }}
title="查看版本对比"
onMouseDown={(event) => event.stopPropagation()}
@@ -472,7 +472,7 @@ export const CanvasNode = React.memo(function CanvasNode({
</button>
) : null}
{showStatusTrack ? (
<div className={`absolute right-3 top-3 z-40 flex min-w-0 items-center justify-end gap-1 ${data.metadata?.versionLabel ? "max-w-[calc(100%-104px)]" : "max-w-[calc(100%-24px)]"}`}>
<div className={`absolute right-3 top-3 z-[var(--node-z-overlay)] flex min-w-0 items-center justify-end gap-1 ${data.metadata?.versionLabel ? "max-w-[calc(100%-104px)]" : "max-w-[calc(100%-24px)]"}`}>
{resourceLabel ? <ResourceLabelBadge reference={resourceLabel} theme={theme} /> : null}
{hasMediaContent && !readOnly ? <ResourceStorageBadge storageKey={data.metadata?.storageKey} active={isActive} theme={theme} /> : null}
{isBatchRoot ? <BatchToggleBadge count={batchCount} expanded={batchExpanded} theme={theme} onToggle={() => onToggleBatch?.(data.id)} /> : null}
@@ -481,7 +481,7 @@ export const CanvasNode = React.memo(function CanvasNode({
</div>
) : null}
{assetTags.length || (showImageInfo && hasImageContent) ? (
<div className="pointer-events-none absolute inset-x-3 bottom-3 z-40 flex items-end justify-between gap-2">
<div className="pointer-events-none absolute inset-x-3 bottom-3 z-[var(--node-z-overlay)] flex items-end justify-between gap-2">
{assetTags.length ? <AssetTagBadges tags={assetTags} theme={theme} /> : null}
{showImageInfo && hasImageContent ? <ImageInfoBar node={data} /> : null}
</div>
@@ -1128,7 +1128,7 @@ function ResizeHandle({ corner, onMouseDown }: { corner: ResizeCorner; onMouseDo
"bottom-right": "-bottom-[14px] -right-[14px] cursor-nwse-resize",
}[corner];
return <div className={`absolute z-50 size-7 ${positionClass}`} onMouseDown={(event) => onMouseDown(event, corner)} />;
return <div className={`absolute z-[var(--node-z-handle)] size-7 ${positionClass}`} onMouseDown={(event) => onMouseDown(event, corner)} />;
}
const NODE_EXTERNAL_HEADER_MIN_SCALE = 0.35;
@@ -1154,7 +1154,7 @@ function NodeExternalHeader({ node, scale, active, editable, editing, draft, the
return (
<div
className="canvas-node-external-header absolute bottom-full left-0 z-40 flex h-6 items-center gap-1 overflow-hidden"
className="canvas-node-external-header absolute bottom-full left-0 z-[var(--node-z-overlay)] flex h-6 items-center gap-1 overflow-hidden"
style={{ maxWidth: maxHeaderWidth, color: active ? theme.node.text : theme.node.label, transform: `scale(var(--canvas-live-inverse-scale, ${inverseScale}))`, transformOrigin: "left bottom" }}
onMouseDown={(event) => event.stopPropagation()}
onPointerDown={(event) => event.stopPropagation()}
@@ -1264,7 +1264,7 @@ function ConnectionSideRail({ side, scale, visible, theme, onPointerDown }: { si
return (
<button
type="button"
className={`group absolute top-1/2 z-30 touch-none -translate-y-1/2 outline-none transition-opacity duration-150 ${visible ? "pointer-events-auto opacity-100" : "pointer-events-none opacity-0"}`}
className={`group absolute top-1/2 z-[var(--node-z-overlay)] touch-none -translate-y-1/2 outline-none transition-opacity duration-150 ${visible ? "pointer-events-auto opacity-100" : "pointer-events-none opacity-0"}`}
style={{ width: 56 * inverseScale, height: `min(100%, ${72 * inverseScale}px)`, ...(side === "left" ? { right: "100%" } : { left: "100%" }) }}
onPointerEnter={updateAnchor}
onPointerMove={updateAnchor}

View File

@@ -492,7 +492,7 @@ function RowHandle({ side, top, scale, tone, theme, title, onPointerDown }: { si
type="button"
aria-label={title || `${side === "left" ? "输入" : "输出"}连接点`}
title={title || `${side === "left" ? "引入参考" : "连接到图片、视频或生成节点"}`}
className={`canvas-connection-handle absolute z-50 flex -translate-y-1/2 cursor-pointer items-center justify-center rounded-full outline-none focus-visible:ring-2 ${side === "left" ? "left-0 -translate-x-1/2" : "right-0 translate-x-1/2"}`}
className={`canvas-connection-handle absolute z-[var(--node-z-handle)] flex -translate-y-1/2 cursor-pointer items-center justify-center rounded-full outline-none focus-visible:ring-2 ${side === "left" ? "left-0 -translate-x-1/2" : "right-0 translate-x-1/2"}`}
style={{ top, width: 32 * inverseHitScale, height: 32 * inverseHitScale, "--tw-ring-color": theme.accent.primary } as CSSProperties}
onPointerDown={onPointerDown}
>

View File

@@ -130,13 +130,13 @@ export function CanvasShortDramaGuide({ progress, collapsed, onToggle, onSkip, o
if (!progress.active) return null;
if (collapsed) {
return (
<button type="button" data-canvas-no-zoom className="absolute left-[calc(50%+64px)] top-2 z-[var(--z-toolbar-floating)] inline-flex h-8 max-w-[calc(50vw-76px)] min-w-0 items-center gap-2 rounded-lg border px-3 text-xs font-medium shadow-sm backdrop-blur outline-none focus-visible:ring-2" style={{ background: theme.toolbar.panel, borderColor: theme.toolbar.border, color: theme.node.text, "--tw-ring-color": theme.accent.primary } as CSSProperties} onClick={onToggle}>
<button type="button" data-canvas-no-zoom className="absolute left-[calc(50%+64px)] top-2 z-[var(--z-toolbar)] inline-flex h-8 max-w-[calc(50vw-76px)] min-w-0 items-center gap-2 rounded-lg border px-3 text-xs font-medium shadow-sm backdrop-blur outline-none focus-visible:ring-2" style={{ background: theme.toolbar.panel, borderColor: theme.toolbar.border, color: theme.node.text, "--tw-ring-color": theme.accent.primary } as CSSProperties} onClick={onToggle}>
<Clapperboard className="size-3.5 shrink-0" /><span className="truncate">短剧流程 {progress.completedCount}/5</span><ChevronDown className="size-3 shrink-0" />
</button>
);
}
return (
<div data-canvas-no-zoom className="absolute left-1/2 top-[68px] z-[var(--z-toolbar-floating)] flex max-w-[calc(100%_-_24px)] -translate-x-1/2 items-center gap-1 rounded-lg border p-1 shadow-sm backdrop-blur" style={{ background: theme.toolbar.panel, borderColor: theme.toolbar.border, color: theme.node.text }}>
<div data-canvas-no-zoom className="absolute left-1/2 top-[68px] z-[var(--z-toolbar)] flex max-w-[calc(100%_-_24px)] -translate-x-1/2 items-center gap-1 rounded-lg border p-1 shadow-sm backdrop-blur" style={{ background: theme.toolbar.panel, borderColor: theme.toolbar.border, color: theme.node.text }}>
<div className="hide-scrollbar flex max-w-[min(760px,calc(100vw-150px))] items-center overflow-x-auto">
{progress.steps.map((step, index) => (
<span key={step.id} className="flex shrink-0 items-center">

View File

@@ -175,7 +175,7 @@ export function CanvasToolbar({
const resourceCommands = addNodeCommands.filter((cmd) => cmd.section === "resource").map(toCommand);
return (
<div ref={rootRef} data-canvas-no-zoom className="pointer-events-none absolute inset-x-4 bottom-4 z-50 flex justify-center">
<div ref={rootRef} data-canvas-no-zoom className="pointer-events-none absolute inset-x-4 bottom-4 z-[var(--z-toolbar)] flex justify-center">
<AnimatePresence>
{addOpen ? (
<AddNodeMenu
@@ -191,7 +191,7 @@ export function CanvasToolbar({
<AnimatePresence>
{appearanceOpen ? (
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} transition={{ duration: aceternityMotion.duration.instant }} className="pointer-events-auto absolute bottom-[50px] z-30 w-[224px] max-w-[calc(100vw-24px)] -translate-x-1/2" style={{ left: panelX || "50%" }}>
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} transition={{ duration: aceternityMotion.duration.instant }} className="pointer-events-auto absolute bottom-[50px] z-[var(--dock-z-popover)] w-[224px] max-w-[calc(100vw-24px)] -translate-x-1/2" style={{ left: panelX || "50%" }}>
<SpotlightSurface spotlightColor={theme.toolbar.itemHover} initial={{ y: 6, scale: 0.97 }} animate={{ y: 0, scale: 1 }} exit={{ y: 4, scale: 0.98 }} transition={{ duration: aceternityMotion.duration.instant, ease: aceternityMotion.easing.enter }} className="aceternity-floating-panel overflow-hidden rounded-[var(--panel-radius)] border p-2.5 backdrop-blur-2xl" style={{ background: theme.spatial.elevated, borderColor: theme.toolbar.border, color: theme.toolbar.item, boxShadow: `0 24px 64px ${theme.spatial.shadow}` }} onWheel={(event) => event.stopPropagation()}>
<PanelHeading icon={<Palette className="size-4" />} title="画布外观" subtitle="调整整个创作空间" theme={theme} />
<div className="mt-3 text-[var(--fs-micro)] font-semibold uppercase opacity-45">主题模式</div>
@@ -231,7 +231,7 @@ function AddNodeMenu({ x, theme, nodeCommands, resourceCommands }: {
resourceCommands: CanvasCreateCommand[];
}) {
return (
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} transition={{ duration: aceternityMotion.duration.instant }} className="pointer-events-auto absolute bottom-[50px] z-40 w-[260px] max-w-[calc(100vw-24px)] -translate-x-1/2" style={{ left: x || "50%" }}>
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} transition={{ duration: aceternityMotion.duration.instant }} className="pointer-events-auto absolute bottom-[50px] z-[var(--dock-z-popover)] w-[260px] max-w-[calc(100vw-24px)] -translate-x-1/2" style={{ left: x || "50%" }}>
<SpotlightSurface spotlightColor={theme.toolbar.itemHover} initial={{ y: 6, scale: 0.97 }} animate={{ y: 0, scale: 1 }} exit={{ y: 4, scale: 0.98 }} transition={{ duration: aceternityMotion.duration.instant, ease: aceternityMotion.easing.enter }} className="aceternity-floating-panel overflow-hidden rounded-[var(--panel-radius)] border p-2 backdrop-blur-2xl" style={{ background: theme.spatial.elevated, borderColor: theme.toolbar.border, color: theme.node.text, boxShadow: `0 24px 64px ${theme.spatial.shadow}` }} onWheel={(event) => event.stopPropagation()}>
<PanelHeading icon={<Plus className="size-4" />} title="创建内容" subtitle="选择节点类型" theme={theme} />
<MenuSection title="创作节点" />

View File

@@ -86,7 +86,7 @@ export function CanvasZoomControls({ scale, onScaleChange, onReset, isMiniMapOpe
];
return (
<div ref={rootRef} data-canvas-no-zoom className="relative z-50" onMouseDown={(event) => event.stopPropagation()} onPointerDown={(event) => event.stopPropagation()} onWheel={(event) => event.stopPropagation()}>
<div ref={rootRef} data-canvas-no-zoom className="relative z-[var(--z-toolbar)]" onMouseDown={(event) => event.stopPropagation()} onPointerDown={(event) => event.stopPropagation()} onWheel={(event) => event.stopPropagation()}>
<AnimatePresence>
{precisionOpen ? (
<motion.div

View File

@@ -0,0 +1,45 @@
import { useCallback, useEffect, useState } from "react";
const FOCUS_MODE_KEY = "canvas-focus-mode-v1";
const FORCED_FOCUS_BREAKPOINT = 1024;
function readInitialPreference(): boolean {
if (typeof window === "undefined") return true;
const stored = window.localStorage.getItem(FOCUS_MODE_KEY);
if (stored === "1") return true;
if (stored === "0") return false;
return true;
}
export function useFocusMode() {
const [userPreference, setUserPreference] = useState<boolean>(readInitialPreference);
const [viewportWidth, setViewportWidth] = useState<number>(() => (typeof window !== "undefined" ? window.innerWidth : 1024));
useEffect(() => {
if (typeof window === "undefined") return;
const handleResize = () => setViewportWidth(window.innerWidth);
handleResize();
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);
const forcedOn = viewportWidth < FORCED_FOCUS_BREAKPOINT;
const focusMode = forcedOn || userPreference;
const toggleFocusMode = useCallback(() => {
setUserPreference((prev) => {
const next = !prev;
if (typeof window !== "undefined") {
window.localStorage.setItem(FOCUS_MODE_KEY, next ? "1" : "0");
}
return next;
});
}, []);
return {
focusMode,
userPreference,
forcedOn,
toggleFocusMode,
};
}

View File

@@ -1,7 +1,7 @@
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import { useEffect, useRef, useState, type CSSProperties, type ReactNode } from "react";
import { Link } from "react-router";
import { Bot, Check, ChevronDown, Coins, FolderKanban, Gauge, Home, LayoutGrid, LoaderCircle, Menu, Pencil, Plus, Redo2, Search, Settings2, Share2, Sparkles, Trash2, Undo2, Upload } from "lucide-react";
import { Bot, Check, ChevronDown, Coins, FolderKanban, Gauge, Home, LayoutGrid, LoaderCircle, Menu, Pencil, PanelRightClose, PanelRightOpen, Plus, Redo2, Search, Settings2, Share2, Sparkles, Trash2, Undo2, Upload } from "lucide-react";
import { Button, Dropdown, Modal, Tooltip } from "antd";
import { useWalletBalance } from "@/hooks/use-wallet-balance";
@@ -38,6 +38,9 @@ type CanvasTopBarProps = {
onMediaPerformanceModeChange: (mode: CanvasMediaPerformanceMode) => void;
onOpenSearch: () => void;
projectContext?: CanvasContextSummary & { projectId: string; projectName: string };
focusMode: boolean;
focusModeForced: boolean;
onToggleFocusMode: () => void;
};
export function CanvasTopBar({
@@ -66,6 +69,9 @@ export function CanvasTopBar({
onMediaPerformanceModeChange,
onOpenSearch,
projectContext,
focusMode,
focusModeForced,
onToggleFocusMode,
}: CanvasTopBarProps) {
const theme = canvasThemes[useThemeStore((state) => state.theme)];
const user = useUserStore((state) => state.user);
@@ -89,7 +95,7 @@ export function CanvasTopBar({
return (
<>
<div className="pointer-events-none absolute left-0 right-0 top-0 z-50 flex h-16 items-center justify-between px-4">
<div className="pointer-events-none absolute left-0 right-0 top-0 z-[var(--z-toolbar)] flex h-16 items-center justify-between px-4">
<div className="pointer-events-auto flex min-w-0 items-center gap-3">
<Dropdown
trigger={["click"]}
@@ -200,6 +206,22 @@ export function CanvasTopBar({
<span>{availableMicrocredits === null ? "--" : (availableMicrocredits / 1_000_000).toLocaleString("zh-CN", { maximumFractionDigits: 3 })}</span>
</Link>
) : null}
<Tooltip title={focusModeForced ? "小屏强制避让模式" : focusMode ? "专注模式:已避让(点击切换浮层)" : "专注模式:已浮层(点击切换避让)"}>
<Button
type="text"
className="!h-10 !w-10 !min-w-10 !rounded-xl !p-0"
style={{
color: theme.node.text,
opacity: focusModeForced ? 0.45 : 1,
background: focusMode ? theme.toolbar.panel : undefined,
}}
icon={focusMode ? <PanelRightClose className="size-4" /> : <PanelRightOpen className="size-4" />}
onClick={onToggleFocusMode}
disabled={focusModeForced}
aria-pressed={focusMode}
aria-label="专注模式:画布与侧面板避让/浮层切换"
/>
</Tooltip>
<Button type="text" className="!h-10 !w-10 !min-w-10 !rounded-xl !p-0" style={{ color: theme.node.text }} icon={<Share2 className="size-4" />} onClick={onShare} aria-label="分享画布" title="分享画布" />
<span className="h-6 w-px" style={{ background: theme.toolbar.border }} />
<Button
@@ -269,7 +291,7 @@ function CanvasWorkspaceModeSwitch({ mode, onChange }: { mode: CanvasWorkspaceMo
};
return (
<div ref={rootRef} className="aceternity-mode-switch pointer-events-auto absolute left-1/2 top-2 z-30 -translate-x-1/2">
<div ref={rootRef} className="aceternity-mode-switch pointer-events-auto absolute left-1/2 top-2 z-[var(--dock-z-popover)] -translate-x-1/2">
<motion.button
type="button"
whileHover={reducedMotion ? undefined : { y: -1, scale: 1.015 }}

View File

@@ -161,7 +161,7 @@ export function CanvasProjectWorldLayers(props: CanvasProjectWorldLayersProps) {
{props.selectedNodeBounds && !props.selectionBox && !props.isNodeDragging ? (
<div
ref={props.selectionBoundsElementRef}
className="pointer-events-none absolute z-[65] rounded-xl"
className="pointer-events-none absolute z-[var(--z-panel-floating)] rounded-xl"
style={{
left: props.selectedNodeBounds.left - 12 / viewportScale,
top: props.selectedNodeBounds.top - 12 / viewportScale,

View File

@@ -27,7 +27,7 @@ import { CanvasCharacterReferenceNodeContent } from "@/components/canvas/canvas-
import { CanvasCharacterReferenceModal } from "@/components/canvas/canvas-character-reference-modal";
import { WorkspaceState } from "@/components/layout/workspace-state";
import { resolveCanvasStylePreset } from "@/components/canvas/canvas-style-picker-modal";
import { CanvasNodeHoverToolbar, CanvasNodeInfoModal } from "@/components/canvas/canvas-node-hover-toolbar";
import { CanvasNodeToolbar, CanvasNodeInfoModal } from "@/components/canvas/canvas-node-toolbar";
import { CanvasNodeAnglePanel } from "@/components/canvas/canvas-node-angle-dialog";
import { CanvasTextEditorModal } from "@/components/canvas/canvas-text-editor-modal";
import { CanvasNodeSearchModal } from "@/components/canvas/canvas-node-search-modal";
@@ -45,6 +45,7 @@ import { CanvasScriptEditor, CanvasScriptNodeContent, STORYBOARD_HEADER_HEIGHT,
import { CanvasDirectorNodePanel } from "@/components/canvas/director/canvas-director-node-panel";
import { CanvasVersionCompareModal } from "@/components/canvas/canvas-version-compare-modal";
import { CanvasLocalAgentPanel } from "@/components/canvas/canvas-local-agent-panel";
import { useFocusMode } from "@/hooks/use-focus-mode";
import { useCanvasAgentStore } from "@/stores/canvas/use-canvas-agent-store";
import type { CanvasResourceReference } from "@/lib/canvas/canvas-resource-references";
import { CanvasConnectionCreateMenu, CanvasNodePanelOverlay } from "@/components/canvas/canvas-workspace-overlays";
@@ -151,20 +152,20 @@ function CanvasRefreshShell() {
}}
/>
<div className="absolute bottom-5 left-1/2 z-50 flex h-14 -translate-x-1/2 items-center gap-1 rounded-xl border px-2 shadow-lg backdrop-blur" style={{ background: "var(--background)", borderColor: "var(--border)" }} aria-hidden="true">
<div className="absolute bottom-5 left-1/2 z-[var(--z-toolbar)] flex h-14 -translate-x-1/2 items-center gap-1 rounded-xl border px-2 shadow-lg backdrop-blur" style={{ background: "var(--background)", borderColor: "var(--border)" }} aria-hidden="true">
{Array.from({ length: 7 }).map((_, index) => (
<div key={index} className="size-8 rounded-md bg-current opacity-10" />
))}
</div>
<div className="absolute bottom-24 left-6 z-50 h-40 w-[240px] rounded-lg border shadow-2xl backdrop-blur-sm" style={{ background: "var(--background)", borderColor: "var(--border)" }} aria-hidden="true">
<div className="absolute bottom-24 left-6 z-[var(--z-toolbar)] h-40 w-[240px] rounded-lg border shadow-2xl backdrop-blur-sm" style={{ background: "var(--background)", borderColor: "var(--border)" }} aria-hidden="true">
<div className="absolute left-7 top-7 h-5 w-12 rounded-sm bg-current opacity-10" />
<div className="absolute left-28 top-16 h-6 w-16 rounded-sm bg-current opacity-10" />
<div className="absolute bottom-7 left-16 h-8 w-20 rounded-sm bg-current opacity-10" />
<div className="absolute inset-5 rounded border border-current opacity-15" />
</div>
<div className="absolute bottom-5 left-5 z-50 flex h-14 w-[260px] items-center gap-2 rounded-xl border px-2 shadow-lg backdrop-blur" style={{ background: "var(--background)", borderColor: "var(--border)" }} aria-hidden="true">
<div className="absolute bottom-5 left-5 z-[var(--z-toolbar)] flex h-14 w-[260px] items-center gap-2 rounded-xl border px-2 shadow-lg backdrop-blur" style={{ background: "var(--background)", borderColor: "var(--border)" }} aria-hidden="true">
<div className="size-8 rounded-md bg-current opacity-10" />
<div className="size-8 rounded-md bg-current opacity-10" />
<div className="h-1 flex-1 rounded-full bg-current opacity-10" />
@@ -238,8 +239,12 @@ function InfiniteCanvasPage() {
const [titleDraft, setTitleDraft] = useState("");
const [shortcutRequestNonce, setShortcutRequestNonce] = useState(0);
const [cinematicAgentEntry, setCinematicAgentEntry] = useState(false);
const [assistantWidth, setAssistantWidth] = useState(520);
const { agentMode, assistantClosing, assistantMounted, assistantOpen, closeAgent, openAgent, setAgentMode } = useCanvasAssistantVisibility();
const { tasks: activeTasks } = useCanvasActiveTasks(projectId, projectLoaded);
const { focusMode, forcedOn, toggleFocusMode } = useFocusMode();
const anyPanelOpen = assistantMounted && !assistantClosing;
const panelColumnWidth = anyPanelOpen ? Math.max(assistantWidth, 332) : 0;
useEffect(() => {
persistCanvasWorkspaceMode(workspaceMode);
@@ -805,7 +810,6 @@ function InfiniteCanvasPage() {
collapsingBatchIds,
addedSkills,
directorScenes: currentProject?.directorScenes,
toolbarNodeId,
infoNodeId,
cropNodeId,
maskEditNodeId,
@@ -1378,7 +1382,12 @@ function InfiniteCanvasPage() {
<a href="#canvas-main" className="sr-only focus:not-sr-only focus:absolute focus:left-4 focus:top-4 focus:z-[var(--z-toast)] focus:rounded-md focus:border focus:bg-background focus:px-4 focus:py-2 focus:text-sm focus:font-medium focus:shadow-lg">跳转到画布主内容</a>
<main id="canvas-main" tabIndex={-1} className="flex h-full min-h-0 overflow-hidden outline-none" style={{ background: theme.canvas.background, color: theme.node.text }}>
{shortDramaEnabled && currentProject?.projectId ? <CanvasProjectSidebar projectId={currentProject.projectId} detail={linkedProjectQuery.data} onAddChapter={handleProjectChapterInsert} onLocateStyle={locateProjectStyleNode} onOpenAssets={() => openProjectAssets()} /> : null}
<section className="relative min-w-0 flex-1 overflow-hidden">
<section
className="relative min-w-0 flex-1 overflow-hidden transition-[padding]"
style={{
paddingRight: focusMode && anyPanelOpen ? panelColumnWidth : undefined,
}}
>
<CanvasTopBar
title={currentProject?.title || "未命名画布"}
workspaceMode={workspaceMode}
@@ -1409,6 +1418,9 @@ function InfiniteCanvasPage() {
projectId: currentProject.projectId,
projectName: linkedProjectQuery.data?.project.name || currentProject.title,
} : undefined}
focusMode={focusMode}
focusModeForced={forcedOn}
onToggleFocusMode={toggleFocusMode}
/>
<CanvasNodeSearchModal
@@ -1564,7 +1576,7 @@ function InfiniteCanvasPage() {
{mergeVideoProgress ? <CanvasMergeStatusToast progress={mergeVideoProgress} theme={theme} /> : null}
{lastAgentChange ? <CanvasAgentChangeToast change={lastAgentChange} theme={theme} onView={viewLastAgentChange} onUndo={() => { undoAgentOps(); }} onClose={dismissLastAgentChange} /> : null}
<CanvasNodeHoverToolbar
<CanvasNodeToolbar
node={isNodeDragging || nodeImageSettingsOpen || emotionNodeId ? null : toolbarNode}
workspaceMode={workspaceMode}
viewport={viewport}
@@ -1777,31 +1789,34 @@ function InfiniteCanvasPage() {
/>
<CanvasProjectAssetModal open={projectAssetOpen} detail={linkedProjectQuery.data} initialCategory={projectAssetInitialCategory} onClose={closeProjectAssets} onInsert={(payloads) => handleProjectAssetsInsert(payloads, projectAssetInsertPosition)} />
{codexCompactAgent && !assistantMounted ? <CanvasLocalAgentPanel headless snapshot={agentSnapshot} canUndoOps={canUndoAgentOps} undoOpsCount={agentUndoCount} onApplyOps={applyAgentOps} onUndoOps={undoAgentOps} autoConnect={codexAutoConnect} /> : null}
{assistantMounted ? (
<CanvasAssistantPanel
nodes={nodes}
selectedNodeIds={selectedNodeIds}
snapshot={agentSnapshot}
projectId={projectId}
sessions={chatSessions}
activeSessionId={activeChatId}
onSelectNodeIds={setSelectedNodeIds}
onSessionsChange={handleAssistantSessionsChange}
onApplyOps={applyAgentOps}
canUndoOps={canUndoAgentOps}
undoOpsCount={agentUndoCount}
onUndoOps={undoAgentOps}
onPasteImage={pasteAssistantImage}
agentMode={agentMode}
onAgentModeChange={setAgentMode}
autoConnectLocal={codexAutoConnect}
closing={assistantClosing}
onCollapse={closeAgent}
cinematicEntry={cinematicAgentEntry}
onCinematicEntryConsumed={() => setCinematicAgentEntry(false)}
width={assistantWidth}
onWidthChange={setAssistantWidth}
focusMode={focusMode}
/>
) : null}
</section>
{assistantMounted ? (
<CanvasAssistantPanel
nodes={nodes}
selectedNodeIds={selectedNodeIds}
snapshot={agentSnapshot}
projectId={projectId}
sessions={chatSessions}
activeSessionId={activeChatId}
onSelectNodeIds={setSelectedNodeIds}
onSessionsChange={handleAssistantSessionsChange}
onApplyOps={applyAgentOps}
canUndoOps={canUndoAgentOps}
undoOpsCount={agentUndoCount}
onUndoOps={undoAgentOps}
onPasteImage={pasteAssistantImage}
agentMode={agentMode}
onAgentModeChange={setAgentMode}
autoConnectLocal={codexAutoConnect}
closing={assistantClosing}
onCollapse={closeAgent}
cinematicEntry={cinematicAgentEntry}
onCinematicEntryConsumed={() => setCinematicAgentEntry(false)}
/>
) : null}
</main>
</>
);

View File

@@ -5,7 +5,7 @@ import { Link, useParams } from "react-router";
import { nanoid } from "nanoid";
import { ConnectionPath } from "@/components/canvas/canvas-connections";
import { CanvasNodeHoverToolbar, CanvasNodeInfoModal } from "@/components/canvas/canvas-node-hover-toolbar";
import { CanvasNodeToolbar, CanvasNodeInfoModal } from "@/components/canvas/canvas-node-toolbar";
import { CanvasFrameNode } from "@/components/canvas/canvas-frame-node";
import { CanvasNode } from "@/components/canvas/canvas-node";
import { CanvasZoomControls } from "@/components/canvas/canvas-zoom-controls";
@@ -215,7 +215,7 @@ export default function SharedCanvasPage() {
};
}));
const renderSharedNode = useCallback((node: CanvasNodeData): ReactNode => node.type === CanvasNodeType.Script ? <SharedScriptNode node={node} onUnauthorized={unauthorized} /> : <SharedConfigNode node={node} onUnauthorized={unauthorized} />, [unauthorized]);
const toolbarNodeKey = toolbarNodeId || selectedNodeId;
const toolbarNodeKey = selectedNodeId;
const toolbarNode = toolbarNodeKey ? nodeById.get(toolbarNodeKey) || null : null;
if (loading) return <FullScreenLoader label="正在打开共享画布" detail="读取节点、连线和视图状态" />;
@@ -256,7 +256,7 @@ export default function SharedCanvasPage() {
}} onHoverStart={keepToolbar} onHoverEnd={hideToolbar} onConnectStart={unauthorized} onResize={() => undefined} onContentChange={unauthorized} onRetry={unauthorized} onCancelTask={unauthorized} onOpenTaskDetails={unauthorized} onViewImage={(target) => setInfoNodeId(target.id)} onContextMenu={(event, nodeId) => openContextMenu(event, nodeId)} />)}
</InfiniteCanvas>
<CanvasNodeHoverToolbar node={dragRef.current ? null : toolbarNode} viewport={viewport} containerRef={containerRef} onKeep={keepToolbar} onLeave={hideToolbar} onInfo={(node) => setInfoNodeId(node.id)} onEditText={unauthorized} onDecreaseFont={unauthorized} onIncreaseFont={unauthorized} onToggleDialog={unauthorized} onAnnotate={unauthorized} onGenerateImage={unauthorized} onUpload={unauthorized} onDownload={unauthorized} onSaveAsset={unauthorized} onMaskEdit={unauthorized} onEmotion={unauthorized} onPortraitTexture={unauthorized} onCrop={unauthorized} onSplit={unauthorized} onUpscale={unauthorized} onSuperResolve={unauthorized} onAngle={unauthorized} onViewImage={unauthorized} onExtractVideoLastFrame={unauthorized} extractingVideoFrame={false} onReversePrompt={unauthorized} onRetry={unauthorized} onToggleFreeResize={unauthorized} onToggleLocked={unauthorized} onDelete={unauthorized} />
<CanvasNodeToolbar node={dragRef.current ? null : toolbarNode} viewport={viewport} containerRef={containerRef} onKeep={keepToolbar} onLeave={hideToolbar} onInfo={(node) => setInfoNodeId(node.id)} onEditText={unauthorized} onDecreaseFont={unauthorized} onIncreaseFont={unauthorized} onToggleDialog={unauthorized} onAnnotate={unauthorized} onGenerateImage={unauthorized} onUpload={unauthorized} onDownload={unauthorized} onSaveAsset={unauthorized} onMaskEdit={unauthorized} onEmotion={unauthorized} onPortraitTexture={unauthorized} onCrop={unauthorized} onSplit={unauthorized} onUpscale={unauthorized} onSuperResolve={unauthorized} onAngle={unauthorized} onViewImage={unauthorized} onExtractVideoLastFrame={unauthorized} extractingVideoFrame={false} onReversePrompt={unauthorized} onRetry={unauthorized} onToggleFreeResize={unauthorized} onToggleLocked={unauthorized} onDelete={unauthorized} />
<div className="absolute bottom-5 left-5 z-[var(--z-panel-floating)]"><CanvasZoomControls scale={viewport.k} containerRef={containerRef} onScaleChange={setZoom} onReset={resetViewport} isMiniMapOpen={false} onToggleMiniMap={unauthorized} onOpenShortcuts={unauthorized} /></div>
<div className="pointer-events-none absolute bottom-5 right-5 z-[var(--z-panel-floating)] max-w-[340px] text-right text-xs leading-5" style={{ color: theme.node.muted }}>访客操作仅在当前页面临时生效</div>

View File

@@ -26,7 +26,6 @@ type UseCanvasRenderModelOptions = {
collapsingBatchIds: Set<string>;
addedSkills: Skill[];
directorScenes?: DirectorScene[];
toolbarNodeId: string | null;
infoNodeId: string | null;
cropNodeId: string | null;
maskEditNodeId: string | null;
@@ -57,7 +56,6 @@ export function useCanvasRenderModel({
collapsingBatchIds,
addedSkills,
directorScenes,
toolbarNodeId,
infoNodeId,
cropNodeId,
maskEditNodeId,
@@ -140,7 +138,8 @@ export function useCanvasRenderModel({
return nodes.filter((node) => (node.metadata?.versionOfNodeId || node.id) === versionCompareRootId).sort((a, b) => (a.metadata?.versionLabel || "").localeCompare(b.metadata?.versionLabel || ""));
}, [nodes, versionCompareRootId]);
const toolbarCandidate = toolbarNodeId ? nodeById.get(toolbarNodeId) || null : null;
const selectedNodeIdForToolbar = selectedNodeIds.size === 1 ? [...selectedNodeIds][0] : null;
const toolbarCandidate = selectedNodeIdForToolbar ? nodeById.get(selectedNodeIdForToolbar) || null : null;
const toolbarNode = isFrameNode(toolbarCandidate) ? null : toolbarCandidate;
const infoNode = infoNodeId ? nodeById.get(infoNodeId) || null : null;
const cropNode = cropNodeId ? nodeById.get(cropNodeId) || null : null;

View File

@@ -101,19 +101,21 @@
--shadow-glow-md: 0 0 24px rgba(125, 162, 255, 0.3);
--shadow-glow-lg: 0 0 40px rgba(125, 162, 255, 0.4);
/* --- 6. Elevation(z-index 12 档,对应 #98 五层体系)--- */
--z-canvas: 0; /* L0 画布层 */
--z-canvas-overlay: 5; /* L0 画布覆盖(网格、连线)*/
--z-toolbar: 10; /* L1 浮动工具层 */
--z-toolbar-floating: 20; /* L1 悬停工具栏 */
--z-panel: 50; /* L2 侧边面板层 */
--z-panel-floating: 80; /* L2 浮动面板 */
--z-modal: 100; /* L3 对话框层 */
--z-modal-overlay: 110; /* L3 遮罩 */
--z-popover: 200; /* L3 popover */
--z-toast: 500; /* L4 通知层 */
--z-tooltip: 1000; /* L4 tooltip */
--z-max: 2000; /* 最高层(紧急通知)*/
/* --- 6. Elevation(z-index 14 档,按视觉层划分,见 ADR-001)--- */
--z-canvas: 0; /* L0 画布底 */
--z-canvas-overlay: 5; /* L1 画布覆盖(网格、连线)*/
--z-node: 10; /* L2 节点层(未选中)*/
--z-node-active: 20; /* L3 激活节点(选中/拖拽)*/
--z-toolbar: 30; /* L4 浮动工具栏(顶栏、dock、zoom)*/
--z-node-toolbar: 50; /* L5 节点工具栏(click 触发)*/
--z-panel: 60; /* L6 侧边面板(侧栏、素材托盘)*/
--z-panel-floating: 80; /* L7 浮动面板(助手/agent/活动任务)*/
--z-modal: 100; /* L8 对话框 */
--z-modal-overlay: 110; /* L9 遮罩 */
--z-popover: 200; /* L10 弹出层 */
--z-toast: 500; /* L11 通知 */
--z-tooltip: 1000; /* L12 提示 */
--z-max: 2000; /* L13 最高(紧急通知)*/
/* --- 7. 动效(对应 #99 动效 token)--- */
/* duration */
@@ -464,6 +466,7 @@
--dock-shadow: var(--shadow-lg);
--dock-item-radius: var(--r-md); /* 8px */
--dock-item-radius-labeled: var(--r-lg); /* 12px */
--dock-z-popover: 30; /* dock 子面板在 toolbar 根 stacking context 内的层级(见 ADR-001)*/
/* Node 组件(画布节点,对应 #97 节点视觉规范)*/
--node-radius: var(--r-2xl); /* 16px */
@@ -483,6 +486,9 @@
--node-badge-fs: var(--fs-tiny); /* 10px */
--node-type-strip-width: 3px; /* 色带宽度 */
--node-type-badge-size: 20px; /* 图标徽章尺寸 */
--node-z-content: 30; /* 节点遮罩层(stacking context 内,见 ADR-001)*/
--node-z-overlay: 40; /* 节点角标/按钮层 */
--node-z-handle: 50; /* 节点 resize handle 层 */
/* Modal 组件(对话框,对应 #98 面板层级)*/
--modal-radius: var(--r-3xl); /* 20px */