From 262357acef2f5b0d6e7d5ca8d322e43501d8161d Mon Sep 17 00:00:00 2001 From: bbassie <17990055+bbassie@users.noreply.github.com> Date: Mon, 22 Jun 2026 21:16:49 +0000 Subject: [PATCH 1/4] 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) --- packages/web_ui/src/components/SiteLayout.tsx | 119 +++++++++--------- packages/web_ui/src/style.css | 80 +----------- 2 files changed, 65 insertions(+), 134 deletions(-) diff --git a/packages/web_ui/src/components/SiteLayout.tsx b/packages/web_ui/src/components/SiteLayout.tsx index 76338056..d2a9f1f9 100644 --- a/packages/web_ui/src/components/SiteLayout.tsx +++ b/packages/web_ui/src/components/SiteLayout.tsx @@ -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["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 setDraggingProxy(1)} onDragLeave={() => setDraggingProxy(-1)} @@ -170,70 +173,68 @@ export default function SiteLayout() { }} /> 0}> -
-
- Clusterio - - setChangeLogOpen(true)} - > - {webUiPackage.version} - - - - - - -
- - + + + + Clusterio logo + + Clusterio + + setChangeLogOpen(true)} + > + {webUiPackage.version} + + + + navigate(key)} items={menuItems} /> - - - - {combinedPages.map(({ path, sidebarPath, content }) => - - - {content} - - } - />)} - } /> - - + , + label: account.name, + children: accountMenuProps.items, + }]} + /> + + + + + {combinedPages.map(({ path, sidebarPath, content }) => + + + {content} + + } + />)} + } /> + ; diff --git a/packages/web_ui/src/style.css b/packages/web_ui/src/style.css index b1f53739..9a07fc34 100644 --- a/packages/web_ui/src/style.css +++ b/packages/web_ui/src/style.css @@ -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; From ed84a476c6120ae2b3e8dd45549f5aab967e5f21 Mon Sep 17 00:00:00 2001 From: bbassie <17990055+bbassie@users.noreply.github.com> Date: Tue, 23 Jun 2026 19:19:36 +0000 Subject: [PATCH 2/4] Add top bar and overlay nav drawer on mobile Reintroduce a slim top bar to host the menu toggle, branding and the account menu, which gives the hamburger a natural home now that the header is gone. The bar is more compact than the original header and uses the dark sider colour for better contrast. On phone screens the sider becomes an overlay Drawer (opening below the bar and closing on navigation), following normal hamburger conventions instead of shifting the content across. On desktop the same toggle collapses the sider, with its width animation disabled to avoid the content reflow glitch while resizing. Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/web_ui/src/components/SiteLayout.tsx | 169 +++++++++++------- packages/web_ui/src/style.css | 30 +++- 2 files changed, 125 insertions(+), 74 deletions(-) diff --git a/packages/web_ui/src/components/SiteLayout.tsx b/packages/web_ui/src/components/SiteLayout.tsx index d2a9f1f9..f74b5ab5 100644 --- a/packages/web_ui/src/components/SiteLayout.tsx +++ b/packages/web_ui/src/components/SiteLayout.tsx @@ -1,7 +1,7 @@ import React, { Fragment, useContext, useEffect, useState } from "react"; import { Route, Routes, useNavigate } from "react-router-dom"; -import { Button, Flex, Layout, Menu, MenuProps, Tooltip, Typography } from "antd"; -import { UserOutlined, DownloadOutlined } from "@ant-design/icons"; +import { Button, Drawer, Dropdown, Flex, Grid, Layout, Menu, MenuProps, Tooltip, Typography } from "antd"; +import { UserOutlined, DownloadOutlined, MenuOutlined } from "@ant-design/icons"; import ErrorBoundary from "./ErrorBoundary"; import ErrorPage from "./ErrorPage"; @@ -20,7 +20,7 @@ import { ControlConfig } from "@clusterio/lib"; type MenuItem = Required["items"][number]; -const { Sider } = Layout; +const { Sider, Header } = Layout; function isActiveDropzone(element: HTMLElement | null): boolean { if (!element) { @@ -43,6 +43,13 @@ function isActiveDropzone(element: HTMLElement | null): boolean { export default function SiteLayout() { let navigate = useNavigate(); let [currentSidebarPath, setCurrentSidebarPath] = useState(null); + const screens = Grid.useBreakpoint(); + // On phone screens the sidebar becomes an overlay drawer instead of a fixed sider. + // `md` matches the previous Sider breakpoint; `screens.md` is undefined until measured, + // so treat the initial render as desktop to avoid flashing the hamburger. + const isMobile = screens.md === false; + const [drawerOpen, setDrawerOpen] = useState(false); + const [collapsed, setCollapsed] = useState(false); let account = useAccount(); let plugins = useContext(ControlContext).plugins; const control = useContext(ControlContext); @@ -127,8 +134,38 @@ export default function SiteLayout() { setDragging(dragging + dragChange); } + // Navigate and dismiss the mobile drawer, matching hamburger menu conventions. + function navigateAndClose(key: string) { + navigate(key); + setDrawerOpen(false); + } + + // The header trigger collapses the sider on desktop and toggles the overlay drawer on phones. + function toggleSidebar() { + if (isMobile) { + setDrawerOpen(value => !value); + } else { + setCollapsed(value => !value); + } + } + + let sidebarContent = navigateAndClose(key)} + items={menuItems} + />; + + let accountMenu = + + ; + return setDraggingProxy(1)} @@ -172,69 +209,69 @@ export default function SiteLayout() { setAboutOpen(wasAboutOpen); }} /> +
+
0}> - - - - Clusterio logo - - Clusterio - - setChangeLogOpen(true)} - > - {webUiPackage.version} - - - - - navigate(key)} - items={menuItems} - /> - , - label: account.name, - children: accountMenuProps.items, - }]} - /> - - - - - {combinedPages.map(({ path, sidebarPath, content }) => - - - {content} - - } - />)} - } /> - + + {isMobile + ? setDrawerOpen(false)} + width={250} + closable={false} + // Start below the sticky 64px header so it stays visible and usable. + rootStyle={{ top: 64 }} + styles={{ body: { padding: 0, background: "#001529" } }} + > + {sidebarContent} + + : + {sidebarContent} + + } + + + {combinedPages.map(({ path, sidebarPath, content }) => + + + {content} + + } + />)} + } /> + + ; diff --git a/packages/web_ui/src/style.css b/packages/web_ui/src/style.css index 9a07fc34..ef9fcdf4 100644 --- a/packages/web_ui/src/style.css +++ b/packages/web_ui/src/style.css @@ -33,17 +33,13 @@ body { } .site-breadcrumb { - margin: 16px 0 16px 52px; + margin: 16px 0; } @media (min-width: 576px) { .site-layout-content-container { padding: 0 24px 24px; } - - .site-breadcrumb { - margin-left: 28px; - } } @media (min-width: 768px) { @@ -56,14 +52,32 @@ body { } } -/* Keep the sidebar fixed in place while the page content scrolls */ -.site-layout-sider { +/* Top bar hosting the menu toggle and branding, sticky above the content. */ +.site-layout-header { position: sticky; top: 0; - height: 100vh; + z-index: 100; + display: flex; + align-items: center; + gap: 16px; + padding-inline: 16px; + background: #001529; +} + +/* Keep the sidebar fixed in place (below the header) while content scrolls. */ +.site-layout-sider { + position: sticky; + top: 64px; + height: calc(100vh - 64px); align-self: flex-start; } +/* Snap the sider open/closed instead of animating its width — the width + transition reflowed the content area and caused styling glitches. */ +.ant-layout-sider.site-layout-sider { + transition: none; +} + .page-header { padding: 0 0 16px; } From d4394f1190f5c1b9f26f939001fd6b1d8d84a741 Mon Sep 17 00:00:00 2001 From: bbassie <17990055+bbassie@users.noreply.github.com> Date: Thu, 25 Jun 2026 02:15:53 +0000 Subject: [PATCH 3/4] Link the sidebar logo to the overview page Make the Clusterio logo in the top bar a link to / so it lands on the overview/dashboard page. Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/web_ui/src/components/SiteLayout.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/web_ui/src/components/SiteLayout.tsx b/packages/web_ui/src/components/SiteLayout.tsx index f74b5ab5..3ff57a60 100644 --- a/packages/web_ui/src/components/SiteLayout.tsx +++ b/packages/web_ui/src/components/SiteLayout.tsx @@ -8,6 +8,7 @@ import ErrorPage from "./ErrorPage"; import ControlContext from "./ControlContext"; import ChangeLogModal from "./ChangeLogModal"; import AboutModal from "./AboutModal"; +import Link from "./Link"; import { pages } from "../pages"; import { saveJson } from "../util/save_file"; @@ -216,7 +217,9 @@ export default function SiteLayout() { onClick={toggleSidebar} aria-label="Toggle navigation menu" /> - Clusterio logo + + Clusterio logo + Clusterio From cc0be690193d7bc9f64ab06c5d97e8767e9c4255 Mon Sep 17 00:00:00 2001 From: bbassie <17990055+bbassie@users.noreply.github.com> Date: Thu, 25 Jun 2026 18:41:25 +0000 Subject: [PATCH 4/4] Enlarge the sidebar logo to 56px Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/web_ui/src/components/SiteLayout.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/web_ui/src/components/SiteLayout.tsx b/packages/web_ui/src/components/SiteLayout.tsx index 3ff57a60..6a6adcad 100644 --- a/packages/web_ui/src/components/SiteLayout.tsx +++ b/packages/web_ui/src/components/SiteLayout.tsx @@ -218,7 +218,7 @@ export default function SiteLayout() { aria-label="Toggle navigation menu" /> - Clusterio logo + Clusterio logo Clusterio