mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-12 06:06:37 +03:00
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:
+40
-44
@@ -110,15 +110,19 @@ ${bodyHtml}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the left sidebar used across the /skills section.
|
||||
* Shows every skill grouped by category. Pass the current skill id to
|
||||
* mark it with aria-current="page".
|
||||
* Render the unified Docs sidebar used across /skills and /tutorials.
|
||||
* Shows every skill grouped by category, then tutorials as a final
|
||||
* group. Pass the current page identifier so we can mark it:
|
||||
*
|
||||
* { kind: 'skill', id: 'polish' }
|
||||
* { kind: 'tutorial', slug: 'getting-started' }
|
||||
* null (no current page)
|
||||
*/
|
||||
function renderSkillsSidebar(skillsByCategory, currentSkillId = null) {
|
||||
function renderDocsSidebar(skillsByCategory, tutorials, current = null) {
|
||||
let html = `
|
||||
<aside class="skills-sidebar" aria-label="All skills">
|
||||
<aside class="skills-sidebar" aria-label="Documentation">
|
||||
<div class="skills-sidebar-inner">
|
||||
<p class="skills-sidebar-label">Skills</p>
|
||||
<p class="skills-sidebar-label">Docs</p>
|
||||
`;
|
||||
|
||||
for (const category of CATEGORY_ORDER) {
|
||||
@@ -130,8 +134,26 @@ function renderSkillsSidebar(skillsByCategory, currentSkillId = null) {
|
||||
<ul class="skills-sidebar-list">
|
||||
${list
|
||||
.map((s) => {
|
||||
const current = s.id === currentSkillId ? ' aria-current="page"' : '';
|
||||
return ` <li><a href="/skills/${s.id}"${current}>/${escapeHtml(s.id)}</a></li>`;
|
||||
const isCurrent = current?.kind === 'skill' && current.id === s.id;
|
||||
const attr = isCurrent ? ' aria-current="page"' : '';
|
||||
return ` <li><a href="/skills/${s.id}"${attr}>/${escapeHtml(s.id)}</a></li>`;
|
||||
})
|
||||
.join('\n')}
|
||||
</ul>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
if (tutorials && tutorials.length > 0) {
|
||||
html += `
|
||||
<div class="skills-sidebar-group" data-category="tutorials">
|
||||
<p class="skills-sidebar-group-title">Tutorials</p>
|
||||
<ul class="skills-sidebar-list">
|
||||
${tutorials
|
||||
.map((t) => {
|
||||
const isCurrent = current?.kind === 'tutorial' && current.slug === t.slug;
|
||||
const attr = isCurrent ? ' aria-current="page"' : '';
|
||||
return ` <li><a href="/tutorials/${t.slug}"${attr}>${escapeHtml(t.title)}</a></li>`;
|
||||
})
|
||||
.join('\n')}
|
||||
</ul>
|
||||
@@ -292,32 +314,6 @@ function renderRuleCard(rule) {
|
||||
</article>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the tutorials sidebar: list of tutorial pages in order, with
|
||||
* the current tutorial marked via aria-current.
|
||||
*/
|
||||
function renderTutorialsSidebar(tutorials, currentSlug = null) {
|
||||
const entries = tutorials
|
||||
.map((t) => {
|
||||
const current = t.slug === currentSlug ? ' aria-current="page"' : '';
|
||||
return ` <li><a href="/tutorials/${t.slug}"${current}>${escapeHtml(t.title)}</a></li>`;
|
||||
})
|
||||
.join('\n');
|
||||
|
||||
return `
|
||||
<aside class="skills-sidebar tutorials-sidebar" aria-label="Tutorials">
|
||||
<div class="skills-sidebar-inner">
|
||||
<p class="skills-sidebar-label">Tutorials</p>
|
||||
<div class="skills-sidebar-group">
|
||||
<p class="skills-sidebar-group-title">Walk-throughs</p>
|
||||
<ul class="skills-sidebar-list">
|
||||
${entries}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</aside>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the /tutorials index main content.
|
||||
*/
|
||||
@@ -431,16 +427,16 @@ export async function generateSubPages(rootDir) {
|
||||
|
||||
const generated = [];
|
||||
|
||||
// Skills index: docs-browser layout with sticky sidebar.
|
||||
// Skills index: docs-browser layout with unified sidebar.
|
||||
{
|
||||
const sidebar = renderSkillsSidebar(data.skillsByCategory, null);
|
||||
const sidebar = renderDocsSidebar(data.skillsByCategory, data.tutorials, null);
|
||||
const main = renderSkillsOverviewMain(data.skillsByCategory);
|
||||
const html = renderPage({
|
||||
title: 'Skills | Impeccable',
|
||||
description:
|
||||
'21 commands that teach your AI harness how to design. Browse by category: create, evaluate, refine, simplify, harden, system.',
|
||||
bodyHtml: wrapInDocsLayout(sidebar, main),
|
||||
activeNav: 'skills',
|
||||
activeNav: 'docs',
|
||||
canonicalPath: '/skills',
|
||||
bodyClass: 'sub-page skills-layout-page',
|
||||
});
|
||||
@@ -451,7 +447,7 @@ export async function generateSubPages(rootDir) {
|
||||
|
||||
// Skills detail pages: same docs-browser shell as the overview.
|
||||
for (const skill of data.skills) {
|
||||
const sidebar = renderSkillsSidebar(data.skillsByCategory, skill.id);
|
||||
const sidebar = renderDocsSidebar(data.skillsByCategory, data.tutorials, { kind: 'skill', id: skill.id });
|
||||
const main = renderSkillDetail(skill, data.knownSkillIds);
|
||||
const title = `/${skill.id} | Impeccable`;
|
||||
const description = skill.editorial?.frontmatter?.tagline || skill.description;
|
||||
@@ -459,7 +455,7 @@ export async function generateSubPages(rootDir) {
|
||||
title,
|
||||
description,
|
||||
bodyHtml: wrapInDocsLayout(sidebar, main),
|
||||
activeNav: 'skills',
|
||||
activeNav: 'docs',
|
||||
canonicalPath: `/skills/${skill.id}`,
|
||||
bodyClass: 'sub-page skills-layout-page',
|
||||
});
|
||||
@@ -486,15 +482,15 @@ export async function generateSubPages(rootDir) {
|
||||
generated.push(out);
|
||||
}
|
||||
|
||||
// Tutorials index.
|
||||
// Tutorials index (under the unified Docs umbrella).
|
||||
if (data.tutorials.length > 0) {
|
||||
const sidebar = renderTutorialsSidebar(data.tutorials, null);
|
||||
const sidebar = renderDocsSidebar(data.skillsByCategory, data.tutorials, null);
|
||||
const main = renderTutorialsIndexMain(data.tutorials);
|
||||
const html = renderPage({
|
||||
title: 'Tutorials | Impeccable',
|
||||
description: `${data.tutorials.length} short, opinionated walk-throughs of the highest-leverage Impeccable workflows.`,
|
||||
bodyHtml: wrapInDocsLayout(sidebar, main),
|
||||
activeNav: 'tutorials',
|
||||
activeNav: 'docs',
|
||||
canonicalPath: '/tutorials',
|
||||
bodyClass: 'sub-page skills-layout-page tutorials-page',
|
||||
});
|
||||
@@ -505,13 +501,13 @@ export async function generateSubPages(rootDir) {
|
||||
|
||||
// Tutorial detail pages.
|
||||
for (const tutorial of data.tutorials) {
|
||||
const sidebar = renderTutorialsSidebar(data.tutorials, tutorial.slug);
|
||||
const sidebar = renderDocsSidebar(data.skillsByCategory, data.tutorials, { kind: 'tutorial', slug: tutorial.slug });
|
||||
const main = renderTutorialDetail(tutorial, data.knownSkillIds);
|
||||
const html = renderPage({
|
||||
title: `${tutorial.title} | Tutorials | Impeccable`,
|
||||
description: tutorial.description || tutorial.tagline || '',
|
||||
bodyHtml: wrapInDocsLayout(sidebar, main),
|
||||
activeNav: 'tutorials',
|
||||
activeNav: 'docs',
|
||||
canonicalPath: `/tutorials/${tutorial.slug}`,
|
||||
bodyClass: 'sub-page skills-layout-page tutorials-page',
|
||||
});
|
||||
|
||||
@@ -56,9 +56,12 @@ export function createRenderer({ knownSkillIds = new Set(), currentSkillId = nul
|
||||
};
|
||||
|
||||
// Fenced code blocks — minimal glass-terminal styling, no syntax highlighter in v1.
|
||||
// Wrapped in a container with a copy button; click handling lives in the
|
||||
// page-level inline script added by render-page.js.
|
||||
renderer.code = ({ text, lang }) => {
|
||||
const langClass = lang ? ` code-block--${escapeAttr(lang)}` : '';
|
||||
return `<pre class="code-block${langClass}"><code>${escapeHtml(text)}</code></pre>\n`;
|
||||
const copyValue = escapeAttr(text);
|
||||
return `<div class="code-block-wrap"><pre class="code-block${langClass}"><code>${escapeHtml(text)}</code></pre><button class="code-block-copy" type="button" data-copy="${copyValue}" aria-label="Copy to clipboard"></button></div>\n`;
|
||||
};
|
||||
|
||||
return renderer;
|
||||
|
||||
@@ -102,6 +102,19 @@ export function renderPage({
|
||||
<main id="main">
|
||||
${bodyHtml}
|
||||
</main>
|
||||
<script>
|
||||
// Copy buttons on rendered code blocks
|
||||
document.addEventListener('click', (e) => {
|
||||
const btn = e.target.closest('[data-copy]');
|
||||
if (!btn) return;
|
||||
const text = btn.getAttribute('data-copy');
|
||||
if (!text) return;
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
btn.classList.add('is-copied');
|
||||
setTimeout(() => btn.classList.remove('is-copied'), 1500);
|
||||
}).catch(() => {});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
Reference in New Issue
Block a user