@@ -997,60 +960,6 @@ function _initToolbarDrag(tbEl) {
_makeDraggable(tbEl, handle, { loadFn: _loadToolbarPos, saveFn: _saveToolbarPos });
}
-/* ── Search ── */
-
-function _renderSearchResults(query) {
- const results = document.querySelector('.graph-search-results');
- if (!results) return;
-
- const q = query.toLowerCase().trim();
- const filtered = q
- ? _searchItems.filter(n => n.name.toLowerCase().includes(q) || n.kind.includes(q) || (n.subtype || '').includes(q))
- : _searchItems.slice(0, 20);
-
- _searchIndex = filtered.length > 0 ? 0 : -1;
-
- results.innerHTML = filtered.map((n, i) => {
- const color = ENTITY_COLORS[n.kind] || '#666';
- return `
-
- ${_escHtml(n.name)}
- ${n.kind.replace(/_/g, ' ')}
-
`;
- }).join('');
-
- results.querySelectorAll('.graph-search-item').forEach(item => {
- item.addEventListener('click', () => {
- _navigateToNode(item.getAttribute('data-id'));
- closeGraphSearch();
- });
- });
-}
-
-function _onSearchKeydown(e) {
- const results = document.querySelectorAll('.graph-search-item');
- if (e.key === 'ArrowDown') { e.preventDefault(); _searchIndex = Math.min(_searchIndex + 1, results.length - 1); _updateSearchActive(results); }
- else if (e.key === 'ArrowUp') { e.preventDefault(); _searchIndex = Math.max(_searchIndex - 1, 0); _updateSearchActive(results); }
- else if (e.key === 'Enter') { e.preventDefault(); if (results[_searchIndex]) { _navigateToNode(results[_searchIndex].getAttribute('data-id')); closeGraphSearch(); } }
- else if (e.key === 'Escape') { closeGraphSearch(); }
-}
-
-function _updateSearchActive(items) {
- items.forEach((el, i) => el.classList.toggle('active', i === _searchIndex));
- if (items[_searchIndex]) items[_searchIndex].scrollIntoView({ block: 'nearest' });
-}
-
-function _navigateToNode(nodeId) {
- const node = _nodeMap?.get(nodeId);
- if (!node || !_canvas) return;
- _canvas.panTo(node.x + node.width / 2, node.y + node.height / 2, true);
-
- const nodeGroup = document.querySelector('.graph-nodes');
- if (nodeGroup) { highlightNode(nodeGroup, nodeId); setTimeout(() => highlightNode(nodeGroup, null), 3000); }
-
- const edgeGroup = document.querySelector('.graph-edges');
- if (edgeGroup && _edges) { highlightChain(edgeGroup, nodeId, _edges); setTimeout(() => clearEdgeHighlights(edgeGroup), 5000); }
-}
/* ── Node callbacks ── */
@@ -1213,11 +1122,10 @@ function _onKeydown(e) {
// Skip when typing in search input (except Escape/F11)
const inInput = e.target.matches('input, textarea, select');
- if (e.key === '/' && !_searchVisible && !inInput) { e.preventDefault(); openGraphSearch(); }
+ if (e.key === '/' && !inInput) { e.preventDefault(); window.openCommandPalette?.(); }
if ((e.key === 'f' || e.key === 'F') && !inInput && !e.ctrlKey && !e.metaKey) { e.preventDefault(); toggleGraphFilter(); }
if (e.key === 'Escape') {
if (_filterVisible) { toggleGraphFilter(); }
- else if (_searchVisible) { closeGraphSearch(); }
else {
const ng = document.querySelector('.graph-nodes');
const eg = document.querySelector('.graph-edges');
@@ -1249,7 +1157,7 @@ function _onKeydown(e) {
graphAddEntity();
}
// Arrow keys / WASD → spatial navigation between nodes
- if (_selectedIds.size <= 1 && !_searchVisible && !inInput) {
+ if (_selectedIds.size <= 1 && !inInput) {
const dir = _arrowDir(e);
if (dir) {
e.preventDefault();
@@ -1649,14 +1557,6 @@ function _calcBounds(nodeMap) {
return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
}
-/* ── Helpers ── */
-
-function _escHtml(s) {
- const d = document.createElement('div');
- d.textContent = s;
- return d.innerHTML;
-}
-
/* ── Port drag (connect/reconnect) ── */
const SVG_NS = 'http://www.w3.org/2000/svg';
diff --git a/server/src/wled_controller/static/js/features/kc-targets.js b/server/src/wled_controller/static/js/features/kc-targets.js
index 6771fa0..7ec4b66 100644
--- a/server/src/wled_controller/static/js/features/kc-targets.js
+++ b/server/src/wled_controller/static/js/features/kc-targets.js
@@ -228,7 +228,7 @@ export function createKCTargetCard(target, sourceMap, patternTemplateMap, valueS
${ICON_LINK_SOURCE} ${escapeHtml(sourceName)}
-
${ICON_PATTERN_TEMPLATE} ${escapeHtml(patternName)}
+
${ICON_PATTERN_TEMPLATE} ${escapeHtml(patternName)}
▭ ${rectCount} rect${rectCount !== 1 ? 's' : ''}
${ICON_FPS} ${kcSettings.fps ?? 10}
${bvs ? `
${getValueSourceIcon(bvs.source_type)} ${escapeHtml(bvs.name)}` : ''}
diff --git a/server/src/wled_controller/static/js/features/streams.js b/server/src/wled_controller/static/js/features/streams.js
index 621cd50..0c8abb4 100644
--- a/server/src/wled_controller/static/js/features/streams.js
+++ b/server/src/wled_controller/static/js/features/streams.js
@@ -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 = `
${ICON_MONITOR} ${stream.display_index ?? 0}
${ICON_FPS} ${stream.target_fps ?? 30}
- ${capTmplName ? `${ICON_TEMPLATE} ${capTmplName}` : ''}
+ ${capTmplName ? `${ICON_TEMPLATE} ${capTmplName}` : ''}
`;
} 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 = `
${ICON_LINK_SOURCE} ${sourceName}
- ${ppTmplName ? `${ICON_TEMPLATE} ${ppTmplName}` : ''}
+ ${ppTmplName ? `${ICON_TEMPLATE} ${ppTmplName}` : ''}
`;
} 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(`
`);
_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',
diff --git a/server/src/wled_controller/static/js/features/targets.js b/server/src/wled_controller/static/js/features/targets.js
index c498054..900d5f1 100644
--- a/server/src/wled_controller/static/js/features/targets.js
+++ b/server/src/wled_controller/static/js/features/targets.js
@@ -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 = `
-
- ${csDevices.render(deviceItems)}
- ${csLedTargets.render(ledTargetItems)}
-
`;
- const kcPanel = `
-
- ${csKCTargets.render(kcTargetItems)}
- ${csPatternTemplates.render(patternItems)}
-
`;
- 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 => `
${p.html}
`).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