From ae5348b330c2073d00439a396ec55bff85932230 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Sun, 31 May 2026 05:24:08 -0700 Subject: [PATCH] perf: fold pet size clamp into resize effect (#4129) --- .../src/components/pet/PetOverlay.test.ts | 23 +++++++++++ .../src/components/pet/PetOverlay.tsx | 40 +++++++++++-------- 2 files changed, 47 insertions(+), 16 deletions(-) create mode 100644 src/renderer/src/components/pet/PetOverlay.test.ts diff --git a/src/renderer/src/components/pet/PetOverlay.test.ts b/src/renderer/src/components/pet/PetOverlay.test.ts new file mode 100644 index 00000000000..87260e3e609 --- /dev/null +++ b/src/renderer/src/components/pet/PetOverlay.test.ts @@ -0,0 +1,23 @@ +import { describe, expect, it } from 'vitest' + +import { clampPositionToViewport } from './PetOverlay' + +describe('clampPositionToViewport', () => { + it('keeps positions inside the viewport minus pet size', () => { + expect( + clampPositionToViewport({ x: 480, y: 390 }, 120, { + width: 500, + height: 400 + }) + ).toEqual({ x: 380, y: 280 }) + }) + + it('clamps negative positions to the origin', () => { + expect( + clampPositionToViewport({ x: -25, y: -40 }, 120, { + width: 500, + height: 400 + }) + ).toEqual({ x: 0, y: 0 }) + }) +}) diff --git a/src/renderer/src/components/pet/PetOverlay.tsx b/src/renderer/src/components/pet/PetOverlay.tsx index a3fc537c383..56358171fb6 100644 --- a/src/renderer/src/components/pet/PetOverlay.tsx +++ b/src/renderer/src/components/pet/PetOverlay.tsx @@ -183,18 +183,29 @@ const SIZE = 180 const POSITION_STORAGE_KEY = 'pet-overlay-position' const LEGACY_POSITION_STORAGE_KEY = 'sidekick-overlay-position' -type Position = { x: number; y: number } +export type Position = { x: number; y: number } + +export function clampPositionToViewport( + pos: Position, + size: number, + viewport: { width: number; height: number } +): Position { + const maxX = Math.max(0, viewport.width - size) + const maxY = Math.max(0, viewport.height - size) + return { + x: Math.min(Math.max(0, pos.x), maxX), + y: Math.min(Math.max(0, pos.y), maxY) + } +} function clampToViewport(pos: Position, size: number = SIZE): Position { if (typeof window === 'undefined') { return pos } - const maxX = Math.max(0, window.innerWidth - size) - const maxY = Math.max(0, window.innerHeight - size) - return { - x: Math.min(Math.max(0, pos.x), maxX), - y: Math.min(Math.max(0, pos.y), maxY) - } + return clampPositionToViewport(pos, size, { + width: window.innerWidth, + height: window.innerHeight + }) } function loadStoredPosition(size: number = SIZE): Position | null { @@ -262,15 +273,12 @@ export function PetOverlay(): React.JSX.Element { const dragOffsetRef = useRef({ x: 0, y: 0 }) useEffect(() => { - const onResize = (): void => setPosition((prev) => clampToViewport(prev, size)) - window.addEventListener('resize', onResize) - return () => window.removeEventListener('resize', onResize) - }, [size]) - - // Why: when the user shrinks/grows the overlay, re-clamp so the box never - // overflows the viewport edges (which would otherwise leave it un-draggable). - useEffect(() => { - setPosition((prev) => clampToViewport(prev, size)) + const clampPosition = (): void => setPosition((prev) => clampToViewport(prev, size)) + // Why: resizing the pet or the window uses the same viewport boundary; keep + // that clamp in one Effect so size changes do not need a second render pass. + clampPosition() + window.addEventListener('resize', clampPosition) + return () => window.removeEventListener('resize', clampPosition) }, [size]) useEffect(() => {