feat(route): single right-edge advance arrow — forward-only affordance
Replaces the prev/next button pair removed last commit with a single
viewport-anchored circular control.
- 48px diameter, 1px terracotta border, terracotta chevron on cream.
- Hover / focus-visible fills to terracotta with cream chevron and a
1.06× scale-up.
- Anchored absolute inside .rr-wrap (already position: relative):
right: 32px / top: 50% / translateY(-50%).
- Toggles .rr-at-end (opacity 0.25, pointer-events: none) when the
scroll container reaches its right edge.
- First-load hint: .rr-hint class added 100ms after mount fires a
rr-advance-pulse keyframe three times (iteration-count: 3) — soft
8px shadow ring in 15% terracotta pulses out and back. Animation
stops naturally; no JS cleanup needed.
No left arrow on purpose — the path reads past → future, and the
user's instinct at any milestone is 'what's next?' The right arrow
earns its keep by hinting the existence of more track beyond the
visible window. A symmetric left arrow would just be noise.
Click handler today: scrollBy({behavior: 'smooth'}) by 60% of viewport
width. Step 4 replaces this with a custom-animated glide and adds the
drag + wheel scroll modalities.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
22a55aa073
commit
acbb722a0a
1 changed files with 72 additions and 2 deletions
|
|
@ -107,6 +107,21 @@ const initialShippingX = lastShippingIndex >= 0 ? layout.itemX[lastShippingIndex
|
|||
|
||||
<div class="rr-fade-left" id="rr-fade-l" aria-hidden="true"></div>
|
||||
<div class="rr-fade-right" id="rr-fade-r" aria-hidden="true"></div>
|
||||
|
||||
<!-- Single forward-only advance affordance anchored to the right
|
||||
viewport edge. There's no left arrow on purpose — the path
|
||||
reads left-to-right and the user's instinct after looking at
|
||||
a milestone is 'what's next?', not 'what came before?'. -->
|
||||
<button
|
||||
type="button"
|
||||
class="rr-advance"
|
||||
id="rr-advance"
|
||||
aria-label="Further along the route"
|
||||
>
|
||||
<svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true">
|
||||
<path d="M9 6l6 6-6 6" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Legend lives in /roadmap.astro now so it returns to centred
|
||||
|
|
@ -153,6 +168,7 @@ const initialShippingX = lastShippingIndex >= 0 ? layout.itemX[lastShippingIndex
|
|||
const milestones = Array.from(section.querySelectorAll<HTMLElement>('.rr-milestone'));
|
||||
const fadeL = section.querySelector<HTMLElement>('#rr-fade-l');
|
||||
const fadeR = section.querySelector<HTMLElement>('#rr-fade-r');
|
||||
const advance = section.querySelector<HTMLButtonElement>('#rr-advance');
|
||||
if (!scroll || !track || !svg) return;
|
||||
|
||||
const itemCount = milestones.length;
|
||||
|
|
@ -194,16 +210,22 @@ const initialShippingX = lastShippingIndex >= 0 ? layout.itemX[lastShippingIndex
|
|||
milestones.forEach((m, i) => { m.style.left = `${itemX[i]}px`; });
|
||||
}
|
||||
|
||||
/* Arrow handler + edge-state updates are rewritten in the next commit
|
||||
(drag + wheel + glide). For now only the fades update on scroll. */
|
||||
/* Edge state — fades + advance disable. The smooth glide animation
|
||||
on advance click lands in the next commit; for now the click
|
||||
uses native smooth scrollBy. */
|
||||
function updateNav() {
|
||||
const max = scroll!.scrollWidth - scroll!.clientWidth;
|
||||
const atStart = scroll!.scrollLeft <= 2;
|
||||
const atEnd = scroll!.scrollLeft >= max - 2;
|
||||
if (fadeL) fadeL.style.opacity = atStart ? '0' : '1';
|
||||
if (fadeR) fadeR.style.opacity = atEnd ? '0' : '1';
|
||||
if (advance) advance.classList.toggle('rr-at-end', atEnd);
|
||||
}
|
||||
|
||||
advance?.addEventListener('click', () => {
|
||||
scroll!.scrollBy({ left: scroll!.clientWidth * 0.6, behavior: 'smooth' });
|
||||
});
|
||||
|
||||
scroll.addEventListener('scroll', updateNav, { passive: true });
|
||||
|
||||
// Debounced resize → recompute layout + refresh nav state. 120ms is
|
||||
|
|
@ -226,6 +248,10 @@ const initialShippingX = lastShippingIndex >= 0 ? layout.itemX[lastShippingIndex
|
|||
}
|
||||
setTimeout(updateNav, 50);
|
||||
updateNav();
|
||||
|
||||
// Three-pulse hint on the advance arrow ~100ms after layout settles
|
||||
// so the user notices the affordance once and then it sits quietly.
|
||||
setTimeout(() => advance?.classList.add('rr-hint'), 100);
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
@ -387,6 +413,50 @@ const initialShippingX = lastShippingIndex >= 0 ? layout.itemX[lastShippingIndex
|
|||
margin: 0;
|
||||
}
|
||||
|
||||
/* ── Advance arrow ─────────────────────────────────────────────── */
|
||||
.rr-advance {
|
||||
position: absolute;
|
||||
right: 32px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid var(--pigment-terracotta);
|
||||
background: var(--background);
|
||||
color: var(--pigment-terracotta);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
z-index: 5;
|
||||
transition: background .2s ease,
|
||||
color .2s ease,
|
||||
opacity .25s ease,
|
||||
transform .25s ease;
|
||||
}
|
||||
.rr-advance:hover,
|
||||
.rr-advance:focus-visible {
|
||||
background: var(--pigment-terracotta);
|
||||
color: var(--background);
|
||||
outline: none;
|
||||
transform: translateY(-50%) scale(1.06);
|
||||
}
|
||||
.rr-advance[disabled],
|
||||
.rr-advance.rr-at-end {
|
||||
opacity: 0.25;
|
||||
pointer-events: none;
|
||||
}
|
||||
/* Three-pulse hint on first load — fires once, then stops. */
|
||||
@keyframes rr-advance-pulse {
|
||||
0%, 100% { box-shadow: 0 0 0 0 rgba(185, 107, 88, 0); }
|
||||
50% { box-shadow: 0 0 0 8px rgba(185, 107, 88, 0.15); }
|
||||
}
|
||||
.rr-advance.rr-hint {
|
||||
animation: rr-advance-pulse 1.4s ease-in-out 3;
|
||||
}
|
||||
|
||||
/* Edge fades cover only the track itself — the top/bottom padding
|
||||
zones (60/80) on .rr-scroll exist so hover cards can overflow there
|
||||
without clipping, so the fades shouldn't paint over them. */
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue