Copy buttons on code blocks + merge Skills and Tutorials under Docs

Two small-to-medium improvements bundled together.

1. Copy buttons on every rendered code block.
   - render-markdown.js: wrap each fenced code block in a .code-block-wrap
     container and emit a <button class="code-block-copy" data-copy="...">
     alongside it. Button text is set via CSS ::before content so the
     'Copy' / 'Copied' label is a single toggle class (.is-copied).
   - render-page.js: 12-line inline script at the end of the body wires
     a delegated click handler that calls navigator.clipboard.writeText
     and flips .is-copied for 1.5s.
   - sub-pages.css: button styles matching the dark terminal palette,
     hidden until you hover the code block, accent-colored success state.

2. Merge Skills and Tutorials under a single Docs nav item.
   - Rename the Skills nav link to 'Docs' in every header (partial +
     4 hand-authored pages). Drop the separate Tutorials nav item; it
     now lives inside Docs. Anti-patterns stays as its own top-level.
   - scripts/build-sub-pages.js: replace renderSkillsSidebar and
     renderTutorialsSidebar with a unified renderDocsSidebar that shows
     every skill category followed by a Tutorials group. Takes a
     current descriptor of shape { kind: 'skill'|'tutorial', id|slug }
     so both skill detail and tutorial detail pages can mark the active
     row. activeNav on every /skills/* and /tutorials/* page is now
     'docs'; the shared site header's data-nav matches.

Verified: /skills/polish and /tutorials/getting-started both render
with the unified Docs sidebar (all 21 skills grouped by category +
both tutorials as a final group). The Docs nav item is aria-current
on both. Copy buttons appear on every fenced code block and toggle
to 'Copied' when clicked.
This commit is contained in:
Paul Bakaus
2026-04-08 10:50:23 -07:00
parent a89f7f5040
commit eb130c4af9
9 changed files with 120 additions and 56 deletions
+58 -1
View File
@@ -1312,8 +1312,16 @@ main#main {
color: var(--color-ink);
}
.prose .code-block {
.prose .code-block-wrap {
position: relative;
margin: 1.25em 0;
}
.prose .code-block-wrap .code-block {
margin: 0;
}
.prose .code-block {
padding: var(--spacing-md);
background: oklch(12% 0.005 350);
color: oklch(92% 0.005 350);
@@ -1325,6 +1333,55 @@ main#main {
border: 1px solid oklch(20% 0.005 350);
}
.code-block-copy {
position: absolute;
top: 10px;
right: 10px;
padding: 4px 10px;
background: oklch(20% 0.005 350);
border: 1px solid oklch(30% 0.005 350);
color: oklch(78% 0.005 350);
border-radius: 4px;
font-family: var(--font-mono);
font-size: 0.6875rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.08em;
cursor: pointer;
opacity: 0;
transition:
opacity var(--duration-fast) var(--ease-out),
background var(--duration-fast) var(--ease-out),
color var(--duration-fast) var(--ease-out),
border-color var(--duration-fast) var(--ease-out);
}
.code-block-wrap:hover .code-block-copy,
.code-block-copy:focus-visible {
opacity: 1;
}
.code-block-copy::before {
content: "Copy";
}
.code-block-copy:hover {
background: oklch(30% 0.005 350);
color: oklch(92% 0.005 350);
border-color: oklch(40% 0.005 350);
}
.code-block-copy.is-copied {
opacity: 1;
background: var(--color-accent);
color: var(--color-paper);
border-color: var(--color-accent);
}
.code-block-copy.is-copied::before {
content: "Copied";
}
.prose .code-block code {
background: transparent;
border: none;