openvibers/openvibe.tools Commits

Shared navbar options and shared footer across the gateway and satellite apps

OP OpenVibers committed 18 Sept 2026 · bf1dc622a5 · 18 files, +324 −208

Navbar: no more brandName/brandIcon overrides (the brand derives from the
hostname), every page passes history + silentLogin, the paste page adds a
My pastes menu row and records the paste title. The apex page drops its
duplicate topbar/user chip and boots the navbar without waiting on /auth/me;
food drops its duplicate site header. Hand-rolled footers on the apex,
net/dev/paste, gateway img/audio showcases and img/audio/docs/yt/maps/food
are replaced by OpenVibeFooter (full on hubs, compact on tool pages) with
each site's own tool links.

Changes

ddiff --git a/apps/audio/public/index.html b/apps/audio/public/index.html
iindex 6ad3408..3c8c8c4 100644
---- a/apps/audio/public/index.html
++++ b/apps/audio/public/index.html
@@@ -328,23 +328,11 @@
</div>
<!-- Footer -->
-- <footer class="site-footer">
-- <div class="footer-inner">
-- <span><i class="fa-solid fa-circle-nodes"></i> Audio.OpenVibe &mdash; Part of the <a href="https://openvibe.network">OpenVibe Network</a></span>
-- <span class="footer-links">
-- <a href="https://openvibe.tools">OpenVibe.Tools</a>
-- <a href="https://openvibe.live">OpenVibe.Live</a>
-- <a href="https://openvibe.games">Scraplandia</a>
-- <a href="https://img.openvibe.tools">Img.OpenVibe</a>
-- <a href="https://yt.openvibe.tools">YT.OpenVibe</a>
-- <a href="https://maps.openvibe.tools">Maps.OpenVibe</a>
-- <a href="https://food.openvibe.tools">Food.OpenVibe</a>
-- </span>
-- </div>
-- </footer>
++ <div id="ov-footer"></div>
<script src="https://openvibe.network/shared/theme-loader.js"></script>
<script src="https://openvibe.network/shared/navbar.js"></script>
++ <script src="https://openvibe.network/shared/footer.js"></script>
<script src="https://openvibe.network/shared/notification-ui.js"></script>
<script src="https://openvibe.network/shared/account-switcher.js"></script>
<script src="/js/app.js"></script>
ddiff --git a/apps/audio/public/js/app.js b/apps/audio/public/js/app.js
iindex 22bb8c3..4e41d85 100644
---- a/apps/audio/public/js/app.js
++++ b/apps/audio/public/js/app.js
@@@ -107,16 +107,42 @@
} catch { /* invalid token */ }
}
++ // Brand name/icon come from the hostname now (png.openvibe.tools → "PNG.OpenVibe.Tools").
if (typeof OpenVibeNavbar !== 'undefined') {
OpenVibeNavbar.init({
service: 'audio',
-- brandName: ctx?.brandName || undefined,
-- brandIcon: ctx?.faIcon || undefined,
token,
user: ctx?.user || user,
apiBase: 'https://openvibe.network',
++ history: { type: 'tool', title: ctx?.brandName || 'Audio.OpenVibe' },
++ silentLogin: 'https://openvibe.tools/auth/login?silent=1&next={url}',
});
}
++ initFooter();
++ }
++
++ /* ---------- Footer (shared OpenVibeFooter) ---------- */
++ // Full footer on the hub host, the compact strip on the single-tool hosts.
++ function initFooter() {
++ if (typeof OpenVibeFooter === 'undefined') return;
++ try {
++ OpenVibeFooter.init({
++ service: 'audio',
++ variant: (!ctx || ctx.toolId === 'hub') ? 'full' : 'compact',
++ mount: '#ov-footer',
++ apiBase: 'https://openvibe.network',
++ links: [{ heading: 'Audio tools', items: [
++ { name: 'Convert', url: 'https://convert.audio.openvibe.tools' },
++ { name: 'Trim', url: 'https://trim.openvibe.tools' },
++ { name: 'Merge', url: 'https://merge.openvibe.tools' },
++ { name: 'Pitch', url: 'https://pitch.openvibe.tools' },
++ { name: 'Speed', url: 'https://speed.openvibe.tools' },
++ { name: 'Normalize', url: 'https://normalize.openvibe.tools' },
++ { name: 'MP3', url: 'https://mp3.openvibe.tools' },
++ { name: 'WAV', url: 'https://wav.openvibe.tools' },
++ ] }],
++ });
++ } catch { /* optional */ }
}
/* ---------- Notifications (OpenVibe bell + toasts) ---------- */
ddiff --git a/apps/docs/public/index.html b/apps/docs/public/index.html
iindex aa974be..9da0584 100644
---- a/apps/docs/public/index.html
++++ b/apps/docs/public/index.html
@@@ -213,22 +213,11 @@
</div>
<!-- Footer -->
-- <footer class="site-footer">
-- <div class="footer-inner">
-- <span><i class="fa-solid fa-file-pdf"></i> Docs.OpenVibe &mdash; Part of the <a href="https://openvibe.network">OpenVibe Network</a></span>
-- <span class="footer-links">
-- <a href="https://openvibe.tools">OpenVibe.Tools</a>
-- <a href="https://openvibe.live">OpenVibe.Live</a>
-- <a href="https://openvibe.games">Scraplandia</a>
-- <a href="https://img.openvibe.tools">Img.OpenVibe</a>
-- <a href="https://audio.openvibe.tools">Audio.OpenVibe</a>
-- <a href="https://yt.openvibe.tools">YT.OpenVibe</a>
-- </span>
-- </div>
-- </footer>
++ <div id="ov-footer"></div>
<script src="https://openvibe.network/shared/theme-loader.js"></script>
<script src="https://openvibe.network/shared/navbar.js"></script>
++ <script src="https://openvibe.network/shared/footer.js"></script>
<script src="https://openvibe.network/shared/notification-ui.js"></script>
<script src="https://openvibe.network/shared/account-switcher.js"></script>
<script src="/js/app.js"></script>
ddiff --git a/apps/docs/public/js/app.js b/apps/docs/public/js/app.js
iindex caa05f2..1328629 100644
---- a/apps/docs/public/js/app.js
++++ b/apps/docs/public/js/app.js
@@@ -140,16 +140,42 @@
} catch { /* invalid token */ }
}
++ // Brand name/icon come from the hostname now (png.openvibe.tools → "PNG.OpenVibe.Tools").
if (typeof OpenVibeNavbar !== 'undefined') {
OpenVibeNavbar.init({
service: 'docs',
-- brandName: ctx?.brandName || undefined,
-- brandIcon: ctx?.faIcon || undefined,
token,
user: ctx?.user || user,
apiBase: 'https://openvibe.network',
++ history: { type: 'tool', title: ctx?.brandName || 'Docs.OpenVibe' },
++ silentLogin: 'https://openvibe.tools/auth/login?silent=1&next={url}',
});
}
++ initFooter();
++ }
++
++ /* ---------- Footer (shared OpenVibeFooter) ---------- */
++ // Full footer on the hub host, the compact strip on the single-tool hosts.
++ function initFooter() {
++ if (typeof OpenVibeFooter === 'undefined') return;
++ try {
++ OpenVibeFooter.init({
++ service: 'docs',
++ variant: (!ctx || ctx.toolId === 'hub') ? 'full' : 'compact',
++ mount: '#ov-footer',
++ apiBase: 'https://openvibe.network',
++ links: [{ heading: 'Docs tools', items: [
++ { name: 'Merge PDF', url: 'https://mergepdf.openvibe.tools' },
++ { name: 'Split PDF', url: 'https://splitpdf.openvibe.tools' },
++ { name: 'Compress PDF', url: 'https://compresspdf.openvibe.tools' },
++ { name: 'Rotate PDF', url: 'https://rotatepdf.openvibe.tools' },
++ { name: 'Watermark PDF', url: 'https://watermarkpdf.openvibe.tools' },
++ { name: 'Protect PDF', url: 'https://protectpdf.openvibe.tools' },
++ { name: 'Images to PDF', url: 'https://image2pdf.openvibe.tools' },
++ { name: 'PDF to images', url: 'https://pdf2jpg.openvibe.tools' },
++ ] }],
++ });
++ } catch { /* optional */ }
}
function initNotifications() {
ddiff --git a/apps/food/public/css/style.css b/apps/food/public/css/style.css
iindex 1e4cdcd..5ca1bf3 100644
---- a/apps/food/public/css/style.css
++++ b/apps/food/public/css/style.css
@@@ -215,10 +215,10 @@ body { min-height:100%; font-family:var(--font); background:var(--bg); color:var
@keyframes spin { to { transform:rotate(360deg); } }
/* Footer */
--#footer {
-- display:flex; align-items:center; justify-content:space-between;
-- padding:0 20px; height:32px; background:var(--bg-card);
-- border-top:1px solid var(--border); font-size:11px; color:var(--text-muted); flex-shrink:0;
++/* Data attribution line above the shared footer (OpenVibeFooter mounts on #ov-footer) */
++.data-credit {
++ padding:10px 20px 0; font-size:11px; color:var(--text-muted); flex-shrink:0;
++ border-top:1px solid var(--border);
}
/* Mobile */
ddiff --git a/apps/food/public/index.html b/apps/food/public/index.html
iindex 18e56b0..7ba828b 100644
---- a/apps/food/public/index.html
++++ b/apps/food/public/index.html
@@@ -37,22 +37,6 @@
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
-- <!-- Header -->
-- <header id="header">
-- <div class="header-left">
-- <div class="brand">
-- <i class="fa-solid fa-utensils brand-icon"></i>
-- <span class="brand-text">OPENVIBE<span class="green">FOOD</span></span>
-- </div>
-- <div class="header-divider"></div>
-- <span class="header-tagline"><i class="fa-solid fa-apple-whole"></i> Budget Grocery & Meal Planning</span>
-- </div>
-- <div class="header-right">
-- <a href="https://maps.openvibe.tools" class="header-btn" title="Maps.OpenVibe"><i class="fa-solid fa-map-location-dot"></i></a>
-- <button id="btn-theme" class="header-btn" title="Toggle theme"><i class="fa-solid fa-moon"></i></button>
-- </div>
-- </header>
--
<!-- Location Bar -->
<div id="loc-bar">
<div class="loc-search">
@@@ -176,15 +160,12 @@
</div>
</main>
-- <!-- Footer -->
-- <footer id="footer">
-- <span><i class="fa-solid fa-utensils"></i> food.openvibe.tools</span>
-- <span>Data from USDA, OSM, store pricing research</span>
-- <a href="https://maps.openvibe.tools" style="color:var(--green);text-decoration:none"><i class="fa-solid fa-map"></i> maps.openvibe.tools</a>
-- </footer>
++ <p class="data-credit"><i class="fa-solid fa-database"></i> Data from USDA, OSM and store pricing research</p>
++ <div id="ov-footer"></div>
<script src="https://openvibe.network/shared/theme-loader.js"></script>
<script src="https://openvibe.network/shared/navbar.js"></script>
++ <script src="https://openvibe.network/shared/footer.js"></script>
<script src="https://openvibe.network/shared/notification-ui.js"></script>
<script src="https://openvibe.network/shared/account-switcher.js"></script>
<script src="/js/app.js"></script>
@@@ -198,7 +179,19 @@
let user = null;
if (token) { try { user = JSON.parse(atob(token.split('.')[1])); } catch {} }
if (typeof OpenVibeNavbar !== 'undefined') {
-- OpenVibeNavbar.init({ service: 'food', brandName: 'Food.OpenVibe', brandIcon: 'fa-utensils', token, user, apiBase: 'https://openvibe.network' });
++ OpenVibeNavbar.init({ service: 'food', token, user, apiBase: 'https://openvibe.network', history: { type: 'tool', title: 'Food.OpenVibe' }, silentLogin: 'https://openvibe.tools/auth/login?silent=1&next={url}' });
++ }
++ if (typeof OpenVibeFooter !== 'undefined') {
++ try {
++ OpenVibeFooter.init({
++ service: 'food', variant: 'compact', mount: '#ov-footer', apiBase: 'https://openvibe.network',
++ links: [{ heading: 'Food.OpenVibe', items: [
++ { name: 'Find food', url: 'https://food.openvibe.tools' },
++ { name: 'Maps.OpenVibe', url: 'https://maps.openvibe.tools' },
++ { name: 'All tools', url: 'https://openvibe.tools' },
++ ] }],
++ });
++ } catch (e) { /* optional */ }
}
if (typeof OpenVibeAccountSwitcher !== 'undefined') {
OpenVibeAccountSwitcher.init({ apiBase: 'https://openvibe.network' });
ddiff --git a/apps/food/public/js/app.js b/apps/food/public/js/app.js
iindex 97e1522..2762754 100644
---- a/apps/food/public/js/app.js
++++ b/apps/food/public/js/app.js
@@@ -280,7 +280,7 @@
$('#db-shelf')?.addEventListener('change', filterFoods);
// Theme
-- $('#btn-theme').addEventListener('click', toggleTheme);
++ $('#btn-theme')?.addEventListener('click', toggleTheme); // theme switching lives in the shared navbar now
}
// ═══ Init ═══
ddiff --git a/apps/gateway/public/audio.html b/apps/gateway/public/audio.html
iindex 1ac3a6b..e6e3f74 100644
---- a/apps/gateway/public/audio.html
++++ b/apps/gateway/public/audio.html
@@@ -21,6 +21,7 @@
</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
++ <script src="https://openvibe.network/shared/theme-loader.js"></script>
<style>
:root {
--bg-primary: #0b0c12;
@@@ -93,7 +94,7 @@
.search-wrapper input { width: 100%; padding: 16px 20px; border-radius: 12px; border: 1px solid var(--border-light); background: var(--bg-card); color: var(--text-primary); font-family: inherit; font-size: 0.95rem; outline: none; transition: all 0.25s ease; }
.search-wrapper input:focus { border-color: rgba(139,92,246,0.4); background: var(--bg-hover); }
-- footer { text-align: center; padding: 40px 20px; color: var(--text-muted); font-size: 0.9rem; border-top: 1px solid var(--border); margin-top: 60px; }
++ .note { text-align: center; padding: 40px 20px 0; color: var(--text-muted); font-size: 0.9rem; border-top: 1px solid var(--border); margin-top: 60px; }
footer a { color: var(--accent-light); text-decoration: none; }
footer a:hover { text-decoration: underline; }
@@@ -125,15 +126,15 @@
<div class="stats-section" id="stats"></div>
</div>
-- <footer>
-- <p>All audio processing is performed locally in your browser. No files are uploaded to our servers. <a href="https://github.com/OpenVibe.Live">Learn more &#8594;</a></p>
-- </footer>
++ <p class="note">All audio processing is performed locally in your browser. No files are uploaded to our servers.</p>
</div>
++ <div id="ov-footer"></div>
<script src="https://openvibe.network/shared/navbar.js"></script>
++ <script src="https://openvibe.network/shared/footer.js"></script>
<script>
const AUDIO_TOOLS = {
-- 'convert': { name: 'Converter', domain: 'convert.openvibe.tools', icon: 'fa-arrow-right-arrow-left', category: 'Convert' },
++ 'convert': { name: 'Converter', domain: 'convert.audio.openvibe.tools', icon: 'fa-arrow-right-arrow-left', category: 'Convert' },
'trim': { name: 'Trim', domain: 'trim.openvibe.tools', icon: 'fa-scissors', category: 'Edit' },
'merge': { name: 'Merge', domain: 'merge.openvibe.tools', icon: 'fa-code-merge', category: 'Mix' },
'pitch': { name: 'Pitch', domain: 'pitch.openvibe.tools', icon: 'fa-wave-square', category: 'Effects' },
@@@ -191,7 +192,37 @@
}
// Initialize
++ function getCookie(n) {
++ const m = document.cookie.match(new RegExp('(?:^|; )' + n + '=([^;]*)'));
++ return m ? decodeURIComponent(m[1]) : null;
++ }
++ function initChrome() {
++ const token = getCookie('ov_token') || localStorage.getItem('ov_token');
++ if (window.OpenVibeNavbar) {
++ try {
++ OpenVibeNavbar.init({
++ service: 'audio', apiBase: 'https://openvibe.network', token,
++ history: { type: 'tool', title: document.title.split(' — ')[0] },
++ silentLogin: 'https://openvibe.tools/auth/login?silent=1&next={url}',
++ });
++ } catch { /* non-critical */ }
++ }
++ if (window.OpenVibeFooter) {
++ try {
++ const items = Object.values(AUDIO_TOOLS).map(t => ({ name: t.name, url: 'https://' + t.domain }));
++ OpenVibeFooter.init({
++ service: 'audio', variant: 'full', mount: '#ov-footer', apiBase: 'https://openvibe.network',
++ links: [
++ { heading: 'Tools', items: items.slice(0, Math.ceil(items.length / 2)) },
++ { heading: 'Formats', items: items.slice(Math.ceil(items.length / 2)) },
++ ],
++ });
++ } catch { /* non-critical */ }
++ }
++ }
++
window.addEventListener('DOMContentLoaded', () => {
++ initChrome();
const showcase = document.getElementById('showcase-section');
showcase.style.display = 'block';
buildShowcaseGrid();
ddiff --git a/apps/gateway/public/dev.html b/apps/gateway/public/dev.html
iindex abb6f72..9485717 100644
---- a/apps/gateway/public/dev.html
++++ b/apps/gateway/public/dev.html
@@@ -137,8 +137,11 @@ a{color:inherit;text-decoration:none}
<div id="hub"></div>
<div class="rel" id="rel"></div>
</div></div>
++<div id="ov-footer"></div>
<div class="toast" id="toast"></div>
<script src="https://openvibe.network/shared/navbar.js"></script>
++<script src="https://openvibe.network/shared/footer.js"></script>
++<script src="https://openvibe.network/shared/theme-loader.js"></script>
<script>
'use strict';
const API='https://openvibe.tools/api/dev',$=s=>document.getElementById(s),
@@@ -180,7 +183,13 @@ let ACT=T[sub]?sub:'dev',MODE='';
document.addEventListener('DOMContentLoaded',()=>{
const tok=localStorage.getItem('ov_token');
-- if(window.OpenVibeNavbar)try{OpenVibeNavbar.init({service:'dev',apiBase:'https://openvibe.network',token:tok})}catch(e){}
++ const cur=T[ACT]||T.dev;
++ if(window.OpenVibeNavbar)try{OpenVibeNavbar.init({service:'dev',apiBase:'https://openvibe.network',token:tok,history:{type:'tool',title:ACT==='dev'?'Dev.OpenVibe':cur.n},silentLogin:'https://openvibe.tools/auth/login?silent=1&next={url}'})}catch(e){}
++ if(window.OpenVibeFooter)try{
++ // Footer columns come from the same catalog as the hub, one column per category.
++ const links=Object.entries(CATS).map(([cid,cat])=>({heading:cat.l,items:Object.entries(T).filter(([,t])=>t.c===cid).map(([id,t])=>({name:t.n,url:'https://'+id+'.openvibe.tools'}))})).filter(g=>g.items.length);
++ OpenVibeFooter.init({service:'dev',variant:ACT==='dev'?'full':'compact',mount:'#ov-footer',apiBase:'https://openvibe.network',links});
++ }catch(e){}
ACT==='dev'?showHub():showTool();
});
ddiff --git a/apps/gateway/public/img.html b/apps/gateway/public/img.html
iindex dd518c1..8d32251 100644
---- a/apps/gateway/public/img.html
++++ b/apps/gateway/public/img.html
@@@ -21,6 +21,7 @@
</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
++ <script src="https://openvibe.network/shared/theme-loader.js"></script>
<style>
:root {
--bg-primary: #0b0c12;
@@@ -93,7 +94,7 @@
.search-wrapper input { width: 100%; padding: 16px 20px; border-radius: 12px; border: 1px solid var(--border-light); background: var(--bg-card); color: var(--text-primary); font-family: inherit; font-size: 0.95rem; outline: none; transition: all 0.25s ease; }
.search-wrapper input:focus { border-color: rgba(139,92,246,0.4); background: var(--bg-hover); }
-- footer { text-align: center; padding: 40px 20px; color: var(--text-muted); font-size: 0.9rem; border-top: 1px solid var(--border); margin-top: 60px; }
++ .note { text-align: center; padding: 40px 20px 0; color: var(--text-muted); font-size: 0.9rem; border-top: 1px solid var(--border); margin-top: 60px; }
footer a { color: var(--accent-light); text-decoration: none; }
footer a:hover { text-decoration: underline; }
@@@ -125,12 +126,12 @@
<div class="stats-section" id="stats"></div>
</div>
-- <footer>
-- <p>All image processing is performed locally in your browser. No files are uploaded to our servers. <a href="https://github.com/OpenVibe.Live">Learn more &#8594;</a></p>
-- </footer>
++ <p class="note">All image processing is performed locally in your browser. No files are uploaded to our servers.</p>
</div>
++ <div id="ov-footer"></div>
<script src="https://openvibe.network/shared/navbar.js"></script>
++ <script src="https://openvibe.network/shared/footer.js"></script>
<script>
const IMAGE_TOOLS = {
'convert': { name: 'Converter', domain: 'convert.openvibe.tools', icon: 'fa-arrow-right-arrow-left', category: 'Convert' },
@@@ -191,7 +192,37 @@
}
// Initialize
++ function getCookie(n) {
++ const m = document.cookie.match(new RegExp('(?:^|; )' + n + '=([^;]*)'));
++ return m ? decodeURIComponent(m[1]) : null;
++ }
++ function initChrome() {
++ const token = getCookie('ov_token') || localStorage.getItem('ov_token');
++ if (window.OpenVibeNavbar) {
++ try {
++ OpenVibeNavbar.init({
++ service: 'img', apiBase: 'https://openvibe.network', token,
++ history: { type: 'tool', title: document.title.split(' — ')[0] },
++ silentLogin: 'https://openvibe.tools/auth/login?silent=1&next={url}',
++ });
++ } catch { /* non-critical */ }
++ }
++ if (window.OpenVibeFooter) {
++ try {
++ const items = Object.values(IMAGE_TOOLS).map(t => ({ name: t.name, url: 'https://' + t.domain }));
++ OpenVibeFooter.init({
++ service: 'img', variant: 'full', mount: '#ov-footer', apiBase: 'https://openvibe.network',
++ links: [
++ { heading: 'Tools', items: items.slice(0, Math.ceil(items.length / 2)) },
++ { heading: 'Formats', items: items.slice(Math.ceil(items.length / 2)) },
++ ],
++ });
++ } catch { /* non-critical */ }
++ }
++ }
++
window.addEventListener('DOMContentLoaded', () => {
++ initChrome();
const showcase = document.getElementById('showcase-section');
showcase.style.display = 'block';
buildShowcaseGrid();
ddiff --git a/apps/gateway/public/index.html b/apps/gateway/public/index.html
iindex e8cbca3..3357eb6 100644
---- a/apps/gateway/public/index.html
++++ b/apps/gateway/public/index.html
@@@ -88,19 +88,6 @@
a { color: inherit; text-decoration: none; }
.shell { max-width: 1180px; margin: 0 auto; padding: 0 24px; }
-- /* ─── Top bar ─── */
-- .topbar {
-- display: flex; align-items: center; justify-content: space-between;
-- padding: 18px 0; gap: 16px;
-- }
-- .brand { display: flex; align-items: center; gap: 11px; font-weight: 900; font-size: 1.12rem; letter-spacing: -0.01em; }
-- .brand-badge {
-- width: 36px; height: 36px; border-radius: 10px; display: grid; place-items: center;
-- background: linear-gradient(135deg, var(--accent), var(--accent-dark));
-- color: #fff; font-size: 1rem; box-shadow: 0 6px 18px rgba(139,92,246,0.35);
-- }
-- .brand small { display: block; font-size: 0.68rem; font-weight: 600; color: var(--text-muted); letter-spacing: 0.06em; text-transform: uppercase; }
-- .topbar-actions { display: flex; align-items: center; gap: 10px; }
.btn {
display: inline-flex; align-items: center; gap: 8px; cursor: pointer;
padding: 10px 18px; border-radius: 12px; font-weight: 700; font-size: 0.9rem;
@@@ -113,18 +100,6 @@
border-color: transparent; color: #fff; box-shadow: 0 8px 22px rgba(139,92,246,0.35);
}
.btn-primary:hover { filter: brightness(1.1); background: linear-gradient(135deg, var(--accent), var(--accent-dark)); }
-- .user-chip {
-- display: none; align-items: center; gap: 10px; padding: 7px 12px;
-- border: 1px solid var(--border-light); border-radius: 999px; font-size: 0.88rem; font-weight: 600;
-- }
-- .user-chip.visible { display: inline-flex; }
-- .user-chip .avatar {
-- width: 26px; height: 26px; border-radius: 50%; display: grid; place-items: center;
-- background: var(--accent); color: #fff; font-size: 0.72rem; font-weight: 800; overflow: hidden;
-- }
-- .user-chip .avatar img { width: 100%; height: 100%; object-fit: cover; }
-- .user-chip a { color: var(--text-muted); font-size: 0.8rem; }
-- .user-chip a:hover { color: var(--text-primary); }
/* ─── Hero ─── */
.hero { padding: 64px 0 40px; text-align: center; }
@@@ -259,37 +234,15 @@
.dir-empty i { display: block; font-size: 1.6rem; margin-bottom: 10px; }
/* ─── Footer ─── */
-- footer { border-top: 1px solid var(--border); margin-top: 40px; padding: 34px 0 44px; }
-- .foot { display: flex; justify-content: space-between; gap: 20px; flex-wrap: wrap; color: var(--text-muted); font-size: 0.84rem; }
-- .foot a { color: var(--text-secondary); }
-- .foot a:hover { color: var(--text-primary); }
-- .foot-links { display: flex; gap: 18px; flex-wrap: wrap; }
@media (max-width: 640px) {
.hero { padding: 40px 0 28px; }
.hero-stats { gap: 18px; }
-- .topbar-actions .btn span { display: none; }
}
</style>
</head>
<body>
<div class="shell">
-- <!-- ─── Top bar ─── -->
-- <header class="topbar">
-- <a class="brand" href="/">
-- <span class="brand-badge"><i class="fa-solid fa-circle-nodes"></i></span>
-- <span>OpenVibe.Tools<small>Part of the OpenVibe network</small></span>
-- </a>
-- <div class="topbar-actions">
-- <span class="user-chip" id="user-chip">
-- <span class="avatar" id="user-avatar">?</span>
-- <span id="user-name"></span>
-- <a href="/auth/logout" title="Sign out"><i class="fa-solid fa-right-from-bracket"></i></a>
-- </span>
-- <a class="btn btn-primary" id="signin-btn" href="/auth/login"><i class="fa-solid fa-right-to-bracket"></i> <span>Sign in</span></a>
-- </div>
-- </header>
--
<!-- ─── Hero ─── -->
<section class="hero">
<h1>Every tool you need,<br><span class="grad">one vibe apart.</span></h1>
@@@ -426,20 +379,10 @@
</div>
</div>
-- <footer>
-- <div class="shell foot">
-- <div>© OpenVibe — open tools, community run. <a href="https://openvibe.network">One Account. All of OpenVibe.</a></div>
-- <div class="foot-links">
-- <a href="https://openvibe.live">Live</a>
-- <a href="https://openvibe.games">Games</a>
-- <a href="https://openvibe.network">Network</a>
-- <a href="https://openvibe.live/tos" rel="noopener">Terms</a>
-- <a href="https://openvibe.live/dmca" rel="noopener">DMCA</a>
-- </div>
-- </div>
-- </footer>
++ <div id="ov-footer"></div>
<script src="https://openvibe.network/shared/navbar.js" defer></script>
++ <script src="https://openvibe.network/shared/footer.js" defer></script>
<script>
'use strict';
@@@ -749,41 +692,53 @@
document.getElementById('stat-tools').textContent =
DIR_DATA.reduce((sum, c) => sum + c.tools.length, 0) + '+';
-- /* ─── Session (OAuth client of OpenVibe.Network) ─── */
++ /* ─── Shared chrome (navbar + footer from openvibe.network) ─── */
function getCookie(n) {
const m = document.cookie.match(new RegExp('(?:^|; )' + n + '=([^;]*)'));
return m ? decodeURIComponent(m[1]) : null;
}
-- async function initSession() {
-- let user = null;
-- try {
-- const res = await fetch('/auth/me', { credentials: 'include' });
-- if (res.ok) user = (await res.json()).user;
-- } catch { /* signed out */ }
--
-- const chip = document.getElementById('user-chip');
-- const signin = document.getElementById('signin-btn');
-- if (user) {
-- document.getElementById('user-name').textContent = user.display_name || user.username || 'Account';
-- const avatar = document.getElementById('user-avatar');
-- if (user.avatar_url) avatar.innerHTML = `<img src="${escHtml(user.avatar_url)}" alt="">`;
-- else avatar.textContent = (user.display_name || user.username || '?').slice(0, 1).toUpperCase();
-- chip.classList.add('visible');
-- signin.style.display = 'none';
-- } else {
-- chip.classList.remove('visible');
-- signin.style.display = '';
-- }
--
-- // Shared navbar (loaded from openvibe.network) — best effort
++ // The navbar owns the account UI (sign-in, avatar, menu) and boots from the shared
++ // ov_token cookie straight away — no /auth/me round trip before it can render.
++ function initChrome() {
if (window.OpenVibeNavbar) {
try {
-- OpenVibeNavbar.init({ service: 'tools', apiBase: 'https://openvibe.network', token: getCookie('ov_token'), user });
++ OpenVibeNavbar.init({
++ service: 'tools',
++ apiBase: 'https://openvibe.network',
++ token: getCookie('ov_token') || localStorage.getItem('ov_token'),
++ history: { type: 'tool', title: 'OpenVibe.Tools' },
++ silentLogin: 'https://openvibe.tools/auth/login?silent=1&next={url}',
++ });
++ } catch { /* non-critical */ }
++ }
++ if (window.OpenVibeFooter) {
++ try {
++ OpenVibeFooter.init({
++ service: 'tools', variant: 'full', mount: '#ov-footer', apiBase: 'https://openvibe.network',
++ links: [
++ { heading: 'Tool suites', items: [
++ { name: 'Net.OpenVibe', url: 'https://net.openvibe.tools' },
++ { name: 'Dev.OpenVibe', url: 'https://dev.openvibe.tools' },
++ { name: 'Paste.OpenVibe', url: 'https://pastes.openvibe.tools' },
++ { name: 'Img.OpenVibe', url: 'https://img.openvibe.tools' },
++ { name: 'Audio.OpenVibe', url: 'https://audio.openvibe.tools' },
++ { name: 'Text.OpenVibe', url: 'https://text.openvibe.tools' },
++ ] },
++ { heading: 'More suites', items: [
++ { name: 'Logo.OpenVibe', url: 'https://logo.openvibe.tools' },
++ { name: 'Docs.OpenVibe', url: 'https://docs.openvibe.tools' },
++ { name: 'YT.OpenVibe', url: 'https://yt.openvibe.tools' },
++ { name: 'Maps.OpenVibe', url: 'https://maps.openvibe.tools' },
++ { name: 'Food.OpenVibe', url: 'https://food.openvibe.tools' },
++ { name: 'All tools', url: '#', onclick: 'openDirectory(); return false;' },
++ ] },
++ ],
++ });
} catch { /* non-critical */ }
}
}
-- window.addEventListener('DOMContentLoaded', initSession);
++ window.addEventListener('DOMContentLoaded', initChrome);
</script>
</body>
</html>
ddiff --git a/apps/gateway/public/net.html b/apps/gateway/public/net.html
iindex 861050e..a9e7317 100644
---- a/apps/gateway/public/net.html
++++ b/apps/gateway/public/net.html
@@@ -280,12 +280,6 @@
.tab-content.active { display: block; }
/* ─── Footer ─── */
-- .footer {
-- text-align: center; padding: 32px 0; margin-top: 40px;
-- border-top: 1px solid var(--border); color: var(--text-muted); font-size: 12px;
-- }
-- .footer a { color: var(--text-secondary); }
-- .footer .footer-links { display: flex; gap: 16px; justify-content: center; margin-bottom: 8px; flex-wrap: wrap; }
/* ─── Responsive ─── */
@media (max-width: 640px) {
@@@ -587,22 +581,14 @@
<!-- Hub: tool directory (shown on net.openvibe.tools) -->
<div class="shell" id="hub-directory" style="display:none"></div>
-- <!-- Footer -->
-- <div class="footer">
-- <div class="footer-links">
-- <a href="https://openvibe.network">OpenVibe</a>
-- <a href="https://net.openvibe.tools">Net.OpenVibe</a>
-- <a href="https://openvibe.live">OpenVibe.Live</a>
-- <a href="https://openvibe.games">Scraplandia</a>
-- </div>
-- <p>Part of the <a href="https://openvibe.network">OpenVibe</a>. Open source. Built in the open.</p>
-- </div>
++ <div id="ov-footer"></div>
</div>
<div class="toast-container" id="toasts"></div>
<!-- Shared OpenVibe Navbar -->
<script src="https://openvibe.network/shared/navbar.js"></script>
++ <script src="https://openvibe.network/shared/footer.js"></script>
<script src="https://openvibe.network/shared/theme-loader.js"></script>
<script>
'use strict';
@@@ -709,10 +695,36 @@
// ── Init ─────────────────────────────────────────────────
document.addEventListener('DOMContentLoaded', () => {
-- // Init navbar
++ // Shared navbar + footer (openvibe.network)
try {
const token = localStorage.getItem('ov_token') || getCookie('ov_token');
-- OpenVibeNavbar.init({ service: 'net', apiBase: 'https://openvibe.network', token });
++ OpenVibeNavbar.init({
++ service: 'net', apiBase: 'https://openvibe.network', token,
++ history: { type: 'tool', title: isHub ? 'Net.OpenVibe' : tool.name },
++ silentLogin: 'https://openvibe.tools/auth/login?silent=1&next={url}',
++ });
++ } catch {}
++ try {
++ OpenVibeFooter.init({
++ service: 'net', variant: isHub ? 'full' : 'compact', mount: '#ov-footer', apiBase: 'https://openvibe.network',
++ links: [
++ { heading: 'Network tools', items: [
++ { name: 'Domain & IP lookup', url: 'https://lookup.openvibe.tools' },
++ { name: 'DNS lookup', url: 'https://dns.openvibe.tools' },
++ { name: 'Ping', url: 'https://ping.openvibe.tools' },
++ { name: 'SSL checker', url: 'https://ssl.openvibe.tools' },
++ { name: 'Whois', url: 'https://whois.openvibe.tools' },
++ { name: 'HTTP headers', url: 'https://headers.openvibe.tools' },
++ ] },
++ { heading: 'Email & uptime', items: [
++ { name: 'SPF checker', url: 'https://spf.openvibe.tools' },
++ { name: 'DMARC checker', url: 'https://dmarc.openvibe.tools' },
++ { name: 'Blacklist check', url: 'https://blacklist.openvibe.tools' },
++ { name: 'Uptime check', url: 'https://uptime.openvibe.tools' },
++ { name: 'All network tools', url: 'https://net.openvibe.tools' },
++ ] },
++ ],
++ });
} catch {}
setupUI();
ddiff --git a/apps/gateway/public/paste.html b/apps/gateway/public/paste.html
iindex 0d1b480..dd16b64 100644
---- a/apps/gateway/public/paste.html
++++ b/apps/gateway/public/paste.html
@@@ -299,9 +299,11 @@
</div>
</div>
++ <div id="ov-footer"></div>
<div class="toast-container" id="toasts"></div>
<script src="https://openvibe.network/shared/navbar.js"></script>
++ <script src="https://openvibe.network/shared/footer.js"></script>
<script src="https://openvibe.network/shared/theme-loader.js"></script>
<script>
'use strict';
@@@ -425,8 +427,10 @@
const data = await api(`/${slug}`);
const p = data.paste;
renderPasteView(p);
++ initChrome({ type: 'paste', title: p.title || slug });
loadComments(slug);
} catch (err) {
++ initChrome();
detail.innerHTML = `<div class="empty">${esc(err.message)}</div>`;
}
}
@@@ -568,12 +572,40 @@
document.getElementById('view-paste').classList.toggle('hidden', view !== 'paste');
}
++ // ── Shared chrome ──────────────────────────────────────
++ // The navbar records this visit in the account's cross-network history, so on a paste
++ // page it waits for the paste's title (viewPaste calls it); everywhere else it boots at once.
++ let chromeReady = false;
++ function initChrome(history) {
++ if (chromeReady) return;
++ chromeReady = true;
++ try {
++ OpenVibeNavbar.init({
++ service: 'paste', apiBase: 'https://openvibe.network', token,
++ history: history || { type: 'tool', title: 'Paste.OpenVibe' },
++ silentLogin: 'https://openvibe.tools/auth/login?silent=1&next={url}',
++ menu: { after: [{ id: 'my-pastes', label: 'My pastes', href: 'https://openvibe.community/my', icon: 'fa-clipboard-list' }] },
++ });
++ } catch {}
++ try {
++ OpenVibeFooter.init({
++ service: 'paste', variant: 'compact', mount: '#ov-footer', apiBase: 'https://openvibe.network',
++ links: [{ heading: 'Pastes', items: [
++ { name: 'New paste', url: '/new' },
++ { name: 'Browse pastes', url: '/' },
++ { name: 'My pastes', url: 'https://openvibe.community/my' },
++ ] }],
++ });
++ } catch {}
++ }
++
function route() {
const path = location.pathname;
if (path.startsWith('/p/')) {
const slug = path.split('/')[2];
if (slug) { viewPaste(slug); return; }
}
++ initChrome();
if (path === '/new') { showView('new'); return; }
showView('browse');
}
@@@ -593,11 +625,6 @@
// ── Init ───────────────────────────────────────────────
document.addEventListener('DOMContentLoaded', () => {
token = localStorage.getItem('ov_token') || getCookie('ov_token');
--
-- try {
-- OpenVibeNavbar.init({ service: 'paste', apiBase: 'https://openvibe.network', token });
-- } catch {}
--
route();
});
</script>
ddiff --git a/apps/img/public/index.html b/apps/img/public/index.html
iindex 68e76ad..3302733 100644
---- a/apps/img/public/index.html
++++ b/apps/img/public/index.html
@@@ -162,22 +162,11 @@
</div>
<!-- Footer -->
-- <footer class="site-footer">
-- <div class="footer-inner">
-- <span><i class="fa-solid fa-circle-nodes"></i> Img.OpenVibe &mdash; Part of the <a href="https://openvibe.network">OpenVibe Network</a></span>
-- <span class="footer-links">
-- <a href="https://openvibe.tools">OpenVibe.Tools</a>
-- <a href="https://openvibe.live">OpenVibe.Live</a>
-- <a href="https://openvibe.games">Scraplandia</a>
-- <a href="https://audio.openvibe.tools">Audio.OpenVibe</a>
-- <a href="https://yt.openvibe.tools">YT.OpenVibe</a>
-- <a href="https://maps.openvibe.tools">Maps.OpenVibe</a>
-- </span>
-- </div>
-- </footer>
++ <div id="ov-footer"></div>
<script src="https://openvibe.network/shared/theme-loader.js"></script>
<script src="https://openvibe.network/shared/navbar.js"></script>
++ <script src="https://openvibe.network/shared/footer.js"></script>
<script src="https://openvibe.network/shared/notification-ui.js"></script>
<script src="https://openvibe.network/shared/account-switcher.js"></script>
<script src="/js/app.js"></script>
ddiff --git a/apps/img/public/js/app.js b/apps/img/public/js/app.js
iindex e4898e4..cf7efc0 100644
---- a/apps/img/public/js/app.js
++++ b/apps/img/public/js/app.js
@@@ -119,16 +119,42 @@
} catch { /* invalid token */ }
}
++ // Brand name/icon come from the hostname now (png.openvibe.tools → "PNG.OpenVibe.Tools").
if (typeof OpenVibeNavbar !== 'undefined') {
OpenVibeNavbar.init({
service: 'img',
-- brandName: ctx?.brandName || undefined,
-- brandIcon: ctx?.faIcon || undefined,
token,
user: ctx?.user || user,
apiBase: 'https://openvibe.network',
++ history: { type: 'tool', title: ctx?.brandName || 'Img.OpenVibe' },
++ silentLogin: 'https://openvibe.tools/auth/login?silent=1&next={url}',
});
}
++ initFooter();
++ }
++
++ /* ---------- Footer (shared OpenVibeFooter) ---------- */
++ // Full footer on the hub host, the compact strip on the single-tool hosts.
++ function initFooter() {
++ if (typeof OpenVibeFooter === 'undefined') return;
++ try {
++ OpenVibeFooter.init({
++ service: 'img',
++ variant: (!ctx || ctx.toolId === 'hub') ? 'full' : 'compact',
++ mount: '#ov-footer',
++ apiBase: 'https://openvibe.network',
++ links: [{ heading: 'Img tools', items: [
++ { name: 'Convert', url: 'https://convert.openvibe.tools' },
++ { name: 'Compress', url: 'https://compress.openvibe.tools' },
++ { name: 'Resize', url: 'https://resize.openvibe.tools' },
++ { name: 'Crop', url: 'https://crop.openvibe.tools' },
++ { name: 'Favicon', url: 'https://favicon.openvibe.tools' },
++ { name: 'PNG', url: 'https://png.openvibe.tools' },
++ { name: 'JPG', url: 'https://jpg.openvibe.tools' },
++ { name: 'WebP', url: 'https://webp.openvibe.tools' },
++ ] }],
++ });
++ } catch { /* optional */ }
}
// ── Notifications (OpenVibe bell + toasts) ───────────
ddiff --git a/apps/maps/public/index.html b/apps/maps/public/index.html
iindex ae03b15..8f2bb3a 100644
---- a/apps/maps/public/index.html
++++ b/apps/maps/public/index.html
@@@ -236,6 +236,7 @@
<span id="footer-coords"></span>
<span id="footer-weather"></span>
</footer>
++ <div id="ov-footer"></div>
</div>
<!-- ═══ Detail Panel ═══ -->
@@@ -390,6 +391,7 @@
<script src="https://openvibe.network/shared/theme-loader.js"></script>
<script src="https://openvibe.network/shared/navbar.js"></script>
++<script src="https://openvibe.network/shared/footer.js"></script>
<script src="https://openvibe.network/shared/notification-ui.js"></script>
<script src="https://openvibe.network/shared/account-switcher.js"></script>
<script src="/js/app.js"></script>
@@@ -403,7 +405,19 @@
let user = null;
if (token) { try { user = JSON.parse(atob(token.split('.')[1])); } catch {} }
if (typeof OpenVibeNavbar !== 'undefined') {
-- OpenVibeNavbar.init({ service: 'maps', brandName: 'Maps.OpenVibe', brandIcon: 'fa-person-shelter', token, user, apiBase: 'https://openvibe.network' });
++ OpenVibeNavbar.init({ service: 'maps', token, user, apiBase: 'https://openvibe.network', history: { type: 'tool', title: 'Maps.OpenVibe' }, silentLogin: 'https://openvibe.tools/auth/login?silent=1&next={url}' });
++ }
++ if (typeof OpenVibeFooter !== 'undefined') {
++ try {
++ OpenVibeFooter.init({
++ service: 'maps', variant: 'compact', mount: '#ov-footer', apiBase: 'https://openvibe.network',
++ links: [{ heading: 'Maps.OpenVibe', items: [
++ { name: 'Survival map', url: 'https://maps.openvibe.tools' },
++ { name: 'Food.OpenVibe', url: 'https://food.openvibe.tools' },
++ { name: 'All tools', url: 'https://openvibe.tools' },
++ ] }],
++ });
++ } catch (e) { /* optional */ }
}
if (typeof OpenVibeAccountSwitcher !== 'undefined') {
OpenVibeAccountSwitcher.init({ apiBase: 'https://openvibe.network' });
ddiff --git a/apps/yt/public/index.html b/apps/yt/public/index.html
iindex d1e0471..b760cb1 100644
---- a/apps/yt/public/index.html
++++ b/apps/yt/public/index.html
@@@ -116,23 +116,11 @@
</div>
<!-- Footer -->
-- <footer class="site-footer">
-- <div class="footer-inner">
-- <span><i class="fa-solid fa-circle-nodes"></i> YT.OpenVibe &mdash; Part of the <a href="https://openvibe.network">OpenVibe Network</a></span>
-- <span class="footer-links">
-- <a href="https://openvibe.tools">OpenVibe.Tools</a>
-- <a href="https://openvibe.live">OpenVibe.Live</a>
-- <a href="https://openvibe.games">Scraplandia</a>
-- <a href="https://img.openvibe.tools">Img.OpenVibe</a>
-- <a href="https://audio.openvibe.tools">Audio.OpenVibe</a>
-- <a href="https://maps.openvibe.tools">Maps.OpenVibe</a>
-- <a href="https://food.openvibe.tools">Food.OpenVibe</a>
-- </span>
-- </div>
-- </footer>
++ <div id="ov-footer"></div>
<script src="https://openvibe.network/shared/theme-loader.js"></script>
<script src="https://openvibe.network/shared/navbar.js"></script>
++ <script src="https://openvibe.network/shared/footer.js"></script>
<script src="https://openvibe.network/shared/notification-ui.js"></script>
<script src="https://openvibe.network/shared/account-switcher.js"></script>
<script src="/js/app.js"></script>
ddiff --git a/apps/yt/public/js/app.js b/apps/yt/public/js/app.js
iindex 45c945d..8aa4f9b 100644
---- a/apps/yt/public/js/app.js
++++ b/apps/yt/public/js/app.js
@@@ -369,12 +369,24 @@
if (typeof OpenVibeNavbar !== 'undefined') {
OpenVibeNavbar.init({
service: 'yt',
-- brandName: 'YT.OpenVibe',
-- brandIcon: 'fa-circle-play',
token, user,
apiBase: 'https://openvibe.network',
++ history: { type: 'tool', title: 'YT.OpenVibe' },
++ silentLogin: 'https://openvibe.tools/auth/login?silent=1&next={url}',
});
}
++ if (typeof OpenVibeFooter !== 'undefined') {
++ try {
++ OpenVibeFooter.init({
++ service: 'yt', variant: 'compact', mount: '#ov-footer', apiBase: 'https://openvibe.network',
++ links: [{ heading: 'YT.OpenVibe', items: [
++ { name: 'Video downloader', url: 'https://yt.openvibe.tools' },
++ { name: 'Audio converter', url: 'https://audio.openvibe.tools' },
++ { name: 'All tools', url: 'https://openvibe.tools' },
++ ] }],
++ });
++ } catch { /* optional */ }
++ }
if (typeof OpenVibeAccountSwitcher !== 'undefined') {
OpenVibeAccountSwitcher.init({ apiBase: 'https://openvibe.network' });
}