Rework site layout into a fixed sidebar

Remove the top header and move its contents into the sidebar, which is
now sticky and full height: logo and version at the top, navigation in
the middle, and the account menu pinned to the bottom (opening to the
right).

The sidebar is rebuilt with antd Flex, Typography and Menu so almost all
styling comes from antd, reducing the custom css to a single sticky rule.
The nav menus also use theme="dark" to match the sider and fix the low
contrast active item.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
bbassieandClaude Opus 4.8 committed 2026-06-22 21:16:49 +00:00
1 parent 9e727f9a13
commit 262357acef
2 files changed
+65 -134

No files matched your search

+60 -59
View File
@@ -1,6 +1,6 @@
import React, { Fragment, useContext, useEffect, useState } from "react";
import { Route, Routes, useNavigate } from "react-router-dom";
import { Button, Dropdown, Layout, Menu, MenuProps, Tooltip } from "antd";
import { Button, Flex, Layout, Menu, MenuProps, Tooltip, Typography } from "antd";
import { UserOutlined, DownloadOutlined } from "@ant-design/icons";
import ErrorBoundary from "./ErrorBoundary";
@@ -14,12 +14,13 @@ import { saveJson } from "../util/save_file";
import { useAccount } from "../model/account";
import { DraggingContext } from "../model/is_dragging";
import webUiPackage from "../../package.json";
import logo from "../images/logo.png";
import { ControlConfig } from "@clusterio/lib";
type MenuItem = Required<MenuProps>["items"][number];
const { Header, Sider } = Layout;
const { Sider } = Layout;
function isActiveDropzone(element: HTMLElement | null): boolean {
if (!element) {
@@ -127,6 +128,8 @@ export default function SiteLayout() {
}
return <Layout
hasSider
className="site-layout"
style={{ minHeight: "100vh" }}
onDragEnter={() => setDraggingProxy(1)}
onDragLeave={() => setDraggingProxy(-1)}
@@ -170,70 +173,68 @@ export default function SiteLayout() {
}}
/>
<DraggingContext.Provider value={dragging > 0}>
<Header className="header">
<div className="site-logo" />
<span className="site-name">Clusterio</span>
<Tooltip
title="View changelog"
placement="bottom"
align={{ offset: [0, -10] }}
>
<span
className="site-version"
style={{ cursor: "pointer" }}
onClick={() => setChangeLogOpen(true)}
>
{webUiPackage.version}
</span>
</Tooltip>
<Menu
theme="dark"
mode="horizontal"
defaultSelectedKeys={["1"]}
items={[{ label: "Dashboard", key: "1" }]}
/>
<Dropdown
className="account-dropdown-header"
placement="bottomRight"
trigger={["click"]}
menu={accountMenuProps}
>
<UserOutlined />
</Dropdown>
</Header>
<Layout className="site-layout">
<Sider
collapsible
collapsedWidth={0}
breakpoint="md"
zeroWidthTriggerStyle={{ top: 6, zIndex: -1 }}
width={250}
className="site-layout-sider"
>
<Sider
collapsible
collapsedWidth={0}
breakpoint="md"
zeroWidthTriggerStyle={{ top: 6, zIndex: -1 }}
width={250}
className="site-layout-sider"
>
<Flex vertical style={{ height: "100%" }}>
<Flex align="center" gap="middle" style={{ padding: 16 }}>
<img src={logo} width={48} height={48} alt="Clusterio logo" />
<Flex vertical>
<Typography.Title level={4} style={{ margin: 0 }}>Clusterio</Typography.Title>
<Tooltip title="View changelog" placement="right">
<Typography.Text
type="danger"
style={{ cursor: "pointer" }}
onClick={() => setChangeLogOpen(true)}
>
{webUiPackage.version}
</Typography.Text>
</Tooltip>
</Flex>
</Flex>
<Menu
theme="dark"
mode="inline"
defaultOpenKeys={[...menuGroups.keys()]}
selectedKeys={currentSidebarPath ? [currentSidebarPath] : []}
style={{ height: "100%", borderRight: 0 }}
style={{ flex: 1, overflow: "auto", borderInlineEnd: 0 }}
onClick={({ key }) => navigate(key)}
items={menuItems}
/>
</Sider>
<Layout className="site-layout-content-container">
<Routes>
{combinedPages.map(({ path, sidebarPath, content }) => <Route
path={path}
key={path}
element={<Fragment key={path}>
<SetSidebar path={sidebarPath ? sidebarPath : path} />
<ErrorBoundary Component={ErrorPage}>
{content}
</ErrorBoundary>
</Fragment>}
/>)}
<Route element={<SetSidebar path={null} />} />
</Routes>
</Layout>
<Menu
theme="dark"
mode="vertical"
selectable={false}
style={{ borderInlineEnd: 0 }}
onClick={accountMenuProps.onClick}
items={[{
key: "account",
icon: <UserOutlined />,
label: account.name,
children: accountMenuProps.items,
}]}
/>
</Flex>
</Sider>
<Layout className="site-layout-content-container">
<Routes>
{combinedPages.map(({ path, sidebarPath, content }) => <Route
path={path}
key={path}
element={<Fragment key={path}>
<SetSidebar path={sidebarPath ? sidebarPath : path} />
<ErrorBoundary Component={ErrorPage}>
{content}
</ErrorBoundary>
</Fragment>}
/>)}
<Route element={<SetSidebar path={null} />} />
</Routes>
</Layout>
</DraggingContext.Provider>
</Layout>;
+5 -75
View File
@@ -56,13 +56,12 @@ body {
}
}
/* Keep the sidebar fixed in place while the page content scrolls */
.site-layout-sider {
z-index: 0;
}
.header {
padding-left: 250px;
padding-right: 64px;
position: sticky;
top: 0;
height: 100vh;
align-self: flex-start;
}
.page-header {
@@ -105,75 +104,6 @@ body {
margin-left: auto;
}
.site-name {
position: absolute;
top: -10px;
left: 112px;
font-size: 18pt;
font-weight: 400;
}
.site-version {
position: absolute;
top: 10px;
left: 120px;
color: #f00;
}
.site-logo {
position: absolute;
left: 36px;
width: 64px;
height: 64px;
background-image: url("./images/logo.png");
background-size: 64px;
}
@media not all and (min-width: 576px) {
.header {
padding-left: 88px;
}
.site-logo {
left: 12px;
}
.site-name {
display: none;
}
.site-version {
display: none;
}
}
.account-dropdown-header {
position: absolute;
right: 0;
top: 0;
width: 64px;
height: 64px;
font-size: 18px;
line-height: 72px;
display: flex;
justify-content: center;
}
.account-dropdown-header::after {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
background: transparent;
transition: all 0.3s;
content: "";
}
.account-dropdown-header:hover::after {
background: rgba(255, 255, 255, 0.1);
}
.error-traceback code {
display: block;
margin: 0;