diff --git a/apps/apollo-vertex/app/_meta.ts b/apps/apollo-vertex/app/_meta.ts
index 152665709..d737f8f11 100644
--- a/apps/apollo-vertex/app/_meta.ts
+++ b/apps/apollo-vertex/app/_meta.ts
@@ -1,5 +1,6 @@
export default {
index: "Introduction",
+ "design-system-status": "Roadmap status",
foundation: "Foundation",
components: "Components",
patterns: "Patterns",
diff --git a/apps/apollo-vertex/app/api/jira/route.ts b/apps/apollo-vertex/app/api/jira/route.ts
new file mode 100644
index 000000000..0d43db67f
--- /dev/null
+++ b/apps/apollo-vertex/app/api/jira/route.ts
@@ -0,0 +1,17 @@
+import { type NextRequest, NextResponse } from 'next/server';
+import { fetchJiraIssues } from '@/lib/jira';
+import { processIssues } from '@/lib/jira-resolve';
+
+export async function GET(_req: NextRequest) {
+ try {
+ const issues = await fetchJiraIssues();
+ const jiraBaseUrl = process.env.JIRA_BASE_URL ?? 'https://uipath.atlassian.net';
+ const data = processIssues(issues, jiraBaseUrl);
+ return NextResponse.json(data, {
+ headers: { 'Cache-Control': 'no-store' },
+ });
+ } catch (e) {
+ const message = e instanceof Error ? e.message : String(e);
+ return NextResponse.json({ error: message }, { status: 500 });
+ }
+}
diff --git a/apps/apollo-vertex/app/design-system-status/_components/status-board.tsx b/apps/apollo-vertex/app/design-system-status/_components/status-board.tsx
new file mode 100644
index 000000000..1574fbc8a
--- /dev/null
+++ b/apps/apollo-vertex/app/design-system-status/_components/status-board.tsx
@@ -0,0 +1,235 @@
+import Link from "next/link";
+import {
+ ArrowUpRight,
+ CheckCircle2,
+ Clock,
+ Inbox,
+ TriangleAlert,
+} from "lucide-react";
+import { Badge } from "@/registry/badge/badge";
+import { fetchJiraIssues } from "@/lib/jira";
+import {
+ processIssues,
+ type BadgeLabel,
+ type BoardData,
+ type ProcessedCard,
+} from "@/lib/jira-resolve";
+
+// ─── status tag ──────────────────────────────────────────────────────────────
+
+function StatusTag({ status }: { status: string }) {
+ const s = status.toLowerCase();
+
+ if (s === "in review" || s === "review") {
+ return (
+
+ In Review
+
+ );
+ }
+ if (s === "in progress") {
+ return In Progress;
+ }
+ if (s === "closed" || s === "done") {
+ return (
+
+ Delivered
+
+ );
+ }
+ return {status};
+}
+
+// ─── label badge ─────────────────────────────────────────────────────────────
+
+function LegalBadge({ label }: { label: BadgeLabel }) {
+ if (label === "required") {
+ return (
+
+ Required
+
+ );
+ }
+ if (label === "best-practice") {
+ return (
+
+ Best practice
+
+ );
+ }
+ return null;
+}
+
+// ─── card ────────────────────────────────────────────────────────────────────
+
+function StatusCard({ card }: { card: ProcessedCard }) {
+ const isExternal = card.link.startsWith("http");
+
+ return (
+
+
+
+
+ {card.badge && }
+
+
+
+
+
+ {card.summary}
+
+
+
+ {card.epicName && (
+
{card.epicName}
+ )}
+
{card.key}
+
+
+ );
+}
+
+// ─── section ─────────────────────────────────────────────────────────────────
+
+function Section({
+ title,
+ description,
+ icon,
+ cards,
+ empty,
+}: {
+ title: string;
+ description: string;
+ icon: React.ReactNode;
+ cards: ProcessedCard[];
+ empty: string;
+}) {
+ return (
+
+
+ {icon}
+
+
{title}
+
{description}
+
+
+ {cards.length}
+
+
+
+ {cards.length === 0 ? (
+
+ {empty}
+
+ ) : (
+
+ {cards.map((card) => (
+
+ ))}
+
+ )}
+
+ );
+}
+
+// ─── error state ─────────────────────────────────────────────────────────────
+
+function SetupError({ message }: { message: string }) {
+ const isMissingConfig = message.includes("Missing Jira configuration");
+ return (
+
+
+
+
+ {isMissingConfig
+ ? "Jira credentials not configured"
+ : "Could not load Jira data"}
+
+
+ {isMissingConfig ? (
+
+
+ Add{" "}
+
+ JIRA_BASE_URL
+
+ ,{" "}
+
+ JIRA_EMAIL
+
+ , and{" "}
+
+ JIRA_API_TOKEN
+ {" "}
+ to{" "}
+
+ apps/apollo-vertex/.env.local
+
+ , then restart the dev server.
+
+
+ ) : (
+
{message}
+ )}
+
+ );
+}
+
+// ─── board ───────────────────────────────────────────────────────────────────
+
+export async function StatusBoard() {
+ let data: BoardData | null = null;
+ let error: string | null = null;
+
+ try {
+ const issues = await fetchJiraIssues();
+ const jiraBaseUrl =
+ process.env.JIRA_BASE_URL ?? "https://uipath.atlassian.net";
+ data = processIssues(issues, jiraBaseUrl);
+ } catch (e) {
+ error = e instanceof Error ? e.message : String(e);
+ }
+
+ if (error) {
+ return (
+
+
+
+ );
+ }
+
+ if (!data) return null;
+
+ return (
+
+ }
+ cards={data.delivered}
+ empty="Nothing shipped in the last 30 days."
+ />
+
+ }
+ cards={data.comingSoon}
+ empty="Nothing in progress right now."
+ />
+
+ }
+ cards={data.backlog}
+ empty="Backlog is empty."
+ />
+
+ );
+}
diff --git a/apps/apollo-vertex/app/design-system-status/page.mdx b/apps/apollo-vertex/app/design-system-status/page.mdx
new file mode 100644
index 000000000..5e076aec2
--- /dev/null
+++ b/apps/apollo-vertex/app/design-system-status/page.mdx
@@ -0,0 +1,11 @@
+---
+title: Roadmap status
+---
+
+import { StatusBoard } from './_components/status-board'
+
+# Roadmap status
+
+Live view of design system work for developers building on Vertex. Jira is the single source of truth. [View the VS Horizontal UX board →](https://uipath.atlassian.net/jira/software/projects/DESIGN/boards)
+
+
diff --git a/apps/apollo-vertex/lib/jira-resolve.ts b/apps/apollo-vertex/lib/jira-resolve.ts
new file mode 100644
index 000000000..610f7ea8c
--- /dev/null
+++ b/apps/apollo-vertex/lib/jira-resolve.ts
@@ -0,0 +1,225 @@
+import { type JiraIssue, extractAdfText } from "./jira";
+
+// All known routable slugs per section, derived from the site's _meta.ts files
+const SLUGS_BY_SECTION: Record = {
+ components: [
+ "accordion",
+ "alert",
+ "alert-dialog",
+ "aspect-ratio",
+ "avatar",
+ "badge",
+ "breadcrumb",
+ "button",
+ "button-group",
+ "calendar",
+ "card",
+ "carousel",
+ "chart",
+ "line-chart",
+ "multi-line-chart",
+ "bar-chart",
+ "distribution-chart",
+ "kpi-chart",
+ "table-chart",
+ "checkbox",
+ "collapsible",
+ "combobox",
+ "command",
+ "context-menu",
+ "data-table",
+ "date-picker",
+ "dialog",
+ "drawer",
+ "dropdown-menu",
+ "empty",
+ "field",
+ "feature-flags",
+ "filter-dropdown",
+ "form",
+ "form-wizard",
+ "hover-card",
+ "input",
+ "input-group",
+ "input-otp",
+ "item",
+ "kbd",
+ "label",
+ "menubar",
+ "navigation-menu",
+ "pagination",
+ "popover",
+ "progress",
+ "radio-group",
+ "resizable",
+ "scroll-area",
+ "select",
+ "separator",
+ "sheet",
+ "sidebar",
+ "skeleton",
+ "slider",
+ "sonner",
+ "spinner",
+ "switch",
+ "table",
+ "tabs",
+ "textarea",
+ "toggle",
+ "toggle-group",
+ "tooltip",
+ ],
+ patterns: [
+ "ai-chat",
+ "feedback-vote-widget",
+ "metric-card",
+ "page-header",
+ "shell",
+ ],
+ templates: ["list-page", "settings", "solution-tests"],
+ guidelines: ["ai-toolkit", "notifications"],
+ foundation: ["colors", "spacing", "grid", "typography", "icons", "logos"],
+};
+
+// Flat map: slug → absolute path
+const SLUG_PATH_MAP = new Map();
+for (const [section, slugs] of Object.entries(SLUGS_BY_SECTION)) {
+ for (const slug of slugs) {
+ SLUG_PATH_MAP.set(slug, `/${section}/${slug}`);
+ }
+}
+
+export type LinkSource = "explicit" | "convention" | "jira";
+export type Section = "delivered" | "coming-soon" | "backlog";
+export type BadgeLabel = "required" | "best-practice" | null;
+
+export interface ProcessedCard {
+ key: string;
+ summary: string;
+ status: string;
+ section: Section;
+ badge: BadgeLabel;
+ link: string;
+ linkSource: LinkSource;
+ jiraUrl: string;
+ updated: string;
+ epicName: string | null;
+ epicKey: string | null;
+}
+
+export interface BoardData {
+ delivered: ProcessedCard[];
+ comingSoon: ProcessedCard[];
+ backlog: ProcessedCard[];
+ linkStats: { explicit: number; convention: number; jiraFallback: number };
+}
+
+function toSection(statusName: string): Section {
+ const s = statusName.toLowerCase();
+ if (s === "closed" || s === "done") return "delivered";
+ if (s === "in progress" || s === "in review" || s === "review")
+ return "coming-soon";
+ return "backlog";
+}
+
+function toBadge(labels: string[]): BadgeLabel {
+ if (labels.includes("ai-legal-required")) return "required";
+ if (labels.includes("ai-legal-best-practice")) return "best-practice";
+ return null;
+}
+
+function resolveDeliveredLink(
+ issue: JiraIssue,
+ jiraUrl: string,
+): { url: string; source: LinkSource } {
+ const descText = extractAdfText(issue.fields.description);
+
+ // Priority 1: explicit "Vertex: https://..." or "Vertex URL: https://..." in description
+ const vertexMatch = descText.match(
+ /Vertex(?:\s+URL)?:\s*(https?:\/\/[^\s\n)]+)/i,
+ );
+ if (vertexMatch) {
+ return { url: vertexMatch[1].trim(), source: "explicit" };
+ }
+
+ // Priority 2: "Component: " convention — slugify and look up known paths
+ const componentMatch = descText.match(/^Component:\s*(.+)$/im);
+ if (componentMatch) {
+ const rawSlug = componentMatch[1].match(/^([^\s(,]+)/)?.[1];
+ if (rawSlug) {
+ const slug = rawSlug
+ .toLowerCase()
+ .replace(/[^a-z0-9]+/g, "-")
+ .replace(/(^-|-$)/g, "");
+ const path = SLUG_PATH_MAP.get(slug);
+ if (path) return { url: path, source: "convention" };
+ }
+ }
+
+ // Priority 3: fall back to Jira ticket
+ return { url: jiraUrl, source: "jira" };
+}
+
+export function processIssues(
+ issues: JiraIssue[],
+ jiraBaseUrl: string,
+): BoardData {
+ const delivered: ProcessedCard[] = [];
+ const comingSoon: ProcessedCard[] = [];
+ const backlog: ProcessedCard[] = [];
+ const linkStats = { explicit: 0, convention: 0, jiraFallback: 0 };
+
+ for (const issue of issues) {
+ const statusName = issue.fields.status.name;
+ const section = toSection(statusName);
+ const jiraUrl = `${jiraBaseUrl}/browse/${issue.key}`;
+
+ let link: string;
+ let linkSource: LinkSource;
+
+ if (section === "delivered") {
+ const resolved = resolveDeliveredLink(issue, jiraUrl);
+ link = resolved.url;
+ linkSource = resolved.source;
+ if (linkSource === "explicit") linkStats.explicit++;
+ else if (linkSource === "convention") linkStats.convention++;
+ else linkStats.jiraFallback++;
+ } else {
+ link = jiraUrl;
+ linkSource = "jira";
+ }
+
+ const parentIsEpic =
+ issue.fields.parent?.fields.issuetype.name === "Epic" ||
+ issue.fields.parent?.fields.issuetype.hierarchyLevel === 1;
+
+ const card: ProcessedCard = {
+ key: issue.key,
+ summary: issue.fields.summary,
+ status: statusName,
+ section,
+ badge: toBadge(issue.fields.labels),
+ link,
+ linkSource,
+ jiraUrl,
+ updated: issue.fields.updated,
+ epicName: parentIsEpic
+ ? (issue.fields.parent?.fields.summary ?? null)
+ : null,
+ epicKey: parentIsEpic ? (issue.fields.parent?.key ?? null) : null,
+ };
+
+ if (section === "delivered") {
+ delivered.push(card);
+ } else if (section === "coming-soon") {
+ // Review/In Review sorts before In Progress — closer to landing
+ const sl = statusName.toLowerCase();
+ if (sl === "in review" || sl === "review") comingSoon.unshift(card);
+ else comingSoon.push(card);
+ } else {
+ backlog.push(card);
+ }
+ }
+
+ return { delivered, comingSoon, backlog, linkStats };
+}
diff --git a/apps/apollo-vertex/lib/jira.ts b/apps/apollo-vertex/lib/jira.ts
new file mode 100644
index 000000000..58264f9e9
--- /dev/null
+++ b/apps/apollo-vertex/lib/jira.ts
@@ -0,0 +1,103 @@
+const JQL =
+ 'project = DESIGN AND labels = "horizontal-ux" AND status != "On hold" AND (statusCategory != Done OR labels = "ds-delivered") ORDER BY updated DESC';
+
+const FIELDS = [
+ "summary",
+ "status",
+ "labels",
+ "updated",
+ "resolutiondate",
+ "description",
+ "parent",
+];
+
+interface AdfNode {
+ type: string;
+ text?: string;
+ content?: AdfNode[];
+ attrs?: Record;
+}
+
+export interface JiraIssue {
+ key: string;
+ fields: {
+ summary: string;
+ status: { name: string };
+ labels: string[];
+ updated: string;
+ resolutiondate: string | null;
+ description: AdfNode | null;
+ parent?: {
+ key: string;
+ fields: {
+ summary: string;
+ issuetype: { name: string; hierarchyLevel: number };
+ };
+ } | null;
+ };
+}
+
+export function extractAdfText(node: AdfNode | null | undefined): string {
+ if (!node) return "";
+ if (node.type === "text") return node.text ?? "";
+ if (node.type === "inlineCard") return node.attrs?.url ?? "";
+ if (!node.content) return "";
+ const parts = node.content.map(extractAdfText);
+ const isBlock =
+ node.type === "paragraph" ||
+ node.type === "heading" ||
+ node.type === "listItem" ||
+ node.type === "bulletList" ||
+ node.type === "orderedList";
+ return isBlock ? `${parts.join("")}\n` : parts.join("");
+}
+
+export async function fetchJiraIssues(): Promise {
+ const base = process.env.JIRA_BASE_URL;
+ const email = process.env.JIRA_EMAIL;
+ const token = process.env.JIRA_API_TOKEN;
+
+ if (!base || !email || !token) {
+ throw new Error(
+ "Missing Jira configuration. Add JIRA_BASE_URL, JIRA_EMAIL, and JIRA_API_TOKEN to .env.local",
+ );
+ }
+
+ const auth = Buffer.from(`${email}:${token}`).toString("base64");
+ const issues: JiraIssue[] = [];
+ let nextPageToken: string | undefined;
+
+ do {
+ const body: Record = {
+ jql: JQL,
+ fields: FIELDS,
+ maxResults: 50,
+ };
+ if (nextPageToken) body["nextPageToken"] = nextPageToken;
+
+ const res = await fetch(`${base}/rest/api/3/search/jql`, {
+ method: "POST",
+ headers: {
+ Authorization: `Basic ${auth}`,
+ "Content-Type": "application/json",
+ Accept: "application/json",
+ },
+ body: JSON.stringify(body),
+ cache: "no-store",
+ });
+
+ if (!res.ok) {
+ const text = await res.text();
+ throw new Error(`Jira API error ${res.status}: ${text}`);
+ }
+
+ const data = (await res.json()) as {
+ issues?: JiraIssue[];
+ nextPageToken?: string;
+ };
+ issues.push(...(data.issues ?? []));
+ nextPageToken = data.nextPageToken;
+ } while (nextPageToken);
+
+ return issues;
+}