feat(web): adopt shadcn context-menu for mascot + desktop right-click menus
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled
ci / web (push) Has been cancelled
Desktop App / Build Linux (amd64) (push) Has been cancelled
Desktop App / Attach to Release (push) Has been cancelled

Problem: the mascot's right-click menu was non-interactive — RadialMenu's
root div rendered inside MascotLayer's pointer-events-none root (and the
new DockedLayer wrapper compounded it) without re-enabling pointer-events,
so clicks passed straight through. The desktop right-click menu was a
hand-rolled positioned div, inconsistent with the rest of the UI.

Change: both menus now use the shadcn-svelte context-menu primitive
(bits-ui, portaled to <body>).
- Mascot: MascotMenu.svelte renders the action tree recursively —
  children become ContextMenu.Sub (native hover sub-menu navigation,
  replacing the manual breadcrumb stack), leaves become ContextMenu.Item
  with onSelect. MascotLayer wraps <Mascot> in a ContextMenu.Trigger;
  visibility predicates read reactively off ctx.model so items
  appear/disappear live. Removed the manual menuPos/openMenu/closeMenu
  machinery. RadialMenu.svelte deleted.
- Desktop: the surface's bare-desktop hit area is now a
  ContextMenu.Trigger layer (absolute inset-0, pointer-events-auto)
  placed before the icons/windows in the DOM. The DOM-structure gate
  (icons/windows are pointer-events-auto siblings that paint on top and
  intercept their own right-clicks; bare desktop falls through to the
  trigger) replaces the old fragile e.currentTarget === e.target check.
  Left-click blur moved onto the trigger; Undo/Redo disabled state
  snapshotted via onOpenChange (canUndo/canRedo are wmkit methods).

Risk: the blocker that made the mascot menu non-interactive in the first
place — Mascot.svelte's handleContextMenu called e.stopPropagation(),
which would have prevented a ContextMenu.Trigger wrapper from ever
seeing the right-click. Removed that handler; bits-ui now owns
right-click on the mascot, left-click drag/pet passes through. The
context-menu content portals to <body>, escaping the pointer-events-none
mascot and docked layers entirely — the structural fix, not just a
component swap.

Verification: vitest 38/38; svelte-check + tsc clean for changed files;
eslint clean (the shadcn-generated ui/context-menu/* files carry the
same baseline custom_element_props_identifier warnings as the rest of
the ui/ folder, not from this change); vite build green; runtime
confirmed — right-click mascot opens the action tree with hover
sub-menus, right-click bare desktop opens Cascade/Tile/Show/Reset/
Undo/Redo, right-click on an icon or window does not.
This commit is contained in:
2026-07-21 16:03:57 +02:00
parent 482c7f3448
commit 1c12d40712
25 changed files with 508 additions and 297 deletions

View File

@@ -0,0 +1,52 @@
import Root from "./context-menu.svelte";
import Sub from "./context-menu-sub.svelte";
import Portal from "./context-menu-portal.svelte";
import Trigger from "./context-menu-trigger.svelte";
import Group from "./context-menu-group.svelte";
import RadioGroup from "./context-menu-radio-group.svelte";
import Item from "./context-menu-item.svelte";
import GroupHeading from "./context-menu-group-heading.svelte";
import Content from "./context-menu-content.svelte";
import Shortcut from "./context-menu-shortcut.svelte";
import RadioItem from "./context-menu-radio-item.svelte";
import Separator from "./context-menu-separator.svelte";
import SubContent from "./context-menu-sub-content.svelte";
import SubTrigger from "./context-menu-sub-trigger.svelte";
import CheckboxItem from "./context-menu-checkbox-item.svelte";
import Label from "./context-menu-label.svelte";
export {
Root,
Sub,
Portal,
Item,
GroupHeading,
Label,
Group,
Trigger,
Content,
Shortcut,
Separator,
RadioItem,
SubContent,
SubTrigger,
RadioGroup,
CheckboxItem,
//
Root as ContextMenu,
Sub as ContextMenuSub,
Portal as ContextMenuPortal,
Item as ContextMenuItem,
GroupHeading as ContextMenuGroupHeading,
Group as ContextMenuGroup,
Content as ContextMenuContent,
Trigger as ContextMenuTrigger,
Shortcut as ContextMenuShortcut,
RadioItem as ContextMenuRadioItem,
Separator as ContextMenuSeparator,
RadioGroup as ContextMenuRadioGroup,
SubContent as ContextMenuSubContent,
SubTrigger as ContextMenuSubTrigger,
CheckboxItem as ContextMenuCheckboxItem,
Label as ContextMenuLabel,
};