diff --git a/src/App.css b/src/App.css new file mode 100644 index 0000000000000000000000000000000000000000..b9d355df2a5956b526c004531b7b0ffe412461e0 --- /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 0000000000000000000000000000000000000000..18daf2e9004ae5e366e0127f288b7ca9d3a71db5 --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,27 @@ +import { Toaster } from "@/components/ui/toaster"; +import { Toaster as Sonner } from "@/components/ui/sonner"; +import { TooltipProvider } from "@/components/ui/tooltip"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { BrowserRouter, Routes, Route } from "react-router-dom"; +import Index from "./pages/Index"; +import NotFound from "./pages/NotFound"; + +const queryClient = new QueryClient(); + +const App = () => ( + + + + + + + } /> + {/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */} + } /> + + + + +); + +export default App; diff --git a/src/components/ActionButton.tsx b/src/components/ActionButton.tsx new file mode 100644 index 0000000000000000000000000000000000000000..32045718afb46fa95c13a4b55bc9be006056dffe --- /dev/null +++ b/src/components/ActionButton.tsx @@ -0,0 +1,40 @@ + +import React from 'react'; + +interface ActionButtonProps { + label: string; + onClick: () => void; + color: string; + icon: string; + disabled?: boolean; +} + +const ActionButton: React.FC = ({ label, onClick, color, icon, disabled = false }) => { + return ( + + ); +}; + +export default ActionButton; diff --git a/src/components/ActivityLog.tsx b/src/components/ActivityLog.tsx new file mode 100644 index 0000000000000000000000000000000000000000..8d0c7d1d0ea1ff0a8bbf5c6ec213af3698743acc --- /dev/null +++ b/src/components/ActivityLog.tsx @@ -0,0 +1,70 @@ + +import React from 'react'; + +interface LogEntry { + id: string; + message: string; + type: 'success' | 'warning' | 'error' | 'info'; + timestamp: Date; +} + +interface ActivityLogProps { + entries: LogEntry[]; +} + +const ActivityLog: React.FC = ({ entries }) => { + const getLogColor = (type: LogEntry['type']) => { + switch (type) { + case 'success': return 'bg-green-100 text-green-800 border-green-200'; + case 'warning': return 'bg-yellow-100 text-yellow-800 border-yellow-200'; + case 'error': return 'bg-red-100 text-red-800 border-red-200'; + case 'info': return 'bg-blue-100 text-blue-800 border-blue-200'; + default: return 'bg-gray-100 text-gray-800 border-gray-200'; + } + }; + + const getLogIcon = (type: LogEntry['type']) => { + switch (type) { + case 'success': return '✅'; + case 'warning': return '⚠️'; + case 'error': return '❌'; + case 'info': return 'ℹ️'; + default: return '📝'; + } + }; + + return ( +
+

+ 📋 Recent Activity +

+ +
+ {entries.length === 0 ? ( +
+ No recent activity... Your pet is waiting! 🐾 +
+ ) : ( + entries.map((entry) => ( +
+
+ {getLogIcon(entry.type)} +
+

{entry.message}

+

+ {entry.timestamp.toLocaleTimeString()} +

+
+
+
+ )) + )} +
+
+ ); +}; + +export default ActivityLog; diff --git a/src/components/CompactPetAvatar.tsx b/src/components/CompactPetAvatar.tsx new file mode 100644 index 0000000000000000000000000000000000000000..e7a910d24bb6f5963bba1323aff68b938b542075 --- /dev/null +++ b/src/components/CompactPetAvatar.tsx @@ -0,0 +1,92 @@ + +import React, { useState, useEffect } from 'react'; + +interface CompactPetAvatarProps { + health: number; + energy: number; + attention: number; +} + +const CompactPetAvatar: React.FC = ({ health, energy, attention }) => { + const [isBlinking, setIsBlinking] = useState(false); + const [mood, setMood] = useState<'happy' | 'normal' | 'tired' | 'sick'>('normal'); + + useEffect(() => { + const blinkInterval = setInterval(() => { + setIsBlinking(true); + setTimeout(() => setIsBlinking(false), 150); + }, 3000 + Math.random() * 2000); + + return () => clearInterval(blinkInterval); + }, []); + + useEffect(() => { + const avgStats = (health + energy + attention) / 3; + if (avgStats >= 80) setMood('happy'); + else if (avgStats >= 60) setMood('normal'); + else if (avgStats >= 30) setMood('tired'); + else setMood('sick'); + }, [health, energy, attention]); + + const getMoodColor = () => { + switch (mood) { + case 'happy': return '#A7F3D0'; + case 'normal': return '#BFDBFE'; + case 'tired': return '#FDE68A'; + case 'sick': return '#FECACA'; + default: return '#BFDBFE'; + } + }; + + return ( +
+
+ {/* Pet body with gentle bounce animation */} +
+ {/* Eyes */} +
+
+ {!isBlinking &&
} +
+
+ {!isBlinking &&
} +
+
+ + {/* Mouth based on mood */} +
+ {mood === 'happy' && ( +
+ )} + {mood === 'normal' && ( +
+ )} + {(mood === 'tired' || mood === 'sick') && ( +
+ )} +
+ + {/* Cheek blush for happy mood */} + {mood === 'happy' && ( + <> +
+
+ + )} +
+ + {/* Floating particles for happy mood */} + {mood === 'happy' && ( +
+
+
+ )} +
+
+ ); +}; + +export default CompactPetAvatar; diff --git a/src/components/Dashboard.tsx b/src/components/Dashboard.tsx new file mode 100644 index 0000000000000000000000000000000000000000..fa9cef1787650cabb506729895904c28b9fd6066 --- /dev/null +++ b/src/components/Dashboard.tsx @@ -0,0 +1,154 @@ + +import React from 'react'; +import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, BarChart, Bar } from 'recharts'; + +interface DashboardProps { + health: number; + energy: number; + attention: number; +} + +const Dashboard: React.FC = ({ health, energy, attention }) => { + // Generate sample data for the last 15 time periods + const generateChartData = () => { + const data = []; + for (let i = 0; i < 15; i++) { + data.push({ + time: i + 1, + health: Math.max(0, health + (Math.random() - 0.5) * 40), + energy: Math.max(0, energy + (Math.random() - 0.5) * 30), + attention: Math.max(0, attention + (Math.random() - 0.5) * 35), + dataQuality: Math.random() * 100, + processedSamples: Math.floor(Math.random() * 1000) + 500, + }); + } + return data; + }; + + const chartData = generateChartData(); + + const metrics = [ + { label: 'Dataset Quality', value: '94.2%', color: '#10B981', trend: '+2.1%' }, + { label: 'Processed Samples', value: '12.5K', color: '#F59E0B', trend: '+8.3%' }, + { label: 'Model Accuracy', value: '89.7%', color: '#3B82F6', trend: '+1.4%' }, + { label: 'Clean Records', value: '98.1%', color: '#8B5CF6', trend: '+0.9%' }, + ]; + + return ( +
+

+ 📊 Pet Performance Dashboard +

+ +
+ {/* Pet Stats Chart */} +
+

Pet Vital Stats Over Time

+ + + + + + + + + + + +
+ + {/* Data Processing Chart */} +
+

Data Processing Activity

+ + + + + + + + + +
+
+ + {/* Metrics Grid */} +
+ {metrics.map((metric, index) => ( +
+
+ {metric.value} +
+
{metric.label}
+
+ {metric.trend} +
+
+ ))} +
+
+ ); +}; + +export default Dashboard; diff --git a/src/components/DataVisualization.tsx b/src/components/DataVisualization.tsx new file mode 100644 index 0000000000000000000000000000000000000000..68bc2b2e73d65dfb45991efbf666ebe7a5f1d7fa --- /dev/null +++ b/src/components/DataVisualization.tsx @@ -0,0 +1,178 @@ + +import React from 'react'; +import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts'; + +const DataVisualization: React.FC = () => { + // Generate sample data similar to your reference image + const generateTimeSeriesData = () => { + const data = []; + for (let i = 0; i <= 15; i++) { + data.push({ + time: i, + shoulder_pan_pos: 100 - i * 8 + Math.sin(i * 0.5) * 20, + shoulder_lift_pos: -30 + Math.cos(i * 0.3) * 15, + elbow_flex_pos: 50 - i * 2 + Math.sin(i * 0.8) * 10, + wrist_flex_pos: 80 - i * 3 + Math.cos(i * 0.6) * 25, + wrist_roll_pos: 10 + Math.sin(i * 0.4) * 8, + gripper_pos: -5 + Math.cos(i * 0.2) * 12, + }); + } + return data; + }; + + const leftChartData = generateTimeSeriesData(); + const rightChartData = generateTimeSeriesData().map(item => ({ + ...item, + wrist_flex_pos: item.wrist_flex_pos + 20, + wrist_roll_pos: item.wrist_roll_pos - 10, + gripper_pos: item.gripper_pos + 15, + })); + + const metricsList = [ + { name: 'shoulder_pan_pos', color: '#ef4444', value: '-15.33' }, + { name: 'shoulder_lift_pos', color: '#22c55e', value: '-18.39' }, + { name: 'elbow_flex_pos', color: '#3b82f6', value: '34.99' }, + ]; + + const rightMetricsList = [ + { name: 'wrist_flex_pos', color: '#ef4444', value: '68.87' }, + { name: 'wrist_roll_pos', color: '#22c55e', value: '-4.01' }, + { name: 'gripper_pos', color: '#3b82f6', value: '19.82' }, + ]; + + return ( +
+ {/* Header with observation images */} +
+
+
+

observation_images_front

+
+ + +
+
+
+
📦 Dataset Sample View
+
+
+ +
+
+

observation_images_side

+
+ + +
+
+
+
🎯 Model Predictions
+
+
+
+ + {/* Time series charts */} +
+ {/* Left Chart */} +
+ + + + + + + + + + + + + {/* Metrics table */} +
+ {metricsList.map((metric, index) => ( +
+
+
+ {metric.name} +
+
+ action + {metric.value} +
+
+ ))} +
+
+ + {/* Right Chart */} +
+ + + + + + + + + + + + + {/* Metrics table */} +
+ {rightMetricsList.map((metric, index) => ( +
+
+
+ {metric.name} +
+
+ action + {metric.value} +
+
+ ))} +
+
+
+
+ ); +}; + +export default DataVisualization; diff --git a/src/components/ModeToggle.tsx b/src/components/ModeToggle.tsx new file mode 100644 index 0000000000000000000000000000000000000000..541fbec93cb9deec953ef5e1af1c292ed0184d4c --- /dev/null +++ b/src/components/ModeToggle.tsx @@ -0,0 +1,44 @@ + +import React from 'react'; +import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group'; + +interface ModeToggleProps { + currentMode: 'pet' | 'data' | 'hybrid'; + onModeChange: (mode: 'pet' | 'data' | 'hybrid') => void; +} + +const ModeToggle: React.FC = ({ currentMode, onModeChange }) => { + return ( +
+
+ value && onModeChange(value as 'pet' | 'data' | 'hybrid')} + className="gap-1" + > + + 🐾 Pet Mode + + + 🔄 Hybrid View + + + 📊 Data Mode + + +
+
+ ); +}; + +export default ModeToggle; diff --git a/src/components/PetAvatar.tsx b/src/components/PetAvatar.tsx new file mode 100644 index 0000000000000000000000000000000000000000..ae7f6639456364434838fc88ecb85e0bcde24546 --- /dev/null +++ b/src/components/PetAvatar.tsx @@ -0,0 +1,92 @@ + +import React, { useState, useEffect } from 'react'; + +interface PetAvatarProps { + health: number; + energy: number; + attention: number; +} + +const PetAvatar: React.FC = ({ health, energy, attention }) => { + const [isBlinking, setIsBlinking] = useState(false); + const [mood, setMood] = useState<'happy' | 'normal' | 'tired' | 'sick'>('normal'); + + useEffect(() => { + const blinkInterval = setInterval(() => { + setIsBlinking(true); + setTimeout(() => setIsBlinking(false), 150); + }, 3000 + Math.random() * 2000); + + return () => clearInterval(blinkInterval); + }, []); + + useEffect(() => { + const avgStats = (health + energy + attention) / 3; + if (avgStats >= 80) setMood('happy'); + else if (avgStats >= 60) setMood('normal'); + else if (avgStats >= 30) setMood('tired'); + else setMood('sick'); + }, [health, energy, attention]); + + const getMoodColor = () => { + switch (mood) { + case 'happy': return '#A7F3D0'; + case 'normal': return '#BFDBFE'; + case 'tired': return '#FDE68A'; + case 'sick': return '#FECACA'; + default: return '#BFDBFE'; + } + }; + + return ( +
+
+ {/* Pet body with gentle bounce animation */} +
+ {/* Eyes */} +
+
+ {!isBlinking &&
} +
+
+ {!isBlinking &&
} +
+
+ + {/* Mouth based on mood */} +
+ {mood === 'happy' && ( +
+ )} + {mood === 'normal' && ( +
+ )} + {(mood === 'tired' || mood === 'sick') && ( +
+ )} +
+ + {/* Cheek blush for happy mood */} + {mood === 'happy' && ( + <> +
+
+ + )} +
+ + {/* Floating particles for happy mood */} + {mood === 'happy' && ( +
+
+
+ )} +
+
+ ); +}; + +export default PetAvatar; diff --git a/src/components/StatusBar.tsx b/src/components/StatusBar.tsx new file mode 100644 index 0000000000000000000000000000000000000000..7f555cc5330b53881e9249bb3101f17a5aea73cd --- /dev/null +++ b/src/components/StatusBar.tsx @@ -0,0 +1,55 @@ + +import React from 'react'; + +interface StatusBarProps { + label: string; + value: number; + maxValue: number; + color: string; + icon: string; +} + +const StatusBar: React.FC = ({ label, value, maxValue, color, icon }) => { + const percentage = (value / maxValue) * 100; + + const getBarColor = () => { + if (percentage >= 70) return color; + if (percentage >= 40) return '#FDE68A'; + return '#FECACA'; + }; + + return ( +
+
+ + {icon} + {label.replace(/^[^\s]+\s/, '')} + + {value}/{maxValue} +
+ +
+
+ {percentage > 10 && ( +
+ )} +
+
+ + {percentage < 30 && ( +
+ ⚠️ Low {label.split(' ')[1] || label} +
+ )} +
+ ); +}; + +export default StatusBar; diff --git a/src/components/ui/accordion.tsx b/src/components/ui/accordion.tsx new file mode 100644 index 0000000000000000000000000000000000000000..e6a723d06574ee5cec8b00759b98f3fbe1ac7cc9 --- /dev/null +++ b/src/components/ui/accordion.tsx @@ -0,0 +1,56 @@ +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 0000000000000000000000000000000000000000..8722561cf6bda62d62f9a0c67730aefda971873a --- /dev/null +++ b/src/components/ui/alert-dialog.tsx @@ -0,0 +1,139 @@ +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 0000000000000000000000000000000000000000..41fa7e0561a3fdb5f986c1213a35e563de740e96 --- /dev/null +++ b/src/components/ui/alert.tsx @@ -0,0 +1,59 @@ +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< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +AlertTitle.displayName = "AlertTitle" + +const AlertDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ 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 0000000000000000000000000000000000000000..c4abbf37f217c715a0eaade7f45ac78600df419f --- /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 0000000000000000000000000000000000000000..991f56ecb117e96284bf0f6cad3b14ea2fdf5264 --- /dev/null +++ b/src/components/ui/avatar.tsx @@ -0,0 +1,48 @@ +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 0000000000000000000000000000000000000000..f000e3ef5176395b067dfc3f3e1256a80c450015 --- /dev/null +++ b/src/components/ui/badge.tsx @@ -0,0 +1,36 @@ +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 0000000000000000000000000000000000000000..71a5c325cdce2e6898d11cfeb4f2fdd458e3e2da --- /dev/null +++ b/src/components/ui/breadcrumb.tsx @@ -0,0 +1,115 @@ +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) =>