diff --git a/packages/web_ui/src/components/SiteLayout.tsx b/packages/web_ui/src/components/SiteLayout.tsx index 76338056..6a6adcad 100644 --- a/packages/web_ui/src/components/SiteLayout.tsx +++ b/packages/web_ui/src/components/SiteLayout.tsx @@ -1,25 +1,27 @@ 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 { 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"; 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"; 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, Header } = Layout; function isActiveDropzone(element: HTMLElement | null): boolean { if (!element) { @@ -42,6 +44,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); @@ -126,7 +135,39 @@ 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)} onDragLeave={() => setDraggingProxy(-1)} @@ -169,56 +210,56 @@ export default function SiteLayout() { setAboutOpen(wasAboutOpen); }} /> - 0}> -
-
- Clusterio - - +
- - - navigate(key)} - items={menuItems} - /> - + + {accountMenu} + + 0}> + + {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 }) =>