Skip to content

Commit 1de80a2

Browse files
committed
Adopt Base UI primitives for shared form controls
- Replace custom button, dialog, and input wrappers with Base UI - Add editor defaults for JSON and JSONC files - Include PR review cockpit design notes
1 parent 501f9df commit 1de80a2

5 files changed

Lines changed: 301 additions & 92 deletions

File tree

.vscode/settings.json

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@
44
"editor.codeActionsOnSave": {
55
"source.fixAll.oxc": "always"
66
},
7-
"oxc.unusedDisableDirectives": "warn",
8-
"typescript.tsdk": "node_modules/typescript/lib"
7+
"typescript.tsdk": "node_modules/typescript/lib",
8+
"[json]": {
9+
"editor.defaultFormatter": "vscode.json-language-features"
10+
},
11+
"[jsonc]": {
12+
"editor.defaultFormatter": "vscode.json-language-features"
13+
}
914
}
Lines changed: 15 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1+
import { useRender } from "@base-ui/react/use-render";
12
import { cva, type VariantProps } from "class-variance-authority";
2-
import * as React from "react";
33

44
import { cn } from "~/lib/utils";
55

@@ -12,6 +12,8 @@ const buttonVariants = cva(
1212
"bg-linear-to-b from-primary to-[hsl(223_82%_62%)] text-primary-foreground shadow-[0_18px_45px_-20px_hsl(var(--primary)/0.85)] hover:brightness-110 hover:shadow-[0_22px_55px_-22px_hsl(var(--primary)/0.95)]",
1313
destructive:
1414
"bg-linear-to-b from-destructive to-[hsl(352_72%_52%)] text-destructive-foreground shadow-[0_18px_45px_-20px_hsl(var(--destructive)/0.7)] hover:brightness-105 hover:shadow-[0_22px_55px_-22px_hsl(var(--destructive)/0.8)]",
15+
"destructive-outline":
16+
"border border-destructive/35 bg-destructive/6 text-destructive shadow-[inset_0_1px_0_hsl(0_0%_100%/0.04)] hover:border-destructive/45 hover:bg-destructive/10 hover:text-destructive",
1517
outline:
1618
"border border-border/80 bg-card/70 text-foreground shadow-[inset_0_1px_0_hsl(0_0%_100%/0.04)] hover:border-primary/30 hover:bg-accent/80 hover:text-foreground",
1719
secondary:
@@ -26,6 +28,7 @@ const buttonVariants = cva(
2628
lg: "h-10 rounded-xl px-6",
2729
icon: "size-9 rounded-xl",
2830
"icon-xs": "size-7 rounded-lg",
31+
"icon-sm": "size-8 rounded-lg",
2932
},
3033
},
3134
defaultVariants: {
@@ -35,33 +38,19 @@ const buttonVariants = cva(
3538
},
3639
);
3740

38-
function Button({
39-
className,
40-
variant,
41-
size,
42-
asChild = false,
43-
...props
44-
}: React.ComponentProps<"button"> &
45-
VariantProps<typeof buttonVariants> & {
46-
asChild?: boolean;
47-
}) {
48-
const Comp = asChild ? (React.Fragment as never) : "button";
41+
interface ButtonProps
42+
extends useRender.ComponentProps<"button">, VariantProps<typeof buttonVariants> {}
4943

50-
if (asChild) {
51-
const child = React.Children.only(props.children) as React.ReactElement;
52-
return React.cloneElement(child, {
44+
function Button({ className, variant, size, render, ...props }: ButtonProps) {
45+
return useRender({
46+
defaultTagName: "button",
47+
props: {
5348
...props,
54-
className: cn(buttonVariants({ variant, size, className }), child.props.className),
55-
});
56-
}
57-
58-
return (
59-
<Comp
60-
data-slot="button"
61-
className={cn(buttonVariants({ variant, size, className }))}
62-
{...props}
63-
/>
64-
);
49+
className: cn(buttonVariants({ variant, size, className })),
50+
"data-slot": "button",
51+
},
52+
render,
53+
});
6554
}
6655

6756
export { Button, buttonVariants };
Lines changed: 81 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,124 +1,154 @@
1-
import * as DialogPrimitive from "@radix-ui/react-dialog";
1+
"use client";
2+
3+
import { Dialog as DialogPrimitive } from "@base-ui/react/dialog";
24
import { XIcon } from "lucide-react";
3-
import * as React from "react";
5+
import type * as React from "react";
46

57
import { cn } from "~/lib/utils";
68

7-
function Dialog({ ...props }: React.ComponentProps<typeof DialogPrimitive.Root>) {
8-
return <DialogPrimitive.Root data-slot="dialog" {...props} />;
9-
}
9+
const DialogCreateHandle = DialogPrimitive.createHandle;
10+
11+
const Dialog = DialogPrimitive.Root;
12+
13+
const DialogPortal = DialogPrimitive.Portal;
1014

11-
function DialogTrigger({ ...props }: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
15+
function DialogTrigger(props: DialogPrimitive.Trigger.Props) {
1216
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />;
1317
}
1418

15-
function DialogPortal({ ...props }: React.ComponentProps<typeof DialogPrimitive.Portal>) {
16-
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
19+
function DialogClose(props: DialogPrimitive.Close.Props) {
20+
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
1721
}
1822

19-
function DialogClose({ ...props }: React.ComponentProps<typeof DialogPrimitive.Close>) {
20-
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
23+
function DialogBackdrop({ className, ...props }: DialogPrimitive.Backdrop.Props) {
24+
return (
25+
<DialogPrimitive.Backdrop
26+
className={cn(
27+
"fixed inset-0 z-50 bg-[radial-gradient(circle_at_top,rgba(96,165,250,0.12),transparent_38%),rgba(3,7,18,0.82)] backdrop-blur-sm transition-all duration-200 data-ending-style:opacity-0 data-starting-style:opacity-0",
28+
className,
29+
)}
30+
data-slot="dialog-backdrop"
31+
{...props}
32+
/>
33+
);
2134
}
2235

23-
function DialogOverlay({
24-
className,
25-
...props
26-
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
36+
function DialogViewport({ className, ...props }: DialogPrimitive.Viewport.Props) {
2737
return (
28-
<DialogPrimitive.Overlay
29-
data-slot="dialog-overlay"
38+
<DialogPrimitive.Viewport
3039
className={cn(
31-
"fixed inset-0 z-50 bg-[radial-gradient(circle_at_top,rgba(96,165,250,0.12),transparent_38%),rgba(3,7,18,0.82)] backdrop-blur-sm",
32-
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
40+
"fixed inset-0 z-50 grid grid-rows-[1fr_auto_1fr] justify-items-center p-4",
3341
className,
3442
)}
43+
data-slot="dialog-viewport"
3544
{...props}
3645
/>
3746
);
3847
}
3948

40-
function DialogContent({
49+
function DialogPopup({
4150
className,
4251
children,
4352
showCloseButton = true,
4453
...props
45-
}: React.ComponentProps<typeof DialogPrimitive.Content> & { showCloseButton?: boolean }) {
54+
}: DialogPrimitive.Popup.Props & {
55+
showCloseButton?: boolean;
56+
}) {
4657
return (
47-
<DialogPortal data-slot="dialog-portal">
48-
<DialogOverlay />
49-
<DialogPrimitive.Content
50-
data-slot="dialog-content"
51-
className={cn(
52-
"fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-2xl border border-border/80 bg-card/94 p-6 text-card-foreground shadow-[0_40px_120px_-45px_hsl(220_80%_2%/0.98)] backdrop-blur-2xl duration-200 sm:max-w-lg",
53-
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
54-
className,
55-
)}
56-
{...props}
57-
>
58-
{children}
59-
{showCloseButton && (
60-
<DialogPrimitive.Close className="absolute top-4 right-4 rounded-lg p-1.5 text-muted-foreground/75 transition-colors hover:bg-accent/80 hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring/70 focus-visible:ring-offset-2 focus-visible:ring-offset-card">
61-
<XIcon className="size-4" />
62-
<span className="sr-only">Close</span>
63-
</DialogPrimitive.Close>
64-
)}
65-
</DialogPrimitive.Content>
58+
<DialogPortal>
59+
<DialogBackdrop />
60+
<DialogViewport>
61+
<DialogPrimitive.Popup
62+
className={cn(
63+
"-translate-y-[calc(1.25rem*var(--nested-dialogs))] relative row-start-2 flex max-h-full min-h-0 w-full min-w-0 max-w-lg scale-[calc(1-0.1*var(--nested-dialogs))] flex-col rounded-2xl border border-border/80 bg-card/94 text-card-foreground opacity-[calc(1-0.1*var(--nested-dialogs))] shadow-[0_40px_120px_-45px_hsl(220_80%_2%/0.98)] outline-none backdrop-blur-2xl transition-[scale,opacity,translate] duration-200 ease-in-out will-change-transform before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-2xl)-1px)] before:shadow-[0_1px_--theme(--color-black/4%)] data-nested:data-ending-style:translate-y-8 data-nested:data-starting-style:translate-y-8 data-nested-dialog-open:origin-top data-ending-style:scale-98 data-starting-style:scale-98 data-ending-style:opacity-0 data-starting-style:opacity-0 dark:before:shadow-[0_-1px_--theme(--color-white/6%)]",
64+
className,
65+
)}
66+
data-slot="dialog-popup"
67+
{...props}
68+
>
69+
{children}
70+
{showCloseButton && (
71+
<DialogPrimitive.Close className="absolute top-4 right-4 rounded-lg p-1.5 text-muted-foreground/75 transition-colors hover:bg-accent/80 hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring/70 focus-visible:ring-offset-2 focus-visible:ring-offset-card">
72+
<XIcon className="size-4" />
73+
<span className="sr-only">Close</span>
74+
</DialogPrimitive.Close>
75+
)}
76+
</DialogPrimitive.Popup>
77+
</DialogViewport>
6678
</DialogPortal>
6779
);
6880
}
6981

7082
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
7183
return (
7284
<div
85+
className={cn("flex flex-col gap-2 px-6 pt-6 text-center sm:text-left", className)}
7386
data-slot="dialog-header"
74-
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
7587
{...props}
7688
/>
7789
);
7890
}
7991

80-
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
92+
function DialogPanel({ className, ...props }: React.ComponentProps<"div">) {
93+
return <div className={cn("px-6 pb-6", className)} data-slot="dialog-panel" {...props} />;
94+
}
95+
96+
function DialogFooter({
97+
className,
98+
variant = "default",
99+
...props
100+
}: React.ComponentProps<"div"> & {
101+
variant?: "default" | "bare";
102+
}) {
81103
return (
82104
<div
105+
className={cn(
106+
"flex flex-col-reverse gap-2 px-6 sm:flex-row sm:justify-end",
107+
variant === "default" &&
108+
"rounded-b-[calc(var(--radius-2xl)-1px)] border-t bg-muted/72 py-4",
109+
variant === "bare" && "pb-6",
110+
className,
111+
)}
83112
data-slot="dialog-footer"
84-
className={cn("flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className)}
85113
{...props}
86114
/>
87115
);
88116
}
89117

90-
function DialogTitle({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Title>) {
118+
function DialogTitle({ className, ...props }: DialogPrimitive.Title.Props) {
91119
return (
92120
<DialogPrimitive.Title
93-
data-slot="dialog-title"
94121
className={cn("text-lg leading-none font-semibold tracking-tight", className)}
122+
data-slot="dialog-title"
95123
{...props}
96124
/>
97125
);
98126
}
99127

100-
function DialogDescription({
101-
className,
102-
...props
103-
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
128+
function DialogDescription({ className, ...props }: DialogPrimitive.Description.Props) {
104129
return (
105130
<DialogPrimitive.Description
106-
data-slot="dialog-description"
107131
className={cn("text-sm leading-6 text-muted-foreground", className)}
132+
data-slot="dialog-description"
108133
{...props}
109134
/>
110135
);
111136
}
112137

113138
export {
139+
DialogCreateHandle,
114140
Dialog,
141+
DialogBackdrop,
142+
DialogBackdrop as DialogOverlay,
115143
DialogClose,
116-
DialogContent,
117144
DialogDescription,
118145
DialogFooter,
119146
DialogHeader,
120-
DialogOverlay,
147+
DialogPanel,
148+
DialogPopup,
149+
DialogPopup as DialogContent,
121150
DialogPortal,
122151
DialogTitle,
123152
DialogTrigger,
153+
DialogViewport,
124154
};
Lines changed: 70 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,77 @@
1-
import * as React from "react";
1+
"use client";
2+
3+
import { Field as FieldPrimitive } from "@base-ui/react/field";
4+
import { mergeProps } from "@base-ui/react/merge-props";
5+
import type * as React from "react";
26

37
import { cn } from "~/lib/utils";
48

5-
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
9+
type InputSize = "sm" | "default" | "lg" | number;
10+
11+
type InputProps = Omit<React.ComponentProps<"input">, "size"> & {
12+
nativeInput?: boolean;
13+
size?: InputSize;
14+
unstyled?: boolean;
15+
};
16+
17+
function inputFieldClassName(size: InputSize) {
18+
return cn(
19+
"w-full min-w-0 rounded-[inherit] bg-transparent outline-none placeholder:text-muted-foreground/65",
20+
size === "sm" && "h-8 px-[calc(--spacing(2.5)-1px)] text-sm",
21+
size === "default" && "h-9 px-[calc(--spacing(3)-1px)] text-base sm:h-8 sm:text-sm",
22+
size === "lg" && "h-10 px-[calc(--spacing(3)-1px)] text-base sm:h-9 sm:text-sm",
23+
typeof size === "number" && "px-[calc(--spacing(3)-1px)]",
24+
);
25+
}
26+
27+
function Input({
28+
className,
29+
type,
30+
size = "default",
31+
nativeInput = false,
32+
unstyled = false,
33+
...props
34+
}: InputProps) {
35+
const inputProps = {
36+
...props,
37+
size: typeof size === "number" ? size : undefined,
38+
type,
39+
};
40+
41+
if (nativeInput) {
42+
return (
43+
<input
44+
className={cn(inputFieldClassName(size), className)}
45+
data-size={typeof size === "string" ? size : undefined}
46+
data-slot="input"
47+
{...inputProps}
48+
/>
49+
);
50+
}
51+
652
return (
7-
<input
8-
type={type}
9-
data-slot="input"
10-
className={cn(
11-
"flex h-10 w-full min-w-0 rounded-xl border border-border/80 bg-card/75 px-3 py-2 text-sm text-foreground shadow-[inset_0_1px_0_hsl(0_0%_100%/0.03)] transition-all duration-200 outline-none file:inline-flex file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground/65 hover:border-border disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50",
12-
"focus-visible:border-primary/50 focus-visible:bg-card focus-visible:ring-2 focus-visible:ring-ring/70 focus-visible:ring-offset-2 focus-visible:ring-offset-background",
13-
className,
14-
)}
15-
{...props}
16-
/>
53+
<span
54+
className={
55+
cn(
56+
!unstyled &&
57+
"relative inline-flex w-full rounded-lg border border-input bg-background not-dark:bg-clip-padding text-base text-foreground shadow-xs/5 ring-ring/24 transition-shadow before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] has-focus-visible:has-aria-invalid:border-destructive/64 has-focus-visible:has-aria-invalid:ring-destructive/16 has-aria-invalid:border-destructive/36 has-focus-visible:border-ring has-disabled:opacity-64 has-[:disabled,:focus-visible,[aria-invalid]]:shadow-none has-focus-visible:ring-[3px] not-has-disabled:has-not-focus-visible:not-has-aria-invalid:before:shadow-[0_1px_--theme(--color-black/4%)] sm:text-sm dark:bg-input/32 dark:has-aria-invalid:ring-destructive/24 dark:not-has-disabled:has-not-focus-visible:not-has-aria-invalid:before:shadow-[0_-1px_--theme(--color-white/6%)]",
58+
className,
59+
) || undefined
60+
}
61+
data-size={typeof size === "string" ? size : undefined}
62+
data-slot="input-control"
63+
>
64+
<FieldPrimitive.Control
65+
render={(defaultProps) => (
66+
<input
67+
className={inputFieldClassName(size)}
68+
data-slot="input"
69+
{...mergeProps<"input">(defaultProps, inputProps)}
70+
/>
71+
)}
72+
/>
73+
</span>
1774
);
1875
}
1976

20-
export { Input };
77+
export { Input, type InputProps };

0 commit comments

Comments
 (0)