From cbf247a4efb168ea5b906cc60bd7feefbe76b56d Mon Sep 17 00:00:00 2001 From: SlayTheDragons <138079274+SlayTheDragons@users.noreply.github.com> Date: Tue, 11 Nov 2025 12:49:25 +1300 Subject: [PATCH] feat (frontend): Text size accessibility settings (As requested in issue #1482) (#1624) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Screenshot 2025-10-24 200100 This PR adds settings for text size, small, medium (default), large and extra large. This should help more people use Dyad more productively. Closes #1482 --- ## Summary by cubic Adds adjustable workspace zoom levels (90%, 100%, 110%, 125%, 150%) to improve readability and accessibility. Changes apply instantly and persist per user, addressing issue #1482. - **New Features** - Zoom selector in Settings → General. - Persists zoomLevel in user settings and schema. - Applies zoom via Electron webFrame for consistent app scaling. - **Refactors** - Removed deprecated workspaceTextSize setting. - Removed root font-size from globals.css. Written for commit 54bad25f016a83e9f414025a07e80fdbad859366. Summary will update automatically on new commits. --- > [!NOTE] > Adds adjustable app zoom levels (90–150%) with a Settings selector, persisted in user settings, and applied via Electron webFrame. > > - **UI/Settings**: > - Add `ZoomSelector` component and surface it in `Settings → General` (`src/components/ZoomSelector.tsx`, `src/pages/settings.tsx`). > - **Schema/Persistence**: > - Introduce `ZoomLevelSchema` and optional `zoomLevel` in `UserSettingsSchema` (`src/lib/schemas.ts`). > - **Electron Integration**: > - Expose `webFrame.setZoomFactor/getZoomFactor` in `window.electron` (`src/preload.ts`). > - **App Behavior**: > - Apply zoom factor on load and when settings change with default `100%` (`src/app/layout.tsx`). > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 54bad25f016a83e9f414025a07e80fdbad859366. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). --------- Co-authored-by: Devforschool <138079274+Devforschool@users.noreply.github.com> Co-authored-by: Will Chen --- src/app/layout.tsx | 38 ++++++++++++++--- src/components/ZoomSelector.tsx | 72 +++++++++++++++++++++++++++++++++ src/lib/schemas.ts | 4 ++ src/pages/settings.tsx | 5 +++ src/preload.ts | 8 +++- src/styles/globals.css | 1 - 6 files changed, 120 insertions(+), 8 deletions(-) create mode 100644 src/components/ZoomSelector.tsx 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); */