Commit d693142d authored by Steven's avatar Steven

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: 's avatarClaude <noreply@anthropic.com>
parent b00df8a9
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 });
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment