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

@@ -0,0 +1,76 @@
import { useCallback, useRef, useState, type ReactNode } from "react";
// 根据视口宽度动态计算面板宽度约束,避免小屏幕上面板挤压画布
export function getPanelWidthBounds(): { min: number; max: number } {
const vw = typeof window !== "undefined" ? window.innerWidth : 1280;
if (vw < 768) return { min: 260, max: 360 };
if (vw < 1024) return { min: 280, max: 440 };
if (vw < 1440) return { min: 320, max: 560 };
return { min: 360, max: 760 };
}
// 智能体面板列包裹器:承载面板宽度管理、resize 拖拽和顶部停靠偏移。
// 面板始终作为 flex 子元素避让画布;topInset 用于避让绝对定位的顶部栏(沉浸专注时顶部栏隐藏,传 0)。
// closing 期间保持宽度和渲染,让面板播放滑出动画;动画结束后由父组件卸载列。
export function AssistantPanelColumn({
width,
closing,
topInset,
onWidthChange,
children,
}: {
width: number;
closing: boolean;
topInset: string;
onWidthChange: (width: number) => void;
children: (resizing: boolean) => ReactNode;
}) {
const columnRef = useRef<HTMLDivElement>(null);
const [resizing, setResizing] = useState(false);
// 拖拽时列右边缘固定(flex 末位),左边缘随鼠标移动。
const startResize = useCallback((event: React.MouseEvent) => {
event.preventDefault();
const rightEdge = columnRef.current?.getBoundingClientRect().right ?? 0;
const { min, max } = getPanelWidthBounds();
const move = (e: MouseEvent) => {
onWidthChange(Math.min(max, Math.max(min, rightEdge - e.clientX)));
};
const stop = () => {
setResizing(false);
document.body.style.cursor = "";
document.body.style.userSelect = "";
document.removeEventListener("mousemove", move);
document.removeEventListener("mouseup", stop);
};
setResizing(true);
document.body.style.cursor = "col-resize";
document.body.style.userSelect = "none";
document.addEventListener("mousemove", move);
document.addEventListener("mouseup", stop);
}, [onWidthChange]);
return (
<div
ref={columnRef}
className="relative flex shrink-0 overflow-hidden"
style={{
width,
paddingTop: topInset,
transition: resizing ? "none" : "width var(--motion-dur-base-calc) var(--motion-ease-out), padding-top var(--motion-dur-base-calc) var(--motion-ease-out)",
}}
>
<div className="h-full w-full">
{!closing ? (
<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="调整右侧面板宽度"
/>
) : null}
{children(resizing)}
</div>
</div>
);
}