openvibers/openvibe.shared Commits
openvibe-shared 4.223.1: the menu keeps its search field and tabs (#499)
Changes
@@@ -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-12You redone as tabs, icons that fill their squares everywhere in the menu, names in the brand face, and one composer with@@@ -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 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.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)",@@@ -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.@@@ -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);}@@@ -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",@@@ -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": {@@@ -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 .ortabsEl.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();nnew file mode 100644@@@ -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); });
@@ -1,5 +1,20 @@
@@ -1,5 +1,20 @@
# Changelog
# 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
## 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
You redone as tabs, icons that fill their squares everywhere in the menu, names in the brand face, and one composer with
@@ -5,7 +5,7 @@
@@ -5,7 +5,7 @@
**Docs:** https://openvibe.services · **For AI:** https://openvibe.network/llms.txt
**Docs:** https://openvibe.services · **For AI:** https://openvibe.network/llms.txt
**Install and test:** `npm ci && npm test`
**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
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.223.0",
+ "version": "4.223.1",
"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)",
@@ -604,7 +604,7 @@
@@ -604,7 +604,7 @@
* serves its own pinned copy at /shared/ gets its own), else the network's copy.
* 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.
// 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]}` : ''}`;
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.
// ov-tip.js loads at the first hover over a tooltip's owner.
@@ -452,6 +452,10 @@ html[data-ov-motion=off] .ovh-face.is-glyph:has(.is-working) { animation: none;
@@ -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-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; }
.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); } }
@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);
document.head.appendChild(st);
}
}
@@ -1,12 +1,12 @@
@@ -1,12 +1,12 @@
{
{
"name": "openvibe-shared",
"name": "openvibe-shared",
- "version": "4.223.0",
+ "version": "4.223.1",
"lockfileVersion": 3,
"lockfileVersion": 3,
"requires": true,
"requires": true,
"packages": {
"packages": {
"": {
"": {
"name": "openvibe-shared",
"name": "openvibe-shared",
- "version": "4.223.0",
+ "version": "4.223.1",
"license": "MIT",
"license": "MIT",
"dependencies": {
"dependencies": {
"clean-css": "^5.3.3",
"clean-css": "^5.3.3",
@@ -1,6 +1,6 @@
@@ -1,6 +1,6 @@
{
{
"name": "openvibe-shared",
"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",
"description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site",
"main": "index.js",
"main": "index.js",
"bin": {
"bin": {
@@ -340,7 +340,9 @@ html[data-ov-motion=off] .ovs-grid.is-mini .ovs-ic { transition: none; }
@@ -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-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); }
.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). */
/* 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;
.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%);
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; }
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
@@ -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(); } });
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.addEventListener('mousedown', (e) => { if (e.target === el) close(); });
el.querySelector('.ovs-x').addEventListener('click', 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 = document.createElement('button');
orbEl.type = 'button'; orbEl.className = 'ovs-orb'; orbEl.setAttribute('aria-label', 'Close the menu'); orbEl.title = 'Close (Esc)';
orbEl.type = 'button'; orbEl.className = 'ovs-orb'; orbEl.setAttribute('aria-label', 'Close the menu'); orbEl.title = 'Close (Esc)';
orbEl.innerHTML = orbSvg();
orbEl.innerHTML = orbSvg();
new file mode 100644
new file mode 100644
@@ -0,0 +1,67 @@
@@ -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); });