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:
@@ -28,9 +28,6 @@ let _selectedIds = new Set();
|
||||
let _initialized = false;
|
||||
let _legendVisible = (() => { try { return localStorage.getItem('graph_legend_visible') === '1'; } catch { return false; } })();
|
||||
let _minimapVisible = true;
|
||||
let _searchVisible = false;
|
||||
let _searchIndex = -1;
|
||||
let _searchItems = [];
|
||||
let _loading = false;
|
||||
let _filterVisible = false;
|
||||
let _filterQuery = ''; // current active filter text
|
||||
@@ -209,29 +206,6 @@ export async function loadGraphEditor() {
|
||||
}
|
||||
}
|
||||
|
||||
export function openGraphSearch() {
|
||||
if (!_nodeMap) return;
|
||||
const panel = document.querySelector('.graph-search');
|
||||
if (!panel) return;
|
||||
|
||||
_searchItems = [];
|
||||
for (const node of _nodeMap.values()) _searchItems.push(node);
|
||||
|
||||
_searchIndex = -1;
|
||||
_searchVisible = true;
|
||||
panel.classList.add('visible');
|
||||
const input = panel.querySelector('.graph-search-input');
|
||||
input.value = '';
|
||||
input.focus();
|
||||
_renderSearchResults('');
|
||||
}
|
||||
|
||||
export function closeGraphSearch() {
|
||||
_searchVisible = false;
|
||||
const panel = document.querySelector('.graph-search');
|
||||
if (panel) panel.classList.remove('visible');
|
||||
}
|
||||
|
||||
export function toggleGraphLegend() {
|
||||
_legendVisible = !_legendVisible;
|
||||
try { localStorage.setItem('graph_legend_visible', _legendVisible ? '1' : '0'); } catch {}
|
||||
@@ -587,12 +561,6 @@ function _renderGraph(container) {
|
||||
_onEdgeContextMenu(edgePath, e, container);
|
||||
});
|
||||
|
||||
const searchInput = container.querySelector('.graph-search-input');
|
||||
if (searchInput) {
|
||||
searchInput.addEventListener('input', (e) => _renderSearchResults(e.target.value));
|
||||
searchInput.addEventListener('keydown', _onSearchKeydown);
|
||||
}
|
||||
|
||||
const filterInput = container.querySelector('.graph-filter-input');
|
||||
if (filterInput) {
|
||||
filterInput.addEventListener('input', (e) => _applyFilter(e.target.value));
|
||||
@@ -714,7 +682,7 @@ function _graphHTML() {
|
||||
<svg class="icon" viewBox="0 0 24 24"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/><path d="M8 11h6"/></svg>
|
||||
</button>
|
||||
<span class="graph-toolbar-sep"></span>
|
||||
<button class="btn-icon" onclick="openGraphSearch()" title="${t('graph.search')} (/)">
|
||||
<button class="btn-icon" onclick="openCommandPalette()" title="${t('graph.search')} (/)">
|
||||
<svg class="icon" viewBox="0 0 24 24"><path d="m21 21-4.34-4.34"/><circle cx="11" cy="11" r="8"/></svg>
|
||||
</button>
|
||||
<button class="btn-icon graph-filter-btn" onclick="toggleGraphFilter()" title="${t('graph.filter')} (F)">
|
||||
@@ -753,11 +721,6 @@ function _graphHTML() {
|
||||
<svg></svg>
|
||||
</div>
|
||||
|
||||
<div class="graph-search">
|
||||
<input class="graph-search-input" placeholder="${t('graph.search_placeholder')}" autocomplete="off">
|
||||
<div class="graph-search-results"></div>
|
||||
</div>
|
||||
|
||||
<div class="graph-filter">
|
||||
<div class="graph-filter-row">
|
||||
<svg class="graph-filter-icon" viewBox="0 0 24 24" width="16" height="16"><polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"/></svg>
|
||||
@@ -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 `<div class="graph-search-item${i === _searchIndex ? ' active' : ''}" data-id="${n.id}">
|
||||
<span class="graph-search-item-dot" style="background:${color}"></span>
|
||||
<span class="graph-search-item-name">${_escHtml(n.name)}</span>
|
||||
<span class="graph-search-item-type">${n.kind.replace(/_/g, ' ')}</span>
|
||||
</div>`;
|
||||
}).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';
|
||||
|
||||
Reference in New Issue
Block a user