openvibers/openvibe.shared Commits
Shared 1.29.2: navbar dropdowns (Live's Go Live → Dashboard) are no longer clipped by the sideways-scrolling links row: the menu is fixed-positioned under its link on hover, focus or tap, offset by a filtered bar's box. Usage notes moved from navbar.js to the README (navbar 28.7 KB brotli, budget 29). npm test 42/42.
Changes
@@@ -4,6 +4,10 @@ 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.29.2 — 2026-09-28++++**Navbar dropdowns open again.** A top link's dropdown (Live's "Go Live" → Dashboard) opened but was cut off: it hangs below the links row, which scrolls sideways (`overflow-x: auto`) and so clips anything below it. The menu is now fixed-positioned under its link whenever it can show (hover, keyboard focus, a tap), measured from the box a filtered or transformed bar makes for fixed children. The usage notes at the top of `navbar.js` moved to the README ("The navbar"), which keeps the file inside its 29 KB brotli budget (28.7 KB).++## 1.29.1 — 2026-09-28**Themes repaint the whole page and keep button text readable.** `applyVars` sets the canvas through the variables (`var(--bg-primary, value)`), so a page with its own theme switcher that sets `--bg-primary` later repaints the background too, instead of leaving the first theme's colour behind; `save()` applies the theme it saves, so every token, the canvas and the browser UI follow at once. A theme without its own `--on-accent` (custom colours, older server themes) gets one derived by WCAG relative luminance, near-black on bright accents (a lime green) and white on dark ones; `OpenVibeThemeLoader.onColor(hex)` is exported for pages that paint their own accent surfaces. **Navbar brand, compact:** the mark and the name sit 6px apart with 2px padding, the ".Live" part hugs the name, and the bar's side padding follows the viewport (`clamp(8px, 1.6vw, 16px)`), so narrow screens keep more room for links.@@@ -345,6 +345,36 @@ can't load, those icons fall back to `<i class="fa-solid …">`. Two options cha- A Content-Security-Policy refusal, a signed-out 401/403, or Network's `REALTIME_TICKETS=off` (503) stop the stream, and the bell polls.- **The site's CSP** needs `connect-src https://events.openvibe.network` (and `https://openvibe.network` for the ticket, which the bell already calls). `OpenVibeNotifications.realtimeState()` reports the feed's state.++### The navbar (`navbar.js`)++++```text++Consistent top bar across all services with logo, navigation,++notification bell, account switcher, and theme-aware styling.++Usage: OpenVibeNavbar.init({ service, token, user, apiBase })++ Optional: loginUrl (override the Sign In href), sessionUrl (same-origin++ endpoint returning { user } — asked when no usable ov_token exists, including++ when a stored token turned out stale), logoutUrl (where Sign out goes, e.g. the++ site's '/auth/logout?next={path}', so a server-side session ends too; {url} is++ the full return URL, {path} its local path — loginUrl takes both too),++ onLogin/onLogout callbacks. When no `user` is passed the navbar resolves++ it itself: ov_token cookie → localStorage → token opt → sessionUrl, then++ GET {apiBase}/api/auth/me with `Authorization: Bearer <token>`.++Brand: derived from the hostname — `pastes.openvibe.tools` renders as++ Pastes · OpenVibe · Tools (three segments, the subdomain first so the++ context reads left-to-right), `openvibe.live` as OpenVibe · Live. Pass++ brand: { sub, tld, name, icon, variant } to override any part, or the++ legacy brandName/brandIcon. compact: 'auto' (default — the brand shortens++ to the subdomain on narrow viewports), 'always', 'never'.++Menus are modular: every site keeps the shared account menu and adds its own++ pieces — links: [{label, href, icon?, active?}] replaces the service's top++ links; menu: { before: [item], after: [item] } adds dropdown rows++ ({label, href, icon, onClick, danger, external}); OpenVibeNavbar.addMenuItem()++ / setLinks() do the same at runtime. Signed-in users also get a++ "Recently used" row fed by the shared history module when it is loaded.++```++++Dropdowns under a top link (`children`) are fixed-positioned under their link when they show (hover, keyboard focus, a tap), because the links row scrolls sideways and would clip them.++## Development```bash@@@ -4,7 +4,7 @@"code": true,"wave": "W2","package": "openvibe-shared",-- "version": "1.29.1",++ "version": "1.29.2","plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026)","deployed": false,"deployment": "none of its own: a package released as git tags; OpenVibe.Network serves /shared/* from its pinned tag, and a site can serve its own pinned copy (openvibe-shared/serve)",@@@ -1,30 +1,4 @@--// ═══════════════════════════════════════════════════════════════--// OpenVibe — Universal Navbar--// Consistent top bar across all services with logo, navigation,--// notification bell, account switcher, and theme-aware styling.--// Usage: OpenVibeNavbar.init({ service, token, user, apiBase })--// Optional: loginUrl (override the Sign In href), sessionUrl (same-origin--// endpoint returning { user } — asked when no usable ov_token exists, including--// when a stored token turned out stale), logoutUrl (where Sign out goes, e.g. the--// site's '/auth/logout?next={path}', so a server-side session ends too; {url} is--// the full return URL, {path} its local path — loginUrl takes both too),--// onLogin/onLogout callbacks. When no `user` is passed the navbar resolves--// it itself: ov_token cookie → localStorage → token opt → sessionUrl, then--// GET {apiBase}/api/auth/me with `Authorization: Bearer <token>`.--//--// Brand: derived from the hostname — `pastes.openvibe.tools` renders as--// Pastes · OpenVibe · Tools (three segments, the subdomain first so the--// context reads left-to-right), `openvibe.live` as OpenVibe · Live. Pass--// brand: { sub, tld, name, icon, variant } to override any part, or the--// legacy brandName/brandIcon. compact: 'auto' (default — the brand shortens--// to the subdomain on narrow viewports), 'always', 'never'.--// Menus are modular: every site keeps the shared account menu and adds its own--// pieces — links: [{label, href, icon?, active?}] replaces the service's top--// links; menu: { before: [item], after: [item] } adds dropdown rows--// ({label, href, icon, onClick, danger, external}); OpenVibeNavbar.addMenuItem()--// / setLinks() do the same at runtime. Signed-in users also get a--// "Recently used" row fed by the shared history module when it is loaded.--// ═══════════════════════════════════════════════════════════════++// OpenVibe — the shared navbar (the OpenVibe Frame's top bar). Options and behaviour: README.md, "The navbar".(function (root) {'use strict';@@@ -96,7 +70,7 @@@keyframes ovnavPulse { 70% { box-shadow: 0 0 0 6px rgba(239,68,68,0); } 100% { box-shadow: 0 0 0 0 rgba(239,68,68,0); } }.openvibe-navbar .ovnav-caret { font-size: 10px; opacity: .6; }.openvibe-navbar .ovnav-dd { position: relative; display: inline-flex; flex: none; }-- .openvibe-navbar .ovnav-dd-menu { display: none; position: absolute; top: 100%; left: 0; min-width: 180px; padding: 5px; border-radius: 12px; z-index: 1002; background: var(--bg-elevated, var(--bg-secondary, #111826)); border: 1px solid var(--border, rgba(255,255,255,.12)); box-shadow: 0 18px 44px rgba(0,0,0,.45); flex-direction: column; gap: 2px; }++ .openvibe-navbar .ovnav-dd-menu { display: none; position: fixed; top: 56px; left: 0; min-width: 180px; padding: 5px; border-radius: 12px; z-index: 1002; background: var(--bg-elevated, var(--bg-secondary, #111826)); border: 1px solid var(--border, rgba(255,255,255,.12)); box-shadow: 0 18px 44px rgba(0,0,0,.45); flex-direction: column; gap: 2px; }.openvibe-navbar .ovnav-dd:hover .ovnav-dd-menu, .openvibe-navbar .ovnav-dd.open .ovnav-dd-menu, .openvibe-navbar .ovnav-dd:focus-within .ovnav-dd-menu { display: flex; }.openvibe-navbar .ovnav-chip { all: unset; box-sizing: border-box; cursor: pointer; display: inline-flex; align-items: center; gap: 6px; height: 32px; padding: 0 11px; border-radius: 999px; font: 700 13px/1 inherit; color: var(--chip-c, var(--text-primary, #e6edf7)); background: color-mix(in srgb, var(--chip-c, var(--accent, #3b82f6)) 12%, transparent); border: 1px solid color-mix(in srgb, var(--chip-c, var(--accent, #3b82f6)) 38%, transparent); font-variant-numeric: tabular-nums; }.ovnav-chip[data-tone="gold"] { --chip-c: #fbbf24; } .ovnav-chip[data-tone="green"] { --chip-c: #4ade80; } .ovnav-chip[hidden] { display: none !important; }@@@ -715,7 +689,7 @@const item = byId.get(a.getAttribute('data-link-id'));// A parent of a dropdown opens it on touch (there is no hover); a second tap follows the link.const dd = a.parentElement && a.parentElement.classList.contains('ovnav-dd') ? a.parentElement : null;-- if (dd && matchMedia('(hover: none)').matches && !dd.classList.contains('open')) { e.preventDefault(); nav.querySelectorAll('.ovnav-dd.open').forEach(x => x.classList.remove('open')); dd.classList.add('open'); return; }++ if (dd && matchMedia('(hover: none)').matches && !dd.classList.contains('open')) { e.preventDefault(); nav.querySelectorAll('.ovnav-dd.open').forEach(x => x.classList.remove('open')); dd.classList.add('open'); placeDd(dd); return; }if (item && typeof item.onClick === 'function') { const r = item.onClick(e); if (r === false) e.preventDefault(); closeDrawer(); if (e.defaultPrevented) return; }if (typeof _config.onNavigate === 'function' && a.target !== '_blank') {let same = false; try { same = new URL(a.href, location.href).origin === location.origin; } catch { /* */ }@@@ -724,6 +698,16 @@closeDrawer();});document.addEventListener('click', (e) => { if (!e.target.closest || !e.target.closest('.ovnav-dd')) nav.querySelectorAll('.ovnav-dd.open').forEach(x => x.classList.remove('open')); });++ // Dropdowns hang below the links row, which scrolls sideways and would clip them: fixed, under the link (less a filtered bar's offset).++ const placeDd = (dd) => {++ const m = dd && dd.querySelector(':scope>.ovnav-dd-menu'); if (!m) return;++ let cb = dd.parentElement, o = { top: 0, left: 0 };++ for (; cb && cb !== document.body; cb = cb.parentElement) { const c = getComputedStyle(cb); if (c.transform + c.filter + (c.backdropFilter || 'none') !== 'nonenonenone') { o = cb.getBoundingClientRect(); break; } }++ const r = dd.getBoundingClientRect();++ m.style.top = `${r.bottom - o.top}px`; m.style.left = `${Math.max(8, Math.min(r.left, innerWidth - 188)) - o.left}px`;++ };++ const place = (e) => placeDd(e.target.closest && e.target.closest('.ovnav-dd'));++ nav.addEventListener('pointerover', place); nav.addEventListener('focusin', place);if (burger && drawer) {burger.addEventListener('click', (e) => { e.stopPropagation(); const open = drawer.classList.toggle('open'); burger.setAttribute('aria-expanded', String(open)); });document.addEventListener('click', (e) => { if (drawer.classList.contains('open') && !drawer.contains(e.target) && !burger.contains(e.target)) closeDrawer(); });@@@ -1,6 +1,6 @@{"name": "openvibe-shared",-- "version": "1.29.1",++ "version": "1.29.2","description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site","main": "index.js","exports": {
@@ -4,6 +4,10 @@ All notable changes to `openvibe-shared`. Versions follow [semver](https://semve
@@ -4,6 +4,10 @@ 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.29.2 — 2026-09-28
+
+**Navbar dropdowns open again.** A top link's dropdown (Live's "Go Live" → Dashboard) opened but was cut off: it hangs below the links row, which scrolls sideways (`overflow-x: auto`) and so clips anything below it. The menu is now fixed-positioned under its link whenever it can show (hover, keyboard focus, a tap), measured from the box a filtered or transformed bar makes for fixed children. The usage notes at the top of `navbar.js` moved to the README ("The navbar"), which keeps the file inside its 29 KB brotli budget (28.7 KB).
+
## 1.29.1 — 2026-09-28
## 1.29.1 — 2026-09-28
**Themes repaint the whole page and keep button text readable.** `applyVars` sets the canvas through the variables (`var(--bg-primary, value)`), so a page with its own theme switcher that sets `--bg-primary` later repaints the background too, instead of leaving the first theme's colour behind; `save()` applies the theme it saves, so every token, the canvas and the browser UI follow at once. A theme without its own `--on-accent` (custom colours, older server themes) gets one derived by WCAG relative luminance, near-black on bright accents (a lime green) and white on dark ones; `OpenVibeThemeLoader.onColor(hex)` is exported for pages that paint their own accent surfaces. **Navbar brand, compact:** the mark and the name sit 6px apart with 2px padding, the ".Live" part hugs the name, and the bar's side padding follows the viewport (`clamp(8px, 1.6vw, 16px)`), so narrow screens keep more room for links.
**Themes repaint the whole page and keep button text readable.** `applyVars` sets the canvas through the variables (`var(--bg-primary, value)`), so a page with its own theme switcher that sets `--bg-primary` later repaints the background too, instead of leaving the first theme's colour behind; `save()` applies the theme it saves, so every token, the canvas and the browser UI follow at once. A theme without its own `--on-accent` (custom colours, older server themes) gets one derived by WCAG relative luminance, near-black on bright accents (a lime green) and white on dark ones; `OpenVibeThemeLoader.onColor(hex)` is exported for pages that paint their own accent surfaces. **Navbar brand, compact:** the mark and the name sit 6px apart with 2px padding, the ".Live" part hugs the name, and the bar's side padding follows the viewport (`clamp(8px, 1.6vw, 16px)`), so narrow screens keep more room for links.
@@ -345,6 +345,36 @@ can't load, those icons fall back to `<i class="fa-solid …">`. Two options cha
@@ -345,6 +345,36 @@ can't load, those icons fall back to `<i class="fa-solid …">`. Two options cha
- A Content-Security-Policy refusal, a signed-out 401/403, or Network's `REALTIME_TICKETS=off` (503) stop the stream, and the bell polls.
- A Content-Security-Policy refusal, a signed-out 401/403, or Network's `REALTIME_TICKETS=off` (503) stop the stream, and the bell polls.
- **The site's CSP** needs `connect-src https://events.openvibe.network` (and `https://openvibe.network` for the ticket, which the bell already calls). `OpenVibeNotifications.realtimeState()` reports the feed's state.
- **The site's CSP** needs `connect-src https://events.openvibe.network` (and `https://openvibe.network` for the ticket, which the bell already calls). `OpenVibeNotifications.realtimeState()` reports the feed's state.
+### The navbar (`navbar.js`)
+
+```text
+Consistent top bar across all services with logo, navigation,
+notification bell, account switcher, and theme-aware styling.
+Usage: OpenVibeNavbar.init({ service, token, user, apiBase })
+ Optional: loginUrl (override the Sign In href), sessionUrl (same-origin
+ endpoint returning { user } — asked when no usable ov_token exists, including
+ when a stored token turned out stale), logoutUrl (where Sign out goes, e.g. the
+ site's '/auth/logout?next={path}', so a server-side session ends too; {url} is
+ the full return URL, {path} its local path — loginUrl takes both too),
+ onLogin/onLogout callbacks. When no `user` is passed the navbar resolves
+ it itself: ov_token cookie → localStorage → token opt → sessionUrl, then
+ GET {apiBase}/api/auth/me with `Authorization: Bearer <token>`.
+Brand: derived from the hostname — `pastes.openvibe.tools` renders as
+ Pastes · OpenVibe · Tools (three segments, the subdomain first so the
+ context reads left-to-right), `openvibe.live` as OpenVibe · Live. Pass
+ brand: { sub, tld, name, icon, variant } to override any part, or the
+ legacy brandName/brandIcon. compact: 'auto' (default — the brand shortens
+ to the subdomain on narrow viewports), 'always', 'never'.
+Menus are modular: every site keeps the shared account menu and adds its own
+ pieces — links: [{label, href, icon?, active?}] replaces the service's top
+ links; menu: { before: [item], after: [item] } adds dropdown rows
+ ({label, href, icon, onClick, danger, external}); OpenVibeNavbar.addMenuItem()
+ / setLinks() do the same at runtime. Signed-in users also get a
+ "Recently used" row fed by the shared history module when it is loaded.
+```
+
+Dropdowns under a top link (`children`) are fixed-positioned under their link when they show (hover, keyboard focus, a tap), because the links row scrolls sideways and would clip them.
+
## Development
## Development
```bash
```bash
@@ -4,7 +4,7 @@
@@ -4,7 +4,7 @@
"code": true,
"code": true,
"wave": "W2",
"wave": "W2",
"package": "openvibe-shared",
"package": "openvibe-shared",
- "version": "1.29.1",
+ "version": "1.29.2",
"plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026)",
"plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026)",
"deployed": false,
"deployed": false,
"deployment": "none of its own: a package released as git tags; OpenVibe.Network serves /shared/* from its pinned tag, and a site can serve its own pinned copy (openvibe-shared/serve)",
"deployment": "none of its own: a package released as git tags; OpenVibe.Network serves /shared/* from its pinned tag, and a site can serve its own pinned copy (openvibe-shared/serve)",
@@ -1,30 +1,4 @@
@@ -1,30 +1,4 @@
-// ═══════════════════════════════════════════════════════════════
-// OpenVibe — Universal Navbar
-// Consistent top bar across all services with logo, navigation,
-// notification bell, account switcher, and theme-aware styling.
-// Usage: OpenVibeNavbar.init({ service, token, user, apiBase })
-// Optional: loginUrl (override the Sign In href), sessionUrl (same-origin
-// endpoint returning { user } — asked when no usable ov_token exists, including
-// when a stored token turned out stale), logoutUrl (where Sign out goes, e.g. the
-// site's '/auth/logout?next={path}', so a server-side session ends too; {url} is
-// the full return URL, {path} its local path — loginUrl takes both too),
-// onLogin/onLogout callbacks. When no `user` is passed the navbar resolves
-// it itself: ov_token cookie → localStorage → token opt → sessionUrl, then
-// GET {apiBase}/api/auth/me with `Authorization: Bearer <token>`.
-//
-// Brand: derived from the hostname — `pastes.openvibe.tools` renders as
-// Pastes · OpenVibe · Tools (three segments, the subdomain first so the
-// context reads left-to-right), `openvibe.live` as OpenVibe · Live. Pass
-// brand: { sub, tld, name, icon, variant } to override any part, or the
-// legacy brandName/brandIcon. compact: 'auto' (default — the brand shortens
-// to the subdomain on narrow viewports), 'always', 'never'.
-// Menus are modular: every site keeps the shared account menu and adds its own
-// pieces — links: [{label, href, icon?, active?}] replaces the service's top
-// links; menu: { before: [item], after: [item] } adds dropdown rows
-// ({label, href, icon, onClick, danger, external}); OpenVibeNavbar.addMenuItem()
-// / setLinks() do the same at runtime. Signed-in users also get a
-// "Recently used" row fed by the shared history module when it is loaded.
-// ═══════════════════════════════════════════════════════════════
+// OpenVibe — the shared navbar (the OpenVibe Frame's top bar). Options and behaviour: README.md, "The navbar".
(function (root) {
(function (root) {
'use strict';
'use strict';
@@ -96,7 +70,7 @@
@@ -96,7 +70,7 @@
@keyframes ovnavPulse { 70% { box-shadow: 0 0 0 6px rgba(239,68,68,0); } 100% { box-shadow: 0 0 0 0 rgba(239,68,68,0); } }
@keyframes ovnavPulse { 70% { box-shadow: 0 0 0 6px rgba(239,68,68,0); } 100% { box-shadow: 0 0 0 0 rgba(239,68,68,0); } }
.openvibe-navbar .ovnav-caret { font-size: 10px; opacity: .6; }
.openvibe-navbar .ovnav-caret { font-size: 10px; opacity: .6; }
.openvibe-navbar .ovnav-dd { position: relative; display: inline-flex; flex: none; }
.openvibe-navbar .ovnav-dd { position: relative; display: inline-flex; flex: none; }
- .openvibe-navbar .ovnav-dd-menu { display: none; position: absolute; top: 100%; left: 0; min-width: 180px; padding: 5px; border-radius: 12px; z-index: 1002; background: var(--bg-elevated, var(--bg-secondary, #111826)); border: 1px solid var(--border, rgba(255,255,255,.12)); box-shadow: 0 18px 44px rgba(0,0,0,.45); flex-direction: column; gap: 2px; }
+ .openvibe-navbar .ovnav-dd-menu { display: none; position: fixed; top: 56px; left: 0; min-width: 180px; padding: 5px; border-radius: 12px; z-index: 1002; background: var(--bg-elevated, var(--bg-secondary, #111826)); border: 1px solid var(--border, rgba(255,255,255,.12)); box-shadow: 0 18px 44px rgba(0,0,0,.45); flex-direction: column; gap: 2px; }
.openvibe-navbar .ovnav-dd:hover .ovnav-dd-menu, .openvibe-navbar .ovnav-dd.open .ovnav-dd-menu, .openvibe-navbar .ovnav-dd:focus-within .ovnav-dd-menu { display: flex; }
.openvibe-navbar .ovnav-dd:hover .ovnav-dd-menu, .openvibe-navbar .ovnav-dd.open .ovnav-dd-menu, .openvibe-navbar .ovnav-dd:focus-within .ovnav-dd-menu { display: flex; }
.openvibe-navbar .ovnav-chip { all: unset; box-sizing: border-box; cursor: pointer; display: inline-flex; align-items: center; gap: 6px; height: 32px; padding: 0 11px; border-radius: 999px; font: 700 13px/1 inherit; color: var(--chip-c, var(--text-primary, #e6edf7)); background: color-mix(in srgb, var(--chip-c, var(--accent, #3b82f6)) 12%, transparent); border: 1px solid color-mix(in srgb, var(--chip-c, var(--accent, #3b82f6)) 38%, transparent); font-variant-numeric: tabular-nums; }
.openvibe-navbar .ovnav-chip { all: unset; box-sizing: border-box; cursor: pointer; display: inline-flex; align-items: center; gap: 6px; height: 32px; padding: 0 11px; border-radius: 999px; font: 700 13px/1 inherit; color: var(--chip-c, var(--text-primary, #e6edf7)); background: color-mix(in srgb, var(--chip-c, var(--accent, #3b82f6)) 12%, transparent); border: 1px solid color-mix(in srgb, var(--chip-c, var(--accent, #3b82f6)) 38%, transparent); font-variant-numeric: tabular-nums; }
.ovnav-chip[data-tone="gold"] { --chip-c: #fbbf24; } .ovnav-chip[data-tone="green"] { --chip-c: #4ade80; } .ovnav-chip[hidden] { display: none !important; }
.ovnav-chip[data-tone="gold"] { --chip-c: #fbbf24; } .ovnav-chip[data-tone="green"] { --chip-c: #4ade80; } .ovnav-chip[hidden] { display: none !important; }
@@ -715,7 +689,7 @@
@@ -715,7 +689,7 @@
const item = byId.get(a.getAttribute('data-link-id'));
const item = byId.get(a.getAttribute('data-link-id'));
// A parent of a dropdown opens it on touch (there is no hover); a second tap follows the link.
// A parent of a dropdown opens it on touch (there is no hover); a second tap follows the link.
const dd = a.parentElement && a.parentElement.classList.contains('ovnav-dd') ? a.parentElement : null;
const dd = a.parentElement && a.parentElement.classList.contains('ovnav-dd') ? a.parentElement : null;
- if (dd && matchMedia('(hover: none)').matches && !dd.classList.contains('open')) { e.preventDefault(); nav.querySelectorAll('.ovnav-dd.open').forEach(x => x.classList.remove('open')); dd.classList.add('open'); return; }
+ if (dd && matchMedia('(hover: none)').matches && !dd.classList.contains('open')) { e.preventDefault(); nav.querySelectorAll('.ovnav-dd.open').forEach(x => x.classList.remove('open')); dd.classList.add('open'); placeDd(dd); return; }
if (item && typeof item.onClick === 'function') { const r = item.onClick(e); if (r === false) e.preventDefault(); closeDrawer(); if (e.defaultPrevented) return; }
if (item && typeof item.onClick === 'function') { const r = item.onClick(e); if (r === false) e.preventDefault(); closeDrawer(); if (e.defaultPrevented) return; }
if (typeof _config.onNavigate === 'function' && a.target !== '_blank') {
if (typeof _config.onNavigate === 'function' && a.target !== '_blank') {
let same = false; try { same = new URL(a.href, location.href).origin === location.origin; } catch { /* */ }
let same = false; try { same = new URL(a.href, location.href).origin === location.origin; } catch { /* */ }
@@ -724,6 +698,16 @@
@@ -724,6 +698,16 @@
closeDrawer();
closeDrawer();
});
});
document.addEventListener('click', (e) => { if (!e.target.closest || !e.target.closest('.ovnav-dd')) nav.querySelectorAll('.ovnav-dd.open').forEach(x => x.classList.remove('open')); });
document.addEventListener('click', (e) => { if (!e.target.closest || !e.target.closest('.ovnav-dd')) nav.querySelectorAll('.ovnav-dd.open').forEach(x => x.classList.remove('open')); });
+ // Dropdowns hang below the links row, which scrolls sideways and would clip them: fixed, under the link (less a filtered bar's offset).
+ const placeDd = (dd) => {
+ const m = dd && dd.querySelector(':scope>.ovnav-dd-menu'); if (!m) return;
+ let cb = dd.parentElement, o = { top: 0, left: 0 };
+ for (; cb && cb !== document.body; cb = cb.parentElement) { const c = getComputedStyle(cb); if (c.transform + c.filter + (c.backdropFilter || 'none') !== 'nonenonenone') { o = cb.getBoundingClientRect(); break; } }
+ const r = dd.getBoundingClientRect();
+ m.style.top = `${r.bottom - o.top}px`; m.style.left = `${Math.max(8, Math.min(r.left, innerWidth - 188)) - o.left}px`;
+ };
+ const place = (e) => placeDd(e.target.closest && e.target.closest('.ovnav-dd'));
+ nav.addEventListener('pointerover', place); nav.addEventListener('focusin', place);
if (burger && drawer) {
if (burger && drawer) {
burger.addEventListener('click', (e) => { e.stopPropagation(); const open = drawer.classList.toggle('open'); burger.setAttribute('aria-expanded', String(open)); });
burger.addEventListener('click', (e) => { e.stopPropagation(); const open = drawer.classList.toggle('open'); burger.setAttribute('aria-expanded', String(open)); });
document.addEventListener('click', (e) => { if (drawer.classList.contains('open') && !drawer.contains(e.target) && !burger.contains(e.target)) closeDrawer(); });
document.addEventListener('click', (e) => { if (drawer.classList.contains('open') && !drawer.contains(e.target) && !burger.contains(e.target)) closeDrawer(); });
@@ -1,6 +1,6 @@
@@ -1,6 +1,6 @@
{
{
"name": "openvibe-shared",
"name": "openvibe-shared",
- "version": "1.29.1",
+ "version": "1.29.2",
"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": {