129 lines
5.5 KiB
TypeScript
129 lines
5.5 KiB
TypeScript
import * as React from "react"
|
|
import { cva, type VariantProps } from "class-variance-authority"
|
|
import { Slot } from "@radix-ui/react-slot"
|
|
|
|
import { cn } from "@/lib/utils"
|
|
|
|
/**
|
|
* Premium luxury button system.
|
|
* Gold primary (metallic gradient), sapphire secondary (navy gradient), shared hover/active.
|
|
* Light reflection (::after) on primary and secondary.
|
|
*/
|
|
const buttonVariants = cva(
|
|
[
|
|
"inline-flex items-center justify-center gap-2 whitespace-nowrap text-sm font-medium",
|
|
"rounded-lg outline-none select-none",
|
|
"transition-all duration-[280ms] ease-out",
|
|
"hover:-translate-y-0.5 active:translate-y-0",
|
|
"disabled:pointer-events-none disabled:opacity-50 disabled:translate-y-0 disabled:hover:translate-y-0 disabled:active:translate-y-0",
|
|
"focus-visible:ring-[3px] focus-visible:ring-offset-2 focus-visible:ring-offset-background",
|
|
"[&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0",
|
|
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40",
|
|
].join(" "),
|
|
{
|
|
variants: {
|
|
variant: {
|
|
primary: [
|
|
"relative overflow-hidden",
|
|
"bg-[linear-gradient(180deg,var(--btn-gold-from)_0%,var(--btn-gold-via)_50%,var(--btn-gold-to)_100%)]",
|
|
"text-[var(--btn-gold-text)]",
|
|
"shadow-[var(--btn-gold-shadow)]",
|
|
"hover:shadow-[var(--btn-gold-shadow-hover)] hover:shadow-[var(--btn-gold-glow-hover)]",
|
|
"active:shadow-[var(--btn-gold-shadow-active)]",
|
|
"focus-visible:ring-primary/30",
|
|
"after:content-[''] after:absolute after:inset-0 after:rounded-lg after:overflow-hidden after:pointer-events-none",
|
|
"after:bg-[linear-gradient(105deg,transparent_0%,rgba(255,255,255,0.38)_50%,transparent_100%)]",
|
|
"after:opacity-0 hover:after:opacity-100 hover:after:animate-[btn-shine_0.55s_ease-out]",
|
|
].join(" "),
|
|
secondary: [
|
|
"relative overflow-hidden",
|
|
"bg-[linear-gradient(180deg,var(--btn-sapphire-from)_0%,var(--btn-sapphire-to)_100%)]",
|
|
"text-secondary-foreground",
|
|
"shadow-[var(--btn-sapphire-shadow)]",
|
|
"hover:shadow-[var(--btn-sapphire-shadow-hover)] hover:shadow-[var(--btn-sapphire-glow-hover)]",
|
|
"active:shadow-[var(--btn-sapphire-shadow-active)]",
|
|
"focus-visible:ring-secondary/30",
|
|
"after:content-[''] after:absolute after:inset-0 after:rounded-lg after:overflow-hidden after:pointer-events-none",
|
|
"after:bg-[linear-gradient(105deg,transparent_0%,rgba(255,255,255,0.25)_50%,transparent_100%)]",
|
|
"after:opacity-0 hover:after:opacity-100 hover:after:animate-[btn-shine_0.55s_ease-out]",
|
|
].join(" "),
|
|
outline: [
|
|
"border-2 border-secondary bg-transparent text-secondary",
|
|
"shadow-sm",
|
|
"hover:bg-secondary/10 hover:shadow-md hover:shadow-secondary/20",
|
|
"active:bg-secondary/15 active:shadow-sm",
|
|
"focus-visible:ring-secondary/30",
|
|
].join(" "),
|
|
ghost: [
|
|
"bg-transparent",
|
|
"hover:bg-accent hover:text-accent-foreground",
|
|
"active:bg-accent/80",
|
|
"focus-visible:ring-accent/30",
|
|
].join(" "),
|
|
link: [
|
|
"text-primary underline-offset-4 shadow-none",
|
|
"hover:underline hover:opacity-90 hover:-translate-y-0.5 active:translate-y-0",
|
|
"focus-visible:ring-primary/20",
|
|
].join(" "),
|
|
destructive: [
|
|
"bg-destructive text-destructive-foreground shadow-sm",
|
|
"hover:bg-destructive/90 hover:shadow-md",
|
|
"active:bg-destructive/95 active:shadow-sm active:translate-y-0",
|
|
"focus-visible:ring-destructive/30",
|
|
].join(" "),
|
|
default: [
|
|
"relative overflow-hidden",
|
|
"bg-[linear-gradient(180deg,var(--btn-gold-from)_0%,var(--btn-gold-via)_50%,var(--btn-gold-to)_100%)]",
|
|
"text-[var(--btn-gold-text)]",
|
|
"shadow-[var(--btn-gold-shadow)]",
|
|
"hover:shadow-[var(--btn-gold-shadow-hover)] hover:shadow-[var(--btn-gold-glow-hover)]",
|
|
"active:shadow-[var(--btn-gold-shadow-active)]",
|
|
"focus-visible:ring-primary/30",
|
|
"after:content-[''] after:absolute after:inset-0 after:rounded-lg after:overflow-hidden after:pointer-events-none",
|
|
"after:bg-[linear-gradient(105deg,transparent_0%,rgba(255,255,255,0.38)_50%,transparent_100%)]",
|
|
"after:opacity-0 hover:after:opacity-100 hover:after:animate-[btn-shine_0.55s_ease-out]",
|
|
].join(" "),
|
|
},
|
|
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 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
|
lg: "h-10 rounded-lg px-6 has-[>svg]:px-4",
|
|
icon: "size-9",
|
|
"icon-xs": "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3",
|
|
"icon-sm": "size-8",
|
|
"icon-lg": "size-10",
|
|
},
|
|
},
|
|
defaultVariants: {
|
|
variant: "default",
|
|
size: "default",
|
|
},
|
|
}
|
|
)
|
|
|
|
function Button({
|
|
className,
|
|
variant = "default",
|
|
size = "default",
|
|
asChild = false,
|
|
...props
|
|
}: React.ComponentProps<"button"> &
|
|
VariantProps<typeof buttonVariants> & {
|
|
asChild?: boolean
|
|
}) {
|
|
const Comp = asChild ? Slot : "button"
|
|
|
|
return (
|
|
<Comp
|
|
data-slot="button"
|
|
data-variant={variant}
|
|
data-size={size}
|
|
className={cn(buttonVariants({ variant, size, className }))}
|
|
{...props}
|
|
/>
|
|
)
|
|
}
|
|
|
|
export { Button, buttonVariants }
|