Skip to content

Commit 9efa84f

Browse files
Merge pull request #4 from copilot-dev-days/copilot/add-copy-buttons-to-code-blocks
Add copy controls to docs code blocks
2 parents d08cb72 + e93beca commit 9efa84f

2 files changed

Lines changed: 122 additions & 0 deletions

File tree

docs/step.css

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,8 @@
338338
padding: 1.25rem;
339339
overflow-x: auto;
340340
margin: 1.5rem 0;
341+
position: relative;
342+
padding-top: 3.25rem;
341343
}
342344

343345
.markdown pre code {
@@ -346,6 +348,51 @@
346348
padding: 0;
347349
}
348350

351+
.code-copy-btn {
352+
position: absolute;
353+
top: 0.9rem;
354+
right: 1rem;
355+
display: inline-flex;
356+
align-items: center;
357+
justify-content: center;
358+
min-width: 4.75rem;
359+
padding: 0.35rem 0.75rem;
360+
border: 1px solid rgba(0, 245, 255, 0.25);
361+
border-radius: 999px;
362+
background: rgba(10, 10, 15, 0.88);
363+
color: var(--neon-cyan);
364+
font-family: 'Space Grotesk', sans-serif;
365+
font-size: 0.75rem;
366+
font-weight: 700;
367+
letter-spacing: 0.04em;
368+
text-transform: uppercase;
369+
cursor: pointer;
370+
transition: transform 0.2s ease, border-color 0.2s ease, color 0.2s ease, background 0.2s ease;
371+
}
372+
373+
.code-copy-btn:hover {
374+
transform: translateY(-1px);
375+
border-color: var(--neon-cyan);
376+
background: rgba(0, 245, 255, 0.12);
377+
}
378+
379+
.code-copy-btn:focus-visible {
380+
outline: 2px solid var(--neon-cyan);
381+
outline-offset: 2px;
382+
}
383+
384+
.code-copy-btn.copied {
385+
color: var(--success-green);
386+
border-color: rgba(78, 201, 176, 0.45);
387+
background: rgba(78, 201, 176, 0.12);
388+
}
389+
390+
.code-copy-btn.failed {
391+
color: var(--warning-yellow);
392+
border-color: rgba(220, 220, 170, 0.45);
393+
background: rgba(220, 220, 170, 0.12);
394+
}
395+
349396
/* Blockquotes / Tips */
350397
.markdown blockquote {
351398
background: var(--bg-card);

docs/step.html

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,7 @@
221221
? '../workshop/'
222222
: 'https://raw.githubusercontent.com/copilot-dev-days/agent-lab-python/main/workshop/';
223223
const CHECKBOX_STORAGE_KEY = 'agent-lab-checkboxes-v1';
224+
const COPY_FEEDBACK_DURATION_MS = 1600;
224225
let checkboxSaveTimeout;
225226

226227
// Get current step from URL
@@ -255,6 +256,21 @@
255256
checkboxSaveTimeout = setTimeout(() => writeCheckboxState(state), 100);
256257
}
257258

259+
function applyVisuallyHiddenStyles(element) {
260+
Object.assign(element.style, {
261+
position: 'absolute',
262+
width: '1px',
263+
height: '1px',
264+
padding: '0',
265+
margin: '-1px',
266+
border: '0',
267+
overflow: 'hidden',
268+
clip: 'rect(0 0 0 0)',
269+
clipPath: 'inset(50%)',
270+
whiteSpace: 'nowrap'
271+
});
272+
}
273+
258274
function initializeTaskListCheckboxes(stepId) {
259275
const container = document.getElementById('markdown-content');
260276
if (!container) return;
@@ -282,6 +298,64 @@
282298
});
283299
}
284300

301+
async function copyTextToClipboard(text) {
302+
if (navigator.clipboard?.writeText) {
303+
await navigator.clipboard.writeText(text);
304+
return;
305+
}
306+
307+
const textArea = document.createElement('textarea');
308+
textArea.value = text;
309+
textArea.setAttribute('readonly', '');
310+
applyVisuallyHiddenStyles(textArea);
311+
document.body.appendChild(textArea);
312+
textArea.select();
313+
314+
const copySucceeded = document.execCommand('copy');
315+
textArea.remove();
316+
317+
if (!copySucceeded) {
318+
throw new Error('Copy failed');
319+
}
320+
}
321+
322+
function initializeCodeBlockCopyButtons() {
323+
const container = document.getElementById('markdown-content');
324+
if (!container) return;
325+
326+
const codeBlocks = container.querySelectorAll('pre');
327+
codeBlocks.forEach((block) => {
328+
const codeElement = block.querySelector('code');
329+
if (!codeElement || block.querySelector('.code-copy-btn')) return;
330+
331+
const button = document.createElement('button');
332+
button.type = 'button';
333+
button.className = 'code-copy-btn';
334+
button.textContent = 'Copy';
335+
button.setAttribute('aria-label', 'Copy code block');
336+
337+
let resetTimeoutId = null;
338+
button.addEventListener('click', async () => {
339+
try {
340+
await copyTextToClipboard(codeElement.textContent ?? '');
341+
button.textContent = 'Copied!';
342+
button.classList.add('copied');
343+
} catch {
344+
button.textContent = 'Failed';
345+
button.classList.add('failed');
346+
}
347+
348+
clearTimeout(resetTimeoutId);
349+
resetTimeoutId = setTimeout(() => {
350+
button.textContent = 'Copy';
351+
button.classList.remove('copied', 'failed');
352+
}, COPY_FEEDBACK_DURATION_MS);
353+
});
354+
355+
block.appendChild(button);
356+
});
357+
}
358+
285359
// Build sidebar navigation
286360
function buildSidebar() {
287361
const nav = document.getElementById('stepNav');
@@ -390,6 +464,7 @@
390464

391465
document.getElementById('markdown-content').innerHTML = marked.parse(md);
392466
initializeTaskListCheckboxes(step.id);
467+
initializeCodeBlockCopyButtons();
393468

394469
// If this is the completion page, add confetti!
395470
if (step.id === '05-complete') {

0 commit comments

Comments
 (0)