From 5290150a056dbb5725c2a42a13f6ae2802c17961 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Wed, 19 Aug 2026 23:13:08 -0700 Subject: [PATCH] fix(ui): keep semantic button colours on hover after the no-op hover cleanup (#37580) PR #37579 read `text-X hover:text-X` on a shadcn Button as dead weight and removed the hover half. On the ghost and outline variants it was not dead: both carry their own `hover:text-foreground`, and the duplicate in the className was the thing displacing it through tailwind-merge. Dropping it handed the hover back to the variant, so the Remove button in a team's logging settings, the chat storage banner's dismiss control, and the collapsed enterprise-usage rail all lose their colour the moment you point at them. Each of the three now carries a distinct hover value, following the alpha-step idiom the rest of that migration used, which restores the colour and keeps `local/no-noop-hover-variant` satisfied. Every other hover utility that PR dropped sits on a plain element or a variant with no competing `hover:text-`, so those stay as they are. --- .../src/app/chat/page.integration.test.tsx | 25 ++++++++++++++++--- ui/litellm-dashboard/src/app/chat/page.tsx | 2 +- .../src/components/SidebarUsageCard.test.tsx | 8 ++++++ .../src/components/SidebarUsageCard.tsx | 2 +- .../components/team/LoggingSettings.test.tsx | 16 ++++++++++++ .../src/components/team/LoggingSettings.tsx | 2 +- 6 files changed, 49 insertions(+), 6 deletions(-) diff --git a/ui/litellm-dashboard/src/app/chat/page.integration.test.tsx b/ui/litellm-dashboard/src/app/chat/page.integration.test.tsx index 1918d19dae6..29267e522d9 100644 --- a/ui/litellm-dashboard/src/app/chat/page.integration.test.tsx +++ b/ui/litellm-dashboard/src/app/chat/page.integration.test.tsx @@ -1,11 +1,12 @@ import React from "react"; -import { fireEvent, render, screen, waitFor } from "@testing-library/react"; +import { fireEvent, render, screen, waitFor, within } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { useChatHistory } from "@/components/chat/useChatHistory"; import ChatConversationPage from "./page"; -const { mockMakeOpenAIResponsesRequest } = vi.hoisted(() => ({ +const { mockMakeOpenAIResponsesRequest, shellState } = vi.hoisted(() => ({ mockMakeOpenAIResponsesRequest: vi.fn(), + shellState: { storageUnavailable: false }, })); vi.mock("next/navigation", () => ({ @@ -50,7 +51,7 @@ vi.mock("@/contexts/ChatShellContext", () => ({ conversations: history.conversations, activeConversation: history.activeConversation, activeConversationId: history.currentActiveId, - storageUnavailable: false, + storageUnavailable: shellState.storageUnavailable, staleId: false, createConversation: history.createConversation, appendMessage: history.appendMessage, @@ -81,6 +82,7 @@ describe("/ui/chat request metrics", () => { beforeEach(() => { localStorage.clear(); mockMakeOpenAIResponsesRequest.mockReset(); + shellState.storageUnavailable = false; }); it("renders latency, TTFT, token counts and cost reported for the assistant turn", async () => { @@ -130,3 +132,20 @@ describe("/ui/chat request metrics", () => { expect(document.querySelector(".response-metrics")).toBeNull(); }); }); + +describe("/ui/chat storage banner", () => { + beforeEach(() => { + localStorage.clear(); + mockMakeOpenAIResponsesRequest.mockReset(); + shellState.storageUnavailable = true; + }); + + it("keeps the dismiss control amber on hover instead of the ghost variant's foreground", async () => { + render(); + + const banner = await screen.findByText("Chat history won't be saved in this browser session"); + const dismiss = within(banner.parentElement!).getByRole("button"); + expect(dismiss).toHaveClass("hover:text-warning/80"); + expect(dismiss).not.toHaveClass("hover:text-foreground"); + }); +}); diff --git a/ui/litellm-dashboard/src/app/chat/page.tsx b/ui/litellm-dashboard/src/app/chat/page.tsx index f4525219f51..2e3547aa28c 100644 --- a/ui/litellm-dashboard/src/app/chat/page.tsx +++ b/ui/litellm-dashboard/src/app/chat/page.tsx @@ -518,7 +518,7 @@ export default function ChatConversationPage() { variant="ghost" size="icon-xs" onClick={() => setStorageBannerDismissed(true)} - className="text-warning hover:bg-warning/15 " + className="text-warning hover:bg-warning/15 hover:text-warning/80" > diff --git a/ui/litellm-dashboard/src/components/SidebarUsageCard.test.tsx b/ui/litellm-dashboard/src/components/SidebarUsageCard.test.tsx index 09e56d8fb5a..8a8164dd661 100644 --- a/ui/litellm-dashboard/src/components/SidebarUsageCard.test.tsx +++ b/ui/litellm-dashboard/src/components/SidebarUsageCard.test.tsx @@ -156,4 +156,12 @@ describe("SidebarUsageCard", () => { await user.click(rail); expect(onExpandRail).toHaveBeenCalledOnce(); }); + + it("keeps the collapsed rail tinted on hover instead of the outline variant's foreground", async () => { + renderWithClient(); + + const rail = await screen.findByTitle("Enterprise usage"); + expect(rail).toHaveClass("hover:text-sidebar-primary/80"); + expect(rail).not.toHaveClass("hover:text-foreground"); + }); }); diff --git a/ui/litellm-dashboard/src/components/SidebarUsageCard.tsx b/ui/litellm-dashboard/src/components/SidebarUsageCard.tsx index 2d9533dce36..76240565cda 100644 --- a/ui/litellm-dashboard/src/components/SidebarUsageCard.tsx +++ b/ui/litellm-dashboard/src/components/SidebarUsageCard.tsx @@ -85,7 +85,7 @@ export default function SidebarUsageCard({ accessToken, collapsed, onExpandRail variant="outline" onClick={onExpandRail} title="Enterprise usage" - className="h-9 w-full rounded-lg border-sidebar-border bg-sidebar text-sidebar-primary shadow-none hover:bg-sidebar-accent" + className="h-9 w-full rounded-lg border-sidebar-border bg-sidebar text-sidebar-primary shadow-none hover:bg-sidebar-accent hover:text-sidebar-primary/80" > diff --git a/ui/litellm-dashboard/src/components/team/LoggingSettings.test.tsx b/ui/litellm-dashboard/src/components/team/LoggingSettings.test.tsx index efdef4c2536..b63a6cb98aa 100644 --- a/ui/litellm-dashboard/src/components/team/LoggingSettings.test.tsx +++ b/ui/litellm-dashboard/src/components/team/LoggingSettings.test.tsx @@ -181,6 +181,22 @@ describe("LoggingSettings", () => { expect(source.match(HARDCODED_PALETTE) ?? []).toHaveLength(0); }); + it("keeps the remove button destructive on hover instead of the ghost variant's foreground", () => { + const initialValue = [ + { + callback_name: "langsmith", + callback_type: "success", + callback_vars: {}, + }, + ]; + + renderWithProviders(); + + const remove = screen.getByRole("button", { name: "Remove" }); + expect(remove).toHaveClass("hover:text-destructive/80"); + expect(remove).not.toHaveClass("hover:text-foreground"); + }); + it("reports the chosen event type when a different option is picked", async () => { const user = userEvent.setup({ delay: null }); const mockOnChange = vi.fn(); diff --git a/ui/litellm-dashboard/src/components/team/LoggingSettings.tsx b/ui/litellm-dashboard/src/components/team/LoggingSettings.tsx index 71d54ec6620..d526b710bcf 100644 --- a/ui/litellm-dashboard/src/components/team/LoggingSettings.tsx +++ b/ui/litellm-dashboard/src/components/team/LoggingSettings.tsx @@ -278,7 +278,7 @@ const LoggingSettings: React.FC = ({ variant="ghost" onClick={() => removeLoggingConfig(index)} size="sm" - className="text-destructive hover:bg-destructive/10" + className="text-destructive hover:bg-destructive/10 hover:text-destructive/80" type="button" >