All files / client/src/hooks use-mobile-layout.ts

86.04% Statements 74/86
75.34% Branches 55/73
100% Functions 11/11
92.2% Lines 71/77

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      2x     49x 49x 49x 49x 49x     49x 49x     49x 37x 37x 37x 3x 3x   3x   3x     37x 36x 1x 37x 37x 36x 1x         49x 37x 37x 37x 13x   24x   37x 37x         49x 22x 22x   23x   23x 23x 17x     17x   122x   122x   122x 122x         122x                   23x   23x 23x 6x 6x 6x   23x   23x 23x 6x 6x 6x   23x 23x 23x         22x 22x 22x 22x 4x 4x 4x 4x 4x     18x 18x       49x                  
import { useState, useEffect } from "react";
import { Logger } from "@shared/logger";
 
const logger = new Logger("MobileLayout");
 
export function useMobileLayout() {
  const isClient = typeof window !== "undefined";
  const mqQuery = "(max-width: 768px)";
  const initialIsMobile = isClient ? window.matchMedia(mqQuery).matches : false;
  const [isMobile, setIsMobile] = useState<boolean>(initialIsMobile);
  const [mobilePanel, setMobilePanel] = useState<"code" | "compile" | "serial" | "board" | null>(
    initialIsMobile ? "code" : null,
  );
  const [headerHeight, setHeaderHeight] = useState<number>(40);
  const [overlayZ, setOverlayZ] = useState<number>(30);
 
  // Media query listener for responsive layout
  useEffect(() => {
    Iif (!isClient) return;
    const mq = window.matchMedia(mqQuery);
    const onChange = (e: MediaQueryListEvent | MediaQueryList) => {
      const matches = "matches" in e ? e.matches : mq.matches;
      setIsMobile(matches);
      // If switching into mobile mode, open code panel immediately
      if (matches && !mobilePanel) setMobilePanel("code");
      // If switching out of mobile, close any mobile panel
      if (!matches) setMobilePanel(null);
    };
    // Modern browsers: addEventListener; fallback to addListener
    if (typeof mq.addEventListener === "function")
      mq.addEventListener("change", onChange as any);
    else mq.addListener(onChange as any);
    return () => {
      if (typeof mq.removeEventListener === "function")
        mq.removeEventListener("change", onChange as any);
      else mq.removeListener(onChange as any);
    };
  }, [isClient, mobilePanel]);
 
  // Prevent body scroll when mobile panel is open
  useEffect(() => {
    Iif (!isClient) return;
    const prev = document.body.style.overflow;
    if (mobilePanel) {
      document.body.style.overflow = "hidden";
    } else {
      document.body.style.overflow = prev || "";
    }
    return () => {
      document.body.style.overflow = prev || "";
    };
  }, [mobilePanel, isClient]);
 
  // Compute header height and overlay z-index
  useEffect(() => {
    Iif (!isClient) return;
    const measure = () => {
      // First try to find our mobile header by data attribute
      let hdr: Element | null = document.querySelector("[data-mobile-header]");
      // Fallback to <header> tag
      if (!hdr) hdr = document.querySelector("header");
      if (!hdr) {
        const all = Array.from(
          document.body.querySelectorAll("*"),
        ) as HTMLElement[];
        hdr =
          all.find((el) => {
            Iif (!el) return false;
            // ignore html/body
            Iif (el === document.body || el === document.documentElement)
              return false;
            const style = getComputedStyle(el);
            Eif (
              style.display === "none" ||
              style.visibility === "hidden" ||
              Number(style.opacity) === 0
            )
              return false;
            const r = el.getBoundingClientRect();
            // must be near the top and reasonably small (not full-page)
            if (r.top < -5 || r.top > 48) return false;
            if (r.height < 24 || r.height > window.innerHeight / 2)
              return false;
            return true;
          }) || null;
      }
 
      Iif (hdr === document.body || hdr === document.documentElement) hdr = null;
 
      let h = 40;
      if (hdr) {
        const rect = (hdr as HTMLElement).getBoundingClientRect();
        Eif (rect.height > 0 && rect.height < window.innerHeight / 2)
          h = Math.ceil(rect.height);
      }
      setHeaderHeight(h);
 
      let z = 0;
      if (hdr) {
        const zStr = getComputedStyle(hdr as HTMLElement).zIndex;
        const zNum = parseInt(zStr || "", 10);
        z = Number.isFinite(zNum) ? zNum : 0;
      }
      const chosenZ = z > 0 ? Math.max(z - 1, 5) : 30;
      setOverlayZ(chosenZ);
      logger.debug(
        `[mobile overlay] header detect: ${hdr} headerHeight=${h} overlayZ=${chosenZ}`,
      );
    };
 
    measure();
    window.addEventListener("resize", measure);
    const hdr = document.querySelector("header");
    if (hdr) {
      const obs = new MutationObserver(measure);
      obs.observe(hdr, { attributes: true, childList: true, subtree: true });
      return () => {
        window.removeEventListener("resize", measure);
        obs.disconnect();
      };
    }
    return () => {
      window.removeEventListener("resize", measure);
    };
  }, [isClient]);
 
  return {
    isMobile,
    mobilePanel,
    setMobilePanel,
    headerHeight,
    overlayZ,
  };
}