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) { )} - + {!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 */} - - +