diff --git a/apps/ui/src/components/ui/button.tsx b/apps/ui/src/components/ui/button.tsx index a7163ed3..683d1237 100644 --- a/apps/ui/src/components/ui/button.tsx +++ b/apps/ui/src/components/ui/button.tsx @@ -3,7 +3,7 @@ import { Slot } from '@radix-ui/react-slot'; import { cva, type VariantProps } from 'class-variance-authority'; import { cn } from '@/lib/utils'; -import { Spinner } from '@/components/ui/spinner'; +import { Spinner, type SpinnerVariant } from '@/components/ui/spinner'; const buttonVariants = cva( "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all duration-200 cursor-pointer disabled:pointer-events-none disabled:opacity-50 disabled:cursor-not-allowed [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive active:scale-[0.98]", @@ -37,9 +37,19 @@ const buttonVariants = cva( } ); -// Loading spinner component -function ButtonSpinner({ className }: { className?: string }) { - return ; +/** Button variants that have colored backgrounds requiring foreground spinner color */ +const COLORED_BACKGROUND_VARIANTS = new Set(['default', 'destructive']); + +/** Get spinner variant based on button variant - use foreground for colored backgrounds */ +function getSpinnerVariant( + buttonVariant: VariantProps['variant'] +): SpinnerVariant { + const variant = buttonVariant ?? 'default'; + if (COLORED_BACKGROUND_VARIANTS.has(variant)) { + return 'foreground'; + } + // outline, secondary, ghost, link, animated-outline use standard backgrounds + return 'primary'; } function Button({ @@ -57,6 +67,7 @@ function Button({ loading?: boolean; }) { const isDisabled = disabled || loading; + const spinnerVariant = getSpinnerVariant(variant); // Special handling for animated-outline variant if (variant === 'animated-outline' && !asChild) { @@ -83,7 +94,7 @@ function Button({ size === 'icon' && 'p-0 gap-0' )} > - {loading && } + {loading && } {children} @@ -99,7 +110,7 @@ function Button({ disabled={isDisabled} {...props} > - {loading && } + {loading && } {children} ); diff --git a/apps/ui/src/components/ui/spinner.tsx b/apps/ui/src/components/ui/spinner.tsx index c66b7684..d515dc7b 100644 --- a/apps/ui/src/components/ui/spinner.tsx +++ b/apps/ui/src/components/ui/spinner.tsx @@ -1,7 +1,8 @@ import { Loader2 } from 'lucide-react'; import { cn } from '@/lib/utils'; -type SpinnerSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl'; +export type SpinnerSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl'; +export type SpinnerVariant = 'primary' | 'foreground' | 'muted'; const sizeClasses: Record = { xs: 'h-3 w-3', @@ -11,9 +12,17 @@ const sizeClasses: Record = { xl: 'h-8 w-8', }; +const variantClasses: Record = { + primary: 'text-primary', + foreground: 'text-primary-foreground', + muted: 'text-muted-foreground', +}; + interface SpinnerProps { /** Size of the spinner */ size?: SpinnerSize; + /** Color variant - use 'foreground' when on primary backgrounds */ + variant?: SpinnerVariant; /** Additional class names */ className?: string; } @@ -21,11 +30,12 @@ interface SpinnerProps { /** * Themed spinner component using the primary brand color. * Use this for all loading indicators throughout the app for consistency. + * Use variant='foreground' when placing on primary-colored backgrounds. */ -export function Spinner({ size = 'md', className }: SpinnerProps) { +export function Spinner({ size = 'md', variant = 'primary', className }: SpinnerProps) { return (