-
{brandIcon}
-
+
+
+ {onToggleCollapsed ? (
+
+ ) : (
+
+ {brandIcon}
+
+ )}
+
{brandLabel}
@@ -40,7 +88,15 @@ export function AppSidebar({
-
+
{footerContent}
diff --git a/common/shell/index.ts b/common/shell/index.ts
index 2de0b142..8194d31c 100644
--- a/common/shell/index.ts
+++ b/common/shell/index.ts
@@ -27,6 +27,8 @@ type ShellProfileSummaryParams = {
export const SHELL_THEME_STORAGE_KEY = "admin_theme";
export const SHELL_SESSION_EXPIRY_STORAGE_KEY = SESSION_EXPIRY_STORAGE_KEY;
+export const SHELL_SIDEBAR_COLLAPSED_STORAGE_KEY =
+ "baron_shell_sidebar_collapsed";
export type { ShellSidebarNavItem } from "./AppSidebar";
export { AppSidebar } from "./AppSidebar";
export { shellLayoutClasses } from "./layout";
@@ -52,6 +54,25 @@ export function writeShellSessionExpiryEnabled(isEnabled: boolean) {
writeSessionExpiryEnabled(isEnabled);
}
+export function readShellSidebarCollapsed(defaultCollapsed = false) {
+ const stored = window.localStorage.getItem(
+ SHELL_SIDEBAR_COLLAPSED_STORAGE_KEY,
+ );
+
+ if (stored === null) {
+ return defaultCollapsed;
+ }
+
+ return stored === "true";
+}
+
+export function writeShellSidebarCollapsed(isCollapsed: boolean) {
+ window.localStorage.setItem(
+ SHELL_SIDEBAR_COLLAPSED_STORAGE_KEY,
+ String(isCollapsed),
+ );
+}
+
export function buildShellProfileSummary({
profileName,
profileEmail,
diff --git a/common/shell/layout.ts b/common/shell/layout.ts
index 96b002d9..7227efaa 100644
--- a/common/shell/layout.ts
+++ b/common/shell/layout.ts
@@ -1,22 +1,34 @@
export const shellLayoutClasses = {
root: "grid min-h-screen grid-cols-[240px,minmax(0,1fr)] bg-background text-foreground",
+ rootCollapsed:
+ "grid min-h-screen grid-cols-[80px,minmax(0,1fr)] bg-background text-foreground",
aside:
"sticky top-0 flex h-screen flex-col justify-between border-r border-border bg-card backdrop-blur",
+ asideCollapsed:
+ "sticky top-0 flex h-screen flex-col justify-between border-r border-border bg-card backdrop-blur",
asideStatic:
"sticky top-0 h-screen border-r border-border bg-card backdrop-blur",
brandSection:
"flex items-center justify-between px-5 py-4 md:block md:space-y-6 md:py-6",
+ brandSectionCollapsed:
+ "flex items-center justify-between px-3 py-4 md:block md:space-y-4 md:px-2 md:py-6",
brandWrap: "flex items-center gap-3 md:flex-col md:items-start",
+ brandWrapCollapsed: "flex items-center gap-3 md:flex-col md:items-center",
brandIcon:
"grid h-11 w-11 place-items-center rounded-xl bg-primary/15 text-primary shadow-[0_12px_30px_rgba(54,211,153,0.22)]",
+ brandIconCollapsed:
+ "grid h-11 w-11 place-items-center rounded-xl bg-primary/15 text-primary shadow-[0_12px_30px_rgba(54,211,153,0.22)]",
scopeBadge:
"hidden rounded-full border border-border px-3 py-2 text-xs text-muted-foreground md:inline-flex md:items-center md:gap-2",
navWrap: "px-2 pb-4 md:px-3 md:pb-8",
+ navWrapCollapsed: "px-2 pb-4 md:px-2 md:pb-8",
navMeta:
"flex flex-wrap gap-2 px-3 pb-4 text-[11px] text-muted-foreground md:flex-col md:items-start",
navList: "flex flex-col gap-1",
navItemBase:
"flex items-center gap-3 rounded-xl px-3 py-3 text-sm transition",
+ navItemBaseCollapsed:
+ "flex items-center justify-center gap-0 rounded-xl px-3 py-3 text-sm transition",
navItemActive:
"bg-primary/10 text-primary shadow-[0_12px_40px_rgba(54,211,153,0.18)]",
navItemIdle: "text-muted-foreground hover:bg-muted/10 hover:text-foreground",
@@ -24,6 +36,8 @@ export const shellLayoutClasses = {
"hidden space-y-2 px-5 pb-6 pt-2 text-xs text-[var(--color-muted)] md:block",
logoutButton:
"flex w-full items-center gap-3 rounded-xl px-3 py-3 text-sm text-muted-foreground transition hover:bg-destructive/10 hover:text-destructive",
+ logoutButtonCollapsed:
+ "flex w-full items-center justify-center gap-0 rounded-xl px-3 py-3 text-sm text-muted-foreground transition hover:bg-destructive/10 hover:text-destructive",
header:
"sticky top-0 z-20 border-b border-border bg-background/90 backdrop-blur",
headerElevated:
@@ -31,8 +45,11 @@ export const shellLayoutClasses = {
headerInner: "flex items-center justify-between px-5 py-4 md:px-8",
headerTitleWrap: "flex flex-col gap-1",
headerActions: "flex items-center gap-2 text-sm",
+ headerActionsCollapsed: "flex items-center gap-2 text-sm",
actionButton:
"inline-flex items-center gap-2 rounded-full border border-border px-3 py-2 text-muted-foreground transition hover:bg-muted/20",
+ sidebarToggleButton:
+ "inline-flex items-center gap-2 rounded-full border border-border px-3 py-2 text-muted-foreground transition hover:bg-muted/20",
sessionBadge:
"hidden rounded-full border px-3 py-2 text-xs font-medium md:inline-flex",
profileInitial:
diff --git a/devfront/src/components/layout/AppLayout.test.tsx b/devfront/src/components/layout/AppLayout.test.tsx
index 9bdac4e8..c4167588 100644
--- a/devfront/src/components/layout/AppLayout.test.tsx
+++ b/devfront/src/components/layout/AppLayout.test.tsx
@@ -116,6 +116,24 @@ describe("devfront AppLayout", () => {
expect(document.documentElement.classList.contains("light")).toBe(true);
});
+ it("toggles the sidebar and persists the collapsed state", async () => {
+ const container = await renderLayout();
+
+ const collapseButton = container.querySelector(
+ 'button[aria-label="Collapse sidebar"]',
+ ) as HTMLButtonElement;
+ await act(async () => {
+ collapseButton.click();
+ });
+
+ expect(window.localStorage.getItem("baron_shell_sidebar_collapsed")).toBe(
+ "true",
+ );
+ expect(
+ container.querySelector('button[aria-label="Expand sidebar"]'),
+ ).not.toBeNull();
+ });
+
it("toggles profile menu, navigates to profile, toggles theme, and logs out", async () => {
const container = await renderLayout();
diff --git a/devfront/src/components/layout/AppLayout.tsx b/devfront/src/components/layout/AppLayout.tsx
index 9df95752..3572cc18 100644
--- a/devfront/src/components/layout/AppLayout.tsx
+++ b/devfront/src/components/layout/AppLayout.tsx
@@ -19,11 +19,13 @@ import {
buildShellProfileSummary,
buildShellSessionStatus,
readShellSessionExpiryEnabled,
+ readShellSidebarCollapsed,
readShellTheme,
type ShellSidebarNavItem,
type ShellTranslator,
shellLayoutClasses,
writeShellSessionExpiryEnabled,
+ writeShellSidebarCollapsed,
} from "../../../../common/shell";
import { fetchMe } from "../../features/auth/authApi";
import { t } from "../../lib/i18n";
@@ -118,6 +120,9 @@ function AppLayout() {
const isDevelopmentRuntime = import.meta.env.MODE === "development";
const [theme, setTheme] = useState<"light" | "dark">(readShellTheme);
const [isProfileMenuOpen, setIsProfileMenuOpen] = useState(false);
+ const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(() =>
+ readShellSidebarCollapsed(false),
+ );
const [, setDevelopmentRenderRevision] = useState(0);
const [isSessionExpiryEnabled, setIsSessionExpiryEnabled] = useState(() =>
readShellSessionExpiryEnabled(!isDevelopmentRuntime),
@@ -352,26 +357,42 @@ function AppLayout() {
return next;
});
};
+ const handleSidebarToggle = () => {
+ setIsSidebarCollapsed((prev) => {
+ const next = !prev;
+ writeShellSidebarCollapsed(next);
+ return next;
+ });
+ };
const sidebarNavContent = (
- {navItems.map(({ labelKey, labelFallback, to, icon: Icon }) => (
-
- [
- shellLayoutClasses.navItemBase,
- isActive
- ? shellLayoutClasses.navItemActive
- : shellLayoutClasses.navItemIdle,
- ].join(" ")
- }
- >
-
- {t(labelKey, labelFallback)}
-
- ))}
+ {navItems.map(({ labelKey, labelFallback, to, icon: Icon }) => {
+ const label = t(labelKey, labelFallback);
+
+ return (
+
+ [
+ shellLayoutClasses.navItemBase,
+ isSidebarCollapsed
+ ? shellLayoutClasses.navItemBaseCollapsed
+ : "",
+ isActive
+ ? shellLayoutClasses.navItemActive
+ : shellLayoutClasses.navItemIdle,
+ ].join(" ")
+ }
+ title={label}
+ aria-label={label}
+ >
+
+ {label}
+
+ );
+ })}
);
const sidebarFooterContent = (
@@ -379,22 +400,39 @@ function AppLayout() {