# All files / src/components/layout Sidebar.tsx

100% Statements 32/32
100% Branches 11/11
100% Functions 6/6
100% Lines 32/32

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

    2x

    2x

    2x

    2x

    3x
    3x
    3x
    1x
    1x
    1x

    2x

    2x
    2x

    3x
    3x

    3x
    3x
    3x
    1x

    2x
    2x
    2x

    2x
    2x
    2x
    2x

    2x
    2x
    1x

    3x
    1x

    2x

     import { useState, useRef, useEffect, type JSX, type ReactNode } from "react";
    import styles from "./Sidebar.module.scss";

    /** Default sidebar width in pixels. */
    const DEFAULT_SIDEBAR_WIDTH: number = 320;
    /** Minimum sidebar width in pixels. */
    const MIN_SIDEBAR_WIDTH: number = 220;
    /** Maximum sidebar width in pixels. */
    const MAX_SIDEBAR_WIDTH: number = 600;
    /** localStorage key for persisted sidebar width. */
    const STORAGE_KEY: string = "grackle-sidebar-width";

    /** Read persisted sidebar width from localStorage, falling back to the default. */
    function loadWidth(): number {
      try {
        const stored = localStorage.getItem(STORAGE_KEY);
        if (stored !== null) {
          const parsed = Number(stored);
          if (Number.isFinite(parsed) && parsed >= MIN_SIDEBAR_WIDTH && parsed <= MAX_SIDEBAR_WIDTH) {
            return parsed;
          }
        }
      } catch {
        // localStorage unavailable
      }
      return DEFAULT_SIDEBAR_WIDTH;
    }

    /** Persist sidebar width to localStorage. */
    function saveWidth(width: number): void {
      try {
        localStorage.setItem(STORAGE_KEY, String(width));
      } catch {
        // localStorage unavailable
      }
    }

    /** Props for the Sidebar component. */
    export interface SidebarProps {
      /** Content to render inside the sidebar slot. When undefined, the sidebar is hidden. */
      content: ReactNode | undefined;
    }

    /** Left sidebar rendering slot content passed via props. */
    export function Sidebar({ content }: SidebarProps): JSX.Element | undefined {
      const [width] = useState<number>(loadWidth);
      const containerRef = useRef<HTMLDivElement>(null);

      /** Observe container resizes and persist width to localStorage. */
      useEffect(() => {
        const element = containerRef.current;
        if (!element) {
          return;
        }

        const observer = new ResizeObserver((entries) => {
          for (const entry of entries) {
            const borderBox = entry.borderBoxSize[0];
            // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- borderBoxSize[0] may be undefined in some browsers
            if (borderBox) {
              const boxWidth = Math.round(borderBox.inlineSize);
              if (boxWidth >= MIN_SIDEBAR_WIDTH && boxWidth <= MAX_SIDEBAR_WIDTH) {
                saveWidth(boxWidth);
              }
            }
          }
        });

        observer.observe(element);
        return () => {
          observer.disconnect();
        };
      }, []);

      if (content === undefined) {
        return undefined;
      }

      return (
        <div className={styles.container} ref={containerRef} data-testid="sidebar" style={{ width }}>
          <div className={styles.content}>{content}</div>
        </div>
      );
    }