webui: add copy button to markdown code blocks
Co-Authored-By: sketch <hello@sketch.dev>
Change-ID: skad1b3ffhsk
diff --git a/webui/src/web-components/sketch-timeline-message.ts b/webui/src/web-components/sketch-timeline-message.ts
index 1edaff2..8a4f7d5 100644
--- a/webui/src/web-components/sketch-timeline-message.ts
+++ b/webui/src/web-components/sketch-timeline-message.ts
@@ -233,6 +233,75 @@
box-sizing: border-box; /* Include padding in width calculation */
}
+ /* Code block container styles */
+ .code-block-container {
+ position: relative;
+ margin: 8px 0;
+ border-radius: 6px;
+ overflow: hidden;
+ background: rgba(0, 0, 0, 0.05);
+ }
+
+ .user .code-block-container {
+ background: rgba(255, 255, 255, 0.2);
+ }
+
+ .code-block-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 4px 8px;
+ background: rgba(0, 0, 0, 0.1);
+ font-size: 12px;
+ }
+
+ .user .code-block-header {
+ background: rgba(255, 255, 255, 0.2);
+ color: white;
+ }
+
+ .code-language {
+ font-family: monospace;
+ font-size: 11px;
+ font-weight: 500;
+ }
+
+ .code-copy-button {
+ background: transparent;
+ border: none;
+ color: inherit;
+ cursor: pointer;
+ padding: 2px;
+ border-radius: 3px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ opacity: 0.7;
+ transition: all 0.15s ease;
+ }
+
+ .code-copy-button:hover {
+ opacity: 1;
+ background: rgba(0, 0, 0, 0.1);
+ }
+
+ .user .code-copy-button:hover {
+ background: rgba(255, 255, 255, 0.2);
+ }
+
+ .code-block-container pre {
+ margin: 0;
+ padding: 8px;
+ background: transparent;
+ }
+
+ .code-block-container code {
+ background: transparent;
+ padding: 0;
+ display: block;
+ width: 100%;
+ }
+
.user .message-text pre,
.user .message-text code {
background: rgba(255, 255, 255, 0.2);
@@ -549,6 +618,7 @@
updated(changedProperties: Map<string, unknown>) {
super.updated(changedProperties);
this.renderMermaidDiagrams();
+ this.setupCodeBlockCopyButtons();
}
// Render mermaid diagrams after the component is updated
@@ -589,6 +659,56 @@
}, 100); // Small delay to ensure DOM is ready
}
+ // Setup code block copy buttons after component is updated
+ setupCodeBlockCopyButtons() {
+ setTimeout(() => {
+ // Find all copy buttons in code blocks
+ const copyButtons =
+ this.shadowRoot?.querySelectorAll(".code-copy-button");
+ if (!copyButtons || copyButtons.length === 0) return;
+
+ // Add click event listener to each button
+ copyButtons.forEach((button) => {
+ button.addEventListener("click", (e) => {
+ e.stopPropagation();
+ const codeId = (button as HTMLElement).dataset.codeId;
+ if (!codeId) return;
+
+ const codeElement = this.shadowRoot?.querySelector(`#${codeId}`);
+ if (!codeElement) return;
+
+ const codeText = codeElement.textContent || "";
+ const buttonRect = button.getBoundingClientRect();
+
+ // Copy code to clipboard
+ navigator.clipboard
+ .writeText(codeText)
+ .then(() => {
+ // Show success indicator
+ const originalHTML = button.innerHTML;
+ button.innerHTML = `
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+ <path d="M20 6L9 17l-5-5"></path>
+ </svg>
+ `;
+
+ // Display floating message
+ this.showFloatingMessage("Copied!", buttonRect, "success");
+
+ // Reset button after delay
+ setTimeout(() => {
+ button.innerHTML = originalHTML;
+ }, 2000);
+ })
+ .catch((err) => {
+ console.error("Failed to copy code:", err);
+ this.showFloatingMessage("Failed to copy!", buttonRect, "error");
+ });
+ });
+ });
+ }, 100); // Small delay to ensure DOM is ready
+ }
+
// See https://lit.dev/docs/components/lifecycle/
disconnectedCallback() {
super.disconnectedCallback();
@@ -600,7 +720,7 @@
const renderer = new Renderer();
const originalCodeRenderer = renderer.code.bind(renderer);
- // Override the code renderer to handle mermaid diagrams
+ // Override the code renderer to handle mermaid diagrams and add copy buttons
renderer.code = function ({ text, lang, escaped }: Tokens.Code): string {
if (lang === "mermaid") {
// Generate a unique ID for this diagram
@@ -611,8 +731,23 @@
<div class="mermaid" id="${id}">${text}</div>
</div>`;
}
- // Default rendering for other code blocks
- return originalCodeRenderer({ text, lang, escaped });
+
+ // For regular code blocks, add a copy button
+ const id = `code-block-${Math.random().toString(36).substring(2, 10)}`;
+ const langClass = lang ? ` class="language-${lang}"` : "";
+
+ return `<div class="code-block-container">
+ <div class="code-block-header">
+ ${lang ? `<span class="code-language">${lang}</span>` : ""}
+ <button class="code-copy-button" title="Copy code" data-code-id="${id}">
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+ <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
+ <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
+ </svg>
+ </button>
+ </div>
+ <pre><code id="${id}"${langClass}>${text}</code></pre>
+ </div>`;
};
// Set markdown options for proper code block highlighting and safety