diff --git a/docs/step.css b/docs/step.css index 4abc40a..4d7c380 100644 --- a/docs/step.css +++ b/docs/step.css @@ -338,6 +338,8 @@ padding: 1.25rem; overflow-x: auto; margin: 1.5rem 0; + position: relative; + padding-top: 3.25rem; } .markdown pre code { @@ -346,6 +348,51 @@ padding: 0; } + .code-copy-btn { + position: absolute; + top: 0.9rem; + right: 1rem; + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 4.75rem; + padding: 0.35rem 0.75rem; + border: 1px solid rgba(0, 245, 255, 0.25); + border-radius: 999px; + background: rgba(10, 10, 15, 0.88); + color: var(--neon-cyan); + font-family: 'Space Grotesk', sans-serif; + font-size: 0.75rem; + font-weight: 700; + letter-spacing: 0.04em; + text-transform: uppercase; + cursor: pointer; + transition: transform 0.2s ease, border-color 0.2s ease, color 0.2s ease, background 0.2s ease; + } + + .code-copy-btn:hover { + transform: translateY(-1px); + border-color: var(--neon-cyan); + background: rgba(0, 245, 255, 0.12); + } + + .code-copy-btn:focus-visible { + outline: 2px solid var(--neon-cyan); + outline-offset: 2px; + } + + .code-copy-btn.copied { + color: var(--success-green); + border-color: rgba(78, 201, 176, 0.45); + background: rgba(78, 201, 176, 0.12); + } + + .code-copy-btn.failed { + color: var(--warning-yellow); + border-color: rgba(220, 220, 170, 0.45); + background: rgba(220, 220, 170, 0.12); + } + /* Blockquotes / Tips */ .markdown blockquote { background: var(--bg-card); diff --git a/docs/step.html b/docs/step.html index 990edf4..acaad3f 100644 --- a/docs/step.html +++ b/docs/step.html @@ -221,6 +221,7 @@ ? '../workshop/' : 'https://raw.githubusercontent.com/copilot-dev-days/agent-lab-python/main/workshop/'; const CHECKBOX_STORAGE_KEY = 'agent-lab-checkboxes-v1'; + const COPY_FEEDBACK_DURATION_MS = 1600; let checkboxSaveTimeout; // Get current step from URL @@ -255,6 +256,21 @@ checkboxSaveTimeout = setTimeout(() => writeCheckboxState(state), 100); } + function applyVisuallyHiddenStyles(element) { + Object.assign(element.style, { + position: 'absolute', + width: '1px', + height: '1px', + padding: '0', + margin: '-1px', + border: '0', + overflow: 'hidden', + clip: 'rect(0 0 0 0)', + clipPath: 'inset(50%)', + whiteSpace: 'nowrap' + }); + } + function initializeTaskListCheckboxes(stepId) { const container = document.getElementById('markdown-content'); if (!container) return; @@ -282,6 +298,64 @@ }); } + async function copyTextToClipboard(text) { + if (navigator.clipboard?.writeText) { + await navigator.clipboard.writeText(text); + return; + } + + const textArea = document.createElement('textarea'); + textArea.value = text; + textArea.setAttribute('readonly', ''); + applyVisuallyHiddenStyles(textArea); + document.body.appendChild(textArea); + textArea.select(); + + const copySucceeded = document.execCommand('copy'); + textArea.remove(); + + if (!copySucceeded) { + throw new Error('Copy failed'); + } + } + + function initializeCodeBlockCopyButtons() { + const container = document.getElementById('markdown-content'); + if (!container) return; + + const codeBlocks = container.querySelectorAll('pre'); + codeBlocks.forEach((block) => { + const codeElement = block.querySelector('code'); + if (!codeElement || block.querySelector('.code-copy-btn')) return; + + const button = document.createElement('button'); + button.type = 'button'; + button.className = 'code-copy-btn'; + button.textContent = 'Copy'; + button.setAttribute('aria-label', 'Copy code block'); + + let resetTimeoutId = null; + button.addEventListener('click', async () => { + try { + await copyTextToClipboard(codeElement.textContent ?? ''); + button.textContent = 'Copied!'; + button.classList.add('copied'); + } catch { + button.textContent = 'Failed'; + button.classList.add('failed'); + } + + clearTimeout(resetTimeoutId); + resetTimeoutId = setTimeout(() => { + button.textContent = 'Copy'; + button.classList.remove('copied', 'failed'); + }, COPY_FEEDBACK_DURATION_MS); + }); + + block.appendChild(button); + }); + } + // Build sidebar navigation function buildSidebar() { const nav = document.getElementById('stepNav'); @@ -390,6 +464,7 @@ document.getElementById('markdown-content').innerHTML = marked.parse(md); initializeTaskListCheckboxes(step.id); + initializeCodeBlockCopyButtons(); // If this is the completion page, add confetti! if (step.id === '05-complete') {