From 583de6ec2d94bdb655c29267d680b640006aa8f3 Mon Sep 17 00:00:00 2001 From: Brian Krabach Date: Sun, 29 Mar 2026 19:19:05 -0700 Subject: [PATCH] =?UTF-8?q?feat:=20match=20preview=20styling=20to=20xterm.?= =?UTF-8?q?js=20=E2=80=94=20palette,=20background,=20font,=20line-height?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ANSI_COLORS: replaced with xterm.js default GTK/Tango palette. Background: #000000 on tile-body, sidebar-item-body, preview-popover. Font: 'SF Mono', 'Fira Code', Consolas, monospace (same as terminal). Line-height: 1.0 across all preview surfaces (xterm.js default). Gradient overlays: fade to #000 instead of theme bg. Result: previews are visually indistinguishable from the terminal. --- muxplex/frontend/app.js | 4 +- muxplex/frontend/style.css | 23 ++++--- muxplex/frontend/tests/test_app.mjs | 13 ++++ muxplex/tests/test_frontend_css.py | 98 +++++++++++++++++++++++++++-- 4 files changed, 122 insertions(+), 16 deletions(-) diff --git a/muxplex/frontend/app.js b/muxplex/frontend/app.js index b883084..0e21afe 100644 --- a/muxplex/frontend/app.js +++ b/muxplex/frontend/app.js @@ -269,8 +269,8 @@ function escapeHtml(str) { // Converts terminal color sequences to tags with inline styles. // --------------------------------------------------------------------------- var ANSI_COLORS = [ - '#000','#c23621','#25bc26','#adad27','#492ee1','#d338d3','#33bbc8','#cbcccd', - '#818383','#fc391f','#31e722','#eaec23','#5833ff','#f935f8','#14f0f0','#e9ebeb' + '#2e3436','#cc0000','#4e9a06','#c4a000','#3465a4','#75507b','#06989a','#d3d7cf', + '#555753','#ef2929','#8ae234','#fce94f','#729fcf','#ad7fa8','#34e2e2','#eeeeec' ]; function ansiToHtml(raw) { diff --git a/muxplex/frontend/style.css b/muxplex/frontend/style.css index c7be667..1093ff8 100644 --- a/muxplex/frontend/style.css +++ b/muxplex/frontend/style.css @@ -221,6 +221,7 @@ body { flex: 1; overflow: hidden; position: relative; + background: #000000; } .tile-body pre { @@ -229,8 +230,9 @@ body { left: 0; right: 0; padding: 6px 8px; - font-family: var(--font-mono); + font-family: 'SF Mono', 'Fira Code', Consolas, monospace; font-size: 11px; + line-height: 1.0; color: #c9d1d9; /* match xterm.js foreground */ white-space: pre; overflow: hidden; @@ -257,7 +259,7 @@ body { left: 0; right: 0; height: 40px; - background: linear-gradient(to bottom, transparent, var(--bg-tile)); + background: linear-gradient(to bottom, transparent, #000000); pointer-events: none; z-index: 1; } @@ -518,6 +520,7 @@ body { flex: 1; position: relative; overflow: hidden; + background: #000000; } .sidebar-item-body::before { @@ -527,13 +530,13 @@ body { left: 0; right: 0; height: 32px; - background: linear-gradient(to bottom, transparent, var(--bg-secondary)); + background: linear-gradient(to bottom, transparent, #000000); pointer-events: none; z-index: 1; } .sidebar-item--active .sidebar-item-body::before { - background: linear-gradient(to bottom, transparent, var(--bg-surface)); + background: linear-gradient(to bottom, transparent, #000000); } .sidebar-item-body pre { @@ -541,9 +544,9 @@ body { bottom: 0; left: 0; right: 0; - font-family: var(--font-mono); + font-family: 'SF Mono', 'Fira Code', Consolas, monospace; font-size: 10px; - line-height: 1.4; + line-height: 1.0; color: #c9d1d9; /* match xterm.js foreground */ white-space: pre; padding: 0 8px 6px; @@ -966,7 +969,7 @@ body { .preview-popover { position: fixed; z-index: 500; - background: var(--bg-secondary); + background: #000000; border: 2px solid var(--accent); border-radius: 8px; padding: 16px 20px; @@ -982,9 +985,9 @@ body { .preview-popover pre { margin: 0; - font-family: var(--font-mono); - font-size: 12px; - line-height: 1.5; + font-family: 'SF Mono', 'Fira Code', Consolas, monospace; + font-size: 14px; + line-height: 1.0; color: #c9d1d9; /* match xterm.js foreground exactly */ white-space: pre-wrap; /* WRAP text instead of horizontal overflow */ word-break: break-all; /* break long lines at viewport edge */ diff --git a/muxplex/frontend/tests/test_app.mjs b/muxplex/frontend/tests/test_app.mjs index e6dcf54..e6b982d 100644 --- a/muxplex/frontend/tests/test_app.mjs +++ b/muxplex/frontend/tests/test_app.mjs @@ -2114,4 +2114,17 @@ test('ansiToHtml converts SGR codes to styled spans', () => { assert.ok(source.includes('ansiToHtml(session.snapshot)'), 'overlay must use ansiToHtml'); }); +test('ANSI_COLORS uses xterm.js default GTK/Tango palette', () => { + // xterm.js default palette (GTK/Tango-derived) — must match exactly + // so previews render colors identically to the interactive terminal + assert.strictEqual(app.ansi256Color(0), '#2e3436', 'color 0 must be xterm.js default: #2e3436'); + assert.strictEqual(app.ansi256Color(1), '#cc0000', 'color 1 must be xterm.js default: #cc0000'); + assert.strictEqual(app.ansi256Color(2), '#4e9a06', 'color 2 must be xterm.js default: #4e9a06'); + assert.strictEqual(app.ansi256Color(3), '#c4a000', 'color 3 must be xterm.js default: #c4a000'); + assert.strictEqual(app.ansi256Color(7), '#d3d7cf', 'color 7 must be xterm.js default: #d3d7cf'); + assert.strictEqual(app.ansi256Color(8), '#555753', 'color 8 must be xterm.js default: #555753'); + assert.strictEqual(app.ansi256Color(10), '#8ae234', 'color 10 must be xterm.js default: #8ae234'); + assert.strictEqual(app.ansi256Color(14), '#34e2e2', 'color 14 must be xterm.js default: #34e2e2'); +}); + diff --git a/muxplex/tests/test_frontend_css.py b/muxplex/tests/test_frontend_css.py index d0f7f18..b88eb40 100644 --- a/muxplex/tests/test_frontend_css.py +++ b/muxplex/tests/test_frontend_css.py @@ -403,7 +403,7 @@ def test_css_sidebar_item_body(): def test_css_sidebar_item_body_pre(): - """.sidebar-item-body pre must be anchored to bottom with 10px monospace font using --font-mono token.""" + """.sidebar-item-body pre must be anchored to bottom with 10px monospace font matching xterm.js.""" css = read_css() assert ".sidebar-item-body pre" in css block = _extract_rule_block(css, ".sidebar-item-body pre {") @@ -412,12 +412,12 @@ def test_css_sidebar_item_body_pre(): assert "left: 0" in block assert "right: 0" in block assert "font-size: 10px" in block - assert "line-height: 1.4" in block + assert "line-height: 1.0" in block, "sidebar-item-body pre must use line-height: 1.0 (xterm.js default)" assert "#c9d1d9" in block, "sidebar-item-body pre must match xterm.js foreground" assert "white-space: pre" in block assert "padding: 0 8px 6px" in block - # Monospace font family via design token - assert "font-family: var(--font-mono)" in block + # Explicit xterm.js font family (not design token variable) + assert "'SF Mono'" in block or "SF Mono" in block, "sidebar-item-body pre must use explicit xterm.js font family" def test_css_sidebar_empty(): @@ -640,3 +640,93 @@ def test_preview_popover_has_accent_border(): end = css.index("}", start) block = css[start:end] assert "var(--accent)" in block, ".preview-popover must use var(--accent) border" + + +def test_preview_popover_has_black_background(): + """.preview-popover must use #000000 background to match xterm.js terminal.""" + css = read_css() + start = css.index(".preview-popover {") + end = css.index("}", start) + block = css[start:end] + assert "#000000" in block or "background: #000" in block, ( + ".preview-popover must use #000000 background (not var(--bg-secondary))" + ) + + +def test_preview_popover_pre_matches_xterm_typography(): + """.preview-popover pre must match xterm.js terminal: 14px, line-height 1.0, explicit font stack.""" + css = read_css() + block = _extract_rule_block(css, ".preview-popover pre {") + assert "font-size: 14px" in block, ".preview-popover pre must use 14px (xterm.js default)" + assert "line-height: 1.0" in block, ".preview-popover pre must use line-height: 1.0 (xterm.js default)" + assert "'SF Mono'" in block or "SF Mono" in block, ( + ".preview-popover pre must use explicit xterm.js font family" + ) + + +def test_tile_body_has_black_background(): + """.tile-body must use #000000 background to match xterm.js terminal.""" + css = read_css() + block = _extract_rule_block(css, ".tile-body {") + assert "#000000" in block or "background: #000" in block, ( + ".tile-body must use #000000 background to match xterm.js" + ) + + +def test_tile_body_pre_has_xterm_line_height(): + """.tile-body pre must use line-height: 1.0 to match xterm.js terminal.""" + css = read_css() + block = _extract_rule_block(css, ".tile-body pre {") + assert "line-height: 1.0" in block, ".tile-body pre must use line-height: 1.0 (xterm.js default)" + + +def test_tile_body_pre_has_explicit_font_family(): + """.tile-body pre must use explicit xterm.js font family, not CSS variable.""" + css = read_css() + block = _extract_rule_block(css, ".tile-body pre {") + assert "'SF Mono'" in block or "SF Mono" in block, ( + ".tile-body pre must use explicit xterm.js font family" + ) + + +def test_sidebar_item_body_has_black_background(): + """.sidebar-item-body must use #000000 background to match xterm.js terminal.""" + css = read_css() + block = _extract_rule_block(css, ".sidebar-item-body {") + assert "#000000" in block or "background: #000" in block, ( + ".sidebar-item-body must use #000000 background to match xterm.js" + ) + + +def test_sidebar_item_body_pre_has_xterm_line_height(): + """.sidebar-item-body pre must use line-height: 1.0 to match xterm.js terminal.""" + css = read_css() + block = _extract_rule_block(css, ".sidebar-item-body pre {") + assert "line-height: 1.0" in block, ".sidebar-item-body pre must use line-height: 1.0 (xterm.js default)" + + +def test_sidebar_item_body_pre_has_explicit_font_family(): + """.sidebar-item-body pre must use explicit xterm.js font family, not CSS variable.""" + css = read_css() + block = _extract_rule_block(css, ".sidebar-item-body pre {") + assert "'SF Mono'" in block or "SF Mono" in block, ( + ".sidebar-item-body pre must use explicit xterm.js font family" + ) + + +def test_sidebar_item_body_gradient_fades_to_black(): + """.sidebar-item-body::before gradient must fade to #000000 (not theme background).""" + css = read_css() + block = _extract_rule_block(css, ".sidebar-item-body::before {") + assert "#000000" in block or "transparent, #000" in block, ( + ".sidebar-item-body::before gradient must fade to #000000 to match black terminal bg" + ) + + +def test_tile_body_gradient_fades_to_black(): + """.tile-body::before gradient must fade to #000000 (not theme background).""" + css = read_css() + block = _extract_rule_block(css, ".tile-body::before {") + assert "#000000" in block or "transparent, #000" in block, ( + ".tile-body::before gradient must fade to #000000 to match black terminal bg" + )