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

@@ -1246,9 +1246,11 @@ export function switchStreamTab(tabKey) {
}
const _streamSectionMap = {
raw: [csRawStreams, csRawTemplates],
raw: [csRawStreams],
raw_templates: [csRawTemplates],
static_image: [csStaticStreams],
processed: [csProcStreams, csProcTemplates],
processed: [csProcStreams],
proc_templates: [csProcTemplates],
css_processing: [csCSPTemplates],
color_strip: [csColorStrips],
audio: [csAudioMulti, csAudioMono, csAudioTemplates],
@@ -1283,7 +1285,7 @@ function renderPictureSourcesList(streams) {
detailsHtml = `<div class="stream-card-props">
<span class="stream-card-prop" title="${t('streams.display')}">${ICON_MONITOR} ${stream.display_index ?? 0}</span>
<span class="stream-card-prop" title="${t('streams.target_fps')}">${ICON_FPS} ${stream.target_fps ?? 30}</span>
${capTmplName ? `<span class="stream-card-prop stream-card-link" title="${t('streams.capture_template')}" onclick="event.stopPropagation(); navigateToCard('streams','raw','raw-templates','data-template-id','${stream.capture_template_id}')">${ICON_TEMPLATE} ${capTmplName}</span>` : ''}
${capTmplName ? `<span class="stream-card-prop stream-card-link" title="${t('streams.capture_template')}" onclick="event.stopPropagation(); navigateToCard('streams','raw_templates','raw-templates','data-template-id','${stream.capture_template_id}')">${ICON_TEMPLATE} ${capTmplName}</span>` : ''}
</div>`;
} else if (stream.stream_type === 'processed') {
const sourceStream = _cachedStreams.find(s => s.id === stream.source_stream_id);
@@ -1297,7 +1299,7 @@ function renderPictureSourcesList(streams) {
}
detailsHtml = `<div class="stream-card-props">
<span class="stream-card-prop stream-card-link" title="${t('streams.source')}" onclick="event.stopPropagation(); navigateToCard('streams','${sourceSubTab}','${sourceSection}','data-stream-id','${stream.source_stream_id}')">${ICON_LINK_SOURCE} ${sourceName}</span>
${ppTmplName ? `<span class="stream-card-prop stream-card-link" title="${t('streams.pp_template')}" onclick="event.stopPropagation(); navigateToCard('streams','processed','proc-templates','data-pp-template-id','${stream.postprocessing_template_id}')">${ICON_TEMPLATE} ${ppTmplName}</span>` : ''}
${ppTmplName ? `<span class="stream-card-prop stream-card-link" title="${t('streams.pp_template')}" onclick="event.stopPropagation(); navigateToCard('streams','proc_templates','proc-templates','data-pp-template-id','${stream.postprocessing_template_id}')">${ICON_TEMPLATE} ${ppTmplName}</span>` : ''}
</div>`;
} else if (stream.stream_type === 'static_image') {
const src = stream.image_source || '';
@@ -1440,8 +1442,10 @@ function renderPictureSourcesList(streams) {
const tabs = [
{ key: 'raw', icon: getPictureSourceIcon('raw'), titleKey: 'streams.group.raw', count: rawStreams.length },
{ key: 'raw_templates', icon: ICON_CAPTURE_TEMPLATE, titleKey: 'streams.group.raw_templates', count: _cachedCaptureTemplates.length },
{ key: 'static_image', icon: getPictureSourceIcon('static_image'), titleKey: 'streams.group.static_image', count: staticImageStreams.length },
{ key: 'processed', icon: getPictureSourceIcon('processed'), titleKey: 'streams.group.processed', count: processedStreams.length },
{ key: 'proc_templates', icon: ICON_PP_TEMPLATE, titleKey: 'streams.group.proc_templates', count: _cachedPPTemplates.length },
{ key: 'css_processing', icon: ICON_CSPT, titleKey: 'streams.group.css_processing', count: csptTemplates.length },
{ key: 'color_strip', icon: getColorStripIcon('static'), titleKey: 'streams.group.color_strip', count: colorStrips.length },
{ key: 'audio', icon: getAudioSourceIcon('multichannel'), titleKey: 'streams.group.audio', count: _cachedAudioSources.length },
@@ -1452,11 +1456,21 @@ function renderPictureSourcesList(streams) {
// Build tree navigation structure
const treeGroups = [
{
key: 'picture_group', icon: getPictureSourceIcon('raw'), titleKey: 'tree.group.picture',
key: 'capture_group', icon: getPictureSourceIcon('raw'), titleKey: 'tree.group.capture',
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: 'raw_templates', titleKey: 'streams.group.raw_templates', icon: ICON_CAPTURE_TEMPLATE, count: _cachedCaptureTemplates.length },
]
},
{
key: 'static_image', icon: getPictureSourceIcon('static_image'), titleKey: 'streams.group.static_image',
count: staticImageStreams.length,
},
{
key: 'processing_group', icon: getPictureSourceIcon('processed'), titleKey: 'tree.group.processing',
children: [
{ key: 'processed', titleKey: 'streams.group.processed', icon: getPictureSourceIcon('processed'), count: processedStreams.length },
{ key: 'proc_templates', titleKey: 'streams.group.proc_templates', icon: ICON_PP_TEMPLATE, count: _cachedPPTemplates.length },
]
},
{
@@ -1584,8 +1598,10 @@ function renderPictureSourcesList(streams) {
// Incremental update: reconcile cards in-place
_streamsTree.updateCounts({
raw: rawStreams.length,
raw_templates: _cachedCaptureTemplates.length,
static_image: staticImageStreams.length,
processed: processedStreams.length,
proc_templates: _cachedPPTemplates.length,
css_processing: csptTemplates.length,
color_strip: colorStrips.length,
audio: _cachedAudioSources.length + _cachedAudioTemplates.length,
@@ -1608,8 +1624,10 @@ function renderPictureSourcesList(streams) {
// First render: build full HTML
const panels = tabs.map(tab => {
let panelContent = '';
if (tab.key === 'raw') panelContent = csRawStreams.render(rawStreamItems) + csRawTemplates.render(rawTemplateItems);
else if (tab.key === 'processed') panelContent = csProcStreams.render(procStreamItems) + csProcTemplates.render(procTemplateItems);
if (tab.key === 'raw') panelContent = csRawStreams.render(rawStreamItems);
else if (tab.key === 'raw_templates') panelContent = csRawTemplates.render(rawTemplateItems);
else if (tab.key === 'processed') panelContent = csProcStreams.render(procStreamItems);
else if (tab.key === 'proc_templates') panelContent = csProcTemplates.render(procTemplateItems);
else if (tab.key === 'css_processing') panelContent = csCSPTemplates.render(csptItems);
else if (tab.key === 'color_strip') panelContent = csColorStrips.render(colorStripItems);
else if (tab.key === 'audio') panelContent = csAudioMulti.render(multiItems) + csAudioMono.render(monoItems) + csAudioTemplates.render(audioTemplateItems);
@@ -1626,9 +1644,9 @@ function renderPictureSourcesList(streams) {
_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);
_streamsTree.observeSections('streams-list', {
'raw-streams': 'raw', 'raw-templates': 'raw',
'raw-streams': 'raw', 'raw-templates': 'raw_templates',
'static-streams': 'static_image',
'proc-streams': 'processed', 'proc-templates': 'processed',
'proc-streams': 'processed', 'proc-templates': 'proc_templates',
'css-proc-templates': 'css_processing',
'color-strips': 'color_strip',
'audio-multi': 'audio', 'audio-mono': 'audio', 'audio-templates': 'audio',