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