openvibers/openvibe.shared Commits

Navbar: the phone menu is anchored to the bar, so it opens on screen even on a page wider than the phone (it opened off-screen: 'Missing menu') (#112)

OP OpenVibers committed 19 days ago · 832298607c · 4 files, +63 −2

Changes

ddiff --git a/CHANGELOG.md b/CHANGELOG.md
iindex 0d7cc0c..d4caadc 100644
---- a/CHANGELOG.md
++++ b/CHANGELOG.md
@@@ -6,6 +6,10 @@ A release is the git tag `vX.Y.Z`; consumers pin the tag's tarball (see README).
## 4.13.0 — unreleased
++- **The phone menu always opens on screen** (`navbar.js`). The burger's drawer is anchored to the bar (`position:
++ absolute`, right under it) instead of the viewport. On a page wider than the phone (openvibe.chat/messages with a long
++ pasted link was 939 px on a 412 px phone), the fixed drawer opened off-screen and the menu looked missing (the owner,
++ 2026-10-25). `test/navbar-drawer-overflow.test.js`. The navbar's brotli budget is 33.5 KB (was 33; 33.0 KB now).
- **Ask Vibe from every site** (`navbar.js`). The account menu's "Ask Actor" is now **Ask Vibe**: it opens a new chat
with Vibe, OpenVibe's assistant, at `openvibe.chat/vibe`, with the text selected on the page and the page's address
filled in (never sent by itself; OpenVibe.Chat#165). `_ask.askVibeHref` replaces `askActorHref`;
ddiff --git a/navbar.js b/navbar.js
iindex b2257a9..c64b73d 100644
---- a/navbar.js
++++ b/navbar.js
@@@ -91,7 +91,7 @@
.openvibe-navbar .ovnav-burger[aria-expanded="true"] span:nth-child(2) { opacity: 0; }
.openvibe-navbar .ovnav-burger[aria-expanded="true"] span:nth-child(3) { transform: translateY(-6px) rotate(-45deg); }
.openvibe-navbar .ovnav-burger:hover, .openvibe-navbar .ovnav-burger[aria-expanded="true"] { background: var(--accent-glow, rgba(59,130,246,.14)); }
-- .ovnav-drawer { display: none; position: fixed; top: calc(var(--ovnav-h, 56px) + 6px); right: 8px; width: min(300px, calc(100vw - 16px)); padding: 7px; border-radius: 16px; z-index: 1001; flex-direction: column; gap: 2px;
++ .ovnav-drawer { display: none; position: absolute; top: calc(100% + 6px); right: 8px; width: min(300px, calc(100vw - 16px)); padding: 7px; border-radius: 16px; z-index: 1001; flex-direction: column; gap: 2px;
background: var(--bg-elevated, var(--bg-secondary, #111826)); border: 1px solid var(--border, rgba(255,255,255,.12)); box-shadow: 0 22px 48px rgba(0,0,0,.45);
max-height: calc(100vh - var(--ovnav-h, 56px) - 16px); max-height: calc(100dvh - var(--ovnav-h, 56px) - 16px - env(safe-area-inset-bottom, 0px)); overflow-y: auto; overscroll-behavior: contain; }
.ovnav-drawer.open { display: flex; animation: openvibe-slide-down .2s ease; }
ddiff --git a/test/nav-icons.test.js b/test/nav-icons.test.js
iindex 6082600..6373fe6 100644
---- a/test/nav-icons.test.js
++++ b/test/nav-icons.test.js
@@@ -13,7 +13,7 @@ const { brotli } = require('../scripts/size-report');
const ROOT = path.join(__dirname, '..');
const NAVBAR_PATH = path.join(ROOT, 'navbar.js');
const ICONS = require('../nav-icons');
--const NAVBAR_BROTLI_BUDGET = 33 * 1024; // measured 27.6 KB on 2026-09-22 (39.0 KB with every glyph inline); 29.4 KB with the top-of-page fade (2.15.0); 30.6 KB with Ask Actor and its glyph (4.2.0); 31.4 KB with every site in groups in the launcher (4.4.0); 32.1 KB with the season's badge and switches (4.11.0)
++const NAVBAR_BROTLI_BUDGET = 33.5 * 1024; // measured 27.6 KB on 2026-09-22 (39.0 KB with every glyph inline); 29.4 KB with the top-of-page fade (2.15.0); 30.6 KB with Ask Actor and its glyph (4.2.0); 31.4 KB with every site in groups in the launcher (4.4.0); 32.1 KB with the season's badge and switches (4.11.0); 33.0 KB with Work together and the drawer anchored to the bar (4.13.0)
// ── size budget and generated block ──────────────────────────
{
ddiff --git a/test/navbar-drawer-overflow.test.js b/test/navbar-drawer-overflow.test.js
nnew file mode 100644
iindex 0000000..9210639
---- /dev/null
++++ b/test/navbar-drawer-overflow.test.js
@@@ -0,0 +1,57 @@
++'use strict';
++// The phone menu (the burger's drawer) opens in view even on a page that is wider than the phone. It is anchored to the
++// bar, not to the viewport: on openvibe.chat/messages a long pasted link made the page 939 px wide on a 412 px phone,
++// and the drawer, then position: fixed against that wider layout viewport, opened at x 631-931, off-screen (the owner,
++// 2026-10-25: "Missing menu"). Runs in a real headless Chrome with phone emulation (skipped without Chrome or with
++// OV_SKIP_BROWSER=1).
++const assert = require('assert');
++const fs = require('fs');
++const os = require('os');
++const path = require('path');
++const http = require('http');
++
++const ROOT = path.join(__dirname, '..');
++const h = require('../browser-harness');
++if (process.env.OV_SKIP_BROWSER === '1' || !h.findChrome()) {
++ console.log('navbar-drawer-overflow: skipped (no Chrome; set CHROME_BIN)');
++ process.exit(0);
++}
++
++const PAGE = `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Wide</title><link rel="icon" href="data:,">
++<script src="/shared/navbar.js" defer></script></head>
++<body style="margin:0"><div id="navbar-mount"></div><main><p id="wide" style="white-space:nowrap">https://www.example.com/a-very-long-link-that-nobody-wrapped-so-it-runs-far-past-the-right-edge-of-any-phone-screen?and=more&amp;still=more</p></main>
++<script>document.addEventListener('DOMContentLoaded', function () { OpenVibeNavbar.init({ service: 'site', user: { id: 7, username: 'probe', display_name: 'Probe' }, links: [{ label: 'Rooms', href: '/rooms' }, { label: 'Messages', href: '/messages' }, { label: 'Settings', href: '/settings' }], networkLinks: false, countViews: false, notifications: false, recent: false }); });</script>
++</body></html>`;
++const server = http.createServer((req, res) => {
++ const u = new URL(req.url, 'http://x');
++ if (u.pathname === '/') { res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' }); return res.end(PAGE); }
++ const m = /^\/shared\/([a-z0-9-]+\.js)$/.exec(u.pathname);
++ if (m && fs.existsSync(path.join(ROOT, m[1]))) { res.writeHead(200, { 'content-type': 'application/javascript' }); return res.end(fs.readFileSync(path.join(ROOT, m[1]))); }
++ res.writeHead(404); return res.end();
++});
++
++(async () => {
++ await new Promise((ok) => server.listen(0, '127.0.0.1', ok));
++ const base = `http://127.0.0.1:${server.address().port}`;
++ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-drawer-'));
++ const browser = await h.launch({ tmpDir });
++ try {
++ const context = await browser.newContext();
++ const p = await h.openPage(browser, context, { width: 412, height: 860, block: ['*openvibe.network*', '*openvibe.tools*', '*openvibe.media*', '*jsdelivr*'] });
++ await p.goto(`${base}/`, { maxMs: 5000 });
++ await p.evaluate(`new Promise((r) => { const t = setInterval(() => { if (document.getElementById('openvibe-burger')) { clearInterval(t); r(true); } }, 20); })`);
++ const wide = await p.evaluate('document.documentElement.scrollWidth');
++ assert.ok(wide > 600, `the page is wider than the phone (${wide} px)`);
++ await p.evaluate(`document.getElementById('openvibe-burger').click()`);
++ const box = JSON.parse(await p.evaluate(`new Promise((r) => setTimeout(() => { const d = document.getElementById('openvibe-drawer'); const b = d.getBoundingClientRect(); r(JSON.stringify({ open: d.classList.contains('open'), x: b.x, right: b.right, top: b.top, links: d.querySelectorAll('a').length })); }, 350))`));
++ assert.strictEqual(box.open, true, 'the drawer opened');
++ assert.ok(box.x >= 0 && box.right <= 412, `the drawer is on the phone's screen (x ${Math.round(box.x)} to ${Math.round(box.right)} of 412)`);
++ assert.ok(box.top > 40 && box.top < 80, `right under the bar (top ${Math.round(box.top)})`);
++ assert.ok(box.links >= 3, 'with the site\'s links');
++ console.log('navbar-drawer-overflow: ok');
++ } finally {
++ await browser.close();
++ server.close();
++ fs.rmSync(tmpDir, { recursive: true, force: true });
++ }
++})().catch((e) => { console.error(e); process.exit(1); });