Fix sidebar unread indicator placement (#1319)

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Pinky <44b8e82baa6e0e254e0208d68f335c283c94e7b78dd1fa10d5a49d3f13dd0435@sprout-oss.stage.blox.sqprod.co>
This commit is contained in:
Wes
2026-06-29 10:18:16 -06:00
committed by GitHub
parent 7adbd05b45
commit 51b2613c0e
3 changed files with 387 additions and 301 deletions

View File

@@ -1,17 +1,9 @@
// biome-ignore format: keep compact to stay within file size limit
import {
Activity,
Bot,
FolderGit2,
Inbox,
MessageCirclePlus,
Zap,
} from "lucide-react";
import { MessageCirclePlus } from "lucide-react";
import * as React from "react";
import { AnimatePresence } from "motion/react";
import { FeatureGate } from "@/shared/features";
import { SidebarDndContext } from "@/features/sidebar/ui/SidebarDnd";
import { TopbarSearch } from "@/features/search/ui/TopbarSearch";
import type { Workspace } from "@/features/workspaces/types";
import { AddWorkspaceDialog } from "@/features/workspaces/ui/AddWorkspaceDialog";
@@ -31,6 +23,7 @@ import {
RenameSectionDialog,
useLeaveChannelDialog,
} from "@/features/sidebar/ui/ChannelSectionDialogs";
import { AppSidebarPinnedHeader } from "@/features/sidebar/ui/AppSidebarPinnedHeader";
import { MoreUnreadButton } from "@/features/sidebar/ui/MoreUnreadButton";
import { SidebarSection } from "@/features/sidebar/ui/SidebarSection";
import {
@@ -66,10 +59,7 @@ import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarHeader,
SidebarMenu,
SidebarMenuBadge,
SidebarMenuButton,
SidebarMenuItem,
SidebarRail,
useSidebar,
@@ -534,201 +524,73 @@ export function AppSidebar({
className="relative flex min-h-0 flex-1 flex-col overflow-hidden"
data-testid="app-sidebar-scroll-anchor"
>
{unreadAboveCount > 0 ? (
<MoreUnreadButton
count={unreadAboveCount}
onClick={scrollToNextAbove}
position="top"
testId="sidebar-more-unread-above"
/>
) : null}
<div
className="shrink-0 px-2 pt-2.5"
data-testid="sidebar-pinned-header"
>
<TopbarSearch
channelLabels={dmChannelLabels}
channels={searchChannels}
currentPubkey={currentPubkey}
focusRequest={searchFocusRequest}
onOpenChannel={onSelectChannel}
onOpenResult={onOpenSearchResult}
onOpenUser={(user) => onOpenDm({ pubkeys: [user.pubkey] })}
onCreateAgent={onCreateAgent}
onCreateChannel={handleOpenCreateChannel}
suggestionChannels={channels}
/>
<SidebarHeader
className="cursor-default select-none px-0 pb-0 pt-2.5"
data-tauri-drag-region
>
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton
isActive={selectedView === "home"}
onClick={onSelectHome}
tooltip="Inbox"
type="button"
>
<Inbox className="h-4 w-4" />
<span>Inbox</span>
</SidebarMenuButton>
{homeBadgeCount > 0 ? (
<SidebarMenuBadge
className="right-2 rounded-full bg-primary/15 px-1.5 text-2xs text-primary peer-data-[active=true]/menu-button:bg-sidebar-active-foreground/20 peer-data-[active=true]/menu-button:text-sidebar-active-foreground"
data-testid="sidebar-home-count"
>
{Math.min(homeBadgeCount, 99)}
</SidebarMenuBadge>
) : null}
</SidebarMenuItem>
<FeatureGate feature="pulse">
<SidebarMenuItem>
<SidebarMenuButton
data-testid="open-pulse-view"
isActive={selectedView === "pulse"}
onClick={onSelectPulse}
tooltip="Pulse"
type="button"
>
<Activity className="h-4 w-4" />
<span>Pulse</span>
</SidebarMenuButton>
</SidebarMenuItem>
</FeatureGate>
<FeatureGate feature="projects">
<SidebarMenuItem>
<SidebarMenuButton
data-testid="open-projects-view"
isActive={selectedView === "projects"}
onClick={onSelectProjects}
tooltip="Projects"
type="button"
>
<FolderGit2 className="h-4 w-4" />
<span>Projects</span>
</SidebarMenuButton>
</SidebarMenuItem>
</FeatureGate>
<SidebarMenuItem>
<SidebarMenuButton
data-testid="open-agents-view"
isActive={selectedView === "agents"}
onClick={onSelectAgents}
tooltip="Agents"
type="button"
>
<Bot className="h-4 w-4" />
<span>Agents</span>
</SidebarMenuButton>
</SidebarMenuItem>
<FeatureGate feature="workflows">
<SidebarMenuItem>
<SidebarMenuButton
data-testid="open-workflows-view"
isActive={selectedView === "workflows"}
onClick={onSelectWorkflows}
tooltip="Workflows"
type="button"
>
<Zap className="h-4 w-4" />
<span>Workflows</span>
</SidebarMenuButton>
</SidebarMenuItem>
</FeatureGate>
</SidebarMenu>
</SidebarHeader>
</div>
<AppSidebarPinnedHeader
channelLabels={dmChannelLabels}
currentPubkey={currentPubkey}
homeBadgeCount={homeBadgeCount}
onCreateAgent={onCreateAgent}
onCreateChannel={handleOpenCreateChannel}
onOpenDm={onOpenDm}
onOpenSearchResult={onOpenSearchResult}
onSelectAgents={onSelectAgents}
onSelectChannel={onSelectChannel}
onSelectHome={onSelectHome}
onSelectProjects={onSelectProjects}
onSelectPulse={onSelectPulse}
onSelectWorkflows={onSelectWorkflows}
searchChannels={searchChannels}
searchFocusRequest={searchFocusRequest}
selectedView={selectedView}
suggestionChannels={channels}
/>
<SidebarContent
className="buzz-sidebar-scrollbar overscroll-none pt-4"
ref={scrollRef}
<div
className="relative flex min-h-0 flex-1 flex-col"
data-testid="sidebar-channel-content"
>
{isLoading ? (
<SidebarLoadingContent shape={sidebarLoadingShape} />
{unreadAboveCount > 0 ? (
<MoreUnreadButton
count={unreadAboveCount}
onClick={scrollToNextAbove}
position="top"
testId="sidebar-more-unread-above"
/>
) : null}
{!isLoading ? (
<>
{starredChannels.length > 0 ? (
<ChannelGroupSection
createAriaLabel="Starred channels"
hasUnread={starredChannels.some((c) =>
unreadChannelIds.has(c.id),
)}
isCollapsed={collapsedGroups.starred}
isActiveChannel={selectedView === "channel"}
activeWorkingByChannelId={activeWorkingByChannelId}
items={starredChannels}
listTestId="starred-list"
onMarkAllRead={() => {
for (const channel of starredChannels) {
onMarkChannelRead(channel.id, channel.lastMessageAt);
}
}}
onMarkChannelRead={onMarkChannelRead}
onMarkChannelUnread={onMarkChannelUnread}
onSelectChannel={onSelectChannel}
onToggleCollapsed={() => toggleCollapsedGroup("starred")}
selectedChannelId={selectedChannelId}
title="Starred"
unreadChannelCounts={unreadChannelCounts}
unreadChannelIds={unreadChannelIds}
mutedChannelIds={mutedChannelIds}
onMuteChannel={onMuteChannel}
onUnmuteChannel={onUnmuteChannel}
starredChannelIds={starredChannelIds}
onStarChannel={onStarChannel}
onUnstarChannel={onUnstarChannel}
onLeaveChannel={requestLeaveChannel}
/>
) : null}
<SidebarDndContext
channels={channels}
sections={channelSections}
sectionIds={sectionIds}
onAssignChannel={assignChannel}
onUnassignChannel={unassignChannel}
onReorderSections={reorderSections}
>
{channelSections.map((section, idx) => (
<CustomChannelSection
key={section.id}
section={section}
channels={sectionBuckets.bySection[section.id] ?? []}
hasUnread={
sectionBuckets.bySection[section.id]?.some((c) =>
unreadChannelIds.has(c.id),
) ?? false
}
isCollapsed={collapsedSections[section.id] ?? false}
<SidebarContent
className="buzz-sidebar-scrollbar overscroll-none pt-4"
ref={scrollRef}
>
{isLoading ? (
<SidebarLoadingContent shape={sidebarLoadingShape} />
) : null}
{!isLoading ? (
<>
{starredChannels.length > 0 ? (
<ChannelGroupSection
createAriaLabel="Starred channels"
hasUnread={starredChannels.some((c) =>
unreadChannelIds.has(c.id),
)}
isCollapsed={collapsedGroups.starred}
isActiveChannel={selectedView === "channel"}
activeWorkingByChannelId={activeWorkingByChannelId}
selectedChannelId={selectedChannelId}
unreadChannelCounts={unreadChannelCounts}
unreadChannelIds={unreadChannelIds}
sections={channelSections}
assignments={channelAssignments}
isFirst={idx === 0}
isLast={idx === channelSections.length - 1}
onToggleCollapsed={() => toggleCollapsedSection(section.id)}
onSelectChannel={onSelectChannel}
onMarkChannelRead={onMarkChannelRead}
onMarkChannelUnread={onMarkChannelUnread}
onMarkSectionRead={() => {
for (const channel of sectionBuckets.bySection[
section.id
] ?? []) {
items={starredChannels}
listTestId="starred-list"
onMarkAllRead={() => {
for (const channel of starredChannels) {
onMarkChannelRead(channel.id, channel.lastMessageAt);
}
}}
onAssignChannel={assignChannel}
onUnassignChannel={unassignChannel}
onCreateSectionForChannel={handleCreateSectionForChannel}
onRenameSection={() => setRenameSectionTarget(section)}
onDeleteSection={() => setDeleteSectionTarget(section)}
onMoveSectionUp={() => moveSectionUp(section.id)}
onMoveSectionDown={() => moveSectionDown(section.id)}
onMarkChannelRead={onMarkChannelRead}
onMarkChannelUnread={onMarkChannelUnread}
onSelectChannel={onSelectChannel}
onToggleCollapsed={() => toggleCollapsedGroup("starred")}
selectedChannelId={selectedChannelId}
title="Starred"
unreadChannelCounts={unreadChannelCounts}
unreadChannelIds={unreadChannelIds}
mutedChannelIds={mutedChannelIds}
onMuteChannel={onMuteChannel}
onUnmuteChannel={onUnmuteChannel}
@@ -737,115 +599,175 @@ export function AppSidebar({
onUnstarChannel={onUnstarChannel}
onLeaveChannel={requestLeaveChannel}
/>
))}
<ChannelGroupSection
browseAriaLabel="Browse channels"
createAriaLabel="Create a channel"
draggable
hasUnread={unreadChannelIds.size > 0}
isCollapsed={collapsedGroups.channels}
isActiveChannel={selectedView === "channel"}
activeWorkingByChannelId={activeWorkingByChannelId}
items={sectionBuckets.unassigned}
listTestId="stream-list"
onBrowseClick={onBrowseChannels}
onCreateClick={() => openCreateDialog("stream")}
onMarkAllRead={onMarkAllChannelsRead}
onMarkChannelRead={onMarkChannelRead}
onMarkChannelUnread={onMarkChannelUnread}
onSelectChannel={onSelectChannel}
onToggleCollapsed={() => toggleCollapsedGroup("channels")}
selectedChannelId={selectedChannelId}
title="Channels"
unreadChannelCounts={unreadChannelCounts}
unreadChannelIds={unreadChannelIds}
) : null}
<SidebarDndContext
channels={channels}
sections={channelSections}
assignments={channelAssignments}
sectionIds={sectionIds}
onAssignChannel={assignChannel}
onUnassignChannel={unassignChannel}
onCreateSectionForChannel={handleCreateSectionForChannel}
mutedChannelIds={mutedChannelIds}
onMuteChannel={onMuteChannel}
onUnmuteChannel={onUnmuteChannel}
starredChannelIds={starredChannelIds}
onStarChannel={onStarChannel}
onUnstarChannel={onUnstarChannel}
onLeaveChannel={requestLeaveChannel}
/>
</SidebarDndContext>
<FeatureGate feature="forum">
<ChannelGroupSection
createAriaLabel="Create a forum"
hasUnread={unreadChannelIds.size > 0}
isCollapsed={collapsedGroups.forums}
onReorderSections={reorderSections}
>
{channelSections.map((section, idx) => (
<CustomChannelSection
key={section.id}
section={section}
channels={sectionBuckets.bySection[section.id] ?? []}
hasUnread={
sectionBuckets.bySection[section.id]?.some((c) =>
unreadChannelIds.has(c.id),
) ?? false
}
isCollapsed={collapsedSections[section.id] ?? false}
isActiveChannel={selectedView === "channel"}
activeWorkingByChannelId={activeWorkingByChannelId}
selectedChannelId={selectedChannelId}
unreadChannelCounts={unreadChannelCounts}
unreadChannelIds={unreadChannelIds}
sections={channelSections}
assignments={channelAssignments}
isFirst={idx === 0}
isLast={idx === channelSections.length - 1}
onToggleCollapsed={() =>
toggleCollapsedSection(section.id)
}
onSelectChannel={onSelectChannel}
onMarkChannelRead={onMarkChannelRead}
onMarkChannelUnread={onMarkChannelUnread}
onMarkSectionRead={() => {
for (const channel of sectionBuckets.bySection[
section.id
] ?? []) {
onMarkChannelRead(channel.id, channel.lastMessageAt);
}
}}
onAssignChannel={assignChannel}
onUnassignChannel={unassignChannel}
onCreateSectionForChannel={handleCreateSectionForChannel}
onRenameSection={() => setRenameSectionTarget(section)}
onDeleteSection={() => setDeleteSectionTarget(section)}
onMoveSectionUp={() => moveSectionUp(section.id)}
onMoveSectionDown={() => moveSectionDown(section.id)}
mutedChannelIds={mutedChannelIds}
onMuteChannel={onMuteChannel}
onUnmuteChannel={onUnmuteChannel}
starredChannelIds={starredChannelIds}
onStarChannel={onStarChannel}
onUnstarChannel={onUnstarChannel}
onLeaveChannel={requestLeaveChannel}
/>
))}
<ChannelGroupSection
browseAriaLabel="Browse channels"
createAriaLabel="Create a channel"
draggable
hasUnread={unreadChannelIds.size > 0}
isCollapsed={collapsedGroups.channels}
isActiveChannel={selectedView === "channel"}
activeWorkingByChannelId={activeWorkingByChannelId}
items={sectionBuckets.unassigned}
listTestId="stream-list"
onBrowseClick={onBrowseChannels}
onCreateClick={() => openCreateDialog("stream")}
onMarkAllRead={onMarkAllChannelsRead}
onMarkChannelRead={onMarkChannelRead}
onMarkChannelUnread={onMarkChannelUnread}
onSelectChannel={onSelectChannel}
onToggleCollapsed={() => toggleCollapsedGroup("channels")}
selectedChannelId={selectedChannelId}
title="Channels"
unreadChannelCounts={unreadChannelCounts}
unreadChannelIds={unreadChannelIds}
sections={channelSections}
assignments={channelAssignments}
onAssignChannel={assignChannel}
onUnassignChannel={unassignChannel}
onCreateSectionForChannel={handleCreateSectionForChannel}
mutedChannelIds={mutedChannelIds}
onMuteChannel={onMuteChannel}
onUnmuteChannel={onUnmuteChannel}
starredChannelIds={starredChannelIds}
onStarChannel={onStarChannel}
onUnstarChannel={onUnstarChannel}
onLeaveChannel={requestLeaveChannel}
/>
</SidebarDndContext>
<FeatureGate feature="forum">
<ChannelGroupSection
createAriaLabel="Create a forum"
hasUnread={unreadChannelIds.size > 0}
isCollapsed={collapsedGroups.forums}
isActiveChannel={selectedView === "channel"}
activeWorkingByChannelId={activeWorkingByChannelId}
items={forumChannels}
listTestId="forum-list"
onCreateClick={() => openCreateDialog("forum")}
onMarkAllRead={onMarkAllChannelsRead}
onMarkChannelRead={onMarkChannelRead}
onMarkChannelUnread={onMarkChannelUnread}
onSelectChannel={onSelectChannel}
onToggleCollapsed={() => toggleCollapsedGroup("forums")}
selectedChannelId={selectedChannelId}
title="Forums"
unreadChannelCounts={unreadChannelCounts}
unreadChannelIds={unreadChannelIds}
mutedChannelIds={mutedChannelIds}
onMuteChannel={onMuteChannel}
onUnmuteChannel={onUnmuteChannel}
/>
</FeatureGate>
<SidebarSection
action={
<div className="absolute right-1 top-1/2 z-10 flex -translate-y-1/2 items-center gap-0.5">
<button
aria-expanded={isNewDmOpen}
aria-label="Compose new message"
className={`${SECTION_ICON_BUTTON_CLASS} ${SECTION_ACTION_VISIBILITY_CLASS}`}
data-testid="new-dm-trigger"
onClick={() => {
setIsNewDmOpen(true);
}}
title="Compose new message"
type="button"
>
<MessageCirclePlus className="h-4 w-4" />
</button>
</div>
}
dmParticipantsByChannelId={dmParticipantsByChannelId}
isCollapsed={collapsedGroups.directMessages}
isActiveChannel={selectedView === "channel"}
activeWorkingByChannelId={activeWorkingByChannelId}
items={forumChannels}
listTestId="forum-list"
onCreateClick={() => openCreateDialog("forum")}
onMarkAllRead={onMarkAllChannelsRead}
items={sortedDirectMessages}
channelLabels={dmChannelLabels}
onHideDm={onHideDm}
onMarkChannelRead={onMarkChannelRead}
onMarkChannelUnread={onMarkChannelUnread}
onSelectChannel={onSelectChannel}
onToggleCollapsed={() => toggleCollapsedGroup("forums")}
onToggleCollapsed={() =>
toggleCollapsedGroup("directMessages")
}
presenceByChannelId={dmPresenceByChannelId}
selectedChannelId={selectedChannelId}
title="Forums"
testId="dm-list"
title="Direct messages"
unreadChannelCounts={unreadChannelCounts}
unreadChannelIds={unreadChannelIds}
mutedChannelIds={mutedChannelIds}
onMuteChannel={onMuteChannel}
onUnmuteChannel={onUnmuteChannel}
/>
</FeatureGate>
<SidebarSection
action={
<div className="absolute right-1 top-1/2 z-10 flex -translate-y-1/2 items-center gap-0.5">
<button
aria-expanded={isNewDmOpen}
aria-label="Compose new message"
className={`${SECTION_ICON_BUTTON_CLASS} ${SECTION_ACTION_VISIBILITY_CLASS}`}
data-testid="new-dm-trigger"
onClick={() => {
setIsNewDmOpen(true);
}}
title="Compose new message"
type="button"
>
<MessageCirclePlus className="h-4 w-4" />
</button>
</div>
}
dmParticipantsByChannelId={dmParticipantsByChannelId}
isCollapsed={collapsedGroups.directMessages}
isActiveChannel={selectedView === "channel"}
activeWorkingByChannelId={activeWorkingByChannelId}
items={sortedDirectMessages}
channelLabels={dmChannelLabels}
onHideDm={onHideDm}
onMarkChannelRead={onMarkChannelRead}
onMarkChannelUnread={onMarkChannelUnread}
onSelectChannel={onSelectChannel}
onToggleCollapsed={() => toggleCollapsedGroup("directMessages")}
presenceByChannelId={dmPresenceByChannelId}
selectedChannelId={selectedChannelId}
testId="dm-list"
title="Direct messages"
unreadChannelCounts={unreadChannelCounts}
unreadChannelIds={unreadChannelIds}
mutedChannelIds={mutedChannelIds}
onMuteChannel={onMuteChannel}
onUnmuteChannel={onUnmuteChannel}
/>
</>
) : null}
</>
) : null}
{errorMessage &&
!sidebarRelayConnectionCard.hasRelayUnreachableError ? (
<div className="px-3 py-2 text-sm text-destructive">
{errorMessage}
</div>
) : null}
</SidebarContent>
{errorMessage &&
!sidebarRelayConnectionCard.hasRelayUnreachableError ? (
<div className="px-3 py-2 text-sm text-destructive">
{errorMessage}
</div>
) : null}
</SidebarContent>
</div>
<div className="relative z-30 shrink-0">
{unreadBelowCount > 0 ? (

View File

@@ -0,0 +1,157 @@
import { Activity, Bot, FolderGit2, Inbox, Zap } from "lucide-react";
import { TopbarSearch } from "@/features/search/ui/TopbarSearch";
import { FeatureGate } from "@/shared/features";
import type { Channel, SearchHit } from "@/shared/api/types";
import {
SidebarHeader,
SidebarMenu,
SidebarMenuBadge,
SidebarMenuButton,
SidebarMenuItem,
} from "@/shared/ui/sidebar";
type SidebarSelectedView =
| "home"
| "channel"
| "agents"
| "workflows"
| "pulse"
| "projects";
type AppSidebarPinnedHeaderProps = {
channelLabels: Record<string, string>;
currentPubkey?: string;
homeBadgeCount: number;
onCreateAgent: () => void;
onCreateChannel: () => void;
onOpenDm: (input: { pubkeys: string[] }) => Promise<void>;
onOpenSearchResult: (hit: SearchHit) => void;
onSelectAgents: () => void;
onSelectChannel: (channelId: string) => void;
onSelectHome: () => void;
onSelectProjects: () => void;
onSelectPulse: () => void;
onSelectWorkflows: () => void;
searchChannels: Channel[];
searchFocusRequest: number;
selectedView: SidebarSelectedView;
suggestionChannels: Channel[];
};
export function AppSidebarPinnedHeader({
channelLabels,
currentPubkey,
homeBadgeCount,
onCreateAgent,
onCreateChannel,
onOpenDm,
onOpenSearchResult,
onSelectAgents,
onSelectChannel,
onSelectHome,
onSelectProjects,
onSelectPulse,
onSelectWorkflows,
searchChannels,
searchFocusRequest,
selectedView,
suggestionChannels,
}: AppSidebarPinnedHeaderProps) {
return (
<div className="shrink-0 px-2 pt-2.5" data-testid="sidebar-pinned-header">
<TopbarSearch
channelLabels={channelLabels}
channels={searchChannels}
currentPubkey={currentPubkey}
focusRequest={searchFocusRequest}
onOpenChannel={onSelectChannel}
onOpenResult={onOpenSearchResult}
onOpenUser={(user) => onOpenDm({ pubkeys: [user.pubkey] })}
onCreateAgent={onCreateAgent}
onCreateChannel={onCreateChannel}
suggestionChannels={suggestionChannels}
/>
<SidebarHeader
className="cursor-default select-none px-0 pb-0 pt-2.5"
data-tauri-drag-region
>
<SidebarMenu className="pb-2">
<SidebarMenuItem>
<SidebarMenuButton
isActive={selectedView === "home"}
onClick={onSelectHome}
tooltip="Inbox"
type="button"
>
<Inbox className="h-4 w-4" />
<span>Inbox</span>
</SidebarMenuButton>
{homeBadgeCount > 0 ? (
<SidebarMenuBadge
className="right-2 rounded-full bg-primary/15 px-1.5 text-2xs text-primary peer-data-[active=true]/menu-button:bg-sidebar-active-foreground/20 peer-data-[active=true]/menu-button:text-sidebar-active-foreground"
data-testid="sidebar-home-count"
>
{Math.min(homeBadgeCount, 99)}
</SidebarMenuBadge>
) : null}
</SidebarMenuItem>
<FeatureGate feature="pulse">
<SidebarMenuItem>
<SidebarMenuButton
data-testid="open-pulse-view"
isActive={selectedView === "pulse"}
onClick={onSelectPulse}
tooltip="Pulse"
type="button"
>
<Activity className="h-4 w-4" />
<span>Pulse</span>
</SidebarMenuButton>
</SidebarMenuItem>
</FeatureGate>
<FeatureGate feature="projects">
<SidebarMenuItem>
<SidebarMenuButton
data-testid="open-projects-view"
isActive={selectedView === "projects"}
onClick={onSelectProjects}
tooltip="Projects"
type="button"
>
<FolderGit2 className="h-4 w-4" />
<span>Projects</span>
</SidebarMenuButton>
</SidebarMenuItem>
</FeatureGate>
<SidebarMenuItem>
<SidebarMenuButton
data-testid="open-agents-view"
isActive={selectedView === "agents"}
onClick={onSelectAgents}
tooltip="Agents"
type="button"
>
<Bot className="h-4 w-4" />
<span>Agents</span>
</SidebarMenuButton>
</SidebarMenuItem>
<FeatureGate feature="workflows">
<SidebarMenuItem>
<SidebarMenuButton
data-testid="open-workflows-view"
isActive={selectedView === "workflows"}
onClick={onSelectWorkflows}
tooltip="Workflows"
type="button"
>
<Zap className="h-4 w-4" />
<span>Workflows</span>
</SidebarMenuButton>
</SidebarMenuItem>
</FeatureGate>
</SidebarMenu>
</SidebarHeader>
</div>
);
}

View File

@@ -10,8 +10,9 @@
* sidebar row, so `top-0` inside the sidebar starts below the traffic-light
* strip.
*
* This spec injects a synthetic pill into the live sidebar's relative
* container and asserts the pill clears the chrome strip.
* This spec injects a synthetic pill into the live sidebar channel-content
* container (the same relative wrapper that owns the real top unread pill) and
* asserts the pill clears the pinned header.
*/
import { expect, test } from "@playwright/test";
@@ -39,9 +40,9 @@ async function injectSyntheticPill(
await page.evaluate(
({ topClass, base, html, testId }) => {
const container = document.querySelector(
'[data-testid="app-sidebar-scroll-anchor"]',
'[data-testid="sidebar-channel-content"]',
) as HTMLElement | null;
if (!container) throw new Error("sidebar scroll anchor not found");
if (!container) throw new Error("sidebar channel content not found");
// Remove any prior injection so retries start from a clean sidebar.
container
@@ -68,16 +69,22 @@ test.describe("sidebar MoreUnreadButton top chrome overlap", () => {
await expect(page.getByTestId("app-sidebar")).toBeVisible();
});
test("top pill clears the in-flow traffic-light strip", async ({ page }) => {
test("top pill clears the pinned header", async ({ page }) => {
await injectSyntheticPill(page, TOP_CLASS, "synthetic-top");
const pill = page.getByTestId("synthetic-top");
const pinnedHeader = page.getByTestId("sidebar-pinned-header");
await expect(pill).toBeVisible();
await expect(pinnedHeader).toBeVisible();
const box = await pill.boundingBox();
expect(box).not.toBeNull();
// The pill is anchored at the top of the sidebar row, below the 40px
// in-flow chrome strip.
expect(box?.y ?? Number.NaN).toBeGreaterThanOrEqual(40);
const pillBox = await pill.boundingBox();
const pinnedHeaderBox = await pinnedHeader.boundingBox();
expect(pillBox).not.toBeNull();
expect(pinnedHeaderBox).not.toBeNull();
expect(pillBox?.y ?? Number.NaN).toBeGreaterThanOrEqual(
pinnedHeaderBox?.y == null || pinnedHeaderBox.height == null
? Number.NaN
: pinnedHeaderBox.y + pinnedHeaderBox.height,
);
await waitForAnimations(page);
});