213 lines
5.8 KiB
Text
213 lines
5.8 KiB
Text
---
|
|
import { readFileSync } from 'fs';
|
|
import { join } from 'path';
|
|
import AppLayout from '../layouts/AppLayout.astro';
|
|
import { marked } from 'marked';
|
|
|
|
const user = Astro.locals.user;
|
|
|
|
// Single-file roadmap — not a content collection
|
|
const raw = readFileSync(join(process.cwd(), 'content/roadmap.md'), 'utf-8');
|
|
|
|
// Strip YAML frontmatter
|
|
const body = raw.replace(/^---[\s\S]*?---\n/, '');
|
|
|
|
// Parse sections by ## headings
|
|
function parseSections(md: string) {
|
|
const sectionRe = /^## (.+)$/gm;
|
|
const sections: { title: string; items: { title: string; body: string; pilotOnly: boolean }[] }[] = [];
|
|
const matches = [...md.matchAll(sectionRe)];
|
|
|
|
for (let i = 0; i < matches.length; i++) {
|
|
const m = matches[i];
|
|
const start = m.index! + m[0].length;
|
|
const end = matches[i + 1]?.index ?? md.length;
|
|
const sectionBody = md.slice(start, end).trim();
|
|
|
|
// Each item starts with **Title** — description
|
|
const itemRe = /\*\*([^*]+)\*\*\s*—\s*([\s\S]*?)(?=\n\n\*\*|\n\n##|$)/g;
|
|
const items: { title: string; body: string; pilotOnly: boolean }[] = [];
|
|
let itemMatch: RegExpExecArray | null;
|
|
while ((itemMatch = itemRe.exec(sectionBody)) !== null) {
|
|
const rawBody = itemMatch[2].trim();
|
|
const pilotOnly = rawBody.includes('`pilot-only`');
|
|
const cleanBody = rawBody.replace(/`pilot-only`/g, '').trim();
|
|
items.push({ title: itemMatch[1], body: cleanBody, pilotOnly });
|
|
}
|
|
|
|
sections.push({ title: m[1], items });
|
|
}
|
|
return sections;
|
|
}
|
|
|
|
const sections = parseSections(body);
|
|
|
|
const horizonColors: Record<string, string> = {
|
|
'In progress': 'var(--pigment-copper)',
|
|
'Next': 'var(--pigment-ochre)',
|
|
'Later': 'var(--pigment-indigo)',
|
|
};
|
|
---
|
|
<AppLayout title="Roadmap" user={user}>
|
|
<div class="page">
|
|
|
|
<header class="page-header">
|
|
<p class="label-sm eyebrow">Roadmap</p>
|
|
<h1 class="display-md page-title">What we are building.</h1>
|
|
<p class="lead subtitle">
|
|
Three horizons. What is in progress now, what comes next,
|
|
and what is further out. This is the live picture.
|
|
</p>
|
|
</header>
|
|
|
|
<div class="horizons">
|
|
{sections.map((section) => (
|
|
<section class="horizon">
|
|
<div class="horizon-header">
|
|
<span
|
|
class="horizon-dot"
|
|
style={`background: ${horizonColors[section.title] ?? 'var(--on-surface-muted)'}`}
|
|
aria-hidden="true"
|
|
/>
|
|
<h2 class="headline-sm horizon-title">{section.title}</h2>
|
|
</div>
|
|
|
|
<ul class="item-list">
|
|
{section.items.map((item) => (
|
|
<li class="item">
|
|
<div class="item-header">
|
|
<h3 class="item-title body-lg">{item.title}</h3>
|
|
{item.pilotOnly && (
|
|
<span class="pilot-badge label-sm" title="Available to pilot participants only">
|
|
Pilot
|
|
</span>
|
|
)}
|
|
</div>
|
|
<p class="body-md item-body">{item.body}</p>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</section>
|
|
))}
|
|
</div>
|
|
|
|
</div>
|
|
</AppLayout>
|
|
|
|
<style>
|
|
.page {
|
|
padding: var(--space-12) var(--space-20) var(--space-16);
|
|
max-width: var(--content-max);
|
|
margin: 0 auto;
|
|
}
|
|
|
|
/* ── Header ──────────────────────────────────────────────────────── */
|
|
.page-header {
|
|
max-width: 44rem;
|
|
margin-bottom: var(--space-12);
|
|
}
|
|
|
|
.eyebrow {
|
|
letter-spacing: var(--tracking-wider);
|
|
text-transform: uppercase;
|
|
color: var(--on-surface-muted);
|
|
margin-bottom: var(--space-3);
|
|
}
|
|
|
|
.page-title {
|
|
margin-bottom: var(--space-5);
|
|
}
|
|
|
|
.subtitle {
|
|
color: var(--on-surface-variant);
|
|
max-width: var(--reading-max);
|
|
margin: 0;
|
|
}
|
|
|
|
/* ── Horizons ────────────────────────────────────────────────────── */
|
|
.horizons {
|
|
display: grid;
|
|
grid-template-columns: repeat(3, 1fr);
|
|
gap: var(--space-8);
|
|
align-items: start;
|
|
}
|
|
|
|
.horizon {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--space-5);
|
|
}
|
|
|
|
.horizon-header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--space-3);
|
|
padding-bottom: var(--space-4);
|
|
border-bottom: var(--ghost-border);
|
|
}
|
|
|
|
.horizon-dot {
|
|
width: 10px;
|
|
height: 10px;
|
|
border-radius: var(--radius-full);
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.horizon-title {
|
|
font-family: var(--font-sans);
|
|
font-weight: 500;
|
|
letter-spacing: var(--tracking-wide);
|
|
text-transform: uppercase;
|
|
font-size: var(--text-label-md);
|
|
margin: 0;
|
|
color: var(--on-surface-variant);
|
|
}
|
|
|
|
/* ── Items ───────────────────────────────────────────────────────── */
|
|
.item-list {
|
|
list-style: none;
|
|
padding: 0;
|
|
margin: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--space-6);
|
|
}
|
|
|
|
.item {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--space-2);
|
|
}
|
|
|
|
.item-header {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: var(--space-3);
|
|
}
|
|
|
|
.item-title {
|
|
font-family: var(--font-serif);
|
|
font-weight: 400;
|
|
letter-spacing: var(--tracking-snug);
|
|
margin: 0;
|
|
color: var(--on-surface);
|
|
flex: 1;
|
|
}
|
|
|
|
.pilot-badge {
|
|
flex-shrink: 0;
|
|
padding: 0.2em var(--space-2);
|
|
background: var(--surface-container);
|
|
border-radius: var(--radius-full);
|
|
letter-spacing: var(--tracking-wide);
|
|
text-transform: uppercase;
|
|
color: var(--on-surface-muted);
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.item-body {
|
|
margin: 0;
|
|
color: var(--on-surface-variant);
|
|
line-height: var(--leading-relaxed);
|
|
}
|
|
</style>
|