Add settings: theme, zoom controls, keyboard shortcuts (step 8)

Theme (light/dark/system) persisted via Zustand's persist middleware
to localStorage, applied by toggling .dark on <html> — the CSS custom
variant was already wired up from step 1 but nothing had driven it
until now, so the app was silently stuck in light mode regardless of
OS appearance. "System" resolves via matchMedia and stays live via a
change listener. Zoom in/out/reset go through a small main-process IPC
(webContents.setZoomLevel) since the renderer can't reach Electron's
webFrame directly with contextIsolation on. Keyboard shortcuts
(Cmd/Ctrl+N, +comma, +=/-/0) are a single window-level listener
mounted once at the app root.

Verified end-to-end via CDP: confirmed system theme correctly resolved
dark to match the real OS appearance at test time, manually switching
to Light actually removed the .dark class and persisted to
localStorage, zoom in/reset both confirmed via the IPC's returned zoom
level (cumulative across calls), and both keyboard shortcuts
(dispatched as real KeyboardEvents) correctly opened their modals.

Also added a matchMedia polyfill to the Vitest setup file — jsdom
doesn't implement it, which broke App.test.tsx once useAppliedTheme
started running on every render.
This commit is contained in:
R3ap3R
2026-08-03 00:26:15 -05:00
parent 541d719680
commit 0820c2f3d8
9 changed files with 277 additions and 10 deletions
+2
View File
@@ -8,6 +8,7 @@ import { registerDatabaseIpc } from './ipc/database'
import { registerAddonsIpc } from './ipc/addons'
import { registerLogsIpc } from './ipc/logs'
import { registerCreateIpc } from './ipc/create'
import { registerWindowIpc } from './ipc/window'
import { killAllRunningCommands } from './commandRunner'
function createWindow(): void {
@@ -63,6 +64,7 @@ app.whenReady().then(() => {
registerAddonsIpc()
registerLogsIpc()
registerCreateIpc()
registerWindowIpc()
createWindow()
+23
View File
@@ -0,0 +1,23 @@
import { ipcMain } from 'electron'
const MIN_ZOOM = -3
const MAX_ZOOM = 5
export function registerWindowIpc(): void {
ipcMain.handle('window:zoomIn', (event) => {
const level = Math.min(MAX_ZOOM, event.sender.getZoomLevel() + 0.5)
event.sender.setZoomLevel(level)
return level
})
ipcMain.handle('window:zoomOut', (event) => {
const level = Math.max(MIN_ZOOM, event.sender.getZoomLevel() - 0.5)
event.sender.setZoomLevel(level)
return level
})
ipcMain.handle('window:zoomReset', (event) => {
event.sender.setZoomLevel(0)
return 0
})
}