Skip to content

Commit ce966cf

Browse files
Merge pull request #5 from copilot-dev-days/copilot/add-copy-buttons-to-code-blocks
Add copy actions to workshop code blocks
2 parents 21ac866 + 6727422 commit ce966cf

2 files changed

Lines changed: 125 additions & 1 deletion

File tree

docs/step.css

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
--border-color: #2a2a3a;
1111
--success-green: #4ec9b0;
1212
--warning-yellow: #dcdcaa;
13+
--code-copy-bg: rgba(10, 10, 15, 0.85);
14+
--code-copy-offset: 2.75rem;
1315
}
1416

1517
* {
@@ -335,9 +337,10 @@
335337
background: var(--bg-code);
336338
border: 1px solid var(--border-color);
337339
border-radius: 8px;
338-
padding: 1.25rem;
340+
padding: var(--code-copy-offset) 1.25rem 1.25rem;
339341
overflow-x: auto;
340342
margin: 1.5rem 0;
343+
position: relative;
341344
}
342345

343346
.markdown pre code {
@@ -346,6 +349,43 @@
346349
padding: 0;
347350
}
348351

352+
.code-copy-button {
353+
position: absolute;
354+
top: 0.75rem;
355+
right: 0.75rem;
356+
border: 1px solid var(--border-color);
357+
border-radius: 999px;
358+
background: var(--code-copy-bg);
359+
color: var(--text-secondary);
360+
font-family: inherit;
361+
font-size: 0.75rem;
362+
font-weight: 700;
363+
letter-spacing: 0.04em;
364+
text-transform: uppercase;
365+
padding: 0.35rem 0.8rem;
366+
cursor: pointer;
367+
transition: all 0.2s ease;
368+
backdrop-filter: blur(8px);
369+
}
370+
371+
.code-copy-button:hover,
372+
.code-copy-button:focus-visible {
373+
border-color: var(--neon-cyan);
374+
color: var(--neon-cyan);
375+
outline: none;
376+
transform: translateY(-1px);
377+
}
378+
379+
.code-copy-button[data-copy-state="copied"] {
380+
border-color: var(--success-green);
381+
color: var(--success-green);
382+
}
383+
384+
.code-copy-button[data-copy-state="error"] {
385+
border-color: var(--neon-magenta);
386+
color: var(--neon-magenta);
387+
}
388+
349389
/* Blockquotes / Tips */
350390
.markdown blockquote {
351391
background: var(--bg-card);

docs/step.html

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,88 @@
304304
return md;
305305
}
306306

307+
const resetTimers = new WeakMap();
308+
309+
function resetCopyButton(button) {
310+
const existingTimerId = resetTimers.get(button);
311+
if (existingTimerId) {
312+
window.clearTimeout(existingTimerId);
313+
}
314+
315+
const timerId = window.setTimeout(() => {
316+
button.textContent = 'Copy';
317+
button.setAttribute('aria-label', 'Copy code block');
318+
button.dataset.copyState = 'idle';
319+
}, 2000);
320+
resetTimers.set(button, timerId);
321+
}
322+
323+
async function copyText(text) {
324+
if (navigator.clipboard?.writeText) {
325+
try {
326+
await navigator.clipboard.writeText(text);
327+
return;
328+
} catch (error) {
329+
// Fall back below when the Clipboard API is unavailable or denied.
330+
}
331+
}
332+
333+
const textArea = document.createElement('textarea');
334+
textArea.value = text;
335+
textArea.setAttribute('readonly', '');
336+
textArea.style.position = 'absolute';
337+
textArea.style.left = '-9999px';
338+
document.body.appendChild(textArea);
339+
textArea.select();
340+
341+
try {
342+
if (!document.execCommand('copy')) {
343+
throw new Error('Failed to copy code to clipboard');
344+
}
345+
} finally {
346+
document.body.removeChild(textArea);
347+
}
348+
}
349+
350+
function enhanceCodeBlocks() {
351+
document.querySelectorAll('#markdown-content pre code').forEach((code) => {
352+
const pre = code.parentElement;
353+
if (!pre || pre.querySelector('.code-copy-button')) {
354+
return;
355+
}
356+
357+
const text = code?.textContent?.trimEnd();
358+
if (!text) {
359+
return;
360+
}
361+
362+
const button = document.createElement('button');
363+
button.type = 'button';
364+
button.className = 'code-copy-button';
365+
button.textContent = 'Copy';
366+
button.dataset.copyState = 'idle';
367+
button.setAttribute('aria-label', 'Copy code block');
368+
button.setAttribute('aria-live', 'polite');
369+
370+
button.addEventListener('click', async () => {
371+
try {
372+
await copyText(text);
373+
button.textContent = 'Copied!';
374+
button.setAttribute('aria-label', 'Code copied');
375+
button.dataset.copyState = 'copied';
376+
} catch (error) {
377+
button.textContent = 'Failed';
378+
button.setAttribute('aria-label', 'Copy failed');
379+
button.dataset.copyState = 'error';
380+
}
381+
382+
resetCopyButton(button);
383+
});
384+
385+
pre.appendChild(button);
386+
});
387+
}
388+
307389
// Load and render markdown
308390
async function loadContent() {
309391
const idx = getCurrentStepIndex();
@@ -342,6 +424,8 @@
342424
checkbox.disabled = false;
343425
});
344426

427+
enhanceCodeBlocks();
428+
345429
// If this is the completion page, add confetti!
346430
if (step.id === '05-complete') {
347431
// Trigger celebration on load

0 commit comments

Comments
 (0)