diff --git a/server/src/wled_controller/static/css/base.css b/server/src/wled_controller/static/css/base.css index 6f86db6..6495564 100644 --- a/server/src/wled_controller/static/css/base.css +++ b/server/src/wled_controller/static/css/base.css @@ -136,17 +136,7 @@ body.modal-open { animation: blobC 22s ease-in-out infinite alternate; } -[data-theme="dark"] { - --bg-anim-a: rgba(76, 175, 80, 0.10); - --bg-anim-b: rgba(33, 150, 243, 0.08); - --bg-anim-c: rgba(156, 39, 176, 0.07); -} - -[data-theme="light"] { - --bg-anim-a: rgba(76, 175, 80, 0.10); - --bg-anim-b: rgba(33, 150, 243, 0.08); - --bg-anim-c: rgba(156, 39, 176, 0.07); -} +/* Blob colors derived from accent via JS (--bg-anim-a/b/c set in index.html) */ @keyframes blobA { 0% { transform: translate(0, 0) scale(1); } diff --git a/server/src/wled_controller/static/css/cards.css b/server/src/wled_controller/static/css/cards.css index 11cddba..8ebc505 100644 --- a/server/src/wled_controller/static/css/cards.css +++ b/server/src/wled_controller/static/css/cards.css @@ -56,6 +56,7 @@ section { border-radius: 8px; padding: 12px 20px 20px; position: relative; + overflow: hidden; transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease; display: flex; flex-direction: column; @@ -66,6 +67,38 @@ section { transform: translateY(-2px); } +/* ── Card glare effect ── */ +.card-glare::after, +.template-card.card-glare::after { + content: ''; + position: absolute; + inset: 0; + border-radius: inherit; + pointer-events: none; + opacity: 0; + transition: opacity 0.3s ease; + background: radial-gradient( + circle 200px at var(--glare-x, 50%) var(--glare-y, 50%), + rgba(255, 255, 255, 0.035) 0%, + transparent 70% + ); + z-index: 1; +} + +.card-glare::after, +.template-card.card-glare::after { + opacity: 1; +} + +[data-theme="light"] .card-glare::after, +[data-theme="light"] .template-card.card-glare::after { + background: radial-gradient( + circle 200px at var(--glare-x, 50%) var(--glare-y, 50%), + rgba(255, 255, 255, 0.12) 0%, + transparent 70% + ); +} + /* ── Card entrance animation ── */ @keyframes cardEnter { from { opacity: 0; transform: translateY(12px); } diff --git a/server/src/wled_controller/static/css/streams.css b/server/src/wled_controller/static/css/streams.css index 6f90ac1..3e7c39c 100644 --- a/server/src/wled_controller/static/css/streams.css +++ b/server/src/wled_controller/static/css/streams.css @@ -13,6 +13,7 @@ border: 1px solid var(--border-color); border-radius: 8px; padding: 16px; + overflow: hidden; transition: box-shadow 0.2s ease, transform 0.2s ease; display: flex; flex-direction: column; diff --git a/server/src/wled_controller/static/js/app.js b/server/src/wled_controller/static/js/app.js index 38e6b0d..3d28855 100644 --- a/server/src/wled_controller/static/js/app.js +++ b/server/src/wled_controller/static/js/app.js @@ -10,6 +10,9 @@ import { Modal } from './core/modal.js'; import { loadServerInfo, loadDisplays, configureApiKey, startConnectionMonitor, stopConnectionMonitor } from './core/api.js'; import { t, initLocale, changeLocale } from './core/i18n.js'; +// Layer 1.5: visual effects +import { initCardGlare } from './core/card-glare.js'; + // Layer 2: ui import { toggleHint, lockBody, unlockBody, closeLightbox, @@ -528,6 +531,9 @@ document.addEventListener('DOMContentLoaded', async () => { // Show content now that translations are loaded and tabs are set document.body.style.visibility = 'visible'; + // Initialize card glare effect + initCardGlare(); + // Set CSS variable for sticky header height (header now includes tab bar) const headerEl = document.querySelector('header'); if (headerEl) { diff --git a/server/src/wled_controller/static/js/core/card-glare.js b/server/src/wled_controller/static/js/core/card-glare.js new file mode 100644 index 0000000..d073f2d --- /dev/null +++ b/server/src/wled_controller/static/js/core/card-glare.js @@ -0,0 +1,44 @@ +/** + * Card glare effect — cursor-tracking spotlight on .card and .template-card elements. + * + * Uses a single document-level mousemove listener (event delegation) and + * CSS custom properties (--glare-x, --glare-y) to position a radial gradient + * overlay via the ::after pseudo-element defined in cards.css. + */ + +const CARD_SEL = '.card, .template-card'; + +let _active = null; // currently illuminated card element + +function _onMove(e) { + const card = e.target.closest(CARD_SEL); + + if (card && !card.classList.contains('add-device-card')) { + const rect = card.getBoundingClientRect(); + const x = e.clientX - rect.left; + const y = e.clientY - rect.top; + card.style.setProperty('--glare-x', `${x}px`); + card.style.setProperty('--glare-y', `${y}px`); + + if (_active !== card) { + if (_active) _active.classList.remove('card-glare'); + card.classList.add('card-glare'); + _active = card; + } + } else if (_active) { + _active.classList.remove('card-glare'); + _active = null; + } +} + +function _onLeave() { + if (_active) { + _active.classList.remove('card-glare'); + _active = null; + } +} + +export function initCardGlare() { + document.addEventListener('mousemove', _onMove, { passive: true }); + document.addEventListener('mouseleave', _onLeave); +} diff --git a/server/src/wled_controller/templates/index.html b/server/src/wled_controller/templates/index.html index 55bea7b..0dd484b 100644 --- a/server/src/wled_controller/templates/index.html +++ b/server/src/wled_controller/templates/index.html @@ -289,6 +289,21 @@ return L > 0.36 ? '#1a1a1a' : '#ffffff'; } + function updateBgAnimColors(hex) { + const r = parseInt(hex.slice(1,3),16); + const g = parseInt(hex.slice(3,5),16); + const b = parseInt(hex.slice(5,7),16); + const root = document.documentElement; + const isDark = root.getAttribute('data-theme') !== 'light'; + const a = isDark ? 0.12 : 0.10; + // Blob A: accent color + root.style.setProperty('--bg-anim-a', `rgba(${r},${g},${b},${a})`); + // Blob B: hue-shifted ~120° toward blue (swap channels) + root.style.setProperty('--bg-anim-b', `rgba(${b},${r},${g},${a * 0.8})`); + // Blob C: hue-shifted ~240° toward magenta + root.style.setProperty('--bg-anim-c', `rgba(${g},${b},${r},${a * 0.7})`); + } + function applyAccentColor(hex, silent) { const root = document.documentElement; root.style.setProperty('--primary-color', hex); @@ -296,6 +311,7 @@ root.style.setProperty('--primary-text-color', adjustLightness(hex, theme === 'dark' ? 15 : -15)); root.style.setProperty('--primary-hover', adjustLightness(hex, 8)); root.style.setProperty('--primary-contrast', contrastColor(hex)); + updateBgAnimColors(hex); const swatch = document.getElementById('cp-swatch-accent'); if (swatch) swatch.style.background = hex; const native = document.getElementById('cp-native-accent'); @@ -332,6 +348,7 @@ const savedAccent = localStorage.getItem('accentColor'); if (savedAccent) applyAccentColor(savedAccent, true); + updateBgAnimColors(savedAccent || '#4CAF50'); // Initialize auth state function updateAuthUI() {