name add-dashboard updated "2026-03-03T00:00:00.000Z" description Create a new customizable dashboard with its own chart registry, provider, and page. Use when adding dashboards like DRep or SPO dashboard. argument-hint ["DashboardName"] allowed-tools Read, Edit, Write, Glob, Grep, Bash
Add New Dashboard
Create a complete new dashboard instance with its own charts, state management, and page.
Arguments
$0 - Dashboard name in PascalCase (e.g., DRep, SPO, Voter)
Architecture Overview
Each dashboard is a self-contained module with these features:
Charts : Lazy-loaded chart components in a registry
Side Panel : Tabbed panel for Charts, Elements, Layout, Share
Text Elements : User-addable text labels
Page Margins : Draggable margin handles for width control
Multi-Select : Box selection and Ctrl+click for multiple elements
localStorage : All settings persist per-dashboard
src/
├── components/dashboards/
│ ├── shared/ # Reused across all dashboards
│ │ ├── DashboardProvider.tsx # State + localStorage (COPY & MODIFY)
│ │ ├── DashboardGrid.tsx # Canvas with selection (COPY & MODIFY)
│ │ ├── DashboardChartCard.tsx # Chart wrapper (REUSE AS-IS)
│ │ ├── DashboardTextElement.tsx # Text labels (REUSE AS-IS)
│ │ ├── DashboardSidePanel.tsx # Customization panel (COPY & MODIFY)
│ │ ├── DashboardMarginHandles.tsx # Margin controls (REUSE AS-IS)
│ │ └── chartTheme.ts # Theme colors (REUSE AS-IS)
│ │
│ ├── governance/ # Reference implementation
│ │ └── charts/
│ │ ├── index.tsx # CHART_REGISTRY
│ │ └── *.tsx # Chart components
│ │
│ ├── development_activity/ # Dev activity dashboard (real example)
│ │ └── charts/
│ │ ├── index.ts # CHART_REGISTRY
│ │ └── *.tsx # 13+ chart components
│ │
│ └── ${lowercase}/ # YOUR NEW DASHBOARD
│ ├── charts/
│ │ ├── index.tsx # CHART_REGISTRY for this dashboard
│ │ └── *.tsx # Chart components
│ ├── ${$0}DashboardProvider.tsx
│ ├── ${$0}DashboardGrid.tsx
│ ├── ${$0}DashboardSidePanel.tsx
│ └── index.ts # Barrel export
│
├── pages/
│ ├── adadev.tsx # Governance dashboard page
│ └── ${lowercase}-dashboard.tsx # YOUR NEW DASHBOARD PAGE
│
└── types/
└── ${lowercase}-dashboard.ts # Dashboard-specific types
Step 1: Create Dashboard Directory Structure
mkdir -p src/components/dashboards/${lowercase} /charts
Step 2: Create Chart Registry
Create src/components/dashboards/${lowercase}/charts/index.tsx:
import dynamic from "next/dynamic" ;
import { BarChart3 } from "lucide-react" ;
import { ChartSkeleton } from "@/components/dashboards/shared/ChartSkeleton" ;
import type { ChartDefinition } from "@/types/dashboard" ;
import { DEFAULT_ ${UPPERCASE }_CHART_LAYOUTS } from "@/types/${lowercase}-dashboard" ;
const Example ${$0}Chart = dynamic (
() => import ("./Example${$0}Chart" ).then ((mod ) => mod.Example$ {$0}Chart ),
{ loading : () => <ChartSkeleton /> , ssr : false }
);
export const ${UPPERCASE }_CHART_REGISTRY : ChartDefinition [] = [
{
id : "example-chart" ,
title : "Example Chart" ,
description : "Description of what this chart shows" ,
component : Example ${$0}Chart ,
defaultVisible : true ,
defaultLayout : DEFAULT_ ${UPPERCASE }_CHART_LAYOUTS["example-chart" ],
icon : BarChart3 ,
},
];
export function get${$0}ChartById (id : string ): ChartDefinition | undefined {
return ${UPPERCASE }_CHART_REGISTRY.find ((chart ) => chart.id === id);
}
export { ChartSkeleton } from "@/components/dashboards/shared/ChartSkeleton" ;
export { Example ${$0}Chart };
Step 3: Create Dashboard Types
Create src/types/${lowercase}-dashboard.ts:
import type { ChartLayout , TextElement , PageMargins } from "./dashboard" ;
import { PAGE_MARGIN_CONSTRAINTS , DEFAULT_PAGE_MARGINS } from "./dashboard" ;
export type ${$0}ChartId = "example-chart" ;
export const ALL_ ${UPPERCASE }_CHART_IDS : ${$0}ChartId [] = ["example-chart" ];
export const DEFAULT_ ${UPPERCASE }_CHART_LAYOUTS : Record <${$0}ChartId , ChartLayout > = {
"example-chart" : { x : 0 , y : 0 , width : 380 , height : 320 },
};
export interface ${$0}DashboardConfig {
visibleCharts : ${$0}ChartId [];
chartOrder : ${$0}ChartId [];
layouts : Record <${$0}ChartId , ChartLayout >;
textElements : TextElement [];
pageMargins : PageMargins ;
version : number ;
}
export const DEFAULT_ ${UPPERCASE }_DASHBOARD_CONFIG : ${$0}DashboardConfig = {
visibleCharts : ALL_ ${UPPERCASE }_CHART_IDS,
chartOrder : ALL_ ${UPPERCASE }_CHART_IDS,
layouts : DEFAULT_ ${UPPERCASE }_CHART_LAYOUTS,
textElements : [],
pageMargins : DEFAULT_PAGE_MARGINS ,
version : 1 ,
};
export { PAGE_MARGIN_CONSTRAINTS };
Step 4: Create Dashboard Provider
Create src/components/dashboards/${lowercase}/${$0}DashboardProvider.tsx:
Copy from shared/DashboardProvider.tsx and modify:
Change STORAGE_KEY to "${lowercase}-dashboard-config"
Update imports to use ${$0}ChartId, ${$0}DashboardConfig, etc.
Update default config imports
Rename context and hook
Key changes:
const STORAGE_KEY = "${lowercase}-dashboard-config" ;
import type {
${$0}ChartId ,
${$0}DashboardConfig ,
} from "@/types/${lowercase}-dashboard" ;
import {
DEFAULT_ ${UPPERCASE }_DASHBOARD_CONFIG,
DEFAULT_ ${UPPERCASE }_CHART_LAYOUTS,
ALL_ ${UPPERCASE }_CHART_IDS,
PAGE_MARGIN_CONSTRAINTS ,
} from "@/types/${lowercase}-dashboard" ;
interface ${$0}DashboardContextValue {
config : ${$0}DashboardConfig ;
mounted : boolean ;
isChartVisible : (chartId : ${$0}ChartId ) => boolean ;
toggleChartVisibility : (chartId : ${$0}ChartId ) => void ;
setVisibleCharts : (chartIds : ${$0}ChartId [] ) => void ;
getLayout : (chartId : ${$0}ChartId ) => ChartLayout ;
updateLayout : (chartId : ${$0}ChartId , layout : Partial <ChartLayout > ) => void ;
reorderCharts : (fromIndex : number , toIndex : number ) => void ;
resetToDefaults : () => void ;
addTextElement : () => void ;
updateTextElement : (id : string , updates : Partial <TextElement > ) => void ;
removeTextElement : (id : string ) => void ;
updatePageMargins : (margins : Partial <PageMargins > ) => void ;
exportConfig : () => string ;
importConfig : (code : string ) => { success : boolean ; error ?: string };
}
export function ${$0}DashboardProvider ({ children }) { ... }
export function use${$0}Dashboard () { ... }
Step 5: Create Dashboard Grid
Create src/components/dashboards/${lowercase}/${$0}DashboardGrid.tsx:
Copy from shared/DashboardGrid.tsx and modify:
Import from your chart registry: ${UPPERCASE}_CHART_REGISTRY, get${$0}ChartById
Use your dashboard hook: use${$0}Dashboard
Update type references to ${$0}ChartId
Key Features to Preserve
Data Attributes (required for selection exclusion):
<DashboardChartCard data-chart-card ... />
<DashboardTextElement data-text-element ... />
Document-Level Selection Handler :
useEffect (() => {
const handleMouseDown = (e : MouseEvent ) => {
const target = e.target as HTMLElement ;
if (target.closest ("[data-chart-card], [data-text-element], [data-margin-handle], button, [role='button'], input, textarea, a" )) {
return ;
}
};
document .addEventListener ("mousedown" , handleMouseDown);
return () => document .removeEventListener ("mousedown" , handleMouseDown);
}, []);
Card Position Constraints (when margins change):
useEffect (() => {
if (!mounted || containerWidth < 400 ) return ;
for (const chartId of config.visibleCharts ) {
const layout = getLayout (chartId);
const maxX = Math .max (0 , containerWidth - layout.width );
if (layout.x > maxX) {
updateLayout (chartId, { x : snapToGrid (maxX) });
}
}
}, [containerWidth, mounted, ...]);
Step 6: Create Side Panel
Create src/components/dashboards/${lowercase}/${$0}DashboardSidePanel.tsx:
Copy from shared/DashboardSidePanel.tsx and modify:
Import your chart registry: get${$0}ChartById
Use your dashboard hook: use${$0}Dashboard
Side Panel Tabs
The panel has 4 tabs:
Tab Purpose Charts Toggle visibility, reorder via drag Elements Add/manage text labels Layout Page margin sliders Share Export/import config as base64
Step 7: Create Dashboard Page
Create src/pages/${lowercase}-dashboard.tsx:
import { useRef } from "react" ;
import Head from "next/head" ;
import { Card } from "@/components/ui/card" ;
import { GameLoader } from "@/components/ui/game-loader" ;
import { useTheme } from "@/lib/theme" ;
import {
${$0}DashboardProvider ,
${$0}DashboardGrid ,
${$0}DashboardSidePanel ,
use${$0}Dashboard ,
} from "@/components/dashboards/${lowercase}" ;
import { DashboardMarginHandles } from "@/components/dashboards/shared" ;
function ${$0}DashboardContent () {
const { activeTheme } = useTheme ();
const isGame = activeTheme.id === "game" ;
const { config, mounted } = use${$0}Dashboard ();
const gridAreaRef = useRef<HTMLDivElement >(null );
const isLoading = false ;
const error = null ;
const hasData = true ;
const minPadding = 24 ;
const leftPadding = Math .max (minPadding, config.pageMargins .left );
const rightPadding = Math .max (minPadding, config.pageMargins .right );
return (
<>
<Head >
<title > ${$0} Dashboard - CGOV</title >
<meta name ="description" content ="${$0} Dashboard" />
</Head >
<div className ="min-h-screen bg-background" >
{/* Margin handles - only show when mounted */}
{mounted && <DashboardMarginHandles containerRef ={gridAreaRef} /> }
{/* Full-width container with dynamic padding */}
<div
className ="py-4 sm:py-6 md:py-8 transition-[padding] duration-75"
style ={{
paddingLeft: leftPadding ,
paddingRight: rightPadding ,
}}
>
{/* Header */}
<div className ="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 mb-4 sm:mb-6 md:mb-8" >
<div className ="text-left" >
<h1 className ="landing-title text-2xl sm:text-3xl md:text-4xl lg:text-5xl font-bold mb-2 sm:mb-3 md:mb-4 text-black dark:text-foreground" >
${$0} Dashboard
</h1 >
<p className ="landing-subtitle text-muted-foreground text-sm sm:text-base md:text-lg" >
Your customizable ${$0} overview
</p >
</div >
<${$0}DashboardSidePanel />
</div >
{/* Loading state */}
{isLoading && !hasData && (
isGame ? (
<div className ="flex items-center justify-center py-24" >
<GameLoader />
</div >
) : (
<Card className ="p-12 mb-6" >
<div className ="flex flex-col items-center justify-center" >
<div className ="animate-spin rounded-full h-12 w-12 border-b-2 border-primary mb-4" />
<p className ="text-muted-foreground" > Loading...</p >
</div >
</Card >
)
)}
{/* Error state - inline warning, doesn't block content */}
{error && (
<Card className ="p-3 sm:p-4 mb-4 sm:mb-6 border-destructive/50 bg-destructive/5" >
<div className ="flex items-center justify-between gap-3" >
<p className ="text-destructive text-sm" > {error}</p >
</div >
</Card >
)}
{/* Dashboard Grid - show even if some data fails */}
{(hasData || (!isLoading && error)) && (
<div ref ={gridAreaRef} >
<${$0}DashboardGrid isLoading={isLoading} />
</div >
)}
</div >
</div >
</>
);
}
export default function ${$0}Dashboard () {
return (
<${$0}DashboardProvider >
<${$0}DashboardContent />
</${$0}DashboardProvider >
);
}
Step 8: Create First Chart
Create src/components/dashboards/${lowercase}/charts/Example${$0}Chart.tsx:
Use the /add-chart skill patterns but place in your dashboard's charts folder.
Step 9: Create Barrel Export
Create src/components/dashboards/${lowercase}/index.ts:
export { ${$0}DashboardProvider , use${$0}Dashboard } from "./${$0}DashboardProvider" ;
export { ${$0}DashboardGrid } from "./${$0}DashboardGrid" ;
export { ${$0}DashboardSidePanel } from "./${$0}DashboardSidePanel" ;
export { ${UPPERCASE }_CHART_REGISTRY, get${$0}ChartById } from "./charts" ;
Step 10: Add Navigation Link (Optional)
Update header navigation to include link to new dashboard.
Data Attributes Reference
These data attributes are used for selection box exclusion:
Attribute Element Purpose data-chart-cardChart wrapper Exclude from selection start data-text-elementText label Exclude from selection start data-margin-handleMargin lines Exclude from selection start
When dragging starts on elements with these attributes, the selection box won't activate.
Files Created Summary
File Purpose components/dashboards/${lowercase}/charts/index.tsxChart registry components/dashboards/${lowercase}/charts/Example${$0}Chart.tsxFirst chart components/dashboards/${lowercase}/${$0}DashboardProvider.tsxState management components/dashboards/${lowercase}/${$0}DashboardGrid.tsxGrid canvas with selection components/dashboards/${lowercase}/${$0}DashboardSidePanel.tsxCustomization panel components/dashboards/${lowercase}/index.tsBarrel export types/${lowercase}-dashboard.tsDashboard-specific types pages/${lowercase}-dashboard.tsxDashboard page
Gotchas: Graceful Degradation
Never gate all page content behind a single data endpoint's success. When a page uses multiple independent API endpoints, each section should handle errors independently:
Stats/summary cards: Show "--" when their endpoint fails
Lists/tables from separate endpoints: Render even if stats fail
Charts: Show empty state, not a full-page error
Use inline warnings instead of full-page blocking error cards
Anti-pattern:
{error && <FullPageError /> }
{data && !error && <AllContent /> }
Correct pattern:
{!isLoading && (
<>
{error && <InlineWarning /> }
{data && <DataDependentSection /> }
<IndependentSection /> {/* always renders */}
</>
)}
Verification Checklist
Dashboard page renders at /${lowercase}-dashboard
Charts display with correct theming (all 3 themes)
Drag and resize work correctly
Layout persists to localStorage (separate from governance)
Side panel opens with all 4 tabs working
Text elements can be added and edited
Page margins can be adjusted via handles and sliders
Box selection works for multi-select
Export/import config works
Page degrades gracefully when backend endpoints fail (no full-page errors)
Run npm run build - no TypeScript errors