f811245f90
Remove the spellbook-themed settings index: drop the "Codex of Configuration" heading, the gradient ornaments with ✦/◆ dividers, the sigil that appeared after two minutes of inactivity, and the IntroImage illustration tiles on every section row and sub-page intro block. The index is now a flat divider-separated list of labels and one-line descriptions, with breathing room on both sides. Delete the Magic settings page, its CursorFireEffect overlay, the animate-sigil-glow / animate-pulse-slow keyframes, the magicMouse AppConfig flag (schema, default, test fixture), and the /settings/magic route. Delete the now-unreferenced IntroImage component and the ten *-intro.png assets it masked. Disable content types that don't fit an activist tool by default: vines, treasures (geocaches + found logs), colors, decks, webxdc, birdstar (detections / birdex / constellations), emoji packs, custom emojis, user statuses, music, podcasts, and development. They remain available in settings — just off out of the box. Highlights is bumped on by default to pair with Articles. Posts, comments, reposts, articles, highlights, events, polls, communities, people lists, badges, photos, videos, and voice messages stay on.
145 lines
7.0 KiB
TypeScript
145 lines
7.0 KiB
TypeScript
import { Suspense, useState, useMemo, useCallback, useRef, lazy } from 'react';
|
|
import { Outlet } from 'react-router-dom';
|
|
import { LeftSidebar } from '@/components/LeftSidebar';
|
|
import { MobileTopBar } from '@/components/MobileTopBar';
|
|
import { MobileDrawer } from '@/components/MobileDrawer';
|
|
import { FloatingComposeButton } from '@/components/FloatingComposeButton';
|
|
import { Skeleton } from '@/components/ui/skeleton';
|
|
import { CenterColumnContext, DrawerContext, LayoutStore, LayoutStoreContext, NavHiddenContext, useLayoutSnapshot } from '@/contexts/LayoutContext';
|
|
import { useScrollDirection } from '@/hooks/useScrollDirection';
|
|
import { cn } from '@/lib/utils';
|
|
|
|
const WidgetSidebar = lazy(() => import('@/components/WidgetSidebar').then((m) => ({ default: m.WidgetSidebar })));
|
|
|
|
/** Skeleton shown in the content area while a lazy page chunk is loading. */
|
|
function PageSkeleton() {
|
|
return (
|
|
<>
|
|
{/* Main column skeleton — mirrors the Outlet wrapper's border + bg classes */}
|
|
<main className="flex-1 min-w-0 min-h-screen sidebar:border-l sidebar:border-r border-border bg-background/85 sidebar:max-w-[600px]">
|
|
{/* Header skeleton */}
|
|
<div className="flex items-center gap-4 px-4 pt-4 pb-5">
|
|
<Skeleton className="h-6 w-32" />
|
|
</div>
|
|
{/* Content skeletons */}
|
|
<div className="space-y-4 px-4 min-h-dvh">
|
|
{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 placeholder — preserves layout width */}
|
|
<div className="w-[300px] shrink-0 hidden xl:block" />
|
|
</>
|
|
);
|
|
}
|
|
|
|
/** Inner component that reads layout options from the context store. */
|
|
function MainLayoutInner() {
|
|
const { rightSidebar, showFAB = false, fabKind = 1, fabHref, onFabClick, fabIcon, fabMenu, wrapperClassName, noOverscroll, noMaxWidth, scrollContainer, hasSubHeader, hideTopBar } = useLayoutSnapshot();
|
|
const [drawerOpen, setDrawerOpen] = useState(false);
|
|
const openDrawer = useCallback(() => setDrawerOpen(true), []);
|
|
const centerColumnRef = useRef<HTMLDivElement>(null);
|
|
const [centerColumnEl, setCenterColumnEl] = useState<HTMLElement | null>(null);
|
|
const { hidden: navHidden } = useScrollDirection(scrollContainer);
|
|
return (
|
|
<CenterColumnContext.Provider value={centerColumnEl}>
|
|
<DrawerContext.Provider value={openDrawer}>
|
|
<NavHiddenContext.Provider value={navHidden}>
|
|
{/* Mobile top bar - only on small screens, hidden when page requests immersive mode */}
|
|
{!hideTopBar && <MobileTopBar onAvatarClick={() => setDrawerOpen(true)} hasSubHeader={hasSubHeader} />}
|
|
|
|
{/* 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 */}
|
|
<LeftSidebar />
|
|
|
|
{/* Main content + right sidebar: inside Suspense so the left sidebar persists while lazy pages load */}
|
|
<Suspense fallback={<PageSkeleton />}>
|
|
{/* -mt-mobile-bar pulls content up behind the mobile top bar so the
|
|
transparent SVG header arc and page content overlap seamlessly.
|
|
The corresponding padding-top (set in CSS) prevents content from
|
|
being hidden. This depends on MobileTopBar having a transparent /
|
|
semi-transparent background — a solid top bar would obscure the
|
|
content underneath. Only active below the sidebar breakpoint. */}
|
|
<div
|
|
ref={(el) => { centerColumnRef.current = el; setCenterColumnEl(el); }}
|
|
className={cn("relative z-0 flex-1 min-w-0 sidebar:border-l sidebar:border-r border-border bg-background/85", !hideTopBar && "-mt-mobile-bar", !noMaxWidth && "sidebar:max-w-[600px]", !noOverscroll && "pb-overscroll")}
|
|
>
|
|
<Outlet />
|
|
|
|
{/* Desktop FAB — sticky within the feed column so it stays
|
|
anchored to the bottom-right of the content area, not the
|
|
viewport. Hidden below the sidebar breakpoint where the
|
|
mobile fixed FAB takes over. */}
|
|
{showFAB && (
|
|
<div className="hidden sidebar:block sticky bottom-6 z-30 pointer-events-none">
|
|
<div className="flex justify-end pr-4">
|
|
<div className="pointer-events-auto">
|
|
<FloatingComposeButton kind={fabKind} href={fabHref} onFabClick={onFabClick} icon={fabIcon} menu={fabMenu} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
{/* Right sidebar — render page-provided sidebar, or the default
|
|
widget sidebar. `null` (explicit) means "no sidebar"; `undefined`
|
|
(unset) falls back to the default. We distinguish these because
|
|
`??` would otherwise treat `null` the same as unset and render
|
|
the default — which silently breaks pages that intend to be
|
|
full-bleed (e.g. /world). */}
|
|
{rightSidebar === undefined
|
|
? <Suspense fallback={<div className="w-[300px] shrink-0 hidden xl:block" />}><WidgetSidebar /></Suspense>
|
|
: rightSidebar}
|
|
</Suspense>
|
|
</div>
|
|
|
|
{/* Mobile FAB — fixed to viewport, hidden on desktop where the
|
|
in-column sticky FAB (above) takes over. Mirrors bottom nav
|
|
hide/show transition on scroll. */}
|
|
{showFAB && (
|
|
<div
|
|
className="fixed bottom-fab right-6 z-30 pointer-events-none transition-transform duration-300 ease-in-out sidebar:hidden"
|
|
style={navHidden ? { transform: `translateY(calc(var(--bottom-nav-height) + var(--safe-area-inset-bottom, env(safe-area-inset-bottom, 0px))))` } : undefined}
|
|
>
|
|
<div className="pointer-events-auto">
|
|
<FloatingComposeButton kind={fabKind} href={fabHref} onFabClick={onFabClick} icon={fabIcon} menu={fabMenu} />
|
|
</div>
|
|
</div>
|
|
)}
|
|
</NavHiddenContext.Provider>
|
|
</DrawerContext.Provider>
|
|
</CenterColumnContext.Provider>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 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>
|
|
);
|
|
}
|