openvibers/openvibe.shared Commits
4.19.0: footer variant 'auto': the full footer on a site's front page (and fullOn), the one quiet line everywhere else, on the server (ssr path) and in the browser (#142)
Changes
@@@ -6,6 +6,18 @@ A release is the git tag `vX.Y.Z`; consumers pin the tag's tarball (see README).## Unreleased++## 4.19.0 — 2026-10-28++++- **Footer `variant: 'auto'`** (`footer.js`). The owner, 2026-10-28: "the full-size footer on the chat pages might be a++ little much … if we want to have that app-like design … make it a little more sleeker across sites".++ - It shows the full footer on the site's front page (and the paths in `fullOn`, default `['/']`), and the one quiet++ line (`compact`, with "More from OpenVibe" to open the rest) everywhere else.++ - On the server, `ssr({ variant: 'auto', path })` picks from the request's path; with no path it is compact.++ - In the browser it reads `location.pathname`, follows back and forward on a single-page site, and++ `setVariant('auto')` is a variant like the others.++ - A claim still wins over it (Live's channel while live).++ - `autoFor(path, fullOn)` is exported.++## 4.18.0 — 2026-10-28- **The account menu, reviewed** (`navbar.js`). The owner, 2026-10-28: "duplicated things … could just be organized@@@ -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.18.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed++**Status:** alpha, v4.19.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.18.0",++ "version": "4.19.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)",@@@ -2,7 +2,7 @@* openvibe-shared/footer.js — the cross-site footer, the counterpart to navbar.js.** OpenVibeFooter.init({ service, variant, links, apiBase, mount, pulse })-- * OpenVibeFooter.setVariant('full' | 'compact') the page's own variant (SPA route changes)++ * OpenVibeFooter.setVariant('full' | 'compact' | 'auto') the page's own variant (SPA route changes)* OpenVibeFooter.claim('compact' | 'full', key) → release(): a part of the page asks for a variant while it needs* it (Live's channel while its stream is live: compact; offline:* the page's own full). Compact wins over full, any claim over the@@@ -16,6 +16,10 @@* property or changing a policy link updates every site at once.** Variants:++ * auto full on the site's front page (and the paths in fullOn), compact everywhere else: a site looks like an app++ * once someone is in it (the owner, 2026-10-28: "the full-size footer on the chat pages might be a little++ * much … if we want to have that app-like design … make it a little more sleeker across sites"). On the++ * server, ssr({ variant: 'auto', path }) picks with the request's path (no path: compact).* full the multi-column footer, for home and browse pages* compact one quiet line, for pages where the footer is not the point — a streamer's* channel, a player, a tool someone is mid-task in. "More from OpenVibe" opens the@@@ -126,7 +130,8 @@service: null, // 'live' | 'tools' | 'dev' | … (auto-detected when omitted)brandName: null,tagline: 'Open source and community run. Built in the open by the people using it.',-- variant: 'full',++ variant: 'full', // 'full' | 'compact' | 'auto'++ fullOn: ['/'], // with variant 'auto': the paths that get the full footermount: '#ov-footer',links: [], // [{ heading, items: [{ name, url, onclick? }] }]legalBase: null, // origin that hosts /tos, /privacy, /dmca for this site@@@ -143,11 +148,19 @@let _cfg = { ...DEFAULTS };let _mounted = null;const _claims = new Map(); // key → 'compact' | 'full' (claim())-- /** The variant shown: a compact claim, else a full one, else the page's own. */++ /** 'auto' for a path: full on the front page and the paths in fullOn, compact elsewhere (no path: compact). */++ function autoFor(path, fullOn) {++ if (typeof path !== 'string' || !path) return 'compact';++ const p = path.replace(/[?#].*$/, '').replace(/(.)\/+$/, '$1') || '/';++ return (Array.isArray(fullOn) && fullOn.length ? fullOn : ['/']).includes(p) ? 'full' : 'compact';++ }++ const herePath = () => (typeof location !== 'undefined' && location.pathname) || '/';++ /** The variant shown: a compact claim, else a full one, else the page's own ('auto' read from where the page is). */function shownVariant(base) {const asked = [..._claims.values()];if (asked.includes('compact')) return 'compact';if (asked.includes('full')) return 'full';++ if (base === 'auto') return autoFor(herePath(), _cfg.fullOn);return base === 'compact' ? 'compact' : 'full';}@@@ -549,12 +562,19 @@if (!el) return null;_mounted = el;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).++ if (_cfg.variant === 'auto' && !_routeWatch && typeof window !== 'undefined' && window.addEventListener) {++ _routeWatch = true;++ window.addEventListener('popstate', () => { if (_cfg.variant === 'auto' && _mounted) render(); });++ }return el;}++ let _routeWatch = false;/** The page's own variant (an SPA route's); a claim still wins while it holds. */function setVariant(variant) {-- if (variant !== 'full' && variant !== 'compact') return;++ if (variant !== 'full' && variant !== 'compact' && variant !== 'auto') return;if (_cfg.variant === variant) return;const before = shownVariant(_cfg.variant);_cfg.variant = variant;@@@ -587,9 +607,10 @@*/function ssr(cfg = {}) {const c = { ...DEFAULTS, ...cfg };++ if (c.variant === 'auto') c.variant = autoFor(cfg.path, c.fullOn); // the request's path, on the serverconst id = String(cfg.mountId || 'ov-footer').replace(/[^\w-]/g, '');return `<style id="ovf-css">${CSS}</style><footer id="${id}" class="ovf" data-variant="${c.variant === 'compact' ? 'compact' : 'full'}">${buildHTML(c)}</footer>`;}-- return { init, ssr, setVariant, claim, buildHTML, render, CSS, NETWORK, LEGAL, detectService, pulseHTML, get config() { return { ..._cfg }; }, get variant() { return shownVariant(_cfg.variant); } };++ return { init, ssr, setVariant, claim, buildHTML, render, CSS, NETWORK, LEGAL, detectService, pulseHTML, autoFor, get config() { return { ..._cfg }; }, get variant() { return shownVariant(_cfg.variant); } };});@@@ -1,6 +1,6 @@{"name": "openvibe-shared",-- "version": "4.18.0",++ "version": "4.19.0","description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site","main": "index.js","bin": {nnew file mode 100644@@@ -0,0 +1,42 @@++'use strict';++// The footer's 'auto' variant (footer.js; the owner, 2026-10-28: "the full-size footer on the chat pages might be a++// little much … if we want to have that app-like design … make it a little more sleeker across sites"): the full footer++// on a site's front page (and the paths it names in fullOn), the one quiet line everywhere else, on the server as in the++// browser, and a claim still wins.++const assert = require('assert');++const footer = require('../footer');++++assert.strictEqual(footer.autoFor('/', ['/']), 'full');++assert.strictEqual(footer.autoFor('/?ref=x', ['/']), 'full', 'a query is not another page');++assert.strictEqual(footer.autoFor('/r/staff/tasks/1', ['/']), 'compact');++assert.strictEqual(footer.autoFor('/updates/', ['/', '/updates']), 'full', 'a trailing slash is the same path');++assert.strictEqual(footer.autoFor('/updates', undefined), 'compact', 'fullOn defaults to the front page');++assert.strictEqual(footer.autoFor(undefined, ['/']), 'compact', 'no path on the server: compact');++++const ssr = (cfg) => (/class="ovf" data-variant="(\w+)"/.exec(footer.ssr({ service: 'chat', ...cfg })) || [])[1];++assert.strictEqual(ssr({ variant: 'auto', path: '/' }), 'full');++assert.strictEqual(ssr({ variant: 'auto', path: '/messages/12' }), 'compact');++assert.strictEqual(ssr({ variant: 'auto', path: '/developers', fullOn: ['/', '/developers'] }), 'full');++assert.strictEqual(ssr({ variant: 'full', path: '/messages/12' }), 'full', 'full stays full');++assert.strictEqual(ssr({ variant: 'compact', path: '/' }), 'compact', 'compact stays compact');++++// In the browser: the page's path decides, a claim wins over it, and setVariant('auto') is a variant like the others++// (unmounted here: the variant is what a render would draw).++global.location = { pathname: '/', hostname: 'openvibe.chat' };++footer.setVariant('auto');++assert.strictEqual(footer.variant, 'full', 'the front page');++global.location.pathname = '/r/staff';++assert.strictEqual(footer.variant, 'compact', 'a room');++const release = footer.claim('full', 'test');++assert.strictEqual(footer.variant, 'full', 'a claim wins');++release();++assert.strictEqual(footer.variant, 'compact');++footer.setVariant('full');++assert.strictEqual(footer.variant, 'full');++footer.setVariant('nonsense');++assert.strictEqual(footer.variant, 'full', 'an unknown variant changes nothing');++footer.setVariant('auto');++assert.strictEqual(footer.variant, 'compact');++delete global.location;++++console.log('footer-auto: all checks passed');
@@ -6,6 +6,18 @@ A release is the git tag `vX.Y.Z`; consumers pin the tag's tarball (see README).
@@ -6,6 +6,18 @@ A release is the git tag `vX.Y.Z`; consumers pin the tag's tarball (see README).
## Unreleased
## Unreleased
+## 4.19.0 — 2026-10-28
+
+- **Footer `variant: 'auto'`** (`footer.js`). The owner, 2026-10-28: "the full-size footer on the chat pages might be a
+ little much … if we want to have that app-like design … make it a little more sleeker across sites".
+ - It shows the full footer on the site's front page (and the paths in `fullOn`, default `['/']`), and the one quiet
+ line (`compact`, with "More from OpenVibe" to open the rest) everywhere else.
+ - On the server, `ssr({ variant: 'auto', path })` picks from the request's path; with no path it is compact.
+ - In the browser it reads `location.pathname`, follows back and forward on a single-page site, and
+ `setVariant('auto')` is a variant like the others.
+ - A claim still wins over it (Live's channel while live).
+ - `autoFor(path, fullOn)` is exported.
+
## 4.18.0 — 2026-10-28
## 4.18.0 — 2026-10-28
- **The account menu, reviewed** (`navbar.js`). The owner, 2026-10-28: "duplicated things … could just be organized
- **The account menu, reviewed** (`navbar.js`). The owner, 2026-10-28: "duplicated things … could just be organized
@@ -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.18.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed
+**Status:** alpha, v4.19.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.18.0",
+ "version": "4.19.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)",
@@ -2,7 +2,7 @@
@@ -2,7 +2,7 @@
* openvibe-shared/footer.js — the cross-site footer, the counterpart to navbar.js.
* openvibe-shared/footer.js — the cross-site footer, the counterpart to navbar.js.
*
*
* OpenVibeFooter.init({ service, variant, links, apiBase, mount, pulse })
* OpenVibeFooter.init({ service, variant, links, apiBase, mount, pulse })
- * OpenVibeFooter.setVariant('full' | 'compact') the page's own variant (SPA route changes)
+ * OpenVibeFooter.setVariant('full' | 'compact' | 'auto') the page's own variant (SPA route changes)
* OpenVibeFooter.claim('compact' | 'full', key) → release(): a part of the page asks for a variant while it needs
* OpenVibeFooter.claim('compact' | 'full', key) → release(): a part of the page asks for a variant while it needs
* it (Live's channel while its stream is live: compact; offline:
* it (Live's channel while its stream is live: compact; offline:
* the page's own full). Compact wins over full, any claim over the
* the page's own full). Compact wins over full, any claim over the
@@ -16,6 +16,10 @@
@@ -16,6 +16,10 @@
* property or changing a policy link updates every site at once.
* property or changing a policy link updates every site at once.
*
*
* Variants:
* Variants:
+ * auto full on the site's front page (and the paths in fullOn), compact everywhere else: a site looks like an app
+ * once someone is in it (the owner, 2026-10-28: "the full-size footer on the chat pages might be a little
+ * much … if we want to have that app-like design … make it a little more sleeker across sites"). On the
+ * server, ssr({ variant: 'auto', path }) picks with the request's path (no path: compact).
* full the multi-column footer, for home and browse pages
* full the multi-column footer, for home and browse pages
* compact one quiet line, for pages where the footer is not the point — a streamer's
* compact one quiet line, for pages where the footer is not the point — a streamer's
* channel, a player, a tool someone is mid-task in. "More from OpenVibe" opens the
* channel, a player, a tool someone is mid-task in. "More from OpenVibe" opens the
@@ -126,7 +130,8 @@
@@ -126,7 +130,8 @@
service: null, // 'live' | 'tools' | 'dev' | … (auto-detected when omitted)
service: null, // 'live' | 'tools' | 'dev' | … (auto-detected when omitted)
brandName: null,
brandName: null,
tagline: 'Open source and community run. Built in the open by the people using it.',
tagline: 'Open source and community run. Built in the open by the people using it.',
- variant: 'full',
+ variant: 'full', // 'full' | 'compact' | 'auto'
+ fullOn: ['/'], // with variant 'auto': the paths that get the full footer
mount: '#ov-footer',
mount: '#ov-footer',
links: [], // [{ heading, items: [{ name, url, onclick? }] }]
links: [], // [{ heading, items: [{ name, url, onclick? }] }]
legalBase: null, // origin that hosts /tos, /privacy, /dmca for this site
legalBase: null, // origin that hosts /tos, /privacy, /dmca for this site
@@ -143,11 +148,19 @@
@@ -143,11 +148,19 @@
let _cfg = { ...DEFAULTS };
let _cfg = { ...DEFAULTS };
let _mounted = null;
let _mounted = null;
const _claims = new Map(); // key → 'compact' | 'full' (claim())
const _claims = new Map(); // key → 'compact' | 'full' (claim())
- /** The variant shown: a compact claim, else a full one, else the page's own. */
+ /** 'auto' for a path: full on the front page and the paths in fullOn, compact elsewhere (no path: compact). */
+ function autoFor(path, fullOn) {
+ if (typeof path !== 'string' || !path) return 'compact';
+ const p = path.replace(/[?#].*$/, '').replace(/(.)\/+$/, '$1') || '/';
+ return (Array.isArray(fullOn) && fullOn.length ? fullOn : ['/']).includes(p) ? 'full' : 'compact';
+ }
+ const herePath = () => (typeof location !== 'undefined' && location.pathname) || '/';
+ /** The variant shown: a compact claim, else a full one, else the page's own ('auto' read from where the page is). */
function shownVariant(base) {
function shownVariant(base) {
const asked = [..._claims.values()];
const asked = [..._claims.values()];
if (asked.includes('compact')) return 'compact';
if (asked.includes('compact')) return 'compact';
if (asked.includes('full')) return 'full';
if (asked.includes('full')) return 'full';
+ if (base === 'auto') return autoFor(herePath(), _cfg.fullOn);
return base === 'compact' ? 'compact' : 'full';
return base === 'compact' ? 'compact' : 'full';
}
}
@@ -549,12 +562,19 @@
@@ -549,12 +562,19 @@
if (!el) return null;
if (!el) return null;
_mounted = el;
_mounted = el;
render();
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).
+ if (_cfg.variant === 'auto' && !_routeWatch && typeof window !== 'undefined' && window.addEventListener) {
+ _routeWatch = true;
+ window.addEventListener('popstate', () => { if (_cfg.variant === 'auto' && _mounted) render(); });
+ }
return el;
return el;
}
}
+ let _routeWatch = false;
/** The page's own variant (an SPA route's); a claim still wins while it holds. */
/** The page's own variant (an SPA route's); a claim still wins while it holds. */
function setVariant(variant) {
function setVariant(variant) {
- if (variant !== 'full' && variant !== 'compact') return;
+ if (variant !== 'full' && variant !== 'compact' && variant !== 'auto') return;
if (_cfg.variant === variant) return;
if (_cfg.variant === variant) return;
const before = shownVariant(_cfg.variant);
const before = shownVariant(_cfg.variant);
_cfg.variant = variant;
_cfg.variant = variant;
@@ -587,9 +607,10 @@
@@ -587,9 +607,10 @@
*/
*/
function ssr(cfg = {}) {
function ssr(cfg = {}) {
const c = { ...DEFAULTS, ...cfg };
const c = { ...DEFAULTS, ...cfg };
+ if (c.variant === 'auto') c.variant = autoFor(cfg.path, c.fullOn); // the request's path, on the server
const id = String(cfg.mountId || 'ov-footer').replace(/[^\w-]/g, '');
const id = String(cfg.mountId || 'ov-footer').replace(/[^\w-]/g, '');
return `<style id="ovf-css">${CSS}</style><footer id="${id}" class="ovf" data-variant="${c.variant === 'compact' ? 'compact' : 'full'}">${buildHTML(c)}</footer>`;
return `<style id="ovf-css">${CSS}</style><footer id="${id}" class="ovf" data-variant="${c.variant === 'compact' ? 'compact' : 'full'}">${buildHTML(c)}</footer>`;
}
}
- return { init, ssr, setVariant, claim, buildHTML, render, CSS, NETWORK, LEGAL, detectService, pulseHTML, get config() { return { ..._cfg }; }, get variant() { return shownVariant(_cfg.variant); } };
+ return { init, ssr, setVariant, claim, buildHTML, render, CSS, NETWORK, LEGAL, detectService, pulseHTML, autoFor, get config() { return { ..._cfg }; }, get variant() { return shownVariant(_cfg.variant); } };
});
});
@@ -1,6 +1,6 @@
@@ -1,6 +1,6 @@
{
{
"name": "openvibe-shared",
"name": "openvibe-shared",
- "version": "4.18.0",
+ "version": "4.19.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": {
new file mode 100644
new file mode 100644
@@ -0,0 +1,42 @@
@@ -0,0 +1,42 @@
+'use strict';
+// The footer's 'auto' variant (footer.js; the owner, 2026-10-28: "the full-size footer on the chat pages might be a
+// little much … if we want to have that app-like design … make it a little more sleeker across sites"): the full footer
+// on a site's front page (and the paths it names in fullOn), the one quiet line everywhere else, on the server as in the
+// browser, and a claim still wins.
+const assert = require('assert');
+const footer = require('../footer');
+
+assert.strictEqual(footer.autoFor('/', ['/']), 'full');
+assert.strictEqual(footer.autoFor('/?ref=x', ['/']), 'full', 'a query is not another page');
+assert.strictEqual(footer.autoFor('/r/staff/tasks/1', ['/']), 'compact');
+assert.strictEqual(footer.autoFor('/updates/', ['/', '/updates']), 'full', 'a trailing slash is the same path');
+assert.strictEqual(footer.autoFor('/updates', undefined), 'compact', 'fullOn defaults to the front page');
+assert.strictEqual(footer.autoFor(undefined, ['/']), 'compact', 'no path on the server: compact');
+
+const ssr = (cfg) => (/class="ovf" data-variant="(\w+)"/.exec(footer.ssr({ service: 'chat', ...cfg })) || [])[1];
+assert.strictEqual(ssr({ variant: 'auto', path: '/' }), 'full');
+assert.strictEqual(ssr({ variant: 'auto', path: '/messages/12' }), 'compact');
+assert.strictEqual(ssr({ variant: 'auto', path: '/developers', fullOn: ['/', '/developers'] }), 'full');
+assert.strictEqual(ssr({ variant: 'full', path: '/messages/12' }), 'full', 'full stays full');
+assert.strictEqual(ssr({ variant: 'compact', path: '/' }), 'compact', 'compact stays compact');
+
+// In the browser: the page's path decides, a claim wins over it, and setVariant('auto') is a variant like the others
+// (unmounted here: the variant is what a render would draw).
+global.location = { pathname: '/', hostname: 'openvibe.chat' };
+footer.setVariant('auto');
+assert.strictEqual(footer.variant, 'full', 'the front page');
+global.location.pathname = '/r/staff';
+assert.strictEqual(footer.variant, 'compact', 'a room');
+const release = footer.claim('full', 'test');
+assert.strictEqual(footer.variant, 'full', 'a claim wins');
+release();
+assert.strictEqual(footer.variant, 'compact');
+footer.setVariant('full');
+assert.strictEqual(footer.variant, 'full');
+footer.setVariant('nonsense');
+assert.strictEqual(footer.variant, 'full', 'an unknown variant changes nothing');
+footer.setVariant('auto');
+assert.strictEqual(footer.variant, 'compact');
+delete global.location;
+
+console.log('footer-auto: all checks passed');