<img width="496" height="470" alt="Screenshot 2025-10-24 200100" src="https://github.com/user-attachments/assets/e64ee081-dc08-4b54-94a6-9ed41453cfcf" /> 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 <!-- This is an auto-generated description by cubic. --> --- ## 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. <sup>Written for commit 54bad25f016a83e9f414025a07e80fdbad859366. Summary will update automatically on new commits.</sup> <!-- End of auto-generated description by cubic. --> <!-- CURSOR_SUMMARY --> --- > [!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`). > > <sup>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).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Devforschool <138079274+Devforschool@users.noreply.github.com> Co-authored-by: Will Chen <willchen90@gmail.com>
This commit is contained in:
@@ -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) => {
|
||||
|
||||
72
src/components/ZoomSelector.tsx
Normal file
72
src/components/ZoomSelector.tsx
Normal file
@@ -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<ZoomLevel, string> = {
|
||||
"90": "90%",
|
||||
"100": "100%",
|
||||
"110": "110%",
|
||||
"125": "125%",
|
||||
"150": "150%",
|
||||
};
|
||||
|
||||
const ZOOM_LEVEL_DESCRIPTIONS: Record<ZoomLevel, string> = {
|
||||
"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 (
|
||||
<div className="space-y-2">
|
||||
<div className="flex flex-col gap-1">
|
||||
<Label htmlFor="zoom-level">Zoom level</Label>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Adjusts the zoom level to make content easier to read.
|
||||
</p>
|
||||
</div>
|
||||
<Select
|
||||
value={currentZoomLevel}
|
||||
onValueChange={(value) =>
|
||||
updateSettings({ zoomLevel: value as ZoomLevel })
|
||||
}
|
||||
>
|
||||
<SelectTrigger id="zoom-level" className="w-[220px]">
|
||||
<SelectValue placeholder="Select zoom level" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{Object.entries(ZOOM_LEVEL_LABELS).map(([value, label]) => (
|
||||
<SelectItem key={value} value={value}>
|
||||
<div className="flex flex-col text-left">
|
||||
<span>{label}</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{ZOOM_LEVEL_DESCRIPTIONS[value as ZoomLevel]}
|
||||
</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -211,6 +211,9 @@ export type ContextPathResults = {
|
||||
export const ReleaseChannelSchema = z.enum(["stable", "beta"]);
|
||||
export type ReleaseChannel = z.infer<typeof ReleaseChannelSchema>;
|
||||
|
||||
export const ZoomLevelSchema = z.enum(["90", "100", "110", "125", "150"]);
|
||||
export type ZoomLevel = z.infer<typeof ZoomLevelSchema>;
|
||||
|
||||
/**
|
||||
* 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(),
|
||||
|
||||
@@ -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 }) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4">
|
||||
<ZoomSelector />
|
||||
</div>
|
||||
|
||||
<div className="space-y-1 mt-4">
|
||||
<AutoUpdateSwitch />
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">
|
||||
|
||||
@@ -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(),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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); */
|
||||
|
||||
Reference in New Issue
Block a user