aboutsummaryrefslogtreecommitdiffstats
path: root/ui/src/components/Sidebar/Sidebar.tsx
diff options
context:
space:
mode:
authorEllie Huxtable <ellie@atuin.sh>2024-07-10 15:56:33 +0100
committerGitHub <noreply@github.com>2024-07-10 15:56:33 +0100
commit8d9f677c4e9ccfcc6dc9297864dc49446fb5ee59 (patch)
tree1fe507403c4d690937834a815a663336bf104039 /ui/src/components/Sidebar/Sidebar.tsx
parentchore(deps): update to tonic 0.12, prost 0.13 (#2250) (diff)
downloadatuin-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 'ui/src/components/Sidebar/Sidebar.tsx')
-rw-r--r--ui/src/components/Sidebar/Sidebar.tsx328
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;