Commit graph

6 commits

Author SHA1 Message Date
220f8e0290 style(roadmap): align first milestone with content column left edge
The first route item now starts where the dispatch banner's left edge
sits (the page's content-max column), instead of 60px from the
viewport edge. Looks intentional now — the route and the dispatch
banner share a vertical anchor.

- computeRouteLayout now accepts optional paddingLeft / paddingRight
  that override the symmetric paddingX. Existing call sites and
  tests are unchanged.
- RoadmapRoute SSR + client recompute set paddingLeft = max(60,
  (vw - 1152) / 2), so on viewports ≤ 1152px nothing moves (degrades
  gracefully) and on wider screens the start migrates inward.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 17:06:30 +02:00
c0592f7ca5 fix(route): cards grow toward centreline — fixes top/bottom clipping at the source
The actual cause of the persistent top/bottom card clipping wasn't the
track height or the padding — it's that the CSS spec forces
overflow-y: visible to compute as auto whenever overflow-x is auto.
Browsers clip the scroll container on both axes regardless of how we
declare overflow-y. Every previous fix was band-aiding the same
underlying problem.

Geometric fix: flip cardSide so cards hang toward the centreline
instead of away from it.

  - i=0 (dot on centreline)         → card below (default, no clip risk)
  - i=1 (dot above-centre, odd)     → card below (grows toward midY)
  - i=2 (dot below-centre, even >0) → card above (grows toward midY)
  - …alternating thereafter

Cards now always grow into the track, never out of it. Both axes are
naturally bounded by the track's height. Hover-expanded cards stay
inside the scroll container's clip box, so the browser-forced clipping
has nothing to remove.

Tests updated to expect the new pattern. The 7-item case carries an
extra spot-check that every card's side is opposite to its dot's
offset from the centreline — i.e. the geometric invariant the fix
relies on.

Visual rhythm: cards still alternate above/below as the path swings
up and down; the wave reads the same. What changes is which milestones
have cards above vs below — and only at the visual top of the page
where it improves things by stopping the clipping.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 14:43:41 +02:00
b4df8e10f1 fix(route): span ~80% of viewport + scroll padding back to 60 + track 420
Three coupled fixes to the route's geometry:

- computeRouteLayout's width calculation flipped to Math.max(viewport
  * 0.80, itemCount * minSpacing + padding * 2). On a wide screen with
  few items the 80% target wins and the path stretches across the
  page; once item count makes the data-driven width exceed 80% (the
  carousel case), the data-driven value wins and the track extends
  past the viewport unchanged.
- .rr-scroll horizontal padding 140 → 60 each side. The previous 140
  was overcompensating; with the new 80% target the milestones already
  sit inside their own breathing room. 60 is card-half + 30px buffer,
  enough for a 220px card centred under a dot 60px from the edge.
  scroll-padding kept in sync at 60 for snap-stop landings.
- trackHeight default 580 → 420; midY 290 → 210. The 580 was bandaging
  the vertical-clipping issue — that fix lands in the next commit. With
  the clip properly addressed, 420 fits the path's amplitude 120 swing
  cleanly with no wasted vertical space.

Tests rewritten to match the new width semantics: 1 item @ 1000 →
920px (0.8 * 1000 + 120); 3 items @ 1400 → 1240px; 20 items @ 800 →
6200px (data-driven wins). midY assertion 290 → 210.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 14:32:29 +02:00
ac52e97c28 feat(route): drop progress dots, move legend below, vary path amplitude
Three coupled changes that all serve the same goal — less furniture
above the route, more honest information below it.

Progress dots gone. At 5 pills × ~400px per pill the strip was too
coarse to feel meaningful; the arrows + edge fades already
communicate scroll position. .rr-progress markup, the script logic
that updated the .active class, and the .rr-progress / .rr-progress-dot
styles are all deleted.

Legend moves from beside 'The route' in the section header to below
the track, centred. Reading order is now title → walk the path → key,
which is the order it makes sense in. The header collapses to just
the title on the left and the two arrow buttons on the right.

Path amplitude is no longer constant. computeRouteLayout multiplies
the base amplitude (120) by a per-item factor that ramps 0.78 (first
off-axis item) → 1.18 (last item), so closer-in items swing tighter
and further-out items swing wider. The visual effect is subtle but
the path now feels hand-planned instead of strictly sinusoidal.

Test updated to verify the multiplier — |itemY[2] - midY| now exceeds
|itemY[1] - midY| in the 3-item case.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 11:58:59 +02:00
fde07b1f11 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>
2026-05-12 11:56:01 +02:00
66b460c35f feat(lib): roadmap-layout — coordinate generation for the route component
Pure math, no DOM. computeRouteLayout(opts) takes itemCount + viewport
width and returns trackWidth, pathD, itemX, itemY, cardSide, midY:

- itemX evenly distributes items between padding and viewport-padding,
  expanding the canvas beyond the viewport when itemCount * minSpacingX
  exceeds the available width. Single-item case centres the dot.
- itemY puts the first item on the centreline; subsequent items
  alternate +amplitude / -amplitude so the path snakes gently up and
  down. The route reads as a river rather than a saw-tooth because the
  cubic-bezier control points use the segment midpoint x — that holds
  the tangent flat at each milestone.
- cardSide alternates 'below' / 'above' starting from 'below' on item 0.
  Cards hang from their dot via a thin vertical connector in the
  consuming component.

Also adds travelledStopFor(statuses) — the stop position on the path
stroke gradient where 'travelled' fades into 'ahead'. Clamps to 0.98
even when every item is shipping so the fade is always visible.

9 unit tests cover itemCount 1/2/3/7/20 plus the travelledStop edge
cases (no shipping → 0; all shipping → ≤ 0.98; mixed → exact midpoint).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 11:40:26 +02:00