From 1cb926d52d2e19f1cf62c988817014e2473fd7ea Mon Sep 17 00:00:00 2001 From: Naiyuan Qing <145280634+NevilleQingNY@users.noreply.github.com> Date: Fri, 15 May 2026 17:39:28 +0800 Subject: [PATCH] feat(views): refine navigation progress bar with brand color and glow (MUL-2269) (#2681) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 Co-authored-by: multica-agent * 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 --------- Co-authored-by: Claude Opus 4.7 Co-authored-by: multica-agent --- packages/ui/styles/base.css | 11 +++--- packages/views/layout/navigation-progress.tsx | 34 ++++++++++++++++--- 2 files changed, 36 insertions(+), 9 deletions(-) diff --git a/packages/ui/styles/base.css b/packages/ui/styles/base.css index b83ec98b9..29f103b92 100644 --- a/packages/ui/styles/base.css +++ b/packages/ui/styles/base.css @@ -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 diff --git a/packages/views/layout/navigation-progress.tsx b/packages/views/layout/navigation-progress.tsx index 0dedbf5e8..e24418619 100644 --- a/packages/views/layout/navigation-progress.tsx +++ b/packages/views/layout/navigation-progress.tsx @@ -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 (
{ + 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" > -
+ {renderSweep && ( +
+ )}
); }