mirror of
https://github.com/BerriAI/litellm.git
synced 2026-08-28 05:25:59 +00:00
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 <name> --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.
This commit is contained in:
parent
463261d21d
commit
47c988e05c
27 changed files with 304 additions and 270 deletions
|
|
@ -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
|
||||
|
|
|
|||
37
ui/litellm-dashboard/package-lock.json
generated
37
ui/litellm-dashboard/package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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<AddAgentFormProps> = ({ visible, onClose, accessTok
|
|||
<span className="block">
|
||||
<span className="flex items-center gap-2">
|
||||
<span className="font-medium text-warning">Custom / Other</span>
|
||||
<Badge variant="warning" className="h-4 px-1 text-[10px]">
|
||||
GENERIC
|
||||
</Badge>
|
||||
<StatusBadge tone="warning" label="GENERIC" className="h-4 px-1 text-[10px]" />
|
||||
</span>
|
||||
<span className="block text-xs whitespace-normal text-warning">
|
||||
For agents that don't follow a standard protocol, just needs a virtual key
|
||||
|
|
@ -935,7 +934,7 @@ const AddAgentForm: React.FC<AddAgentFormProps> = ({ visible, onClose, accessTok
|
|||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Badge variant="success">Recommended</Badge>
|
||||
<StatusBadge tone="success" label="Recommended" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -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<UserEnvVarsModalProps> = ({ server, open, acces
|
|||
<DialogHeader>
|
||||
<div className="flex items-center gap-2">
|
||||
<DialogTitle className="text-base font-semibold">Set your credentials</DialogTitle>
|
||||
<Badge variant="info">Per-user</Badge>
|
||||
<StatusBadge tone="info" label="Per-user" />
|
||||
</div>
|
||||
<span className="text-xs text-muted-foreground">{displayName}</span>
|
||||
</DialogHeader>
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -94,7 +94,16 @@ export function ChatComposer({
|
|||
/>
|
||||
)}
|
||||
|
||||
<InputGroupAddon align="block-end" className="justify-between gap-2 px-3 pb-3 pt-1">
|
||||
<InputGroupAddon
|
||||
align="block-end"
|
||||
className="justify-between gap-2 px-3 pb-3 pt-1"
|
||||
onClick={(event) => {
|
||||
if ((event.target as HTMLElement).closest("button")) {
|
||||
return;
|
||||
}
|
||||
event.currentTarget.parentElement?.querySelector<HTMLElement>("[data-slot=input-group-control]")?.focus();
|
||||
}}
|
||||
>
|
||||
<div className="flex min-w-0 items-center gap-1">{tools}</div>
|
||||
|
||||
{isLoading && onCancel ? (
|
||||
|
|
|
|||
|
|
@ -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<AddPolicyFormProps> = ({
|
|||
</span>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{resolvedGuardrails.map((g) => (
|
||||
<Badge key={g} variant="info">
|
||||
{g}
|
||||
</Badge>
|
||||
<StatusBadge key={g} tone="info" label={g} />
|
||||
))}
|
||||
</div>
|
||||
</AlertDescription>
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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 ? (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{row.original.groups.map((group, index) => (
|
||||
<Badge key={index} variant="info">
|
||||
{group}
|
||||
</Badge>
|
||||
<StatusBadge key={index} tone="info" label={group} />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ import {
|
|||
SidebarMenuSub,
|
||||
SidebarSeparator,
|
||||
sidebarMenuButtonVariants,
|
||||
} from "@/components/ui/sidebar";
|
||||
} from "@/components/shared/Sidebar";
|
||||
import {
|
||||
Activity,
|
||||
BarChart3,
|
||||
|
|
|
|||
|
|
@ -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<typeof alertVariants>;
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
@ -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",
|
||||
36
ui/litellm-dashboard/src/components/shared/Sidebar.test.tsx
Normal file
36
ui/litellm-dashboard/src/components/shared/Sidebar.test.tsx
Normal file
|
|
@ -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(<SidebarMenuButton isActive>Keys</SidebarMenuButton>);
|
||||
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);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -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<SidebarContextValue>({ collapsed: false });
|
||||
|
|
@ -136,8 +137,8 @@ const SidebarMenuBadge = React.forwardRef<HTMLSpanElement, React.ComponentPropsW
|
|||
);
|
||||
SidebarMenuBadge.displayName = "SidebarMenuBadge";
|
||||
|
||||
const sidebarMenuButtonVariants = cva({
|
||||
base: [
|
||||
const sidebarMenuButtonVariants = cva(
|
||||
[
|
||||
"group/menu-btn relative flex w-full items-center gap-2.5 overflow-hidden rounded-md px-2.5 text-left text-[13px] font-medium no-underline",
|
||||
"text-sidebar-foreground/70 outline-none transition-colors",
|
||||
"hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
|
||||
|
|
@ -145,19 +146,21 @@ const sidebarMenuButtonVariants = cva({
|
|||
"disabled:pointer-events-none disabled:opacity-50",
|
||||
"[&>svg]: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<typeof sidebarMenuButtonVariants>;
|
||||
|
||||
|
|
@ -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<HTMLFieldSetElement, React.ComponentPropsWithoutRef<"fieldset">>(
|
||||
({ className, ...props }, ref) => (
|
||||
|
|
@ -51,8 +52,7 @@ const FieldGroup = React.forwardRef<HTMLDivElement, React.ComponentPropsWithoutR
|
|||
);
|
||||
FieldGroup.displayName = "FieldGroup";
|
||||
|
||||
const fieldVariants = cva({
|
||||
base: "group/field flex w-full gap-3 data-[invalid=true]:text-destructive",
|
||||
const fieldVariants = cva("group/field flex w-full gap-3 data-[invalid=true]:text-destructive", {
|
||||
variants: {
|
||||
orientation: {
|
||||
vertical: "flex-col *:w-full [&>.sr-only]:w-auto",
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 = (
|
||||
<Badge variant="outline" data-testid={dataTestId} className={cn("whitespace-nowrap font-normal", TONE_CLASS[tone])}>
|
||||
<Badge
|
||||
variant="outline"
|
||||
data-testid={dataTestId}
|
||||
className={cn("whitespace-nowrap font-normal", TONE_CLASS[tone], className)}
|
||||
>
|
||||
{label}
|
||||
</Badge>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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<typeof badgeVariants>;
|
||||
|
||||
const Badge = React.forwardRef<HTMLSpanElement, BadgeProps>(
|
||||
({ 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<typeof badgeVariants>) {
|
||||
return useRender({
|
||||
defaultTagName: "span",
|
||||
props: mergeProps<"span">(
|
||||
{
|
||||
className: cn(badgeVariants({ variant }), className),
|
||||
},
|
||||
props,
|
||||
),
|
||||
render,
|
||||
state: {
|
||||
slot: "badge",
|
||||
variant,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants };
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<typeof buttonVariants>;
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant = "default", size = "default", ...props }, ref) => {
|
||||
return (
|
||||
<ButtonPrimitive
|
||||
ref={ref}
|
||||
data-slot="button"
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
Button.displayName = "Button";
|
||||
|
||||
function Button({
|
||||
className,
|
||||
variant = "default",
|
||||
size = "default",
|
||||
...props
|
||||
}: ButtonPrimitive.Props & VariantProps<typeof buttonVariants>) {
|
||||
return <ButtonPrimitive data-slot="button" className={cn(buttonVariants({ variant, size, className }))} {...props} />;
|
||||
}
|
||||
|
||||
export { Button, buttonVariants };
|
||||
|
|
|
|||
|
|
@ -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<HTMLElement>("[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<typeof Button>,
|
||||
Omit<React.ComponentProps<typeof Button>, "size" | "type"> &
|
||||
VariantProps<typeof inputGroupButtonVariants> & {
|
||||
type?: "button" | "submit" | "reset";
|
||||
}
|
||||
>(({ className, type = "button", variant = "ghost", size = "xs", ...props }, ref) => {
|
||||
function InputGroupButton({
|
||||
className,
|
||||
type = "button",
|
||||
variant = "ghost",
|
||||
size = "xs",
|
||||
...props
|
||||
}: Omit<React.ComponentProps<typeof Button>, "size" | "type"> &
|
||||
VariantProps<typeof inputGroupButtonVariants> & {
|
||||
type?: "button" | "submit" | "reset";
|
||||
}) {
|
||||
return (
|
||||
<Button
|
||||
ref={ref}
|
||||
type={type}
|
||||
data-size={size}
|
||||
variant={variant}
|
||||
|
|
@ -92,8 +95,7 @@ const InputGroupButton = React.forwardRef<
|
|||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
InputGroupButton.displayName = "InputGroupButton";
|
||||
}
|
||||
|
||||
function InputGroupText({ className, ...props }: React.ComponentProps<"span">) {
|
||||
return (
|
||||
|
|
@ -107,38 +109,30 @@ function InputGroupText({ className, ...props }: React.ComponentProps<"span">) {
|
|||
);
|
||||
}
|
||||
|
||||
const InputGroupInput = React.forwardRef<HTMLInputElement, React.ComponentPropsWithoutRef<"input">>(
|
||||
({ className, ...props }, ref) => {
|
||||
return (
|
||||
<Input
|
||||
ref={ref}
|
||||
data-slot="input-group-control"
|
||||
className={cn(
|
||||
"flex-1 rounded-none border-0 bg-transparent shadow-none ring-0 focus-visible:ring-0 aria-invalid:ring-0 dark:bg-transparent",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
InputGroupInput.displayName = "InputGroupInput";
|
||||
function InputGroupInput({ className, ...props }: React.ComponentProps<"input">) {
|
||||
return (
|
||||
<Input
|
||||
data-slot="input-group-control"
|
||||
className={cn(
|
||||
"flex-1 rounded-none border-0 bg-transparent shadow-none ring-0 focus-visible:ring-0 aria-invalid:ring-0 dark:bg-transparent",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const InputGroupTextarea = React.forwardRef<HTMLTextAreaElement, React.ComponentPropsWithoutRef<"textarea">>(
|
||||
({ className, ...props }, ref) => {
|
||||
return (
|
||||
<Textarea
|
||||
ref={ref}
|
||||
data-slot="input-group-control"
|
||||
className={cn(
|
||||
"flex-1 resize-none rounded-none border-0 bg-transparent py-2 shadow-none ring-0 focus-visible:ring-0 aria-invalid:ring-0 dark:bg-transparent",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
InputGroupTextarea.displayName = "InputGroupTextarea";
|
||||
function InputGroupTextarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
||||
return (
|
||||
<Textarea
|
||||
data-slot="input-group-control"
|
||||
className={cn(
|
||||
"flex-1 resize-none rounded-none border-0 bg-transparent py-2 shadow-none ring-0 focus-visible:ring-0 aria-invalid:ring-0 dark:bg-transparent",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { InputGroup, InputGroupAddon, InputGroupButton, InputGroupText, InputGroupInput, InputGroupTextarea };
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
"use client";
|
||||
|
||||
import { Tabs as TabsPrimitive } from "@base-ui/react/tabs";
|
||||
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";
|
||||
|
||||
function Tabs({ className, orientation = "horizontal", ...props }: TabsPrimitive.Root.Props) {
|
||||
return (
|
||||
|
|
@ -16,18 +16,20 @@ function Tabs({ className, orientation = "horizontal", ...props }: TabsPrimitive
|
|||
);
|
||||
}
|
||||
|
||||
const tabsListVariants = cva({
|
||||
base: "group/tabs-list inline-flex w-fit items-center justify-center rounded-lg p-[3px] text-muted-foreground group-data-horizontal/tabs:h-9 group-data-vertical/tabs:h-fit group-data-vertical/tabs:flex-col data-[variant=line]:rounded-none",
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-muted",
|
||||
line: "gap-1 bg-transparent",
|
||||
const tabsListVariants = cva(
|
||||
"group/tabs-list inline-flex w-fit items-center justify-center rounded-lg p-[3px] text-muted-foreground group-data-horizontal/tabs:h-9 group-data-vertical/tabs:h-fit group-data-vertical/tabs:flex-col data-[variant=line]:rounded-none",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-muted",
|
||||
line: "gap-1 bg-transparent",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
});
|
||||
);
|
||||
|
||||
function TabsList({
|
||||
className,
|
||||
|
|
|
|||
|
|
@ -1,10 +1,8 @@
|
|||
import { defineConfig } from "cva";
|
||||
import { clsx, type ClassValue } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export const { cva, cx, compose } = defineConfig({
|
||||
hooks: {
|
||||
onComplete: (className) => twMerge(className),
|
||||
},
|
||||
});
|
||||
export { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
export const cn = cx;
|
||||
export const cn = (...inputs: ClassValue[]) => twMerge(clsx(inputs));
|
||||
|
||||
export const cx = cn;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue