feat(roadmap): Now/Next/Later view

This commit is contained in:
Dave 2026-09-17 07:17:35 +02:00
parent 6e231374e2
commit bc8cc9f776
2 changed files with 121 additions and 0 deletions

View file

@ -0,0 +1,66 @@
import { RoadmapCard } from "@/components/roadmap/roadmap-card";
import type { Band } from "@/lib/roadmap/band";
import type { RoadmapCard as Card } from "@/lib/roadmap/load";
const BANDS: Array<{ key: Band; title: string; blurb: string }> = [
{ key: "now", title: "Now", blurb: "In flight, critical, or due within a week" },
{ key: "next", title: "Next", blurb: "Queued up" },
{ key: "later", title: "Later", blurb: "Not started" },
];
export function BandView({
cards,
inboxCount,
}: {
cards: Card[];
inboxCount: number;
}) {
return (
<div className="space-y-6">
{inboxCount > 0 && (
// Counted, not placed: an unaccepted request is not committed work,
// but it should not be invisible either.
<p className="text-xs text-text-muted">
<span className="tabular">{inboxCount}</span>{" "}
{inboxCount === 1 ? "request is" : "requests are"} waiting in an Inbox.
</p>
)}
<div className="grid gap-5 lg:grid-cols-3">
{BANDS.map((band) => {
const inBand = cards.filter((card) => card.band === band.key);
return (
<section key={band.key} className="space-y-3">
<div>
<div className="flex items-baseline gap-2">
<h2 className="text-xs font-semibold uppercase tracking-wide text-text-secondary">
{band.title}
</h2>
<span className="tabular rounded-full bg-surface-raised px-1.5 py-0.5 text-[11px] text-text-muted">
{inBand.length}
</span>
</div>
<p className="mt-0.5 text-[11px] text-text-muted">{band.blurb}</p>
</div>
{inBand.length === 0 ? (
<p className="rounded-[var(--radius-tile)] border border-dashed border-border-subtle p-6 text-center text-xs text-text-muted">
Nothing here
</p>
) : (
<ul className="space-y-2">
{inBand.map((card) => (
<li key={card.id}>
<RoadmapCard card={card} />
</li>
))}
</ul>
)}
</section>
);
})}
</div>
</div>
);
}

View file

@ -0,0 +1,55 @@
import Link from "next/link";
import { Pill } from "@/components/ui/pill";
import { classifyDueDate, formatDueDate } from "@/lib/board/dates";
import type { RoadmapCard as Card } from "@/lib/roadmap/load";
const PRIORITY_TONE = {
critical: "critical",
high: "high",
medium: "medium",
low: "low",
} as const;
export function RoadmapCard({ card }: { card: Card }) {
const due = classifyDueDate(card.targetDate);
const dueLabel = formatDueDate(card.targetDate);
return (
<Link
href={card.href}
className="block rounded-[var(--radius-tile)] border border-border-subtle bg-surface p-3 transition-colors hover:border-border-strong"
>
<div className="flex items-start gap-2.5">
{/* The client's own colour, so a cross-client page is scannable. */}
<span
aria-hidden
className="mt-1 h-2.5 w-2.5 shrink-0 rounded-full"
style={{ background: card.clientColour }}
/>
<div className="min-w-0 flex-1">
<p className="text-[13px] font-medium leading-[1.45] text-text-primary">
{card.title}
</p>
<p className="mt-0.5 truncate text-[11px] text-text-muted">
{card.clientName} · {card.projectName}
</p>
</div>
{card.percent !== null && (
<span className="tabular shrink-0 text-[11px] text-text-muted">
{card.percent}%
</span>
)}
</div>
<div className="mt-2.5 flex flex-wrap items-center gap-1.5">
<Pill tone={PRIORITY_TONE[card.priority]}>{card.priority}</Pill>
{dueLabel && (
<Pill tone={due === "overdue" ? "accent" : "neutral"}>
<span className="tabular">{dueLabel}</span>
</Pill>
)}
</div>
</Link>
);
}