openvibers/openvibe.live Commits
The hero frame's veil follows the theme (#535)
Changes
@@@ -1106,7 +1106,10 @@ html[data-theme-mode="light"] .hero-scrim {linear-gradient(180deg, color-mix(in srgb, var(--bg-primary) 55%, transparent) 0%, transparent 22%, transparent 78%, var(--bg-primary) 100%),linear-gradient(color-mix(in srgb, var(--bg-primary) 64%, transparent), color-mix(in srgb, var(--bg-primary) 64%, transparent));}--html[data-theme-mode="light"] .hero-bg-layer { filter: brightness(1.25) saturate(.8) contrast(.85); }++/* The frame's veil, baked into its background (home-fx.css keeps filters off this animating layer, so the filter that++ was here never applied): black on dark themes, white on light ones, where the black veil turned the hero grey. */++.hero-bg { --hero-veil: rgba(0, 0, 0, 0.5); }++html[data-theme-mode="light"] .hero-bg { --hero-veil: rgba(255, 255, 255, 0.62); }/* ── Hero background: one AI moment frame at a time, Ken-Burns crossfade ─────────── */.hero-bg { position: absolute; inset: 0; z-index: 0; overflow: hidden; pointer-events: none; }@@@ -723,7 +723,7 @@ function renderHeroBackground(media, moments) {_heroBgActive = 0;// Unless the server painted this frame (seo/pages.js): set it and restart the Ken Burns drift.if (!(layers[0].style.backgroundImage || '').includes(frames[0])) {-- layers[0].style.backgroundImage = `linear-gradient(rgba(0,0,0,0.5), rgba(0,0,0,0.5)), url("${frames[0]}")`;++ layers[0].style.backgroundImage = `linear-gradient(var(--hero-veil, rgba(0,0,0,0.5)), var(--hero-veil, rgba(0,0,0,0.5))), url("${frames[0]}")`;layers[0].classList.remove('active'); void layers[0].offsetWidth; layers[0].classList.add('active');} else layers[0].classList.add('active');layers[1].classList.remove('active');@@@ -736,9 +736,9 @@ function renderHeroBackground(media, moments) {const el = layers[next];const pre = new Image();pre.onload = () => {-- // The darkening is part of the frame's own background rather than a filter or an overlay element:-- // either of those becomes a second full-hero layer on top of an animating one.-- el.style.backgroundImage = `linear-gradient(rgba(0,0,0,0.5), rgba(0,0,0,0.5)), url("${frames[idx]}")`;++ // The veil is part of the frame's own background rather than a filter or an overlay element: either of those++ // becomes a second full-hero layer on top of an animating one. Its colour is the theme's (--hero-veil, style.css).++ el.style.backgroundImage = `linear-gradient(var(--hero-veil, rgba(0,0,0,0.5)), var(--hero-veil, rgba(0,0,0,0.5))), url("${frames[idx]}")`;el.classList.remove('active'); void el.offsetWidth; el.classList.add('active');layers[_heroBgActive].classList.remove('active');_heroBgActive = next;@@@ -865,7 +865,7 @@ function render(meta, urlPath) {// it summarises the same content the SPA renders.if (meta.snapshot) html = _prerender(html, meta.snapshot + _summary(meta));// The first hero layer shows that frame already (the same background app-home.js gives it).-- if (meta.heroImage) html = html.replace('<div class="hero-bg-layer"></div>', `<div class="hero-bg-layer active" style="background-image:linear-gradient(rgba(0,0,0,0.5),rgba(0,0,0,0.5)),url("${esc(meta.heroImage)}")"></div>`);++ if (meta.heroImage) html = html.replace('<div class="hero-bg-layer"></div>', `<div class="hero-bg-layer active" style="background-image:linear-gradient(var(--hero-veil,rgba(0,0,0,0.5)),var(--hero-veil,rgba(0,0,0,0.5))),url("${esc(meta.heroImage)}")"></div>`);return html;}@@@ -297,7 +297,7 @@ const server = http.createServer(app).listen(0, '127.0.0.1', async () => {assert.strictEqual(meta.heroImage, frame);const html = pages.render(meta, '/');assert.ok(head(html).includes(`<link rel="preload" as="image" href="${frame}" fetchpriority="high">`), 'preloaded in the head');-- assert.ok(html.includes(`<div class="hero-bg-layer active" style="background-image:linear-gradient(rgba(0,0,0,0.5),rgba(0,0,0,0.5)),url("${frame}")"></div><div class="hero-bg-layer"></div>`), 'the first layer shows it');++ assert.ok(html.includes(`<div class="hero-bg-layer active" style="background-image:linear-gradient(var(--hero-veil,rgba(0,0,0,0.5)),var(--hero-veil,rgba(0,0,0,0.5))),url("${frame}")"></div><div class="hero-bg-layer"></div>`), 'the first layer shows it');// A thumbnail that is not a plain https URL is never written into the page.await db.setState('home_hero_moments', JSON.stringify({ moments: [{ vodId: 100, thumbnail: 'https://x.test/a")%3Bbackground:url("//evil' }] }));assert.strictEqual((await pages._pageMeta('/')).heroImage, null);@@@ -50,4 +50,9 @@ assert.match(channel, /\.ch-lang-chip \{[^}]*color: color-mix\(in srgb, #f59e0bconst allCss = files.filter((f) => f.endsWith('.css')).map((f) => fs.readFileSync(f, 'utf8')).join('\n');assert.ok(!/(^|[^-\w])color:\s*(#b388ff|var\(--reward-color, #b388ff\))/m.test(allCss), 'no fixed pale violet text');assert.match(style, /\.chat-relay-badge \{ color: color-mix\(in srgb, var\(--relay-color, #888\) 55%, var\(--text-primary\)\)/);++// The hero frame's veil is the theme's: white on light themes (a black veil greyed the whole hero on Daylight).++assert.match(style, /\.hero-bg \{ --hero-veil: rgba\(0, 0, 0, 0\.5\); \}/);++assert.match(style, /html\[data-theme-mode="light"\] \.hero-bg \{ --hero-veil: rgba\(255, 255, 255, 0\.62\); \}/);++const home = fs.readFileSync(path.join(root, 'js', 'app-home.js'), 'utf8');++assert.ok(!/linear-gradient\(rgba\(0,0,0,0\.5\)/.test(home), 'no fixed black veil on the hero frames');console.log('theme tokens defined: all checks passed');
@@ -1106,7 +1106,10 @@ html[data-theme-mode="light"] .hero-scrim {
@@ -1106,7 +1106,10 @@ html[data-theme-mode="light"] .hero-scrim {
linear-gradient(180deg, color-mix(in srgb, var(--bg-primary) 55%, transparent) 0%, transparent 22%, transparent 78%, var(--bg-primary) 100%),
linear-gradient(180deg, color-mix(in srgb, var(--bg-primary) 55%, transparent) 0%, transparent 22%, transparent 78%, var(--bg-primary) 100%),
linear-gradient(color-mix(in srgb, var(--bg-primary) 64%, transparent), color-mix(in srgb, var(--bg-primary) 64%, transparent));
linear-gradient(color-mix(in srgb, var(--bg-primary) 64%, transparent), color-mix(in srgb, var(--bg-primary) 64%, transparent));
}
}
-html[data-theme-mode="light"] .hero-bg-layer { filter: brightness(1.25) saturate(.8) contrast(.85); }
+/* The frame's veil, baked into its background (home-fx.css keeps filters off this animating layer, so the filter that
+ was here never applied): black on dark themes, white on light ones, where the black veil turned the hero grey. */
+.hero-bg { --hero-veil: rgba(0, 0, 0, 0.5); }
+html[data-theme-mode="light"] .hero-bg { --hero-veil: rgba(255, 255, 255, 0.62); }
/* ── Hero background: one AI moment frame at a time, Ken-Burns crossfade ─────────── */
/* ── Hero background: one AI moment frame at a time, Ken-Burns crossfade ─────────── */
.hero-bg { position: absolute; inset: 0; z-index: 0; overflow: hidden; pointer-events: none; }
.hero-bg { position: absolute; inset: 0; z-index: 0; overflow: hidden; pointer-events: none; }
@@ -723,7 +723,7 @@ function renderHeroBackground(media, moments) {
@@ -723,7 +723,7 @@ function renderHeroBackground(media, moments) {
_heroBgActive = 0;
_heroBgActive = 0;
// Unless the server painted this frame (seo/pages.js): set it and restart the Ken Burns drift.
// Unless the server painted this frame (seo/pages.js): set it and restart the Ken Burns drift.
if (!(layers[0].style.backgroundImage || '').includes(frames[0])) {
if (!(layers[0].style.backgroundImage || '').includes(frames[0])) {
- layers[0].style.backgroundImage = `linear-gradient(rgba(0,0,0,0.5), rgba(0,0,0,0.5)), url("${frames[0]}")`;
+ layers[0].style.backgroundImage = `linear-gradient(var(--hero-veil, rgba(0,0,0,0.5)), var(--hero-veil, rgba(0,0,0,0.5))), url("${frames[0]}")`;
layers[0].classList.remove('active'); void layers[0].offsetWidth; layers[0].classList.add('active');
layers[0].classList.remove('active'); void layers[0].offsetWidth; layers[0].classList.add('active');
} else layers[0].classList.add('active');
} else layers[0].classList.add('active');
layers[1].classList.remove('active');
layers[1].classList.remove('active');
@@ -736,9 +736,9 @@ function renderHeroBackground(media, moments) {
@@ -736,9 +736,9 @@ function renderHeroBackground(media, moments) {
const el = layers[next];
const el = layers[next];
const pre = new Image();
const pre = new Image();
pre.onload = () => {
pre.onload = () => {
- // The darkening is part of the frame's own background rather than a filter or an overlay element:
- // either of those becomes a second full-hero layer on top of an animating one.
- el.style.backgroundImage = `linear-gradient(rgba(0,0,0,0.5), rgba(0,0,0,0.5)), url("${frames[idx]}")`;
+ // The veil is part of the frame's own background rather than a filter or an overlay element: either of those
+ // becomes a second full-hero layer on top of an animating one. Its colour is the theme's (--hero-veil, style.css).
+ el.style.backgroundImage = `linear-gradient(var(--hero-veil, rgba(0,0,0,0.5)), var(--hero-veil, rgba(0,0,0,0.5))), url("${frames[idx]}")`;
el.classList.remove('active'); void el.offsetWidth; el.classList.add('active');
el.classList.remove('active'); void el.offsetWidth; el.classList.add('active');
layers[_heroBgActive].classList.remove('active');
layers[_heroBgActive].classList.remove('active');
_heroBgActive = next;
_heroBgActive = next;
@@ -865,7 +865,7 @@ function render(meta, urlPath) {
@@ -865,7 +865,7 @@ function render(meta, urlPath) {
// it summarises the same content the SPA renders.
// it summarises the same content the SPA renders.
if (meta.snapshot) html = _prerender(html, meta.snapshot + _summary(meta));
if (meta.snapshot) html = _prerender(html, meta.snapshot + _summary(meta));
// The first hero layer shows that frame already (the same background app-home.js gives it).
// The first hero layer shows that frame already (the same background app-home.js gives it).
- if (meta.heroImage) html = html.replace('<div class="hero-bg-layer"></div>', `<div class="hero-bg-layer active" style="background-image:linear-gradient(rgba(0,0,0,0.5),rgba(0,0,0,0.5)),url("${esc(meta.heroImage)}")"></div>`);
+ if (meta.heroImage) html = html.replace('<div class="hero-bg-layer"></div>', `<div class="hero-bg-layer active" style="background-image:linear-gradient(var(--hero-veil,rgba(0,0,0,0.5)),var(--hero-veil,rgba(0,0,0,0.5))),url("${esc(meta.heroImage)}")"></div>`);
return html;
return html;
}
}
@@ -297,7 +297,7 @@ const server = http.createServer(app).listen(0, '127.0.0.1', async () => {
@@ -297,7 +297,7 @@ const server = http.createServer(app).listen(0, '127.0.0.1', async () => {
assert.strictEqual(meta.heroImage, frame);
assert.strictEqual(meta.heroImage, frame);
const html = pages.render(meta, '/');
const html = pages.render(meta, '/');
assert.ok(head(html).includes(`<link rel="preload" as="image" href="${frame}" fetchpriority="high">`), 'preloaded in the head');
assert.ok(head(html).includes(`<link rel="preload" as="image" href="${frame}" fetchpriority="high">`), 'preloaded in the head');
- assert.ok(html.includes(`<div class="hero-bg-layer active" style="background-image:linear-gradient(rgba(0,0,0,0.5),rgba(0,0,0,0.5)),url("${frame}")"></div><div class="hero-bg-layer"></div>`), 'the first layer shows it');
+ assert.ok(html.includes(`<div class="hero-bg-layer active" style="background-image:linear-gradient(var(--hero-veil,rgba(0,0,0,0.5)),var(--hero-veil,rgba(0,0,0,0.5))),url("${frame}")"></div><div class="hero-bg-layer"></div>`), 'the first layer shows it');
// A thumbnail that is not a plain https URL is never written into the page.
// A thumbnail that is not a plain https URL is never written into the page.
await db.setState('home_hero_moments', JSON.stringify({ moments: [{ vodId: 100, thumbnail: 'https://x.test/a")%3Bbackground:url("//evil' }] }));
await db.setState('home_hero_moments', JSON.stringify({ moments: [{ vodId: 100, thumbnail: 'https://x.test/a")%3Bbackground:url("//evil' }] }));
assert.strictEqual((await pages._pageMeta('/')).heroImage, null);
assert.strictEqual((await pages._pageMeta('/')).heroImage, null);
@@ -50,4 +50,9 @@ assert.match(channel, /\.ch-lang-chip \{[^}]*color: color-mix\(in srgb, #f59e0b
@@ -50,4 +50,9 @@ assert.match(channel, /\.ch-lang-chip \{[^}]*color: color-mix\(in srgb, #f59e0b
const allCss = files.filter((f) => f.endsWith('.css')).map((f) => fs.readFileSync(f, 'utf8')).join('\n');
const allCss = files.filter((f) => f.endsWith('.css')).map((f) => fs.readFileSync(f, 'utf8')).join('\n');
assert.ok(!/(^|[^-\w])color:\s*(#b388ff|var\(--reward-color, #b388ff\))/m.test(allCss), 'no fixed pale violet text');
assert.ok(!/(^|[^-\w])color:\s*(#b388ff|var\(--reward-color, #b388ff\))/m.test(allCss), 'no fixed pale violet text');
assert.match(style, /\.chat-relay-badge \{ color: color-mix\(in srgb, var\(--relay-color, #888\) 55%, var\(--text-primary\)\)/);
assert.match(style, /\.chat-relay-badge \{ color: color-mix\(in srgb, var\(--relay-color, #888\) 55%, var\(--text-primary\)\)/);
+// The hero frame's veil is the theme's: white on light themes (a black veil greyed the whole hero on Daylight).
+assert.match(style, /\.hero-bg \{ --hero-veil: rgba\(0, 0, 0, 0\.5\); \}/);
+assert.match(style, /html\[data-theme-mode="light"\] \.hero-bg \{ --hero-veil: rgba\(255, 255, 255, 0\.62\); \}/);
+const home = fs.readFileSync(path.join(root, 'js', 'app-home.js'), 'utf8');
+assert.ok(!/linear-gradient\(rgba\(0,0,0,0\.5\)/.test(home), 'no fixed black veil on the hero frames');
console.log('theme tokens defined: all checks passed');
console.log('theme tokens defined: all checks passed');