mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
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:
co-authored by
Cursor
parent
c91fabe54c
commit
524142c05e
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user