Build & Push Docker Image / build (push) Has been cancelled
Refresh the homepage around new hero/services/roadmap sections, shared design tokens, and updated brand assets. Co-authored-by: Cursor <cursoragent@cursor.com>
149 lines
5.6 KiB
TypeScript
149 lines
5.6 KiB
TypeScript
import * as React from "react"
|
|
import { Slot } from "@radix-ui/react-slot"
|
|
import { cva, type VariantProps } from "class-variance-authority"
|
|
|
|
import { type as typeScale } from "@/lib/typography"
|
|
import { cn } from "@/lib/utils"
|
|
|
|
/**
|
|
* Unified button system
|
|
*
|
|
* Variants (marketing hierarchy):
|
|
* - default → Primary (main CTA / conversion)
|
|
* - secondary → Secondary (bordered supporting action)
|
|
* - ghost → Ghost (minimal / nav)
|
|
*
|
|
* Aliases kept for shadcn / existing blocks:
|
|
* - outline → same as secondary
|
|
*
|
|
* Utility variants:
|
|
* - destructive, link
|
|
*
|
|
* Surfaces:
|
|
* - default → normal page canvas
|
|
* - brand → teal / dark marketing panels (inverse contrast)
|
|
*
|
|
* Sizes: sm | default (medium) | lg | icon-sm | icon | icon-lg
|
|
*
|
|
* Persian text: padding and height are sized for IRANSansX so glyphs
|
|
* are not clipped; labels use `type.button` / `type["button-lg"]`.
|
|
*/
|
|
const buttonVariants = cva(
|
|
[
|
|
"inline-flex items-center justify-center whitespace-nowrap",
|
|
"rounded-[var(--btn-radius)]",
|
|
"transition-[color,background-color,border-color,box-shadow,opacity,transform]",
|
|
"duration-200 ease-out",
|
|
"outline-none select-none shrink-0",
|
|
"disabled:pointer-events-none disabled:opacity-50 disabled:shadow-none",
|
|
"[&_svg]:pointer-events-none [&_svg]:shrink-0",
|
|
"[&_svg:not([class*='size-'])]:size-[var(--btn-icon-size)]",
|
|
"focus-visible:ring-[3px] focus-visible:ring-ring/40 focus-visible:border-ring",
|
|
"active:scale-[0.98]",
|
|
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
|
].join(" "),
|
|
{
|
|
variants: {
|
|
variant: {
|
|
// Primary — brand neon rim via shadow-glow-*
|
|
default:
|
|
"bg-primary text-primary-foreground shadow-glow-sm hover:bg-primary-hover hover:shadow-glow-primary active:bg-primary-active",
|
|
// Secondary (bordered) — preferred name
|
|
secondary:
|
|
"border border-surface bg-transparent text-foreground shadow-surface-sm hover:border-primary/45 hover:bg-accent hover:text-accent-foreground hover:shadow-glow-sm active:bg-accent/80 dark:border-white/18 dark:hover:border-primary/50 dark:hover:bg-accent",
|
|
// Alias of secondary (shadcn / imported blocks)
|
|
outline:
|
|
"border border-surface bg-transparent text-foreground shadow-surface-sm hover:border-primary/45 hover:bg-accent hover:text-accent-foreground hover:shadow-glow-sm active:bg-accent/80 dark:border-white/18 dark:hover:border-primary/50 dark:hover:bg-accent",
|
|
// Ghost
|
|
ghost:
|
|
"bg-transparent text-foreground hover:bg-accent hover:text-accent-foreground active:bg-accent/80 dark:hover:bg-white/8",
|
|
destructive:
|
|
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90 hover:shadow-md focus-visible:ring-destructive/25 dark:focus-visible:ring-destructive/40 active:bg-destructive/85",
|
|
link:
|
|
"bg-transparent text-primary underline-offset-4 hover:underline hover:text-primary-hover active:text-primary-active",
|
|
},
|
|
surface: {
|
|
default: "",
|
|
/** Inverse contrast for brand / dark marketing panels */
|
|
brand: "",
|
|
},
|
|
size: {
|
|
sm: cn(
|
|
"h-[var(--btn-height-sm)] gap-[var(--btn-gap-sm)] px-[var(--btn-px-sm)] [--btn-icon-size:0.875rem] has-[>svg]:px-[calc(var(--btn-px-sm)-0.125rem)]",
|
|
typeScale.button
|
|
),
|
|
default: cn(
|
|
"h-[var(--btn-height-md)] gap-[var(--btn-gap-md)] px-[var(--btn-px-md)] [--btn-icon-size:1rem] has-[>svg]:px-[calc(var(--btn-px-md)-0.25rem)]",
|
|
typeScale.button
|
|
),
|
|
lg: cn(
|
|
"h-[var(--btn-height-lg)] gap-[var(--btn-gap-lg)] px-[var(--btn-px-lg)] [--btn-icon-size:1.25rem] has-[>svg]:px-[calc(var(--btn-px-lg)-0.25rem)]",
|
|
typeScale["button-lg"]
|
|
),
|
|
"icon-sm":
|
|
"size-[var(--btn-height-sm)] gap-0 p-0 [--btn-icon-size:0.875rem]",
|
|
icon: "size-[var(--btn-height-md)] gap-0 p-0 [--btn-icon-size:1rem]",
|
|
"icon-lg":
|
|
"size-[var(--btn-height-lg)] gap-0 p-0 [--btn-icon-size:1.25rem]",
|
|
},
|
|
},
|
|
compoundVariants: [
|
|
{
|
|
variant: "default",
|
|
surface: "brand",
|
|
className:
|
|
"bg-white text-brand-dark shadow-glow-sm hover:bg-brand-light hover:text-brand-dark hover:shadow-glow-primary active:bg-brand-light/90",
|
|
},
|
|
{
|
|
variant: ["secondary", "outline"],
|
|
surface: "brand",
|
|
className:
|
|
"border-white/40 bg-transparent text-white shadow-none hover:border-white/60 hover:bg-white/10 hover:text-white hover:shadow-[0_0_0_1px_rgb(255_255_255_/_0.35),0_0_18px_-4px_rgb(255_255_255_/_0.35)] active:bg-white/15",
|
|
},
|
|
{
|
|
variant: "ghost",
|
|
surface: "brand",
|
|
className:
|
|
"text-white hover:bg-white/10 hover:text-white active:bg-white/15",
|
|
},
|
|
{
|
|
variant: "link",
|
|
surface: "brand",
|
|
className: "text-white hover:text-white/90",
|
|
},
|
|
],
|
|
defaultVariants: {
|
|
variant: "default",
|
|
surface: "default",
|
|
size: "default",
|
|
},
|
|
}
|
|
)
|
|
|
|
function Button({
|
|
className,
|
|
variant,
|
|
size,
|
|
surface,
|
|
asChild = false,
|
|
...props
|
|
}: React.ComponentProps<"button"> &
|
|
VariantProps<typeof buttonVariants> & {
|
|
asChild?: boolean
|
|
}) {
|
|
const Comp = asChild ? Slot : "button"
|
|
|
|
return (
|
|
<Comp
|
|
data-slot="button"
|
|
data-variant={variant ?? "default"}
|
|
data-surface={surface ?? "default"}
|
|
data-size={size ?? "default"}
|
|
className={cn(buttonVariants({ variant, size, surface }), className)}
|
|
{...props}
|
|
/>
|
|
)
|
|
}
|
|
|
|
export { Button, buttonVariants }
|