summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/app.css5
-rw-r--r--app/root.tsx42
2 files changed, 46 insertions, 1 deletions
diff --git a/app/app.css b/app/app.css
index 99345d8..24e2c92 100644
--- a/app/app.css
+++ b/app/app.css
@@ -5,6 +5,11 @@
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
}
+@keyframes nav-progress {
+ from { transform: scaleX(0.05); }
+ to { transform: scaleX(0.85); }
+}
+
html,
body {
@apply bg-white dark:bg-gray-950;
diff --git a/app/root.tsx b/app/root.tsx
index 37827e4..50fca78 100644
--- a/app/root.tsx
+++ b/app/root.tsx
@@ -1,3 +1,4 @@
+import { useEffect, useState } from "react";
import {
isRouteErrorResponse,
Links,
@@ -5,6 +6,7 @@ import {
Outlet,
Scripts,
ScrollRestoration,
+ useNavigation,
} from "react-router";
import type { Route } from "./+types/root";
@@ -42,8 +44,46 @@ export function Layout({ children }: { children: React.ReactNode }) {
);
}
+function NavigationProgress() {
+ const navigation = useNavigation();
+ const [phase, setPhase] = useState<"idle" | "loading" | "done">("idle");
+
+ useEffect(() => {
+ if (navigation.state !== "idle") {
+ setPhase("loading");
+ } else {
+ setPhase((prev) => (prev === "loading" ? "done" : prev));
+ }
+ }, [navigation.state]);
+
+ useEffect(() => {
+ if (phase !== "done") return;
+ const t = setTimeout(() => setPhase("idle"), 500);
+ return () => clearTimeout(t);
+ }, [phase]);
+
+ if (phase === "idle") return null;
+
+ return (
+ <div
+ aria-hidden
+ className="fixed top-0 left-0 right-0 z-50 h-0.5 bg-indigo-400 origin-left"
+ style={
+ phase === "loading"
+ ? { animation: "nav-progress 10s cubic-bezier(0.1, 0.9, 0.2, 1) forwards" }
+ : { transform: "scaleX(1)", opacity: 0, transition: "opacity 400ms" }
+ }
+ />
+ );
+}
+
export default function App() {
- return <Outlet />;
+ return (
+ <>
+ <NavigationProgress />
+ <Outlet />
+ </>
+ );
}
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {