Replace sections with tabs, add device card to grid
Some checks failed
Validate / validate (push) Failing after 9s
Some checks failed
Validate / validate (push) Failing after 9s
- Switch Devices/Displays from collapsible sections to tab layout - Remember active tab in localStorage - Re-render display layout when switching to Displays tab - Replace floating "+" button with dashed add-device card in grid - Rename sections to "Devices" and "Displays" Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -164,8 +164,8 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||||||
// Show content now that translations are loaded
|
// Show content now that translations are loaded
|
||||||
document.body.style.visibility = 'visible';
|
document.body.style.visibility = 'visible';
|
||||||
|
|
||||||
// Restore collapsible section states
|
// Restore active tab
|
||||||
initCollapsibleSections();
|
initTabs();
|
||||||
|
|
||||||
// Load API key from localStorage
|
// Load API key from localStorage
|
||||||
apiKey = localStorage.getItem('wled_api_key');
|
apiKey = localStorage.getItem('wled_api_key');
|
||||||
@@ -298,7 +298,8 @@ async function loadDisplays() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render visual layout
|
// Cache and render visual layout
|
||||||
|
_cachedDisplays = data.displays;
|
||||||
renderDisplayLayout(data.displays);
|
renderDisplayLayout(data.displays);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load displays:', error);
|
console.error('Failed to load displays:', error);
|
||||||
@@ -309,25 +310,22 @@ async function loadDisplays() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleSection(name) {
|
let _cachedDisplays = null;
|
||||||
const header = document.querySelector(`.collapsible-header[data-section="${name}"]`);
|
|
||||||
const content = document.getElementById(`${name}-content`);
|
function switchTab(name) {
|
||||||
if (!header || !content) return;
|
document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.toggle('active', btn.dataset.tab === name));
|
||||||
const collapsed = !header.classList.contains('collapsed');
|
document.querySelectorAll('.tab-panel').forEach(panel => panel.classList.toggle('active', panel.id === `tab-${name}`));
|
||||||
header.classList.toggle('collapsed', collapsed);
|
localStorage.setItem('activeTab', name);
|
||||||
content.classList.toggle('collapsed', collapsed);
|
if (name === 'displays' && _cachedDisplays) {
|
||||||
localStorage.setItem(`section_${name}_collapsed`, collapsed ? '1' : '0');
|
requestAnimationFrame(() => renderDisplayLayout(_cachedDisplays));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function initCollapsibleSections() {
|
function initTabs() {
|
||||||
document.querySelectorAll('.collapsible-header[data-section]').forEach(header => {
|
const saved = localStorage.getItem('activeTab');
|
||||||
const name = header.getAttribute('data-section');
|
if (saved && document.getElementById(`tab-${saved}`)) {
|
||||||
const content = document.getElementById(`${name}-content`);
|
switchTab(saved);
|
||||||
if (localStorage.getItem(`section_${name}_collapsed`) === '1' && content) {
|
}
|
||||||
header.classList.add('collapsed');
|
|
||||||
content.classList.add('collapsed');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderDisplayLayout(displays) {
|
function renderDisplayLayout(displays) {
|
||||||
@@ -409,7 +407,10 @@ async function loadDevices() {
|
|||||||
const container = document.getElementById('devices-list');
|
const container = document.getElementById('devices-list');
|
||||||
|
|
||||||
if (!devices || devices.length === 0) {
|
if (!devices || devices.length === 0) {
|
||||||
container.innerHTML = `<div class="loading">${t('devices.none')}</div>`;
|
container.innerHTML = `<div class="card add-device-card" onclick="showAddDevice()">
|
||||||
|
<div class="add-device-icon">+</div>
|
||||||
|
<div class="add-device-label">${t('devices.add')}</div>
|
||||||
|
</div>`;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -473,7 +474,11 @@ async function loadDevices() {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
container.innerHTML = devicesWithState.map(device => createDeviceCard(device)).join('');
|
container.innerHTML = devicesWithState.map(device => createDeviceCard(device)).join('')
|
||||||
|
+ `<div class="card add-device-card" onclick="showAddDevice()">
|
||||||
|
<div class="add-device-icon">+</div>
|
||||||
|
<div class="add-device-label">${t('devices.add')}</div>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
// Update footer WLED Web UI link with first device's URL
|
// Update footer WLED Web UI link with first device's URL
|
||||||
const webuiLink = document.querySelector('.wled-webui-link');
|
const webuiLink = document.querySelector('.wled-webui-link');
|
||||||
|
|||||||
@@ -32,12 +32,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<section class="devices-section">
|
<div class="tabs">
|
||||||
<div class="section-header">
|
<div class="tab-bar">
|
||||||
<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="tab-btn active" data-tab="devices" onclick="switchTab('devices')"><span data-i18n="devices.title">💡 Devices</span></button>
|
||||||
<button class="btn btn-icon btn-primary" onclick="showAddDevice()" data-i18n-title="devices.add" title="Add New Device">+</button>
|
<button class="tab-btn" data-tab="displays" onclick="switchTab('displays')"><span data-i18n="displays.layout">🖥️ Displays</span></button>
|
||||||
</div>
|
</div>
|
||||||
<div id="devices-content" class="collapsible-content">
|
|
||||||
|
<div class="tab-panel active" id="tab-devices">
|
||||||
<p class="section-tip">
|
<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>
|
<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>
|
<a href="https://kno.wled.ge/" target="_blank" rel="noopener" data-i18n="devices.wled_link">official WLED app</a>
|
||||||
@@ -50,11 +51,8 @@
|
|||||||
<div class="loading" data-i18n="devices.loading">Loading devices...</div>
|
<div class="loading" data-i18n="devices.loading">Loading devices...</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="displays-section">
|
<div class="tab-panel" id="tab-displays">
|
||||||
<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 class="display-layout-preview">
|
||||||
<div id="display-layout-canvas" class="display-layout-canvas">
|
<div id="display-layout-canvas" class="display-layout-canvas">
|
||||||
<div class="loading" data-i18n="displays.loading">Loading layout...</div>
|
<div class="loading" data-i18n="displays.loading">Loading layout...</div>
|
||||||
@@ -62,7 +60,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="displays-list" style="display: none;"></div>
|
<div id="displays-list" style="display: none;"></div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</div>
|
||||||
|
|
||||||
<footer class="app-footer">
|
<footer class="app-footer">
|
||||||
<div class="footer-content">
|
<div class="footer-content">
|
||||||
|
|||||||
@@ -168,6 +168,41 @@ section {
|
|||||||
grid-column: 1 / -1;
|
grid-column: 1 / -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.add-device-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 2px dashed var(--border-color);
|
||||||
|
background: transparent;
|
||||||
|
min-height: 160px;
|
||||||
|
transition: border-color 0.2s, background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-device-card:hover {
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
background: rgba(33, 150, 243, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-device-icon {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
font-weight: 300;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
line-height: 1;
|
||||||
|
transition: color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-device-card:hover .add-device-icon {
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-device-label {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
background: var(--card-bg);
|
background: var(--card-bg);
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
@@ -374,40 +409,43 @@ section {
|
|||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Collapsible sections */
|
/* Tabs */
|
||||||
.collapsible-header {
|
.tab-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
border-bottom: 2px solid var(--border-color);
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
padding: 10px 18px;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-secondary);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
user-select: none;
|
border-bottom: 2px solid transparent;
|
||||||
|
margin-bottom: -2px;
|
||||||
|
transition: color 0.2s, border-color 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.collapsible-header:hover {
|
.tab-btn:hover {
|
||||||
opacity: 0.8;
|
color: var(--text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.collapsible-header .collapse-chevron {
|
.tab-btn.active {
|
||||||
display: inline-block;
|
color: var(--primary-color);
|
||||||
font-size: 0.6em;
|
border-bottom-color: var(--primary-color);
|
||||||
margin-left: 6px;
|
|
||||||
transition: transform 0.2s ease;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.collapsible-header.collapsed .collapse-chevron {
|
.tab-panel {
|
||||||
transform: rotate(-90deg);
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.collapsible-content {
|
.tab-panel.active {
|
||||||
overflow: hidden;
|
display: block;
|
||||||
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 Visualization */
|
||||||
|
|||||||
Reference in New Issue
Block a user