import { useClickOutside, useHotkeys } from "@mantine/hooks"; import { IconChevronDown, IconChevronUp } from "@tabler/icons-react"; import { clampNumber } from "core/utils/numbers"; 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 sidebarWidth = appShellStore((store) => store.sidebarWidth); return (
{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 }) { const open = appShellStore((store) => store.sidebarOpen); const close = appShellStore((store) => store.closeSidebar); const ref = useClickOutside(close, ["mouseup", "touchend"]); //, [document.getElementById("header")]); 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 ( <>
); } const SidebarResize = () => { const setSidebarWidth = appShellStore((store) => store.setSidebarWidth); const [isResizing, setIsResizing] = useState(false); const [startX, setStartX] = useState(0); const [startWidth, setStartWidth] = useState(0); const handleMouseDown = (e: React.MouseEvent) => { e.preventDefault(); setIsResizing(true); setStartX(e.clientX); setStartWidth( Number.parseInt( getComputedStyle(document.getElementById("app-shell")!) .getPropertyValue("--sidebar-width") .replace("px", ""), ), ); }; const handleMouseMove = (e: MouseEvent) => { if (!isResizing) return; const diff = e.clientX - startX; const newWidth = clampNumber(startWidth + diff, 250, window.innerWidth * 0.5); 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, startX, startWidth]); 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 Scrollable({ children, initialOffset = 64, }: { children: React.ReactNode; initialOffset?: number; }) { const scrollRef = useRef>(null); const [offset, setOffset] = useState(initialOffset); function updateHeaderHeight() { if (scrollRef.current) { setOffset(scrollRef.current.offsetTop); } } 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";