- useViewport: matchMedia-based hook (no resize polling). Breakpoints mobile <768 / tablet 768-1023 / desktop >=1024. SSR-safe. - useSidebarDrawer: Context provider with open/setOpen/toggle + auto-close on useLocation().pathname change. - App.tsx: wraps SidebarDrawerProvider around AppShell, renders a MobileBackdrop (z-30) when the drawer is open on mobile. - ProjectSidebar: aside is fixed/translate-x-full off-screen on mobile, slides in (z-40, 200ms transform) when drawerOpen. Inline column on desktop, unchanged. - Session.tsx + Project.tsx: hamburger (Menu icon, >=44x44 min) on mobile opens the drawer. Headers gain paddingTop: max(0.75rem, env(safe-area-inset-top)) for notch devices. Home.tsx left alone (sidebar content duplicates the home page). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
46 lines
1.3 KiB
TypeScript
46 lines
1.3 KiB
TypeScript
import { useEffect, useState } from 'react';
|
|
|
|
// Breakpoints (px): mobile <768, tablet 768-1023, desktop >=1024.
|
|
const MOBILE_MAX = 767;
|
|
const TABLET_MAX = 1023;
|
|
|
|
export interface ViewportSnapshot {
|
|
isMobile: boolean;
|
|
isTablet: boolean;
|
|
width: number;
|
|
}
|
|
|
|
function snapshot(): ViewportSnapshot {
|
|
if (typeof window === 'undefined') {
|
|
return { isMobile: false, isTablet: false, width: 1280 };
|
|
}
|
|
const width = window.innerWidth;
|
|
return {
|
|
isMobile: width <= MOBILE_MAX,
|
|
isTablet: width > MOBILE_MAX && width <= TABLET_MAX,
|
|
width,
|
|
};
|
|
}
|
|
|
|
// matchMedia-based, no resize polling. We listen to two breakpoint queries
|
|
// and recompute the snapshot on any change.
|
|
export function useViewport(): ViewportSnapshot {
|
|
const [state, setState] = useState<ViewportSnapshot>(snapshot);
|
|
|
|
useEffect(() => {
|
|
if (typeof window === 'undefined') return;
|
|
const mobileMq = window.matchMedia(`(max-width: ${MOBILE_MAX}px)`);
|
|
const tabletMq = window.matchMedia(`(min-width: ${MOBILE_MAX + 1}px) and (max-width: ${TABLET_MAX}px)`);
|
|
const update = () => setState(snapshot());
|
|
mobileMq.addEventListener('change', update);
|
|
tabletMq.addEventListener('change', update);
|
|
update();
|
|
return () => {
|
|
mobileMq.removeEventListener('change', update);
|
|
tabletMq.removeEventListener('change', update);
|
|
};
|
|
}, []);
|
|
|
|
return state;
|
|
}
|