openvibers/openvibe.shared Commits

4.234.0: the bottom bar hides all the way; the composer stops repeating itself; the GitHub picker fixed (#517)

OP OpenVibers committed 3 h ago · 998c8263d3 · 13 files, +85 −27

* 4.234.0: the bottom bar hides all the way; the + sheet repeats nothing; the GitHub picker draws initials, caches and cancels

* Sibling revisions for the 4.234.0 changes

* Sibling revisions settle (dock, then the navbar)

Changes

ddiff --git a/CHANGELOG.md b/CHANGELOG.md
iindex af642ee..9d0442e 100644
---- a/CHANGELOG.md
++++ b/CHANGELOG.md
@@@ -1,5 +1,24 @@
# Changelog
++## 4.234.0 — 2026-11-13
++
++**The bottom bar hides all the way, and the composer stops repeating itself** (the owner's phone screenshots, 11-13).
++
++- **The bottom bar (`appbar.js`).** It stepped aside for a keyboard or an open panel with `translateY(110%)` while its
++ lift above the visible viewport still applied, so above a keyboard, or with Android's toolbar gap, it stopped half on
++ the screen. A hidden bar now drops the lift, sits fully below the screen, and is invisible and not clickable until it
++ comes back.
++- **The + sheet.** It no longer repeats a tool the row already shows; the emoji button and an "Emoji" tile side by
++ side were the same thing twice. The sheet holds what a narrow row hides, and the extras (Timer, GitHub).
++- **The GitHub picker:**
++ - each row's mark is its owner's initial on a colour of the name; GitHub's own pictures were blocked by most sites'
++ image policy, so every row showed a broken picture, and each one asked GitHub;
++ - the same words are answered from the page's cache for ten minutes;
++ - a new search cancels the one still running;
++ - it waits a third of a second after the last key;
++ - the list scrolls inside its panel and no longer runs over the box.
++- Tests `composer-v2-chrome` (nothing repeated, the initial, the cache) and `navbar-appbar` (hidden is all the way).
++
## 4.233.0 — 2026-11-13
**`ov-metrics.js` on a phone, and prices under a dollar.**
ddiff --git a/README.md b/README.md
iindex f5dd471..51a4265 100644
---- a/README.md
++++ b/README.md
@@@ -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.233.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed
++**Status:** alpha, v4.234.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed
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
the tag it pins.
ddiff --git a/STATUS.json b/STATUS.json
iindex ba35783..a231e76 100644
---- a/STATUS.json
++++ b/STATUS.json
@@@ -4,7 +4,7 @@
"code": true,
"wave": "W2",
"package": "openvibe-shared",
-- "version": "4.233.0",
++ "version": "4.234.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)",
ddiff --git a/appbar.js b/appbar.js
iindex 1781d16..1cf4b2f 100644
---- a/appbar.js
++++ b/appbar.js
@@@ -10,7 +10,8 @@
* OpenVibeNavbar.setAppBar(spec | false) change it later (a single-page app's sections)
*
* It shows under 760 px only. html.ov-has-appbar and --ovnav-appbar-h keep the page's last lines above it (kit.css's
-- * dock and save bar sit on it), and it steps aside while the keyboard is up or a panel is open. A link's icon is a
++ * dock and save bar sit on it), and it steps aside while the keyboard is up or a panel is open: all the way, without the
++ * lift (4.234.0; with the lift still applied it stopped half on the screen above a keyboard or Android's toolbar gap). A link's icon is a
* navbar icon name (fa-…), its dot a small red mark, the open page (its path, a path under it, or setActive's page)
* a filled pill. Clicks go through the link's onClick and the site's onNavigate, as the navbar's own links do.
*
@@@ -49,7 +50,7 @@
html.ov-has-appbar body { padding-bottom: var(--ovnav-appbar-h); }
.ovnav-appbar { position: fixed; left: 0; right: 0; bottom: 0; z-index: var(--ovb-z-bar, 9990); translate: 0 calc(-1 * var(--ovb-lift, 0px)); display: flex; align-items: stretch; height: var(--ovnav-appbar-h); box-sizing: border-box; padding: 0 4px env(safe-area-inset-bottom, 0px);
background: color-mix(in srgb, var(--bg-secondary, #121826) 90%, transparent); -webkit-backdrop-filter: saturate(1.5) blur(16px); backdrop-filter: saturate(1.5) blur(16px);
-- border-top: 1px solid var(--border, rgba(255,255,255,.1)); font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; transition: transform .2s ease; }
++ border-top: 1px solid var(--border, rgba(255,255,255,.1)); font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; transition: transform .2s ease, visibility 0s; }
.ovnav-appbar .ovnav-ab { all: unset; box-sizing: border-box; flex: 1 1 0; min-width: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 3px; padding: 6px 2px; cursor: pointer;
color: var(--text-muted, #93a0b4); font-size: 12px; font-weight: 650; line-height: 1.1; text-align: center; -webkit-tap-highlight-color: transparent; }
.ovnav-appbar .ovnav-ab:focus-visible { outline: 2px solid var(--accent, #3b82f6); outline-offset: -4px; border-radius: 12px; }
@@@ -73,7 +74,7 @@
.ovnav-appbar .ovnav-ab-dot { position: absolute; top: 2px; right: 12px; width: 8px; height: 8px; border-radius: 50%; background: var(--danger, #ef4444); box-shadow: 0 0 0 2px var(--bg-secondary, #121826); }
html.ov-has-appbar:has(:is(input:not([type=checkbox]):not([type=radio]):not([type=range]), textarea, select, [contenteditable="true"]):focus) { --ovnav-appbar-h: 0px; }
html.ov-has-appbar:has(:is(input:not([type=checkbox]):not([type=radio]):not([type=range]), textarea, select, [contenteditable="true"]):focus) .ovnav-appbar,
-- body.ov-panel-open .ovnav-appbar { transform: translateY(110%); }
++ body.ov-panel-open .ovnav-appbar { translate: none; transform: translateY(calc(100% + 24px)); visibility: hidden; pointer-events: none; transition: transform .2s ease, visibility 0s linear .2s; }
}
@media print { .ovnav-appbar { display: none !important; } }`;
document.head.appendChild(st);
ddiff --git a/dock-vibe.js b/dock-vibe.js
iindex 820792f..cbcf93e 100644
---- a/dock-vibe.js
++++ b/dock-vibe.js
@@@ -114,7 +114,7 @@
// 4.203.0 (ask 349, "a better composer system unified across all sites"): Vibe's box is the one composer too, for
// drafts, "/" commands and your own, the marks, what sends and your settings; its picture keeps its own upload.
const selfSrc = (typeof document !== 'undefined' && document.currentScript && document.currentScript.src) || '';
-- const VIBE_REV = { 'ov-composer.js': 'ac7107f5d733' };
++ const VIBE_REV = { 'ov-composer.js': '1b6c159fef2e' };
function joinComposer(form) {
if (!form || form.ownerDocument !== document) return; // a popped-out Dock keeps its plain box
form.setAttribute('data-ovc-no-files', '');
ddiff --git a/dock.js b/dock.js
iindex cb6c240..4d3c961 100644
---- a/dock.js
++++ b/dock.js
@@@ -84,7 +84,7 @@
}
// Each sibling's content hash (scripts/sibling-revs.js writes it), so a release gives each a new URL.
-- const DOCK_REV = { 'dock-vibe.js': 'c5fbc40a2cce', 'dock-code.js': '7a768982eff3', 'dock-mount.js': 'fb07dccadb8c', 'dock-runs.js': '4368f039df7c', 'ai-marks-client.js': 'f4a880a9f206', 'action-center.js': 'e40346ec1f16', 'dock-timeline.js': 'bffaed38fccf', 'dock-actor.js': 'f9aea671c7ad', 'dock-agent.js': '8787080a04d3', 'dock-timer.js': '460d68aa8321', 'dock-share.js': 'e83b2e27b3ef', 'dock-card.js': '2643c20b0016', 'dock-chat-edits.js': '6cf8c6b6daa5' };
++ const DOCK_REV = { 'dock-vibe.js': 'ffa4189c143c', 'dock-code.js': '7a768982eff3', 'dock-mount.js': 'fb07dccadb8c', 'dock-runs.js': '4368f039df7c', 'ai-marks-client.js': 'f4a880a9f206', 'action-center.js': 'e40346ec1f16', 'dock-timeline.js': 'bffaed38fccf', 'dock-actor.js': 'f9aea671c7ad', 'dock-agent.js': '8787080a04d3', 'dock-timer.js': '460d68aa8321', 'dock-share.js': 'e83b2e27b3ef', 'dock-card.js': '2643c20b0016', 'dock-chat-edits.js': '6cf8c6b6daa5' };
function load(src, test) {
return new Promise((ok) => {
if (test()) return ok(true);
ddiff --git a/navbar.js b/navbar.js
iindex 3549e6f..18bc792 100644
---- a/navbar.js
++++ b/navbar.js
@@@ -609,7 +609,7 @@
* serves its own pinned copy at /shared/ gets its own), else the network's copy.
*/
// The files loaded from beside this one, by content (scripts/sibling-revs.js): a release changes their URLs.
-- const SIBLING_REV = { 'ov-mark.js': 'f34f731c3a94', 'ov-glyphs.js': '2ee871bd8ffa', 'ov-brand.woff2': '3b4e6a8a5878', 'ov-look.js': 'dd84a30bb646', 'ov-fx.js': '947820d69097', 'ov-labels.js': 'd40f7ca14bc4', 'ov-forms.js': '2eb9e2604c3e', 'ov-forms.css': '07e8f59d9e6d', 'ov-share.js': 'd419508a797e', 'ov-share.css': 'd609a71f611a', 'search.js': 'ef289c4550b1', 'dock.js': '9f15df28050c', 'presence.js': '5f195d41f8c5', 'panels.js': 'd90bfe987d81', 'surfaces.js': 'a94e76bd1200', 'notification-ui.js': '0cef1f895965', 'history.js': 'f9fb9e096c65', 'bottom.js': '1671b6949137', 'appbar.js': 'c21485176ae9', 'release-watch.js': '25f02a3f0684', 'ov-icons.js': '8701ebc6f8a3', 'sso-client.js': 'f574ac3331a5', 'overlay.js': '2bc56b3cf726', 'ov-home.js': 'b11405559831', 'nav-slots.js': '7b11e6916732', 'ov-wallet.js': '3fa6bb23b429', 'ov-wallet.css': '4923c808c9ad', 'purse.js': '9d3fea0dae02', 'ov-drop.js': '445689a5eb80', 'ov-tip.js': '1d918e7de69d', 'ov-arrange.js': 'ae24086bd2be', 'soft.js': '98ab9b8080b1', 'ov-run.js': '91be6059adcf', 'ov-composer.js': 'ac7107f5d733' };
++ const SIBLING_REV = { 'ov-mark.js': 'f34f731c3a94', 'ov-glyphs.js': '2ee871bd8ffa', 'ov-brand.woff2': '3b4e6a8a5878', 'ov-look.js': 'dd84a30bb646', 'ov-fx.js': '947820d69097', 'ov-labels.js': 'd40f7ca14bc4', 'ov-forms.js': '2eb9e2604c3e', 'ov-forms.css': '07e8f59d9e6d', 'ov-share.js': 'd419508a797e', 'ov-share.css': 'd609a71f611a', 'search.js': 'ef289c4550b1', 'dock.js': 'b4e65d63a7f6', 'presence.js': '5f195d41f8c5', 'panels.js': 'd90bfe987d81', 'surfaces.js': 'a94e76bd1200', 'notification-ui.js': '0cef1f895965', 'history.js': 'f9fb9e096c65', 'bottom.js': '1671b6949137', 'appbar.js': 'b2a16f53eae0', 'release-watch.js': '25f02a3f0684', 'ov-icons.js': '8701ebc6f8a3', 'sso-client.js': 'f574ac3331a5', 'overlay.js': '2bc56b3cf726', 'ov-home.js': 'b11405559831', 'nav-slots.js': '7b11e6916732', 'ov-wallet.js': '3fa6bb23b429', 'ov-wallet.css': '4923c808c9ad', 'purse.js': '9d3fea0dae02', 'ov-drop.js': '445689a5eb80', 'ov-tip.js': '1d918e7de69d', 'ov-arrange.js': 'ae24086bd2be', 'soft.js': '98ab9b8080b1', 'ov-run.js': '91be6059adcf', 'ov-composer.js': '1b6c159fef2e' };
const siblingV = (name) => `${sibling(name)}${SIBLING_REV[name] ? `?v=${SIBLING_REV[name]}` : ''}`;
// ov-tip.js loads at the first hover over a tooltip's owner.
ddiff --git a/ov-composer.js b/ov-composer.js
iindex 9d2fa5e..4769368 100644
---- a/ov-composer.js
++++ b/ov-composer.js
@@@ -294,6 +294,8 @@ html[data-ov-motion=off] .ovc-cam-shutter { transition: none; }
.ovc-repo-q { width: 100%; box-sizing: border-box; min-height: 42px; padding: 0 12px; border-radius: 12px; border: 1px solid var(--ovt-line, rgba(148, 163, 184, .22)); background: var(--bg-primary, rgba(0, 0, 0, .2)); color: inherit; font: inherit; font-size: 15px; outline: 0; }
.ovc-repo-q:focus-visible { border-color: var(--ovt-accent, var(--accent, #3b82f6)); }
.ovc-repo-list { display: grid; gap: 2px; overflow-y: auto; overscroll-behavior: contain; min-height: 0; }
++.ovc-repo:not([hidden]) { grid-template-rows: auto minmax(0, 1fr) auto; overflow: hidden; }
++.ovc-repo-mono { font-weight: 700; font-size: 15px; color: hsl(var(--ovc-mono-h, 210) 70% 72%); background: hsl(var(--ovc-mono-h, 210) 45% 22%); }
.ovc-repo-list:empty { display: none; }
.ovc-repo-row { display: flex; align-items: flex-start; gap: 10px; width: 100%; min-height: 52px; padding: 8px; border: 0; border-radius: 12px; background: none; color: inherit; font: inherit; text-align: left; cursor: pointer; }
.ovc-repo-row:hover, .ovc-repo-row:focus-visible { background: color-mix(in srgb, var(--ovt-accent, var(--accent, #3b82f6)) 12%, transparent); outline: 0; }
@@@ -847,6 +849,9 @@ html[data-ov-motion=off] .ovc-tray, html[data-ov-motion=off] .ovc-more svg { ani
[...tools.children].forEach((t) => {
if (t === mic || t === more || !t.matches('.ovt-btn, button, label') || t.matches('[type="submit"], [data-ovt-send], .ovc-mic, .ovdk-mic, .ovdk-go, [hidden]')) return;
const label = t.getAttribute('aria-label') || t.title || ''; if (!label || seen.has(label)) return; seen.add(label);
++ // 4.234.0: a tool the row shows is not repeated in the sheet (the owner, 11-13: "why twice"); the sheet has
++ // what a narrow row hides, and the extras.
++ if (getComputedStyle(t).display !== 'none' && t.getClientRects().length) return;
const input = t.querySelector('input[type=file]');
add(label, '', () => { if (input) input.click(); else t.click(); }, t.querySelector('svg'));
});
@@@ -1080,8 +1085,12 @@ html[data-ov-motion=off] .ovc-tray, html[data-ov-motion=off] .ovc-more svg { ani
if (typeof dlg.showModal === 'function') dlg.showModal(); else dlg.setAttribute('open', '');
start();
}
-- /** The GitHub tool's panel: a field, the repositories as you type (a quarter second after the last key), one press
-- * puts the address in the box. Everything is text; only https pictures from GitHub's avatar host are drawn. */
++ /** Repository searches already answered on this page, by their words, for ten minutes (60 at most). */
++ const REPO_CACHE = new Map();
++ const REPO_CACHE_MS = 10 * 60 * 1000;
++
++ /** The GitHub tool's panel: a field, the repositories as you type (a third of a second after the last key), one press
++ * puts the address in the box. Everything is text, and each row's mark is its owner's initial (no picture is fetched). */
function openRepoSearch(ctx, button, form) {
let panel = form.querySelector('.ovc-repo');
if (panel) { panel.hidden = !panel.hidden; if (!panel.hidden) panel.querySelector('input').focus(); return; }
@@@ -1092,16 +1101,20 @@ html[data-ov-motion=off] .ovc-tray, html[data-ov-motion=off] .ovc-more svg { ani
const note = el('p', 'ovc-repo-note'); note.textContent = 'Type what the project does: “goal bar overlay”, “markdown editor”.';
panel.append(field, list, note);
const frame = String(form.getAttribute('data-ovc-frame') || 'https://openvibe.network').replace(/\/+$/, '');
-- let t = 0, gen = 0;
++ let t = 0, gen = 0, ctl = null;
const close = () => { panel.hidden = true; button.focus(); };
const pick = (r) => { ctx.insert(`${r.url} `); panel.hidden = true; };
const draw = (items) => {
list.textContent = '';
items.forEach((r) => {
const b = el('button', 'ovc-repo-row'); b.type = 'button'; b.setAttribute('role', 'option');
-- let av = null;
-- try { const u = new URL(String(r.owner_avatar_url || '')); if (u.protocol === 'https:' && u.hostname === 'avatars.githubusercontent.com') { av = el('img', 'ovc-repo-av'); av.src = u.href; av.alt = ''; av.loading = 'lazy'; } } catch { /* no picture */ }
-- if (!av) { av = el('span', 'ovc-repo-av'); av.innerHTML = svg(IC.repo); }
++ // 4.234.0: the owner's initial on a colour of its name, drawn here. GitHub's pictures were blocked by most
++ // sites' image policy (a broken picture on every row) and asked GitHub once per row.
++ const owner = String(r.full_name || '').split('/')[0] || '?';
++ const av = el('span', 'ovc-repo-av ovc-repo-mono'); av.setAttribute('aria-hidden', 'true');
++ let h = 0; for (const ch of owner) h = (h * 31 + ch.charCodeAt(0)) >>> 0;
++ av.style.setProperty('--ovc-mono-h', String(h % 360));
++ av.textContent = owner.slice(0, 1).toUpperCase();
const t1 = el('span', 'ovc-repo-t');
const name = el('b'); name.textContent = String(r.full_name || '');
const sub = el('small'); sub.textContent = String(r.description || '');
@@@ -1113,22 +1126,31 @@ html[data-ov-motion=off] .ovc-tray, html[data-ov-motion=off] .ovc-more svg { ani
});
};
const search = async () => {
-- const q = field.value.trim();
++ const q = field.value.trim().replace(/\s+/g, ' ');
const my = ++gen;
++ if (ctl) { try { ctl.abort(); } catch { /* */ } ctl = null; }
if (q.length < 2) { list.textContent = ''; note.hidden = false; note.textContent = 'Type what the project does: “goal bar overlay”, “markdown editor”.'; return; }
-- note.hidden = false; note.textContent = 'Searching…';
++ const key = q.toLowerCase();
++ const hit = REPO_CACHE.get(key);
++ if (hit && Date.now() - hit.at < REPO_CACHE_MS) { draw(hit.items); note.hidden = hit.items.length > 0; note.textContent = hit.items.length ? '' : 'Nothing found. Try other words.'; return; }
++ // The list stays while a new one is asked for; only an empty panel says it is searching.
++ if (!list.children.length) { note.hidden = false; note.textContent = 'Searching…'; }
try {
-- const r = await fetch(`${frame}/api/frame/code/search?q=${encodeURIComponent(q)}&limit=8`, { credentials: 'omit', headers: { accept: 'application/json' } });
++ ctl = typeof AbortController === 'function' ? new AbortController() : null;
++ const r = await fetch(`${frame}/api/frame/code/search?q=${encodeURIComponent(q)}&limit=8`, { credentials: 'omit', headers: { accept: 'application/json' }, signal: ctl ? ctl.signal : undefined });
const j = await r.json().catch(() => ({}));
if (my !== gen) return;
if (!r.ok) throw new Error(j.error || 'Search is unavailable right now');
const items = Array.isArray(j.items) ? j.items : [];
++ REPO_CACHE.set(key, { at: Date.now(), items });
++ if (REPO_CACHE.size > 60) REPO_CACHE.delete(REPO_CACHE.keys().next().value);
draw(items);
note.hidden = items.length > 0;
note.textContent = items.length ? '' : 'Nothing found. Try other words.';
-- } catch (err) { if (my === gen) { list.textContent = ''; note.hidden = false; note.textContent = (err && err.message) || 'Search is unavailable right now'; } }
++ } catch (err) { if (my === gen && !(err && err.name === 'AbortError')) { list.textContent = ''; note.hidden = false; note.textContent = (err && err.message) || 'Search is unavailable right now'; } }
};
-- field.addEventListener('input', () => { clearTimeout(t); t = setTimeout(search, 250); });
++ // A third of a second after the last key, and never the same words twice in ten minutes (REPO_CACHE).
++ field.addEventListener('input', () => { clearTimeout(t); t = setTimeout(search, 350); });
field.addEventListener('keydown', (e) => {
if (e.key === 'Escape') { e.preventDefault(); close(); }
else if (e.key === 'Enter') { e.preventDefault(); const first = list.querySelector('.ovc-repo-row'); if (first) first.click(); else { clearTimeout(t); search(); } }
ddiff --git a/ov-thread.js b/ov-thread.js
iindex 6a2419c..3b8222c 100644
---- a/ov-thread.js
++++ b/ov-thread.js
@@@ -24,7 +24,7 @@
// Motion off (html[data-ov-motion=off], theme-loader.js) is the only setting that skips an effect; Calm plays it.
var reduce = document.documentElement.getAttribute('data-ov-motion') === 'off';
var canCopy = !!(root.navigator && navigator.clipboard && navigator.clipboard.writeText);
-- const THREAD_REV = { 'ov-composer.js': 'ac7107f5d733', 'ov-combo.js': '8001a3b19c2f' };
++ const THREAD_REV = { 'ov-composer.js': '1b6c159fef2e', 'ov-combo.js': '8001a3b19c2f' };
var selfSrc = (document.currentScript && document.currentScript.src) || '';
// 4.202.0: the one dropdown for a field (ov-combo.js: a repository, a person), wherever a page asks for it.
function loadCombo() {
ddiff --git a/package-lock.json b/package-lock.json
iindex 40f46a3..2add4bf 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -1,12 +1,12 @@
{
"name": "openvibe-shared",
-- "version": "4.233.0",
++ "version": "4.234.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "openvibe-shared",
-- "version": "4.233.0",
++ "version": "4.234.0",
"license": "MIT",
"dependencies": {
"clean-css": "^5.3.3",
ddiff --git a/package.json b/package.json
iindex 5411c80..7ba9b6c 100644
---- a/package.json
++++ b/package.json
@@@ -1,6 +1,6 @@
{
"name": "openvibe-shared",
-- "version": "4.233.0",
++ "version": "4.234.0",
"description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site",
"main": "index.js",
"bin": {
ddiff --git a/test/composer-v2-chrome.test.js b/test/composer-v2-chrome.test.js
iindex afa3abf..8f0e889 100644
---- a/test/composer-v2-chrome.test.js
++++ b/test/composer-v2-chrome.test.js
@@@ -149,16 +149,23 @@ window.reply = C.mount(document.getElementById('d'), { preset: 'comment', id: 'r
await page.evaluate(`document.getElementById('g1').setAttribute('data-ovc-frame', location.origin), true`);
await page.evaluate(`document.querySelector('#g1 .ovc-more').click(), true`);
assert.ok((await page.evaluate(`[...document.querySelectorAll('#g1 .ovc-tray-t')].map((t) => t.textContent)`)).includes('GitHub'), 'GitHub is in the +');
++ // 4.234.0: the sheet does not repeat a tool the row already shows (the owner, 11-13: "why twice").
++ const repeat = await page.evaluate(`(() => { const row = [...document.querySelectorAll('#g1 .ovt-c-tools > .ovt-btn')].filter((b) => getComputedStyle(b).display !== 'none' && b.getClientRects().length).map((b) => b.getAttribute('aria-label')); const sheet = [...document.querySelectorAll('#g1 .ovc-tray-t')].map((t) => t.textContent); return row.filter((l) => l && sheet.includes(l)); })()`);
++ assert.deepEqual(repeat, [], 'nothing the row shows is in the + again');
await page.evaluate(`[...document.querySelectorAll('#g1 .ovc-tray-i')].find((b) => b.textContent === 'GitHub').click(), true`);
assert.equal(await page.evaluate(`document.activeElement === document.querySelector('#g1 .ovc-repo-q')`), true, 'the search field has the focus');
await page.evaluate(`(() => { const f = document.querySelector('#g1 .ovc-repo-q'); f.value = 'goal bar'; f.dispatchEvent(new Event('input', { bubbles: true })); })()`);
let rows = 0;
for (let i = 0; i < 40 && rows < 2; i++) { await wait(50); rows = await page.evaluate(`document.querySelectorAll('#g1 .ovc-repo-row').length`); }
assert.equal(rows, 2, 'the repositories as you type');
-- assert.deepEqual(searched, ['goal bar'], 'asked once, a quarter second after the last key');
-- const shown = await page.evaluate(`[...document.querySelectorAll('#g1 .ovc-repo-row')].map((r) => ({ name: r.querySelector('b').textContent, sub: r.querySelector('small').textContent, meta: r.querySelector('i').textContent, img: r.querySelector('img') ? r.querySelector('img').getAttribute('src') : null, bold: !!r.querySelector('small b') }))`);
-- assert.deepEqual(shown[0], { name: 'ada/goal-bar', sub: 'A goal bar <b>for</b> streams', meta: 'JavaScript · ★ 1.3k · MIT', img: 'https://avatars.githubusercontent.com/u/1?v=4', bold: false }, 'everything as text');
-- assert.equal(shown[1].img, null, 'a picture from anywhere but GitHub\'s avatar host is not drawn');
++ assert.deepEqual(searched, ['goal bar'], 'asked once, a third of a second after the last key');
++ const shown = await page.evaluate(`[...document.querySelectorAll('#g1 .ovc-repo-row')].map((r) => ({ name: r.querySelector('b').textContent, sub: r.querySelector('small').textContent, meta: r.querySelector('i').textContent, img: r.querySelector('img') ? r.querySelector('img').getAttribute('src') : null, mono: (r.querySelector('.ovc-repo-mono') || {}).textContent || null, bold: !!r.querySelector('small b') }))`);
++ assert.deepEqual(shown[0], { name: 'ada/goal-bar', sub: 'A goal bar <b>for</b> streams', meta: 'JavaScript · ★ 1.3k · MIT', img: null, mono: 'A', bold: false }, 'everything as text; the owner\'s initial, no picture fetched');
++ assert.ok(shown.every((r) => r.img === null), 'no row asks for a picture (they were broken on every site with an image policy)');
++ // The same words again: from the page's cache, not another search.
++ await page.evaluate(`(() => { const f = document.querySelector('#g1 .ovc-repo-q'); f.value = 'goal bar'; f.dispatchEvent(new Event('input', { bubbles: true })); })()`);
++ await wait(500);
++ assert.deepEqual(searched, ['goal bar'], 'the same words are answered from the cache');
await page.evaluate(`document.querySelector('#g1 .ovc-repo-q').dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true, cancelable: true })), true`);
assert.match(await page.evaluate(`document.querySelector('#g1 textarea').value`), /https:\/\/github\.com\/ada\/goal-bar $/, 'Enter puts the first one\'s address in the box');
assert.equal(await page.evaluate(`document.querySelector('#g1 .ovc-repo').hidden`), true, 'and the panel goes');
ddiff --git a/test/navbar-appbar.test.js b/test/navbar-appbar.test.js
iindex 5c4db90..1e184e9 100644
---- a/test/navbar-appbar.test.js
++++ b/test/navbar-appbar.test.js
@@@ -108,3 +108,12 @@ const LINKS = [
assert.strictEqual(barOf(body), null, 'one tab is no tab bar');
}
console.log('navbar app bar: opt-in, from links or a list, More, the open page, escaping, removal ok');
++
++// 4.234.0: a hidden bar is all the way hidden. With the lift still applied it stopped half on the screen above a keyboard
++// or Android's toolbar gap (the owner's screenshot, 11-13).
++{
++ const css = require('fs').readFileSync(require('path').join(__dirname, '..', 'appbar.js'), 'utf8');
++ const hidden = /body\.ov-panel-open \.ovnav-appbar \{([^}]*)\}/.exec(css);
++ require('assert').ok(hidden && /translate: none/.test(hidden[1]) && /visibility: hidden/.test(hidden[1]) && /pointer-events: none/.test(hidden[1]) && /translateY\(calc\(100% \+ 24px\)\)/.test(hidden[1]), 'hidden: no lift, below the screen, invisible, not clickable');
++ console.log('navbar-appbar: a hidden bar is all the way hidden');
++}