fix(sidebar): keep sticky activity label in sync
Track Chats and Recent section sentinels so the desktop sticky overlay changes identity only when the corresponding section reaches the top of the sidebar.
This commit is contained in:
@@ -29,7 +29,7 @@
|
||||
- `SidebarHeader.tsx`: Top header UI for add-project, session search, selection mode, project sort, and the display menu (recent toggle, collapse/expand all).
|
||||
- A successful add/create/clone from the project-directory dialog transitions to a new-session draft targeted at that project, matching the project's `+` action; changing project metadata alone must not leave the visible session or draft on a different directory.
|
||||
- `SidebarNav.tsx`: Text navigation rows above the tree (New session, Scheduled, Multi-run, Archive); hidden in VS Code.
|
||||
- `SidebarActivitySections.tsx`: Global top section renderer for project-only `recent` sessions followed by OpenChamber-managed `chats`, styled as zone headers.
|
||||
- `SidebarActivitySections.tsx`: Global top section renderer for OpenChamber-managed `chats` followed by optional project-only `recent` sessions, styled as zone headers. The desktop sticky identity overlay follows the activity header whose sentinel has crossed the scroller edge, so a small scroll cannot relabel Chats as Recent.
|
||||
- `SidebarFooter.tsx`: Static footer with icon-only settings, shortcuts, and about actions.
|
||||
- `SidebarProjectsList.tsx`: Main scrollable renderer for project zones and their flat/archived groups plus empty/search states; owns project drag-to-reorder.
|
||||
- `SessionGroupSection.tsx`: Renders one flat (or archived) group: sessions first, then flat folder entries with path labels, show-more batching, and explicit loading/error/retry state for empty groups. Archived buckets (VS Code) virtualize past 50 rows.
|
||||
|
||||
@@ -183,6 +183,11 @@ export function SidebarActivitySections(props: Props): React.ReactNode {
|
||||
|
||||
return (
|
||||
<div key={section.key} className="relative space-y-1">
|
||||
<div
|
||||
className="absolute h-px w-px pointer-events-none"
|
||||
data-sidebar-activity-sentinel={section.key}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<div className={cn(
|
||||
'relative group/chats',
|
||||
'-ml-2.5 -mr-2',
|
||||
|
||||
@@ -37,6 +37,13 @@ type ProjectSection = {
|
||||
const TOP_FADE_MAX_SIZE = 48;
|
||||
const TOP_FADE_MIN_SIZE = 32;
|
||||
const TOP_FADE_CLEAR_MAX_SIZE = 24;
|
||||
type ActivitySectionKey = 'chats' | 'active-now';
|
||||
|
||||
const readActivitySectionKey = (element: Element): ActivitySectionKey | null => {
|
||||
const key = element.getAttribute('data-sidebar-activity-sentinel');
|
||||
if (key === 'chats' || key === 'active-now') return key;
|
||||
return null;
|
||||
};
|
||||
|
||||
const getProjectLabel = (project: ProjectSection['project'], homeDirectory: string | null): string => (
|
||||
formatProjectLabel(
|
||||
@@ -135,6 +142,7 @@ function SidebarProjectsListComponent(props: Props): React.ReactNode {
|
||||
// can resolve the scrolling ancestor synchronously (no getComputedStyle
|
||||
// walk) and skip the cost of a style recalc on every render.
|
||||
const scrollContainerRef = React.useRef<HTMLElement | null>(null);
|
||||
const [leadingActivitySection, setLeadingActivitySection] = React.useState<ActivitySectionKey>('chats');
|
||||
// Keep per-scroll measurements out of React state so the interaction guard
|
||||
// can read the current fade boundary without rerendering the sidebar.
|
||||
const topFadeSizeRef = React.useRef(0);
|
||||
@@ -167,6 +175,38 @@ function SidebarProjectsListComponent(props: Props): React.ReactNode {
|
||||
syncTopFade(scrollContainerRef.current);
|
||||
}
|
||||
}, [enableStickyFade, hasProjectScroller, syncTopFade]);
|
||||
React.useEffect(() => {
|
||||
const root = scrollContainerRef.current;
|
||||
if (!enableStickyFade || !root || !props.hasSharedSessions) return;
|
||||
|
||||
const sentinels = Array.from(root.querySelectorAll<HTMLElement>('[data-sidebar-activity-sentinel]'));
|
||||
if (sentinels.length === 0) return;
|
||||
const stuckSections = new Set<ActivitySectionKey>();
|
||||
const syncLeadingSection = (): void => {
|
||||
let nextSection = sentinels[0] ? readActivitySectionKey(sentinels[0]) : null;
|
||||
for (const sentinel of sentinels) {
|
||||
const key = readActivitySectionKey(sentinel);
|
||||
if (key && stuckSections.has(key)) nextSection = key;
|
||||
}
|
||||
if (nextSection) setLeadingActivitySection((current) => current === nextSection ? current : nextSection);
|
||||
};
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
const rootTop = root.getBoundingClientRect().top;
|
||||
for (const entry of entries) {
|
||||
const key = readActivitySectionKey(entry.target);
|
||||
if (!key) continue;
|
||||
if (!entry.isIntersecting && entry.boundingClientRect.top < (entry.rootBounds?.top ?? rootTop)) {
|
||||
stuckSections.add(key);
|
||||
} else {
|
||||
stuckSections.delete(key);
|
||||
}
|
||||
}
|
||||
syncLeadingSection();
|
||||
}, { root, threshold: 0 });
|
||||
sentinels.forEach((sentinel) => observer.observe(sentinel));
|
||||
syncLeadingSection();
|
||||
return () => observer.disconnect();
|
||||
}, [enableStickyFade, props.hasSharedSessions, props.topContent]);
|
||||
let stuckProject: ProjectSection['project'] | null = null;
|
||||
for (const section of props.projectSections) {
|
||||
if (props.stuckProjectHeaders.has(section.project.id)) {
|
||||
@@ -393,9 +433,11 @@ function SidebarProjectsListComponent(props: Props): React.ReactNode {
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<Icon name="history" className="h-3.5 w-3.5 flex-shrink-0 text-muted-foreground/80" />
|
||||
<Icon name={leadingActivitySection === 'chats' ? 'chat-4' : 'history'} className="h-3.5 w-3.5 flex-shrink-0 text-muted-foreground/80" />
|
||||
<span className="truncate text-[14px] font-semibold lowercase text-foreground">
|
||||
{t('sessions.sidebar.activity.recentTitle')}
|
||||
{t(leadingActivitySection === 'chats'
|
||||
? 'sessions.sidebar.activity.chatsTitle'
|
||||
: 'sessions.sidebar.activity.recentTitle')}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user