feat(views): refine navigation progress bar with brand color and glow (MUL-2269) (#2681)

* feat(views): refine navigation progress bar with brand color and glow (MUL-2269)

The previous 1px bg-primary bar read as near-black on light theme and
snapped on/off in a single frame, which felt abrupt despite being a small
visual element. Switch to a 2px brand-colored sweep with right-edge glow,
slower 1.4s cubic-bezier easing, and a 200ms fade-out so completion
doesn't pop.

- Container: h-px → h-0.5 (2px); always mounted with opacity-driven fade
- Bar: bg-primary → bg-brand + two-layer box-shadow glow via color-mix
- Keyframe: 1.1s ease-in-out → 1.4s cubic-bezier(0.4, 0, 0.2, 1)

Zero new design tokens (reuses existing --brand) and zero tailwind config
changes. Desktop unaffected — same component, same prefetch=no-op path.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>

* fix(views): unmount nav progress sweep when hidden (MUL-2269)

Hiding the bar with opacity-0 left the inner element's `infinite` keyframe
animation running on every dashboard page, defeating the perceived-perf goal.
Mount the sweep only while navigating, plus the 200ms fade tail (unmount on
opacity transitionend), so nothing animates while hidden.

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
Naiyuan Qing
2026-05-15 17:39:28 +08:00
committed by GitHub
parent 2980ead4c7
commit 1cb926d52d
2 changed files with 36 additions and 9 deletions

View File

@@ -114,17 +114,18 @@
animation: chat-text-shimmer 2.5s linear infinite;
}
/* Navigation progress bar: 1px brand-tinted indeterminate sweep that shows
* across the top of the dashboard while a transition-wrapped push/replace is
* committing. Driven by useIsNavigating(); independent of the actual network,
* so it disappears the moment React commits the new route. */
/* Navigation progress bar: 2px brand-colored indeterminate sweep with a
* right-edge glow that shows across the top of the dashboard while a
* transition-wrapped push/replace is committing. Driven by useIsNavigating();
* independent of the actual network, so it disappears the moment React commits
* the new route. */
@keyframes nav-progress-sweep {
0% { transform: translateX(-100%); }
100% { transform: translateX(100%); }
}
.animate-nav-progress-sweep {
animation: nav-progress-sweep 1.1s ease-in-out infinite;
animation: nav-progress-sweep 1.4s cubic-bezier(0.4, 0, 0.2, 1) infinite;
}
/* Border beam: a brand-tinted highlight sweeps continuously around the

View File

@@ -1,19 +1,45 @@
"use client";
import { useEffect, useState } from "react";
import { useIsNavigating } from "../navigation";
// 1px top-of-content progress bar shown while a transition-wrapped
// 2px top-of-content progress bar shown while a transition-wrapped
// push/replace is mid-flight. Indeterminate by design — we don't know
// when the next route will commit, just that it's coming.
//
// The container stays mounted so it can fade out over 200ms instead of
// vanishing in one frame. The inner sweep is mounted only while navigating
// (plus the fade-out tail); leaving its `infinite` keyframe animation
// running while hidden would burn paints on every dashboard view.
export function NavigationProgress() {
const isNavigating = useIsNavigating();
if (!isNavigating) return null;
const [renderSweep, setRenderSweep] = useState(false);
useEffect(() => {
if (isNavigating) setRenderSweep(true);
}, [isNavigating]);
return (
<div
aria-hidden
className="pointer-events-none absolute inset-x-0 top-0 z-50 h-px overflow-hidden"
data-visible={isNavigating ? "true" : "false"}
onTransitionEnd={(event) => {
if (event.propertyName === "opacity" && !isNavigating) {
setRenderSweep(false);
}
}}
className="pointer-events-none absolute inset-x-0 top-0 z-50 h-0.5 overflow-hidden opacity-0 transition-opacity duration-200 data-[visible=true]:opacity-100"
>
<div className="h-full w-1/3 animate-nav-progress-sweep bg-primary" />
{renderSweep && (
<div
className="h-full w-1/3 animate-nav-progress-sweep bg-brand"
style={{
boxShadow:
"0 0 8px color-mix(in oklab, var(--brand) 60%, transparent), 0 0 2px color-mix(in oklab, var(--brand) 80%, transparent)",
}}
/>
)}
</div>
);
}