Files
paseo/packages/app/src/components/markdown/renderers.web.tsx
T
Mohamed Boudra df7f1f0b75 Restore Markdown link hover underline and spacing after code blocks on web (#7276)
* fix(markdown): restore link hover underline and code block spacing on web

The web renderer swap dropped both. Links: the library styles anchors
inline with text-decoration: none, so the link renderer has to track hover
itself; a stylesheet :hover rule cannot win. Spacing: the patched
renderers.codeBlock override returned the custom block bare, so it lost
the library's codeBlock margins and whatever followed sat flush against
it. The override now keeps those margins, including the zeroed trailing
margin of the last block, so web and native share one spacing config.

* test(desktop): expect Find to return focus to the chat viewport

#7263 moved focus on closing Find from the no-drag focus scope to the
scroll viewport so Page Up and Page Down keep working. The viewport emits
no window region by design (#5233), so the old assertion that the focused
element is no-drag failed on every run. Assert the new focus target and
re-check that the titlebar still has no drag exclusions.
2026-10-18 12:48:10 +07:00

97 lines
3.3 KiB
TypeScript

import { Text } from "react-native";
import { forwardRef, useCallback, useMemo, useState, type ComponentProps } from "react";
import type {
CodeBlockRendererProps,
LinkRendererProps,
MarkdownRenderers,
} from "react-native-enriched-markdown";
import { StyleSheet, withUnistyles } from "react-native-unistyles";
import { useFileLink } from "@/assistant-file-links";
import { Tooltip, TooltipTrigger, TooltipContent } from "@/components/ui/tooltip";
import { HighlightedCodeBlock } from "@/components/highlighted-code-block";
import type { Theme } from "@/styles/theme";
import { MermaidFence } from "./fence/mermaid";
import { markdownLinkSource } from "./source";
const ThemedCodeBlock = withUnistyles(HighlightedCodeBlock);
const codeStyle = (theme: Theme) => ({
inheritedStyles: {
color: theme.colors.foreground,
fontFamily: theme.fontFamily.mono,
fontSize: theme.fontSize.code,
},
textStyle: { backgroundColor: theme.colors.surface2, padding: 12, borderRadius: 8 },
});
function WebCodeBlock({ code, language }: CodeBlockRendererProps) {
if (language?.toLowerCase() === "mermaid") return <MermaidFence code={code} />;
return <ThemedCodeBlock code={code} language={language} uniProps={codeStyle} />;
}
// The library styles links inline, so a stylesheet :hover rule cannot underline them.
const LINK_HOVER_STYLE = { textDecoration: "underline" } as const;
function WebLink({ link, anchorProps, children }: LinkRendererProps) {
const { tooltipPath, onHoverIn } = useFileLink(markdownLinkSource(link.url, link.sourceType));
const [hovered, setHovered] = useState(false);
const handleMouseEnter = useCallback(() => setHovered(true), []);
const handleMouseLeave = useCallback(() => setHovered(false), []);
const anchorStyle = useMemo(
() => ({
...anchorProps.style,
overflowWrap: "anywhere" as const,
...(hovered ? LINK_HOVER_STYLE : null),
}),
[anchorProps.style, hovered],
);
const anchor = (
<a
{...anchorProps}
style={anchorStyle}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
{children}
</a>
);
return (
<Tooltip delayDuration={400}>
<TooltipTrigger asChild onHoverIn={onHoverIn}>
<InlineLinkTrigger>{anchor}</InlineLinkTrigger>
</TooltipTrigger>
{tooltipPath ? (
<TooltipContent side="top" align="start" maxWidth={520}>
<Text selectable={false} style={styles.tooltipPath}>
{tooltipPath}
</Text>
</TooltipContent>
) : null}
</Tooltip>
);
}
// TooltipTrigger also supplies native Pressable handlers; forward only DOM events.
// A View renders a div, which is invalid inside the library's prose paragraphs.
const InlineLinkTrigger = forwardRef<HTMLSpanElement, ComponentProps<"span">>(
function InlineLinkTrigger({ children, onMouseEnter, onMouseLeave, onFocus, onBlur }, ref) {
return (
<span
ref={ref}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
onFocus={onFocus}
onBlur={onBlur}
>
{children}
</span>
);
},
);
const styles = StyleSheet.create((theme) => ({
tooltipPath: {
color: theme.colors.foreground,
fontSize: theme.fontSize.sm,
fontWeight: theme.fontWeight.normal,
},
}));
export const WEB_RENDERERS: MarkdownRenderers = { codeBlock: WebCodeBlock, link: WebLink };