authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-09-21 22:57:43-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-21 22:57:43-07:00
logb5a0f810108d42ca8635b5a3d75a6e885bb3a254
tree0e6da149664827942d11cebd78a9c6abbfffdf0a
parent438bf466fd7e97bb80f0d1f936b2c857830249b7
signature Signed by PGP key B5690EEEBB952194

refactor(web): give Spinner and RefreshIcon a size prop (#12985)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

36 files changed, 101 insertions(+), 59 deletions(-)

apps/web/src/components/BranchToolbarBranchSelector.tsx+1-1
...@@ -905,7 +905,7 @@ export function BranchToolbarBranchSelector({...@@ -905,7 +905,7 @@ export function BranchToolbarBranchSelector({
905 className="flex cursor-pointer items-center justify-between gap-3 border-t border-border/60 px-3 py-2 text-xs"905 className="flex cursor-pointer items-center justify-between gap-3 border-t border-border/60 px-3 py-2 text-xs"
906 >906 >
907 <span className="flex min-w-0 items-center gap-1.5 font-medium text-muted-foreground">907 <span className="flex min-w-0 items-center gap-1.5 font-medium text-muted-foreground">
908 <RefreshIcon aria-hidden="true" className="size-3 shrink-0 opacity-70" />908 <RefreshIcon aria-hidden="true" size="xs" className="shrink-0" />
909 <span className="truncate">Start from origin</span>909 <span className="truncate">Start from origin</span>
910 </span>910 </span>
911 <Switch911 <Switch
apps/web/src/components/DiffPanel.tsx+1-1
...@@ -856,7 +856,7 @@ export default function DiffPanel({...@@ -856,7 +856,7 @@ export default function DiffPanel({
856 />856 />
857 }857 }
858 >858 >
859 <RefreshIcon className="size-3.5" refreshing={isRefreshingDiff} />859 <RefreshIcon size="sm" refreshing={isRefreshingDiff} />
860 </TooltipTrigger>860 </TooltipTrigger>
861 <TooltipPopup side="top">861 <TooltipPopup side="top">
862 {isRefreshingDiff ? "Refreshing diff…" : "Refresh diff"}862 {isRefreshingDiff ? "Refreshing diff…" : "Refresh diff"}
apps/web/src/components/GitActionsControl.tsx+2-2
...@@ -851,7 +851,7 @@ function PublishRepositoryDialog(props: PublishRepositoryDialogProps) {...@@ -851,7 +851,7 @@ function PublishRepositoryDialog(props: PublishRepositoryDialogProps) {
851 aria-live="polite"851 aria-live="polite"
852 className="flex items-center gap-2 rounded-md border border-input bg-muted/40 px-3 py-2 text-xs text-muted-foreground dark:border-transparent dark:bg-white/[0.035]"852 className="flex items-center gap-2 rounded-md border border-input bg-muted/40 px-3 py-2 text-xs text-muted-foreground dark:border-transparent dark:bg-white/[0.035]"
853 >853 >
854 <Spinner className="size-3.5" aria-hidden />854 <Spinner size="sm" aria-hidden />
855 Publishing repository to {publishProviderLabel}...855 Publishing repository to {publishProviderLabel}...
856 </div>856 </div>
857 ) : null}857 ) : null}
...@@ -938,7 +938,7 @@ function PublishRepositoryDialog(props: PublishRepositoryDialogProps) {...@@ -938,7 +938,7 @@ function PublishRepositoryDialog(props: PublishRepositoryDialogProps) {
938 <Button disabled={!canSubmitPublishRepository} onClick={submitPublishRepository}>938 <Button disabled={!canSubmitPublishRepository} onClick={submitPublishRepository}>
939 {publishRepositoryAction.isPending ? (939 {publishRepositoryAction.isPending ? (
940 <>940 <>
941 <Spinner className="size-3.5" aria-hidden />941 <Spinner size="sm" aria-hidden />
942 Publishing...942 Publishing...
943 </>943 </>
944 ) : (944 ) : (
apps/web/src/components/ProviderUpdateEnvironmentRows.tsx+1-1
...@@ -118,7 +118,7 @@ function EnvironmentUpdateRow({...@@ -118,7 +118,7 @@ function EnvironmentUpdateRow({
118 let trailing: ReactNode;118 let trailing: ReactNode;
119 switch (status.kind) {119 switch (status.kind) {
120 case "loading":120 case "loading":
121 trailing = <Spinner className="size-4 text-muted-foreground" />;121 trailing = <Spinner size="md" tone="muted" />;
122 break;122 break;
123 case "success":123 case "success":
124 trailing = <CheckIcon aria-hidden="true" className="size-4 text-success" />;124 trailing = <CheckIcon aria-hidden="true" className="size-4 text-success" />;
apps/web/src/components/PullRequestThreadDialog.tsx+1-1
...@@ -252,7 +252,7 @@ export function PullRequestThreadDialog({...@@ -252,7 +252,7 @@ export function PullRequestThreadDialog({
252252
253 {isResolving ? (253 {isResolving ? (
254 <div className="flex items-center gap-2 text-muted-foreground text-xs">254 <div className="flex items-center gap-2 text-muted-foreground text-xs">
255 <Spinner className="size-3.5" />255 <Spinner size="sm" />
256 Resolving {terminology.singular}...256 Resolving {terminology.singular}...
257 </div>257 </div>
258 ) : null}258 ) : null}
apps/web/src/components/chat/ComposerPrimaryActions.tsx+1-1
...@@ -254,7 +254,7 @@ export const ComposerPrimaryActions = memo(function ComposerPrimaryActions({...@@ -254,7 +254,7 @@ export const ComposerPrimaryActions = memo(function ComposerPrimaryActions({
254 </span>254 </span>
255 ) : null}255 ) : null}
256 {isConnecting || isSendBusy ? (256 {isConnecting || isSendBusy ? (
257 <Spinner className="size-3.5" aria-hidden="true" />257 <Spinner size="sm" aria-hidden="true" />
258 ) : (258 ) : (
259 <svg width="14" height="14" viewBox="0 0 14 14" fill="none" aria-hidden="true">259 <svg width="14" height="14" viewBox="0 0 14 14" fill="none" aria-hidden="true">
260 <path260 <path
apps/web/src/components/chat/MessagesTimeline.tsx+1-1
...@@ -2567,7 +2567,7 @@ function BackgroundWorktreeSetupChip({ snapshot }: { snapshot: WorktreeSetupSnap...@@ -2567,7 +2567,7 @@ function BackgroundWorktreeSetupChip({ snapshot }: { snapshot: WorktreeSetupSnap
2567 />2567 />
2568 }2568 }
2569 >2569 >
2570 <Spinner className="size-3 shrink-0" />2570 <Spinner size="xs" className="shrink-0" />
2571 <span className="truncate">{scriptName}</span>2571 <span className="truncate">{scriptName}</span>
2572 </PopoverTrigger>2572 </PopoverTrigger>
2573 <PopoverPopup2573 <PopoverPopup
apps/web/src/components/chat/WorktreeSetupCard.tsx+1-1
...@@ -61,7 +61,7 @@ function StageIcon({ status }: { status: WorktreeSetupStage["status"] }) {...@@ -61,7 +61,7 @@ function StageIcon({ status }: { status: WorktreeSetupStage["status"] }) {
61 case "done":61 case "done":
62 return <CheckIcon aria-hidden className={className} />;62 return <CheckIcon aria-hidden className={className} />;
63 case "running":63 case "running":
64 return <Spinner className={className} />;64 return <Spinner size="md" className="shrink-0" />;
65 case "failed":65 case "failed":
66 return <XIcon aria-hidden className={className} />;66 return <XIcon aria-hidden className={className} />;
67 case "warning":67 case "warning":
apps/web/src/components/clerk/ClerkUserProfilePage.tsx+1-1
...@@ -56,7 +56,7 @@ export function ClerkUserProfileRefreshButton({...@@ -56,7 +56,7 @@ export function ClerkUserProfileRefreshButton({
56 disabled={disabled || isPending}56 disabled={disabled || isPending}
57 onClick={onClick}57 onClick={onClick}
58 >58 >
59 <RefreshIcon aria-hidden="true" className="size-3.5" refreshing={isPending} />59 <RefreshIcon aria-hidden="true" size="sm" refreshing={isPending} />
60 Refresh60 Refresh
61 </Button>61 </Button>
62 );62 );
apps/web/src/components/device/DeviceLoadingView.tsx+1-1
...@@ -27,7 +27,7 @@ export function DeviceLoadingView(props: {...@@ -27,7 +27,7 @@ export function DeviceLoadingView(props: {
27 ) : null}27 ) : null}
28 </div>28 </div>
29 <div className="flex items-center gap-2 text-xs text-muted-foreground">29 <div className="flex items-center gap-2 text-xs text-muted-foreground">
30 {!props.error ? <Spinner className="size-3" /> : null}30 {!props.error ? <Spinner size="xs" /> : null}
31 <span>{props.message}</span>31 <span>{props.message}</span>
32 </div>32 </div>
33 {props.children}33 {props.children}
apps/web/src/components/device/DevicePanel.tsx+1-1
...@@ -367,7 +367,7 @@ export function DevicePanel(props: {...@@ -367,7 +367,7 @@ export function DevicePanel(props: {
367 onClick={() => void selectDevice(deviceKey(device))}367 onClick={() => void selectDevice(deviceKey(device))}
368 action={368 action={
369 pendingDeviceKey === deviceKey(device) ? (369 pendingDeviceKey === deviceKey(device) ? (
370 <Spinner className="size-3" />370 <Spinner size="xs" />
371 ) : (371 ) : (
372 <span className="text-xs text-muted-foreground">372 <span className="text-xs text-muted-foreground">
373 {device.booted ? "Open" : "Start"}373 {device.booted ? "Open" : "Start"}
apps/web/src/components/device/DeviceSetup.tsx+3-3
...@@ -203,7 +203,7 @@ export function DeviceHubSetupStatus({...@@ -203,7 +203,7 @@ export function DeviceHubSetupStatus({
203 if (!pending && state.hostStatus !== "ready") return null;203 if (!pending && state.hostStatus !== "ready") return null;
204 return (204 return (
205 <p role="status" className="flex items-center gap-2 text-xs text-muted-foreground">205 <p role="status" className="flex items-center gap-2 text-xs text-muted-foreground">
206 {pending ? <Spinner className="size-3" /> : <Check className="size-3 text-success" />}206 {pending ? <Spinner size="xs" /> : <Check className="size-3 text-success" />}
207 {pending207 {pending
208 ? state.hostStatus === "installing"208 ? state.hostStatus === "installing"
209 ? compact209 ? compact
...@@ -235,7 +235,7 @@ function DevicePlatformSetup(props: {...@@ -235,7 +235,7 @@ function DevicePlatformSetup(props: {
235 You can use either platform. Fixing a missing platform does not block the other one.235 You can use either platform. Fixing a missing platform does not block the other one.
236 </p>236 </p>
237 <Button size="compact" variant="outline" disabled={props.disabled} onClick={props.onCheck}>237 <Button size="compact" variant="outline" disabled={props.disabled} onClick={props.onCheck}>
238 {props.checking ? <Spinner className="size-3" /> : null}238 {props.checking ? <Spinner size="xs" /> : null}
239 {props.checking ? "Checking…" : "Check again"}239 {props.checking ? "Checking…" : "Check again"}
240 </Button>240 </Button>
241 </div>241 </div>
...@@ -262,7 +262,7 @@ export function AgentDeviceSetupStatus(props: {...@@ -262,7 +262,7 @@ export function AgentDeviceSetupStatus(props: {
262 : "Updating agent access…";262 : "Updating agent access…";
263 return (263 return (
264 <p role="status" className="flex items-center gap-2 text-xs text-muted-foreground">264 <p role="status" className="flex items-center gap-2 text-xs text-muted-foreground">
265 <Spinner className="size-3" />265 <Spinner size="xs" />
266 {label}266 {label}
267 </p>267 </p>
268 );268 );
apps/web/src/components/device/DeviceToolsPanel.tsx+2-2
...@@ -173,7 +173,7 @@ export function DeviceToolsPanel(props: {...@@ -173,7 +173,7 @@ export function DeviceToolsPanel(props: {
173 >173 >
174 <div className="flex h-9 shrink-0 items-center gap-2 border-b px-3">174 <div className="flex h-9 shrink-0 items-center gap-2 border-b px-3">
175 <span className="font-medium">Tools</span>175 <span className="font-medium">Tools</span>
176 {pending ? <Spinner className="size-3.5" /> : null}176 {pending ? <Spinner size="sm" /> : null}
177 <Button177 <Button
178 size="icon-xs"178 size="icon-xs"
179 variant="ghost-muted"179 variant="ghost-muted"
...@@ -190,7 +190,7 @@ export function DeviceToolsPanel(props: {...@@ -190,7 +190,7 @@ export function DeviceToolsPanel(props: {
190 ) : null}190 ) : null}
191 {detail === null && !error ? (191 {detail === null && !error ? (
192 <div className="flex items-center gap-2 px-3 py-3 text-xs text-muted-foreground">192 <div className="flex items-center gap-2 px-3 py-3 text-xs text-muted-foreground">
193 <Spinner className="size-3.5" /> Reading device settings…193 <Spinner size="sm" /> Reading device settings…
194 </div>194 </div>
195 ) : null}195 ) : null}
196196
apps/web/src/components/files/FilePreviewPanel.tsx+3-3
...@@ -169,7 +169,7 @@ function WorkspaceImagePreview(props: {...@@ -169,7 +169,7 @@ function WorkspaceImagePreview(props: {
169 </div>169 </div>
170 ) : (170 ) : (
171 <div className="flex min-h-0 flex-1 items-center justify-center text-muted-foreground">171 <div className="flex min-h-0 flex-1 items-center justify-center text-muted-foreground">
172 <Spinner className="size-5" />172 <Spinner size="lg" />
173 </div>173 </div>
174 );174 );
175}175}
...@@ -214,7 +214,7 @@ function WorkspaceBrowserPreview(props: {...@@ -214,7 +214,7 @@ function WorkspaceBrowserPreview(props: {
214 if (assetUrl._tag !== "Success") {214 if (assetUrl._tag !== "Success") {
215 return (215 return (
216 <div className="flex min-h-0 flex-1 items-center justify-center text-muted-foreground">216 <div className="flex min-h-0 flex-1 items-center justify-center text-muted-foreground">
217 <Spinner className="size-5" />217 <Spinner size="lg" />
218 </div>218 </div>
219 );219 );
220 }220 }
...@@ -1232,7 +1232,7 @@ export default function FilePreviewPanel({...@@ -1232,7 +1232,7 @@ export default function FilePreviewPanel({
1232 </div>1232 </div>
1233 ) : relativePath && file.data === null ? (1233 ) : relativePath && file.data === null ? (
1234 <div className="flex min-h-0 flex-1 items-center justify-center text-muted-foreground">1234 <div className="flex min-h-0 flex-1 items-center justify-center text-muted-foreground">
1235 <Spinner className="size-5" />1235 <Spinner size="lg" />
1236 </div>1236 </div>
1237 ) : relativePath && file.data ? (1237 ) : relativePath && file.data ? (
1238 isMarkdown && renderMarkdown ? (1238 isMarkdown && renderMarkdown ? (
apps/web/src/components/files/fileSurfaceChrome.tsx+1-1
...@@ -131,7 +131,7 @@ export function FileSurfaceLoading(props: { readonly className?: string }) {...@@ -131,7 +131,7 @@ export function FileSurfaceLoading(props: { readonly className?: string }) {
131 props.className,131 props.className,
132 )}132 )}
133 >133 >
134 <Spinner className="size-5" />134 <Spinner size="lg" />
135 </div>135 </div>
136 );136 );
137}137}
apps/web/src/components/onboarding/WelcomeWizard.tsx+2-2
...@@ -1162,7 +1162,7 @@ function ImportStep({...@@ -1162,7 +1162,7 @@ function ImportStep({
1162 <div className="flex h-full min-h-40 flex-col">1162 <div className="flex h-full min-h-40 flex-col">
1163 <h1 className="text-2xl font-semibold tracking-tight text-foreground">Your projects</h1>1163 <h1 className="text-2xl font-semibold tracking-tight text-foreground">Your projects</h1>
1164 <div className="flex flex-1 flex-col items-center justify-center gap-3 py-6">1164 <div className="flex flex-1 flex-col items-center justify-center gap-3 py-6">
1165 <Spinner className="size-5 text-muted-foreground" />1165 <Spinner size="lg" tone="muted" />
1166 <p className="text-center text-sm text-muted-foreground">1166 <p className="text-center text-sm text-muted-foreground">
1167 Looking for projects from Claude Code and Codex…1167 Looking for projects from Claude Code and Codex…
1168 </p>1168 </p>
...@@ -1229,7 +1229,7 @@ function ImportStep({...@@ -1229,7 +1229,7 @@ function ImportStep({
1229 ) : null}1229 ) : null}
1230 {scan.isPending && scan.data === null ? (1230 {scan.isPending && scan.data === null ? (
1231 <div className="flex items-center gap-2 py-3 text-sm text-muted-foreground">1231 <div className="flex items-center gap-2 py-3 text-sm text-muted-foreground">
1232 <Spinner className="size-4" />1232 <Spinner size="md" />
1233 Looking for projects…1233 Looking for projects…
1234 </div>1234 </div>
1235 ) : scan.error !== null ? (1235 ) : scan.error !== null ? (
apps/web/src/components/pullRequest/PullRequestActivityUnavailableState.tsx+1-1
...@@ -23,7 +23,7 @@ export function PullRequestActivityUnavailableState({...@@ -23,7 +23,7 @@ export function PullRequestActivityUnavailableState({
23 <p className="text-sm font-medium text-foreground">Could not load pull request activity</p>23 <p className="text-sm font-medium text-foreground">Could not load pull request activity</p>
24 <p className="max-w-md text-xs text-muted-foreground">{error}</p>24 <p className="max-w-md text-xs text-muted-foreground">{error}</p>
25 <Button size="sm" variant="outline" onClick={onRetry}>25 <Button size="sm" variant="outline" onClick={onRetry}>
26 <RefreshIcon aria-hidden className="size-3.5" />26 <RefreshIcon aria-hidden size="sm" />
27 Retry27 Retry
28 </Button>28 </Button>
29 </div>29 </div>
apps/web/src/components/pullRequest/PullRequestDetailPanel.tsx+2-2
...@@ -2010,7 +2010,7 @@ export function PullRequestDetailPanel({...@@ -2010,7 +2010,7 @@ export function PullRequestDetailPanel({
2010 the spinning glyph in place of the dots: the reader sees the panel2010 the spinning glyph in place of the dots: the reader sees the panel
2011 is fetching without a control appearing or the row shifting. */}2011 is fetching without a control appearing or the row shifting. */}
2012 {refreshing ? (2012 {refreshing ? (
2013 <RefreshIcon refreshing className="size-4" />2013 <RefreshIcon refreshing size="md" />
2014 ) : (2014 ) : (
2015 <MoreHorizontalIcon className="size-4" />2015 <MoreHorizontalIcon className="size-4" />
2016 )}2016 )}
...@@ -2034,7 +2034,7 @@ export function PullRequestDetailPanel({...@@ -2034,7 +2034,7 @@ export function PullRequestDetailPanel({
2034 onPickerOpenChange={setThreadPickerOpen}2034 onPickerOpenChange={setThreadPickerOpen}
2035 />2035 />
2036 <MenuItem disabled={refreshing} onClick={() => void refreshFromHost()}>2036 <MenuItem disabled={refreshing} onClick={() => void refreshFromHost()}>
2037 <RefreshIcon className="size-3.5" refreshing={refreshing} />2037 <RefreshIcon size="sm" refreshing={refreshing} />
2038 Refresh2038 Refresh
2039 </MenuItem>2039 </MenuItem>
2040 <MenuItem disabled={handoff !== null} onClick={askAboutPullRequest}>2040 <MenuItem disabled={handoff !== null} onClick={askAboutPullRequest}>
apps/web/src/components/pullRequest/PullRequestListEmptyState.tsx+2-2
...@@ -150,7 +150,7 @@ export function PullRequestListEmptyState({...@@ -150,7 +150,7 @@ export function PullRequestListEmptyState({
150 {/* The hosts answered this query once; a pull request opened since then would answer150 {/* The hosts answered this query once; a pull request opened since then would answer
151 differently, and nothing on screen says which of the two the reader is looking at. */}151 differently, and nothing on screen says which of the two the reader is looking at. */}
152 <Button size="sm" variant="outline" disabled={refreshing} onClick={onRefresh}>152 <Button size="sm" variant="outline" disabled={refreshing} onClick={onRefresh}>
153 <RefreshIcon className="size-3.5" refreshing={refreshing} />153 <RefreshIcon size="sm" refreshing={refreshing} />
154 {refreshing ? "Checking..." : "Check again"}154 {refreshing ? "Checking..." : "Check again"}
155 </Button>155 </Button>
156 </EmptyContent>156 </EmptyContent>
...@@ -176,7 +176,7 @@ export function PullRequestListEmptyState({...@@ -176,7 +176,7 @@ export function PullRequestListEmptyState({
176 </Button>176 </Button>
177 ) : null}177 ) : null}
178 <Button size="sm" variant="outline" disabled={refreshing} onClick={onRefresh}>178 <Button size="sm" variant="outline" disabled={refreshing} onClick={onRefresh}>
179 <RefreshIcon className="size-3.5" refreshing={refreshing} />179 <RefreshIcon size="sm" refreshing={refreshing} />
180 {refreshing ? "Checking..." : "Check again"}180 {refreshing ? "Checking..." : "Check again"}
181 </Button>181 </Button>
182 </EmptyContent>182 </EmptyContent>
apps/web/src/components/pullRequest/PullRequestsUnavailableState.tsx+1-1
...@@ -46,7 +46,7 @@ export function PullRequestsUnavailableState({...@@ -46,7 +46,7 @@ export function PullRequestsUnavailableState({
46 disabled={refreshing}46 disabled={refreshing}
47 aria-busy={refreshing}47 aria-busy={refreshing}
48 >48 >
49 <RefreshIcon className="size-3.5" refreshing={refreshing} />49 <RefreshIcon size="sm" refreshing={refreshing} />
50 Retry50 Retry
51 </Button>51 </Button>
52 ) : null}52 ) : null}
apps/web/src/components/search/ProjectContentSearchDialog.tsx+1-1
...@@ -226,7 +226,7 @@ function OpenContentSearchDialog(props: {...@@ -226,7 +226,7 @@ function OpenContentSearchDialog(props: {
226 <div className="flex h-9 shrink-0 items-center border-b px-3 text-xs text-muted-foreground">226 <div className="flex h-9 shrink-0 items-center border-b px-3 text-xs text-muted-foreground">
227 {search.isPending ? (227 {search.isPending ? (
228 <span className="flex items-center gap-2">228 <span className="flex items-center gap-2">
229 <Spinner className="size-3.5" /> Searching…229 <Spinner size="sm" /> Searching…
230 </span>230 </span>
231 ) : search.error ? (231 ) : search.error ? (
232 <span className="text-destructive">{search.error}</span>232 <span className="text-destructive">{search.error}</span>
apps/web/src/components/settings/BrowserImportWizard.tsx+2-2
...@@ -491,7 +491,7 @@ function ImportingStep() {...@@ -491,7 +491,7 @@ function ImportingStep() {
491 <DialogDescription>This may take a moment.</DialogDescription>491 <DialogDescription>This may take a moment.</DialogDescription>
492 </DialogHeader>492 </DialogHeader>
493 <DialogPanel className="flex items-center gap-3 py-6">493 <DialogPanel className="flex items-center gap-3 py-6">
494 <Spinner className="size-4 text-muted-foreground" />494 <Spinner size="md" tone="muted" />
495 <span className="text-sm text-muted-foreground">Importing…</span>495 <span className="text-sm text-muted-foreground">Importing…</span>
496 </DialogPanel>496 </DialogPanel>
497 </>497 </>
...@@ -516,7 +516,7 @@ function CheckingStep({...@@ -516,7 +516,7 @@ function CheckingStep({
516 </DialogDescription>516 </DialogDescription>
517 </DialogHeader>517 </DialogHeader>
518 <DialogPanel className="flex items-center gap-3 py-6">518 <DialogPanel className="flex items-center gap-3 py-6">
519 <Spinner className="size-4 text-muted-foreground" />519 <Spinner size="md" tone="muted" />
520 <span className="text-sm text-muted-foreground">520 <span className="text-sm text-muted-foreground">
521 {check === "fullDiskAccess" ? "Checking access…" : "Checking…"}521 {check === "fullDiskAccess" ? "Checking access…" : "Checking…"}
522 </span>522 </span>
apps/web/src/components/settings/ConnectionsSettings.tsx+6-6
...@@ -3435,7 +3435,7 @@ export function ConnectionsSettings() {...@@ -3435,7 +3435,7 @@ export function ConnectionsSettings() {
3435 pendingDesktopServerExposureMode === null || isUpdatingDesktopServerExposure3435 pendingDesktopServerExposureMode === null || isUpdatingDesktopServerExposure
3436 }3436 }
3437 >3437 >
3438 {isUpdatingDesktopServerExposure && <Spinner className="size-3.5" />}3438 {isUpdatingDesktopServerExposure && <Spinner size="sm" />}
3439 <span className="[text-box:trim-both_cap_alphabetic]">3439 <span className="[text-box:trim-both_cap_alphabetic]">
3440 {isUpdatingDesktopServerExposure3440 {isUpdatingDesktopServerExposure
3441 ? "Restarting…"3441 ? "Restarting…"
...@@ -3499,7 +3499,7 @@ export function ConnectionsSettings() {...@@ -3499,7 +3499,7 @@ export function ConnectionsSettings() {
3499 >3499 >
3500 {isUpdatingWslBackend ? (3500 {isUpdatingWslBackend ? (
3501 <>3501 <>
3502 <Spinner className="size-3.5" />3502 <Spinner size="sm" />
3503 Applying…3503 Applying…
3504 </>3504 </>
3505 ) : (3505 ) : (
...@@ -3513,7 +3513,7 @@ export function ConnectionsSettings() {...@@ -3513,7 +3513,7 @@ export function ConnectionsSettings() {
3513 >3513 >
3514 {isUpdatingWslBackend ? (3514 {isUpdatingWslBackend ? (
3515 <>3515 <>
3516 <Spinner className="size-3.5" />3516 <Spinner size="sm" />
3517 Applying…3517 Applying…
3518 </>3518 </>
3519 ) : (3519 ) : (
...@@ -3534,7 +3534,7 @@ export function ConnectionsSettings() {...@@ -3534,7 +3534,7 @@ export function ConnectionsSettings() {
3534 >3534 >
3535 {isUpdatingWslBackend ? (3535 {isUpdatingWslBackend ? (
3536 <>3536 <>
3537 <Spinner className="size-3.5" />3537 <Spinner size="sm" />
3538 Applying…3538 Applying…
3539 </>3539 </>
3540 ) : pendingWslChange?.kind === "disable" ? (3540 ) : pendingWslChange?.kind === "disable" ? (
...@@ -3583,7 +3583,7 @@ export function ConnectionsSettings() {...@@ -3583,7 +3583,7 @@ export function ConnectionsSettings() {
3583 >3583 >
3584 {isUpdatingTailscaleServe ? (3584 {isUpdatingTailscaleServe ? (
3585 <>3585 <>
3586 <Spinner className="size-3.5" />3586 <Spinner size="sm" />
3587 Restarting…3587 Restarting…
3588 </>3588 </>
3589 ) : (3589 ) : (
...@@ -3657,7 +3657,7 @@ export function ConnectionsSettings() {...@@ -3657,7 +3657,7 @@ export function ConnectionsSettings() {
3657 >3657 >
3658 {isUpdatingTailscaleServe ? (3658 {isUpdatingTailscaleServe ? (
3659 <>3659 <>
3660 <Spinner className="size-3.5" />3660 <Spinner size="sm" />
3661 Restarting…3661 Restarting…
3662 </>3662 </>
3663 ) : (3663 ) : (
apps/web/src/components/settings/DeviceHostEditor.tsx+3-3
...@@ -156,7 +156,7 @@ export function DeviceHostEditor({...@@ -156,7 +156,7 @@ export function DeviceHostEditor({
156 if (Option.isSome(input)) void testConnection(input.value);156 if (Option.isSome(input)) void testConnection(input.value);
157 }}157 }}
158 >158 >
159 {checking ? <Spinner className="size-3" /> : null} Test connection159 {checking ? <Spinner size="xs" /> : null} Test connection
160 </Button>160 </Button>
161 </div>161 </div>
162 {results ? (162 {results ? (
...@@ -173,7 +173,7 @@ export function DeviceHostEditor({...@@ -173,7 +173,7 @@ export function DeviceHostEditor({
173 >173 >
174 {result.status === "pending" ? (174 {result.status === "pending" ? (
175 <>175 <>
176 <Spinner className="size-3" /> Checking…176 <Spinner size="xs" /> Checking…
177 </>177 </>
178 ) : result.status === "local" ? (178 ) : result.status === "local" ? (
179 <>179 <>
...@@ -215,7 +215,7 @@ export function DeviceHostEditor({...@@ -215,7 +215,7 @@ export function DeviceHostEditor({
215 Cancel215 Cancel
216 </Button>216 </Button>
217 <Button type="submit" disabled={busy || checking || !valid || !draft.label.trim()}>217 <Button type="submit" disabled={busy || checking || !valid || !draft.label.trim()}>
218 {busy ? <Spinner className="size-3" /> : null} Save host218 {busy ? <Spinner size="xs" /> : null} Save host
219 </Button>219 </Button>
220 </DialogFooter>220 </DialogFooter>
221 </DialogPopup>221 </DialogPopup>
apps/web/src/components/settings/DeviceHostsSettings.tsx+1-1
...@@ -267,7 +267,7 @@ function DeviceHostList({...@@ -267,7 +267,7 @@ function DeviceHostList({
267 role="status"267 role="status"
268 className="inline-flex items-center gap-1.5 text-xs text-muted-foreground"268 className="inline-flex items-center gap-1.5 text-xs text-muted-foreground"
269 >269 >
270 <Spinner className="size-3" />270 <Spinner size="xs" />
271 {progress}271 {progress}
272 </span>272 </span>
273 ) : null}273 ) : null}
apps/web/src/components/settings/LocalEnvironmentSetting.tsx+1-1
...@@ -86,7 +86,7 @@ export function LocalEnvironmentSetting() {...@@ -86,7 +86,7 @@ export function LocalEnvironmentSetting() {
86 >86 >
87 {isUpdating ? (87 {isUpdating ? (
88 <>88 <>
89 <Spinner className="size-3.5" />89 <Spinner size="sm" />
90 Restarting…90 Restarting…
91 </>91 </>
92 ) : enabled ? (92 ) : enabled ? (
apps/web/src/components/settings/ResourceTelemetryDiagnostics.tsx+3-3
...@@ -976,7 +976,7 @@ export function ResourceTelemetryDiagnostics({...@@ -976,7 +976,7 @@ export function ResourceTelemetryDiagnostics({
976 onClick={telemetry.refresh}976 onClick={telemetry.refresh}
977 aria-label="Refresh resource telemetry"977 aria-label="Refresh resource telemetry"
978 >978 >
979 <RefreshIcon className="size-3" refreshing={telemetry.isPending} />979 <RefreshIcon size="xs" refreshing={telemetry.isPending} />
980 </Button>980 </Button>
981 }981 }
982 />982 />
...@@ -1087,7 +1087,7 @@ export function ResourceTelemetryDiagnostics({...@@ -1087,7 +1087,7 @@ export function ResourceTelemetryDiagnostics({
1087 headerAction={1087 headerAction={
1088 collectorNeedsRetry ? (1088 collectorNeedsRetry ? (
1089 <Button size="xs" variant="outline" disabled={isRetrying} onClick={retryCollector}>1089 <Button size="xs" variant="outline" disabled={isRetrying} onClick={retryCollector}>
1090 <RefreshIcon className="size-3" refreshing={isRetrying} />1090 <RefreshIcon size="xs" refreshing={isRetrying} />
1091 Retry monitor1091 Retry monitor
1092 </Button>1092 </Button>
1093 ) : null1093 ) : null
...@@ -1225,7 +1225,7 @@ export function ResourceTelemetryDiagnostics({...@@ -1225,7 +1225,7 @@ export function ResourceTelemetryDiagnostics({
1225 onClick={history.refresh}1225 onClick={history.refresh}
1226 aria-label="Refresh resource history"1226 aria-label="Refresh resource history"
1227 >1227 >
1228 <RefreshIcon className="size-3" refreshing={history.isPending} />1228 <RefreshIcon size="xs" refreshing={history.isPending} />
1229 </Button>1229 </Button>
1230 </div>1230 </div>
1231 }1231 }
apps/web/src/components/settings/SettingsPanels.tsx+1-1
...@@ -3359,7 +3359,7 @@ export function ArchivedThreadsPanel() {...@@ -3359,7 +3359,7 @@ export function ArchivedThreadsPanel() {
3359 title={3359 title={
3360 <span className="inline-flex items-center gap-2">3360 <span className="inline-flex items-center gap-2">
3361 {isLoadingArchive ? (3361 {isLoadingArchive ? (
3362 <Spinner className="size-3.5 text-muted-foreground" />3362 <Spinner size="sm" tone="muted" />
3363 ) : (3363 ) : (
3364 <ArchiveIcon className="size-3.5 text-muted-foreground" />3364 <ArchiveIcon className="size-3.5 text-muted-foreground" />
3365 )}3365 )}
apps/web/src/components/settings/SourceControlSettings.tsx+1-1
...@@ -491,7 +491,7 @@ function EmptySourceControlDiscovery({...@@ -491,7 +491,7 @@ function EmptySourceControlDiscovery({
491 </EmptyHeader>491 </EmptyHeader>
492 <EmptyContent>492 <EmptyContent>
493 <Button size="sm" variant="outline" onClick={onScan} disabled={isPending}>493 <Button size="sm" variant="outline" onClick={onScan} disabled={isPending}>
494 <RefreshIcon className="size-3.5" refreshing={isPending} />494 <RefreshIcon size="sm" refreshing={isPending} />
495 Scan495 Scan
496 </Button>496 </Button>
497 </EmptyContent>497 </EmptyContent>
apps/web/src/components/sidebar/SidebarProviderUpdatePill.tsx+1-1
...@@ -174,7 +174,7 @@ export function SidebarProviderUpdatePill() {...@@ -174,7 +174,7 @@ export function SidebarProviderUpdatePill() {
174 onClick={openProviderSettings}174 onClick={openProviderSettings}
175 >175 >
176 {displayedView.tone === "loading" ? (176 {displayedView.tone === "loading" ? (
177 <Spinner className="size-3.5 shrink-0" />177 <Spinner size="sm" className="shrink-0" />
178 ) : displayedView.tone === "success" ? (178 ) : displayedView.tone === "success" ? (
179 <CircleCheckIcon className="size-3.5 shrink-0" />179 <CircleCheckIcon className="size-3.5 shrink-0" />
180 ) : displayedView.tone === "error" ? (180 ) : displayedView.tone === "error" ? (
apps/web/src/components/ui/refresh-icon.tsx+21-2
...@@ -1,19 +1,38 @@...@@ -1,19 +1,38 @@
1import { RefreshCwIcon } from "lucide-react";1import { RefreshCwIcon } from "lucide-react";
2import { cva, type VariantProps } from "class-variance-authority";
23
3import { cn } from "~/lib/utils";4import { cn } from "~/lib/utils";
4import { observeVisibleAnimation } from "~/lib/visibleAnimation";5import { observeVisibleAnimation } from "~/lib/visibleAnimation";
56
7// No default size: inside a Button the parent's svg rule sizes the glyph.
8const refreshIconVariants = cva("", {
9 variants: {
10 size: {
11 xs: "size-3",
12 sm: "size-3.5",
13 md: "size-4",
14 lg: "size-5",
15 },
16 },
17});
18
6/** Keep the refresh glyph in place while its owning action is running. */19/** Keep the refresh glyph in place while its owning action is running. */
7export function RefreshIcon({20export function RefreshIcon({
8 refreshing = false,21 refreshing = false,
9 className,22 className,
23 size,
10 ...props24 ...props
11}: React.ComponentPropsWithoutRef<typeof RefreshCwIcon> & { refreshing?: boolean }) {25}: React.ComponentPropsWithoutRef<typeof RefreshCwIcon> &
26 VariantProps<typeof refreshIconVariants> & { refreshing?: boolean }) {
12 return (27 return (
13 <RefreshCwIcon28 <RefreshCwIcon
14 aria-hidden29 aria-hidden
15 ref={refreshing ? observeVisibleAnimation : undefined}30 ref={refreshing ? observeVisibleAnimation : undefined}
16 className={cn(refreshing && "motion-safe:visible-animate-spin", className)}31 className={cn(
32 refreshIconVariants({ size }),
33 refreshing && "motion-safe:visible-animate-spin",
34 className,
35 )}
17 {...props}36 {...props}
18 />37 />
19 );38 );
apps/web/src/components/ui/spinner.tsx+25-2
...@@ -1,13 +1,36 @@...@@ -1,13 +1,36 @@
1import { LoaderCircleIcon } from "lucide-react";1import { LoaderCircleIcon } from "lucide-react";
2import { cva, type VariantProps } from "class-variance-authority";
2import { observeVisibleAnimation } from "~/lib/visibleAnimation";3import { observeVisibleAnimation } from "~/lib/visibleAnimation";
3import { cn } from "~/lib/utils";4import { cn } from "~/lib/utils";
45
5function Spinner({ className, ...props }: React.ComponentPropsWithoutRef<typeof LoaderCircleIcon>) {6// No default size: inside a Button the parent's svg rule sizes the glyph.
7const spinnerVariants = cva("motion-safe:visible-animate-spin", {
8 variants: {
9 size: {
10 xs: "size-3",
11 sm: "size-3.5",
12 md: "size-4",
13 lg: "size-5",
14 },
15 tone: {
16 current: "",
17 muted: "text-muted-foreground",
18 },
19 },
20 defaultVariants: { tone: "current" },
21});
22
23function Spinner({
24 className,
25 size,
26 tone,
27 ...props
28}: React.ComponentPropsWithoutRef<typeof LoaderCircleIcon> & VariantProps<typeof spinnerVariants>) {
6 return (29 return (
7 <LoaderCircleIcon30 <LoaderCircleIcon
8 aria-label="Loading"31 aria-label="Loading"
9 ref={observeVisibleAnimation}32 ref={observeVisibleAnimation}
10 className={cn("motion-safe:visible-animate-spin", className)}33 className={cn(spinnerVariants({ size, tone }), className)}
11 role="status"34 role="status"
12 {...props}35 {...props}
13 />36 />
apps/web/src/components/usage/UsagePage.tsx+2-2
...@@ -300,7 +300,7 @@ export function UsagePage() {...@@ -300,7 +300,7 @@ export function UsagePage() {
300 size="icon-sm"300 size="icon-sm"
301 variant="ghost"301 variant="ghost"
302 >302 >
303 <RefreshIcon className="size-3.5" refreshing={isRefreshing} />303 <RefreshIcon size="sm" refreshing={isRefreshing} />
304 </Button>304 </Button>
305 </div>305 </div>
306 <div className="col-span-2 ms-auto flex min-w-0 items-center justify-end gap-1 xl:hidden">306 <div className="col-span-2 ms-auto flex min-w-0 items-center justify-end gap-1 xl:hidden">
...@@ -359,7 +359,7 @@ export function UsagePage() {...@@ -359,7 +359,7 @@ export function UsagePage() {
359 size="icon-sm"359 size="icon-sm"
360 variant="ghost"360 variant="ghost"
361 >361 >
362 <RefreshIcon className="size-3.5" refreshing={isRefreshing} />362 <RefreshIcon size="sm" refreshing={isRefreshing} />
363 </Button>363 </Button>
364 </div>364 </div>
365 </div>365 </div>
apps/web/src/routes/_chat.index.tsx+1-1
...@@ -99,7 +99,7 @@ function DraftStartError({ onRetry }: { readonly onRetry: () => void }) {...@@ -99,7 +99,7 @@ function DraftStartError({ onRetry }: { readonly onRetry: () => void }) {
99 </EmptyDescription>99 </EmptyDescription>
100 <div className="mt-5 flex justify-center">100 <div className="mt-5 flex justify-center">
101 <Button size="sm" onClick={onRetry}>101 <Button size="sm" onClick={onRetry}>
102 <RefreshIcon className="size-4" />102 <RefreshIcon size="md" />
103 Try again103 Try again
104 </Button>104 </Button>
105 </div>105 </div>
apps/web/src/routes/_chat.pull-requests.tsx+2-2
...@@ -1830,7 +1830,7 @@ function PullRequestsRouteView() {...@@ -1830,7 +1830,7 @@ function PullRequestsRouteView() {
1830 <div className="flex justify-center py-3 text-xs text-muted-foreground">1830 <div className="flex justify-center py-3 text-xs text-muted-foreground">
1831 {loadingMore ? (1831 {loadingMore ? (
1832 <span className="flex items-center gap-2">1832 <span className="flex items-center gap-2">
1833 <Spinner aria-hidden className="size-3.5" />1833 <Spinner aria-hidden size="sm" />
1834 {sentCursors === null ? "Updating pull requests" : "Loading more"}1834 {sentCursors === null ? "Updating pull requests" : "Loading more"}
1835 </span>1835 </span>
1836 ) : canContinue || pageSize < MAX_PAGE_SIZE ? (1836 ) : canContinue || pageSize < MAX_PAGE_SIZE ? (
...@@ -2592,7 +2592,7 @@ function PullRequestRefreshControl({...@@ -2592,7 +2592,7 @@ function PullRequestRefreshControl({
2592 onClick={onRefresh}2592 onClick={onRefresh}
2593 disabled={refreshing}2593 disabled={refreshing}
2594 >2594 >
2595 <RefreshIcon className="size-4" refreshing={refreshing} />2595 <RefreshIcon size="md" refreshing={refreshing} />
2596 </Button>2596 </Button>
2597 );2597 );
2598}2598}
scripts/lint-restyle-ceiling.ts+1-1
...@@ -8,7 +8,7 @@ import * as NodeURL from "node:url";...@@ -8,7 +8,7 @@ import * as NodeURL from "node:url";
8// components/ui exports are migrated to variants (see vite.config.ts). This gate keeps the8// components/ui exports are migrated to variants (see vite.config.ts). This gate keeps the
9// count from growing: CI fails when findings exceed the ceiling. Lower the ceiling when you9// count from growing: CI fails when findings exceed the ceiling. Lower the ceiling when you
10// migrate a file, and delete this script when the rule becomes an error.10// migrate a file, and delete this script when the rule becomes an error.
11export const RESTYLE_CEILING = 1214;11export const RESTYLE_CEILING = 1207;
1212
13const RULE = "shadcn(no-restyle)";13const RULE = "shadcn(no-restyle)";
1414