openvibers/openvibe.shared Commits

4.148.0: the phone's menu button is the orb, and the bar's controls are one size (#338)

OP OpenVibers committed 5 days ago · 624944a985 · 8 files, +70 −14

* 4.148.0: on a phone the menu button is the orb, in the burger's place

The owner: "replace the hamburger with that button… we don't need both". The phone's menu button, at the bar's
right where the burger was, now draws the orb (the nine dots, the first-visit ripple, the wave while the menu loads,
the glow and the turn into a cross while it is open) instead of three lines. The orb beside the site's name still
steps aside at 860 px and below, so there is one button; a wide bar is unchanged.

* 4.148.0: one size for the bar's controls

The owner: the orb should match the Sign In button, and the bar's things should be one size. Sign In, the orb,
messages, the picture, the balances and a featured link are 36 px tall (34 under 400 px); the orb draws its dots at
26 px (24), and Sign In's corners follow the orb's.

* 4.148.0: the phone's menu button keeps the bar's edge

The orb's tap area reaches 12 px past the button, so on a 390 px phone the bar ran 6 px past the screen and the page zoomed out to fit it (the Dock's pill sat 10 px from the edge instead of 16). The button is now as wide as the burger it replaces and its tap area stops inside the bar. The navbar.js brotli budget moves to 45 KB (44.1 KB measured).

# Conflicts:
#	CHANGELOG.md

* 4.148.0: the navbar.js budget note, measured

The bar's controls grew with the orb (44.3 KB brotli); the budget is 45 KB and the note says what was measured.

* 4.148.0: the orb's tap area stays on the screen at the bar's right

The brand orb's tap area reached 12 px to its right; on the menu button at the right edge that made the page 6 px
wider than a 390 px phone (dock-chrome's pill check caught it). It grows to the left there. navbar-phone-fit-chrome
now checks the page is never wider than the screen. navbar.js brotli budget 44 to 44.5 KB (44.2 measured).

* Merge remote-tracking branch 'origin/navbar/orb-on-phones' into navbar/orb-on-phones

# Conflicts:
#	test/nav-icons.test.js

Changes

ddiff --git a/CHANGELOG.md b/CHANGELOG.md
iindex d1cb41f..4810af4 100644
---- a/CHANGELOG.md
++++ b/CHANGELOG.md
@@@ -1,5 +1,26 @@
# Changelog
++## 4.148.0 — 2026-11-08
++
++- **On a phone the menu button is the orb, in the burger's place** (the owner, 11-08: "replace the hamburger with
++ that button… we don't need both"). 4.137.0 kept one menu button on a phone but drew it as a plain three-line burger
++ and hid the orb, the network's own nine dots with their ripple, wave and turn. The phone's menu button, at the bar's
++ right where the burger was, now draws the orb: the same dots, the first-visit ripple, the wave while the menu loads,
++ the glow on a press and the turn into a ✕ while the menu is open. Still one button: the orb beside the site's name
++ steps aside at 860 px and below, as before; a wide bar is unchanged. The button opens the menu on this site's pages
++ first, and the menu's round ✕ (4.142.0) closes it.
++- **One size for the bar's controls** (the owner: "it should be a little bigger so it matches the sign in button…
++ the things need to be more consistent in the nav bar in size"). Sign In, the orb, messages, the picture, the
++ balances and a featured link are all 36 px tall (34 px on a phone under 400 px); before, Sign In was 33, the orb 34
++ (28 by 32 on a phone, its dots drawn at 22 px), messages 32 to 36, the picture 32 and the balances 30 to 32. The
++ orb draws its dots at 26 px (24 on a phone), and Sign In's corners follow the orb's (10 px).
++ Tests: navbar-phone-fit-chrome checks, 320 to 430 px, that the one menu button draws the orb's nine dots and no
++ burger lines and that nothing in the bar overlaps; navbar-brand checks the markup.
++- **The phone's menu button keeps the bar's edge.** The orb's tap area reaches 12 px past the button, which pushed the
++ bar 6 px past a 390 px phone: the page zoomed out to fit it and the Dock's pill sat 10 px from the edge instead of
++ 16. The button's tap area now stops inside the bar.
++- Budgets: navbar.js 45 KB brotli (44.3 with the phone's menu button drawn as the orb).
++
## 4.147.0 — 2026-11-08
- **A link in a Dock message shows its card.** OpenVibe.Chat now resolves a known OpenVibe link when a message is sent
ddiff --git a/README.md b/README.md
iindex bef2d22..43bcdc3 100644
---- a/README.md
++++ b/README.md
@@@ -2,7 +2,7 @@
> Versioned UI, the OpenVibe Frame (the navbar, footer and "shipped" views every site sits in), SEO, legal and release-client packages every OpenVibe site renders.
--**Status:** alpha, v4.147.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed
++**Status:** alpha, v4.148.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed
consumer installs a tagged release and none keeps a vendored copy
([docs/migration-plan.md](docs/migration-plan.md) is done); each repository's `package.json` names
the tag it pins.
ddiff --git a/STATUS.json b/STATUS.json
iindex ffa25b5..56456b3 100644
---- a/STATUS.json
++++ b/STATUS.json
@@@ -4,7 +4,7 @@
"code": true,
"wave": "W2",
"package": "openvibe-shared",
-- "version": "4.147.0",
++ "version": "4.148.0",
"plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026)",
"deployed": false,
"deployment": "none of its own: a package released as git tags; OpenVibe.Network serves /shared/* from its pinned tag, and a site can serve its own pinned copy (openvibe-shared/serve)",
ddiff --git a/navbar.js b/navbar.js
iindex 9196b82..e780542 100644
---- a/navbar.js
++++ b/navbar.js
@@@ -172,7 +172,7 @@
.openvibe-navbar-right.has-chips-all .ovnav-chip[data-chip-id] { display: none; }
.openvibe-navbar-right.has-chips-all .ovnav-chip--all { display: inline-flex; }
}
-- @media (max-width: 860px) { .openvibe-navbar .ovnav-burger, .openvibe-navbar .ovnav-featured-phone { display: inline-flex; } .openvibe-navbar:has(.ovnav-burger) .ovnav-orb { display: none; } .openvibe-navbar .openvibe-navbar-links, .openvibe-navbar .openvibe-navbar-spacer { display: none; } .openvibe-navbar .openvibe-navbar-right { margin-left: auto; } .openvibe-navbar .ovnav-chip .ovnav-ic + .ovnav-chip-v:empty { display: none; } }
++ @media (max-width: 860px) { .openvibe-navbar .ovnav-burger, .openvibe-navbar .ovnav-featured-phone { display: inline-flex; } .openvibe-navbar:has(.ovnav-burger) .ovnav-orb:not(.ovnav-burger) { display: none; } .openvibe-navbar .openvibe-navbar-links, .openvibe-navbar .openvibe-navbar-spacer { display: none; } .openvibe-navbar .openvibe-navbar-right { margin-left: auto; } .openvibe-navbar .ovnav-chip .ovnav-ic + .ovnav-chip-v:empty { display: none; } }
@media (max-width: 420px) { .openvibe-navbar .ovnav-chip { padding: 0 8px; height: 30px; font-size: 12px; } }
/* A narrow phone never draws the site's name over the bar's buttons (owner, 2026-10-28: on openvibe.games, signed
in, the name ran under the launcher and the magnifier). The name gives way with an ellipsis, and up to 440px
@@@ -295,6 +295,14 @@
@media (prefers-reduced-motion: reduce) { .ovnav-orb .ovo-g, .ovnav-orb .ovo-d, .ovnav-orb::before { transition: none; animation: none !important; } }
html[data-ov-motion="reduced"] .ovnav-orb .ovo-d { animation: none !important; }
.openvibe-navbar .openvibe-navbar-brand .ovnav-orb { margin-left: 1px; }
++ /* 4.148.0 (the owner: "replace the hamburger with that button"): the phone's menu button is the orb, in the
++ burger's place at the bar's right. Hidden on a wide bar (its links are there), shown at 860 px and below;
++ the orb's glow is its hover, not the burger's square. */
++ .openvibe-navbar .ovnav-orb.ovnav-burger { display: none; }
++ /* At the bar's right edge its tap area grows to the left (the brand orb's reached 12 px right, past the screen). */
++ .openvibe-navbar .ovnav-orb.ovnav-burger::after { inset: -6px -4px -6px -10px; }
++ .openvibe-navbar .ovnav-orb.ovnav-burger:hover, .openvibe-navbar .ovnav-orb.ovnav-burger[aria-expanded="true"] { background: none; }
++ @media (max-width: 860px) { .openvibe-navbar .ovnav-orb.ovnav-burger { display: inline-grid; } }
@media (max-width: 400px) { .openvibe-navbar .ovnav-orb { width: 28px; height: 32px; } }
/* Messages (4.25.0; the owner, 2026-10-28: the floating DM button was "buggy as hell on mobile… app style"): a
site that has messages puts them in the bar beside the bell, with the unread count, instead of a bubble
@@@ -327,6 +335,22 @@
transition: filter .15s; text-decoration: none; display: inline-flex; align-items: center;
}
.openvibe-navbar-login:hover { filter: brightness(1.12); }
++ /* 4.148.0 (the owner, 11-08: "the things need to be more consistent in the nav bar in size"): one height for
++ every control at the bar's right, 36 px (34 px on a phone under 400 px): Sign In, the orb, messages, the
++ picture, the balances and a featured link. The orb draws its dots at 26 px (24) so it reads as large as
++ Sign In beside it; Sign In's corners follow the orb's. */
++ .openvibe-navbar .ovnav-orb { width: 36px; height: 36px; }
++ .openvibe-navbar .ovnav-orb svg { width: 26px; height: 26px; }
++ .openvibe-navbar .ovnav-msgs, .openvibe-navbar .ovnav-featured-phone { width: 36px; height: 36px; }
++ .openvibe-navbar .openvibe-navbar-avatar { width: 36px; height: 36px; box-sizing: border-box; }
++ .openvibe-navbar .ovnav-chip { height: 36px; }
++ .openvibe-navbar .openvibe-navbar-login { box-sizing: border-box; height: 36px; padding: 0 16px; font-size: 14px; border-radius: 10px; }
++ @media (max-width: 400px) {
++ .openvibe-navbar .ovnav-orb, .openvibe-navbar .ovnav-msgs, .openvibe-navbar .ovnav-featured-phone, .openvibe-navbar .openvibe-navbar-avatar { width: 34px; height: 34px; }
++ .openvibe-navbar .ovnav-orb svg { width: 24px; height: 24px; }
++ .openvibe-navbar .ovnav-chip, .openvibe-navbar .openvibe-navbar-login { height: 34px; }
++ .openvibe-navbar .openvibe-navbar-login { padding: 0 13px; }
++ }
.openvibe-navbar-dropdown {
position: absolute; top: 48px; right: 8px;
@@@ -882,9 +906,12 @@
// The OpenVibe menu's button: nine dots as a diamond (the old launcher's, which the owner liked), the centre in the
// accent. Rings: 0 the centre, 1 the edges, 2 the corners (the ripple's order; corners and centre make the ×).
const ORB_DOTS = [[12, 12, 0], [12, 5, 1], [19, 12, 1], [12, 19, 1], [5, 12, 1], [5, 5, 2], [19, 5, 2], [19, 19, 2], [5, 19, 2]];
-- function orbHTML() {
++ function orbSVG() {
const dots = ORB_DOTS.map(([x, y, r]) => `<circle class="ovo-d ovo-r${r}" cx="${x}" cy="${y}" r="${r ? 2.15 : 2.5}"/>`).join('');
-- return `<button type="button" class="ovnav-search ovnav-orb" id="openvibe-search-btn" aria-label="OpenVibe menu: search, every site and tool (/)" title="Search and every OpenVibe site (/)" aria-haspopup="dialog" aria-expanded="false"><svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true"><g class="ovo-g">${dots}</g></svg></button>`;
++ return `<svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true"><g class="ovo-g">${dots}</g></svg>`;
++ }
++ function orbHTML() {
++ return `<button type="button" class="ovnav-search ovnav-orb" id="openvibe-search-btn" aria-label="OpenVibe menu: search, every site and tool (/)" title="Search and every OpenVibe site (/)" aria-haspopup="dialog" aria-expanded="false">${orbSVG()}</button>`;
}
@@@ -1929,7 +1956,7 @@
<div id="openvibe-bell-mount"></div>
${u ? avatarImg(u, 64, 'openvibe-navbar-avatar', 'openvibe-avatar-btn') :
`<a class="openvibe-navbar-login" id="openvibe-login-btn" href="${escapeAttr(loginHref)}">Sign In</a>`}
-- ${links.length ? '<button type="button" class="ovnav-burger" id="openvibe-burger" aria-label="Menu" aria-haspopup="true" aria-expanded="false"><span></span><span></span><span></span></button>' : ''}
++ ${links.length ? `<button type="button" class="ovnav-burger ovnav-orb" id="openvibe-burger" aria-label="Menu: this site, search and every OpenVibe site" aria-haspopup="dialog" aria-expanded="false">${orbSVG()}</button>` : ''}
</div>
${links.length ? `<div class="ovnav-drawer" id="openvibe-drawer" role="menu">${drawerOrder(links).map(l => linkHTML(l, true)).join('')}<div class="ovnav-drawer-net">${networkLinksHTML(links, true)}</div>${u ? '' : '<div class="ovnav-display" hidden></div>'}</div>` : ''}
`;
@@@ -2110,7 +2137,8 @@
}
/** The OpenVibe menu (search.js, loaded on first use): search, every site and every tool. tab: 'you' | 'sites' | 'tools'. */
function openSearch(q, tab, fromEl) {
-- const orb = _navEl && _navEl.querySelector('#openvibe-search-btn');
++ // The orb that waves while the menu loads: the one pressed (the phone's menu button is an orb too, 4.148.0).
++ const orb = (fromEl && fromEl.classList && fromEl.classList.contains('ovnav-orb') ? fromEl : null) || (_navEl && _navEl.querySelector('#openvibe-search-btn'));
// The menu drops from the button that opened it (the orb, or the burger), under the bar.
const from = fromEl || orb;
const anchor = () => {
@@@ -2194,8 +2222,8 @@
// One ripple a tab, a moment after the first paint: here is the menu.
let seen = _orbIntro; try { seen = seen || sessionStorage.getItem('ov-orb-intro') === '1'; sessionStorage.setItem('ov-orb-intro', '1'); } catch { /* */ }
// The timers look the orb up when they fire: holding this one would keep a swapped-out bar alive (boost).
-- const orbNow = () => _navEl && _navEl.querySelector && _navEl.querySelector('#openvibe-search-btn');
-- if (!seen && b.classList) { _orbIntro = true; setTimeout(() => { const o = orbNow(); if (o) o.classList.add('ovo-intro'); setTimeout(() => { const o2 = orbNow(); if (o2) o2.classList.remove('ovo-intro'); }, 1100); }, 700); }
++ const orbsNow = () => (_navEl && _navEl.querySelectorAll ? [..._navEl.querySelectorAll('#openvibe-search-btn, #openvibe-burger.ovnav-orb')] : []);
++ if (!seen && b.classList) { _orbIntro = true; setTimeout(() => { orbsNow().forEach((o) => o.classList.add('ovo-intro')); setTimeout(() => { orbsNow().forEach((o) => o.classList.remove('ovo-intro')); }, 1100); }, 700); }
}
if (_searchKeys || _config.search === false || typeof document === 'undefined') return;
_searchKeys = true;
ddiff --git a/package.json b/package.json
iindex a74d591..eaed229 100644
---- a/package.json
++++ b/package.json
@@@ -1,6 +1,6 @@
{
"name": "openvibe-shared",
-- "version": "4.147.0",
++ "version": "4.148.0",
"description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site",
"main": "index.js",
"bin": {
ddiff --git a/test/nav-icons.test.js b/test/nav-icons.test.js
iindex 6f66aa7..a25b9bf 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 = 44 * 1024; // 43.5 KB with the menu warmed at idle and on intent (4.137.0); 43.2 KB with every sibling's ?v= content hash (4.137.0, 2026-11-07: the owner's menu was a day stale without them); 42.8 KB keeping the menu's Frame fields (4.132.0, 2026-11-07); 42.6 KB with Messages and the balances on every site (4.130.0, 2026-11-07); 42.1 KB with the featured link's phone button (4.119.0, 2026-11-06); 41.4 KB with the page-move code (4.70.0: busy(), the mark on plain links, prefetch rules on unboosted sites); was 41 KB with the Dock's loader (4.51.0)
++const NAVBAR_BROTLI_BUDGET = 45 * 1024; // 44.3 KB with the phone's menu button drawn as the orb (4.148.0, 2026-11-08); 43.5 KB with the menu warmed at idle and on intent (4.137.0); 43.2 KB with every sibling's ?v= content hash (4.137.0, 2026-11-07: the owner's menu was a day stale without them); 42.8 KB keeping the menu's Frame fields (4.132.0, 2026-11-07); 42.6 KB with Messages and the balances on every site (4.130.0, 2026-11-07); 42.1 KB with the featured link's phone button (4.119.0, 2026-11-06); 41.4 KB with the page-move code (4.70.0: busy(), the mark on plain links, prefetch rules on unboosted sites); was 41 KB with the Dock's loader (4.51.0)
// ── size budget and generated block ──────────────────────────
{
ddiff --git a/test/navbar-brand.test.js b/test/navbar-brand.test.js
iindex facde84..8f0713c 100644
---- a/test/navbar-brand.test.js
++++ b/test/navbar-brand.test.js
@@@ -251,7 +251,7 @@ function load(hostname, opts = {}) {
assert.ok(nav.innerHTML.includes('class="b-sub">Pastes<') && nav.innerHTML.includes('class="b-tld">Tools<'), 'segments rendered');
assert.ok(nav.innerHTML.includes('data-variant="tools"'), 'ov-mark variant');
assert.ok(/href="\/mine" class="ovnav-link active"/.test(nav.innerHTML), 'current path marked active');
-- assert.ok(/class="ovnav-burger"/.test(nav.innerHTML) && /class="ovnav-drawer"/.test(nav.innerHTML), 'links get a mobile drawer');
++ assert.ok(/class="ovnav-burger ovnav-orb"/.test(nav.innerHTML) && /class="ovnav-drawer"/.test(nav.innerHTML), 'links get a phone menu button, drawn as the orb (4.148.0)');
assert.ok(/<span class="icon"><svg class="ovnav-ic[ "]/.test(nav.innerHTML), 'link icon rendered inline (no icon font needed)');
assert.ok(/<span class="icon"><svg class="ovnav-ic ovnav-ovg" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="/.test(nav.innerHTML), 'a glyph the OpenVibe set has is drawn from it, on its 24-unit grid (4.71.0)');
assert.ok(!/fa-solid fa-plus/.test(nav.innerHTML), 'known icons do not depend on Font Awesome');
ddiff --git a/test/navbar-phone-fit-chrome.test.js b/test/navbar-phone-fit-chrome.test.js
iindex 4eb2fc5..a19ffa9 100644
---- a/test/navbar-phone-fit-chrome.test.js
++++ b/test/navbar-phone-fit-chrome.test.js
@@@ -46,7 +46,9 @@ const MEASURE = `(() => {
['balances', bar.querySelector('.ovnav-chip--all')], ['sign in', bar.querySelector('#openvibe-login-btn')],
['picture', bar.querySelector('#openvibe-avatar-btn')], ['menu', bar.querySelector('#openvibe-burger')]].filter(([, el]) => vis(el)).map(([k, el]) => [k, box(el)]);
const minis = [...bar.querySelectorAll('.ovnav-chip--all .ovnav-chip-mini')].filter(vis).length;
-- return { parts, clipped: name.scrollWidth > name.clientWidth + 1, visibleText: name.innerText, minis, width: innerWidth };
++ const menu = bar.querySelector('#openvibe-burger');
++ const orbMenu = !!(menu && menu.classList.contains('ovnav-orb') && menu.querySelectorAll('.ovo-g .ovo-d').length === 9 && !menu.querySelector('span'));
++ return { parts, clipped: name.scrollWidth > name.clientWidth + 1, visibleText: name.innerText, minis, width: innerWidth, orbMenu, docW: document.documentElement.scrollWidth };
})()`;
(async () => {
@@@ -74,7 +76,12 @@ const MEASURE = `(() => {
if (who === 'in') assert.strictEqual(v.minis, 2, `${at}: the folded balances show both, OpenCoins and Vibes (4.25.0)`);
// One menu button on a phone (4.137.0, the owner: "these buttons do the same shit"): the burger opens the
// OpenVibe menu, and the orb that opened the same menu steps aside.
-- assert.ok(v.parts.some(([k]) => k === 'menu'), `${at}: the burger is the menu button`);
++ assert.ok(v.parts.some(([k]) => k === 'menu'), `${at}: the menu button is there`);
++ // …and it is the orb, the network's own nine dots with its ripple and turn, not three lines (4.148.0, the
++ // owner: "replace the hamburger with that button").
++ assert.ok(v.orbMenu, `${at}: the menu button draws the orb, not a burger`);
++ // Nothing in the bar (a tap area included) pushes the page past the screen (4.148.0: the orb's reached 6 px past it).
++ assert.ok(v.docW <= width, `${at}: the page is ${v.docW} px wide`);
assert.ok(!v.parts.some(([k]) => k === 'search'), `${at}: no second button for the same menu (4.137.0)`);
assert.ok(!v.parts.some(([k]) => k === 'launcher'), `${at}: no launcher button: the magnifier opens the one menu (4.23.0)`);
await p.close();