From 0820c2f3d8acf668c4ef2974ff53af1ee92365e2 Mon Sep 17 00:00:00 2001 From: R3ap3R Date: Mon, 3 Aug 2026 00:26:15 -0500 Subject: [PATCH] Add settings: theme, zoom controls, keyboard shortcuts (step 8) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Theme (light/dark/system) persisted via Zustand's persist middleware to localStorage, applied by toggling .dark on — 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. --- src/main/index.ts | 2 + src/main/ipc/window.ts | 23 ++++ src/preload/index.ts | 5 + src/renderer/src/App.tsx | 40 +++++-- .../src/components/settings/SettingsModal.tsx | 109 ++++++++++++++++++ src/renderer/src/hooks/useAppliedTheme.ts | 27 +++++ .../src/hooks/useKeyboardShortcuts.ts | 44 +++++++ src/renderer/src/stores/themeStore.ts | 21 ++++ src/test/setup.ts | 16 +++ 9 files changed, 277 insertions(+), 10 deletions(-) create mode 100644 src/main/ipc/window.ts create mode 100644 src/renderer/src/components/settings/SettingsModal.tsx create mode 100644 src/renderer/src/hooks/useAppliedTheme.ts create mode 100644 src/renderer/src/hooks/useKeyboardShortcuts.ts create mode 100644 src/renderer/src/stores/themeStore.ts diff --git a/src/main/index.ts b/src/main/index.ts index 6a5513a..a17ff7e 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -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() diff --git a/src/main/ipc/window.ts b/src/main/ipc/window.ts new file mode 100644 index 0000000..5c4af33 --- /dev/null +++ b/src/main/ipc/window.ts @@ -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 + }) +} diff --git a/src/preload/index.ts b/src/preload/index.ts index 693022a..3c3d3d3 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -97,6 +97,11 @@ const api = { projectType, docroot ) + }, + zoom: { + in: (): Promise => ipcRenderer.invoke('window:zoomIn'), + out: (): Promise => ipcRenderer.invoke('window:zoomOut'), + reset: (): Promise => ipcRenderer.invoke('window:zoomReset') } } diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 1a7fb5c..9bb1d0a 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -1,18 +1,27 @@ -import { useState } from 'react' -import { Plus } from 'lucide-react' +import { useCallback, useState } from 'react' +import { Plus, Settings } from 'lucide-react' import { ProjectDetail } from './components/projects/ProjectDetail' import { ProjectList } from './components/projects/ProjectList' import { TerminalPanel } from './components/terminal/TerminalPanel' import { StatusBar } from './components/layout/StatusBar' import { Toaster } from './components/ui/Toaster' import { CreateProjectModal } from './components/create/CreateProjectModal' +import { SettingsModal } from './components/settings/SettingsModal' import { useAppStore } from './stores/appStore' import { useTerminalEvents } from './hooks/useTerminalEvents' +import { useAppliedTheme } from './hooks/useAppliedTheme' +import { useKeyboardShortcuts } from './hooks/useKeyboardShortcuts' function App(): React.JSX.Element { const selectedProjectName = useAppStore((s) => s.selectedProjectName) const [isCreateOpen, setIsCreateOpen] = useState(false) + const [isSettingsOpen, setIsSettingsOpen] = useState(false) useTerminalEvents() + useAppliedTheme() + useKeyboardShortcuts({ + onNewProject: useCallback(() => setIsCreateOpen(true), []), + onOpenSettings: useCallback(() => setIsSettingsOpen(true), []) + }) return (
@@ -20,14 +29,24 @@ function App(): React.JSX.Element {