fix(canvas): 画布工具栏 - 修复设置面板布局与命令标签遮挡
This commit is contained in:
@@ -27,17 +27,21 @@ export function CanvasCreateCommandGrid({ commands, variant = "node" }: { comman
|
|||||||
whileTap={reducedMotion ? undefined : { scale: 0.96 }}
|
whileTap={reducedMotion ? undefined : { scale: 0.96 }}
|
||||||
transition={aceternityMotion.spring.dock}
|
transition={aceternityMotion.spring.dock}
|
||||||
className={cn(
|
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",
|
"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-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",
|
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}
|
style={{ color: theme.node.text, "--tw-ring-color": theme.node.muted } as CSSProperties}
|
||||||
title={command.label}
|
title={command.label}
|
||||||
onMouseDown={(event) => event.stopPropagation()}
|
onMouseDown={(event) => event.stopPropagation()}
|
||||||
onClick={command.onClick}
|
onClick={command.onClick}
|
||||||
>
|
>
|
||||||
<span className="grid size-5 shrink-0 place-items-center opacity-65 transition-opacity group-hover:opacity-100 [&_svg]:size-3.5">{command.icon}</span>
|
{variant === "node" ? <span className="flex h-4 w-full shrink-0 items-start justify-end px-1 pt-1">
|
||||||
<span className="max-w-full truncate text-[var(--fs-micro)] font-semibold leading-none">{command.label}</span>
|
{command.badge ? <span className="inline-flex max-w-full items-center rounded-full border px-1 py-0.5 text-[var(--fs-nano)] font-bold leading-none" style={{ background: theme.toolbar.activeBg, borderColor: theme.toolbar.border, color: theme.node.muted }}>{command.badge}</span> : null}
|
||||||
{command.badge ? <span className="absolute right-1 top-1 rounded-full border px-1 py-0.5 text-[var(--fs-nano)] font-bold leading-none" style={{ background: theme.toolbar.activeBg, borderColor: theme.toolbar.border, color: theme.node.muted }}>{command.badge}</span> : null}
|
</span> : null}
|
||||||
|
<span className="flex min-h-0 w-full min-w-0 flex-1 flex-col items-center justify-center gap-1 overflow-hidden px-1">
|
||||||
|
<span className="grid size-5 shrink-0 place-items-center opacity-65 transition-opacity group-hover:opacity-100 [&_svg]:size-3.5">{command.icon}</span>
|
||||||
|
<span className="block max-w-full truncate whitespace-nowrap text-[var(--fs-micro)] font-semibold leading-none" title={command.label}>{command.label}</span>
|
||||||
|
</span>
|
||||||
</motion.button>
|
</motion.button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -76,7 +76,9 @@ export function CanvasZoomControls({ scale, onScaleChange, onReset, isMiniMapOpe
|
|||||||
{
|
{
|
||||||
id: "zoom-precision",
|
id: "zoom-precision",
|
||||||
label: "精确缩放",
|
label: "精确缩放",
|
||||||
icon: <span className="flex items-baseline text-[var(--fs-micro)] font-bold tabular-nums"><span ref={dockLabelRef}>{Math.round(scale * 100)}</span><span className="ml-px text-[var(--fs-nano)] opacity-55">%</span></span>,
|
wide: true,
|
||||||
|
quiet: true,
|
||||||
|
icon: <span className="inline-flex h-full items-center justify-center whitespace-nowrap text-[var(--fs-caption)] font-semibold leading-none tabular-nums"><span ref={dockLabelRef}>{Math.round(scale * 100)}</span><span className="ml-px text-[var(--fs-micro)] font-medium leading-none opacity-50">%</span></span>,
|
||||||
active: precisionOpen,
|
active: precisionOpen,
|
||||||
onClick: () => setPrecisionOpen((value) => !value),
|
onClick: () => setPrecisionOpen((value) => !value),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Modal, Switch } from "antd";
|
import { Modal, Switch } from "antd";
|
||||||
import { GripVertical, RotateCcw } from "lucide-react";
|
import { GripVertical, RotateCcw, X } from "lucide-react";
|
||||||
import { Reorder, useReducedMotion } from "motion/react";
|
import { motion, useReducedMotion } from "motion/react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
import { canvasThemes } from "@/lib/canvas-theme";
|
import { canvasThemes } from "@/lib/canvas-theme";
|
||||||
import { defaultToolbarPrefs, getToolbarTools, persistToolbarPrefs, readToolbarPrefs, type ToolbarId, type ToolbarPrefs, type ToolContext, type ToolDefinition } from "@/lib/canvas/tool-registry";
|
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 reducedMotion = useReducedMotion();
|
||||||
const [items, setItems] = useState<SettingsItem[]>([]);
|
const [items, setItems] = useState<SettingsItem[]>([]);
|
||||||
const [toolbarId, setToolbarId] = useState<ToolbarId>(toolbar);
|
const [toolbarId, setToolbarId] = useState<ToolbarId>(toolbar);
|
||||||
|
const draggedItemIdRef = useRef<string | null>(null);
|
||||||
|
const dragTargetIdRef = useRef<string | null>(null);
|
||||||
|
const [draggedItemId, setDraggedItemId] = useState<string | null>(null);
|
||||||
|
const visibleCount = items.filter((item) => item.visible).length;
|
||||||
|
|
||||||
// 当 modal 打开或 toolbar 变化时,加载工具列表与偏好
|
// 当 modal 打开或 toolbar 变化时,加载工具列表与偏好
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -66,9 +70,34 @@ export function ToolbarSettingsModal({ open, onClose, toolbar }: ToolbarSettings
|
|||||||
})));
|
})));
|
||||||
}, [open, toolbar]);
|
}, [open, toolbar]);
|
||||||
|
|
||||||
const handleReorder = (newOrder: SettingsItem[]) => {
|
const handleDragStart = (id: string) => {
|
||||||
setItems(newOrder);
|
draggedItemIdRef.current = id;
|
||||||
persistCurrent(newOrder);
|
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) => {
|
const handleToggleVisible = (id: string, visible: boolean) => {
|
||||||
@@ -102,54 +131,103 @@ export function ToolbarSettingsModal({ open, onClose, toolbar }: ToolbarSettings
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
|
className="canvas-toolbar-settings-modal"
|
||||||
open={open}
|
open={open}
|
||||||
onCancel={onClose}
|
onCancel={onClose}
|
||||||
footer={null}
|
footer={null}
|
||||||
title="工具栏设置"
|
closable={false}
|
||||||
width={360}
|
width={720}
|
||||||
centered
|
centered
|
||||||
destroyOnClose
|
destroyOnClose
|
||||||
styles={{ body: { padding: 0 } }}
|
styles={{
|
||||||
|
container: { padding: 0, background: theme.spatial.elevated, border: 0, boxShadow: "none" },
|
||||||
|
body: { padding: 0, background: theme.spatial.elevated },
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between px-4 pb-2 pt-1">
|
<div className="flex items-start justify-between gap-4 px-5 pb-3 pt-5">
|
||||||
<span className="text-[var(--fs-label)]" style={{ color: theme.node.muted }}>拖拽排序 · 开关控制显隐</span>
|
<div className="min-w-0">
|
||||||
|
<h2 className="text-[var(--fs-heading)] font-semibold leading-none">工具栏设置</h2>
|
||||||
|
<p className="mt-2 text-[var(--fs-caption)] leading-none" style={{ color: theme.node.muted }}>拖动调整顺序,关闭不常用入口</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onClose}
|
||||||
|
className="grid size-8 shrink-0 place-items-center rounded-[var(--dock-item-radius)] outline-none transition-colors hover:bg-black/5 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 dark:hover:bg-white/8"
|
||||||
|
style={{ color: theme.node.muted, outlineColor: theme.accent.primary }}
|
||||||
|
aria-label="关闭工具栏设置"
|
||||||
|
>
|
||||||
|
<X className="size-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between gap-3 px-5 pb-2 pt-1">
|
||||||
|
<span className="text-[var(--fs-tiny)] font-medium" style={{ color: theme.node.muted }}>已显示 {visibleCount}/{items.length}</span>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleReset}
|
onClick={handleReset}
|
||||||
className="inline-flex items-center gap-1 rounded-[var(--dock-item-radius)] px-2 py-1 text-[var(--fs-tiny)] font-semibold transition-colors hover:bg-current/5"
|
className="inline-flex h-7 items-center gap-1.5 rounded-[var(--dock-item-radius)] px-2 text-[var(--fs-tiny)] font-medium outline-none transition-colors hover:bg-black/5 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-1 dark:hover:bg-white/8"
|
||||||
style={{ color: theme.node.muted }}
|
style={{ color: theme.node.muted, outlineColor: theme.accent.primary }}
|
||||||
|
aria-label="恢复默认工具栏设置"
|
||||||
>
|
>
|
||||||
<RotateCcw className="size-3" />
|
<RotateCcw className="size-3" />
|
||||||
恢复默认
|
恢复默认
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<Reorder.Group
|
<div className="grid grid-cols-2 gap-2 px-5 pb-5 pt-2 md:grid-cols-5" aria-label="主工具栏顺序">
|
||||||
axis="y"
|
|
||||||
values={items}
|
|
||||||
onReorder={handleReorder}
|
|
||||||
className="thin-scrollbar max-h-[60vh] space-y-1 overflow-y-auto px-3 pb-4"
|
|
||||||
>
|
|
||||||
{items.map((item) => (
|
{items.map((item) => (
|
||||||
<Reorder.Item
|
<ToolbarSettingsItem
|
||||||
key={item.id}
|
key={item.id}
|
||||||
value={item}
|
item={item}
|
||||||
initial={false}
|
reducedMotion={Boolean(reducedMotion)}
|
||||||
animate={reducedMotion ? undefined : { opacity: 1 }}
|
theme={theme}
|
||||||
dragTransition={reducedMotion ? undefined : { bounceStiffness: 600, bounceDamping: 40 }}
|
dragging={draggedItemId === item.id}
|
||||||
className="flex items-center gap-2 rounded-[var(--dock-item-radius)] border px-2.5 py-2"
|
onToggleVisible={handleToggleVisible}
|
||||||
style={{ background: theme.spatial.surface, borderColor: theme.toolbar.border, color: theme.node.text }}
|
onDragStart={handleDragStart}
|
||||||
>
|
onDragEnter={handleDragEnter}
|
||||||
<GripVertical className="size-4 shrink-0 cursor-grab opacity-40 active:cursor-grabbing" />
|
onDragEnd={handleDragEnd}
|
||||||
<span className="grid size-5 shrink-0 place-items-center opacity-70 [&_svg]:size-3.5">{item.icon}</span>
|
/>
|
||||||
<span className="min-w-0 flex-1 truncate text-[var(--fs-label)] font-medium">{item.label}</span>
|
|
||||||
<Switch size="small" checked={item.visible} onChange={(checked) => handleToggleVisible(item.id, checked)} />
|
|
||||||
</Reorder.Item>
|
|
||||||
))}
|
))}
|
||||||
</Reorder.Group>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<motion.div
|
||||||
|
layout={!reducedMotion}
|
||||||
|
transition={reducedMotion ? { duration: 0 } : undefined}
|
||||||
|
className={`canvas-toolbar-settings-card flex h-24 min-w-0 flex-col rounded-[var(--r-md)] p-3 ${item.visible ? "" : "is-hidden"} ${dragging ? "is-dragging" : ""}`}
|
||||||
|
style={{ color: theme.node.text }}
|
||||||
|
onDragEnter={() => onDragEnter(item.id)}
|
||||||
|
onDragOver={(event) => event.preventDefault()}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
draggable
|
||||||
|
className="grid size-6 touch-none cursor-grab place-items-center rounded-[var(--r-sm)] outline-none opacity-35 transition-opacity hover:opacity-70 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-1 active:cursor-grabbing"
|
||||||
|
style={{ color: theme.node.muted, outlineColor: theme.accent.primary }}
|
||||||
|
onDragStart={(event) => {
|
||||||
|
event.dataTransfer.effectAllowed = "move";
|
||||||
|
onDragStart(item.id);
|
||||||
|
}}
|
||||||
|
onDragEnd={onDragEnd}
|
||||||
|
aria-label={`拖动调整${item.label}顺序`}
|
||||||
|
>
|
||||||
|
<GripVertical className="size-4" />
|
||||||
|
</button>
|
||||||
|
<Switch size="small" checked={item.visible} onChange={(checked) => onToggleVisible(item.id, checked)} aria-label={`${item.visible ? "隐藏" : "显示"}${item.label}`} />
|
||||||
|
</div>
|
||||||
|
<div className="canvas-toolbar-settings-card-content mt-auto flex min-w-0 flex-col items-center gap-1">
|
||||||
|
<span className="grid size-8 shrink-0 place-items-center rounded-[var(--r-md)]" style={{ background: theme.toolbar.itemHover, color: theme.node.muted }}>
|
||||||
|
<span className="grid size-4 place-items-center [&_svg]:size-4">{item.icon}</span>
|
||||||
|
</span>
|
||||||
|
<span className="max-w-full whitespace-nowrap text-[var(--fs-caption)] font-medium leading-5" title={item.label}>{item.label}</span>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function resolveLabel(tool: ToolDefinition, ctx: ToolContext): string {
|
function resolveLabel(tool: ToolDefinition, ctx: ToolContext): string {
|
||||||
return typeof tool.label === "function" ? tool.label(ctx) : tool.label;
|
return typeof tool.label === "function" ? tool.label(ctx) : tool.label;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ export type FloatingDockCommand = {
|
|||||||
label: string;
|
label: string;
|
||||||
displayLabel?: string;
|
displayLabel?: string;
|
||||||
icon: ReactNode;
|
icon: ReactNode;
|
||||||
|
wide?: boolean;
|
||||||
|
quiet?: boolean;
|
||||||
onClick?: (event: MouseEvent<HTMLButtonElement>) => void;
|
onClick?: (event: MouseEvent<HTMLButtonElement>) => void;
|
||||||
active?: boolean;
|
active?: boolean;
|
||||||
disabled?: 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 iconTarget = useTransform(distance, (value) => proximitySize(value, metrics.icon, metrics.iconMagnified, metrics.distance, motionEnabled));
|
||||||
const itemSize = useSpring(itemTarget, aceternityMotion.spring.dock);
|
const itemSize = useSpring(itemTarget, aceternityMotion.spring.dock);
|
||||||
const iconSize = useSpring(iconTarget, aceternityMotion.spring.dock);
|
const iconSize = useSpring(iconTarget, aceternityMotion.spring.dock);
|
||||||
|
// 鼠标点击产生的 focus 不能阻塞提示收起,只有键盘可见焦点才持续显示提示。
|
||||||
const showTooltip = !showLabel && (hovered || focused) && !command.disabled;
|
const showTooltip = !showLabel && (hovered || focused) && !command.disabled;
|
||||||
|
|
||||||
if (showLabel) {
|
if (showLabel) {
|
||||||
@@ -187,7 +190,7 @@ function DockCommandButton({ command, mouseX, metrics, motionEnabled, compact, s
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.span ref={ref} className="relative block shrink-0" style={{ width: itemSize, height: itemSize }}>
|
<motion.span ref={ref} className={cn("relative block shrink-0", command.wide && "min-w-[var(--dock-precision-width)]")} style={{ width: itemSize, height: itemSize }}>
|
||||||
{/* 放大项留在 Flex 流内,由布局推开邻项,保持 Aceternity Floating Dock 的空间关系。 */}
|
{/* 放大项留在 Flex 流内,由布局推开邻项,保持 Aceternity Floating Dock 的空间关系。 */}
|
||||||
<motion.button
|
<motion.button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -195,16 +198,17 @@ function DockCommandButton({ command, mouseX, metrics, motionEnabled, compact, s
|
|||||||
aria-expanded={command.expands ? command.active || undefined : undefined}
|
aria-expanded={command.expands ? command.active || undefined : undefined}
|
||||||
aria-pressed={command.expands ? undefined : command.active || undefined}
|
aria-pressed={command.expands ? undefined : command.active || undefined}
|
||||||
disabled={command.disabled}
|
disabled={command.disabled}
|
||||||
className={cn("aceternity-dock-command group relative grid size-full place-items-center rounded-full border outline-none", command.active && "is-active", command.danger && "is-danger")}
|
className={cn("aceternity-dock-command group relative grid size-full place-items-center rounded-full border outline-none", command.quiet && "is-quiet", command.active && "is-active", command.danger && "is-danger")}
|
||||||
whileTap={motionEnabled && !command.disabled ? { scale: 0.92 } : undefined}
|
whileTap={motionEnabled && !command.disabled ? { scale: 0.92 } : undefined}
|
||||||
transition={aceternityMotion.spring.dock}
|
transition={aceternityMotion.spring.dock}
|
||||||
onMouseEnter={() => setHovered(true)}
|
onMouseEnter={() => setHovered(true)}
|
||||||
onMouseLeave={() => setHovered(false)}
|
onMouseLeave={() => setHovered(false)}
|
||||||
onFocus={() => setFocused(true)}
|
onFocus={(event) => setFocused(event.currentTarget.matches(":focus-visible"))}
|
||||||
onBlur={() => setFocused(false)}
|
onBlur={() => setFocused(false)}
|
||||||
|
onMouseDown={() => setFocused(false)}
|
||||||
onClick={command.onClick}
|
onClick={command.onClick}
|
||||||
>
|
>
|
||||||
<motion.span className="grid place-items-center" style={{ width: iconSize, height: iconSize }}>
|
<motion.span className={cn("grid place-items-center", command.wide && "w-full")} style={command.wide ? { height: iconSize } : { width: iconSize, height: iconSize }}>
|
||||||
{command.icon}
|
{command.icon}
|
||||||
</motion.span>
|
</motion.span>
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
@@ -212,7 +216,7 @@ function DockCommandButton({ command, mouseX, metrics, motionEnabled, compact, s
|
|||||||
<motion.span
|
<motion.span
|
||||||
initial={{ opacity: 0, y: 7, scale: 0.94 }}
|
initial={{ opacity: 0, y: 7, scale: 0.94 }}
|
||||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||||
exit={{ opacity: 0, y: 4, scale: 0.96 }}
|
exit={{ opacity: 0, y: 4, scale: 0.96, transition: { duration: 0 } }}
|
||||||
transition={{ duration: aceternityMotion.duration.instant, ease: aceternityMotion.easing.enter }}
|
transition={{ duration: aceternityMotion.duration.instant, ease: aceternityMotion.easing.enter }}
|
||||||
className={cn("aceternity-dock-tooltip pointer-events-none absolute left-1/2 z-[140] -translate-x-1/2 whitespace-nowrap border font-medium shadow-xl backdrop-blur-xl", compact ? "-top-7 rounded-md px-1.5 py-0.5 text-[var(--fs-micro)]" : "-top-8 rounded-md px-2 py-1 text-[var(--fs-tiny)]")}
|
className={cn("aceternity-dock-tooltip pointer-events-none absolute left-1/2 z-[140] -translate-x-1/2 whitespace-nowrap border font-medium shadow-xl backdrop-blur-xl", compact ? "-top-7 rounded-md px-1.5 py-0.5 text-[var(--fs-micro)]" : "-top-8 rounded-md px-2 py-1 text-[var(--fs-tiny)]")}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -294,29 +294,26 @@ function CanvasWorkspaceModeSwitch({ mode, onChange }: { mode: CanvasWorkspaceMo
|
|||||||
<div ref={rootRef} className="aceternity-mode-switch pointer-events-auto absolute left-1/2 top-2 z-[var(--dock-z-popover)] -translate-x-1/2">
|
<div ref={rootRef} className="aceternity-mode-switch pointer-events-auto absolute left-1/2 top-2 z-[var(--dock-z-popover)] -translate-x-1/2">
|
||||||
<motion.button
|
<motion.button
|
||||||
type="button"
|
type="button"
|
||||||
whileHover={reducedMotion ? undefined : { y: -1, scale: 1.015 }}
|
whileHover={reducedMotion ? undefined : { y: -1 }}
|
||||||
whileTap={reducedMotion ? undefined : { scale: 0.97 }}
|
whileTap={reducedMotion ? undefined : { scale: 0.97 }}
|
||||||
transition={aceternityMotion.spring.dock}
|
transition={aceternityMotion.spring.dock}
|
||||||
className="flex h-8 min-w-[112px] items-center gap-1.5 rounded-full border px-2 text-left outline-none backdrop-blur-2xl focus-visible:ring-2"
|
className="flex h-9 min-w-28 items-center gap-2 rounded-full px-2.5 text-left outline-none backdrop-blur-2xl focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2"
|
||||||
style={{ background: theme.spatial.elevated, borderColor: open ? theme.spatial.glowStrong : theme.toolbar.border, color: theme.node.text, boxShadow: `0 16px 44px ${theme.spatial.shadow}` }}
|
style={{ background: open ? theme.spatial.surface : theme.spatial.elevated, color: theme.node.text, boxShadow: "var(--workspace-overlay-shadow)", outlineColor: theme.accent.primary }}
|
||||||
aria-haspopup="listbox"
|
aria-haspopup="listbox"
|
||||||
aria-expanded={open}
|
aria-expanded={open}
|
||||||
aria-label={`当前为${simple ? "简洁" : "专业"}模式,点击切换`}
|
aria-label={`当前为${simple ? "简洁" : "专业"}模式,点击切换`}
|
||||||
onClick={() => setOpen((value) => !value)}
|
onClick={() => setOpen((value) => !value)}
|
||||||
>
|
>
|
||||||
<span className="grid size-6 shrink-0 place-items-center rounded-full border" style={{ background: theme.spatial.surface, borderColor: theme.toolbar.border, color: theme.accent.primary }}>
|
<span className="grid size-6 shrink-0 place-items-center rounded-full" style={{ background: theme.toolbar.itemHover, color: theme.accent.primary }}>
|
||||||
{simple ? <Sparkles className="size-3" /> : <Settings2 className="size-3" />}
|
{simple ? <Sparkles className="size-3" /> : <Settings2 className="size-3" />}
|
||||||
</span>
|
</span>
|
||||||
<span className="min-w-0 flex-1">
|
<span className="min-w-0 flex-1 whitespace-nowrap text-[var(--fs-caption)] font-semibold leading-none">{simple ? "简洁模式" : "专业模式"}</span>
|
||||||
<span className="block text-[var(--fs-micro)] leading-none" style={{ color: theme.node.muted }}>工作空间</span>
|
<motion.span animate={{ rotate: open ? 180 : 0 }} transition={reducedMotion ? { duration: 0 } : aceternityMotion.spring.dock} className="grid size-4 place-items-center" style={{ color: theme.node.muted }}>
|
||||||
<span className="mt-0.5 block text-[var(--fs-tiny)] font-semibold leading-none">{simple ? "简洁模式" : "专业模式"}</span>
|
<ChevronDown className="size-3" />
|
||||||
</span>
|
|
||||||
<motion.span animate={{ rotate: open ? 180 : 0 }} transition={reducedMotion ? { duration: 0 } : aceternityMotion.spring.dock} className="grid size-5 place-items-center rounded-full" style={{ background: theme.toolbar.itemHover }}>
|
|
||||||
<ChevronDown className="size-2.5" />
|
|
||||||
</motion.span>
|
</motion.span>
|
||||||
</motion.button>
|
</motion.button>
|
||||||
|
|
||||||
<div className="absolute left-1/2 top-[38px] w-[236px] -translate-x-1/2">
|
<div className="absolute left-1/2 top-11 w-72 -translate-x-1/2" style={{ maxWidth: "calc(100vw - var(--space-8))" }}>
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{open ? (
|
{open ? (
|
||||||
<motion.div
|
<motion.div
|
||||||
@@ -326,10 +323,9 @@ function CanvasWorkspaceModeSwitch({ mode, onChange }: { mode: CanvasWorkspaceMo
|
|||||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||||
exit={reducedMotion ? { opacity: 0 } : { opacity: 0, y: -6, scale: 0.95 }}
|
exit={reducedMotion ? { opacity: 0 } : { opacity: 0, y: -6, scale: 0.95 }}
|
||||||
transition={aceternityMotion.spring.panel}
|
transition={aceternityMotion.spring.panel}
|
||||||
className="aceternity-floating-panel w-full overflow-hidden rounded-[var(--panel-radius)] border p-1.5 backdrop-blur-2xl"
|
className="aceternity-floating-panel w-full overflow-hidden rounded-[var(--r-lg)] p-1.5 backdrop-blur-2xl"
|
||||||
style={{ background: theme.spatial.elevated, borderColor: theme.toolbar.border, color: theme.node.text, boxShadow: `0 28px 80px ${theme.spatial.shadow}` }}
|
style={{ background: theme.spatial.elevated, color: theme.node.text, boxShadow: "var(--workspace-overlay-shadow)" }}
|
||||||
>
|
>
|
||||||
<div className="absolute inset-x-10 top-0 h-px" style={{ background: `linear-gradient(90deg, transparent, ${theme.spatial.glowStrong}, transparent)` }} />
|
|
||||||
<ModeOption active={simple} motionEnabled={!reducedMotion} icon={<Sparkles className="size-4" />} title="简洁模式" description="保留核心创作路径,降低参数密度" theme={theme} onClick={() => selectMode("simple")} />
|
<ModeOption active={simple} motionEnabled={!reducedMotion} icon={<Sparkles className="size-4" />} title="简洁模式" description="保留核心创作路径,降低参数密度" theme={theme} onClick={() => selectMode("simple")} />
|
||||||
<ModeOption active={!simple} motionEnabled={!reducedMotion} icon={<Settings2 className="size-4" />} title="专业模式" description="显示完整节点、导演台与生成控制" theme={theme} onClick={() => selectMode("professional")} />
|
<ModeOption active={!simple} motionEnabled={!reducedMotion} icon={<Settings2 className="size-4" />} title="专业模式" description="显示完整节点、导演台与生成控制" theme={theme} onClick={() => selectMode("professional")} />
|
||||||
</motion.div>
|
</motion.div>
|
||||||
@@ -348,16 +344,15 @@ function ModeOption({ active, motionEnabled, icon, title, description, theme, on
|
|||||||
type="button"
|
type="button"
|
||||||
role="option"
|
role="option"
|
||||||
aria-selected={active}
|
aria-selected={active}
|
||||||
whileHover={motionEnabled ? { x: 3 } : undefined}
|
|
||||||
whileTap={motionEnabled ? { scale: 0.98 } : undefined}
|
whileTap={motionEnabled ? { scale: 0.98 } : undefined}
|
||||||
transition={aceternityMotion.spring.dock}
|
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"
|
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", borderColor: active ? theme.spatial.glowStrong : "transparent", color: theme.node.text }}
|
style={{ background: active ? theme.accent.primarySoft : "transparent", color: theme.node.text, outlineColor: theme.accent.primary }}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
>
|
>
|
||||||
<span className="grid size-8 shrink-0 place-items-center rounded-[var(--dock-item-radius)] border [&_svg]:size-3.5" style={{ background: theme.spatial.surface, borderColor: theme.toolbar.border, color: active ? theme.accent.primary : theme.node.muted }}>{icon}</span>
|
<span className="grid size-8 shrink-0 place-items-center rounded-[var(--dock-item-radius)] [&_svg]:size-3.5" style={{ background: theme.spatial.surface, color: active ? theme.accent.primary : theme.node.muted }}>{icon}</span>
|
||||||
<span className="min-w-0 flex-1"><span className="block text-[var(--fs-tiny)] font-semibold">{title}</span><span className="mt-0.5 block text-[var(--fs-micro)]" style={{ color: theme.node.muted }}>{description}</span></span>
|
<span className="min-w-0 flex-1"><span className="block text-[var(--fs-body)] font-semibold leading-none">{title}</span><span className="mt-1.5 block text-[var(--fs-caption)] leading-5" style={{ color: theme.node.muted }}>{description}</span></span>
|
||||||
<span className="grid size-5 shrink-0 place-items-center rounded-full border transition-opacity" style={{ background: active ? theme.accent.primary : theme.spatial.surface, borderColor: active ? theme.accent.primary : theme.toolbar.border, color: active ? "white" : theme.node.muted, opacity: active ? 1 : 0.28 }}><Check className="size-3" /></span>
|
<span className="grid size-5 shrink-0 place-items-center" style={{ color: theme.accent.primary, opacity: active ? 1 : 0 }}><Check className="size-3.5" /></span>
|
||||||
</motion.button>
|
</motion.button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -485,6 +485,8 @@
|
|||||||
--dock-shadow: var(--shadow-lg);
|
--dock-shadow: var(--shadow-lg);
|
||||||
--dock-item-radius: var(--r-md); /* 8px */
|
--dock-item-radius: var(--r-md); /* 8px */
|
||||||
--dock-item-radius-labeled: var(--r-lg); /* 12px */
|
--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)*/
|
--dock-z-popover: 30; /* dock 子面板在 toolbar 根 stacking context 内的层级(见 ADR-001)*/
|
||||||
|
|
||||||
/* Node 组件(画布节点,对应 #97 节点视觉规范)*/
|
/* Node 组件(画布节点,对应 #97 节点视觉规范)*/
|
||||||
@@ -3387,6 +3389,52 @@
|
|||||||
backdrop-filter: blur(var(--space-2));
|
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) {
|
:where(.ant-select-dropdown, .ant-dropdown-menu, .ant-popover-inner) {
|
||||||
border: 1px solid var(--border);
|
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;
|
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;
|
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 {
|
.aceternity-dock-command.is-danger {
|
||||||
color: var(--dock-command-danger);
|
color: var(--dock-command-danger);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user