snapshot of https://github.com/shadcn-ui/ui.git (main, 2026-09-15; blobs > 131072B omitted)
This commit is contained in:
163
apps/v4/hooks/use-layout.tsx
Normal file
163
apps/v4/hooks/use-layout.tsx
Normal file
@@ -0,0 +1,163 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
type Layout = "fixed" | "full"
|
||||
|
||||
interface LayoutProviderProps {
|
||||
children: React.ReactNode
|
||||
defaultLayout?: Layout
|
||||
forcedLayout?: Layout
|
||||
storageKey?: string
|
||||
attribute?: string | string[]
|
||||
value?: Record<string, string>
|
||||
}
|
||||
|
||||
interface LayoutProviderState {
|
||||
layout: Layout
|
||||
setLayout: (layout: Layout | ((prev: Layout) => Layout)) => void
|
||||
forcedLayout?: Layout
|
||||
}
|
||||
|
||||
const isServer = typeof window === "undefined"
|
||||
const LayoutContext = React.createContext<LayoutProviderState | undefined>(
|
||||
undefined
|
||||
)
|
||||
|
||||
const saveToLS = (storageKey: string, value: string) => {
|
||||
try {
|
||||
localStorage.setItem(storageKey, value)
|
||||
} catch {
|
||||
// Unsupported
|
||||
}
|
||||
}
|
||||
|
||||
const useLayout = () => {
|
||||
const context = React.useContext(LayoutContext)
|
||||
if (context === undefined) {
|
||||
throw new Error("useLayout must be used within a LayoutProvider")
|
||||
}
|
||||
return context
|
||||
}
|
||||
|
||||
const Layout = ({
|
||||
forcedLayout,
|
||||
storageKey = "layout",
|
||||
defaultLayout = "full",
|
||||
attribute = "class",
|
||||
value,
|
||||
children,
|
||||
}: LayoutProviderProps) => {
|
||||
const [layout, setLayoutState] = React.useState<Layout>(() => {
|
||||
if (isServer) return defaultLayout
|
||||
try {
|
||||
const saved = localStorage.getItem(storageKey)
|
||||
if (saved === "fixed" || saved === "full") {
|
||||
return saved
|
||||
}
|
||||
return defaultLayout
|
||||
} catch {
|
||||
return defaultLayout
|
||||
}
|
||||
})
|
||||
|
||||
const attrs = React.useMemo(
|
||||
() => (!value ? ["layout-fixed", "layout-full"] : Object.values(value)),
|
||||
[value]
|
||||
)
|
||||
|
||||
const applyLayout = React.useCallback(
|
||||
(layout: Layout) => {
|
||||
if (!layout) return
|
||||
|
||||
const name = value ? value[layout] : `layout-${layout}`
|
||||
const d = document.documentElement
|
||||
|
||||
const handleAttribute = (attr: string) => {
|
||||
if (attr === "class") {
|
||||
d.classList.remove(...attrs)
|
||||
if (name) d.classList.add(name)
|
||||
} else if (attr.startsWith("data-")) {
|
||||
if (name) {
|
||||
d.setAttribute(attr, name)
|
||||
} else {
|
||||
d.removeAttribute(attr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(attribute)) attribute.forEach(handleAttribute)
|
||||
else handleAttribute(attribute)
|
||||
},
|
||||
[attrs, attribute, value]
|
||||
)
|
||||
|
||||
const setLayout = React.useCallback(
|
||||
(value: Layout | ((prev: Layout) => Layout)) => {
|
||||
if (typeof value === "function") {
|
||||
setLayoutState((prevLayout) => {
|
||||
const newLayout = value(prevLayout)
|
||||
saveToLS(storageKey, newLayout)
|
||||
return newLayout
|
||||
})
|
||||
} else {
|
||||
setLayoutState(value)
|
||||
saveToLS(storageKey, value)
|
||||
}
|
||||
},
|
||||
[storageKey]
|
||||
)
|
||||
|
||||
// localStorage event handling
|
||||
React.useEffect(() => {
|
||||
const handleStorage = (e: StorageEvent) => {
|
||||
if (e.key !== storageKey) return
|
||||
|
||||
if (!e.newValue) {
|
||||
setLayout(defaultLayout)
|
||||
} else if (e.newValue === "fixed" || e.newValue === "full") {
|
||||
setLayoutState(e.newValue)
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("storage", handleStorage)
|
||||
return () => window.removeEventListener("storage", handleStorage)
|
||||
}, [setLayout, storageKey, defaultLayout])
|
||||
|
||||
// Apply layout on mount and when it changes
|
||||
React.useEffect(() => {
|
||||
const currentLayout = forcedLayout ?? layout
|
||||
applyLayout(currentLayout)
|
||||
}, [forcedLayout, layout, applyLayout])
|
||||
|
||||
// Prevent layout changes during hydration
|
||||
const [isHydrated, setIsHydrated] = React.useState(false)
|
||||
React.useEffect(() => {
|
||||
setIsHydrated(true)
|
||||
}, [])
|
||||
|
||||
const providerValue = React.useMemo(
|
||||
() => ({
|
||||
layout: isHydrated ? layout : defaultLayout,
|
||||
setLayout,
|
||||
forcedLayout,
|
||||
}),
|
||||
[layout, setLayout, forcedLayout, isHydrated, defaultLayout]
|
||||
)
|
||||
|
||||
return (
|
||||
<LayoutContext.Provider value={providerValue}>
|
||||
{children}
|
||||
</LayoutContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
const LayoutProvider = (props: LayoutProviderProps) => {
|
||||
const context = React.useContext(LayoutContext)
|
||||
|
||||
// Ignore nested context providers, just passthrough children
|
||||
if (context) return <>{props.children}</>
|
||||
return <Layout {...props} />
|
||||
}
|
||||
|
||||
export { useLayout, LayoutProvider }
|
||||
Reference in New Issue
Block a user