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

128
public/settings.ts Normal file
View File

@@ -0,0 +1,128 @@
/**
* public/settings.ts — theme + font-size settings (M3).
*
* A ⚙ toolbar button opens a small panel; choices persist to localStorage and
* apply to every terminal via the onChange callback.
*/
import type { ITheme } from '@xterm/xterm'
export interface Settings {
theme: string
fontSize: number
}
export const THEMES: Record<string, ITheme> = {
dark: { background: '#1a1a1a', foreground: '#e8e8e8', cursor: '#e8e8e8' },
light: {
background: '#f5f5f5',
foreground: '#1a1a1a',
cursor: '#1a1a1a',
selectionBackground: '#bcd3f5',
},
solarized: { background: '#002b36', foreground: '#93a1a1', cursor: '#93a1a1' },
}
const KEY = 'web-terminal:settings'
export const DEFAULT_SETTINGS: Settings = { theme: 'dark', fontSize: 14 }
export function loadSettings(): Settings {
try {
const raw = localStorage.getItem(KEY)
if (raw) {
const s = JSON.parse(raw) as Partial<Settings>
return {
theme: typeof s.theme === 'string' && THEMES[s.theme] ? s.theme : DEFAULT_SETTINGS.theme,
fontSize:
typeof s.fontSize === 'number' && s.fontSize >= 10 && s.fontSize <= 24
? s.fontSize
: DEFAULT_SETTINGS.fontSize,
}
}
} catch {
// ignore
}
return { ...DEFAULT_SETTINGS }
}
function saveSettings(s: Settings): void {
try {
localStorage.setItem(KEY, JSON.stringify(s))
} catch {
// ignore
}
}
export function mountSettings(
toolbar: HTMLElement,
current: () => Settings,
onChange: (s: Settings) => void,
): void {
const panel = document.createElement('div')
panel.id = 'settingspanel'
panel.style.display = 'none'
const apply = (next: Settings): void => {
saveSettings(next)
onChange(next)
render()
}
function render(): void {
const s = current()
panel.replaceChildren()
const themeRow = document.createElement('div')
themeRow.className = 'settings-row'
themeRow.append(label('Theme'))
for (const key of Object.keys(THEMES)) {
const b = document.createElement('button')
b.textContent = key
b.className = key === s.theme ? 'settings-opt active' : 'settings-opt'
b.addEventListener('click', () => apply({ ...s, theme: key }))
themeRow.appendChild(b)
}
const fontRow = document.createElement('div')
fontRow.className = 'settings-row'
fontRow.append(label('Font'))
const minus = document.createElement('button')
minus.className = 'settings-opt'
minus.textContent = 'A'
minus.addEventListener('click', () => apply({ ...s, fontSize: Math.max(10, s.fontSize - 1) }))
const size = document.createElement('span')
size.className = 'settings-size'
size.textContent = String(s.fontSize)
const plus = document.createElement('button')
plus.className = 'settings-opt'
plus.textContent = 'A+'
plus.addEventListener('click', () => apply({ ...s, fontSize: Math.min(24, s.fontSize + 1) }))
fontRow.append(minus, size, plus)
panel.append(themeRow, fontRow)
}
function label(text: string): HTMLElement {
const el = document.createElement('span')
el.className = 'settings-label'
el.textContent = text
return el
}
document.body.appendChild(panel)
const toggle = document.createElement('button')
toggle.className = 'toolbtn'
toggle.textContent = '⚙'
toggle.title = 'Settings (theme, font)'
toggle.setAttribute('aria-label', 'Settings')
toggle.addEventListener('click', () => {
if (panel.style.display === 'none') {
render()
panel.style.display = 'block'
} else {
panel.style.display = 'none'
}
})
toolbar.appendChild(toggle)
}