diff --git a/src/main.tsx b/src/main.tsx
index 00ed6260..1e4c8894 100644
--- a/src/main.tsx
+++ b/src/main.tsx
@@ -10,6 +10,7 @@ import { Toaster } from "@/components/sonner";
import { Auth, getStoredAuth, clearStoredAuth } from "@/auth/Auth";
import { getUserInfo, getCurrentToken, appReadyPromise } from "@/main-axios";
import { applyAccentColor, applyFontSize } from "@/lib/theme";
+import { installElectronWheelZoomGuard } from "@/lib/electron-wheel-zoom";
import type { FontSizeId } from "@/types/ui-types";
import { useServiceWorker } from "@/hooks/use-service-worker";
import { useTranslation } from "react-i18next";
@@ -340,6 +341,8 @@ function RootApp() {
return ;
}
+installElectronWheelZoomGuard();
+
prepareClientCacheVersion().finally(() => {
createRoot(document.getElementById("root")!).render(
diff --git a/src/ui/lib/electron-wheel-zoom.test.ts b/src/ui/lib/electron-wheel-zoom.test.ts
new file mode 100644
index 00000000..2be6a5fa
--- /dev/null
+++ b/src/ui/lib/electron-wheel-zoom.test.ts
@@ -0,0 +1,39 @@
+import { afterEach, describe, expect, it } from "vitest";
+import { installElectronWheelZoomGuard } from "./electron-wheel-zoom";
+
+const win = window as unknown as Record;
+let cleanup: (() => void) | undefined;
+
+afterEach(() => {
+ cleanup?.();
+ cleanup = undefined;
+ delete win.IS_ELECTRON;
+ delete win.electronAPI;
+});
+
+function dispatchWheel(init?: WheelEventInit): WheelEvent {
+ const event = new WheelEvent("wheel", {
+ bubbles: true,
+ cancelable: true,
+ ...init,
+ });
+ window.dispatchEvent(event);
+ return event;
+}
+
+describe("installElectronWheelZoomGuard", () => {
+ it("does not block ordinary browser wheel events", () => {
+ cleanup = installElectronWheelZoomGuard();
+
+ expect(dispatchWheel({ ctrlKey: true }).defaultPrevented).toBe(false);
+ });
+
+ it("blocks modifier wheel zoom in Electron", () => {
+ win.electronAPI = { isElectron: true };
+ cleanup = installElectronWheelZoomGuard();
+
+ expect(dispatchWheel({ metaKey: true }).defaultPrevented).toBe(true);
+ expect(dispatchWheel({ ctrlKey: true }).defaultPrevented).toBe(true);
+ expect(dispatchWheel().defaultPrevented).toBe(false);
+ });
+});
diff --git a/src/ui/lib/electron-wheel-zoom.ts b/src/ui/lib/electron-wheel-zoom.ts
new file mode 100644
index 00000000..43374e2e
--- /dev/null
+++ b/src/ui/lib/electron-wheel-zoom.ts
@@ -0,0 +1,20 @@
+import { isElectron } from "./electron";
+
+export function installElectronWheelZoomGuard(): () => void {
+ if (!isElectron()) return () => {};
+
+ const preventModifierWheelZoom = (event: WheelEvent) => {
+ if (!event.metaKey && !event.ctrlKey) return;
+ event.preventDefault();
+ };
+
+ window.addEventListener("wheel", preventModifierWheelZoom, {
+ capture: true,
+ passive: false,
+ });
+
+ return () =>
+ window.removeEventListener("wheel", preventModifierWheelZoom, {
+ capture: true,
+ });
+}