feat(desktop): integrate MagneticHoverEffect into MarkdownLink and EntryTitle components

- Replaced standard anchor tags with MagneticHoverEffect for enhanced visual feedback on hover.
- Removed custom mouse event handlers from EntryTitle, simplifying the component's code while maintaining hover effects.

These changes improve user interaction and visual appeal across the affected components.

Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
Innei 2025-04-09 23:56:21 +08:00
parent d9ca464590
commit feba117ba3
No known key found for this signature in database
GPG Key ID: 0F62D33977F021F7
3 changed files with 72 additions and 42 deletions

View File

@ -1,3 +1,4 @@
import { MagneticHoverEffect } from "@follow/components/ui/effect/MagneticHoverEffect.js"
import {
Tooltip,
TooltipContent,
@ -31,9 +32,10 @@ export const MarkdownLink = (props: LinkProps) => {
return (
<Tooltip delayDuration={0}>
<TooltipTrigger asChild>
<a
<MagneticHoverEffect
as="a"
draggable="false"
className="follow-link--underline text-foreground font-semibold no-underline"
className="text-foreground before:!bg-accent/50 font-semibold no-underline"
href={populatedFullHref}
title={props.title}
target="_blank"
@ -44,7 +46,7 @@ export const MarkdownLink = (props: LinkProps) => {
{typeof props.children === "string" && (
<i className="i-mgc-arrow-right-up-cute-re size-[0.9em] translate-y-[2px] opacity-70" />
)}
</a>
</MagneticHoverEffect>
</TooltipTrigger>
{!!props.href && (
<TooltipPortal>

View File

@ -1,5 +1,6 @@
import { cn, formatEstimatedMins, formatTimeToSeconds } from "@follow/utils/utils"
import { useCallback, useMemo, useRef } from "react"
import { MagneticHoverEffect } from "@follow/components/ui/effect/MagneticHoverEffect.js"
import { formatEstimatedMins, formatTimeToSeconds } from "@follow/utils/utils"
import { useMemo } from "react"
import { useUISettingKey } from "~/atoms/settings/ui"
import { useWhoami } from "~/atoms/user"
@ -69,32 +70,6 @@ export const EntryTitle = ({ entryId, compact }: EntryLinkProps) => {
return formatEstimatedMins(Math.floor(durationInSeconds / 60))
}, [audioAttachment])
const titleRef = useRef<HTMLAnchorElement>(null)
const handleMouseEnter = () => {
if (!titleRef.current) return
titleRef.current.style.transition = "transform 0.2s cubic-bezier(0.33, 1, 0.68, 1)"
}
const handleMouseLeave = () => {
if (!titleRef.current) return
titleRef.current.style.transform = "translate(0px, 0px)"
titleRef.current.style.transition = "transform 0.4s cubic-bezier(0.33, 1, 0.68, 1)"
}
const handleMouseMove = useCallback((e: React.MouseEvent<HTMLAnchorElement>) => {
if (!titleRef.current) return
const rect = titleRef.current.getBoundingClientRect()
const centerX = rect.left + rect.width / 2
const centerY = rect.top + rect.height / 2
const distanceX = (e.clientX - centerX) * 0.05
const distanceY = (e.clientY - centerY) * 0.05
titleRef.current.style.transform = `translate(${distanceX}px, ${distanceY}px)`
}, [])
if (!entry) return null
return compact ? (
@ -123,26 +98,19 @@ export const EntryTitle = ({ entryId, compact }: EntryLinkProps) => {
<FeedIcon fallback feed={feed || inbox} entry={entry.entries} size={16} />
{getPreferredTitle(feed || inbox, entry.entries)}
</a>
<a
<MagneticHoverEffect
as="a"
href={populatedFullHref ?? "#"}
target="_blank"
rel="noopener noreferrer"
ref={titleRef}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
onMouseMove={handleMouseMove}
className={cn(
"cursor-link relative select-text break-words text-2xl font-medium leading-normal",
"inline-block transition-all duration-200 ease-out",
"before:absolute before:-inset-x-2 before:inset-y-0 before:rounded-xl before:bg-black/[0.03] before:opacity-0 before:transition-opacity hover:before:opacity-100 dark:before:bg-white/[0.07]",
)}
className={"select-text break-words text-2xl font-medium leading-normal"}
>
<EntryTranslation
source={entry.entries.title}
target={translation.data?.title}
className="select-text hyphens-auto"
/>
</a>
</MagneticHoverEffect>
</div>
{/* Meta Information with improved layout */}

View File

@ -0,0 +1,60 @@
import { cn } from "@follow/utils/utils"
import * as React from "react"
import { useCallback, useRef } from "react"
type MagneticHoverEffectProps<T extends React.ElementType> = {
as?: T
children: React.ReactNode
} & Omit<React.ComponentPropsWithoutRef<T>, "as" | "children">
export const MagneticHoverEffect = <T extends React.ElementType = "div">({
as,
children,
...rest
}: MagneticHoverEffectProps<T>) => {
const Component = as || "div"
const itemRef = useRef<HTMLElement>(null)
const handleMouseEnter = () => {
if (!itemRef.current) return
itemRef.current.style.transition = "transform 0.2s cubic-bezier(0.33, 1, 0.68, 1)"
}
const handleMouseLeave = () => {
if (!itemRef.current) return
itemRef.current.style.transform = "translate(0px, 0px)"
itemRef.current.style.transition = "transform 0.4s cubic-bezier(0.33, 1, 0.68, 1)"
}
const handleMouseMove = useCallback((e: React.MouseEvent<HTMLElement>) => {
if (!itemRef.current) return
const rect = itemRef.current.getBoundingClientRect()
const centerX = rect.left + rect.width / 2
const centerY = rect.top + rect.height / 2
const distanceX = (e.clientX - centerX) * 0.05
const distanceY = (e.clientY - centerY) * 0.05
itemRef.current.style.transform = `translate(${distanceX}px, ${distanceY}px)`
}, [])
return (
<Component
ref={itemRef as any}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
onMouseMove={handleMouseMove}
{...rest}
className={cn(
"relative !cursor-none",
"inline-block transition-all duration-200 ease-out",
"before:absolute before:-inset-x-2 before:inset-y-0 before:z-[-1] before:origin-center before:scale-[0.92] before:rounded-xl before:bg-black/[0.03] before:opacity-0 before:backdrop-blur before:transition-all before:duration-200 hover:before:scale-100 hover:before:opacity-100 dark:before:bg-white/[0.10]",
rest.className,
)}
>
{children}
</Component>
)
}