snapshot of https://github.com/shadcn-ui/ui.git (main, 2026-09-15; blobs > 131072B omitted)
This commit is contained in:
185
apps/v4/app/(app)/(create)/hooks/use-history.tsx
Normal file
185
apps/v4/app/(app)/(create)/hooks/use-history.tsx
Normal file
@@ -0,0 +1,185 @@
|
||||
"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<HistoryContextValue | null>(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<string[]>([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<DesignSystemSearchParams>,
|
||||
{ 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<DesignSystemSearchParams>,
|
||||
{ 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 (
|
||||
<HistoryContext value={value}>
|
||||
<Suspense>
|
||||
<PresetSync onPresetChange={onPresetChange} />
|
||||
</Suspense>
|
||||
{children}
|
||||
</HistoryContext>
|
||||
)
|
||||
}
|
||||
|
||||
export function useHistory() {
|
||||
const context = React.useContext(HistoryContext)
|
||||
if (!context) {
|
||||
throw new Error("useHistory must be used within HistoryProvider")
|
||||
}
|
||||
return context
|
||||
}
|
||||
Reference in New Issue
Block a user