fix(client): address fifth round of Codex review findings

Compose the shared Button through HoverCardTrigger's asChild for the web search
details trigger, which recreated rounding, hover colors, focus ring and motion
locally. This also replaces Radix's default anchor with a real button. Only the
hover-card reveal and the icon geometry stay local.

Copy compact reasoning through copy-to-clipboard for the same reason as the
handoff control: navigator.clipboard is undefined on a non-secure origin, where
dereferencing it threw before anything was copied.

Document the streaming-peek fade as a narrowly scoped custom-CSS exception. It
is a mask-image stencil rather than paint, so only its alpha channel is read
and no theme role could meaningfully restyle it.
This commit is contained in:
Marco Beretta 2026-08-28 20:54:51 +02:00
parent 107841551e
commit 2ebe785ed5
No known key found for this signature in database
GPG key ID: D918033D8E74CC11
2 changed files with 33 additions and 15 deletions

View file

@ -1,4 +1,5 @@
import { memo, useMemo, useState, useEffect, useCallback, useRef, useId } from 'react';
import copy from 'copy-to-clipboard';
import { useAtomValue } from 'jotai';
import { Lightbulb, ChevronDown } from 'lucide-react';
import { ContentTypes } from 'librechat-data-provider';
@ -35,7 +36,15 @@ const lastSentences = (text: string): string => {
/** Symmetric top + bottom edge fade so streaming text dissolves in at the
* bottom and out at the top, framed by the same rounded outline the expanded
* panel uses. */
* panel uses.
*
* Custom-CSS exception, narrowly scoped: this is a `mask-image` stencil, not
* paint. Only the alpha channel is read, so `#000` means "keep this pixel"
* and `transparent` means "hide it" — the hue never reaches the screen and no
* theme could meaningfully restyle it. Routing it through a theme role would
* invite a token with alpha, which would silently wash out the text the mask
* is supposed to keep. Tailwind has no mask-image utility that expresses a
* four-stop gradient with `calc()` offsets, hence the inline style. */
const PEEK_FADE =
'linear-gradient(to bottom, transparent, #000 1.25rem, #000 calc(100% - 1.25rem), transparent)';
@ -292,7 +301,9 @@ export const ReasoningCompact = memo(
}, []);
const handleCopy = useCallback(() => {
navigator.clipboard.writeText(reasoningText);
if (!copy(reasoningText, { format: 'text/plain' })) {
return;
}
setIsCopied(true);
setTimeout(() => setIsCopied(false), 2000);
}, [reasoningText]);

View file

@ -3,6 +3,7 @@ import { useRecoilValue } from 'recoil';
import { Tools } from 'librechat-data-provider';
import { Globe, ChevronDown, Info } from 'lucide-react';
import {
Button,
HoverCard,
HoverCardTrigger,
HoverCardPortal,
@ -306,19 +307,25 @@ export default function WebSearch({
</button>
{hasDetails && (
<HoverCard openDelay={50} open={showDetails} onOpenChange={setShowDetails}>
<HoverCardTrigger
tabIndex={0}
className={cn(
'ml-auto inline-flex cursor-help items-center justify-center rounded-md p-1 text-text-secondary opacity-0 transition-all duration-200 ease-out',
'hover:bg-surface-hover hover:text-text-primary',
'group-focus-within/websearch:opacity-100 group-hover/websearch:opacity-100',
'focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-border-heavy',
)}
onFocus={() => setShowDetails(true)}
onBlur={() => setShowDetails(false)}
aria-label={localize('com_ui_web_search_details')}
>
<Info className="size-3.5" aria-hidden="true" />
{/* Composed through `asChild` so the shared Button owns the
rounding, hover fill, focus ring and motion (and gives a real
<button> instead of Radix's default anchor). Only the
hover-card reveal and the icon geometry stay local. */}
<HoverCardTrigger asChild>
<Button
variant="ghost"
size="icon"
className={cn(
'ml-auto size-auto cursor-help rounded-md p-1 text-text-secondary opacity-0',
'group-focus-within/websearch:opacity-100 group-hover/websearch:opacity-100',
'focus-visible:opacity-100',
)}
onFocus={() => setShowDetails(true)}
onBlur={() => setShowDetails(false)}
aria-label={localize('com_ui_web_search_details')}
>
<Info className="size-3.5" aria-hidden="true" />
</Button>
</HoverCardTrigger>
<HoverCardPortal>
<HoverCardContent side="top" className="z-[999] w-80">