Add CPU/GPU names on perf charts, reusable color picker, and header toolbar redesign

- Show CPU and GPU model names as overlays on performance chart cards
- Add cpu_name field to performance API with cross-platform detection
- Extract reusable color-picker popover module (9 presets + custom picker)
- Per-chart color customization for CPU/RAM/GPU performance charts
- Redesign header: compact toolbar container with icon-only buttons
- Compact language dropdown (EN/RU/ZH), icon-only login/logout
- Use accent color for FPS charts, range slider accent, dashboard icons

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-27 21:12:13 +03:00
parent 2bca119ad4
commit 6a7ba3d0b7
13 changed files with 361 additions and 127 deletions

View File

@@ -26,52 +26,54 @@
<h1 data-i18n="app.title">LED Grab</h1>
<span id="server-version"><span id="version-number"></span></span>
</div>
<div class="server-info">
<div class="header-toolbar">
<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">
<span class="header-toolbar-sep"></span>
<button class="header-btn" 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)">
<button class="header-btn" 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">
<button class="header-btn" onclick="toggleTheme()" data-i18n-title="theme.toggle" title="Toggle theme">
<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>
<span class="color-picker-wrapper" id="cp-wrap-accent">
<button class="header-btn" onclick="event.stopPropagation(); window._cpToggle('accent')" data-i18n-title="accent.title" title="Accent color">
<span id="cp-swatch-accent" class="color-picker-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 class="color-picker-popover anchor-right" id="cp-pop-accent" style="display:none" onclick="event.stopPropagation()">
<div class="color-picker-grid">
<button class="color-picker-dot" style="background:#4CAF50" onclick="event.stopPropagation(); window._cpPick('accent','#4CAF50')"></button>
<button class="color-picker-dot" style="background:#7C4DFF" onclick="event.stopPropagation(); window._cpPick('accent','#7C4DFF')"></button>
<button class="color-picker-dot" style="background:#FF6D00" onclick="event.stopPropagation(); window._cpPick('accent','#FF6D00')"></button>
<button class="color-picker-dot" style="background:#E91E63" onclick="event.stopPropagation(); window._cpPick('accent','#E91E63')"></button>
<button class="color-picker-dot" style="background:#00BCD4" onclick="event.stopPropagation(); window._cpPick('accent','#00BCD4')"></button>
<button class="color-picker-dot" style="background:#FF5252" onclick="event.stopPropagation(); window._cpPick('accent','#FF5252')"></button>
<button class="color-picker-dot" style="background:#26A69A" onclick="event.stopPropagation(); window._cpPick('accent','#26A69A')"></button>
<button class="color-picker-dot" style="background:#2196F3" onclick="event.stopPropagation(); window._cpPick('accent','#2196F3')"></button>
<button class="color-picker-dot" style="background:#FFC107" onclick="event.stopPropagation(); window._cpPick('accent','#FFC107')"></button>
</div>
<div class="accent-custom">
<input type="color" id="accent-picker" value="#4CAF50"
oninput="pickAccent(this.value)" onchange="pickAccent(this.value)">
<div class="color-picker-custom" onclick="this.querySelector('input').click()">
<input type="color" id="cp-native-accent" value="#4CAF50"
oninput="event.stopPropagation(); window._cpPick('accent',this.value)" onchange="event.stopPropagation(); window._cpPick('accent',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">
</span>
<button class="header-btn" onclick="openSettingsModal()" data-i18n-title="settings.title" title="Settings">
<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>
<option value="ru">Русский</option>
<option value="zh">中文</option>
<select id="locale-select" class="header-locale" onchange="changeLocale()" data-i18n-title="locale.change" title="Change language">
<option value="en">EN</option>
<option value="ru">RU</option>
<option value="zh">ZH</option>
</select>
<button id="login-btn" class="btn btn-primary" onclick="showLogin()" style="display: none; padding: 4px 12px; font-size: 0.8rem;">
<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>
<span class="header-toolbar-sep"></span>
<button id="login-btn" class="header-btn" onclick="showLogin()" style="display: none" data-i18n-title="auth.login" title="Login">
<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>
</button>
<button id="logout-btn" class="btn btn-danger" onclick="logout()" style="display: none; padding: 4px 12px; font-size: 0.8rem;">
<button id="logout-btn" class="header-btn" onclick="logout()" style="display: none" data-i18n-title="auth.logout" title="Logout">
<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>
@@ -239,49 +241,37 @@
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));
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());
});
const swatch = document.getElementById('cp-swatch-accent');
if (swatch) swatch.style.background = hex;
const native = document.getElementById('cp-native-accent');
if (native) native.value = hex;
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());
});
}
}
// Bootstrap _cpToggle/_cpPick globals before the color-picker module loads
// (the module will overwrite them with proper versions that handle all pickers)
window._cpCallbacks = { accent: function(hex) { applyAccentColor(hex); } };
window._cpToggle = window._cpToggle || function(id) {
document.querySelectorAll('.color-picker-popover').forEach(p => {
if (p.id !== 'cp-pop-' + id) p.style.display = 'none';
});
const pop = document.getElementById('cp-pop-' + id);
if (pop) pop.style.display = pop.style.display === 'none' ? '' : 'none';
};
window._cpPick = window._cpPick || function(id, hex) {
var s = document.getElementById('cp-swatch-' + id);
if (s) s.style.background = hex;
var n = document.getElementById('cp-native-' + id);
if (n) n.value = hex;
var p = document.getElementById('cp-pop-' + id);
if (p) p.style.display = 'none';
if (window._cpCallbacks[id]) window._cpCallbacks[id](hex);
};
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';
}
// Close all color pickers on outside click
document.addEventListener('click', function() {
document.querySelectorAll('.color-picker-popover').forEach(function(p) { p.style.display = 'none'; });
});
const savedAccent = localStorage.getItem('accentColor');