import { Slot } from "@radix-ui/react-slot"; import { cva, type VariantProps } from "class-variance-authority"; import * as React from "react"; import { useEffect, useMemo, useState } from "react"; import { Counter, Icon, LinkWrapper, LinkWrapperProps, Spinner, TooltipContent, TooltipPortal, TooltipProvider, TooltipRoot, TooltipTrigger, } from "@sparkle/components/"; import { SpinnerProps } from "@sparkle/components/Spinner"; import { ChevronDownIcon } from "@sparkle/icons/app"; import { cn } from "@sparkle/lib/utils"; const PULSE_ANIMATION_DURATION = 1; export const BUTTON_VARIANTS = [ "primary", "highlight", "highlight-secondary", "warning", "warning-secondary", "outline", "ghost", "ghost-secondary", ] as const; export type ButtonVariantType = (typeof BUTTON_VARIANTS)[number]; export const BUTTON_SIZES = ["xmini", "mini", "xs", "sm", "md"] as const; export type ButtonSizeType = (typeof BUTTON_SIZES)[number]; // Define button styling with cva const buttonVariants = cva( cn( "s-inline-flex s-items-center s-justify-center s-whitespace-nowrap s-ring-offset-background s-transition-colors s-ring-inset s-select-none", "focus-visible:s-outline-none focus-visible:s-ring-2 focus-visible:s-ring-ring focus-visible:s-ring-offset-0", "dark:focus-visible:s-ring-0 dark:focus-visible:s-ring-offset-1" ), { variants: { variant: { primary: cn( "s-bg-primary-800 dark:s-bg-primary-800-night", "s-text-primary-50 dark:s-text-primary-50-night", "hover:s-bg-primary-light dark:hover:s-bg-primary-dark-night", "active:s-bg-primary-dark dark:active:s-bg-primary-light-night", "disabled:s-bg-primary-muted disabled:s-text-highlight-50/60 dark:disabled:s-bg-primary-muted-night" ), highlight: cn( "s-bg-highlight", "s-text-highlight-50", "hover:s-bg-highlight-light", "active:s-bg-highlight-dark", "disabled:s-bg-highlight-muted disabled:s-text-highlight-50/60 dark:disabled:s-bg-highlight-muted-night" ), "highlight-secondary": cn( "s-border", "s-border-border dark:s-border-border-night", "s-text-highlight-500 dark:s-text-highlight-500-night", "s-bg-background dark:s-bg-background-night", "hover:s-text-highlight-500 dark:hover:s-text-highlight-500-night", "hover:s-bg-highlight-50 dark:hover:s-bg-highlight-900", "hover:s-border-primary-150 dark:hover:s-border-border-night", "active:s-bg-primary-300 dark:active:s-bg-primary-900", "disabled:s-text-primary-muted dark:disabled:s-text-primary-muted-night", "disabled:s-border-primary-100 dark:disabled:s-border-primary-100-night", "disabled:hover:s-bg-background dark:disabled:hover:s-bg-background-night", "disabled:hover:s-border-primary-100 dark:disabled:hover:s-border-primary-100-night", "disabled:hover:s-text-primary-muted dark:disabled:hover:s-text-primary-muted-night" ), warning: cn( "s-bg-warning", "s-text-warning-50", "hover:s-bg-warning-light", "active:s-bg-warning-dark", "disabled:s-bg-warning-muted disabled:s-text-highlight-50/60 dark:disabled:s-bg-warning-muted-night" ), "warning-secondary": cn( "s-border", "s-border-border dark:s-border-border-night", "s-text-warning-500 dark:s-text-warning-500-night", "s-bg-background dark:s-bg-background-night", "hover:s-text-warning-500 dark:hover:s-text-warning-500-night", "hover:s-bg-warning-50 dark:hover:s-bg-warning-900", "hover:s-border-primary-150 dark:hover:s-border-border-night", "active:s-bg-primary-300 dark:active:s-bg-primary-900", "disabled:s-text-primary-muted dark:disabled:s-text-primary-muted-night", "disabled:s-border-primary-100 dark:disabled:s-border-primary-100-night", "disabled:hover:s-bg-background dark:disabled:hover:s-bg-background-night", "disabled:hover:s-border-primary-100 dark:disabled:hover:s-border-primary-100-night", "disabled:hover:s-text-primary-muted dark:disabled:hover:s-text-primary-muted-night" ), outline: cn( "s-border", "s-border-border dark:s-border-border-night", "s-text-primary dark:s-text-primary-night", "s-bg-background dark:s-bg-background-night", "hover:s-text-primary dark:hover:s-text-primary-night", "hover:s-bg-primary-100 dark:hover:s-bg-primary-900", "hover:s-border-primary-150 dark:hover:s-border-border-night", "active:s-bg-primary-300 dark:active:s-bg-primary-900", "disabled:s-text-primary-muted dark:disabled:s-text-primary-muted-night", "disabled:s-border-primary-100 dark:disabled:s-border-primary-100-night", "disabled:hover:s-bg-background dark:disabled:hover:s-bg-background-night", "disabled:hover:s-border-primary-100 dark:disabled:hover:s-border-primary-100-night", "disabled:hover:s-text-primary-muted dark:disabled:hover:s-text-primary-muted-night" ), ghost: cn( "s-border", "s-border-border/0 dark:s-border-border-night/0", "s-text-foreground dark:s-text-white", "hover:s-bg-primary-100 dark:hover:s-bg-primary-900", "hover:s-text-primary-900 dark:hover:s-text-white", "hover:s-border-border-dark dark:hover:s-border-border-night", "active:s-bg-primary-300 dark:active:s-bg-primary-900", "disabled:s-text-primary-400 dark:disabled:s-text-primary-400-night", "disabled:hover:s-bg-transparent dark:disabled:hover:s-bg-transparent", "disabled:hover:s-border-border/0 dark:disabled:hover:s-border-border-night/0", "disabled:hover:s-text-primary-400 dark:disabled:hover:s-text-primary-400-night" ), "ghost-secondary": cn( "s-border", "s-border-border/0 dark:s-border-border-night/0", "s-text-muted-foreground dark:s-text-muted-foreground-night", "hover:s-bg-primary-100 dark:hover:s-bg-primary-900", "hover:s-text-primary-900 dark:hover:s-text-primary-900-night", "hover:s-border-border-dark dark:hover:s-border-border-night", "active:s-bg-primary-300 dark:active:s-bg-primary-900", "disabled:s-text-primary-400 dark:disabled:s-text-primary-400-night", "disabled:hover:s-bg-transparent dark:disabled:hover:s-bg-transparent", "disabled:hover:s-border-border/0 dark:disabled:hover:s-border-border-night/0", "disabled:hover:s-text-primary-400 dark:disabled:hover:s-text-primary-400-night" ), }, size: { xmini: "s-h-6 s-w-6 s-label-xs s-gap-1 s-shrink-0", mini: "s-h-7 s-w-7 s-label-xs s-gap-1.5 s-shrink-0", xs: "s-h-7 s-px-2.5 s-label-xs s-gap-1.5 s-shrink-0", sm: "s-h-9 s-px-3 s-label-sm s-gap-2 s-shrink-0", md: "s-h-12 s-px-4 s-py-2 s-label-base s-gap-2.5 s-shrink-0", }, rounded: { xmini: "s-rounded-lg", mini: "s-rounded-lg", xs: "s-rounded-lg", sm: "s-rounded-xl", md: "s-rounded-2xl", full: "s-rounded-full", }, }, defaultVariants: { variant: "primary", size: "sm", rounded: "sm", }, } ); const labelVariants = cva("", { variants: { size: { xmini: "s-label-xs s-hidden", mini: "s-label-xs s-hidden", xs: "s-label-xs", sm: "s-label-sm", md: "s-label-base", }, }, defaultVariants: { size: "sm", }, }); type SpinnerVariant = NonNullable; const spinnerVariantsMap: Record = { primary: "revert", highlight: "light", "highlight-secondary": "mono", warning: "light", "warning-secondary": "mono", outline: "mono", ghost: "mono", "ghost-secondary": "mono", }; const chevronVariantMap = { primary: "s-text-muted-foreground-night dark:s-text-muted-foreground", outline: "s-text-faint", ghost: "s-text-faint", "ghost-secondary": "s-text-faint", highlight: "s-text-white/60", "highlight-secondary": "s-text-highlight-500 dark:s-text-highlight-500-night", warning: "s-text-white/60", "warning-secondary": "s-text-warning-500 dark:s-text-warning-500-night", } as const; export interface MetaButtonProps extends React.ButtonHTMLAttributes, VariantProps { asChild?: boolean; isRounded?: boolean; } const MetaButton = React.forwardRef( ( { className, asChild = false, variant, size, isRounded, children, ...props }, ref ) => { const Comp = asChild ? Slot : "button"; // Determine rounded variant based on isRounded prop const rounded = isRounded ? "full" : size; return ( {children} ); } ); MetaButton.displayName = "MetaButton"; type IconSizeType = "xs" | "sm" | "md"; type CounterSizeType = "xs" | "sm" | "md"; export const ICON_SIZE_MAP: Record = { xmini: "xs", mini: "sm", xs: "xs", sm: "sm", md: "md", }; const COUNTER_SIZE_MAP: Record = { xmini: "xs", mini: "xs", xs: "xs", sm: "sm", md: "md", }; type CommonButtonProps = Omit & Omit & { isSelect?: boolean; isLoading?: boolean; isPulsing?: boolean; briefPulse?: boolean; tooltip?: string; isCounter?: boolean; counterValue?: string; isRounded?: boolean; }; export type MiniButtonProps = CommonButtonProps & { size: "mini"; icon: React.ComponentType; label?: never; }; export type RegularButtonProps = CommonButtonProps & { size?: Exclude; icon?: React.ComponentType; label?: string; }; export type ButtonProps = MiniButtonProps | RegularButtonProps; const Button = React.forwardRef( ( { label, icon, className, isLoading = false, variant = "primary", tooltip, isSelect = false, isPulsing = false, briefPulse = false, isCounter = false, counterValue, size = "sm", isRounded = false, href, target, rel, replace, shallow, "aria-label": ariaLabel, ...props }, ref ) => { const iconSize = ICON_SIZE_MAP[size]; const counterSize = COUNTER_SIZE_MAP[size]; const [isPulsingBriefly, setIsPulsingBriefly] = useState(false); useEffect(() => { if (!briefPulse) { return; } const startPulse = () => { setIsPulsingBriefly(true); setTimeout( () => setIsPulsingBriefly(false), PULSE_ANIMATION_DURATION * 3000 ); }; startPulse(); }, [briefPulse]); const renderIcon = (visual: React.ComponentType, extraClass = "") => ( ); const renderChevron = (visual: React.ComponentType, extraClass = "") => ( ); const showCounter = isCounter && counterValue != null; const showContainer = label || showCounter; const content = ( <> {isLoading ? (
) : ( icon && renderIcon(icon, "-s-mx-0.5") )} {showContainer && (
{label} {showCounter && ( )}
)} {isSelect && renderChevron(ChevronDownIcon, isLoading ? "" : "-s-mr-1")} ); const pointerEventProps = useMemo(() => { if (isLoading || props.disabled) { return { onPointerDown: (e: React.PointerEvent) => { e.preventDefault(); e.stopPropagation(); }, }; } return {}; }, [isLoading, props.disabled]); // We cannot skip a button tag when it's disabled. We need // to apply disabled class manually (currently it has :disabled pseudo-class, which won't work if it's not a button) // and disable pointer events. const shouldUseSlot = !!href && !props.disabled; const innerContent = shouldUseSlot ? {content} : content; const innerButton = ( {innerContent} ); const wrappedContent = tooltip ? ( {innerButton} {tooltip} ) : ( innerButton ); return href ? ( {wrappedContent} ) : ( wrappedContent ); } ); Button.displayName = "Button"; export { Button, buttonVariants, MetaButton };