From 066ffe56397891ffb657491b517dfaf6fa6efbcb Mon Sep 17 00:00:00 2001 From: Shirone Date: Sat, 24 Jan 2026 15:26:47 +0100 Subject: [PATCH] fix: Improve spinner visibility on primary-colored backgrounds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add variant prop to Spinner component to support different color contexts: - 'primary' (default): Uses text-primary for standard backgrounds - 'foreground': Uses text-primary-foreground for primary backgrounds - 'muted': Uses text-muted-foreground for subtle contexts Updated components where spinners were invisible against primary backgrounds: - TaskProgressPanel: Active task indicators now visible - Button: Auto-detects spinner variant based on button style - Various dialogs and setup views using buttons with loaders Fixes #670 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- apps/ui/src/components/ui/button.tsx | 23 ++++++++++++++----- apps/ui/src/components/ui/spinner.tsx | 16 ++++++++++--- .../src/components/ui/task-progress-panel.tsx | 2 +- .../dialogs/merge-worktree-dialog.tsx | 2 +- .../dialogs/plan-approval-dialog.tsx | 2 +- .../src/components/views/interview-view.tsx | 2 +- apps/ui/src/components/views/login-view.tsx | 2 +- .../components/cli-installation-card.tsx | 2 +- .../setup-view/steps/claude-setup-step.tsx | 4 ++-- .../views/setup-view/steps/cli-setup-step.tsx | 4 ++-- .../setup-view/steps/cursor-setup-step.tsx | 2 +- .../setup-view/steps/opencode-setup-step.tsx | 2 +- .../setup-view/steps/providers-setup-step.tsx | 22 ++++++++++-------- 13 files changed, 55 insertions(+), 30 deletions(-) diff --git a/apps/ui/src/components/ui/button.tsx b/apps/ui/src/components/ui/button.tsx index a7163ed3..bce53665 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 = ['default', 'destructive'] as const; + +/** Get spinner variant based on button variant - use foreground for colored backgrounds */ +function getSpinnerVariant( + buttonVariant: VariantProps['variant'] +): SpinnerVariant { + // undefined defaults to 'default' variant which has a colored background + if (!buttonVariant || COLORED_BACKGROUND_VARIANTS.includes(buttonVariant as any)) { + 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 (