terminal: theme picker (carbon/slate/mono/ink) replacing violet

Drops the hardcoded violet/indigo palette. Four themes are defined as
CSS variables on body.theme-{carbon,slate,mono,ink}:

- Carbon (default): warm dark, ivory text, restrained amber accent.
- Slate: cool dark, GitHub/Linear-ish charcoal with electric blue.
- Mono: strict greyscale, off-white accent.
- Ink: warm paper light, deep ink, terracotta accent.

The lobby reads the choice from localStorage and applies the class
before render. The picker lives at the bottom of the sidebar
(margin-top: auto pins it). On change, the iframe is bounced through
about:blank so the inner xterm picks up the new computed CSS vars
(--terminal-bg/fg/cursor/selection) on the next mount.

Picker UI uses native buttons, current theme highlighted with the
accent border + color. No gradients, hairline borders only.
This commit is contained in:
Viktor Barzin 2026-05-13 20:46:21 +00:00 committed by Viktor Barzin
parent cbe83597c0
commit a44aa52e1a

View file

@ -6,50 +6,118 @@
<title>Terminal</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/css/xterm.min.css">
<style>
html, body { margin: 0; padding: 0; height: 100%; overflow: hidden; background: #000; }
/* Themes — set on <body> as theme-{carbon,slate,mono,ink}. Carbon is the default. */
body.theme-carbon {
--bg-page: #0c0c0a;
--bg-sidebar: #131311;
--bg-card: #1a1916;
--bg-card-hover: #22211d;
--text-primary: #e8e3d6;
--text-muted: #8a8474;
--border: #2a2826;
--border-strong: #3a3733;
--accent: #d4a574;
--danger: #c47a5a;
--success: #88a888;
--terminal-bg: #0c0c0a;
--terminal-fg: #e8e3d6;
--terminal-cursor: #d4a574;
--terminal-selection: rgba(212,165,116,0.22);
}
body.theme-slate {
--bg-page: #0d1117;
--bg-sidebar: #11151c;
--bg-card: #161b22;
--bg-card-hover: #1c2128;
--text-primary: #e6e8eb;
--text-muted: #7d8590;
--border: #1f242d;
--border-strong: #30363d;
--accent: #4493f8;
--danger: #f47067;
--success: #56d364;
--terminal-bg: #0d1117;
--terminal-fg: #e6e8eb;
--terminal-cursor: #4493f8;
--terminal-selection: rgba(68,147,248,0.22);
}
body.theme-mono {
--bg-page: #0d0d0d;
--bg-sidebar: #131313;
--bg-card: #1a1a1a;
--bg-card-hover: #232323;
--text-primary: #e0e0e0;
--text-muted: #7d7d7d;
--border: #2a2a2a;
--border-strong: #3a3a3a;
--accent: #e8e8e8;
--danger: #cc7070;
--success: #9ccb9c;
--terminal-bg: #0d0d0d;
--terminal-fg: #e0e0e0;
--terminal-cursor: #e8e8e8;
--terminal-selection: rgba(255,255,255,0.18);
}
body.theme-ink {
--bg-page: #faf7f2;
--bg-sidebar: #f1ebda;
--bg-card: #ffffff;
--bg-card-hover: #f4eee0;
--text-primary: #1d1b18;
--text-muted: #6e6862;
--border: #d8d2c4;
--border-strong: #b8b0a0;
--accent: #b5482d;
--danger: #b5482d;
--success: #3d6b3d;
--terminal-bg: #faf7f2;
--terminal-fg: #1d1b18;
--terminal-cursor: #b5482d;
--terminal-selection: rgba(181,72,45,0.18);
}
html, body {
margin: 0; padding: 0; height: 100%; overflow: hidden;
background: var(--bg-page, #0c0c0a);
color: var(--text-primary, #e8e3d6);
}
#terminal { height: 100%; width: 100%; }
.hidden { display: none !important; }
#toast {
position: fixed; top: 16px; right: 16px; z-index: 9999;
background: #1a1a2e; color: #a29bfe; border: 1px solid #333;
border-radius: 8px; padding: 10px 18px; font-family: monospace;
background: var(--bg-card); color: var(--text-primary);
border: 1px solid var(--border); border-radius: 8px;
padding: 10px 18px; font-family: monospace;
font-size: 14px; opacity: 0; transition: opacity 0.3s;
pointer-events: none; max-width: 500px; word-break: break-all;
}
#toast.visible { opacity: 1; }
#toast.error { color: #e74c3c; border-color: #e74c3c; }
#toast.success { color: #2ecc71; border-color: #2ecc71; }
#paste-btn {
position: fixed; bottom: 24px; right: 24px; z-index: 9999;
#toast.error { color: var(--danger); border-color: var(--danger); }
#toast.success { color: var(--success); border-color: var(--success); }
#paste-btn, #img-btn {
position: fixed; bottom: 24px; z-index: 9999;
width: 48px; height: 48px; border-radius: 12px;
background: rgba(108, 92, 231, 0.6); border: 1px solid rgba(162, 155, 254, 0.4);
color: #eee; font-size: 22px; cursor: pointer;
background: color-mix(in srgb, var(--accent) 22%, transparent);
border: 1px solid color-mix(in srgb, var(--accent) 40%, transparent);
color: var(--text-primary); font-size: 22px; cursor: pointer;
display: flex; align-items: center; justify-content: center;
backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
transition: background 0.2s, transform 0.1s;
touch-action: manipulation; -webkit-tap-highlight-color: transparent;
}
#paste-btn:hover { background: rgba(108, 92, 231, 0.85); }
#paste-btn:active { transform: scale(0.92); }
#img-btn {
position: fixed; bottom: 24px; right: 80px; z-index: 9999;
width: 48px; height: 48px; border-radius: 12px;
background: rgba(108, 92, 231, 0.6); border: 1px solid rgba(162, 155, 254, 0.4);
color: #eee; font-size: 22px; cursor: pointer;
display: flex; align-items: center; justify-content: center;
backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
transition: background 0.2s, transform 0.1s;
touch-action: manipulation; -webkit-tap-highlight-color: transparent;
#paste-btn { right: 24px; }
#img-btn { right: 80px; }
#paste-btn:hover, #img-btn:hover {
background: color-mix(in srgb, var(--accent) 35%, transparent);
}
#img-btn:hover { background: rgba(108, 92, 231, 0.85); }
#img-btn:active { transform: scale(0.92); }
#paste-btn:active, #img-btn:active { transform: scale(0.92); }
#img-input { display: none; }
/* Lobby */
#lobby {
display: none; height: 100vh; box-sizing: border-box;
font-family: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', Menlo, Monaco, 'Courier New', monospace;
color: #eee; background: #1a1a2e;
color: var(--text-primary); background: var(--bg-sidebar);
}
#lobby.visible { display: block; }
#lobby.access-denied { padding: 32px; overflow-y: auto; }
@ -58,81 +126,114 @@
display: grid; grid-template-columns: 260px 1fr; height: 100vh;
}
#lobby-sidebar {
padding: 20px 14px; overflow-y: auto; border-right: 1px solid #2a2a3f;
background: #1a1a2e; min-width: 0;
padding: 20px 14px; overflow-y: auto;
border-right: 1px solid var(--border);
background: var(--bg-sidebar); min-width: 0;
display: flex; flex-direction: column;
}
#lobby-content {
position: relative; background: #000; min-width: 0;
height: 100vh; overflow: hidden;
position: relative; background: var(--terminal-bg);
min-width: 0; height: 100vh; overflow: hidden;
}
#session-frame { width: 100%; height: 100%; border: 0; display: block; }
#session-frame.hidden { display: none; }
#lobby-empty {
color: #888; display: flex; align-items: center; justify-content: center;
color: var(--text-muted); display: flex; align-items: center; justify-content: center;
height: 100%; font-style: italic; padding: 24px; text-align: center;
}
#lobby-empty.hidden { display: none; }
.lobby-header { font-size: 18px; color: #a29bfe; margin: 0 0 4px 0; }
.lobby-sub { color: #888; font-size: 12px; margin: 0 0 18px 0; line-height: 1.5; }
.lobby-header { font-size: 18px; color: var(--accent); margin: 0 0 4px 0; font-weight: 600; }
.lobby-sub { color: var(--text-muted); font-size: 12px; margin: 0 0 18px 0; line-height: 1.5; }
.new-row { display: flex; flex-direction: column; gap: 6px; margin-bottom: 18px; }
.new-row input {
box-sizing: border-box; padding: 8px 12px; border-radius: 8px;
border: 1px solid #333; background: #0f0f1f; color: #eee;
font-family: inherit; font-size: 13px;
border: 1px solid var(--border); background: var(--bg-card);
color: var(--text-primary); font-family: inherit; font-size: 13px;
}
.new-row input:focus { outline: none; border-color: #a29bfe; }
.new-row input:focus { outline: none; border-color: var(--accent); }
.new-row button {
padding: 8px 12px; border-radius: 8px;
border: 1px solid rgba(162, 155, 254, 0.4);
background: rgba(108, 92, 231, 0.6); color: #eee;
font-family: inherit; font-size: 13px; cursor: pointer;
border: 1px solid color-mix(in srgb, var(--accent) 45%, transparent);
background: color-mix(in srgb, var(--accent) 18%, transparent);
color: var(--text-primary); font-family: inherit; font-size: 13px; cursor: pointer;
}
.new-row button:hover {
background: color-mix(in srgb, var(--accent) 30%, transparent);
}
.new-row button:hover { background: rgba(108, 92, 231, 0.85); }
.session-list { display: flex; flex-direction: column; gap: 6px; }
.session-card {
display: flex; align-items: center; justify-content: space-between;
background: #0f0f1f; border: 1px solid #2a2a3f;
background: var(--bg-card); border: 1px solid var(--border);
border-left: 3px solid transparent; border-radius: 8px;
padding: 10px 12px; cursor: pointer;
transition: background 0.15s, border-color 0.15s;
font-family: inherit; color: inherit; text-align: left;
font-family: inherit; color: var(--text-primary); text-align: left;
}
.session-card:hover { background: #161629; }
.session-card:hover { background: var(--bg-card-hover); }
.session-card:focus-visible {
outline: 2px solid rgba(162, 155, 254, 0.6); outline-offset: -2px;
outline: 2px solid color-mix(in srgb, var(--accent) 60%, transparent);
outline-offset: -2px;
}
.session-card.active {
background: #161629;
border-color: rgba(162, 155, 254, 0.5);
border-left-color: rgba(162, 155, 254, 0.95);
background: var(--bg-card-hover);
border-color: color-mix(in srgb, var(--accent) 50%, transparent);
border-left-color: var(--accent);
}
.session-meta {
display: flex; flex-direction: column; gap: 2px;
min-width: 0; flex: 1;
}
.session-name {
font-size: 14px; font-weight: 600; color: #eee;
font-size: 14px; font-weight: 600; color: var(--text-primary);
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.session-detail { font-size: 11px; color: #888; }
.session-detail.attached { color: #2ecc71; }
.session-detail { font-size: 11px; color: var(--text-muted); }
.session-detail.attached { color: var(--success); }
.session-actions { display: flex; gap: 6px; flex-shrink: 0; }
.session-actions button {
padding: 4px 8px; border-radius: 6px; border: 1px solid #333;
background: #1f1f33; color: #eee; font-family: inherit;
font-size: 11px; cursor: pointer;
padding: 4px 8px; border-radius: 6px; border: 1px solid var(--border);
background: var(--bg-card-hover); color: var(--text-primary);
font-family: inherit; font-size: 11px; cursor: pointer;
}
.session-actions button.rename {
color: #a29bfe; border-color: rgba(162, 155, 254, 0.4);
color: var(--accent);
border-color: color-mix(in srgb, var(--accent) 45%, transparent);
}
.session-actions button.kill {
color: #e74c3c; border-color: rgba(231, 76, 60, 0.4);
color: var(--danger);
border-color: color-mix(in srgb, var(--danger) 45%, transparent);
}
.session-actions button:hover { filter: brightness(1.3); }
.session-actions button:hover { filter: brightness(1.25); }
.session-card.dragging { opacity: 0.4; cursor: grabbing; }
.empty { color: #888; font-style: italic; padding: 12px 0; font-size: 12px; }
.empty { color: var(--text-muted); font-style: italic; padding: 12px 0; font-size: 12px; }
/* Theme picker — sits at the bottom of the sidebar */
.theme-picker {
margin-top: auto; padding-top: 14px;
border-top: 1px solid var(--border);
font-size: 11px; color: var(--text-muted);
}
.theme-picker-label {
display: block; margin-bottom: 6px;
text-transform: uppercase; letter-spacing: 0.06em;
}
.theme-options { display: flex; gap: 4px; }
.theme-options button {
flex: 1; padding: 5px 4px; border-radius: 6px;
background: var(--bg-card); color: var(--text-muted);
border: 1px solid var(--border); font-family: inherit;
font-size: 11px; cursor: pointer;
transition: background 0.15s, color 0.15s, border-color 0.15s;
}
.theme-options button:hover {
background: var(--bg-card-hover); color: var(--text-primary);
}
.theme-options button.active {
border-color: var(--accent); color: var(--accent);
background: var(--bg-card-hover);
}
@media (max-width: 720px) {
#lobby-shell {
@ -140,7 +241,7 @@
grid-template-rows: auto 60vh;
}
#lobby-sidebar {
border-right: 0; border-bottom: 1px solid #2a2a3f;
border-right: 0; border-bottom: 1px solid var(--border);
max-height: 40vh;
}
#lobby-content { height: 60vh; }
@ -159,6 +260,15 @@
<button id="new-btn">Create &amp; Open</button>
</div>
<div id="session-list" class="session-list"></div>
<div id="theme-picker" class="theme-picker">
<span class="theme-picker-label">Theme</span>
<div class="theme-options">
<button type="button" data-theme="carbon">Carbon</button>
<button type="button" data-theme="slate">Slate</button>
<button type="button" data-theme="mono">Mono</button>
<button type="button" data-theme="ink">Ink</button>
</div>
</div>
</aside>
<main id="lobby-content">
<div id="lobby-empty">Pick a session, or create one above.</div>
@ -184,6 +294,31 @@
const rawArg = params.get('arg');
const validArg = rawArg && NAME_RE.test(rawArg) ? rawArg : null;
// Theme — shared by lobby + terminal-mode iframe (same-origin localStorage).
const THEMES = ['carbon', 'slate', 'mono', 'ink'];
const THEME_KEY = 'tmux-theme';
function getTheme() {
try {
const t = localStorage.getItem(THEME_KEY);
return THEMES.includes(t) ? t : 'carbon';
} catch (e) { return 'carbon'; }
}
function applyTheme(t) {
THEMES.forEach(x => document.body.classList.remove('theme-' + x));
document.body.classList.add('theme-' + t);
}
function readTerminalTheme() {
const cs = getComputedStyle(document.body);
const v = k => cs.getPropertyValue(k).trim();
return {
background: v('--terminal-bg') || '#0c0c0a',
foreground: v('--terminal-fg') || '#e8e3d6',
cursor: v('--terminal-cursor') || '#d4a574',
selectionBackground: v('--terminal-selection') || 'rgba(255,255,255,0.2)'
};
}
applyTheme(getTheme());
function showToast(msg, type, duration) {
const el = document.getElementById('toast');
el.textContent = msg;
@ -534,6 +669,32 @@
}
});
// Theme picker — clicking a swatch swaps body class, persists, and
// reloads the iframe so the inner terminal re-themes too.
const themePickerEl = document.getElementById('theme-picker');
function paintThemeButtons() {
const active = getTheme();
themePickerEl.querySelectorAll('button[data-theme]').forEach(b => {
b.classList.toggle('active', b.dataset.theme === active);
});
}
themePickerEl.querySelectorAll('button[data-theme]').forEach(btn => {
btn.addEventListener('click', () => {
const t = btn.dataset.theme;
if (!THEMES.includes(t)) return;
try { localStorage.setItem(THEME_KEY, t); } catch (e) {}
applyTheme(t);
paintThemeButtons();
if (currentActive && frameEl.src) {
// Reload iframe so xterm picks up the new theme.
const url = frameEl.src;
frameEl.src = 'about:blank';
setTimeout(() => { frameEl.src = url; }, 0);
}
});
});
paintThemeButtons();
renderLobby();
setInterval(renderLobby, 5000);
@ -563,12 +724,7 @@
cursorBlink: true,
fontFamily: "'JetBrains Mono', 'Fira Code', 'Cascadia Code', Menlo, Monaco, 'Courier New', monospace",
fontSize: 15,
theme: {
background: '#1a1a2e',
foreground: '#eee',
cursor: '#a29bfe',
selectionBackground: 'rgba(162, 155, 254, 0.3)'
},
theme: readTerminalTheme(),
allowProposedApi: true
});