diff --git a/server/src/wled_controller/static/css/base.css b/server/src/wled_controller/static/css/base.css index 93be9d2..0a9ceee 100644 --- a/server/src/wled_controller/static/css/base.css +++ b/server/src/wled_controller/static/css/base.css @@ -102,6 +102,37 @@ body.modal-open { 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 { position: fixed; @@ -128,7 +159,7 @@ body.modal-open { opacity: 1; } [data-theme="light"] #tab-indicator svg { - opacity: 0.035; + opacity: 0.07; } .container { diff --git a/server/src/wled_controller/static/css/cards.css b/server/src/wled_controller/static/css/cards.css index 5fb763c..75e09e3 100644 --- a/server/src/wled_controller/static/css/cards.css +++ b/server/src/wled_controller/static/css/cards.css @@ -21,7 +21,7 @@ section { justify-content: center; cursor: pointer; border: 2px dashed var(--border-color); - background: transparent; + background: var(--card-bg); min-height: 160px; transition: border-color 0.25s ease, background 0.25s ease, transform 0.2s ease; } @@ -34,8 +34,8 @@ section { .add-device-icon { font-size: 2.5rem; - font-weight: 300; - color: var(--text-secondary); + font-weight: 400; + color: var(--text-color); line-height: 1; transition: color 0.2s; } @@ -156,14 +156,25 @@ section { 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 { background: conic-gradient( from var(--border-angle), var(--primary-color), - rgba(0,0,0,0.05) 25%, + rgba(0,0,0,0.12) 25%, var(--primary-color) 50%, - rgba(0,0,0,0.05) 75%, + rgba(0,0,0,0.12) 75%, var(--primary-color) ) border-box; } diff --git a/server/src/wled_controller/static/js/core/bg-anim.js b/server/src/wled_controller/static/js/core/bg-anim.js index 479e86f..4a89f49 100644 --- a/server/src/wled_controller/static/js/core/bg-anim.js +++ b/server/src/wled_controller/static/js/core/bg-anim.js @@ -18,6 +18,7 @@ uniform float u_time; uniform vec2 u_res; uniform vec3 u_accent; 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 // Simplex-style noise @@ -72,8 +73,12 @@ void main() { float colorMix = n2 * 0.5 + 0.5; vec3 color = mix(col1, col2, colorMix); - // Noise background - vec3 result = mix(u_bg, u_bg + color * 0.6, glow * 0.14); + float boost = 1.0 + u_light * 1.2; // 1.0 dark, 2.2 light + + // 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 float particleGlow = 0.0; @@ -90,20 +95,24 @@ void main() { 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)); - 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); } `; let _canvas, _gl, _prog; -let _uTime, _uRes, _uAccent, _uBg, _uParticles; +let _uTime, _uRes, _uAccent, _uBg, _uLight, _uParticles; let _raf = null; let _startTime = 0; let _accent = [76 / 255, 175 / 255, 80 / 255]; let _bgColor = [26 / 255, 26 / 255, 26 / 255]; +let _isLight = 0.0; // Particle state (CPU-side, positions in 0..1 UV space) const _particles = []; @@ -174,6 +183,7 @@ function _initGL() { _uRes = gl.getUniformLocation(_prog, 'u_res'); _uAccent = gl.getUniformLocation(_prog, 'u_accent'); _uBg = gl.getUniformLocation(_prog, 'u_bg'); + _uLight = gl.getUniformLocation(_prog, 'u_light'); _uParticles = []; for (let i = 0; i < PARTICLE_COUNT; i++) { _uParticles.push(gl.getUniformLocation(_prog, `u_particles[${i}]`)); @@ -202,6 +212,7 @@ function _draw(time) { gl.uniform2f(_uRes, _canvas.width, _canvas.height); gl.uniform3f(_uAccent, _accent[0], _accent[1], _accent[2]); gl.uniform3f(_uBg, _bgColor[0], _bgColor[1], _bgColor[2]); + gl.uniform1f(_uLight, _isLight); for (let i = 0; i < PARTICLE_COUNT; i++) { const p = _particles[i]; @@ -241,6 +252,7 @@ export function updateBgAnimAccent(hex) { export function updateBgAnimTheme(isDark) { _bgColor = isDark ? [26 / 255, 26 / 255, 26 / 255] : [245 / 255, 245 / 255, 245 / 255]; + _isLight = isDark ? 0.0 : 1.0; } export function initBgAnim() { diff --git a/server/src/wled_controller/static/js/core/color-picker.js b/server/src/wled_controller/static/js/core/color-picker.js index 89c5711..828b414 100644 --- a/server/src/wled_controller/static/js/core/color-picker.js +++ b/server/src/wled_controller/static/js/core/color-picker.js @@ -88,15 +88,25 @@ window._cpToggle = function (id) { const card = pop.closest('.card, .template-card'); if (card) card.classList.toggle('cp-elevated', true); - // On small screens, if inside an overflow container (e.g. header toolbar), - // switch to fixed positioning so the popover isn't clipped. - const wrapper = pop.closest('.color-picker-wrapper'); - if (wrapper && window.innerWidth <= 600) { - const rect = wrapper.getBoundingClientRect(); + // On small/touch screens, move popover to body as a centered bottom-sheet + // to avoid any parent overflow/containment/positioning issues. + const isMobile = window.innerWidth <= 768 || ('ontouchstart' in window); + if (isMobile && pop.parentElement !== document.body) { + pop._cpOrigParent = pop.parentElement; + pop._cpOrigNext = pop.nextSibling; + document.body.appendChild(pop); + } + if (isMobile) { pop.style.position = 'fixed'; - pop.style.top = (rect.bottom + 4) + 'px'; - pop.style.right = Math.max(4, window.innerWidth - rect.right) + 'px'; - pop.style.left = 'auto'; + pop.style.left = '0'; + pop.style.right = '0'; + 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'); } @@ -118,6 +128,18 @@ function _cpClosePopover(pop) { pop.style.top = ''; pop.style.right = ''; 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'); if (card) card.classList.remove('cp-elevated');