BRIDGE_Modul-S_prod / src /modules /PlatformSidebar.tsx
BRIDGE Platform Agent
feat(platform): BRIDGE Modules hub with /modules/e and /modules/s routed workspaces
1955a0f
Raw History Blame Contribute Delete
6.53 kB
import { Link, useLocation, useNavigate } from "react-router-dom";
import { LayoutDashboard } from "lucide-react";
import { cn } from "../utils/cn";
import { useTheme } from "../context/ThemeContext";
import AnimatedLogo from "./../components/AnimatedLogo";
import Logo from "./../components/Logo";
import { useLaunch } from "./LaunchContext";
import { MODULES, ModuleId, moduleHref } from "./registry";
/**
* Platform sidebar — wires the BRIDGE MODULES direct-launch section plus the
* active module's navigation registry. Launching a module plays its
* random-reassembly animation (re-seeded every launch).
*/
export default function PlatformSidebar({ module, onClose }: { module: ModuleId; onClose?: () => void }) {
const location = useLocation();
const navigate = useNavigate();
const { config } = useTheme();
const { startLaunch } = useLaunch();
const def = MODULES[module];
const launch = (target: ModuleId | "hub") => {
onClose?.();
startLaunch(target);
};
const isActiveModule = (id: ModuleId) => location.pathname.startsWith(MODULES[id].basePath);
const isActivePath = (path: string) => {
const full = moduleHref(module, path);
return path === "" ? location.pathname === full : location.pathname.startsWith(full);
};
return (
<div className="w-64 border-r border-white/5 bg-zinc-950/90 md:bg-zinc-950/60 backdrop-blur-2xl flex flex-col shrink-0 h-full relative z-20 shadow-[4px_0_24px_-4px_rgba(0,0,0,0.8)]">
<div className="h-16 flex items-center px-6 border-b border-white/5 shrink-0 gap-3">
<AnimatedLogo className="w-7 h-7" />
<Logo wordmark={def.wordmark} />
</div>
<nav className="flex-1 overflow-y-auto py-4 px-3 space-y-1">
{/* BRIDGE MODULES — direct-launch section (Phase 5) */}
<div className="mb-4">
<div className="px-3 pb-2 text-[9px] font-bold tracking-[0.22em] uppercase text-zinc-600 font-mono">
Bridge Modules
</div>
<div className="space-y-1.5">
{(["e", "s"] as ModuleId[]).map((id) => {
const m = MODULES[id];
const active = isActiveModule(id);
const Icon = m.icon;
return (
<button
key={id}
type="button"
onClick={() => launch(id)}
aria-current={active ? "page" : undefined}
className={cn(
"w-full flex items-center gap-3 px-3 py-2.5 text-[13px] font-medium rounded-lg transition-all duration-300 relative group overflow-hidden border text-left",
active
? "bg-white/5 border-white/10 shadow-[inset_0_1px_1px_rgba(255,255,255,0.05)]"
: "text-zinc-400 hover:text-zinc-200 hover:bg-white/5 border-transparent"
)}
style={active ? { color: m.theme.accentText } : {}}
>
{active && (
<span
className="absolute left-0 top-1/2 -translate-y-1/2 w-1 h-6 rounded-r-full"
style={{ backgroundColor: m.theme.accent, boxShadow: `0 0 15px ${m.theme.accent}` }}
/>
)}
<span
className="w-5 h-5 rounded-md flex items-center justify-center shrink-0 border transition-transform duration-300 group-hover:scale-110"
style={{
backgroundColor: `${m.theme.accent}14`,
borderColor: `${m.theme.accent}44`,
}}
>
<Icon className="w-3 h-3" style={{ color: m.theme.accent }} />
</span>
<span className="flex-1">{m.label}</span>
<span
className="w-1.5 h-1.5 rounded-full shrink-0 transition-opacity"
style={{
backgroundColor: m.theme.accent,
boxShadow: `0 0 8px ${m.theme.accent}`,
opacity: active ? 1 : 0.35,
}}
/>
</button>
);
})}
</div>
<button
type="button"
onClick={() => launch("hub")}
className="mt-1.5 w-full flex items-center gap-3 px-3 py-2 text-[11px] font-medium rounded-lg text-zinc-500 hover:text-zinc-300 hover:bg-white/5 border border-transparent transition-all duration-300 text-left"
>
<LayoutDashboard className="w-3.5 h-3.5" />
Platform Hub
</button>
</div>
{/* Active module navigation */}
<div className="px-3 pb-2 pt-1 text-[9px] font-bold tracking-[0.22em] uppercase text-zinc-600 font-mono">
{def.label}
</div>
{def.nav.map((item) => {
const isActive = isActivePath(item.path);
const full = moduleHref(module, item.path);
return (
<button
key={item.name}
type="button"
onClick={() => {
onClose?.();
if (location.pathname !== full) navigate(full);
}}
aria-current={isActive ? "page" : undefined}
className={cn(
"w-full flex items-center gap-3 px-3 py-2.5 text-[13px] font-medium rounded-lg transition-all duration-300 relative group overflow-hidden border text-left",
isActive
? "bg-white/5 border-white/10 shadow-[inset_0_1px_1px_rgba(255,255,255,0.05)]"
: "text-zinc-400 hover:text-zinc-200 hover:bg-white/5 border-transparent"
)}
style={isActive ? { color: config.accentText } : {}}
>
{isActive && (
<div
className="absolute left-0 top-1/2 -translate-y-1/2 w-1 h-6 rounded-r-full"
style={{
backgroundColor: config.accent,
boxShadow: `0 0 15px ${config.accent}`,
}}
/>
)}
<item.icon
className={cn("w-4 h-4 transition-transform duration-300 group-hover:scale-110", isActive ? "" : "text-zinc-500")}
style={isActive ? { color: config.accent, filter: `drop-shadow(0 0 8px ${config.accent})` } : {}}
/>
{item.name}
</button>
);
})}
</nav>
</div>
);
}