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)
Changes
@@@ -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 chatwith Vibe, OpenVibe's assistant, at `openvibe.chat/vibe`, with the text selected on the page and the page's addressfilled in (never sent by itself; OpenVibe.Chat#165). `_ask.askVibeHref` replaces `askActorHref`;@@@ -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; }@@@ -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 ──────────────────────────{nnew file mode 100644@@@ -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&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); });
@@ -6,6 +6,10 @@ A release is the git tag `vX.Y.Z`; consumers pin the tag's tarball (see README).
@@ -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
## 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
- **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
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`;
filled in (never sent by itself; OpenVibe.Chat#165). `_ask.askVibeHref` replaces `askActorHref`;
@@ -91,7 +91,7 @@
@@ -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(2) { opacity: 0; }
.openvibe-navbar .ovnav-burger[aria-expanded="true"] span:nth-child(3) { transform: translateY(-6px) rotate(-45deg); }
.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)); }
.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);
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; }
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; }
.ovnav-drawer.open { display: flex; animation: openvibe-slide-down .2s ease; }
@@ -13,7 +13,7 @@ const { brotli } = require('../scripts/size-report');
@@ -13,7 +13,7 @@ const { brotli } = require('../scripts/size-report');
const ROOT = path.join(__dirname, '..');
const ROOT = path.join(__dirname, '..');
const NAVBAR_PATH = path.join(ROOT, 'navbar.js');
const NAVBAR_PATH = path.join(ROOT, 'navbar.js');
const ICONS = require('../nav-icons');
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 ──────────────────────────
// ── size budget and generated block ──────────────────────────
{
{
new file mode 100644
new file mode 100644
@@ -0,0 +1,57 @@
@@ -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&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); });