- Light mode with the house ☀/◐ toggle and lt_theme preference; light terminal palette; Claude runs with dark-ansi so its colours follow the palette; tmux and the statusLine use ANSI colours. - Command palette (⌕, Ctrl+Shift+P) with session, terminal, view, link and help commands. - .lt-modal dialogs for new/rename/end and a keyboard shortcuts sheet. - .lt-toast results for actions, uploads, copy and CI. - ttyd dropped the reply of fast one-shot commands; replies now wait for the browser to hang up, and the client hangs up as soon as it has one. - Sessions panel re-fetches after an action instead of reusing a stale poll. Closes #31 Closes #32 Closes #33 Closes #34 Closes #35 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
63 lines
2.0 KiB
JavaScript
63 lines
2.0 KiB
JavaScript
// Dark / light theme, interchangeable with web_template's lt.theme (devdesk#31):
|
|
// same localStorage key (lt_theme), same data-theme attribute on <html>, the OS
|
|
// preference while nothing is saved, and tabs follow each other via `storage`.
|
|
// The inline script in index.template.html applies it before first paint.
|
|
|
|
const KEY = 'lt_theme';
|
|
const prefersLight = window.matchMedia('(prefers-color-scheme: light)');
|
|
const listeners = new Set();
|
|
|
|
function saved() {
|
|
try {
|
|
const value = localStorage.getItem(KEY);
|
|
return value === 'light' || value === 'dark' ? value : null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export function getTheme() {
|
|
return document.documentElement.getAttribute('data-theme') === 'light' ? 'light' : 'dark';
|
|
}
|
|
|
|
function apply(theme) {
|
|
const root = document.documentElement;
|
|
root.setAttribute('data-theme', theme);
|
|
root.style.colorScheme = theme;
|
|
document.querySelector('meta[name="theme-color"]')?.setAttribute('content', theme === 'light' ? '#edf0f5' : '#030508');
|
|
const next = theme === 'light' ? 'dark' : 'light';
|
|
for (const button of document.querySelectorAll('.lt-theme-btn')) {
|
|
button.textContent = theme === 'light' ? '◐' : '☀';
|
|
button.setAttribute('aria-label', `Switch to ${next} mode`);
|
|
button.title = `Switch to ${next} mode`;
|
|
}
|
|
for (const listener of listeners) listener(theme);
|
|
}
|
|
|
|
export function setTheme(theme) {
|
|
try {
|
|
localStorage.setItem(KEY, theme);
|
|
} catch {
|
|
// Private mode: the choice lasts until the tab closes.
|
|
}
|
|
apply(theme);
|
|
}
|
|
|
|
export function toggleTheme() {
|
|
setTheme(getTheme() === 'light' ? 'dark' : 'light');
|
|
}
|
|
|
|
export function onThemeChange(listener) {
|
|
listeners.add(listener);
|
|
}
|
|
|
|
export function initTheme() {
|
|
apply(saved() ?? (prefersLight.matches ? 'light' : 'dark'));
|
|
prefersLight.addEventListener('change', (event) => {
|
|
if (!saved()) apply(event.matches ? 'light' : 'dark');
|
|
});
|
|
window.addEventListener('storage', (event) => {
|
|
if (event.key === KEY) apply(saved() ?? (prefersLight.matches ? 'light' : 'dark'));
|
|
});
|
|
}
|