add theme switcher and inbox drawer to header layout admin panel

This commit is contained in:
vergiLgood1 2025-02-21 18:00:23 +07:00
parent 56caad6b13
commit 67ecc950c2
10 changed files with 638 additions and 88 deletions

View File

@ -17,23 +17,6 @@ import {
export default function CrimeOverview() {
return (
<>
<header className="flex h-16 shrink-0 items-center gap-2 transition-[width,height] ease-linear group-has-[[data-collapsible=icon]]/sidebar-wrapper:h-12">
<div className="flex items-center gap-2 px-4">
<SidebarTrigger className="-ml-1" />
<Separator orientation="vertical" className="mr-2 h-4" />
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem className="hidden md:block">
<BreadcrumbLink href="#">Sigap - v</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator className="hidden md:block" />
<BreadcrumbItem>
<BreadcrumbPage>Map</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</div>
</header>
<div className="flex flex-1 flex-col gap-4 p-4 pt-0">
<div className="min-h-[100vh] flex-1 rounded-xl bg-muted/50 md:min-h-min">
<MapboxMap />

View File

@ -7,13 +7,22 @@ import {
BreadcrumbPage,
BreadcrumbSeparator,
} from "@/components/ui/breadcrumb";
import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
import {
SidebarInset,
SidebarProvider,
SidebarTrigger,
} from "@/components/ui/sidebar";
import { redirect } from "next/navigation";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { MoreHorizontal, MoreVertical } from "lucide-react";
import { InboxDrawer } from "@/components/inbox-drawer";
import { ThemeSwitcher } from "@/components/theme-switcher";
export default async function Layout({
children,
@ -23,7 +32,43 @@ export default async function Layout({
return (
<SidebarProvider>
<AppSidebar />
<SidebarInset>{children}</SidebarInset>
<SidebarInset>
<header className="flex h-16 shrink-0 items-center justify-between px-4 transition-[width,height] ease-linear group-has-[[data-collapsible=icon]]/sidebar-wrapper:h-12">
<div className="flex items-center gap-2">
<SidebarTrigger className="-ml-1" />
<Separator orientation="vertical" className="mr-2 h-4" />
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem className="hidden md:block">
<BreadcrumbLink href="#">Sigap - v</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator className="hidden md:block" />
<BreadcrumbItem>
<BreadcrumbPage>Map</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</div>
<div className="flex items-center gap-2">
<InboxDrawer showTitle={true} />
<ThemeSwitcher showTitle={true} />
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon">
<MoreHorizontal className="h-5 w-5" />
<span className="sr-only">More options</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem>Settings</DropdownMenuItem>
<DropdownMenuItem>Help</DropdownMenuItem>
<DropdownMenuItem>About</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</header>
{children}
</SidebarInset>
</SidebarProvider>
);
}
}

View File

@ -0,0 +1,280 @@
"use client";
import * as React from "react";
import { Inbox, Search, ArrowLeft } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Sheet,
SheetContent,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "@/components/ui/sheet";
import { Input } from "@/components/ui/input";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Badge } from "@/components/ui/badge";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
interface InboxDrawerProps {
showTitle?: boolean;
showAvatar?: boolean;
}
interface MailMessage {
id: string;
name: string;
email: string;
subject: string;
teaser: string;
content: string;
date: string;
read: boolean;
}
const ICON_SIZE = 20;
// Sample data
const sampleMails: MailMessage[] = [
{
id: "1",
name: "William Smith",
email: "williamsmith@example.com",
subject: "Meeting Tomorrow",
teaser:
"Hi team, just a reminder about our meeting tomorrow at 10 AM. Please come prepared with your project updates.",
content:
"Hi team,\n\nJust a reminder about our meeting tomorrow at 10 AM. Please come prepared with your project updates. We'll be discussing the progress of Project X and setting goals for the next sprint.\n\nIf you have any questions or need to reschedule, please let me know as soon as possible.\n\nBest regards,\nWilliam",
date: "09:34 AM",
read: false,
},
{
id: "2",
name: "Alice Johnson",
email: "alicejohnson@example.com",
subject: "Re: Project Update",
teaser:
"Thanks for the update. The progress looks great so far. Let's schedule a call to discuss the next steps.",
content:
"Hi team,\n\nThanks for the update. The progress looks great so far. I'm impressed with the work you've done on the new feature.\n\nLet's schedule a call to discuss the next steps. How does tomorrow at 2 PM sound? We can go over any challenges you're facing and plan out the rest of the sprint.\n\nLooking forward to our discussion.\n\nBest,\nAlice",
date: "Yesterday",
read: true,
},
{
id: "3",
name: "Bob Davis",
email: "bobdavis@example.com",
subject: "Weekend Plans",
teaser:
"Hey everyone! I'm thinking of organizing a team outing this weekend. Would you be interested in a hiking trip or a beach day?",
content:
"Hey everyone!\n\nI hope this email finds you well. I'm thinking of organizing a team outing this weekend to boost morale and have some fun outside of work.\n\nWould you be interested in a hiking trip or a beach day? I'm open to other suggestions as well. Let me know your preferences, and we can plan accordingly.\n\nLooking forward to spending some quality time with the team!\n\nCheers,\nBob",
date: "2 days ago",
read: false,
},
{
id: "4",
name: "Charlie Brown",
email: "charliebrown@example.com",
subject: "Client Feedback",
teaser:
"The client has provided feedback on the recent deliverables. Please review and make the necessary adjustments.",
content:
"Hi team,\n\nThe client has provided feedback on the recent deliverables. Please review the attached document and make the necessary adjustments.\n\nLet's aim to have the revisions completed by the end of the week so we can send the updated version to the client.\n\nThanks,\nCharlie",
date: "3 days ago",
read: false,
},
{
id: "5",
name: "Diana Prince",
email: "dianaprince@example.com",
subject: "Team Lunch",
teaser:
"We're planning a team lunch this Friday at 1 PM. Please RSVP by tomorrow.",
content:
"Hello team,\n\nWe're planning a team lunch this Friday at 1 PM at the new Italian restaurant downtown. Please RSVP by tomorrow so we can make a reservation.\n\nLooking forward to seeing you all there!\n\nBest,\nDiana",
date: "4 days ago",
read: true,
},
];
const InboxDrawerComponent: React.FC<InboxDrawerProps> = ({
showTitle = false,
showAvatar = false,
}) => {
const [messages, setMessages] = React.useState<MailMessage[]>(sampleMails);
const [isOpen, setIsOpen] = React.useState(false);
const [searchTerm, setSearchTerm] = React.useState("");
const [selectedMessage, setSelectedMessage] =
React.useState<MailMessage | null>(null);
const unreadCount = React.useMemo(() => {
return messages.filter((message) => !message.read).length;
}, [messages]);
const filteredMessages = React.useMemo(() => {
return messages.filter(
(message) =>
message.subject.toLowerCase().includes(searchTerm.toLowerCase()) ||
message.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
message.teaser.toLowerCase().includes(searchTerm.toLowerCase())
);
}, [messages, searchTerm]);
const markAsRead = React.useCallback((id: string) => {
setMessages((prevMessages) =>
prevMessages.map((message) =>
message.id === id ? { ...message, read: true } : message
)
);
}, []);
const handleMessageClick = (message: MailMessage) => {
setSelectedMessage(message);
markAsRead(message.id);
};
const handleBackToList = () => {
setSelectedMessage(null);
};
return (
<Sheet open={isOpen} onOpenChange={setIsOpen}>
<SheetTrigger asChild>
<Button
variant="ghost"
size={showTitle ? "sm" : "icon"}
className={`relative ${showTitle ? "flex items-center" : ""}`}
aria-label="Open inbox"
>
<Inbox
className={`h-${ICON_SIZE} w-${ICON_SIZE} text-muted-foreground`}
/>
{showTitle && (
<span className="text-muted-foreground font-medium">Inbox</span>
)}
{unreadCount > 0 && (
<Badge
variant="destructive"
className="absolute -top-2 -right-2 px-1.5 py-0.5 text-xs"
>
{unreadCount}
</Badge>
)}
</Button>
</SheetTrigger>
<SheetContent side="right" className="w-[400px] sm:w-[540px] p-0">
{selectedMessage ? (
<div className="flex flex-col h-full">
<SheetHeader className="p-4 border-b">
<Button
variant="ghost"
onClick={handleBackToList}
className="w-fit px-2"
>
<ArrowLeft className="mr-2 h-4 w-4" />
Back to Inbox
</Button>
</SheetHeader>
<ScrollArea className="flex-grow p-4">
<h2 className="text-xl font-bold mb-2">
{selectedMessage.subject}
</h2>
<div className="flex justify-between items-center mb-4">
{showAvatar && (
<div className="flex items-center gap-3">
<Avatar className="w-10 h-10">
<AvatarImage
src={`https://api.dicebear.com/6.x/initials/svg?seed=${selectedMessage.name}`}
alt={selectedMessage.name}
/>
<AvatarFallback>
{selectedMessage.name
.split(" ")
.map((n) => n[0])
.join("")
.toUpperCase()}
</AvatarFallback>
</Avatar>
<div>
<p className="font-medium">{selectedMessage.name}</p>
<p className="text-sm text-muted-foreground">
{selectedMessage.email}
</p>
</div>
</div>
)}
<p className="text-sm text-muted-foreground">
{selectedMessage.date}
</p>
</div>
<div className="whitespace-pre-wrap">
{selectedMessage.content}
</div>
</ScrollArea>
</div>
) : (
<>
<SheetHeader className="p-4 border-b">
<SheetTitle className="text-left">Inbox</SheetTitle>
<div className="relative">
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Search emails..."
className="pl-8"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
</div>
</SheetHeader>
<ScrollArea className="h-[calc(100vh-120px)]">
{filteredMessages.map((message) => (
<div
key={message.id}
className={`flex items-start gap-3 border-b p-4 text-sm leading-tight hover:bg-accent cursor-pointer ${
message.read ? "opacity-60" : ""
}`}
onClick={() => handleMessageClick(message)}
>
{showAvatar && (
<Avatar className="w-10 h-10">
<AvatarImage
src={`https://api.dicebear.com/6.x/initials/svg?seed=${message.name}`}
alt={message.name}
/>
<AvatarFallback>
{message.name
.split(" ")
.map((n) => n[0])
.join("")
.toUpperCase()}
</AvatarFallback>
</Avatar>
)}
<div className="flex-1">
<div className="flex w-full items-center justify-between">
<span className="font-medium">{message.name}</span>
<span className="text-xs text-muted-foreground">
{message.date}
</span>
</div>
<span className="font-medium">{message.subject}</span>
<span className="line-clamp-2 text-xs text-muted-foreground">
{message.teaser}
</span>
{!message.read && (
<Badge variant="secondary" className="mt-1">
New
</Badge>
)}
</div>
</div>
))}
</ScrollArea>
</>
)}
</SheetContent>
</Sheet>
);
};
export const InboxDrawer = React.memo(InboxDrawerComponent);

View File

@ -17,6 +17,7 @@ import {
} from "@/components/ui/sidebar";
import * as TablerIcons from "@tabler/icons-react";
import { useNavigations } from "@/hooks/use-navigations";
interface SubSubItem {
title: string;
@ -39,8 +40,11 @@ interface NavItem {
}
function SubSubItemComponent({ item }: { item: SubSubItem }) {
const router = useNavigations();
const isActive = router.pathname === item.url;
return (
<SidebarMenuItem>
<SidebarMenuItem className={isActive ? "active text-primary" : ""}>
<SidebarMenuButton asChild>
<a href={item.url}>
<span>{item.title}</span>
@ -51,11 +55,13 @@ function SubSubItemComponent({ item }: { item: SubSubItem }) {
}
function SubItemComponent({ item }: { item: SubItem }) {
const router = useNavigations();
const isActive = router.pathname === item.url;
const hasSubSubItems = item.subSubItems && item.subSubItems.length > 0;
if (!hasSubSubItems) {
return (
<SidebarMenuItem>
<SidebarMenuItem className={isActive ? "active text-primary" : ""}>
<SidebarMenuButton asChild>
<a href={item.url}>
{item.icon && <item.icon />}
@ -68,7 +74,7 @@ function SubItemComponent({ item }: { item: SubItem }) {
return (
<Collapsible asChild className="group/collapsible">
<SidebarMenuItem>
<SidebarMenuItem className={isActive ? "active text-primary" : ""}>
<CollapsibleTrigger asChild>
<SidebarMenuButton>
{item.icon && <item.icon />}
@ -89,11 +95,13 @@ function SubItemComponent({ item }: { item: SubItem }) {
}
function RecursiveNavItem({ item, index }: { item: NavItem; index: number }) {
const router = useNavigations();
const isActive = router.pathname === item.url;
const hasSubItems = item.subItems && item.subItems.length > 0;
if (!hasSubItems) {
return (
<SidebarMenuItem>
<SidebarMenuItem className={isActive ? "active text-primary" : ""}>
<SidebarMenuButton tooltip={item.title} asChild>
<a href={item.url}>
{item.icon && <item.icon />}
@ -111,7 +119,7 @@ function RecursiveNavItem({ item, index }: { item: NavItem; index: number }) {
defaultOpen={index === 1}
className="group/collapsible"
>
<SidebarMenuItem>
<SidebarMenuItem className={isActive ? "active text-primary" : ""}>
<CollapsibleTrigger asChild>
<SidebarMenuButton tooltip={item.title}>
{item.icon && <item.icon />}

View File

@ -29,17 +29,24 @@ import {
SidebarMenuItem,
useSidebar,
} from "@/components/ui/sidebar"
import {
IconBadgeCc,
IconBell,
IconCreditCard,
IconLogout,
IconSparkles,
} from "@tabler/icons-react";
export function NavUser({
user,
}: {
user: {
name: string
email: string
avatar: string
}
name: string;
email: string;
avatar: string;
};
}) {
const { isMobile } = useSidebar()
const { isMobile } = useSidebar();
return (
<SidebarMenu>
@ -81,34 +88,34 @@ export function NavUser({
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>
<Sparkles />
Upgrade to Pro
<DropdownMenuItem className="space-x-2">
<IconSparkles />
<span>Upgrade to Pro</span>
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>
<BadgeCheck />
Account
<DropdownMenuItem className="space-x-2">
<IconBadgeCc />
<span>Account</span>
</DropdownMenuItem>
<DropdownMenuItem>
<CreditCard />
Billing
<DropdownMenuItem className="space-x-2">
<IconCreditCard />
<span>Billing</span>
</DropdownMenuItem>
<DropdownMenuItem>
<Bell />
Notifications
<DropdownMenuItem className="space-x-2">
<IconBell />
<span>Notifications</span>
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuItem>
<LogOut />
Log out
<DropdownMenuItem className="space-x-2">
<IconLogout />
<span>Log out</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
</SidebarMenu>
)
);
}

View File

@ -1,5 +1,7 @@
"use client";
import React from "react";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
@ -8,71 +10,82 @@ import {
DropdownMenuRadioItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Laptop, Moon, Sun } from "lucide-react";
import { Laptop, Moon, Sun, type LucideIcon } from "lucide-react";
import { useTheme } from "next-themes";
import { useEffect, useState } from "react";
import { useEffect, useState, useMemo } from "react";
const ThemeSwitcher = () => {
interface ThemeSwitcherComponentProps {
showTitle?: boolean;
}
type ThemeOption = {
value: string;
icon: LucideIcon;
label: string;
};
const ICON_SIZE = 16;
const themeOptions: ThemeOption[] = [
{ value: "light", icon: Sun, label: "Light" },
{ value: "dark", icon: Moon, label: "Dark" },
{ value: "system", icon: Laptop, label: "System" },
];
const ThemeSwitcherComponent = ({
showTitle = false,
}: ThemeSwitcherComponentProps) => {
const [mounted, setMounted] = useState(false);
const { theme, setTheme } = useTheme();
// useEffect only runs on the client, so now we can safely show the UI
useEffect(() => {
setMounted(true);
}, []);
const currentTheme = useMemo(
() =>
themeOptions.find((option) => option.value === theme) || themeOptions[2],
[theme]
);
if (!mounted) {
return null;
}
const ICON_SIZE = 16;
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size={"md"}>
{theme === "light" ? (
<Sun
key="light"
size={ICON_SIZE}
className={"text-muted-foreground"}
/>
) : theme === "dark" ? (
<Moon
key="dark"
size={ICON_SIZE}
className={"text-muted-foreground"}
/>
) : (
<Laptop
key="system"
size={ICON_SIZE}
className={"text-muted-foreground"}
/>
<Button
variant="ghost"
size={showTitle ? "sm" : "icon"}
className={showTitle ? "flex justify-center items-center" : ""}
aria-label={`Current theme: theme`}
>
<currentTheme.icon
size={ICON_SIZE}
className="text-muted-foreground"
/>
{showTitle && (
<span className="text-muted-foreground font-medium">Theme</span>
)}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-content" align="start">
<DropdownMenuRadioGroup
value={theme}
onValueChange={(e) => setTheme(e)}
>
<DropdownMenuRadioItem className="flex gap-2" value="light">
<Sun size={ICON_SIZE} className="text-muted-foreground" />{" "}
<span>Light</span>
</DropdownMenuRadioItem>
<DropdownMenuRadioItem className="flex gap-2" value="dark">
<Moon size={ICON_SIZE} className="text-muted-foreground" />{" "}
<span>Dark</span>
</DropdownMenuRadioItem>
<DropdownMenuRadioItem className="flex gap-2" value="system">
<Laptop size={ICON_SIZE} className="text-muted-foreground" />{" "}
<span>System</span>
</DropdownMenuRadioItem>
<DropdownMenuRadioGroup value={theme} onValueChange={setTheme}>
{themeOptions.map((option) => (
<DropdownMenuRadioItem
key={option.value}
className="flex gap-2"
value={option.value}
>
{/* <option.icon size={ICON_SIZE} className="text-muted-foreground" /> */}
<span>{option.label}</span>
</DropdownMenuRadioItem>
))}
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
);
};
export { ThemeSwitcher };
export const ThemeSwitcher = React.memo(ThemeSwitcherComponent);

View File

@ -0,0 +1,118 @@
"use client"
import * as React from "react"
import { Drawer as DrawerPrimitive } from "vaul"
import { cn } from "@/lib/utils"
const Drawer = ({
shouldScaleBackground = true,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Root>) => (
<DrawerPrimitive.Root
shouldScaleBackground={shouldScaleBackground}
{...props}
/>
)
Drawer.displayName = "Drawer"
const DrawerTrigger = DrawerPrimitive.Trigger
const DrawerPortal = DrawerPrimitive.Portal
const DrawerClose = DrawerPrimitive.Close
const DrawerOverlay = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DrawerPrimitive.Overlay
ref={ref}
className={cn("fixed inset-0 z-50 bg-black/80", className)}
{...props}
/>
))
DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName
const DrawerContent = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DrawerPortal>
<DrawerOverlay />
<DrawerPrimitive.Content
ref={ref}
className={cn(
"fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background",
className
)}
{...props}
>
<div className="mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted" />
{children}
</DrawerPrimitive.Content>
</DrawerPortal>
))
DrawerContent.displayName = "DrawerContent"
const DrawerHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn("grid gap-1.5 p-4 text-center sm:text-left", className)}
{...props}
/>
)
DrawerHeader.displayName = "DrawerHeader"
const DrawerFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
{...props}
/>
)
DrawerFooter.displayName = "DrawerFooter"
const DrawerTitle = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Title>
>(({ className, ...props }, ref) => (
<DrawerPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
DrawerTitle.displayName = DrawerPrimitive.Title.displayName
const DrawerDescription = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Description>
>(({ className, ...props }, ref) => (
<DrawerPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
DrawerDescription.displayName = DrawerPrimitive.Description.displayName
export {
Drawer,
DrawerPortal,
DrawerOverlay,
DrawerTrigger,
DrawerClose,
DrawerContent,
DrawerHeader,
DrawerFooter,
DrawerTitle,
DrawerDescription,
}

View File

@ -0,0 +1,48 @@
"use client"
import * as React from "react"
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
import { cn } from "@/lib/utils"
const ScrollArea = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<ScrollAreaPrimitive.Root
ref={ref}
className={cn("relative overflow-hidden", className)}
{...props}
>
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
))
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
const ScrollBar = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
>(({ className, orientation = "vertical", ...props }, ref) => (
<ScrollAreaPrimitive.ScrollAreaScrollbar
ref={ref}
orientation={orientation}
className={cn(
"flex touch-none select-none transition-colors",
orientation === "vertical" &&
"h-full w-2.5 border-l border-l-transparent p-[1px]",
orientation === "horizontal" &&
"h-2.5 flex-col border-t border-t-transparent p-[1px]",
className
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
</ScrollAreaPrimitive.ScrollAreaScrollbar>
))
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
export { ScrollArea, ScrollBar }

View File

@ -17,6 +17,7 @@
"@radix-ui/react-dialog": "^1.1.6",
"@radix-ui/react-dropdown-menu": "^2.1.6",
"@radix-ui/react-label": "^2.1.2",
"@radix-ui/react-scroll-area": "^1.2.3",
"@radix-ui/react-select": "^2.1.6",
"@radix-ui/react-separator": "^1.1.2",
"@radix-ui/react-slot": "^1.1.2",
@ -40,6 +41,7 @@
"react-dom": "18.3.1",
"react-hook-form": "^7.54.2",
"resend": "^4.1.2",
"vaul": "^1.1.2",
"zod": "^3.24.2"
},
"devDependencies": {
@ -2209,6 +2211,37 @@
}
}
},
"node_modules/@radix-ui/react-scroll-area": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.3.tgz",
"integrity": "sha512-l7+NNBfBYYJa9tNqVcP2AGvxdE3lmE6kFTBXdvHgUaZuy+4wGCL1Cl2AfaR7RKyimj7lZURGLwFO59k4eBnDJQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/number": "1.1.0",
"@radix-ui/primitive": "1.1.1",
"@radix-ui/react-compose-refs": "1.1.1",
"@radix-ui/react-context": "1.1.1",
"@radix-ui/react-direction": "1.1.0",
"@radix-ui/react-presence": "1.1.2",
"@radix-ui/react-primitive": "2.0.2",
"@radix-ui/react-use-callback-ref": "1.1.0",
"@radix-ui/react-use-layout-effect": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-select": {
"version": "2.1.6",
"resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.1.6.tgz",
@ -7090,6 +7123,19 @@
"node": ">= 0.8"
}
},
"node_modules/vaul": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vaul/-/vaul-1.1.2.tgz",
"integrity": "sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-dialog": "^1.1.1"
},
"peerDependencies": {
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc"
}
},
"node_modules/vt-pbf": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/vt-pbf/-/vt-pbf-3.1.3.tgz",

View File

@ -18,6 +18,7 @@
"@radix-ui/react-dialog": "^1.1.6",
"@radix-ui/react-dropdown-menu": "^2.1.6",
"@radix-ui/react-label": "^2.1.2",
"@radix-ui/react-scroll-area": "^1.2.3",
"@radix-ui/react-select": "^2.1.6",
"@radix-ui/react-separator": "^1.1.2",
"@radix-ui/react-slot": "^1.1.2",
@ -41,6 +42,7 @@
"react-dom": "18.3.1",
"react-hook-form": "^7.54.2",
"resend": "^4.1.2",
"vaul": "^1.1.2",
"zod": "^3.24.2"
},
"devDependencies": {