diff --git a/src/client/lib/markdown.ts b/src/client/lib/markdown.ts
index c730150..f6d2a19 100644
--- a/src/client/lib/markdown.ts
+++ b/src/client/lib/markdown.ts
@@ -18,6 +18,15 @@ marked.use(
})
);
+// Custom renderer to wrap code blocks with copy button
+const renderer = new marked.Renderer();
+renderer.code = function ({ text, lang }: { text: string; lang?: string | undefined; escaped?: boolean }) {
+ const langLabel = lang ? `${escapeHtml(lang)}` : "";
+ return `
`;
+};
+
+marked.use({ renderer });
+
export function renderMarkdown(text: string): string {
if (!text) return "";
try {
diff --git a/src/client/main.tsx b/src/client/main.tsx
index 0b11abd..eaf3b04 100644
--- a/src/client/main.tsx
+++ b/src/client/main.tsx
@@ -3,6 +3,23 @@ import ReactDOM from "react-dom/client";
import { App } from "./app.js";
import "./styles/main.css";
+// Delegated click handler for code block copy buttons
+document.addEventListener("click", (e) => {
+ const btn = (e.target as HTMLElement).closest("[data-copy-code]") as HTMLButtonElement | null;
+ if (!btn) return;
+ e.stopPropagation();
+ const wrapper = btn.closest(".code-block-wrapper");
+ const code = wrapper?.querySelector("code");
+ if (!code) return;
+ navigator.clipboard.writeText(code.textContent || "").then(() => {
+ btn.textContent = "Copied!";
+ setTimeout(() => { btn.textContent = "Copy"; }, 1500);
+ }).catch(() => {
+ btn.textContent = "Failed";
+ setTimeout(() => { btn.textContent = "Copy"; }, 1500);
+ });
+});
+
ReactDOM.createRoot(document.getElementById("root")!).render(