now left menu in admin panel can be minimized

This commit is contained in:
onysd 2026-09-08 02:24:58 +03:00
parent db40f100cd
commit eb69c8500c
8 changed files with 119 additions and 16 deletions

View file

@ -5,17 +5,20 @@ export function AppLink({
href,
navigate,
className,
title,
children
}: {
href: string;
navigate: Navigate;
className?: string;
title?: string;
children: ReactNode;
}) {
return (
<a
className={className}
href={href}
title={title}
onClick={(event) => {
event.preventDefault();
navigate(href);

View file

@ -9,6 +9,8 @@ import {
LogOut,
Megaphone,
MessageSquareText,
PanelLeftClose,
PanelLeftOpen,
Settings,
UserCog,
UserRound,
@ -113,6 +115,29 @@ export function Shell({
// sections are granted independently, so one entry can be visible without the other.
const canReviewBotVerification = useCan(permissionBotVerificationReview);
const canManageServer = useCan(permissionServerManage);
// Remembered per browser: an operator who works in a narrow window should not
// have to re-collapse the navigation on every visit. A failed read (private
// mode, blocked storage) just means the default.
const [navCollapsed, setNavCollapsed] = useState(() => {
try {
return localStorage.getItem("owpengram.nav.collapsed") === "1";
} catch {
return false;
}
});
function toggleNav() {
setNavCollapsed((current) => {
const next = !current;
try {
localStorage.setItem("owpengram.nav.collapsed", next ? "1" : "0");
} catch {
// Not being able to remember the choice is not a reason to refuse it.
}
return next;
});
}
const [addServerLinkOpen, setAddServerLinkOpen] = useState(false);
const [connecting, setConnecting] = useState(false);
const [connectError, setConnectError] = useState("");
@ -221,7 +246,7 @@ export function Shell({
}
return (
<div className="shell">
<div className={`shell ${navCollapsed ? "shell--nav-collapsed" : ""}`.trim()}>
<aside className="sidebar">
<AppLink className="brand" href="/" navigate={navigate}>
<span className="brand-mark"><img src={brandIconSrc} alt={brandName} onError={() => setBrandIconFailed(true)} /></span>
@ -326,7 +351,17 @@ export function Shell({
</aside>
<div className="workspace">
<header className="topbar">
<div>
<div className="topbar-lead">
<button
className="icon-btn nav-toggle"
type="button"
onClick={toggleNav}
aria-expanded={!navCollapsed}
aria-label={navCollapsed ? "Expand navigation" : "Collapse navigation"}
title={navCollapsed ? "Expand navigation" : "Collapse navigation"}
>
{navCollapsed ? <PanelLeftOpen size={16} /> : <PanelLeftClose size={16} />}
</button>
<h1>{routeTitle(route.path)}</h1>
</div>
<div className="topbar-actions">
@ -363,9 +398,16 @@ function NavLink({
}) {
const active = activeWhen ? activeWhen(route.path) : href === "/" ? route.path === "/" : route.path.startsWith(href);
return (
<AppLink className={`nav-item ${active ? "active" : ""}`} href={href} navigate={navigate}>
<AppLink
className={`nav-item ${active ? "active" : ""}`}
href={href}
navigate={navigate}
// The label is hidden when the sidebar is collapsed, so it moves to the
// tooltip -- an icon rail with no names is a memory test.
title={typeof children === "string" ? children : undefined}
>
{icon ?? <span aria-hidden="true" className="nav-dot" />}
<span>{children}</span>
<span className="nav-item-label">{children}</span>
</AppLink>
);
}

View file

@ -506,3 +506,61 @@ a {
text-transform: uppercase;
letter-spacing: 0.04em;
}
/* Collapsible navigation.
Scoped to the width where the sidebar is actually a sidebar: below 1120px the
shell already stacks it into a horizontal strip (05-responsive.css), and
collapsing that would just hide the labels of a bar that has room for them. */
@media (min-width: 1121px) {
.shell--nav-collapsed {
grid-template-columns: 68px minmax(0, 1fr);
}
/* Everything that only makes sense with room for words goes away: the
wordmark beside the logo, the "Navigation" heading, the version block and
the Connect/Share buttons. The icons and their tooltips remain. */
.shell--nav-collapsed .brand > span:not(.brand-mark),
.shell--nav-collapsed .sidebar-label,
.shell--nav-collapsed .sidebar-status,
.shell--nav-collapsed .sidebar-server-actions,
.shell--nav-collapsed .nav-item-label {
display: none;
}
.shell--nav-collapsed .sidebar {
padding: 18px 10px;
align-items: center;
}
.shell--nav-collapsed .brand {
justify-content: center;
padding: 0;
}
/* One column instead of icon + label, so the glyph sits centred in the rail
rather than clinging to the left edge where the text used to start. */
.shell--nav-collapsed .nav-item {
grid-template-columns: 1fr;
justify-items: center;
padding: 0;
width: 44px;
}
.shell--nav-collapsed .nav-list {
justify-items: center;
}
}
/* The toggle sits with the page title rather than inside the sidebar: it has to
stay reachable at 68px wide, and a control that hides with the thing it
controls is a trap. */
.topbar-lead {
display: flex;
min-width: 0;
align-items: center;
gap: 12px;
}
.nav-toggle {
flex: 0 0 auto;
}