fix: dropdown menus use fixed positioning to escape overflow clipping

Dropdown menus in app-switcher and space-switcher were clipped by
overflow:hidden on .rstack-header__left (mobile). Changed from
position:absolute to position:fixed with dynamic getBoundingClientRect
positioning. Bumped shell asset versions to v=5 to bypass CF cache.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-02-28 21:36:18 +00:00
parent 08928a9f8e
commit d9bd7557fa
3 changed files with 24 additions and 14 deletions

View File

@ -54,7 +54,7 @@ export function renderShell(opts: ShellOptions): string {
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🌌</text></svg>"> <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🌌</text></svg>">
<title>${escapeHtml(title)}</title> <title>${escapeHtml(title)}</title>
<link rel="stylesheet" href="/shell.css?v=4"> <link rel="stylesheet" href="/shell.css?v=5">
<style> <style>
/* When loaded inside an iframe (e.g. standalone domain redirecting back), /* When loaded inside an iframe (e.g. standalone domain redirecting back),
hide the shell chrome the parent rSpace page already provides it. */ hide the shell chrome the parent rSpace page already provides it. */
@ -94,7 +94,7 @@ export function renderShell(opts: ShellOptions): string {
${renderWelcomeOverlay()} ${renderWelcomeOverlay()}
<script type="module"> <script type="module">
import '/shell.js?v=4'; import '/shell.js?v=5';
// Provide module list to app switcher // Provide module list to app switcher
document.querySelector('rstack-app-switcher')?.setModules(${moduleListJSON}); document.querySelector('rstack-app-switcher')?.setModules(${moduleListJSON});
@ -355,7 +355,7 @@ export function renderExternalAppShell(opts: ExternalAppShellOptions): string {
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🌌</text></svg>"> <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🌌</text></svg>">
<title>${escapeHtml(title)}</title> <title>${escapeHtml(title)}</title>
<link rel="stylesheet" href="/shell.css?v=4"> <link rel="stylesheet" href="/shell.css?v=5">
<style> <style>
html.rspace-embedded .rstack-header { display: none !important; } html.rspace-embedded .rstack-header { display: none !important; }
html.rspace-embedded .rstack-tab-row { display: none !important; } html.rspace-embedded .rstack-tab-row { display: none !important; }
@ -398,7 +398,7 @@ export function renderExternalAppShell(opts: ExternalAppShellOptions): string {
</div> </div>
<script type="module"> <script type="module">
import '/shell.js?v=4'; import '/shell.js?v=5';
document.querySelector('rstack-app-switcher')?.setModules(${moduleListJSON}); document.querySelector('rstack-app-switcher')?.setModules(${moduleListJSON});
const tabBar = document.querySelector('rstack-tab-bar'); const tabBar = document.querySelector('rstack-tab-bar');
@ -581,7 +581,7 @@ export function renderModuleLanding(opts: ModuleLandingOptions): string {
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>${mod.icon}</text></svg>"> <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>${mod.icon}</text></svg>">
<title>${escapeHtml(mod.name)} rSpace</title> <title>${escapeHtml(mod.name)} rSpace</title>
<link rel="stylesheet" href="/shell.css?v=4"> <link rel="stylesheet" href="/shell.css?v=5">
${cssBlock} ${cssBlock}
<script defer src="https://rdata.online/collect.js" data-website-id="6ee7917b-0ed7-44cb-a4c8-91037638526b"></script> <script defer src="https://rdata.online/collect.js" data-website-id="6ee7917b-0ed7-44cb-a4c8-91037638526b"></script>
</head> </head>
@ -601,7 +601,7 @@ export function renderModuleLanding(opts: ModuleLandingOptions): string {
</header> </header>
${bodyContent} ${bodyContent}
<script type="module"> <script type="module">
import '/shell.js?v=4'; import '/shell.js?v=5';
document.querySelector('rstack-app-switcher')?.setModules(${moduleListJSON}); document.querySelector('rstack-app-switcher')?.setModules(${moduleListJSON});
try { try {
var raw = localStorage.getItem('encryptid_session'); var raw = localStorage.getItem('encryptid_session');

View File

@ -241,7 +241,12 @@ export class RStackAppSwitcher extends HTMLElement {
trigger.addEventListener("click", (e) => { trigger.addEventListener("click", (e) => {
e.stopPropagation(); e.stopPropagation();
menu.classList.toggle("open"); const isOpen = menu.classList.toggle("open");
if (isOpen) {
const rect = trigger.getBoundingClientRect();
menu.style.top = `${rect.bottom + 6}px`;
menu.style.left = `${Math.max(4, rect.left)}px`;
}
}); });
// Prevent external links from closing the menu prematurely // Prevent external links from closing the menu prematurely
@ -298,10 +303,10 @@ const STYLES = `
.caret { font-size: 0.7em; opacity: 0.6; } .caret { font-size: 0.7em; opacity: 0.6; }
.menu { .menu {
position: absolute; top: 100%; left: 0; margin-top: 6px; position: fixed; margin-top: 0;
min-width: 300px; border-radius: 12px; overflow: hidden; min-width: 300px; border-radius: 12px; overflow: hidden;
overflow-y: auto; max-height: 70vh; overflow-y: auto; max-height: 70vh;
box-shadow: 0 8px 30px rgba(0,0,0,0.25); display: none; z-index: 200; box-shadow: 0 8px 30px rgba(0,0,0,0.25); display: none; z-index: 10001;
} }
.menu.open { display: block; } .menu.open { display: block; }
:host-context([data-theme="light"]) .menu { :host-context([data-theme="light"]) .menu {

View File

@ -100,9 +100,14 @@ export class RStackSpaceSwitcher extends HTMLElement {
trigger.addEventListener("click", async (e) => { trigger.addEventListener("click", async (e) => {
e.stopPropagation(); e.stopPropagation();
const isOpen = menu.classList.toggle("open"); const isOpen = menu.classList.toggle("open");
if (isOpen && !this.#loaded) { if (isOpen) {
await this.#loadSpaces(); const rect = trigger.getBoundingClientRect();
this.#renderMenu(menu, current); menu.style.top = `${rect.bottom + 6}px`;
menu.style.left = `${Math.max(4, rect.left)}px`;
if (!this.#loaded) {
await this.#loadSpaces();
this.#renderMenu(menu, current);
}
} }
}); });
@ -689,10 +694,10 @@ const STYLES = `
.caret { font-size: 0.7em; opacity: 0.6; } .caret { font-size: 0.7em; opacity: 0.6; }
.menu { .menu {
position: absolute; top: 100%; left: 0; margin-top: 6px; position: fixed; margin-top: 0;
min-width: 260px; max-height: 400px; overflow-y: auto; min-width: 260px; max-height: 400px; overflow-y: auto;
border-radius: 12px; box-shadow: 0 8px 30px rgba(0,0,0,0.25); border-radius: 12px; box-shadow: 0 8px 30px rgba(0,0,0,0.25);
display: none; z-index: 200; display: none; z-index: 10001;
} }
.menu.open { display: block; } .menu.open { display: block; }
:host-context([data-theme="light"]) .menu { background: white; border: 1px solid rgba(0,0,0,0.1); } :host-context([data-theme="light"]) .menu { background: white; border: 1px solid rgba(0,0,0,0.1); }