'use client'; import { useSyncExternalStore } from 'react'; // 3-tier breakpoints aligned with Tailwind: // mobile : < 768px (sm and below) // tablet : 768-1023 (md) // desktop : >= 1024 (lg and up) // // Previously the app used a binary `(max-width: 1023.98px)` split, which // rendered the mobile shell on iPad portrait AND on a half-screen 13" // Mac browser - neither is really "mobile." The tablet tier fills that // gap so the desktop shell can render with a hidden-but-accessible // sidebar at those widths. export type ViewportTier = 'mobile' | 'tablet' | 'desktop'; const TABLET_QUERY = '(min-width: 768px) and (max-width: 1023.98px)'; const DESKTOP_QUERY = '(min-width: 1024px)'; function subscribeTier(callback: () => void): () => void { const mqTablet = window.matchMedia(TABLET_QUERY); const mqDesktop = window.matchMedia(DESKTOP_QUERY); mqTablet.addEventListener('change', callback); mqDesktop.addEventListener('change', callback); return () => { mqTablet.removeEventListener('change', callback); mqDesktop.removeEventListener('change', callback); }; } function getTierSnapshot(): ViewportTier { if (window.matchMedia(DESKTOP_QUERY).matches) return 'desktop'; if (window.matchMedia(TABLET_QUERY).matches) return 'tablet'; return 'mobile'; } function getTierServerSnapshot(): ViewportTier { // Server has no window - default to desktop so the desktop shell // mounts on first paint. Client re-evaluates immediately on hydration // (useSyncExternalStore is server-mismatch-safe). return 'desktop'; } /** * Returns the active viewport tier. Backed by useSyncExternalStore so * render reads stay pure (no useEffect → setState cascade); React * Compiler-safe. */ export function useViewportTier(): ViewportTier { return useSyncExternalStore(subscribeTier, getTierSnapshot, getTierServerSnapshot); } // ─── Back-compat alias ────────────────────────────────────────────────────── // Every existing call site treats "mobile OR tablet" as one bucket (e.g. // "show short labels", "stack vertically"). Returning `tier !== 'desktop'` // preserves that behaviour so the tier rollout doesn't have to touch // dozens of components in lockstep. const LEGACY_QUERY = '(max-width: 1023.98px)'; function subscribeLegacy(callback: () => void): () => void { const mq = window.matchMedia(LEGACY_QUERY); mq.addEventListener('change', callback); return () => mq.removeEventListener('change', callback); } function getLegacySnapshot(): boolean { return window.matchMedia(LEGACY_QUERY).matches; } function getLegacyServerSnapshot(): boolean { return false; } /** * Returns true when the viewport is below the `lg` Tailwind breakpoint * (i.e. mobile OR tablet). Kept as an alias for backwards compatibility * with call sites that don't care about the mobile-vs-tablet distinction. * New code should prefer `useViewportTier()` for explicit tier checks. */ export function useIsMobile(): boolean { return useSyncExternalStore(subscribeLegacy, getLegacySnapshot, getLegacyServerSnapshot); }