feat(db): roadmap_items gains 'considering' + 'in_beta' rename, --on-ink tokens

Migration 0006 (the spec said 0005 but that number was already taken by
polls_on_dispatches from the previous session): rebuilds the
roadmap_items CHECK to ('shipping','in_beta','exploring','considering')
and renames any existing 'beta' rows to 'in_beta' in-place. FKs from
roadmap_attributions are preserved across the DROP/RENAME by toggling
PRAGMA foreign_keys off around the rebuild — attribution count unchanged
after migrate (verified 4 rows survive on the demo DB).

Tokens (src/styles/tokens.css): adds --on-ink, --on-ink-body,
--on-ink-muted, --ink-divider. The bleached #fffcf7 cream replaces the
warm #e8e0d0 --ink-text wherever it sits on indigo. Legacy --ink-text /
--ink-muted stay in tokens.css for now — if any later commit references
them they remain defined; the migration of existing call sites is
covered here.

Migrated to the new tokens in this pass:
  - src/components/MembershipCard.astro (members/:slug card)
  - src/pages/events.astro (hero invitation card)
  Both render with cleaner whites on indigo as a side effect.

Code updates for the new status enum:
  - db.ts: RoadmapStatus = shipping | in_beta | exploring | considering
  - admin/RoadmapTab.astro: Status select gains Considering + In beta;
    grouped section iteration covers all four
  - admin/index.astro: validation list updated
  - scripts/seed-roadmap.js: 'In progress' markdown bucket → 'in_beta'
  - pulse.astro: roadmapStatusDot + roadmapStatusBlurb temporarily widened
    (full rewrite of that section lands in step 7)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jonathan Hvid 2026-05-12 10:46:39 +02:00
parent cb2efa70f3
commit 9ae8422527
9 changed files with 100 additions and 45 deletions

View file

@ -0,0 +1,40 @@
-- Roadmap status enum gains a fourth value `considering` for items that are
-- under discussion but not yet committed to. Same migration also renames
-- the existing `beta` value to `in_beta` so the canonical names line up
-- with the v4 spec (no second display label layer needed).
--
-- SQLite can't widen a CHECK constraint in place, so this is a full table
-- rebuild. 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.
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','exploring','considering')),
target TEXT,
display_order INTEGER NOT NULL DEFAULT 0,
shipped_at 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, created_at, updated_at)
SELECT
id, title, description,
CASE status WHEN 'beta' THEN 'in_beta' ELSE status END,
target, display_order, shipped_at, 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

@ -30,7 +30,7 @@ const md = readFileSync(mdPath, 'utf8');
// schema's three statuses. In-progress items are actively being built and
// tested with pilots → beta. Next/Later are roadmap intent, not started → exploring.
const SECTION_STATUS = {
'In progress': { status: 'beta', target: null },
'In progress': { status: 'in_beta', target: null },
'Next': { status: 'exploring', target: 'Next quarter' },
'Later': { status: 'exploring', target: 'Later this year' },
};

View file

@ -51,7 +51,7 @@ const tags = readFocusTags(member.focus_tags);
<style>
.m-card {
background: var(--ink);
color: var(--ink-text);
color: var(--on-ink);
border-radius: var(--radius-lg);
padding: 1.25rem;
display: flex;
@ -70,7 +70,7 @@ const tags = readFocusTags(member.focus_tags);
width: 22px;
height: 22px;
border-radius: 50%;
background: var(--ink-text);
background: var(--on-ink);
color: var(--ink);
display: inline-flex;
align-items: center;
@ -89,7 +89,7 @@ const tags = readFocusTags(member.focus_tags);
font-weight: 500;
letter-spacing: var(--tracking-wider);
text-transform: uppercase;
color: var(--ink-muted);
color: var(--on-ink-muted);
}
.m-name {
@ -99,7 +99,7 @@ const tags = readFocusTags(member.focus_tags);
font-weight: 400;
font-size: 1.5rem;
line-height: 1.15;
color: var(--ink-text);
color: var(--on-ink);
display: flex;
flex-direction: column;
}
@ -116,12 +116,12 @@ const tags = readFocusTags(member.focus_tags);
font-size: var(--text-label-sm);
letter-spacing: var(--tracking-wider);
text-transform: uppercase;
color: var(--ink-muted);
color: var(--on-ink-muted);
}
.m-since-value {
font-family: var(--font-sans);
font-size: var(--text-body-sm);
color: var(--ink-text);
color: var(--on-ink);
}
.m-tags {
@ -133,8 +133,8 @@ const tags = readFocusTags(member.focus_tags);
gap: 6px;
}
.m-tag {
border: 0.5px solid rgba(232, 224, 208, 0.3);
color: var(--ink-text);
border: 0.5px solid rgba(255, 252, 247, 0.3);
color: var(--on-ink);
padding: 3px 8px;
border-radius: 999px;
font-family: var(--font-sans);

View file

@ -9,17 +9,23 @@ interface Props {
const { items, editing, cabUsers } = Astro.props;
const STATUS_LABEL = { shipping: 'Shipping', beta: 'Beta', exploring: 'Exploring' } as const;
const STATUS_LABEL = {
shipping: 'Shipping',
in_beta: 'In beta',
exploring: 'Exploring',
considering: 'Considering',
} as const;
const formAction = editing ? 'update_roadmap' : 'create_roadmap';
const attributedSet = new Set((editing?.attributed ?? []).map(a => a.id));
// Group items by status for display
type Status = 'shipping' | 'beta' | 'exploring';
type Status = 'shipping' | 'in_beta' | 'exploring' | 'considering';
const grouped: Record<Status, RoadmapItemWithAttribution[]> = {
shipping: items.filter(i => i.status === 'shipping' ).sort((a,b) => a.display_order - b.display_order),
beta: items.filter(i => i.status === 'beta' ).sort((a,b) => a.display_order - b.display_order),
exploring: items.filter(i => i.status === 'exploring').sort((a,b) => a.display_order - b.display_order),
in_beta: items.filter(i => i.status === 'in_beta' ).sort((a,b) => a.display_order - b.display_order),
exploring: items.filter(i => i.status === 'exploring' ).sort((a,b) => a.display_order - b.display_order),
considering: items.filter(i => i.status === 'considering').sort((a,b) => a.display_order - b.display_order),
};
---
<div class="tab-content">
@ -39,8 +45,9 @@ const grouped: Record<Status, RoadmapItemWithAttribution[]> = {
<div class="field">
<label for="status" class="label-sm field-label">Status</label>
<select id="status" name="status" class="select body-md" required>
<option value="considering" selected={editing?.status === 'considering'}>Considering</option>
<option value="exploring" selected={editing?.status === 'exploring'}>Exploring</option>
<option value="beta" selected={editing?.status === 'beta'}>Beta</option>
<option value="in_beta" selected={editing?.status === 'in_beta'}>In beta</option>
<option value="shipping" selected={editing?.status === 'shipping'}>Shipping</option>
</select>
</div>
@ -78,7 +85,7 @@ const grouped: Record<Status, RoadmapItemWithAttribution[]> = {
</section>
<!-- ── List by status ────────────────────────────────────────── -->
{(['shipping','beta','exploring'] as const).map(status => (
{(['shipping','in_beta','exploring','considering'] as const).map(status => (
<section class="section">
<h2 class="label-sm section-heading">{STATUS_LABEL[status]} · {grouped[status].length}</h2>
{grouped[status].length === 0 ? (

View file

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

View file

@ -202,7 +202,7 @@ if (Astro.request.method === 'POST') {
const displayOrder = Number(data.get('display_order') ?? 0);
const attributedIds = data.getAll('attributed_user_ids').map(v => Number(v)).filter(Boolean);
if (!title || !['shipping','beta','exploring'].includes(status)) {
if (!title || !['shipping','in_beta','exploring','considering'].includes(status)) {
formError = 'Title and status are required.';
} else if (action === 'create_roadmap') {
const id = createRoadmapItem({ title, description, status, target, display_order: displayOrder });

View file

@ -220,7 +220,7 @@ const heroAudience = hero?.audience ?? 'Members only';
/* ── Hero ─────────────────────────────────────────────────────── */
.hero {
background: var(--ink);
color: var(--ink-text);
color: var(--on-ink);
border-radius: var(--radius-lg);
padding: 1.75rem;
display: flex;
@ -232,7 +232,7 @@ const heroAudience = hero?.audience ?? 'Members only';
display: flex;
justify-content: space-between;
align-items: center;
color: var(--ink-muted);
color: var(--on-ink-muted);
}
.hero-eyebrow {
font-family: var(--font-sans);
@ -255,7 +255,7 @@ const heroAudience = hero?.audience ?? 'Members only';
left: 100px;
top: 0; bottom: 0;
width: 0.5px;
background: rgba(232, 224, 208, 0.2);
background: var(--ink-divider);
}
.hero-date { display: flex; flex-direction: column; gap: 2px; }
@ -264,13 +264,13 @@ const heroAudience = hero?.audience ?? 'Members only';
font-size: var(--text-label-sm);
letter-spacing: var(--tracking-wider);
text-transform: uppercase;
color: var(--ink-text);
color: var(--on-ink);
}
.hero-day {
font-family: var(--font-serif);
font-size: 2.75rem;
line-height: 1;
color: var(--ink-text);
color: var(--on-ink);
}
.hero-detail { padding-left: var(--space-5); }
@ -279,22 +279,22 @@ const heroAudience = hero?.audience ?? 'Members only';
font-weight: 400;
font-size: 1.75rem;
line-height: 1.2;
color: var(--ink-text);
color: var(--on-ink);
margin: 0 0 var(--space-3);
}
.hero-desc {
color: rgba(232, 224, 208, 0.85);
color: var(--on-ink-body);
margin: 0 0 var(--space-3);
max-width: 40rem;
}
.hero-meta {
color: var(--ink-muted);
color: var(--on-ink-muted);
font-size: var(--text-body-sm);
margin: 0;
}
.hero-foot {
border-top: 0.5px solid rgba(232, 224, 208, 0.2);
border-top: 0.5px solid var(--ink-divider);
padding-top: var(--space-4);
display: flex;
justify-content: space-between;
@ -304,7 +304,7 @@ const heroAudience = hero?.audience ?? 'Members only';
}
.hero-foot-left { display: flex; align-items: center; gap: var(--space-4); }
.hero-foot-stat {
color: var(--ink-muted);
color: var(--on-ink-muted);
font-family: var(--font-sans);
font-size: var(--text-label-sm);
letter-spacing: var(--tracking-wider);
@ -313,7 +313,7 @@ const heroAudience = hero?.audience ?? 'Members only';
.hero-foot-right { display: flex; align-items: center; gap: var(--space-3); }
.hero-cta {
background: var(--ink-text);
background: var(--on-ink);
color: var(--ink);
border: none;
padding: 10px 20px;
@ -329,20 +329,20 @@ const heroAudience = hero?.audience ?? 'Members only';
.hero-cta:hover { opacity: 0.85; }
.hero-confirmed {
color: var(--ink-text);
color: var(--on-ink);
font-family: var(--font-sans);
font-size: var(--text-label-md);
font-weight: 600;
letter-spacing: var(--tracking-wide);
text-transform: uppercase;
padding: 10px 16px;
border: 0.5px solid rgba(232, 224, 208, 0.4);
border: 0.5px solid rgba(255, 252, 247, 0.4);
border-radius: 999px;
}
.hero-change {
background: transparent;
border: none;
color: var(--ink-muted);
color: var(--on-ink-muted);
font-family: var(--font-sans);
font-size: var(--text-label-sm);
letter-spacing: var(--tracking-wide);
@ -359,7 +359,7 @@ const heroAudience = hero?.audience ?? 'Members only';
display: flex;
}
.hero-empty-line {
color: var(--ink-text);
color: var(--on-ink);
font-family: var(--font-serif);
font-size: 1.25rem;
margin: auto;

View file

@ -98,19 +98,21 @@ const roadmapPreview = getAllRoadmapItems()
.sort((a, b) => (b.updated_at > a.updated_at ? 1 : -1))
.slice(0, 3);
function roadmapStatusDot(status: 'shipping' | 'beta' | 'exploring'): string {
function roadmapStatusDot(status: 'shipping' | 'in_beta' | 'exploring' | 'considering'): string {
return ({
shipping: 'var(--pigment-copper)',
beta: 'var(--pigment-ochre)',
exploring: 'var(--on-surface-muted)',
in_beta: 'var(--pigment-terracotta)',
exploring: '#b4b2a9',
considering: '#d4d2c8',
})[status];
}
function roadmapStatusBlurb(item: { status: 'shipping' | 'beta' | 'exploring'; target: string | null }): string {
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 'beta': return `In beta${target}`;
case 'in_beta': return `In beta${target}`;
case 'exploring': return `Exploring${target}`;
case 'considering': return `Considering${target}`;
}
}

View file

@ -45,8 +45,14 @@
--surface-card: #ffffff;
--surface-card-border: rgba(0, 0, 0, 0.08);
--ink: #2c3a52; /* deep indigo — membership card + event hero */
--ink-text: #e8e0d0; /* readable cream on --ink */
--ink-muted: #b8a989; /* muted label tone on --ink */
--ink-text: #e8e0d0; /* legacy warm cream — superseded by --on-ink */
--ink-muted: #b8a989; /* legacy tan — superseded by --on-ink-muted */
/* --- v4: bleached cream on indigo surfaces (replaces --ink-text) --- */
--on-ink: #fffcf7; /* primary text on --ink */
--on-ink-body: rgba(255, 252, 247, 0.85); /* body copy */
--on-ink-muted: rgba(255, 252, 247, 0.65); /* tracked labels */
--ink-divider: rgba(255, 252, 247, 0.18); /* 0.5px lines on --ink */
/* --- Semantic state mappings --- */
--color-success: var(--pigment-copper);