openvibers/openvibe.sites Commits

openvibe-shared v2.20.0 (was v2.17.0) (#32)

OP OpenVibers committed 9 Oct 2026 · 53616dccf2 · 8 files, +131 −34

* openvibe-shared v2.20.0 (was v2.17.0)

Opened by scripts/pin-bump.js in OpenVibers/OpenVibe.Shared after the v2.20.0 release.

* Sites: dist/_shared rebuilt from openvibe-shared v2.20.0 (node build.js)

Changes

ddiff --git a/README.md b/README.md
iindex 63f0642..dfea6d2 100644
---- a/README.md
++++ b/README.md
@@@ -50,5 +50,5 @@ Report security issues through [SECURITY.md](SECURITY.md).
<!-- versions:start -->
- openvibe-contracts: v0.122.1
--- openvibe-shared: v2.17.0
++- openvibe-shared: v2.20.0
<!-- versions:end -->
ddiff --git a/STATUS.json b/STATUS.json
iindex d489b89..e71e741 100644
---- a/STATUS.json
++++ b/STATUS.json
@@@ -23,7 +23,7 @@
"status.openvibe.network (status notice)"
],
"contracts": "openvibe-contracts v0.122.1: sites.json mirrors products.catalog() (scripts/sync-catalog.js)",
-- "shared": "openvibe-shared v2.17.0",
++ "shared": "openvibe-shared v2.20.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/footer.js b/dist/_shared/footer.js
iindex c113bc0..0f4114a 100644
---- a/dist/_shared/footer.js
++++ b/dist/_shared/footer.js
@@@ -143,7 +143,7 @@
const cap = (w) => w ? w.charAt(0).toUpperCase() + w.slice(1) : '';
let m = h.match(/^(?:(.+)\.)?openvibe\.([a-z]+)$/);
if (m) { const sub = m[1] && m[1] !== 'www' ? m[1] : null; return [sub ? (sub.length <= 4 ? sub.toUpperCase() : cap(sub)) : null, 'OpenVibe', TLD_LABELS[m[2]] || cap(m[2])].filter(Boolean).join('.'); }
-- if ((m = h.match(/^(?:(.+)\.)?openre\.stream$/))) return 'OpenRe.Stream';
++ if ((m = h.match(/^(?:(.+)\.)?openre\.stream$/))) return 'OpenRestream';
const found = NETWORK.find(n => n.id === service);
if (service && TLD_LABELS[service]) return `OpenVibe.${TLD_LABELS[service]}`;
if (found && found.name !== 'Account & themes' && found.name !== 'The whole network →') return `${cap(service)}.OpenVibe.Tools`;
ddiff --git a/dist/_shared/items.js b/dist/_shared/items.js
iindex 8fe949c..b59be52 100644
---- a/dist/_shared/items.js
++++ b/dist/_shared/items.js
@@@ -7,26 +7,39 @@
* OpenVibeItems.decorate(listEl) again after new names are added (only the new ones are read)
*
* It reads OpenVibe.Inventory's public equipped sets (GET /api/v1/equipped?subjects=…, 100 people per request, no
-- * credentials) and keeps each answer 60 s, so a page full of names costs one or two requests. Each name gets the
-- * classes its items' renderers ask for:
-- * live.name_effect.css@1 `ov-fx` + the token (name-fx-rainbow, …): items.css draws it
-- * Anything else is left alone. Inventory unreachable: names stay as they are. Only tokens of the known shape are ever
-- * used as class names.
++ * credentials) and keeps each answer 60 s, so a page full of names costs one or two requests. Each name gets what its
++ * items' renderers ask for:
++ * live.name_effect.css@1 `ov-fx` + the token (name-fx-rainbow, …) on the name: items.css draws it
++ * live.hat.glyph@1 the hat's emoji just before the name (`ov-hat`, with its float/pulse/warp animation)
++ * live.particle.css@1 a short burst of the item's characters when you point at or focus the name (`ov-px`)
++ * network.badge.image@1 a community badge (the Workshop): its reviewed image just before the name (`ov-badge`),
++ * straight from the equipped read's media_id, served by OpenVibe.Media
++ * Hats and particles need their definitions (emoji, animation, characters): one public read of Live's definitions per
++ * page, only when someone on it wears one. Anything else is left alone. Inventory unreachable: names stay as they
++ * are. Only tokens of the known shape become class names; emoji and characters are text, never markup. A burst never
++ * runs by itself, and never under reduced motion or the network's Calm setting.
*/
(function (root) {
'use strict';
const SUBJECT = /^usr_[0-9A-HJKMNP-TV-Z]{26}$/;
const NAME_FX = /^name-fx-[a-z]{2,24}$/;
++ const PX = /^px-[a-z]{2,24}$/;
++ const MEDIA_ID = /^med_[0-9A-HJKMNP-TV-Z]{26}$/;
++ const HAT_MOTION = ['float', 'pulse', 'warp'];
const BATCH = 100;
const TTL_MS = 60 * 1000;
const cache = new Map(); // subject → { at, slots }
const waiting = new Map(); // subject → Promise
++ let defsPromise = null; // Map(definition id → { art, attributes })
const api = () => String((root.OpenVibeItemsConfig && root.OpenVibeItemsConfig.api) || 'https://inventory.openvibe.network').replace(/\/+$/, '');
++ const getJson = async (url) => {
++ const r = await fetch(url, { credentials: 'omit', headers: { Accept: 'application/json' } });
++ if (!r.ok) throw new Error(`inventory answered ${r.status}`);
++ return r.json();
++ };
async function fetchSets(subjects) {
-- const r = await fetch(`${api()}/api/v1/equipped?subjects=${subjects.map(encodeURIComponent).join(',')}`, { credentials: 'omit', headers: { Accept: 'application/json' } });
-- if (!r.ok) throw new Error(`inventory answered ${r.status}`);
-- const body = await r.json();
++ const body = await getJson(`${api()}/api/v1/equipped?subjects=${subjects.map(encodeURIComponent).join(',')}`);
const now = Date.now();
const got = new Map();
for (const set of (body && body.equipped) || []) if (set && SUBJECT.test(set.subject)) got.set(set.subject, set.slots || {});
@@@ -48,11 +61,89 @@
return new Map(want.map((s) => [s, (cache.get(s) || { slots: {} }).slots]));
}
-- /** Give one element the classes of a set of slots (removing an earlier name effect). */
-- function apply(el, slots) {
-- for (const c of [...el.classList]) if (c === 'ov-fx' || NAME_FX.test(c)) el.classList.remove(c);
-- for (const [key, v] of Object.entries(slots || {})) {
-- if (key.split(':')[0] === 'live.name_effect' && v && NAME_FX.test(String(v.token || ''))) el.classList.add('ov-fx', v.token);
++ /** Live's item definitions (hats and particles carry their emoji, animation and characters there), once a page. */
++ function definitions() {
++ if (!defsPromise) {
++ defsPromise = getJson(`${api()}/api/v1/definitions?issuer=${encodeURIComponent('service:live')}&limit=500`)
++ .then((b) => new Map(((b && b.definitions) || []).map((d) => [d.id, { art: d.art || {}, attributes: d.attributes || {} }])))
++ .catch(() => { defsPromise = null; return new Map(); });
++ }
++ return defsPromise;
++ }
++
++ const slotOf = (slots, kind) => { for (const [key, v] of Object.entries(slots || {})) if (key.split(':')[0] === kind && v) return v; return null; };
++ const calm = () => {
++ const d = root.document && root.document.documentElement;
++ if (d && d.getAttribute && d.getAttribute('data-ov-motion') === 'reduced') return true;
++ return !!(root.matchMedia && root.matchMedia('(prefers-reduced-motion: reduce)').matches);
++ };
++
++ /** A short burst of characters out of the name (pointer or keyboard focus); never under reduced motion. */
++ function burst(el) {
++ if (calm() || !el.isConnected || el.dataset.ovPxBusy) return;
++ const chars = Array.from(el.dataset.ovPxChars || '').slice(0, 6);
++ if (!chars.length) return;
++ el.dataset.ovPxBusy = '1';
++ for (let i = 0; i < 6; i++) {
++ const p = el.ownerDocument.createElement('span');
++ p.className = 'ov-px-p';
++ p.setAttribute('aria-hidden', 'true');
++ p.textContent = chars[i % chars.length];
++ const angle = (Math.PI * 2 * i) / 6 + Math.random() * 0.6;
++ p.style.setProperty('--px-dx', `${Math.round(Math.cos(angle) * 22)}px`);
++ p.style.setProperty('--px-dy', `${Math.round(Math.sin(angle) * 16 - 10)}px`);
++ p.style.left = `${Math.round(30 + Math.random() * 40)}%`;
++ el.appendChild(p);
++ setTimeout(() => p.remove(), 1900);
++ }
++ setTimeout(() => { delete el.dataset.ovPxBusy; }, 1200);
++ }
++ function onBurst(e) { burst(e.currentTarget); }
++
++ /** Give one element what a set of slots draws (removing what an earlier set drew). defs: Map from definitions(). */
++ function apply(el, slots, defs) {
++ for (const c of [...el.classList]) if (c === 'ov-fx' || NAME_FX.test(c) || c === 'ov-px' || PX.test(c)) el.classList.remove(c);
++ for (let prev = el.previousElementSibling; prev && prev.classList && (prev.classList.contains('ov-hat') || prev.classList.contains('ov-badge')); prev = el.previousElementSibling) prev.remove();
++ el.removeEventListener('mouseenter', onBurst);
++ el.removeEventListener('focusin', onBurst);
++ delete el.dataset.ovPxChars;
++
++ const fx = slotOf(slots, 'live.name_effect');
++ if (fx && NAME_FX.test(String(fx.token || ''))) el.classList.add('ov-fx', fx.token);
++
++ const badge = slotOf(slots, 'network.badge');
++ if (badge && MEDIA_ID.test(String(badge.media_id || '')) && el.parentNode) {
++ const b = el.ownerDocument.createElement('img');
++ b.className = 'ov-badge';
++ b.src = `${String((root.OpenVibeItemsConfig && root.OpenVibeItemsConfig.media) || 'https://openvibe.media').replace(/\/+$/, '')}/o/${badge.media_id}`;
++ b.alt = '';
++ b.width = 16; b.height = 16;
++ b.setAttribute('loading', 'lazy');
++ b.setAttribute('aria-hidden', 'true');
++ el.parentNode.insertBefore(b, el);
++ }
++
++ const hat = slotOf(slots, 'live.hat');
++ const hatDef = hat && defs && defs.get(hat.definition_id);
++ const glyph = hatDef && String(hatDef.art.emoji || hatDef.attributes.hat_char || '').slice(0, 8);
++ if (glyph && el.parentNode) {
++ const h = el.ownerDocument.createElement('span');
++ const motion = String(hatDef.attributes.animated || '');
++ h.className = `ov-hat${HAT_MOTION.includes(motion) ? ` ov-hat-${motion}` : ''}`;
++ h.setAttribute('aria-hidden', 'true');
++ h.textContent = glyph;
++ el.parentNode.insertBefore(h, el);
++ }
++
++ const px = slotOf(slots, 'live.particle');
++ const pxDef = px && defs && defs.get(px.definition_id);
++ const token = String((px && px.token) || (pxDef && pxDef.art.token) || '');
++ const chars = pxDef && String(pxDef.attributes.chars || '').slice(0, 24);
++ if (PX.test(token) && chars) {
++ el.classList.add('ov-px', token);
++ el.dataset.ovPxChars = chars;
++ el.addEventListener('mouseenter', onBurst);
++ el.addEventListener('focusin', onBurst);
}
}
@@@ -64,9 +155,11 @@
for (const el of els) el.dataset.ovItems = '1';
let sets;
try { sets = await equipped(els.map((el) => el.dataset.ovSubject)); } catch { return 0; }
-- for (const el of els) apply(el, sets.get(el.dataset.ovSubject));
++ const needsDefs = [...sets.values()].some((slots) => slotOf(slots, 'live.hat') || slotOf(slots, 'live.particle'));
++ const defs = needsDefs ? await definitions() : new Map();
++ for (const el of els) apply(el, sets.get(el.dataset.ovSubject), defs);
return els.length;
}
-- root.OpenVibeItems = { decorate, equipped, apply, _cache: cache };
++ root.OpenVibeItems = { decorate, equipped, definitions, apply, burst, _cache: cache };
})(typeof window !== 'undefined' ? window : globalThis);
ddiff --git a/dist/_shared/navbar.js b/dist/_shared/navbar.js
iindex 698a957..9af4953 100644
---- a/dist/_shared/navbar.js
++++ b/dist/_shared/navbar.js
@@@ -478,10 +478,12 @@
function resolveBrand() {
const b = Object.assign({}, _config.brand || {});
const host = currentHost().toLowerCase();
-- let sub = null, tld = null, core = 'OpenVibe';
++ // OpenRestream lives at openre.stream: its brand is one word, "OpenRe" + "stream" with no dot between, so it
++ // reads as OpenRestream while the two colours still show the domain.
++ let sub = null, tld = null, core = 'OpenVibe', joined = false;
let m = host.match(/^(?:(.+)\.)?openvibe\.([a-z]+)$/);
if (m) { sub = m[1] && m[1] !== 'www' ? m[1] : null; tld = m[2]; }
-- else if ((m = host.match(/^(?:(.+)\.)?openre\.stream$/))) { sub = m[1] && m[1] !== 'www' ? m[1] : null; core = 'OpenRe'; tld = 'stream'; }
++ else if ((m = host.match(/^(?:(.+)\.)?openre\.stream$/))) { sub = m[1] && m[1] !== 'www' ? m[1] : null; core = 'OpenRe'; tld = 'stream'; joined = true; }
// Off-network hosts (localhost, previews): fall back to the service id.
if (!tld) { tld = SERVICE_TLD[_config.service] || (SERVICE_SUB[_config.service] ? 'tools' : 'network'); sub = SERVICE_SUB[_config.service] || null; }
// Legacy brandName ("Paste.OpenVibe", "OpenVibe.Live") still steers the segments.
@@@ -494,27 +496,29 @@
if (b.sub !== undefined) sub = b.sub ? String(b.sub).toLowerCase() : null;
if (b.tld) tld = String(b.tld).toLowerCase();
const subText = b.subLabel || (sub ? subLabel(sub) : null);
-- const tldText = b.tldLabel || TLD_LABELS[tld] || titleCase(tld);
-- const name = b.name || [subText, core, tldText].filter(Boolean).join('.');
++ const tldText = b.tldLabel || (joined ? tld : TLD_LABELS[tld] || titleCase(tld));
++ const site = joined ? `${core}${tldText}` : `${core}.${tldText}`;
++ const name = b.name || [subText, site].filter(Boolean).join('.');
const icon = b.icon || _config.brandIcon || null;
-- const variant = b.variant || (core === 'OpenRe' ? 'stream' : tld);
-- return { sub, subText, core, tld, tldText, name, short: subText || `${core}.${tldText}`, icon, variant, tag: b.tag || null, href: b.href || '/' };
++ const variant = b.variant || (joined ? 'stream' : tld);
++ return { sub, subText, core, tld, tldText, joined, site, name, short: subText || site, icon, variant, tag: b.tag || null, href: b.href || '/' };
}
/** Where each brand segment goes: sub → this tool, OpenVibe → the network, TLD → the site's apex. */
function brandLinks(brand) {
-- const apex = brand.core === 'OpenRe' ? 'https://openre.stream/' : `https://openvibe.${brand.tld}/`;
++ const apex = brand.joined ? 'https://openre.stream/' : `https://openvibe.${brand.tld}/`;
if (!brand.subText) return { sub: brand.href, core: brand.href, tld: brand.href, apex };
-- return { sub: brand.href, core: 'https://openvibe.network/', tld: apex, apex };
++ return { sub: brand.href, core: brand.joined ? apex : 'https://openvibe.network/', tld: apex, apex };
}
function brandHTML(brand) {
const to = brandLinks(brand);
const seg = (cls, text, href, title) => `<a href="${escapeAttr(href)}"${title ? ` title="${escapeAttr(title)}"` : ''} class="${cls}">${escapeAttr(text)}</a>`;
const dot = '<span class="b-dot">.</span>';
++ const join = brand.joined ? '' : dot;
const text = brand.subText
-- ? seg('b-sub', brand.subText, to.sub, brand.name) + dot + seg('b-core', brand.core, to.core, 'OpenVibe Network') + dot + seg('b-tld', brand.tldText, to.tld, `All of ${brand.core}.${brand.tldText}`)
-- : seg('b-core', brand.core, to.core, brand.name) + dot + seg('b-tld', brand.tldText, to.tld, brand.name);
++ ? seg('b-sub', brand.subText, to.sub, brand.name) + dot + seg('b-core', brand.core, to.core, brand.joined ? brand.site : 'OpenVibe Network') + join + seg('b-tld', brand.tldText, to.tld, `All of ${brand.site}`)
++ : seg('b-core', brand.core, to.core, brand.name) + join + seg('b-tld', brand.tldText, to.tld, brand.name);
const mark = brand.icon
? `<i class="fa-solid ${escapeAttr(brand.icon)}"></i>`
: `<span class="ov-mark" data-size="28" data-variant="${escapeAttr(brand.variant)}"></span>`;
ddiff --git a/dist/_shared/shipped.js b/dist/_shared/shipped.js
iindex 0793619..2f6cae0 100644
---- a/dist/_shared/shipped.js
++++ b/dist/_shared/shipped.js
@@@ -40,7 +40,7 @@
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',
++ tips: 'Tips', vip: 'VIP', openre: 'OpenRestream', 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',
ddiff --git a/package-lock.json b/package-lock.json
iindex fbb48cc..963169c 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.122.1",
-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.17.0"
++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.20.0"
},
"engines": {
"node": ">=20"
@@@ -197,9 +197,9 @@
}
},
"node_modules/openvibe-shared": {
-- "version": "2.17.0",
-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.17.0",
-- "integrity": "sha512-z0dDgGH9QqOoinQybWDEYfs4t8ewwFOgsiKxmNbNzWYlGXf5ZYAkfK+zExtFqO2EUBpRzC4MhZkTvF6ydvhltA==",
++ "version": "2.20.0",
++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.20.0",
++ "integrity": "sha512-rE43C5ik80zixTb+cOGI8nNj2uoKGzIt4J25pPJ8CDFUAIqE5ZRYFfdXa9+dX2GlFQEjclruNbpAF+p8wli+Dw==",
"license": "MIT",
"dependencies": {
"jsonwebtoken": "^9.0.0"
ddiff --git a/package.json b/package.json
iindex e82df79..a5b3f2a 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.122.1",
-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.17.0"
++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.20.0"
},
"engines": {
"node": ">=20"