openvibers/openvibe.shared Commits

4.139.0: the menu's feed rows lead with their picture (#327)

OP OpenVibers committed 6 days ago · caaa980a14 · 8 files, +32 −12

A row with a thumbnail no longer shows the site's icon beside it; the picture is 72 x 40 and the title moves left. In
a group where some rows have a picture, the others carry their icon in a picture-sized tile so the titles line up; a
group with no pictures keeps its icons. SIBLING_REV carries search.js's new hash.

Changes

ddiff --git a/CHANGELOG.md b/CHANGELOG.md
iindex 60e6a68..50113b7 100644
---- a/CHANGELOG.md
++++ b/CHANGELOG.md
@@@ -1,5 +1,13 @@
# Changelog
++## 4.139.0 — 2026-11-07
++
++- **The menu's feed rows lead with their picture.** A row with a thumbnail (Trending on Video, a live channel) no
++ longer shows the site's icon beside it: the picture is larger (72 × 40) and the title moves left. In a group where
++ some rows have a picture, the others carry their icon in a picture-sized tile, so the titles line up. A group with
++ no pictures (New worlds) keeps its icons.
++- `navbar.js`'s SIBLING_REV carries search.js's new hash.
++
## 4.138.0 — 2026-11-07
- **38 KB off most first visits** (the owner, 11-07: "make things faster and more optimized"):
ddiff --git a/README.md b/README.md
iindex 2b398aa..dfe8db6 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.138.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed
++**Status:** alpha, v4.139.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed
consumer installs a tagged release and none keeps a vendored copy
([docs/migration-plan.md](docs/migration-plan.md) is done); each repository's `package.json` names
the tag it pins.
ddiff --git a/STATUS.json b/STATUS.json
iindex 1c086f9..49dcb28 100644
---- a/STATUS.json
++++ b/STATUS.json
@@@ -4,13 +4,14 @@
"code": true,
"wave": "W2",
"package": "openvibe-shared",
-- "version": "4.138.0",
++ "version": "4.139.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)",
"contracts": "openvibe-contracts v0.312.0 (devDependency: the release manifest contract)",
"sdk": "openvibe-sdk v0.94.0",
"features": [
++ "the menu's feed rows with a picture show the picture, not a site icon beside it; a row without one in that group keeps its icon in a picture-sized tile (4.139.0)",
"the footer's shipped line and brand logos load when the footer nears the screen (38 KB off most first visits); the footer, Boost and the glyphs load their files by content hash (4.138.0)",
"every script the bar, the Dock and the notifications load goes by its content hash (?v=), so a release reaches every open page at once instead of up to a day later; one menu button on a phone (4.137.0)",
"the Dock's Agent panel reads the timeline again (the token is resolved when a call is made), and the Dock can open at a step: OpenVibeNavbar.openDock('agent', { task }) marks it and brings it into view (4.134.0)",
ddiff --git a/navbar.js b/navbar.js
iindex 8fee5a3..f723eff 100644
---- a/navbar.js
++++ b/navbar.js
@@@ -513,7 +513,7 @@
const NAV_ICONS_REV = 'd9a4febbbad7'; // sha256(nav-icons.js), first 12 hex: its ?v= (cacheable forever)
// END generated
// The files loaded from beside this one, by content (scripts/sibling-revs.js): a release changes their URLs.
-- const SIBLING_REV = { 'ov-mark.js': '1e91c9c0f347', 'ov-glyphs.js': '1a0d8ce6909f', 'ov-brand.woff2': '3b4e6a8a5878', 'ov-look.js': '2a6c53a7b176', 'ov-fx.js': '46fce1e2b8db', 'ov-labels.js': 'a30b178391f5', 'ov-forms.js': '068ce1b1c4d5', 'ov-forms.css': '98cf0f28bb68', 'ov-share.js': '7d4affbb5093', 'ov-share.css': 'cbd18e6ce991', 'search.js': '08f0452e6a79', 'dock.js': 'e13066db0aac', 'presence.js': '1b675c37d888', 'panels.js': '7d8267ca8035', 'notification-ui.js': '3ceb5266e7b8', 'history.js': '1e6598f78ba4', 'site-switcher.js': '292bea111aa8', 'appbar.js': '1edf7532f228', 'release-watch.js': '25f02a3f0684', 'ov-icons.js': 'ba3981246bc0', 'sso-client.js': '52439265b980' };
++ const SIBLING_REV = { 'ov-mark.js': '1e91c9c0f347', 'ov-glyphs.js': '1a0d8ce6909f', 'ov-brand.woff2': '3b4e6a8a5878', 'ov-look.js': '2a6c53a7b176', 'ov-fx.js': '46fce1e2b8db', 'ov-labels.js': 'a30b178391f5', 'ov-forms.js': '068ce1b1c4d5', 'ov-forms.css': '98cf0f28bb68', 'ov-share.js': '7d4affbb5093', 'ov-share.css': 'cbd18e6ce991', 'search.js': 'c9c68464d19e', 'dock.js': 'e13066db0aac', 'presence.js': '1b675c37d888', 'panels.js': '7d8267ca8035', 'notification-ui.js': '3ceb5266e7b8', 'history.js': '1e6598f78ba4', 'site-switcher.js': '292bea111aa8', 'appbar.js': '1edf7532f228', 'release-watch.js': '25f02a3f0684', 'ov-icons.js': 'ba3981246bc0', 'sso-client.js': '52439265b980' };
// The navbar's icons are filled paths (no Font Awesome CSS needed): the ones above ship here, the rest
// (nav-icons.js) load after first paint when a link, chip or menu row names one, into an empty <svg> of their
// size. Anything unknown, or nav-icons.js failing, falls back to <i class="fa-solid">.
ddiff --git a/package.json b/package.json
iindex d4e0676..2adb867 100644
---- a/package.json
++++ b/package.json
@@@ -1,6 +1,6 @@
{
"name": "openvibe-shared",
-- "version": "4.138.0",
++ "version": "4.139.0",
"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 6c55e90..abf0979 100644
---- a/search.js
++++ b/search.js
@@@ -198,7 +198,8 @@
.ovs-ic { flex: none; display: grid; place-items: center; width: 32px; height: 32px; border-radius: 10px; background: color-mix(in srgb, var(--accent, #3b82f6) 16%, transparent); color: var(--accent-light, #8ab4ff); font-size: 12px; font-weight: 800; }
.ovs-ic > svg { width: 16px; height: 16px; }
.ovs-ic.ovs-site { background: none; }
--.ovs-thumb { flex: none; width: 64px; height: 36px; border-radius: 9px; object-fit: cover; background: color-mix(in srgb, var(--text-primary, #e8ecf3) 6%, transparent); }
++.ovs-thumb { flex: none; width: 72px; height: 40px; border-radius: 9px; object-fit: cover; background: color-mix(in srgb, var(--text-primary, #e8ecf3) 6%, transparent); }
++.ovs-thumb--ico { display: grid; place-items: center; }
.ovs .ov-icon:empty { border-radius: 50%; box-shadow: inset 0 0 0 2px color-mix(in srgb, var(--accent, #3b82f6) 26%, transparent); background: color-mix(in srgb, var(--accent, #3b82f6) 10%, transparent); }
.ovs-main { flex: 1; min-width: 0; display: grid; gap: 1px; }
.ovs-title { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 15px; font-weight: 600; }
@@@ -519,6 +520,9 @@
return;
}
if (as === 'rows') {
++ // A row with a picture needs no site icon beside it (4.139.0); in a group where some rows have one, the
++ // others carry their icon in a picture-sized tile so the titles line up.
++ const pictured = g.items.some((x) => x.image);
g.items.forEach((it) => {
const li = document.createElement('li'); li.className = 'ovs-item'; li.setAttribute('role', 'presentation');
if (g.section) li.setAttribute('data-sec', g.section);
@@@ -528,13 +532,15 @@
if (it.tag) { const tg = document.createElement('i'); tg.className = 'ovs-tag'; tg.textContent = it.tag; t.appendChild(tg); }
if (it.meta) { const m = document.createElement('span'); m.className = 'ovs-meta'; m.textContent = it.meta; main.appendChild(m); }
if (it.snippet) { const sn = document.createElement('span'); sn.className = 'ovs-snip'; sn.textContent = it.snippet; main.appendChild(sn); }
-- a.append(icon(it));
if (it.image) {
const im = document.createElement('img');
im.className = 'ovs-thumb'; im.src = it.image; im.alt = '';
-- im.width = 64; im.height = 36; im.loading = 'lazy'; im.decoding = 'async';
++ im.width = 72; im.height = 40; im.loading = 'lazy'; im.decoding = 'async';
a.appendChild(im);
-- }
++ } else if (pictured) {
++ const tile = document.createElement('span'); tile.className = 'ovs-thumb ovs-thumb--ico';
++ tile.append(icon(it)); a.appendChild(tile);
++ } else a.append(icon(it));
a.appendChild(main);
const q = input.value.trim();
if (q && view !== 'results' && (it.kind === 'results' || (items.length === 1 && exact(it, q)))) { const k = document.createElement('span'); k.className = 'ovs-key'; k.textContent = 'Enter'; a.appendChild(k); }
ddiff --git a/test/menu-feeds-chrome.test.js b/test/menu-feeds-chrome.test.js
iindex bc421e9..d2fd2e5 100644
---- a/test/menu-feeds-chrome.test.js
++++ b/test/menu-feeds-chrome.test.js
@@@ -17,7 +17,7 @@ if (process.env.OV_SKIP_BROWSER === '1' || !h.findChrome()) {
process.exit(0);
}
--const card = (site, id, title, n) => ({ id, site, title, kind: 'rows', items: Array.from({ length: n }, (_, i) => ({ title: `${title} ${i + 1}`, url: `https://openvibe.${site === 'media-hub' ? 'video' : site}/x/${i}`, meta: `${i + 1} things` })) });
++const card = (site, id, title, n) => ({ id, site, title, kind: 'rows', items: Array.from({ length: n }, (_, i) => ({ title: `${title} ${i + 1}`, url: `https://openvibe.${site === 'media-hub' ? 'video' : site}/x/${i}`, meta: `${i + 1} things`, ...(site === 'media-hub' && i < 2 ? { image: { src: `https://openvibe.video/t/${i}.jpg` } } : {}) })) });
const FRAME = { nav: [{ id: 'live', name: 'Live', url: 'https://openvibe.live/', icon: 'live', group: 'watch' }], groups: [{ id: 'watch', name: 'Watch' }], soon: [], footer: {},
cards: [card('media-hub', 'trending', 'Trending', 3), card('games', 'new-worlds', 'New worlds', 2), card('news', 'top', 'Top stories', 4), card('today', 'today', 'Today', 2), card('bad', 'empty', 'Nothing', 0)] };
const PAGE_IN = (saved) => `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Feeds</title><link rel="icon" href="data:,">
@@@ -65,6 +65,10 @@ const SHOWN = `[...document.querySelectorAll('.ovs-list .ovs-group[data-sec^="ne
assert.ok(await until(p, `!!document.querySelector('.ovs.is-open .ovs-group')`), 'the menu opened');
await wait(150);
assert.deepStrictEqual(await p.evaluate(SHOWN), ['Trending', 'New worlds'], 'by default, the Network\'s first two');
++ // 4.139.0: a row with a picture has no site icon beside it; in its group a row without one carries the icon in a
++ // picture-sized tile, so the titles line up. A group with no pictures keeps plain icons.
++ const looks = await p.evaluate(`[...document.querySelectorAll('.ovs-list .ovs-group[data-sec^="net:"]')].map((g) => [...document.querySelectorAll('.ovs-list li.ovs-item')].filter((li) => li.getAttribute('data-sec') === g.getAttribute('data-sec')).map((li) => { const a = li.firstElementChild; const first = a && a.firstElementChild; return first ? (first.tagName === 'IMG' ? 'img' : first.classList.contains('ovs-thumb--ico') ? 'tile' : first.classList.contains('ovs-ic') ? 'icon' : first.className) : '?'; }))`);
++ assert.deepStrictEqual(looks, [['img', 'img', 'tile'], ['icon', 'icon']], 'pictures replace icons; titles line up');
const fold = await p.evaluate(`(document.querySelector('[data-ovs-fold="feeds"]') || {}).textContent`);
assert.strictEqual(fold, 'Choose your feeds (4)', 'every card with entries can be chosen; the empty one is not offered');
ddiff --git a/test/search-loading-chrome.test.js b/test/search-loading-chrome.test.js
iindex b10e108..7198589 100644
---- a/test/search-loading-chrome.test.js
++++ b/test/search-loading-chrome.test.js
@@@ -163,12 +163,13 @@ const server = http.createServer((req, res) => {
], more: { label: 'Watch more', url: 'https://openvibe.video/' } },
] } }); 1`);
await wait(300);
-- v = await p.evaluate(`(() => { const imgs = [...document.querySelectorAll('.ovs-thumb')]; const row = imgs[0] && imgs[0].closest('.ovs-item');
-- return { count: imgs.length, attrs: imgs[0] && ['src', 'width', 'height', 'loading', 'decoding', 'alt'].map((k) => imgs[0].getAttribute(k)),
++ v = await p.evaluate(`(() => { const imgs = [...document.querySelectorAll('img.ovs-thumb')]; const tiles = document.querySelectorAll('.ovs-thumb--ico').length; const row = imgs[0] && imgs[0].closest('.ovs-item');
++ return { count: imgs.length, tiles, attrs: imgs[0] && ['src', 'width', 'height', 'loading', 'decoding', 'alt'].map((k) => imgs[0].getAttribute(k)),
href: row && row.querySelector('a').getAttribute('href'), title: row && row.querySelector('.ovs-title').textContent,
beforeTitle: !!(imgs[0] && imgs[0].nextElementSibling && imgs[0].nextElementSibling.classList.contains('ovs-main')) }; })()`);
assert.strictEqual(v.count, 1, 'only the https picture is drawn');
-- assert.deepStrictEqual(v.attrs, ['https://openvibe.media/t/1.jpg?w=640', '64', '36', 'lazy', 'async', ''], 'a 64x36 lazy thumbnail with empty alt');
++ assert.ok(v.tiles >= 1, 'the rows without a picture in that group keep their icon in a picture-sized tile (4.139.0)');
++ assert.deepStrictEqual(v.attrs, ['https://openvibe.media/t/1.jpg?w=640', '72', '40', 'lazy', 'async', ''], 'a 72x40 lazy thumbnail with empty alt (4.139.0)');
assert.deepStrictEqual([v.href, v.title, v.beforeTitle], ['https://openvibe.video/v/1', 'A walk', true], 'the same row, picture before its title');
await p.evaluate('OpenVibeSearch.close(); 1');