fix(canvas): 画布工具栏 - 修复设置面板布局与命令标签遮挡
This commit is contained in:
@@ -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}
|
||||
>
|
||||
<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="max-w-full truncate text-[var(--fs-micro)] font-semibold leading-none">{command.label}</span>
|
||||
{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}
|
||||
{variant === "node" ? <span className="flex h-4 w-full shrink-0 items-start justify-end px-1 pt-1">
|
||||
{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}
|
||||
</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>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -76,7 +76,9 @@ export function CanvasZoomControls({ scale, onScaleChange, onReset, isMiniMapOpe
|
||||
{
|
||||
id: "zoom-precision",
|
||||
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,
|
||||
onClick: () => setPrecisionOpen((value) => !value),
|
||||
},
|
||||
|
||||
@@ -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<SettingsItem[]>([]);
|
||||
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 变化时,加载工具列表与偏好
|
||||
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 (
|
||||
<Modal
|
||||
className="canvas-toolbar-settings-modal"
|
||||
open={open}
|
||||
onCancel={onClose}
|
||||
footer={null}
|
||||
title="工具栏设置"
|
||||
width={360}
|
||||
closable={false}
|
||||
width={720}
|
||||
centered
|
||||
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">
|
||||
<span className="text-[var(--fs-label)]" style={{ color: theme.node.muted }}>拖拽排序 · 开关控制显隐</span>
|
||||
<div className="flex items-start justify-between gap-4 px-5 pb-3 pt-5">
|
||||
<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
|
||||
type="button"
|
||||
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"
|
||||
style={{ color: theme.node.muted }}
|
||||
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, outlineColor: theme.accent.primary }}
|
||||
aria-label="恢复默认工具栏设置"
|
||||
>
|
||||
<RotateCcw className="size-3" />
|
||||
恢复默认
|
||||
</button>
|
||||
</div>
|
||||
<Reorder.Group
|
||||
axis="y"
|
||||
values={items}
|
||||
onReorder={handleReorder}
|
||||
className="thin-scrollbar max-h-[60vh] space-y-1 overflow-y-auto px-3 pb-4"
|
||||
>
|
||||
<div className="grid grid-cols-2 gap-2 px-5 pb-5 pt-2 md:grid-cols-5" aria-label="主工具栏顺序">
|
||||
{items.map((item) => (
|
||||
<Reorder.Item
|
||||
<ToolbarSettingsItem
|
||||
key={item.id}
|
||||
value={item}
|
||||
initial={false}
|
||||
animate={reducedMotion ? undefined : { opacity: 1 }}
|
||||
dragTransition={reducedMotion ? undefined : { bounceStiffness: 600, bounceDamping: 40 }}
|
||||
className="flex items-center gap-2 rounded-[var(--dock-item-radius)] border px-2.5 py-2"
|
||||
style={{ background: theme.spatial.surface, borderColor: theme.toolbar.border, color: theme.node.text }}
|
||||
>
|
||||
<GripVertical className="size-4 shrink-0 cursor-grab opacity-40 active:cursor-grabbing" />
|
||||
<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>
|
||||
item={item}
|
||||
reducedMotion={Boolean(reducedMotion)}
|
||||
theme={theme}
|
||||
dragging={draggedItemId === item.id}
|
||||
onToggleVisible={handleToggleVisible}
|
||||
onDragStart={handleDragStart}
|
||||
onDragEnter={handleDragEnter}
|
||||
onDragEnd={handleDragEnd}
|
||||
/>
|
||||
))}
|
||||
</Reorder.Group>
|
||||
</div>
|
||||
</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 {
|
||||
return typeof tool.label === "function" ? tool.label(ctx) : tool.label;
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ export type FloatingDockCommand = {
|
||||
label: string;
|
||||
displayLabel?: string;
|
||||
icon: ReactNode;
|
||||
wide?: boolean;
|
||||
quiet?: boolean;
|
||||
onClick?: (event: MouseEvent<HTMLButtonElement>) => 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 (
|
||||
<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 的空间关系。 */}
|
||||
<motion.button
|
||||
type="button"
|
||||
@@ -195,16 +198,17 @@ function DockCommandButton({ command, mouseX, metrics, motionEnabled, compact, s
|
||||
aria-expanded={command.expands ? command.active || undefined : undefined}
|
||||
aria-pressed={command.expands ? undefined : command.active || undefined}
|
||||
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}
|
||||
transition={aceternityMotion.spring.dock}
|
||||
onMouseEnter={() => 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}
|
||||
>
|
||||
<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}
|
||||
</motion.span>
|
||||
<AnimatePresence>
|
||||
@@ -212,7 +216,7 @@ function DockCommandButton({ command, mouseX, metrics, motionEnabled, compact, s
|
||||
<motion.span
|
||||
initial={{ opacity: 0, y: 7, scale: 0.94 }}
|
||||
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 }}
|
||||
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">
|
||||
<motion.button
|
||||
type="button"
|
||||
whileHover={reducedMotion ? undefined : { y: -1, scale: 1.015 }}
|
||||
whileHover={reducedMotion ? undefined : { y: -1 }}
|
||||
whileTap={reducedMotion ? undefined : { scale: 0.97 }}
|
||||
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"
|
||||
style={{ background: theme.spatial.elevated, borderColor: open ? theme.spatial.glowStrong : theme.toolbar.border, color: theme.node.text, boxShadow: `0 16px 44px ${theme.spatial.shadow}` }}
|
||||
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: open ? theme.spatial.surface : theme.spatial.elevated, color: theme.node.text, boxShadow: "var(--workspace-overlay-shadow)", outlineColor: theme.accent.primary }}
|
||||
aria-haspopup="listbox"
|
||||
aria-expanded={open}
|
||||
aria-label={`当前为${simple ? "简洁" : "专业"}模式,点击切换`}
|
||||
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" />}
|
||||
</span>
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="block text-[var(--fs-micro)] leading-none" style={{ color: theme.node.muted }}>工作空间</span>
|
||||
<span className="mt-0.5 block text-[var(--fs-tiny)] font-semibold leading-none">{simple ? "简洁模式" : "专业模式"}</span>
|
||||
</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" />
|
||||
<span className="min-w-0 flex-1 whitespace-nowrap text-[var(--fs-caption)] font-semibold leading-none">{simple ? "简洁模式" : "专业模式"}</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 }}>
|
||||
<ChevronDown className="size-3" />
|
||||
</motion.span>
|
||||
</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>
|
||||
{open ? (
|
||||
<motion.div
|
||||
@@ -326,10 +323,9 @@ function CanvasWorkspaceModeSwitch({ mode, onChange }: { mode: CanvasWorkspaceMo
|
||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
exit={reducedMotion ? { opacity: 0 } : { opacity: 0, y: -6, scale: 0.95 }}
|
||||
transition={aceternityMotion.spring.panel}
|
||||
className="aceternity-floating-panel w-full overflow-hidden rounded-[var(--panel-radius)] border 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}` }}
|
||||
className="aceternity-floating-panel w-full overflow-hidden rounded-[var(--r-lg)] p-1.5 backdrop-blur-2xl"
|
||||
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={<Settings2 className="size-4" />} title="专业模式" description="显示完整节点、导演台与生成控制" theme={theme} onClick={() => selectMode("professional")} />
|
||||
</motion.div>
|
||||
@@ -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}
|
||||
>
|
||||
<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="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="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-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-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" style={{ color: theme.accent.primary, opacity: active ? 1 : 0 }}><Check className="size-3.5" /></span>
|
||||
</motion.button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user