From 7bd4902b9dd51c10fa699d349b3fe92523e6e974 Mon Sep 17 00:00:00 2001 From: Jonathan Hvid Date: Tue, 12 May 2026 11:42:32 +0200 Subject: [PATCH] =?UTF-8?q?feat(component):=20RoadmapRoute=20=E2=80=94=20S?= =?UTF-8?q?VG=20path=20+=20milestones=20+=20hover-reveal=20cards=20(static?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The treasure-map. Static render only; nav script lands in the next commit. Section header: serif 'The route' + tracked-uppercase legend (Shipping / In beta / Exploring / Considering) on the left; two 32px round arrow buttons on the right (matching the /pulse RoadmapCarousel chrome). Body — desktop layout (.rr-desktop): - Outer .rr-wrap holds an overflow-x: auto .rr-scroll with snap-x. - Track is sized to layout.trackWidth × 460. Cubic-bezier SVG path rendered behind milestones, stroked with a horizontal gradient that fades from #2a2520 / 0.55 alpha through to #2a2520 / 0.15 at the travelled-stop position (computed by travelledStopFor in step 3). - Each milestone is a 14px round dot in its status colour, with a 5px cream halo cutting the path beneath. The 'you are here' marker (most recent shipping item) gets a 1.15× scale + a quiet 2.4s pulse ring. - Cards hang from each dot via a 1px / 30px vertical connector, on the alternating cardSide returned by layout. .rr-card is the anchor target; hover and :focus-visible both reveal the description + trailing line via max-height + opacity transitions, so keyboard tab is a first-class interaction (no mouse required). - Trailing line: item.metadata_text if set, else 'Shaped by {first names}' if attributed_members non-empty, else nothing. - Edge fades on both sides for scroll affordance (left fade hidden when at scrollLeft 0; right fade hides when at scrollEnd — the JS in step 6 will toggle their opacity). Progress dots row underneath — count = max(2, min(6, ceil(items/2))). First dot starts active; nav script will move it. Mobile vertical fallback (.rr-mobile) markup is included now but kept display:none on desktop. Step 7 turns it on at the (max-width: 767px) breakpoint. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/components/RoadmapRoute.astro | 411 ++++++++++++++++++++++++++++++ 1 file changed, 411 insertions(+) create mode 100644 src/components/RoadmapRoute.astro diff --git a/src/components/RoadmapRoute.astro b/src/components/RoadmapRoute.astro new file mode 100644 index 0000000..5a8ecb8 --- /dev/null +++ b/src/components/RoadmapRoute.astro @@ -0,0 +1,411 @@ +--- +import type { RoadmapItemWithAttribution, RoadmapStatus } from '../lib/db'; +import { computeRouteLayout, travelledStopFor } from '../lib/roadmap-layout'; + +interface Props { + items: RoadmapItemWithAttribution[]; + viewportWidth?: number; // SSR fallback for the layout math +} + +const { items, viewportWidth = 1100 } = Astro.props; + +const layout = computeRouteLayout({ itemCount: items.length, viewportWidth }); +const travelledStop = travelledStopFor(items.map(i => i.status)); + +const STATUS_LABEL: Record = { + shipping: 'SHIPPING', + in_beta: 'IN BETA', + exploring: 'EXPLORING', + considering: 'CONSIDERING', +}; +const STATUS_LABEL_COLOR: Record = { + shipping: '#6d8c7c', + in_beta: '#b96b58', + exploring: '#b4b2a9', + considering: '#b4b2a9', +}; +const STATUS_DOT_COLOR: Record = { + shipping: '#6d8c7c', + in_beta: '#b96b58', + exploring: '#b4b2a9', + considering: '#d4d2c8', +}; + +// "You are here" — the most recent shipping item. -1 if nothing has shipped yet. +let lastShippingIndex = -1; +items.forEach((it, i) => { if (it.status === 'shipping') lastShippingIndex = i; }); + +function trailingLine(item: RoadmapItemWithAttribution): string | null { + if (item.metadata_text && item.metadata_text.trim().length > 0) return item.metadata_text; + if (item.attributed.length > 0) { + const names = item.attributed.map(a => a.name.split(' ')[0]); + if (names.length === 1) return `Shaped by ${names[0]}`; + if (names.length === 2) return `Shaped by ${names[0]} and ${names[1]}`; + return `Shaped by ${names.slice(0, -1).join(', ')} and ${names.at(-1)}`; + } + return null; +} + +// Progress dots — between 2 and 6, scaling with item count. +const progressDots = Math.max(2, Math.min(6, Math.ceil(items.length / 2))); + +// JSON-stringified ids for the nav script's initial-scroll logic. +const itemXByIndex = layout.itemX; +const initialShippingX = lastShippingIndex >= 0 ? itemXByIndex[lastShippingIndex] : 0; +--- +
+ + +
+
+

The route

+
    +
  • Shipping
  • +
  • In beta
  • +
  • Exploring
  • +
  • Considering
  • +
+
+
+ + +
+
+ + + + + + + + +
    + {items.map((item, i) => ( +
  1. + +
    +

    + {item.target ? `${item.target.toUpperCase()} · ` : ''}{STATUS_LABEL[item.status]} +

    +

    {item.title}

    + {item.description &&

    {item.description}

    } + {trailingLine(item) &&

    {trailingLine(item)}

    } +
    +
  2. + ))} +
+
+ +