feat(web): enhance code blocks with copy button and fix link navigation

Add custom code block renderer with language display and copy functionality. Links now open in new windows, and clicking image links no longer triggers both link navigation and image preview.

- Add CodeBlock component with copy-to-clipboard button and language label
- Configure all markdown links to open in new windows with target="_blank"
- Fix image link behavior to prevent duplicate actions when clicked

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
pull/5203/head
Steven 1 week ago
parent b00df8a9d1
commit d693142dd4

@ -0,0 +1,55 @@
import { CheckIcon, CopyIcon } from "lucide-react";
import { useState } from "react";
import { cn } from "@/lib/utils";
interface PreProps {
children?: React.ReactNode;
className?: string;
}
export const CodeBlock = ({ children, className, ...props }: PreProps) => {
const [copied, setCopied] = useState(false);
// Extract the code element and its props
const codeElement = children as React.ReactElement;
const codeClassName = codeElement?.props?.className || "";
const codeContent = String(codeElement?.props?.children || "").replace(/\n$/, "");
// Extract language from className (format: language-xxx)
const match = /language-(\w+)/.exec(codeClassName);
const language = match ? match[1] : "";
const handleCopy = async () => {
try {
await navigator.clipboard.writeText(codeContent);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch (err) {
console.error("Failed to copy code:", err);
}
};
return (
<pre className="relative group">
<div className="w-full flex flex-row justify-between items-center">
<span className="text-[10px] font-medium text-muted-foreground/60 uppercase tracking-wider select-none">{language}</span>
<button
onClick={handleCopy}
className={cn(
"p-1.5 rounded-md transition-all",
"hover:bg-accent/50",
"focus:outline-none focus:ring-1 focus:ring-ring",
copied ? "text-primary" : "text-muted-foreground",
)}
aria-label={copied ? "Copied" : "Copy code"}
title={copied ? "Copied!" : "Copy code"}
>
{copied ? <CheckIcon className="w-3.5 h-3.5" /> : <CopyIcon className="w-3.5 h-3.5" />}
</button>
</div>
<div className={className} {...props}>
{children}
</div>
</pre>
);
};

@ -10,6 +10,7 @@ import { useTranslate } from "@/utils/i18n";
import { remarkPreserveType } from "@/utils/remark-plugins/remark-preserve-type";
import { remarkTag } from "@/utils/remark-plugins/remark-tag";
import { isSuperUser } from "@/utils/user";
import { CodeBlock } from "./CodeBlock";
import { createConditionalComponent, isTagNode, isTaskListItemNode } from "./ConditionalComponent";
import { MemoContentContext } from "./MemoContentContext";
import { Tag } from "./Tag";
@ -102,6 +103,12 @@ const MemoContent = observer((props: Props) => {
// Conditionally render custom components based on AST node type
input: createConditionalComponent(TaskListItem, "input", isTaskListItemNode),
span: createConditionalComponent(Tag, "span", isTagNode),
pre: CodeBlock,
a: ({ href, children, ...props }) => (
<a href={href} target="_blank" rel="noopener noreferrer" {...props}>
{children}
</a>
),
}}
>
{content}

@ -84,6 +84,13 @@ const MemoView: React.FC<Props> = observer((props: Props) => {
const targetEl = e.target as HTMLElement;
if (targetEl.tagName === "IMG") {
// Check if the image is inside a link
const linkElement = targetEl.closest("a");
if (linkElement) {
// If image is inside a link, only navigate to the link (don't show preview)
return;
}
const imgUrl = targetEl.getAttribute("src");
if (imgUrl) {
setPreviewImage({ open: true, urls: [imgUrl], index: 0 });

Loading…
Cancel
Save