mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-12 06:06:37 +03:00
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:
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user