From 86f375ee644df0c3f4cc6c05bf919601cac2a73d Mon Sep 17 00:00:00 2001 From: tylerslaton Date: Tue, 22 Oct 2024 13:44:02 -0400 Subject: [PATCH] feat: overhaul sidebar The sidebar now uses the ShadCN component. This reduces code complexity by a lot. Throughout the applications the various components for this are used and overall styling has changed quite a bit. As such, the old sidebar components are no longer needed and are deleted. Signed-off-by: tylerslaton --- ui/admin/app/components/header/HeaderNav.tsx | 42 +- ui/admin/app/components/sidebar/Sidebar.tsx | 118 ++- .../components/sidebar/SidebarCollapsed.tsx | 35 - .../app/components/sidebar/SidebarFull.tsx | 40 - .../app/components/sidebar/SidebarSection.tsx | 35 - ui/admin/app/components/sidebar/index.tsx | 2 +- ui/admin/app/components/ui/sheet.tsx | 17 +- ui/admin/app/components/ui/sidebar.tsx | 787 ++++++++++++++++++ ui/admin/app/components/ui/skeleton.tsx | 15 + ui/admin/app/components/ui/tooltip.tsx | 28 +- ui/admin/app/hooks/use-mobile.tsx | 23 + ui/admin/app/root.tsx | 9 +- ui/admin/app/routes/_auth.tsx | 10 +- ui/admin/app/tailwind.css | 16 + ui/admin/package-lock.json | 2 +- ui/admin/package.json | 2 +- ui/admin/tailwind.config.ts | 12 + 17 files changed, 972 insertions(+), 221 deletions(-) delete mode 100644 ui/admin/app/components/sidebar/SidebarCollapsed.tsx delete mode 100644 ui/admin/app/components/sidebar/SidebarFull.tsx delete mode 100644 ui/admin/app/components/sidebar/SidebarSection.tsx create mode 100644 ui/admin/app/components/ui/sidebar.tsx create mode 100644 ui/admin/app/components/ui/skeleton.tsx create mode 100644 ui/admin/app/hooks/use-mobile.tsx diff --git a/ui/admin/app/components/header/HeaderNav.tsx b/ui/admin/app/components/header/HeaderNav.tsx index a46c60999..6b7a74e0c 100644 --- a/ui/admin/app/components/header/HeaderNav.tsx +++ b/ui/admin/app/components/header/HeaderNav.tsx @@ -1,5 +1,5 @@ import { Link, useLocation, useParams } from "@remix-run/react"; -import { ArrowLeftIcon, MenuIcon } from "lucide-react"; +import { ArrowLeftIcon } from "lucide-react"; import { $params, $path } from "remix-routes"; import useSWR from "swr"; @@ -10,19 +10,11 @@ import { cn, parseQueryParams } from "~/lib/utils"; import { DarkModeToggle } from "~/components/DarkModeToggle"; import { TypographyH4, TypographySmall } from "~/components/Typography"; -import { OttoLogo } from "~/components/branding/OttoLogo"; -import { useLayout } from "~/components/layout/LayoutProvider"; import { Button } from "~/components/ui/button"; -import { UserMenu } from "~/components/user/UserMenu"; -export function HeaderNav() { - const { - isExpanded, - onExpandedChange, - smallSidebarWidth, - fullSidebarWidth, - } = useLayout(); +import { SidebarTrigger } from "../ui/sidebar"; +export function HeaderNav() { const { pathname } = useLocation(); const headerHeight = "h-[60px]"; @@ -34,39 +26,15 @@ export function HeaderNav() { )} >
-
- - - -
-
-
+
+ {getHeaderContent(pathname)}
-
diff --git a/ui/admin/app/components/sidebar/Sidebar.tsx b/ui/admin/app/components/sidebar/Sidebar.tsx index a6a773d98..22989d9e5 100644 --- a/ui/admin/app/components/sidebar/Sidebar.tsx +++ b/ui/admin/app/components/sidebar/Sidebar.tsx @@ -1,49 +1,97 @@ -import React from "react"; +import { Link } from "@remix-run/react"; +import { BotIcon, MessageSquare, User } from "lucide-react"; +import { $path } from "remix-routes"; import { cn } from "~/lib/utils"; -import { SidebarFull } from "~/components/sidebar/SidebarFull"; +import { + Sidebar, + SidebarContent, + SidebarFooter, + SidebarGroup, + SidebarGroupContent, + SidebarHeader, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + SidebarRail, + useSidebar, +} from "~/components/ui/sidebar"; -import { useLayout } from "../layout/LayoutProvider"; -import { SidebarCollapsed } from "./SidebarCollapsed"; +import { OttoLogo } from "../branding/OttoLogo"; +import { UserMenu } from "../user/UserMenu"; -type SidebarProps = React.HTMLAttributes; - -export function Sidebar({ className, ...props }: SidebarProps) { - const { isExpanded, sidebarWidth } = useLayout(); +// Menu items. +const items = [ + { + title: "Agents", + url: $path("/agents"), + icon: BotIcon, + }, + { + title: "Threads", + url: $path("/threads"), + icon: MessageSquare, + }, + { + title: "Users", + url: $path("/users"), + icon: User, + }, +]; +export function AppSidebar() { + const { state } = useSidebar(); return ( -
-
-
-
- -
-
- + + +
-
- -
+
-
-
+ + + + + + {items.map((item) => ( + + + + + {item.title} + + + + ))} + + + + + + + + ); } diff --git a/ui/admin/app/components/sidebar/SidebarCollapsed.tsx b/ui/admin/app/components/sidebar/SidebarCollapsed.tsx deleted file mode 100644 index dd26fe2ca..000000000 --- a/ui/admin/app/components/sidebar/SidebarCollapsed.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { BotIcon, Key, MessageSquare, User } from "lucide-react"; -import { Link } from "react-router-dom"; -import { $path } from "remix-routes"; - -import { Button } from "~/components/ui/button"; - -export function SidebarCollapsed() { - return ( -
- - - - - - - -
- ); -} diff --git a/ui/admin/app/components/sidebar/SidebarFull.tsx b/ui/admin/app/components/sidebar/SidebarFull.tsx deleted file mode 100644 index 00d26a59e..000000000 --- a/ui/admin/app/components/sidebar/SidebarFull.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { BotIcon, Key, MessageSquare, User } from "lucide-react"; -import { $path } from "remix-routes"; - -import { cn } from "~/lib/utils"; - -import { SidebarSection } from "~/components/sidebar/SidebarSection"; -import { ScrollArea } from "~/components/ui/scroll-area"; - -type SidebarFullProps = React.HTMLAttributes; - -export function SidebarFull({ className }: SidebarFullProps) { - return ( -
- -
- } - /> - } - /> - } - /> - } - /> -
-
-
- ); -} diff --git a/ui/admin/app/components/sidebar/SidebarSection.tsx b/ui/admin/app/components/sidebar/SidebarSection.tsx deleted file mode 100644 index bb993139f..000000000 --- a/ui/admin/app/components/sidebar/SidebarSection.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { Link } from "react-router-dom"; - -import { Button } from "~/components/ui/button"; - -type SidebarSectionProps = { - title: string; - linkTo: string; - children?: React.ReactNode; - icon?: React.ReactNode; -}; - -export function SidebarSection({ - title, - linkTo, - children, - icon, -}: SidebarSectionProps) { - return ( -
- -
{children}
-
- ); -} diff --git a/ui/admin/app/components/sidebar/index.tsx b/ui/admin/app/components/sidebar/index.tsx index df73ac99c..f0ca56e50 100644 --- a/ui/admin/app/components/sidebar/index.tsx +++ b/ui/admin/app/components/sidebar/index.tsx @@ -1 +1 @@ -export { Sidebar } from "~/components/sidebar/Sidebar"; +export { AppSidebar as Sidebar } from "~/components/sidebar/Sidebar"; diff --git a/ui/admin/app/components/ui/sheet.tsx b/ui/admin/app/components/ui/sheet.tsx index 7d0798c57..afef28b0f 100644 --- a/ui/admin/app/components/ui/sheet.tsx +++ b/ui/admin/app/components/ui/sheet.tsx @@ -47,14 +47,12 @@ const sheetVariants = cva( interface SheetContentProps extends React.ComponentPropsWithoutRef, - VariantProps { - hideClose?: boolean; -} + VariantProps {} const SheetContent = React.forwardRef< React.ElementRef, SheetContentProps ->(({ side = "right", className, children, hideClose, ...props }, ref) => ( +>(({ side = "right", className, children, ...props }, ref) => ( - {!hideClose && ( - - - Close - - )} - + + + Close + {children} diff --git a/ui/admin/app/components/ui/sidebar.tsx b/ui/admin/app/components/ui/sidebar.tsx new file mode 100644 index 000000000..31418dcd1 --- /dev/null +++ b/ui/admin/app/components/ui/sidebar.tsx @@ -0,0 +1,787 @@ +import { Slot } from "@radix-ui/react-slot"; +import { VariantProps, cva } from "class-variance-authority"; +import { PanelLeft } from "lucide-react"; +import * as React from "react"; + +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"; +import { useIsMobile } from "~/hooks/use-mobile"; + +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)) => { + if (setOpenProp) { + return setOpenProp?.( + typeof value === "function" ? value(open) : value + ); + } + + _setOpen(value); + + // This sets the cookie to keep the sidebar state. + document.cookie = `${SIDEBAR_COOKIE_NAME}=${open}; 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 ( +