Merge pull request #676 from AutoMaker-Org/feature/bug-after-v0-13-0-version-got-merged-some-ui-load-d8lr

fix: Improve spinner visibility on primary-colored backgrounds
This commit is contained in:
Shirone
2026-01-24 19:58:17 +00:00
committed by GitHub
15 changed files with 74 additions and 35 deletions

View File

@@ -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 = new Set<string>(['default', 'destructive']);
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 {
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({ 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>
); );

View File

@@ -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"
/> />
); );

View File

@@ -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>

View File

@@ -463,6 +463,16 @@ export function BoardView() {
const selectedWorktreeBranch = const selectedWorktreeBranch =
currentWorktreeBranch || worktrees.find((w) => w.isMain)?.branch || 'main'; currentWorktreeBranch || worktrees.find((w) => w.isMain)?.branch || 'main';
// Aggregate running auto tasks across all worktrees for this project
const autoModeByWorktree = useAppStore((state) => state.autoModeByWorktree);
const runningAutoTasksAllWorktrees = useMemo(() => {
if (!currentProject?.id) return [];
const prefix = `${currentProject.id}::`;
return Object.entries(autoModeByWorktree)
.filter(([key]) => key.startsWith(prefix))
.flatMap(([, state]) => state.runningTasks ?? []);
}, [autoModeByWorktree, currentProject?.id]);
// Get in-progress features for keyboard shortcuts (needed before actions hook) // Get in-progress features for keyboard shortcuts (needed before actions hook)
// Must be after runningAutoTasks is defined // Must be after runningAutoTasks is defined
const inProgressFeaturesForShortcuts = useMemo(() => { const inProgressFeaturesForShortcuts = useMemo(() => {
@@ -1372,7 +1382,7 @@ export function BoardView() {
setWorktreeRefreshKey((k) => k + 1); setWorktreeRefreshKey((k) => k + 1);
}} }}
onRemovedWorktrees={handleRemovedWorktrees} onRemovedWorktrees={handleRemovedWorktrees}
runningFeatureIds={runningAutoTasks} runningFeatureIds={runningAutoTasksAllWorktrees}
branchCardCounts={branchCardCounts} branchCardCounts={branchCardCounts}
features={hookFeatures.map((f) => ({ features={hookFeatures.map((f) => ({
id: f.id, id: f.id,

View File

@@ -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...
</> </>
) : ( ) : (

View File

@@ -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" />
)} )}

View File

@@ -260,8 +260,10 @@ export function WorktreeTab({
aria-label={worktree.branch} aria-label={worktree.branch}
data-testid={`worktree-branch-${worktree.branch}`} data-testid={`worktree-branch-${worktree.branch}`}
> >
{isRunning && <Spinner size="xs" />} {isRunning && <Spinner size="xs" variant={isSelected ? 'foreground' : 'primary'} />}
{isActivating && !isRunning && <Spinner size="xs" />} {isActivating && !isRunning && (
<Spinner size="xs" variant={isSelected ? 'foreground' : 'primary'} />
)}
{worktree.branch} {worktree.branch}
{cardCount !== undefined && cardCount > 0 && ( {cardCount !== undefined && cardCount > 0 && (
<span className="inline-flex items-center justify-center h-4 min-w-[1rem] px-1 text-[10px] font-medium rounded bg-background/80 text-foreground border border-border"> <span className="inline-flex items-center justify-center h-4 min-w-[1rem] px-1 text-[10px] font-medium rounded bg-background/80 text-foreground border border-border">
@@ -327,8 +329,10 @@ export function WorktreeTab({
: 'Click to switch to this branch' : 'Click to switch to this branch'
} }
> >
{isRunning && <Spinner size="xs" />} {isRunning && <Spinner size="xs" variant={isSelected ? 'foreground' : 'primary'} />}
{isActivating && !isRunning && <Spinner size="xs" />} {isActivating && !isRunning && (
<Spinner size="xs" variant={isSelected ? 'foreground' : 'primary'} />
)}
{worktree.branch} {worktree.branch}
{cardCount !== undefined && cardCount > 0 && ( {cardCount !== undefined && cardCount > 0 && (
<span className="inline-flex items-center justify-center h-4 min-w-[1rem] px-1 text-[10px] font-medium rounded bg-background/80 text-foreground border border-border"> <span className="inline-flex items-center justify-center h-4 min-w-[1rem] px-1 text-[10px] font-medium rounded bg-background/80 text-foreground border border-border">

View File

@@ -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...
</> </>
) : ( ) : (

View File

@@ -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...
</> </>
) : ( ) : (

View File

@@ -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...
</> </>
) : ( ) : (

View File

@@ -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...
</> </>
) : ( ) : (

View File

@@ -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...
</> </>
) : ( ) : (

View File

@@ -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...
</> </>
) : ( ) : (

View File

@@ -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...
</> </>
) : ( ) : (

View File

@@ -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...
</> </>
) : ( ) : (