/** * 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 = { dark: { background: '#0e0f13', foreground: '#e7e8ec', cursor: '#7c8cff', selectionBackground: 'rgba(124,140,255,0.28)', }, light: { background: '#f6f7f9', foreground: '#1a1d24', cursor: '#5b6cff', selectionBackground: '#cdd6ff', }, solarized: { background: '#002b36', foreground: '#93a1a1', cursor: '#268bd2' }, } 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 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) }