diff --git a/manifest.json b/manifest.json index c519468..6f47e2a 100644 --- a/manifest.json +++ b/manifest.json @@ -3,7 +3,7 @@ "name": "browser.cpp", "short_name": "browser.cpp", "description": "In-browser C++20 IDE powered by Monaco Editor and WASM Clang", - "version": "0.5.2", + "version": "0.5.3", "minimum_chrome_version": "105", "icons": { "16": "icons/icon16.png", diff --git a/package-lock.json b/package-lock.json index 2518194..541be3e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "browser.cpp", - "version": "0.5.2", + "version": "0.5.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "browser.cpp", - "version": "0.5.2", + "version": "0.5.3", "dependencies": { "@xterm/addon-fit": "^0.11.0", "@xterm/addon-web-links": "^0.12.0", diff --git a/package.json b/package.json index 97be7c0..c2f1130 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "browser.cpp", - "version": "0.5.2", + "version": "0.5.3", "description": "In-browser C++20 IDE with WASM Clang toolchain", "private": true, "scripts": { diff --git a/scripts/e2e-settings-memory-debug.test.mjs b/scripts/e2e-settings-memory-debug.test.mjs index 7cd4f06..89ddab4 100644 --- a/scripts/e2e-settings-memory-debug.test.mjs +++ b/scripts/e2e-settings-memory-debug.test.mjs @@ -162,6 +162,12 @@ test('Settings markup exposes the official UBSan help link in a protected new ta assert.doesNotMatch(html, /settings-save|Save settings|Apply settings/i); }); +test('Settings gear uses the toolbar icon size', async () => { + const css = await readFile(new URL('../src/ui/styles.css', import.meta.url), 'utf8'); + + assert.match(css, /\.settings-gear-icon\s*\{[^}]*font-size:\s*16px\s*;/s); +}); + test('Settings backdrop preserves accurate workspace theme previews', async () => { const css = await readFile(new URL('../src/ui/styles.css', import.meta.url), 'utf8'); const backdrop = css.match(/\.settings-backdrop\s*\{([^}]*)\}/)?.[1]; diff --git a/scripts/e2e-settings-visual-theme.test.mjs b/scripts/e2e-settings-visual-theme.test.mjs index 557146b..9d8452a 100644 --- a/scripts/e2e-settings-visual-theme.test.mjs +++ b/scripts/e2e-settings-visual-theme.test.mjs @@ -104,6 +104,31 @@ test('monochrome Monaco selection backgrounds contrast with normal code text', a assert.doesNotMatch(css, /\.monaco-editor\s+\.selected-text\s*\{[^}]*color:/s); }); +test('monochrome editor selections share the highest-contrast background without changing terminal selections', () => { + const expectedEditorSelections = { + 'monochrome-light': { foreground: '#000000', background: '#bbbbbb' }, + 'monochrome-dark': { foreground: '#ffffff', background: '#555555' }, + }; + const expectedTerminalSelections = { + 'monochrome-light': { background: '#999999', inactiveBackground: '#bbbbbb' }, + 'monochrome-dark': { background: '#555555', inactiveBackground: '#707070' }, + }; + + for (const [id, expected] of Object.entries(expectedEditorSelections)) { + const theme = getVisualTheme(id); + assert.deepEqual(theme.editorSelection, { + ...expected, + inactiveBackground: expected.background, + }); + assert.equal(theme.editorSelection.foreground, theme.cssTokens.text); + assert.ok(contrastRatio(theme.editorSelection.foreground, theme.editorSelection.background) >= 4.5); + assert.deepEqual(theme.selection, { + foreground: expected.foreground, + ...expectedTerminalSelections[id], + }); + } +}); + test('Monaco selection smoke hook reads a real editor selection', async () => { const editor = await readFile(new URL('../src/ui/editor.js', import.meta.url), 'utf8'); assert.match(editor, /_editor\.setSelection\(new monaco\.Range\(/); diff --git a/scripts/smoke-browser.mjs b/scripts/smoke-browser.mjs index b9b35d0..4369215 100644 --- a/scripts/smoke-browser.mjs +++ b/scripts/smoke-browser.mjs @@ -952,6 +952,10 @@ int main() { const panel = document.getElementById('settings-panel'); const toggle = document.getElementById('setting-memory-debug'); const help = document.querySelector('.settings-help-link'); + const iconSize = (selector) => { + const icon = document.querySelector(selector); + return icon ? getComputedStyle(icon).fontSize : null; + }; return { buttonText: button?.textContent?.replace(/\\s+/g, ' ').trim(), expanded: button?.getAttribute('aria-expanded'), @@ -960,12 +964,19 @@ int main() { helpHref: help?.href, helpTarget: help?.target, helpRel: help?.rel, + settingsGearSize: iconSize('#btn-settings .settings-gear-icon'), + openIconSize: iconSize('#btn-open .codicon'), + compileIconSize: iconSize('#btn-compile .codicon'), }; })()`); assert(settingsDefault.buttonText?.includes('Settings'), 'Settings button label did not render'); assert(settingsDefault.expanded === 'false', 'Settings button should initially be collapsed'); assert(settingsDefault.panelHidden === true, 'Settings panel should initially be hidden'); assert(settingsDefault.toggleChecked === false, 'Memory Debug Mode should default to off'); + assert(settingsDefault.settingsGearSize === settingsDefault.openIconSize, + 'Settings gear size does not match the Open icon'); + assert(settingsDefault.settingsGearSize === settingsDefault.compileIconSize, + 'Settings gear size does not match the Compile icon'); assert( settingsDefault.helpHref === 'https://clang.llvm.org/docs/UndefinedBehaviorSanitizer.html', `Unexpected UBSan help URL: ${settingsDefault.helpHref}` @@ -1050,6 +1061,10 @@ int main() { }, 'real help output in terminal buffer', 5_000); assert(helpOutput.includes('Run the last compiled binary'), 'Real help output is missing the run-command description'); + const monochromeTerminalSelections = { + 'monochrome-light': { background: '#999999', foreground: '#000000', inactiveBackground: '#bbbbbb' }, + 'monochrome-dark': { background: '#555555', foreground: '#ffffff', inactiveBackground: '#707070' }, + }; let previousSelection = null; 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()`); @@ -1064,6 +1079,11 @@ int main() { assert(monacoColors?.background && monacoColors.foreground && monacoColors.inactiveBackground, `${themeId} Monaco selection styles were not computed`); if (themeId.startsWith('monochrome-')) { + const expectedBackground = themeId === 'monochrome-light' ? '#bbbbbb' : '#555555'; + assert(monacoColors.background === expectedBackground, + `${themeId} active Monaco selection has the wrong background`); + assert(monacoColors.inactiveBackground === expectedBackground, + `${themeId} inactive Monaco selection has the wrong background`); assert(wcagContrastRatio(monacoColors.foreground, monacoColors.background) >= 4.5, `${themeId} active Monaco selection fails WCAG AA contrast`); assert(wcagContrastRatio(monacoColors.foreground, monacoColors.inactiveBackground) >= 4.5, @@ -1078,6 +1098,7 @@ int main() { return channels?.length >= 3 && channels[0] === channels[1] && channels[1] === channels[2]; }), `${themeId} Monaco text contains chromatic syntax or bracket colors`); } + const terminalColors = monochromeTerminalSelections[themeId] ?? monacoColors; if (previousSelection) { const retained = await evaluate(cdp, sessionId, `document.getElementById('terminal-container')?.__browserCppReadTerminalSelection?.() || null`); @@ -1086,28 +1107,28 @@ int main() { const changedPaint = await waitFor(async () => { const current = await evaluate(cdp, sessionId, `document.getElementById('terminal-container')?.__browserCppReadSelectionPaint?.() || null`); - return current?.rects?.length && normalizeCssColor(current.rects[0].background) === monacoColors.inactiveBackground + return current?.rects?.length && normalizeCssColor(current.rects[0].background) === terminalColors.inactiveBackground ? current : null; }, `${themeId} inactive terminal selection repaint`, 5_000); - assert(changedPaint.rects.every(({ background }) => normalizeCssColor(background) === monacoColors.inactiveBackground), - `${themeId} existing selection did not adopt Monaco's inactive selection color`); + assert(changedPaint.rects.every(({ background }) => normalizeCssColor(background) === terminalColors.inactiveBackground), + `${themeId} existing selection did not adopt the configured inactive selection color`); } const targetText = previousSelection ? 'g++' : 'Available commands:'; const selection = await selectTerminalText(cdp, sessionId, targetText); - assert(selection.theme.selectionBackground === monacoColors.background, - `${themeId} terminal active selection background ${selection.theme.selectionBackground} does not exactly match Monaco ${monacoColors.background}`); - assert(selection.theme.selectionForeground === monacoColors.foreground, - `${themeId} terminal selection foreground ${selection.theme.selectionForeground} does not exactly match Monaco ${monacoColors.foreground}`); - assert(selection.theme.selectionInactiveBackground === monacoColors.inactiveBackground, - `${themeId} terminal inactive selection background ${selection.theme.selectionInactiveBackground} does not exactly match Monaco ${monacoColors.inactiveBackground}`); + assert(selection.theme.selectionBackground === terminalColors.background, + `${themeId} terminal active selection background is incorrect`); + assert(selection.theme.selectionForeground === terminalColors.foreground, + `${themeId} terminal selection foreground is incorrect`); + assert(selection.theme.selectionInactiveBackground === terminalColors.inactiveBackground, + `${themeId} terminal inactive selection background is incorrect`); assert(selection.text.includes(targetText), `Terminal selected text does not include ${JSON.stringify(targetText)}: ${JSON.stringify(selection.text)}`); - assert(selection.paint.rects.every(({ background }) => normalizeCssColor(background) === monacoColors.background), - `${themeId} rendered active selection background does not exactly match Monaco`); - assert(selection.paint.selectedTextForegrounds.some((color) => normalizeCssColor(color) === monacoColors.foreground), - `${themeId} rendered selected text foreground does not exactly match Monaco's ${monacoColors.foreground}`); + assert(selection.paint.rects.every(({ background }) => normalizeCssColor(background) === terminalColors.background), + `${themeId} rendered active terminal selection background is incorrect`); + assert(selection.paint.selectedTextForegrounds.some((color) => normalizeCssColor(color) === terminalColors.foreground), + `${themeId} rendered terminal selected text foreground is incorrect`); previousSelection = { ...selection, colors: monacoColors, @@ -1125,15 +1146,15 @@ int main() { const inactivePaint = await waitFor(async () => { const current = await evaluate(cdp, sessionId, `document.getElementById('terminal-container')?.__browserCppReadSelectionPaint?.() || null`); - return current?.rects?.length && normalizeCssColor(current.rects[0].background) === monacoColors.inactiveBackground + return current?.rects?.length && normalizeCssColor(current.rects[0].background) === terminalColors.inactiveBackground ? current : null; }, `${themeId} unfocused selection color`, 5_000); - assert(inactivePaint.rects.every(({ background }) => normalizeCssColor(background) === monacoColors.inactiveBackground), - `${themeId} rendered inactive selection background does not exactly match Monaco`); + assert(inactivePaint.rects.every(({ background }) => normalizeCssColor(background) === terminalColors.inactiveBackground), + `${themeId} rendered inactive terminal selection background is incorrect`); const afterBlur = await captureScreenshot(cdp, sessionId); assertScreenshotSelectionChanged(selection.before, afterBlur, selection.paintBounds, - monacoColors.inactiveBackground); + terminalColors.inactiveBackground); previousSelection = { ...selection, beforeThemeChange: afterBlur }; } diff --git a/src/ui/editor.js b/src/ui/editor.js index 9ebf073..0776b7c 100644 --- a/src/ui/editor.js +++ b/src/ui/editor.js @@ -79,6 +79,7 @@ export function createEditor(container, { theme = 'browser-cpp-dark' } = {}) { if (visualTheme.id === 'browser-cpp-dark') continue; if (visualTheme.id.startsWith('monochrome-')) { const { cssTokens: colors } = visualTheme; + const selection = visualTheme.editorSelection ?? visualTheme.selection; const isLight = visualTheme.id === 'monochrome-light'; monaco.editor.defineTheme(visualTheme.monacoTheme, { base: isLight ? 'vs' : 'vs-dark', @@ -91,9 +92,9 @@ export function createEditor(container, { theme = 'browser-cpp-dark' } = {}) { '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, + 'editor.selectionForeground': selection.foreground, + 'editor.selectionBackground': selection.background, + 'editor.inactiveSelectionBackground': selection.inactiveBackground, 'editorIndentGuide.background': colors.surface1, 'editorWidget.background': colors.mantle, 'editorSuggestWidget.background': colors.mantle, diff --git a/src/ui/styles.css b/src/ui/styles.css index 54d736d..97eea61 100644 --- a/src/ui/styles.css +++ b/src/ui/styles.css @@ -187,6 +187,10 @@ select:focus { flex: 0 0 auto; } +.settings-gear-icon { + font-size: 16px; +} + /* ── Extension settings drawer ───────────────────────────────────────────── */ .settings-backdrop { diff --git a/src/ui/themes.mjs b/src/ui/themes.mjs index dee6d5e..97d6c95 100644 --- a/src/ui/themes.mjs +++ b/src/ui/themes.mjs @@ -28,11 +28,12 @@ const monochromeAnsi = (foreground, tertiary) => ({ brightCyan: foreground, brightWhite: foreground, }); -const visualTheme = ({ id, label, monacoTheme, selection, cssTokens, terminalColors, preview }) => ({ +const visualTheme = ({ id, label, monacoTheme, selection, editorSelection, cssTokens, terminalColors, preview }) => ({ id, label, monacoTheme, selection, + editorSelection, cssTokens, terminalTheme: terminal( terminalColors.background, @@ -78,12 +79,14 @@ export const VISUAL_THEMES = Object.freeze([ visualTheme({ id: 'monochrome-light', label: 'Monochrome Light', monacoTheme: 'browser-cpp-monochrome-light', selection: { foreground: '#000000', background: '#999999', inactiveBackground: '#bbbbbb' }, + editorSelection: { foreground: '#000000', background: '#bbbbbb', inactiveBackground: '#bbbbbb' }, 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: '#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: '#ffffff', background: '#555555', inactiveBackground: '#707070' }, + editorSelection: { foreground: '#ffffff', background: '#555555', inactiveBackground: '#555555' }, 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: '#222222', foreground: '#ffffff', accent: '#999999', ansi: monochromeAnsi('#ffffff', '#999999') }, preview: { background: '#000000', foreground: '#ffffff', accent: '#999999' }, }),