From 45c6e4c25870ba7dec539184b8ac9d519b927c9b Mon Sep 17 00:00:00 2001 From: Peter Vachon Date: Mon, 20 Apr 2026 09:03:42 -0400 Subject: [PATCH 1/2] =?UTF-8?q?feat(apollo-vertex):=20dashboard=20template?= =?UTF-8?q?=20foundation=20=E2=80=94=20registry,=20data=20layer,=20shell?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds registry token updates (insight color palette, font tokens), nav scaffolding, preview pages, and the core dashboard shell components: DashboardTemplate, DashboardShellWrapper, DashboardRoutes, DashboardContent, DashboardLoading, DashboardDataProvider, and dashboard-data. Also adds Autopilot SVG assets and card component update. Co-Authored-By: Claude Sonnet 4.6 --- apps/apollo-vertex/app/globals.css | 2 + apps/apollo-vertex/app/preview/_meta.ts | 3 + .../app/preview/dashboard-minimal/page.tsx | 16 ++ .../app/preview/dashboard/page.tsx | 16 ++ apps/apollo-vertex/public/Autopilot_dark.svg | 6 + apps/apollo-vertex/public/Autopilot_light.svg | 6 + apps/apollo-vertex/registry.json | 36 +++ apps/apollo-vertex/registry/card/card.tsx | 2 +- .../templates/dashboard/DashboardContent.tsx | 6 + .../dashboard/DashboardDataProvider.tsx | 15 ++ .../templates/dashboard/DashboardLoading.tsx | 135 +++++++++++ .../templates/dashboard/DashboardRoutes.tsx | 59 +++++ .../dashboard/DashboardShellWrapper.tsx | 53 ++++ .../templates/dashboard/DashboardTemplate.tsx | 88 +++++++ .../dashboard/DashboardTemplateDynamic.tsx | 16 ++ .../dashboard/dashboard-data-context.ts | 22 ++ .../templates/dashboard/dashboard-data.ts | 228 ++++++++++++++++++ 17 files changed, 708 insertions(+), 1 deletion(-) 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/public/Autopilot_dark.svg create mode 100644 apps/apollo-vertex/public/Autopilot_light.svg create mode 100644 apps/apollo-vertex/templates/dashboard/DashboardContent.tsx create mode 100644 apps/apollo-vertex/templates/dashboard/DashboardDataProvider.tsx create mode 100644 apps/apollo-vertex/templates/dashboard/DashboardLoading.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 create mode 100644 apps/apollo-vertex/templates/dashboard/dashboard-data-context.ts create mode 100644 apps/apollo-vertex/templates/dashboard/dashboard-data.ts diff --git a/apps/apollo-vertex/app/globals.css b/apps/apollo-vertex/app/globals.css index 646f02405..b6ebbc8fb 100644 --- a/apps/apollo-vertex/app/globals.css +++ b/apps/apollo-vertex/app/globals.css @@ -1,4 +1,6 @@ @import 'tailwindcss'; +@source "../registry"; +@source "../templates"; /* Optional: import Nextra theme styles */ @import 'nextra-theme-docs/style.css'; diff --git a/apps/apollo-vertex/app/preview/_meta.ts b/apps/apollo-vertex/app/preview/_meta.ts new file mode 100644 index 000000000..e48eafb8a --- /dev/null +++ b/apps/apollo-vertex/app/preview/_meta.ts @@ -0,0 +1,3 @@ +export default { + "*": { display: "hidden" }, +}; diff --git a/apps/apollo-vertex/app/preview/dashboard-minimal/page.tsx b/apps/apollo-vertex/app/preview/dashboard-minimal/page.tsx new file mode 100644 index 000000000..30acb999a --- /dev/null +++ b/apps/apollo-vertex/app/preview/dashboard-minimal/page.tsx @@ -0,0 +1,16 @@ +"use client"; + +import dynamic from "next/dynamic"; + +const DashboardTemplate = dynamic( + () => import("@/templates/dashboard/DashboardTemplate").then((mod) => mod.DashboardTemplate), + { ssr: false }, +); + +export default function DashboardMinimalPreviewPage() { + return ( +
+ +
+ ); +} diff --git a/apps/apollo-vertex/app/preview/dashboard/page.tsx b/apps/apollo-vertex/app/preview/dashboard/page.tsx new file mode 100644 index 000000000..db71ea817 --- /dev/null +++ b/apps/apollo-vertex/app/preview/dashboard/page.tsx @@ -0,0 +1,16 @@ +"use client"; + +import dynamic from "next/dynamic"; + +const DashboardTemplate = dynamic( + () => import("@/templates/dashboard/DashboardTemplate").then((mod) => mod.DashboardTemplate), + { ssr: false }, +); + +export default function DashboardPreviewPage() { + return ( +
+ +
+ ); +} diff --git a/apps/apollo-vertex/public/Autopilot_dark.svg b/apps/apollo-vertex/public/Autopilot_dark.svg new file mode 100644 index 000000000..5f35c08dc --- /dev/null +++ b/apps/apollo-vertex/public/Autopilot_dark.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/apps/apollo-vertex/public/Autopilot_light.svg b/apps/apollo-vertex/public/Autopilot_light.svg new file mode 100644 index 000000000..4f28c9a53 --- /dev/null +++ b/apps/apollo-vertex/public/Autopilot_light.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/apps/apollo-vertex/registry.json b/apps/apollo-vertex/registry.json index aa715781d..ac8939b6a 100644 --- a/apps/apollo-vertex/registry.json +++ b/apps/apollo-vertex/registry.json @@ -66,6 +66,16 @@ "color-sidebar-accent-foreground": "var(--sidebar-accent-foreground)", "color-sidebar-border": "var(--sidebar-border)", "color-sidebar-ring": "var(--sidebar-ring)", + "color-insight-50": "var(--insight-50)", + "color-insight-100": "var(--insight-100)", + "color-insight-200": "var(--insight-200)", + "color-insight-300": "var(--insight-300)", + "color-insight-400": "var(--insight-400)", + "color-insight-500": "var(--insight-500)", + "color-insight-600": "var(--insight-600)", + "color-insight-700": "var(--insight-700)", + "color-insight-800": "var(--insight-800)", + "color-insight-900": "var(--insight-900)", "font-sans": "var(--font-sans)" }, "light": { @@ -120,6 +130,19 @@ "sidebar-accent-foreground": "oklch(0.1660 0.0283 203.3380)", "sidebar-border": "oklch(0.9237 0.0133 262.3780)", "sidebar-ring": "oklch(0.64 0.115 208)", + "insight-50": "oklch(0.96 0.03 277)", + "insight-100": "oklch(0.92 0.05 277)", + "insight-200": "oklch(0.86 0.09 277)", + "insight-300": "oklch(0.78 0.14 277)", + "insight-400": "oklch(0.70 0.19 277)", + "insight-500": "oklch(0.62 0.22 277)", + "insight-600": "oklch(0.56 0.20 277)", + "insight-700": "oklch(0.48 0.17 277)", + "insight-800": "oklch(0.38 0.13 278)", + "insight-900": "oklch(0.30 0.10 278)", + "font-sans": "Inter, ui-sans-serif, sans-serif, system-ui", + "font-serif": "IBM Plex Serif, ui-serif, serif", + "font-mono": "IBM Plex Mono, ui-monospace, monospace", "radius": "0.625rem", "shadow-x": "0", "shadow-y": "0px", @@ -206,6 +229,19 @@ "sidebar-accent-foreground": "oklch(0.9525 0.0110 225.9830)", "sidebar-border": "oklch(0.9525 0.0110 225.9830)", "sidebar-ring": "oklch(0.69 0.112 207)", + "insight-50": "oklch(0.96 0.03 277)", + "insight-100": "oklch(0.92 0.05 277)", + "insight-200": "oklch(0.86 0.09 277)", + "insight-300": "oklch(0.78 0.14 277)", + "insight-400": "oklch(0.70 0.19 277)", + "insight-500": "oklch(0.62 0.22 277)", + "insight-600": "oklch(0.56 0.20 277)", + "insight-700": "oklch(0.48 0.17 277)", + "insight-800": "oklch(0.38 0.13 278)", + "insight-900": "oklch(0.30 0.10 278)", + "font-sans": "Inter, ui-sans-serif, sans-serif, system-ui", + "font-serif": "IBM Plex Serif, ui-serif, serif", + "font-mono": "IBM Plex Mono, ui-monospace, monospace", "radius": "0.625rem", "shadow-x": "0", "shadow-y": "0px", diff --git a/apps/apollo-vertex/registry/card/card.tsx b/apps/apollo-vertex/registry/card/card.tsx index 7bd13d2e1..f61a8ae2d 100644 --- a/apps/apollo-vertex/registry/card/card.tsx +++ b/apps/apollo-vertex/registry/card/card.tsx @@ -10,7 +10,7 @@ export const GLASS_CLASSES = [ "dark:shadow-[0_2px_24px_2px_rgba(0,0,0,0.12),inset_0_1px_0_0_color-mix(in_srgb,var(--sidebar)_5%,transparent)]", ] as const; -const cardVariants = cva("flex flex-col text-card-foreground", { +const cardVariants = cva("flex flex-col gap-6 py-6 text-card-foreground", { variants: { variant: { default: GLASS_CLASSES, diff --git a/apps/apollo-vertex/templates/dashboard/DashboardContent.tsx b/apps/apollo-vertex/templates/dashboard/DashboardContent.tsx new file mode 100644 index 000000000..70dd812bd --- /dev/null +++ b/apps/apollo-vertex/templates/dashboard/DashboardContent.tsx @@ -0,0 +1,6 @@ +"use client"; + +// Stub — full implementation is added in a later PR once all dependencies are available. +export function DashboardContent() { + return null; +} diff --git a/apps/apollo-vertex/templates/dashboard/DashboardDataProvider.tsx b/apps/apollo-vertex/templates/dashboard/DashboardDataProvider.tsx new file mode 100644 index 000000000..c29b620f7 --- /dev/null +++ b/apps/apollo-vertex/templates/dashboard/DashboardDataProvider.tsx @@ -0,0 +1,15 @@ +"use client"; + +import { useState, type ReactNode } from "react"; +import { ecommerceDataset, type DashboardDataset } from "./dashboard-data"; +import { DashboardDataContext } from "./dashboard-data-context"; + +export function DashboardDataProvider({ children }: { children: ReactNode }) { + const [data, setData] = useState(ecommerceDataset); + + return ( + + {children} + + ); +} diff --git a/apps/apollo-vertex/templates/dashboard/DashboardLoading.tsx b/apps/apollo-vertex/templates/dashboard/DashboardLoading.tsx new file mode 100644 index 000000000..c023f29d8 --- /dev/null +++ b/apps/apollo-vertex/templates/dashboard/DashboardLoading.tsx @@ -0,0 +1,135 @@ +"use client"; + +import { useEffect, useState } from "react"; + +type Phase = "logo" | "skeleton" | "done"; + +interface DashboardLoadingProps { + children: React.ReactNode; + triggerReplay?: number; +} + +function LogoPhase({ exiting }: { exiting: boolean }) { + return ( +
+ {/* Morphing glow */} +
+
+
+
+
+
+ + {/* App icon */} +
+ 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); + + useEffect(() => { + if (triggerReplay === 0) return; + if (triggerReplay) { + setExiting(false); + setPhase("logo"); + } + }, [triggerReplay]); + + useEffect(() => { + if (phase === "done") return; + + if (phase === "logo") { + const timer = setTimeout(() => { + setExiting(true); + setTimeout(() => { + setExiting(false); + setPhase("skeleton"); + }, 500); + }, 2000); + return () => clearTimeout(timer); + } + + if (phase === "skeleton") { + const timer = setTimeout(() => { + setExiting(true); + setTimeout(() => { + setPhase("done"); + }, 500); + }, 1000); + return () => clearTimeout(timer); + } + }, [phase]); + + if (phase === "done") { + return ( +
{children}
+ ); + } + + return ( +
+ {phase === "logo" && } + {phase === "skeleton" && } +
+ ); +} diff --git a/apps/apollo-vertex/templates/dashboard/DashboardRoutes.tsx b/apps/apollo-vertex/templates/dashboard/DashboardRoutes.tsx new file mode 100644 index 000000000..30965402e --- /dev/null +++ b/apps/apollo-vertex/templates/dashboard/DashboardRoutes.tsx @@ -0,0 +1,59 @@ +import { createRootRoute, createRoute, Outlet } from "@tanstack/react-router"; +import { DashboardContent } from "./DashboardContent"; +import { DashboardShellWrapper } from "./DashboardShellWrapper"; + +export const dashboardRootRoute = createRootRoute(); + +// --- Sidebar variant routes --- + +export const dashboardShellRoute = createRoute({ + getParentRoute: () => dashboardRootRoute, + path: "/preview/dashboard", + component: () => ( + + + + ), +}); + +export const dashboardIndexRoute = createRoute({ + getParentRoute: () => dashboardShellRoute, + path: "/", + component: DashboardContent, +}); + +export const dashboardHomeRoute = createRoute({ + getParentRoute: () => dashboardShellRoute, + path: "/home", + component: DashboardContent, +}); + +export const dashboardCatchAllRoute = createRoute({ + getParentRoute: () => dashboardShellRoute, + path: "$", + component: DashboardContent, +}); + +// --- Minimal variant routes --- + +export const dashboardMinimalShellRoute = createRoute({ + getParentRoute: () => dashboardRootRoute, + path: "/preview/dashboard-minimal", + component: () => ( + + + + ), +}); + +export const dashboardMinimalIndexRoute = createRoute({ + getParentRoute: () => dashboardMinimalShellRoute, + path: "/", + component: DashboardContent, +}); + +export const dashboardMinimalCatchAllRoute = createRoute({ + getParentRoute: () => dashboardMinimalShellRoute, + path: "$", + component: DashboardContent, +}); diff --git a/apps/apollo-vertex/templates/dashboard/DashboardShellWrapper.tsx b/apps/apollo-vertex/templates/dashboard/DashboardShellWrapper.tsx new file mode 100644 index 000000000..95b838514 --- /dev/null +++ b/apps/apollo-vertex/templates/dashboard/DashboardShellWrapper.tsx @@ -0,0 +1,53 @@ +import type { ReactNode } from "react"; +import type { ShellNavItem } from "@/registry/shell/shell"; +import { ApolloShell } from "@/registry/shell/shell"; +import { SidebarProvider } from "@/components/ui/sidebar"; +import { BarChart3, FolderOpen, Home, Settings, Users } from "lucide-react"; + +const sidebarNavItems: ShellNavItem[] = [ + { path: "/preview/dashboard/home", label: "dashboard", icon: Home }, + { path: "/preview/dashboard/projects", label: "projects", icon: FolderOpen }, + { path: "/preview/dashboard/analytics", label: "analytics", icon: BarChart3 }, + { path: "/preview/dashboard/team", label: "team", icon: Users }, + { path: "/preview/dashboard/settings", label: "settings", icon: Settings }, +]; + +const minimalNavItems: ShellNavItem[] = [ + { path: "/preview/dashboard-minimal", label: "dashboard", icon: Home }, + { + path: "/preview/dashboard-minimal/projects", + label: "projects", + icon: FolderOpen, + }, + { + path: "/preview/dashboard-minimal/analytics", + label: "analytics", + icon: BarChart3, + }, +]; + +export function DashboardShellWrapper({ + variant, + children, +}: { + variant?: "minimal"; + children: ReactNode; +}) { + return ( + + + {children} + + + ); +} diff --git a/apps/apollo-vertex/templates/dashboard/DashboardTemplate.tsx b/apps/apollo-vertex/templates/dashboard/DashboardTemplate.tsx new file mode 100644 index 000000000..5e2d6ae00 --- /dev/null +++ b/apps/apollo-vertex/templates/dashboard/DashboardTemplate.tsx @@ -0,0 +1,88 @@ +"use client"; + +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { + createMemoryHistory, + createRouter, + RouterProvider, +} from "@tanstack/react-router"; +import { useEffect, useState } from "react"; +import { + dashboardCatchAllRoute, + dashboardHomeRoute, + dashboardIndexRoute, + dashboardMinimalCatchAllRoute, + dashboardMinimalIndexRoute, + dashboardMinimalShellRoute, + dashboardRootRoute, + dashboardShellRoute, +} from "./DashboardRoutes"; + +export interface DashboardTemplateProps { + shellVariant?: "minimal"; +} + +const DASHBOARD_PREVIEW_PATH_KEY = "dashboard-preview-path"; +const DASHBOARD_MINIMAL_PREVIEW_PATH_KEY = "dashboard-minimal-preview-path"; + +type DashboardPreviewPathKey = + | typeof DASHBOARD_PREVIEW_PATH_KEY + | typeof DASHBOARD_MINIMAL_PREVIEW_PATH_KEY; + +const queryClient = new QueryClient(); + +const routeTree = dashboardRootRoute.addChildren([ + dashboardShellRoute.addChildren([ + dashboardIndexRoute, + dashboardHomeRoute, + dashboardCatchAllRoute, + ]), + dashboardMinimalShellRoute.addChildren([ + dashboardMinimalIndexRoute, + dashboardMinimalCatchAllRoute, + ]), +]); + +function getInitialEntry( + storageKey: DashboardPreviewPathKey, + variant?: "minimal", +) { + const stored = localStorage.getItem(storageKey); + if (stored) return stored; + return variant === "minimal" + ? "/preview/dashboard-minimal" + : "/preview/dashboard"; +} + +function createDashboardRouter( + storageKey: DashboardPreviewPathKey, + variant?: "minimal", +) { + const history = createMemoryHistory({ + initialEntries: [getInitialEntry(storageKey, variant)], + }); + return createRouter({ routeTree, history }); +} + +export function DashboardTemplate({ shellVariant }: DashboardTemplateProps) { + const storageKey = + shellVariant === "minimal" + ? DASHBOARD_MINIMAL_PREVIEW_PATH_KEY + : DASHBOARD_PREVIEW_PATH_KEY; + const [router] = useState(() => + createDashboardRouter(storageKey, shellVariant), + ); + + useEffect(() => { + const unsubscribe = router.subscribe("onResolved", ({ toLocation }) => { + localStorage.setItem(storageKey, toLocation.pathname); + }); + return unsubscribe; + }, [router, storageKey]); + + return ( + + + + ); +} diff --git a/apps/apollo-vertex/templates/dashboard/DashboardTemplateDynamic.tsx b/apps/apollo-vertex/templates/dashboard/DashboardTemplateDynamic.tsx new file mode 100644 index 000000000..8e522eaa5 --- /dev/null +++ b/apps/apollo-vertex/templates/dashboard/DashboardTemplateDynamic.tsx @@ -0,0 +1,16 @@ +"use client"; + +import dynamic from "next/dynamic"; +import type React from "react"; + +type DashboardTemplateProps = React.ComponentProps< + typeof import("./DashboardTemplate").DashboardTemplate +>; + +export const DashboardTemplate = dynamic( + () => + import("./DashboardTemplate").then((mod) => ({ + default: mod.DashboardTemplate, + })), + { ssr: false }, +); diff --git a/apps/apollo-vertex/templates/dashboard/dashboard-data-context.ts b/apps/apollo-vertex/templates/dashboard/dashboard-data-context.ts new file mode 100644 index 000000000..f628d6ac0 --- /dev/null +++ b/apps/apollo-vertex/templates/dashboard/dashboard-data-context.ts @@ -0,0 +1,22 @@ +"use client"; + +import { createContext, useContext } from "react"; +import { ecommerceDataset, type DashboardDataset } from "./dashboard-data"; + +export interface DashboardDataContextValue { + data: DashboardDataset; + setDataset: (data: DashboardDataset) => void; +} + +export const DashboardDataContext = createContext({ + data: ecommerceDataset, + setDataset: () => { + throw new Error( + "useDashboardData must be used within DashboardDataProvider", + ); + }, +}); + +export function useDashboardData() { + return useContext(DashboardDataContext); +} diff --git a/apps/apollo-vertex/templates/dashboard/dashboard-data.ts b/apps/apollo-vertex/templates/dashboard/dashboard-data.ts new file mode 100644 index 000000000..a16846b58 --- /dev/null +++ b/apps/apollo-vertex/templates/dashboard/dashboard-data.ts @@ -0,0 +1,228 @@ +export interface InsightCardData { + title: string; + type: "kpi" | "chart"; + chartType: "donut" | "horizontal-bars" | "sparkline" | "area" | "stacked-bar"; + size?: "sm" | "md" | "lg"; + interaction?: "static" | "expand" | "navigate"; + // Navigate config + navigateTo?: string; + // Expand config — additional content shown when card is expanded + expandContent?: { + summary?: string; + details?: string[]; + }; + // KPI data + kpiNumber?: string; + kpiBadge?: string; + kpiDescription?: string; + // Horizontal bars data + bars?: { label: string; value: number }[]; + // Stacked bar data + stackedBars?: { label: string; segments: number[] }[]; + stackedLegend?: string[]; + // Donut data + donutPercent?: number; + donutLabel?: string; + donutDescription?: string; + // Sparkline / Area data + points?: number[]; +} + +export interface DashboardDataset { + name: string; + brandName: string; + brandLine: string; + dashboardTitle: string; + badgeText: string; + greeting: string; + headline: string; + subhead: string; + chartLabels: { y: string[]; target: string }; + promptPlaceholder: string; + promptSuggestions: string[]; + insightCards: [ + InsightCardData, + InsightCardData, + InsightCardData, + InsightCardData, + ]; +} + +export const defaultDataset: DashboardDataset = { + name: "Loan Setup", + brandName: "UiPath", + brandLine: "Vertical Solutions", + dashboardTitle: "Product", + badgeText: "Experimental", + greeting: "Good morning, Peter", + headline: "Loan volume scales as setup time drops by 3.5 days.", + subhead: + "Setup time declined ↓21% month over month while volume increased ↑18%.", + chartLabels: { y: ["200", "150", "100", "50"], target: "Target" }, + promptPlaceholder: + "What would you like to understand about loan performance?", + promptSuggestions: [ + "Show me top risk factors", + "Compare Q1 vs Q2 performance", + ], + insightCards: [ + { + title: "Upfront decision efficiency", + type: "kpi", + chartType: "donut", + size: "sm", + interaction: "static", + kpiNumber: "94.2%", + kpiBadge: "+6.8%", + kpiDescription: "Loans finalized on first review without rework.", + }, + { + title: "Top issues", + type: "chart", + chartType: "horizontal-bars", + size: "md", + interaction: "expand", + expandContent: { + summary: "Risk flags have increased 12% this quarter", + details: ["Review underwriting criteria", "Update risk scoring model"], + }, + bars: [ + { label: "Risk flag in notes", value: 34 }, + { label: "Credit report >120 days old", value: 29 }, + { label: "Owner name mismatch", value: 23 }, + { label: "High DTI ratio", value: 14 }, + { label: "Missing appraisal docs", value: 11 }, + ], + }, + { + title: "Pipeline", + type: "chart", + chartType: "stacked-bar", + size: "md", + interaction: "expand", + expandContent: { + summary: "Weekly volume trending up with stable rejection rates", + details: [ + "Monitor Thursday spike pattern", + "Review rejected applications", + ], + }, + stackedBars: [ + { label: "Mon", segments: [30, 20, 10] }, + { label: "Tue", segments: [40, 15, 20] }, + { label: "Wed", segments: [25, 30, 15] }, + { label: "Thu", segments: [45, 10, 25] }, + { label: "Fri", segments: [35, 25, 18] }, + ], + stackedLegend: ["Approved", "Pending", "Rejected"], + }, + { + title: "SLA compliance", + type: "kpi", + chartType: "donut", + size: "sm", + interaction: "static", + kpiNumber: "99.5%", + kpiBadge: "+1.2%", + kpiDescription: "Loans processed within defined SLA thresholds.", + }, + ], +}; + +export const ecommerceDataset: DashboardDataset = { + name: "E-commerce Order Fulfillment", + brandName: "UiPath", + brandLine: "Vertical Solutions", + dashboardTitle: "Order fulfillment", + badgeText: "Experimental", + greeting: "Good morning, Peter", + headline: + "Order volume climbs as delivery performance improves, but fit-related returns remain the biggest drag on margin.", + subhead: + "Orders shipped increased ↑26% month over month while on-time delivery improved ↑2.4%, with size and fit issues now driving the largest share of returns.", + chartLabels: { y: ["600", "450", "300", "150"], target: "Target" }, + promptPlaceholder: + "What would you like to understand about order fulfillment?", + promptSuggestions: [ + "Why are fit-related returns increasing?", + "Show me products driving return volume", + "Compare warehouse performance", + "Which orders are most at risk of delay?", + ], + insightCards: [ + { + title: "On-time delivery rate", + type: "kpi", + chartType: "donut", + size: "sm", + interaction: "navigate", + kpiNumber: "97.1%", + kpiBadge: "+2.4%", + kpiDescription: + "Orders delivered within promised windows, supported by lower carrier delays and faster pick-pack turnaround.", + }, + { + title: "Top issues", + type: "chart", + chartType: "horizontal-bars", + size: "md", + interaction: "expand", + expandContent: { + summary: + "Return-related friction is now concentrated in product fit, transit handling, and expectation gaps, with apparel and footwear accounting for the highest exception volume.", + details: [ + "Investigate top SKUs contributing to wrong size and fit returns", + "Review packaging and carrier handoff for damage-related issues by warehouse", + "Use AI prompts to explain issue concentration by category, region, and fulfillment center", + ], + }, + bars: [ + { label: "Wrong size/fit", value: 39 }, + { label: "Damaged in transit", value: 23 }, + { label: "Not as described", value: 18 }, + { label: "Late delivery", value: 13 }, + { label: "Changed mind", value: 7 }, + ], + }, + { + title: "Pipeline", + type: "chart", + chartType: "stacked-bar", + size: "md", + interaction: "expand", + expandContent: { + summary: + "Fulfillment volume builds steadily through the week, with the highest shipped volume on Thursday and Friday and a midweek rise in processing backlog.", + details: [ + "Monitor Wednesday processing buildup for labor or inventory bottlenecks", + "Review Thursday and Friday shipment spikes by warehouse and carrier", + "Use AI prompts to identify whether growth is concentrated in apparel, footwear, or home goods", + ], + }, + stackedBars: [ + { label: "Mon", segments: [188, 46, 11] }, + { label: "Tue", segments: [204, 41, 13] }, + { label: "Wed", segments: [198, 57, 14] }, + { label: "Thu", segments: [236, 38, 15] }, + { label: "Fri", segments: [249, 43, 16] }, + ], + stackedLegend: ["Shipped", "Processing", "Returned"], + }, + { + title: "Customer satisfaction", + type: "kpi", + chartType: "donut", + size: "sm", + interaction: "navigate", + kpiNumber: "4.6", + kpiBadge: "+0.2", + kpiDescription: + "Average rating remains strong, though recent feedback highlights sizing inconsistency and occasional packaging damage.", + }, + ], +}; + +export const datasetPresets: Record = { + default: defaultDataset, + ecommerce: ecommerceDataset, +}; From e49b9879976a6657366a8836aae4aee74ae0a542 Mon Sep 17 00:00:00 2001 From: Peter Vachon Date: Tue, 28 Apr 2026 10:58:01 -0400 Subject: [PATCH 2/2] feat(apollo-vertex): glow config, card renderers, and drilldown content --- .../app/preview/dashboard-minimal/page.tsx | 5 +- .../app/preview/dashboard/page.tsx | 5 +- .../dashboard/ExpandedInsightContent.tsx | 326 ++++++++++++++++ .../templates/dashboard/glow-config.ts | 294 +++++++++++++++ .../dashboard/insight-card-renderers.tsx | 349 ++++++++++++++++++ 5 files changed, 977 insertions(+), 2 deletions(-) create mode 100644 apps/apollo-vertex/templates/dashboard/ExpandedInsightContent.tsx create mode 100644 apps/apollo-vertex/templates/dashboard/glow-config.ts create mode 100644 apps/apollo-vertex/templates/dashboard/insight-card-renderers.tsx diff --git a/apps/apollo-vertex/app/preview/dashboard-minimal/page.tsx b/apps/apollo-vertex/app/preview/dashboard-minimal/page.tsx index 30acb999a..deba5cb66 100644 --- a/apps/apollo-vertex/app/preview/dashboard-minimal/page.tsx +++ b/apps/apollo-vertex/app/preview/dashboard-minimal/page.tsx @@ -3,7 +3,10 @@ import dynamic from "next/dynamic"; const DashboardTemplate = dynamic( - () => import("@/templates/dashboard/DashboardTemplate").then((mod) => mod.DashboardTemplate), + () => + import("@/templates/dashboard/DashboardTemplate").then( + (mod) => mod.DashboardTemplate, + ), { ssr: false }, ); diff --git a/apps/apollo-vertex/app/preview/dashboard/page.tsx b/apps/apollo-vertex/app/preview/dashboard/page.tsx index db71ea817..018db5cc2 100644 --- a/apps/apollo-vertex/app/preview/dashboard/page.tsx +++ b/apps/apollo-vertex/app/preview/dashboard/page.tsx @@ -3,7 +3,10 @@ import dynamic from "next/dynamic"; const DashboardTemplate = dynamic( - () => import("@/templates/dashboard/DashboardTemplate").then((mod) => mod.DashboardTemplate), + () => + import("@/templates/dashboard/DashboardTemplate").then( + (mod) => mod.DashboardTemplate, + ), { ssr: false }, ); diff --git a/apps/apollo-vertex/templates/dashboard/ExpandedInsightContent.tsx b/apps/apollo-vertex/templates/dashboard/ExpandedInsightContent.tsx new file mode 100644 index 000000000..71f1fd239 --- /dev/null +++ b/apps/apollo-vertex/templates/dashboard/ExpandedInsightContent.tsx @@ -0,0 +1,326 @@ +"use client"; + +import { useState } from "react"; +import { Badge } from "@/components/ui/badge"; +import type { DrilldownTab } from "./drilldown-tabs"; + +// --- Sample data --- + +const trendData = { + weeks: ["W1", "W2", "W3", "W4", "W5", "W6", "W7", "W8"], + series: [ + { + label: "Wrong size/fit", + color: "bg-chart-1", + stroke: "stroke-chart-1", + values: [31, 33, 34, 35, 36, 37, 39, 41], + }, + { + label: "Damaged in transit", + color: "bg-chart-2", + stroke: "stroke-chart-2", + values: [25, 24, 26, 23, 22, 24, 23, 21], + }, + { + label: "Not as described", + color: "bg-chart-3", + stroke: "stroke-chart-3", + values: [20, 19, 18, 19, 18, 17, 18, 17], + }, + ], + takeaway: + "Fit-related returns have grown steadily over 8 weeks (+32%), while damage and description issues remain flat.", +}; + +const categoryBreakdown = [ + { category: "Women's Apparel", pct: 48, highlight: true }, + { category: "Footwear", pct: 27 }, + { category: "Men's Apparel", pct: 16 }, + { category: "Accessories", pct: 9 }, +]; +const categoryInsight = + "Women's apparel and footwear account for 75% of all fit-related returns. Sizing inconsistency across brands is the primary driver."; + +const topProducts = [ + { + name: "Slim Fit Chinos — Navy", + returnRate: 18.4, + issue: "Wrong size", + impact: "$12,400", + }, + { + name: "Running Shoe Pro V2", + returnRate: 15.2, + issue: "Wrong fit", + impact: "$9,800", + }, + { + name: "Wrap Dress — Floral", + returnRate: 14.7, + issue: "Wrong size", + impact: "$8,200", + }, + { + name: "Oversized Hoodie — Black", + returnRate: 12.1, + issue: "Too large", + impact: "$6,900", + }, + { + name: "Ankle Boot — Tan", + returnRate: 11.8, + issue: "Wrong fit", + impact: "$5,400", + }, +]; + +const recommendations = [ + { + action: "Deploy dynamic size recommendation for top 3 SKUs", + impact: "Est. 22% reduction in fit returns", + priority: "High", + }, + { + action: "Add fit-specific review prompts to product pages", + impact: "Improve size confidence pre-purchase", + priority: "Medium", + }, + { + action: "Flag brands with >15% size variance for supplier review", + impact: "Address root cause across catalog", + priority: "Medium", + }, +]; + +const suggestedPrompts = [ + "Why are fit-related returns increasing?", + "Which products are driving return volume?", + "What orders are at risk of return?", +]; + +// --- Components --- + +function TrendChart({ data }: { data: typeof trendData }) { + const allValues = data.series.flatMap((s) => s.values); + const max = Math.max(...allValues); + const h = 60; + const w = 180; + const step = w / (data.weeks.length - 1); + + return ( +
+
+
+ Trend over time +
+

+ 8-week view of the top 3 return reasons +

+
+ + {data.series.map((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 +
+

+ {`Where "Wrong size/fit" returns are concentrated`} +

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

{categoryInsight}

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

+ Ranked by return rate with revenue impact +

+
+
+
+ Product + Return % + Issue + Impact +
+ {topProducts.map((p) => ( +
+ {p.name} + + {p.returnRate}% + + + {p.issue} + + + {p.impact} + +
+ ))} +
+
+ ); +} + +function Recommendations() { + return ( +
+
+
+ Recommended actions +
+

+ AI-assisted next steps based on current data +

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

{rec.action}

+

+ {rec.impact} +

+
+ ))} +
+
+ ); +} + +// --- Exports --- + +export function DrilldownTabContent({ tab }: { tab: DrilldownTab }) { + if (tab === "trend") return ; + if (tab === "categories") return ; + if (tab === "products") return ; + if (tab === "actions") return ; + // "overview" is handled by the original card content + return null; +} + +export function AutopilotPrompts({ + onPromptSelect, +}: { + onPromptSelect?: (prompt: string) => void; +}) { + const [pressedPrompt, setPressedPrompt] = useState(null); + + return ( +
+
+ Autopilot + Autopilot + Ask Autopilot +
+
+ {suggestedPrompts.map((prompt) => ( + + ))} +
+
+ ); +} diff --git a/apps/apollo-vertex/templates/dashboard/glow-config.ts b/apps/apollo-vertex/templates/dashboard/glow-config.ts new file mode 100644 index 000000000..553f05d45 --- /dev/null +++ b/apps/apollo-vertex/templates/dashboard/glow-config.ts @@ -0,0 +1,294 @@ +export interface GlowConfig { + start: string; + end: string; + containerOpacity: number; + fillOpacity: number; + startStopOpacity: number; + endStopOpacity: number; + endOffset: number; +} + +export interface CardGradient { + enabled: boolean; + start: string; + end: string; + angle: number; + opacity: number; +} + +export interface CardConfig { + overviewBg: string; + overviewOpacity: number; + overviewGradient: CardGradient; + insightBg: string; + insightOpacity: number; + insightGradient: CardGradient; + promptBg: string; + promptOpacity: number; + promptGradient: CardGradient; + borderVisible: boolean; + backdropBlur: boolean; +} + +export const defaultLightGlow: GlowConfig = { + start: "var(--insight-500)", + end: "var(--primary-400)", + containerOpacity: 70, + fillOpacity: 0.3, + startStopOpacity: 1, + endStopOpacity: 1, + endOffset: 0.35, +}; + +export const defaultDarkGlow: GlowConfig = { + start: "var(--insight-700)", + end: "var(--primary-600)", + containerOpacity: 45, + fillOpacity: 1, + startStopOpacity: 1, + endStopOpacity: 0.4, + endOffset: 0.5, +}; + +export type CardSize = "sm" | "md" | "lg"; + +export type InsightCardType = "kpi" | "chart"; +export type ChartType = + | "donut" + | "horizontal-bars" + | "sparkline" + | "area" + | "stacked-bar"; + +export interface InsightCardContent { + type: InsightCardType; + chartType: ChartType; + title: string; +} + +export type CardInteraction = "static" | "expand" | "navigate"; + +export interface InsightCardConfig { + size: CardSize; + visible: boolean; + content: InsightCardContent; + interaction: CardInteraction; + navigateTo?: string; +} + +export interface LayoutConfig { + gap: number; + overviewRatio: number; + promptRatio: number; + insightCards: [ + InsightCardConfig, + InsightCardConfig, + InsightCardConfig, + InsightCardConfig, + ]; + padding: number; + containerBg: string; +} + +export const defaultLayout: LayoutConfig = { + gap: 4, + overviewRatio: 4, + promptRatio: 1, + insightCards: [ + { + size: "sm", + visible: true, + interaction: "static", + content: { + type: "kpi", + chartType: "donut", + title: "Upfront decision efficiency", + }, + }, + { + size: "md", + visible: true, + interaction: "expand", + content: { + type: "chart", + chartType: "horizontal-bars", + title: "Top issues", + }, + }, + { + size: "md", + visible: true, + interaction: "expand", + content: { + type: "chart", + chartType: "stacked-bar", + title: "Pipeline", + }, + }, + { + size: "sm", + visible: true, + interaction: "static", + content: { + type: "kpi", + chartType: "donut", + title: "SLA compliance", + }, + }, + ], + padding: 24, + containerBg: "none", +}; + +const defaultGradient: CardGradient = { + enabled: false, + start: "var(--insight-500)", + end: "var(--primary-400)", + angle: 135, + opacity: 100, +}; + +export const insightOptions = [ + { label: "300", value: "var(--insight-300)" }, + { label: "400", value: "var(--insight-400)" }, + { label: "500", value: "var(--insight-500)" }, + { label: "600", value: "var(--insight-600)" }, + { label: "700", value: "var(--insight-700)" }, + { label: "800", value: "var(--insight-800)" }, + { label: "900", value: "var(--insight-900)" }, +]; + +export const primaryOptions = [ + { label: "300", value: "var(--primary-300)" }, + { label: "400", value: "var(--primary-400)" }, + { label: "500", value: "var(--primary-500)" }, + { label: "600", value: "var(--primary-600)" }, + { label: "700", value: "var(--primary-700)" }, + { label: "800", value: "var(--primary-800)" }, + { label: "900", value: "var(--primary-900)" }, +]; + +export const cardTypeOptions = [ + { label: "KPI", value: "kpi" }, + { label: "Chart", value: "chart" }, +]; + +export const interactionOptions = [ + { label: "Static", value: "static" }, + { label: "Expand", value: "expand" }, + { label: "Navigate", value: "navigate" }, +]; + +export const chartTypeOptions = [ + { label: "Donut", value: "donut" }, + { label: "Horizontal Bars", value: "horizontal-bars" }, + { label: "Sparkline", value: "sparkline" }, + { label: "Area", value: "area" }, + { label: "Stacked Bar", value: "stacked-bar" }, +]; + +export const sizeOptions = [ + { label: "Small (1 col)", value: "sm" }, + { label: "Medium (1 col)", value: "md" }, + { label: "Large (full)", value: "lg" }, +]; + +export const containerBgOptions = [ + { label: "None", value: "none" }, + { label: "white", value: "white" }, + { label: "sidebar", value: "sidebar" }, + { label: "card", value: "card" }, + { label: "background", value: "background" }, + { label: "muted", value: "muted" }, +]; + +export const bgColorOptions = [ + { label: "white", value: "white" }, + { label: "sidebar", value: "sidebar" }, + { label: "card", value: "card" }, + { label: "background", value: "background" }, + { label: "muted", value: "muted" }, +]; + +export function cardBgStyle( + bg: string, + opacity: number, + gradient: CardGradient, +): React.CSSProperties { + if (gradient.enabled) { + const alpha = gradient.opacity / 100; + const style = { + "--card-bg-override": `linear-gradient(${gradient.angle}deg, color-mix(in srgb, ${gradient.start} ${alpha * 100}%, transparent), color-mix(in srgb, ${gradient.end} ${alpha * 100}%, transparent))`, + borderColor: "transparent", + }; + // oxlint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- CSS custom properties require assertion + return style as unknown as React.CSSProperties; + } + const value = + bg === "white" + ? `rgba(255,255,255,${opacity / 100})` + : `color-mix(in srgb, var(--${bg}) ${opacity}%, transparent)`; + // oxlint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- CSS custom properties require assertion + return { "--card-bg-override": value } as unknown as React.CSSProperties; +} + +export function getInsightCardClasses( + content: InsightCardContent, + viewMode: "desktop" | "compact" | "stacked" = "desktop", +): { + cardClassName: string; + contentClassName: string; +} { + if (content.type === "kpi") { + const isCompact = viewMode === "compact"; + return { + cardClassName: isCompact ? "!gap-0" : "!gap-4", + contentClassName: isCompact + ? "flex-1 flex flex-col overflow-hidden" + : "flex-1 flex flex-col", + }; + } + const isBarChart = content.chartType === "horizontal-bars"; + return { + cardClassName: content.chartType === "donut" ? "!gap-0" : "", + contentClassName: isBarChart ? "flex-1" : "flex-1 flex flex-col", + }; +} + +export const defaultDarkCards: CardConfig = { + overviewBg: "sidebar", + overviewOpacity: 69, + overviewGradient: { ...defaultGradient, opacity: 30 }, + insightBg: "sidebar", + insightOpacity: 60, + insightGradient: { ...defaultGradient }, + promptBg: "sidebar", + promptOpacity: 80, + promptGradient: { ...defaultGradient }, + borderVisible: false, + backdropBlur: true, +}; + +const CARD_SIZES = new Set(["sm", "md", "lg"]); +const INSIGHT_CARD_TYPES = new Set(["kpi", "chart"]); +const CHART_TYPES = new Set([ + "donut", + "horizontal-bars", + "sparkline", + "area", + "stacked-bar", +]); +const CARD_INTERACTIONS = new Set(["static", "expand", "navigate"]); + +export function isCardSize(v: string): v is CardSize { + return CARD_SIZES.has(v); +} +export function isInsightCardType(v: string): v is InsightCardType { + return INSIGHT_CARD_TYPES.has(v); +} +export function isChartType(v: string): v is ChartType { + return CHART_TYPES.has(v); +} +export function isCardInteraction(v: string): v is CardInteraction { + return CARD_INTERACTIONS.has(v); +} diff --git a/apps/apollo-vertex/templates/dashboard/insight-card-renderers.tsx b/apps/apollo-vertex/templates/dashboard/insight-card-renderers.tsx new file mode 100644 index 000000000..3d8298085 --- /dev/null +++ b/apps/apollo-vertex/templates/dashboard/insight-card-renderers.tsx @@ -0,0 +1,349 @@ +"use client"; + +import { useRef, useState, useEffect } from "react"; +import { Badge } from "@/components/ui/badge"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/registry/tooltip/tooltip"; +import type { InsightCardContent } from "./glow-config"; +import { useDashboardData } from "./dashboard-data-context"; +import type { InsightCardData } from "./dashboard-data"; +import { DonutContent, SparklineContent, AreaContent } from "./chart-stubs"; + +type ViewMode = "desktop" | "compact" | "stacked"; + +// --- Truncated text with conditional tooltip --- + +function TruncatedText({ + children, + className, +}: { + children: string | undefined; + className?: string; +}) { + const textRef = useRef(null); + const [isTruncated, setIsTruncated] = useState(false); + + useEffect(() => { + const el = textRef.current; + if (!el) return; + const check = () => setIsTruncated(el.scrollHeight > el.clientHeight); + check(); + const observer = new ResizeObserver(check); + observer.observe(el); + return () => observer.disconnect(); + }, [children]); + + const textEl = ( +

+ {children} +

+ ); + + if (!isTruncated) return textEl; + + return ( + + {textEl} + + {children} + + + ); +} + +function KpiContent({ + cardData, + viewMode, +}: { + cardData: InsightCardData; + viewMode: ViewMode; +}) { + if (viewMode === "compact") { + return ( + <> +
+
+ {cardData.kpiNumber} +
+ + {cardData.kpiBadge} + +
+ + {cardData.kpiDescription} + + + ); + } + + return ( + <> +
+ {cardData.kpiNumber} +
+
+ + {cardData.kpiBadge} + +

+ {cardData.kpiDescription} +

+
+ + ); +} + +function HorizontalBarsContent({ + cardData, + viewMode, + isExpanded = false, +}: { + cardData: InsightCardData; + viewMode: ViewMode; + isExpanded?: boolean; +}) { + const bars = cardData.bars ?? []; + const chartColors = [ + "bg-chart-1", + "bg-chart-2", + "bg-chart-3", + "bg-chart-4", + "bg-chart-5", + ]; + const barsWithColor = bars.map((b, i) => ({ + ...b, + color: chartColors[i % chartColors.length], + })); + + if (viewMode === "compact" && !isExpanded) { + const total = barsWithColor.reduce((sum, s) => sum + s.value, 0); + return ( +
+
+ {barsWithColor.map((issue) => ( +
+
+
+ ))} +
+
+ {barsWithColor.map((issue) => { + const pct = Math.round((issue.value / total) * 100); + return ( +
+
+ + {issue.label} {pct}% + +
+ ); + })} +
+
+ ); + } + + return ( +
+ {barsWithColor.map((issue) => ( +
+
+ {issue.label} + {issue.value}% +
+
+
+
+
+
+
+ ))} +
+ ); +} + +function StackedBarContent({ + cardData, + viewMode, + isExpanded = false, +}: { + cardData: InsightCardData; + viewMode: ViewMode; + isExpanded?: boolean; +}) { + const chartColors = [ + "bg-chart-1", + "bg-chart-2", + "bg-chart-3", + "bg-chart-4", + "bg-chart-5", + ]; + const rawBars = cardData.stackedBars ?? []; + const legend = (cardData.stackedLegend ?? []).map((label, i) => ({ + label, + color: chartColors[i % chartColors.length], + })); + const barData = rawBars.map((bar) => ({ + label: bar.label, + segments: bar.segments.map((value, i) => ({ + value, + color: chartColors[i % chartColors.length], + })), + })); + const maxTotal = Math.max( + ...barData.map((d) => d.segments.reduce((sum, s) => sum + s.value, 0)), + ); + + if (viewMode === "compact" && !isExpanded) { + // Summary: aggregate all days into one horizontal stacked bar + const totals = barData.reduce( + (acc, day) => { + for (const seg of day.segments) { + const key = seg.color; + acc[key] = (acc[key] ?? 0) + seg.value; + } + return acc; + }, + {} as Record, + ); + const grandTotal = Object.values(totals).reduce((a, b) => a + b, 0); + + return ( +
+
+ {legend.map((item) => ( +
+
+
+ ))} +
+
+ {legend.map((item) => { + const val = totals[item.color] ?? 0; + const pct = Math.round((val / grandTotal) * 100); + return ( +
+
+ + {item.label} {pct}% + +
+ ); + })} +
+
+ ); + } + + return ( +
+
+ {barData.map((bar) => { + const total = bar.segments.reduce((sum, s) => sum + s.value, 0); + const pct = (total / maxTotal) * 100; + return ( +
+
+
+ {bar.segments.map((seg) => ( +
+ ))} +
+
+ {bar.segments.map((seg) => ( +
+ ))} +
+
+ + {bar.label} + +
+ ); + })} +
+
+ {legend.map((item) => ( +
+
+ + {item.label} + +
+ ))} +
+
+ ); +} + +export function InsightCardBody({ + content, + cardIndex, + viewMode = "desktop", + isExpanded = false, +}: { + content: InsightCardContent; + cardIndex: number; + viewMode?: ViewMode; + isExpanded?: boolean; +}) { + const { data } = useDashboardData(); + const cardData = data.insightCards[cardIndex] ?? data.insightCards[0]; + + if (content.type === "kpi") { + return ; + } + if (content.chartType === "horizontal-bars") + return ( + + ); + if (content.chartType === "donut") return ; + if (content.chartType === "sparkline") return ; + if (content.chartType === "stacked-bar") + return ( + + ); + return ; +}