fix(modal): keep body-portaled dropdowns above their tool modal at any stack depth (#4720) (#4724)

* fix(memory): keep the Brain memory item menu above the modal at any stack depth

The memory item "⋮" dropdown is portaled to <body> with a hardcoded
z-index of 10001. Tool modals, however, get a monotonically increasing
z-index from modalManager's bring-to-front counter (_modalTopZ), which
climbs unbounded as modals are opened/restored over a session. Once that
counter passes 10001, the Brain modal stacks above the body-portaled
dropdown, so the menu renders behind the panel — visible only where it
spills past the modal's edge (#4720).

Derive the dropdown's z-index from the owning modal's current z-index
(+1), keeping 10001 as a floor for the common low-counter case, so the
menu always sits just above its modal however high the counter has climbed.

Verified with document.elementFromPoint at the dropdown's location: with a
high modal z-index the old build returns the modal at every sampled point
(menu behind); the fixed build returns the dropdown (menu on top). The
default low-counter case is unchanged (z stays 10001).

* refactor(modal): route body-portaled dropdowns through a shared topPortalZ() helper

The hardcoded z-index:10001 the Brain memory menu used (#4720) is the same
literal shared by ~16 body-portaled dropdowns across calendar, cookbook,
cookbookServe, documentLibrary, emailLibrary, gallery, notes, emojiPicker and
memory — each renders behind its owning tool modal once modalManager's
bring-to-front counter climbs past the literal over a long session.

Promote the per-dropdown fix into a single topPortalZ() helper in
toolWindowZOrder.js — the existing source of truth for tool-window z, already
imported by modalManager's _bringToFront and notes.js — returning
max(topToolWindowZ(), dock-chip floor) + 1, so a portaled dropdown always sits
just above the live tool-window stack however high the counter has climbed.
Route all 16 sites through it. The slashCommands tour tooltips and the
cookbookServe VRAM dialog are intentionally left out (neither is a modal-owned
portaled dropdown).

Add tests/test_portal_dropdown_z_js.py covering the helper, including the #4720
scenario (modal counter at 99999 -> dropdown at 100000). Existing
test_notes_z_order_js.py stays green.
This commit is contained in:
Max Hsu
2026-06-23 16:24:31 +08:00
committed by GitHub
parent 7e5db9a3c6
commit fef08ed114
11 changed files with 137 additions and 16 deletions
+3 -2
View File
@@ -4,6 +4,7 @@
* Extracted from document.js to reduce file size.
*/
import { topPortalZ } from './toolWindowZOrder.js';
import uiModule from './ui.js';
import sessionModule from './sessions.js';
import spinnerModule from './spinner.js';
@@ -227,7 +228,7 @@ let _libraryArchivedView = false; // Documents tab showing archived docs?
dd.style.right = (window.innerWidth - rect.right) + 'px';
dd.style.top = (rect.bottom + 2) + 'px';
dd.style.display = 'block';
dd.style.zIndex = '100000';
dd.style.zIndex = String(topPortalZ());
requestAnimationFrame(() => {
const mr = dd.getBoundingClientRect();
if (mr.bottom > window.innerHeight - 8) {
@@ -629,7 +630,7 @@ let _libraryArchivedView = false; // Documents tab showing archived docs?
const rect = menuBtn.getBoundingClientRect();
document.body.appendChild(dropdown);
dropdown.dataset.owner = doc.id;
dropdown.style.cssText = 'position:fixed;z-index:10000;min-width:0;width:max-content;padding:4px;background:var(--panel);border:1px solid var(--border);border-radius:8px;box-shadow:0 8px 24px rgba(0,0,0,0.3);backdrop-filter:blur(12px);font-size:12px;display:block;';
dropdown.style.cssText = `position:fixed;z-index:${topPortalZ()};min-width:0;width:max-content;padding:4px;background:var(--panel);border:1px solid var(--border);border-radius:8px;box-shadow:0 8px 24px rgba(0,0,0,0.3);backdrop-filter:blur(12px);font-size:12px;display:block;`;
dropdown.style.top = (rect.bottom + 4) + 'px';
dropdown.style.left = 'auto';
dropdown.style.right = (window.innerWidth - rect.right) + 'px';