openvibers/openvibe.shared Commits
4.92.0: the page starts the footer it renders; the compact footer centres its links (#248)
Changes
@@@ -4,6 +4,17 @@ 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).++## 4.92.0 — 2026-11-02++++- **The page starts the footer it renders** (`shell.page`, `shell.scripts`).++ - Six sites rendered the shared footer and never started it: Video, Pics, Run, Events, AI and Website. On a phone++ every fold stood open, and there was no shipped line.++ - The page's boot now starts it a moment after the site's own scripts, with the page's footer config.++ - A site that starts it itself (`OpenVibeFooter.init`) keeps its own: a started footer carries `data-ovf-live`, and++ the shell leaves it.++- **The compact footer centres its links** under its centred brand. On a phone the row sat at the left, and "Report a++ problem" wrapped alone.++## 4.91.1 — 2026-11-01- **A numbers strip can be reached by keyboard** (`kit.stats`): on a phone the strip scrolls sideways, and a strip with@@@ -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.91.1 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed++**Status:** alpha, v4.92.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.91.1",++ "version": "4.92.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)",@@@ -409,6 +409,7 @@padding-inline:clamp(0px, calc(84px - (100vw - 1240px) / 2), 84px)}.ovf-copy{color:var(--text-muted,#8b93ad);font-size:.78rem}.ovf-legal-inline,.ovf-compact-links{display:flex;flex-wrap:wrap;gap:14px}++.ovf-compact-links{justify-content:center}.ovf [hidden],.ovf-account[hidden]{display:none!important}.ovf .ovf-shipped.ov-shipped-latest{font-size:.76rem;padding:4px 12px;max-width:min(520px,100%);background:transparent}.ovf-legal-inline a,.ovf-compact-links a{color:var(--text-muted,#8b93ad);font-size:.78rem;text-decoration:none;transition:color .15s}@@@ -598,6 +599,8 @@const el = typeof _cfg.mount === 'string' ? document.querySelector(_cfg.mount) : _cfg.mount;if (!el) return null;_mounted = el;++ // Started: shell.page's own start (after the site's scripts) leaves a footer the site started alone.++ try { el.setAttribute('data-ovf-live', ''); } catch { /* */ }render();// 'auto' on a single-page site follows its routes: back and forward re-read the path (a route the site pushes// itself calls setVariant or render).@@@ -1,6 +1,6 @@{"name": "openvibe-shared",-- "version": "4.91.1",++ "version": "4.92.0","description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site","main": "index.js","bin": {@@@ -58,6 +58,7 @@ function attrs(map) {function scripts(o = {}) {const name = o.name || o.service || 'OpenVibe';const navbar = { service: name, ...(o.navbar || {}) };++ const footer = o.footer || { service: name, variant: 'compact' };const boot = { features: o.features || {}, routes: o.routes || [] };if (o.versions) boot.versions = o.versions;return [@@@ -65,7 +66,9 @@ function scripts(o = {}) {`<script src="${esc(serve.url('web-runtime.js'))}" defer></script>`,`<script src="${esc(serve.url('navbar.js'))}" defer></script>`,`<script src="${esc(serve.url('footer.js'))}" defer></script>`,-- `<script>window.addEventListener('DOMContentLoaded', function () { OpenVibeNavbar.init(${json(navbar)}); OVWebRuntime.create(${json(boot)}).boot(); });</script>`,++ // The footer this page renders (frame.footer) is started here too, a moment after the site's own scripts: a site++ // that starts it itself (OpenVibeFooter.init) keeps its own; one that does not gets its phone folds and its line.++ `<script>window.addEventListener('DOMContentLoaded', function () { OpenVibeNavbar.init(${json(navbar)}); OVWebRuntime.create(${json(boot)}).boot(); setTimeout(function () { var f = document.getElementById('ov-footer'); if (window.OpenVibeFooter && f && !f.hasAttribute('data-ovf-live')) OpenVibeFooter.init(${json(footer)}); }, 0); });</script>`,].join('\n');}@@@ -5,6 +5,8 @@// escaped, the page summary appears only when asked for, and the no-JS nav + shared footer are there.const assert = require('node:assert/strict');const shell = require('openvibe-shared/shell');++const fs = require('fs');++const path = require('path');// ── A whole document, in order ────────────────────────────────────────────────────────────────────const html = shell.page({@@@ -61,6 +63,13 @@ assert.deepEqual(JSON.parse(m[1]), {versions: { '/js/chat.js': 'abc123' },});assert.match(bootSrc, /OpenVibeNavbar\.init\(\{"service":"example"\}\);/, 'navbar init carries the service');++// The footer it renders is started too, unless the site started it (Video and five more never did: 2026-11-02).++assert.match(bootSrc, /setTimeout\(function \(\) \{ var f = document\.getElementById\('ov-footer'\); if \(window\.OpenVibeFooter && f && !f\.hasAttribute\('data-ovf-live'\)\) OpenVibeFooter\.init\(\{"service":"example","variant":"compact"\}\); \}, 0\);/, 'the page starts its footer after the site\'s own scripts');++const withFooter = shell.scripts({ name: 'v', footer: { service: 'media-hub', variant: 'full', brandName: 'OpenVibe.Video' } });++assert.ok(withFooter.includes('OpenVibeFooter.init({"service":"media-hub","variant":"full","brandName":"OpenVibe.Video"})'), 'with the page\'s own footer config');++const footerSrc = fs.readFileSync(path.join(__dirname, '..', 'footer.js'), 'utf8');++assert.ok(/_mounted = el;\s*\/\/[^\n]*\n\s*try \{ el\.setAttribute\('data-ovf-live', ''\); \}/.test(footerSrc), 'a started footer says so');++assert.ok(footerSrc.includes('.ovf-compact-links{justify-content:center}'), 'the compact row centres its links under the brand');const evil = shell.page({ name: 'x', features: { a: '</script><script>alert(1)</script>' } });assert.ok(!evil.includes('</script><script>alert(1)'), 'a feature name cannot break out of the boot script');assert.ok(evil.includes('\\u003c/script>'), 'the boot escapes <');
@@ -4,6 +4,17 @@ All notable changes to `openvibe-shared`. Versions follow [semver](https://semve
@@ -4,6 +4,17 @@ 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).
+## 4.92.0 — 2026-11-02
+
+- **The page starts the footer it renders** (`shell.page`, `shell.scripts`).
+ - Six sites rendered the shared footer and never started it: Video, Pics, Run, Events, AI and Website. On a phone
+ every fold stood open, and there was no shipped line.
+ - The page's boot now starts it a moment after the site's own scripts, with the page's footer config.
+ - A site that starts it itself (`OpenVibeFooter.init`) keeps its own: a started footer carries `data-ovf-live`, and
+ the shell leaves it.
+- **The compact footer centres its links** under its centred brand. On a phone the row sat at the left, and "Report a
+ problem" wrapped alone.
+
## 4.91.1 — 2026-11-01
## 4.91.1 — 2026-11-01
- **A numbers strip can be reached by keyboard** (`kit.stats`): on a phone the strip scrolls sideways, and a strip with
- **A numbers strip can be reached by keyboard** (`kit.stats`): on a phone the strip scrolls sideways, and a strip with
@@ -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.91.1 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed
+**Status:** alpha, v4.92.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.91.1",
+ "version": "4.92.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)",
@@ -409,6 +409,7 @@
@@ -409,6 +409,7 @@
padding-inline:clamp(0px, calc(84px - (100vw - 1240px) / 2), 84px)}
padding-inline:clamp(0px, calc(84px - (100vw - 1240px) / 2), 84px)}
.ovf-copy{color:var(--text-muted,#8b93ad);font-size:.78rem}
.ovf-copy{color:var(--text-muted,#8b93ad);font-size:.78rem}
.ovf-legal-inline,.ovf-compact-links{display:flex;flex-wrap:wrap;gap:14px}
.ovf-legal-inline,.ovf-compact-links{display:flex;flex-wrap:wrap;gap:14px}
+.ovf-compact-links{justify-content:center}
.ovf [hidden],.ovf-account[hidden]{display:none!important}
.ovf [hidden],.ovf-account[hidden]{display:none!important}
.ovf .ovf-shipped.ov-shipped-latest{font-size:.76rem;padding:4px 12px;max-width:min(520px,100%);background:transparent}
.ovf .ovf-shipped.ov-shipped-latest{font-size:.76rem;padding:4px 12px;max-width:min(520px,100%);background:transparent}
.ovf-legal-inline a,.ovf-compact-links a{color:var(--text-muted,#8b93ad);font-size:.78rem;text-decoration:none;transition:color .15s}
.ovf-legal-inline a,.ovf-compact-links a{color:var(--text-muted,#8b93ad);font-size:.78rem;text-decoration:none;transition:color .15s}
@@ -598,6 +599,8 @@
@@ -598,6 +599,8 @@
const el = typeof _cfg.mount === 'string' ? document.querySelector(_cfg.mount) : _cfg.mount;
const el = typeof _cfg.mount === 'string' ? document.querySelector(_cfg.mount) : _cfg.mount;
if (!el) return null;
if (!el) return null;
_mounted = el;
_mounted = el;
+ // Started: shell.page's own start (after the site's scripts) leaves a footer the site started alone.
+ try { el.setAttribute('data-ovf-live', ''); } catch { /* */ }
render();
render();
// 'auto' on a single-page site follows its routes: back and forward re-read the path (a route the site pushes
// 'auto' on a single-page site follows its routes: back and forward re-read the path (a route the site pushes
// itself calls setVariant or render).
// itself calls setVariant or render).
@@ -1,6 +1,6 @@
@@ -1,6 +1,6 @@
{
{
"name": "openvibe-shared",
"name": "openvibe-shared",
- "version": "4.91.1",
+ "version": "4.92.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": {
@@ -58,6 +58,7 @@ function attrs(map) {
@@ -58,6 +58,7 @@ function attrs(map) {
function scripts(o = {}) {
function scripts(o = {}) {
const name = o.name || o.service || 'OpenVibe';
const name = o.name || o.service || 'OpenVibe';
const navbar = { service: name, ...(o.navbar || {}) };
const navbar = { service: name, ...(o.navbar || {}) };
+ const footer = o.footer || { service: name, variant: 'compact' };
const boot = { features: o.features || {}, routes: o.routes || [] };
const boot = { features: o.features || {}, routes: o.routes || [] };
if (o.versions) boot.versions = o.versions;
if (o.versions) boot.versions = o.versions;
return [
return [
@@ -65,7 +66,9 @@ function scripts(o = {}) {
@@ -65,7 +66,9 @@ function scripts(o = {}) {
`<script src="${esc(serve.url('web-runtime.js'))}" defer></script>`,
`<script src="${esc(serve.url('web-runtime.js'))}" defer></script>`,
`<script src="${esc(serve.url('navbar.js'))}" defer></script>`,
`<script src="${esc(serve.url('navbar.js'))}" defer></script>`,
`<script src="${esc(serve.url('footer.js'))}" defer></script>`,
`<script src="${esc(serve.url('footer.js'))}" defer></script>`,
- `<script>window.addEventListener('DOMContentLoaded', function () { OpenVibeNavbar.init(${json(navbar)}); OVWebRuntime.create(${json(boot)}).boot(); });</script>`,
+ // The footer this page renders (frame.footer) is started here too, a moment after the site's own scripts: a site
+ // that starts it itself (OpenVibeFooter.init) keeps its own; one that does not gets its phone folds and its line.
+ `<script>window.addEventListener('DOMContentLoaded', function () { OpenVibeNavbar.init(${json(navbar)}); OVWebRuntime.create(${json(boot)}).boot(); setTimeout(function () { var f = document.getElementById('ov-footer'); if (window.OpenVibeFooter && f && !f.hasAttribute('data-ovf-live')) OpenVibeFooter.init(${json(footer)}); }, 0); });</script>`,
].join('\n');
].join('\n');
}
}
@@ -5,6 +5,8 @@
@@ -5,6 +5,8 @@
// escaped, the page summary appears only when asked for, and the no-JS nav + shared footer are there.
// escaped, the page summary appears only when asked for, and the no-JS nav + shared footer are there.
const assert = require('node:assert/strict');
const assert = require('node:assert/strict');
const shell = require('openvibe-shared/shell');
const shell = require('openvibe-shared/shell');
+const fs = require('fs');
+const path = require('path');
// ── A whole document, in order ────────────────────────────────────────────────────────────────────
// ── A whole document, in order ────────────────────────────────────────────────────────────────────
const html = shell.page({
const html = shell.page({
@@ -61,6 +63,13 @@ assert.deepEqual(JSON.parse(m[1]), {
@@ -61,6 +63,13 @@ assert.deepEqual(JSON.parse(m[1]), {
versions: { '/js/chat.js': 'abc123' },
versions: { '/js/chat.js': 'abc123' },
});
});
assert.match(bootSrc, /OpenVibeNavbar\.init\(\{"service":"example"\}\);/, 'navbar init carries the service');
assert.match(bootSrc, /OpenVibeNavbar\.init\(\{"service":"example"\}\);/, 'navbar init carries the service');
+// The footer it renders is started too, unless the site started it (Video and five more never did: 2026-11-02).
+assert.match(bootSrc, /setTimeout\(function \(\) \{ var f = document\.getElementById\('ov-footer'\); if \(window\.OpenVibeFooter && f && !f\.hasAttribute\('data-ovf-live'\)\) OpenVibeFooter\.init\(\{"service":"example","variant":"compact"\}\); \}, 0\);/, 'the page starts its footer after the site\'s own scripts');
+const withFooter = shell.scripts({ name: 'v', footer: { service: 'media-hub', variant: 'full', brandName: 'OpenVibe.Video' } });
+assert.ok(withFooter.includes('OpenVibeFooter.init({"service":"media-hub","variant":"full","brandName":"OpenVibe.Video"})'), 'with the page\'s own footer config');
+const footerSrc = fs.readFileSync(path.join(__dirname, '..', 'footer.js'), 'utf8');
+assert.ok(/_mounted = el;\s*\/\/[^\n]*\n\s*try \{ el\.setAttribute\('data-ovf-live', ''\); \}/.test(footerSrc), 'a started footer says so');
+assert.ok(footerSrc.includes('.ovf-compact-links{justify-content:center}'), 'the compact row centres its links under the brand');
const evil = shell.page({ name: 'x', features: { a: '</script><script>alert(1)</script>' } });
const evil = shell.page({ name: 'x', features: { a: '</script><script>alert(1)</script>' } });
assert.ok(!evil.includes('</script><script>alert(1)'), 'a feature name cannot break out of the boot script');
assert.ok(!evil.includes('</script><script>alert(1)'), 'a feature name cannot break out of the boot script');
assert.ok(evil.includes('\\u003c/script>'), 'the boot escapes <');
assert.ok(evil.includes('\\u003c/script>'), 'the boot escapes <');