fix(sidebar,about): smart positioning for playlist menus + platform label

Two bugs surfaced during 1.2.5 QA:

* The per-playlist right-click context menu opened at the cursor and
  could fall below the bottom of the window when invoked on items near
  the last of the sidebar. It now measures its own rect and flips above
  the cursor if there's no room below, or clamps to the bottom padding
  as a last resort. Same pattern already used for the main playlists
  dropdown menu.
* The Sort submenu inside the sidebar's general playlists menu was
  being clipped by its parent .dropdown-menu because the submenu lived
  inside that menu's DOM subtree. Teleport it via a local `use:portal`
  action so absolute positioning escapes any ancestor `overflow: hidden`.

Also fix a duplicated "License" label in the About dialog's Build Info
block: the second row meant to show the platform was using the license
key. Swap to about.buildInfo.platform (the key already existed in both
locales).

cc
This commit is contained in:
vicrodh committed 2026-04-15 13:16:07 -06:00
1 parent 7d1e5604da
commit 372357497b
2 files changed
+48 -3

No files matched your search

+2 -2
View File
@@ -99,10 +99,10 @@
<span class="label">{$t('about.buildInfo.version')}</span>
<span class="value">{appVersion}</span>
<span class="label">{$t('about.buildInfo.codename')}</span>
<span class="value codename">Hardening playback</span>
<span class="value codename">Accolade Watch</span>
<span class="label">{$t('about.buildInfo.license')}</span>
<span class="value">MIT</span>
<span class="label">{$t('about.buildInfo.license')}</span>
<span class="label">{$t('about.buildInfo.platform')}</span>
<span class="value">{platformLabel}</span>
<span class="label">{$t('about.buildInfo.build')}</span>
<span class="value">{BUILD_DATE} {#if BUILD_COMMIT}<span class="commit">({BUILD_COMMIT})</span>{/if}</span>
+46 -1
View File
@@ -188,6 +188,30 @@
currentFolderId: null
});
let contextMenuSearch = $state('');
// Bound to the rendered context menu element so the positioning helper
// can measure it after mount and flip/clamp to keep it inside the
// viewport (bug: playlist context menu could fall below the window).
let contextMenuEl = $state<HTMLDivElement | null>(null);
let contextMenuStyle = $state('');
async function setContextMenuPosition() {
await tick();
if (!contextMenuEl) return;
const menuRect = contextMenuEl.getBoundingClientRect();
const pad = 8;
let x = contextMenu.x;
let y = contextMenu.y;
if (x + menuRect.width > window.innerWidth - pad) {
x = Math.max(pad, window.innerWidth - menuRect.width - pad);
}
if (y + menuRect.height > window.innerHeight - pad) {
// Prefer flipping the menu upward from the cursor; if still spilling
// off the top, clamp to the bottom edge of the viewport.
const flipped = y - menuRect.height;
y = flipped >= pad ? flipped : Math.max(pad, window.innerHeight - menuRect.height - pad);
}
contextMenuStyle = `left: ${x}px; top: ${y}px;`;
}
const FOLDER_SEARCH_THRESHOLD = 8;
let draggedPlaylistId = $state<number | null>(null);
let draggedFromFolderId = $state<string | null>(null);
@@ -558,6 +582,21 @@
menuStyle = `left: ${left}px; top: ${top}px;`;
}
/**
* Svelte action: teleport the node into document.body on mount so it
* escapes any ancestor with `overflow: hidden`. Used by the Sort
* submenu which lives inside the dropdown-menu and was being clipped
* against the dropdown's right edge.
*/
function portal(node: HTMLElement) {
document.body.appendChild(node);
return {
destroy() {
if (node.parentNode) node.parentNode.removeChild(node);
},
};
}
async function setSubmenuPosition() {
await tick();
if (!sortTriggerRef || !submenuEl) return;
@@ -1231,6 +1270,8 @@
folder: null,
currentFolderId
};
contextMenuStyle = `left: ${e.clientX}px; top: ${e.clientY}px;`;
void setContextMenuPosition();
}
function handleFolderContextMenu(e: MouseEvent, folder: PlaylistFolder) {
@@ -1246,6 +1287,8 @@
folder,
currentFolderId: folder.id
};
contextMenuStyle = `left: ${e.clientX}px; top: ${e.clientY}px;`;
void setContextMenuPosition();
}
function closeContextMenu() {
@@ -1698,6 +1741,7 @@
tabindex="-1"
onmouseenter={openSubmenu}
onmouseleave={closeSubmenuDelayed}
use:portal
>
<button class="menu-item" class:selected={sortOption === 'name'} onclick={() => handleSortChange('name')}>
{$t('sort.nameAZ')}
@@ -1955,7 +1999,8 @@
<div
class="context-menu"
class:has-search={showSearch}
style="left: {contextMenu.x}px; top: {contextMenu.y}px;"
bind:this={contextMenuEl}
style={contextMenuStyle}
onclick={(e) => e.stopPropagation()}
onmouseenter={() => isHoveringContextMenu = true}
onmouseleave={() => isHoveringContextMenu = false}