openvibers/openvibe.shared Commits

4.107.3: on a phone, a site's own room picture sits below the hero (#279)

OP OpenVibers committed 10 days ago · 5691abe17e · 7 files, +82 −5

Under 760px the room drew its picture behind the hero's text at 0.28 opacity. A site's own picture is usually a
sample card of words and numbers, and it ran through the headline on Report, Domains, Surf, Shoes and Tours. It now
sits below the hero, whole; the default motif stays a faint backdrop.

Changes

ddiff --git a/CHANGELOG.md b/CHANGELOG.md
iindex 81f31c2..7ca4ce5 100644
---- a/CHANGELOG.md
++++ b/CHANGELOG.md
@@@ -4,6 +4,15 @@ All notable changes to `openvibe-shared`. Versions follow [semver](https://semve
breaking change to any exported module, browser global or served file name is a new major.
A release is the git tag `vX.Y.Z`; consumers pin the tag's tarball (see README).
++## 4.107.3 — 2026-11-03
++
++- **On a phone, a site's own room picture sits below the hero, not behind its text.** Under 760px the room drew its
++ picture behind the hero's text at 0.28 opacity. For the default motif (the orbits) that is a backdrop. A site's own
++ picture is usually a sample card of words and numbers, and on 11-03 it ran through the headline on Report ("OpenVibe,
++ honest" over "2 h 14 m"), Domains, Surf, Shoes and Tours. Now a picture that is not the default motif sits below the
++ hero, whole and at full strength; the default motif stays a faint backdrop. `test/room-pic-phone-chrome.test.js`
++ checks both at 360, 390 and 700px.
++
## 4.107.2 — 2026-11-03
- **The last small touch targets in the shared bar and footer reach 44px.** A re-run of the tap audit over every
ddiff --git a/README.md b/README.md
iindex e17f663..95f7a49 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.107.2 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed
++**Status:** alpha, v4.107.3 (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 347e86b..27868c6 100644
---- a/STATUS.json
++++ b/STATUS.json
@@@ -4,7 +4,7 @@
"code": true,
"wave": "W2",
"package": "openvibe-shared",
-- "version": "4.107.2",
++ "version": "4.107.3",
"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/package-lock.json b/package-lock.json
iindex f94ca70..9d680a1 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -1,12 +1,12 @@
{
"name": "openvibe-shared",
-- "version": "4.107.2",
++ "version": "4.107.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "openvibe-shared",
-- "version": "4.107.2",
++ "version": "4.107.3",
"license": "MIT",
"dependencies": {
"jsonwebtoken": "^9.0.0",
ddiff --git a/package.json b/package.json
iindex 3e9e12d..222319f 100644
---- a/package.json
++++ b/package.json
@@@ -1,6 +1,6 @@
{
"name": "openvibe-shared",
-- "version": "4.107.2",
++ "version": "4.107.3",
"description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site",
"main": "index.js",
"bin": {
ddiff --git a/room.css b/room.css
iindex eb13bc1..1a1210a 100644
---- a/room.css
++++ b/room.css
@@@ -229,6 +229,10 @@ html.ov-motion [data-ov-reveal].is-in{opacity:1;transform:none}
@media (max-width:759px){
.sc-room-hero{grid-template-columns:minmax(0,1fr);margin-inline:-16px;padding-inline:16px;overflow-x:clip}
.sc-room-pic{position:absolute;z-index:-1;top:-4px;right:-30px;width:168px;opacity:.28;pointer-events:none}
++/* 4.107.3: a site's own picture (anything but the default motif) is a sample card of words and numbers more often than a
++ backdrop: behind the hero's text it ran through the headline (Report, Domains, Surf, Shoes, Tours on 11-03). On a phone
++ it sits below the hero, whole; the default motif stays a faint backdrop. */
++.sc-room-pic:not(:has(> .sc-room-motif)){position:static;z-index:auto;width:auto;opacity:1;margin-top:24px;pointer-events:auto}
.sc-scene,.sc-scene.is-flip{grid-template-columns:minmax(0,1fr)}.sc-scene.is-flip .sc-scene-text{order:0}
}
@media (max-width:599px){.sc-room-chips{flex-wrap:nowrap;overflow-x:auto;margin-inline:-16px;padding:2px 16px;scrollbar-width:none}.sc-room-chips::-webkit-scrollbar{display:none}.sc-room-box input{height:52px;padding-left:18px}}
ddiff --git a/test/room-pic-phone-chrome.test.js b/test/room-pic-phone-chrome.test.js
nnew file mode 100644
iindex 0000000..3b2ca44
---- /dev/null
++++ b/test/room-pic-phone-chrome.test.js
@@@ -0,0 +1,64 @@
++'use strict';
++// A room's picture on a phone (4.107.3), in a real headless Chrome. Under 760px the room drew its picture behind the
++// hero's text at 0.28 opacity. For the default motif (orbits) that is a backdrop; a site's own picture is usually a
++// sample card of words and numbers, and on 11-03 it ran through the headline on Report, Domains, Surf, Shoes and Tours.
++// Now a site's own picture sits below the hero, whole; the default motif stays a faint backdrop. 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');
++const sc = require('../showcase');
++
++if (process.env.OV_SKIP_BROWSER === '1' || !h.findChrome()) {
++ console.log('room-pic-phone-chrome: skipped (no Chrome; set CHROME_BIN)');
++ process.exit(0);
++}
++
++const ROOT = path.join(__dirname, '..');
++const css = ['kit.css', 'room.css', 'showcase.css'].filter((f) => fs.existsSync(path.join(ROOT, f))).map((f) => fs.readFileSync(path.join(ROOT, f), 'utf8')).join('\n');
++const page = (motif) => `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Room</title><style>${css}</style></head>
++<body style="margin:0;padding:0 16px">${sc.room({ room: 'surf', hero: { title: 'Every number on OpenVibe, honest', lede: 'The network’s numbers, open to everyone.', ...(motif ? { motif } : {}) } })}</body></html>`;
++const CARD = '<div class="x-card" aria-hidden="true"><b>Tuesday stream · 2 h 14 m</b><p>1,204 chat messages · 186 people came by</p></div>';
++const server = http.createServer((req, res) => {
++ res.writeHead(200, { 'content-type': 'text/html' });
++ res.end(page(req.url === '/card' ? CARD : null));
++});
++
++const MEASURE = `(() => {
++ const pic = document.querySelector('.sc-room-pic'), h1 = document.querySelector('.sc-room-hero h1');
++ const p = pic.getBoundingClientRect(), t = h1.getBoundingClientRect(), cs = getComputedStyle(pic);
++ return { position: cs.position, opacity: Number(cs.opacity), picTop: p.top, h1Bottom: t.bottom, overlaps: p.left < t.right && p.right > t.left && p.top < t.bottom && p.bottom > t.top, width: document.documentElement.scrollWidth };
++})()`;
++
++(async () => {
++ await new Promise((ok) => server.listen(0, '127.0.0.1', ok));
++ const base = `http://127.0.0.1:${server.address().port}`;
++ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-roompic-'));
++ const browser = await h.launch({ tmpDir });
++ try {
++ const context = await browser.newContext();
++ for (const width of [360, 390, 700]) {
++ const card = await h.openPage(browser, context, { width, height: 800 });
++ await card.goto(`${base}/card`, { maxMs: 5000 });
++ const c = await card.evaluate(MEASURE);
++ assert.strictEqual(c.position, 'static', `${width}px: a site's own picture is in the flow`);
++ assert.strictEqual(c.opacity, 1, `${width}px: and at full strength`);
++ assert.ok(!c.overlaps && c.picTop >= c.h1Bottom, `${width}px: it sits below the headline, not behind it (${JSON.stringify(c)})`);
++ assert.ok(c.width <= width, `${width}px: no sideways scroll`);
++ await card.close();
++ const motif = await h.openPage(browser, context, { width, height: 800 });
++ await motif.goto(`${base}/default`, { maxMs: 5000 });
++ const m = await motif.evaluate(MEASURE);
++ assert.strictEqual(m.position, 'absolute', `${width}px: the default motif stays a backdrop`);
++ assert.ok(m.opacity < 0.5, `${width}px: a faint one`);
++ await motif.close();
++ }
++ console.log('room-pic-phone-chrome: a site’s own picture sits below the hero on a phone; the default motif stays a backdrop ok');
++ } finally {
++ await browser.close();
++ server.close();
++ fs.rmSync(tmpDir, { recursive: true, force: true });
++ }
++})().catch((e) => { console.error(e); process.exit(1); });