openvibers/openvibe.shared Commits
Shared 1.29.1: themes repaint the canvas through the variables (a page's own theme switcher changes the background too), save() applies the theme it saves, and themes without --on-accent get readable button text derived by WCAG luminance (near-black on bright accents); onColor(hex) exported. Navbar brand compact: 6px mark gap, 2px padding, side padding clamp(8px,1.6vw,16px). 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.29.1 — 2026-09-28++++**Themes repaint the whole page and keep button text readable.** `applyVars` sets the canvas through the variables (`var(--bg-primary, value)`), so a page with its own theme switcher that sets `--bg-primary` later repaints the background too, instead of leaving the first theme's colour behind; `save()` applies the theme it saves, so every token, the canvas and the browser UI follow at once. A theme without its own `--on-accent` (custom colours, older server themes) gets one derived by WCAG relative luminance, near-black on bright accents (a lime green) and white on dark ones; `OpenVibeThemeLoader.onColor(hex)` is exported for pages that paint their own accent surfaces. **Navbar brand, compact:** the mark and the name sit 6px apart with 2px padding, the ".Live" part hugs the name, and the bar's side padding follows the viewport (`clamp(8px, 1.6vw, 16px)`), so narrow screens keep more room for links.++## 1.29.0 — 2026-09-28**`openvibe-shared/analytics/pg`: request analytics on PostgreSQL** (ADR-035; ADR-021's privacy bounds unchanged). `AnalyticsTrackerPg` is the tracker on an openvibe-sdk/db handle, with the same tables and `getStats`, `getOverview` and `getBotAnalysis` shapes; `analyticsSchema()` is the DDL for the service's migrations; `pruneRawEventsPg` is the 30-day retention in batches. The request path never waits for the database: rows buffer in memory and `flush()` writes them in one transaction every 5 s (or at 100). Unique visitors stay right across processes: the day's salt is shared through `analytics_day_salts`, so the visitor hash is computed at flush, while (address, user agent) wait in memory until then, as the rate counters always did; no address reaches the database. Session ids stay per process, as they were. Every SUM is cast to bigint (numeric comes back as text). `test/analytics-pg.test.js` on PGlite: two trackers count one visitor once, DNT records nothing, rollups, dashboards and retention. Needed by Network's and Live's PostgreSQL passes.@@@ -4,7 +4,7 @@"code": true,"wave": "W2","package": "openvibe-shared",-- "version": "1.29.0",++ "version": "1.29.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)",@@@ -57,7 +57,7 @@s.textContent = `.openvibe-navbar {position: sticky; top: 0; z-index: 10000;-- height: 52px; display: flex; align-items: center; padding: 0 16px; gap: 8px;++ height: 52px; display: flex; align-items: center; padding: 0 clamp(8px, 1.6vw, 16px); gap: 6px;background: var(--bg-secondary, #252530);border-bottom: 1px solid var(--border, #333340);font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;@@@ -67,19 +67,19 @@/* Brand group: mark + linked wordmark + launcher read as one control. Every property a hostpage might set on bare "nav a" / "a" is reset here, so the brand looks the same everywhere. */.openvibe-navbar .openvibe-navbar-brand a, .openvibe-navbar .openvibe-navbar-brand a:hover { all: unset; cursor: pointer; color: inherit; font: inherit; }-- .openvibe-navbar .openvibe-navbar-brand { gap: 8px; padding: 3px 4px 3px 3px; border-radius: 12px; margin-right: 2px; }++ .openvibe-navbar .openvibe-navbar-brand { gap: 6px; padding: 2px; border-radius: 12px; margin-right: 0; }.openvibe-navbar .openvibe-navbar-brand a.flame { display: inline-grid; place-items: center; width: 30px; height: 30px; border-radius: 9px; transition: background .15s; }.openvibe-navbar .openvibe-navbar-brand a.flame:hover { background: var(--accent-glow, rgba(59,130,246,.14)); }.openvibe-navbar .openvibe-navbar-brand .name { align-items: center; gap: 0; font-size: 15.5px; letter-spacing: -.25px; }-- .openvibe-navbar .openvibe-navbar-brand .name a { padding: 3px 2px; border-radius: 6px; transition: color .15s, background .15s; }++ .openvibe-navbar .openvibe-navbar-brand .name a { padding: 3px 1px; border-radius: 6px; transition: color .15s, background .15s; }.openvibe-navbar .openvibe-navbar-brand .name a.b-sub { color: var(--text-primary, #e6edf7); font-weight: 750; }.openvibe-navbar .openvibe-navbar-brand .name a.b-core { color: var(--text-primary, #e6edf7); font-weight: 700; }.openvibe-navbar .openvibe-navbar-brand .name a.b-tld { color: var(--accent-light, var(--accent, #60a5fa)); font-weight: 700; }.openvibe-navbar .openvibe-navbar-brand.has-sub .name a.b-core, .openvibe-navbar .openvibe-navbar-brand.has-sub .name a.b-tld { color: var(--text-secondary, #a8b3c4); font-weight: 600; }.openvibe-navbar .openvibe-navbar-brand .name a:hover { color: var(--accent-light, var(--accent, #60a5fa)); background: var(--accent-glow, rgba(59,130,246,.12)); }-- .openvibe-navbar .openvibe-navbar-brand .b-dot { margin: 0; padding: 0 .5px; opacity: .55; }++ .openvibe-navbar .openvibe-navbar-brand .b-dot { margin: 0; padding: 0; opacity: .5; }.openvibe-navbar .openvibe-navbar-brand a:focus-visible, .ovnav-launch:focus-visible { outline: 2px solid var(--accent, #3b82f6); outline-offset: 1px; }-- .ovnav-launch { all: unset; box-sizing: border-box; cursor: pointer; color: var(--text-secondary, #a8b3c4); width: 32px; height: 32px; border-radius: 10px; display: inline-grid; place-items: center; flex: none; margin-right: 10px; border: 1px solid transparent; transition: background .15s, color .15s, border-color .15s; }++ .ovnav-launch { all: unset; box-sizing: border-box; cursor: pointer; color: var(--text-secondary, #a8b3c4); width: 32px; height: 32px; border-radius: 10px; display: inline-grid; place-items: center; flex: none; margin-right: 2px; border: 1px solid transparent; transition: background .15s, color .15s, border-color .15s; }.ovnav-launch:hover, .ovnav-launch[aria-expanded="true"] { background: var(--accent-glow, rgba(59,130,246,.14)); color: var(--accent-light, var(--accent, #60a5fa)); border-color: color-mix(in srgb, var(--accent, #3b82f6) 35%, transparent); }.ovnav-launch svg { display: block; transition: transform .25s cubic-bezier(.2,1.4,.3,1); }.ovnav-launch[aria-expanded="true"] svg { transform: rotate(45deg) scale(.92); }@@@ -198,7 +198,7 @@.openvibe-navbar-dropdown-header .ud-wallet a { display: inline-flex; align-items: center; gap: 5px; margin-top: 5px; padding: 2px 9px; border-radius: 999px; font-size: 12px; font-weight: 700; text-decoration: none; color: #fbbf24; background: rgba(251,191,36,.12); border: 1px solid rgba(251,191,36,.3); }.openvibe-navbar-dropdown-menu .sep { height: 1px; background: var(--border, #333340); margin: 4px -8px; }-- .openvibe-navbar-links { display: flex; align-items: center; gap: 4px; margin-left: 8px; }++ .openvibe-navbar-links { display: flex; align-items: center; gap: 2px; margin-left: 2px; }.openvibe-navbar-links a {padding: 6px 12px; border-radius: 6px; font-size: 13px; font-weight: 500;color: var(--text-secondary, #b0b0b8); text-decoration: none;@@@ -1,6 +1,6 @@{"name": "openvibe-shared",-- "version": "1.29.0",++ "version": "1.29.1","description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site","main": "index.js","exports": {@@@ -98,8 +98,22 @@// Every token any built-in theme sets. Applying a theme clears the ones it does not set, so a// theme that names no --success cannot inherit the previous theme's --success on <html>.const KNOWN_PROPS = (function () { const out = {}; for (const id in THEMES) for (const k in THEMES[id]) out[k] = true; return Object.keys(out); })();++ /** Readable text on a colour: near-black or white, whichever contrasts more (WCAG relative luminance). */++ function onColor(c) {++ const m = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(String(c || '').trim());++ if (!m) return null;++ const h = m[1].length === 3 ? m[1].split('').map((x) => x + x).join('') : m[1];++ const lin = (i) => { const v = parseInt(h.slice(i, i + 2), 16) / 255; return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4); };++ const L = 0.2126 * lin(0) + 0.7152 * lin(2) + 0.0722 * lin(4);++ return (L + 0.05) / 0.05 >= 1.05 / (L + 0.05) ? '#0b0d10' : '#ffffff';++ }function applyVars(vars) {if (!vars || typeof vars !== 'object') return;++ // A theme without its own text-on-accent colours (custom and older server themes) gets readable ones derived.++ if (vars['--accent'] && !vars['--on-accent']) {++ const on = onColor(vars['--accent']);++ if (on) vars = Object.assign({}, vars, { '--on-accent': on, '--on-accent-strong': vars['--on-accent-strong'] || onColor(vars['--accent-strong'] || vars['--accent']) || on });++ }const el = document.documentElement;for (let i = 0; i < KNOWN_PROPS.length; i++) if (!(KNOWN_PROPS[i] in vars)) el.style.removeProperty(KNOWN_PROPS[i]);for (const [prop, val] of Object.entries(vars)) {@@@ -109,8 +123,10 @@const scheme = vars['--color-scheme'];if (scheme === 'light' || scheme === 'dark') { el.style.colorScheme = scheme; el.setAttribute('data-theme-mode', scheme); }// The inline critical style paints a fixed dark canvas before any stylesheet; keep it in step with the theme.-- if (vars['--bg-primary']) el.style.background = vars['--bg-primary'];-- if (vars['--text-primary']) el.style.color = vars['--text-primary'];++ // Through the variables (with the value as fallback): a page that sets --bg-primary later (its own theme++ // switcher) repaints the canvas too, instead of leaving the first theme's colour behind.++ if (vars['--bg-primary']) el.style.background = `var(--bg-primary, ${vars['--bg-primary']})`;++ if (vars['--text-primary']) el.style.color = `var(--text-primary, ${vars['--text-primary']})`;syncBrowserUi();}@@@ -217,6 +233,7 @@const base = variables || THEMES[themeId] || {};const vars = custom ? Object.assign({}, base, custom) : base;if (!vars || !Object.keys(vars).length) return;++ applyVars(vars); // saving a theme shows it: every token, the canvas and the browser UI follow at oncecacheLocally(themeId, vars);if (opts && opts.sync === false) return;if (typeof fetch === 'undefined') return;@@@ -335,6 +352,7 @@display: { get: readDisplay, set: setDisplay, options: DISPLAY_OPTS },apply: applyById,applyVars: applyVars,++ onColor: onColor,save: save,getCurrent: getCurrent,resolveAndApply: resolveAndApply,
@@ -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.29.1 — 2026-09-28
+
+**Themes repaint the whole page and keep button text readable.** `applyVars` sets the canvas through the variables (`var(--bg-primary, value)`), so a page with its own theme switcher that sets `--bg-primary` later repaints the background too, instead of leaving the first theme's colour behind; `save()` applies the theme it saves, so every token, the canvas and the browser UI follow at once. A theme without its own `--on-accent` (custom colours, older server themes) gets one derived by WCAG relative luminance, near-black on bright accents (a lime green) and white on dark ones; `OpenVibeThemeLoader.onColor(hex)` is exported for pages that paint their own accent surfaces. **Navbar brand, compact:** the mark and the name sit 6px apart with 2px padding, the ".Live" part hugs the name, and the bar's side padding follows the viewport (`clamp(8px, 1.6vw, 16px)`), so narrow screens keep more room for links.
+
## 1.29.0 — 2026-09-28
## 1.29.0 — 2026-09-28
**`openvibe-shared/analytics/pg`: request analytics on PostgreSQL** (ADR-035; ADR-021's privacy bounds unchanged). `AnalyticsTrackerPg` is the tracker on an openvibe-sdk/db handle, with the same tables and `getStats`, `getOverview` and `getBotAnalysis` shapes; `analyticsSchema()` is the DDL for the service's migrations; `pruneRawEventsPg` is the 30-day retention in batches. The request path never waits for the database: rows buffer in memory and `flush()` writes them in one transaction every 5 s (or at 100). Unique visitors stay right across processes: the day's salt is shared through `analytics_day_salts`, so the visitor hash is computed at flush, while (address, user agent) wait in memory until then, as the rate counters always did; no address reaches the database. Session ids stay per process, as they were. Every SUM is cast to bigint (numeric comes back as text). `test/analytics-pg.test.js` on PGlite: two trackers count one visitor once, DNT records nothing, rollups, dashboards and retention. Needed by Network's and Live's PostgreSQL passes.
**`openvibe-shared/analytics/pg`: request analytics on PostgreSQL** (ADR-035; ADR-021's privacy bounds unchanged). `AnalyticsTrackerPg` is the tracker on an openvibe-sdk/db handle, with the same tables and `getStats`, `getOverview` and `getBotAnalysis` shapes; `analyticsSchema()` is the DDL for the service's migrations; `pruneRawEventsPg` is the 30-day retention in batches. The request path never waits for the database: rows buffer in memory and `flush()` writes them in one transaction every 5 s (or at 100). Unique visitors stay right across processes: the day's salt is shared through `analytics_day_salts`, so the visitor hash is computed at flush, while (address, user agent) wait in memory until then, as the rate counters always did; no address reaches the database. Session ids stay per process, as they were. Every SUM is cast to bigint (numeric comes back as text). `test/analytics-pg.test.js` on PGlite: two trackers count one visitor once, DNT records nothing, rollups, dashboards and retention. Needed by Network's and Live's PostgreSQL passes.
@@ -4,7 +4,7 @@
@@ -4,7 +4,7 @@
"code": true,
"code": true,
"wave": "W2",
"wave": "W2",
"package": "openvibe-shared",
"package": "openvibe-shared",
- "version": "1.29.0",
+ "version": "1.29.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)",
@@ -57,7 +57,7 @@
@@ -57,7 +57,7 @@
s.textContent = `
s.textContent = `
.openvibe-navbar {
.openvibe-navbar {
position: sticky; top: 0; z-index: 10000;
position: sticky; top: 0; z-index: 10000;
- height: 52px; display: flex; align-items: center; padding: 0 16px; gap: 8px;
+ height: 52px; display: flex; align-items: center; padding: 0 clamp(8px, 1.6vw, 16px); gap: 6px;
background: var(--bg-secondary, #252530);
background: var(--bg-secondary, #252530);
border-bottom: 1px solid var(--border, #333340);
border-bottom: 1px solid var(--border, #333340);
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
@@ -67,19 +67,19 @@
@@ -67,19 +67,19 @@
/* Brand group: mark + linked wordmark + launcher read as one control. Every property a host
/* Brand group: mark + linked wordmark + launcher read as one control. Every property a host
page might set on bare "nav a" / "a" is reset here, so the brand looks the same everywhere. */
page might set on bare "nav a" / "a" is reset here, so the brand looks the same everywhere. */
.openvibe-navbar .openvibe-navbar-brand a, .openvibe-navbar .openvibe-navbar-brand a:hover { all: unset; cursor: pointer; color: inherit; font: inherit; }
.openvibe-navbar .openvibe-navbar-brand a, .openvibe-navbar .openvibe-navbar-brand a:hover { all: unset; cursor: pointer; color: inherit; font: inherit; }
- .openvibe-navbar .openvibe-navbar-brand { gap: 8px; padding: 3px 4px 3px 3px; border-radius: 12px; margin-right: 2px; }
+ .openvibe-navbar .openvibe-navbar-brand { gap: 6px; padding: 2px; border-radius: 12px; margin-right: 0; }
.openvibe-navbar .openvibe-navbar-brand a.flame { display: inline-grid; place-items: center; width: 30px; height: 30px; border-radius: 9px; transition: background .15s; }
.openvibe-navbar .openvibe-navbar-brand a.flame { display: inline-grid; place-items: center; width: 30px; height: 30px; border-radius: 9px; transition: background .15s; }
.openvibe-navbar .openvibe-navbar-brand a.flame:hover { background: var(--accent-glow, rgba(59,130,246,.14)); }
.openvibe-navbar .openvibe-navbar-brand a.flame:hover { background: var(--accent-glow, rgba(59,130,246,.14)); }
.openvibe-navbar .openvibe-navbar-brand .name { align-items: center; gap: 0; font-size: 15.5px; letter-spacing: -.25px; }
.openvibe-navbar .openvibe-navbar-brand .name { align-items: center; gap: 0; font-size: 15.5px; letter-spacing: -.25px; }
- .openvibe-navbar .openvibe-navbar-brand .name a { padding: 3px 2px; border-radius: 6px; transition: color .15s, background .15s; }
+ .openvibe-navbar .openvibe-navbar-brand .name a { padding: 3px 1px; border-radius: 6px; transition: color .15s, background .15s; }
.openvibe-navbar .openvibe-navbar-brand .name a.b-sub { color: var(--text-primary, #e6edf7); font-weight: 750; }
.openvibe-navbar .openvibe-navbar-brand .name a.b-sub { color: var(--text-primary, #e6edf7); font-weight: 750; }
.openvibe-navbar .openvibe-navbar-brand .name a.b-core { color: var(--text-primary, #e6edf7); font-weight: 700; }
.openvibe-navbar .openvibe-navbar-brand .name a.b-core { color: var(--text-primary, #e6edf7); font-weight: 700; }
.openvibe-navbar .openvibe-navbar-brand .name a.b-tld { color: var(--accent-light, var(--accent, #60a5fa)); font-weight: 700; }
.openvibe-navbar .openvibe-navbar-brand .name a.b-tld { color: var(--accent-light, var(--accent, #60a5fa)); font-weight: 700; }
.openvibe-navbar .openvibe-navbar-brand.has-sub .name a.b-core, .openvibe-navbar .openvibe-navbar-brand.has-sub .name a.b-tld { color: var(--text-secondary, #a8b3c4); font-weight: 600; }
.openvibe-navbar .openvibe-navbar-brand.has-sub .name a.b-core, .openvibe-navbar .openvibe-navbar-brand.has-sub .name a.b-tld { color: var(--text-secondary, #a8b3c4); font-weight: 600; }
.openvibe-navbar .openvibe-navbar-brand .name a:hover { color: var(--accent-light, var(--accent, #60a5fa)); background: var(--accent-glow, rgba(59,130,246,.12)); }
.openvibe-navbar .openvibe-navbar-brand .name a:hover { color: var(--accent-light, var(--accent, #60a5fa)); background: var(--accent-glow, rgba(59,130,246,.12)); }
- .openvibe-navbar .openvibe-navbar-brand .b-dot { margin: 0; padding: 0 .5px; opacity: .55; }
+ .openvibe-navbar .openvibe-navbar-brand .b-dot { margin: 0; padding: 0; opacity: .5; }
.openvibe-navbar .openvibe-navbar-brand a:focus-visible, .ovnav-launch:focus-visible { outline: 2px solid var(--accent, #3b82f6); outline-offset: 1px; }
.openvibe-navbar .openvibe-navbar-brand a:focus-visible, .ovnav-launch:focus-visible { outline: 2px solid var(--accent, #3b82f6); outline-offset: 1px; }
- .ovnav-launch { all: unset; box-sizing: border-box; cursor: pointer; color: var(--text-secondary, #a8b3c4); width: 32px; height: 32px; border-radius: 10px; display: inline-grid; place-items: center; flex: none; margin-right: 10px; border: 1px solid transparent; transition: background .15s, color .15s, border-color .15s; }
+ .ovnav-launch { all: unset; box-sizing: border-box; cursor: pointer; color: var(--text-secondary, #a8b3c4); width: 32px; height: 32px; border-radius: 10px; display: inline-grid; place-items: center; flex: none; margin-right: 2px; border: 1px solid transparent; transition: background .15s, color .15s, border-color .15s; }
.ovnav-launch:hover, .ovnav-launch[aria-expanded="true"] { background: var(--accent-glow, rgba(59,130,246,.14)); color: var(--accent-light, var(--accent, #60a5fa)); border-color: color-mix(in srgb, var(--accent, #3b82f6) 35%, transparent); }
.ovnav-launch:hover, .ovnav-launch[aria-expanded="true"] { background: var(--accent-glow, rgba(59,130,246,.14)); color: var(--accent-light, var(--accent, #60a5fa)); border-color: color-mix(in srgb, var(--accent, #3b82f6) 35%, transparent); }
.ovnav-launch svg { display: block; transition: transform .25s cubic-bezier(.2,1.4,.3,1); }
.ovnav-launch svg { display: block; transition: transform .25s cubic-bezier(.2,1.4,.3,1); }
.ovnav-launch[aria-expanded="true"] svg { transform: rotate(45deg) scale(.92); }
.ovnav-launch[aria-expanded="true"] svg { transform: rotate(45deg) scale(.92); }
@@ -198,7 +198,7 @@
@@ -198,7 +198,7 @@
.openvibe-navbar-dropdown-header .ud-wallet a { display: inline-flex; align-items: center; gap: 5px; margin-top: 5px; padding: 2px 9px; border-radius: 999px; font-size: 12px; font-weight: 700; text-decoration: none; color: #fbbf24; background: rgba(251,191,36,.12); border: 1px solid rgba(251,191,36,.3); }
.openvibe-navbar-dropdown-header .ud-wallet a { display: inline-flex; align-items: center; gap: 5px; margin-top: 5px; padding: 2px 9px; border-radius: 999px; font-size: 12px; font-weight: 700; text-decoration: none; color: #fbbf24; background: rgba(251,191,36,.12); border: 1px solid rgba(251,191,36,.3); }
.openvibe-navbar-dropdown-menu .sep { height: 1px; background: var(--border, #333340); margin: 4px -8px; }
.openvibe-navbar-dropdown-menu .sep { height: 1px; background: var(--border, #333340); margin: 4px -8px; }
- .openvibe-navbar-links { display: flex; align-items: center; gap: 4px; margin-left: 8px; }
+ .openvibe-navbar-links { display: flex; align-items: center; gap: 2px; margin-left: 2px; }
.openvibe-navbar-links a {
.openvibe-navbar-links a {
padding: 6px 12px; border-radius: 6px; font-size: 13px; font-weight: 500;
padding: 6px 12px; border-radius: 6px; font-size: 13px; font-weight: 500;
color: var(--text-secondary, #b0b0b8); text-decoration: none;
color: var(--text-secondary, #b0b0b8); text-decoration: none;
@@ -1,6 +1,6 @@
@@ -1,6 +1,6 @@
{
{
"name": "openvibe-shared",
"name": "openvibe-shared",
- "version": "1.29.0",
+ "version": "1.29.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": {
@@ -98,8 +98,22 @@
@@ -98,8 +98,22 @@
// Every token any built-in theme sets. Applying a theme clears the ones it does not set, so a
// Every token any built-in theme sets. Applying a theme clears the ones it does not set, so a
// theme that names no --success cannot inherit the previous theme's --success on <html>.
// theme that names no --success cannot inherit the previous theme's --success on <html>.
const KNOWN_PROPS = (function () { const out = {}; for (const id in THEMES) for (const k in THEMES[id]) out[k] = true; return Object.keys(out); })();
const KNOWN_PROPS = (function () { const out = {}; for (const id in THEMES) for (const k in THEMES[id]) out[k] = true; return Object.keys(out); })();
+ /** Readable text on a colour: near-black or white, whichever contrasts more (WCAG relative luminance). */
+ function onColor(c) {
+ const m = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(String(c || '').trim());
+ if (!m) return null;
+ const h = m[1].length === 3 ? m[1].split('').map((x) => x + x).join('') : m[1];
+ const lin = (i) => { const v = parseInt(h.slice(i, i + 2), 16) / 255; return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4); };
+ const L = 0.2126 * lin(0) + 0.7152 * lin(2) + 0.0722 * lin(4);
+ return (L + 0.05) / 0.05 >= 1.05 / (L + 0.05) ? '#0b0d10' : '#ffffff';
+ }
function applyVars(vars) {
function applyVars(vars) {
if (!vars || typeof vars !== 'object') return;
if (!vars || typeof vars !== 'object') return;
+ // A theme without its own text-on-accent colours (custom and older server themes) gets readable ones derived.
+ if (vars['--accent'] && !vars['--on-accent']) {
+ const on = onColor(vars['--accent']);
+ if (on) vars = Object.assign({}, vars, { '--on-accent': on, '--on-accent-strong': vars['--on-accent-strong'] || onColor(vars['--accent-strong'] || vars['--accent']) || on });
+ }
const el = document.documentElement;
const el = document.documentElement;
for (let i = 0; i < KNOWN_PROPS.length; i++) if (!(KNOWN_PROPS[i] in vars)) el.style.removeProperty(KNOWN_PROPS[i]);
for (let i = 0; i < KNOWN_PROPS.length; i++) if (!(KNOWN_PROPS[i] in vars)) el.style.removeProperty(KNOWN_PROPS[i]);
for (const [prop, val] of Object.entries(vars)) {
for (const [prop, val] of Object.entries(vars)) {
@@ -109,8 +123,10 @@
@@ -109,8 +123,10 @@
const scheme = vars['--color-scheme'];
const scheme = vars['--color-scheme'];
if (scheme === 'light' || scheme === 'dark') { el.style.colorScheme = scheme; el.setAttribute('data-theme-mode', scheme); }
if (scheme === 'light' || scheme === 'dark') { el.style.colorScheme = scheme; el.setAttribute('data-theme-mode', scheme); }
// The inline critical style paints a fixed dark canvas before any stylesheet; keep it in step with the theme.
// The inline critical style paints a fixed dark canvas before any stylesheet; keep it in step with the theme.
- if (vars['--bg-primary']) el.style.background = vars['--bg-primary'];
- if (vars['--text-primary']) el.style.color = vars['--text-primary'];
+ // Through the variables (with the value as fallback): a page that sets --bg-primary later (its own theme
+ // switcher) repaints the canvas too, instead of leaving the first theme's colour behind.
+ if (vars['--bg-primary']) el.style.background = `var(--bg-primary, ${vars['--bg-primary']})`;
+ if (vars['--text-primary']) el.style.color = `var(--text-primary, ${vars['--text-primary']})`;
syncBrowserUi();
syncBrowserUi();
}
}
@@ -217,6 +233,7 @@
@@ -217,6 +233,7 @@
const base = variables || THEMES[themeId] || {};
const base = variables || THEMES[themeId] || {};
const vars = custom ? Object.assign({}, base, custom) : base;
const vars = custom ? Object.assign({}, base, custom) : base;
if (!vars || !Object.keys(vars).length) return;
if (!vars || !Object.keys(vars).length) return;
+ applyVars(vars); // saving a theme shows it: every token, the canvas and the browser UI follow at once
cacheLocally(themeId, vars);
cacheLocally(themeId, vars);
if (opts && opts.sync === false) return;
if (opts && opts.sync === false) return;
if (typeof fetch === 'undefined') return;
if (typeof fetch === 'undefined') return;
@@ -335,6 +352,7 @@
@@ -335,6 +352,7 @@
display: { get: readDisplay, set: setDisplay, options: DISPLAY_OPTS },
display: { get: readDisplay, set: setDisplay, options: DISPLAY_OPTS },
apply: applyById,
apply: applyById,
applyVars: applyVars,
applyVars: applyVars,
+ onColor: onColor,
save: save,
save: save,
getCurrent: getCurrent,
getCurrent: getCurrent,
resolveAndApply: resolveAndApply,
resolveAndApply: resolveAndApply,