mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-14 07:24:52 +09:00
Merge pull request #924 from bbassie/feature/sidebar-layout
Rework site layout into a fixed sidebar
This commit is contained in:
2 files changed
+113
-128
No files matched your search
@@ -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<MenuProps>["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<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);
|
||||
@@ -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 = <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
|
||||
className="site-layout"
|
||||
style={{ minHeight: "100vh" }}
|
||||
onDragEnter={() => setDraggingProxy(1)}
|
||||
onDragLeave={() => setDraggingProxy(-1)}
|
||||
@@ -169,56 +210,56 @@ export default function SiteLayout() {
|
||||
setAboutOpen(wasAboutOpen);
|
||||
}}
|
||||
/>
|
||||
<DraggingContext.Provider value={dragging > 0}>
|
||||
<Header className="header">
|
||||
<div className="site-logo" />
|
||||
<span className="site-name">Clusterio</span>
|
||||
<Tooltip
|
||||
title="View changelog"
|
||||
placement="bottom"
|
||||
align={{ offset: [0, -10] }}
|
||||
>
|
||||
<span
|
||||
className="site-version"
|
||||
style={{ cursor: "pointer" }}
|
||||
<Header className="site-layout-header">
|
||||
<Button
|
||||
type="text"
|
||||
icon={<MenuOutlined />}
|
||||
onClick={toggleSidebar}
|
||||
aria-label="Toggle navigation menu"
|
||||
/>
|
||||
<Link to="/" style={{ display: "flex", alignItems: "center" }}>
|
||||
<img src={logo} width={56} height={56} alt="Clusterio logo" />
|
||||
</Link>
|
||||
<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}
|
||||
</span>
|
||||
</Typography.Text>
|
||||
</Tooltip>
|
||||
<Menu
|
||||
theme="dark"
|
||||
mode="horizontal"
|
||||
defaultSelectedKeys={["1"]}
|
||||
items={[{ label: "Dashboard", key: "1" }]}
|
||||
/>
|
||||
<Dropdown
|
||||
className="account-dropdown-header"
|
||||
placement="bottomRight"
|
||||
trigger={["click"]}
|
||||
menu={accountMenuProps}
|
||||
>
|
||||
<UserOutlined />
|
||||
</Dropdown>
|
||||
</Header>
|
||||
<Layout className="site-layout">
|
||||
<Sider
|
||||
collapsible
|
||||
collapsedWidth={0}
|
||||
breakpoint="md"
|
||||
zeroWidthTriggerStyle={{ top: 6, zIndex: -1 }}
|
||||
width={250}
|
||||
className="site-layout-sider"
|
||||
>
|
||||
<Menu
|
||||
mode="inline"
|
||||
defaultOpenKeys={[...menuGroups.keys()]}
|
||||
selectedKeys={currentSidebarPath ? [currentSidebarPath] : []}
|
||||
style={{ height: "100%", borderRight: 0 }}
|
||||
onClick={({ key }) => navigate(key)}
|
||||
items={menuItems}
|
||||
/>
|
||||
</Sider>
|
||||
</Flex>
|
||||
{accountMenu}
|
||||
</Header>
|
||||
<DraggingContext.Provider value={dragging > 0}>
|
||||
<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
|
||||
|
||||
@@ -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,13 +52,30 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
.site-layout-sider {
|
||||
z-index: 0;
|
||||
/* Top bar hosting the menu toggle and branding, sticky above the content. */
|
||||
.site-layout-header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding-inline: 16px;
|
||||
background: #001529;
|
||||
}
|
||||
|
||||
.header {
|
||||
padding-left: 250px;
|
||||
padding-right: 64px;
|
||||
/* 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 {
|
||||
@@ -105,75 +118,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;
|
||||
|
||||
Reference in new issue
Block a user