Restyle main page: collapsible sections, theme-aware ticks, UI polish
Some checks failed
Validate / validate (push) Failing after 9s

- Make Devices and Displays sections collapsible with persistent state
- Move WLED config tip from footer to under Devices heading
- Add theme-aware colors for calibration canvas ticks and chevrons
- Rename sections to "Devices" and "Displays" with emoji prefix icons
- Fix display layout scaling to fill available space
- Remove unused footer-tip and modal code

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-09 01:57:43 +03:00
parent 57e6754461
commit c9929e3b7f
5 changed files with 116 additions and 46 deletions

View File

@@ -164,6 +164,9 @@ document.addEventListener('DOMContentLoaded', async () => {
// Show content now that translations are loaded
document.body.style.visibility = 'visible';
// Restore collapsible section states
initCollapsibleSections();
// Load API key from localStorage
apiKey = localStorage.getItem('wled_api_key');
@@ -306,6 +309,27 @@ async function loadDisplays() {
}
}
function toggleSection(name) {
const header = document.querySelector(`.collapsible-header[data-section="${name}"]`);
const content = document.getElementById(`${name}-content`);
if (!header || !content) return;
const collapsed = !header.classList.contains('collapsed');
header.classList.toggle('collapsed', collapsed);
content.classList.toggle('collapsed', collapsed);
localStorage.setItem(`section_${name}_collapsed`, collapsed ? '1' : '0');
}
function initCollapsibleSections() {
document.querySelectorAll('.collapsible-header[data-section]').forEach(header => {
const name = header.getAttribute('data-section');
const content = document.getElementById(`${name}-content`);
if (localStorage.getItem(`section_${name}_collapsed`) === '1' && content) {
header.classList.add('collapsed');
content.classList.add('collapsed');
}
});
}
function renderDisplayLayout(displays) {
const canvas = document.getElementById('display-layout-canvas');
@@ -329,11 +353,10 @@ function renderDisplayLayout(displays) {
// Scale factor to fit in canvas (respect available width, maintain aspect ratio)
const availableWidth = canvas.clientWidth - 60; // account for padding
const maxCanvasWidth = Math.min(600, availableWidth);
const maxCanvasHeight = 450;
const scaleX = maxCanvasWidth / totalWidth;
const maxCanvasHeight = 350;
const scaleX = availableWidth / totalWidth;
const scaleY = maxCanvasHeight / totalHeight;
const scale = Math.min(scaleX, scaleY, 0.3); // Max 0.3 scale to keep monitors reasonably sized
const scale = Math.min(scaleX, scaleY);
const canvasWidth = totalWidth * scale;
const canvasHeight = totalHeight * scale;
@@ -1199,6 +1222,12 @@ function renderCalibrationCanvas() {
const totalLeds = calibration.leds_top + calibration.leds_right + calibration.leds_bottom + calibration.leds_left;
// Theme-aware colors
const isDark = document.documentElement.getAttribute('data-theme') !== 'light';
const tickStroke = isDark ? 'rgba(255, 255, 255, 0.4)' : 'rgba(0, 0, 0, 0.3)';
const tickFill = isDark ? 'rgba(255, 255, 255, 0.65)' : 'rgba(0, 0, 0, 0.6)';
const chevronStroke = isDark ? 'rgba(255, 255, 255, 0.6)' : 'rgba(0, 0, 0, 0.4)';
// Edge bar geometry (matches CSS: corner zones 56px × 36px fixed)
const cw = 56;
const ch = 36;
@@ -1287,9 +1316,9 @@ function renderCalibrationCanvas() {
// Tick styling
const tickLen = 5;
ctx.strokeStyle = 'rgba(255, 255, 255, 0.4)';
ctx.strokeStyle = tickStroke;
ctx.lineWidth = 1;
ctx.fillStyle = 'rgba(255, 255, 255, 0.65)';
ctx.fillStyle = tickFill;
ctx.font = '12px -apple-system, BlinkMacSystemFont, sans-serif';
labelsToShow.forEach(i => {
@@ -1347,7 +1376,7 @@ function renderCalibrationCanvas() {
ctx.translate(mx, my);
ctx.rotate(angle);
ctx.fillStyle = 'rgba(76, 175, 80, 0.85)';
ctx.strokeStyle = 'rgba(255, 255, 255, 0.6)';
ctx.strokeStyle = chevronStroke;
ctx.lineWidth = 1;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';

View File

@@ -32,31 +32,13 @@
</div>
</header>
<section class="displays-section">
<h2 data-i18n="displays.layout">Display Layout</h2>
<div class="display-layout-preview">
<div id="display-layout-canvas" class="display-layout-canvas">
<div class="loading" data-i18n="displays.loading">Loading layout...</div>
</div>
</div>
<div id="displays-list" style="display: none;"></div>
</section>
<section class="devices-section">
<div class="section-header">
<h2 data-i18n="devices.title">WLED Devices</h2>
<h2 class="collapsible-header" onclick="toggleSection('devices')" data-section="devices"><span data-i18n="devices.title">💡 Devices</span><span class="collapse-chevron"></span></h2>
<button class="btn btn-icon btn-primary" onclick="showAddDevice()" data-i18n-title="devices.add" title="Add New Device">+</button>
</div>
<div id="devices-list" class="devices-grid">
<div class="loading" data-i18n="devices.loading">Loading devices...</div>
</div>
</section>
<footer class="app-footer">
<div class="footer-content">
<p class="footer-tip">
<div id="devices-content" class="collapsible-content">
<p class="section-tip">
<strong><span data-i18n="devices.wled_config">WLED Configuration:</span></strong> <span data-i18n="devices.wled_note">Configure your WLED device (effects, segments, color order, power limits, etc.) using the</span>
<a href="https://kno.wled.ge/" target="_blank" rel="noopener" data-i18n="devices.wled_link">official WLED app</a>
<span data-i18n="devices.wled_note_or">or the built-in</span>
@@ -64,6 +46,26 @@
<span data-i18n="devices.wled_note_webui">(open your device's IP in a browser).</span>
<span data-i18n="devices.wled_note2">This controller sends pixel color data and controls brightness per device.</span>
</p>
<div id="devices-list" class="devices-grid">
<div class="loading" data-i18n="devices.loading">Loading devices...</div>
</div>
</div>
</section>
<section class="displays-section">
<h2 class="collapsible-header" onclick="toggleSection('displays')" data-section="displays"><span data-i18n="displays.layout">🖥️ Displays</span><span class="collapse-chevron"></span></h2>
<div id="displays-content" class="collapsible-content">
<div class="display-layout-preview">
<div id="display-layout-canvas" class="display-layout-canvas">
<div class="loading" data-i18n="displays.loading">Loading layout...</div>
</div>
</div>
<div id="displays-list" style="display: none;"></div>
</div>
</section>
<footer class="app-footer">
<div class="footer-content">
<p>
Created by <strong>Alexei Dolgolyov</strong>
<a href="mailto:dolgolyov.alexei@gmail.com">dolgolyov.alexei@gmail.com</a>

View File

@@ -19,7 +19,7 @@
"auth.logout.success": "Logged out successfully",
"auth.please_login": "Please login to view",
"displays.title": "Available Displays",
"displays.layout": "Display Layout",
"displays.layout": "\uD83D\uDDA5\uFE0F Displays",
"displays.information": "Display Information",
"displays.legend.primary": "Primary Display",
"displays.legend.secondary": "Secondary Display",
@@ -32,7 +32,7 @@
"displays.loading": "Loading displays...",
"displays.none": "No displays available",
"displays.failed": "Failed to load displays",
"devices.title": "WLED Devices",
"devices.title": "\uD83D\uDCA1 Devices",
"devices.add": "Add New Device",
"devices.loading": "Loading devices...",
"devices.none": "No devices configured",

View File

@@ -19,7 +19,7 @@
"auth.logout.success": "Выход выполнен успешно",
"auth.please_login": "Пожалуйста, войдите для просмотра",
"displays.title": "Доступные Дисплеи",
"displays.layout": "Расположение Дисплеев",
"displays.layout": "\uD83D\uDDA5\uFE0F Дисплеи",
"displays.information": "Информация о Дисплеях",
"displays.legend.primary": "Основной Дисплей",
"displays.legend.secondary": "Вторичный Дисплей",
@@ -32,7 +32,7 @@
"displays.loading": "Загрузка дисплеев...",
"displays.none": "Нет доступных дисплеев",
"displays.failed": "Не удалось загрузить дисплеи",
"devices.title": "WLED Устройства",
"devices.title": "\uD83D\uDCA1 Устройства",
"devices.add": "Добавить Новое Устройство",
"devices.loading": "Загрузка устройств...",
"devices.none": "Устройства не настроены",

View File

@@ -374,6 +374,42 @@ section {
font-size: 1.2rem;
}
/* Collapsible sections */
.collapsible-header {
cursor: pointer;
user-select: none;
}
.collapsible-header:hover {
opacity: 0.8;
}
.collapsible-header .collapse-chevron {
display: inline-block;
font-size: 0.6em;
margin-left: 6px;
transition: transform 0.2s ease;
vertical-align: middle;
}
.collapsible-header.collapsed .collapse-chevron {
transform: rotate(-90deg);
}
.collapsible-content {
overflow: hidden;
transition: max-height 0.3s ease, opacity 0.2s ease;
max-height: 2000px;
opacity: 1;
}
.collapsible-content.collapsed {
max-height: 0;
opacity: 0;
margin: 0;
padding: 0;
}
/* Display Layout Visualization */
.display-layout-preview {
background: var(--card-bg);
@@ -527,13 +563,29 @@ section {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 15px;
margin-bottom: 8px;
}
.section-header h2 {
margin-bottom: 0;
}
.section-tip {
font-size: 0.82rem;
color: var(--text-secondary);
margin: 0 0 15px 0;
line-height: 1.5;
padding: 8px 12px;
background: rgba(33, 150, 243, 0.08);
border-left: 3px solid var(--info-color, #2196F3);
border-radius: 0 6px 6px 0;
}
.section-tip a {
color: var(--info-color, #2196F3);
text-decoration: underline;
}
.form-group {
margin-bottom: 15px;
}
@@ -794,19 +846,6 @@ input:-webkit-autofill:focus {
margin: 0;
}
.footer-tip {
margin-bottom: 12px !important;
padding: 10px 16px;
background: rgba(33, 150, 243, 0.08);
border-radius: 6px;
font-size: 0.85rem;
display: inline-block;
}
.footer-tip a {
color: var(--info-color) !important;
text-decoration: underline;
}
.footer-content strong {
color: var(--text-color);