openvibers/openvibe.shared Commits
1.11.0: the OpenVibe Frame — the shared navbar, footer and shipped views are no longer called 'chrome': openvibe-shared/frame (chrome-ssr stays a deprecated alias until 2.0), /api/frame and /api/frame/hit, window.OpenVibeFrame (OpenVibeChrome aliases it), cache key ov_frame_v1; the theme loader's browser-UI colour sync is syncBrowserUi. No behaviour change
Changes
@@@ -4,6 +4,17 @@ All notable changes to `openvibe-shared`. Versions follow [semver](https://semvebreaking change to any exported module, browser global or served file name is a new major.A release is the git tag `vX.Y.Z`; consumers pin the tag's tarball (see README).++## 1.11.0 — 2026-09-24++++**The OpenVibe Frame.** The shared navbar, footer and "shipped" views every OpenVibe site sits in were called the "chrome", which was borrowed jargon easily mistaken for the browser. They are now the Frame:++++- `openvibe-shared/frame` is the server module (formerly `openvibe-shared/chrome-ssr`, which stays as a deprecated alias until 2.0.0).++- The navbar and footer read `https://openvibe.network/api/frame` (Network keeps `/api/chrome` as an alias for older copies) and send their page-view beacon to `/api/frame/hit`.++- They share `window.OpenVibeFrame`; `OpenVibeChrome` points at the same object.++- The browser cache key is `ov_frame_v1`.++++No behaviour change.++## 1.10.0 — 2026-09-24`openvibe-shared/chrome-ssr` gains the server-side pieces of the shared update system, so every site renders the same markup instead of its own copy:@@@ -1,6 +1,6 @@# OpenVibe.Shared--> Versioned UI, chrome, SEO, legal and release-client packages every OpenVibe site renders.++> Versioned UI, the OpenVibe Frame (the navbar, footer and "shipped" views every site sits in), SEO, legal and release-client packages every OpenVibe site renders.**Status:** alpha. The `openvibe-shared` package lives here; the latest tag is v1.4.0, and 1.5.0 (Track R:release manifests, in-place updates in open tabs, update metrics, the mixed-version harness) is on@@@ -24,7 +24,7 @@ were reconciled first.| Kind | Files ||---|---|| Browser scripts, served at `/shared/<file>` (listed in `files.js`) | `navbar.js`, `nav-icons.js`, `theme-loader.js`, `footer.js`, `notification-ui.js`, `account-switcher.js`, `user-card.js`, `ov-mark.js`, `ov-icons.js`, `history.js`, `sso-client.js`, `panels.js`, `ui.js`, `island.js`, `tooltip.js`, `release-watch.js`, `release-update.js`, `openvibe-sw.js` |--| Node modules (`require('openvibe-shared/<name>')`) | `index` (`.`), `analytics` (+ `analytics/{privacy,tracker,retention,schema,event,prune-cli}`), `app-icon`, `auth-client`, `brand`, `builtin-themes`, `chrome-ssr`, `legal`, `middleware`, `notifications`, `seo`, `theme-sync`, `url-resolver`, `files`, `egress` (SSRF-safe addresses and connect-time DNS for outbound fetches of user-chosen hosts), `trace` (the request's W3C trace on outbound calls inside the network), `release`, `release-compat` (tests), `metrics`, `ready`; `footer`, `shipped` (the shared "shipped X ago" pill, recent list and `/updates` log, from the network changelog) and `icons` (= `ov-icons.js`) work on both sides, and `release-update` gives Node its pure `plan()` |++| Node modules (`require('openvibe-shared/<name>')`) | `index` (`.`), `analytics` (+ `analytics/{privacy,tracker,retention,schema,event,prune-cli}`), `app-icon`, `auth-client`, `brand`, `builtin-themes`, `frame` (the OpenVibe Frame on the server; `chrome-ssr` is a deprecated alias), `legal`, `middleware`, `notifications`, `seo`, `theme-sync`, `url-resolver`, `files`, `egress` (SSRF-safe addresses and connect-time DNS for outbound fetches of user-chosen hosts), `trace` (the request's W3C trace on outbound calls inside the network), `release`, `release-compat` (tests), `metrics`, `ready`; `footer`, `shipped` (the shared "shipped X ago" pill, recent list and `/updates` log, from the network changelog) and `icons` (= `ov-icons.js`) work on both sides, and `release-update` gives Node its pure `plan()` || Schemas | `docs/schemas/analytics-event.v1.json` (`analytics/event.v1`, exported as `openvibe-shared/analytics/event.v1.json`) || Generators | `scripts/build-nav-icons.py` (Font Awesome glyphs → `nav-icons.js`, `ov-icons.js`), `scripts/build-navbar-icons.js` (navbar.js's built-in glyphs), `scripts/build-theme-loader.js`, `scripts/build-app-icons.js` |@@@ -277,7 +277,7 @@ immutable versioned artifacts with a CDN-compatible mirror, instead of being venrsynced from Network.**Owns**--- `@openvibe/tokens|ui|chrome|auth-ui|seo|legal|icons|release-client|web-runtime|server-web|testing-web`++- `@openvibe/tokens|ui|frame|auth-ui|seo|legal|icons|release-client|web-runtime|server-web|testing-web`(today still one package, `openvibe-shared`)- design tokens and theme rendering- release manifest client (active-tab update coordinator)@@@ -1,52 +1,4 @@'use strict';--// openvibe-shared/chrome-ssr — the parts of the shared chrome that can be baked into HTML on the server.--// footer(cfg) the full shared footer (HTML + CSS); the browser's footer.js renders into the same element--// noscriptNav(site) a plain navigation bar inside <noscript>: the JavaScript navbar cannot exist without--// scripts, so visitors who block them (and text browsers) still get the brand and the network--// shipped(opts) a home page's "shipped" block: the "🚀 shipped X ago" pill and the recent changes,--// filled by shipped.js; without JavaScript, a link to the update log--// updatesBody(opts) the body of a site's /updates page (the full log; openvibe.network's server-rendered--// log is the no-JavaScript fallback)--// shippedScript() the <script> tag for shipped.js (the footer loads it lazily too; a page that shows the--// log should load it itself)--const footerModule = require('./footer');----const SITES = [['Live', 'https://openvibe.live/'], ['Tools', 'https://openvibe.tools/'], ['Community', 'https://openvibe.community/'], ['Games', 'https://openvibe.games/'], ['Media', 'https://openvibe.media/'], ['Network', 'https://openvibe.network/']];--const esc = (s) => String(s == null ? '' : s).replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));----function noscriptNav({ name, home = '/', links = [] } = {}) {-- const own = links.map(l => `<a href="${esc(l.href)}">${esc(l.label)}</a>`).join('');-- const net = SITES.filter(([n]) => !String(name || '').endsWith('.' + n)).map(([n, u]) => `<a href="${u}">${n}</a>`).join('');-- return `<noscript><nav aria-label="Site" style="display:flex;flex-wrap:wrap;align-items:center;gap:6px 16px;padding:12px 20px;border-bottom:1px solid rgba(255,255,255,.1);font:500 14px/1.4 system-ui,sans-serif"><a href="${esc(home)}" style="font-weight:700;color:inherit;text-decoration:none">${esc(name || 'OpenVibe')}</a>${own}<span style="flex:1"></span>${net}</nav></noscript>`;--}----const NETWORK = 'https://openvibe.network';--const SERVICE_RE = /^[a-z][a-z0-9-]{1,39}$/;--const svc = (s) => (SERVICE_RE.test(String(s || '')) ? String(s) : '');----function shipped({ service, updates = '/updates', title = null, limit = 5 } = {}) {-- const id = svc(service);-- const n = Math.min(Math.max(parseInt(limit, 10) || 5, 1), 20);-- return `<section class="ov-shipped-home" aria-label="What shipped">`-- + `<a data-ov-shipped="latest" data-service="${esc(id)}" href="${esc(updates)}" hidden></a>`-- + `<div data-ov-shipped="list" data-service="${esc(id)}" data-limit="${n}" data-more="${esc(updates)}"${title ? ` data-title="${esc(title)}"` : ''} hidden></div>`-- + `<noscript><p><a href="${esc(updates)}">What shipped recently</a></p></noscript>`-- + `</section>`;--}----function updatesBody({ service, siteName = null, base = NETWORK } = {}) {-- const id = svc(service);-- const name = siteName || (id ? `OpenVibe.${id.charAt(0).toUpperCase()}${id.slice(1)}` : 'OpenVibe');-- const network = `${String(base).replace(/\/+$/, '')}/updates${id ? `?site=${encodeURIComponent(id)}` : ''}`;-- return `<section class="ov-updates">`-- + `<h1>What shipped on ${esc(name)}</h1>`-- + `<p class="ov-updates-lede">Every change deployed to ${esc(name)}, newest first. Each line is a commit from the OpenVibers repositories, linked to the change itself; batches become <a href="https://openvibe.blog/@openvibe">Patch notes on openvibe.blog</a>. Switch to <a href="${esc(String(base).replace(/\/+$/, ''))}/updates">the whole network</a>.</p>`-- + `<div data-ov-shipped="log" data-service="${esc(id)}" data-limit="50"><noscript><p>The update log needs JavaScript here; <a href="${esc(network)}">the same log on openvibe.network</a> works without it.</p></noscript></div>`-- + `</section>`;--}----function shippedScript({ base = NETWORK } = {}) {-- return `<script src="${esc(String(base).replace(/\/+$/, ''))}/shared/shipped.js" defer></script>`;--}----module.exports = { footer: (cfg) => footerModule.ssr(cfg), noscriptNav, shipped, updatesBody, shippedScript };++// Deprecated alias: openvibe-shared/chrome-ssr is now openvibe-shared/frame (the OpenVibe Frame). Kept so++// sites on older code keep working; it goes away in 2.0.0.++module.exports = require('./frame');@@@ -16,7 +16,7 @@ const BROWSER = Object.freeze(['navbar.js', // shared top bar (eager on every site)'nav-icons.js', // navbar glyphs beyond its own, fetched by navbar.js after first paint'theme-loader.js', // applies the theme before first paint-- 'footer.js', // shared footer (also required on the server by chrome-ssr.js)++ 'footer.js', // shared footer (also required on the server by frame.js)'shipped.js', // "Recently shipped" from the network changelog (openvibe.blog)'notification-ui.js', // bell + notification panel'account-switcher.js',@@@ -31,12 +31,12 @@// Where this file was served from: shipped.js is loaded from the same place (the site's own// /shared/ copy, or openvibe.network), so the site's CSP already allows it.const OWN_SRC = (function () { try { return document.currentScript && document.currentScript.src ? document.currentScript.src : null; } catch { return null; } })();-- // ── Shared chrome data (https://openvibe.network/api/chrome) ─────────────────────────────++ // ── Frame data (https://openvibe.network/api/frame) ─────────────────────────────// Sites ordered by real use, footer copy and per-site legal links. Cached per host for 30// minutes in localStorage and refreshed in the background, so pages paint from cache and the// order never jumps while someone is looking at it. Defined once, shared by navbar + footer.-- const OVChrome = root.OpenVibeChrome || (root.OpenVibeChrome = (function () {-- const KEY = 'ov_chrome_v1', TTL = 30 * 60000;++ const OVFrame = root.OpenVibeFrame || root.OpenVibeChrome || (root.OpenVibeFrame = root.OpenVibeChrome = (function () {++ const KEY = 'ov_frame_v1', TTL = 30 * 60000;let inflight = null;const host = () => (typeof location !== 'undefined' ? location.hostname : '');const https = (u) => { try { return new URL(u).protocol === 'https:'; } catch { return false; } };@@@ -52,7 +52,7 @@function refresh() {if (typeof window === 'undefined' || typeof document === 'undefined') return Promise.resolve(null); // server-side render: never call outif (inflight || typeof fetch === 'undefined') return inflight || Promise.resolve(null);-- inflight = fetch('https://openvibe.network/api/chrome?host=' + encodeURIComponent(host()), { credentials: 'omit' })++ inflight = fetch('https://openvibe.network/api/frame?host=' + encodeURIComponent(host()), { credentials: 'omit' }).then(r => (r.ok ? r.json() : null)).then(clean).then(d => { if (d) { try { localStorage.setItem(KEY, JSON.stringify({ at: Date.now(), host: host(), data: d })); } catch { /* */ } } return d; }).catch(() => null);@@@ -151,9 +151,9 @@}function legalHref(item, cfg) {-- // Each site answers for itself: the chrome service names this domain's own documents.-- const chrome = cfg._chrome;-- if (chrome && chrome.footer.legal && !cfg.legalBase) return chrome.footer.legal[item.key];++ // Each site answers for itself: the Frame service names this domain's own documents.++ const frame = cfg._frame;++ if (frame && frame.footer.legal && !cfg.legalBase) return frame.footer.legal[item.key];// Policies live on whichever site owns them; a tool subdomain links out rather than 404ing.if (cfg.legalBase) return cfg.legalBase.replace(/\/$/, '') + item.path;if (typeof location !== 'undefined' && location.hostname.endsWith('openvibe.live')) return item.path;@@@ -202,14 +202,14 @@const c = { ...DEFAULTS, ...(cfg || {}) };const service = c.service || detectService();const brand = c.brandName || brandFor(service);-- const chrome = c._chrome = OVChrome.get(() => { try { render(); } catch { /* */ } });-- // Most used sites first when the chrome service has spoken; the built-in list otherwise.-- const network = chrome && chrome.nav.length-- ? chrome.nav.filter(n => n.id !== service && !(typeof location !== 'undefined' && new URL(n.url).hostname === location.hostname)).map(n => ({ name: 'OpenVibe.' + n.name, url: n.url, desc: n.tagline }))++ const frame = c._frame = OVFrame.get(() => { try { render(); } catch { /* */ } });++ // Most used sites first when the Frame service has spoken; the built-in list otherwise.++ const network = frame && frame.nav.length++ ? frame.nav.filter(n => n.id !== service && !(typeof location !== 'undefined' && new URL(n.url).hostname === location.hostname)).map(n => ({ name: 'OpenVibe.' + n.name, url: n.url, desc: n.tagline })): NETWORK.filter(n => n.id !== service);-- const tagline = (cfg && cfg.tagline) || (chrome && chrome.footer.blurb) || c.tagline;-- const popular = chrome ? chrome.footer.popular : [];-- const discover = chrome ? chrome.footer.discover.filter(d => !network.slice(0, 6).some(n => n.url === d.url)) : [];++ const tagline = (cfg && cfg.tagline) || (frame && frame.footer.blurb) || c.tagline;++ const popular = frame ? frame.footer.popular : [];++ const discover = frame ? frame.footer.discover.filter(d => !network.slice(0, 6).some(n => n.url === d.url)) : [];const year = new Date().getFullYear();if (c.variant === 'compact') {nnew file mode 100644@@@ -0,0 +1,53 @@++'use strict';++// openvibe-shared/frame — the OpenVibe Frame (the navbar, footer and "shipped" views every OpenVibe site sits++// in): the parts that can be baked into HTML on the server. (Formerly openvibe-shared/chrome-ssr, still an alias.)++// footer(cfg) the full shared footer (HTML + CSS); the browser's footer.js renders into the same element++// noscriptNav(site) a plain navigation bar inside <noscript>: the JavaScript navbar cannot exist without++// scripts, so visitors who block them (and text browsers) still get the brand and the network++// shipped(opts) a home page's "shipped" block: the "🚀 shipped X ago" pill and the recent changes,++// filled by shipped.js; without JavaScript, a link to the update log++// updatesBody(opts) the body of a site's /updates page (the full log; openvibe.network's server-rendered++// log is the no-JavaScript fallback)++// shippedScript() the <script> tag for shipped.js (the footer loads it lazily too; a page that shows the++// log should load it itself)++const footerModule = require('./footer');++++const SITES = [['Live', 'https://openvibe.live/'], ['Tools', 'https://openvibe.tools/'], ['Community', 'https://openvibe.community/'], ['Games', 'https://openvibe.games/'], ['Media', 'https://openvibe.media/'], ['Network', 'https://openvibe.network/']];++const esc = (s) => String(s == null ? '' : s).replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));++++function noscriptNav({ name, home = '/', links = [] } = {}) {++ const own = links.map(l => `<a href="${esc(l.href)}">${esc(l.label)}</a>`).join('');++ const net = SITES.filter(([n]) => !String(name || '').endsWith('.' + n)).map(([n, u]) => `<a href="${u}">${n}</a>`).join('');++ return `<noscript><nav aria-label="Site" style="display:flex;flex-wrap:wrap;align-items:center;gap:6px 16px;padding:12px 20px;border-bottom:1px solid rgba(255,255,255,.1);font:500 14px/1.4 system-ui,sans-serif"><a href="${esc(home)}" style="font-weight:700;color:inherit;text-decoration:none">${esc(name || 'OpenVibe')}</a>${own}<span style="flex:1"></span>${net}</nav></noscript>`;++}++++const NETWORK = 'https://openvibe.network';++const SERVICE_RE = /^[a-z][a-z0-9-]{1,39}$/;++const svc = (s) => (SERVICE_RE.test(String(s || '')) ? String(s) : '');++++function shipped({ service, updates = '/updates', title = null, limit = 5 } = {}) {++ const id = svc(service);++ const n = Math.min(Math.max(parseInt(limit, 10) || 5, 1), 20);++ return `<section class="ov-shipped-home" aria-label="What shipped">`++ + `<a data-ov-shipped="latest" data-service="${esc(id)}" href="${esc(updates)}" hidden></a>`++ + `<div data-ov-shipped="list" data-service="${esc(id)}" data-limit="${n}" data-more="${esc(updates)}"${title ? ` data-title="${esc(title)}"` : ''} hidden></div>`++ + `<noscript><p><a href="${esc(updates)}">What shipped recently</a></p></noscript>`++ + `</section>`;++}++++function updatesBody({ service, siteName = null, base = NETWORK } = {}) {++ const id = svc(service);++ const name = siteName || (id ? `OpenVibe.${id.charAt(0).toUpperCase()}${id.slice(1)}` : 'OpenVibe');++ const network = `${String(base).replace(/\/+$/, '')}/updates${id ? `?site=${encodeURIComponent(id)}` : ''}`;++ return `<section class="ov-updates">`++ + `<h1>What shipped on ${esc(name)}</h1>`++ + `<p class="ov-updates-lede">Every change deployed to ${esc(name)}, newest first. Each line is a commit from the OpenVibers repositories, linked to the change itself; batches become <a href="https://openvibe.blog/@openvibe">Patch notes on openvibe.blog</a>. Switch to <a href="${esc(String(base).replace(/\/+$/, ''))}/updates">the whole network</a>.</p>`++ + `<div data-ov-shipped="log" data-service="${esc(id)}" data-limit="50"><noscript><p>The update log needs JavaScript here; <a href="${esc(network)}">the same log on openvibe.network</a> works without it.</p></noscript></div>`++ + `</section>`;++}++++function shippedScript({ base = NETWORK } = {}) {++ return `<script src="${esc(String(base).replace(/\/+$/, ''))}/shared/shipped.js" defer></script>`;++}++++module.exports = { footer: (cfg) => footerModule.ssr(cfg), noscriptNav, shipped, updatesBody, shippedScript };@@@ -29,7 +29,7 @@ const userContent = (site) => has(site, 'streaming', 'ugc', 'hosting', 'games');// Clause helpers. Every statement of fact below is backed by code in one of the repositories:// retention windows (OpenVibe.Tools apps/*/server/config.js), cookies (server/auth/*), the anonymous--// page-view count (Network server/chrome), cross-site history (Network server/history), display and++// page-view count (Network server/frame), cross-site history (Network server/history), display and// theme preferences (theme-loader.js), AI site copy (Live server/internal/routes.js).const P = (...t) => t.map(x => `<p>${x}</p>`).join('');const UL = (items) => `<ul>${items.filter(Boolean).map(i => `<li>${i}</li>`).join('')}</ul>`;@@@ -180,7 +180,7 @@ function page(kind, site) {${head}${require('./app-icon').headTags({ site: site.id })}<script src="https://openvibe.network/shared/theme-loader.js" defer></script><style>${CSS}</style></head><body>--${require('./chrome-ssr').noscriptNav({ name: site.name })}++${require('./frame').noscriptNav({ name: site.name })}<main>${article(kind, site)}</main>${require('./footer').ssr({ service: site.service || site.id || 'network', variant: 'compact' })}<script src="https://openvibe.network/shared/navbar.js" defer></script><script src="https://openvibe.network/shared/footer.js" defer></script><script>addEventListener('DOMContentLoaded',function(){var t=null;try{t=(document.cookie.match(/(?:^|; )ov_token=([^;]*)/)||[])[1]||localStorage.getItem('ov_token')}catch(e){}@@@ -299,9 +299,9 @@}/* Touch and keyboard feedback. Phones flash a square, theme-blind blue box over anything-- tapped (-webkit-tap-highlight-color); the chrome turns it off and shows a slight press++ tapped (-webkit-tap-highlight-color); the Frame turns it off and shows a slight pressinstead, and keyboard focus gets one ring in the theme accent. :where() keeps it at zero-- specificity, so the chrome's own :focus-visible rules and a host page's rules still win.++ specificity, so the Frame's own :focus-visible rules and a host page's rules still win.\`scale\` is its own property, so it composes with any transform already in use. */.openvibe-navbar, .openvibe-navbar-dropdown, .ovnav-launcher, .ovnav-drawer { -webkit-tap-highlight-color: transparent; }:where(.openvibe-navbar, .openvibe-navbar-dropdown, .ovnav-launcher, .ovnav-drawer) :where(a, button, [role="button"]):not(:disabled):active { scale: .96; }@@@ -536,12 +536,12 @@}-- // ── Shared chrome data (https://openvibe.network/api/chrome) ─────────────────────────────++ // ── Frame data (https://openvibe.network/api/frame) ─────────────────────────────// Sites ordered by real use, footer copy and per-site legal links. Cached per host for 30// minutes in localStorage and refreshed in the background, so pages paint from cache and the// order never jumps while someone is looking at it. Defined once, shared by navbar + footer.-- const OVChrome = root.OpenVibeChrome || (root.OpenVibeChrome = (function () {-- const KEY = 'ov_chrome_v1', TTL = 30 * 60000;++ const OVFrame = root.OpenVibeFrame || root.OpenVibeChrome || (root.OpenVibeFrame = root.OpenVibeChrome = (function () {++ const KEY = 'ov_frame_v1', TTL = 30 * 60000;let inflight = null;const host = () => (typeof location !== 'undefined' ? location.hostname : '');const https = (u) => { try { return new URL(u).protocol === 'https:'; } catch { return false; } };@@@ -556,7 +556,7 @@function cached() { try { const c = JSON.parse(localStorage.getItem(KEY) || 'null'); return c && c.host === host() && c.data ? c : null; } catch { return null; } }function refresh() {if (inflight || typeof fetch === 'undefined') return inflight || Promise.resolve(null);-- inflight = fetch('https://openvibe.network/api/chrome?host=' + encodeURIComponent(host()), { credentials: 'omit' })++ inflight = fetch('https://openvibe.network/api/frame?host=' + encodeURIComponent(host()), { credentials: 'omit' }).then(r => (r.ok ? r.json() : null)).then(clean).then(d => { if (d) { try { localStorage.setItem(KEY, JSON.stringify({ at: Date.now(), host: host(), data: d })); } catch { /* */ } } return d; }).catch(() => null);@@@ -618,7 +618,7 @@// Sites and families always filter locally; tools need the catalog. Without it (offline, blocked),// the last row hands the query to the Tools search page, so typing never does nothing.const hit = (x) => (x.name + ' ' + (x.desc || '')).toLowerCase().includes(q);-- const siteList = (OVChrome.get() && OVChrome.get().nav.length ? OVChrome.get().nav.map(n => ({ name: n.name, desc: n.tagline, icon: n.icon || n.id, url: n.url })) : LAUNCHER_SITES).filter(hit);++ const siteList = (OVFrame.get() && OVFrame.get().nav.length ? OVFrame.get().nav.map(n => ({ name: n.name, desc: n.tagline, icon: n.icon || n.id, url: n.url })) : LAUNCHER_SITES).filter(hit);const famList = fams.filter(hit);const tools = cat ? cat.tools.filter(t => t.k.includes(q)).slice(0, 12) : [];const more = `<a class="ovl-fam ovl-all" href="${escapeAttr(TOOLS_SEARCH + encodeURIComponent(q))}"><span class="ov-icon" data-icon="search" data-size="24" data-fx="none"></span><span><b>Search all tools for “${escapeAttr(q)}”</b></span></a>`;@@@ -627,9 +627,9 @@+ `<div class="ovl-h">Tools${cat ? '' : ' <span>loading…</span>'}</div><div class="ovl-fams">${tools.map(t => tile(t, 'ovl-fam')).join('')}${more}</div>`;return;}-- const chrome = OVChrome.get();-- const sites = chrome && chrome.nav.length ? chrome.nav.map(n => ({ name: n.name, desc: n.tagline, icon: n.icon || n.id, url: n.url })) : LAUNCHER_SITES;-- const soon = chrome ? chrome.soon : [];++ const frame = OVFrame.get();++ const sites = frame && frame.nav.length ? frame.nav.map(n => ({ name: n.name, desc: n.tagline, icon: n.icon || n.id, url: n.url })) : LAUNCHER_SITES;++ const soon = frame ? frame.soon : [];// On a tool: every address it answers to (short, search-friendly, mirrors, custom domains), so people// can pick the one they will remember.const cur = currentHost().toLowerCase();@@@ -676,7 +676,7 @@try {if (navigator.globalPrivacyControl || navigator.doNotTrack === '1') return;if (!/^https:$/.test(location.protocol)) return;-- const url = 'https://openvibe.network/api/chrome/hit';++ const url = 'https://openvibe.network/api/frame/hit';if (navigator.sendBeacon) navigator.sendBeacon(url);} catch { /* */ }}@@@ -733,11 +733,11 @@document.head.appendChild(sc);}-- /** Sites in the user menu: most used first (chrome data), never the one we are on. */++ /** Sites in the user menu: most used first (Frame data), never the one we are on. */function acrossHTML() {-- const chrome = OVChrome.get();++ const frame = OVFrame.get();const here = currentHost().toLowerCase();-- const list = (chrome && chrome.nav.length ? chrome.nav.map(n => ({ name: n.name, url: n.url, icon: n.icon || n.id })) : LAUNCHER_SITES)++ const list = (frame && frame.nav.length ? frame.nav.map(n => ({ name: n.name, url: n.url, icon: n.icon || n.id })) : LAUNCHER_SITES).filter(n => { try { const h = new URL(n.url).hostname; return h !== here && !here.endsWith('.' + h); } catch { return false; } }).slice(0, 5);return list.map(n => `<a href="${escapeAttr(n.url)}"><span class="icon"><span class="ov-icon" data-icon="${escapeAttr(n.icon)}" data-size="20" data-fx="none"></span></span> ${escapeAttr(n.name)}</a>`).join('');}@@@ -767,12 +767,12 @@/** The network's most used sites, after the page's own links (networkLinks: false turns it off). */function networkLinksHTML(pageLinks, inDrawer) {if (_config.networkLinks === false) return '';-- const chrome = OVChrome.get((d) => { if (d && _navEl && !_navEl.querySelector('.ovnav-net')) { try { render(); } catch { /* */ } } });-- if (!chrome) return '';++ const frame = OVFrame.get((d) => { if (d && _navEl && !_navEl.querySelector('.ovnav-net')) { try { render(); } catch { /* */ } } });++ if (!frame) return '';const here = currentHost().toLowerCase();const taken = new Set((pageLinks || []).map(l => { try { return new URL(l.href, location.href).hostname; } catch { return ''; } }));const max = inDrawer ? 6 : (typeof _config.networkLinks === 'number' ? _config.networkLinks : 4);-- const pick = chrome.nav.filter(n => { try { const h = new URL(n.url).hostname; return h !== here && !here.endsWith('.' + h) && !taken.has(h); } catch { return false; } }).slice(0, max);++ const pick = frame.nav.filter(n => { try { const h = new URL(n.url).hostname; return h !== here && !here.endsWith('.' + h) && !taken.has(h); } catch { return false; } }).slice(0, max);if (!pick.length) return '';return (inDrawer ? '<div class="ud-label">Across OpenVibe</div>' : `<span class="ovnav-sep" aria-hidden="true"></span>`) + pick.map(n => `<a class="ovnav-net" href="${escapeAttr(n.url)}" title="${escapeAttr(n.tagline)}">${escapeAttr(n.name)}</a>`).join('');}@@@ -1,12 +1,12 @@{"name": "openvibe-shared",-- "version": "1.10.0",++ "version": "1.11.0","lockfileVersion": 3,"requires": true,"packages": {"": {"name": "openvibe-shared",-- "version": "1.10.0",++ "version": "1.11.0","license": "MIT","dependencies": {"jsonwebtoken": "^9.0.0"@@@ -1,6 +1,6 @@{"name": "openvibe-shared",-- "version": "1.10.0",++ "version": "1.11.0","description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site","main": "index.js","exports": {@@@ -19,6 +19,7 @@"./auth-client": "./auth-client.js","./brand": "./brand.js","./builtin-themes": "./builtin-themes.js",++ "./frame": "./frame.js","./chrome-ssr": "./chrome-ssr.js","./egress": "./egress.js","./files": "./files.js",ddeleted file mode 100644@@@ -1,22 +0,0 @@--'use strict';--// openvibe-shared/chrome-ssr: the server-side pieces of the shared chrome, including the "shipped"--// block for a home page and the body of a site's /updates page (shipped.js fills both).--const assert = require('assert');--const chrome = require('../chrome-ssr');----const home = chrome.shipped({ service: 'wiki', title: 'Recently shipped on OpenVibe.Wiki <x>' });--assert.ok(home.includes('data-ov-shipped="latest" data-service="wiki" href="/updates"'));--assert.ok(home.includes('data-ov-shipped="list" data-service="wiki" data-limit="5" data-more="/updates"'));--assert.ok(home.includes('data-title="Recently shipped on OpenVibe.Wiki <x>"'), 'title escaped');--assert.ok(home.includes('<noscript><p><a href="/updates">'), 'a plain link without JavaScript');--assert.ok(chrome.shipped({ service: '"><script>' }).includes('data-service=""'), 'a bad id never reaches the markup');----const body = chrome.updatesBody({ service: 'wiki' });--assert.ok(body.includes('<h1>What shipped on OpenVibe.Wiki</h1>'));--assert.ok(body.includes('data-ov-shipped="log" data-service="wiki"'));--assert.ok(body.includes('https://openvibe.network/updates?site=wiki'), 'the no-JavaScript fallback is the network log');--assert.strictEqual(chrome.shippedScript(), '<script src="https://openvibe.network/shared/shipped.js" defer></script>');----const foot = chrome.footer({ service: 'wiki', variant: 'full', updates: '/updates' });--assert.ok(foot.includes('data-ov-shipped="latest"') && foot.includes('href="/updates"'));--console.log('chrome-ssr: all checks passed');nnew file mode 100644@@@ -0,0 +1,23 @@++'use strict';++// openvibe-shared/frame: the server-side pieces of the OpenVibe Frame, including the "shipped"++// block for a home page and the body of a site's /updates page (shipped.js fills both).++const assert = require('assert');++const frame = require('../frame');++assert.strictEqual(require('../chrome-ssr'), frame, 'the old name is an alias');++++const home = frame.shipped({ service: 'wiki', title: 'Recently shipped on OpenVibe.Wiki <x>' });++assert.ok(home.includes('data-ov-shipped="latest" data-service="wiki" href="/updates"'));++assert.ok(home.includes('data-ov-shipped="list" data-service="wiki" data-limit="5" data-more="/updates"'));++assert.ok(home.includes('data-title="Recently shipped on OpenVibe.Wiki <x>"'), 'title escaped');++assert.ok(home.includes('<noscript><p><a href="/updates">'), 'a plain link without JavaScript');++assert.ok(frame.shipped({ service: '"><script>' }).includes('data-service=""'), 'a bad id never reaches the markup');++++const body = frame.updatesBody({ service: 'wiki' });++assert.ok(body.includes('<h1>What shipped on OpenVibe.Wiki</h1>'));++assert.ok(body.includes('data-ov-shipped="log" data-service="wiki"'));++assert.ok(body.includes('https://openvibe.network/updates?site=wiki'), 'the no-JavaScript fallback is the network log');++assert.strictEqual(frame.shippedScript(), '<script src="https://openvibe.network/shared/shipped.js" defer></script>');++++const foot = frame.footer({ service: 'wiki', variant: 'full', updates: '/updates' });++assert.ok(foot.includes('data-ov-shipped="latest"') && foot.includes('href="/updates"'));++console.log('frame: all checks passed');@@@ -111,10 +111,10 @@// The inline critical style paints a fixed dark canvas before any stylesheet; keep it in step with the theme.if (vars['--bg-primary']) el.style.background = vars['--bg-primary'];if (vars['--text-primary']) el.style.color = vars['--text-primary'];-- syncChrome();++ syncBrowserUi();}-- // ── Browser chrome follows the theme ─────────────────────────────────────────────────++ // ── The browser's own UI (address bar, theme-color) follows the theme ─────────────────────────────────────────────────// The tab icon and the browser/OS toolbar colour are repainted from the theme that was just// applied, so a violet, light or custom theme gets a matching favicon and address bar. Runs on// every apply: first paint, a change on this page, a change in another tab, a server sync.@@@ -132,12 +132,12 @@+ '<path d="M0,-18 A18,18 0 0,1 17.4,4.6" fill="none" stroke="url(#r)" stroke-width="4.4" stroke-linecap="round"/><path d="M-9.5,-7 L0,10 L9.5,-7" fill="none" stroke="' + (document.documentElement.getAttribute('data-theme-mode') === 'light' ? accent : '#fff') + '" stroke-width="4.8" stroke-linecap="round" stroke-linejoin="round"/>'+ '<circle cx="17.4" cy="4.6" r="3.2" fill="' + (DOTS[site] || light) + '"/></g></svg>');}-- let _chromeKey = '';-- function syncChrome() {++ let _browserUiKey = '';++ function syncBrowserUi() {if (typeof document === 'undefined' || !document.head) return;const accent = readColor('--accent', '#3b82f6'), light = readColor('--accent-light', accent), bg = readColor('--bg-secondary', readColor('--bg-primary', '#0a0f1c'));const mode = document.documentElement.getAttribute('data-theme-mode') || 'dark';-- const key = [accent, light, bg, mode].join('|'); if (key === _chromeKey) return; _chromeKey = key;++ const key = [accent, light, bg, mode].join('|'); if (key === _browserUiKey) return; _browserUiKey = key;try {let meta = document.querySelector('meta[name="theme-color"]');if (!meta) { meta = document.createElement('meta'); meta.name = 'theme-color'; document.head.appendChild(meta); }@@@ -282,7 +282,7 @@const activeId = resolveAndApply();// The icon link may be parsed after this script: repaint once the head is complete.-- if (typeof document !== 'undefined' && document.readyState === 'loading') document.addEventListener('DOMContentLoaded', function () { _chromeKey = ''; syncChrome(); });++ if (typeof document !== 'undefined' && document.readyState === 'loading') document.addEventListener('DOMContentLoaded', function () { _browserUiKey = ''; syncBrowserUi(); });// A theme picked in another tab (or on another OpenVibe page of this site) applies here at once.if (typeof window !== 'undefined') window.addEventListener('storage', function (e) { if (e.key === 'ov_theme') { try { resolveAndApply(); } catch (err) { /* */ } } if (e.key === DISPLAY_KEY) applyDisplay(readDisplay()); });
@@ -4,6 +4,17 @@ All notable changes to `openvibe-shared`. Versions follow [semver](https://semve
@@ -4,6 +4,17 @@ All notable changes to `openvibe-shared`. Versions follow [semver](https://semve
breaking change to any exported module, browser global or served file name is a new major.
breaking change to any exported module, browser global or served file name is a new major.
A release is the git tag `vX.Y.Z`; consumers pin the tag's tarball (see README).
A release is the git tag `vX.Y.Z`; consumers pin the tag's tarball (see README).
+## 1.11.0 — 2026-09-24
+
+**The OpenVibe Frame.** The shared navbar, footer and "shipped" views every OpenVibe site sits in were called the "chrome", which was borrowed jargon easily mistaken for the browser. They are now the Frame:
+
+- `openvibe-shared/frame` is the server module (formerly `openvibe-shared/chrome-ssr`, which stays as a deprecated alias until 2.0.0).
+- The navbar and footer read `https://openvibe.network/api/frame` (Network keeps `/api/chrome` as an alias for older copies) and send their page-view beacon to `/api/frame/hit`.
+- They share `window.OpenVibeFrame`; `OpenVibeChrome` points at the same object.
+- The browser cache key is `ov_frame_v1`.
+
+No behaviour change.
+
## 1.10.0 — 2026-09-24
## 1.10.0 — 2026-09-24
`openvibe-shared/chrome-ssr` gains the server-side pieces of the shared update system, so every site renders the same markup instead of its own copy:
`openvibe-shared/chrome-ssr` gains the server-side pieces of the shared update system, so every site renders the same markup instead of its own copy:
@@ -1,6 +1,6 @@
@@ -1,6 +1,6 @@
# OpenVibe.Shared
# OpenVibe.Shared
-> Versioned UI, chrome, SEO, legal and release-client packages every OpenVibe site renders.
+> Versioned UI, the OpenVibe Frame (the navbar, footer and "shipped" views every site sits in), SEO, legal and release-client packages every OpenVibe site renders.
**Status:** alpha. The `openvibe-shared` package lives here; the latest tag is v1.4.0, and 1.5.0 (Track R:
**Status:** alpha. The `openvibe-shared` package lives here; the latest tag is v1.4.0, and 1.5.0 (Track R:
release manifests, in-place updates in open tabs, update metrics, the mixed-version harness) is on
release manifests, in-place updates in open tabs, update metrics, the mixed-version harness) is on
@@ -24,7 +24,7 @@ were reconciled first.
@@ -24,7 +24,7 @@ were reconciled first.
| Kind | Files |
| Kind | Files |
|---|---|
|---|---|
| Browser scripts, served at `/shared/<file>` (listed in `files.js`) | `navbar.js`, `nav-icons.js`, `theme-loader.js`, `footer.js`, `notification-ui.js`, `account-switcher.js`, `user-card.js`, `ov-mark.js`, `ov-icons.js`, `history.js`, `sso-client.js`, `panels.js`, `ui.js`, `island.js`, `tooltip.js`, `release-watch.js`, `release-update.js`, `openvibe-sw.js` |
| Browser scripts, served at `/shared/<file>` (listed in `files.js`) | `navbar.js`, `nav-icons.js`, `theme-loader.js`, `footer.js`, `notification-ui.js`, `account-switcher.js`, `user-card.js`, `ov-mark.js`, `ov-icons.js`, `history.js`, `sso-client.js`, `panels.js`, `ui.js`, `island.js`, `tooltip.js`, `release-watch.js`, `release-update.js`, `openvibe-sw.js` |
-| Node modules (`require('openvibe-shared/<name>')`) | `index` (`.`), `analytics` (+ `analytics/{privacy,tracker,retention,schema,event,prune-cli}`), `app-icon`, `auth-client`, `brand`, `builtin-themes`, `chrome-ssr`, `legal`, `middleware`, `notifications`, `seo`, `theme-sync`, `url-resolver`, `files`, `egress` (SSRF-safe addresses and connect-time DNS for outbound fetches of user-chosen hosts), `trace` (the request's W3C trace on outbound calls inside the network), `release`, `release-compat` (tests), `metrics`, `ready`; `footer`, `shipped` (the shared "shipped X ago" pill, recent list and `/updates` log, from the network changelog) and `icons` (= `ov-icons.js`) work on both sides, and `release-update` gives Node its pure `plan()` |
+| Node modules (`require('openvibe-shared/<name>')`) | `index` (`.`), `analytics` (+ `analytics/{privacy,tracker,retention,schema,event,prune-cli}`), `app-icon`, `auth-client`, `brand`, `builtin-themes`, `frame` (the OpenVibe Frame on the server; `chrome-ssr` is a deprecated alias), `legal`, `middleware`, `notifications`, `seo`, `theme-sync`, `url-resolver`, `files`, `egress` (SSRF-safe addresses and connect-time DNS for outbound fetches of user-chosen hosts), `trace` (the request's W3C trace on outbound calls inside the network), `release`, `release-compat` (tests), `metrics`, `ready`; `footer`, `shipped` (the shared "shipped X ago" pill, recent list and `/updates` log, from the network changelog) and `icons` (= `ov-icons.js`) work on both sides, and `release-update` gives Node its pure `plan()` |
| Schemas | `docs/schemas/analytics-event.v1.json` (`analytics/event.v1`, exported as `openvibe-shared/analytics/event.v1.json`) |
| Schemas | `docs/schemas/analytics-event.v1.json` (`analytics/event.v1`, exported as `openvibe-shared/analytics/event.v1.json`) |
| Generators | `scripts/build-nav-icons.py` (Font Awesome glyphs → `nav-icons.js`, `ov-icons.js`), `scripts/build-navbar-icons.js` (navbar.js's built-in glyphs), `scripts/build-theme-loader.js`, `scripts/build-app-icons.js` |
| Generators | `scripts/build-nav-icons.py` (Font Awesome glyphs → `nav-icons.js`, `ov-icons.js`), `scripts/build-navbar-icons.js` (navbar.js's built-in glyphs), `scripts/build-theme-loader.js`, `scripts/build-app-icons.js` |
@@ -277,7 +277,7 @@ immutable versioned artifacts with a CDN-compatible mirror, instead of being ven
@@ -277,7 +277,7 @@ immutable versioned artifacts with a CDN-compatible mirror, instead of being ven
rsynced from Network.
rsynced from Network.
**Owns**
**Owns**
-- `@openvibe/tokens|ui|chrome|auth-ui|seo|legal|icons|release-client|web-runtime|server-web|testing-web`
+- `@openvibe/tokens|ui|frame|auth-ui|seo|legal|icons|release-client|web-runtime|server-web|testing-web`
(today still one package, `openvibe-shared`)
(today still one package, `openvibe-shared`)
- design tokens and theme rendering
- design tokens and theme rendering
- release manifest client (active-tab update coordinator)
- release manifest client (active-tab update coordinator)
@@ -1,52 +1,4 @@
@@ -1,52 +1,4 @@
'use strict';
'use strict';
-// openvibe-shared/chrome-ssr — the parts of the shared chrome that can be baked into HTML on the server.
-// footer(cfg) the full shared footer (HTML + CSS); the browser's footer.js renders into the same element
-// noscriptNav(site) a plain navigation bar inside <noscript>: the JavaScript navbar cannot exist without
-// scripts, so visitors who block them (and text browsers) still get the brand and the network
-// shipped(opts) a home page's "shipped" block: the "🚀 shipped X ago" pill and the recent changes,
-// filled by shipped.js; without JavaScript, a link to the update log
-// updatesBody(opts) the body of a site's /updates page (the full log; openvibe.network's server-rendered
-// log is the no-JavaScript fallback)
-// shippedScript() the <script> tag for shipped.js (the footer loads it lazily too; a page that shows the
-// log should load it itself)
-const footerModule = require('./footer');
-
-const SITES = [['Live', 'https://openvibe.live/'], ['Tools', 'https://openvibe.tools/'], ['Community', 'https://openvibe.community/'], ['Games', 'https://openvibe.games/'], ['Media', 'https://openvibe.media/'], ['Network', 'https://openvibe.network/']];
-const esc = (s) => String(s == null ? '' : s).replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
-
-function noscriptNav({ name, home = '/', links = [] } = {}) {
- const own = links.map(l => `<a href="${esc(l.href)}">${esc(l.label)}</a>`).join('');
- const net = SITES.filter(([n]) => !String(name || '').endsWith('.' + n)).map(([n, u]) => `<a href="${u}">${n}</a>`).join('');
- return `<noscript><nav aria-label="Site" style="display:flex;flex-wrap:wrap;align-items:center;gap:6px 16px;padding:12px 20px;border-bottom:1px solid rgba(255,255,255,.1);font:500 14px/1.4 system-ui,sans-serif"><a href="${esc(home)}" style="font-weight:700;color:inherit;text-decoration:none">${esc(name || 'OpenVibe')}</a>${own}<span style="flex:1"></span>${net}</nav></noscript>`;
-}
-
-const NETWORK = 'https://openvibe.network';
-const SERVICE_RE = /^[a-z][a-z0-9-]{1,39}$/;
-const svc = (s) => (SERVICE_RE.test(String(s || '')) ? String(s) : '');
-
-function shipped({ service, updates = '/updates', title = null, limit = 5 } = {}) {
- const id = svc(service);
- const n = Math.min(Math.max(parseInt(limit, 10) || 5, 1), 20);
- return `<section class="ov-shipped-home" aria-label="What shipped">`
- + `<a data-ov-shipped="latest" data-service="${esc(id)}" href="${esc(updates)}" hidden></a>`
- + `<div data-ov-shipped="list" data-service="${esc(id)}" data-limit="${n}" data-more="${esc(updates)}"${title ? ` data-title="${esc(title)}"` : ''} hidden></div>`
- + `<noscript><p><a href="${esc(updates)}">What shipped recently</a></p></noscript>`
- + `</section>`;
-}
-
-function updatesBody({ service, siteName = null, base = NETWORK } = {}) {
- const id = svc(service);
- const name = siteName || (id ? `OpenVibe.${id.charAt(0).toUpperCase()}${id.slice(1)}` : 'OpenVibe');
- const network = `${String(base).replace(/\/+$/, '')}/updates${id ? `?site=${encodeURIComponent(id)}` : ''}`;
- return `<section class="ov-updates">`
- + `<h1>What shipped on ${esc(name)}</h1>`
- + `<p class="ov-updates-lede">Every change deployed to ${esc(name)}, newest first. Each line is a commit from the OpenVibers repositories, linked to the change itself; batches become <a href="https://openvibe.blog/@openvibe">Patch notes on openvibe.blog</a>. Switch to <a href="${esc(String(base).replace(/\/+$/, ''))}/updates">the whole network</a>.</p>`
- + `<div data-ov-shipped="log" data-service="${esc(id)}" data-limit="50"><noscript><p>The update log needs JavaScript here; <a href="${esc(network)}">the same log on openvibe.network</a> works without it.</p></noscript></div>`
- + `</section>`;
-}
-
-function shippedScript({ base = NETWORK } = {}) {
- return `<script src="${esc(String(base).replace(/\/+$/, ''))}/shared/shipped.js" defer></script>`;
-}
-
-module.exports = { footer: (cfg) => footerModule.ssr(cfg), noscriptNav, shipped, updatesBody, shippedScript };
+// Deprecated alias: openvibe-shared/chrome-ssr is now openvibe-shared/frame (the OpenVibe Frame). Kept so
+// sites on older code keep working; it goes away in 2.0.0.
+module.exports = require('./frame');
@@ -16,7 +16,7 @@ const BROWSER = Object.freeze([
@@ -16,7 +16,7 @@ const BROWSER = Object.freeze([
'navbar.js', // shared top bar (eager on every site)
'navbar.js', // shared top bar (eager on every site)
'nav-icons.js', // navbar glyphs beyond its own, fetched by navbar.js after first paint
'nav-icons.js', // navbar glyphs beyond its own, fetched by navbar.js after first paint
'theme-loader.js', // applies the theme before first paint
'theme-loader.js', // applies the theme before first paint
- 'footer.js', // shared footer (also required on the server by chrome-ssr.js)
+ 'footer.js', // shared footer (also required on the server by frame.js)
'shipped.js', // "Recently shipped" from the network changelog (openvibe.blog)
'shipped.js', // "Recently shipped" from the network changelog (openvibe.blog)
'notification-ui.js', // bell + notification panel
'notification-ui.js', // bell + notification panel
'account-switcher.js',
'account-switcher.js',
@@ -31,12 +31,12 @@
@@ -31,12 +31,12 @@
// Where this file was served from: shipped.js is loaded from the same place (the site's own
// Where this file was served from: shipped.js is loaded from the same place (the site's own
// /shared/ copy, or openvibe.network), so the site's CSP already allows it.
// /shared/ copy, or openvibe.network), so the site's CSP already allows it.
const OWN_SRC = (function () { try { return document.currentScript && document.currentScript.src ? document.currentScript.src : null; } catch { return null; } })();
const OWN_SRC = (function () { try { return document.currentScript && document.currentScript.src ? document.currentScript.src : null; } catch { return null; } })();
- // ── Shared chrome data (https://openvibe.network/api/chrome) ─────────────────────────────
+ // ── Frame data (https://openvibe.network/api/frame) ─────────────────────────────
// Sites ordered by real use, footer copy and per-site legal links. Cached per host for 30
// Sites ordered by real use, footer copy and per-site legal links. Cached per host for 30
// minutes in localStorage and refreshed in the background, so pages paint from cache and the
// minutes in localStorage and refreshed in the background, so pages paint from cache and the
// order never jumps while someone is looking at it. Defined once, shared by navbar + footer.
// order never jumps while someone is looking at it. Defined once, shared by navbar + footer.
- const OVChrome = root.OpenVibeChrome || (root.OpenVibeChrome = (function () {
- const KEY = 'ov_chrome_v1', TTL = 30 * 60000;
+ const OVFrame = root.OpenVibeFrame || root.OpenVibeChrome || (root.OpenVibeFrame = root.OpenVibeChrome = (function () {
+ const KEY = 'ov_frame_v1', TTL = 30 * 60000;
let inflight = null;
let inflight = null;
const host = () => (typeof location !== 'undefined' ? location.hostname : '');
const host = () => (typeof location !== 'undefined' ? location.hostname : '');
const https = (u) => { try { return new URL(u).protocol === 'https:'; } catch { return false; } };
const https = (u) => { try { return new URL(u).protocol === 'https:'; } catch { return false; } };
@@ -52,7 +52,7 @@
@@ -52,7 +52,7 @@
function refresh() {
function refresh() {
if (typeof window === 'undefined' || typeof document === 'undefined') return Promise.resolve(null); // server-side render: never call out
if (typeof window === 'undefined' || typeof document === 'undefined') return Promise.resolve(null); // server-side render: never call out
if (inflight || typeof fetch === 'undefined') return inflight || Promise.resolve(null);
if (inflight || typeof fetch === 'undefined') return inflight || Promise.resolve(null);
- inflight = fetch('https://openvibe.network/api/chrome?host=' + encodeURIComponent(host()), { credentials: 'omit' })
+ inflight = fetch('https://openvibe.network/api/frame?host=' + encodeURIComponent(host()), { credentials: 'omit' })
.then(r => (r.ok ? r.json() : null)).then(clean)
.then(r => (r.ok ? r.json() : null)).then(clean)
.then(d => { if (d) { try { localStorage.setItem(KEY, JSON.stringify({ at: Date.now(), host: host(), data: d })); } catch { /* */ } } return d; })
.then(d => { if (d) { try { localStorage.setItem(KEY, JSON.stringify({ at: Date.now(), host: host(), data: d })); } catch { /* */ } } return d; })
.catch(() => null);
.catch(() => null);
@@ -151,9 +151,9 @@
@@ -151,9 +151,9 @@
}
}
function legalHref(item, cfg) {
function legalHref(item, cfg) {
- // Each site answers for itself: the chrome service names this domain's own documents.
- const chrome = cfg._chrome;
- if (chrome && chrome.footer.legal && !cfg.legalBase) return chrome.footer.legal[item.key];
+ // Each site answers for itself: the Frame service names this domain's own documents.
+ const frame = cfg._frame;
+ if (frame && frame.footer.legal && !cfg.legalBase) return frame.footer.legal[item.key];
// Policies live on whichever site owns them; a tool subdomain links out rather than 404ing.
// Policies live on whichever site owns them; a tool subdomain links out rather than 404ing.
if (cfg.legalBase) return cfg.legalBase.replace(/\/$/, '') + item.path;
if (cfg.legalBase) return cfg.legalBase.replace(/\/$/, '') + item.path;
if (typeof location !== 'undefined' && location.hostname.endsWith('openvibe.live')) return item.path;
if (typeof location !== 'undefined' && location.hostname.endsWith('openvibe.live')) return item.path;
@@ -202,14 +202,14 @@
@@ -202,14 +202,14 @@
const c = { ...DEFAULTS, ...(cfg || {}) };
const c = { ...DEFAULTS, ...(cfg || {}) };
const service = c.service || detectService();
const service = c.service || detectService();
const brand = c.brandName || brandFor(service);
const brand = c.brandName || brandFor(service);
- const chrome = c._chrome = OVChrome.get(() => { try { render(); } catch { /* */ } });
- // Most used sites first when the chrome service has spoken; the built-in list otherwise.
- const network = chrome && chrome.nav.length
- ? chrome.nav.filter(n => n.id !== service && !(typeof location !== 'undefined' && new URL(n.url).hostname === location.hostname)).map(n => ({ name: 'OpenVibe.' + n.name, url: n.url, desc: n.tagline }))
+ const frame = c._frame = OVFrame.get(() => { try { render(); } catch { /* */ } });
+ // Most used sites first when the Frame service has spoken; the built-in list otherwise.
+ const network = frame && frame.nav.length
+ ? frame.nav.filter(n => n.id !== service && !(typeof location !== 'undefined' && new URL(n.url).hostname === location.hostname)).map(n => ({ name: 'OpenVibe.' + n.name, url: n.url, desc: n.tagline }))
: NETWORK.filter(n => n.id !== service);
: NETWORK.filter(n => n.id !== service);
- const tagline = (cfg && cfg.tagline) || (chrome && chrome.footer.blurb) || c.tagline;
- const popular = chrome ? chrome.footer.popular : [];
- const discover = chrome ? chrome.footer.discover.filter(d => !network.slice(0, 6).some(n => n.url === d.url)) : [];
+ const tagline = (cfg && cfg.tagline) || (frame && frame.footer.blurb) || c.tagline;
+ const popular = frame ? frame.footer.popular : [];
+ const discover = frame ? frame.footer.discover.filter(d => !network.slice(0, 6).some(n => n.url === d.url)) : [];
const year = new Date().getFullYear();
const year = new Date().getFullYear();
if (c.variant === 'compact') {
if (c.variant === 'compact') {
new file mode 100644
new file mode 100644
@@ -0,0 +1,53 @@
@@ -0,0 +1,53 @@
+'use strict';
+// openvibe-shared/frame — the OpenVibe Frame (the navbar, footer and "shipped" views every OpenVibe site sits
+// in): the parts that can be baked into HTML on the server. (Formerly openvibe-shared/chrome-ssr, still an alias.)
+// footer(cfg) the full shared footer (HTML + CSS); the browser's footer.js renders into the same element
+// noscriptNav(site) a plain navigation bar inside <noscript>: the JavaScript navbar cannot exist without
+// scripts, so visitors who block them (and text browsers) still get the brand and the network
+// shipped(opts) a home page's "shipped" block: the "🚀 shipped X ago" pill and the recent changes,
+// filled by shipped.js; without JavaScript, a link to the update log
+// updatesBody(opts) the body of a site's /updates page (the full log; openvibe.network's server-rendered
+// log is the no-JavaScript fallback)
+// shippedScript() the <script> tag for shipped.js (the footer loads it lazily too; a page that shows the
+// log should load it itself)
+const footerModule = require('./footer');
+
+const SITES = [['Live', 'https://openvibe.live/'], ['Tools', 'https://openvibe.tools/'], ['Community', 'https://openvibe.community/'], ['Games', 'https://openvibe.games/'], ['Media', 'https://openvibe.media/'], ['Network', 'https://openvibe.network/']];
+const esc = (s) => String(s == null ? '' : s).replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
+
+function noscriptNav({ name, home = '/', links = [] } = {}) {
+ const own = links.map(l => `<a href="${esc(l.href)}">${esc(l.label)}</a>`).join('');
+ const net = SITES.filter(([n]) => !String(name || '').endsWith('.' + n)).map(([n, u]) => `<a href="${u}">${n}</a>`).join('');
+ return `<noscript><nav aria-label="Site" style="display:flex;flex-wrap:wrap;align-items:center;gap:6px 16px;padding:12px 20px;border-bottom:1px solid rgba(255,255,255,.1);font:500 14px/1.4 system-ui,sans-serif"><a href="${esc(home)}" style="font-weight:700;color:inherit;text-decoration:none">${esc(name || 'OpenVibe')}</a>${own}<span style="flex:1"></span>${net}</nav></noscript>`;
+}
+
+const NETWORK = 'https://openvibe.network';
+const SERVICE_RE = /^[a-z][a-z0-9-]{1,39}$/;
+const svc = (s) => (SERVICE_RE.test(String(s || '')) ? String(s) : '');
+
+function shipped({ service, updates = '/updates', title = null, limit = 5 } = {}) {
+ const id = svc(service);
+ const n = Math.min(Math.max(parseInt(limit, 10) || 5, 1), 20);
+ return `<section class="ov-shipped-home" aria-label="What shipped">`
+ + `<a data-ov-shipped="latest" data-service="${esc(id)}" href="${esc(updates)}" hidden></a>`
+ + `<div data-ov-shipped="list" data-service="${esc(id)}" data-limit="${n}" data-more="${esc(updates)}"${title ? ` data-title="${esc(title)}"` : ''} hidden></div>`
+ + `<noscript><p><a href="${esc(updates)}">What shipped recently</a></p></noscript>`
+ + `</section>`;
+}
+
+function updatesBody({ service, siteName = null, base = NETWORK } = {}) {
+ const id = svc(service);
+ const name = siteName || (id ? `OpenVibe.${id.charAt(0).toUpperCase()}${id.slice(1)}` : 'OpenVibe');
+ const network = `${String(base).replace(/\/+$/, '')}/updates${id ? `?site=${encodeURIComponent(id)}` : ''}`;
+ return `<section class="ov-updates">`
+ + `<h1>What shipped on ${esc(name)}</h1>`
+ + `<p class="ov-updates-lede">Every change deployed to ${esc(name)}, newest first. Each line is a commit from the OpenVibers repositories, linked to the change itself; batches become <a href="https://openvibe.blog/@openvibe">Patch notes on openvibe.blog</a>. Switch to <a href="${esc(String(base).replace(/\/+$/, ''))}/updates">the whole network</a>.</p>`
+ + `<div data-ov-shipped="log" data-service="${esc(id)}" data-limit="50"><noscript><p>The update log needs JavaScript here; <a href="${esc(network)}">the same log on openvibe.network</a> works without it.</p></noscript></div>`
+ + `</section>`;
+}
+
+function shippedScript({ base = NETWORK } = {}) {
+ return `<script src="${esc(String(base).replace(/\/+$/, ''))}/shared/shipped.js" defer></script>`;
+}
+
+module.exports = { footer: (cfg) => footerModule.ssr(cfg), noscriptNav, shipped, updatesBody, shippedScript };
@@ -29,7 +29,7 @@ const userContent = (site) => has(site, 'streaming', 'ugc', 'hosting', 'games');
@@ -29,7 +29,7 @@ const userContent = (site) => has(site, 'streaming', 'ugc', 'hosting', 'games');
// Clause helpers. Every statement of fact below is backed by code in one of the repositories:
// Clause helpers. Every statement of fact below is backed by code in one of the repositories:
// retention windows (OpenVibe.Tools apps/*/server/config.js), cookies (server/auth/*), the anonymous
// retention windows (OpenVibe.Tools apps/*/server/config.js), cookies (server/auth/*), the anonymous
-// page-view count (Network server/chrome), cross-site history (Network server/history), display and
+// page-view count (Network server/frame), cross-site history (Network server/history), display and
// theme preferences (theme-loader.js), AI site copy (Live server/internal/routes.js).
// theme preferences (theme-loader.js), AI site copy (Live server/internal/routes.js).
const P = (...t) => t.map(x => `<p>${x}</p>`).join('');
const P = (...t) => t.map(x => `<p>${x}</p>`).join('');
const UL = (items) => `<ul>${items.filter(Boolean).map(i => `<li>${i}</li>`).join('')}</ul>`;
const UL = (items) => `<ul>${items.filter(Boolean).map(i => `<li>${i}</li>`).join('')}</ul>`;
@@ -180,7 +180,7 @@ function page(kind, site) {
@@ -180,7 +180,7 @@ function page(kind, site) {
${head}
${head}
${require('./app-icon').headTags({ site: site.id })}
${require('./app-icon').headTags({ site: site.id })}
<script src="https://openvibe.network/shared/theme-loader.js" defer></script><style>${CSS}</style></head><body>
<script src="https://openvibe.network/shared/theme-loader.js" defer></script><style>${CSS}</style></head><body>
-${require('./chrome-ssr').noscriptNav({ name: site.name })}
+${require('./frame').noscriptNav({ name: site.name })}
<main>${article(kind, site)}</main>${require('./footer').ssr({ service: site.service || site.id || 'network', variant: 'compact' })}
<main>${article(kind, site)}</main>${require('./footer').ssr({ service: site.service || site.id || 'network', variant: 'compact' })}
<script src="https://openvibe.network/shared/navbar.js" defer></script><script src="https://openvibe.network/shared/footer.js" defer></script>
<script src="https://openvibe.network/shared/navbar.js" defer></script><script src="https://openvibe.network/shared/footer.js" defer></script>
<script>addEventListener('DOMContentLoaded',function(){var t=null;try{t=(document.cookie.match(/(?:^|; )ov_token=([^;]*)/)||[])[1]||localStorage.getItem('ov_token')}catch(e){}
<script>addEventListener('DOMContentLoaded',function(){var t=null;try{t=(document.cookie.match(/(?:^|; )ov_token=([^;]*)/)||[])[1]||localStorage.getItem('ov_token')}catch(e){}
@@ -299,9 +299,9 @@
@@ -299,9 +299,9 @@
}
}
/* Touch and keyboard feedback. Phones flash a square, theme-blind blue box over anything
/* Touch and keyboard feedback. Phones flash a square, theme-blind blue box over anything
- tapped (-webkit-tap-highlight-color); the chrome turns it off and shows a slight press
+ tapped (-webkit-tap-highlight-color); the Frame turns it off and shows a slight press
instead, and keyboard focus gets one ring in the theme accent. :where() keeps it at zero
instead, and keyboard focus gets one ring in the theme accent. :where() keeps it at zero
- specificity, so the chrome's own :focus-visible rules and a host page's rules still win.
+ specificity, so the Frame's own :focus-visible rules and a host page's rules still win.
\`scale\` is its own property, so it composes with any transform already in use. */
\`scale\` is its own property, so it composes with any transform already in use. */
.openvibe-navbar, .openvibe-navbar-dropdown, .ovnav-launcher, .ovnav-drawer { -webkit-tap-highlight-color: transparent; }
.openvibe-navbar, .openvibe-navbar-dropdown, .ovnav-launcher, .ovnav-drawer { -webkit-tap-highlight-color: transparent; }
:where(.openvibe-navbar, .openvibe-navbar-dropdown, .ovnav-launcher, .ovnav-drawer) :where(a, button, [role="button"]):not(:disabled):active { scale: .96; }
:where(.openvibe-navbar, .openvibe-navbar-dropdown, .ovnav-launcher, .ovnav-drawer) :where(a, button, [role="button"]):not(:disabled):active { scale: .96; }
@@ -536,12 +536,12 @@
@@ -536,12 +536,12 @@
}
}
- // ── Shared chrome data (https://openvibe.network/api/chrome) ─────────────────────────────
+ // ── Frame data (https://openvibe.network/api/frame) ─────────────────────────────
// Sites ordered by real use, footer copy and per-site legal links. Cached per host for 30
// Sites ordered by real use, footer copy and per-site legal links. Cached per host for 30
// minutes in localStorage and refreshed in the background, so pages paint from cache and the
// minutes in localStorage and refreshed in the background, so pages paint from cache and the
// order never jumps while someone is looking at it. Defined once, shared by navbar + footer.
// order never jumps while someone is looking at it. Defined once, shared by navbar + footer.
- const OVChrome = root.OpenVibeChrome || (root.OpenVibeChrome = (function () {
- const KEY = 'ov_chrome_v1', TTL = 30 * 60000;
+ const OVFrame = root.OpenVibeFrame || root.OpenVibeChrome || (root.OpenVibeFrame = root.OpenVibeChrome = (function () {
+ const KEY = 'ov_frame_v1', TTL = 30 * 60000;
let inflight = null;
let inflight = null;
const host = () => (typeof location !== 'undefined' ? location.hostname : '');
const host = () => (typeof location !== 'undefined' ? location.hostname : '');
const https = (u) => { try { return new URL(u).protocol === 'https:'; } catch { return false; } };
const https = (u) => { try { return new URL(u).protocol === 'https:'; } catch { return false; } };
@@ -556,7 +556,7 @@
@@ -556,7 +556,7 @@
function cached() { try { const c = JSON.parse(localStorage.getItem(KEY) || 'null'); return c && c.host === host() && c.data ? c : null; } catch { return null; } }
function cached() { try { const c = JSON.parse(localStorage.getItem(KEY) || 'null'); return c && c.host === host() && c.data ? c : null; } catch { return null; } }
function refresh() {
function refresh() {
if (inflight || typeof fetch === 'undefined') return inflight || Promise.resolve(null);
if (inflight || typeof fetch === 'undefined') return inflight || Promise.resolve(null);
- inflight = fetch('https://openvibe.network/api/chrome?host=' + encodeURIComponent(host()), { credentials: 'omit' })
+ inflight = fetch('https://openvibe.network/api/frame?host=' + encodeURIComponent(host()), { credentials: 'omit' })
.then(r => (r.ok ? r.json() : null)).then(clean)
.then(r => (r.ok ? r.json() : null)).then(clean)
.then(d => { if (d) { try { localStorage.setItem(KEY, JSON.stringify({ at: Date.now(), host: host(), data: d })); } catch { /* */ } } return d; })
.then(d => { if (d) { try { localStorage.setItem(KEY, JSON.stringify({ at: Date.now(), host: host(), data: d })); } catch { /* */ } } return d; })
.catch(() => null);
.catch(() => null);
@@ -618,7 +618,7 @@
@@ -618,7 +618,7 @@
// Sites and families always filter locally; tools need the catalog. Without it (offline, blocked),
// Sites and families always filter locally; tools need the catalog. Without it (offline, blocked),
// the last row hands the query to the Tools search page, so typing never does nothing.
// the last row hands the query to the Tools search page, so typing never does nothing.
const hit = (x) => (x.name + ' ' + (x.desc || '')).toLowerCase().includes(q);
const hit = (x) => (x.name + ' ' + (x.desc || '')).toLowerCase().includes(q);
- const siteList = (OVChrome.get() && OVChrome.get().nav.length ? OVChrome.get().nav.map(n => ({ name: n.name, desc: n.tagline, icon: n.icon || n.id, url: n.url })) : LAUNCHER_SITES).filter(hit);
+ const siteList = (OVFrame.get() && OVFrame.get().nav.length ? OVFrame.get().nav.map(n => ({ name: n.name, desc: n.tagline, icon: n.icon || n.id, url: n.url })) : LAUNCHER_SITES).filter(hit);
const famList = fams.filter(hit);
const famList = fams.filter(hit);
const tools = cat ? cat.tools.filter(t => t.k.includes(q)).slice(0, 12) : [];
const tools = cat ? cat.tools.filter(t => t.k.includes(q)).slice(0, 12) : [];
const more = `<a class="ovl-fam ovl-all" href="${escapeAttr(TOOLS_SEARCH + encodeURIComponent(q))}"><span class="ov-icon" data-icon="search" data-size="24" data-fx="none"></span><span><b>Search all tools for “${escapeAttr(q)}”</b></span></a>`;
const more = `<a class="ovl-fam ovl-all" href="${escapeAttr(TOOLS_SEARCH + encodeURIComponent(q))}"><span class="ov-icon" data-icon="search" data-size="24" data-fx="none"></span><span><b>Search all tools for “${escapeAttr(q)}”</b></span></a>`;
@@ -627,9 +627,9 @@
@@ -627,9 +627,9 @@
+ `<div class="ovl-h">Tools${cat ? '' : ' <span>loading…</span>'}</div><div class="ovl-fams">${tools.map(t => tile(t, 'ovl-fam')).join('')}${more}</div>`;
+ `<div class="ovl-h">Tools${cat ? '' : ' <span>loading…</span>'}</div><div class="ovl-fams">${tools.map(t => tile(t, 'ovl-fam')).join('')}${more}</div>`;
return;
return;
}
}
- const chrome = OVChrome.get();
- const sites = chrome && chrome.nav.length ? chrome.nav.map(n => ({ name: n.name, desc: n.tagline, icon: n.icon || n.id, url: n.url })) : LAUNCHER_SITES;
- const soon = chrome ? chrome.soon : [];
+ const frame = OVFrame.get();
+ const sites = frame && frame.nav.length ? frame.nav.map(n => ({ name: n.name, desc: n.tagline, icon: n.icon || n.id, url: n.url })) : LAUNCHER_SITES;
+ const soon = frame ? frame.soon : [];
// On a tool: every address it answers to (short, search-friendly, mirrors, custom domains), so people
// On a tool: every address it answers to (short, search-friendly, mirrors, custom domains), so people
// can pick the one they will remember.
// can pick the one they will remember.
const cur = currentHost().toLowerCase();
const cur = currentHost().toLowerCase();
@@ -676,7 +676,7 @@
@@ -676,7 +676,7 @@
try {
try {
if (navigator.globalPrivacyControl || navigator.doNotTrack === '1') return;
if (navigator.globalPrivacyControl || navigator.doNotTrack === '1') return;
if (!/^https:$/.test(location.protocol)) return;
if (!/^https:$/.test(location.protocol)) return;
- const url = 'https://openvibe.network/api/chrome/hit';
+ const url = 'https://openvibe.network/api/frame/hit';
if (navigator.sendBeacon) navigator.sendBeacon(url);
if (navigator.sendBeacon) navigator.sendBeacon(url);
} catch { /* */ }
} catch { /* */ }
}
}
@@ -733,11 +733,11 @@
@@ -733,11 +733,11 @@
document.head.appendChild(sc);
document.head.appendChild(sc);
}
}
- /** Sites in the user menu: most used first (chrome data), never the one we are on. */
+ /** Sites in the user menu: most used first (Frame data), never the one we are on. */
function acrossHTML() {
function acrossHTML() {
- const chrome = OVChrome.get();
+ const frame = OVFrame.get();
const here = currentHost().toLowerCase();
const here = currentHost().toLowerCase();
- const list = (chrome && chrome.nav.length ? chrome.nav.map(n => ({ name: n.name, url: n.url, icon: n.icon || n.id })) : LAUNCHER_SITES)
+ const list = (frame && frame.nav.length ? frame.nav.map(n => ({ name: n.name, url: n.url, icon: n.icon || n.id })) : LAUNCHER_SITES)
.filter(n => { try { const h = new URL(n.url).hostname; return h !== here && !here.endsWith('.' + h); } catch { return false; } }).slice(0, 5);
.filter(n => { try { const h = new URL(n.url).hostname; return h !== here && !here.endsWith('.' + h); } catch { return false; } }).slice(0, 5);
return list.map(n => `<a href="${escapeAttr(n.url)}"><span class="icon"><span class="ov-icon" data-icon="${escapeAttr(n.icon)}" data-size="20" data-fx="none"></span></span> ${escapeAttr(n.name)}</a>`).join('');
return list.map(n => `<a href="${escapeAttr(n.url)}"><span class="icon"><span class="ov-icon" data-icon="${escapeAttr(n.icon)}" data-size="20" data-fx="none"></span></span> ${escapeAttr(n.name)}</a>`).join('');
}
}
@@ -767,12 +767,12 @@
@@ -767,12 +767,12 @@
/** The network's most used sites, after the page's own links (networkLinks: false turns it off). */
/** The network's most used sites, after the page's own links (networkLinks: false turns it off). */
function networkLinksHTML(pageLinks, inDrawer) {
function networkLinksHTML(pageLinks, inDrawer) {
if (_config.networkLinks === false) return '';
if (_config.networkLinks === false) return '';
- const chrome = OVChrome.get((d) => { if (d && _navEl && !_navEl.querySelector('.ovnav-net')) { try { render(); } catch { /* */ } } });
- if (!chrome) return '';
+ const frame = OVFrame.get((d) => { if (d && _navEl && !_navEl.querySelector('.ovnav-net')) { try { render(); } catch { /* */ } } });
+ if (!frame) return '';
const here = currentHost().toLowerCase();
const here = currentHost().toLowerCase();
const taken = new Set((pageLinks || []).map(l => { try { return new URL(l.href, location.href).hostname; } catch { return ''; } }));
const taken = new Set((pageLinks || []).map(l => { try { return new URL(l.href, location.href).hostname; } catch { return ''; } }));
const max = inDrawer ? 6 : (typeof _config.networkLinks === 'number' ? _config.networkLinks : 4);
const max = inDrawer ? 6 : (typeof _config.networkLinks === 'number' ? _config.networkLinks : 4);
- const pick = chrome.nav.filter(n => { try { const h = new URL(n.url).hostname; return h !== here && !here.endsWith('.' + h) && !taken.has(h); } catch { return false; } }).slice(0, max);
+ const pick = frame.nav.filter(n => { try { const h = new URL(n.url).hostname; return h !== here && !here.endsWith('.' + h) && !taken.has(h); } catch { return false; } }).slice(0, max);
if (!pick.length) return '';
if (!pick.length) return '';
return (inDrawer ? '<div class="ud-label">Across OpenVibe</div>' : `<span class="ovnav-sep" aria-hidden="true"></span>`) + pick.map(n => `<a class="ovnav-net" href="${escapeAttr(n.url)}" title="${escapeAttr(n.tagline)}">${escapeAttr(n.name)}</a>`).join('');
return (inDrawer ? '<div class="ud-label">Across OpenVibe</div>' : `<span class="ovnav-sep" aria-hidden="true"></span>`) + pick.map(n => `<a class="ovnav-net" href="${escapeAttr(n.url)}" title="${escapeAttr(n.tagline)}">${escapeAttr(n.name)}</a>`).join('');
}
}
@@ -1,12 +1,12 @@
@@ -1,12 +1,12 @@
{
{
"name": "openvibe-shared",
"name": "openvibe-shared",
- "version": "1.10.0",
+ "version": "1.11.0",
"lockfileVersion": 3,
"lockfileVersion": 3,
"requires": true,
"requires": true,
"packages": {
"packages": {
"": {
"": {
"name": "openvibe-shared",
"name": "openvibe-shared",
- "version": "1.10.0",
+ "version": "1.11.0",
"license": "MIT",
"license": "MIT",
"dependencies": {
"dependencies": {
"jsonwebtoken": "^9.0.0"
"jsonwebtoken": "^9.0.0"
@@ -1,6 +1,6 @@
@@ -1,6 +1,6 @@
{
{
"name": "openvibe-shared",
"name": "openvibe-shared",
- "version": "1.10.0",
+ "version": "1.11.0",
"description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site",
"description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site",
"main": "index.js",
"main": "index.js",
"exports": {
"exports": {
@@ -19,6 +19,7 @@
@@ -19,6 +19,7 @@
"./auth-client": "./auth-client.js",
"./auth-client": "./auth-client.js",
"./brand": "./brand.js",
"./brand": "./brand.js",
"./builtin-themes": "./builtin-themes.js",
"./builtin-themes": "./builtin-themes.js",
+ "./frame": "./frame.js",
"./chrome-ssr": "./chrome-ssr.js",
"./chrome-ssr": "./chrome-ssr.js",
"./egress": "./egress.js",
"./egress": "./egress.js",
"./files": "./files.js",
"./files": "./files.js",
deleted file mode 100644
deleted file mode 100644
@@ -1,22 +0,0 @@
@@ -1,22 +0,0 @@
-'use strict';
-// openvibe-shared/chrome-ssr: the server-side pieces of the shared chrome, including the "shipped"
-// block for a home page and the body of a site's /updates page (shipped.js fills both).
-const assert = require('assert');
-const chrome = require('../chrome-ssr');
-
-const home = chrome.shipped({ service: 'wiki', title: 'Recently shipped on OpenVibe.Wiki <x>' });
-assert.ok(home.includes('data-ov-shipped="latest" data-service="wiki" href="/updates"'));
-assert.ok(home.includes('data-ov-shipped="list" data-service="wiki" data-limit="5" data-more="/updates"'));
-assert.ok(home.includes('data-title="Recently shipped on OpenVibe.Wiki <x>"'), 'title escaped');
-assert.ok(home.includes('<noscript><p><a href="/updates">'), 'a plain link without JavaScript');
-assert.ok(chrome.shipped({ service: '"><script>' }).includes('data-service=""'), 'a bad id never reaches the markup');
-
-const body = chrome.updatesBody({ service: 'wiki' });
-assert.ok(body.includes('<h1>What shipped on OpenVibe.Wiki</h1>'));
-assert.ok(body.includes('data-ov-shipped="log" data-service="wiki"'));
-assert.ok(body.includes('https://openvibe.network/updates?site=wiki'), 'the no-JavaScript fallback is the network log');
-assert.strictEqual(chrome.shippedScript(), '<script src="https://openvibe.network/shared/shipped.js" defer></script>');
-
-const foot = chrome.footer({ service: 'wiki', variant: 'full', updates: '/updates' });
-assert.ok(foot.includes('data-ov-shipped="latest"') && foot.includes('href="/updates"'));
-console.log('chrome-ssr: all checks passed');
new file mode 100644
new file mode 100644
@@ -0,0 +1,23 @@
@@ -0,0 +1,23 @@
+'use strict';
+// openvibe-shared/frame: the server-side pieces of the OpenVibe Frame, including the "shipped"
+// block for a home page and the body of a site's /updates page (shipped.js fills both).
+const assert = require('assert');
+const frame = require('../frame');
+assert.strictEqual(require('../chrome-ssr'), frame, 'the old name is an alias');
+
+const home = frame.shipped({ service: 'wiki', title: 'Recently shipped on OpenVibe.Wiki <x>' });
+assert.ok(home.includes('data-ov-shipped="latest" data-service="wiki" href="/updates"'));
+assert.ok(home.includes('data-ov-shipped="list" data-service="wiki" data-limit="5" data-more="/updates"'));
+assert.ok(home.includes('data-title="Recently shipped on OpenVibe.Wiki <x>"'), 'title escaped');
+assert.ok(home.includes('<noscript><p><a href="/updates">'), 'a plain link without JavaScript');
+assert.ok(frame.shipped({ service: '"><script>' }).includes('data-service=""'), 'a bad id never reaches the markup');
+
+const body = frame.updatesBody({ service: 'wiki' });
+assert.ok(body.includes('<h1>What shipped on OpenVibe.Wiki</h1>'));
+assert.ok(body.includes('data-ov-shipped="log" data-service="wiki"'));
+assert.ok(body.includes('https://openvibe.network/updates?site=wiki'), 'the no-JavaScript fallback is the network log');
+assert.strictEqual(frame.shippedScript(), '<script src="https://openvibe.network/shared/shipped.js" defer></script>');
+
+const foot = frame.footer({ service: 'wiki', variant: 'full', updates: '/updates' });
+assert.ok(foot.includes('data-ov-shipped="latest"') && foot.includes('href="/updates"'));
+console.log('frame: all checks passed');
@@ -111,10 +111,10 @@
@@ -111,10 +111,10 @@
// The inline critical style paints a fixed dark canvas before any stylesheet; keep it in step with the theme.
// The inline critical style paints a fixed dark canvas before any stylesheet; keep it in step with the theme.
if (vars['--bg-primary']) el.style.background = vars['--bg-primary'];
if (vars['--bg-primary']) el.style.background = vars['--bg-primary'];
if (vars['--text-primary']) el.style.color = vars['--text-primary'];
if (vars['--text-primary']) el.style.color = vars['--text-primary'];
- syncChrome();
+ syncBrowserUi();
}
}
- // ── Browser chrome follows the theme ─────────────────────────────────────────────────
+ // ── The browser's own UI (address bar, theme-color) follows the theme ─────────────────────────────────────────────────
// The tab icon and the browser/OS toolbar colour are repainted from the theme that was just
// The tab icon and the browser/OS toolbar colour are repainted from the theme that was just
// applied, so a violet, light or custom theme gets a matching favicon and address bar. Runs on
// applied, so a violet, light or custom theme gets a matching favicon and address bar. Runs on
// every apply: first paint, a change on this page, a change in another tab, a server sync.
// every apply: first paint, a change on this page, a change in another tab, a server sync.
@@ -132,12 +132,12 @@
@@ -132,12 +132,12 @@
+ '<path d="M0,-18 A18,18 0 0,1 17.4,4.6" fill="none" stroke="url(#r)" stroke-width="4.4" stroke-linecap="round"/><path d="M-9.5,-7 L0,10 L9.5,-7" fill="none" stroke="' + (document.documentElement.getAttribute('data-theme-mode') === 'light' ? accent : '#fff') + '" stroke-width="4.8" stroke-linecap="round" stroke-linejoin="round"/>'
+ '<path d="M0,-18 A18,18 0 0,1 17.4,4.6" fill="none" stroke="url(#r)" stroke-width="4.4" stroke-linecap="round"/><path d="M-9.5,-7 L0,10 L9.5,-7" fill="none" stroke="' + (document.documentElement.getAttribute('data-theme-mode') === 'light' ? accent : '#fff') + '" stroke-width="4.8" stroke-linecap="round" stroke-linejoin="round"/>'
+ '<circle cx="17.4" cy="4.6" r="3.2" fill="' + (DOTS[site] || light) + '"/></g></svg>');
+ '<circle cx="17.4" cy="4.6" r="3.2" fill="' + (DOTS[site] || light) + '"/></g></svg>');
}
}
- let _chromeKey = '';
- function syncChrome() {
+ let _browserUiKey = '';
+ function syncBrowserUi() {
if (typeof document === 'undefined' || !document.head) return;
if (typeof document === 'undefined' || !document.head) return;
const accent = readColor('--accent', '#3b82f6'), light = readColor('--accent-light', accent), bg = readColor('--bg-secondary', readColor('--bg-primary', '#0a0f1c'));
const accent = readColor('--accent', '#3b82f6'), light = readColor('--accent-light', accent), bg = readColor('--bg-secondary', readColor('--bg-primary', '#0a0f1c'));
const mode = document.documentElement.getAttribute('data-theme-mode') || 'dark';
const mode = document.documentElement.getAttribute('data-theme-mode') || 'dark';
- const key = [accent, light, bg, mode].join('|'); if (key === _chromeKey) return; _chromeKey = key;
+ const key = [accent, light, bg, mode].join('|'); if (key === _browserUiKey) return; _browserUiKey = key;
try {
try {
let meta = document.querySelector('meta[name="theme-color"]');
let meta = document.querySelector('meta[name="theme-color"]');
if (!meta) { meta = document.createElement('meta'); meta.name = 'theme-color'; document.head.appendChild(meta); }
if (!meta) { meta = document.createElement('meta'); meta.name = 'theme-color'; document.head.appendChild(meta); }
@@ -282,7 +282,7 @@
@@ -282,7 +282,7 @@
const activeId = resolveAndApply();
const activeId = resolveAndApply();
// The icon link may be parsed after this script: repaint once the head is complete.
// The icon link may be parsed after this script: repaint once the head is complete.
- if (typeof document !== 'undefined' && document.readyState === 'loading') document.addEventListener('DOMContentLoaded', function () { _chromeKey = ''; syncChrome(); });
+ if (typeof document !== 'undefined' && document.readyState === 'loading') document.addEventListener('DOMContentLoaded', function () { _browserUiKey = ''; syncBrowserUi(); });
// A theme picked in another tab (or on another OpenVibe page of this site) applies here at once.
// A theme picked in another tab (or on another OpenVibe page of this site) applies here at once.
if (typeof window !== 'undefined') window.addEventListener('storage', function (e) { if (e.key === 'ov_theme') { try { resolveAndApply(); } catch (err) { /* */ } } if (e.key === DISPLAY_KEY) applyDisplay(readDisplay()); });
if (typeof window !== 'undefined') window.addEventListener('storage', function (e) { if (e.key === 'ov_theme') { try { resolveAndApply(); } catch (err) { /* */ } } if (e.key === DISPLAY_KEY) applyDisplay(readDisplay()); });