openvibers/openvibe.network Commits
OV mark in the shared navbar brand + network badge (lazy-loads /shared/ov-mark.js), login toasts and button
Changes
@@@ -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>`}nnew file mode 100644@@@ -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();++})();@@@ -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;
@@ -702,9 +702,13 @@
@@ -702,9 +702,13 @@
const loginHref = resolveLoginHref(currentHost(), window.location.href);
const loginHref = resolveLoginHref(currentHost(), window.location.href);
const addAccountHref = `${_config.apiBase}/login?add_account=1&return=${encodeURIComponent(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 = `
nav.innerHTML = `
<a class="openvibe-navbar-brand" href="${brandHref}">
<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>
<div class="name">${svcName}</div>
<div class="name">${svcName}</div>
</div>
</div>
@@ -715,7 +719,7 @@
@@ -715,7 +719,7 @@
</div>
</div>
<div class="openvibe-navbar-spacer"></div>
<div class="openvibe-navbar-spacer"></div>
<div class="openvibe-navbar-right">
<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>
<div id="openvibe-bell-mount"></div>
${u ? avatarImg(u, 64, 'openvibe-navbar-avatar', 'openvibe-avatar-btn') :
${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>`}
`<a class="openvibe-navbar-login" id="openvibe-login-btn" href="${escapeAttr(loginHref)}">Sign In</a>`}
new file mode 100644
new file mode 100644
@@ -0,0 +1,59 @@
@@ -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();
+})();
@@ -320,7 +320,7 @@ body{min-height:100dvh;background:var(--bg);color:var(--text);font-family:'Inter
@@ -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">
<input type="password" name="password_confirm" placeholder="Repeat password" required autocomplete="new-password">
</div>
</div>
<button type="submit" class="submit-btn" id="register-submit">
<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>
</button>
</form>
</form>
<a class="back-link" id="register-back" onclick="showChooser()" style="display:none">← Back to saved accounts</a>
<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) {
@@ -673,7 +673,7 @@ async function selectAccount(account) {
}
}
} else {
} else {
// Not OAuth — just switch the active account and go home
// 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);
setTimeout(() => window.location.href = getReturnUrl(), 600);
}
}
}
}
@@ -775,10 +775,10 @@ async function handleLogin(e) {
@@ -775,10 +775,10 @@ async function handleLogin(e) {
if (!confirmData.redirect || isLoginLikeRedirect(confirmData.redirect)) {
if (!confirmData.redirect || isLoginLikeRedirect(confirmData.redirect)) {
throw new Error('Invalid redirect target returned by OAuth server');
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);
setTimeout(() => window.location.href = confirmData.redirect, 800);
} else {
} 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);
setTimeout(() => window.location.href = getReturnUrl(), 800);
}
}
} catch (err) {
} catch (err) {
@@ -854,15 +854,15 @@ async function handleRegister(e) {
@@ -854,15 +854,15 @@ async function handleRegister(e) {
if (!confirmData.redirect || isLoginLikeRedirect(confirmData.redirect)) {
if (!confirmData.redirect || isLoginLikeRedirect(confirmData.redirect)) {
throw new Error('Invalid redirect target returned by OAuth server');
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);
setTimeout(() => window.location.href = confirmData.redirect, 800);
} else {
} 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);
setTimeout(() => window.location.href = getReturnUrl(), 800);
}
}
} catch (err) {
} catch (err) {
showMsg('register-msg', err.message, 'error');
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');
btn.classList.remove('loading');
} finally {
} finally {
oauthConfirmInFlight = false;
oauthConfirmInFlight = false;