Skip to content

feat(roadmap): public /roadmap page with a workstream timeline - #350

Merged
QSchlegel merged 3 commits into
preprodfrom
claude/roadmap-page
Jul 27, 2026
Merged

feat(roadmap): public /roadmap page with a workstream timeline#350
QSchlegel merged 3 commits into
preprodfrom
claude/roadmap-page

Conversation

@QSchlegel

Copy link
Copy Markdown
Collaborator

Adds a dedicated /roadmap page that renders the 12-month plan as a workstream timeline, so the roadmap is legible without reading ROADMAP.md.

Companion to #347, which updates ROADMAP.md itself. This PR is the page; the two touch different files.

Approach

Follows the existing public-page pattern — a thin src/pages/roadmap.tsx over a component under components/pages/homepage/roadmap, using Reveal for staggered entrance and the shared Table and Tooltip primitives. Content lives in data.ts so the plan can be edited without touching layout code.

The timeline is grouped by workstream rather than by owner, so continuity from delivered to planned stays visible; owner initials ride on each bar as a corner badge instead of consuming label width.

Colour

Status is encoded by fill, icon and label together — never colour alone. Emerald and amber carry the two chart states because they stay distinguishable under the common forms of colour blindness, where emerald/red does not; red is reserved for the single critical callout and is never used as a chart colour. "Planned" is a dashed outline rather than a fourth hue, since absence of status reads better as an outline than as grey fill.

Three layout details, each found by measuring the rendered page

  • Every grid cell is placed explicitly. The "today" rule spans all rows, and a grid item with a definite position is packed before auto-placed siblings — which was shifting the month headers a column to the right and wrapping the last one onto its own row, so the headers no longer lined up with the bars beneath them.
  • The page root needs w-full + min-w-0. It is a flex item, and mx-auto suppresses flex stretch, so the box stayed sized to the grid's 1180px min-content and pushed the whole page wider than the viewport — clipped by the global overflow-x: hidden — instead of letting the grid scroll on its own. This is what makes the page usable on mobile.
  • Bar labels need min-w-0 so a long single word wraps inside its bar rather than spilling into the next month.

Wiring

Route added to the SEO registry (routeSeo), the sitemap (INDEXABLE_ROUTES), the public-route allowlist, the no-JS crawler fallback, and the footer's product links.

Verification

  • next build clean; npx tsc --noEmit reports nothing in the new files
  • Full unit suite: 498 passed, 2 skipped
  • Rendered and measured at 375px and 1440px, light and dark: no page-level horizontal overflow, zero bar overlaps, zero clipped labels, tooltips and keyboard focus working

Adds a dedicated Roadmap page rendering the 12-month plan as a
workstream timeline, so the roadmap is legible without reading
ROADMAP.md.

Layout follows the existing public-page pattern (thin page + component
under components/pages/homepage, Reveal for staggered entrance, the
shared Table and Tooltip primitives). Content lives in data.ts so the
plan can be edited without touching layout code.

The timeline is grouped by workstream rather than by owner so the
continuity from delivered to planned stays visible; owner initials ride
on each bar as a corner badge. Status is encoded by fill, icon and
label together, never colour alone. Emerald and amber are used for the
two chart states because they stay distinguishable under the common
forms of colour blindness, where emerald/red would not; red is reserved
for the single critical callout and is never a chart colour. "Planned"
is a dashed outline rather than a fourth hue, since absence of status
reads better as an outline than as grey fill.

Three layout details worth noting, each verified in the browser:
- Every grid cell is placed explicitly. The "today" rule spans all rows,
  and a grid item with a definite position is packed before auto-placed
  siblings, which otherwise shifted the month headers a column right.
- The page root needs w-full + min-w-0. It is a flex item, and mx-auto
  suppresses flex stretch, which left the box sized to the grid's
  min-content and pushed the whole page wider than the viewport instead
  of letting the grid scroll on its own.
- Bar labels need min-w-0 so a long word wraps inside the bar instead of
  spilling into the next month.

Wires the route into the SEO registry, the sitemap, the public-route
allowlist, the no-JS crawler fallback and the footer.

Verified at 375px and 1440px, in light and dark: no page-level
horizontal overflow, no bar overlaps, no clipped labels.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 27, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
multisig Ready Ready Preview, Comment Jul 27, 2026 9:45am

Request Review

Scrolling the timeline horizontally slid the bars over the sticky
workstream column instead of under it, so labels and bars collided, and
the frozen corner was translucent enough for scrolled month headers to
show through it.

Sticky cells need both a stacking order and an opaque fill. Cells sit at
the base, bars and the today rule at z-10, the frozen workstream column
at z-20, the corner at z-30 — and the corner moves from bg-muted/50 with
a backdrop blur to solid bg-muted. Verified scrolled in light and dark.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds a `/vault` folder at the repo root describing every feature and the
state it is in, and an interactive graph at /roadmap/graph driven by it.

The vault is an Obsidian-compatible folder of plain markdown with YAML
frontmatter and [[wikilinks]] — 52 features across 10 areas and 4 states,
openable directly in Obsidian with no plugins. Each feature's `area` and
`state` become edges to those notes, and every wikilink in a body becomes
an edge between features, so there is no separate index to keep in sync:
changing one `state:` line is what moves a node.

src/lib/vault.ts parses it at build time via getStaticProps — the only
page in the app not using a no-op getServerSideProps. The vault only
changes when the repo does, so this costs nothing at runtime, and a
malformed note fails the build rather than a visitor's request. A feature
naming an area or state with no note throws, so typos surface at build.

The frontmatter parser covers a documented YAML subset (scalars, inline
arrays, block lists) rather than adding a dependency — a deliberate
trade-off in a wallet codebase. Types live in vault-types.ts because
importing any *value* from the loader would pull `fs` into the client
bundle.

The graph is a small force simulation rather than a graph library: same
reasoning on dependencies, and at 66 nodes the O(n²) pass is cheap. It
settles before paint instead of animating, so there is no motion to sit
through. Layout is seeded from a deterministic hash so server and client
agree.

Interaction: click to read a note, hover or focus to isolate a node and
its neighbours, drag to rearrange, filter by state, search across titles,
summaries, areas and owners, and toggle the state hubs off to untangle
the layout into pure area structure.

Details worth keeping:
- Pointer-down deliberately changes no state. Seeding the drag there
  re-ran the layout and moved the node out from under the cursor, so the
  click never completed. Dragging now starts after a 5px threshold, and
  only the dropped node is pinned while the rest relax around it.
- Hub labels get a placement pass that tries below, above, then further
  out, skipping slots that collide or fall outside the canvas. Without
  it two area labels sat on top of each other.
- Feature labels appear on demand; 52 of them at once is an unreadable
  mat of text.

Wires the route into the SEO registry, sitemap, public-route allowlist,
no-JS crawler fallback and footer, and links it from the roadmap page.
/roadmap moves to /roadmap/index so the graph is a sibling route.

Verified at 375px and 1440px, light and dark: no page overflow, no
overlapping or clipped labels, selection, filters, search and drag all
working. 514 unit tests pass, 16 of them covering the parser.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@QSchlegel

Copy link
Copy Markdown
Collaborator Author

Pushed a second, larger piece to this branch: a feature vault at the repo root and an interactive knowledge graph driven by it at /roadmap/graph.

It is stacked here rather than opened separately because it moves src/pages/roadmap.tsx (introduced in this PR) to src/pages/roadmap/index.tsx so the graph can be a sibling route — a separate PR off preprod would conflict on that file.

The vault

/vault is an Obsidian-compatible folder of plain markdown — YAML frontmatter and [[wikilinks]], no plugins needed. 52 features across 10 areas and 4 states. Each note carries the state its feature is in:

---
type: feature
area: Governance
state: delivered      # delivered | in-progress | planned | blocked
owner: Quirin
milestone: 2026-06
issues: [122]
prs: [272, 296]
---

area and state become edges to those notes, and every wikilink in a body becomes an edge between features. There is no separate index to keep in sync — changing one state: line is what moves a node in the graph.

How it is read

src/lib/vault.ts parses the vault at build time via getStaticProps — the only page in the app not using a no-op getServerSideProps. The vault only changes when the repo does, so this costs nothing at runtime, and a malformed note fails the build rather than a visitor's request. A feature naming an area or state that has no note throws, so a typo surfaces at build time instead of silently dropping an edge.

Two deliberate trade-offs:

  • No new dependencies. The frontmatter parser covers a documented YAML subset (scalars, inline arrays, block lists) instead of pulling in gray-matter, and the layout is a small force simulation instead of a graph library. In a wallet codebase I would rather keep the dependency surface flat; at 66 nodes the O(n²) pass is cheap.
  • Types live in vault-types.ts. Importing any value from the loader would drag fs into the client bundle — which is exactly how the first build failed.

The graph

Click a node to read it, hover or focus to isolate it and its neighbours, drag to rearrange, filter by state, search across titles, summaries, areas and owners, and toggle the state hubs off to untangle the layout into pure area structure. It settles before paint rather than animating, and the layout is seeded from a deterministic hash so server and client agree.

Three fixes found by measuring the rendered page, not by reading the code:

  • Pointer-down deliberately changes no state. Seeding the drag there re-ran the layout and moved the node out from under the cursor, so a click never completed — nodes simply would not select. Dragging now begins after a 5px threshold, and only the dropped node is pinned while the rest relax around it.
  • Hub labels get a placement pass that tries below, above, then further out, skipping slots that collide or fall outside the canvas. Without it, two area labels sat on top of each other.
  • Feature labels appear on demand. All 52 at once is an unreadable mat of text.

Verification

  • next build clean; /roadmap/graph prerenders as a static page (●)
  • 514 unit tests pass, 16 of them new and covering the parser — frontmatter subset, CRLF, unterminated fences, wikilink de-duplication, and graph invariants (every feature has exactly one area and one state edge, no self-links, no dangling edges, degree matches the edges that touch each node)
  • Rendered and measured at 375px and 1440px, light and dark: no page overflow, zero overlapping or clipped labels, and selection, filters, search and drag all confirmed working

@QSchlegel
QSchlegel merged commit a336539 into preprod Jul 27, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant