feat(web): 工作台与创作区 - 收敛彩虹选中色并补中性滑动指示与边框高光
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
86
web/src/components/ui/aceternity/hover-border-gradient.tsx
Normal file
86
web/src/components/ui/aceternity/hover-border-gradient.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
import { motion, useReducedMotion } from "motion/react";
|
||||
import { useEffect, useState, type ComponentPropsWithoutRef, type ElementType, type MouseEvent, type ReactNode } from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type Direction = "TOP" | "LEFT" | "BOTTOM" | "RIGHT";
|
||||
|
||||
type HoverBorderGradientProps<T extends ElementType = "button"> = {
|
||||
as?: T;
|
||||
containerClassName?: string;
|
||||
className?: string;
|
||||
duration?: number;
|
||||
clockwise?: boolean;
|
||||
children?: ReactNode;
|
||||
} & Omit<ComponentPropsWithoutRef<T>, "as" | "children">;
|
||||
|
||||
const movingMap: Record<Direction, string> = {
|
||||
TOP: "radial-gradient(20.7% 50% at 50% 0%, color-mix(in srgb, var(--user-ink) 38%, transparent) 0%, transparent 100%)",
|
||||
LEFT: "radial-gradient(16.6% 43.1% at 0% 50%, color-mix(in srgb, var(--user-ink) 38%, transparent) 0%, transparent 100%)",
|
||||
BOTTOM: "radial-gradient(20.7% 50% at 50% 100%, color-mix(in srgb, var(--user-ink) 38%, transparent) 0%, transparent 100%)",
|
||||
RIGHT: "radial-gradient(16.2% 41.2% at 100% 50%, color-mix(in srgb, var(--user-ink) 38%, transparent) 0%, transparent 100%)",
|
||||
};
|
||||
|
||||
const highlight = "radial-gradient(75% 181% at 50% 50%, color-mix(in srgb, var(--user-ink) 22%, transparent) 0%, transparent 100%)";
|
||||
|
||||
function rotateDirection(current: Direction, clockwise: boolean): Direction {
|
||||
const directions: Direction[] = clockwise
|
||||
? ["TOP", "LEFT", "BOTTOM", "RIGHT"]
|
||||
: ["TOP", "RIGHT", "BOTTOM", "LEFT"];
|
||||
const index = directions.indexOf(current);
|
||||
return directions[(index - 1 + directions.length) % directions.length];
|
||||
}
|
||||
|
||||
// Aceternity Hover Border Gradient:中性墨色描边,不用彩色高光。
|
||||
export function HoverBorderGradient<T extends ElementType = "button">({
|
||||
children,
|
||||
containerClassName,
|
||||
className,
|
||||
as,
|
||||
duration = 1,
|
||||
clockwise = true,
|
||||
onMouseEnter,
|
||||
onMouseLeave,
|
||||
...props
|
||||
}: HoverBorderGradientProps<T>) {
|
||||
const Tag = (as || "button") as ElementType;
|
||||
const reducedMotion = useReducedMotion();
|
||||
const [hovered, setHovered] = useState(false);
|
||||
const [direction, setDirection] = useState<Direction>("TOP");
|
||||
|
||||
useEffect(() => {
|
||||
if (reducedMotion || hovered) return;
|
||||
const interval = window.setInterval(() => {
|
||||
setDirection((current) => rotateDirection(current, clockwise));
|
||||
}, duration * 1000);
|
||||
return () => window.clearInterval(interval);
|
||||
}, [clockwise, duration, hovered, reducedMotion]);
|
||||
|
||||
return (
|
||||
<Tag
|
||||
{...props}
|
||||
className={cn("relative flex h-min w-full content-center items-center overflow-hidden bg-[var(--user-surface)] p-px transition-colors duration-500", containerClassName)}
|
||||
onMouseEnter={(event: MouseEvent<HTMLElement>) => {
|
||||
setHovered(true);
|
||||
onMouseEnter?.(event as never);
|
||||
}}
|
||||
onMouseLeave={(event: MouseEvent<HTMLElement>) => {
|
||||
setHovered(false);
|
||||
onMouseLeave?.(event as never);
|
||||
}}
|
||||
>
|
||||
<div className={cn("relative z-10 w-full rounded-[inherit]", className)}>{children}</div>
|
||||
{reducedMotion ? null : (
|
||||
<motion.div
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute inset-0 z-0 overflow-hidden rounded-[inherit]"
|
||||
style={{ filter: "blur(1px)" }}
|
||||
initial={{ background: movingMap[direction] }}
|
||||
animate={{ background: hovered ? [movingMap[direction], highlight] : movingMap[direction] }}
|
||||
transition={{ ease: "linear", duration }}
|
||||
/>
|
||||
)}
|
||||
<div aria-hidden className="pointer-events-none absolute inset-px z-[1] rounded-[inherit] bg-[var(--user-surface)]" />
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
@@ -7,11 +7,12 @@ type SpotlightSurfaceProps = Omit<HTMLMotionProps<"div">, "children"> & {
|
||||
children?: ReactNode;
|
||||
spotlightColor: string;
|
||||
spotlightRadius?: number;
|
||||
contentClassName?: string;
|
||||
};
|
||||
|
||||
// 基于 Aceternity Card Spotlight 改造:只保留中性指针高光,避免高频工具面板持续动画。
|
||||
export const SpotlightSurface = forwardRef<HTMLDivElement, SpotlightSurfaceProps>(function SpotlightSurface(
|
||||
{ children, className, spotlightColor, spotlightRadius = 220, onPointerEnter, onPointerLeave, onPointerMove, ...props },
|
||||
{ children, className, contentClassName, spotlightColor, spotlightRadius = 220, onPointerEnter, onPointerLeave, onPointerMove, ...props },
|
||||
ref,
|
||||
) {
|
||||
const mouseX = useMotionValue(0);
|
||||
@@ -45,7 +46,7 @@ export const SpotlightSurface = forwardRef<HTMLDivElement, SpotlightSurfaceProps
|
||||
className="pointer-events-none absolute -inset-px z-0 rounded-[inherit] opacity-0 transition-opacity duration-150 group-hover/spotlight:opacity-100"
|
||||
style={{ backgroundColor: spotlightColor, maskImage, WebkitMaskImage: maskImage, opacity: reducedMotion ? 0 : undefined }}
|
||||
/>
|
||||
<div className="relative z-[1] flex min-h-0 flex-1 flex-col">{children}</div>
|
||||
<div className={cn(contentClassName ?? "relative z-[1] flex min-h-0 flex-1 flex-col")}>{children}</div>
|
||||
</motion.div>
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user