diff --git a/package-lock.json b/package-lock.json index 9bfdddb2..70ef8682 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,25 +14,25 @@ "@noble/hashes": "2.0.1", "@radix-ui/react-accordion": "1.2.12", "@radix-ui/react-avatar": "1.1.11", - "@radix-ui/react-dialog": "1.1.15", + "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "2.1.16", "@radix-ui/react-label": "2.1.8", "@radix-ui/react-popover": "1.1.15", "@radix-ui/react-radio-group": "1.3.8", "@radix-ui/react-select": "2.2.6", - "@radix-ui/react-separator": "1.1.8", - "@radix-ui/react-slot": "1.2.4", + "@radix-ui/react-separator": "^1.1.8", + "@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-switch": "1.2.6", "@radix-ui/react-tabs": "1.1.13", - "@radix-ui/react-tooltip": "1.2.8", + "@radix-ui/react-tooltip": "^1.2.8", "@tailwindcss/vite": "4.1.18", "@tanstack/match-sorter-utils": "8.19.4", "@tanstack/react-query": "5.90.16", "@tanstack/react-table": "8.21.3", - "class-variance-authority": "0.7.1", + "class-variance-authority": "^0.7.1", "clsx": "2.1.1", "i18next-browser-languagedetector": "8.2.0", - "lucide-react": "0.562.0", + "lucide-react": "^0.562.0", "next-themes": "0.4.6", "qrcode": "1.5.4", "react": "19.2.3", diff --git a/package.json b/package.json index a19b1231..5a647579 100644 --- a/package.json +++ b/package.json @@ -49,25 +49,25 @@ "@noble/hashes": "2.0.1", "@radix-ui/react-accordion": "1.2.12", "@radix-ui/react-avatar": "1.1.11", - "@radix-ui/react-dialog": "1.1.15", + "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "2.1.16", "@radix-ui/react-label": "2.1.8", "@radix-ui/react-popover": "1.1.15", "@radix-ui/react-radio-group": "1.3.8", "@radix-ui/react-select": "2.2.6", - "@radix-ui/react-separator": "1.1.8", - "@radix-ui/react-slot": "1.2.4", + "@radix-ui/react-separator": "^1.1.8", + "@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-switch": "1.2.6", "@radix-ui/react-tabs": "1.1.13", - "@radix-ui/react-tooltip": "1.2.8", + "@radix-ui/react-tooltip": "^1.2.8", "@tailwindcss/vite": "4.1.18", "@tanstack/match-sorter-utils": "8.19.4", "@tanstack/react-query": "5.90.16", "@tanstack/react-table": "8.21.3", - "class-variance-authority": "0.7.1", + "class-variance-authority": "^0.7.1", "clsx": "2.1.1", "i18next-browser-languagedetector": "8.2.0", - "lucide-react": "0.562.0", + "lucide-react": "^0.562.0", "next-themes": "0.4.6", "qrcode": "1.5.4", "react": "19.2.3", diff --git a/src/components/ui/sheet.tsx b/src/components/ui/sheet.tsx new file mode 100644 index 00000000..7b450a7a --- /dev/null +++ b/src/components/ui/sheet.tsx @@ -0,0 +1,102 @@ +'use client' + +import * as React from 'react' +import * as SheetPrimitive from '@radix-ui/react-dialog' +import { XIcon } from 'lucide-react' +import { cn } from '@/lib/utils' + +function Sheet({ ...props }: React.ComponentProps) { + return +} + +function SheetTrigger({ ...props }: React.ComponentProps) { + return +} + +function SheetClose({ ...props }: React.ComponentProps) { + return +} + +function SheetPortal({ ...props }: React.ComponentProps) { + return +} + +function SheetOverlay({ className, ...props }: React.ComponentProps) { + return ( + + ) +} + +function SheetContent({ + className, + children, + side = 'right', + ...props +}: React.ComponentProps & { + side?: 'top' | 'right' | 'bottom' | 'left' +}) { + return ( + + + + {children} + + + Close + + + + ) +} + +function SheetHeader({ className, ...props }: React.ComponentProps<'div'>) { + return
+} + +function SheetFooter({ className, ...props }: React.ComponentProps<'div'>) { + return
+} + +function SheetTitle({ className, ...props }: React.ComponentProps) { + return ( + + ) +} + +function SheetDescription({ className, ...props }: React.ComponentProps) { + return ( + + ) +} + +export { Sheet, 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 00000000..7910f07d --- /dev/null +++ b/src/components/ui/sidebar.tsx @@ -0,0 +1,677 @@ +'use client' + +import * as React from 'react' +import { Slot } from '@radix-ui/react-slot' +import { cva, type VariantProps } from 'class-variance-authority' +import { PanelLeftIcon } from 'lucide-react' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Separator } from '@/components/ui/separator' +import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from '@/components/ui/sheet' +import { Skeleton } from '@/components/ui/skeleton' +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip' +import { useIsMobile } from '@/hooks/use-mobile' +import { cn } from '@/lib/utils' + +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 SidebarContextProps = { + 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 +} + +function SidebarProvider({ + defaultOpen = true, + open: openProp, + onOpenChange: setOpenProp, + className, + style, + children, + ...props +}: React.ComponentProps<'div'> & { + defaultOpen?: boolean + open?: boolean + onOpenChange?: (open: boolean) => void +}) { + 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} +
+
+
+ ) +} + +function Sidebar({ + side = 'left', + variant = 'sidebar', + collapsible = 'offcanvas', + className, + children, + ...props +}: React.ComponentProps<'div'> & { + side?: 'left' | 'right' + variant?: 'sidebar' | 'floating' | 'inset' + collapsible?: 'offcanvas' | 'icon' | 'none' +}) { + const { isMobile, state, openMobile, setOpenMobile } = useSidebar() + + if (collapsible === 'none') { + return ( +
+ {children} +
+ ) + } + + if (isMobile) { + return ( + + + + Sidebar + Displays the mobile sidebar. + +
{children}
+
+
+ ) + } + + return ( +
+ {/* This is what handles the sidebar gap on desktop */} +
+ +
+ ) +} + +function SidebarTrigger({ className, onClick, ...props }: React.ComponentProps) { + const { toggleSidebar } = useSidebar() + + return ( + + ) +} + +function SidebarRail({ className, ...props }: React.ComponentProps<'button'>) { + const { toggleSidebar } = useSidebar() + + return ( +