openvibers/openvibe.shared Commits
Shared 4.1.3: shell.page() holds the navbar's place; the updates log no longer jumps while it loads (#70)
Changes
@@@ -4,8 +4,12 @@ All notable changes to `openvibe-shared`. Versions follow [semver](https://semvebreaking 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).--## Unreleased++## 4.1.3 — 2026-10-12++- **`shell.page()` holds the navbar's place.** The navbar script is deferred and mounts into the page's `#navbar-mount`;++ on a slow first paint the 52 px bar arrived after it and pushed the page down (CLS 0.058 on many homes in a phone++ sweep). The head now reserves `#navbar-mount{min-height:52px}`, and a `<noscript>` style removes the empty mount++ without JavaScript.- **The updates log no longer jumps while it loads.** `shipped.js` log showed "Loading…" inside the days, then the sitechips and the patch notes arrived above it and pushed it down (CLS 0.17 on openvibe.live/updates on a phone). Thefirst load now shows one standalone "Loading…" line, a site's own chips are drawn before the data, and the chips,@@@ -1,6 +1,6 @@{"name": "openvibe-shared",-- "version": "4.1.2",++ "version": "4.1.3","description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site","main": "index.js","bin": {@@@ -113,6 +113,10 @@ function manifestLinks(o) {return `<link rel="manifest" href="${esc(manifest)}">\n<link rel="apple-touch-icon" href="${esc(touch)}">`;}++// The shared navbar's script is deferred and mounts into the page's #navbar-mount: the mount holds the bar's 52px from++// the first paint so the page does not drop when it lands (layout shift), and without JavaScript it goes away.++const NAVBAR_RESERVE = '<style>#navbar-mount{min-height:52px}</style><noscript><style>#navbar-mount{display:none}</style></noscript>';++function page(o = {}) {const name = o.name || o.service || 'OpenVibe';const title = o.title || name;@@@ -136,6 +140,7 @@ function page(o = {}) {summary ? summary.head : '',o.head || '',manifestLinks(o),++ NAVBAR_RESERVE,scripts(o),'</head>',`<body${attrs(bodyAttributes)}>`,@@@ -26,6 +26,9 @@ assert.match(html, /<noscript><nav aria-label="Site"/, 'no-JS nav present');assert.match(html, /<footer id="ov-footer"/, 'shared footer present');assert.ok(html.includes('<main id="hero">Hello</main>'), 'body kept verbatim');assert.ok(html.includes('<script src="/shared/theme-loader.js?v='), 'theme-loader via serve.url');++// The deferred navbar lands in a #navbar-mount that already holds its height; without JavaScript the mount goes away.++assert.ok(html.includes('<style>#navbar-mount{min-height:52px}</style><noscript><style>#navbar-mount{display:none}</style></noscript>'), 'navbar mount reserved');++assert.ok(html.indexOf('#navbar-mount{min-height:52px}') < html.indexOf('</head>'), 'in the head, before the first paint');// ── Script order: theme-loader first (eager); the runtime after it (deferred) ─────────────────────const scripts = shell.scripts({ name: 'example' });
@@ -4,8 +4,12 @@ All notable changes to `openvibe-shared`. Versions follow [semver](https://semve
@@ -4,8 +4,12 @@ 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.
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).
A release is the git tag `vX.Y.Z`; consumers pin the tag's tarball (see README).
-## Unreleased
+## 4.1.3 — 2026-10-12
+- **`shell.page()` holds the navbar's place.** The navbar script is deferred and mounts into the page's `#navbar-mount`;
+ on a slow first paint the 52 px bar arrived after it and pushed the page down (CLS 0.058 on many homes in a phone
+ sweep). The head now reserves `#navbar-mount{min-height:52px}`, and a `<noscript>` style removes the empty mount
+ without JavaScript.
- **The updates log no longer jumps while it loads.** `shipped.js` log showed "Loading…" inside the days, then the site
- **The updates log no longer jumps while it loads.** `shipped.js` log showed "Loading…" inside the days, then the site
chips and the patch notes arrived above it and pushed it down (CLS 0.17 on openvibe.live/updates on a phone). The
chips and the patch notes arrived above it and pushed it down (CLS 0.17 on openvibe.live/updates on a phone). The
first load now shows one standalone "Loading…" line, a site's own chips are drawn before the data, and the chips,
first load now shows one standalone "Loading…" line, a site's own chips are drawn before the data, and the chips,
@@ -1,6 +1,6 @@
@@ -1,6 +1,6 @@
{
{
"name": "openvibe-shared",
"name": "openvibe-shared",
- "version": "4.1.2",
+ "version": "4.1.3",
"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": {
@@ -113,6 +113,10 @@ function manifestLinks(o) {
@@ -113,6 +113,10 @@ function manifestLinks(o) {
return `<link rel="manifest" href="${esc(manifest)}">\n<link rel="apple-touch-icon" href="${esc(touch)}">`;
return `<link rel="manifest" href="${esc(manifest)}">\n<link rel="apple-touch-icon" href="${esc(touch)}">`;
}
}
+// The shared navbar's script is deferred and mounts into the page's #navbar-mount: the mount holds the bar's 52px from
+// the first paint so the page does not drop when it lands (layout shift), and without JavaScript it goes away.
+const NAVBAR_RESERVE = '<style>#navbar-mount{min-height:52px}</style><noscript><style>#navbar-mount{display:none}</style></noscript>';
+
function page(o = {}) {
function page(o = {}) {
const name = o.name || o.service || 'OpenVibe';
const name = o.name || o.service || 'OpenVibe';
const title = o.title || name;
const title = o.title || name;
@@ -136,6 +140,7 @@ function page(o = {}) {
@@ -136,6 +140,7 @@ function page(o = {}) {
summary ? summary.head : '',
summary ? summary.head : '',
o.head || '',
o.head || '',
manifestLinks(o),
manifestLinks(o),
+ NAVBAR_RESERVE,
scripts(o),
scripts(o),
'</head>',
'</head>',
`<body${attrs(bodyAttributes)}>`,
`<body${attrs(bodyAttributes)}>`,
@@ -26,6 +26,9 @@ assert.match(html, /<noscript><nav aria-label="Site"/, 'no-JS nav present');
@@ -26,6 +26,9 @@ assert.match(html, /<noscript><nav aria-label="Site"/, 'no-JS nav present');
assert.match(html, /<footer id="ov-footer"/, 'shared footer present');
assert.match(html, /<footer id="ov-footer"/, 'shared footer present');
assert.ok(html.includes('<main id="hero">Hello</main>'), 'body kept verbatim');
assert.ok(html.includes('<main id="hero">Hello</main>'), 'body kept verbatim');
assert.ok(html.includes('<script src="/shared/theme-loader.js?v='), 'theme-loader via serve.url');
assert.ok(html.includes('<script src="/shared/theme-loader.js?v='), 'theme-loader via serve.url');
+// The deferred navbar lands in a #navbar-mount that already holds its height; without JavaScript the mount goes away.
+assert.ok(html.includes('<style>#navbar-mount{min-height:52px}</style><noscript><style>#navbar-mount{display:none}</style></noscript>'), 'navbar mount reserved');
+assert.ok(html.indexOf('#navbar-mount{min-height:52px}') < html.indexOf('</head>'), 'in the head, before the first paint');
// ── Script order: theme-loader first (eager); the runtime after it (deferred) ─────────────────────
// ── Script order: theme-loader first (eager); the runtime after it (deferred) ─────────────────────
const scripts = shell.scripts({ name: 'example' });
const scripts = shell.scripts({ name: 'example' });