mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-15 07:24:52 +09:00
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) <noreply@anthropic.com>
This commit is contained in:
1 parent
262357acef
commit
ed84a476c6
2 files changed
+125
-74
No files matched your search
@@ -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<MenuProps>["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<string | null>(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 = <Menu
|
||||
theme="dark"
|
||||
mode="inline"
|
||||
defaultOpenKeys={[...menuGroups.keys()]}
|
||||
selectedKeys={currentSidebarPath ? [currentSidebarPath] : []}
|
||||
style={{ height: "100%", overflow: "auto", borderInlineEnd: 0 }}
|
||||
onClick={({ key }) => navigateAndClose(key)}
|
||||
items={menuItems}
|
||||
/>;
|
||||
|
||||
let accountMenu = <Dropdown menu={accountMenuProps} trigger={["click"]} placement="bottomRight">
|
||||
<Button type="text" icon={<UserOutlined />} style={{ marginInlineStart: "auto" }}>
|
||||
{account.name}
|
||||
</Button>
|
||||
</Dropdown>;
|
||||
|
||||
return <Layout
|
||||
hasSider
|
||||
className="site-layout"
|
||||
style={{ minHeight: "100vh" }}
|
||||
onDragEnter={() => setDraggingProxy(1)}
|
||||
@@ -172,69 +209,69 @@ export default function SiteLayout() {
|
||||
setAboutOpen(wasAboutOpen);
|
||||
}}
|
||||
/>
|
||||
<Header className="site-layout-header">
|
||||
<Button
|
||||
type="text"
|
||||
icon={<MenuOutlined />}
|
||||
onClick={toggleSidebar}
|
||||
aria-label="Toggle navigation menu"
|
||||
/>
|
||||
<img src={logo} width={32} height={32} alt="Clusterio logo" />
|
||||
<Flex vertical justify="center">
|
||||
<Typography.Title level={4} style={{ margin: 0, lineHeight: 1.2 }}>Clusterio</Typography.Title>
|
||||
<Tooltip title="View changelog" placement="right">
|
||||
<Typography.Text
|
||||
type="danger"
|
||||
style={{ cursor: "pointer", lineHeight: 1.2 }}
|
||||
onClick={() => setChangeLogOpen(true)}
|
||||
>
|
||||
{webUiPackage.version}
|
||||
</Typography.Text>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
{accountMenu}
|
||||
</Header>
|
||||
<DraggingContext.Provider value={dragging > 0}>
|
||||
<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={{ flex: 1, overflow: "auto", borderInlineEnd: 0 }}
|
||||
onClick={({ key }) => navigate(key)}
|
||||
items={menuItems}
|
||||
/>
|
||||
<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 hasSider={!isMobile}>
|
||||
{isMobile
|
||||
? <Drawer
|
||||
placement="left"
|
||||
open={drawerOpen}
|
||||
onClose={() => 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}
|
||||
</Drawer>
|
||||
: <Sider
|
||||
width={250}
|
||||
collapsible
|
||||
collapsed={collapsed}
|
||||
collapsedWidth={0}
|
||||
trigger={null}
|
||||
className="site-layout-sider"
|
||||
>
|
||||
{sidebarContent}
|
||||
</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>
|
||||
</Layout>
|
||||
</DraggingContext.Provider>
|
||||
</Layout>;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user