Replace all emoji icons with Lucide SVGs, add accent color picker

- Replace all emoji characters across WebUI with inline Lucide SVG icons
  for cross-platform consistency (icon paths in icon-paths.js)
- Add accent color picker popover with 9 preset colors + custom picker,
  persisted to localStorage, updates all CSS custom properties
- Remove subtab separator line for cleaner look
- Color badge icons with accent color for visual pop
- Remove processing badge from target cards
- Fix hardcoded #4CAF50 in FPS labels and active badges to use CSS vars
- Replace CSS content emoji (▶) with pure CSS triangle

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-27 16:14:18 +03:00
parent efb6cf7ce6
commit c262ec0775
39 changed files with 634 additions and 311 deletions

View File

@@ -29,16 +29,39 @@
<div class="server-info">
<a href="/docs" target="_blank" class="header-link" data-i18n-title="app.api_docs" title="API Docs">API</a>
<button class="search-toggle" id="tour-restart-btn" onclick="startGettingStartedTutorial()" data-i18n-title="tour.restart" title="Restart tutorial">
?
<svg class="icon" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><path d="M12 17h.01"/></svg>
</button>
<button class="search-toggle" onclick="openCommandPalette()" data-i18n-title="search.open" title="Search (Ctrl+K)">
🔍
<svg class="icon" viewBox="0 0 24 24"><path d="m21 21-4.34-4.34"/><circle cx="11" cy="11" r="8"/></svg>
</button>
<button class="theme-toggle" onclick="toggleTheme()" data-i18n-title="theme.toggle" title="Toggle theme">
<span id="theme-icon">🌙</span>
<span id="theme-icon"><svg class="icon" viewBox="0 0 24 24"><circle cx="12" cy="12" r="4"/><path d="M12 2v2"/><path d="M12 20v2"/><path d="m4.93 4.93 1.41 1.41"/><path d="m17.66 17.66 1.41 1.41"/><path d="M2 12h2"/><path d="M20 12h2"/><path d="m6.34 17.66-1.41 1.41"/><path d="m19.07 4.93-1.41 1.41"/></svg></span>
</button>
<div class="accent-wrapper">
<button class="search-toggle" onclick="toggleAccentPicker()" data-i18n-title="accent.title" title="Accent color">
<span id="accent-swatch" class="accent-swatch" style="background: var(--primary-color)"></span>
</button>
<div id="accent-popover" class="accent-popover" style="display:none">
<div class="accent-grid">
<button class="accent-dot" style="background:#4CAF50" onclick="pickAccent('#4CAF50')"></button>
<button class="accent-dot" style="background:#7C4DFF" onclick="pickAccent('#7C4DFF')"></button>
<button class="accent-dot" style="background:#FF6D00" onclick="pickAccent('#FF6D00')"></button>
<button class="accent-dot" style="background:#E91E63" onclick="pickAccent('#E91E63')"></button>
<button class="accent-dot" style="background:#00BCD4" onclick="pickAccent('#00BCD4')"></button>
<button class="accent-dot" style="background:#FF5252" onclick="pickAccent('#FF5252')"></button>
<button class="accent-dot" style="background:#26A69A" onclick="pickAccent('#26A69A')"></button>
<button class="accent-dot" style="background:#2196F3" onclick="pickAccent('#2196F3')"></button>
<button class="accent-dot" style="background:#FFC107" onclick="pickAccent('#FFC107')"></button>
</div>
<div class="accent-custom">
<input type="color" id="accent-picker" value="#4CAF50"
oninput="pickAccent(this.value)" onchange="pickAccent(this.value)">
<span data-i18n="accent.custom">Custom</span>
</div>
</div>
</div>
<button class="search-toggle" onclick="openSettingsModal()" data-i18n-title="settings.title" title="Settings">
&#x2699;&#xFE0F;
<svg class="icon" viewBox="0 0 24 24"><path d="M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915"/><circle cx="12" cy="12" r="3"/></svg>
</button>
<select id="locale-select" onchange="changeLocale()" data-i18n-title="locale.change" title="Change language" style="padding: 4px 8px; border: 1px solid var(--border-color); border-radius: 4px; background: var(--bg-color); color: var(--text-color); font-size: 0.8rem; cursor: pointer;">
<option value="en">English</option>
@@ -46,20 +69,20 @@
<option value="zh">中文</option>
</select>
<button id="login-btn" class="btn btn-primary" onclick="showLogin()" style="display: none; padding: 4px 12px; font-size: 0.8rem;">
🔑 <span data-i18n="auth.login">Login</span>
<svg class="icon" viewBox="0 0 24 24"><path d="M2.586 17.414A2 2 0 0 0 2 18.828V21a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h1a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h.172a2 2 0 0 0 1.414-.586l.814-.814a6.5 6.5 0 1 0-4-4z"/><circle cx="16.5" cy="7.5" r=".5" fill="currentColor"/></svg> <span data-i18n="auth.login">Login</span>
</button>
<button id="logout-btn" class="btn btn-danger" onclick="logout()" style="display: none; padding: 4px 12px; font-size: 0.8rem;">
🚪
<svg class="icon" viewBox="0 0 24 24"><path d="m16 17 5-5-5-5"/><path d="M21 12H9"/><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/></svg>
</button>
</div>
</header>
<div class="tabs">
<div class="tab-bar" role="tablist">
<button class="tab-btn" data-tab="dashboard" onclick="switchTab('dashboard')" role="tab" aria-selected="true" aria-controls="tab-dashboard" id="tab-btn-dashboard" title="Ctrl+1"><span data-i18n="dashboard.title">📊 Dashboard</span></button>
<button class="tab-btn" data-tab="profiles" onclick="switchTab('profiles')" role="tab" aria-selected="false" aria-controls="tab-profiles" id="tab-btn-profiles" title="Ctrl+2"><span data-i18n="profiles.title">📋 Profiles</span><span class="tab-badge" id="tab-badge-profiles" style="display:none"></span></button>
<button class="tab-btn" data-tab="targets" onclick="switchTab('targets')" role="tab" aria-selected="false" aria-controls="tab-targets" id="tab-btn-targets" title="Ctrl+3"><span data-i18n="targets.title">Targets</span><span class="tab-badge" id="tab-badge-targets" style="display:none"></span></button>
<button class="tab-btn" data-tab="streams" onclick="switchTab('streams')" role="tab" aria-selected="false" aria-controls="tab-streams" id="tab-btn-streams" title="Ctrl+4"><span data-i18n="streams.title">📺 Sources</span></button>
<button class="tab-btn" data-tab="dashboard" onclick="switchTab('dashboard')" role="tab" aria-selected="true" aria-controls="tab-dashboard" id="tab-btn-dashboard" title="Ctrl+1"><svg class="icon" viewBox="0 0 24 24"><rect width="7" height="9" x="3" y="3" rx="1"/><rect width="7" height="5" x="14" y="3" rx="1"/><rect width="7" height="9" x="14" y="12" rx="1"/><rect width="7" height="5" x="3" y="16" rx="1"/></svg> <span data-i18n="dashboard.title">Dashboard</span></button>
<button class="tab-btn" data-tab="profiles" onclick="switchTab('profiles')" role="tab" aria-selected="false" aria-controls="tab-profiles" id="tab-btn-profiles" title="Ctrl+2"><svg class="icon" viewBox="0 0 24 24"><rect width="8" height="4" x="8" y="2" rx="1" ry="1"/><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/><path d="M12 11h4"/><path d="M12 16h4"/><path d="M8 11h.01"/><path d="M8 16h.01"/></svg> <span data-i18n="profiles.title">Profiles</span><span class="tab-badge" id="tab-badge-profiles" style="display:none"></span></button>
<button class="tab-btn" data-tab="targets" onclick="switchTab('targets')" role="tab" aria-selected="false" aria-controls="tab-targets" id="tab-btn-targets" title="Ctrl+3"><svg class="icon" viewBox="0 0 24 24"><path d="M4 14a1 1 0 0 1-.78-1.63l9.9-10.2a.5.5 0 0 1 .86.46l-1.92 6.02A1 1 0 0 0 13 10h7a1 1 0 0 1 .78 1.63l-9.9 10.2a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14z"/></svg> <span data-i18n="targets.title">Targets</span><span class="tab-badge" id="tab-badge-targets" style="display:none"></span></button>
<button class="tab-btn" data-tab="streams" onclick="switchTab('streams')" role="tab" aria-selected="false" aria-controls="tab-streams" id="tab-btn-streams" title="Ctrl+4"><svg class="icon" viewBox="0 0 24 24"><path d="m17 2-5 5-5-5"/><rect width="20" height="15" x="2" y="7" rx="2"/></svg> <span data-i18n="streams.title">Sources</span></button>
</div>
<div class="tab-panel" id="tab-dashboard" role="tabpanel" aria-labelledby="tab-btn-dashboard">
@@ -158,7 +181,9 @@
function updateThemeIcon(theme) {
const icon = document.getElementById('theme-icon');
icon.textContent = theme === 'dark' ? '☀️' : '🌙';
icon.innerHTML = theme === 'dark'
? '<svg class="icon" viewBox="0 0 24 24"><circle cx="12" cy="12" r="4"/><path d="M12 2v2"/><path d="M12 20v2"/><path d="m4.93 4.93 1.41 1.41"/><path d="m17.66 17.66 1.41 1.41"/><path d="M2 12h2"/><path d="M20 12h2"/><path d="m6.34 17.66-1.41 1.41"/><path d="m19.07 4.93-1.41 1.41"/></svg>'
: '<svg class="icon" viewBox="0 0 24 24"><path d="M20.985 12.486a9 9 0 1 1-9.473-9.472c.405-.022.617.46.402.803a6 6 0 0 0 8.268 8.268c.344-.215.825-.004.803.401"/></svg>';
}
function toggleTheme() {
@@ -168,9 +193,90 @@
document.documentElement.setAttribute('data-theme', newTheme);
localStorage.setItem('theme', newTheme);
updateThemeIcon(newTheme);
// Re-derive accent text variant for the new theme
const accent = localStorage.getItem('accentColor');
if (accent) applyAccentColor(accent, true);
showToast(`Switched to ${newTheme} theme`, 'info');
}
// Initialize accent color
function adjustLightness(hex, amount) {
const r = parseInt(hex.slice(1,3),16)/255;
const g = parseInt(hex.slice(3,5),16)/255;
const b = parseInt(hex.slice(5,7),16)/255;
const max = Math.max(r,g,b), min = Math.min(r,g,b);
let h, s, l = (max+min)/2;
if (max===min) { h=s=0; } else {
const d = max-min;
s = l>0.5 ? d/(2-max-min) : d/(max+min);
if (max===r) h=((g-b)/d+(g<b?6:0))/6;
else if (max===g) h=((b-r)/d+2)/6;
else h=((r-g)/d+4)/6;
}
l = Math.max(0, Math.min(1, l + amount/100));
const hue2rgb = (p,q,t) => { if(t<0)t+=1; if(t>1)t-=1; if(t<1/6)return p+(q-p)*6*t; if(t<1/2)return q; if(t<2/3)return p+(q-p)*(2/3-t)*6; return p; };
let rr,gg,bb;
if (s===0) { rr=gg=bb=l; } else {
const q = l<0.5 ? l*(1+s) : l+s-l*s, p = 2*l-q;
rr=hue2rgb(p,q,h+1/3); gg=hue2rgb(p,q,h); bb=hue2rgb(p,q,h-1/3);
}
return '#'+[rr,gg,bb].map(x=>Math.round(x*255).toString(16).padStart(2,'0')).join('');
}
function applyAccentColor(hex, silent) {
const root = document.documentElement;
root.style.setProperty('--primary-color', hex);
const theme = root.getAttribute('data-theme');
root.style.setProperty('--primary-text-color', adjustLightness(hex, theme === 'dark' ? 15 : -15));
root.style.setProperty('--primary-hover', adjustLightness(hex, 8));
document.getElementById('accent-swatch').style.background = hex;
document.getElementById('accent-picker').value = hex;
// Mark the active preset dot
document.querySelectorAll('.accent-dot').forEach(d => {
d.classList.toggle('active', d.style.background === hex || d.style.backgroundColor === hex
|| d.style.background.toLowerCase() === hex.toLowerCase());
});
localStorage.setItem('accentColor', hex);
if (!silent) showToast('Accent color updated', 'info');
}
function toggleAccentPicker() {
const pop = document.getElementById('accent-popover');
const show = pop.style.display === 'none';
pop.style.display = show ? '' : 'none';
if (show) {
// Mark active dot on open
const cur = localStorage.getItem('accentColor') || '#4CAF50';
document.querySelectorAll('.accent-dot').forEach(d => {
const dColor = d.style.backgroundColor || d.style.background;
d.classList.toggle('active', rgbToHex(dColor) === cur.toUpperCase());
});
}
}
function rgbToHex(rgb) {
if (rgb.startsWith('#')) return rgb.toUpperCase();
const m = rgb.match(/\d+/g);
if (!m) return rgb;
return '#' + m.slice(0,3).map(n => parseInt(n).toString(16).padStart(2,'0')).join('').toUpperCase();
}
function pickAccent(hex) {
applyAccentColor(hex);
document.getElementById('accent-popover').style.display = 'none';
}
// Close popover on outside click
document.addEventListener('click', function(e) {
const wrapper = document.querySelector('.accent-wrapper');
if (wrapper && !wrapper.contains(e.target)) {
document.getElementById('accent-popover').style.display = 'none';
}
});
const savedAccent = localStorage.getItem('accentColor');
if (savedAccent) applyAccentColor(savedAccent, true);
// Initialize auth state
function updateAuthUI() {
const apiKey = localStorage.getItem('wled_api_key');
@@ -228,10 +334,10 @@
const button = document.querySelector('.password-toggle');
if (input.type === 'password') {
input.type = 'text';
button.textContent = '🙈';
button.innerHTML = '<svg class="icon" viewBox="0 0 24 24"><path d="M10.733 5.076a10.744 10.744 0 0 1 11.205 6.575 1 1 0 0 1 0 .696 10.747 10.747 0 0 1-1.444 2.49"/><path d="M14.084 14.158a3 3 0 0 1-4.242-4.242"/><path d="M17.479 17.499a10.75 10.75 0 0 1-15.417-5.151 1 1 0 0 1 0-.696 10.75 10.75 0 0 1 4.446-5.143"/><path d="m2 2 20 20"/></svg>';
} else {
input.type = 'password';
button.textContent = '👁️';
button.innerHTML = '<svg class="icon" viewBox="0 0 24 24"><path d="M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0"/><circle cx="12" cy="12" r="3"/></svg>';
}
}