* 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:
@@ -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}>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) }),
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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()}
|
||||
|
||||
@@ -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 }) {
|
||||
|
||||
@@ -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) }),
|
||||
|
||||
@@ -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 />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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] }}
|
||||
|
||||
@@ -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}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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) }),
|
||||
|
||||
@@ -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}` }}
|
||||
|
||||
@@ -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 }))} />
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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)} />
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user