diff --git a/apps/web/src/pages/_app.tsx b/apps/web/src/pages/_app.tsx
index 2d5e75a..3397519 100644
--- a/apps/web/src/pages/_app.tsx
+++ b/apps/web/src/pages/_app.tsx
@@ -5,6 +5,7 @@ import React, {useEffect} from 'react';
import {Toaster} from 'sonner';
import {SWRConfig} from 'swr';
import {DefaultSeo} from 'next-seo';
+import {Loader} from '@plunk/ui';
import {ActiveProjectProvider} from '../lib/contexts/ActiveProjectProvider';
import {useProjects} from '../lib/hooks/useProject';
import {useUser} from '../lib/hooks/useUser';
@@ -43,26 +44,7 @@ function AuthGuard({children}: {children: React.ReactNode}) {
// Show loading state while checking authentication (only for protected routes)
if (isLoading && !isPublicRoute) {
- return (
-
- );
+ return ;
}
// Don't render protected content if redirecting
@@ -87,26 +69,7 @@ function ProjectGuard({children}: {children: React.ReactNode}) {
// Show loading state while checking projects (only for routes that need a project)
if (isLoading && !isNoProjectRoute) {
- return (
-
- );
+ return ;
}
// Don't render protected content if redirecting
diff --git a/packages/ui/src/components/atoms/IconSpinner.tsx b/packages/ui/src/components/atoms/IconSpinner.tsx
new file mode 100644
index 0000000..0519dc9
--- /dev/null
+++ b/packages/ui/src/components/atoms/IconSpinner.tsx
@@ -0,0 +1,74 @@
+import {cn} from '../../lib';
+
+export interface IconSpinnerProps {
+ size?: 'sm' | 'md' | 'lg' | 'xl';
+ className?: string;
+}
+
+const sizeConfig = {
+ sm: {
+ container: 'w-4 h-4',
+ border: 'border-2',
+ innerInset: 'inset-0.5',
+ dot: 'w-1 h-1',
+ },
+ md: {
+ container: 'w-8 h-8',
+ border: 'border-[2.5px]',
+ innerInset: 'inset-1',
+ dot: 'w-1.5 h-1.5',
+ },
+ lg: {
+ container: 'w-12 h-12',
+ border: 'border-[3px]',
+ innerInset: 'inset-1.5',
+ dot: 'w-2 h-2',
+ },
+ xl: {
+ container: 'w-16 h-16',
+ border: 'border-[3px]',
+ innerInset: 'inset-2',
+ dot: 'w-2.5 h-2.5',
+ },
+};
+
+/**
+ * Animated spinner icon component with dual rotating rings
+ */
+export function IconSpinner({size = 'md', className}: IconSpinnerProps) {
+ const config = sizeConfig[size];
+
+ return (
+
+ {/* Outer glow effect */}
+
+
+ {/* Base circle */}
+
+
+ {/* Primary animated ring */}
+
+
+ {/* Secondary animated ring (counter-rotating) */}
+
+
+ {/* Center dot */}
+
+
+ );
+}
diff --git a/packages/ui/src/components/atoms/Loader.tsx b/packages/ui/src/components/atoms/Loader.tsx
new file mode 100644
index 0000000..77f205b
--- /dev/null
+++ b/packages/ui/src/components/atoms/Loader.tsx
@@ -0,0 +1,54 @@
+import Image from 'next/image';
+
+export interface LoaderProps {
+ message?: string;
+ showLogo?: boolean;
+}
+
+/**
+ * Full-screen loader component with animated logo and spinner
+ */
+export function Loader({message = 'Loading...', showLogo = true}: LoaderProps) {
+ return (
+
+
+ {showLogo && (
+
+ )}
+
+ {/* Animated spinner */}
+
+ {/* Outer glow effect */}
+
+
+ {/* Base circle */}
+
+
+ {/* Primary animated ring */}
+
+
+ {/* Secondary animated ring (counter-rotating) */}
+
+
+ {/* Center dot */}
+
+
+
+ {/* Loading message */}
+ {message &&
{message}
}
+
+
+ );
+}
diff --git a/packages/ui/src/components/atoms/index.ts b/packages/ui/src/components/atoms/index.ts
index 9fb081f..b81cf71 100644
--- a/packages/ui/src/components/atoms/index.ts
+++ b/packages/ui/src/components/atoms/index.ts
@@ -9,8 +9,10 @@ export * from './Command';
export * from './Dialog';
export * from './DropdownMenu';
export * from './Form';
+export * from './IconSpinner';
export * from './Input';
export * from './Label';
+export * from './Loader';
export * from './Popover';
export * from './Progress';
export * from './RadioGroup';