feat(canvas): 面板层级管理 — 五层Elevation体系+渐进式三区+z-index全量迁移 #98 (#102)

* feat(canvas): 面板层级管理优化 - 五层Elevation+渐进式三区+进度增强 #98 #94

基于 Wayfinder 地图 #98 决议执行面板层级管理优化:

1. 五层 Elevation 体系(决策1)
   - 画布组件 z-index 硬编码值全部迁移到 token
   - L0 画布层 → L1 工具层 → L2 面板层 → L3 对话框层 → L4 通知层
   - 涉及 14 个 .tsx 文件,0 处 z-[N] 硬编码残留

2. prompt 面板渐进式三区(决策2)
   - B区参数区改为手风琴折叠模式(默认折叠)
   - 新增 SlidersHorizontal + ChevronDown 图标切换
   - 点击展开 video 工具参数,收起后释放视觉空间
   - A区核心区(prompt+生成)始终展开

3. 任务进度条增强(决策3)
   - 进度条高度 1px→1.5px,视觉更醒目
   - 新增 shimmer 扫描动画(running 态,1.4s/cycle)
   - 新增百分比数字显示(tabular-nums 等宽数字)
   - 新增 indeterminate 模式(无百分比时循环扫描)
   - 尊重 prefers-reduced-motion(reducedMotion 时禁用动画)

4. CSS 动画
   - 新增 @keyframes canvas-task-progress-shimmer
   - .canvas-task-progress-shimmer 类

Closes #98

* refactor(canvas): z-index 全量迁移到五层Elevation token #98 #94

补全 #98 五层 Elevation 体系的剩余硬编码 z-index 清理:
- 三个设置弹层(图/音/视频)zIndex:1200 → var(--z-popover)
- model-setup-guide z-[900] → var(--z-toast)
- 项目侧栏 z-30 → var(--z-panel),章节预览 z-50 → var(--z-panel-floating)
- shared 页头/缩放控件/访客提示 z-[80]/z-[70] → var(--z-panel-floating),右键菜单 z-[90] → var(--z-modal)
- project 缩放控件容器 z-50 → var(--z-panel),绘图加载遮罩 z-[500] → var(--z-toast)
- 反馈面板 z-[90] → var(--z-panel-floating)

共 16 处全局浮层 z-index 迁移到 token,消除层级冲突隐患。

---------

Co-authored-by: sikongyue <3155157671@qq.com>
This commit is contained in:
爱笑的毛毛虫
2026-08-03 22:16:25 +08:00
committed by GitHub
parent 6eb77b93a0
commit 39844d7c8d
21 changed files with 103 additions and 43 deletions

View File

@@ -39,7 +39,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-[120] w-[min(332px,calc(100vw-24px))]"
className="pointer-events-none absolute right-3 top-[72px] z-[var(--z-modal-overlay)] w-[min(332px,calc(100vw-24px))]"
>
<LayoutGroup id="canvas-active-tasks">
<motion.section
@@ -134,14 +134,47 @@ function ActiveTaskCard({ task, now, theme, expanded, onToggle, reducedMotion }:
{expanded ? <ChevronUp className="mt-0.5 size-3.5 shrink-0" style={{ color: theme.node.muted }} /> : <ChevronDown className="mt-0.5 size-3.5 shrink-0" style={{ color: theme.node.muted }} />}
</div>
<div className="mt-3 h-1 overflow-hidden rounded-full" style={{ background: theme.toolbar.itemHover }}>
<motion.div className="h-full rounded-full" animate={{ width: `${progress ?? 8}%` }} transition={reducedMotion ? { duration: 0 } : { duration: 0.3 }} style={{ background: statusTone }} />
<div className="mt-3 h-1.5 overflow-hidden rounded-full" style={{ background: theme.toolbar.itemHover }}>
{progress !== undefined ? (
<motion.div
className="relative h-full rounded-full overflow-hidden"
animate={{ width: `${progress}%` }}
transition={reducedMotion ? { duration: 0 } : { duration: 0.3, ease: "easeOut" }}
style={{ background: statusTone }}
>
{/* 进度条 shimmer 扫描动画(对应 #98 决策3)*/}
{task.status === "running" && !reducedMotion ? (
<span
className="absolute inset-0 canvas-task-progress-shimmer"
style={{ background: "linear-gradient(90deg, transparent, rgba(255,255,255,.3), transparent)" }}
aria-hidden
/>
) : null}
</motion.div>
) : (
// indeterminate 模式(无具体百分比,从左到右循环扫描)
<motion.div
className="h-full rounded-full"
initial={{ width: "20%", x: "-100%" }}
animate={{ width: "20%", x: "400%" }}
transition={reducedMotion ? { duration: 0 } : { duration: 1.2, repeat: Infinity, ease: "easeInOut" }}
style={{ background: statusTone }}
/>
)}
</div>
<div className="mt-3 grid grid-cols-2 gap-2 text-[var(--fs-tiny)]" style={{ color: theme.node.muted }}>
<span className="inline-flex min-w-0 items-center gap-1 truncate" title={durationLabel}><Clock3 className="size-3 shrink-0" />{durationLabel}</span>
<span className="inline-flex min-w-0 items-center justify-end gap-1 truncate" title={billingLabel}><Coins className="size-3 shrink-0" />{billingLabel}</span>
</div>
{/* 进度百分比显示(对应 #98 决策3:列表视图)*/}
{progress !== undefined && task.status === "running" ? (
<div className="mt-1 flex items-center justify-between text-[var(--fs-micro)]" style={{ color: theme.node.muted }}>
<span>{durationLabel}</span>
<span className="font-medium tabular-nums" style={{ color: statusTone }}>{progress}%</span>
</div>
) : (
<div className="mt-3 grid grid-cols-2 gap-2 text-[var(--fs-tiny)]" style={{ color: theme.node.muted }}>
<span className="inline-flex min-w-0 items-center gap-1 truncate" title={durationLabel}><Clock3 className="size-3 shrink-0" />{durationLabel}</span>
<span className="inline-flex min-w-0 items-center justify-end gap-1 truncate" title={billingLabel}><Coins className="size-3 shrink-0" />{billingLabel}</span>
</div>
)}
</button>
<AnimatePresence initial={false}>

View File

@@ -125,7 +125,7 @@ export function CanvasAssetTray({ assetImages, canvasImages, showLibrary = true,
];
return (
<div ref={rootRef} data-canvas-no-zoom className="relative z-[62]" onPointerDown={(event) => event.stopPropagation()} onMouseDown={(event) => event.stopPropagation()} onWheel={(event) => event.stopPropagation()}>
<div ref={rootRef} data-canvas-no-zoom className="relative z-[var(--z-panel)]" onPointerDown={(event) => event.stopPropagation()} onMouseDown={(event) => event.stopPropagation()} onWheel={(event) => event.stopPropagation()}>
<AnimatePresence>
{open ? (
<motion.aside

View File

@@ -85,7 +85,7 @@ function AudioSettingsPortal({
const topPlacement = placement?.startsWith("top");
const style = {
position: "fixed",
zIndex: 1200,
zIndex: "var(--z-popover)",
width,
left: Math.max(margin, Math.min(window.innerWidth - width - margin, left)),
...(topPlacement ? { bottom: window.innerHeight - buttonRect.top + gap, maxHeight: Math.max(260, buttonRect.top - margin * 2) } : { top: buttonRect.bottom + gap, maxHeight: Math.max(260, window.innerHeight - buttonRect.bottom - margin * 2) }),

View File

@@ -272,7 +272,7 @@ function MentionMenu({ candidates, allInputs, activeIndex, theme, onSelect }: {
};
return (
<div className="aceternity-floating-panel absolute left-2 top-[calc(100%+6px)] z-[90] max-h-56 w-64 overflow-y-auto rounded-lg border p-1" style={{ background: theme.spatial.elevated, borderColor: theme.toolbar.border }}>
<div className="aceternity-floating-panel absolute left-2 top-[calc(100%+6px)] z-[var(--z-modal)] max-h-56 w-64 overflow-y-auto rounded-lg border p-1" style={{ background: theme.spatial.elevated, borderColor: theme.toolbar.border }}>
{candidates.map((candidate, index) => (
<button
key={candidate.kind === "skill" ? candidate.reference.id : candidate.input.nodeId}

View File

@@ -140,7 +140,7 @@ export function CanvasNodeContextMenu({
initial={reducedMotion ? { opacity: 0 } : { opacity: 0, scale: 0.97, x: -3, y: -3 }}
animate={{ opacity: 1, scale: 1, x: 0, y: 0 }}
transition={{ duration: aceternityMotion.duration.instant, ease: aceternityMotion.easing.enter }}
className="aceternity-floating-panel fixed z-[180] flex w-[224px] max-h-[calc(100vh-56px)] origin-top-left flex-col overflow-hidden rounded-xl border p-1.5 backdrop-blur-2xl"
className="aceternity-floating-panel fixed z-[var(--z-popover)] flex w-[224px] max-h-[calc(100vh-56px)] origin-top-left flex-col overflow-hidden rounded-xl border p-1.5 backdrop-blur-2xl"
style={{ left: position.left, top: position.top, background: theme.spatial.elevated, borderColor: theme.toolbar.border, color: theme.node.text, boxShadow: `0 30px 90px ${theme.spatial.shadow}` }}
onContextMenu={(event) => event.preventDefault()}
onPointerDown={(event) => event.stopPropagation()}
@@ -274,7 +274,7 @@ function AddNodeContextMenu({ parentPosition, workspaceMode, isProjectLinked, re
animate={{ opacity: 1, x: 0, scale: 1 }}
exit={reducedMotion ? { opacity: 0 } : { opacity: 0, x: left > parentPosition.left ? -4 : 4, scale: 0.98 }}
transition={{ duration: aceternityMotion.duration.instant, ease: aceternityMotion.easing.enter }}
className="aceternity-floating-panel fixed z-[181] w-[260px] origin-top overflow-hidden rounded-[var(--dock-radius)] border p-2 backdrop-blur-2xl"
className="aceternity-floating-panel fixed z-[var(--z-popover)] w-[260px] origin-top overflow-hidden rounded-[var(--dock-radius)] border p-2 backdrop-blur-2xl"
style={{ left, top: parentPosition.top, background: theme.spatial.elevated, borderColor: theme.toolbar.border, color: theme.node.text, boxShadow: `0 30px 90px ${theme.spatial.shadow}` }}
onContextMenu={(event) => event.preventDefault()}
onPointerDown={(event) => event.stopPropagation()}

View File

@@ -207,7 +207,7 @@ function FaceSelectionOverlay({ node, viewport, containerRef, imageWidth, imageH
<div
ref={overlayRef}
data-canvas-no-zoom
className={`absolute z-[105] overflow-hidden rounded-[var(--r-2xl)] ${status === "manual" ? "cursor-crosshair touch-none" : "pointer-events-none"}`}
className={`absolute z-[var(--z-modal)] overflow-hidden rounded-[var(--r-2xl)] ${status === "manual" ? "cursor-crosshair touch-none" : "pointer-events-none"}`}
style={{ left: 0, top: 0, width: node.width * viewport.k, height: node.height * viewport.k }}
onPointerDown={status === "manual" ? (event) => {
event.preventDefault();
@@ -287,7 +287,7 @@ function SelectionToolbar({ node, viewport, containerRef, status, faceCount, err
initial={reducedMotion ? { opacity: 0 } : { opacity: 0, y: -5, scale: 0.96 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
transition={reducedMotion ? { duration: 0 } : aceternityMotion.spring.panel}
className="aceternity-floating-panel absolute z-[125] flex h-12 w-[420px] max-w-[calc(100%_-_24px)] items-center rounded-[15px] border px-2 backdrop-blur-2xl"
className="aceternity-floating-panel absolute z-[var(--z-modal-overlay)] flex h-12 w-[420px] max-w-[calc(100%_-_24px)] items-center rounded-[15px] border px-2 backdrop-blur-2xl"
style={{ left: 12, top: 76, background: theme.spatial.elevated, borderColor: theme.toolbar.border, color: theme.node.text, boxShadow: `0 22px 60px ${theme.spatial.shadow}` }}
onPointerDown={(event) => event.stopPropagation()}
>
@@ -305,7 +305,7 @@ function SelectionToolbar({ node, viewport, containerRef, status, faceCount, err
function EmotionPanelOverlay({ node, viewport, containerRef, children }: { node: CanvasNodeData; viewport: ViewportTransform; containerRef: RefObject<HTMLDivElement | null>; children: ReactNode }) {
const panelRef = useRef<HTMLDivElement>(null);
useScreenAnchor(panelRef, node, viewport, containerRef, (next, container) => panelScreenRect(node, next, container, panelRef.current));
return <div ref={panelRef} data-canvas-no-zoom className="absolute z-[120] w-[580px] max-w-[calc(100%_-_24px)]" style={{ left: 12, top: 76 }} onPointerDown={(event) => event.stopPropagation()}>{children}</div>;
return <div ref={panelRef} data-canvas-no-zoom className="absolute z-[var(--z-modal-overlay)] w-[580px] max-w-[calc(100%_-_24px)]" style={{ left: 12, top: 76 }} onPointerDown={(event) => event.stopPropagation()}>{children}</div>;
}
function useScreenAnchor(ref: RefObject<HTMLElement | null>, node: CanvasNodeData, viewport: ViewportTransform, containerRef: RefObject<HTMLDivElement | null>, resolve: (viewport: ViewportTransform, container: HTMLDivElement) => { left: number; top: number; width?: number; height?: number }) {

View File

@@ -99,7 +99,7 @@ function ImageSettingsPortal({
const topPlacement = placement?.startsWith("top");
const style = {
position: "fixed",
zIndex: 1200,
zIndex: "var(--z-popover)",
width,
left: Math.max(margin, Math.min(window.innerWidth - width - margin, left)),
...(topPlacement ? { bottom: window.innerHeight - buttonRect.top + gap, maxHeight: Math.max(260, buttonRect.top - margin * 2) } : { top: buttonRect.bottom + gap, maxHeight: Math.max(260, window.innerHeight - buttonRect.bottom - margin * 2) }),

View File

@@ -159,7 +159,7 @@ export function CanvasLeaferGraphicsLayer(props: CanvasLeaferGraphicsLayerProps)
return (
<>
<div ref={underlayHostRef} data-canvas-leafer-underlay className="pointer-events-none absolute inset-0 z-0 overflow-hidden" aria-hidden />
<div ref={overlayHostRef} data-canvas-leafer-overlay className="pointer-events-none absolute inset-0 z-[55] overflow-hidden" aria-hidden />
<div ref={overlayHostRef} data-canvas-leafer-overlay className="pointer-events-none absolute inset-0 z-[var(--z-canvas-overlay)] overflow-hidden" aria-hidden />
</>
);
}

View File

@@ -574,7 +574,7 @@ export const CanvasLocalAgentPanel = memo(function CanvasLocalAgentPanel({ snaps
return (
<motion.div
className="relative z-[70] flex h-full shrink-0"
className="relative z-[var(--z-panel-floating)] flex h-full shrink-0"
initial={{ width: 0, opacity: 0 }}
animate={{ width: collapsed ? 0 : width + 1, opacity: collapsed ? 0 : 1 }}
transition={{ duration: resizing ? 0 : PANEL_MOTION_SECONDS, ease: [0.22, 1, 0.36, 1] }}

View File

@@ -1,5 +1,5 @@
import { useCallback, useEffect, useMemo, useState, type ReactNode } from "react";
import { ArrowUp, AtSign, Boxes, FileText, ImageIcon, ImagePlus, Maximize2, Music2, Pencil, Square, UserRound, Video } from "lucide-react";
import { ArrowUp, AtSign, Boxes, ChevronDown, FileText, ImageIcon, ImagePlus, Maximize2, Music2, Pencil, SlidersHorizontal, Square, UserRound, Video } from "lucide-react";
import { Button, Modal, Tooltip } from "antd";
import { ModelPicker } from "@/components/model-picker";
@@ -50,6 +50,7 @@ export function CanvasNodePromptPanel({ node, isRunning, onPromptChange, onConfi
const [expandedPresetOpen, setExpandedPresetOpen] = useState(false);
const [expandedPromptOpen, setExpandedPromptOpen] = useState(false);
const [promptContentHeight, setPromptContentHeight] = useState(0);
const [paramsExpanded, setParamsExpanded] = useState(false); // #98 决策2:B区参数区折叠状态(手风琴)
const generationCount = Math.max(1, Math.min(15, Math.floor(Math.abs(Number(config.count)) || 1)));
const priceChannel = resolveModelChannel(config, config.model);
const credits = requestCreditCost({ channelMode: priceChannel.scope === "system" ? "remote" : "local", modelCosts: priceChannel.modelCosts, model: modelOptionName(config.model), count: mode === "image" ? generationCount : 1, seconds: mode === "video" ? config.videoSeconds : 1 });
@@ -261,9 +262,26 @@ export function CanvasNodePromptPanel({ node, isRunning, onPromptChange, onConfi
/>
</div>
{/* B区 参数区(对应 #98 决策2:默认折叠,手风琴展开)*/}
{mode === "video" && !simpleMode ? (
<div className="mt-1.5 rounded-md border p-0.5" style={{ background: controlSurface, borderColor: insetBorder }}>
<CanvasVideoPromptTools metadata={node.metadata} frameOptions={videoFrameOptions} onMetadataChange={(patch) => onConfigChange(node.id, patch)} />
<div className="mt-1.5 overflow-hidden rounded-md border" style={{ background: controlSurface, borderColor: insetBorder }}>
<button
type="button"
className="flex w-full items-center gap-1.5 px-2 py-1 text-[var(--fs-micro)] font-medium transition-colors hover:bg-white/[.04]"
style={{ color: theme.node.muted }}
onClick={() => setParamsExpanded(!paramsExpanded)}
aria-expanded={paramsExpanded}
aria-label={paramsExpanded ? "收起参数" : "展开参数"}
>
<SlidersHorizontal className="size-3" strokeWidth={1.8} />
<span className="flex-1 text-left">参数</span>
<ChevronDown className={`size-3 transition-transform duration-200 ${paramsExpanded ? "rotate-180" : ""}`} strokeWidth={1.8} />
</button>
{paramsExpanded ? (
<div className="border-t p-0.5" style={{ borderColor: insetBorder }}>
<CanvasVideoPromptTools metadata={node.metadata} frameOptions={videoFrameOptions} onMetadataChange={(patch) => onConfigChange(node.id, patch)} />
</div>
) : null}
</div>
) : null}

View File

@@ -118,7 +118,7 @@ export function CanvasProjectSidebar({ projectId, detail, onAddChapter, onLocate
if (collapsed) {
return (
<aside className="relative z-30 hidden w-11 shrink-0 flex-col items-center border-r border-border bg-background/94 py-2 backdrop-blur-xl lg:flex">
<aside className="relative z-[var(--z-panel)] hidden w-11 shrink-0 flex-col items-center border-r border-border bg-background/94 py-2 backdrop-blur-xl lg:flex">
<button type="button" className="grid size-7 place-items-center rounded-md text-foreground/55 hover:bg-foreground/[.06]" title="展开项目侧栏" aria-label="展开项目侧栏" onClick={() => setCollapsed(false)}><ChevronRight className="size-4" /></button>
<Link to={`/projects/${projectId}/canvases`} className="mt-2 grid size-7 place-items-center rounded-md text-foreground/55 hover:bg-foreground/[.06]" title="返回项目画布列表"><FolderKanban className="size-4" /></Link>
</aside>
@@ -126,7 +126,7 @@ export function CanvasProjectSidebar({ projectId, detail, onAddChapter, onLocate
}
return (
<aside className="relative z-30 hidden w-[232px] shrink-0 flex-col border-r border-border bg-background/94 backdrop-blur-xl lg:flex">
<aside className="relative z-[var(--z-panel)] hidden w-[232px] shrink-0 flex-col border-r border-border bg-background/94 backdrop-blur-xl lg:flex">
<header className="flex h-12 shrink-0 items-center justify-between gap-2 border-b border-border px-2.5">
<Link to={`/projects/${projectId}/canvases`} className="flex min-w-0 items-center gap-2 text-xs font-semibold" title="返回项目画布列表"><FolderKanban className="size-3.5 shrink-0" /><span className="truncate">{projectDetail?.project.name || "项目空间"}</span></Link>
<button type="button" className="grid size-7 shrink-0 place-items-center rounded-md text-foreground/45 hover:bg-foreground/[.06]" title="收起项目侧栏" aria-label="收起项目侧栏" onClick={() => setCollapsed(true)}><ChevronLeft className="size-4" /></button>
@@ -193,7 +193,7 @@ export function CanvasProjectSidebar({ projectId, detail, onAddChapter, onLocate
function ChapterPreview({ projectId, unit, chapterNumber, loading, loadError, adding, onAdd, onClose }: { projectId: string; unit: ProjectUnit; chapterNumber: number; loading: boolean; loadError: boolean; adding: boolean; onAdd: () => void | Promise<void>; onClose: () => void }) {
const text = htmlToPlainText(unit.sourceText);
return (
<section className="absolute bottom-20 left-[calc(100%+8px)] top-14 z-50 flex w-[320px] flex-col overflow-hidden rounded-lg border border-border/85 bg-background/[.96] shadow-[0_18px_48px_rgba(0,0,0,.22)] backdrop-blur-xl" aria-label={`第 ${chapterNumber} 章预览`}>
<section className="absolute bottom-20 left-[calc(100%+8px)] top-14 z-[var(--z-panel-floating)] flex w-[320px] flex-col overflow-hidden rounded-lg border border-border/85 bg-background/[.96] shadow-[0_18px_48px_rgba(0,0,0,.22)] backdrop-blur-xl" aria-label={`第 ${chapterNumber} 章预览`}>
<header className="flex items-start gap-2 border-b border-border/70 px-3 py-2.5"><div className="min-w-0 flex-1"><div className="text-[var(--fs-tiny)] tabular-nums text-foreground/38">第 {chapterNumber.toLocaleString("zh-CN")} 章</div><h2 className="mt-0.5 truncate text-sm font-semibold">{unit.title}</h2></div><button type="button" onClick={onClose} className="grid size-6 shrink-0 place-items-center rounded text-foreground/38 hover:bg-foreground/[.06] hover:text-foreground" aria-label="关闭章节预览"><X className="size-3.5" /></button></header>
<div className="thin-scrollbar min-h-0 flex-1 overflow-y-auto px-3 py-3"><p className="whitespace-pre-wrap text-xs leading-5 text-foreground/58">{loading ? "正在读取本章正文…" : loadError ? "正文读取失败,请稍后重试" : text || "本章还没有正文"}</p>{!loading && !loadError ? <div className="mt-2 text-[var(--fs-tiny)] tabular-nums text-foreground/35">{text.length.toLocaleString("zh-CN")} 字</div> : null}</div>
<footer className="flex shrink-0 items-center justify-between border-t border-border/70 px-2 py-2"><button type="button" disabled={adding} onClick={() => void onAdd()} className="inline-flex h-8 items-center gap-1.5 rounded-md px-2.5 text-xs font-medium text-[var(--workspace-accent)] hover:bg-[var(--workspace-accent-soft)] disabled:cursor-wait">{adding ? <LoaderCircle className="size-3.5 animate-spin" /> : <Plus className="size-3.5" />}添加到画布</button><Link to={`/projects/${projectId}/chapters/${unit.id}`} className="inline-flex h-8 items-center gap-1.5 rounded-md px-2.5 text-xs text-foreground/62 hover:bg-foreground/[.06] hover:text-foreground">打开编辑器<ArrowUpRight className="size-3.5" /></Link></footer>

View File

@@ -415,7 +415,7 @@ function MentionMenu({ anchor, references, activeIndex, theme, preferredWidth, o
return createPortal(
<div
data-canvas-resource-mention-menu="true"
className="fixed z-[1300] max-h-56 overflow-y-auto rounded-xl border p-1 shadow-2xl backdrop-blur-md"
className="fixed z-[var(--z-tooltip)] max-h-56 overflow-y-auto rounded-xl border p-1 shadow-2xl backdrop-blur-md"
style={{ left, top, width: menuWidth, background: theme.toolbar.panel, borderColor: theme.toolbar.border, color: theme.node.text }}
onPointerDown={stopCanvasInteraction}
onMouseDown={stopCanvasInteraction}

View File

@@ -115,13 +115,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-[48] 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-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}>
<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-[48] 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-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 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

@@ -86,7 +86,7 @@ function VideoSettingsPortal({
const placeAbove = topPlacement ? topSpace >= estimatedHeight || topSpace >= bottomSpace : bottomSpace < estimatedHeight && topSpace > bottomSpace;
const style = {
position: "fixed",
zIndex: 1200,
zIndex: "var(--z-popover)",
width,
left: Math.max(margin, Math.min(window.innerWidth - width - margin, left)),
...(placeAbove ? { bottom: window.innerHeight - buttonRect.top + gap, maxHeight: Math.max(260, topSpace) } : { top: buttonRect.bottom + gap, maxHeight: Math.max(260, bottomSpace) }),

View File

@@ -73,7 +73,7 @@ export function CanvasSelectionToolbar({ anchorRef, containerRef, count, childre
<div
ref={toolbarRef}
data-canvas-no-zoom
className={`absolute z-[70] max-w-[calc(100%_-_24px)] -translate-x-1/2 ${anchor.placement === "above" ? "-translate-y-full" : ""}`}
className={`absolute z-[var(--z-panel-floating)] max-w-[calc(100%_-_24px)] -translate-x-1/2 ${anchor.placement === "above" ? "-translate-y-full" : ""}`}
style={{ left: anchor.left, top: anchor.top, color: theme.node.text, transformOrigin: anchor.placement === "above" ? "bottom center" : "top center" }}
onMouseDown={(event) => event.stopPropagation()}
onPointerDown={(event) => event.stopPropagation()}
@@ -114,7 +114,7 @@ export function CanvasNodePanelOverlay({ node, viewport, containerRef, panelWidt
<div
ref={panelRef}
data-canvas-no-zoom
className="thin-scrollbar absolute z-[120] max-w-[calc(100%_-_24px)] overflow-y-auto"
className="thin-scrollbar absolute z-[var(--z-modal-overlay)] max-w-[calc(100%_-_24px)] overflow-y-auto"
style={{ left: initialPosition.left, top: initialPosition.top, width: panelWidth, maxHeight: "calc(100% - 84px)" }}
onMouseDown={(event) => event.stopPropagation()}
onPointerDown={(event) => event.stopPropagation()}
@@ -154,7 +154,7 @@ export function CanvasConnectionCreateMenu({ pending, viewport, viewportSize, co
initial={reducedMotion ? { opacity: 0 } : { opacity: 0, y: 6, scale: 0.97, rotateX: 2 }}
animate={{ opacity: 1, y: 0, scale: 1, rotateX: 0 }}
transition={{ duration: aceternityMotion.duration.instant, ease: aceternityMotion.easing.enter }}
className="aceternity-floating-panel absolute z-[120] w-[248px] origin-top-left overflow-hidden rounded-[var(--r-2xl)] border p-2 backdrop-blur-2xl"
className="aceternity-floating-panel absolute z-[var(--z-modal-overlay)] w-[248px] origin-top-left overflow-hidden rounded-[var(--r-2xl)] border p-2 backdrop-blur-2xl"
data-canvas-no-zoom
data-connection-create-menu
style={{ left: initialPosition.left, top: initialPosition.top, background: theme.spatial.elevated, borderColor: theme.toolbar.border, color: theme.node.text, boxShadow: `0 30px 90px ${theme.spatial.shadow}` }}

View File

@@ -195,7 +195,7 @@ export function CanvasDirectorWorkbench({ open, scene, imageNodes, onClose, onCh
if (!open || !draft || !activeShot) return null;
return (
<div data-canvas-no-zoom className="fixed inset-0 z-[500] flex min-h-0 flex-col overflow-hidden" style={{ background: theme.canvas.background, color: theme.node.text }}>
<div data-canvas-no-zoom className="fixed inset-0 z-[var(--z-toast)] flex min-h-0 flex-col overflow-hidden" style={{ background: theme.canvas.background, color: theme.node.text }}>
<header className="flex h-12 shrink-0 items-center gap-2 border-b px-2" style={{ background: theme.toolbar.panel, borderColor: theme.toolbar.border }}>
<IconButton label="关闭导演台" onClick={onClose}><X className="size-4" /></IconButton>
<Input variant="borderless" value={draft.title} className="max-w-56 font-medium" onChange={(event) => replaceWithoutHistory((current) => ({ ...current, title: event.target.value }))} />

View File

@@ -27,7 +27,7 @@ export function ModelSetupGuide({ hidden = false }: { hidden?: boolean }) {
};
return (
<motion.aside initial={reducedMotion ? false : { opacity: 0, y: 14, scale: 0.97 }} animate={{ opacity: 1, y: 0, scale: 1 }} transition={{ duration: aceternityMotion.duration.panel, ease: aceternityMotion.easing.enter }} className="fixed bottom-5 right-5 z-[900] w-[min(360px,calc(100vw-32px))] overflow-hidden rounded-2xl border border-border/70 bg-background/92 shadow-[0_24px_70px_rgba(15,23,42,.20)] backdrop-blur-2xl dark:shadow-[0_28px_80px_rgba(0,0,0,.48)]">
<motion.aside initial={reducedMotion ? false : { opacity: 0, y: 14, scale: 0.97 }} animate={{ opacity: 1, y: 0, scale: 1 }} transition={{ duration: aceternityMotion.duration.panel, ease: aceternityMotion.easing.enter }} className="fixed bottom-5 right-5 z-[var(--z-toast)] w-[min(360px,calc(100vw-32px))] overflow-hidden rounded-2xl border border-border/70 bg-background/92 shadow-[0_24px_70px_rgba(15,23,42,.20)] backdrop-blur-2xl dark:shadow-[0_28px_80px_rgba(0,0,0,.48)]">
<div className="flex items-start gap-3 p-4">
<span className="grid size-9 shrink-0 place-items-center rounded-xl border border-amber-400/25 bg-amber-400/10 text-amber-600 dark:text-amber-300"><Sparkles className="size-4" /></span>
<button type="button" className="min-w-0 flex-1 text-left" onClick={openModels}>

View File

@@ -29,7 +29,7 @@ export function CanvasUploadStatusToast({ status, theme }: { status: CanvasUploa
initial={{ opacity: 0, x: 22, scale: 0.94 }}
animate={{ opacity: 1, x: 0, scale: 1 }}
transition={aceternityMotion.spring.panel}
className="pointer-events-none absolute right-4 top-20 z-[90] w-[320px] overflow-hidden rounded-[22px] border px-4 py-4 backdrop-blur-2xl"
className="pointer-events-none absolute right-4 top-20 z-[var(--z-panel-floating)] w-[320px] overflow-hidden rounded-[22px] border px-4 py-4 backdrop-blur-2xl"
style={{ background: theme.spatial.elevated, borderColor: status.error ? `${theme.accent.danger}66` : status.done ? "rgba(34,197,94,.4)" : theme.spatial.glowStrong, color: theme.node.text, boxShadow: `0 24px 72px ${theme.spatial.shadow}, inset 0 1px 0 rgba(255,255,255,.14)` }}
>
<div className="absolute inset-x-0 top-0 h-px" style={{ background: `linear-gradient(90deg, transparent, ${accent}, transparent)` }} />
@@ -61,7 +61,7 @@ export function CanvasMergeStatusToast({ progress, theme }: { progress: MergeVid
const detail = progress.phase === "loading" ? "加载视频工具" : progress.phase === "reading" ? "读取选中视频" : "正在编码合并成片";
const percent = Math.max(0, Math.min(100, Math.round(progress.progress)));
return (
<div data-canvas-no-zoom aria-live="polite" className="pointer-events-none absolute right-4 top-[164px] z-[90] w-[292px] overflow-hidden rounded-2xl border px-3 py-3 shadow-lg backdrop-blur-xl" style={{ background: theme.toolbar.panel, borderColor: theme.toolbar.border, color: theme.node.text }}>
<div data-canvas-no-zoom aria-live="polite" className="pointer-events-none absolute right-4 top-[164px] z-[var(--z-panel-floating)] w-[292px] overflow-hidden rounded-2xl border px-3 py-3 shadow-lg backdrop-blur-xl" style={{ background: theme.toolbar.panel, borderColor: theme.toolbar.border, color: theme.node.text }}>
<div className="flex items-start gap-2.5">
<span className="mt-0.5 grid size-7 shrink-0 place-items-center rounded-xl" style={{ background: theme.toolbar.itemHover, color: theme.node.activeStroke }}>
<LoaderCircle className="size-4 animate-spin" />
@@ -81,7 +81,7 @@ export function CanvasMergeStatusToast({ progress, theme }: { progress: MergeVid
export function CanvasAgentChangeToast({ change, theme, onView, onUndo, onClose }: { change: CanvasAgentChange; theme: CanvasTheme; onView: () => void; onUndo: () => void; onClose: () => void }) {
return (
<div data-canvas-no-zoom aria-live="polite" className="absolute bottom-20 right-4 z-[90] w-[320px] rounded-lg border p-3 shadow-lg backdrop-blur-xl" style={{ background: theme.toolbar.panel, borderColor: theme.toolbar.border, color: theme.node.text }}>
<div data-canvas-no-zoom aria-live="polite" className="absolute bottom-20 right-4 z-[var(--z-panel-floating)] w-[320px] rounded-lg border p-3 shadow-lg backdrop-blur-xl" style={{ background: theme.toolbar.panel, borderColor: theme.toolbar.border, color: theme.node.text }}>
<div className="flex items-start gap-2.5">
<span className="grid size-8 shrink-0 place-items-center rounded-md" style={{ background: theme.toolbar.itemHover, color: theme.node.activeStroke }}><span className="size-2 rounded-full bg-current" /></span>
<span className="min-w-0 flex-1"><span className="block text-xs font-semibold">Agent 已写回画布</span><span className="mt-0.5 block truncate text-[var(--fs-label)]" style={{ color: theme.node.muted }}>{change.summary} · 可撤销最近 {change.undoCount} 批</span></span>

View File

@@ -1608,7 +1608,7 @@ function InfiniteCanvasPage() {
{isMiniMapOpen ? <Minimap nodes={nodes} viewport={viewport} viewportSize={size} canvasContainerRef={containerRef} onViewportPreviewChange={previewViewport} onViewportChange={handleViewportChange} /> : null}
<div data-canvas-no-zoom className="absolute bottom-4 left-4 z-50 flex items-end gap-2" onMouseDown={(event) => event.stopPropagation()} onPointerDown={(event) => event.stopPropagation()} onWheel={(event) => event.stopPropagation()}>
<div data-canvas-no-zoom className="absolute bottom-4 left-4 z-[var(--z-panel)] flex items-end gap-2" onMouseDown={(event) => event.stopPropagation()} onPointerDown={(event) => event.stopPropagation()} onWheel={(event) => event.stopPropagation()}>
<CanvasZoomControls scale={viewport.k} containerRef={containerRef} onScaleChange={setZoomScale} onReset={resetViewport} isMiniMapOpen={isMiniMapOpen} onToggleMiniMap={() => setIsMiniMapOpen((value) => !value)} onOpenShortcuts={() => setShortcutRequestNonce((value) => value + 1)} />
<CanvasAssetTray assetImages={imageAssets} canvasImages={canvasImageNodes} showLibrary={!currentProject?.projectId} activeNodeId={selectedNodeIds.size === 1 ? Array.from(selectedNodeIds)[0] : null} onInsertAssetImage={(asset) => void createImageAssetNode(asset)} onFocusCanvasImage={focusCanvasImageNode} />
</div>
@@ -1665,7 +1665,7 @@ function InfiniteCanvasPage() {
}}
/>
{drawingNode ? <Suspense fallback={<div className="fixed inset-0 z-[500] grid place-items-center px-5" style={{ background: theme.canvas.background, color: theme.node.text }}><WorkspaceState icon="loading" title="正在加载绘图编辑器" description="正在准备绘图画布。" /></div>}>
{drawingNode ? <Suspense fallback={<div className="fixed inset-0 z-[var(--z-toast)] grid place-items-center px-5" style={{ background: theme.canvas.background, color: theme.node.text }}><WorkspaceState icon="loading" title="正在加载绘图编辑器" description="正在准备绘图画布。" /></div>}>
<CanvasDrawingEditorModal
node={drawingNode}
projectId={projectId}
@@ -1692,7 +1692,7 @@ function InfiniteCanvasPage() {
/>
{directorNodeId && activeDirectorScene ? (
<Suspense fallback={<div className="fixed inset-0 z-[500] grid place-items-center px-5" style={{ background: theme.canvas.background, color: theme.node.text }}><WorkspaceState icon="loading" title="正在加载 3D 导演台" description="准备场景、镜头与空间控制。" /></div>}>
<Suspense fallback={<div className="fixed inset-0 z-[var(--z-toast)] grid place-items-center px-5" style={{ background: theme.canvas.background, color: theme.node.text }}><WorkspaceState icon="loading" title="正在加载 3D 导演台" description="准备场景、镜头与空间控制。" /></div>}>
<CanvasDirectorWorkbench
open
scene={activeDirectorScene}

View File

@@ -223,7 +223,7 @@ export default function SharedCanvasPage() {
return (
<main className="relative h-screen overflow-hidden" style={{ background: theme.canvas.background, color: theme.node.text }}>
<header className="pointer-events-none absolute inset-x-0 top-0 z-[80] flex h-16 items-center justify-between px-5">
<header className="pointer-events-none absolute inset-x-0 top-0 z-[var(--z-panel-floating)] flex h-16 items-center justify-between px-5">
<div className="pointer-events-auto flex min-w-0 items-center gap-3">
<Share2 className="size-4" style={{ color: theme.node.muted }} />
<span className="max-w-[45vw] truncate text-base font-semibold">{title}</span>
@@ -258,8 +258,8 @@ export default function SharedCanvasPage() {
<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} />
<div className="absolute bottom-5 left-5 z-[70]"><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-[70] max-w-[340px] text-right text-xs leading-5" style={{ color: theme.node.muted }}>访客操作仅在当前页面临时生效</div>
<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>
{contextMenu ? <SharedContextMenu menu={contextMenu} onAdd={addNode} onInfo={() => { if (contextMenu.nodeId) setInfoNodeId(contextMenu.nodeId); setContextMenu(null); }} onUnauthorized={() => { setContextMenu(null); unauthorized(); }} /> : null}
<CanvasNodeInfoModal node={infoNode} open={Boolean(infoNode)} onClose={() => setInfoNodeId(null)} readOnly onUnauthorized={unauthorized} />
@@ -269,7 +269,7 @@ export default function SharedCanvasPage() {
function SharedContextMenu({ menu, onAdd, onInfo, onUnauthorized }: { menu: ContextMenu; onAdd: (type: CanvasNodeType) => void; onInfo: () => void; onUnauthorized: () => void }) {
const theme = canvasThemes[useThemeStore((state) => state.theme)];
return <div data-canvas-no-zoom className="absolute z-[90] min-w-48 rounded-lg border p-1.5 shadow-xl" style={{ left: menu.x, top: menu.y, background: theme.toolbar.panel, borderColor: theme.toolbar.border, color: theme.node.text }} onMouseDown={(event) => event.stopPropagation()}>
return <div data-canvas-no-zoom className="absolute z-[var(--z-modal)] min-w-48 rounded-lg border p-1.5 shadow-xl" style={{ left: menu.x, top: menu.y, background: theme.toolbar.panel, borderColor: theme.toolbar.border, color: theme.node.text }} onMouseDown={(event) => event.stopPropagation()}>
{menu.nodeId ? <><MenuButton icon={<Eye />} label="查看节点信息" onClick={onInfo} /><MenuButton icon={<LockKeyhole />} label="编辑或生成" onClick={onUnauthorized} /></> : <>
<div className="px-2 py-1.5 text-[var(--fs-label)]" style={{ color: theme.node.muted }}>添加临时节点</div>
<MenuButton icon={<FileText />} label="文本节点" onClick={() => onAdd(CanvasNodeType.Text)} />

View File

@@ -4369,6 +4369,15 @@
100% { opacity: 0; transform: scale(0.9); }
}
/* 任务进度条 shimmer 扫描动画(对应 #98 决策3)*/
@keyframes canvas-task-progress-shimmer {
0% { transform: translateX(-100%); }
100% { transform: translateX(200%); }
}
.canvas-task-progress-shimmer {
animation: canvas-task-progress-shimmer 1.4s linear infinite;
}
.canvas-node-shell-generating::before {
position: absolute;
z-index: 45;