feat(v0.3): M3 themes/font settings + M7 session dashboard

- M3: settings.ts (⚙ panel: dark/light/solarized theme + font size A-/A+),
  persisted to localStorage; terminal-session.applyTheme; TabApp.applySettings
  applies to all + new tabs
- M7: dashboard.ts (▦ overlay listing every tab with connection dot + folder +
  Claude status; click a row to focus; live re-render while open);
  TabApp.snapshot()/focusTab()
- Browser-verified: light theme renders + persists; dashboard lists tabs and
  focuses on click. 208 tests green, typechecks + build ok.
This commit is contained in:
Yaojia Wang
2026-06-18 05:40:35 +02:00
parent dcb1bf4845
commit f79f14b89d
6 changed files with 384 additions and 0 deletions

94
public/dashboard.ts Normal file
View File

@@ -0,0 +1,94 @@
/**
* public/dashboard.ts — all-sessions overview (M7).
*
* A ▦ toolbar button opens an overlay listing every tab with its connection
* dot, folder name, and Claude status; click a row to focus that tab. While
* open it re-renders every second so statuses stay live.
*/
export interface TabInfo {
idx: number
title: string
conn: string // SessionStatus
claude: string // ClaudeStatus
active: boolean
pending: boolean
}
export interface DashboardHooks {
snapshot: () => TabInfo[]
focus: (idx: number) => void
}
function claudeText(claude: string): string {
if (claude === 'working') return '⚙ working'
if (claude === 'waiting') return '⏳ needs approval'
if (claude === 'idle') return '✓ idle'
return ''
}
export function mountDashboard(toolbar: HTMLElement, hooks: DashboardHooks): void {
const overlay = document.createElement('div')
overlay.id = 'dashboard'
overlay.style.display = 'none'
const card = document.createElement('div')
card.className = 'dash-card'
overlay.appendChild(card)
document.body.appendChild(overlay)
let timer: ReturnType<typeof setInterval> | null = null
const render = (): void => {
card.replaceChildren()
const title = document.createElement('div')
title.className = 'dash-title'
title.textContent = 'Sessions'
card.appendChild(title)
for (const t of hooks.snapshot()) {
const row = document.createElement('div')
row.className = t.active ? 'dash-row active' : 'dash-row'
const dot = document.createElement('span')
dot.className = `tab-dot dot-${t.conn}`
const name = document.createElement('span')
name.className = 'dash-name'
name.textContent = t.title
const claude = document.createElement('span')
claude.className = t.pending ? 'dash-claude pending' : 'dash-claude'
claude.textContent = claudeText(t.claude)
row.append(dot, name, claude)
row.addEventListener('click', () => {
hooks.focus(t.idx)
hide()
})
card.appendChild(row)
}
}
const open = (): void => {
render()
overlay.style.display = 'flex'
timer = setInterval(render, 1000)
}
const hide = (): void => {
overlay.style.display = 'none'
if (timer !== null) {
clearInterval(timer)
timer = null
}
}
overlay.addEventListener('click', (e) => {
if (e.target === overlay) hide()
})
const btn = document.createElement('button')
btn.className = 'toolbtn'
btn.textContent = '▦'
btn.title = 'All sessions'
btn.setAttribute('aria-label', 'All sessions')
btn.addEventListener('click', () => (overlay.style.display === 'none' ? open() : hide()))
toolbar.appendChild(btn)
}