Improve background effects for light theme and fix mobile color picker

- WebGL shader: theme-aware blending (tint toward accent on light, additive
  glow on dark) with u_light uniform for proper light-theme visibility
- Cards: translucent backgrounds only on entity cards when bg-anim is active,
  keeping modals/pickers/tab bars/header fully opaque
- Running card border and tab indicator: boosted contrast for light theme
- Header: backdrop-filter via pseudo-element to avoid breaking fixed tab-bar
- Color picker: move popover to document.body on mobile as centered bottom-sheet
- Add card: use --card-bg background and bolder + icon for visibility

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-12 21:42:22 +03:00
parent b9c71d5bb9
commit 812d15419c
4 changed files with 95 additions and 19 deletions

View File

@@ -102,6 +102,37 @@ body.modal-open {
background: transparent; background: transparent;
} }
/* When bg-anim is active, make entity cards slightly translucent
so the shader bleeds through. Only target cards — NOT modals,
pickers, tab bars, headers, or other chrome. */
[data-bg-anim="on"][data-theme="dark"] .card,
[data-bg-anim="on"][data-theme="dark"] .template-card,
[data-bg-anim="on"][data-theme="dark"] .add-device-card,
[data-bg-anim="on"][data-theme="dark"] .dashboard-target {
background: rgba(45, 45, 45, 0.88);
}
[data-bg-anim="on"][data-theme="light"] .card,
[data-bg-anim="on"][data-theme="light"] .template-card,
[data-bg-anim="on"][data-theme="light"] .add-device-card,
[data-bg-anim="on"][data-theme="light"] .dashboard-target {
background: rgba(255, 255, 255, 0.85);
}
/* Blur behind header via pseudo-element — applying backdrop-filter directly
to header would create a containing block and break position:fixed on
the .tab-bar nested inside it (mobile bottom nav). */
[data-bg-anim="on"] header {
background: transparent;
}
[data-bg-anim="on"] header::after {
content: '';
position: absolute;
inset: 0;
z-index: -1;
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
background: color-mix(in srgb, var(--bg-color) 60%, transparent);
}
/* ── Tab indicator (background watermark) ── */ /* ── Tab indicator (background watermark) ── */
#tab-indicator { #tab-indicator {
position: fixed; position: fixed;
@@ -128,7 +159,7 @@ body.modal-open {
opacity: 1; opacity: 1;
} }
[data-theme="light"] #tab-indicator svg { [data-theme="light"] #tab-indicator svg {
opacity: 0.035; opacity: 0.07;
} }
.container { .container {

View File

@@ -21,7 +21,7 @@ section {
justify-content: center; justify-content: center;
cursor: pointer; cursor: pointer;
border: 2px dashed var(--border-color); border: 2px dashed var(--border-color);
background: transparent; background: var(--card-bg);
min-height: 160px; min-height: 160px;
transition: border-color 0.25s ease, background 0.25s ease, transform 0.2s ease; transition: border-color 0.25s ease, background 0.25s ease, transform 0.2s ease;
} }
@@ -34,8 +34,8 @@ section {
.add-device-icon { .add-device-icon {
font-size: 2.5rem; font-size: 2.5rem;
font-weight: 300; font-weight: 400;
color: var(--text-secondary); color: var(--text-color);
line-height: 1; line-height: 1;
transition: color 0.2s; transition: color 0.2s;
} }
@@ -156,14 +156,25 @@ section {
to { --border-angle: 360deg; } to { --border-angle: 360deg; }
} }
[data-theme="light"] .card-running {
background: linear-gradient(
calc(var(--border-angle) + 45deg),
var(--card-bg) 0%,
color-mix(in srgb, var(--primary-color) 18%, var(--card-bg)) 40%,
var(--card-bg) 60%,
color-mix(in srgb, var(--primary-color) 14%, var(--card-bg)) 85%,
var(--card-bg) 100%
);
}
[data-theme="light"] .card-running::before { [data-theme="light"] .card-running::before {
background: background:
conic-gradient( conic-gradient(
from var(--border-angle), from var(--border-angle),
var(--primary-color), var(--primary-color),
rgba(0,0,0,0.05) 25%, rgba(0,0,0,0.12) 25%,
var(--primary-color) 50%, var(--primary-color) 50%,
rgba(0,0,0,0.05) 75%, rgba(0,0,0,0.12) 75%,
var(--primary-color) var(--primary-color)
) border-box; ) border-box;
} }

View File

@@ -18,6 +18,7 @@ uniform float u_time;
uniform vec2 u_res; uniform vec2 u_res;
uniform vec3 u_accent; uniform vec3 u_accent;
uniform vec3 u_bg; uniform vec3 u_bg;
uniform float u_light; // 0.0 = dark theme, 1.0 = light theme
uniform vec3 u_particles[${PARTICLE_COUNT}]; // xy = position (0-1), z = radius uniform vec3 u_particles[${PARTICLE_COUNT}]; // xy = position (0-1), z = radius
// Simplex-style noise // Simplex-style noise
@@ -72,8 +73,12 @@ void main() {
float colorMix = n2 * 0.5 + 0.5; float colorMix = n2 * 0.5 + 0.5;
vec3 color = mix(col1, col2, colorMix); vec3 color = mix(col1, col2, colorMix);
// Noise background float boost = 1.0 + u_light * 1.2; // 1.0 dark, 2.2 light
vec3 result = mix(u_bg, u_bg + color * 0.6, glow * 0.14);
// Noise background: additive glow on dark, tint toward accent on light
vec3 noiseDark = mix(u_bg, u_bg + color * 0.6, glow * 0.14);
vec3 noiseLight = mix(u_bg, u_accent * 0.82, glow * 0.14 * boost);
vec3 result = mix(noiseDark, noiseLight, u_light);
// Floating particles — accumulate glow from each // Floating particles — accumulate glow from each
float particleGlow = 0.0; float particleGlow = 0.0;
@@ -90,20 +95,24 @@ void main() {
particleColor += g * step(0.5, fract(float(i) * 0.5)); particleColor += g * step(0.5, fract(float(i) * 0.5));
} }
// Mix particle colors: accent and white-ish accent // Dark: additive white-ish accent glow; Light: blend toward accent color
vec3 pCol = mix(u_accent, mix(u_accent, vec3(1.0), 0.5), particleColor / max(particleGlow, 0.001)); vec3 pCol = mix(u_accent, mix(u_accent, vec3(1.0), 0.5), particleColor / max(particleGlow, 0.001));
result += pCol * particleGlow * 0.5; float pI = particleGlow * 0.5;
vec3 pDark = result + pCol * pI;
vec3 pLight = mix(result, u_accent * 0.7, pI * boost * 0.35);
result = mix(pDark, pLight, u_light);
gl_FragColor = vec4(result, 1.0); gl_FragColor = vec4(result, 1.0);
} }
`; `;
let _canvas, _gl, _prog; let _canvas, _gl, _prog;
let _uTime, _uRes, _uAccent, _uBg, _uParticles; let _uTime, _uRes, _uAccent, _uBg, _uLight, _uParticles;
let _raf = null; let _raf = null;
let _startTime = 0; let _startTime = 0;
let _accent = [76 / 255, 175 / 255, 80 / 255]; let _accent = [76 / 255, 175 / 255, 80 / 255];
let _bgColor = [26 / 255, 26 / 255, 26 / 255]; let _bgColor = [26 / 255, 26 / 255, 26 / 255];
let _isLight = 0.0;
// Particle state (CPU-side, positions in 0..1 UV space) // Particle state (CPU-side, positions in 0..1 UV space)
const _particles = []; const _particles = [];
@@ -174,6 +183,7 @@ function _initGL() {
_uRes = gl.getUniformLocation(_prog, 'u_res'); _uRes = gl.getUniformLocation(_prog, 'u_res');
_uAccent = gl.getUniformLocation(_prog, 'u_accent'); _uAccent = gl.getUniformLocation(_prog, 'u_accent');
_uBg = gl.getUniformLocation(_prog, 'u_bg'); _uBg = gl.getUniformLocation(_prog, 'u_bg');
_uLight = gl.getUniformLocation(_prog, 'u_light');
_uParticles = []; _uParticles = [];
for (let i = 0; i < PARTICLE_COUNT; i++) { for (let i = 0; i < PARTICLE_COUNT; i++) {
_uParticles.push(gl.getUniformLocation(_prog, `u_particles[${i}]`)); _uParticles.push(gl.getUniformLocation(_prog, `u_particles[${i}]`));
@@ -202,6 +212,7 @@ function _draw(time) {
gl.uniform2f(_uRes, _canvas.width, _canvas.height); gl.uniform2f(_uRes, _canvas.width, _canvas.height);
gl.uniform3f(_uAccent, _accent[0], _accent[1], _accent[2]); gl.uniform3f(_uAccent, _accent[0], _accent[1], _accent[2]);
gl.uniform3f(_uBg, _bgColor[0], _bgColor[1], _bgColor[2]); gl.uniform3f(_uBg, _bgColor[0], _bgColor[1], _bgColor[2]);
gl.uniform1f(_uLight, _isLight);
for (let i = 0; i < PARTICLE_COUNT; i++) { for (let i = 0; i < PARTICLE_COUNT; i++) {
const p = _particles[i]; const p = _particles[i];
@@ -241,6 +252,7 @@ export function updateBgAnimAccent(hex) {
export function updateBgAnimTheme(isDark) { export function updateBgAnimTheme(isDark) {
_bgColor = isDark ? [26 / 255, 26 / 255, 26 / 255] : [245 / 255, 245 / 255, 245 / 255]; _bgColor = isDark ? [26 / 255, 26 / 255, 26 / 255] : [245 / 255, 245 / 255, 245 / 255];
_isLight = isDark ? 0.0 : 1.0;
} }
export function initBgAnim() { export function initBgAnim() {

View File

@@ -88,15 +88,25 @@ window._cpToggle = function (id) {
const card = pop.closest('.card, .template-card'); const card = pop.closest('.card, .template-card');
if (card) card.classList.toggle('cp-elevated', true); if (card) card.classList.toggle('cp-elevated', true);
// On small screens, if inside an overflow container (e.g. header toolbar), // On small/touch screens, move popover to body as a centered bottom-sheet
// switch to fixed positioning so the popover isn't clipped. // to avoid any parent overflow/containment/positioning issues.
const wrapper = pop.closest('.color-picker-wrapper'); const isMobile = window.innerWidth <= 768 || ('ontouchstart' in window);
if (wrapper && window.innerWidth <= 600) { if (isMobile && pop.parentElement !== document.body) {
const rect = wrapper.getBoundingClientRect(); pop._cpOrigParent = pop.parentElement;
pop._cpOrigNext = pop.nextSibling;
document.body.appendChild(pop);
}
if (isMobile) {
pop.style.position = 'fixed'; pop.style.position = 'fixed';
pop.style.top = (rect.bottom + 4) + 'px'; pop.style.left = '0';
pop.style.right = Math.max(4, window.innerWidth - rect.right) + 'px'; pop.style.right = '0';
pop.style.left = 'auto'; pop.style.top = 'auto';
pop.style.bottom = '70px';
pop.style.margin = '0 auto';
pop.style.width = 'fit-content';
pop.style.transform = 'none';
pop.style.animation = 'none';
pop.style.zIndex = '10000';
pop.classList.add('cp-fixed'); pop.classList.add('cp-fixed');
} }
@@ -118,6 +128,18 @@ function _cpClosePopover(pop) {
pop.style.top = ''; pop.style.top = '';
pop.style.right = ''; pop.style.right = '';
pop.style.left = ''; pop.style.left = '';
pop.style.bottom = '';
pop.style.transform = '';
pop.style.animation = '';
pop.style.margin = '';
pop.style.width = '';
pop.style.zIndex = '';
// Return popover to its original parent
if (pop._cpOrigParent) {
pop._cpOrigParent.insertBefore(pop, pop._cpOrigNext || null);
delete pop._cpOrigParent;
delete pop._cpOrigNext;
}
} }
const card = pop.closest('.card, .template-card'); const card = pop.closest('.card, .template-card');
if (card) card.classList.remove('cp-elevated'); if (card) card.classList.remove('cp-elevated');