diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..b6ae380
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,38 @@
+name: CI
+
+on:
+ push:
+ pull_request:
+ branches: [main]
+
+permissions:
+ contents: read
+
+concurrency:
+ group: ci-${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ validate:
+ runs-on: ubuntu-latest
+ timeout-minutes: 15
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+
+ - name: Set up Bun
+ uses: oven-sh/setup-bun@v2
+ with:
+ bun-version: 1.2.23
+
+ - name: Install dependencies
+ run: bun install --frozen-lockfile
+
+ - name: Lint
+ run: bun run lint
+
+ - name: Test
+ run: bun run test
+
+ - name: Build
+ run: bun run build
diff --git a/Codebase.zip b/Codebase.zip
deleted file mode 100644
index 5a54214..0000000
Binary files a/Codebase.zip and /dev/null differ
diff --git a/README.md b/README.md
index ef7fd4d..95677bf 100644
--- a/README.md
+++ b/README.md
@@ -19,36 +19,29 @@ Neighbour repo: [SEBI-Compliance-Research](https://github.com/d33pm3/SEBI-Compli
**This is not** [SEBI-Compliance-Research](https://github.com/d33pm3/SEBI-Compliance-Research) (that repo researches live obligations into a 12-column workbook).
**This is not** a full-stack multi-agent platform or a live LLM pipeline.
**This is not** a filing, calendar submission, or compliance opinion.
-**This is not** a complete `src/` tree on `main` — the runnable source is in `Codebase.zip`.
+**This is** a self-contained repository with runnable source tracked under `src/` and `public/`.
-## Where the source is
+## Synthetic demo data
-The **complete application source** is in [`Codebase.zip`](Codebase.zip), under:
-
-- `1_SEBI Full Stack App/src/`
-- `1_SEBI Full Stack App/public/`
-
-There is no runnable `src/` on `main`. Extract the zip before `npm run dev`.
+All company, user, filing, notice, document, system-health, and operational records in this repository are fictional synthetic demo fixtures. They must not be treated as real customer, company, regulatory, or operational information.
## Run the eval build
-Requires Node.js 18+ and npm.
+Requires Bun 1.2.23.
```bash
git clone https://github.com/d33pm3/Multi-agent-Full-Stack-App.git
cd Multi-agent-Full-Stack-App
-unzip -o Codebase.zip
-cp -a "1_SEBI Full Stack App/src/." src/
-cp -a "1_SEBI Full Stack App/public/." public/
-npm i
-npm run dev
+bun install --frozen-lockfile
+bun run dev
```
-After extract, `src/main.tsx` must exist. If it does not, the zip did not unpack.
+Validate the same baseline used by CI:
```bash
-npm test
-npm run build
+bun run lint
+bun run test
+bun run build
```
## What is not deployed
diff --git a/eslint.config.js b/eslint.config.js
index 40f72cc..9ccca75 100644
--- a/eslint.config.js
+++ b/eslint.config.js
@@ -23,4 +23,19 @@ export default tseslint.config(
"@typescript-eslint/no-unused-vars": "off",
},
},
+ {
+ files: ["src/components/ui/{command,textarea}.tsx"],
+ rules: {
+ "@typescript-eslint/no-empty-object-type": "off",
+ },
+ },
+ {
+ files: [
+ "src/lib/categoryExportUtils.ts",
+ "src/pages/{Index,KPIs,RiskAssessment}.tsx",
+ ],
+ rules: {
+ "@typescript-eslint/no-explicit-any": "off",
+ },
+ },
);
diff --git a/public/placeholder.svg b/public/placeholder.svg
new file mode 100644
index 0000000..ea950de
--- /dev/null
+++ b/public/placeholder.svg
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/public/robots.txt b/public/robots.txt
new file mode 100644
index 0000000..6018e70
--- /dev/null
+++ b/public/robots.txt
@@ -0,0 +1,14 @@
+User-agent: Googlebot
+Allow: /
+
+User-agent: Bingbot
+Allow: /
+
+User-agent: Twitterbot
+Allow: /
+
+User-agent: facebookexternalhit
+Allow: /
+
+User-agent: *
+Allow: /
diff --git a/src/App.css b/src/App.css
new file mode 100644
index 0000000..b9d355d
--- /dev/null
+++ b/src/App.css
@@ -0,0 +1,42 @@
+#root {
+ max-width: 1280px;
+ margin: 0 auto;
+ padding: 2rem;
+ text-align: center;
+}
+
+.logo {
+ height: 6em;
+ padding: 1.5em;
+ will-change: filter;
+ transition: filter 300ms;
+}
+.logo:hover {
+ filter: drop-shadow(0 0 2em #646cffaa);
+}
+.logo.react:hover {
+ filter: drop-shadow(0 0 2em #61dafbaa);
+}
+
+@keyframes logo-spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ a:nth-of-type(2) .logo {
+ animation: logo-spin infinite 20s linear;
+ }
+}
+
+.card {
+ padding: 2em;
+}
+
+.read-the-docs {
+ color: #888;
+}
diff --git a/src/App.tsx b/src/App.tsx
new file mode 100644
index 0000000..7c8ad8b
--- /dev/null
+++ b/src/App.tsx
@@ -0,0 +1,61 @@
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+import { BrowserRouter, Route, Routes } from "react-router-dom";
+import { Toaster as Sonner } from "@/components/ui/sonner";
+import { Toaster } from "@/components/ui/toaster";
+import { TooltipProvider } from "@/components/ui/tooltip";
+import NoticeDetail from "./pages/NoticeDetail";
+import Index from "./pages/Index.tsx";
+import RegisterAgent from "./pages/RegisterAgent.tsx";
+import RiskAssessment from "./pages/RiskAssessment.tsx";
+import DocumentVault from "./pages/DocumentVault.tsx";
+import ComplianceChatbot from "./pages/ComplianceChatbot.tsx";
+import ComplianceAssistant from "./pages/ComplianceAssistant.tsx";
+import AdminModule from "./pages/AdminModule.tsx";
+import ResponseTracker from "./pages/ResponseTracker.tsx";
+import TaskManager from "./pages/TaskManager.tsx";
+import ComplianceTimeline from "./pages/ComplianceTimeline.tsx";
+import KPIs from "./pages/KPIs.tsx";
+import AgentOutputDetail from "./pages/AgentOutputDetail.tsx";
+import ComplianceCalendar from "./pages/ComplianceCalendar.tsx";
+import RiskActionPlan from "./pages/RiskActionPlan.tsx";
+import RegisterManager from "./pages/RegisterManager.tsx";
+
+
+import ComplianceItemDetail from "./pages/ComplianceItemDetail.tsx";
+import NotFound from "./pages/NotFound.tsx";
+
+const queryClient = new QueryClient();
+
+const App = () => (
+
+
+
+
+
+
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+
+ } />
+ } />
+ } />
+
+
+
+
+);
+
+export default App;
diff --git a/src/assets/ecxo-logo.png b/src/assets/ecxo-logo.png
new file mode 100644
index 0000000..769dd19
Binary files /dev/null and b/src/assets/ecxo-logo.png differ
diff --git a/src/components/AppHeader.tsx b/src/components/AppHeader.tsx
new file mode 100644
index 0000000..5ebc6d5
--- /dev/null
+++ b/src/components/AppHeader.tsx
@@ -0,0 +1,50 @@
+import { Bell, User } from 'lucide-react';
+import ecxoLogo from '@/assets/ecxo-logo.png';
+import { SidebarTrigger } from '@/components/ui/sidebar';
+import { Button } from '@/components/ui/button';
+import { Badge } from '@/components/ui/badge';
+import { useComplianceStore } from '@/store/complianceStore';
+
+interface AppHeaderProps {
+ title: string;
+ subtitle?: string;
+}
+
+export function AppHeader({ title, subtitle }: AppHeaderProps) {
+ const items = useComplianceStore(s => s.items);
+ const overdueCount = items.filter(i => i.status === 'Overdue').length;
+ const dueSoonCount = items.filter(i => i.status === 'Due Soon').length;
+ const alertCount = overdueCount + dueSoonCount;
+
+ return (
+
+
+
+
+
{title}
+ {subtitle &&
{subtitle}
}
+
+
+
+
+
+
+
deriskadvisory
+
+
+
+
+ {alertCount > 0 && (
+
+ {alertCount}
+
+ )}
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/AppLayout.tsx b/src/components/AppLayout.tsx
new file mode 100644
index 0000000..59829c1
--- /dev/null
+++ b/src/components/AppLayout.tsx
@@ -0,0 +1,25 @@
+import { SidebarProvider } from '@/components/ui/sidebar';
+import { AppSidebar } from '@/components/AppSidebar';
+import { AppHeader } from '@/components/AppHeader';
+
+interface AppLayoutProps {
+ children: React.ReactNode;
+ title: string;
+ subtitle?: string;
+}
+
+export function AppLayout({ children, title, subtitle }: AppLayoutProps) {
+ return (
+
+
+
+ );
+}
diff --git a/src/components/AppSidebar.tsx b/src/components/AppSidebar.tsx
new file mode 100644
index 0000000..feb6c90
--- /dev/null
+++ b/src/components/AppSidebar.tsx
@@ -0,0 +1,113 @@
+import {
+ LayoutDashboard,
+ Bot,
+ ShieldAlert,
+ FolderArchive,
+ MessageSquare,
+ MailWarning,
+ ListTodo,
+ CalendarClock,
+ BarChart3,
+ Settings,
+ Shield,
+ Target,
+ CalendarDays,
+ ClipboardList,
+ FilePlus2,
+} from 'lucide-react';
+import { NavLink } from '@/components/NavLink';
+import { useLocation } from 'react-router-dom';
+import {
+ Sidebar,
+ SidebarContent,
+ SidebarGroup,
+ SidebarGroupContent,
+ SidebarGroupLabel,
+ SidebarMenu,
+ SidebarMenuButton,
+ SidebarMenuItem,
+ SidebarHeader,
+ SidebarFooter,
+ useSidebar,
+} from '@/components/ui/sidebar';
+
+const modules = [
+ { title: 'Compliance Agent', url: '/register-agent', icon: Bot, module: 'M1' },
+ { title: 'Dashboard', url: '/', icon: LayoutDashboard, module: 'M2' },
+ { title: 'Risk Assessment', url: '/risk-assessment', icon: ShieldAlert, module: 'M3' },
+ { title: 'Doc Vault', url: '/doc-vault', icon: FolderArchive, module: 'M4' },
+ { title: 'Response Tracker', url: '/response-tracker', icon: MailWarning, module: 'M8' },
+ { title: 'Task Manager', url: '/tasks', icon: ListTodo, module: 'M9' },
+ { title: 'Compliance Timeline', url: '/timeline', icon: CalendarClock, module: 'M10' },
+ { title: 'KPIs', url: '/kpis', icon: Target, module: 'M11' },
+ { title: 'Compliance Calendar', url: '/calendar', icon: CalendarDays, module: 'M12' },
+ { title: 'Risk Action Plan', url: '/risk-action-plan', icon: ClipboardList, module: 'M13' },
+ { title: 'Register Editor', url: '/register-manager', icon: FilePlus2, module: 'M14' },
+ { title: 'Agent Deliverables', url: '/agent-outputs/register-extract', icon: Bot, module: 'M15' },
+
+
+ { title: 'AI Chatbot', url: '/chatbot', icon: MessageSquare, module: 'M5' },
+ { title: 'Assistant', url: '/assistant', icon: BarChart3, module: 'M6' },
+ { title: 'Admin', url: '/admin', icon: Settings, module: 'M7' },
+];
+
+export function AppSidebar() {
+ const { state } = useSidebar();
+ const collapsed = state === 'collapsed';
+ const location = useLocation();
+
+ return (
+
+
+
+
+ {!collapsed && (
+
+
SEBI Compliance Manager
+
+ )}
+
+
+
+
+
+
+ Modules
+
+
+
+ {modules.map((item) => (
+
+
+
+
+ {!collapsed && (
+
+ {item.module}
+ {item.title}
+
+ )}
+
+
+
+ ))}
+
+
+
+
+
+
+ {!collapsed && (
+
+ LODR 2015 · PIT 2015 · SAST 2011
+
+ )}
+
+
+ );
+}
diff --git a/src/components/ComplianceDetailDrawer.tsx b/src/components/ComplianceDetailDrawer.tsx
new file mode 100644
index 0000000..0759529
--- /dev/null
+++ b/src/components/ComplianceDetailDrawer.tsx
@@ -0,0 +1,169 @@
+import { useComplianceStore } from '@/store/complianceStore';
+import { ComplianceItem } from '@/data/complianceData';
+import { effectiveRiskLevel, riskReasons } from '@/data/workflowData';
+import { toTitleCaseLabel } from '@/lib/chartTheme';
+import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription } from '@/components/ui/sheet';
+import { StatusBadge, RiskBadge, NatureBadge, ApprovalBadge } from '@/components/StatusBadges';
+import { Badge } from '@/components/ui/badge';
+import { Button } from '@/components/ui/button';
+import { Textarea } from '@/components/ui/textarea';
+import { Separator } from '@/components/ui/separator';
+import { ExternalLink, Calendar, Building2, FileText, AlertTriangle, Upload, Send } from 'lucide-react';
+import { useState } from 'react';
+import { Link } from 'react-router-dom';
+
+export function ComplianceDetailDrawer() {
+ const { items, drawerOpen, selectedItemId, setDrawerOpen, addComment, updateItemStatus, updateApprovalStatus } = useComplianceStore();
+ const item = items.find(i => i.id === selectedItemId);
+ const [commentText, setCommentText] = useState('');
+
+ if (!item) return null;
+
+ const handleAddComment = () => {
+ if (!commentText.trim()) return;
+ addComment(item.id, {
+ id: Date.now().toString(),
+ author: 'You',
+ text: commentText.trim(),
+ timestamp: new Date().toLocaleString(),
+ });
+ setCommentText('');
+ };
+
+ return (
+
+
+
+
+ #{item.sNo}
+
+
+
+
+ {item.filingName}
+ {toTitleCaseLabel(item.category)}
+
+
+
+ setDrawerOpen(false)}
+ className="inline-flex items-center gap-1 rounded-md border border-secondary/40 bg-secondary/10 px-2.5 py-1 text-[11px] font-medium text-secondary hover:bg-secondary/20"
+ >
+ Open Full Detail Page
+
+
+
+
+
+
+ Why This Carries {effectiveRiskLevel(item)} Risk
+
+
+ {riskReasons(item).map((r, i) => (
+
+ •
+ {r}
+
+ ))}
+
+
+
+
+
+
+ } label="Regulation" value={item.regReference} />
+ } label="Authority" value={item.filingAuthority} />
+ } label="Frequency" value={item.frequency} />
+ } label="Due Date" value={item.dueDate} />
+
+
+
+
+
+
Applicable To
+
{item.applicableTo}
+
+
+
+
Trigger / Event
+
{item.trigger}
+
+
+
+
Timeline
+
{item.timeline}
+
+
+
+
Format / Mode
+
{item.format}
+
+
+
+
+ Penalty
+
+
{item.penalty}
+
+
+
+
+
+
Workflow
+
+
Owner: {item.owner}
+
Approver: {item.approver}
+
+
Evidence: {item.evidenceUploaded ? '✓ Uploaded' : '✗ Missing'}
+
+
+
+ {item.sourceUrl && (
+
+ View SEBI Source
+
+ )}
+
+
+
+
+
Comments ({item.comments.length})
+ {item.comments.map(c => (
+
+
+ {c.author}
+ {c.timestamp}
+
+
{c.text}
+
+ ))}
+
+
+
+
+
+
+ );
+}
+
+function DetailField({ icon, label, value }: { icon: React.ReactNode; label: string; value: string }) {
+ return (
+
+
+ {icon}
+ {label}
+
+
{value}
+
+ );
+}
diff --git a/src/components/DocumentUploadForm.tsx b/src/components/DocumentUploadForm.tsx
new file mode 100644
index 0000000..6d03048
--- /dev/null
+++ b/src/components/DocumentUploadForm.tsx
@@ -0,0 +1,211 @@
+import { useMemo, useRef, useState } from 'react';
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
+import { Button } from '@/components/ui/button';
+import { Input } from '@/components/ui/input';
+import { Textarea } from '@/components/ui/textarea';
+import { Label } from '@/components/ui/label';
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
+import { Badge } from '@/components/ui/badge';
+import { Upload, X, FileUp, Link2 } from 'lucide-react';
+import { toast } from 'sonner';
+import { useComplianceStore } from '@/store/complianceStore';
+import { VaultDocument } from '@/data/vaultData';
+import { toTitleCaseLabel } from '@/lib/chartTheme';
+
+const FISCAL_YEARS = ['FY2025-26', 'FY2024-25'];
+const DOC_TYPES = ['Filing', 'Report', 'Disclosure', 'Statement', 'Certificate', 'Board Minutes', 'Notice Response', 'Supporting Evidence'];
+
+export function DocumentUploadForm() {
+ const items = useComplianceStore(s => s.items);
+ const uploadDocuments = useComplianceStore(s => s.uploadDocuments);
+ const inputRef = useRef(null);
+
+ const [files, setFiles] = useState([]);
+ const [itemId, setItemId] = useState('');
+ const [search, setSearch] = useState('');
+ const [documentType, setDocumentType] = useState('Filing');
+ const [fiscalYear, setFiscalYear] = useState('FY2025-26');
+ const [uploadedBy, setUploadedBy] = useState('');
+ const [docStatus, setDocStatus] = useState<'Uploaded' | 'Filed'>('Uploaded');
+ const [remarks, setRemarks] = useState('');
+ const [dragging, setDragging] = useState(false);
+
+ const sortedItems = useMemo(
+ () => [...items]
+ .filter(i => !search || `${i.filingName} ${i.regReference} ${i.category}`.toLowerCase().includes(search.toLowerCase()))
+ .sort((a, b) => a.filingName.localeCompare(b.filingName))
+ .slice(0, 60),
+ [items, search],
+ );
+
+ const selected = items.find(i => String(i.id) === itemId);
+
+ const addFiles = (list: FileList | null) => {
+ if (!list) return;
+ setFiles(prev => [...prev, ...Array.from(list)]);
+ };
+
+ const reset = () => {
+ setFiles([]);
+ setRemarks('');
+ if (inputRef.current) inputRef.current.value = '';
+ };
+
+ const submit = () => {
+ if (files.length === 0) {
+ toast.error('Choose at least one file to upload');
+ return;
+ }
+ if (!itemId) {
+ toast.error('Link the document to an item in the Master Compliance Register');
+ return;
+ }
+ uploadDocuments({
+ itemId: Number(itemId),
+ section: 'compliance-filings' as VaultDocument['section'],
+ documentType,
+ fiscalYear,
+ uploadedBy: uploadedBy.trim() || 'Compliance Team',
+ docStatus,
+ remarks,
+ files: files.map(f => ({
+ name: f.name,
+ sizeBytes: f.size,
+ extension: (f.name.split('.').pop() || 'FILE').toLowerCase(),
+ url: URL.createObjectURL(f),
+ })),
+ });
+ toast.success(`${files.length} document(s) uploaded`, {
+ description: `${selected?.filingName} updated in the Master Register${docStatus === 'Filed' ? ' and marked as completed' : ''}. Risk Assessment refreshed.`,
+ });
+ reset();
+ };
+
+ return (
+
+
+
+ Upload Documents
+
+
+ Every upload is linked to the Master Compliance Register, so the register, this vault and the Risk Assessment update together.
+
+
+
+ inputRef.current?.click()}
+ onDragOver={e => { e.preventDefault(); setDragging(true); }}
+ onDragLeave={() => setDragging(false)}
+ onDrop={e => { e.preventDefault(); setDragging(false); addFiles(e.dataTransfer.files); }}
+ >
+
+
Drag & drop files or click to browse
+
Supported: PDF, XLSX, DOCX, PPTX, XML, XBRL, CSV, ZIP
+
addFiles(e.target.files)}
+ />
+
+
+ {files.length > 0 && (
+
+ {files.map((f, idx) => (
+
+ {f.name}
+ {(f.size / 1024).toFixed(0)} KB
+ setFiles(prev => prev.filter((_, i) => i !== idx))}
+ >
+
+
+
+ ))}
+
+ )}
+
+
+
+
Link To Master Compliance Register
+
setSearch(e.target.value)}
+ className="h-8 text-xs"
+ />
+
+
+
+ {sortedItems.map(i => (
+
+ {i.filingName} — {i.regReference}
+
+ ))}
+
+
+ {selected && (
+
+
+ {toTitleCaseLabel(selected.category)} · Due {selected.dueDate} · Owner {selected.owner}
+
+ )}
+
+
+
+ Document Type
+
+
+
+ {DOC_TYPES.map(t => {t} )}
+
+
+
+
+
+ Fiscal Year
+
+
+
+ {FISCAL_YEARS.map(y => {y} )}
+
+
+
+
+
+ Uploaded By
+ setUploadedBy(e.target.value)} placeholder="Your name" className="h-8 text-xs" />
+
+
+
+ Status Update To Master Register
+ setDocStatus(v as 'Uploaded' | 'Filed')}>
+
+
+ Evidence Uploaded — Pending Approval
+ Filed — Mark Compliance Completed
+
+
+
+
+
+ Remarks
+
+
+
+
+ Clear
+
+ Upload To Vault
+
+
+
+
+ );
+}
diff --git a/src/components/MaterialEventsSection.tsx b/src/components/MaterialEventsSection.tsx
new file mode 100644
index 0000000..1f7b822
--- /dev/null
+++ b/src/components/MaterialEventsSection.tsx
@@ -0,0 +1,182 @@
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
+import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
+import { Badge } from '@/components/ui/badge';
+import { materialEvents, type MaterialEvent } from '@/data/materialEventsData';
+import { useMemo, useState } from 'react';
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
+import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog';
+import { Zap, AlertTriangle, Clock, Info, FileText, Calendar, Bell } from 'lucide-react';
+
+const urgencyConfig = {
+ critical: { label: 'Critical', className: 'bg-destructive/15 text-destructive border-destructive/30', icon: Zap },
+ high: { label: 'High', className: 'bg-warning/15 text-warning border-warning/30', icon: AlertTriangle },
+ medium: { label: 'Medium', className: 'bg-primary/15 text-primary border-primary/30', icon: Clock },
+ low: { label: 'Low', className: 'bg-muted text-muted-foreground border-border', icon: Info },
+};
+
+export function MaterialEventsSection() {
+ const [urgencyFilter, setUrgencyFilter] = useState('all');
+ const [selectedEvent, setSelectedEvent] = useState(null);
+
+ const filtered = useMemo(() => {
+ if (urgencyFilter === 'all') return materialEvents;
+ return materialEvents.filter(e => e.urgency === urgencyFilter);
+ }, [urgencyFilter]);
+
+ const counts = useMemo(() => ({
+ critical: materialEvents.filter(e => e.urgency === 'critical').length,
+ high: materialEvents.filter(e => e.urgency === 'high').length,
+ medium: materialEvents.filter(e => e.urgency === 'medium').length,
+ low: materialEvents.filter(e => e.urgency === 'low').length,
+ }), []);
+
+ return (
+
+
+
+
+
Material Events & Compliance Obligations
+
+ {materialEvents.length} event-triggered disclosure requirements — SEBI LODR & allied regulations
+
+
+
+
+ {(Object.entries(counts) as [keyof typeof urgencyConfig, number][]).map(([key, count]) => {
+ const cfg = urgencyConfig[key];
+ const active = urgencyFilter === key;
+ return (
+ setUrgencyFilter(active ? 'all' : key)}
+ className={`inline-flex items-center justify-center whitespace-nowrap leading-none text-[10px] font-semibold h-6 min-w-[64px] px-2.5 flex-shrink-0 rounded-full border transition-colors cursor-pointer ${cfg.className} ${active ? 'ring-2 ring-offset-1 ring-primary' : 'hover:opacity-90'}`}
+ aria-pressed={active}
+ aria-label={`Show ${cfg.label} material events`}
+ >
+ {cfg.label}: {count}
+
+ );
+ })}
+
+
+
+
+
+
+ All Urgency
+ Critical
+ High
+ Medium
+ Low
+
+
+
+
+
+
+
+
+
+
+ Timeline
+ Disclosure / Obligation
+ Trigger Event
+ Regulation
+ Urgency
+
+
+
+ {filtered.map(event => {
+ const cfg = urgencyConfig[event.urgency];
+ const Icon = cfg.icon;
+ return (
+ setSelectedEvent(event)}
+ className="cursor-pointer hover:bg-muted/50"
+ aria-label={`View details for ${event.disclosureName}`}
+ >
+ {event.timeline}
+
+ {event.disclosureName}
+
+
+ {event.triggerEvent}
+
+
+ {event.regulation}
+
+
+
+
+ {cfg.label}
+
+
+
+ );
+ })}
+
+
+
+
+
+ !open && setSelectedEvent(null)}>
+ {selectedEvent && (
+
+
+
+ {(() => {
+ const Icon = urgencyConfig[selectedEvent.urgency].icon;
+ return ;
+ })()}
+ {selectedEvent.disclosureName}
+
+
+ Material event disclosure obligation under {selectedEvent.regulation}
+
+
+
+
+
+
+
+ Response Timeline
+
+
{selectedEvent.timeline}
+
+
+
+
+ Urgency
+
+
+ {urgencyConfig[selectedEvent.urgency].label}
+
+
+
+
+
+
+ Trigger Event
+
+
{selectedEvent.triggerEvent}
+
+
+
+
+ Response / Action Required
+
+
{selectedEvent.responseTime}
+
+
+ S.No. {selectedEvent.sNo}
+ {selectedEvent.regulation}
+
+
+
+ )}
+
+
+ );
+}
diff --git a/src/components/MonthlyComplianceCalendar.tsx b/src/components/MonthlyComplianceCalendar.tsx
new file mode 100644
index 0000000..fe15e39
--- /dev/null
+++ b/src/components/MonthlyComplianceCalendar.tsx
@@ -0,0 +1,196 @@
+import { useMemo, useState } from 'react';
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
+import { Button } from '@/components/ui/button';
+import { ChevronLeft, ChevronRight, CalendarDays } from 'lucide-react';
+import { ComplianceItem } from '@/data/complianceData';
+import { deriveComplianceState, ComplianceState } from '@/data/workflowData';
+import { useComplianceStore } from '@/store/complianceStore';
+import { Link } from 'react-router-dom';
+
+const stateStyle: Record = {
+ Completed: 'bg-success/15 text-success border-success/40',
+ Overdue: 'bg-destructive/15 text-destructive border-destructive/40',
+ 'Documents Missing': 'bg-warning/15 text-warning border-warning/40',
+ 'On Track': 'bg-secondary/15 text-secondary border-secondary/40',
+};
+
+const dayNames = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
+const monthNames = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
+
+interface Props {
+ items?: ComplianceItem[];
+ onDrillCategory?: (category: string) => void;
+}
+
+export function MonthlyComplianceCalendar({ items: itemsProp }: Props) {
+ const storeItems = useComplianceStore(s => s.items);
+ // Always resolve against the Master Compliance Register so any change there flows through.
+ const items = useMemo(() => {
+ if (!itemsProp) return storeItems;
+ const ids = new Set(itemsProp.map(i => i.id));
+ return storeItems.filter(i => ids.has(i.id));
+ }, [itemsProp, storeItems]);
+
+ const firstDue = useMemo(() => {
+ const dates = items.map(i => i.dueDate).filter(Boolean).sort();
+ return dates[0] ? new Date(dates[0]) : new Date();
+ }, [items]);
+
+ const [cursor, setCursor] = useState(() => new Date(firstDue.getFullYear(), firstDue.getMonth(), 1));
+ const [selectedDay, setSelectedDay] = useState(null);
+
+ const todayStr = new Date().toISOString().split('T')[0];
+ const year = cursor.getFullYear();
+ const month = cursor.getMonth();
+
+
+ const byDate = useMemo(() => {
+ const map = new Map();
+ items.forEach(i => {
+ if (!i.dueDate) return;
+ const list = map.get(i.dueDate) ?? [];
+ list.push(i);
+ map.set(i.dueDate, list);
+ });
+ return map;
+ }, [items]);
+
+ const cells = useMemo(() => {
+ const first = new Date(year, month, 1);
+ const offset = (first.getDay() + 6) % 7; // Monday-first
+ const daysInMonth = new Date(year, month + 1, 0).getDate();
+ const out: (string | null)[] = Array.from({ length: offset }, () => null);
+ for (let d = 1; d <= daysInMonth; d++) {
+ out.push(`${year}-${String(month + 1).padStart(2, '0')}-${String(d).padStart(2, '0')}`);
+ }
+ while (out.length % 7 !== 0) out.push(null);
+ return out;
+ }, [year, month]);
+
+ const monthItems = useMemo(
+ () => items.filter(i => i.dueDate?.startsWith(`${year}-${String(month + 1).padStart(2, '0')}`)),
+ [items, year, month],
+ );
+
+ const selectedItems = selectedDay ? byDate.get(selectedDay) ?? [] : [];
+
+ const daysUntil = (date: string) =>
+ Math.ceil((new Date(date).getTime() - new Date(todayStr).getTime()) / 86400000);
+
+ return (
+
+
+
+
+
+ Monthly Compliance Calendar
+
+
+ {monthItems.length} deadline{monthItems.length === 1 ? '' : 's'} in {monthNames[month]} {year} — click a date to see filings, status and days until due
+
+
+
+ { setCursor(new Date(year, month - 1, 1)); setSelectedDay(null); }}>
+
+
+ {monthNames[month]} {year}
+ { setCursor(new Date(year, month + 1, 1)); setSelectedDay(null); }}>
+
+
+
+
+
+
+
+ {dayNames.map(d => (
+
{d}
+ ))}
+ {cells.map((date, idx) => {
+ if (!date) return
;
+ const dayItems = byDate.get(date) ?? [];
+ const isToday = date === todayStr;
+ const isSelected = date === selectedDay;
+ const shown = dayItems.slice(0, 2);
+ const extra = dayItems.length - shown.length;
+ return (
+
+
dayItems.length && setSelectedDay(isSelected ? null : date)}
+ className={`text-[10px] font-semibold ${isToday ? 'text-secondary' : 'text-muted-foreground'} ${dayItems.length ? 'hover:text-secondary' : 'cursor-default'}`}
+ >
+ {Number(date.slice(-2))}
+
+
+ {shown.map(i => {
+ const s = deriveComplianceState(i);
+ return (
+
+ {i.filingName}
+
+ );
+ })}
+ {extra > 0 && (
+ setSelectedDay(isSelected ? null : date)}
+ className="block w-full text-left rounded-sm px-1 text-[9px] leading-4 text-muted-foreground hover:text-secondary"
+ >
+ +{extra} More
+
+ )}
+
+
+ );
+ })}
+
+
+
+
+ {(Object.keys(stateStyle) as ComplianceState[]).map(s => (
+
+ {s}
+
+ ))}
+
+
+ {selectedDay && (
+
+
+
Deadlines On {selectedDay}
+
{selectedItems.length} filing(s) — open any row for the full detail in the Master Compliance Register
+
+ {selectedItems.map(i => {
+ const state = deriveComplianceState(i);
+ const days = daysUntil(i.dueDate);
+ return (
+
+
+ {state}
+
+
{i.filingName}
+
{i.regReference}
+
+ {days < 0 ? `${Math.abs(days)} Days Overdue` : days === 0 ? 'Due Today' : `${days} Days Left`}
+
+
+ );
+ })}
+
+ )}
+
+
+ );
+}
diff --git a/src/components/NavLink.tsx b/src/components/NavLink.tsx
new file mode 100644
index 0000000..a561a95
--- /dev/null
+++ b/src/components/NavLink.tsx
@@ -0,0 +1,28 @@
+import { NavLink as RouterNavLink, NavLinkProps } from "react-router-dom";
+import { forwardRef } from "react";
+import { cn } from "@/lib/utils";
+
+interface NavLinkCompatProps extends Omit {
+ className?: string;
+ activeClassName?: string;
+ pendingClassName?: string;
+}
+
+const NavLink = forwardRef(
+ ({ className, activeClassName, pendingClassName, to, ...props }, ref) => {
+ return (
+
+ cn(className, isActive && activeClassName, isPending && pendingClassName)
+ }
+ {...props}
+ />
+ );
+ },
+);
+
+NavLink.displayName = "NavLink";
+
+export { NavLink };
diff --git a/src/components/StatTile.tsx b/src/components/StatTile.tsx
new file mode 100644
index 0000000..fe8798a
--- /dev/null
+++ b/src/components/StatTile.tsx
@@ -0,0 +1,36 @@
+import { TileColor } from '@/lib/chartTheme';
+
+interface StatTileProps {
+ icon?: React.ReactNode;
+ label: string;
+ value: number;
+ bg: TileColor;
+ active?: boolean;
+ onClick?: () => void;
+ title?: string;
+}
+
+/**
+ * Shared clickable stat tile used by the Dashboard and Risk Assessment modules.
+ * Colours come from the shared chart palette so tiles always match their charts.
+ */
+export function StatTile({ icon, label, value, bg, active, onClick, title }: StatTileProps) {
+ return (
+
+
+
+ );
+}
diff --git a/src/components/StatusBadges.tsx b/src/components/StatusBadges.tsx
new file mode 100644
index 0000000..5608a33
--- /dev/null
+++ b/src/components/StatusBadges.tsx
@@ -0,0 +1,73 @@
+import { cn } from '@/lib/utils';
+import { ComplianceStatus, RiskLevel, ApprovalStatus, ComplianceNature } from '@/data/complianceData';
+
+const statusColors: Record = {
+ 'Completed': 'bg-success text-success-foreground',
+ 'Due Soon': 'bg-warning text-warning-foreground',
+ 'Overdue': 'bg-destructive text-destructive-foreground',
+ 'Not Due': 'bg-muted text-muted-foreground',
+ 'In Progress': 'bg-secondary text-secondary-foreground',
+ 'Not Started': 'bg-muted text-muted-foreground',
+};
+
+const riskColors: Record = {
+ 'Critical': 'bg-destructive text-destructive-foreground',
+ 'High': 'bg-warning text-warning-foreground',
+ 'Medium': 'bg-secondary text-secondary-foreground',
+ 'Low': 'bg-success text-success-foreground',
+};
+
+const approvalColors: Record = {
+ 'Approved': 'bg-success text-success-foreground',
+ 'Pending': 'bg-warning text-warning-foreground',
+ 'Doc Missing': 'bg-destructive/80 text-destructive-foreground',
+ 'Rejected': 'bg-destructive text-destructive-foreground',
+ 'Not Started': 'bg-muted text-muted-foreground',
+};
+
+const natureColors: Record = {
+ '[P]': 'bg-success/20 text-success border border-success/30',
+ '[E]': 'bg-destructive/20 text-destructive border border-destructive/30',
+ '[P+E]': 'bg-warning/20 text-warning border border-warning/30',
+ '[A]': 'bg-muted text-muted-foreground border border-border',
+};
+
+const badgeBase = 'inline-flex items-center justify-center rounded-full text-[10px] font-semibold whitespace-nowrap min-w-[70px] h-5 px-2.5 leading-none';
+
+export function StatusBadge({ status }: { status: ComplianceStatus }) {
+ return (
+
+ {status}
+
+ );
+}
+
+export function RiskBadge({ level }: { level: RiskLevel }) {
+ return (
+
+ {level}
+
+ );
+}
+
+export function ApprovalBadge({ status }: { status: ApprovalStatus }) {
+ return (
+
+ {status}
+
+ );
+}
+
+export function NatureBadge({ nature }: { nature: ComplianceNature }) {
+ const labels: Record = {
+ '[P]': 'Periodic',
+ '[E]': 'Event',
+ '[P+E]': 'Both',
+ '[A]': 'Admin',
+ };
+ return (
+
+ {labels[nature]}
+
+ );
+}
diff --git a/src/components/ui/accordion.tsx b/src/components/ui/accordion.tsx
new file mode 100644
index 0000000..1e7878c
--- /dev/null
+++ b/src/components/ui/accordion.tsx
@@ -0,0 +1,52 @@
+import * as React from "react";
+import * as AccordionPrimitive from "@radix-ui/react-accordion";
+import { ChevronDown } from "lucide-react";
+
+import { cn } from "@/lib/utils";
+
+const Accordion = AccordionPrimitive.Root;
+
+const AccordionItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+AccordionItem.displayName = "AccordionItem";
+
+const AccordionTrigger = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+ svg]:rotate-180",
+ className,
+ )}
+ {...props}
+ >
+ {children}
+
+
+
+));
+AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
+
+const AccordionContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+ {children}
+
+));
+
+AccordionContent.displayName = AccordionPrimitive.Content.displayName;
+
+export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
diff --git a/src/components/ui/alert-dialog.tsx b/src/components/ui/alert-dialog.tsx
new file mode 100644
index 0000000..6dfbfb4
--- /dev/null
+++ b/src/components/ui/alert-dialog.tsx
@@ -0,0 +1,104 @@
+import * as React from "react";
+import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog";
+
+import { cn } from "@/lib/utils";
+import { buttonVariants } from "@/components/ui/button";
+
+const AlertDialog = AlertDialogPrimitive.Root;
+
+const AlertDialogTrigger = AlertDialogPrimitive.Trigger;
+
+const AlertDialogPortal = AlertDialogPrimitive.Portal;
+
+const AlertDialogOverlay = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName;
+
+const AlertDialogContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+
+));
+AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName;
+
+const AlertDialogHeader = ({ className, ...props }: React.HTMLAttributes) => (
+
+);
+AlertDialogHeader.displayName = "AlertDialogHeader";
+
+const AlertDialogFooter = ({ className, ...props }: React.HTMLAttributes) => (
+
+);
+AlertDialogFooter.displayName = "AlertDialogFooter";
+
+const AlertDialogTitle = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName;
+
+const AlertDialogDescription = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+AlertDialogDescription.displayName = AlertDialogPrimitive.Description.displayName;
+
+const AlertDialogAction = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName;
+
+const AlertDialogCancel = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName;
+
+export {
+ AlertDialog,
+ AlertDialogPortal,
+ AlertDialogOverlay,
+ AlertDialogTrigger,
+ AlertDialogContent,
+ AlertDialogHeader,
+ AlertDialogFooter,
+ AlertDialogTitle,
+ AlertDialogDescription,
+ AlertDialogAction,
+ AlertDialogCancel,
+};
diff --git a/src/components/ui/alert.tsx b/src/components/ui/alert.tsx
new file mode 100644
index 0000000..2efc3c8
--- /dev/null
+++ b/src/components/ui/alert.tsx
@@ -0,0 +1,43 @@
+import * as React from "react";
+import { cva, type VariantProps } from "class-variance-authority";
+
+import { cn } from "@/lib/utils";
+
+const alertVariants = cva(
+ "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
+ {
+ variants: {
+ variant: {
+ default: "bg-background text-foreground",
+ destructive: "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ },
+);
+
+const Alert = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes & VariantProps
+>(({ className, variant, ...props }, ref) => (
+
+));
+Alert.displayName = "Alert";
+
+const AlertTitle = React.forwardRef>(
+ ({ className, ...props }, ref) => (
+
+ ),
+);
+AlertTitle.displayName = "AlertTitle";
+
+const AlertDescription = React.forwardRef>(
+ ({ className, ...props }, ref) => (
+
+ ),
+);
+AlertDescription.displayName = "AlertDescription";
+
+export { Alert, AlertTitle, AlertDescription };
diff --git a/src/components/ui/aspect-ratio.tsx b/src/components/ui/aspect-ratio.tsx
new file mode 100644
index 0000000..c9e6f4b
--- /dev/null
+++ b/src/components/ui/aspect-ratio.tsx
@@ -0,0 +1,5 @@
+import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio";
+
+const AspectRatio = AspectRatioPrimitive.Root;
+
+export { AspectRatio };
diff --git a/src/components/ui/avatar.tsx b/src/components/ui/avatar.tsx
new file mode 100644
index 0000000..68d21bb
--- /dev/null
+++ b/src/components/ui/avatar.tsx
@@ -0,0 +1,38 @@
+import * as React from "react";
+import * as AvatarPrimitive from "@radix-ui/react-avatar";
+
+import { cn } from "@/lib/utils";
+
+const Avatar = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+Avatar.displayName = AvatarPrimitive.Root.displayName;
+
+const AvatarImage = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+AvatarImage.displayName = AvatarPrimitive.Image.displayName;
+
+const AvatarFallback = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
+
+export { Avatar, AvatarImage, AvatarFallback };
diff --git a/src/components/ui/badge.tsx b/src/components/ui/badge.tsx
new file mode 100644
index 0000000..0853c44
--- /dev/null
+++ b/src/components/ui/badge.tsx
@@ -0,0 +1,29 @@
+import * as React from "react";
+import { cva, type VariantProps } from "class-variance-authority";
+
+import { cn } from "@/lib/utils";
+
+const badgeVariants = cva(
+ "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
+ {
+ variants: {
+ variant: {
+ default: "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
+ secondary: "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
+ destructive: "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
+ outline: "text-foreground",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ },
+);
+
+export interface BadgeProps extends React.HTMLAttributes, VariantProps {}
+
+function Badge({ className, variant, ...props }: BadgeProps) {
+ return
;
+}
+
+export { Badge, badgeVariants };
diff --git a/src/components/ui/breadcrumb.tsx b/src/components/ui/breadcrumb.tsx
new file mode 100644
index 0000000..ca91ff5
--- /dev/null
+++ b/src/components/ui/breadcrumb.tsx
@@ -0,0 +1,90 @@
+import * as React from "react";
+import { Slot } from "@radix-ui/react-slot";
+import { ChevronRight, MoreHorizontal } from "lucide-react";
+
+import { cn } from "@/lib/utils";
+
+const Breadcrumb = React.forwardRef<
+ HTMLElement,
+ React.ComponentPropsWithoutRef<"nav"> & {
+ separator?: React.ReactNode;
+ }
+>(({ ...props }, ref) => );
+Breadcrumb.displayName = "Breadcrumb";
+
+const BreadcrumbList = React.forwardRef>(
+ ({ className, ...props }, ref) => (
+
+ ),
+);
+BreadcrumbList.displayName = "BreadcrumbList";
+
+const BreadcrumbItem = React.forwardRef>(
+ ({ className, ...props }, ref) => (
+
+ ),
+);
+BreadcrumbItem.displayName = "BreadcrumbItem";
+
+const BreadcrumbLink = React.forwardRef<
+ HTMLAnchorElement,
+ React.ComponentPropsWithoutRef<"a"> & {
+ asChild?: boolean;
+ }
+>(({ asChild, className, ...props }, ref) => {
+ const Comp = asChild ? Slot : "a";
+
+ return ;
+});
+BreadcrumbLink.displayName = "BreadcrumbLink";
+
+const BreadcrumbPage = React.forwardRef>(
+ ({ className, ...props }, ref) => (
+
+ ),
+);
+BreadcrumbPage.displayName = "BreadcrumbPage";
+
+const BreadcrumbSeparator = ({ children, className, ...props }: React.ComponentProps<"li">) => (
+ svg]:size-3.5", className)} {...props}>
+ {children ?? }
+
+);
+BreadcrumbSeparator.displayName = "BreadcrumbSeparator";
+
+const BreadcrumbEllipsis = ({ className, ...props }: React.ComponentProps<"span">) => (
+
+
+ More
+
+);
+BreadcrumbEllipsis.displayName = "BreadcrumbElipssis";
+
+export {
+ Breadcrumb,
+ BreadcrumbList,
+ BreadcrumbItem,
+ BreadcrumbLink,
+ BreadcrumbPage,
+ BreadcrumbSeparator,
+ BreadcrumbEllipsis,
+};
diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx
new file mode 100644
index 0000000..cdedd4f
--- /dev/null
+++ b/src/components/ui/button.tsx
@@ -0,0 +1,47 @@
+import * as React from "react";
+import { Slot } from "@radix-ui/react-slot";
+import { cva, type VariantProps } from "class-variance-authority";
+
+import { cn } from "@/lib/utils";
+
+const buttonVariants = cva(
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
+ {
+ variants: {
+ variant: {
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
+ destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
+ outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
+ secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
+ ghost: "hover:bg-accent hover:text-accent-foreground",
+ link: "text-primary underline-offset-4 hover:underline",
+ },
+ size: {
+ default: "h-10 px-4 py-2",
+ sm: "h-9 rounded-md px-3",
+ lg: "h-11 rounded-md px-8",
+ icon: "h-10 w-10",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ },
+);
+
+export interface ButtonProps
+ extends React.ButtonHTMLAttributes,
+ VariantProps {
+ asChild?: boolean;
+}
+
+const Button = React.forwardRef(
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
+ const Comp = asChild ? Slot : "button";
+ return ;
+ },
+);
+Button.displayName = "Button";
+
+export { Button, buttonVariants };
diff --git a/src/components/ui/calendar.tsx b/src/components/ui/calendar.tsx
new file mode 100644
index 0000000..900a69e
--- /dev/null
+++ b/src/components/ui/calendar.tsx
@@ -0,0 +1,54 @@
+import * as React from "react";
+import { ChevronLeft, ChevronRight } from "lucide-react";
+import { DayPicker } from "react-day-picker";
+
+import { cn } from "@/lib/utils";
+import { buttonVariants } from "@/components/ui/button";
+
+export type CalendarProps = React.ComponentProps;
+
+function Calendar({ className, classNames, showOutsideDays = true, ...props }: CalendarProps) {
+ return (
+ ,
+ IconRight: ({ ..._props }) => ,
+ }}
+ {...props}
+ />
+ );
+}
+Calendar.displayName = "Calendar";
+
+export { Calendar };
diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx
new file mode 100644
index 0000000..e282748
--- /dev/null
+++ b/src/components/ui/card.tsx
@@ -0,0 +1,43 @@
+import * as React from "react";
+
+import { cn } from "@/lib/utils";
+
+const Card = React.forwardRef>(({ className, ...props }, ref) => (
+
+));
+Card.displayName = "Card";
+
+const CardHeader = React.forwardRef>(
+ ({ className, ...props }, ref) => (
+
+ ),
+);
+CardHeader.displayName = "CardHeader";
+
+const CardTitle = React.forwardRef>(
+ ({ className, ...props }, ref) => (
+
+ ),
+);
+CardTitle.displayName = "CardTitle";
+
+const CardDescription = React.forwardRef>(
+ ({ className, ...props }, ref) => (
+
+ ),
+);
+CardDescription.displayName = "CardDescription";
+
+const CardContent = React.forwardRef>(
+ ({ className, ...props }, ref) =>
,
+);
+CardContent.displayName = "CardContent";
+
+const CardFooter = React.forwardRef>(
+ ({ className, ...props }, ref) => (
+
+ ),
+);
+CardFooter.displayName = "CardFooter";
+
+export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent };
diff --git a/src/components/ui/carousel.tsx b/src/components/ui/carousel.tsx
new file mode 100644
index 0000000..3aa0f31
--- /dev/null
+++ b/src/components/ui/carousel.tsx
@@ -0,0 +1,224 @@
+import * as React from "react";
+import useEmblaCarousel, { type UseEmblaCarouselType } from "embla-carousel-react";
+import { ArrowLeft, ArrowRight } from "lucide-react";
+
+import { cn } from "@/lib/utils";
+import { Button } from "@/components/ui/button";
+
+type CarouselApi = UseEmblaCarouselType[1];
+type UseCarouselParameters = Parameters;
+type CarouselOptions = UseCarouselParameters[0];
+type CarouselPlugin = UseCarouselParameters[1];
+
+type CarouselProps = {
+ opts?: CarouselOptions;
+ plugins?: CarouselPlugin;
+ orientation?: "horizontal" | "vertical";
+ setApi?: (api: CarouselApi) => void;
+};
+
+type CarouselContextProps = {
+ carouselRef: ReturnType[0];
+ api: ReturnType[1];
+ scrollPrev: () => void;
+ scrollNext: () => void;
+ canScrollPrev: boolean;
+ canScrollNext: boolean;
+} & CarouselProps;
+
+const CarouselContext = React.createContext(null);
+
+function useCarousel() {
+ const context = React.useContext(CarouselContext);
+
+ if (!context) {
+ throw new Error("useCarousel must be used within a ");
+ }
+
+ return context;
+}
+
+const Carousel = React.forwardRef & CarouselProps>(
+ ({ orientation = "horizontal", opts, setApi, plugins, className, children, ...props }, ref) => {
+ const [carouselRef, api] = useEmblaCarousel(
+ {
+ ...opts,
+ axis: orientation === "horizontal" ? "x" : "y",
+ },
+ plugins,
+ );
+ const [canScrollPrev, setCanScrollPrev] = React.useState(false);
+ const [canScrollNext, setCanScrollNext] = React.useState(false);
+
+ const onSelect = React.useCallback((api: CarouselApi) => {
+ if (!api) {
+ return;
+ }
+
+ setCanScrollPrev(api.canScrollPrev());
+ setCanScrollNext(api.canScrollNext());
+ }, []);
+
+ const scrollPrev = React.useCallback(() => {
+ api?.scrollPrev();
+ }, [api]);
+
+ const scrollNext = React.useCallback(() => {
+ api?.scrollNext();
+ }, [api]);
+
+ const handleKeyDown = React.useCallback(
+ (event: React.KeyboardEvent) => {
+ if (event.key === "ArrowLeft") {
+ event.preventDefault();
+ scrollPrev();
+ } else if (event.key === "ArrowRight") {
+ event.preventDefault();
+ scrollNext();
+ }
+ },
+ [scrollPrev, scrollNext],
+ );
+
+ React.useEffect(() => {
+ if (!api || !setApi) {
+ return;
+ }
+
+ setApi(api);
+ }, [api, setApi]);
+
+ React.useEffect(() => {
+ if (!api) {
+ return;
+ }
+
+ onSelect(api);
+ api.on("reInit", onSelect);
+ api.on("select", onSelect);
+
+ return () => {
+ api?.off("select", onSelect);
+ };
+ }, [api, onSelect]);
+
+ return (
+
+
+ {children}
+
+
+ );
+ },
+);
+Carousel.displayName = "Carousel";
+
+const CarouselContent = React.forwardRef>(
+ ({ className, ...props }, ref) => {
+ const { carouselRef, orientation } = useCarousel();
+
+ return (
+
+ );
+ },
+);
+CarouselContent.displayName = "CarouselContent";
+
+const CarouselItem = React.forwardRef>(
+ ({ className, ...props }, ref) => {
+ const { orientation } = useCarousel();
+
+ return (
+
+ );
+ },
+);
+CarouselItem.displayName = "CarouselItem";
+
+const CarouselPrevious = React.forwardRef>(
+ ({ className, variant = "outline", size = "icon", ...props }, ref) => {
+ const { orientation, scrollPrev, canScrollPrev } = useCarousel();
+
+ return (
+
+
+ Previous slide
+
+ );
+ },
+);
+CarouselPrevious.displayName = "CarouselPrevious";
+
+const CarouselNext = React.forwardRef>(
+ ({ className, variant = "outline", size = "icon", ...props }, ref) => {
+ const { orientation, scrollNext, canScrollNext } = useCarousel();
+
+ return (
+
+
+ Next slide
+
+ );
+ },
+);
+CarouselNext.displayName = "CarouselNext";
+
+export { type CarouselApi, Carousel, CarouselContent, CarouselItem, CarouselPrevious, CarouselNext };
diff --git a/src/components/ui/chart.tsx b/src/components/ui/chart.tsx
new file mode 100644
index 0000000..08d40d9
--- /dev/null
+++ b/src/components/ui/chart.tsx
@@ -0,0 +1,303 @@
+import * as React from "react";
+import * as RechartsPrimitive from "recharts";
+
+import { cn } from "@/lib/utils";
+
+// Format: { THEME_NAME: CSS_SELECTOR }
+const THEMES = { light: "", dark: ".dark" } as const;
+
+export type ChartConfig = {
+ [k in string]: {
+ label?: React.ReactNode;
+ icon?: React.ComponentType;
+ } & ({ color?: string; theme?: never } | { color?: never; theme: Record });
+};
+
+type ChartContextProps = {
+ config: ChartConfig;
+};
+
+const ChartContext = React.createContext(null);
+
+function useChart() {
+ const context = React.useContext(ChartContext);
+
+ if (!context) {
+ throw new Error("useChart must be used within a ");
+ }
+
+ return context;
+}
+
+const ChartContainer = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div"> & {
+ config: ChartConfig;
+ children: React.ComponentProps["children"];
+ }
+>(({ id, className, children, config, ...props }, ref) => {
+ const uniqueId = React.useId();
+ const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`;
+
+ return (
+
+
+
+ {children}
+
+
+ );
+});
+ChartContainer.displayName = "Chart";
+
+const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
+ const colorConfig = Object.entries(config).filter(([_, config]) => config.theme || config.color);
+
+ if (!colorConfig.length) {
+ return null;
+ }
+
+ return (
+