mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
fix(desktop): refine header scaling and shadow (#573)
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
)}
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
Reference in New Issue
Block a user