openvibers/openvibe.shared Commits

4.155.0: the menu names each site by a short product name, never a tagline (#348)

OP OpenVibers committed 5 days ago · aefaddc398 · 7 files, +128 −11

On openvibe.website the menu's first tab read 'Websites that l…' and the box 'Search Websites that look finished, or
a…': the page title's last part. The name now comes from the site's short label, the network's name for the address
(path-aware: openvibe.my/inventory is Inventory, openvibe.my is not), the OpenVibe.X in the title, then the address;
a title part only when short. Website, My, Link, Party and Computer read right; every other site is unchanged.

Changes

ddiff --git a/CHANGELOG.md b/CHANGELOG.md
iindex 04e741a..0dd2756 100644
---- a/CHANGELOG.md
++++ b/CHANGELOG.md
@@@ -1,5 +1,18 @@
# Changelog
++## 4.155.0 — 2026-11-08
++
++- **The menu names each site by a short product name** (the owner, 11-08: "the names of websites … not cut off …
++ cleaner and nicer across all the websites"). On openvibe.website the first tab read "Websites that l…", the box
++ "Search Websites that look finished, or a…" and the heading "On Websites that look finished": the page title's last
++ part, a tagline. The name now comes, in order, from the site's own short label, the network's name for the address,
++ the "OpenVibe.X" in the title, then the address itself (openvibe.website → Website, openvibe.party → Party), and a
++ title part only when it is short. Website, My, Link, Party and Computer read right now; every other site keeps its name.
++- **A site under a path is that site only on that path:** Inventory lives at openvibe.my/inventory, so openvibe.my
++ itself is My, not Inventory (the match was by host alone).
++- Tests: `test/search-site-name-chrome.test.js` (a tagline title, a long label, and a path-aware site; it fails on
++ 4.154.0 with the owner's exact text).
++
## 4.154.0 — 2026-11-08
- **Float your agent's screen** (the owner: overlays "usable for agents, Vibe chat, Codes"). The Dock's Agents tab
ddiff --git a/README.md b/README.md
iindex 499d746..a4b5919 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.154.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed
++**Status:** alpha, v4.155.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 ef8fdb6..49fab01 100644
---- a/STATUS.json
++++ b/STATUS.json
@@@ -4,7 +4,7 @@
"code": true,
"wave": "W2",
"package": "openvibe-shared",
-- "version": "4.154.0",
++ "version": "4.155.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)",
ddiff --git a/navbar.js b/navbar.js
iindex 14a031f..d81c0e4 100644
---- a/navbar.js
++++ b/navbar.js
@@@ -539,7 +539,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': 'fa4ded2928f4', 'ov-share.css': 'cbd18e6ce991', 'search.js': 'c036db4ac93a', 'dock.js': '4dacf614797b', 'presence.js': '1b675c37d888', 'panels.js': '7d8267ca8035', 'notification-ui.js': 'a2d37237ba0b', 'history.js': '1e6598f78ba4', 'site-switcher.js': '292bea111aa8', 'appbar.js': '1edf7532f228', 'release-watch.js': '25f02a3f0684', 'ov-icons.js': 'ba3981246bc0', 'sso-client.js': '52439265b980', 'overlay.js': '25960cf0c075' };
++ 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': 'fa4ded2928f4', 'ov-share.css': 'cbd18e6ce991', 'search.js': 'b2e5ef9b3773', 'dock.js': '4dacf614797b', 'presence.js': '1b675c37d888', 'panels.js': '7d8267ca8035', 'notification-ui.js': 'a2d37237ba0b', 'history.js': '1e6598f78ba4', 'site-switcher.js': '292bea111aa8', 'appbar.js': '1edf7532f228', 'release-watch.js': '25f02a3f0684', 'ov-icons.js': 'ba3981246bc0', 'sso-client.js': '52439265b980', 'overlay.js': '25960cf0c075' };
// 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 039a507..bfa87b3 100644
---- a/package.json
++++ b/package.json
@@@ -1,6 +1,6 @@
{
"name": "openvibe-shared",
-- "version": "4.154.0",
++ "version": "4.155.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 43ebc8b..af4471e 100644
---- a/search.js
++++ b/search.js
@@@ -112,9 +112,18 @@
return FALLBACK_SITES;
}
/** The site this page belongs to: the longest matching host (play.openvibe.network is Play, not Network). */
++ // The site this page is on: the longest matching address, and a site that lives under a path (openvibe.my/inventory)
++ // only on that path.
function hereSite(list) {
-- const h = location.hostname.toLowerCase(); let best = null, len = 0;
-- for (const s of list) { const sh = hostOf(s.url); if (sh && (h === sh || h.endsWith(`.${sh}`)) && sh.length > len) { best = s; len = sh.length; } }
++ const h = location.hostname.toLowerCase(), at = location.pathname || '/'; let best = null, score = 0;
++ for (const s of list) {
++ let u; try { u = new URL(s.url); } catch { continue; }
++ const sh = u.hostname, sp = u.pathname.replace(/\/+$/, '');
++ if (!sh || !(h === sh || h.endsWith(`.${sh}`))) continue;
++ if (sp && at !== sp && !at.startsWith(`${sp}/`)) continue;
++ const n = sh.length * 1000 + sp.length + 1;
++ if (n > score) { best = s; score = n; }
++ }
return best;
}
/** The person's own use of each site (their history's counts), for "your sites" and for ranking results. */
@@@ -678,14 +687,27 @@
if (typeof ctx.icons === 'function') { try { ctx.icons(listEl); } catch { /* the icons are optional */ } }
}
-- // This site's short name for the field and the headings: the frame's name for this address ("Download"), not the page
-- // title ("OpenVibe.Download: a free 25 GB drive with share links" was cut off in the field and ran over two lines).
-- const shortName = (t) => String(t || '').split(/\s[·|–—-]\s|:\s/)[0].replace(/^OpenVibe\./, '').trim().slice(0, 28);
++ // This site's short name for the field, the first tab and the headings: a product name ("Download", "Space"), never a
++ // page title or a tagline ("OpenVibe.Download: a free 25 GB drive with share links" ran over two lines; on 11-08 the
++ // tab read "Websites that l…", openvibe.website's tagline). In order: the site's own short label, the network's name
++ // for this address (path-aware: openvibe.my/inventory is Inventory, openvibe.my itself is not), the "OpenVibe.X" in
++ // the title, the address (openvibe.website → Website, connect.openvibe.link → Connect), then a short title segment.
++ const SHORT_NAME = 18;
++ const shortName = (t) => String(t || '').split(/\s[·|–—-]\s|:\s/)[0].replace(/^OpenVibe\./, '').trim();
++ const brief = (t) => { const v = shortName(t); return v && v.length <= SHORT_NAME ? v : ''; };
++ const capital = (w) => (w ? w.charAt(0).toUpperCase() + w.slice(1) : '');
++ function addressName(host) {
++ const p = String(host || '').toLowerCase().replace(/^www\./, '').split('.');
++ if (p.length === 2 && p[0] === 'openvibe') return p[1] === 'ai' ? 'AI' : capital(p[1]);
++ if (p.length >= 3 && p[p.length - 2] === 'openvibe' && /^[a-z][a-z-]{1,17}$/.test(p[0])) return capital(p[0]);
++ return '';
++ }
const here = () => {
-- if (ctx.scope && ctx.scope.label) return shortName(ctx.scope.label);
const f = ctx.frame, list = f && Array.isArray(f.nav) ? f.nav.map((n) => ({ name: n.name, url: safeUrl(n.url) })) : [];
const s = list.length ? hereSite(list) : null;
-- return (s && s.name) || shortName(document.title.split(/[·|–-]/).pop()) || location.hostname;
++ const titled = /OpenVibe\.([A-Za-z][A-Za-z0-9-]{0,17})\b/.exec(document.title || '');
++ return brief(ctx.scope && ctx.scope.label) || (s && brief(s.name)) || (titled && titled[1]) || addressName(location.hostname)
++ || brief(String(document.title || '').split(/\s[·|–—-]\s/).pop()) || location.hostname;
};
const siteItem = (s, all) => ({ title: s.name, meta: s.desc, url: s.url, site: s.icon || s.id, kind: 'site', here: all && s === hereSite(all) });
function pagesOf(q) {
ddiff --git a/test/search-site-name-chrome.test.js b/test/search-site-name-chrome.test.js
nnew file mode 100644
iindex 0000000..73034c6
---- /dev/null
++++ b/test/search-site-name-chrome.test.js
@@@ -0,0 +1,82 @@
++'use strict';
++// The menu names the site it is on by a short product name (4.155.0). On 11-08 the owner sent openvibe.website's menu:
++// its first tab read "Websites that l…" and the box "Search Websites that look finished, or a…", the site's tagline
++// taken from the page title. Now: the site's own short label, the network's name for the address (a site under a path,
++// like openvibe.my/inventory, only on that path), the "OpenVibe.X" in the title, then the address; a tagline never.
++// In a real headless Chrome; skipped without one.
++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('search-site-name-chrome: skipped (no Chrome; set CHROME_BIN)');
++ process.exit(0);
++}
++
++const ROOT = path.join(__dirname, '..');
++const page = (title, search) => `<!doctype html><html lang="en"><head><meta charset="utf-8"><title>${title}</title><link rel="icon" href="data:,"></head>
++<body><div id="navbar-mount"></div><main></main><script src="/navbar.js"></script>
++<script>
++ window.__realFetch = window.fetch;
++ window.fetch = async (u, init) => {
++ const url = new URL(String(u), location.href);
++ // The network's sites (https only, as the navbar keeps them): an account hub at localhost's root and Inventory under
++ // /inventory there. The other pages are on 127.0.0.1, an address the list does not know.
++ if (url.hostname === 'openvibe.network' && url.pathname === '/api/frame') return new Response(JSON.stringify({
++ nav: [{ name: 'My', url: 'https://localhost/' }, { name: 'Inventory', url: 'https://localhost/inventory' }] }), { headers: { 'content-type': 'application/json' } });
++ if (url.hostname === 'openvibe.network' || url.hostname === 'search.openvibe.network') return new Response('{}', { status: 404 });
++ return window.__realFetch(u, init);
++ };
++ OpenVibeNavbar.init({ service: 'test', user: null, links: [{ label: 'Home', href: '/' }], search: ${JSON.stringify(search || {})} });
++</script></body></html>`;
++const PAGES = {
++ '/website': page('OpenVibe.Website · Websites that look finished'),
++ '/long-label': page('Templates · OpenVibe.Space', { label: 'Websites that look finished, from the first preview' }),
++ '/inventory/items': page('Your items'),
++ '/account': page('Account settings and everything else you own'),
++};
++const server = http.createServer((req, res) => {
++ const u = new URL(req.url, 'http://x');
++ if (PAGES[u.pathname]) { res.writeHead(200, { 'content-type': 'text/html' }); return res.end(PAGES[u.pathname]); }
++ const name = u.pathname.slice(1);
++ if (/^[a-z-]+\.js$/.test(name) && fs.existsSync(path.join(ROOT, name))) { res.writeHead(200, { 'content-type': 'application/javascript' }); return res.end(fs.readFileSync(path.join(ROOT, name))); }
++ res.writeHead(404); return res.end();
++});
++
++(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-search-name-'));
++ const browser = await h.launch({ tmpDir });
++ const wait = (ms) => new Promise((r) => setTimeout(r, ms));
++ const seen = {};
++ try {
++ for (const at of Object.keys(PAGES)) {
++ const context = await browser.newContext();
++ const p = await h.openPage(browser, context, { width: 390 });
++ const origin = /^\/(inventory|account)/.test(at) ? base.replace('127.0.0.1', 'localhost') : base;
++ await p.goto(`${origin}${at}`, { maxMs: 5000 });
++ await wait(300);
++ await p.evaluate('document.dispatchEvent(new KeyboardEvent("keydown", { key: "/", bubbles: true })); 1');
++ for (let i = 0; i < 50 && !(await p.evaluate('!!document.querySelector(".ovs.is-open .ovs-input")')); i++) await wait(100);
++ await wait(300);
++ seen[at] = await p.evaluate(`({ placeholder: document.querySelector('.ovs-input').placeholder,
++ tab: (document.querySelector('.ovs-tab[data-tab]') || {}).textContent || '',
++ group: ((document.querySelector('.ovs-group') || {}).firstChild || {}).textContent || '' })`);
++ }
++ const name = (at) => seen[at].placeholder.replace(/^Search /, '').replace(/, or ask Vibe$/, '');
++ assert.strictEqual(name('/website'), 'Website', `a tagline title gives the OpenVibe.X name: ${JSON.stringify(seen['/website'])}`);
++ assert.ok(!/look finished/.test(seen['/website'].tab + seen['/website'].group), 'the tagline is nowhere in the tab or the heading');
++ assert.strictEqual(name('/long-label'), 'Space', 'a long label is passed over for the name in the title');
++ assert.strictEqual(name('/inventory/items'), 'Inventory', 'under /inventory the site is Inventory');
++ assert.strictEqual(name('/account'), 'My', 'at the root of the same address it is not Inventory');
++ for (const at of Object.keys(seen)) assert.ok(name(at).length <= 18, `${at}: a short name (${name(at)})`);
++ console.log('search-site-name-chrome: short product names (tagline title, long label, path-aware sites) ok');
++ } finally {
++ await browser.close();
++ server.close();
++ }
++})().catch((e) => { console.error(e); process.exit(1); });