Polish desktop profile menu interactions (#836)

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Brain <21994759fc7a6fa6b965551d35cfd7897d262f2495467f2d78694ddcfa6a5c7e@sprout-oss.stage.blox.sqprod.co>
This commit is contained in:
thomaspblock
2026-06-04 11:48:57 -04:00
committed by GitHub
parent 0d9b8148f8
commit a13691b620
15 changed files with 777 additions and 392 deletions

View File

@@ -1,6 +1,7 @@
import {
Check,
ChevronDown,
ChevronRight,
MoreHorizontal,
Plus,
WifiOff,
@@ -20,13 +21,13 @@ 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 {
isRelayConnectionDegraded,
useRelayConnection,
} from "@/shared/api/useRelayConnection";
import { EditWorkspaceDialog } from "./EditWorkspaceDialog";
const CONNECTION_STATE_LABEL: Record<ConnectionState, string> = {
@@ -41,7 +42,7 @@ const CONNECTION_STATE_LABEL: Record<ConnectionState, string> = {
type WorkspaceSwitcherProps = {
activeWorkspace: Workspace | null;
workspaces: Workspace[];
variant?: "sidebar" | "profile";
variant?: "sidebar" | "profile" | "profile-menu";
onSwitchWorkspace: (id: string) => void;
onAddWorkspace: () => void;
onUpdateWorkspace: (
@@ -63,9 +64,47 @@ 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 isProfileVariant = variant === "profile";
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 = (
<>
@@ -75,26 +114,24 @@ export function WorkspaceSwitcher({
<span
aria-hidden="false"
className={
variant === "profile"
isProfileVariant
? "flex h-5 w-5 shrink-0 animate-pulse items-center justify-center rounded-md border border-sidebar-border/70 bg-sidebar-accent/40 text-destructive"
: "flex h-5 w-5 shrink-0 animate-pulse items-center justify-center text-destructive"
}
data-testid="relay-connection-warning"
role="img"
>
<WifiOff
className={variant === "profile" ? "h-3 w-3" : "h-4 w-4"}
/>
<WifiOff className={isProfileVariant ? "h-3 w-3" : "h-4 w-4"} />
</span>
</TooltipTrigger>
<TooltipContent side={variant === "profile" ? "top" : "bottom"}>
<TooltipContent side={isProfileVariant ? "top" : "bottom"}>
{connectionLabel}
</TooltipContent>
</Tooltip>
) : (
<span
className={
variant === "profile"
isProfileVariant
? "flex h-5 w-5 shrink-0 items-center justify-center rounded-md border border-sidebar-border/70 bg-sidebar-accent/40 text-[10px] leading-none"
: "flex h-5 w-5 shrink-0 items-center justify-center text-xs leading-none"
}
@@ -111,16 +148,106 @@ export function WorkspaceSwitcher({
>
{activeWorkspace?.name ?? "No workspace"}
</span>
<ChevronDown
className={
variant === "profile"
? "h-3 w-3 shrink-0 text-sidebar-foreground/45"
: "h-3.5 w-3.5 shrink-0 text-sidebar-foreground/50"
}
/>
{variant === "profile-menu" ? (
<ChevronRight className="h-4 w-4 shrink-0 text-muted-foreground" />
) : (
<ChevronDown
className={
isProfileVariant
? "h-3 w-3 shrink-0 text-sidebar-foreground/45"
: "h-3.5 w-3.5 shrink-0 text-sidebar-foreground/50"
}
/>
)}
</>
);
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>
@@ -131,7 +258,7 @@ export function WorkspaceSwitcher({
? `${activeWorkspace?.name ?? "Workspace"} — ${connectionLabel}`
: "Switch workspace"
}
className="flex min-w-0 max-w-full items-center gap-1.5 rounded-md py-0.5 text-left text-xs text-sidebar-foreground/50 transition-colors hover:text-sidebar-foreground data-[state=open]:text-sidebar-foreground"
className="flex min-w-0 max-w-full items-center gap-1.5 rounded-md py-0.5 text-left text-xs text-sidebar-foreground/50 outline-hidden transition-colors hover:text-sidebar-foreground focus:outline-none focus-visible:outline-none data-[state=open]:text-sidebar-foreground"
data-testid="workspace-switcher"
type="button"
>
@@ -201,6 +328,8 @@ export function WorkspaceSwitcher({
<>
{variant === "profile" ? (
switcherDropdown
) : variant === "profile-menu" ? (
profileMenuPopover
) : (
<SidebarMenu>
<SidebarMenuItem>{switcherDropdown}</SidebarMenuItem>