diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 2d2f372..e280c9a 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -4,18 +4,44 @@ import { ThemeProvider } from "../contexts/ThemeContext"; import { DeepLinkProvider } from "../contexts/DeepLinkContext"; import { Toaster } from "sonner"; import { TitleBar } from "./TitleBar"; -import { useEffect } from "react"; +import { useEffect, type ReactNode } from "react"; import { useRunApp } from "@/hooks/useRunApp"; import { useAtomValue } from "jotai"; import { previewModeAtom } from "@/atoms/appAtoms"; +import { useSettings } from "@/hooks/useSettings"; +import type { ZoomLevel } from "@/lib/schemas"; -export default function RootLayout({ - children, -}: { - children: React.ReactNode; -}) { +const DEFAULT_ZOOM_LEVEL: ZoomLevel = "100"; + +export default function RootLayout({ children }: { children: ReactNode }) { const { refreshAppIframe } = useRunApp(); const previewMode = useAtomValue(previewModeAtom); + const { settings } = useSettings(); + + useEffect(() => { + const zoomLevel = settings?.zoomLevel ?? DEFAULT_ZOOM_LEVEL; + const zoomFactor = Number(zoomLevel) / 100; + + const electronApi = ( + window as Window & { + electron?: { + webFrame?: { + setZoomFactor: (factor: number) => void; + }; + }; + } + ).electron; + + if (electronApi?.webFrame?.setZoomFactor) { + electronApi.webFrame.setZoomFactor(zoomFactor); + + return () => { + electronApi.webFrame?.setZoomFactor(Number(DEFAULT_ZOOM_LEVEL) / 100); + }; + } + + return () => {}; + }, [settings?.zoomLevel]); // Global keyboard listener for refresh events useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { diff --git a/src/components/ZoomSelector.tsx b/src/components/ZoomSelector.tsx new file mode 100644 index 0000000..2a8c8be --- /dev/null +++ b/src/components/ZoomSelector.tsx @@ -0,0 +1,72 @@ +import { useMemo } from "react"; +import { useSettings } from "@/hooks/useSettings"; +import { ZoomLevel, ZoomLevelSchema } from "@/lib/schemas"; +import { Label } from "@/components/ui/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; + +const ZOOM_LEVEL_LABELS: Record = { + "90": "90%", + "100": "100%", + "110": "110%", + "125": "125%", + "150": "150%", +}; + +const ZOOM_LEVEL_DESCRIPTIONS: Record = { + "90": "Slightly zoomed out to fit more content on screen.", + "100": "Default zoom level.", + "110": "Zoom in a little for easier reading.", + "125": "Large zoom for improved readability.", + "150": "Maximum zoom for maximum accessibility.", +}; + +const DEFAULT_ZOOM_LEVEL: ZoomLevel = "100"; + +export function ZoomSelector() { + const { settings, updateSettings } = useSettings(); + const currentZoomLevel: ZoomLevel = useMemo(() => { + const value = settings?.zoomLevel ?? DEFAULT_ZOOM_LEVEL; + return ZoomLevelSchema.safeParse(value).success + ? (value as ZoomLevel) + : DEFAULT_ZOOM_LEVEL; + }, [settings?.zoomLevel]); + + return ( +
+
+ +

+ Adjusts the zoom level to make content easier to read. +

+
+ +
+ ); +} diff --git a/src/lib/schemas.ts b/src/lib/schemas.ts index cb25fe1..e69361f 100644 --- a/src/lib/schemas.ts +++ b/src/lib/schemas.ts @@ -211,6 +211,9 @@ export type ContextPathResults = { export const ReleaseChannelSchema = z.enum(["stable", "beta"]); export type ReleaseChannel = z.infer; +export const ZoomLevelSchema = z.enum(["90", "100", "110", "125", "150"]); +export type ZoomLevel = z.infer; + /** * Zod schema for user settings */ @@ -242,6 +245,7 @@ export const UserSettingsSchema = z.object({ enableSupabaseWriteSqlMigration: z.boolean().optional(), selectedChatMode: ChatModeSchema.optional(), acceptedCommunityCode: z.boolean().optional(), + zoomLevel: ZoomLevelSchema.optional(), enableAutoFixProblems: z.boolean().optional(), enableNativeGit: z.boolean().optional(), diff --git a/src/pages/settings.tsx b/src/pages/settings.tsx index 50a9f37..6cd6c09 100644 --- a/src/pages/settings.tsx +++ b/src/pages/settings.tsx @@ -26,6 +26,7 @@ import { NeonIntegration } from "@/components/NeonIntegration"; import { RuntimeModeSelector } from "@/components/RuntimeModeSelector"; import { NodePathSelector } from "@/components/NodePathSelector"; import { ToolsMcpSettings } from "@/components/settings/ToolsMcpSettings"; +import { ZoomSelector } from "@/components/ZoomSelector"; import { useSetAtom } from "jotai"; import { activeSettingsSectionAtom } from "@/atoms/viewAtoms"; @@ -265,6 +266,10 @@ export function GeneralSettings({ appVersion }: { appVersion: string | null }) { +
+ +
+
diff --git a/src/preload.ts b/src/preload.ts index b6c78b1..9e931c7 100644 --- a/src/preload.ts +++ b/src/preload.ts @@ -1,7 +1,7 @@ // See the Electron documentation for details on how to use preload scripts: // https://www.electronjs.org/docs/latest/tutorial/process-model#preload-scripts -import { contextBridge, ipcRenderer } from "electron"; +import { contextBridge, ipcRenderer, webFrame } from "electron"; // Whitelist of valid channels const validInvokeChannels = [ @@ -201,4 +201,10 @@ contextBridge.exposeInMainWorld("electron", { } }, }, + webFrame: { + setZoomFactor: (factor: number) => { + webFrame.setZoomFactor(factor); + }, + getZoomFactor: () => webFrame.getZoomFactor(), + }, }); diff --git a/src/styles/globals.css b/src/styles/globals.css index 651ca39..3c3dd2e 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -73,7 +73,6 @@ } :root { --docs-bg: #f5f5f5; - --default-font-family: "Geist", sans-serif; --default-mono-font-family: "Geist Mono", monospace; /* --background: oklch(1 0 0); */