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] 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; }