openvibers/openvibe.shared Commits
4.186.0: the mark works as a ring of dots and can think; the menu and the Dock are one sheet; bare controls wear the theme (#407)
Changes
@@@ -1,5 +1,38 @@# Changelog++## 4.186.0 — 2026-11-10++++**The mark at work is a ring of dots, and thinking is a state of its own.** The owner, 2026-11-10: "the progress++animation… should make the O turn into a bunch of little circles, like how a normal loading spinner looks… a cooler++animation that actually looks like it's loading or doing something. Also, there should be one dedicated for thinking++when your agent is thinking or replying… easier to know that it's replying or outputting tokens."++++- **`data-state="busy"` (working):** the O breaks into eight dots (a dash of no length with a round cap) and three++ bright ones go round the ring, dot to dot, while the V bobs. It was the O breathing. This replaces the old line++ "nothing spins": a wait is the mark's own O going round, never a borrowed spinner.++- **`data-state="think"` (new):** three dots rise in turn inside the O, the ring quiets, and the V's two strokes glimmer++ one after the other. One sign, wherever an agent is thinking or tokens are on their way.++- Both are shown at every size and stay readable with motion off (the dots are simply there).++++**The menu and the Dock are one kind of sheet on a phone.** The owner: "make the two UIs look match a little better,++especially the close button, so it feels more seamless… make the animation effect for the menu opening and closing more++similar to how the other ones are… and make it cooler."++++- **One close button:** the Dock's is the menu's now: the same round, ringed button, the same × of dots in the same++ colours, at the same place on the screen (measured: both centred 36px from the right edge, 34px from the top).++- **One move:** the Dock's sheet opens as a circle growing from the orb and folds back into it, with the menu's own++ timing (380 ms in, 280 ms out); it faded and scaled in 4.185.0.++- **A ring leaves the orb** as either sheet opens (smaller under Calm, by `--ov-move`).++- The close orb is measured after the sheet's layout is set, so it starts exactly on the navbar's button.++++**A control nobody styled wears the theme.** The owner: "customize all the inputs and buttons and things that aren't++styled." A `<button>`, a text field, a `<select>` or a `<textarea>` with **no class of its own** gets the theme's face++from the loader: its colour, border, corner and font, and the accent on a tick box or a slider. Only the face, and at++no specificity (`:where`), so any rule a site wrote still wins and no layout moves; a control's real shape is the++kit's (`ovk-btn`, `ovk-input`). The loader grew about 1 KB raw (budget 67,600 characters).++++The Dock is 20.00 KB brotli (budget 20.25 KB, with the reason beside it).++## 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@@@ -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.185.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed++**Status:** alpha, v4.186.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.185.0",++ "version": "4.186.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 mark at work is the O as eight dots with three bright ones going round, and it has a state of its own for an agent that is thinking or writing (data-state=think); a phone's Dock opens and closes exactly as the menu does and wears its close button in its place; a button, field, select or text box that nobody styled wears the theme instead of the browser's grey (4.186.0)","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)",@@@ -146,7 +146,7 @@ function connect(wsUrl, onClose) {* { version, userAgent, send, newContext(), close() }. close() kills only the process it started and resolves once* its temporary profile is removed.*/--async function launch({ bin, browserURL, tmpDir, args = [], sandbox = false, headless = true } = {}) {++async function launch({ bin, browserURL, tmpDir, args = [], sandbox = false, headless = true, _again = false } = {}) {let proc = null, profile = null, wsUrl;if (browserURL) {const v = await (await fetch(`${browserURL.replace(/\/$/, '')}/json/version`)).json();@@@ -175,7 +175,13 @@ async function launch({ bin, browserURL, tmpDir, args = [], sandbox = false, hea} catch { /* not yet */ }if (!wsUrl) await sleep(100);}-- if (!wsUrl) { proc.kill('SIGKILL'); fs.rmSync(profile, { recursive: true, force: true }); throw new Error('Chrome did not start'); }++ if (!wsUrl) {++ proc.kill('SIGKILL'); fs.rmSync(profile, { recursive: true, force: true });++ // Once more before giving up: on a loaded runner the first Chrome of a batch can take longer than the++ // wait, and the second, with the binary warm, comes up in a second or two (macOS CI, 2026-11-10).++ if (!_again) return launch({ bin, browserURL, tmpDir, args, sandbox, headless, _again: true });++ throw new Error('Chrome did not start');++ }}const conn = connect(wsUrl);await conn.opened;@@@ -193,17 +193,24 @@.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))); } }++ /* A phone's sheet opens out of the navbar's orb as the menu's does (4.186.0; see CHANGELOG): one move, one close button. */++ .ovdk.from-orb.is-in { animation: ovdk-reveal .38s cubic-bezier(.2,.9,.25,1) both; }++ .ovdk.from-orb.is-out { animation: ovdk-fold .28s cubic-bezier(.5,0,.75,.2) both; pointer-events: none; }++ @keyframes ovdk-reveal { from { clip-path: circle(28px at var(--ovdk-ox) var(--ovdk-oy)); } to { clip-path: circle(150% at var(--ovdk-ox) var(--ovdk-oy)); } }++ @keyframes ovdk-fold { from { clip-path: circle(150% at var(--ovdk-ox) var(--ovdk-oy)); } to { clip-path: circle(20px at var(--ovdk-ox) var(--ovdk-oy)); opacity: 0; } }++ .ovdk.from-orb .ovdk-head { padding: 4px 14px 6px 8px; }++ .from-orb .ovdk-acts .ovdk-orbx { position: relative; width: 40px; height: 40px; margin: 2px; border-radius: 50%; background: color-mix(in srgb, var(--accent, #3b82f6) 16%, var(--bg-primary, #0b0f19));++ box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent-light, #60a5fa) 35%, transparent), 0 6px 18px -6px color-mix(in srgb, var(--accent, #3b82f6) 70%, transparent); transition: background-color .3s, box-shadow .3s; }++ .from-orb .ovdk-acts .ovdk-orbx.is-from { background: transparent; box-shadow: none; }++ .from-orb.is-in .ovdk-orbx::before { content: ""; position: absolute; inset: 0; border-radius: 50%; border: 2px solid var(--accent-light, #60a5fa); pointer-events: none; animation: ovdk-ring .6s cubic-bezier(.2,.8,.2,1) .1s both; }++ @keyframes ovdk-ring { from { opacity: .8; transform: scale(.9); } to { opacity: 0; transform: scale(calc(1 + 2.2 * 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); }++ .from-orb .ovdk-ox { transform: rotate(180deg) scale(.95); }++ .from-orb .ovdk-ox :is(.ovdk-o0, .ovdk-o2) { fill: var(--accent-light, #60a5fa); transform: scale(1.12); }++ .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); }@@@ -977,13 +984,14 @@unlinkOrb();const nav = navOrb(), mine = el.querySelector('.ovdk-orbx'), mineSvg = mine && mine.querySelector('svg');if (!nav || !mineSvg || reduced() || popped) return;++ el.classList.add('from-orb'); // first: it sets the header's layout, and the close orb is measured where it will restconst a = (nav.querySelector('svg') || nav).getBoundingClientRect(), b = mineSvg.getBoundingClientRect(), box = el.getBoundingClientRect();-- if (!a.width || !b.width) return;++ if (!a.width || !b.width) { el.classList.remove('from-orb'); 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');++ mine.classList.add('is-set', 'is-from');orbLink = { nav, mine };void mine.offsetWidth; // the starting pose is put on, not animated intomine.classList.remove('is-set');@@@ -1039,7 +1047,7 @@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);++ closing = setTimeout(() => { closing = 0; shut(); }, 280);return;}shut();@@@ -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': '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' };++ const SIBLING_REV = { 'ov-mark.js': '4fb7af77a431', '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': 'c6dbd7d46ae7', 'dock.js': '2c59b0f0e035', '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">.@@@ -26,7 +26,11 @@* - a page on its way (navbar): the ring empties and draws itself round again as the next page loads, paced by how* long this site's pages usually take in this browser (learned per site, boost moves and full loads apart); the* V nods while it waits and hops when the page is here (window.OVMark.navStart/navEnd for single-page apps);-- * - busy: the V bobs and the O breathes, in turn; ok: the V becomes a tick, green, and hops (the tick only ever++ * - busy (working): the O breaks into eight dots and three bright ones go round it, dot to dot, while the V bobs++ * (the owner, 2026-11-10: "make the O turn into a bunch of little circles… a spinner that has little dots around++ * it… that actually looks like it's loading"); think (your agent is thinking or writing): three dots rise in turn++ * inside the O and the V's two strokes glimmer one after the other, the one sign used wherever tokens are on++ * their way; ok: the V becomes a tick, green, and hops (the tick only ever* means "done"); error: the V becomes "!", red, and the mark shakes; no: the V becomes "×", grey, and the O* shakes its head; notify: a dot pops up by the V and the O bounces; live: the O is the record light, red, and* ripples go out from it; sleep: the O closes like an eye and the V sinks.@@@ -134,7 +138,19 @@ html[data-ov-season-mark] .ov-mark[data-ovm-intro] .sl{animation:ovmLeaf .9s cub.ov-mark[data-ovm-arrive] .og{animation:ovmPulse .5s ease-out 1}/* The states. */.ov-mark[data-state=busy] .vg{animation:ovmBob .9s ease-in-out infinite}--.ov-mark[data-state=busy] .og{animation:ovmBreathe .9s ease-in-out .45s infinite}++/* Working: the O as eight dots (a dash of no length with a round cap is a dot), three bright ones hopping round. */++.ov-mark[data-state=busy] :is(.o,.ot){stroke-linecap:round;stroke-width:var(--ovm-dot-w)}++.ov-mark[data-state=busy] .ot{opacity:.3;stroke-dasharray:0 12.5}++.ov-mark[data-state=busy] .o{stroke-dasharray:0 12.5 0 12.5 0 75;animation:ovmDots .96s steps(8,end) infinite}++.ov-mark[data-state=busy] .og{animation:ovmSettle .4s cubic-bezier(.2,1.3,.3,1) 1}++/* Thinking: three dots inside the O rise in turn, the ring quiets, the V's strokes glimmer one after the other. */++.ov-mark .td{display:none;fill:var(--ovm-o);transform-box:fill-box;transform-origin:center}++.ov-mark[data-state=think] .td{display:inline;animation:ovmThink 1.2s ease-in-out infinite}++.ov-mark[data-state=think] .td2{animation-delay:.16s}++.ov-mark[data-state=think] .td3{animation-delay:.32s}++.ov-mark[data-state=think] .o{stroke-opacity:.5;transition:stroke-opacity .3s}++.ov-mark[data-state=think] .va{animation:ovmSpeak 1.2s ease-in-out infinite}++.ov-mark[data-state=think] .vb{animation:ovmSpeak 1.2s ease-in-out .6s infinite}.ov-mark[data-state=ok]{--ovm-o:var(--success,#22c55e);--ovm-v:var(--success,#22c55e)}.ov-mark[data-state=ok] .vg{animation:ovmHop .56s ease-out .14s 1 both}.ov-mark[data-state=error]{--ovm-o:var(--danger,#ef4444);--ovm-v:var(--danger,#ef4444)}@@@ -171,6 +187,10 @@ html[data-ov-season-mark] .ov-mark[data-ovm-intro] .sl{animation:ovmLeaf .9s cub@keyframes ovmHop{0%,100%{transform:translateY(0) scaleY(1)}35%{transform:translateY(-9px) scaleY(1.04)}70%{transform:translateY(0) scaleY(.96)}}@keyframes ovmBob{0%,100%{transform:translateY(0)}50%{transform:translateY(6px)}}@keyframes ovmBreathe{0%,100%{transform:scale(1)}50%{transform:scale(.88)}}++@keyframes ovmDots{to{stroke-dashoffset:-100}}++@keyframes ovmSettle{from{transform:scale(.86)}}++@keyframes ovmThink{0%,55%,100%{transform:translateY(0);opacity:.4}28%{transform:translateY(-2.6px);opacity:1}}++@keyframes ovmSpeak{0%,100%{opacity:1}50%{opacity:.5}}@keyframes ovmPulse{0%,100%{transform:scale(1)}50%{transform:scale(1.06)}}@keyframes ovmShake{0%,100%{transform:translateX(0)}15%{transform:translateX(-7px)}30%{transform:translateX(7px)}45%{transform:translateX(-6px)}60%{transform:translateX(6px)}80%{transform:translateX(-3px)}}@keyframes ovmNo{0%,100%{transform:translateX(0)}20%{transform:translateX(-4px)}40%{transform:translateX(4px)}60%{transform:translateX(-3px)}80%{transform:translateX(3px)}}@@@ -208,7 +228,7 @@ html[data-ov-motion=off] .ov-mark:not([data-fx-demo] *) *{animation-iteration-co<linearGradient id="${id}gg"><stop offset="0" stop-color="#fff" stop-opacity="0"/><stop offset=".5" stop-color="#fff" stop-opacity=".8"/><stop offset="1" stop-color="#fff" stop-opacity="0"/></linearGradient><mask id="${id}s" maskUnits="userSpaceOnUse" x="-20" y="-20" width="140" height="140"><g mask="url(#${id}h)"><g class="og" style="${oc}"><path d="${G.ring}" fill="none" stroke="#fff" stroke-width="${G.sw}"/></g></g><g class="vg" style="${vc}"><path class="ma" d="${seg(p, 0)}" fill="none" stroke="#fff" stroke-linecap="round" stroke-width="${G.vsw}"/><path class="mb" d="${seg(p, 4)}" fill="none" stroke="#fff" stroke-linecap="round" stroke-width="${G.vsw}"/></g></mask></defs><g class="m"><circle class="rp" cx="${G.cx}" cy="${G.cy}" r="${G.R}"/><circle class="rp rp2" cx="${G.cx}" cy="${G.cy}" r="${G.R}"/>--<g mask="url(#${id}h)"><g class="og" style="${oc}"><path class="ot" d="${G.ring}" stroke-width="${G.sw}"/><path class="o" d="${G.ring}" pathLength="100" stroke-width="${G.sw}" style="stroke:url(#${id}go)"/><circle class="cd" cx="${G.cx}" cy="${G.cy}" r="${f(ri * 0.62)}"/>++<g mask="url(#${id}h)"><g class="og" style="${oc};--ovm-dot-w:${f(G.sw * 0.62)}"><path class="ot" d="${G.ring}" pathLength="100" stroke-width="${G.sw}"/><path class="o" d="${G.ring}" pathLength="100" stroke-width="${G.sw}" style="stroke:url(#${id}go)"/><circle class="cd" cx="${G.cx}" cy="${G.cy}" r="${f(ri * 0.62)}"/>${[-1, 0, 1].map((k, n) => `<circle class="td${n ? ` td${n + 1}` : ''}" cx="${f(G.cx + k * ri * 0.52)}" cy="${G.cy}" r="${f(ri * 0.21)}"/>`).join('')}<g class="sw2"><path class="sc" d="M${c0[0]} ${c0[1]}A${capR} ${capR} 0 0 1 ${c1[0]} ${c1[1]}"/></g></g></g><g class="vg" style="${vc}"><path class="va" d="${seg(p, 0)}" stroke-width="${G.vsw}" style="stroke:url(#${id}gv)"/><path class="vb" d="${seg(p, 4)}" stroke-width="${G.vsw}" style="stroke:url(#${id}gv)"/><g class="sw2"><circle class="sd" cx="${G.A[0]}" cy="${f(G.A[1] - G.vsw * 0.18)}" r="${f(G.vsw * 0.36)}"/><circle class="sd" cx="${G.B[0]}" cy="${f(G.B[1] - G.vsw * 0.18)}" r="${f(G.vsw * 0.36)}"/></g></g>@@@ -1,12 +1,12 @@{"name": "openvibe-shared",-- "version": "4.185.0",++ "version": "4.186.0","lockfileVersion": 3,"requires": true,"packages": {"": {"name": "openvibe-shared",-- "version": "4.185.0",++ "version": "4.186.0","license": "MIT","dependencies": {"jsonwebtoken": "^9.0.0",@@@ -1,6 +1,6 @@{"name": "openvibe-shared",-- "version": "4.185.0",++ "version": "4.186.0","description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site","main": "index.js","bin": {@@@ -374,6 +374,9 @@transform: none; transition: transform .42s cubic-bezier(.2, 1.1, .3, 1), background-color .3s, box-shadow .3s; }.ovs.ovs-anchored .ovs-orb.is-from { transform: translate(var(--orb-dx, 0px), var(--orb-dy, 0px)) scale(.9); background: transparent; box-shadow: none; }.ovs.ovs-anchored .ovs-orb::after { content: ""; position: absolute; inset: -4px; }++ /* A ring leaves the orb as its sheet opens (4.186.0; the Dock's sheet does the same). */++ .ovs.ovs-anchored .ovs-orb.is-x::before { content: ""; position: absolute; inset: 0; border-radius: 50%; border: 2px solid var(--ovo-a); pointer-events: none; animation: ovs-ring .6s cubic-bezier(.2,.8,.2,1) .1s both; }++ @keyframes ovs-ring { from { opacity: .8; transform: scale(.9); } to { opacity: 0; transform: scale(calc(1 + 2.2 * var(--ov-move, 1))); } }.ovs.ovs-anchored .ovs-orb:active { transform: scale(.9); }.ovs.ovs-anchored .ovs-orb svg { display: block; overflow: visible; }.ovs.is-closing .ovs-panel { animation: ovs-fold .28s cubic-bezier(.5, 0, .75, .2) both; }@@@ -217,7 +217,7 @@ const server = http.createServer((req, res) => {});const wait = (ms) => new Promise((r) => setTimeout(r, ms));--async function until(p, expr, what, ms = 5000) {++async function until(p, expr, what, ms = 15000) { // a limit on a poll: a loaded runner drew the pill after 5 sconst end = Date.now() + ms;for (;;) {const v = await p.evaluate(expr);@@@ -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 = 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 DOCK_BROTLI_BUDGET = 20736; // 20.25 KB: 20.04 KB with a phone's sheet opening and closing as the menu's does (a circle from the orb, the menu's close button in the menu's place, a ring as it opens; 4.186.0, 2026-11-10); 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 ──────────────────────────────────────────────@@@ -74,6 +74,12 @@ assert.ok(/html\[data-ov-motion=off\] \*:not\(\[data-fx-demo\] \*\)[^{]*\{animatassert.ok(/CALM_RATE = 0\.6/.test(SRC) && SRC.includes('t.iterations !== Infinity') && SRC.includes("addEventListener('animationstart'"), 'under Calm an endless loop is slowed as it starts; anything that ends plays as written');assert.ok(SRC.includes("motionOf() === 'off') return;"), 'the theme crossfade is a colour fade: only Off skips it');++// ── a control nobody styled wears the theme (4.186.0) ──++assert.ok(/:where\(button,input:is\(\[type=submit\],\[type=button\],\[type=reset\]\)\):where\(:not\(\[class\]\)\)\{font-family:inherit;border:1px solid var\(--border-light,[^;]*;border-radius:10px;color:var\(--text-primary,inherit\);background:var\(--bg-hover,/.test(css), 'a bare button: the theme\'s face, at no specificity, readable even where no theme is set');++assert.ok(/:where\(input:not\(\[type=checkbox\][^)]*\),select,textarea\):where\(:not\(\[class\]\)\)\{font-family:inherit;border:1px solid/.test(css), 'a bare field, select or text box too');++const bare = css.match(/:where\((?:button|input)[^{]*\{[^}]*\}/g) || [];++assert.ok(bare.length >= 3 && bare.every((r) => /^:where\([^{]*:where\(:not\(\[class\]\)\)\{/.test(r) && !/padding|height|width|margin|display/.test(r)), 'only controls with no class, and nothing that moves a layout');++// ── the package: nothing stops an effect because the device asked for less ──const STOPS = /(animation|transition)\s*:\s*none/;const offenders = [];@@@ -352,6 +352,13 @@ function setup({ reduce = false, session = memoryStorage(), start = 1000000, rooassert.match(css, /\.ov-mark\[data-ovm-intro\] \.gr\{animation:ovmGlint/, 'a glint ends the intro');assert.match(css, /\.ov-mark:hover \.gr\{animation:ovmGlint|\.ov-mark:hover \.gr,/, 'and crosses on hover');assert.match(css, /\.ov-mark\[data-glow\]::before\{opacity:\.42/, 'data-glow keeps the glow on');++ // Working and thinking (4.186.0): the O as eight dots with three bright ones going round, dot to dot; three dots++ // rising in turn inside the O while the V's strokes glimmer. One sign each, wherever something works or writes.++ assert.match(css, /\.ov-mark\[data-state=busy\] \.ot\{opacity:\.3;stroke-dasharray:0 12\.5\}/, 'working: the O is eight dots');++ assert.match(css, /\.ov-mark\[data-state=busy\] \.o\{stroke-dasharray:0 12\.5 0 12\.5 0 75;animation:ovmDots \.96s steps\(8,end\) infinite\}/, 'three bright ones hop round it');++ assert.ok(!/\[data-state=busy\] \.og\{animation:ovmBreathe/.test(css), 'the O no longer just breathes while it works');++ assert.match(css, /\.ov-mark\[data-state=think\] \.td\{display:inline;animation:ovmThink 1\.2s ease-in-out infinite\}/, 'thinking: three dots inside the O');++ assert.match(css, /\.ov-mark\[data-state=think\] \.vb\{animation:ovmSpeak 1\.2s ease-in-out \.6s infinite\}/, 'and the V glimmers, one stroke after the other');assert.match(css, /\.ov-mark\[data-tone\]::before,\.ov-mark\[data-state=sleep\]::before\{display:none\}/);assert.ok(!css.includes('::before{transition:none!important}') && css.includes('html[data-ov-motion=off] .ov-mark:not([data-fx-demo] *) *{animation-iteration-count:1!important}'), 'the glow fades in under Calm too; only motion off holds the mark still (outside a demo scope)');assert.match(css, /\.ov-mark:active \.m\{transform:scale\(\.9\)/, 'a press gives a little');@@@ -58,7 +58,12 @@ const server = http.createServer((req, res) => {assert.strictEqual(await p.evaluate('Math.round(scrollY)'), await p.evaluate('Math.round(window.__before)'), 'close restores page position');assert.strictEqual(await p.evaluate('document.activeElement.id'), 'open', 'focus returns to opener');await p.evaluate(`(() => { const btn=document.querySelector('#open');btn.dispatchEvent(new PointerEvent('pointerdown',{bubbles:true}));btn.click(); })()`);-- await new Promise((ok) => setTimeout(ok, 40));++ // The sheet takes the swipe only once the surfaces layer has adopted it (the page is locked): wait for that, not++ // for a fixed 40 ms (a slow macOS runner had not got there yet, twice on 2026-11-10).++ let adopted = false;++ for (let i = 0; i < 60 && !adopted; i++) { adopted = await p.evaluate('document.querySelector("#sheet").open && document.body.style.position === "fixed"'); if (!adopted) await new Promise((ok) => setTimeout(ok, 50)); }++ assert.strictEqual(adopted, true, 'the sheet opens again and locks the page');++ await new Promise((ok) => setTimeout(ok, 60));await p.evaluate(`(() => { const head=document.querySelector('[data-ov-sheet-head]');const fire=(type,y)=>head.dispatchEvent(new PointerEvent(type,{bubbles:true,pointerId:5,pointerType:'touch',clientY:y}));fire('pointerdown',200);fire('pointermove',330);fire('pointerup',330); })()`);@@@ -51,7 +51,8 @@ assert.ok(LT.arctic['--success'], 'loader entries carry the full token set');// 62,000 since 4.64.0: the look travels between sites (?ov_look=) and every copy is dated; brotli 15.45 KB (was 15.41) (2026-10-31).// 63,500 since 4.111.1 (2026-11-05): the FAVICON block draws the Pair (about 1.2 KB raw) instead of the retired ring.// 66,500 since 4.183.0 (2026-11-10): motion has four choices, --ov-move, and Calm paces endless loops (about 2.9 KB raw).--assert.ok(loader.length < 66500, `theme-loader.js stays small for first paint (${loader.length} characters)`);++// 67,600 since 4.186.0 (2026-11-10): a control nobody styled wears the theme (three rules, about 0.9 KB raw with their note).++assert.ok(loader.length < 67600, `theme-loader.js stays small for first paint (${loader.length} characters)`);assert.ok(loader.includes('KNOWN_PROPS'), 'loader clears tokens the new theme does not set');// 5. Derived tokens: text on the accent always reads, on every theme.@@@ -357,6 +357,12 @@// Status names some sites use (--error, --error-text, --ok, --warn, --accent-text) that no theme sets: they resolve to the theme's// readable status colours instead of each site's fallback literal (pale on light themes). :where() keeps them// below any site's own value (4.86.0).++ // A control nobody styled wears the theme, not the browser's grey (4.186.0): only one with no class, only its face,++ // at no specificity, so any rule a site wrote still wins and no layout moves. Its shape is the kit's job.++ const BARE = ':where(:not([class]))', FACE = '{font-family:inherit;border:1px solid var(--border-light,var(--border,color-mix(in srgb,currentColor 30%,transparent)));border-radius:10px;color:var(--text-primary,inherit);background:';++ const BARE_CSS = ':where(button,input:is([type=submit],[type=button],[type=reset]))' + BARE + FACE + 'var(--bg-hover,var(--bg-card,color-mix(in srgb,currentColor 8%,transparent)))}'++ + ':where(input:not([type=checkbox],[type=radio],[type=range],[type=color],[type=file],[type=image],[type=submit],[type=button],[type=reset],[type=hidden]),select,textarea)' + BARE + FACE + 'var(--bg-input,var(--bg-primary,transparent))}'++ + ':where(input:is([type=checkbox],[type=radio],[type=range]),progress)' + BARE + '{accent-color:var(--accent,auto)}';const ALIAS_CSS = ':where(html){--error:var(--danger);--error-text:var(--danger-text);--ok:var(--success-text);--warn:var(--warning-text);--accent-text:var(--accent-strong)}';// Comfort (4.45.0): one <html> attribute each, !important over the inline theme (Okabe-Ito colours, OLED black).const COMFORT_CSS = 'html[data-ov-contrast=more]{--text-secondary:color-mix(in srgb,var(--text-primary) 88%,var(--bg-primary))!important;--text-muted:color-mix(in srgb,var(--text-primary) 76%,var(--bg-primary))!important;--border:color-mix(in srgb,var(--text-primary) 40%,var(--bg-primary))!important;--border-light:color-mix(in srgb,var(--text-primary) 55%,var(--bg-primary))!important}'@@@ -411,7 +417,7 @@for (const k in COMFORT_ATTRS) { const [attr, off] = COMFORT_ATTRS[k]; if (d[k] && d[k] !== off) el.setAttribute(attr, d[k]); else el.removeAttribute(attr); }if (!document.getElementById('ov-display-css') && document.head) {const st = document.createElement('style'); st.id = 'ov-display-css';-- st.textContent = 'html[data-ov-text]{font-size:calc(100% * var(--ov-text-scale,1))}' + MOTION_CSS + ALIAS_CSS + COMFORT_CSS;++ st.textContent = 'html[data-ov-text]{font-size:calc(100% * var(--ov-text-scale,1))}' + MOTION_CSS + BARE_CSS + ALIAS_CSS + COMFORT_CSS;document.head.appendChild(st);}paceLoops();
@@ -1,5 +1,38 @@
@@ -1,5 +1,38 @@
# Changelog
# Changelog
+## 4.186.0 — 2026-11-10
+
+**The mark at work is a ring of dots, and thinking is a state of its own.** The owner, 2026-11-10: "the progress
+animation… should make the O turn into a bunch of little circles, like how a normal loading spinner looks… a cooler
+animation that actually looks like it's loading or doing something. Also, there should be one dedicated for thinking
+when your agent is thinking or replying… easier to know that it's replying or outputting tokens."
+
+- **`data-state="busy"` (working):** the O breaks into eight dots (a dash of no length with a round cap) and three
+ bright ones go round the ring, dot to dot, while the V bobs. It was the O breathing. This replaces the old line
+ "nothing spins": a wait is the mark's own O going round, never a borrowed spinner.
+- **`data-state="think"` (new):** three dots rise in turn inside the O, the ring quiets, and the V's two strokes glimmer
+ one after the other. One sign, wherever an agent is thinking or tokens are on their way.
+- Both are shown at every size and stay readable with motion off (the dots are simply there).
+
+**The menu and the Dock are one kind of sheet on a phone.** The owner: "make the two UIs look match a little better,
+especially the close button, so it feels more seamless… make the animation effect for the menu opening and closing more
+similar to how the other ones are… and make it cooler."
+
+- **One close button:** the Dock's is the menu's now: the same round, ringed button, the same × of dots in the same
+ colours, at the same place on the screen (measured: both centred 36px from the right edge, 34px from the top).
+- **One move:** the Dock's sheet opens as a circle growing from the orb and folds back into it, with the menu's own
+ timing (380 ms in, 280 ms out); it faded and scaled in 4.185.0.
+- **A ring leaves the orb** as either sheet opens (smaller under Calm, by `--ov-move`).
+- The close orb is measured after the sheet's layout is set, so it starts exactly on the navbar's button.
+
+**A control nobody styled wears the theme.** The owner: "customize all the inputs and buttons and things that aren't
+styled." A `<button>`, a text field, a `<select>` or a `<textarea>` with **no class of its own** gets the theme's face
+from the loader: its colour, border, corner and font, and the accent on a tick box or a slider. Only the face, and at
+no specificity (`:where`), so any rule a site wrote still wins and no layout moves; a control's real shape is the
+kit's (`ovk-btn`, `ovk-input`). The loader grew about 1 KB raw (budget 67,600 characters).
+
+The Dock is 20.00 KB brotli (budget 20.25 KB, with the reason beside it).
+
## 4.185.0 — 2026-11-10
## 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
**The You tab: your home first, a shorter bar, Most used as shortcuts.** The owner, 2026-11-10: "Your home should be
@@ -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.185.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed
+**Status:** alpha, v4.186.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.185.0",
+ "version": "4.186.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 mark at work is the O as eight dots with three bright ones going round, and it has a state of its own for an agent that is thinking or writing (data-state=think); a phone's Dock opens and closes exactly as the menu does and wears its close button in its place; a button, field, select or text box that nobody styled wears the theme instead of the browser's grey (4.186.0)",
"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)",
"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)",
@@ -146,7 +146,7 @@ function connect(wsUrl, onClose) {
@@ -146,7 +146,7 @@ function connect(wsUrl, onClose) {
* { version, userAgent, send, newContext(), close() }. close() kills only the process it started and resolves once
* { version, userAgent, send, newContext(), close() }. close() kills only the process it started and resolves once
* its temporary profile is removed.
* its temporary profile is removed.
*/
*/
-async function launch({ bin, browserURL, tmpDir, args = [], sandbox = false, headless = true } = {}) {
+async function launch({ bin, browserURL, tmpDir, args = [], sandbox = false, headless = true, _again = false } = {}) {
let proc = null, profile = null, wsUrl;
let proc = null, profile = null, wsUrl;
if (browserURL) {
if (browserURL) {
const v = await (await fetch(`${browserURL.replace(/\/$/, '')}/json/version`)).json();
const v = await (await fetch(`${browserURL.replace(/\/$/, '')}/json/version`)).json();
@@ -175,7 +175,13 @@ async function launch({ bin, browserURL, tmpDir, args = [], sandbox = false, hea
@@ -175,7 +175,13 @@ async function launch({ bin, browserURL, tmpDir, args = [], sandbox = false, hea
} catch { /* not yet */ }
} catch { /* not yet */ }
if (!wsUrl) await sleep(100);
if (!wsUrl) await sleep(100);
}
}
- if (!wsUrl) { proc.kill('SIGKILL'); fs.rmSync(profile, { recursive: true, force: true }); throw new Error('Chrome did not start'); }
+ if (!wsUrl) {
+ proc.kill('SIGKILL'); fs.rmSync(profile, { recursive: true, force: true });
+ // Once more before giving up: on a loaded runner the first Chrome of a batch can take longer than the
+ // wait, and the second, with the binary warm, comes up in a second or two (macOS CI, 2026-11-10).
+ if (!_again) return launch({ bin, browserURL, tmpDir, args, sandbox, headless, _again: true });
+ throw new Error('Chrome did not start');
+ }
}
}
const conn = connect(wsUrl);
const conn = connect(wsUrl);
await conn.opened;
await conn.opened;
@@ -193,17 +193,24 @@
@@ -193,17 +193,24 @@
.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))); } }
+ /* A phone's sheet opens out of the navbar's orb as the menu's does (4.186.0; see CHANGELOG): one move, one close button. */
+ .ovdk.from-orb.is-in { animation: ovdk-reveal .38s cubic-bezier(.2,.9,.25,1) both; }
+ .ovdk.from-orb.is-out { animation: ovdk-fold .28s cubic-bezier(.5,0,.75,.2) both; pointer-events: none; }
+ @keyframes ovdk-reveal { from { clip-path: circle(28px at var(--ovdk-ox) var(--ovdk-oy)); } to { clip-path: circle(150% at var(--ovdk-ox) var(--ovdk-oy)); } }
+ @keyframes ovdk-fold { from { clip-path: circle(150% at var(--ovdk-ox) var(--ovdk-oy)); } to { clip-path: circle(20px at var(--ovdk-ox) var(--ovdk-oy)); opacity: 0; } }
+ .ovdk.from-orb .ovdk-head { padding: 4px 14px 6px 8px; }
+ .from-orb .ovdk-acts .ovdk-orbx { position: relative; width: 40px; height: 40px; margin: 2px; border-radius: 50%; background: color-mix(in srgb, var(--accent, #3b82f6) 16%, var(--bg-primary, #0b0f19));
+ box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent-light, #60a5fa) 35%, transparent), 0 6px 18px -6px color-mix(in srgb, var(--accent, #3b82f6) 70%, transparent); transition: background-color .3s, box-shadow .3s; }
+ .from-orb .ovdk-acts .ovdk-orbx.is-from { background: transparent; box-shadow: none; }
+ .from-orb.is-in .ovdk-orbx::before { content: ""; position: absolute; inset: 0; border-radius: 50%; border: 2px solid var(--accent-light, #60a5fa); pointer-events: none; animation: ovdk-ring .6s cubic-bezier(.2,.8,.2,1) .1s both; }
+ @keyframes ovdk-ring { from { opacity: .8; transform: scale(.9); } to { opacity: 0; transform: scale(calc(1 + 2.2 * var(--ov-move, 1))); } }
.from-orb.is-in .ovdk-ox, .from-orb.is-in .ovdk-o1 { animation: none !important; }
.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 { color: var(--text-primary, #ececf1); }
.from-orb .ovdk-orbx svg { transition: transform .42s cubic-bezier(.2, 1.1, .3, 1); }
.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 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); }
+ .from-orb .ovdk-ox { transform: rotate(180deg) scale(.95); }
+ .from-orb .ovdk-ox :is(.ovdk-o0, .ovdk-o2) { fill: var(--accent-light, #60a5fa); transform: scale(1.12); }
+ .from-orb .is-from .ovdk-ox .ovdk-o0 { fill: var(--accent-light, #60a5fa); }
.ovdk-orbx.is-set, .ovdk-orbx.is-set * { transition: none !important; }
.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 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 { transform: rotate(45deg) scale(.8); }
@@ -977,13 +984,14 @@
@@ -977,13 +984,14 @@
unlinkOrb();
unlinkOrb();
const nav = navOrb(), mine = el.querySelector('.ovdk-orbx'), mineSvg = mine && mine.querySelector('svg');
const nav = navOrb(), mine = el.querySelector('.ovdk-orbx'), mineSvg = mine && mine.querySelector('svg');
if (!nav || !mineSvg || reduced() || popped) return;
if (!nav || !mineSvg || reduced() || popped) return;
+ el.classList.add('from-orb'); // first: it sets the header's layout, and the close orb is measured where it will rest
const a = (nav.querySelector('svg') || nav).getBoundingClientRect(), b = mineSvg.getBoundingClientRect(), box = el.getBoundingClientRect();
const a = (nav.querySelector('svg') || nav).getBoundingClientRect(), b = mineSvg.getBoundingClientRect(), box = el.getBoundingClientRect();
- if (!a.width || !b.width) return;
+ if (!a.width || !b.width) { el.classList.remove('from-orb'); 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;
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`);
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));
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));
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');
+ mine.classList.add('is-set', 'is-from');
orbLink = { nav, mine };
orbLink = { nav, mine };
void mine.offsetWidth; // the starting pose is put on, not animated into
void mine.offsetWidth; // the starting pose is put on, not animated into
mine.classList.remove('is-set');
mine.classList.remove('is-set');
@@ -1039,7 +1047,7 @@
@@ -1039,7 +1047,7 @@
if (orbLink && !closing && !popped && !reduced() && el.classList.contains('from-orb')) {
if (orbLink && !closing && !popped && !reduced() && el.classList.contains('from-orb')) {
el.classList.remove('is-in'); el.classList.add('is-out');
el.classList.remove('is-in'); el.classList.add('is-out');
orbLink.mine.classList.add('is-from'); orbLink.nav.classList.remove('is-dock');
orbLink.mine.classList.add('is-from'); orbLink.nav.classList.remove('is-dock');
- closing = setTimeout(() => { closing = 0; shut(); }, 240);
+ closing = setTimeout(() => { closing = 0; shut(); }, 280);
return;
return;
}
}
shut();
shut();
@@ -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': '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' };
+ const SIBLING_REV = { 'ov-mark.js': '4fb7af77a431', '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': 'c6dbd7d46ae7', 'dock.js': '2c59b0f0e035', '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">.
@@ -26,7 +26,11 @@
@@ -26,7 +26,11 @@
* - a page on its way (navbar): the ring empties and draws itself round again as the next page loads, paced by how
* - a page on its way (navbar): the ring empties and draws itself round again as the next page loads, paced by how
* long this site's pages usually take in this browser (learned per site, boost moves and full loads apart); the
* long this site's pages usually take in this browser (learned per site, boost moves and full loads apart); the
* V nods while it waits and hops when the page is here (window.OVMark.navStart/navEnd for single-page apps);
* V nods while it waits and hops when the page is here (window.OVMark.navStart/navEnd for single-page apps);
- * - busy: the V bobs and the O breathes, in turn; ok: the V becomes a tick, green, and hops (the tick only ever
+ * - busy (working): the O breaks into eight dots and three bright ones go round it, dot to dot, while the V bobs
+ * (the owner, 2026-11-10: "make the O turn into a bunch of little circles… a spinner that has little dots around
+ * it… that actually looks like it's loading"); think (your agent is thinking or writing): three dots rise in turn
+ * inside the O and the V's two strokes glimmer one after the other, the one sign used wherever tokens are on
+ * their way; ok: the V becomes a tick, green, and hops (the tick only ever
* means "done"); error: the V becomes "!", red, and the mark shakes; no: the V becomes "×", grey, and the O
* means "done"); error: the V becomes "!", red, and the mark shakes; no: the V becomes "×", grey, and the O
* shakes its head; notify: a dot pops up by the V and the O bounces; live: the O is the record light, red, and
* shakes its head; notify: a dot pops up by the V and the O bounces; live: the O is the record light, red, and
* ripples go out from it; sleep: the O closes like an eye and the V sinks.
* ripples go out from it; sleep: the O closes like an eye and the V sinks.
@@ -134,7 +138,19 @@ html[data-ov-season-mark] .ov-mark[data-ovm-intro] .sl{animation:ovmLeaf .9s cub
@@ -134,7 +138,19 @@ html[data-ov-season-mark] .ov-mark[data-ovm-intro] .sl{animation:ovmLeaf .9s cub
.ov-mark[data-ovm-arrive] .og{animation:ovmPulse .5s ease-out 1}
.ov-mark[data-ovm-arrive] .og{animation:ovmPulse .5s ease-out 1}
/* The states. */
/* The states. */
.ov-mark[data-state=busy] .vg{animation:ovmBob .9s ease-in-out infinite}
.ov-mark[data-state=busy] .vg{animation:ovmBob .9s ease-in-out infinite}
-.ov-mark[data-state=busy] .og{animation:ovmBreathe .9s ease-in-out .45s infinite}
+/* Working: the O as eight dots (a dash of no length with a round cap is a dot), three bright ones hopping round. */
+.ov-mark[data-state=busy] :is(.o,.ot){stroke-linecap:round;stroke-width:var(--ovm-dot-w)}
+.ov-mark[data-state=busy] .ot{opacity:.3;stroke-dasharray:0 12.5}
+.ov-mark[data-state=busy] .o{stroke-dasharray:0 12.5 0 12.5 0 75;animation:ovmDots .96s steps(8,end) infinite}
+.ov-mark[data-state=busy] .og{animation:ovmSettle .4s cubic-bezier(.2,1.3,.3,1) 1}
+/* Thinking: three dots inside the O rise in turn, the ring quiets, the V's strokes glimmer one after the other. */
+.ov-mark .td{display:none;fill:var(--ovm-o);transform-box:fill-box;transform-origin:center}
+.ov-mark[data-state=think] .td{display:inline;animation:ovmThink 1.2s ease-in-out infinite}
+.ov-mark[data-state=think] .td2{animation-delay:.16s}
+.ov-mark[data-state=think] .td3{animation-delay:.32s}
+.ov-mark[data-state=think] .o{stroke-opacity:.5;transition:stroke-opacity .3s}
+.ov-mark[data-state=think] .va{animation:ovmSpeak 1.2s ease-in-out infinite}
+.ov-mark[data-state=think] .vb{animation:ovmSpeak 1.2s ease-in-out .6s infinite}
.ov-mark[data-state=ok]{--ovm-o:var(--success,#22c55e);--ovm-v:var(--success,#22c55e)}
.ov-mark[data-state=ok]{--ovm-o:var(--success,#22c55e);--ovm-v:var(--success,#22c55e)}
.ov-mark[data-state=ok] .vg{animation:ovmHop .56s ease-out .14s 1 both}
.ov-mark[data-state=ok] .vg{animation:ovmHop .56s ease-out .14s 1 both}
.ov-mark[data-state=error]{--ovm-o:var(--danger,#ef4444);--ovm-v:var(--danger,#ef4444)}
.ov-mark[data-state=error]{--ovm-o:var(--danger,#ef4444);--ovm-v:var(--danger,#ef4444)}
@@ -171,6 +187,10 @@ html[data-ov-season-mark] .ov-mark[data-ovm-intro] .sl{animation:ovmLeaf .9s cub
@@ -171,6 +187,10 @@ html[data-ov-season-mark] .ov-mark[data-ovm-intro] .sl{animation:ovmLeaf .9s cub
@keyframes ovmHop{0%,100%{transform:translateY(0) scaleY(1)}35%{transform:translateY(-9px) scaleY(1.04)}70%{transform:translateY(0) scaleY(.96)}}
@keyframes ovmHop{0%,100%{transform:translateY(0) scaleY(1)}35%{transform:translateY(-9px) scaleY(1.04)}70%{transform:translateY(0) scaleY(.96)}}
@keyframes ovmBob{0%,100%{transform:translateY(0)}50%{transform:translateY(6px)}}
@keyframes ovmBob{0%,100%{transform:translateY(0)}50%{transform:translateY(6px)}}
@keyframes ovmBreathe{0%,100%{transform:scale(1)}50%{transform:scale(.88)}}
@keyframes ovmBreathe{0%,100%{transform:scale(1)}50%{transform:scale(.88)}}
+@keyframes ovmDots{to{stroke-dashoffset:-100}}
+@keyframes ovmSettle{from{transform:scale(.86)}}
+@keyframes ovmThink{0%,55%,100%{transform:translateY(0);opacity:.4}28%{transform:translateY(-2.6px);opacity:1}}
+@keyframes ovmSpeak{0%,100%{opacity:1}50%{opacity:.5}}
@keyframes ovmPulse{0%,100%{transform:scale(1)}50%{transform:scale(1.06)}}
@keyframes ovmPulse{0%,100%{transform:scale(1)}50%{transform:scale(1.06)}}
@keyframes ovmShake{0%,100%{transform:translateX(0)}15%{transform:translateX(-7px)}30%{transform:translateX(7px)}45%{transform:translateX(-6px)}60%{transform:translateX(6px)}80%{transform:translateX(-3px)}}
@keyframes ovmShake{0%,100%{transform:translateX(0)}15%{transform:translateX(-7px)}30%{transform:translateX(7px)}45%{transform:translateX(-6px)}60%{transform:translateX(6px)}80%{transform:translateX(-3px)}}
@keyframes ovmNo{0%,100%{transform:translateX(0)}20%{transform:translateX(-4px)}40%{transform:translateX(4px)}60%{transform:translateX(-3px)}80%{transform:translateX(3px)}}
@keyframes ovmNo{0%,100%{transform:translateX(0)}20%{transform:translateX(-4px)}40%{transform:translateX(4px)}60%{transform:translateX(-3px)}80%{transform:translateX(3px)}}
@@ -208,7 +228,7 @@ html[data-ov-motion=off] .ov-mark:not([data-fx-demo] *) *{animation-iteration-co
@@ -208,7 +228,7 @@ html[data-ov-motion=off] .ov-mark:not([data-fx-demo] *) *{animation-iteration-co
<linearGradient id="${id}gg"><stop offset="0" stop-color="#fff" stop-opacity="0"/><stop offset=".5" stop-color="#fff" stop-opacity=".8"/><stop offset="1" stop-color="#fff" stop-opacity="0"/></linearGradient>
<linearGradient id="${id}gg"><stop offset="0" stop-color="#fff" stop-opacity="0"/><stop offset=".5" stop-color="#fff" stop-opacity=".8"/><stop offset="1" stop-color="#fff" stop-opacity="0"/></linearGradient>
<mask id="${id}s" maskUnits="userSpaceOnUse" x="-20" y="-20" width="140" height="140"><g mask="url(#${id}h)"><g class="og" style="${oc}"><path d="${G.ring}" fill="none" stroke="#fff" stroke-width="${G.sw}"/></g></g><g class="vg" style="${vc}"><path class="ma" d="${seg(p, 0)}" fill="none" stroke="#fff" stroke-linecap="round" stroke-width="${G.vsw}"/><path class="mb" d="${seg(p, 4)}" fill="none" stroke="#fff" stroke-linecap="round" stroke-width="${G.vsw}"/></g></mask></defs>
<mask id="${id}s" maskUnits="userSpaceOnUse" x="-20" y="-20" width="140" height="140"><g mask="url(#${id}h)"><g class="og" style="${oc}"><path d="${G.ring}" fill="none" stroke="#fff" stroke-width="${G.sw}"/></g></g><g class="vg" style="${vc}"><path class="ma" d="${seg(p, 0)}" fill="none" stroke="#fff" stroke-linecap="round" stroke-width="${G.vsw}"/><path class="mb" d="${seg(p, 4)}" fill="none" stroke="#fff" stroke-linecap="round" stroke-width="${G.vsw}"/></g></mask></defs>
<g class="m"><circle class="rp" cx="${G.cx}" cy="${G.cy}" r="${G.R}"/><circle class="rp rp2" cx="${G.cx}" cy="${G.cy}" r="${G.R}"/>
<g class="m"><circle class="rp" cx="${G.cx}" cy="${G.cy}" r="${G.R}"/><circle class="rp rp2" cx="${G.cx}" cy="${G.cy}" r="${G.R}"/>
-<g mask="url(#${id}h)"><g class="og" style="${oc}"><path class="ot" d="${G.ring}" stroke-width="${G.sw}"/><path class="o" d="${G.ring}" pathLength="100" stroke-width="${G.sw}" style="stroke:url(#${id}go)"/><circle class="cd" cx="${G.cx}" cy="${G.cy}" r="${f(ri * 0.62)}"/>
+<g mask="url(#${id}h)"><g class="og" style="${oc};--ovm-dot-w:${f(G.sw * 0.62)}"><path class="ot" d="${G.ring}" pathLength="100" stroke-width="${G.sw}"/><path class="o" d="${G.ring}" pathLength="100" stroke-width="${G.sw}" style="stroke:url(#${id}go)"/><circle class="cd" cx="${G.cx}" cy="${G.cy}" r="${f(ri * 0.62)}"/>${[-1, 0, 1].map((k, n) => `<circle class="td${n ? ` td${n + 1}` : ''}" cx="${f(G.cx + k * ri * 0.52)}" cy="${G.cy}" r="${f(ri * 0.21)}"/>`).join('')}
<g class="sw2"><path class="sc" d="M${c0[0]} ${c0[1]}A${capR} ${capR} 0 0 1 ${c1[0]} ${c1[1]}"/></g></g></g>
<g class="sw2"><path class="sc" d="M${c0[0]} ${c0[1]}A${capR} ${capR} 0 0 1 ${c1[0]} ${c1[1]}"/></g></g></g>
<g class="vg" style="${vc}"><path class="va" d="${seg(p, 0)}" stroke-width="${G.vsw}" style="stroke:url(#${id}gv)"/><path class="vb" d="${seg(p, 4)}" stroke-width="${G.vsw}" style="stroke:url(#${id}gv)"/>
<g class="vg" style="${vc}"><path class="va" d="${seg(p, 0)}" stroke-width="${G.vsw}" style="stroke:url(#${id}gv)"/><path class="vb" d="${seg(p, 4)}" stroke-width="${G.vsw}" style="stroke:url(#${id}gv)"/>
<g class="sw2"><circle class="sd" cx="${G.A[0]}" cy="${f(G.A[1] - G.vsw * 0.18)}" r="${f(G.vsw * 0.36)}"/><circle class="sd" cx="${G.B[0]}" cy="${f(G.B[1] - G.vsw * 0.18)}" r="${f(G.vsw * 0.36)}"/></g></g>
<g class="sw2"><circle class="sd" cx="${G.A[0]}" cy="${f(G.A[1] - G.vsw * 0.18)}" r="${f(G.vsw * 0.36)}"/><circle class="sd" cx="${G.B[0]}" cy="${f(G.B[1] - G.vsw * 0.18)}" r="${f(G.vsw * 0.36)}"/></g></g>
@@ -1,12 +1,12 @@
@@ -1,12 +1,12 @@
{
{
"name": "openvibe-shared",
"name": "openvibe-shared",
- "version": "4.185.0",
+ "version": "4.186.0",
"lockfileVersion": 3,
"lockfileVersion": 3,
"requires": true,
"requires": true,
"packages": {
"packages": {
"": {
"": {
"name": "openvibe-shared",
"name": "openvibe-shared",
- "version": "4.185.0",
+ "version": "4.186.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.185.0",
+ "version": "4.186.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": {
@@ -374,6 +374,9 @@
@@ -374,6 +374,9 @@
transform: none; transition: transform .42s cubic-bezier(.2, 1.1, .3, 1), background-color .3s, box-shadow .3s; }
transform: none; transition: transform .42s cubic-bezier(.2, 1.1, .3, 1), background-color .3s, box-shadow .3s; }
.ovs.ovs-anchored .ovs-orb.is-from { transform: translate(var(--orb-dx, 0px), var(--orb-dy, 0px)) scale(.9); background: transparent; box-shadow: none; }
.ovs.ovs-anchored .ovs-orb.is-from { transform: translate(var(--orb-dx, 0px), var(--orb-dy, 0px)) scale(.9); background: transparent; box-shadow: none; }
.ovs.ovs-anchored .ovs-orb::after { content: ""; position: absolute; inset: -4px; }
.ovs.ovs-anchored .ovs-orb::after { content: ""; position: absolute; inset: -4px; }
+ /* A ring leaves the orb as its sheet opens (4.186.0; the Dock's sheet does the same). */
+ .ovs.ovs-anchored .ovs-orb.is-x::before { content: ""; position: absolute; inset: 0; border-radius: 50%; border: 2px solid var(--ovo-a); pointer-events: none; animation: ovs-ring .6s cubic-bezier(.2,.8,.2,1) .1s both; }
+ @keyframes ovs-ring { from { opacity: .8; transform: scale(.9); } to { opacity: 0; transform: scale(calc(1 + 2.2 * var(--ov-move, 1))); } }
.ovs.ovs-anchored .ovs-orb:active { transform: scale(.9); }
.ovs.ovs-anchored .ovs-orb:active { transform: scale(.9); }
.ovs.ovs-anchored .ovs-orb svg { display: block; overflow: visible; }
.ovs.ovs-anchored .ovs-orb svg { display: block; overflow: visible; }
.ovs.is-closing .ovs-panel { animation: ovs-fold .28s cubic-bezier(.5, 0, .75, .2) both; }
.ovs.is-closing .ovs-panel { animation: ovs-fold .28s cubic-bezier(.5, 0, .75, .2) both; }
@@ -217,7 +217,7 @@ const server = http.createServer((req, res) => {
@@ -217,7 +217,7 @@ const server = http.createServer((req, res) => {
});
});
const wait = (ms) => new Promise((r) => setTimeout(r, ms));
const wait = (ms) => new Promise((r) => setTimeout(r, ms));
-async function until(p, expr, what, ms = 5000) {
+async function until(p, expr, what, ms = 15000) { // a limit on a poll: a loaded runner drew the pill after 5 s
const end = Date.now() + ms;
const end = Date.now() + ms;
for (;;) {
for (;;) {
const v = await p.evaluate(expr);
const v = await p.evaluate(expr);
@@ -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 = 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 DOCK_BROTLI_BUDGET = 20736; // 20.25 KB: 20.04 KB with a phone's sheet opening and closing as the menu's does (a circle from the orb, the menu's close button in the menu's place, a ring as it opens; 4.186.0, 2026-11-10); 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 ──────────────────────────────────────────────
@@ -74,6 +74,12 @@ assert.ok(/html\[data-ov-motion=off\] \*:not\(\[data-fx-demo\] \*\)[^{]*\{animat
@@ -74,6 +74,12 @@ assert.ok(/html\[data-ov-motion=off\] \*:not\(\[data-fx-demo\] \*\)[^{]*\{animat
assert.ok(/CALM_RATE = 0\.6/.test(SRC) && SRC.includes('t.iterations !== Infinity') && SRC.includes("addEventListener('animationstart'"), 'under Calm an endless loop is slowed as it starts; anything that ends plays as written');
assert.ok(/CALM_RATE = 0\.6/.test(SRC) && SRC.includes('t.iterations !== Infinity') && SRC.includes("addEventListener('animationstart'"), 'under Calm an endless loop is slowed as it starts; anything that ends plays as written');
assert.ok(SRC.includes("motionOf() === 'off') return;"), 'the theme crossfade is a colour fade: only Off skips it');
assert.ok(SRC.includes("motionOf() === 'off') return;"), 'the theme crossfade is a colour fade: only Off skips it');
+// ── a control nobody styled wears the theme (4.186.0) ──
+assert.ok(/:where\(button,input:is\(\[type=submit\],\[type=button\],\[type=reset\]\)\):where\(:not\(\[class\]\)\)\{font-family:inherit;border:1px solid var\(--border-light,[^;]*;border-radius:10px;color:var\(--text-primary,inherit\);background:var\(--bg-hover,/.test(css), 'a bare button: the theme\'s face, at no specificity, readable even where no theme is set');
+assert.ok(/:where\(input:not\(\[type=checkbox\][^)]*\),select,textarea\):where\(:not\(\[class\]\)\)\{font-family:inherit;border:1px solid/.test(css), 'a bare field, select or text box too');
+const bare = css.match(/:where\((?:button|input)[^{]*\{[^}]*\}/g) || [];
+assert.ok(bare.length >= 3 && bare.every((r) => /^:where\([^{]*:where\(:not\(\[class\]\)\)\{/.test(r) && !/padding|height|width|margin|display/.test(r)), 'only controls with no class, and nothing that moves a layout');
+
// ── the package: nothing stops an effect because the device asked for less ──
// ── the package: nothing stops an effect because the device asked for less ──
const STOPS = /(animation|transition)\s*:\s*none/;
const STOPS = /(animation|transition)\s*:\s*none/;
const offenders = [];
const offenders = [];
@@ -352,6 +352,13 @@ function setup({ reduce = false, session = memoryStorage(), start = 1000000, roo
@@ -352,6 +352,13 @@ function setup({ reduce = false, session = memoryStorage(), start = 1000000, roo
assert.match(css, /\.ov-mark\[data-ovm-intro\] \.gr\{animation:ovmGlint/, 'a glint ends the intro');
assert.match(css, /\.ov-mark\[data-ovm-intro\] \.gr\{animation:ovmGlint/, 'a glint ends the intro');
assert.match(css, /\.ov-mark:hover \.gr\{animation:ovmGlint|\.ov-mark:hover \.gr,/, 'and crosses on hover');
assert.match(css, /\.ov-mark:hover \.gr\{animation:ovmGlint|\.ov-mark:hover \.gr,/, 'and crosses on hover');
assert.match(css, /\.ov-mark\[data-glow\]::before\{opacity:\.42/, 'data-glow keeps the glow on');
assert.match(css, /\.ov-mark\[data-glow\]::before\{opacity:\.42/, 'data-glow keeps the glow on');
+ // Working and thinking (4.186.0): the O as eight dots with three bright ones going round, dot to dot; three dots
+ // rising in turn inside the O while the V's strokes glimmer. One sign each, wherever something works or writes.
+ assert.match(css, /\.ov-mark\[data-state=busy\] \.ot\{opacity:\.3;stroke-dasharray:0 12\.5\}/, 'working: the O is eight dots');
+ assert.match(css, /\.ov-mark\[data-state=busy\] \.o\{stroke-dasharray:0 12\.5 0 12\.5 0 75;animation:ovmDots \.96s steps\(8,end\) infinite\}/, 'three bright ones hop round it');
+ assert.ok(!/\[data-state=busy\] \.og\{animation:ovmBreathe/.test(css), 'the O no longer just breathes while it works');
+ assert.match(css, /\.ov-mark\[data-state=think\] \.td\{display:inline;animation:ovmThink 1\.2s ease-in-out infinite\}/, 'thinking: three dots inside the O');
+ assert.match(css, /\.ov-mark\[data-state=think\] \.vb\{animation:ovmSpeak 1\.2s ease-in-out \.6s infinite\}/, 'and the V glimmers, one stroke after the other');
assert.match(css, /\.ov-mark\[data-tone\]::before,\.ov-mark\[data-state=sleep\]::before\{display:none\}/);
assert.match(css, /\.ov-mark\[data-tone\]::before,\.ov-mark\[data-state=sleep\]::before\{display:none\}/);
assert.ok(!css.includes('::before{transition:none!important}') && css.includes('html[data-ov-motion=off] .ov-mark:not([data-fx-demo] *) *{animation-iteration-count:1!important}'), 'the glow fades in under Calm too; only motion off holds the mark still (outside a demo scope)');
assert.ok(!css.includes('::before{transition:none!important}') && css.includes('html[data-ov-motion=off] .ov-mark:not([data-fx-demo] *) *{animation-iteration-count:1!important}'), 'the glow fades in under Calm too; only motion off holds the mark still (outside a demo scope)');
assert.match(css, /\.ov-mark:active \.m\{transform:scale\(\.9\)/, 'a press gives a little');
assert.match(css, /\.ov-mark:active \.m\{transform:scale\(\.9\)/, 'a press gives a little');
@@ -58,7 +58,12 @@ const server = http.createServer((req, res) => {
@@ -58,7 +58,12 @@ const server = http.createServer((req, res) => {
assert.strictEqual(await p.evaluate('Math.round(scrollY)'), await p.evaluate('Math.round(window.__before)'), 'close restores page position');
assert.strictEqual(await p.evaluate('Math.round(scrollY)'), await p.evaluate('Math.round(window.__before)'), 'close restores page position');
assert.strictEqual(await p.evaluate('document.activeElement.id'), 'open', 'focus returns to opener');
assert.strictEqual(await p.evaluate('document.activeElement.id'), 'open', 'focus returns to opener');
await p.evaluate(`(() => { const btn=document.querySelector('#open');btn.dispatchEvent(new PointerEvent('pointerdown',{bubbles:true}));btn.click(); })()`);
await p.evaluate(`(() => { const btn=document.querySelector('#open');btn.dispatchEvent(new PointerEvent('pointerdown',{bubbles:true}));btn.click(); })()`);
- await new Promise((ok) => setTimeout(ok, 40));
+ // The sheet takes the swipe only once the surfaces layer has adopted it (the page is locked): wait for that, not
+ // for a fixed 40 ms (a slow macOS runner had not got there yet, twice on 2026-11-10).
+ let adopted = false;
+ for (let i = 0; i < 60 && !adopted; i++) { adopted = await p.evaluate('document.querySelector("#sheet").open && document.body.style.position === "fixed"'); if (!adopted) await new Promise((ok) => setTimeout(ok, 50)); }
+ assert.strictEqual(adopted, true, 'the sheet opens again and locks the page');
+ await new Promise((ok) => setTimeout(ok, 60));
await p.evaluate(`(() => { const head=document.querySelector('[data-ov-sheet-head]');
await p.evaluate(`(() => { const head=document.querySelector('[data-ov-sheet-head]');
const fire=(type,y)=>head.dispatchEvent(new PointerEvent(type,{bubbles:true,pointerId:5,pointerType:'touch',clientY:y}));
const fire=(type,y)=>head.dispatchEvent(new PointerEvent(type,{bubbles:true,pointerId:5,pointerType:'touch',clientY:y}));
fire('pointerdown',200);fire('pointermove',330);fire('pointerup',330); })()`);
fire('pointerdown',200);fire('pointermove',330);fire('pointerup',330); })()`);
@@ -51,7 +51,8 @@ assert.ok(LT.arctic['--success'], 'loader entries carry the full token set');
@@ -51,7 +51,8 @@ assert.ok(LT.arctic['--success'], 'loader entries carry the full token set');
// 62,000 since 4.64.0: the look travels between sites (?ov_look=) and every copy is dated; brotli 15.45 KB (was 15.41) (2026-10-31).
// 62,000 since 4.64.0: the look travels between sites (?ov_look=) and every copy is dated; brotli 15.45 KB (was 15.41) (2026-10-31).
// 63,500 since 4.111.1 (2026-11-05): the FAVICON block draws the Pair (about 1.2 KB raw) instead of the retired ring.
// 63,500 since 4.111.1 (2026-11-05): the FAVICON block draws the Pair (about 1.2 KB raw) instead of the retired ring.
// 66,500 since 4.183.0 (2026-11-10): motion has four choices, --ov-move, and Calm paces endless loops (about 2.9 KB raw).
// 66,500 since 4.183.0 (2026-11-10): motion has four choices, --ov-move, and Calm paces endless loops (about 2.9 KB raw).
-assert.ok(loader.length < 66500, `theme-loader.js stays small for first paint (${loader.length} characters)`);
+// 67,600 since 4.186.0 (2026-11-10): a control nobody styled wears the theme (three rules, about 0.9 KB raw with their note).
+assert.ok(loader.length < 67600, `theme-loader.js stays small for first paint (${loader.length} characters)`);
assert.ok(loader.includes('KNOWN_PROPS'), 'loader clears tokens the new theme does not set');
assert.ok(loader.includes('KNOWN_PROPS'), 'loader clears tokens the new theme does not set');
// 5. Derived tokens: text on the accent always reads, on every theme.
// 5. Derived tokens: text on the accent always reads, on every theme.
@@ -357,6 +357,12 @@
@@ -357,6 +357,12 @@
// Status names some sites use (--error, --error-text, --ok, --warn, --accent-text) that no theme sets: they resolve to the theme's
// Status names some sites use (--error, --error-text, --ok, --warn, --accent-text) that no theme sets: they resolve to the theme's
// readable status colours instead of each site's fallback literal (pale on light themes). :where() keeps them
// readable status colours instead of each site's fallback literal (pale on light themes). :where() keeps them
// below any site's own value (4.86.0).
// below any site's own value (4.86.0).
+ // A control nobody styled wears the theme, not the browser's grey (4.186.0): only one with no class, only its face,
+ // at no specificity, so any rule a site wrote still wins and no layout moves. Its shape is the kit's job.
+ const BARE = ':where(:not([class]))', FACE = '{font-family:inherit;border:1px solid var(--border-light,var(--border,color-mix(in srgb,currentColor 30%,transparent)));border-radius:10px;color:var(--text-primary,inherit);background:';
+ const BARE_CSS = ':where(button,input:is([type=submit],[type=button],[type=reset]))' + BARE + FACE + 'var(--bg-hover,var(--bg-card,color-mix(in srgb,currentColor 8%,transparent)))}'
+ + ':where(input:not([type=checkbox],[type=radio],[type=range],[type=color],[type=file],[type=image],[type=submit],[type=button],[type=reset],[type=hidden]),select,textarea)' + BARE + FACE + 'var(--bg-input,var(--bg-primary,transparent))}'
+ + ':where(input:is([type=checkbox],[type=radio],[type=range]),progress)' + BARE + '{accent-color:var(--accent,auto)}';
const ALIAS_CSS = ':where(html){--error:var(--danger);--error-text:var(--danger-text);--ok:var(--success-text);--warn:var(--warning-text);--accent-text:var(--accent-strong)}';
const ALIAS_CSS = ':where(html){--error:var(--danger);--error-text:var(--danger-text);--ok:var(--success-text);--warn:var(--warning-text);--accent-text:var(--accent-strong)}';
// Comfort (4.45.0): one <html> attribute each, !important over the inline theme (Okabe-Ito colours, OLED black).
// Comfort (4.45.0): one <html> attribute each, !important over the inline theme (Okabe-Ito colours, OLED black).
const COMFORT_CSS = 'html[data-ov-contrast=more]{--text-secondary:color-mix(in srgb,var(--text-primary) 88%,var(--bg-primary))!important;--text-muted:color-mix(in srgb,var(--text-primary) 76%,var(--bg-primary))!important;--border:color-mix(in srgb,var(--text-primary) 40%,var(--bg-primary))!important;--border-light:color-mix(in srgb,var(--text-primary) 55%,var(--bg-primary))!important}'
const COMFORT_CSS = 'html[data-ov-contrast=more]{--text-secondary:color-mix(in srgb,var(--text-primary) 88%,var(--bg-primary))!important;--text-muted:color-mix(in srgb,var(--text-primary) 76%,var(--bg-primary))!important;--border:color-mix(in srgb,var(--text-primary) 40%,var(--bg-primary))!important;--border-light:color-mix(in srgb,var(--text-primary) 55%,var(--bg-primary))!important}'
@@ -411,7 +417,7 @@
@@ -411,7 +417,7 @@
for (const k in COMFORT_ATTRS) { const [attr, off] = COMFORT_ATTRS[k]; if (d[k] && d[k] !== off) el.setAttribute(attr, d[k]); else el.removeAttribute(attr); }
for (const k in COMFORT_ATTRS) { const [attr, off] = COMFORT_ATTRS[k]; if (d[k] && d[k] !== off) el.setAttribute(attr, d[k]); else el.removeAttribute(attr); }
if (!document.getElementById('ov-display-css') && document.head) {
if (!document.getElementById('ov-display-css') && document.head) {
const st = document.createElement('style'); st.id = 'ov-display-css';
const st = document.createElement('style'); st.id = 'ov-display-css';
- st.textContent = 'html[data-ov-text]{font-size:calc(100% * var(--ov-text-scale,1))}' + MOTION_CSS + ALIAS_CSS + COMFORT_CSS;
+ st.textContent = 'html[data-ov-text]{font-size:calc(100% * var(--ov-text-scale,1))}' + MOTION_CSS + BARE_CSS + ALIAS_CSS + COMFORT_CSS;
document.head.appendChild(st);
document.head.appendChild(st);
}
}
paceLoops();
paceLoops();