diff --git a/src/components/RoadmapCarousel.astro b/src/components/RoadmapCarousel.astro new file mode 100644 index 0000000..bfeccb6 --- /dev/null +++ b/src/components/RoadmapCarousel.astro @@ -0,0 +1,260 @@ +--- +import type { RoadmapItemWithAttribution, RoadmapStatus } from '../lib/db'; + +interface Props { + items: RoadmapItemWithAttribution[]; +} + +const { items } = Astro.props; + +const STATUS_LABEL: Record = { + shipping: 'SHIPPING', + in_beta: 'IN BETA', + exploring: 'EXPLORING', + considering: 'CONSIDERING', +}; + +const STATUS_LABEL_COLOR: Record = { + shipping: 'var(--pigment-copper)', + in_beta: 'var(--pigment-terracotta)', + exploring: '#b4b2a9', + considering: '#b4b2a9', +}; + +const STATUS_DOT_COLOR: Record = { + shipping: 'var(--pigment-copper)', + in_beta: 'var(--pigment-terracotta)', + exploring: '#b4b2a9', + considering: '#d4d2c8', +}; + +/** First-names-only attribution string. Empty when no attribution exists. */ +function attributionLine(attributed: { name: string }[]): string { + if (!attributed.length) return ''; + const names = 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)}.`; +} + +const hasArrows = items.length > 3; +--- +
+
+

On the roadmap

+
+ See the full roadmap → + {hasArrows && ( +
+ + +
+ )} +
+
+ + +
+ + + + diff --git a/src/pages/pulse.astro b/src/pages/pulse.astro index 87af797..efbdc62 100644 --- a/src/pages/pulse.astro +++ b/src/pages/pulse.astro @@ -2,6 +2,7 @@ import AppLayout from '../layouts/AppLayout.astro'; import Avatar from '../components/Avatar.astro'; import EventHeroCard from '../components/EventHeroCard.astro'; +import RoadmapCarousel from '../components/RoadmapCarousel.astro'; import { getUpcomingEvents, getEventBySlug, getEventAttendees, getUserRsvp, setEventRsvp, recordActivity, @@ -98,28 +99,9 @@ function closeDayLabel(closesAt: string): string { }).format(parseUtc(closesAt)); } -// ── Roadmap preview (3 most-recently-updated, horizontal) ────────── -const roadmapPreview = getAllRoadmapItems() - .sort((a, b) => (b.updated_at > a.updated_at ? 1 : -1)) - .slice(0, 3); - -function roadmapStatusDot(status: 'shipping' | 'in_beta' | 'exploring' | 'considering'): string { - return ({ - shipping: 'var(--pigment-copper)', - in_beta: 'var(--pigment-terracotta)', - exploring: '#b4b2a9', - considering: '#d4d2c8', - })[status]; -} -function roadmapStatusBlurb(item: { status: 'shipping' | 'in_beta' | 'exploring' | 'considering'; target: string | null }): string { - const target = item.target ? ` · ${item.target}` : ''; - switch (item.status) { - case 'shipping': return `Shipping${target}`; - case 'in_beta': return `In beta${target}`; - case 'exploring': return `Exploring${target}`; - case 'considering': return `Considering${target}`; - } -} +// ── Roadmap (all items, admin orders chronologically near→far) ───── +const roadmapItems = getAllRoadmapItems() + .sort((a, b) => a.display_order - b.display_order || a.id - b.id); // ── Council ───────────────────────────────────────────────────────── const members = getAllCabMembers(); @@ -243,26 +225,11 @@ const members = getAllCabMembers(); )} - - {roadmapPreview.length > 0 && ( -
-
    - {roadmapPreview.map(item => ( -
  • - -
    -

    {item.title}

    -

    {roadmapStatusBlurb(item)}

    -
    -
  • - ))} -
- See the full roadmap -
+ + {roadmapItems.length > 0 && ( +
+ +
)} @@ -636,59 +603,6 @@ const members = getAllCabMembers(); .pulse-option.chosen .pulse-option-letter { color: var(--pigment-terracotta); } .pulse-option-text { flex: 1; } - /* ── Roadmap horizontal cards ─────────────────────────────────── */ - .roadmap-section { - display: flex; - flex-direction: column; - gap: var(--space-5); - } - .roadmap-grid { - list-style: none; - padding: 0; - margin: 0; - display: grid; - grid-template-columns: repeat(3, 1fr); - gap: var(--space-5); - } - .roadmap-card { - background: var(--surface-card); - border: 0.5px solid var(--surface-card-border); - border-radius: var(--radius-md); - padding: var(--space-6); - display: flex; - flex-direction: column; - gap: var(--space-3); - min-height: 140px; - } - .status-dot { - width: 10px; - height: 10px; - border-radius: 50%; - flex-shrink: 0; - } - @keyframes breathe { - 0%, 100% { transform: scale(1); opacity: 1; } - 50% { transform: scale(1.4); opacity: 0.5; } - } - .status-dot.breathing { animation: breathe 2.4s ease-in-out infinite; } - .roadmap-card-text { display: flex; flex-direction: column; gap: var(--space-1); } - .roadmap-card-title { - font-family: var(--font-serif); - font-weight: 400; - font-size: 1.0625rem; - line-height: 1.3; - color: var(--on-surface); - margin: 0; - } - .roadmap-card-blurb { - color: var(--on-surface-muted); - font-family: var(--font-sans); - font-size: var(--text-label-sm); - letter-spacing: var(--tracking-wide); - text-transform: uppercase; - margin: 0; - } - /* ── Council — one outer surface, no per-member boxes ─────────── */ .council-section { background: var(--surface-card); @@ -740,7 +654,6 @@ const members = getAllCabMembers(); /* ── Responsive ───────────────────────────────────────────────── */ @media (max-width: 880px) { - .roadmap-grid { grid-template-columns: 1fr; } .editorial-row { grid-template-columns: 1fr; gap: var(--space-8); } .also-coming-up { flex-direction: column; align-items: flex-start; gap: var(--space-3); } }