import { useEffect, useMemo, useState } from "react"; export const DEFAULT_PHONE_MAX = 768; export const DEFAULT_TABLET_MAX = 1024; type DeviceType = "phone" | "tablet" | "desktop"; type DeviceInfo = { isMobile: boolean; deviceType: DeviceType; // Señales útiles para decisiones finas de UX: widthNarrow: boolean; // <= phoneMax widthTablet: boolean; // > phoneMax && <= tabletMax pointerCoarse: boolean; // puntero táctil grueso hoverNone: boolean; // no hay hover maxTouchPoints: number; // Info de plataforma cuando está disponible platform?: string; model?: string; uaMobileHint?: boolean; // de UA/Client Hints si se puede }; type Options = { phoneMax?: number; // por defecto DEFAULT_PHONE_MAX tabletMax?: number; // por defecto DEFAULT_TABLET_MAX // Para SSR/Next: primera suposición que evitará saltos visuales serverGuess?: Partial>; }; /** Hook base para media queries */ function useMediaQuery(query: string, fallback = false) { const [matches, setMatches] = useState(fallback); useEffect(() => { if (typeof window === "undefined") return; // SSR const mql = window.matchMedia(query); const onChange = (e: MediaQueryListEvent) => setMatches(e.matches); setMatches(mql.matches); mql.addEventListener?.("change", onChange); return () => mql.removeEventListener?.("change", onChange); }, [query]); return matches; } /** Clasificador a partir de señales */ function classifyDevice( widthNarrow: boolean, widthTablet: boolean, pointerCoarse: boolean, hoverNone: boolean ): DeviceType { // Heurística práctica: if (pointerCoarse && (widthNarrow || hoverNone)) return "phone"; if (pointerCoarse && widthTablet) return "tablet"; return "desktop"; } export function useDeviceInfo(opts: Options = {}): DeviceInfo { const phoneMax = opts.phoneMax ?? DEFAULT_PHONE_MAX; const tabletMax = opts.tabletMax ?? DEFAULT_TABLET_MAX; // Inicialización segura para SSR para evitar hydration mismatch. // Si hay serverGuess, úsalo; si no, asume desktop hasta montar. const [initial, setInitial] = useState(() => { if (typeof window === "undefined") { const deviceType = opts.serverGuess?.deviceType ?? "desktop"; const isMobile = opts.serverGuess?.isMobile ?? deviceType !== "desktop"; return { isMobile, deviceType, widthNarrow: deviceType === "phone", widthTablet: deviceType === "tablet", pointerCoarse: deviceType !== "desktop", hoverNone: deviceType !== "desktop", maxTouchPoints: 0, }; } return null; // en cliente, calcularemos real en useEffect }); const widthNarrow = useMediaQuery(`(max-width: ${phoneMax}px)`, initial?.widthNarrow ?? false); const widthTablet = useMediaQuery( `(min-width: ${phoneMax + 1}px) and (max-width: ${tabletMax}px)`, initial?.widthTablet ?? false ); const pointerCoarse = useMediaQuery("(pointer: coarse)", initial?.pointerCoarse ?? false); const hoverNone = useMediaQuery("(hover: none)", initial?.hoverNone ?? false); const [maxTouchPoints, setMaxTouchPoints] = useState(initial?.maxTouchPoints ?? 0); const [uaHints, setUaHints] = useState<{ platform?: string; model?: string; mobile?: boolean }>( {} ); useEffect(() => { if (typeof window === "undefined") return; // maxTouchPoints es útil para detectar iPadOS que se disfraza de Mac setMaxTouchPoints(navigator.maxTouchPoints || 0); // Client Hints (Chromium). No bloquea si no existen. (async () => { const nav = navigator as any; if (nav.userAgentData?.getHighEntropyValues) { try { const { platform, mobile, model } = await nav.userAgentData.getHighEntropyValues([ "platform", "mobile", "model", ]); setUaHints({ platform, mobile, model }); } catch { // ignorar excepciones } } })(); }, []); const deviceType = useMemo(() => { // Si es Mac pero con touchPoints > 1 => probablemente iPadOS en modo desktop const forceTouch = typeof window !== "undefined" && /Macintosh/i.test(navigator.userAgent || "") && maxTouchPoints > 1; return classifyDevice( widthNarrow, widthTablet, pointerCoarse || forceTouch, hoverNone || forceTouch ); }, [widthNarrow, widthTablet, pointerCoarse, hoverNone, maxTouchPoints]); const isMobile = deviceType !== "desktop"; // Si teníamos estado inicial en SSR, al primer render en cliente lo reemplazamos por el real useEffect(() => { if (initial) setInitial(null); }, [initial]); return { isMobile, deviceType, widthNarrow, widthTablet, pointerCoarse, hoverNone, maxTouchPoints, platform: uaHints.platform, model: uaHints.model, uaMobileHint: uaHints.mobile, }; }