From ed32c212d3f3bd5dffe5f50f831c4347684367d8 Mon Sep 17 00:00:00 2001 From: jelveh Date: Thu, 1 Oct 2026 22:17:18 -0700 Subject: [PATCH] Keep toolbar buttons in place when a menu opens near the viewport edge On phones the expanded frame was clamped to an 8px viewport margin, which moved the whole toolbar row (by the header padding minus 8px) under the user's finger. The frame now shrinks to fit beside and below the collapsed toolbar, and only falls back to shifting when there isn't enough room. --- src/gui/src/helpers/toolbarEmbed.js | 34 +++++++++++++++++++----- src/gui/src/helpers/toolbarEmbed.test.js | 15 +++++++++++ 2 files changed, 42 insertions(+), 7 deletions(-) diff --git a/src/gui/src/helpers/toolbarEmbed.js b/src/gui/src/helpers/toolbarEmbed.js index 7a7ccf816..a3a35ba47 100644 --- a/src/gui/src/helpers/toolbarEmbed.js +++ b/src/gui/src/helpers/toolbarEmbed.js @@ -22,12 +22,32 @@ export function shouldShowUpgrade (user) { return !user.subscription?.active && !user.team?.uid; } +const MIN_PANEL_WIDTH = 280; +const MIN_PANEL_HEIGHT = 240; + +/** + * Places the expanded toolbar frame inside the viewport. It grows left and down + * from the collapsed frame and shrinks rather than shifts when there is room, so + * the toolbar buttons stay under the pointer. + */ export function toolbarBounds (anchor, viewport, panel, requestedHeight) { - const width = Math.min(panel === 'apps' ? 440 : 360, viewport.width - 16); - const height = Math.min(panel === 'apps' ? 620 : (Number.isFinite(requestedHeight) ? Math.max(180, Math.min(requestedHeight, 800)) : 560), viewport.height - 16); - return { - width: Math.max(0, width), height: Math.max(0, height), - left: Math.max(8, Math.min(anchor.right - width, viewport.width - width - 8)), - top: Math.max(8, Math.min(anchor.top, viewport.height - height - 8)), - }; + let width = Math.max(0, Math.min(panel === 'apps' ? 440 : 360, viewport.width - 16)); + let height = Math.max(0, Math.min(panel === 'apps' ? 620 : (Number.isFinite(requestedHeight) ? Math.max(180, Math.min(requestedHeight, 800)) : 560), viewport.height - 16)); + let left; + let top; + const roomLeft = anchor.right - 8; + if ( anchor.right <= viewport.width - 8 && roomLeft >= Math.min(width, MIN_PANEL_WIDTH) ) { + width = Math.min(width, roomLeft); + left = anchor.right - width; + } else { + left = Math.max(8, Math.min(anchor.right - width, viewport.width - width - 8)); + } + const roomBelow = viewport.height - anchor.top - 8; + if ( anchor.top >= 8 && roomBelow >= Math.min(height, MIN_PANEL_HEIGHT) ) { + height = Math.min(height, roomBelow); + top = anchor.top; + } else { + top = Math.max(8, Math.min(anchor.top, viewport.height - height - 8)); + } + return { width, height, left, top }; } diff --git a/src/gui/src/helpers/toolbarEmbed.test.js b/src/gui/src/helpers/toolbarEmbed.test.js index 742232174..237d575d0 100644 --- a/src/gui/src/helpers/toolbarEmbed.test.js +++ b/src/gui/src/helpers/toolbarEmbed.test.js @@ -34,6 +34,21 @@ describe('toolbar integration', () => { } } }); + it('keeps the toolbar buttons in place when the menu fits beside them', () => { + for ( const [viewport, anchor] of [ + [{ width: 390, height: 700 }, { right: 374, top: 8 }], + [{ width: 320, height: 560 }, { right: 304, top: 12 }], + [{ width: 1100, height: 600 }, { right: 1084, top: 12 }], + ] ) { + for ( const panel of ['apps', 'account'] ) { + const box = toolbarBounds(anchor, viewport, panel); + expect(box.left + box.width).toBe(anchor.right); + expect(box.top).toBe(anchor.top); + expect(box.left).toBeGreaterThanOrEqual(8); + expect(box.top + box.height).toBeLessThanOrEqual(viewport.height - 8); + } + } + }); it('opens an upgrade deep link once the billing UI is ready', () => { let opens = 0; const target = { location: { href: 'https://puter.com/dashboard?upgrade=1#usage' } };