feat(roadmap): add planned status

Adds a fifth roadmap status, `planned`, for items that are committed and
scheduled but not yet started — sitting between `in_beta` and `exploring`
in the progression. Rendered with the design system's indigo pigment
(#5a6d83) on the route, carousel, legend, and admin pill.

Migration 0008 widens the status CHECK constraint via a table rebuild
(SQLite can't alter it in place), preserving rows and attributions.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jonathan Hvid 2026-06-10 11:30:06 +02:00
parent a520e8534e
commit 59842432bd
8 changed files with 52 additions and 2 deletions

View file

@ -0,0 +1,39 @@
-- Roadmap status enum gains a fifth value `planned` for items that are
-- committed and scheduled but not yet started — sitting between `in_beta`
-- and `exploring` in the progression.
--
-- SQLite can't widen a CHECK constraint in place, so this is a full table
-- rebuild (same approach as 0006). roadmap_attributions has an ON DELETE
-- CASCADE FK to roadmap_items(id), so foreign keys are toggled off around
-- the rebuild to preserve attribution rows across the DROP/RENAME. The
-- metadata_text column added in 0007 is carried through.
PRAGMA foreign_keys = OFF;
CREATE TABLE roadmap_items_new (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
description TEXT NOT NULL DEFAULT '',
status TEXT NOT NULL DEFAULT 'exploring'
CHECK(status IN ('shipping','in_beta','planned','exploring','considering')),
target TEXT,
display_order INTEGER NOT NULL DEFAULT 0,
shipped_at TEXT,
metadata_text TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
INSERT INTO roadmap_items_new
(id, title, description, status, target, display_order, shipped_at, metadata_text, created_at, updated_at)
SELECT
id, title, description, status, target, display_order, shipped_at, metadata_text, created_at, updated_at
FROM roadmap_items;
DROP TABLE roadmap_items;
ALTER TABLE roadmap_items_new RENAME TO roadmap_items;
CREATE INDEX idx_roadmap_status ON roadmap_items(status, display_order);
CREATE INDEX idx_roadmap_shipped ON roadmap_items(shipped_at);
PRAGMA foreign_keys = ON;

View file

@ -802,6 +802,7 @@
.pill-declined { background: rgba(0, 0, 0, 0.04); color: var(--on-surface-muted); }
.pill-shipping { background: rgba(109, 140, 124, 0.18); color: #5a7268; }
.pill-in-beta { background: rgba(185, 107, 88, 0.10); color: #b96b58; }
.pill-planned { background: rgba(90, 109, 131, 0.12); color: #5a6d83; }
.pill-exploring { background: rgba(186, 186, 176, 0.20); color: var(--on-surface-variant); }
.pill-considering{ background: rgba(186, 186, 176, 0.10); color: var(--on-surface-muted); }
.pill-active { background: rgba(109, 140, 124, 0.15); color: #6d8c7c; }

View file

@ -48,6 +48,7 @@ export const roadmapResource: Resource<RoadmapItemWithAttribution> = {
pillVariants: {
shipping: { label: 'Shipping', class: 'pill-shipping' },
in_beta: { label: 'In beta', class: 'pill-in-beta' },
planned: { label: 'Planned', class: 'pill-planned' },
exploring: { label: 'Exploring', class: 'pill-exploring' },
considering: { label: 'Considering', class: 'pill-considering' },
},
@ -69,6 +70,7 @@ export const roadmapResource: Resource<RoadmapItemWithAttribution> = {
{ key: 'all', label: 'All', predicate: () => true, isDefault: true },
{ key: 'shipping', label: 'Shipping', predicate: (i) => i.status === 'shipping' },
{ key: 'in_beta', label: 'In beta', predicate: (i) => i.status === 'in_beta' },
{ key: 'planned', label: 'Planned', predicate: (i) => i.status === 'planned' },
{ key: 'exploring', label: 'Exploring', predicate: (i) => i.status === 'exploring' },
{ key: 'considering', label: 'Considering', predicate: (i) => i.status === 'considering' },
],
@ -99,6 +101,7 @@ export const roadmapResource: Resource<RoadmapItemWithAttribution> = {
options: [
{ value: 'shipping', label: 'Shipping' },
{ value: 'in_beta', label: 'In beta' },
{ value: 'planned', label: 'Planned' },
{ value: 'exploring', label: 'Exploring' },
{ value: 'considering', label: 'Considering' },
],

View file

@ -10,6 +10,7 @@ const { items } = Astro.props;
const STATUS_LABEL: Record<RoadmapStatus, string> = {
shipping: 'SHIPPING',
in_beta: 'IN BETA',
planned: 'PLANNED',
exploring: 'EXPLORING',
considering: 'CONSIDERING',
};
@ -17,6 +18,7 @@ const STATUS_LABEL: Record<RoadmapStatus, string> = {
const STATUS_LABEL_COLOR: Record<RoadmapStatus, string> = {
shipping: 'var(--pigment-copper)',
in_beta: 'var(--pigment-terracotta)',
planned: 'var(--pigment-indigo)',
exploring: '#b4b2a9',
considering: '#b4b2a9',
};
@ -24,6 +26,7 @@ const STATUS_LABEL_COLOR: Record<RoadmapStatus, string> = {
const STATUS_DOT_COLOR: Record<RoadmapStatus, string> = {
shipping: 'var(--pigment-copper)',
in_beta: 'var(--pigment-terracotta)',
planned: 'var(--pigment-indigo)',
exploring: '#b4b2a9',
considering: '#d4d2c8',
};

View file

@ -25,18 +25,21 @@ const travelledStop = travelledStopFor(items.map(i => i.status));
const STATUS_LABEL: Record<RoadmapStatus, string> = {
shipping: 'SHIPPING',
in_beta: 'IN BETA',
planned: 'PLANNED',
exploring: 'EXPLORING',
considering: 'CONSIDERING',
};
const STATUS_LABEL_COLOR: Record<RoadmapStatus, string> = {
shipping: '#6d8c7c',
in_beta: '#b96b58',
planned: '#5a6d83',
exploring: '#b4b2a9',
considering: '#b4b2a9',
};
const STATUS_DOT_COLOR: Record<RoadmapStatus, string> = {
shipping: '#6d8c7c',
in_beta: '#b96b58',
planned: '#5a6d83',
exploring: '#b4b2a9',
considering: '#d4d2c8',
};

View file

@ -688,7 +688,7 @@ export function countPulseParticipants(pulseId: number): number {
// ── Roadmap items ────────────────────────────────────────────────
export type RoadmapStatus = 'shipping' | 'in_beta' | 'exploring' | 'considering';
export type RoadmapStatus = 'shipping' | 'in_beta' | 'planned' | 'exploring' | 'considering';
export interface RoadmapItem {
id: number;

View file

@ -113,7 +113,7 @@ export function computeRouteLayout(opts: LayoutOpts): LayoutResult {
* - Clamped to [0, 0.98] so the fade-to-ahead is always visible
*/
export function travelledStopFor(
statuses: ReadonlyArray<'shipping' | 'in_beta' | 'exploring' | 'considering'>,
statuses: ReadonlyArray<'shipping' | 'in_beta' | 'planned' | 'exploring' | 'considering'>,
): number {
if (statuses.length === 0) return 0;
let last = -1;

View file

@ -28,6 +28,7 @@ const items = getAllRoadmapItems()
<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:#5a6d83"></i>Planned</span>
<span><i style="background:#b4b2a9"></i>Exploring</span>
<span><i style="background:#d4d2c8"></i>Considering</span>
</div>