mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-03 21:03:08 +00:00
fix: Improve spinner visibility on primary-colored backgrounds
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 <noreply@anthropic.com>
This commit is contained in:
@@ -3,7 +3,7 @@ import { Slot } from '@radix-ui/react-slot';
|
|||||||
import { cva, type VariantProps } from 'class-variance-authority';
|
import { cva, type VariantProps } from 'class-variance-authority';
|
||||||
|
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { Spinner } from '@/components/ui/spinner';
|
import { Spinner, type SpinnerVariant } from '@/components/ui/spinner';
|
||||||
|
|
||||||
const buttonVariants = cva(
|
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]",
|
"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
|
/** Button variants that have colored backgrounds requiring foreground spinner color */
|
||||||
function ButtonSpinner({ className }: { className?: string }) {
|
const COLORED_BACKGROUND_VARIANTS = ['default', 'destructive'] as const;
|
||||||
return <Spinner size="sm" className={className} />;
|
|
||||||
|
/** Get spinner variant based on button variant - use foreground for colored backgrounds */
|
||||||
|
function getSpinnerVariant(
|
||||||
|
buttonVariant: VariantProps<typeof buttonVariants>['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({
|
function Button({
|
||||||
@@ -57,6 +67,7 @@ function Button({
|
|||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const isDisabled = disabled || loading;
|
const isDisabled = disabled || loading;
|
||||||
|
const spinnerVariant = getSpinnerVariant(variant);
|
||||||
|
|
||||||
// Special handling for animated-outline variant
|
// Special handling for animated-outline variant
|
||||||
if (variant === 'animated-outline' && !asChild) {
|
if (variant === 'animated-outline' && !asChild) {
|
||||||
@@ -83,7 +94,7 @@ function Button({
|
|||||||
size === 'icon' && 'p-0 gap-0'
|
size === 'icon' && 'p-0 gap-0'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{loading && <ButtonSpinner />}
|
{loading && <Spinner size="sm" variant={spinnerVariant} />}
|
||||||
{children}
|
{children}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -99,7 +110,7 @@ function Button({
|
|||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{loading && <ButtonSpinner />}
|
{loading && <Spinner size="sm" variant={spinnerVariant} />}
|
||||||
{children}
|
{children}
|
||||||
</Comp>
|
</Comp>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { Loader2 } from 'lucide-react';
|
import { Loader2 } from 'lucide-react';
|
||||||
import { cn } from '@/lib/utils';
|
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<SpinnerSize, string> = {
|
const sizeClasses: Record<SpinnerSize, string> = {
|
||||||
xs: 'h-3 w-3',
|
xs: 'h-3 w-3',
|
||||||
@@ -11,9 +12,17 @@ const sizeClasses: Record<SpinnerSize, string> = {
|
|||||||
xl: 'h-8 w-8',
|
xl: 'h-8 w-8',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const variantClasses: Record<SpinnerVariant, string> = {
|
||||||
|
primary: 'text-primary',
|
||||||
|
foreground: 'text-primary-foreground',
|
||||||
|
muted: 'text-muted-foreground',
|
||||||
|
};
|
||||||
|
|
||||||
interface SpinnerProps {
|
interface SpinnerProps {
|
||||||
/** Size of the spinner */
|
/** Size of the spinner */
|
||||||
size?: SpinnerSize;
|
size?: SpinnerSize;
|
||||||
|
/** Color variant - use 'foreground' when on primary backgrounds */
|
||||||
|
variant?: SpinnerVariant;
|
||||||
/** Additional class names */
|
/** Additional class names */
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
@@ -21,11 +30,12 @@ interface SpinnerProps {
|
|||||||
/**
|
/**
|
||||||
* Themed spinner component using the primary brand color.
|
* Themed spinner component using the primary brand color.
|
||||||
* Use this for all loading indicators throughout the app for consistency.
|
* 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 (
|
return (
|
||||||
<Loader2
|
<Loader2
|
||||||
className={cn(sizeClasses[size], 'animate-spin text-primary', className)}
|
className={cn(sizeClasses[size], 'animate-spin', variantClasses[variant], className)}
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -261,7 +261,7 @@ export function TaskProgressPanel({
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{isCompleted && <Check className="h-3.5 w-3.5" />}
|
{isCompleted && <Check className="h-3.5 w-3.5" />}
|
||||||
{isActive && <Spinner size="xs" />}
|
{isActive && <Spinner size="xs" variant="foreground" />}
|
||||||
{isPending && <Circle className="h-2 w-2 fill-current opacity-50" />}
|
{isPending && <Circle className="h-2 w-2 fill-current opacity-50" />}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -330,7 +330,7 @@ export function MergeWorktreeDialog({
|
|||||||
>
|
>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<>
|
<>
|
||||||
<Spinner size="sm" className="mr-2" />
|
<Spinner size="sm" variant="foreground" className="mr-2" />
|
||||||
Merging...
|
Merging...
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -210,7 +210,7 @@ export function PlanApprovalDialog({
|
|||||||
className="bg-green-600 hover:bg-green-700 text-white"
|
className="bg-green-600 hover:bg-green-700 text-white"
|
||||||
>
|
>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<Spinner size="sm" className="mr-2" />
|
<Spinner size="sm" variant="foreground" className="mr-2" />
|
||||||
) : (
|
) : (
|
||||||
<Check className="w-4 h-4 mr-2" />
|
<Check className="w-4 h-4 mr-2" />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -572,7 +572,7 @@ export function InterviewView() {
|
|||||||
>
|
>
|
||||||
{isGenerating ? (
|
{isGenerating ? (
|
||||||
<>
|
<>
|
||||||
<Spinner size="sm" className="mr-2" />
|
<Spinner size="sm" variant="foreground" className="mr-2" />
|
||||||
Creating...
|
Creating...
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -448,7 +448,7 @@ export function LoginView() {
|
|||||||
>
|
>
|
||||||
{isLoggingIn ? (
|
{isLoggingIn ? (
|
||||||
<>
|
<>
|
||||||
<Spinner size="sm" className="mr-2" />
|
<Spinner size="sm" variant="foreground" className="mr-2" />
|
||||||
Authenticating...
|
Authenticating...
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ export function CliInstallationCard({
|
|||||||
>
|
>
|
||||||
{isInstalling ? (
|
{isInstalling ? (
|
||||||
<>
|
<>
|
||||||
<Spinner size="sm" className="mr-2" />
|
<Spinner size="sm" variant="foreground" className="mr-2" />
|
||||||
Installing...
|
Installing...
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -412,7 +412,7 @@ export function ClaudeSetupStep({ onNext, onBack, onSkip }: ClaudeSetupStepProps
|
|||||||
>
|
>
|
||||||
{isInstalling ? (
|
{isInstalling ? (
|
||||||
<>
|
<>
|
||||||
<Spinner size="sm" className="mr-2" />
|
<Spinner size="sm" variant="foreground" className="mr-2" />
|
||||||
Installing...
|
Installing...
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
@@ -574,7 +574,7 @@ export function ClaudeSetupStep({ onNext, onBack, onSkip }: ClaudeSetupStepProps
|
|||||||
>
|
>
|
||||||
{isSavingApiKey ? (
|
{isSavingApiKey ? (
|
||||||
<>
|
<>
|
||||||
<Spinner size="sm" className="mr-2" />
|
<Spinner size="sm" variant="foreground" className="mr-2" />
|
||||||
Saving...
|
Saving...
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -408,7 +408,7 @@ export function CliSetupStep({ config, state, onNext, onBack, onSkip }: CliSetup
|
|||||||
>
|
>
|
||||||
{isInstalling ? (
|
{isInstalling ? (
|
||||||
<>
|
<>
|
||||||
<Spinner size="sm" className="mr-2" />
|
<Spinner size="sm" variant="foreground" className="mr-2" />
|
||||||
Installing...
|
Installing...
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
@@ -681,7 +681,7 @@ export function CliSetupStep({ config, state, onNext, onBack, onSkip }: CliSetup
|
|||||||
>
|
>
|
||||||
{isSavingApiKey ? (
|
{isSavingApiKey ? (
|
||||||
<>
|
<>
|
||||||
<Spinner size="sm" className="mr-2" />
|
<Spinner size="sm" variant="foreground" className="mr-2" />
|
||||||
Saving...
|
Saving...
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -318,7 +318,7 @@ export function CursorSetupStep({ onNext, onBack, onSkip }: CursorSetupStepProps
|
|||||||
>
|
>
|
||||||
{isLoggingIn ? (
|
{isLoggingIn ? (
|
||||||
<>
|
<>
|
||||||
<Spinner size="sm" className="mr-2" />
|
<Spinner size="sm" variant="foreground" className="mr-2" />
|
||||||
Waiting for login...
|
Waiting for login...
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -316,7 +316,7 @@ export function OpencodeSetupStep({ onNext, onBack, onSkip }: OpencodeSetupStepP
|
|||||||
>
|
>
|
||||||
{isLoggingIn ? (
|
{isLoggingIn ? (
|
||||||
<>
|
<>
|
||||||
<Spinner size="sm" className="mr-2" />
|
<Spinner size="sm" variant="foreground" className="mr-2" />
|
||||||
Waiting for login...
|
Waiting for login...
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -329,7 +329,7 @@ function ClaudeContent() {
|
|||||||
>
|
>
|
||||||
{isInstalling ? (
|
{isInstalling ? (
|
||||||
<>
|
<>
|
||||||
<Spinner size="sm" className="mr-2" />
|
<Spinner size="sm" variant="foreground" className="mr-2" />
|
||||||
Installing...
|
Installing...
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
@@ -424,7 +424,11 @@ function ClaudeContent() {
|
|||||||
disabled={isSavingApiKey || !apiKey.trim()}
|
disabled={isSavingApiKey || !apiKey.trim()}
|
||||||
className="flex-1 bg-brand-500 hover:bg-brand-600 text-white"
|
className="flex-1 bg-brand-500 hover:bg-brand-600 text-white"
|
||||||
>
|
>
|
||||||
{isSavingApiKey ? <Spinner size="sm" /> : 'Save API Key'}
|
{isSavingApiKey ? (
|
||||||
|
<Spinner size="sm" variant="foreground" />
|
||||||
|
) : (
|
||||||
|
'Save API Key'
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
{hasApiKey && (
|
{hasApiKey && (
|
||||||
<Button
|
<Button
|
||||||
@@ -661,7 +665,7 @@ function CursorContent() {
|
|||||||
>
|
>
|
||||||
{isLoggingIn ? (
|
{isLoggingIn ? (
|
||||||
<>
|
<>
|
||||||
<Spinner size="sm" className="mr-2" />
|
<Spinner size="sm" variant="foreground" className="mr-2" />
|
||||||
Waiting for login...
|
Waiting for login...
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
@@ -918,7 +922,7 @@ function CodexContent() {
|
|||||||
>
|
>
|
||||||
{isLoggingIn ? (
|
{isLoggingIn ? (
|
||||||
<>
|
<>
|
||||||
<Spinner size="sm" className="mr-2" />
|
<Spinner size="sm" variant="foreground" className="mr-2" />
|
||||||
Waiting for login...
|
Waiting for login...
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
@@ -961,7 +965,7 @@ function CodexContent() {
|
|||||||
disabled={isSaving || !apiKey.trim()}
|
disabled={isSaving || !apiKey.trim()}
|
||||||
className="w-full bg-brand-500 hover:bg-brand-600 text-white"
|
className="w-full bg-brand-500 hover:bg-brand-600 text-white"
|
||||||
>
|
>
|
||||||
{isSaving ? <Spinner size="sm" /> : 'Save API Key'}
|
{isSaving ? <Spinner size="sm" variant="foreground" /> : 'Save API Key'}
|
||||||
</Button>
|
</Button>
|
||||||
</AccordionContent>
|
</AccordionContent>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
@@ -1194,7 +1198,7 @@ function OpencodeContent() {
|
|||||||
>
|
>
|
||||||
{isLoggingIn ? (
|
{isLoggingIn ? (
|
||||||
<>
|
<>
|
||||||
<Spinner size="sm" className="mr-2" />
|
<Spinner size="sm" variant="foreground" className="mr-2" />
|
||||||
Waiting for login...
|
Waiting for login...
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
@@ -1466,7 +1470,7 @@ function GeminiContent() {
|
|||||||
>
|
>
|
||||||
{isLoggingIn ? (
|
{isLoggingIn ? (
|
||||||
<>
|
<>
|
||||||
<Spinner size="sm" className="mr-2" />
|
<Spinner size="sm" variant="foreground" className="mr-2" />
|
||||||
Waiting for login...
|
Waiting for login...
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
@@ -1509,7 +1513,7 @@ function GeminiContent() {
|
|||||||
disabled={isSaving || !apiKey.trim()}
|
disabled={isSaving || !apiKey.trim()}
|
||||||
className="w-full bg-brand-500 hover:bg-brand-600 text-white"
|
className="w-full bg-brand-500 hover:bg-brand-600 text-white"
|
||||||
>
|
>
|
||||||
{isSaving ? <Spinner size="sm" /> : 'Save API Key'}
|
{isSaving ? <Spinner size="sm" variant="foreground" /> : 'Save API Key'}
|
||||||
</Button>
|
</Button>
|
||||||
</AccordionContent>
|
</AccordionContent>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
@@ -1745,7 +1749,7 @@ function CopilotContent() {
|
|||||||
>
|
>
|
||||||
{isLoggingIn ? (
|
{isLoggingIn ? (
|
||||||
<>
|
<>
|
||||||
<Spinner size="sm" className="mr-2" />
|
<Spinner size="sm" variant="foreground" className="mr-2" />
|
||||||
Waiting for login...
|
Waiting for login...
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
Reference in New Issue
Block a user