diff --git a/src/app/App.tsx b/src/app/App.tsx
index cbbc4fc..4c2ce1b 100644
--- a/src/app/App.tsx
+++ b/src/app/App.tsx
@@ -13,6 +13,7 @@ import MediaManager from './components/MediaManager';
import logoSvg from '../imports/logo.svg';
import { useSettings } from './settings';
import { WsProvider } from './ws';
+import { LazyLoader } from './components/ui/lazy-loader';
// Three.js lives only in RealityOverridePage — keep lazy so it doesn't load on startup.
// CodeMirror/syntax-highlighter/ReactMarkdown live in MediaViewerEditor — lazy-loaded
@@ -47,13 +48,6 @@ type AppId =
| 'reality-override'
| 'reality-override-admin';
-function PageLoader() {
- return (
-
-
-
- );
-}
export default function App() {
const [currentPage, setCurrentPage] = useState('status');
@@ -314,7 +308,7 @@ function AppPage({ title, onBack }: { title: string; onBack: () => void }) {
- }>
+ }>
{pages[currentPage]}
diff --git a/src/app/components/ui/lazy-loader.tsx b/src/app/components/ui/lazy-loader.tsx
new file mode 100644
index 0000000..fac3eb4
--- /dev/null
+++ b/src/app/components/ui/lazy-loader.tsx
@@ -0,0 +1,38 @@
+import { useEffect, useState } from 'react';
+import { Loader2 } from 'lucide-react';
+
+export function LazyLoader() {
+ const [pct, setPct] = useState(0);
+
+ useEffect(() => {
+ const steps: [number, number][] = [
+ [80, 30],
+ [400, 60],
+ [1200, 80],
+ [2500, 92],
+ ];
+ const timers = steps.map(([delay, val]) => setTimeout(() => setPct(val), delay));
+ return () => timers.forEach(clearTimeout);
+ }, []);
+
+ const duration =
+ pct <= 30 ? '150ms' :
+ pct <= 60 ? '500ms' :
+ pct <= 80 ? '1000ms' : '1500ms';
+
+ return (
+
+ );
+}