diff options
| author | yyamashita <yyamashita@hetzner.yyamashita.com> | 2026-07-14 23:09:27 +0900 |
|---|---|---|
| committer | yyamashita <yyamashita@hetzner.yyamashita.com> | 2026-07-14 23:09:27 +0900 |
| commit | 7370e6c128b819bf51260783c6ffbe47be8e0b47 (patch) | |
| tree | 4b48861b5761e475c06159c91366707142f27d30 | |
| parent | c0f61ba314bb2ed0503ec1a00cda0e8e15f89e2d (diff) | |
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
| -rw-r--r-- | app/app.css | 5 | ||||
| -rw-r--r-- | app/root.tsx | 42 |
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) { |
