diff --git a/apps/apollo-vertex/app/globals.css b/apps/apollo-vertex/app/globals.css
index 646f02405..b6ebbc8fb 100644
--- a/apps/apollo-vertex/app/globals.css
+++ b/apps/apollo-vertex/app/globals.css
@@ -1,4 +1,6 @@
@import 'tailwindcss';
+@source "../registry";
+@source "../templates";
/* Optional: import Nextra theme styles */
@import 'nextra-theme-docs/style.css';
diff --git a/apps/apollo-vertex/app/preview/_meta.ts b/apps/apollo-vertex/app/preview/_meta.ts
new file mode 100644
index 000000000..e48eafb8a
--- /dev/null
+++ b/apps/apollo-vertex/app/preview/_meta.ts
@@ -0,0 +1,3 @@
+export default {
+ "*": { display: "hidden" },
+};
diff --git a/apps/apollo-vertex/app/preview/dashboard-minimal/page.tsx b/apps/apollo-vertex/app/preview/dashboard-minimal/page.tsx
new file mode 100644
index 000000000..deba5cb66
--- /dev/null
+++ b/apps/apollo-vertex/app/preview/dashboard-minimal/page.tsx
@@ -0,0 +1,19 @@
+"use client";
+
+import dynamic from "next/dynamic";
+
+const DashboardTemplate = dynamic(
+ () =>
+ import("@/templates/dashboard/DashboardTemplate").then(
+ (mod) => mod.DashboardTemplate,
+ ),
+ { ssr: false },
+);
+
+export default function DashboardMinimalPreviewPage() {
+ return (
+
+
+
+ );
+}
diff --git a/apps/apollo-vertex/app/preview/dashboard/page.tsx b/apps/apollo-vertex/app/preview/dashboard/page.tsx
new file mode 100644
index 000000000..018db5cc2
--- /dev/null
+++ b/apps/apollo-vertex/app/preview/dashboard/page.tsx
@@ -0,0 +1,19 @@
+"use client";
+
+import dynamic from "next/dynamic";
+
+const DashboardTemplate = dynamic(
+ () =>
+ import("@/templates/dashboard/DashboardTemplate").then(
+ (mod) => mod.DashboardTemplate,
+ ),
+ { ssr: false },
+);
+
+export default function DashboardPreviewPage() {
+ return (
+
+
+
+ );
+}
diff --git a/apps/apollo-vertex/public/Autopilot_dark.svg b/apps/apollo-vertex/public/Autopilot_dark.svg
new file mode 100644
index 000000000..5f35c08dc
--- /dev/null
+++ b/apps/apollo-vertex/public/Autopilot_dark.svg
@@ -0,0 +1,6 @@
+
diff --git a/apps/apollo-vertex/public/Autopilot_light.svg b/apps/apollo-vertex/public/Autopilot_light.svg
new file mode 100644
index 000000000..4f28c9a53
--- /dev/null
+++ b/apps/apollo-vertex/public/Autopilot_light.svg
@@ -0,0 +1,6 @@
+
diff --git a/apps/apollo-vertex/registry.json b/apps/apollo-vertex/registry.json
index aa715781d..ac8939b6a 100644
--- a/apps/apollo-vertex/registry.json
+++ b/apps/apollo-vertex/registry.json
@@ -66,6 +66,16 @@
"color-sidebar-accent-foreground": "var(--sidebar-accent-foreground)",
"color-sidebar-border": "var(--sidebar-border)",
"color-sidebar-ring": "var(--sidebar-ring)",
+ "color-insight-50": "var(--insight-50)",
+ "color-insight-100": "var(--insight-100)",
+ "color-insight-200": "var(--insight-200)",
+ "color-insight-300": "var(--insight-300)",
+ "color-insight-400": "var(--insight-400)",
+ "color-insight-500": "var(--insight-500)",
+ "color-insight-600": "var(--insight-600)",
+ "color-insight-700": "var(--insight-700)",
+ "color-insight-800": "var(--insight-800)",
+ "color-insight-900": "var(--insight-900)",
"font-sans": "var(--font-sans)"
},
"light": {
@@ -120,6 +130,19 @@
"sidebar-accent-foreground": "oklch(0.1660 0.0283 203.3380)",
"sidebar-border": "oklch(0.9237 0.0133 262.3780)",
"sidebar-ring": "oklch(0.64 0.115 208)",
+ "insight-50": "oklch(0.96 0.03 277)",
+ "insight-100": "oklch(0.92 0.05 277)",
+ "insight-200": "oklch(0.86 0.09 277)",
+ "insight-300": "oklch(0.78 0.14 277)",
+ "insight-400": "oklch(0.70 0.19 277)",
+ "insight-500": "oklch(0.62 0.22 277)",
+ "insight-600": "oklch(0.56 0.20 277)",
+ "insight-700": "oklch(0.48 0.17 277)",
+ "insight-800": "oklch(0.38 0.13 278)",
+ "insight-900": "oklch(0.30 0.10 278)",
+ "font-sans": "Inter, ui-sans-serif, sans-serif, system-ui",
+ "font-serif": "IBM Plex Serif, ui-serif, serif",
+ "font-mono": "IBM Plex Mono, ui-monospace, monospace",
"radius": "0.625rem",
"shadow-x": "0",
"shadow-y": "0px",
@@ -206,6 +229,19 @@
"sidebar-accent-foreground": "oklch(0.9525 0.0110 225.9830)",
"sidebar-border": "oklch(0.9525 0.0110 225.9830)",
"sidebar-ring": "oklch(0.69 0.112 207)",
+ "insight-50": "oklch(0.96 0.03 277)",
+ "insight-100": "oklch(0.92 0.05 277)",
+ "insight-200": "oklch(0.86 0.09 277)",
+ "insight-300": "oklch(0.78 0.14 277)",
+ "insight-400": "oklch(0.70 0.19 277)",
+ "insight-500": "oklch(0.62 0.22 277)",
+ "insight-600": "oklch(0.56 0.20 277)",
+ "insight-700": "oklch(0.48 0.17 277)",
+ "insight-800": "oklch(0.38 0.13 278)",
+ "insight-900": "oklch(0.30 0.10 278)",
+ "font-sans": "Inter, ui-sans-serif, sans-serif, system-ui",
+ "font-serif": "IBM Plex Serif, ui-serif, serif",
+ "font-mono": "IBM Plex Mono, ui-monospace, monospace",
"radius": "0.625rem",
"shadow-x": "0",
"shadow-y": "0px",
diff --git a/apps/apollo-vertex/registry/card/card.tsx b/apps/apollo-vertex/registry/card/card.tsx
index 7bd13d2e1..f61a8ae2d 100644
--- a/apps/apollo-vertex/registry/card/card.tsx
+++ b/apps/apollo-vertex/registry/card/card.tsx
@@ -10,7 +10,7 @@ export const GLASS_CLASSES = [
"dark:shadow-[0_2px_24px_2px_rgba(0,0,0,0.12),inset_0_1px_0_0_color-mix(in_srgb,var(--sidebar)_5%,transparent)]",
] as const;
-const cardVariants = cva("flex flex-col text-card-foreground", {
+const cardVariants = cva("flex flex-col gap-6 py-6 text-card-foreground", {
variants: {
variant: {
default: GLASS_CLASSES,
diff --git a/apps/apollo-vertex/templates/dashboard/DashboardContent.tsx b/apps/apollo-vertex/templates/dashboard/DashboardContent.tsx
new file mode 100644
index 000000000..70dd812bd
--- /dev/null
+++ b/apps/apollo-vertex/templates/dashboard/DashboardContent.tsx
@@ -0,0 +1,6 @@
+"use client";
+
+// Stub — full implementation is added in a later PR once all dependencies are available.
+export function DashboardContent() {
+ return null;
+}
diff --git a/apps/apollo-vertex/templates/dashboard/DashboardDataProvider.tsx b/apps/apollo-vertex/templates/dashboard/DashboardDataProvider.tsx
new file mode 100644
index 000000000..c29b620f7
--- /dev/null
+++ b/apps/apollo-vertex/templates/dashboard/DashboardDataProvider.tsx
@@ -0,0 +1,15 @@
+"use client";
+
+import { useState, type ReactNode } from "react";
+import { ecommerceDataset, type DashboardDataset } from "./dashboard-data";
+import { DashboardDataContext } from "./dashboard-data-context";
+
+export function DashboardDataProvider({ children }: { children: ReactNode }) {
+ const [data, setData] = useState(ecommerceDataset);
+
+ return (
+
+ {children}
+
+ );
+}
diff --git a/apps/apollo-vertex/templates/dashboard/DashboardLoading.tsx b/apps/apollo-vertex/templates/dashboard/DashboardLoading.tsx
new file mode 100644
index 000000000..c023f29d8
--- /dev/null
+++ b/apps/apollo-vertex/templates/dashboard/DashboardLoading.tsx
@@ -0,0 +1,135 @@
+"use client";
+
+import { useEffect, useState } from "react";
+
+type Phase = "logo" | "skeleton" | "done";
+
+interface DashboardLoadingProps {
+ children: React.ReactNode;
+ triggerReplay?: number;
+}
+
+function LogoPhase({ exiting }: { exiting: boolean }) {
+ return (
+
+ {/* Morphing glow */}
+
+
+
+ {/* App icon */}
+
+

+
+
+ {/* Loading text */}
+
+ Creating your overview...
+
+
+
+
+ );
+}
+
+function SkeletonPhase({ exiting }: { exiting: boolean }) {
+ return (
+
+ );
+}
+
+export function DashboardLoading({
+ children,
+ triggerReplay,
+}: DashboardLoadingProps) {
+ const [phase, setPhase] = useState("done");
+ const [exiting, setExiting] = useState(false);
+
+ useEffect(() => {
+ if (triggerReplay === 0) return;
+ if (triggerReplay) {
+ setExiting(false);
+ setPhase("logo");
+ }
+ }, [triggerReplay]);
+
+ useEffect(() => {
+ if (phase === "done") return;
+
+ if (phase === "logo") {
+ const timer = setTimeout(() => {
+ setExiting(true);
+ setTimeout(() => {
+ setExiting(false);
+ setPhase("skeleton");
+ }, 500);
+ }, 2000);
+ return () => clearTimeout(timer);
+ }
+
+ if (phase === "skeleton") {
+ const timer = setTimeout(() => {
+ setExiting(true);
+ setTimeout(() => {
+ setPhase("done");
+ }, 500);
+ }, 1000);
+ return () => clearTimeout(timer);
+ }
+ }, [phase]);
+
+ if (phase === "done") {
+ return (
+ {children}
+ );
+ }
+
+ return (
+
+ {phase === "logo" && }
+ {phase === "skeleton" && }
+
+ );
+}
diff --git a/apps/apollo-vertex/templates/dashboard/DashboardRoutes.tsx b/apps/apollo-vertex/templates/dashboard/DashboardRoutes.tsx
new file mode 100644
index 000000000..30965402e
--- /dev/null
+++ b/apps/apollo-vertex/templates/dashboard/DashboardRoutes.tsx
@@ -0,0 +1,59 @@
+import { createRootRoute, createRoute, Outlet } from "@tanstack/react-router";
+import { DashboardContent } from "./DashboardContent";
+import { DashboardShellWrapper } from "./DashboardShellWrapper";
+
+export const dashboardRootRoute = createRootRoute();
+
+// --- Sidebar variant routes ---
+
+export const dashboardShellRoute = createRoute({
+ getParentRoute: () => dashboardRootRoute,
+ path: "/preview/dashboard",
+ component: () => (
+
+
+
+ ),
+});
+
+export const dashboardIndexRoute = createRoute({
+ getParentRoute: () => dashboardShellRoute,
+ path: "/",
+ component: DashboardContent,
+});
+
+export const dashboardHomeRoute = createRoute({
+ getParentRoute: () => dashboardShellRoute,
+ path: "/home",
+ component: DashboardContent,
+});
+
+export const dashboardCatchAllRoute = createRoute({
+ getParentRoute: () => dashboardShellRoute,
+ path: "$",
+ component: DashboardContent,
+});
+
+// --- Minimal variant routes ---
+
+export const dashboardMinimalShellRoute = createRoute({
+ getParentRoute: () => dashboardRootRoute,
+ path: "/preview/dashboard-minimal",
+ component: () => (
+
+
+
+ ),
+});
+
+export const dashboardMinimalIndexRoute = createRoute({
+ getParentRoute: () => dashboardMinimalShellRoute,
+ path: "/",
+ component: DashboardContent,
+});
+
+export const dashboardMinimalCatchAllRoute = createRoute({
+ getParentRoute: () => dashboardMinimalShellRoute,
+ path: "$",
+ component: DashboardContent,
+});
diff --git a/apps/apollo-vertex/templates/dashboard/DashboardShellWrapper.tsx b/apps/apollo-vertex/templates/dashboard/DashboardShellWrapper.tsx
new file mode 100644
index 000000000..95b838514
--- /dev/null
+++ b/apps/apollo-vertex/templates/dashboard/DashboardShellWrapper.tsx
@@ -0,0 +1,53 @@
+import type { ReactNode } from "react";
+import type { ShellNavItem } from "@/registry/shell/shell";
+import { ApolloShell } from "@/registry/shell/shell";
+import { SidebarProvider } from "@/components/ui/sidebar";
+import { BarChart3, FolderOpen, Home, Settings, Users } from "lucide-react";
+
+const sidebarNavItems: ShellNavItem[] = [
+ { path: "/preview/dashboard/home", label: "dashboard", icon: Home },
+ { path: "/preview/dashboard/projects", label: "projects", icon: FolderOpen },
+ { path: "/preview/dashboard/analytics", label: "analytics", icon: BarChart3 },
+ { path: "/preview/dashboard/team", label: "team", icon: Users },
+ { path: "/preview/dashboard/settings", label: "settings", icon: Settings },
+];
+
+const minimalNavItems: ShellNavItem[] = [
+ { path: "/preview/dashboard-minimal", label: "dashboard", icon: Home },
+ {
+ path: "/preview/dashboard-minimal/projects",
+ label: "projects",
+ icon: FolderOpen,
+ },
+ {
+ path: "/preview/dashboard-minimal/analytics",
+ label: "analytics",
+ icon: BarChart3,
+ },
+];
+
+export function DashboardShellWrapper({
+ variant,
+ children,
+}: {
+ variant?: "minimal";
+ children: ReactNode;
+}) {
+ return (
+
+
+ {children}
+
+
+ );
+}
diff --git a/apps/apollo-vertex/templates/dashboard/DashboardTemplate.tsx b/apps/apollo-vertex/templates/dashboard/DashboardTemplate.tsx
new file mode 100644
index 000000000..5e2d6ae00
--- /dev/null
+++ b/apps/apollo-vertex/templates/dashboard/DashboardTemplate.tsx
@@ -0,0 +1,88 @@
+"use client";
+
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+import {
+ createMemoryHistory,
+ createRouter,
+ RouterProvider,
+} from "@tanstack/react-router";
+import { useEffect, useState } from "react";
+import {
+ dashboardCatchAllRoute,
+ dashboardHomeRoute,
+ dashboardIndexRoute,
+ dashboardMinimalCatchAllRoute,
+ dashboardMinimalIndexRoute,
+ dashboardMinimalShellRoute,
+ dashboardRootRoute,
+ dashboardShellRoute,
+} from "./DashboardRoutes";
+
+export interface DashboardTemplateProps {
+ shellVariant?: "minimal";
+}
+
+const DASHBOARD_PREVIEW_PATH_KEY = "dashboard-preview-path";
+const DASHBOARD_MINIMAL_PREVIEW_PATH_KEY = "dashboard-minimal-preview-path";
+
+type DashboardPreviewPathKey =
+ | typeof DASHBOARD_PREVIEW_PATH_KEY
+ | typeof DASHBOARD_MINIMAL_PREVIEW_PATH_KEY;
+
+const queryClient = new QueryClient();
+
+const routeTree = dashboardRootRoute.addChildren([
+ dashboardShellRoute.addChildren([
+ dashboardIndexRoute,
+ dashboardHomeRoute,
+ dashboardCatchAllRoute,
+ ]),
+ dashboardMinimalShellRoute.addChildren([
+ dashboardMinimalIndexRoute,
+ dashboardMinimalCatchAllRoute,
+ ]),
+]);
+
+function getInitialEntry(
+ storageKey: DashboardPreviewPathKey,
+ variant?: "minimal",
+) {
+ const stored = localStorage.getItem(storageKey);
+ if (stored) return stored;
+ return variant === "minimal"
+ ? "/preview/dashboard-minimal"
+ : "/preview/dashboard";
+}
+
+function createDashboardRouter(
+ storageKey: DashboardPreviewPathKey,
+ variant?: "minimal",
+) {
+ const history = createMemoryHistory({
+ initialEntries: [getInitialEntry(storageKey, variant)],
+ });
+ return createRouter({ routeTree, history });
+}
+
+export function DashboardTemplate({ shellVariant }: DashboardTemplateProps) {
+ const storageKey =
+ shellVariant === "minimal"
+ ? DASHBOARD_MINIMAL_PREVIEW_PATH_KEY
+ : DASHBOARD_PREVIEW_PATH_KEY;
+ const [router] = useState(() =>
+ createDashboardRouter(storageKey, shellVariant),
+ );
+
+ useEffect(() => {
+ const unsubscribe = router.subscribe("onResolved", ({ toLocation }) => {
+ localStorage.setItem(storageKey, toLocation.pathname);
+ });
+ return unsubscribe;
+ }, [router, storageKey]);
+
+ return (
+
+
+
+ );
+}
diff --git a/apps/apollo-vertex/templates/dashboard/DashboardTemplateDynamic.tsx b/apps/apollo-vertex/templates/dashboard/DashboardTemplateDynamic.tsx
new file mode 100644
index 000000000..8e522eaa5
--- /dev/null
+++ b/apps/apollo-vertex/templates/dashboard/DashboardTemplateDynamic.tsx
@@ -0,0 +1,16 @@
+"use client";
+
+import dynamic from "next/dynamic";
+import type React from "react";
+
+type DashboardTemplateProps = React.ComponentProps<
+ typeof import("./DashboardTemplate").DashboardTemplate
+>;
+
+export const DashboardTemplate = dynamic(
+ () =>
+ import("./DashboardTemplate").then((mod) => ({
+ default: mod.DashboardTemplate,
+ })),
+ { ssr: false },
+);
diff --git a/apps/apollo-vertex/templates/dashboard/ExpandedInsightContent.tsx b/apps/apollo-vertex/templates/dashboard/ExpandedInsightContent.tsx
new file mode 100644
index 000000000..71f1fd239
--- /dev/null
+++ b/apps/apollo-vertex/templates/dashboard/ExpandedInsightContent.tsx
@@ -0,0 +1,326 @@
+"use client";
+
+import { useState } from "react";
+import { Badge } from "@/components/ui/badge";
+import type { DrilldownTab } from "./drilldown-tabs";
+
+// --- Sample data ---
+
+const trendData = {
+ weeks: ["W1", "W2", "W3", "W4", "W5", "W6", "W7", "W8"],
+ series: [
+ {
+ label: "Wrong size/fit",
+ color: "bg-chart-1",
+ stroke: "stroke-chart-1",
+ values: [31, 33, 34, 35, 36, 37, 39, 41],
+ },
+ {
+ label: "Damaged in transit",
+ color: "bg-chart-2",
+ stroke: "stroke-chart-2",
+ values: [25, 24, 26, 23, 22, 24, 23, 21],
+ },
+ {
+ label: "Not as described",
+ color: "bg-chart-3",
+ stroke: "stroke-chart-3",
+ values: [20, 19, 18, 19, 18, 17, 18, 17],
+ },
+ ],
+ takeaway:
+ "Fit-related returns have grown steadily over 8 weeks (+32%), while damage and description issues remain flat.",
+};
+
+const categoryBreakdown = [
+ { category: "Women's Apparel", pct: 48, highlight: true },
+ { category: "Footwear", pct: 27 },
+ { category: "Men's Apparel", pct: 16 },
+ { category: "Accessories", pct: 9 },
+];
+const categoryInsight =
+ "Women's apparel and footwear account for 75% of all fit-related returns. Sizing inconsistency across brands is the primary driver.";
+
+const topProducts = [
+ {
+ name: "Slim Fit Chinos — Navy",
+ returnRate: 18.4,
+ issue: "Wrong size",
+ impact: "$12,400",
+ },
+ {
+ name: "Running Shoe Pro V2",
+ returnRate: 15.2,
+ issue: "Wrong fit",
+ impact: "$9,800",
+ },
+ {
+ name: "Wrap Dress — Floral",
+ returnRate: 14.7,
+ issue: "Wrong size",
+ impact: "$8,200",
+ },
+ {
+ name: "Oversized Hoodie — Black",
+ returnRate: 12.1,
+ issue: "Too large",
+ impact: "$6,900",
+ },
+ {
+ name: "Ankle Boot — Tan",
+ returnRate: 11.8,
+ issue: "Wrong fit",
+ impact: "$5,400",
+ },
+];
+
+const recommendations = [
+ {
+ action: "Deploy dynamic size recommendation for top 3 SKUs",
+ impact: "Est. 22% reduction in fit returns",
+ priority: "High",
+ },
+ {
+ action: "Add fit-specific review prompts to product pages",
+ impact: "Improve size confidence pre-purchase",
+ priority: "Medium",
+ },
+ {
+ action: "Flag brands with >15% size variance for supplier review",
+ impact: "Address root cause across catalog",
+ priority: "Medium",
+ },
+];
+
+const suggestedPrompts = [
+ "Why are fit-related returns increasing?",
+ "Which products are driving return volume?",
+ "What orders are at risk of return?",
+];
+
+// --- Components ---
+
+function TrendChart({ data }: { data: typeof trendData }) {
+ const allValues = data.series.flatMap((s) => s.values);
+ const max = Math.max(...allValues);
+ const h = 60;
+ const w = 180;
+ const step = w / (data.weeks.length - 1);
+
+ return (
+
+
+
+ Trend over time
+
+
+ 8-week view of the top 3 return reasons
+
+
+
+
+ {data.series.map((s) => (
+
+ ))}
+
+
+
+ );
+}
+
+function CategoryBreakdown() {
+ return (
+
+
+
+ Category breakdown
+
+
+ {`Where "Wrong size/fit" returns are concentrated`}
+
+
+
+ {categoryBreakdown.map((cat) => (
+
+
+ {cat.category}
+ {cat.pct}%
+
+
+
+ ))}
+
+
+
+ );
+}
+
+function TopProducts() {
+ return (
+
+
+
+ Top products driving issues
+
+
+ Ranked by return rate with revenue impact
+
+
+
+
+ Product
+ Return %
+ Issue
+ Impact
+
+ {topProducts.map((p) => (
+
+ {p.name}
+
+ {p.returnRate}%
+
+
+ {p.issue}
+
+
+ {p.impact}
+
+
+ ))}
+
+
+ );
+}
+
+function Recommendations() {
+ return (
+
+
+
+ Recommended actions
+
+
+ AI-assisted next steps based on current data
+
+
+
+ {recommendations.map((rec, i) => (
+
+
+
+ {i + 1}
+
+
+ {rec.priority}
+
+
+
{rec.action}
+
+ {rec.impact}
+
+
+ ))}
+
+
+ );
+}
+
+// --- Exports ---
+
+export function DrilldownTabContent({ tab }: { tab: DrilldownTab }) {
+ if (tab === "trend") return ;
+ if (tab === "categories") return ;
+ if (tab === "products") return ;
+ if (tab === "actions") return ;
+ // "overview" is handled by the original card content
+ return null;
+}
+
+export function AutopilotPrompts({
+ onPromptSelect,
+}: {
+ onPromptSelect?: (prompt: string) => void;
+}) {
+ const [pressedPrompt, setPressedPrompt] = useState(null);
+
+ return (
+
+
+

+

+
Ask Autopilot
+
+
+ {suggestedPrompts.map((prompt) => (
+
+ ))}
+
+
+ );
+}
diff --git a/apps/apollo-vertex/templates/dashboard/dashboard-data-context.ts b/apps/apollo-vertex/templates/dashboard/dashboard-data-context.ts
new file mode 100644
index 000000000..f628d6ac0
--- /dev/null
+++ b/apps/apollo-vertex/templates/dashboard/dashboard-data-context.ts
@@ -0,0 +1,22 @@
+"use client";
+
+import { createContext, useContext } from "react";
+import { ecommerceDataset, type DashboardDataset } from "./dashboard-data";
+
+export interface DashboardDataContextValue {
+ data: DashboardDataset;
+ setDataset: (data: DashboardDataset) => void;
+}
+
+export const DashboardDataContext = createContext({
+ data: ecommerceDataset,
+ setDataset: () => {
+ throw new Error(
+ "useDashboardData must be used within DashboardDataProvider",
+ );
+ },
+});
+
+export function useDashboardData() {
+ return useContext(DashboardDataContext);
+}
diff --git a/apps/apollo-vertex/templates/dashboard/dashboard-data.ts b/apps/apollo-vertex/templates/dashboard/dashboard-data.ts
new file mode 100644
index 000000000..a16846b58
--- /dev/null
+++ b/apps/apollo-vertex/templates/dashboard/dashboard-data.ts
@@ -0,0 +1,228 @@
+export interface InsightCardData {
+ title: string;
+ type: "kpi" | "chart";
+ chartType: "donut" | "horizontal-bars" | "sparkline" | "area" | "stacked-bar";
+ size?: "sm" | "md" | "lg";
+ interaction?: "static" | "expand" | "navigate";
+ // Navigate config
+ navigateTo?: string;
+ // Expand config — additional content shown when card is expanded
+ expandContent?: {
+ summary?: string;
+ details?: string[];
+ };
+ // KPI data
+ kpiNumber?: string;
+ kpiBadge?: string;
+ kpiDescription?: string;
+ // Horizontal bars data
+ bars?: { label: string; value: number }[];
+ // Stacked bar data
+ stackedBars?: { label: string; segments: number[] }[];
+ stackedLegend?: string[];
+ // Donut data
+ donutPercent?: number;
+ donutLabel?: string;
+ donutDescription?: string;
+ // Sparkline / Area data
+ points?: number[];
+}
+
+export interface DashboardDataset {
+ name: string;
+ brandName: string;
+ brandLine: string;
+ dashboardTitle: string;
+ badgeText: string;
+ greeting: string;
+ headline: string;
+ subhead: string;
+ chartLabels: { y: string[]; target: string };
+ promptPlaceholder: string;
+ promptSuggestions: string[];
+ insightCards: [
+ InsightCardData,
+ InsightCardData,
+ InsightCardData,
+ InsightCardData,
+ ];
+}
+
+export const defaultDataset: DashboardDataset = {
+ name: "Loan Setup",
+ brandName: "UiPath",
+ brandLine: "Vertical Solutions",
+ dashboardTitle: "Product",
+ badgeText: "Experimental",
+ greeting: "Good morning, Peter",
+ headline: "Loan volume scales as setup time drops by 3.5 days.",
+ subhead:
+ "Setup time declined ↓21% month over month while volume increased ↑18%.",
+ chartLabels: { y: ["200", "150", "100", "50"], target: "Target" },
+ promptPlaceholder:
+ "What would you like to understand about loan performance?",
+ promptSuggestions: [
+ "Show me top risk factors",
+ "Compare Q1 vs Q2 performance",
+ ],
+ insightCards: [
+ {
+ title: "Upfront decision efficiency",
+ type: "kpi",
+ chartType: "donut",
+ size: "sm",
+ interaction: "static",
+ kpiNumber: "94.2%",
+ kpiBadge: "+6.8%",
+ kpiDescription: "Loans finalized on first review without rework.",
+ },
+ {
+ title: "Top issues",
+ type: "chart",
+ chartType: "horizontal-bars",
+ size: "md",
+ interaction: "expand",
+ expandContent: {
+ summary: "Risk flags have increased 12% this quarter",
+ details: ["Review underwriting criteria", "Update risk scoring model"],
+ },
+ bars: [
+ { label: "Risk flag in notes", value: 34 },
+ { label: "Credit report >120 days old", value: 29 },
+ { label: "Owner name mismatch", value: 23 },
+ { label: "High DTI ratio", value: 14 },
+ { label: "Missing appraisal docs", value: 11 },
+ ],
+ },
+ {
+ title: "Pipeline",
+ type: "chart",
+ chartType: "stacked-bar",
+ size: "md",
+ interaction: "expand",
+ expandContent: {
+ summary: "Weekly volume trending up with stable rejection rates",
+ details: [
+ "Monitor Thursday spike pattern",
+ "Review rejected applications",
+ ],
+ },
+ stackedBars: [
+ { label: "Mon", segments: [30, 20, 10] },
+ { label: "Tue", segments: [40, 15, 20] },
+ { label: "Wed", segments: [25, 30, 15] },
+ { label: "Thu", segments: [45, 10, 25] },
+ { label: "Fri", segments: [35, 25, 18] },
+ ],
+ stackedLegend: ["Approved", "Pending", "Rejected"],
+ },
+ {
+ title: "SLA compliance",
+ type: "kpi",
+ chartType: "donut",
+ size: "sm",
+ interaction: "static",
+ kpiNumber: "99.5%",
+ kpiBadge: "+1.2%",
+ kpiDescription: "Loans processed within defined SLA thresholds.",
+ },
+ ],
+};
+
+export const ecommerceDataset: DashboardDataset = {
+ name: "E-commerce Order Fulfillment",
+ brandName: "UiPath",
+ brandLine: "Vertical Solutions",
+ dashboardTitle: "Order fulfillment",
+ badgeText: "Experimental",
+ greeting: "Good morning, Peter",
+ headline:
+ "Order volume climbs as delivery performance improves, but fit-related returns remain the biggest drag on margin.",
+ subhead:
+ "Orders shipped increased ↑26% month over month while on-time delivery improved ↑2.4%, with size and fit issues now driving the largest share of returns.",
+ chartLabels: { y: ["600", "450", "300", "150"], target: "Target" },
+ promptPlaceholder:
+ "What would you like to understand about order fulfillment?",
+ promptSuggestions: [
+ "Why are fit-related returns increasing?",
+ "Show me products driving return volume",
+ "Compare warehouse performance",
+ "Which orders are most at risk of delay?",
+ ],
+ insightCards: [
+ {
+ title: "On-time delivery rate",
+ type: "kpi",
+ chartType: "donut",
+ size: "sm",
+ interaction: "navigate",
+ kpiNumber: "97.1%",
+ kpiBadge: "+2.4%",
+ kpiDescription:
+ "Orders delivered within promised windows, supported by lower carrier delays and faster pick-pack turnaround.",
+ },
+ {
+ title: "Top issues",
+ type: "chart",
+ chartType: "horizontal-bars",
+ size: "md",
+ interaction: "expand",
+ expandContent: {
+ summary:
+ "Return-related friction is now concentrated in product fit, transit handling, and expectation gaps, with apparel and footwear accounting for the highest exception volume.",
+ details: [
+ "Investigate top SKUs contributing to wrong size and fit returns",
+ "Review packaging and carrier handoff for damage-related issues by warehouse",
+ "Use AI prompts to explain issue concentration by category, region, and fulfillment center",
+ ],
+ },
+ bars: [
+ { label: "Wrong size/fit", value: 39 },
+ { label: "Damaged in transit", value: 23 },
+ { label: "Not as described", value: 18 },
+ { label: "Late delivery", value: 13 },
+ { label: "Changed mind", value: 7 },
+ ],
+ },
+ {
+ title: "Pipeline",
+ type: "chart",
+ chartType: "stacked-bar",
+ size: "md",
+ interaction: "expand",
+ expandContent: {
+ summary:
+ "Fulfillment volume builds steadily through the week, with the highest shipped volume on Thursday and Friday and a midweek rise in processing backlog.",
+ details: [
+ "Monitor Wednesday processing buildup for labor or inventory bottlenecks",
+ "Review Thursday and Friday shipment spikes by warehouse and carrier",
+ "Use AI prompts to identify whether growth is concentrated in apparel, footwear, or home goods",
+ ],
+ },
+ stackedBars: [
+ { label: "Mon", segments: [188, 46, 11] },
+ { label: "Tue", segments: [204, 41, 13] },
+ { label: "Wed", segments: [198, 57, 14] },
+ { label: "Thu", segments: [236, 38, 15] },
+ { label: "Fri", segments: [249, 43, 16] },
+ ],
+ stackedLegend: ["Shipped", "Processing", "Returned"],
+ },
+ {
+ title: "Customer satisfaction",
+ type: "kpi",
+ chartType: "donut",
+ size: "sm",
+ interaction: "navigate",
+ kpiNumber: "4.6",
+ kpiBadge: "+0.2",
+ kpiDescription:
+ "Average rating remains strong, though recent feedback highlights sizing inconsistency and occasional packaging damage.",
+ },
+ ],
+};
+
+export const datasetPresets: Record = {
+ default: defaultDataset,
+ ecommerce: ecommerceDataset,
+};
diff --git a/apps/apollo-vertex/templates/dashboard/glow-config.ts b/apps/apollo-vertex/templates/dashboard/glow-config.ts
new file mode 100644
index 000000000..553f05d45
--- /dev/null
+++ b/apps/apollo-vertex/templates/dashboard/glow-config.ts
@@ -0,0 +1,294 @@
+export interface GlowConfig {
+ start: string;
+ end: string;
+ containerOpacity: number;
+ fillOpacity: number;
+ startStopOpacity: number;
+ endStopOpacity: number;
+ endOffset: number;
+}
+
+export interface CardGradient {
+ enabled: boolean;
+ start: string;
+ end: string;
+ angle: number;
+ opacity: number;
+}
+
+export interface CardConfig {
+ overviewBg: string;
+ overviewOpacity: number;
+ overviewGradient: CardGradient;
+ insightBg: string;
+ insightOpacity: number;
+ insightGradient: CardGradient;
+ promptBg: string;
+ promptOpacity: number;
+ promptGradient: CardGradient;
+ borderVisible: boolean;
+ backdropBlur: boolean;
+}
+
+export const defaultLightGlow: GlowConfig = {
+ start: "var(--insight-500)",
+ end: "var(--primary-400)",
+ containerOpacity: 70,
+ fillOpacity: 0.3,
+ startStopOpacity: 1,
+ endStopOpacity: 1,
+ endOffset: 0.35,
+};
+
+export const defaultDarkGlow: GlowConfig = {
+ start: "var(--insight-700)",
+ end: "var(--primary-600)",
+ containerOpacity: 45,
+ fillOpacity: 1,
+ startStopOpacity: 1,
+ endStopOpacity: 0.4,
+ endOffset: 0.5,
+};
+
+export type CardSize = "sm" | "md" | "lg";
+
+export type InsightCardType = "kpi" | "chart";
+export type ChartType =
+ | "donut"
+ | "horizontal-bars"
+ | "sparkline"
+ | "area"
+ | "stacked-bar";
+
+export interface InsightCardContent {
+ type: InsightCardType;
+ chartType: ChartType;
+ title: string;
+}
+
+export type CardInteraction = "static" | "expand" | "navigate";
+
+export interface InsightCardConfig {
+ size: CardSize;
+ visible: boolean;
+ content: InsightCardContent;
+ interaction: CardInteraction;
+ navigateTo?: string;
+}
+
+export interface LayoutConfig {
+ gap: number;
+ overviewRatio: number;
+ promptRatio: number;
+ insightCards: [
+ InsightCardConfig,
+ InsightCardConfig,
+ InsightCardConfig,
+ InsightCardConfig,
+ ];
+ padding: number;
+ containerBg: string;
+}
+
+export const defaultLayout: LayoutConfig = {
+ gap: 4,
+ overviewRatio: 4,
+ promptRatio: 1,
+ insightCards: [
+ {
+ size: "sm",
+ visible: true,
+ interaction: "static",
+ content: {
+ type: "kpi",
+ chartType: "donut",
+ title: "Upfront decision efficiency",
+ },
+ },
+ {
+ size: "md",
+ visible: true,
+ interaction: "expand",
+ content: {
+ type: "chart",
+ chartType: "horizontal-bars",
+ title: "Top issues",
+ },
+ },
+ {
+ size: "md",
+ visible: true,
+ interaction: "expand",
+ content: {
+ type: "chart",
+ chartType: "stacked-bar",
+ title: "Pipeline",
+ },
+ },
+ {
+ size: "sm",
+ visible: true,
+ interaction: "static",
+ content: {
+ type: "kpi",
+ chartType: "donut",
+ title: "SLA compliance",
+ },
+ },
+ ],
+ padding: 24,
+ containerBg: "none",
+};
+
+const defaultGradient: CardGradient = {
+ enabled: false,
+ start: "var(--insight-500)",
+ end: "var(--primary-400)",
+ angle: 135,
+ opacity: 100,
+};
+
+export const insightOptions = [
+ { label: "300", value: "var(--insight-300)" },
+ { label: "400", value: "var(--insight-400)" },
+ { label: "500", value: "var(--insight-500)" },
+ { label: "600", value: "var(--insight-600)" },
+ { label: "700", value: "var(--insight-700)" },
+ { label: "800", value: "var(--insight-800)" },
+ { label: "900", value: "var(--insight-900)" },
+];
+
+export const primaryOptions = [
+ { label: "300", value: "var(--primary-300)" },
+ { label: "400", value: "var(--primary-400)" },
+ { label: "500", value: "var(--primary-500)" },
+ { label: "600", value: "var(--primary-600)" },
+ { label: "700", value: "var(--primary-700)" },
+ { label: "800", value: "var(--primary-800)" },
+ { label: "900", value: "var(--primary-900)" },
+];
+
+export const cardTypeOptions = [
+ { label: "KPI", value: "kpi" },
+ { label: "Chart", value: "chart" },
+];
+
+export const interactionOptions = [
+ { label: "Static", value: "static" },
+ { label: "Expand", value: "expand" },
+ { label: "Navigate", value: "navigate" },
+];
+
+export const chartTypeOptions = [
+ { label: "Donut", value: "donut" },
+ { label: "Horizontal Bars", value: "horizontal-bars" },
+ { label: "Sparkline", value: "sparkline" },
+ { label: "Area", value: "area" },
+ { label: "Stacked Bar", value: "stacked-bar" },
+];
+
+export const sizeOptions = [
+ { label: "Small (1 col)", value: "sm" },
+ { label: "Medium (1 col)", value: "md" },
+ { label: "Large (full)", value: "lg" },
+];
+
+export const containerBgOptions = [
+ { label: "None", value: "none" },
+ { label: "white", value: "white" },
+ { label: "sidebar", value: "sidebar" },
+ { label: "card", value: "card" },
+ { label: "background", value: "background" },
+ { label: "muted", value: "muted" },
+];
+
+export const bgColorOptions = [
+ { label: "white", value: "white" },
+ { label: "sidebar", value: "sidebar" },
+ { label: "card", value: "card" },
+ { label: "background", value: "background" },
+ { label: "muted", value: "muted" },
+];
+
+export function cardBgStyle(
+ bg: string,
+ opacity: number,
+ gradient: CardGradient,
+): React.CSSProperties {
+ if (gradient.enabled) {
+ const alpha = gradient.opacity / 100;
+ const style = {
+ "--card-bg-override": `linear-gradient(${gradient.angle}deg, color-mix(in srgb, ${gradient.start} ${alpha * 100}%, transparent), color-mix(in srgb, ${gradient.end} ${alpha * 100}%, transparent))`,
+ borderColor: "transparent",
+ };
+ // oxlint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- CSS custom properties require assertion
+ return style as unknown as React.CSSProperties;
+ }
+ const value =
+ bg === "white"
+ ? `rgba(255,255,255,${opacity / 100})`
+ : `color-mix(in srgb, var(--${bg}) ${opacity}%, transparent)`;
+ // oxlint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- CSS custom properties require assertion
+ return { "--card-bg-override": value } as unknown as React.CSSProperties;
+}
+
+export function getInsightCardClasses(
+ content: InsightCardContent,
+ viewMode: "desktop" | "compact" | "stacked" = "desktop",
+): {
+ cardClassName: string;
+ contentClassName: string;
+} {
+ if (content.type === "kpi") {
+ const isCompact = viewMode === "compact";
+ return {
+ cardClassName: isCompact ? "!gap-0" : "!gap-4",
+ contentClassName: isCompact
+ ? "flex-1 flex flex-col overflow-hidden"
+ : "flex-1 flex flex-col",
+ };
+ }
+ const isBarChart = content.chartType === "horizontal-bars";
+ return {
+ cardClassName: content.chartType === "donut" ? "!gap-0" : "",
+ contentClassName: isBarChart ? "flex-1" : "flex-1 flex flex-col",
+ };
+}
+
+export const defaultDarkCards: CardConfig = {
+ overviewBg: "sidebar",
+ overviewOpacity: 69,
+ overviewGradient: { ...defaultGradient, opacity: 30 },
+ insightBg: "sidebar",
+ insightOpacity: 60,
+ insightGradient: { ...defaultGradient },
+ promptBg: "sidebar",
+ promptOpacity: 80,
+ promptGradient: { ...defaultGradient },
+ borderVisible: false,
+ backdropBlur: true,
+};
+
+const CARD_SIZES = new Set(["sm", "md", "lg"]);
+const INSIGHT_CARD_TYPES = new Set(["kpi", "chart"]);
+const CHART_TYPES = new Set([
+ "donut",
+ "horizontal-bars",
+ "sparkline",
+ "area",
+ "stacked-bar",
+]);
+const CARD_INTERACTIONS = new Set(["static", "expand", "navigate"]);
+
+export function isCardSize(v: string): v is CardSize {
+ return CARD_SIZES.has(v);
+}
+export function isInsightCardType(v: string): v is InsightCardType {
+ return INSIGHT_CARD_TYPES.has(v);
+}
+export function isChartType(v: string): v is ChartType {
+ return CHART_TYPES.has(v);
+}
+export function isCardInteraction(v: string): v is CardInteraction {
+ return CARD_INTERACTIONS.has(v);
+}
diff --git a/apps/apollo-vertex/templates/dashboard/insight-card-renderers.tsx b/apps/apollo-vertex/templates/dashboard/insight-card-renderers.tsx
new file mode 100644
index 000000000..3d8298085
--- /dev/null
+++ b/apps/apollo-vertex/templates/dashboard/insight-card-renderers.tsx
@@ -0,0 +1,349 @@
+"use client";
+
+import { useRef, useState, useEffect } from "react";
+import { Badge } from "@/components/ui/badge";
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipTrigger,
+} from "@/registry/tooltip/tooltip";
+import type { InsightCardContent } from "./glow-config";
+import { useDashboardData } from "./dashboard-data-context";
+import type { InsightCardData } from "./dashboard-data";
+import { DonutContent, SparklineContent, AreaContent } from "./chart-stubs";
+
+type ViewMode = "desktop" | "compact" | "stacked";
+
+// --- Truncated text with conditional tooltip ---
+
+function TruncatedText({
+ children,
+ className,
+}: {
+ children: string | undefined;
+ className?: string;
+}) {
+ const textRef = useRef(null);
+ const [isTruncated, setIsTruncated] = useState(false);
+
+ useEffect(() => {
+ const el = textRef.current;
+ if (!el) return;
+ const check = () => setIsTruncated(el.scrollHeight > el.clientHeight);
+ check();
+ const observer = new ResizeObserver(check);
+ observer.observe(el);
+ return () => observer.disconnect();
+ }, [children]);
+
+ const textEl = (
+
+ {children}
+
+ );
+
+ if (!isTruncated) return textEl;
+
+ return (
+
+ {textEl}
+
+ {children}
+
+
+ );
+}
+
+function KpiContent({
+ cardData,
+ viewMode,
+}: {
+ cardData: InsightCardData;
+ viewMode: ViewMode;
+}) {
+ if (viewMode === "compact") {
+ return (
+ <>
+
+
+ {cardData.kpiNumber}
+
+
+ {cardData.kpiBadge}
+
+
+
+ {cardData.kpiDescription}
+
+ >
+ );
+ }
+
+ return (
+ <>
+
+ {cardData.kpiNumber}
+
+
+
+ {cardData.kpiBadge}
+
+
+ {cardData.kpiDescription}
+
+
+ >
+ );
+}
+
+function HorizontalBarsContent({
+ cardData,
+ viewMode,
+ isExpanded = false,
+}: {
+ cardData: InsightCardData;
+ viewMode: ViewMode;
+ isExpanded?: boolean;
+}) {
+ const bars = cardData.bars ?? [];
+ const chartColors = [
+ "bg-chart-1",
+ "bg-chart-2",
+ "bg-chart-3",
+ "bg-chart-4",
+ "bg-chart-5",
+ ];
+ const barsWithColor = bars.map((b, i) => ({
+ ...b,
+ color: chartColors[i % chartColors.length],
+ }));
+
+ if (viewMode === "compact" && !isExpanded) {
+ const total = barsWithColor.reduce((sum, s) => sum + s.value, 0);
+ return (
+
+
+ {barsWithColor.map((issue) => (
+
+ ))}
+
+
+ {barsWithColor.map((issue) => {
+ const pct = Math.round((issue.value / total) * 100);
+ return (
+
+
+
+ {issue.label} {pct}%
+
+
+ );
+ })}
+
+
+ );
+ }
+
+ return (
+
+ {barsWithColor.map((issue) => (
+
+
+ {issue.label}
+ {issue.value}%
+
+
+
+ ))}
+
+ );
+}
+
+function StackedBarContent({
+ cardData,
+ viewMode,
+ isExpanded = false,
+}: {
+ cardData: InsightCardData;
+ viewMode: ViewMode;
+ isExpanded?: boolean;
+}) {
+ const chartColors = [
+ "bg-chart-1",
+ "bg-chart-2",
+ "bg-chart-3",
+ "bg-chart-4",
+ "bg-chart-5",
+ ];
+ const rawBars = cardData.stackedBars ?? [];
+ const legend = (cardData.stackedLegend ?? []).map((label, i) => ({
+ label,
+ color: chartColors[i % chartColors.length],
+ }));
+ const barData = rawBars.map((bar) => ({
+ label: bar.label,
+ segments: bar.segments.map((value, i) => ({
+ value,
+ color: chartColors[i % chartColors.length],
+ })),
+ }));
+ const maxTotal = Math.max(
+ ...barData.map((d) => d.segments.reduce((sum, s) => sum + s.value, 0)),
+ );
+
+ if (viewMode === "compact" && !isExpanded) {
+ // Summary: aggregate all days into one horizontal stacked bar
+ const totals = barData.reduce(
+ (acc, day) => {
+ for (const seg of day.segments) {
+ const key = seg.color;
+ acc[key] = (acc[key] ?? 0) + seg.value;
+ }
+ return acc;
+ },
+ {} as Record,
+ );
+ const grandTotal = Object.values(totals).reduce((a, b) => a + b, 0);
+
+ return (
+
+
+ {legend.map((item) => (
+
+ ))}
+
+
+ {legend.map((item) => {
+ const val = totals[item.color] ?? 0;
+ const pct = Math.round((val / grandTotal) * 100);
+ return (
+
+
+
+ {item.label} {pct}%
+
+
+ );
+ })}
+
+
+ );
+ }
+
+ return (
+
+
+ {barData.map((bar) => {
+ const total = bar.segments.reduce((sum, s) => sum + s.value, 0);
+ const pct = (total / maxTotal) * 100;
+ return (
+
+
+
+ {bar.segments.map((seg) => (
+
+ ))}
+
+
+ {bar.segments.map((seg) => (
+
+ ))}
+
+
+
+ {bar.label}
+
+
+ );
+ })}
+
+
+ {legend.map((item) => (
+
+ ))}
+
+
+ );
+}
+
+export function InsightCardBody({
+ content,
+ cardIndex,
+ viewMode = "desktop",
+ isExpanded = false,
+}: {
+ content: InsightCardContent;
+ cardIndex: number;
+ viewMode?: ViewMode;
+ isExpanded?: boolean;
+}) {
+ const { data } = useDashboardData();
+ const cardData = data.insightCards[cardIndex] ?? data.insightCards[0];
+
+ if (content.type === "kpi") {
+ return ;
+ }
+ if (content.chartType === "horizontal-bars")
+ return (
+
+ );
+ if (content.chartType === "donut") return ;
+ if (content.chartType === "sparkline") return ;
+ if (content.chartType === "stacked-bar")
+ return (
+
+ );
+ return ;
+}