feat(canvas): 交互可读性 - 优化连线、字号与工作条

This commit is contained in:
ddCat
2026-09-19 22:29:41 +08:00
parent db9e18da1e
commit ec5abc967e
12 changed files with 210 additions and 57 deletions

View File

@@ -9,6 +9,7 @@ export type FloatingDockCommand = {
id: string;
label: string;
displayLabel?: string;
badge?: ReactNode;
icon: ReactNode;
wide?: boolean;
quiet?: boolean;
@@ -25,6 +26,7 @@ export type FloatingDockSwitchOption = {
label: string;
icon: ReactNode;
value: string;
displayLabel?: string;
};
export type FloatingDockSwitch = {
@@ -237,6 +239,7 @@ function DockCommandButton({ command, mouseX, metrics, motionEnabled, compact, s
>
<span className="grid size-3.5 shrink-0 place-items-center">{command.icon}</span>
<span className="inline-flex h-4 items-center text-[var(--fs-label)] font-medium leading-none">{command.displayLabel || command.label}</span>
{command.badge !== undefined ? <span className="aceternity-dock-command-badge inline-flex min-w-4 items-center justify-center rounded-full px-1 text-[var(--fs-micro)] font-bold leading-4">{command.badge}</span> : null}
</motion.button>
</motion.span>
);
@@ -292,15 +295,17 @@ function DockSwitch({ entry, compact, showLabel, motionEnabled, metrics }: { ent
const [focusedId, setFocusedId] = useState<string | null>(null);
const selectedIndex = Math.max(0, entry.options.findIndex((option) => option.value === entry.value));
const touch = metrics.base >= 40;
const slot = touch ? 32 : compact ? 24 : 26;
const gap = touch ? 10 : compact ? 8 : 10;
const padX = touch ? 7 : compact ? 6 : 7;
const labeled = showLabel || entry.options.some((option) => option.displayLabel);
const slot = labeled ? (touch ? 68 : compact ? 58 : 64) : touch ? 32 : compact ? 24 : 26;
const slotHeight = labeled ? metrics.base : slot;
const gap = labeled ? (touch ? 4 : 3) : touch ? 10 : compact ? 8 : 10;
const padX = labeled ? (touch ? 5 : 4) : touch ? 7 : compact ? 6 : 7;
return (
<span
role="radiogroup"
aria-label={entry.label}
className="aceternity-dock-switch relative flex shrink-0 self-end items-center"
className={cn("aceternity-dock-switch relative flex shrink-0 self-end items-center", labeled && "is-labeled")}
onKeyDown={(event) => {
if (event.key !== "ArrowRight" && event.key !== "ArrowLeft") return;
event.preventDefault();
@@ -315,25 +320,25 @@ function DockSwitch({ entry, compact, showLabel, motionEnabled, metrics }: { ent
>
<motion.span
aria-hidden
className="aceternity-dock-switch-thumb pointer-events-none absolute top-1/2 rounded-full"
className={cn("aceternity-dock-switch-thumb pointer-events-none absolute top-1/2", labeled ? "rounded-[var(--dock-item-radius)]" : "rounded-full")}
initial={false}
animate={{ x: selectedIndex * (slot + gap), y: "-50%" }}
transition={reducedMotion || !motionEnabled ? { duration: 0 } : aceternityMotion.spring.dock}
style={{ width: slot, height: slot, left: padX }}
style={{ width: slot, height: slotHeight, left: padX }}
/>
{entry.options.map((option) => {
const checked = option.value === entry.value;
const showTooltip = !showLabel && (hoveredId === option.id || focusedId === option.id);
const showTooltip = !labeled && (hoveredId === option.id || focusedId === option.id);
return (
<span key={option.id} className="relative">
<span key={option.id} className="relative inline-flex shrink-0">
<button
type="button"
role="radio"
aria-checked={checked}
aria-label={option.label}
title={!motionEnabled ? option.label : undefined}
className="aceternity-dock-switch-option relative z-[1] grid place-items-center rounded-full border-0 outline-none"
style={{ width: slot, height: slot }}
className={cn("aceternity-dock-switch-option relative z-[1] inline-flex items-center justify-center border-0 outline-none", labeled ? "gap-1 rounded-[var(--dock-item-radius)] px-2" : "rounded-full")}
style={{ width: slot, height: slotHeight }}
onMouseEnter={() => setHoveredId(option.id)}
onMouseLeave={() => setHoveredId((current) => current === option.id ? null : current)}
onFocus={(event) => {
@@ -345,7 +350,8 @@ function DockSwitch({ entry, compact, showLabel, motionEnabled, metrics }: { ent
if (!checked) entry.onChange(option.value);
}}
>
<span className={cn("grid place-items-center", touch ? "[&_svg]:size-4" : "[&_svg]:size-[15px]")}>{option.icon}</span>
<span className="grid size-4 shrink-0 place-items-center">{option.icon}</span>
{labeled ? <span className="shrink-0 whitespace-nowrap text-[length:var(--fs-micro)] font-semibold leading-none">{option.displayLabel || option.label}</span> : null}
</button>
<AnimatePresence>
{showTooltip ? (