From 220f8e0290aad48015c32832715ec0e99ca7a1ce Mon Sep 17 00:00:00 2001 From: Jonathan Hvid Date: Tue, 12 May 2026 17:06:30 +0200 Subject: [PATCH] style(roadmap): align first milestone with content column left edge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- src/components/RoadmapRoute.astro | 22 ++++++++++++++++++---- src/lib/roadmap-layout.ts | 14 +++++++++----- 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/src/components/RoadmapRoute.astro b/src/components/RoadmapRoute.astro index d798c91..e2507ef 100644 --- a/src/components/RoadmapRoute.astro +++ b/src/components/RoadmapRoute.astro @@ -9,7 +9,17 @@ interface Props { const { items, viewportWidth = 1100 } = Astro.props; -const layout = computeRouteLayout({ itemCount: items.length, viewportWidth }); +// Align the first milestone with the left edge of the page's content column +// (matches the LatestDispatchBanner below). --content-max is 72rem = 1152px. +const CONTENT_MAX = 1152; +const DEFAULT_PADDING = 60; +const paddingLeft = Math.max(DEFAULT_PADDING, (viewportWidth - CONTENT_MAX) / 2); + +const layout = computeRouteLayout({ + itemCount: items.length, + viewportWidth, + paddingLeft, +}); const travelledStop = travelledStopFor(items.map(i => i.status)); const STATUS_LABEL: Record = { @@ -158,6 +168,7 @@ const initialShippingX = lastShippingIndex >= 0 ? layout.itemX[lastShippingIndex // amplitude doesn't change with viewport, only the horizontal spread). const MIN_SPACING = 320; const PADDING_X = 60; + const CONTENT_MAX = 1152; // matches --content-max (72rem) document.querySelectorAll('.route').forEach((section) => { const scroll = section.querySelector('#rr-scroll'); @@ -180,14 +191,17 @@ const initialShippingX = lastShippingIndex >= 0 ? layout.itemX[lastShippingIndex const targetUsableWidth = vw * 0.80; const dataDrivenWidth = (itemCount - 1) * MIN_SPACING; const usableWidth = Math.max(targetUsableWidth, dataDrivenWidth); - const trackWidth = usableWidth + PADDING_X * 2; + // Match the SSR offset — first item aligns with the content-column + // left edge so the route lines up with the dispatch banner below. + const paddingLeft = Math.max(PADDING_X, (vw - CONTENT_MAX) / 2); + const trackWidth = paddingLeft + usableWidth + PADDING_X; const itemX: number[] = []; for (let i = 0; i < itemCount; i += 1) { itemX.push( itemCount === 1 - ? PADDING_X + usableWidth / 2 - : PADDING_X + (i / (itemCount - 1)) * usableWidth, + ? paddingLeft + usableWidth / 2 + : paddingLeft + (i / (itemCount - 1)) * usableWidth, ); } diff --git a/src/lib/roadmap-layout.ts b/src/lib/roadmap-layout.ts index 3d98469..da82424 100644 --- a/src/lib/roadmap-layout.ts +++ b/src/lib/roadmap-layout.ts @@ -19,7 +19,9 @@ export interface LayoutOpts { minSpacingX?: number; // default 320 trackHeight?: number; // default 460 amplitude?: number; // default 120 - paddingX?: number; // default 60 + paddingX?: number; // default 60 — symmetric leading + trailing padding + paddingLeft?: number; // overrides paddingX on the leading edge only + paddingRight?: number; // overrides paddingX on the trailing edge only } export interface LayoutResult { @@ -35,7 +37,9 @@ export function computeRouteLayout(opts: LayoutOpts): LayoutResult { const minSpacing = opts.minSpacingX ?? 320; const trackHeight = opts.trackHeight ?? 420; const amplitude = opts.amplitude ?? 120; - const padding = opts.paddingX ?? 60; + const paddingDef = opts.paddingX ?? 60; + const paddingL = opts.paddingLeft ?? paddingDef; + const paddingR = opts.paddingRight ?? paddingDef; const midY = trackHeight / 2; const itemCount = Math.max(0, opts.itemCount); @@ -57,12 +61,12 @@ export function computeRouteLayout(opts: LayoutOpts): LayoutResult { const targetUsableWidth = opts.viewportWidth * 0.80; const dataDrivenWidth = (itemCount - 1) * minSpacing; const usableWidth = Math.max(targetUsableWidth, dataDrivenWidth); - const trackWidth = usableWidth + padding * 2; + const trackWidth = paddingL + usableWidth + paddingR; const itemX: number[] = Array.from({ length: itemCount }, (_, i) => itemCount === 1 - ? padding + usableWidth / 2 - : padding + (i / (itemCount - 1)) * usableWidth, + ? paddingL + usableWidth / 2 + : paddingL + (i / (itemCount - 1)) * usableWidth, ); // First item on the centreline; subsequent items alternate up/down with