From fea94632b7f55566aea8525a21e1b69773f29a71 Mon Sep 17 00:00:00 2001 From: armorbreak001 Date: Tue, 14 Apr 2026 21:01:57 +0800 Subject: [PATCH] feat(frontend): add animated loading spinners and overlay components - Add Spinner component with size (sm/md/lg/xl) and color props - Uses Lucide Loader2 SVG with Tailwind animate-spin for crisp Retina rendering - Add LoadingOverlay for full-screen backdrop with spinner + optional label - Add PageSpinner for inline/page-level loading states - All components exported from UI index --- frontend/src/components/ui/Spinner.tsx | 112 +++++++++++++++++++++++++ frontend/src/components/ui/index.ts | 1 + 2 files changed, 113 insertions(+) create mode 100644 frontend/src/components/ui/Spinner.tsx diff --git a/frontend/src/components/ui/Spinner.tsx b/frontend/src/components/ui/Spinner.tsx new file mode 100644 index 0000000..6ca2547 --- /dev/null +++ b/frontend/src/components/ui/Spinner.tsx @@ -0,0 +1,112 @@ +'use client' + +import React from 'react' +import { cn } from '@/lib/utils' +import { Loader2 } from 'lucide-react' + +/* ---------- types ---------- */ + +type SpinnerSize = 'sm' | 'md' | 'lg' | 'xl' + +interface SpinnerProps { + size?: SpinnerSize + className?: string + /** Tailwind color class — default: text-violet-400 */ + colorClass?: string +} + +/* ---------- size map ---------- */ + +const SIZE_MAP: Record = { + sm: 'h-4 w-4', + md: 'h-6 w-6', + lg: 'h-8 w-8', + xl: 'h-12 w-12', +} + +/* ---------- component ---------- */ + +export function Spinner({ + size = 'md', + className, + colorClass = 'text-violet-400', +}: SpinnerProps) { + return ( + + ) +} + +/* ---------- LoadingOverlay ---------- */ + +interface LoadingOverlayProps { + isOpen: boolean + /** Optional label text below spinner */ + label?: string + /** Custom spinner size */ + spinnerSize?: SpinnerSize + className?: string +} + +export function LoadingOverlay({ + isOpen, + label, + spinnerSize = 'lg', + className, +}: LoadingOverlayProps) { + if (!isOpen) return null + + return ( +
+ + {label && ( +

+ {label} +

+ )} +
+ ) +} + +/* ---------- PageSkeleton (full-page placeholder) ---------- */ + +interface PageSpinnerProps { + /** Full-screen center */ + fullscreen?: boolean + label?: string + spinnerSize?: SpinnerSize +} + +export function PageSpinner({ + fullscreen = false, + label, + spinnerSize = 'lg', +}: PageSpinnerProps) { + return ( +
+ + {label && ( +

{label}

+ )} +
+ ) +} + +export default Spinner diff --git a/frontend/src/components/ui/index.ts b/frontend/src/components/ui/index.ts index 531166f..c3d40bc 100644 --- a/frontend/src/components/ui/index.ts +++ b/frontend/src/components/ui/index.ts @@ -14,3 +14,4 @@ export { DropdownMenuItem, } from "./DropdownMenu"; export { Pagination } from "./Pagination"; +export { Spinner, LoadingOverlay, PageSpinner } from "./Spinner";