diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx new file mode 100644 index 0000000..4bfe34b --- /dev/null +++ b/src/app/dashboard/page.tsx @@ -0,0 +1,52 @@ +import { AppSidebar } from "@/components/app-sidebar"; +import { + Breadcrumb, + BreadcrumbItem, + BreadcrumbLink, + BreadcrumbList, + BreadcrumbPage, + BreadcrumbSeparator, +} from "@/components/ui/breadcrumb"; +import { Separator } from "@/components/ui/separator"; +import { + SidebarInset, + SidebarProvider, + SidebarTrigger, +} from "@/components/ui/sidebar"; + +export default function Page() { + return ( + + + + + + + + + + + + Building Your Application + + + + + Data Fetching + + + + + + + + + + + + + + + + ); +} diff --git a/src/app/globals.css b/src/app/globals.css index 1c0e1e0..5238951 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -29,6 +29,14 @@ --chart-4: 43 74% 66%; --chart-5: 27 87% 67%; --radius: 0.5rem; + --sidebar-background: 0 0% 98%; + --sidebar-foreground: 240 5.3% 26.1%; + --sidebar-primary: 240 5.9% 10%; + --sidebar-primary-foreground: 0 0% 98%; + --sidebar-accent: 240 4.8% 95.9%; + --sidebar-accent-foreground: 240 5.9% 10%; + --sidebar-border: 220 13% 91%; + --sidebar-ring: 217.2 91.2% 59.8%; } .dark { --background: 240 10% 3.9%; @@ -55,6 +63,14 @@ --chart-3: 30 80% 55%; --chart-4: 280 65% 60%; --chart-5: 340 75% 55%; + --sidebar-background: 240 5.9% 10%; + --sidebar-foreground: 240 4.8% 95.9%; + --sidebar-primary: 224.3 76.3% 48%; + --sidebar-primary-foreground: 0 0% 100%; + --sidebar-accent: 240 3.7% 15.9%; + --sidebar-accent-foreground: 240 4.8% 95.9%; + --sidebar-border: 240 3.7% 15.9%; + --sidebar-ring: 217.2 91.2% 59.8%; } } diff --git a/src/components/app-sidebar.tsx b/src/components/app-sidebar.tsx new file mode 100644 index 0000000..642d7f1 --- /dev/null +++ b/src/components/app-sidebar.tsx @@ -0,0 +1,175 @@ +"use client"; + +import * as React from "react"; +import { + AudioWaveform, + BookOpen, + Bot, + Command, + Frame, + GalleryVerticalEnd, + Map, + PieChart, + Settings2, + SquareTerminal, +} from "lucide-react"; + +import { NavMain } from "@/components/nav-main"; +import { NavProjects } from "@/components/nav-projects"; +import { NavUser } from "@/components/nav-user"; +import { TeamSwitcher } from "@/components/team-switcher"; +import { + Sidebar, + SidebarContent, + SidebarFooter, + SidebarHeader, + SidebarRail, +} from "@/components/ui/sidebar"; + +// This is sample data. +const data = { + user: { + name: "shadcn", + email: "m@example.com", + avatar: "/avatars/shadcn.jpg", + }, + teams: [ + { + name: "Acme Inc", + logo: GalleryVerticalEnd, + plan: "Enterprise", + }, + { + name: "Acme Corp.", + logo: AudioWaveform, + plan: "Startup", + }, + { + name: "Evil Corp.", + logo: Command, + plan: "Free", + }, + ], + navMain: [ + { + title: "Playground", + url: "#", + icon: SquareTerminal, + isActive: true, + items: [ + { + title: "History", + url: "#", + }, + { + title: "Starred", + url: "#", + }, + { + title: "Settings", + url: "#", + }, + ], + }, + { + title: "Models", + url: "#", + icon: Bot, + items: [ + { + title: "Genesis", + url: "#", + }, + { + title: "Explorer", + url: "#", + }, + { + title: "Quantum", + url: "#", + }, + ], + }, + { + title: "Documentation", + url: "#", + icon: BookOpen, + items: [ + { + title: "Introduction", + url: "#", + }, + { + title: "Get Started", + url: "#", + }, + { + title: "Tutorials", + url: "#", + }, + { + title: "Changelog", + url: "#", + }, + ], + }, + { + title: "Settings", + url: "#", + icon: Settings2, + items: [ + { + title: "General", + url: "#", + }, + { + title: "Team", + url: "#", + }, + { + title: "Billing", + url: "#", + }, + { + title: "Limits", + url: "#", + }, + ], + }, + ], + projects: [ + { + name: "Design Engineering", + url: "#", + icon: Frame, + }, + { + name: "Sales & Marketing", + url: "#", + icon: PieChart, + }, + { + name: "Travel", + url: "#", + icon: Map, + }, + ], +}; + +export function AppSidebar({ ...props }: React.ComponentProps) { + return ( + + + + + + + + + + + + + + ); +} diff --git a/src/components/nav-main.tsx b/src/components/nav-main.tsx new file mode 100644 index 0000000..1236292 --- /dev/null +++ b/src/components/nav-main.tsx @@ -0,0 +1,73 @@ +"use client"; + +import { ChevronRight, type LucideIcon } from "lucide-react"; + +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "@/components/ui/collapsible"; +import { + SidebarGroup, + SidebarGroupLabel, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + SidebarMenuSub, + SidebarMenuSubButton, + SidebarMenuSubItem, +} from "@/components/ui/sidebar"; + +export function NavMain({ + items, +}: { + items: { + title: string; + url: string; + icon?: LucideIcon; + isActive?: boolean; + items?: { + title: string; + url: string; + }[]; + }[]; +}) { + return ( + + Platform + + {items.map((item) => ( + + + + + {item.icon && } + {item.title} + + + + + + {item.items?.map((subItem) => ( + + + + {subItem.title} + + + + ))} + + + + + ))} + + + ); +} diff --git a/src/components/nav-projects.tsx b/src/components/nav-projects.tsx new file mode 100644 index 0000000..85abd21 --- /dev/null +++ b/src/components/nav-projects.tsx @@ -0,0 +1,89 @@ +"use client"; + +import { + Folder, + Forward, + MoreHorizontal, + Trash2, + type LucideIcon, +} from "lucide-react"; + +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { + SidebarGroup, + SidebarGroupLabel, + SidebarMenu, + SidebarMenuAction, + SidebarMenuButton, + SidebarMenuItem, + useSidebar, +} from "@/components/ui/sidebar"; + +export function NavProjects({ + projects, +}: { + projects: { + name: string; + url: string; + icon: LucideIcon; + }[]; +}) { + const { isMobile } = useSidebar(); + + return ( + + Projects + + {projects.map((item) => ( + + + + + {item.name} + + + + + + + More + + + + + + View Project + + + + Share Project + + + + + Delete Project + + + + + ))} + + + + More + + + + + ); +} diff --git a/src/components/nav-user.tsx b/src/components/nav-user.tsx new file mode 100644 index 0000000..2019263 --- /dev/null +++ b/src/components/nav-user.tsx @@ -0,0 +1,110 @@ +"use client"; + +import { + BadgeCheck, + Bell, + ChevronsUpDown, + CreditCard, + LogOut, + Sparkles, +} from "lucide-react"; + +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + useSidebar, +} from "@/components/ui/sidebar"; + +export function NavUser({ + user, +}: { + user: { + name: string; + email: string; + avatar: string; + }; +}) { + const { isMobile } = useSidebar(); + + return ( + + + + + + + + CN + + + {user.name} + {user.email} + + + + + + + + + + CN + + + {user.name} + {user.email} + + + + + + + + Upgrade to Pro + + + + + + + Account + + + + Billing + + + + Notifications + + + + + + Log out + + + + + + ); +} diff --git a/src/components/team-switcher.tsx b/src/components/team-switcher.tsx new file mode 100644 index 0000000..33de027 --- /dev/null +++ b/src/components/team-switcher.tsx @@ -0,0 +1,89 @@ +"use client"; + +import * as React from "react"; +import { ChevronsUpDown, Plus } from "lucide-react"; + +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + useSidebar, +} from "@/components/ui/sidebar"; + +export function TeamSwitcher({ + teams, +}: { + teams: { + name: string; + logo: React.ElementType; + plan: string; + }[]; +}) { + const { isMobile } = useSidebar(); + const [activeTeam, setActiveTeam] = React.useState(teams[0]); + + return ( + + + + + + + + + + + {activeTeam.name} + + {activeTeam.plan} + + + + + + + Teams + + {teams.map((team, index) => ( + setActiveTeam(team)} + className="gap-2 p-2" + > + + + + {team.name} + ⌘{index + 1} + + ))} + + + + + + Add team + + + + + + ); +} diff --git a/src/components/ui/sidebar.tsx b/src/components/ui/sidebar.tsx new file mode 100644 index 0000000..1f54110 --- /dev/null +++ b/src/components/ui/sidebar.tsx @@ -0,0 +1,763 @@ +"use client"; + +import * as React from "react"; +import { Slot } from "@radix-ui/react-slot"; +import { VariantProps, cva } from "class-variance-authority"; +import { PanelLeft } from "lucide-react"; + +import { useIsMobile } from "@/hooks/use-mobile"; +import { cn } from "@/lib/utils"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Separator } from "@/components/ui/separator"; +import { Sheet, SheetContent } from "@/components/ui/sheet"; +import { Skeleton } from "@/components/ui/skeleton"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; + +const SIDEBAR_COOKIE_NAME = "sidebar:state"; +const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7; +const SIDEBAR_WIDTH = "16rem"; +const SIDEBAR_WIDTH_MOBILE = "18rem"; +const SIDEBAR_WIDTH_ICON = "3rem"; +const SIDEBAR_KEYBOARD_SHORTCUT = "b"; + +type SidebarContext = { + state: "expanded" | "collapsed"; + open: boolean; + setOpen: (open: boolean) => void; + openMobile: boolean; + setOpenMobile: (open: boolean) => void; + isMobile: boolean; + toggleSidebar: () => void; +}; + +const SidebarContext = React.createContext(null); + +function useSidebar() { + const context = React.useContext(SidebarContext); + if (!context) { + throw new Error("useSidebar must be used within a SidebarProvider."); + } + + return context; +} + +const SidebarProvider = React.forwardRef< + HTMLDivElement, + React.ComponentProps<"div"> & { + defaultOpen?: boolean; + open?: boolean; + onOpenChange?: (open: boolean) => void; + } +>( + ( + { + defaultOpen = true, + open: openProp, + onOpenChange: setOpenProp, + className, + style, + children, + ...props + }, + ref + ) => { + const isMobile = useIsMobile(); + const [openMobile, setOpenMobile] = React.useState(false); + + // This is the internal state of the sidebar. + // We use openProp and setOpenProp for control from outside the component. + const [_open, _setOpen] = React.useState(defaultOpen); + const open = openProp ?? _open; + const setOpen = React.useCallback( + (value: boolean | ((value: boolean) => boolean)) => { + const openState = typeof value === "function" ? value(open) : value; + if (setOpenProp) { + setOpenProp(openState); + } else { + _setOpen(openState); + } + + // This sets the cookie to keep the sidebar state. + document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`; + }, + [setOpenProp, open] + ); + + // Helper to toggle the sidebar. + const toggleSidebar = React.useCallback(() => { + return isMobile + ? setOpenMobile((open) => !open) + : setOpen((open) => !open); + }, [isMobile, setOpen, setOpenMobile]); + + // Adds a keyboard shortcut to toggle the sidebar. + React.useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + if ( + event.key === SIDEBAR_KEYBOARD_SHORTCUT && + (event.metaKey || event.ctrlKey) + ) { + event.preventDefault(); + toggleSidebar(); + } + }; + + window.addEventListener("keydown", handleKeyDown); + return () => window.removeEventListener("keydown", handleKeyDown); + }, [toggleSidebar]); + + // We add a state so that we can do data-state="expanded" or "collapsed". + // This makes it easier to style the sidebar with Tailwind classes. + const state = open ? "expanded" : "collapsed"; + + const contextValue = React.useMemo( + () => ({ + state, + open, + setOpen, + isMobile, + openMobile, + setOpenMobile, + toggleSidebar, + }), + [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar] + ); + + return ( + + + + {children} + + + + ); + } +); +SidebarProvider.displayName = "SidebarProvider"; + +const Sidebar = React.forwardRef< + HTMLDivElement, + React.ComponentProps<"div"> & { + side?: "left" | "right"; + variant?: "sidebar" | "floating" | "inset"; + collapsible?: "offcanvas" | "icon" | "none"; + } +>( + ( + { + side = "left", + variant = "sidebar", + collapsible = "offcanvas", + className, + children, + ...props + }, + ref + ) => { + const { isMobile, state, openMobile, setOpenMobile } = useSidebar(); + + if (collapsible === "none") { + return ( + + {children} + + ); + } + + if (isMobile) { + return ( + + + {children} + + + ); + } + + return ( + + {/* This is what handles the sidebar gap on desktop */} + + + + {children} + + + + ); + } +); +Sidebar.displayName = "Sidebar"; + +const SidebarTrigger = React.forwardRef< + React.ElementRef, + React.ComponentProps +>(({ className, onClick, ...props }, ref) => { + const { toggleSidebar } = useSidebar(); + + return ( + { + onClick?.(event); + toggleSidebar(); + }} + {...props} + > + + Toggle Sidebar + + ); +}); +SidebarTrigger.displayName = "SidebarTrigger"; + +const SidebarRail = React.forwardRef< + HTMLButtonElement, + React.ComponentProps<"button"> +>(({ className, ...props }, ref) => { + const { toggleSidebar } = useSidebar(); + + return ( + + ); +}); +SidebarRail.displayName = "SidebarRail"; + +const SidebarInset = React.forwardRef< + HTMLDivElement, + React.ComponentProps<"main"> +>(({ className, ...props }, ref) => { + return ( + + ); +}); +SidebarInset.displayName = "SidebarInset"; + +const SidebarInput = React.forwardRef< + React.ElementRef, + React.ComponentProps +>(({ className, ...props }, ref) => { + return ( + + ); +}); +SidebarInput.displayName = "SidebarInput"; + +const SidebarHeader = React.forwardRef< + HTMLDivElement, + React.ComponentProps<"div"> +>(({ className, ...props }, ref) => { + return ( + + ); +}); +SidebarHeader.displayName = "SidebarHeader"; + +const SidebarFooter = React.forwardRef< + HTMLDivElement, + React.ComponentProps<"div"> +>(({ className, ...props }, ref) => { + return ( + + ); +}); +SidebarFooter.displayName = "SidebarFooter"; + +const SidebarSeparator = React.forwardRef< + React.ElementRef, + React.ComponentProps +>(({ className, ...props }, ref) => { + return ( + + ); +}); +SidebarSeparator.displayName = "SidebarSeparator"; + +const SidebarContent = React.forwardRef< + HTMLDivElement, + React.ComponentProps<"div"> +>(({ className, ...props }, ref) => { + return ( + + ); +}); +SidebarContent.displayName = "SidebarContent"; + +const SidebarGroup = React.forwardRef< + HTMLDivElement, + React.ComponentProps<"div"> +>(({ className, ...props }, ref) => { + return ( + + ); +}); +SidebarGroup.displayName = "SidebarGroup"; + +const SidebarGroupLabel = React.forwardRef< + HTMLDivElement, + React.ComponentProps<"div"> & { asChild?: boolean } +>(({ className, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "div"; + + return ( + svg]:size-4 [&>svg]:shrink-0", + "group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0", + className + )} + {...props} + /> + ); +}); +SidebarGroupLabel.displayName = "SidebarGroupLabel"; + +const SidebarGroupAction = React.forwardRef< + HTMLButtonElement, + React.ComponentProps<"button"> & { asChild?: boolean } +>(({ className, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button"; + + return ( + svg]:size-4 [&>svg]:shrink-0", + // Increases the hit area of the button on mobile. + "after:absolute after:-inset-2 after:md:hidden", + "group-data-[collapsible=icon]:hidden", + className + )} + {...props} + /> + ); +}); +SidebarGroupAction.displayName = "SidebarGroupAction"; + +const SidebarGroupContent = React.forwardRef< + HTMLDivElement, + React.ComponentProps<"div"> +>(({ className, ...props }, ref) => ( + +)); +SidebarGroupContent.displayName = "SidebarGroupContent"; + +const SidebarMenu = React.forwardRef< + HTMLUListElement, + React.ComponentProps<"ul"> +>(({ className, ...props }, ref) => ( + +)); +SidebarMenu.displayName = "SidebarMenu"; + +const SidebarMenuItem = React.forwardRef< + HTMLLIElement, + React.ComponentProps<"li"> +>(({ className, ...props }, ref) => ( + +)); +SidebarMenuItem.displayName = "SidebarMenuItem"; + +const sidebarMenuButtonVariants = cva( + "peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0", + { + variants: { + variant: { + default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground", + outline: + "bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]", + }, + size: { + default: "h-8 text-sm", + sm: "h-7 text-xs", + lg: "h-12 text-sm group-data-[collapsible=icon]:!p-0", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +); + +const SidebarMenuButton = React.forwardRef< + HTMLButtonElement, + React.ComponentProps<"button"> & { + asChild?: boolean; + isActive?: boolean; + tooltip?: string | React.ComponentProps; + } & VariantProps +>( + ( + { + asChild = false, + isActive = false, + variant = "default", + size = "default", + tooltip, + className, + ...props + }, + ref + ) => { + const Comp = asChild ? Slot : "button"; + const { isMobile, state } = useSidebar(); + + const button = ( + + ); + + if (!tooltip) { + return button; + } + + if (typeof tooltip === "string") { + tooltip = { + children: tooltip, + }; + } + + return ( + + {button} + + + ); + } +); +SidebarMenuButton.displayName = "SidebarMenuButton"; + +const SidebarMenuAction = React.forwardRef< + HTMLButtonElement, + React.ComponentProps<"button"> & { + asChild?: boolean; + showOnHover?: boolean; + } +>(({ className, asChild = false, showOnHover = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button"; + + return ( + svg]:size-4 [&>svg]:shrink-0", + // Increases the hit area of the button on mobile. + "after:absolute after:-inset-2 after:md:hidden", + "peer-data-[size=sm]/menu-button:top-1", + "peer-data-[size=default]/menu-button:top-1.5", + "peer-data-[size=lg]/menu-button:top-2.5", + "group-data-[collapsible=icon]:hidden", + showOnHover && + "group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 peer-data-[active=true]/menu-button:text-sidebar-accent-foreground md:opacity-0", + className + )} + {...props} + /> + ); +}); +SidebarMenuAction.displayName = "SidebarMenuAction"; + +const SidebarMenuBadge = React.forwardRef< + HTMLDivElement, + React.ComponentProps<"div"> +>(({ className, ...props }, ref) => ( + +)); +SidebarMenuBadge.displayName = "SidebarMenuBadge"; + +const SidebarMenuSkeleton = React.forwardRef< + HTMLDivElement, + React.ComponentProps<"div"> & { + showIcon?: boolean; + } +>(({ className, showIcon = false, ...props }, ref) => { + // Random width between 50 to 90%. + const width = React.useMemo(() => { + return `${Math.floor(Math.random() * 40) + 50}%`; + }, []); + + return ( + + {showIcon && ( + + )} + + + ); +}); +SidebarMenuSkeleton.displayName = "SidebarMenuSkeleton"; + +const SidebarMenuSub = React.forwardRef< + HTMLUListElement, + React.ComponentProps<"ul"> +>(({ className, ...props }, ref) => ( + +)); +SidebarMenuSub.displayName = "SidebarMenuSub"; + +const SidebarMenuSubItem = React.forwardRef< + HTMLLIElement, + React.ComponentProps<"li"> +>(({ ...props }, ref) => ); +SidebarMenuSubItem.displayName = "SidebarMenuSubItem"; + +const SidebarMenuSubButton = React.forwardRef< + HTMLAnchorElement, + React.ComponentProps<"a"> & { + asChild?: boolean; + size?: "sm" | "md"; + isActive?: boolean; + } +>(({ asChild = false, size = "md", isActive, className, ...props }, ref) => { + const Comp = asChild ? Slot : "a"; + + return ( + span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground", + "data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground", + size === "sm" && "text-xs", + size === "md" && "text-sm", + "group-data-[collapsible=icon]:hidden", + className + )} + {...props} + /> + ); +}); +SidebarMenuSubButton.displayName = "SidebarMenuSubButton"; + +export { + Sidebar, + SidebarContent, + SidebarFooter, + SidebarGroup, + SidebarGroupAction, + SidebarGroupContent, + SidebarGroupLabel, + SidebarHeader, + SidebarInput, + SidebarInset, + SidebarMenu, + SidebarMenuAction, + SidebarMenuBadge, + SidebarMenuButton, + SidebarMenuItem, + SidebarMenuSkeleton, + SidebarMenuSub, + SidebarMenuSubButton, + SidebarMenuSubItem, + SidebarProvider, + SidebarRail, + SidebarSeparator, + SidebarTrigger, + useSidebar, +}; diff --git a/src/hooks/use-mobile.tsx b/src/hooks/use-mobile.tsx new file mode 100644 index 0000000..48fab93 --- /dev/null +++ b/src/hooks/use-mobile.tsx @@ -0,0 +1,21 @@ +import * as React from "react"; + +const MOBILE_BREAKPOINT = 768; + +export function useIsMobile() { + const [isMobile, setIsMobile] = React.useState( + undefined + ); + + React.useEffect(() => { + const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`); + const onChange = () => { + setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); + }; + mql.addEventListener("change", onChange); + setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); + return () => mql.removeEventListener("change", onChange); + }, []); + + return !!isMobile; +} diff --git a/tailwind.config.ts b/tailwind.config.ts index 4d99f4a..a506dfe 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -54,6 +54,16 @@ export default { "4": "hsl(var(--chart-4))", "5": "hsl(var(--chart-5))", }, + sidebar: { + DEFAULT: "hsl(var(--sidebar-background))", + foreground: "hsl(var(--sidebar-foreground))", + primary: "hsl(var(--sidebar-primary))", + "primary-foreground": "hsl(var(--sidebar-primary-foreground))", + accent: "hsl(var(--sidebar-accent))", + "accent-foreground": "hsl(var(--sidebar-accent-foreground))", + border: "hsl(var(--sidebar-border))", + ring: "hsl(var(--sidebar-ring))", + }, }, borderRadius: { lg: "var(--radius)",