openvibers/openvibe.shared Commits
4.185.0: You tab (your home first, shorter bar, Most used as shortcuts); a phone Dock opens out of the orb (#406)
Changes
@@@ -1,5 +1,39 @@# Changelog++## 4.185.0 — 2026-11-10++++**The You tab: your home first, a shorter bar, Most used as shortcuts.** The owner, 2026-11-10: "Your home should be++above Jump back in… the thing at the top with your username and online and the settings… a little more compact… most++used is cool, but maybe we should actually have links to whatever the user uses the most… so it's just easier to jump++to the app."++++- **The order** is you, your own AI, **Your home**, Most used, Jump back in (it was you, AI, Jump back in, Most used,++ Your home). Nothing above the grid comes or goes while Home is being arranged, as before.++- **The bar with your name** is 52px tall (was 68): a 36px picture, name and handle on two tight lines. Display and++ Settings keep their 44px.++- **Most used** keeps its one bar; its key is now the apps themselves, five across: each app's icon in its own hue, its++ name, and how often with the bar's colour beside the number. Each is a link (`.ovh-key`, "Live, opened 183 times") and++ a full touch target; it was a row of small legend entries.++- **Jump back in** comes last and says what it is: "where you were last".++++**A phone's Dock opens out of the navbar's menu button, and that button is its close button.** The owner, 2026-11-10:++"that close button should, instead of coming from that menu, fade and have an animation effect from the top right, the++menu button that already exists… it's supposed to be nicer connected to this." Until now the sheet rose from the bottom++and carried its own close button up with it.++++- On a phone (640px and under) with the orb at the bar's right, the sheet fades and grows from the orb's centre++ (`.ovdk.from-orb`); its close orb starts exactly on the navbar's orb, as its nine dots, and settles into its x in the++ header (a glide of a few pixels and the same turn the main menu's orb makes). The navbar's own orb makes the same++ move underneath (`.ovnav-orb.is-dock`), so it is one button the whole way. The x wears the menu's colours.++- **Closing runs it back:** the x turns into the dots and returns to the bar while the sheet fades (240 ms), then the++ sheet is gone and the button is where it always was. `OpenVibeDock.close()` therefore hides a phone's sheet a moment++ later than it is called; with motion off, in a window of its own, or on a bar without the orb, it hides at once as++ before (and such a bar keeps the rise from the bottom).++- Calm keeps the move (the grow is smaller, by `--ov-move`); only motion off skips it.++++`test/menu-home-screen-chrome.test.js` holds both: the order, the bar's height, the shortcuts' links and targets; the++close orb starting on the navbar's orb, settling, folding back, and standing still with motion off.++## 4.184.0 — 2026-11-10- **Server-rendered navbar links.** The shared shell emits the configured navbar links in the reserved `#navbar-mount`@@@ -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.184.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed++**Status:** alpha, v4.185.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,13 +4,14 @@"code": true,"wave": "W2","package": "openvibe-shared",-- "version": "4.184.0",++ "version": "4.185.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)","contracts": "openvibe-contracts v0.384.0 (devDependency: the release manifest contract)","sdk": "openvibe-sdk v0.133.0","features": [++ "the menu's You tab: your home right under your name (a 52px bar, was 68) and the AI card; Most used is the apps themselves under their bar, each a link and a full touch target; Jump back in last, saying what it is. A phone's Dock opens out of the navbar's menu button: its close orb starts on that orb and settles into its x while the navbar's orb makes the same move underneath, and closing runs it back (4.185.0)","motion has four choices (Automatic, Full, Calm, Off) and one signal, html[data-ov-motion]: Calm keeps every effect and makes it smaller and slower (--ov-move, display.move(), endless loops at 0.6 of their speed), and only Off stops motion; no rule in the package stops an effect because the device asked for less (4.183.0)","every shape glyph has a solid drawing (203 of 278; the other 75 are line glyphs that thicken when small), glyphs.svg() draws a solid's line parts as lines, and every product id has its own site mark, so no product draws the bare V (4.183.0)","the Dock's agent view, timeline and Actor ask Network's /api/frame relay instead of openvibe.codes and openvibe.actor, so a signed-in page logs no cross-origin console errors; an explicit base still reaches the product (4.181.0)",@@@ -193,6 +193,27 @@.ovdk-orbx:active .ovdk-ox { transform: rotate(270deg) scale(.7); }@keyframes ovdk-ox-in { from { transform: rotate(45deg) scale(.8); } }@keyframes ovdk-o1-out { from { opacity: 1; transform: scale(1); } }++ /* A phone's sheet opens out of the navbar's orb, which becomes its close button (4.185.0; see CHANGELOG). */++ .ovdk.from-orb { transform-origin: var(--ovdk-ox, 100%) var(--ovdk-oy, 0%) !important; }++ .ovdk.from-orb.is-in { animation: ovdk-from-orb .3s cubic-bezier(.2,.9,.25,1) both; }++ .ovdk.from-orb.is-out { animation: ovdk-to-orb .24s ease both; pointer-events: none; }++ @keyframes ovdk-from-orb { from { opacity: 0; transform: scale(calc(1 - .06 * var(--ov-move, 1))); } }++ @keyframes ovdk-to-orb { to { opacity: 0; transform: scale(calc(1 - .04 * var(--ov-move, 1))); } }++ .from-orb.is-in .ovdk-ox, .from-orb.is-in .ovdk-o1 { animation: none !important; }++ .from-orb .ovdk-orbx { color: var(--text-primary, #ececf1); }++ .from-orb .ovdk-orbx svg { transition: transform .42s cubic-bezier(.2, 1.1, .3, 1); }++ .from-orb .ovdk-ox circle { transition: fill .3s, opacity .3s, transform .35s cubic-bezier(.2, 1.4, .3, 1); }++ .from-orb .ovdk-ox :is(.ovdk-o0, .ovdk-o2), .from-orb .is-from .ovdk-ox .ovdk-o0 { fill: var(--accent-light, #60a5fa); }++ .ovdk-orbx.is-set, .ovdk-orbx.is-set * { transition: none !important; }++ .from-orb .is-from svg { transform: translate(var(--orb-dx, 0), var(--orb-dy, 0)) scale(var(--orb-s, 1)); }++ .from-orb .is-from .ovdk-ox { transform: rotate(45deg) scale(.8); }++ .from-orb .is-from .ovdk-ox circle { fill: var(--text-secondary, #a8b3c4); opacity: 1; transform: none; }++ .from-orb .is-from .ovdk-ox .ovdk-o2 { opacity: .6; }++ .ovnav-orb.is-dock { transform: translate(var(--dock-dx, 0), var(--dock-dy, 0)) scale(var(--dock-s, 1)) !important; color: var(--text-primary, #e6edf7) !important; }++ .ovnav-orb.is-dock .ovo-g { transform: rotate(180deg) scale(.86); transition-duration: .45s; }++ .ovnav-orb.is-dock .ovo-d { animation: none; }++ .ovnav-orb.is-dock .ovo-r1 { opacity: 0; transform: scale(0); }++ .ovnav-orb.is-dock :is(.ovo-r0, .ovo-r2) { fill: var(--ovo-a); opacity: 1; }.ovdk-more { position: absolute; z-index: 6; right: 8px; top: calc(100% - 4px); width: min(320px, calc(100% - 16px)); display: grid; gap: 2px; padding: 6px; border-radius: 16px;border: 1px solid var(--border, rgba(255,255,255,.12)); background: var(--bg-elevated, var(--bg-secondary, #1d1f27)); box-shadow: 0 18px 40px -12px rgba(0,0,0,.55); cursor: default; }.ovdk-more[hidden] { display: none; }@@@ -944,6 +965,39 @@// Ask your agent (4.135.0): Vibe opens with the question in its box, unsent.function ask({ text = '' } = {}, n = 0) { if (!n) open({ tab: 'vibe' }); if (vibe.api && vibe.api.ask) vibe.api.ask(text); else if (n < 40) setTimeout(() => ask({ text }, n + 1), 100); }function isOpen() { return !!(el && !el.hidden); }++ // A phone's sheet and the navbar's orb are one button (.from-orb).++ let orbLink = null, closing = 0;++ function navOrb() {++ if (!phone()) return null;++ const list = document.querySelectorAll('.openvibe-navbar .ovnav-orb');++ for (let i = 0; i < list.length; i++) { const r = list[i].getBoundingClientRect(); if (r.width && r.left > root.innerWidth / 2 && r.top < 120 && r.bottom > 0) return list[i]; }++ return null;++ }++ function fromOrb() {++ unlinkOrb();++ const nav = navOrb(), mine = el.querySelector('.ovdk-orbx'), mineSvg = mine && mine.querySelector('svg');++ if (!nav || !mineSvg || reduced() || popped) return;++ const a = (nav.querySelector('svg') || nav).getBoundingClientRect(), b = mineSvg.getBoundingClientRect(), box = el.getBoundingClientRect();++ if (!a.width || !b.width) return;++ const ax = a.left + a.width / 2, ay = a.top + a.height / 2, bx = b.left + b.width / 2, by = b.top + b.height / 2, s = a.width / b.width;++ el.style.setProperty('--ovdk-ox', `${Math.round(ax - box.left)}px`); el.style.setProperty('--ovdk-oy', `${Math.round(ay - box.top)}px`);++ mine.style.setProperty('--orb-dx', `${(ax - bx).toFixed(1)}px`); mine.style.setProperty('--orb-dy', `${(ay - by).toFixed(1)}px`); mine.style.setProperty('--orb-s', s.toFixed(3));++ nav.style.setProperty('--dock-dx', `${(bx - ax).toFixed(1)}px`); nav.style.setProperty('--dock-dy', `${(by - ay).toFixed(1)}px`); nav.style.setProperty('--dock-s', (1 / s).toFixed(3));++ el.classList.add('from-orb'); mine.classList.add('is-set', 'is-from');++ orbLink = { nav, mine };++ void mine.offsetWidth; // the starting pose is put on, not animated into++ mine.classList.remove('is-set');++ void mine.offsetWidth;++ nav.style.transition = 'transform .42s cubic-bezier(.2, 1.1, .3, 1), color .3s';++ root.requestAnimationFrame(() => { if (!orbLink || orbLink.mine !== mine) return; mine.classList.remove('is-from'); nav.classList.add('is-dock'); });++ }++ function unlinkOrb() {++ if (closing) { clearTimeout(closing); closing = 0; }++ if (el) el.classList.remove('from-orb', 'is-out');++ if (!orbLink) return;++ orbLink.nav.classList.remove('is-dock'); orbLink.nav.style.transition = ''; orbLink.mine.classList.remove('is-from', 'is-set');++ orbLink = null;++ }function share({ text = '', title = '' } = {}) {const t = String(text || '').trim().slice(0, 4000);if (!t) return;@@@ -965,8 +1019,10 @@paintPin();paintPop();el.hidden = false;-- el.classList.remove('is-in'); void el.offsetWidth; el.classList.add('is-in');-- // On a computer it grows out of its pill (the navbar's shared motion); a phone's sheet rises as before.++ el.classList.remove('is-in');++ fromOrb();++ void el.offsetWidth; el.classList.add('is-in');++ // On a computer it grows out of its pill; a phone's sheet opens out of the navbar's orb (fromOrb), or rises.if (!phone() && root.OpenVibeNavbar && root.OpenVibeNavbar.pop) root.OpenVibeNavbar.pop(el, document.getElementById('ovdk-pill'));if (phone()) {el.setAttribute('aria-modal', 'true'); document.documentElement.classList.add('ovdk-sheet-open');@@@ -979,6 +1035,18 @@}function close() {if (!isOpen()) return;++ // Fold back into the navbar's orb first, then go.++ if (orbLink && !closing && !popped && !reduced() && el.classList.contains('from-orb')) {++ el.classList.remove('is-in'); el.classList.add('is-out');++ orbLink.mine.classList.add('is-from'); orbLink.nav.classList.remove('is-dock');++ closing = setTimeout(() => { closing = 0; shut(); }, 240);++ return;++ }++ shut();++ }++ function shut() {++ if (!isOpen()) return;++ unlinkOrb();const wasPopped = popped;if (wasPopped) { popped = false; const w = pipWin; pipWin = null; pipClosing = true; try { if (w) w.close(); } catch { /* already gone */ } pipClosing = false; if (el.parentNode !== document.body) document.body.appendChild(el); }const inside = el.contains(document.activeElement);@@@ -567,7 +567,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': '8cc78bde2190', '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': 'b170f6e994fa', 'dock.js': 'd45d3e6e7c39', 'presence.js': '5f195d41f8c5', 'panels.js': 'd90bfe987d81', 'surfaces.js': 'e72f5a304f2d', 'notification-ui.js': 'dc24929109b5', 'history.js': '1e6598f78ba4', 'site-switcher.js': 'd8980cf95157', 'appbar.js': '4e34b7d60372', 'release-watch.js': '25f02a3f0684', 'ov-icons.js': '8e5047755456', 'sso-client.js': '52439265b980', 'overlay.js': '336b30eae325', 'ov-home.js': '093d5979ae12', 'nav-slots.js': '1db19edaeac4', 'ov-wallet.js': '5c4d7fdb448d', 'ov-wallet.css': 'e46f653fa59e' };++ const SIBLING_REV = { 'ov-mark.js': '8cc78bde2190', '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': 'b170f6e994fa', 'dock.js': '4756db83581e', 'presence.js': '5f195d41f8c5', 'panels.js': 'd90bfe987d81', 'surfaces.js': 'e72f5a304f2d', 'notification-ui.js': 'dc24929109b5', 'history.js': '1e6598f78ba4', 'site-switcher.js': 'd8980cf95157', 'appbar.js': '4e34b7d60372', 'release-watch.js': '25f02a3f0684', 'ov-icons.js': '8e5047755456', 'sso-client.js': '52439265b980', 'overlay.js': '336b30eae325', 'ov-home.js': '6252acb43bd4', '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">.@@@ -85,7 +85,7 @@box-shadow: inset 0 0 0 1px var(--border, rgba(255,255,255,.1)); transition: transform .18s cubic-bezier(.2,1.4,.4,1); }.ovh-recent .ovh-ic { width: 48px; height: 48px; border-radius: 15px; }/* App icons: the ring goes, the glyph fills the tile, each app keeps its own hue (11-09). */--.ovh-grid .ovh-ic, .ovh-recent .ovh-ic, .ovh-card .ovh-ic { background: linear-gradient(150deg, hsl(var(--ovh-h, 215) 72% 58% / .34), hsl(var(--ovh-h, 215) 70% 38% / .18)); box-shadow: inset 0 0 0 1px hsl(var(--ovh-h, 215) 70% 64% / .28), 0 4px 14px hsl(var(--ovh-h, 215) 70% 30% / .18); overflow: hidden; }++.ovh-grid .ovh-ic, .ovh-recent .ovh-ic, .ovh-card .ovh-ic, .ovh-key .ovh-ic { background: linear-gradient(150deg, hsl(var(--ovh-h, 215) 72% 58% / .34), hsl(var(--ovh-h, 215) 70% 38% / .18)); box-shadow: inset 0 0 0 1px hsl(var(--ovh-h, 215) 70% 64% / .28), 0 4px 14px hsl(var(--ovh-h, 215) 70% 30% / .18); overflow: hidden; }.ovh-ic .ovi-ring, .ovh-ic .ovi-bg, .ovh-ic .ovi-comet, .ovh-ic .ovi-prog { display: none; }.ovh-ic .ov-icon { --ovi-glyph: hsl(var(--ovh-h, 215) 90% 82%); color: hsl(var(--ovh-h, 215) 90% 82%); }:root[data-theme="light"] .ovh-ic .ov-icon { --ovi-glyph: hsl(var(--ovh-h, 215) 70% 32%); color: hsl(var(--ovh-h, 215) 70% 32%); }@@@ -184,18 +184,20 @@ html[data-ov-motion=reduced] .ovh.is-edit .ovh-grid:not(.ovh-recent) .ovh-ic { a.ovh-lives a { display: inline-flex; align-items: center; gap: 6px; min-height: 36px; padding: 0 12px; border-radius: 999px; background: color-mix(in srgb, var(--live, #ef4444) 12%, transparent); color: inherit; text-decoration: none; font-size: 13px; font-weight: 650; }.ovh-lives a::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: var(--live, #ef4444); }--/* You (11-09, the owner: "the U tab still sucks"): you on top, with Display and Settings beside your name; what you were-- doing as a rail of cards; what you use most as one bar; then your home. */--.ovh-me { display: flex; align-items: center; gap: 10px; margin: 4px 4px 8px; padding: 10px 10px 10px 12px; border-radius: 20px;++/* You (11-09, the owner: "the U tab still sucks"; 11-10: "Your home should be above Jump back in… the thing at the top++ a little more compact… links to whatever the user uses the most"): you on top in one short bar, with Display and++ Settings beside your name; then your home; then what you use most, as shortcuts under one bar; then where you were++ last, as a rail of cards. */++.ovh-me { display: flex; align-items: center; gap: 6px; margin: 2px 4px 6px; padding: 4px 4px 4px 8px; border-radius: 16px;background: radial-gradient(120% 140% at 0% 0%, color-mix(in srgb, var(--accent, #3b82f6) 20%, transparent), transparent 60%), color-mix(in srgb, var(--text-primary, #e8ecf3) 4%, transparent);box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent, #3b82f6) 18%, var(--border, rgba(255,255,255,.08))); }--.ovh-who { flex: 1; min-width: 0; display: flex; align-items: center; gap: 12px; min-height: 48px; border-radius: 14px; color: inherit; text-decoration: none; }--.ovh-av { position: relative; flex: none; display: grid; place-items: center; width: 48px; height: 48px; border-radius: 50%; background: linear-gradient(135deg, var(--accent, #3b82f6), color-mix(in srgb, var(--accent, #3b82f6) 50%, #a855f7)); color: #fff; font-size: 19px; font-weight: 800; }++.ovh-who { flex: 1; min-width: 0; display: flex; align-items: center; gap: 10px; min-height: 44px; border-radius: 12px; color: inherit; text-decoration: none; }++.ovh-av { position: relative; flex: none; display: grid; place-items: center; width: 36px; height: 36px; border-radius: 50%; background: linear-gradient(135deg, var(--accent, #3b82f6), color-mix(in srgb, var(--accent, #3b82f6) 50%, #a855f7)); color: #fff; font-size: 15px; font-weight: 800; }.ovh-av img { width: 100%; height: 100%; border-radius: 50%; object-fit: cover; }--.ovh-av::after { content: ""; position: absolute; right: -1px; bottom: -1px; width: 12px; height: 12px; border-radius: 50%; background: var(--success, #22c55e); box-shadow: 0 0 0 3px var(--bg-secondary, #141a24); }--.ovh-who-txt { min-width: 0; display: grid; gap: 2px; }--.ovh-who-txt b { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 16.5px; font-weight: 800; letter-spacing: -.01em; }--.ovh-who-txt small { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12.5px; color: var(--text-secondary, #a8b3c4); }++.ovh-av::after { content: ""; position: absolute; right: -1px; bottom: -1px; width: 10px; height: 10px; border-radius: 50%; background: var(--success, #22c55e); box-shadow: 0 0 0 2px var(--bg-secondary, #141a24); }++.ovh-who-txt { min-width: 0; display: grid; gap: 0; line-height: 1.25; }++.ovh-who-txt b { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 15px; font-weight: 800; letter-spacing: -.01em; }++.ovh-who-txt small { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px; color: var(--text-secondary, #a8b3c4); }.ovh-ib { flex: none; display: grid; place-items: center; width: 44px; height: 44px; padding: 0; border: 0; border-radius: 50%; cursor: pointer; text-decoration: none;background: color-mix(in srgb, var(--text-primary, #e8ecf3) 8%, transparent); color: var(--text-primary, #e8ecf3); transition: background .16s, color .16s, transform .16s cubic-bezier(.2,1.4,.4,1); }.ovh-ib svg { display: block; width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-width: 1.9; stroke-linecap: round; stroke-linejoin: round; }@@@ -221,14 +223,19 @@ html[data-ov-motion=reduced] .ovh.is-edit .ovh-grid:not(.ovh-recent) .ovh-ic { a.ovh-live { flex: none; padding: 2px 6px; border-radius: 6px; background: var(--live, #ef4444); color: #fff; font-size: 10px; font-weight: 800; letter-spacing: .05em; line-height: 1.2; }@media (hover: hover) { .ovh-card:hover { background: var(--bg-hover, rgba(255,255,255,.08)); } }@media (min-width: 861px) { .ovh-card { width: 232px; } }--.ovh-use { margin: 0 4px 8px; padding: 12px 12px 6px; border-radius: 16px; background: color-mix(in srgb, var(--text-primary, #e8ecf3) 4%, transparent); }++.ovh-use { margin: 0 4px 8px; padding: 12px 12px 4px; border-radius: 16px; background: color-mix(in srgb, var(--text-primary, #e8ecf3) 4%, transparent); }.ovh-use-bar { display: flex; gap: 3px; height: 12px; border-radius: 999px; overflow: hidden; }.ovh-use-bar > span { flex: var(--n, 1) 1 0; min-width: 8px; background: var(--c, #3b82f6); transform-origin: left center; animation: ovh-grow .7s cubic-bezier(.2,.8,.2,1) both; animation-delay: calc(var(--i, 0) * 60ms); }--.ovh-use-keys { display: grid; grid-template-columns: repeat(auto-fill, minmax(104px, 1fr)); gap: 0 4px; margin: 6px -6px 0; }--.ovh-key { display: flex; align-items: center; gap: 7px; min-width: 0; min-height: 38px; padding: 0 8px; border-radius: 10px; color: inherit; text-decoration: none; font-size: 13px; font-weight: 650; white-space: nowrap; }--.ovh-key::before { content: ""; flex: none; width: 10px; height: 10px; border-radius: 3px; background: var(--c, #3b82f6); }--.ovh-key small { color: var(--text-muted, #93a0b4); font-size: 12px; font-weight: 600; font-variant-numeric: tabular-nums; }--@media (hover: hover) { .ovh-key:hover { background: var(--bg-hover, rgba(255,255,255,.07)); } }++/* The bar's key is the apps themselves: each one its icon (a way in, not a legend), its name, and how often, with the++ bar's own colour beside the number. Five across, as the home grid is. */++.ovh-use-apps { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 2px; margin: 8px -8px 0; }++.ovh-key { display: grid; justify-items: center; align-content: start; gap: 3px; min-width: 0; padding: 6px 0; border-radius: 14px; color: inherit; text-decoration: none; -webkit-tap-highlight-color: transparent; }++.ovh-key .ovh-ic { width: 44px; height: 44px; border-radius: 14px; transition: transform .16s cubic-bezier(.2,1.4,.4,1); }++.ovh-key b { max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 11.5px; font-weight: 650; letter-spacing: -.01em; color: var(--text-secondary, #a8b3c4); }++.ovh-key small { display: inline-flex; align-items: center; gap: 4px; color: var(--text-muted, #93a0b4); font-size: 11.5px; font-weight: 600; font-variant-numeric: tabular-nums; line-height: 1.2; }++.ovh-key small::before { content: ""; flex: none; width: 7px; height: 7px; border-radius: 50%; background: var(--c, #3b82f6); }++.ovh-key:active .ovh-ic { transform: scale(calc(1 - .08 * var(--ov-move, 1))); }++@media (hover: hover) { .ovh-key:hover { background: var(--bg-hover, rgba(255,255,255,.07)); } .ovh-key:hover b { color: var(--text-primary, #e8ecf3); } }.ovh-sec small { flex: none; font-size: 11px; font-weight: 600; letter-spacing: 0; text-transform: none; color: var(--text-muted, #93a0b4); }@keyframes ovh-rise { from { opacity: 0; transform: translateY(6px) scale(.98); } }@keyframes ovh-grow { from { transform: scaleX(0); } }@@@ -419,6 +426,7 @@ html[data-ov-motion=reduced] .ovh-card { animation-name: ovh-fade; animation-durif (!items.length) return null;const wrap = h('div');const head = sec('Jump back in');++ head.append(h('small', '', 'where you were last'));wrap.append(head);const rail = h('div', 'ovh-rail'); rail.setAttribute('role', 'list'); rail.setAttribute('aria-label', 'Jump back in');items.forEach((x, i) => {@@@ -441,7 +449,7 @@ html[data-ov-motion=reduced] .ovh-card { animation-name: ovh-fade; animation-durreturn wrap;}-- /** Most used: the pages you opened, by site, as one bar and its key. */++ /** Most used: the pages you opened, by site, as one bar; its key is the apps themselves, each a way in. */function usage() {const rows = (o.usage || []).map((u) => ({ a: L.byId.get(u.id), n: u.count })).filter((r) => r.a && r.n > 0).sort((x, y) => y.n - x.n).slice(0, 5);if (rows.length < 2) return null;@@@ -454,13 +462,16 @@ html[data-ov-motion=reduced] .ovh-card { animation-name: ovh-fade; animation-durconst box = h('div', 'ovh-use');const bar = h('div', 'ovh-use-bar'); bar.setAttribute('role', 'img');bar.setAttribute('aria-label', rows.map((r) => `${r.a.title} ${r.n}`).join(', '));-- const keys = h('div', 'ovh-use-keys');++ const keys = h('div', 'ovh-use-apps'); keys.setAttribute('role', 'list');const CHART = ['#3b82f6', '#a855f7', '#f59e0b', '#14b8a6', '#ec4899']; // five that tell apart in dark and lightrows.forEach((r, i) => {const c = CHART[i % CHART.length];const seg = h('span'); seg.style.setProperty('--n', String(r.n)); seg.style.setProperty('--c', c); seg.style.setProperty('--i', String(i)); bar.append(seg);-- const k = h('a', 'ovh-key', r.a.title); k.href = safe(r.a.url) || '#'; k.style.setProperty('--c', c);-- k.append(h('small', '', num(r.n)));++ const k = h('a', 'ovh-key'); k.href = safe(r.a.url) || '#'; k.style.setProperty('--c', c); k.setAttribute('role', 'listitem');++ k.setAttribute('aria-label', `${r.a.title}, opened ${num(r.n)} ${r.n === 1 ? 'time' : 'times'}`);++ const ic = h('span', 'ovh-ic'); ic.style.setProperty('--ovh-h', String(hueOf(r.a.site || r.a.id))); ic.append(siteIcon(r.a.site, 44));++ k.append(ic, h('b', '', r.a.title), h('small', '', num(r.n)));++ k.addEventListener('click', () => { if (o.onOpen) o.onOpen(r.a); });keys.append(k);});box.append(bar, keys);@@@ -477,7 +488,6 @@ html[data-ov-motion=reduced] .ovh-card { animation-name: ovh-fade; animation-durconst calm = (x) => { x.classList.add('ovh-calm'); x.inert = state.edit; return x; };if (signedIn(o)) el.append(calm(me()));if (signedIn(o)) { actorBox = actorBox || h('div'); el.append(actorBox); }-- { const b = backRail(); if (b) el.append(calm(b)); const u = usage(); if (u) el.append(calm(u)); }const head = sec(L.arranged || signedIn(o) ? 'Your home' : 'Popular on OpenVibe');const edit = h('button', state.edit ? 'ovh-edit is-done' : 'ovh-edit');edit.type = 'button';@@@ -497,6 +507,8 @@ html[data-ov-motion=reduced] .ovh-card { animation-name: ovh-fade; animation-durconst all = h('button', 'ovh-all', `All apps (${total})`); all.type = 'button';all.addEventListener('click', () => { state.view = 'drawer'; paint(); });el.append(all);++ // Under your home: what you use most (shortcuts), then where you were last (history).++ { const u = usage(); if (u) el.append(calm(u)); const b = backRail(); if (b) el.append(calm(b)); }icons();if (signedIn(o) && actorBox && !actorBox.childNodes.length) actor(actorBox, o);}@@@ -1,12 +1,12 @@{"name": "openvibe-shared",-- "version": "4.183.0",++ "version": "4.185.0","lockfileVersion": 3,"requires": true,"packages": {"": {"name": "openvibe-shared",-- "version": "4.184.0",++ "version": "4.185.0","license": "MIT","dependencies": {"jsonwebtoken": "^9.0.0",@@@ -1,6 +1,6 @@{"name": "openvibe-shared",-- "version": "4.184.0",++ "version": "4.185.0","description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site","main": "index.js","bin": {@@@ -428,8 +428,9 @@ async function drag(p, from, to) {await wait(300);assert.strictEqual(await p.evaluate("!!document.querySelector('.ovdk:not([hidden])')"), true, 'the opening tap does not close the sheet');await p.evaluate("document.body.click()");-- await wait(150);-- assert.strictEqual(await p.evaluate("!!document.querySelector('.ovdk:not([hidden])')"), false, 'a later tap outside closes it');++ // A phone's sheet folds back into the navbar's orb before it goes (4.185.0: 240 ms), so it is gone a moment++ // after the tap, not in the same instant.++ await until(p, "!document.querySelector('.ovdk:not([hidden])')", 'a later tap outside closes it');await share();await until(p, "!!document.querySelector('.ovdk .ovdk-share')", 'the bar again');// Cancel: back to the list, nothing waiting.@@@ -21,7 +21,7 @@ const DOCK_PATH = path.join(ROOT, 'dock.js');// time something is sent, not on every page.// The timer itself (menu, countdown, chime, focus rounds) is dock-timer.js (3.7 KB brotli), loaded only while a timer// runs or its menu opens; the header's tab rail and narrow-width rules came with it.--const DOCK_BROTLI_BUDGET = 19456; // 19.00 KB: 18.63 KB with Dock v2's AI chats row, the conversation list, the group faces and the maker's marks (2026-11-09); 18.50 KB: 18.1 KB with the Actor tab, the sheets' shared surface and Dock chats from a site's session (4.162.0, 2026-11-09); 18.34 KB measured on 2026-11-09 with Dock v2's Chats summary and its lazy Agents rows; 18.00 KB: 17.9 KB with the orb's × as Close, dots drawn on the centre line and the pop from the pill (4.161.0, 2026-11-09); 17.50 KB: 17.28 KB with the full-screen phone Dock, the one-row header and its More menu (2026-11-09, owner: the pin unclear, pop-overs that did not close) + headroom; 16.80 KB: 16.55 KB with the phone header's two rows (4.150.0, 2026-11-08: the open tab read "Ac") + headroom; 16.40 KB: 16.30 KB with the link cards' loader (4.147.0, 2026-11-08; the card itself is dock-card.js, loaded the first time one shows) + headroom; 16.25 KB: 16.07 KB with OpenVibe Chat's hooks (share(), canSend(), the draft; 4.144.0, 2026-11-08), Send to… itself is dock-share.js (1.9 KB, loaded on first use); 16.0 KB: 15.74 KB with its siblings' ?v= content hashes (4.137.0, 2026-11-07) + headroom; 15.70 KB: 15.55 KB with Ask your agent (4.135.0, 2026-11-07) + headroom; was 15.50 KB (15435 after 4.120.0)++const DOCK_BROTLI_BUDGET = 20480; // 20.00 KB: 19.80 KB with a phone's sheet opening out of the navbar's orb (4.185.0, 2026-11-10: the hand-off's styles and its two functions, about 0.95 KB; it runs as the sheet opens, so it cannot be a sibling loaded later); 19.00 KB: 18.63 KB with Dock v2's AI chats row, the conversation list, the group faces and the maker's marks (2026-11-09); 18.50 KB: 18.1 KB with the Actor tab, the sheets' shared surface and Dock chats from a site's session (4.162.0, 2026-11-09); 18.34 KB measured on 2026-11-09 with Dock v2's Chats summary and its lazy Agents rows; 18.00 KB: 17.9 KB with the orb's × as Close, dots drawn on the centre line and the pop from the pill (4.161.0, 2026-11-09); 17.50 KB: 17.28 KB with the full-screen phone Dock, the one-row header and its More menu (2026-11-09, owner: the pin unclear, pop-overs that did not close) + headroom; 16.80 KB: 16.55 KB with the phone header's two rows (4.150.0, 2026-11-08: the open tab read "Ac") + headroom; 16.40 KB: 16.30 KB with the link cards' loader (4.147.0, 2026-11-08; the card itself is dock-card.js, loaded the first time one shows) + headroom; 16.25 KB: 16.07 KB with OpenVibe Chat's hooks (share(), canSend(), the draft; 4.144.0, 2026-11-08), Send to… itself is dock-share.js (1.9 KB, loaded on first use); 16.0 KB: 15.74 KB with its siblings' ?v= content hashes (4.137.0, 2026-11-07) + headroom; 15.70 KB: 15.55 KB with Ask your agent (4.135.0, 2026-11-07) + headroom; was 15.50 KB (15435 after 4.120.0)const SIBLINGS = ['action-center.js', 'notification-ui.js', 'notification-live.js', 'ov-mark.js', 'dock-vibe.js', 'dock-runs.js', 'ai-marks-client.js', 'dock-timer.js', 'dock-agent.js', 'dock-share.js', 'dock-card.js'];// ── the budget ──────────────────────────────────────────────@@@ -179,7 +179,7 @@ const server = http.createServer((req, res) => {await openTab(s, 'you');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; Jump back in as cards; Most used as one bar.++ // 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');assert.ok(await s.evaluate(`!document.querySelector('.ovs-displayrow')`), 'Display is not at the foot any more');@@@ -187,6 +187,16 @@ const server = http.createServer((req, res) => {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-use .ovh-key')].map((k) => k.textContent)`), ['Live5', 'Pics2'], 'Most used: pages opened per site');assert.strictEqual(await s.evaluate(`(document.querySelector('.ovh-sec a.ovh-link[href="https://openvibe.report/me"]') || {}).textContent`), 'Your stats', 'Most used links to your stats on Report');++ // The owner, 2026-11-10: "Your home should be above Jump back in… the thing at the top a little more compact… links++ // 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.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');++ assert.strictEqual(await s.evaluate(`(document.querySelector('.ovh-rail').parentElement.querySelector('.ovh-sec small') || {}).textContent`), 'where you were last', 'Jump back in says what it is');assert.strictEqual(await s.evaluate(`(document.querySelector('.ovh-sec:not(:first-child) span, .ovh-grid:not(.ovh-recent)') && [...document.querySelectorAll('.ovh-sec span')].map((x) => x.textContent).find((x) => /home|Popular/i.test(x)))`), 'Your home', 'signed in, Home is theirs from the start');assert.deepStrictEqual(await s.evaluate(`[...document.querySelectorAll('.ovh-folder[data-id="folder:tools"] .ov-icon')].map((i) => i.getAttribute('data-icon'))`), ['pdf', 'image', 'qr', 'audio'], 'the Tools folder shows its tools\' own icons');await s.evaluate(`document.querySelector('.ovh-sec button').click(), true`);@@@ -214,6 +224,32 @@ const server = http.createServer((req, res) => {assert.deepStrictEqual(sent.map((c) => c.u), ['attachments', 'attachments', 'posts'], 'each picture, then the post');assert.ok(sent.every((c) => c.auth === 'Bearer tok-1'), 'with the person\'s token');assert.deepStrictEqual(JSON.parse(sent[2].body).pictures.map((x) => x.alt), ['A dark sky', 'The same sky']);++ // ── A phone's Dock opens out of the navbar's menu button, and that button is its close button (the owner,++ // 2026-11-10: "that close button should… fade and have an animation effect from the top right, the menu button++ // that already exists"). The close orb starts on the navbar's orb and settles into its x; the navbar's orb makes++ // the same move underneath; closing runs it back, then the sheet is gone. With motion off there is no move.++ await s.evaluate(`(document.querySelector('.ovs.is-open [data-close], .ovs-orb') || { click() {} }).click(), true`);++ await new Promise((ok) => setTimeout(ok, 700));++ assert.ok(await until(s, `!!(window.OpenVibeDock && window.OpenVibeDock.open)`), 'the Dock is loaded beside the navbar');++ const start = await s.evaluate(`(() => { const c = (e) => { const r = e.getBoundingClientRect(); return [r.left + r.width / 2, r.top + r.height / 2]; };++ const nav = [...document.querySelectorAll('.openvibe-navbar .ovnav-orb')].find((b) => b.getBoundingClientRect().width > 0);++ const before = c(nav.querySelector('svg'));++ window.OpenVibeDock.open({ tab: 'activity' });++ const dock = document.querySelector('.ovdk'), mine = dock.querySelector('.ovdk-orbx');++ return { before, at: c(mine.querySelector('svg')), fromOrb: dock.classList.contains('from-orb'), isFrom: mine.classList.contains('is-from') }; })()`);++ assert.ok(start.fromOrb && start.isFrom, 'the sheet opens from the orb');++ assert.ok(Math.abs(start.at[0] - start.before[0]) <= 1.5 && Math.abs(start.at[1] - start.before[1]) <= 1.5, `the close orb starts on the navbar's orb (${JSON.stringify(start)})`);++ await new Promise((ok) => setTimeout(ok, 900));++ const open = await s.evaluate(`(() => { const dock = document.querySelector('.ovdk'), mine = dock.querySelector('.ovdk-orbx'), sv = mine.querySelector('svg'), r = sv.getBoundingClientRect(), b = mine.getBoundingClientRect();++ return { settled: getComputedStyle(sv).transform === 'none' && !mine.classList.contains('is-from'), centred: Math.abs((r.left + r.width / 2) - (b.left + b.width / 2)) <= 1, navIsDock: !!document.querySelector('.openvibe-navbar .ovnav-orb.is-dock'), opacity: getComputedStyle(dock).opacity }; })()`);++ assert.ok(open.settled && open.centred, `it settles into its place in the header (${JSON.stringify(open)})`);++ assert.ok(open.navIsDock && open.opacity === '1', 'the navbar\'s orb made the same move underneath');++ const closing = await s.evaluate(`(() => { window.OpenVibeDock.close(); const dock = document.querySelector('.ovdk'); return { hidden: dock.hidden, out: dock.classList.contains('is-out'), back: dock.querySelector('.ovdk-orbx').classList.contains('is-from'), navIsDock: !!document.querySelector('.ovnav-orb.is-dock') }; })()`);++ assert.ok(!closing.hidden && closing.out && closing.back && !closing.navIsDock, `closing folds it back into the orb first (${JSON.stringify(closing)})`);++ assert.ok(await until(s, `document.querySelector('.ovdk').hidden`), 'then the sheet is gone');++ assert.ok(await s.evaluate(`!document.querySelector('.ovdk').classList.contains('from-orb') && !document.querySelector('.ovnav-orb.is-dock') && [...document.querySelectorAll('.openvibe-navbar .ovnav-orb')].every((b) => !b.style.transition)`), 'and the navbar\'s orb is as it was');++ const still = await s.evaluate(`(() => { document.documentElement.setAttribute('data-ov-motion', 'off'); window.OpenVibeDock.open({ tab: 'activity' }); const dock = document.querySelector('.ovdk'); const a = dock.classList.contains('from-orb'); window.OpenVibeDock.close(); const out = { fromOrb: a, hidden: dock.hidden }; document.documentElement.removeAttribute('data-ov-motion'); return out; })()`);++ assert.ok(!still.fromOrb && still.hidden, 'motion off: no move, and it closes at once');await s.close();console.log('menu-home-screen-chrome: home grid, long-press and drag, keys, remove and put back, folders with tool icons, the wall, today, the composer with pictures, Network sync ok');} finally {
@@ -1,5 +1,39 @@
@@ -1,5 +1,39 @@
# Changelog
# Changelog
+## 4.185.0 — 2026-11-10
+
+**The You tab: your home first, a shorter bar, Most used as shortcuts.** The owner, 2026-11-10: "Your home should be
+above Jump back in… the thing at the top with your username and online and the settings… a little more compact… most
+used is cool, but maybe we should actually have links to whatever the user uses the most… so it's just easier to jump
+to the app."
+
+- **The order** is you, your own AI, **Your home**, Most used, Jump back in (it was you, AI, Jump back in, Most used,
+ Your home). Nothing above the grid comes or goes while Home is being arranged, as before.
+- **The bar with your name** is 52px tall (was 68): a 36px picture, name and handle on two tight lines. Display and
+ Settings keep their 44px.
+- **Most used** keeps its one bar; its key is now the apps themselves, five across: each app's icon in its own hue, its
+ name, and how often with the bar's colour beside the number. Each is a link (`.ovh-key`, "Live, opened 183 times") and
+ a full touch target; it was a row of small legend entries.
+- **Jump back in** comes last and says what it is: "where you were last".
+
+**A phone's Dock opens out of the navbar's menu button, and that button is its close button.** The owner, 2026-11-10:
+"that close button should, instead of coming from that menu, fade and have an animation effect from the top right, the
+menu button that already exists… it's supposed to be nicer connected to this." Until now the sheet rose from the bottom
+and carried its own close button up with it.
+
+- On a phone (640px and under) with the orb at the bar's right, the sheet fades and grows from the orb's centre
+ (`.ovdk.from-orb`); its close orb starts exactly on the navbar's orb, as its nine dots, and settles into its x in the
+ header (a glide of a few pixels and the same turn the main menu's orb makes). The navbar's own orb makes the same
+ move underneath (`.ovnav-orb.is-dock`), so it is one button the whole way. The x wears the menu's colours.
+- **Closing runs it back:** the x turns into the dots and returns to the bar while the sheet fades (240 ms), then the
+ sheet is gone and the button is where it always was. `OpenVibeDock.close()` therefore hides a phone's sheet a moment
+ later than it is called; with motion off, in a window of its own, or on a bar without the orb, it hides at once as
+ before (and such a bar keeps the rise from the bottom).
+- Calm keeps the move (the grow is smaller, by `--ov-move`); only motion off skips it.
+
+`test/menu-home-screen-chrome.test.js` holds both: the order, the bar's height, the shortcuts' links and targets; the
+close orb starting on the navbar's orb, settling, folding back, and standing still with motion off.
+
## 4.184.0 — 2026-11-10
## 4.184.0 — 2026-11-10
- **Server-rendered navbar links.** The shared shell emits the configured navbar links in the reserved `#navbar-mount`
- **Server-rendered navbar links.** The shared shell emits the configured navbar links in the reserved `#navbar-mount`
@@ -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.184.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed
+**Status:** alpha, v4.185.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,13 +4,14 @@
@@ -4,13 +4,14 @@
"code": true,
"code": true,
"wave": "W2",
"wave": "W2",
"package": "openvibe-shared",
"package": "openvibe-shared",
- "version": "4.184.0",
+ "version": "4.185.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)",
"contracts": "openvibe-contracts v0.384.0 (devDependency: the release manifest contract)",
"contracts": "openvibe-contracts v0.384.0 (devDependency: the release manifest contract)",
"sdk": "openvibe-sdk v0.133.0",
"sdk": "openvibe-sdk v0.133.0",
"features": [
"features": [
+ "the menu's You tab: your home right under your name (a 52px bar, was 68) and the AI card; Most used is the apps themselves under their bar, each a link and a full touch target; Jump back in last, saying what it is. A phone's Dock opens out of the navbar's menu button: its close orb starts on that orb and settles into its x while the navbar's orb makes the same move underneath, and closing runs it back (4.185.0)",
"motion has four choices (Automatic, Full, Calm, Off) and one signal, html[data-ov-motion]: Calm keeps every effect and makes it smaller and slower (--ov-move, display.move(), endless loops at 0.6 of their speed), and only Off stops motion; no rule in the package stops an effect because the device asked for less (4.183.0)",
"motion has four choices (Automatic, Full, Calm, Off) and one signal, html[data-ov-motion]: Calm keeps every effect and makes it smaller and slower (--ov-move, display.move(), endless loops at 0.6 of their speed), and only Off stops motion; no rule in the package stops an effect because the device asked for less (4.183.0)",
"every shape glyph has a solid drawing (203 of 278; the other 75 are line glyphs that thicken when small), glyphs.svg() draws a solid's line parts as lines, and every product id has its own site mark, so no product draws the bare V (4.183.0)",
"every shape glyph has a solid drawing (203 of 278; the other 75 are line glyphs that thicken when small), glyphs.svg() draws a solid's line parts as lines, and every product id has its own site mark, so no product draws the bare V (4.183.0)",
"the Dock's agent view, timeline and Actor ask Network's /api/frame relay instead of openvibe.codes and openvibe.actor, so a signed-in page logs no cross-origin console errors; an explicit base still reaches the product (4.181.0)",
"the Dock's agent view, timeline and Actor ask Network's /api/frame relay instead of openvibe.codes and openvibe.actor, so a signed-in page logs no cross-origin console errors; an explicit base still reaches the product (4.181.0)",
@@ -193,6 +193,27 @@
@@ -193,6 +193,27 @@
.ovdk-orbx:active .ovdk-ox { transform: rotate(270deg) scale(.7); }
.ovdk-orbx:active .ovdk-ox { transform: rotate(270deg) scale(.7); }
@keyframes ovdk-ox-in { from { transform: rotate(45deg) scale(.8); } }
@keyframes ovdk-ox-in { from { transform: rotate(45deg) scale(.8); } }
@keyframes ovdk-o1-out { from { opacity: 1; transform: scale(1); } }
@keyframes ovdk-o1-out { from { opacity: 1; transform: scale(1); } }
+ /* A phone's sheet opens out of the navbar's orb, which becomes its close button (4.185.0; see CHANGELOG). */
+ .ovdk.from-orb { transform-origin: var(--ovdk-ox, 100%) var(--ovdk-oy, 0%) !important; }
+ .ovdk.from-orb.is-in { animation: ovdk-from-orb .3s cubic-bezier(.2,.9,.25,1) both; }
+ .ovdk.from-orb.is-out { animation: ovdk-to-orb .24s ease both; pointer-events: none; }
+ @keyframes ovdk-from-orb { from { opacity: 0; transform: scale(calc(1 - .06 * var(--ov-move, 1))); } }
+ @keyframes ovdk-to-orb { to { opacity: 0; transform: scale(calc(1 - .04 * var(--ov-move, 1))); } }
+ .from-orb.is-in .ovdk-ox, .from-orb.is-in .ovdk-o1 { animation: none !important; }
+ .from-orb .ovdk-orbx { color: var(--text-primary, #ececf1); }
+ .from-orb .ovdk-orbx svg { transition: transform .42s cubic-bezier(.2, 1.1, .3, 1); }
+ .from-orb .ovdk-ox circle { transition: fill .3s, opacity .3s, transform .35s cubic-bezier(.2, 1.4, .3, 1); }
+ .from-orb .ovdk-ox :is(.ovdk-o0, .ovdk-o2), .from-orb .is-from .ovdk-ox .ovdk-o0 { fill: var(--accent-light, #60a5fa); }
+ .ovdk-orbx.is-set, .ovdk-orbx.is-set * { transition: none !important; }
+ .from-orb .is-from svg { transform: translate(var(--orb-dx, 0), var(--orb-dy, 0)) scale(var(--orb-s, 1)); }
+ .from-orb .is-from .ovdk-ox { transform: rotate(45deg) scale(.8); }
+ .from-orb .is-from .ovdk-ox circle { fill: var(--text-secondary, #a8b3c4); opacity: 1; transform: none; }
+ .from-orb .is-from .ovdk-ox .ovdk-o2 { opacity: .6; }
+ .ovnav-orb.is-dock { transform: translate(var(--dock-dx, 0), var(--dock-dy, 0)) scale(var(--dock-s, 1)) !important; color: var(--text-primary, #e6edf7) !important; }
+ .ovnav-orb.is-dock .ovo-g { transform: rotate(180deg) scale(.86); transition-duration: .45s; }
+ .ovnav-orb.is-dock .ovo-d { animation: none; }
+ .ovnav-orb.is-dock .ovo-r1 { opacity: 0; transform: scale(0); }
+ .ovnav-orb.is-dock :is(.ovo-r0, .ovo-r2) { fill: var(--ovo-a); opacity: 1; }
.ovdk-more { position: absolute; z-index: 6; right: 8px; top: calc(100% - 4px); width: min(320px, calc(100% - 16px)); display: grid; gap: 2px; padding: 6px; border-radius: 16px;
.ovdk-more { position: absolute; z-index: 6; right: 8px; top: calc(100% - 4px); width: min(320px, calc(100% - 16px)); display: grid; gap: 2px; padding: 6px; border-radius: 16px;
border: 1px solid var(--border, rgba(255,255,255,.12)); background: var(--bg-elevated, var(--bg-secondary, #1d1f27)); box-shadow: 0 18px 40px -12px rgba(0,0,0,.55); cursor: default; }
border: 1px solid var(--border, rgba(255,255,255,.12)); background: var(--bg-elevated, var(--bg-secondary, #1d1f27)); box-shadow: 0 18px 40px -12px rgba(0,0,0,.55); cursor: default; }
.ovdk-more[hidden] { display: none; }
.ovdk-more[hidden] { display: none; }
@@ -944,6 +965,39 @@
@@ -944,6 +965,39 @@
// Ask your agent (4.135.0): Vibe opens with the question in its box, unsent.
// Ask your agent (4.135.0): Vibe opens with the question in its box, unsent.
function ask({ text = '' } = {}, n = 0) { if (!n) open({ tab: 'vibe' }); if (vibe.api && vibe.api.ask) vibe.api.ask(text); else if (n < 40) setTimeout(() => ask({ text }, n + 1), 100); }
function ask({ text = '' } = {}, n = 0) { if (!n) open({ tab: 'vibe' }); if (vibe.api && vibe.api.ask) vibe.api.ask(text); else if (n < 40) setTimeout(() => ask({ text }, n + 1), 100); }
function isOpen() { return !!(el && !el.hidden); }
function isOpen() { return !!(el && !el.hidden); }
+ // A phone's sheet and the navbar's orb are one button (.from-orb).
+ let orbLink = null, closing = 0;
+ function navOrb() {
+ if (!phone()) return null;
+ const list = document.querySelectorAll('.openvibe-navbar .ovnav-orb');
+ for (let i = 0; i < list.length; i++) { const r = list[i].getBoundingClientRect(); if (r.width && r.left > root.innerWidth / 2 && r.top < 120 && r.bottom > 0) return list[i]; }
+ return null;
+ }
+ function fromOrb() {
+ unlinkOrb();
+ const nav = navOrb(), mine = el.querySelector('.ovdk-orbx'), mineSvg = mine && mine.querySelector('svg');
+ if (!nav || !mineSvg || reduced() || popped) return;
+ const a = (nav.querySelector('svg') || nav).getBoundingClientRect(), b = mineSvg.getBoundingClientRect(), box = el.getBoundingClientRect();
+ if (!a.width || !b.width) return;
+ const ax = a.left + a.width / 2, ay = a.top + a.height / 2, bx = b.left + b.width / 2, by = b.top + b.height / 2, s = a.width / b.width;
+ el.style.setProperty('--ovdk-ox', `${Math.round(ax - box.left)}px`); el.style.setProperty('--ovdk-oy', `${Math.round(ay - box.top)}px`);
+ mine.style.setProperty('--orb-dx', `${(ax - bx).toFixed(1)}px`); mine.style.setProperty('--orb-dy', `${(ay - by).toFixed(1)}px`); mine.style.setProperty('--orb-s', s.toFixed(3));
+ nav.style.setProperty('--dock-dx', `${(bx - ax).toFixed(1)}px`); nav.style.setProperty('--dock-dy', `${(by - ay).toFixed(1)}px`); nav.style.setProperty('--dock-s', (1 / s).toFixed(3));
+ el.classList.add('from-orb'); mine.classList.add('is-set', 'is-from');
+ orbLink = { nav, mine };
+ void mine.offsetWidth; // the starting pose is put on, not animated into
+ mine.classList.remove('is-set');
+ void mine.offsetWidth;
+ nav.style.transition = 'transform .42s cubic-bezier(.2, 1.1, .3, 1), color .3s';
+ root.requestAnimationFrame(() => { if (!orbLink || orbLink.mine !== mine) return; mine.classList.remove('is-from'); nav.classList.add('is-dock'); });
+ }
+ function unlinkOrb() {
+ if (closing) { clearTimeout(closing); closing = 0; }
+ if (el) el.classList.remove('from-orb', 'is-out');
+ if (!orbLink) return;
+ orbLink.nav.classList.remove('is-dock'); orbLink.nav.style.transition = ''; orbLink.mine.classList.remove('is-from', 'is-set');
+ orbLink = null;
+ }
function share({ text = '', title = '' } = {}) {
function share({ text = '', title = '' } = {}) {
const t = String(text || '').trim().slice(0, 4000);
const t = String(text || '').trim().slice(0, 4000);
if (!t) return;
if (!t) return;
@@ -965,8 +1019,10 @@
@@ -965,8 +1019,10 @@
paintPin();
paintPin();
paintPop();
paintPop();
el.hidden = false;
el.hidden = false;
- el.classList.remove('is-in'); void el.offsetWidth; el.classList.add('is-in');
- // On a computer it grows out of its pill (the navbar's shared motion); a phone's sheet rises as before.
+ el.classList.remove('is-in');
+ fromOrb();
+ void el.offsetWidth; el.classList.add('is-in');
+ // On a computer it grows out of its pill; a phone's sheet opens out of the navbar's orb (fromOrb), or rises.
if (!phone() && root.OpenVibeNavbar && root.OpenVibeNavbar.pop) root.OpenVibeNavbar.pop(el, document.getElementById('ovdk-pill'));
if (!phone() && root.OpenVibeNavbar && root.OpenVibeNavbar.pop) root.OpenVibeNavbar.pop(el, document.getElementById('ovdk-pill'));
if (phone()) {
if (phone()) {
el.setAttribute('aria-modal', 'true'); document.documentElement.classList.add('ovdk-sheet-open');
el.setAttribute('aria-modal', 'true'); document.documentElement.classList.add('ovdk-sheet-open');
@@ -979,6 +1035,18 @@
@@ -979,6 +1035,18 @@
}
}
function close() {
function close() {
if (!isOpen()) return;
if (!isOpen()) return;
+ // Fold back into the navbar's orb first, then go.
+ if (orbLink && !closing && !popped && !reduced() && el.classList.contains('from-orb')) {
+ el.classList.remove('is-in'); el.classList.add('is-out');
+ orbLink.mine.classList.add('is-from'); orbLink.nav.classList.remove('is-dock');
+ closing = setTimeout(() => { closing = 0; shut(); }, 240);
+ return;
+ }
+ shut();
+ }
+ function shut() {
+ if (!isOpen()) return;
+ unlinkOrb();
const wasPopped = popped;
const wasPopped = popped;
if (wasPopped) { popped = false; const w = pipWin; pipWin = null; pipClosing = true; try { if (w) w.close(); } catch { /* already gone */ } pipClosing = false; if (el.parentNode !== document.body) document.body.appendChild(el); }
if (wasPopped) { popped = false; const w = pipWin; pipWin = null; pipClosing = true; try { if (w) w.close(); } catch { /* already gone */ } pipClosing = false; if (el.parentNode !== document.body) document.body.appendChild(el); }
const inside = el.contains(document.activeElement);
const inside = el.contains(document.activeElement);
@@ -567,7 +567,7 @@
@@ -567,7 +567,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': '8cc78bde2190', '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': 'b170f6e994fa', 'dock.js': 'd45d3e6e7c39', 'presence.js': '5f195d41f8c5', 'panels.js': 'd90bfe987d81', 'surfaces.js': 'e72f5a304f2d', 'notification-ui.js': 'dc24929109b5', 'history.js': '1e6598f78ba4', 'site-switcher.js': 'd8980cf95157', 'appbar.js': '4e34b7d60372', 'release-watch.js': '25f02a3f0684', 'ov-icons.js': '8e5047755456', 'sso-client.js': '52439265b980', 'overlay.js': '336b30eae325', 'ov-home.js': '093d5979ae12', 'nav-slots.js': '1db19edaeac4', 'ov-wallet.js': '5c4d7fdb448d', 'ov-wallet.css': 'e46f653fa59e' };
+ const SIBLING_REV = { 'ov-mark.js': '8cc78bde2190', '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': 'b170f6e994fa', 'dock.js': '4756db83581e', 'presence.js': '5f195d41f8c5', 'panels.js': 'd90bfe987d81', 'surfaces.js': 'e72f5a304f2d', 'notification-ui.js': 'dc24929109b5', 'history.js': '1e6598f78ba4', 'site-switcher.js': 'd8980cf95157', 'appbar.js': '4e34b7d60372', 'release-watch.js': '25f02a3f0684', 'ov-icons.js': '8e5047755456', 'sso-client.js': '52439265b980', 'overlay.js': '336b30eae325', 'ov-home.js': '6252acb43bd4', '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">.
@@ -85,7 +85,7 @@
@@ -85,7 +85,7 @@
box-shadow: inset 0 0 0 1px var(--border, rgba(255,255,255,.1)); transition: transform .18s cubic-bezier(.2,1.4,.4,1); }
box-shadow: inset 0 0 0 1px var(--border, rgba(255,255,255,.1)); transition: transform .18s cubic-bezier(.2,1.4,.4,1); }
.ovh-recent .ovh-ic { width: 48px; height: 48px; border-radius: 15px; }
.ovh-recent .ovh-ic { width: 48px; height: 48px; border-radius: 15px; }
/* App icons: the ring goes, the glyph fills the tile, each app keeps its own hue (11-09). */
/* App icons: the ring goes, the glyph fills the tile, each app keeps its own hue (11-09). */
-.ovh-grid .ovh-ic, .ovh-recent .ovh-ic, .ovh-card .ovh-ic { background: linear-gradient(150deg, hsl(var(--ovh-h, 215) 72% 58% / .34), hsl(var(--ovh-h, 215) 70% 38% / .18)); box-shadow: inset 0 0 0 1px hsl(var(--ovh-h, 215) 70% 64% / .28), 0 4px 14px hsl(var(--ovh-h, 215) 70% 30% / .18); overflow: hidden; }
+.ovh-grid .ovh-ic, .ovh-recent .ovh-ic, .ovh-card .ovh-ic, .ovh-key .ovh-ic { background: linear-gradient(150deg, hsl(var(--ovh-h, 215) 72% 58% / .34), hsl(var(--ovh-h, 215) 70% 38% / .18)); box-shadow: inset 0 0 0 1px hsl(var(--ovh-h, 215) 70% 64% / .28), 0 4px 14px hsl(var(--ovh-h, 215) 70% 30% / .18); overflow: hidden; }
.ovh-ic .ovi-ring, .ovh-ic .ovi-bg, .ovh-ic .ovi-comet, .ovh-ic .ovi-prog { display: none; }
.ovh-ic .ovi-ring, .ovh-ic .ovi-bg, .ovh-ic .ovi-comet, .ovh-ic .ovi-prog { display: none; }
.ovh-ic .ov-icon { --ovi-glyph: hsl(var(--ovh-h, 215) 90% 82%); color: hsl(var(--ovh-h, 215) 90% 82%); }
.ovh-ic .ov-icon { --ovi-glyph: hsl(var(--ovh-h, 215) 90% 82%); color: hsl(var(--ovh-h, 215) 90% 82%); }
:root[data-theme="light"] .ovh-ic .ov-icon { --ovi-glyph: hsl(var(--ovh-h, 215) 70% 32%); color: hsl(var(--ovh-h, 215) 70% 32%); }
:root[data-theme="light"] .ovh-ic .ov-icon { --ovi-glyph: hsl(var(--ovh-h, 215) 70% 32%); color: hsl(var(--ovh-h, 215) 70% 32%); }
@@ -184,18 +184,20 @@ html[data-ov-motion=reduced] .ovh.is-edit .ovh-grid:not(.ovh-recent) .ovh-ic { a
@@ -184,18 +184,20 @@ html[data-ov-motion=reduced] .ovh.is-edit .ovh-grid:not(.ovh-recent) .ovh-ic { a
.ovh-lives a { display: inline-flex; align-items: center; gap: 6px; min-height: 36px; padding: 0 12px; border-radius: 999px; background: color-mix(in srgb, var(--live, #ef4444) 12%, transparent); color: inherit; text-decoration: none; font-size: 13px; font-weight: 650; }
.ovh-lives a { display: inline-flex; align-items: center; gap: 6px; min-height: 36px; padding: 0 12px; border-radius: 999px; background: color-mix(in srgb, var(--live, #ef4444) 12%, transparent); color: inherit; text-decoration: none; font-size: 13px; font-weight: 650; }
.ovh-lives a::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: var(--live, #ef4444); }
.ovh-lives a::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: var(--live, #ef4444); }
-/* You (11-09, the owner: "the U tab still sucks"): you on top, with Display and Settings beside your name; what you were
- doing as a rail of cards; what you use most as one bar; then your home. */
-.ovh-me { display: flex; align-items: center; gap: 10px; margin: 4px 4px 8px; padding: 10px 10px 10px 12px; border-radius: 20px;
+/* You (11-09, the owner: "the U tab still sucks"; 11-10: "Your home should be above Jump back in… the thing at the top
+ a little more compact… links to whatever the user uses the most"): you on top in one short bar, with Display and
+ Settings beside your name; then your home; then what you use most, as shortcuts under one bar; then where you were
+ last, as a rail of cards. */
+.ovh-me { display: flex; align-items: center; gap: 6px; margin: 2px 4px 6px; padding: 4px 4px 4px 8px; border-radius: 16px;
background: radial-gradient(120% 140% at 0% 0%, color-mix(in srgb, var(--accent, #3b82f6) 20%, transparent), transparent 60%), color-mix(in srgb, var(--text-primary, #e8ecf3) 4%, transparent);
background: radial-gradient(120% 140% at 0% 0%, color-mix(in srgb, var(--accent, #3b82f6) 20%, transparent), transparent 60%), color-mix(in srgb, var(--text-primary, #e8ecf3) 4%, transparent);
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent, #3b82f6) 18%, var(--border, rgba(255,255,255,.08))); }
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent, #3b82f6) 18%, var(--border, rgba(255,255,255,.08))); }
-.ovh-who { flex: 1; min-width: 0; display: flex; align-items: center; gap: 12px; min-height: 48px; border-radius: 14px; color: inherit; text-decoration: none; }
-.ovh-av { position: relative; flex: none; display: grid; place-items: center; width: 48px; height: 48px; border-radius: 50%; background: linear-gradient(135deg, var(--accent, #3b82f6), color-mix(in srgb, var(--accent, #3b82f6) 50%, #a855f7)); color: #fff; font-size: 19px; font-weight: 800; }
+.ovh-who { flex: 1; min-width: 0; display: flex; align-items: center; gap: 10px; min-height: 44px; border-radius: 12px; color: inherit; text-decoration: none; }
+.ovh-av { position: relative; flex: none; display: grid; place-items: center; width: 36px; height: 36px; border-radius: 50%; background: linear-gradient(135deg, var(--accent, #3b82f6), color-mix(in srgb, var(--accent, #3b82f6) 50%, #a855f7)); color: #fff; font-size: 15px; font-weight: 800; }
.ovh-av img { width: 100%; height: 100%; border-radius: 50%; object-fit: cover; }
.ovh-av img { width: 100%; height: 100%; border-radius: 50%; object-fit: cover; }
-.ovh-av::after { content: ""; position: absolute; right: -1px; bottom: -1px; width: 12px; height: 12px; border-radius: 50%; background: var(--success, #22c55e); box-shadow: 0 0 0 3px var(--bg-secondary, #141a24); }
-.ovh-who-txt { min-width: 0; display: grid; gap: 2px; }
-.ovh-who-txt b { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 16.5px; font-weight: 800; letter-spacing: -.01em; }
-.ovh-who-txt small { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12.5px; color: var(--text-secondary, #a8b3c4); }
+.ovh-av::after { content: ""; position: absolute; right: -1px; bottom: -1px; width: 10px; height: 10px; border-radius: 50%; background: var(--success, #22c55e); box-shadow: 0 0 0 2px var(--bg-secondary, #141a24); }
+.ovh-who-txt { min-width: 0; display: grid; gap: 0; line-height: 1.25; }
+.ovh-who-txt b { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 15px; font-weight: 800; letter-spacing: -.01em; }
+.ovh-who-txt small { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px; color: var(--text-secondary, #a8b3c4); }
.ovh-ib { flex: none; display: grid; place-items: center; width: 44px; height: 44px; padding: 0; border: 0; border-radius: 50%; cursor: pointer; text-decoration: none;
.ovh-ib { flex: none; display: grid; place-items: center; width: 44px; height: 44px; padding: 0; border: 0; border-radius: 50%; cursor: pointer; text-decoration: none;
background: color-mix(in srgb, var(--text-primary, #e8ecf3) 8%, transparent); color: var(--text-primary, #e8ecf3); transition: background .16s, color .16s, transform .16s cubic-bezier(.2,1.4,.4,1); }
background: color-mix(in srgb, var(--text-primary, #e8ecf3) 8%, transparent); color: var(--text-primary, #e8ecf3); transition: background .16s, color .16s, transform .16s cubic-bezier(.2,1.4,.4,1); }
.ovh-ib svg { display: block; width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-width: 1.9; stroke-linecap: round; stroke-linejoin: round; }
.ovh-ib svg { display: block; width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-width: 1.9; stroke-linecap: round; stroke-linejoin: round; }
@@ -221,14 +223,19 @@ html[data-ov-motion=reduced] .ovh.is-edit .ovh-grid:not(.ovh-recent) .ovh-ic { a
@@ -221,14 +223,19 @@ html[data-ov-motion=reduced] .ovh.is-edit .ovh-grid:not(.ovh-recent) .ovh-ic { a
.ovh-live { flex: none; padding: 2px 6px; border-radius: 6px; background: var(--live, #ef4444); color: #fff; font-size: 10px; font-weight: 800; letter-spacing: .05em; line-height: 1.2; }
.ovh-live { flex: none; padding: 2px 6px; border-radius: 6px; background: var(--live, #ef4444); color: #fff; font-size: 10px; font-weight: 800; letter-spacing: .05em; line-height: 1.2; }
@media (hover: hover) { .ovh-card:hover { background: var(--bg-hover, rgba(255,255,255,.08)); } }
@media (hover: hover) { .ovh-card:hover { background: var(--bg-hover, rgba(255,255,255,.08)); } }
@media (min-width: 861px) { .ovh-card { width: 232px; } }
@media (min-width: 861px) { .ovh-card { width: 232px; } }
-.ovh-use { margin: 0 4px 8px; padding: 12px 12px 6px; border-radius: 16px; background: color-mix(in srgb, var(--text-primary, #e8ecf3) 4%, transparent); }
+.ovh-use { margin: 0 4px 8px; padding: 12px 12px 4px; border-radius: 16px; background: color-mix(in srgb, var(--text-primary, #e8ecf3) 4%, transparent); }
.ovh-use-bar { display: flex; gap: 3px; height: 12px; border-radius: 999px; overflow: hidden; }
.ovh-use-bar { display: flex; gap: 3px; height: 12px; border-radius: 999px; overflow: hidden; }
.ovh-use-bar > span { flex: var(--n, 1) 1 0; min-width: 8px; background: var(--c, #3b82f6); transform-origin: left center; animation: ovh-grow .7s cubic-bezier(.2,.8,.2,1) both; animation-delay: calc(var(--i, 0) * 60ms); }
.ovh-use-bar > span { flex: var(--n, 1) 1 0; min-width: 8px; background: var(--c, #3b82f6); transform-origin: left center; animation: ovh-grow .7s cubic-bezier(.2,.8,.2,1) both; animation-delay: calc(var(--i, 0) * 60ms); }
-.ovh-use-keys { display: grid; grid-template-columns: repeat(auto-fill, minmax(104px, 1fr)); gap: 0 4px; margin: 6px -6px 0; }
-.ovh-key { display: flex; align-items: center; gap: 7px; min-width: 0; min-height: 38px; padding: 0 8px; border-radius: 10px; color: inherit; text-decoration: none; font-size: 13px; font-weight: 650; white-space: nowrap; }
-.ovh-key::before { content: ""; flex: none; width: 10px; height: 10px; border-radius: 3px; background: var(--c, #3b82f6); }
-.ovh-key small { color: var(--text-muted, #93a0b4); font-size: 12px; font-weight: 600; font-variant-numeric: tabular-nums; }
-@media (hover: hover) { .ovh-key:hover { background: var(--bg-hover, rgba(255,255,255,.07)); } }
+/* The bar's key is the apps themselves: each one its icon (a way in, not a legend), its name, and how often, with the
+ bar's own colour beside the number. Five across, as the home grid is. */
+.ovh-use-apps { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 2px; margin: 8px -8px 0; }
+.ovh-key { display: grid; justify-items: center; align-content: start; gap: 3px; min-width: 0; padding: 6px 0; border-radius: 14px; color: inherit; text-decoration: none; -webkit-tap-highlight-color: transparent; }
+.ovh-key .ovh-ic { width: 44px; height: 44px; border-radius: 14px; transition: transform .16s cubic-bezier(.2,1.4,.4,1); }
+.ovh-key b { max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 11.5px; font-weight: 650; letter-spacing: -.01em; color: var(--text-secondary, #a8b3c4); }
+.ovh-key small { display: inline-flex; align-items: center; gap: 4px; color: var(--text-muted, #93a0b4); font-size: 11.5px; font-weight: 600; font-variant-numeric: tabular-nums; line-height: 1.2; }
+.ovh-key small::before { content: ""; flex: none; width: 7px; height: 7px; border-radius: 50%; background: var(--c, #3b82f6); }
+.ovh-key:active .ovh-ic { transform: scale(calc(1 - .08 * var(--ov-move, 1))); }
+@media (hover: hover) { .ovh-key:hover { background: var(--bg-hover, rgba(255,255,255,.07)); } .ovh-key:hover b { color: var(--text-primary, #e8ecf3); } }
.ovh-sec small { flex: none; font-size: 11px; font-weight: 600; letter-spacing: 0; text-transform: none; color: var(--text-muted, #93a0b4); }
.ovh-sec small { flex: none; font-size: 11px; font-weight: 600; letter-spacing: 0; text-transform: none; color: var(--text-muted, #93a0b4); }
@keyframes ovh-rise { from { opacity: 0; transform: translateY(6px) scale(.98); } }
@keyframes ovh-rise { from { opacity: 0; transform: translateY(6px) scale(.98); } }
@keyframes ovh-grow { from { transform: scaleX(0); } }
@keyframes ovh-grow { from { transform: scaleX(0); } }
@@ -419,6 +426,7 @@ html[data-ov-motion=reduced] .ovh-card { animation-name: ovh-fade; animation-dur
@@ -419,6 +426,7 @@ html[data-ov-motion=reduced] .ovh-card { animation-name: ovh-fade; animation-dur
if (!items.length) return null;
if (!items.length) return null;
const wrap = h('div');
const wrap = h('div');
const head = sec('Jump back in');
const head = sec('Jump back in');
+ head.append(h('small', '', 'where you were last'));
wrap.append(head);
wrap.append(head);
const rail = h('div', 'ovh-rail'); rail.setAttribute('role', 'list'); rail.setAttribute('aria-label', 'Jump back in');
const rail = h('div', 'ovh-rail'); rail.setAttribute('role', 'list'); rail.setAttribute('aria-label', 'Jump back in');
items.forEach((x, i) => {
items.forEach((x, i) => {
@@ -441,7 +449,7 @@ html[data-ov-motion=reduced] .ovh-card { animation-name: ovh-fade; animation-dur
@@ -441,7 +449,7 @@ html[data-ov-motion=reduced] .ovh-card { animation-name: ovh-fade; animation-dur
return wrap;
return wrap;
}
}
- /** Most used: the pages you opened, by site, as one bar and its key. */
+ /** Most used: the pages you opened, by site, as one bar; its key is the apps themselves, each a way in. */
function usage() {
function usage() {
const rows = (o.usage || []).map((u) => ({ a: L.byId.get(u.id), n: u.count })).filter((r) => r.a && r.n > 0).sort((x, y) => y.n - x.n).slice(0, 5);
const rows = (o.usage || []).map((u) => ({ a: L.byId.get(u.id), n: u.count })).filter((r) => r.a && r.n > 0).sort((x, y) => y.n - x.n).slice(0, 5);
if (rows.length < 2) return null;
if (rows.length < 2) return null;
@@ -454,13 +462,16 @@ html[data-ov-motion=reduced] .ovh-card { animation-name: ovh-fade; animation-dur
@@ -454,13 +462,16 @@ html[data-ov-motion=reduced] .ovh-card { animation-name: ovh-fade; animation-dur
const box = h('div', 'ovh-use');
const box = h('div', 'ovh-use');
const bar = h('div', 'ovh-use-bar'); bar.setAttribute('role', 'img');
const bar = h('div', 'ovh-use-bar'); bar.setAttribute('role', 'img');
bar.setAttribute('aria-label', rows.map((r) => `${r.a.title} ${r.n}`).join(', '));
bar.setAttribute('aria-label', rows.map((r) => `${r.a.title} ${r.n}`).join(', '));
- const keys = h('div', 'ovh-use-keys');
+ const keys = h('div', 'ovh-use-apps'); keys.setAttribute('role', 'list');
const CHART = ['#3b82f6', '#a855f7', '#f59e0b', '#14b8a6', '#ec4899']; // five that tell apart in dark and light
const CHART = ['#3b82f6', '#a855f7', '#f59e0b', '#14b8a6', '#ec4899']; // five that tell apart in dark and light
rows.forEach((r, i) => {
rows.forEach((r, i) => {
const c = CHART[i % CHART.length];
const c = CHART[i % CHART.length];
const seg = h('span'); seg.style.setProperty('--n', String(r.n)); seg.style.setProperty('--c', c); seg.style.setProperty('--i', String(i)); bar.append(seg);
const seg = h('span'); seg.style.setProperty('--n', String(r.n)); seg.style.setProperty('--c', c); seg.style.setProperty('--i', String(i)); bar.append(seg);
- const k = h('a', 'ovh-key', r.a.title); k.href = safe(r.a.url) || '#'; k.style.setProperty('--c', c);
- k.append(h('small', '', num(r.n)));
+ const k = h('a', 'ovh-key'); k.href = safe(r.a.url) || '#'; k.style.setProperty('--c', c); k.setAttribute('role', 'listitem');
+ k.setAttribute('aria-label', `${r.a.title}, opened ${num(r.n)} ${r.n === 1 ? 'time' : 'times'}`);
+ const ic = h('span', 'ovh-ic'); ic.style.setProperty('--ovh-h', String(hueOf(r.a.site || r.a.id))); ic.append(siteIcon(r.a.site, 44));
+ k.append(ic, h('b', '', r.a.title), h('small', '', num(r.n)));
+ k.addEventListener('click', () => { if (o.onOpen) o.onOpen(r.a); });
keys.append(k);
keys.append(k);
});
});
box.append(bar, keys);
box.append(bar, keys);
@@ -477,7 +488,6 @@ html[data-ov-motion=reduced] .ovh-card { animation-name: ovh-fade; animation-dur
@@ -477,7 +488,6 @@ html[data-ov-motion=reduced] .ovh-card { animation-name: ovh-fade; animation-dur
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()));
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 b = backRail(); if (b) el.append(calm(b)); const u = usage(); if (u) el.append(calm(u)); }
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');
edit.type = 'button';
edit.type = 'button';
@@ -497,6 +507,8 @@ html[data-ov-motion=reduced] .ovh-card { animation-name: ovh-fade; animation-dur
@@ -497,6 +507,8 @@ html[data-ov-motion=reduced] .ovh-card { animation-name: ovh-fade; animation-dur
const all = h('button', 'ovh-all', `All apps (${total})`); all.type = 'button';
const all = h('button', 'ovh-all', `All apps (${total})`); all.type = 'button';
all.addEventListener('click', () => { state.view = 'drawer'; paint(); });
all.addEventListener('click', () => { state.view = 'drawer'; paint(); });
el.append(all);
el.append(all);
+ // Under your home: what you use most (shortcuts), then where you were last (history).
+ { const u = usage(); if (u) el.append(calm(u)); const b = backRail(); if (b) el.append(calm(b)); }
icons();
icons();
if (signedIn(o) && actorBox && !actorBox.childNodes.length) actor(actorBox, o);
if (signedIn(o) && actorBox && !actorBox.childNodes.length) actor(actorBox, o);
}
}
@@ -1,12 +1,12 @@
@@ -1,12 +1,12 @@
{
{
"name": "openvibe-shared",
"name": "openvibe-shared",
- "version": "4.183.0",
+ "version": "4.185.0",
"lockfileVersion": 3,
"lockfileVersion": 3,
"requires": true,
"requires": true,
"packages": {
"packages": {
"": {
"": {
"name": "openvibe-shared",
"name": "openvibe-shared",
- "version": "4.184.0",
+ "version": "4.185.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.184.0",
+ "version": "4.185.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": {
@@ -428,8 +428,9 @@ async function drag(p, from, to) {
@@ -428,8 +428,9 @@ async function drag(p, from, to) {
await wait(300);
await wait(300);
assert.strictEqual(await p.evaluate("!!document.querySelector('.ovdk:not([hidden])')"), true, 'the opening tap does not close the sheet');
assert.strictEqual(await p.evaluate("!!document.querySelector('.ovdk:not([hidden])')"), true, 'the opening tap does not close the sheet');
await p.evaluate("document.body.click()");
await p.evaluate("document.body.click()");
- await wait(150);
- assert.strictEqual(await p.evaluate("!!document.querySelector('.ovdk:not([hidden])')"), false, 'a later tap outside closes it');
+ // A phone's sheet folds back into the navbar's orb before it goes (4.185.0: 240 ms), so it is gone a moment
+ // after the tap, not in the same instant.
+ await until(p, "!document.querySelector('.ovdk:not([hidden])')", 'a later tap outside closes it');
await share();
await share();
await until(p, "!!document.querySelector('.ovdk .ovdk-share')", 'the bar again');
await until(p, "!!document.querySelector('.ovdk .ovdk-share')", 'the bar again');
// Cancel: back to the list, nothing waiting.
// Cancel: back to the list, nothing waiting.
@@ -21,7 +21,7 @@ const DOCK_PATH = path.join(ROOT, 'dock.js');
@@ -21,7 +21,7 @@ const DOCK_PATH = path.join(ROOT, 'dock.js');
// time something is sent, not on every page.
// time something is sent, not on every page.
// The timer itself (menu, countdown, chime, focus rounds) is dock-timer.js (3.7 KB brotli), loaded only while a timer
// The timer itself (menu, countdown, chime, focus rounds) is dock-timer.js (3.7 KB brotli), loaded only while a timer
// runs or its menu opens; the header's tab rail and narrow-width rules came with it.
// runs or its menu opens; the header's tab rail and narrow-width rules came with it.
-const DOCK_BROTLI_BUDGET = 19456; // 19.00 KB: 18.63 KB with Dock v2's AI chats row, the conversation list, the group faces and the maker's marks (2026-11-09); 18.50 KB: 18.1 KB with the Actor tab, the sheets' shared surface and Dock chats from a site's session (4.162.0, 2026-11-09); 18.34 KB measured on 2026-11-09 with Dock v2's Chats summary and its lazy Agents rows; 18.00 KB: 17.9 KB with the orb's × as Close, dots drawn on the centre line and the pop from the pill (4.161.0, 2026-11-09); 17.50 KB: 17.28 KB with the full-screen phone Dock, the one-row header and its More menu (2026-11-09, owner: the pin unclear, pop-overs that did not close) + headroom; 16.80 KB: 16.55 KB with the phone header's two rows (4.150.0, 2026-11-08: the open tab read "Ac") + headroom; 16.40 KB: 16.30 KB with the link cards' loader (4.147.0, 2026-11-08; the card itself is dock-card.js, loaded the first time one shows) + headroom; 16.25 KB: 16.07 KB with OpenVibe Chat's hooks (share(), canSend(), the draft; 4.144.0, 2026-11-08), Send to… itself is dock-share.js (1.9 KB, loaded on first use); 16.0 KB: 15.74 KB with its siblings' ?v= content hashes (4.137.0, 2026-11-07) + headroom; 15.70 KB: 15.55 KB with Ask your agent (4.135.0, 2026-11-07) + headroom; was 15.50 KB (15435 after 4.120.0)
+const DOCK_BROTLI_BUDGET = 20480; // 20.00 KB: 19.80 KB with a phone's sheet opening out of the navbar's orb (4.185.0, 2026-11-10: the hand-off's styles and its two functions, about 0.95 KB; it runs as the sheet opens, so it cannot be a sibling loaded later); 19.00 KB: 18.63 KB with Dock v2's AI chats row, the conversation list, the group faces and the maker's marks (2026-11-09); 18.50 KB: 18.1 KB with the Actor tab, the sheets' shared surface and Dock chats from a site's session (4.162.0, 2026-11-09); 18.34 KB measured on 2026-11-09 with Dock v2's Chats summary and its lazy Agents rows; 18.00 KB: 17.9 KB with the orb's × as Close, dots drawn on the centre line and the pop from the pill (4.161.0, 2026-11-09); 17.50 KB: 17.28 KB with the full-screen phone Dock, the one-row header and its More menu (2026-11-09, owner: the pin unclear, pop-overs that did not close) + headroom; 16.80 KB: 16.55 KB with the phone header's two rows (4.150.0, 2026-11-08: the open tab read "Ac") + headroom; 16.40 KB: 16.30 KB with the link cards' loader (4.147.0, 2026-11-08; the card itself is dock-card.js, loaded the first time one shows) + headroom; 16.25 KB: 16.07 KB with OpenVibe Chat's hooks (share(), canSend(), the draft; 4.144.0, 2026-11-08), Send to… itself is dock-share.js (1.9 KB, loaded on first use); 16.0 KB: 15.74 KB with its siblings' ?v= content hashes (4.137.0, 2026-11-07) + headroom; 15.70 KB: 15.55 KB with Ask your agent (4.135.0, 2026-11-07) + headroom; was 15.50 KB (15435 after 4.120.0)
const SIBLINGS = ['action-center.js', 'notification-ui.js', 'notification-live.js', 'ov-mark.js', 'dock-vibe.js', 'dock-runs.js', 'ai-marks-client.js', 'dock-timer.js', 'dock-agent.js', 'dock-share.js', 'dock-card.js'];
const SIBLINGS = ['action-center.js', 'notification-ui.js', 'notification-live.js', 'ov-mark.js', 'dock-vibe.js', 'dock-runs.js', 'ai-marks-client.js', 'dock-timer.js', 'dock-agent.js', 'dock-share.js', 'dock-card.js'];
// ── the budget ──────────────────────────────────────────────
// ── the budget ──────────────────────────────────────────────
@@ -179,7 +179,7 @@ const server = http.createServer((req, res) => {
@@ -179,7 +179,7 @@ const server = http.createServer((req, res) => {
await openTab(s, 'you');
await openTab(s, 'you');
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; Jump back in as cards; Most used as one bar.
+ // 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.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');
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');
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');
@@ -187,6 +187,16 @@ const server = http.createServer((req, res) => {
@@ -187,6 +187,16 @@ const server = http.createServer((req, res) => {
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']);
assert.deepStrictEqual(await s.evaluate(`[...document.querySelectorAll('.ovh-use .ovh-key')].map((k) => k.textContent)`), ['Live5', 'Pics2'], 'Most used: pages opened per site');
assert.deepStrictEqual(await s.evaluate(`[...document.querySelectorAll('.ovh-use .ovh-key')].map((k) => k.textContent)`), ['Live5', 'Pics2'], 'Most used: pages opened per site');
assert.strictEqual(await s.evaluate(`(document.querySelector('.ovh-sec a.ovh-link[href="https://openvibe.report/me"]') || {}).textContent`), 'Your stats', 'Most used links to your stats on Report');
assert.strictEqual(await s.evaluate(`(document.querySelector('.ovh-sec a.ovh-link[href="https://openvibe.report/me"]') || {}).textContent`), 'Your stats', 'Most used links to your stats on Report');
+ // The owner, 2026-11-10: "Your home should be above Jump back in… the thing at the top a little more compact… links
+ // 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.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');
+ assert.strictEqual(await s.evaluate(`(document.querySelector('.ovh-rail').parentElement.querySelector('.ovh-sec small') || {}).textContent`), 'where you were last', 'Jump back in says what it is');
assert.strictEqual(await s.evaluate(`(document.querySelector('.ovh-sec:not(:first-child) span, .ovh-grid:not(.ovh-recent)') && [...document.querySelectorAll('.ovh-sec span')].map((x) => x.textContent).find((x) => /home|Popular/i.test(x)))`), 'Your home', 'signed in, Home is theirs from the start');
assert.strictEqual(await s.evaluate(`(document.querySelector('.ovh-sec:not(:first-child) span, .ovh-grid:not(.ovh-recent)') && [...document.querySelectorAll('.ovh-sec span')].map((x) => x.textContent).find((x) => /home|Popular/i.test(x)))`), 'Your home', 'signed in, Home is theirs from the start');
assert.deepStrictEqual(await s.evaluate(`[...document.querySelectorAll('.ovh-folder[data-id="folder:tools"] .ov-icon')].map((i) => i.getAttribute('data-icon'))`), ['pdf', 'image', 'qr', 'audio'], 'the Tools folder shows its tools\' own icons');
assert.deepStrictEqual(await s.evaluate(`[...document.querySelectorAll('.ovh-folder[data-id="folder:tools"] .ov-icon')].map((i) => i.getAttribute('data-icon'))`), ['pdf', 'image', 'qr', 'audio'], 'the Tools folder shows its tools\' own icons');
await s.evaluate(`document.querySelector('.ovh-sec button').click(), true`);
await s.evaluate(`document.querySelector('.ovh-sec button').click(), true`);
@@ -214,6 +224,32 @@ const server = http.createServer((req, res) => {
@@ -214,6 +224,32 @@ const server = http.createServer((req, res) => {
assert.deepStrictEqual(sent.map((c) => c.u), ['attachments', 'attachments', 'posts'], 'each picture, then the post');
assert.deepStrictEqual(sent.map((c) => c.u), ['attachments', 'attachments', 'posts'], 'each picture, then the post');
assert.ok(sent.every((c) => c.auth === 'Bearer tok-1'), 'with the person\'s token');
assert.ok(sent.every((c) => c.auth === 'Bearer tok-1'), 'with the person\'s token');
assert.deepStrictEqual(JSON.parse(sent[2].body).pictures.map((x) => x.alt), ['A dark sky', 'The same sky']);
assert.deepStrictEqual(JSON.parse(sent[2].body).pictures.map((x) => x.alt), ['A dark sky', 'The same sky']);
+ // ── A phone's Dock opens out of the navbar's menu button, and that button is its close button (the owner,
+ // 2026-11-10: "that close button should… fade and have an animation effect from the top right, the menu button
+ // that already exists"). The close orb starts on the navbar's orb and settles into its x; the navbar's orb makes
+ // the same move underneath; closing runs it back, then the sheet is gone. With motion off there is no move.
+ await s.evaluate(`(document.querySelector('.ovs.is-open [data-close], .ovs-orb') || { click() {} }).click(), true`);
+ await new Promise((ok) => setTimeout(ok, 700));
+ assert.ok(await until(s, `!!(window.OpenVibeDock && window.OpenVibeDock.open)`), 'the Dock is loaded beside the navbar');
+ const start = await s.evaluate(`(() => { const c = (e) => { const r = e.getBoundingClientRect(); return [r.left + r.width / 2, r.top + r.height / 2]; };
+ const nav = [...document.querySelectorAll('.openvibe-navbar .ovnav-orb')].find((b) => b.getBoundingClientRect().width > 0);
+ const before = c(nav.querySelector('svg'));
+ window.OpenVibeDock.open({ tab: 'activity' });
+ const dock = document.querySelector('.ovdk'), mine = dock.querySelector('.ovdk-orbx');
+ return { before, at: c(mine.querySelector('svg')), fromOrb: dock.classList.contains('from-orb'), isFrom: mine.classList.contains('is-from') }; })()`);
+ assert.ok(start.fromOrb && start.isFrom, 'the sheet opens from the orb');
+ assert.ok(Math.abs(start.at[0] - start.before[0]) <= 1.5 && Math.abs(start.at[1] - start.before[1]) <= 1.5, `the close orb starts on the navbar's orb (${JSON.stringify(start)})`);
+ await new Promise((ok) => setTimeout(ok, 900));
+ const open = await s.evaluate(`(() => { const dock = document.querySelector('.ovdk'), mine = dock.querySelector('.ovdk-orbx'), sv = mine.querySelector('svg'), r = sv.getBoundingClientRect(), b = mine.getBoundingClientRect();
+ return { settled: getComputedStyle(sv).transform === 'none' && !mine.classList.contains('is-from'), centred: Math.abs((r.left + r.width / 2) - (b.left + b.width / 2)) <= 1, navIsDock: !!document.querySelector('.openvibe-navbar .ovnav-orb.is-dock'), opacity: getComputedStyle(dock).opacity }; })()`);
+ assert.ok(open.settled && open.centred, `it settles into its place in the header (${JSON.stringify(open)})`);
+ assert.ok(open.navIsDock && open.opacity === '1', 'the navbar\'s orb made the same move underneath');
+ const closing = await s.evaluate(`(() => { window.OpenVibeDock.close(); const dock = document.querySelector('.ovdk'); return { hidden: dock.hidden, out: dock.classList.contains('is-out'), back: dock.querySelector('.ovdk-orbx').classList.contains('is-from'), navIsDock: !!document.querySelector('.ovnav-orb.is-dock') }; })()`);
+ assert.ok(!closing.hidden && closing.out && closing.back && !closing.navIsDock, `closing folds it back into the orb first (${JSON.stringify(closing)})`);
+ assert.ok(await until(s, `document.querySelector('.ovdk').hidden`), 'then the sheet is gone');
+ assert.ok(await s.evaluate(`!document.querySelector('.ovdk').classList.contains('from-orb') && !document.querySelector('.ovnav-orb.is-dock') && [...document.querySelectorAll('.openvibe-navbar .ovnav-orb')].every((b) => !b.style.transition)`), 'and the navbar\'s orb is as it was');
+ const still = await s.evaluate(`(() => { document.documentElement.setAttribute('data-ov-motion', 'off'); window.OpenVibeDock.open({ tab: 'activity' }); const dock = document.querySelector('.ovdk'); const a = dock.classList.contains('from-orb'); window.OpenVibeDock.close(); const out = { fromOrb: a, hidden: dock.hidden }; document.documentElement.removeAttribute('data-ov-motion'); return out; })()`);
+ assert.ok(!still.fromOrb && still.hidden, 'motion off: no move, and it closes at once');
await s.close();
await s.close();
console.log('menu-home-screen-chrome: home grid, long-press and drag, keys, remove and put back, folders with tool icons, the wall, today, the composer with pictures, Network sync ok');
console.log('menu-home-screen-chrome: home grid, long-press and drag, keys, remove and put back, folders with tool icons, the wall, today, the composer with pictures, Network sync ok');
} finally {
} finally {