From 5328245b63aa40f993dd91273fc996088041a343 Mon Sep 17 00:00:00 2001 From: Bohdan Triapitsyn Date: Thu, 30 Jul 2026 18:46:53 +0300 Subject: [PATCH] fix(ui): remove blink and shift in desktop sidebar sticky headers - Drive the sticky-fade identity overlay's visibility synchronously from the scroller's top-scroll attribute (CSS :has), and seed its leading project synchronously, so it appears in the same frame the mask hides the real header instead of a React commit later (removes the one-frame blink) - Match the overlay's vertical box to the real header rows (py-1 instead of a centered h-7) so the identity does not shift a few pixels on the sticky handoff --- .../session/sidebar/SidebarProjectsList.tsx | 32 +++++++++++-------- packages/ui/src/index.css | 15 +++++++++ 2 files changed, 34 insertions(+), 13 deletions(-) diff --git a/packages/ui/src/components/session/sidebar/SidebarProjectsList.tsx b/packages/ui/src/components/session/sidebar/SidebarProjectsList.tsx index 4da3a078..c0622515 100644 --- a/packages/ui/src/components/session/sidebar/SidebarProjectsList.tsx +++ b/packages/ui/src/components/session/sidebar/SidebarProjectsList.tsx @@ -98,7 +98,6 @@ type Props = { function SidebarProjectsListComponent(props: Props): React.ReactNode { streamPerfCount('ui.sidebar_projects_list.render'); const { t } = useI18n(); - const [hasTopScroll, setHasTopScroll] = React.useState(false); const enableStickyFade = props.isDesktopShellRuntime && props.stickyZoneHeaders; const projectSensors = useSensors( useSensor(PointerSensor, { activationConstraint: { distance: 8 } }), @@ -152,7 +151,6 @@ function SidebarProjectsListComponent(props: Props): React.ReactNode { '--scroll-shadow-top-clear-size', `${Math.min(Math.max(topFadeSize - 8, 0), TOP_FADE_CLEAR_MAX_SIZE)}px`, ); - setHasTopScroll((prev) => (prev === hasTopScroll ? prev : hasTopScroll)); }, []); const blockObscuredInteraction = React.useCallback(( event: React.MouseEvent | React.PointerEvent, @@ -175,7 +173,15 @@ function SidebarProjectsListComponent(props: Props): React.ReactNode { stuckProject = section.project; } } - const stickyProjectLabel = stuckProject ? getProjectLabel(stuckProject, props.homeDirectory) : null; + // The IntersectionObserver reports the stuck header asynchronously, a frame or + // two after the (synchronous) mask has already hidden the real header — which + // otherwise leaves a one-frame gap where the title blinks out with no crisp + // replacement. Seed the overlay with the topmost rendered project so it is + // ready in the same frame; the observer then corrects it. When shared sessions + // lead the list, the Recent fallback below owns the top instead of a project. + const leadingProject = + stuckProject ?? (props.hasSharedSessions ? null : props.sectionsForRender[0]?.project ?? null); + const leadingProjectLabel = leadingProject ? getProjectLabel(leadingProject, props.homeDirectory) : null; if (props.sharedSessionsOnly) { return ( @@ -201,7 +207,7 @@ function SidebarProjectsListComponent(props: Props): React.ReactNode { // like they insert upward. With anchoring off, scrollTop stays put and new // rows appear below naturally.
)} - {enableStickyFade && hasTopScroll && (stuckProject || props.hasSharedSessions) ? ( + {enableStickyFade && (leadingProject || props.hasSharedSessions) ? (