openvibers/openvibe.shared Commits
Shared 1.30.1: the first in-site move animates (first page = first enter() at route generation ≤ 1), route-transition.js preloads after load, the loading line lives outside the section (re-renders cannot remove it), requested by path so the versions map hashes it. Measured on Live at 400 ms latency: bar, 'Loading Content…' → 'starting up', fade-in, never unstyled. npm test 42/42.
Changes
@@@ -4,6 +4,10 @@ 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).++## 1.30.1 — 2026-09-28++++Page moves, measured on Live with a 400 ms network: the bar runs from the click, "Loading Content…" appears at 150 ms and follows the phase ("starting up"), the styled page fades in, and nothing shows unstyled in between. Fixes to 1.30.0: the first move after a server-rendered page is animated (the "first page" is the first `enter()` before any route change, route generation ≤ 1, not simply the first call); `route-transition.js` is fetched a few seconds after load, so it is ready for the first click; the loading line sits over the page's visible area but outside the section (a page script that re-renders its section while loading can no longer take it away); the file is requested by path, so the site's versions map hashes it.++## 1.30.0 — 2026-09-28**Pages never show unstyled, and moving between them looks finished.** `web-runtime.js`: a feature's stylesheets (its dependencies' too, `rt.stylesOf(name)`) start downloading together with its markup fragment, and the fragment is inserted only once they have loaded (or timed out), so a route no longer flashes unstyled markup for a moment. New `rt.enter(section, work, { label })`: the page's content stays hidden while its route loads, keeping its space (no layout jump), and is shown when the work settles, failure included. On the first in-site move the runtime fetches the new **`route-transition.js`** from next to itself (a browser file in `files.js`, export `openvibe-shared/route-transition`): a progress bar across the top, after 150 ms a centred "Loading Content…" line with the current phase under it (`<prefix>:phase` events: "applying styles", "starting up", or the site's own `rt.status(text)`), then an opacity-only fade-in (a transform would make the page the containing block of its fixed children). Theme tokens colour it; reduced motion gets none of the animation. The first page of a visit is never animated (it arrived server-rendered). The runtime's usage notes moved to the README ("The web runtime"), keeping it at 6.2 KB brotli (budget 6.5); the transition layer is 1.6 KB and loads only when someone moves.@@@ -4,7 +4,7 @@"code": true,"wave": "W2","package": "openvibe-shared",-- "version": "1.30.0",++ "version": "1.30.1","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)",@@@ -1,6 +1,6 @@{"name": "openvibe-shared",-- "version": "1.30.0",++ "version": "1.30.1","description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site","main": "index.js","exports": {@@@ -12,7 +12,7 @@(function (root) {'use strict';if (typeof document === 'undefined' || root.OVRouteFx) return;-- const css = `.ovrt-wait{position:absolute;inset:0;display:grid;place-content:center;justify-items:center;gap:12px;padding:24px;color:var(--text-muted,#8a96a8);font:600 .84rem/1.35 system-ui,sans-serif;text-align:center;animation:ovrtIn .25s ease both}++ const css = `.ovrt-wait{position:fixed;z-index:40;pointer-events:none;display:grid;place-content:center;justify-items:center;gap:12px;padding:24px;color:var(--text-muted,#8a96a8);font:600 .84rem/1.35 system-ui,sans-serif;text-align:center;animation:ovrtIn .25s ease both}.ovrt-wait i{display:block;width:min(200px,50vw);height:3px;border-radius:3px;overflow:hidden;position:relative;background:color-mix(in srgb,var(--accent,#3b82f6) 18%,transparent)}.ovrt-wait i::after{content:'';position:absolute;inset:0;width:38%;border-radius:3px;background:var(--accent,#3b82f6);animation:ovrtSlide 1.05s cubic-bezier(.45,0,.2,1) infinite}.ovrt-wait small{font-weight:500;font-size:.76rem;opacity:.85;min-height:1.2em;transition:opacity .2s}@@@ -51,7 +51,11 @@wait.setAttribute('role', 'status');wait.innerHTML = '<i></i><span></span><small></small>';wait.querySelector('span').textContent = `Loading ${label || 'page'}…`;-- section.appendChild(wait);++ // Over the page's visible area, but outside it: a page script that re-renders its section while the++ // route loads cannot take the line away.++ const r = section.getBoundingClientRect(), top = Math.max(r.top, 0);++ Object.assign(wait.style, { left: `${r.left}px`, width: `${r.width}px`, top: `${top}px`, height: `${Math.max(160, Math.min(r.bottom, root.innerHeight) - top)}px` });++ document.body.appendChild(wait);}, 150);return {status(text) { if (wait) wait.querySelector('small').textContent = String(text || ''); },@@@ -135,7 +135,7 @@ const ROUTES = [{ path: '^/player', features: ['player'] }, { path: '^/ops', feaawait done;assert.strictEqual(section.dataset.fragmentLoaded, '1');assert.ok(!section.hasAttribute('data-ov-loading'), 'shown once ready');-- assert.ok(p.document.querySelector('style').textContent.includes('[data-ov-loading]>:not(.ovrt-wait){visibility:hidden}'), 'keeps its space while hidden');++ assert.ok(p.document.querySelector('style').textContent.includes('[data-ov-loading]>*{visibility:hidden}'), 'keeps its space while hidden');assert.ok(phases.includes('player:styles') && phases.includes('player:code'), 'phases for the loading line');await assert.rejects(rt.enter(section, Promise.reject(new Error('x'))), /x/);assert.ok(!section.hasAttribute('data-ov-loading'), 'a failed route is not left hidden');@@@ -192,10 +192,13 @@let enters = 0, fx = null;// Its own URL, without web-runtime's ?v= (the site's versions map hashes it, as any asset).const fxSrc = selfSrc && assetKey(selfSrc).replace(/web-runtime\.js$/, 'route-transition.js');++ // Ready before the first click: fetched a few seconds after the page settles.++ if (fxSrc) root.setTimeout(() => { if (!root.OVRouteFx) loadScript(fxSrc).catch(() => {}); }, 4000);function enter(section, work, opts = {}) {-- const p = Promise.resolve(work), attr = `data-${prefix}-loading`, first = !enters++;++ // The page that arrived server-rendered: the first enter() before any in-site move (route generation ≤ 1).++ const p = Promise.resolve(work), attr = `data-${prefix}-loading`, first = !enters++ && generation <= 1;if (!section) return p;-- if (first) { const st = document.createElement('style'); st.textContent = `[${attr}]{position:relative;min-height:40vh}[${attr}]>:not(.ovrt-wait){visibility:hidden}`; document.head.appendChild(st); }++ if (!document.getElementById(`${prefix}-rt-css`)) { const st = document.createElement('style'); st.id = `${prefix}-rt-css`; st.textContent = `[${attr}]{position:relative;min-height:40vh}[${attr}]>*{visibility:hidden}`; document.head.appendChild(st); }section.setAttribute(attr, '');let h = null;if (!first && fxSrc) (root.OVRouteFx ? p.constructor.resolve() : loadScript(fxSrc)).then(() => { if (section.hasAttribute(attr)) fx = h = root.OVRouteFx.start(section, { label: opts.label, prefix }); }, () => {});
@@ -4,6 +4,10 @@ All notable changes to `openvibe-shared`. Versions follow [semver](https://semve
@@ -4,6 +4,10 @@ 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).
+## 1.30.1 — 2026-09-28
+
+Page moves, measured on Live with a 400 ms network: the bar runs from the click, "Loading Content…" appears at 150 ms and follows the phase ("starting up"), the styled page fades in, and nothing shows unstyled in between. Fixes to 1.30.0: the first move after a server-rendered page is animated (the "first page" is the first `enter()` before any route change, route generation ≤ 1, not simply the first call); `route-transition.js` is fetched a few seconds after load, so it is ready for the first click; the loading line sits over the page's visible area but outside the section (a page script that re-renders its section while loading can no longer take it away); the file is requested by path, so the site's versions map hashes it.
+
## 1.30.0 — 2026-09-28
## 1.30.0 — 2026-09-28
**Pages never show unstyled, and moving between them looks finished.** `web-runtime.js`: a feature's stylesheets (its dependencies' too, `rt.stylesOf(name)`) start downloading together with its markup fragment, and the fragment is inserted only once they have loaded (or timed out), so a route no longer flashes unstyled markup for a moment. New `rt.enter(section, work, { label })`: the page's content stays hidden while its route loads, keeping its space (no layout jump), and is shown when the work settles, failure included. On the first in-site move the runtime fetches the new **`route-transition.js`** from next to itself (a browser file in `files.js`, export `openvibe-shared/route-transition`): a progress bar across the top, after 150 ms a centred "Loading Content…" line with the current phase under it (`<prefix>:phase` events: "applying styles", "starting up", or the site's own `rt.status(text)`), then an opacity-only fade-in (a transform would make the page the containing block of its fixed children). Theme tokens colour it; reduced motion gets none of the animation. The first page of a visit is never animated (it arrived server-rendered). The runtime's usage notes moved to the README ("The web runtime"), keeping it at 6.2 KB brotli (budget 6.5); the transition layer is 1.6 KB and loads only when someone moves.
**Pages never show unstyled, and moving between them looks finished.** `web-runtime.js`: a feature's stylesheets (its dependencies' too, `rt.stylesOf(name)`) start downloading together with its markup fragment, and the fragment is inserted only once they have loaded (or timed out), so a route no longer flashes unstyled markup for a moment. New `rt.enter(section, work, { label })`: the page's content stays hidden while its route loads, keeping its space (no layout jump), and is shown when the work settles, failure included. On the first in-site move the runtime fetches the new **`route-transition.js`** from next to itself (a browser file in `files.js`, export `openvibe-shared/route-transition`): a progress bar across the top, after 150 ms a centred "Loading Content…" line with the current phase under it (`<prefix>:phase` events: "applying styles", "starting up", or the site's own `rt.status(text)`), then an opacity-only fade-in (a transform would make the page the containing block of its fixed children). Theme tokens colour it; reduced motion gets none of the animation. The first page of a visit is never animated (it arrived server-rendered). The runtime's usage notes moved to the README ("The web runtime"), keeping it at 6.2 KB brotli (budget 6.5); the transition layer is 1.6 KB and loads only when someone moves.
@@ -4,7 +4,7 @@
@@ -4,7 +4,7 @@
"code": true,
"code": true,
"wave": "W2",
"wave": "W2",
"package": "openvibe-shared",
"package": "openvibe-shared",
- "version": "1.30.0",
+ "version": "1.30.1",
"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)",
@@ -1,6 +1,6 @@
@@ -1,6 +1,6 @@
{
{
"name": "openvibe-shared",
"name": "openvibe-shared",
- "version": "1.30.0",
+ "version": "1.30.1",
"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",
"exports": {
"exports": {
@@ -12,7 +12,7 @@
@@ -12,7 +12,7 @@
(function (root) {
(function (root) {
'use strict';
'use strict';
if (typeof document === 'undefined' || root.OVRouteFx) return;
if (typeof document === 'undefined' || root.OVRouteFx) return;
- const css = `.ovrt-wait{position:absolute;inset:0;display:grid;place-content:center;justify-items:center;gap:12px;padding:24px;color:var(--text-muted,#8a96a8);font:600 .84rem/1.35 system-ui,sans-serif;text-align:center;animation:ovrtIn .25s ease both}
+ const css = `.ovrt-wait{position:fixed;z-index:40;pointer-events:none;display:grid;place-content:center;justify-items:center;gap:12px;padding:24px;color:var(--text-muted,#8a96a8);font:600 .84rem/1.35 system-ui,sans-serif;text-align:center;animation:ovrtIn .25s ease both}
.ovrt-wait i{display:block;width:min(200px,50vw);height:3px;border-radius:3px;overflow:hidden;position:relative;background:color-mix(in srgb,var(--accent,#3b82f6) 18%,transparent)}
.ovrt-wait i{display:block;width:min(200px,50vw);height:3px;border-radius:3px;overflow:hidden;position:relative;background:color-mix(in srgb,var(--accent,#3b82f6) 18%,transparent)}
.ovrt-wait i::after{content:'';position:absolute;inset:0;width:38%;border-radius:3px;background:var(--accent,#3b82f6);animation:ovrtSlide 1.05s cubic-bezier(.45,0,.2,1) infinite}
.ovrt-wait i::after{content:'';position:absolute;inset:0;width:38%;border-radius:3px;background:var(--accent,#3b82f6);animation:ovrtSlide 1.05s cubic-bezier(.45,0,.2,1) infinite}
.ovrt-wait small{font-weight:500;font-size:.76rem;opacity:.85;min-height:1.2em;transition:opacity .2s}
.ovrt-wait small{font-weight:500;font-size:.76rem;opacity:.85;min-height:1.2em;transition:opacity .2s}
@@ -51,7 +51,11 @@
@@ -51,7 +51,11 @@
wait.setAttribute('role', 'status');
wait.setAttribute('role', 'status');
wait.innerHTML = '<i></i><span></span><small></small>';
wait.innerHTML = '<i></i><span></span><small></small>';
wait.querySelector('span').textContent = `Loading ${label || 'page'}…`;
wait.querySelector('span').textContent = `Loading ${label || 'page'}…`;
- section.appendChild(wait);
+ // Over the page's visible area, but outside it: a page script that re-renders its section while the
+ // route loads cannot take the line away.
+ const r = section.getBoundingClientRect(), top = Math.max(r.top, 0);
+ Object.assign(wait.style, { left: `${r.left}px`, width: `${r.width}px`, top: `${top}px`, height: `${Math.max(160, Math.min(r.bottom, root.innerHeight) - top)}px` });
+ document.body.appendChild(wait);
}, 150);
}, 150);
return {
return {
status(text) { if (wait) wait.querySelector('small').textContent = String(text || ''); },
status(text) { if (wait) wait.querySelector('small').textContent = String(text || ''); },
@@ -135,7 +135,7 @@ const ROUTES = [{ path: '^/player', features: ['player'] }, { path: '^/ops', fea
@@ -135,7 +135,7 @@ const ROUTES = [{ path: '^/player', features: ['player'] }, { path: '^/ops', fea
await done;
await done;
assert.strictEqual(section.dataset.fragmentLoaded, '1');
assert.strictEqual(section.dataset.fragmentLoaded, '1');
assert.ok(!section.hasAttribute('data-ov-loading'), 'shown once ready');
assert.ok(!section.hasAttribute('data-ov-loading'), 'shown once ready');
- assert.ok(p.document.querySelector('style').textContent.includes('[data-ov-loading]>:not(.ovrt-wait){visibility:hidden}'), 'keeps its space while hidden');
+ assert.ok(p.document.querySelector('style').textContent.includes('[data-ov-loading]>*{visibility:hidden}'), 'keeps its space while hidden');
assert.ok(phases.includes('player:styles') && phases.includes('player:code'), 'phases for the loading line');
assert.ok(phases.includes('player:styles') && phases.includes('player:code'), 'phases for the loading line');
await assert.rejects(rt.enter(section, Promise.reject(new Error('x'))), /x/);
await assert.rejects(rt.enter(section, Promise.reject(new Error('x'))), /x/);
assert.ok(!section.hasAttribute('data-ov-loading'), 'a failed route is not left hidden');
assert.ok(!section.hasAttribute('data-ov-loading'), 'a failed route is not left hidden');
@@ -192,10 +192,13 @@
@@ -192,10 +192,13 @@
let enters = 0, fx = null;
let enters = 0, fx = null;
// Its own URL, without web-runtime's ?v= (the site's versions map hashes it, as any asset).
// Its own URL, without web-runtime's ?v= (the site's versions map hashes it, as any asset).
const fxSrc = selfSrc && assetKey(selfSrc).replace(/web-runtime\.js$/, 'route-transition.js');
const fxSrc = selfSrc && assetKey(selfSrc).replace(/web-runtime\.js$/, 'route-transition.js');
+ // Ready before the first click: fetched a few seconds after the page settles.
+ if (fxSrc) root.setTimeout(() => { if (!root.OVRouteFx) loadScript(fxSrc).catch(() => {}); }, 4000);
function enter(section, work, opts = {}) {
function enter(section, work, opts = {}) {
- const p = Promise.resolve(work), attr = `data-${prefix}-loading`, first = !enters++;
+ // The page that arrived server-rendered: the first enter() before any in-site move (route generation ≤ 1).
+ const p = Promise.resolve(work), attr = `data-${prefix}-loading`, first = !enters++ && generation <= 1;
if (!section) return p;
if (!section) return p;
- if (first) { const st = document.createElement('style'); st.textContent = `[${attr}]{position:relative;min-height:40vh}[${attr}]>:not(.ovrt-wait){visibility:hidden}`; document.head.appendChild(st); }
+ if (!document.getElementById(`${prefix}-rt-css`)) { const st = document.createElement('style'); st.id = `${prefix}-rt-css`; st.textContent = `[${attr}]{position:relative;min-height:40vh}[${attr}]>*{visibility:hidden}`; document.head.appendChild(st); }
section.setAttribute(attr, '');
section.setAttribute(attr, '');
let h = null;
let h = null;
if (!first && fxSrc) (root.OVRouteFx ? p.constructor.resolve() : loadScript(fxSrc)).then(() => { if (section.hasAttribute(attr)) fx = h = root.OVRouteFx.start(section, { label: opts.label, prefix }); }, () => {});
if (!first && fxSrc) (root.OVRouteFx ? p.constructor.resolve() : loadScript(fxSrc)).then(() => { if (section.hasAttribute(attr)) fx = h = root.OVRouteFx.start(section, { label: opts.label, prefix }); }, () => {});