From 6ec8c304a953146b344366e849d908521cec872e Mon Sep 17 00:00:00 2001 From: Andy Dixon Date: Fri, 17 Apr 2026 00:00:00 +0100 Subject: [PATCH] Polish terminal search UI with case/regex toggles and match status Replaces the basic find bar with a polished overlay that matches the terminal-search CSS. Features: - Case sensitivity toggle (Aa button) - Regular expression toggle (.* button) - Match status indicator (found/no matches) - Smooth entrance/exit animation - Clears search decorations on close - Re-focuses existing bar if already open --- frontend/src/css/terminal.css | 13 ++++ frontend/src/js/app.js | 117 +++++++++++++++++++++++++++------- 2 files changed, 108 insertions(+), 22 deletions(-) diff --git a/frontend/src/css/terminal.css b/frontend/src/css/terminal.css index 9d0c7f3..cb92ae6 100644 --- a/frontend/src/css/terminal.css +++ b/frontend/src/css/terminal.css @@ -168,6 +168,19 @@ color: var(--text-tertiary); padding: 0 var(--space-1); white-space: nowrap; + min-width: 70px; +} + +.terminal-search-toggle { + font-family: var(--font-mono); + font-size: var(--text-xs); + opacity: 0.5; + transition: opacity var(--transition-fast), color var(--transition-fast); +} + +.terminal-search-toggle.is-active { + opacity: 1; + color: var(--accent); } /* ----------------------------------------------------------------------- diff --git a/frontend/src/js/app.js b/frontend/src/js/app.js index 9279d81..ef3eb72 100644 --- a/frontend/src/js/app.js +++ b/frontend/src/js/app.js @@ -577,47 +577,120 @@ class App { const sessionId = this.state.activeSessionId; if (!sessionId) return; - // Check if search bar already exists const panel = document.getElementById(`panel-${sessionId}`); - if (!panel || panel.querySelector('.terminal-search-bar')) return; + if (!panel) return; + + // If search bar already exists, just focus the input + const existing = panel.querySelector('.terminal-search'); + if (existing) { + const input = existing.querySelector('input'); + if (input) input.focus(); + return; + } const bar = document.createElement('div'); - bar.className = 'terminal-search-bar'; - bar.innerHTML = ` - - - - - `; + bar.className = 'terminal-search'; - const input = bar.querySelector('.terminal-search-input'); - const closeBtn = bar.querySelector('.terminal-search-close'); - const prevBtn = bar.querySelector('.terminal-search-prev'); - const nextBtn = bar.querySelector('.terminal-search-next'); + const input = document.createElement('input'); + input.type = 'text'; + input.placeholder = 'Find in terminal…'; + input.className = 'terminal-search-input'; - const doSearch = () => this.terminals.search(sessionId, input.value); - const doPrev = () => this.terminals.searchPrevious(sessionId, input.value); + const count = document.createElement('span'); + count.className = 'terminal-search-count'; + count.textContent = ''; + + const prevBtn = document.createElement('button'); + prevBtn.className = 'btn btn-xs btn-ghost'; + prevBtn.textContent = '▲'; + prevBtn.title = 'Previous match (Shift+Enter)'; + + const nextBtn = document.createElement('button'); + nextBtn.className = 'btn btn-xs btn-ghost'; + nextBtn.textContent = '▼'; + nextBtn.title = 'Next match (Enter)'; + + const caseSensBtn = document.createElement('button'); + caseSensBtn.className = 'btn btn-xs btn-ghost terminal-search-toggle'; + caseSensBtn.textContent = 'Aa'; + caseSensBtn.title = 'Toggle case sensitivity'; + let caseSensitive = false; + + const regexBtn = document.createElement('button'); + regexBtn.className = 'btn btn-xs btn-ghost terminal-search-toggle'; + regexBtn.textContent = '.*'; + regexBtn.title = 'Toggle regular expression'; + let useRegex = false; + + const closeBtn = document.createElement('button'); + closeBtn.className = 'btn btn-xs btn-ghost'; + closeBtn.textContent = '✕'; + closeBtn.title = 'Close (Escape)'; + + bar.appendChild(input); + bar.appendChild(count); + bar.appendChild(prevBtn); + bar.appendChild(nextBtn); + bar.appendChild(caseSensBtn); + bar.appendChild(regexBtn); + bar.appendChild(closeBtn); + + const searchOpts = () => ({ caseSensitive, regex: useRegex }); + + const doSearch = () => { + const found = this.terminals.search(sessionId, input.value, searchOpts()); + count.textContent = input.value ? (found ? 'Match found' : 'No matches') : ''; + }; + const doPrev = () => { + const found = this.terminals.searchPrevious(sessionId, input.value, searchOpts()); + count.textContent = input.value ? (found ? 'Match found' : 'No matches') : ''; + }; + + const closeSearch = () => { + // Clear search decorations + const entry = this.terminals.terminals.get(sessionId); + if (entry && entry.searchAddon) { + entry.searchAddon.clearDecorations(); + } + bar.classList.remove('is-visible'); + setTimeout(() => bar.remove(), 200); + this.terminals.focusTerminal(sessionId); + }; input.addEventListener('input', doSearch); input.addEventListener('keydown', (e) => { if (e.key === 'Enter') { + e.preventDefault(); e.shiftKey ? doPrev() : doSearch(); } if (e.key === 'Escape') { - bar.remove(); - this.terminals.focusTerminal(sessionId); + e.preventDefault(); + closeSearch(); } }); nextBtn.addEventListener('click', doSearch); prevBtn.addEventListener('click', doPrev); - closeBtn.addEventListener('click', () => { - bar.remove(); - this.terminals.focusTerminal(sessionId); + closeBtn.addEventListener('click', closeSearch); + + caseSensBtn.addEventListener('click', () => { + caseSensitive = !caseSensitive; + caseSensBtn.classList.toggle('is-active', caseSensitive); + doSearch(); }); - panel.prepend(bar); - input.focus(); + regexBtn.addEventListener('click', () => { + useRegex = !useRegex; + regexBtn.classList.toggle('is-active', useRegex); + doSearch(); + }); + + panel.appendChild(bar); + // Trigger the entrance animation + requestAnimationFrame(() => { + bar.classList.add('is-visible'); + input.focus(); + }); } /**