mirror of
https://github.com/block/buzz.git
synced 2026-11-13 04:41:12 +00:00
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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user