openvibers/openvibe.shared Commits
4.157.0: every shell page has a tab icon (#349)
Changes
@@@ -1,5 +1,13 @@# Changelog++## 4.157.0 — 2026-11-09++++- **Every shell page has a tab icon.** `shell.page()` adds the service's OpenVibe mark (`openvibe-shared/app-icon`++ `favicon`, the same tint as its app icons) as an inline SVG `<link rel="icon">` when the page's `head` has none.++ Without one the browser asked `/favicon.ico`, which most sites answer 404, and the tab showed a blank page icon++ (11-09: openvibe.computer, openvibe.email and openvibe.studio). A page with its own icon keeps it; `favicon: false`++ opts out.++## 4.156.0 — 2026-11-08- **Locker unlocks with a passkey on this device.** Setting up a locker can use a passkey instead of a passphrase:@@@ -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.156.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed++**Status:** alpha, v4.157.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.156.0",++ "version": "4.157.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)",@@@ -1,12 +1,12 @@{"name": "openvibe-shared",-- "version": "4.156.0",++ "version": "4.157.0","lockfileVersion": 3,"requires": true,"packages": {"": {"name": "openvibe-shared",-- "version": "4.156.0",++ "version": "4.157.0","license": "MIT","dependencies": {"jsonwebtoken": "^9.0.0",@@@ -1,6 +1,6 @@{"name": "openvibe-shared",-- "version": "4.156.0",++ "version": "4.157.0","description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site","main": "index.js","bin": {@@@ -84,6 +84,8 @@ function scripts(o = {}) {* head extra <head> markup appended verbatim (already trusted/escaped)* manifest the installable web app links (manifest + apple-touch-icon) are added when the* page's service has an icon set; `false` opts out++ * favicon the tab icon: the service's OpenVibe mark (openvibe-shared/app-icon favicon) as an inline++ * SVG, added unless `head` already has a rel=icon link; `false` opts out (4.157.0)* body the page's main content (trusted markup — the caller escaped it)* bodyAttributes attributes for <body>, escaped; bodyClass is shorthand for `class`* navLinks, home the `frame.noscriptNav` no-JavaScript navigation@@@ -124,6 +126,19 @@ function manifestLinks(o) {return `<link rel="manifest" href="${esc(manifest)}">\n<link rel="apple-touch-icon" href="${esc(touch)}">`;}++// The tab icon (4.157.0): without one the browser asks /favicon.ico, which most sites answer 404, and the tab shows a++// blank page icon (11-09: Computer, Email and Studio, all on this shell). The service's mark, the same tint as its app++// icons; a page whose `head` already has an icon keeps its own.++function faviconLink(o) {++ if (o.favicon === false) return '';++ if (typeof o.head === 'string' && /rel=["']?(?:shortcut )?icon\b/i.test(o.head)) return '';++ const service = siteId(o) || 'network';++ const known = appIcons.site(service);++ const variant = String((known && known.icon) || service).replace(/[^a-z0-9-]/g, '');++ const uri = 'data:image/svg+xml,' + encodeURIComponent(require('./app-icon').favicon({ site: variant })).replace(/'/g, '%27');++ return `<link rel="icon" type="image/svg+xml" data-ov-icon="${esc(variant)}" href="${uri}">`;++}++// 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. The body// has no browser margin, so the Frame's navbar and footer meet the window's edges on a page with no stylesheet of its own@@@ -156,6 +171,7 @@ function page(o = {}) {o.agentLayer && o.canonical ? require('./agent-layer').linkHeaderTags(o.canonical, o.llms).head : '',summary ? summary.head : '',o.head || '',++ faviconLink(o),manifestLinks(o),NAVBAR_RESERVE,scripts(o),@@@ -93,4 +93,17 @@ assert.ok(withSummary.indexOf('</head>') < withSummary.indexOf('<noscript><sectiassert.match(shellLib.page({ name: 'X', navbar: { service: 'games' }, body: '' }), /data-ov-service="games"/);assert.doesNotMatch(shellLib.page({ name: 'OpenVibe.Design', body: '' }), /data-ov-service/, 'a display name is not an id');}++// ── The tab icon (4.157.0): the service's mark unless the page brings its own; `favicon: false` opts out ──++{++ const withIcon = shell.page({ name: 'OpenVibe.Email', service: 'mail', body: '' });++ const icons = withIcon.match(/<link rel="icon"[^>]*>/g) || [];++ assert.equal(icons.length, 1, 'one tab icon');++ assert.match(icons[0], /type="image\/svg\+xml" data-ov-icon="[a-z0-9-]+" href="data:image\/svg\+xml,%3Csvg/, 'an inline SVG of the mark');++ assert.ok(!/['"<>]/.test(icons[0].slice(icons[0].indexOf('href="') + 6, -2)), 'the data URI is fully encoded');++ assert.ok(withIcon.indexOf(icons[0]) < withIcon.indexOf('</head>'), 'in the head');++ const own = shell.page({ name: 'X', service: 'mail', head: '<link rel="icon" href="/icon.svg">', body: '' });++ assert.equal((own.match(/rel="icon"/g) || []).length, 1, 'a page with its own icon keeps only that one');++ assert.doesNotMatch(shell.page({ name: 'X', service: 'mail', favicon: false, body: '' }), /rel="icon"/, 'favicon: false opts out');++ assert.match(shell.page({ name: 'Plain', body: '' }), /data-ov-icon="network"/, 'no service: the network mark');++}console.log('shell: all checks passed');
@@ -1,5 +1,13 @@
@@ -1,5 +1,13 @@
# Changelog
# Changelog
+## 4.157.0 — 2026-11-09
+
+- **Every shell page has a tab icon.** `shell.page()` adds the service's OpenVibe mark (`openvibe-shared/app-icon`
+ `favicon`, the same tint as its app icons) as an inline SVG `<link rel="icon">` when the page's `head` has none.
+ Without one the browser asked `/favicon.ico`, which most sites answer 404, and the tab showed a blank page icon
+ (11-09: openvibe.computer, openvibe.email and openvibe.studio). A page with its own icon keeps it; `favicon: false`
+ opts out.
+
## 4.156.0 — 2026-11-08
## 4.156.0 — 2026-11-08
- **Locker unlocks with a passkey on this device.** Setting up a locker can use a passkey instead of a passphrase:
- **Locker unlocks with a passkey on this device.** Setting up a locker can use a passkey instead of a passphrase:
@@ -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.156.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed
+**Status:** alpha, v4.157.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.156.0",
+ "version": "4.157.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)",
@@ -1,12 +1,12 @@
@@ -1,12 +1,12 @@
{
{
"name": "openvibe-shared",
"name": "openvibe-shared",
- "version": "4.156.0",
+ "version": "4.157.0",
"lockfileVersion": 3,
"lockfileVersion": 3,
"requires": true,
"requires": true,
"packages": {
"packages": {
"": {
"": {
"name": "openvibe-shared",
"name": "openvibe-shared",
- "version": "4.156.0",
+ "version": "4.157.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.156.0",
+ "version": "4.157.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": {
@@ -84,6 +84,8 @@ function scripts(o = {}) {
@@ -84,6 +84,8 @@ function scripts(o = {}) {
* head extra <head> markup appended verbatim (already trusted/escaped)
* head extra <head> markup appended verbatim (already trusted/escaped)
* manifest the installable web app links (manifest + apple-touch-icon) are added when the
* manifest the installable web app links (manifest + apple-touch-icon) are added when the
* page's service has an icon set; `false` opts out
* page's service has an icon set; `false` opts out
+ * favicon the tab icon: the service's OpenVibe mark (openvibe-shared/app-icon favicon) as an inline
+ * SVG, added unless `head` already has a rel=icon link; `false` opts out (4.157.0)
* body the page's main content (trusted markup — the caller escaped it)
* body the page's main content (trusted markup — the caller escaped it)
* bodyAttributes attributes for <body>, escaped; bodyClass is shorthand for `class`
* bodyAttributes attributes for <body>, escaped; bodyClass is shorthand for `class`
* navLinks, home the `frame.noscriptNav` no-JavaScript navigation
* navLinks, home the `frame.noscriptNav` no-JavaScript navigation
@@ -124,6 +126,19 @@ function manifestLinks(o) {
@@ -124,6 +126,19 @@ 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 tab icon (4.157.0): without one the browser asks /favicon.ico, which most sites answer 404, and the tab shows a
+// blank page icon (11-09: Computer, Email and Studio, all on this shell). The service's mark, the same tint as its app
+// icons; a page whose `head` already has an icon keeps its own.
+function faviconLink(o) {
+ if (o.favicon === false) return '';
+ if (typeof o.head === 'string' && /rel=["']?(?:shortcut )?icon\b/i.test(o.head)) return '';
+ const service = siteId(o) || 'network';
+ const known = appIcons.site(service);
+ const variant = String((known && known.icon) || service).replace(/[^a-z0-9-]/g, '');
+ const uri = 'data:image/svg+xml,' + encodeURIComponent(require('./app-icon').favicon({ site: variant })).replace(/'/g, '%27');
+ return `<link rel="icon" type="image/svg+xml" data-ov-icon="${esc(variant)}" href="${uri}">`;
+}
+
// The shared navbar's script is deferred and mounts into the page's #navbar-mount: the mount holds the bar's 52px from
// 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. The body
// the first paint so the page does not drop when it lands (layout shift), and without JavaScript it goes away. The body
// has no browser margin, so the Frame's navbar and footer meet the window's edges on a page with no stylesheet of its own
// has no browser margin, so the Frame's navbar and footer meet the window's edges on a page with no stylesheet of its own
@@ -156,6 +171,7 @@ function page(o = {}) {
@@ -156,6 +171,7 @@ function page(o = {}) {
o.agentLayer && o.canonical ? require('./agent-layer').linkHeaderTags(o.canonical, o.llms).head : '',
o.agentLayer && o.canonical ? require('./agent-layer').linkHeaderTags(o.canonical, o.llms).head : '',
summary ? summary.head : '',
summary ? summary.head : '',
o.head || '',
o.head || '',
+ faviconLink(o),
manifestLinks(o),
manifestLinks(o),
NAVBAR_RESERVE,
NAVBAR_RESERVE,
scripts(o),
scripts(o),
@@ -93,4 +93,17 @@ assert.ok(withSummary.indexOf('</head>') < withSummary.indexOf('<noscript><secti
@@ -93,4 +93,17 @@ assert.ok(withSummary.indexOf('</head>') < withSummary.indexOf('<noscript><secti
assert.match(shellLib.page({ name: 'X', navbar: { service: 'games' }, body: '' }), /data-ov-service="games"/);
assert.match(shellLib.page({ name: 'X', navbar: { service: 'games' }, body: '' }), /data-ov-service="games"/);
assert.doesNotMatch(shellLib.page({ name: 'OpenVibe.Design', body: '' }), /data-ov-service/, 'a display name is not an id');
assert.doesNotMatch(shellLib.page({ name: 'OpenVibe.Design', body: '' }), /data-ov-service/, 'a display name is not an id');
}
}
+// ── The tab icon (4.157.0): the service's mark unless the page brings its own; `favicon: false` opts out ──
+{
+ const withIcon = shell.page({ name: 'OpenVibe.Email', service: 'mail', body: '' });
+ const icons = withIcon.match(/<link rel="icon"[^>]*>/g) || [];
+ assert.equal(icons.length, 1, 'one tab icon');
+ assert.match(icons[0], /type="image\/svg\+xml" data-ov-icon="[a-z0-9-]+" href="data:image\/svg\+xml,%3Csvg/, 'an inline SVG of the mark');
+ assert.ok(!/['"<>]/.test(icons[0].slice(icons[0].indexOf('href="') + 6, -2)), 'the data URI is fully encoded');
+ assert.ok(withIcon.indexOf(icons[0]) < withIcon.indexOf('</head>'), 'in the head');
+ const own = shell.page({ name: 'X', service: 'mail', head: '<link rel="icon" href="/icon.svg">', body: '' });
+ assert.equal((own.match(/rel="icon"/g) || []).length, 1, 'a page with its own icon keeps only that one');
+ assert.doesNotMatch(shell.page({ name: 'X', service: 'mail', favicon: false, body: '' }), /rel="icon"/, 'favicon: false opts out');
+ assert.match(shell.page({ name: 'Plain', body: '' }), /data-ov-icon="network"/, 'no service: the network mark');
+}
console.log('shell: all checks passed');
console.log('shell: all checks passed');