feat(roadmap): centred 'Roadmap' page header + legend escapes route component
Page-level rebuild. /roadmap.astro renders a centred header block that reads as one calm vertical stack: Roadmap ← 11px tracked uppercase eyebrow (--on-surface-variant) Roadmap ← 48px serif h1, single word A live picture of the work… ← 14px subtitle, 480px max-width The eyebrow + h1 read 'Roadmap → Roadmap' on purpose — the tracked uppercase eyebrow primes the eye for the serif headline, and the repetition feels confident rather than redundant. If it starts to grate in practice, the eyebrow's the easy drop. 'What we are building.' is gone. The earlier 'The route' sub-header inside <RoadmapRoute> is gone. The two prev/next arrow buttons in that sub-header are gone (the single right-edge advance arrow lands in the next commit). Legend moves out of <RoadmapRoute> and into /roadmap.astro as the page's final block. With the route still .rr-fullbleed, this lets the legend return to centred content-column width — exactly the spec's 'header-centred / route-wide / legend-centred' rhythm. Mid-state in this commit: the advance arrow doesn't exist yet, so there's no in-page scroll affordance beyond the still-active scroll-snap behaviour. Step 3 adds the arrow; step 4 strips the snap and adds drag + wheel + animated glide. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
1c020f191c
commit
22a55aa073
2 changed files with 69 additions and 126 deletions
|
|
@ -52,23 +52,6 @@ const initialShippingX = lastShippingIndex >= 0 ? layout.itemX[lastShippingIndex
|
|||
---
|
||||
<section class="route" aria-label="Roadmap route" data-initial-x={initialShippingX}>
|
||||
|
||||
<!-- Section header (title + arrows). Legend moves below the track. -->
|
||||
<header class="route-header">
|
||||
<h2 class="route-title">The route</h2>
|
||||
<div class="route-arrows" role="group" aria-label="Scroll the route">
|
||||
<button type="button" class="route-arrow" id="rr-prev" data-dir="prev" aria-label="Previous" disabled>
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" aria-hidden="true">
|
||||
<path d="M9 2 L4 7 L9 12" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button type="button" class="route-arrow" id="rr-next" data-dir="next" aria-label="Next">
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" aria-hidden="true">
|
||||
<path d="M5 2 L10 7 L5 12" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- The route — desktop horizontal. .rr-fullbleed escapes the parent
|
||||
.page max-width so the route can span the actual viewport while
|
||||
the header above and legend below stay centred in the content
|
||||
|
|
@ -126,16 +109,10 @@ const initialShippingX = lastShippingIndex >= 0 ? layout.itemX[lastShippingIndex
|
|||
<div class="rr-fade-right" id="rr-fade-r" aria-hidden="true"></div>
|
||||
</div>
|
||||
|
||||
<!-- Legend — sits under the track, centred. Reading order is now
|
||||
"The route" → walk the path → "ah, the dots mean these things." -->
|
||||
<div class="rr-legend rr-desktop" aria-label="Status legend">
|
||||
<span><i style="background:#6d8c7c"></i>Shipping</span>
|
||||
<span><i style="background:#b96b58"></i>In beta</span>
|
||||
<span><i style="background:#b4b2a9"></i>Exploring</span>
|
||||
<span><i style="background:#d4d2c8"></i>Considering</span>
|
||||
</div>
|
||||
<!-- Legend lives in /roadmap.astro now so it returns to centred
|
||||
content-column width below the full-bleed route. -->
|
||||
|
||||
<!-- Mobile vertical timeline — built in step 7 -->
|
||||
<!-- Mobile vertical timeline -->
|
||||
<ol class="rr-mobile" aria-label="Roadmap timeline">
|
||||
{items.map((item, i) => (
|
||||
<li class="rrm-row">
|
||||
|
|
@ -174,8 +151,6 @@ const initialShippingX = lastShippingIndex >= 0 ? layout.itemX[lastShippingIndex
|
|||
const svg = section.querySelector<SVGSVGElement>('#rr-path-svg');
|
||||
const pathD = section.querySelector<SVGPathElement>('#rr-path-d');
|
||||
const milestones = Array.from(section.querySelectorAll<HTMLElement>('.rr-milestone'));
|
||||
const prev = section.querySelector<HTMLButtonElement>('#rr-prev');
|
||||
const next = section.querySelector<HTMLButtonElement>('#rr-next');
|
||||
const fadeL = section.querySelector<HTMLElement>('#rr-fade-l');
|
||||
const fadeR = section.querySelector<HTMLElement>('#rr-fade-r');
|
||||
if (!scroll || !track || !svg) return;
|
||||
|
|
@ -219,16 +194,12 @@ const initialShippingX = lastShippingIndex >= 0 ? layout.itemX[lastShippingIndex
|
|||
milestones.forEach((m, i) => { m.style.left = `${itemX[i]}px`; });
|
||||
}
|
||||
|
||||
const step = () => scroll!.clientWidth * 0.72;
|
||||
prev?.addEventListener('click', () => scroll!.scrollBy({ left: -step(), behavior: 'smooth' }));
|
||||
next?.addEventListener('click', () => scroll!.scrollBy({ left: step(), behavior: 'smooth' }));
|
||||
|
||||
/* Arrow handler + edge-state updates are rewritten in the next commit
|
||||
(drag + wheel + glide). For now only the fades update on scroll. */
|
||||
function updateNav() {
|
||||
const max = scroll!.scrollWidth - scroll!.clientWidth;
|
||||
const atStart = scroll!.scrollLeft <= 2;
|
||||
const atEnd = scroll!.scrollLeft >= max - 2;
|
||||
if (prev) prev.disabled = atStart;
|
||||
if (next) next.disabled = atEnd;
|
||||
if (fadeL) fadeL.style.opacity = atStart ? '0' : '1';
|
||||
if (fadeR) fadeR.style.opacity = atEnd ? '0' : '1';
|
||||
}
|
||||
|
|
@ -259,45 +230,6 @@ const initialShippingX = lastShippingIndex >= 0 ? layout.itemX[lastShippingIndex
|
|||
</script>
|
||||
|
||||
<style>
|
||||
/* ── Section header ─────────────────────────────────────────────── */
|
||||
.route-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
gap: 24px;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
.route-title {
|
||||
font-family: var(--font-serif);
|
||||
font-weight: 400;
|
||||
font-size: 20px;
|
||||
line-height: 1.2;
|
||||
color: var(--on-surface);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.route-arrows {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
.route-arrow {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
border: 0.5px solid rgba(0, 0, 0, 0.18);
|
||||
background: var(--background);
|
||||
color: var(--on-surface);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
transition: opacity var(--duration-fast) var(--ease-standard),
|
||||
background var(--duration-fast) var(--ease-standard);
|
||||
}
|
||||
.route-arrow:hover:not(:disabled) { background: var(--surface-container-low); }
|
||||
.route-arrow:disabled { opacity: 0.25; cursor: default; }
|
||||
|
||||
/* ── Desktop route ──────────────────────────────────────────────── */
|
||||
.rr-wrap { position: relative; }
|
||||
|
||||
|
|
@ -475,37 +407,11 @@ const initialShippingX = lastShippingIndex >= 0 ? layout.itemX[lastShippingIndex
|
|||
background: linear-gradient(to right, transparent, var(--background));
|
||||
}
|
||||
|
||||
/* Legend below the track — reading order: title → track → key. */
|
||||
.rr-legend {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 24px;
|
||||
margin-top: 28px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.rr-legend span {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
font-family: var(--font-sans);
|
||||
font-size: 10px;
|
||||
letter-spacing: 1px;
|
||||
text-transform: uppercase;
|
||||
color: var(--on-surface-variant);
|
||||
}
|
||||
.rr-legend i {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* ── Mobile vertical timeline ──────────────────────────────────── */
|
||||
.rr-mobile { display: none; }
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.rr-desktop { display: none; }
|
||||
.route-arrows { display: none; }
|
||||
.rr-mobile {
|
||||
display: block;
|
||||
list-style: none;
|
||||
|
|
|
|||
|
|
@ -6,17 +6,16 @@ import { getAllRoadmapItems } from '../lib/db';
|
|||
|
||||
const user = Astro.locals.user;
|
||||
|
||||
// Admin orders chronologically nearest-to-furthest via display_order.
|
||||
const items = getAllRoadmapItems()
|
||||
.sort((a, b) => a.display_order - b.display_order || a.id - b.id);
|
||||
---
|
||||
<AppLayout title="Roadmap" user={user}>
|
||||
<div class="page">
|
||||
<article class="roadmap-page">
|
||||
|
||||
<header class="page-header">
|
||||
<p class="page-eyebrow">Roadmap</p>
|
||||
<h1 class="page-title">What we are building.</h1>
|
||||
<p class="page-sub">
|
||||
<header class="roadmap-header">
|
||||
<p class="roadmap-eyebrow">Roadmap</p>
|
||||
<h1 class="roadmap-title">Roadmap</h1>
|
||||
<p class="roadmap-subtitle">
|
||||
A live picture of the work. What's in motion, what's queued,
|
||||
what we're still thinking about. Tap or hover any milestone
|
||||
for the full story.
|
||||
|
|
@ -26,27 +25,42 @@ const items = getAllRoadmapItems()
|
|||
<LatestDispatchBanner />
|
||||
|
||||
<RoadmapRoute items={items} />
|
||||
</div>
|
||||
|
||||
<!-- Legend lives outside the route component so it can return to
|
||||
centred content-column width while the route goes full-bleed. -->
|
||||
<div class="roadmap-legend" aria-label="Status legend">
|
||||
<span><i style="background:#6d8c7c"></i>Shipping</span>
|
||||
<span><i style="background:#b96b58"></i>In beta</span>
|
||||
<span><i style="background:#b4b2a9"></i>Exploring</span>
|
||||
<span><i style="background:#d4d2c8"></i>Considering</span>
|
||||
</div>
|
||||
|
||||
</article>
|
||||
</AppLayout>
|
||||
|
||||
<style>
|
||||
.page {
|
||||
padding: 40px 36px 80px;
|
||||
.roadmap-page {
|
||||
padding: 0 36px 80px;
|
||||
max-width: var(--content-max);
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* ── Page header ─────────────────────────────────────────────── */
|
||||
.page-header { margin-bottom: 36px; max-width: 540px; }
|
||||
.page-eyebrow {
|
||||
/* ── Centred page header ──────────────────────────────────────── */
|
||||
.roadmap-header {
|
||||
text-align: center;
|
||||
max-width: 640px;
|
||||
margin: 0 auto 48px;
|
||||
padding-top: 32px;
|
||||
}
|
||||
.roadmap-eyebrow {
|
||||
font-family: var(--font-sans);
|
||||
font-size: 11px;
|
||||
letter-spacing: var(--tracking-wider);
|
||||
letter-spacing: 1.4px;
|
||||
text-transform: uppercase;
|
||||
color: var(--on-surface-variant);
|
||||
margin: 0 0 12px;
|
||||
}
|
||||
.page-title {
|
||||
.roadmap-title {
|
||||
font-family: var(--font-serif);
|
||||
font-weight: 400;
|
||||
font-size: 48px;
|
||||
|
|
@ -55,24 +69,47 @@ const items = getAllRoadmapItems()
|
|||
color: var(--on-surface);
|
||||
margin: 0 0 14px;
|
||||
}
|
||||
.page-sub {
|
||||
.roadmap-subtitle {
|
||||
font-size: 14px;
|
||||
line-height: 1.55;
|
||||
line-height: 1.65;
|
||||
color: var(--on-surface-variant);
|
||||
margin: 0;
|
||||
max-width: 540px;
|
||||
margin: 0 auto;
|
||||
max-width: 480px;
|
||||
}
|
||||
|
||||
/* Banner sits directly above the route now — restore the original
|
||||
56px gap so the editorial banner reads as its own beat. */
|
||||
.page :global(.banner),
|
||||
.page :global(.rr-dispatch) { margin-bottom: 56px; }
|
||||
.roadmap-page :global(.banner),
|
||||
.roadmap-page :global(.rr-dispatch) { margin-bottom: 56px; }
|
||||
|
||||
/* ── Legend ───────────────────────────────────────────────────── */
|
||||
.roadmap-legend {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 24px;
|
||||
margin-top: 28px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.roadmap-legend span {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
font-family: var(--font-sans);
|
||||
font-size: 10px;
|
||||
letter-spacing: 1px;
|
||||
text-transform: uppercase;
|
||||
color: var(--on-surface-variant);
|
||||
}
|
||||
.roadmap-legend i {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.page { padding: 32px 24px 64px; }
|
||||
.page-title { font-size: 36px; }
|
||||
.page-header { margin-bottom: 28px; }
|
||||
.page :global(.banner),
|
||||
.page :global(.rr-dispatch) { margin-bottom: 40px; }
|
||||
.roadmap-page { padding: 0 24px 64px; }
|
||||
.roadmap-header { padding-top: 24px; margin-bottom: 32px; }
|
||||
.roadmap-title { font-size: 36px; }
|
||||
.roadmap-page :global(.banner),
|
||||
.roadmap-page :global(.rr-dispatch) { margin-bottom: 40px; }
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue