1041 lines · 33.7 KB · TypeScript
RawDownload
import { mergeProps } from "@base-ui/react/merge-props";import { useRender } from "@base-ui/react/use-render";import { cva, type VariantProps } from "class-variance-authority";import { PanelLeftCloseIcon, PanelLeftIcon } from "lucide-react";import * as React from "react";import { cn } from "~/lib/utils";import { Button } from "~/components/ui/button";import { Input } from "~/components/ui/input";import { ScrollArea } from "~/components/ui/scroll-area";import { Separator } from "~/components/ui/separator";import {  Sheet,  SheetDescription,  SheetHeader,  SheetPopup,  SheetTitle,} from "~/components/ui/sheet";import { Skeleton } from "~/components/ui/skeleton";import { Tooltip, TooltipPopup, TooltipTrigger } from "~/components/ui/tooltip";import { useIsMobile } from "~/hooks/useMediaQuery";import { getLocalStorageItem, setLocalStorageItem } from "~/hooks/useLocalStorage";import { resolveSidebarState, type ResponsiveSidebarState } from "./sidebarState";import * as Schema from "effect/Schema"; const SIDEBAR_COOKIE_NAME = "sidebar_state";const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;const SIDEBAR_WIDTH = "16rem";const SIDEBAR_WIDTH_MOBILE = "calc(100vw - var(--spacing(3)))";const SIDEBAR_WIDTH_ICON = "3rem";const SIDEBAR_RESIZE_DEFAULT_MIN_WIDTH = 16 * 16; type SidebarContextProps = {  state: ResponsiveSidebarState;  open: boolean;  setOpen: (open: boolean) => void;  openMobile: boolean;  setOpenMobile: (open: boolean) => void;  isMobile: boolean;  toggleSidebar: () => void;}; type SidebarResizableOptions = {  maxWidth?: number;  minWidth?: number;  onResize?: (width: number) => void;  shouldAcceptWidth?: (context: {    currentWidth: number;    nextWidth: number;    rail: HTMLButtonElement;    side: "left" | "right";    sidebarRoot: HTMLElement;    wrapper: HTMLElement;  }) => boolean;  storageKey?: string;}; type SidebarResolvedResizableOptions = {  maxWidth: number;  minWidth: number;  onResize?: (width: number) => void;  shouldAcceptWidth?: (context: {    currentWidth: number;    nextWidth: number;    rail: HTMLButtonElement;    side: "left" | "right";    sidebarRoot: HTMLElement;    wrapper: HTMLElement;  }) => boolean;  storageKey: string | null;}; type SidebarInstanceContextProps = {  resizable: SidebarResolvedResizableOptions | null;  side: "left" | "right";}; const SidebarContext = React.createContext<SidebarContextProps | null>(null);const SidebarInstanceContext = React.createContext<SidebarInstanceContextProps | null>(null); function useSidebar() {  const context = React.use(SidebarContext);  if (!context) {    throw new Error("useSidebar must be used within a SidebarProvider.");  }   return context;} function useSidebarVisibility() {  const { isMobile, open, openMobile } = useSidebar();  return isMobile ? openMobile : open;} function SidebarProvider({  defaultOpen = true,  open: openProp,  onOpenChange: setOpenProp,  className,  style,  children,  ...props}: React.ComponentProps<"div"> & {  defaultOpen?: boolean;  open?: boolean;  onOpenChange?: (open: boolean) => void;}) {  const isMobile = useIsMobile();  const [openMobile, setOpenMobile] = React.useState(false);   // This is the internal state of the sidebar.  // We use openProp and setOpenProp for control from outside the component.  const [_open, _setOpen] = React.useState(defaultOpen);  const open = openProp ?? _open;  const setOpen = React.useCallback(    async (value: boolean | ((value: boolean) => boolean)) => {      const openState = typeof value === "function" ? value(open) : value;      if (setOpenProp) {        setOpenProp(openState);      } else {        _setOpen(openState);      }       // This sets the cookie to keep the sidebar state.      await cookieStore.set({        expires: Date.now() + SIDEBAR_COOKIE_MAX_AGE * 1000,        name: SIDEBAR_COOKIE_NAME,        path: "/",        value: String(openState),      });    },    [setOpenProp, open],  );   // Helper to toggle the sidebar.  const toggleSidebar = React.useCallback(() => {    return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open);  }, [isMobile, setOpen]);   // We add a state so that we can do data-state="expanded" or "collapsed".  // This makes it easier to style the sidebar with Tailwind classes.  const state = resolveSidebarState({ isMobile, open, openMobile });   const contextValue = React.useMemo<SidebarContextProps>(    () => ({      isMobile,      open,      openMobile,      setOpen,      setOpenMobile,      state,      toggleSidebar,    }),    [state, open, setOpen, isMobile, openMobile, toggleSidebar],  );   return (    <SidebarContext value={contextValue}>      <div        className={cn(          "group/sidebar-wrapper flex min-h-svh w-full has-data-[variant=inset]:bg-sidebar",          className,        )}        data-sidebar-state={state}        data-slot="sidebar-wrapper"        style={          {            "--sidebar-width": SIDEBAR_WIDTH,            "--sidebar-width-icon": SIDEBAR_WIDTH_ICON,            ...style,          } as React.CSSProperties        }        {...props}      >        {children}      </div>    </SidebarContext>  );} function Sidebar({  side = "left",  variant = "sidebar",  collapsible = "offcanvas",  resizable = false,  className,  children,  ...props}: React.ComponentProps<"div"> & {  side?: "left" | "right";  variant?: "sidebar" | "floating" | "inset";  collapsible?: "offcanvas" | "icon" | "none";  resizable?: boolean | SidebarResizableOptions;}) {  const { isMobile, state, openMobile, setOpenMobile } = useSidebar();  const resolvedResizable = React.useMemo<SidebarResolvedResizableOptions | null>(() => {    if (isMobile || collapsible === "none" || !resizable) {      return null;    }     const options = typeof resizable === "boolean" ? {} : resizable;    return {      maxWidth: options.maxWidth ?? Number.POSITIVE_INFINITY,      minWidth: options.minWidth ?? SIDEBAR_RESIZE_DEFAULT_MIN_WIDTH,      storageKey: options.storageKey ?? null,      ...(options.onResize ? { onResize: options.onResize } : {}),      ...(options.shouldAcceptWidth ? { shouldAcceptWidth: options.shouldAcceptWidth } : {}),    };  }, [collapsible, isMobile, resizable]);  const instanceContextValue = React.useMemo<SidebarInstanceContextProps>(    () => ({ side, resizable: resolvedResizable }),    [resolvedResizable, side],  );   if (collapsible === "none") {    return (      <SidebarInstanceContext value={instanceContextValue}>        <div          className={cn(            "flex h-full w-(--sidebar-width) flex-col bg-sidebar surface-grain text-sidebar-foreground",            className,          )}          data-slot="sidebar"          {...props}        >          {children}        </div>      </SidebarInstanceContext>    );  }   if (isMobile) {    return (      <SidebarInstanceContext value={instanceContextValue}>        <Sheet onOpenChange={setOpenMobile} open={openMobile} {...props}>          <SheetPopup            className={cn(              "w-(--sidebar-width) max-w-none bg-sidebar surface-grain p-0 text-sidebar-foreground",              className,            )}            data-mobile="true"            data-sidebar="sidebar"            data-slot="sidebar"            showCloseButton={false}            side={side}            style={              {                "--sidebar-width": SIDEBAR_WIDTH_MOBILE,              } as React.CSSProperties            }          >            <SheetHeader className="sr-only">              <SheetTitle>Sidebar</SheetTitle>              <SheetDescription>Displays the mobile sidebar.</SheetDescription>            </SheetHeader>            <div              className={cn(                "flex h-full w-full flex-col pb-safe pt-safe",                side === "left" ? "pl-safe" : "pr-safe",              )}            >              {children}            </div>          </SheetPopup>        </Sheet>      </SidebarInstanceContext>    );  }   return (    <SidebarInstanceContext value={instanceContextValue}>      <div        className="group peer hidden text-sidebar-foreground md:block"        data-collapsible={state === "collapsed" ? collapsible : ""}        data-side={side}        data-slot="sidebar"        data-state={state}        data-variant={variant}      >        {/* This is what handles the sidebar gap on desktop */}        <div          className={cn(            "relative w-(--sidebar-width) bg-transparent transition-[width] duration-200 ease-linear",            "group-data-[collapsible=offcanvas]:w-0",            "group-data-[side=right]:rotate-180",            variant === "floating" || variant === "inset"              ? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]"              : "group-data-[collapsible=icon]:w-(--sidebar-width-icon)",          )}          data-slot="sidebar-gap"        />        <div          className={cn(            "fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear md:flex",            side === "left"              ? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]"              : "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]",            // Adjust the padding for floating and inset variants.            variant === "floating" || variant === "inset"              ? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]"              : "group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l",            className,          )}          data-slot="sidebar-container"          {...props}        >          <div            className="flex h-full w-full flex-col bg-sidebar surface-grain group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:border-sidebar-border group-data-[variant=floating]:shadow-sm/5"            data-sidebar="sidebar"            data-slot="sidebar-inner"          >            {children}          </div>        </div>      </div>    </SidebarInstanceContext>  );} function SidebarTrigger({ className, onClick, ...props }: React.ComponentProps<typeof Button>) {  const { toggleSidebar } = useSidebar();  const isOpen = useSidebarVisibility();   return (    <Button      className={cn(        "size-[var(--workspace-titlebar-control-size)]! [-webkit-app-region:no-drag]",        className,      )}      data-sidebar="trigger"      data-slot="sidebar-trigger"      aria-pressed={isOpen}      onClick={(event) => {        onClick?.(event);        toggleSidebar();      }}      size="icon"      variant="ghost"      {...props}    >      {isOpen ? <PanelLeftCloseIcon /> : <PanelLeftIcon />}      <span className="sr-only">Toggle Sidebar</span>    </Button>  );} function clampSidebarWidth(width: number, options: SidebarResolvedResizableOptions): number {  return Math.max(options.minWidth, Math.min(width, options.maxWidth));} function SidebarRail({  className,  onClick,  onPointerCancel,  onPointerDown,  onPointerMove,  onPointerUp,  ...props}: React.ComponentProps<"button">) {  const { open, toggleSidebar } = useSidebar();  const sidebarInstance = React.use(SidebarInstanceContext);  const railRef = React.useRef<HTMLButtonElement | null>(null);  const suppressClickRef = React.useRef(false);  const resizeStateRef = React.useRef<{    moved: boolean;    pointerId: number;    pendingWidth: number;    rail: HTMLButtonElement;    rafId: number | null;    sidebarRoot: HTMLElement;    side: "left" | "right";    startWidth: number;    startX: number;    transitionTargets: HTMLElement[];    width: number;    wrapper: HTMLElement;  } | null>(null);  const resolvedResizable = sidebarInstance?.resizable ?? null;  const canResize = resolvedResizable !== null && open;  const railLabel = canResize ? "Resize Sidebar" : "Toggle Sidebar";  const railTitle = canResize ? "Drag to resize sidebar" : "Toggle Sidebar";   const stopResize = React.useCallback(    (pointerId: number) => {      const resizeState = resizeStateRef.current;      if (!resizeState) {        return;      }      if (resizeState.rafId !== null) {        window.cancelAnimationFrame(resizeState.rafId);      }      resizeState.transitionTargets.forEach((element) => {        element.style.removeProperty("transition-duration");      });      if (resolvedResizable?.storageKey && typeof window !== "undefined") {        setLocalStorageItem(resolvedResizable.storageKey, resizeState.width, Schema.Finite);      }      resolvedResizable?.onResize?.(resizeState.width);      resizeStateRef.current = null;      if (resizeState.rail.hasPointerCapture(pointerId)) {        resizeState.rail.releasePointerCapture(pointerId);      }      document.body.style.removeProperty("cursor");      document.body.style.removeProperty("user-select");    },    [resolvedResizable],  );   const handlePointerDown = React.useCallback(    (event: React.PointerEvent<HTMLButtonElement>) => {      onPointerDown?.(event);      if (event.defaultPrevented) return;      if (!resolvedResizable || !open || event.button !== 0) return;       const wrapper = event.currentTarget.closest<HTMLElement>("[data-slot='sidebar-wrapper']");      const sidebarRoot = event.currentTarget.closest<HTMLElement>("[data-slot='sidebar']");      if (!wrapper || !sidebarRoot) {        return;      }       const sidebarContainer = sidebarRoot.querySelector<HTMLElement>(        "[data-slot='sidebar-container']",      );      if (!sidebarContainer) {        return;      }       const startWidth = sidebarContainer.getBoundingClientRect().width;      const initialWidth = clampSidebarWidth(startWidth, resolvedResizable);      const transitionTargets = [        sidebarRoot.querySelector<HTMLElement>("[data-slot='sidebar-gap']"),        sidebarRoot.querySelector<HTMLElement>("[data-slot='sidebar-container']"),      ].filter((element): element is HTMLElement => element !== null);      transitionTargets.forEach((element) => {        element.style.setProperty("transition-duration", "0ms");      });       event.preventDefault();      event.stopPropagation();      resizeStateRef.current = {        moved: false,        pointerId: event.pointerId,        pendingWidth: initialWidth,        rail: event.currentTarget,        rafId: null,        sidebarRoot,        side: sidebarInstance?.side ?? "left",        startWidth: initialWidth,        startX: event.clientX,        transitionTargets,        width: initialWidth,        wrapper,      };      wrapper.style.setProperty("--sidebar-width", `${initialWidth}px`);      event.currentTarget.setPointerCapture(event.pointerId);      document.body.style.cursor = "col-resize";      document.body.style.userSelect = "none";    },    [onPointerDown, open, resolvedResizable, sidebarInstance?.side],  );   const handlePointerMove = React.useCallback(    (event: React.PointerEvent<HTMLButtonElement>) => {      onPointerMove?.(event);      if (event.defaultPrevented) return;      const resizeState = resizeStateRef.current;      if (!resizeState || resizeState.pointerId !== event.pointerId || !resolvedResizable) return;       event.preventDefault();      const delta =        resizeState.side === "right"          ? resizeState.startX - event.clientX          : event.clientX - resizeState.startX;      if (Math.abs(delta) > 2) {        resizeState.moved = true;      }      resizeState.pendingWidth = clampSidebarWidth(        resizeState.startWidth + delta,        resolvedResizable,      );      if (resizeState.rafId !== null) {        return;      }       resizeState.rafId = window.requestAnimationFrame(() => {        const activeResizeState = resizeStateRef.current;        if (!activeResizeState || !resolvedResizable) return;         activeResizeState.rafId = null;        const nextWidth = activeResizeState.pendingWidth;        const accepted =          resolvedResizable.shouldAcceptWidth?.({            currentWidth: activeResizeState.width,            nextWidth,            rail: activeResizeState.rail,            side: activeResizeState.side,            sidebarRoot: activeResizeState.sidebarRoot,            wrapper: activeResizeState.wrapper,          }) ?? true;        if (!accepted) {          return;

Lines 1–500 of 1041.