Before/after split demos on skill pages + sidebar reorder

Two changes bundled:

1. Before/after split demos on every skill detail page.
   - loadCommandDemos() in sub-pages-data.js: dynamically imports each
     module in public/js/demos/commands (the same files the homepage
     uses), returning a { skillId: { id, caption, before, after } } map.
     Falls back to a warn-and-continue if a demo file can't be loaded
     so one bad demo doesn't break the whole generator.
   - buildSubPageData becomes async; caller in build-sub-pages.js
     awaits it.
   - Each skill object gets a .demo field (may be null for /shape).
   - renderSkillDemo() produces the .split-comparison markup matching
     the homepage: .split-container with .split-before + .split-after
     + .split-divider, plus Before/After labels and the caption. The
     block sits between the detail header and the editorial wrapper
     so readers see the visual before reading any prose.
   - sub-pages.css ports the core .split-* layout from main.css (the
     .slop-* and .impeccable-* helpers are homepage-specific and not
     copied). Height is 360px to match the docs column.
   - render-page.js grows a lightweight inline split-compare init
     script (60 lines of vanilla JS) that handles drag and the skewed
     clip-path without depending on the homepage's full lerp/ResizeObserver
     module. Runs only on pages that actually have .split-container.

2. Sidebar reorder: Tutorials first, then skills.
   Walk-throughs are the on-ramp; they belong at the top of the sidebar
   where a new visitor will find them. Add <hr class="skills-sidebar-divider">
   between the Tutorials group and the first skill category so the two
   sections read as distinct.

Verified: /skills/polish, /skills/bolder, /skills/critique all render
the demo block. /skills/shape correctly has none. Sidebar on any /skills
or /tutorials page shows Tutorials first, then a thin mist divider,
then Create / Evaluate / Refine / Simplify / Harden / System skill
categories.
This commit is contained in:
Paul Bakaus
2026-04-08 10:54:18 -07:00
parent eb130c4af9
commit 7d7f77d2ba
4 changed files with 265 additions and 19 deletions
+56
View File
@@ -114,6 +114,62 @@ ${bodyHtml}
setTimeout(() => btn.classList.remove('is-copied'), 1500);
}).catch(() => {});
});
// Lightweight split-compare interaction for before/after demos.
// Drag or hover horizontally over .split-container to sweep the
// divider. No lerp, no ResizeObserver — good enough for docs.
(function initSplitCompare() {
const containers = document.querySelectorAll('.split-container');
if (containers.length === 0) return;
for (const container of containers) {
const splitAfter = container.querySelector('.split-after');
const splitDivider = container.querySelector('.split-divider');
if (!splitAfter || !splitDivider) continue;
const skewAngle = 10 * Math.PI / 180;
const tanAngle = Math.tan(skewAngle);
let skewOffset = 8;
const recalcSkew = () => {
const r = container.getBoundingClientRect();
if (r.width > 0 && r.height > 0) {
skewOffset = 50 * r.height * tanAngle / r.width;
}
};
recalcSkew();
window.addEventListener('resize', recalcSkew, { passive: true });
const update = (pct) => {
const x = Math.max(-skewOffset, Math.min(100 + skewOffset, pct));
splitAfter.style.clipPath = \`polygon(\${x + skewOffset}% 0%, 100% 0%, 100% 100%, \${x - skewOffset}% 100%)\`;
splitDivider.style.left = \`\${x}%\`;
};
update(50);
let tracking = false;
const onMove = (clientX) => {
const rect = container.getBoundingClientRect();
const pct = ((clientX - rect.left) / rect.width) * 100;
update(pct);
};
container.addEventListener('pointerdown', (e) => {
tracking = true;
container.setPointerCapture(e.pointerId);
onMove(e.clientX);
});
container.addEventListener('pointermove', (e) => {
if (!tracking) return;
onMove(e.clientX);
});
const stop = (e) => {
if (!tracking) return;
tracking = false;
try { container.releasePointerCapture(e.pointerId); } catch {}
};
container.addEventListener('pointerup', stop);
container.addEventListener('pointercancel', stop);
container.addEventListener('pointerleave', stop);
}
})();
</script>
</body>
</html>