From 1f1dba30b64c6b331628b8e3803428c752e79128 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Mar 2026 22:50:10 +0000 Subject: [PATCH 1/8] feat: add copy buttons to docs code blocks Co-authored-by: jamesmontemagno <1676321+jamesmontemagno@users.noreply.github.com> Agent-Logs-Url: https://github.com/copilot-dev-days/agent-lab-python/sessions/f398ad34-4328-47f8-b47a-52157fb85dc3 --- docs/step.css | 47 ++++++++++++++++++++++++++++++++++++++++ docs/step.html | 59 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+) 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..72552d4 100644 --- a/docs/step.html +++ b/docs/step.html @@ -282,6 +282,64 @@ }); } + async function copyTextToClipboard(text) { + if (navigator.clipboard?.writeText && window.isSecureContext) { + await navigator.clipboard.writeText(text); + return; + } + + const textArea = document.createElement('textarea'); + textArea.value = text; + textArea.setAttribute('readonly', ''); + textArea.style.cssText = 'position: fixed; inset: auto; opacity: 0; pointer-events: none;'; + document.body.appendChild(textArea); + textArea.select(); + + const successful = document.execCommand('copy'); + textArea.remove(); + + if (!successful) { + 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 code = block.querySelector('code'); + if (!code || 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 resetTimeout; + button.addEventListener('click', async () => { + try { + await copyTextToClipboard(code.textContent ?? ''); + button.textContent = 'Copied!'; + button.classList.add('copied'); + } catch { + button.textContent = 'Failed'; + button.classList.add('failed'); + } + + clearTimeout(resetTimeout); + resetTimeout = setTimeout(() => { + button.textContent = 'Copy'; + button.classList.remove('copied', 'failed'); + }, 1600); + }); + + block.appendChild(button); + }); + } + // Build sidebar navigation function buildSidebar() { const nav = document.getElementById('stepNav'); @@ -390,6 +448,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') { From ba4766e1d5c5dd1efdc755d801c494a34f874035 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Mar 2026 22:51:13 +0000 Subject: [PATCH 2/8] fix: harden clipboard fallback positioning Co-authored-by: jamesmontemagno <1676321+jamesmontemagno@users.noreply.github.com> Agent-Logs-Url: https://github.com/copilot-dev-days/agent-lab-python/sessions/f398ad34-4328-47f8-b47a-52157fb85dc3 --- docs/step.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/step.html b/docs/step.html index 72552d4..049c207 100644 --- a/docs/step.html +++ b/docs/step.html @@ -291,7 +291,7 @@ const textArea = document.createElement('textarea'); textArea.value = text; textArea.setAttribute('readonly', ''); - textArea.style.cssText = 'position: fixed; inset: auto; opacity: 0; pointer-events: none;'; + textArea.style.cssText = 'position: fixed; top: -9999px; left: -9999px; opacity: 0; pointer-events: none;'; document.body.appendChild(textArea); textArea.select(); From 831b96a789a247e7b3baa62195b283e8ced21b11 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Mar 2026 22:51:44 +0000 Subject: [PATCH 3/8] refactor: polish copy button behavior Co-authored-by: jamesmontemagno <1676321+jamesmontemagno@users.noreply.github.com> Agent-Logs-Url: https://github.com/copilot-dev-days/agent-lab-python/sessions/f398ad34-4328-47f8-b47a-52157fb85dc3 --- docs/step.html | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/step.html b/docs/step.html index 049c207..e3bb84e 100644 --- a/docs/step.html +++ b/docs/step.html @@ -283,7 +283,7 @@ } async function copyTextToClipboard(text) { - if (navigator.clipboard?.writeText && window.isSecureContext) { + if (navigator.clipboard?.writeText) { await navigator.clipboard.writeText(text); return; } @@ -318,7 +318,7 @@ button.textContent = 'Copy'; button.setAttribute('aria-label', 'Copy code block'); - let resetTimeout; + let resetTimeoutId; button.addEventListener('click', async () => { try { await copyTextToClipboard(code.textContent ?? ''); @@ -329,8 +329,8 @@ button.classList.add('failed'); } - clearTimeout(resetTimeout); - resetTimeout = setTimeout(() => { + clearTimeout(resetTimeoutId); + resetTimeoutId = setTimeout(() => { button.textContent = 'Copy'; button.classList.remove('copied', 'failed'); }, 1600); From 8f416fc40f10a68c8b01f6e2d924eea3fe5383f5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Mar 2026 22:52:17 +0000 Subject: [PATCH 4/8] fix: use accessible clipboard fallback styles Co-authored-by: jamesmontemagno <1676321+jamesmontemagno@users.noreply.github.com> Agent-Logs-Url: https://github.com/copilot-dev-days/agent-lab-python/sessions/f398ad34-4328-47f8-b47a-52157fb85dc3 --- docs/step.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/step.html b/docs/step.html index e3bb84e..4f876ca 100644 --- a/docs/step.html +++ b/docs/step.html @@ -291,7 +291,7 @@ const textArea = document.createElement('textarea'); textArea.value = text; textArea.setAttribute('readonly', ''); - textArea.style.cssText = 'position: fixed; top: -9999px; left: -9999px; opacity: 0; pointer-events: none;'; + textArea.style.cssText = 'position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; border: 0; overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;'; document.body.appendChild(textArea); textArea.select(); From 85939dc61c2e604c2f8e9a1cfd073339ca407a5f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Mar 2026 22:52:37 +0000 Subject: [PATCH 5/8] chore: clarify copy button timeout state Co-authored-by: jamesmontemagno <1676321+jamesmontemagno@users.noreply.github.com> Agent-Logs-Url: https://github.com/copilot-dev-days/agent-lab-python/sessions/f398ad34-4328-47f8-b47a-52157fb85dc3 --- docs/step.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/step.html b/docs/step.html index 4f876ca..5537be8 100644 --- a/docs/step.html +++ b/docs/step.html @@ -318,7 +318,7 @@ button.textContent = 'Copy'; button.setAttribute('aria-label', 'Copy code block'); - let resetTimeoutId; + let resetTimeoutId = null; button.addEventListener('click', async () => { try { await copyTextToClipboard(code.textContent ?? ''); From d29e5be791c6b8a7b7bfb8d5aab69c664075c468 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Mar 2026 22:52:58 +0000 Subject: [PATCH 6/8] refactor: rename clipboard result flag Co-authored-by: jamesmontemagno <1676321+jamesmontemagno@users.noreply.github.com> Agent-Logs-Url: https://github.com/copilot-dev-days/agent-lab-python/sessions/f398ad34-4328-47f8-b47a-52157fb85dc3 --- docs/step.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/step.html b/docs/step.html index 5537be8..d245464 100644 --- a/docs/step.html +++ b/docs/step.html @@ -295,10 +295,10 @@ document.body.appendChild(textArea); textArea.select(); - const successful = document.execCommand('copy'); + const copySucceeded = document.execCommand('copy'); textArea.remove(); - if (!successful) { + if (!copySucceeded) { throw new Error('Copy failed'); } } From 07311535a3cf0492da3e4ef8a879fce6500a9b9d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Mar 2026 22:53:31 +0000 Subject: [PATCH 7/8] refactor: extract copy feedback constants Co-authored-by: jamesmontemagno <1676321+jamesmontemagno@users.noreply.github.com> Agent-Logs-Url: https://github.com/copilot-dev-days/agent-lab-python/sessions/f398ad34-4328-47f8-b47a-52157fb85dc3 --- docs/step.html | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/docs/step.html b/docs/step.html index d245464..6178f17 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; @@ -291,7 +307,7 @@ const textArea = document.createElement('textarea'); textArea.value = text; textArea.setAttribute('readonly', ''); - textArea.style.cssText = 'position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; border: 0; overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;'; + applyVisuallyHiddenStyles(textArea); document.body.appendChild(textArea); textArea.select(); @@ -333,7 +349,7 @@ resetTimeoutId = setTimeout(() => { button.textContent = 'Copy'; button.classList.remove('copied', 'failed'); - }, 1600); + }, COPY_FEEDBACK_DURATION_MS); }); block.appendChild(button); From e93becac6936a4943b93a7e0668f1d6308c5efe6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Mar 2026 22:53:52 +0000 Subject: [PATCH 8/8] refactor: clarify code element naming Co-authored-by: jamesmontemagno <1676321+jamesmontemagno@users.noreply.github.com> Agent-Logs-Url: https://github.com/copilot-dev-days/agent-lab-python/sessions/f398ad34-4328-47f8-b47a-52157fb85dc3 --- docs/step.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/step.html b/docs/step.html index 6178f17..acaad3f 100644 --- a/docs/step.html +++ b/docs/step.html @@ -325,8 +325,8 @@ const codeBlocks = container.querySelectorAll('pre'); codeBlocks.forEach((block) => { - const code = block.querySelector('code'); - if (!code || block.querySelector('.code-copy-btn')) return; + const codeElement = block.querySelector('code'); + if (!codeElement || block.querySelector('.code-copy-btn')) return; const button = document.createElement('button'); button.type = 'button'; @@ -337,7 +337,7 @@ let resetTimeoutId = null; button.addEventListener('click', async () => { try { - await copyTextToClipboard(code.textContent ?? ''); + await copyTextToClipboard(codeElement.textContent ?? ''); button.textContent = 'Copied!'; button.classList.add('copied'); } catch {