openvibers/openvibe.shared Commits

4.107.2: the last small touch targets in the shared bar and footer reach 44px (#278)

OP OpenVibers committed 10 days ago · f7efe9d198 · 10 files, +127 −11

The 11-03 re-audit of every site's home found Sign In 40px tall, the menu orb 36.9px wide on a phone, the footer
brand 33px tall and the orbit's big nodes 42px. Their invisible hit areas now reach 44px; the orb grows to the right,
so the site's name keeps the taps on its last letters. A Chrome test measures each to a finger from 320 to 430px.

Changes

ddiff --git a/CHANGELOG.md b/CHANGELOG.md
iindex 0f5b781..81f31c2 100644
---- a/CHANGELOG.md
++++ b/CHANGELOG.md
@@@ -4,6 +4,19 @@ All notable changes to `openvibe-shared`. Versions follow [semver](https://semve
breaking change to any exported module, browser global or served file name is a new major.
A release is the git tag `vX.Y.Z`; consumers pin the tag's tarball (see README).
++## 4.107.2 — 2026-11-03
++
++- **The last small touch targets in the shared bar and footer reach 44px.** A re-run of the tap audit over every
++ site's home (11-03: 31 of 45 sites with a small target, down from 40 of 42 on 10-29) found these:
++ - Sign In was 40px tall; its invisible hit area now reaches 45px;
++ - the OpenVibe menu's orb was 36.9px wide on a phone; it grows to the right, toward the open bar, so it measures
++ 44px while the site's name keeps the taps on its own last letters;
++ - the footer's brand was 33px tall, now 45px;
++ - the orbit's big nodes on a phone were 42px, now 44px.
++ Nothing is drawn differently. `test/tap-areas-chrome.test.js` measures each to a finger with
++ `elementFromPoint`, from 320 to 430px, signed in and out, and checks that the name and the right-hand group keep
++ their own taps.
++
## 4.107.1 — 2026-11-03
- **Legal pages draw the brand in its own font on every site.** `legal.page()` loaded the theme loader, the navbar and
ddiff --git a/README.md b/README.md
iindex c0920ba..e17f663 100644
---- a/README.md
++++ b/README.md
@@@ -2,7 +2,7 @@
> Versioned UI, the OpenVibe Frame (the navbar, footer and "shipped" views every site sits in), SEO, legal and release-client packages every OpenVibe site renders.
--**Status:** alpha, v4.107.1 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed
++**Status:** alpha, v4.107.2 (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 8b59dc1..347e86b 100644
---- a/STATUS.json
++++ b/STATUS.json
@@@ -4,7 +4,7 @@
"code": true,
"wave": "W2",
"package": "openvibe-shared",
-- "version": "4.107.1",
++ "version": "4.107.2",
"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/footer.js b/footer.js
iindex 79d055e..716468d 100644
---- a/footer.js
++++ b/footer.js
@@@ -384,8 +384,9 @@
.ovf-col a:hover{color:var(--accent,#60a5fa);transform:translateX(2px)}
@media (pointer:coarse){.ovf-col a{display:inline-flex;align-items:center;min-height:32px}.ovf-col .ovf-social a{width:44px;height:44px}.ovf-legal-inline a,.ovf-compact-links a{position:relative}.ovf-legal-inline a::after,.ovf-compact-links a::after{content:'';position:absolute;left:0;right:0;top:-12px;bottom:-12px}}
.ovf-more{font-weight:600;opacity:.85}.ovf-ic{flex:none;vertical-align:-.14em}.ovf-more .ovf-ic{font-size:.85em;margin-left:3px;transition:transform .15s}.ovf-more:hover .ovf-ic{transform:translateX(3px)}.ovf-social .ovf-ic{font-size:17px}.ovf-account-links .ovf-ic{font-size:1.05em;opacity:.85}
--.ovf-brand{display:inline-flex;align-items:center;gap:9px;color:var(--text-primary,#f1f4fb);font-family:'OV Brand',Nunito,Inter,-apple-system,BlinkMacSystemFont,sans-serif;font-size:1.12rem;font-weight:800;letter-spacing:-.1px;text-decoration:none}
++.ovf-brand{position:relative;display:inline-flex;align-items:center;gap:9px;color:var(--text-primary,#f1f4fb);font-family:'OV Brand',Nunito,Inter,-apple-system,BlinkMacSystemFont,sans-serif;font-size:1.12rem;font-weight:800;letter-spacing:-.1px;text-decoration:none}
.ovf-brand--sm{font-size:.9rem}
++.ovf-brand::after{content:'';position:absolute;inset:-6px 0}
.ovf-tagline{margin:0;font-size:.82rem;line-height:1.55;color:var(--text-muted,#8b93ad);max-width:46ch}
.ovf-social{display:flex;gap:8px;margin-top:4px}
.ovf-social a{width:36px;height:36px;border-radius:10px;display:grid;place-items:center;color:var(--text-secondary,#b6bdd2);
ddiff --git a/navbar.js b/navbar.js
iindex a6818c0..b8a771e 100644
---- a/navbar.js
++++ b/navbar.js
@@@ -77,10 +77,13 @@
.openvibe-navbar .openvibe-navbar-brand .flame::after { content: ''; position: absolute; inset: -8px; }
/* 4.46.0 (the fleet tap audit): Sign In, the menu and the orb reach 44px to a finger, still drawn as before. */
.openvibe-navbar .openvibe-navbar-login, .openvibe-navbar .ovnav-burger, .openvibe-navbar .ovnav-orb { position: relative; }
-- .openvibe-navbar .openvibe-navbar-login::after { content: ''; position: absolute; inset: -6px 0; }
++ /* 4.107.2 (the 11-03 re-audit): Sign In measured 40px tall; 9px above and below makes 45. */
++ .openvibe-navbar .openvibe-navbar-login::after { content: ''; position: absolute; inset: -9px 0; }
.openvibe-navbar .ovnav-burger::after { content: ''; position: absolute; inset: -3px -5px; }
-- /* The orb grows up and down only a little sideways: the site's name sits 8px from it and keeps its own taps. */
-- .openvibe-navbar .ovnav-orb::after { content: ''; position: absolute; inset: -6px -4px; }
++ /* The orb grows up and down, and sideways mostly to the right: the site's name sits 8px to its left and keeps
++ its own taps (4px there); the extra width goes to the open bar on its right (12px), so it measures 44px
++ wide on a phone (it was 36.9 in the 11-03 re-audit). */
++ .openvibe-navbar .ovnav-orb::after { content: ''; position: absolute; inset: -6px -12px -6px -4px; }
}
.openvibe-navbar .ovnav-dd { position: relative; display: inline-flex; flex: none; }
.openvibe-navbar .ovnav-dd-menu { display: none; position: fixed; top: 56px; left: 0; min-width: 180px; padding: 5px; border-radius: 12px; z-index: 1002; background: var(--bg-elevated, var(--bg-secondary, #111826)); border: 1px solid var(--border, rgba(255,255,255,.12)); box-shadow: 0 18px 44px rgba(0,0,0,.45); flex-direction: column; gap: 2px; }
ddiff --git a/package-lock.json b/package-lock.json
iindex 32fde25..f94ca70 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -1,12 +1,12 @@
{
"name": "openvibe-shared",
-- "version": "4.107.1",
++ "version": "4.107.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "openvibe-shared",
-- "version": "4.107.1",
++ "version": "4.107.2",
"license": "MIT",
"dependencies": {
"jsonwebtoken": "^9.0.0",
ddiff --git a/package.json b/package.json
iindex dac0390..3e9e12d 100644
---- a/package.json
++++ b/package.json
@@@ -1,6 +1,6 @@
{
"name": "openvibe-shared",
-- "version": "4.107.1",
++ "version": "4.107.2",
"description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site",
"main": "index.js",
"bin": {
ddiff --git a/showcase.css b/showcase.css
iindex 0409118..600f63d 100644
---- a/showcase.css
++++ b/showcase.css
@@@ -33,7 +33,7 @@ html[data-ov-motion=reduced] .sc-ring,html[data-ov-motion=reduced] .sc-aura-glow
.sc-hero.sc-has-orbit .sc-hero-vis{order:-1;position:static;width:auto;opacity:1;pointer-events:auto;margin:0 0 6px}
.sc-hero.sc-has-orbit .sc-vis{width:min(70vw,250px)}
.sc-hero.sc-has-orbit .sc-orb-core .sc-vis-mark{width:50px!important;height:50px!important}
--.sc-orb-node{width:32px;height:32px;margin:-16px}.sc-orb-node.is-big{width:42px;height:42px;margin:-21px}.sc-orb-name{font-size:10.5px}
++.sc-orb-node{width:32px;height:32px;margin:-16px}.sc-orb-node.is-big{width:44px;height:44px;margin:-22px}.sc-orb-name{font-size:10.5px}
}
.sc-hero,.sc-sec,.sc-cta{max-width:1080px;margin:0 auto;padding:0 24px;font-family:var(--ov-font,'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif)}
ddiff --git a/showcase.js b/showcase.js
iindex 45b828e..602fcac 100644
---- a/showcase.js
++++ b/showcase.js
@@@ -461,7 +461,7 @@ html[data-ov-motion=reduced] .sc-ring,html[data-ov-motion=reduced] .sc-aura-glow
.sc-hero.sc-has-orbit .sc-hero-vis{order:-1;position:static;width:auto;opacity:1;pointer-events:auto;margin:0 0 6px}
.sc-hero.sc-has-orbit .sc-vis{width:min(70vw,250px)}
.sc-hero.sc-has-orbit .sc-orb-core .sc-vis-mark{width:50px!important;height:50px!important}
--.sc-orb-node{width:32px;height:32px;margin:-16px}.sc-orb-node.is-big{width:42px;height:42px;margin:-21px}.sc-orb-name{font-size:10.5px}
++.sc-orb-node{width:32px;height:32px;margin:-16px}.sc-orb-node.is-big{width:44px;height:44px;margin:-22px}.sc-orb-name{font-size:10.5px}
}
.sc-hero,.sc-sec,.sc-cta{max-width:1080px;margin:0 auto;padding:0 24px;font-family:var(--ov-font,'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif)}
ddiff --git a/test/tap-areas-chrome.test.js b/test/tap-areas-chrome.test.js
nnew file mode 100644
iindex 0000000..3ecb5c3
---- /dev/null
++++ b/test/tap-areas-chrome.test.js
@@@ -0,0 +1,99 @@
++'use strict';
++// Touch targets measured to a finger, in a real headless Chrome (4.107.2, the 11-03 re-audit of every site's home:
++// tools/tap-audit.js). A target's effective size is the run of points around its centre that land on it
++// (document.elementFromPoint), so an invisible ::after hit area counts and a neighbour that covers it does not.
++// From 320px to 430px, signed in and out: Sign In and the OpenVibe menu's orb measure 44px or more each way, and the
++// footer's brand 44px tall. The orb's extra width goes right, so the site's name keeps the taps on its own last
++// letters, and nothing in the bar's right-hand group loses its own. Skipped without Chrome.
++const assert = require('assert');
++const fs = require('fs');
++const os = require('os');
++const path = require('path');
++const http = require('http');
++const h = require('../browser-harness');
++
++if (process.env.OV_SKIP_BROWSER === '1' || !h.findChrome()) {
++ console.log('tap-areas-chrome: skipped (no Chrome; set CHROME_BIN)');
++ process.exit(0);
++}
++
++const ROOT = path.join(__dirname, '..');
++const PAGE = (signedIn) => `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Live · OpenVibe.Live</title><link rel="icon" href="data:,"></head>
++<body><div id="navbar-mount"></div><main style="min-height:40px"><h1>Live</h1></main><footer id="footer-mount"></footer>
++<script>
++ localStorage.setItem('ov-wallet:42', JSON.stringify({ coins: 161000, vibes: 400 }));
++ window.fetch = async () => new Response('{}', { status: 404 });
++</script>
++<script src="/navbar.js"></script><script src="/footer.js"></script>
++<script>
++ OpenVibeNavbar.init({ service: 'live', brandName: 'OpenVibe.Live', wallet: true, messages: { href: 'https://openvibe.chat/messages' }, ${signedIn ? "token: 'x', user: { id: 42, username: 'moosely', display_name: 'moosely', avatar_url: null }," : 'user: null,'}
++ links: [{ label: 'Home', href: '/', icon: 'fa-house' }] });
++ OpenVibeFooter.init({ service: 'live', mount: '#footer-mount', variant: 'full' });
++</script></body></html>`;
++const server = http.createServer((req, res) => {
++ const u = new URL(req.url, 'http://x');
++ if (u.pathname === '/in' || u.pathname === '/out') { res.writeHead(200, { 'content-type': 'text/html' }); return res.end(PAGE(u.pathname === '/in')); }
++ const name = u.pathname.slice(1);
++ if (/^[a-z-]+\.js$/.test(name) && fs.existsSync(path.join(ROOT, name))) { res.writeHead(200, { 'content-type': 'application/javascript' }); return res.end(fs.readFileSync(path.join(ROOT, name))); }
++ res.writeHead(404); return res.end();
++});
++
++// The effective size of a target: the longest run of points through its centre, 1px apart, that land on it.
++const MEASURE = `(() => {
++ const on = (el, x, y) => { const hit = document.elementFromPoint(x, y); return !!hit && (hit === el || el.contains(hit)); };
++ const size = (el) => {
++ if (!el) return null;
++ el.scrollIntoView({ block: 'center' });
++ const r = el.getBoundingClientRect();
++ const cx = r.left + r.width / 2, cy = r.top + r.height / 2;
++ const run = (dx, dy) => { let a = 0, b = 0; while (a < 60 && on(el, cx - (a + 1) * dx, cy - (a + 1) * dy)) a++; while (b < 60 && on(el, cx + (b + 1) * dx, cy + (b + 1) * dy)) b++; return a + b + 1; };
++ return { w: run(1, 0), h: run(0, 1), box: [Math.round(r.left), Math.round(r.right)] };
++ };
++ const bar = document.querySelector('.openvibe-navbar');
++ scrollTo(0, 0);
++ const name = bar.querySelector('.openvibe-navbar-brand .name');
++ const nameLinks = [...name.querySelectorAll('a')];
++ const lastLink = nameLinks[nameLinks.length - 1];
++ const lr = lastLink.getBoundingClientRect();
++ // At 320px the name is cut short (it reads whole from 360px): its last link ends where the name's box does.
++ const edge = Math.min(lr.right, name.getBoundingClientRect().right);
++ const nameKeepsItsEnd = on(lastLink, edge - 2, lr.top + lr.height / 2) || !on(bar.querySelector('.ovnav-orb'), edge - 2, lr.top + lr.height / 2);
++ const right = bar.querySelector('.openvibe-navbar-right');
++ const firstRight = right && [...right.querySelectorAll('a, button')].find((el) => el.getBoundingClientRect().width > 0);
++ const fr = firstRight && firstRight.getBoundingClientRect();
++ const rightKeepsItsStart = !firstRight || on(firstRight, fr.left + 2, fr.top + fr.height / 2);
++ const out = { signIn: size(bar.querySelector('#openvibe-login-btn')), orb: size(bar.querySelector('.ovnav-orb')), nameKeepsItsEnd, rightKeepsItsStart };
++ out.footerBrand = size(document.querySelector('.ovf-brand'));
++ return out;
++})()`;
++
++(async () => {
++ await new Promise((ok) => server.listen(0, '127.0.0.1', ok));
++ const base = `http://127.0.0.1:${server.address().port}`;
++ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-taps-'));
++ const browser = await h.launch({ tmpDir });
++ const wait = (ms) => new Promise((r) => setTimeout(r, ms));
++ try {
++ const context = await browser.newContext();
++ for (const width of [320, 360, 393, 430]) {
++ for (const who of ['out', 'in']) {
++ const p = await h.openPage(browser, context, { width, height: 800 });
++ await p.goto(`${base}/${who}`, { maxMs: 5000 });
++ await wait(300);
++ const v = await p.evaluate(MEASURE);
++ const at = `${width}px signed ${who}`;
++ if (who === 'out') assert.ok(v.signIn && v.signIn.h >= 44 && v.signIn.w >= 44, `${at}: Sign In ${JSON.stringify(v.signIn)}`);
++ assert.ok(v.orb && v.orb.w >= 44 && v.orb.h >= 44, `${at}: the orb ${JSON.stringify(v.orb)}`);
++ assert.ok(v.nameKeepsItsEnd, `${at}: the site's name keeps the taps on its last letters`);
++ assert.ok(v.rightKeepsItsStart, `${at}: the right-hand group keeps its own first taps`);
++ assert.ok(v.footerBrand && v.footerBrand.h >= 44, `${at}: the footer's brand ${JSON.stringify(v.footerBrand)}`);
++ await p.close();
++ }
++ }
++ console.log('tap-areas-chrome: Sign In, the orb and the footer brand reach 44px; the name and the right group keep their taps ok');
++ } finally {
++ await browser.close();
++ server.close();
++ fs.rmSync(tmpDir, { recursive: true, force: true });
++ }
++})().catch((e) => { console.error(e); process.exit(1); });