refactor(web): share one PointerMenu for the space and section menus

The space and section context menus repeated the same dropdown with an
invisible trigger anchored at the pointer; only their label and items differ.

Move-only: PointerMenu in pages/shell renders that dropdown and both menus
pass their label, close handler and items. DOM, attributes and handlers are
unchanged, and the labels stay in ShellPage, so the catalogs do not change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
This commit is contained in:
Claude 2026-10-04 10:06:20 +00:00
parent 0ab148ba03
commit ebb3cecd97
No known key found for this signature in database
2 changed files with 73 additions and 73 deletions

View File

@ -72,10 +72,7 @@ import {
AvatarStyleProvider, AvatarStyleProvider,
BotAvatar, BotAvatar,
Button, Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger,
GroupAvatar, GroupAvatar,
type GroupAvatarMember, type GroupAvatarMember,
InputGroup, InputGroup,
@ -214,6 +211,7 @@ import {
} from "./shell/dialogs"; } from "./shell/dialogs";
import { FullscreenComputerOverlay } from "./shell/fullscreen-computer-overlay"; import { FullscreenComputerOverlay } from "./shell/fullscreen-computer-overlay";
import { MainConversationRow } from "./shell/main-conversation-row"; import { MainConversationRow } from "./shell/main-conversation-row";
import { PointerMenu } from "./shell/pointer-menu";
import { cleanRosterPreview, formatRosterTime, stripSpaceTitlePrefix } from "./shell/roster-format"; import { cleanRosterPreview, formatRosterTime, stripSpaceTitlePrefix } from "./shell/roster-format";
import { rosterRowClass } from "./shell/roster-row-style"; import { rosterRowClass } from "./shell/roster-row-style";
import { ProjectRosterRow, TeamRosterRow } from "./shell/roster-rows"; import { ProjectRosterRow, TeamRosterRow } from "./shell/roster-rows";
@ -4552,43 +4550,23 @@ export function ShellPage() {
) : null} ) : null}
{spaceMenu ? ( {spaceMenu ? (
<DropdownMenu <PointerMenu
open position={spaceMenu.position}
onOpenChange={(open) => { label={t`Actions for space`}
if (!open) closeSpaceMenu(); onClose={closeSpaceMenu}
}}
> >
{/* Invisible anchor at the pointer position, mirroring the bot menu. */} <DropdownMenuItem
<DropdownMenuTrigger variant="destructive"
render={ onClick={() => {
<button const target = spaces.find((space) => space.id === spaceMenu.id);
type="button" if (target) setDeleteSpaceTarget(target);
tabIndex={-1} setSpaceMenu(null);
aria-hidden }}
className="fixed size-0 p-0 opacity-0"
style={{ left: spaceMenu.position.x, top: spaceMenu.position.y }}
/>
}
/>
<DropdownMenuContent
aria-label={t`Actions for space`}
align="start"
sideOffset={0}
className="w-[220px]"
> >
<DropdownMenuItem <Trash2 />
variant="destructive" {t`Delete space`}
onClick={() => { </DropdownMenuItem>
const target = spaces.find((space) => space.id === spaceMenu.id); </PointerMenu>
if (target) setDeleteSpaceTarget(target);
setSpaceMenu(null);
}}
>
<Trash2 />
{t`Delete space`}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
) : null} ) : null}
{deleteTarget ? ( {deleteTarget ? (
@ -4679,43 +4657,24 @@ export function ShellPage() {
) : null} ) : null}
{sectionMenu ? ( {sectionMenu ? (
<DropdownMenu <PointerMenu
open position={sectionMenu.position}
onOpenChange={(open) => { label={t`Actions for ${sectionMenu.section.name}`}
if (!open) closeSectionMenu(); onClose={closeSectionMenu}
}}
> >
<DropdownMenuTrigger <DropdownMenuItem
render={ onClick={() => {
<button setRenameSectionTarget({
type="button" section: sectionMenu.section,
tabIndex={-1} spaceId: sectionMenu.spaceId,
aria-hidden });
className="fixed size-0 p-0 opacity-0" setSectionMenu(null);
style={{ left: sectionMenu.position.x, top: sectionMenu.position.y }} }}
/>
}
/>
<DropdownMenuContent
aria-label={t`Actions for ${sectionMenu.section.name}`}
align="start"
sideOffset={0}
className="w-[220px]"
> >
<DropdownMenuItem <Pencil />
onClick={() => { {t`Rename section`}
setRenameSectionTarget({ </DropdownMenuItem>
section: sectionMenu.section, </PointerMenu>
spaceId: sectionMenu.spaceId,
});
setSectionMenu(null);
}}
>
<Pencil />
{t`Rename section`}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
) : null} ) : null}
<CommandPalette <CommandPalette

View File

@ -0,0 +1,41 @@
import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from "@rakazo/ui-web";
import type { ReactNode } from "react";
import type { ContextMenuPosition } from "../BotContextMenu";
/** A dropdown menu opened at the pointer, for sidebar context menus. */
export function PointerMenu({
position,
label,
onClose,
children,
}: {
position: ContextMenuPosition;
label: string;
onClose: () => void;
children: ReactNode;
}) {
return (
<DropdownMenu
open
onOpenChange={(open) => {
if (!open) onClose();
}}
>
{/* Invisible anchor at the pointer position, mirroring the bot menu. */}
<DropdownMenuTrigger
render={
<button
type="button"
tabIndex={-1}
aria-hidden
className="fixed size-0 p-0 opacity-0"
style={{ left: position.x, top: position.y }}
/>
}
/>
<DropdownMenuContent aria-label={label} align="start" sideOffset={0} className="w-[220px]">
{children}
</DropdownMenuContent>
</DropdownMenu>
);
}