From e80af6b8e52ce07db4f7f7919a99d18be97cdd4b Mon Sep 17 00:00:00 2001 From: Anastasiia Solop <35258279+anastasiya1155@users.noreply.github.com> Date: Thu, 15 Jan 2026 11:14:02 +0100 Subject: [PATCH] Fix mobile preview scaling to fit container (Vibe Kanban) (#2057) * The fix is in place. Let me summarize the changes made: ## Summary Fixed the mobile preview browser by implementing automatic scaling when the phone frame doesn't fit in the container. ### Changes made: **`frontend/src/components/ui-new/views/PreviewBrowser.tsx`:** 1. Exported constants `MOBILE_WIDTH`, `MOBILE_HEIGHT`, and `PHONE_FRAME_PADDING` for use in the container 2. Added `mobileScale` prop to the component interface 3. Changed the content area's overflow from always `overflow-auto` to `overflow-hidden` when in mobile mode (to prevent scrollbars when the scaled content is smaller) 4. Applied CSS `transform: scale()` to the phone frame when `mobileScale < 1`, using `origin-center` to scale from the center **`frontend/src/components/ui-new/containers/PreviewBrowserContainer.tsx`:** 1. Imported `useLayoutEffect` and the exported constants from `PreviewBrowser` 2. Added `mobileScale` state that calculates the appropriate scale factor 3. Added a `useLayoutEffect` that: - Only runs when in mobile mode - Uses `ResizeObserver` to track container size changes - Calculates scale based on available space vs. phone frame dimensions - Never scales up (only down when needed) 4. Passes `mobileScale` prop to the `PreviewBrowser` component The mobile preview will now automatically scale down to fit within the available container space, preventing the vertical cutoff issue. * Cleanup script changes for workspace abe7b640-b3ea-4716-850e-6fd096ac1a3e --- .../containers/PreviewBrowserContainer.tsx | 54 ++++++++++++++++++- .../ui-new/views/PreviewBrowser.tsx | 27 +++++++--- 2 files changed, 71 insertions(+), 10 deletions(-) diff --git a/frontend/src/components/ui-new/containers/PreviewBrowserContainer.tsx b/frontend/src/components/ui-new/containers/PreviewBrowserContainer.tsx index 14c6ff52..8f0ca66a 100644 --- a/frontend/src/components/ui-new/containers/PreviewBrowserContainer.tsx +++ b/frontend/src/components/ui-new/containers/PreviewBrowserContainer.tsx @@ -1,5 +1,16 @@ -import { useCallback, useState, useEffect, useRef } from 'react'; -import { PreviewBrowser } from '../views/PreviewBrowser'; +import { + useCallback, + useState, + useEffect, + useLayoutEffect, + useRef, +} from 'react'; +import { + PreviewBrowser, + MOBILE_WIDTH, + MOBILE_HEIGHT, + PHONE_FRAME_PADDING, +} from '../views/PreviewBrowser'; import { usePreviewDevServer } from '../hooks/usePreviewDevServer'; import { usePreviewUrl } from '../hooks/usePreviewUrl'; import { @@ -108,6 +119,44 @@ export function PreviewBrowserContainer({ } }, [responsiveDimensions]); + // Calculate scale for mobile preview to fit container + const [mobileScale, setMobileScale] = useState(1); + + useLayoutEffect(() => { + if (screenSize !== 'mobile' || !containerRef.current) { + setMobileScale(1); + return; + } + + const updateScale = () => { + const container = containerRef.current; + if (!container) return; + + // Get available space (subtract padding from p-double which is typically 32px total) + const availableWidth = container.clientWidth - 32; + const availableHeight = container.clientHeight - 32; + + // Total phone frame dimensions including padding + const totalFrameWidth = MOBILE_WIDTH + PHONE_FRAME_PADDING; + const totalFrameHeight = MOBILE_HEIGHT + PHONE_FRAME_PADDING; + + // Calculate scale needed to fit + const scaleX = availableWidth / totalFrameWidth; + const scaleY = availableHeight / totalFrameHeight; + const scale = Math.min(scaleX, scaleY, 1); // Don't scale up, only down + + setMobileScale(scale); + }; + + updateScale(); + + // Observe container size changes + const resizeObserver = new ResizeObserver(updateScale); + resizeObserver.observe(containerRef.current); + + return () => resizeObserver.disconnect(); + }, [screenSize]); + // Handle resize events - register listeners once on mount useEffect(() => { const handleMove = (clientX: number, clientY: number) => { @@ -297,6 +346,7 @@ export function PreviewBrowserContainer({ attemptId && repos.length > 0 ? handleFixDevScript : undefined } hasFailedDevServer={hasFailedDevServer} + mobileScale={mobileScale} className={className} /> ); diff --git a/frontend/src/components/ui-new/views/PreviewBrowser.tsx b/frontend/src/components/ui-new/views/PreviewBrowser.tsx index d74492d3..2b8dcdf7 100644 --- a/frontend/src/components/ui-new/views/PreviewBrowser.tsx +++ b/frontend/src/components/ui-new/views/PreviewBrowser.tsx @@ -25,8 +25,10 @@ import type { ResponsiveDimensions, } from '@/hooks/usePreviewSettings'; -const MOBILE_WIDTH = 390; -const MOBILE_HEIGHT = 844; +export const MOBILE_WIDTH = 390; +export const MOBILE_HEIGHT = 844; +// Phone frame adds padding (p-3 = 12px * 2) and rounded corners +export const PHONE_FRAME_PADDING = 24; interface PreviewBrowserProps { url?: string; @@ -56,6 +58,7 @@ interface PreviewBrowserProps { handleEditDevScript: () => void; handleFixDevScript?: () => void; hasFailedDevServer?: boolean; + mobileScale: number; className?: string; } @@ -85,6 +88,7 @@ export function PreviewBrowser({ handleEditDevScript, handleFixDevScript, hasFailedDevServer, + mobileScale, className, }: PreviewBrowserProps) { const { t } = useTranslation(['tasks', 'common']); @@ -260,20 +264,27 @@ export function PreviewBrowser({ {/* Content area */}