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