Files
margin-docs/src/useMedia.ts
T
2026-08-28 09:22:58 +05:30

64 lines
2.4 KiB
TypeScript

import { useEffect, useState } from "react";
export function useMediaQuery(query: string): boolean {
const [matches, setMatches] = useState(() => window.matchMedia(query).matches);
useEffect(() => {
const mq = window.matchMedia(query);
const onChange = () => setMatches(mq.matches);
onChange();
mq.addEventListener("change", onChange);
return () => mq.removeEventListener("change", onChange);
}, [query]);
return matches;
}
/**
* Responsiveness is JS-driven: this writes `data-compact` on the root and the stylesheet keys off
* that attribute, so there are essentially no media queries in app.css.
*/
export function useCompact(): boolean {
const compact = useMediaQuery("(max-width: 899px)");
useEffect(() => {
document.documentElement.toggleAttribute("data-compact", compact);
}, [compact]);
return compact;
}
/** Kept in step with the same two queries in index.html's boot script. */
export const PHONE_QUERY = "(max-width: 640px)";
export const TOUCH_QUERY = "(pointer: coarse)";
/**
* A phone, meaning a window too narrow for the sidebar, the document and the sticky toolbar to
* all be worth showing at once. The chrome collapses and the sidebar becomes an overlay instead
* of a permanent column.
*
* Deliberately measured in CSS pixels rather than sniffed off the user agent: a phone in landscape
* is 850 wide and wants the desktop layout back, a tablet at 768 has always wanted it, and a
* narrow desktop window is a free way to exercise all of this without a device.
*/
export function usePhone(): boolean {
const phone = useMediaQuery(PHONE_QUERY);
useEffect(() => {
document.documentElement.toggleAttribute("data-phone", phone);
}, [phone]);
return phone;
}
/**
* A coarse pointer, which is a different question from `usePhone`: a tablet is touch and not a
* phone, and a narrow desktop window is a phone and not touch.
*
* This is the one that governs interaction rather than layout. Anything that only appears on hover
* is unreachable here and has to have a tap that does the same job, and a gesture that starts on
* pointerdown has to wait for a long press instead, because a finger cannot press without also
* intending to maybe scroll.
*/
export function useTouch(): boolean {
const touch = useMediaQuery(TOUCH_QUERY);
useEffect(() => {
document.documentElement.toggleAttribute("data-touch", touch);
}, [touch]);
return touch;
}