feat(roadmap): 'In motion right now' strip + subtitle copy + admin helper
New <RoadmapInMotion> component renders between the dispatch banner and the route's section header. Pulls the most-recent shipping item (same selection rule the .rr-current marker uses) and prints the first sentence of its description as a 18px serif italic line preceded by an 'IN MOTION RIGHT NOW' tracked eyebrow. A member who only spends 5 seconds on /roadmap now still walks away with a sentence about what just shipped — no scroll, no hover. firstSentenceOf() is the obvious regex against the first [.!?](?=\s|$). Bails to the 200-char slice if no sentence boundary fits (covers 'Dr.' / 'e.g.' confusables). Returns '' on null. The strip hides itself entirely when there's no shipping item, or when the shipping item has no description text. Page subtitle: 'Hover any milestone for the full story.' → 'Tap or hover any milestone for the full story.' — touch devices don't have hover, and the kind of detail that says we're paying attention. Admin description-field gains a helper note: 'For shipping items: the first sentence appears on /roadmap as the "In motion right now" line. Make it count.' Nudges good first-sentence writing without adding a new field to maintain. Banner margin under the dispatch banner reduces 56 → 40px because the in-motion strip carries its own 36px bottom margin to the route. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
ac52e97c28
commit
1ec01a2257
3 changed files with 79 additions and 6 deletions
69
src/components/RoadmapInMotion.astro
Normal file
69
src/components/RoadmapInMotion.astro
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
---
|
||||
import type { RoadmapItemWithAttribution } from '../lib/db';
|
||||
|
||||
interface Props {
|
||||
items: RoadmapItemWithAttribution[];
|
||||
}
|
||||
|
||||
const { items } = Astro.props;
|
||||
|
||||
/** First sentence of a description — naive but matches the user's needs.
|
||||
* Returns '' for null/empty input. Falls back to a 200-char slice if no
|
||||
* sentence-ending punctuation is found in a reasonable window. */
|
||||
function firstSentenceOf(text: string | null): string {
|
||||
if (!text) return '';
|
||||
const trimmed = text.trim();
|
||||
const match = trimmed.match(/^[^.!?]*[.!?](?=\s|$)/);
|
||||
return match ? match[0] : trimmed.slice(0, 200);
|
||||
}
|
||||
|
||||
// Most recent shipping item, in display_order (same selection rule as
|
||||
// the .rr-current marker on the route).
|
||||
let currentItem: RoadmapItemWithAttribution | null = null;
|
||||
items.forEach((it) => { if (it.status === 'shipping') currentItem = it; });
|
||||
|
||||
const line = currentItem ? firstSentenceOf(currentItem.description) : '';
|
||||
const visible = !!currentItem && line.length > 0;
|
||||
---
|
||||
{visible && (
|
||||
<div class="rr-in-motion">
|
||||
<span class="rr-in-motion-eyebrow">In motion right now</span>
|
||||
<p class="rr-in-motion-line">{line}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<style>
|
||||
.rr-in-motion {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 18px;
|
||||
margin-bottom: 36px;
|
||||
padding: 0 4px;
|
||||
}
|
||||
.rr-in-motion-eyebrow {
|
||||
font-family: var(--font-sans);
|
||||
font-size: 10px;
|
||||
letter-spacing: 1.4px;
|
||||
text-transform: uppercase;
|
||||
color: var(--on-surface-muted);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.rr-in-motion-line {
|
||||
font-family: var(--font-serif);
|
||||
font-style: italic;
|
||||
font-size: 18px;
|
||||
line-height: 1.4;
|
||||
color: var(--on-surface);
|
||||
margin: 0;
|
||||
max-width: 720px;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.rr-in-motion {
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
.rr-in-motion-line { font-size: 16px; }
|
||||
}
|
||||
</style>
|
||||
|
|
@ -64,6 +64,7 @@ const grouped: Record<Status, RoadmapItemWithAttribution[]> = {
|
|||
<div class="field">
|
||||
<label for="description" class="label-sm field-label">Description</label>
|
||||
<textarea id="description" name="description" class="input body-md" rows="3">{editing?.description ?? ''}</textarea>
|
||||
<span class="body-sm muted">For shipping items: the first sentence appears on /roadmap as the "In motion right now" line. Make it count.</span>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
---
|
||||
import AppLayout from '../layouts/AppLayout.astro';
|
||||
import LatestDispatchBanner from '../components/LatestDispatchBanner.astro';
|
||||
import RoadmapInMotion from '../components/RoadmapInMotion.astro';
|
||||
import RoadmapRoute from '../components/RoadmapRoute.astro';
|
||||
import { getAllRoadmapItems } from '../lib/db';
|
||||
|
||||
|
|
@ -18,13 +19,15 @@ const items = getAllRoadmapItems()
|
|||
<h1 class="page-title">What we are building.</h1>
|
||||
<p class="page-sub">
|
||||
A live picture of the work. What's in motion, what's queued,
|
||||
what we're still thinking about. Hover any milestone for the
|
||||
full story.
|
||||
what we're still thinking about. Tap or hover any milestone
|
||||
for the full story.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<LatestDispatchBanner />
|
||||
|
||||
<RoadmapInMotion items={items} />
|
||||
|
||||
<RoadmapRoute items={items} />
|
||||
</div>
|
||||
</AppLayout>
|
||||
|
|
@ -63,14 +66,14 @@ const items = getAllRoadmapItems()
|
|||
max-width: 540px;
|
||||
}
|
||||
|
||||
/* The banner already has its own bottom-margin via the layout in
|
||||
the parent. Add the spec'd 56px below it before the route header. */
|
||||
.page :global(.banner) { margin-bottom: 56px; }
|
||||
/* In-motion strip lives between banner and route; banner margin
|
||||
tightens to 40px because the strip carries its own 36px below. */
|
||||
.page :global(.banner) { margin-bottom: 40px; }
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.page { padding: 32px 24px 64px; }
|
||||
.page-title { font-size: 36px; }
|
||||
.page-header { margin-bottom: 28px; }
|
||||
.page :global(.banner) { margin-bottom: 36px; }
|
||||
.page :global(.banner) { margin-bottom: 28px; }
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue