refactor(ui): switch shadcn primitives from Radix to Base UI (#32124)

* refactor(ui): switch shadcn primitives from Radix to Base UI

shadcn made Base UI the default primitive library in July 2026 and our
only shadcn component so far is the Button canary, so this is the last
cheap moment to switch before the primitives phase adds the full set.

components.json style moves from new-york (a legacy alias that resolves
to the Radix variant) to base-vega. Button is regenerated from the
base-vega registry with the same local adaptations as before: cva beta
object form via lib/cva.config and a React 18 forwardRef wrapper. The
polymorphic asChild prop becomes Base UI's render prop.

radix-ui is replaced by @base-ui/react 1.6.0. Base UI optionally peers
on date-fns 4 while tremor pins 3, so date-fns is bumped to 4.4.0 with
an npm override; our only usage (add) is API-identical and the override
can go away when tremor does.

* refactor(ui): convert chat UI shadcn components from Radix to Base UI

The chat UI migration landed 11 components/ui files generated against
the old Radix registry config after this branch cut over to Base UI,
which would have left them importing a deleted package. All 11 (dialog,
alert-dialog, select, popover, tooltip, tabs, switch, scroll-area,
collapsible, separator, label) are regenerated from the base-vega
registry, with the repo conventions re-applied where relevant (cva beta
object form from lib/cva.config in tabs; the Button canary keeps its
React 18 forwardRef adaptation).

Chat feature call sites move from the Radix asChild pattern to Base
UI's render prop, and TooltipProvider delayDuration becomes delay.

* fix(ui): restore security override pins clobbered by the date-fns override

The date-fns 4 override was written by replacing the whole overrides
object, dropping the ten security pins (prismjs, js-yaml, glob,
minimatch, lodash, ws, braces, axios, postcss, esbuild) that keep
patched versions in the lockfile; osv-scan caught the vulnerable
versions resurfacing. Restores the pins alongside date-fns and
regenerates the lockfile.

* test(ui): pin tremor DateRangePicker behavior on date-fns 4

The date-fns 4 override forces react-day-picker 8 (authored against v3)
onto v4 at runtime, which a build or lint pass cannot validate. This
renders the shared UsageDatePicker wrapper, opens the calendar, checks
the month grid, and selects a day, so a date-fns API break in the
tremor date path fails tests instead of throwing in production. Delete
alongside the override when tremor is removed.

* fix(ui): close the alert dialog when AlertDialogAction is clicked

The base-vega registry template renders AlertDialogAction as a plain
Button with no Close binding, so confirm buttons fired their onClick
but left the dialog open; both consumers (conversation delete,
MCP credential revoke) were written against the Radix semantics where
Action dismisses on click. Binds Action to AlertDialogPrimitive.Close
via the render prop, mirroring AlertDialogCancel, and pins the
behavior with a test so a future shadcn add --overwrite cannot
silently reintroduce the template's non-closing Action.
This commit is contained in:
ryan-crabbe-berri 2026-07-07 09:55:41 -07:00 committed by GitHub
parent dc48b20491
commit cfe9e39e55
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 586 additions and 2099 deletions

View file

@ -1,6 +1,6 @@
{ {
"$schema": "https://ui.shadcn.com/schema.json", "$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york", "style": "base-vega",
"rsc": true, "rsc": true,
"tsx": true, "tsx": true,
"tailwind": { "tailwind": {

File diff suppressed because it is too large Load diff

View file

@ -26,6 +26,7 @@
"dependencies": { "dependencies": {
"@ant-design/cssinjs": "1.24.0", "@ant-design/cssinjs": "1.24.0",
"@anthropic-ai/sdk": "0.92.0", "@anthropic-ai/sdk": "0.92.0",
"@base-ui/react": "^1.6.0",
"@headlessui/tailwindcss": "0.2.2", "@headlessui/tailwindcss": "0.2.2",
"@heroicons/react": "1.0.6", "@heroicons/react": "1.0.6",
"@tanstack/react-pacer": "0.2.0", "@tanstack/react-pacer": "0.2.0",
@ -35,7 +36,7 @@
"@types/papaparse": "5.5.2", "@types/papaparse": "5.5.2",
"antd": "5.29.3", "antd": "5.29.3",
"cva": "1.0.0-beta.4", "cva": "1.0.0-beta.4",
"date-fns": "3.6.0", "date-fns": "^4.4.0",
"dayjs": "1.11.19", "dayjs": "1.11.19",
"jwt-decode": "4.0.0", "jwt-decode": "4.0.0",
"lucide-react": "0.513.0", "lucide-react": "0.513.0",
@ -43,7 +44,6 @@
"next": "16.2.6", "next": "16.2.6",
"openai": "4.104.0", "openai": "4.104.0",
"papaparse": "5.5.3", "papaparse": "5.5.3",
"radix-ui": "1.6.1",
"react": "18.3.1", "react": "18.3.1",
"react-copy-to-clipboard": "5.1.1", "react-copy-to-clipboard": "5.1.1",
"react-dom": "18.3.1", "react-dom": "18.3.1",
@ -96,7 +96,8 @@
"braces": "3.0.3", "braces": "3.0.3",
"axios": "1.13.6", "axios": "1.13.6",
"postcss": "8.5.13", "postcss": "8.5.13",
"esbuild": "0.28.1" "esbuild": "0.28.1",
"date-fns": "^4.4.0"
}, },
"engines": { "engines": {
"node": ">=20.9.0", "node": ">=20.9.0",

View file

@ -403,32 +403,34 @@ export default function ChatConversationPage() {
if (!open) setModelSearchText(""); if (!open) setModelSearchText("");
}} }}
> >
<PopoverTrigger asChild> <PopoverTrigger
<Button variant="outline" size="sm" className="max-w-[240px] justify-start gap-1.5 overflow-hidden"> render={
{selectedModel ? ( <Button variant="outline" size="sm" className="max-w-[240px] justify-start gap-1.5 overflow-hidden">
<> {selectedModel ? (
{(() => { <>
const provider = getProviderFromModelName(selectedModel); {(() => {
const { logo } = provider ? getProviderLogoAndName(provider) : { logo: "" }; const provider = getProviderFromModelName(selectedModel);
return logo ? ( const { logo } = provider ? getProviderLogoAndName(provider) : { logo: "" };
<img return logo ? (
src={logo} <img
alt="" src={logo}
className="w-4 h-4 object-contain shrink-0" alt=""
onError={(e) => { className="w-4 h-4 object-contain shrink-0"
(e.currentTarget as HTMLImageElement).style.display = "none"; onError={(e) => {
}} (e.currentTarget as HTMLImageElement).style.display = "none";
/> }}
) : null; />
})()} ) : null;
<span className="overflow-hidden text-ellipsis whitespace-nowrap">{selectedModel}</span> })()}
</> <span className="overflow-hidden text-ellipsis whitespace-nowrap">{selectedModel}</span>
) : ( </>
<span className="text-muted-foreground">Select model</span> ) : (
)} <span className="text-muted-foreground">Select model</span>
<ChevronDown className="h-3 w-3 text-muted-foreground shrink-0" /> )}
</Button> <ChevronDown className="h-3 w-3 text-muted-foreground shrink-0" />
</PopoverTrigger> </Button>
}
/>
<PopoverContent align="start" side="top" className="p-0 w-auto"> <PopoverContent align="start" side="top" className="p-0 w-auto">
{modelSelectorContent} {modelSelectorContent}
</PopoverContent> </PopoverContent>
@ -456,14 +458,16 @@ export default function ChatConversationPage() {
<div className="flex items-center gap-2 min-w-0"> <div className="flex items-center gap-2 min-w-0">
{modelSelectorTrigger} {modelSelectorTrigger}
<Popover open={mcpPopoverOpen} onOpenChange={setMcpPopoverOpen}> <Popover open={mcpPopoverOpen} onOpenChange={setMcpPopoverOpen}>
<PopoverTrigger asChild> <PopoverTrigger
<Button variant="outline" size="sm" className="gap-1 px-2.5 text-muted-foreground"> render={
<Plus className="h-3.5 w-3.5" /> <Button variant="outline" size="sm" className="gap-1 px-2.5 text-muted-foreground">
{selectedMCPServers.length > 0 && ( <Plus className="h-3.5 w-3.5" />
<span className="text-xs text-primary font-medium">{selectedMCPServers.length}</span> {selectedMCPServers.length > 0 && (
)} <span className="text-xs text-primary font-medium">{selectedMCPServers.length}</span>
</Button> )}
</PopoverTrigger> </Button>
}
/>
<PopoverContent side="top" align="start" className="p-0 w-auto"> <PopoverContent side="top" align="start" className="p-0 w-auto">
<MCPConnectPicker <MCPConnectPicker
accessToken={accessToken} accessToken={accessToken}

View file

@ -150,21 +150,23 @@ function UserBubble({ message, onEdit, isStreaming }: UserBubbleProps) {
> >
<div className="flex items-end gap-1.5 max-w-[72%]"> <div className="flex items-end gap-1.5 max-w-[72%]">
{hovered && !isStreaming && onEdit && ( {hovered && !isStreaming && onEdit && (
<TooltipProvider delayDuration={300}> <TooltipProvider delay={300}>
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger
<Button render={
variant="ghost" <Button
size="icon-xs" variant="ghost"
onClick={() => { size="icon-xs"
setEditValue(message.content); onClick={() => {
setEditing(true); setEditValue(message.content);
}} setEditing(true);
className="text-muted-foreground hover:text-foreground shrink-0" }}
> className="text-muted-foreground hover:text-foreground shrink-0"
<Pencil className="size-3.5" /> >
</Button> <Pencil className="size-3.5" />
</TooltipTrigger> </Button>
}
/>
<TooltipContent> <TooltipContent>
<p>Edit message</p> <p>Edit message</p>
</TooltipContent> </TooltipContent>
@ -265,18 +267,20 @@ function CopyButton({ text }: { text: string }) {
return ( return (
<div className="flex items-center gap-1 mt-1.5"> <div className="flex items-center gap-1 mt-1.5">
<TooltipProvider delayDuration={300}> <TooltipProvider delay={300}>
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger
<Button render={
variant="ghost" <Button
size="icon-xs" variant="ghost"
onClick={handleCopy} size="icon-xs"
className={copied ? "text-emerald-600" : "text-muted-foreground hover:text-foreground"} onClick={handleCopy}
> className={copied ? "text-emerald-600" : "text-muted-foreground hover:text-foreground"}
{copied ? <Check className="size-3.5" /> : <Copy className="size-3.5" />} >
</Button> {copied ? <Check className="size-3.5" /> : <Copy className="size-3.5" />}
</TooltipTrigger> </Button>
}
/>
<TooltipContent> <TooltipContent>
<p>{copied ? "Copied!" : "Copy"}</p> <p>{copied ? "Copied!" : "Copy"}</p>
</TooltipContent> </TooltipContent>

View file

@ -143,13 +143,15 @@ const ConversationRow: React.FC<ConversationRowProps> = ({ conv, isActive, onSel
className="flex gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity shrink-0" className="flex gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity shrink-0"
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
> >
<TooltipProvider delayDuration={300}> <TooltipProvider delay={300}>
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger
<Button onClick={startEditing} variant="ghost" size="icon-xs" className="text-muted-foreground"> render={
<Pencil className="h-3 w-3" /> <Button onClick={startEditing} variant="ghost" size="icon-xs" className="text-muted-foreground">
</Button> <Pencil className="h-3 w-3" />
</TooltipTrigger> </Button>
}
/>
<TooltipContent side="bottom"> <TooltipContent side="bottom">
<p>Rename</p> <p>Rename</p>
</TooltipContent> </TooltipContent>
@ -157,15 +159,23 @@ const ConversationRow: React.FC<ConversationRowProps> = ({ conv, isActive, onSel
</TooltipProvider> </TooltipProvider>
<AlertDialog> <AlertDialog>
<TooltipProvider delayDuration={300}> <TooltipProvider delay={300}>
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger
<AlertDialogTrigger asChild> render={
<Button variant="ghost" size="icon-xs" className="text-muted-foreground hover:text-destructive"> <AlertDialogTrigger
<Trash2 className="h-3 w-3" /> render={
</Button> <Button
</AlertDialogTrigger> variant="ghost"
</TooltipTrigger> size="icon-xs"
className="text-muted-foreground hover:text-destructive"
>
<Trash2 className="h-3 w-3" />
</Button>
}
/>
}
/>
<TooltipContent side="bottom"> <TooltipContent side="bottom">
<p>Delete</p> <p>Delete</p>
</TooltipContent> </TooltipContent>

View file

@ -184,21 +184,23 @@ const MCPCredentialsTab: React.FC<Props> = ({ accessToken }) => {
</TableCell> </TableCell>
<TableCell className="text-right"> <TableCell className="text-right">
<AlertDialog> <AlertDialog>
<AlertDialogTrigger asChild> <AlertDialogTrigger
<Button render={
variant="outline" <Button
size="icon-sm" variant="outline"
disabled={isRevoking} size="icon-sm"
title="Revoke connection" disabled={isRevoking}
className="text-muted-foreground hover:text-destructive hover:border-destructive/50" title="Revoke connection"
> className="text-muted-foreground hover:text-destructive hover:border-destructive/50"
{isRevoking ? ( >
<Loader2 className="h-3.5 w-3.5 animate-spin" /> {isRevoking ? (
) : ( <Loader2 className="h-3.5 w-3.5 animate-spin" />
<Trash2 className="h-3.5 w-3.5" /> ) : (
)} <Trash2 className="h-3.5 w-3.5" />
</Button> )}
</AlertDialogTrigger> </Button>
}
/>
<AlertDialogContent> <AlertDialogContent>
<AlertDialogHeader> <AlertDialogHeader>
<AlertDialogTitle>Revoke connection?</AlertDialogTitle> <AlertDialogTitle>Revoke connection?</AlertDialogTitle>

View file

@ -0,0 +1,50 @@
import { render, screen, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { beforeAll, describe, expect, it, vi } from "vitest";
import UsageDatePicker from "./usage_date_picker";
beforeAll(() => {
vi.stubGlobal("requestIdleCallback", (cb: IdleRequestCallback) => {
cb({ didTimeout: false, timeRemaining: () => 50 } as IdleDeadline);
return 0;
});
});
describe("UsageDatePicker (tremor DateRangePicker on date-fns 4)", () => {
const value = { from: new Date(2026, 5, 1), to: new Date(2026, 5, 15) };
it("renders the formatted range label", () => {
render(<UsageDatePicker value={value} onValueChange={() => {}} />);
const triggerText = screen.getAllByRole("button")[0].textContent ?? "";
expect(triggerText).toMatch(/Jun/);
expect(triggerText).toMatch(/2026/);
expect(triggerText).toMatch(/15/);
});
it("opens the calendar and renders a full month grid", async () => {
const user = userEvent.setup();
render(<UsageDatePicker value={value} onValueChange={() => {}} />);
await user.click(screen.getAllByRole("button")[0]);
const grid = await screen.findByRole("grid");
const dayCells = within(grid).getAllByRole("gridcell");
expect(dayCells.length).toBeGreaterThanOrEqual(28);
expect(within(grid).getByText("15")).toBeInTheDocument();
});
it("fires onValueChange when a day is selected", async () => {
const user = userEvent.setup();
const onValueChange = vi.fn();
render(<UsageDatePicker value={value} onValueChange={onValueChange} />);
await user.click(screen.getAllByRole("button")[0]);
const grid = await screen.findByRole("grid");
await user.click(within(grid).getByText("10"));
expect(onValueChange).toHaveBeenCalled();
const newValue = onValueChange.mock.calls[0][0];
expect(newValue.from).toBeInstanceOf(Date);
});
});

View file

@ -0,0 +1,61 @@
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it, vi } from "vitest";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "./alert-dialog";
import { Button } from "./button";
function ConfirmDialog({ onConfirm }: { onConfirm: () => void }) {
return (
<AlertDialog>
<AlertDialogTrigger render={<Button>Open</Button>} />
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete this?</AlertDialogTitle>
<AlertDialogDescription>Cannot be undone</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={onConfirm}>Confirm</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
}
describe("AlertDialog", () => {
it("fires the action handler and closes the dialog on confirm", async () => {
const user = userEvent.setup();
const onConfirm = vi.fn();
render(<ConfirmDialog onConfirm={onConfirm} />);
await user.click(screen.getByRole("button", { name: "Open" }));
expect(screen.getByText("Delete this?")).toBeInTheDocument();
await user.click(screen.getByRole("button", { name: "Confirm" }));
expect(onConfirm).toHaveBeenCalledOnce();
expect(screen.queryByText("Delete this?")).not.toBeInTheDocument();
});
it("closes without firing the action on cancel", async () => {
const user = userEvent.setup();
const onConfirm = vi.fn();
render(<ConfirmDialog onConfirm={onConfirm} />);
await user.click(screen.getByRole("button", { name: "Open" }));
await user.click(screen.getByRole("button", { name: "Cancel" }));
expect(onConfirm).not.toHaveBeenCalled();
expect(screen.queryByText("Delete this?")).not.toBeInTheDocument();
});
});

View file

@ -1,29 +1,29 @@
"use client"; "use client";
import * as React from "react"; import * as React from "react";
import { AlertDialog as AlertDialogPrimitive } from "radix-ui"; import { AlertDialog as AlertDialogPrimitive } from "@base-ui/react/alert-dialog";
import { cn } from "@/lib/cva.config"; import { cn } from "@/lib/cva.config";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
function AlertDialog({ ...props }: React.ComponentProps<typeof AlertDialogPrimitive.Root>) { function AlertDialog({ ...props }: AlertDialogPrimitive.Root.Props) {
return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />; return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />;
} }
function AlertDialogTrigger({ ...props }: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) { function AlertDialogTrigger({ ...props }: AlertDialogPrimitive.Trigger.Props) {
return <AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />; return <AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />;
} }
function AlertDialogPortal({ ...props }: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) { function AlertDialogPortal({ ...props }: AlertDialogPrimitive.Portal.Props) {
return <AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />; return <AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />;
} }
function AlertDialogOverlay({ className, ...props }: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) { function AlertDialogOverlay({ className, ...props }: AlertDialogPrimitive.Backdrop.Props) {
return ( return (
<AlertDialogPrimitive.Overlay <AlertDialogPrimitive.Backdrop
data-slot="alert-dialog-overlay" data-slot="alert-dialog-overlay"
className={cn( className={cn(
"fixed inset-0 z-50 bg-black/50 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0", "fixed inset-0 isolate z-50 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0",
className, className,
)} )}
{...props} {...props}
@ -35,17 +35,17 @@ function AlertDialogContent({
className, className,
size = "default", size = "default",
...props ...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Content> & { }: AlertDialogPrimitive.Popup.Props & {
size?: "default" | "sm"; size?: "default" | "sm";
}) { }) {
return ( return (
<AlertDialogPortal> <AlertDialogPortal>
<AlertDialogOverlay /> <AlertDialogOverlay />
<AlertDialogPrimitive.Content <AlertDialogPrimitive.Popup
data-slot="alert-dialog-content" data-slot="alert-dialog-content"
data-size={size} data-size={size}
className={cn( className={cn(
"group/alert-dialog-content fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border bg-background p-6 shadow-lg duration-200 data-[size=sm]:max-w-xs data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 data-[size=default]:sm:max-w-lg", "group/alert-dialog-content fixed top-1/2 left-1/2 z-50 grid w-full -translate-x-1/2 -translate-y-1/2 gap-6 rounded-xl bg-popover p-6 text-popover-foreground ring-1 ring-foreground/10 duration-100 outline-none data-[size=default]:max-w-xs data-[size=sm]:max-w-xs data-[size=default]:sm:max-w-lg data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
className, className,
)} )}
{...props} {...props}
@ -80,12 +80,25 @@ function AlertDialogFooter({ className, ...props }: React.ComponentProps<"div">)
); );
} }
function AlertDialogMedia({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-dialog-media"
className={cn(
"mb-2 inline-flex size-16 items-center justify-center rounded-md bg-muted sm:group-data-[size=default]/alert-dialog-content:row-span-2 *:[svg:not([class*='size-'])]:size-8",
className,
)}
{...props}
/>
);
}
function AlertDialogTitle({ className, ...props }: React.ComponentProps<typeof AlertDialogPrimitive.Title>) { function AlertDialogTitle({ className, ...props }: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
return ( return (
<AlertDialogPrimitive.Title <AlertDialogPrimitive.Title
data-slot="alert-dialog-title" data-slot="alert-dialog-title"
className={cn( className={cn(
"text-lg font-semibold sm:group-data-[size=default]/alert-dialog-content:group-has-data-[slot=alert-dialog-media]/alert-dialog-content:col-start-2", "text-lg font-medium sm:group-data-[size=default]/alert-dialog-content:group-has-data-[slot=alert-dialog-media]/alert-dialog-content:col-start-2",
className, className,
)} )}
{...props} {...props}
@ -100,18 +113,8 @@ function AlertDialogDescription({
return ( return (
<AlertDialogPrimitive.Description <AlertDialogPrimitive.Description
data-slot="alert-dialog-description" data-slot="alert-dialog-description"
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
);
}
function AlertDialogMedia({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-dialog-media"
className={cn( className={cn(
"mb-2 inline-flex size-16 items-center justify-center rounded-md bg-muted sm:group-data-[size=default]/alert-dialog-content:row-span-2 *:[svg:not([class*='size-'])]:size-8", "text-sm text-balance text-muted-foreground md:text-pretty *:[a]:underline *:[a]:underline-offset-3 *:[a]:hover:text-foreground",
className, className,
)} )}
{...props} {...props}
@ -124,12 +127,14 @@ function AlertDialogAction({
variant = "default", variant = "default",
size = "default", size = "default",
...props ...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Action> & }: AlertDialogPrimitive.Close.Props & Pick<React.ComponentProps<typeof Button>, "variant" | "size">) {
Pick<React.ComponentProps<typeof Button>, "variant" | "size">) {
return ( return (
<Button variant={variant} size={size} asChild> <AlertDialogPrimitive.Close
<AlertDialogPrimitive.Action data-slot="alert-dialog-action" className={cn(className)} {...props} /> data-slot="alert-dialog-action"
</Button> className={cn(className)}
render={<Button variant={variant} size={size} />}
{...props}
/>
); );
} }
@ -138,12 +143,14 @@ function AlertDialogCancel({
variant = "outline", variant = "outline",
size = "default", size = "default",
...props ...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Cancel> & }: AlertDialogPrimitive.Close.Props & Pick<React.ComponentProps<typeof Button>, "variant" | "size">) {
Pick<React.ComponentProps<typeof Button>, "variant" | "size">) {
return ( return (
<Button variant={variant} size={size} asChild> <AlertDialogPrimitive.Close
<AlertDialogPrimitive.Cancel data-slot="alert-dialog-cancel" className={cn(className)} {...props} /> data-slot="alert-dialog-cancel"
</Button> className={cn(className)}
render={<Button variant={variant} size={size} />}
{...props}
/>
); );
} }

View file

@ -9,7 +9,6 @@ describe("Button", () => {
const button = screen.getByRole("button", { name: "Save" }); const button = screen.getByRole("button", { name: "Save" });
expect(button).toHaveClass("bg-primary"); expect(button).toHaveClass("bg-primary");
expect(button).toHaveAttribute("data-slot", "button"); expect(button).toHaveAttribute("data-slot", "button");
expect(button).toHaveAttribute("data-variant", "default");
}); });
it("applies variant and size props", () => { it("applies variant and size props", () => {
@ -19,9 +18,8 @@ describe("Button", () => {
</Button>, </Button>,
); );
const button = screen.getByRole("button", { name: "Delete" }); const button = screen.getByRole("button", { name: "Delete" });
expect(button).toHaveClass("bg-destructive"); expect(button).toHaveClass("bg-destructive/10");
expect(button).toHaveClass("h-8"); expect(button).toHaveClass("h-8");
expect(button).toHaveAttribute("data-variant", "destructive");
}); });
it("resolves conflicting classes through twMerge so className wins", () => { it("resolves conflicting classes through twMerge so className wins", () => {
@ -31,15 +29,12 @@ describe("Button", () => {
expect(button).not.toHaveClass("bg-primary"); expect(button).not.toHaveClass("bg-primary");
}); });
it("renders the child element when asChild is set", () => { it("renders the element passed via the render prop with button semantics", () => {
render( render(<Button nativeButton={false} render={<a href="https://docs.litellm.ai">Docs</a>} />);
<Button asChild> const button = screen.getByRole("button", { name: "Docs" });
<a href="https://docs.litellm.ai">Docs</a> expect(button.tagName).toBe("A");
</Button>, expect(button).toHaveAttribute("href", "https://docs.litellm.ai");
); expect(button).toHaveClass("bg-primary");
const link = screen.getByRole("link", { name: "Docs" });
expect(link).toHaveClass("bg-primary");
expect(screen.queryByRole("button")).not.toBeInTheDocument();
}); });
it("forwards refs to the underlying button element", () => { it("forwards refs to the underlying button element", () => {

View file

@ -1,30 +1,34 @@
import * as React from "react"; import * as React from "react";
import { Button as ButtonPrimitive } from "@base-ui/react/button";
import { type VariantProps } from "cva"; import { type VariantProps } from "cva";
import { Slot } from "radix-ui";
import { cn, cva } from "@/lib/cva.config"; import { cn, cva } from "@/lib/cva.config";
const buttonVariants = cva({ const buttonVariants = cva({
base: "inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", 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: { variants: {
variant: { variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90", default: "bg-primary text-primary-foreground hover:bg-primary/80",
destructive:
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40",
outline: outline:
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50", "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-secondary/80", secondary:
ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", "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", link: "text-primary underline-offset-4 hover:underline",
}, },
size: { size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3", default:
xs: "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3", "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",
sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5", 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",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4", 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: "size-9",
"icon-xs": "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3", "icon-xs":
"icon-sm": "size-8", "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", "icon-lg": "size-10",
}, },
}, },
@ -34,21 +38,14 @@ const buttonVariants = cva({
}, },
}); });
type ButtonProps = React.ComponentPropsWithoutRef<"button"> & type ButtonProps = ButtonPrimitive.Props & VariantProps<typeof buttonVariants>;
VariantProps<typeof buttonVariants> & {
asChild?: boolean;
};
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>( const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant = "default", size = "default", asChild = false, ...props }, ref) => { ({ className, variant = "default", size = "default", ...props }, ref) => {
const Comp = asChild ? Slot.Root : "button";
return ( return (
<Comp <ButtonPrimitive
ref={ref} ref={ref}
data-slot="button" data-slot="button"
data-variant={variant}
data-size={size}
className={cn(buttonVariants({ variant, size, className }))} className={cn(buttonVariants({ variant, size, className }))}
{...props} {...props}
/> />

View file

@ -1,17 +1,17 @@
"use client"; "use client";
import { Collapsible as CollapsiblePrimitive } from "radix-ui"; import { Collapsible as CollapsiblePrimitive } from "@base-ui/react/collapsible";
function Collapsible({ ...props }: React.ComponentProps<typeof CollapsiblePrimitive.Root>) { function Collapsible({ ...props }: CollapsiblePrimitive.Root.Props) {
return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />; return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />;
} }
function CollapsibleTrigger({ ...props }: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) { function CollapsibleTrigger({ ...props }: CollapsiblePrimitive.Trigger.Props) {
return <CollapsiblePrimitive.CollapsibleTrigger data-slot="collapsible-trigger" {...props} />; return <CollapsiblePrimitive.Trigger data-slot="collapsible-trigger" {...props} />;
} }
function CollapsibleContent({ ...props }: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) { function CollapsibleContent({ ...props }: CollapsiblePrimitive.Panel.Props) {
return <CollapsiblePrimitive.CollapsibleContent data-slot="collapsible-content" {...props} />; return <CollapsiblePrimitive.Panel data-slot="collapsible-content" {...props} />;
} }
export { Collapsible, CollapsibleTrigger, CollapsibleContent }; export { Collapsible, CollapsibleTrigger, CollapsibleContent };

View file

@ -1,34 +1,34 @@
"use client"; "use client";
import * as React from "react"; import * as React from "react";
import { XIcon } from "lucide-react"; import { Dialog as DialogPrimitive } from "@base-ui/react/dialog";
import { Dialog as DialogPrimitive } from "radix-ui";
import { cn } from "@/lib/cva.config"; import { cn } from "@/lib/cva.config";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { XIcon } from "lucide-react";
function Dialog({ ...props }: React.ComponentProps<typeof DialogPrimitive.Root>) { function Dialog({ ...props }: DialogPrimitive.Root.Props) {
return <DialogPrimitive.Root data-slot="dialog" {...props} />; return <DialogPrimitive.Root data-slot="dialog" {...props} />;
} }
function DialogTrigger({ ...props }: React.ComponentProps<typeof DialogPrimitive.Trigger>) { function DialogTrigger({ ...props }: DialogPrimitive.Trigger.Props) {
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />; return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />;
} }
function DialogPortal({ ...props }: React.ComponentProps<typeof DialogPrimitive.Portal>) { function DialogPortal({ ...props }: DialogPrimitive.Portal.Props) {
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />; return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
} }
function DialogClose({ ...props }: React.ComponentProps<typeof DialogPrimitive.Close>) { function DialogClose({ ...props }: DialogPrimitive.Close.Props) {
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />; return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
} }
function DialogOverlay({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Overlay>) { function DialogOverlay({ className, ...props }: DialogPrimitive.Backdrop.Props) {
return ( return (
<DialogPrimitive.Overlay <DialogPrimitive.Backdrop
data-slot="dialog-overlay" data-slot="dialog-overlay"
className={cn( className={cn(
"fixed inset-0 z-50 bg-black/50 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0", "fixed inset-0 isolate z-50 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0",
className, className,
)} )}
{...props} {...props}
@ -41,16 +41,16 @@ function DialogContent({
children, children,
showCloseButton = true, showCloseButton = true,
...props ...props
}: React.ComponentProps<typeof DialogPrimitive.Content> & { }: DialogPrimitive.Popup.Props & {
showCloseButton?: boolean; showCloseButton?: boolean;
}) { }) {
return ( return (
<DialogPortal data-slot="dialog-portal"> <DialogPortal>
<DialogOverlay /> <DialogOverlay />
<DialogPrimitive.Content <DialogPrimitive.Popup
data-slot="dialog-content" data-slot="dialog-content"
className={cn( className={cn(
"fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border bg-background p-6 shadow-lg duration-200 outline-none data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 sm:max-w-lg", "fixed top-1/2 left-1/2 z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-6 rounded-xl bg-popover p-6 text-sm text-popover-foreground ring-1 ring-foreground/10 duration-100 outline-none sm:max-w-md data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
className, className,
)} )}
{...props} {...props}
@ -59,25 +59,19 @@ function DialogContent({
{showCloseButton && ( {showCloseButton && (
<DialogPrimitive.Close <DialogPrimitive.Close
data-slot="dialog-close" data-slot="dialog-close"
className="absolute top-4 right-4 rounded-xs opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:ring-2 focus:ring-ring focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4" render={<Button variant="ghost" className="absolute top-4 right-4" size="icon-sm" />}
> >
<XIcon /> <XIcon />
<span className="sr-only">Close</span> <span className="sr-only">Close</span>
</DialogPrimitive.Close> </DialogPrimitive.Close>
)} )}
</DialogPrimitive.Content> </DialogPrimitive.Popup>
</DialogPortal> </DialogPortal>
); );
} }
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
return ( return <div data-slot="dialog-header" className={cn("flex flex-col gap-2", className)} {...props} />;
<div
data-slot="dialog-header"
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
{...props}
/>
);
} }
function DialogFooter({ function DialogFooter({
@ -95,30 +89,25 @@ function DialogFooter({
{...props} {...props}
> >
{children} {children}
{showCloseButton && ( {showCloseButton && <DialogPrimitive.Close render={<Button variant="outline" />}>Close</DialogPrimitive.Close>}
<DialogPrimitive.Close asChild>
<Button variant="outline">Close</Button>
</DialogPrimitive.Close>
)}
</div> </div>
); );
} }
function DialogTitle({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Title>) { function DialogTitle({ className, ...props }: DialogPrimitive.Title.Props) {
return ( return (
<DialogPrimitive.Title <DialogPrimitive.Title data-slot="dialog-title" className={cn("leading-none font-medium", className)} {...props} />
data-slot="dialog-title"
className={cn("text-lg leading-none font-semibold", className)}
{...props}
/>
); );
} }
function DialogDescription({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Description>) { function DialogDescription({ className, ...props }: DialogPrimitive.Description.Props) {
return ( return (
<DialogPrimitive.Description <DialogPrimitive.Description
data-slot="dialog-description" data-slot="dialog-description"
className={cn("text-sm text-muted-foreground", className)} className={cn(
"text-sm text-muted-foreground *:[a]:underline *:[a]:underline-offset-3 *:[a]:hover:text-foreground",
className,
)}
{...props} {...props}
/> />
); );

View file

@ -1,13 +1,12 @@
"use client"; "use client";
import * as React from "react"; import * as React from "react";
import { Label as LabelPrimitive } from "radix-ui";
import { cn } from "@/lib/cva.config"; import { cn } from "@/lib/cva.config";
function Label({ className, ...props }: React.ComponentProps<typeof LabelPrimitive.Root>) { function Label({ className, ...props }: React.ComponentProps<"label">) {
return ( return (
<LabelPrimitive.Root <label
data-slot="label" data-slot="label"
className={cn( className={cn(
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50", "flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",

View file

@ -1,54 +1,65 @@
"use client"; "use client";
import * as React from "react"; import * as React from "react";
import { Popover as PopoverPrimitive } from "radix-ui"; import { Popover as PopoverPrimitive } from "@base-ui/react/popover";
import { cn } from "@/lib/cva.config"; import { cn } from "@/lib/cva.config";
function Popover({ ...props }: React.ComponentProps<typeof PopoverPrimitive.Root>) { function Popover({ ...props }: PopoverPrimitive.Root.Props) {
return <PopoverPrimitive.Root data-slot="popover" {...props} />; return <PopoverPrimitive.Root data-slot="popover" {...props} />;
} }
function PopoverTrigger({ ...props }: React.ComponentProps<typeof PopoverPrimitive.Trigger>) { function PopoverTrigger({ ...props }: PopoverPrimitive.Trigger.Props) {
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />; return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />;
} }
function PopoverContent({ function PopoverContent({
className, className,
align = "center", align = "center",
alignOffset = 0,
side = "bottom",
sideOffset = 4, sideOffset = 4,
...props ...props
}: React.ComponentProps<typeof PopoverPrimitive.Content>) { }: PopoverPrimitive.Popup.Props &
Pick<PopoverPrimitive.Positioner.Props, "align" | "alignOffset" | "side" | "sideOffset">) {
return ( return (
<PopoverPrimitive.Portal> <PopoverPrimitive.Portal>
<PopoverPrimitive.Content <PopoverPrimitive.Positioner
data-slot="popover-content"
align={align} align={align}
alignOffset={alignOffset}
side={side}
sideOffset={sideOffset} sideOffset={sideOffset}
className={cn( className="isolate z-50"
"z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-hidden data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95", >
className, <PopoverPrimitive.Popup
)} data-slot="popover-content"
{...props} className={cn(
/> "z-50 flex w-72 origin-(--transform-origin) flex-col gap-4 rounded-md bg-popover p-4 text-sm text-popover-foreground shadow-md ring-1 ring-foreground/10 outline-hidden duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
className,
)}
{...props}
/>
</PopoverPrimitive.Positioner>
</PopoverPrimitive.Portal> </PopoverPrimitive.Portal>
); );
} }
function PopoverAnchor({ ...props }: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />;
}
function PopoverHeader({ className, ...props }: React.ComponentProps<"div">) { function PopoverHeader({ className, ...props }: React.ComponentProps<"div">) {
return <div data-slot="popover-header" className={cn("flex flex-col gap-1 text-sm", className)} {...props} />; return <div data-slot="popover-header" className={cn("flex flex-col gap-1 text-sm", className)} {...props} />;
} }
function PopoverTitle({ className, ...props }: React.ComponentProps<"h2">) { function PopoverTitle({ className, ...props }: PopoverPrimitive.Title.Props) {
return <div data-slot="popover-title" className={cn("font-medium", className)} {...props} />; return <PopoverPrimitive.Title data-slot="popover-title" className={cn("font-medium", className)} {...props} />;
} }
function PopoverDescription({ className, ...props }: React.ComponentProps<"p">) { function PopoverDescription({ className, ...props }: PopoverPrimitive.Description.Props) {
return <p data-slot="popover-description" className={cn("text-muted-foreground", className)} {...props} />; return (
<PopoverPrimitive.Description
data-slot="popover-description"
className={cn("text-muted-foreground", className)}
{...props}
/>
);
} }
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor, PopoverHeader, PopoverTitle, PopoverDescription }; export { Popover, PopoverContent, PopoverDescription, PopoverHeader, PopoverTitle, PopoverTrigger };

View file

@ -1,11 +1,11 @@
"use client"; "use client";
import * as React from "react"; import * as React from "react";
import { ScrollArea as ScrollAreaPrimitive } from "radix-ui"; import { ScrollArea as ScrollAreaPrimitive } from "@base-ui/react/scroll-area";
import { cn } from "@/lib/cva.config"; import { cn } from "@/lib/cva.config";
function ScrollArea({ className, children, ...props }: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) { function ScrollArea({ className, children, ...props }: ScrollAreaPrimitive.Root.Props) {
return ( return (
<ScrollAreaPrimitive.Root data-slot="scroll-area" className={cn("relative", className)} {...props}> <ScrollAreaPrimitive.Root data-slot="scroll-area" className={cn("relative", className)} {...props}>
<ScrollAreaPrimitive.Viewport <ScrollAreaPrimitive.Viewport
@ -20,28 +20,20 @@ function ScrollArea({ className, children, ...props }: React.ComponentProps<type
); );
} }
function ScrollBar({ function ScrollBar({ className, orientation = "vertical", ...props }: ScrollAreaPrimitive.Scrollbar.Props) {
className,
orientation = "vertical",
...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
return ( return (
<ScrollAreaPrimitive.ScrollAreaScrollbar <ScrollAreaPrimitive.Scrollbar
data-slot="scroll-area-scrollbar" data-slot="scroll-area-scrollbar"
data-orientation={orientation}
orientation={orientation} orientation={orientation}
className={cn( className={cn(
"flex touch-none p-px transition-colors select-none", "flex touch-none p-px transition-colors select-none data-horizontal:h-2.5 data-horizontal:flex-col data-horizontal:border-t data-horizontal:border-t-transparent data-vertical:h-full data-vertical:w-2.5 data-vertical:border-l data-vertical:border-l-transparent",
orientation === "vertical" && "h-full w-2.5 border-l border-l-transparent",
orientation === "horizontal" && "h-2.5 flex-col border-t border-t-transparent",
className, className,
)} )}
{...props} {...props}
> >
<ScrollAreaPrimitive.ScrollAreaThumb <ScrollAreaPrimitive.Thumb data-slot="scroll-area-thumb" className="relative flex-1 rounded-full bg-border" />
data-slot="scroll-area-thumb" </ScrollAreaPrimitive.Scrollbar>
className="relative flex-1 rounded-full bg-border"
/>
</ScrollAreaPrimitive.ScrollAreaScrollbar>
); );
} }

View file

@ -1,21 +1,21 @@
"use client"; "use client";
import * as React from "react"; import * as React from "react";
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"; import { Select as SelectPrimitive } from "@base-ui/react/select";
import { Select as SelectPrimitive } from "radix-ui";
import { cn } from "@/lib/cva.config"; import { cn } from "@/lib/cva.config";
import { ChevronDownIcon, CheckIcon, ChevronUpIcon } from "lucide-react";
function Select({ ...props }: React.ComponentProps<typeof SelectPrimitive.Root>) { const Select = SelectPrimitive.Root;
return <SelectPrimitive.Root data-slot="select" {...props} />;
function SelectGroup({ className, ...props }: SelectPrimitive.Group.Props) {
return <SelectPrimitive.Group data-slot="select-group" className={cn("scroll-my-1 p-1", className)} {...props} />;
} }
function SelectGroup({ ...props }: React.ComponentProps<typeof SelectPrimitive.Group>) { function SelectValue({ className, ...props }: SelectPrimitive.Value.Props) {
return <SelectPrimitive.Group data-slot="select-group" {...props} />; return (
} <SelectPrimitive.Value data-slot="select-value" className={cn("flex flex-1 text-left", className)} {...props} />
);
function SelectValue({ ...props }: React.ComponentProps<typeof SelectPrimitive.Value>) {
return <SelectPrimitive.Value data-slot="select-value" {...props} />;
} }
function SelectTrigger({ function SelectTrigger({
@ -23,7 +23,7 @@ function SelectTrigger({
size = "default", size = "default",
children, children,
...props ...props
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & { }: SelectPrimitive.Trigger.Props & {
size?: "sm" | "default"; size?: "sm" | "default";
}) { }) {
return ( return (
@ -31,15 +31,13 @@ function SelectTrigger({
data-slot="select-trigger" data-slot="select-trigger"
data-size={size} data-size={size}
className={cn( className={cn(
"flex w-fit items-center justify-between gap-2 rounded-md border border-input bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[placeholder]:text-muted-foreground data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground", "flex w-fit items-center justify-between gap-1.5 rounded-md border border-input bg-transparent py-2 pr-2 pl-2.5 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-placeholder:text-muted-foreground data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-1.5 dark:bg-input/30 dark:hover:bg-input/50 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",
className, className,
)} )}
{...props} {...props}
> >
{children} {children}
<SelectPrimitive.Icon asChild> <SelectPrimitive.Icon render={<ChevronDownIcon className="pointer-events-none size-4 text-muted-foreground" />} />
<ChevronDownIcon className="size-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger> </SelectPrimitive.Trigger>
); );
} }
@ -47,43 +45,45 @@ function SelectTrigger({
function SelectContent({ function SelectContent({
className, className,
children, children,
position = "item-aligned", side = "bottom",
sideOffset = 4,
align = "center", align = "center",
alignOffset = 0,
alignItemWithTrigger = true,
...props ...props
}: React.ComponentProps<typeof SelectPrimitive.Content>) { }: SelectPrimitive.Popup.Props &
Pick<SelectPrimitive.Positioner.Props, "align" | "alignOffset" | "side" | "sideOffset" | "alignItemWithTrigger">) {
return ( return (
<SelectPrimitive.Portal> <SelectPrimitive.Portal>
<SelectPrimitive.Content <SelectPrimitive.Positioner
data-slot="select-content" side={side}
className={cn( sideOffset={sideOffset}
"relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border bg-popover text-popover-foreground shadow-md data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className,
)}
position={position}
align={align} align={align}
{...props} alignOffset={alignOffset}
alignItemWithTrigger={alignItemWithTrigger}
className="isolate z-50"
> >
<SelectScrollUpButton /> <SelectPrimitive.Popup
<SelectPrimitive.Viewport data-slot="select-content"
data-align-trigger={alignItemWithTrigger}
className={cn( className={cn(
"p-1", "relative isolate z-50 max-h-(--available-height) w-(--anchor-width) min-w-36 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-md bg-popover text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[align-trigger=true]:animate-none data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
position === "popper" && className,
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1",
)} )}
{...props}
> >
{children} <SelectScrollUpButton />
</SelectPrimitive.Viewport> <SelectPrimitive.List>{children}</SelectPrimitive.List>
<SelectScrollDownButton /> <SelectScrollDownButton />
</SelectPrimitive.Content> </SelectPrimitive.Popup>
</SelectPrimitive.Positioner>
</SelectPrimitive.Portal> </SelectPrimitive.Portal>
); );
} }
function SelectLabel({ className, ...props }: React.ComponentProps<typeof SelectPrimitive.Label>) { function SelectLabel({ className, ...props }: SelectPrimitive.GroupLabel.Props) {
return ( return (
<SelectPrimitive.Label <SelectPrimitive.GroupLabel
data-slot="select-label" data-slot="select-label"
className={cn("px-2 py-1.5 text-xs text-muted-foreground", className)} className={cn("px-2 py-1.5 text-xs text-muted-foreground", className)}
{...props} {...props}
@ -91,27 +91,29 @@ function SelectLabel({ className, ...props }: React.ComponentProps<typeof Select
); );
} }
function SelectItem({ className, children, ...props }: React.ComponentProps<typeof SelectPrimitive.Item>) { function SelectItem({ className, children, ...props }: SelectPrimitive.Item.Props) {
return ( return (
<SelectPrimitive.Item <SelectPrimitive.Item
data-slot="select-item" data-slot="select-item"
className={cn( className={cn(
"relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2", "relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
className, className,
)} )}
{...props} {...props}
> >
<span data-slot="select-item-indicator" className="absolute right-2 flex size-3.5 items-center justify-center"> <SelectPrimitive.ItemText className="flex flex-1 shrink-0 gap-2 whitespace-nowrap">
<SelectPrimitive.ItemIndicator> {children}
<CheckIcon className="size-4" /> </SelectPrimitive.ItemText>
</SelectPrimitive.ItemIndicator> <SelectPrimitive.ItemIndicator
</span> render={<span className="pointer-events-none absolute right-2 flex size-4 items-center justify-center" />}
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText> >
<CheckIcon className="pointer-events-none" />
</SelectPrimitive.ItemIndicator>
</SelectPrimitive.Item> </SelectPrimitive.Item>
); );
} }
function SelectSeparator({ className, ...props }: React.ComponentProps<typeof SelectPrimitive.Separator>) { function SelectSeparator({ className, ...props }: SelectPrimitive.Separator.Props) {
return ( return (
<SelectPrimitive.Separator <SelectPrimitive.Separator
data-slot="select-separator" data-slot="select-separator"
@ -121,30 +123,33 @@ function SelectSeparator({ className, ...props }: React.ComponentProps<typeof Se
); );
} }
function SelectScrollUpButton({ className, ...props }: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) { function SelectScrollUpButton({ className, ...props }: React.ComponentProps<typeof SelectPrimitive.ScrollUpArrow>) {
return ( return (
<SelectPrimitive.ScrollUpButton <SelectPrimitive.ScrollUpArrow
data-slot="select-scroll-up-button" data-slot="select-scroll-up-button"
className={cn("flex cursor-default items-center justify-center py-1", className)} className={cn(
"top-0 z-10 flex w-full cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props} {...props}
> >
<ChevronUpIcon className="size-4" /> <ChevronUpIcon />
</SelectPrimitive.ScrollUpButton> </SelectPrimitive.ScrollUpArrow>
); );
} }
function SelectScrollDownButton({ function SelectScrollDownButton({ className, ...props }: React.ComponentProps<typeof SelectPrimitive.ScrollDownArrow>) {
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
return ( return (
<SelectPrimitive.ScrollDownButton <SelectPrimitive.ScrollDownArrow
data-slot="select-scroll-down-button" data-slot="select-scroll-down-button"
className={cn("flex cursor-default items-center justify-center py-1", className)} className={cn(
"bottom-0 z-10 flex w-full cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props} {...props}
> >
<ChevronDownIcon className="size-4" /> <ChevronDownIcon />
</SelectPrimitive.ScrollDownButton> </SelectPrimitive.ScrollDownArrow>
); );
} }

View file

@ -1,23 +1,16 @@
"use client"; "use client";
import * as React from "react"; import { Separator as SeparatorPrimitive } from "@base-ui/react/separator";
import { Separator as SeparatorPrimitive } from "radix-ui";
import { cn } from "@/lib/cva.config"; import { cn } from "@/lib/cva.config";
function Separator({ function Separator({ className, orientation = "horizontal", ...props }: SeparatorPrimitive.Props) {
className,
orientation = "horizontal",
decorative = true,
...props
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
return ( return (
<SeparatorPrimitive.Root <SeparatorPrimitive
data-slot="separator" data-slot="separator"
decorative={decorative}
orientation={orientation} orientation={orientation}
className={cn( className={cn(
"shrink-0 bg-border data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px", "shrink-0 bg-border data-horizontal:h-px data-horizontal:w-full data-vertical:w-px data-vertical:self-stretch",
className, className,
)} )}
{...props} {...props}

View file

@ -1,7 +1,6 @@
"use client"; "use client";
import * as React from "react"; import { Switch as SwitchPrimitive } from "@base-ui/react/switch";
import { Switch as SwitchPrimitive } from "radix-ui";
import { cn } from "@/lib/cva.config"; import { cn } from "@/lib/cva.config";
@ -9,7 +8,7 @@ function Switch({
className, className,
size = "default", size = "default",
...props ...props
}: React.ComponentProps<typeof SwitchPrimitive.Root> & { }: SwitchPrimitive.Root.Props & {
size?: "sm" | "default"; size?: "sm" | "default";
}) { }) {
return ( return (
@ -17,16 +16,14 @@ function Switch({
data-slot="switch" data-slot="switch"
data-size={size} data-size={size}
className={cn( className={cn(
"peer group/switch inline-flex shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-[1.15rem] data-[size=default]:w-8 data-[size=sm]:h-3.5 data-[size=sm]:w-6 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input dark:data-[state=unchecked]:bg-input/80", "peer group/switch relative inline-flex shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-[size=default]:h-[18.4px] data-[size=default]:w-[32px] data-[size=sm]:h-[14px] data-[size=sm]:w-[24px] dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 data-checked:bg-primary data-unchecked:bg-input dark:data-unchecked:bg-input/80 data-disabled:cursor-not-allowed data-disabled:opacity-50",
className, className,
)} )}
{...props} {...props}
> >
<SwitchPrimitive.Thumb <SwitchPrimitive.Thumb
data-slot="switch-thumb" data-slot="switch-thumb"
className={cn( className="pointer-events-none block rounded-full bg-background ring-0 transition-transform group-data-[size=default]/switch:size-4 group-data-[size=sm]/switch:size-3 group-data-[size=default]/switch:data-checked:translate-x-[calc(100%-2px)] group-data-[size=sm]/switch:data-checked:translate-x-[calc(100%-2px)] dark:data-checked:bg-primary-foreground group-data-[size=default]/switch:data-unchecked:translate-x-0 group-data-[size=sm]/switch:data-unchecked:translate-x-0 dark:data-unchecked:bg-foreground"
"pointer-events-none block rounded-full bg-background ring-0 transition-transform group-data-[size=default]/switch:size-4 group-data-[size=sm]/switch:size-3 data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0 dark:data-[state=checked]:bg-primary-foreground dark:data-[state=unchecked]:bg-foreground",
)}
/> />
</SwitchPrimitive.Root> </SwitchPrimitive.Root>
); );

View file

@ -1,25 +1,23 @@
"use client"; "use client";
import * as React from "react"; import { Tabs as TabsPrimitive } from "@base-ui/react/tabs";
import { type VariantProps } from "cva"; import { type VariantProps } from "cva";
import { Tabs as TabsPrimitive } from "radix-ui";
import { cn, cva } from "@/lib/cva.config"; import { cn, cva } from "@/lib/cva.config";
function Tabs({ className, orientation = "horizontal", ...props }: React.ComponentProps<typeof TabsPrimitive.Root>) { function Tabs({ className, orientation = "horizontal", ...props }: TabsPrimitive.Root.Props) {
return ( return (
<TabsPrimitive.Root <TabsPrimitive.Root
data-slot="tabs" data-slot="tabs"
data-orientation={orientation} data-orientation={orientation}
orientation={orientation} className={cn("group/tabs flex gap-2 data-horizontal:flex-col", className)}
className={cn("group/tabs flex gap-2 data-[orientation=horizontal]:flex-col", className)}
{...props} {...props}
/> />
); );
} }
const tabsListVariants = cva({ const tabsListVariants = cva({
base: "group/tabs-list inline-flex w-fit items-center justify-center rounded-lg p-[3px] text-muted-foreground group-data-[orientation=horizontal]/tabs:h-9 group-data-[orientation=vertical]/tabs:h-fit group-data-[orientation=vertical]/tabs:flex-col data-[variant=line]:rounded-none", 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: { variants: {
variant: { variant: {
default: "bg-muted", default: "bg-muted",
@ -35,7 +33,7 @@ function TabsList({
className, className,
variant = "default", variant = "default",
...props ...props
}: React.ComponentProps<typeof TabsPrimitive.List> & VariantProps<typeof tabsListVariants>) { }: TabsPrimitive.List.Props & VariantProps<typeof tabsListVariants>) {
return ( return (
<TabsPrimitive.List <TabsPrimitive.List
data-slot="tabs-list" data-slot="tabs-list"
@ -46,15 +44,15 @@ function TabsList({
); );
} }
function TabsTrigger({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Trigger>) { function TabsTrigger({ className, ...props }: TabsPrimitive.Tab.Props) {
return ( return (
<TabsPrimitive.Trigger <TabsPrimitive.Tab
data-slot="tabs-trigger" data-slot="tabs-trigger"
className={cn( className={cn(
"relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap text-foreground/60 transition-all group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 group-data-[variant=default]/tabs-list:data-[state=active]:shadow-sm group-data-[variant=line]/tabs-list:data-[state=active]:shadow-none dark:text-muted-foreground dark:hover:text-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", "relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap text-foreground/60 transition-all group-data-vertical/tabs:w-full group-data-vertical/tabs:justify-start hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-disabled:pointer-events-none aria-disabled:opacity-50 dark:text-muted-foreground dark:hover:text-foreground group-data-[variant=default]/tabs-list:data-active:shadow-sm group-data-[variant=line]/tabs-list:data-active:shadow-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:border-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent", "group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-active:bg-transparent dark:group-data-[variant=line]/tabs-list:data-active:border-transparent dark:group-data-[variant=line]/tabs-list:data-active:bg-transparent",
"data-[state=active]:bg-background data-[state=active]:text-foreground dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 dark:data-[state=active]:text-foreground", "data-active:bg-background data-active:text-foreground dark:data-active:border-input dark:data-active:bg-input/30 dark:data-active:text-foreground",
"after:absolute after:bg-foreground after:opacity-0 after:transition-opacity group-data-[orientation=horizontal]/tabs:after:inset-x-0 group-data-[orientation=horizontal]/tabs:after:bottom-[-5px] group-data-[orientation=horizontal]/tabs:after:h-0.5 group-data-[orientation=vertical]/tabs:after:inset-y-0 group-data-[orientation=vertical]/tabs:after:-right-1 group-data-[orientation=vertical]/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-[state=active]:after:opacity-100", "after:absolute after:bg-foreground after:opacity-0 after:transition-opacity group-data-horizontal/tabs:after:inset-x-0 group-data-horizontal/tabs:after:bottom-[-5px] group-data-horizontal/tabs:after:h-0.5 group-data-vertical/tabs:after:inset-y-0 group-data-vertical/tabs:after:-right-1 group-data-vertical/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-active:after:opacity-100",
className, className,
)} )}
{...props} {...props}
@ -62,8 +60,10 @@ function TabsTrigger({ className, ...props }: React.ComponentProps<typeof TabsPr
); );
} }
function TabsContent({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Content>) { function TabsContent({ className, ...props }: TabsPrimitive.Panel.Props) {
return <TabsPrimitive.Content data-slot="tabs-content" className={cn("flex-1 outline-none", className)} {...props} />; return (
<TabsPrimitive.Panel data-slot="tabs-content" className={cn("flex-1 text-sm outline-none", className)} {...props} />
);
} }
export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants }; export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants };

View file

@ -1,42 +1,52 @@
"use client"; "use client";
import * as React from "react"; import { Tooltip as TooltipPrimitive } from "@base-ui/react/tooltip";
import { Tooltip as TooltipPrimitive } from "radix-ui";
import { cn } from "@/lib/cva.config"; import { cn } from "@/lib/cva.config";
function TooltipProvider({ delayDuration = 0, ...props }: React.ComponentProps<typeof TooltipPrimitive.Provider>) { function TooltipProvider({ delay = 0, ...props }: TooltipPrimitive.Provider.Props) {
return <TooltipPrimitive.Provider data-slot="tooltip-provider" delayDuration={delayDuration} {...props} />; return <TooltipPrimitive.Provider data-slot="tooltip-provider" delay={delay} {...props} />;
} }
function Tooltip({ ...props }: React.ComponentProps<typeof TooltipPrimitive.Root>) { function Tooltip({ ...props }: TooltipPrimitive.Root.Props) {
return <TooltipPrimitive.Root data-slot="tooltip" {...props} />; return <TooltipPrimitive.Root data-slot="tooltip" {...props} />;
} }
function TooltipTrigger({ ...props }: React.ComponentProps<typeof TooltipPrimitive.Trigger>) { function TooltipTrigger({ ...props }: TooltipPrimitive.Trigger.Props) {
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />; return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />;
} }
function TooltipContent({ function TooltipContent({
className, className,
sideOffset = 0, side = "top",
sideOffset = 4,
align = "center",
alignOffset = 0,
children, children,
...props ...props
}: React.ComponentProps<typeof TooltipPrimitive.Content>) { }: TooltipPrimitive.Popup.Props &
Pick<TooltipPrimitive.Positioner.Props, "align" | "alignOffset" | "side" | "sideOffset">) {
return ( return (
<TooltipPrimitive.Portal> <TooltipPrimitive.Portal>
<TooltipPrimitive.Content <TooltipPrimitive.Positioner
data-slot="tooltip-content" align={align}
alignOffset={alignOffset}
side={side}
sideOffset={sideOffset} sideOffset={sideOffset}
className={cn( className="isolate z-50"
"z-50 w-fit origin-(--radix-tooltip-content-transform-origin) animate-in rounded-md bg-foreground px-3 py-1.5 text-xs text-balance text-background fade-in-0 zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
className,
)}
{...props}
> >
{children} <TooltipPrimitive.Popup
<TooltipPrimitive.Arrow className="z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px] bg-foreground fill-foreground" /> data-slot="tooltip-content"
</TooltipPrimitive.Content> className={cn(
"z-50 inline-flex w-fit max-w-xs origin-(--transform-origin) items-center gap-1.5 rounded-md bg-foreground px-3 py-1.5 text-xs text-background has-data-[slot=kbd]:pr-1.5 data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 **:data-[slot=kbd]:relative **:data-[slot=kbd]:isolate **:data-[slot=kbd]:z-50 **:data-[slot=kbd]:rounded-sm data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
className,
)}
{...props}
>
{children}
<TooltipPrimitive.Arrow className="z-50 size-2.5 translate-y-[calc(-50%-2px)] rotate-45 rounded-[2px] bg-foreground fill-foreground data-[side=bottom]:top-1 data-[side=inline-end]:top-1/2! data-[side=inline-end]:-left-1 data-[side=inline-end]:-translate-y-1/2 data-[side=inline-start]:top-1/2! data-[side=inline-start]:-right-1 data-[side=inline-start]:-translate-y-1/2 data-[side=left]:top-1/2! data-[side=left]:-right-1 data-[side=left]:-translate-y-1/2 data-[side=right]:top-1/2! data-[side=right]:-left-1 data-[side=right]:-translate-y-1/2 data-[side=top]:-bottom-2.5" />
</TooltipPrimitive.Popup>
</TooltipPrimitive.Positioner>
</TooltipPrimitive.Portal> </TooltipPrimitive.Portal>
); );
} }