diff --git a/package.json b/package.json
index 466ac97a5..15fa524fc 100644
--- a/package.json
+++ b/package.json
@@ -41,6 +41,7 @@
"@prisma/client": "^5.18.0",
"@radix-ui/react-accordion": "^1.1.2",
"@radix-ui/react-avatar": "^1.0.4",
+ "@radix-ui/react-collapsible": "^1.1.1",
"@radix-ui/react-dialog": "^1.1.1",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-label": "^2.0.2",
diff --git a/src/app/(new)/new/layout.tsx b/src/app/(new)/new/layout.tsx
new file mode 100644
index 000000000..b1fd878c1
--- /dev/null
+++ b/src/app/(new)/new/layout.tsx
@@ -0,0 +1,15 @@
+import { SidebarProvider, SidebarTrigger } from '@/components/ui/sidebar';
+import { AppSidebar } from '@/components/new/app-sidebar';
+import React from 'react';
+
+export default function Layout({ children }: { children: React.ReactNode }) {
+ return (
+
+
+
+
+ {children}
+
+
+ );
+}
diff --git a/src/app/(new)/new/page.tsx b/src/app/(new)/new/page.tsx
new file mode 100644
index 000000000..e10053ba3
--- /dev/null
+++ b/src/app/(new)/new/page.tsx
@@ -0,0 +1,6 @@
+export default function Page() {
+ return (
+
+
+ );
+}
diff --git a/src/app/globals.css b/src/app/globals.css
index 7db494853..16b81d103 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -355,4 +355,28 @@
to {
mask-size: 350vmax;
}
-}
\ No newline at end of file
+}
+
+@layer base {
+ :root {
+ --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 {
+ --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/app/layout.tsx b/src/app/layout.tsx
index f5198fcba..1c02b4093 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -7,7 +7,7 @@ import { Providers } from './providers';
import { GoogleAnalytics } from '@/components/analytics/GoogleAnalytics';
import { siteConfig } from '@/config/site-config';
import { Toaster } from 'sonner';
-import { Navbar } from '@/components/Navbar';
+// import { Navbar } from '@/components/Navbar';
import NextTopLoader from 'nextjs-toploader';
import OfflineNotification from '@/components/OfflineNavigator';
@@ -38,7 +38,7 @@ export default function RootLayout({ children }: { children: ReactNode }) {
showSpinner={false}
/>
-
+ {/* */}
{children}
diff --git a/src/components/new/app-sidebar.tsx b/src/components/new/app-sidebar.tsx
new file mode 100644
index 000000000..628b92d85
--- /dev/null
+++ b/src/components/new/app-sidebar.tsx
@@ -0,0 +1,229 @@
+'use client';
+
+import * as React from 'react';
+
+import {
+ Sidebar,
+ SidebarContent,
+ SidebarFooter,
+ // SidebarGroup,
+ SidebarHeader,
+ SidebarMenu,
+ // SidebarMenuButton,
+ SidebarMenuItem,
+ // SidebarMenuSub,
+ // SidebarMenuSubButton,
+ // SidebarMenuSubItem,
+ SidebarRail,
+} from '@/components/ui/sidebar';
+import Link from 'next/link';
+import { useSession } from 'next-auth/react';
+import { Button } from '../ui/button';
+import { usePathname, useRouter } from 'next/navigation';
+import {
+ ArrowLeft,
+ AudioWaveform,
+ BookOpen,
+ Bot,
+ Command,
+ CommandIcon,
+ Frame,
+ GalleryVerticalEnd,
+ PieChart,
+ Settings2,
+} from 'lucide-react';
+import { NavMain } from './nav-main';
+import { NavUser } from './nav-user';
+// import { NavProjects } from './nav-projects';
+
+// 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: 'Week 0',
+ url: '#',
+ icon: CommandIcon,
+ isActive: true,
+ items: [
+ {
+ title: 'Intro, Setting Up Your IDE',
+ url: '#',
+ },
+ {
+ title: 'HTML Basics',
+ url: '#',
+ },
+ {
+ title: 'Css Basics',
+ 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) {
+ const { data: session } = useSession();
+ const router = useRouter();
+ const pathname = usePathname();
+
+ return (
+
+
+
+
+ {/* */}
+
+ {/* */}
+
+
+
+
+
+ {/* */}
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/new/nav-main.tsx b/src/components/new/nav-main.tsx
new file mode 100644
index 000000000..af34f41da
--- /dev/null
+++ b/src/components/new/nav-main.tsx
@@ -0,0 +1,74 @@
+'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/new/nav-projects.tsx b/src/components/new/nav-projects.tsx
new file mode 100644
index 000000000..7986c0ea6
--- /dev/null
+++ b/src/components/new/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/new/nav-user.tsx b/src/components/new/nav-user.tsx
new file mode 100644
index 000000000..4a7fd07d4
--- /dev/null
+++ b/src/components/new/nav-user.tsx
@@ -0,0 +1,147 @@
+'use client';
+
+import {
+ Bookmark,
+ Calendar,
+ ChevronsUpDown,
+ CreditCard,
+ History,
+ LogOut,
+ MessageSquare,
+} 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';
+import ExternalLinks from '../profile-menu/ExternalLinks';
+import Link from 'next/link';
+import { signOut, useSession } from 'next-auth/react';
+import ProfileDropdown from '../profile-menu/ProfileDropdown';
+
+export function NavUser({
+ user,
+}: {
+ user: {
+ name: string;
+ email: string;
+ avatar: string;
+ };
+}) {
+ const { isMobile } = useSidebar();
+ const { data: session } = useSession();
+
+ const menuItemLinks = [
+ {
+ href: '/watch-history',
+ icon: ,
+ label: 'Watch History',
+ },
+ {
+ href: '/bookmark',
+ icon: ,
+ label: 'Bookmarks',
+ },
+ {
+ href: '/question',
+ icon: ,
+ label: 'Questions',
+ },
+ {
+ href: '/payout-methods',
+ icon: ,
+ label: 'Payout Methods',
+ },
+ {
+ href: '/calendar',
+ icon: ,
+ label: 'Calendar',
+ },
+ ];
+
+ return (
+
+
+
+
+
+
+
+ CN
+
+
+ {user.name}
+ {user.email}
+
+
+
+
+
+
+
+
+
+
+ CN
+
+
+ {user.name}
+ {user.email}
+
+
+
+
+
+ {menuItemLinks.map(({ href, label, icon }) => (
+
+
+ {icon}
+ {label}
+
+
+ ))}
+
+
+
+
+
+ {
+ signOut();
+ }}
+ >
+
+
+ Logout
+
+
+
+ {session?.user && }
+
+
+
+
+ );
+}
diff --git a/src/components/ui/collapsible.tsx b/src/components/ui/collapsible.tsx
new file mode 100644
index 000000000..9fa48946a
--- /dev/null
+++ b/src/components/ui/collapsible.tsx
@@ -0,0 +1,11 @@
+"use client"
+
+import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
+
+const Collapsible = CollapsiblePrimitive.Root
+
+const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
+
+const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
+
+export { Collapsible, CollapsibleTrigger, CollapsibleContent }
diff --git a/src/components/ui/sheet.tsx b/src/components/ui/sheet.tsx
new file mode 100644
index 000000000..a37f17ba0
--- /dev/null
+++ b/src/components/ui/sheet.tsx
@@ -0,0 +1,140 @@
+"use client"
+
+import * as React from "react"
+import * as SheetPrimitive from "@radix-ui/react-dialog"
+import { cva, type VariantProps } from "class-variance-authority"
+import { X } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+const Sheet = SheetPrimitive.Root
+
+const SheetTrigger = SheetPrimitive.Trigger
+
+const SheetClose = SheetPrimitive.Close
+
+const SheetPortal = SheetPrimitive.Portal
+
+const SheetOverlay = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
+
+const sheetVariants = cva(
+ "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
+ {
+ variants: {
+ side: {
+ top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
+ bottom:
+ "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
+ left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
+ right:
+ "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
+ },
+ },
+ defaultVariants: {
+ side: "right",
+ },
+ }
+)
+
+interface SheetContentProps
+ extends React.ComponentPropsWithoutRef,
+ VariantProps {}
+
+const SheetContent = React.forwardRef<
+ React.ElementRef,
+ SheetContentProps
+>(({ side = "right", className, children, ...props }, ref) => (
+
+
+
+ {children}
+
+
+ Close
+
+
+
+))
+SheetContent.displayName = SheetPrimitive.Content.displayName
+
+const SheetHeader = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+)
+SheetHeader.displayName = "SheetHeader"
+
+const SheetFooter = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+)
+SheetFooter.displayName = "SheetFooter"
+
+const SheetTitle = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+SheetTitle.displayName = SheetPrimitive.Title.displayName
+
+const SheetDescription = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+SheetDescription.displayName = SheetPrimitive.Description.displayName
+
+export {
+ Sheet,
+ SheetPortal,
+ SheetOverlay,
+ SheetTrigger,
+ SheetClose,
+ SheetContent,
+ SheetHeader,
+ SheetFooter,
+ SheetTitle,
+ SheetDescription,
+}
diff --git a/src/components/ui/sidebar.tsx b/src/components/ui/sidebar.tsx
new file mode 100644
index 000000000..eb58669de
--- /dev/null
+++ b/src/components/ui/sidebar.tsx
@@ -0,0 +1,771 @@
+'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/useMobile';
+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 */}
+
+
+
+ );
+ },
+);
+Sidebar.displayName = 'Sidebar';
+
+const SidebarTrigger = React.forwardRef<
+ React.ElementRef,
+ React.ComponentProps
+>(({ className, onClick, ...props }, ref) => {
+ const { toggleSidebar } = useSidebar();
+
+ return (
+
+ );
+});
+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 &&
+ 'peer-data-[active=true]/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 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 (
+ svg]:text-sidebar-accent-foreground flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 outline-none focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0',
+ '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/useMobile.tsx b/src/hooks/useMobile.tsx
new file mode 100644
index 000000000..821f8ff4a
--- /dev/null
+++ b/src/hooks/useMobile.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.js b/tailwind.config.js
index 2767ee02d..6c20d69e4 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -11,7 +11,7 @@ module.exports = {
prefix: '',
theme: {
container: {
- center: true,
+ center: 'true',
padding: '2rem',
},
extend: {
@@ -52,6 +52,16 @@ module.exports = {
DEFAULT: 'hsl(var(--card))',
foreground: 'hsl(var(--card-foreground))',
},
+ 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)',
@@ -60,12 +70,20 @@ module.exports = {
},
keyframes: {
'accordion-down': {
- from: { height: '0' },
- to: { height: 'var(--radix-accordion-content-height)' },
+ from: {
+ height: '0',
+ },
+ to: {
+ height: 'var(--radix-accordion-content-height)',
+ },
},
'accordion-up': {
- from: { height: 'var(--radix-accordion-content-height)' },
- to: { height: '0' },
+ from: {
+ height: 'var(--radix-accordion-content-height)',
+ },
+ to: {
+ height: '0',
+ },
},
scroll: {
to: {
@@ -73,8 +91,12 @@ module.exports = {
},
},
pulse: {
- '0%, 100%': { opacity: 1 },
- '50%': { opacity: 0.5 },
+ '0%, 100%': {
+ opacity: '1',
+ },
+ '50%': {
+ opacity: '0.5',
+ },
},
},
animation: {