diff --git a/ui/litellm-dashboard/e2e_tests/tests/auth/logout.spec.ts b/ui/litellm-dashboard/e2e_tests/tests/auth/logout.spec.ts index 1e2f2269dbe..351ba91e8d7 100644 --- a/ui/litellm-dashboard/e2e_tests/tests/auth/logout.spec.ts +++ b/ui/litellm-dashboard/e2e_tests/tests/auth/logout.spec.ts @@ -9,22 +9,17 @@ test.describe("Logout", () => { // Scope to the sidebar; the top-bar breadcrumb also shows "Virtual Keys". await expect(page.getByRole("complementary").getByText("Virtual Keys")).toBeVisible({ timeout: 10_000 }); - // Open the navbar User dropdown. The trigger button exposes an aria-label - // of "Account menu — — signed in as ", and the antd Dropdown - // is declared with trigger={["click"]}, so a plain click opens the popup. + // Open the sidebar account menu. The trigger button exposes an aria-label + // of "Account menu — — signed in as "; clicking it opens the + // Base UI popover panel. await page.getByRole("button", { name: /Account menu/i }).click(); - const popup = page - .locator(".ant-dropdown:visible") - .filter({ - has: page.locator(".bg-white.rounded-lg.shadow-lg"), - }) - .first(); + const popup = page.getByTestId("sidebar-account-menu-panel"); await expect(popup).toBeVisible({ timeout: 5_000 }); // Click Logout — the handler clears the auth cookie and navigates via // window.location.href = PROXY_LOGOUT_URL (empty string in the e2e env). - await popup.getByText("Logout", { exact: true }).click(); + await popup.getByRole("button", { name: "Logout" }).click(); // The cookie is now gone — visiting a protected page must redirect to /ui/login. await page.goto("/ui?page=llm-playground", { waitUntil: "domcontentloaded" }); diff --git a/ui/litellm-dashboard/e2e_tests/tests/auth/proxyLogoutUrl.spec.ts b/ui/litellm-dashboard/e2e_tests/tests/auth/proxyLogoutUrl.spec.ts index 9faf6741333..7f6cc6f2f87 100644 --- a/ui/litellm-dashboard/e2e_tests/tests/auth/proxyLogoutUrl.spec.ts +++ b/ui/litellm-dashboard/e2e_tests/tests/auth/proxyLogoutUrl.spec.ts @@ -34,8 +34,8 @@ test.describe("PROXY_LOGOUT_URL redirect", () => { }), ); - // navbar.tsx populates the logout target only after the proxy UI settings - // fetch (/sso/get/ui_settings) resolves. Clicking Logout before that lands + // The logout handler populates the logout target only after the proxy UI + // settings fetch (/sso/get/ui_settings) resolves. Clicking Logout before that lands // runs `window.location.href = ""` — a same-origin reload, not a redirect — // so gate the click on the settings response, not just on first paint. const settingsLoaded = page.waitForResponse((r) => r.url().includes("/sso/get/ui_settings") && r.ok(), { @@ -51,10 +51,10 @@ test.describe("PROXY_LOGOUT_URL redirect", () => { const tokensBefore = (await page.context().cookies()).filter((c) => c.name === "token"); expect(tokensBefore.length, "should start logged in with a token cookie").toBeGreaterThan(0); - // Open the navbar account dropdown (trigger=click) and click Logout by role - // rather than internal Ant Design CSS classes, which are not a stable API. + // Open the sidebar account menu and click Logout by role rather than by + // internal CSS classes, which are not a stable API. await page.getByRole("button", { name: /^Account menu/ }).click(); - const logout = page.getByRole("menuitem", { name: "Logout" }); + const logout = page.getByRole("button", { name: "Logout" }); await expect(logout).toBeVisible({ timeout: 5_000 }); // handleLogout clears cookies/local storage, then assigns window.location.href. diff --git a/ui/litellm-dashboard/e2e_tests/tests/login/internalUserIdentity.spec.ts b/ui/litellm-dashboard/e2e_tests/tests/login/internalUserIdentity.spec.ts index 569908c5f75..101b91daeb1 100644 --- a/ui/litellm-dashboard/e2e_tests/tests/login/internalUserIdentity.spec.ts +++ b/ui/litellm-dashboard/e2e_tests/tests/login/internalUserIdentity.spec.ts @@ -27,13 +27,13 @@ test.describe("Navbar identity scoping", () => { { timeout: 5_000 }, ); - // Open the dropdown (UserDropdown configures trigger=["click"]). + // Open the account menu (click to open the Base UI popover). await accountButton.click(); - // Locate the panel by its test id (data-testid on the popupRender div in - // UserDropdown.tsx) rather than Ant/Tailwind class names, so styling - // refactors don't silently break the identity-scoping assertions below. - const popup = page.getByTestId("user-dropdown-panel"); + // Locate the panel by its test id (data-testid on SidebarAccountMenu's + // popover content) rather than class names, so styling refactors don't + // silently break the identity-scoping assertions below. + const popup = page.getByTestId("sidebar-account-menu-panel"); await expect(popup).toBeVisible({ timeout: 5_000 }); // The popup must show the internal user's identity — not the seeded diff --git a/ui/litellm-dashboard/e2e_tests/tests/login/login.spec.ts b/ui/litellm-dashboard/e2e_tests/tests/login/login.spec.ts index 11febf0ed48..88378df36c3 100644 --- a/ui/litellm-dashboard/e2e_tests/tests/login/login.spec.ts +++ b/ui/litellm-dashboard/e2e_tests/tests/login/login.spec.ts @@ -12,22 +12,14 @@ test("user can log in", async ({ page }) => { // Scope to the sidebar; the top-bar breadcrumb also shows "Virtual Keys". await expect(page.getByRole("complementary").getByText("Virtual Keys")).toBeVisible(); - // Match the navbar account button by its stable aria-label (UserDropdown.tsx - // emits "Account menu — — signed in as "). Earlier this used - // `hasText: /^User$/`, which never matched the rendered button (text is - // displayName = "Account" for the master-key admin), so the trigger evaluate - // would time out in CI. + // Match the sidebar account button by its stable aria-label + // (SidebarAccountMenu emits "Account menu — — signed in as "; + // displayName is "Account" for the master-key admin, so match on the label). const userTrigger = page.locator('button[aria-label^="Account menu"]').first(); await userTrigger.click(); - // Filter by the popupRender wrapper class to disambiguate from other - // ant-dropdown popups. - const popup = page - .locator(".ant-dropdown:visible") - .filter({ - has: page.locator(".bg-white.rounded-lg.shadow-lg"), - }) - .first(); + // The account menu is a Base UI popover; locate its panel by test id. + const popup = page.getByTestId("sidebar-account-menu-panel"); await expect(popup).toBeVisible({ timeout: 5_000 }); await expect(popup.getByText("Admin", { exact: true })).toBeVisible({ timeout: 5_000 }); await expect(popup.getByText("default_user_id", { exact: true })).toBeVisible({ timeout: 5_000 }); diff --git a/ui/litellm-dashboard/src/components/SidebarAccountMenu/SidebarAccountMenu.test.tsx b/ui/litellm-dashboard/src/components/SidebarAccountMenu/SidebarAccountMenu.test.tsx new file mode 100644 index 00000000000..716a2ebe6e2 --- /dev/null +++ b/ui/litellm-dashboard/src/components/SidebarAccountMenu/SidebarAccountMenu.test.tsx @@ -0,0 +1,302 @@ +import userEvent from "@testing-library/user-event"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { renderWithProviders, screen, waitFor } from "../../../tests/test-utils"; +import SidebarAccountMenu from "./SidebarAccountMenu"; + +interface AuthMock { + userId: string | null; + userEmail: string | null; + userRole: string; + premiumUser: boolean; + accessToken: string; +} + +let mockUseAuthorizedImpl: () => AuthMock = () => ({ + userId: "test-user-id", + userEmail: "test@example.com", + userRole: "Admin", + premiumUser: false, + accessToken: "test-token", +}); + +let mockUseDisableShowPromptsImpl = () => false; +let mockUseDisableBouncingIconImpl = () => false; +let mockHealthDataImpl = (): { litellm_version?: string } | undefined => ({ litellm_version: "1.99.0" }); + +let mockGetLocalStorageItemImpl = (key: string): string | null => { + if (key === "disableShowNewBadge") return null; + if (key === "disableShowPrompts") return null; + return null; +}; + +vi.mock("@/app/(dashboard)/hooks/useAuthorized", () => ({ + default: () => mockUseAuthorizedImpl(), +})); + +vi.mock("@/app/(dashboard)/hooks/useDisableShowPrompts", () => ({ + useDisableShowPrompts: () => mockUseDisableShowPromptsImpl(), +})); + +vi.mock("@/app/(dashboard)/hooks/useDisableUsageIndicator", () => ({ + useDisableUsageIndicator: () => false, +})); + +vi.mock("@/app/(dashboard)/hooks/useDisableBlogPosts", () => ({ + useDisableBlogPosts: () => false, +})); + +vi.mock("@/app/(dashboard)/hooks/useDisableBouncingIcon", () => ({ + useDisableBouncingIcon: () => mockUseDisableBouncingIconImpl(), +})); + +vi.mock("@/app/(dashboard)/hooks/healthReadiness/useHealthReadinessDetails", () => ({ + useHealthReadinessDetails: () => ({ data: mockHealthDataImpl() }), +})); + +vi.mock("@/utils/localStorageUtils", () => ({ + LOCAL_STORAGE_EVENT: "local-storage-change", + getLocalStorageItem: (key: string) => mockGetLocalStorageItemImpl(key), + setLocalStorageItem: vi.fn(), + removeLocalStorageItem: vi.fn(), + emitLocalStorageChange: vi.fn(), +})); + +describe("SidebarAccountMenu", () => { + const mockOnLogout = vi.fn(); + + const getAccountTrigger = () => screen.getByRole("button", { name: /account menu/i }); + + const openMenu = async (user: ReturnType) => { + await user.click(getAccountTrigger()); + await waitFor(() => { + expect(screen.getByTestId("sidebar-account-menu-panel")).toBeInTheDocument(); + }); + }; + + beforeEach(() => { + vi.clearAllMocks(); + mockUseAuthorizedImpl = () => ({ + userId: "test-user-id", + userEmail: "test@example.com", + userRole: "Admin", + premiumUser: false, + accessToken: "test-token", + }); + mockUseDisableShowPromptsImpl = () => false; + mockUseDisableBouncingIconImpl = () => false; + mockHealthDataImpl = () => ({ litellm_version: "1.99.0" }); + mockGetLocalStorageItemImpl = (key: string): string | null => { + if (key === "disableShowNewBadge") return null; + if (key === "disableShowPrompts") return null; + return null; + }; + }); + + it("should render the account trigger with initials", () => { + renderWithProviders(); + expect(getAccountTrigger()).toBeInTheDocument(); + expect(screen.getByText("TE")).toBeInTheDocument(); + }); + + it("should render only the avatar (no name/role) when collapsed", () => { + renderWithProviders(); + expect(getAccountTrigger()).toBeInTheDocument(); + expect(screen.getByText("TE")).toBeInTheDocument(); + expect(screen.queryByText("Admin")).not.toBeInTheDocument(); + expect(screen.queryByText("test@example.com")).not.toBeInTheDocument(); + }); + + it("should show email, user ID, and role when the menu is opened", async () => { + const user = userEvent.setup(); + renderWithProviders(); + + await openMenu(user); + + expect(screen.getAllByText("test@example.com").length).toBeGreaterThan(0); + expect(screen.getByText("test-user-id")).toBeInTheDocument(); + expect(screen.getAllByText("Admin").length).toBeGreaterThan(0); + }); + + it("should display Standard tier for non-premium users", async () => { + const user = userEvent.setup(); + renderWithProviders(); + + await openMenu(user); + + expect(screen.getByText("Standard")).toBeInTheDocument(); + }); + + it("should display Premium tier for premium users", async () => { + const user = userEvent.setup(); + mockUseAuthorizedImpl = () => ({ + userId: "test-user-id", + userEmail: "test@example.com", + userRole: "Admin", + premiumUser: true, + accessToken: "test-token", + }); + + renderWithProviders(); + + await openMenu(user); + + expect(screen.getByText("Premium")).toBeInTheDocument(); + }); + + it("should render a clickable version badge linking to the release notes", async () => { + const user = userEvent.setup(); + renderWithProviders(); + + await openMenu(user); + + const versionLink = screen.getByRole("link", { name: /v1\.99\.0/ }); + expect(versionLink).toHaveAttribute("href", "https://docs.litellm.ai/release_notes"); + expect(versionLink).toHaveAttribute("target", "_blank"); + }); + + it("should not render the version badge when the version is unavailable", async () => { + const user = userEvent.setup(); + mockHealthDataImpl = () => undefined; + renderWithProviders(); + + await openMenu(user); + + expect(screen.queryByRole("link", { name: /^v/ })).not.toBeInTheDocument(); + }); + + it("should show the bouncing icon by default", async () => { + const user = userEvent.setup(); + renderWithProviders(); + await openMenu(user); + expect(screen.getByTitle("Thanks for using LiteLLM!")).toBeInTheDocument(); + }); + + it("should hide the bouncing icon when Hide Bouncing Icon is enabled", async () => { + const user = userEvent.setup(); + mockUseDisableBouncingIconImpl = () => true; + renderWithProviders(); + await openMenu(user); + expect(screen.queryByTitle("Thanks for using LiteLLM!")).not.toBeInTheDocument(); + }); + + it("should copy the email to the clipboard", async () => { + const user = userEvent.setup(); + const writeText = vi.fn().mockResolvedValue(undefined); + Object.defineProperty(navigator, "clipboard", { value: { writeText }, configurable: true }); + + renderWithProviders(); + await openMenu(user); + + await user.click(screen.getByRole("button", { name: "Copy email" })); + + expect(writeText).toHaveBeenCalledWith("test@example.com"); + }); + + it("should call onLogout when logout is clicked", async () => { + const user = userEvent.setup(); + renderWithProviders(); + + await openMenu(user); + + await user.click(screen.getByRole("button", { name: /logout/i })); + + expect(mockOnLogout).toHaveBeenCalledTimes(1); + }); + + it("should toggle hide new feature indicators on", async () => { + const user = userEvent.setup(); + renderWithProviders(); + + await openMenu(user); + + const toggle = screen.getByLabelText("Toggle hide new feature indicators"); + expect(toggle).not.toBeChecked(); + + await user.click(toggle); + + const localStorageUtils = vi.mocked(await import("@/utils/localStorageUtils")); + expect(localStorageUtils.setLocalStorageItem).toHaveBeenCalledWith("disableShowNewBadge", "true"); + expect(localStorageUtils.emitLocalStorageChange).toHaveBeenCalledWith("disableShowNewBadge"); + }); + + it("should toggle hide new feature indicators off", async () => { + const user = userEvent.setup(); + mockGetLocalStorageItemImpl = (key: string): string | null => { + if (key === "disableShowNewBadge") return "true"; + return null; + }; + + renderWithProviders(); + + await openMenu(user); + + const toggle = screen.getByLabelText("Toggle hide new feature indicators"); + expect(toggle).toBeChecked(); + + await user.click(toggle); + + const localStorageUtils = vi.mocked(await import("@/utils/localStorageUtils")); + expect(localStorageUtils.removeLocalStorageItem).toHaveBeenCalledWith("disableShowNewBadge"); + expect(localStorageUtils.emitLocalStorageChange).toHaveBeenCalledWith("disableShowNewBadge"); + }); + + it("should toggle hide all prompts on", async () => { + const user = userEvent.setup(); + renderWithProviders(); + + await openMenu(user); + + const toggle = screen.getByLabelText("Toggle hide all prompts"); + expect(toggle).not.toBeChecked(); + + await user.click(toggle); + + const localStorageUtils = vi.mocked(await import("@/utils/localStorageUtils")); + expect(localStorageUtils.setLocalStorageItem).toHaveBeenCalledWith("disableShowPrompts", "true"); + expect(localStorageUtils.emitLocalStorageChange).toHaveBeenCalledWith("disableShowPrompts"); + }); + + it("should initialize hide new feature indicators from localStorage", async () => { + const user = userEvent.setup(); + mockGetLocalStorageItemImpl = (key: string): string | null => { + if (key === "disableShowNewBadge") return "true"; + return null; + }; + + renderWithProviders(); + + await openMenu(user); + + const toggle = screen.getByLabelText("Toggle hide new feature indicators"); + expect(toggle).toBeChecked(); + }); + + it("should show Account in the trigger for the default placeholder user id", () => { + mockUseAuthorizedImpl = () => ({ + userId: "default_user_id", + userEmail: null, + userRole: "Admin", + premiumUser: false, + accessToken: "test-token", + }); + renderWithProviders(); + expect(screen.getByText("Account")).toBeInTheDocument(); + }); + + it("should display a dash when email is unavailable", async () => { + const user = userEvent.setup(); + mockUseAuthorizedImpl = () => ({ + userId: "test-user-id", + userEmail: null, + userRole: "Admin", + premiumUser: false, + accessToken: "test-token", + }); + + renderWithProviders(); + + await openMenu(user); + + expect(screen.getByText("-")).toBeInTheDocument(); + }); +}); diff --git a/ui/litellm-dashboard/src/components/SidebarAccountMenu/SidebarAccountMenu.tsx b/ui/litellm-dashboard/src/components/SidebarAccountMenu/SidebarAccountMenu.tsx new file mode 100644 index 00000000000..46d7271c9df --- /dev/null +++ b/ui/litellm-dashboard/src/components/SidebarAccountMenu/SidebarAccountMenu.tsx @@ -0,0 +1,295 @@ +import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; +import { useHealthReadinessDetails } from "@/app/(dashboard)/hooks/healthReadiness/useHealthReadinessDetails"; +import { useDisableBlogPosts } from "@/app/(dashboard)/hooks/useDisableBlogPosts"; +import { useDisableBouncingIcon } from "@/app/(dashboard)/hooks/useDisableBouncingIcon"; +import { useDisableShowNewBadge } from "@/app/(dashboard)/hooks/useDisableShowNewBadge"; +import { useDisableShowPrompts } from "@/app/(dashboard)/hooks/useDisableShowPrompts"; +import { useDisableUsageIndicator } from "@/app/(dashboard)/hooks/useDisableUsageIndicator"; +import { emitLocalStorageChange, removeLocalStorageItem, setLocalStorageItem } from "@/utils/localStorageUtils"; +import { navAccountDisplayName } from "@/components/Navbar/navDisplayName"; +import { Avatar, AvatarFallback } from "@/components/ui/avatar"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; +import { Separator } from "@/components/ui/separator"; +import { Switch } from "@/components/ui/switch"; +import { cn } from "@/lib/cva.config"; +import { Check, ChevronsUpDown, Copy, Crown, IdCard, LogOut, Mail, ShieldCheck } from "lucide-react"; +import React, { useEffect, useState } from "react"; + +const RELEASE_NOTES_URL = "https://docs.litellm.ai/release_notes"; + +function hueFromString(seed: string): number { + let h = 0; + for (let i = 0; i < seed.length; i += 1) { + h = seed.charCodeAt(i) + ((h << 5) - h); + } + return Math.abs(h) % 360; +} + +function initialsFromIdentity(email: string | null, userId: string | null): string { + const local = email?.split("@")[0]?.trim(); + if (local) { + const parts = local + .replace(/[^a-zA-Z0-9]+/g, " ") + .trim() + .split(/\s+/) + .filter(Boolean); + if (parts.length >= 2) { + return `${parts[0]!.charAt(0)}${parts[1]!.charAt(0)}`.toUpperCase(); + } + if (parts.length === 1) { + const p = parts[0]!; + return p.length >= 2 ? p.slice(0, 2).toUpperCase() : `${p.charAt(0)}`.toUpperCase(); + } + } + if (userId && userId.length >= 2) { + return userId.slice(0, 2).toUpperCase(); + } + if (userId && userId.length === 1) { + return `${userId.toUpperCase()}•`; + } + return "?"; +} + +const CopyButton: React.FC<{ value: string | null; label: string }> = ({ value, label }) => { + const [copied, setCopied] = useState(false); + + useEffect(() => { + if (!copied) return; + const timer = setTimeout(() => setCopied(false), 1200); + return () => clearTimeout(timer); + }, [copied]); + + if (!value) return null; + + return ( + + ); +}; + +const InfoRow: React.FC<{ icon: React.ReactNode; label: string; children: React.ReactNode }> = ({ + icon, + label, + children, +}) => ( +
+ + {icon} + {label} + + {children} +
+); + +const MonoValue: React.FC<{ value: string | null; copyLabel: string }> = ({ value, copyLabel }) => ( + + + {value || "-"} + + + +); + +interface SidebarAccountMenuProps { + onLogout: () => void; + collapsed?: boolean; +} + +const SidebarAccountMenu: React.FC = ({ onLogout, collapsed = false }) => { + const { userId, userEmail, userRole, premiumUser, accessToken } = useAuthorized(); + const { data: healthData } = useHealthReadinessDetails(accessToken); + const version = healthData?.litellm_version; + const disableShowPrompts = useDisableShowPrompts(); + const disableUsageIndicator = useDisableUsageIndicator(); + const disableBlogPosts = useDisableBlogPosts(); + const disableBouncingIcon = useDisableBouncingIcon(); + const disableShowNewBadge = useDisableShowNewBadge(); + + const setFlag = (key: string, checked: boolean) => { + if (checked) { + setLocalStorageItem(key, "true"); + } else { + removeLocalStorageItem(key); + } + emitLocalStorageChange(key); + }; + + const toggles = [ + { + key: "disableShowNewBadge", + label: "Hide New Feature Indicators", + ariaLabel: "Toggle hide new feature indicators", + checked: disableShowNewBadge, + onCheckedChange: (checked: boolean) => setFlag("disableShowNewBadge", checked), + }, + { + key: "disableShowPrompts", + label: "Hide All Prompts", + ariaLabel: "Toggle hide all prompts", + checked: disableShowPrompts, + onCheckedChange: (checked: boolean) => setFlag("disableShowPrompts", checked), + }, + { + key: "disableUsageIndicator", + label: "Hide Usage Indicator", + ariaLabel: "Toggle hide usage indicator", + checked: disableUsageIndicator, + onCheckedChange: (checked: boolean) => setFlag("disableUsageIndicator", checked), + }, + { + key: "disableBlogPosts", + label: "Hide Blog Posts", + ariaLabel: "Toggle hide blog posts", + checked: disableBlogPosts, + onCheckedChange: (checked: boolean) => setFlag("disableBlogPosts", checked), + }, + { + key: "disableBouncingIcon", + label: "Hide Bouncing Icon", + ariaLabel: "Toggle hide bouncing icon", + checked: disableBouncingIcon, + onCheckedChange: (checked: boolean) => setFlag("disableBouncingIcon", checked), + }, + ]; + + const seed = userEmail || userId || "user"; + const initials = initialsFromIdentity(userEmail, userId); + const hue = hueFromString(seed); + const displayName = navAccountDisplayName(userEmail, userId); + const triggerLabel = `Account menu — ${userRole ?? "Unknown role"} — signed in as ${userEmail || userId || "unknown"}`; + + return ( + + + + + {initials} + + + {!collapsed && ( + <> + + {displayName} + {userRole && {userRole}} + + + + )} + + + +
+ LiteLLM + {!disableBouncingIcon && ( + + 🌑 + + )} + + {version && ( + } + className="px-1.5 py-0 font-mono text-[10px] font-medium text-muted-foreground" + > + v{version} + + )} +
+ +
+ } label="Tier"> + {premiumUser ? ( + + + Premium + + ) : ( + + + Standard + + )} + + } label="Role"> + {userRole} + + } label="Email"> + + + } label="User ID"> + + +
+ + + +
+ {toggles.map((toggle) => ( +
+ {toggle.label} + +
+ ))} +
+ + + + +
+
+ ); +}; + +export default SidebarAccountMenu; diff --git a/ui/litellm-dashboard/src/components/leftnav.tsx b/ui/litellm-dashboard/src/components/leftnav.tsx index 0fb9fc84a31..c24cebe7e7d 100644 --- a/ui/litellm-dashboard/src/components/leftnav.tsx +++ b/ui/litellm-dashboard/src/components/leftnav.tsx @@ -74,8 +74,8 @@ import { } from "../utils/roles"; import NewBadge from "./common_components/NewBadge"; import type { Organization } from "./networking"; +import SidebarAccountMenu from "./SidebarAccountMenu/SidebarAccountMenu"; import SidebarUsageCard from "./SidebarUsageCard"; -import UserDropdown from "./Navbar/UserDropdown/UserDropdown"; import { MIGRATED_PAGES, migratedHref, legacyPageHref } from "@/utils/migratedPages"; const ICON = { strokeWidth: 1.75 } as const; @@ -626,7 +626,7 @@ const Sidebar_: React.FC = ({ onExpandRail={() => onToggleCollapsed?.()} /> )} - + );