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

85.18% Statements 69/81
73.91% Branches 51/69
100% Functions 11/11
91.78% Lines 67/73

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      2x     50x 50x 50x 50x 50x     50x 50x     50x 37x 37x 37x 3x 3x   3x   3x     37x 37x 37x         50x 37x 37x 37x 13x   24x   37x 37x         50x 22x 22x   23x   23x 23x 17x     17x   125x   125x   125x 125x         125x                   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       50x                  
import { useState, useEffect } from "react";
import { Logger } from "@shared/logger";
 
const logger = new Logger("MobileLayout");
 
export function useMobileLayout() {
  const isClient = globalThis.window !== undefined;
  const mqQuery = "(max-width: 768px)";
  const initialIsMobile = isClient ? globalThis.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 = globalThis.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
    mq.addEventListener("change", onChange as any);
    return () => {
      mq.removeEventListener("change", 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
      hdr ??= document.querySelector("header");
      if (!hdr) {
        const all = Array.from<HTMLElement>(
          document.body.querySelectorAll<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 > globalThis.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 < globalThis.innerHeight / 2)
          h = Math.ceil(rect.height);
      }
      setHeaderHeight(h);
 
      let z = 0;
      if (hdr) {
        const zStr = getComputedStyle(hdr as HTMLElement).zIndex;
        const zNum = Number.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?.tagName ?? "null"} headerHeight=${h} overlayZ=${chosenZ}`,
      );
    };
 
    measure();
    globalThis.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 () => {
        globalThis.removeEventListener("resize", measure);
        obs.disconnect();
      };
    }
    return () => {
      globalThis.removeEventListener("resize", measure);
    };
  }, [isClient]);
 
  return {
    isMobile,
    mobilePanel,
    setMobilePanel,
    headerHeight,
    overlayZ,
  };
}