diff --git a/desktop/src/app/useWebviewZoomShortcuts.ts b/desktop/src/app/useWebviewZoomShortcuts.ts index 162062f0d..5ac6dc56c 100644 --- a/desktop/src/app/useWebviewZoomShortcuts.ts +++ b/desktop/src/app/useWebviewZoomShortcuts.ts @@ -4,12 +4,18 @@ import { getCurrentWebview } from "@tauri-apps/api/webview"; import { hasPrimaryShortcutModifier } from "@/shared/lib/platform"; const DEFAULT_ZOOM_FACTOR = 1; -const MIN_ZOOM_FACTOR = 0.2; -const MAX_ZOOM_FACTOR = 10; -const ZOOM_STEP = 0.2; +const MIN_ZOOM_FACTOR = 0.75; +const MAX_ZOOM_FACTOR = 1.5; +const ZOOM_STEP = 0.1; +const BASE_FONT_SIZE_PX = 16; +const TEXT_SCALE_STORAGE_KEY = "sprout:text-scale"; type ZoomAction = "increase" | "decrease" | "reset"; +function roundZoomFactor(zoomFactor: number) { + return Math.round(zoomFactor * 10) / 10; +} + function getZoomAction(event: KeyboardEvent): ZoomAction | null { if (!hasPrimaryShortcutModifier(event) || event.altKey) { return null; @@ -49,10 +55,35 @@ function getNextZoomFactor(action: ZoomAction, zoomFactor: number) { } if (action === "increase") { - return Math.min(zoomFactor + ZOOM_STEP, MAX_ZOOM_FACTOR); + return Math.min(roundZoomFactor(zoomFactor + ZOOM_STEP), MAX_ZOOM_FACTOR); } - return Math.max(zoomFactor - ZOOM_STEP, MIN_ZOOM_FACTOR); + return Math.max(roundZoomFactor(zoomFactor - ZOOM_STEP), MIN_ZOOM_FACTOR); +} + +function readStoredZoomFactor() { + const raw = window.localStorage.getItem(TEXT_SCALE_STORAGE_KEY); + if (!raw) { + return DEFAULT_ZOOM_FACTOR; + } + + const parsed = Number.parseFloat(raw); + if (!Number.isFinite(parsed)) { + return DEFAULT_ZOOM_FACTOR; + } + + return Math.min(Math.max(parsed, MIN_ZOOM_FACTOR), MAX_ZOOM_FACTOR); +} + +function applyTextScale(zoomFactor: number) { + if (zoomFactor === DEFAULT_ZOOM_FACTOR) { + document.documentElement.style.fontSize = ""; + window.localStorage.removeItem(TEXT_SCALE_STORAGE_KEY); + return; + } + + document.documentElement.style.fontSize = `${BASE_FONT_SIZE_PX * zoomFactor}px`; + window.localStorage.setItem(TEXT_SCALE_STORAGE_KEY, String(zoomFactor)); } export function useWebviewZoomShortcuts() { @@ -60,6 +91,15 @@ export function useWebviewZoomShortcuts() { React.useLayoutEffect(() => { const webview = getCurrentWebview(); + const storedZoomFactor = readStoredZoomFactor(); + + zoomFactorRef.current = storedZoomFactor; + applyTextScale(storedZoomFactor); + + // Keep the webview coordinate system stable; only text should scale. + void webview.setZoom(DEFAULT_ZOOM_FACTOR).catch((error) => { + console.error("Failed to reset webview zoom", error); + }); function handleKeyDown(event: KeyboardEvent) { const action = getZoomAction(event); @@ -77,11 +117,7 @@ export function useWebviewZoomShortcuts() { } zoomFactorRef.current = nextZoomFactor; - - void webview.setZoom(nextZoomFactor).catch((error) => { - zoomFactorRef.current = previousZoomFactor; - console.error("Failed to update webview zoom", error); - }); + applyTextScale(nextZoomFactor); } window.addEventListener("keydown", handleKeyDown); diff --git a/desktop/src/features/chat/ui/ChatHeader.tsx b/desktop/src/features/chat/ui/ChatHeader.tsx index 01476324c..20b070ce6 100644 --- a/desktop/src/features/chat/ui/ChatHeader.tsx +++ b/desktop/src/features/chat/ui/ChatHeader.tsx @@ -27,7 +27,7 @@ type ChatHeaderProps = { statusBadge?: React.ReactNode; }; -const HEADER_ICON_CLASS = "h-3.5 w-3.5 text-muted-foreground"; +const HEADER_ICON_CLASS = "h-[14px] w-[14px] text-muted-foreground"; function ChannelIcon({ channelType, @@ -90,15 +90,15 @@ export function ChatHeader({ return (
-
+
-
+
diff --git a/desktop/src/shared/ui/sidebar.tsx b/desktop/src/shared/ui/sidebar.tsx index a40fe2868..83cd5a757 100644 --- a/desktop/src/shared/ui/sidebar.tsx +++ b/desktop/src/shared/ui/sidebar.tsx @@ -26,9 +26,9 @@ import { const SIDEBAR_COOKIE_NAME = "sidebar_state"; const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7; -const SIDEBAR_WIDTH = "16rem"; -const SIDEBAR_WIDTH_MOBILE = "18rem"; -const SIDEBAR_WIDTH_ICON = "3rem"; +const SIDEBAR_WIDTH = "256px"; +const SIDEBAR_WIDTH_MOBILE = "288px"; +const SIDEBAR_WIDTH_ICON = "48px"; const SIDEBAR_KEYBOARD_SHORTCUT = "s"; type SidebarContextProps = { @@ -237,7 +237,7 @@ const Sidebar = React.forwardRef< "group-data-[collapsible=offcanvas]:w-0", "group-data-[side=right]:rotate-180", variant === "floating" || variant === "inset" - ? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4))]" + ? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_16px)]" : "group-data-[collapsible=icon]:w-[--sidebar-width-icon]", )} /> @@ -249,7 +249,7 @@ const Sidebar = React.forwardRef< : "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]", // Adjust the padding for floating and inset variants. variant === "floating" || variant === "inset" - ? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4)_+2px)]" + ? "p-[8px] group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_18px)]" : "group-data-[collapsible=icon]:w-[--sidebar-width-icon]", className, )} diff --git a/desktop/tests/e2e/profile.spec.ts b/desktop/tests/e2e/profile.spec.ts index 59e74cedb..893a2e8d1 100644 --- a/desktop/tests/e2e/profile.spec.ts +++ b/desktop/tests/e2e/profile.spec.ts @@ -346,40 +346,67 @@ test("supports webview zoom keyboard shortcuts", async ({ page }) => { await page.goto("/"); await expect(page.getByTestId("chat-title")).toHaveText("Home"); - await page.keyboard.press( - process.platform === "darwin" ? "Meta+Shift+Equal" : "Control+Shift+Equal", - ); + const getTextScaleState = () => + page.evaluate(() => ({ + fontSize: getComputedStyle(document.documentElement).fontSize, + storedScale: localStorage.getItem("sprout:text-scale"), + webviewZoom: window.__SPROUT_E2E_WEBVIEW_ZOOM__, + })); + const dispatchPrimaryShortcut = ( + key: string, + code: string, + shiftKey = false, + ) => + page.evaluate( + ({ code, key, shiftKey }) => { + const isMac = /mac|iphone|ipad|ipod/i.test(navigator.platform); + window.dispatchEvent( + new KeyboardEvent("keydown", { + bubbles: true, + cancelable: true, + code, + ctrlKey: !isMac, + key, + metaKey: isMac, + shiftKey, + }), + ); + }, + { code, key, shiftKey }, + ); + + await dispatchPrimaryShortcut("+", "Equal", true); + + await expect.poll(getTextScaleState).toEqual({ + fontSize: "17.6px", + storedScale: "1.1", + webviewZoom: 1, + }); - await expect - .poll(() => page.evaluate(() => window.__SPROUT_E2E_WEBVIEW_ZOOM__)) - .toBe(1.2); + await dispatchPrimaryShortcut("-", "Minus"); - await page.keyboard.press( - process.platform === "darwin" ? "Meta+Minus" : "Control+Minus", - ); + await expect.poll(getTextScaleState).toEqual({ + fontSize: "16px", + storedScale: null, + webviewZoom: 1, + }); - await expect - .poll(() => page.evaluate(() => window.__SPROUT_E2E_WEBVIEW_ZOOM__)) - .toBe(1); + await dispatchPrimaryShortcut("+", "Equal", true); + await dispatchPrimaryShortcut("+", "Equal", true); - await page.keyboard.press( - process.platform === "darwin" ? "Meta+Shift+Equal" : "Control+Shift+Equal", - ); - await page.keyboard.press( - process.platform === "darwin" ? "Meta+Shift+Equal" : "Control+Shift+Equal", - ); - - await expect - .poll(() => page.evaluate(() => window.__SPROUT_E2E_WEBVIEW_ZOOM__)) - .toBe(1.4); + await expect.poll(getTextScaleState).toEqual({ + fontSize: "19.2px", + storedScale: "1.2", + webviewZoom: 1, + }); - await page.keyboard.press( - process.platform === "darwin" ? "Meta+Digit0" : "Control+Digit0", - ); + await dispatchPrimaryShortcut("0", "Digit0"); - await expect - .poll(() => page.evaluate(() => window.__SPROUT_E2E_WEBVIEW_ZOOM__)) - .toBe(1); + await expect.poll(getTextScaleState).toEqual({ + fontSize: "16px", + storedScale: null, + webviewZoom: 1, + }); }); test("shows doctor checks for local sprout tooling", async ({ page }) => {