diff --git a/ui/litellm-dashboard/src/components/DashboardHeader.test.tsx b/ui/litellm-dashboard/src/components/DashboardHeader.test.tsx
index 6c656382deb..6a06e1ba612 100644
--- a/ui/litellm-dashboard/src/components/DashboardHeader.test.tsx
+++ b/ui/litellm-dashboard/src/components/DashboardHeader.test.tsx
@@ -1,6 +1,7 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { act, fireEvent, render, screen } from "@testing-library/react";
import { DashboardHeader } from "./DashboardHeader";
+import { NAV_PRODUCT_LINK_CLASS } from "@/components/Navbar/navProductLinkClass";
const { mockUsePluginMode, mockUseUISettings, state } = vi.hoisted(() => {
const state = {
@@ -55,6 +56,16 @@ describe("DashboardHeader breadcrumb", () => {
expect(screen.queryByText("Observability")).not.toBeInTheDocument();
});
+ it("styles Docs with the shared product-link class instead of a muted toolbar button", () => {
+ render();
+
+ const docs = screen.getByRole("link", { name: "Docs" });
+ for (const cls of NAV_PRODUCT_LINK_CLASS.trim().split(/\s+/)) {
+ expect(docs).toHaveClass(cls);
+ }
+ expect(docs).not.toHaveClass("text-muted-foreground");
+ });
+
it("renders the tools divider centered rather than stretched to the top of the row", () => {
const { container } = render();
diff --git a/ui/litellm-dashboard/src/components/DashboardHeader.tsx b/ui/litellm-dashboard/src/components/DashboardHeader.tsx
index 31f2a0b715a..fe824ce074d 100644
--- a/ui/litellm-dashboard/src/components/DashboardHeader.tsx
+++ b/ui/litellm-dashboard/src/components/DashboardHeader.tsx
@@ -1,6 +1,5 @@
"use client";
-import { Button } from "@/components/ui/button";
import {
Breadcrumb,
BreadcrumbItem,
@@ -11,6 +10,7 @@ import {
import { ToolbarSeparator } from "@/components/shared/ToolbarSeparator";
import { getBreadcrumb } from "@/components/leftnav";
import { BlogDropdown } from "@/components/Navbar/BlogDropdown/BlogDropdown";
+import { DocsLink } from "@/components/Navbar/DocsLink/DocsLink";
import { CommunityEngagementButtons } from "@/components/Navbar/CommunityEngagementButtons/CommunityEngagementButtons";
import { NotificationsBell } from "@/components/Navbar/NotificationsBell/NotificationsBell";
import ViewSwitcher from "@/components/Navbar/ViewSwitcher";
@@ -62,15 +62,7 @@ export function DashboardHeader({ page }: DashboardHeaderProps) {
>
)}
- }
- className="text-muted-foreground"
- >
- Docs
-
+
{!hideCommunityLinks && }
diff --git a/ui/litellm-dashboard/src/components/Navbar/DocsLink/DocsLink.test.tsx b/ui/litellm-dashboard/src/components/Navbar/DocsLink/DocsLink.test.tsx
new file mode 100644
index 00000000000..d5a155cf503
--- /dev/null
+++ b/ui/litellm-dashboard/src/components/Navbar/DocsLink/DocsLink.test.tsx
@@ -0,0 +1,27 @@
+import { render, screen } from "@testing-library/react";
+import { describe, expect, it } from "vitest";
+import { NAV_PRODUCT_LINK_CLASS } from "@/components/Navbar/navProductLinkClass";
+import { DocsLink } from "./DocsLink";
+
+const sharedClasses = NAV_PRODUCT_LINK_CLASS.trim().split(/\s+/);
+
+describe("DocsLink", () => {
+ it("opens the docs in a new tab without leaking the opener", () => {
+ render();
+
+ const link = screen.getByRole("link", { name: "Docs" });
+ expect(link).toHaveAttribute("href", "https://docs.litellm.ai/docs/");
+ expect(link).toHaveAttribute("target", "_blank");
+ expect(link).toHaveAttribute("rel", "noopener noreferrer");
+ });
+
+ it("carries the same product-link styling as the Blog trigger, so the two never drift apart", () => {
+ render();
+
+ const link = screen.getByRole("link", { name: "Docs" });
+ for (const cls of sharedClasses) {
+ expect(link).toHaveClass(cls);
+ }
+ expect(link).not.toHaveClass("text-muted-foreground");
+ });
+});
diff --git a/ui/litellm-dashboard/src/components/Navbar/DocsLink/DocsLink.tsx b/ui/litellm-dashboard/src/components/Navbar/DocsLink/DocsLink.tsx
new file mode 100644
index 00000000000..9176e1f9c38
--- /dev/null
+++ b/ui/litellm-dashboard/src/components/Navbar/DocsLink/DocsLink.tsx
@@ -0,0 +1,15 @@
+import { NAV_PRODUCT_LINK_CLASS } from "@/components/Navbar/navProductLinkClass";
+import { ChevronDown } from "lucide-react";
+import React from "react";
+
+export const DOCS_URL = "https://docs.litellm.ai/docs/";
+
+export const DocsLink: React.FC = () => (
+
+ Docs
+ {/* Docs is a single outbound link; the hidden chevron keeps its box identical to the Blog dropdown trigger. */}
+
+
+);
+
+export default DocsLink;
diff --git a/ui/litellm-dashboard/src/components/ThemeToggle/ThemeToggle.test.tsx b/ui/litellm-dashboard/src/components/ThemeToggle/ThemeToggle.test.tsx
index 41efedcd43a..c5dda3d3f7c 100644
--- a/ui/litellm-dashboard/src/components/ThemeToggle/ThemeToggle.test.tsx
+++ b/ui/litellm-dashboard/src/components/ThemeToggle/ThemeToggle.test.tsx
@@ -11,12 +11,7 @@ const renderToggle = () =>
,
);
-const openMenu = async () => {
- await userEvent.click(screen.getByRole("button", { name: "Theme" }));
- await screen.findByRole("menu");
-};
-
-const pick = async (label: string | RegExp) => userEvent.click(screen.getByRole("menuitemradio", { name: label }));
+const toggle = () => screen.getByRole("button", { name: /Switch to (light|dark) mode/ });
beforeEach(() => {
localStorage.clear();
@@ -28,50 +23,49 @@ afterAll(() => {
});
describe("ThemeToggle", () => {
- it("starts on light rather than following the system preference", async () => {
+ it("switches to dark on a single click, with no menu in between", async () => {
renderToggle();
- await openMenu();
- expect(screen.getByRole("menuitemradio", { name: "Light" })).toBeChecked();
- expect(screen.getByRole("menuitemradio", { name: /^Dark/ })).not.toBeChecked();
- expect(screen.getByRole("menuitemradio", { name: "System" })).not.toBeChecked();
- });
-
- it("puts the dark class on the document and remembers the choice", async () => {
- renderToggle();
- await openMenu();
-
- await pick(/^Dark/);
+ await userEvent.click(toggle());
+ expect(screen.queryByRole("menu")).not.toBeInTheDocument();
expect(document.documentElement).toHaveClass("dark");
expect(localStorage.getItem("theme")).toBe("dark");
});
- it("hands control back to the system preference when asked", async () => {
+ it("switches back to light on the next click", async () => {
renderToggle();
- await openMenu();
- await pick(/^Dark/);
+ await userEvent.click(toggle());
- await pick("System");
+ await userEvent.click(toggle());
+
+ expect(document.documentElement).not.toHaveClass("dark");
+ expect(localStorage.getItem("theme")).toBe("light");
+ });
+
+ it("names the mode the click will switch to, so the button says what it does", async () => {
+ renderToggle();
+ expect(screen.getByRole("button", { name: "Switch to dark mode (beta)" })).toBeInTheDocument();
+
+ await userEvent.click(toggle());
+
+ expect(await screen.findByRole("button", { name: "Switch to light mode" })).toBeInTheDocument();
+ });
+
+ it("leaves a stored system preference following the OS until the user clicks", () => {
+ localStorage.setItem("theme", "system");
+
+ renderToggle();
expect(localStorage.getItem("theme")).toBe("system");
- expect(document.documentElement).not.toHaveClass("dark");
});
- it("marks dark as beta in the menu, and leaves the other choices unmarked", async () => {
+ it("keeps the beta marker out of the toolbar label once dark is on", async () => {
renderToggle();
- await openMenu();
- expect(screen.getByRole("menuitemradio", { name: /^Dark/ })).toHaveTextContent("Beta");
- expect(screen.getByRole("menuitemradio", { name: "Light" })).not.toHaveTextContent("Beta");
- expect(screen.getByRole("menuitemradio", { name: "System" })).not.toHaveTextContent("Beta");
- });
+ await userEvent.click(toggle());
- it("keeps the beta marker inside the menu rather than in the toolbar", async () => {
- renderToggle();
- await openMenu();
- await pick(/^Dark/);
-
- expect(screen.getByRole("button", { name: "Theme" })).not.toHaveTextContent("Beta");
+ expect(toggle()).not.toHaveTextContent("Beta");
+ expect(toggle()).toHaveAccessibleName("Switch to light mode");
});
});
diff --git a/ui/litellm-dashboard/src/components/ThemeToggle/ThemeToggle.tsx b/ui/litellm-dashboard/src/components/ThemeToggle/ThemeToggle.tsx
index 3fbb3d2eb5b..2f941dbc2d2 100644
--- a/ui/litellm-dashboard/src/components/ThemeToggle/ThemeToggle.tsx
+++ b/ui/litellm-dashboard/src/components/ThemeToggle/ThemeToggle.tsx
@@ -1,57 +1,27 @@
"use client";
-import { Monitor, Moon, Sun } from "lucide-react";
+import { Moon, Sun } from "lucide-react";
import { useTheme } from "next-themes";
import React from "react";
-import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuRadioGroup,
- DropdownMenuRadioItem,
- DropdownMenuTrigger,
-} from "@/components/ui/dropdown-menu";
-
-const THEMES = [
- { value: "system", label: "System", Icon: Monitor, beta: false },
- { value: "light", label: "Light", Icon: Sun, beta: false },
- { value: "dark", label: "Dark", Icon: Moon, beta: true },
-] as const;
const ThemeToggle: React.FC = () => {
- const { theme, setTheme, resolvedTheme } = useTheme();
+ const { setTheme, resolvedTheme } = useTheme();
+ const isDark = resolvedTheme === "dark";
+ const label = isDark ? "Switch to light mode" : "Switch to dark mode (beta)";
return (
-
-
- }
- >
- {resolvedTheme === "dark" ? : }
-
-
-
- {THEMES.map(({ value, label, Icon, beta }) => (
-
-
- {label}
- {beta && (
-
- Beta
-
- )}
-
- ))}
-
-
-
+
);
};
diff --git a/ui/litellm-dashboard/src/components/navbar.tsx b/ui/litellm-dashboard/src/components/navbar.tsx
index e81c67cc958..88cd125ea15 100644
--- a/ui/litellm-dashboard/src/components/navbar.tsx
+++ b/ui/litellm-dashboard/src/components/navbar.tsx
@@ -9,12 +9,12 @@ import { clearTokenCookies } from "@/utils/cookieUtils";
import { clearStoredReturnUrl, getLoginUrl } from "@/utils/returnUrlUtils";
import useProxySettings from "@/app/(dashboard)/hooks/proxySettings/useProxySettings";
import { Badge } from "@/components/ui/badge";
-import { ChevronDown, PanelLeftClose, PanelLeftOpen } from "lucide-react";
+import { PanelLeftClose, PanelLeftOpen } from "lucide-react";
import Link from "next/link";
import React from "react";
import { BlogDropdown } from "./Navbar/BlogDropdown/BlogDropdown";
+import { DocsLink } from "./Navbar/DocsLink/DocsLink";
import { CommunityEngagementButtons } from "./Navbar/CommunityEngagementButtons/CommunityEngagementButtons";
-import { NAV_PRODUCT_LINK_CLASS } from "./Navbar/navProductLinkClass";
import { cn } from "@/lib/cva.config";
import { NotificationsBell } from "./Navbar/NotificationsBell/NotificationsBell";
import UserDropdown from "./Navbar/UserDropdown/UserDropdown";
@@ -143,16 +143,7 @@ const Navbar: React.FC = ({
aria-label="Product documentation"
className={`flex min-w-0 items-center gap-2 ${showWorkerSwitch ? "border-l border-border pl-4" : ""}`}
>
-
- Docs
- {/* Layout parity with Blog chevron — intentional single-level link */}
-
-
+