diff --git a/web/src/components/canvas/canvas-create-command-grid.tsx b/web/src/components/canvas/canvas-create-command-grid.tsx index a18cac9d3..3284086ab 100644 --- a/web/src/components/canvas/canvas-create-command-grid.tsx +++ b/web/src/components/canvas/canvas-create-command-grid.tsx @@ -27,17 +27,21 @@ export function CanvasCreateCommandGrid({ commands, variant = "node" }: { comman whileTap={reducedMotion ? undefined : { scale: 0.96 }} transition={aceternityMotion.spring.dock} className={cn( - "group relative min-w-0 border border-black/10 bg-white/70 text-center outline-none transition-colors hover:border-black/20 hover:bg-black/5 focus-visible:ring-2 dark:border-white/10 dark:bg-white/[.04] dark:hover:border-white/20 dark:hover:bg-white/8", - variant === "node" ? "flex h-12 flex-col items-center justify-center gap-1 rounded-[var(--dock-item-radius)]" : "flex h-9 items-center justify-center gap-1.5 rounded-[var(--dock-item-radius)] px-2", + "group relative min-w-0 overflow-hidden border border-black/10 bg-white/70 text-center outline-none transition-colors hover:border-black/20 hover:bg-black/5 focus-visible:ring-2 dark:border-white/10 dark:bg-white/[.04] dark:hover:border-white/20 dark:hover:bg-white/8", + variant === "node" ? "flex h-[var(--dock-create-node-height)] flex-col items-center rounded-[var(--dock-item-radius)]" : "flex h-9 items-center justify-center gap-1.5 rounded-[var(--dock-item-radius)] px-2", )} style={{ color: theme.node.text, "--tw-ring-color": theme.node.muted } as CSSProperties} title={command.label} onMouseDown={(event) => event.stopPropagation()} onClick={command.onClick} > - {command.icon} - {command.label} - {command.badge ? {command.badge} : null} + {variant === "node" ? + {command.badge ? {command.badge} : null} + : null} + + {command.icon} + {command.label} + ))} diff --git a/web/src/components/canvas/canvas-zoom-controls.tsx b/web/src/components/canvas/canvas-zoom-controls.tsx index c5c74266c..7b93208f9 100644 --- a/web/src/components/canvas/canvas-zoom-controls.tsx +++ b/web/src/components/canvas/canvas-zoom-controls.tsx @@ -76,7 +76,9 @@ export function CanvasZoomControls({ scale, onScaleChange, onReset, isMiniMapOpe { id: "zoom-precision", label: "精确缩放", - icon: {Math.round(scale * 100)}%, + wide: true, + quiet: true, + icon: {Math.round(scale * 100)}%, active: precisionOpen, onClick: () => setPrecisionOpen((value) => !value), }, diff --git a/web/src/components/canvas/toolbars/toolbar-settings-modal.tsx b/web/src/components/canvas/toolbars/toolbar-settings-modal.tsx index 091152eb5..017db4bc2 100644 --- a/web/src/components/canvas/toolbars/toolbar-settings-modal.tsx +++ b/web/src/components/canvas/toolbars/toolbar-settings-modal.tsx @@ -1,7 +1,7 @@ import { Modal, Switch } from "antd"; -import { GripVertical, RotateCcw } from "lucide-react"; -import { Reorder, useReducedMotion } from "motion/react"; -import { useEffect, useState } from "react"; +import { GripVertical, RotateCcw, X } from "lucide-react"; +import { motion, useReducedMotion } from "motion/react"; +import { useEffect, useRef, useState } from "react"; import { canvasThemes } from "@/lib/canvas-theme"; import { defaultToolbarPrefs, getToolbarTools, persistToolbarPrefs, readToolbarPrefs, type ToolbarId, type ToolbarPrefs, type ToolContext, type ToolDefinition } from "@/lib/canvas/tool-registry"; @@ -43,6 +43,10 @@ export function ToolbarSettingsModal({ open, onClose, toolbar }: ToolbarSettings const reducedMotion = useReducedMotion(); const [items, setItems] = useState([]); const [toolbarId, setToolbarId] = useState(toolbar); + const draggedItemIdRef = useRef(null); + const dragTargetIdRef = useRef(null); + const [draggedItemId, setDraggedItemId] = useState(null); + const visibleCount = items.filter((item) => item.visible).length; // 当 modal 打开或 toolbar 变化时,加载工具列表与偏好 useEffect(() => { @@ -66,9 +70,34 @@ export function ToolbarSettingsModal({ open, onClose, toolbar }: ToolbarSettings }))); }, [open, toolbar]); - const handleReorder = (newOrder: SettingsItem[]) => { - setItems(newOrder); - persistCurrent(newOrder); + const handleDragStart = (id: string) => { + draggedItemIdRef.current = id; + dragTargetIdRef.current = id; + setDraggedItemId(id); + }; + + const handleDragEnter = (targetId: string) => { + const sourceId = draggedItemIdRef.current; + if (!sourceId || dragTargetIdRef.current === targetId) return; + dragTargetIdRef.current = targetId; + + setItems((current) => { + const sourceIndex = current.findIndex((item) => item.id === sourceId); + const targetIndex = current.findIndex((item) => item.id === targetId); + if (sourceIndex < 0 || targetIndex < 0) return current; + + const next = [...current]; + const [movedItem] = next.splice(sourceIndex, 1); + next.splice(targetIndex, 0, movedItem); + persistCurrent(next); + return next; + }); + }; + + const handleDragEnd = () => { + draggedItemIdRef.current = null; + dragTargetIdRef.current = null; + setDraggedItemId(null); }; const handleToggleVisible = (id: string, visible: boolean) => { @@ -102,54 +131,103 @@ export function ToolbarSettingsModal({ open, onClose, toolbar }: ToolbarSettings return ( -
- 拖拽排序 · 开关控制显隐 +
+
+

工具栏设置

+

拖动调整顺序,关闭不常用入口

+
+ +
+
+ 已显示 {visibleCount}/{items.length}
- +
{items.map((item) => ( - - - {item.icon} - {item.label} - handleToggleVisible(item.id, checked)} /> - + item={item} + reducedMotion={Boolean(reducedMotion)} + theme={theme} + dragging={draggedItemId === item.id} + onToggleVisible={handleToggleVisible} + onDragStart={handleDragStart} + onDragEnter={handleDragEnter} + onDragEnd={handleDragEnd} + /> ))} - +
); } +function ToolbarSettingsItem({ item, reducedMotion, theme, dragging, onToggleVisible, onDragStart, onDragEnter, onDragEnd }: { item: SettingsItem; reducedMotion: boolean; theme: (typeof canvasThemes)[keyof typeof canvasThemes]; dragging: boolean; onToggleVisible: (id: string, visible: boolean) => void; onDragStart: (id: string) => void; onDragEnter: (id: string) => void; onDragEnd: () => void }) { + return ( + onDragEnter(item.id)} + onDragOver={(event) => event.preventDefault()} + > +
+ + onToggleVisible(item.id, checked)} aria-label={`${item.visible ? "隐藏" : "显示"}${item.label}`} /> +
+
+ + {item.icon} + + {item.label} +
+
+ ); +} + function resolveLabel(tool: ToolDefinition, ctx: ToolContext): string { return typeof tool.label === "function" ? tool.label(ctx) : tool.label; } diff --git a/web/src/components/ui/aceternity/floating-dock.tsx b/web/src/components/ui/aceternity/floating-dock.tsx index f6442b4af..f16df275a 100644 --- a/web/src/components/ui/aceternity/floating-dock.tsx +++ b/web/src/components/ui/aceternity/floating-dock.tsx @@ -10,6 +10,8 @@ export type FloatingDockCommand = { label: string; displayLabel?: string; icon: ReactNode; + wide?: boolean; + quiet?: boolean; onClick?: (event: MouseEvent) => void; active?: boolean; disabled?: boolean; @@ -159,6 +161,7 @@ function DockCommandButton({ command, mouseX, metrics, motionEnabled, compact, s const iconTarget = useTransform(distance, (value) => proximitySize(value, metrics.icon, metrics.iconMagnified, metrics.distance, motionEnabled)); const itemSize = useSpring(itemTarget, aceternityMotion.spring.dock); const iconSize = useSpring(iconTarget, aceternityMotion.spring.dock); + // 鼠标点击产生的 focus 不能阻塞提示收起,只有键盘可见焦点才持续显示提示。 const showTooltip = !showLabel && (hovered || focused) && !command.disabled; if (showLabel) { @@ -187,7 +190,7 @@ function DockCommandButton({ command, mouseX, metrics, motionEnabled, compact, s } return ( - + {/* 放大项留在 Flex 流内,由布局推开邻项,保持 Aceternity Floating Dock 的空间关系。 */} setHovered(true)} onMouseLeave={() => setHovered(false)} - onFocus={() => setFocused(true)} + onFocus={(event) => setFocused(event.currentTarget.matches(":focus-visible"))} onBlur={() => setFocused(false)} + onMouseDown={() => setFocused(false)} onClick={command.onClick} > - + {command.icon} @@ -212,7 +216,7 @@ function DockCommandButton({ command, mouseX, metrics, motionEnabled, compact, s diff --git a/web/src/pages/canvas/canvas-project-top-bar.tsx b/web/src/pages/canvas/canvas-project-top-bar.tsx index c5da5d6e2..f5d028eec 100644 --- a/web/src/pages/canvas/canvas-project-top-bar.tsx +++ b/web/src/pages/canvas/canvas-project-top-bar.tsx @@ -294,29 +294,26 @@ function CanvasWorkspaceModeSwitch({ mode, onChange }: { mode: CanvasWorkspaceMo
setOpen((value) => !value)} > - + {simple ? : } - - 工作空间 - {simple ? "简洁模式" : "专业模式"} - - - + {simple ? "简洁模式" : "专业模式"} + + -
+
{open ? ( -
} title="简洁模式" description="保留核心创作路径,降低参数密度" theme={theme} onClick={() => selectMode("simple")} /> } title="专业模式" description="显示完整节点、导演台与生成控制" theme={theme} onClick={() => selectMode("professional")} /> @@ -348,16 +344,15 @@ function ModeOption({ active, motionEnabled, icon, title, description, theme, on type="button" role="option" aria-selected={active} - whileHover={motionEnabled ? { x: 3 } : undefined} whileTap={motionEnabled ? { scale: 0.98 } : undefined} transition={aceternityMotion.spring.dock} - className="group flex min-h-11 w-full items-center gap-2 rounded-[var(--r-lg)] border px-2 py-1.5 text-left outline-none focus-visible:ring-2" - style={{ background: active ? theme.accent.primarySoft : "transparent", borderColor: active ? theme.spatial.glowStrong : "transparent", color: theme.node.text }} + className="group flex min-h-14 w-full items-center gap-3 rounded-[var(--r-md)] px-3 py-2 text-left outline-none transition-colors hover:bg-black/5 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-0 dark:hover:bg-white/8" + style={{ background: active ? theme.accent.primarySoft : "transparent", color: theme.node.text, outlineColor: theme.accent.primary }} onClick={onClick} > - {icon} - {title}{description} - + {icon} + {title}{description} + ); } diff --git a/web/src/styles/globals.css b/web/src/styles/globals.css index 17178ea49..b7335fe01 100644 --- a/web/src/styles/globals.css +++ b/web/src/styles/globals.css @@ -485,6 +485,8 @@ --dock-shadow: var(--shadow-lg); --dock-item-radius: var(--r-md); /* 8px */ --dock-item-radius-labeled: var(--r-lg); /* 12px */ + --dock-precision-width: var(--space-12); + --dock-create-node-height: calc(var(--space-12) + var(--space-2)); --dock-z-popover: 30; /* dock 子面板在 toolbar 根 stacking context 内的层级(见 ADR-001)*/ /* Node 组件(画布节点,对应 #97 节点视觉规范)*/ @@ -3387,6 +3389,52 @@ backdrop-filter: blur(var(--space-2)); } + .canvas-toolbar-settings-modal { + max-width: calc(100vw - var(--space-8)); + } + + .canvas-toolbar-settings-modal .ant-modal-container, + .canvas-toolbar-settings-modal .ant-modal-content { + overflow: hidden; + border: 0 !important; + border-radius: var(--r-xl); + padding: 0 !important; + background: var(--cn-spatial-elevated) !important; + } + + .canvas-toolbar-settings-modal .ant-modal-content { + box-shadow: var(--workspace-overlay-shadow) !important; + } + + .canvas-toolbar-settings-modal .ant-modal-container { + box-shadow: none !important; + } + + .canvas-toolbar-settings-modal .ant-modal-body { + padding: 0 !important; + } + + .canvas-toolbar-settings-card { + background: var(--cn-spatial-surface); + transition: background-color var(--motion-dur-fast) var(--motion-ease-out), opacity var(--motion-dur-fast) var(--motion-ease-out); + } + + .canvas-toolbar-settings-card:hover { + background: color-mix(in srgb, var(--cn-spatial-surface) 72%, var(--cn-spatial-elevated)); + } + + .canvas-toolbar-settings-card.is-hidden { + background: color-mix(in srgb, var(--cn-spatial-surface) 52%, transparent); + } + + .canvas-toolbar-settings-card.is-hidden .canvas-toolbar-settings-card-content { + opacity: var(--opa-subtle); + } + + .canvas-toolbar-settings-card.is-dragging { + opacity: var(--opa-muted); + } + :where(.ant-select-dropdown, .ant-dropdown-menu, .ant-popover-inner) { border: 1px solid var(--border); box-shadow: 0 20px 56px rgba(15, 23, 42, 0.15), inset 0 1px 0 rgba(255, 255, 255, 0.08) !important; @@ -3583,6 +3631,19 @@ opacity: 1; } + .aceternity-dock-command.is-quiet { + border-color: transparent; + background: transparent; + box-shadow: none; + } + + .aceternity-dock-command.is-quiet:hover:not(:disabled), + .aceternity-dock-command.is-quiet.is-active { + border-color: transparent; + background: var(--dock-command-hover); + box-shadow: none; + } + .aceternity-dock-command.is-danger { color: var(--dock-command-danger); }