openvibers/openvibe.shared Commits
4.188.0: the account is in the menu (no dropdown under the picture), and the mark is larger in the bar (#411)
Changes
@@@ -1,5 +1,23 @@# Changelog++## 4.188.0 — 2026-11-10++++**The account's things are in the menu.** The owner, 2026-11-10, with the dropdown under the picture marked "Replace →++to You tab": "this user drop down is just redundant … move it into the You tab of the menu … add account switching++and adding accounts to that … consolidate the two into one menu."++++- **The picture opens the OpenVibe menu on You.** The block the dropdown drew (who you are, add another account,++ settings, sign out, your other accounts, the site's own items from `addMenuItem`, light or dark and Display) is a++ block of that tab, at its top, where a shorter bar with the name was. It is there wherever the bar has a person, with++ or without a token for the network's reads.++- **No dropdown.** The popup, its position, its height rules and its place among the panels (`user-menu`) are gone.++ The block rests in the bar, out of sight, while the menu is closed: a site that looks for `#openvibe-logout-btn`,++ `[data-ov-ask]` or its own item still finds it.++- **The mark is larger in the bar** (42, was 34; 34 on a small phone, was 28). The owner: "make the OV branding take++ up most of its area … stand out a little more … compared to the My." Nothing overlaps from 320 px++ (`navbar-phone-fit-chrome`).++- In the You tab, "All 65" keeps its own case (it was drawn in the heading's capitals).++## 4.187.0 — 2026-11-10**The bar the owner chose.** 2026-11-10, in the Arena: "here's the new menu button and the logo … these two, but@@@ -5,7 +5,7 @@**Docs:** https://openvibe.services · **For AI:** https://openvibe.network/llms.txt**Install and test:** `npm ci && npm test`--**Status:** alpha, v4.187.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed++**Status:** alpha, v4.188.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.@@@ -831,6 +831,12 @@ One menu (4.187): the OpenVibe mark is a button that opens the OpenVibe menuon its first tab, as the menu button does (there is no separate siteswitcher). The site's name stays the link home; switcher: false keeps themark as a home link.++The account's things (4.188): the person's picture opens the OpenVibe menu++ on You, where the account's block is (who you are, add account, settings,++ sign out, your other accounts, this site's own items from addMenuItem,++ Display). There is no dropdown under the picture; the block rests in the bar,++ out of sight, while the menu is closed, so #openvibe-logout-btn and a++ site's own items are always in the page.The bar's order (4.187): balances, the bell, the menu button, then theperson's picture (or Sign In), last. New messages are counted on the bell(setMessages(count)); there is no Messages button.@@@ -4,7 +4,7 @@"code": true,"wave": "W2","package": "openvibe-shared",-- "version": "4.187.0",++ "version": "4.188.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)",@@@ -66,7 +66,7 @@.openvibe-navbar .openvibe-navbar-brand button.flame:focus-visible { outline: 2px solid var(--accent, #3b82f6); outline-offset: 1px; }[data-ov-scroll-focus]:focus-visible { outline: 2px solid var(--accent, #3b82f6); outline-offset: 2px; }.openvibe-navbar .openvibe-navbar-brand { gap: 6px; padding: 2px; border-radius: 12px; margin-right: 0; }-- .openvibe-navbar .openvibe-navbar-brand .flame { display: inline-grid; place-items: center; width: auto; min-width: 30px; height: 30px; padding: 0 4px; box-sizing: border-box; border-radius: 9px; transition: background .15s; }++ .openvibe-navbar .openvibe-navbar-brand .flame { display: inline-grid; place-items: center; width: auto; min-width: 30px; height: 34px; padding: 0 4px; box-sizing: border-box; border-radius: 9px; transition: background .15s; }.openvibe-navbar .openvibe-navbar-brand .flame:hover { background: var(--accent-glow, rgba(59,130,246,.14)); }.openvibe-navbar .openvibe-navbar-brand .name { align-items: center; gap: 0; font-family: 'OV Brand', Nunito, 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; font-size: 19px; letter-spacing: -.25px; }.openvibe-navbar .openvibe-navbar-brand .name a { padding: 3px 1px; border-radius: 6px; transition: color .15s, background .15s; }@@@ -207,7 +207,7 @@@media (max-width: 440px) { .ovnav-chip-mini { padding: 0 5px; gap: 3px; } .openvibe-navbar .ovnav-chip.ovnav-chip--all { font-size: 12px; } .ovnav-chip-mini .ovnav-ic { width: 13px; height: 13px; } }@media (max-width: 400px) { .openvibe-navbar .openvibe-navbar-right { gap: 1px; } .ovnav-chip-mini { padding: 0 3px; gap: 2px; } .openvibe-navbar .ovnav-chip.ovnav-chip--all { font-size: 11.5px; letter-spacing: -.2px; } .openvibe-navbar .openvibe-navbar-brand { gap: 4px; }.openvibe-navbar { padding-left: 8px; padding-right: 6px; gap: 2px; } .openvibe-navbar .ovnav-burger { width: 34px; } .openvibe-navbar .ovnav-featured-phone { width: 34px; height: 34px; } .openvibe-navbar .openvibe-navbar-avatar { width: 32px; height: 32px; box-sizing: border-box; }-- .openvibe-navbar .openvibe-navbar-brand .name { font-size: 16.5px; letter-spacing: -.25px; } .openvibe-navbar .openvibe-navbar-brand .flame { min-width: 28px; height: 28px; padding: 0 2px; } .openvibe-navbar .openvibe-navbar-brand .flame .ov-mark { width: 28px !important; } }++ .openvibe-navbar .openvibe-navbar-brand .name { font-size: 16.5px; letter-spacing: -.25px; } .openvibe-navbar .openvibe-navbar-brand .flame { min-width: 28px; height: 28px; padding: 0 2px; } .openvibe-navbar .openvibe-navbar-brand .flame .ov-mark { width: 34px !important; } }/* Under 350px the mark alone names the site, so both balances keep their icons (a number alone says nothing). */@media (max-width: 350px) { .openvibe-navbar .openvibe-navbar-brand .name { display: none; } }/* The site's name reads whole on a signed-in phone (the owner, 2026-11-02: "the label or the branding of the@@@ -379,26 +379,13 @@.openvibe-navbar .openvibe-navbar-login { padding: 0 13px; }}-- .openvibe-navbar-dropdown {-- position: absolute; top: 48px; right: 8px;-- width: min(320px, calc(100vw - 16px)); background: var(--bg-card, #22222c);-- border: 1px solid var(--border, #333340); border-radius: 10px;-- box-shadow: var(--shadow-lg, 0 8px 32px rgba(0,0,0,0.5));-- display: none; flex-direction: column; overflow: hidden;-- animation: openvibe-slide-down .2s ease;-- }-- .openvibe-navbar-dropdown.open { display: flex; }-- /* The panel scrolls as a whole; its sections keep their height. A flex column lets children shrink by-- default, which squeezed the accounts block into a sliver with its own scrollbar when the panel was-- height-limited (phones). */++ /* The account's block (4.188.0): not a popup. It rests in the bar out of sight and is a block of the menu's++ You tab while the menu is open: no position, no shadow, no height of its own. */++ .openvibe-navbar-dropdown { display: none; flex-direction: column; border: 1px solid var(--border, #333340); }.openvibe-navbar-dropdown > * { flex-shrink: 0; }-- /* Panels never leave the screen: dvh is the VISIBLE height on phones (100vh reaches behind the address-- bar), the panel scrolls inside itself, and it is never wider than the viewport. */-- .openvibe-navbar-dropdown {-- max-width: calc(100vw - 16px);-- max-height: calc(100vh - 64px); max-height: calc(100dvh - 64px - env(safe-area-inset-bottom, 0px));-- overflow-y: auto; overflow-x: hidden; overscroll-behavior: contain; -webkit-overflow-scrolling: touch; scrollbar-width: thin;-- }++ .ovh-account > .openvibe-navbar-dropdown.ovnav-account .openvibe-navbar-dropdown-header .ud-hbtn { min-width: 44px; min-height: 44px; }++ .ovh-account > .openvibe-navbar-dropdown.ovnav-account { display: flex; overflow: hidden; margin: 2px 4px 10px; border-radius: 16px;++ background: color-mix(in srgb, var(--text-primary, #e6edf7) 4%, transparent); border-color: color-mix(in srgb, var(--text-primary, #e6edf7) 10%, transparent); }@keyframes openvibe-slide-down { from { transform: translateY(-8px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }.ov-pop { transform-origin: var(--ov-pop-x, 50%) var(--ov-pop-y, 0); animation: ov-pop .34s cubic-bezier(.2, 1.1, .3, 1) both !important; }.ov-pop-out { transform-origin: var(--ov-pop-x, 50%) var(--ov-pop-y, 0); animation: ov-pop-out .16s ease-in both !important; pointer-events: none; }@@@ -571,7 +558,7 @@const NAV_ICONS_REV = '148a0eda9ca8'; // sha256(nav-icons.js), first 12 hex: its ?v= (cacheable forever)// END generated// The files loaded from beside this one, by content (scripts/sibling-revs.js): a release changes their URLs.-- const SIBLING_REV = { 'ov-mark.js': '01cf79e36af5', 'ov-glyphs.js': '97224763226b', 'ov-brand.woff2': '3b4e6a8a5878', 'ov-look.js': '7ef00feeb07c', 'ov-fx.js': 'bb5afd571c72', 'ov-labels.js': 'd40f7ca14bc4', 'ov-forms.js': '2eb9e2604c3e', 'ov-forms.css': '06208fe89eac', 'ov-share.js': 'd419508a797e', 'ov-share.css': 'd609a71f611a', 'search.js': 'bb502f67c4d5', 'dock.js': '00e20b4b2895', 'presence.js': '5f195d41f8c5', 'panels.js': 'd90bfe987d81', 'surfaces.js': 'e72f5a304f2d', 'notification-ui.js': 'b64ea0c39d34', 'history.js': '1e6598f78ba4', 'appbar.js': '4e34b7d60372', 'release-watch.js': '25f02a3f0684', 'ov-icons.js': '8e5047755456', 'sso-client.js': '52439265b980', 'overlay.js': '336b30eae325', 'ov-home.js': 'db1d79b7b932', 'nav-slots.js': '1db19edaeac4', 'ov-wallet.js': '5c4d7fdb448d', 'ov-wallet.css': 'e46f653fa59e' };++ const SIBLING_REV = { 'ov-mark.js': '01cf79e36af5', 'ov-glyphs.js': '97224763226b', 'ov-brand.woff2': '3b4e6a8a5878', 'ov-look.js': '7ef00feeb07c', 'ov-fx.js': 'bb5afd571c72', 'ov-labels.js': 'd40f7ca14bc4', 'ov-forms.js': '2eb9e2604c3e', 'ov-forms.css': '06208fe89eac', 'ov-share.js': 'd419508a797e', 'ov-share.css': 'd609a71f611a', 'search.js': '9c0b9621ca9f', 'dock.js': '00e20b4b2895', 'presence.js': '5f195d41f8c5', 'panels.js': 'd90bfe987d81', 'surfaces.js': 'e72f5a304f2d', 'notification-ui.js': 'b64ea0c39d34', 'history.js': '1e6598f78ba4', 'appbar.js': '4e34b7d60372', 'release-watch.js': '25f02a3f0684', 'ov-icons.js': '8e5047755456', 'sso-client.js': '52439265b980', 'overlay.js': '336b30eae325', 'ov-home.js': '135d730d03fd', 'nav-slots.js': '1db19edaeac4', 'ov-wallet.js': '5c4d7fdb448d', 'ov-wallet.css': 'e46f653fa59e' };// The navbar's icons are filled paths (no Font Awesome CSS needed): the ones above ship here, the rest// (nav-icons.js) load after first paint when a link, chip or menu row names one, into an empty <svg> of their// size. Anything unknown, or nav-icons.js failing, falls back to <i class="fa-solid">.@@@ -927,7 +914,7 @@: seg('b-core', brand.core, to.core, brand.name) + join + seg('b-tld', brand.tldText, to.tld, brand.name);const mark = brand.icon? `<i class="fa-solid ${escapeAttr(brand.icon)}"></i>`-- : `<span class="ov-mark" data-size="34" data-wide data-variant="${escapeAttr(brand.variant)}"></span>`;++ : `<span class="ov-mark" data-size="42" data-wide data-variant="${escapeAttr(brand.variant)}"></span>`; // 42 since 4.188.0 (was 34: beside a short name it read as smaller than the word)return `<div class="openvibe-navbar-brand${brand.subText ? ' has-sub' : ''}${brand.joined ? ' joined' : ''}">${_config.switcher === false? `<a class="flame" href="${escapeAttr(brand.href)}" aria-label="${escapeAttr(brand.name)} home">${mark}</a>`@@@ -1073,6 +1060,7 @@/** The chips folded into one (shown instead of them on a phone): each chip's icon and short value. */// New messages are counted on the bell (4.187.0; the owner, 2026-11-10: "instead of having messaging and// notifications, just have the notifications … it'll show you new messages as well"): there is no Messages button.++ let _accountEl = null; // the account's block, built at render, hosted by the menu's You tablet _msgCount = 0;// New messages, for the bell's count: Chat's GET /api/dm/unread, at most once a minute (README, "The navbar").let _unreadAt = 0;@@@ -2198,26 +2186,30 @@</div>`;dropdown.addEventListener('click', (e) => { const t = e.target.closest && e.target.closest('[data-use]'); if (t) countUse(t.getAttribute('data-use')); });++ // 4.188.0: not a popup under the picture. It rests in the bar, out of sight (so a site that looks for its++ // sign-out button or its own item still finds it), and the menu's You tab hosts it while the menu is open++ // (openSearch's account(); onClose brings it back).++ dropdown.classList.add('ovnav-account');nav.appendChild(dropdown);++ _accountEl = dropdown;bindMenuItems(dropdown, before.concat(after, sectionItems(menuCfg.sections)));dropdown.addEventListener('click', (e) => { if (!e.target.closest || !e.target.closest('[data-ov-dock]')) return; dropdown.classList.remove('open'); openDock(); });bindChips(dropdown);if (typeof _config.onNavigate === 'function') dropdown.addEventListener('click', (e) => { const a = e.target.closest && e.target.closest('a[href]'); if (!a || e.defaultPrevented || a.target === '_blank' || e.metaKey || e.ctrlKey) return; const path = sitePath(a); if (path != null) { if (_config.onNavigate(path, e) === false) e.preventDefault(); dropdown.classList.remove('open'); } });bindDisplayRows(dropdown);bindLook(dropdown);-- regPanel(dropdown, 'user-menu', () => dropdown.classList.remove('open'), nav.querySelector('#openvibe-avatar-btn'));-- // Avatar click toggles dropdown (the selection for Ask Vibe is read on pointerdown: the press can clear it)++ // The picture opens the menu on You, where the account's things are (the selection for Ask Vibe is read on++ // pointerdown: the press can clear it).const avatarBtn = nav.querySelector('#openvibe-avatar-btn'); let sel = null;avatarBtn.addEventListener('pointerdown', () => { sel = selectedText(); });avatarBtn.addEventListener('click', () => {-- if (dropdown.classList.contains('open') && !dropdown.classList.contains('ov-pop-out')) { unpop(dropdown, () => { if (!dropdown.classList.contains('ov-pop')) dropdown.classList.remove('open'); }); return; }-- dropdown.classList.add('open'); popFrom(dropdown, avatarBtn);const ask = dropdown.querySelector('[data-ov-ask]'); if (ask) ask.href = askVibeHref(sel ?? selectedText()); sel = null;-- if (dropdown.classList.contains('open')) { loadWallet(dropdown); dropdown.scrollTop = 0; loadOvIcons(dropdown); }++ if (menuOpen() && root.OpenVibeSearch) { root.OpenVibeSearch.close(); return; }++ const orbs = nav.querySelectorAll ? [...nav.querySelectorAll('#openvibe-burger.ovnav-orb, #openvibe-search-btn')] : [];++ openSearch('', 'you', orbs.find((o) => o.getBoundingClientRect && o.getBoundingClientRect().width > 0) || null);});-- // panels.js closes this on an outside pointerdown, including another control in the navbar.dropdown.querySelectorAll('[data-account-id]').forEach(el => {el.addEventListener('click', () => {@@@ -2317,7 +2309,9 @@// 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); },look: bindLook,-- 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'); } });++ // The account's own block (who you are, your accounts, sign out, this site's items): the You tab hosts it.++ account: (host) => { if (!_accountEl || !host) return false; host.appendChild(_accountEl); loadWallet(_accountEl); loadOvIcons(_accountEl); return true; },++ onClose: () => { if (_accountEl && _navEl && _accountEl.parentNode !== _navEl) _navEl.appendChild(_accountEl); 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();@@@ -114,7 +114,7 @@ html[data-ov-motion=reduced] .ovh.is-edit .ovh-grid:not(.ovh-recent) .ovh-ic { a.ovh-calm { transition: opacity .2s; }.ovh.is-edit .ovh-calm { opacity: .38; pointer-events: none; }.ovh-headtools { display: inline-flex; align-items: center; gap: 6px; margin-left: auto; }--.ovh-sec .ovh-allbtn { all: unset; box-sizing: border-box; display: inline-flex; align-items: center; gap: 6px; min-height: 36px; padding: 0 11px 0 10px; border-radius: 999px; cursor: pointer; font-size: 13px; font-weight: 700;++.ovh-sec .ovh-allbtn { all: unset; box-sizing: border-box; text-transform: none; letter-spacing: 0; display: inline-flex; align-items: center; gap: 6px; min-height: 36px; padding: 0 11px 0 10px; border-radius: 999px; cursor: pointer; font-size: 13px; font-weight: 700;color: var(--text-secondary, #a8b3c4); background: color-mix(in srgb, var(--text-primary, #e8ecf3) 7%, transparent); transition: color .15s, background-color .15s, transform .12s; }.ovh-sec .ovh-allbtn svg { width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }.ovh-sec .ovh-allbtn b { font-weight: 800; font-variant-numeric: tabular-nums; color: var(--text-primary, #e8ecf3); }@@@ -409,6 +409,16 @@ html[data-ov-motion=reduced] .ovh-card { animation-name: ovh-fade; animation-dur/** You: your picture (online), your name, and Display and Settings beside it. Made once per opening, so an open* Look panel survives the repaints that come as history and the catalogue arrive. */++ // The account's block (navbar.js builds it: who you are, add account, settings, sign out, your other accounts,++ // this site's own items): the You tab is where it lives since 4.188.0. Without a bar to give it, the short bar below.++ let accountBox = null;++ function account() {++ if (typeof o.account !== 'function') return null;++ accountBox = accountBox || h('div', 'ovh-account');++ let ok = false;++ try { ok = o.account(accountBox) !== false; } catch { ok = false; }++ return ok ? accountBox : null;++ }function me() {if (meBox) return meBox;const u = o.user || {};@@@ -496,7 +506,9 @@ html[data-ov-motion=reduced] .ovh-card { animation-name: ovh-fade; animation-dur// nothing above the grid may come or go (the macOS run of #394 dropped onto the wrong place when the hint// pushed the grid down mid-drag). The cards above stay, quiet and inert; the hint goes under the grid.const calm = (x) => { x.classList.add('ovh-calm'); x.inert = state.edit; return x; };-- if (signedIn(o)) el.append(calm(me()));++ // The account's block wherever the bar has a person (a site with its own session may have no token for++ // the network's reads yet: sign out and the rest must still be there); else the short bar.++ { const a = account(); if (a) el.append(calm(a)); else if (signedIn(o)) el.append(calm(me())); }if (signedIn(o)) { actorBox = actorBox || h('div'); el.append(actorBox); }const head = sec(L.arranged || signedIn(o) ? 'Your home' : 'Popular on OpenVibe');const edit = h('button', state.edit ? 'ovh-edit is-done' : 'ovh-edit');@@@ -1,12 +1,12 @@{"name": "openvibe-shared",-- "version": "4.187.0",++ "version": "4.188.0","lockfileVersion": 3,"requires": true,"packages": {"": {"name": "openvibe-shared",-- "version": "4.187.0",++ "version": "4.188.0","license": "MIT","dependencies": {"jsonwebtoken": "^9.0.0",@@@ -1,6 +1,6 @@{"name": "openvibe-shared",-- "version": "4.187.0",++ "version": "4.188.0","description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site","main": "index.js","bin": {@@@ -1236,7 +1236,7 @@const back = mine ? mine.items.filter((x) => { if (seenB.has(x.url)) return false; seenB.add(x.url); return true; }).slice(0, 8).map(backRow) : [];const usage = mine ? mine.services.filter((x) => x && x.count > 0).map((x) => ({ id: x.service === 'media' ? 'media-hub' : x.service, count: Number(x.count) || 0 })) : [];return { apps, yours, tools, toolCount: catalog ? catalog.tools.length : 0, recent: recentIds, back, usage, groups: f && Array.isArray(f.groups) ? f.groups : [],-- token: ctx.token, apiBase: ctx.apiBase, user: ctx.user, look: ctx.look, mountIcons: (scope) => { if (typeof ctx.icons === 'function') ctx.icons(scope); }, close,++ token: ctx.token, apiBase: ctx.apiBase, user: ctx.user, look: ctx.look, account: ctx.account, mountIcons: (scope) => { if (typeof ctx.icons === 'function') ctx.icons(scope); }, close,pulseReady: pulseReady || (pulseReady = root.OpenVibeHome.loadPulse({ token: ctx.token, user: ctx.user })),// The feed answered: the Feed tab is the stream (its sections would sit after an endless list); it did not: as before.feedLive: () => { if (!feedV2) { feedV2 = true; if (el && el.classList.contains('is-open') && !input.value && tab === 'feed') render(''); } } };@@@ -38,7 +38,7 @@ assert.strictEqual(files.path('ov-brand.woff2'), path.join(__dirname, '..', 'ov-assert.ok(/function brandFace\(\)[\s\S]*ov-brand\.woff2/.test(cards) && /\.name\{[^}]*font-family:'OV Brand'/.test(cards), 'the share cards, with the face embedded');// The mark sits beside the name in the wide box: level with the words, not a square with space above and below.-- assert.match(nav, /<span class="ov-mark" data-size="34" data-wide data-variant=/, 'navbar: the wide mark');++ assert.match(nav, /<span class="ov-mark" data-size="42" data-wide data-variant=/, 'navbar: the wide mark');assert.match(read('footer.js'), /<span class="ov-mark" data-size="38" data-wide><\/span>/, 'footer: the wide mark');console.log('brand face: served as a font with CORS, declared by the navbar, used for the names with a fallback; the wide mark beside them');})().catch((e) => { console.error(e); process.exit(1); });@@@ -180,8 +180,10 @@ const server = http.createServer((req, res) => {assert.ok(await until(s, `!!document.querySelector('.ovh-actor')`), 'the Actor strip is on top');assert.match(await s.evaluate(`document.querySelector('.ovh-actor').textContent`), /Juniper.*Working.*Reading job posts/, 'it names the Actor, its state and its step');// You on top: the name, Display (the Look panel) and Settings beside it; then your home, Most used and Jump back in.-- assert.strictEqual(await s.evaluate(`document.querySelector('.ovh-me .ovh-who-txt b').textContent`), 'Ada', 'your name on top');-- assert.ok(await s.evaluate(`!!document.querySelector('.ovh-me [data-ov-look-open]') && !!document.querySelector('.ovh-me a[href="https://openvibe.network/my"]')`), 'Display and Settings beside it');++ // 4.188.0: the account's block is here (it was a popup under the picture): who you are, add account, settings, sign out.++ assert.strictEqual(await s.evaluate(`document.querySelector('.ovh-account .openvibe-navbar-dropdown-header .name').textContent`), 'Ada', 'your name on top');++ assert.ok(await s.evaluate(`!!document.querySelector('.ovh-account #openvibe-logout-btn') && !document.querySelector('.ovh-me')`), 'with Sign out, and no second bar with your name');++ assert.ok(await s.evaluate(`!!document.querySelector('.ovh-account [data-ov-look-open]') && !!document.querySelector('.ovh-account a[href="https://openvibe.network/my"]')`), 'Display and Settings in it');assert.ok(await s.evaluate(`!document.querySelector('.ovs-displayrow')`), 'Display is not at the foot any more');assert.ok(await until(s, `document.querySelectorAll('.ovh-rail .ovh-card').length === 2`), 'Jump back in shows the places opened lately');assert.deepStrictEqual(await s.evaluate(`[...document.querySelectorAll('.ovh-rail .ovh-card')].map((a) => a.getAttribute('href'))`), ['https://openvibe.live/ada', 'https://openvibe.pics/1']);@@@ -191,8 +193,9 @@ const server = http.createServer((req, res) => {// to whatever the user uses the most". Your home comes first, then what you use most, then where you were last;// the bar with your name is short; each most-used app is a way in (its icon, a full touch target), not a legend.assert.deepStrictEqual(await s.evaluate(`[...document.querySelectorAll('.ovh-sec > span')].map((x) => x.textContent)`), ['Your home', 'Most used', 'Jump back in'], 'your home, then most used, then where you were');-- assert.ok(await s.evaluate(`document.querySelector('.ovh-me').getBoundingClientRect().height <= 56`), 'the bar with your name is short');-- assert.ok(await s.evaluate(`[...document.querySelectorAll('.ovh-me .ovh-ib')].every((b) => b.getBoundingClientRect().height >= 44)`), 'and its two buttons are still full touch targets');++ assert.ok(await s.evaluate(`document.querySelector('.ovh-account .openvibe-navbar-dropdown-header').getBoundingClientRect().height <= 96`), 'the row with your name is short');++ assert.ok(await s.evaluate(`[...document.querySelectorAll('.ovh-account .ud-hbtn')].every((b) => { const r = b.getBoundingClientRect(); return r.height >= 44 && r.width >= 44; })`), 'and its buttons are full touch targets');++ assert.strictEqual(await s.evaluate(`getComputedStyle(document.querySelector('.ovh-account > .openvibe-navbar-dropdown')).position`), 'static', 'a block of the page, not a popup');assert.deepStrictEqual(await s.evaluate(`[...document.querySelectorAll('.ovh-use .ovh-key')].map((k) => [k.getAttribute('href'), k.getAttribute('aria-label'), !!k.querySelector('.ovh-ic > *')].join('|'))`),['https://openvibe.live/|Live, opened 5 times|true', 'https://openvibe.pics/|Pics, opened 2 times|true'], 'each most-used app is a link with its icon and says how often');assert.ok(await s.evaluate(`[...document.querySelectorAll('.ovh-use .ovh-key')].every((k) => { const r = k.getBoundingClientRect(); return r.height >= 44 && r.width >= 44; })`), 'each a full touch target');@@@ -88,7 +88,10 @@ const server = http.createServer((req, res) => {const u = new URL(await askHref());assert.strictEqual(u.searchParams.get('ask'), 'The rover drives to the door.');assert.strictEqual(u.searchParams.get('from'), null, 'an http test page is not named');-- assert.strictEqual(await p.evaluate(`document.querySelector('.openvibe-navbar-dropdown').classList.contains('open')`), true, 'the menu opened');++ // 4.188.0: the picture opens the menu on You, and the account's block (with Ask Vibe in it) is there.++ const until = async (expr, ms = 6000) => { const end = Date.now() + ms; while (Date.now() < end) { if (await p.evaluate(expr)) return true; await new Promise((r) => setTimeout(r, 80)); } return false; };++ assert.ok(await until(`!!document.querySelector('.ovs.is-open .ovh-account > .openvibe-navbar-dropdown [data-ov-ask]')`), "the picture opened the menu's You tab, with the account's things in it");++ assert.strictEqual(await p.evaluate(`getComputedStyle(document.querySelector('.ovh-account > .openvibe-navbar-dropdown')).position`), 'static', 'as a block of that page, not a popup');console.log('navbar-ask-vibe: browser ok');} finally {await browser.close();@@@ -192,7 +192,7 @@ function load(hostname, opts = {}) {global.addEventListener = (type, fn, opts) => winListeners.push({ type, opts });navbar.init({ service: 'space', user: { id: 1, username: 'x' } });const first = regs.slice(), firstDisplay = winListeners.filter((l) => l.type === 'ov:display');-- assert.deepStrictEqual(first.map((r) => r.id).sort(), ['nav-drawer', 'user-menu'], 'the drawer and the account menu are registered');++ assert.deepStrictEqual(first.map((r) => r.id).sort(), ['nav-drawer'], 'the drawer is registered (the account\'s things are in the menu since 4.188.0: no popup of their own)');assert.ok(first.every((r) => r.signal && !r.signal.aborted), 'each registration carries the render\'s signal');assert.ok(firstDisplay.length === 1 && firstDisplay[0].opts && firstDisplay[0].opts.signal, 'the display rows\' window listener carries it too');navbar.init({ service: 'space', user: { id: 1, username: 'x' } });@@@ -211,7 +211,7 @@ function load(hostname, opts = {}) {global.OpenVibePanels = { register: (o) => { regs.push(o); } };loaders[loaders.length - 1].onload();delete global.OpenVibePanels;-- assert.deepStrictEqual(regs.map((r) => r.id).sort(), ['nav-drawer', 'user-menu'], 'only the live render\'s panels are registered');++ assert.deepStrictEqual(regs.map((r) => r.id).sort(), ['nav-drawer'], 'only the live render\'s panels are registered');assert.ok(regs.every((r) => !r.signal.aborted));}// ── balance chips fold into one on a phone (owner, 2026-10-27): the folded chip, its short values, and setChip ──
@@ -1,5 +1,23 @@
@@ -1,5 +1,23 @@
# Changelog
# Changelog
+## 4.188.0 — 2026-11-10
+
+**The account's things are in the menu.** The owner, 2026-11-10, with the dropdown under the picture marked "Replace →
+to You tab": "this user drop down is just redundant … move it into the You tab of the menu … add account switching
+and adding accounts to that … consolidate the two into one menu."
+
+- **The picture opens the OpenVibe menu on You.** The block the dropdown drew (who you are, add another account,
+ settings, sign out, your other accounts, the site's own items from `addMenuItem`, light or dark and Display) is a
+ block of that tab, at its top, where a shorter bar with the name was. It is there wherever the bar has a person, with
+ or without a token for the network's reads.
+- **No dropdown.** The popup, its position, its height rules and its place among the panels (`user-menu`) are gone.
+ The block rests in the bar, out of sight, while the menu is closed: a site that looks for `#openvibe-logout-btn`,
+ `[data-ov-ask]` or its own item still finds it.
+- **The mark is larger in the bar** (42, was 34; 34 on a small phone, was 28). The owner: "make the OV branding take
+ up most of its area … stand out a little more … compared to the My." Nothing overlaps from 320 px
+ (`navbar-phone-fit-chrome`).
+- In the You tab, "All 65" keeps its own case (it was drawn in the heading's capitals).
+
## 4.187.0 — 2026-11-10
## 4.187.0 — 2026-11-10
**The bar the owner chose.** 2026-11-10, in the Arena: "here's the new menu button and the logo … these two, but
**The bar the owner chose.** 2026-11-10, in the Arena: "here's the new menu button and the logo … these two, but
@@ -5,7 +5,7 @@
@@ -5,7 +5,7 @@
**Docs:** https://openvibe.services · **For AI:** https://openvibe.network/llms.txt
**Docs:** https://openvibe.services · **For AI:** https://openvibe.network/llms.txt
**Install and test:** `npm ci && npm test`
**Install and test:** `npm ci && npm test`
-**Status:** alpha, v4.187.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed
+**Status:** alpha, v4.188.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.
@@ -831,6 +831,12 @@ One menu (4.187): the OpenVibe mark is a button that opens the OpenVibe menu
@@ -831,6 +831,12 @@ One menu (4.187): the OpenVibe mark is a button that opens the OpenVibe menu
on its first tab, as the menu button does (there is no separate site
on its first tab, as the menu button does (there is no separate site
switcher). The site's name stays the link home; switcher: false keeps the
switcher). The site's name stays the link home; switcher: false keeps the
mark as a home link.
mark as a home link.
+The account's things (4.188): the person's picture opens the OpenVibe menu
+ on You, where the account's block is (who you are, add account, settings,
+ sign out, your other accounts, this site's own items from addMenuItem,
+ Display). There is no dropdown under the picture; the block rests in the bar,
+ out of sight, while the menu is closed, so #openvibe-logout-btn and a
+ site's own items are always in the page.
The bar's order (4.187): balances, the bell, the menu button, then the
The bar's order (4.187): balances, the bell, the menu button, then the
person's picture (or Sign In), last. New messages are counted on the bell
person's picture (or Sign In), last. New messages are counted on the bell
(setMessages(count)); there is no Messages button.
(setMessages(count)); there is no Messages button.
@@ -4,7 +4,7 @@
@@ -4,7 +4,7 @@
"code": true,
"code": true,
"wave": "W2",
"wave": "W2",
"package": "openvibe-shared",
"package": "openvibe-shared",
- "version": "4.187.0",
+ "version": "4.188.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)",
@@ -66,7 +66,7 @@
@@ -66,7 +66,7 @@
.openvibe-navbar .openvibe-navbar-brand button.flame:focus-visible { outline: 2px solid var(--accent, #3b82f6); outline-offset: 1px; }
.openvibe-navbar .openvibe-navbar-brand button.flame:focus-visible { outline: 2px solid var(--accent, #3b82f6); outline-offset: 1px; }
[data-ov-scroll-focus]:focus-visible { outline: 2px solid var(--accent, #3b82f6); outline-offset: 2px; }
[data-ov-scroll-focus]:focus-visible { outline: 2px solid var(--accent, #3b82f6); outline-offset: 2px; }
.openvibe-navbar .openvibe-navbar-brand { gap: 6px; padding: 2px; border-radius: 12px; margin-right: 0; }
.openvibe-navbar .openvibe-navbar-brand { gap: 6px; padding: 2px; border-radius: 12px; margin-right: 0; }
- .openvibe-navbar .openvibe-navbar-brand .flame { display: inline-grid; place-items: center; width: auto; min-width: 30px; height: 30px; padding: 0 4px; box-sizing: border-box; border-radius: 9px; transition: background .15s; }
+ .openvibe-navbar .openvibe-navbar-brand .flame { display: inline-grid; place-items: center; width: auto; min-width: 30px; height: 34px; padding: 0 4px; box-sizing: border-box; border-radius: 9px; transition: background .15s; }
.openvibe-navbar .openvibe-navbar-brand .flame:hover { background: var(--accent-glow, rgba(59,130,246,.14)); }
.openvibe-navbar .openvibe-navbar-brand .flame:hover { background: var(--accent-glow, rgba(59,130,246,.14)); }
.openvibe-navbar .openvibe-navbar-brand .name { align-items: center; gap: 0; font-family: 'OV Brand', Nunito, 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; font-size: 19px; letter-spacing: -.25px; }
.openvibe-navbar .openvibe-navbar-brand .name { align-items: center; gap: 0; font-family: 'OV Brand', Nunito, 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; font-size: 19px; letter-spacing: -.25px; }
.openvibe-navbar .openvibe-navbar-brand .name a { padding: 3px 1px; border-radius: 6px; transition: color .15s, background .15s; }
.openvibe-navbar .openvibe-navbar-brand .name a { padding: 3px 1px; border-radius: 6px; transition: color .15s, background .15s; }
@@ -207,7 +207,7 @@
@@ -207,7 +207,7 @@
@media (max-width: 440px) { .ovnav-chip-mini { padding: 0 5px; gap: 3px; } .openvibe-navbar .ovnav-chip.ovnav-chip--all { font-size: 12px; } .ovnav-chip-mini .ovnav-ic { width: 13px; height: 13px; } }
@media (max-width: 440px) { .ovnav-chip-mini { padding: 0 5px; gap: 3px; } .openvibe-navbar .ovnav-chip.ovnav-chip--all { font-size: 12px; } .ovnav-chip-mini .ovnav-ic { width: 13px; height: 13px; } }
@media (max-width: 400px) { .openvibe-navbar .openvibe-navbar-right { gap: 1px; } .ovnav-chip-mini { padding: 0 3px; gap: 2px; } .openvibe-navbar .ovnav-chip.ovnav-chip--all { font-size: 11.5px; letter-spacing: -.2px; } .openvibe-navbar .openvibe-navbar-brand { gap: 4px; }
@media (max-width: 400px) { .openvibe-navbar .openvibe-navbar-right { gap: 1px; } .ovnav-chip-mini { padding: 0 3px; gap: 2px; } .openvibe-navbar .ovnav-chip.ovnav-chip--all { font-size: 11.5px; letter-spacing: -.2px; } .openvibe-navbar .openvibe-navbar-brand { gap: 4px; }
.openvibe-navbar { padding-left: 8px; padding-right: 6px; gap: 2px; } .openvibe-navbar .ovnav-burger { width: 34px; } .openvibe-navbar .ovnav-featured-phone { width: 34px; height: 34px; } .openvibe-navbar .openvibe-navbar-avatar { width: 32px; height: 32px; box-sizing: border-box; }
.openvibe-navbar { padding-left: 8px; padding-right: 6px; gap: 2px; } .openvibe-navbar .ovnav-burger { width: 34px; } .openvibe-navbar .ovnav-featured-phone { width: 34px; height: 34px; } .openvibe-navbar .openvibe-navbar-avatar { width: 32px; height: 32px; box-sizing: border-box; }
- .openvibe-navbar .openvibe-navbar-brand .name { font-size: 16.5px; letter-spacing: -.25px; } .openvibe-navbar .openvibe-navbar-brand .flame { min-width: 28px; height: 28px; padding: 0 2px; } .openvibe-navbar .openvibe-navbar-brand .flame .ov-mark { width: 28px !important; } }
+ .openvibe-navbar .openvibe-navbar-brand .name { font-size: 16.5px; letter-spacing: -.25px; } .openvibe-navbar .openvibe-navbar-brand .flame { min-width: 28px; height: 28px; padding: 0 2px; } .openvibe-navbar .openvibe-navbar-brand .flame .ov-mark { width: 34px !important; } }
/* Under 350px the mark alone names the site, so both balances keep their icons (a number alone says nothing). */
/* Under 350px the mark alone names the site, so both balances keep their icons (a number alone says nothing). */
@media (max-width: 350px) { .openvibe-navbar .openvibe-navbar-brand .name { display: none; } }
@media (max-width: 350px) { .openvibe-navbar .openvibe-navbar-brand .name { display: none; } }
/* The site's name reads whole on a signed-in phone (the owner, 2026-11-02: "the label or the branding of the
/* The site's name reads whole on a signed-in phone (the owner, 2026-11-02: "the label or the branding of the
@@ -379,26 +379,13 @@
@@ -379,26 +379,13 @@
.openvibe-navbar .openvibe-navbar-login { padding: 0 13px; }
.openvibe-navbar .openvibe-navbar-login { padding: 0 13px; }
}
}
- .openvibe-navbar-dropdown {
- position: absolute; top: 48px; right: 8px;
- width: min(320px, calc(100vw - 16px)); background: var(--bg-card, #22222c);
- border: 1px solid var(--border, #333340); border-radius: 10px;
- box-shadow: var(--shadow-lg, 0 8px 32px rgba(0,0,0,0.5));
- display: none; flex-direction: column; overflow: hidden;
- animation: openvibe-slide-down .2s ease;
- }
- .openvibe-navbar-dropdown.open { display: flex; }
- /* The panel scrolls as a whole; its sections keep their height. A flex column lets children shrink by
- default, which squeezed the accounts block into a sliver with its own scrollbar when the panel was
- height-limited (phones). */
+ /* The account's block (4.188.0): not a popup. It rests in the bar out of sight and is a block of the menu's
+ You tab while the menu is open: no position, no shadow, no height of its own. */
+ .openvibe-navbar-dropdown { display: none; flex-direction: column; border: 1px solid var(--border, #333340); }
.openvibe-navbar-dropdown > * { flex-shrink: 0; }
.openvibe-navbar-dropdown > * { flex-shrink: 0; }
- /* Panels never leave the screen: dvh is the VISIBLE height on phones (100vh reaches behind the address
- bar), the panel scrolls inside itself, and it is never wider than the viewport. */
- .openvibe-navbar-dropdown {
- max-width: calc(100vw - 16px);
- max-height: calc(100vh - 64px); max-height: calc(100dvh - 64px - env(safe-area-inset-bottom, 0px));
- overflow-y: auto; overflow-x: hidden; overscroll-behavior: contain; -webkit-overflow-scrolling: touch; scrollbar-width: thin;
- }
+ .ovh-account > .openvibe-navbar-dropdown.ovnav-account .openvibe-navbar-dropdown-header .ud-hbtn { min-width: 44px; min-height: 44px; }
+ .ovh-account > .openvibe-navbar-dropdown.ovnav-account { display: flex; overflow: hidden; margin: 2px 4px 10px; border-radius: 16px;
+ background: color-mix(in srgb, var(--text-primary, #e6edf7) 4%, transparent); border-color: color-mix(in srgb, var(--text-primary, #e6edf7) 10%, transparent); }
@keyframes openvibe-slide-down { from { transform: translateY(-8px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
@keyframes openvibe-slide-down { from { transform: translateY(-8px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
.ov-pop { transform-origin: var(--ov-pop-x, 50%) var(--ov-pop-y, 0); animation: ov-pop .34s cubic-bezier(.2, 1.1, .3, 1) both !important; }
.ov-pop { transform-origin: var(--ov-pop-x, 50%) var(--ov-pop-y, 0); animation: ov-pop .34s cubic-bezier(.2, 1.1, .3, 1) both !important; }
.ov-pop-out { transform-origin: var(--ov-pop-x, 50%) var(--ov-pop-y, 0); animation: ov-pop-out .16s ease-in both !important; pointer-events: none; }
.ov-pop-out { transform-origin: var(--ov-pop-x, 50%) var(--ov-pop-y, 0); animation: ov-pop-out .16s ease-in both !important; pointer-events: none; }
@@ -571,7 +558,7 @@
@@ -571,7 +558,7 @@
const NAV_ICONS_REV = '148a0eda9ca8'; // sha256(nav-icons.js), first 12 hex: its ?v= (cacheable forever)
const NAV_ICONS_REV = '148a0eda9ca8'; // sha256(nav-icons.js), first 12 hex: its ?v= (cacheable forever)
// END generated
// END generated
// The files loaded from beside this one, by content (scripts/sibling-revs.js): a release changes their URLs.
// The files loaded from beside this one, by content (scripts/sibling-revs.js): a release changes their URLs.
- const SIBLING_REV = { 'ov-mark.js': '01cf79e36af5', 'ov-glyphs.js': '97224763226b', 'ov-brand.woff2': '3b4e6a8a5878', 'ov-look.js': '7ef00feeb07c', 'ov-fx.js': 'bb5afd571c72', 'ov-labels.js': 'd40f7ca14bc4', 'ov-forms.js': '2eb9e2604c3e', 'ov-forms.css': '06208fe89eac', 'ov-share.js': 'd419508a797e', 'ov-share.css': 'd609a71f611a', 'search.js': 'bb502f67c4d5', 'dock.js': '00e20b4b2895', 'presence.js': '5f195d41f8c5', 'panels.js': 'd90bfe987d81', 'surfaces.js': 'e72f5a304f2d', 'notification-ui.js': 'b64ea0c39d34', 'history.js': '1e6598f78ba4', 'appbar.js': '4e34b7d60372', 'release-watch.js': '25f02a3f0684', 'ov-icons.js': '8e5047755456', 'sso-client.js': '52439265b980', 'overlay.js': '336b30eae325', 'ov-home.js': 'db1d79b7b932', 'nav-slots.js': '1db19edaeac4', 'ov-wallet.js': '5c4d7fdb448d', 'ov-wallet.css': 'e46f653fa59e' };
+ const SIBLING_REV = { 'ov-mark.js': '01cf79e36af5', 'ov-glyphs.js': '97224763226b', 'ov-brand.woff2': '3b4e6a8a5878', 'ov-look.js': '7ef00feeb07c', 'ov-fx.js': 'bb5afd571c72', 'ov-labels.js': 'd40f7ca14bc4', 'ov-forms.js': '2eb9e2604c3e', 'ov-forms.css': '06208fe89eac', 'ov-share.js': 'd419508a797e', 'ov-share.css': 'd609a71f611a', 'search.js': '9c0b9621ca9f', 'dock.js': '00e20b4b2895', 'presence.js': '5f195d41f8c5', 'panels.js': 'd90bfe987d81', 'surfaces.js': 'e72f5a304f2d', 'notification-ui.js': 'b64ea0c39d34', 'history.js': '1e6598f78ba4', 'appbar.js': '4e34b7d60372', 'release-watch.js': '25f02a3f0684', 'ov-icons.js': '8e5047755456', 'sso-client.js': '52439265b980', 'overlay.js': '336b30eae325', 'ov-home.js': '135d730d03fd', 'nav-slots.js': '1db19edaeac4', 'ov-wallet.js': '5c4d7fdb448d', 'ov-wallet.css': 'e46f653fa59e' };
// The navbar's icons are filled paths (no Font Awesome CSS needed): the ones above ship here, the rest
// The navbar's icons are filled paths (no Font Awesome CSS needed): the ones above ship here, the rest
// (nav-icons.js) load after first paint when a link, chip or menu row names one, into an empty <svg> of their
// (nav-icons.js) load after first paint when a link, chip or menu row names one, into an empty <svg> of their
// size. Anything unknown, or nav-icons.js failing, falls back to <i class="fa-solid">.
// size. Anything unknown, or nav-icons.js failing, falls back to <i class="fa-solid">.
@@ -927,7 +914,7 @@
@@ -927,7 +914,7 @@
: seg('b-core', brand.core, to.core, brand.name) + join + seg('b-tld', brand.tldText, to.tld, brand.name);
: seg('b-core', brand.core, to.core, brand.name) + join + seg('b-tld', brand.tldText, to.tld, brand.name);
const mark = brand.icon
const mark = brand.icon
? `<i class="fa-solid ${escapeAttr(brand.icon)}"></i>`
? `<i class="fa-solid ${escapeAttr(brand.icon)}"></i>`
- : `<span class="ov-mark" data-size="34" data-wide data-variant="${escapeAttr(brand.variant)}"></span>`;
+ : `<span class="ov-mark" data-size="42" data-wide data-variant="${escapeAttr(brand.variant)}"></span>`; // 42 since 4.188.0 (was 34: beside a short name it read as smaller than the word)
return `<div class="openvibe-navbar-brand${brand.subText ? ' has-sub' : ''}${brand.joined ? ' joined' : ''}">
return `<div class="openvibe-navbar-brand${brand.subText ? ' has-sub' : ''}${brand.joined ? ' joined' : ''}">
${_config.switcher === false
${_config.switcher === false
? `<a class="flame" href="${escapeAttr(brand.href)}" aria-label="${escapeAttr(brand.name)} home">${mark}</a>`
? `<a class="flame" href="${escapeAttr(brand.href)}" aria-label="${escapeAttr(brand.name)} home">${mark}</a>`
@@ -1073,6 +1060,7 @@
@@ -1073,6 +1060,7 @@
/** The chips folded into one (shown instead of them on a phone): each chip's icon and short value. */
/** The chips folded into one (shown instead of them on a phone): each chip's icon and short value. */
// New messages are counted on the bell (4.187.0; the owner, 2026-11-10: "instead of having messaging and
// New messages are counted on the bell (4.187.0; the owner, 2026-11-10: "instead of having messaging and
// notifications, just have the notifications … it'll show you new messages as well"): there is no Messages button.
// notifications, just have the notifications … it'll show you new messages as well"): there is no Messages button.
+ let _accountEl = null; // the account's block, built at render, hosted by the menu's You tab
let _msgCount = 0;
let _msgCount = 0;
// New messages, for the bell's count: Chat's GET /api/dm/unread, at most once a minute (README, "The navbar").
// New messages, for the bell's count: Chat's GET /api/dm/unread, at most once a minute (README, "The navbar").
let _unreadAt = 0;
let _unreadAt = 0;
@@ -2198,26 +2186,30 @@
@@ -2198,26 +2186,30 @@
</div>
</div>
`;
`;
dropdown.addEventListener('click', (e) => { const t = e.target.closest && e.target.closest('[data-use]'); if (t) countUse(t.getAttribute('data-use')); });
dropdown.addEventListener('click', (e) => { const t = e.target.closest && e.target.closest('[data-use]'); if (t) countUse(t.getAttribute('data-use')); });
+ // 4.188.0: not a popup under the picture. It rests in the bar, out of sight (so a site that looks for its
+ // sign-out button or its own item still finds it), and the menu's You tab hosts it while the menu is open
+ // (openSearch's account(); onClose brings it back).
+ dropdown.classList.add('ovnav-account');
nav.appendChild(dropdown);
nav.appendChild(dropdown);
+ _accountEl = dropdown;
bindMenuItems(dropdown, before.concat(after, sectionItems(menuCfg.sections)));
bindMenuItems(dropdown, before.concat(after, sectionItems(menuCfg.sections)));
dropdown.addEventListener('click', (e) => { if (!e.target.closest || !e.target.closest('[data-ov-dock]')) return; dropdown.classList.remove('open'); openDock(); });
dropdown.addEventListener('click', (e) => { if (!e.target.closest || !e.target.closest('[data-ov-dock]')) return; dropdown.classList.remove('open'); openDock(); });
bindChips(dropdown);
bindChips(dropdown);
if (typeof _config.onNavigate === 'function') dropdown.addEventListener('click', (e) => { const a = e.target.closest && e.target.closest('a[href]'); if (!a || e.defaultPrevented || a.target === '_blank' || e.metaKey || e.ctrlKey) return; const path = sitePath(a); if (path != null) { if (_config.onNavigate(path, e) === false) e.preventDefault(); dropdown.classList.remove('open'); } });
if (typeof _config.onNavigate === 'function') dropdown.addEventListener('click', (e) => { const a = e.target.closest && e.target.closest('a[href]'); if (!a || e.defaultPrevented || a.target === '_blank' || e.metaKey || e.ctrlKey) return; const path = sitePath(a); if (path != null) { if (_config.onNavigate(path, e) === false) e.preventDefault(); dropdown.classList.remove('open'); } });
bindDisplayRows(dropdown);
bindDisplayRows(dropdown);
bindLook(dropdown);
bindLook(dropdown);
- regPanel(dropdown, 'user-menu', () => dropdown.classList.remove('open'), nav.querySelector('#openvibe-avatar-btn'));
- // Avatar click toggles dropdown (the selection for Ask Vibe is read on pointerdown: the press can clear it)
+ // The picture opens the menu on You, where the account's things are (the selection for Ask Vibe is read on
+ // pointerdown: the press can clear it).
const avatarBtn = nav.querySelector('#openvibe-avatar-btn'); let sel = null;
const avatarBtn = nav.querySelector('#openvibe-avatar-btn'); let sel = null;
avatarBtn.addEventListener('pointerdown', () => { sel = selectedText(); });
avatarBtn.addEventListener('pointerdown', () => { sel = selectedText(); });
avatarBtn.addEventListener('click', () => {
avatarBtn.addEventListener('click', () => {
- if (dropdown.classList.contains('open') && !dropdown.classList.contains('ov-pop-out')) { unpop(dropdown, () => { if (!dropdown.classList.contains('ov-pop')) dropdown.classList.remove('open'); }); return; }
- dropdown.classList.add('open'); popFrom(dropdown, avatarBtn);
const ask = dropdown.querySelector('[data-ov-ask]'); if (ask) ask.href = askVibeHref(sel ?? selectedText()); sel = null;
const ask = dropdown.querySelector('[data-ov-ask]'); if (ask) ask.href = askVibeHref(sel ?? selectedText()); sel = null;
- if (dropdown.classList.contains('open')) { loadWallet(dropdown); dropdown.scrollTop = 0; loadOvIcons(dropdown); }
+ if (menuOpen() && root.OpenVibeSearch) { root.OpenVibeSearch.close(); return; }
+ const orbs = nav.querySelectorAll ? [...nav.querySelectorAll('#openvibe-burger.ovnav-orb, #openvibe-search-btn')] : [];
+ openSearch('', 'you', orbs.find((o) => o.getBoundingClientRect && o.getBoundingClientRect().width > 0) || null);
});
});
- // panels.js closes this on an outside pointerdown, including another control in the navbar.
dropdown.querySelectorAll('[data-account-id]').forEach(el => {
dropdown.querySelectorAll('[data-account-id]').forEach(el => {
el.addEventListener('click', () => {
el.addEventListener('click', () => {
@@ -2317,7 +2309,9 @@
@@ -2317,7 +2309,9 @@
// Display (theme, text size, motion) at the menu's foot, drawn by the same controls the drawer had.
// 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); },
display: (el) => { const box = document.createElement('div'); box.className = 'ovnav-display ovs-display'; el.appendChild(box); bindDisplayControls(box); },
look: bindLook,
look: bindLook,
- 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'); } });
+ // The account's own block (who you are, your accounts, sign out, this site's items): the You tab hosts it.
+ account: (host) => { if (!_accountEl || !host) return false; host.appendChild(_accountEl); loadWallet(_accountEl); loadOvIcons(_accountEl); return true; },
+ onClose: () => { if (_accountEl && _navEl && _accountEl.parentNode !== _navEl) _navEl.appendChild(_accountEl); 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');
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();
@@ -114,7 +114,7 @@ html[data-ov-motion=reduced] .ovh.is-edit .ovh-grid:not(.ovh-recent) .ovh-ic { a
@@ -114,7 +114,7 @@ html[data-ov-motion=reduced] .ovh.is-edit .ovh-grid:not(.ovh-recent) .ovh-ic { a
.ovh-calm { transition: opacity .2s; }
.ovh-calm { transition: opacity .2s; }
.ovh.is-edit .ovh-calm { opacity: .38; pointer-events: none; }
.ovh.is-edit .ovh-calm { opacity: .38; pointer-events: none; }
.ovh-headtools { display: inline-flex; align-items: center; gap: 6px; margin-left: auto; }
.ovh-headtools { display: inline-flex; align-items: center; gap: 6px; margin-left: auto; }
-.ovh-sec .ovh-allbtn { all: unset; box-sizing: border-box; display: inline-flex; align-items: center; gap: 6px; min-height: 36px; padding: 0 11px 0 10px; border-radius: 999px; cursor: pointer; font-size: 13px; font-weight: 700;
+.ovh-sec .ovh-allbtn { all: unset; box-sizing: border-box; text-transform: none; letter-spacing: 0; display: inline-flex; align-items: center; gap: 6px; min-height: 36px; padding: 0 11px 0 10px; border-radius: 999px; cursor: pointer; font-size: 13px; font-weight: 700;
color: var(--text-secondary, #a8b3c4); background: color-mix(in srgb, var(--text-primary, #e8ecf3) 7%, transparent); transition: color .15s, background-color .15s, transform .12s; }
color: var(--text-secondary, #a8b3c4); background: color-mix(in srgb, var(--text-primary, #e8ecf3) 7%, transparent); transition: color .15s, background-color .15s, transform .12s; }
.ovh-sec .ovh-allbtn svg { width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.ovh-sec .ovh-allbtn svg { width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.ovh-sec .ovh-allbtn b { font-weight: 800; font-variant-numeric: tabular-nums; color: var(--text-primary, #e8ecf3); }
.ovh-sec .ovh-allbtn b { font-weight: 800; font-variant-numeric: tabular-nums; color: var(--text-primary, #e8ecf3); }
@@ -409,6 +409,16 @@ html[data-ov-motion=reduced] .ovh-card { animation-name: ovh-fade; animation-dur
@@ -409,6 +409,16 @@ html[data-ov-motion=reduced] .ovh-card { animation-name: ovh-fade; animation-dur
/** You: your picture (online), your name, and Display and Settings beside it. Made once per opening, so an open
/** You: your picture (online), your name, and Display and Settings beside it. Made once per opening, so an open
* Look panel survives the repaints that come as history and the catalogue arrive. */
* Look panel survives the repaints that come as history and the catalogue arrive. */
+ // The account's block (navbar.js builds it: who you are, add account, settings, sign out, your other accounts,
+ // this site's own items): the You tab is where it lives since 4.188.0. Without a bar to give it, the short bar below.
+ let accountBox = null;
+ function account() {
+ if (typeof o.account !== 'function') return null;
+ accountBox = accountBox || h('div', 'ovh-account');
+ let ok = false;
+ try { ok = o.account(accountBox) !== false; } catch { ok = false; }
+ return ok ? accountBox : null;
+ }
function me() {
function me() {
if (meBox) return meBox;
if (meBox) return meBox;
const u = o.user || {};
const u = o.user || {};
@@ -496,7 +506,9 @@ html[data-ov-motion=reduced] .ovh-card { animation-name: ovh-fade; animation-dur
@@ -496,7 +506,9 @@ html[data-ov-motion=reduced] .ovh-card { animation-name: ovh-fade; animation-dur
// nothing above the grid may come or go (the macOS run of #394 dropped onto the wrong place when the hint
// nothing above the grid may come or go (the macOS run of #394 dropped onto the wrong place when the hint
// pushed the grid down mid-drag). The cards above stay, quiet and inert; the hint goes under the grid.
// pushed the grid down mid-drag). The cards above stay, quiet and inert; the hint goes under the grid.
const calm = (x) => { x.classList.add('ovh-calm'); x.inert = state.edit; return x; };
const calm = (x) => { x.classList.add('ovh-calm'); x.inert = state.edit; return x; };
- if (signedIn(o)) el.append(calm(me()));
+ // The account's block wherever the bar has a person (a site with its own session may have no token for
+ // the network's reads yet: sign out and the rest must still be there); else the short bar.
+ { const a = account(); if (a) el.append(calm(a)); else if (signedIn(o)) el.append(calm(me())); }
if (signedIn(o)) { actorBox = actorBox || h('div'); el.append(actorBox); }
if (signedIn(o)) { actorBox = actorBox || h('div'); el.append(actorBox); }
const head = sec(L.arranged || signedIn(o) ? 'Your home' : 'Popular on OpenVibe');
const head = sec(L.arranged || signedIn(o) ? 'Your home' : 'Popular on OpenVibe');
const edit = h('button', state.edit ? 'ovh-edit is-done' : 'ovh-edit');
const edit = h('button', state.edit ? 'ovh-edit is-done' : 'ovh-edit');
@@ -1,12 +1,12 @@
@@ -1,12 +1,12 @@
{
{
"name": "openvibe-shared",
"name": "openvibe-shared",
- "version": "4.187.0",
+ "version": "4.188.0",
"lockfileVersion": 3,
"lockfileVersion": 3,
"requires": true,
"requires": true,
"packages": {
"packages": {
"": {
"": {
"name": "openvibe-shared",
"name": "openvibe-shared",
- "version": "4.187.0",
+ "version": "4.188.0",
"license": "MIT",
"license": "MIT",
"dependencies": {
"dependencies": {
"jsonwebtoken": "^9.0.0",
"jsonwebtoken": "^9.0.0",
@@ -1,6 +1,6 @@
@@ -1,6 +1,6 @@
{
{
"name": "openvibe-shared",
"name": "openvibe-shared",
- "version": "4.187.0",
+ "version": "4.188.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": {
@@ -1236,7 +1236,7 @@
@@ -1236,7 +1236,7 @@
const back = mine ? mine.items.filter((x) => { if (seenB.has(x.url)) return false; seenB.add(x.url); return true; }).slice(0, 8).map(backRow) : [];
const back = mine ? mine.items.filter((x) => { if (seenB.has(x.url)) return false; seenB.add(x.url); return true; }).slice(0, 8).map(backRow) : [];
const usage = mine ? mine.services.filter((x) => x && x.count > 0).map((x) => ({ id: x.service === 'media' ? 'media-hub' : x.service, count: Number(x.count) || 0 })) : [];
const usage = mine ? mine.services.filter((x) => x && x.count > 0).map((x) => ({ id: x.service === 'media' ? 'media-hub' : x.service, count: Number(x.count) || 0 })) : [];
return { apps, yours, tools, toolCount: catalog ? catalog.tools.length : 0, recent: recentIds, back, usage, groups: f && Array.isArray(f.groups) ? f.groups : [],
return { apps, yours, tools, toolCount: catalog ? catalog.tools.length : 0, recent: recentIds, back, usage, groups: f && Array.isArray(f.groups) ? f.groups : [],
- token: ctx.token, apiBase: ctx.apiBase, user: ctx.user, look: ctx.look, mountIcons: (scope) => { if (typeof ctx.icons === 'function') ctx.icons(scope); }, close,
+ token: ctx.token, apiBase: ctx.apiBase, user: ctx.user, look: ctx.look, account: ctx.account, mountIcons: (scope) => { if (typeof ctx.icons === 'function') ctx.icons(scope); }, close,
pulseReady: pulseReady || (pulseReady = root.OpenVibeHome.loadPulse({ token: ctx.token, user: ctx.user })),
pulseReady: pulseReady || (pulseReady = root.OpenVibeHome.loadPulse({ token: ctx.token, user: ctx.user })),
// The feed answered: the Feed tab is the stream (its sections would sit after an endless list); it did not: as before.
// The feed answered: the Feed tab is the stream (its sections would sit after an endless list); it did not: as before.
feedLive: () => { if (!feedV2) { feedV2 = true; if (el && el.classList.contains('is-open') && !input.value && tab === 'feed') render(''); } } };
feedLive: () => { if (!feedV2) { feedV2 = true; if (el && el.classList.contains('is-open') && !input.value && tab === 'feed') render(''); } } };
@@ -38,7 +38,7 @@ assert.strictEqual(files.path('ov-brand.woff2'), path.join(__dirname, '..', 'ov-
@@ -38,7 +38,7 @@ assert.strictEqual(files.path('ov-brand.woff2'), path.join(__dirname, '..', 'ov-
assert.ok(/function brandFace\(\)[\s\S]*ov-brand\.woff2/.test(cards) && /\.name\{[^}]*font-family:'OV Brand'/.test(cards), 'the share cards, with the face embedded');
assert.ok(/function brandFace\(\)[\s\S]*ov-brand\.woff2/.test(cards) && /\.name\{[^}]*font-family:'OV Brand'/.test(cards), 'the share cards, with the face embedded');
// The mark sits beside the name in the wide box: level with the words, not a square with space above and below.
// The mark sits beside the name in the wide box: level with the words, not a square with space above and below.
- assert.match(nav, /<span class="ov-mark" data-size="34" data-wide data-variant=/, 'navbar: the wide mark');
+ assert.match(nav, /<span class="ov-mark" data-size="42" data-wide data-variant=/, 'navbar: the wide mark');
assert.match(read('footer.js'), /<span class="ov-mark" data-size="38" data-wide><\/span>/, 'footer: the wide mark');
assert.match(read('footer.js'), /<span class="ov-mark" data-size="38" data-wide><\/span>/, 'footer: the wide mark');
console.log('brand face: served as a font with CORS, declared by the navbar, used for the names with a fallback; the wide mark beside them');
console.log('brand face: served as a font with CORS, declared by the navbar, used for the names with a fallback; the wide mark beside them');
})().catch((e) => { console.error(e); process.exit(1); });
})().catch((e) => { console.error(e); process.exit(1); });
@@ -180,8 +180,10 @@ const server = http.createServer((req, res) => {
@@ -180,8 +180,10 @@ const server = http.createServer((req, res) => {
assert.ok(await until(s, `!!document.querySelector('.ovh-actor')`), 'the Actor strip is on top');
assert.ok(await until(s, `!!document.querySelector('.ovh-actor')`), 'the Actor strip is on top');
assert.match(await s.evaluate(`document.querySelector('.ovh-actor').textContent`), /Juniper.*Working.*Reading job posts/, 'it names the Actor, its state and its step');
assert.match(await s.evaluate(`document.querySelector('.ovh-actor').textContent`), /Juniper.*Working.*Reading job posts/, 'it names the Actor, its state and its step');
// You on top: the name, Display (the Look panel) and Settings beside it; then your home, Most used and Jump back in.
// You on top: the name, Display (the Look panel) and Settings beside it; then your home, Most used and Jump back in.
- assert.strictEqual(await s.evaluate(`document.querySelector('.ovh-me .ovh-who-txt b').textContent`), 'Ada', 'your name on top');
- assert.ok(await s.evaluate(`!!document.querySelector('.ovh-me [data-ov-look-open]') && !!document.querySelector('.ovh-me a[href="https://openvibe.network/my"]')`), 'Display and Settings beside it');
+ // 4.188.0: the account's block is here (it was a popup under the picture): who you are, add account, settings, sign out.
+ assert.strictEqual(await s.evaluate(`document.querySelector('.ovh-account .openvibe-navbar-dropdown-header .name').textContent`), 'Ada', 'your name on top');
+ assert.ok(await s.evaluate(`!!document.querySelector('.ovh-account #openvibe-logout-btn') && !document.querySelector('.ovh-me')`), 'with Sign out, and no second bar with your name');
+ assert.ok(await s.evaluate(`!!document.querySelector('.ovh-account [data-ov-look-open]') && !!document.querySelector('.ovh-account a[href="https://openvibe.network/my"]')`), 'Display and Settings in it');
assert.ok(await s.evaluate(`!document.querySelector('.ovs-displayrow')`), 'Display is not at the foot any more');
assert.ok(await s.evaluate(`!document.querySelector('.ovs-displayrow')`), 'Display is not at the foot any more');
assert.ok(await until(s, `document.querySelectorAll('.ovh-rail .ovh-card').length === 2`), 'Jump back in shows the places opened lately');
assert.ok(await until(s, `document.querySelectorAll('.ovh-rail .ovh-card').length === 2`), 'Jump back in shows the places opened lately');
assert.deepStrictEqual(await s.evaluate(`[...document.querySelectorAll('.ovh-rail .ovh-card')].map((a) => a.getAttribute('href'))`), ['https://openvibe.live/ada', 'https://openvibe.pics/1']);
assert.deepStrictEqual(await s.evaluate(`[...document.querySelectorAll('.ovh-rail .ovh-card')].map((a) => a.getAttribute('href'))`), ['https://openvibe.live/ada', 'https://openvibe.pics/1']);
@@ -191,8 +193,9 @@ const server = http.createServer((req, res) => {
@@ -191,8 +193,9 @@ const server = http.createServer((req, res) => {
// to whatever the user uses the most". Your home comes first, then what you use most, then where you were last;
// to whatever the user uses the most". Your home comes first, then what you use most, then where you were last;
// the bar with your name is short; each most-used app is a way in (its icon, a full touch target), not a legend.
// the bar with your name is short; each most-used app is a way in (its icon, a full touch target), not a legend.
assert.deepStrictEqual(await s.evaluate(`[...document.querySelectorAll('.ovh-sec > span')].map((x) => x.textContent)`), ['Your home', 'Most used', 'Jump back in'], 'your home, then most used, then where you were');
assert.deepStrictEqual(await s.evaluate(`[...document.querySelectorAll('.ovh-sec > span')].map((x) => x.textContent)`), ['Your home', 'Most used', 'Jump back in'], 'your home, then most used, then where you were');
- assert.ok(await s.evaluate(`document.querySelector('.ovh-me').getBoundingClientRect().height <= 56`), 'the bar with your name is short');
- assert.ok(await s.evaluate(`[...document.querySelectorAll('.ovh-me .ovh-ib')].every((b) => b.getBoundingClientRect().height >= 44)`), 'and its two buttons are still full touch targets');
+ assert.ok(await s.evaluate(`document.querySelector('.ovh-account .openvibe-navbar-dropdown-header').getBoundingClientRect().height <= 96`), 'the row with your name is short');
+ assert.ok(await s.evaluate(`[...document.querySelectorAll('.ovh-account .ud-hbtn')].every((b) => { const r = b.getBoundingClientRect(); return r.height >= 44 && r.width >= 44; })`), 'and its buttons are full touch targets');
+ assert.strictEqual(await s.evaluate(`getComputedStyle(document.querySelector('.ovh-account > .openvibe-navbar-dropdown')).position`), 'static', 'a block of the page, not a popup');
assert.deepStrictEqual(await s.evaluate(`[...document.querySelectorAll('.ovh-use .ovh-key')].map((k) => [k.getAttribute('href'), k.getAttribute('aria-label'), !!k.querySelector('.ovh-ic > *')].join('|'))`),
assert.deepStrictEqual(await s.evaluate(`[...document.querySelectorAll('.ovh-use .ovh-key')].map((k) => [k.getAttribute('href'), k.getAttribute('aria-label'), !!k.querySelector('.ovh-ic > *')].join('|'))`),
['https://openvibe.live/|Live, opened 5 times|true', 'https://openvibe.pics/|Pics, opened 2 times|true'], 'each most-used app is a link with its icon and says how often');
['https://openvibe.live/|Live, opened 5 times|true', 'https://openvibe.pics/|Pics, opened 2 times|true'], 'each most-used app is a link with its icon and says how often');
assert.ok(await s.evaluate(`[...document.querySelectorAll('.ovh-use .ovh-key')].every((k) => { const r = k.getBoundingClientRect(); return r.height >= 44 && r.width >= 44; })`), 'each a full touch target');
assert.ok(await s.evaluate(`[...document.querySelectorAll('.ovh-use .ovh-key')].every((k) => { const r = k.getBoundingClientRect(); return r.height >= 44 && r.width >= 44; })`), 'each a full touch target');
@@ -88,7 +88,10 @@ const server = http.createServer((req, res) => {
@@ -88,7 +88,10 @@ const server = http.createServer((req, res) => {
const u = new URL(await askHref());
const u = new URL(await askHref());
assert.strictEqual(u.searchParams.get('ask'), 'The rover drives to the door.');
assert.strictEqual(u.searchParams.get('ask'), 'The rover drives to the door.');
assert.strictEqual(u.searchParams.get('from'), null, 'an http test page is not named');
assert.strictEqual(u.searchParams.get('from'), null, 'an http test page is not named');
- assert.strictEqual(await p.evaluate(`document.querySelector('.openvibe-navbar-dropdown').classList.contains('open')`), true, 'the menu opened');
+ // 4.188.0: the picture opens the menu on You, and the account's block (with Ask Vibe in it) is there.
+ const until = async (expr, ms = 6000) => { const end = Date.now() + ms; while (Date.now() < end) { if (await p.evaluate(expr)) return true; await new Promise((r) => setTimeout(r, 80)); } return false; };
+ assert.ok(await until(`!!document.querySelector('.ovs.is-open .ovh-account > .openvibe-navbar-dropdown [data-ov-ask]')`), "the picture opened the menu's You tab, with the account's things in it");
+ assert.strictEqual(await p.evaluate(`getComputedStyle(document.querySelector('.ovh-account > .openvibe-navbar-dropdown')).position`), 'static', 'as a block of that page, not a popup');
console.log('navbar-ask-vibe: browser ok');
console.log('navbar-ask-vibe: browser ok');
} finally {
} finally {
await browser.close();
await browser.close();
@@ -192,7 +192,7 @@ function load(hostname, opts = {}) {
@@ -192,7 +192,7 @@ function load(hostname, opts = {}) {
global.addEventListener = (type, fn, opts) => winListeners.push({ type, opts });
global.addEventListener = (type, fn, opts) => winListeners.push({ type, opts });
navbar.init({ service: 'space', user: { id: 1, username: 'x' } });
navbar.init({ service: 'space', user: { id: 1, username: 'x' } });
const first = regs.slice(), firstDisplay = winListeners.filter((l) => l.type === 'ov:display');
const first = regs.slice(), firstDisplay = winListeners.filter((l) => l.type === 'ov:display');
- assert.deepStrictEqual(first.map((r) => r.id).sort(), ['nav-drawer', 'user-menu'], 'the drawer and the account menu are registered');
+ assert.deepStrictEqual(first.map((r) => r.id).sort(), ['nav-drawer'], 'the drawer is registered (the account\'s things are in the menu since 4.188.0: no popup of their own)');
assert.ok(first.every((r) => r.signal && !r.signal.aborted), 'each registration carries the render\'s signal');
assert.ok(first.every((r) => r.signal && !r.signal.aborted), 'each registration carries the render\'s signal');
assert.ok(firstDisplay.length === 1 && firstDisplay[0].opts && firstDisplay[0].opts.signal, 'the display rows\' window listener carries it too');
assert.ok(firstDisplay.length === 1 && firstDisplay[0].opts && firstDisplay[0].opts.signal, 'the display rows\' window listener carries it too');
navbar.init({ service: 'space', user: { id: 1, username: 'x' } });
navbar.init({ service: 'space', user: { id: 1, username: 'x' } });
@@ -211,7 +211,7 @@ function load(hostname, opts = {}) {
@@ -211,7 +211,7 @@ function load(hostname, opts = {}) {
global.OpenVibePanels = { register: (o) => { regs.push(o); } };
global.OpenVibePanels = { register: (o) => { regs.push(o); } };
loaders[loaders.length - 1].onload();
loaders[loaders.length - 1].onload();
delete global.OpenVibePanels;
delete global.OpenVibePanels;
- assert.deepStrictEqual(regs.map((r) => r.id).sort(), ['nav-drawer', 'user-menu'], 'only the live render\'s panels are registered');
+ assert.deepStrictEqual(regs.map((r) => r.id).sort(), ['nav-drawer'], 'only the live render\'s panels are registered');
assert.ok(regs.every((r) => !r.signal.aborted));
assert.ok(regs.every((r) => !r.signal.aborted));
}
}
// ── balance chips fold into one on a phone (owner, 2026-10-27): the folded chip, its short values, and setChip ──
// ── balance chips fold into one on a phone (owner, 2026-10-27): the folded chip, its short values, and setChip ──