veza/apps/web/desy/components/ui/button.tsx
2026-01-22 17:23:11 +01:00

133 lines
4.8 KiB
TypeScript

import * as React 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-semibold tracking-wide uppercase transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:ring-ring/50 focus-visible:ring-[3px]",
{
variants: {
variant: {
// Primary: Neon Cyan glow
default:
'bg-primary text-primary-foreground shadow-sm hover:bg-primary/90 hover:shadow-[0_0_20px_oklch(0.75_0.18_195_/_0.4)]',
// Secondary: Hot Magenta glow
secondary:
'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/90 hover:shadow-[0_0_20px_oklch(0.65_0.25_330_/_0.4)]',
// Destructive: Error state
destructive:
'bg-destructive text-white shadow-sm hover:bg-destructive/90 hover:shadow-[0_0_15px_oklch(0.60_0.22_25_/_0.3)] focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40',
// Outline: Bordered with hover fill
outline:
'border border-border bg-transparent text-foreground hover:bg-accent hover:text-accent-foreground hover:border-primary dark:border-border dark:hover:border-primary',
// Ghost: Minimal, text only
ghost:
'text-muted-foreground hover:bg-accent hover:text-foreground',
// Link: Underlined text
link:
'text-primary underline-offset-4 hover:underline normal-case font-medium tracking-normal',
// Success: Lime green glow
success:
'bg-success text-success-foreground shadow-sm hover:bg-success/90 hover:shadow-[0_0_20px_oklch(0.72_0.19_145_/_0.4)]',
// Gaming: XP Gold style with depth
gaming:
'bg-gradient-to-b from-[#2a2a2a] to-[#1a1a1a] text-[oklch(0.88_0.16_85)] border-2 border-[oklch(0.88_0.16_85)] shadow-[inset_0_1px_0_rgba(255,255,255,0.1),inset_0_-2px_0_rgba(0,0,0,0.3),0_2px_8px_rgba(0,0,0,0.5)] hover:shadow-[inset_0_1px_0_rgba(255,255,255,0.1),inset_0_-2px_0_rgba(0,0,0,0.3),0_0_20px_oklch(0.88_0.16_85_/_0.4)] font-mono',
// Nature: Organic moss green
nature:
'bg-gradient-to-br from-[oklch(0.40_0.08_145)] to-[oklch(0.55_0.12_145)] text-white shadow-[0_4px_15px_oklch(0.40_0.08_145_/_0.4)] hover:shadow-[0_6px_25px_oklch(0.40_0.08_145_/_0.5)] hover:scale-[1.02] rounded-full normal-case',
// Hacker: Terminal style
hacker:
'bg-black text-[oklch(0.72_0.19_145)] border border-[oklch(0.72_0.19_145)] font-mono normal-case tracking-normal hover:bg-[oklch(0.72_0.19_145)] hover:text-black hover:shadow-[0_0_20px_oklch(0.72_0.19_145_/_0.4)]',
},
size: {
default: 'h-10 px-5 py-2',
sm: 'h-8 px-3 text-xs',
lg: 'h-12 px-8 text-base',
xl: 'h-14 px-10 text-lg',
icon: 'size-10',
'icon-sm': 'size-8',
'icon-lg': 'size-12',
},
shape: {
default: 'rounded-md',
pill: 'rounded-full',
manga: 'rounded-none [clip-path:polygon(0_0,calc(100%-10px)_0,100%_10px,100%_100%,10px_100%,0_calc(100%-10px))]',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
shape: 'default',
},
},
)
interface ButtonProps
extends React.ComponentProps<'button'>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
loading?: boolean
}
function Button({
className,
variant,
size,
shape,
asChild = false,
loading = false,
children,
disabled,
...props
}: ButtonProps) {
const Comp = asChild ? Slot : 'button'
return (
<Comp
data-slot="button"
className={cn(buttonVariants({ variant, size, shape, className }))}
disabled={disabled || loading}
{...props}
>
{loading ? (
<>
<svg
className="size-4 animate-spin"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
/>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
<span>Loading...</span>
</>
) : (
children
)}
</Comp>
)
}
export { Button, buttonVariants }