style(webui): simplify Markdown code blocks (#5002)
This commit is contained in:
@@ -40,8 +40,6 @@ const CODE_FONT_STACK = [
|
|||||||
].join(", ");
|
].join(", ");
|
||||||
|
|
||||||
const ANSI_LANGUAGES = new Set(["ansi", "ansi-output"]);
|
const ANSI_LANGUAGES = new Set(["ansi", "ansi-output"]);
|
||||||
const CODE_SURFACE_LIGHT = "#f4f4f5";
|
|
||||||
const CODE_SURFACE_DARK = "#27272a";
|
|
||||||
|
|
||||||
const LazyHighlightedCode = lazy(async () => {
|
const LazyHighlightedCode = lazy(async () => {
|
||||||
const [
|
const [
|
||||||
@@ -81,15 +79,11 @@ const LazyHighlightedCode = lazy(async () => {
|
|||||||
language={language || "text"}
|
language={language || "text"}
|
||||||
style={transparentTheme}
|
style={transparentTheme}
|
||||||
customStyle={{
|
customStyle={{
|
||||||
background: chrome === "none"
|
background: "transparent",
|
||||||
? "transparent"
|
|
||||||
: isDark
|
|
||||||
? CODE_SURFACE_DARK
|
|
||||||
: CODE_SURFACE_LIGHT,
|
|
||||||
margin: 0,
|
margin: 0,
|
||||||
padding: chrome === "none" ? "0.75rem 1rem" : "1rem",
|
padding: chrome === "none" ? "0.75rem 1rem" : "1rem 3.5rem 1rem 1.25rem",
|
||||||
fontFamily: CODE_FONT_STACK,
|
fontFamily: CODE_FONT_STACK,
|
||||||
fontSize: chrome === "none" ? "13px" : "0.875rem",
|
fontSize: "13px",
|
||||||
lineHeight: chrome === "none" ? 1.55 : 1.6,
|
lineHeight: chrome === "none" ? 1.55 : 1.6,
|
||||||
tabSize: 2,
|
tabSize: 2,
|
||||||
}}
|
}}
|
||||||
@@ -148,10 +142,11 @@ function CodeTextBlock({
|
|||||||
return (
|
return (
|
||||||
<pre
|
<pre
|
||||||
className={cn(
|
className={cn(
|
||||||
"m-0 overflow-x-auto p-4 font-mono text-sm leading-[1.6] text-foreground/90",
|
"m-0 overflow-x-auto bg-transparent font-mono text-[13px] text-foreground/90",
|
||||||
showLineNumbers ? "whitespace-pre" : "whitespace-pre-wrap",
|
showLineNumbers ? "whitespace-pre" : "whitespace-pre-wrap",
|
||||||
chrome === "default" ? "bg-zinc-100 dark:bg-zinc-800" : "bg-transparent",
|
chrome === "default"
|
||||||
chrome === "none" && "p-3 text-[13px] leading-[1.55]",
|
? "py-4 pl-5 pr-14 leading-[1.6]"
|
||||||
|
: "p-3 leading-[1.55]",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
data-testid={testId}
|
data-testid={testId}
|
||||||
@@ -193,6 +188,7 @@ export function CodeBlock({
|
|||||||
const hasChrome = chrome === "default";
|
const hasChrome = chrome === "default";
|
||||||
const renderAnsi = shouldRenderAnsi(language, code);
|
const renderAnsi = shouldRenderAnsi(language, code);
|
||||||
const syntaxLanguage = normalizeCodeLanguage(language);
|
const syntaxLanguage = normalizeCodeLanguage(language);
|
||||||
|
const copyLabel = copied ? t("code.copied") : t("code.copyAria");
|
||||||
|
|
||||||
const onCopy = useCallback(() => {
|
const onCopy = useCallback(() => {
|
||||||
void copyTextToClipboard(renderAnsi ? stripAnsi(code) : code).then((ok) => {
|
void copyTextToClipboard(renderAnsi ? stripAnsi(code) : code).then((ok) => {
|
||||||
@@ -205,44 +201,12 @@ export function CodeBlock({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"not-prose overflow-hidden",
|
"not-prose relative overflow-hidden",
|
||||||
hasChrome && "rounded-lg border",
|
hasChrome && "rounded-[18px] bg-secondary/70",
|
||||||
hasChrome && (isDark ? "border-white/10" : "border-black/10"),
|
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
|
data-language={language || t("code.fallbackLanguage")}
|
||||||
>
|
>
|
||||||
{hasChrome ? (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"flex items-center justify-between px-4 pb-1.5 pt-2 text-xs font-medium",
|
|
||||||
isDark
|
|
||||||
? "bg-zinc-800 text-zinc-300"
|
|
||||||
: "bg-zinc-100 text-zinc-600",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<span className="lowercase font-mono">
|
|
||||||
{language || t("code.fallbackLanguage")}
|
|
||||||
</span>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={onCopy}
|
|
||||||
className={cn(
|
|
||||||
"inline-flex items-center gap-1 rounded px-1.5 py-0.5 font-mono transition-colors",
|
|
||||||
isDark
|
|
||||||
? "text-zinc-400 hover:bg-zinc-700 hover:text-zinc-200"
|
|
||||||
: "text-zinc-500 hover:bg-zinc-200 hover:text-zinc-700",
|
|
||||||
)}
|
|
||||||
aria-label={t("code.copyAria")}
|
|
||||||
>
|
|
||||||
{copied ? (
|
|
||||||
<Check className="h-3.5 w-3.5" />
|
|
||||||
) : (
|
|
||||||
<Copy className="h-3.5 w-3.5" />
|
|
||||||
)}
|
|
||||||
<span>{copied ? t("code.copied") : t("code.copy")}</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
{renderAnsi ? (
|
{renderAnsi ? (
|
||||||
<CodeTextBlock
|
<CodeTextBlock
|
||||||
code={code}
|
code={code}
|
||||||
@@ -279,6 +243,25 @@ export function CodeBlock({
|
|||||||
testId="plain-code-fallback"
|
testId="plain-code-fallback"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{hasChrome ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onCopy}
|
||||||
|
className={cn(
|
||||||
|
"absolute right-2.5 top-2.5 z-10 inline-flex h-8 w-8 items-center justify-center rounded-full",
|
||||||
|
"text-muted-foreground/75 transition-colors hover:bg-background/70 hover:text-foreground",
|
||||||
|
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/60",
|
||||||
|
)}
|
||||||
|
aria-label={copyLabel}
|
||||||
|
title={copyLabel}
|
||||||
|
>
|
||||||
|
{copied ? (
|
||||||
|
<Check className="h-4 w-4" aria-hidden />
|
||||||
|
) : (
|
||||||
|
<Copy className="h-4 w-4" aria-hidden />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,8 +48,20 @@ describe("CodeBlock", () => {
|
|||||||
|
|
||||||
expect(screen.queryByTestId("highlighted-code")).not.toBeInTheDocument();
|
expect(screen.queryByTestId("highlighted-code")).not.toBeInTheDocument();
|
||||||
expect(screen.getByText("const value = 1;")).toBeInTheDocument();
|
expect(screen.getByText("const value = 1;")).toBeInTheDocument();
|
||||||
expect(screen.getByText("ts")).toBeInTheDocument();
|
expect(screen.queryByText("ts")).not.toBeInTheDocument();
|
||||||
expect(screen.getByTestId("plain-code-fallback")).toHaveClass("text-foreground/90");
|
expect(screen.getByTestId("plain-code-fallback")).toHaveClass("text-foreground/90");
|
||||||
|
expect(screen.getByTestId("plain-code-fallback")).toHaveClass("bg-transparent");
|
||||||
|
expect(screen.getByTestId("plain-code-fallback")).toHaveClass("py-4", "pl-5", "pr-14");
|
||||||
|
|
||||||
|
const container = screen.getByTestId("plain-code-fallback").closest(".not-prose");
|
||||||
|
expect(container).toHaveClass("relative", "rounded-[18px]", "bg-secondary/70");
|
||||||
|
expect(container).not.toHaveClass("border");
|
||||||
|
expect(container).toHaveAttribute("data-language", "ts");
|
||||||
|
|
||||||
|
const copyButton = screen.getByRole("button", { name: "Copy code" });
|
||||||
|
expect(copyButton.parentElement).toBe(container);
|
||||||
|
expect(copyButton).toHaveClass("absolute", "h-8", "w-8", "rounded-full");
|
||||||
|
expect(copyButton).toHaveTextContent("");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("can render without chat-style chrome for file previews", () => {
|
it("can render without chat-style chrome for file previews", () => {
|
||||||
@@ -115,8 +127,11 @@ describe("CodeBlock", () => {
|
|||||||
|
|
||||||
expect(screen.queryByTestId("highlighted-code")).not.toBeInTheDocument();
|
expect(screen.queryByTestId("highlighted-code")).not.toBeInTheDocument();
|
||||||
expect(screen.getByTestId("ansi-code")).toBeInTheDocument();
|
expect(screen.getByTestId("ansi-code")).toBeInTheDocument();
|
||||||
expect(screen.getByTestId("ansi-code").closest(".not-prose")).toBeTruthy();
|
expect(screen.getByTestId("ansi-code").closest(".not-prose")).toHaveAttribute(
|
||||||
expect(screen.getByText("ansi")).toBeInTheDocument();
|
"data-language",
|
||||||
|
"ansi",
|
||||||
|
);
|
||||||
|
expect(screen.queryByText("ansi")).not.toBeInTheDocument();
|
||||||
expect(screen.getByText("PASS")).toHaveStyle({ color: "#0dbc79" });
|
expect(screen.getByText("PASS")).toHaveStyle({ color: "#0dbc79" });
|
||||||
expect(screen.getByText("<script>alert(1)</script>")).toBeInTheDocument();
|
expect(screen.getByText("<script>alert(1)</script>")).toBeInTheDocument();
|
||||||
expect(document.querySelector("script")).toBeNull();
|
expect(document.querySelector("script")).toBeNull();
|
||||||
@@ -183,7 +198,7 @@ describe("CodeBlock", () => {
|
|||||||
await user.click(screen.getByRole("button", { name: /copy/i }));
|
await user.click(screen.getByRole("button", { name: /copy/i }));
|
||||||
|
|
||||||
await waitFor(() => expect(execCommand).toHaveBeenCalledWith("copy"));
|
await waitFor(() => expect(execCommand).toHaveBeenCalledWith("copy"));
|
||||||
expect(screen.getByText("Copied")).toBeInTheDocument();
|
expect(screen.getByRole("button", { name: "Copied" })).toBeInTheDocument();
|
||||||
} finally {
|
} finally {
|
||||||
Reflect.deleteProperty(navigator, "clipboard");
|
Reflect.deleteProperty(navigator, "clipboard");
|
||||||
Reflect.deleteProperty(document, "execCommand");
|
Reflect.deleteProperty(document, "execCommand");
|
||||||
|
|||||||
@@ -163,7 +163,8 @@ describe("MarkdownTextRenderer", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(screen.getByText("code without language")).toBeInTheDocument();
|
expect(screen.getByText("code without language")).toBeInTheDocument();
|
||||||
expect(screen.getByText("text")).toBeInTheDocument();
|
expect(screen.queryByText("text")).not.toBeInTheDocument();
|
||||||
|
expect(container.querySelector(".not-prose")).toHaveAttribute("data-language", "text");
|
||||||
expect(container.querySelectorAll("pre")).toHaveLength(1);
|
expect(container.querySelectorAll("pre")).toHaveLength(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user