feat: polish IDE layout with sidebar nav, resizable panels, and routing

This commit is contained in:
plenarius
2026-04-20 22:09:41 -04:00
parent c310b04f74
commit 83d1a0a169
+82 -36
View File
@@ -5,17 +5,18 @@ import { useWebSocket } from "../hooks/useWebSocket";
import { useTheme } from "../hooks/useTheme";
const NAV_ITEMS = [
{ path: "/", label: "Home" },
{ path: "/editor", label: "Editor" },
{ path: "/build", label: "Build" },
{ path: "/server", label: "Server" },
{ path: "/toolset", label: "Toolset" },
{ path: "/repos", label: "Repos" },
{ path: "/editor", label: "Editor", icon: "\u270E" },
{ path: "/toolset", label: "Toolset", icon: "\u2699" },
{ path: "/build", label: "Build", icon: "\u2692" },
{ path: "/server", label: "Server", icon: "\u25B6" },
{ path: "/repos", label: "Repos", icon: "\u2387" },
{ path: "/settings", label: "Settings", icon: "\u2318" },
];
export function IDELayout({ sidebar }: { sidebar?: React.ReactNode }) {
const [terminalOpen, setTerminalOpen] = useState(false);
const [upstreamBehind, setUpstreamBehind] = useState(0);
const [pendingToolset, setPendingToolset] = useState(0);
const location = useLocation();
const { subscribe } = useWebSocket();
const { theme, toggleTheme } = useTheme();
@@ -30,64 +31,109 @@ export function IDELayout({ sidebar }: { sidebar?: React.ReactNode }) {
}, [subscribe]);
useEffect(() => {
if (location.pathname === "/repos") {
setUpstreamBehind(0);
}
return subscribe("toolset:changes", (event) => {
const data = event.data as { count?: number };
if (data.count !== undefined) setPendingToolset(data.count);
});
}, [subscribe]);
useEffect(() => {
if (location.pathname === "/repos") setUpstreamBehind(0);
}, [location.pathname]);
useEffect(() => {
if (location.pathname === "/toolset") setPendingToolset(0);
}, [location.pathname]);
const getBadge = (path: string): number => {
if (path === "/repos") return upstreamBehind;
if (path === "/toolset") return pendingToolset;
return 0;
};
return (
<div className="flex h-screen flex-col overflow-hidden" style={{ backgroundColor: "var(--forge-bg)" }}>
<header
className="flex shrink-0 items-center gap-6 px-4 py-2"
style={{ borderBottom: "1px solid var(--forge-border)" }}
<div className="flex h-screen overflow-hidden" style={{ backgroundColor: "var(--forge-bg)" }}>
{/* Left sidebar nav */}
<nav
className="flex shrink-0 flex-col"
style={{
width: "56px",
borderRight: "1px solid var(--forge-border)",
backgroundColor: "var(--forge-surface)",
}}
>
<div className="flex items-center gap-2">
<img src="/layonara.png" alt="Layonara" style={{ width: "130px" }} />
<span
className="text-lg font-bold"
style={{
fontFamily: "'Baskerville', 'Georgia', 'Palatino', serif",
color: "var(--forge-accent)",
}}
>
Forge
</span>
</div>
<nav className="flex flex-1 items-center gap-1">
<Link
to="/"
className="flex items-center justify-center py-3"
title="Dashboard"
>
<img src="/layonara.png" alt="Layonara" style={{ width: "40px" }} />
</Link>
<div className="mt-2 flex flex-1 flex-col">
{NAV_ITEMS.map((item) => {
const isActive =
item.path === "/" ? location.pathname === "/" : location.pathname.startsWith(item.path);
item.path === "/"
? location.pathname === "/"
: location.pathname.startsWith(item.path);
const badge = getBadge(item.path);
return (
<Link
key={item.path}
to={item.path}
className="relative rounded px-2.5 py-1 text-sm transition-colors hover:bg-white/5"
className="relative flex flex-col items-center justify-center py-2.5 text-center transition-colors hover:bg-white/5"
style={{
borderLeft: isActive
? "3px solid var(--forge-accent)"
: "3px solid transparent",
backgroundColor: isActive ? "rgba(148, 98, 0, 0.1)" : undefined,
color: isActive ? "var(--forge-accent)" : "var(--forge-text-secondary)",
fontWeight: isActive ? 600 : 400,
}}
title={item.label}
>
{item.label}
{item.path === "/repos" && upstreamBehind > 0 && (
<span className="absolute -right-1 -top-1 flex h-4 min-w-4 items-center justify-center rounded-full bg-amber-500 px-1 text-[10px] font-bold text-black">
{upstreamBehind}
<span className="text-base">{item.icon}</span>
<span className="mt-0.5 text-[9px] leading-tight">{item.label}</span>
{badge > 0 && (
<span className="absolute right-1 top-1 flex h-3.5 min-w-3.5 items-center justify-center rounded-full bg-amber-500 px-0.5 text-[8px] font-bold text-black">
{badge}
</span>
)}
</Link>
);
})}
</nav>
</div>
<button
onClick={toggleTheme}
className="rounded px-2 py-1 text-sm transition-colors hover:bg-white/10"
className="flex items-center justify-center py-3 text-sm transition-colors hover:bg-white/5"
style={{ color: "var(--forge-text-secondary)" }}
title={`Switch to ${theme === "dark" ? "light" : "dark"} mode`}
>
{theme === "dark" ? "\u2600\uFE0F" : "\uD83C\uDF19"}
</button>
</header>
</nav>
{/* Main content area */}
<div className="flex flex-1 flex-col overflow-hidden">
<header
className="flex shrink-0 items-center gap-4 px-4 py-1.5"
style={{ borderBottom: "1px solid var(--forge-border)" }}
>
<div className="flex items-center gap-2">
<span
className="text-lg font-bold"
style={{
fontFamily: "'Baskerville', 'Georgia', 'Palatino', serif",
color: "var(--forge-accent)",
}}
>
Layonara Forge
</span>
</div>
<div className="flex-1" />
</header>
<div className="flex flex-1 overflow-hidden">
{sidebar && (
<aside