diff --git a/apps/apollo-vertex/app/_meta.ts b/apps/apollo-vertex/app/_meta.ts
index 96c326ad0..8ad97f65a 100644
--- a/apps/apollo-vertex/app/_meta.ts
+++ b/apps/apollo-vertex/app/_meta.ts
@@ -3,6 +3,7 @@ export default {
dashboards: "Dashboards",
"data-querying": "Data Querying",
patterns: "Patterns",
+ layouts: "Layouts",
"shadcn-components": "Shadcn Components",
"vertex-components": "Vertex Components",
themes: "Themes",
diff --git a/apps/apollo-vertex/app/globals.css b/apps/apollo-vertex/app/globals.css
index ee69bfa7f..6db7e92ac 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/layouts/_meta.ts b/apps/apollo-vertex/app/layouts/_meta.ts
new file mode 100644
index 000000000..c454dc3d1
--- /dev/null
+++ b/apps/apollo-vertex/app/layouts/_meta.ts
@@ -0,0 +1,3 @@
+export default {
+ dashboard: "Dashboard",
+};
diff --git a/apps/apollo-vertex/app/layouts/dashboard/page.mdx b/apps/apollo-vertex/app/layouts/dashboard/page.mdx
new file mode 100644
index 000000000..15e0fc487
--- /dev/null
+++ b/apps/apollo-vertex/app/layouts/dashboard/page.mdx
@@ -0,0 +1,149 @@
+import { DashboardTemplate } from '@/templates/dashboard/DashboardTemplateDynamic';
+import { PreviewFullScreen } from '@/app/components/preview-full-screen';
+
+# Dashboard
+
+A configurable dashboard template for building AI-assisted operational views. The dashboard is composed of named card regions with defined interaction patterns, designed to be data-driven and adaptable across verticals.
+
+## Previews
+
+### Sidebar Shell
+
+
+
+
+
+Open standalone preview →
+
+### Minimal Header Shell
+
+
+
+
+
+Open standalone preview →
+
+---
+
+## Anatomy
+
+The dashboard is built from four semantic regions, each with a distinct role:
+
+| Region | Description |
+|---|---|
+| **Overview Card** | The primary narrative area. Displays a greeting, headline, and subhead that summarize the current state. Occupies the top-left of the layout. |
+| **Prompt Bar** | The AI input surface. Supports text entry, recommendation chips, and an expand interaction that opens an inline chat view. Anchored below the overview card. |
+| **Insight Cards** | A grid of data cards on the right side. Each card has a type, a chart visualization, and an interaction pattern. Cards are arranged in rows of two. |
+| **Autopilot Panel** | A slide-in panel that provides AI-generated analysis for a specific insight card. Triggered from the card's autopilot icon. |
+
+## Card Types
+
+Every insight card has a `type` that determines what it displays:
+
+| Type | Renders | Example |
+|---|---|---|
+| `kpi` | A large number, badge, and description | "97.1%" with "+2.4%" badge |
+| `chart` | A data visualization determined by `chartType` | Horizontal bars, stacked bars |
+
+## Chart Types
+
+Cards with `type: "chart"` use a `chartType` to select the visualization:
+
+| Chart Type | Description |
+|---|---|
+| `horizontal-bars` | Ranked list of items with proportional bars and percentages |
+| `stacked-bar` | Grouped bars with color-coded segments and a legend |
+| `donut` | Circular progress indicator with a center label |
+| `sparkline` | Compact line chart for trend indication |
+| `area` | Filled area chart for volume over time |
+
+## Card Sizes
+
+Each card has a `size` that controls its column weight in the grid:
+
+| Size | Grid Weight | Use Case |
+|---|---|---|
+| `sm` | `1fr` | KPI cards, compact metrics |
+| `md` | `2fr` | Chart cards, detailed visualizations |
+| `lg` | `1fr` | Full-width cards |
+
+## Interaction Patterns
+
+Cards support three interaction modes:
+
+### Static
+
+No interactive behavior. The card displays its content without any hover or click affordance. Used for simple KPIs that don't need drill-down.
+
+### Navigate
+
+On hover, an **arrow-up-right** icon appears in the card header. Clicking the card navigates to a detail page. Used for KPIs or summaries that link to a deeper view.
+
+### Expand
+
+On hover, a **maximize** icon appears in the card header. Clicking expands the card to fill the grid with a multi-phase animation:
+
+1. **Width phase** — Card expands horizontally, sibling card collapses
+2. **Height phase** — Card expands vertically, other rows collapse
+3. **Full phase** — Expanded content fades in (drilldown tabs, autopilot prompts)
+
+Clicking the **minimize** icon or another card collapses back to the grid.
+
+For `horizontal-bars` cards, the expanded state includes **drilldown tabs** below the title for switching between data views.
+
+## Prompt Bar
+
+The prompt bar supports three ways to expand into the inline chat view:
+
+| Trigger | Behavior |
+|---|---|
+| **Type + Enter / Submit** | Expands and passes the user's typed query |
+| **Click a recommendation chip** | Expands and passes the chip text as the query |
+| **Click the chat icon** | Expands and shows the current session conversation |
+
+When expanded, the overview card collapses and the prompt bar grows to fill the left column. A **minimize** icon in the header collapses back to the default layout.
+
+## Autopilot Panel
+
+Each expandable card includes an **autopilot** icon alongside the expand icon. Clicking it slides in a panel from the right that provides AI-generated context for that card. The dashboard content shifts left to make room. Clicking the autopilot icon again or the close button dismisses the panel.
+
+## Data Configuration
+
+The dashboard is driven by a `DashboardDataset` object that defines all text and card content:
+
+```ts
+interface DashboardDataset {
+ name: string; // Dataset identifier
+ brandName: string; // Company name in header
+ brandLine: string; // Tagline in header
+ dashboardTitle: string; // Page title
+ badgeText: string; // Badge next to title
+ greeting: string; // Overview card greeting
+ headline: string; // Overview card headline
+ subhead: string; // Overview card description
+ promptPlaceholder: string; // Prompt bar placeholder text
+ promptSuggestions: string[]; // Recommendation chips
+ insightCards: [ // Exactly 4 insight cards
+ InsightCardData,
+ InsightCardData,
+ InsightCardData,
+ InsightCardData,
+ ];
+}
+```
+
+Each `InsightCardData` specifies its title, type, chart type, size, interaction, and the data for its visualization (KPI values, bar data, stacked segments, etc.).
+
+## Layout Modes
+
+The dashboard responds to container width:
+
+| Mode | Breakpoint | Behavior |
+|---|---|---|
+| **Desktop** | ≥ 1100px | Two-column layout, full card grid |
+| **Compact** | 800–1099px | Two-column layout, condensed card content |
+| **Stacked** | < 800px | Single-column, vertically stacked |
+
+## Theming
+
+Card backgrounds, glow effects, and gradients are configurable through `CardConfig` and `GlowConfig` objects. The dashboard supports light and dark modes with independent styling for each. Color tokens and theme values should be updated in `registry.json`, not directly in CSS files.
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..30acb999a
--- /dev/null
+++ b/apps/apollo-vertex/app/preview/dashboard-minimal/page.tsx
@@ -0,0 +1,16 @@
+"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..db71ea817
--- /dev/null
+++ b/apps/apollo-vertex/app/preview/dashboard/page.tsx
@@ -0,0 +1,16 @@
+"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 09f7884fd..ba4087f26 100644
--- a/apps/apollo-vertex/registry.json
+++ b/apps/apollo-vertex/registry.json
@@ -64,7 +64,17 @@
"color-sidebar-accent": "var(--sidebar-accent)",
"color-sidebar-accent-foreground": "var(--sidebar-accent-foreground)",
"color-sidebar-border": "var(--sidebar-border)",
- "color-sidebar-ring": "var(--sidebar-ring)"
+ "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)"
},
"light": {
"background": "oklch(1 0 89.8800)",
@@ -118,6 +128,16 @@
"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",
@@ -207,6 +227,16 @@
"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",
diff --git a/apps/apollo-vertex/registry/card/card.tsx b/apps/apollo-vertex/registry/card/card.tsx
index d9d31dfa0..23bd35901 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/AutopilotInsight.tsx b/apps/apollo-vertex/templates/dashboard/AutopilotInsight.tsx
new file mode 100644
index 000000000..4f98b2323
--- /dev/null
+++ b/apps/apollo-vertex/templates/dashboard/AutopilotInsight.tsx
@@ -0,0 +1,76 @@
+"use client";
+
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+
+interface AutopilotInsightProps {
+ onClose: () => void;
+ sourceCardTitle: string;
+}
+
+export function AutopilotInsight({
+ onClose,
+ sourceCardTitle,
+}: AutopilotInsightProps) {
+ return (
+
+ {/* Close button */}
+
+
+
+
+
+
+
+
+
+
+
+
+ Autopilot Insight
+
+
+
+ Analyzing {sourceCardTitle}
+
+
+
+
+ {/* Placeholder for future chat UX responses */}
+
+
+
+ Autopilot response area
+
+
+ Chat UX content will appear here
+
+
+
+
+
+ );
+}
diff --git a/apps/apollo-vertex/templates/dashboard/DashboardCards.tsx b/apps/apollo-vertex/templates/dashboard/DashboardCards.tsx
new file mode 100644
index 000000000..3a6ad30aa
--- /dev/null
+++ b/apps/apollo-vertex/templates/dashboard/DashboardCards.tsx
@@ -0,0 +1,288 @@
+import { AlertTriangle, CheckCircle, Clock, XCircle } from "lucide-react";
+import type { LucideIcon } from "lucide-react";
+import { Badge } from "@/components/ui/badge";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Progress } from "@/components/ui/progress";
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "@/components/ui/table";
+
+// --- Types ---
+
+export interface KpiItem {
+ label: string;
+ value: string;
+ icon: LucideIcon;
+ change: string;
+}
+
+// --- Sample data ---
+
+const invoices = [
+ {
+ id: "INV-4021",
+ vendor: "Acme Corp",
+ amount: "$12,450.00",
+ status: "Processed" as const,
+ date: "Mar 18, 2026",
+ },
+ {
+ id: "INV-4020",
+ vendor: "Global Supplies Ltd",
+ amount: "$3,280.50",
+ status: "Pending" as const,
+ date: "Mar 18, 2026",
+ },
+ {
+ id: "INV-4019",
+ vendor: "TechParts Inc",
+ amount: "$8,920.00",
+ status: "In Review" as const,
+ date: "Mar 17, 2026",
+ },
+ {
+ id: "INV-4018",
+ vendor: "Office Depot",
+ amount: "$1,150.75",
+ status: "Processed" as const,
+ date: "Mar 17, 2026",
+ },
+ {
+ id: "INV-4017",
+ vendor: "CloudServ Solutions",
+ amount: "$24,000.00",
+ status: "Failed" as const,
+ date: "Mar 16, 2026",
+ },
+ {
+ id: "INV-4016",
+ vendor: "Metro Logistics",
+ amount: "$6,780.00",
+ status: "Processed" as const,
+ date: "Mar 16, 2026",
+ },
+];
+
+const statusVariant: Record<
+ string,
+ "default" | "secondary" | "destructive" | "outline"
+> = {
+ Processed: "default",
+ Pending: "secondary",
+ Failed: "destructive",
+ "In Review": "outline",
+};
+
+const statusIcon: Record = {
+ Processed: CheckCircle,
+ Pending: Clock,
+ Failed: XCircle,
+ "In Review": AlertTriangle,
+};
+
+const activityBars = [
+ { label: "Mon", height: 60 },
+ { label: "Tue", height: 85 },
+ { label: "Wed", height: 45 },
+ { label: "Thu", height: 92 },
+ { label: "Fri", height: 78 },
+ { label: "Sat", height: 30 },
+ { label: "Sun", height: 15 },
+];
+
+const recentActivity = [
+ { text: "INV-4021 processed successfully", time: "2 min ago" },
+ { text: "INV-4020 submitted for review", time: "15 min ago" },
+ { text: "Batch processing completed (42 invoices)", time: "1 hr ago" },
+ { text: "INV-4017 failed — missing PO number", time: "3 hrs ago" },
+];
+
+const pipelineStages = [
+ { label: "OCR Extraction", value: 96 },
+ { label: "Field Validation", value: 88 },
+ { label: "Approval Routing", value: 72 },
+ { label: "Final Review", value: 64 },
+];
+
+const complianceChecks = [
+ { label: "Income Verification", pass: 98 },
+ { label: "Credit Score Threshold", pass: 96 },
+ { label: "Debt-to-Income Ratio", pass: 91 },
+ { label: "Collateral Appraisal", pass: 87 },
+ { label: "Document Completeness", pass: 94 },
+];
+
+// --- Card components ---
+
+export function KpiCards({ kpis }: { kpis: KpiItem[] }) {
+ return (
+ <>
+ {kpis.map((kpi) => (
+
+
+
+
+ {kpi.label}
+
+
+
+
+
+ {kpi.value}
+
+ {kpi.change} from last
+ week
+
+
+
+ ))}
+ >
+ );
+}
+
+export function InvoiceTable() {
+ return (
+
+
+ Recent Invoices
+
+
+
+
+
+ Invoice
+ Vendor
+ Amount
+ Status
+ Date
+
+
+
+ {invoices.map((inv) => {
+ const StatusIcon = statusIcon[inv.status];
+ return (
+
+ {inv.id}
+ {inv.vendor}
+ {inv.amount}
+
+
+
+ {inv.status}
+
+
+
+ {inv.date}
+
+
+ );
+ })}
+
+
+
+
+ );
+}
+
+export function ActivityBarChart() {
+ return (
+
+
+ Processing Activity
+
+
+
+ {activityBars.map((bar) => (
+
+ ))}
+
+
+
+ );
+}
+
+export function ActivityFeed() {
+ return (
+
+
+ Recent Activity
+
+
+
+ {recentActivity.map((event) => (
+
+
+
+
{event.text}
+
{event.time}
+
+
+ ))}
+
+
+
+ );
+}
+
+export function PipelineProgress() {
+ return (
+
+
+ Processing Pipeline
+
+
+
+ {pipelineStages.map((stage) => (
+
+
+ {stage.label}
+ {stage.value}%
+
+
+
+ ))}
+
+
+
+ );
+}
+
+export function ComplianceProgress() {
+ return (
+
+
+ Compliance Pass Rates
+
+
+
+ {complianceChecks.map((check) => (
+
+
+ {check.label}
+ {check.pass}%
+
+
+
+ ))}
+
+
+
+ );
+}
diff --git a/apps/apollo-vertex/templates/dashboard/DashboardContent.tsx b/apps/apollo-vertex/templates/dashboard/DashboardContent.tsx
new file mode 100644
index 000000000..07673ad7a
--- /dev/null
+++ b/apps/apollo-vertex/templates/dashboard/DashboardContent.tsx
@@ -0,0 +1,306 @@
+"use client";
+
+import { useEffect, useRef, useState } from "react";
+import { Badge } from "@/components/ui/badge";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { DashboardGlow } from "./DashboardGlow";
+import {
+ cardBgStyle,
+ defaultDarkCards,
+ defaultDarkGlow,
+ defaultLayout,
+ type CardConfig,
+ type GlowConfig,
+ type LayoutConfig,
+} from "./glow-config";
+import { GlowDevControls } from "./GlowDevControls";
+import { InsightGrid } from "./InsightGrid";
+import { DashboardLoading } from "./DashboardLoading";
+import { PromptBar } from "./PromptBar";
+import { AutopilotInsight } from "./AutopilotInsight";
+import {
+ useDashboardData,
+ DashboardDataProvider,
+} from "./DashboardDataProvider";
+
+type LayoutType = "executive" | "operational" | "analytics";
+
+function ExecutiveLayout({
+ cards,
+ layout,
+ viewMode,
+ onAutopilotOpen,
+ autopilotActiveIdx,
+}: {
+ cards: CardConfig;
+ layout: LayoutConfig;
+ viewMode: ViewMode;
+ onAutopilotOpen?: (sourceTitle: string, idx: number) => void;
+ autopilotActiveIdx?: number | null;
+}) {
+ const { data } = useDashboardData();
+ const [promptExpanded, setPromptExpanded] = useState(false);
+ const borderClass = cards.borderVisible ? "" : "dark:!border-transparent";
+ const blurClass = cards.backdropBlur ? "" : "dark:!backdrop-blur-none";
+ const shared = `!shadow-none dark:![background:var(--card-bg-override)] ${borderClass} ${blurClass}`;
+ const gapStyle = { gap: `${layout.gap}px` };
+
+ return (
+
+
+
+
+
+
+
+
+ {data.greeting}
+
+
+
+
+
+ {data.headline}
+
+
+ {data.subhead}
+
+
+
+
+
+
setPromptExpanded(true)}
+ onExpand={() => setPromptExpanded(true)}
+ onCollapse={() => setPromptExpanded(false)}
+ />
+
+
+
+
+
+ );
+}
+
+function OperationalLayout() {
+ return (
+
+ Operational layout — coming soon
+
+ );
+}
+
+function AnalyticsLayout() {
+ return (
+
+ Analytics layout — coming soon
+
+ );
+}
+
+// --- Main component ---
+
+type ViewMode = "desktop" | "compact" | "stacked";
+
+function useViewMode(ref: React.RefObject): ViewMode {
+ const [mode, setMode] = useState("desktop");
+
+ useEffect(() => {
+ const el = ref.current;
+ if (!el) return;
+ const observer = new ResizeObserver(([entry]) => {
+ const w = entry.contentRect.width;
+ if (w >= 1100) setMode("desktop");
+ else if (w >= 800) setMode("compact");
+ else setMode("stacked");
+ });
+ observer.observe(el);
+ return () => observer.disconnect();
+ }, [ref]);
+
+ return mode;
+}
+
+function DashboardContentInner() {
+ const { data } = useDashboardData();
+ const [layout] = useState("executive");
+ const [darkGlow, setDarkGlow] = useState(defaultDarkGlow);
+ const [darkCards, setDarkCards] = useState(defaultDarkCards);
+ const [layoutCfg, setLayoutCfg] = useState(defaultLayout);
+ const [replayCount] = useState(0);
+ const [autopilotOpen, setAutopilotOpen] = useState(false);
+ const [autopilotSource, setAutopilotSource] = useState("");
+ const [autopilotActiveIdx, setAutopilotActiveIdx] = useState(
+ null,
+ );
+ const containerRef = useRef(null);
+ const viewMode = useViewMode(containerRef);
+
+ const handleAutopilotOpen = (sourceTitle: string, idx: number) => {
+ if (autopilotOpen && autopilotActiveIdx === idx) {
+ setAutopilotOpen(false);
+ setAutopilotActiveIdx(null);
+ } else {
+ setAutopilotSource(sourceTitle);
+ setAutopilotActiveIdx(idx);
+ setAutopilotOpen(true);
+ }
+ };
+
+ const handleAutopilotClose = () => {
+ setAutopilotOpen(false);
+ setAutopilotActiveIdx(null);
+ };
+
+ return (
+
+
+
+
+
+ {/* Header — stays in place */}
+
+
+
+ {data.brandName} {" "}
+ {data.brandLine}
+
+
+ {data.dashboardTitle}
+
+ {data.badgeText}
+
+
+
+
+
+
+
+
+
+ Last 7 days
+ Last 14 days
+ Last 30 days
+ Last 90 days
+ Last 12 months
+
+
+
+ Primary action
+
+
+
+
+ {/* Layout content */}
+
+ {/* Dashboard cards — shifts left for autopilot */}
+
+ {layout === "executive" && (
+
+ )}
+ {layout === "operational" &&
}
+ {layout === "analytics" &&
}
+
+ {/* Autopilot panel — slides in from right */}
+
+
+
+
+
+ );
+}
+
+export function DashboardContent() {
+ return (
+
+
+
+ );
+}
diff --git a/apps/apollo-vertex/templates/dashboard/DashboardDataProvider.tsx b/apps/apollo-vertex/templates/dashboard/DashboardDataProvider.tsx
new file mode 100644
index 000000000..d1d56a42e
--- /dev/null
+++ b/apps/apollo-vertex/templates/dashboard/DashboardDataProvider.tsx
@@ -0,0 +1,28 @@
+"use client";
+
+import { createContext, useContext, useState, type ReactNode } from "react";
+import { ecommerceDataset, type DashboardDataset } from "./dashboard-data";
+
+interface DashboardDataContextValue {
+ data: DashboardDataset;
+ setDataset: (data: DashboardDataset) => void;
+}
+
+const DashboardDataContext = createContext({
+ data: ecommerceDataset,
+ setDataset: () => {},
+});
+
+export function useDashboardData() {
+ return useContext(DashboardDataContext);
+}
+
+export function DashboardDataProvider({ children }: { children: ReactNode }) {
+ const [data, setData] = useState(ecommerceDataset);
+
+ return (
+
+ {children}
+
+ );
+}
diff --git a/apps/apollo-vertex/templates/dashboard/DashboardGlow.tsx b/apps/apollo-vertex/templates/dashboard/DashboardGlow.tsx
new file mode 100644
index 000000000..953d42119
--- /dev/null
+++ b/apps/apollo-vertex/templates/dashboard/DashboardGlow.tsx
@@ -0,0 +1,95 @@
+"use client";
+
+import {
+ defaultDarkGlow,
+ defaultLightGlow,
+ type GlowConfig,
+} from "./glow-config";
+
+interface DashboardGlowProps {
+ className?: string;
+ darkConfig?: GlowConfig;
+}
+
+function GlowSvg({ id, config }: { id: string; config: GlowConfig }) {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export function DashboardGlow({ className, darkConfig }: DashboardGlowProps) {
+ const light = defaultLightGlow;
+ const dark = darkConfig ?? defaultDarkGlow;
+
+ return (
+
+ );
+}
diff --git a/apps/apollo-vertex/templates/dashboard/DashboardLoading.tsx b/apps/apollo-vertex/templates/dashboard/DashboardLoading.tsx
new file mode 100644
index 000000000..9c378dc38
--- /dev/null
+++ b/apps/apollo-vertex/templates/dashboard/DashboardLoading.tsx
@@ -0,0 +1,137 @@
+"use client";
+
+import { useCallback, 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);
+
+ const startSequence = useCallback(() => {
+ setExiting(false);
+ setPhase("logo");
+ }, []);
+
+ useEffect(() => {
+ if (triggerReplay === 0) return;
+ if (triggerReplay) startSequence();
+ }, [triggerReplay, startSequence]);
+
+ 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..fe3ffc163
--- /dev/null
+++ b/apps/apollo-vertex/templates/dashboard/DashboardShellWrapper.tsx
@@ -0,0 +1,50 @@
+import type { ReactNode } from "react";
+import type { ShellNavItem } from "@/registry/shell/shell";
+import { ApolloShell } from "@/registry/shell/shell";
+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..e3321b5cb
--- /dev/null
+++ b/apps/apollo-vertex/templates/dashboard/ExpandedInsightContent.tsx
@@ -0,0 +1,338 @@
+"use client";
+
+import { useState } from "react";
+import { Badge } from "@/components/ui/badge";
+
+// --- 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((series) => {
+ const points = series.values
+ .map((v, i) => `${i * step},${h - (v / max) * h * 0.85}`)
+ .join(" ");
+ return (
+
+ );
+ })}
+
+
+ {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 type DrilldownTab =
+ | "overview"
+ | "trend"
+ | "categories"
+ | "products"
+ | "actions";
+
+export const drilldownTabs: { key: DrilldownTab; label: string }[] = [
+ { key: "overview", label: "Overview" },
+ { key: "categories", label: "Categories" },
+ { key: "products", label: "Products" },
+ { key: "actions", label: "Actions" },
+];
+
+export function DrilldownTabContent({ tab }: { tab: DrilldownTab }) {
+ if (tab === "trend") return ;
+ if (tab === "categories") return ;
+ if (tab === "products") return ;
+ if (tab === "actions") return ;
+ return null; // "overview" is handled by the original card content
+}
+
+export function AutopilotPrompts({
+ onPromptSelect,
+}: {
+ onPromptSelect?: (prompt: string) => void;
+}) {
+ const [pressedPrompt, setPressedPrompt] = useState(null);
+
+ return (
+
+
+
+
+
Ask Autopilot
+
+
+ {suggestedPrompts.map((prompt) => (
+ {
+ setPressedPrompt(prompt);
+ onPromptSelect?.(prompt);
+ setTimeout(() => setPressedPrompt(null), 600);
+ }}
+ className={`text-[11px] px-2.5 py-1 rounded-full transition-colors duration-200 ${
+ pressedPrompt === prompt
+ ? "bg-gradient-to-r from-insight-500 to-primary-400 text-white"
+ : "bg-secondary text-secondary-foreground hover:bg-secondary/80"
+ }`}
+ >
+ {prompt}
+
+ ))}
+
+
+ );
+}
diff --git a/apps/apollo-vertex/templates/dashboard/GlowDevControls.tsx b/apps/apollo-vertex/templates/dashboard/GlowDevControls.tsx
new file mode 100644
index 000000000..036aeb1bb
--- /dev/null
+++ b/apps/apollo-vertex/templates/dashboard/GlowDevControls.tsx
@@ -0,0 +1,224 @@
+"use client";
+
+import { ChevronLeft, ChevronRight } from "lucide-react";
+import { useRef, useState } from "react";
+import type { CardConfig, GlowConfig, LayoutConfig } from "./glow-config";
+import { CardsTab, GlowTab, LayoutTab } from "./dev-controls-tabs";
+import { useDashboardData } from "./DashboardDataProvider";
+import { datasetPresets, type DashboardDataset } from "./dashboard-data";
+
+interface DevControlsProps {
+ glowConfig: GlowConfig;
+ onGlowChange: (config: GlowConfig) => void;
+ cardConfig: CardConfig;
+ onCardChange: (config: CardConfig) => void;
+ layoutConfig: LayoutConfig;
+ onLayoutChange: (config: LayoutConfig) => void;
+}
+
+type Tab = "glow" | "cards" | "layout" | "data";
+
+export function GlowDevControls({
+ glowConfig,
+ onGlowChange,
+ cardConfig,
+ onCardChange,
+ layoutConfig,
+ onLayoutChange,
+}: DevControlsProps) {
+ const [open, setOpen] = useState(false);
+ const [tab, setTab] = useState("glow");
+ const { data, setDataset } = useDashboardData();
+ const fileInputRef = useRef(null);
+ const [uploadError, setUploadError] = useState("");
+ const [uploadedDatasets, setUploadedDatasets] = useState(
+ [],
+ );
+
+ const configMap: Record = {
+ glow: glowConfig,
+ cards: cardConfig,
+ layout: layoutConfig,
+ data: data,
+ };
+ const currentConfig = configMap[tab];
+
+ return (
+ <>
+ {
+ const file = e.target.files?.[0];
+ if (!file) return;
+ setUploadError("");
+ const reader = new FileReader();
+ reader.onload = () => {
+ try {
+ const parsed = JSON.parse(
+ reader.result as string,
+ ) as DashboardDataset;
+ if (!parsed.brandName || !parsed.insightCards) {
+ setUploadError("Invalid format");
+ return;
+ }
+ setUploadedDatasets((prev) => {
+ const exists = prev.some((d) => d.name === parsed.name);
+ return exists
+ ? prev.map((d) => (d.name === parsed.name ? parsed : d))
+ : [...prev, parsed];
+ });
+ setDataset(parsed);
+ } catch {
+ setUploadError("Invalid JSON");
+ }
+ };
+ reader.readAsText(file);
+ e.target.value = "";
+ }}
+ />
+
+ {open && (
+
+
+ setTab("glow")}
+ className={`flex-1 px-2 py-2 text-xs font-medium ${tab === "glow" ? "bg-muted" : ""}`}
+ >
+ Glow
+
+ setTab("cards")}
+ className={`flex-1 px-2 py-2 text-xs font-medium ${tab === "cards" ? "bg-muted" : ""}`}
+ >
+ Cards
+
+ setTab("layout")}
+ className={`flex-1 px-2 py-2 text-xs font-medium ${tab === "layout" ? "bg-muted" : ""}`}
+ >
+ Layout
+
+ setTab("data")}
+ className={`flex-1 px-2 py-2 text-xs font-medium ${tab === "data" ? "bg-muted" : ""}`}
+ >
+ Data
+
+
+
+ {tab === "glow" && (
+
+ )}
+ {tab === "cards" && (
+
+ )}
+ {tab === "layout" && (
+
+ )}
+ {tab === "data" && (
+
+
+ Dataset: {data.name}
+
+
+
Preset
+
v.name === data.name,
+ )?.[0] ?? `uploaded:${data.name}`
+ }
+ onChange={(e) => {
+ const val = e.target.value;
+ if (val.startsWith("uploaded:")) {
+ const name = val.slice("uploaded:".length);
+ const uploaded = uploadedDatasets.find(
+ (d) => d.name === name,
+ );
+ if (uploaded) setDataset(uploaded);
+ } else {
+ const preset = datasetPresets[val];
+ if (preset) setDataset(preset);
+ }
+ }}
+ className="w-full h-7 rounded border bg-background px-1 text-xs"
+ >
+ {Object.entries(datasetPresets).map(([key, val]) => (
+
+ {val.name}
+
+ ))}
+ {uploadedDatasets.map((d) => (
+
+ {d.name} (uploaded)
+
+ ))}
+
+
+
+ {
+ const blob = new Blob([JSON.stringify(data, null, 2)], {
+ type: "application/json",
+ });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement("a");
+ a.href = url;
+ a.download = `dashboard-${data.name.toLowerCase().replace(/\s+/g, "-")}.json`;
+ a.click();
+ URL.revokeObjectURL(url);
+ }}
+ className="flex-1 h-7 rounded border bg-background text-xs hover:bg-muted transition-colors"
+ >
+ Download
+
+ fileInputRef.current?.click()}
+ className="flex-1 h-7 rounded border bg-background text-xs hover:bg-muted transition-colors"
+ >
+ Upload
+
+
+ {uploadError && (
+
+ {uploadError}
+
+ )}
+
+ )}
+
+
Config:
+
+ {JSON.stringify(currentConfig, null, 2)}
+
+
+
+
+ )}
+
setOpen(!open)}
+ className="self-center h-8 w-5 bg-popover border rounded-r flex items-center justify-center shadow-md"
+ >
+ {open ? (
+
+ ) : (
+
+ )}
+
+
+ >
+ );
+}
diff --git a/apps/apollo-vertex/templates/dashboard/InsightGrid.tsx b/apps/apollo-vertex/templates/dashboard/InsightGrid.tsx
new file mode 100644
index 000000000..e57ca42a2
--- /dev/null
+++ b/apps/apollo-vertex/templates/dashboard/InsightGrid.tsx
@@ -0,0 +1,508 @@
+"use client";
+
+import { useEffect, useState } from "react";
+import { ArrowUpRight, Maximize2, Minimize2 } from "lucide-react";
+import { Card, CardAction, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import {
+ cardBgStyle,
+ getInsightCardClasses,
+ type CardConfig,
+ type InsightCardConfig,
+ type LayoutConfig,
+} from "./glow-config";
+import { InsightCardBody } from "./insight-card-renderers";
+import { useDashboardData } from "./DashboardDataProvider";
+import {
+ type DrilldownTab,
+ drilldownTabs,
+ DrilldownTabContent,
+ AutopilotPrompts,
+} from "./ExpandedInsightContent";
+
+const sizeToFr: Record = { sm: "1fr", md: "2fr", lg: "1fr" };
+
+// --- Shared card inner content ---
+
+interface InsightCardInnerProps {
+ cfg: InsightCardConfig;
+ cardIndex: number;
+ shared: string;
+ cards: CardConfig;
+ isExpanding: boolean;
+ isThis: boolean;
+ phase: ExpandPhase;
+ viewMode: "desktop" | "compact" | "stacked";
+ drilldownTab: DrilldownTab;
+ onDrilldownTabChange: (tab: DrilldownTab) => void;
+ onExpandClick: () => void;
+ onAutopilotOpen?: () => void;
+ isAutopilotActive?: boolean;
+ className?: string;
+ style?: React.CSSProperties;
+}
+
+function InsightCardInner({
+ cfg,
+ cardIndex,
+ shared,
+ cards,
+ isExpanding,
+ isThis,
+ phase,
+ viewMode,
+ onExpandClick,
+ onAutopilotOpen,
+ drilldownTab,
+ onDrilldownTabChange,
+ isAutopilotActive = false,
+ className = "",
+ style,
+}: InsightCardInnerProps) {
+ const { data } = useDashboardData();
+ const cardTitle = data.insightCards[cardIndex]?.title ?? cfg.content.title;
+ const hasDrilldown = cfg.content.chartType === "horizontal-bars";
+ const isExpandedWithDrilldown =
+ isThis && isExpanding && hasDrilldown && phase === "full";
+ const classes = getInsightCardClasses(cfg.content, viewMode);
+ const isInteractive = cfg.interaction !== "static";
+
+ return (
+
+
+
+ {cardTitle}
+
+ {isInteractive && cfg.interaction === "expand" && (
+
+
+ {onAutopilotOpen && (
+
{
+ e.stopPropagation();
+ onAutopilotOpen();
+ }}
+ className={`size-7 rounded-md flex items-center justify-center transition-all ${
+ isAutopilotActive
+ ? "bg-gradient-to-br from-insight-500 to-primary-400 text-white"
+ : "text-muted-foreground hover:text-foreground hover:bg-muted/50"
+ }`}
+ >
+ {isAutopilotActive ? (
+
+ ) : (
+ <>
+
+
+ >
+ )}
+
+ )}
+
+ {isThis && isExpanding ? (
+
+ ) : (
+
+ )}
+
+
+
+ )}
+ {isInteractive && cfg.interaction === "navigate" && !isThis && (
+
+
+
+
+
+ )}
+ {/* Drilldown tabs — below title when expanded */}
+ {isThis &&
+ isExpanding &&
+ hasDrilldown &&
+ (phase === "height" || phase === "full") &&
+ (() => {
+ const visibleTabs = drilldownTabs.slice(0, 4);
+ const overflowTabs = drilldownTabs.slice(4);
+ const isOverflowActive = overflowTabs.some(
+ (t) => t.key === drilldownTab,
+ );
+ return (
+
+ {visibleTabs.map((tab) => (
+
onDrilldownTabChange(tab.key)}
+ className={`px-2 py-1 text-xs rounded transition-colors font-medium ${
+ drilldownTab === tab.key
+ ? "bg-muted dark:bg-foreground/15"
+ : "text-muted-foreground hover:text-foreground hover:bg-muted dark:hover:bg-foreground/15"
+ }`}
+ >
+ {tab.label}
+
+ ))}
+ {overflowTabs.length > 0 && (
+
+
{
+ if (e.target.value)
+ onDrilldownTabChange(e.target.value as DrilldownTab);
+ }}
+ className={`appearance-none px-2 py-1 text-xs rounded transition-colors cursor-pointer bg-transparent pr-5 ${
+ isOverflowActive
+ ? "bg-muted font-medium"
+ : "font-medium text-muted-foreground hover:text-foreground hover:bg-muted/50"
+ }`}
+ >
+ {!isOverflowActive && More… }
+ {overflowTabs.map((tab) => (
+
+ {tab.label}
+
+ ))}
+
+
+
+
+
+ )}
+
+ );
+ })()}
+
+ {isExpandedWithDrilldown ? (
+ /* Expanded with drilldown — unified layout for all tabs */
+
+
+
+ {drilldownTab === "overview" ? (
+
+ ) : (
+
+ )}
+
+
+
+
onAutopilotOpen?.()} />
+
+
+ ) : (
+ /* Default card content — not expanded or no drilldown */
+
+
+
+ )}
+ {/* Non-drilldown expanded content (other card types) */}
+ {isThis &&
+ isExpanding &&
+ !hasDrilldown &&
+ (phase === "height" || phase === "full") && (
+
+ {phase === "full" ? (
+
+
+ Additional content
+
+
+ ) : (
+
+ )}
+
+ )}
+
+ );
+}
+
+// --- Main grid ---
+
+type ExpandPhase = "idle" | "width" | "height" | "full";
+
+export function InsightGrid({
+ layout,
+ shared,
+ cards,
+ viewMode = "desktop",
+ onAutopilotOpen,
+ autopilotActiveIdx,
+}: {
+ layout: LayoutConfig;
+ shared: string;
+ cards: CardConfig;
+ viewMode?: "desktop" | "compact" | "stacked";
+ onAutopilotOpen?: (sourceTitle: string, idx: number) => void;
+ autopilotActiveIdx?: number | null;
+}) {
+ const { data } = useDashboardData();
+ const [expandedIdx, setExpandedIdx] = useState(null);
+ const [phase, setPhase] = useState("idle");
+ const [drilldownTab, setDrilldownTab] = useState("overview");
+
+ useEffect(() => {
+ if (expandedIdx === null) {
+ setPhase("idle");
+ setDrilldownTab("overview");
+ return;
+ }
+ requestAnimationFrame(() => setPhase("width"));
+ const t1 = setTimeout(() => setPhase("height"), 300);
+ const t2 = setTimeout(() => setPhase("full"), 600);
+ return () => {
+ clearTimeout(t1);
+ clearTimeout(t2);
+ };
+ }, [expandedIdx]);
+
+ const visibleCards = layout.insightCards
+ .map((cfg, i) => {
+ const dataCard = data.insightCards[i];
+ const merged = dataCard
+ ? {
+ ...cfg,
+ size: dataCard.size ?? cfg.size,
+ interaction: dataCard.interaction ?? cfg.interaction,
+ content: {
+ ...cfg.content,
+ title: dataCard.title ?? cfg.content.title,
+ },
+ }
+ : cfg;
+ return { cfg: merged, idx: i };
+ })
+ .filter(({ cfg }) => cfg.visible);
+ const rows: (typeof visibleCards)[] = [];
+ for (let i = 0; i < visibleCards.length; i += 2) {
+ rows.push(visibleCards.slice(i, i + 2));
+ }
+
+ const isExpanding = expandedIdx !== null;
+ const expandedRow = isExpanding
+ ? rows.findIndex((row) => row.some(({ idx }) => idx === expandedIdx))
+ : -1;
+
+ const handleClick = (cfg: InsightCardConfig, idx: number) => {
+ if (cfg.interaction === "expand") {
+ setExpandedIdx(expandedIdx === idx ? null : idx);
+ }
+ };
+
+ // Build grid-template-rows
+ let rowTemplates: string[];
+ if (viewMode === "compact") {
+ rowTemplates = visibleCards.map(({ idx }) => {
+ if (!isExpanding) return "1fr";
+ if (idx === expandedIdx) return "1fr";
+ if (phase !== "idle") return "0fr";
+ return "1fr";
+ });
+ } else {
+ rowTemplates = rows.map((_, rowIndex) => {
+ const isOtherRow = isExpanding && rowIndex !== expandedRow;
+ if (isOtherRow && (phase === "height" || phase === "full")) return "0fr";
+ return "1fr";
+ });
+ }
+
+ const sharedProps = {
+ shared,
+ cards,
+ isExpanding,
+ phase,
+ viewMode,
+ drilldownTab,
+ onDrilldownTabChange: setDrilldownTab,
+ };
+
+ return (
+
+ {viewMode === "compact"
+ ? visibleCards.map(({ cfg, idx }) => {
+ const isThis = idx === expandedIdx;
+ const isOther = isExpanding && !isThis;
+ return (
+
+ handleClick(cfg, idx)}
+ onAutopilotOpen={
+ onAutopilotOpen
+ ? () =>
+ onAutopilotOpen(
+ data.insightCards[idx]?.title ?? cfg.content.title,
+ idx,
+ )
+ : undefined
+ }
+ isAutopilotActive={autopilotActiveIdx === idx}
+ className="h-full"
+ />
+
+ );
+ })
+ : rows.map((row, rowIndex) => {
+ const isRowWithExpanded = rowIndex === expandedRow;
+ const isOtherRow = isExpanding && !isRowWithExpanded;
+ const cols = row
+ .map(({ cfg, idx }) => {
+ if (!isExpanding)
+ return cfg.size === "lg" ? "1fr" : sizeToFr[cfg.size];
+ if (idx === expandedIdx)
+ return phase === "idle"
+ ? cfg.size === "lg"
+ ? "1fr"
+ : sizeToFr[cfg.size]
+ : "1fr";
+ if (isRowWithExpanded)
+ return phase === "idle"
+ ? cfg.size === "lg"
+ ? "1fr"
+ : sizeToFr[cfg.size]
+ : "0fr";
+ return cfg.size === "lg" ? "1fr" : sizeToFr[cfg.size];
+ })
+ .join(" ");
+ return (
+
idx).join("-")}
+ className="grid transition-all duration-300 ease-in-out overflow-hidden min-h-0"
+ style={
+ {
+ gridTemplateColumns: cols,
+ gap: isRowWithExpanded && phase !== "idle" ? 0 : layout.gap,
+ opacity:
+ isOtherRow && (phase === "height" || phase === "full")
+ ? 0
+ : 1,
+ } as React.CSSProperties
+ }
+ >
+ {row.map(({ cfg, idx }) => {
+ const isThis = idx === expandedIdx;
+ const isSibling = isExpanding && !isThis && isRowWithExpanded;
+ return (
+ handleClick(cfg, idx)}
+ onAutopilotOpen={
+ onAutopilotOpen
+ ? () =>
+ onAutopilotOpen(
+ data.insightCards[idx]?.title ??
+ cfg.content.title,
+ idx,
+ )
+ : undefined
+ }
+ isAutopilotActive={autopilotActiveIdx === idx}
+ style={{
+ opacity: isSibling && phase !== "idle" ? 0 : 1,
+ transform:
+ isSibling && phase !== "idle"
+ ? "scale(0.95)"
+ : "scale(1)",
+ }}
+ />
+ );
+ })}
+
+ );
+ })}
+
+ );
+}
diff --git a/apps/apollo-vertex/templates/dashboard/PromptBar.tsx b/apps/apollo-vertex/templates/dashboard/PromptBar.tsx
new file mode 100644
index 000000000..9bd863f88
--- /dev/null
+++ b/apps/apollo-vertex/templates/dashboard/PromptBar.tsx
@@ -0,0 +1,193 @@
+"use client";
+
+import { useState } from "react";
+import { MessagesSquare, Minimize2 } from "lucide-react";
+import { Badge } from "@/components/ui/badge";
+import type { CardConfig, CardGradient } from "./glow-config";
+import { useDashboardData } from "./DashboardDataProvider";
+
+function cardBgStyle(
+ bg: string,
+ opacity: number,
+ gradient: CardGradient,
+): React.CSSProperties {
+ if (gradient.enabled) {
+ const alpha = gradient.opacity / 100;
+ return {
+ "--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",
+ } as React.CSSProperties;
+ }
+ const value =
+ bg === "white"
+ ? `rgba(255,255,255,${opacity / 100})`
+ : `color-mix(in srgb, var(--${bg}) ${opacity}%, transparent)`;
+ return { "--card-bg-override": value } as React.CSSProperties;
+}
+
+export function PromptBar({
+ shared,
+ cards,
+ isExpanded = false,
+ onSubmit,
+ onExpand,
+ onCollapse,
+}: {
+ shared: string;
+ cards: CardConfig;
+ isExpanded?: boolean;
+ onSubmit?: (query: string) => void;
+ onExpand?: () => void;
+ onCollapse?: () => void;
+}) {
+ const { data } = useDashboardData();
+ const [value, setValue] = useState("");
+ const hasInput = value.trim().length > 0;
+
+ const handleSubmit = () => {
+ if (hasInput && onSubmit) {
+ onSubmit(value);
+ }
+ };
+
+ const handleChipClick = (suggestion: string) => {
+ setValue(suggestion);
+ onSubmit?.(suggestion);
+ };
+
+ return (
+
+ {/* Expanded response area */}
+ {isExpanded && (
+
+
+
+
+
+
+ Autopilot
+
+
+ {onCollapse && (
+
+
+
+ )}
+
+
+
+ Responses will appear here
+
+
+
+
+ )}
+ {/* Suggestion badges — hidden when expanded */}
+ {!isExpanded && (
+
+
+
+
+ handleChipClick(
+ data.promptSuggestions[0] ?? "Show me top risk factors",
+ )
+ }
+ >
+ {data.promptSuggestions[0] ?? "Show me top risk factors"}
+
+
+ handleChipClick(
+ data.promptSuggestions[1] ??
+ "Compare Q1 vs Q2 performance",
+ )
+ }
+ >
+ {data.promptSuggestions[1] ?? "Compare Q1 vs Q2 performance"}
+
+
+
+
+ )}
+ {/* Input bar */}
+
+
setValue(e.target.value)}
+ onKeyDown={(e) => {
+ if (e.key === "Enter") handleSubmit();
+ }}
+ placeholder={data.promptPlaceholder}
+ className="flex-1 bg-transparent text-sm outline-none placeholder:text-muted-foreground"
+ />
+
+
e.preventDefault()}
+ onClick={onExpand}
+ className="size-8 rounded-md flex items-center justify-center text-muted-foreground hover:text-foreground hover:bg-muted/50 transition-all"
+ aria-label="Open chat"
+ >
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
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/dev-controls-primitives.tsx b/apps/apollo-vertex/templates/dashboard/dev-controls-primitives.tsx
new file mode 100644
index 000000000..a779dd432
--- /dev/null
+++ b/apps/apollo-vertex/templates/dashboard/dev-controls-primitives.tsx
@@ -0,0 +1,113 @@
+"use client";
+
+export function Slider({
+ label,
+ value,
+ min,
+ max,
+ step,
+ onChange,
+ displayValue,
+}: {
+ label: string;
+ value: number;
+ min: number;
+ max: number;
+ step: number;
+ onChange: (v: number) => void;
+ displayValue?: string;
+}) {
+ return (
+
+
+ {label}
+ {displayValue ?? value}
+
+
onChange(Number(e.target.value))}
+ className="w-full h-1 accent-primary"
+ />
+
+ );
+}
+
+export function SelectControl({
+ label,
+ value,
+ options,
+ onChange,
+}: {
+ label: string;
+ value: string;
+ options: { label: string; value: string }[];
+ onChange: (v: string) => void;
+}) {
+ return (
+
+
{label}
+
onChange(e.target.value)}
+ className="w-full h-7 rounded border bg-background px-1 text-xs"
+ >
+ {options.map((opt) => (
+
+ {opt.label}
+
+ ))}
+
+
+ );
+}
+
+export function Toggle({
+ label,
+ checked,
+ onChange,
+}: {
+ label: string;
+ checked: boolean;
+ onChange: (v: boolean) => void;
+}) {
+ return (
+
+ {label}
+ onChange(e.target.checked)}
+ className="accent-primary"
+ />
+
+ );
+}
+
+export function TextInput({
+ label,
+ value,
+ onChange,
+ placeholder,
+}: {
+ label: string;
+ value: string;
+ onChange: (v: string) => void;
+ placeholder?: string;
+}) {
+ return (
+
+
{label}
+
onChange(e.target.value)}
+ className="w-full h-7 rounded border bg-background px-1 text-xs"
+ placeholder={placeholder}
+ />
+
+ );
+}
diff --git a/apps/apollo-vertex/templates/dashboard/dev-controls-tabs.tsx b/apps/apollo-vertex/templates/dashboard/dev-controls-tabs.tsx
new file mode 100644
index 000000000..5a6efbe03
--- /dev/null
+++ b/apps/apollo-vertex/templates/dashboard/dev-controls-tabs.tsx
@@ -0,0 +1,398 @@
+"use client";
+
+import {
+ bgColorOptions,
+ cardTypeOptions,
+ chartTypeOptions,
+ containerBgOptions,
+ insightOptions,
+ interactionOptions,
+ primaryOptions,
+ sizeOptions,
+ type CardConfig,
+ type CardGradient,
+ type CardInteraction,
+ type CardSize,
+ type ChartType,
+ type GlowConfig,
+ type InsightCardConfig,
+ type InsightCardType,
+ type LayoutConfig,
+} from "./glow-config";
+import {
+ SelectControl,
+ Slider,
+ TextInput,
+ Toggle,
+} from "./dev-controls-primitives";
+
+function GradientSection({
+ gradient,
+ onChange,
+}: {
+ gradient: CardGradient;
+ onChange: (g: CardGradient) => void;
+}) {
+ const update = (partial: Partial) =>
+ onChange({ ...gradient, ...partial });
+
+ return (
+
+ update({ enabled: v })}
+ />
+ {gradient.enabled && (
+ <>
+ update({ start: v })}
+ />
+ update({ end: v })}
+ />
+ update({ angle: v })}
+ displayValue={`${gradient.angle}°`}
+ />
+ update({ opacity: v })}
+ displayValue={`${gradient.opacity}%`}
+ />
+ >
+ )}
+
+ );
+}
+
+export function GlowTab({
+ config,
+ onChange,
+}: {
+ config: GlowConfig;
+ onChange: (c: GlowConfig) => void;
+}) {
+ const update = (partial: Partial) =>
+ onChange({ ...config, ...partial });
+
+ return (
+
+ update({ start: v })}
+ />
+ update({ end: v })}
+ />
+ update({ containerOpacity: v })}
+ displayValue={`${config.containerOpacity}%`}
+ />
+ update({ fillOpacity: v })}
+ />
+ update({ startStopOpacity: v })}
+ />
+ update({ endStopOpacity: v })}
+ />
+ update({ endOffset: v })}
+ />
+
+ );
+}
+
+export function CardsTab({
+ config,
+ onChange,
+}: {
+ config: CardConfig;
+ onChange: (c: CardConfig) => void;
+}) {
+ const update = (partial: Partial) =>
+ onChange({ ...config, ...partial });
+
+ return (
+
+
Overview Card
+
update({ overviewBg: v })}
+ />
+ update({ overviewOpacity: v })}
+ displayValue={`${config.overviewOpacity}%`}
+ />
+ update({ overviewGradient: g })}
+ />
+
+
+ Insight Cards
+
+ update({ insightBg: v })}
+ />
+ update({ insightOpacity: v })}
+ displayValue={`${config.insightOpacity}%`}
+ />
+ update({ insightGradient: g })}
+ />
+
+ Prompt Bar
+ update({ promptBg: v })}
+ />
+ update({ promptOpacity: v })}
+ displayValue={`${config.promptOpacity}%`}
+ />
+ update({ promptGradient: g })}
+ />
+
+ Shared
+ update({ borderVisible: v })}
+ />
+ update({ backdropBlur: v })}
+ />
+
+ );
+}
+
+export function LayoutTab({
+ config,
+ onChange,
+}: {
+ config: LayoutConfig;
+ onChange: (c: LayoutConfig) => void;
+}) {
+ const update = (partial: Partial) =>
+ onChange({ ...config, ...partial });
+
+ const updateInsightCard = (
+ index: number,
+ partial: Partial,
+ ) => {
+ const cards = [...config.insightCards] as [
+ InsightCardConfig,
+ InsightCardConfig,
+ InsightCardConfig,
+ InsightCardConfig,
+ ];
+ cards[index] = { ...cards[index], ...partial };
+ update({ insightCards: cards });
+ };
+
+ return (
+
+
update({ containerBg: v })}
+ />
+ update({ gap: v })}
+ displayValue={`${config.gap}px`}
+ />
+ update({ padding: v })}
+ displayValue={`${config.padding}px`}
+ />
+ Left Column
+ update({ overviewRatio: v })}
+ />
+ update({ promptRatio: v })}
+ />
+
+ Insight Cards
+
+ {["Top Left", "Top Right", "Bottom Left", "Bottom Right"].map(
+ (label, i) => (
+
+
+ {label}
+ updateInsightCard(i, { visible: v })}
+ />
+
+ {config.insightCards[i].visible && (
+ <>
+
+ updateInsightCard(i, { size: v as CardSize })
+ }
+ />
+
+ updateInsightCard(i, {
+ content: {
+ ...config.insightCards[i].content,
+ type: v as InsightCardType,
+ },
+ })
+ }
+ />
+ {config.insightCards[i].content.type === "chart" && (
+
+ updateInsightCard(i, {
+ content: {
+ ...config.insightCards[i].content,
+ chartType: v as ChartType,
+ },
+ })
+ }
+ />
+ )}
+
+ updateInsightCard(i, {
+ content: { ...config.insightCards[i].content, title: v },
+ })
+ }
+ />
+
+ updateInsightCard(i, {
+ interaction: v as CardInteraction,
+ })
+ }
+ />
+ {config.insightCards[i].interaction === "navigate" && (
+ updateInsightCard(i, { navigateTo: v })}
+ placeholder="/preview/dashboard/..."
+ />
+ )}
+ >
+ )}
+
+ ),
+ )}
+
+ );
+}
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..c48b6fb8f
--- /dev/null
+++ b/apps/apollo-vertex/templates/dashboard/glow-config.ts
@@ -0,0 +1,267 @@
+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: 85,
+ 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;
+ return {
+ "--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",
+ } as React.CSSProperties;
+ }
+ const value =
+ bg === "white"
+ ? `rgba(255,255,255,${opacity / 100})`
+ : `color-mix(in srgb, var(--${bg}) ${opacity}%, transparent)`;
+ return { "--card-bg-override": value } 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,
+};
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..ff08e13fd
--- /dev/null
+++ b/apps/apollo-vertex/templates/dashboard/insight-card-renderers.tsx
@@ -0,0 +1,445 @@
+"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 "./DashboardDataProvider";
+import type { InsightCardData } from "./dashboard-data";
+
+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}
+
+
+ );
+}
+
+// --- Sample data per card type ---
+
+const sparklinePoints = [4, 7, 5, 9, 6, 8, 12, 10, 14, 11, 15, 13];
+const areaPoints = [3, 5, 4, 8, 6, 9, 7, 11, 10, 14, 12, 16];
+
+// --- Renderers ---
+
+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 DonutContent() {
+ return (
+
+
+
+
+
+
+
+
+ 47%
+
+
+ funded
+
+
+
+
+ );
+}
+
+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 SparklineContent() {
+ const max = Math.max(...sparklinePoints);
+ const h = 40;
+ const w = 120;
+ const step = w / (sparklinePoints.length - 1);
+ const points = sparklinePoints
+ .map((v, i) => `${i * step},${h - (v / max) * h}`)
+ .join(" ");
+
+ return (
+
+ );
+}
+
+function AreaContent() {
+ const max = Math.max(...areaPoints);
+ const h = 40;
+ const w = 120;
+ const step = w / (areaPoints.length - 1);
+ const linePoints = areaPoints
+ .map((v, i) => `${i * step},${h - (v / max) * h}`)
+ .join(" ");
+ const areaPath = `0,${h} ${linePoints} ${w},${h}`;
+
+ return (
+
+ );
+}
+
+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 ;
+}