chore(dev): react-grab viewport sync helper for in-page debug toolbar
Mounts a dev-only client component that syncs the react-grab debug toolbar's pinned edge / collapsed state across viewport changes (so the toolbar doesn't drift off-screen when resizing or rotating). Render is gated by NODE_ENV === 'development' in src/app/layout.tsx; production builds tree-shake the import out via process.env replacement. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,7 @@ import { headers } from 'next/headers';
|
||||
import { Inter, JetBrains_Mono } from 'next/font/google';
|
||||
import { Toaster } from 'sonner';
|
||||
import { classifyFormFactor } from '@/lib/form-factor';
|
||||
import { ReactGrabViewportSync } from '@/components/dev/react-grab-viewport-sync';
|
||||
import './globals.css';
|
||||
|
||||
const inter = Inter({
|
||||
@@ -66,6 +67,7 @@ export default async function RootLayout({ children }: { children: React.ReactNo
|
||||
>
|
||||
{children}
|
||||
<Toaster richColors position="top-right" />
|
||||
{process.env.NODE_ENV === 'development' && <ReactGrabViewportSync />}
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
104
src/components/dev/react-grab-viewport-sync.tsx
Normal file
104
src/components/dev/react-grab-viewport-sync.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect } from 'react';
|
||||
|
||||
type Edge = 'top' | 'bottom' | 'left' | 'right';
|
||||
|
||||
interface ToolbarState {
|
||||
edge: Edge;
|
||||
ratio: number;
|
||||
collapsed: boolean;
|
||||
enabled: boolean;
|
||||
defaultAction?: string;
|
||||
}
|
||||
|
||||
interface ReactGrabAPI {
|
||||
setToolbarState: (state: Partial<ToolbarState>) => void;
|
||||
onToolbarStateChange: (cb: (state: ToolbarState) => void) => () => void;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
__REACT_GRAB__?: ReactGrabAPI;
|
||||
}
|
||||
}
|
||||
|
||||
const MOBILE_QUERY = '(max-width: 1023.98px)';
|
||||
const DESKTOP_KEY = 'react-grab-toolbar-state-desktop';
|
||||
const MOBILE_KEY = 'react-grab-toolbar-state-mobile';
|
||||
|
||||
const DESKTOP_DEFAULT: Partial<ToolbarState> = {
|
||||
edge: 'bottom',
|
||||
ratio: 0.5,
|
||||
collapsed: false,
|
||||
};
|
||||
|
||||
const MOBILE_DEFAULT: Partial<ToolbarState> = {
|
||||
edge: 'right',
|
||||
ratio: 0.5,
|
||||
collapsed: false,
|
||||
};
|
||||
|
||||
export function ReactGrabViewportSync() {
|
||||
useEffect(() => {
|
||||
if (process.env.NODE_ENV !== 'development') return;
|
||||
|
||||
const cleanups: Array<() => void> = [];
|
||||
let pollId: number | undefined;
|
||||
|
||||
const wireUp = (api: ReactGrabAPI) => {
|
||||
const mql = window.matchMedia(MOBILE_QUERY);
|
||||
const keyFor = () => (mql.matches ? MOBILE_KEY : DESKTOP_KEY);
|
||||
const defaultFor = () => (mql.matches ? MOBILE_DEFAULT : DESKTOP_DEFAULT);
|
||||
|
||||
let suppressNextWrite = false;
|
||||
const apply = () => {
|
||||
const stored = localStorage.getItem(keyFor());
|
||||
suppressNextWrite = true;
|
||||
api.setToolbarState(stored ? (JSON.parse(stored) as ToolbarState) : defaultFor());
|
||||
};
|
||||
|
||||
apply();
|
||||
|
||||
const unsubscribe = api.onToolbarStateChange((state) => {
|
||||
if (suppressNextWrite) {
|
||||
suppressNextWrite = false;
|
||||
return;
|
||||
}
|
||||
localStorage.setItem(keyFor(), JSON.stringify(state));
|
||||
});
|
||||
|
||||
mql.addEventListener('change', apply);
|
||||
cleanups.push(unsubscribe, () => mql.removeEventListener('change', apply));
|
||||
};
|
||||
|
||||
const tryWire = () => {
|
||||
const api = window.__REACT_GRAB__;
|
||||
if (!api) return false;
|
||||
wireUp(api);
|
||||
return true;
|
||||
};
|
||||
|
||||
if (!tryWire()) {
|
||||
pollId = window.setInterval(() => {
|
||||
if (tryWire() && pollId !== undefined) {
|
||||
window.clearInterval(pollId);
|
||||
pollId = undefined;
|
||||
}
|
||||
}, 100);
|
||||
window.setTimeout(() => {
|
||||
if (pollId !== undefined) {
|
||||
window.clearInterval(pollId);
|
||||
pollId = undefined;
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (pollId !== undefined) window.clearInterval(pollId);
|
||||
cleanups.forEach((fn) => fn());
|
||||
};
|
||||
}, []);
|
||||
|
||||
return null;
|
||||
}
|
||||
Reference in New Issue
Block a user