diff --git a/README.md b/README.md
index aaa11d1..7b983da 100644
--- a/README.md
+++ b/README.md
@@ -47,6 +47,16 @@ Right now, that means a VSCode + Web UI flow that works without extra Git config
- IDE integration via extension commands, status bar notifications, and file conflict discovery.
- Extremely active development (issues and feedback are always welcome, and feel free to email me!).
+- Light and dark themes (the demo above needs to be updated!):
+
+
+
+
+
+
+
+
+
> [!IMPORTANT]
> MergeNB is currently **not compatible with nbdime** in the same merge flow.
diff --git a/esbuild.js b/esbuild.js
index 95a8c25..41a4abc 100644
--- a/esbuild.js
+++ b/esbuild.js
@@ -67,6 +67,21 @@ function copyKatex() {
}
}
+/**
+ * Copy local web fonts to dist/web/fonts directory
+ */
+function copyWebFonts() {
+ const srcDir = path.join(__dirname, 'src', 'web', 'client', 'fonts');
+ const destDir = path.join(__dirname, 'dist', 'web', 'fonts');
+
+ if (fs.existsSync(srcDir)) {
+ copyDir(srcDir, destDir);
+ console.log('[fonts] Copied bundled web fonts');
+ } else {
+ console.warn(`[fonts] Warning: ${srcDir} not found`);
+ }
+}
+
async function main() {
// Extension bundle (Node.js / VSCode)
const extensionCtx = await esbuild.context({
@@ -119,6 +134,7 @@ async function main() {
// Copy KaTeX after build
copyKatex();
+ copyWebFonts();
}
main().catch(e => {
diff --git a/package-lock.json b/package-lock.json
index ea49609..ff54ed0 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -984,6 +984,7 @@
"integrity": "sha512-BkmoP5/FhRYek5izySdkOneRyXYN35I860MFAGupTdebyE66uZaR+bXLHq8k4DirE5DwQi3NuhvRU1jqTVwUrQ==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"undici-types": "~7.16.0"
}
@@ -994,6 +995,7 @@
"integrity": "sha512-KkiJeU6VbYbUOp5ITMIc7kBfqlYkKA5KhEHVrGMmUUMt7NeaZg65ojdPk+FtNrBAOXNVM5QM72jnADjM+XVRAQ==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"csstype": "^3.2.2"
}
@@ -1077,6 +1079,7 @@
"integrity": "sha512-4z2nCSBfVIMnbuu8uinj+f0o4qOeggYJLbjpPHka3KH1om7e+H9yLKTYgksTaHcGco+NClhhY2vyO3HsMH1RGw==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.55.0",
"@typescript-eslint/types": "8.55.0",
@@ -1318,6 +1321,7 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
+ "peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -2304,6 +2308,7 @@
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -4725,6 +4730,7 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
"integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -4734,6 +4740,7 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",
"integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"scheduler": "^0.27.0"
},
@@ -5554,6 +5561,7 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=12"
},
@@ -5738,6 +5746,7 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
+ "peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
diff --git a/package.json b/package.json
index 2e06b24..c2c9afc 100644
--- a/package.json
+++ b/package.json
@@ -112,6 +112,12 @@
"type": "boolean",
"default": false,
"description": "Show the base (common ancestor) branch column in the conflict resolution view. When false, only current and incoming columns are shown."
+ },
+ "mergeNB.ui.theme": {
+ "type": "string",
+ "enum": ["dark", "light"],
+ "default": "dark",
+ "description": "Visual theme for the conflict resolution UI. 'light' provides a light, beige theme inspired by the MergeNB logo. 'dark' provides an inverse dark theme."
}
}
}
diff --git a/readme-assets/dark-theme.png b/readme-assets/dark-theme.png
new file mode 100644
index 0000000..dedfdb6
Binary files /dev/null and b/readme-assets/dark-theme.png differ
diff --git a/readme-assets/light-theme.png b/readme-assets/light-theme.png
new file mode 100644
index 0000000..2494aca
Binary files /dev/null and b/readme-assets/light-theme.png differ
diff --git a/src/resolver.ts b/src/resolver.ts
index d67ccc4..72a5d14 100644
--- a/src/resolver.ts
+++ b/src/resolver.ts
@@ -272,7 +272,8 @@ export class NotebookConflictResolver {
autoResolveResult: autoResolveResult,
hideNonConflictOutputs: settings.hideNonConflictOutputs,
enableUndoRedoHotkeys: settings.enableUndoRedoHotkeys,
- showBaseColumn: settings.showBaseColumn
+ showBaseColumn: settings.showBaseColumn,
+ theme: settings.theme
};
const resolutionCallback = async (resolution: UnifiedResolution): Promise => {
diff --git a/src/settings.ts b/src/settings.ts
index 828aa1e..fa5c23f 100644
--- a/src/settings.ts
+++ b/src/settings.ts
@@ -10,6 +10,7 @@
* - hideNonConflictOutputs: Hide outputs for non-conflicted cells in UI (default: true)
* - enableUndoRedoHotkeys: Enable Ctrl+Z / Ctrl+Shift+Z in web UI (default: true)
* - showBaseColumn: Show base branch column in 3-column view (default: false, true in headless/testing)
+ * - theme: UI theme selection ('dark' | 'light', default: 'light')
*
* These reduce manual conflict resolution for common non-semantic differences.
*/
@@ -30,6 +31,7 @@ export interface MergeNBSettings {
hideNonConflictOutputs: boolean;
enableUndoRedoHotkeys: boolean;
showBaseColumn: boolean;
+ theme: 'dark' | 'light';
}
/** Default settings used in headless mode */
@@ -40,7 +42,8 @@ const DEFAULT_SETTINGS: MergeNBSettings = {
autoResolveWhitespace: true,
hideNonConflictOutputs: true,
enableUndoRedoHotkeys: true,
- showBaseColumn: true
+ showBaseColumn: true,
+ theme: 'light'
};
/**
@@ -64,6 +67,7 @@ export function getSettings(): MergeNBSettings {
hideNonConflictOutputs: true,
enableUndoRedoHotkeys: true,
showBaseColumn: false,
+ theme: 'light',
};
const config = vscode.workspace.getConfiguration('mergeNB');
@@ -76,13 +80,17 @@ export function getSettings(): MergeNBSettings {
hideNonConflictOutputs: config.get('ui.hideNonConflictOutputs', defaults.hideNonConflictOutputs),
enableUndoRedoHotkeys: config.get('ui.enableUndoRedoHotkeys', defaults.enableUndoRedoHotkeys),
showBaseColumn: config.get('ui.showBaseColumn', defaults.showBaseColumn),
+ theme: config.get<'dark' | 'light'>('ui.theme', defaults.theme),
};
}
/**
- * Check if a specific auto-resolve setting is enabled
+ * Check if a specific auto-resolve setting is enabled.
+ * Only checks actual auto-resolve settings, not UI settings.
*/
-export function isAutoResolveEnabled(setting: keyof MergeNBSettings): boolean {
+export function isAutoResolveEnabled(
+ setting: 'autoResolveExecutionCount' | 'autoResolveKernelVersion' | 'stripOutputs' | 'autoResolveWhitespace'
+): boolean {
const settings = getSettings();
return settings[setting];
}
diff --git a/src/tests/logicRegression.test.ts b/src/tests/logicRegression.test.ts
index c93d627..ee11091 100644
--- a/src/tests/logicRegression.test.ts
+++ b/src/tests/logicRegression.test.ts
@@ -68,6 +68,7 @@ export async function run(): Promise {
hideNonConflictOutputs: true,
enableUndoRedoHotkeys: true,
showBaseColumn: true,
+ theme: 'light',
});
assert.ok(
conflicts.some(c => c.type === 'metadata-changed'),
diff --git a/src/web/WebConflictPanel.ts b/src/web/WebConflictPanel.ts
index df96acc..c102f06 100644
--- a/src/web/WebConflictPanel.ts
+++ b/src/web/WebConflictPanel.ts
@@ -92,7 +92,8 @@ export class WebConflictPanel {
void server.openSession(
this._sessionId,
'', // No HTML content needed - server generates shell
- (message: unknown) => this._handleMessage(message)
+ (message: unknown) => this._handleMessage(message),
+ this._conflict?.theme ?? 'light'
).then(() => {
// Send conflict data to browser once connected
this._sendConflictData();
@@ -120,6 +121,7 @@ export class WebConflictPanel {
hideNonConflictOutputs: this._conflict.hideNonConflictOutputs,
enableUndoRedoHotkeys: this._conflict.enableUndoRedoHotkeys,
showBaseColumn: this._conflict.showBaseColumn,
+ theme: this._conflict.theme,
currentBranch: this._conflict.semanticConflict?.currentBranch,
incomingBranch: this._conflict.semanticConflict?.incomingBranch,
};
diff --git a/src/web/client/App.tsx b/src/web/client/App.tsx
index 96e33df..1078d64 100644
--- a/src/web/client/App.tsx
+++ b/src/web/client/App.tsx
@@ -3,14 +3,22 @@
* @description Root React component for the conflict resolver.
*/
-import React from 'react';
+import React, { useEffect } from 'react';
import { useWebSocket } from './useWebSocket';
import { ConflictResolver } from './ConflictResolver';
+import { injectStyles } from './styles';
import type { ConflictChoice, ResolvedRow } from './types';
export function App(): React.ReactElement {
const { connected, conflictData, sendMessage, resolutionStatus, resolutionMessage } = useWebSocket();
+ // Inject theme styles when conflict data loads
+ useEffect(() => {
+ if (conflictData?.theme) {
+ injectStyles(conflictData.theme);
+ }
+ }, [conflictData?.theme]);
+
const handleResolve = (resolutions: ConflictChoice[], markAsResolved: boolean, renumberExecutionCounts: boolean, resolvedRows: ResolvedRow[]) => {
sendMessage({
command: 'resolve',
diff --git a/src/web/client/ConflictResolver.tsx b/src/web/client/ConflictResolver.tsx
index feace59..4c2bafa 100644
--- a/src/web/client/ConflictResolver.tsx
+++ b/src/web/client/ConflictResolver.tsx
@@ -541,7 +541,14 @@ export function ConflictResolver({