Files
kandev/apps/web/components/task/chat/messages/inline-code.tsx
T
João Salavisa d3058276d7 feat(i18n): localize task chat surface (#2303)
Co-authored-by: Kandev Agent <agent@kandev.dev>
2026-08-06 03:01:51 +02:00

100 lines
3.1 KiB
TypeScript

"use client";
import { useEffect, useId, useRef, useState } from "react";
import { createPortal } from "react-dom";
import { cn } from "@/lib/utils";
import { useCopyToClipboard } from "@/hooks/use-copy-to-clipboard";
import { useTranslation } from "react-i18next";
type InlineCodeProps = {
children: React.ReactNode;
};
const COPIED_RESET_MS = 1500;
type TooltipAnchor = { left: number; top: number };
// Tooltip is portaled to document.body so overflow-hidden ancestors (e.g. the user-message bubble) can't clip it.
export function InlineCode({ children }: InlineCodeProps) {
const { t } = useTranslation();
const { copy } = useCopyToClipboard();
const [anchor, setAnchor] = useState<TooltipAnchor | null>(null);
const [hovered, setHovered] = useState(false);
const [copied, setCopied] = useState(false);
const resetTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
const tooltipId = useId();
useEffect(
() => () => {
if (resetTimer.current) clearTimeout(resetTimer.current);
},
[],
);
// Viewport snapshot; dismiss on scroll/resize rather than repositioning.
const visible = hovered || copied;
useEffect(() => {
if (!visible) return;
const dismiss = () => {
setHovered(false);
setCopied(false);
};
window.addEventListener("scroll", dismiss, true);
window.addEventListener("resize", dismiss);
return () => {
window.removeEventListener("scroll", dismiss, true);
window.removeEventListener("resize", dismiss);
};
}, [visible]);
const anchorTo = (el: HTMLElement) => {
const rect = el.getBoundingClientRect();
setAnchor({ left: rect.left + rect.width / 2, top: rect.top });
};
const handleClick = async (event: React.MouseEvent<HTMLElement>) => {
anchorTo(event.currentTarget);
await copy(String(children));
setCopied(true);
if (resetTimer.current) clearTimeout(resetTimer.current);
resetTimer.current = setTimeout(() => setCopied(false), COPIED_RESET_MS);
};
return (
<>
<code
onClick={handleClick}
onMouseEnter={(event) => {
anchorTo(event.currentTarget);
setHovered(true);
}}
onMouseLeave={() => setHovered(false)}
aria-describedby={visible ? tooltipId : undefined}
className="cursor-pointer hover:bg-foreground/10 transition-colors"
>
{children}
</code>
{visible &&
anchor &&
typeof document !== "undefined" &&
createPortal(
<span
role="tooltip"
id={tooltipId}
style={{ left: anchor.left, top: anchor.top - 4 }}
className={cn(
"fixed z-50 -translate-x-1/2 -translate-y-full",
"rounded border border-border bg-popover px-2 py-1 text-xs text-popover-foreground shadow-md",
"pointer-events-none select-none whitespace-nowrap",
"animate-in fade-in-0 duration-150",
)}
>
{copied ? t("task:copied") : t("task:copyToClipboard")}
</span>,
document.body,
)}
</>
);
}