Spaces:
Sleeping
Sleeping
| import { | |
| Accordion, | |
| AccordionContent, | |
| AccordionItem, | |
| AccordionTrigger, | |
| } from "@/components/ui/accordion"; | |
| import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; | |
| import { AspectRatio } from "@/components/ui/aspect-ratio"; | |
| import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; | |
| import { Badge } from "@/components/ui/badge"; | |
| import { | |
| Breadcrumb, | |
| BreadcrumbItem, | |
| BreadcrumbLink, | |
| BreadcrumbList, | |
| BreadcrumbPage, | |
| BreadcrumbSeparator, | |
| } from "@/components/ui/breadcrumb"; | |
| import { Button } from "@/components/ui/button"; | |
| import { Calendar } from "@/components/ui/calendar"; | |
| import { | |
| Card, | |
| CardContent, | |
| CardDescription, | |
| CardFooter, | |
| CardHeader, | |
| CardTitle, | |
| } from "@/components/ui/card"; | |
| import { | |
| Carousel, | |
| CarouselContent, | |
| CarouselItem, | |
| CarouselNext, | |
| CarouselPrevious, | |
| } from "@/components/ui/carousel"; | |
| import { Checkbox } from "@/components/ui/checkbox"; | |
| import { | |
| Collapsible, | |
| CollapsibleContent, | |
| CollapsibleTrigger, | |
| } from "@/components/ui/collapsible"; | |
| import { | |
| Command, | |
| CommandEmpty, | |
| CommandGroup, | |
| CommandInput, | |
| CommandItem, | |
| CommandList, | |
| } from "@/components/ui/command"; | |
| import { | |
| ContextMenu, | |
| ContextMenuContent, | |
| ContextMenuItem, | |
| ContextMenuTrigger, | |
| } from "@/components/ui/context-menu"; | |
| import { | |
| Dialog, | |
| DialogContent, | |
| DialogDescription, | |
| DialogHeader, | |
| DialogTitle, | |
| DialogTrigger, | |
| } from "@/components/ui/dialog"; | |
| import { | |
| Drawer, | |
| DrawerClose, | |
| DrawerContent, | |
| DrawerDescription, | |
| DrawerFooter, | |
| DrawerHeader, | |
| DrawerTitle, | |
| DrawerTrigger, | |
| } from "@/components/ui/drawer"; | |
| import { | |
| DropdownMenu, | |
| DropdownMenuContent, | |
| DropdownMenuItem, | |
| DropdownMenuLabel, | |
| DropdownMenuSeparator, | |
| DropdownMenuTrigger, | |
| } from "@/components/ui/dropdown-menu"; | |
| import { | |
| HoverCard, | |
| HoverCardContent, | |
| HoverCardTrigger, | |
| } from "@/components/ui/hover-card"; | |
| import { Input } from "@/components/ui/input"; | |
| import { | |
| InputOTP, | |
| InputOTPGroup, | |
| InputOTPSlot, | |
| } from "@/components/ui/input-otp"; | |
| import { Label } from "@/components/ui/label"; | |
| import { | |
| Menubar, | |
| MenubarContent, | |
| MenubarItem, | |
| MenubarMenu, | |
| MenubarSeparator, | |
| MenubarTrigger, | |
| } from "@/components/ui/menubar"; | |
| import { | |
| Pagination, | |
| PaginationContent, | |
| PaginationItem, | |
| PaginationLink, | |
| PaginationNext, | |
| PaginationPrevious, | |
| } from "@/components/ui/pagination"; | |
| import { | |
| Popover, | |
| PopoverContent, | |
| PopoverTrigger, | |
| } from "@/components/ui/popover"; | |
| import { Progress } from "@/components/ui/progress"; | |
| import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; | |
| import { | |
| ResizableHandle, | |
| ResizablePanel, | |
| ResizablePanelGroup, | |
| } from "@/components/ui/resizable"; | |
| import { ScrollArea } from "@/components/ui/scroll-area"; | |
| import { | |
| Select, | |
| SelectContent, | |
| SelectItem, | |
| SelectTrigger, | |
| SelectValue, | |
| } from "@/components/ui/select"; | |
| import { Separator } from "@/components/ui/separator"; | |
| import { | |
| Sheet, | |
| SheetContent, | |
| SheetDescription, | |
| SheetHeader, | |
| SheetTitle, | |
| SheetTrigger, | |
| } from "@/components/ui/sheet"; | |
| import { Skeleton } from "@/components/ui/skeleton"; | |
| import { Slider } from "@/components/ui/slider"; | |
| import { Switch } from "@/components/ui/switch"; | |
| import { | |
| Table, | |
| TableBody, | |
| TableCaption, | |
| TableCell, | |
| TableHead, | |
| TableHeader, | |
| TableRow, | |
| } from "@/components/ui/table"; | |
| import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; | |
| import { Textarea } from "@/components/ui/textarea"; | |
| import { Toggle } from "@/components/ui/toggle"; | |
| import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"; | |
| import { | |
| Tooltip, | |
| TooltipContent, | |
| TooltipTrigger, | |
| } from "@/components/ui/tooltip"; | |
| import { useTheme } from "@/contexts/ThemeContext"; | |
| import { format } from "date-fns"; | |
| import { zhCN } from "date-fns/locale"; | |
| import { | |
| AlertCircle, | |
| CalendarIcon, | |
| Check, | |
| Clock, | |
| Moon, | |
| Sun, | |
| X, | |
| } from "lucide-react"; | |
| import { useState } from "react"; | |
| import { toast as sonnerToast } from "sonner"; | |
| import { AIChatBox, type Message } from "@/components/AIChatBox"; | |
| export default function ComponentsShowcase() { | |
| const { theme, toggleTheme } = useTheme(); | |
| const [date, setDate] = useState<Date | undefined>(new Date()); | |
| const [datePickerDate, setDatePickerDate] = useState<Date>(); | |
| const [selectedFruits, setSelectedFruits] = useState<string[]>([]); | |
| const [progress, setProgress] = useState(33); | |
| const [currentPage, setCurrentPage] = useState(2); | |
| const [openCombobox, setOpenCombobox] = useState(false); | |
| const [selectedFramework, setSelectedFramework] = useState(""); | |
| const [selectedMonth, setSelectedMonth] = useState(""); | |
| const [selectedYear, setSelectedYear] = useState(""); | |
| const [dialogInput, setDialogInput] = useState(""); | |
| const [dialogOpen, setDialogOpen] = useState(false); | |
| // AI ChatBox demo state | |
| const [chatMessages, setChatMessages] = useState<Message[]>([ | |
| { role: "system", content: "You are a helpful assistant." }, | |
| ]); | |
| const [isChatLoading, setIsChatLoading] = useState(false); | |
| const handleDialogSubmit = () => { | |
| console.log("Dialog submitted with value:", dialogInput); | |
| sonnerToast.success("Submitted successfully", { | |
| description: `Input: ${dialogInput}`, | |
| }); | |
| setDialogInput(""); | |
| setDialogOpen(false); | |
| }; | |
| const handleDialogKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => { | |
| if (e.key === "Enter" && !e.nativeEvent.isComposing) { | |
| e.preventDefault(); | |
| handleDialogSubmit(); | |
| } | |
| }; | |
| const handleChatSend = (content: string) => { | |
| // Add user message | |
| const newMessages: Message[] = [...chatMessages, { role: "user", content }]; | |
| setChatMessages(newMessages); | |
| // Simulate AI response with delay | |
| setIsChatLoading(true); | |
| setTimeout(() => { | |
| const aiResponse: Message = { | |
| role: "assistant", | |
| content: `This is a **demo response**. In a real app, you would call a tRPC mutation here:\n\n\`\`\`typescript\nconst chatMutation = trpc.ai.chat.useMutation({\n onSuccess: (response) => {\n setChatMessages(prev => [...prev, {\n role: "assistant",\n content: response.choices[0].message.content\n }]);\n }\n});\n\nchatMutation.mutate({ messages: newMessages });\n\`\`\`\n\nYour message was: "${content}"`, | |
| }; | |
| setChatMessages([...newMessages, aiResponse]); | |
| setIsChatLoading(false); | |
| }, 1500); | |
| }; | |
| return ( | |
| <div className="min-h-screen bg-background text-foreground"> | |
| <main className="container max-w-6xl mx-auto"> | |
| <div className="space-y-2 justify-between flex"> | |
| <h2 className="text-3xl font-bold tracking-tight mb-6"> | |
| Shadcn/ui Component Library | |
| </h2> | |
| <Button variant="outline" size="icon" onClick={toggleTheme}> | |
| {theme === "light" ? ( | |
| <Moon className="h-5 w-5" /> | |
| ) : ( | |
| <Sun className="h-5 w-5" /> | |
| )} | |
| </Button> | |
| </div> | |
| <div className="space-y-12"> | |
| {/* Text Colors Section */} | |
| <section className="space-y-4"> | |
| <h3 className="text-2xl font-semibold">Text Colors</h3> | |
| <Card> | |
| <CardContent className="pt-6"> | |
| <div className="grid grid-cols-1 md:grid-cols-2 gap-6"> | |
| <div className="space-y-3"> | |
| <div> | |
| <p className="text-sm text-muted-foreground mb-1"> | |
| Foreground (Default) | |
| </p> | |
| <p className="text-foreground text-lg"> | |
| Default text color for main content | |
| </p> | |
| </div> | |
| <div> | |
| <p className="text-sm text-muted-foreground mb-1"> | |
| Muted Foreground | |
| </p> | |
| <p className="text-muted-foreground text-lg"> | |
| Muted text for secondary information | |
| </p> | |
| </div> | |
| <div> | |
| <p className="text-sm text-muted-foreground mb-1"> | |
| Primary | |
| </p> | |
| <p className="text-primary text-lg font-medium"> | |
| Primary brand color text | |
| </p> | |
| </div> | |
| <div> | |
| <p className="text-sm text-muted-foreground mb-1"> | |
| Secondary Foreground | |
| </p> | |
| <p className="text-secondary-foreground text-lg"> | |
| Secondary action text color | |
| </p> | |
| </div> | |
| </div> | |
| <div className="space-y-3"> | |
| <div> | |
| <p className="text-sm text-muted-foreground mb-1"> | |
| Accent Foreground | |
| </p> | |
| <p className="text-accent-foreground text-lg"> | |
| Accent text for emphasis | |
| </p> | |
| </div> | |
| <div> | |
| <p className="text-sm text-muted-foreground mb-1"> | |
| Destructive | |
| </p> | |
| <p className="text-destructive text-lg font-medium"> | |
| Error or destructive action text | |
| </p> | |
| </div> | |
| <div> | |
| <p className="text-sm text-muted-foreground mb-1"> | |
| Card Foreground | |
| </p> | |
| <p className="text-card-foreground text-lg"> | |
| Text color on card backgrounds | |
| </p> | |
| </div> | |
| <div> | |
| <p className="text-sm text-muted-foreground mb-1"> | |
| Popover Foreground | |
| </p> | |
| <p className="text-popover-foreground text-lg"> | |
| Text color in popovers | |
| </p> | |
| </div> | |
| </div> | |
| </div> | |
| </CardContent> | |
| </Card> | |
| </section> | |
| {/* Color Combinations Section */} | |
| <section className="space-y-4"> | |
| <h3 className="text-2xl font-semibold">Color Combinations</h3> | |
| <Card> | |
| <CardContent className="pt-6"> | |
| <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> | |
| <div className="bg-primary text-primary-foreground rounded-lg p-4"> | |
| <p className="font-medium mb-1">Primary</p> | |
| <p className="text-sm opacity-90"> | |
| Primary background with foreground text | |
| </p> | |
| </div> | |
| <div className="bg-secondary text-secondary-foreground rounded-lg p-4"> | |
| <p className="font-medium mb-1">Secondary</p> | |
| <p className="text-sm opacity-90"> | |
| Secondary background with foreground text | |
| </p> | |
| </div> | |
| <div className="bg-muted text-muted-foreground rounded-lg p-4"> | |
| <p className="font-medium mb-1">Muted</p> | |
| <p className="text-sm opacity-90"> | |
| Muted background with foreground text | |
| </p> | |
| </div> | |
| <div className="bg-accent text-accent-foreground rounded-lg p-4"> | |
| <p className="font-medium mb-1">Accent</p> | |
| <p className="text-sm opacity-90"> | |
| Accent background with foreground text | |
| </p> | |
| </div> | |
| <div className="bg-destructive text-destructive-foreground rounded-lg p-4"> | |
| <p className="font-medium mb-1">Destructive</p> | |
| <p className="text-sm opacity-90"> | |
| Destructive background with foreground text | |
| </p> | |
| </div> | |
| <div className="bg-card text-card-foreground rounded-lg p-4 border"> | |
| <p className="font-medium mb-1">Card</p> | |
| <p className="text-sm opacity-90"> | |
| Card background with foreground text | |
| </p> | |
| </div> | |
| <div className="bg-popover text-popover-foreground rounded-lg p-4 border"> | |
| <p className="font-medium mb-1">Popover</p> | |
| <p className="text-sm opacity-90"> | |
| Popover background with foreground text | |
| </p> | |
| </div> | |
| <div className="bg-background text-foreground rounded-lg p-4 border"> | |
| <p className="font-medium mb-1">Background</p> | |
| <p className="text-sm opacity-90"> | |
| Default background with foreground text | |
| </p> | |
| </div> | |
| </div> | |
| </CardContent> | |
| </Card> | |
| </section> | |
| {/* Buttons Section */} | |
| <section className="space-y-4"> | |
| <h3 className="text-2xl font-semibold">Buttons</h3> | |
| <Card> | |
| <CardContent className="pt-6"> | |
| <div className="flex flex-wrap gap-4"> | |
| <Button>Default</Button> | |
| <Button variant="secondary">Secondary</Button> | |
| <Button variant="destructive">Destructive</Button> | |
| <Button variant="outline">Outline</Button> | |
| <Button variant="ghost">Ghost</Button> | |
| <Button variant="link">Link</Button> | |
| <Button size="sm">Small</Button> | |
| <Button size="lg">Large</Button> | |
| <Button size="icon"> | |
| <Check className="h-4 w-4" /> | |
| </Button> | |
| </div> | |
| </CardContent> | |
| </Card> | |
| </section> | |
| {/* Form Inputs Section */} | |
| <section className="space-y-4"> | |
| <h3 className="text-2xl font-semibold">Form Inputs</h3> | |
| <Card> | |
| <CardContent className="pt-6 space-y-6"> | |
| <div className="space-y-2"> | |
| <Label htmlFor="email">Email</Label> | |
| <Input id="email" type="email" placeholder="Email" /> | |
| </div> | |
| <div className="space-y-2"> | |
| <Label htmlFor="message">Message</Label> | |
| <Textarea | |
| id="message" | |
| placeholder="Type your message here." | |
| /> | |
| </div> | |
| <div className="space-y-2"> | |
| <Label>Select</Label> | |
| <Select> | |
| <SelectTrigger> | |
| <SelectValue placeholder="Select a fruit" /> | |
| </SelectTrigger> | |
| <SelectContent> | |
| <SelectItem value="apple">Apple</SelectItem> | |
| <SelectItem value="banana">Banana</SelectItem> | |
| <SelectItem value="orange">Orange</SelectItem> | |
| </SelectContent> | |
| </Select> | |
| </div> | |
| <div className="flex items-center space-x-2"> | |
| <Checkbox id="terms" /> | |
| <Label htmlFor="terms">Accept terms and conditions</Label> | |
| </div> | |
| <div className="flex items-center space-x-2"> | |
| <Switch id="airplane-mode" /> | |
| <Label htmlFor="airplane-mode">Airplane Mode</Label> | |
| </div> | |
| <div className="space-y-2"> | |
| <Label>Radio Group</Label> | |
| <RadioGroup defaultValue="option-one"> | |
| <div className="flex items-center space-x-2"> | |
| <RadioGroupItem value="option-one" id="option-one" /> | |
| <Label htmlFor="option-one">Option One</Label> | |
| </div> | |
| <div className="flex items-center space-x-2"> | |
| <RadioGroupItem value="option-two" id="option-two" /> | |
| <Label htmlFor="option-two">Option Two</Label> | |
| </div> | |
| </RadioGroup> | |
| </div> | |
| <div className="space-y-2"> | |
| <Label>Slider</Label> | |
| <Slider defaultValue={[50]} max={100} step={1} /> | |
| </div> | |
| <div className="space-y-2"> | |
| <Label>Input OTP</Label> | |
| <InputOTP maxLength={6}> | |
| <InputOTPGroup> | |
| <InputOTPSlot index={0} /> | |
| <InputOTPSlot index={1} /> | |
| <InputOTPSlot index={2} /> | |
| <InputOTPSlot index={3} /> | |
| <InputOTPSlot index={4} /> | |
| <InputOTPSlot index={5} /> | |
| </InputOTPGroup> | |
| </InputOTP> | |
| </div> | |
| <div className="space-y-2"> | |
| <Label>Date Time Picker</Label> | |
| <Popover> | |
| <PopoverTrigger asChild> | |
| <Button | |
| variant="outline" | |
| className={`w-full justify-start text-left font-normal ${ | |
| !datePickerDate && "text-muted-foreground" | |
| }`} | |
| > | |
| <CalendarIcon className="mr-2 h-4 w-4" /> | |
| {datePickerDate ? ( | |
| format(datePickerDate, "PPP HH:mm", { locale: zhCN }) | |
| ) : ( | |
| <span>Select date and time</span> | |
| )} | |
| </Button> | |
| </PopoverTrigger> | |
| <PopoverContent className="w-auto p-0" align="start"> | |
| <div className="p-3 space-y-3"> | |
| <Calendar | |
| mode="single" | |
| selected={datePickerDate} | |
| onSelect={setDatePickerDate} | |
| /> | |
| <div className="border-t pt-3 space-y-2"> | |
| <Label className="flex items-center gap-2"> | |
| <Clock className="h-4 w-4" /> | |
| Time | |
| </Label> | |
| <div className="flex gap-2"> | |
| <Input | |
| type="time" | |
| value={ | |
| datePickerDate | |
| ? format(datePickerDate, "HH:mm") | |
| : "00:00" | |
| } | |
| onChange={e => { | |
| const [hours, minutes] = | |
| e.target.value.split(":"); | |
| const newDate = datePickerDate | |
| ? new Date(datePickerDate) | |
| : new Date(); | |
| newDate.setHours(parseInt(hours)); | |
| newDate.setMinutes(parseInt(minutes)); | |
| setDatePickerDate(newDate); | |
| }} | |
| /> | |
| </div> | |
| </div> | |
| </div> | |
| </PopoverContent> | |
| </Popover> | |
| {datePickerDate && ( | |
| <p className="text-sm text-muted-foreground"> | |
| Selected:{" "} | |
| {format(datePickerDate, "yyyy/MM/dd HH:mm", { | |
| locale: zhCN, | |
| })} | |
| </p> | |
| )} | |
| </div> | |
| <div className="space-y-2"> | |
| <Label>Searchable Dropdown</Label> | |
| <Popover open={openCombobox} onOpenChange={setOpenCombobox}> | |
| <PopoverTrigger asChild> | |
| <Button | |
| variant="outline" | |
| role="combobox" | |
| aria-expanded={openCombobox} | |
| className="w-full justify-between" | |
| > | |
| {selectedFramework | |
| ? [ | |
| { value: "react", label: "React" }, | |
| { value: "vue", label: "Vue" }, | |
| { value: "angular", label: "Angular" }, | |
| { value: "svelte", label: "Svelte" }, | |
| { value: "nextjs", label: "Next.js" }, | |
| { value: "nuxt", label: "Nuxt" }, | |
| { value: "remix", label: "Remix" }, | |
| ].find(fw => fw.value === selectedFramework)?.label | |
| : "Select framework..."} | |
| <CalendarIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" /> | |
| </Button> | |
| </PopoverTrigger> | |
| <PopoverContent className="w-full p-0"> | |
| <Command> | |
| <CommandInput placeholder="Search frameworks..." /> | |
| <CommandList> | |
| <CommandEmpty>No framework found</CommandEmpty> | |
| <CommandGroup> | |
| {[ | |
| { value: "react", label: "React" }, | |
| { value: "vue", label: "Vue" }, | |
| { value: "angular", label: "Angular" }, | |
| { value: "svelte", label: "Svelte" }, | |
| { value: "nextjs", label: "Next.js" }, | |
| { value: "nuxt", label: "Nuxt" }, | |
| { value: "remix", label: "Remix" }, | |
| ].map(framework => ( | |
| <CommandItem | |
| key={framework.value} | |
| value={framework.value} | |
| onSelect={currentValue => { | |
| setSelectedFramework( | |
| currentValue === selectedFramework | |
| ? "" | |
| : currentValue | |
| ); | |
| setOpenCombobox(false); | |
| }} | |
| > | |
| <Check | |
| className={`mr-2 h-4 w-4 ${ | |
| selectedFramework === framework.value | |
| ? "opacity-100" | |
| : "opacity-0" | |
| }`} | |
| /> | |
| {framework.label} | |
| </CommandItem> | |
| ))} | |
| </CommandGroup> | |
| </CommandList> | |
| </Command> | |
| </PopoverContent> | |
| </Popover> | |
| {selectedFramework && ( | |
| <p className="text-sm text-muted-foreground"> | |
| Selected:{" "} | |
| { | |
| [ | |
| { value: "react", label: "React" }, | |
| { value: "vue", label: "Vue" }, | |
| { value: "angular", label: "Angular" }, | |
| { value: "svelte", label: "Svelte" }, | |
| { value: "nextjs", label: "Next.js" }, | |
| { value: "nuxt", label: "Nuxt" }, | |
| { value: "remix", label: "Remix" }, | |
| ].find(fw => fw.value === selectedFramework)?.label | |
| } | |
| </p> | |
| )} | |
| </div> | |
| <div className="space-y-2"> | |
| <div className="grid grid-cols-2 gap-4"> | |
| <div className="space-y-2"> | |
| <Label htmlFor="month" className="text-sm font-medium"> | |
| Month | |
| </Label> | |
| <Select | |
| value={selectedMonth} | |
| onValueChange={setSelectedMonth} | |
| > | |
| <SelectTrigger id="month"> | |
| <SelectValue placeholder="MM" /> | |
| </SelectTrigger> | |
| <SelectContent> | |
| {Array.from({ length: 12 }, (_, i) => i + 1).map( | |
| month => ( | |
| <SelectItem | |
| key={month} | |
| value={month.toString().padStart(2, "0")} | |
| > | |
| {month.toString().padStart(2, "0")} | |
| </SelectItem> | |
| ) | |
| )} | |
| </SelectContent> | |
| </Select> | |
| </div> | |
| <div className="space-y-2"> | |
| <Label htmlFor="year" className="text-sm font-medium"> | |
| Year | |
| </Label> | |
| <Select | |
| value={selectedYear} | |
| onValueChange={setSelectedYear} | |
| > | |
| <SelectTrigger id="year"> | |
| <SelectValue placeholder="YYYY" /> | |
| </SelectTrigger> | |
| <SelectContent> | |
| {Array.from( | |
| { length: 10 }, | |
| (_, i) => new Date().getFullYear() - 5 + i | |
| ).map(year => ( | |
| <SelectItem key={year} value={year.toString()}> | |
| {year} | |
| </SelectItem> | |
| ))} | |
| </SelectContent> | |
| </Select> | |
| </div> | |
| </div> | |
| {selectedMonth && selectedYear && ( | |
| <p className="text-sm text-muted-foreground"> | |
| Selected: {selectedYear}/{selectedMonth}/ | |
| </p> | |
| )} | |
| </div> | |
| </CardContent> | |
| </Card> | |
| </section> | |
| {/* Data Display Section */} | |
| <section className="space-y-4"> | |
| <h3 className="text-2xl font-semibold">Data Display</h3> | |
| <Card> | |
| <CardContent className="pt-6 space-y-6"> | |
| <div className="space-y-2"> | |
| <Label>Badges</Label> | |
| <div className="flex flex-wrap gap-2"> | |
| <Badge>Default</Badge> | |
| <Badge variant="secondary">Secondary</Badge> | |
| <Badge variant="destructive">Destructive</Badge> | |
| <Badge variant="outline">Outline</Badge> | |
| </div> | |
| </div> | |
| <Separator /> | |
| <div className="space-y-2"> | |
| <Label>Avatar</Label> | |
| <div className="flex gap-4"> | |
| <Avatar> | |
| <AvatarImage src="https://github.com/shadcn.png" /> | |
| <AvatarFallback>CN</AvatarFallback> | |
| </Avatar> | |
| <Avatar> | |
| <AvatarFallback>AB</AvatarFallback> | |
| </Avatar> | |
| </div> | |
| </div> | |
| <Separator /> | |
| <div className="space-y-2"> | |
| <Label>Progress</Label> | |
| <Progress value={progress} /> | |
| <div className="flex gap-2"> | |
| <Button | |
| size="sm" | |
| onClick={() => setProgress(Math.max(0, progress - 10))} | |
| > | |
| -10 | |
| </Button> | |
| <Button | |
| size="sm" | |
| onClick={() => setProgress(Math.min(100, progress + 10))} | |
| > | |
| +10 | |
| </Button> | |
| </div> | |
| </div> | |
| <Separator /> | |
| <div className="space-y-2"> | |
| <Label>Skeleton</Label> | |
| <div className="space-y-2"> | |
| <Skeleton className="h-4 w-full" /> | |
| <Skeleton className="h-4 w-3/4" /> | |
| <Skeleton className="h-4 w-1/2" /> | |
| </div> | |
| </div> | |
| <Separator /> | |
| <div className="space-y-2"> | |
| <Label>Pagination</Label> | |
| <Pagination> | |
| <PaginationContent> | |
| <PaginationItem> | |
| <PaginationPrevious | |
| href="#" | |
| onClick={e => { | |
| e.preventDefault(); | |
| setCurrentPage(Math.max(1, currentPage - 1)); | |
| }} | |
| /> | |
| </PaginationItem> | |
| {[1, 2, 3, 4, 5].map(page => ( | |
| <PaginationItem key={page}> | |
| <PaginationLink | |
| href="#" | |
| isActive={currentPage === page} | |
| onClick={e => { | |
| e.preventDefault(); | |
| setCurrentPage(page); | |
| }} | |
| > | |
| {page} | |
| </PaginationLink> | |
| </PaginationItem> | |
| ))} | |
| <PaginationItem> | |
| <PaginationNext | |
| href="#" | |
| onClick={e => { | |
| e.preventDefault(); | |
| setCurrentPage(Math.min(5, currentPage + 1)); | |
| }} | |
| /> | |
| </PaginationItem> | |
| </PaginationContent> | |
| </Pagination> | |
| <p className="text-sm text-muted-foreground text-center"> | |
| Current page: {currentPage} | |
| </p> | |
| </div> | |
| <Separator /> | |
| <div className="space-y-2"> | |
| <Label>Table</Label> | |
| <Table> | |
| <TableCaption>A list of your recent invoices.</TableCaption> | |
| <TableHeader> | |
| <TableRow> | |
| <TableHead className="w-[100px]">Invoice</TableHead> | |
| <TableHead>Status</TableHead> | |
| <TableHead>Method</TableHead> | |
| <TableHead className="text-right">Amount</TableHead> | |
| </TableRow> | |
| </TableHeader> | |
| <TableBody> | |
| <TableRow> | |
| <TableCell className="font-medium">INV001</TableCell> | |
| <TableCell>Paid</TableCell> | |
| <TableCell>Credit Card</TableCell> | |
| <TableCell className="text-right">$250.00</TableCell> | |
| </TableRow> | |
| <TableRow> | |
| <TableCell className="font-medium">INV002</TableCell> | |
| <TableCell>Pending</TableCell> | |
| <TableCell>PayPal</TableCell> | |
| <TableCell className="text-right">$150.00</TableCell> | |
| </TableRow> | |
| <TableRow> | |
| <TableCell className="font-medium">INV003</TableCell> | |
| <TableCell>Unpaid</TableCell> | |
| <TableCell>Bank Transfer</TableCell> | |
| <TableCell className="text-right">$350.00</TableCell> | |
| </TableRow> | |
| </TableBody> | |
| </Table> | |
| </div> | |
| <Separator /> | |
| <div className="space-y-2"> | |
| <Label>Menubar</Label> | |
| <Menubar> | |
| <MenubarMenu> | |
| <MenubarTrigger>File</MenubarTrigger> | |
| <MenubarContent> | |
| <MenubarItem>New Tab</MenubarItem> | |
| <MenubarItem>New Window</MenubarItem> | |
| <MenubarSeparator /> | |
| <MenubarItem>Share</MenubarItem> | |
| <MenubarSeparator /> | |
| <MenubarItem>Print</MenubarItem> | |
| </MenubarContent> | |
| </MenubarMenu> | |
| <MenubarMenu> | |
| <MenubarTrigger>Edit</MenubarTrigger> | |
| <MenubarContent> | |
| <MenubarItem>Undo</MenubarItem> | |
| <MenubarItem>Redo</MenubarItem> | |
| </MenubarContent> | |
| </MenubarMenu> | |
| <MenubarMenu> | |
| <MenubarTrigger>View</MenubarTrigger> | |
| <MenubarContent> | |
| <MenubarItem>Reload</MenubarItem> | |
| <MenubarItem>Force Reload</MenubarItem> | |
| </MenubarContent> | |
| </MenubarMenu> | |
| </Menubar> | |
| </div> | |
| <Separator /> | |
| <div className="space-y-2"> | |
| <Label>Breadcrumb</Label> | |
| <Breadcrumb> | |
| <BreadcrumbList> | |
| <BreadcrumbItem> | |
| <BreadcrumbLink href="/">Home</BreadcrumbLink> | |
| </BreadcrumbItem> | |
| <BreadcrumbSeparator /> | |
| <BreadcrumbItem> | |
| <BreadcrumbLink href="/components"> | |
| Components | |
| </BreadcrumbLink> | |
| </BreadcrumbItem> | |
| <BreadcrumbSeparator /> | |
| <BreadcrumbItem> | |
| <BreadcrumbPage>Breadcrumb</BreadcrumbPage> | |
| </BreadcrumbItem> | |
| </BreadcrumbList> | |
| </Breadcrumb> | |
| </div> | |
| </CardContent> | |
| </Card> | |
| </section> | |
| {/* Alerts Section */} | |
| <section className="space-y-4"> | |
| <h3 className="text-2xl font-semibold">Alerts</h3> | |
| <div className="space-y-4"> | |
| <Alert> | |
| <AlertCircle className="h-4 w-4" /> | |
| <AlertTitle>Heads up!</AlertTitle> | |
| <AlertDescription> | |
| You can add components to your app using the cli. | |
| </AlertDescription> | |
| </Alert> | |
| <Alert variant="destructive"> | |
| <X className="h-4 w-4" /> | |
| <AlertTitle>Error</AlertTitle> | |
| <AlertDescription> | |
| Your session has expired. Please log in again. | |
| </AlertDescription> | |
| </Alert> | |
| </div> | |
| </section> | |
| {/* Tabs Section */} | |
| <section className="space-y-4"> | |
| <h3 className="text-2xl font-semibold">Tabs</h3> | |
| <Tabs defaultValue="account" className="w-full"> | |
| <TabsList className="grid w-full grid-cols-3"> | |
| <TabsTrigger value="account">Account</TabsTrigger> | |
| <TabsTrigger value="password">Password</TabsTrigger> | |
| <TabsTrigger value="settings">Settings</TabsTrigger> | |
| </TabsList> | |
| <TabsContent value="account"> | |
| <Card> | |
| <CardHeader> | |
| <CardTitle>Account</CardTitle> | |
| <CardDescription> | |
| Make changes to your account here. | |
| </CardDescription> | |
| </CardHeader> | |
| <CardContent className="space-y-2"> | |
| <div className="space-y-1"> | |
| <Label htmlFor="name">Name</Label> | |
| <Input id="name" defaultValue="Pedro Duarte" /> | |
| </div> | |
| </CardContent> | |
| <CardFooter> | |
| <Button>Save changes</Button> | |
| </CardFooter> | |
| </Card> | |
| </TabsContent> | |
| <TabsContent value="password"> | |
| <Card> | |
| <CardHeader> | |
| <CardTitle>Password</CardTitle> | |
| <CardDescription> | |
| Change your password here. | |
| </CardDescription> | |
| </CardHeader> | |
| <CardContent className="space-y-2"> | |
| <div className="space-y-1"> | |
| <Label htmlFor="current">Current password</Label> | |
| <Input id="current" type="password" /> | |
| </div> | |
| <div className="space-y-1"> | |
| <Label htmlFor="new">New password</Label> | |
| <Input id="new" type="password" /> | |
| </div> | |
| </CardContent> | |
| <CardFooter> | |
| <Button>Save password</Button> | |
| </CardFooter> | |
| </Card> | |
| </TabsContent> | |
| <TabsContent value="settings"> | |
| <Card> | |
| <CardHeader> | |
| <CardTitle>Settings</CardTitle> | |
| <CardDescription> | |
| Manage your settings here. | |
| </CardDescription> | |
| </CardHeader> | |
| <CardContent> | |
| <p className="text-sm text-muted-foreground"> | |
| Settings content goes here. | |
| </p> | |
| </CardContent> | |
| </Card> | |
| </TabsContent> | |
| </Tabs> | |
| </section> | |
| {/* Accordion Section */} | |
| <section className="space-y-4"> | |
| <h3 className="text-2xl font-semibold">Accordion</h3> | |
| <Accordion type="single" collapsible className="w-full"> | |
| <AccordionItem value="item-1"> | |
| <AccordionTrigger>Is it accessible?</AccordionTrigger> | |
| <AccordionContent> | |
| Yes. It adheres to the WAI-ARIA design pattern. | |
| </AccordionContent> | |
| </AccordionItem> | |
| <AccordionItem value="item-2"> | |
| <AccordionTrigger>Is it styled?</AccordionTrigger> | |
| <AccordionContent> | |
| Yes. It comes with default styles that matches the other | |
| components' aesthetic. | |
| </AccordionContent> | |
| </AccordionItem> | |
| <AccordionItem value="item-3"> | |
| <AccordionTrigger>Is it animated?</AccordionTrigger> | |
| <AccordionContent> | |
| Yes. It's animated by default, but you can disable it if you | |
| prefer. | |
| </AccordionContent> | |
| </AccordionItem> | |
| </Accordion> | |
| </section> | |
| {/* Collapsible Section */} | |
| <section className="space-y-4"> | |
| <h3 className="text-2xl font-semibold">Collapsible</h3> | |
| <Collapsible> | |
| <Card> | |
| <CardHeader> | |
| <CollapsibleTrigger asChild> | |
| <Button variant="ghost" className="w-full justify-between"> | |
| <CardTitle>@peduarte starred 3 repositories</CardTitle> | |
| </Button> | |
| </CollapsibleTrigger> | |
| </CardHeader> | |
| <CollapsibleContent> | |
| <CardContent> | |
| <div className="space-y-2"> | |
| <div className="rounded-md border px-4 py-3 font-mono text-sm"> | |
| @radix-ui/primitives | |
| </div> | |
| <div className="rounded-md border px-4 py-3 font-mono text-sm"> | |
| @radix-ui/colors | |
| </div> | |
| <div className="rounded-md border px-4 py-3 font-mono text-sm"> | |
| @stitches/react | |
| </div> | |
| </div> | |
| </CardContent> | |
| </CollapsibleContent> | |
| </Card> | |
| </Collapsible> | |
| </section> | |
| {/* Dialog, Sheet, Drawer Section */} | |
| <section className="space-y-4"> | |
| <h3 className="text-2xl font-semibold">Overlays</h3> | |
| <Card> | |
| <CardContent className="pt-6"> | |
| <div className="flex flex-wrap gap-4"> | |
| <Dialog open={dialogOpen} onOpenChange={setDialogOpen}> | |
| <DialogTrigger asChild> | |
| <Button variant="outline">Open Dialog</Button> | |
| </DialogTrigger> | |
| <DialogContent> | |
| <DialogHeader> | |
| <DialogTitle>Test Input</DialogTitle> | |
| <DialogDescription> | |
| Enter some text below. Press Enter to submit (IME composition supported). | |
| </DialogDescription> | |
| </DialogHeader> | |
| <div className="space-y-4 py-4"> | |
| <div className="space-y-2"> | |
| <Label htmlFor="dialog-input">Input</Label> | |
| <Input | |
| id="dialog-input" | |
| placeholder="Type something..." | |
| value={dialogInput} | |
| onChange={(e) => setDialogInput(e.target.value)} | |
| onKeyDown={handleDialogKeyDown} | |
| autoFocus | |
| /> | |
| </div> | |
| </div> | |
| <div className="flex justify-end gap-2"> | |
| <Button | |
| variant="outline" | |
| onClick={() => setDialogOpen(false)} | |
| > | |
| Cancel | |
| </Button> | |
| <Button onClick={handleDialogSubmit}>Submit</Button> | |
| </div> | |
| </DialogContent> | |
| </Dialog> | |
| <Sheet> | |
| <SheetTrigger asChild> | |
| <Button variant="outline">Open Sheet</Button> | |
| </SheetTrigger> | |
| <SheetContent> | |
| <SheetHeader> | |
| <SheetTitle>Edit profile</SheetTitle> | |
| <SheetDescription> | |
| Make changes to your profile here. Click save when | |
| you're done. | |
| </SheetDescription> | |
| </SheetHeader> | |
| </SheetContent> | |
| </Sheet> | |
| <Drawer> | |
| <DrawerTrigger asChild> | |
| <Button variant="outline">Open Drawer</Button> | |
| </DrawerTrigger> | |
| <DrawerContent> | |
| <DrawerHeader> | |
| <DrawerTitle>Are you absolutely sure?</DrawerTitle> | |
| <DrawerDescription> | |
| This action cannot be undone. | |
| </DrawerDescription> | |
| </DrawerHeader> | |
| <DrawerFooter> | |
| <Button>Submit</Button> | |
| <DrawerClose asChild> | |
| <Button variant="outline">Cancel</Button> | |
| </DrawerClose> | |
| </DrawerFooter> | |
| </DrawerContent> | |
| </Drawer> | |
| <Popover> | |
| <PopoverTrigger asChild> | |
| <Button variant="outline">Open Popover</Button> | |
| </PopoverTrigger> | |
| <PopoverContent> | |
| <div className="space-y-2"> | |
| <h4 className="font-medium leading-none">Dimensions</h4> | |
| <p className="text-sm text-muted-foreground"> | |
| Set the dimensions for the layer. | |
| </p> | |
| </div> | |
| </PopoverContent> | |
| </Popover> | |
| <Tooltip> | |
| <TooltipTrigger asChild> | |
| <Button variant="outline">Hover me</Button> | |
| </TooltipTrigger> | |
| <TooltipContent> | |
| <p>Add to library</p> | |
| </TooltipContent> | |
| </Tooltip> | |
| </div> | |
| </CardContent> | |
| </Card> | |
| </section> | |
| {/* Menus Section */} | |
| <section className="space-y-4"> | |
| <h3 className="text-2xl font-semibold">Menus</h3> | |
| <Card> | |
| <CardContent className="pt-6"> | |
| <div className="flex flex-wrap gap-4"> | |
| <DropdownMenu> | |
| <DropdownMenuTrigger asChild> | |
| <Button variant="outline">Dropdown Menu</Button> | |
| </DropdownMenuTrigger> | |
| <DropdownMenuContent> | |
| <DropdownMenuLabel>My Account</DropdownMenuLabel> | |
| <DropdownMenuSeparator /> | |
| <DropdownMenuItem>Profile</DropdownMenuItem> | |
| <DropdownMenuItem>Billing</DropdownMenuItem> | |
| <DropdownMenuItem>Team</DropdownMenuItem> | |
| <DropdownMenuItem>Subscription</DropdownMenuItem> | |
| </DropdownMenuContent> | |
| </DropdownMenu> | |
| <ContextMenu> | |
| <ContextMenuTrigger asChild> | |
| <Button variant="outline">Right Click Me</Button> | |
| </ContextMenuTrigger> | |
| <ContextMenuContent> | |
| <ContextMenuItem>Profile</ContextMenuItem> | |
| <ContextMenuItem>Billing</ContextMenuItem> | |
| <ContextMenuItem>Team</ContextMenuItem> | |
| <ContextMenuItem>Subscription</ContextMenuItem> | |
| </ContextMenuContent> | |
| </ContextMenu> | |
| <HoverCard> | |
| <HoverCardTrigger asChild> | |
| <Button variant="outline">Hover Card</Button> | |
| </HoverCardTrigger> | |
| <HoverCardContent> | |
| <div className="space-y-2"> | |
| <h4 className="text-sm font-semibold">@nextjs</h4> | |
| <p className="text-sm"> | |
| The React Framework – created and maintained by | |
| @vercel. | |
| </p> | |
| </div> | |
| </HoverCardContent> | |
| </HoverCard> | |
| </div> | |
| </CardContent> | |
| </Card> | |
| </section> | |
| {/* Calendar Section */} | |
| <section className="space-y-4"> | |
| <h3 className="text-2xl font-semibold">Calendar</h3> | |
| <Card> | |
| <CardContent className="pt-6 flex justify-center"> | |
| <Calendar | |
| mode="single" | |
| selected={date} | |
| onSelect={setDate} | |
| className="rounded-md border" | |
| /> | |
| </CardContent> | |
| </Card> | |
| </section> | |
| {/* Carousel Section */} | |
| <section className="space-y-4"> | |
| <h3 className="text-2xl font-semibold">Carousel</h3> | |
| <Card> | |
| <CardContent className="pt-6"> | |
| <Carousel className="w-full max-w-xs mx-auto"> | |
| <CarouselContent> | |
| {Array.from({ length: 5 }).map((_, index) => ( | |
| <CarouselItem key={index}> | |
| <div className="p-1"> | |
| <Card> | |
| <CardContent className="flex aspect-square items-center justify-center p-6"> | |
| <span className="text-4xl font-semibold"> | |
| {index + 1} | |
| </span> | |
| </CardContent> | |
| </Card> | |
| </div> | |
| </CarouselItem> | |
| ))} | |
| </CarouselContent> | |
| <CarouselPrevious /> | |
| <CarouselNext /> | |
| </Carousel> | |
| </CardContent> | |
| </Card> | |
| </section> | |
| {/* Toggle Section */} | |
| <section className="space-y-4"> | |
| <h3 className="text-2xl font-semibold">Toggle</h3> | |
| <Card> | |
| <CardContent className="pt-6 space-y-4"> | |
| <div className="space-y-2"> | |
| <Label>Toggle</Label> | |
| <div className="flex gap-2"> | |
| <Toggle aria-label="Toggle italic"> | |
| <span className="font-bold">B</span> | |
| </Toggle> | |
| <Toggle aria-label="Toggle italic"> | |
| <span className="italic">I</span> | |
| </Toggle> | |
| <Toggle aria-label="Toggle underline"> | |
| <span className="underline">U</span> | |
| </Toggle> | |
| </div> | |
| </div> | |
| <Separator /> | |
| <div className="space-y-2"> | |
| <Label>Toggle Group</Label> | |
| <ToggleGroup type="multiple"> | |
| <ToggleGroupItem value="bold" aria-label="Toggle bold"> | |
| <span className="font-bold">B</span> | |
| </ToggleGroupItem> | |
| <ToggleGroupItem value="italic" aria-label="Toggle italic"> | |
| <span className="italic">I</span> | |
| </ToggleGroupItem> | |
| <ToggleGroupItem | |
| value="underline" | |
| aria-label="Toggle underline" | |
| > | |
| <span className="underline">U</span> | |
| </ToggleGroupItem> | |
| </ToggleGroup> | |
| </div> | |
| </CardContent> | |
| </Card> | |
| </section> | |
| {/* Aspect Ratio & Scroll Area Section */} | |
| <section className="space-y-4"> | |
| <h3 className="text-2xl font-semibold">Layout Components</h3> | |
| <Card> | |
| <CardContent className="pt-6 space-y-6"> | |
| <div className="space-y-2"> | |
| <Label>Aspect Ratio (16/9)</Label> | |
| <AspectRatio ratio={16 / 9} className="bg-muted"> | |
| <div className="flex h-full items-center justify-center"> | |
| <p className="text-muted-foreground">16:9 Aspect Ratio</p> | |
| </div> | |
| </AspectRatio> | |
| </div> | |
| <Separator /> | |
| <div className="space-y-2"> | |
| <Label>Scroll Area</Label> | |
| <ScrollArea className="h-[200px] w-full rounded-md border overflow-hidden"> | |
| <div className="p-4"> | |
| <div className="space-y-4"> | |
| {Array.from({ length: 20 }).map((_, i) => ( | |
| <div key={i} className="text-sm"> | |
| Item {i + 1}: This is a scrollable content area | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| </ScrollArea> | |
| </div> | |
| </CardContent> | |
| </Card> | |
| </section> | |
| {/* Resizable Section */} | |
| <section className="space-y-4"> | |
| <h3 className="text-2xl font-semibold">Resizable Panels</h3> | |
| <Card> | |
| <CardContent className="pt-6"> | |
| <ResizablePanelGroup | |
| direction="horizontal" | |
| className="min-h-[200px] rounded-lg border" | |
| > | |
| <ResizablePanel defaultSize={50}> | |
| <div className="flex h-full items-center justify-center p-6"> | |
| <span className="font-semibold">Panel One</span> | |
| </div> | |
| </ResizablePanel> | |
| <ResizableHandle /> | |
| <ResizablePanel defaultSize={50}> | |
| <div className="flex h-full items-center justify-center p-6"> | |
| <span className="font-semibold">Panel Two</span> | |
| </div> | |
| </ResizablePanel> | |
| </ResizablePanelGroup> | |
| </CardContent> | |
| </Card> | |
| </section> | |
| {/* Toast Section */} | |
| <section className="space-y-4"> | |
| <h3 className="text-2xl font-semibold">Toast</h3> | |
| <Card> | |
| <CardContent className="pt-6 space-y-4"> | |
| <div className="space-y-2"> | |
| <Label>Sonner Toast</Label> | |
| <div className="flex flex-wrap gap-2"> | |
| <Button | |
| variant="outline" | |
| onClick={() => { | |
| sonnerToast.success("Operation successful", { | |
| description: "Your changes have been saved", | |
| }); | |
| }} | |
| > | |
| Success | |
| </Button> | |
| <Button | |
| variant="outline" | |
| onClick={() => { | |
| sonnerToast.error("Operation failed", { | |
| description: | |
| "Cannot complete operation, please try again", | |
| }); | |
| }} | |
| > | |
| Error | |
| </Button> | |
| <Button | |
| variant="outline" | |
| onClick={() => { | |
| sonnerToast.info("Information", { | |
| description: "This is an information message", | |
| }); | |
| }} | |
| > | |
| Info | |
| </Button> | |
| <Button | |
| variant="outline" | |
| onClick={() => { | |
| sonnerToast.warning("Warning", { | |
| description: | |
| "Please note the impact of this operation", | |
| }); | |
| }} | |
| > | |
| Warning | |
| </Button> | |
| <Button | |
| variant="outline" | |
| onClick={() => { | |
| sonnerToast.loading("Loading", { | |
| description: "Please wait", | |
| }); | |
| }} | |
| > | |
| Loading | |
| </Button> | |
| <Button | |
| variant="outline" | |
| onClick={() => { | |
| const promise = new Promise(resolve => | |
| setTimeout(resolve, 2000) | |
| ); | |
| sonnerToast.promise(promise, { | |
| loading: "Processing...", | |
| success: "Processing complete!", | |
| error: "Processing failed", | |
| }); | |
| }} | |
| > | |
| Promise | |
| </Button> | |
| </div> | |
| </div> | |
| </CardContent> | |
| </Card> | |
| </section> | |
| {/* AI ChatBox Section */} | |
| <section className="space-y-4"> | |
| <h3 className="text-2xl font-semibold">AI ChatBox</h3> | |
| <Card> | |
| <CardContent className="pt-6"> | |
| <div className="space-y-4"> | |
| <div className="text-sm text-muted-foreground"> | |
| <p> | |
| A ready-to-use chat interface component that integrates with the LLM system. | |
| Features markdown rendering, auto-scrolling, and loading states. | |
| </p> | |
| <p className="mt-2"> | |
| This is a demo with simulated responses. In a real app, you'd connect it to a tRPC mutation. | |
| </p> | |
| </div> | |
| <AIChatBox | |
| messages={chatMessages} | |
| onSendMessage={handleChatSend} | |
| isLoading={isChatLoading} | |
| placeholder="Try sending a message..." | |
| height="500px" | |
| emptyStateMessage="How can I help you today?" | |
| suggestedPrompts={[ | |
| "What is React?", | |
| "Explain TypeScript", | |
| "How to use tRPC?", | |
| "Best practices for web development", | |
| ]} | |
| /> | |
| </div> | |
| </CardContent> | |
| </Card> | |
| </section> | |
| </div> | |
| </main> | |
| <footer className="border-t py-6 mt-12"> | |
| <div className="container text-center text-sm text-muted-foreground"> | |
| <p>Shadcn/ui Component Showcase</p> | |
| </div> | |
| </footer> | |
| </div> | |
| ); | |
| } | |