From 8d9f677c4e9ccfcc6dc9297864dc49446fb5ee59 Mon Sep 17 00:00:00 2001 From: Ellie Huxtable Date: Wed, 10 Jul 2024 15:56:33 +0100 Subject: feat(gui): use fancy new side nav (#2243) * feat(gui): use fancy new side nav * compact only sidebar, no expand-collapse * custom drag region, remove titlebar * add user popup * wire up login/logout/register, move user button to bottom and add menu * link help and feedback to forum --- ui/src/components/Sidebar/Sidebar.tsx | 328 ++++++++++++++++++++++++++++++++++ ui/src/components/Sidebar/index.tsx | 4 + 2 files changed, 332 insertions(+) create mode 100644 ui/src/components/Sidebar/Sidebar.tsx create mode 100644 ui/src/components/Sidebar/index.tsx (limited to 'ui/src/components/Sidebar') diff --git a/ui/src/components/Sidebar/Sidebar.tsx b/ui/src/components/Sidebar/Sidebar.tsx new file mode 100644 index 00000000..99e2bf82 --- /dev/null +++ b/ui/src/components/Sidebar/Sidebar.tsx @@ -0,0 +1,328 @@ +"use client"; + +import { + Accordion, + AccordionItem, + type ListboxProps, + type ListboxSectionProps, + type Selection, +} from "@nextui-org/react"; +import React from "react"; +import { + Listbox, + Tooltip, + ListboxItem, + ListboxSection, +} from "@nextui-org/react"; +import { Icon } from "@iconify/react"; + +import { cn } from "@/lib/utils"; + +export enum SidebarItemType { + Nest = "nest", +} + +export type SidebarItem = { + key: string; + title: string; + icon?: string; + href?: string; + onPress?: () => void; + type?: SidebarItemType.Nest; + startContent?: React.ReactNode; + endContent?: React.ReactNode; + items?: SidebarItem[]; + className?: string; +}; + +export type SidebarProps = Omit, "children"> & { + items: SidebarItem[]; + isCompact?: boolean; + hideEndContent?: boolean; + iconClassName?: string; + sectionClasses?: ListboxSectionProps["classNames"]; + classNames?: ListboxProps["classNames"]; + defaultSelectedKey: string; + onSelect?: (key: string) => void; +}; + +const Sidebar = React.forwardRef( + ( + { + items, + isCompact, + defaultSelectedKey, + onSelect, + hideEndContent, + sectionClasses: sectionClassesProp = {}, + itemClasses: itemClassesProp = {}, + iconClassName, + classNames, + className, + ...props + }, + ref, + ) => { + const [selected, setSelected] = + React.useState(defaultSelectedKey); + + const sectionClasses = { + ...sectionClassesProp, + base: cn(sectionClassesProp?.base, "w-full", { + "p-0 max-w-[44px]": isCompact, + }), + group: cn(sectionClassesProp?.group, { + "flex flex-col gap-1": isCompact, + }), + heading: cn(sectionClassesProp?.heading, { + hidden: isCompact, + }), + }; + + const itemClasses = { + ...itemClassesProp, + base: cn(itemClassesProp?.base, { + "w-11 h-11 gap-0 p-0": isCompact, + }), + }; + + const renderNestItem = React.useCallback( + (item: SidebarItem) => { + const isNestType = + item.items && + item.items?.length > 0 && + item?.type === SidebarItemType.Nest; + + if (isNestType) { + // Is a nest type item , so we need to remove the href + delete item.href; + } + + return ( + + ) : ( + item.startContent ?? null + ) + } + title={isCompact || isNestType ? null : item.title} + > + {isCompact ? ( + +
+ {item.icon ? ( + + ) : ( + item.startContent ?? null + )} +
+
+ ) : null} + {!isCompact && isNestType ? ( + + + + + {item.title} + + + ) : ( + item.startContent ?? null + ) + } + > + {item.items && item.items?.length > 0 ? ( + + {item.items.map(renderItem)} + + ) : ( + renderItem(item) + )} + + + ) : null} +
+ ); + }, + [isCompact, hideEndContent, iconClassName, items], + ); + + const renderItem = React.useCallback( + (item: SidebarItem) => { + const isNestType = + item.items && + item.items?.length > 0 && + item?.type === SidebarItemType.Nest; + + if (isNestType) { + return renderNestItem(item); + } + + return ( + + ) : ( + item.startContent ?? null + ) + } + textValue={item.title} + title={isCompact ? null : item.title} + > + {isCompact ? ( + +
+ {item.icon ? ( + + ) : ( + item.startContent ?? null + )} +
+
+ ) : null} +
+ ); + }, + [isCompact, hideEndContent, iconClassName, itemClasses?.base], + ); + + return ( + { + const key = Array.from(keys)[0]; + + setSelected(key as React.Key); + onSelect?.(key as string); + }} + {...props} + > + {(item) => { + return item.items && + item.items?.length > 0 && + item?.type === SidebarItemType.Nest ? ( + renderNestItem(item) + ) : item.items && item.items?.length > 0 ? ( + + {item.items.map(renderItem)} + + ) : ( + renderItem(item) + ); + }} + + ); + }, +); + +Sidebar.displayName = "Sidebar"; + +export default Sidebar; diff --git a/ui/src/components/Sidebar/index.tsx b/ui/src/components/Sidebar/index.tsx new file mode 100644 index 00000000..10020952 --- /dev/null +++ b/ui/src/components/Sidebar/index.tsx @@ -0,0 +1,4 @@ +import Sidebar, { SidebarItem } from "./Sidebar"; + +export type { SidebarItem }; +export default Sidebar; -- cgit v1.3.1