fix(route): track 580 + 140px scroll-container padding — no more clipping

The route was clipping at three places: top and bottom of hovered
cards (the track was only 460 tall) and at the left/right viewport
edges (first card half-off-screen at scrollLeft 0, last card off the
right at scrollEnd).

Track height: default trackHeight in roadmap-layout 460 → 580; .rr-track
inline-style and the SVG height matched. midY now 290. Path centreline
stays in the visual centre and gains 60px breathing room above + 60px
below — which is exactly the room a hovered card needs to expand into.

Scroll-container padding: .rr-scroll gains 140px of horizontal padding
plus matching scroll-padding-left/right so snap-stops land cleanly.
The 140 figure is 220px card-width / 2 + 30px buffer, so the first and
last cards have a full card-width of clear space inside the viewport
at the scroll extremes.

Layout helper test verifies midY === 290.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jonathan Hvid 2026-05-12 11:56:01 +02:00
parent 33a21735e6
commit fde07b1f11
3 changed files with 10 additions and 4 deletions

View file

@ -83,8 +83,8 @@ const initialShippingX = lastShippingIndex >= 0 ? itemXByIndex[lastShippingIndex
<!-- The route — desktop horizontal --> <!-- The route — desktop horizontal -->
<div class="rr-wrap rr-desktop"> <div class="rr-wrap rr-desktop">
<div class="rr-scroll" id="rr-scroll"> <div class="rr-scroll" id="rr-scroll">
<div class="rr-track" style={`width: ${layout.trackWidth}px; height: 460px;`}> <div class="rr-track" style={`width: ${layout.trackWidth}px; height: 580px;`}>
<svg class="rr-path" width={layout.trackWidth} height="460" aria-hidden="true"> <svg class="rr-path" width={layout.trackWidth} height="580" aria-hidden="true">
<defs> <defs>
<linearGradient id="rr-path-gradient" x1="0" y1="0" x2="1" y2="0"> <linearGradient id="rr-path-gradient" x1="0" y1="0" x2="1" y2="0">
<stop offset="0" stop-color="#2a2520" stop-opacity="0.55"/> <stop offset="0" stop-color="#2a2520" stop-opacity="0.55"/>
@ -289,7 +289,12 @@ const initialShippingX = lastShippingIndex >= 0 ? itemXByIndex[lastShippingIndex
scroll-snap-type: x mandatory; scroll-snap-type: x mandatory;
scroll-behavior: smooth; scroll-behavior: smooth;
scrollbar-width: none; scrollbar-width: none;
padding-bottom: 8px; /* Card-half (110px) + breathing buffer (30px) so first/last dots
have a card-width of clear space inside the viewport when scrolled
to the extremes. scroll-padding makes snap-stops land cleanly. */
padding: 0 140px 8px;
scroll-padding-left: 140px;
scroll-padding-right: 140px;
} }
.rr-scroll::-webkit-scrollbar { display: none; } .rr-scroll::-webkit-scrollbar { display: none; }
.rr-track { position: relative; } .rr-track { position: relative; }

View file

@ -33,7 +33,7 @@ export interface LayoutResult {
export function computeRouteLayout(opts: LayoutOpts): LayoutResult { export function computeRouteLayout(opts: LayoutOpts): LayoutResult {
const minSpacing = opts.minSpacingX ?? 320; const minSpacing = opts.minSpacingX ?? 320;
const trackHeight = opts.trackHeight ?? 460; const trackHeight = opts.trackHeight ?? 580;
const amplitude = opts.amplitude ?? 120; const amplitude = opts.amplitude ?? 120;
const padding = opts.paddingX ?? 60; const padding = opts.paddingX ?? 60;
const midY = trackHeight / 2; const midY = trackHeight / 2;

View file

@ -10,6 +10,7 @@ describe('computeRouteLayout', () => {
it('1 item — produces a valid single-point M path on the centreline', () => { it('1 item — produces a valid single-point M path on the centreline', () => {
const out = computeRouteLayout({ itemCount: 1, viewportWidth: 1000 }); const out = computeRouteLayout({ itemCount: 1, viewportWidth: 1000 });
expect(out.itemX).toHaveLength(1); expect(out.itemX).toHaveLength(1);
expect(out.midY).toBe(290); // trackHeight 580 / 2
expect(out.itemY).toEqual([out.midY]); expect(out.itemY).toEqual([out.midY]);
expect(out.cardSide).toEqual(['below']); expect(out.cardSide).toEqual(['below']);
expect(out.pathD.startsWith('M ')).toBe(true); expect(out.pathD.startsWith('M ')).toBe(true);