Skip to content

slides: a right-click menu that belongs to the deck - #275

Open
iamgeo92 wants to merge 1 commit into
nyblnet:mainfrom
iamgeo92:context-menu
Open

slides: a right-click menu that belongs to the deck#275
iamgeo92 wants to merge 1 commit into
nyblnet:mainfrom
iamgeo92:context-menu

Conversation

@iamgeo92

@iamgeo92 iamgeo92 commented Aug 10, 2026

Copy link
Copy Markdown

The problem

The editor has no context menu at all. grep -rn "contextmenu" slides/src kernel/src returns nothing. So the one gesture every user of every other editor
tries first produces the browser's menu — Back, Reload, View source, Save page
as — which is precisely the wrong set of verbs for a slide.

It also matters beyond the mouse: a long-press on a phone has nothing to open,
which is the follow-up PR this one exists to enable.

What this adds

Three menus, chosen by what was actually right-clicked.

target items
an element Edit text · Cut · Copy · Duplicate · Bring to front · Send to back · Group / Ungroup · Delete
a slide thumbnail New slide · Duplicate slide · Delete slide
the canvas beside the slide Paste · Duplicate slide · Delete slide

Every verb routes to the code that already implements it — duplicateSelection,
panel.reorder, panel.group/ungroup, duplicateSlide, deleteSlide,
openLayoutPicker. Nothing is reimplemented, so nothing can drift from the
keyboard shortcuts, whose accelerators are shown in the rows.

Right-clicking something outside the selection selects it first — the rule
every editor follows, and the only way the verbs can be honest about what they
are about to act on. Group greys out below two elements; the menu flips to
Ungroup when the selection already is one.

Two details worth reviewing

The browser's menu is deliberately preserved where it is better — form
fields, links, and text mid-edit, where the system menu carries spelling,
dictation, look-up and a real paste. Getting that last case right needed two
non-obvious things:

  • The question must be asked on the press. A right-click commits a live
    text edit (the press blurs the caret), and it does so before contextmenu is
    dispatched — so asking the canvas at menu time always hears "not editing".
  • The answer must come from geometry, not DOM containment. Moveable's
    control box sits on top of the text being edited, so a press aimed squarely at
    the caret is delivered to a resize handle and node.contains(target) is
    false. Both were caught by tests, not by reading.

ctxmenu.ts is deliberately dumb. It renders rows, positions itself and
dismisses; it knows nothing about slides or elements. It flips rather than clamps
near a screen edge, so the pointer never ends up inside the menu it just
opened (a clamped menu slides under the cursor and the first thing that happens
is a mis-click).

Paste shares the text path with the paste event rather than growing a
second copy — which is why wirePaste is split into a pasteFromText(text).
A menu click carries no clipboardData, so it has to read the clipboard itself,
and being refused (Safari prompts; Firefox has no readText) is a real answer
that gets a toast rather than silence.

Demo

context menu

Stills — the three menus
element thumbnail canvas

How this was verified

Dev server at 1280×820 with a real mouse right-click:

case result
right-click an element opens the menu pass
the menu carries all eight element verbs pass
right-clicking outside the selection selects that element pass
Group is disabled for a single element pass
Duplicate through the menu adds an element pass
Delete through the menu removes one pass
Edit text through the menu opens the inline editor pass
slide background offers Paste + the slide verbs pass
thumbnail offers New / Duplicate / Delete slide pass
choosing an item closes the menu pass
clicking away closes it pass
Escape closes it pass
a menu opened at the bottom-right corner flips fully on-screen pass
no custom menu over the deck-title field pass
no custom menu inside text being edited pass

Regression tests for the wirePaste split (the riskiest part of the diff):

case result
⌘C then ⌘V still pastes elements pass
plain text on the clipboard still becomes a text box pass
menu Paste inserts the same way pass

Gates:

node_modules/.bin/tsc -b                                        clean
npm run build:single                                            ok (667KB shell)
node scripts/shell-gate.mjs …/Bento_Slides.bento.html           splice contract OK
node scripts/build-i18n.mjs --check                             current (736 × 8)

i18n

Eight new strings, added to all eight catalogs (de · es · fr · it · ja · pt ·
zh-Hans · zh-Hant) at the top of each map per docs/PARALLEL-WORK.md §3, with
packed.ts regenerated by scripts/build-i18n.mjs:

Cut · Copy · Paste · Bring to front · Send to back · Ungroup ·
Edit text · Nothing to paste — use ⌘V

The rest reuse keys the catalogs already carry (Duplicate, Delete, Group,
New slide, Duplicate slide, Delete slide). Translations are machine-drafted
to match the existing catalogs' own stated standard — native review welcome, as
the file headers invite.

No format change, no kernel change, nothing under sync/.

Follow-up: long-press opens this same menu on touch. Kept separate so this
one can be judged as a desktop feature on its own.


Changelog entry — not in the diff, on purpose

CHANGELOG.md is this repo's named conflict magnet (docs/PARALLEL-WORK.md §3). With several of these open at once, and [Unreleased] being emptied every time a release is cut, a changelog hunk made every one of them conflict on that file and nothing else — twice over. They carry no CHANGELOG.md change so they stay mergeable; here is the entry to drop in at release time, or I'll add it back in whatever form you prefer.

- **Right-click now opens a menu that belongs to the deck.** Until now the
  editor had no context menu at all, so a right-click anywhere on the canvas
  produced the browser's own — Back, Reload, View source — which is exactly the
  wrong set of verbs for a slide. Right-clicking an element offers Edit text,
  Cut, Copy, Duplicate, Bring to front, Send to back, Group / Ungroup and
  Delete; a slide thumbnail offers New slide, Duplicate slide and Delete slide;
  the canvas beside the slide offers Paste and the slide's own verbs. Aiming at
  something outside the selection selects it first, the way every editor does,
  so the verbs always describe what they are about to act on. The browser's menu
  is deliberately left alone where it is the better one — form fields, links,
  and text being edited, where the system carries spelling, dictation, look-up
  and a real paste.

The editor had no context menu at all, so a right-click on the canvas
produced the browser's own — Back, Reload, View source — which is
precisely the wrong set of verbs for a slide, and the one gesture every
user of every other editor tries first.

Three menus, chosen by what was actually clicked. An element offers Edit
text, Cut, Copy, Duplicate, Bring to front, Send to back, Group /
Ungroup and Delete. A slide thumbnail offers New slide, Duplicate slide
and Delete slide. The canvas beside the slide offers Paste and the
slide's own verbs. Aiming at something outside the selection selects it
first, the way every editor does, so the verbs always describe what they
are about to act on.

The browser's menu is deliberately left where it is the better one: form
fields, links, and text mid-edit, where the system carries spelling,
dictation, look-up and a real paste. That last case needs the question
asked on the PRESS — a right-click commits the edit before contextmenu
is dispatched — and answered by geometry, because Moveable's control box
sits on top of the caret and swallows the target.

ctxmenu.ts renders and dismisses; it knows nothing about slides. Paste
shares the text path with the paste EVENT rather than growing a second
copy of it, which is why wirePaste is split.
@iamgeo92

Copy link
Copy Markdown
Author

@nyblnet — this is the largest of the batch after #292, so it's the one I'd most value your read on.

The editor has no context menu at all (grep -rn contextmenu slides/src returns nothing), so right-click gives the browser's own — Back, Reload, View source — over a slide. This adds three menus chosen by target: element, slide thumbnail, canvas. Every verb routes to code that already exists (duplicateSelection, panel.reorder, group/ungroup, duplicateSlide, …), so nothing can drift from the keyboard shortcuts.

Two decisions I'd like checked:

  • The browser's menu is deliberately kept over form fields, links and text mid-edit, where the system carries spelling, dictation and a real paste. Getting that last case right needed the question asked on the press (a right-click commits the edit before contextmenu fires) and answered by geometry (Moveable's control box sits on top of the caret, so contains(target) says false).
  • wirePaste is split so the menu's Paste shares the text path rather than growing a second copy. That's the riskiest hunk in the diff; there are regression tests for ⌘C/⌘V, plain-text paste and menu Paste.

Also adds 8 strings to all eight catalogs. #276 is stacked on this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant