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>
105 lines
2.5 KiB
TypeScript
105 lines
2.5 KiB
TypeScript
'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;
|
|
}
|