import { useClickOutside, useHotkeys } from "@mantine/hooks"; import { IconChevronDown, IconChevronUp } from "@tabler/icons-react"; import { transformObject, clampNumber } from "bknd/utils"; import { throttle } from "lodash-es"; import { ScrollArea } from "radix-ui"; import { type ComponentProps, type ComponentPropsWithoutRef, useEffect, useRef, useState, } from "react"; import type { IconType } from "react-icons"; import { twMerge } from "tailwind-merge"; import { IconButton } from "ui/components/buttons/IconButton"; import { useRoutePathState } from "ui/hooks/use-route-path-state"; import { AppShellProvider, useAppShell } from "ui/layouts/AppShell/use-appshell"; import { appShellStore } from "ui/store"; import { useLocation } from "wouter"; export function Root({ children }: { children: React.ReactNode }) { const sidebarWidths = appShellStore((store) => store.sidebars); const style = transformObject(sidebarWidths, (value) => value.width); return (
[ `--sidebar-width-${key}`, `${value}px`, ]), )} > {children}
); } type NavLinkProps = { Icon?: IconType; children: React.ReactNode; className?: string; to?: string; // @todo: workaround as?: E; disabled?: boolean; }; export const NavLink = ({ children, as, className, Icon, disabled, ...otherProps }: NavLinkProps & Omit, keyof NavLinkProps>) => { const Tag = as || "a"; return ( {Icon && } {typeof children === "string" ? {children} : children} ); }; export function Content({ children, center }: { children: React.ReactNode; center?: boolean }) { return (
{children}
); } export function Main({ children }) { const { sidebar } = useAppShell(); return (
{children}
); } export function Sidebar({ children, name = "default", handle = "right", minWidth, maxWidth, }: { children: React.ReactNode; name?: string; handle?: "right" | "left"; minWidth?: number; maxWidth?: number; }) { const open = appShellStore((store) => store.sidebars[name]?.open); const close = appShellStore((store) => store.closeSidebar(name)); const width = appShellStore((store) => store.sidebars[name]?.width ?? 350); const ref = useClickOutside(close, ["mouseup", "touchend"]); //, [document.getElementById("header")]); const sidebarRef = useRef(null!); const [location] = useLocation(); const closeHandler = () => { open && close(); }; // listen for window location change useEffect(closeHandler, [location]); // @todo: potentially has to be added to the root, as modals could be opened useHotkeys([["Escape", closeHandler]]); return ( <> {handle === "left" && ( )} {handle === "right" && ( )}
); } const SidebarResize = ({ name = "default", handle = "right", sidebarRef, minWidth = 250, maxWidth = window.innerWidth * 0.5, }: { name?: string; handle?: "right" | "left"; sidebarRef: React.RefObject; minWidth?: number; maxWidth?: number; }) => { const setSidebarWidth = appShellStore((store) => store.setSidebarWidth(name)); const [isResizing, setIsResizing] = useState(false); const [start, setStart] = useState(0); const [startWidth, setStartWidth] = useState(sidebarRef.current?.offsetWidth ?? 0); const handleMouseDown = (e: React.MouseEvent) => { e.preventDefault(); setIsResizing(true); setStart(e.clientX); setStartWidth(sidebarRef.current?.offsetWidth ?? 0); }; const handleMouseMove = (e: MouseEvent) => { if (!isResizing) return; const diff = handle === "right" ? e.clientX - start : start - e.clientX; const newWidth = clampNumber(startWidth + diff, minWidth, maxWidth); setSidebarWidth(newWidth); }; const handleMouseUp = () => { setIsResizing(false); }; useEffect(() => { if (isResizing) { window.addEventListener("mousemove", handleMouseMove); window.addEventListener("mouseup", handleMouseUp); } return () => { window.removeEventListener("mousemove", handleMouseMove); window.removeEventListener("mouseup", handleMouseUp); }; }, [isResizing, start, startWidth, minWidth, maxWidth]); return (
); }; export function SectionHeaderTitle({ children, className, ...props }: ComponentProps<"h2">) { return (

{children}

); } export function SectionHeader({ children, right, className, scrollable, sticky }: any = {}) { return (
{typeof children === "string" ? ( {children} ) : ( children )}
{right && !scrollable &&
{right}
} {right && scrollable && (
{right}
)}
); } type SidebarLinkProps = { children: React.ReactNode; as?: E; to?: string; // @todo: workaround params?: Record; // @todo: workaround disabled?: boolean; }; export const SidebarLink = ({ children, as, className, disabled = false, ...otherProps }: SidebarLinkProps & Omit, keyof SidebarLinkProps>) => { const Tag = as || "a"; return ( {children} ); }; type SectionHeaderLinkProps = { children: React.ReactNode; as?: E; to?: string; // @todo: workaround disabled?: boolean; active?: boolean; badge?: string | number; }; export const SectionHeaderLink = ({ children, as, className, disabled = false, active = false, badge, ...props }: SectionHeaderLinkProps & Omit, keyof SectionHeaderLinkProps>) => { const Tag = as || "a"; return ( {children} {badge ? ( {badge} ) : null} ); }; export type SectionHeaderTabsProps = { title?: string; items?: (Omit, "children"> & { label: string; })[]; }; export const SectionHeaderTabs = ({ title, items }: SectionHeaderTabsProps) => { return (
{title && ( {title} )}
{items?.map(({ label, ...item }, key) => ( {label} ))}
); }; export function MaxHeightContainer(props: ComponentPropsWithoutRef<"div">) { const scrollRef = useRef>(null); const [offset, setOffset] = useState(0); const [height, setHeight] = useState(window.innerHeight); function updateHeaderHeight() { if (scrollRef.current) { // get offset to top of window const offset = scrollRef.current.getBoundingClientRect().top; const height = window.innerHeight; setOffset(offset); setHeight(height); } } useEffect(() => { updateHeaderHeight(); const resize = throttle(updateHeaderHeight, 500); window.addEventListener("resize", resize); return () => { window.removeEventListener("resize", resize); }; }, []); return (
{props.children}
); } export function Scrollable({ children, initialOffset = 64, }: { children: React.ReactNode; initialOffset?: number; }) { const scrollRef = useRef>(null); const [offset, setOffset] = useState(initialOffset); function updateHeaderHeight() { if (scrollRef.current) { // get offset to top of window const offset = scrollRef.current.getBoundingClientRect().top; setOffset(offset); } } useEffect(updateHeaderHeight, []); if (typeof window !== "undefined") { window.addEventListener("resize", throttle(updateHeaderHeight, 500)); } return ( {children} ); } type SectionHeaderAccordionItemProps = { title: string; open: boolean; toggle: () => void; ActiveIcon?: any; children?: React.ReactNode; renderHeaderRight?: (props: { open: boolean }) => React.ReactNode; }; export const SectionHeaderAccordionItem = ({ title, open, toggle, ActiveIcon = IconChevronUp, children, renderHeaderRight, }: SectionHeaderAccordionItemProps) => (

{title}

{renderHeaderRight?.({ open })}
{children}
); export const RouteAwareSectionHeaderAccordionItem = ({ routePattern, identifier, ...props }: Omit & { // it's optional because it could be provided using the context routePattern?: string; identifier: string; }) => { const { active, toggle } = useRoutePathState(routePattern, identifier); return ; }; export const Separator = ({ className, ...props }: ComponentPropsWithoutRef<"hr">) => (
); export { Header } from "./Header";