From b8944d0f358e6efc5d4e049aaf840e5cd411cdfa Mon Sep 17 00:00:00 2001 From: Peter Vachon Date: Fri, 20 Mar 2026 17:39:42 -0400 Subject: [PATCH 01/44] chore(apollo-vertex): Add Layout menu item --- apps/apollo-vertex/app/_meta.ts | 1 + apps/apollo-vertex/app/layouts/_meta.ts | 3 +++ apps/apollo-vertex/app/layouts/dashboard/page.mdx | 3 +++ apps/apollo-vertex/next-env.d.ts | 2 +- 4 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 apps/apollo-vertex/app/layouts/_meta.ts create mode 100644 apps/apollo-vertex/app/layouts/dashboard/page.mdx 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/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..0fddd1fda --- /dev/null +++ b/apps/apollo-vertex/app/layouts/dashboard/page.mdx @@ -0,0 +1,3 @@ +# Dashboard + +Dashboard layout documentation. diff --git a/apps/apollo-vertex/next-env.d.ts b/apps/apollo-vertex/next-env.d.ts index 9edff1c7c..c4b7818fb 100644 --- a/apps/apollo-vertex/next-env.d.ts +++ b/apps/apollo-vertex/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -import "./.next/types/routes.d.ts"; +import "./.next/dev/types/routes.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. From c57705c5711d704c6933d60fb1602b0707f6b821 Mon Sep 17 00:00:00 2001 From: Peter Vachon Date: Fri, 20 Mar 2026 18:46:50 -0400 Subject: [PATCH 02/44] chore(apollo-vertex): General layout --- .../app/layouts/dashboard/page.mdx | 18 +- apps/apollo-vertex/next-env.d.ts | 2 +- .../templates/dashboard/DashboardCards.tsx | 288 ++++++++++++++++++ .../templates/dashboard/DashboardContent.tsx | 148 +++++++++ .../templates/dashboard/DashboardRoutes.tsx | 59 ++++ .../dashboard/DashboardShellWrapper.tsx | 53 ++++ .../templates/dashboard/DashboardTemplate.tsx | 88 ++++++ .../dashboard/DashboardTemplateDynamic.tsx | 16 + 8 files changed, 670 insertions(+), 2 deletions(-) create mode 100644 apps/apollo-vertex/templates/dashboard/DashboardCards.tsx create mode 100644 apps/apollo-vertex/templates/dashboard/DashboardContent.tsx create mode 100644 apps/apollo-vertex/templates/dashboard/DashboardRoutes.tsx create mode 100644 apps/apollo-vertex/templates/dashboard/DashboardShellWrapper.tsx create mode 100644 apps/apollo-vertex/templates/dashboard/DashboardTemplate.tsx create mode 100644 apps/apollo-vertex/templates/dashboard/DashboardTemplateDynamic.tsx diff --git a/apps/apollo-vertex/app/layouts/dashboard/page.mdx b/apps/apollo-vertex/app/layouts/dashboard/page.mdx index 0fddd1fda..847f0ca12 100644 --- a/apps/apollo-vertex/app/layouts/dashboard/page.mdx +++ b/apps/apollo-vertex/app/layouts/dashboard/page.mdx @@ -1,3 +1,19 @@ +import { DashboardTemplate } from '@/templates/dashboard/DashboardTemplateDynamic'; +import { PreviewFullScreen } from '@/app/components/preview-full-screen'; + # Dashboard -Dashboard layout documentation. +Dashboard layout patterns using Card tiles in configurable grid arrangements. +Use the layout toggle inside the preview to switch between Executive, Operational, and Analytics configurations. + +## Sidebar Shell + + + + + +## Minimal Header Shell + + + + diff --git a/apps/apollo-vertex/next-env.d.ts b/apps/apollo-vertex/next-env.d.ts index c4b7818fb..9edff1c7c 100644 --- a/apps/apollo-vertex/next-env.d.ts +++ b/apps/apollo-vertex/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -import "./.next/dev/types/routes.d.ts"; +import "./.next/types/routes.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. 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) => ( +
+
+ {bar.label} +
+ ))} +
+ + + ); +} + +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..055866f2a --- /dev/null +++ b/apps/apollo-vertex/templates/dashboard/DashboardContent.tsx @@ -0,0 +1,148 @@ +"use client"; + +import { useState } from "react"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; + +// --- Layout type --- + +type LayoutType = "executive" | "operational" | "analytics"; + +// --- Layout renderers --- + +function ExecutiveLayout() { + return ( +
+ {/* Left half — 80/20 split */} +
+ + + Overview + + +

Total revenue this quarter: $2.4M (+14%)

+

Active users: 12,847 across 3 regions

+

Top performing segment: Enterprise ($1.8M)

+

Pipeline value: $4.2M (68 deals)

+

Avg. deal cycle: 32 days (-4 days)

+
+
+ + + + Conversational Prompt Bar + + + +
+ Ask a question about your data... +
+
+
+
+ {/* Right half — 2x2 grid */} +
+ + + Insight + + +

Churn risk detected for 3 accounts

+

Estimated impact: $120K ARR

+
+
+ + + Insight + + +

Q1 target: 85% achieved

+

15 days remaining in quarter

+
+
+ + + Insight + + +

Support tickets down 12% WoW

+

Avg. resolution: 4.2 hrs

+
+
+ + + Insight + + +

3 automations flagged for review

+

Success rate: 98.1%

+
+
+
+
+ ); +} + +function OperationalLayout() { + return ( +
+ Operational layout — coming soon +
+ ); +} + +function AnalyticsLayout() { + return ( +
+ Analytics layout — coming soon +
+ ); +} + +// --- Main component --- + +const layoutLabels: Record = { + executive: "Executive", + operational: "Operational", + analytics: "Analytics", +}; + +const layoutDescriptions: Record = { + executive: "High-level overview with KPIs, summary table, and activity chart", + operational: + "Detailed operational view with data table, activity feed, and pipeline", + analytics: + "Data-focused layout with charts, compliance metrics, and detailed table", +}; + +export function DashboardContent() { + const [layout, setLayout] = useState("executive"); + + return ( +
+ {/* Header with layout toggle */} +
+
+

Dashboard

+

+ {layoutDescriptions[layout]} +

+
+ setLayout(v as LayoutType)}> + + {(Object.keys(layoutLabels) as LayoutType[]).map((key) => ( + + {layoutLabels[key]} + + ))} + + +
+ + {/* Layout content */} + {layout === "executive" && } + {layout === "operational" && } + {layout === "analytics" && } +
+ ); +} 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..3ba37e992 --- /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 { 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 }, +); From 0aa56c1bce664843fdcf77dffd4d54e017f42d20 Mon Sep 17 00:00:00 2001 From: Peter Vachon Date: Sat, 21 Mar 2026 06:01:12 -0400 Subject: [PATCH 03/44] chore(apollo-vertex): Base setup for glow --- .../templates/dashboard/DashboardContent.tsx | 51 ++++++++------ .../templates/dashboard/DashboardGlow.tsx | 67 +++++++++++++++++++ 2 files changed, 96 insertions(+), 22 deletions(-) create mode 100644 apps/apollo-vertex/templates/dashboard/DashboardGlow.tsx diff --git a/apps/apollo-vertex/templates/dashboard/DashboardContent.tsx b/apps/apollo-vertex/templates/dashboard/DashboardContent.tsx index 055866f2a..2bc5f15a4 100644 --- a/apps/apollo-vertex/templates/dashboard/DashboardContent.tsx +++ b/apps/apollo-vertex/templates/dashboard/DashboardContent.tsx @@ -3,6 +3,7 @@ import { useState } from "react"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { DashboardGlow } from "./DashboardGlow"; // --- Layout type --- @@ -119,30 +120,36 @@ export function DashboardContent() { const [layout, setLayout] = useState("executive"); return ( -
- {/* Header with layout toggle */} -
-
-

Dashboard

-

- {layoutDescriptions[layout]} -

+
+ +
+ {/* Header with layout toggle */} +
+
+

Dashboard

+

+ {layoutDescriptions[layout]} +

+
+ setLayout(v as LayoutType)} + > + + {(Object.keys(layoutLabels) as LayoutType[]).map((key) => ( + + {layoutLabels[key]} + + ))} + +
- setLayout(v as LayoutType)}> - - {(Object.keys(layoutLabels) as LayoutType[]).map((key) => ( - - {layoutLabels[key]} - - ))} - - -
- {/* Layout content */} - {layout === "executive" && } - {layout === "operational" && } - {layout === "analytics" && } + {/* Layout content */} + {layout === "executive" && } + {layout === "operational" && } + {layout === "analytics" && } +
); } diff --git a/apps/apollo-vertex/templates/dashboard/DashboardGlow.tsx b/apps/apollo-vertex/templates/dashboard/DashboardGlow.tsx new file mode 100644 index 000000000..2041a3980 --- /dev/null +++ b/apps/apollo-vertex/templates/dashboard/DashboardGlow.tsx @@ -0,0 +1,67 @@ +interface DashboardGlowProps { + startColor?: string; + endColor?: string; + className?: string; +} + +export function DashboardGlow({ + startColor = "#6C5AEF", + endColor = "#69C7DD", + className, +}: DashboardGlowProps) { + return ( +
+ + + + + + + + + + + + + + + + +
+ ); +} From 5f84fdaa082d68e081786440cdd21feea29e0148 Mon Sep 17 00:00:00 2001 From: Peter Vachon Date: Sun, 22 Mar 2026 06:46:48 -0400 Subject: [PATCH 04/44] chore(apollo-vertex): Devtool setup for canvas --- apps/apollo-vertex/registry.json | 32 +- .../templates/dashboard/DashboardContent.tsx | 243 ++++++++++--- .../templates/dashboard/DashboardGlow.tsx | 175 ++++++--- .../templates/dashboard/GlowDevControls.tsx | 96 +++++ .../dashboard/dev-controls-primitives.tsx | 88 +++++ .../templates/dashboard/dev-controls-tabs.tsx | 339 ++++++++++++++++++ .../templates/dashboard/glow-config.ts | 136 +++++++ 7 files changed, 1000 insertions(+), 109 deletions(-) create mode 100644 apps/apollo-vertex/templates/dashboard/GlowDevControls.tsx create mode 100644 apps/apollo-vertex/templates/dashboard/dev-controls-primitives.tsx create mode 100644 apps/apollo-vertex/templates/dashboard/dev-controls-tabs.tsx create mode 100644 apps/apollo-vertex/templates/dashboard/glow-config.ts 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/templates/dashboard/DashboardContent.tsx b/apps/apollo-vertex/templates/dashboard/DashboardContent.tsx index 2bc5f15a4..6f2fa0b7b 100644 --- a/apps/apollo-vertex/templates/dashboard/DashboardContent.tsx +++ b/apps/apollo-vertex/templates/dashboard/DashboardContent.tsx @@ -4,21 +4,166 @@ import { useState } from "react"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { DashboardGlow } from "./DashboardGlow"; +import { + defaultDarkCards, + defaultDarkGlow, + defaultLayout, + type CardConfig, + type CardGradient, + type GlowConfig, + type LayoutConfig, +} from "./glow-config"; +import { GlowDevControls } from "./GlowDevControls"; // --- Layout type --- type LayoutType = "executive" | "operational" | "analytics"; +// --- Helpers --- + +function resolveColor(value: string): string { + if (typeof document === "undefined") return value; + const match = value.match(/^var\(--(.+)\)$/); + if (!match) return value; + const computed = getComputedStyle(document.documentElement).getPropertyValue( + `--${match[1]}`, + ); + return computed.trim() || value; +} + +function cardBgStyle( + bg: string, + opacity: number, + gradient: CardGradient, +): React.CSSProperties { + if (gradient.enabled) { + const start = resolveColor(gradient.start); + const end = resolveColor(gradient.end); + const alpha = gradient.opacity / 100; + return { + "--card-bg-override": `linear-gradient(${gradient.angle}deg, color-mix(in srgb, ${start} ${alpha * 100}%, transparent), color-mix(in srgb, ${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; +} + +// --- Helpers --- + +const sizeToFr: Record = { sm: "1fr", md: "2fr", lg: "1fr" }; + +const insightData = [ + { t: "Churn risk detected for 3 accounts", s: "Estimated impact: $120K ARR" }, + { t: "Q1 target: 85% achieved", s: "15 days remaining in quarter" }, + { t: "Support tickets down 12% WoW", s: "Avg. resolution: 4.2 hrs" }, + { t: "3 automations flagged for review", s: "Success rate: 98.1%" }, +]; + +function InsightGrid({ + layout, + shared, + cards, +}: { + layout: LayoutConfig; + shared: string; + cards: CardConfig; +}) { + const gapStyle = { gap: `${layout.gap}px` }; + const visibleCards = insightData + .map((item, i) => ({ item, cfg: layout.insightCards[i] })) + .filter(({ cfg }) => cfg.visible); + + // Group into rows of 2 + const rows: (typeof visibleCards)[] = []; + for (let i = 0; i < visibleCards.length; i += 2) { + rows.push(visibleCards.slice(i, i + 2)); + } + + return ( +
+ {rows.map((row) => { + const cols = row + .map(({ cfg }) => (cfg.size === "lg" ? "1fr" : sizeToFr[cfg.size])) + .join(" "); + return ( +
item.t).join()} + className="grid flex-1" + style={{ ...gapStyle, gridTemplateColumns: cols }} + > + {row.map(({ item, cfg }) => { + const spanClass = cfg.size === "lg" && row.length === 1 ? "" : ""; + return ( + + + + Insight + + + +

{item.t}

+

{item.s}

+
+
+ ); + })} +
+ ); + })} +
+ ); +} + // --- Layout renderers --- -function ExecutiveLayout() { +function ExecutiveLayout({ + cards, + layout, +}: { + cards: CardConfig; + layout: LayoutConfig; +}) { + 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 ( -
- {/* Left half — 80/20 split */} -
- +
+ {/* Left half */} +
+ - Overview + + Overview +

Total revenue this quarter: $2.4M (+14%)

@@ -28,9 +173,17 @@ function ExecutiveLayout() {

Avg. deal cycle: 32 days (-4 days)

- + - + Conversational Prompt Bar @@ -41,45 +194,8 @@ function ExecutiveLayout() {
- {/* Right half — 2x2 grid */} -
- - - Insight - - -

Churn risk detected for 3 accounts

-

Estimated impact: $120K ARR

-
-
- - - Insight - - -

Q1 target: 85% achieved

-

15 days remaining in quarter

-
-
- - - Insight - - -

Support tickets down 12% WoW

-

Avg. resolution: 4.2 hrs

-
-
- - - Insight - - -

3 automations flagged for review

-

Success rate: 98.1%

-
-
-
+ {/* Right half — insight grid */} +
); } @@ -118,15 +234,36 @@ const layoutDescriptions: Record = { export function DashboardContent() { const [layout, setLayout] = useState("executive"); + const [darkGlow, setDarkGlow] = useState(defaultDarkGlow); + const [darkCards, setDarkCards] = useState(defaultDarkCards); + const [layoutCfg, setLayoutCfg] = useState(defaultLayout); return ( -
- -
+
+ + +
{/* Header with layout toggle */}
-

Dashboard

+

Dashboard

{layoutDescriptions[layout]}

@@ -146,7 +283,9 @@ export function DashboardContent() {
{/* Layout content */} - {layout === "executive" && } + {layout === "executive" && ( + + )} {layout === "operational" && } {layout === "analytics" && }
diff --git a/apps/apollo-vertex/templates/dashboard/DashboardGlow.tsx b/apps/apollo-vertex/templates/dashboard/DashboardGlow.tsx index 2041a3980..fd3855d49 100644 --- a/apps/apollo-vertex/templates/dashboard/DashboardGlow.tsx +++ b/apps/apollo-vertex/templates/dashboard/DashboardGlow.tsx @@ -1,67 +1,130 @@ +"use client"; + +import { useEffect, useMemo, useState } from "react"; +import { + defaultDarkGlow, + defaultLightGlow, + type GlowConfig, +} from "./glow-config"; + interface DashboardGlowProps { - startColor?: string; - endColor?: string; className?: string; + darkConfig?: GlowConfig; +} + +function resolveColor(value: string): string { + if (typeof document === "undefined") return value; + const match = value.match(/^var\(--(.+)\)$/); + if (!match) return value; + const computed = getComputedStyle(document.documentElement).getPropertyValue( + `--${match[1]}`, + ); + return computed.trim() || value; +} + +function GlowSvg({ uid, config }: { uid: string; config: GlowConfig }) { + const gradientId = `glow-grad-${uid}`; + const filterId = `glow-blur-${uid}`; + const startColor = resolveColor(config.start); + const endColor = resolveColor(config.end); + + return ( + + + + + + + + + + + + + + + + + ); } -export function DashboardGlow({ - startColor = "#6C5AEF", - endColor = "#69C7DD", - className, -}: DashboardGlowProps) { +let globalCounter = 0; + +export function DashboardGlow({ className, darkConfig }: DashboardGlowProps) { + const light = defaultLightGlow; + const dark = darkConfig ?? defaultDarkGlow; + const [revision, setRevision] = useState(() => ++globalCounter); + + const stableId = useMemo(() => `g${Date.now().toString(36)}`, []); + + useEffect(() => { + setRevision(++globalCounter); + }, [ + dark.start, + dark.end, + dark.startStopOpacity, + dark.endStopOpacity, + dark.endOffset, + dark.fillOpacity, + ]); + return (
- - - - - - - - - - - - - - - - + +
+ {/* Dark mode glow */} +
+ +
); } diff --git a/apps/apollo-vertex/templates/dashboard/GlowDevControls.tsx b/apps/apollo-vertex/templates/dashboard/GlowDevControls.tsx new file mode 100644 index 000000000..ccf31a0dd --- /dev/null +++ b/apps/apollo-vertex/templates/dashboard/GlowDevControls.tsx @@ -0,0 +1,96 @@ +"use client"; + +import { ChevronLeft, ChevronRight } from "lucide-react"; +import { useState } from "react"; +import type { CardConfig, GlowConfig, LayoutConfig } from "./glow-config"; +import { CardsTab, GlowTab, LayoutTab } from "./dev-controls-tabs"; + +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"; + +export function GlowDevControls({ + glowConfig, + onGlowChange, + cardConfig, + onCardChange, + layoutConfig, + onLayoutChange, +}: DevControlsProps) { + const [open, setOpen] = useState(true); + const [tab, setTab] = useState("glow"); + + const configMap = { + glow: glowConfig, + cards: cardConfig, + layout: layoutConfig, + }; + const currentConfig = configMap[tab]; + + return ( +
+ {open && ( +
+
+ + + +
+
+ {tab === "glow" && ( + + )} + {tab === "cards" && ( + + )} + {tab === "layout" && ( + + )} +
+
Config:
+
+                {JSON.stringify(currentConfig, null, 2)}
+              
+
+
+
+ )} + +
+ ); +} 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..a112383da --- /dev/null +++ b/apps/apollo-vertex/templates/dashboard/dev-controls-primitives.tsx @@ -0,0 +1,88 @@ +"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}
+ +
+ ); +} + +export function Toggle({ + label, + checked, + onChange, +}: { + label: string; + checked: boolean; + onChange: (v: boolean) => void; +}) { + return ( + + ); +} 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..d4ebd16d5 --- /dev/null +++ b/apps/apollo-vertex/templates/dashboard/dev-controls-tabs.tsx @@ -0,0 +1,339 @@ +"use client"; + +import { + bgColorOptions, + insightOptions, + primaryOptions, + type CardConfig, + type CardGradient, + type CardSize, + type GlowConfig, + type InsightCardConfig, + type LayoutConfig, +} from "./glow-config"; +import { SelectControl, Slider, 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 })} + /> +
+ ); +} + +const sizeOptions = [ + { label: "Small (1 col)", value: "sm" }, + { label: "Medium (1 col)", value: "md" }, + { label: "Large (full)", value: "lg" }, +]; + +const containerBgOptions = [ + { label: "None", value: "none" }, + ...bgColorOptions, +]; + +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 })} + /> + )} +
+ ), + )} +
+ ); +} 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..1742c9fcc --- /dev/null +++ b/apps/apollo-vertex/templates/dashboard/glow-config.ts @@ -0,0 +1,136 @@ +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-800)", + containerOpacity: 25, + fillOpacity: 1, + startStopOpacity: 1, + endStopOpacity: 0.5, + endOffset: 0.4, +}; + +export type CardSize = "sm" | "md" | "lg"; + +export interface InsightCardConfig { + size: CardSize; + visible: boolean; +} + +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: "md", visible: true }, + { size: "md", visible: true }, + { size: "md", visible: true }, + { size: "md", visible: true }, + ], + 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 bgColorOptions = [ + { label: "white", value: "white" }, + { label: "sidebar", value: "sidebar" }, + { label: "card", value: "card" }, + { label: "background", value: "background" }, + { label: "muted", value: "muted" }, +]; + +export const defaultDarkCards: CardConfig = { + overviewBg: "white", + overviewOpacity: 6, + overviewGradient: { ...defaultGradient }, + insightBg: "white", + insightOpacity: 6, + insightGradient: { ...defaultGradient }, + promptBg: "white", + promptOpacity: 6, + promptGradient: { ...defaultGradient }, + borderVisible: false, + backdropBlur: true, +}; From 30b0803b28348960ae6aa35620f6bb7d9920c584 Mon Sep 17 00:00:00 2001 From: Peter Vachon Date: Sun, 22 Mar 2026 08:43:12 -0400 Subject: [PATCH 05/44] chore(apollo-vertex): Insight card config --- apps/apollo-vertex/next-env.d.ts | 2 +- .../templates/dashboard/DashboardContent.tsx | 60 ++---- .../templates/dashboard/DashboardGlow.tsx | 55 +---- .../templates/dashboard/dev-controls-tabs.tsx | 66 +++++- .../templates/dashboard/glow-config.ts | 101 +++++++-- .../dashboard/insight-card-renderers.tsx | 201 ++++++++++++++++++ 6 files changed, 375 insertions(+), 110 deletions(-) create mode 100644 apps/apollo-vertex/templates/dashboard/insight-card-renderers.tsx diff --git a/apps/apollo-vertex/next-env.d.ts b/apps/apollo-vertex/next-env.d.ts index 9edff1c7c..c4b7818fb 100644 --- a/apps/apollo-vertex/next-env.d.ts +++ b/apps/apollo-vertex/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -import "./.next/types/routes.d.ts"; +import "./.next/dev/types/routes.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/apps/apollo-vertex/templates/dashboard/DashboardContent.tsx b/apps/apollo-vertex/templates/dashboard/DashboardContent.tsx index 6f2fa0b7b..6f8291336 100644 --- a/apps/apollo-vertex/templates/dashboard/DashboardContent.tsx +++ b/apps/apollo-vertex/templates/dashboard/DashboardContent.tsx @@ -8,12 +8,14 @@ import { defaultDarkCards, defaultDarkGlow, defaultLayout, + getInsightCardClasses, type CardConfig, type CardGradient, type GlowConfig, type LayoutConfig, } from "./glow-config"; import { GlowDevControls } from "./GlowDevControls"; +import { InsightCardBody } from "./insight-card-renderers"; // --- Layout type --- @@ -21,27 +23,15 @@ type LayoutType = "executive" | "operational" | "analytics"; // --- Helpers --- -function resolveColor(value: string): string { - if (typeof document === "undefined") return value; - const match = value.match(/^var\(--(.+)\)$/); - if (!match) return value; - const computed = getComputedStyle(document.documentElement).getPropertyValue( - `--${match[1]}`, - ); - return computed.trim() || value; -} - function cardBgStyle( bg: string, opacity: number, gradient: CardGradient, ): React.CSSProperties { if (gradient.enabled) { - const start = resolveColor(gradient.start); - const end = resolveColor(gradient.end); const alpha = gradient.opacity / 100; return { - "--card-bg-override": `linear-gradient(${gradient.angle}deg, color-mix(in srgb, ${start} ${alpha * 100}%, transparent), color-mix(in srgb, ${end} ${alpha * 100}%, transparent))`, + "--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; } @@ -56,13 +46,6 @@ function cardBgStyle( const sizeToFr: Record = { sm: "1fr", md: "2fr", lg: "1fr" }; -const insightData = [ - { t: "Churn risk detected for 3 accounts", s: "Estimated impact: $120K ARR" }, - { t: "Q1 target: 85% achieved", s: "15 days remaining in quarter" }, - { t: "Support tickets down 12% WoW", s: "Avg. resolution: 4.2 hrs" }, - { t: "3 automations flagged for review", s: "Success rate: 98.1%" }, -]; - function InsightGrid({ layout, shared, @@ -73,8 +56,8 @@ function InsightGrid({ cards: CardConfig; }) { const gapStyle = { gap: `${layout.gap}px` }; - const visibleCards = insightData - .map((item, i) => ({ item, cfg: layout.insightCards[i] })) + const visibleCards = layout.insightCards + .map((cfg, i) => ({ cfg, idx: i })) .filter(({ cfg }) => cfg.visible); // Group into rows of 2 @@ -91,17 +74,17 @@ function InsightGrid({ .join(" "); return (
item.t).join()} + key={row.map(({ idx }) => idx).join("-")} className="grid flex-1" style={{ ...gapStyle, gridTemplateColumns: cols }} > - {row.map(({ item, cfg }) => { - const spanClass = cfg.size === "lg" && row.length === 1 ? "" : ""; + {row.map(({ cfg, idx }) => { + const classes = getInsightCardClasses(cfg.content); return ( - Insight + {cfg.content.title} - -

{item.t}

-

{item.s}

+ +
); @@ -224,14 +206,6 @@ const layoutLabels: Record = { analytics: "Analytics", }; -const layoutDescriptions: Record = { - executive: "High-level overview with KPIs, summary table, and activity chart", - operational: - "Detailed operational view with data table, activity feed, and pipeline", - analytics: - "Data-focused layout with charts, compliance metrics, and detailed table", -}; - export function DashboardContent() { const [layout, setLayout] = useState("executive"); const [darkGlow, setDarkGlow] = useState(defaultDarkGlow); @@ -263,9 +237,11 @@ export function DashboardContent() { {/* Header with layout toggle */}
-

Dashboard

-

- {layoutDescriptions[layout]} +

+ UiPath Vertical Solutions +

+

+ {layoutLabels[layout]} Dashboard

- + @@ -86,44 +70,25 @@ function GlowSvg({ uid, config }: { uid: string; config: GlowConfig }) { ); } -let globalCounter = 0; - export function DashboardGlow({ className, darkConfig }: DashboardGlowProps) { const light = defaultLightGlow; const dark = darkConfig ?? defaultDarkGlow; - const [revision, setRevision] = useState(() => ++globalCounter); - - const stableId = useMemo(() => `g${Date.now().toString(36)}`, []); - - useEffect(() => { - setRevision(++globalCounter); - }, [ - dark.start, - dark.end, - dark.startStopOpacity, - dark.endStopOpacity, - dark.endOffset, - dark.fillOpacity, - ]); return (
- {/* Light mode glow */}
- +
- {/* Dark mode glow */}
- +
); diff --git a/apps/apollo-vertex/templates/dashboard/dev-controls-tabs.tsx b/apps/apollo-vertex/templates/dashboard/dev-controls-tabs.tsx index d4ebd16d5..ab4155320 100644 --- a/apps/apollo-vertex/templates/dashboard/dev-controls-tabs.tsx +++ b/apps/apollo-vertex/templates/dashboard/dev-controls-tabs.tsx @@ -2,13 +2,17 @@ import { bgColorOptions, + cardTypeOptions, + chartTypeOptions, insightOptions, primaryOptions, type CardConfig, type CardGradient, type CardSize, + type ChartType, type GlowConfig, type InsightCardConfig, + type InsightCardType, type LayoutConfig, } from "./glow-config"; import { SelectControl, Slider, Toggle } from "./dev-controls-primitives"; @@ -316,7 +320,7 @@ export function LayoutTab({ (label, i) => (
- {label} + {label}
{config.insightCards[i].visible && ( - updateInsightCard(i, { size: v as CardSize })} - /> + <> + + 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, + }, + }) + } + /> + )} +
+
Title
+ + updateInsightCard(i, { + content: { + ...config.insightCards[i].content, + title: e.target.value, + }, + }) + } + className="w-full h-7 rounded border bg-background px-1 text-xs" + /> +
+ )}
), diff --git a/apps/apollo-vertex/templates/dashboard/glow-config.ts b/apps/apollo-vertex/templates/dashboard/glow-config.ts index 1742c9fcc..a89b6e377 100644 --- a/apps/apollo-vertex/templates/dashboard/glow-config.ts +++ b/apps/apollo-vertex/templates/dashboard/glow-config.ts @@ -42,19 +42,29 @@ export const defaultLightGlow: GlowConfig = { export const defaultDarkGlow: GlowConfig = { start: "var(--insight-700)", - end: "var(--primary-800)", - containerOpacity: 25, + end: "var(--primary-600)", + containerOpacity: 45, fillOpacity: 1, startStopOpacity: 1, - endStopOpacity: 0.5, - endOffset: 0.4, + 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"; + +export interface InsightCardContent { + type: InsightCardType; + chartType: ChartType; + title: string; +} + export interface InsightCardConfig { size: CardSize; visible: boolean; + content: InsightCardContent; } export interface LayoutConfig { @@ -76,10 +86,42 @@ export const defaultLayout: LayoutConfig = { overviewRatio: 4, promptRatio: 1, insightCards: [ - { size: "md", visible: true }, - { size: "md", visible: true }, - { size: "md", visible: true }, - { size: "md", visible: true }, + { + size: "sm", + visible: true, + content: { + type: "kpi", + chartType: "donut", + title: "Upfront decision efficiency", + }, + }, + { + size: "md", + visible: true, + content: { + type: "chart", + chartType: "horizontal-bars", + title: "Top issues", + }, + }, + { + size: "md", + visible: true, + content: { + type: "chart", + chartType: "donut", + title: "Pipeline", + }, + }, + { + size: "md", + visible: true, + content: { + type: "kpi", + chartType: "donut", + title: "SLA compliance", + }, + }, ], padding: 24, containerBg: "none", @@ -113,6 +155,18 @@ export const primaryOptions = [ { label: "900", value: "var(--primary-900)" }, ]; +export const cardTypeOptions = [ + { label: "KPI", value: "kpi" }, + { label: "Chart", value: "chart" }, +]; + +export const chartTypeOptions = [ + { label: "Donut", value: "donut" }, + { label: "Horizontal Bars", value: "horizontal-bars" }, + { label: "Sparkline", value: "sparkline" }, + { label: "Area", value: "area" }, +]; + export const bgColorOptions = [ { label: "white", value: "white" }, { label: "sidebar", value: "sidebar" }, @@ -121,15 +175,32 @@ export const bgColorOptions = [ { label: "muted", value: "muted" }, ]; +export function getInsightCardClasses(content: InsightCardContent): { + cardClassName: string; + contentClassName: string; +} { + if (content.type === "kpi") { + return { + cardClassName: "!gap-4", + contentClassName: "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: "white", - overviewOpacity: 6, - overviewGradient: { ...defaultGradient }, - insightBg: "white", - insightOpacity: 6, + overviewBg: "sidebar", + overviewOpacity: 69, + overviewGradient: { ...defaultGradient, opacity: 30 }, + insightBg: "sidebar", + insightOpacity: 54, insightGradient: { ...defaultGradient }, - promptBg: "white", - promptOpacity: 6, + 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..33e2fcf87 --- /dev/null +++ b/apps/apollo-vertex/templates/dashboard/insight-card-renderers.tsx @@ -0,0 +1,201 @@ +"use client"; + +import { Badge } from "@/components/ui/badge"; +import type { InsightCardContent } from "./glow-config"; + +// --- Sample data per card type --- + +const kpiSamples = [ + { + title: "Upfront decision efficiency", + number: "94.2%", + badge: "+6.8%", + description: "Loans finalized on first review without rework.", + }, + { + title: "SLA compliance", + number: "99.5%", + badge: "+1.2%", + description: "Loans processed within defined SLA thresholds.", + }, + { + title: "Automation rate", + number: "78.3%", + badge: "+4.1%", + description: "Processes completed without manual intervention.", + }, + { + title: "First-pass yield", + number: "91.7%", + badge: "+2.3%", + description: "Documents accepted on initial submission.", + }, +]; + +const barSamples = [ + { label: "Risk flag in notes", value: 34, color: "bg-chart-1" }, + { label: "Credit report >120 days old", value: 29, color: "bg-chart-2" }, + { label: "Owner name mismatch", value: 23, color: "bg-chart-3" }, + { label: "High DTI ratio", value: 14, color: "bg-chart-4" }, +]; + +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({ title }: { title: string }) { + const sample = kpiSamples.find((s) => s.title === title) ?? kpiSamples[0]; + + return ( + <> +
+ {sample.number} +
+
+ + {sample.badge} + +

+ {sample.description} +

+
+ + ); +} + +function DonutContent() { + return ( + <> +
+
+ + + + +
+ + 47% + + + funded + +
+
+
+

+ $1.58M away from your $3M funded target — 3 closings to go +

+ + ); +} + +function HorizontalBarsContent() { + return ( +
+ {barSamples.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 ( + <> +
+ + + +
+

+ Trending upward over the last 12 weeks +

+ + ); +} + +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 ( + <> +
+ + + + +
+

+ Volume trending up — 16% increase over prior period +

+ + ); +} + +export function InsightCardBody({ content }: { content: InsightCardContent }) { + if (content.type === "kpi") { + return ; + } + if (content.chartType === "horizontal-bars") return ; + if (content.chartType === "donut") return ; + if (content.chartType === "sparkline") return ; + return ; +} From e89878d9453d67b7101969968005832bf6f910c1 Mon Sep 17 00:00:00 2001 From: Peter Vachon Date: Sun, 22 Mar 2026 17:54:47 -0400 Subject: [PATCH 06/44] chore(apollo-vertex): Prompt card --- .../templates/dashboard/DashboardContent.tsx | 142 ++++++++--- .../templates/dashboard/glow-config.ts | 12 +- .../dashboard/insight-card-renderers.tsx | 228 ++++++++++++------ 3 files changed, 273 insertions(+), 109 deletions(-) diff --git a/apps/apollo-vertex/templates/dashboard/DashboardContent.tsx b/apps/apollo-vertex/templates/dashboard/DashboardContent.tsx index 6f8291336..584b5ef33 100644 --- a/apps/apollo-vertex/templates/dashboard/DashboardContent.tsx +++ b/apps/apollo-vertex/templates/dashboard/DashboardContent.tsx @@ -1,6 +1,7 @@ "use client"; import { useState } from "react"; +import { Badge } from "@/components/ui/badge"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { DashboardGlow } from "./DashboardGlow"; @@ -109,6 +110,98 @@ function InsightGrid({ ); } +// --- Prompt bar --- + +function PromptBar({ shared, cards }: { shared: string; cards: CardConfig }) { + const [value, setValue] = useState(""); + const hasInput = value.trim().length > 0; + + return ( +
+ {/* Suggestion pills */} +
+
+
+ + Show me top risk factors + + + Compare Q1 vs Q2 performance + +
+
+
+ {/* Input */} +
+ setValue(e.target.value)} + placeholder="What would you like to understand about loan performance?" + className="flex-1 bg-transparent text-sm outline-none placeholder:text-muted-foreground" + /> +
+ + +
+
+
+ ); +} + // --- Layout renderers --- function ExecutiveLayout({ @@ -124,18 +217,12 @@ function ExecutiveLayout({ const gapStyle = { gap: `${layout.gap}px` }; return ( -
+
{/* Left half */} -
+
Avg. deal cycle: 32 days (-4 days)

- - - - Conversational Prompt Bar - - - -
- Ask a question about your data... -
-
-
+
{/* Right half — insight grid */} @@ -214,7 +282,7 @@ export function DashboardContent() { return (
{/* Header with layout toggle */} @@ -259,11 +327,13 @@ export function DashboardContent() {
{/* Layout content */} - {layout === "executive" && ( - - )} - {layout === "operational" && } - {layout === "analytics" && } +
+ {layout === "executive" && ( + + )} + {layout === "operational" && } + {layout === "analytics" && } +
); diff --git a/apps/apollo-vertex/templates/dashboard/glow-config.ts b/apps/apollo-vertex/templates/dashboard/glow-config.ts index a89b6e377..d5e473fdc 100644 --- a/apps/apollo-vertex/templates/dashboard/glow-config.ts +++ b/apps/apollo-vertex/templates/dashboard/glow-config.ts @@ -53,7 +53,12 @@ export const defaultDarkGlow: GlowConfig = { export type CardSize = "sm" | "md" | "lg"; export type InsightCardType = "kpi" | "chart"; -export type ChartType = "donut" | "horizontal-bars" | "sparkline" | "area"; +export type ChartType = + | "donut" + | "horizontal-bars" + | "sparkline" + | "area" + | "stacked-bar"; export interface InsightCardContent { type: InsightCardType; @@ -109,12 +114,12 @@ export const defaultLayout: LayoutConfig = { visible: true, content: { type: "chart", - chartType: "donut", + chartType: "stacked-bar", title: "Pipeline", }, }, { - size: "md", + size: "sm", visible: true, content: { type: "kpi", @@ -165,6 +170,7 @@ export const chartTypeOptions = [ { label: "Horizontal Bars", value: "horizontal-bars" }, { label: "Sparkline", value: "sparkline" }, { label: "Area", value: "area" }, + { label: "Stacked Bar", value: "stacked-bar" }, ]; export const bgColorOptions = [ diff --git a/apps/apollo-vertex/templates/dashboard/insight-card-renderers.tsx b/apps/apollo-vertex/templates/dashboard/insight-card-renderers.tsx index 33e2fcf87..bfa1d1d1b 100644 --- a/apps/apollo-vertex/templates/dashboard/insight-card-renderers.tsx +++ b/apps/apollo-vertex/templates/dashboard/insight-card-renderers.tsx @@ -66,44 +66,39 @@ function KpiContent({ title }: { title: string }) { function DonutContent() { return ( - <> -
-
- - - - -
- - 47% - - - funded - -
+
+
+ + + + +
+ + 47% + + + funded +
-

- $1.58M away from your $3M funded target — 3 closings to go -

- +
); } @@ -138,23 +133,18 @@ function SparklineContent() { .join(" "); return ( - <> -
- - - -
-

- Trending upward over the last 12 weeks -

- +
+ + + +
); } @@ -169,24 +159,121 @@ function AreaContent() { const areaPath = `0,${h} ${linePoints} ${w},${h}`; return ( - <> -
- - - - +
+ + + + +
+ ); +} + +const stackedBarData = [ + { + label: "Mon", + segments: [ + { value: 30, color: "bg-chart-1" }, + { value: 20, color: "bg-chart-2" }, + { value: 10, color: "bg-chart-3" }, + ], + }, + { + label: "Tue", + segments: [ + { value: 40, color: "bg-chart-1" }, + { value: 15, color: "bg-chart-2" }, + { value: 20, color: "bg-chart-3" }, + ], + }, + { + label: "Wed", + segments: [ + { value: 25, color: "bg-chart-1" }, + { value: 30, color: "bg-chart-2" }, + { value: 15, color: "bg-chart-3" }, + ], + }, + { + label: "Thu", + segments: [ + { value: 45, color: "bg-chart-1" }, + { value: 10, color: "bg-chart-2" }, + { value: 25, color: "bg-chart-3" }, + ], + }, + { + label: "Fri", + segments: [ + { value: 35, color: "bg-chart-1" }, + { value: 25, color: "bg-chart-2" }, + { value: 18, color: "bg-chart-3" }, + ], + }, +]; + +function StackedBarContent() { + const maxTotal = Math.max( + ...stackedBarData.map((d) => + d.segments.reduce((sum, s) => sum + s.value, 0), + ), + ); + + const barMaxHeight = 140; + + const legendItems = [ + { label: "Approved", color: "bg-chart-1" }, + { label: "Pending", color: "bg-chart-2" }, + { label: "Rejected", color: "bg-chart-3" }, + ]; + + return ( +
+
+ {stackedBarData.map((bar) => { + const total = bar.segments.reduce((sum, s) => sum + s.value, 0); + const barHeight = (total / maxTotal) * barMaxHeight; + return ( +
+
+ {bar.segments.map((seg) => ( +
+ ))} +
+ + {bar.label} + +
+ ); + })}
-

- Volume trending up — 16% increase over prior period -

- +
+ {legendItems.map((item) => ( +
+
+ + {item.label} + +
+ ))} +
+
); } @@ -197,5 +284,6 @@ export function InsightCardBody({ content }: { content: InsightCardContent }) { if (content.chartType === "horizontal-bars") return ; if (content.chartType === "donut") return ; if (content.chartType === "sparkline") return ; + if (content.chartType === "stacked-bar") return ; return ; } From c9dfee6a41a5039f2aca125826644bc0e5d64390 Mon Sep 17 00:00:00 2001 From: Peter Vachon Date: Mon, 23 Mar 2026 05:54:24 -0400 Subject: [PATCH 07/44] chore(apollo-vertex): OverviewCard --- .../app/layouts/dashboard/page.mdx | 4 + apps/apollo-vertex/app/preview/_meta.ts | 3 + .../app/preview/dashboard-minimal/page.tsx | 11 + .../app/preview/dashboard/page.tsx | 11 + .../templates/dashboard/DashboardContent.tsx | 324 ++++++++++-------- .../templates/dashboard/DashboardLoading.tsx | 137 ++++++++ .../templates/dashboard/GlowDevControls.tsx | 2 +- .../templates/dashboard/PromptBar.tsx | 118 +++++++ 8 files changed, 458 insertions(+), 152 deletions(-) create mode 100644 apps/apollo-vertex/app/preview/_meta.ts create mode 100644 apps/apollo-vertex/app/preview/dashboard-minimal/page.tsx create mode 100644 apps/apollo-vertex/app/preview/dashboard/page.tsx create mode 100644 apps/apollo-vertex/templates/dashboard/DashboardLoading.tsx create mode 100644 apps/apollo-vertex/templates/dashboard/PromptBar.tsx diff --git a/apps/apollo-vertex/app/layouts/dashboard/page.mdx b/apps/apollo-vertex/app/layouts/dashboard/page.mdx index 847f0ca12..f1f437d3b 100644 --- a/apps/apollo-vertex/app/layouts/dashboard/page.mdx +++ b/apps/apollo-vertex/app/layouts/dashboard/page.mdx @@ -12,8 +12,12 @@ Use the layout toggle inside the preview to switch between Executive, Operationa +Open standalone preview → + ## Minimal Header Shell + +Open standalone preview → 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..69bb6d5f5 --- /dev/null +++ b/apps/apollo-vertex/app/preview/dashboard-minimal/page.tsx @@ -0,0 +1,11 @@ +"use client"; + +import { DashboardTemplate } from "@/templates/dashboard/DashboardTemplateDynamic"; + +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..5b063110f --- /dev/null +++ b/apps/apollo-vertex/app/preview/dashboard/page.tsx @@ -0,0 +1,11 @@ +"use client"; + +import { DashboardTemplate } from "@/templates/dashboard/DashboardTemplateDynamic"; + +export default function DashboardPreviewPage() { + return ( +
+ +
+ ); +} diff --git a/apps/apollo-vertex/templates/dashboard/DashboardContent.tsx b/apps/apollo-vertex/templates/dashboard/DashboardContent.tsx index 584b5ef33..2025a360a 100644 --- a/apps/apollo-vertex/templates/dashboard/DashboardContent.tsx +++ b/apps/apollo-vertex/templates/dashboard/DashboardContent.tsx @@ -1,7 +1,6 @@ "use client"; import { useState } from "react"; -import { Badge } from "@/components/ui/badge"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { DashboardGlow } from "./DashboardGlow"; @@ -17,6 +16,8 @@ import { } from "./glow-config"; import { GlowDevControls } from "./GlowDevControls"; import { InsightCardBody } from "./insight-card-renderers"; +import { DashboardLoading } from "./DashboardLoading"; +import { PromptBar } from "./PromptBar"; // --- Layout type --- @@ -110,98 +111,6 @@ function InsightGrid({ ); } -// --- Prompt bar --- - -function PromptBar({ shared, cards }: { shared: string; cards: CardConfig }) { - const [value, setValue] = useState(""); - const hasInput = value.trim().length > 0; - - return ( -
- {/* Suggestion pills */} -
-
-
- - Show me top risk factors - - - Compare Q1 vs Q2 performance - -
-
-
- {/* Input */} -
- setValue(e.target.value)} - placeholder="What would you like to understand about loan performance?" - className="flex-1 bg-transparent text-sm outline-none placeholder:text-muted-foreground" - /> -
- - -
-
-
- ); -} - // --- Layout renderers --- function ExecutiveLayout({ @@ -222,24 +131,126 @@ function ExecutiveLayout({
- + + + + - Overview + Good morning, Peter - -

Total revenue this quarter: $2.4M (+14%)

-

Active users: 12,847 across 3 regions

-

Top performing segment: Enterprise ($1.8M)

-

Pipeline value: $4.2M (68 deals)

-

Avg. deal cycle: 32 days (-4 days)

+ +
+

+ Loan volume scales as setup time drops by 3.5 days. +

+

+ Setup time declined ↓21% month over month while volume increased + ↑18%. +

+
+
+ + + + + + + + + + + + {/* Horizontal guide lines */} + + + + {/* Benchmark line */} + + {/* Fill area */} + + + +
@@ -279,62 +290,73 @@ export function DashboardContent() { const [darkGlow, setDarkGlow] = useState(defaultDarkGlow); const [darkCards, setDarkCards] = useState(defaultDarkCards); const [layoutCfg, setLayoutCfg] = useState(defaultLayout); + const [replayCount, setReplayCount] = useState(0); return ( -
- - +
- {/* Header with layout toggle */} -
-
-

- UiPath Vertical Solutions -

-

- {layoutLabels[layout]} Dashboard -

+ + +
+ {/* Header with layout toggle */} +
+
+

+ UiPath Vertical Solutions +

+

+ {layoutLabels[layout]} Dashboard +

+
+ setLayout(v as LayoutType)} + className="hidden" + > + + {(Object.keys(layoutLabels) as LayoutType[]).map((key) => ( + + {layoutLabels[key]} + + ))} + + +
- setLayout(v as LayoutType)} - > - - {(Object.keys(layoutLabels) as LayoutType[]).map((key) => ( - - {layoutLabels[key]} - - ))} - - -
- {/* Layout content */} -
- {layout === "executive" && ( - - )} - {layout === "operational" && } - {layout === "analytics" && } + {/* Layout content */} +
+ {layout === "executive" && ( + + )} + {layout === "operational" && } + {layout === "analytics" && } +
-
+ ); } 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 */} +
+ UiPath +
+ + {/* 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/GlowDevControls.tsx b/apps/apollo-vertex/templates/dashboard/GlowDevControls.tsx index ccf31a0dd..e18a7c4b1 100644 --- a/apps/apollo-vertex/templates/dashboard/GlowDevControls.tsx +++ b/apps/apollo-vertex/templates/dashboard/GlowDevControls.tsx @@ -24,7 +24,7 @@ export function GlowDevControls({ layoutConfig, onLayoutChange, }: DevControlsProps) { - const [open, setOpen] = useState(true); + const [open, setOpen] = useState(false); const [tab, setTab] = useState("glow"); const configMap = { diff --git a/apps/apollo-vertex/templates/dashboard/PromptBar.tsx b/apps/apollo-vertex/templates/dashboard/PromptBar.tsx new file mode 100644 index 000000000..a5000aa50 --- /dev/null +++ b/apps/apollo-vertex/templates/dashboard/PromptBar.tsx @@ -0,0 +1,118 @@ +"use client"; + +import { useState } from "react"; +import { Badge } from "@/components/ui/badge"; +import type { CardConfig, CardGradient } from "./glow-config"; + +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, +}: { + shared: string; + cards: CardConfig; +}) { + const [value, setValue] = useState(""); + const hasInput = value.trim().length > 0; + + return ( +
+
+
+
+ + Show me top risk factors + + + Compare Q1 vs Q2 performance + +
+
+
+
+ setValue(e.target.value)} + placeholder="What would you like to understand about loan performance?" + className="flex-1 bg-transparent text-sm outline-none placeholder:text-muted-foreground" + /> +
+ + +
+
+
+ ); +} From b57ab9c392e9afd726541e9b91b46512e426aadd Mon Sep 17 00:00:00 2001 From: Peter Vachon Date: Mon, 23 Mar 2026 06:14:19 -0400 Subject: [PATCH 08/44] chore(apollo-vertex): branchupdate --- .../templates/dashboard/DashboardShellWrapper.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/apps/apollo-vertex/templates/dashboard/DashboardShellWrapper.tsx b/apps/apollo-vertex/templates/dashboard/DashboardShellWrapper.tsx index 3ba37e992..fe3ffc163 100644 --- a/apps/apollo-vertex/templates/dashboard/DashboardShellWrapper.tsx +++ b/apps/apollo-vertex/templates/dashboard/DashboardShellWrapper.tsx @@ -42,9 +42,6 @@ export function DashboardShellWrapper({ alt: "UiPath logo", }} variant={variant} - clientId="e74e5981-cde0-4cd4-971c-6525cfba86b5" - scope="openid profile email offline_access" - baseUrl={typeof window === "undefined" ? "" : window.location.origin} navItems={variant === "minimal" ? minimalNavItems : sidebarNavItems} > {children} From 56c4be3802bdd20337d44cf2fb2b8904be16a7b8 Mon Sep 17 00:00:00 2001 From: Peter Vachon Date: Mon, 23 Mar 2026 09:54:12 -0400 Subject: [PATCH 09/44] chore(apollo-vertex): Overview card updates --- apps/apollo-vertex/public/Autopilot_dark.svg | 6 + apps/apollo-vertex/public/Autopilot_light.svg | 6 + .../templates/dashboard/DashboardContent.tsx | 124 ++++++++++++------ 3 files changed, 93 insertions(+), 43 deletions(-) create mode 100644 apps/apollo-vertex/public/Autopilot_dark.svg create mode 100644 apps/apollo-vertex/public/Autopilot_light.svg 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/templates/dashboard/DashboardContent.tsx b/apps/apollo-vertex/templates/dashboard/DashboardContent.tsx index 2025a360a..a1b8166a9 100644 --- a/apps/apollo-vertex/templates/dashboard/DashboardContent.tsx +++ b/apps/apollo-vertex/templates/dashboard/DashboardContent.tsx @@ -19,12 +19,8 @@ import { InsightCardBody } from "./insight-card-renderers"; import { DashboardLoading } from "./DashboardLoading"; import { PromptBar } from "./PromptBar"; -// --- Layout type --- - type LayoutType = "executive" | "operational" | "analytics"; -// --- Helpers --- - function cardBgStyle( bg: string, opacity: number, @@ -45,9 +41,7 @@ function cardBgStyle( } // --- Helpers --- - const sizeToFr: Record = { sm: "1fr", md: "2fr", lg: "1fr" }; - function InsightGrid({ layout, shared, @@ -131,31 +125,29 @@ function ExecutiveLayout({
- - - - + + Autopilot + Autopilot Good morning, Peter - +

Loan volume scales as setup time drops by 3.5 days. @@ -165,37 +157,40 @@ function ExecutiveLayout({ ↑18%.

-
+
- - - - - + + + + {/* Horizontal guide lines */} - {/* Fill area */} + {/* Fill area with soft edges */} + {/* Line */} + {/* Y-axis labels */} + + 200 + + + 150 + + + 100 + + + 50 + + {/* Target label */} + + Target + + {/* Dot at end of line */} +
From ed47e2543805471328de197ae5f9413ea83bca34 Mon Sep 17 00:00:00 2001 From: Peter Vachon Date: Mon, 23 Mar 2026 10:52:20 -0400 Subject: [PATCH 10/44] chore(apollo-vertex): Initial interaction insights --- .../templates/dashboard/DashboardContent.tsx | 91 +--------- .../templates/dashboard/InsightGrid.tsx | 170 ++++++++++++++++++ .../dashboard/dev-controls-primitives.tsx | 25 +++ .../templates/dashboard/dev-controls-tabs.tsx | 65 ++++--- .../templates/dashboard/glow-config.ts | 48 +++++ 5 files changed, 281 insertions(+), 118 deletions(-) create mode 100644 apps/apollo-vertex/templates/dashboard/InsightGrid.tsx diff --git a/apps/apollo-vertex/templates/dashboard/DashboardContent.tsx b/apps/apollo-vertex/templates/dashboard/DashboardContent.tsx index a1b8166a9..e25c8d88d 100644 --- a/apps/apollo-vertex/templates/dashboard/DashboardContent.tsx +++ b/apps/apollo-vertex/templates/dashboard/DashboardContent.tsx @@ -5,108 +5,21 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { DashboardGlow } from "./DashboardGlow"; import { + cardBgStyle, defaultDarkCards, defaultDarkGlow, defaultLayout, - getInsightCardClasses, type CardConfig, - type CardGradient, type GlowConfig, type LayoutConfig, } from "./glow-config"; import { GlowDevControls } from "./GlowDevControls"; -import { InsightCardBody } from "./insight-card-renderers"; +import { InsightGrid } from "./InsightGrid"; import { DashboardLoading } from "./DashboardLoading"; import { PromptBar } from "./PromptBar"; type LayoutType = "executive" | "operational" | "analytics"; -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; -} - -// --- Helpers --- -const sizeToFr: Record = { sm: "1fr", md: "2fr", lg: "1fr" }; -function InsightGrid({ - layout, - shared, - cards, -}: { - layout: LayoutConfig; - shared: string; - cards: CardConfig; -}) { - const gapStyle = { gap: `${layout.gap}px` }; - const visibleCards = layout.insightCards - .map((cfg, i) => ({ cfg, idx: i })) - .filter(({ cfg }) => cfg.visible); - - // Group into rows of 2 - const rows: (typeof visibleCards)[] = []; - for (let i = 0; i < visibleCards.length; i += 2) { - rows.push(visibleCards.slice(i, i + 2)); - } - - return ( -
- {rows.map((row) => { - const cols = row - .map(({ cfg }) => (cfg.size === "lg" ? "1fr" : sizeToFr[cfg.size])) - .join(" "); - return ( -
idx).join("-")} - className="grid flex-1" - style={{ ...gapStyle, gridTemplateColumns: cols }} - > - {row.map(({ cfg, idx }) => { - const classes = getInsightCardClasses(cfg.content); - return ( - - - - {cfg.content.title} - - - - - - - ); - })} -
- ); - })} -
- ); -} - -// --- Layout renderers --- - function ExecutiveLayout({ cards, layout, diff --git a/apps/apollo-vertex/templates/dashboard/InsightGrid.tsx b/apps/apollo-vertex/templates/dashboard/InsightGrid.tsx new file mode 100644 index 000000000..3a2550d9e --- /dev/null +++ b/apps/apollo-vertex/templates/dashboard/InsightGrid.tsx @@ -0,0 +1,170 @@ +"use client"; + +import { useState } from "react"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { + cardBgStyle, + getInsightCardClasses, + type CardConfig, + type LayoutConfig, +} from "./glow-config"; +import { InsightCardBody } from "./insight-card-renderers"; + +const sizeToFr: Record = { sm: "1fr", md: "2fr", lg: "1fr" }; + +function ExpandIcon() { + return ( + + + + + + + ); +} + +function NavigateIcon() { + return ( + + + + + ); +} + +function MinimizeIcon() { + return ( + + + + + + + ); +} + +export function InsightGrid({ + layout, + shared, + cards, +}: { + layout: LayoutConfig; + shared: string; + cards: CardConfig; +}) { + const [expandedIdx, setExpandedIdx] = useState(null); + const gapStyle = { gap: `${layout.gap}px` }; + const visibleCards = layout.insightCards + .map((cfg, i) => ({ cfg, 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 handleClick = (cfg: (typeof visibleCards)[0]["cfg"], idx: number) => { + if (cfg.interaction === "expand") setExpandedIdx(idx); + }; + return ( +
+ {rows.map((row) => { + const cols = row + .map(({ cfg }) => (cfg.size === "lg" ? "1fr" : sizeToFr[cfg.size])) + .join(" "); + return ( +
idx).join("-")} + className="grid flex-1" + style={{ ...gapStyle, gridTemplateColumns: cols }} + > + {row.map(({ cfg, idx }) => { + const classes = getInsightCardClasses(cfg.content); + const isInteractive = cfg.interaction !== "static"; + const isExpanded = expandedIdx === idx; + const isHidden = expandedIdx !== null && !isExpanded; + return ( + handleClick(cfg, idx) + : () => { + /* static — no action */ + } + } + className={`!bg-white/90 ${shared} ${classes.cardClassName} group/card relative transition-all duration-300 ${ + isInteractive ? "cursor-pointer hover:brightness-110" : "" + } ${isHidden ? "opacity-0 scale-95 pointer-events-none" : "opacity-100 scale-100"} ${ + isExpanded ? "!absolute !inset-0 !z-10" : "" + }`} + style={cardBgStyle( + cards.insightBg, + cards.insightOpacity, + cards.insightGradient, + )} + > + {isInteractive && !isExpanded && ( +
+ {cfg.interaction === "expand" ? ( + + ) : ( + + )} +
+ )} + {isExpanded && ( + + )} + + + {cfg.content.title} + + + + + +
+ ); + })} +
+ ); + })} +
+ ); +} diff --git a/apps/apollo-vertex/templates/dashboard/dev-controls-primitives.tsx b/apps/apollo-vertex/templates/dashboard/dev-controls-primitives.tsx index a112383da..a779dd432 100644 --- a/apps/apollo-vertex/templates/dashboard/dev-controls-primitives.tsx +++ b/apps/apollo-vertex/templates/dashboard/dev-controls-primitives.tsx @@ -86,3 +86,28 @@ export function Toggle({ ); } + +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 index ab4155320..5a6efbe03 100644 --- a/apps/apollo-vertex/templates/dashboard/dev-controls-tabs.tsx +++ b/apps/apollo-vertex/templates/dashboard/dev-controls-tabs.tsx @@ -4,10 +4,14 @@ import { bgColorOptions, cardTypeOptions, chartTypeOptions, + containerBgOptions, insightOptions, + interactionOptions, primaryOptions, + sizeOptions, type CardConfig, type CardGradient, + type CardInteraction, type CardSize, type ChartType, type GlowConfig, @@ -15,7 +19,12 @@ import { type InsightCardType, type LayoutConfig, } from "./glow-config"; -import { SelectControl, Slider, Toggle } from "./dev-controls-primitives"; +import { + SelectControl, + Slider, + TextInput, + Toggle, +} from "./dev-controls-primitives"; function GradientSection({ gradient, @@ -233,17 +242,6 @@ export function CardsTab({ ); } -const sizeOptions = [ - { label: "Small (1 col)", value: "sm" }, - { label: "Medium (1 col)", value: "md" }, - { label: "Large (full)", value: "lg" }, -]; - -const containerBgOptions = [ - { label: "None", value: "none" }, - ...bgColorOptions, -]; - export function LayoutTab({ config, onChange, @@ -294,7 +292,6 @@ export function LayoutTab({ onChange={(v) => update({ padding: v })} displayValue={`${config.padding}px`} /> -
Left Column
update({ promptRatio: v })} /> -
Insight Cards
@@ -365,22 +361,33 @@ export function LayoutTab({ } /> )} -
-
Title
- - updateInsightCard(i, { - content: { - ...config.insightCards[i].content, - title: e.target.value, - }, - }) - } - className="w-full h-7 rounded border bg-background px-1 text-xs" + + 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 index d5e473fdc..cf3041edc 100644 --- a/apps/apollo-vertex/templates/dashboard/glow-config.ts +++ b/apps/apollo-vertex/templates/dashboard/glow-config.ts @@ -66,10 +66,14 @@ export interface InsightCardContent { title: string; } +export type CardInteraction = "static" | "expand" | "navigate"; + export interface InsightCardConfig { size: CardSize; visible: boolean; content: InsightCardContent; + interaction: CardInteraction; + navigateTo?: string; } export interface LayoutConfig { @@ -94,6 +98,7 @@ export const defaultLayout: LayoutConfig = { { size: "sm", visible: true, + interaction: "static", content: { type: "kpi", chartType: "donut", @@ -103,6 +108,7 @@ export const defaultLayout: LayoutConfig = { { size: "md", visible: true, + interaction: "static", content: { type: "chart", chartType: "horizontal-bars", @@ -112,6 +118,7 @@ export const defaultLayout: LayoutConfig = { { size: "md", visible: true, + interaction: "static", content: { type: "chart", chartType: "stacked-bar", @@ -121,6 +128,7 @@ export const defaultLayout: LayoutConfig = { { size: "sm", visible: true, + interaction: "static", content: { type: "kpi", chartType: "donut", @@ -165,6 +173,12 @@ export const cardTypeOptions = [ { 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" }, @@ -173,6 +187,21 @@ export const chartTypeOptions = [ { 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" }, @@ -181,6 +210,25 @@ export const bgColorOptions = [ { 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): { cardClassName: string; contentClassName: string; From 6c88e81d855054e141058020ec1741162b18a6a8 Mon Sep 17 00:00:00 2001 From: Peter Vachon Date: Mon, 23 Mar 2026 11:57:25 -0400 Subject: [PATCH 11/44] chore(apollo-vertex): Card interaction --- .../templates/dashboard/DashboardContent.tsx | 6 +- .../templates/dashboard/InsightGrid.tsx | 163 ++++++++++-------- .../templates/dashboard/glow-config.ts | 8 +- 3 files changed, 102 insertions(+), 75 deletions(-) diff --git a/apps/apollo-vertex/templates/dashboard/DashboardContent.tsx b/apps/apollo-vertex/templates/dashboard/DashboardContent.tsx index e25c8d88d..63aff9bc0 100644 --- a/apps/apollo-vertex/templates/dashboard/DashboardContent.tsx +++ b/apps/apollo-vertex/templates/dashboard/DashboardContent.tsx @@ -207,7 +207,9 @@ function ExecutiveLayout({
{/* Right half — insight grid */} - +
+ +
); } @@ -246,7 +248,7 @@ export function DashboardContent() { return (
= { sm: "1fr", md: "2fr", lg: "1fr" }; -function ExpandIcon() { +function DiagonalArrow({ collapsed }: { collapsed: boolean }) { return ( - - - - - - ); -} - -function NavigateIcon() { - return ( - @@ -50,7 +30,7 @@ function NavigateIcon() { ); } -function MinimizeIcon() { +function NavigateIcon() { return ( - - - - + + + ); } +type ExpandPhase = "idle" | "width" | "full"; + export function InsightGrid({ layout, shared, @@ -80,6 +61,19 @@ export function InsightGrid({ cards: CardConfig; }) { const [expandedIdx, setExpandedIdx] = useState(null); + const [phase, setPhase] = useState("idle"); + + useEffect(() => { + if (expandedIdx === null) { + setPhase("idle"); + return; + } + // Start with width expansion + requestAnimationFrame(() => setPhase("width")); + const timer = setTimeout(() => setPhase("full"), 300); + return () => clearTimeout(timer); + }, [expandedIdx]); + const gapStyle = { gap: `${layout.gap}px` }; const visibleCards = layout.insightCards .map((cfg, i) => ({ cfg, idx: i })) @@ -88,69 +82,100 @@ export function InsightGrid({ 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: (typeof visibleCards)[0]["cfg"], idx: number) => { - if (cfg.interaction === "expand") setExpandedIdx(idx); + if (cfg.interaction === "expand") { + setExpandedIdx(expandedIdx === idx ? null : idx); + } }; + return ( -
- {rows.map((row) => { +
+ {rows.map((row, rowIndex) => { + const isRowWithExpanded = rowIndex === expandedRow; + const isOtherRow = isExpanding && !isRowWithExpanded; const cols = row - .map(({ cfg }) => (cfg.size === "lg" ? "1fr" : sizeToFr[cfg.size])) + .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 flex-1" - style={{ ...gapStyle, gridTemplateColumns: cols }} + className="grid transition-all duration-300 ease-in-out overflow-hidden" + style={{ + ...gapStyle, + gridTemplateColumns: cols, + flex: isOtherRow && phase === "full" ? "0" : "1", + opacity: isOtherRow && phase === "full" ? 0 : 1, + minHeight: isOtherRow && phase === "full" ? 0 : "auto", + }} > {row.map(({ cfg, idx }) => { const classes = getInsightCardClasses(cfg.content); const isInteractive = cfg.interaction !== "static"; - const isExpanded = expandedIdx === idx; - const isHidden = expandedIdx !== null && !isExpanded; + const isThis = idx === expandedIdx; + const isSibling = isExpanding && !isThis && isRowWithExpanded; return ( handleClick(cfg, idx) - : () => { - /* static — no action */ - } - } - className={`!bg-white/90 ${shared} ${classes.cardClassName} group/card relative transition-all duration-300 ${ - isInteractive ? "cursor-pointer hover:brightness-110" : "" - } ${isHidden ? "opacity-0 scale-95 pointer-events-none" : "opacity-100 scale-100"} ${ - isExpanded ? "!absolute !inset-0 !z-10" : "" - }`} - style={cardBgStyle( - cards.insightBg, - cards.insightOpacity, - cards.insightGradient, - )} + className={`!bg-white/90 ${shared} ${classes.cardClassName} group/card relative transition-all duration-300 ease-in-out overflow-hidden`} + style={{ + ...cardBgStyle( + cards.insightBg, + cards.insightOpacity, + cards.insightGradient, + ), + opacity: isSibling && phase !== "idle" ? 0 : 1, + transform: + isSibling && phase !== "idle" + ? "scale(0.95)" + : "scale(1)", + }} > - {isInteractive && !isExpanded && ( -
- {cfg.interaction === "expand" ? ( - - ) : ( - - )} -
- )} - {isExpanded && ( + {isInteractive && cfg.interaction === "expand" && ( )} + {isInteractive && + cfg.interaction === "navigate" && + !isThis && ( + + )} {cfg.content.title} diff --git a/apps/apollo-vertex/templates/dashboard/glow-config.ts b/apps/apollo-vertex/templates/dashboard/glow-config.ts index cf3041edc..07ffd61cd 100644 --- a/apps/apollo-vertex/templates/dashboard/glow-config.ts +++ b/apps/apollo-vertex/templates/dashboard/glow-config.ts @@ -98,7 +98,7 @@ export const defaultLayout: LayoutConfig = { { size: "sm", visible: true, - interaction: "static", + interaction: "expand", content: { type: "kpi", chartType: "donut", @@ -108,7 +108,7 @@ export const defaultLayout: LayoutConfig = { { size: "md", visible: true, - interaction: "static", + interaction: "expand", content: { type: "chart", chartType: "horizontal-bars", @@ -118,7 +118,7 @@ export const defaultLayout: LayoutConfig = { { size: "md", visible: true, - interaction: "static", + interaction: "expand", content: { type: "chart", chartType: "stacked-bar", @@ -128,7 +128,7 @@ export const defaultLayout: LayoutConfig = { { size: "sm", visible: true, - interaction: "static", + interaction: "expand", content: { type: "kpi", chartType: "donut", From e3a5444d70bad734aeaa1b9c6beac71af80b8f2c Mon Sep 17 00:00:00 2001 From: Peter Vachon Date: Mon, 23 Mar 2026 12:11:28 -0400 Subject: [PATCH 12/44] chore(apollo-vertex): Experiment badge --- apps/apollo-vertex/templates/dashboard/DashboardContent.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/apollo-vertex/templates/dashboard/DashboardContent.tsx b/apps/apollo-vertex/templates/dashboard/DashboardContent.tsx index 63aff9bc0..29ed0873b 100644 --- a/apps/apollo-vertex/templates/dashboard/DashboardContent.tsx +++ b/apps/apollo-vertex/templates/dashboard/DashboardContent.tsx @@ -1,6 +1,7 @@ "use client"; import { useState } from "react"; +import { Badge } from "@/components/ui/badge"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { DashboardGlow } from "./DashboardGlow"; @@ -274,8 +275,11 @@ export function DashboardContent() {

UiPath Vertical Solutions

-

+

{layoutLabels[layout]} Dashboard + + Experiment +

Date: Mon, 23 Mar 2026 12:43:02 -0400 Subject: [PATCH 13/44] chore(apollo-vertex): update interaction --- .../templates/dashboard/InsightGrid.tsx | 53 ++++++++++++++++--- 1 file changed, 45 insertions(+), 8 deletions(-) diff --git a/apps/apollo-vertex/templates/dashboard/InsightGrid.tsx b/apps/apollo-vertex/templates/dashboard/InsightGrid.tsx index a7d7f8184..463c974b2 100644 --- a/apps/apollo-vertex/templates/dashboard/InsightGrid.tsx +++ b/apps/apollo-vertex/templates/dashboard/InsightGrid.tsx @@ -49,7 +49,7 @@ function NavigateIcon() { ); } -type ExpandPhase = "idle" | "width" | "full"; +type ExpandPhase = "idle" | "width" | "height" | "full"; export function InsightGrid({ layout, @@ -68,10 +68,13 @@ export function InsightGrid({ setPhase("idle"); return; } - // Start with width expansion requestAnimationFrame(() => setPhase("width")); - const timer = setTimeout(() => setPhase("full"), 300); - return () => clearTimeout(timer); + const t1 = setTimeout(() => setPhase("height"), 300); + const t2 = setTimeout(() => setPhase("full"), 600); + return () => { + clearTimeout(t1); + clearTimeout(t2); + }; }, [expandedIdx]); const gapStyle = { gap: `${layout.gap}px` }; @@ -95,7 +98,10 @@ export function InsightGrid({ }; return ( -
+
{rows.map((row, rowIndex) => { const isRowWithExpanded = rowIndex === expandedRow; const isOtherRow = isExpanding && !isRowWithExpanded; @@ -125,9 +131,17 @@ export function InsightGrid({ style={{ ...gapStyle, gridTemplateColumns: cols, - flex: isOtherRow && phase === "full" ? "0" : "1", - opacity: isOtherRow && phase === "full" ? 0 : 1, - minHeight: isOtherRow && phase === "full" ? 0 : "auto", + flex: + isOtherRow && (phase === "height" || phase === "full") + ? "0" + : "1", + maxHeight: isOtherRow && phase === "full" ? 0 : 9999, + opacity: + isOtherRow && (phase === "height" || phase === "full") ? 0 : 1, + transform: + isOtherRow && (phase === "height" || phase === "full") + ? "scale(0.95)" + : "scale(1)", }} > {row.map(({ cfg, idx }) => { @@ -184,6 +198,29 @@ export function InsightGrid({ + {isThis && isExpanding && ( +
+ {phase === "full" ? ( +
+ + Additional content + +
+ ) : ( +
+
+
+
+
+ )} +
+ )} ); })} From a1863527296019dd2eb29c06e0c9464d7b2cbeb1 Mon Sep 17 00:00:00 2001 From: Peter Vachon Date: Mon, 23 Mar 2026 13:04:30 -0400 Subject: [PATCH 14/44] chore(apollo-vertex): Small adjustments --- apps/apollo-vertex/templates/dashboard/DashboardContent.tsx | 2 +- apps/apollo-vertex/templates/dashboard/InsightGrid.tsx | 4 ++-- apps/apollo-vertex/templates/dashboard/glow-config.ts | 6 +++--- .../templates/dashboard/insight-card-renderers.tsx | 6 +++--- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/apps/apollo-vertex/templates/dashboard/DashboardContent.tsx b/apps/apollo-vertex/templates/dashboard/DashboardContent.tsx index 29ed0873b..c64435200 100644 --- a/apps/apollo-vertex/templates/dashboard/DashboardContent.tsx +++ b/apps/apollo-vertex/templates/dashboard/DashboardContent.tsx @@ -39,7 +39,7 @@ function ExecutiveLayout({
- {isThis && isExpanding && ( + {isThis && isExpanding && (phase === "height" || phase === "full") && (
-
+
+
{stackedBarData.map((bar) => { const total = bar.segments.reduce((sum, s) => sum + s.value, 0); const barHeight = (total / maxTotal) * barMaxHeight; @@ -263,7 +263,7 @@ function StackedBarContent() { ); })}
-
+
{legendItems.map((item) => (
From 6f26b652fe5f0209155cd8e114af9b4fb47df68a Mon Sep 17 00:00:00 2001 From: Peter Vachon Date: Mon, 23 Mar 2026 13:19:30 -0400 Subject: [PATCH 15/44] chore(apollo-vertex): Card cleanup --- .../templates/dashboard/InsightGrid.tsx | 50 ++++++++++--------- .../templates/dashboard/PromptBar.tsx | 12 +++-- .../templates/dashboard/glow-config.ts | 2 +- 3 files changed, 34 insertions(+), 30 deletions(-) diff --git a/apps/apollo-vertex/templates/dashboard/InsightGrid.tsx b/apps/apollo-vertex/templates/dashboard/InsightGrid.tsx index a944b02af..47c5ac115 100644 --- a/apps/apollo-vertex/templates/dashboard/InsightGrid.tsx +++ b/apps/apollo-vertex/templates/dashboard/InsightGrid.tsx @@ -153,7 +153,7 @@ export function InsightGrid({ - {isThis && isExpanding && (phase === "height" || phase === "full") && ( -
- {phase === "full" ? ( -
- - Additional content - -
- ) : ( -
-
-
-
-
- )} -
- )} + {isThis && + isExpanding && + (phase === "height" || phase === "full") && ( +
+ {phase === "full" ? ( +
+ + Additional content + +
+ ) : ( +
+
+
+
+
+ )} +
+ )} ); })} diff --git a/apps/apollo-vertex/templates/dashboard/PromptBar.tsx b/apps/apollo-vertex/templates/dashboard/PromptBar.tsx index a5000aa50..d9068e494 100644 --- a/apps/apollo-vertex/templates/dashboard/PromptBar.tsx +++ b/apps/apollo-vertex/templates/dashboard/PromptBar.tsx @@ -73,8 +73,9 @@ export function PromptBar({
)} - {isInteractive && - cfg.interaction === "navigate" && - !isThis && ( - - )} + {isInteractive && cfg.interaction === "navigate" && !isThis && ( + + )} {cfg.content.title} @@ -201,37 +183,120 @@ export function InsightGrid({ - {isThis && - isExpanding && - (phase === "height" || phase === "full") && ( -
- {phase === "full" ? ( -
- - Additional content - -
- ) : ( -
-
-
-
-
- )} -
- )} + {isThis && isExpanding && (phase === "height" || phase === "full") && ( +
+ {phase === "full" ? ( +
+ Additional content +
+ ) : ( +
+
+
+
+
+ )} +
+ )} - ); - })} -
- ); - })} +
+ ); + }) + : 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 classes = getInsightCardClasses(cfg.content); + const isInteractive = cfg.interaction !== "static"; + const isThis = idx === expandedIdx; + const isSibling = isExpanding && !isThis && isRowWithExpanded; + return ( + + {isInteractive && cfg.interaction === "expand" && ( + + )} + {isInteractive && cfg.interaction === "navigate" && !isThis && ( + + )} + + + {cfg.content.title} + + + + + + {isThis && isExpanding && (phase === "height" || phase === "full") && ( +
+ {phase === "full" ? ( +
+ Additional content +
+ ) : ( +
+
+
+
+
+ )} +
+ )} + + ); + })} +
+ ); + })}
); } From 32370ba22330fdebdeabafe2f9988067426da816 Mon Sep 17 00:00:00 2001 From: Peter Vachon Date: Wed, 1 Apr 2026 16:36:28 -0400 Subject: [PATCH 22/44] chore(apollo-vertex): Componentize card content --- .../templates/dashboard/InsightGrid.tsx | 247 +++++++++--------- 1 file changed, 118 insertions(+), 129 deletions(-) diff --git a/apps/apollo-vertex/templates/dashboard/InsightGrid.tsx b/apps/apollo-vertex/templates/dashboard/InsightGrid.tsx index 5eb1d36ad..52d3c8ff3 100644 --- a/apps/apollo-vertex/templates/dashboard/InsightGrid.tsx +++ b/apps/apollo-vertex/templates/dashboard/InsightGrid.tsx @@ -6,6 +6,7 @@ import { cardBgStyle, getInsightCardClasses, type CardConfig, + type InsightCardConfig, type LayoutConfig, } from "./glow-config"; import { InsightCardBody } from "./insight-card-renderers"; @@ -49,6 +50,97 @@ function NavigateIcon() { ); } +// --- Shared card inner content --- + +interface InsightCardInnerProps { + cfg: InsightCardConfig; + shared: string; + cards: CardConfig; + isExpanding: boolean; + isThis: boolean; + phase: ExpandPhase; + onExpandClick: () => void; + className?: string; + style?: React.CSSProperties; +} + +function InsightCardInner({ + cfg, + shared, + cards, + isExpanding, + isThis, + phase, + onExpandClick, + className = "", + style, +}: InsightCardInnerProps) { + const classes = getInsightCardClasses(cfg.content); + const isInteractive = cfg.interaction !== "static"; + + return ( + + {isInteractive && cfg.interaction === "expand" && ( + + )} + {isInteractive && cfg.interaction === "navigate" && !isThis && ( + + )} + + + {cfg.content.title} + + + + + + {isThis && isExpanding && (phase === "height" || phase === "full") && ( +
+ {phase === "full" ? ( +
+ Additional content +
+ ) : ( +
+
+
+
+
+ )} +
+ )} + + ); +} + +// --- Main grid --- + type ExpandPhase = "idle" | "width" | "height" | "full"; export function InsightGrid({ @@ -70,14 +162,6 @@ export function InsightGrid({ setPhase("idle"); return; } - if (viewMode === "compact") { - // Compact: "width" phase = other row collapses (grows top) - // "height" phase = expanding row stretches (grows bottom) - requestAnimationFrame(() => setPhase("width")); - const t1 = setTimeout(() => setPhase("height"), 300); - const t2 = setTimeout(() => setPhase("full"), 600); - return () => { clearTimeout(t1); clearTimeout(t2); }; - } requestAnimationFrame(() => setPhase("width")); const t1 = setTimeout(() => setPhase("height"), 300); const t2 = setTimeout(() => setPhase("full"), 600); @@ -100,16 +184,15 @@ export function InsightGrid({ ? rows.findIndex((row) => row.some(({ idx }) => idx === expandedIdx)) : -1; - const handleClick = (cfg: (typeof visibleCards)[0]["cfg"], idx: number) => { + const handleClick = (cfg: InsightCardConfig, idx: number) => { if (cfg.interaction === "expand") { setExpandedIdx(expandedIdx === idx ? null : idx); } }; - // Build grid-template-rows for smooth animation + // Build grid-template-rows let rowTemplates: string[]; if (viewMode === "compact") { - // In compact, each card is its own row rowTemplates = visibleCards.map(({ idx }) => { if (!isExpanding) return "1fr"; if (idx === expandedIdx) return "1fr"; @@ -118,29 +201,32 @@ export function InsightGrid({ }); } else { rowTemplates = rows.map((_, rowIndex) => { - const isRowWithExpanded = rowIndex === expandedRow; - const isOtherRow = isExpanding && !isRowWithExpanded; + const isOtherRow = isExpanding && rowIndex !== expandedRow; if (isOtherRow && (phase === "height" || phase === "full")) return "0fr"; return "1fr"; }); } + const sharedProps = { shared, cards, isExpanding, phase }; + return (
{viewMode === "compact" ? visibleCards.map(({ cfg, idx }) => { - const classes = getInsightCardClasses(cfg.content); - const isInteractive = cfg.interaction !== "static"; const isThis = idx === expandedIdx; const isOther = isExpanding && !isThis; return ( @@ -149,60 +235,13 @@ export function InsightGrid({ className="overflow-hidden min-h-0 transition-all duration-300 ease-in-out" style={{ opacity: isOther && phase !== "idle" ? 0 : 1 }} > - - {isInteractive && cfg.interaction === "expand" && ( - - )} - {isInteractive && cfg.interaction === "navigate" && !isThis && ( - - )} - - - {cfg.content.title} - - - - - - {isThis && isExpanding && (phase === "height" || phase === "full") && ( -
- {phase === "full" ? ( -
- Additional content -
- ) : ( -
-
-
-
-
- )} -
- )} - + handleClick(cfg, idx)} + className="h-full" + />
); }) @@ -228,70 +267,20 @@ export function InsightGrid({ } as React.CSSProperties} > {row.map(({ cfg, idx }) => { - const classes = getInsightCardClasses(cfg.content); - const isInteractive = cfg.interaction !== "static"; const isThis = idx === expandedIdx; const isSibling = isExpanding && !isThis && isRowWithExpanded; return ( - handleClick(cfg, idx)} style={{ - ...cardBgStyle(cards.insightBg, cards.insightOpacity, cards.insightGradient), opacity: isSibling && phase !== "idle" ? 0 : 1, transform: isSibling && phase !== "idle" ? "scale(0.95)" : "scale(1)", }} - > - {isInteractive && cfg.interaction === "expand" && ( - - )} - {isInteractive && cfg.interaction === "navigate" && !isThis && ( - - )} - - - {cfg.content.title} - - - - - - {isThis && isExpanding && (phase === "height" || phase === "full") && ( -
- {phase === "full" ? ( -
- Additional content -
- ) : ( -
-
-
-
-
- )} -
- )} - + /> ); })}
From 5531d9603ae1c91df48a0dd070dc34fc77314a16 Mon Sep 17 00:00:00 2001 From: Peter Vachon Date: Wed, 1 Apr 2026 17:19:53 -0400 Subject: [PATCH 23/44] chore(apollo-vertex): Compact view --- .../templates/dashboard/InsightGrid.tsx | 6 +- .../dashboard/insight-card-renderers.tsx | 136 ++++++++++++++++-- 2 files changed, 126 insertions(+), 16 deletions(-) diff --git a/apps/apollo-vertex/templates/dashboard/InsightGrid.tsx b/apps/apollo-vertex/templates/dashboard/InsightGrid.tsx index 52d3c8ff3..6b0806c69 100644 --- a/apps/apollo-vertex/templates/dashboard/InsightGrid.tsx +++ b/apps/apollo-vertex/templates/dashboard/InsightGrid.tsx @@ -59,6 +59,7 @@ interface InsightCardInnerProps { isExpanding: boolean; isThis: boolean; phase: ExpandPhase; + viewMode: "desktop" | "compact" | "stacked"; onExpandClick: () => void; className?: string; style?: React.CSSProperties; @@ -71,6 +72,7 @@ function InsightCardInner({ isExpanding, isThis, phase, + viewMode, onExpandClick, className = "", style, @@ -114,7 +116,7 @@ function InsightCardInner({ - + {isThis && isExpanding && (phase === "height" || phase === "full") && (
s.title === title) ?? kpiSamples[0]; + if (viewMode === "compact") { + return ( + <> +
+
+ {sample.number} +
+ + {sample.badge} + +
+

+ {sample.description} +

+ + ); + } + return ( <>
@@ -103,7 +123,41 @@ function DonutContent() { ); } -function HorizontalBarsContent() { +function HorizontalBarsContent({ viewMode, isExpanded = false }: { viewMode: ViewMode; isExpanded?: boolean }) { + if (viewMode === "compact" && !isExpanded) { + const total = barSamples.reduce((sum, s) => sum + s.value, 0); + return ( +
+
+ {barSamples.map((issue) => ( +
+
+
+ ))} +
+
+ {barSamples.map((issue) => { + const pct = Math.round((issue.value / total) * 100); + return ( +
+
+ + {issue.label} {pct}% + +
+ ); + })} +
+
+ ); + } + return (
{barSamples.map((issue) => ( @@ -223,35 +277,80 @@ const stackedBarData = [ }, ]; -function StackedBarContent() { +function StackedBarContent({ viewMode, isExpanded = false }: { viewMode: ViewMode; isExpanded?: boolean }) { const maxTotal = Math.max( ...stackedBarData.map((d) => d.segments.reduce((sum, s) => sum + s.value, 0), ), ); - const barMaxHeight = 140; - const legendItems = [ { label: "Approved", color: "bg-chart-1" }, { label: "Pending", color: "bg-chart-2" }, { label: "Rejected", color: "bg-chart-3" }, ]; + if (viewMode === "compact" && !isExpanded) { + // Summary: aggregate all days into one horizontal stacked bar + const totals = stackedBarData.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 ( +
+
+ {legendItems.map((item) => ( +
+
+
+ ))} +
+
+ {legendItems.map((item) => { + const val = totals[item.color] ?? 0; + const pct = Math.round((val / grandTotal) * 100); + return ( +
+
+ + {item.label} {pct}% + +
+ ); + })} +
+
+ ); + } + return (
-
+
{stackedBarData.map((bar) => { const total = bar.segments.reduce((sum, s) => sum + s.value, 0); - const barHeight = (total / maxTotal) * barMaxHeight; + const pct = (total / maxTotal) * 100; return (
{bar.segments.map((seg) => ( @@ -293,13 +392,22 @@ function StackedBarContent() { ); } -export function InsightCardBody({ content }: { content: InsightCardContent }) { +export function InsightCardBody({ + content, + viewMode = "desktop", + isExpanded = false, +}: { + content: InsightCardContent; + viewMode?: ViewMode; + isExpanded?: boolean; +}) { if (content.type === "kpi") { - return ; + return ; } - if (content.chartType === "horizontal-bars") return ; + if (content.chartType === "horizontal-bars") return ; + if (content.chartType === "donut") return ; if (content.chartType === "sparkline") return ; - if (content.chartType === "stacked-bar") return ; + if (content.chartType === "stacked-bar") return ; return ; } From 645eb622acc99e9cbe39f95c0a88ff29ba15c471 Mon Sep 17 00:00:00 2001 From: Peter Vachon Date: Wed, 1 Apr 2026 17:25:31 -0400 Subject: [PATCH 24/44] chore(apollo-vertex): scroll behavior --- apps/apollo-vertex/templates/dashboard/DashboardContent.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/apollo-vertex/templates/dashboard/DashboardContent.tsx b/apps/apollo-vertex/templates/dashboard/DashboardContent.tsx index ba6bc0512..191babaf1 100644 --- a/apps/apollo-vertex/templates/dashboard/DashboardContent.tsx +++ b/apps/apollo-vertex/templates/dashboard/DashboardContent.tsx @@ -153,7 +153,7 @@ export function DashboardContent() { return (
Date: Wed, 1 Apr 2026 18:05:18 -0400 Subject: [PATCH 25/44] chore(apollo-vertex): Autopilot surface initial --- .../templates/dashboard/AutopilotInsight.tsx | 76 +++++++++++++++++++ .../templates/dashboard/DashboardContent.tsx | 54 ++++++++++--- .../templates/dashboard/InsightGrid.tsx | 20 +++++ 3 files changed, 139 insertions(+), 11 deletions(-) create mode 100644 apps/apollo-vertex/templates/dashboard/AutopilotInsight.tsx 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 + Autopilot + + 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/DashboardContent.tsx b/apps/apollo-vertex/templates/dashboard/DashboardContent.tsx index 191babaf1..184d75d9a 100644 --- a/apps/apollo-vertex/templates/dashboard/DashboardContent.tsx +++ b/apps/apollo-vertex/templates/dashboard/DashboardContent.tsx @@ -18,16 +18,20 @@ import { GlowDevControls } from "./GlowDevControls"; import { InsightGrid } from "./InsightGrid"; import { DashboardLoading } from "./DashboardLoading"; import { PromptBar } from "./PromptBar"; +import { AutopilotInsight } from "./AutopilotInsight"; + type LayoutType = "executive" | "operational" | "analytics"; function ExecutiveLayout({ cards, layout, viewMode, + onAutopilotOpen, }: { cards: CardConfig; layout: LayoutConfig; viewMode: ViewMode; + onAutopilotOpen?: (sourceTitle: string) => void; }) { const borderClass = cards.borderVisible ? "" : "dark:!border-transparent"; const blurClass = cards.backdropBlur ? "" : "dark:!backdrop-blur-none"; @@ -90,7 +94,7 @@ function ExecutiveLayout({ {promptBarEl}
- +
); @@ -147,17 +151,24 @@ export function DashboardContent() { const [darkCards, setDarkCards] = useState(defaultDarkCards); const [layoutCfg, setLayoutCfg] = useState(defaultLayout); const [replayCount, setReplayCount] = useState(0); + const [autopilotOpen, setAutopilotOpen] = useState(false); + const [autopilotSource, setAutopilotSource] = useState(""); const containerRef = useRef(null); const viewMode = useViewMode(containerRef); + const handleAutopilotOpen = (sourceTitle: string) => { + setAutopilotSource(sourceTitle); + setAutopilotOpen(true); + }; + return (
@@ -174,7 +185,7 @@ export function DashboardContent() { className="@container flex flex-col gap-4 relative z-10 h-full" style={{ padding: layoutCfg.padding }} > - {/* Header with layout toggle */} + {/* Header — stays in place */}

@@ -210,12 +221,33 @@ export function DashboardContent() {

{/* Layout content */} -
- {layout === "executive" && ( - - )} - {layout === "operational" && } - {layout === "analytics" && } +
+ {/* Dashboard cards — shifts left for autopilot */} +
+ {layout === "executive" && ( + + )} + {layout === "operational" && } + {layout === "analytics" && } +
+ {/* Autopilot panel — slides in from right */} +
+
+ setAutopilotOpen(false)} + sourceCardTitle={autopilotSource} + /> +
+
diff --git a/apps/apollo-vertex/templates/dashboard/InsightGrid.tsx b/apps/apollo-vertex/templates/dashboard/InsightGrid.tsx index 6b0806c69..f40bd9a24 100644 --- a/apps/apollo-vertex/templates/dashboard/InsightGrid.tsx +++ b/apps/apollo-vertex/templates/dashboard/InsightGrid.tsx @@ -61,6 +61,7 @@ interface InsightCardInnerProps { phase: ExpandPhase; viewMode: "desktop" | "compact" | "stacked"; onExpandClick: () => void; + onAutopilotOpen?: () => void; className?: string; style?: React.CSSProperties; } @@ -74,6 +75,7 @@ function InsightCardInner({ phase, viewMode, onExpandClick, + onAutopilotOpen, className = "", style, }: InsightCardInnerProps) { @@ -137,6 +139,20 @@ function InsightCardInner({ )}
)} + {/* Autopilot trigger — visible when card is fully expanded */} + {isThis && isExpanding && phase === "full" && onAutopilotOpen && ( + + )} ); } @@ -150,11 +166,13 @@ export function InsightGrid({ shared, cards, viewMode = "desktop", + onAutopilotOpen, }: { layout: LayoutConfig; shared: string; cards: CardConfig; viewMode?: "desktop" | "compact" | "stacked"; + onAutopilotOpen?: (sourceTitle: string) => void; }) { const [expandedIdx, setExpandedIdx] = useState(null); const [phase, setPhase] = useState("idle"); @@ -242,6 +260,7 @@ export function InsightGrid({ {...sharedProps} isThis={isThis} onExpandClick={() => handleClick(cfg, idx)} + onAutopilotOpen={onAutopilotOpen ? () => onAutopilotOpen(cfg.content.title) : undefined} className="h-full" />
@@ -278,6 +297,7 @@ export function InsightGrid({ {...sharedProps} isThis={isThis} onExpandClick={() => handleClick(cfg, idx)} + onAutopilotOpen={onAutopilotOpen ? () => onAutopilotOpen(cfg.content.title) : undefined} style={{ opacity: isSibling && phase !== "idle" ? 0 : 1, transform: isSibling && phase !== "idle" ? "scale(0.95)" : "scale(1)", From e15556b814a9429278b731f2f88628cba17d2612 Mon Sep 17 00:00:00 2001 From: Peter Vachon Date: Wed, 1 Apr 2026 18:31:46 -0400 Subject: [PATCH 26/44] chore(apollo-vertex): Utility --- .../templates/dashboard/DashboardContent.tsx | 28 ++++++-- .../templates/dashboard/InsightGrid.tsx | 66 ++++++++++++------- 2 files changed, 62 insertions(+), 32 deletions(-) diff --git a/apps/apollo-vertex/templates/dashboard/DashboardContent.tsx b/apps/apollo-vertex/templates/dashboard/DashboardContent.tsx index 184d75d9a..be657cbe9 100644 --- a/apps/apollo-vertex/templates/dashboard/DashboardContent.tsx +++ b/apps/apollo-vertex/templates/dashboard/DashboardContent.tsx @@ -27,11 +27,13 @@ function ExecutiveLayout({ layout, viewMode, onAutopilotOpen, + autopilotActiveIdx, }: { cards: CardConfig; layout: LayoutConfig; viewMode: ViewMode; - onAutopilotOpen?: (sourceTitle: string) => void; + onAutopilotOpen?: (sourceTitle: string, idx: number) => void; + autopilotActiveIdx?: number | null; }) { const borderClass = cards.borderVisible ? "" : "dark:!border-transparent"; const blurClass = cards.backdropBlur ? "" : "dark:!backdrop-blur-none"; @@ -94,7 +96,7 @@ function ExecutiveLayout({ {promptBarEl}
- +
); @@ -153,12 +155,24 @@ export function DashboardContent() { const [replayCount, setReplayCount] = 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) => { - setAutopilotSource(sourceTitle); - setAutopilotOpen(true); + 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 ( @@ -228,7 +242,7 @@ export function DashboardContent() { style={{ transform: autopilotOpen ? "translateX(-50%)" : "translateX(0)" }} > {layout === "executive" && ( - + )} {layout === "operational" && } {layout === "analytics" && } @@ -243,7 +257,7 @@ export function DashboardContent() { >
setAutopilotOpen(false)} + onClose={handleAutopilotClose} sourceCardTitle={autopilotSource} />
diff --git a/apps/apollo-vertex/templates/dashboard/InsightGrid.tsx b/apps/apollo-vertex/templates/dashboard/InsightGrid.tsx index f40bd9a24..486e6558c 100644 --- a/apps/apollo-vertex/templates/dashboard/InsightGrid.tsx +++ b/apps/apollo-vertex/templates/dashboard/InsightGrid.tsx @@ -62,6 +62,7 @@ interface InsightCardInnerProps { viewMode: "desktop" | "compact" | "stacked"; onExpandClick: () => void; onAutopilotOpen?: () => void; + isAutopilotActive?: boolean; className?: string; style?: React.CSSProperties; } @@ -76,6 +77,7 @@ function InsightCardInner({ viewMode, onExpandClick, onAutopilotOpen, + isAutopilotActive = false, className = "", style, }: InsightCardInnerProps) { @@ -85,24 +87,48 @@ function InsightCardInner({ return ( {isInteractive && cfg.interaction === "expand" && ( - + {onAutopilotOpen && ( + + )} + +
)} {isInteractive && cfg.interaction === "navigate" && !isThis && ( - )} ); } @@ -167,12 +179,14 @@ export function InsightGrid({ cards, viewMode = "desktop", onAutopilotOpen, + autopilotActiveIdx, }: { layout: LayoutConfig; shared: string; cards: CardConfig; viewMode?: "desktop" | "compact" | "stacked"; - onAutopilotOpen?: (sourceTitle: string) => void; + onAutopilotOpen?: (sourceTitle: string, idx: number) => void; + autopilotActiveIdx?: number | null; }) { const [expandedIdx, setExpandedIdx] = useState(null); const [phase, setPhase] = useState("idle"); @@ -260,7 +274,8 @@ export function InsightGrid({ {...sharedProps} isThis={isThis} onExpandClick={() => handleClick(cfg, idx)} - onAutopilotOpen={onAutopilotOpen ? () => onAutopilotOpen(cfg.content.title) : undefined} + onAutopilotOpen={onAutopilotOpen ? () => onAutopilotOpen(cfg.content.title, idx) : undefined} + isAutopilotActive={autopilotActiveIdx === idx} className="h-full" />
@@ -297,7 +312,8 @@ export function InsightGrid({ {...sharedProps} isThis={isThis} onExpandClick={() => handleClick(cfg, idx)} - onAutopilotOpen={onAutopilotOpen ? () => onAutopilotOpen(cfg.content.title) : undefined} + onAutopilotOpen={onAutopilotOpen ? () => onAutopilotOpen(cfg.content.title, idx) : undefined} + isAutopilotActive={autopilotActiveIdx === idx} style={{ opacity: isSibling && phase !== "idle" ? 0 : 1, transform: isSibling && phase !== "idle" ? "scale(0.95)" : "scale(1)", From 22e83c6394f7321c572d6f4c5c1b2029e5d939e6 Mon Sep 17 00:00:00 2001 From: Peter Vachon Date: Thu, 2 Apr 2026 09:03:26 -0400 Subject: [PATCH 27/44] chore(apollo-vertex): Dev tool for data --- .../templates/dashboard/DashboardContent.tsx | 34 ++-- .../dashboard/DashboardDataProvider.tsx | 28 ++++ .../templates/dashboard/GlowDevControls.tsx | 94 ++++++++++- .../templates/dashboard/InsightGrid.tsx | 16 +- .../templates/dashboard/PromptBar.tsx | 9 +- .../templates/dashboard/dashboard-data.ts | 149 ++++++++++++++++++ .../dashboard/insight-card-renderers.tsx | 78 +++++---- 7 files changed, 356 insertions(+), 52 deletions(-) create mode 100644 apps/apollo-vertex/templates/dashboard/DashboardDataProvider.tsx create mode 100644 apps/apollo-vertex/templates/dashboard/dashboard-data.ts diff --git a/apps/apollo-vertex/templates/dashboard/DashboardContent.tsx b/apps/apollo-vertex/templates/dashboard/DashboardContent.tsx index be657cbe9..d5e7504c2 100644 --- a/apps/apollo-vertex/templates/dashboard/DashboardContent.tsx +++ b/apps/apollo-vertex/templates/dashboard/DashboardContent.tsx @@ -19,6 +19,7 @@ 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"; @@ -35,10 +36,13 @@ function ExecutiveLayout({ onAutopilotOpen?: (sourceTitle: string, idx: number) => void; autopilotActiveIdx?: number | null; }) { + const { data } = useDashboardData(); 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` }; + const yLabels = data.chartLabels.y; + const yPositions = [15, 25, 30, 45]; const overviewCardEl = ( Autopilot Autopilot - Good morning, Peter + {data.greeting}

- Loan volume scales as setup time drops by 3.5 days. + {data.headline}

- Setup time declined ↓21% month over month while volume increased ↑18%. + {data.subhead}

@@ -76,11 +80,10 @@ function ExecutiveLayout({ - 200 - 150 - 100 - 50 - Target + {yLabels.map((label, i) => ( + {label} + ))} + {data.chartLabels.target}
@@ -147,7 +150,8 @@ function useViewMode(ref: React.RefObject): ViewMode { return mode; } -export function DashboardContent() { +function DashboardContentInner() { + const { data } = useDashboardData(); const [layout, setLayout] = useState("executive"); const [darkGlow, setDarkGlow] = useState(defaultDarkGlow); const [darkCards, setDarkCards] = useState(defaultDarkCards); @@ -203,12 +207,12 @@ export function DashboardContent() {

- UiPath Vertical Solutions + {data.brandName} {data.brandLine}

{layoutLabels[layout]} Dashboard - Experimental + {data.badgeText}

@@ -268,3 +272,11 @@ export function DashboardContent() { ); } + +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..6c035ab1f --- /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 { defaultDataset, type DashboardDataset } from "./dashboard-data"; + +interface DashboardDataContextValue { + data: DashboardDataset; + setDataset: (data: DashboardDataset) => void; +} + +const DashboardDataContext = createContext({ + data: defaultDataset, + setDataset: () => {}, +}); + +export function useDashboardData() { + return useContext(DashboardDataContext); +} + +export function DashboardDataProvider({ children }: { children: ReactNode }) { + const [data, setData] = useState(defaultDataset); + + return ( + + {children} + + ); +} diff --git a/apps/apollo-vertex/templates/dashboard/GlowDevControls.tsx b/apps/apollo-vertex/templates/dashboard/GlowDevControls.tsx index e18a7c4b1..fcdaeaf75 100644 --- a/apps/apollo-vertex/templates/dashboard/GlowDevControls.tsx +++ b/apps/apollo-vertex/templates/dashboard/GlowDevControls.tsx @@ -1,9 +1,11 @@ "use client"; import { ChevronLeft, ChevronRight } from "lucide-react"; -import { useState } from "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; @@ -14,7 +16,7 @@ interface DevControlsProps { onLayoutChange: (config: LayoutConfig) => void; } -type Tab = "glow" | "cards" | "layout"; +type Tab = "glow" | "cards" | "layout" | "data"; export function GlowDevControls({ glowConfig, @@ -26,11 +28,15 @@ export function GlowDevControls({ }: DevControlsProps) { const [open, setOpen] = useState(false); const [tab, setTab] = useState("glow"); + const { data, setDataset } = useDashboardData(); + const fileInputRef = useRef(null); + const [uploadError, setUploadError] = useState(""); - const configMap = { + const configMap: Record = { glow: glowConfig, cards: cardConfig, layout: layoutConfig, + data: data, }; const currentConfig = configMap[tab]; @@ -60,6 +66,13 @@ export function GlowDevControls({ > Layout +
{tab === "glow" && ( @@ -71,6 +84,81 @@ export function GlowDevControls({ {tab === "layout" && ( )} + {tab === "data" && ( +
+
+ Dataset: {data.name} +
+
+
Preset
+ +
+
+ + + { + 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; + } + setDataset(parsed); + } catch { + setUploadError("Invalid JSON"); + } + }; + reader.readAsText(file); + e.target.value = ""; + }} + /> +
+ {uploadError && ( +
{uploadError}
+ )} +
+ )}
Config:
diff --git a/apps/apollo-vertex/templates/dashboard/InsightGrid.tsx b/apps/apollo-vertex/templates/dashboard/InsightGrid.tsx
index 486e6558c..d01af9cf7 100644
--- a/apps/apollo-vertex/templates/dashboard/InsightGrid.tsx
+++ b/apps/apollo-vertex/templates/dashboard/InsightGrid.tsx
@@ -10,6 +10,7 @@ import {
   type LayoutConfig,
 } from "./glow-config";
 import { InsightCardBody } from "./insight-card-renderers";
+import { useDashboardData } from "./DashboardDataProvider";
 
 const sizeToFr: Record = { sm: "1fr", md: "2fr", lg: "1fr" };
 
@@ -54,6 +55,7 @@ function NavigateIcon() {
 
 interface InsightCardInnerProps {
   cfg: InsightCardConfig;
+  cardIndex: number;
   shared: string;
   cards: CardConfig;
   isExpanding: boolean;
@@ -69,6 +71,7 @@ interface InsightCardInnerProps {
 
 function InsightCardInner({
   cfg,
+  cardIndex,
   shared,
   cards,
   isExpanding,
@@ -81,6 +84,8 @@ function InsightCardInner({
   className = "",
   style,
 }: InsightCardInnerProps) {
+  const { data } = useDashboardData();
+  const cardTitle = data.insightCards[cardIndex]?.title ?? cfg.content.title;
   const classes = getInsightCardClasses(cfg.content);
   const isInteractive = cfg.interaction !== "static";
 
@@ -140,11 +145,11 @@ function InsightCardInner({
       )}
       
         
-          {cfg.content.title}
+          {cardTitle}
         
       
       
-        
+        
       
       {isThis && isExpanding && (phase === "height" || phase === "full") && (
         
void; autopilotActiveIdx?: number | null; }) { + const { data } = useDashboardData(); const [expandedIdx, setExpandedIdx] = useState(null); const [phase, setPhase] = useState("idle"); @@ -271,10 +277,11 @@ export function InsightGrid({ > handleClick(cfg, idx)} - onAutopilotOpen={onAutopilotOpen ? () => onAutopilotOpen(cfg.content.title, idx) : undefined} + onAutopilotOpen={onAutopilotOpen ? () => onAutopilotOpen(data.insightCards[idx]?.title ?? cfg.content.title, idx) : undefined} isAutopilotActive={autopilotActiveIdx === idx} className="h-full" /> @@ -309,10 +316,11 @@ export function InsightGrid({ handleClick(cfg, idx)} - onAutopilotOpen={onAutopilotOpen ? () => onAutopilotOpen(cfg.content.title, idx) : undefined} + onAutopilotOpen={onAutopilotOpen ? () => onAutopilotOpen(data.insightCards[idx]?.title ?? cfg.content.title, idx) : undefined} isAutopilotActive={autopilotActiveIdx === idx} style={{ opacity: isSibling && phase !== "idle" ? 0 : 1, diff --git a/apps/apollo-vertex/templates/dashboard/PromptBar.tsx b/apps/apollo-vertex/templates/dashboard/PromptBar.tsx index d9068e494..42650a4d7 100644 --- a/apps/apollo-vertex/templates/dashboard/PromptBar.tsx +++ b/apps/apollo-vertex/templates/dashboard/PromptBar.tsx @@ -3,6 +3,7 @@ import { useState } from "react"; import { Badge } from "@/components/ui/badge"; import type { CardConfig, CardGradient } from "./glow-config"; +import { useDashboardData } from "./DashboardDataProvider"; function cardBgStyle( bg: string, @@ -27,9 +28,11 @@ export function PromptBar({ shared, cards, }: { + shared: string; cards: CardConfig; }) { + const { data } = useDashboardData(); const [value, setValue] = useState(""); const hasInput = value.trim().length > 0; @@ -43,14 +46,14 @@ export function PromptBar({ status="info" className="!bg-white/35 !text-foreground opacity-0 translate-y-2 group-focus-within:opacity-100 group-focus-within:translate-y-0 transition-all duration-300 cursor-pointer" > - Show me top risk factors + {data.promptSuggestions[0] ?? "Show me top risk factors"} - Compare Q1 vs Q2 performance + {data.promptSuggestions[1] ?? "Compare Q1 vs Q2 performance"}
@@ -67,7 +70,7 @@ export function PromptBar({ type="text" value={value} onChange={(e) => setValue(e.target.value)} - placeholder="What would you like to understand about loan performance?" + placeholder={data.promptPlaceholder} className="flex-1 bg-transparent text-sm outline-none placeholder:text-muted-foreground" />
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..c96ec7d58 --- /dev/null +++ b/apps/apollo-vertex/templates/dashboard/dashboard-data.ts @@ -0,0 +1,149 @@ +export interface InsightCardData { + title: string; + type: "kpi" | "chart"; + chartType: "donut" | "horizontal-bars" | "sparkline" | "area" | "stacked-bar"; + kpiNumber?: string; + kpiBadge?: string; + kpiDescription?: string; + bars?: { label: string; value: number }[]; + stackedBars?: { label: string; segments: number[] }[]; + stackedLegend?: string[]; + donutPercent?: number; + donutLabel?: string; + donutDescription?: string; + 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", + kpiNumber: "94.2%", + kpiBadge: "+6.8%", + kpiDescription: "Loans finalized on first review without rework.", + }, + { + title: "Top issues", + type: "chart", + chartType: "horizontal-bars", + 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", + 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", + 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: "Product", + badgeText: "Experimental", + greeting: "Good morning, Peter", + headline: "Order throughput doubles as returns drop by 12%.", + subhead: "Returns declined ↓12% month over month while orders increased ↑34%.", + chartLabels: { y: ["500", "375", "250", "125"], target: "Target" }, + promptPlaceholder: "What would you like to understand about order fulfillment?", + promptSuggestions: ["Show delayed shipments", "Compare warehouse performance"], + insightCards: [ + { + title: "On-time delivery rate", + type: "kpi", + chartType: "donut", + kpiNumber: "96.8%", + kpiBadge: "+3.2%", + kpiDescription: "Orders delivered within promised window.", + }, + { + title: "Top return reasons", + type: "chart", + chartType: "horizontal-bars", + bars: [ + { label: "Wrong size/fit", value: 38 }, + { label: "Damaged in transit", value: 24 }, + { label: "Not as described", value: 19 }, + { label: "Late delivery", value: 12 }, + { label: "Changed mind", value: 7 }, + ], + }, + { + title: "Fulfillment volume", + type: "chart", + chartType: "stacked-bar", + stackedBars: [ + { label: "Mon", segments: [120, 45, 8] }, + { label: "Tue", segments: [145, 38, 12] }, + { label: "Wed", segments: [130, 52, 6] }, + { label: "Thu", segments: [160, 35, 15] }, + { label: "Fri", segments: [180, 42, 10] }, + ], + stackedLegend: ["Shipped", "Processing", "Returned"], + }, + { + title: "Customer satisfaction", + type: "kpi", + chartType: "donut", + kpiNumber: "4.7★", + kpiBadge: "+0.3", + kpiDescription: "Average rating across all fulfilled orders.", + }, + ], +}; + +export const datasetPresets: Record = { + default: defaultDataset, + ecommerce: ecommerceDataset, +}; diff --git a/apps/apollo-vertex/templates/dashboard/insight-card-renderers.tsx b/apps/apollo-vertex/templates/dashboard/insight-card-renderers.tsx index f24814741..026fc4eee 100644 --- a/apps/apollo-vertex/templates/dashboard/insight-card-renderers.tsx +++ b/apps/apollo-vertex/templates/dashboard/insight-card-renderers.tsx @@ -2,6 +2,8 @@ import { Badge } from "@/components/ui/badge"; import type { InsightCardContent } from "./glow-config"; +import { useDashboardData } from "./DashboardDataProvider"; +import type { InsightCardData } from "./dashboard-data"; type ViewMode = "desktop" | "compact" | "stacked"; @@ -34,7 +36,7 @@ const kpiSamples = [ }, ]; -const barSamples = [ +const barsWithColor = [ { label: "Risk flag in notes", value: 34, color: "bg-chart-1" }, { label: "Credit report >120 days old", value: 29, color: "bg-chart-2" }, { label: "Owner name mismatch", value: 23, color: "bg-chart-3" }, @@ -47,22 +49,21 @@ const areaPoints = [3, 5, 4, 8, 6, 9, 7, 11, 10, 14, 12, 16]; // --- Renderers --- -function KpiContent({ title, viewMode }: { title: string; viewMode: ViewMode }) { - const sample = kpiSamples.find((s) => s.title === title) ?? kpiSamples[0]; +function KpiContent({ cardData, viewMode }: { cardData: InsightCardData; viewMode: ViewMode }) { if (viewMode === "compact") { return ( <>
- {sample.number} + {cardData.kpiNumber}
- {sample.badge} + {cardData.kpiBadge}

- {sample.description} + {cardData.kpiDescription}

); @@ -71,14 +72,14 @@ function KpiContent({ title, viewMode }: { title: string; viewMode: ViewMode }) return ( <>
- {sample.number} + {cardData.kpiNumber}
- {sample.badge} + {cardData.kpiBadge}

- {sample.description} + {cardData.kpiDescription}

@@ -123,13 +124,17 @@ function DonutContent() { ); } -function HorizontalBarsContent({ viewMode, isExpanded = false }: { viewMode: ViewMode; isExpanded?: boolean }) { +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 = barSamples.reduce((sum, s) => sum + s.value, 0); + const total = barsWithColor.reduce((sum, s) => sum + s.value, 0); return (
- {barSamples.map((issue) => ( + {barsWithColor.map((issue) => (
- {barSamples.map((issue) => { + {barsWithColor.map((issue) => { const pct = Math.round((issue.value / total) * 100); return (
@@ -160,7 +165,7 @@ function HorizontalBarsContent({ viewMode, isExpanded = false }: { viewMode: Vie return (
- {barSamples.map((issue) => ( + {barsWithColor.map((issue) => (
{issue.label} @@ -277,22 +282,29 @@ const stackedBarData = [ }, ]; -function StackedBarContent({ viewMode, isExpanded = false }: { viewMode: ViewMode; isExpanded?: boolean }) { +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( - ...stackedBarData.map((d) => + ...barData.map((d) => d.segments.reduce((sum, s) => sum + s.value, 0), ), ); - const legendItems = [ - { label: "Approved", color: "bg-chart-1" }, - { label: "Pending", color: "bg-chart-2" }, - { label: "Rejected", color: "bg-chart-3" }, - ]; - if (viewMode === "compact" && !isExpanded) { // Summary: aggregate all days into one horizontal stacked bar - const totals = stackedBarData.reduce( + const totals = barData.reduce( (acc, day) => { for (const seg of day.segments) { const key = seg.color; @@ -307,7 +319,7 @@ function StackedBarContent({ viewMode, isExpanded = false }: { viewMode: ViewMod return (
- {legendItems.map((item) => ( + {legend.map((item) => (
- {legendItems.map((item) => { + {legend.map((item) => { const val = totals[item.color] ?? 0; const pct = Math.round((val / grandTotal) * 100); return ( @@ -340,7 +352,7 @@ function StackedBarContent({ viewMode, isExpanded = false }: { viewMode: ViewMod return (
- {stackedBarData.map((bar) => { + {barData.map((bar) => { const total = bar.segments.reduce((sum, s) => sum + s.value, 0); const pct = (total / maxTotal) * 100; return ( @@ -379,7 +391,7 @@ function StackedBarContent({ viewMode, isExpanded = false }: { viewMode: ViewMod })}
- {legendItems.map((item) => ( + {legend.map((item) => (
@@ -394,20 +406,24 @@ function StackedBarContent({ viewMode, isExpanded = false }: { viewMode: ViewMod 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 ; + return ; } - if (content.chartType === "horizontal-bars") return ; - + if (content.chartType === "horizontal-bars") return ; if (content.chartType === "donut") return ; if (content.chartType === "sparkline") return ; - if (content.chartType === "stacked-bar") return ; + if (content.chartType === "stacked-bar") return ; return ; } From 8a6086bd20c5013898e8a4dc73c18cb1b484b23e Mon Sep 17 00:00:00 2001 From: Peter Vachon Date: Thu, 2 Apr 2026 11:28:53 -0400 Subject: [PATCH 28/44] chore(apollo-vertex): Expanded card content --- .../templates/dashboard/DashboardContent.tsx | 27 +-- .../dashboard/ExpandedInsightContent.tsx | 214 ++++++++++++++++++ .../templates/dashboard/GlowDevControls.tsx | 78 ++++--- .../templates/dashboard/InsightGrid.tsx | 71 +++++- .../templates/dashboard/dashboard-data.ts | 46 ++++ 5 files changed, 375 insertions(+), 61 deletions(-) create mode 100644 apps/apollo-vertex/templates/dashboard/ExpandedInsightContent.tsx diff --git a/apps/apollo-vertex/templates/dashboard/DashboardContent.tsx b/apps/apollo-vertex/templates/dashboard/DashboardContent.tsx index d5e7504c2..eb120eda7 100644 --- a/apps/apollo-vertex/templates/dashboard/DashboardContent.tsx +++ b/apps/apollo-vertex/templates/dashboard/DashboardContent.tsx @@ -55,7 +55,7 @@ function ExecutiveLayout({ Autopilot {data.greeting} - +

{data.headline} @@ -64,28 +64,7 @@ function ExecutiveLayout({ {data.subhead}

-
- - - - - - - - - - - - - - - - {yLabels.map((label, i) => ( - {label} - ))} - {data.chartLabels.target} -
-
+ {/* Chart removed — evaluating layout without it */} ); @@ -210,7 +189,7 @@ function DashboardContentInner() { {data.brandName} {data.brandLine}

- {layoutLabels[layout]} Dashboard + {data.dashboardTitle} Dashboard {data.badgeText} diff --git a/apps/apollo-vertex/templates/dashboard/ExpandedInsightContent.tsx b/apps/apollo-vertex/templates/dashboard/ExpandedInsightContent.tsx new file mode 100644 index 000000000..b66826200 --- /dev/null +++ b/apps/apollo-vertex/templates/dashboard/ExpandedInsightContent.tsx @@ -0,0 +1,214 @@ +"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?", + "Show warehouses contributing to damage issues", + "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
+ + {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) => ( +
+
+ {s.label} +
+ ))} +
+

{data.takeaway}

+
+ ); +} + +function CategoryBreakdown() { + return ( +
+
Category breakdown — Wrong size/fit
+
+ {categoryBreakdown.map((cat) => ( +
+ {cat.category} +
+
+
+ {cat.pct}% +
+ ))} +
+

{categoryInsight}

+
+ ); +} + +function TopProducts() { + return ( +
+
Top products driving issues
+
+
+ Product + Return % + Issue + Impact +
+ {topProducts.map((p) => ( +
+ {p.name} + {p.returnRate}% + {p.issue} + {p.impact} +
+ ))} +
+
+ ); +} + +function Recommendations() { + return ( +
+
Recommended actions
+
+ {recommendations.map((rec) => ( +
+ + {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: "trend", label: "Trend" }, + { 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 ( +
+
+ Autopilot + Autopilot + Ask Autopilot +
+
+ {suggestedPrompts.map((prompt) => ( + + ))} +
+
+ ); +} diff --git a/apps/apollo-vertex/templates/dashboard/GlowDevControls.tsx b/apps/apollo-vertex/templates/dashboard/GlowDevControls.tsx index fcdaeaf75..101bfceae 100644 --- a/apps/apollo-vertex/templates/dashboard/GlowDevControls.tsx +++ b/apps/apollo-vertex/templates/dashboard/GlowDevControls.tsx @@ -31,6 +31,7 @@ export function GlowDevControls({ const { data, setDataset } = useDashboardData(); const fileInputRef = useRef(null); const [uploadError, setUploadError] = useState(""); + const [uploadedDatasets, setUploadedDatasets] = useState([]); const configMap: Record = { glow: glowConfig, @@ -41,6 +42,37 @@ export function GlowDevControls({ 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 && (
@@ -92,16 +124,29 @@ export function GlowDevControls({
Preset
@@ -127,32 +172,6 @@ export function GlowDevControls({ > Upload - { - 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; - } - setDataset(parsed); - } catch { - setUploadError("Invalid JSON"); - } - }; - reader.readAsText(file); - e.target.value = ""; - }} - />
{uploadError && (
{uploadError}
@@ -180,5 +199,6 @@ export function GlowDevControls({ )}
+ ); } diff --git a/apps/apollo-vertex/templates/dashboard/InsightGrid.tsx b/apps/apollo-vertex/templates/dashboard/InsightGrid.tsx index d01af9cf7..c10007003 100644 --- a/apps/apollo-vertex/templates/dashboard/InsightGrid.tsx +++ b/apps/apollo-vertex/templates/dashboard/InsightGrid.tsx @@ -11,6 +11,12 @@ import { } 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" }; @@ -86,6 +92,9 @@ function InsightCardInner({ }: InsightCardInnerProps) { const { data } = useDashboardData(); const cardTitle = data.insightCards[cardIndex]?.title ?? cfg.content.title; + const [drilldownTab, setDrilldownTab] = useState("overview"); + const hasDrilldown = cfg.content.chartType === "horizontal-bars"; + const isExpandedWithDrilldown = isThis && isExpanding && hasDrilldown && phase === "full"; const classes = getInsightCardClasses(cfg.content); const isInteractive = cfg.interaction !== "static"; @@ -143,15 +152,52 @@ function InsightCardInner({ )} - - - {cardTitle} - + +
+ + {cardTitle} + + {/* Drilldown tabs — show when expanded with drilldown */} + {isExpandedWithDrilldown && ( +
+ {drilldownTabs.map((tab) => ( + + ))} +
+ )} +
- - + + {/* Show original chart on "overview" tab or when not expanded */} + {(!isExpandedWithDrilldown || drilldownTab === "overview") && ( + + )} + {/* Show drilldown module content */} + {isExpandedWithDrilldown && drilldownTab !== "overview" && ( +
+ +
+ )}
- {isThis && isExpanding && (phase === "height" || phase === "full") && ( + {/* Autopilot prompts — persistent at bottom when expanded with drilldown */} + {isExpandedWithDrilldown && ( +
+ onAutopilotOpen?.()} /> +
+ )} + {/* Non-drilldown expanded content (other card types) */} + {isThis && isExpanding && !hasDrilldown && (phase === "height" || phase === "full") && (
({ cfg, idx: i })) + .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) { diff --git a/apps/apollo-vertex/templates/dashboard/dashboard-data.ts b/apps/apollo-vertex/templates/dashboard/dashboard-data.ts index c96ec7d58..05818b70f 100644 --- a/apps/apollo-vertex/templates/dashboard/dashboard-data.ts +++ b/apps/apollo-vertex/templates/dashboard/dashboard-data.ts @@ -2,15 +2,29 @@ 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[]; } @@ -46,6 +60,8 @@ export const defaultDataset: DashboardDataset = { 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.", @@ -54,6 +70,12 @@ export const defaultDataset: DashboardDataset = { 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 }, @@ -66,6 +88,12 @@ export const defaultDataset: DashboardDataset = { 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] }, @@ -79,6 +107,8 @@ export const defaultDataset: DashboardDataset = { title: "SLA compliance", type: "kpi", chartType: "donut", + size: "sm", + interaction: "static", kpiNumber: "99.5%", kpiBadge: "+1.2%", kpiDescription: "Loans processed within defined SLA thresholds.", @@ -103,6 +133,8 @@ export const ecommerceDataset: DashboardDataset = { title: "On-time delivery rate", type: "kpi", chartType: "donut", + size: "sm", + interaction: "static", kpiNumber: "96.8%", kpiBadge: "+3.2%", kpiDescription: "Orders delivered within promised window.", @@ -111,6 +143,12 @@ export const ecommerceDataset: DashboardDataset = { title: "Top return reasons", type: "chart", chartType: "horizontal-bars", + size: "md", + interaction: "expand", + expandContent: { + summary: "Size/fit issues dominate returns across apparel categories", + details: ["Improve size guide accuracy", "Add AR try-on feature"], + }, bars: [ { label: "Wrong size/fit", value: 38 }, { label: "Damaged in transit", value: 24 }, @@ -123,6 +161,12 @@ export const ecommerceDataset: DashboardDataset = { title: "Fulfillment volume", type: "chart", chartType: "stacked-bar", + size: "md", + interaction: "expand", + expandContent: { + summary: "Friday volumes peak — processing capacity near limit", + details: ["Scale Friday shift staffing", "Review returned item processing time"], + }, stackedBars: [ { label: "Mon", segments: [120, 45, 8] }, { label: "Tue", segments: [145, 38, 12] }, @@ -134,6 +178,8 @@ export const ecommerceDataset: DashboardDataset = { }, { title: "Customer satisfaction", + size: "sm", + interaction: "static", type: "kpi", chartType: "donut", kpiNumber: "4.7★", From 6cb26cde5486bce4bdd060fdcd80d18289140d56 Mon Sep 17 00:00:00 2001 From: Peter Vachon Date: Thu, 2 Apr 2026 12:35:16 -0400 Subject: [PATCH 29/44] chore(apollo-vertex): Expanded content --- .../templates/dashboard/DashboardContent.tsx | 54 +++++----- .../dashboard/ExpandedInsightContent.tsx | 98 +++++++++++-------- .../templates/dashboard/InsightGrid.tsx | 61 +++++++++--- 3 files changed, 132 insertions(+), 81 deletions(-) diff --git a/apps/apollo-vertex/templates/dashboard/DashboardContent.tsx b/apps/apollo-vertex/templates/dashboard/DashboardContent.tsx index eb120eda7..d3fc9b470 100644 --- a/apps/apollo-vertex/templates/dashboard/DashboardContent.tsx +++ b/apps/apollo-vertex/templates/dashboard/DashboardContent.tsx @@ -2,8 +2,8 @@ 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 { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { DashboardGlow } from "./DashboardGlow"; import { cardBgStyle, @@ -41,8 +41,6 @@ function ExecutiveLayout({ 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` }; - const yLabels = data.chartLabels.y; - const yPositions = [15, 25, 30, 45]; const overviewCardEl = ( = { - executive: "Product", - operational: "Operational", - analytics: "Analytics", -}; - type ViewMode = "desktop" | "compact" | "stacked"; function useViewMode(ref: React.RefObject): ViewMode { @@ -131,11 +123,11 @@ function useViewMode(ref: React.RefObject): ViewMode { function DashboardContentInner() { const { data } = useDashboardData(); - const [layout, setLayout] = useState("executive"); + const [layout] = useState("executive"); const [darkGlow, setDarkGlow] = useState(defaultDarkGlow); const [darkCards, setDarkCards] = useState(defaultDarkCards); const [layoutCfg, setLayoutCfg] = useState(defaultLayout); - const [replayCount, setReplayCount] = useState(0); + const [replayCount] = useState(0); const [autopilotOpen, setAutopilotOpen] = useState(false); const [autopilotSource, setAutopilotSource] = useState(""); const [autopilotActiveIdx, setAutopilotActiveIdx] = useState(null); @@ -195,26 +187,26 @@ function DashboardContentInner() {

- setLayout(v as LayoutType)} - className="hidden" - > - - {(Object.keys(layoutLabels) as LayoutType[]).map((key) => ( - - {layoutLabels[key]} - - ))} - - - +
+ + +
{/* Layout content */} diff --git a/apps/apollo-vertex/templates/dashboard/ExpandedInsightContent.tsx b/apps/apollo-vertex/templates/dashboard/ExpandedInsightContent.tsx index b66826200..70c097ee2 100644 --- a/apps/apollo-vertex/templates/dashboard/ExpandedInsightContent.tsx +++ b/apps/apollo-vertex/templates/dashboard/ExpandedInsightContent.tsx @@ -54,9 +54,12 @@ function TrendChart({ data }: { data: typeof trendData }) { const step = w / (data.weeks.length - 1); return ( -
-
Trend over time
- +
+
+
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}`) @@ -74,57 +77,71 @@ function TrendChart({ data }: { data: typeof trendData }) { ); })} -
+
{data.series.map((s) => (
-
- {s.label} +
+ {s.label}
))}
-

{data.takeaway}

+
+

{data.takeaway}

+
); } function CategoryBreakdown() { return ( -
-
Category breakdown — Wrong size/fit
-
+
+
+
Category breakdown
+

Where "Wrong size/fit" returns are concentrated

+
+
{categoryBreakdown.map((cat) => ( -
- {cat.category} -
+
+
+ {cat.category} + {cat.pct}% +
+
+ > +
+
- {cat.pct}%
))}
-

{categoryInsight}

+
+

{categoryInsight}

+
); } function TopProducts() { return ( -
-
Top products driving issues
-
-
+
+
+
Top products driving issues
+

Ranked by return rate with revenue impact

+
+
+
Product Return % Issue Impact
{topProducts.map((p) => ( -
+
{p.name} - {p.returnRate}% + {p.returnRate}% {p.issue} {p.impact}
@@ -136,22 +153,26 @@ function TopProducts() { function Recommendations() { return ( -
-
Recommended actions
-
- {recommendations.map((rec) => ( -
- - {rec.priority} - -
-

{rec.action}

-

{rec.impact}

+
+
+
Recommended actions
+

AI-assisted next steps based on current data

+
+
+ {recommendations.map((rec, i) => ( +
+
+ {i + 1} + + {rec.priority} +
+

{rec.action}

+

{rec.impact}

))}
@@ -165,7 +186,6 @@ export type DrilldownTab = "overview" | "trend" | "categories" | "products" | "a export const drilldownTabs: { key: DrilldownTab; label: string }[] = [ { key: "overview", label: "Overview" }, - { key: "trend", label: "Trend" }, { key: "categories", label: "Categories" }, { key: "products", label: "Products" }, { key: "actions", label: "Actions" }, diff --git a/apps/apollo-vertex/templates/dashboard/InsightGrid.tsx b/apps/apollo-vertex/templates/dashboard/InsightGrid.tsx index c10007003..9d20a2c45 100644 --- a/apps/apollo-vertex/templates/dashboard/InsightGrid.tsx +++ b/apps/apollo-vertex/templates/dashboard/InsightGrid.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { cardBgStyle, @@ -94,6 +94,13 @@ function InsightCardInner({ const cardTitle = data.insightCards[cardIndex]?.title ?? cfg.content.title; const [drilldownTab, setDrilldownTab] = useState("overview"); const hasDrilldown = cfg.content.chartType === "horizontal-bars"; + const wasExpanded = useRef(false); + if (isThis && isExpanding) { + wasExpanded.current = true; + } else if (wasExpanded.current) { + wasExpanded.current = false; + if (drilldownTab !== "overview") setDrilldownTab("overview"); + } const isExpandedWithDrilldown = isThis && isExpanding && hasDrilldown && phase === "full"; const classes = getInsightCardClasses(cfg.content); const isInteractive = cfg.interaction !== "static"; @@ -153,29 +160,57 @@ function InsightCardInner({ )} -
- +
+ {cardTitle} {/* Drilldown tabs — show when expanded with drilldown */} - {isExpandedWithDrilldown && ( -
- {drilldownTabs.map((tab) => ( + {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) => ( ))} + {overflowTabs.length > 0 && ( +
+ + + + +
+ )}
- )} + ); + })()}
@@ -191,8 +226,12 @@ function InsightCardInner({ )} {/* Autopilot prompts — persistent at bottom when expanded with drilldown */} - {isExpandedWithDrilldown && ( -
+ {isThis && isExpanding && hasDrilldown && (phase === "height" || phase === "full") && ( +
onAutopilotOpen?.()} />
)} From 95d55411f552cc203d353aabd2b288d4039a6442 Mon Sep 17 00:00:00 2001 From: Peter Vachon Date: Thu, 2 Apr 2026 12:42:11 -0400 Subject: [PATCH 30/44] chore(apollo-vertex): Small styles adjustments --- apps/apollo-vertex/templates/dashboard/DashboardContent.tsx | 2 +- apps/apollo-vertex/templates/dashboard/InsightGrid.tsx | 2 +- apps/apollo-vertex/templates/dashboard/glow-config.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/apollo-vertex/templates/dashboard/DashboardContent.tsx b/apps/apollo-vertex/templates/dashboard/DashboardContent.tsx index d3fc9b470..68094af06 100644 --- a/apps/apollo-vertex/templates/dashboard/DashboardContent.tsx +++ b/apps/apollo-vertex/templates/dashboard/DashboardContent.tsx @@ -181,7 +181,7 @@ function DashboardContentInner() { {data.brandName} {data.brandLine}

- {data.dashboardTitle} Dashboard + {data.dashboardTitle} {data.badgeText} diff --git a/apps/apollo-vertex/templates/dashboard/InsightGrid.tsx b/apps/apollo-vertex/templates/dashboard/InsightGrid.tsx index 9d20a2c45..14c86a774 100644 --- a/apps/apollo-vertex/templates/dashboard/InsightGrid.tsx +++ b/apps/apollo-vertex/templates/dashboard/InsightGrid.tsx @@ -161,7 +161,7 @@ function InsightCardInner({ )}

- + {cardTitle} {/* Drilldown tabs — show when expanded with drilldown */} diff --git a/apps/apollo-vertex/templates/dashboard/glow-config.ts b/apps/apollo-vertex/templates/dashboard/glow-config.ts index b6b7dc006..6dcf72cdf 100644 --- a/apps/apollo-vertex/templates/dashboard/glow-config.ts +++ b/apps/apollo-vertex/templates/dashboard/glow-config.ts @@ -43,7 +43,7 @@ export const defaultLightGlow: GlowConfig = { export const defaultDarkGlow: GlowConfig = { start: "var(--insight-700)", end: "var(--primary-600)", - containerOpacity: 45, + containerOpacity: 85, fillOpacity: 1, startStopOpacity: 1, endStopOpacity: 0.4, From 038c114bd4099c2ec81b39a1613b45ae39592461 Mon Sep 17 00:00:00 2001 From: Peter Vachon Date: Thu, 2 Apr 2026 13:32:59 -0400 Subject: [PATCH 31/44] chore(apollo-vertex): Expand cleanup --- .../dashboard/ExpandedInsightContent.tsx | 1 - .../templates/dashboard/InsightGrid.tsx | 81 ++++++++++--------- 2 files changed, 43 insertions(+), 39 deletions(-) diff --git a/apps/apollo-vertex/templates/dashboard/ExpandedInsightContent.tsx b/apps/apollo-vertex/templates/dashboard/ExpandedInsightContent.tsx index 70c097ee2..7684cf205 100644 --- a/apps/apollo-vertex/templates/dashboard/ExpandedInsightContent.tsx +++ b/apps/apollo-vertex/templates/dashboard/ExpandedInsightContent.tsx @@ -40,7 +40,6 @@ const recommendations = [ const suggestedPrompts = [ "Why are fit-related returns increasing?", "Which products are driving return volume?", - "Show warehouses contributing to damage issues", "What orders are at risk of return?", ]; diff --git a/apps/apollo-vertex/templates/dashboard/InsightGrid.tsx b/apps/apollo-vertex/templates/dashboard/InsightGrid.tsx index 14c86a774..c1ea03f03 100644 --- a/apps/apollo-vertex/templates/dashboard/InsightGrid.tsx +++ b/apps/apollo-vertex/templates/dashboard/InsightGrid.tsx @@ -68,6 +68,8 @@ interface InsightCardInnerProps { isThis: boolean; phase: ExpandPhase; viewMode: "desktop" | "compact" | "stacked"; + drilldownTab: DrilldownTab; + onDrilldownTabChange: (tab: DrilldownTab) => void; onExpandClick: () => void; onAutopilotOpen?: () => void; isAutopilotActive?: boolean; @@ -86,21 +88,15 @@ function InsightCardInner({ viewMode, onExpandClick, onAutopilotOpen, + drilldownTab, + onDrilldownTabChange, isAutopilotActive = false, className = "", style, }: InsightCardInnerProps) { const { data } = useDashboardData(); const cardTitle = data.insightCards[cardIndex]?.title ?? cfg.content.title; - const [drilldownTab, setDrilldownTab] = useState("overview"); const hasDrilldown = cfg.content.chartType === "horizontal-bars"; - const wasExpanded = useRef(false); - if (isThis && isExpanding) { - wasExpanded.current = true; - } else if (wasExpanded.current) { - wasExpanded.current = false; - if (drilldownTab !== "overview") setDrilldownTab("overview"); - } const isExpandedWithDrilldown = isThis && isExpanding && hasDrilldown && phase === "full"; const classes = getInsightCardClasses(cfg.content); const isInteractive = cfg.interaction !== "static"; @@ -160,22 +156,21 @@ function InsightCardInner({ )} -
- - {cardTitle} - - {/* Drilldown tabs — show when expanded with drilldown */} - {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 ( -
+ + {cardTitle} + + {/* 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) => ( - - - -
-
- {tab === "glow" && ( - - )} - {tab === "cards" && ( - - )} - {tab === "layout" && ( - - )} - {tab === "data" && ( -
-
- Dataset: {data.name} -
-
-
Preset
- v.name === data.name, + )?.[0] ?? `uploaded:${data.name}` } - }} - className="w-full h-7 rounded border bg-background px-1 text-xs" - > - {Object.entries(datasetPresets).map(([key, val]) => ( - - ))} - {uploadedDatasets.map((d) => ( - - ))} - -
-
- - + 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]) => ( + + ))} + {uploadedDatasets.map((d) => ( + + ))} + +
+
+ + +
+ {uploadError && ( +
+ {uploadError} +
+ )}
- {uploadError && ( -
{uploadError}
- )} + )} +
+
Config:
+
+                  {JSON.stringify(currentConfig, null, 2)}
+                
- )} -
-
Config:
-
-                {JSON.stringify(currentConfig, null, 2)}
-              
-
- )} - -
+ +
); } diff --git a/apps/apollo-vertex/templates/dashboard/InsightGrid.tsx b/apps/apollo-vertex/templates/dashboard/InsightGrid.tsx index e06b395f4..f4a78188b 100644 --- a/apps/apollo-vertex/templates/dashboard/InsightGrid.tsx +++ b/apps/apollo-vertex/templates/dashboard/InsightGrid.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect, useRef, useState } from "react"; +import { useEffect, useState } from "react"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { cardBgStyle, @@ -97,16 +97,21 @@ function InsightCardInner({ 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 isExpandedWithDrilldown = + isThis && isExpanding && hasDrilldown && phase === "full"; const classes = getInsightCardClasses(cfg.content); const isInteractive = cfg.interaction !== "static"; return ( @@ -121,7 +126,10 @@ function InsightCardInner({ {onAutopilotOpen && ( @@ -160,52 +180,69 @@ function InsightCardInner({ {cardTitle} {/* 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) => ( - - ))} - {overflowTabs.length > 0 && ( -
- - - - -
- )} -
- ); - })()} + {tab.label} + + ))} + {overflowTabs.length > 0 && ( +
+ + + + +
+ )} +
+ ); + })()} {isExpandedWithDrilldown ? ( /* Expanded with drilldown — unified layout for all tabs */ @@ -213,10 +250,18 @@ function InsightCardInner({
{drilldownTab === "overview" ? ( - + ) : ( )} @@ -229,29 +274,41 @@ function InsightCardInner({ ) : ( /* 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 -
- ) : ( -
-
-
-
-
- )} -
- )} + {isThis && + isExpanding && + !hasDrilldown && + (phase === "height" || phase === "full") && ( +
+ {phase === "full" ? ( +
+ + Additional content + +
+ ) : ( +
+
+
+
+
+ )} +
+ )} ); } @@ -298,12 +355,17 @@ export function InsightGrid({ 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; + 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); @@ -340,7 +402,15 @@ export function InsightGrid({ }); } - const sharedProps = { shared, cards, isExpanding, phase, viewMode, drilldownTab, onDrilldownTabChange: setDrilldownTab }; + const sharedProps = { + shared, + cards, + isExpanding, + phase, + viewMode, + drilldownTab, + onDrilldownTabChange: setDrilldownTab, + }; return (
handleClick(cfg, idx)} - onAutopilotOpen={onAutopilotOpen ? () => onAutopilotOpen(data.insightCards[idx]?.title ?? cfg.content.title, idx) : undefined} + onAutopilotOpen={ + onAutopilotOpen + ? () => + onAutopilotOpen( + data.insightCards[idx]?.title ?? cfg.content.title, + idx, + ) + : undefined + } isAutopilotActive={autopilotActiveIdx === idx} className="h-full" /> @@ -386,9 +464,20 @@ export function InsightGrid({ 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"; + 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(" "); @@ -396,11 +485,16 @@ export function InsightGrid({
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} + 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; @@ -413,11 +507,23 @@ export function InsightGrid({ {...sharedProps} isThis={isThis} onExpandClick={() => handleClick(cfg, idx)} - onAutopilotOpen={onAutopilotOpen ? () => onAutopilotOpen(data.insightCards[idx]?.title ?? cfg.content.title, idx) : undefined} - isAutopilotActive={autopilotActiveIdx === 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)", + 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 index 42650a4d7..9596c715e 100644 --- a/apps/apollo-vertex/templates/dashboard/PromptBar.tsx +++ b/apps/apollo-vertex/templates/dashboard/PromptBar.tsx @@ -28,7 +28,6 @@ export function PromptBar({ shared, cards, }: { - shared: string; cards: CardConfig; }) { diff --git a/apps/apollo-vertex/templates/dashboard/dashboard-data.ts b/apps/apollo-vertex/templates/dashboard/dashboard-data.ts index 52899b9af..c485e06c4 100644 --- a/apps/apollo-vertex/templates/dashboard/dashboard-data.ts +++ b/apps/apollo-vertex/templates/dashboard/dashboard-data.ts @@ -40,7 +40,12 @@ export interface DashboardDataset { chartLabels: { y: string[]; target: string }; promptPlaceholder: string; promptSuggestions: string[]; - insightCards: [InsightCardData, InsightCardData, InsightCardData, InsightCardData]; + insightCards: [ + InsightCardData, + InsightCardData, + InsightCardData, + InsightCardData, + ]; } export const defaultDataset: DashboardDataset = { @@ -51,10 +56,15 @@ export const defaultDataset: DashboardDataset = { 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%.", + 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"], + 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", @@ -92,7 +102,10 @@ export const defaultDataset: DashboardDataset = { interaction: "expand", expandContent: { summary: "Weekly volume trending up with stable rejection rates", - details: ["Monitor Thursday spike pattern", "Review rejected applications"], + details: [ + "Monitor Thursday spike pattern", + "Review rejected applications", + ], }, stackedBars: [ { label: "Mon", segments: [30, 20, 10] }, @@ -123,11 +136,19 @@ export const ecommerceDataset: DashboardDataset = { 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.", + 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?"], + 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", @@ -137,7 +158,8 @@ export const ecommerceDataset: DashboardDataset = { interaction: "static", kpiNumber: "97.1%", kpiBadge: "+2.4%", - kpiDescription: "Orders delivered within promised windows, supported by lower carrier delays and faster pick-pack turnaround.", + kpiDescription: + "Orders delivered within promised windows, supported by lower carrier delays and faster pick-pack turnaround.", }, { title: "Top issues", @@ -146,7 +168,8 @@ export const ecommerceDataset: DashboardDataset = { 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.", + 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", @@ -168,7 +191,8 @@ export const ecommerceDataset: DashboardDataset = { 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.", + 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", @@ -192,7 +216,8 @@ export const ecommerceDataset: DashboardDataset = { interaction: "static", kpiNumber: "4.6", kpiBadge: "+0.2", - kpiDescription: "Average rating remains strong, though recent feedback highlights sizing inconsistency and occasional packaging damage.", + kpiDescription: + "Average rating remains strong, though recent feedback highlights sizing inconsistency and occasional packaging damage.", }, ], }; diff --git a/apps/apollo-vertex/templates/dashboard/insight-card-renderers.tsx b/apps/apollo-vertex/templates/dashboard/insight-card-renderers.tsx index 07adf26e6..a7f1b9e42 100644 --- a/apps/apollo-vertex/templates/dashboard/insight-card-renderers.tsx +++ b/apps/apollo-vertex/templates/dashboard/insight-card-renderers.tsx @@ -9,48 +9,18 @@ type ViewMode = "desktop" | "compact" | "stacked"; // --- Sample data per card type --- -const kpiSamples = [ - { - title: "Upfront decision efficiency", - number: "94.2%", - badge: "+6.8%", - description: "Loans finalized on first review without rework.", - }, - { - title: "SLA compliance", - number: "99.5%", - badge: "+1.2%", - description: "Loans processed within defined SLA thresholds.", - }, - { - title: "Automation rate", - number: "78.3%", - badge: "+4.1%", - description: "Processes completed without manual intervention.", - }, - { - title: "First-pass yield", - number: "91.7%", - badge: "+2.3%", - description: "Documents accepted on initial submission.", - }, -]; - -const barsWithColor = [ - { label: "Risk flag in notes", value: 34, color: "bg-chart-1" }, - { label: "Credit report >120 days old", value: 29, color: "bg-chart-2" }, - { label: "Owner name mismatch", value: 23, color: "bg-chart-3" }, - { label: "High DTI ratio", value: 14, color: "bg-chart-4" }, - { label: "Missing appraisal docs", value: 11, color: "bg-chart-5" }, -]; - 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 }) { - +function KpiContent({ + cardData, + viewMode, +}: { + cardData: InsightCardData; + viewMode: ViewMode; +}) { if (viewMode === "compact") { return ( <> @@ -124,10 +94,27 @@ function DonutContent() { ); } -function HorizontalBarsContent({ cardData, viewMode, isExpanded = false }: { cardData: InsightCardData; viewMode: ViewMode; isExpanded?: boolean }) { +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] })); + 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); @@ -239,51 +226,22 @@ function AreaContent() { ); } -const stackedBarData = [ - { - label: "Mon", - segments: [ - { value: 30, color: "bg-chart-1" }, - { value: 20, color: "bg-chart-2" }, - { value: 10, color: "bg-chart-3" }, - ], - }, - { - label: "Tue", - segments: [ - { value: 40, color: "bg-chart-1" }, - { value: 15, color: "bg-chart-2" }, - { value: 20, color: "bg-chart-3" }, - ], - }, - { - label: "Wed", - segments: [ - { value: 25, color: "bg-chart-1" }, - { value: 30, color: "bg-chart-2" }, - { value: 15, color: "bg-chart-3" }, - ], - }, - { - label: "Thu", - segments: [ - { value: 45, color: "bg-chart-1" }, - { value: 10, color: "bg-chart-2" }, - { value: 25, color: "bg-chart-3" }, - ], - }, - { - label: "Fri", - segments: [ - { value: 35, color: "bg-chart-1" }, - { value: 25, color: "bg-chart-2" }, - { value: 18, color: "bg-chart-3" }, - ], - }, -]; - -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"]; +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, @@ -297,9 +255,7 @@ function StackedBarContent({ cardData, viewMode, isExpanded = false }: { cardDat })), })); const maxTotal = Math.max( - ...barData.map((d) => - d.segments.reduce((sum, s) => sum + s.value, 0), - ), + ...barData.map((d) => d.segments.reduce((sum, s) => sum + s.value, 0)), ); if (viewMode === "compact" && !isExpanded) { @@ -421,9 +377,23 @@ export function InsightCardBody({ if (content.type === "kpi") { return ; } - if (content.chartType === "horizontal-bars") return ; + if (content.chartType === "horizontal-bars") + return ( + + ); if (content.chartType === "donut") return ; if (content.chartType === "sparkline") return ; - if (content.chartType === "stacked-bar") return ; + if (content.chartType === "stacked-bar") + return ( + + ); return ; } From 3c71efefc3a61391b89e5d093de703efd90ed7b5 Mon Sep 17 00:00:00 2001 From: Peter Vachon Date: Thu, 2 Apr 2026 17:27:06 -0400 Subject: [PATCH 37/44] chore(apollo-vertex): quick fix --- apps/apollo-vertex/app/preview/dashboard-minimal/page.tsx | 2 +- apps/apollo-vertex/app/preview/dashboard/page.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/apollo-vertex/app/preview/dashboard-minimal/page.tsx b/apps/apollo-vertex/app/preview/dashboard-minimal/page.tsx index 69bb6d5f5..77f7253b0 100644 --- a/apps/apollo-vertex/app/preview/dashboard-minimal/page.tsx +++ b/apps/apollo-vertex/app/preview/dashboard-minimal/page.tsx @@ -4,7 +4,7 @@ import { DashboardTemplate } from "@/templates/dashboard/DashboardTemplateDynami 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 index 5b063110f..1f046f091 100644 --- a/apps/apollo-vertex/app/preview/dashboard/page.tsx +++ b/apps/apollo-vertex/app/preview/dashboard/page.tsx @@ -4,7 +4,7 @@ import { DashboardTemplate } from "@/templates/dashboard/DashboardTemplateDynami export default function DashboardPreviewPage() { return ( -
+
); From 8391de5cb119b555d811064a75381a326e758d22 Mon Sep 17 00:00:00 2001 From: Peter Vachon Date: Fri, 3 Apr 2026 05:34:34 -0400 Subject: [PATCH 38/44] chore(apollo-vertex): cleanup --- apps/apollo-vertex/app/preview/dashboard-minimal/page.tsx | 2 +- apps/apollo-vertex/app/preview/dashboard/page.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/apollo-vertex/app/preview/dashboard-minimal/page.tsx b/apps/apollo-vertex/app/preview/dashboard-minimal/page.tsx index 77f7253b0..816e8608d 100644 --- a/apps/apollo-vertex/app/preview/dashboard-minimal/page.tsx +++ b/apps/apollo-vertex/app/preview/dashboard-minimal/page.tsx @@ -4,7 +4,7 @@ import { DashboardTemplate } from "@/templates/dashboard/DashboardTemplateDynami 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 index 1f046f091..dd9e38a42 100644 --- a/apps/apollo-vertex/app/preview/dashboard/page.tsx +++ b/apps/apollo-vertex/app/preview/dashboard/page.tsx @@ -4,7 +4,7 @@ import { DashboardTemplate } from "@/templates/dashboard/DashboardTemplateDynami export default function DashboardPreviewPage() { return ( -
+
); From 95b563f9f3e58072ddd0123fead805b22bb03d22 Mon Sep 17 00:00:00 2001 From: Peter Vachon Date: Fri, 3 Apr 2026 06:44:04 -0400 Subject: [PATCH 39/44] chore(apollo-vertex): Rebase and linting --- .../apollo-vertex/app/preview/dashboard-minimal/page.tsx | 9 +++++++-- apps/apollo-vertex/app/preview/dashboard/page.tsx | 9 +++++++-- apps/apollo-vertex/next-env.d.ts | 2 +- apps/apollo-vertex/registry/card/card.tsx | 2 +- 4 files changed, 16 insertions(+), 6 deletions(-) diff --git a/apps/apollo-vertex/app/preview/dashboard-minimal/page.tsx b/apps/apollo-vertex/app/preview/dashboard-minimal/page.tsx index 816e8608d..30acb999a 100644 --- a/apps/apollo-vertex/app/preview/dashboard-minimal/page.tsx +++ b/apps/apollo-vertex/app/preview/dashboard-minimal/page.tsx @@ -1,10 +1,15 @@ "use client"; -import { DashboardTemplate } from "@/templates/dashboard/DashboardTemplateDynamic"; +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 index dd9e38a42..db71ea817 100644 --- a/apps/apollo-vertex/app/preview/dashboard/page.tsx +++ b/apps/apollo-vertex/app/preview/dashboard/page.tsx @@ -1,10 +1,15 @@ "use client"; -import { DashboardTemplate } from "@/templates/dashboard/DashboardTemplateDynamic"; +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/next-env.d.ts b/apps/apollo-vertex/next-env.d.ts index 9edff1c7c..c4b7818fb 100644 --- a/apps/apollo-vertex/next-env.d.ts +++ b/apps/apollo-vertex/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -import "./.next/types/routes.d.ts"; +import "./.next/dev/types/routes.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. 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, From 5e77663dadab9ff9b06cb04395af7516d2a3b66b Mon Sep 17 00:00:00 2001 From: Peter Vachon Date: Fri, 3 Apr 2026 09:02:55 -0400 Subject: [PATCH 40/44] chore(apollo-vertex): KPI card styles --- .../templates/dashboard/InsightGrid.tsx | 2 +- .../templates/dashboard/glow-config.ts | 12 +++-- .../dashboard/insight-card-renderers.tsx | 50 ++++++++++++++++++- 3 files changed, 58 insertions(+), 6 deletions(-) diff --git a/apps/apollo-vertex/templates/dashboard/InsightGrid.tsx b/apps/apollo-vertex/templates/dashboard/InsightGrid.tsx index f4a78188b..b94641e29 100644 --- a/apps/apollo-vertex/templates/dashboard/InsightGrid.tsx +++ b/apps/apollo-vertex/templates/dashboard/InsightGrid.tsx @@ -99,7 +99,7 @@ function InsightCardInner({ const hasDrilldown = cfg.content.chartType === "horizontal-bars"; const isExpandedWithDrilldown = isThis && isExpanding && hasDrilldown && phase === "full"; - const classes = getInsightCardClasses(cfg.content); + const classes = getInsightCardClasses(cfg.content, viewMode); const isInteractive = cfg.interaction !== "static"; return ( diff --git a/apps/apollo-vertex/templates/dashboard/glow-config.ts b/apps/apollo-vertex/templates/dashboard/glow-config.ts index 6dcf72cdf..c48b6fb8f 100644 --- a/apps/apollo-vertex/templates/dashboard/glow-config.ts +++ b/apps/apollo-vertex/templates/dashboard/glow-config.ts @@ -229,14 +229,20 @@ export function cardBgStyle( return { "--card-bg-override": value } as React.CSSProperties; } -export function getInsightCardClasses(content: InsightCardContent): { +export function getInsightCardClasses( + content: InsightCardContent, + viewMode: "desktop" | "compact" | "stacked" = "desktop", +): { cardClassName: string; contentClassName: string; } { if (content.type === "kpi") { + const isCompact = viewMode === "compact"; return { - cardClassName: "!gap-4", - contentClassName: "flex-1 flex flex-col", + 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"; diff --git a/apps/apollo-vertex/templates/dashboard/insight-card-renderers.tsx b/apps/apollo-vertex/templates/dashboard/insight-card-renderers.tsx index a7f1b9e42..ff08e13fd 100644 --- a/apps/apollo-vertex/templates/dashboard/insight-card-renderers.tsx +++ b/apps/apollo-vertex/templates/dashboard/insight-card-renderers.tsx @@ -1,12 +1,58 @@ "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]; @@ -32,9 +78,9 @@ function KpiContent({ {cardData.kpiBadge}
-

+ {cardData.kpiDescription} -

+ ); } From ae9991ea7e54f5de12e561d473a3493f6e4243f3 Mon Sep 17 00:00:00 2001 From: Peter Vachon Date: Fri, 3 Apr 2026 09:36:22 -0400 Subject: [PATCH 41/44] chore(apollo-vertex): Expand load --- apps/apollo-vertex/next-env.d.ts | 2 +- .../templates/dashboard/InsightGrid.tsx | 168 ++++++++---------- .../templates/dashboard/dashboard-data.ts | 4 +- 3 files changed, 73 insertions(+), 101 deletions(-) diff --git a/apps/apollo-vertex/next-env.d.ts b/apps/apollo-vertex/next-env.d.ts index c4b7818fb..9edff1c7c 100644 --- a/apps/apollo-vertex/next-env.d.ts +++ b/apps/apollo-vertex/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -import "./.next/dev/types/routes.d.ts"; +import "./.next/types/routes.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/apps/apollo-vertex/templates/dashboard/InsightGrid.tsx b/apps/apollo-vertex/templates/dashboard/InsightGrid.tsx index b94641e29..e57ca42a2 100644 --- a/apps/apollo-vertex/templates/dashboard/InsightGrid.tsx +++ b/apps/apollo-vertex/templates/dashboard/InsightGrid.tsx @@ -1,7 +1,8 @@ "use client"; import { useEffect, useState } from "react"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { ArrowUpRight, Maximize2, Minimize2 } from "lucide-react"; +import { Card, CardAction, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { cardBgStyle, getInsightCardClasses, @@ -20,43 +21,6 @@ import { const sizeToFr: Record = { sm: "1fr", md: "2fr", lg: "1fr" }; -function DiagonalArrow({ collapsed }: { collapsed: boolean }) { - return ( - - - - - ); -} - -function NavigateIcon() { - return ( - - - - - - ); -} - // --- Shared card inner content --- interface InsightCardInnerProps { @@ -115,70 +79,78 @@ function InsightCardInner({ ...style, }} > - {isInteractive && cfg.interaction === "expand" && ( -
- {onAutopilotOpen && ( - - )} - -
- )} - {isInteractive && cfg.interaction === "navigate" && !isThis && ( - - )} {cardTitle} + {isInteractive && cfg.interaction === "expand" && ( + +
+ {onAutopilotOpen && ( + + )} + +
+
+ )} + {isInteractive && cfg.interaction === "navigate" && !isThis && ( + + + + )} {/* Drilldown tabs — below title when expanded */} {isThis && isExpanding && diff --git a/apps/apollo-vertex/templates/dashboard/dashboard-data.ts b/apps/apollo-vertex/templates/dashboard/dashboard-data.ts index c485e06c4..a16846b58 100644 --- a/apps/apollo-vertex/templates/dashboard/dashboard-data.ts +++ b/apps/apollo-vertex/templates/dashboard/dashboard-data.ts @@ -155,7 +155,7 @@ export const ecommerceDataset: DashboardDataset = { type: "kpi", chartType: "donut", size: "sm", - interaction: "static", + interaction: "navigate", kpiNumber: "97.1%", kpiBadge: "+2.4%", kpiDescription: @@ -213,7 +213,7 @@ export const ecommerceDataset: DashboardDataset = { type: "kpi", chartType: "donut", size: "sm", - interaction: "static", + interaction: "navigate", kpiNumber: "4.6", kpiBadge: "+0.2", kpiDescription: From fd95649525484d7c119df87770d5424211f07319 Mon Sep 17 00:00:00 2001 From: Peter Vachon Date: Fri, 3 Apr 2026 10:08:50 -0400 Subject: [PATCH 42/44] chore(apollo-vertex): Initial autopilot promptbar --- .../templates/dashboard/DashboardContent.tsx | 97 +++++++------ .../templates/dashboard/PromptBar.tsx | 130 +++++++++++++----- 2 files changed, 147 insertions(+), 80 deletions(-) diff --git a/apps/apollo-vertex/templates/dashboard/DashboardContent.tsx b/apps/apollo-vertex/templates/dashboard/DashboardContent.tsx index 9299f5f01..07673ad7a 100644 --- a/apps/apollo-vertex/templates/dashboard/DashboardContent.tsx +++ b/apps/apollo-vertex/templates/dashboard/DashboardContent.tsx @@ -46,60 +46,69 @@ function ExecutiveLayout({ 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` }; - const overviewCardEl = ( - - - Autopilot - Autopilot - - {data.greeting} - - - -
-

- {data.headline} -

-

- {data.subhead} -

-
- {/* Chart removed — evaluating layout without it */} -
-
- ); - - const promptBarEl = ; - return (
-
- {overviewCardEl} - {promptBarEl} +
+
+ + + Autopilot + Autopilot + + {data.greeting} + + + +
+

+ {data.headline} +

+

+ {data.subhead} +

+
+
+
+
+ setPromptExpanded(true)} + onExpand={() => setPromptExpanded(true)} + onCollapse={() => setPromptExpanded(false)} + />
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 ( -
-
-
-
- - {data.promptSuggestions[0] ?? "Show me top risk factors"} - - - {data.promptSuggestions[1] ?? "Compare Q1 vs Q2 performance"} - +
+ {/* 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" /> @@ -76,28 +146,16 @@ export function PromptBar({
-
+

Responses will appear here

+
)} {/* Suggestion badges — hidden when expanded */} @@ -150,7 +163,7 @@ export function PromptBar({ 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" > - +