From d26bc2091863592e01c1f16331d3049390bc3d41 Mon Sep 17 00:00:00 2001 From: Kevin Buffardi Date: Thu, 29 Oct 2026 23:26:15 -0700 Subject: [PATCH 01/13] feat: register monochrome visual themes --- scripts/e2e-settings-visual-theme.test.mjs | 33 +++++++++++++++++ src/ui/themes.mjs | 41 ++++++++++++++++++---- 2 files changed, 67 insertions(+), 7 deletions(-) diff --git a/scripts/e2e-settings-visual-theme.test.mjs b/scripts/e2e-settings-visual-theme.test.mjs index 34c49c7..4ffed73 100644 --- a/scripts/e2e-settings-visual-theme.test.mjs +++ b/scripts/e2e-settings-visual-theme.test.mjs @@ -22,6 +22,7 @@ test('theme registry exposes the requested Monaco themes and safe fallback', () assert.equal(DEFAULT_VISUAL_THEME_ID, 'browser-cpp-dark'); assert.deepEqual(VISUAL_THEMES.map(({ id }) => id), [ 'browser-cpp-dark', 'vs', 'vs-dark', 'hc-light', 'hc-black', + 'monochrome-light', 'monochrome-dark', ]); assert.equal(getVisualTheme('not-a-theme').id, DEFAULT_VISUAL_THEME_ID); for (const theme of VISUAL_THEMES) { @@ -34,10 +35,25 @@ test('theme registry exposes the requested Monaco themes and safe fallback', () } assert.deepEqual(VISUAL_THEMES.map(({ id }) => id), [ 'browser-cpp-dark', 'vs', 'vs-dark', 'hc-light', 'hc-black', + 'monochrome-light', 'monochrome-dark', ]); assert.deepEqual(VISUAL_THEMES.slice(1).map(({ monacoTheme }) => monacoTheme), [ 'browser-cpp-vs', 'browser-cpp-vs-dark', 'browser-cpp-hc-light', 'browser-cpp-hc-black', + 'browser-cpp-monochrome-light', 'browser-cpp-monochrome-dark', ]); + + for (const theme of VISUAL_THEMES.slice(-2)) { + const colors = [ + ...Object.values(theme.cssTokens), + ...Object.values(theme.terminalTheme), + ...Object.values(theme.preview), + theme.selection.foreground, + theme.selection.background, + theme.selection.inactiveBackground, + ].filter((value) => typeof value === 'string' && value.startsWith('#')); + assert.ok(colors.length > 0); + assert.ok(colors.every(isGrayscale), `${theme.id} contains a chromatic color`); + } }); test('terminal viewport inherits the active visual theme background', async () => { @@ -77,3 +93,20 @@ test('visual theme defaults, sanitizes legacy storage, and persists immediately' assert.deepEqual(settings.get(), { memoryDebugMode: true, visualTheme: 'vs' }); assert.deepEqual(storage.value, { memoryDebugMode: true, visualTheme: 'vs' }); }); + +test('monochrome themes persist and unknown values still use the existing default', async () => { + const storage = createStorage({ memoryDebugMode: false, visualTheme: 'monochrome-light' }); + const settings = createExtensionSettings({ storage }); + assert.equal((await settings.load()).visualTheme, 'monochrome-light'); + await settings.setVisualTheme('monochrome-dark'); + assert.equal(storage.value.visualTheme, 'monochrome-dark'); + + const invalidStorage = createStorage({ visualTheme: 'not-a-theme' }); + const invalidSettings = createExtensionSettings({ storage: invalidStorage }); + assert.equal((await invalidSettings.load()).visualTheme, DEFAULT_VISUAL_THEME_ID); +}); + +function isGrayscale(color) { + const channels = color.slice(1).match(/.{2}/g)?.map((channel) => parseInt(channel, 16)); + return channels?.length === 3 && channels[0] === channels[1] && channels[1] === channels[2]; +} diff --git a/src/ui/themes.mjs b/src/ui/themes.mjs index 2f792bb..2a2fa17 100644 --- a/src/ui/themes.mjs +++ b/src/ui/themes.mjs @@ -2,16 +2,30 @@ export const DEFAULT_VISUAL_THEME_ID = 'browser-cpp-dark'; -const terminal = (background, foreground, accent, selection) => ({ +const DEFAULT_ANSI_COLORS = { + black: foreground => foreground, red: '#c01c28', green: '#2d7d46', yellow: '#9a6700', + blue: '#0969da', magenta: '#8250df', cyan: '#0a7b83', white: foreground => foreground, + brightBlack: '#768390', brightRed: '#d1242f', brightGreen: '#1a7f37', + brightYellow: '#bf8700', brightBlue: '#218bff', brightMagenta: '#a475f9', + brightCyan: '#1b9aaa', brightWhite: foreground => foreground, +}; + +const terminal = (background, foreground, accent, selection, ansi = DEFAULT_ANSI_COLORS) => ({ background, foreground, cursor: accent, cursorAccent: background, selectionForeground: selection.foreground, selectionBackground: selection.background, selectionInactiveBackground: selection.inactiveBackground, - black: foreground, red: '#c01c28', green: '#2d7d46', yellow: '#9a6700', - blue: '#0969da', magenta: '#8250df', cyan: '#0a7b83', white: foreground, - brightBlack: '#768390', brightRed: '#d1242f', brightGreen: '#1a7f37', - brightYellow: '#bf8700', brightBlue: '#218bff', brightMagenta: '#a475f9', - brightCyan: '#1b9aaa', brightWhite: foreground, + ...Object.fromEntries(Object.entries(ansi).map(([key, value]) => [ + key, typeof value === 'function' ? value(foreground) : value, + ])), +}); + +const monochromeAnsi = (foreground, tertiary) => ({ + black: foreground, red: tertiary, green: tertiary, yellow: tertiary, + blue: tertiary, magenta: tertiary, cyan: tertiary, white: foreground, + brightBlack: tertiary, brightRed: foreground, brightGreen: foreground, + brightYellow: foreground, brightBlue: foreground, brightMagenta: foreground, + brightCyan: foreground, brightWhite: foreground, }); const visualTheme = ({ id, label, monacoTheme, selection, cssTokens, terminalColors, preview }) => ({ @@ -24,7 +38,8 @@ const visualTheme = ({ id, label, monacoTheme, selection, cssTokens, terminalCol terminalColors.background, terminalColors.foreground, terminalColors.accent, - selection + selection, + terminalColors.ansi ), preview, }); @@ -60,6 +75,18 @@ export const VISUAL_THEMES = Object.freeze([ cssTokens: { crust: '#000000', mantle: '#000000', base: '#000000', surface0: '#1f1f1f', surface1: '#ffffff', overlay0: '#d0d0d0', text: '#ffffff', subtext0: '#ffffff', green: '#00ff00', red: '#ff6b6b', yellow: '#ffff00', blue: '#3daee9', mauve: '#ff8cff', peach: '#ffb86c' }, terminalColors: { background: '#000000', foreground: '#ffffff', accent: '#ffffff' }, preview: { background: '#000000', foreground: '#ffffff', accent: '#ffff00' }, }), + visualTheme({ + id: 'monochrome-light', label: 'Monochrome Light', monacoTheme: 'browser-cpp-monochrome-light', + selection: { foreground: '#ffffff', background: '#666666', inactiveBackground: '#999999' }, + cssTokens: { crust: '#ffffff', mantle: '#ffffff', base: '#ffffff', surface0: '#ffffff', surface1: '#666666', overlay0: '#666666', text: '#000000', subtext0: '#666666', green: '#000000', red: '#666666', yellow: '#666666', blue: '#000000', mauve: '#000000', peach: '#666666' }, + terminalColors: { background: '#ffffff', foreground: '#000000', accent: '#666666', ansi: monochromeAnsi('#000000', '#666666') }, preview: { background: '#ffffff', foreground: '#000000', accent: '#666666' }, + }), + visualTheme({ + id: 'monochrome-dark', label: 'Monochrome Dark', monacoTheme: 'browser-cpp-monochrome-dark', + selection: { foreground: '#000000', background: '#999999', inactiveBackground: '#666666' }, + cssTokens: { crust: '#000000', mantle: '#000000', base: '#000000', surface0: '#000000', surface1: '#999999', overlay0: '#999999', text: '#ffffff', subtext0: '#999999', green: '#ffffff', red: '#999999', yellow: '#999999', blue: '#ffffff', mauve: '#ffffff', peach: '#999999' }, + terminalColors: { background: '#000000', foreground: '#ffffff', accent: '#999999', ansi: monochromeAnsi('#ffffff', '#999999') }, preview: { background: '#000000', foreground: '#ffffff', accent: '#999999' }, + }), ]); export function getVisualTheme(id) { From a2b6be3b766a02dfb22decc99338c235854d2431 Mon Sep 17 00:00:00 2001 From: Kevin Buffardi Date: Thu, 29 Oct 2026 23:27:49 -0700 Subject: [PATCH 02/13] feat: apply monochrome shell and editor palettes --- scripts/e2e-settings-visual-theme.test.mjs | 2 +- scripts/smoke-browser.mjs | 17 +++++++++++--- src/ui/editor.js | 26 ++++++++++++++++++++++ src/ui/styles.css | 14 ++++++++---- 4 files changed, 51 insertions(+), 8 deletions(-) diff --git a/scripts/e2e-settings-visual-theme.test.mjs b/scripts/e2e-settings-visual-theme.test.mjs index 4ffed73..c1de884 100644 --- a/scripts/e2e-settings-visual-theme.test.mjs +++ b/scripts/e2e-settings-visual-theme.test.mjs @@ -66,7 +66,7 @@ test('terminal viewport inherits the active visual theme background', async () = ); assert.deepEqual( [...css.matchAll(/--terminal-background:\s*(#[\da-f]{3,6});/gi)].map(([, color]) => color.toLowerCase()), - ['#11111b', '#fff', '#181818', '#fff', '#000'] + ['#11111b', '#fff', '#181818', '#fff', '#000', '#fff', '#000'] ); }); diff --git a/scripts/smoke-browser.mjs b/scripts/smoke-browser.mjs index acc63e5..1cee310 100644 --- a/scripts/smoke-browser.mjs +++ b/scripts/smoke-browser.mjs @@ -1039,7 +1039,7 @@ int main() { assert(helpOutput.includes('Run the last compiled binary'), 'Real help output is missing the run-command description'); let previousSelection = null; - for (const themeId of ['vs', 'hc-light', 'browser-cpp-dark', 'vs-dark', 'hc-black']) { + for (const themeId of ['vs', 'hc-light', 'browser-cpp-dark', 'vs-dark', 'hc-black', 'monochrome-light', 'monochrome-dark']) { await evaluate(cdp, sessionId, `document.querySelector('input[name="visual-theme"][value="${themeId}"]')?.click()`); await waitFor(async () => evaluate( cdp, @@ -1167,7 +1167,7 @@ int main() { assert(createdFileText === 'hello from fstream\n', `Expected output.txt to be created, got: ${JSON.stringify(createdFileText)}`); assert(terminalText.includes('5 stream wrote output.txt'), `Expected stream-insertion output, got: ${JSON.stringify(terminalText)}`); - for (const themeId of ['vs', 'hc-light']) { + for (const themeId of ['vs', 'hc-light', 'monochrome-light', 'monochrome-dark']) { await evaluate(cdp, sessionId, `(() => { const input = document.querySelector('input[name="visual-theme"][value="${themeId}"]'); input?.click(); @@ -1180,13 +1180,24 @@ int main() { ), `${themeId} theme selection`, 10_000); const selection = await selectTerminalText(cdp, sessionId); - const expectedSelectionBackground = themeId === 'vs' ? '#0969da' : '#0037da'; + const expectedSelectionBackground = { + vs: '#0969da', + 'hc-light': '#0037da', + 'monochrome-light': '#666666', + 'monochrome-dark': '#999999', + }[themeId]; assert(selection.theme.selectionBackground, `${themeId} terminal selection background is missing`); assert(selection.theme.selectionForeground, `${themeId} terminal selection foreground is missing`); assert(selection.theme.selectionBackground === expectedSelectionBackground, `${themeId} did not apply its configured terminal selection background`); assert(selection.theme.selectionBackground !== selection.theme.background, `${themeId} terminal selection is not distinct from its background`); + if (themeId.startsWith('monochrome-')) { + const colors = [selection.theme.background, selection.theme.foreground, + selection.theme.selectionBackground, selection.theme.selectionForeground]; + assert(colors.every((color) => /^#([\da-f]{2})\1\1$/i.test(color)), + `${themeId} terminal colors are not grayscale: ${colors.join(', ')}`); + } const currentTerminal = await evaluate( cdp, sessionId, diff --git a/src/ui/editor.js b/src/ui/editor.js index 2dfb152..d15971c 100644 --- a/src/ui/editor.js +++ b/src/ui/editor.js @@ -73,6 +73,32 @@ export function createEditor(container, { theme = 'browser-cpp-dark' } = {}) { for (const visualTheme of VISUAL_THEMES) { if (visualTheme.id === 'browser-cpp-dark') continue; + if (visualTheme.id.startsWith('monochrome-')) { + const { cssTokens: colors } = visualTheme; + const isLight = visualTheme.id === 'monochrome-light'; + monaco.editor.defineTheme(visualTheme.monacoTheme, { + base: isLight ? 'vs' : 'vs-dark', + inherit: false, + rules: [], + colors: { + 'editor.background': colors.base, + 'editor.foreground': colors.text, + 'editorLineNumber.foreground': colors.subtext0, + 'editorLineNumber.activeForeground': colors.text, + 'editor.lineHighlightBackground': colors.surface0, + 'editorCursor.foreground': colors.text, + 'editor.selectionForeground': visualTheme.selection.foreground, + 'editor.selectionBackground': visualTheme.selection.background, + 'editor.inactiveSelectionBackground': visualTheme.selection.inactiveBackground, + 'editorIndentGuide.background': colors.surface1, + 'editorWidget.background': colors.mantle, + 'editorSuggestWidget.background': colors.mantle, + 'editorSuggestWidget.border': colors.surface1, + 'minimap.background': colors.mantle, + }, + }); + continue; + } monaco.editor.defineTheme(visualTheme.monacoTheme, { base: visualTheme.id, inherit: true, diff --git a/src/ui/styles.css b/src/ui/styles.css index 7fe3546..b9334d1 100644 --- a/src/ui/styles.css +++ b/src/ui/styles.css @@ -23,6 +23,10 @@ --mauve: #cba6f7; --peach: #fab387; --terminal-background: #11111b; + --primary-hover: #b9f0b3; + --danger-wash: rgba(243, 139, 168, 0.12); + --danger-wash-hover: rgba(243, 139, 168, 0.22); + --reload-overlay: rgba(17, 17, 27, 0.6); --toolbar-h: 36px; --statusbar-h: 22px; @@ -40,6 +44,8 @@ :root[data-visual-theme="vs-dark"] { --crust:#181818; --mantle:#202020; --base:#1e1e1e; --surface0:#2d2d30; --surface1:#3e3e42; --overlay0:#858585; --text:#d4d4d4; --subtext0:#a0a0a0; --green:#89d185; --red:#f48771; --yellow:#dcdcaa; --blue:#569cd6; --mauve:#c586c0; --peach:#ce9178; --terminal-background:#181818; } :root[data-visual-theme="hc-light"] { --crust:#fff; --mantle:#fff; --base:#fff; --surface0:#e5e5e5; --surface1:#6f6f6f; --overlay0:#333; --text:#000; --subtext0:#333; --green:#064; --red:#a80000; --yellow:#704d00; --blue:#0037da; --mauve:#5b00a5; --peach:#8a3100; --terminal-background:#fff; } :root[data-visual-theme="hc-black"] { --crust:#000; --mantle:#000; --base:#000; --surface0:#1f1f1f; --surface1:#fff; --overlay0:#d0d0d0; --text:#fff; --subtext0:#fff; --green:#0f0; --red:#ff6b6b; --yellow:#ff0; --blue:#3daee9; --mauve:#ff8cff; --peach:#ffb86c; --terminal-background:#000; } +:root[data-visual-theme="monochrome-light"] { --crust:#fff; --mantle:#fff; --base:#fff; --surface0:#fff; --surface1:#666; --overlay0:#666; --text:#000; --subtext0:#666; --green:#000; --red:#666; --yellow:#666; --blue:#000; --mauve:#000; --peach:#666; --terminal-background:#fff; --primary-hover:#000; --danger-wash:rgba(102,102,102,.12); --danger-wash-hover:rgba(102,102,102,.22); --reload-overlay:rgba(102,102,102,.6); } +:root[data-visual-theme="monochrome-dark"] { --crust:#000; --mantle:#000; --base:#000; --surface0:#000; --surface1:#999; --overlay0:#999; --text:#fff; --subtext0:#999; --green:#fff; --red:#999; --yellow:#999; --blue:#fff; --mauve:#fff; --peach:#999; --terminal-background:#000; --primary-hover:#fff; --danger-wash:rgba(153,153,153,.12); --danger-wash-hover:rgba(153,153,153,.22); --reload-overlay:rgba(153,153,153,.6); } body { height: 100vh; @@ -141,7 +147,7 @@ button:disabled:active { } .btn-primary:hover { - background: #b9f0b3; + background: var(--primary-hover); border-color: transparent; } @@ -620,7 +626,7 @@ select:focus { padding: 1px 6px; border: 1px solid var(--red); border-radius: 3px; - background: rgba(243, 139, 168, 0.12); + background: var(--danger-wash); color: var(--red); font-size: 11px; font-weight: 700; @@ -637,7 +643,7 @@ select:focus { } .terminal-stop-btn:hover:not(:disabled) { - background: rgba(243, 139, 168, 0.22); + background: var(--danger-wash-hover); border-color: var(--red); color: var(--text); } @@ -742,7 +748,7 @@ body.is-resizing-horizontal * { display: flex; align-items: center; justify-content: center; - background: rgba(17, 17, 27, 0.6); + background: var(--reload-overlay); } .session-reload-dialog { From d789e49c10afde07ff49b7752147d3ad827c54b9 Mon Sep 17 00:00:00 2001 From: Kevin Buffardi Date: Thu, 29 Oct 2026 23:33:25 -0700 Subject: [PATCH 03/13] fix: keep monochrome editor decorations neutral --- scripts/smoke-browser.mjs | 11 +++++++++++ src/ui/editor.js | 20 ++++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/scripts/smoke-browser.mjs b/scripts/smoke-browser.mjs index 1cee310..a23a039 100644 --- a/scripts/smoke-browser.mjs +++ b/scripts/smoke-browser.mjs @@ -1051,6 +1051,17 @@ int main() { `${themeId} computed Monaco selection styles`, 5_000); assert(monacoColors?.background && monacoColors.foreground && monacoColors.inactiveBackground, `${themeId} Monaco selection styles were not computed`); + if (themeId.startsWith('monochrome-')) { + const editorTextColors = await evaluate(cdp, sessionId, `(() => { + const spans = [...document.querySelectorAll('.monaco-editor .view-lines .view-line span')]; + return spans.map((element) => getComputedStyle(element).color).filter(Boolean); + })()`); + assert(editorTextColors.length > 0, `${themeId} rendered no Monaco text spans`); + assert(editorTextColors.every((color) => { + const channels = color.match(/[\\d.]+/g)?.map(Number); + return channels?.length >= 3 && channels[0] === channels[1] && channels[1] === channels[2]; + }), `${themeId} Monaco text contains chromatic syntax or bracket colors`); + } if (previousSelection) { const retained = await evaluate(cdp, sessionId, `document.getElementById('terminal-container')?.__browserCppReadTerminalSelection?.() || null`); diff --git a/src/ui/editor.js b/src/ui/editor.js index d15971c..eee011c 100644 --- a/src/ui/editor.js +++ b/src/ui/editor.js @@ -32,6 +32,10 @@ self.MonacoEnvironment = { // ── Internal state ──────────────────────────────────────────────────────────── let _editor = null; +function isMonochromeTheme(theme) { + return theme === 'browser-cpp-monochrome-light' || theme === 'browser-cpp-monochrome-dark'; +} + // ── Public API ──────────────────────────────────────────────────────────────── /** @@ -95,6 +99,13 @@ export function createEditor(container, { theme = 'browser-cpp-dark' } = {}) { 'editorSuggestWidget.background': colors.mantle, 'editorSuggestWidget.border': colors.surface1, 'minimap.background': colors.mantle, + 'editorBracketHighlight.foreground1': colors.text, + 'editorBracketHighlight.foreground2': colors.text, + 'editorBracketHighlight.foreground3': colors.text, + 'editorBracketHighlight.foreground4': colors.text, + 'editorBracketHighlight.foreground5': colors.text, + 'editorBracketHighlight.foreground6': colors.text, + 'editorBracketHighlight.unexpectedBracket.foreground': colors.text, }, }); continue; @@ -133,6 +144,11 @@ export function createEditor(container, { theme = 'browser-cpp-dark' } = {}) { automaticLayout: true, // resize with container }); + _editor.updateOptions({ + bracketPairColorization: { enabled: !isMonochromeTheme(theme) }, + guides: { bracketPairs: !isMonochromeTheme(theme), indentation: true }, + }); + if (new URLSearchParams(globalThis.location?.search || '').has('smoke-test')) { container.__browserCppPrepareSelectionColorRead = () => { return true; @@ -166,6 +182,10 @@ export function createEditor(container, { theme = 'browser-cpp-dark' } = {}) { export function setTheme(theme) { monaco.editor.setTheme(theme); + _editor?.updateOptions({ + bracketPairColorization: { enabled: !isMonochromeTheme(theme) }, + guides: { bracketPairs: !isMonochromeTheme(theme), indentation: true }, + }); } /** Get the full text of the current document. */ From 38ae9f718a6aedae5a5d291275a2d40bb2421c34 Mon Sep 17 00:00:00 2001 From: Kevin Buffardi Date: Thu, 29 Oct 2026 23:39:16 -0700 Subject: [PATCH 04/13] feat: separate monochrome panels and button outlines --- scripts/e2e-settings-visual-theme.test.mjs | 16 +++++++- scripts/smoke-browser.mjs | 31 ++++++++++++++++ src/ui/styles.css | 43 ++++++++++++++-------- src/ui/themes.mjs | 4 +- 4 files changed, 76 insertions(+), 18 deletions(-) diff --git a/scripts/e2e-settings-visual-theme.test.mjs b/scripts/e2e-settings-visual-theme.test.mjs index c1de884..9bb3ecb 100644 --- a/scripts/e2e-settings-visual-theme.test.mjs +++ b/scripts/e2e-settings-visual-theme.test.mjs @@ -66,10 +66,24 @@ test('terminal viewport inherits the active visual theme background', async () = ); assert.deepEqual( [...css.matchAll(/--terminal-background:\s*(#[\da-f]{3,6});/gi)].map(([, color]) => color.toLowerCase()), - ['#11111b', '#fff', '#181818', '#fff', '#000', '#fff', '#000'] + ['#11111b', '#fff', '#181818', '#fff', '#000', '#e6e6e6', '#262626'] ); }); +test('monochrome themes separate panels and outline buttons', async () => { + const css = await readFile(new URL('../src/ui/styles.css', import.meta.url), 'utf8'); + assert.match(css, /--panel-explorer:#f2f2f2/); + assert.match(css, /--panel-editor:#fff/); + assert.match(css, /--panel-terminal:#e6e6e6/); + assert.match(css, /--panel-explorer:#1a1a1a/); + assert.match(css, /--panel-editor:#000/); + assert.match(css, /--panel-terminal:#262626/); + assert.match(css, /--button-border:#666/); + assert.match(css, /--button-border:#999/); + assert.match(css, /:root\[data-visual-theme="monochrome-light"\][\s\S]*button[\s\S]*border-color: var\(--button-border\);/); + assert.match(css, /:root\[data-visual-theme="monochrome-dark"\][\s\S]*button[\s\S]*border-color: var\(--button-border\);/); +}); + function contrastRatio(foreground, background) { const luminance = (color) => { const channels = color.match(/[0-9a-f]{2}/gi).map((channel) => parseInt(channel, 16) / 255); diff --git a/scripts/smoke-browser.mjs b/scripts/smoke-browser.mjs index a23a039..f82c6ce 100644 --- a/scripts/smoke-browser.mjs +++ b/scripts/smoke-browser.mjs @@ -1190,6 +1190,37 @@ int main() { `document.documentElement.dataset.visualTheme === '${themeId}'` ), `${themeId} theme selection`, 10_000); + if (themeId.startsWith('monochrome-')) { + const panelStyles = await evaluate(cdp, sessionId, `(() => { + const style = (selector) => { + const element = document.querySelector(selector); + const computed = element && getComputedStyle(element); + return computed && { + background: computed.backgroundColor, + border: computed.borderRightColor || computed.borderTopColor, + }; + }; + const button = document.querySelector('#toolbar button'); + return { + explorer: style('#sidebar'), + editor: style('#editor-container'), + terminal: style('#terminal-panel'), + buttonBorder: button ? getComputedStyle(button).borderTopColor : null, + }; + })()`); + const expectedPanels = themeId === 'monochrome-light' + ? { explorer: '#f2f2f2', editor: '#ffffff', terminal: '#e6e6e6', border: '#666666' } + : { explorer: '#1a1a1a', editor: '#000000', terminal: '#262626', border: '#999999' }; + assert(normalizeCssColor(panelStyles.explorer.background) === expectedPanels.explorer, + `${themeId} explorer panel has the wrong background`); + assert(normalizeCssColor(panelStyles.editor.background) === expectedPanels.editor, + `${themeId} editor panel has the wrong background`); + assert(normalizeCssColor(panelStyles.terminal.background) === expectedPanels.terminal, + `${themeId} terminal panel has the wrong background`); + assert(normalizeCssColor(panelStyles.buttonBorder) === expectedPanels.border, + `${themeId} buttons do not have a contrasting outline`); + } + const selection = await selectTerminalText(cdp, sessionId); const expectedSelectionBackground = { vs: '#0969da', diff --git a/src/ui/styles.css b/src/ui/styles.css index b9334d1..3a6bd01 100644 --- a/src/ui/styles.css +++ b/src/ui/styles.css @@ -27,6 +27,11 @@ --danger-wash: rgba(243, 139, 168, 0.12); --danger-wash-hover: rgba(243, 139, 168, 0.22); --reload-overlay: rgba(17, 17, 27, 0.6); + --panel-explorer: var(--mantle); + --panel-editor: var(--base); + --panel-terminal: var(--crust); + --panel-border: var(--surface0); + --button-border: transparent; --toolbar-h: 36px; --statusbar-h: 22px; @@ -44,8 +49,8 @@ :root[data-visual-theme="vs-dark"] { --crust:#181818; --mantle:#202020; --base:#1e1e1e; --surface0:#2d2d30; --surface1:#3e3e42; --overlay0:#858585; --text:#d4d4d4; --subtext0:#a0a0a0; --green:#89d185; --red:#f48771; --yellow:#dcdcaa; --blue:#569cd6; --mauve:#c586c0; --peach:#ce9178; --terminal-background:#181818; } :root[data-visual-theme="hc-light"] { --crust:#fff; --mantle:#fff; --base:#fff; --surface0:#e5e5e5; --surface1:#6f6f6f; --overlay0:#333; --text:#000; --subtext0:#333; --green:#064; --red:#a80000; --yellow:#704d00; --blue:#0037da; --mauve:#5b00a5; --peach:#8a3100; --terminal-background:#fff; } :root[data-visual-theme="hc-black"] { --crust:#000; --mantle:#000; --base:#000; --surface0:#1f1f1f; --surface1:#fff; --overlay0:#d0d0d0; --text:#fff; --subtext0:#fff; --green:#0f0; --red:#ff6b6b; --yellow:#ff0; --blue:#3daee9; --mauve:#ff8cff; --peach:#ffb86c; --terminal-background:#000; } -:root[data-visual-theme="monochrome-light"] { --crust:#fff; --mantle:#fff; --base:#fff; --surface0:#fff; --surface1:#666; --overlay0:#666; --text:#000; --subtext0:#666; --green:#000; --red:#666; --yellow:#666; --blue:#000; --mauve:#000; --peach:#666; --terminal-background:#fff; --primary-hover:#000; --danger-wash:rgba(102,102,102,.12); --danger-wash-hover:rgba(102,102,102,.22); --reload-overlay:rgba(102,102,102,.6); } -:root[data-visual-theme="monochrome-dark"] { --crust:#000; --mantle:#000; --base:#000; --surface0:#000; --surface1:#999; --overlay0:#999; --text:#fff; --subtext0:#999; --green:#fff; --red:#999; --yellow:#999; --blue:#fff; --mauve:#fff; --peach:#999; --terminal-background:#000; --primary-hover:#fff; --danger-wash:rgba(153,153,153,.12); --danger-wash-hover:rgba(153,153,153,.22); --reload-overlay:rgba(153,153,153,.6); } +:root[data-visual-theme="monochrome-light"] { --crust:#fff; --mantle:#fff; --base:#fff; --surface0:#fff; --surface1:#666; --overlay0:#666; --text:#000; --subtext0:#666; --green:#000; --red:#666; --yellow:#666; --blue:#000; --mauve:#000; --peach:#666; --terminal-background:#e6e6e6; --primary-hover:#000; --danger-wash:rgba(102,102,102,.12); --danger-wash-hover:rgba(102,102,102,.22); --reload-overlay:rgba(102,102,102,.6); --panel-explorer:#f2f2f2; --panel-editor:#fff; --panel-terminal:#e6e6e6; --panel-border:#666; --button-border:#666; } +:root[data-visual-theme="monochrome-dark"] { --crust:#000; --mantle:#000; --base:#000; --surface0:#000; --surface1:#999; --overlay0:#999; --text:#fff; --subtext0:#999; --green:#fff; --red:#999; --yellow:#999; --blue:#fff; --mauve:#fff; --peach:#999; --terminal-background:#262626; --primary-hover:#fff; --danger-wash:rgba(153,153,153,.12); --danger-wash-hover:rgba(153,153,153,.22); --reload-overlay:rgba(153,153,153,.6); --panel-explorer:#1a1a1a; --panel-editor:#000; --panel-terminal:#262626; --panel-border:#999; --button-border:#999; } body { height: 100vh; @@ -113,6 +118,11 @@ button { transition: background 0.1s; } +:root[data-visual-theme="monochrome-light"] button, +:root[data-visual-theme="monochrome-dark"] button { + border-color: var(--button-border); +} + button:hover { background: var(--surface1); border-color: var(--overlay0); @@ -129,7 +139,7 @@ button:disabled:active { cursor: not-allowed; opacity: 0.5; background: var(--surface0); - border-color: transparent; + border-color: var(--button-border); filter: none; } @@ -148,7 +158,7 @@ button:disabled:active { .btn-primary:hover { background: var(--primary-hover); - border-color: transparent; + border-color: var(--button-border); } select { @@ -342,8 +352,8 @@ select:focus { #sidebar { width: var(--sidebar-w); min-width: 120px; - background: var(--mantle); - border-right: 1px solid var(--surface0); + background: var(--panel-explorer); + border-right: 1px solid var(--panel-border); display: flex; flex-direction: column; overflow: hidden; @@ -365,7 +375,7 @@ select:focus { .panel-header-btn { background: transparent; - border: none; + border: 1px solid var(--button-border); padding: 2px 4px; color: var(--subtext0); font-size: 14px; @@ -496,6 +506,7 @@ select:focus { display: flex; flex-direction: column; overflow: hidden; + background: var(--panel-editor); } /* ── Tab bar ──────────────────────────────────────────────────────────────── */ @@ -503,8 +514,8 @@ select:focus { #tab-bar { display: flex; height: var(--tab-h); - background: var(--mantle); - border-bottom: 1px solid var(--surface0); + background: var(--panel-editor); + border-bottom: 1px solid var(--panel-border); overflow-x: auto; overflow-y: hidden; flex-shrink: 0; @@ -556,7 +567,7 @@ select:focus { .tab-close { background: none; - border: none; + border: 1px solid var(--button-border); color: var(--subtext0); padding: 0 2px; font-size: 14px; @@ -575,6 +586,7 @@ select:focus { #editor-container { flex: 1; overflow: hidden; + background: var(--panel-editor); } /* ── Terminal panel ───────────────────────────────────────────────────────── */ @@ -582,8 +594,8 @@ select:focus { #terminal-panel { height: var(--terminal-h); min-height: 80px; - background: var(--crust); - border-top: 2px solid var(--surface0); + background: var(--panel-terminal); + border-top: 2px solid var(--panel-border); display: flex; flex-direction: column; flex-shrink: 0; @@ -600,8 +612,8 @@ select:focus { align-items: center; justify-content: space-between; padding: 4px 10px; - background: var(--mantle); - border-bottom: 1px solid var(--surface0); + background: var(--panel-terminal); + border-bottom: 1px solid var(--panel-border); font-size: 11px; font-weight: 700; letter-spacing: 0.08em; @@ -661,7 +673,7 @@ select:focus { .panel-controls button { background: none; - border: none; + border: 1px solid var(--button-border); color: var(--subtext0); padding: 1px 4px; font-size: 13px; @@ -677,6 +689,7 @@ select:focus { flex: 1; overflow: hidden; padding: 4px 6px; + background: var(--panel-terminal); } /* xterm overrides */ diff --git a/src/ui/themes.mjs b/src/ui/themes.mjs index 2a2fa17..173a9bb 100644 --- a/src/ui/themes.mjs +++ b/src/ui/themes.mjs @@ -79,13 +79,13 @@ export const VISUAL_THEMES = Object.freeze([ id: 'monochrome-light', label: 'Monochrome Light', monacoTheme: 'browser-cpp-monochrome-light', selection: { foreground: '#ffffff', background: '#666666', inactiveBackground: '#999999' }, cssTokens: { crust: '#ffffff', mantle: '#ffffff', base: '#ffffff', surface0: '#ffffff', surface1: '#666666', overlay0: '#666666', text: '#000000', subtext0: '#666666', green: '#000000', red: '#666666', yellow: '#666666', blue: '#000000', mauve: '#000000', peach: '#666666' }, - terminalColors: { background: '#ffffff', foreground: '#000000', accent: '#666666', ansi: monochromeAnsi('#000000', '#666666') }, preview: { background: '#ffffff', foreground: '#000000', accent: '#666666' }, + terminalColors: { background: '#e6e6e6', foreground: '#000000', accent: '#666666', ansi: monochromeAnsi('#000000', '#666666') }, preview: { background: '#ffffff', foreground: '#000000', accent: '#666666' }, }), visualTheme({ id: 'monochrome-dark', label: 'Monochrome Dark', monacoTheme: 'browser-cpp-monochrome-dark', selection: { foreground: '#000000', background: '#999999', inactiveBackground: '#666666' }, cssTokens: { crust: '#000000', mantle: '#000000', base: '#000000', surface0: '#000000', surface1: '#999999', overlay0: '#999999', text: '#ffffff', subtext0: '#999999', green: '#ffffff', red: '#999999', yellow: '#999999', blue: '#ffffff', mauve: '#ffffff', peach: '#999999' }, - terminalColors: { background: '#000000', foreground: '#ffffff', accent: '#999999', ansi: monochromeAnsi('#ffffff', '#999999') }, preview: { background: '#000000', foreground: '#ffffff', accent: '#999999' }, + terminalColors: { background: '#262626', foreground: '#ffffff', accent: '#999999', ansi: monochromeAnsi('#ffffff', '#999999') }, preview: { background: '#000000', foreground: '#ffffff', accent: '#999999' }, }), ]); From 8e67d95e09dc74896a11afe89593d6ab780c4e9a Mon Sep 17 00:00:00 2001 From: Kevin Buffardi Date: Thu, 29 Oct 2026 23:42:06 -0700 Subject: [PATCH 05/13] fix: preserve existing control dimensions --- scripts/e2e-settings-visual-theme.test.mjs | 3 +++ src/ui/styles.css | 6 +++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/scripts/e2e-settings-visual-theme.test.mjs b/scripts/e2e-settings-visual-theme.test.mjs index 9bb3ecb..4a03eea 100644 --- a/scripts/e2e-settings-visual-theme.test.mjs +++ b/scripts/e2e-settings-visual-theme.test.mjs @@ -82,6 +82,9 @@ test('monochrome themes separate panels and outline buttons', async () => { assert.match(css, /--button-border:#999/); assert.match(css, /:root\[data-visual-theme="monochrome-light"\][\s\S]*button[\s\S]*border-color: var\(--button-border\);/); assert.match(css, /:root\[data-visual-theme="monochrome-dark"\][\s\S]*button[\s\S]*border-color: var\(--button-border\);/); + assert.match(css, /\.panel-header-btn[\s\S]*border: none;/); + assert.match(css, /\.tab-close[\s\S]*border: none;/); + assert.match(css, /\.panel-controls button[\s\S]*border: none;/); }); function contrastRatio(foreground, background) { diff --git a/src/ui/styles.css b/src/ui/styles.css index 3a6bd01..ef973d9 100644 --- a/src/ui/styles.css +++ b/src/ui/styles.css @@ -375,7 +375,7 @@ select:focus { .panel-header-btn { background: transparent; - border: 1px solid var(--button-border); + border: none; padding: 2px 4px; color: var(--subtext0); font-size: 14px; @@ -567,7 +567,7 @@ select:focus { .tab-close { background: none; - border: 1px solid var(--button-border); + border: none; color: var(--subtext0); padding: 0 2px; font-size: 14px; @@ -673,7 +673,7 @@ select:focus { .panel-controls button { background: none; - border: 1px solid var(--button-border); + border: none; color: var(--subtext0); padding: 1px 4px; font-size: 13px; From 969eb970d2ecb2863824471960025454661498bb Mon Sep 17 00:00:00 2001 From: Kevin Buffardi Date: Thu, 29 Oct 2026 23:43:17 -0700 Subject: [PATCH 06/13] fix: restore monochrome outlines on compact controls --- scripts/e2e-settings-visual-theme.test.mjs | 1 + src/ui/styles.css | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/scripts/e2e-settings-visual-theme.test.mjs b/scripts/e2e-settings-visual-theme.test.mjs index 4a03eea..99fd70a 100644 --- a/scripts/e2e-settings-visual-theme.test.mjs +++ b/scripts/e2e-settings-visual-theme.test.mjs @@ -85,6 +85,7 @@ test('monochrome themes separate panels and outline buttons', async () => { assert.match(css, /\.panel-header-btn[\s\S]*border: none;/); assert.match(css, /\.tab-close[\s\S]*border: none;/); assert.match(css, /\.panel-controls button[\s\S]*border: none;/); + assert.match(css, /monochrome-light[^}]*\.panel-header-btn[\s\S]*border: 1px solid var\(--button-border\);/); }); function contrastRatio(foreground, background) { diff --git a/src/ui/styles.css b/src/ui/styles.css index ef973d9..697349e 100644 --- a/src/ui/styles.css +++ b/src/ui/styles.css @@ -685,6 +685,15 @@ select:focus { background: var(--surface0); } +:root[data-visual-theme="monochrome-light"] .panel-header-btn, +:root[data-visual-theme="monochrome-dark"] .panel-header-btn, +:root[data-visual-theme="monochrome-light"] .tab-close, +:root[data-visual-theme="monochrome-dark"] .tab-close, +:root[data-visual-theme="monochrome-light"] .panel-controls button, +:root[data-visual-theme="monochrome-dark"] .panel-controls button { + border: 1px solid var(--button-border); +} + #terminal-container { flex: 1; overflow: hidden; From a6f83bb1c2a17bff78d0ce06af883bb46948ae72 Mon Sep 17 00:00:00 2001 From: Kevin Buffardi Date: Thu, 29 Oct 2026 23:47:43 -0700 Subject: [PATCH 07/13] fix: unify monochrome secondary panel backgrounds --- scripts/e2e-settings-visual-theme.test.mjs | 10 +++++----- scripts/smoke-browser.mjs | 4 ++-- src/ui/styles.css | 4 ++-- src/ui/themes.mjs | 4 ++-- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/scripts/e2e-settings-visual-theme.test.mjs b/scripts/e2e-settings-visual-theme.test.mjs index 99fd70a..4e8c387 100644 --- a/scripts/e2e-settings-visual-theme.test.mjs +++ b/scripts/e2e-settings-visual-theme.test.mjs @@ -66,18 +66,18 @@ test('terminal viewport inherits the active visual theme background', async () = ); assert.deepEqual( [...css.matchAll(/--terminal-background:\s*(#[\da-f]{3,6});/gi)].map(([, color]) => color.toLowerCase()), - ['#11111b', '#fff', '#181818', '#fff', '#000', '#e6e6e6', '#262626'] + ['#11111b', '#fff', '#181818', '#fff', '#000', '#d9d9d9', '#333'] ); }); test('monochrome themes separate panels and outline buttons', async () => { const css = await readFile(new URL('../src/ui/styles.css', import.meta.url), 'utf8'); - assert.match(css, /--panel-explorer:#f2f2f2/); + assert.match(css, /--panel-explorer:#d9d9d9/); assert.match(css, /--panel-editor:#fff/); - assert.match(css, /--panel-terminal:#e6e6e6/); - assert.match(css, /--panel-explorer:#1a1a1a/); + assert.match(css, /--panel-terminal:#d9d9d9/); + assert.match(css, /--panel-explorer:#333/); assert.match(css, /--panel-editor:#000/); - assert.match(css, /--panel-terminal:#262626/); + assert.match(css, /--panel-terminal:#333/); assert.match(css, /--button-border:#666/); assert.match(css, /--button-border:#999/); assert.match(css, /:root\[data-visual-theme="monochrome-light"\][\s\S]*button[\s\S]*border-color: var\(--button-border\);/); diff --git a/scripts/smoke-browser.mjs b/scripts/smoke-browser.mjs index f82c6ce..71c5ae3 100644 --- a/scripts/smoke-browser.mjs +++ b/scripts/smoke-browser.mjs @@ -1209,8 +1209,8 @@ int main() { }; })()`); const expectedPanels = themeId === 'monochrome-light' - ? { explorer: '#f2f2f2', editor: '#ffffff', terminal: '#e6e6e6', border: '#666666' } - : { explorer: '#1a1a1a', editor: '#000000', terminal: '#262626', border: '#999999' }; + ? { explorer: '#d9d9d9', editor: '#ffffff', terminal: '#d9d9d9', border: '#666666' } + : { explorer: '#333333', editor: '#000000', terminal: '#333333', border: '#999999' }; assert(normalizeCssColor(panelStyles.explorer.background) === expectedPanels.explorer, `${themeId} explorer panel has the wrong background`); assert(normalizeCssColor(panelStyles.editor.background) === expectedPanels.editor, diff --git a/src/ui/styles.css b/src/ui/styles.css index 697349e..7941a67 100644 --- a/src/ui/styles.css +++ b/src/ui/styles.css @@ -49,8 +49,8 @@ :root[data-visual-theme="vs-dark"] { --crust:#181818; --mantle:#202020; --base:#1e1e1e; --surface0:#2d2d30; --surface1:#3e3e42; --overlay0:#858585; --text:#d4d4d4; --subtext0:#a0a0a0; --green:#89d185; --red:#f48771; --yellow:#dcdcaa; --blue:#569cd6; --mauve:#c586c0; --peach:#ce9178; --terminal-background:#181818; } :root[data-visual-theme="hc-light"] { --crust:#fff; --mantle:#fff; --base:#fff; --surface0:#e5e5e5; --surface1:#6f6f6f; --overlay0:#333; --text:#000; --subtext0:#333; --green:#064; --red:#a80000; --yellow:#704d00; --blue:#0037da; --mauve:#5b00a5; --peach:#8a3100; --terminal-background:#fff; } :root[data-visual-theme="hc-black"] { --crust:#000; --mantle:#000; --base:#000; --surface0:#1f1f1f; --surface1:#fff; --overlay0:#d0d0d0; --text:#fff; --subtext0:#fff; --green:#0f0; --red:#ff6b6b; --yellow:#ff0; --blue:#3daee9; --mauve:#ff8cff; --peach:#ffb86c; --terminal-background:#000; } -:root[data-visual-theme="monochrome-light"] { --crust:#fff; --mantle:#fff; --base:#fff; --surface0:#fff; --surface1:#666; --overlay0:#666; --text:#000; --subtext0:#666; --green:#000; --red:#666; --yellow:#666; --blue:#000; --mauve:#000; --peach:#666; --terminal-background:#e6e6e6; --primary-hover:#000; --danger-wash:rgba(102,102,102,.12); --danger-wash-hover:rgba(102,102,102,.22); --reload-overlay:rgba(102,102,102,.6); --panel-explorer:#f2f2f2; --panel-editor:#fff; --panel-terminal:#e6e6e6; --panel-border:#666; --button-border:#666; } -:root[data-visual-theme="monochrome-dark"] { --crust:#000; --mantle:#000; --base:#000; --surface0:#000; --surface1:#999; --overlay0:#999; --text:#fff; --subtext0:#999; --green:#fff; --red:#999; --yellow:#999; --blue:#fff; --mauve:#fff; --peach:#999; --terminal-background:#262626; --primary-hover:#fff; --danger-wash:rgba(153,153,153,.12); --danger-wash-hover:rgba(153,153,153,.22); --reload-overlay:rgba(153,153,153,.6); --panel-explorer:#1a1a1a; --panel-editor:#000; --panel-terminal:#262626; --panel-border:#999; --button-border:#999; } +:root[data-visual-theme="monochrome-light"] { --crust:#fff; --mantle:#fff; --base:#fff; --surface0:#fff; --surface1:#666; --overlay0:#666; --text:#000; --subtext0:#666; --green:#000; --red:#666; --yellow:#666; --blue:#000; --mauve:#000; --peach:#666; --terminal-background:#d9d9d9; --primary-hover:#000; --danger-wash:rgba(102,102,102,.12); --danger-wash-hover:rgba(102,102,102,.22); --reload-overlay:rgba(102,102,102,.6); --panel-explorer:#d9d9d9; --panel-editor:#fff; --panel-terminal:#d9d9d9; --panel-border:#666; --button-border:#666; } +:root[data-visual-theme="monochrome-dark"] { --crust:#000; --mantle:#000; --base:#000; --surface0:#000; --surface1:#999; --overlay0:#999; --text:#fff; --subtext0:#999; --green:#fff; --red:#999; --yellow:#999; --blue:#fff; --mauve:#fff; --peach:#999; --terminal-background:#333; --primary-hover:#fff; --danger-wash:rgba(153,153,153,.12); --danger-wash-hover:rgba(153,153,153,.22); --reload-overlay:rgba(153,153,153,.6); --panel-explorer:#333; --panel-editor:#000; --panel-terminal:#333; --panel-border:#999; --button-border:#999; } body { height: 100vh; diff --git a/src/ui/themes.mjs b/src/ui/themes.mjs index 173a9bb..d4f71d1 100644 --- a/src/ui/themes.mjs +++ b/src/ui/themes.mjs @@ -79,13 +79,13 @@ export const VISUAL_THEMES = Object.freeze([ id: 'monochrome-light', label: 'Monochrome Light', monacoTheme: 'browser-cpp-monochrome-light', selection: { foreground: '#ffffff', background: '#666666', inactiveBackground: '#999999' }, cssTokens: { crust: '#ffffff', mantle: '#ffffff', base: '#ffffff', surface0: '#ffffff', surface1: '#666666', overlay0: '#666666', text: '#000000', subtext0: '#666666', green: '#000000', red: '#666666', yellow: '#666666', blue: '#000000', mauve: '#000000', peach: '#666666' }, - terminalColors: { background: '#e6e6e6', foreground: '#000000', accent: '#666666', ansi: monochromeAnsi('#000000', '#666666') }, preview: { background: '#ffffff', foreground: '#000000', accent: '#666666' }, + terminalColors: { background: '#d9d9d9', foreground: '#000000', accent: '#666666', ansi: monochromeAnsi('#000000', '#666666') }, preview: { background: '#ffffff', foreground: '#000000', accent: '#666666' }, }), visualTheme({ id: 'monochrome-dark', label: 'Monochrome Dark', monacoTheme: 'browser-cpp-monochrome-dark', selection: { foreground: '#000000', background: '#999999', inactiveBackground: '#666666' }, cssTokens: { crust: '#000000', mantle: '#000000', base: '#000000', surface0: '#000000', surface1: '#999999', overlay0: '#999999', text: '#ffffff', subtext0: '#999999', green: '#ffffff', red: '#999999', yellow: '#999999', blue: '#ffffff', mauve: '#ffffff', peach: '#999999' }, - terminalColors: { background: '#262626', foreground: '#ffffff', accent: '#999999', ansi: monochromeAnsi('#ffffff', '#999999') }, preview: { background: '#000000', foreground: '#ffffff', accent: '#999999' }, + terminalColors: { background: '#333333', foreground: '#ffffff', accent: '#999999', ansi: monochromeAnsi('#ffffff', '#999999') }, preview: { background: '#000000', foreground: '#ffffff', accent: '#999999' }, }), ]); From 976059906ea2375cbea764bc00823f4e0d0bc89c Mon Sep 17 00:00:00 2001 From: Kevin Buffardi Date: Thu, 29 Oct 2026 23:53:43 -0700 Subject: [PATCH 08/13] fix: increase monochrome panel contrast --- scripts/e2e-settings-visual-theme.test.mjs | 10 +++++----- scripts/smoke-browser.mjs | 4 ++-- src/ui/styles.css | 4 ++-- src/ui/themes.mjs | 4 ++-- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/scripts/e2e-settings-visual-theme.test.mjs b/scripts/e2e-settings-visual-theme.test.mjs index 4e8c387..312b6a1 100644 --- a/scripts/e2e-settings-visual-theme.test.mjs +++ b/scripts/e2e-settings-visual-theme.test.mjs @@ -66,18 +66,18 @@ test('terminal viewport inherits the active visual theme background', async () = ); assert.deepEqual( [...css.matchAll(/--terminal-background:\s*(#[\da-f]{3,6});/gi)].map(([, color]) => color.toLowerCase()), - ['#11111b', '#fff', '#181818', '#fff', '#000', '#d9d9d9', '#333'] + ['#11111b', '#fff', '#181818', '#fff', '#000', '#ddd', '#222'] ); }); test('monochrome themes separate panels and outline buttons', async () => { const css = await readFile(new URL('../src/ui/styles.css', import.meta.url), 'utf8'); - assert.match(css, /--panel-explorer:#d9d9d9/); + assert.match(css, /--panel-explorer:#ddd/); assert.match(css, /--panel-editor:#fff/); - assert.match(css, /--panel-terminal:#d9d9d9/); - assert.match(css, /--panel-explorer:#333/); + assert.match(css, /--panel-terminal:#ddd/); + assert.match(css, /--panel-explorer:#222/); assert.match(css, /--panel-editor:#000/); - assert.match(css, /--panel-terminal:#333/); + assert.match(css, /--panel-terminal:#222/); assert.match(css, /--button-border:#666/); assert.match(css, /--button-border:#999/); assert.match(css, /:root\[data-visual-theme="monochrome-light"\][\s\S]*button[\s\S]*border-color: var\(--button-border\);/); diff --git a/scripts/smoke-browser.mjs b/scripts/smoke-browser.mjs index 71c5ae3..3d3c547 100644 --- a/scripts/smoke-browser.mjs +++ b/scripts/smoke-browser.mjs @@ -1209,8 +1209,8 @@ int main() { }; })()`); const expectedPanels = themeId === 'monochrome-light' - ? { explorer: '#d9d9d9', editor: '#ffffff', terminal: '#d9d9d9', border: '#666666' } - : { explorer: '#333333', editor: '#000000', terminal: '#333333', border: '#999999' }; + ? { explorer: '#dddddd', editor: '#ffffff', terminal: '#dddddd', border: '#666666' } + : { explorer: '#222222', editor: '#000000', terminal: '#222222', border: '#999999' }; assert(normalizeCssColor(panelStyles.explorer.background) === expectedPanels.explorer, `${themeId} explorer panel has the wrong background`); assert(normalizeCssColor(panelStyles.editor.background) === expectedPanels.editor, diff --git a/src/ui/styles.css b/src/ui/styles.css index 7941a67..b835a6f 100644 --- a/src/ui/styles.css +++ b/src/ui/styles.css @@ -49,8 +49,8 @@ :root[data-visual-theme="vs-dark"] { --crust:#181818; --mantle:#202020; --base:#1e1e1e; --surface0:#2d2d30; --surface1:#3e3e42; --overlay0:#858585; --text:#d4d4d4; --subtext0:#a0a0a0; --green:#89d185; --red:#f48771; --yellow:#dcdcaa; --blue:#569cd6; --mauve:#c586c0; --peach:#ce9178; --terminal-background:#181818; } :root[data-visual-theme="hc-light"] { --crust:#fff; --mantle:#fff; --base:#fff; --surface0:#e5e5e5; --surface1:#6f6f6f; --overlay0:#333; --text:#000; --subtext0:#333; --green:#064; --red:#a80000; --yellow:#704d00; --blue:#0037da; --mauve:#5b00a5; --peach:#8a3100; --terminal-background:#fff; } :root[data-visual-theme="hc-black"] { --crust:#000; --mantle:#000; --base:#000; --surface0:#1f1f1f; --surface1:#fff; --overlay0:#d0d0d0; --text:#fff; --subtext0:#fff; --green:#0f0; --red:#ff6b6b; --yellow:#ff0; --blue:#3daee9; --mauve:#ff8cff; --peach:#ffb86c; --terminal-background:#000; } -:root[data-visual-theme="monochrome-light"] { --crust:#fff; --mantle:#fff; --base:#fff; --surface0:#fff; --surface1:#666; --overlay0:#666; --text:#000; --subtext0:#666; --green:#000; --red:#666; --yellow:#666; --blue:#000; --mauve:#000; --peach:#666; --terminal-background:#d9d9d9; --primary-hover:#000; --danger-wash:rgba(102,102,102,.12); --danger-wash-hover:rgba(102,102,102,.22); --reload-overlay:rgba(102,102,102,.6); --panel-explorer:#d9d9d9; --panel-editor:#fff; --panel-terminal:#d9d9d9; --panel-border:#666; --button-border:#666; } -:root[data-visual-theme="monochrome-dark"] { --crust:#000; --mantle:#000; --base:#000; --surface0:#000; --surface1:#999; --overlay0:#999; --text:#fff; --subtext0:#999; --green:#fff; --red:#999; --yellow:#999; --blue:#fff; --mauve:#fff; --peach:#999; --terminal-background:#333; --primary-hover:#fff; --danger-wash:rgba(153,153,153,.12); --danger-wash-hover:rgba(153,153,153,.22); --reload-overlay:rgba(153,153,153,.6); --panel-explorer:#333; --panel-editor:#000; --panel-terminal:#333; --panel-border:#999; --button-border:#999; } +:root[data-visual-theme="monochrome-light"] { --crust:#fff; --mantle:#fff; --base:#fff; --surface0:#fff; --surface1:#666; --overlay0:#666; --text:#000; --subtext0:#666; --green:#000; --red:#666; --yellow:#666; --blue:#000; --mauve:#000; --peach:#666; --terminal-background:#ddd; --primary-hover:#000; --danger-wash:rgba(102,102,102,.12); --danger-wash-hover:rgba(102,102,102,.22); --reload-overlay:rgba(102,102,102,.6); --panel-explorer:#ddd; --panel-editor:#fff; --panel-terminal:#ddd; --panel-border:#666; --button-border:#666; } +:root[data-visual-theme="monochrome-dark"] { --crust:#000; --mantle:#000; --base:#000; --surface0:#000; --surface1:#999; --overlay0:#999; --text:#fff; --subtext0:#999; --green:#fff; --red:#999; --yellow:#999; --blue:#fff; --mauve:#fff; --peach:#999; --terminal-background:#222; --primary-hover:#fff; --danger-wash:rgba(153,153,153,.12); --danger-wash-hover:rgba(153,153,153,.22); --reload-overlay:rgba(153,153,153,.6); --panel-explorer:#222; --panel-editor:#000; --panel-terminal:#222; --panel-border:#999; --button-border:#999; } body { height: 100vh; diff --git a/src/ui/themes.mjs b/src/ui/themes.mjs index d4f71d1..a20295c 100644 --- a/src/ui/themes.mjs +++ b/src/ui/themes.mjs @@ -79,13 +79,13 @@ export const VISUAL_THEMES = Object.freeze([ id: 'monochrome-light', label: 'Monochrome Light', monacoTheme: 'browser-cpp-monochrome-light', selection: { foreground: '#ffffff', background: '#666666', inactiveBackground: '#999999' }, cssTokens: { crust: '#ffffff', mantle: '#ffffff', base: '#ffffff', surface0: '#ffffff', surface1: '#666666', overlay0: '#666666', text: '#000000', subtext0: '#666666', green: '#000000', red: '#666666', yellow: '#666666', blue: '#000000', mauve: '#000000', peach: '#666666' }, - terminalColors: { background: '#d9d9d9', foreground: '#000000', accent: '#666666', ansi: monochromeAnsi('#000000', '#666666') }, preview: { background: '#ffffff', foreground: '#000000', accent: '#666666' }, + terminalColors: { background: '#dddddd', foreground: '#000000', accent: '#666666', ansi: monochromeAnsi('#000000', '#666666') }, preview: { background: '#ffffff', foreground: '#000000', accent: '#666666' }, }), visualTheme({ id: 'monochrome-dark', label: 'Monochrome Dark', monacoTheme: 'browser-cpp-monochrome-dark', selection: { foreground: '#000000', background: '#999999', inactiveBackground: '#666666' }, cssTokens: { crust: '#000000', mantle: '#000000', base: '#000000', surface0: '#000000', surface1: '#999999', overlay0: '#999999', text: '#ffffff', subtext0: '#999999', green: '#ffffff', red: '#999999', yellow: '#999999', blue: '#ffffff', mauve: '#ffffff', peach: '#999999' }, - terminalColors: { background: '#333333', foreground: '#ffffff', accent: '#999999', ansi: monochromeAnsi('#ffffff', '#999999') }, preview: { background: '#000000', foreground: '#ffffff', accent: '#999999' }, + terminalColors: { background: '#222222', foreground: '#ffffff', accent: '#999999', ansi: monochromeAnsi('#ffffff', '#999999') }, preview: { background: '#000000', foreground: '#ffffff', accent: '#999999' }, }), ]); From 090aee91ccb6bb87cedd3dd7cdc96e7f9421bb6f Mon Sep 17 00:00:00 2001 From: Kevin Buffardi Date: Thu, 29 Oct 2026 23:57:41 -0700 Subject: [PATCH 09/13] fix: outline monochrome file tabs --- scripts/e2e-settings-visual-theme.test.mjs | 2 ++ src/ui/styles.css | 7 +++++++ 2 files changed, 9 insertions(+) diff --git a/scripts/e2e-settings-visual-theme.test.mjs b/scripts/e2e-settings-visual-theme.test.mjs index 312b6a1..b51e3de 100644 --- a/scripts/e2e-settings-visual-theme.test.mjs +++ b/scripts/e2e-settings-visual-theme.test.mjs @@ -86,6 +86,8 @@ test('monochrome themes separate panels and outline buttons', async () => { assert.match(css, /\.tab-close[\s\S]*border: none;/); assert.match(css, /\.panel-controls button[\s\S]*border: none;/); assert.match(css, /monochrome-light[^}]*\.panel-header-btn[\s\S]*border: 1px solid var\(--button-border\);/); + assert.match(css, /monochrome-light[^}]*\.tab[\s\S]*border: 1px solid var\(--panel-border\);/); + assert.match(css, /monochrome-dark[^}]*\.tab\.active[\s\S]*border: 1px solid var\(--panel-border\);/); }); function contrastRatio(foreground, background) { diff --git a/src/ui/styles.css b/src/ui/styles.css index b835a6f..447929d 100644 --- a/src/ui/styles.css +++ b/src/ui/styles.css @@ -558,6 +558,13 @@ select:focus { border-top: 1px solid var(--blue); } +:root[data-visual-theme="monochrome-light"] .tab, +:root[data-visual-theme="monochrome-dark"] .tab, +:root[data-visual-theme="monochrome-light"] .tab.active, +:root[data-visual-theme="monochrome-dark"] .tab.active { + border: 1px solid var(--panel-border); +} + .tab.dirty::after { content: '●'; color: var(--yellow); From 1caa2ab24842703b6ef7af372fb834c1cd7824a6 Mon Sep 17 00:00:00 2001 From: Kevin Buffardi Date: Fri, 30 Oct 2026 00:00:14 -0700 Subject: [PATCH 10/13] fix: reinforce monochrome tab right borders --- scripts/e2e-settings-visual-theme.test.mjs | 1 + src/ui/styles.css | 1 + 2 files changed, 2 insertions(+) diff --git a/scripts/e2e-settings-visual-theme.test.mjs b/scripts/e2e-settings-visual-theme.test.mjs index b51e3de..3da7cf5 100644 --- a/scripts/e2e-settings-visual-theme.test.mjs +++ b/scripts/e2e-settings-visual-theme.test.mjs @@ -88,6 +88,7 @@ test('monochrome themes separate panels and outline buttons', async () => { assert.match(css, /monochrome-light[^}]*\.panel-header-btn[\s\S]*border: 1px solid var\(--button-border\);/); assert.match(css, /monochrome-light[^}]*\.tab[\s\S]*border: 1px solid var\(--panel-border\);/); assert.match(css, /monochrome-dark[^}]*\.tab\.active[\s\S]*border: 1px solid var\(--panel-border\);/); + assert.match(css, /monochrome-light[^}]*box-shadow: inset 0 0 0 1px var\(--panel-border\);/); }); function contrastRatio(foreground, background) { diff --git a/src/ui/styles.css b/src/ui/styles.css index 447929d..46c09b9 100644 --- a/src/ui/styles.css +++ b/src/ui/styles.css @@ -563,6 +563,7 @@ select:focus { :root[data-visual-theme="monochrome-light"] .tab.active, :root[data-visual-theme="monochrome-dark"] .tab.active { border: 1px solid var(--panel-border); + box-shadow: inset 0 0 0 1px var(--panel-border); } .tab.dirty::after { From 9889212b40f94b32bc1cd7362742c792dffe9f0c Mon Sep 17 00:00:00 2001 From: Kevin Buffardi Date: Fri, 30 Oct 2026 00:02:17 -0700 Subject: [PATCH 11/13] fix: match active tab side border color --- scripts/e2e-settings-visual-theme.test.mjs | 1 + src/ui/styles.css | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/scripts/e2e-settings-visual-theme.test.mjs b/scripts/e2e-settings-visual-theme.test.mjs index 3da7cf5..af35ab7 100644 --- a/scripts/e2e-settings-visual-theme.test.mjs +++ b/scripts/e2e-settings-visual-theme.test.mjs @@ -89,6 +89,7 @@ test('monochrome themes separate panels and outline buttons', async () => { assert.match(css, /monochrome-light[^}]*\.tab[\s\S]*border: 1px solid var\(--panel-border\);/); assert.match(css, /monochrome-dark[^}]*\.tab\.active[\s\S]*border: 1px solid var\(--panel-border\);/); assert.match(css, /monochrome-light[^}]*box-shadow: inset 0 0 0 1px var\(--panel-border\);/); + assert.match(css, /monochrome-dark[^}]*border-right-color: var\(--blue\);/); }); function contrastRatio(foreground, background) { diff --git a/src/ui/styles.css b/src/ui/styles.css index 46c09b9..207481a 100644 --- a/src/ui/styles.css +++ b/src/ui/styles.css @@ -566,6 +566,13 @@ select:focus { box-shadow: inset 0 0 0 1px var(--panel-border); } +:root[data-visual-theme="monochrome-light"] .tab.active, +:root[data-visual-theme="monochrome-dark"] .tab.active { + border-top-color: var(--blue); + border-right-color: var(--blue); + box-shadow: inset 0 0 0 1px var(--blue); +} + .tab.dirty::after { content: '●'; color: var(--yellow); From da692e7e11dddff043be7fa4f84cef4c003438b5 Mon Sep 17 00:00:00 2001 From: Kevin Buffardi Date: Fri, 30 Oct 2026 00:04:39 -0700 Subject: [PATCH 12/13] fix: force monochrome tab edge visibility --- scripts/e2e-settings-visual-theme.test.mjs | 1 + src/ui/styles.css | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/scripts/e2e-settings-visual-theme.test.mjs b/scripts/e2e-settings-visual-theme.test.mjs index af35ab7..d85fb07 100644 --- a/scripts/e2e-settings-visual-theme.test.mjs +++ b/scripts/e2e-settings-visual-theme.test.mjs @@ -90,6 +90,7 @@ test('monochrome themes separate panels and outline buttons', async () => { assert.match(css, /monochrome-dark[^}]*\.tab\.active[\s\S]*border: 1px solid var\(--panel-border\);/); assert.match(css, /monochrome-light[^}]*box-shadow: inset 0 0 0 1px var\(--panel-border\);/); assert.match(css, /monochrome-dark[^}]*border-right-color: var\(--blue\);/); + assert.match(css, /monochrome-light[^}]*border-right: 2px solid var\(--blue\) !important;/); }); function contrastRatio(foreground, background) { diff --git a/src/ui/styles.css b/src/ui/styles.css index 207481a..f00107f 100644 --- a/src/ui/styles.css +++ b/src/ui/styles.css @@ -573,6 +573,13 @@ select:focus { box-shadow: inset 0 0 0 1px var(--blue); } +:root[data-visual-theme="monochrome-light"] .tab, +:root[data-visual-theme="monochrome-dark"] .tab { + position: relative; + z-index: 1; + border-right: 2px solid var(--blue) !important; +} + .tab.dirty::after { content: '●'; color: var(--yellow); From 1374e40eb1f9f425c077784319260574ca7a4514 Mon Sep 17 00:00:00 2001 From: Kevin Buffardi Date: Fri, 30 Oct 2026 00:10:08 -0700 Subject: [PATCH 13/13] fix: normalize monochrome tab outlines --- scripts/e2e-settings-visual-theme.test.mjs | 4 +--- src/ui/styles.css | 12 +----------- 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/scripts/e2e-settings-visual-theme.test.mjs b/scripts/e2e-settings-visual-theme.test.mjs index d85fb07..0871c1f 100644 --- a/scripts/e2e-settings-visual-theme.test.mjs +++ b/scripts/e2e-settings-visual-theme.test.mjs @@ -88,9 +88,7 @@ test('monochrome themes separate panels and outline buttons', async () => { assert.match(css, /monochrome-light[^}]*\.panel-header-btn[\s\S]*border: 1px solid var\(--button-border\);/); assert.match(css, /monochrome-light[^}]*\.tab[\s\S]*border: 1px solid var\(--panel-border\);/); assert.match(css, /monochrome-dark[^}]*\.tab\.active[\s\S]*border: 1px solid var\(--panel-border\);/); - assert.match(css, /monochrome-light[^}]*box-shadow: inset 0 0 0 1px var\(--panel-border\);/); - assert.match(css, /monochrome-dark[^}]*border-right-color: var\(--blue\);/); - assert.match(css, /monochrome-light[^}]*border-right: 2px solid var\(--blue\) !important;/); + assert.match(css, /monochrome-dark[^}]*border-color: var\(--blue\);/); }); function contrastRatio(foreground, background) { diff --git a/src/ui/styles.css b/src/ui/styles.css index f00107f..54d736d 100644 --- a/src/ui/styles.css +++ b/src/ui/styles.css @@ -563,21 +563,11 @@ select:focus { :root[data-visual-theme="monochrome-light"] .tab.active, :root[data-visual-theme="monochrome-dark"] .tab.active { border: 1px solid var(--panel-border); - box-shadow: inset 0 0 0 1px var(--panel-border); } :root[data-visual-theme="monochrome-light"] .tab.active, :root[data-visual-theme="monochrome-dark"] .tab.active { - border-top-color: var(--blue); - border-right-color: var(--blue); - box-shadow: inset 0 0 0 1px var(--blue); -} - -:root[data-visual-theme="monochrome-light"] .tab, -:root[data-visual-theme="monochrome-dark"] .tab { - position: relative; - z-index: 1; - border-right: 2px solid var(--blue) !important; + border-color: var(--blue); } .tab.dirty::after {