openvibers/openvibe.network Commits

OV mark in the shared navbar brand + network badge (lazy-loads /shared/ov-mark.js), login toasts and button

OP OpenVibers committed 16 Sept 2026 · 864fd6b2f3 · 3 files, +72 −9

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CMxJm1dB8rdsaMSgNjVa82

Changes

ddiff --git a/packages/openvibe-shared/navbar.js b/packages/openvibe-shared/navbar.js
iindex ad00a31f..db9f7c71 100644
---- a/packages/openvibe-shared/navbar.js
++++ b/packages/openvibe-shared/navbar.js
@@@ -702,9 +702,13 @@
const loginHref = resolveLoginHref(currentHost(), window.location.href);
const addAccountHref = `${_config.apiBase}/login?add_account=1&return=${encodeURIComponent(window.location.href)}`;
++ // The OV brand mark is a self-contained drop-in (mounts every .ov-mark it finds).
++ if (!window.__ovMark && !document.getElementById('ov-mark-loader')) {
++ try { const sc = document.createElement('script'); sc.id = 'ov-mark-loader'; sc.src = 'https://openvibe.network/shared/ov-mark.js'; sc.async = true; document.head.appendChild(sc); } catch { /* */ }
++ }
nav.innerHTML = `
<a class="openvibe-navbar-brand" href="${brandHref}">
-- <span class="flame"><i class="fa-solid ${svcIcon}"></i></span>
++ <span class="flame">${svcIcon === 'fa-circle-nodes' ? '<span class="ov-mark" data-size="28"></span>' : `<i class="fa-solid ${svcIcon}"></i>`}</span>
<div>
<div class="name">${svcName}</div>
</div>
@@@ -715,7 +719,7 @@
</div>
<div class="openvibe-navbar-spacer"></div>
<div class="openvibe-navbar-right">
-- <a class="openvibe-network-badge" href="https://openvibe.network" title="Connected to OpenVibe"><i class="fa-solid fa-circle-nodes"></i> OpenVibe</a>
++ <a class="openvibe-network-badge" href="https://openvibe.network" title="Connected to OpenVibe"><span class="ov-mark" data-size="16" data-static="1"></span> OpenVibe</a>
<div id="openvibe-bell-mount"></div>
${u ? avatarImg(u, 64, 'openvibe-navbar-avatar', 'openvibe-avatar-btn') :
`<a class="openvibe-navbar-login" id="openvibe-login-btn" href="${escapeAttr(loginHref)}">Sign In</a>`}
ddiff --git a/packages/openvibe-shared/ov-mark.js b/packages/openvibe-shared/ov-mark.js
nnew file mode 100644
iindex 00000000..2c018cf5
---- /dev/null
++++ b/packages/openvibe-shared/ov-mark.js
@@@ -0,0 +1,59 @@
++/**
++ * ov-mark.js — the OpenVibe "OV" brand mark as a drop-in.
++ * <span class="ov-mark"></span> → 32px animated mark
++ * <span class="ov-mark" data-size="56"> → any size; add data-static="1" for no motion
++ * Self-contained: injects its own CSS once, works on any page (Live, Network, static pages).
++ * The O is a ring with a comet arc running around it, the V is drawn inside with a light
++ * sweep, and a dot orbits the ring. Transform/opacity/stroke motion only.
++ */
++(function () {
++ 'use strict';
++ if (window.__ovMark) return; window.__ovMark = true;
++ let n = 0;
++ const CSS = `
++.ov-mark{display:inline-grid;place-items:center;width:32px;height:32px;color:var(--accent,#8b5cf6);flex:none;vertical-align:middle;line-height:0}
++.ov-mark svg{width:100%;height:100%;overflow:visible;transform-origin:50% 50%;animation:ovmFloat 5s ease-in-out infinite}
++.ov-mark .g{fill:currentColor;opacity:.1;transform-box:fill-box;transform-origin:center;animation:ovmGlow 3.4s ease-in-out infinite}
++.ov-mark .r{fill:none;stroke:currentColor;stroke-width:4;opacity:.28}
++.ov-mark .c{fill:none;stroke-width:4;stroke-linecap:round;stroke-dasharray:34 79;transform-box:fill-box;transform-origin:center;animation:ovmComet 3.6s linear infinite}
++.ov-mark .v{fill:none;stroke-width:4.6;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:46;animation:ovmDraw 1.1s cubic-bezier(.2,.8,.2,1) both}
++.ov-mark .d{fill:#fff;transform-box:fill-box;transform-origin:center;animation:ovmDot 2.4s ease-in-out infinite}
++.ov-mark .o{fill:#fff;filter:drop-shadow(0 0 3px currentColor)}
++.ov-mark .o2{opacity:.5}
++.ov-mark:hover svg,a:hover>.ov-mark svg,button:hover>.ov-mark svg{animation:ovmSpin .9s cubic-bezier(.2,1.5,.3,1) 1}
++.ov-mark:hover .c{animation-duration:.9s}
++.ov-mark[data-static] svg,.ov-mark[data-static] .g,.ov-mark[data-static] .c,.ov-mark[data-static] .v,.ov-mark[data-static] .d{animation:none!important}
++.ov-mark[data-static] .o{display:none}
++@keyframes ovmFloat{0%,100%{transform:translateY(0) rotate(0deg)}50%{transform:translateY(-1px) rotate(2deg)}}
++@keyframes ovmGlow{0%,100%{opacity:.08;transform:scale(.92)}50%{opacity:.2;transform:scale(1.06)}}
++@keyframes ovmComet{to{transform:rotate(360deg)}}
++@keyframes ovmDraw{from{stroke-dashoffset:46}to{stroke-dashoffset:0}}
++@keyframes ovmDot{0%,100%{transform:scale(1);opacity:.85}50%{transform:scale(1.5);opacity:1}}
++@keyframes ovmSpin{from{transform:rotate(0deg) scale(1)}40%{transform:rotate(200deg) scale(1.18)}to{transform:rotate(360deg) scale(1)}}
++@media (prefers-reduced-motion:reduce){.ov-mark svg,.ov-mark .g,.ov-mark .c,.ov-mark .v,.ov-mark .d{animation:none!important}.ov-mark .o{display:none}.ov-mark .c{stroke-dasharray:none;opacity:.9}}`;
++ function svg(id) {
++ return `<svg viewBox="0 0 48 48" aria-hidden="true"><defs>
++<linearGradient id="${id}g" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="currentColor"/><stop offset="1" stop-color="currentColor" stop-opacity="0.45"/></linearGradient>
++<linearGradient id="${id}v" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#fff"/><stop offset="0.5" stop-color="currentColor"/><stop offset="1" stop-color="#fff" stop-opacity="0.9"/><animateTransform attributeName="gradientTransform" type="translate" from="-1 0" to="1 0" dur="3.2s" repeatCount="indefinite"/></linearGradient>
++<path id="${id}p" d="M24,6 A18,18 0 1,1 23.99,6 Z"/></defs>
++<circle class="g" cx="24" cy="24" r="21"/><circle class="r" cx="24" cy="24" r="18"/><circle class="c" cx="24" cy="24" r="18" stroke="url(#${id}g)"/>
++<path class="v" d="M14.5,17 L24,34 L33.5,17" stroke="url(#${id}v)"/><circle class="d" cx="24" cy="34" r="2.6"/>
++<circle class="o" r="2"><animateMotion dur="3.6s" repeatCount="indefinite"><mpath href="#${id}p"/></animateMotion></circle>
++<circle class="o o2" r="1.4"><animateMotion dur="3.6s" begin="0.14s" repeatCount="indefinite"><mpath href="#${id}p"/></animateMotion></circle></svg>`;
++ }
++ function mount(root) {
++ (root || document).querySelectorAll('.ov-mark:not([data-ov])').forEach(el => {
++ el.setAttribute('data-ov', '1');
++ const size = parseInt(el.getAttribute('data-size'), 10);
++ if (size) { el.style.width = size + 'px'; el.style.height = size + 'px'; }
++ el.innerHTML = svg('ovm' + (++n) + '_');
++ });
++ }
++ function init() {
++ if (!document.getElementById('ov-mark-css')) { const st = document.createElement('style'); st.id = 'ov-mark-css'; st.textContent = CSS; document.head.appendChild(st); }
++ mount();
++ try { new MutationObserver(() => mount()).observe(document.body, { childList: true, subtree: true }); } catch { /* */ }
++ }
++ window.ovMarkMount = mount;
++ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init); else init();
++})();
ddiff --git a/public/login.html b/public/login.html
iindex 2366f686..519c673f 100644
---- a/public/login.html
++++ b/public/login.html
@@@ -320,7 +320,7 @@ body{min-height:100dvh;background:var(--bg);color:var(--text);font-family:'Inter
<input type="password" name="password_confirm" placeholder="Repeat password" required autocomplete="new-password">
</div>
<button type="submit" class="submit-btn" id="register-submit">
-- Create Account <i class="fa-solid fa-circle-nodes"></i>
++ Create Account <span class="ov-mark" data-size="16" data-static="1"></span>
</button>
</form>
<a class="back-link" id="register-back" onclick="showChooser()" style="display:none">← Back to saved accounts</a>
@@@ -673,7 +673,7 @@ async function selectAccount(account) {
}
} else {
// Not OAuth — just switch the active account and go home
-- toast('Welcome back, ' + (account.display_name || account.username) + '! <i class="fa-solid fa-circle-nodes"></i>', 'success');
++ toast('Welcome back, ' + (account.display_name || account.username) + '! <span class="ov-mark" data-size="16" data-static="1"></span>', 'success');
setTimeout(() => window.location.href = getReturnUrl(), 600);
}
}
@@@ -775,10 +775,10 @@ async function handleLogin(e) {
if (!confirmData.redirect || isLoginLikeRedirect(confirmData.redirect)) {
throw new Error('Invalid redirect target returned by OAuth server');
}
-- toast('Welcome back, ' + (data.user.display_name || data.user.username) + '! <i class="fa-solid fa-circle-nodes"></i>');
++ toast('Welcome back, ' + (data.user.display_name || data.user.username) + '! <span class="ov-mark" data-size="16" data-static="1"></span>');
setTimeout(() => window.location.href = confirmData.redirect, 800);
} else {
-- toast('Welcome back, ' + (data.user.display_name || data.user.username) + '! <i class="fa-solid fa-circle-nodes"></i>');
++ toast('Welcome back, ' + (data.user.display_name || data.user.username) + '! <span class="ov-mark" data-size="16" data-static="1"></span>');
setTimeout(() => window.location.href = getReturnUrl(), 800);
}
} catch (err) {
@@@ -854,15 +854,15 @@ async function handleRegister(e) {
if (!confirmData.redirect || isLoginLikeRedirect(confirmData.redirect)) {
throw new Error('Invalid redirect target returned by OAuth server');
}
-- toast('Account created! Welcome to the network <i class="fa-solid fa-circle-nodes"></i>');
++ toast('Account created! Welcome to the network <span class="ov-mark" data-size="16" data-static="1"></span>');
setTimeout(() => window.location.href = confirmData.redirect, 800);
} else {
-- toast('Account created! Welcome to the network <i class="fa-solid fa-circle-nodes"></i>');
++ toast('Account created! Welcome to the network <span class="ov-mark" data-size="16" data-static="1"></span>');
setTimeout(() => window.location.href = getReturnUrl(), 800);
}
} catch (err) {
showMsg('register-msg', err.message, 'error');
-- btn.innerHTML = 'Create Account <i class="fa-solid fa-circle-nodes"></i>';
++ btn.innerHTML = 'Create Account <span class="ov-mark" data-size="16" data-static="1"></span>';
btn.classList.remove('loading');
} finally {
oauthConfirmInFlight = false;