Fix before/after demo layout, labels, and interaction

Four issues reported on /skills/overdrive (and every other skill demo):

1. The demo block was centered inside the content column, looking odd
   against the otherwise left-aligned page. Drop 'margin: 0 auto' from
   both .split-comparison and .split-container, and remove the nested
   max-width so the whole demo left-aligns at 500px max-width with no
   centering.

2. The BEFORE and AFTER labels were stretching beyond the demo box
   because .split-comparison (560px) was wider than .split-container
   (500px) and .split-labels was using justify-content: space-between
   across the wider parent. Collapse the two max-widths to a single
   500px cap so the labels now sit flush with the container edges.

3. The label row was sitting way below the demo (16px margin-top plus
   the height-stretched container). Tighten margin-top to 10px.

4. The inline split-compare handler only supported click-and-drag. The
   homepage effect also tracks hover on devices with hover:hover, so
   the mouse sweeps the divider and leaving the box eases it back to
   the default. Port that behavior: matchMedia('(hover: hover)') to
   detect, pointerenter/leave to toggle a hovering flag, pointerdown/up
   for drag, and a tiny lerp on requestAnimationFrame so the return to
   center feels smooth. Eyebrow text now reads 'Drag or hover to
   compare' to signal both modes.

Also drop text-align: center on .skill-demo-eyebrow and .skill-demo-caption
for the same left-align consistency.
This commit is contained in:
Paul Bakaus
2026-04-08 11:04:35 -07:00
parent feea28058f
commit ac710d5ec1
3 changed files with 71 additions and 31 deletions
+4 -7
View File
@@ -997,16 +997,14 @@ main#main {
.split-comparison {
position: relative;
width: 100%;
max-width: 560px;
margin: 0 auto clamp(2rem, 4vw, 3rem);
max-width: 500px;
margin: 0 0 clamp(2rem, 4vw, 3rem);
}
.split-container {
position: relative;
width: 100%;
max-width: 500px;
height: 360px;
margin: 0 auto;
border-radius: 12px;
overflow: hidden;
background: var(--color-cream);
@@ -1060,7 +1058,7 @@ main#main {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: var(--spacing-sm);
margin-top: 10px;
font-family: var(--font-mono);
font-size: 0.6875rem;
font-weight: 600;
@@ -1078,11 +1076,11 @@ main#main {
}
.skill-demo-caption {
text-align: center;
font-size: 0.875rem;
color: var(--color-charcoal);
margin-top: var(--spacing-md);
font-style: italic;
max-width: 500px;
}
.skill-demo-eyebrow {
@@ -1092,7 +1090,6 @@ main#main {
text-transform: uppercase;
letter-spacing: 0.14em;
color: var(--color-ash);
text-align: center;
margin-bottom: var(--spacing-sm);
}
+1 -1
View File
@@ -39,7 +39,7 @@ function renderSkillDemo(skill) {
const { before, after, caption } = skill.demo;
return `
<section class="skill-demo" aria-label="Before and after demo">
<p class="skill-demo-eyebrow">Drag to compare</p>
<p class="skill-demo-eyebrow">Drag or hover to compare</p>
<div class="split-comparison" data-demo="skill-${skill.id}">
<div class="split-container">
<div class="split-before">
+66 -23
View File
@@ -115,19 +115,20 @@ ${bodyHtml}
}).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.
// Before/after split-compare: drag on touch, hover OR drag on mouse.
// On mouse-leave the divider eases back to the default position.
(function initSplitCompare() {
const containers = document.querySelectorAll('.split-container');
if (containers.length === 0) return;
const hasHover = matchMedia('(hover: hover)').matches;
const DEFAULT_POSITION = 50;
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);
const tanAngle = Math.tan(10 * Math.PI / 180);
let skewOffset = 8;
const recalcSkew = () => {
const r = container.getBoundingClientRect();
@@ -138,36 +139,78 @@ ${bodyHtml}
recalcSkew();
window.addEventListener('resize', recalcSkew, { passive: true });
const update = (pct) => {
let targetX = DEFAULT_POSITION;
let currentX = DEFAULT_POSITION;
let rafId = null;
const paint = (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%)\`;
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 step = () => {
currentX += (targetX - currentX) * 0.2;
if (Math.abs(targetX - currentX) < 0.1) {
currentX = targetX;
rafId = null;
} else {
rafId = requestAnimationFrame(step);
}
paint(currentX);
};
const setTarget = (pct) => {
targetX = pct;
if (rafId === null) rafId = requestAnimationFrame(step);
};
paint(DEFAULT_POSITION);
const pctFromClientX = (clientX) => {
const rect = container.getBoundingClientRect();
const pct = ((clientX - rect.left) / rect.width) * 100;
update(pct);
return ((clientX - rect.left) / rect.width) * 100;
};
let hovering = false;
let dragging = false;
container.addEventListener('pointerenter', (e) => {
if (hasHover && e.pointerType === 'mouse') {
hovering = true;
}
});
container.addEventListener('pointerdown', (e) => {
tracking = true;
dragging = true;
container.setPointerCapture(e.pointerId);
onMove(e.clientX);
setTarget(pctFromClientX(e.clientX));
});
container.addEventListener('pointermove', (e) => {
if (!tracking) return;
onMove(e.clientX);
if (dragging || hovering) {
setTarget(pctFromClientX(e.clientX));
}
});
const stop = (e) => {
if (!tracking) return;
tracking = false;
try { container.releasePointerCapture(e.pointerId); } catch {}
const endDrag = (e) => {
if (dragging) {
dragging = false;
try { container.releasePointerCapture(e.pointerId); } catch {}
}
};
container.addEventListener('pointerup', stop);
container.addEventListener('pointercancel', stop);
container.addEventListener('pointerleave', stop);
container.addEventListener('pointerup', endDrag);
container.addEventListener('pointercancel', endDrag);
container.addEventListener('pointerleave', (e) => {
endDrag(e);
if (hovering) {
hovering = false;
setTarget(DEFAULT_POSITION);
}
});
}
})();
</script>