feat(canvas): 沉浸专注与短剧流程 - 支持专注模式与流程条并入顶栏体系 (#157)

- 沉浸专注:进入后隐藏顶部栏、Dock、缩放控件、小地图与素材托盘,保留专注胶囊可唤出工具与智能体;小屏默认进入,⇧⌘F 切换,Esc 在无选中与浮层时退出
- 智能体面板改贴边停靠并按顶栏让位,作为沉浸专注的配套依赖
- 短剧流程条并入顶栏:折叠入口移入顶栏按钮组,展开条锚定顶栏下缘,与工作模式开关互斥
- 流程步骤改为里程碑节点与状态轨道,新增 --flow-step-* Design Token

Co-authored-by: sikongyue <3155157671@qq.com>
This commit is contained in:
爱笑的毛毛虫
2026-08-06 08:32:38 +08:00
committed by GitHub
parent 6671c331e3
commit 971489a336
12 changed files with 519 additions and 255 deletions

View File

@@ -1,45 +1,45 @@
import { useCallback, useEffect, useState } from "react";
const FOCUS_MODE_KEY = "canvas-focus-mode-v1";
const FORCED_FOCUS_BREAKPOINT = 1024;
const FOCUS_MODE_KEY = "canvas-focus-mode-v2";
const SMALL_SCREEN_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;
if (stored !== null) return stored === "true";
return window.innerWidth < SMALL_SCREEN_BREAKPOINT;
}
export function useFocusMode() {
const [userPreference, setUserPreference] = useState<boolean>(readInitialPreference);
const [viewportWidth, setViewportWidth] = useState<number>(() => (typeof window !== "undefined" ? window.innerWidth : 1024));
const [smallScreen, setSmallScreen] = useState<boolean>(() => window.innerWidth < SMALL_SCREEN_BREAKPOINT);
const focusMode = smallScreen || userPreference;
useEffect(() => {
if (typeof window === "undefined") return;
const handleResize = () => setViewportWidth(window.innerWidth);
handleResize();
const handleResize = () => setSmallScreen(window.innerWidth < SMALL_SCREEN_BREAKPOINT);
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;
});
const persist = useCallback((next: boolean) => {
setUserPreference(next);
try {
window.localStorage.setItem(FOCUS_MODE_KEY, String(next));
} catch {
// 忽略 localStorage 不可用场景,专注模式仍可在本次会话生效。
}
}, []);
const enterFocusMode = useCallback(() => persist(true), [persist]);
const exitFocusMode = useCallback(() => persist(false), [persist]);
const toggleFocusMode = useCallback(() => persist(!(smallScreen || userPreference)), [persist, smallScreen, userPreference]);
return {
focusMode,
userPreference,
forcedOn,
enterFocusMode,
exitFocusMode,
toggleFocusMode,
};
}