openvibers/openvibe.shared Commits
4.119.0: a featured link keeps an icon button on the phone's bar (#304)
Changes
@@@ -1,5 +1,9 @@# Changelog++## 4.119.0 — 2026-11-06++++- A featured link keeps a place on the phone's bar: where the links row folds into the menu, the featured link (4.117.0) also shows as one tinted icon button beside Sign In or the avatar (38 px, 34 px under 400 px, a 44 px hit area on the element), named by its label for screen readers. The Node on the Network is one tap away on a phone. Only a featured link with an icon gets one; no animation.++## 4.118.0 — 2026-11-06- Locker reset and unlock errors requiring fresh authentication now link to Network’s password reauthentication with the complete current page as the return destination, including `locker.proof_required`.@@@ -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.118.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed++**Status:** alpha, v4.119.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployedconsumer installs a tagged release and none keeps a vendored copy([docs/migration-plan.md](docs/migration-plan.md) is done); each repository's `package.json` namesthe tag it pins.@@@ -4,7 +4,7 @@"code": true,"wave": "W2","package": "openvibe-shared",-- "version": "4.118.0",++ "version": "4.119.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)",@@@ -101,6 +101,11 @@.openvibe-navbar-dropdown-header a.name.ud-me { line-height: inherit; display: block; }.openvibe-navbar-dropdown-header a.name.ud-me:hover, .openvibe-navbar-dropdown-header a.name.ud-me:focus-visible { text-decoration: underline; }.openvibe-navbar-dropdown .ovnav-chip--menu { all: unset; box-sizing: border-box; cursor: pointer; display: inline-flex; align-items: center; gap: 5px; padding: 3px 10px; border-radius: 999px; font: 700 12px/1.2 inherit; color: var(--chip-c, #fbbf24); background: color-mix(in srgb, var(--chip-c, #fbbf24) 12%, transparent); border: 1px solid color-mix(in srgb, var(--chip-c, #fbbf24) 34%, transparent); }++ /* 4.119.0: the featured link's phone button */++ .openvibe-navbar .ovnav-featured-phone { all: unset; box-sizing: border-box; position: relative; cursor: pointer; display: none; width: 38px; height: 38px; border-radius: 999px; align-items: center; justify-content: center; flex: none; color: var(--accent-light, #60a5fa); background: color-mix(in srgb, var(--accent, #3b82f6) 14%, transparent); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent, #3b82f6) 35%, transparent); }++ .openvibe-navbar .ovnav-featured-phone::after { content: ''; position: absolute; inset: -5px; }++ .openvibe-navbar .ovnav-featured-phone :is(.ovnav-ic, svg, i) { width: 17px; height: 17px; font-size: 16px; }++ .openvibe-navbar .ovnav-featured-phone:focus-visible { outline: 2px solid var(--accent-light, #60a5fa); outline-offset: 2px; }.openvibe-navbar .ovnav-burger { all: unset; box-sizing: border-box; position: relative; cursor: pointer; display: none; width: 38px; height: 38px; border-radius: 11px; flex-direction: column; align-items: center; justify-content: center; gap: 4px; flex: none; }.openvibe-navbar .ovnav-burger span { display: block; width: 18px; height: 2px; border-radius: 2px; background: currentColor; transition: transform .2s, opacity .2s; }.openvibe-navbar .ovnav-burger[aria-expanded="true"] span:nth-child(1) { transform: translateY(6px) rotate(45deg); }@@@ -167,7 +172,7 @@.openvibe-navbar-right.has-chips-all .ovnav-chip[data-chip-id] { display: none; }.openvibe-navbar-right.has-chips-all .ovnav-chip--all { display: inline-flex; }}-- @media (max-width: 860px) { .openvibe-navbar .ovnav-burger { display: inline-flex; } .openvibe-navbar .openvibe-navbar-links, .openvibe-navbar .openvibe-navbar-spacer { display: none; } .openvibe-navbar .openvibe-navbar-right { margin-left: auto; } .openvibe-navbar .ovnav-chip .ovnav-ic + .ovnav-chip-v:empty { display: none; } }++ @media (max-width: 860px) { .openvibe-navbar .ovnav-burger, .openvibe-navbar .ovnav-featured-phone { display: inline-flex; } .openvibe-navbar .openvibe-navbar-links, .openvibe-navbar .openvibe-navbar-spacer { display: none; } .openvibe-navbar .openvibe-navbar-right { margin-left: auto; } .openvibe-navbar .ovnav-chip .ovnav-ic + .ovnav-chip-v:empty { display: none; } }@media (max-width: 420px) { .openvibe-navbar .ovnav-chip { padding: 0 8px; height: 30px; font-size: 12px; } }/* A narrow phone never draws the site's name over the bar's buttons (owner, 2026-10-28: on openvibe.games, signedin, the name ran under the launcher and the magnifier). The name gives way with an ellipsis, and up to 440px@@@ -179,7 +184,7 @@tightens instead of folding to one. */@media (max-width: 440px) { .ovnav-chip-mini { padding: 0 5px; gap: 3px; } .openvibe-navbar .ovnav-chip.ovnav-chip--all { font-size: 12px; } .ovnav-chip-mini .ovnav-ic { width: 13px; height: 13px; } }@media (max-width: 400px) { .openvibe-navbar .openvibe-navbar-right { gap: 1px; } .ovnav-chip-mini { padding: 0 3px; gap: 2px; } .openvibe-navbar .ovnav-chip.ovnav-chip--all { font-size: 11.5px; letter-spacing: -.2px; } .openvibe-navbar .openvibe-navbar-brand { gap: 4px; }-- .openvibe-navbar { padding-left: 8px; padding-right: 6px; gap: 2px; } .openvibe-navbar .ovnav-burger { width: 34px; } .openvibe-navbar .openvibe-navbar-avatar { width: 32px; height: 32px; box-sizing: border-box; }++ .openvibe-navbar { padding-left: 8px; padding-right: 6px; gap: 2px; } .openvibe-navbar .ovnav-burger { width: 34px; } .openvibe-navbar .ovnav-featured-phone { width: 34px; height: 34px; } .openvibe-navbar .openvibe-navbar-avatar { width: 32px; height: 32px; box-sizing: border-box; }.openvibe-navbar .openvibe-navbar-brand .name { font-size: 16.5px; letter-spacing: -.25px; } .openvibe-navbar .openvibe-navbar-brand .flame { min-width: 28px; height: 28px; padding: 0 2px; } .openvibe-navbar .openvibe-navbar-brand .flame .ov-mark { width: 28px !important; } }/* Under 350px the mark alone names the site, so both balances keep their icons (a number alone says nothing). */@media (max-width: 350px) { .openvibe-navbar .openvibe-navbar-brand .name { display: none; } }@@@ -1259,6 +1264,12 @@const menu = kids.map(k => `<a href="${escapeAttr(k.href || '#')}" class="ovnav-link ovnav-sublink"${k.id ? ` data-link-id="${escapeAttr(k.id)}"` : ''}${k.hidden ? ' hidden' : ''}>${k.icon ? `<span class="icon">${navIcon(k.icon)}</span>` : ''}<span class="ovnav-l">${escapeAttr(k.label)}</span></a>`).join('');return inDrawer ? a + menu : `<div class="ovnav-dd"${l.hidden ? ' hidden' : ''}>${a}<div class="ovnav-dd-menu">${menu}</div></div>`;}++ /** 4.119.0: a featured link with an icon keeps a button on the phone's bar. */++ function featuredPhoneHTML(links) {++ const f = (links || []).find(l => l && l.featured && !l.hidden && l.icon);++ if (!f) return '';++ return `<a class="ovnav-featured-phone${f.active ? ' active' : ''}" href="${escapeAttr(f.href || '#')}" aria-label="${escapeAttr(f.label)}" title="${escapeAttr(f.label)}"${f.active ? ' aria-current="page"' : ''}>${navIcon(f.icon)}</a>`;++ }const flatLinks = (list) => list.reduce((out, l) => out.concat([l], Array.isArray(l.children) ? l.children : []), []);/** The drawer's order: a featured link leads the menu (the phone's first entry), the rest keep theirs. */@@@ -1857,6 +1868,7 @@<div class="openvibe-navbar-right">${u ? chipsHTML(_config.chips) + chipsAllHTML(_config.chips) : ''}${u && !isAnon && _config.messages ? messagesHTML() : ''}++ ${featuredPhoneHTML(links)}<div id="openvibe-bell-mount"></div>${u ? avatarImg(u, 64, 'openvibe-navbar-avatar', 'openvibe-avatar-btn') :`<a class="openvibe-navbar-login" id="openvibe-login-btn" href="${escapeAttr(loginHref)}">Sign In</a>`}@@@ -1,12 +1,12 @@{"name": "openvibe-shared",-- "version": "4.118.0",++ "version": "4.119.0","lockfileVersion": 3,"requires": true,"packages": {"": {"name": "openvibe-shared",-- "version": "4.118.0",++ "version": "4.119.0","license": "MIT","dependencies": {"jsonwebtoken": "^9.0.0",@@@ -1,6 +1,6 @@{"name": "openvibe-shared",-- "version": "4.118.0",++ "version": "4.119.0","description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site","main": "index.js","bin": {@@@ -13,7 +13,7 @@ const { brotli } = require('../scripts/size-report');const ROOT = path.join(__dirname, '..');const NAVBAR_PATH = path.join(ROOT, 'navbar.js');const ICONS = require('../nav-icons');--const NAVBAR_BROTLI_BUDGET = 42 * 1024; // 41.4 KB with the page-move code (4.70.0: busy(), the mark on plain links, prefetch rules on unboosted sites); was 41 KB with the Dock's loader (4.51.0)++const NAVBAR_BROTLI_BUDGET = 42.5 * 1024; // 42.1 KB with the featured link's phone button (4.119.0, 2026-11-06); 41.4 KB with the page-move code (4.70.0: busy(), the mark on plain links, prefetch rules on unboosted sites); was 41 KB with the Dock's loader (4.51.0)// ── size budget and generated block ──────────────────────────{@@@ -51,6 +51,10 @@ const between = (html, a, b) => { const i = html.indexOf(a); const j = html.indeassert.ok(!/ovnav-link--featured[^}]*animation/.test(bar), 'the pill does not animate');const drawer = between(barHtml(created), 'id="openvibe-drawer"', 'ovnav-drawer-net');assert.ok(drawer.indexOf('>Node<') >= 0 && drawer.indexOf('>Node<') < drawer.indexOf('>My Account<'), 'on a phone the Node is the first entry of the menu');++ // 4.119.0: on a phone the links row folds away, so the featured link keeps an icon button on the bar itself.++ const right = between(barHtml(created), 'openvibe-navbar-right', 'openvibe-bell-mount');++ assert.ok(/<a class="ovnav-featured-phone" href="https:\/\/node\.openvibe\.network" aria-label="Node"/.test(right), 'the phone bar has the Node button, named for readers');++ assert.strictEqual((barHtml(created).match(/ovnav-featured-phone"/g) || []).length, 1, 'one phone button');}{// Without the option, nothing changes: an ordinary link, in the order it was given.@@@ -63,6 +67,7 @@ const between = (html, a, b) => { const i = html.indexOf(a); const j = html.indeassert.ok(!bar.includes('ovnav-link--featured'), 'a plain link is not a pill');const drawer = between(barHtml(created), 'id="openvibe-drawer"', 'ovnav-drawer-net');assert.ok(drawer.indexOf('>My Account<') < drawer.indexOf('>Node<'), 'and it keeps its place in the menu');++ assert.ok(!barHtml(created).includes('ovnav-featured-phone"'), 'no phone button without a featured link');}{// The stylesheet carries the pill: one hairline, one tint, the glyph, and nothing that moves.
@@ -1,5 +1,9 @@
@@ -1,5 +1,9 @@
# Changelog
# Changelog
+## 4.119.0 — 2026-11-06
+
+- A featured link keeps a place on the phone's bar: where the links row folds into the menu, the featured link (4.117.0) also shows as one tinted icon button beside Sign In or the avatar (38 px, 34 px under 400 px, a 44 px hit area on the element), named by its label for screen readers. The Node on the Network is one tap away on a phone. Only a featured link with an icon gets one; no animation.
+
## 4.118.0 — 2026-11-06
## 4.118.0 — 2026-11-06
- Locker reset and unlock errors requiring fresh authentication now link to Network’s password reauthentication with the complete current page as the return destination, including `locker.proof_required`.
- Locker reset and unlock errors requiring fresh authentication now link to Network’s password reauthentication with the complete current page as the return destination, including `locker.proof_required`.
@@ -2,7 +2,7 @@
@@ -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.
> 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.118.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed
+**Status:** alpha, v4.119.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed
consumer installs a tagged release and none keeps a vendored copy
consumer installs a tagged release and none keeps a vendored copy
([docs/migration-plan.md](docs/migration-plan.md) is done); each repository's `package.json` names
([docs/migration-plan.md](docs/migration-plan.md) is done); each repository's `package.json` names
the tag it pins.
the tag it pins.
@@ -4,7 +4,7 @@
@@ -4,7 +4,7 @@
"code": true,
"code": true,
"wave": "W2",
"wave": "W2",
"package": "openvibe-shared",
"package": "openvibe-shared",
- "version": "4.118.0",
+ "version": "4.119.0",
"plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026)",
"plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026)",
"deployed": false,
"deployed": false,
"deployment": "none of its own: a package released as git tags; OpenVibe.Network serves /shared/* from its pinned tag, and a site can serve its own pinned copy (openvibe-shared/serve)",
"deployment": "none of its own: a package released as git tags; OpenVibe.Network serves /shared/* from its pinned tag, and a site can serve its own pinned copy (openvibe-shared/serve)",
@@ -101,6 +101,11 @@
@@ -101,6 +101,11 @@
.openvibe-navbar-dropdown-header a.name.ud-me { line-height: inherit; display: block; }
.openvibe-navbar-dropdown-header a.name.ud-me { line-height: inherit; display: block; }
.openvibe-navbar-dropdown-header a.name.ud-me:hover, .openvibe-navbar-dropdown-header a.name.ud-me:focus-visible { text-decoration: underline; }
.openvibe-navbar-dropdown-header a.name.ud-me:hover, .openvibe-navbar-dropdown-header a.name.ud-me:focus-visible { text-decoration: underline; }
.openvibe-navbar-dropdown .ovnav-chip--menu { all: unset; box-sizing: border-box; cursor: pointer; display: inline-flex; align-items: center; gap: 5px; padding: 3px 10px; border-radius: 999px; font: 700 12px/1.2 inherit; color: var(--chip-c, #fbbf24); background: color-mix(in srgb, var(--chip-c, #fbbf24) 12%, transparent); border: 1px solid color-mix(in srgb, var(--chip-c, #fbbf24) 34%, transparent); }
.openvibe-navbar-dropdown .ovnav-chip--menu { all: unset; box-sizing: border-box; cursor: pointer; display: inline-flex; align-items: center; gap: 5px; padding: 3px 10px; border-radius: 999px; font: 700 12px/1.2 inherit; color: var(--chip-c, #fbbf24); background: color-mix(in srgb, var(--chip-c, #fbbf24) 12%, transparent); border: 1px solid color-mix(in srgb, var(--chip-c, #fbbf24) 34%, transparent); }
+ /* 4.119.0: the featured link's phone button */
+ .openvibe-navbar .ovnav-featured-phone { all: unset; box-sizing: border-box; position: relative; cursor: pointer; display: none; width: 38px; height: 38px; border-radius: 999px; align-items: center; justify-content: center; flex: none; color: var(--accent-light, #60a5fa); background: color-mix(in srgb, var(--accent, #3b82f6) 14%, transparent); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent, #3b82f6) 35%, transparent); }
+ .openvibe-navbar .ovnav-featured-phone::after { content: ''; position: absolute; inset: -5px; }
+ .openvibe-navbar .ovnav-featured-phone :is(.ovnav-ic, svg, i) { width: 17px; height: 17px; font-size: 16px; }
+ .openvibe-navbar .ovnav-featured-phone:focus-visible { outline: 2px solid var(--accent-light, #60a5fa); outline-offset: 2px; }
.openvibe-navbar .ovnav-burger { all: unset; box-sizing: border-box; position: relative; cursor: pointer; display: none; width: 38px; height: 38px; border-radius: 11px; flex-direction: column; align-items: center; justify-content: center; gap: 4px; flex: none; }
.openvibe-navbar .ovnav-burger { all: unset; box-sizing: border-box; position: relative; cursor: pointer; display: none; width: 38px; height: 38px; border-radius: 11px; flex-direction: column; align-items: center; justify-content: center; gap: 4px; flex: none; }
.openvibe-navbar .ovnav-burger span { display: block; width: 18px; height: 2px; border-radius: 2px; background: currentColor; transition: transform .2s, opacity .2s; }
.openvibe-navbar .ovnav-burger span { display: block; width: 18px; height: 2px; border-radius: 2px; background: currentColor; transition: transform .2s, opacity .2s; }
.openvibe-navbar .ovnav-burger[aria-expanded="true"] span:nth-child(1) { transform: translateY(6px) rotate(45deg); }
.openvibe-navbar .ovnav-burger[aria-expanded="true"] span:nth-child(1) { transform: translateY(6px) rotate(45deg); }
@@ -167,7 +172,7 @@
@@ -167,7 +172,7 @@
.openvibe-navbar-right.has-chips-all .ovnav-chip[data-chip-id] { display: none; }
.openvibe-navbar-right.has-chips-all .ovnav-chip[data-chip-id] { display: none; }
.openvibe-navbar-right.has-chips-all .ovnav-chip--all { display: inline-flex; }
.openvibe-navbar-right.has-chips-all .ovnav-chip--all { display: inline-flex; }
}
}
- @media (max-width: 860px) { .openvibe-navbar .ovnav-burger { display: inline-flex; } .openvibe-navbar .openvibe-navbar-links, .openvibe-navbar .openvibe-navbar-spacer { display: none; } .openvibe-navbar .openvibe-navbar-right { margin-left: auto; } .openvibe-navbar .ovnav-chip .ovnav-ic + .ovnav-chip-v:empty { display: none; } }
+ @media (max-width: 860px) { .openvibe-navbar .ovnav-burger, .openvibe-navbar .ovnav-featured-phone { display: inline-flex; } .openvibe-navbar .openvibe-navbar-links, .openvibe-navbar .openvibe-navbar-spacer { display: none; } .openvibe-navbar .openvibe-navbar-right { margin-left: auto; } .openvibe-navbar .ovnav-chip .ovnav-ic + .ovnav-chip-v:empty { display: none; } }
@media (max-width: 420px) { .openvibe-navbar .ovnav-chip { padding: 0 8px; height: 30px; font-size: 12px; } }
@media (max-width: 420px) { .openvibe-navbar .ovnav-chip { padding: 0 8px; height: 30px; font-size: 12px; } }
/* A narrow phone never draws the site's name over the bar's buttons (owner, 2026-10-28: on openvibe.games, signed
/* A narrow phone never draws the site's name over the bar's buttons (owner, 2026-10-28: on openvibe.games, signed
in, the name ran under the launcher and the magnifier). The name gives way with an ellipsis, and up to 440px
in, the name ran under the launcher and the magnifier). The name gives way with an ellipsis, and up to 440px
@@ -179,7 +184,7 @@
@@ -179,7 +184,7 @@
tightens instead of folding to one. */
tightens instead of folding to one. */
@media (max-width: 440px) { .ovnav-chip-mini { padding: 0 5px; gap: 3px; } .openvibe-navbar .ovnav-chip.ovnav-chip--all { font-size: 12px; } .ovnav-chip-mini .ovnav-ic { width: 13px; height: 13px; } }
@media (max-width: 440px) { .ovnav-chip-mini { padding: 0 5px; gap: 3px; } .openvibe-navbar .ovnav-chip.ovnav-chip--all { font-size: 12px; } .ovnav-chip-mini .ovnav-ic { width: 13px; height: 13px; } }
@media (max-width: 400px) { .openvibe-navbar .openvibe-navbar-right { gap: 1px; } .ovnav-chip-mini { padding: 0 3px; gap: 2px; } .openvibe-navbar .ovnav-chip.ovnav-chip--all { font-size: 11.5px; letter-spacing: -.2px; } .openvibe-navbar .openvibe-navbar-brand { gap: 4px; }
@media (max-width: 400px) { .openvibe-navbar .openvibe-navbar-right { gap: 1px; } .ovnav-chip-mini { padding: 0 3px; gap: 2px; } .openvibe-navbar .ovnav-chip.ovnav-chip--all { font-size: 11.5px; letter-spacing: -.2px; } .openvibe-navbar .openvibe-navbar-brand { gap: 4px; }
- .openvibe-navbar { padding-left: 8px; padding-right: 6px; gap: 2px; } .openvibe-navbar .ovnav-burger { width: 34px; } .openvibe-navbar .openvibe-navbar-avatar { width: 32px; height: 32px; box-sizing: border-box; }
+ .openvibe-navbar { padding-left: 8px; padding-right: 6px; gap: 2px; } .openvibe-navbar .ovnav-burger { width: 34px; } .openvibe-navbar .ovnav-featured-phone { width: 34px; height: 34px; } .openvibe-navbar .openvibe-navbar-avatar { width: 32px; height: 32px; box-sizing: border-box; }
.openvibe-navbar .openvibe-navbar-brand .name { font-size: 16.5px; letter-spacing: -.25px; } .openvibe-navbar .openvibe-navbar-brand .flame { min-width: 28px; height: 28px; padding: 0 2px; } .openvibe-navbar .openvibe-navbar-brand .flame .ov-mark { width: 28px !important; } }
.openvibe-navbar .openvibe-navbar-brand .name { font-size: 16.5px; letter-spacing: -.25px; } .openvibe-navbar .openvibe-navbar-brand .flame { min-width: 28px; height: 28px; padding: 0 2px; } .openvibe-navbar .openvibe-navbar-brand .flame .ov-mark { width: 28px !important; } }
/* Under 350px the mark alone names the site, so both balances keep their icons (a number alone says nothing). */
/* Under 350px the mark alone names the site, so both balances keep their icons (a number alone says nothing). */
@media (max-width: 350px) { .openvibe-navbar .openvibe-navbar-brand .name { display: none; } }
@media (max-width: 350px) { .openvibe-navbar .openvibe-navbar-brand .name { display: none; } }
@@ -1259,6 +1264,12 @@
@@ -1259,6 +1264,12 @@
const menu = kids.map(k => `<a href="${escapeAttr(k.href || '#')}" class="ovnav-link ovnav-sublink"${k.id ? ` data-link-id="${escapeAttr(k.id)}"` : ''}${k.hidden ? ' hidden' : ''}>${k.icon ? `<span class="icon">${navIcon(k.icon)}</span>` : ''}<span class="ovnav-l">${escapeAttr(k.label)}</span></a>`).join('');
const menu = kids.map(k => `<a href="${escapeAttr(k.href || '#')}" class="ovnav-link ovnav-sublink"${k.id ? ` data-link-id="${escapeAttr(k.id)}"` : ''}${k.hidden ? ' hidden' : ''}>${k.icon ? `<span class="icon">${navIcon(k.icon)}</span>` : ''}<span class="ovnav-l">${escapeAttr(k.label)}</span></a>`).join('');
return inDrawer ? a + menu : `<div class="ovnav-dd"${l.hidden ? ' hidden' : ''}>${a}<div class="ovnav-dd-menu">${menu}</div></div>`;
return inDrawer ? a + menu : `<div class="ovnav-dd"${l.hidden ? ' hidden' : ''}>${a}<div class="ovnav-dd-menu">${menu}</div></div>`;
}
}
+ /** 4.119.0: a featured link with an icon keeps a button on the phone's bar. */
+ function featuredPhoneHTML(links) {
+ const f = (links || []).find(l => l && l.featured && !l.hidden && l.icon);
+ if (!f) return '';
+ return `<a class="ovnav-featured-phone${f.active ? ' active' : ''}" href="${escapeAttr(f.href || '#')}" aria-label="${escapeAttr(f.label)}" title="${escapeAttr(f.label)}"${f.active ? ' aria-current="page"' : ''}>${navIcon(f.icon)}</a>`;
+ }
const flatLinks = (list) => list.reduce((out, l) => out.concat([l], Array.isArray(l.children) ? l.children : []), []);
const flatLinks = (list) => list.reduce((out, l) => out.concat([l], Array.isArray(l.children) ? l.children : []), []);
/** The drawer's order: a featured link leads the menu (the phone's first entry), the rest keep theirs. */
/** The drawer's order: a featured link leads the menu (the phone's first entry), the rest keep theirs. */
@@ -1857,6 +1868,7 @@
@@ -1857,6 +1868,7 @@
<div class="openvibe-navbar-right">
<div class="openvibe-navbar-right">
${u ? chipsHTML(_config.chips) + chipsAllHTML(_config.chips) : ''}
${u ? chipsHTML(_config.chips) + chipsAllHTML(_config.chips) : ''}
${u && !isAnon && _config.messages ? messagesHTML() : ''}
${u && !isAnon && _config.messages ? messagesHTML() : ''}
+ ${featuredPhoneHTML(links)}
<div id="openvibe-bell-mount"></div>
<div id="openvibe-bell-mount"></div>
${u ? avatarImg(u, 64, 'openvibe-navbar-avatar', 'openvibe-avatar-btn') :
${u ? avatarImg(u, 64, 'openvibe-navbar-avatar', 'openvibe-avatar-btn') :
`<a class="openvibe-navbar-login" id="openvibe-login-btn" href="${escapeAttr(loginHref)}">Sign In</a>`}
`<a class="openvibe-navbar-login" id="openvibe-login-btn" href="${escapeAttr(loginHref)}">Sign In</a>`}
@@ -1,12 +1,12 @@
@@ -1,12 +1,12 @@
{
{
"name": "openvibe-shared",
"name": "openvibe-shared",
- "version": "4.118.0",
+ "version": "4.119.0",
"lockfileVersion": 3,
"lockfileVersion": 3,
"requires": true,
"requires": true,
"packages": {
"packages": {
"": {
"": {
"name": "openvibe-shared",
"name": "openvibe-shared",
- "version": "4.118.0",
+ "version": "4.119.0",
"license": "MIT",
"license": "MIT",
"dependencies": {
"dependencies": {
"jsonwebtoken": "^9.0.0",
"jsonwebtoken": "^9.0.0",
@@ -1,6 +1,6 @@
@@ -1,6 +1,6 @@
{
{
"name": "openvibe-shared",
"name": "openvibe-shared",
- "version": "4.118.0",
+ "version": "4.119.0",
"description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site",
"description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site",
"main": "index.js",
"main": "index.js",
"bin": {
"bin": {
@@ -13,7 +13,7 @@ const { brotli } = require('../scripts/size-report');
@@ -13,7 +13,7 @@ const { brotli } = require('../scripts/size-report');
const ROOT = path.join(__dirname, '..');
const ROOT = path.join(__dirname, '..');
const NAVBAR_PATH = path.join(ROOT, 'navbar.js');
const NAVBAR_PATH = path.join(ROOT, 'navbar.js');
const ICONS = require('../nav-icons');
const ICONS = require('../nav-icons');
-const NAVBAR_BROTLI_BUDGET = 42 * 1024; // 41.4 KB with the page-move code (4.70.0: busy(), the mark on plain links, prefetch rules on unboosted sites); was 41 KB with the Dock's loader (4.51.0)
+const NAVBAR_BROTLI_BUDGET = 42.5 * 1024; // 42.1 KB with the featured link's phone button (4.119.0, 2026-11-06); 41.4 KB with the page-move code (4.70.0: busy(), the mark on plain links, prefetch rules on unboosted sites); was 41 KB with the Dock's loader (4.51.0)
// ── size budget and generated block ──────────────────────────
// ── size budget and generated block ──────────────────────────
{
{
@@ -51,6 +51,10 @@ const between = (html, a, b) => { const i = html.indexOf(a); const j = html.inde
@@ -51,6 +51,10 @@ const between = (html, a, b) => { const i = html.indexOf(a); const j = html.inde
assert.ok(!/ovnav-link--featured[^}]*animation/.test(bar), 'the pill does not animate');
assert.ok(!/ovnav-link--featured[^}]*animation/.test(bar), 'the pill does not animate');
const drawer = between(barHtml(created), 'id="openvibe-drawer"', 'ovnav-drawer-net');
const drawer = between(barHtml(created), 'id="openvibe-drawer"', 'ovnav-drawer-net');
assert.ok(drawer.indexOf('>Node<') >= 0 && drawer.indexOf('>Node<') < drawer.indexOf('>My Account<'), 'on a phone the Node is the first entry of the menu');
assert.ok(drawer.indexOf('>Node<') >= 0 && drawer.indexOf('>Node<') < drawer.indexOf('>My Account<'), 'on a phone the Node is the first entry of the menu');
+ // 4.119.0: on a phone the links row folds away, so the featured link keeps an icon button on the bar itself.
+ const right = between(barHtml(created), 'openvibe-navbar-right', 'openvibe-bell-mount');
+ assert.ok(/<a class="ovnav-featured-phone" href="https:\/\/node\.openvibe\.network" aria-label="Node"/.test(right), 'the phone bar has the Node button, named for readers');
+ assert.strictEqual((barHtml(created).match(/ovnav-featured-phone"/g) || []).length, 1, 'one phone button');
}
}
{
{
// Without the option, nothing changes: an ordinary link, in the order it was given.
// Without the option, nothing changes: an ordinary link, in the order it was given.
@@ -63,6 +67,7 @@ const between = (html, a, b) => { const i = html.indexOf(a); const j = html.inde
@@ -63,6 +67,7 @@ const between = (html, a, b) => { const i = html.indexOf(a); const j = html.inde
assert.ok(!bar.includes('ovnav-link--featured'), 'a plain link is not a pill');
assert.ok(!bar.includes('ovnav-link--featured'), 'a plain link is not a pill');
const drawer = between(barHtml(created), 'id="openvibe-drawer"', 'ovnav-drawer-net');
const drawer = between(barHtml(created), 'id="openvibe-drawer"', 'ovnav-drawer-net');
assert.ok(drawer.indexOf('>My Account<') < drawer.indexOf('>Node<'), 'and it keeps its place in the menu');
assert.ok(drawer.indexOf('>My Account<') < drawer.indexOf('>Node<'), 'and it keeps its place in the menu');
+ assert.ok(!barHtml(created).includes('ovnav-featured-phone"'), 'no phone button without a featured link');
}
}
{
{
// The stylesheet carries the pill: one hairline, one tint, the glyph, and nothing that moves.
// The stylesheet carries the pill: one hairline, one tint, the glyph, and nothing that moves.