openvibers/openvibe.shared Commits

openvibe-shared 4.223.1: the menu keeps its search field and tabs (#499)

OP OpenVibers committed 22 h ago · 3d48b8c1a6 · 9 files, +98 −7

The panel clips its light instead of being a hidden-overflow scroll container that a focus could
scroll; any scroll it is given is put back. "Your own AI" keeps its name on one line.

Changes

ddiff --git a/CHANGELOG.md b/CHANGELOG.md
iindex 669ee43..1fbd34e 100644
---- a/CHANGELOG.md
++++ b/CHANGELOG.md
@@@ -1,5 +1,20 @@
# Changelog
++## 4.223.1 — 2026-11-12
++
++The menu keeps its search field and tabs (the owner, 11-12, two phone screenshots: "the gui gets messed up and loses
++its tabs", "Missing top tabs").
++
++- **What went wrong:**
++ - 4.223.0's slow light behind the menu is drawn 20% past the panel's edges, and the panel was `overflow: hidden`.
++ - A hidden-overflow box is still a scroll container, so a focus or a scroll into view inside the menu scrolled the
++ whole panel up: the field and the tabs left the screen, with an empty band at the bottom.
++- **The fix:** the panel now clips (`overflow: clip` is never a scroll container), and any scroll an older engine
++ gives it anyway is put back at once.
++- "Your own AI" keeps its name on one line; its sentence takes the wrap.
++- **New test** `menu-panel-still-chrome`: at 390 and 1280 px, a focus deep in the site list moves the list, never the
++ panel.
++
## 4.223.0 — 2026-11-12
You redone as tabs, icons that fill their squares everywhere in the menu, names in the brand face, and one composer with
ddiff --git a/README.md b/README.md
iindex 4f5b8b8..05f93f4 100644
---- a/README.md
++++ b/README.md
@@@ -5,7 +5,7 @@
**Docs:** https://openvibe.services · **For AI:** https://openvibe.network/llms.txt
**Install and test:** `npm ci && npm test`
--**Status:** alpha, v4.223.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed
++**Status:** alpha, v4.223.1 (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 4d46ebe..8697c96 100644
---- a/STATUS.json
++++ b/STATUS.json
@@@ -4,7 +4,7 @@
"code": true,
"wave": "W2",
"package": "openvibe-shared",
-- "version": "4.223.0",
++ "version": "4.223.1",
"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 437c494..deff4f2 100644
---- a/navbar.js
++++ b/navbar.js
@@@ -604,7 +604,7 @@
* serves its own pinned copy at /shared/ gets its own), else the network's copy.
*/
// The files loaded from beside this one, by content (scripts/sibling-revs.js): a release changes their URLs.
-- const SIBLING_REV = { 'ov-mark.js': 'f34f731c3a94', 'ov-glyphs.js': 'c96584376da1', 'ov-brand.woff2': '3b4e6a8a5878', 'ov-look.js': 'dd84a30bb646', 'ov-fx.js': '947820d69097', 'ov-labels.js': 'd40f7ca14bc4', 'ov-forms.js': '2eb9e2604c3e', 'ov-forms.css': '06208fe89eac', 'ov-share.js': 'd419508a797e', 'ov-share.css': 'd609a71f611a', 'search.js': 'f1d4e50592b4', 'dock.js': '1fb16839cb0e', 'presence.js': '5f195d41f8c5', 'panels.js': 'd90bfe987d81', 'surfaces.js': 'a94e76bd1200', 'notification-ui.js': '4086b813ffda', 'history.js': 'f9fb9e096c65', 'bottom.js': '1671b6949137', 'appbar.js': 'c21485176ae9', 'release-watch.js': '25f02a3f0684', 'ov-icons.js': 'aaa688bf6fc7', 'sso-client.js': 'f574ac3331a5', 'overlay.js': '2bc56b3cf726', 'ov-home.js': '02d7b0204716', 'nav-slots.js': '7b11e6916732', 'ov-wallet.js': '3fa6bb23b429', 'ov-wallet.css': '4923c808c9ad', 'purse.js': '9d3fea0dae02', 'ov-drop.js': '445689a5eb80', 'ov-tip.js': '1d918e7de69d', 'ov-arrange.js': '898af091b3e1', 'soft.js': '98ab9b8080b1' };
++ const SIBLING_REV = { 'ov-mark.js': 'f34f731c3a94', 'ov-glyphs.js': 'c96584376da1', 'ov-brand.woff2': '3b4e6a8a5878', 'ov-look.js': 'dd84a30bb646', 'ov-fx.js': '947820d69097', 'ov-labels.js': 'd40f7ca14bc4', 'ov-forms.js': '2eb9e2604c3e', 'ov-forms.css': '06208fe89eac', 'ov-share.js': 'd419508a797e', 'ov-share.css': 'd609a71f611a', 'search.js': 'b5f1f966bae3', 'dock.js': '1fb16839cb0e', 'presence.js': '5f195d41f8c5', 'panels.js': 'd90bfe987d81', 'surfaces.js': 'a94e76bd1200', 'notification-ui.js': '4086b813ffda', 'history.js': 'f9fb9e096c65', 'bottom.js': '1671b6949137', 'appbar.js': 'c21485176ae9', 'release-watch.js': '25f02a3f0684', 'ov-icons.js': 'aaa688bf6fc7', 'sso-client.js': 'f574ac3331a5', 'overlay.js': '2bc56b3cf726', 'ov-home.js': '6215643437c0', 'nav-slots.js': '7b11e6916732', 'ov-wallet.js': '3fa6bb23b429', 'ov-wallet.css': '4923c808c9ad', 'purse.js': '9d3fea0dae02', 'ov-drop.js': '445689a5eb80', 'ov-tip.js': '1d918e7de69d', 'ov-arrange.js': '898af091b3e1', 'soft.js': '98ab9b8080b1' };
const siblingV = (name) => `${sibling(name)}${SIBLING_REV[name] ? `?v=${SIBLING_REV[name]}` : ''}`;
// ov-tip.js loads at the first hover over a tooltip's owner.
ddiff --git a/ov-home.js b/ov-home.js
iindex b7253e6..030aa48 100644
---- a/ov-home.js
++++ b/ov-home.js
@@@ -452,6 +452,10 @@ html[data-ov-motion=off] .ovh-face.is-glyph:has(.is-working) { animation: none;
.ovh-actor .ovh-go.is-ic svg, .ovh-actor .ovh-go.is-answer svg { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width: 2.2; stroke-linecap: round; stroke-linejoin: round; }
.ovh-actor .ovh-go.is-answer { flex: none; display: inline-flex; align-items: center; gap: 6px; height: 36px; padding: 0 14px 0 11px; border-radius: 999px; background: var(--accent-strong, var(--accent, #2563eb)); color: var(--on-accent-strong, #fff); font-weight: 750; font-size: 13.5px; }
@media (hover: hover) and (pointer: fine) { .ovh-actor:hover .ovh-go.is-ic { transform: translate(2px, -2px); background: color-mix(in srgb, var(--accent, #3b82f6) 24%, transparent); } }
++
++/* 4.223.1: the notice's name stays on one line ("Your own AI", not "Your own / AI"); its sentence takes the wrap. */
++.ovh-notice .ovh-actor.is-invite .ovh-actor-txt b { white-space: nowrap; overflow: hidden; text-overflow: clip; }
++@container ovh (max-width: 400px) { .ovh-notice .ovh-actor.is-invite .ovh-go { padding: 0 12px; font-size: 13px; } }
`;
document.head.appendChild(st);
}
ddiff --git a/package-lock.json b/package-lock.json
iindex 31346df..5fda43f 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -1,12 +1,12 @@
{
"name": "openvibe-shared",
-- "version": "4.223.0",
++ "version": "4.223.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "openvibe-shared",
-- "version": "4.223.0",
++ "version": "4.223.1",
"license": "MIT",
"dependencies": {
"clean-css": "^5.3.3",
ddiff --git a/package.json b/package.json
iindex 82a693e..bd3edaa 100644
---- a/package.json
++++ b/package.json
@@@ -1,6 +1,6 @@
{
"name": "openvibe-shared",
-- "version": "4.223.0",
++ "version": "4.223.1",
"description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site",
"main": "index.js",
"bin": {
ddiff --git a/search.js b/search.js
iindex b53c000..a163fa2 100644
---- a/search.js
++++ b/search.js
@@@ -340,7 +340,9 @@ html[data-ov-motion=off] .ovs-grid.is-mini .ovs-ic { transition: none; }
.ovs-grid.is-mini .ovs-tile b { font-size: 13px; color: color-mix(in srgb, var(--text-primary, #e6edf7) 92%, transparent); }
.ovs-group[data-sec="all"] { font-family: 'OV Brand', Nunito, Inter, -apple-system, BlinkMacSystemFont, sans-serif; font-size: 13.5px; font-weight: 800; letter-spacing: .01em; text-transform: none; color: var(--text-primary, #e6edf7); }
/* A slow light drifts behind the menu (Calm slows it, only Off stops it; openvibe.design: calm reduces, only off removes). */
--.ovs-panel { isolation: isolate; }
++/* 4.223.1: the panel clips its light (overflow: clip is never a scroll container; hidden was, and a focus inside it
++ scrolled the whole panel up, taking the search field and the tabs off the screen). */
++.ovs-panel { isolation: isolate; overflow: clip; }
.ovs-panel::before { content: ""; position: absolute; inset: -20%; z-index: -1; pointer-events: none; opacity: .5;
background: radial-gradient(38% 30% at 20% 10%, color-mix(in srgb, var(--accent, #3b82f6) 22%, transparent), transparent 70%), radial-gradient(34% 28% at 85% 30%, color-mix(in srgb, #a855f7 16%, transparent), transparent 70%), radial-gradient(40% 32% at 50% 95%, color-mix(in srgb, #14b8a6 12%, transparent), transparent 70%);
animation: ovs-drift 26s ease-in-out infinite alternate; }
@@@ -648,6 +650,9 @@ html[data-ov-motion=off] .ovs-door-ink, html[data-ov-motion=off] .ovs-door-sw .o
rtabsEl.addEventListener('click', (e) => { const b = e.target.closest && e.target.closest('[data-rtab]'); if (b) { showResults(rq, b.getAttribute('data-rtab')); input.focus(); } });
el.addEventListener('mousedown', (e) => { if (e.target === el) close(); });
el.querySelector('.ovs-x').addEventListener('click', close);
++ // 4.223.1: the panel never scrolls as a whole (only its list does); an engine that still scrolls it on a focus
++ // is put back at once, so the search field and the tabs stay in place.
++ { const panel = el.querySelector('.ovs-panel'); if (panel) panel.addEventListener('scroll', () => { if (panel.scrollTop || panel.scrollLeft) { panel.scrollTop = 0; panel.scrollLeft = 0; } }, { passive: true }); }
orbEl = document.createElement('button');
orbEl.type = 'button'; orbEl.className = 'ovs-orb'; orbEl.setAttribute('aria-label', 'Close the menu'); orbEl.title = 'Close (Esc)';
orbEl.innerHTML = orbSvg();
ddiff --git a/test/menu-panel-still-chrome.test.js b/test/menu-panel-still-chrome.test.js
nnew file mode 100644
iindex 0000000..d88e4d5
---- /dev/null
++++ b/test/menu-panel-still-chrome.test.js
@@@ -0,0 +1,67 @@
++'use strict';
++// The menu's panel never scrolls as a whole (4.223.1; the owner, 11-12, two phone screenshots: the menu "loses its
++// tabs"). 4.223.0 drew a light 20% past the panel's edges and the panel was overflow: hidden, which is still a scroll
++// container: a focus or a scroll into view inside it scrolled the panel up and the search field and the tabs left the
++// screen. Here, at 390 and 1280 px, the You tab and the site list are opened, the last tile is focused and scrolled into
++// view, and the panel must still be at 0 with its field and tabs where they were. Skipped without Chrome.
++const assert = require('assert');
++const fs = require('fs');
++const os = require('os');
++const path = require('path');
++const http = require('http');
++const h = require('../browser-harness');
++
++if (process.env.OV_SKIP_BROWSER === '1' || !h.findChrome()) { console.log('menu-panel-still-chrome: skipped (no Chrome)'); process.exit(0); }
++
++const ROOT = path.join(__dirname, '..');
++const SITES = Array.from({ length: 24 }, (_, i) => ({ id: `s${i}`, name: `Site ${i}`, url: `https://openvibe.s${i}.example/`, icon: 'live', group: i < 12 ? 'play' : 'more' }));
++const FRAME = { nav: SITES, groups: [{ id: 'play', name: 'Play' }, { id: 'more', name: 'More' }], soon: [], footer: {} };
++const page = () => `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Here</title><link rel="icon" href="data:,">
++<script>
++ const J = (b, ok = true, status = 200) => Promise.resolve({ ok, status, json: async () => b });
++ const realFetch = window.fetch;
++ window.fetch = (u, o) => { const s = String(u);
++ if (s.startsWith('https://openvibe.network/api/frame?')) return J(${JSON.stringify(FRAME)});
++ if (s.startsWith('https://')) return J({}, false, 404);
++ return realFetch(u, o); };
++</script>
++<script src="/shared/navbar.js" defer></script></head><body><div id="navbar-mount"></div>
++<script>document.addEventListener('DOMContentLoaded', function () { try { localStorage.clear(); sessionStorage.clear(); } catch (e) {} OpenVibeNavbar.init({ service: 'site', user: null, mount: '#navbar-mount' }); });</script>
++</body></html>`;
++const server = http.createServer((req, res) => {
++ const m = /^\/shared\/([a-z0-9-]+\.(?:js|css|woff2))$/.exec(new URL(req.url, 'http://x').pathname);
++ if (m) { if (!fs.existsSync(path.join(ROOT, m[1]))) { res.writeHead(404); return res.end(); } res.writeHead(200, { 'content-type': m[1].endsWith('.js') ? 'application/javascript' : 'application/octet-stream' }); return res.end(fs.readFileSync(path.join(ROOT, m[1]))); }
++ res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' }); return res.end(page());
++});
++
++(async () => {
++ await new Promise((ok) => server.listen(0, '127.0.0.1', ok));
++ const base = `http://127.0.0.1:${server.address().port}`;
++ const browser = await h.launch({ tmpDir: fs.mkdtempSync(path.join(os.tmpdir(), 'ov-still-')) });
++ const wait = (ms) => new Promise((r) => setTimeout(r, ms));
++ const until = async (p, expr, ms = 6000) => { const end = Date.now() + ms; for (;;) { const v = await p.evaluate(expr); if (v || Date.now() > end) return v; await wait(60); } };
++ try {
++ const context = await browser.newContext();
++ for (const [w, ht] of [[390, 844], [1280, 860]]) {
++ const p = await h.openPage(browser, context, { width: w, height: ht });
++ await p.goto(`${base}/`, { maxMs: 6000 });
++ await until(p, `!!(document.getElementById('openvibe-burger') || document.getElementById('openvibe-search-btn'))`);
++ await p.evaluate(`(document.getElementById('openvibe-burger') || document.getElementById('openvibe-search-btn')).click(), true`);
++ assert.ok(await until(p, `document.querySelectorAll('.ovs.is-open .ovs-grid.is-mini .ovs-tile').length >= 20`), `${w}px: the site list is drawn`);
++ await wait(500);
++ const head = () => p.evaluate(`(() => { const panel = document.querySelector('.ovs.is-open .ovs-panel'); const field = panel.querySelector('.ovs-input'); const tabs = panel.querySelector('.ovs-starttabs');
++ return { scroll: panel.scrollTop, field: Math.round(field.getBoundingClientRect().top), tabs: Math.round(tabs.getBoundingClientRect().top), overflow: getComputedStyle(panel).overflowY }; })()`);
++ const before = await head();
++ // What a focus deep in the list does: scroll the last tile into view, then focus it.
++ await p.evaluate(`(() => { const t = [...document.querySelectorAll('.ovs.is-open .ovs-grid.is-mini .ovs-tile')].pop(); t.scrollIntoView({ block: 'end' }); t.focus(); })(), true`);
++ await wait(300);
++ const after = await head();
++ assert.strictEqual(after.scroll, 0, `${w}px: the panel never scrolls as a whole (${JSON.stringify(after)})`);
++ assert.strictEqual(after.overflow, 'clip', `${w}px: the panel clips, it is not a scroll container`);
++ assert.deepStrictEqual([after.field, after.tabs], [before.field, before.tabs], `${w}px: the search field and the tabs stay where they were`);
++ assert.ok(after.field >= 0 && after.tabs > after.field, `${w}px: the field and the tabs are on the screen`);
++ await p.close();
++ }
++ console.log('menu-panel-still-chrome: a focus deep in the menu scrolls its list, never the panel; the field and the tabs stay on screen');
++ } finally { await browser.close(); server.close(); }
++})().catch((e) => { console.error(e); process.exit(1); });