openvibers/openvibe.live Commits
Fix hamburger: the desktop-dropdown outside-click no longer closes the mobile drawer; chat button steps aside while a panel is open (no dead space at the end of the user menu)
Changes
@@@ -11864,12 +11864,14 @@ a .arena-thread:hover { color: var(--text-primary); background: color-mix(in srg/* Below tablet width the user menu is a sheet pinned under the bar, as wide as the phone allows. */@media (max-width: 900px) {.user-dropdown { position: fixed; top: calc(var(--navbar-h) + 6px); right: 8px; left: auto; min-width: 0; width: min(340px, calc(100vw - 16px)); }-- /* The floating chat button sits over the bottom-right corner: let the last row scroll clear of it. */-- .user-dropdown > :last-child { padding-bottom: calc(10px + env(safe-area-inset-bottom, 0px)); }-- .user-dropdown::after { content: ''; display: block; height: 64px; }++ /* The floating chat button steps aside while a navbar panel is open (body.ov-panel-open), so the menu++ needs no dead space at its end to clear it. */++ .user-dropdown > :last-child { padding-bottom: calc(6px + env(safe-area-inset-bottom, 0px)); }}/* Short screens (landscape phones, split view): tighter rows so more of the menu is visible at once. */@media (max-height: 560px) {.user-dropdown .ud-group a, .nav-links.show .nav-link { padding-top: 7px; padding-bottom: 7px; font-size: 0.86rem; }.user-dropdown .ud-label { padding-top: 8px; padding-bottom: 2px; }}++++body.ov-panel-open #floating-chat-fab { opacity: 0; pointer-events: none; transform: scale(.85); transition: opacity .15s, transform .15s; }@@@ -1426,7 +1426,6 @@ function toggleNavDropdown(id) {function closeNavDropdowns() {document.querySelectorAll('.nav-dropdown.open').forEach(d => d.classList.remove('open'));-- closeMobileNav();}// Close nav dropdowns when clicking outside@@@ -1476,10 +1475,20 @@ function positionNavDropdownMenu(dropdown) {menu.style.maxHeight = `${Math.max(120, vh - rect.bottom - pad)}px`;}++// While the user menu or the mobile drawer is open, the floating chat button steps aside (it used to sit on++// top of the menu's last rows). Driven by the panels' own classes, so every open/close path is covered.++{++ const sync = () => document.body.classList.toggle('ov-panel-open',++ !!document.querySelector('#user-dropdown.show, .nav-links.show'));++ const watch = () => { for (const el of [document.getElementById('user-dropdown'), document.querySelector('.nav-links')]) if (el) new MutationObserver(sync).observe(el, { attributes: true, attributeFilter: ['class'] }); };++ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', watch); else watch();++}++/** Close every navbar panel (used when the viewport changes under an open menu). */function closeNavPanels() {document.getElementById('user-dropdown')?.classList.remove('show');-- document.querySelectorAll('.nav-dropdown.open').forEach(d => d.classList.remove('open'));++ closeNavDropdowns();++ closeMobileNav();}// A rotated phone, a resized window or an opened keyboard invalidates a fixed menu's position: reposition// what is open, and close panels on orientation change. Escape closes them too.nnew file mode 100644@@@ -0,0 +1,11 @@++'use strict';++// Regression: a desktop-dropdown "click outside" handler must never close the mobile drawer, or the hamburger++// opens and closes it in the same click (shipped once, 2026-09-18).++const assert = require('assert');++const fs = require('fs'); const path = require('path');++const src = fs.readFileSync(path.join(__dirname, '..', 'public', 'js', 'app.js'), 'utf8');++const body = (name) => { const i = src.indexOf(`function ${name}(`); assert.ok(i >= 0, `${name} exists`); let d = 0, j = src.indexOf('{', i); const s = j; for (; j < src.length; j++) { if (src[j] === '{') d++; else if (src[j] === '}' && --d === 0) break; } return src.slice(s, j + 1); };++assert.ok(!/closeMobileNav\s*\(/.test(body('closeNavDropdowns')), 'closeNavDropdowns leaves the mobile drawer alone');++assert.ok(/closeMobileNav\s*\(/.test(body('closeNavPanels')) && /closeNavDropdowns\s*\(/.test(body('closeNavPanels')), 'closeNavPanels closes everything');++assert.ok(/classList\.toggle\('show'\)/.test(body('toggleMobileNav')), 'hamburger toggles the drawer');++console.log('nav panels: all checks passed');
@@ -11864,12 +11864,14 @@ a .arena-thread:hover { color: var(--text-primary); background: color-mix(in srg
@@ -11864,12 +11864,14 @@ a .arena-thread:hover { color: var(--text-primary); background: color-mix(in srg
/* Below tablet width the user menu is a sheet pinned under the bar, as wide as the phone allows. */
/* Below tablet width the user menu is a sheet pinned under the bar, as wide as the phone allows. */
@media (max-width: 900px) {
@media (max-width: 900px) {
.user-dropdown { position: fixed; top: calc(var(--navbar-h) + 6px); right: 8px; left: auto; min-width: 0; width: min(340px, calc(100vw - 16px)); }
.user-dropdown { position: fixed; top: calc(var(--navbar-h) + 6px); right: 8px; left: auto; min-width: 0; width: min(340px, calc(100vw - 16px)); }
- /* The floating chat button sits over the bottom-right corner: let the last row scroll clear of it. */
- .user-dropdown > :last-child { padding-bottom: calc(10px + env(safe-area-inset-bottom, 0px)); }
- .user-dropdown::after { content: ''; display: block; height: 64px; }
+ /* The floating chat button steps aside while a navbar panel is open (body.ov-panel-open), so the menu
+ needs no dead space at its end to clear it. */
+ .user-dropdown > :last-child { padding-bottom: calc(6px + env(safe-area-inset-bottom, 0px)); }
}
}
/* Short screens (landscape phones, split view): tighter rows so more of the menu is visible at once. */
/* Short screens (landscape phones, split view): tighter rows so more of the menu is visible at once. */
@media (max-height: 560px) {
@media (max-height: 560px) {
.user-dropdown .ud-group a, .nav-links.show .nav-link { padding-top: 7px; padding-bottom: 7px; font-size: 0.86rem; }
.user-dropdown .ud-group a, .nav-links.show .nav-link { padding-top: 7px; padding-bottom: 7px; font-size: 0.86rem; }
.user-dropdown .ud-label { padding-top: 8px; padding-bottom: 2px; }
.user-dropdown .ud-label { padding-top: 8px; padding-bottom: 2px; }
}
}
+
+body.ov-panel-open #floating-chat-fab { opacity: 0; pointer-events: none; transform: scale(.85); transition: opacity .15s, transform .15s; }
@@ -1426,7 +1426,6 @@ function toggleNavDropdown(id) {
@@ -1426,7 +1426,6 @@ function toggleNavDropdown(id) {
function closeNavDropdowns() {
function closeNavDropdowns() {
document.querySelectorAll('.nav-dropdown.open').forEach(d => d.classList.remove('open'));
document.querySelectorAll('.nav-dropdown.open').forEach(d => d.classList.remove('open'));
- closeMobileNav();
}
}
// Close nav dropdowns when clicking outside
// Close nav dropdowns when clicking outside
@@ -1476,10 +1475,20 @@ function positionNavDropdownMenu(dropdown) {
@@ -1476,10 +1475,20 @@ function positionNavDropdownMenu(dropdown) {
menu.style.maxHeight = `${Math.max(120, vh - rect.bottom - pad)}px`;
menu.style.maxHeight = `${Math.max(120, vh - rect.bottom - pad)}px`;
}
}
+// While the user menu or the mobile drawer is open, the floating chat button steps aside (it used to sit on
+// top of the menu's last rows). Driven by the panels' own classes, so every open/close path is covered.
+{
+ const sync = () => document.body.classList.toggle('ov-panel-open',
+ !!document.querySelector('#user-dropdown.show, .nav-links.show'));
+ const watch = () => { for (const el of [document.getElementById('user-dropdown'), document.querySelector('.nav-links')]) if (el) new MutationObserver(sync).observe(el, { attributes: true, attributeFilter: ['class'] }); };
+ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', watch); else watch();
+}
+
/** Close every navbar panel (used when the viewport changes under an open menu). */
/** Close every navbar panel (used when the viewport changes under an open menu). */
function closeNavPanels() {
function closeNavPanels() {
document.getElementById('user-dropdown')?.classList.remove('show');
document.getElementById('user-dropdown')?.classList.remove('show');
- document.querySelectorAll('.nav-dropdown.open').forEach(d => d.classList.remove('open'));
+ closeNavDropdowns();
+ closeMobileNav();
}
}
// A rotated phone, a resized window or an opened keyboard invalidates a fixed menu's position: reposition
// A rotated phone, a resized window or an opened keyboard invalidates a fixed menu's position: reposition
// what is open, and close panels on orientation change. Escape closes them too.
// what is open, and close panels on orientation change. Escape closes them too.
new file mode 100644
new file mode 100644
@@ -0,0 +1,11 @@
@@ -0,0 +1,11 @@
+'use strict';
+// Regression: a desktop-dropdown "click outside" handler must never close the mobile drawer, or the hamburger
+// opens and closes it in the same click (shipped once, 2026-09-18).
+const assert = require('assert');
+const fs = require('fs'); const path = require('path');
+const src = fs.readFileSync(path.join(__dirname, '..', 'public', 'js', 'app.js'), 'utf8');
+const body = (name) => { const i = src.indexOf(`function ${name}(`); assert.ok(i >= 0, `${name} exists`); let d = 0, j = src.indexOf('{', i); const s = j; for (; j < src.length; j++) { if (src[j] === '{') d++; else if (src[j] === '}' && --d === 0) break; } return src.slice(s, j + 1); };
+assert.ok(!/closeMobileNav\s*\(/.test(body('closeNavDropdowns')), 'closeNavDropdowns leaves the mobile drawer alone');
+assert.ok(/closeMobileNav\s*\(/.test(body('closeNavPanels')) && /closeNavDropdowns\s*\(/.test(body('closeNavPanels')), 'closeNavPanels closes everything');
+assert.ok(/classList\.toggle\('show'\)/.test(body('toggleMobileNav')), 'hamburger toggles the drawer');
+console.log('nav panels: all checks passed');