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:
@@ -37,6 +37,7 @@ import { Modal } from '../core/modal.js';
|
||||
import { showToast, showConfirm, openLightbox, openFullImageLightbox, showOverlaySpinner, hideOverlaySpinner, updateOverlayPreview, setTabRefreshing } from '../core/ui.js';
|
||||
import { openDisplayPicker, formatDisplayLabel } from './displays.js';
|
||||
import { CardSection } from '../core/card-sections.js';
|
||||
import { TreeNav } from '../core/tree-nav.js';
|
||||
import { updateSubTabHash } from './tabs.js';
|
||||
import { createValueSourceCard } from './value-sources.js';
|
||||
import { createSyncClockCard } from './sync-clocks.js';
|
||||
@@ -1189,22 +1190,33 @@ export async function loadPictureSources() {
|
||||
}
|
||||
}
|
||||
|
||||
let _streamsTreeTriggered = false;
|
||||
|
||||
const _streamsTree = new TreeNav('streams-tree-nav', {
|
||||
onSelect: (key) => {
|
||||
_streamsTreeTriggered = true;
|
||||
switchStreamTab(key);
|
||||
_streamsTreeTriggered = false;
|
||||
}
|
||||
});
|
||||
|
||||
export function switchStreamTab(tabKey) {
|
||||
document.querySelectorAll('.stream-tab-btn[data-stream-tab]').forEach(btn =>
|
||||
btn.classList.toggle('active', btn.dataset.streamTab === tabKey)
|
||||
);
|
||||
document.querySelectorAll('.stream-tab-panel[id^="stream-tab-"]').forEach(panel =>
|
||||
panel.classList.toggle('active', panel.id === `stream-tab-${tabKey}`)
|
||||
);
|
||||
localStorage.setItem('activeStreamTab', tabKey);
|
||||
updateSubTabHash('streams', tabKey);
|
||||
// Update tree active state (unless the tree triggered this switch)
|
||||
if (!_streamsTreeTriggered) {
|
||||
_streamsTree.setActive(tabKey);
|
||||
}
|
||||
}
|
||||
|
||||
const _streamSectionMap = {
|
||||
raw: [csRawStreams, csRawTemplates],
|
||||
static_image: [csStaticStreams],
|
||||
processed: [csProcStreams, csProcTemplates],
|
||||
audio: [csAudioMulti, csAudioMono],
|
||||
audio: [csAudioMulti, csAudioMono, csAudioTemplates],
|
||||
value: [csValueSources],
|
||||
sync: [csSyncClocks],
|
||||
};
|
||||
@@ -1365,9 +1377,28 @@ function renderPictureSourcesList(streams) {
|
||||
{ key: 'sync', icon: ICON_CLOCK, titleKey: 'streams.group.sync', count: _cachedSyncClocks.length },
|
||||
];
|
||||
|
||||
const tabBar = `<div class="stream-tab-bar">${tabs.map(tab =>
|
||||
`<button class="stream-tab-btn${tab.key === activeTab ? ' active' : ''}" data-stream-tab="${tab.key}" onclick="switchStreamTab('${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="expandAllStreamSections()" data-i18n-title="section.expand_all" title="${t('section.expand_all')}">⊞</button><button class="btn-expand-collapse" onclick="collapseAllStreamSections()" data-i18n-title="section.collapse_all" title="${t('section.collapse_all')}">⊟</button><button class="tutorial-trigger-btn" onclick="startSourcesTutorial()" data-i18n-title="tour.restart" title="${t('tour.restart')}">${ICON_HELP}</button></span></div>`;
|
||||
// Build tree navigation structure
|
||||
const treeGroups = [
|
||||
{
|
||||
key: 'picture_group', icon: getPictureSourceIcon('raw'), titleKey: 'tree.group.picture',
|
||||
children: [
|
||||
{ key: 'raw', titleKey: 'streams.group.raw', icon: getPictureSourceIcon('raw'), count: rawStreams.length },
|
||||
{ key: 'static_image', titleKey: 'streams.group.static_image', icon: getPictureSourceIcon('static_image'), count: staticImageStreams.length },
|
||||
{ key: 'processed', titleKey: 'streams.group.processed', icon: getPictureSourceIcon('processed'), count: processedStreams.length },
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'audio', icon: getAudioSourceIcon('multichannel'), titleKey: 'streams.group.audio',
|
||||
count: _cachedAudioSources.length + _cachedAudioTemplates.length,
|
||||
},
|
||||
{
|
||||
key: 'utility_group', icon: ICON_WRENCH, titleKey: 'tree.group.utility',
|
||||
children: [
|
||||
{ key: 'value', titleKey: 'streams.group.value', icon: ICON_VALUE_SOURCE, count: _cachedValueSources.length },
|
||||
{ key: 'sync', titleKey: 'streams.group.sync', icon: ICON_CLOCK, count: _cachedSyncClocks.length },
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
const renderAudioSourceCard = (src) => {
|
||||
const isMono = src.source_type === 'mono';
|
||||
@@ -1470,9 +1501,13 @@ function renderPictureSourcesList(streams) {
|
||||
|
||||
if (csRawStreams.isMounted()) {
|
||||
// Incremental update: reconcile cards in-place
|
||||
tabs.forEach(tab => {
|
||||
const btn = container.querySelector(`.stream-tab-btn[data-stream-tab="${tab.key}"] .stream-tab-count`);
|
||||
if (btn) btn.textContent = tab.count;
|
||||
_streamsTree.updateCounts({
|
||||
raw: rawStreams.length,
|
||||
static_image: staticImageStreams.length,
|
||||
processed: processedStreams.length,
|
||||
audio: _cachedAudioSources.length + _cachedAudioTemplates.length,
|
||||
value: _cachedValueSources.length,
|
||||
sync: _cachedSyncClocks.length,
|
||||
});
|
||||
csRawStreams.reconcile(rawStreamItems);
|
||||
csRawTemplates.reconcile(rawTemplateItems);
|
||||
@@ -1497,8 +1532,12 @@ function renderPictureSourcesList(streams) {
|
||||
return `<div class="stream-tab-panel${tab.key === activeTab ? ' active' : ''}" id="stream-tab-${tab.key}">${panelContent}</div>`;
|
||||
}).join('');
|
||||
|
||||
container.innerHTML = tabBar + panels;
|
||||
container.innerHTML = panels;
|
||||
CardSection.bindAll([csRawStreams, csRawTemplates, csProcStreams, csProcTemplates, csAudioMulti, csAudioMono, csAudioTemplates, csStaticStreams, csValueSources, csSyncClocks]);
|
||||
|
||||
// Render tree sidebar with expand/collapse buttons
|
||||
_streamsTree.setExtraHtml(`<button class="btn-expand-collapse" onclick="expandAllStreamSections()" data-i18n-title="section.expand_all" title="${t('section.expand_all')}">⊞</button><button class="btn-expand-collapse" onclick="collapseAllStreamSections()" data-i18n-title="section.collapse_all" title="${t('section.collapse_all')}">⊟</button><button class="tutorial-trigger-btn" onclick="startSourcesTutorial()" data-i18n-title="tour.restart" title="${t('tour.restart')}">${ICON_HELP}</button>`);
|
||||
_streamsTree.update(treeGroups, activeTab);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user