All files / web/src/providers side-bar.tsx

93.45% Statements 100/107
81.81% Branches 18/22
100% Functions 3/3
93.45% Lines 100/107

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155    1x 1x 1x 1x   1x 1x 1x 1x 1x                           1x                   1x 23x 23x 23x 23x 23x 23x 23x 23x 23x 23x 23x     23x 23x 23x 23x 4x 4x 1x 4x 3x 3x     4x   4x             4x   4x 4x 4x     4x 23x 23x     23x 3x 23x     23x 16x 1x 1x 1x 1x 1x 1x 1x 1x   16x 16x 23x       23x   23x 23x 17x 17x 17x 17x 17x 17x 17x 17x 17x 17x 23x 23x   23x 23x 23x 23x 23x 23x 23x 23x 23x 23x 23x   23x 23x 23x 23x 23x   23x 23x 23x 23x   23x   1x 18x 18x 2x 2x   15x 15x  
'use client';
 
import { TooltipProvider } from '@radix-ui/react-tooltip';
import { cn } from '@web/utils/ui/utils';
import React from 'react';
import { useIsMobile } from '../hooks/use-mobile';
 
const SIDEBAR_COOKIE_NAME = 'sidebar_state';
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
const SIDEBAR_WIDTH = '16rem';
const SIDEBAR_WIDTH_ICON = '3rem';
const SIDEBAR_KEYBOARD_SHORTCUT = 'b';
 
interface SidebarContextProps {
  state: 'expanded' | 'collapsed';
  open: boolean;
  setOpen: (open: boolean) => void;
  openMobile: boolean;
  setOpenMobile: (open: boolean) => void;
  isMobile: boolean;
  toggleSidebar: () => void;
  openedSections: string[];
  setOpenedSections: (sections: string[]) => void;
}
 
const SidebarContext = React.createContext<SidebarContextProps | null>(null);
 
interface SidebarProviderProps extends React.ComponentProps<'div'> {
  defaultOpen?: boolean;
  open?: boolean;
  onOpenChange?: (open: boolean) => void;
  className?: string;
  style?: React.CSSProperties;
}
 
export function SidebarProvider({
  defaultOpen = true,
  open: openProp,
  onOpenChange: onOpenChangeProp,
  className,
  style,
  children,
  ...props
}: SidebarProviderProps): React.ReactNode {
  const isMobile = useIsMobile();
  const [openMobile, setOpenMobile] = React.useState(false);
  const [openedSections, setOpenedSections] = React.useState<string[]>([]);
  // 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(
    (value: boolean | ((value: boolean) => boolean)) => {
      const openState = typeof value === 'function' ? value(open) : value;
      if (onOpenChangeProp) {
        onOpenChangeProp(openState);
      } else {
        _setOpen(openState);
      }
 
      // This sets the cookie to keep the sidebar state.
      try {
        // Prefer Cookie Store API when available
        if (window?.cookieStore?.set) {
          window.cookieStore.set({
            name: SIDEBAR_COOKIE_NAME,
            value: String(openState),
            expires: Date.now() + SIDEBAR_COOKIE_MAX_AGE * 1000,
            path: '/',
          });
        } else {
          // biome-ignore lint/suspicious/noDocumentCookie: Fallback for browsers without Cookie Store API
          document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
        }
      } catch {
        // ignore
      }
    },
    [onOpenChangeProp, open],
  );
 
  // Helper to toggle the sidebar.
  const toggleSidebar = React.useCallback(() => {
    return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open);
  }, [isMobile, setOpen]);
 
  // Adds a keyboard shortcut to toggle the sidebar.
  React.useEffect(() => {
    const handleKeyDown = (event: KeyboardEvent): void => {
      if (
        event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
        (event.metaKey || event.ctrlKey)
      ) {
        event.preventDefault();
        toggleSidebar();
      }
    };
 
    window.addEventListener('keydown', handleKeyDown);
    return (): void => window.removeEventListener('keydown', handleKeyDown);
  }, [toggleSidebar]);
 
  // 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 = open ? 'expanded' : 'collapsed';
 
  const contextValue = React.useMemo<SidebarContextProps>(
    () => ({
      state,
      open,
      setOpen,
      isMobile,
      openMobile,
      setOpenMobile,
      toggleSidebar,
      openedSections,
      setOpenedSections,
    }),
    [state, open, setOpen, isMobile, openMobile, toggleSidebar, openedSections],
  );
 
  return (
    <SidebarContext.Provider value={contextValue}>
      <TooltipProvider delayDuration={0}>
        <div
          data-slot="sidebar-wrapper"
          style={
            {
              '--sidebar-width': SIDEBAR_WIDTH,
              '--sidebar-width-icon': SIDEBAR_WIDTH_ICON,
              ...style,
            } as React.CSSProperties
          }
          className={cn(
            'group/sidebar-wrapper has-data-[variant=inset]:bg-sidebar flex min-h-svh w-full',
            className,
          )}
          {...props}
        >
          {children}
        </div>
      </TooltipProvider>
    </SidebarContext.Provider>
  );
}
 
export const useSidebar = (): SidebarContextProps => {
  const context = React.useContext(SidebarContext);
  if (!context) {
    throw new Error('useSidebar must be used within a SidebarProvider.');
  }
 
  return context;
};