Skip to content

Commit b37c9af

Browse files
Merge pull request #4 from copilot-dev-days/copilot/add-copy-buttons-to-code-blocks
Add copy controls to workshop code blocks
2 parents 8fa3179 + 9d183e9 commit b37c9af

2 files changed

Lines changed: 119 additions & 1 deletion

File tree

docs/step.css

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
--bg-dark: #0a0a0f;
33
--bg-card: #12121a;
44
--bg-code: #1a1a2e;
5+
--bg-overlay: rgba(18, 18, 26, 0.92);
56
--neon-cyan: #00f5ff;
67
--neon-magenta: #ff00ff;
78
--neon-purple: #b366ff;
@@ -10,6 +11,8 @@
1011
--border-color: #2a2a3a;
1112
--success-green: #4ec9b0;
1213
--warning-yellow: #dcdcaa;
14+
--code-block-top-padding: 3.5rem;
15+
--code-block-horizontal-padding: 1.25rem;
1316
}
1417

1518
* { margin: 0; padding: 0; box-sizing: border-box; }
@@ -159,10 +162,43 @@
159162
border-radius: 4px; color: var(--warning-yellow);
160163
}
161164
.markdown pre {
165+
position: relative;
162166
background: var(--bg-code); border: 1px solid var(--border-color);
163-
border-radius: 8px; padding: 1.25rem; overflow-x: auto; margin: 1.5rem 0;
167+
border-radius: 8px;
168+
padding: var(--code-block-top-padding) var(--code-block-horizontal-padding) var(--code-block-horizontal-padding) var(--code-block-horizontal-padding);
169+
overflow-x: auto; margin: 1.5rem 0;
164170
}
165171
.markdown pre code { color: var(--text-primary); background: none; padding: 0; }
172+
.markdown .code-copy-btn {
173+
position: absolute; top: 0.75rem; right: 0.75rem;
174+
display: inline-flex; align-items: center; justify-content: center;
175+
min-width: max-content; padding: 0.4rem 0.75rem;
176+
border: 1px solid var(--border-color); border-radius: 999px;
177+
background: var(--bg-overlay); color: var(--text-secondary);
178+
font: inherit; font-size: 0.8rem; cursor: pointer; transition: all 0.2s ease;
179+
}
180+
.markdown .code-copy-btn:hover,
181+
.markdown .code-copy-btn:focus-visible {
182+
border-color: var(--neon-cyan); color: var(--neon-cyan);
183+
outline: none;
184+
}
185+
.markdown .code-copy-btn[data-state="copied"] {
186+
border-color: var(--success-green); color: var(--success-green);
187+
}
188+
.markdown .code-copy-btn[data-state="error"] {
189+
border-color: var(--neon-magenta); color: var(--neon-magenta);
190+
}
191+
.sr-only {
192+
position: absolute;
193+
width: 1px;
194+
height: 1px;
195+
padding: 0;
196+
margin: -1px;
197+
overflow: hidden;
198+
clip: rect(0, 0, 0, 0);
199+
white-space: nowrap;
200+
border: 0;
201+
}
166202

167203
.markdown blockquote {
168204
background: var(--bg-card); border-left: 4px solid var(--neon-cyan);

docs/step.html

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@
8181

8282
const GITHUB_RAW_BASE = 'https://raw.githubusercontent.com/copilot-dev-days/agent-lab-typescript/main/workshop/';
8383
const CHECKBOX_STATE_KEY_PREFIX = 'workshop-checkboxes:';
84+
const COPY_STATUS_RESET_DELAY = 2000;
85+
const pendingCopyResetTimers = new Set();
8486

8587
function getCurrentStepId() {
8688
const params = new URLSearchParams(window.location.search);
@@ -169,6 +171,7 @@
169171

170172
function setupInteractiveCheckboxes(stepId) {
171173
const container = document.getElementById('markdown-content');
174+
if (!container) return;
172175
const checkboxes = container.querySelectorAll('input[type="checkbox"]');
173176
if (!checkboxes.length) return;
174177

@@ -198,6 +201,81 @@
198201
});
199202
}
200203

204+
async function copyCodeToClipboard(text) {
205+
if (navigator.clipboard?.writeText) {
206+
await navigator.clipboard.writeText(text);
207+
return;
208+
}
209+
210+
const textarea = document.createElement('textarea');
211+
textarea.value = text;
212+
textarea.setAttribute('readonly', '');
213+
textarea.style.position = 'absolute';
214+
textarea.style.left = '-9999px';
215+
document.body.appendChild(textarea);
216+
textarea.select();
217+
// Keep the deprecated fallback for older browsers that lack navigator.clipboard.
218+
const copied = document.execCommand('copy');
219+
document.body.removeChild(textarea);
220+
if (!copied) throw new Error('Fallback copy failed');
221+
}
222+
223+
function setupCodeBlockCopyButtons() {
224+
const container = document.getElementById('markdown-content');
225+
if (!container) return;
226+
const codeBlocks = container.querySelectorAll('pre');
227+
228+
codeBlocks.forEach((pre) => {
229+
const code = pre.querySelector('code');
230+
const codeText = code?.textContent?.trim();
231+
if (!codeText) return;
232+
233+
const copyButton = document.createElement('button');
234+
copyButton.type = 'button';
235+
copyButton.className = 'code-copy-btn';
236+
copyButton.textContent = 'Copy';
237+
copyButton.setAttribute('aria-label', 'Copy code');
238+
239+
const liveRegion = document.createElement('span');
240+
liveRegion.className = 'sr-only';
241+
liveRegion.setAttribute('aria-live', 'polite');
242+
let resetTimer = null;
243+
244+
const resetButtonState = () => {
245+
copyButton.textContent = 'Copy';
246+
copyButton.dataset.state = 'idle';
247+
liveRegion.textContent = '';
248+
};
249+
250+
copyButton.addEventListener('click', async () => {
251+
if (resetTimer) {
252+
clearTimeout(resetTimer);
253+
pendingCopyResetTimers.delete(resetTimer);
254+
}
255+
256+
try {
257+
await copyCodeToClipboard(codeText);
258+
copyButton.textContent = 'Copied!';
259+
copyButton.dataset.state = 'copied';
260+
liveRegion.textContent = 'Code copied to clipboard';
261+
} catch {
262+
copyButton.textContent = 'Failed';
263+
copyButton.dataset.state = 'error';
264+
liveRegion.textContent = 'Unable to copy code';
265+
}
266+
267+
resetTimer = setTimeout(() => {
268+
pendingCopyResetTimers.delete(resetTimer);
269+
resetTimer = null;
270+
resetButtonState();
271+
}, COPY_STATUS_RESET_DELAY);
272+
pendingCopyResetTimers.add(resetTimer);
273+
});
274+
275+
pre.append(copyButton, liveRegion);
276+
});
277+
}
278+
201279
async function loadContent() {
202280
const idx = getCurrentStepIndex();
203281
if (idx === -1) {
@@ -209,6 +287,9 @@
209287
document.title = `${step.title} | VS Code Copilot Agent Lab`;
210288

211289
try {
290+
pendingCopyResetTimers.forEach(timer => clearTimeout(timer));
291+
pendingCopyResetTimers.clear();
292+
212293
const response = await fetch(`${GITHUB_RAW_BASE}${step.file}`);
213294
if (!response.ok) throw new Error('Failed to load');
214295

@@ -225,6 +306,7 @@
225306
marked.setOptions({ breaks: true, gfm: true });
226307
document.getElementById('markdown-content').innerHTML = marked.parse(md);
227308
setupInteractiveCheckboxes(step.id);
309+
setupCodeBlockCopyButtons();
228310

229311
if (step.id === '05-complete') {
230312
setTimeout(celebrate, 500);

0 commit comments

Comments
 (0)