"use client" import * as React from "react" import { Suspense } from "react" import { useSearchParams } from "next/navigation" import { useDesignSystemSearchParams, type DesignSystemSearchParams, } from "@/app/(app)/(create)/lib/search-params" type HistoryContextValue = { canGoBack: boolean canGoForward: boolean goBack: () => void goForward: () => void } const HistoryContext = React.createContext(null) // Reads useSearchParams() in its own Suspense boundary so the // provider never blanks out children while search params resolve. // useSearchParams reflects the *settled* preset, which coalesces the // transient values nuqs emits mid-update — reading nuqs state directly // here would record those transients as phantom history entries. function PresetSync({ onPresetChange, }: { onPresetChange: (preset: string) => void }) { const searchParams = useSearchParams() const preset = searchParams.get("preset") ?? "" React.useEffect(() => { onPresetChange(preset) }, [preset, onPresetChange]) return null } export function HistoryProvider({ children }: { children: React.ReactNode }) { // Write through the shared search-params hook (shallow) instead of // router.replace. router.replace triggers a full server navigation that // resets the preset on prod — the same failure the customizer avoids by // going shallow (#11060). const [, setParams] = useDesignSystemSearchParams() const [preset, setPreset] = React.useState("") const entriesRef = React.useRef([preset]) const indexRef = React.useRef(0) const maxIndexRef = React.useRef(0) const isNavigatingRef = React.useRef(false) const [index, setIndex] = React.useState(0) const [maxIndex, setMaxIndex] = React.useState(0) const onPresetChange = React.useCallback((nextPreset: string) => { setPreset(nextPreset) }, []) React.useEffect(() => { if (isNavigatingRef.current) { isNavigatingRef.current = false return } if (preset === entriesRef.current[indexRef.current]) { return } const nextEntries = entriesRef.current.slice(0, indexRef.current + 1) nextEntries.push(preset) entriesRef.current = nextEntries const nextIndex = nextEntries.length - 1 indexRef.current = nextIndex maxIndexRef.current = nextIndex setIndex(nextIndex) setMaxIndex(nextIndex) }, [preset]) const canGoBack = index > 0 const canGoForward = index < maxIndex const goBack = React.useCallback(() => { if (indexRef.current <= 0) { return } isNavigatingRef.current = true const nextIndex = indexRef.current - 1 indexRef.current = nextIndex setIndex(nextIndex) // The first history entry is "" (no preset in the URL); null clears the // param to restore that state. nuqs accepts null to clear a key, which the // wrapper's input type does not model. setParams( { preset: entriesRef.current[nextIndex] || null, } as Partial, { history: "replace" } ) }, [setParams]) const goForward = React.useCallback(() => { if (indexRef.current >= maxIndexRef.current) { return } isNavigatingRef.current = true const nextIndex = indexRef.current + 1 indexRef.current = nextIndex setIndex(nextIndex) // The first history entry is "" (no preset in the URL); null clears the // param to restore that state. nuqs accepts null to clear a key, which the // wrapper's input type does not model. setParams( { preset: entriesRef.current[nextIndex] || null, } as Partial, { history: "replace" } ) }, [setParams]) React.useEffect(() => { const down = (e: KeyboardEvent) => { if (!e.metaKey && !e.ctrlKey) { return } if ( (e.target instanceof HTMLElement && e.target.isContentEditable) || e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement || e.target instanceof HTMLSelectElement ) { return } const key = e.key.toLowerCase() if ((key === "z" && e.shiftKey) || (key === "y" && e.ctrlKey)) { e.preventDefault() goForward() return } if (key === "z") { e.preventDefault() goBack() } } document.addEventListener("keydown", down) return () => { document.removeEventListener("keydown", down) } }, [goBack, goForward]) const value = React.useMemo( () => ({ canGoBack, canGoForward, goBack, goForward }), [canGoBack, canGoForward, goBack, goForward] ) return ( {children} ) } export function useHistory() { const context = React.useContext(HistoryContext) if (!context) { throw new Error("useHistory must be used within HistoryProvider") } return context }