From 47c988e05c4e5a54cef4abd97cae0a888754c341 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Mon, 24 Aug 2026 14:10:31 -0700 Subject: [PATCH] refactor(ui): move the dashboard onto class-variance-authority (#38125) The dashboard used `cva@1.0.0-beta.4` with the object-argument API behind `@/lib/cva.config`, while shadcn emits `class-variance-authority` with the positional API. Every `shadcn add` of a cva-based primitive therefore needed a hand fix-up before it compiled, which meant `components/ui/` could never match a fresh CLI run and `shadcn add --diff` reported the whole file as changed instead of showing real upstream drift. Swap the dependency, and regenerate `badge`, `button`, `button-group`, `input-group` and `tabs` straight from the base-vega registry so they are now byte-identical to the CLI output plus prettier. Two primitives could not be regenerated because they are local code rather than registry items, so they move out of `components/ui/`: `sidebar` (203 lines against upstream's 730, and only `leftnav` consumes it) and `meter` (no registry entry at all, it wraps Base UI's Meter). The customisations that were baked into the regenerated files move to wrappers, following the rule that `components/ui/` holds CLI output and anything on top of it lives outside: - badge carried info, success and warning variants that duplicated the existing `StatusBadge` tone map, so its five call sites now use `StatusBadge`, which gains an optional `className` - input-group's addon focuses `[data-slot=input-group-control]` rather than upstream's `input`, which matters because the chat composer puts a textarea there. That handler now sits at the one call site that needs it `cx` keeps its previous twMerge behaviour. It came from the old `defineConfig({hooks: {onComplete: twMerge}})`, and CVA's own `cx` is plain clsx, so pointing it at `cn` avoids silently dropping conflict resolution in the six files that use it. `Sidebar.test.tsx` covers the failure mode this migration can hide: passing the object form to the positional API is accepted by clsx and renders the literal class string "base variants defaultVariants", so the component loses every style while the type checker and the existing suite stay green. --- ui/litellm-dashboard/eslint-suppressions.json | 10 -- ui/litellm-dashboard/package-lock.json | 37 +++--- ui/litellm-dashboard/package.json | 3 +- .../agents/_components/add_agent_form.tsx | 7 +- .../_components/UserEnvVarsModal.tsx | 3 +- .../old-usage/_components/usage.tsx | 2 +- .../components/chat_ui/ChatComposer.tsx | 11 +- .../policies/_components/add_policy_form.tsx | 5 +- .../_components/ProjectDetailsPage.tsx | 2 +- .../components/EndpointUsageTable.tsx | 2 +- .../SSOSettings/RoleMappings.tsx | 6 +- .../src/components/SidebarUsageCard.tsx | 2 +- .../src/components/leftnav.tsx | 2 +- .../src/components/shared/Alert.tsx | 38 +++--- .../meter.test.tsx => shared/Meter.test.tsx} | 2 +- .../{ui/meter.tsx => shared/Meter.tsx} | 8 +- .../src/components/shared/Sidebar.test.tsx | 36 ++++++ .../{ui/sidebar.tsx => shared/Sidebar.tsx} | 33 ++--- .../src/components/shared/form/field.tsx | 8 +- .../shared/table_cells/spend_budget_cell.tsx | 2 +- .../shared/table_cells/status_badge.tsx | 9 +- .../src/components/ui/badge.tsx | 74 ++++++------ .../src/components/ui/button-group.tsx | 30 ++--- .../src/components/ui/button.tsx | 90 +++++++------- .../src/components/ui/input-group.tsx | 114 +++++++++--------- .../src/components/ui/tabs.tsx | 26 ++-- ui/litellm-dashboard/src/lib/cva.config.ts | 12 +- 27 files changed, 304 insertions(+), 270 deletions(-) rename ui/litellm-dashboard/src/components/{ui/meter.test.tsx => shared/Meter.test.tsx} (99%) rename ui/litellm-dashboard/src/components/{ui/meter.tsx => shared/Meter.tsx} (91%) create mode 100644 ui/litellm-dashboard/src/components/shared/Sidebar.test.tsx rename ui/litellm-dashboard/src/components/{ui/sidebar.tsx => shared/Sidebar.tsx} (91%) diff --git a/ui/litellm-dashboard/eslint-suppressions.json b/ui/litellm-dashboard/eslint-suppressions.json index b7c578d8ec6..03de475c8c3 100644 --- a/ui/litellm-dashboard/eslint-suppressions.json +++ b/ui/litellm-dashboard/eslint-suppressions.json @@ -2218,11 +2218,6 @@ "count": 1 } }, - "src/components/ui/meter.tsx": { - "local/filename-pascal-case": { - "count": 1 - } - }, "src/components/ui/popover.tsx": { "local/filename-pascal-case": { "count": 1 @@ -2253,11 +2248,6 @@ "count": 1 } }, - "src/components/ui/sidebar.tsx": { - "local/filename-pascal-case": { - "count": 1 - } - }, "src/components/ui/skeleton.tsx": { "local/filename-pascal-case": { "count": 1 diff --git a/ui/litellm-dashboard/package-lock.json b/ui/litellm-dashboard/package-lock.json index 564f32e2573..d2a64b93384 100644 --- a/ui/litellm-dashboard/package-lock.json +++ b/ui/litellm-dashboard/package-lock.json @@ -17,7 +17,8 @@ "@tanstack/react-query": "5.100.7", "@tanstack/react-table": "8.21.3", "@types/papaparse": "5.5.2", - "cva": "1.0.0-beta.4", + "class-variance-authority": "0.7.1", + "clsx": "^2.1.1", "date-fns": "^4.4.0", "dayjs": "1.11.19", "jwt-decode": "4.0.0", @@ -5161,6 +5162,18 @@ "node": ">= 16" } }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", @@ -5299,26 +5312,6 @@ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "license": "MIT" }, - "node_modules/cva": { - "version": "1.0.0-beta.4", - "resolved": "https://registry.npmjs.org/cva/-/cva-1.0.0-beta.4.tgz", - "integrity": "sha512-F/JS9hScapq4DBVQXcK85l9U91M6ePeXoBMSp7vypzShoefUBxjQTo3g3935PUHgQd+IW77DjbPRIxugy4/GCQ==", - "license": "Apache-2.0", - "dependencies": { - "clsx": "^2.1.1" - }, - "funding": { - "url": "https://polar.sh/cva" - }, - "peerDependencies": { - "typescript": ">= 4.5.5" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, "node_modules/d3-array": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", @@ -12175,7 +12168,7 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", diff --git a/ui/litellm-dashboard/package.json b/ui/litellm-dashboard/package.json index ff6448ad75c..ededdfb4606 100644 --- a/ui/litellm-dashboard/package.json +++ b/ui/litellm-dashboard/package.json @@ -33,7 +33,8 @@ "@tanstack/react-query": "5.100.7", "@tanstack/react-table": "8.21.3", "@types/papaparse": "5.5.2", - "cva": "1.0.0-beta.4", + "class-variance-authority": "0.7.1", + "clsx": "^2.1.1", "date-fns": "^4.4.0", "dayjs": "1.11.19", "jwt-decode": "4.0.0", diff --git a/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/add_agent_form.tsx b/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/add_agent_form.tsx index b5c04029d69..9e8ea64fc45 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/add_agent_form.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/add_agent_form.tsx @@ -5,6 +5,7 @@ import { Logo } from "@/components/molecules/logo/Logo"; import { Bot, Check, CircleCheck, Key, LayoutGrid } from "lucide-react"; import CreatedKeyDisplay from "@/components/shared/CreatedKeyDisplay"; import { Badge } from "@/components/ui/badge"; +import { StatusBadge } from "@/components/shared/table_cells/status_badge"; import { Button } from "@/components/ui/button"; import { UiLoadingSpinner } from "@/components/ui/ui-loading-spinner"; import { Input } from "@/components/ui/input"; @@ -764,9 +765,7 @@ const AddAgentForm: React.FC = ({ visible, onClose, accessTok Custom / Other - - GENERIC - + For agents that don't follow a standard protocol, just needs a virtual key @@ -935,7 +934,7 @@ const AddAgentForm: React.FC = ({ visible, onClose, accessTok )} - Recommended + diff --git a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/UserEnvVarsModal.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/UserEnvVarsModal.tsx index ea2addcd5f1..66b33ff2db8 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/UserEnvVarsModal.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/UserEnvVarsModal.tsx @@ -10,6 +10,7 @@ import { FormField } from "@/components/shared/form/FormField"; import { Alert, AlertTitle } from "@/components/shared/Alert"; import { PasswordInput } from "@/components/shared/PasswordInput"; import { Badge } from "@/components/ui/badge"; +import { StatusBadge } from "@/components/shared/table_cells/status_badge"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { UiLoadingSpinner } from "@/components/ui/ui-loading-spinner"; @@ -133,7 +134,7 @@ const UserEnvVarsModal: React.FC = ({ server, open, acces
Set your credentials - Per-user +
{displayName}
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/old-usage/_components/usage.tsx b/ui/litellm-dashboard/src/app/(dashboard)/old-usage/_components/usage.tsx index aca1db0fbae..42be1b34f07 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/old-usage/_components/usage.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/old-usage/_components/usage.tsx @@ -17,7 +17,7 @@ import { ComboboxValue, useComboboxAnchor, } from "@/components/ui/combobox"; -import { Meter, MeterIndicator, MeterTrack } from "@/components/ui/meter"; +import { Meter, MeterIndicator, MeterTrack } from "@/components/shared/Meter"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/ChatComposer.tsx b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/ChatComposer.tsx index 6ae7b855f1e..4925e775ac7 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/ChatComposer.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/ChatComposer.tsx @@ -94,7 +94,16 @@ export function ChatComposer({ /> )} - + { + if ((event.target as HTMLElement).closest("button")) { + return; + } + event.currentTarget.parentElement?.querySelector("[data-slot=input-group-control]")?.focus(); + }} + >
{tools}
{isLoading && onCancel ? ( diff --git a/ui/litellm-dashboard/src/app/(dashboard)/policies/_components/add_policy_form.tsx b/ui/litellm-dashboard/src/app/(dashboard)/policies/_components/add_policy_form.tsx index 4214db277a3..403562bc7a3 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/policies/_components/add_policy_form.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/policies/_components/add_policy_form.tsx @@ -10,6 +10,7 @@ import { SearchSelect } from "@/components/shared/SearchSelect"; import { FieldGroup } from "@/components/shared/form/field"; import { FormField } from "@/components/shared/form/FormField"; import { Badge } from "@/components/ui/badge"; +import { StatusBadge } from "@/components/shared/table_cells/status_badge"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; @@ -466,9 +467,7 @@ const AddPolicyForm: React.FC = ({
{resolvedGuardrails.map((g) => ( - - {g} - + ))}
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/projects/_components/ProjectDetailsPage.tsx b/ui/litellm-dashboard/src/app/(dashboard)/projects/_components/ProjectDetailsPage.tsx index d7a4fce4beb..f94240f3c9e 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/projects/_components/ProjectDetailsPage.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/projects/_components/ProjectDetailsPage.tsx @@ -9,7 +9,7 @@ import { StatusBadge } from "@/components/shared/table_cells/status_badge"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { Meter, MeterIndicator, MeterTrack } from "@/components/ui/meter"; +import { Meter, MeterIndicator, MeterTrack } from "@/components/shared/Meter"; import { UiLoadingSpinner } from "@/components/ui/ui-loading-spinner"; import { EditProjectModal } from "./ProjectModals/EditProjectModal"; import { ProjectKeysSection } from "./ProjectKeysSection"; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EndpointUsage/components/EndpointUsageTable.tsx b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EndpointUsage/components/EndpointUsageTable.tsx index 384590f754d..3d19d825c0c 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EndpointUsage/components/EndpointUsageTable.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EndpointUsage/components/EndpointUsageTable.tsx @@ -1,6 +1,6 @@ import React from "react"; import type { ColumnDef } from "@tanstack/react-table"; -import { Meter, MeterIndicator, MeterTrack } from "@/components/ui/meter"; +import { Meter, MeterIndicator, MeterTrack } from "@/components/shared/Meter"; import { DataTable } from "@/components/shared/DataTable"; import { MoneyCell } from "@/components/shared/table_cells"; import { MetricWithMetadata } from "@/components/UsagePage/types"; diff --git a/ui/litellm-dashboard/src/components/Settings/AdminSettings/SSOSettings/RoleMappings.tsx b/ui/litellm-dashboard/src/components/Settings/AdminSettings/SSOSettings/RoleMappings.tsx index 2ea78f4b12b..966a2a9904d 100644 --- a/ui/litellm-dashboard/src/components/Settings/AdminSettings/SSOSettings/RoleMappings.tsx +++ b/ui/litellm-dashboard/src/components/Settings/AdminSettings/SSOSettings/RoleMappings.tsx @@ -1,7 +1,7 @@ import type { RoleMappings as RoleMappingsType } from "@/app/(dashboard)/hooks/sso/useSSOSettings"; import type { ColumnDef } from "@tanstack/react-table"; import { DataTable } from "@/components/shared/DataTable"; -import { Badge } from "@/components/ui/badge"; +import { StatusBadge } from "@/components/shared/table_cells/status_badge"; import { Card, CardContent } from "@/components/ui/card"; import { Separator } from "@/components/ui/separator"; import { Users } from "lucide-react"; @@ -34,9 +34,7 @@ export default function RoleMappings({ roleMappings }: { roleMappings: RoleMappi row.original.groups.length > 0 ? (
{row.original.groups.map((group, index) => ( - - {group} - + ))}
) : ( diff --git a/ui/litellm-dashboard/src/components/SidebarUsageCard.tsx b/ui/litellm-dashboard/src/components/SidebarUsageCard.tsx index 76240565cda..effb6c29e8e 100644 --- a/ui/litellm-dashboard/src/components/SidebarUsageCard.tsx +++ b/ui/litellm-dashboard/src/components/SidebarUsageCard.tsx @@ -2,7 +2,7 @@ import { useLicenseInfo } from "@/app/(dashboard)/hooks/license/useLicenseInfo"; import { formatExpirationStatus } from "@/utils/licenseUtils"; import { Button } from "@/components/ui/button"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; -import { Meter, MeterIndicator, MeterLabel, MeterTrack } from "@/components/ui/meter"; +import { Meter, MeterIndicator, MeterLabel, MeterTrack } from "@/components/shared/Meter"; import { useQuery } from "@tanstack/react-query"; import { Award, ChevronDown, Loader2 } from "lucide-react"; import { getRemainingUsers } from "./networking"; diff --git a/ui/litellm-dashboard/src/components/leftnav.tsx b/ui/litellm-dashboard/src/components/leftnav.tsx index 7fef1e62a6a..b58d7bd0d02 100644 --- a/ui/litellm-dashboard/src/components/leftnav.tsx +++ b/ui/litellm-dashboard/src/components/leftnav.tsx @@ -20,7 +20,7 @@ import { SidebarMenuSub, SidebarSeparator, sidebarMenuButtonVariants, -} from "@/components/ui/sidebar"; +} from "@/components/shared/Sidebar"; import { Activity, BarChart3, diff --git a/ui/litellm-dashboard/src/components/shared/Alert.tsx b/ui/litellm-dashboard/src/components/shared/Alert.tsx index e69a79d4629..490fcba6ef5 100644 --- a/ui/litellm-dashboard/src/components/shared/Alert.tsx +++ b/ui/litellm-dashboard/src/components/shared/Alert.tsx @@ -1,25 +1,29 @@ import * as React from "react"; -import { type VariantProps } from "cva"; -import { cn, cva } from "@/lib/cva.config"; +import { cva, type VariantProps } from "class-variance-authority"; -const alertVariants = cva({ - base: "group/alert relative grid w-full gap-0.5 rounded-lg border px-4 py-3 text-left text-sm has-data-[slot=alert-action]:relative has-data-[slot=alert-action]:pr-18 has-[>svg]:grid-cols-[auto_1fr] has-[>svg]:gap-x-2.5 *:[svg]:row-span-2 *:[svg]:translate-y-0.5 *:[svg]:text-current *:[svg:not([class*='size-'])]:size-4", - variants: { - variant: { - default: "bg-card text-card-foreground", - destructive: "bg-card text-destructive *:data-[slot=alert-description]:text-destructive/90 *:[svg]:text-current", - info: "border-info/20 bg-info/5 text-info *:[svg]:text-current", - success: "border-success/20 bg-success/5 text-success *:[svg]:text-current", - warning: "border-warning/20 bg-warning/5 text-warning *:[svg]:text-current", - error: - "border-destructive/20 bg-destructive/10 text-destructive *:data-[slot=alert-description]:text-destructive/90 *:[svg]:text-destructive", +import { cn } from "@/lib/cva.config"; + +const alertVariants = cva( + "group/alert relative grid w-full gap-0.5 rounded-lg border px-4 py-3 text-left text-sm has-data-[slot=alert-action]:relative has-data-[slot=alert-action]:pr-18 has-[>svg]:grid-cols-[auto_1fr] has-[>svg]:gap-x-2.5 *:[svg]:row-span-2 *:[svg]:translate-y-0.5 *:[svg]:text-current *:[svg:not([class*='size-'])]:size-4", + { + variants: { + variant: { + default: "bg-card text-card-foreground", + destructive: + "bg-card text-destructive *:data-[slot=alert-description]:text-destructive/90 *:[svg]:text-current", + info: "border-info/20 bg-info/5 text-info *:[svg]:text-current", + success: "border-success/20 bg-success/5 text-success *:[svg]:text-current", + warning: "border-warning/20 bg-warning/5 text-warning *:[svg]:text-current", + error: + "border-destructive/20 bg-destructive/10 text-destructive *:data-[slot=alert-description]:text-destructive/90 *:[svg]:text-destructive", + }, + }, + defaultVariants: { + variant: "default", }, }, - defaultVariants: { - variant: "default", - }, -}); +); type AlertProps = React.ComponentProps<"div"> & VariantProps; diff --git a/ui/litellm-dashboard/src/components/ui/meter.test.tsx b/ui/litellm-dashboard/src/components/shared/Meter.test.tsx similarity index 99% rename from ui/litellm-dashboard/src/components/ui/meter.test.tsx rename to ui/litellm-dashboard/src/components/shared/Meter.test.tsx index 70aedefee40..1fbb785e55d 100644 --- a/ui/litellm-dashboard/src/components/ui/meter.test.tsx +++ b/ui/litellm-dashboard/src/components/shared/Meter.test.tsx @@ -1,6 +1,6 @@ import { render, screen } from "@testing-library/react"; import { describe, expect, it } from "vitest"; -import { Meter, MeterIndicator, MeterLabel, MeterTrack } from "./meter"; +import { Meter, MeterIndicator, MeterLabel, MeterTrack } from "./Meter"; const renderMeter = (value: number, max: number) => render( diff --git a/ui/litellm-dashboard/src/components/ui/meter.tsx b/ui/litellm-dashboard/src/components/shared/Meter.tsx similarity index 91% rename from ui/litellm-dashboard/src/components/ui/meter.tsx rename to ui/litellm-dashboard/src/components/shared/Meter.tsx index ac18829efa9..26c60f3361f 100644 --- a/ui/litellm-dashboard/src/components/ui/meter.tsx +++ b/ui/litellm-dashboard/src/components/shared/Meter.tsx @@ -1,13 +1,13 @@ "use client"; import { Meter as MeterPrimitive } from "@base-ui/react/meter"; -import { type VariantProps } from "cva"; import * as React from "react"; -import { cn, cva } from "@/lib/cva.config"; +import { cva, type VariantProps } from "class-variance-authority"; -const meterIndicatorVariants = cva({ - base: "h-full rounded-full transition-[width] duration-300", +import { cn } from "@/lib/cva.config"; + +const meterIndicatorVariants = cva("h-full rounded-full transition-[width] duration-300", { variants: { tone: { default: "bg-primary", diff --git a/ui/litellm-dashboard/src/components/shared/Sidebar.test.tsx b/ui/litellm-dashboard/src/components/shared/Sidebar.test.tsx new file mode 100644 index 00000000000..8c0d2790489 --- /dev/null +++ b/ui/litellm-dashboard/src/components/shared/Sidebar.test.tsx @@ -0,0 +1,36 @@ +import { render, screen } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; + +import { SidebarMenuButton, sidebarMenuButtonVariants } from "./Sidebar"; + +const CVA_CONFIG_KEYS = ["base", "variants", "defaultVariants"]; + +describe("sidebarMenuButtonVariants", () => { + it("emits its base classes rather than the names of its own config keys", () => { + const emitted = sidebarMenuButtonVariants({}).split(" "); + + expect(emitted).toContain("rounded-md"); + expect(emitted).toContain("text-sidebar-foreground/70"); + expect(CVA_CONFIG_KEYS.filter((key) => emitted.includes(key))).toEqual([]); + }); + + it("applies the isActive variant on top of the base classes", () => { + const active = sidebarMenuButtonVariants({ isActive: true }).split(" "); + + expect(active).toContain("bg-sidebar-accent"); + expect(active).toContain("rounded-md"); + expect(sidebarMenuButtonVariants({ isActive: false }).split(" ")).not.toContain("bg-sidebar-accent"); + }); +}); + +describe("SidebarMenuButton", () => { + it("renders the variant classes onto the button", () => { + render(Keys); + const button = screen.getByRole("button", { name: "Keys" }); + + expect(button).toHaveClass("bg-sidebar-accent", "rounded-md"); + for (const key of CVA_CONFIG_KEYS) { + expect(button).not.toHaveClass(key); + } + }); +}); diff --git a/ui/litellm-dashboard/src/components/ui/sidebar.tsx b/ui/litellm-dashboard/src/components/shared/Sidebar.tsx similarity index 91% rename from ui/litellm-dashboard/src/components/ui/sidebar.tsx rename to ui/litellm-dashboard/src/components/shared/Sidebar.tsx index a0c94ab6fee..06e19d1b674 100644 --- a/ui/litellm-dashboard/src/components/ui/sidebar.tsx +++ b/ui/litellm-dashboard/src/components/shared/Sidebar.tsx @@ -2,9 +2,10 @@ import * as React from "react"; import { Button as ButtonPrimitive } from "@base-ui/react/button"; -import { type VariantProps } from "cva"; -import { cn, cva } from "@/lib/cva.config"; +import { cva, type VariantProps } from "class-variance-authority"; + +import { cn } from "@/lib/cva.config"; type SidebarContextValue = { collapsed: boolean }; const SidebarContext = React.createContext({ collapsed: false }); @@ -136,8 +137,8 @@ const SidebarMenuBadge = React.forwardRefsvg]:size-[18px] [&>svg]:shrink-0", "group-data-[collapsed=true]/sidebar:mx-auto group-data-[collapsed=true]/sidebar:size-9 group-data-[collapsed=true]/sidebar:justify-center group-data-[collapsed=true]/sidebar:gap-0 group-data-[collapsed=true]/sidebar:px-0", - ].join(" "), - variants: { - isActive: { - true: "bg-sidebar-accent text-sidebar-accent-foreground before:absolute before:inset-y-1.5 before:left-0 before:w-[3px] before:rounded-r-full before:bg-sidebar-primary group-data-[collapsed=true]/sidebar:before:hidden", - false: "", - }, - size: { - default: "h-[34px]", - sub: "h-[34px]", + ], + { + variants: { + isActive: { + true: "bg-sidebar-accent text-sidebar-accent-foreground before:absolute before:inset-y-1.5 before:left-0 before:w-[3px] before:rounded-r-full before:bg-sidebar-primary group-data-[collapsed=true]/sidebar:before:hidden", + false: "", + }, + size: { + default: "h-[34px]", + sub: "h-[34px]", + }, }, + defaultVariants: { isActive: false, size: "default" }, }, - defaultVariants: { isActive: false, size: "default" }, -}); +); type SidebarMenuButtonProps = ButtonPrimitive.Props & VariantProps; diff --git a/ui/litellm-dashboard/src/components/shared/form/field.tsx b/ui/litellm-dashboard/src/components/shared/form/field.tsx index 36ce691827c..a5bf896313a 100644 --- a/ui/litellm-dashboard/src/components/shared/form/field.tsx +++ b/ui/litellm-dashboard/src/components/shared/form/field.tsx @@ -1,11 +1,12 @@ "use client"; import * as React from "react"; -import { type VariantProps } from "cva"; import { Label } from "@/components/ui/label"; import { Separator } from "@/components/ui/separator"; -import { cn, cva } from "@/lib/cva.config"; +import { cva, type VariantProps } from "class-variance-authority"; + +import { cn } from "@/lib/cva.config"; const FieldSet = React.forwardRef>( ({ className, ...props }, ref) => ( @@ -51,8 +52,7 @@ const FieldGroup = React.forwardRef.sr-only]:w-auto", diff --git a/ui/litellm-dashboard/src/components/shared/table_cells/spend_budget_cell.tsx b/ui/litellm-dashboard/src/components/shared/table_cells/spend_budget_cell.tsx index 943b9aa766c..127c7202121 100644 --- a/ui/litellm-dashboard/src/components/shared/table_cells/spend_budget_cell.tsx +++ b/ui/litellm-dashboard/src/components/shared/table_cells/spend_budget_cell.tsx @@ -1,7 +1,7 @@ "use client"; import { InheritedBudgetHint, type InheritedBudgetGate } from "@/components/shared/InheritedBudgetHint"; -import { Meter, MeterIndicator, MeterTrack } from "@/components/ui/meter"; +import { Meter, MeterIndicator, MeterTrack } from "@/components/shared/Meter"; import { formatNumberWithCommas, getSpendString } from "@/utils/dataUtils"; interface SpendBudgetCellProps { diff --git a/ui/litellm-dashboard/src/components/shared/table_cells/status_badge.tsx b/ui/litellm-dashboard/src/components/shared/table_cells/status_badge.tsx index dbd4d511d89..cc7f32d0bd6 100644 --- a/ui/litellm-dashboard/src/components/shared/table_cells/status_badge.tsx +++ b/ui/litellm-dashboard/src/components/shared/table_cells/status_badge.tsx @@ -22,11 +22,16 @@ interface StatusBadgeProps { label: string; tooltip?: React.ReactNode; dataTestId?: string; + className?: string; } -export function StatusBadge({ tone, label, tooltip, dataTestId }: StatusBadgeProps) { +export function StatusBadge({ tone, label, tooltip, dataTestId, className }: StatusBadgeProps) { const badge = ( - + {label} ); diff --git a/ui/litellm-dashboard/src/components/ui/badge.tsx b/ui/litellm-dashboard/src/components/ui/badge.tsx index ec5417d227a..0117fd4252e 100644 --- a/ui/litellm-dashboard/src/components/ui/badge.tsx +++ b/ui/litellm-dashboard/src/components/ui/badge.tsx @@ -1,43 +1,49 @@ -import * as React from "react"; import { mergeProps } from "@base-ui/react/merge-props"; import { useRender } from "@base-ui/react/use-render"; -import { type VariantProps } from "cva"; +import { cva, type VariantProps } from "class-variance-authority"; -import { cn, cva } from "@/lib/cva.config"; +import { cn } from "@/lib/cva.config"; -const badgeVariants = cva({ - base: "group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!", - variants: { - variant: { - default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80", - secondary: "bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80", - destructive: - "bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20", - success: "bg-success/10 text-success dark:bg-success/20 [a]:hover:bg-success/20", - warning: "bg-warning/10 text-warning dark:bg-warning/20 [a]:hover:bg-warning/20", - info: "bg-info/10 text-info dark:bg-info/20 [a]:hover:bg-info/20", - outline: "border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground", - ghost: "hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50", - link: "text-primary underline-offset-4 hover:underline", +const badgeVariants = cva( + "group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80", + secondary: "bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80", + destructive: + "bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20", + outline: "border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground", + ghost: "hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50", + link: "text-primary underline-offset-4 hover:underline", + }, + }, + defaultVariants: { + variant: "default", }, }, - defaultVariants: { - variant: "default", - }, -}); - -type BadgeProps = useRender.ComponentProps<"span"> & VariantProps; - -const Badge = React.forwardRef( - ({ className, variant = "default", render, ...props }, ref) => - useRender({ - defaultTagName: "span", - ref, - props: mergeProps<"span">({ className: cn(badgeVariants({ variant }), className) }, props), - render, - state: { slot: "badge", variant }, - }), ); -Badge.displayName = "Badge"; + +function Badge({ + className, + variant = "default", + render, + ...props +}: useRender.ComponentProps<"span"> & VariantProps) { + return useRender({ + defaultTagName: "span", + props: mergeProps<"span">( + { + className: cn(badgeVariants({ variant }), className), + }, + props, + ), + render, + state: { + slot: "badge", + variant, + }, + }); +} export { Badge, badgeVariants }; diff --git a/ui/litellm-dashboard/src/components/ui/button-group.tsx b/ui/litellm-dashboard/src/components/ui/button-group.tsx index bfbd6577044..4ad0b493e65 100644 --- a/ui/litellm-dashboard/src/components/ui/button-group.tsx +++ b/ui/litellm-dashboard/src/components/ui/button-group.tsx @@ -1,24 +1,26 @@ import { mergeProps } from "@base-ui/react/merge-props"; import { useRender } from "@base-ui/react/use-render"; -import { type VariantProps } from "cva"; +import { cva, type VariantProps } from "class-variance-authority"; -import { cn, cva } from "@/lib/cva.config"; +import { cn } from "@/lib/cva.config"; import { Separator } from "@/components/ui/separator"; -const buttonGroupVariants = cva({ - base: "flex w-fit items-stretch *:focus-visible:relative *:focus-visible:z-10 has-[>[data-slot=button-group]]:gap-2 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-md [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1", - variants: { - orientation: { - horizontal: - "*:data-slot:rounded-r-none [&>[data-slot]:not(:has(~[data-slot]))]:rounded-r-md! [&>[data-slot]~[data-slot]]:rounded-l-none [&>[data-slot]~[data-slot]]:border-l-0", - vertical: - "flex-col *:data-slot:rounded-b-none [&>[data-slot]:not(:has(~[data-slot]))]:rounded-b-md! [&>[data-slot]~[data-slot]]:rounded-t-none [&>[data-slot]~[data-slot]]:border-t-0", +const buttonGroupVariants = cva( + "flex w-fit items-stretch *:focus-visible:relative *:focus-visible:z-10 has-[>[data-slot=button-group]]:gap-2 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-md [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1", + { + variants: { + orientation: { + horizontal: + "*:data-slot:rounded-r-none [&>[data-slot]:not(:has(~[data-slot]))]:rounded-r-md! [&>[data-slot]~[data-slot]]:rounded-l-none [&>[data-slot]~[data-slot]]:border-l-0", + vertical: + "flex-col *:data-slot:rounded-b-none [&>[data-slot]:not(:has(~[data-slot]))]:rounded-b-md! [&>[data-slot]~[data-slot]]:rounded-t-none [&>[data-slot]~[data-slot]]:border-t-0", + }, + }, + defaultVariants: { + orientation: "horizontal", }, }, - defaultVariants: { - orientation: "horizontal", - }, -}); +); function ButtonGroup({ className, diff --git a/ui/litellm-dashboard/src/components/ui/button.tsx b/ui/litellm-dashboard/src/components/ui/button.tsx index b60160567b6..4fb8eb0d27b 100644 --- a/ui/litellm-dashboard/src/components/ui/button.tsx +++ b/ui/litellm-dashboard/src/components/ui/button.tsx @@ -1,57 +1,51 @@ -import * as React from "react"; import { Button as ButtonPrimitive } from "@base-ui/react/button"; -import { type VariantProps } from "cva"; +import { cva, type VariantProps } from "class-variance-authority"; -import { cn, cva } from "@/lib/cva.config"; +import { cn } from "@/lib/cva.config"; -const buttonVariants = cva({ - base: "group/button inline-flex shrink-0 items-center justify-center rounded-md border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", - variants: { - variant: { - default: "bg-primary text-primary-foreground hover:bg-primary/80", - outline: - "border-border bg-background shadow-xs hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50", - secondary: - "bg-secondary text-secondary-foreground hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)] aria-expanded:bg-secondary aria-expanded:text-secondary-foreground", - ghost: - "hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50", - destructive: - "bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40", - link: "text-primary underline-offset-4 hover:underline", +const buttonVariants = cva( + "group/button inline-flex shrink-0 items-center justify-center rounded-md border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground hover:bg-primary/80", + outline: + "border-border bg-background shadow-xs hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50", + secondary: + "bg-secondary text-secondary-foreground hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)] aria-expanded:bg-secondary aria-expanded:text-secondary-foreground", + ghost: + "hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50", + destructive: + "bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: + "h-9 gap-1.5 px-2.5 in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2", + xs: "h-6 gap-1 rounded-[min(var(--radius-md),8px)] px-2 text-xs in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3", + sm: "h-8 gap-1 rounded-[min(var(--radius-md),10px)] px-2.5 in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5", + lg: "h-10 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2", + icon: "size-9", + "icon-xs": + "size-6 rounded-[min(var(--radius-md),8px)] in-data-[slot=button-group]:rounded-md [&_svg:not([class*='size-'])]:size-3", + "icon-sm": "size-8 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-md", + "icon-lg": "size-10", + }, }, - size: { - default: - "h-9 gap-1.5 px-2.5 in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2", - xs: "h-6 gap-1 rounded-[min(var(--radius-md),8px)] px-2 text-xs in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3", - sm: "h-8 gap-1 rounded-[min(var(--radius-md),10px)] px-2.5 in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5", - lg: "h-10 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2", - icon: "size-9", - "icon-xs": - "size-6 rounded-[min(var(--radius-md),8px)] in-data-[slot=button-group]:rounded-md [&_svg:not([class*='size-'])]:size-3", - "icon-sm": "size-8 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-md", - "icon-lg": "size-10", + defaultVariants: { + variant: "default", + size: "default", }, }, - defaultVariants: { - variant: "default", - size: "default", - }, -}); - -type ButtonProps = ButtonPrimitive.Props & VariantProps; - -const Button = React.forwardRef( - ({ className, variant = "default", size = "default", ...props }, ref) => { - return ( - - ); - }, ); -Button.displayName = "Button"; + +function Button({ + className, + variant = "default", + size = "default", + ...props +}: ButtonPrimitive.Props & VariantProps) { + return ; +} export { Button, buttonVariants }; diff --git a/ui/litellm-dashboard/src/components/ui/input-group.tsx b/ui/litellm-dashboard/src/components/ui/input-group.tsx index f1a5c356f9a..78eb24f97ad 100644 --- a/ui/litellm-dashboard/src/components/ui/input-group.tsx +++ b/ui/litellm-dashboard/src/components/ui/input-group.tsx @@ -1,9 +1,9 @@ "use client"; import * as React from "react"; -import { type VariantProps } from "cva"; +import { cva, type VariantProps } from "class-variance-authority"; -import { cn, cva } from "@/lib/cva.config"; +import { cn } from "@/lib/cva.config"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; @@ -22,21 +22,23 @@ function InputGroup({ className, ...props }: React.ComponentProps<"div">) { ); } -const inputGroupAddonVariants = cva({ - base: "flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium text-muted-foreground select-none group-data-[disabled=true]/input-group:opacity-50 [&>kbd]:rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-4", - variants: { - align: { - "inline-start": "order-first pl-2 has-[>button]:-ml-1 has-[>kbd]:ml-[-0.15rem]", - "inline-end": "order-last pr-2 has-[>button]:-mr-1 has-[>kbd]:mr-[-0.15rem]", - "block-start": - "order-first w-full justify-start px-2.5 pt-2 group-has-[>input]/input-group:pt-2 [.border-b]:pb-2", - "block-end": "order-last w-full justify-start px-2.5 pb-2 group-has-[>input]/input-group:pb-2 [.border-t]:pt-2", +const inputGroupAddonVariants = cva( + "flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium text-muted-foreground select-none group-data-[disabled=true]/input-group:opacity-50 [&>kbd]:rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-4", + { + variants: { + align: { + "inline-start": "order-first pl-2 has-[>button]:-ml-1 has-[>kbd]:ml-[-0.15rem]", + "inline-end": "order-last pr-2 has-[>button]:-mr-1 has-[>kbd]:mr-[-0.15rem]", + "block-start": + "order-first w-full justify-start px-2.5 pt-2 group-has-[>input]/input-group:pt-2 [.border-b]:pb-2", + "block-end": "order-last w-full justify-start px-2.5 pb-2 group-has-[>input]/input-group:pb-2 [.border-t]:pt-2", + }, + }, + defaultVariants: { + align: "inline-start", }, }, - defaultVariants: { - align: "inline-start", - }, -}); +); function InputGroupAddon({ className, @@ -53,15 +55,14 @@ function InputGroupAddon({ if ((e.target as HTMLElement).closest("button")) { return; } - e.currentTarget.parentElement?.querySelector("[data-slot=input-group-control]")?.focus(); + e.currentTarget.parentElement?.querySelector("input")?.focus(); }} {...props} /> ); } -const inputGroupButtonVariants = cva({ - base: "flex items-center gap-2 text-sm shadow-none", +const inputGroupButtonVariants = cva("flex items-center gap-2 text-sm shadow-none", { variants: { size: { xs: "h-6 gap-1 rounded-[calc(var(--radius)-5px)] px-1.5 [&>svg:not([class*='size-'])]:size-3.5", @@ -75,16 +76,18 @@ const inputGroupButtonVariants = cva({ }, }); -const InputGroupButton = React.forwardRef< - React.ComponentRef, - Omit, "size" | "type"> & - VariantProps & { - type?: "button" | "submit" | "reset"; - } ->(({ className, type = "button", variant = "ghost", size = "xs", ...props }, ref) => { +function InputGroupButton({ + className, + type = "button", + variant = "ghost", + size = "xs", + ...props +}: Omit, "size" | "type"> & + VariantProps & { + type?: "button" | "submit" | "reset"; + }) { return (