openvibers/openvibe.shared Commits
4.139.0: the menu's feed rows lead with their picture (#327)
Changes
@@@ -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"):@@@ -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 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,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)",@@@ -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">.@@@ -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": {@@@ -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); }@@@ -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^="neassert.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');@@@ -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');
@@ -1,5 +1,13 @@
@@ -1,5 +1,13 @@
# Changelog
# 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
## 4.138.0 — 2026-11-07
- **38 KB off most first visits** (the owner, 11-07: "make things faster and more optimized"):
- **38 KB off most first visits** (the owner, 11-07: "make things faster and more optimized"):
@@ -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.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
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,13 +4,14 @@
@@ -4,13 +4,14 @@
"code": true,
"code": true,
"wave": "W2",
"wave": "W2",
"package": "openvibe-shared",
"package": "openvibe-shared",
- "version": "4.138.0",
+ "version": "4.139.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)",
"contracts": "openvibe-contracts v0.312.0 (devDependency: the release manifest contract)",
"contracts": "openvibe-contracts v0.312.0 (devDependency: the release manifest contract)",
"sdk": "openvibe-sdk v0.94.0",
"sdk": "openvibe-sdk v0.94.0",
"features": [
"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)",
"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)",
"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)",
"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)",
@@ -513,7 +513,7 @@
@@ -513,7 +513,7 @@
const NAV_ICONS_REV = 'd9a4febbbad7'; // sha256(nav-icons.js), first 12 hex: its ?v= (cacheable forever)
const NAV_ICONS_REV = 'd9a4febbbad7'; // sha256(nav-icons.js), first 12 hex: its ?v= (cacheable forever)
// END generated
// END generated
// 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': '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
// 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
// (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">.
// size. Anything unknown, or nav-icons.js failing, falls back to <i class="fa-solid">.
@@ -1,6 +1,6 @@
@@ -1,6 +1,6 @@
{
{
"name": "openvibe-shared",
"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",
"description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site",
"main": "index.js",
"main": "index.js",
"bin": {
"bin": {
@@ -198,7 +198,8 @@
@@ -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 { 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 > svg { width: 16px; height: 16px; }
.ovs-ic.ovs-site { background: none; }
.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 .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-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; }
.ovs-title { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 15px; font-weight: 600; }
@@ -519,6 +520,9 @@
@@ -519,6 +520,9 @@
return;
return;
}
}
if (as === 'rows') {
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) => {
g.items.forEach((it) => {
const li = document.createElement('li'); li.className = 'ovs-item'; li.setAttribute('role', 'presentation');
const li = document.createElement('li'); li.className = 'ovs-item'; li.setAttribute('role', 'presentation');
if (g.section) li.setAttribute('data-sec', g.section);
if (g.section) li.setAttribute('data-sec', g.section);
@@ -528,13 +532,15 @@
@@ -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.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.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); }
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) {
if (it.image) {
const im = document.createElement('img');
const im = document.createElement('img');
im.className = 'ovs-thumb'; im.src = it.image; im.alt = '';
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);
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);
a.appendChild(main);
const q = input.value.trim();
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); }
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); }
@@ -17,7 +17,7 @@ if (process.env.OV_SKIP_BROWSER === '1' || !h.findChrome()) {
@@ -17,7 +17,7 @@ if (process.env.OV_SKIP_BROWSER === '1' || !h.findChrome()) {
process.exit(0);
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: {},
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)] };
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:,">
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
@@ -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');
assert.ok(await until(p, `!!document.querySelector('.ovs.is-open .ovs-group')`), 'the menu opened');
await wait(150);
await wait(150);
assert.deepStrictEqual(await p.evaluate(SHOWN), ['Trending', 'New worlds'], 'by default, the Network\'s first two');
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`);
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');
assert.strictEqual(fold, 'Choose your feeds (4)', 'every card with entries can be chosen; the empty one is not offered');
@@ -163,12 +163,13 @@ const server = http.createServer((req, res) => {
@@ -163,12 +163,13 @@ const server = http.createServer((req, res) => {
], more: { label: 'Watch more', url: 'https://openvibe.video/' } },
], more: { label: 'Watch more', url: 'https://openvibe.video/' } },
] } }); 1`);
] } }); 1`);
await wait(300);
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,
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')) }; })()`);
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.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');
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');
await p.evaluate('OpenVibeSearch.close(); 1');