fix(17): WR-05 IN-03 IN-01 resize-aware useIsPhone hook; OidcRedirect side-effect in effect

This commit is contained in:
Lucas Berger
2026-06-18 13:40:00 -04:00
parent c2ceebf130
commit a4a7438641
6 changed files with 73 additions and 25 deletions
+50
View File
@@ -0,0 +1,50 @@
/**
* useIsPhone / useMediaQuery — resize-aware breakpoint hooks (WR-05, IN-03).
*
* The previous pattern read `window.matchMedia('(max-width: 767px)').matches`
* synchronously at render time with no `change` listener, so rotating an iPad
* across the 767px breakpoint (or resizing a desktop window) left the layout
* stale until an unrelated re-render. These hooks subscribe to the media query
* via `matchMedia.addEventListener('change', …)` so components re-render on
* breakpoint crossings.
*
* The phone breakpoint is centralised here (PHONE_MAX_QUERY) so the JS constant
* stays in one place. It corresponds to one pixel below `--bp-tablet: 768px`
* in tokens.css.
*/
import { useEffect, useState } from 'react';
/** Phone breakpoint: ≤767px (one below --bp-tablet: 768px). */
export const PHONE_MAX_QUERY = '(max-width: 767px)';
/**
* useMediaQuery — subscribe to a media query and re-render on change.
*
* SSR-safe: returns `false` when `window`/`matchMedia` is unavailable.
*/
export function useMediaQuery(query: string): boolean {
const [matches, setMatches] = useState<boolean>(() => {
if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return false;
return window.matchMedia(query).matches;
});
useEffect(() => {
if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return;
const mql = window.matchMedia(query);
// Sync immediately in case the query changed between render and effect.
setMatches(mql.matches);
const onChange = (e: MediaQueryListEvent) => setMatches(e.matches);
mql.addEventListener('change', onChange);
return () => mql.removeEventListener('change', onChange);
}, [query]);
return matches;
}
/**
* useIsPhone — true when the viewport is at or below the phone breakpoint.
* Re-renders on breakpoint crossings (resize / orientation change).
*/
export function useIsPhone(): boolean {
return useMediaQuery(PHONE_MAX_QUERY);
}