diff --git a/src/renderer/src/assets/main.css b/src/renderer/src/assets/main.css index 304fa28..d0b3faf 100644 --- a/src/renderer/src/assets/main.css +++ b/src/renderer/src/assets/main.css @@ -574,14 +574,43 @@ html, body, #root { @keyframes toast-enter { from { opacity: 0; transform: translateX(12px); } to { opacity: 1; transform: translateX(0); } } /* ─── Map loading bar ───────────────────────────────────────────────── */ -.map-loading-overlay { position: absolute; top: 0; left: 0; right: 0; z-index: 500; pointer-events: none; } -.map-loading-bar { - height: 2px; - background: linear-gradient(90deg, transparent 0%, #6366f1 40%, #a78bfa 60%, transparent 100%); - background-size: 200% 100%; - animation: loading-sweep 1.2s ease-in-out infinite; +.map-loading-overlay { + position: absolute; top: 0; left: 0; right: 0; z-index: 500; + pointer-events: none; } -@keyframes loading-sweep { 0% { background-position: -100% 0; } 100% { background-position: 200% 0; } } +.map-loading-track { + height: 3px; + background: rgba(255,255,255,0.06); + overflow: hidden; +} +.map-loading-fill { + height: 100%; + background: linear-gradient(90deg, #3b82f6, #6366f1, #a78bfa); + transition: width 0.12s ease-out; + box-shadow: 0 0 10px rgba(99,102,241,0.7); +} +.map-loading-fill--done { + transition: width 0.18s ease-out; + background: linear-gradient(90deg, #4ade80, #34d399); + box-shadow: 0 0 10px rgba(74,222,128,0.7); +} +.map-loading-badge { + position: absolute; + top: 7px; + transform: translateX(-50%); + background: rgba(10,13,22,0.88); + backdrop-filter: blur(8px); + border: 1px solid rgba(255,255,255,0.1); + border-radius: 100px; + color: var(--text-muted); + font-size: 10px; + font-weight: 700; + padding: 2px 7px; + letter-spacing: 0.04em; + transition: left 0.12s ease-out, color 0.2s; + white-space: nowrap; +} +.map-loading-badge--done { color: #4ade80 !important; border-color: rgba(74,222,128,0.25); } /* ─── Map context menu ──────────────────────────────────────────────── */ .map-context-menu { diff --git a/src/renderer/src/components/MapOverlay.tsx b/src/renderer/src/components/MapOverlay.tsx index a8e38a3..ad72cec 100644 --- a/src/renderer/src/components/MapOverlay.tsx +++ b/src/renderer/src/components/MapOverlay.tsx @@ -1,11 +1,67 @@ +import { useEffect, useRef, useState } from 'react' import { useAppStore } from '../store/useAppStore' +// Estimate how long a calculation will take based on max range (seconds) +function estimateDuration(maxRangeSec: number): number { + // Base 1.5s + ~2s per hour of isochrone range + return 1500 + (maxRangeSec / 3600) * 2200 +} + export function MapOverlay(): React.JSX.Element | null { const loading = useAppStore((s) => s.loading) - if (!loading) return null + const timeRanges = useAppStore((s) => s.timeRanges) + const [progress, setProgress] = useState(0) + const [visible, setVisible] = useState(false) + const intervalRef = useRef | null>(null) + const startRef = useRef(0) + const estimatedRef = useRef(3000) + + useEffect(() => { + if (loading) { + const maxRange = Math.max(...timeRanges) + estimatedRef.current = estimateDuration(maxRange) + startRef.current = Date.now() + setProgress(0) + setVisible(true) + + intervalRef.current = setInterval(() => { + const elapsed = Date.now() - startRef.current + const ratio = elapsed / estimatedRef.current + // Ease to 88%: asymptotic curve that never quite reaches it + const pct = 88 * (1 - Math.exp(-3 * ratio)) + setProgress(Math.min(pct, 88)) + }, 80) + } else { + if (intervalRef.current) clearInterval(intervalRef.current) + // Snap to 100% then fade out + setProgress(100) + const t = setTimeout(() => { + setVisible(false) + setProgress(0) + }, 500) + return () => clearTimeout(t) + } + return () => { if (intervalRef.current) clearInterval(intervalRef.current) } + }, [loading]) // eslint-disable-line react-hooks/exhaustive-deps + + if (!visible) return null + + const pct = Math.round(progress) + return (
-
+
+
= 100 ? ' map-loading-fill--done' : ''}`} + style={{ width: `${progress}%` }} + /> +
+
= 100 ? ' map-loading-badge--done' : ''}`} + style={{ left: `clamp(32px, ${progress}%, calc(100% - 40px))` }} + > + {pct < 100 ? `${pct}%` : '✓'} +
) }