mirror of
https://github.com/block/goose.git
synced 2026-07-17 12:56:20 +02:00
refactor: replace CSS group-hover with Radix HoverCard for message actions
Swap the fragile CSS group-hover opacity pattern for Radix HoverCard which handles hover intent, open/close delays, and portal rendering. Actions appear below the bubble on hover via stripped HoverCardContent (no border, shadow, padding, or background). Removes group class.
This commit is contained in:
@@ -30,6 +30,11 @@ import {
|
||||
ReasoningContent,
|
||||
} from "@/shared/ui/ai-elements/reasoning";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import {
|
||||
HoverCard,
|
||||
HoverCardTrigger,
|
||||
HoverCardContent,
|
||||
} from "@/shared/ui/hover-card";
|
||||
import { ToolChainCards, type ToolChainItem } from "./ToolChainCards";
|
||||
import { ClickableImage } from "./ClickableImage";
|
||||
import { useArtifactLinkHandler } from "@/features/chat/hooks/useArtifactLinkHandler";
|
||||
@@ -414,7 +419,7 @@ export const MessageBubble = memo(function MessageBubble({
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"group flex px-4 py-1",
|
||||
"flex px-4 py-1",
|
||||
"animate-in fade-in duration-200 motion-reduce:animate-none",
|
||||
isEditing
|
||||
? "flex-row"
|
||||
@@ -494,81 +499,90 @@ export const MessageBubble = memo(function MessageBubble({
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<HoverCard openDelay={0} closeDelay={150}>
|
||||
{/* biome-ignore lint/a11y/useKeyWithClickEvents: delegated link handler */}
|
||||
{/* biome-ignore lint/a11y/noStaticElementInteractions: delegated link handler */}
|
||||
<div
|
||||
className={cn(
|
||||
"w-full min-w-0 text-[13px] leading-relaxed",
|
||||
isUser && "rounded-2xl bg-muted px-4 py-2.5",
|
||||
)}
|
||||
onClick={handleContentClick}
|
||||
>
|
||||
{messageAttachments.length > 0 && (
|
||||
<div className="mb-2 flex flex-wrap gap-2">
|
||||
{messageAttachments.map((attachment) => (
|
||||
<MessageAttachmentRow
|
||||
key={`${attachment.type}-${attachment.path ?? attachment.name}`}
|
||||
attachment={attachment}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{groupContentSections(content).map((section, sectionIdx) => {
|
||||
if (section.type === "toolChain") {
|
||||
const toolItems = section.items as ToolChainItem[];
|
||||
return <ToolChainCards key={section.key} toolItems={toolItems} />;
|
||||
}
|
||||
const block = section.items[0] as MessageContent;
|
||||
return (
|
||||
<div key={`${message.id}-${section.key}`}>
|
||||
{renderContentBlock(
|
||||
block,
|
||||
sectionIdx,
|
||||
{
|
||||
defaultImageAlt: t("message.defaultImageAlt"),
|
||||
redactedThinking: t("message.redactedThinking"),
|
||||
},
|
||||
isStreaming,
|
||||
isUser,
|
||||
)}
|
||||
<HoverCardTrigger asChild>
|
||||
<div
|
||||
className={cn(
|
||||
"w-full min-w-0 text-[13px] leading-relaxed",
|
||||
isUser && "rounded-2xl bg-muted px-4 py-2.5",
|
||||
)}
|
||||
onClick={handleContentClick}
|
||||
>
|
||||
{messageAttachments.length > 0 && (
|
||||
<div className="mb-2 flex flex-wrap gap-2">
|
||||
{messageAttachments.map((attachment) => (
|
||||
<MessageAttachmentRow
|
||||
key={`${attachment.type}-${attachment.path ?? attachment.name}`}
|
||||
attachment={attachment}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{pathNotice && (
|
||||
<p className="mt-2 text-xs text-destructive" role="status">
|
||||
{pathNotice}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Hover actions + timestamp */}
|
||||
<MessageActions className="opacity-0 transition-opacity duration-150 group-hover:opacity-100">
|
||||
{textContent && <CopyAction text={textContent} />}
|
||||
{onRetryMessage && (
|
||||
<MessageAction
|
||||
tooltip={t("common:actions.retry")}
|
||||
onClick={() => onRetryMessage(message.id)}
|
||||
>
|
||||
<RotateCcw className="size-3.5" />
|
||||
</MessageAction>
|
||||
)}
|
||||
{isUser && onEditMessage && !isEditing && (
|
||||
<MessageAction
|
||||
tooltip={t("common:actions.edit")}
|
||||
onClick={() => onEditMessage(message.id)}
|
||||
>
|
||||
<Pencil className="size-3.5" />
|
||||
</MessageAction>
|
||||
)}
|
||||
<span className="px-1 text-[10px] text-muted-foreground">
|
||||
{formatDate(created, {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
)}
|
||||
{groupContentSections(content).map((section, sectionIdx) => {
|
||||
if (section.type === "toolChain") {
|
||||
const toolItems = section.items as ToolChainItem[];
|
||||
return <ToolChainCards key={section.key} toolItems={toolItems} />;
|
||||
}
|
||||
const block = section.items[0] as MessageContent;
|
||||
return (
|
||||
<div key={`${message.id}-${section.key}`}>
|
||||
{renderContentBlock(
|
||||
block,
|
||||
sectionIdx,
|
||||
{
|
||||
defaultImageAlt: t("message.defaultImageAlt"),
|
||||
redactedThinking: t("message.redactedThinking"),
|
||||
},
|
||||
isStreaming,
|
||||
isUser,
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</span>
|
||||
</MessageActions>
|
||||
</>
|
||||
{pathNotice && (
|
||||
<p className="mt-2 text-xs text-destructive" role="status">
|
||||
{pathNotice}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</HoverCardTrigger>
|
||||
|
||||
{/* Hover actions + timestamp — rendered in portal, stripped styling */}
|
||||
<HoverCardContent
|
||||
side="bottom"
|
||||
align={isUser ? "end" : "start"}
|
||||
sideOffset={0}
|
||||
className="w-auto border-none bg-transparent p-0 shadow-none"
|
||||
>
|
||||
<MessageActions>
|
||||
{textContent && <CopyAction text={textContent} />}
|
||||
{onRetryMessage && (
|
||||
<MessageAction
|
||||
tooltip={t("common:actions.retry")}
|
||||
onClick={() => onRetryMessage(message.id)}
|
||||
>
|
||||
<RotateCcw className="size-3.5" />
|
||||
</MessageAction>
|
||||
)}
|
||||
{isUser && onEditMessage && (
|
||||
<MessageAction
|
||||
tooltip={t("common:actions.edit")}
|
||||
onClick={() => onEditMessage(message.id)}
|
||||
>
|
||||
<Pencil className="size-3.5" />
|
||||
</MessageAction>
|
||||
)}
|
||||
<span className="px-1 text-[10px] text-muted-foreground">
|
||||
{formatDate(created, {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})}
|
||||
</span>
|
||||
</MessageActions>
|
||||
</HoverCardContent>
|
||||
</HoverCard>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user