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",
"style": "new-york",
"style": "base-vega",
"rsc": true,
"tsx": true,
"tailwind": {

File diff suppressed because it is too large Load diff

View file

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

View file

@ -403,32 +403,34 @@ export default function ChatConversationPage() {
if (!open) setModelSearchText("");
}}
>
<PopoverTrigger asChild>
<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: "" };
return logo ? (
<img
src={logo}
alt=""
className="w-4 h-4 object-contain shrink-0"
onError={(e) => {
(e.currentTarget as HTMLImageElement).style.display = "none";
}}
/>
) : null;
})()}
<span className="overflow-hidden text-ellipsis whitespace-nowrap">{selectedModel}</span>
</>
) : (
<span className="text-muted-foreground">Select model</span>
)}
<ChevronDown className="h-3 w-3 text-muted-foreground shrink-0" />
</Button>
</PopoverTrigger>
<PopoverTrigger
render={
<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: "" };
return logo ? (
<img
src={logo}
alt=""
className="w-4 h-4 object-contain shrink-0"
onError={(e) => {
(e.currentTarget as HTMLImageElement).style.display = "none";
}}
/>
) : null;
})()}
<span className="overflow-hidden text-ellipsis whitespace-nowrap">{selectedModel}</span>
</>
) : (
<span className="text-muted-foreground">Select model</span>
)}
<ChevronDown className="h-3 w-3 text-muted-foreground shrink-0" />
</Button>
}
/>
<PopoverContent align="start" side="top" className="p-0 w-auto">
{modelSelectorContent}
</PopoverContent>
@ -456,14 +458,16 @@ export default function ChatConversationPage() {
<div className="flex items-center gap-2 min-w-0">
{modelSelectorTrigger}
<Popover open={mcpPopoverOpen} onOpenChange={setMcpPopoverOpen}>
<PopoverTrigger asChild>
<Button variant="outline" size="sm" className="gap-1 px-2.5 text-muted-foreground">
<Plus className="h-3.5 w-3.5" />
{selectedMCPServers.length > 0 && (
<span className="text-xs text-primary font-medium">{selectedMCPServers.length}</span>
)}
</Button>
</PopoverTrigger>
<PopoverTrigger
render={
<Button variant="outline" size="sm" className="gap-1 px-2.5 text-muted-foreground">
<Plus className="h-3.5 w-3.5" />
{selectedMCPServers.length > 0 && (
<span className="text-xs text-primary font-medium">{selectedMCPServers.length}</span>
)}
</Button>
}
/>
<PopoverContent side="top" align="start" className="p-0 w-auto">
<MCPConnectPicker
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%]">
{hovered && !isStreaming && onEdit && (
<TooltipProvider delayDuration={300}>
<TooltipProvider delay={300}>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon-xs"
onClick={() => {
setEditValue(message.content);
setEditing(true);
}}
className="text-muted-foreground hover:text-foreground shrink-0"
>
<Pencil className="size-3.5" />
</Button>
</TooltipTrigger>
<TooltipTrigger
render={
<Button
variant="ghost"
size="icon-xs"
onClick={() => {
setEditValue(message.content);
setEditing(true);
}}
className="text-muted-foreground hover:text-foreground shrink-0"
>
<Pencil className="size-3.5" />
</Button>
}
/>
<TooltipContent>
<p>Edit message</p>
</TooltipContent>
@ -265,18 +267,20 @@ function CopyButton({ text }: { text: string }) {
return (
<div className="flex items-center gap-1 mt-1.5">
<TooltipProvider delayDuration={300}>
<TooltipProvider delay={300}>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon-xs"
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>
</TooltipTrigger>
<TooltipTrigger
render={
<Button
variant="ghost"
size="icon-xs"
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>
}
/>
<TooltipContent>
<p>{copied ? "Copied!" : "Copy"}</p>
</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"
onClick={(e) => e.stopPropagation()}
>
<TooltipProvider delayDuration={300}>
<TooltipProvider delay={300}>
<Tooltip>
<TooltipTrigger asChild>
<Button onClick={startEditing} variant="ghost" size="icon-xs" className="text-muted-foreground">
<Pencil className="h-3 w-3" />
</Button>
</TooltipTrigger>
<TooltipTrigger
render={
<Button onClick={startEditing} variant="ghost" size="icon-xs" className="text-muted-foreground">
<Pencil className="h-3 w-3" />
</Button>
}
/>
<TooltipContent side="bottom">
<p>Rename</p>
</TooltipContent>
@ -157,15 +159,23 @@ const ConversationRow: React.FC<ConversationRowProps> = ({ conv, isActive, onSel
</TooltipProvider>
<AlertDialog>
<TooltipProvider delayDuration={300}>
<TooltipProvider delay={300}>
<Tooltip>
<TooltipTrigger asChild>
<AlertDialogTrigger asChild>
<Button variant="ghost" size="icon-xs" className="text-muted-foreground hover:text-destructive">
<Trash2 className="h-3 w-3" />
</Button>
</AlertDialogTrigger>
</TooltipTrigger>
<TooltipTrigger
render={
<AlertDialogTrigger
render={
<Button
variant="ghost"
size="icon-xs"
className="text-muted-foreground hover:text-destructive"
>
<Trash2 className="h-3 w-3" />
</Button>
}
/>
}
/>
<TooltipContent side="bottom">
<p>Delete</p>
</TooltipContent>

View file

@ -184,21 +184,23 @@ const MCPCredentialsTab: React.FC<Props> = ({ accessToken }) => {
</TableCell>
<TableCell className="text-right">
<AlertDialog>
<AlertDialogTrigger asChild>
<Button
variant="outline"
size="icon-sm"
disabled={isRevoking}
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" />
) : (
<Trash2 className="h-3.5 w-3.5" />
)}
</Button>
</AlertDialogTrigger>
<AlertDialogTrigger
render={
<Button
variant="outline"
size="icon-sm"
disabled={isRevoking}
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" />
) : (
<Trash2 className="h-3.5 w-3.5" />
)}
</Button>
}
/>
<AlertDialogContent>
<AlertDialogHeader>
<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";
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 { 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} />;
}
function AlertDialogTrigger({ ...props }: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
function AlertDialogTrigger({ ...props }: AlertDialogPrimitive.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} />;
}
function AlertDialogOverlay({ className, ...props }: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
function AlertDialogOverlay({ className, ...props }: AlertDialogPrimitive.Backdrop.Props) {
return (
<AlertDialogPrimitive.Overlay
<AlertDialogPrimitive.Backdrop
data-slot="alert-dialog-overlay"
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,
)}
{...props}
@ -35,17 +35,17 @@ function AlertDialogContent({
className,
size = "default",
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Content> & {
}: AlertDialogPrimitive.Popup.Props & {
size?: "default" | "sm";
}) {
return (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
<AlertDialogPrimitive.Popup
data-slot="alert-dialog-content"
data-size={size}
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,
)}
{...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>) {
return (
<AlertDialogPrimitive.Title
data-slot="alert-dialog-title"
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,
)}
{...props}
@ -100,18 +113,8 @@ function AlertDialogDescription({
return (
<AlertDialogPrimitive.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(
"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,
)}
{...props}
@ -124,12 +127,14 @@ function AlertDialogAction({
variant = "default",
size = "default",
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Action> &
Pick<React.ComponentProps<typeof Button>, "variant" | "size">) {
}: AlertDialogPrimitive.Close.Props & Pick<React.ComponentProps<typeof Button>, "variant" | "size">) {
return (
<Button variant={variant} size={size} asChild>
<AlertDialogPrimitive.Action data-slot="alert-dialog-action" className={cn(className)} {...props} />
</Button>
<AlertDialogPrimitive.Close
data-slot="alert-dialog-action"
className={cn(className)}
render={<Button variant={variant} size={size} />}
{...props}
/>
);
}
@ -138,12 +143,14 @@ function AlertDialogCancel({
variant = "outline",
size = "default",
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Cancel> &
Pick<React.ComponentProps<typeof Button>, "variant" | "size">) {
}: AlertDialogPrimitive.Close.Props & Pick<React.ComponentProps<typeof Button>, "variant" | "size">) {
return (
<Button variant={variant} size={size} asChild>
<AlertDialogPrimitive.Cancel data-slot="alert-dialog-cancel" className={cn(className)} {...props} />
</Button>
<AlertDialogPrimitive.Close
data-slot="alert-dialog-cancel"
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" });
expect(button).toHaveClass("bg-primary");
expect(button).toHaveAttribute("data-slot", "button");
expect(button).toHaveAttribute("data-variant", "default");
});
it("applies variant and size props", () => {
@ -19,9 +18,8 @@ describe("Button", () => {
</Button>,
);
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).toHaveAttribute("data-variant", "destructive");
});
it("resolves conflicting classes through twMerge so className wins", () => {
@ -31,15 +29,12 @@ describe("Button", () => {
expect(button).not.toHaveClass("bg-primary");
});
it("renders the child element when asChild is set", () => {
render(
<Button asChild>
<a href="https://docs.litellm.ai">Docs</a>
</Button>,
);
const link = screen.getByRole("link", { name: "Docs" });
expect(link).toHaveClass("bg-primary");
expect(screen.queryByRole("button")).not.toBeInTheDocument();
it("renders the element passed via the render prop with button semantics", () => {
render(<Button nativeButton={false} render={<a href="https://docs.litellm.ai">Docs</a>} />);
const button = screen.getByRole("button", { name: "Docs" });
expect(button.tagName).toBe("A");
expect(button).toHaveAttribute("href", "https://docs.litellm.ai");
expect(button).toHaveClass("bg-primary");
});
it("forwards refs to the underlying button element", () => {

View file

@ -1,30 +1,34 @@
import * as React from "react";
import { Button as ButtonPrimitive } from "@base-ui/react/button";
import { type VariantProps } from "cva";
import { Slot } from "radix-ui";
import { cn, cva } from "@/lib/cva.config";
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: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40",
default: "bg-primary text-primary-foreground hover:bg-primary/80",
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",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/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-[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 px-4 py-2 has-[>svg]:px-3",
xs: "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3",
sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
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-md [&_svg:not([class*='size-'])]:size-3",
"icon-sm": "size-8",
"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",
},
},
@ -34,21 +38,14 @@ const buttonVariants = cva({
},
});
type ButtonProps = React.ComponentPropsWithoutRef<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean;
};
type ButtonProps = ButtonPrimitive.Props & VariantProps<typeof buttonVariants>;
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant = "default", size = "default", asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot.Root : "button";
({ className, variant = "default", size = "default", ...props }, ref) => {
return (
<Comp
<ButtonPrimitive
ref={ref}
data-slot="button"
data-variant={variant}
data-size={size}
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>

View file

@ -1,17 +1,17 @@
"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} />;
}
function CollapsibleTrigger({ ...props }: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) {
return <CollapsiblePrimitive.CollapsibleTrigger data-slot="collapsible-trigger" {...props} />;
function CollapsibleTrigger({ ...props }: CollapsiblePrimitive.Trigger.Props) {
return <CollapsiblePrimitive.Trigger data-slot="collapsible-trigger" {...props} />;
}
function CollapsibleContent({ ...props }: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) {
return <CollapsiblePrimitive.CollapsibleContent data-slot="collapsible-content" {...props} />;
function CollapsibleContent({ ...props }: CollapsiblePrimitive.Panel.Props) {
return <CollapsiblePrimitive.Panel data-slot="collapsible-content" {...props} />;
}
export { Collapsible, CollapsibleTrigger, CollapsibleContent };

View file

@ -1,34 +1,34 @@
"use client";
import * as React from "react";
import { XIcon } from "lucide-react";
import { Dialog as DialogPrimitive } from "radix-ui";
import { Dialog as DialogPrimitive } from "@base-ui/react/dialog";
import { cn } from "@/lib/cva.config";
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} />;
}
function DialogTrigger({ ...props }: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
function DialogTrigger({ ...props }: DialogPrimitive.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} />;
}
function DialogClose({ ...props }: React.ComponentProps<typeof DialogPrimitive.Close>) {
function DialogClose({ ...props }: DialogPrimitive.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 (
<DialogPrimitive.Overlay
<DialogPrimitive.Backdrop
data-slot="dialog-overlay"
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,
)}
{...props}
@ -41,16 +41,16 @@ function DialogContent({
children,
showCloseButton = true,
...props
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
}: DialogPrimitive.Popup.Props & {
showCloseButton?: boolean;
}) {
return (
<DialogPortal data-slot="dialog-portal">
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
<DialogPrimitive.Popup
data-slot="dialog-content"
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,
)}
{...props}
@ -59,25 +59,19 @@ function DialogContent({
{showCloseButton && (
<DialogPrimitive.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 />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
)}
</DialogPrimitive.Content>
</DialogPrimitive.Popup>
</DialogPortal>
);
}
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-header"
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
{...props}
/>
);
return <div data-slot="dialog-header" className={cn("flex flex-col gap-2", className)} {...props} />;
}
function DialogFooter({
@ -95,30 +89,25 @@ function DialogFooter({
{...props}
>
{children}
{showCloseButton && (
<DialogPrimitive.Close asChild>
<Button variant="outline">Close</Button>
</DialogPrimitive.Close>
)}
{showCloseButton && <DialogPrimitive.Close render={<Button variant="outline" />}>Close</DialogPrimitive.Close>}
</div>
);
}
function DialogTitle({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Title>) {
function DialogTitle({ className, ...props }: DialogPrimitive.Title.Props) {
return (
<DialogPrimitive.Title
data-slot="dialog-title"
className={cn("text-lg leading-none font-semibold", className)}
{...props}
/>
<DialogPrimitive.Title data-slot="dialog-title" className={cn("leading-none font-medium", className)} {...props} />
);
}
function DialogDescription({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Description>) {
function DialogDescription({ className, ...props }: DialogPrimitive.Description.Props) {
return (
<DialogPrimitive.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}
/>
);

View file

@ -1,13 +1,12 @@
"use client";
import * as React from "react";
import { Label as LabelPrimitive } from "radix-ui";
import { cn } from "@/lib/cva.config";
function Label({ className, ...props }: React.ComponentProps<typeof LabelPrimitive.Root>) {
function Label({ className, ...props }: React.ComponentProps<"label">) {
return (
<LabelPrimitive.Root
<label
data-slot="label"
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",

View file

@ -1,54 +1,65 @@
"use client";
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";
function Popover({ ...props }: React.ComponentProps<typeof PopoverPrimitive.Root>) {
function Popover({ ...props }: PopoverPrimitive.Root.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} />;
}
function PopoverContent({
className,
align = "center",
alignOffset = 0,
side = "bottom",
sideOffset = 4,
...props
}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
}: PopoverPrimitive.Popup.Props &
Pick<PopoverPrimitive.Positioner.Props, "align" | "alignOffset" | "side" | "sideOffset">) {
return (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
data-slot="popover-content"
<PopoverPrimitive.Positioner
align={align}
alignOffset={alignOffset}
side={side}
sideOffset={sideOffset}
className={cn(
"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,
)}
{...props}
/>
className="isolate z-50"
>
<PopoverPrimitive.Popup
data-slot="popover-content"
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>
);
}
function PopoverAnchor({ ...props }: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />;
}
function PopoverHeader({ className, ...props }: React.ComponentProps<"div">) {
return <div data-slot="popover-header" className={cn("flex flex-col gap-1 text-sm", className)} {...props} />;
}
function PopoverTitle({ className, ...props }: React.ComponentProps<"h2">) {
return <div data-slot="popover-title" className={cn("font-medium", className)} {...props} />;
function PopoverTitle({ className, ...props }: PopoverPrimitive.Title.Props) {
return <PopoverPrimitive.Title data-slot="popover-title" className={cn("font-medium", className)} {...props} />;
}
function PopoverDescription({ className, ...props }: React.ComponentProps<"p">) {
return <p data-slot="popover-description" className={cn("text-muted-foreground", className)} {...props} />;
function PopoverDescription({ className, ...props }: PopoverPrimitive.Description.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";
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";
function ScrollArea({ className, children, ...props }: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
function ScrollArea({ className, children, ...props }: ScrollAreaPrimitive.Root.Props) {
return (
<ScrollAreaPrimitive.Root data-slot="scroll-area" className={cn("relative", className)} {...props}>
<ScrollAreaPrimitive.Viewport
@ -20,28 +20,20 @@ function ScrollArea({ className, children, ...props }: React.ComponentProps<type
);
}
function ScrollBar({
className,
orientation = "vertical",
...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
function ScrollBar({ className, orientation = "vertical", ...props }: ScrollAreaPrimitive.Scrollbar.Props) {
return (
<ScrollAreaPrimitive.ScrollAreaScrollbar
<ScrollAreaPrimitive.Scrollbar
data-slot="scroll-area-scrollbar"
data-orientation={orientation}
orientation={orientation}
className={cn(
"flex touch-none p-px transition-colors select-none",
orientation === "vertical" && "h-full w-2.5 border-l border-l-transparent",
orientation === "horizontal" && "h-2.5 flex-col border-t border-t-transparent",
"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",
className,
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb
data-slot="scroll-area-thumb"
className="relative flex-1 rounded-full bg-border"
/>
</ScrollAreaPrimitive.ScrollAreaScrollbar>
<ScrollAreaPrimitive.Thumb data-slot="scroll-area-thumb" className="relative flex-1 rounded-full bg-border" />
</ScrollAreaPrimitive.Scrollbar>
);
}

View file

@ -1,21 +1,21 @@
"use client";
import * as React from "react";
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react";
import { Select as SelectPrimitive } from "radix-ui";
import { Select as SelectPrimitive } from "@base-ui/react/select";
import { cn } from "@/lib/cva.config";
import { ChevronDownIcon, CheckIcon, ChevronUpIcon } from "lucide-react";
function Select({ ...props }: React.ComponentProps<typeof SelectPrimitive.Root>) {
return <SelectPrimitive.Root data-slot="select" {...props} />;
const Select = SelectPrimitive.Root;
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>) {
return <SelectPrimitive.Group data-slot="select-group" {...props} />;
}
function SelectValue({ ...props }: React.ComponentProps<typeof SelectPrimitive.Value>) {
return <SelectPrimitive.Value data-slot="select-value" {...props} />;
function SelectValue({ className, ...props }: SelectPrimitive.Value.Props) {
return (
<SelectPrimitive.Value data-slot="select-value" className={cn("flex flex-1 text-left", className)} {...props} />
);
}
function SelectTrigger({
@ -23,7 +23,7 @@ function SelectTrigger({
size = "default",
children,
...props
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
}: SelectPrimitive.Trigger.Props & {
size?: "sm" | "default";
}) {
return (
@ -31,15 +31,13 @@ function SelectTrigger({
data-slot="select-trigger"
data-size={size}
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,
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDownIcon className="size-4 opacity-50" />
</SelectPrimitive.Icon>
<SelectPrimitive.Icon render={<ChevronDownIcon className="pointer-events-none size-4 text-muted-foreground" />} />
</SelectPrimitive.Trigger>
);
}
@ -47,43 +45,45 @@ function SelectTrigger({
function SelectContent({
className,
children,
position = "item-aligned",
side = "bottom",
sideOffset = 4,
align = "center",
alignOffset = 0,
alignItemWithTrigger = true,
...props
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
}: SelectPrimitive.Popup.Props &
Pick<SelectPrimitive.Positioner.Props, "align" | "alignOffset" | "side" | "sideOffset" | "alignItemWithTrigger">) {
return (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
data-slot="select-content"
className={cn(
"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}
<SelectPrimitive.Positioner
side={side}
sideOffset={sideOffset}
align={align}
{...props}
alignOffset={alignOffset}
alignItemWithTrigger={alignItemWithTrigger}
className="isolate z-50"
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
<SelectPrimitive.Popup
data-slot="select-content"
data-align-trigger={alignItemWithTrigger}
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-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",
className,
)}
{...props}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
<SelectScrollUpButton />
<SelectPrimitive.List>{children}</SelectPrimitive.List>
<SelectScrollDownButton />
</SelectPrimitive.Popup>
</SelectPrimitive.Positioner>
</SelectPrimitive.Portal>
);
}
function SelectLabel({ className, ...props }: React.ComponentProps<typeof SelectPrimitive.Label>) {
function SelectLabel({ className, ...props }: SelectPrimitive.GroupLabel.Props) {
return (
<SelectPrimitive.Label
<SelectPrimitive.GroupLabel
data-slot="select-label"
className={cn("px-2 py-1.5 text-xs text-muted-foreground", className)}
{...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 (
<SelectPrimitive.Item
data-slot="select-item"
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,
)}
{...props}
>
<span data-slot="select-item-indicator" className="absolute right-2 flex size-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
<SelectPrimitive.ItemText className="flex flex-1 shrink-0 gap-2 whitespace-nowrap">
{children}
</SelectPrimitive.ItemText>
<SelectPrimitive.ItemIndicator
render={<span className="pointer-events-none absolute right-2 flex size-4 items-center justify-center" />}
>
<CheckIcon className="pointer-events-none" />
</SelectPrimitive.ItemIndicator>
</SelectPrimitive.Item>
);
}
function SelectSeparator({ className, ...props }: React.ComponentProps<typeof SelectPrimitive.Separator>) {
function SelectSeparator({ className, ...props }: SelectPrimitive.Separator.Props) {
return (
<SelectPrimitive.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 (
<SelectPrimitive.ScrollUpButton
<SelectPrimitive.ScrollUpArrow
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}
>
<ChevronUpIcon className="size-4" />
</SelectPrimitive.ScrollUpButton>
<ChevronUpIcon />
</SelectPrimitive.ScrollUpArrow>
);
}
function SelectScrollDownButton({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
function SelectScrollDownButton({ className, ...props }: React.ComponentProps<typeof SelectPrimitive.ScrollDownArrow>) {
return (
<SelectPrimitive.ScrollDownButton
<SelectPrimitive.ScrollDownArrow
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}
>
<ChevronDownIcon className="size-4" />
</SelectPrimitive.ScrollDownButton>
<ChevronDownIcon />
</SelectPrimitive.ScrollDownArrow>
);
}

View file

@ -1,23 +1,16 @@
"use client";
import * as React from "react";
import { Separator as SeparatorPrimitive } from "radix-ui";
import { Separator as SeparatorPrimitive } from "@base-ui/react/separator";
import { cn } from "@/lib/cva.config";
function Separator({
className,
orientation = "horizontal",
decorative = true,
...props
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
function Separator({ className, orientation = "horizontal", ...props }: SeparatorPrimitive.Props) {
return (
<SeparatorPrimitive.Root
<SeparatorPrimitive
data-slot="separator"
decorative={decorative}
orientation={orientation}
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,
)}
{...props}

View file

@ -1,7 +1,6 @@
"use client";
import * as React from "react";
import { Switch as SwitchPrimitive } from "radix-ui";
import { Switch as SwitchPrimitive } from "@base-ui/react/switch";
import { cn } from "@/lib/cva.config";
@ -9,7 +8,7 @@ function Switch({
className,
size = "default",
...props
}: React.ComponentProps<typeof SwitchPrimitive.Root> & {
}: SwitchPrimitive.Root.Props & {
size?: "sm" | "default";
}) {
return (
@ -17,16 +16,14 @@ function Switch({
data-slot="switch"
data-size={size}
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,
)}
{...props}
>
<SwitchPrimitive.Thumb
data-slot="switch-thumb"
className={cn(
"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",
)}
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"
/>
</SwitchPrimitive.Root>
);

View file

@ -1,25 +1,23 @@
"use client";
import * as React from "react";
import { Tabs as TabsPrimitive } from "@base-ui/react/tabs";
import { type VariantProps } from "cva";
import { Tabs as TabsPrimitive } from "radix-ui";
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 (
<TabsPrimitive.Root
data-slot="tabs"
data-orientation={orientation}
orientation={orientation}
className={cn("group/tabs flex gap-2 data-[orientation=horizontal]:flex-col", className)}
className={cn("group/tabs flex gap-2 data-horizontal:flex-col", className)}
{...props}
/>
);
}
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: {
variant: {
default: "bg-muted",
@ -35,7 +33,7 @@ function TabsList({
className,
variant = "default",
...props
}: React.ComponentProps<typeof TabsPrimitive.List> & VariantProps<typeof tabsListVariants>) {
}: TabsPrimitive.List.Props & VariantProps<typeof tabsListVariants>) {
return (
<TabsPrimitive.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 (
<TabsPrimitive.Trigger
<TabsPrimitive.Tab
data-slot="tabs-trigger"
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",
"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",
"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",
"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",
"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-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-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-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,
)}
{...props}
@ -62,8 +60,10 @@ function TabsTrigger({ className, ...props }: React.ComponentProps<typeof TabsPr
);
}
function TabsContent({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Content>) {
return <TabsPrimitive.Content data-slot="tabs-content" className={cn("flex-1 outline-none", className)} {...props} />;
function TabsContent({ className, ...props }: TabsPrimitive.Panel.Props) {
return (
<TabsPrimitive.Panel data-slot="tabs-content" className={cn("flex-1 text-sm outline-none", className)} {...props} />
);
}
export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants };

View file

@ -1,42 +1,52 @@
"use client";
import * as React from "react";
import { Tooltip as TooltipPrimitive } from "radix-ui";
import { Tooltip as TooltipPrimitive } from "@base-ui/react/tooltip";
import { cn } from "@/lib/cva.config";
function TooltipProvider({ delayDuration = 0, ...props }: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
return <TooltipPrimitive.Provider data-slot="tooltip-provider" delayDuration={delayDuration} {...props} />;
function TooltipProvider({ delay = 0, ...props }: TooltipPrimitive.Provider.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} />;
}
function TooltipTrigger({ ...props }: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
function TooltipTrigger({ ...props }: TooltipPrimitive.Trigger.Props) {
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />;
}
function TooltipContent({
className,
sideOffset = 0,
side = "top",
sideOffset = 4,
align = "center",
alignOffset = 0,
children,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
}: TooltipPrimitive.Popup.Props &
Pick<TooltipPrimitive.Positioner.Props, "align" | "alignOffset" | "side" | "sideOffset">) {
return (
<TooltipPrimitive.Portal>
<TooltipPrimitive.Content
data-slot="tooltip-content"
<TooltipPrimitive.Positioner
align={align}
alignOffset={alignOffset}
side={side}
sideOffset={sideOffset}
className={cn(
"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}
className="isolate z-50"
>
{children}
<TooltipPrimitive.Arrow className="z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px] bg-foreground fill-foreground" />
</TooltipPrimitive.Content>
<TooltipPrimitive.Popup
data-slot="tooltip-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>
);
}