Pixel editor: fix dropdown clipping when toolbar wraps near the sidebar

The pixel-art canvas-size dropdown (and the PNG-export dropdown that
shares the same class) used `position: absolute; right: 0` relative to
its toolbar wrap. The wrap lives inside `.pixelEditorBar`, which has
`flex-wrap: wrap`. When the bar wrapped and the trigger button landed
on the LEFT of its row, the menu's right-anchor pushed it 260px to the
LEFT — straight under the main sidebar.

Fix:
  - Switched .pixelExportMenu to `position: fixed` (only when open).
  - Added a `placeFlexMenu(menu, btn)` helper that anchors the menu's
    right edge to the button's right edge by default, flips to the
    button's left edge if anchoring right would push the menu past the
    sidebar (using the aside's actual right edge as the safe-left
    boundary, not just the viewport edge), and finally clamps to the
    viewport on both sides.
  - Added a fixed width (260px) + box-sizing border-box to keep the
    menu from expanding to its natural content width once it's freed
    from the previous absolute-positioning constraint.
  - Window-resize handler auto-closes any open menu — the fixed coords
    were computed against the previous layout, so resizing would
    otherwise strand the menu away from its button.

Verified via CDP across 920px / 1000px / 1100px / 1440px viewports and
in both sidebar-open and sidebar-collapsed states. Also exercised the
specific failure scenario (button moved to the leftmost slot of the
bar, simulating the worst-case wrap layout) — menu now opens to the
right of the button, fully inside the canvas area.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
This commit is contained in:
thePR0M3TH3ANandClaude Opus 4.7 committed 2026-05-28 09:44:29 -04:00
1 parent 733d123edc
commit 809e0f8fae
2 files changed
+64 -8

No files matched your search

+12 -6
View File
@@ -5,12 +5,8 @@ Running list of deferred work. Detailed specs for the larger items live in
## Adam's working list
- **Pixel canvas size dropdown — fix sidebar clipping.** When the
sidebar is open and the canvas size row wraps to a second line on
the left, the pixel-art canvas-size dropdown is clipped by the
sidebar. Easiest fix is probably "always right-align when wrapped"
so the dropdown opens away from the sidebar; or move the dropdown
to a placement that can't collide with the sidebar.
_(Empty — see "Shipped (recent)" below for the items that just
landed. Drop new items in here as they come up.)_
## QR code layer — remaining phases
@@ -88,6 +84,16 @@ Each has a full spec in `docs/`.
## Shipped (recent)
- **Pixel-art dropdowns — no more sidebar clipping.** The pixel-editor
resize-grid and PNG-export menus were `position: absolute; right: 0`
inside their toolbar wrap. When the toolbar wrapped and the trigger
button landed at the left of its row, the menu slid leftward under
the main sidebar. Switched both menus to `position: fixed` with a
JS-placed coord that anchors to the button's right edge by default,
flips to its left edge if anchoring right would clip past the
sidebar (sidebar's actual right edge, not just the viewport),
and clamps inside the viewport on both sides. Also auto-closes any
open menu on window resize to avoid stranded fixed-position remnants.
- **Top-right action strip — permanent, never wraps.** Pulled the
canvas-size readouts and the undo / redo pair out of the topbar's
flex flow and pinned them in a fixed strip directly under the Nostr
+52 -2
View File
@@ -889,14 +889,20 @@
/* PNG export dropdown in the pixel editor toolbar */
.pixelExportWrap { position: relative; }
/* The menu is `position: fixed` so we can clamp it inside the viewport
regardless of which row its button wrapped onto in the toolbar. When
the button lands at the left of a wrapped row, an absolute-positioned
menu anchored `right: 0` slides under the sidebar — fixed + JS
placement avoids that. The actual top/left are set by placeFlexMenu(). */
.pixelExportMenu {
position: absolute; right: 0; top: calc(100% + 6px); z-index: 20;
min-width: 260px; padding: 10px;
position: fixed; z-index: 20;
width: 260px; padding: 10px;
background: var(--panel, #fff); color: var(--ink);
border: 1px solid var(--line); border-radius: 8px;
box-shadow: 0 8px 24px rgba(15,23,42,.18);
flex-direction: column; gap: 8px;
display: none;
box-sizing: border-box;
}
.pixelExportMenu.open { display: flex; }
.pixelExportRow { display: flex; align-items: center; gap: 8px; }
@@ -9069,8 +9075,51 @@
const willOpen = forceOpen !== undefined ? forceOpen : !isOpen;
menu.classList.toggle('open', willOpen);
btn.setAttribute('aria-expanded', willOpen ? 'true' : 'false');
if (willOpen) placeFlexMenu(menu, btn);
}
// Position a `.pixelExportMenu` (fixed) below its trigger button,
// right-aligned to the button by default and clamped inside the
// viewport so it can't slide under the sidebar or off the right edge.
// The safe-left boundary is the sidebar's right edge when visible
// (not just the viewport edge) — otherwise a button that wrapped to
// the left of its toolbar row would let the menu slide under the
// sidebar.
function placeFlexMenu(menu, btn) {
if (!menu || !btn) return;
const PAD = 8;
const b = btn.getBoundingClientRect();
const menuW = menu.offsetWidth || 260;
const vw = window.innerWidth;
const aside = document.querySelector('aside');
const asideRect = aside ? aside.getBoundingClientRect() : null;
// Sidebar is "visible at the left" when it's not collapsed AND its
// right edge is inside the viewport (rules out the mobile 1-col layout
// where the aside stacks above the canvas).
const sidebarRight = (asideRect && asideRect.right > 0 && asideRect.left <= 0)
? asideRect.right
: 0;
const minLeft = Math.max(PAD, sidebarRight + PAD);
let left = b.right - menuW; // default: anchor right of button
if (left < minLeft) left = b.left; // would clip left → anchor left of button
if (left < minLeft) left = minLeft; // still clipped → clamp to sidebar edge
if (left + menuW > vw - PAD) left = vw - menuW - PAD;
menu.style.left = `${Math.round(left)}px`;
menu.style.top = `${Math.round(b.bottom + 6)}px`;
menu.style.right = 'auto';
}
// Close any open .pixelExportMenu on window resize — its `position: fixed`
// coords were computed against the previous layout, so on resize they'd
// strand the menu away from its button.
window.addEventListener('resize', () => {
document.querySelectorAll('.pixelExportMenu.open').forEach((menu) => {
menu.classList.remove('open');
const btn = menu.parentElement?.querySelector('[aria-haspopup="true"]');
if (btn) btn.setAttribute('aria-expanded', 'false');
});
});
// Draw a pixel-art layer onto an export canvas (ctx centred on the layer).
function drawPixelArtToCanvas(ctx, layer, w, h) {
try {
@@ -9640,6 +9689,7 @@
const wi = $('pixelResizeW'); if (wi) wi.value = _pixelEdit.w;
const hi = $('pixelResizeH'); if (hi) hi.value = _pixelEdit.h;
}
if (willOpen) placeFlexMenu(menu, btn);
}
function pixelFloodFill(x, y, idx) {