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"; import { useTheme } from "../hooks/useTheme";
const NAV_ITEMS = [ const NAV_ITEMS = [
{ path: "/", label: "Home" }, { path: "/editor", label: "Editor", icon: "\u270E" },
{ path: "/editor", label: "Editor" }, { path: "/toolset", label: "Toolset", icon: "\u2699" },
{ path: "/build", label: "Build" }, { path: "/build", label: "Build", icon: "\u2692" },
{ path: "/server", label: "Server" }, { path: "/server", label: "Server", icon: "\u25B6" },
{ path: "/toolset", label: "Toolset" }, { path: "/repos", label: "Repos", icon: "\u2387" },
{ path: "/repos", label: "Repos" }, { path: "/settings", label: "Settings", icon: "\u2318" },
]; ];
export function IDELayout({ sidebar }: { sidebar?: React.ReactNode }) { export function IDELayout({ sidebar }: { sidebar?: React.ReactNode }) {
const [terminalOpen, setTerminalOpen] = useState(false); const [terminalOpen, setTerminalOpen] = useState(false);
const [upstreamBehind, setUpstreamBehind] = useState(0); const [upstreamBehind, setUpstreamBehind] = useState(0);
const [pendingToolset, setPendingToolset] = useState(0);
const location = useLocation(); const location = useLocation();
const { subscribe } = useWebSocket(); const { subscribe } = useWebSocket();
const { theme, toggleTheme } = useTheme(); const { theme, toggleTheme } = useTheme();
@@ -30,64 +31,109 @@ export function IDELayout({ sidebar }: { sidebar?: React.ReactNode }) {
}, [subscribe]); }, [subscribe]);
useEffect(() => { useEffect(() => {
if (location.pathname === "/repos") { return subscribe("toolset:changes", (event) => {
setUpstreamBehind(0); const data = event.data as { count?: number };
} if (data.count !== undefined) setPendingToolset(data.count);
});
}, [subscribe]);
useEffect(() => {
if (location.pathname === "/repos") setUpstreamBehind(0);
}, [location.pathname]); }, [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 ( return (
<div className="flex h-screen flex-col overflow-hidden" style={{ backgroundColor: "var(--forge-bg)" }}> <div className="flex h-screen overflow-hidden" style={{ backgroundColor: "var(--forge-bg)" }}>
<header {/* Left sidebar nav */}
className="flex shrink-0 items-center gap-6 px-4 py-2" <nav
style={{ borderBottom: "1px solid var(--forge-border)" }} 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"> <Link
<img src="/layonara.png" alt="Layonara" style={{ width: "130px" }} /> to="/"
<span className="flex items-center justify-center py-3"
className="text-lg font-bold" title="Dashboard"
style={{ >
fontFamily: "'Baskerville', 'Georgia', 'Palatino', serif", <img src="/layonara.png" alt="Layonara" style={{ width: "40px" }} />
color: "var(--forge-accent)", </Link>
}}
> <div className="mt-2 flex flex-1 flex-col">
Forge
</span>
</div>
<nav className="flex flex-1 items-center gap-1">
{NAV_ITEMS.map((item) => { {NAV_ITEMS.map((item) => {
const isActive = 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 ( return (
<Link <Link
key={item.path} key={item.path}
to={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={{ 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)", color: isActive ? "var(--forge-accent)" : "var(--forge-text-secondary)",
fontWeight: isActive ? 600 : 400,
}} }}
title={item.label}
> >
{item.label} <span className="text-base">{item.icon}</span>
{item.path === "/repos" && upstreamBehind > 0 && ( <span className="mt-0.5 text-[9px] leading-tight">{item.label}</span>
<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"> {badge > 0 && (
{upstreamBehind} <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> </span>
)} )}
</Link> </Link>
); );
})} })}
</nav> </div>
<button <button
onClick={toggleTheme} 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)" }} style={{ color: "var(--forge-text-secondary)" }}
title={`Switch to ${theme === "dark" ? "light" : "dark"} mode`} title={`Switch to ${theme === "dark" ? "light" : "dark"} mode`}
> >
{theme === "dark" ? "\u2600\uFE0F" : "\uD83C\uDF19"} {theme === "dark" ? "\u2600\uFE0F" : "\uD83C\uDF19"}
</button> </button>
</header> </nav>
{/* Main content area */}
<div className="flex flex-1 flex-col overflow-hidden"> <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"> <div className="flex flex-1 overflow-hidden">
{sidebar && ( {sidebar && (
<aside <aside