refactor(v5): migrate UI primitives from radix-ui to @base-ui/react

This commit is contained in:
Andras Bacsai
2026-06-16 19:49:36 +02:00
parent 6b8f50a90e
commit 859a701669
8 changed files with 165 additions and 1782 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "radix-lyra",
"style": "base-lyra",
"rsc": false,
"tsx": false,
"tailwind": {
+84 -1719
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -17,6 +17,7 @@
"vite": "7.3.2"
},
"dependencies": {
"@base-ui/react": "^1.5.0",
"@fontsource-variable/geist": "^5.2.9",
"@inertiajs/react": "^3.3.0",
"@inertiajs/vite": "^3.3.0",
@@ -28,7 +29,6 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"playwright": "^1.58.2",
"radix-ui": "^1.6.0",
"react": "^19.2.7",
"react-dom": "^19.2.7",
"tailwind-merge": "^3.6.0",
+16 -1
View File
@@ -53,6 +53,15 @@ export function AppNavbar({
persistSelection(projectUuid, nextEnvironmentUuid);
}
const projectItems = projects.map((project) => ({
label: project.name,
value: project.uuid,
}));
const environmentItems = (selectedProject?.environments ?? []).map((environment) => ({
label: environment.name,
value: environment.uuid,
}));
return (
<header className="fixed inset-x-0 top-0 z-40 border-b border-border bg-background">
<nav className="flex h-16 items-center gap-4 px-6" aria-label="Main navigation">
@@ -65,7 +74,12 @@ export function AppNavbar({
</a>
<div className="flex min-w-0 items-center gap-2">
<Select value={selectedProject?.uuid ?? ''} onValueChange={selectProject} disabled={projects.length === 0}>
<Select
items={projectItems}
value={selectedProject?.uuid ?? ''}
onValueChange={selectProject}
disabled={projects.length === 0}
>
<SelectTrigger aria-label="Select a project" variant="ghost" className="max-w-[10rem]">
<SelectValue placeholder="Select a project" />
</SelectTrigger>
@@ -83,6 +97,7 @@ export function AppNavbar({
<span className="text-muted-foreground">/</span>
<Select
items={environmentItems}
value={selectedEnvironment?.uuid ?? ''}
onValueChange={selectEnvironment}
disabled={!selectedProject || (selectedProject.environments ?? []).length === 0}
+2 -8
View File
@@ -1,6 +1,5 @@
import * as React from "react"
import { Button as ButtonPrimitive } from "@base-ui/react/button"
import { cva } from "class-variance-authority";
import { Slot } from "radix-ui"
import { cn } from "@/lib/utils"
@@ -43,16 +42,11 @@ function Button({
className,
variant = "default",
size = "default",
asChild = false,
...props
}) {
const Comp = asChild ? Slot.Root : "button"
return (
<Comp
<ButtonPrimitive
data-slot="button"
data-variant={variant}
data-size={size}
className={cn(buttonVariants({ variant, size, className }))}
{...props} />
);
+54 -45
View File
@@ -1,14 +1,10 @@
import * as React from "react"
import { Select as SelectPrimitive } from "radix-ui"
import { Select as SelectPrimitive } from "@base-ui/react/select"
import { cn } from "@/lib/utils"
import { CaretDownIcon, CheckIcon, CaretUpIcon } from "@phosphor-icons/react"
function Select({
...props
}) {
return <SelectPrimitive.Root data-slot="select" {...props} />;
}
const Select = SelectPrimitive.Root
function SelectGroup({
className,
@@ -23,9 +19,15 @@ function SelectGroup({
}
function SelectValue({
className,
...props
}) {
return <SelectPrimitive.Value data-slot="select-value" {...props} />;
return (
<SelectPrimitive.Value
data-slot="select-value"
className={cn("flex flex-1 text-left", className)}
{...props} />
);
}
function SelectTrigger({
@@ -39,6 +41,7 @@ function SelectTrigger({
<SelectPrimitive.Trigger
data-slot="select-trigger"
data-size={size}
data-variant={variant}
className={cn(
"flex w-fit items-center justify-between gap-1.5 rounded-none whitespace-nowrap transition-colors outline-none select-none focus-visible:ring-1 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-1 aria-invalid:ring-destructive/20 data-placeholder:text-muted-foreground data-[size=default]:h-8 data-[size=sm]:h-7 data-[size=sm]:rounded-none *: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:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
variant === "default" && "border border-input bg-transparent py-2 pr-2 pl-2.5 text-xs focus-visible:border-ring dark:bg-input/30 dark:hover:bg-input/50",
@@ -47,9 +50,10 @@ function SelectTrigger({
)}
{...props}>
{children}
<SelectPrimitive.Icon asChild>
<CaretDownIcon className="pointer-events-none size-4 text-muted-foreground" />
</SelectPrimitive.Icon>
<SelectPrimitive.Icon
render={
<CaretDownIcon className="pointer-events-none size-4 text-muted-foreground" />
} />
</SelectPrimitive.Trigger>
);
}
@@ -57,34 +61,36 @@ function SelectTrigger({
function SelectContent({
className,
children,
position = "item-aligned",
position,
side = "bottom",
sideOffset = 4,
align = "center",
alignOffset = 0,
alignItemWithTrigger = position === "popper" ? false : true,
...props
}) {
return (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
data-slot="select-content"
data-align-trigger={position === "item-aligned"}
className={cn(
"relative z-50 max-h-(--radix-select-content-available-height) min-w-36 origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-none 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=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"&&"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}>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
data-position={position}
alignOffset={alignOffset}
alignItemWithTrigger={alignItemWithTrigger}
className="isolate z-50">
<SelectPrimitive.Popup
data-slot="select-content"
data-align-trigger={alignItemWithTrigger}
className={cn(
"data-[position=popper]:h-(--radix-select-trigger-height) data-[position=popper]:w-full data-[position=popper]:min-w-(--radix-select-trigger-width)",
position === "popper" && ""
)}>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
"relative isolate z-50 max-h-(--available-height) w-(--anchor-width) min-w-36 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-none 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}>
<SelectScrollUpButton />
<SelectPrimitive.List>{children}</SelectPrimitive.List>
<SelectScrollDownButton />
</SelectPrimitive.Popup>
</SelectPrimitive.Positioner>
</SelectPrimitive.Portal>
);
}
@@ -94,7 +100,7 @@ function SelectLabel({
...props
}) {
return (
<SelectPrimitive.Label
<SelectPrimitive.GroupLabel
data-slot="select-label"
className={cn("px-2 py-2 text-xs text-muted-foreground", className)}
{...props} />
@@ -114,13 +120,16 @@ function SelectItem({
className
)}
{...props}>
<span
className="pointer-events-none absolute right-2 flex size-4 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<CheckIcon className="pointer-events-none" />
</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>
);
}
@@ -142,15 +151,15 @@ function SelectScrollUpButton({
...props
}) {
return (
<SelectPrimitive.ScrollUpButton
<SelectPrimitive.ScrollUpArrow
data-slot="select-scroll-up-button"
className={cn(
"z-10 flex cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-4",
"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}>
<CaretUpIcon />
</SelectPrimitive.ScrollUpButton>
</SelectPrimitive.ScrollUpArrow>
);
}
@@ -159,15 +168,15 @@ function SelectScrollDownButton({
...props
}) {
return (
<SelectPrimitive.ScrollDownButton
<SelectPrimitive.ScrollDownArrow
data-slot="select-scroll-down-button"
className={cn(
"z-10 flex cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-4",
"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}>
<CaretDownIcon />
</SelectPrimitive.ScrollDownButton>
</SelectPrimitive.ScrollDownArrow>
);
}
+2 -5
View File
@@ -1,20 +1,17 @@
"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/utils"
function Separator({
className,
orientation = "horizontal",
decorative = true,
...props
}) {
return (
<SeparatorPrimitive.Root
<SeparatorPrimitive
data-slot="separator"
decorative={decorative}
orientation={orientation}
className={cn(
"shrink-0 bg-border data-horizontal:h-px data-horizontal:w-full data-vertical:w-px data-vertical:self-stretch",
+5 -2
View File
@@ -380,12 +380,15 @@ it('defines a ghost variant for compact v5 select triggers', function () {
$select = file_get_contents(resource_path('js/v5/components/ui/select.jsx'));
expect($select)
->toContain('@base-ui/react/select')
->not->toContain('radix-ui')
->toContain('variant = "default"')
->toContain('variant === "default"')
->toContain('variant === "ghost"')
->toContain('border-transparent')
->toContain('h-auto')
->toContain('text-sm');
->toContain('text-sm')
->toContain('position === "popper" ? false : true');
});
it('uses the requested shadcn preset configuration for v5', function () {
@@ -393,7 +396,7 @@ it('uses the requested shadcn preset configuration for v5', function () {
$css = file_get_contents(resource_path('css/v5/app.css'));
expect($components['style'])
->toBe('radix-lyra')
->toBe('base-lyra')
->and($components['iconLibrary'])->toBe('phosphor')
->and($components['tailwind']['css'])->toBe('resources/css/v5/app.css')
->and($components['tailwind']['baseColor'])->toBe('zinc')