refactor(ui): finish the Spinner migration, retire the Loader2 alias from JSX

The remaining 20 rendered Loader2 spinners carried a deliberate size or colour, so
the earlier pass left them. They convert cleanly after all: Loader2 is a lucide
alias of LoaderCircle (`LoaderCircle as Loader2` in lucide-react's d.ts) and that
is exactly what Spinner variant="circle" renders, while Circle merges an incoming
className over its built-in animate-spin. So each site keeps its size/colour class
and only drops the now-redundant animate-spin. Every rendered spinner in the app is
the Spinner primitive now.

flow-status-icon and flow-task-status-icon keep Loader2: there it is a value in a
Record<StatusType, { icon: LucideIcon }> map, not a rendered spinner, and Spinner
is a variant wrapper rather than a LucideIcon — it does not fit that slot.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Sergey Kozyrenko
2026-07-17 10:15:35 +07:00
co-authored by Claude Opus 4.8
parent d4dabc2783
commit 9cbda709b6
16 changed files with 99 additions and 36 deletions
@@ -1,9 +1,10 @@
import type { ReactNode } from 'react';
import { BarChart2, Loader2 } from 'lucide-react';
import { BarChart2 } from 'lucide-react';
import { ResponsiveContainer } from 'recharts';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Spinner } from '@/components/ui/spinner';
export function ChartCard({
children,
@@ -34,7 +35,10 @@ export function ChartCard({
className="flex items-center justify-center"
style={{ height }}
>
<Loader2 className="text-muted-foreground size-6 animate-spin" />
<Spinner
className="text-muted-foreground size-6"
variant="circle"
/>
</div>
) : empty ? (
<div
@@ -1,10 +1,10 @@
import type { AriaAttributes, Ref } from 'react';
import { Loader2 } from 'lucide-react';
import { lazy, Suspense, useImperativeHandle, useRef } from 'react';
import type { TextareaRef } from '@/components/ui/textarea';
import { Spinner } from '@/components/ui/spinner';
import { Textarea } from '@/components/ui/textarea';
import { cn } from '@/lib/utils';
@@ -118,7 +118,10 @@ export function MarkdownEditorField({
)}
id={id}
>
<Loader2 className="text-muted-foreground size-5 animate-spin" />
<Spinner
className="text-muted-foreground size-5"
variant="circle"
/>
</div>
}
>
@@ -4,9 +4,9 @@ import type { AriaAttributes, Ref } from 'react';
import { history } from '@tiptap/pm/history';
import { EditorState, TextSelection } from '@tiptap/pm/state';
import { EditorContent, useEditor } from '@tiptap/react';
import { Loader2 } from 'lucide-react';
import { useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
import { Spinner } from '@/components/ui/spinner';
import { cn } from '@/lib/utils';
import { createMarkdownExtensions } from './markdown-editor-extensions';
@@ -121,7 +121,10 @@ function MarkdownEditor({
)}
data-slot="markdown-editor"
>
<Loader2 className="text-muted-foreground size-5 animate-spin" />
<Spinner
className="text-muted-foreground size-5"
variant="circle"
/>
</div>
);
}
+8 -2
View File
@@ -1,8 +1,9 @@
'use client';
import { CircleCheckIcon, InfoIcon, Loader2Icon, OctagonXIcon, TriangleAlertIcon } from 'lucide-react';
import { CircleCheckIcon, InfoIcon, OctagonXIcon, TriangleAlertIcon } from 'lucide-react';
import { Toaster as Sonner, type ToasterProps } from 'sonner';
import { Spinner } from '@/components/ui/spinner';
import { useTheme } from '@/hooks/use-theme';
function Toaster({ ...props }: ToasterProps) {
@@ -14,7 +15,12 @@ function Toaster({ ...props }: ToasterProps) {
icons={{
error: <OctagonXIcon className="size-4" />,
info: <InfoIcon className="size-4" />,
loading: <Loader2Icon className="size-4 animate-spin" />,
loading: (
<Spinner
className="size-4"
variant="circle"
/>
),
success: <CircleCheckIcon className="size-4" />,
warning: <TriangleAlertIcon className="size-4" />,
}}
@@ -1,5 +1,5 @@
import { useQuery } from '@apollo/client/react';
import { Activity, CircleDollarSign, Cpu, GitFork, Loader2 } from 'lucide-react';
import { Activity, CircleDollarSign, Cpu, GitFork } from 'lucide-react';
import { useMemo } from 'react';
import type { UsageStatsFragmentFragment } from '@/graphql/types';
@@ -7,6 +7,7 @@ import type { UsageStatsFragmentFragment } from '@/graphql/types';
import { MetricCard } from '@/components/dashboard';
import { Badge } from '@/components/ui/badge';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Spinner } from '@/components/ui/spinner';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import FlowAgentIcon from '@/features/flows/agents/flow-agent-icon';
import {
@@ -304,7 +305,10 @@ export function FlowDashboardOverview({ flowId }: { flowId: string }) {
function LoadingTable() {
return (
<div className="flex items-center justify-center py-8">
<Loader2 className="text-muted-foreground size-6 animate-spin" />
<Spinner
className="text-muted-foreground size-6"
variant="circle"
/>
</div>
);
}
@@ -1,6 +1,6 @@
import { useQuery } from '@apollo/client/react';
import { format } from 'date-fns';
import { ChevronRight, Clock, Loader2, Wrench } from 'lucide-react';
import { ChevronRight, Clock, Wrench } from 'lucide-react';
import { memo, useDeferredValue, useMemo, useRef, useState } from 'react';
import { Area, AreaChart, Bar, BarChart, CartesianGrid, Tooltip, XAxis, YAxis } from 'recharts';
@@ -11,6 +11,7 @@ import { FlowStatusBadge } from '@/components/icons/flow-status-badge';
import { Badge } from '@/components/ui/badge';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
import { Spinner } from '@/components/ui/spinner';
import {
FlowsDocument,
FlowsExecutionStatsByPeriodDocument,
@@ -362,7 +363,10 @@ export function DashboardAnalytics({ period }: { period: UsageStatsPeriod }) {
<CardContent>
{executionStatsLoading ? (
<div className="flex items-center justify-center py-8">
<Loader2 className="text-muted-foreground size-6 animate-spin" />
<Spinner
className="text-muted-foreground size-6"
variant="circle"
/>
</div>
) : !deferredExecutionStats.length ? (
<p className="text-muted-foreground py-8 text-center text-sm">
@@ -1,11 +1,12 @@
import { useQuery } from '@apollo/client/react';
import { Activity, CircleDollarSign, Cpu, GitFork, Loader2 } from 'lucide-react';
import { Activity, CircleDollarSign, Cpu, GitFork } from 'lucide-react';
import type { UsageStatsFragmentFragment } from '@/graphql/types';
import { MetricCard } from '@/components/dashboard';
import { Badge } from '@/components/ui/badge';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Spinner } from '@/components/ui/spinner';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import {
FlowsStatsTotalDocument,
@@ -165,7 +166,10 @@ export function DashboardOverview() {
function LoadingTable() {
return (
<div className="flex items-center justify-center py-8">
<Loader2 className="text-muted-foreground size-6 animate-spin" />
<Spinner
className="text-muted-foreground size-6"
variant="circle"
/>
</div>
);
}
+4 -2
View File
@@ -9,7 +9,6 @@ import {
ExternalLink,
GitFork,
GripVertical,
Loader2,
NotepadText,
Pause,
PencilLine,
@@ -376,7 +375,10 @@ function Flow() {
<div className="relative flex h-[calc(100dvh-3rem)] w-full max-w-full flex-1">
{isFlowLoading && (
<div className="bg-background/50 absolute inset-0 z-50 flex items-center justify-center">
<Loader2 className="size-16 animate-spin" />
<Spinner
className="size-16"
variant="circle"
/>
</div>
)}
{isDesktop ? (
+5 -2
View File
@@ -1,7 +1,7 @@
import type { ColumnDef } from '@tanstack/react-table';
import { useMutation } from '@apollo/client/react';
import { Ellipsis, Eye, GitFork, Loader2, Pause, Pencil, PencilLine, Plus, Star, Trash } from 'lucide-react';
import { Ellipsis, Eye, GitFork, Pause, Pencil, PencilLine, Plus, Star, Trash } from 'lucide-react';
import { CheckCircle2, XCircle } from 'lucide-react';
import { useCallback, useMemo, useRef, useState } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
@@ -593,7 +593,10 @@ function Flows() {
<Empty>
<EmptyHeader>
<EmptyMedia>
<Loader2 className="text-muted-foreground size-10 animate-spin" />
<Spinner
className="text-muted-foreground size-10"
variant="circle"
/>
</EmptyMedia>
<EmptyTitle>Loading flows...</EmptyTitle>
<EmptyDescription>Please wait while we fetch your conversation flows</EmptyDescription>
+5 -2
View File
@@ -1,6 +1,6 @@
import type { ColumnDef } from '@tanstack/react-table';
import { Ellipsis, LibraryBig, Loader2, Pencil, PencilLine, Plus, Trash } from 'lucide-react';
import { Ellipsis, LibraryBig, Pencil, PencilLine, Plus, Trash } from 'lucide-react';
import { useCallback, useRef, useState } from 'react';
import { useLocation, useNavigate, useSearchParams } from 'react-router-dom';
import { toast } from 'sonner';
@@ -408,7 +408,10 @@ function Knowledges() {
<Empty>
<EmptyHeader>
<EmptyMedia>
<Loader2 className="text-muted-foreground size-10 animate-spin" />
<Spinner
className="text-muted-foreground size-10"
variant="circle"
/>
</EmptyMedia>
<EmptyTitle>Loading knowledges...</EmptyTitle>
<EmptyDescription>Please wait while we fetch your knowledge documents</EmptyDescription>
+5 -2
View File
@@ -1,7 +1,7 @@
import { Loader2 } from 'lucide-react';
import { useLocation, useSearchParams } from 'react-router-dom';
import Logo from '@/components/icons/logo';
import { Spinner } from '@/components/ui/spinner';
import LoginForm from '@/features/authentication/login-form';
import { routes } from '@/lib/routes';
import { getSafeReturnUrl } from '@/lib/utils/auth';
@@ -28,7 +28,10 @@ function Login() {
returnUrl={returnUrl}
/>
) : (
<Loader2 className="size-16 animate-spin" />
<Spinner
className="size-16"
variant="circle"
/>
)}
</div>
<div className="from-primary/20 via-primary/10 to-background hidden bg-linear-to-br lg:flex">
@@ -11,7 +11,6 @@ import {
Ellipsis,
ExternalLink,
Key,
Loader2,
Pencil,
Plus,
Trash,
@@ -872,7 +871,10 @@ function SettingsAPITokens() {
<Empty>
<EmptyHeader>
<EmptyMedia>
<Loader2 className="text-muted-foreground size-10 animate-spin" />
<Spinner
className="text-muted-foreground size-10"
variant="circle"
/>
</EmptyMedia>
<EmptyTitle>Loading tokens...</EmptyTitle>
<EmptyDescription>Please wait while we fetch your API tokens</EmptyDescription>
@@ -9,7 +9,6 @@ import {
Ellipsis,
FileDiff,
FileText,
Loader2,
RotateCcw,
Save,
User,
@@ -734,7 +733,10 @@ function SettingsPrompt() {
<Empty>
<EmptyHeader>
<EmptyMedia>
<Loader2 className="text-muted-foreground size-10 animate-spin" />
<Spinner
className="text-muted-foreground size-10"
variant="circle"
/>
</EmptyMedia>
<EmptyTitle>Loading prompt data...</EmptyTitle>
<EmptyDescription>Please wait while we fetch prompt information</EmptyDescription>
@@ -9,7 +9,6 @@ import {
Code,
Ellipsis,
FileText,
Loader2,
Pencil,
RotateCcw,
Settings,
@@ -40,6 +39,7 @@ import {
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from '@/components/ui/empty';
import { Spinner } from '@/components/ui/spinner';
import { DeletePromptDocument, SettingsPromptsDocument } from '@/graphql/types';
import { usePageStorageKeys } from '@/hooks/use-page-storage-keys';
import { routes } from '@/lib/routes';
@@ -414,7 +414,10 @@ function SettingsPrompts() {
resetOperation?.promptName === agent.name &&
resetOperation?.type === 'system' ? (
<>
<Loader2 className="size-3 animate-spin" />
<Spinner
className="size-3"
variant="circle"
/>
Resetting...
</>
) : (
@@ -438,7 +441,10 @@ function SettingsPrompts() {
resetOperation?.promptName === agent.name &&
resetOperation?.type === 'human' ? (
<>
<Loader2 className="size-3 animate-spin" />
<Spinner
className="size-3"
variant="circle"
/>
Resetting...
</>
) : (
@@ -462,7 +468,10 @@ function SettingsPrompts() {
resetOperation?.promptName === agent.name &&
resetOperation?.type === 'all' ? (
<>
<Loader2 className="size-3 animate-spin" />
<Spinner
className="size-3"
variant="circle"
/>
Resetting...
</>
) : (
@@ -565,7 +574,10 @@ function SettingsPrompts() {
resetOperation?.promptName === tool.name &&
resetOperation?.type === 'tool' ? (
<>
<Loader2 className="size-3 animate-spin" />
<Spinner
className="size-3"
variant="circle"
/>
Resetting...
</>
) : (
@@ -786,7 +798,10 @@ function SettingsPrompts() {
<Empty>
<EmptyHeader>
<EmptyMedia>
<Loader2 className="text-muted-foreground size-10 animate-spin" />
<Spinner
className="text-muted-foreground size-10"
variant="circle"
/>
</EmptyMedia>
<EmptyTitle>Loading prompts...</EmptyTitle>
<EmptyDescription>Please wait while we fetch your prompt templates</EmptyDescription>
@@ -7,7 +7,6 @@ import {
Clock,
Ellipsis,
Lightbulb,
Loader2,
Play,
Plug,
Save,
@@ -1564,7 +1563,10 @@ function SettingsProvider() {
<Empty>
<EmptyHeader>
<EmptyMedia>
<Loader2 className="text-muted-foreground size-10 animate-spin" />
<Spinner
className="text-muted-foreground size-10"
variant="circle"
/>
</EmptyMedia>
<EmptyTitle>Loading provider data...</EmptyTitle>
<EmptyDescription>Please wait while we fetch provider configuration</EmptyDescription>
@@ -1,7 +1,7 @@
import type { ColumnDef, Row } from '@tanstack/react-table';
import { useMutation, useQuery } from '@apollo/client/react';
import { AlertCircle, ChevronDown, Copy, Ellipsis, Loader2, Pencil, Plug, Plus, Settings, Trash } from 'lucide-react';
import { AlertCircle, ChevronDown, Copy, Ellipsis, Pencil, Plug, Plus, Settings, Trash } from 'lucide-react';
import { useCallback, useMemo, useState } from 'react';
import { useNavigate } from 'react-router-dom';
@@ -412,7 +412,10 @@ function SettingsProviders() {
<Empty>
<EmptyHeader>
<EmptyMedia>
<Loader2 className="text-muted-foreground size-10 animate-spin" />
<Spinner
className="text-muted-foreground size-10"
variant="circle"
/>
</EmptyMedia>
<EmptyTitle>Loading providers...</EmptyTitle>
<EmptyDescription>Please wait while we fetch your provider configurations</EmptyDescription>