feat: pro: pane-aware responsiveness, scoped sidebar overlay, stable pane keys
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useCallback, useEffect, useSyncExternalStore } from "react";
|
||||
import { useUIStore } from "@/stores/ui-store";
|
||||
import { usePaneSize } from "@/hooks/use-pane-size";
|
||||
|
||||
// Tailwind v4 breakpoints
|
||||
const BREAKPOINTS = {
|
||||
@@ -38,12 +39,38 @@ export function useMediaQuery(query: string): boolean {
|
||||
return useSyncExternalStore(subscribe, getSnapshot, getMediaQueryServerSnapshot);
|
||||
}
|
||||
|
||||
/**
|
||||
* When the Pro shell renders a page inside a (possibly split) pane, that pane
|
||||
* publishes its measured width via `PaneSizeContext`. Inner pages should
|
||||
* branch their layout against the pane width — not the full viewport — so a
|
||||
* narrow pane gets the mobile/tablet layout instead of overflowing.
|
||||
*
|
||||
* Returns `null` when no pane size is published, signalling the caller to
|
||||
* fall back to `window.matchMedia`.
|
||||
*/
|
||||
function classifyPane(paneWidth: number | null) {
|
||||
if (paneWidth === null) return null;
|
||||
return {
|
||||
isMobile: paneWidth < BREAKPOINTS.md,
|
||||
isTablet: paneWidth >= BREAKPOINTS.md && paneWidth < BREAKPOINTS.lg,
|
||||
isDesktop: paneWidth >= BREAKPOINTS.lg,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to detect device type and sync with UI store
|
||||
* Uses Tailwind breakpoints: mobile < 768px, tablet 768-1024px, desktop > 1024px
|
||||
*
|
||||
* When invoked inside a Pro pane, the returned values reflect the pane's
|
||||
* width instead of the window's. The global UI store is NOT updated in that
|
||||
* case — two split panes would otherwise fight to write conflicting values,
|
||||
* and the store is meant to mirror the actual viewport for callers that read
|
||||
* it directly (mobile navigation helpers etc.).
|
||||
*/
|
||||
export function useDeviceDetection() {
|
||||
const { setDeviceType, isMobile, isTablet, isDesktop } = useUIStore();
|
||||
const paneWidth = usePaneSize();
|
||||
const paneClassification = classifyPane(paneWidth);
|
||||
|
||||
const isMobileQuery = useMediaQuery(`(max-width: ${BREAKPOINTS.md - 1}px)`);
|
||||
const isTabletQuery = useMediaQuery(
|
||||
@@ -52,9 +79,11 @@ export function useDeviceDetection() {
|
||||
const isDesktopQuery = useMediaQuery(`(min-width: ${BREAKPOINTS.lg}px)`);
|
||||
|
||||
useEffect(() => {
|
||||
if (paneClassification) return;
|
||||
setDeviceType(isMobileQuery, isTabletQuery, isDesktopQuery);
|
||||
}, [isMobileQuery, isTabletQuery, isDesktopQuery, setDeviceType]);
|
||||
}, [isMobileQuery, isTabletQuery, isDesktopQuery, setDeviceType, paneClassification]);
|
||||
|
||||
if (paneClassification) return paneClassification;
|
||||
return { isMobile, isTablet, isDesktop };
|
||||
}
|
||||
|
||||
@@ -62,19 +91,29 @@ export function useDeviceDetection() {
|
||||
* Convenience hooks for specific breakpoints
|
||||
*/
|
||||
export function useIsMobile() {
|
||||
return useMediaQuery(`(max-width: ${BREAKPOINTS.md - 1}px)`);
|
||||
const paneWidth = usePaneSize();
|
||||
const viewport = useMediaQuery(`(max-width: ${BREAKPOINTS.md - 1}px)`);
|
||||
return paneWidth !== null ? paneWidth < BREAKPOINTS.md : viewport;
|
||||
}
|
||||
|
||||
export function useIsTablet() {
|
||||
return useMediaQuery(
|
||||
const paneWidth = usePaneSize();
|
||||
const viewport = useMediaQuery(
|
||||
`(min-width: ${BREAKPOINTS.md}px) and (max-width: ${BREAKPOINTS.lg - 1}px)`
|
||||
);
|
||||
return paneWidth !== null
|
||||
? paneWidth >= BREAKPOINTS.md && paneWidth < BREAKPOINTS.lg
|
||||
: viewport;
|
||||
}
|
||||
|
||||
export function useIsDesktop() {
|
||||
return useMediaQuery(`(min-width: ${BREAKPOINTS.lg}px)`);
|
||||
const paneWidth = usePaneSize();
|
||||
const viewport = useMediaQuery(`(min-width: ${BREAKPOINTS.lg}px)`);
|
||||
return paneWidth !== null ? paneWidth >= BREAKPOINTS.lg : viewport;
|
||||
}
|
||||
|
||||
export function useBreakpoint(breakpoint: keyof typeof BREAKPOINTS) {
|
||||
return useMediaQuery(`(min-width: ${BREAKPOINTS[breakpoint]}px)`);
|
||||
const paneWidth = usePaneSize();
|
||||
const viewport = useMediaQuery(`(min-width: ${BREAKPOINTS[breakpoint]}px)`);
|
||||
return paneWidth !== null ? paneWidth >= BREAKPOINTS[breakpoint] : viewport;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user