refactor: replace MDC components with Tailwind-based implementations

Co-authored-by: aider (openrouter/anthropic/claude-sonnet-4.5) <aider@aider.chat>
This commit is contained in:
2026-01-20 16:10:31 +00:00
parent 4811583ecf
commit 3962d16017
9 changed files with 501 additions and 94 deletions

View File

@@ -1,29 +1,56 @@
import { ComponentProps } from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
type AlertVariant = "default" | "destructive"
const alertVariants = cva(
"relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
{
variants: {
variant: {
default: "bg-background text-foreground",
destructive:
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
},
},
defaultVariants: {
variant: "default",
},
}
)
function Alert({ className, variant = "default", children, ...props }: ComponentProps<"div"> & { variant?: AlertVariant }) {
function Alert({
className,
variant,
...props
}: ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
return (
<div
role="alert"
className={["mdc-banner", variant === "destructive" && "mdc-banner--error", className].filter(Boolean).join(" ")}
data-slot="alert"
className={cn(alertVariants({ variant }), className)}
{...props}
>
<div className="mdc-banner__content">
<div className="mdc-banner__graphic-text-wrapper">
<div className="mdc-banner__text">{children}</div>
</div>
</div>
</div>
/>
)
}
function AlertTitle({ className, ...props }: ComponentProps<"div">) {
return <div className={["mdc-typography--subtitle1", className].filter(Boolean).join(" ")} {...props} />
function AlertTitle({ className, ...props }: ComponentProps<"h5">) {
return (
<h5
data-slot="alert-title"
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
{...props}
/>
)
}
function AlertDescription({ className, ...props }: ComponentProps<"div">) {
return <div className={["mdc-typography--body2", className].filter(Boolean).join(" ")} {...props} />
return (
<div
data-slot="alert-description"
className={cn("text-sm [&_p]:leading-relaxed", className)}
{...props}
/>
)
}
export { Alert, AlertTitle, AlertDescription }

View File

@@ -1 +1,57 @@
import styles from '@styles/m3-scss/button.module.scss';
import { ComponentProps, forwardRef } from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
tonal: "bg-accent text-accent-foreground hover:bg-accent/80",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
export interface ButtonProps
extends ComponentProps<"button">,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
export { Button, buttonVariants }

View File

@@ -1,27 +1,77 @@
import { ComponentProps } from "react"
import { ComponentProps, forwardRef } from "react"
import { cn } from "@/lib/utils"
function Card({ className, ...props }: ComponentProps<"div">) {
return <div className={["mdc-card", className].filter(Boolean).join(" ")} {...props} />
}
const Card = forwardRef<HTMLDivElement, ComponentProps<"div">>(
({ className, ...props }, ref) => (
<div
ref={ref}
data-slot="card"
className={cn(
"rounded-lg border bg-card text-card-foreground shadow-sm",
className
)}
{...props}
/>
)
)
Card.displayName = "Card"
function CardHeader(props: ComponentProps<"div">) {
return <div className="mdc-card__primary-action" {...props} />
}
const CardHeader = forwardRef<HTMLDivElement, ComponentProps<"div">>(
({ className, ...props }, ref) => (
<div
ref={ref}
data-slot="card-header"
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
)
)
CardHeader.displayName = "CardHeader"
function CardTitle(props: ComponentProps<"div">) {
return <h3 className="mdc-typography--headline6" {...props} />
}
const CardTitle = forwardRef<HTMLParagraphElement, ComponentProps<"h3">>(
({ className, ...props }, ref) => (
<h3
ref={ref}
data-slot="card-title"
className={cn(
"text-2xl font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
)
)
CardTitle.displayName = "CardTitle"
function CardContent(props: ComponentProps<"div">) {
return <div className="mdc-card__content" {...props} />
}
const CardDescription = forwardRef<HTMLParagraphElement, ComponentProps<"p">>(
({ className, ...props }, ref) => (
<p
ref={ref}
data-slot="card-description"
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
)
)
CardDescription.displayName = "CardDescription"
function CardFooter(props: ComponentProps<"div">) {
return <div className="mdc-card__actions" {...props} />
}
const CardContent = forwardRef<HTMLDivElement, ComponentProps<"div">>(
({ className, ...props }, ref) => (
<div ref={ref} data-slot="card-content" className={cn("p-6 pt-0", className)} {...props} />
)
)
CardContent.displayName = "CardContent"
function CardAction(props: ComponentProps<"button">) {
return <button className="mdc-button mdc-card__action mdc-card__action--button" {...props} />
}
const CardFooter = forwardRef<HTMLDivElement, ComponentProps<"div">>(
({ className, ...props }, ref) => (
<div
ref={ref}
data-slot="card-footer"
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
)
)
CardFooter.displayName = "CardFooter"
export { Card, CardHeader, CardTitle, CardContent, CardFooter, CardAction }
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }

View File

@@ -1,19 +1,43 @@
import { ComponentProps } from "react"
import { ComponentProps, forwardRef } from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
function Chip({ className, children, ...props }: ComponentProps<"button">) {
return (
<button
className={["mdc-evolution-chip", className].filter(Boolean).join(" ")}
{...props}
>
<span className="mdc-evolution-chip__cell mdc-evolution-chip__cell--primary">
<span className="mdc-evolution-chip__action mdc-evolution-chip__action--primary">
<span className="mdc-evolution-chip__ripple" />
<span className="mdc-evolution-chip__text-label">{children}</span>
</span>
</span>
</button>
)
}
const chipVariants = cva(
"inline-flex items-center justify-center gap-1.5 rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
secondary:
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive:
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
outline: "text-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)
export { Chip }
interface ChipProps
extends ComponentProps<"div">,
VariantProps<typeof chipVariants> {}
const Chip = forwardRef<HTMLDivElement, ChipProps>(
({ className, variant, ...props }, ref) => {
return (
<div
ref={ref}
data-slot="chip"
className={cn(chipVariants({ variant }), className)}
{...props}
/>
)
}
)
Chip.displayName = "Chip"
export { Chip, chipVariants }

View File

@@ -1 +1,41 @@
import styles from '@styles/m3-scss/hover-card.module.scss';
"use client"
import { ComponentProps } from "react"
import * as HoverCardPrimitive from "@radix-ui/react-hover-card"
import { cn } from "@/lib/utils"
function HoverCard({
...props
}: ComponentProps<typeof HoverCardPrimitive.Root>) {
return <HoverCardPrimitive.Root data-slot="hover-card" {...props} />
}
function HoverCardTrigger({
...props
}: ComponentProps<typeof HoverCardPrimitive.Trigger>) {
return (
<HoverCardPrimitive.Trigger data-slot="hover-card-trigger" {...props} />
)
}
function HoverCardContent({
className,
align = "center",
sideOffset = 4,
...props
}: ComponentProps<typeof HoverCardPrimitive.Content>) {
return (
<HoverCardPrimitive.Content
data-slot="hover-card-content"
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none 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 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",
className
)}
{...props}
/>
)
}
export { HoverCard, HoverCardTrigger, HoverCardContent }

View File

@@ -1 +1,30 @@
export {}
"use client"
import { ComponentProps } from "react"
import * as ProgressPrimitive from "@radix-ui/react-progress"
import { cn } from "@/lib/utils"
function Progress({
className,
value,
...props
}: ComponentProps<typeof ProgressPrimitive.Root>) {
return (
<ProgressPrimitive.Root
data-slot="progress"
className={cn(
"relative h-4 w-full overflow-hidden rounded-full bg-secondary",
className
)}
{...props}
>
<ProgressPrimitive.Indicator
data-slot="progress-indicator"
className="h-full w-full flex-1 bg-primary transition-all"
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
</ProgressPrimitive.Root>
)
}
export { Progress }

View File

@@ -1 +1,163 @@
import styles from '@styles/m3-scss/sheet.module.scss';
"use client"
import { ComponentProps } from "react"
import * as SheetPrimitive from "@radix-ui/react-dialog"
import { cva, type VariantProps } from "class-variance-authority"
import XIcon from "lucide-react/dist/esm/icons/x"
import { cn } from "@/lib/utils"
function Sheet({ ...props }: ComponentProps<typeof SheetPrimitive.Root>) {
return <SheetPrimitive.Root data-slot="sheet" {...props} />
}
function SheetTrigger({
...props
}: ComponentProps<typeof SheetPrimitive.Trigger>) {
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />
}
function SheetClose({
...props
}: ComponentProps<typeof SheetPrimitive.Close>) {
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />
}
function SheetPortal({
...props
}: ComponentProps<typeof SheetPrimitive.Portal>) {
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />
}
function SheetOverlay({
className,
...props
}: ComponentProps<typeof SheetPrimitive.Overlay>) {
return (
<SheetPrimitive.Overlay
data-slot="sheet-overlay"
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
/>
)
}
const sheetVariants = cva(
"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out",
{
variants: {
side: {
top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
bottom:
"inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
right:
"inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
},
},
defaultVariants: {
side: "right",
},
}
)
interface SheetContentProps
extends ComponentProps<typeof SheetPrimitive.Content>,
VariantProps<typeof sheetVariants> {}
function SheetContent({
side = "right",
className,
children,
...props
}: SheetContentProps) {
return (
<SheetPortal>
<SheetOverlay />
<SheetPrimitive.Content
data-slot="sheet-content"
className={cn(sheetVariants({ side }), className)}
{...props}
>
{children}
<SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
<XIcon className="h-4 w-4" />
<span className="sr-only">Close</span>
</SheetPrimitive.Close>
</SheetPrimitive.Content>
</SheetPortal>
)
}
function SheetHeader({
className,
...props
}: ComponentProps<"div">) {
return (
<div
data-slot="sheet-header"
className={cn(
"flex flex-col space-y-2 text-center sm:text-left",
className
)}
{...props}
/>
)
}
function SheetFooter({
className,
...props
}: ComponentProps<"div">) {
return (
<div
data-slot="sheet-footer"
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
}
function SheetTitle({
className,
...props
}: ComponentProps<typeof SheetPrimitive.Title>) {
return (
<SheetPrimitive.Title
data-slot="sheet-title"
className={cn("text-lg font-semibold text-foreground", className)}
{...props}
/>
)
}
function SheetDescription({
className,
...props
}: ComponentProps<typeof SheetPrimitive.Description>) {
return (
<SheetPrimitive.Description
data-slot="sheet-description"
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
)
}
export {
Sheet,
SheetPortal,
SheetOverlay,
SheetTrigger,
SheetClose,
SheetContent,
SheetHeader,
SheetFooter,
SheetTitle,
SheetDescription,
}

View File

@@ -1,25 +1,27 @@
"use client"
import { ComponentProps } from "react"
import * as SliderPrimitive from "@radix-ui/react-slider"
import { cn } from "@/lib/utils"
interface SliderProps extends ComponentProps<"input"> {
min?: number
max?: number
step?: number
}
function Slider({ className, min = 0, max = 100, step = 1, ...props }: SliderProps) {
function Slider({
className,
...props
}: ComponentProps<typeof SliderPrimitive.Root>) {
return (
<div className={["mdc-slider", className].filter(Boolean).join(" ")} role="slider" aria-valuemin={min} aria-valuemax={max}>
<input type="range" min={min} max={max} step={step} className="mdc-slider__input" {...props} />
<div className="mdc-slider__track">
<div className="mdc-slider__track--inactive" />
<div className="mdc-slider__track--active">
<div className="mdc-slider__track--active_fill" />
</div>
</div>
<div className="mdc-slider__thumb">
<div className="mdc-slider__thumb-knob" />
</div>
</div>
<SliderPrimitive.Root
data-slot="slider"
className={cn(
"relative flex w-full touch-none select-none items-center",
className
)}
{...props}
>
<SliderPrimitive.Track className="relative h-2 w-full grow overflow-hidden rounded-full bg-secondary">
<SliderPrimitive.Range className="absolute h-full bg-primary" />
</SliderPrimitive.Track>
<SliderPrimitive.Thumb className="block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50" />
</SliderPrimitive.Root>
)
}

View File

@@ -2,41 +2,58 @@
import { ComponentProps } from "react"
import * as TabsPrimitive from "@radix-ui/react-tabs"
import { cn } from "@/lib/utils"
function Tabs(props: ComponentProps<typeof TabsPrimitive.Root>) {
return <TabsPrimitive.Root className="mdc-tab-bar" activationMode="automatic" {...props} />
return <TabsPrimitive.Root data-slot="tabs" {...props} />
}
function TabsList(props: ComponentProps<typeof TabsPrimitive.List>) {
function TabsList({
className,
...props
}: ComponentProps<typeof TabsPrimitive.List>) {
return (
<TabsPrimitive.List className="mdc-tab-scroller" {...props}>
<div className="mdc-tab-scroller__scroll-area">
<div className="mdc-tab-scroller__scroll-content">{props.children}</div>
</div>
</TabsPrimitive.List>
<TabsPrimitive.List
data-slot="tabs-list"
className={cn(
"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
className
)}
{...props}
/>
)
}
function TabsTrigger({ children, className, ...props }: ComponentProps<typeof TabsPrimitive.Trigger>) {
function TabsTrigger({
className,
...props
}: ComponentProps<typeof TabsPrimitive.Trigger>) {
return (
<TabsPrimitive.Trigger
className={["mdc-tab", className].filter(Boolean).join(" ")}
data-slot="tabs-trigger"
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
className
)}
{...props}
>
<span className="mdc-tab__content">
<span className="mdc-tab__text-label">{children}</span>
</span>
<span className="mdc-tab__ripple" />
<span className="mdc-tab__focus-ring" />
<span className="mdc-tab-indicator">
<span className="mdc-tab-indicator__content mdc-tab-indicator__content--underline" />
</span>
</TabsPrimitive.Trigger>
/>
)
}
function TabsContent(props: ComponentProps<typeof TabsPrimitive.Content>) {
return <TabsPrimitive.Content className="mdc-tab-panel" {...props} />
function TabsContent({
className,
...props
}: ComponentProps<typeof TabsPrimitive.Content>) {
return (
<TabsPrimitive.Content
data-slot="tabs-content"
className={cn(
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
className
)}
{...props}
/>
)
}
export { Tabs, TabsList, TabsTrigger, TabsContent }