feat(v0.3): keybar Claude shortcuts + scrollback search + clickable links

- keybar: add Esc·Esc / ^O / ^T / ^B + per-button tooltips (KEY_MAP 14 keys)
- M1 search: @xterm/addon-search per terminal; 🔍 toolbar button + search box
  (Enter=next, Shift+Enter=prev, Esc=close)
- M2 links: @xterm/addon-web-links per terminal (tap URLs Claude prints)
- tab bar split into #tabs (scrollable) + #toolbar (utility cluster) for future
  utilities (QR/settings/dashboard)
- Browser-verified: 14 keybar buttons, search highlights matches, no console errors.
  199 tests green, typechecks + build ok.
This commit is contained in:
Yaojia Wang
2026-06-17 18:28:40 +02:00
parent 110c1752d4
commit eaeacf3a78
10 changed files with 576 additions and 42 deletions

65
public/search.ts Normal file
View File

@@ -0,0 +1,65 @@
/**
* public/search.ts — scrollback search box for the active terminal (M1).
*
* Adds a 🔍 button to the toolbar that toggles a small search overlay.
* Enter = next, Shift+Enter = previous, Esc = close.
*/
export interface SearchHooks {
find: (query: string, dir: 'next' | 'prev') => void
clear: () => void
}
function makeBtn(label: string, title: string): HTMLButtonElement {
const b = document.createElement('button')
b.textContent = label
b.title = title
b.setAttribute('aria-label', title)
return b
}
export function mountSearch(toolbar: HTMLElement, hooks: SearchHooks): void {
const box = document.createElement('div')
box.id = 'searchbox'
box.style.display = 'none'
const input = document.createElement('input')
input.type = 'text'
input.className = 'search-input'
input.placeholder = 'Search scrollback…'
const prev = makeBtn('↑', 'Previous match (Shift+Enter)')
const next = makeBtn('↓', 'Next match (Enter)')
const close = makeBtn('×', 'Close (Esc)')
box.append(input, prev, next, close)
document.body.appendChild(box)
const open = (): void => {
box.style.display = 'flex'
input.focus()
input.select()
}
const hide = (): void => {
box.style.display = 'none'
hooks.clear()
}
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault()
hooks.find(input.value, e.shiftKey ? 'prev' : 'next')
} else if (e.key === 'Escape') {
e.preventDefault()
hide()
}
e.stopPropagation()
})
prev.addEventListener('click', () => hooks.find(input.value, 'prev'))
next.addEventListener('click', () => hooks.find(input.value, 'next'))
close.addEventListener('click', hide)
const toggle = makeBtn('🔍', 'Search scrollback')
toggle.className = 'toolbtn'
toggle.addEventListener('click', () => (box.style.display === 'none' ? open() : hide()))
toolbar.appendChild(toggle)
}