openvibers/openvibe.sites Commits

openvibe-shared v2.15.0 (was v2.13.2); notices rebuilt (#25)

OP OpenVibers committed 8 Oct 2026 · e528cb8675 · 9 files, +99 −41

Changes

ddiff --git a/README.md b/README.md
iindex f8d65ed..205f148 100644
---- a/README.md
++++ b/README.md
@@@ -38,5 +38,5 @@ Report security issues through [SECURITY.md](SECURITY.md).
<!-- versions:start -->
- openvibe-contracts: v0.117.0
--- openvibe-shared: v2.13.2
++- openvibe-shared: v2.15.0
<!-- versions:end -->
ddiff --git a/STATUS.json b/STATUS.json
iindex 1bdff80..7205d76 100644
---- a/STATUS.json
++++ b/STATUS.json
@@@ -26,7 +26,7 @@
"status.openvibe.network (status notice)"
],
"contracts": "openvibe-contracts v0.117.0: sites.json mirrors products.catalog() (scripts/sync-catalog.js)",
-- "shared": "openvibe-shared v2.13.2",
++ "shared": "openvibe-shared v2.15.0",
"deployed": true,
"deployment": "Deployed with `sudo ovhost deploy sites --restart` (OpenVibe.Host strategy static-build since the 2026-09-27 cutover): /opt/openvibe.sites, nginx serves dist/<domain>/ through deploy/nginx/<domain>.conf; no unit, no port, no env file",
"features": [
ddiff --git a/dist/_shared/navbar.js b/dist/_shared/navbar.js
iindex 11aeba5..7624f44 100644
---- a/dist/_shared/navbar.js
++++ b/dist/_shared/navbar.js
@@@ -8,17 +8,7 @@
onLogin: null, onLogout: null, loginUrl: null, sessionUrl: null, logoutUrl: null,
brand: null, brandName: null, brandIcon: null, compact: 'auto',
links: null, menu: null, recent: true,
-- // history: { type: 'tool'|'stream'|'paste'|'page'|…, title, url, icon } — recorded for the
-- // signed-in user once auth resolves (cross-site "Recently used" / History on the Network).
-- history: null,
-- // silentLogin: 'https://site/auth/login?silent=1&next={url}' — when nobody is signed in here
-- // but this browser has signed in to the network before (ov_sso_hint), try one silent
-- // prompt=none round trip per tab so a session on one site becomes a session on all.
-- silentLogin: null,
-- // fedcm: false to opt out; 'optional' (default) shows the browser's native chip the first
-- // time and re-authenticates silently afterwards; 'silent' only re-authenticates.
-- fedcm: 'optional',
-- fedcmLogin: null, // POST target for the assertion (default: this site's /auth/fedcm)
++ history: null, silentLogin: null, fedcm: 'optional', fedcmLogin: null, // README.md, "The navbar"
};
const _runtimeMenu = { before: [], after: [] };
let _runtimeLinks = null;
@@@ -37,6 +27,19 @@
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
color: var(--text-primary, #e0e0e0);
}
++ /* At the very top the bar fades into the page (no background, no border); a scroll brings the frosted bar back.
++ A page whose hero runs under the bar opts in with body.ov-nav-overlay: the bar then sits over the hero, with a
++ soft scrim for the text. ovnav-top is kept by the navbar itself on every render and scroll. */
++ .openvibe-navbar { transition: background-color .45s cubic-bezier(.2,.7,.2,1), border-color .45s cubic-bezier(.2,.7,.2,1), box-shadow .45s cubic-bezier(.2,.7,.2,1), -webkit-backdrop-filter .45s, backdrop-filter .45s; }
++ .openvibe-navbar::before { content: ""; position: absolute; inset: 0 0 -28px; pointer-events: none; z-index: -1; opacity: 0; transition: opacity .45s cubic-bezier(.2,.7,.2,1); background: linear-gradient(180deg, rgba(4,7,14,.62), rgba(4,7,14,.28) 55%, rgba(4,7,14,0)); }
++ .openvibe-navbar:not(.ovnav-top) { background-color: color-mix(in srgb, var(--bg-secondary, #252530) 84%, transparent); -webkit-backdrop-filter: saturate(1.5) blur(16px); backdrop-filter: saturate(1.5) blur(16px); box-shadow: 0 12px 32px -14px rgba(0,0,0,.6); }
++ .openvibe-navbar.ovnav-top { background-color: transparent; border-bottom-color: transparent; box-shadow: none; -webkit-backdrop-filter: none; backdrop-filter: none; }
++ /* The bar is sticky, but a #navbar-mount wrapper exactly its height gave it no room to stick in: the wrapper sticks. */
++ #navbar-mount { position: sticky; top: 0; z-index: 10000; }
++ body.ov-nav-overlay .openvibe-navbar.ovnav-top::before { opacity: 1; }
++ body.ov-nav-overlay > #navbar-mount, body.ov-nav-overlay > .openvibe-navbar { margin-bottom: calc(-1 * var(--ovnav-real-h, 52px)); }
++ body.ov-panel-open .openvibe-navbar.ovnav-top { background-color: var(--bg-secondary, #252530); border-bottom-color: var(--border, #333340); }
++ @media (prefers-reduced-motion: reduce) { .openvibe-navbar, .openvibe-navbar::before { transition: none; } }
.openvibe-navbar-brand { display: flex; align-items: center; gap: 9px; text-decoration: none; color: inherit; margin-right: 8px; min-width: 0; }
/* Brand group: mark + linked wordmark + launcher read as one control. Every property a host
page might set on bare "nav a" / "a" is reset here, so the brand looks the same everywhere. */
@@@ -592,6 +595,10 @@
} catch { return null; }
}
++ // A render's document/window listeners and panels; the next render (each boost page move) aborts them, so the old bar can go.
++ let _renderAbort = null;
++ const onDocument = () => (_renderAbort ? { signal: _renderAbort.signal } : undefined);
++
function bindLauncher(nav) {
const btn = nav.querySelector('#openvibe-launcher-btn'); if (!btn) return;
let panel = null;
@@@ -646,7 +653,7 @@
ev.preventDefault(); const n = i + (ev.key === 'ArrowDown' ? 1 : -1); (links[n] || (n < 0 ? input : links[0])).focus();
});
panel.addEventListener('click', (ev) => ev.stopPropagation());
-- document.addEventListener('click', close);
++ document.addEventListener('click', close, onDocument());
launcherCatalog().then((c) => { if (c) { cat = c; paint(cat, input.value.trim().toLowerCase()); } });
}
panel.classList.add('open'); btn.setAttribute('aria-expanded', 'true');
@@@ -697,7 +704,7 @@
}
closeDrawer();
});
-- document.addEventListener('click', (e) => { if (!e.target.closest || !e.target.closest('.ovnav-dd')) nav.querySelectorAll('.ovnav-dd.open').forEach(x => x.classList.remove('open')); });
++ document.addEventListener('click', (e) => { if (!e.target.closest || !e.target.closest('.ovnav-dd')) nav.querySelectorAll('.ovnav-dd.open').forEach(x => x.classList.remove('open')); }, onDocument());
// Dropdowns hang below the links row, which scrolls sideways and would clip them: fixed, under the link (less a filtered bar's offset).
const placeDd = (dd) => {
const m = dd && dd.querySelector(':scope>.ovnav-dd-menu'); if (!m) return;
@@@ -710,7 +717,7 @@
nav.addEventListener('pointerover', place); nav.addEventListener('focusin', place);
if (burger && drawer) {
burger.addEventListener('click', (e) => { e.stopPropagation(); const open = drawer.classList.toggle('open'); burger.setAttribute('aria-expanded', String(open)); });
-- document.addEventListener('click', (e) => { if (drawer.classList.contains('open') && !drawer.contains(e.target) && !burger.contains(e.target)) closeDrawer(); });
++ document.addEventListener('click', (e) => { if (drawer.classList.contains('open') && !drawer.contains(e.target) && !burger.contains(e.target)) closeDrawer(); }, onDocument());
regPanel(drawer, 'nav-drawer', closeDrawer);
}
}
@@@ -720,7 +727,7 @@
const _panelQueue = [];
function regPanel(el, id, close) {
if (!el) return;
-- const opts = { el, openClass: 'open', id, close };
++ const opts = Object.assign({ el, openClass: 'open', id, close }, onDocument());
if (root.OpenVibePanels) return root.OpenVibePanels.register(opts);
_panelQueue.push(opts);
if (document.getElementById('ov-panels-loader')) return;
@@@ -745,7 +752,7 @@
const LABEL = { text: { 100: 'Default', 112: 'Large', 125: 'Largest' }, motion: { auto: 'On', reduced: 'Calm' } };
const paint = () => { const d = L.display.get(); rows.forEach(r => { const k = r.getAttribute('data-ov-display'); r.querySelector('.ud-val').textContent = LABEL[k][d[k]] || ''; }); };
rows.forEach(r => r.addEventListener('click', (e) => { e.stopPropagation(); const k = r.getAttribute('data-ov-display'), o = L.display.options[k], cur = L.display.get()[k]; L.display.set({ [k]: o[(o.indexOf(cur) + 1) % o.length] }); paint(); }));
-- root.addEventListener('ov:display', paint); paint();
++ root.addEventListener('ov:display', paint, onDocument()); paint();
}
/** OpenCoins balance in the menu header (one request when the menu first opens). */
@@@ -784,7 +791,7 @@
box.hidden = false;
};
box.addEventListener('click', (e) => { const b = e.target.closest('button[data-k]'); if (!b) return; L.display.set({ [b.dataset.k]: b.dataset.v }); paint(); });
-- root.addEventListener('ov:display', paint);
++ root.addEventListener('ov:display', paint, onDocument());
paint();
}
@@@ -1319,6 +1326,8 @@
// A site may put state classes on the bar (Live's transparent hero mode). A re-render must not lose them.
let carried = []; try { carried = Array.from((_navEl && _navEl.classList) || []).filter(c => c !== 'openvibe-navbar'); } catch { carried = []; }
if (_navEl) _navEl.remove();
++ if (_renderAbort) { _renderAbort.abort(); _panelQueue.length = 0; }
++ _renderAbort = typeof AbortController === 'function' ? new AbortController() : null;
const nav = document.createElement('nav');
nav.className = 'openvibe-navbar';
@@@ -1335,8 +1344,7 @@
const loginHref = resolveLoginHref(currentHost(), window.location.href);
const addAccountHref = `${_config.apiBase}/login?add_account=1&return=${encodeURIComponent(window.location.href)}`;
-- // The OV brand mark is a self-contained drop-in (mounts every .ov-mark it finds). A page may already carry its own
-- // async <script src=".../ov-mark.js?v=…"> that has not run yet: never load a second copy.
++ // The OV brand mark is a drop-in (mounts every .ov-mark); never a second copy of a page's own tag.
if (!window.__ovMark && !document.getElementById('ov-mark-loader') && !(document.querySelector && document.querySelector('script[src*="/ov-mark.js"]'))) {
try { const sc = document.createElement('script'); sc.id = 'ov-mark-loader'; sc.src = sibling('ov-mark.js'); sc.async = true; document.head.appendChild(sc); } catch { /* */ }
}
@@@ -1444,7 +1452,7 @@
// Close on outside click
document.addEventListener('click', e => {
if (!nav.contains(e.target)) dropdown.classList.remove('open');
-- });
++ }, onDocument());
// Account switching
dropdown.querySelectorAll('[data-account-id]').forEach(el => {
@@@ -1494,9 +1502,31 @@
}
_navEl = nav;
attachAvatarFallbacks(nav);
++ hookTop();
return nav;
}
++ // The bar's at-the-top state (ovnav-top, see injectStyles), on every render and scroll. One window listener pair for
++ // the page's life, a frame at most per update; --ovnav-real-h is the bar's height for overlay pages.
++ let _topHooked = false;
++ function syncTop() {
++ if (!_navEl) return;
++ _navEl.classList.toggle('ovnav-top', (root.scrollY || (document.documentElement && document.documentElement.scrollTop) || 0) <= 4);
++ // A fixed bar (a site that positions it itself, like Live) takes no room, so an overlay page needs no pull-up.
++ const fixed = root.getComputedStyle && root.getComputedStyle(_navEl).position === 'fixed';
++ const de = document.documentElement;
++ if (de && de.style) de.style.setProperty('--ovnav-real-h', `${fixed ? 0 : (_navEl.offsetHeight || 52)}px`);
++ }
++ function hookTop() {
++ syncTop();
++ if (_topHooked || typeof root.addEventListener !== 'function') return;
++ _topHooked = true;
++ let raf = 0;
++ const later = () => { if (!raf) raf = (root.requestAnimationFrame || setTimeout)(() => { raf = 0; syncTop(); }); };
++ root.addEventListener('scroll', later, { passive: true });
++ root.addEventListener('resize', later, { passive: true });
++ }
++
// openvibe-shared/boost swapped the page in place: this is a new page for the history and the
// active-link state, and a login from here returns here.
let _boostHooked = false;
ddiff --git a/dist/_shared/panels.js b/dist/_shared/panels.js
iindex 6865d97..692df15 100644
---- a/dist/_shared/panels.js
++++ b/dist/_shared/panels.js
@@@ -8,18 +8,22 @@
* • <body class="ov-panel-open"> while any panel is open (floating buttons step aside)
* • a freshly opened panel starts scrolled to the top
*
-- * OpenVibePanels.register({ el, openClass: 'show', close: () => …, id?: 'user-menu' })
-- * OpenVibePanels.closeAll(exceptEl?) · OpenVibePanels.anyOpen()
++ * OpenVibePanels.register({ el, openClass: 'show', close: () => …, id?: 'user-menu', signal? }) → unregister()
++ * OpenVibePanels.unregister(el) · OpenVibePanels.closeAll(exceptEl?) · OpenVibePanels.anyOpen()
*
* It watches the panel's class instead of wrapping the site's click handlers, so it cannot fight them: a
* panel is "open" exactly when its element has `openClass`, however that came about.
++ *
++ * A registration holds its element until it is unregistered: markup that is thrown away and rebuilt (the navbar
++ * re-renders on every openvibe-shared/boost page move) passes an AbortSignal, or calls unregister, so the old
++ * element can be collected.
*/
(function (root) {
'use strict';
if (root.OpenVibePanels) return;
-- if (typeof document === 'undefined') { const stub = { register() {}, closeAll() {}, anyOpen: () => false }; if (typeof module !== 'undefined') module.exports = stub; return; }
++ if (typeof document === 'undefined') { const stub = { register: () => () => {}, unregister() {}, closeAll() {}, anyOpen: () => false }; if (typeof module !== 'undefined') module.exports = stub; return; }
-- const panels = []; // { el, openClass, close, id }
++ const panels = []; // { el, openClass, close, id, unregister }
const isOpen = (p) => p.el.isConnected && p.el.classList.contains(p.openClass);
const anyOpen = () => panels.some(isOpen);
const syncBody = () => document.body && document.body.classList.toggle('ov-panel-open', anyOpen());
@@@ -32,22 +36,35 @@
}
function register(opts) {
-- const el = opts && opts.el; if (!el || panels.some(p => p.el === el)) return;
++ const el = opts && opts.el, signal = opts && opts.signal;
++ if (!el || (signal && signal.aborted)) return () => {};
++ const known = panels.find(p => p.el === el); if (known) return known.unregister;
const p = { el, openClass: opts.openClass || 'open', id: opts.id || '', close: typeof opts.close === 'function' ? opts.close : () => el.classList.remove(opts.openClass || 'open') };
-- panels.push(p);
let was = isOpen(p);
-- new MutationObserver(() => {
++ const watch = new MutationObserver(() => {
const now = isOpen(p); if (now === was) return; was = now;
if (now) { closeAll(el); try { el.scrollTop = 0; } catch { /* */ } }
syncBody();
-- }).observe(el, { attributes: true, attributeFilter: ['class'] });
++ });
++ p.unregister = () => {
++ const i = panels.indexOf(p); if (i < 0) return;
++ panels.splice(i, 1);
++ watch.disconnect();
++ syncBody();
++ };
++ panels.push(p);
++ watch.observe(el, { attributes: true, attributeFilter: ['class'] });
++ if (signal && typeof signal.addEventListener === 'function') signal.addEventListener('abort', p.unregister, { once: true });
syncBody();
++ return p.unregister;
}
++ function unregister(el) { const p = panels.find(x => x.el === el); if (p) p.unregister(); }
++
document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && anyOpen()) closeAll(); });
root.addEventListener('orientationchange', () => closeAll(), { passive: true });
-- const api = { register, closeAll, anyOpen };
++ const api = { register, unregister, closeAll, anyOpen };
root.OpenVibePanels = api;
if (typeof module !== 'undefined' && module.exports) module.exports = api;
})(typeof window !== 'undefined' ? window : globalThis);
ddiff --git a/dist/_shared/shipped.js b/dist/_shared/shipped.js
iindex b344257..0793619 100644
---- a/dist/_shared/shipped.js
++++ b/dist/_shared/shipped.js
@@@ -34,7 +34,18 @@
}(typeof globalThis !== 'undefined' ? globalThis : this, function () {
'use strict';
const API = 'https://openvibe.network/api/v1/changelog';
-- const SITE_NAMES = { live: 'Live', network: 'Network', tools: 'Tools', media: 'Media', community: 'Community', chat: 'Chat', games: 'Games', blog: 'Blog', wiki: 'Wiki', news: 'News', reviews: 'Reviews', deals: 'Deals', coupons: 'Coupons', trade: 'Trade', codes: 'Codes', host: 'Host', ai: 'AI', search: 'Search', sources: 'Sources', events: 'Events', billing: 'Billing', tips: 'Tips', vip: 'VIP', openre: 'OpenRe', bot: 'Bot', sites: 'Sites', realtime: 'Realtime' };
++ // Every OpenVibe service's display name (openvibe-contracts site.name). A service missing here still reads as a name
++ // (siteName capitalises its id), never as a lowercase id next to capitalised ones.
++ const SITE_NAMES = {
++ live: 'Live', network: 'Network', tools: 'Tools', media: 'Media', community: 'Community', chat: 'Chat', games: 'Games',
++ blog: 'Blog', wiki: 'Wiki', news: 'News', reviews: 'Reviews', deals: 'Deals', coupons: 'Coupons', trade: 'Trade',
++ codes: 'Codes', host: 'Host', ai: 'AI', search: 'Search', sources: 'Sources', events: 'Events', billing: 'Billing',
++ tips: 'Tips', vip: 'VIP', openre: 'OpenRe', bot: 'Bot', sites: 'Sites', realtime: 'Realtime', space: 'Space',
++ services: 'Services', actor: 'Actor', run: 'Run', watch: 'Watch', node: 'Node', zone: 'Zone', download: 'Download',
++ video: 'Video', pics: 'Pics', rent: 'Rent', homes: 'Homes', work: 'Work', food: 'Food', help: 'Help', quest: 'Quest',
++ website: 'Website', fund: 'Fund', 'media-hub': 'Media Hub', extensions: 'Extensions', contracts: 'Contracts', sdk: 'SDK',
++ shared: 'Shared', examples: 'Examples', publishing: 'Publishing',
++ };
const STYLE_ID = 'ov-shipped-style';
const TEXT = 'var(--text-primary,var(--text,inherit))';
const MUTED = 'var(--text-secondary,var(--text-muted,#8b93ad))';
@@@ -98,7 +109,7 @@
function safeUrl(u) { return /^https:\/\//.test(String(u || '')) ? String(u) : null; }
function localHref(u) { const s = String(u || ''); return /^\/(?!\/)/.test(s) || /^https:\/\//.test(s) ? s : null; }
function clamp(n, lo, hi, d) { const v = parseInt(n, 10); return Number.isFinite(v) ? Math.min(Math.max(v, lo), hi) : d; }
-- const siteName = (id) => SITE_NAMES[id] || id;
++ const siteName = (id) => SITE_NAMES[id] || String(id || '').replace(/(^|-)([a-z])/g, (m, d, c) => `${d ? ' ' : ''}${c.toUpperCase()}`);
function timeEl(iso) { const t = el('time', { datetime: iso, 'data-ov-ago': '' }, ago(iso)); t.title = new Date(iso).toLocaleString(); return t; }
function rocket() {
const ns = 'http://www.w3.org/2000/svg';
@@@ -371,5 +382,5 @@
return out;
}
-- return { latest, list, mount: list, log, scan, render, renderLatest, appendDays, serviceFor, ago, API, SITE_NAMES };
++ return { latest, list, mount: list, log, scan, render, renderLatest, appendDays, serviceFor, ago, API, SITE_NAMES, siteName };
}));
ddiff --git a/dist/realtime.openvibe.network/release.json b/dist/realtime.openvibe.network/release.json
iindex 2a92d60..1566a54 100644
---- a/dist/realtime.openvibe.network/release.json
++++ b/dist/realtime.openvibe.network/release.json
@@@ -5,7 +5,7 @@
"booted_at": null,
"contracts_version": null,
"packages": {
-- "openvibe-shared": "2.13.2"
++ "openvibe-shared": "2.15.0"
},
"min_client_release": null,
"mixed_version_window_hours": 24,
ddiff --git a/dist/status.openvibe.network/release.json b/dist/status.openvibe.network/release.json
iindex 8d47ae5..3d593de 100644
---- a/dist/status.openvibe.network/release.json
++++ b/dist/status.openvibe.network/release.json
@@@ -5,7 +5,7 @@
"booted_at": null,
"contracts_version": null,
"packages": {
-- "openvibe-shared": "2.13.2"
++ "openvibe-shared": "2.15.0"
},
"min_client_release": null,
"mixed_version_window_hours": 24,
ddiff --git a/package-lock.json b/package-lock.json
iindex 1115524..2719621 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -7,7 +7,7 @@
"name": "openvibe-sites",
"dependencies": {
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.117.0",
-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.13.2"
++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.15.0"
},
"engines": {
"node": ">=20"
@@@ -197,9 +197,9 @@
}
},
"node_modules/openvibe-shared": {
-- "version": "2.13.2",
-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.13.2",
-- "integrity": "sha512-rvLq6uKd0j84qxZXwGX+gE6+e8SJaP8bBilWu9Eosjgfc70Z4bPJOUJUiLNRte7fU5hW/ea97/JoCLdiwXmPQA==",
++ "version": "2.15.0",
++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.15.0",
++ "integrity": "sha512-xApTyiUBVDQLTdnKEjjr/9uUU4+T5gofz9DqXv0MrLVINmW88uwbfoEht3kAlG9mp1792U6kIFEblJDHn2wyMQ==",
"license": "MIT",
"dependencies": {
"jsonwebtoken": "^9.0.0"
ddiff --git a/package.json b/package.json
iindex 72b4990..2375985 100644
---- a/package.json
++++ b/package.json
@@@ -8,7 +8,7 @@
},
"dependencies": {
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.117.0",
-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.13.2"
++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.15.0"
},
"engines": {
"node": ">=20"