feat: polish IDE layout with sidebar nav, resizable panels, and routing
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user