Replace flat sub-tab bars with tree sidebar navigation
Add TreeNav component that groups related entity types into a collapsible hierarchy for Targets and Sources tabs. Targets tree shows section-level leaves (Devices, Color Strips, LED Targets, KC Targets, Pattern Templates) with scroll-to-section on click. Sources tree groups into Picture, Audio, and Utility categories. Also fixes missing csAudioTemplates in stream section expand/collapse. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -25,12 +25,13 @@ import {
|
||||
ICON_CLONE, ICON_EDIT, ICON_START, ICON_STOP,
|
||||
ICON_LED, ICON_FPS, ICON_OVERLAY, ICON_LED_PREVIEW,
|
||||
ICON_GLOBE, ICON_RADIO, ICON_PLUG, ICON_FILM, ICON_SUN_DIM, ICON_TARGET_ICON, ICON_HELP,
|
||||
ICON_WARNING, ICON_PALETTE, ICON_WRENCH,
|
||||
ICON_WARNING, ICON_PALETTE, ICON_WRENCH, ICON_TEMPLATE,
|
||||
} from '../core/icons.js';
|
||||
import { EntitySelect } from '../core/entity-palette.js';
|
||||
import { wrapCard } from '../core/card-colors.js';
|
||||
import { TagInput, renderTagChips } from '../core/tag-input.js';
|
||||
import { CardSection } from '../core/card-sections.js';
|
||||
import { TreeNav } from '../core/tree-nav.js';
|
||||
import { updateSubTabHash, updateTabBadge } from './tabs.js';
|
||||
|
||||
// createPatternTemplateCard is imported via window.* to avoid circular deps
|
||||
@@ -133,13 +134,6 @@ function _updateTargetFpsChart(targetId, fpsTarget) {
|
||||
chart.update('none');
|
||||
}
|
||||
|
||||
function _updateSubTabCounts(subTabs) {
|
||||
subTabs.forEach(tab => {
|
||||
const btn = document.querySelector(`.target-sub-tab-btn[data-target-sub-tab="${tab.key}"] .stream-tab-count`);
|
||||
if (btn) btn.textContent = tab.count;
|
||||
});
|
||||
}
|
||||
|
||||
// --- Editor state ---
|
||||
let _editorCssSources = []; // populated when editor opens
|
||||
let _targetTagsInput = null;
|
||||
@@ -522,15 +516,35 @@ export async function saveTargetEditor() {
|
||||
|
||||
// ===== TARGETS TAB (WLED devices + targets combined) =====
|
||||
|
||||
let _treeTriggered = false;
|
||||
|
||||
const _targetsTree = new TreeNav('targets-tree-nav', {
|
||||
onSelect: (key, leaf) => {
|
||||
const subTab = leaf?.subTab || key;
|
||||
_treeTriggered = true;
|
||||
switchTargetSubTab(subTab);
|
||||
_treeTriggered = false;
|
||||
// Scroll to specific section
|
||||
if (leaf?.sectionKey) {
|
||||
requestAnimationFrame(() => {
|
||||
const section = document.querySelector(`[data-card-section="${leaf.sectionKey}"]`);
|
||||
if (section) section.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export function switchTargetSubTab(tabKey) {
|
||||
document.querySelectorAll('.target-sub-tab-btn').forEach(btn =>
|
||||
btn.classList.toggle('active', btn.dataset.targetSubTab === tabKey)
|
||||
);
|
||||
document.querySelectorAll('.target-sub-tab-panel').forEach(panel =>
|
||||
panel.classList.toggle('active', panel.id === `target-sub-tab-${tabKey}`)
|
||||
);
|
||||
localStorage.setItem('activeTargetSubTab', tabKey);
|
||||
updateSubTabHash('targets', tabKey);
|
||||
// Update tree active state (unless the tree triggered this switch)
|
||||
if (!_treeTriggered) {
|
||||
const leafKey = _targetsTree.getLeafForSubTab(tabKey);
|
||||
if (leafKey) _targetsTree.setActive(leafKey);
|
||||
}
|
||||
}
|
||||
|
||||
export function expandAllTargetSections() {
|
||||
@@ -631,14 +645,26 @@ export async function loadTargetsTab() {
|
||||
|
||||
const activeSubTab = localStorage.getItem('activeTargetSubTab') || 'led';
|
||||
|
||||
const subTabs = [
|
||||
{ key: 'led', icon: getTargetTypeIcon('led'), titleKey: 'targets.subtab.led', count: ledDevices.length + Object.keys(colorStripSourceMap).length + ledTargets.length },
|
||||
{ key: 'key_colors', icon: getTargetTypeIcon('key_colors'), titleKey: 'targets.subtab.key_colors', count: kcTargets.length + patternTemplates.length },
|
||||
// Build tree navigation structure
|
||||
const treeGroups = [
|
||||
{
|
||||
key: 'led_group', icon: getTargetTypeIcon('led'), titleKey: 'targets.subtab.led',
|
||||
children: [
|
||||
{ key: 'led-devices', titleKey: 'targets.section.devices', icon: getDeviceTypeIcon('wled'), count: ledDevices.length, subTab: 'led', sectionKey: 'led-devices' },
|
||||
{ key: 'led-css', titleKey: 'targets.section.color_strips', icon: getColorStripIcon('static'), count: Object.keys(colorStripSourceMap).length, subTab: 'led', sectionKey: 'led-css' },
|
||||
{ key: 'led-targets', titleKey: 'targets.section.targets', icon: getTargetTypeIcon('led'), count: ledTargets.length, subTab: 'led', sectionKey: 'led-targets' },
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'kc_group', icon: getTargetTypeIcon('key_colors'), titleKey: 'targets.subtab.key_colors',
|
||||
children: [
|
||||
{ key: 'kc-targets', titleKey: 'targets.section.key_colors', icon: getTargetTypeIcon('key_colors'), count: kcTargets.length, subTab: 'key_colors', sectionKey: 'kc-targets' },
|
||||
{ key: 'kc-patterns', titleKey: 'targets.section.pattern_templates', icon: ICON_TEMPLATE, count: patternTemplates.length, subTab: 'key_colors', sectionKey: 'kc-patterns' },
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
const tabBar = `<div class="stream-tab-bar">${subTabs.map(tab =>
|
||||
`<button class="target-sub-tab-btn stream-tab-btn${tab.key === activeSubTab ? ' active' : ''}" data-target-sub-tab="${tab.key}" onclick="switchTargetSubTab('${tab.key}')">${tab.icon} <span data-i18n="${tab.titleKey}">${t(tab.titleKey)}</span> <span class="stream-tab-count">${tab.count}</span></button>`
|
||||
).join('')}<span class="cs-expand-collapse-group"><button class="btn-expand-collapse" onclick="expandAllTargetSections()" data-i18n-title="section.expand_all" title="${t('section.expand_all')}">⊞</button><button class="btn-expand-collapse" onclick="collapseAllTargetSections()" data-i18n-title="section.collapse_all" title="${t('section.collapse_all')}">⊟</button><button class="tutorial-trigger-btn" onclick="startTargetsTutorial()" data-i18n-title="tour.restart" title="${t('tour.restart')}">${ICON_HELP}</button></span></div>`;
|
||||
// Determine which tree leaf is active
|
||||
const activeLeaf = activeSubTab === 'key_colors' ? 'kc-targets' : 'led-devices';
|
||||
|
||||
// Use window.createPatternTemplateCard to avoid circular import
|
||||
const createPatternTemplateCard = window.createPatternTemplateCard || (() => '');
|
||||
@@ -655,7 +681,13 @@ export async function loadTargetsTab() {
|
||||
|
||||
if (csDevices.isMounted()) {
|
||||
// ── Incremental update: reconcile cards in-place ──
|
||||
_updateSubTabCounts(subTabs);
|
||||
_targetsTree.updateCounts({
|
||||
'led-devices': ledDevices.length,
|
||||
'led-css': Object.keys(colorStripSourceMap).length,
|
||||
'led-targets': ledTargets.length,
|
||||
'kc-targets': kcTargets.length,
|
||||
'kc-patterns': patternTemplates.length,
|
||||
});
|
||||
csDevices.reconcile(deviceItems);
|
||||
csColorStrips.reconcile(cssItems);
|
||||
const ledResult = csLedTargets.reconcile(ledTargetItems);
|
||||
@@ -685,8 +717,12 @@ export async function loadTargetsTab() {
|
||||
${csKCTargets.render(kcTargetItems)}
|
||||
${csPatternTemplates.render(patternItems)}
|
||||
</div>`;
|
||||
container.innerHTML = tabBar + ledPanel + kcPanel;
|
||||
container.innerHTML = ledPanel + kcPanel;
|
||||
CardSection.bindAll([csDevices, csColorStrips, csLedTargets, csKCTargets, csPatternTemplates]);
|
||||
|
||||
// Render tree sidebar with expand/collapse buttons
|
||||
_targetsTree.setExtraHtml(`<button class="btn-expand-collapse" onclick="expandAllTargetSections()" data-i18n-title="section.expand_all" title="${t('section.expand_all')}">⊞</button><button class="btn-expand-collapse" onclick="collapseAllTargetSections()" data-i18n-title="section.collapse_all" title="${t('section.collapse_all')}">⊟</button><button class="tutorial-trigger-btn" onclick="startTargetsTutorial()" data-i18n-title="tour.restart" title="${t('tour.restart')}">${ICON_HELP}</button>`);
|
||||
_targetsTree.update(treeGroups, activeLeaf);
|
||||
}
|
||||
|
||||
// Show/hide stop-all buttons based on running state
|
||||
|
||||
Reference in New Issue
Block a user