openvibers/openvibe.community Commits
Re-sync vendored openvibe-shared with the Network's current copy
Changes
@@@ -19,7 +19,7 @@ const BRAND_DEFAULTS = Object.freeze({gamesServiceName: 'OpenVibe.Games',mediaServiceName: 'OpenVibe.Media',tagline: 'One Account. All of OpenVibe.',-- campTagline: 'Free & Open Live Streaming',++ campTagline: 'Open Live Streaming, Community Run',toolsSubdomainBase: 'openvibe.tools',discord: 'https://discord.gg/M6MuRUaeJj',github: 'https://github.com/OpenVibe.Live',@@@ -99,8 +99,8 @@ function buildBrandFromRegistry(registry = {}, env = process.env) {// Build service list using registry URLs where availableconst services = [-- { id: 'live', name: streamerServiceName, url: liveUrl, description: 'Free & Open Live Streaming' },-- { id: 'tools', name: toolsServiceName, url: toolsUrl, description: 'Free Online Tools' },++ { id: 'live', name: streamerServiceName, url: liveUrl, description: 'Open live streaming, community run' },++ { id: 'tools', name: toolsServiceName, url: toolsUrl, description: 'Online tools for everyone' },{ id: 'games', name: gamesServiceName, url: gamesUrl, description: 'Games & Community Canvas' },{ id: 'media', name: mediaServiceName, url: mediaUrl, description: 'VODs, Clips, Pastes & Files' },{ id: 'network', name: BRAND_DEFAULTS.networkServiceName, url: networkUrl, description: 'Accounts, SSO & OpenCoins' },@@@ -155,8 +155,8 @@ const BRAND = Object.freeze({// Primary services in the network (for dashboard display, health checks, etc.)// Static list with OpenVibe defaults — use buildBrandFromRegistry() for live values.services: Object.freeze([-- { id: 'live', name: 'OpenVibe.Live', url: 'https://openvibe.live', description: 'Free & Open Live Streaming' },-- { id: 'tools', name: 'OpenVibe.Tools', url: 'https://openvibe.tools', description: 'Free Online Tools' },++ { id: 'live', name: 'OpenVibe.Live', url: 'https://openvibe.live', description: 'Open live streaming, community run' },++ { id: 'tools', name: 'OpenVibe.Tools', url: 'https://openvibe.tools', description: 'Online tools for everyone' },{ id: 'games', name: 'OpenVibe.Games', url: 'https://openvibe.games', description: 'Games & Community Canvas' },{ id: 'media', name: 'OpenVibe.Media', url: 'https://openvibe.media', description: 'VODs, Clips, Pastes & Files' },{ id: 'network', name: 'OpenVibe.Network', url: 'https://openvibe.network', description: 'Accounts, SSO & OpenCoins' },@@@ -34,7 +34,7 @@// the link, with no edit anywhere else.const NETWORK = [{ id: 'live', name: 'Live streaming', url: 'https://openvibe.live', desc: 'Go live, restream and clip' },-- { id: 'tools', name: 'Free online tools', url: 'https://openvibe.tools', desc: 'The whole toolbox' },++ { id: 'tools', name: 'Online tools', url: 'https://openvibe.tools', desc: 'The whole toolbox' },{ id: 'dev', name: 'Developer tools', url: 'https://dev.openvibe.tools', desc: 'Formatters, encoders, validators' },{ id: 'net', name: 'Network diagnostics', url: 'https://net.openvibe.tools', desc: 'Ping, DNS, Whois, SSL' },{ id: 'img', name: 'Image converter', url: 'https://img.openvibe.tools', desc: 'Convert, resize, compress' },@@@ -42,7 +42,11 @@{ id: 'docs', name: 'PDF & document tools', url: 'https://docs.openvibe.tools', desc: 'Merge, split, compress' },{ id: 'text', name: 'Text tools', url: 'https://text.openvibe.tools', desc: 'Fancy text, case, ASCII' },{ id: 'yt', name: 'YouTube downloader', url: 'https://yt.openvibe.tools', desc: 'Video and audio' },++ { id: 'community', name: 'Community & pastes', url: 'https://openvibe.community', desc: 'Pastes, posts and the people of OpenVibe' },++ { id: 'games', name: 'Games', url: 'https://openvibe.games', desc: 'Scraplandia and browser games' },++ { id: 'media', name: 'Media', url: 'https://openvibe.media', desc: 'VODs, clips and files for every site' },{ id: 'network', name: 'Account & themes', url: NETWORK_URL, desc: 'One account, every site' },++ { id: 'more', name: 'The whole network →', url: NETWORK_URL + '/#network', desc: 'Every OpenVibe site, live and upcoming' },];const LEGAL = [@@@ -59,7 +63,7 @@const DEFAULTS = {service: null, // 'live' | 'tools' | 'dev' | … (auto-detected when omitted)brandName: null,-- tagline: 'Free, open source and community run. Built in the open by the people using it.',++ tagline: 'Open source and community run. Built in the open by the people using it.',variant: 'full',mount: '#ov-footer',links: [], // [{ heading, items: [{ name, url, onclick? }] }]@@@ -84,9 +88,17 @@return sub === h ? 'tools' : (sub || 'tools');}++ const TLD_LABELS = { live: 'Live', tools: 'Tools', network: 'Network', media: 'Media', games: 'Games', community: 'Community', chat: 'Chat', codes: 'Codes', blog: 'Blog', wiki: 'Wiki', news: 'News', reviews: 'Reviews', tips: 'Tips', vip: 'VIP', trade: 'Trade', host: 'Host', deals: 'Deals', coupons: 'Coupons' };++ /** The full site name from the hostname — the same spelling the navbar uses (Pastes.OpenVibe.Tools). */function brandFor(service) {++ const h = typeof location !== 'undefined' ? location.hostname.toLowerCase() : '';++ 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';const found = NETWORK.find(n => n.id === service);-- if (found) return found.name === 'Account & themes' ? 'OpenVibe' : found.name;++ 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`;return 'OpenVibe';}@@@ -896,11 +896,11 @@items.slice(0, 4).map(h => `<a class="item" href="${escapeAttr(h.url)}"><span class="icon"><i class="fa-solid ${escapeAttr(h.icon || 'fa-clock-rotate-left')}"></i></span><span class="t">${escapeAttr(h.title || h.url)}</span><span class="s">${escapeAttr(h.service_label || h.service || '')}</span></a>`).join('');};const H = root.OpenVibeHistory;-- if (H && typeof H.recent === 'function') { H.recent({ limit: 4, token: _config.token }).then(draw).catch(() => draw(null)); return; }++ if (H && typeof H.recent === 'function') { H.recent({ limit: 4, token: _config.token, apiBase: _config.apiBase }).then(draw).catch(() => draw(null)); return; }if (document.getElementById('ov-history-loader')) return;const sc = document.createElement('script'); sc.id = 'ov-history-loader'; sc.async = true;sc.src = `${_config.apiBase}/shared/history.js`;-- sc.onload = () => { try { root.OpenVibeHistory.recent({ limit: 4, token: _config.token }).then(draw).catch(() => draw(null)); } catch { /* */ } };++ sc.onload = () => { try { root.OpenVibeHistory.recent({ limit: 4, token: _config.token, apiBase: _config.apiBase }).then(draw).catch(() => draw(null)); } catch { /* */ } };document.head.appendChild(sc);}@@@ -912,8 +912,7 @@const svc = _config.service;const brand = resolveBrand();-- nav.setAttribute('data-compact', _config.compact || 'auto');-- nav.setAttribute('data-service', svc);++ if (typeof nav.setAttribute === 'function') { nav.setAttribute('data-compact', _config.compact || 'auto'); nav.setAttribute('data-service', svc); }const links = currentLinks();const u = _config.user;@@@ -2,6 +2,7 @@* ov-mark.js — the OpenVibe "OV" brand mark as a drop-in.* <span class="ov-mark"></span> → 32px animated mark* <span class="ov-mark" data-size="56"> → any size; add data-static="1" for no motion++ * <span class="ov-mark" data-variant="live"> → the site's own twist on the motion (see VARIANTS)* Self-contained: injects its own CSS once, works on any page (Live, Network, static pages).* The O is a ring with a comet arc running around it, the V is drawn inside with a light* sweep, and a dot orbits the ring. Transform/opacity/stroke motion only.@@@ -17,7 +18,30 @@.ov-mark .r{fill:none;stroke:currentColor;stroke-width:4;opacity:.28}.ov-mark .c{fill:none;stroke-width:4;stroke-linecap:round;stroke-dasharray:34 79;transform-box:fill-box;transform-origin:center;animation:ovmComet 3.6s linear infinite}.ov-mark .v{fill:none;stroke-width:4.6;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:46;animation:ovmDraw 1.1s cubic-bezier(.2,.8,.2,1) both}--.ov-mark .d{fill:#fff;transform-box:fill-box;transform-origin:center;animation:ovmDot 2.4s ease-in-out infinite}++.ov-mark .d{fill:var(--ovm-dot,#fff);transform-box:fill-box;transform-origin:center;animation:ovmDot 2.4s ease-in-out infinite}++/* Per-site variants: one family, each site with its own motion so the network reads as one brand++ with many rooms. Colours stay with the theme (currentColor); only the dot and the rhythm change. */++.ov-mark[data-variant=live]{--ovm-dot:#ef4444}++.ov-mark[data-variant=live] .d{animation:ovmOnAir 1.4s ease-in-out infinite}++.ov-mark[data-variant=tools] .c{animation-duration:2.2s;stroke-dasharray:22 91}++.ov-mark[data-variant=tools] .o2{display:none}++.ov-mark[data-variant=media] .c{stroke-dasharray:14 14 14 71;animation-duration:4.4s}++.ov-mark[data-variant=games] svg{animation:ovmBounce 1.6s cubic-bezier(.3,1.4,.4,1) infinite}++.ov-mark[data-variant=games] .d{animation-duration:1.6s}++.ov-mark[data-variant=community] .o2{opacity:.9;r:1.8}++.ov-mark[data-variant=community] .c{animation-direction:reverse}++.ov-mark[data-variant=network] .g{animation-duration:2.4s}++.ov-mark[data-variant=chat] .d{animation:ovmDot 1s ease-in-out infinite}++.ov-mark[data-variant=stream] .c{stroke-dasharray:60 53;animation-duration:2.8s}++.ov-mark[data-variant=codes] .v{animation:ovmDraw 1.1s cubic-bezier(.2,.8,.2,1) infinite alternate}++.ov-mark[data-variant=blog] .c,.ov-mark[data-variant=wiki] .c,.ov-mark[data-variant=news] .c{animation-duration:6s}++.ov-mark[data-variant=vip]{--ovm-dot:#facc15}++.ov-mark[data-variant=tips]{--ovm-dot:#22c55e}++.ov-mark[data-variant=deals],.ov-mark[data-variant=coupons]{--ovm-dot:#f59e0b}++.ov-mark[data-variant=trade] .c{animation-timing-function:cubic-bezier(.4,0,.2,1)}++.ov-mark[data-variant=host] svg{animation:none}++@keyframes ovmOnAir{0%,100%{transform:scale(1);opacity:.7}50%{transform:scale(1.7);opacity:1}}++@keyframes ovmBounce{0%,100%{transform:translateY(0)}50%{transform:translateY(-2.5px)}}.ov-mark .o{fill:#fff;filter:drop-shadow(0 0 3px currentColor)}.ov-mark .o2{opacity:.5}.ov-mark:hover svg,a:hover>.ov-mark svg,button:hover>.ov-mark svg{animation:ovmSpin .9s cubic-bezier(.2,1.5,.3,1) 1}nnew file mode 100644@@@ -0,0 +1,129 @@++'use strict';++// navbar.js — brand resolution from the hostname, compact/short form, overrides and the++// modular menu/links API (plain node with the same stubbed globals as navbar-auth.test.js).++const assert = require('assert');++const path = require('path');++const NAVBAR_PATH = path.join(__dirname, '..', 'navbar.js');++++function stubElement() {++ const el = {++ style: {}, textContent: '', className: '', id: '', innerHTML: '', dataset: {}, attrs: {}, children: [],++ classList: { add() {}, remove() {}, toggle() {} },++ setAttribute(k, v) { this.attrs[k] = String(v); }, getAttribute(k) { return this.attrs[k] ?? null; },++ appendChild(c) { this.children.push(c); }, prepend(c) { this.children.unshift(c); }, remove() {}, addEventListener() {},++ querySelector() { return stubElement(); }, querySelectorAll() { return []; },++ };++ return el;++}++++function load(hostname, opts = {}) {++ global.location = { hostname, href: `https://${hostname}/`, pathname: opts.pathname || '/', protocol: 'https:' };++ global.window = { location: global.location };++ try { Object.defineProperty(global, 'navigator', { value: { userAgent: 'test' }, configurable: true }); } catch { /* read-only in some runtimes */ }++ const created = [];++ global.document = {++ cookie: opts.cookie || '', head: stubElement(), body: stubElement(), title: 'T',++ getElementById: () => null, createElement: (tag) => { const e = stubElement(); e.tag = tag; created.push(e); return e; },++ addEventListener() {}, removeEventListener() {}, dispatchEvent() {},++ };++ global.CustomEvent = class { constructor(t, i) { this.type = t; this.detail = i && i.detail; } };++ const store = {};++ global.localStorage = { getItem: (k) => (k in store ? store[k] : null), setItem: (k, v) => { store[k] = String(v); }, removeItem: (k) => { delete store[k]; } };++ global.sessionStorage = { getItem: () => null, setItem() {}, removeItem() {} };++ global.fetch = async () => ({ ok: false, status: 404, json: async () => ({}) });++ delete require.cache[require.resolve(NAVBAR_PATH)];++ const navbar = require(NAVBAR_PATH);++ return { navbar, created };++}++++// ── hostname → segments ──────────────────────────────────────++{++ const { navbar } = load('pastes.openvibe.tools');++ navbar.init({ service: 'paste', user: { id: 1, username: 'x' } });++ const b = navbar.brand();++ assert.strictEqual(b.name, 'Pastes.OpenVibe.Tools');++ assert.strictEqual(b.short, 'Pastes');++ assert.strictEqual(b.tld, 'tools');++ assert.strictEqual(b.variant, 'tools');++}++{++ const { navbar } = load('json.openvibe.tools');++ navbar.init({ service: 'dev' });++ assert.strictEqual(navbar.brand().name, 'JSON.OpenVibe.Tools', 'known acronyms keep their casing');++}++{++ const { navbar } = load('mergepdf.openvibe.tools');++ navbar.init({ service: 'docs' });++ assert.strictEqual(navbar.brand().name, 'MergePDF.OpenVibe.Tools');++}++{++ const { navbar } = load('openvibe.live');++ navbar.init({ service: 'live' });++ const b = navbar.brand();++ assert.strictEqual(b.name, 'OpenVibe.Live');++ assert.strictEqual(b.short, 'OpenVibe.Live', 'no subdomain: nothing to shorten to');++ assert.strictEqual(b.variant, 'live');++}++{++ const { navbar } = load('www.openvibe.community');++ navbar.init({ service: 'community' });++ assert.strictEqual(navbar.brand().name, 'OpenVibe.Community', 'www is not a subdomain brand');++}++{++ const { navbar } = load('play.openvibe.games');++ navbar.init({ service: 'games' });++ assert.strictEqual(navbar.brand().name, 'Play.OpenVibe.Games');++}++{++ const { navbar } = load('ingest.openre.stream');++ navbar.init({ service: 'live' });++ const b = navbar.brand();++ assert.strictEqual(b.name, 'Ingest.OpenRe.Stream');++ assert.strictEqual(b.variant, 'stream');++}++{++ const { navbar } = load('openvibe.vip');++ navbar.init({ service: 'network' });++ assert.strictEqual(navbar.brand().name, 'OpenVibe.VIP');++}++// ── off-network hosts fall back to the service id ────────────++{++ const { navbar } = load('localhost');++ navbar.init({ service: 'net' });++ assert.strictEqual(navbar.brand().name, 'Net.OpenVibe.Tools');++ const { navbar: n2 } = load('localhost');++ n2.init({ service: 'live' });++ assert.strictEqual(n2.brand().name, 'OpenVibe.Live');++}++// ── overrides ────────────────────────────────────────────────++{++ const { navbar } = load('openvibe.tools');++ navbar.init({ service: 'tools', brandName: 'Paste.OpenVibe' });++ assert.strictEqual(navbar.brand().name, 'Pastes.OpenVibe.Tools', 'legacy brandName steers the subdomain segment (paste → Pastes, the host name)');++ const { navbar: n2 } = load('openvibe.tools');++ n2.init({ service: 'tools', brand: { sub: 'logo', tag: 'beta' } });++ assert.strictEqual(n2.brand().name, 'Logo.OpenVibe.Tools');++ assert.strictEqual(n2.brand().tag, 'beta');++ const { navbar: n3 } = load('openvibe.tools');++ n3.init({ service: 'tools', brand: { name: 'Custom Name', variant: 'games' } });++ assert.strictEqual(n3.brand().name, 'Custom Name');++ assert.strictEqual(n3.brand().variant, 'games');++}++// ── rendered markup carries the segments, compact attr and the variant ────++{++ const { navbar, created } = load('pastes.openvibe.tools', { pathname: '/mine' });++ navbar.init({ service: 'paste', user: { id: 1, username: 'x' }, links: [{ label: 'Mine', href: '/mine' }, { label: 'New', href: '/new', icon: 'fa-plus' }], menu: { after: [{ id: 'mp', label: 'My pastes', href: '/my', icon: 'fa-paste' }] }, compact: 'always' });++ const nav = created.find(e => e.tag === 'nav');++ assert.ok(nav, 'nav element created');++ assert.strictEqual(nav.getAttribute('data-compact'), 'always');++ assert.ok(nav.innerHTML.includes('class="b-sub">Pastes<') && nav.innerHTML.includes('class="b-tld">Tools<'), 'segments rendered');++ assert.ok(nav.innerHTML.includes('data-variant="tools"'), 'ov-mark variant');++ assert.ok(nav.innerHTML.includes('href="/mine" class="active"'), 'current path marked active');++ assert.ok(nav.innerHTML.includes('fa-plus icon'), 'link icon rendered');++ const dd = created.find(e => e.tag === 'div' && /openvibe-navbar-dropdown-menu/.test(e.innerHTML));++ assert.ok(dd && dd.innerHTML.includes('data-menu-id="mp"') && dd.innerHTML.includes('My pastes'), 'custom dropdown row rendered');++ assert.ok(dd.innerHTML.includes('/my#history'), 'History link present');++ const id = navbar.addMenuItem({ label: 'Runtime row', href: '/r', position: 'before' });++ assert.ok(id, 'addMenuItem returns an id');++}++console.log('navbar brand: all checks passed');
@@ -19,7 +19,7 @@ const BRAND_DEFAULTS = Object.freeze({
@@ -19,7 +19,7 @@ const BRAND_DEFAULTS = Object.freeze({
gamesServiceName: 'OpenVibe.Games',
gamesServiceName: 'OpenVibe.Games',
mediaServiceName: 'OpenVibe.Media',
mediaServiceName: 'OpenVibe.Media',
tagline: 'One Account. All of OpenVibe.',
tagline: 'One Account. All of OpenVibe.',
- campTagline: 'Free & Open Live Streaming',
+ campTagline: 'Open Live Streaming, Community Run',
toolsSubdomainBase: 'openvibe.tools',
toolsSubdomainBase: 'openvibe.tools',
discord: 'https://discord.gg/M6MuRUaeJj',
discord: 'https://discord.gg/M6MuRUaeJj',
github: 'https://github.com/OpenVibe.Live',
github: 'https://github.com/OpenVibe.Live',
@@ -99,8 +99,8 @@ function buildBrandFromRegistry(registry = {}, env = process.env) {
@@ -99,8 +99,8 @@ function buildBrandFromRegistry(registry = {}, env = process.env) {
// Build service list using registry URLs where available
// Build service list using registry URLs where available
const services = [
const services = [
- { id: 'live', name: streamerServiceName, url: liveUrl, description: 'Free & Open Live Streaming' },
- { id: 'tools', name: toolsServiceName, url: toolsUrl, description: 'Free Online Tools' },
+ { id: 'live', name: streamerServiceName, url: liveUrl, description: 'Open live streaming, community run' },
+ { id: 'tools', name: toolsServiceName, url: toolsUrl, description: 'Online tools for everyone' },
{ id: 'games', name: gamesServiceName, url: gamesUrl, description: 'Games & Community Canvas' },
{ id: 'games', name: gamesServiceName, url: gamesUrl, description: 'Games & Community Canvas' },
{ id: 'media', name: mediaServiceName, url: mediaUrl, description: 'VODs, Clips, Pastes & Files' },
{ id: 'media', name: mediaServiceName, url: mediaUrl, description: 'VODs, Clips, Pastes & Files' },
{ id: 'network', name: BRAND_DEFAULTS.networkServiceName, url: networkUrl, description: 'Accounts, SSO & OpenCoins' },
{ id: 'network', name: BRAND_DEFAULTS.networkServiceName, url: networkUrl, description: 'Accounts, SSO & OpenCoins' },
@@ -155,8 +155,8 @@ const BRAND = Object.freeze({
@@ -155,8 +155,8 @@ const BRAND = Object.freeze({
// Primary services in the network (for dashboard display, health checks, etc.)
// Primary services in the network (for dashboard display, health checks, etc.)
// Static list with OpenVibe defaults — use buildBrandFromRegistry() for live values.
// Static list with OpenVibe defaults — use buildBrandFromRegistry() for live values.
services: Object.freeze([
services: Object.freeze([
- { id: 'live', name: 'OpenVibe.Live', url: 'https://openvibe.live', description: 'Free & Open Live Streaming' },
- { id: 'tools', name: 'OpenVibe.Tools', url: 'https://openvibe.tools', description: 'Free Online Tools' },
+ { id: 'live', name: 'OpenVibe.Live', url: 'https://openvibe.live', description: 'Open live streaming, community run' },
+ { id: 'tools', name: 'OpenVibe.Tools', url: 'https://openvibe.tools', description: 'Online tools for everyone' },
{ id: 'games', name: 'OpenVibe.Games', url: 'https://openvibe.games', description: 'Games & Community Canvas' },
{ id: 'games', name: 'OpenVibe.Games', url: 'https://openvibe.games', description: 'Games & Community Canvas' },
{ id: 'media', name: 'OpenVibe.Media', url: 'https://openvibe.media', description: 'VODs, Clips, Pastes & Files' },
{ id: 'media', name: 'OpenVibe.Media', url: 'https://openvibe.media', description: 'VODs, Clips, Pastes & Files' },
{ id: 'network', name: 'OpenVibe.Network', url: 'https://openvibe.network', description: 'Accounts, SSO & OpenCoins' },
{ id: 'network', name: 'OpenVibe.Network', url: 'https://openvibe.network', description: 'Accounts, SSO & OpenCoins' },
@@ -34,7 +34,7 @@
@@ -34,7 +34,7 @@
// the link, with no edit anywhere else.
// the link, with no edit anywhere else.
const NETWORK = [
const NETWORK = [
{ id: 'live', name: 'Live streaming', url: 'https://openvibe.live', desc: 'Go live, restream and clip' },
{ id: 'live', name: 'Live streaming', url: 'https://openvibe.live', desc: 'Go live, restream and clip' },
- { id: 'tools', name: 'Free online tools', url: 'https://openvibe.tools', desc: 'The whole toolbox' },
+ { id: 'tools', name: 'Online tools', url: 'https://openvibe.tools', desc: 'The whole toolbox' },
{ id: 'dev', name: 'Developer tools', url: 'https://dev.openvibe.tools', desc: 'Formatters, encoders, validators' },
{ id: 'dev', name: 'Developer tools', url: 'https://dev.openvibe.tools', desc: 'Formatters, encoders, validators' },
{ id: 'net', name: 'Network diagnostics', url: 'https://net.openvibe.tools', desc: 'Ping, DNS, Whois, SSL' },
{ id: 'net', name: 'Network diagnostics', url: 'https://net.openvibe.tools', desc: 'Ping, DNS, Whois, SSL' },
{ id: 'img', name: 'Image converter', url: 'https://img.openvibe.tools', desc: 'Convert, resize, compress' },
{ id: 'img', name: 'Image converter', url: 'https://img.openvibe.tools', desc: 'Convert, resize, compress' },
@@ -42,7 +42,11 @@
@@ -42,7 +42,11 @@
{ id: 'docs', name: 'PDF & document tools', url: 'https://docs.openvibe.tools', desc: 'Merge, split, compress' },
{ id: 'docs', name: 'PDF & document tools', url: 'https://docs.openvibe.tools', desc: 'Merge, split, compress' },
{ id: 'text', name: 'Text tools', url: 'https://text.openvibe.tools', desc: 'Fancy text, case, ASCII' },
{ id: 'text', name: 'Text tools', url: 'https://text.openvibe.tools', desc: 'Fancy text, case, ASCII' },
{ id: 'yt', name: 'YouTube downloader', url: 'https://yt.openvibe.tools', desc: 'Video and audio' },
{ id: 'yt', name: 'YouTube downloader', url: 'https://yt.openvibe.tools', desc: 'Video and audio' },
+ { id: 'community', name: 'Community & pastes', url: 'https://openvibe.community', desc: 'Pastes, posts and the people of OpenVibe' },
+ { id: 'games', name: 'Games', url: 'https://openvibe.games', desc: 'Scraplandia and browser games' },
+ { id: 'media', name: 'Media', url: 'https://openvibe.media', desc: 'VODs, clips and files for every site' },
{ id: 'network', name: 'Account & themes', url: NETWORK_URL, desc: 'One account, every site' },
{ id: 'network', name: 'Account & themes', url: NETWORK_URL, desc: 'One account, every site' },
+ { id: 'more', name: 'The whole network →', url: NETWORK_URL + '/#network', desc: 'Every OpenVibe site, live and upcoming' },
];
];
const LEGAL = [
const LEGAL = [
@@ -59,7 +63,7 @@
@@ -59,7 +63,7 @@
const DEFAULTS = {
const DEFAULTS = {
service: null, // 'live' | 'tools' | 'dev' | … (auto-detected when omitted)
service: null, // 'live' | 'tools' | 'dev' | … (auto-detected when omitted)
brandName: null,
brandName: null,
- tagline: 'Free, open source and community run. Built in the open by the people using it.',
+ tagline: 'Open source and community run. Built in the open by the people using it.',
variant: 'full',
variant: 'full',
mount: '#ov-footer',
mount: '#ov-footer',
links: [], // [{ heading, items: [{ name, url, onclick? }] }]
links: [], // [{ heading, items: [{ name, url, onclick? }] }]
@@ -84,9 +88,17 @@
@@ -84,9 +88,17 @@
return sub === h ? 'tools' : (sub || 'tools');
return sub === h ? 'tools' : (sub || 'tools');
}
}
+ const TLD_LABELS = { live: 'Live', tools: 'Tools', network: 'Network', media: 'Media', games: 'Games', community: 'Community', chat: 'Chat', codes: 'Codes', blog: 'Blog', wiki: 'Wiki', news: 'News', reviews: 'Reviews', tips: 'Tips', vip: 'VIP', trade: 'Trade', host: 'Host', deals: 'Deals', coupons: 'Coupons' };
+ /** The full site name from the hostname — the same spelling the navbar uses (Pastes.OpenVibe.Tools). */
function brandFor(service) {
function brandFor(service) {
+ const h = typeof location !== 'undefined' ? location.hostname.toLowerCase() : '';
+ 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';
const found = NETWORK.find(n => n.id === service);
const found = NETWORK.find(n => n.id === service);
- if (found) return found.name === 'Account & themes' ? 'OpenVibe' : found.name;
+ 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`;
return 'OpenVibe';
return 'OpenVibe';
}
}
@@ -896,11 +896,11 @@
@@ -896,11 +896,11 @@
items.slice(0, 4).map(h => `<a class="item" href="${escapeAttr(h.url)}"><span class="icon"><i class="fa-solid ${escapeAttr(h.icon || 'fa-clock-rotate-left')}"></i></span><span class="t">${escapeAttr(h.title || h.url)}</span><span class="s">${escapeAttr(h.service_label || h.service || '')}</span></a>`).join('');
items.slice(0, 4).map(h => `<a class="item" href="${escapeAttr(h.url)}"><span class="icon"><i class="fa-solid ${escapeAttr(h.icon || 'fa-clock-rotate-left')}"></i></span><span class="t">${escapeAttr(h.title || h.url)}</span><span class="s">${escapeAttr(h.service_label || h.service || '')}</span></a>`).join('');
};
};
const H = root.OpenVibeHistory;
const H = root.OpenVibeHistory;
- if (H && typeof H.recent === 'function') { H.recent({ limit: 4, token: _config.token }).then(draw).catch(() => draw(null)); return; }
+ if (H && typeof H.recent === 'function') { H.recent({ limit: 4, token: _config.token, apiBase: _config.apiBase }).then(draw).catch(() => draw(null)); return; }
if (document.getElementById('ov-history-loader')) return;
if (document.getElementById('ov-history-loader')) return;
const sc = document.createElement('script'); sc.id = 'ov-history-loader'; sc.async = true;
const sc = document.createElement('script'); sc.id = 'ov-history-loader'; sc.async = true;
sc.src = `${_config.apiBase}/shared/history.js`;
sc.src = `${_config.apiBase}/shared/history.js`;
- sc.onload = () => { try { root.OpenVibeHistory.recent({ limit: 4, token: _config.token }).then(draw).catch(() => draw(null)); } catch { /* */ } };
+ sc.onload = () => { try { root.OpenVibeHistory.recent({ limit: 4, token: _config.token, apiBase: _config.apiBase }).then(draw).catch(() => draw(null)); } catch { /* */ } };
document.head.appendChild(sc);
document.head.appendChild(sc);
}
}
@@ -912,8 +912,7 @@
@@ -912,8 +912,7 @@
const svc = _config.service;
const svc = _config.service;
const brand = resolveBrand();
const brand = resolveBrand();
- nav.setAttribute('data-compact', _config.compact || 'auto');
- nav.setAttribute('data-service', svc);
+ if (typeof nav.setAttribute === 'function') { nav.setAttribute('data-compact', _config.compact || 'auto'); nav.setAttribute('data-service', svc); }
const links = currentLinks();
const links = currentLinks();
const u = _config.user;
const u = _config.user;
@@ -2,6 +2,7 @@
@@ -2,6 +2,7 @@
* ov-mark.js — the OpenVibe "OV" brand mark as a drop-in.
* ov-mark.js — the OpenVibe "OV" brand mark as a drop-in.
* <span class="ov-mark"></span> → 32px animated mark
* <span class="ov-mark"></span> → 32px animated mark
* <span class="ov-mark" data-size="56"> → any size; add data-static="1" for no motion
* <span class="ov-mark" data-size="56"> → any size; add data-static="1" for no motion
+ * <span class="ov-mark" data-variant="live"> → the site's own twist on the motion (see VARIANTS)
* Self-contained: injects its own CSS once, works on any page (Live, Network, static pages).
* Self-contained: injects its own CSS once, works on any page (Live, Network, static pages).
* The O is a ring with a comet arc running around it, the V is drawn inside with a light
* The O is a ring with a comet arc running around it, the V is drawn inside with a light
* sweep, and a dot orbits the ring. Transform/opacity/stroke motion only.
* sweep, and a dot orbits the ring. Transform/opacity/stroke motion only.
@@ -17,7 +18,30 @@
@@ -17,7 +18,30 @@
.ov-mark .r{fill:none;stroke:currentColor;stroke-width:4;opacity:.28}
.ov-mark .r{fill:none;stroke:currentColor;stroke-width:4;opacity:.28}
.ov-mark .c{fill:none;stroke-width:4;stroke-linecap:round;stroke-dasharray:34 79;transform-box:fill-box;transform-origin:center;animation:ovmComet 3.6s linear infinite}
.ov-mark .c{fill:none;stroke-width:4;stroke-linecap:round;stroke-dasharray:34 79;transform-box:fill-box;transform-origin:center;animation:ovmComet 3.6s linear infinite}
.ov-mark .v{fill:none;stroke-width:4.6;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:46;animation:ovmDraw 1.1s cubic-bezier(.2,.8,.2,1) both}
.ov-mark .v{fill:none;stroke-width:4.6;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:46;animation:ovmDraw 1.1s cubic-bezier(.2,.8,.2,1) both}
-.ov-mark .d{fill:#fff;transform-box:fill-box;transform-origin:center;animation:ovmDot 2.4s ease-in-out infinite}
+.ov-mark .d{fill:var(--ovm-dot,#fff);transform-box:fill-box;transform-origin:center;animation:ovmDot 2.4s ease-in-out infinite}
+/* Per-site variants: one family, each site with its own motion so the network reads as one brand
+ with many rooms. Colours stay with the theme (currentColor); only the dot and the rhythm change. */
+.ov-mark[data-variant=live]{--ovm-dot:#ef4444}
+.ov-mark[data-variant=live] .d{animation:ovmOnAir 1.4s ease-in-out infinite}
+.ov-mark[data-variant=tools] .c{animation-duration:2.2s;stroke-dasharray:22 91}
+.ov-mark[data-variant=tools] .o2{display:none}
+.ov-mark[data-variant=media] .c{stroke-dasharray:14 14 14 71;animation-duration:4.4s}
+.ov-mark[data-variant=games] svg{animation:ovmBounce 1.6s cubic-bezier(.3,1.4,.4,1) infinite}
+.ov-mark[data-variant=games] .d{animation-duration:1.6s}
+.ov-mark[data-variant=community] .o2{opacity:.9;r:1.8}
+.ov-mark[data-variant=community] .c{animation-direction:reverse}
+.ov-mark[data-variant=network] .g{animation-duration:2.4s}
+.ov-mark[data-variant=chat] .d{animation:ovmDot 1s ease-in-out infinite}
+.ov-mark[data-variant=stream] .c{stroke-dasharray:60 53;animation-duration:2.8s}
+.ov-mark[data-variant=codes] .v{animation:ovmDraw 1.1s cubic-bezier(.2,.8,.2,1) infinite alternate}
+.ov-mark[data-variant=blog] .c,.ov-mark[data-variant=wiki] .c,.ov-mark[data-variant=news] .c{animation-duration:6s}
+.ov-mark[data-variant=vip]{--ovm-dot:#facc15}
+.ov-mark[data-variant=tips]{--ovm-dot:#22c55e}
+.ov-mark[data-variant=deals],.ov-mark[data-variant=coupons]{--ovm-dot:#f59e0b}
+.ov-mark[data-variant=trade] .c{animation-timing-function:cubic-bezier(.4,0,.2,1)}
+.ov-mark[data-variant=host] svg{animation:none}
+@keyframes ovmOnAir{0%,100%{transform:scale(1);opacity:.7}50%{transform:scale(1.7);opacity:1}}
+@keyframes ovmBounce{0%,100%{transform:translateY(0)}50%{transform:translateY(-2.5px)}}
.ov-mark .o{fill:#fff;filter:drop-shadow(0 0 3px currentColor)}
.ov-mark .o{fill:#fff;filter:drop-shadow(0 0 3px currentColor)}
.ov-mark .o2{opacity:.5}
.ov-mark .o2{opacity:.5}
.ov-mark:hover svg,a:hover>.ov-mark svg,button:hover>.ov-mark svg{animation:ovmSpin .9s cubic-bezier(.2,1.5,.3,1) 1}
.ov-mark:hover svg,a:hover>.ov-mark svg,button:hover>.ov-mark svg{animation:ovmSpin .9s cubic-bezier(.2,1.5,.3,1) 1}
new file mode 100644
new file mode 100644
@@ -0,0 +1,129 @@
@@ -0,0 +1,129 @@
+'use strict';
+// navbar.js — brand resolution from the hostname, compact/short form, overrides and the
+// modular menu/links API (plain node with the same stubbed globals as navbar-auth.test.js).
+const assert = require('assert');
+const path = require('path');
+const NAVBAR_PATH = path.join(__dirname, '..', 'navbar.js');
+
+function stubElement() {
+ const el = {
+ style: {}, textContent: '', className: '', id: '', innerHTML: '', dataset: {}, attrs: {}, children: [],
+ classList: { add() {}, remove() {}, toggle() {} },
+ setAttribute(k, v) { this.attrs[k] = String(v); }, getAttribute(k) { return this.attrs[k] ?? null; },
+ appendChild(c) { this.children.push(c); }, prepend(c) { this.children.unshift(c); }, remove() {}, addEventListener() {},
+ querySelector() { return stubElement(); }, querySelectorAll() { return []; },
+ };
+ return el;
+}
+
+function load(hostname, opts = {}) {
+ global.location = { hostname, href: `https://${hostname}/`, pathname: opts.pathname || '/', protocol: 'https:' };
+ global.window = { location: global.location };
+ try { Object.defineProperty(global, 'navigator', { value: { userAgent: 'test' }, configurable: true }); } catch { /* read-only in some runtimes */ }
+ const created = [];
+ global.document = {
+ cookie: opts.cookie || '', head: stubElement(), body: stubElement(), title: 'T',
+ getElementById: () => null, createElement: (tag) => { const e = stubElement(); e.tag = tag; created.push(e); return e; },
+ addEventListener() {}, removeEventListener() {}, dispatchEvent() {},
+ };
+ global.CustomEvent = class { constructor(t, i) { this.type = t; this.detail = i && i.detail; } };
+ const store = {};
+ global.localStorage = { getItem: (k) => (k in store ? store[k] : null), setItem: (k, v) => { store[k] = String(v); }, removeItem: (k) => { delete store[k]; } };
+ global.sessionStorage = { getItem: () => null, setItem() {}, removeItem() {} };
+ global.fetch = async () => ({ ok: false, status: 404, json: async () => ({}) });
+ delete require.cache[require.resolve(NAVBAR_PATH)];
+ const navbar = require(NAVBAR_PATH);
+ return { navbar, created };
+}
+
+// ── hostname → segments ──────────────────────────────────────
+{
+ const { navbar } = load('pastes.openvibe.tools');
+ navbar.init({ service: 'paste', user: { id: 1, username: 'x' } });
+ const b = navbar.brand();
+ assert.strictEqual(b.name, 'Pastes.OpenVibe.Tools');
+ assert.strictEqual(b.short, 'Pastes');
+ assert.strictEqual(b.tld, 'tools');
+ assert.strictEqual(b.variant, 'tools');
+}
+{
+ const { navbar } = load('json.openvibe.tools');
+ navbar.init({ service: 'dev' });
+ assert.strictEqual(navbar.brand().name, 'JSON.OpenVibe.Tools', 'known acronyms keep their casing');
+}
+{
+ const { navbar } = load('mergepdf.openvibe.tools');
+ navbar.init({ service: 'docs' });
+ assert.strictEqual(navbar.brand().name, 'MergePDF.OpenVibe.Tools');
+}
+{
+ const { navbar } = load('openvibe.live');
+ navbar.init({ service: 'live' });
+ const b = navbar.brand();
+ assert.strictEqual(b.name, 'OpenVibe.Live');
+ assert.strictEqual(b.short, 'OpenVibe.Live', 'no subdomain: nothing to shorten to');
+ assert.strictEqual(b.variant, 'live');
+}
+{
+ const { navbar } = load('www.openvibe.community');
+ navbar.init({ service: 'community' });
+ assert.strictEqual(navbar.brand().name, 'OpenVibe.Community', 'www is not a subdomain brand');
+}
+{
+ const { navbar } = load('play.openvibe.games');
+ navbar.init({ service: 'games' });
+ assert.strictEqual(navbar.brand().name, 'Play.OpenVibe.Games');
+}
+{
+ const { navbar } = load('ingest.openre.stream');
+ navbar.init({ service: 'live' });
+ const b = navbar.brand();
+ assert.strictEqual(b.name, 'Ingest.OpenRe.Stream');
+ assert.strictEqual(b.variant, 'stream');
+}
+{
+ const { navbar } = load('openvibe.vip');
+ navbar.init({ service: 'network' });
+ assert.strictEqual(navbar.brand().name, 'OpenVibe.VIP');
+}
+// ── off-network hosts fall back to the service id ────────────
+{
+ const { navbar } = load('localhost');
+ navbar.init({ service: 'net' });
+ assert.strictEqual(navbar.brand().name, 'Net.OpenVibe.Tools');
+ const { navbar: n2 } = load('localhost');
+ n2.init({ service: 'live' });
+ assert.strictEqual(n2.brand().name, 'OpenVibe.Live');
+}
+// ── overrides ────────────────────────────────────────────────
+{
+ const { navbar } = load('openvibe.tools');
+ navbar.init({ service: 'tools', brandName: 'Paste.OpenVibe' });
+ assert.strictEqual(navbar.brand().name, 'Pastes.OpenVibe.Tools', 'legacy brandName steers the subdomain segment (paste → Pastes, the host name)');
+ const { navbar: n2 } = load('openvibe.tools');
+ n2.init({ service: 'tools', brand: { sub: 'logo', tag: 'beta' } });
+ assert.strictEqual(n2.brand().name, 'Logo.OpenVibe.Tools');
+ assert.strictEqual(n2.brand().tag, 'beta');
+ const { navbar: n3 } = load('openvibe.tools');
+ n3.init({ service: 'tools', brand: { name: 'Custom Name', variant: 'games' } });
+ assert.strictEqual(n3.brand().name, 'Custom Name');
+ assert.strictEqual(n3.brand().variant, 'games');
+}
+// ── rendered markup carries the segments, compact attr and the variant ────
+{
+ const { navbar, created } = load('pastes.openvibe.tools', { pathname: '/mine' });
+ navbar.init({ service: 'paste', user: { id: 1, username: 'x' }, links: [{ label: 'Mine', href: '/mine' }, { label: 'New', href: '/new', icon: 'fa-plus' }], menu: { after: [{ id: 'mp', label: 'My pastes', href: '/my', icon: 'fa-paste' }] }, compact: 'always' });
+ const nav = created.find(e => e.tag === 'nav');
+ assert.ok(nav, 'nav element created');
+ assert.strictEqual(nav.getAttribute('data-compact'), 'always');
+ assert.ok(nav.innerHTML.includes('class="b-sub">Pastes<') && nav.innerHTML.includes('class="b-tld">Tools<'), 'segments rendered');
+ assert.ok(nav.innerHTML.includes('data-variant="tools"'), 'ov-mark variant');
+ assert.ok(nav.innerHTML.includes('href="/mine" class="active"'), 'current path marked active');
+ assert.ok(nav.innerHTML.includes('fa-plus icon'), 'link icon rendered');
+ const dd = created.find(e => e.tag === 'div' && /openvibe-navbar-dropdown-menu/.test(e.innerHTML));
+ assert.ok(dd && dd.innerHTML.includes('data-menu-id="mp"') && dd.innerHTML.includes('My pastes'), 'custom dropdown row rendered');
+ assert.ok(dd.innerHTML.includes('/my#history'), 'History link present');
+ const id = navbar.addMenuItem({ label: 'Runtime row', href: '/r', position: 'before' });
+ assert.ok(id, 'addMenuItem returns an id');
+}
+console.log('navbar brand: all checks passed');