Separate tree nodes into independent panels, remove graph local search, UI improvements

- Split Sources tab: raw/raw_templates, processed/proc_templates each get own panel
- Split Targets tab: led-devices, led-targets, kc-targets, kc-patterns each get own panel
- Remove graph local search — search button and / key open global command palette
- Add graphNavigateToNode for command palette → graph node navigation
- Add tree group expand/collapse animation (max-height + opacity transition)
- Make tree group headers visually distinct (smaller, uppercase, left border on children)
- Make CardSection collapse opt-in via collapsible flag (disabled by default)
- Move filter textbox next to section title (remove margin-left: auto)
- Fix notification bell button vertical centering in test preview
- Fix clipboard copy on non-HTTPS with execCommand fallback
- Add overlay toggle button on picture-based CSS cards
- Add CSPT to graph add-entity picker and global search
- Update all cross-link navigation paths for new panel keys
- Add i18n keys for new tree groups and search groups (en/ru/zh)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-15 12:32:13 +03:00
parent 3292e0daaf
commit 6c7b7ea7d7
12 changed files with 127 additions and 185 deletions

View File

@@ -535,18 +535,10 @@ export async function saveTargetEditor() {
let _treeTriggered = false;
const _targetsTree = new TreeNav('targets-tree-nav', {
onSelect: (key, leaf) => {
const subTab = leaf?.subTab || key;
onSelect: (key) => {
_treeTriggered = true;
switchTargetSubTab(subTab);
switchTargetSubTab(key);
_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' });
});
}
}
});
@@ -558,25 +550,25 @@ export function switchTargetSubTab(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);
_targetsTree.setActive(tabKey);
}
}
const _targetSectionMap = {
'led-devices': [csDevices],
'led-targets': [csLedTargets],
'kc-targets': [csKCTargets],
'kc-patterns': [csPatternTemplates],
};
export function expandAllTargetSections() {
const activeSubTab = localStorage.getItem('activeTargetSubTab') || 'led';
const sections = activeSubTab === 'key_colors'
? [csKCTargets, csPatternTemplates]
: [csDevices, csLedTargets];
CardSection.expandAll(sections);
const activeSubTab = localStorage.getItem('activeTargetSubTab') || 'led-devices';
CardSection.expandAll(_targetSectionMap[activeSubTab] || []);
}
export function collapseAllTargetSections() {
const activeSubTab = localStorage.getItem('activeTargetSubTab') || 'led';
const sections = activeSubTab === 'key_colors'
? [csKCTargets, csPatternTemplates]
: [csDevices, csLedTargets];
CardSection.collapseAll(sections);
const activeSubTab = localStorage.getItem('activeTargetSubTab') || 'led-devices';
CardSection.collapseAll(_targetSectionMap[activeSubTab] || []);
}
let _loadTargetsLock = false;
@@ -666,20 +658,22 @@ export async function loadTargetsTab() {
{
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-targets', titleKey: 'targets.section.targets', icon: getTargetTypeIcon('led'), count: ledTargets.length, subTab: 'led', sectionKey: 'led-targets' },
{ key: 'led-devices', titleKey: 'targets.section.devices', icon: getDeviceTypeIcon('wled'), count: ledDevices.length },
{ key: 'led-targets', titleKey: 'targets.section.targets', icon: getTargetTypeIcon('led'), count: ledTargets.length },
]
},
{
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' },
{ key: 'kc-targets', titleKey: 'targets.section.key_colors', icon: getTargetTypeIcon('key_colors'), count: kcTargets.length },
{ key: 'kc-patterns', titleKey: 'targets.section.pattern_templates', icon: ICON_TEMPLATE, count: patternTemplates.length },
]
}
];
// Determine which tree leaf is active
const activeLeaf = activeSubTab === 'key_colors' ? 'kc-targets' : 'led-devices';
// Determine which tree leaf is active — migrate old values
const validLeaves = ['led-devices', 'led-targets', 'kc-targets', 'kc-patterns'];
const activeLeaf = validLeaves.includes(activeSubTab) ? activeSubTab
: activeSubTab === 'key_colors' ? 'kc-targets' : 'led-devices';
// Use window.createPatternTemplateCard to avoid circular import
const createPatternTemplateCard = window.createPatternTemplateCard || (() => '');
@@ -718,17 +712,13 @@ export async function loadTargetsTab() {
}
} else {
// ── First render: build full HTML ──
const ledPanel = `
<div class="target-sub-tab-panel stream-tab-panel${activeSubTab === 'led' ? ' active' : ''}" id="target-sub-tab-led">
${csDevices.render(deviceItems)}
${csLedTargets.render(ledTargetItems)}
</div>`;
const kcPanel = `
<div class="target-sub-tab-panel stream-tab-panel${activeSubTab === 'key_colors' ? ' active' : ''}" id="target-sub-tab-key_colors">
${csKCTargets.render(kcTargetItems)}
${csPatternTemplates.render(patternItems)}
</div>`;
container.innerHTML = ledPanel + kcPanel;
const panels = [
{ key: 'led-devices', html: csDevices.render(deviceItems) },
{ key: 'led-targets', html: csLedTargets.render(ledTargetItems) },
{ key: 'kc-targets', html: csKCTargets.render(kcTargetItems) },
{ key: 'kc-patterns', html: csPatternTemplates.render(patternItems) },
].map(p => `<div class="target-sub-tab-panel stream-tab-panel${p.key === activeLeaf ? ' active' : ''}" id="target-sub-tab-${p.key}">${p.html}</div>`).join('');
container.innerHTML = panels;
CardSection.bindAll([csDevices, csLedTargets, csKCTargets, csPatternTemplates]);
// Render tree sidebar with expand/collapse buttons
@@ -1001,7 +991,7 @@ export function createTargetCard(target, deviceMap, colorStripSourceMap, valueSo
</div>
</div>
<div class="stream-card-props">
<span class="stream-card-prop stream-card-link" title="${t('targets.device')}" onclick="event.stopPropagation(); navigateToCard('targets','led','led-devices','data-device-id','${target.device_id}')">${ICON_LED} ${escapeHtml(deviceName)}</span>
<span class="stream-card-prop stream-card-link" title="${t('targets.device')}" onclick="event.stopPropagation(); navigateToCard('targets','led-devices','led-devices','data-device-id','${target.device_id}')">${ICON_LED} ${escapeHtml(deviceName)}</span>
<span class="stream-card-prop" title="${t('targets.fps')}">${ICON_FPS} ${target.fps || 30}</span>
<span class="stream-card-prop" title="${t('targets.protocol')}">${_protocolBadge(device, target)}</span>
<span class="stream-card-prop${cssId ? ' stream-card-link' : ''}" title="${t('targets.color_strip_source')}"${cssId ? ` onclick="event.stopPropagation(); navigateToCard('streams','color_strip','color-strips','data-css-id','${cssId}')"` : ''}>${ICON_FILM} ${cssSummary}</span>