feat: scrollable layout

This commit is contained in:
Oleg Proskurin 2025-11-30 01:54:06 +07:00
parent 3579c8e4cf
commit 5590787f7f
3 changed files with 114 additions and 28 deletions

View File

@ -1,4 +1,40 @@
'use client';
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({
children,
@ -6,29 +42,13 @@ export default function LabLayout({
children: React.ReactNode;
}>) {
return (
<>
<header className="relative z-10 border-b border-white/10 backdrop-blur-sm">
<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>
{children}
</>
<LabScrollProvider>
<div className="h-screen overflow-hidden flex flex-col">
<LabHeader />
<div className="flex-1 min-h-0">
{children}
</div>
</div>
</LabScrollProvider>
);
}

View File

@ -19,28 +19,55 @@
* - SubsectionNav at the top
* - ApiKeyWidget in the right slot
* - 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 { LabSidebar } from './LabSidebar';
import { LabFooter } from './LabFooter';
import { useLabScroll } from '@/contexts/lab-scroll-context';
type LabLayoutProps = {
children: ReactNode;
};
const SCROLL_THRESHOLD = 50;
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 (
<ThreeColumnLayout
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 />
</div>
}
center={
<div className="flex flex-col h-[calc(100vh-3rem)]">
<div className="flex-1 overflow-y-auto">{children}</div>
<div className={`flex flex-col ${containerHeight} transition-all duration-300`}>
<div ref={contentRef} className="flex-1 overflow-y-auto min-h-0">{children}</div>
<LabFooter />
</div>
}

View File

@ -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>
);
};