fix(desktop): the right-panel toggle stays in one place and takes rapid clicks
Summary
Dean: "I don't like how the right panel toggle button moves with the right panel. It should always be in the same place … I want to be able to click it rapidly multiple times."
Before this PR, the toggle was a different button in each state. While the panel was hidden it was the last button on the plan toolbar (#1841). While the panel was open it was in the panel's own top row (#1850). So every press unmounted the button you had just clicked and mounted another one, and the new one rode the panel's slide. A second press during the slide could land on a node that was gone, or on the leaving panel, which is inert, and get dropped.
Now it works the way the left side does (#1827/#1840/#1848):
- One toggle, one element, fixed at the window's top right. It lives in a
.title-bar-controls[data-edge="end"]cluster inDesktopShell, the mirror of the sidebar toggle's cluster.- It is vertically centred on the traffic lights' line, like the left cluster.
- It is inset from the right edge by the same
--title-bar-inset(8px) that the left cluster keeps from the lights. Both clusters now share that variable. - It comes after the drag strips in DOM order and is
no-drag(see #1840). - Opening, closing, full screen, resizing and the slide never remount or move it, so hover, focus and its Tooltip stay where they are.
- Content leaves room for it.
--panel-toggle-clearance(inset + button + the toolbar's 8px gap) is the right padding of the plan toolbar while no panel is beside it, and of the panel's top row always.- Panel closed: ⋯ sits immediately left of the toggle.
- Panel open or full screen: the expand / exit-full-screen button sits immediately left of it, in the panel's row.
- Panel beside the toolbar: the toolbar keeps its old 20px right padding against the panel's edge.
- The panel row's
.side-panel-bar-actionswrapper is gone, because only the full-screen button is left in it.
- Rapid clicks. Every press toggles at once, including mid-animation.
- The existing exit and arrival code already starts from where a motion in flight has reached (
useSidePanelDrawing/pane-motion.tsx). With a button that stays put and isn'tinert, each press now reaches it. - ⌥⌘B repeats behave the same way. The toggle offers that chord and the per-tab chords for as long as it is enabled.
- The existing exit and arrival code already starts from where a motion in flight has reached (
- Where it appears. It follows the left toggle's rule: it is drawn only where its pane can exist.
- It is on every plan page (
PLANS_PAGE.DOCUMENT). It is disabled while that plan is not drawn (reading, failed, missing), so it doesn't flicker out and back in while you switch between plans. - It is absent on the new-plan home and in Settings.
- It is on every plan page (
docs/PLANNING.md's header section now says where the toggle is.
Evidence
./scripts/check.shexits 0 locally.- macOS Electron verification (
./scripts/verify.sh): not run. This was built on a Linux cloud VM. CI builds nothing for the Mac and could not verify this UI change. LUKE-159 is the manual Mac pass.
Headless Chrome, real components and stylesheet
I mounted the real DesktopShell + useSidePanel + useSidebarCollapse with the built styles.css in headless Chrome (puppeteer-core, 1400×860) and drove it with real clicks. It checks the toggle's rect, that it is the same node, and that no toolbar or panel-row control or text intersects the toggle's rect. Then it fires bursts of 2, 3, 5, 6 and 7 clicks 20–110ms apart, so most land mid-slide.
| toggle rect | same node across states | overlaps | rapid-click bursts | |
|---|---|---|---|---|
| before | 1350,11 at rest, but rides the slide | no: new node on every open/close | – | all 5 wrong: clicks hit a detached node or the inert leaving panel (e.g. 2 clicks from closed ended open) |
| after | 1362,10 in every state, mid-burst included, and 962,10 after narrowing to 1000px | yes | none in any state | all 5 right (odd → open, even → closed), the panel at 1000–1400, nothing still animating |
With the panel closed, ⋯ ends at x=1354. With it open or full screen, Expand ends at x=1354. Either way that is 8px left of the toggle.
Panel closed. Before: the toggle is last on the toolbar. After: it stands in the title bar at the same place:


Panel open. Before: the toggle has moved into the panel's row. After: it hasn't moved, and the expand button sits left of it:


Full screen (after):

Mid-slide, 110ms into hiding. Before: the toolbar has already drawn a new toggle while the old one slides off with the panel. After: the one toggle stays put while the panel slides out beneath it:


Tests
All through public components:
pane-motion.test.ts(realDesktopShell, with a stand-in Web Animations engine):- "the panel's toggle is one button standing in the title bar through showing, full screen, hiding, and each motion between":
===the same DOM node at every step, in.title-bar-controls, inside neither the panel nor the toolbar. - "rapid presses on the toggle each land, mid-motion too …": bursts of 1–7 presses with no motion finishing in between. Each burst ends shown for an odd count and hidden for an even one, at 400px, not full screen, not leaving, with nothing left playing.
- "Option-Command-B held down repeats as presses do …": covers the chord, including from full screen.
- "the panel's toggle is one button standing in the title bar through showing, full screen, hiding, and each motion between":
desktop-shell.test.ts: "the panel's toggle stands in the title bar on a plan's page alone, disabled while the plan is not drawn". It is disabled with no chord while the plan is not drawn, and absent on the new-plan page and in Settings.desktop-plans.test.ts: the toolbar ends at the ⋯ menu, and the panel's row ends at its full-screen button and holds no toggle. The page under test standsSidePanelTogglebeside it, the way the shell does.- The first two
pane-motiontests and thedesktop-shelltest fail onmain. The chord test is coverage only: it passes onmaintoo. - Whether content overlaps the toggle is not testable in jsdom, which applies no stylesheet and does no layout. The headless-Chrome rect check above is the evidence for it.
🤖 Generated with Claude Code