feat: add spinner, lowercase version

This commit is contained in:
Austin Pickett 2026-04-28 13:59:33 -04:00
parent 912590a143
commit 47d4b6e31a
16 changed files with 292 additions and 131 deletions

View file

@ -27,7 +27,6 @@ import {
Globe,
Heart,
KeyRound,
Loader2,
Menu,
MessageSquare,
Package,
@ -46,6 +45,7 @@ import {
Button,
ListItem,
SelectionSwitcher,
Spinner,
Typography,
} from "@nous-research/ui";
import { cn } from "@/lib/utils";
@ -573,10 +573,7 @@ export default function App() {
aria-live="polite"
>
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<Loader2
className="h-4 w-4 animate-spin"
aria-hidden
/>
<Spinner />
<span>Loading chat</span>
</div>
</div>
@ -681,12 +678,13 @@ function SidebarSystemActions({ onNavigate }: { onNavigate: () => void }) {
)}
>
{isPending ? (
<Loader2 className="h-3.5 w-3.5 shrink-0 animate-spin" />
<Spinner className="shrink-0 text-[0.875rem]" />
) : isActionRunning && spin ? (
<Spinner className="shrink-0 text-[0.875rem]" />
) : (
<Icon
className={cn(
"h-3.5 w-3.5 shrink-0",
isActionRunning && spin && "animate-spin",
isActionRunning && !spin && "animate-pulse",
)}
/>

View file

@ -1,5 +1,6 @@
import { useEffect, useRef, useState } from "react";
import { Brain, Eye, Gauge, Lightbulb, Wrench, Loader2 } from "lucide-react";
import { Brain, Eye, Gauge, Lightbulb, Wrench } from "lucide-react";
import { Spinner } from "@nous-research/ui";
import { api } from "@/lib/api";
import type { ModelInfoResponse } from "@/lib/api";
import { formatTokenCount } from "@/lib/format";
@ -36,7 +37,7 @@ export function ModelInfoCard({
if (loading) {
return (
<div className="flex items-center gap-2 py-2 text-xs text-muted-foreground">
<Loader2 className="h-3 w-3 animate-spin" />
<Spinner className="text-xs" />
Loading model info
</div>
);

View file

@ -1,7 +1,7 @@
import { Button, ListItem } from "@nous-research/ui";
import { Button, ListItem, Spinner } from "@nous-research/ui";
import { Input } from "@/components/ui/input";
import type { GatewayClient } from "@/lib/gatewayClient";
import { Check, Loader2, Search, X } from "lucide-react";
import { Check, Search, X } from "lucide-react";
import { useEffect, useMemo, useRef, useState } from "react";
/**
@ -261,7 +261,7 @@ function ProviderColumn({
<div className="border-r border-border overflow-y-auto">
{loading && (
<div className="flex items-center gap-2 p-4 text-xs text-muted-foreground">
<Loader2 className="h-3 w-3 animate-spin" /> loading
<Spinner className="text-xs" /> loading
</div>
)}

View file

@ -1,6 +1,6 @@
import { useEffect, useRef, useState } from "react";
import { ExternalLink, X, Check, Loader2 } from "lucide-react";
import { Button, CopyButton, H2 } from "@nous-research/ui";
import { ExternalLink, X, Check } from "lucide-react";
import { Button, CopyButton, H2, Spinner } from "@nous-research/ui";
import { api, type OAuthProvider, type OAuthStartResponse } from "@/lib/api";
import { Input } from "@/components/ui/input";
import { useI18n } from "@/i18n";
@ -200,7 +200,7 @@ export function OAuthLoginModal({ provider, onClose, onSuccess }: Props) {
{phase === "starting" && (
<div className="flex items-center gap-3 py-6 text-sm text-muted-foreground">
<Loader2 className="h-4 w-4 animate-spin" />
<Spinner />
{t.oauth.initiatingLogin}
</div>
)}
@ -246,7 +246,7 @@ export function OAuthLoginModal({ provider, onClose, onSuccess }: Props) {
{phase === "submitting" && (
<div className="flex items-center gap-3 py-6 text-sm text-muted-foreground">
<Loader2 className="h-4 w-4 animate-spin" />
<Spinner />
{t.oauth.exchangingCode}
</div>
)}
@ -295,7 +295,7 @@ export function OAuthLoginModal({ provider, onClose, onSuccess }: Props) {
{t.oauth.reOpenVerification}
</a>
<div className="flex items-center gap-2 text-xs text-muted-foreground border-t border-border pt-3">
<Loader2 className="h-3 w-3 animate-spin" />
<Spinner className="text-xs" />
{t.oauth.waitingAuth}
</div>
</>

View file

@ -9,7 +9,7 @@ import {
LogIn,
} from "lucide-react";
import { api, type OAuthProvider } from "@/lib/api";
import { Button, CopyButton } from "@nous-research/ui";
import { Button, CopyButton, Spinner } from "@nous-research/ui";
import {
Card,
CardContent,
@ -106,9 +106,7 @@ export function OAuthProvidersCard({ onError, onSuccess }: Props) {
outlined
onClick={refresh}
disabled={loading}
prefix={
<RefreshCw className={loading ? "animate-spin" : undefined} />
}
prefix={loading ? <Spinner /> : <RefreshCw />}
>
{t.common.refresh}
</Button>
@ -122,7 +120,7 @@ export function OAuthProvidersCard({ onError, onSuccess }: Props) {
<CardContent>
{loading && providers === null && (
<div className="flex items-center justify-center py-8">
<div className="h-5 w-5 animate-spin rounded-full border-2 border-primary border-t-transparent" />
<Spinner className="text-xl text-primary" />
</div>
)}
{providers && providers.length === 0 && (
@ -238,13 +236,7 @@ export function OAuthProvidersCard({ onError, onSuccess }: Props) {
outlined
onClick={() => handleDisconnect(p)}
disabled={isBusy}
prefix={
isBusy ? (
<RefreshCw className="animate-spin" />
) : (
<LogOut />
)
}
prefix={isBusy ? <Spinner /> : <LogOut />}
>
{t.oauth.disconnect}
</Button>

View file

@ -17,7 +17,7 @@ export function SidebarFooter() {
>
<Typography
mondwest
className="font-mono-ui text-[0.7rem] tabular-nums tracking-[0.1em] text-muted-foreground/70"
className="font-mono-ui text-[0.7rem] tabular-nums tracking-[0.1em] text-muted-foreground/70 lowercase"
>
{status?.version != null ? `v${status.version}` : "—"}
</Typography>

View file

@ -8,7 +8,7 @@ import type {
AnalyticsSkillEntry,
} from "@/lib/api";
import { timeAgo } from "@/lib/utils";
import { Button, Stats } from "@nous-research/ui";
import { Button, Spinner, Stats } from "@nous-research/ui";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@nous-research/ui";
import { usePageHeader } from "@/contexts/usePageHeader";
@ -352,9 +352,7 @@ export default function AnalyticsPage() {
PERIODS.find((p) => p.days === days)?.label ?? `${days}d`;
setAfterTitle(
<span className="flex items-center gap-2">
{loading && (
<div className="h-4 w-4 shrink-0 animate-spin rounded-full border-2 border-primary border-t-transparent" />
)}
{loading && <Spinner className="shrink-0 text-base text-primary" />}
<Badge tone="secondary" className="text-[10px]">
{periodLabel}
</Badge>
@ -381,7 +379,7 @@ export default function AnalyticsPage() {
outlined
onClick={load}
disabled={loading}
prefix={<RefreshCw />}
prefix={loading ? <Spinner /> : <RefreshCw />}
>
{t.common.refresh}
</Button>
@ -402,7 +400,7 @@ export default function AnalyticsPage() {
<PluginSlot name="analytics:top" />
{loading && !data && (
<div className="flex items-center justify-center py-24">
<div className="h-6 w-6 animate-spin rounded-full border-2 border-primary border-t-transparent" />
<Spinner className="text-2xl text-primary" />
</div>
)}

View file

@ -33,7 +33,7 @@ import { getNestedValue, setNestedValue } from "@/lib/nested";
import { useToast } from "@/hooks/useToast";
import { Toast } from "@/components/Toast";
import { AutoField } from "@/components/AutoField";
import { Button, ListItem } from "@nous-research/ui";
import { Button, ListItem, Spinner } from "@nous-research/ui";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Badge } from "@nous-research/ui";
@ -319,7 +319,7 @@ export default function ConfigPage() {
if (!config || !schema) {
return (
<div className="flex items-center justify-center py-24">
<div className="h-6 w-6 animate-spin rounded-full border-2 border-primary border-t-transparent" />
<Spinner className="text-2xl text-primary" />
</div>
);
}
@ -483,7 +483,7 @@ export default function ConfigPage() {
<CardContent className="p-0">
{yamlLoading ? (
<div className="flex items-center justify-center py-12">
<div className="h-5 w-5 animate-spin rounded-full border-2 border-primary border-t-transparent" />
<Spinner className="text-xl text-primary" />
</div>
) : (
<textarea

View file

@ -1,6 +1,6 @@
import { useCallback, useEffect, useState } from "react";
import { Clock, Pause, Play, Plus, Trash2, Zap } from "lucide-react";
import { Badge, Button, H2, Select, SelectOption } from "@nous-research/ui";
import { Badge, Button, H2, Select, SelectOption, Spinner } from "@nous-research/ui";
import { api } from "@/lib/api";
import type { CronJob } from "@/lib/api";
import { DeleteConfirmDialog } from "@/components/DeleteConfirmDialog";
@ -136,7 +136,7 @@ export default function CronPage() {
if (loading) {
return (
<div className="flex items-center justify-center py-24">
<div className="h-6 w-6 animate-spin rounded-full border-2 border-primary border-t-transparent" />
<Spinner className="text-2xl text-primary" />
</div>
);
}

View file

@ -21,7 +21,7 @@ import { Toast } from "@/components/Toast";
import { useConfirmDelete } from "@/hooks/useConfirmDelete";
import { useToast } from "@/hooks/useToast";
import { OAuthProvidersCard } from "@/components/OAuthProvidersCard";
import { Button, ListItem } from "@nous-research/ui";
import { Button, ListItem, Spinner } from "@nous-research/ui";
import {
Card,
CardContent,
@ -651,7 +651,7 @@ export default function EnvPage() {
if (!vars) {
return (
<div className="flex items-center justify-center py-24">
<div className="h-6 w-6 animate-spin rounded-full border-2 border-primary border-t-transparent" />
<Spinner className="text-2xl text-primary" />
</div>
);
}

View file

@ -12,6 +12,7 @@ import {
Button,
FilterGroup,
Segmented,
Spinner,
Switch,
} from "@nous-research/ui";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
@ -82,9 +83,7 @@ export default function LogsPage() {
useLayoutEffect(() => {
setAfterTitle(
<span className="flex items-center gap-2">
{loading && (
<div className="h-4 w-4 shrink-0 animate-spin rounded-full border-2 border-primary border-t-transparent" />
)}
{loading && <Spinner className="shrink-0 text-base text-primary" />}
<Badge tone="secondary" className="text-[10px]">
{file} · {level} · {component}
</Badge>
@ -114,7 +113,7 @@ export default function LogsPage() {
outlined
onClick={fetchLogs}
disabled={loading}
prefix={<RefreshCw />}
prefix={loading ? <Spinner /> : <RefreshCw />}
>
{t.common.refresh}
</Button>

View file

@ -13,7 +13,6 @@ import {
ChevronLeft,
ChevronRight,
Database,
Loader2,
MessageSquare,
Search,
Trash2,
@ -36,7 +35,7 @@ import { timeAgo } from "@/lib/utils";
import { Markdown } from "@/components/Markdown";
import { PlatformsCard } from "@/components/PlatformsCard";
import { Toast } from "@/components/Toast";
import { Button, ListItem } from "@nous-research/ui";
import { Button, ListItem, Spinner } from "@nous-research/ui";
import { Badge } from "@nous-research/ui";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { DeleteConfirmDialog } from "@/components/DeleteConfirmDialog";
@ -388,7 +387,7 @@ function SessionRow({
<div className="border-t border-border bg-background/50 p-4">
{loading && (
<div className="flex items-center justify-center py-8">
<div className="h-5 w-5 animate-spin rounded-full border-2 border-primary border-t-transparent" />
<Spinner className="text-xl text-primary" />
</div>
)}
{error && (
@ -444,7 +443,7 @@ export default function SessionsPage() {
setEnd(
<div className="relative w-full min-w-0 sm:max-w-xs">
{searching ? (
<div className="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 animate-spin rounded-full border-[1.5px] border-primary border-t-transparent" />
<Spinner className="absolute left-2.5 top-1/2 -translate-y-1/2 text-[0.875rem] text-primary" />
) : (
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground" />
)}
@ -617,7 +616,7 @@ export default function SessionsPage() {
if (loading) {
return (
<div className="flex items-center justify-center py-24">
<div className="h-6 w-6 animate-spin rounded-full border-2 border-primary border-t-transparent" />
<Spinner className="text-2xl text-primary" />
</div>
);
}
@ -667,13 +666,13 @@ export default function SessionsPage() {
<div className="flex items-center justify-between gap-2 border-b border-border px-3 py-2">
<div className="flex items-center gap-2 min-w-0">
{actionStatus?.running ? (
<Loader2 className="h-3.5 w-3.5 shrink-0 animate-spin text-warning" />
<Spinner className="shrink-0 text-[0.875rem] text-warning" />
) : actionStatus?.exit_code === 0 ? (
<CheckCircle2 className="h-3.5 w-3.5 shrink-0 text-success" />
) : actionStatus !== null ? (
<AlertTriangle className="h-3.5 w-3.5 shrink-0 text-destructive" />
) : (
<Loader2 className="h-3.5 w-3.5 shrink-0 animate-spin text-muted-foreground" />
<Spinner className="shrink-0 text-[0.875rem] text-muted-foreground" />
)}
<span className="text-xs font-mondwest tracking-[0.12em] truncate">

View file

@ -20,7 +20,7 @@ import type { SkillInfo, ToolsetInfo } from "@/lib/api";
import { useToast } from "@/hooks/useToast";
import { Toast } from "@/components/Toast";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge, Button, ListItem, Switch } from "@nous-research/ui";
import { Badge, Button, ListItem, Spinner, Switch } from "@nous-research/ui";
import { cn } from "@/lib/utils";
import { Input } from "@/components/ui/input";
import { useI18n } from "@/i18n";
@ -239,7 +239,7 @@ export default function SkillsPage() {
if (loading) {
return (
<div className="flex items-center justify-center py-24">
<div className="h-6 w-6 animate-spin rounded-full border-2 border-primary border-t-transparent" />
<Spinner className="text-2xl text-primary" />
</div>
);
}

View file

@ -1,5 +1,5 @@
import { useSyncExternalStore } from "react";
import { Loader2 } from "lucide-react";
import { Spinner } from "@nous-research/ui";
import {
getPluginComponent,
getPluginLoadError,
@ -51,7 +51,7 @@ export function PluginPage({ name }: { name: string }) {
"font-mondwest text-sm tracking-[0.1em] text-midground/60",
)}
>
<Loader2 className="h-4 w-4 shrink-0 animate-spin" aria-hidden />
<Spinner className="shrink-0" />
<span>{t.common.loading}</span>
</div>
);