fix(desktop): refine header scaling and shadow (#573)

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
thomaspblock
2026-05-14 10:07:15 -07:00
committed by GitHub
co-authored by Cursor
parent b74ec95b8e
commit 9e76a08a47
5 changed files with 113 additions and 50 deletions
+46 -10
View File
@@ -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);
+5 -5
View File
@@ -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 (
<header
className={cn(
"relative z-30 flex min-h-11 min-w-0 shrink-0 cursor-default select-none items-center gap-2.5 bg-background/70 py-1.5 pl-4 pr-2 shadow-[0_4px_24px_rgba(0,0,0,0.06)] backdrop-blur-xl transition-[margin,padding] duration-200 ease-linear supports-[backdrop-filter]:bg-background/55 dark:shadow-[0_4px_24px_rgba(0,0,0,0.25)] sm:pl-6 sm:pr-3",
overlaysContent && "-mb-11",
reserveGlobalControls && "md:pl-40",
"relative z-30 flex min-h-[44px] min-w-0 shrink-0 cursor-default select-none items-center gap-[10px] bg-background/70 py-[6px] pl-[16px] pr-[8px] backdrop-blur-xl transition-[margin,padding] duration-200 ease-linear supports-[backdrop-filter]:bg-background/55 sm:pl-[24px] sm:pr-[12px]",
overlaysContent && "-mb-[44px]",
reserveGlobalControls && "md:pl-[160px]",
)}
data-testid="chat-header"
data-tauri-drag-region
>
<div className="min-w-0 flex-1">
<div className="flex min-w-0 flex-wrap items-center gap-1">
<div className="flex min-w-0 flex-wrap items-center gap-[4px]">
<ChannelIcon
channelType={channelType}
mode={mode}
@@ -22,13 +22,13 @@ function LoadingHeaderSkeleton() {
return (
<header
className={cn(
"flex min-h-11 min-w-0 cursor-default select-none items-center gap-2.5 bg-background/70 py-1.5 pl-4 pr-2 shadow-[0_4px_24px_rgba(0,0,0,0.06)] backdrop-blur-xl transition-[padding] duration-200 ease-linear supports-[backdrop-filter]:bg-background/55 dark:shadow-[0_4px_24px_rgba(0,0,0,0.25)] sm:pl-6 sm:pr-3",
sidebarState === "collapsed" && "md:pl-40",
"flex min-h-[44px] min-w-0 cursor-default select-none items-center gap-[10px] bg-background/70 py-[6px] pl-[16px] pr-[8px] shadow-[0_4px_24px_rgba(0,0,0,0.06)] backdrop-blur-xl transition-[padding] duration-200 ease-linear supports-[backdrop-filter]:bg-background/55 dark:shadow-[0_4px_24px_rgba(0,0,0,0.25)] sm:pl-[24px] sm:pr-[12px]",
sidebarState === "collapsed" && "md:pl-[160px]",
)}
data-tauri-drag-region
>
<div className="min-w-0 flex-1">
<div className="flex min-w-0 items-center gap-1.5">
<div className="flex min-w-0 items-center gap-[6px]">
<Skeleton className="h-3.5 w-3.5 rounded-sm" />
<Skeleton className="h-4 w-28 max-w-[50vw]" />
</div>
+5 -5
View File
@@ -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,
)}
+54 -27
View File
@@ -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 expect
.poll(() => page.evaluate(() => window.__SPROUT_E2E_WEBVIEW_ZOOM__))
.toBe(1.2);
await dispatchPrimaryShortcut("+", "Equal", true);
await page.keyboard.press(
process.platform === "darwin" ? "Meta+Minus" : "Control+Minus",
);
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);
await dispatchPrimaryShortcut("-", "Minus");
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(getTextScaleState).toEqual({
fontSize: "16px",
storedScale: null,
webviewZoom: 1,
});
await expect
.poll(() => page.evaluate(() => window.__SPROUT_E2E_WEBVIEW_ZOOM__))
.toBe(1.4);
await dispatchPrimaryShortcut("+", "Equal", true);
await dispatchPrimaryShortcut("+", "Equal", true);
await page.keyboard.press(
process.platform === "darwin" ? "Meta+Digit0" : "Control+Digit0",
);
await expect.poll(getTextScaleState).toEqual({
fontSize: "19.2px",
storedScale: "1.2",
webviewZoom: 1,
});
await expect
.poll(() => page.evaluate(() => window.__SPROUT_E2E_WEBVIEW_ZOOM__))
.toBe(1);
await dispatchPrimaryShortcut("0", "Digit0");
await expect.poll(getTextScaleState).toEqual({
fontSize: "16px",
storedScale: null,
webviewZoom: 1,
});
});
test("shows doctor checks for local sprout tooling", async ({ page }) => {