// Shared constants and pure helpers for the Dispatch board and its // sub-components. Lives outside the page so the host component and the child // views cannot form a circular import. // // Ported from the Express Console's `dispatch/dispatchShared.js`. The geometry // and the Kalman/RTS smoother are carried over verbatim — their tuning was // arrived at against real two-wheeler GPS traces, and re-deriving it would be // guesswork dressed up as a rewrite. import { useState, useEffect } from 'react'; // Replaces MUI's `useMediaQuery(theme.breakpoints.down('md'))`, which was the // last MUI dependency in Preview.js and CompareDataPanel.js. MUI's `md` // breakpoint is 900px and `down()` is exclusive, so this is the identical // `max-width: 899.95px` query it generated — the same width the scoped media // blocks in Dispatch.css already key off. const NARROW_VIEWPORT_QUERY = '(max-width: 899.95px)'; export function useIsNarrowViewport() { const [isNarrow, setIsNarrow] = useState( () => typeof window !== 'undefined' && !!window.matchMedia && window.matchMedia(NARROW_VIEWPORT_QUERY).matches ); useEffect(() => { if (typeof window === 'undefined' || !window.matchMedia) return undefined; const mql = window.matchMedia(NARROW_VIEWPORT_QUERY); const onChange = (e) => setIsNarrow(e.matches); // Re-sync on mount in case the viewport changed between the lazy initial // state and the effect running. setIsNarrow(mql.matches); mql.addEventListener('change', onChange); return () => mql.removeEventListener('change', onChange); }, []); return isNarrow; } // Enter/Space activation for div/li surfaces that carry an onClick. // // The dispatch UI is built from clickable cards and list rows that wrap // headings, badges and bars, so they can't become real