openvibers/openvibe.shared Commits
4.129.0: one menu (#317)
Changes
@@@ -1,5 +1,16 @@# Changelog++## 4.129.0 — 2026-11-07++++- **One menu** (the owner, 11-07, on the right-hand drawer: "sloppy … maybe combining this menu and the other menu and just++ putting our main button on the right-hand side"). The burger now opens the OpenVibe menu, grown from the burger:++ - this site's pages lead, all of them (the drawer's job);++ - then the live bar, your places and workspace, the network, What's new, tools and Jump back in;++ - **Display** (Light/Dark/Auto, text size, Motion/Calm) sits at the foot, drawn by the same controls the drawer had.++++ The orb by the logo opens the same menu with the places first. The old drawer remains only for a site that turns the++ menu off (`search: false`) or asks for it (`drawer: 'legacy'`). The burger's `aria-expanded` follows the menu.++## 4.128.0 — 2026-11-07- **The menu, rethought around real use** (the owner, 11-07: Jump back in came first, people opened Sites every time just to@@@ -2,7 +2,7 @@> Versioned UI, the OpenVibe Frame (the navbar, footer and "shipped" views every site sits in), SEO, legal and release-client packages every OpenVibe site renders.--**Status:** alpha, v4.128.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed++**Status:** alpha, v4.129.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployedconsumer installs a tagged release and none keeps a vendored copy([docs/migration-plan.md](docs/migration-plan.md) is done); each repository's `package.json` namesthe tag it pins.@@@ -4,7 +4,7 @@"code": true,"wave": "W2","package": "openvibe-shared",-- "version": "4.128.0",++ "version": "4.129.0","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)",@@@ -21,6 +21,7 @@"share cards for every site and for its big pages (openvibe-shared/social)","the shared video player (player.js, player.css): one look for every <video>","the player's AD control: a descriptions track is remembered per person, toggled with D and spoken through speech synthesis (4.126.0)",++ "one menu: the burger opens the OpenVibe menu, this site's pages first and Display at its foot; the old drawer only for sites that ask (4.129.0)","the menu rethought around real use: who is live, your places dock, this site's pages, next from here, two places new to you, you and your agents, tools, every site behind a count, jump back in; a click beacon when Network asks (4.128.0)","items.js: what people wear from OpenVibe.Inventory (name effects, hats, particles, community badges), drawn the same on every site","shipped pill, recent list and /updates log",@@@ -1052,7 +1052,14 @@nav.addEventListener('pointerover', place); nav.addEventListener('focusin', place);if (burger && drawer) {if (!_config.user) { bindDisplayControls(drawer.querySelector('.ovnav-display')); bindLook(drawer); } // guests: members have them in the account menu-- burger.addEventListener('click', (e) => { e.stopPropagation(); const open = drawer.classList.toggle('open'); burger.setAttribute('aria-expanded', String(open)); if (open) loadOvIcons(drawer); });++ // One menu (the owner, 11-07: "combining this menu and the other menu … the main button on the right-hand side"):++ // the burger opens the OpenVibe menu, from the burger, leading with this site's pages. The old drawer stays for a++ // site that turns the menu off (search: false) or asks for it (drawer: 'legacy').++ burger.addEventListener('click', (e) => {++ e.stopPropagation();++ if (_config.search !== false && _config.drawer !== 'legacy') { openSearch('', 'here', burger); return; }++ const open = drawer.classList.toggle('open'); burger.setAttribute('aria-expanded', String(open)); if (open) loadOvIcons(drawer);++ });drawer.addEventListener('click', (e) => {const all = e.target.closest && e.target.closest('[data-open-launcher]'); if (!all) return;e.preventDefault(); e.stopPropagation(); closeDrawer();@@@ -2037,12 +2044,13 @@// Search (search.js, loaded on first use). _config.search: false, or { owner, label, page }.let _searchLoading = false;/** The OpenVibe menu (search.js, loaded on first use): search, every site and every tool. tab: 'you' | 'sites' | 'tools'. */-- function openSearch(q, tab) {++ function openSearch(q, tab, fromEl) {const orb = _navEl && _navEl.querySelector('#openvibe-search-btn');-- // The menu drops from the orb, under the bar (the bar stays, and the orb's × closes it).++ // The menu drops from the button that opened it (the orb, or the burger), under the bar.++ const from = fromEl || orb;const anchor = () => {-- if (!_navEl || !orb || !orb.getBoundingClientRect) return null;-- const bar = _navEl.getBoundingClientRect(), at = orb.getBoundingClientRect();++ if (!_navEl || !from || !from.getBoundingClientRect) return null;++ const bar = _navEl.getBoundingClientRect(), at = from.getBoundingClientRect();return bar.bottom > 0 && bar.bottom < 200 ? { top: bar.bottom, left: at.left - 6, cx: at.left + at.width / 2, cy: at.top + at.height / 2 } : null;};const go = () => {@@@ -2051,8 +2059,10 @@if (_navEl) _navEl.classList.remove('ovnav-hide');root.OpenVibeSearch.open({ service: _config.service, scope: Object.assign({ owner: _config.service }, _config.search || {}), links: flatLinks(rawLinks()), q, tab,frame: OVFrame.get(), token: _config.token, apiBase: _config.apiBase, user: _config.user, icons: (scope) => loadOvIcons(scope), anchor: anchor(), navIcon,-- onClose: () => { const b = _navEl && _navEl.querySelector('#openvibe-search-btn'); if (b) b.setAttribute('aria-expanded', 'false'); } });-- const b = _navEl && _navEl.querySelector('#openvibe-search-btn'); if (b) b.setAttribute('aria-expanded', 'true');++ // Display (theme, text size, motion) at the menu's foot, drawn by the same controls the drawer had.++ display: (el) => { const box = document.createElement('div'); box.className = 'ovnav-display ovs-display'; el.appendChild(box); bindDisplayControls(box); },++ onClose: () => { const b = _navEl && _navEl.querySelector('#openvibe-search-btn'); if (b) b.setAttribute('aria-expanded', 'false'); const g = _navEl && _navEl.querySelector('#openvibe-burger'); if (g) g.setAttribute('aria-expanded', 'false'); } });++ const b = fromEl || (_navEl && _navEl.querySelector('#openvibe-search-btn')); if (b) b.setAttribute('aria-expanded', 'true');};if (root.OpenVibeSearch) return go();if (_searchLoading) return;@@@ -1,6 +1,6 @@{"name": "openvibe-shared",-- "version": "4.128.0",++ "version": "4.129.0","description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site","main": "index.js","bin": {@@@ -62,7 +62,7 @@{ name: 'Developer Tools', icon: 'code', url: 'https://dev.openvibe.tools/' },{ name: 'Network Tools', icon: 'dns', url: 'https://net.openvibe.tools/' },];-- const TABS = [['you', 'Start'], ['sites', 'Sites'], ['tools', 'Tools']];++ const TABS = [['you', 'Start'], ['sites', 'Sites'], ['tools', 'Tools'], ['here', 'Here']];let el = null, input = null, ghost = null, listEl = null, tabsEl = null, ctx = {}, items = [], active = -1, inflight = null, timer = null, lastFocus = null;let tab = 'you', mine = null, catalog = null, completion = '';// The results view (4.29.0): Enter shows the Search Fabric's answer here, by scope, instead of leaving the page.@@@ -238,6 +238,8 @@.ovs-tag { margin-left: 7px; padding: 1px 7px; border-radius: 999px; font-style: normal; font-size: 10.5px; font-weight: 800; letter-spacing: .03em; vertical-align: 1px; color: color-mix(in srgb, var(--accent, #3b82f6) 72%, var(--text-primary, #e8ecf3)); background: color-mix(in srgb, var(--accent, #3b82f6) 15%, transparent); }.ovs-card { padding: 2px 6px 6px; }.ovs-cardbody { border-radius: 16px; border: 1px solid var(--border, rgba(255,255,255,.12)); background: color-mix(in srgb, var(--text-primary, #e8ecf3) 4%, transparent); padding: 12px 14px; }++.ovs-displayrow { padding: 4px 8px 10px; }++.ovs-displayrow .ovnav-display { margin-top: 4px; }.ovs-host { display: block; margin-top: 2px; font-size: 11px; font-weight: 650; letter-spacing: .01em; color: color-mix(in srgb, var(--accent, #3b82f6) 60%, var(--text-muted, #93a0b4)); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }.ovs-group .ovs-more { margin-left: auto; color: var(--accent-light, #8ab4ff); font-size: 11.5px; font-weight: 650; text-transform: none; letter-spacing: 0; text-decoration: none; }@media (hover: hover) { .ovs-app:hover, .ovs-link:hover, .ovs-live:hover, .ovs-fold:hover { background: var(--bg-hover, rgba(255,255,255,.07)); color: inherit; } .ovs-app:hover .ovs-appic { transform: translateY(-2px); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent, #3b82f6) 45%, transparent); } }@@@ -424,12 +426,18 @@function paint(groups, note) {items = [];listEl.textContent = '';-- groups.filter((g) => (g.items && g.items.length) || ['note', 'loading', 'route', 'fold', 'card'].includes(g.as)).forEach((g) => {++ groups.filter((g) => (g.items && g.items.length) || ['note', 'loading', 'route', 'fold', 'card', 'display'].includes(g.as)).forEach((g) => {if (g.as === 'route') {const r = document.createElement('li'); r.className = 'ovs-route'; r.setAttribute('role', 'presentation'); r.textContent = g.text; listEl.appendChild(r);return;}if (g.as === 'fold') { listEl.appendChild(foldButton(g.fold, g.section)); return; }++ if (g.as === 'display') {++ const li = document.createElement('li'); li.className = 'ovs-displayrow'; li.setAttribute('role', 'presentation'); li.setAttribute('data-sec', 'display');++ try { ctx.display(li); } catch { return; }++ listEl.appendChild(li);++ return;++ }if (g.as === 'card') {const body = document.createElement('div'); body.className = 'ovs-cardbody';try {@@@ -627,7 +635,7 @@live: 'Streams and calls', 'media-hub': 'Recordings and clips', community: 'Forum and pastes', chat: 'Rooms and messages',tools: 'Free tools', bot: 'Robots and machines', vip: 'Back a streamer', pics: 'Pictures', download: 'Files and drive',};-- let allOpen = false, hereOpen = false, discoverPick = null, pulseData = null;++ let allOpen = false, hereOpen = false, leadHere = false, discoverPick = null, pulseData = null;/** Network's live line (GET /api/frame/pulse, kept 30 s): how many are live now, and who when the Frame sends it. */function loadPulse() {try { const c = JSON.parse(sessionStorage.getItem('ov_pulse') || 'null'); if (c && Date.now() - c.at < 30000) { pulseData = c.data; return Promise.resolve(c.data); } } catch { /* */ }@@@ -753,7 +761,8 @@const allPages = (ctx.links || []).filter((l) => l && l.label && typeof l.href === 'string' && !l.hidden).map((l) => ({ title: l.label, url: safeUrl(l.href), icon: 'go', drawn: l.icon && typeof ctx.navIcon === 'function' ? ctx.navIcon(l.icon) : '', kind: 'page' })).filter((x) => x.url);if (allPages.length) {const head = hereOpen ? allPages : allPages.slice(0, 6);-- groups.push({ title: `On ${here()}`, as: 'links', items: head, section: 'here', fold: allPages.length > 6 ? { open: hereOpen, label: hereOpen ? 'Fewer' : `More on ${here()} (${allPages.length - 6})`, key: 'here' } : null });++ const hereGroup = { title: `On ${here()}`, as: 'links', items: head, section: 'here', fold: allPages.length > 6 ? { open: hereOpen, label: hereOpen ? 'Fewer' : `More on ${here()} (${allPages.length - 6})`, key: 'here' } : null };++ if (leadHere) groups.unshift(hereGroup); else groups.push(hereGroup);}// 4. Where people go next from here: the Frame's list, else the moves Network saw (11-07) and this site's group.const f = ctx.frame;@@@ -811,6 +820,8 @@const back = mine ? mine.items.filter((h) => { if (seen.has(h.url)) return false; seen.add(h.url); return true; }).slice(0, 5).map((h) => ({ title: h.title, meta: [h.site, ago(h.at)].filter(Boolean).join(' · '), url: h.url, icon: 'back', kind: 'history' })) : [];groups.push({ title: 'Jump back in', items: back, section: 'back' });++ // Display: theme, text size and motion, at the foot (the drawer's row, now in the one menu).++ if (typeof ctx.display === 'function') groups.push({ title: '', as: 'display', items: [], section: 'display' });paint(groups);}@@@ -975,7 +986,8 @@lastFocus = document.activeElement;tab = TABS.some(([id]) => id === opts.tab) ? opts.tab : 'you';// The old tabs, kept as a way in: "sites" opens the list of every site, "tools" scrolls to the tools.-- allOpen = tab === 'sites'; hereOpen = false; discoverPick = null;++ // From the burger ('here'): this site's pages lead, all of them, the way the drawer had them.++ allOpen = tab === 'sites'; hereOpen = tab === 'here'; leadHere = tab === 'here'; discoverPick = null;drainCards();el.classList.add('is-open');// Back closes the menu first (the owner, 11-07: pressing Back on a phone left the page instead): opening adds one@@@ -20,7 +20,7 @@ if (process.env.OV_SKIP_BROWSER === '1' || !h.findChrome()) {const PAGE = `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Wide</title><link rel="icon" href="data:,"><script src="/shared/navbar.js" defer></script></head><body style="margin:0"><div id="navbar-mount"></div><main><p id="wide" style="white-space:nowrap">https://www.example.com/a-very-long-link-that-nobody-wrapped-so-it-runs-far-past-the-right-edge-of-any-phone-screen?and=more&still=more</p></main>--<script>document.addEventListener('DOMContentLoaded', function () { OpenVibeNavbar.init({ service: 'site', user: { id: 7, username: 'probe', display_name: 'Probe' }, links: [{ label: 'Rooms', href: '/rooms' }, { label: 'Messages', href: '/messages' }, { label: 'Settings', href: '/settings' }], networkLinks: false, countViews: false, notifications: false, recent: false }); });</script>++<script>document.addEventListener('DOMContentLoaded', function () { OpenVibeNavbar.init({ service: 'site', user: { id: 7, username: 'probe', display_name: 'Probe' }, links: [{ label: 'Rooms', href: '/rooms' }, { label: 'Messages', href: '/messages' }, { label: 'Settings', href: '/settings' }], networkLinks: false, countViews: false, notifications: false, recent: false, drawer: 'legacy' }); });</script></body></html>`;const server = http.createServer((req, res) => {const u = new URL(req.url, 'http://x');@@@ -52,9 +52,14 @@ const server = http.createServer((req, res) => {// The navbar asks for the Frame at render; the launcher paints from what it kept.await p.evaluate(`new Promise((r) => { const t = setInterval(() => { if (localStorage.getItem('ov_frame_v2')) { clearInterval(t); r(true); } }, 20); })`);const wait = (ms) => new Promise((r) => setTimeout(r, ms));-- // The drawer's "All sites" opens the one menu with the list of every site open (4.128.0: one scroll, no tabs).++ // One menu (4.129.0, the owner 11-07): the burger opens the OpenVibe menu, this site's pages first, Display at its foot.await p.evaluate(`document.getElementById('openvibe-burger').click(), true`);-- await wait(150);++ await p.evaluate(`new Promise((r) => { const t = setInterval(() => { if (document.querySelector('.ovs.is-open .ovs-group')) { clearInterval(t); r(true); } }, 20); })`);++ const first = await p.evaluate(`({ head: document.querySelector('.ovs-list .ovs-group').firstChild.textContent, sec: document.querySelector('.ovs-list .ovs-group').getAttribute('data-sec'), display: !!document.querySelector('.ovs-displayrow .ovnav-display'), expanded: document.getElementById('openvibe-burger').getAttribute('aria-expanded') })`);++ assert.strictEqual(first.sec, 'here', 'from the burger, this site\'s pages lead: ' + first.head);++ assert.strictEqual(first.display, true, 'Display (theme, text size, motion) at the menu\'s foot');++ assert.strictEqual(first.expanded, 'true', 'the burger says the menu is open');++ // The drawer's "All sites" (kept for sites that ask for the legacy drawer) opens the list of every site.await p.evaluate(`document.querySelector('#openvibe-drawer [data-open-launcher]').click(), true`);await p.evaluate(`new Promise((r) => { const t = setInterval(() => { if (document.querySelector('.ovs.is-open .ovs-grid.is-sites[data-sec="all"]')) { clearInterval(t); r(true); } }, 20); })`);const read = () => p.evaluate(`(() => { const l = document.querySelector('.ovs-list'); return {
@@ -1,5 +1,16 @@
@@ -1,5 +1,16 @@
# Changelog
# Changelog
+## 4.129.0 — 2026-11-07
+
+- **One menu** (the owner, 11-07, on the right-hand drawer: "sloppy … maybe combining this menu and the other menu and just
+ putting our main button on the right-hand side"). The burger now opens the OpenVibe menu, grown from the burger:
+ - this site's pages lead, all of them (the drawer's job);
+ - then the live bar, your places and workspace, the network, What's new, tools and Jump back in;
+ - **Display** (Light/Dark/Auto, text size, Motion/Calm) sits at the foot, drawn by the same controls the drawer had.
+
+ The orb by the logo opens the same menu with the places first. The old drawer remains only for a site that turns the
+ menu off (`search: false`) or asks for it (`drawer: 'legacy'`). The burger's `aria-expanded` follows the menu.
+
## 4.128.0 — 2026-11-07
## 4.128.0 — 2026-11-07
- **The menu, rethought around real use** (the owner, 11-07: Jump back in came first, people opened Sites every time just to
- **The menu, rethought around real use** (the owner, 11-07: Jump back in came first, people opened Sites every time just to
@@ -2,7 +2,7 @@
@@ -2,7 +2,7 @@
> Versioned UI, the OpenVibe Frame (the navbar, footer and "shipped" views every site sits in), SEO, legal and release-client packages every OpenVibe site renders.
> Versioned UI, the OpenVibe Frame (the navbar, footer and "shipped" views every site sits in), SEO, legal and release-client packages every OpenVibe site renders.
-**Status:** alpha, v4.128.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed
+**Status:** alpha, v4.129.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed
consumer installs a tagged release and none keeps a vendored copy
consumer installs a tagged release and none keeps a vendored copy
([docs/migration-plan.md](docs/migration-plan.md) is done); each repository's `package.json` names
([docs/migration-plan.md](docs/migration-plan.md) is done); each repository's `package.json` names
the tag it pins.
the tag it pins.
@@ -4,7 +4,7 @@
@@ -4,7 +4,7 @@
"code": true,
"code": true,
"wave": "W2",
"wave": "W2",
"package": "openvibe-shared",
"package": "openvibe-shared",
- "version": "4.128.0",
+ "version": "4.129.0",
"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)",
@@ -21,6 +21,7 @@
@@ -21,6 +21,7 @@
"share cards for every site and for its big pages (openvibe-shared/social)",
"share cards for every site and for its big pages (openvibe-shared/social)",
"the shared video player (player.js, player.css): one look for every <video>",
"the shared video player (player.js, player.css): one look for every <video>",
"the player's AD control: a descriptions track is remembered per person, toggled with D and spoken through speech synthesis (4.126.0)",
"the player's AD control: a descriptions track is remembered per person, toggled with D and spoken through speech synthesis (4.126.0)",
+ "one menu: the burger opens the OpenVibe menu, this site's pages first and Display at its foot; the old drawer only for sites that ask (4.129.0)",
"the menu rethought around real use: who is live, your places dock, this site's pages, next from here, two places new to you, you and your agents, tools, every site behind a count, jump back in; a click beacon when Network asks (4.128.0)",
"the menu rethought around real use: who is live, your places dock, this site's pages, next from here, two places new to you, you and your agents, tools, every site behind a count, jump back in; a click beacon when Network asks (4.128.0)",
"items.js: what people wear from OpenVibe.Inventory (name effects, hats, particles, community badges), drawn the same on every site",
"items.js: what people wear from OpenVibe.Inventory (name effects, hats, particles, community badges), drawn the same on every site",
"shipped pill, recent list and /updates log",
"shipped pill, recent list and /updates log",
@@ -1052,7 +1052,14 @@
@@ -1052,7 +1052,14 @@
nav.addEventListener('pointerover', place); nav.addEventListener('focusin', place);
nav.addEventListener('pointerover', place); nav.addEventListener('focusin', place);
if (burger && drawer) {
if (burger && drawer) {
if (!_config.user) { bindDisplayControls(drawer.querySelector('.ovnav-display')); bindLook(drawer); } // guests: members have them in the account menu
if (!_config.user) { bindDisplayControls(drawer.querySelector('.ovnav-display')); bindLook(drawer); } // guests: members have them in the account menu
- burger.addEventListener('click', (e) => { e.stopPropagation(); const open = drawer.classList.toggle('open'); burger.setAttribute('aria-expanded', String(open)); if (open) loadOvIcons(drawer); });
+ // One menu (the owner, 11-07: "combining this menu and the other menu … the main button on the right-hand side"):
+ // the burger opens the OpenVibe menu, from the burger, leading with this site's pages. The old drawer stays for a
+ // site that turns the menu off (search: false) or asks for it (drawer: 'legacy').
+ burger.addEventListener('click', (e) => {
+ e.stopPropagation();
+ if (_config.search !== false && _config.drawer !== 'legacy') { openSearch('', 'here', burger); return; }
+ const open = drawer.classList.toggle('open'); burger.setAttribute('aria-expanded', String(open)); if (open) loadOvIcons(drawer);
+ });
drawer.addEventListener('click', (e) => {
drawer.addEventListener('click', (e) => {
const all = e.target.closest && e.target.closest('[data-open-launcher]'); if (!all) return;
const all = e.target.closest && e.target.closest('[data-open-launcher]'); if (!all) return;
e.preventDefault(); e.stopPropagation(); closeDrawer();
e.preventDefault(); e.stopPropagation(); closeDrawer();
@@ -2037,12 +2044,13 @@
@@ -2037,12 +2044,13 @@
// Search (search.js, loaded on first use). _config.search: false, or { owner, label, page }.
// Search (search.js, loaded on first use). _config.search: false, or { owner, label, page }.
let _searchLoading = false;
let _searchLoading = false;
/** The OpenVibe menu (search.js, loaded on first use): search, every site and every tool. tab: 'you' | 'sites' | 'tools'. */
/** The OpenVibe menu (search.js, loaded on first use): search, every site and every tool. tab: 'you' | 'sites' | 'tools'. */
- function openSearch(q, tab) {
+ function openSearch(q, tab, fromEl) {
const orb = _navEl && _navEl.querySelector('#openvibe-search-btn');
const orb = _navEl && _navEl.querySelector('#openvibe-search-btn');
- // The menu drops from the orb, under the bar (the bar stays, and the orb's × closes it).
+ // The menu drops from the button that opened it (the orb, or the burger), under the bar.
+ const from = fromEl || orb;
const anchor = () => {
const anchor = () => {
- if (!_navEl || !orb || !orb.getBoundingClientRect) return null;
- const bar = _navEl.getBoundingClientRect(), at = orb.getBoundingClientRect();
+ if (!_navEl || !from || !from.getBoundingClientRect) return null;
+ const bar = _navEl.getBoundingClientRect(), at = from.getBoundingClientRect();
return bar.bottom > 0 && bar.bottom < 200 ? { top: bar.bottom, left: at.left - 6, cx: at.left + at.width / 2, cy: at.top + at.height / 2 } : null;
return bar.bottom > 0 && bar.bottom < 200 ? { top: bar.bottom, left: at.left - 6, cx: at.left + at.width / 2, cy: at.top + at.height / 2 } : null;
};
};
const go = () => {
const go = () => {
@@ -2051,8 +2059,10 @@
@@ -2051,8 +2059,10 @@
if (_navEl) _navEl.classList.remove('ovnav-hide');
if (_navEl) _navEl.classList.remove('ovnav-hide');
root.OpenVibeSearch.open({ service: _config.service, scope: Object.assign({ owner: _config.service }, _config.search || {}), links: flatLinks(rawLinks()), q, tab,
root.OpenVibeSearch.open({ service: _config.service, scope: Object.assign({ owner: _config.service }, _config.search || {}), links: flatLinks(rawLinks()), q, tab,
frame: OVFrame.get(), token: _config.token, apiBase: _config.apiBase, user: _config.user, icons: (scope) => loadOvIcons(scope), anchor: anchor(), navIcon,
frame: OVFrame.get(), token: _config.token, apiBase: _config.apiBase, user: _config.user, icons: (scope) => loadOvIcons(scope), anchor: anchor(), navIcon,
- onClose: () => { const b = _navEl && _navEl.querySelector('#openvibe-search-btn'); if (b) b.setAttribute('aria-expanded', 'false'); } });
- const b = _navEl && _navEl.querySelector('#openvibe-search-btn'); if (b) b.setAttribute('aria-expanded', 'true');
+ // Display (theme, text size, motion) at the menu's foot, drawn by the same controls the drawer had.
+ display: (el) => { const box = document.createElement('div'); box.className = 'ovnav-display ovs-display'; el.appendChild(box); bindDisplayControls(box); },
+ onClose: () => { const b = _navEl && _navEl.querySelector('#openvibe-search-btn'); if (b) b.setAttribute('aria-expanded', 'false'); const g = _navEl && _navEl.querySelector('#openvibe-burger'); if (g) g.setAttribute('aria-expanded', 'false'); } });
+ const b = fromEl || (_navEl && _navEl.querySelector('#openvibe-search-btn')); if (b) b.setAttribute('aria-expanded', 'true');
};
};
if (root.OpenVibeSearch) return go();
if (root.OpenVibeSearch) return go();
if (_searchLoading) return;
if (_searchLoading) return;
@@ -1,6 +1,6 @@
@@ -1,6 +1,6 @@
{
{
"name": "openvibe-shared",
"name": "openvibe-shared",
- "version": "4.128.0",
+ "version": "4.129.0",
"description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site",
"description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site",
"main": "index.js",
"main": "index.js",
"bin": {
"bin": {
@@ -62,7 +62,7 @@
@@ -62,7 +62,7 @@
{ name: 'Developer Tools', icon: 'code', url: 'https://dev.openvibe.tools/' },
{ name: 'Developer Tools', icon: 'code', url: 'https://dev.openvibe.tools/' },
{ name: 'Network Tools', icon: 'dns', url: 'https://net.openvibe.tools/' },
{ name: 'Network Tools', icon: 'dns', url: 'https://net.openvibe.tools/' },
];
];
- const TABS = [['you', 'Start'], ['sites', 'Sites'], ['tools', 'Tools']];
+ const TABS = [['you', 'Start'], ['sites', 'Sites'], ['tools', 'Tools'], ['here', 'Here']];
let el = null, input = null, ghost = null, listEl = null, tabsEl = null, ctx = {}, items = [], active = -1, inflight = null, timer = null, lastFocus = null;
let el = null, input = null, ghost = null, listEl = null, tabsEl = null, ctx = {}, items = [], active = -1, inflight = null, timer = null, lastFocus = null;
let tab = 'you', mine = null, catalog = null, completion = '';
let tab = 'you', mine = null, catalog = null, completion = '';
// The results view (4.29.0): Enter shows the Search Fabric's answer here, by scope, instead of leaving the page.
// The results view (4.29.0): Enter shows the Search Fabric's answer here, by scope, instead of leaving the page.
@@ -238,6 +238,8 @@
@@ -238,6 +238,8 @@
.ovs-tag { margin-left: 7px; padding: 1px 7px; border-radius: 999px; font-style: normal; font-size: 10.5px; font-weight: 800; letter-spacing: .03em; vertical-align: 1px; color: color-mix(in srgb, var(--accent, #3b82f6) 72%, var(--text-primary, #e8ecf3)); background: color-mix(in srgb, var(--accent, #3b82f6) 15%, transparent); }
.ovs-tag { margin-left: 7px; padding: 1px 7px; border-radius: 999px; font-style: normal; font-size: 10.5px; font-weight: 800; letter-spacing: .03em; vertical-align: 1px; color: color-mix(in srgb, var(--accent, #3b82f6) 72%, var(--text-primary, #e8ecf3)); background: color-mix(in srgb, var(--accent, #3b82f6) 15%, transparent); }
.ovs-card { padding: 2px 6px 6px; }
.ovs-card { padding: 2px 6px 6px; }
.ovs-cardbody { border-radius: 16px; border: 1px solid var(--border, rgba(255,255,255,.12)); background: color-mix(in srgb, var(--text-primary, #e8ecf3) 4%, transparent); padding: 12px 14px; }
.ovs-cardbody { border-radius: 16px; border: 1px solid var(--border, rgba(255,255,255,.12)); background: color-mix(in srgb, var(--text-primary, #e8ecf3) 4%, transparent); padding: 12px 14px; }
+.ovs-displayrow { padding: 4px 8px 10px; }
+.ovs-displayrow .ovnav-display { margin-top: 4px; }
.ovs-host { display: block; margin-top: 2px; font-size: 11px; font-weight: 650; letter-spacing: .01em; color: color-mix(in srgb, var(--accent, #3b82f6) 60%, var(--text-muted, #93a0b4)); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ovs-host { display: block; margin-top: 2px; font-size: 11px; font-weight: 650; letter-spacing: .01em; color: color-mix(in srgb, var(--accent, #3b82f6) 60%, var(--text-muted, #93a0b4)); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ovs-group .ovs-more { margin-left: auto; color: var(--accent-light, #8ab4ff); font-size: 11.5px; font-weight: 650; text-transform: none; letter-spacing: 0; text-decoration: none; }
.ovs-group .ovs-more { margin-left: auto; color: var(--accent-light, #8ab4ff); font-size: 11.5px; font-weight: 650; text-transform: none; letter-spacing: 0; text-decoration: none; }
@media (hover: hover) { .ovs-app:hover, .ovs-link:hover, .ovs-live:hover, .ovs-fold:hover { background: var(--bg-hover, rgba(255,255,255,.07)); color: inherit; } .ovs-app:hover .ovs-appic { transform: translateY(-2px); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent, #3b82f6) 45%, transparent); } }
@media (hover: hover) { .ovs-app:hover, .ovs-link:hover, .ovs-live:hover, .ovs-fold:hover { background: var(--bg-hover, rgba(255,255,255,.07)); color: inherit; } .ovs-app:hover .ovs-appic { transform: translateY(-2px); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent, #3b82f6) 45%, transparent); } }
@@ -424,12 +426,18 @@
@@ -424,12 +426,18 @@
function paint(groups, note) {
function paint(groups, note) {
items = [];
items = [];
listEl.textContent = '';
listEl.textContent = '';
- groups.filter((g) => (g.items && g.items.length) || ['note', 'loading', 'route', 'fold', 'card'].includes(g.as)).forEach((g) => {
+ groups.filter((g) => (g.items && g.items.length) || ['note', 'loading', 'route', 'fold', 'card', 'display'].includes(g.as)).forEach((g) => {
if (g.as === 'route') {
if (g.as === 'route') {
const r = document.createElement('li'); r.className = 'ovs-route'; r.setAttribute('role', 'presentation'); r.textContent = g.text; listEl.appendChild(r);
const r = document.createElement('li'); r.className = 'ovs-route'; r.setAttribute('role', 'presentation'); r.textContent = g.text; listEl.appendChild(r);
return;
return;
}
}
if (g.as === 'fold') { listEl.appendChild(foldButton(g.fold, g.section)); return; }
if (g.as === 'fold') { listEl.appendChild(foldButton(g.fold, g.section)); return; }
+ if (g.as === 'display') {
+ const li = document.createElement('li'); li.className = 'ovs-displayrow'; li.setAttribute('role', 'presentation'); li.setAttribute('data-sec', 'display');
+ try { ctx.display(li); } catch { return; }
+ listEl.appendChild(li);
+ return;
+ }
if (g.as === 'card') {
if (g.as === 'card') {
const body = document.createElement('div'); body.className = 'ovs-cardbody';
const body = document.createElement('div'); body.className = 'ovs-cardbody';
try {
try {
@@ -627,7 +635,7 @@
@@ -627,7 +635,7 @@
live: 'Streams and calls', 'media-hub': 'Recordings and clips', community: 'Forum and pastes', chat: 'Rooms and messages',
live: 'Streams and calls', 'media-hub': 'Recordings and clips', community: 'Forum and pastes', chat: 'Rooms and messages',
tools: 'Free tools', bot: 'Robots and machines', vip: 'Back a streamer', pics: 'Pictures', download: 'Files and drive',
tools: 'Free tools', bot: 'Robots and machines', vip: 'Back a streamer', pics: 'Pictures', download: 'Files and drive',
};
};
- let allOpen = false, hereOpen = false, discoverPick = null, pulseData = null;
+ let allOpen = false, hereOpen = false, leadHere = false, discoverPick = null, pulseData = null;
/** Network's live line (GET /api/frame/pulse, kept 30 s): how many are live now, and who when the Frame sends it. */
/** Network's live line (GET /api/frame/pulse, kept 30 s): how many are live now, and who when the Frame sends it. */
function loadPulse() {
function loadPulse() {
try { const c = JSON.parse(sessionStorage.getItem('ov_pulse') || 'null'); if (c && Date.now() - c.at < 30000) { pulseData = c.data; return Promise.resolve(c.data); } } catch { /* */ }
try { const c = JSON.parse(sessionStorage.getItem('ov_pulse') || 'null'); if (c && Date.now() - c.at < 30000) { pulseData = c.data; return Promise.resolve(c.data); } } catch { /* */ }
@@ -753,7 +761,8 @@
@@ -753,7 +761,8 @@
const allPages = (ctx.links || []).filter((l) => l && l.label && typeof l.href === 'string' && !l.hidden).map((l) => ({ title: l.label, url: safeUrl(l.href), icon: 'go', drawn: l.icon && typeof ctx.navIcon === 'function' ? ctx.navIcon(l.icon) : '', kind: 'page' })).filter((x) => x.url);
const allPages = (ctx.links || []).filter((l) => l && l.label && typeof l.href === 'string' && !l.hidden).map((l) => ({ title: l.label, url: safeUrl(l.href), icon: 'go', drawn: l.icon && typeof ctx.navIcon === 'function' ? ctx.navIcon(l.icon) : '', kind: 'page' })).filter((x) => x.url);
if (allPages.length) {
if (allPages.length) {
const head = hereOpen ? allPages : allPages.slice(0, 6);
const head = hereOpen ? allPages : allPages.slice(0, 6);
- groups.push({ title: `On ${here()}`, as: 'links', items: head, section: 'here', fold: allPages.length > 6 ? { open: hereOpen, label: hereOpen ? 'Fewer' : `More on ${here()} (${allPages.length - 6})`, key: 'here' } : null });
+ const hereGroup = { title: `On ${here()}`, as: 'links', items: head, section: 'here', fold: allPages.length > 6 ? { open: hereOpen, label: hereOpen ? 'Fewer' : `More on ${here()} (${allPages.length - 6})`, key: 'here' } : null };
+ if (leadHere) groups.unshift(hereGroup); else groups.push(hereGroup);
}
}
// 4. Where people go next from here: the Frame's list, else the moves Network saw (11-07) and this site's group.
// 4. Where people go next from here: the Frame's list, else the moves Network saw (11-07) and this site's group.
const f = ctx.frame;
const f = ctx.frame;
@@ -811,6 +820,8 @@
@@ -811,6 +820,8 @@
const back = mine ? mine.items.filter((h) => { if (seen.has(h.url)) return false; seen.add(h.url); return true; }).slice(0, 5)
const back = mine ? mine.items.filter((h) => { if (seen.has(h.url)) return false; seen.add(h.url); return true; }).slice(0, 5)
.map((h) => ({ title: h.title, meta: [h.site, ago(h.at)].filter(Boolean).join(' · '), url: h.url, icon: 'back', kind: 'history' })) : [];
.map((h) => ({ title: h.title, meta: [h.site, ago(h.at)].filter(Boolean).join(' · '), url: h.url, icon: 'back', kind: 'history' })) : [];
groups.push({ title: 'Jump back in', items: back, section: 'back' });
groups.push({ title: 'Jump back in', items: back, section: 'back' });
+ // Display: theme, text size and motion, at the foot (the drawer's row, now in the one menu).
+ if (typeof ctx.display === 'function') groups.push({ title: '', as: 'display', items: [], section: 'display' });
paint(groups);
paint(groups);
}
}
@@ -975,7 +986,8 @@
@@ -975,7 +986,8 @@
lastFocus = document.activeElement;
lastFocus = document.activeElement;
tab = TABS.some(([id]) => id === opts.tab) ? opts.tab : 'you';
tab = TABS.some(([id]) => id === opts.tab) ? opts.tab : 'you';
// The old tabs, kept as a way in: "sites" opens the list of every site, "tools" scrolls to the tools.
// The old tabs, kept as a way in: "sites" opens the list of every site, "tools" scrolls to the tools.
- allOpen = tab === 'sites'; hereOpen = false; discoverPick = null;
+ // From the burger ('here'): this site's pages lead, all of them, the way the drawer had them.
+ allOpen = tab === 'sites'; hereOpen = tab === 'here'; leadHere = tab === 'here'; discoverPick = null;
drainCards();
drainCards();
el.classList.add('is-open');
el.classList.add('is-open');
// Back closes the menu first (the owner, 11-07: pressing Back on a phone left the page instead): opening adds one
// Back closes the menu first (the owner, 11-07: pressing Back on a phone left the page instead): opening adds one
@@ -20,7 +20,7 @@ if (process.env.OV_SKIP_BROWSER === '1' || !h.findChrome()) {
@@ -20,7 +20,7 @@ if (process.env.OV_SKIP_BROWSER === '1' || !h.findChrome()) {
const PAGE = `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Wide</title><link rel="icon" href="data:,">
const PAGE = `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Wide</title><link rel="icon" href="data:,">
<script src="/shared/navbar.js" defer></script></head>
<script src="/shared/navbar.js" defer></script></head>
<body style="margin:0"><div id="navbar-mount"></div><main><p id="wide" style="white-space:nowrap">https://www.example.com/a-very-long-link-that-nobody-wrapped-so-it-runs-far-past-the-right-edge-of-any-phone-screen?and=more&still=more</p></main>
<body style="margin:0"><div id="navbar-mount"></div><main><p id="wide" style="white-space:nowrap">https://www.example.com/a-very-long-link-that-nobody-wrapped-so-it-runs-far-past-the-right-edge-of-any-phone-screen?and=more&still=more</p></main>
-<script>document.addEventListener('DOMContentLoaded', function () { OpenVibeNavbar.init({ service: 'site', user: { id: 7, username: 'probe', display_name: 'Probe' }, links: [{ label: 'Rooms', href: '/rooms' }, { label: 'Messages', href: '/messages' }, { label: 'Settings', href: '/settings' }], networkLinks: false, countViews: false, notifications: false, recent: false }); });</script>
+<script>document.addEventListener('DOMContentLoaded', function () { OpenVibeNavbar.init({ service: 'site', user: { id: 7, username: 'probe', display_name: 'Probe' }, links: [{ label: 'Rooms', href: '/rooms' }, { label: 'Messages', href: '/messages' }, { label: 'Settings', href: '/settings' }], networkLinks: false, countViews: false, notifications: false, recent: false, drawer: 'legacy' }); });</script>
</body></html>`;
</body></html>`;
const server = http.createServer((req, res) => {
const server = http.createServer((req, res) => {
const u = new URL(req.url, 'http://x');
const u = new URL(req.url, 'http://x');
@@ -52,9 +52,14 @@ const server = http.createServer((req, res) => {
@@ -52,9 +52,14 @@ const server = http.createServer((req, res) => {
// The navbar asks for the Frame at render; the launcher paints from what it kept.
// The navbar asks for the Frame at render; the launcher paints from what it kept.
await p.evaluate(`new Promise((r) => { const t = setInterval(() => { if (localStorage.getItem('ov_frame_v2')) { clearInterval(t); r(true); } }, 20); })`);
await p.evaluate(`new Promise((r) => { const t = setInterval(() => { if (localStorage.getItem('ov_frame_v2')) { clearInterval(t); r(true); } }, 20); })`);
const wait = (ms) => new Promise((r) => setTimeout(r, ms));
const wait = (ms) => new Promise((r) => setTimeout(r, ms));
- // The drawer's "All sites" opens the one menu with the list of every site open (4.128.0: one scroll, no tabs).
+ // One menu (4.129.0, the owner 11-07): the burger opens the OpenVibe menu, this site's pages first, Display at its foot.
await p.evaluate(`document.getElementById('openvibe-burger').click(), true`);
await p.evaluate(`document.getElementById('openvibe-burger').click(), true`);
- await wait(150);
+ await p.evaluate(`new Promise((r) => { const t = setInterval(() => { if (document.querySelector('.ovs.is-open .ovs-group')) { clearInterval(t); r(true); } }, 20); })`);
+ const first = await p.evaluate(`({ head: document.querySelector('.ovs-list .ovs-group').firstChild.textContent, sec: document.querySelector('.ovs-list .ovs-group').getAttribute('data-sec'), display: !!document.querySelector('.ovs-displayrow .ovnav-display'), expanded: document.getElementById('openvibe-burger').getAttribute('aria-expanded') })`);
+ assert.strictEqual(first.sec, 'here', 'from the burger, this site\'s pages lead: ' + first.head);
+ assert.strictEqual(first.display, true, 'Display (theme, text size, motion) at the menu\'s foot');
+ assert.strictEqual(first.expanded, 'true', 'the burger says the menu is open');
+ // The drawer's "All sites" (kept for sites that ask for the legacy drawer) opens the list of every site.
await p.evaluate(`document.querySelector('#openvibe-drawer [data-open-launcher]').click(), true`);
await p.evaluate(`document.querySelector('#openvibe-drawer [data-open-launcher]').click(), true`);
await p.evaluate(`new Promise((r) => { const t = setInterval(() => { if (document.querySelector('.ovs.is-open .ovs-grid.is-sites[data-sec="all"]')) { clearInterval(t); r(true); } }, 20); })`);
await p.evaluate(`new Promise((r) => { const t = setInterval(() => { if (document.querySelector('.ovs.is-open .ovs-grid.is-sites[data-sec="all"]')) { clearInterval(t); r(true); } }, 20); })`);
const read = () => p.evaluate(`(() => { const l = document.querySelector('.ovs-list'); return {
const read = () => p.evaluate(`(() => { const l = document.querySelector('.ovs-list'); return {