Compare commits
2 Commits
f247191ead
...
5590787f7f
| Author | SHA1 | Date |
|---|---|---|
|
|
5590787f7f | |
|
|
3579c8e4cf |
|
|
@ -0,0 +1,54 @@
|
|||
'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,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<LabScrollProvider>
|
||||
<div className="h-screen overflow-hidden flex flex-col">
|
||||
<LabHeader />
|
||||
<div className="flex-1 min-h-0">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</LabScrollProvider>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
import Image from 'next/image';
|
||||
import { Footer } from '@/components/shared/Footer';
|
||||
|
||||
export default function MainLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
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}
|
||||
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,7 +1,5 @@
|
|||
import type { Metadata } from 'next';
|
||||
import { Inter } from 'next/font/google';
|
||||
import Image from 'next/image';
|
||||
import { Footer } from '@/components/shared/Footer';
|
||||
import './globals.css';
|
||||
|
||||
const inter = Inter({
|
||||
|
|
@ -71,38 +69,12 @@ export default function RootLayout({
|
|||
</head>
|
||||
<body className={`${inter.variable} antialiased`}>
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-950 via-slate-900 to-slate-950">
|
||||
{/* Animated gradient background */}
|
||||
<div className="fixed inset-0 overflow-hidden pointer-events-none">
|
||||
<div className="absolute top-1/4 -left-1/4 w-96 h-96 bg-purple-600/10 rounded-full blur-3xl animate-pulse"></div>
|
||||
<div className="absolute bottom-1/4 -right-1/4 w-96 h-96 bg-cyan-600/10 rounded-full blur-3xl animate-pulse delay-700"></div>
|
||||
</div>
|
||||
|
||||
{/* Header */}
|
||||
<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>
|
||||
|
||||
{/* Page content */}
|
||||
{children}
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,62 @@
|
|||
'use client';
|
||||
|
||||
/**
|
||||
* Lab Footer Component
|
||||
*
|
||||
* Simple 1-line footer for lab section with contextual navigation links.
|
||||
* Displays copyright on left and contextual docs/API links on right.
|
||||
*/
|
||||
|
||||
import { usePathname } from 'next/navigation';
|
||||
import Link from 'next/link';
|
||||
|
||||
type LinkMapEntry = {
|
||||
docs: string;
|
||||
api: string;
|
||||
};
|
||||
|
||||
type LinkMap = {
|
||||
[key: string]: LinkMapEntry;
|
||||
};
|
||||
|
||||
const linkMap: LinkMap = {
|
||||
'/lab/generate': { docs: '/docs', api: '/docs/api/text-to-image' },
|
||||
'/lab/images': { docs: '/docs', api: '/docs/api/images' },
|
||||
'/lab/live': { docs: '/docs', api: '/docs/api/text-to-image' },
|
||||
'/lab/upload': { docs: '/docs', api: '/docs/api/upload' },
|
||||
};
|
||||
|
||||
export const LabFooter = () => {
|
||||
const pathname = usePathname();
|
||||
const links = linkMap[pathname] || { docs: '/docs', api: '/docs/api' };
|
||||
|
||||
return (
|
||||
<footer
|
||||
className="border-t border-white/10 bg-slate-950/50 backdrop-blur-sm"
|
||||
role="contentinfo"
|
||||
>
|
||||
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4 md:gap-0 px-6 py-4 md:h-14">
|
||||
{/* Left: Copyright */}
|
||||
<p className="text-sm text-gray-500 order-2 md:order-1">
|
||||
© 2025 Banatie. Built for builders who create.
|
||||
</p>
|
||||
|
||||
{/* Right: Contextual Links */}
|
||||
<nav aria-label="Footer navigation" className="flex items-center gap-6 order-1 md:order-2">
|
||||
<Link
|
||||
href={links.docs}
|
||||
className="text-sm text-gray-500 hover:text-white transition-colors min-h-[44px] flex items-center"
|
||||
>
|
||||
Documentation
|
||||
</Link>
|
||||
<Link
|
||||
href={links.api}
|
||||
className="text-sm text-gray-500 hover:text-white transition-colors min-h-[44px] flex items-center"
|
||||
>
|
||||
API Reference
|
||||
</Link>
|
||||
</nav>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
};
|
||||
|
|
@ -7,33 +7,70 @@
|
|||
* Uses ThreeColumnLayout for consistent column structure across the app.
|
||||
*
|
||||
* Structure:
|
||||
* - Left: LabSidebar (w-64, hidden lg:block)
|
||||
* - Center: Page content (flex-1)
|
||||
* - Left: LabSidebar (w-64, hidden lg:block, scrollable)
|
||||
* - Center: Scrollable content area + fixed LabFooter at bottom
|
||||
* - Right: Reserved for future use (TOC, preview panels, etc.)
|
||||
*
|
||||
* The center column uses a fixed height container with:
|
||||
* - Scrollable content area (flex-1 overflow-y-auto)
|
||||
* - Sticky footer always visible at bottom
|
||||
*
|
||||
* This layout is rendered inside PageProvider context which provides:
|
||||
* - 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={children}
|
||||
center={
|
||||
<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>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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