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:
128
public/settings.ts
Normal file
128
public/settings.ts
Normal 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)
|
||||
}
|
||||
Reference in New Issue
Block a user