feat(canvas): 工具栏统一化 — 注册表驱动+上下文感知+自定义排序+危险隔离 #95 (#105)

将三个硬编码工具栏(主工具栏/多选工具栏/节点悬停工具栏)统一改造为注册表驱动体系:

- 新增 tool-registry 核心基础设施(ToolDefinition/ToolContext/ToolbarHandlers 类型、
  注册表解析器 resolveToolbarEntries/resolveToolbarTools、偏好持久化 readToolbarPrefs/persistToolbarPrefs)
- 提取四个工具定义文件(main/selection/node-hover/add-node-menu),通过 applicable 谓词实现上下文感知
- FloatingDock 新增 expands 字段(aria-expanded 语义修正)和 danger-group 分组渲染(危险操作视觉隔离)
- 主工具栏新增工具栏设置面板(ToolbarSettingsModal),支持拖拽排序+显隐控制(motion/react Reorder,零新增依赖)
- 偏好持久化到 scopedLocalStorage,按工具栏独立存储

Co-authored-by: sikongyue <3155157671@qq.com>
This commit is contained in:
爱笑的毛毛虫andsikongyue authored and GitHub committed 2026-08-03 23:22:44 +08:00
1 parent acaa4bb69f
commit 0c4c258a52
15 files changed
+1112 -112

No files matched your search

@@ -1,10 +1,10 @@
import type { RefObject } from "react";
import { AlignHorizontalJustifyCenter, AlignHorizontalJustifyEnd, AlignHorizontalJustifyStart, AlignHorizontalSpaceAround, AlignHorizontalSpaceBetween, AlignVerticalJustifyCenter, AlignVerticalJustifyEnd, AlignVerticalJustifyStart, AlignVerticalSpaceAround, AlignVerticalSpaceBetween, Film, FolderTree, Grid3X3, LayoutTemplate, LoaderCircle, Workflow } from "lucide-react";
import { CanvasSelectionToolbar } from "@/components/canvas/canvas-workspace-overlays";
import { FloatingDock, type FloatingDockEntry } from "@/components/ui/aceternity/floating-dock";
import { FloatingDock } from "@/components/ui/aceternity/floating-dock";
import { canvasThemes } from "@/lib/canvas-theme";
import { canvasDockStyle } from "@/lib/canvas/canvas-aceternity-style";
import { defaultToolbarPrefs, readToolbarPrefs, resolveToolbarEntries, type ToolContext, type ToolbarHandlers } from "@/lib/canvas/tool-registry";
import type { CanvasAlignmentMode } from "@/lib/canvas/canvas-layout";
import { useThemeStore } from "@/stores/use-theme-store";
@@ -23,25 +23,30 @@ type CanvasProjectSelectionToolbarProps = {
export function CanvasProjectSelectionToolbar({ anchorRef, containerRef, count, selectedVideoCount, mergingVideos, onAlign, onArrange, onCreateStoryboard, onCreateReferenceGroup, onMergeVideos }: CanvasProjectSelectionToolbarProps) {
const theme = canvasThemes[useThemeStore((state) => state.theme)];
const items: FloatingDockEntry[] = [
{ id: "selection-align-left", label: "左对齐", icon: <AlignHorizontalJustifyStart />, onClick: () => onAlign("left") },
{ id: "selection-align-center-x", label: "水平居中", icon: <AlignHorizontalJustifyCenter />, onClick: () => onAlign("centerX") },
{ id: "selection-align-right", label: "右对齐", icon: <AlignHorizontalJustifyEnd />, onClick: () => onAlign("right") },
{ id: "selection-align-top", label: "顶对齐", icon: <AlignVerticalJustifyStart />, onClick: () => onAlign("top") },
{ id: "selection-align-center-y", label: "垂直居中", icon: <AlignVerticalJustifyCenter />, onClick: () => onAlign("centerY") },
{ id: "selection-align-bottom", label: "底对齐", icon: <AlignVerticalJustifyEnd />, onClick: () => onAlign("bottom") },
{ id: "selection-distribute-x", label: "水平等距", icon: <AlignHorizontalSpaceBetween />, disabled: count < 3, onClick: () => onAlign("distributeX") },
{ id: "selection-distribute-y", label: "垂直等距", icon: <AlignVerticalSpaceBetween />, disabled: count < 3, onClick: () => onAlign("distributeY") },
{ kind: "separator", id: "selection-arrange-separator" },
{ id: "selection-arrange-row", label: "横向排列", icon: <AlignHorizontalSpaceAround />, onClick: () => onArrange("row") },
{ id: "selection-arrange-column", label: "纵向排列", icon: <AlignVerticalSpaceAround />, onClick: () => onArrange("column") },
{ id: "selection-arrange-grid", label: "宫格排列", icon: <Grid3X3 />, onClick: () => onArrange("grid") },
{ id: "selection-arrange-flow", label: "按连线整理", icon: <Workflow />, onClick: () => onArrange("flow") },
{ kind: "separator", id: "selection-group-separator" },
{ id: "selection-create-storyboard", label: "创建分镜组", icon: <LayoutTemplate />, disabled: count < 2, onClick: onCreateStoryboard },
{ id: "selection-create-reference-group", label: "创建引用组", icon: <FolderTree />, disabled: count < 2, onClick: onCreateReferenceGroup },
...(selectedVideoCount >= 2 ? [{ id: "selection-merge-videos", label: `合并选中视频(${selectedVideoCount})`, icon: mergingVideos ? <LoaderCircle className="animate-spin" /> : <Film />, disabled: mergingVideos, onClick: onMergeVideos }] : []),
];
const handlers = {
onAlign, onArrange, onCreateStoryboard, onCreateReferenceGroup, onMergeVideos,
} as Partial<ToolbarHandlers> as ToolbarHandlers;
const ctx: ToolContext = {
selectedCount: count,
selectedNodeTypes: new Set(),
selectedVideoCount,
canvasTool: "move",
workspaceMode: "professional",
isProjectLinked: false,
canUndo: false,
canRedo: false,
extractingVideoFrame: false,
mergingVideos,
addPanelOpen: false,
appearancePanelOpen: false,
settingsPanelOpen: false,
handlers,
};
const prefs = readToolbarPrefs("selection") ?? defaultToolbarPrefs("selection");
const items = resolveToolbarEntries("selection", ctx, prefs);
return (
<CanvasSelectionToolbar anchorRef={anchorRef} containerRef={containerRef} count={count}>