Add graph icon grid, search-to-graph nav, overlay on CSS cards, fix clipboard copy
- Convert graph editor add-entity menu to showTypePicker icon grid with SVG icons - Add CSPT to graph add-entity picker and ALL_CACHES watcher - Add graphNavigateToNode() — command palette navigates to graph node when graph tab active - Add CSPT entities to global search palette results - Add overlay toggle button on picture-based CSS cards (toggleCSSOverlay) - Fix clipboard copy on non-HTTPS (LAN) with execCommand fallback for all copy functions - Fix notification bell button vertical centering in test preview strip canvas - Add overlay.toggle, search.group.cspt i18n keys (en/ru/zh) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -17,6 +17,8 @@ import { fetchWithAuth } from '../core/api.js';
|
||||
import { showToast, showConfirm } from '../core/ui.js';
|
||||
import { t } from '../core/i18n.js';
|
||||
import { findConnection, getCompatibleInputs, getConnectionByField, updateConnection, detachConnection, isEditableEdge } from '../core/graph-connections.js';
|
||||
import { showTypePicker } from '../core/icon-select.js';
|
||||
import * as P from '../core/icon-paths.js';
|
||||
|
||||
let _canvas = null;
|
||||
let _nodeMap = null;
|
||||
@@ -377,18 +379,20 @@ export async function graphRelayout() {
|
||||
await loadGraphEditor();
|
||||
}
|
||||
|
||||
// Entity kind → window function to open add/create modal
|
||||
// Entity kind → window function to open add/create modal + icon path
|
||||
const _ico = (d) => `<svg class="icon" viewBox="0 0 24 24">${d}</svg>`;
|
||||
const ADD_ENTITY_MAP = [
|
||||
{ kind: 'device', fn: () => window.showAddDevice?.() },
|
||||
{ kind: 'capture_template', fn: () => window.showAddTemplateModal?.() },
|
||||
{ kind: 'pp_template', fn: () => window.showAddPPTemplateModal?.() },
|
||||
{ kind: 'audio_template', fn: () => window.showAddAudioTemplateModal?.() },
|
||||
{ kind: 'picture_source', fn: () => window.showAddStreamModal?.() },
|
||||
{ kind: 'audio_source', fn: () => window.showAudioSourceModal?.() },
|
||||
{ kind: 'value_source', fn: () => window.showValueSourceModal?.() },
|
||||
{ kind: 'color_strip_source', fn: () => window.showCSSEditor?.() },
|
||||
{ kind: 'output_target', fn: () => window.showTargetEditor?.() },
|
||||
{ kind: 'automation', fn: () => window.openAutomationEditor?.() },
|
||||
{ kind: 'device', fn: () => window.showAddDevice?.(), icon: _ico(P.monitor) },
|
||||
{ kind: 'capture_template', fn: () => window.showAddTemplateModal?.(), icon: _ico(P.camera) },
|
||||
{ kind: 'pp_template', fn: () => window.showAddPPTemplateModal?.(), icon: _ico(P.wrench) },
|
||||
{ kind: 'cspt', fn: () => window.showAddCSPTModal?.(), icon: _ico(P.wrench) },
|
||||
{ kind: 'audio_template', fn: () => window.showAddAudioTemplateModal?.(),icon: _ico(P.music) },
|
||||
{ kind: 'picture_source', fn: () => window.showAddStreamModal?.(), icon: _ico(P.tv) },
|
||||
{ kind: 'audio_source', fn: () => window.showAudioSourceModal?.(), icon: _ico(P.music) },
|
||||
{ kind: 'value_source', fn: () => window.showValueSourceModal?.(), icon: _ico(P.hash) },
|
||||
{ kind: 'color_strip_source', fn: () => window.showCSSEditor?.(), icon: _ico(P.film) },
|
||||
{ kind: 'output_target', fn: () => window.showTargetEditor?.(), icon: _ico(P.zap) },
|
||||
{ kind: 'automation', fn: () => window.openAutomationEditor?.(), icon: _ico(P.clipboardList) },
|
||||
];
|
||||
|
||||
// All caches to watch for new entity creation
|
||||
@@ -397,62 +401,26 @@ const ALL_CACHES = [
|
||||
streamsCache, audioSourcesCache, audioTemplatesCache,
|
||||
valueSourcesCache, colorStripSourcesCache, syncClocksCache,
|
||||
outputTargetsCache, patternTemplatesCache, scenePresetsCache,
|
||||
automationsCacheObj,
|
||||
automationsCacheObj, csptCache,
|
||||
];
|
||||
|
||||
let _addEntityMenu = null;
|
||||
|
||||
export function graphAddEntity() {
|
||||
if (_addEntityMenu) { _dismissAddEntityMenu(); return; }
|
||||
|
||||
const container = document.querySelector('#graph-editor-content .graph-container');
|
||||
if (!container) return;
|
||||
|
||||
const toolbar = container.querySelector('.graph-toolbar');
|
||||
const menu = document.createElement('div');
|
||||
menu.className = 'graph-add-entity-menu';
|
||||
|
||||
// Position below toolbar
|
||||
if (toolbar) {
|
||||
menu.style.left = toolbar.offsetLeft + 'px';
|
||||
menu.style.top = (toolbar.offsetTop + toolbar.offsetHeight + 6) + 'px';
|
||||
}
|
||||
|
||||
for (const item of ADD_ENTITY_MAP) {
|
||||
const btn = document.createElement('button');
|
||||
btn.className = 'graph-add-entity-item';
|
||||
const color = ENTITY_COLORS[item.kind] || '#666';
|
||||
const label = ENTITY_LABELS[item.kind] || item.kind.replace(/_/g, ' ');
|
||||
btn.innerHTML = `<span class="graph-add-entity-dot" style="background:${color}"></span><span>${label}</span>`;
|
||||
btn.addEventListener('click', () => {
|
||||
_dismissAddEntityMenu();
|
||||
_watchForNewEntity();
|
||||
item.fn();
|
||||
});
|
||||
menu.appendChild(btn);
|
||||
}
|
||||
|
||||
container.appendChild(menu);
|
||||
_addEntityMenu = menu;
|
||||
|
||||
// Close on click outside
|
||||
setTimeout(() => {
|
||||
document.addEventListener('click', _onAddEntityClickAway, true);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
function _onAddEntityClickAway(e) {
|
||||
if (_addEntityMenu && !_addEntityMenu.contains(e.target)) {
|
||||
_dismissAddEntityMenu();
|
||||
}
|
||||
}
|
||||
|
||||
function _dismissAddEntityMenu() {
|
||||
if (_addEntityMenu) {
|
||||
_addEntityMenu.remove();
|
||||
_addEntityMenu = null;
|
||||
}
|
||||
document.removeEventListener('click', _onAddEntityClickAway, true);
|
||||
const items = ADD_ENTITY_MAP.map(item => ({
|
||||
value: item.kind,
|
||||
icon: item.icon,
|
||||
label: ENTITY_LABELS[item.kind] || item.kind.replace(/_/g, ' '),
|
||||
}));
|
||||
showTypePicker({
|
||||
title: t('graph.add_entity') || 'Add Entity',
|
||||
items,
|
||||
onPick: (kind) => {
|
||||
const entry = ADD_ENTITY_MAP.find(e => e.kind === kind);
|
||||
if (entry) {
|
||||
_watchForNewEntity();
|
||||
entry.fn();
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Watch for new entity creation after add-entity menu action
|
||||
@@ -1121,6 +1089,18 @@ function _onNodeDblClick(node) {
|
||||
}
|
||||
}
|
||||
|
||||
/** Navigate graph to a node by entity ID — zoom + highlight. */
|
||||
export function graphNavigateToNode(entityId) {
|
||||
const node = _nodeMap?.get(entityId);
|
||||
if (!node || !_canvas) return false;
|
||||
_canvas.zoomToPoint(1.5, node.x + node.width / 2, node.y + node.height / 2);
|
||||
const nodeGroup = document.querySelector('.graph-nodes');
|
||||
if (nodeGroup) { highlightNode(nodeGroup, entityId); setTimeout(() => highlightNode(nodeGroup, null), 3000); }
|
||||
const edgeGroup = document.querySelector('.graph-edges');
|
||||
if (edgeGroup && _edges) { highlightChain(edgeGroup, entityId, _edges); setTimeout(() => clearEdgeHighlights(edgeGroup), 5000); }
|
||||
return true;
|
||||
}
|
||||
|
||||
function _onEditNode(node) {
|
||||
const fnMap = {
|
||||
device: () => window.showSettings?.(node.id),
|
||||
|
||||
Reference in New Issue
Block a user