feat: scrollable layout
This commit is contained in:
parent
3579c8e4cf
commit
5590787f7f
|
|
@ -1,4 +1,40 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
|
import { LabScrollProvider, useLabScroll } from '@/contexts/lab-scroll-context';
|
||||||
|
|
||||||
|
const LabHeader = () => {
|
||||||
|
const { isScrolled } = useLabScroll();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<header
|
||||||
|
className={`
|
||||||
|
relative z-10 border-b border-white/10 backdrop-blur-sm shrink-0
|
||||||
|
transition-all duration-300 ease-in-out
|
||||||
|
${isScrolled ? 'h-0 opacity-0 overflow-hidden border-b-0' : 'h-16 opacity-100'}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<nav className="max-w-7xl mx-auto px-6 py-3 flex justify-between items-center h-16">
|
||||||
|
<div className="h-full flex items-center">
|
||||||
|
<Image
|
||||||
|
src="/banatie-logo-horisontal.png"
|
||||||
|
alt="Banatie Logo"
|
||||||
|
width={150}
|
||||||
|
height={40}
|
||||||
|
priority
|
||||||
|
className="h-full w-auto object-contain"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<a
|
||||||
|
href="#waitlist"
|
||||||
|
className="text-sm text-gray-300 hover:text-white transition-colors"
|
||||||
|
>
|
||||||
|
Join Beta
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default function LabLayout({
|
export default function LabLayout({
|
||||||
children,
|
children,
|
||||||
|
|
@ -6,29 +42,13 @@ export default function LabLayout({
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}>) {
|
}>) {
|
||||||
return (
|
return (
|
||||||
<>
|
<LabScrollProvider>
|
||||||
<header className="relative z-10 border-b border-white/10 backdrop-blur-sm">
|
<div className="h-screen overflow-hidden flex flex-col">
|
||||||
<nav className="max-w-7xl mx-auto px-6 py-3 flex justify-between items-center h-16">
|
<LabHeader />
|
||||||
<div className="h-full flex items-center">
|
<div className="flex-1 min-h-0">
|
||||||
<Image
|
{children}
|
||||||
src="/banatie-logo-horisontal.png"
|
</div>
|
||||||
alt="Banatie Logo"
|
</div>
|
||||||
width={150}
|
</LabScrollProvider>
|
||||||
height={40}
|
|
||||||
priority
|
|
||||||
className="h-full w-auto object-contain"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<a
|
|
||||||
href="#waitlist"
|
|
||||||
className="text-sm text-gray-300 hover:text-white transition-colors"
|
|
||||||
>
|
|
||||||
Join Beta
|
|
||||||
</a>
|
|
||||||
</nav>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
{children}
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,28 +19,55 @@
|
||||||
* - SubsectionNav at the top
|
* - SubsectionNav at the top
|
||||||
* - ApiKeyWidget in the right slot
|
* - ApiKeyWidget in the right slot
|
||||||
* - AnimatedBackground
|
* - AnimatedBackground
|
||||||
|
*
|
||||||
|
* Scroll Detection:
|
||||||
|
* - Detects scroll in the main content area
|
||||||
|
* - After 50px threshold, collapses the main header via LabScrollContext
|
||||||
|
* - Height adjusts dynamically: 100vh-7rem (header visible) → 100vh-3rem (header hidden)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ReactNode } from 'react';
|
import { ReactNode, useRef, useEffect, useCallback } from 'react';
|
||||||
import { ThreeColumnLayout } from '@/components/layout/ThreeColumnLayout';
|
import { ThreeColumnLayout } from '@/components/layout/ThreeColumnLayout';
|
||||||
import { LabSidebar } from './LabSidebar';
|
import { LabSidebar } from './LabSidebar';
|
||||||
import { LabFooter } from './LabFooter';
|
import { LabFooter } from './LabFooter';
|
||||||
|
import { useLabScroll } from '@/contexts/lab-scroll-context';
|
||||||
|
|
||||||
type LabLayoutProps = {
|
type LabLayoutProps = {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const SCROLL_THRESHOLD = 50;
|
||||||
|
|
||||||
export const LabLayout = ({ children }: LabLayoutProps) => {
|
export const LabLayout = ({ children }: LabLayoutProps) => {
|
||||||
|
const { isScrolled, setIsScrolled } = useLabScroll();
|
||||||
|
const contentRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const handleScroll = useCallback(() => {
|
||||||
|
if (!contentRef.current) return;
|
||||||
|
const scrollTop = contentRef.current.scrollTop;
|
||||||
|
setIsScrolled(scrollTop > SCROLL_THRESHOLD);
|
||||||
|
}, [setIsScrolled]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const contentElement = contentRef.current;
|
||||||
|
if (!contentElement) return;
|
||||||
|
|
||||||
|
contentElement.addEventListener('scroll', handleScroll);
|
||||||
|
return () => contentElement.removeEventListener('scroll', handleScroll);
|
||||||
|
}, [handleScroll]);
|
||||||
|
|
||||||
|
const containerHeight = isScrolled ? 'h-[calc(100vh-3rem)]' : 'h-[calc(100vh-7rem)]';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThreeColumnLayout
|
<ThreeColumnLayout
|
||||||
left={
|
left={
|
||||||
<div className="border-r border-white/10 bg-slate-950/50 backdrop-blur-sm sticky top-12 h-[calc(100vh-3rem)] overflow-y-auto">
|
<div className={`border-r border-white/10 bg-slate-950/50 backdrop-blur-sm ${containerHeight} overflow-y-auto transition-all duration-300`}>
|
||||||
<LabSidebar />
|
<LabSidebar />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
center={
|
center={
|
||||||
<div className="flex flex-col h-[calc(100vh-3rem)]">
|
<div className={`flex flex-col ${containerHeight} transition-all duration-300`}>
|
||||||
<div className="flex-1 overflow-y-auto">{children}</div>
|
<div ref={contentRef} className="flex-1 overflow-y-auto min-h-0">{children}</div>
|
||||||
<LabFooter />
|
<LabFooter />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lab Scroll Context
|
||||||
|
*
|
||||||
|
* Shares scroll state between the main content area and the header.
|
||||||
|
* Used to collapse the main header when user scrolls in the lab section.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { createContext, useContext, useState, ReactNode } from 'react';
|
||||||
|
|
||||||
|
type LabScrollContextValue = {
|
||||||
|
isScrolled: boolean;
|
||||||
|
setIsScrolled: (value: boolean) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const LabScrollContext = createContext<LabScrollContextValue | null>(null);
|
||||||
|
|
||||||
|
export const useLabScroll = () => {
|
||||||
|
const context = useContext(LabScrollContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useLabScroll must be used within LabScrollProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
|
|
||||||
|
type LabScrollProviderProps = {
|
||||||
|
children: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const LabScrollProvider = ({ children }: LabScrollProviderProps) => {
|
||||||
|
const [isScrolled, setIsScrolled] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<LabScrollContext.Provider value={{ isScrolled, setIsScrolled }}>
|
||||||
|
{children}
|
||||||
|
</LabScrollContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
Loading…
Reference in New Issue