diff --git a/packages/views/editor/attachment-block.test.tsx b/packages/views/editor/attachment-block.test.tsx index d311379b6f..04027bf25d 100644 --- a/packages/views/editor/attachment-block.test.tsx +++ b/packages/views/editor/attachment-block.test.tsx @@ -23,7 +23,6 @@ vi.mock("../i18n", () => ({ preview_loading: "Loading preview…", preview_failed: "Couldn't load preview", }, - code_block: { copy_code: "Copy code" }, file_card: { uploading: "Uploading {{filename}}" }, }), }), @@ -60,9 +59,11 @@ describe("AttachmentBlock — dispatcher", () => { // HtmlAttachmentPreview never renders the filename row — that's the // file-card chrome it replaces. expect(screen.queryByText("report.html")).toBeNull(); - // Toolbar shows the Maximize-style preview button. + // Toolbar shows Preview + Download only — attachments are files, not + // inline source snippets, so there is no Copy code button. expect(screen.getByTitle("Preview")).toBeTruthy(); - expect(screen.getByTitle("Copy code")).toBeTruthy(); + expect(screen.getByTitle("Download")).toBeTruthy(); + expect(screen.queryByTitle("Copy code")).toBeNull(); }); it("routes html WITHOUT attachmentId to AttachmentCard (URL-only is chrome-only)", () => { diff --git a/packages/views/editor/content-editor.css b/packages/views/editor/content-editor.css index 5a0c7c57fa..bcc9ee36a7 100644 --- a/packages/views/editor/content-editor.css +++ b/packages/views/editor/content-editor.css @@ -315,6 +315,12 @@ .rich-text-editor .hljs-meta { color: var(--muted-foreground); } +/* XML / HTML — lowlight emits .hljs-tag for `<` `>` brackets and .hljs-name + for the element name. Without these rules, HTML source renders mostly in + the default text color and looks unhighlighted. */ +.rich-text-editor .hljs-tag { color: var(--muted-foreground); } +.rich-text-editor .hljs-name { color: oklch(0.55 0.16 255); } + /* Dark mode overrides */ .dark .rich-text-editor .hljs-keyword, .dark .rich-text-editor .hljs-selector-tag, @@ -341,6 +347,8 @@ .dark .rich-text-editor .hljs-deletion { color: oklch(0.7 0.18 25); } +.dark .rich-text-editor .hljs-name { color: oklch(0.72 0.14 255); } + /* Tables */ .rich-text-editor .tableWrapper { overflow-x: auto; diff --git a/packages/views/editor/html-attachment-preview.test.tsx b/packages/views/editor/html-attachment-preview.test.tsx index 37f55d82d6..b357e10dfe 100644 --- a/packages/views/editor/html-attachment-preview.test.tsx +++ b/packages/views/editor/html-attachment-preview.test.tsx @@ -23,7 +23,6 @@ vi.mock("../i18n", () => ({ preview_loading: "Loading preview…", preview_failed: "Couldn't load preview", }, - code_block: { copy_code: "Copy code" }, }), }), })); @@ -125,19 +124,11 @@ describe("HtmlAttachmentPreview — toolbar actions", () => { expect(onDownload).toHaveBeenCalled(); }); - it("writes the loaded text to the clipboard when Copy code is clicked", async () => { + it("does not render a Copy code button — attachments are files, not source snippets", async () => { getAttachmentTextContentMock.mockResolvedValueOnce({ - text: "

chart source

", + text: "

ok

", originalContentType: "text/html", }); - const writeText = vi.fn().mockResolvedValue(undefined); - // jsdom does not implement navigator.clipboard; install it directly on - // the existing navigator instance so the component's `navigator.clipboard` - // global lookup resolves to our mock. - Object.defineProperty(navigator, "clipboard", { - configurable: true, - value: { writeText }, - }); renderWithQuery( { onDownload={() => {}} />, ); - // Wait until the query resolves and the iframe appears — the Copy button - // is rendered in the loading state too (disabled), so we cannot just wait - // for it to exist. await waitFor(() => expect(document.querySelector("iframe")).toBeTruthy()); - fireEvent.mouseDown(screen.getByTitle("Copy code")); - await waitFor(() => { - expect(writeText).toHaveBeenCalledWith("

chart source

"); - }); + expect(screen.queryByTitle("Copy code")).toBeNull(); }); }); describe("HtmlAttachmentPreview — failure mode does not unmount the toolbar", () => { - it("keeps Open and Download enabled and disables Copy code when fetch errors", async () => { + it("keeps Preview and Download enabled when fetch errors", async () => { getAttachmentTextContentMock.mockRejectedValueOnce(new Error("nope")); const onPreview = vi.fn(); const onDownload = vi.fn(); @@ -177,16 +162,14 @@ describe("HtmlAttachmentPreview — failure mode does not unmount the toolbar", ).toBeTruthy(); }); // Critical: the figure does NOT collapse, and the chrome row is NOT - // rendered as a fallback. Open and Download stay reachable. + // rendered as a fallback. Preview and Download stay reachable. expect(document.querySelector("iframe")).toBeNull(); expect(screen.queryByText("report.html")).toBeNull(); const previewBtn = screen.getByTitle("Preview") as HTMLButtonElement; const downloadBtn = screen.getByTitle("Download") as HTMLButtonElement; - const copyBtn = screen.getByTitle("Copy code") as HTMLButtonElement; expect(previewBtn.disabled).toBe(false); expect(downloadBtn.disabled).toBe(false); - expect(copyBtn.disabled).toBe(true); fireEvent.mouseDown(previewBtn); expect(onPreview).toHaveBeenCalled(); diff --git a/packages/views/editor/html-attachment-preview.tsx b/packages/views/editor/html-attachment-preview.tsx index 240648adad..2731c1f679 100644 --- a/packages/views/editor/html-attachment-preview.tsx +++ b/packages/views/editor/html-attachment-preview.tsx @@ -4,8 +4,12 @@ * HtmlAttachmentPreview — inline HTML attachment renderer. * * Visual model mirrors the image renderer: the iframe body is the card, and a - * floating right-top toolbar reveals on hover with Open / Download / Copy code - * actions. No file-card chrome (icon + filename row). + * floating right-top toolbar reveals on hover with Preview (full-screen modal) + * and Download. No file-card chrome (icon + filename row). + * + * No "Copy code" button: this is a FILE, not an inline source snippet. The + * inline ```html``` fenced block (HtmlBlockPreview) is the surface for reading + * / copying HTML source; an attachment's contract is view + download. * * Mounted by AttachmentBlock when the attachment is HTML and the caller can * supply an `attachmentId` (the /content proxy is ID-keyed). For other kinds, @@ -14,13 +18,11 @@ * Failure mode (413 / 415 / transport): we do not unmount the figure or fall * back to AttachmentCard chrome — standalone attachment lists filter URLs * already inlined in the markdown body, so a silent unmount would remove the - * user's only Open/Download entry point. Instead the body collapses to an - * 80px placeholder, the toolbar pins itself open, Open and Download remain - * enabled, and Copy code is disabled (no text payload available). + * user's only Preview/Download entry point. Instead the body collapses to an + * 80px placeholder and the toolbar pins itself open with both actions enabled. */ -import { useState } from "react"; -import { Check, Copy, Download, Maximize2 } from "lucide-react"; +import { Download, Maximize2 } from "lucide-react"; import { cn } from "@multica/ui/lib/utils"; import { useT } from "../i18n"; import { useAttachmentHtmlText } from "./hooks/use-attachment-html-text"; @@ -43,24 +45,10 @@ export function HtmlAttachmentPreview({ }: HtmlAttachmentPreviewProps) { const { t } = useT("editor"); const query = useAttachmentHtmlText(attachmentId); - const [copied, setCopied] = useState(false); const text = query.data?.text; const isLoading = query.isLoading; const isError = !isLoading && (!!query.error || !text); - const canCopy = !!text; - - const handleCopy = async () => { - if (!text) return; - try { - await navigator.clipboard.writeText(text); - setCopied(true); - setTimeout(() => setCopied(false), 2000); - } catch { - // Clipboard failures are user-recoverable (try again, or open in modal - // and use the text view). No toast — keep the toolbar quiet. - } - }; return (
-
);