|
@@ -1,60 +1,106 @@
|
|
|
import * as React from 'react'
|
|
import * as React from 'react'
|
|
|
import { cn } from '@/lib/utils'
|
|
import { cn } from '@/lib/utils'
|
|
|
|
|
|
|
|
|
|
+const LayoutContext = React.createContext<{
|
|
|
|
|
+ offset: number
|
|
|
|
|
+ fixed: boolean
|
|
|
|
|
+} | null>(null)
|
|
|
|
|
+
|
|
|
interface LayoutProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
interface LayoutProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
|
- fadedBelow?: boolean
|
|
|
|
|
- fixedHeight?: boolean
|
|
|
|
|
|
|
+ fixed?: boolean
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-const Layout = React.forwardRef<HTMLDivElement, LayoutProps>(
|
|
|
|
|
- ({ className, fadedBelow = false, fixedHeight = false, ...props }, ref) => (
|
|
|
|
|
- <div
|
|
|
|
|
- ref={ref}
|
|
|
|
|
- className={cn(
|
|
|
|
|
- 'relative flex h-full w-full flex-col',
|
|
|
|
|
- fadedBelow &&
|
|
|
|
|
- 'after:pointer-events-none after:absolute after:bottom-0 after:left-0 after:hidden after:h-32 after:w-full after:bg-[linear-gradient(180deg,_transparent_10%,_hsl(var(--background))_70%)] after:md:block',
|
|
|
|
|
- fixedHeight && 'md:h-svh',
|
|
|
|
|
- className
|
|
|
|
|
- )}
|
|
|
|
|
- {...props}
|
|
|
|
|
- />
|
|
|
|
|
|
|
+const Layout = ({ className, fixed = false, ...props }: LayoutProps) => {
|
|
|
|
|
+ const divRef = React.useRef<HTMLDivElement>(null)
|
|
|
|
|
+ const [offset, setOffset] = React.useState(0)
|
|
|
|
|
+
|
|
|
|
|
+ React.useEffect(() => {
|
|
|
|
|
+ const div = divRef.current
|
|
|
|
|
+
|
|
|
|
|
+ if (!div) return
|
|
|
|
|
+ const onScroll = () => setOffset(div.scrollTop)
|
|
|
|
|
+
|
|
|
|
|
+ // clean up code
|
|
|
|
|
+ div.removeEventListener('scroll', onScroll)
|
|
|
|
|
+ div.addEventListener('scroll', onScroll, { passive: true })
|
|
|
|
|
+ return () => div.removeEventListener('scroll', onScroll)
|
|
|
|
|
+ }, [])
|
|
|
|
|
+
|
|
|
|
|
+ return (
|
|
|
|
|
+ <LayoutContext.Provider value={{ offset, fixed }}>
|
|
|
|
|
+ <div
|
|
|
|
|
+ ref={divRef}
|
|
|
|
|
+ data-layout='layout'
|
|
|
|
|
+ className={cn(
|
|
|
|
|
+ 'h-full overflow-auto',
|
|
|
|
|
+ fixed && 'flex flex-col',
|
|
|
|
|
+ className
|
|
|
|
|
+ )}
|
|
|
|
|
+ {...props}
|
|
|
|
|
+ />
|
|
|
|
|
+ </LayoutContext.Provider>
|
|
|
)
|
|
)
|
|
|
-)
|
|
|
|
|
|
|
+}
|
|
|
Layout.displayName = 'Layout'
|
|
Layout.displayName = 'Layout'
|
|
|
|
|
|
|
|
-const LayoutHeader = React.forwardRef<
|
|
|
|
|
|
|
+interface HeaderProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
|
|
|
+ sticky?: boolean
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const Header = React.forwardRef<HTMLDivElement, HeaderProps>(
|
|
|
|
|
+ ({ className, sticky, ...props }, ref) => {
|
|
|
|
|
+ // Check if Layout.Header is used within Layout
|
|
|
|
|
+ const contextVal = React.useContext(LayoutContext)
|
|
|
|
|
+ if (contextVal === null) {
|
|
|
|
|
+ throw new Error(
|
|
|
|
|
+ `Layout.Header must be used within ${Layout.displayName}.`
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return (
|
|
|
|
|
+ <div
|
|
|
|
|
+ ref={ref}
|
|
|
|
|
+ data-layout='header'
|
|
|
|
|
+ className={cn(
|
|
|
|
|
+ `z-10 flex h-[var(--header-height)] items-center gap-4 bg-background p-4 md:px-8`,
|
|
|
|
|
+ contextVal.offset > 10 && sticky ? 'shadow' : 'shadow-none',
|
|
|
|
|
+ contextVal.fixed && 'flex-none',
|
|
|
|
|
+ sticky && 'sticky top-0',
|
|
|
|
|
+ className
|
|
|
|
|
+ )}
|
|
|
|
|
+ {...props}
|
|
|
|
|
+ />
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+)
|
|
|
|
|
+Header.displayName = 'Header'
|
|
|
|
|
+
|
|
|
|
|
+const Body = React.forwardRef<
|
|
|
HTMLDivElement,
|
|
HTMLDivElement,
|
|
|
React.HTMLAttributes<HTMLDivElement>
|
|
React.HTMLAttributes<HTMLDivElement>
|
|
|
->(({ className, ...props }, ref) => (
|
|
|
|
|
- <div
|
|
|
|
|
- ref={ref}
|
|
|
|
|
- className={cn(
|
|
|
|
|
- 'flex h-[var(--header-height)] flex-none items-center gap-4 bg-background p-4 md:px-8',
|
|
|
|
|
- className
|
|
|
|
|
- )}
|
|
|
|
|
- {...props}
|
|
|
|
|
- />
|
|
|
|
|
-))
|
|
|
|
|
-LayoutHeader.displayName = 'LayoutHeader'
|
|
|
|
|
-
|
|
|
|
|
-interface LayoutBodyProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
|
|
|
- fixedHeight?: boolean
|
|
|
|
|
-}
|
|
|
|
|
|
|
+>(({ className, ...props }, ref) => {
|
|
|
|
|
+ // Check if Layout.Body is used within Layout
|
|
|
|
|
+ const contextVal = React.useContext(LayoutContext)
|
|
|
|
|
+ if (contextVal === null) {
|
|
|
|
|
+ throw new Error(`Layout.Body must be used within ${Layout.displayName}.`)
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
-const LayoutBody = React.forwardRef<HTMLDivElement, LayoutBodyProps>(
|
|
|
|
|
- ({ className, fixedHeight, ...props }, ref) => (
|
|
|
|
|
|
|
+ return (
|
|
|
<div
|
|
<div
|
|
|
ref={ref}
|
|
ref={ref}
|
|
|
|
|
+ data-layout='body'
|
|
|
className={cn(
|
|
className={cn(
|
|
|
- 'flex-1 overflow-hidden px-4 py-6 md:px-8',
|
|
|
|
|
- fixedHeight && 'h-[calc(100%-var(--header-height))]',
|
|
|
|
|
|
|
+ 'px-4 py-6 md:overflow-hidden md:px-8',
|
|
|
|
|
+ contextVal && contextVal.fixed && 'flex-1',
|
|
|
className
|
|
className
|
|
|
)}
|
|
)}
|
|
|
{...props}
|
|
{...props}
|
|
|
/>
|
|
/>
|
|
|
)
|
|
)
|
|
|
-)
|
|
|
|
|
-LayoutBody.displayName = 'LayoutBody'
|
|
|
|
|
|
|
+})
|
|
|
|
|
+Body.displayName = 'Body'
|
|
|
|
|
+
|
|
|
|
|
+Layout.Header = Header
|
|
|
|
|
+Layout.Body = Body
|
|
|
|
|
|
|
|
-export { Layout, LayoutHeader, LayoutBody }
|
|
|
|
|
|
|
+export { Layout }
|