Skip to content

Commit 820638a

Browse files
drmowinckelsclaude
andauthored
Add copy-to-clipboard button on code blocks (#576)
* Add copy-to-clipboard button on code blocks Adds a hover-revealed Copy button to every Hugo .highlight block: - Vanilla JS scaffold in copy-code.js, bundled into the existing rladiesplus-bundle. - Click → navigator.clipboard.writeText(code.innerText). Success swaps the icon to a check and label to "Copied"; failure shows X and "Copy failed". Auto-reverts after 2 s. - CSS in syntax.css uses --color-success / --color-error tokens defined alongside the rest of the brand palette (light + dark overrides) — no hardcoded greens/reds. - Toast logic from the previous attempt is dropped; the button itself carries the feedback, which avoids the stacking-context bug that got the earlier prototype reverted. Closes #569. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Switch to render hooks for code copy + add heading anchors - Replace JS-based code-block scanning with a Hugo render hook (layouts/_markup/render-codeblock.html). The hook wraps every code block — language-tagged or not — in a .code-block container with a Copy button. The previous approach only attached to .highlight divs, so unlanguaged fenced blocks got nothing. - Click feedback is now a toast in a body-level #toast-container (avoids the stacking-context bug that killed the prior toast attempt). Success toast uses the brand blue token; error stays red. - Add layouts/_markup/render-heading.html: appends a link-icon anchor to every heading except on the home page. CSS holds it at opacity:0 until heading hover/focus, plus scroll-margin-top so jumped-to anchors don't slide under the fixed header. - Improve dark-mode contrast on .alert-primary (cross-post notice): text was --color-primary-dark (#a152f8) on a barely-tinted bg, ~3.5:1. Switch to --color-primary-lighter on an 18%-mix bg → ~11:1. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Add es/fr/pt translations for copy-code and heading anchor strings Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 375e067 commit 820638a

14 files changed

Lines changed: 205 additions & 1 deletion

File tree

i18n/en.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,3 +185,12 @@ auto_translation: |
185185
This page was auto-translated using
186186
[DeepL](https://www.deepl.com)
187187
and has not been reviewed by a human yet.
188+
189+
# Code blocks ----
190+
copy_code: 'Copy'
191+
copy_code_aria: 'Copy code to clipboard'
192+
copy_code_success: 'Code copied'
193+
copy_code_error: 'Copy failed'
194+
195+
# Headings ----
196+
heading_anchor_aria: 'Link to this heading'

i18n/es.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,3 +185,12 @@ auto_translation: |
185185
Esta página fue traducida automáticamente usando
186186
[DeepL](https://www.deepl.com)
187187
y aún no ha sido revisada por una persona.
188+
189+
# Code blocks ----
190+
copy_code: 'Copiar'
191+
copy_code_aria: 'Copiar código al portapapeles'
192+
copy_code_success: 'Código copiado'
193+
copy_code_error: 'Error al copiar'
194+
195+
# Headings ----
196+
heading_anchor_aria: 'Enlace a este encabezado'

i18n/fr.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,3 +185,12 @@ auto_translation: |
185185
Cette page a été traduite automatiquement avec
186186
[DeepL](https://www.deepl.com)
187187
et n'a pas encore été relue par une personne.
188+
189+
# Code blocks ----
190+
copy_code: 'Copier'
191+
copy_code_aria: 'Copier le code dans le presse-papiers'
192+
copy_code_success: 'Code copié'
193+
copy_code_error: 'Échec de la copie'
194+
195+
# Headings ----
196+
heading_anchor_aria: 'Lien vers ce titre'

i18n/pt.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,3 +185,12 @@ auto_translation: |
185185
Esta página foi traduzida automaticamente usando
186186
[DeepL](https://www.deepl.com)
187187
e ainda não foi revisada por uma pessoa.
188+
189+
# Code blocks ----
190+
copy_code: 'Copiar'
191+
copy_code_aria: 'Copiar código para a área de transferência'
192+
copy_code_success: 'Código copiado'
193+
copy_code_error: 'Falha ao copiar'
194+
195+
# Headings ----
196+
heading_anchor_aria: 'Link para este título'

themes/hugo-rladiesplus/assets/css/components/darkmode.css

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
--color-accent-rose: #f6a4c3;
1515
--color-accent-rose-75: #f2c9dc;
1616
--color-bg: #121220;
17+
--color-success: #86efac;
18+
--color-error: #fca5a5;
1719
--color-surface: #1a1a2e;
1820
--color-surface-alt: #16213e;
1921
--color-surface-footer: #0f0f1a;
@@ -61,6 +63,12 @@
6163
background: color-mix(in srgb, var(--color-primary) 8%, var(--color-raised));
6264
}
6365

66+
/* ── Alerts ── */
67+
.dark .alert-primary {
68+
background: color-mix(in srgb, var(--color-primary) 18%, var(--color-raised));
69+
color: var(--color-primary-lighter);
70+
}
71+
6472
/* ── Callouts ── */
6573
.dark .callout {
6674
background: color-mix(in srgb, var(--callout-color, var(--color-primary)) 10%, var(--color-raised));

themes/hugo-rladiesplus/assets/css/components/syntax.css

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,4 +157,69 @@
157157
.dark .chroma .gp,
158158
.dark .chroma .gu { color: #9ca3af; }
159159
.dark .chroma .gt { color: #7dd3fc; }
160+
161+
.code-block { position: relative; }
162+
163+
.copy-code-btn {
164+
@apply absolute inline-flex items-center gap-1 px-2 py-1 text-xs cursor-pointer;
165+
top: 0.5rem;
166+
right: 0.5rem;
167+
font-family: var(--font-sans);
168+
color: var(--color-muted);
169+
background-color: color-mix(in srgb, var(--color-bg) 85%, transparent);
170+
border: 1px solid var(--color-border);
171+
border-radius: var(--radius-sm);
172+
opacity: 0;
173+
transition: opacity 0.15s ease, color 0.15s ease, border-color 0.15s ease;
174+
}
175+
176+
.code-block:hover .copy-code-btn,
177+
.code-block:focus-within .copy-code-btn,
178+
.copy-code-btn:focus-visible {
179+
opacity: 1;
180+
}
181+
182+
.copy-code-btn:hover {
183+
color: var(--color-primary);
184+
border-color: var(--color-primary);
185+
}
186+
187+
.copy-code-icon svg {
188+
width: 0.875rem;
189+
height: 0.875rem;
190+
}
191+
}
192+
193+
@layer components {
194+
.toast-container {
195+
position: fixed;
196+
bottom: 1.5rem;
197+
right: 1.5rem;
198+
z-index: 9999;
199+
display: flex;
200+
flex-direction: column;
201+
gap: 0.5rem;
202+
pointer-events: none;
203+
}
204+
205+
.toast {
206+
pointer-events: auto;
207+
padding: 0.625rem 1rem;
208+
font-family: var(--font-sans);
209+
font-size: 0.875rem;
210+
color: #fff;
211+
border-radius: var(--radius-sm);
212+
box-shadow: var(--shadow-elevated);
213+
opacity: 0;
214+
transform: translateY(0.5rem);
215+
transition: opacity 0.2s ease, transform 0.2s ease;
216+
}
217+
218+
.toast-visible {
219+
opacity: 1;
220+
transform: translateY(0);
221+
}
222+
223+
.toast-success { background-color: var(--color-accent-blue); }
224+
.toast-error { background-color: #b91c1c; }
160225
}

themes/hugo-rladiesplus/assets/css/components/typography.css

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,4 +179,34 @@
179179
font-size: 0.95em;
180180
letter-spacing: 0.04em;
181181
}
182+
183+
.heading-anchored {
184+
scroll-margin-top: 5rem;
185+
}
186+
187+
.heading-anchor {
188+
display: inline-flex;
189+
align-items: center;
190+
margin-left: 0.4em;
191+
color: var(--color-primary);
192+
text-decoration: none;
193+
opacity: 0;
194+
transition: opacity 0.15s ease, color 0.15s ease;
195+
vertical-align: middle;
196+
}
197+
198+
.heading-anchor svg {
199+
width: 0.7em;
200+
height: 0.7em;
201+
}
202+
203+
.heading-anchored:hover .heading-anchor,
204+
.heading-anchored:focus-within .heading-anchor,
205+
.heading-anchor:focus-visible {
206+
opacity: 1;
207+
}
208+
209+
.heading-anchor:hover {
210+
color: var(--color-primary-dark);
211+
}
182212
}

themes/hugo-rladiesplus/assets/css/main.css

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@
2929
--color-light: #ededf4;
3030
--color-bg: #ededf4;
3131
--color-muted: #5d5f66;
32+
--color-success: #0a5030;
33+
--color-error: #b91c1c;
3234
--color-surface: #f8f8fc;
3335
--color-surface-alt: #ebe3f6;
3436
--color-surface-footer: #ddd0f0;

themes/hugo-rladiesplus/assets/css/vendor/tailwind.css

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
(function () {
2+
if (!navigator.clipboard || !navigator.clipboard.writeText) return;
3+
4+
document.addEventListener('click', function (e) {
5+
var btn = e.target.closest && e.target.closest('.copy-code-btn');
6+
if (!btn) return;
7+
var wrapper = btn.closest('.code-block');
8+
if (!wrapper) return;
9+
var code = wrapper.querySelector('code');
10+
if (!code) return;
11+
12+
navigator.clipboard.writeText(code.innerText).then(
13+
function () { showToast(btn.dataset.successMsg || 'Code copied', 'success'); },
14+
function () { showToast(btn.dataset.errorMsg || 'Copy failed', 'error'); }
15+
);
16+
});
17+
18+
function showToast(text, state) {
19+
var container = document.getElementById('toast-container');
20+
if (!container) return;
21+
var toast = document.createElement('div');
22+
toast.className = 'toast toast-' + state;
23+
toast.setAttribute('role', state === 'error' ? 'alert' : 'status');
24+
toast.textContent = text;
25+
container.appendChild(toast);
26+
requestAnimationFrame(function () {
27+
toast.classList.add('toast-visible');
28+
});
29+
setTimeout(function () {
30+
toast.classList.remove('toast-visible');
31+
setTimeout(function () { toast.remove(); }, 250);
32+
}, 2000);
33+
}
34+
})();

0 commit comments

Comments
 (0)