Spaces:
Running
Running
File size: 5,493 Bytes
1955a0f | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 | import React, { useEffect, useMemo, useRef, useState } from "react";
import { Outlet } from "react-router-dom";
import { AnimatePresence, motion } from "motion/react";
import { ThemeConfig, ThemeContext } from "../context/ThemeContext";
import GeometricBackground from "../components/GeometricBackground";
import BackgroundElements from "../components/BackgroundElements";
import TopBar from "../components/TopBar";
import PlatformSidebar from "./PlatformSidebar";
import ModuleLaunchAnimation, { hashModuleId } from "./ModuleLaunchAnimation";
import { useLaunch } from "./LaunchContext";
import { MODULES, ModuleId } from "./registry";
/**
* Nested theme provider — overrides the platform default (mint) with the
* active module palette for the whole module subtree.
*/
function ModuleThemeProvider({ config, children }: { config: ThemeConfig; children: React.ReactNode }) {
const value = useMemo(() => ({ theme: config.id, config }), [config]);
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
}
interface ModuleShellProps {
module: ModuleId;
}
/**
* Route shell for /modules/e and /modules/s.
*
* Launch guard: on every mount (menu launch, hub launch, direct URL) the
* module's real dashboard primitives random-reassemble in slow motion before
* the workspace is revealed. Leaving via the menu plays the reverse-assembly
* exit. Internal navigation never replays the animation (shell stays mounted).
*/
export default function ModuleShell({ module }: ModuleShellProps) {
const def = MODULES[module];
const { seq, exiting, finishExit } = useLaunch();
const [launchDone, setLaunchDone] = useState(false);
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
const seedRef = useRef<number>(0);
if (seedRef.current === 0) {
seedRef.current = Date.now() ^ hashModuleId(module) ^ ((seq + 1) * 0x9e3779b9);
}
// A fresh mount is a fresh launch — clear any stale exit flag from the
// previous module.
useEffect(() => {
if (exiting) finishExit();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const isExiting = exiting === module;
return (
<ModuleThemeProvider config={def.theme}>
<div
data-module={module}
data-testid={`module-shell-${module}`}
className={module === "s" ? "bg-module-s min-h-full" : ""}
>
<div className="flex h-screen bg-transparent text-zinc-100 overflow-hidden relative">
<GeometricBackground />
<BackgroundElements />
{/* Mobile overlay */}
{isSidebarOpen && (
<div
className="fixed inset-0 bg-black/60 z-30 md:hidden backdrop-blur-sm"
onClick={() => setIsSidebarOpen(false)}
/>
)}
{/* Sidebar with mobile slide transition */}
<div
className={`fixed inset-y-0 left-0 z-40 transform transition-transform duration-300 ease-in-out md:relative md:translate-x-0 ${
isSidebarOpen ? "translate-x-0" : "-translate-x-full"
}`}
>
<PlatformSidebar module={module} onClose={() => setIsSidebarOpen(false)} />
</div>
<div className="flex flex-col flex-1 min-w-0 overflow-hidden w-full">
<TopBar module={module} onMenuClick={() => setIsSidebarOpen(true)} />
<main className="flex-1 overflow-y-auto bg-transparent relative z-10 w-full p-2 md:p-0">
{/* Launch guard: the animation covers the workspace; pages load
(and prefetch their data) behind the opaque overlay. */}
<div className="relative min-h-full">
{!isExiting && (
<React.Suspense fallback={<div className="p-6" />}>
<Outlet />
</React.Suspense>
)}
<AnimatePresence>
{!launchDone && !isExiting && (
<motion.div
key="launch-overlay"
className={module === "s" ? "absolute inset-0 z-30 bg-module-s" : "absolute inset-0 z-30 bg-[#05070a]"}
exit={{ opacity: 0, transition: { duration: 0.45, ease: "easeOut" } }}
>
<ModuleLaunchAnimation
module={module}
mode="enter"
seed={seedRef.current}
onDone={() => setLaunchDone(true)}
accent={def.theme.accent}
accentSoft={def.theme.accentText}
/>
</motion.div>
)}
{isExiting && (
<motion.div
key="exit-overlay"
className={module === "s" ? "absolute inset-0 z-30 bg-module-s" : "absolute inset-0 z-30 bg-[#05070a]"}
>
<ModuleLaunchAnimation
module={module}
mode="exit"
seed={Date.now() ^ hashModuleId(module)}
onDone={() => finishExit()}
accent={def.theme.accent}
accentSoft={def.theme.accentText}
/>
</motion.div>
)}
</AnimatePresence>
</div>
</main>
</div>
</div>
</div>
</ModuleThemeProvider>
);
}
|