openvibers/openvibe.shared Commits
The Dock fills the phone; one-row header with More; Messages (#355)
Changes
@@@ -16,6 +16,13 @@and counts today's changes.- **The first tab is the site's name, never a page's word for itself** ("Home", "Welcome").++- **The Dock fills the phone, and its header is one row** (owner, 11-09: "go all the way to the top… wasted space", "I++ don't really understand what the pin does", "clicked off… it didn't close the pop over"). Phones: the Dock is the++ whole screen, over the bar, inside the notch area; tablets: a full-height panel on the right; desktops: a 420 × 640++ card by default. The header is the four tabs, then More and Close; More holds Start a timer, "Keep the Dock's button++ on every page" (a switch that says what off means) and Open in a window of its own. More and the timer close on a tap++ anywhere else, in the Dock or on the page, and on Escape. The Chats tab is **Messages**.++## 4.158.0 — 2026-11-09- **The menu's Home.** A site whose links never name its home (Exchange: Models, Chat, Arena…) now gets Home first in@@@ -2031,7 +2038,6 @@ What the front-page passes of 10-29 (Media, Quest, Actor, Rent, Services, Communsites had patched this in their own stylesheets on 2026-10-29; the kit now owns it for every site thatincludes `showcase.css`.--## 4.34.0 — 2026-10-29- **Agent discovery:** the server-only `agent-layer` module serves `/.well-known/agent.json`@@@ -29,7 +29,7 @@'use strict';if (typeof document === 'undefined' || root.OpenVibeDock) return;-- const W = [320, 720], H = [360, 900], DEF = { width: 400, height: 560 };++ const W = [320, 720], H = [360, 900], DEF = { width: 420, height: 640 };const CORNERS = ['bottom-right', 'bottom-left', 'top-right', 'top-left'];const svg = (p) => `<svg viewBox="0 0 24 24" aria-hidden="true">${p}</svg>`;const G = {@@@ -42,10 +42,11 @@back: '<path d="M14.5 6.5L9 12l5.5 5.5"/>',send: '<path d="M4.5 12h11M11 6.5l5.5 5.5-5.5 5.5"/>',vibe: '<path d="M12 4.5a7.5 7.5 0 0 1 6.4 11.4L19.5 19.5l-3.6-1.1A7.5 7.5 0 1 1 12 4.5z"/><path d="M9 11.5h.01M12 11.5h.01M15 11.5h.01"/>',++ more: '<path d="M6 12h.01M12 12h.01M18 12h.01" stroke-width="3"/>',clock: '<circle cx="12" cy="13" r="7.5"/><path d="M12 9.5V13l2.5 1.5M9.5 3h5"/>',};// Vibe first (L48 V2): the person's AI, on every site.-- const TABS = [['vibe', 'Vibe', G.vibe], ['agent', 'Agents', G.spark], ['chats', 'Chats', G.chat], ['activity', 'Activity', G.bell]];++ const TABS = [['vibe', 'Vibe', G.vibe], ['agent', 'Agents', G.spark], ['chats', 'Messages', G.chat], ['activity', 'Activity', G.bell]];// The tabs the Network keeps in the person's Dock state (network.dock-state@1; vibe since contracts 0.270.0); a// newer one stays on this site's copy until the Network learns it.const SAVED_TABS = ['vibe', 'agent', 'chats', 'activity'];@@@ -172,8 +173,20 @@.ovdk-acts button { width: 40px; height: 40px; border: 0; border-radius: 12px; background: transparent; color: var(--text-muted, #8b8b96); cursor: pointer; display: grid; place-items: center; }.ovdk-acts button:hover { background: color-mix(in srgb, var(--text-primary, #ececf1) 7%, transparent); color: var(--text-primary, #ececf1); }.ovdk-acts button svg { width: 19px; height: 19px; }-- .ovdk-acts [data-act="pin"][aria-pressed="true"] { color: var(--accent-light, var(--accent, #60a5fa)); }-- .ovdk-acts [data-act="pin"][aria-pressed="true"] svg { fill: color-mix(in srgb, currentColor 22%, transparent); }++ .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; }++ .ovdk-more button { display: flex; align-items: center; gap: 12px; min-height: 52px; padding: 8px 10px; border: 0; border-radius: 12px; background: transparent; color: var(--text-primary, #ececf1); font: inherit; text-align: left; cursor: pointer; }++ .ovdk-more button[hidden] { display: none; }++ .ovdk-more button:hover, .ovdk-more button:focus-visible { background: color-mix(in srgb, var(--text-primary, #ececf1) 7%, transparent); outline: none; }++ .ovdk-more button > svg { flex: none; width: 20px; height: 20px; color: var(--text-muted, #8b8b96); }++ .ovdk-more span { flex: 1; min-width: 0; display: grid; gap: 2px; }++ .ovdk-more b { font-size: 14px; font-weight: 650; }++ .ovdk-more small { font-size: 12px; color: var(--text-muted, #8b8b96); line-height: 1.35; }++ .ovdk-sw { flex: none; position: relative; width: 38px; height: 22px; border-radius: 11px; background: var(--bg-tertiary, rgba(255,255,255,.14)); transition: background .15s; }++ .ovdk-sw::after { content: ''; position: absolute; top: 3px; left: 3px; width: 16px; height: 16px; border-radius: 50%; background: #fff; transition: transform .15s; }++ .ovdk-more [aria-checked="true"] .ovdk-sw { background: var(--accent-strong, var(--accent, #2563eb)); }++ .ovdk-more [aria-checked="true"] .ovdk-sw::after { transform: translateX(16px); }.ovdk-body { position: relative; flex: 1; min-height: 0; display: flex; flex-direction: column; border-top: 1px solid var(--border, rgba(255,255,255,.08)); }.ovdk-panel { flex: 1; min-height: 0; display: flex; flex-direction: column; }.ovdk-panel[hidden] { display: none; }@@@ -248,11 +261,10 @@.ovdk-err[hidden] { display: none; }.ovdk-none b { display: block; margin-bottom: 4px; font-size: 15px; color: var(--text-primary, #ececf1); }@media (max-width: 640px) {-- .ovdk, .ovdk[data-corner] { left: 0; right: 0; top: auto; bottom: 0; width: auto !important; height: min(86vh, 720px) !important; height: min(86dvh, 720px) !important; max-width: none; max-height: none;-- border-radius: 24px 24px 0 0; border-bottom: 0; padding-bottom: env(safe-area-inset-bottom, 0px); box-shadow: 0 0 0 100vmax rgba(0,0,0,.5), 0 -12px 40px rgba(0,0,0,.35); transform-origin: 50% 100%; }++ .ovdk, .ovdk[data-corner] { z-index: 10010; left: 0; right: 0; top: 0; bottom: 0; width: auto !important; height: auto !important; max-width: none; max-height: none;++ border: 0; border-radius: 0; padding-top: env(safe-area-inset-top, 0px); padding-bottom: env(safe-area-inset-bottom, 0px); box-shadow: none; transform-origin: 50% 100%; }.ovdk.is-in { animation-name: ovdk-rise; animation-duration: .3s; }-- .ovdk-head { padding-top: 22px; cursor: default; }-- .ovdk-head::before { content: ''; position: absolute; top: 8px; left: 50%; width: 40px; height: 4px; margin-left: -20px; border-radius: 2px; background: var(--border-light, var(--border, rgba(255,255,255,.22))); }++ .ovdk-head { cursor: default; }.ovdk-size { display: none; }.ovdk-tabs { flex: 1; } .ovdk-tabs button { flex: 1; justify-content: center; }/* A thumb, not a cursor: every control on the sheet is 44 px (design 2026-11-07 §1, Phone). */@@@ -260,17 +272,23 @@.ovdk-acts button { width: 44px; height: 44px; }/* Two rows (4.150.0, the owner: the strip was "cut off", "Activity" read "Ac"): the tools sit beside the grabhandle; the four tabs share the full width below, each icon over its name, every one whole. */-- .ovdk-head { flex-wrap: wrap; row-gap: 6px; padding: 8px 8px 8px 10px; }-- .ovdk-head::before { top: 10px; }-- .ovdk-acts { order: -1; width: 100%; justify-content: flex-end; gap: 2px; min-height: 44px; }++ .ovdk-head { flex-wrap: nowrap; gap: 4px; padding: 6px 6px 6px 8px; }++ .ovdk-acts { flex: none; gap: 0; }.ovdk-acts button[data-act="pop"] { display: none !important; }-- .ovdk-tabs { order: 0; flex: 1 1 100%; padding: 4px; border-radius: 16px; }++ .ovdk-tabs { order: 0; flex: 1 1 auto; min-width: 0; padding: 3px; border-radius: 16px; }.ovdk-tabs button, .ovdk-tabs button:not([aria-selected="true"]) { flex: 1 1 0; min-width: 0; height: 54px; padding: 4px 2px; flex-direction: column; justify-content: center; gap: 3px; border-radius: 12px; font-size: 11.5px; font-weight: 650; }.ovdk-tabs button span, .ovdk-tabs button:not([aria-selected="true"]) span { position: static; width: auto; height: auto; overflow: hidden; clip: auto; max-width: 100%; text-overflow: ellipsis; white-space: nowrap; }.ovdk-tabs button svg { width: 19px; height: 19px; }.ovdk-tabs .n { position: absolute; top: 4px; left: calc(50% + 6px); min-width: 17px; padding: 1px 5px; font-size: 10.5px; }.ovdk-foot a, .ovdk-foot button { min-height: 44px; }}++ @media (min-width: 641px) and (max-width: 1023px) {++ .ovdk, .ovdk[data-corner] { z-index: 10010; top: 0; bottom: 0; right: 0; left: auto; width: min(460px, calc(100vw - 48px)) !important; height: auto !important;++ max-width: none; max-height: none; border-radius: 22px 0 0 22px; border-right: 0; padding-top: env(safe-area-inset-top, 0px); padding-bottom: env(safe-area-inset-bottom, 0px);++ box-shadow: 0 0 0 100vmax rgba(0,0,0,.4), -16px 0 40px rgba(0,0,0,.3); transform-origin: 100% 50%; }++ .ovdk-size { display: none; }++ .ovdk-head { cursor: default; }++ }html.ovdk-sheet-open, html.ovdk-sheet-open body { overflow: hidden; }@media (prefers-reduced-motion: reduce) { .ovdk.is-in, .ovdk-wait span { animation: none; } }html[data-ov-motion="reduced"] .ovdk.is-in { animation: none; }@@@ -296,11 +314,14 @@<header class="ovdk-head"><div class="ovdk-tabs" role="tablist" aria-label="Dock">${TABS.map(([id, label, g]) => `<button type="button" role="tab" id="ovdk-tab-${id}" aria-controls="ovdk-panel-${id}" data-tab="${id}" aria-selected="false" tabindex="-1">${svg(g)}<span>${label}</span><b class="n" data-n="${id}"></b></button>`).join('')}</div><div class="ovdk-acts">-- <button type="button" data-act="timer" title="A timer" aria-label="Start a timer">${svg(G.clock)}</button>-- <button type="button" data-act="pop" hidden title="Open the Dock in a window of its own" aria-label="Open the Dock in a window of its own">${svg(G.open)}</button>-- <button type="button" data-act="pin" aria-pressed="false" title="Keep the Dock's button on every page" aria-label="Keep the Dock's button on every page">${svg(G.pin)}</button>++ <button type="button" data-act="more" aria-haspopup="true" aria-expanded="false" title="More" aria-label="More: a timer, the Dock's button">${svg(G.more)}</button><button type="button" data-act="close" title="Close" aria-label="Close the Dock">${svg(G.close)}</button></div>++ <div class="ovdk-more" role="menu" aria-label="More" hidden>++ <button type="button" role="menuitem" data-act="timer">${svg(G.clock)}<span><b>Start a timer</b><small>A countdown or a focus round, on the Dock's button</small></span></button>++ <button type="button" role="menuitemcheckbox" aria-checked="false" data-act="pin">${svg(G.pin)}<span><b>Keep the Dock's button on every page</b><small>When off, it shows only while something works or needs you</small></span><i class="ovdk-sw" aria-hidden="true"></i></button>++ <button type="button" role="menuitem" data-act="pop" hidden>${svg(G.open)}<span><b>Open the Dock in a window of its own</b><small>Keep it beside whatever you are doing</small></span></button>++ </div></header><div class="ovdk-body">${TABS.map(([id]) => `<div class="ovdk-panel" role="tabpanel" id="ovdk-panel-${id}" aria-labelledby="ovdk-tab-${id}" data-panel="${id}" hidden></div>`).join('')}</div><span class="ovdk-size" aria-hidden="true"></span>`;@@@ -316,7 +337,27 @@el.querySelector('[data-act="close"]').addEventListener('click', () => close());el.querySelector('[data-act="pop"]').addEventListener('click', () => { if (popped) backToPage(); else popOut(); });el.querySelector('[data-act="pin"]').addEventListener('click', () => { save({ pinned: !state().pinned }); paintPin(); });-- el.querySelector('[data-act="timer"]').addEventListener('click', (e) => { const a = e.currentTarget; withTimer((t) => t.menu(a)); });++ el.querySelector('[data-act="timer"]').addEventListener('click', () => { const a = el.querySelector('[data-act="more"]'); closeMore(); withTimer((t) => t.menu(a)); });++ const moreBtn = el.querySelector('[data-act="more"]'), moreMenu = el.querySelector('.ovdk-more');++ function closeMore() { moreMenu.hidden = true; moreBtn.setAttribute('aria-expanded', 'false'); }++ moreBtn.addEventListener('click', () => {++ const open = moreMenu.hidden;++ closeMore();++ const tm = el.querySelector('.ovdk-tmenu'); if (tm) tm.remove();++ if (open) { moreMenu.hidden = false; moreBtn.setAttribute('aria-expanded', 'true'); const first = moreMenu.querySelector('button:not([hidden])'); if (first) first.focus(); }++ });++ moreMenu.addEventListener('click', (e) => { const b = e.target.closest('button'); if (b && b.dataset.act !== 'pin') closeMore(); });++ moreMenu.addEventListener('keydown', (e) => { if (e.key === 'Escape') { e.stopPropagation(); closeMore(); moreBtn.focus(); } });++ // A pop-over (More, the timer) closes on a tap anywhere else, in the Dock or on the page (owner, 11-09: "clicked off++ // something else, and it didn't actually close").++ const awayFrom = (e) => {++ const path = typeof e.composedPath === 'function' ? e.composedPath() : [e.target];++ if (!moreMenu.hidden && !path.includes(moreMenu) && !path.includes(moreBtn)) closeMore();++ const tm = el.querySelector('.ovdk-tmenu');++ if (tm && !path.includes(tm) && !path.includes(moreMenu) && !path.includes(moreBtn)) tm.remove();++ };++ el.addEventListener('pointerdown', awayFrom, true);++ document.addEventListener('pointerdown', (e) => { if (!el.contains(e.target)) awayFrom(e); }, true);el.addEventListener('keydown', (e) => { if (e.key === 'Escape') { e.stopPropagation(); close(); } });// A tap outside closes the phone sheet. The path is the one at dispatch: a tap that redraws its own panel (opening a// chat) leaves its target detached, and must not read as outside.@@@ -395,7 +436,7 @@setTimeout(() => { el.style.transition = ''; }, 300);};head.addEventListener('pointerdown', (e) => {-- if (e.button || e.target.closest('.ovdk-acts')) return;++ if (e.button || e.target.closest('.ovdk-acts, .ovdk-more')) return;s = { x: e.clientX, y: e.clientY, r: el.getBoundingClientRect(), dx: 0, dy: 0, id: e.pointerId, live: false };root.addEventListener('pointermove', move); root.addEventListener('pointerup', end); root.addEventListener('pointercancel', end);});@@@ -439,15 +480,14 @@n('agent', agent, c.waiting ? 'waiting' : '');n('activity', c.unread || 0);n('chats', chats.unread || 0);-- el.querySelector('#ovdk-tab-chats').setAttribute('aria-label', chats.unread ? `Chats, ${chats.unread} unread` : 'Chats');++ el.querySelector('#ovdk-tab-chats').setAttribute('aria-label', chats.unread ? `Messages, ${chats.unread} unread` : 'Messages');el.querySelector('#ovdk-tab-agent').setAttribute('aria-label', [`Agents`, c.waiting ? `${c.waiting} need${c.waiting === 1 ? 's' : ''} you` : '', c.running ? `${c.running} working` : ''].filter(Boolean).join(', '));el.querySelector('#ovdk-tab-activity').setAttribute('aria-label', c.unread ? `Activity, ${c.unread} unread` : 'Activity');}function paintPin() {const b = el.querySelector('[data-act="pin"]'), on = !!state().pinned;-- b.setAttribute('aria-pressed', String(on));-- const t = on ? "The Dock's button stays on every page" : "Keep the Dock's button on every page";-- b.title = t; b.setAttribute('aria-label', t);++ b.setAttribute('aria-checked', String(on));++ b.title = on ? "On: the Dock's button stays on every page" : "Off: the Dock's button shows only while something works or needs you";}function setTab(id, remember) {tab = TABS.some(([t]) => t === id) ? id : 'activity';@@@ -790,7 +830,7 @@if (!b) return;b.hidden = !pip();const t = popped ? 'Bring the Dock back to the page' : 'Open the Dock in a window of its own';-- b.title = t; b.setAttribute('aria-label', t);++ b.title = t; const name = b.querySelector('b'); if (name) name.textContent = t;}/** The window gets the Dock's styles, the theme's colours and the page's own type: the Dock looks the same there. */@@@ -539,7 +539,7 @@const NAV_ICONS_REV = 'd9a4febbbad7'; // 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': '1e91c9c0f347', 'ov-glyphs.js': '1a0d8ce6909f', 'ov-brand.woff2': '3b4e6a8a5878', 'ov-look.js': '2a6c53a7b176', 'ov-fx.js': '46fce1e2b8db', 'ov-labels.js': 'a30b178391f5', 'ov-forms.js': '068ce1b1c4d5', 'ov-forms.css': '98cf0f28bb68', 'ov-share.js': '8e55bfe93238', 'ov-share.css': 'cbd18e6ce991', 'search.js': 'b113a2fd8e93', 'dock.js': '4dacf614797b', 'presence.js': '1b675c37d888', 'panels.js': '7d8267ca8035', 'notification-ui.js': 'a2d37237ba0b', 'history.js': '1e6598f78ba4', 'site-switcher.js': '292bea111aa8', 'appbar.js': '1edf7532f228', 'release-watch.js': '25f02a3f0684', 'ov-icons.js': 'ba3981246bc0', 'sso-client.js': '52439265b980', 'overlay.js': '25960cf0c075', 'ov-home.js': '267bf753169d' };++ const SIBLING_REV = { 'ov-mark.js': '1e91c9c0f347', 'ov-glyphs.js': '1a0d8ce6909f', 'ov-brand.woff2': '3b4e6a8a5878', 'ov-look.js': '2a6c53a7b176', 'ov-fx.js': '46fce1e2b8db', 'ov-labels.js': 'a30b178391f5', 'ov-forms.js': '068ce1b1c4d5', 'ov-forms.css': '98cf0f28bb68', 'ov-share.js': '8e55bfe93238', 'ov-share.css': 'cbd18e6ce991', 'search.js': 'b113a2fd8e93', 'dock.js': 'd1354be66c41', 'presence.js': '1b675c37d888', 'panels.js': '7d8267ca8035', 'notification-ui.js': 'a2d37237ba0b', 'history.js': '1e6598f78ba4', 'site-switcher.js': '292bea111aa8', 'appbar.js': '1edf7532f228', 'release-watch.js': '25f02a3f0684', 'ov-icons.js': 'ba3981246bc0', 'sso-client.js': '52439265b980', 'overlay.js': '25960cf0c075', 'ov-home.js': '267bf753169d' };// 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">.@@@ -243,7 +243,9 @@ async function drag(p, from, to) {await p.evaluate("document.getElementById('ovdk-pill').click()");const card = await until(p, `(() => { const c = ${CARD}; return c && c.items ? c : null; })()`, 'the Agents tab');assert.deepStrictEqual([card.tab, card.groups, card.items], ['agent', ['waiting', 'running'], 3], 'what needs you first, then what runs');-- assert.ok(card.w === 390 && card.t > 60, 'a sheet across the phone, under the bar');++ await new Promise((ok) => setTimeout(ok, 450)); // the sheet has finished rising++ const placed = await p.evaluate(CARD);++ assert.ok(placed.w === 390 && placed.t === 0 && placed.h === 780, `the whole phone, over the bar (owner, 11-09: "all the way to the top"; ${placed.t}, ${placed.h})`);assert.strictEqual(await p.evaluate('document.getElementById("ovdk-pill").hidden'), true, 'the pill steps aside while the Dock is open');await p.evaluate("document.querySelector('.ovdk .ovac-item[data-bucket=waiting] button[data-do=approve]').click()");await until(p, "document.querySelector('.ovdk .ovac-item[data-bucket=waiting] .primary').textContent === 'Approved'", 'approved in place');@@@ -428,7 +430,7 @@ async function drag(p, from, to) {await until(p, `(() => { const c = ${CARD}; return c && c.items ? c : null; })()`, 'the desktop card');await wait(400); // past the grow-in (a transform), so the box is the card's ownlet card = await p.evaluate(CARD);-- assert.deepStrictEqual([card.corner, card.w, card.h], ['bottom-right', 400, 560], 'a 400 × 560 card in the pill\'s corner');++ assert.deepStrictEqual([card.corner, card.w, card.h], ['bottom-right', 420, 640], 'a 420 × 640 card in the pill\'s corner');// Drag the head from its middle to near the top left: it snaps to that corner.await drag(p, [card.l + 120, card.t + 26], [180, 90]); // from the Activity tab: a drag, not a tapcard = await until(p, `(() => { const c = ${CARD}; return c && c.corner === 'top-left' ? c : null; })()`, 'the card snaps to the top left');@@@ -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 = 17203; // 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 = 17920; // 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-timer.js', 'dock-agent.js', 'dock-share.js', 'dock-card.js'];// ── the budget ──────────────────────────────────────────────
@@ -16,6 +16,13 @@
@@ -16,6 +16,13 @@
and counts today's changes.
and counts today's changes.
- **The first tab is the site's name, never a page's word for itself** ("Home", "Welcome").
- **The first tab is the site's name, never a page's word for itself** ("Home", "Welcome").
+- **The Dock fills the phone, and its header is one row** (owner, 11-09: "go all the way to the top… wasted space", "I
+ don't really understand what the pin does", "clicked off… it didn't close the pop over"). Phones: the Dock is the
+ whole screen, over the bar, inside the notch area; tablets: a full-height panel on the right; desktops: a 420 × 640
+ card by default. The header is the four tabs, then More and Close; More holds Start a timer, "Keep the Dock's button
+ on every page" (a switch that says what off means) and Open in a window of its own. More and the timer close on a tap
+ anywhere else, in the Dock or on the page, and on Escape. The Chats tab is **Messages**.
+
## 4.158.0 — 2026-11-09
## 4.158.0 — 2026-11-09
- **The menu's Home.** A site whose links never name its home (Exchange: Models, Chat, Arena…) now gets Home first in
- **The menu's Home.** A site whose links never name its home (Exchange: Models, Chat, Arena…) now gets Home first in
@@ -2031,7 +2038,6 @@ What the front-page passes of 10-29 (Media, Quest, Actor, Rent, Services, Commun
@@ -2031,7 +2038,6 @@ What the front-page passes of 10-29 (Media, Quest, Actor, Rent, Services, Commun
sites had patched this in their own stylesheets on 2026-10-29; the kit now owns it for every site that
sites had patched this in their own stylesheets on 2026-10-29; the kit now owns it for every site that
includes `showcase.css`.
includes `showcase.css`.
-
## 4.34.0 — 2026-10-29
## 4.34.0 — 2026-10-29
- **Agent discovery:** the server-only `agent-layer` module serves `/.well-known/agent.json`
- **Agent discovery:** the server-only `agent-layer` module serves `/.well-known/agent.json`
@@ -29,7 +29,7 @@
@@ -29,7 +29,7 @@
'use strict';
'use strict';
if (typeof document === 'undefined' || root.OpenVibeDock) return;
if (typeof document === 'undefined' || root.OpenVibeDock) return;
- const W = [320, 720], H = [360, 900], DEF = { width: 400, height: 560 };
+ const W = [320, 720], H = [360, 900], DEF = { width: 420, height: 640 };
const CORNERS = ['bottom-right', 'bottom-left', 'top-right', 'top-left'];
const CORNERS = ['bottom-right', 'bottom-left', 'top-right', 'top-left'];
const svg = (p) => `<svg viewBox="0 0 24 24" aria-hidden="true">${p}</svg>`;
const svg = (p) => `<svg viewBox="0 0 24 24" aria-hidden="true">${p}</svg>`;
const G = {
const G = {
@@ -42,10 +42,11 @@
@@ -42,10 +42,11 @@
back: '<path d="M14.5 6.5L9 12l5.5 5.5"/>',
back: '<path d="M14.5 6.5L9 12l5.5 5.5"/>',
send: '<path d="M4.5 12h11M11 6.5l5.5 5.5-5.5 5.5"/>',
send: '<path d="M4.5 12h11M11 6.5l5.5 5.5-5.5 5.5"/>',
vibe: '<path d="M12 4.5a7.5 7.5 0 0 1 6.4 11.4L19.5 19.5l-3.6-1.1A7.5 7.5 0 1 1 12 4.5z"/><path d="M9 11.5h.01M12 11.5h.01M15 11.5h.01"/>',
vibe: '<path d="M12 4.5a7.5 7.5 0 0 1 6.4 11.4L19.5 19.5l-3.6-1.1A7.5 7.5 0 1 1 12 4.5z"/><path d="M9 11.5h.01M12 11.5h.01M15 11.5h.01"/>',
+ more: '<path d="M6 12h.01M12 12h.01M18 12h.01" stroke-width="3"/>',
clock: '<circle cx="12" cy="13" r="7.5"/><path d="M12 9.5V13l2.5 1.5M9.5 3h5"/>',
clock: '<circle cx="12" cy="13" r="7.5"/><path d="M12 9.5V13l2.5 1.5M9.5 3h5"/>',
};
};
// Vibe first (L48 V2): the person's AI, on every site.
// Vibe first (L48 V2): the person's AI, on every site.
- const TABS = [['vibe', 'Vibe', G.vibe], ['agent', 'Agents', G.spark], ['chats', 'Chats', G.chat], ['activity', 'Activity', G.bell]];
+ const TABS = [['vibe', 'Vibe', G.vibe], ['agent', 'Agents', G.spark], ['chats', 'Messages', G.chat], ['activity', 'Activity', G.bell]];
// The tabs the Network keeps in the person's Dock state (network.dock-state@1; vibe since contracts 0.270.0); a
// The tabs the Network keeps in the person's Dock state (network.dock-state@1; vibe since contracts 0.270.0); a
// newer one stays on this site's copy until the Network learns it.
// newer one stays on this site's copy until the Network learns it.
const SAVED_TABS = ['vibe', 'agent', 'chats', 'activity'];
const SAVED_TABS = ['vibe', 'agent', 'chats', 'activity'];
@@ -172,8 +173,20 @@
@@ -172,8 +173,20 @@
.ovdk-acts button { width: 40px; height: 40px; border: 0; border-radius: 12px; background: transparent; color: var(--text-muted, #8b8b96); cursor: pointer; display: grid; place-items: center; }
.ovdk-acts button { width: 40px; height: 40px; border: 0; border-radius: 12px; background: transparent; color: var(--text-muted, #8b8b96); cursor: pointer; display: grid; place-items: center; }
.ovdk-acts button:hover { background: color-mix(in srgb, var(--text-primary, #ececf1) 7%, transparent); color: var(--text-primary, #ececf1); }
.ovdk-acts button:hover { background: color-mix(in srgb, var(--text-primary, #ececf1) 7%, transparent); color: var(--text-primary, #ececf1); }
.ovdk-acts button svg { width: 19px; height: 19px; }
.ovdk-acts button svg { width: 19px; height: 19px; }
- .ovdk-acts [data-act="pin"][aria-pressed="true"] { color: var(--accent-light, var(--accent, #60a5fa)); }
- .ovdk-acts [data-act="pin"][aria-pressed="true"] svg { fill: color-mix(in srgb, currentColor 22%, transparent); }
+ .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; }
+ .ovdk-more button { display: flex; align-items: center; gap: 12px; min-height: 52px; padding: 8px 10px; border: 0; border-radius: 12px; background: transparent; color: var(--text-primary, #ececf1); font: inherit; text-align: left; cursor: pointer; }
+ .ovdk-more button[hidden] { display: none; }
+ .ovdk-more button:hover, .ovdk-more button:focus-visible { background: color-mix(in srgb, var(--text-primary, #ececf1) 7%, transparent); outline: none; }
+ .ovdk-more button > svg { flex: none; width: 20px; height: 20px; color: var(--text-muted, #8b8b96); }
+ .ovdk-more span { flex: 1; min-width: 0; display: grid; gap: 2px; }
+ .ovdk-more b { font-size: 14px; font-weight: 650; }
+ .ovdk-more small { font-size: 12px; color: var(--text-muted, #8b8b96); line-height: 1.35; }
+ .ovdk-sw { flex: none; position: relative; width: 38px; height: 22px; border-radius: 11px; background: var(--bg-tertiary, rgba(255,255,255,.14)); transition: background .15s; }
+ .ovdk-sw::after { content: ''; position: absolute; top: 3px; left: 3px; width: 16px; height: 16px; border-radius: 50%; background: #fff; transition: transform .15s; }
+ .ovdk-more [aria-checked="true"] .ovdk-sw { background: var(--accent-strong, var(--accent, #2563eb)); }
+ .ovdk-more [aria-checked="true"] .ovdk-sw::after { transform: translateX(16px); }
.ovdk-body { position: relative; flex: 1; min-height: 0; display: flex; flex-direction: column; border-top: 1px solid var(--border, rgba(255,255,255,.08)); }
.ovdk-body { position: relative; flex: 1; min-height: 0; display: flex; flex-direction: column; border-top: 1px solid var(--border, rgba(255,255,255,.08)); }
.ovdk-panel { flex: 1; min-height: 0; display: flex; flex-direction: column; }
.ovdk-panel { flex: 1; min-height: 0; display: flex; flex-direction: column; }
.ovdk-panel[hidden] { display: none; }
.ovdk-panel[hidden] { display: none; }
@@ -248,11 +261,10 @@
@@ -248,11 +261,10 @@
.ovdk-err[hidden] { display: none; }
.ovdk-err[hidden] { display: none; }
.ovdk-none b { display: block; margin-bottom: 4px; font-size: 15px; color: var(--text-primary, #ececf1); }
.ovdk-none b { display: block; margin-bottom: 4px; font-size: 15px; color: var(--text-primary, #ececf1); }
@media (max-width: 640px) {
@media (max-width: 640px) {
- .ovdk, .ovdk[data-corner] { left: 0; right: 0; top: auto; bottom: 0; width: auto !important; height: min(86vh, 720px) !important; height: min(86dvh, 720px) !important; max-width: none; max-height: none;
- border-radius: 24px 24px 0 0; border-bottom: 0; padding-bottom: env(safe-area-inset-bottom, 0px); box-shadow: 0 0 0 100vmax rgba(0,0,0,.5), 0 -12px 40px rgba(0,0,0,.35); transform-origin: 50% 100%; }
+ .ovdk, .ovdk[data-corner] { z-index: 10010; left: 0; right: 0; top: 0; bottom: 0; width: auto !important; height: auto !important; max-width: none; max-height: none;
+ border: 0; border-radius: 0; padding-top: env(safe-area-inset-top, 0px); padding-bottom: env(safe-area-inset-bottom, 0px); box-shadow: none; transform-origin: 50% 100%; }
.ovdk.is-in { animation-name: ovdk-rise; animation-duration: .3s; }
.ovdk.is-in { animation-name: ovdk-rise; animation-duration: .3s; }
- .ovdk-head { padding-top: 22px; cursor: default; }
- .ovdk-head::before { content: ''; position: absolute; top: 8px; left: 50%; width: 40px; height: 4px; margin-left: -20px; border-radius: 2px; background: var(--border-light, var(--border, rgba(255,255,255,.22))); }
+ .ovdk-head { cursor: default; }
.ovdk-size { display: none; }
.ovdk-size { display: none; }
.ovdk-tabs { flex: 1; } .ovdk-tabs button { flex: 1; justify-content: center; }
.ovdk-tabs { flex: 1; } .ovdk-tabs button { flex: 1; justify-content: center; }
/* A thumb, not a cursor: every control on the sheet is 44 px (design 2026-11-07 §1, Phone). */
/* A thumb, not a cursor: every control on the sheet is 44 px (design 2026-11-07 §1, Phone). */
@@ -260,17 +272,23 @@
@@ -260,17 +272,23 @@
.ovdk-acts button { width: 44px; height: 44px; }
.ovdk-acts button { width: 44px; height: 44px; }
/* Two rows (4.150.0, the owner: the strip was "cut off", "Activity" read "Ac"): the tools sit beside the grab
/* Two rows (4.150.0, the owner: the strip was "cut off", "Activity" read "Ac"): the tools sit beside the grab
handle; the four tabs share the full width below, each icon over its name, every one whole. */
handle; the four tabs share the full width below, each icon over its name, every one whole. */
- .ovdk-head { flex-wrap: wrap; row-gap: 6px; padding: 8px 8px 8px 10px; }
- .ovdk-head::before { top: 10px; }
- .ovdk-acts { order: -1; width: 100%; justify-content: flex-end; gap: 2px; min-height: 44px; }
+ .ovdk-head { flex-wrap: nowrap; gap: 4px; padding: 6px 6px 6px 8px; }
+ .ovdk-acts { flex: none; gap: 0; }
.ovdk-acts button[data-act="pop"] { display: none !important; }
.ovdk-acts button[data-act="pop"] { display: none !important; }
- .ovdk-tabs { order: 0; flex: 1 1 100%; padding: 4px; border-radius: 16px; }
+ .ovdk-tabs { order: 0; flex: 1 1 auto; min-width: 0; padding: 3px; border-radius: 16px; }
.ovdk-tabs button, .ovdk-tabs button:not([aria-selected="true"]) { flex: 1 1 0; min-width: 0; height: 54px; padding: 4px 2px; flex-direction: column; justify-content: center; gap: 3px; border-radius: 12px; font-size: 11.5px; font-weight: 650; }
.ovdk-tabs button, .ovdk-tabs button:not([aria-selected="true"]) { flex: 1 1 0; min-width: 0; height: 54px; padding: 4px 2px; flex-direction: column; justify-content: center; gap: 3px; border-radius: 12px; font-size: 11.5px; font-weight: 650; }
.ovdk-tabs button span, .ovdk-tabs button:not([aria-selected="true"]) span { position: static; width: auto; height: auto; overflow: hidden; clip: auto; max-width: 100%; text-overflow: ellipsis; white-space: nowrap; }
.ovdk-tabs button span, .ovdk-tabs button:not([aria-selected="true"]) span { position: static; width: auto; height: auto; overflow: hidden; clip: auto; max-width: 100%; text-overflow: ellipsis; white-space: nowrap; }
.ovdk-tabs button svg { width: 19px; height: 19px; }
.ovdk-tabs button svg { width: 19px; height: 19px; }
.ovdk-tabs .n { position: absolute; top: 4px; left: calc(50% + 6px); min-width: 17px; padding: 1px 5px; font-size: 10.5px; }
.ovdk-tabs .n { position: absolute; top: 4px; left: calc(50% + 6px); min-width: 17px; padding: 1px 5px; font-size: 10.5px; }
.ovdk-foot a, .ovdk-foot button { min-height: 44px; }
.ovdk-foot a, .ovdk-foot button { min-height: 44px; }
}
}
+ @media (min-width: 641px) and (max-width: 1023px) {
+ .ovdk, .ovdk[data-corner] { z-index: 10010; top: 0; bottom: 0; right: 0; left: auto; width: min(460px, calc(100vw - 48px)) !important; height: auto !important;
+ max-width: none; max-height: none; border-radius: 22px 0 0 22px; border-right: 0; padding-top: env(safe-area-inset-top, 0px); padding-bottom: env(safe-area-inset-bottom, 0px);
+ box-shadow: 0 0 0 100vmax rgba(0,0,0,.4), -16px 0 40px rgba(0,0,0,.3); transform-origin: 100% 50%; }
+ .ovdk-size { display: none; }
+ .ovdk-head { cursor: default; }
+ }
html.ovdk-sheet-open, html.ovdk-sheet-open body { overflow: hidden; }
html.ovdk-sheet-open, html.ovdk-sheet-open body { overflow: hidden; }
@media (prefers-reduced-motion: reduce) { .ovdk.is-in, .ovdk-wait span { animation: none; } }
@media (prefers-reduced-motion: reduce) { .ovdk.is-in, .ovdk-wait span { animation: none; } }
html[data-ov-motion="reduced"] .ovdk.is-in { animation: none; }
html[data-ov-motion="reduced"] .ovdk.is-in { animation: none; }
@@ -296,11 +314,14 @@
@@ -296,11 +314,14 @@
<header class="ovdk-head">
<header class="ovdk-head">
<div class="ovdk-tabs" role="tablist" aria-label="Dock">${TABS.map(([id, label, g]) => `<button type="button" role="tab" id="ovdk-tab-${id}" aria-controls="ovdk-panel-${id}" data-tab="${id}" aria-selected="false" tabindex="-1">${svg(g)}<span>${label}</span><b class="n" data-n="${id}"></b></button>`).join('')}</div>
<div class="ovdk-tabs" role="tablist" aria-label="Dock">${TABS.map(([id, label, g]) => `<button type="button" role="tab" id="ovdk-tab-${id}" aria-controls="ovdk-panel-${id}" data-tab="${id}" aria-selected="false" tabindex="-1">${svg(g)}<span>${label}</span><b class="n" data-n="${id}"></b></button>`).join('')}</div>
<div class="ovdk-acts">
<div class="ovdk-acts">
- <button type="button" data-act="timer" title="A timer" aria-label="Start a timer">${svg(G.clock)}</button>
- <button type="button" data-act="pop" hidden title="Open the Dock in a window of its own" aria-label="Open the Dock in a window of its own">${svg(G.open)}</button>
- <button type="button" data-act="pin" aria-pressed="false" title="Keep the Dock's button on every page" aria-label="Keep the Dock's button on every page">${svg(G.pin)}</button>
+ <button type="button" data-act="more" aria-haspopup="true" aria-expanded="false" title="More" aria-label="More: a timer, the Dock's button">${svg(G.more)}</button>
<button type="button" data-act="close" title="Close" aria-label="Close the Dock">${svg(G.close)}</button>
<button type="button" data-act="close" title="Close" aria-label="Close the Dock">${svg(G.close)}</button>
</div>
</div>
+ <div class="ovdk-more" role="menu" aria-label="More" hidden>
+ <button type="button" role="menuitem" data-act="timer">${svg(G.clock)}<span><b>Start a timer</b><small>A countdown or a focus round, on the Dock's button</small></span></button>
+ <button type="button" role="menuitemcheckbox" aria-checked="false" data-act="pin">${svg(G.pin)}<span><b>Keep the Dock's button on every page</b><small>When off, it shows only while something works or needs you</small></span><i class="ovdk-sw" aria-hidden="true"></i></button>
+ <button type="button" role="menuitem" data-act="pop" hidden>${svg(G.open)}<span><b>Open the Dock in a window of its own</b><small>Keep it beside whatever you are doing</small></span></button>
+ </div>
</header>
</header>
<div class="ovdk-body">${TABS.map(([id]) => `<div class="ovdk-panel" role="tabpanel" id="ovdk-panel-${id}" aria-labelledby="ovdk-tab-${id}" data-panel="${id}" hidden></div>`).join('')}</div>
<div class="ovdk-body">${TABS.map(([id]) => `<div class="ovdk-panel" role="tabpanel" id="ovdk-panel-${id}" aria-labelledby="ovdk-tab-${id}" data-panel="${id}" hidden></div>`).join('')}</div>
<span class="ovdk-size" aria-hidden="true"></span>`;
<span class="ovdk-size" aria-hidden="true"></span>`;
@@ -316,7 +337,27 @@
@@ -316,7 +337,27 @@
el.querySelector('[data-act="close"]').addEventListener('click', () => close());
el.querySelector('[data-act="close"]').addEventListener('click', () => close());
el.querySelector('[data-act="pop"]').addEventListener('click', () => { if (popped) backToPage(); else popOut(); });
el.querySelector('[data-act="pop"]').addEventListener('click', () => { if (popped) backToPage(); else popOut(); });
el.querySelector('[data-act="pin"]').addEventListener('click', () => { save({ pinned: !state().pinned }); paintPin(); });
el.querySelector('[data-act="pin"]').addEventListener('click', () => { save({ pinned: !state().pinned }); paintPin(); });
- el.querySelector('[data-act="timer"]').addEventListener('click', (e) => { const a = e.currentTarget; withTimer((t) => t.menu(a)); });
+ el.querySelector('[data-act="timer"]').addEventListener('click', () => { const a = el.querySelector('[data-act="more"]'); closeMore(); withTimer((t) => t.menu(a)); });
+ const moreBtn = el.querySelector('[data-act="more"]'), moreMenu = el.querySelector('.ovdk-more');
+ function closeMore() { moreMenu.hidden = true; moreBtn.setAttribute('aria-expanded', 'false'); }
+ moreBtn.addEventListener('click', () => {
+ const open = moreMenu.hidden;
+ closeMore();
+ const tm = el.querySelector('.ovdk-tmenu'); if (tm) tm.remove();
+ if (open) { moreMenu.hidden = false; moreBtn.setAttribute('aria-expanded', 'true'); const first = moreMenu.querySelector('button:not([hidden])'); if (first) first.focus(); }
+ });
+ moreMenu.addEventListener('click', (e) => { const b = e.target.closest('button'); if (b && b.dataset.act !== 'pin') closeMore(); });
+ moreMenu.addEventListener('keydown', (e) => { if (e.key === 'Escape') { e.stopPropagation(); closeMore(); moreBtn.focus(); } });
+ // A pop-over (More, the timer) closes on a tap anywhere else, in the Dock or on the page (owner, 11-09: "clicked off
+ // something else, and it didn't actually close").
+ const awayFrom = (e) => {
+ const path = typeof e.composedPath === 'function' ? e.composedPath() : [e.target];
+ if (!moreMenu.hidden && !path.includes(moreMenu) && !path.includes(moreBtn)) closeMore();
+ const tm = el.querySelector('.ovdk-tmenu');
+ if (tm && !path.includes(tm) && !path.includes(moreMenu) && !path.includes(moreBtn)) tm.remove();
+ };
+ el.addEventListener('pointerdown', awayFrom, true);
+ document.addEventListener('pointerdown', (e) => { if (!el.contains(e.target)) awayFrom(e); }, true);
el.addEventListener('keydown', (e) => { if (e.key === 'Escape') { e.stopPropagation(); close(); } });
el.addEventListener('keydown', (e) => { if (e.key === 'Escape') { e.stopPropagation(); close(); } });
// A tap outside closes the phone sheet. The path is the one at dispatch: a tap that redraws its own panel (opening a
// A tap outside closes the phone sheet. The path is the one at dispatch: a tap that redraws its own panel (opening a
// chat) leaves its target detached, and must not read as outside.
// chat) leaves its target detached, and must not read as outside.
@@ -395,7 +436,7 @@
@@ -395,7 +436,7 @@
setTimeout(() => { el.style.transition = ''; }, 300);
setTimeout(() => { el.style.transition = ''; }, 300);
};
};
head.addEventListener('pointerdown', (e) => {
head.addEventListener('pointerdown', (e) => {
- if (e.button || e.target.closest('.ovdk-acts')) return;
+ if (e.button || e.target.closest('.ovdk-acts, .ovdk-more')) return;
s = { x: e.clientX, y: e.clientY, r: el.getBoundingClientRect(), dx: 0, dy: 0, id: e.pointerId, live: false };
s = { x: e.clientX, y: e.clientY, r: el.getBoundingClientRect(), dx: 0, dy: 0, id: e.pointerId, live: false };
root.addEventListener('pointermove', move); root.addEventListener('pointerup', end); root.addEventListener('pointercancel', end);
root.addEventListener('pointermove', move); root.addEventListener('pointerup', end); root.addEventListener('pointercancel', end);
});
});
@@ -439,15 +480,14 @@
@@ -439,15 +480,14 @@
n('agent', agent, c.waiting ? 'waiting' : '');
n('agent', agent, c.waiting ? 'waiting' : '');
n('activity', c.unread || 0);
n('activity', c.unread || 0);
n('chats', chats.unread || 0);
n('chats', chats.unread || 0);
- el.querySelector('#ovdk-tab-chats').setAttribute('aria-label', chats.unread ? `Chats, ${chats.unread} unread` : 'Chats');
+ el.querySelector('#ovdk-tab-chats').setAttribute('aria-label', chats.unread ? `Messages, ${chats.unread} unread` : 'Messages');
el.querySelector('#ovdk-tab-agent').setAttribute('aria-label', [`Agents`, c.waiting ? `${c.waiting} need${c.waiting === 1 ? 's' : ''} you` : '', c.running ? `${c.running} working` : ''].filter(Boolean).join(', '));
el.querySelector('#ovdk-tab-agent').setAttribute('aria-label', [`Agents`, c.waiting ? `${c.waiting} need${c.waiting === 1 ? 's' : ''} you` : '', c.running ? `${c.running} working` : ''].filter(Boolean).join(', '));
el.querySelector('#ovdk-tab-activity').setAttribute('aria-label', c.unread ? `Activity, ${c.unread} unread` : 'Activity');
el.querySelector('#ovdk-tab-activity').setAttribute('aria-label', c.unread ? `Activity, ${c.unread} unread` : 'Activity');
}
}
function paintPin() {
function paintPin() {
const b = el.querySelector('[data-act="pin"]'), on = !!state().pinned;
const b = el.querySelector('[data-act="pin"]'), on = !!state().pinned;
- b.setAttribute('aria-pressed', String(on));
- const t = on ? "The Dock's button stays on every page" : "Keep the Dock's button on every page";
- b.title = t; b.setAttribute('aria-label', t);
+ b.setAttribute('aria-checked', String(on));
+ b.title = on ? "On: the Dock's button stays on every page" : "Off: the Dock's button shows only while something works or needs you";
}
}
function setTab(id, remember) {
function setTab(id, remember) {
tab = TABS.some(([t]) => t === id) ? id : 'activity';
tab = TABS.some(([t]) => t === id) ? id : 'activity';
@@ -790,7 +830,7 @@
@@ -790,7 +830,7 @@
if (!b) return;
if (!b) return;
b.hidden = !pip();
b.hidden = !pip();
const t = popped ? 'Bring the Dock back to the page' : 'Open the Dock in a window of its own';
const t = popped ? 'Bring the Dock back to the page' : 'Open the Dock in a window of its own';
- b.title = t; b.setAttribute('aria-label', t);
+ b.title = t; const name = b.querySelector('b'); if (name) name.textContent = t;
}
}
/** The window gets the Dock's styles, the theme's colours and the page's own type: the Dock looks the same there. */
/** The window gets the Dock's styles, the theme's colours and the page's own type: the Dock looks the same there. */
@@ -539,7 +539,7 @@
@@ -539,7 +539,7 @@
const NAV_ICONS_REV = 'd9a4febbbad7'; // sha256(nav-icons.js), first 12 hex: its ?v= (cacheable forever)
const NAV_ICONS_REV = 'd9a4febbbad7'; // 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': '1e91c9c0f347', 'ov-glyphs.js': '1a0d8ce6909f', 'ov-brand.woff2': '3b4e6a8a5878', 'ov-look.js': '2a6c53a7b176', 'ov-fx.js': '46fce1e2b8db', 'ov-labels.js': 'a30b178391f5', 'ov-forms.js': '068ce1b1c4d5', 'ov-forms.css': '98cf0f28bb68', 'ov-share.js': '8e55bfe93238', 'ov-share.css': 'cbd18e6ce991', 'search.js': 'b113a2fd8e93', 'dock.js': '4dacf614797b', 'presence.js': '1b675c37d888', 'panels.js': '7d8267ca8035', 'notification-ui.js': 'a2d37237ba0b', 'history.js': '1e6598f78ba4', 'site-switcher.js': '292bea111aa8', 'appbar.js': '1edf7532f228', 'release-watch.js': '25f02a3f0684', 'ov-icons.js': 'ba3981246bc0', 'sso-client.js': '52439265b980', 'overlay.js': '25960cf0c075', 'ov-home.js': '267bf753169d' };
+ const SIBLING_REV = { 'ov-mark.js': '1e91c9c0f347', 'ov-glyphs.js': '1a0d8ce6909f', 'ov-brand.woff2': '3b4e6a8a5878', 'ov-look.js': '2a6c53a7b176', 'ov-fx.js': '46fce1e2b8db', 'ov-labels.js': 'a30b178391f5', 'ov-forms.js': '068ce1b1c4d5', 'ov-forms.css': '98cf0f28bb68', 'ov-share.js': '8e55bfe93238', 'ov-share.css': 'cbd18e6ce991', 'search.js': 'b113a2fd8e93', 'dock.js': 'd1354be66c41', 'presence.js': '1b675c37d888', 'panels.js': '7d8267ca8035', 'notification-ui.js': 'a2d37237ba0b', 'history.js': '1e6598f78ba4', 'site-switcher.js': '292bea111aa8', 'appbar.js': '1edf7532f228', 'release-watch.js': '25f02a3f0684', 'ov-icons.js': 'ba3981246bc0', 'sso-client.js': '52439265b980', 'overlay.js': '25960cf0c075', 'ov-home.js': '267bf753169d' };
// 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">.
@@ -243,7 +243,9 @@ async function drag(p, from, to) {
@@ -243,7 +243,9 @@ async function drag(p, from, to) {
await p.evaluate("document.getElementById('ovdk-pill').click()");
await p.evaluate("document.getElementById('ovdk-pill').click()");
const card = await until(p, `(() => { const c = ${CARD}; return c && c.items ? c : null; })()`, 'the Agents tab');
const card = await until(p, `(() => { const c = ${CARD}; return c && c.items ? c : null; })()`, 'the Agents tab');
assert.deepStrictEqual([card.tab, card.groups, card.items], ['agent', ['waiting', 'running'], 3], 'what needs you first, then what runs');
assert.deepStrictEqual([card.tab, card.groups, card.items], ['agent', ['waiting', 'running'], 3], 'what needs you first, then what runs');
- assert.ok(card.w === 390 && card.t > 60, 'a sheet across the phone, under the bar');
+ await new Promise((ok) => setTimeout(ok, 450)); // the sheet has finished rising
+ const placed = await p.evaluate(CARD);
+ assert.ok(placed.w === 390 && placed.t === 0 && placed.h === 780, `the whole phone, over the bar (owner, 11-09: "all the way to the top"; ${placed.t}, ${placed.h})`);
assert.strictEqual(await p.evaluate('document.getElementById("ovdk-pill").hidden'), true, 'the pill steps aside while the Dock is open');
assert.strictEqual(await p.evaluate('document.getElementById("ovdk-pill").hidden'), true, 'the pill steps aside while the Dock is open');
await p.evaluate("document.querySelector('.ovdk .ovac-item[data-bucket=waiting] button[data-do=approve]').click()");
await p.evaluate("document.querySelector('.ovdk .ovac-item[data-bucket=waiting] button[data-do=approve]').click()");
await until(p, "document.querySelector('.ovdk .ovac-item[data-bucket=waiting] .primary').textContent === 'Approved'", 'approved in place');
await until(p, "document.querySelector('.ovdk .ovac-item[data-bucket=waiting] .primary').textContent === 'Approved'", 'approved in place');
@@ -428,7 +430,7 @@ async function drag(p, from, to) {
@@ -428,7 +430,7 @@ async function drag(p, from, to) {
await until(p, `(() => { const c = ${CARD}; return c && c.items ? c : null; })()`, 'the desktop card');
await until(p, `(() => { const c = ${CARD}; return c && c.items ? c : null; })()`, 'the desktop card');
await wait(400); // past the grow-in (a transform), so the box is the card's own
await wait(400); // past the grow-in (a transform), so the box is the card's own
let card = await p.evaluate(CARD);
let card = await p.evaluate(CARD);
- assert.deepStrictEqual([card.corner, card.w, card.h], ['bottom-right', 400, 560], 'a 400 × 560 card in the pill\'s corner');
+ assert.deepStrictEqual([card.corner, card.w, card.h], ['bottom-right', 420, 640], 'a 420 × 640 card in the pill\'s corner');
// Drag the head from its middle to near the top left: it snaps to that corner.
// Drag the head from its middle to near the top left: it snaps to that corner.
await drag(p, [card.l + 120, card.t + 26], [180, 90]); // from the Activity tab: a drag, not a tap
await drag(p, [card.l + 120, card.t + 26], [180, 90]); // from the Activity tab: a drag, not a tap
card = await until(p, `(() => { const c = ${CARD}; return c && c.corner === 'top-left' ? c : null; })()`, 'the card snaps to the top left');
card = await until(p, `(() => { const c = ${CARD}; return c && c.corner === 'top-left' ? c : null; })()`, 'the card snaps to the top left');
@@ -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 = 17203; // 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 = 17920; // 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-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-timer.js', 'dock-agent.js', 'dock-share.js', 'dock-card.js'];
// ── the budget ──────────────────────────────────────────────
// ── the budget ──────────────────────────────────────────────