Files
eranos/src/components/MainLayout.tsx
T
Derek Ross 6abe9fade0 Hide FAB while ComposeBox is visible, fade in on scroll
Use IntersectionObserver on the ComposeBox wrapper in Feed.tsx to track
its visibility. When visible, hide the FAB to avoid redundancy. When the
user scrolls past the compose area, the FAB smoothly fades in with an
upward slide animation. Extends LayoutStore with fabHidden state that
is part of the snapshot so useSyncExternalStore triggers re-renders.
2026-03-02 21:35:36 -05:00

132 lines
5.1 KiB
TypeScript

import { Suspense, useState, useMemo } from 'react';
import { Outlet } from 'react-router-dom';
import { LeftSidebar } from '@/components/LeftSidebar';
import { RightSidebar } from '@/components/RightSidebar';
import { MobileTopBar } from '@/components/MobileTopBar';
import { MobileBottomNav } from '@/components/MobileBottomNav';
import { MobileDrawer } from '@/components/MobileDrawer';
import { FloatingComposeButton } from '@/components/FloatingComposeButton';
import { CursorFireEffect } from '@/components/CursorFireEffect';
import { Skeleton } from '@/components/ui/skeleton';
import { LayoutStore, LayoutStoreContext, useLayoutSnapshot } from '@/contexts/LayoutContext';
import { useAppContext } from '@/hooks/useAppContext';
import { cn } from '@/lib/utils';
/** Skeleton shown in the content area while a lazy page chunk is loading. */
function PageSkeleton() {
return (
<>
{/* Main column skeleton */}
<main className="flex-1 min-w-0 min-h-screen">
{/* Header skeleton */}
<div className="flex items-center gap-4 px-4 mt-4 mb-5">
<Skeleton className="h-6 w-32" />
</div>
{/* Content skeletons */}
<div className="space-y-4 px-4">
{Array.from({ length: 4 }).map((_, i) => (
<div key={i} className="space-y-3 py-4 border-b border-border">
<div className="flex items-center gap-3">
<Skeleton className="h-10 w-10 rounded-full" />
<div className="space-y-1.5">
<Skeleton className="h-4 w-28" />
<Skeleton className="h-3 w-20" />
</div>
</div>
<div className="space-y-2">
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-4/5" />
</div>
</div>
))}
</div>
</main>
{/* Right sidebar skeleton */}
<aside className="w-[300px] shrink-0 hidden xl:flex flex-col sticky top-0 h-screen pt-5 pb-3 px-5">
<div className="space-y-6">
<div className="space-y-3">
<Skeleton className="h-6 w-24" />
{Array.from({ length: 4 }).map((_, i) => (
<div key={i} className="flex justify-between items-center">
<div className="space-y-1.5">
<Skeleton className="h-4 w-24" />
<Skeleton className="h-3 w-32" />
</div>
<Skeleton className="h-8 w-12" />
</div>
))}
</div>
</div>
</aside>
</>
);
}
/** Inner component that reads layout options from the context store. */
function MainLayoutInner() {
const { rightSidebar, showFAB = false, fabKind = 1, fabHref, onFabClick, noBottomSpacer = false, wrapperClassName, fabHidden } = useLayoutSnapshot();
const [drawerOpen, setDrawerOpen] = useState(false);
const { config } = useAppContext();
return (
<>
{/* Magic Mouse fire particle overlay */}
{config.magicMouse && <CursorFireEffect />}
{/* Mobile top bar - only on small screens */}
<MobileTopBar onAvatarClick={() => setDrawerOpen(true)} />
{/* Mobile drawer */}
<MobileDrawer open={drawerOpen} onOpenChange={setDrawerOpen} />
{/* Main layout - three column on desktop */}
<div className={cn("flex justify-center mx-auto max-w-[1200px]", wrapperClassName)}>
{/* Desktop left sidebar - hidden below sidebar breakpoint */}
<div className="hidden sidebar:block">
<LeftSidebar />
</div>
{/* Main content + right sidebar: inside Suspense so the left sidebar persists while lazy pages load */}
<Suspense fallback={<PageSkeleton />}>
{/* Wrap the center column in a relative container for the FAB */}
<div className={cn("relative flex-1 min-w-0 sidebar:max-w-[600px] sidebar:border-l border-r border-border bg-background/85")}>
<Outlet />
{showFAB && (
<div className={cn(
'sticky bottom-fab sidebar:bottom-6 z-30 pointer-events-none flex justify-end pr-6 transition-all duration-300',
fabHidden ? 'opacity-0 translate-y-4' : 'opacity-100 translate-y-0',
)}>
<div className={cn('pointer-events-auto', fabHidden && 'pointer-events-none')}>
<FloatingComposeButton kind={fabKind} href={fabHref} onFabClick={onFabClick} />
</div>
</div>
)}
</div>
{rightSidebar ?? <RightSidebar />}
</Suspense>
</div>
{/* Mobile bottom nav - only on small screens */}
<MobileBottomNav />
{/* Bottom padding spacer for mobile bottom nav */}
{!noBottomSpacer && <div className="h-14 sidebar:hidden" />}
</>
);
}
/**
* Persistent layout shell rendered once by the router.
* Provides a LayoutStore so child pages can configure layout options
* (e.g. showFAB, custom right sidebar) via the `useLayoutOptions` hook.
*/
export function MainLayout() {
const store = useMemo(() => new LayoutStore(), []);
return (
<LayoutStoreContext.Provider value={store}>
<MainLayoutInner />
</LayoutStoreContext.Provider>
);
}