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