diff options
| author | Ellie Huxtable <ellie@atuin.sh> | 2024-07-10 15:56:33 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2024-07-10 15:56:33 +0100 |
| commit | 8d9f677c4e9ccfcc6dc9297864dc49446fb5ee59 (patch) | |
| tree | 1fe507403c4d690937834a815a663336bf104039 /ui/src/components/Sidebar/Sidebar.tsx | |
| parent | chore(deps): update to tonic 0.12, prost 0.13 (#2250) (diff) | |
| download | atuin-8d9f677c4e9ccfcc6dc9297864dc49446fb5ee59.zip | |
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
Diffstat (limited to '')
| -rw-r--r-- | ui/src/components/Sidebar/Sidebar.tsx | 328 |
1 files changed, 328 insertions, 0 deletions
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<ListboxProps<SidebarItem>, "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<HTMLElement, SidebarProps>( + ( + { + items, + isCompact, + defaultSelectedKey, + onSelect, + hideEndContent, + sectionClasses: sectionClassesProp = {}, + itemClasses: itemClassesProp = {}, + iconClassName, + classNames, + className, + ...props + }, + ref, + ) => { + const [selected, setSelected] = + React.useState<React.Key>(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 ( + <ListboxItem + {...item} + key={item.key} + classNames={{ + base: cn( + { + "h-auto p-0": !isCompact && isNestType, + }, + { + "inline-block w-11": isCompact && isNestType, + }, + ), + }} + endContent={ + isCompact || isNestType || hideEndContent + ? null + : item.endContent ?? null + } + startContent={ + isCompact || isNestType ? null : item.icon ? ( + <Icon + className={cn( + "text-default-500 group-data-[selected=true]:text-foreground", + iconClassName, + )} + icon={item.icon} + width={24} + /> + ) : ( + item.startContent ?? null + ) + } + title={isCompact || isNestType ? null : item.title} + > + {isCompact ? ( + <Tooltip content={item.title} placement="right"> + <div className="flex w-full items-center justify-center"> + {item.icon ? ( + <Icon + className={cn( + "text-default-500 group-data-[selected=true]:text-foreground", + iconClassName, + )} + icon={item.icon} + width={24} + /> + ) : ( + item.startContent ?? null + )} + </div> + </Tooltip> + ) : null} + {!isCompact && isNestType ? ( + <Accordion className={"p-0"}> + <AccordionItem + key={item.key} + aria-label={item.title} + classNames={{ + heading: "pr-3", + trigger: "p-0", + content: "py-0 pl-4", + }} + title={ + item.icon ? ( + <div + className={"flex h-11 items-center gap-2 px-2 py-1.5"} + > + <Icon + className={cn( + "text-default-500 group-data-[selected=true]:text-foreground", + iconClassName, + )} + icon={item.icon} + width={24} + /> + <span className="text-small font-medium text-default-500 group-data-[selected=true]:text-foreground"> + {item.title} + </span> + </div> + ) : ( + item.startContent ?? null + ) + } + > + {item.items && item.items?.length > 0 ? ( + <Listbox + className={"mt-0.5"} + classNames={{ + list: cn("border-l border-default-200 pl-4"), + }} + items={item.items} + variant="flat" + > + {item.items.map(renderItem)} + </Listbox> + ) : ( + renderItem(item) + )} + </AccordionItem> + </Accordion> + ) : null} + </ListboxItem> + ); + }, + [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 ( + <ListboxItem + {...item} + key={item.key} + endContent={ + isCompact || hideEndContent ? null : item.endContent ?? null + } + startContent={ + isCompact ? null : item.icon ? ( + <Icon + className={cn( + "text-default-500 group-data-[selected=true]:text-foreground", + iconClassName, + )} + icon={item.icon} + width={24} + /> + ) : ( + item.startContent ?? null + ) + } + textValue={item.title} + title={isCompact ? null : item.title} + > + {isCompact ? ( + <Tooltip content={item.title} placement="right"> + <div className="flex w-full items-center justify-center"> + {item.icon ? ( + <Icon + className={cn( + "text-default-500 group-data-[selected=true]:text-foreground", + iconClassName, + )} + icon={item.icon} + width={24} + /> + ) : ( + item.startContent ?? null + )} + </div> + </Tooltip> + ) : null} + </ListboxItem> + ); + }, + [isCompact, hideEndContent, iconClassName, itemClasses?.base], + ); + + return ( + <Listbox + key={isCompact ? "compact" : "default"} + ref={ref} + hideSelectedIcon + as="nav" + className={cn("list-none", className)} + classNames={{ + ...classNames, + list: cn("items-center", classNames?.list), + }} + color="default" + itemClasses={{ + ...itemClasses, + base: cn( + "px-3 min-h-11 rounded-large h-[44px] data-[selected=true]:bg-default-100", + itemClasses?.base, + ), + title: cn( + "text-small font-medium text-default-500 group-data-[selected=true]:text-foreground", + itemClasses?.title, + ), + }} + items={items} + selectedKeys={[selected] as unknown as Selection} + selectionMode="single" + variant="flat" + onSelectionChange={(keys) => { + 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 ? ( + <ListboxSection + key={item.key} + classNames={sectionClasses} + showDivider={isCompact} + title={item.title} + > + {item.items.map(renderItem)} + </ListboxSection> + ) : ( + renderItem(item) + ); + }} + </Listbox> + ); + }, +); + +Sidebar.displayName = "Sidebar"; + +export default Sidebar; |
