diff --git a/src/main.js b/src/main.js
index d6a95fc..281435f 100644
--- a/src/main.js
+++ b/src/main.js
@@ -6140,6 +6140,34 @@ function nodeEditor() {
palette.className = "node-palette";
container.appendChild(palette);
+ // Create persistent search input
+ const paletteSearch = document.createElement("div");
+ paletteSearch.className = "palette-search";
+ paletteSearch.innerHTML = `
+
+
+ `;
+ palette.appendChild(paletteSearch);
+
+ // Create content container that will be updated
+ const paletteContent = document.createElement("div");
+ paletteContent.className = "palette-content";
+ palette.appendChild(paletteContent);
+
+ // Get references to search elements
+ const searchInput = paletteSearch.querySelector(".palette-search-input");
+ const searchClearBtn = paletteSearch.querySelector(".palette-search-clear");
+
+ // Create minimap
+ const minimap = document.createElement("div");
+ minimap.className = "node-minimap";
+ minimap.style.display = 'none'; // Hidden by default
+ minimap.innerHTML = `
+
+
+ `;
+ container.appendChild(minimap);
+
// Category display names
const categoryNames = {
[NodeCategory.INPUT]: 'Inputs',
@@ -6149,12 +6177,31 @@ function nodeEditor() {
[NodeCategory.OUTPUT]: 'Outputs'
};
+ // Search state
+ let searchQuery = '';
+
+ // Handle search input changes
+ searchInput.addEventListener('input', (e) => {
+ searchQuery = e.target.value;
+ searchClearBtn.style.display = searchQuery ? 'flex' : 'none';
+ updatePalette();
+ });
+
+ // Handle search clear
+ searchClearBtn.addEventListener('click', () => {
+ searchQuery = '';
+ searchInput.value = '';
+ searchClearBtn.style.display = 'none';
+ searchInput.focus();
+ updatePalette();
+ });
+
// Function to update palette based on context and selected category
function updatePalette() {
const isTemplate = editingContext !== null;
- if (selectedCategory === null) {
- // Show categories
+ if (selectedCategory === null && !searchQuery) {
+ // Show categories when no search query
const categories = getCategories().filter(category => {
// Filter categories based on context
if (isTemplate) {
@@ -6166,7 +6213,7 @@ function nodeEditor() {
}
});
- palette.innerHTML = `
+ paletteContent.innerHTML = `
Node Categories
${categories.map(category => `
@@ -6174,12 +6221,18 @@ function nodeEditor() {
`).join('')}
`;
- } else {
- // Show nodes in selected category
- const nodesInCategory = getNodesByCategory(selectedCategory);
+ } else if (selectedCategory === null && searchQuery) {
+ // Show all matching nodes across all categories when searching from main panel
+ const allCategories = getCategories();
+ let allNodes = [];
+
+ allCategories.forEach(category => {
+ const nodesInCategory = getNodesByCategory(category);
+ allNodes = allNodes.concat(nodesInCategory);
+ });
// Filter based on context
- const filteredNodes = nodesInCategory.filter(node => {
+ let filteredNodes = allNodes.filter(node => {
if (isTemplate) {
// In template: hide VoiceAllocator, AudioOutput, MidiInput
return node.type !== 'VoiceAllocator' && node.type !== 'AudioOutput' && node.type !== 'MidiInput';
@@ -6189,16 +6242,68 @@ function nodeEditor() {
}
});
- palette.innerHTML = `
+ // Apply search filter
+ const query = searchQuery.toLowerCase();
+ filteredNodes = filteredNodes.filter(node => {
+ return node.name.toLowerCase().includes(query) ||
+ node.description.toLowerCase().includes(query);
+ });
+
+ // Function to highlight search matches in text
+ const highlightMatch = (text) => {
+ const regex = new RegExp(`(${searchQuery.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi');
+ return text.replace(regex, '$1');
+ };
+
+ paletteContent.innerHTML = `
+ Search Results
+ ${filteredNodes.length > 0 ? filteredNodes.map(node => `
+
+ ${highlightMatch(node.name)}
+
+ `).join('') : 'No matching nodes found
'}
+ `;
+ } else {
+ // Show nodes in selected category
+ const nodesInCategory = getNodesByCategory(selectedCategory);
+
+ // Filter based on context
+ let filteredNodes = nodesInCategory.filter(node => {
+ if (isTemplate) {
+ // In template: hide VoiceAllocator, AudioOutput, MidiInput
+ return node.type !== 'VoiceAllocator' && node.type !== 'AudioOutput' && node.type !== 'MidiInput';
+ } else {
+ // In main graph: hide TemplateInput/TemplateOutput
+ return node.type !== 'TemplateInput' && node.type !== 'TemplateOutput';
+ }
+ });
+
+ // Apply search filter
+ if (searchQuery) {
+ const query = searchQuery.toLowerCase();
+ filteredNodes = filteredNodes.filter(node => {
+ return node.name.toLowerCase().includes(query) ||
+ node.description.toLowerCase().includes(query);
+ });
+ }
+
+ // Function to highlight search matches in text
+ const highlightMatch = (text) => {
+ if (!searchQuery) return text;
+ const regex = new RegExp(`(${searchQuery.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi');
+ return text.replace(regex, '$1');
+ };
+
+ paletteContent.innerHTML = `
- ${filteredNodes.map(node => `
+ ${filteredNodes.length > 0 ? filteredNodes.map(node => `
- ${node.name}
+ ${highlightMatch(node.name)}
- `).join('')}
+ `).join('') : 'No matching nodes found
'}
`;
}
}
@@ -6241,6 +6346,147 @@ function nodeEditor() {
set suppressActionRecording(value) { suppressActionRecording = value; }
};
+ // Initialize minimap
+ const minimapCanvas = container.querySelector("#minimap-canvas");
+ const minimapViewport = container.querySelector(".minimap-viewport");
+ const minimapCtx = minimapCanvas.getContext('2d');
+
+ // Set canvas size to match container
+ minimapCanvas.width = 200;
+ minimapCanvas.height = 150;
+
+ function updateMinimap() {
+ if (!editor) return;
+
+ const ctx = minimapCtx;
+ const canvas = minimapCanvas;
+
+ // Clear canvas
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
+
+ // Get all nodes
+ const module = editor.module;
+ const nodes = editor.drawflow.drawflow[module]?.data || {};
+ const nodeList = Object.values(nodes);
+
+ if (nodeList.length === 0) {
+ minimap.style.display = 'none';
+ return;
+ }
+
+ // Calculate bounding box of all nodes
+ let minX = Infinity, minY = Infinity;
+ let maxX = -Infinity, maxY = -Infinity;
+
+ nodeList.forEach(node => {
+ minX = Math.min(minX, node.pos_x);
+ minY = Math.min(minY, node.pos_y);
+ maxX = Math.max(maxX, node.pos_x + 160); // Approximate node width
+ maxY = Math.max(maxY, node.pos_y + 100); // Approximate node height
+ });
+
+ // Add padding
+ const padding = 20;
+ minX -= padding;
+ minY -= padding;
+ maxX += padding;
+ maxY += padding;
+
+ // Calculate graph dimensions
+ const graphWidth = maxX - minX;
+ const graphHeight = maxY - minY;
+
+ // Check if graph fits in viewport
+ const zoom = editor.zoom || 1;
+ const drawflowRect = drawflowDiv.getBoundingClientRect();
+ const viewportWidth = drawflowRect.width / zoom;
+ const viewportHeight = drawflowRect.height / zoom;
+
+ // Only show minimap if graph is larger than viewport
+ if (graphWidth <= viewportWidth && graphHeight <= viewportHeight) {
+ minimap.style.display = 'none';
+ return;
+ } else {
+ minimap.style.display = 'block';
+ }
+
+ // Calculate scale to fit in minimap
+ const scale = Math.min(canvas.width / graphWidth, canvas.height / graphHeight);
+
+ // Draw nodes
+ ctx.fillStyle = '#666';
+ nodeList.forEach(node => {
+ const x = (node.pos_x - minX) * scale;
+ const y = (node.pos_y - minY) * scale;
+ const width = 160 * scale;
+ const height = 100 * scale;
+
+ ctx.fillRect(x, y, width, height);
+ });
+
+ // Update viewport indicator
+ const canvasX = editor.canvas_x || 0;
+ const canvasY = editor.canvas_y || 0;
+
+ const viewportX = (-canvasX / zoom - minX) * scale;
+ const viewportY = (-canvasY / zoom - minY) * scale;
+ const viewportIndicatorWidth = (drawflowRect.width / zoom) * scale;
+ const viewportIndicatorHeight = (drawflowRect.height / zoom) * scale;
+
+ minimapViewport.style.left = Math.max(0, viewportX) + 'px';
+ minimapViewport.style.top = Math.max(0, viewportY) + 'px';
+ minimapViewport.style.width = viewportIndicatorWidth + 'px';
+ minimapViewport.style.height = viewportIndicatorHeight + 'px';
+
+ // Store scale info for click navigation
+ minimapCanvas.dataset.scale = scale;
+ minimapCanvas.dataset.minX = minX;
+ minimapCanvas.dataset.minY = minY;
+ }
+
+ // Update minimap on various events
+ editor.on('nodeCreated', () => setTimeout(updateMinimap, 100));
+ editor.on('nodeRemoved', () => setTimeout(updateMinimap, 100));
+ editor.on('nodeMoved', () => updateMinimap());
+
+ // Update minimap on pan/zoom
+ drawflowDiv.addEventListener('wheel', () => setTimeout(updateMinimap, 10));
+
+ // Initial minimap render
+ setTimeout(updateMinimap, 200);
+
+ // Click-to-navigate on minimap
+ minimapCanvas.addEventListener('mousedown', (e) => {
+ const rect = minimapCanvas.getBoundingClientRect();
+ const clickX = e.clientX - rect.left;
+ const clickY = e.clientY - rect.top;
+
+ const scale = parseFloat(minimapCanvas.dataset.scale || 1);
+ const minX = parseFloat(minimapCanvas.dataset.minX || 0);
+ const minY = parseFloat(minimapCanvas.dataset.minY || 0);
+
+ // Convert click position to graph coordinates
+ const graphX = (clickX / scale) + minX;
+ const graphY = (clickY / scale) + minY;
+
+ // Center the viewport on the clicked position
+ const zoom = editor.zoom || 1;
+ const drawflowRect = drawflowDiv.getBoundingClientRect();
+ const viewportCenterX = drawflowRect.width / (2 * zoom);
+ const viewportCenterY = drawflowRect.height / (2 * zoom);
+
+ editor.canvas_x = -(graphX - viewportCenterX) * zoom;
+ editor.canvas_y = -(graphY - viewportCenterY) * zoom;
+
+ // Update the canvas transform
+ const precanvas = drawflowDiv.querySelector('.drawflow');
+ if (precanvas) {
+ precanvas.style.transform = `translate(${editor.canvas_x}px, ${editor.canvas_y}px) scale(${zoom})`;
+ }
+
+ updateMinimap();
+ });
+
// Add reconnection support: dragging from a connected input disconnects and starts new connection
drawflowDiv.addEventListener('mousedown', (e) => {
// Check if clicking on an input port
diff --git a/src/styles.css b/src/styles.css
index b76c1fb..039f40e 100644
--- a/src/styles.css
+++ b/src/styles.css
@@ -1348,6 +1348,101 @@ button {
background: #5d5d5d;
}
+/* Palette search */
+.palette-search {
+ position: relative;
+ margin-bottom: 8px;
+}
+
+.palette-content {
+ /* Content container for dynamic palette updates */
+}
+
+.palette-search-input {
+ width: 100%;
+ padding: 6px 28px 6px 8px;
+ background: var(--panel-bg);
+ border: 1px solid var(--node-border);
+ border-radius: 3px;
+ color: var(--text-primary);
+ font-size: 12px;
+ box-sizing: border-box;
+}
+
+.palette-search-input:focus {
+ outline: none;
+ border-color: var(--node-primary);
+}
+
+.palette-search-clear {
+ position: absolute;
+ right: 4px;
+ top: 50%;
+ transform: translateY(-50%);
+ width: 20px;
+ height: 20px;
+ background: #3d3d3d;
+ border: none;
+ border-radius: 3px;
+ color: var(--text-secondary);
+ font-size: 16px;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 0;
+ line-height: 1;
+}
+
+.palette-search-clear:hover {
+ background: var(--node-border);
+ color: var(--text-primary);
+}
+
+.no-results {
+ padding: 12px 8px;
+ color: var(--text-tertiary);
+ font-size: 12px;
+ text-align: center;
+ font-style: italic;
+}
+
+.node-palette-item mark {
+ background: var(--success-dark);
+ color: var(--text-inverse);
+ padding: 1px 2px;
+ border-radius: 2px;
+}
+
+/* Minimap */
+.node-minimap {
+ position: absolute;
+ bottom: 20px;
+ right: 20px;
+ width: 200px;
+ height: 150px;
+ background: rgba(40, 40, 40, 0.9);
+ border: 2px solid #555;
+ border-radius: 4px;
+ overflow: hidden;
+ pointer-events: all;
+ z-index: 100;
+}
+
+#minimap-canvas {
+ width: 100%;
+ height: 100%;
+ display: block;
+}
+
+.minimap-viewport {
+ position: absolute;
+ border: 2px solid #4CAF50;
+ background: rgba(76, 175, 80, 0.1);
+ pointer-events: none;
+ box-sizing: border-box;
+}
+
/* Node content styling */
.node-content {
padding: 8px;