fix(desktop): polish profile menu follow-ups

Tighten the sidebar profile label behavior and align the server submenu interactions with the rest of the profile menu.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Thomas Petersen
2026-06-03 12:55:57 -04:00
co-authored by Cursor
parent c91fabe54c
commit 524142c05e
3 changed files with 143 additions and 29 deletions
@@ -121,7 +121,7 @@ export function ProfilePopover({
<PopoverContent
side="top"
align="start"
sideOffset={8}
sideOffset={-32}
className="w-[280px] rounded-xl border border-border bg-popover p-0 shadow-lg"
data-testid="profile-popover"
onInteractOutside={(event) => {
@@ -277,10 +277,7 @@ export function ProfilePopover({
<>
<hr className="mx-3 my-1 h-px border-0 bg-border" />
{/* ── Workspace / relay selector ─────────────────── */}
<div
className="px-4 py-2"
data-testid="profile-popover-workspace"
>
<div data-testid="profile-popover-workspace">
{workspaceSwitcherSlot}
</div>
</>
@@ -54,10 +54,19 @@ export function SidebarProfileCard({
[],
);
const hasStatus = Boolean(selfUserStatus?.text || selfUserStatus?.emoji);
const workspaceLabel = activeWorkspace?.name ?? "No workspace";
const readonlyWorkspaceLabel = (
<span className="flex min-w-0 items-center gap-1 text-xs leading-snug text-sidebar-foreground/70">
<span aria-hidden="true" className="shrink-0 text-[10px] leading-none">
🌱
</span>
<span className="truncate">{workspaceLabel}</span>
</span>
);
return (
<div
className="group/profile-card rounded-xl px-2 py-2 transition-colors hover:bg-sidebar-border/35 focus-within:bg-sidebar-border/35 dark:hover:bg-sidebar-border/30 dark:focus-within:bg-sidebar-border/30"
className="group/profile-card rounded-xl px-2 py-2 transition-colors hover:bg-sidebar-border/35 dark:hover:bg-sidebar-border/30"
data-testid="sidebar-profile-card"
ref={profileCardRef}
>
@@ -132,7 +141,7 @@ export function SidebarProfileCard({
<button
aria-label={`Open profile menu for ${resolvedDisplayName}`}
className={cn(
"flex w-full min-w-0 items-center truncate rounded-sm text-left text-xs text-sidebar-foreground/70 outline-hidden transition-opacity duration-150 focus:outline-none focus-visible:outline-none group-hover/profile-card:opacity-0 group-focus-within/profile-card:opacity-0",
"flex w-full min-w-0 items-center truncate rounded-sm text-left text-xs leading-snug text-sidebar-foreground/70 outline-hidden transition-opacity duration-150 focus:outline-none focus-visible:outline-none group-hover/profile-card:opacity-0",
profilePopoverOpen && "opacity-100",
)}
data-testid="sidebar-profile-user-status"
@@ -146,27 +155,15 @@ export function SidebarProfileCard({
</button>
<div
className={cn(
"pointer-events-none absolute inset-0 flex min-w-0 items-center text-xs leading-none text-sidebar-foreground/70 opacity-0 transition-opacity duration-150 group-hover/profile-card:opacity-100 group-focus-within/profile-card:opacity-100",
"pointer-events-none absolute inset-0 flex min-w-0 items-center text-xs leading-snug text-sidebar-foreground/70 opacity-0 transition-opacity duration-150 group-hover/profile-card:opacity-100",
profilePopoverOpen && "opacity-0",
)}
>
<span className="truncate">
{activeWorkspace?.name ?? "No workspace"}
</span>
{readonlyWorkspaceLabel}
</div>
</div>
) : (
<div className="relative mt-0.5">
<WorkspaceSwitcher
activeWorkspace={activeWorkspace}
onAddWorkspace={onOpenAddWorkspace}
onRemoveWorkspace={onRemoveWorkspace}
onSwitchWorkspace={onSwitchWorkspace}
onUpdateWorkspace={onUpdateWorkspace}
variant="profile"
workspaces={workspaces}
/>
</div>
<div className="relative mt-0.5">{readonlyWorkspaceLabel}</div>
)}
</div>
</div>
@@ -21,6 +21,7 @@ import {
SidebarMenuButton,
SidebarMenuItem,
} from "@/shared/ui/sidebar";
import { Popover, PopoverContent, PopoverTrigger } from "@/shared/ui/popover";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/shared/ui/tooltip";
import type { ConnectionState } from "@/shared/api/relayClientShared";
import {
@@ -63,11 +64,48 @@ export function WorkspaceSwitcher({
const [editingWorkspace, setEditingWorkspace] =
React.useState<Workspace | null>(null);
const [dropdownOpen, setDropdownOpen] = React.useState(false);
const profileMenuHoverTimer = React.useRef<number | null>(null);
const connectionState = useRelayConnection();
const degraded = isRelayConnectionDegraded(connectionState);
const connectionLabel = CONNECTION_STATE_LABEL[connectionState];
const isProfileLike = variant === "profile" || variant === "profile-menu";
function clearProfileMenuHoverTimer() {
if (profileMenuHoverTimer.current !== null) {
window.clearTimeout(profileMenuHoverTimer.current);
profileMenuHoverTimer.current = null;
}
}
function scheduleProfileMenu(nextOpen: boolean) {
if (variant !== "profile-menu") return;
clearProfileMenuHoverTimer();
profileMenuHoverTimer.current = window.setTimeout(
() => setDropdownOpen(nextOpen),
nextOpen ? 80 : 160,
);
}
function handleProfileMenuOpenChange(nextOpen: boolean) {
if (variant !== "profile-menu") {
setDropdownOpen(nextOpen);
return;
}
if (!nextOpen) {
clearProfileMenuHoverTimer();
}
setDropdownOpen(nextOpen);
}
React.useEffect(
() => () => {
if (profileMenuHoverTimer.current !== null) {
window.clearTimeout(profileMenuHoverTimer.current);
}
},
[],
);
const triggerContent = (
<>
{variant === "profile-menu" ? null : degraded ? (
@@ -124,6 +162,92 @@ export function WorkspaceSwitcher({
</>
);
const profileMenuPopover =
variant === "profile-menu" ? (
<Popover open={dropdownOpen} onOpenChange={handleProfileMenuOpenChange}>
<PopoverTrigger asChild>
<button
aria-expanded={dropdownOpen}
aria-haspopup="menu"
aria-label={
degraded
? `${activeWorkspace?.name ?? "Workspace"} — ${connectionLabel}`
: "Switch workspace"
}
className="flex w-full items-center gap-2 px-4 py-2 text-left text-sm text-popover-foreground outline-hidden transition-colors hover:bg-accent focus:bg-accent focus:outline-none focus-visible:bg-accent focus-visible:outline-none data-[state=open]:bg-accent data-[state=open]:text-popover-foreground"
data-testid="workspace-switcher"
onMouseEnter={() => scheduleProfileMenu(true)}
onMouseLeave={() => scheduleProfileMenu(false)}
role="menuitem"
type="button"
>
{triggerContent}
</button>
</PopoverTrigger>
<PopoverContent
align="end"
className="w-56 rounded-xl border border-border bg-popover p-1 shadow-lg"
onMouseEnter={() => scheduleProfileMenu(true)}
onMouseLeave={() => scheduleProfileMenu(false)}
side="right"
sideOffset={0}
>
<div aria-label="Workspaces" role="menu">
{workspaces.map((workspace) => (
<div
className="group flex items-center rounded-xs transition-colors hover:bg-accent focus-within:bg-accent"
key={workspace.id}
>
<button
className="flex min-w-0 flex-1 items-center gap-2 px-2 py-1.5 text-left text-sm outline-hidden focus:outline-none"
onClick={() => {
onSwitchWorkspace(workspace.id);
setDropdownOpen(false);
}}
role="menuitem"
type="button"
>
<span className="flex h-4 w-4 shrink-0 items-center justify-center">
{activeWorkspace?.id === workspace.id ? (
<Check className="h-3.5 w-3.5 text-primary" />
) : null}
</span>
<span className="min-w-0 flex-1 truncate">
{workspace.name}
</span>
</button>
<button
aria-label={`Edit ${workspace.name}`}
className="mr-1 flex h-5 w-5 shrink-0 items-center justify-center rounded opacity-0 hover:bg-accent group-hover:opacity-100 group-focus-within:opacity-100"
onClick={(e) => {
e.stopPropagation();
setDropdownOpen(false);
setEditingWorkspace(workspace);
}}
type="button"
>
<MoreHorizontal className="h-3.5 w-3.5" />
</button>
</div>
))}
<div className="-mx-1 my-1 h-px bg-muted" />
<button
className="flex w-full items-center gap-2 rounded-xs px-2 py-1.5 text-left text-sm outline-hidden transition-colors hover:bg-accent focus:bg-accent focus:outline-none focus-visible:bg-accent focus-visible:outline-none"
onClick={() => {
setDropdownOpen(false);
onAddWorkspace();
}}
role="menuitem"
type="button"
>
<Plus className="h-4 w-4" />
<span>Add Workspace</span>
</button>
</div>
</PopoverContent>
</Popover>
) : null;
const switcherDropdown = (
<DropdownMenu open={dropdownOpen} onOpenChange={setDropdownOpen}>
<DropdownMenuTrigger asChild>
@@ -163,13 +287,7 @@ export function WorkspaceSwitcher({
align="start"
className="w-(--radix-dropdown-menu-trigger-width) min-w-[220px]"
onCloseAutoFocus={(e) => e.preventDefault()}
side={
variant === "profile-menu"
? "right"
: variant === "profile"
? "top"
: "bottom"
}
side={variant === "profile" ? "top" : "bottom"}
sideOffset={4}
>
{workspaces.map((workspace) => (
@@ -214,6 +332,8 @@ export function WorkspaceSwitcher({
<>
{variant === "profile" ? (
switcherDropdown
) : variant === "profile-menu" ? (
profileMenuPopover
) : (
<SidebarMenu>
<SidebarMenuItem>{switcherDropdown}</SidebarMenuItem>