openvibers/openvibe.shared Commits
4.148.0: the phone's menu button is the orb, and the bar's controls are one size (#338)
Changes
@@@ -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@@@ -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 deployedconsumer 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` namesthe tag it pins.@@@ -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)",@@@ -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, signedin, 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"): asite 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;@@@ -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": {@@@ -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 ──────────────────────────{@@@ -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');@@@ -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();
@@ -1,5 +1,26 @@
@@ -1,5 +1,26 @@
# Changelog
# 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
## 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
- **A link in a Dock message shows its card.** OpenVibe.Chat now resolves a known OpenVibe link when a message is sent
@@ -2,7 +2,7 @@
@@ -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.
> 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
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
([docs/migration-plan.md](docs/migration-plan.md) is done); each repository's `package.json` names
the tag it pins.
the tag it pins.
@@ -4,7 +4,7 @@
@@ -4,7 +4,7 @@
"code": true,
"code": true,
"wave": "W2",
"wave": "W2",
"package": "openvibe-shared",
"package": "openvibe-shared",
- "version": "4.147.0",
+ "version": "4.148.0",
"plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026)",
"plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026)",
"deployed": false,
"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)",
"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)",
@@ -172,7 +172,7 @@
@@ -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[data-chip-id] { display: none; }
.openvibe-navbar-right.has-chips-all .ovnav-chip--all { display: inline-flex; }
.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; } }
@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
/* 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
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 @@
@@ -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; } }
@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; }
html[data-ov-motion="reduced"] .ovnav-orb .ovo-d { animation: none !important; }
.openvibe-navbar .openvibe-navbar-brand .ovnav-orb { margin-left: 1px; }
.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; } }
@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
/* 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
site that has messages puts them in the bar beside the bell, with the unread count, instead of a bubble
@@ -327,6 +335,22 @@
@@ -327,6 +335,22 @@
transition: filter .15s; text-decoration: none; display: inline-flex; align-items: center;
transition: filter .15s; text-decoration: none; display: inline-flex; align-items: center;
}
}
.openvibe-navbar-login:hover { filter: brightness(1.12); }
.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 {
.openvibe-navbar-dropdown {
position: absolute; top: 48px; right: 8px;
position: absolute; top: 48px; right: 8px;
@@ -882,9 +906,12 @@
@@ -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
// 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 ×).
// 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]];
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('');
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 @@
@@ -1929,7 +1956,7 @@
<div id="openvibe-bell-mount"></div>
<div id="openvibe-bell-mount"></div>
${u ? avatarImg(u, 64, 'openvibe-navbar-avatar', 'openvibe-avatar-btn') :
${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>`}
`<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>
</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>` : ''}
${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 @@
@@ -2110,7 +2137,8 @@
}
}
/** The OpenVibe menu (search.js, loaded on first use): search, every site and every tool. tab: 'you' | 'sites' | 'tools'. */
/** The OpenVibe menu (search.js, loaded on first use): search, every site and every tool. tab: 'you' | 'sites' | 'tools'. */
function openSearch(q, tab, fromEl) {
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.
// The menu drops from the button that opened it (the orb, or the burger), under the bar.
const from = fromEl || orb;
const from = fromEl || orb;
const anchor = () => {
const anchor = () => {
@@ -2194,8 +2222,8 @@
@@ -2194,8 +2222,8 @@
// One ripple a tab, a moment after the first paint: here is the menu.
// 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 { /* */ }
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).
// 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;
if (_searchKeys || _config.search === false || typeof document === 'undefined') return;
_searchKeys = true;
_searchKeys = true;
@@ -1,6 +1,6 @@
@@ -1,6 +1,6 @@
{
{
"name": "openvibe-shared",
"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",
"description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site",
"main": "index.js",
"main": "index.js",
"bin": {
"bin": {
@@ -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 = 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 ──────────────────────────
// ── size budget and generated block ──────────────────────────
{
{
@@ -251,7 +251,7 @@ function load(hostname, opts = {}) {
@@ -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('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(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(/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[ "]/.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(/<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');
assert.ok(!/fa-solid fa-plus/.test(nav.innerHTML), 'known icons do not depend on Font Awesome');
@@ -46,7 +46,9 @@ const MEASURE = `(() => {
@@ -46,7 +46,9 @@ const MEASURE = `(() => {
['balances', bar.querySelector('.ovnav-chip--all')], ['sign in', bar.querySelector('#openvibe-login-btn')],
['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)]);
['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;
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 () => {
(async () => {
@@ -74,7 +76,12 @@ const MEASURE = `(() => {
@@ -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)`);
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
// 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.
// 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 === '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)`);
assert.ok(!v.parts.some(([k]) => k === 'launcher'), `${at}: no launcher button: the magnifier opens the one menu (4.23.0)`);
await p.close();
await p.close();