packages/devtools-ui/src/components/sidebar.tsx
import React from "react";
import clsx from "clsx";
import { OverviewIcon } from "./icons/overview";
import { MonitorIcon } from "./icons/monitor";
// import { ResourceViewerIcon } from "./icons/resource-viewer";
// import { PackageOverviewIcon } from "./icons/package-overview";
// import { OptionsIcon } from "./icons/options";
import { PlaygroundIcon } from "./icons/playground";
import { InferencerPreviewIcon } from "./icons/inferencer-preview";
// import { SnippetsIcon } from "./icons/snippets";
import { ChatbotIcon } from "./icons/chatbot";
// import { TicketIcon } from "./icons/ticket";
// import { SettingsIcon } from "./icons/settings";
import { NavLink, useLocation } from "react-router-dom";
import { HiddenItemsBgIcon } from "./icons/hidden-items-bg";
import { ActiveItemBackground } from "./active-item-background";
const SidebarItem = ({
item: { icon, path, soon, label: itemLabel },
index,
separator,
active,
hideLabel,
}: {
item: (typeof items)[number];
separator?: boolean;
index: number;
active?: boolean;
hideLabel?: boolean;
}) => {
const timeoutRef = React.useRef<number | null>(null);
const [hover, setHover] = React.useState(false);
const Icon = icon ?? React.Fragment;
const Element = soon ? "div" : NavLink;
const label = itemLabel;
return (
<React.Fragment key={index}>
<Element
to={path ?? "/"}
onMouseEnter={() => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
timeoutRef.current = window.setTimeout(() => {
setHover(true);
}, 150);
}}
onMouseLeave={() => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
timeoutRef.current = window.setTimeout(() => {
setHover(false);
}, 150);
}}
className={clsx(
"re-relative",
"re-flex-shrink-0",
"re-w-10",
"re-h-10",
active ? "re-text-alt-cyan" : "re-text-gray-500",
!active && "hover:re-text-alt-cyan",
"re-transition-colors",
"re-duration-200",
"re-ease-in-out",
"re-flex",
"re-justify-center",
"re-gap-4",
"re-items-center",
"re-group",
)}
>
<ActiveItemBackground active={active} />
<Icon className={clsx("re-z-[1]", soon && "re-opacity-50")} />
{soon && (
<div
className={clsx(
"re-select-none",
"re-absolute",
"re-h-[11px]",
"re-bottom-[-2.5px]",
"re-right-[4.5px]",
"re-text-[7px]",
"re-leading-[7px]",
"re-z-[2]",
"re",
"re-text-center",
"re-font-semibold",
"re-text-alt-cyan",
"re-bg-alt-cyan",
"re-bg-opacity-10",
"re-border",
"re-border-opacity-20",
"re-border-alt-cyan",
"re-py-px",
"re-px-1",
"re-rounded-lg",
)}
>
SOON
</div>
)}
{!hideLabel && (
<div
className={clsx(
"re-transition-transform",
!hover && "re-scale-y-0 re--translate-x-6",
hover && "re-scale-y-100 re-translate-x-0",
"re-absolute",
"re-left-[42px]",
"re-top-0",
"re-h-full",
"re-flex",
"re-items-center",
"re-justify-center",
"re-text-sm",
"re-break-keep",
"re-whitespace-nowrap",
"re-z-[2]",
)}
>
<div
className={clsx(
"re-px-2",
"re-py-1",
"re-border",
"re-border-gray-600",
"re-bg-gray-700",
"re-shadow-md",
"re-rounded",
"re-text-gray-0",
)}
>
{label}
</div>
</div>
)}
</Element>
{separator && (
<div
className={clsx(
"re-w-full",
"re-h-0",
"re--mt-1",
"re--mb-[5px]",
"re-border-b",
"re-border-b-gray-600",
)}
/>
)}
</React.Fragment>
);
};
const SidebarHiddenItemsItem = ({
index,
hiddenItems,
active,
}: {
item: (typeof items)[number];
hiddenItems: typeof items;
separator?: boolean;
index: number;
active?: boolean;
}) => {
const timeoutRef = React.useRef<number | null>(null);
const [hover, setHover] = React.useState(false);
return (
<React.Fragment key={index}>
<div
onMouseEnter={() => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
timeoutRef.current = window.setTimeout(() => {
setHover(true);
}, 150);
}}
onMouseLeave={() => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
timeoutRef.current = window.setTimeout(() => {
setHover(false);
}, 150);
}}
className={clsx(
"re-cursor-pointer",
"re-relative",
"re-flex-shrink-0",
"re-w-10",
"re-h-10",
active ? "re-text-alt-cyan" : "re-text-gray-500",
!active && "hover:re-text-alt-cyan",
"re-transition-colors",
"re-duration-200",
"re-ease-in-out",
"re-flex",
"re-justify-center",
"re-items-center",
"re-group",
)}
>
<HiddenItemsBgIcon className="re-z-[1] re-text-gray-700 re-w-9 re-h-9" />
<span
className={clsx(
"re-absolute",
"re-left-0",
"re-right-0",
"re-flex",
"re-items-center",
"re-justify-center",
"re-z-[1]",
"re-text-gray-300",
"re-text-sm",
)}
>
+{hiddenItems.length}
</span>
<div
className={clsx(
"re-transition-transform",
!hover && "re-scale-x-0 re--translate-x-6",
hover && "re-scale-x-100 re-translate-x-0",
"re-absolute",
"re-left-[42px]",
"re-bottom-[-2px]",
"re-flex",
"re-items-center",
"re-justify-center",
"re-text-sm",
"re-break-keep",
"re-whitespace-nowrap",
)}
>
<div
className={clsx(
"re-px-3",
"re-pt-0",
"re-pb-1",
"re-border",
"re-border-gray-600",
"re-bg-gray-900",
"re-shadow-md",
"re-flex",
"re-items-center",
"re-gap-2",
"re-rounded",
)}
>
{hiddenItems.map((item, index) => {
return (
<SidebarItem
item={item}
key={index}
index={index}
active={false}
hideLabel
/>
);
})}
</div>
</div>
</div>
</React.Fragment>
);
};
type SidebarItemType = {
icon?: React.ComponentType<React.ComponentProps<"svg">>;
label: string;
path?: string;
section?: string;
soon?: boolean;
};
const items: SidebarItemType[] = [
{
icon: OverviewIcon,
label: "Overview",
path: "/overview",
section: "first",
},
{
icon: MonitorIcon,
label: "Monitor",
path: "/monitor",
section: "first",
},
// {
// icon: ResourceViewerIcon,
// label: "Resource Viewer",
// path: "/resource-viewer",
// section: "first",
// soon: true,
// },
// {
// icon: OptionsIcon,
// label: "Options",
// path: "/options",
// section: "first",
// soon: true,
// },
{
icon: PlaygroundIcon,
label: "Playground",
path: "/playground",
section: "second",
soon: true,
},
{
icon: InferencerPreviewIcon,
label: "Inferencer Preview",
path: "/inferencer-preview",
section: "second",
soon: true,
},
// {
// icon: SnippetsIcon,
// label: "Snippets",
// path: "/snippets",
// section: "second",
// soon: true,
// },
{
icon: ChatbotIcon,
label: "Chatbot",
path: "/chatbot",
section: "second",
soon: true,
},
// {
// icon: SettingsIcon,
// label: "Settings",
// path: "/settings",
// section: "fourth",
// soon: true,
// },
];
const ITEM_HEIGHT = 40;
const ITEM_GAP = 10;
export const Sidebar = () => {
const itemContainerRef = React.useRef<HTMLDivElement>(null);
const { pathname } = useLocation();
const [itemsToRender, setItemsToRender] = React.useState<typeof items>([]);
const [hiddenItems, setHiddenItems] = React.useState<typeof items>([]);
React.useEffect(() => {
const handleResize = () => {
if (!itemContainerRef.current) {
setItemsToRender([]);
return;
}
const { clientHeight } = itemContainerRef.current;
const itemCount = Math.floor(clientHeight / (ITEM_HEIGHT + ITEM_GAP));
const realItemCount =
itemCount === items.length ? itemCount : itemCount - 1;
const remainingItemElement = {
label: "__hidden_elements__",
};
setItemsToRender([
...items.slice(0, realItemCount),
...(realItemCount < items.length ? [remainingItemElement] : []),
]);
setHiddenItems(items.slice(realItemCount));
};
handleResize();
window.addEventListener("resize", handleResize);
return () => {
window.removeEventListener("resize", handleResize);
};
}, []);
return (
<nav
className={clsx(
"re-flex-shrink-0",
"re-min-h-full",
"re-bg-gray-900",
"re-border-r",
"re-border-r-gray-700",
"re-px-1.5",
"re-pt-2",
"re-flex",
)}
>
<div
ref={itemContainerRef}
className={clsx(
"re-flex",
"re-flex-1",
"re-flex-col",
"re-gap-2.5",
"re-w-10",
"re-flex-shrink-0",
)}
>
{itemsToRender.map((item, index, array) => {
const nextItemSection = array[index + 1]?.section;
if (item.label === "__hidden_elements__") {
return (
<SidebarHiddenItemsItem
key={index}
item={item}
hiddenItems={hiddenItems}
index={index}
/>
);
}
return (
<SidebarItem
key={index}
item={item}
index={index}
active={pathname === item.path}
separator={Boolean(
nextItemSection && nextItemSection !== item.section,
)}
/>
);
})}
</div>
</nav>
);
};