Skip to content

Commit 93c1461

Browse files
authored
fix(ui): restore hover feedback and dark-mode variants lost in the token migration (#37579)
* fix(ui): restore hover feedback and dark-mode variants lost in the token migration PR #37576 mapped hardcoded Tailwind palette classes onto semantic tokens. Two-tone hover pairs collapsed onto a single token, so 116 hover utilities across 49 files became identical to their base class and produced no visible feedback, and in seven files a dark: variant was dropped while its hardcoded light partner survived, leaving those elements stuck light in dark mode. Hover states now follow the alpha-step idiom the shadcn primitives already use (hover:bg-primary/80, hover:bg-success/20): a duplicated hover:text-X or hover:bg-X becomes /80, hover:border-border becomes hover:border-ring, and a duplicate is dropped where another hover utility on the element already carries the change. One transition-colors that no longer animated anything is removed. For the dark-mode gaps, indigo maps onto info and amber onto warning. There is no purple token in globals.css, so the purple sites keep their palette classes and get their dark: partner back. * fix(ui): add an eslint rule that fails a hover: utility identical to its base The token migration collapsed two-tone hover pairs by hand, so nothing catches the next one. `local/no-noop-hover-variant` reads every string literal and template chunk and errors when a `hover:X` sits alongside a bare `X`, which is exactly the shape that renders no hover feedback. It ships at error with no suppression baseline, so the eleven sites that already carried a dead hover before the migration are fixed here too. The rule reads one class string at a time, so a base class supplied by a different ternary branch than its hover partner is left alone: a selected row whose resting colour already matches its hover colour is deliberate, not a bug.
1 parent 47a7e17 commit 93c1461

66 files changed

Lines changed: 269 additions & 157 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

ui/litellm-dashboard/eslint.config.mjs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ const eslintConfig = [
2222
"local/no-large-inline-object-arg": "warn",
2323
"local/no-long-condition-chain": "warn",
2424
"local/no-complex-jsx-arrow": ["error", { maxStatements: 2 }],
25+
"local/no-noop-hover-variant": "error",
2526
"@typescript-eslint/no-explicit-any": "warn",
2627
"no-console": ["warn", { allow: ["warn", "error"] }],
2728
"@typescript-eslint/no-unused-vars": "off",
@@ -81,6 +82,10 @@ const eslintConfig = [
8182
"no-restricted-syntax": "off",
8283
},
8384
},
85+
{
86+
files: ["tests/eslint-rules/**/*.{ts,tsx}"],
87+
rules: { "local/no-noop-hover-variant": "off" },
88+
},
8489
{
8590
files: ["src/**/*.test.{ts,tsx}", "tests/**/*.{ts,tsx}"],
8691
plugins: { "testing-library": testingLibrary, "jest-dom": jestDom },

ui/litellm-dashboard/scripts/eslint-rules/index.mjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@ import noLargeInlineObjectArg from "./no-large-inline-object-arg.mjs";
22
import noLongConditionChain from "./no-long-condition-chain.mjs";
33
import noComplexJsxArrow from "./no-complex-jsx-arrow.mjs";
44
import filenamePascalCase from "./filename-pascal-case.mjs";
5+
import noNoopHoverVariant from "./no-noop-hover-variant.mjs";
56

67
const plugin = {
78
rules: {
89
"no-large-inline-object-arg": noLargeInlineObjectArg,
910
"no-long-condition-chain": noLongConditionChain,
1011
"no-complex-jsx-arrow": noComplexJsxArrow,
1112
"filename-pascal-case": filenamePascalCase,
13+
"no-noop-hover-variant": noNoopHoverVariant,
1214
},
1315
};
1416

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
const noopHovers = (value) => {
2+
const tokens = value.split(/\s+/).filter(Boolean);
3+
const bare = new Set(tokens.filter((t) => !t.includes(":")));
4+
return tokens
5+
.filter((t) => t.startsWith("hover:"))
6+
.map((t) => [t, t.slice("hover:".length)])
7+
.filter(([, base]) => bare.has(base));
8+
};
9+
10+
const rule = {
11+
meta: {
12+
type: "problem",
13+
docs: {
14+
description:
15+
"Disallow a hover: utility whose value is identical to the base utility in the same class string, which renders no hover feedback.",
16+
},
17+
schema: [],
18+
messages: {
19+
noop: "`{{hover}}` is identical to the base `{{base}}`, so hovering changes nothing. Give it a distinct value (e.g. `{{hover}}/80`) or drop it.",
20+
},
21+
},
22+
create(context) {
23+
const check = (node, value) => {
24+
if (typeof value !== "string" || !value.includes("hover:")) return;
25+
for (const [hover, base] of noopHovers(value)) {
26+
context.report({ node, messageId: "noop", data: { hover, base } });
27+
}
28+
};
29+
return {
30+
Literal(node) {
31+
check(node, node.value);
32+
},
33+
TemplateElement(node) {
34+
check(node, node.value.cooked);
35+
},
36+
};
37+
},
38+
};
39+
40+
export default rule;

ui/litellm-dashboard/src/app/(dashboard)/agents/_components/add_agent_form.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -758,7 +758,7 @@ const AddAgentForm: React.FC<AddAgentFormProps> = ({ visible, onClose, accessTok
758758
<div className="mb-1 px-2 text-xs font-medium tracking-wide text-muted-foreground uppercase">
759759
Not listed?
760760
</div>
761-
<SelectItem value={CUSTOM_AGENT_TYPE} className="focus:bg-warning/10 dark:focus:**:text-amber-400">
761+
<SelectItem value={CUSTOM_AGENT_TYPE} className="focus:bg-warning/10">
762762
<span className="flex items-center gap-3">
763763
<LayoutGrid className="size-4.5 shrink-0 text-warning" />
764764
<span className="block">
@@ -877,7 +877,7 @@ const AddAgentForm: React.FC<AddAgentFormProps> = ({ visible, onClose, accessTok
877877
<div>
878878
{/* Agent name chip */}
879879
<div className="mb-6 flex justify-center">
880-
<Badge className="h-auto gap-1.5 bg-purple-100 px-3 py-1 text-sm text-purple-700">
880+
<Badge className="h-auto gap-1.5 bg-purple-100 px-3 py-1 text-sm text-purple-700 dark:bg-purple-950 dark:text-purple-300">
881881
<Bot className="size-3.5" />
882882
{agentName}
883883
</Badge>
@@ -906,7 +906,7 @@ const AddAgentForm: React.FC<AddAgentFormProps> = ({ visible, onClose, accessTok
906906
<div
907907
className={`cursor-pointer rounded-lg border-2 p-4 transition-colors ${
908908
keyAssignOption === "create_new"
909-
? "border-indigo-600 bg-indigo-50"
909+
? "border-info bg-info/10"
910910
: "border-border bg-background hover:border-muted-foreground/40"
911911
}`}
912912
onClick={() => setKeyAssignOption("create_new")}
@@ -916,7 +916,7 @@ const AddAgentForm: React.FC<AddAgentFormProps> = ({ visible, onClose, accessTok
916916
<RadioGroupItem value="create_new" aria-label="Create a new key for this agent" />
917917
<div className="flex-1">
918918
<div className="flex items-center gap-2">
919-
<Key className="size-4 text-indigo-600" />
919+
<Key className="size-4 text-info" />
920920
<span className="font-medium text-foreground">Create a new key for this agent</span>
921921
</div>
922922
<p className="mt-1 text-sm text-muted-foreground">A dedicated key scoped to this agent.</p>
@@ -943,7 +943,7 @@ const AddAgentForm: React.FC<AddAgentFormProps> = ({ visible, onClose, accessTok
943943
<div
944944
className={`cursor-pointer rounded-lg border-2 p-4 transition-colors ${
945945
keyAssignOption === "existing_key"
946-
? "border-indigo-600 bg-indigo-50"
946+
? "border-info bg-info/10"
947947
: "border-border bg-background hover:border-muted-foreground/40"
948948
}`}
949949
onClick={() => setKeyAssignOption("existing_key")}
@@ -993,7 +993,7 @@ const AddAgentForm: React.FC<AddAgentFormProps> = ({ visible, onClose, accessTok
993993
<CircleCheck className="mb-4 size-12 text-success" />
994994
<h3 className="mb-2 text-xl font-semibold text-foreground">Agent Created!</h3>
995995
<div className="mb-4 flex justify-center">
996-
<Badge className="h-auto gap-1.5 bg-purple-100 px-3 py-1 text-sm text-purple-700">
996+
<Badge className="h-auto gap-1.5 bg-purple-100 px-3 py-1 text-sm text-purple-700 dark:bg-purple-950 dark:text-purple-300">
997997
<Bot className="size-3.5" />
998998
{createdAgentName}
999999
</Badge>

ui/litellm-dashboard/src/app/(dashboard)/agents/_components/agent_form_fields.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ const SkillsFieldArray = () => {
131131
<Button
132132
type="button"
133133
variant="ghost"
134-
className="mt-4 text-destructive hover:text-destructive"
134+
className="mt-4 text-destructive hover:text-destructive/80"
135135
onClick={() => remove(index)}
136136
>
137137
<Trash2 />
@@ -184,7 +184,7 @@ const StaticHeadersFieldArray = () => {
184184
variant="ghost"
185185
size="icon"
186186
aria-label="Remove static header"
187-
className="text-destructive hover:text-destructive"
187+
className="text-destructive hover:text-destructive/80"
188188
onClick={() => remove(index)}
189189
>
190190
<Trash2 />

ui/litellm-dashboard/src/app/(dashboard)/cost-tracking/_components/provider_discount_table.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ const ProviderDiscountTable: React.FC<ProviderDiscountTableProps> = ({
100100
size="icon-sm"
101101
aria-label={`Save discount for ${displayName}`}
102102
onClick={() => handleSaveEdit(row.provider)}
103-
className="cursor-pointer text-success hover:text-success"
103+
className="cursor-pointer text-success hover:text-success/80"
104104
>
105105
<Check className="size-5" />
106106
</Button>
@@ -122,7 +122,7 @@ const ProviderDiscountTable: React.FC<ProviderDiscountTableProps> = ({
122122
size="icon-sm"
123123
aria-label={`Edit discount for ${displayName}`}
124124
onClick={() => handleStartEdit(row.provider, row.discount)}
125-
className="cursor-pointer text-info hover:text-info"
125+
className="cursor-pointer text-info hover:text-info/80"
126126
>
127127
<SquarePen className="size-5" />
128128
</Button>

ui/litellm-dashboard/src/app/(dashboard)/cost-tracking/_components/provider_margin_table.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ const ProviderMarginTable: React.FC<ProviderMarginTableProps> = ({
152152
size="icon-sm"
153153
aria-label={`Save margin for ${displayName}`}
154154
onClick={() => handleSaveEdit(row.provider)}
155-
className="cursor-pointer text-success hover:text-success"
155+
className="cursor-pointer text-success hover:text-success/80"
156156
>
157157
<Check className="size-5" />
158158
</Button>
@@ -174,7 +174,7 @@ const ProviderMarginTable: React.FC<ProviderMarginTableProps> = ({
174174
size="icon-sm"
175175
aria-label={`Edit margin for ${displayName}`}
176176
onClick={() => handleStartEdit(row.provider, row.margin)}
177-
className="cursor-pointer text-info hover:text-info"
177+
className="cursor-pointer text-info hover:text-info/80"
178178
>
179179
<SquarePen className="size-5" />
180180
</Button>

ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/TeamGuardrailsTab.tsx

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@ const STATUS_CONFIG: Record<GuardrailStatus, { label: string; bg: string; text:
199199
};
200200

201201
const TEAM_COLORS: Record<string, string> = {
202-
"ML Platform": "bg-purple-100 text-purple-700",
202+
"ML Platform": "bg-purple-100 text-purple-700 dark:bg-purple-900 dark:text-purple-300",
203203
"Data Science": "bg-info/15 text-info",
204204
Security: "bg-destructive/15 text-destructive",
205205
"Customer Success": "bg-warning/15 text-warning",
@@ -311,7 +311,7 @@ function GuardrailCard({
311311
return (
312312
<div
313313
className={`bg-card border rounded-lg p-4 transition-all ${
314-
isSelected ? "border-info ring-1 ring-blue-200" : "border-border"
314+
isSelected ? "border-info ring-1 ring-info/30" : "border-border"
315315
}`}
316316
>
317317
<div className="flex items-start justify-between gap-4">
@@ -358,7 +358,7 @@ function GuardrailCard({
358358
<button
359359
type="button"
360360
onClick={onApprove}
361-
className="text-xs bg-success hover:bg-success text-white px-3 py-1.5 rounded-md transition-colors font-medium"
361+
className="text-xs bg-success hover:bg-success/80 text-white px-3 py-1.5 rounded-md transition-colors font-medium"
362362
>
363363
Approve
364364
</button>
@@ -600,7 +600,7 @@ function DetailPanel({
600600
setNewStaticHeaderValue("");
601601
}
602602
}}
603-
className="text-xs font-medium text-info hover:text-info border border-info/20 bg-info/10 hover:bg-info/15 px-2 py-1.5 rounded-sm transition-colors shrink-0"
603+
className="text-xs font-medium text-info border border-info/20 bg-info/10 hover:bg-info/15 px-2 py-1.5 rounded-sm transition-colors shrink-0"
604604
>
605605
Add
606606
</button>
@@ -671,7 +671,7 @@ function DetailPanel({
671671
setNewExtraHeader("");
672672
}
673673
}}
674-
className="text-xs font-medium text-info hover:text-info border border-info/20 bg-info/10 hover:bg-info/15 px-2 py-1.5 rounded-sm transition-colors"
674+
className="text-xs font-medium text-info border border-info/20 bg-info/10 hover:bg-info/15 px-2 py-1.5 rounded-sm transition-colors"
675675
>
676676
Add
677677
</button>
@@ -682,7 +682,7 @@ function DetailPanel({
682682
<button
683683
type="button"
684684
onClick={() => setConfigExpanded(!configExpanded)}
685-
className="w-full flex items-center justify-between px-3 py-2 text-left text-xs font-semibold text-foreground bg-muted hover:bg-muted transition-colors"
685+
className="w-full flex items-center justify-between px-3 py-2 text-left text-xs font-semibold text-foreground bg-muted hover:bg-border transition-colors"
686686
>
687687
<span>Equivalent config</span>
688688
{configExpanded ? (
@@ -727,7 +727,7 @@ function DetailPanel({
727727
<button
728728
type="button"
729729
onClick={onApprove}
730-
className="flex-1 flex items-center justify-center gap-1.5 bg-success hover:bg-success text-white text-sm font-medium py-2 rounded-md transition-colors"
730+
className="flex-1 flex items-center justify-center gap-1.5 bg-success hover:bg-success/80 text-white text-sm font-medium py-2 rounded-md transition-colors"
731731
>
732732
<CheckIcon className="h-4 w-4" />
733733
Approve
@@ -793,7 +793,7 @@ function ConfirmDialog({ action, guardrailName, onConfirm, onCancel }: ConfirmDi
793793
type="button"
794794
onClick={onConfirm}
795795
className={`flex-1 text-white text-sm font-medium py-2 rounded-md transition-colors ${
796-
isApprove ? "bg-success hover:bg-success" : "bg-destructive hover:bg-destructive"
796+
isApprove ? "bg-success hover:bg-success/80" : "bg-destructive hover:bg-destructive/80"
797797
}`}
798798
>
799799
{isApprove ? "Approve" : "Reject"}
@@ -1015,7 +1015,7 @@ export function TeamGuardrailsTab({ accessToken }: TeamGuardrailsTabProps) {
10151015
<button
10161016
type="button"
10171017
onClick={() => setIsSubmitModalOpen(true)}
1018-
className="ml-auto flex items-center gap-2 bg-info hover:bg-info text-white text-sm font-medium px-4 py-2 rounded-md transition-colors"
1018+
className="ml-auto flex items-center gap-2 bg-info hover:bg-info/80 text-white text-sm font-medium px-4 py-2 rounded-md transition-colors"
10191019
>
10201020
<PlusIcon className="h-4 w-4" />
10211021
Add Guardrail

ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/add_guardrail_form.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -144,14 +144,14 @@ const createEmptyToolPermissionConfig = (): ToolPermissionConfig => ({
144144
});
145145

146146
const getStepIndicatorClass = (isDone: boolean, isCurrent: boolean): string => {
147-
if (isDone) return "bg-indigo-600 text-white";
148-
if (isCurrent) return "bg-background text-indigo-600 border-2 border-indigo-600";
147+
if (isDone) return "bg-info text-white";
148+
if (isCurrent) return "bg-background text-info border-2 border-info";
149149
return "bg-muted text-muted-foreground border border-border";
150150
};
151151

152152
const getStepTitleClass = (isDone: boolean, isCurrent: boolean): string => {
153153
if (isCurrent) return "font-semibold text-foreground";
154-
if (isDone) return "font-medium text-indigo-600";
154+
if (isDone) return "font-medium text-info";
155155
return "font-medium text-muted-foreground";
156156
};
157157

@@ -1126,7 +1126,7 @@ const AddGuardrailForm: React.FC<AddGuardrailFormProps> = ({ visible, onClose, a
11261126
>
11271127
{isDone ? "\u2713" : index + 1}
11281128
</div>
1129-
{!isLast && <div className={`min-h-4 w-px flex-1 ${isDone ? "bg-indigo-600" : "bg-border"}`} />}
1129+
{!isLast && <div className={`min-h-4 w-px flex-1 ${isDone ? "bg-info" : "bg-border"}`} />}
11301130
</div>
11311131

11321132
{/* Step content */}
@@ -1142,7 +1142,7 @@ const AddGuardrailForm: React.FC<AddGuardrailFormProps> = ({ visible, onClose, a
11421142
{step.optional && !isCurrent && (
11431143
<span className="text-[11px] text-muted-foreground">optional</span>
11441144
)}
1145-
{isDone && <span className="text-[11px] text-indigo-600 hover:underline">Edit</span>}
1145+
{isDone && <span className="text-[11px] text-info hover:underline">Edit</span>}
11461146
</div>
11471147

11481148
{/* Expanded form content for current step */}

ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/guardrail_optional_params.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ const DictField: React.FC<DictFieldProps> = ({ field, fullFieldKey, control, val
167167
<Button
168168
variant="ghost"
169169
size="sm"
170-
className="text-destructive hover:text-destructive"
170+
className="text-destructive hover:text-destructive/80"
171171
onClick={() => removeEntry(entry.id, entry.key)}
172172
>
173173
Remove

0 commit comments

Comments
 (0)