diff --git a/AGENTS.md b/AGENTS.md index aea07a8fb..05aa63681 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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-*`)。 diff --git a/web/src/components/canvas/canvas-active-task-panel.tsx b/web/src/components/canvas/canvas-active-task-panel.tsx index 57fba2c29..07b797876 100644 --- a/web/src/components/canvas/canvas-active-task-panel.tsx +++ b/web/src/components/canvas/canvas-active-task-panel.tsx @@ -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))]" > 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("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 ( - ) : null} {showStatusTrack ? ( -
+
{resourceLabel ? : null} {hasMediaContent && !readOnly ? : null} {isBatchRoot ? onToggleBatch?.(data.id)} /> : null} @@ -481,7 +481,7 @@ export const CanvasNode = React.memo(function CanvasNode({
) : null} {assetTags.length || (showImageInfo && hasImageContent) ? ( -
+
{assetTags.length ? : null} {showImageInfo && hasImageContent ? : null}
@@ -1128,7 +1128,7 @@ function ResizeHandle({ corner, onMouseDown }: { corner: ResizeCorner; onMouseDo "bottom-right": "-bottom-[14px] -right-[14px] cursor-nwse-resize", }[corner]; - return
onMouseDown(event, corner)} />; + return
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 (
event.stopPropagation()} onPointerDown={(event) => event.stopPropagation()} @@ -1264,7 +1264,7 @@ function ConnectionSideRail({ side, scale, visible, theme, onPointerDown }: { si return ( ); } return ( -
+
{progress.steps.map((step, index) => ( diff --git a/web/src/components/canvas/canvas-toolbar.tsx b/web/src/components/canvas/canvas-toolbar.tsx index bdbb083cc..29c4884a7 100644 --- a/web/src/components/canvas/canvas-toolbar.tsx +++ b/web/src/components/canvas/canvas-toolbar.tsx @@ -175,7 +175,7 @@ export function CanvasToolbar({ const resourceCommands = addNodeCommands.filter((cmd) => cmd.section === "resource").map(toCommand); return ( -
+
{addOpen ? ( {appearanceOpen ? ( - + event.stopPropagation()}> } title="画布外观" subtitle="调整整个创作空间" theme={theme} />
主题模式
@@ -231,7 +231,7 @@ function AddNodeMenu({ x, theme, nodeCommands, resourceCommands }: { resourceCommands: CanvasCreateCommand[]; }) { return ( - + event.stopPropagation()}> } title="创建内容" subtitle="选择节点类型" theme={theme} /> diff --git a/web/src/components/canvas/canvas-zoom-controls.tsx b/web/src/components/canvas/canvas-zoom-controls.tsx index 74e944995..e296c1393 100644 --- a/web/src/components/canvas/canvas-zoom-controls.tsx +++ b/web/src/components/canvas/canvas-zoom-controls.tsx @@ -86,7 +86,7 @@ export function CanvasZoomControls({ scale, onScaleChange, onReset, isMiniMapOpe ]; return ( -
event.stopPropagation()} onPointerDown={(event) => event.stopPropagation()} onWheel={(event) => event.stopPropagation()}> +
event.stopPropagation()} onPointerDown={(event) => event.stopPropagation()} onWheel={(event) => event.stopPropagation()}> {precisionOpen ? ( (readInitialPreference); + const [viewportWidth, setViewportWidth] = useState(() => (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, + }; +} diff --git a/web/src/pages/canvas/canvas-project-top-bar.tsx b/web/src/pages/canvas/canvas-project-top-bar.tsx index 3adc4a352..ed54c37d8 100644 --- a/web/src/pages/canvas/canvas-project-top-bar.tsx +++ b/web/src/pages/canvas/canvas-project-top-bar.tsx @@ -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 ( <> -
+
{availableMicrocredits === null ? "--" : (availableMicrocredits / 1_000_000).toLocaleString("zh-CN", { maximumFractionDigits: 3 })} ) : null} + +