mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
fix(desktop): polish status editor interactions
Keep status editing focused in the dialog while preserving the profile card hit area and simplifying workspace menu branches. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
524142c05e
commit
180b1ccfe1
@@ -797,9 +797,6 @@ export function AppShell() {
|
||||
onSetUserStatus={(text, emoji) =>
|
||||
setUserStatusMutation.mutate({ text, emoji })
|
||||
}
|
||||
onClearUserStatus={() =>
|
||||
setUserStatusMutation.mutate({ text: "", emoji: "" })
|
||||
}
|
||||
profile={profileQuery.data}
|
||||
selfUserStatus={
|
||||
deferredPubkey
|
||||
|
||||
@@ -23,7 +23,6 @@ interface ProfilePopoverProps {
|
||||
userStatusEmoji?: string;
|
||||
onSetStatus: (status: PresenceStatus) => void;
|
||||
onSetUserStatus: (text: string, emoji: string) => void;
|
||||
onClearUserStatus: () => void;
|
||||
onOpenSettings: (section?: "profile" | "appearance") => void;
|
||||
children: React.ReactNode;
|
||||
// Optional outer container whose clicks should NOT close the popover.
|
||||
@@ -60,7 +59,6 @@ export function ProfilePopover({
|
||||
userStatusEmoji,
|
||||
onSetStatus,
|
||||
onSetUserStatus,
|
||||
onClearUserStatus,
|
||||
onOpenSettings,
|
||||
children,
|
||||
triggerContainerRef,
|
||||
@@ -292,7 +290,6 @@ export function ProfilePopover({
|
||||
hasExistingStatus={hasUserStatus}
|
||||
initialEmoji={userStatusEmoji}
|
||||
initialText={userStatusText}
|
||||
onClear={onClearUserStatus}
|
||||
onOpenChange={setStatusDialogOpen}
|
||||
onSave={onSetUserStatus}
|
||||
open={statusDialogOpen}
|
||||
|
||||
@@ -138,7 +138,6 @@ type AppSidebarProps = {
|
||||
onSelectSettings: (section?: "profile" | "appearance") => void;
|
||||
onSetPresenceStatus?: (status: "online" | "away" | "offline") => void;
|
||||
onSetUserStatus: (text: string, emoji: string) => void;
|
||||
onClearUserStatus: () => void;
|
||||
onSwitchWorkspace: (id: string) => void;
|
||||
selfUserStatus?: UserStatus;
|
||||
isPresencePending?: boolean;
|
||||
@@ -193,7 +192,6 @@ export function AppSidebar({
|
||||
onSelectSettings,
|
||||
onSetPresenceStatus,
|
||||
onSetUserStatus,
|
||||
onClearUserStatus,
|
||||
onSwitchWorkspace,
|
||||
selfUserStatus,
|
||||
isPresencePending,
|
||||
@@ -662,7 +660,6 @@ export function AppSidebar({
|
||||
<SidebarProfileCard
|
||||
activeWorkspace={activeWorkspace}
|
||||
isPresencePending={isPresencePending}
|
||||
onClearUserStatus={onClearUserStatus}
|
||||
onOpenAddWorkspace={onOpenAddWorkspace}
|
||||
onOpenSettings={onSelectSettings}
|
||||
onRemoveWorkspace={onRemoveWorkspace}
|
||||
|
||||
@@ -12,7 +12,6 @@ import { cn } from "@/shared/lib/cn";
|
||||
type SidebarProfileCardProps = {
|
||||
activeWorkspace: Workspace | null;
|
||||
isPresencePending?: boolean;
|
||||
onClearUserStatus: () => void;
|
||||
onOpenAddWorkspace: () => void;
|
||||
onOpenSettings: (section?: "profile" | "appearance") => void;
|
||||
onRemoveWorkspace: (id: string) => void;
|
||||
@@ -33,7 +32,6 @@ type SidebarProfileCardProps = {
|
||||
export function SidebarProfileCard({
|
||||
activeWorkspace,
|
||||
isPresencePending,
|
||||
onClearUserStatus,
|
||||
onOpenAddWorkspace,
|
||||
onOpenSettings,
|
||||
onRemoveWorkspace,
|
||||
@@ -53,10 +51,23 @@ export function SidebarProfileCard({
|
||||
() => setProfilePopoverOpen((prev) => !prev),
|
||||
[],
|
||||
);
|
||||
const handleCardClick = React.useCallback(
|
||||
(event: React.MouseEvent<HTMLDivElement>) => {
|
||||
const target = event.target;
|
||||
if (
|
||||
!(target instanceof Node) ||
|
||||
!profileCardRef.current?.contains(target)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
toggleProfilePopover();
|
||||
},
|
||||
[toggleProfilePopover],
|
||||
);
|
||||
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 className="flex min-w-0 cursor-pointer items-center gap-1 text-xs leading-snug text-sidebar-foreground/70">
|
||||
<span aria-hidden="true" className="shrink-0 text-[10px] leading-none">
|
||||
🌱
|
||||
</span>
|
||||
@@ -65,9 +76,11 @@ export function SidebarProfileCard({
|
||||
);
|
||||
|
||||
return (
|
||||
// biome-ignore lint/a11y/noStaticElementInteractions lint/a11y/useKeyWithClickEvents: child buttons provide keyboard access; wrapper fills pointer gaps between them.
|
||||
<div
|
||||
className="group/profile-card rounded-xl px-2 py-2 transition-colors hover:bg-sidebar-border/35 dark:hover:bg-sidebar-border/30"
|
||||
className="group/profile-card cursor-pointer rounded-xl px-2 py-2 transition-colors hover:bg-sidebar-border/35 dark:hover:bg-sidebar-border/30"
|
||||
data-testid="sidebar-profile-card"
|
||||
onClick={handleCardClick}
|
||||
ref={profileCardRef}
|
||||
>
|
||||
<div className="flex min-w-0 items-center gap-3">
|
||||
@@ -75,7 +88,10 @@ export function SidebarProfileCard({
|
||||
aria-label={`Open profile menu for ${resolvedDisplayName}`}
|
||||
className="relative shrink-0 rounded-xl outline-hidden focus:outline-none focus-visible:outline-none"
|
||||
data-testid="sidebar-profile-avatar-button"
|
||||
onClick={toggleProfilePopover}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
toggleProfilePopover();
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<ProfileAvatar
|
||||
@@ -107,7 +123,6 @@ export function SidebarProfileCard({
|
||||
userStatusEmoji={selfUserStatus?.emoji}
|
||||
onSetStatus={onSetPresenceStatus ?? (() => {})}
|
||||
onSetUserStatus={onSetUserStatus}
|
||||
onClearUserStatus={onClearUserStatus}
|
||||
onOpenSettings={onOpenSettings}
|
||||
triggerContainerRef={profileCardRef}
|
||||
workspaceSwitcherSlot={
|
||||
@@ -123,6 +138,10 @@ export function SidebarProfileCard({
|
||||
}
|
||||
>
|
||||
<button
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
toggleProfilePopover();
|
||||
}}
|
||||
className="block w-full min-w-0 rounded-sm text-left text-sidebar-foreground outline-hidden focus:outline-none focus-visible:outline-none"
|
||||
data-testid="open-settings"
|
||||
type="button"
|
||||
@@ -145,7 +164,10 @@ export function SidebarProfileCard({
|
||||
profilePopoverOpen && "opacity-100",
|
||||
)}
|
||||
data-testid="sidebar-profile-user-status"
|
||||
onClick={toggleProfilePopover}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
toggleProfilePopover();
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
{selfUserStatus?.emoji ? (
|
||||
|
||||
@@ -2,6 +2,7 @@ import * as React from "react";
|
||||
import Picker from "@emoji-mart/react";
|
||||
import data from "@emoji-mart/data";
|
||||
import * as PopoverPrimitive from "@radix-ui/react-popover";
|
||||
import { X } from "lucide-react";
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
@@ -32,7 +33,6 @@ type SetStatusDialogProps = {
|
||||
initialText?: string;
|
||||
initialEmoji?: string;
|
||||
onSave: (text: string, emoji: string) => void;
|
||||
onClear: () => void;
|
||||
hasExistingStatus: boolean;
|
||||
};
|
||||
|
||||
@@ -46,7 +46,6 @@ export function SetStatusDialog({
|
||||
initialText = "",
|
||||
initialEmoji = "",
|
||||
onSave,
|
||||
onClear,
|
||||
hasExistingStatus,
|
||||
}: SetStatusDialogProps) {
|
||||
const [text, setText] = React.useState(initialText);
|
||||
@@ -75,9 +74,9 @@ export function SetStatusDialog({
|
||||
onOpenChange(false);
|
||||
}
|
||||
|
||||
function handleClear() {
|
||||
onClear();
|
||||
onOpenChange(false);
|
||||
function handleClearDraft() {
|
||||
setText("");
|
||||
setEmoji("");
|
||||
}
|
||||
|
||||
function handleKeyDown(event: React.KeyboardEvent) {
|
||||
@@ -101,36 +100,21 @@ export function SetStatusDialog({
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex flex-col gap-4 pt-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="relative">
|
||||
<Popover onOpenChange={setPickerOpen} open={pickerOpen}>
|
||||
<div className="relative shrink-0">
|
||||
<PopoverTrigger asChild>
|
||||
<button
|
||||
aria-label="Choose status emoji"
|
||||
className="flex h-9 w-9 items-center justify-center rounded-md border border-input text-lg transition-colors hover:bg-accent"
|
||||
type="button"
|
||||
>
|
||||
{emoji || "\uD83D\uDCAC"}
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
{emoji ? (
|
||||
<button
|
||||
aria-label="Clear status emoji"
|
||||
className="absolute -right-1 -top-1 flex h-4 w-4 items-center justify-center rounded-full border border-background bg-muted text-[10px] leading-none text-muted-foreground hover:bg-accent hover:text-foreground"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
setEmoji("");
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
<PopoverTrigger asChild>
|
||||
<button
|
||||
aria-label="Choose status emoji"
|
||||
className="absolute left-1.5 top-1/2 z-10 flex h-7 w-7 -translate-y-1/2 items-center justify-center rounded-md text-lg transition-colors hover:bg-accent"
|
||||
type="button"
|
||||
>
|
||||
{emoji || "\uD83D\uDCAC"}
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
<PopoverPrimitive.Content
|
||||
align="start"
|
||||
className="z-50 w-auto overflow-hidden rounded-2xl bg-transparent shadow-none outline-hidden"
|
||||
sideOffset={4}
|
||||
className="z-50 w-auto overflow-hidden rounded-2xl shadow-md outline-hidden data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95"
|
||||
>
|
||||
<Picker
|
||||
data={data}
|
||||
@@ -146,12 +130,24 @@ export function SetStatusDialog({
|
||||
</Popover>
|
||||
<Input
|
||||
autoFocus
|
||||
className="pl-10 pr-9"
|
||||
data-testid="set-status-input"
|
||||
onChange={(event) => setText(event.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="What's your status?"
|
||||
value={text}
|
||||
/>
|
||||
{hasExistingStatus || text || emoji ? (
|
||||
<button
|
||||
aria-label="Clear status"
|
||||
className="absolute right-1.5 top-1/2 flex h-7 w-7 -translate-y-1/2 items-center justify-center rounded-full text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
|
||||
data-testid="set-status-clear"
|
||||
onClick={handleClearDraft}
|
||||
type="button"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
@@ -168,20 +164,7 @@ export function SetStatusDialog({
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between gap-2 pt-1">
|
||||
<div>
|
||||
{hasExistingStatus ? (
|
||||
<Button
|
||||
data-testid="set-status-clear"
|
||||
onClick={handleClear}
|
||||
size="sm"
|
||||
type="button"
|
||||
variant="ghost"
|
||||
>
|
||||
Clear status
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="flex items-center justify-end gap-2 pt-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
data-testid="set-status-cancel"
|
||||
|
||||
@@ -68,7 +68,7 @@ export function WorkspaceSwitcher({
|
||||
const connectionState = useRelayConnection();
|
||||
const degraded = isRelayConnectionDegraded(connectionState);
|
||||
const connectionLabel = CONNECTION_STATE_LABEL[connectionState];
|
||||
const isProfileLike = variant === "profile" || variant === "profile-menu";
|
||||
const isProfileVariant = variant === "profile";
|
||||
|
||||
function clearProfileMenuHoverTimer() {
|
||||
if (profileMenuHoverTimer.current !== null) {
|
||||
@@ -114,24 +114,24 @@ export function WorkspaceSwitcher({
|
||||
<span
|
||||
aria-hidden="false"
|
||||
className={
|
||||
isProfileLike
|
||||
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={isProfileLike ? "h-3 w-3" : "h-4 w-4"} />
|
||||
<WifiOff className={isProfileVariant ? "h-3 w-3" : "h-4 w-4"} />
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side={isProfileLike ? "top" : "bottom"}>
|
||||
<TooltipContent side={isProfileVariant ? "top" : "bottom"}>
|
||||
{connectionLabel}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<span
|
||||
className={
|
||||
isProfileLike
|
||||
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"
|
||||
}
|
||||
@@ -153,7 +153,7 @@ export function WorkspaceSwitcher({
|
||||
) : (
|
||||
<ChevronDown
|
||||
className={
|
||||
isProfileLike
|
||||
isProfileVariant
|
||||
? "h-3 w-3 shrink-0 text-sidebar-foreground/45"
|
||||
: "h-3.5 w-3.5 shrink-0 text-sidebar-foreground/50"
|
||||
}
|
||||
@@ -251,18 +251,14 @@ export function WorkspaceSwitcher({
|
||||
const switcherDropdown = (
|
||||
<DropdownMenu open={dropdownOpen} onOpenChange={setDropdownOpen}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
{isProfileLike ? (
|
||||
{variant === "profile" ? (
|
||||
<button
|
||||
aria-label={
|
||||
degraded
|
||||
? `${activeWorkspace?.name ?? "Workspace"} — ${connectionLabel}`
|
||||
: "Switch workspace"
|
||||
}
|
||||
className={
|
||||
variant === "profile-menu"
|
||||
? "flex w-full min-w-0 items-center gap-3 py-0.5 text-left text-sm text-popover-foreground outline-hidden transition-colors hover:text-popover-foreground focus:outline-none focus-visible:outline-none data-[state=open]:text-popover-foreground"
|
||||
: "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"
|
||||
}
|
||||
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"
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user