按视觉层重设计 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:
@@ -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-*`)。
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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={() => {
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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="创作节点" />
|
||||
|
||||
@@ -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
|
||||
|
||||
45
web/src/hooks/use-focus-mode.ts
Normal file
45
web/src/hooks/use-focus-mode.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 */
|
||||
|
||||
Reference in New Issue
Block a user