diff --git a/apps/desktop/layer/renderer/src/components/ui/hover-preview/EntryPreviewCard.tsx b/apps/desktop/layer/renderer/src/components/ui/hover-preview/EntryPreviewCard.tsx new file mode 100644 index 000000000..8e4f02c84 --- /dev/null +++ b/apps/desktop/layer/renderer/src/components/ui/hover-preview/EntryPreviewCard.tsx @@ -0,0 +1,92 @@ +import { + HoverCard, + HoverCardContent, + HoverCardTrigger, +} from "@follow/components/ui/hover-card/index.js" +import { useEntry, usePrefetchEntryDetail } from "@follow/store/entry/hooks" +import { useFeedById } from "@follow/store/feed/hooks" +import { feedIconSelector } from "@follow/store/feed/selectors" +import { cn } from "@follow/utils" +import { m } from "motion/react" +import * as React from "react" + +import { RelativeTime } from "~/components/ui/datetime" +import { FeedIcon } from "~/modules/feed/feed-icon" + +interface EntryPreviewCardProps { + entryId: string + children: React.ReactNode + className?: string +} + +export const EntryPreviewCard: React.FC = ({ + entryId, + children, + className, +}) => { + // Prefetch entry details on hover + usePrefetchEntryDetail(entryId) + + const entry = useEntry(entryId, (state) => { + if (!state) return null + return { + title: state.title, + description: state.description, + author: state.author, + publishedAt: state.publishedAt, + feedId: state.feedId, + url: state.url, + } + }) + + const feed = useFeedById(entry?.feedId, feedIconSelector) + + if (!entry || !feed) { + return <>{children} + } + + return ( + + {children} + + + {/* Header */} +
+
+ +
+
{feed.title}
+
{feed.url}
+
+
+
+ + {/* Content */} +
+
+

{entry.title}

+ + {entry.description && ( +

{entry.description}

+ )} + +
+
+ {entry.author && by {entry.author}} +
+
+ +
+
+
+
+
+
+
+ ) +} diff --git a/apps/desktop/layer/renderer/src/components/ui/hover-preview/FeedPreviewCard.tsx b/apps/desktop/layer/renderer/src/components/ui/hover-preview/FeedPreviewCard.tsx new file mode 100644 index 000000000..8921ef5f5 --- /dev/null +++ b/apps/desktop/layer/renderer/src/components/ui/hover-preview/FeedPreviewCard.tsx @@ -0,0 +1,56 @@ +import { + HoverCard, + HoverCardContent, + HoverCardTrigger, +} from "@follow/components/ui/hover-card/index.js" +import { useFeedById } from "@follow/store/feed/hooks" +import { feedIconSelector } from "@follow/store/feed/selectors" +import { cn } from "@follow/utils" +import { m } from "motion/react" +import * as React from "react" + +import { FeedIcon } from "~/modules/feed/feed-icon" + +interface FeedPreviewCardProps { + feedId: string + children: React.ReactNode + className?: string +} + +export const FeedPreviewCard: React.FC = ({ + feedId, + children, + className, +}) => { + const feed = useFeedById(feedId, feedIconSelector) + + if (!feed) { + return <>{children} + } + + return ( + + {children} + + + {/* Header */} +
+
+ +
+
+

{feed.title}

+
+
+
+
+
+
+
+ ) +} diff --git a/apps/desktop/layer/renderer/src/components/ui/hover-preview/index.ts b/apps/desktop/layer/renderer/src/components/ui/hover-preview/index.ts new file mode 100644 index 000000000..a63fd0cdc --- /dev/null +++ b/apps/desktop/layer/renderer/src/components/ui/hover-preview/index.ts @@ -0,0 +1,2 @@ +export { EntryPreviewCard } from "./EntryPreviewCard" +export { FeedPreviewCard } from "./FeedPreviewCard" diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/animated/AnimatedMarkdown.tsx b/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/animated/AnimatedMarkdown.tsx index e150fdacc..d56e195ee 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/animated/AnimatedMarkdown.tsx +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/animated/AnimatedMarkdown.tsx @@ -3,15 +3,6 @@ */ import type { LinkProps } from "@follow/components/ui/link/LinkWithTooltip.js" -import { - Tooltip, - TooltipContent, - TooltipPortal, - TooltipTrigger, -} from "@follow/components/ui/tooltip/index.js" -import { useEntry } from "@follow/store/entry/hooks" -import { useFeedById } from "@follow/store/feed/hooks" -import { feedIconSelector } from "@follow/store/feed/selectors" import { cn, isBizId } from "@follow/utils" import * as React from "react" import type { Components } from "react-markdown" @@ -20,10 +11,10 @@ import rehypeRaw from "rehype-raw" import remarkGfm from "remark-gfm" import { MemoizedShikiCode } from "~/components/ui/code-highlighter" +import { EntryPreviewCard, FeedPreviewCard } from "~/components/ui/hover-preview" import { MarkdownLink } from "~/components/ui/markdown/renderers" import { useNavigateEntry } from "~/hooks/biz/useNavigateEntry" import { usePeekModal } from "~/hooks/biz/usePeekModal" -import { FeedIcon } from "~/modules/feed/feed-icon" import { ANIMATION_STYLE as ANIMATION_STYLE_DEFAULT } from "./constants" import { TokenizedText } from "./TokenizedText" @@ -193,9 +184,6 @@ export const MarkdownAnimateText: React.FC = ({ type BaseInlineFoloReferenceProps = { type: "entry" | "feed" id?: string - reason?: string - description?: string - title?: string } const InlineFoloReference: React.FC< BaseInlineFoloReferenceProps & { @@ -203,7 +191,7 @@ const InlineFoloReference: React.FC< className?: string style?: React.CSSProperties } -> = ({ type, children, className, style, id, title, reason, description }) => { +> = ({ type, children, className, style, id }) => { const peekModal = usePeekModal() const navigateEntry = useNavigateEntry() @@ -240,95 +228,32 @@ const InlineFoloReference: React.FC< if (!targetId) return null - if (!title) - return ( - - ) - - return ( - - - {type === "entry" && } - {type === "feed" && {title}} - - - - - - - - ) -} - -const InnerFoloEntryReference: React.FC<{ id: string }> = ({ id }) => { - const feedId = useEntry(id, (s) => s.feedId) - const feedTitle = useFeedById(feedId, (s) => s.title) - return {feedTitle} -} - -const BaseFoloReferenceBadge = (props: React.PropsWithChildren) => { - return ( + const isEntry = type === "entry" + const button = ( ) -} -export const InlineTooltipContent: React.FC = ({ - title, - reason, - description, - type, - id, -}) => { - if (!id) return null - return ( -
-

- {type === "entry" ? : } - {title} -

- {reason &&

{reason}

} - {description &&

{description}

} -
- ) -} - -const TooltipEntryIcon = ({ entryId }: { entryId: string }) => { - const feedId = useEntry(entryId, (e) => e.feedId) - if (!feedId) return null - return -} - -const TooltipFeedIcon = ({ feedId }: { feedId: string }) => { - const target = useFeedById(feedId, feedIconSelector) - - if (!target) return null - return + if (isEntry) { + return {button} + } else { + return {button} + } } const RelatedEntryLink = (props: LinkProps) => { diff --git a/packages/internal/components/src/ui/hover-card/index.tsx b/packages/internal/components/src/ui/hover-card/index.tsx new file mode 100644 index 000000000..2412d619c --- /dev/null +++ b/packages/internal/components/src/ui/hover-card/index.tsx @@ -0,0 +1,39 @@ +import { cn } from "@follow/utils/utils" +import * as HoverCardPrimitive from "@radix-ui/react-hover-card" +import * as React from "react" + +import { RootPortal } from "../portal" + +const HoverCard = HoverCardPrimitive.Root + +const HoverCardTrigger = HoverCardPrimitive.Trigger + +const HoverCardContent = ({ + ref, + className, + align = "center", + sideOffset = 8, + ...props +}: React.ComponentPropsWithoutRef & { + ref?: React.Ref | null> +}) => ( + + + +) +HoverCardContent.displayName = HoverCardPrimitive.Content.displayName + +const HoverCardArrow = HoverCardPrimitive.Arrow + +export { HoverCard, HoverCardArrow, HoverCardContent, HoverCardTrigger }