From 8130381f6c64c740ba5f6119af6abe75d7a957d9 Mon Sep 17 00:00:00 2001 From: ChangeHow <23733347+ChangeHow@users.noreply.github.com> Date: Thu, 30 Jul 2026 22:51:23 +0800 Subject: [PATCH] fix(ui): smooth desktop sidebar sticky transitions (#2528) * fix(ui): blend vibrant sidebar transitions * fix(ui): fade sidebar content below header * fix(ui): smooth vibrant sidebar sticky headers * fix(ui): scope sticky fade to desktop * fix(ui): respect sticky header preference --- .../src/components/session/SessionSidebar.tsx | 4 +- .../session/sidebar/DOCUMENTATION.md | 2 +- .../sidebar/SidebarActivitySections.tsx | 16 +- .../session/sidebar/SidebarProjectsList.tsx | 109 +++++++++++-- .../sidebar/hooks/useStickyProjectHeaders.ts | 77 +++------ .../session/sidebar/sortableItems.tsx | 153 ++++++++++-------- packages/ui/src/index.css | 12 +- packages/ui/src/styles/design-system.css | 10 -- 8 files changed, 221 insertions(+), 162 deletions(-) diff --git a/packages/ui/src/components/session/SessionSidebar.tsx b/packages/ui/src/components/session/SessionSidebar.tsx index c7a78159..a504dde9 100644 --- a/packages/ui/src/components/session/SessionSidebar.tsx +++ b/packages/ui/src/components/session/SessionSidebar.tsx @@ -1121,6 +1121,7 @@ const SessionSidebarComponent: React.FC = ({ const showRecentSection = useSessionDisplayStore((state) => state.showRecentSection); const showArchivedSessions = useSessionDisplayStore((state) => state.showArchivedSessions); const projectSortOrder = useSessionDisplayStore((state) => state.projectSortOrder); + const stickyZoneHeaders = useSessionDisplayStore((state) => state.stickyZoneHeaders); const manualProjectOrder = useProjectsStore((state) => state.manualProjectOrder); const projectExpandedParentsRef = React.useRef>(new Set()); const recentExpandedParentsRef = React.useRef>(new Set()); @@ -1516,7 +1517,7 @@ const SessionSidebarComponent: React.FC = ({ const headerActionButtonClass = mobileVariant ? mobileHeaderActionButtonClass : desktopHeaderActionButtonClass; const headerActionIconClass = 'h-4.5 w-4.5'; const stuckProjectHeaders = useStickyProjectHeaders({ - enabled: isVisible, + enabled: isVisible && stickyZoneHeaders, isDesktopShellRuntime, projectSections, projectHeaderSentinelRefs, @@ -1861,6 +1862,7 @@ const SessionSidebarComponent: React.FC = ({ hideDirectoryControls={hideDirectoryControls} projectRepoStatus={projectRepoStatus} isDesktopShellRuntime={isDesktopShellRuntime} + stickyZoneHeaders={stickyZoneHeaders} stuckProjectHeaders={stuckProjectHeaders} mobileVariant={mobileVariant} alwaysShowActions={alwaysShowSidebarActions} diff --git a/packages/ui/src/components/session/sidebar/DOCUMENTATION.md b/packages/ui/src/components/session/sidebar/DOCUMENTATION.md index 0926348a..ae05ed68 100644 --- a/packages/ui/src/components/session/sidebar/DOCUMENTATION.md +++ b/packages/ui/src/components/session/sidebar/DOCUMENTATION.md @@ -5,7 +5,7 @@ - `SessionSidebar.tsx` now acts mainly as orchestration; core logic moved to focused hooks/components. - Layout (web/desktop): top navigation (`SidebarNav`: New session, Scheduled, Multi-run, Archive), then the `recent` zone, then one zone per project with a **flat** session list. There is no rendered worktree grouping level. - **Two grouping display modes** (`useSessionDisplayStore.sessionGroupingMode`, toggled in the view dropdown): `'by-worktree'` (default) renders the worktree-grouped `sectionsForRender` with slim PR-aware branch sub-headers inside each project zone; `'flat'` renders `flatSectionsForRender` — one merged non-archived group per project (`id: 'flat'`, `folderScopes` listing every contributing scope) with per-row branch markers. Both derive from the same `projectSections` data layer, which alone feeds bootstrap demand planning and PR polling. -- Project headers are sticky background "zone" bands (`SortableProjectItem`); the `recent` section header uses the same band styling. Collapsed projects show an aggregated busy/unseen indicator (`ProjectAggregateStatusIndicator`), derived from the live status index and notification store scoped to the project's directories. +- When sticky zone headers are enabled, project headers are sticky "zone" bands (`SortableProjectItem`); on a vibrant desktop the scrolling content fades behind an unmasked, non-interactive copy of the stuck icon/title without painting a background. The transparent fade zone blocks interaction with obscured rows. The `recent` section uses the same overlay while it is the leading sticky header. Collapsed projects show an aggregated busy/unseen indicator (`ProjectAggregateStatusIndicator`), derived from the live status index and notification store scoped to the project's directories. - Session rows have a single layout (former `minimal`); the `default`/`minimal` display mode was removed (`session-display-mode` store v4 migration drops the key). Rows show an inline branch label (from `node.worktree` or recent's `secondaryMeta`) when the session lives outside the project root, and bold titles while unread. - Folders render **flat** after the loose sessions: nested folders keep `parentId` in the data model but display at one level with a "Parent / Child" path label (`SessionFolderItem.displayName`); collapsing a folder hides its whole subtree. Folder actions resolve their owning scope per folder entry (folders from multiple worktree scopes can coexist under one project). - Archived sessions are not shown in the web/desktop sidebar; the Archive page (`ArchiveView`, `useUIStore.isArchivePageOpen`) replaces the old toggle. VS Code keeps inline archived buckets behind `showArchivedSessions` (compact webview has no page surfaces). Unarchive is not possible through the upstream OpenCode HTTP API (`session.update` can only set a finite `time.archived`). diff --git a/packages/ui/src/components/session/sidebar/SidebarActivitySections.tsx b/packages/ui/src/components/session/sidebar/SidebarActivitySections.tsx index 8d018ced..6387552c 100644 --- a/packages/ui/src/components/session/sidebar/SidebarActivitySections.tsx +++ b/packages/ui/src/components/session/sidebar/SidebarActivitySections.tsx @@ -10,7 +10,6 @@ import { resolveMenuOpenSessionId, } from './sessionNodeItemUtils'; import type { SessionNodeRenderExtras } from './sessionNodeItemUtils'; -import { useStickyHeader } from './hooks/useStickyProjectHeaders'; type ActivityItem = { node: SessionNode; @@ -62,17 +61,12 @@ export function SidebarActivitySections(props: Props): React.ReactNode { variant = 'section', initialVisibleCount = MAX_VISIBLE_RECENT_SESSIONS, batchSize = MAX_VISIBLE_RECENT_SESSIONS, - isDesktopShellRuntime, } = props; const { t } = useI18n(); const stickyZoneHeaders = useSessionDisplayStore((state) => state.stickyZoneHeaders); const [collapsed, setCollapsed] = React.useState>(new Set()); const [visibleCountBySection, setVisibleCountBySection] = React.useState>(new Map()); const flatVariant = variant === 'flat'; - const { isStuck: isRecentHeaderStuck, sentinelRef: recentHeaderSentinelRef } = useStickyHeader({ - enabled: stickyZoneHeaders && !flatVariant, - isDesktopShellRuntime, - }); const resetSectionLimit = React.useCallback((key: string) => { setVisibleCountBySection((prev) => { @@ -184,18 +178,10 @@ export function SidebarActivitySections(props: Props): React.ReactNode { return (
- {/* Zone header styled like a project header band; its solid - backing applies only after the header is actually stuck. */} -