litellm/ui/litellm-dashboard/eslint.config.mjs
yuneng-jiang 93c1461074
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.
2026-08-19 22:52:36 -07:00

111 lines
3.7 KiB
JavaScript

import js from "@eslint/js";
import tseslint from "typescript-eslint";
import nextCoreWebVitals from "eslint-config-next/core-web-vitals";
import prettier from "eslint-config-prettier/flat";
import unusedImports from "eslint-plugin-unused-imports";
import testingLibrary from "eslint-plugin-testing-library";
import jestDom from "eslint-plugin-jest-dom";
import local from "./scripts/eslint-rules/index.mjs";
const eslintConfig = [
{
ignores: [".next/**", "out/**", "build/**", "coverage/**", "next-env.d.ts", "src/lib/http/schema.d.ts"],
},
js.configs.recommended,
...tseslint.configs.recommended,
...nextCoreWebVitals,
prettier,
{
plugins: { "unused-imports": unusedImports, local },
rules: {
"unused-imports/no-unused-imports": "error",
"local/no-large-inline-object-arg": "warn",
"local/no-long-condition-chain": "warn",
"local/no-complex-jsx-arrow": ["error", { maxStatements: 2 }],
"local/no-noop-hover-variant": "error",
"@typescript-eslint/no-explicit-any": "warn",
"no-console": ["warn", { allow: ["warn", "error"] }],
"@typescript-eslint/no-unused-vars": "off",
"@typescript-eslint/no-unused-expressions": "off",
"@typescript-eslint/ban-ts-comment": "off",
"prefer-const": "error",
"no-empty": "off",
"no-prototype-builtins": "off",
"no-useless-catch": "off",
"no-useless-escape": "off",
"no-self-assign": "error",
"no-var": "error",
"no-nested-ternary": "error",
"react/no-danger": "error",
complexity: ["warn", 20],
"max-depth": ["warn", 4],
"max-params": ["error", 4],
"max-nested-callbacks": ["error", 4],
"no-restricted-syntax": [
"error",
{
selector: "CallExpression[callee.name='fetch']",
message:
"Raw fetch() is only allowed in src/lib/http/. Use the shared client (createApiClient / apiClient) from @/lib/http/client instead.",
},
],
"no-restricted-imports": [
"error",
{
patterns: [
{
group: ["@tremor/react", "@tremor/react/*"],
message:
"@tremor/react is being phased out; build new UI with shadcn/ui primitives instead of adding tremor imports.",
},
],
},
],
},
},
{
files: ["src/**/*.tsx"],
rules: {
"local/filename-pascal-case": "error",
},
},
{
files: ["src/**/*.{ts,tsx}"],
ignores: ["src/**/*.test.{ts,tsx}", "src/**/*.spec.{ts,tsx}", "src/data/**"],
rules: {
"max-lines": ["error", { max: 800, skipBlankLines: true, skipComments: true }],
},
},
{
files: ["src/lib/http/**"],
rules: {
"no-restricted-syntax": "off",
},
},
{
files: ["tests/eslint-rules/**/*.{ts,tsx}"],
rules: { "local/no-noop-hover-variant": "off" },
},
{
files: ["src/**/*.test.{ts,tsx}", "tests/**/*.{ts,tsx}"],
plugins: { "testing-library": testingLibrary, "jest-dom": jestDom },
rules: {
"testing-library/await-async-queries": "error",
"testing-library/no-wait-for-multiple-assertions": "error",
"testing-library/no-wait-for-side-effects": "error",
"testing-library/prefer-find-by": "error",
"testing-library/prefer-presence-queries": "error",
"jest-dom/prefer-checked": "error",
"jest-dom/prefer-empty": "error",
"jest-dom/prefer-enabled-disabled": "error",
"jest-dom/prefer-focus": "error",
"jest-dom/prefer-in-document": "error",
"jest-dom/prefer-to-have-attribute": "error",
"jest-dom/prefer-to-have-class": "error",
"jest-dom/prefer-to-have-style": "error",
"jest-dom/prefer-to-have-text-content": "error",
},
},
];
export default eslintConfig;