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
This commit is contained in:
Anastasiia Solop
2026-01-15 11:14:02 +01:00
committed by GitHub
parent ea5954c8f5
commit e80af6b8e5
2 changed files with 71 additions and 10 deletions

View File

@@ -1,5 +1,16 @@
import { useCallback, useState, useEffect, useRef } from 'react'; import {
import { PreviewBrowser } from '../views/PreviewBrowser'; 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 { usePreviewDevServer } from '../hooks/usePreviewDevServer';
import { usePreviewUrl } from '../hooks/usePreviewUrl'; import { usePreviewUrl } from '../hooks/usePreviewUrl';
import { import {
@@ -108,6 +119,44 @@ export function PreviewBrowserContainer({
} }
}, [responsiveDimensions]); }, [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 // Handle resize events - register listeners once on mount
useEffect(() => { useEffect(() => {
const handleMove = (clientX: number, clientY: number) => { const handleMove = (clientX: number, clientY: number) => {
@@ -297,6 +346,7 @@ export function PreviewBrowserContainer({
attemptId && repos.length > 0 ? handleFixDevScript : undefined attemptId && repos.length > 0 ? handleFixDevScript : undefined
} }
hasFailedDevServer={hasFailedDevServer} hasFailedDevServer={hasFailedDevServer}
mobileScale={mobileScale}
className={className} className={className}
/> />
); );

View File

@@ -25,8 +25,10 @@ import type {
ResponsiveDimensions, ResponsiveDimensions,
} from '@/hooks/usePreviewSettings'; } from '@/hooks/usePreviewSettings';
const MOBILE_WIDTH = 390; export const MOBILE_WIDTH = 390;
const MOBILE_HEIGHT = 844; export const MOBILE_HEIGHT = 844;
// Phone frame adds padding (p-3 = 12px * 2) and rounded corners
export const PHONE_FRAME_PADDING = 24;
interface PreviewBrowserProps { interface PreviewBrowserProps {
url?: string; url?: string;
@@ -56,6 +58,7 @@ interface PreviewBrowserProps {
handleEditDevScript: () => void; handleEditDevScript: () => void;
handleFixDevScript?: () => void; handleFixDevScript?: () => void;
hasFailedDevServer?: boolean; hasFailedDevServer?: boolean;
mobileScale: number;
className?: string; className?: string;
} }
@@ -85,6 +88,7 @@ export function PreviewBrowser({
handleEditDevScript, handleEditDevScript,
handleFixDevScript, handleFixDevScript,
hasFailedDevServer, hasFailedDevServer,
mobileScale,
className, className,
}: PreviewBrowserProps) { }: PreviewBrowserProps) {
const { t } = useTranslation(['tasks', 'common']); const { t } = useTranslation(['tasks', 'common']);
@@ -260,20 +264,27 @@ export function PreviewBrowser({
{/* Content area */} {/* Content area */}
<div <div
ref={containerRef} ref={containerRef}
className="flex-1 min-h-0 relative overflow-auto px-double pb-double" className={cn(
'flex-1 min-h-0 relative px-double pb-double',
screenSize === 'mobile' ? 'overflow-hidden' : 'overflow-auto'
)}
> >
{showIframe ? ( {showIframe ? (
<div <div
className={cn( className={cn(
'h-full', 'h-full',
screenSize === 'desktop' screenSize === 'desktop' ? '' : 'flex items-center justify-center'
? ''
: 'flex items-center justify-center p-double'
)} )}
> >
{screenSize === 'mobile' ? ( {screenSize === 'mobile' ? (
// Phone frame for mobile mode // Phone frame for mobile mode - scales down to fit container
<div className="bg-primary rounded-[2rem] p-3 shadow-xl"> <div
className="bg-primary rounded-[2rem] p-3 shadow-xl origin-center"
style={{
transform:
mobileScale < 1 ? `scale(${mobileScale})` : undefined,
}}
>
<div <div
className="rounded-[1.5rem] overflow-hidden" className="rounded-[1.5rem] overflow-hidden"
style={{ width: MOBILE_WIDTH, height: MOBILE_HEIGHT }} style={{ width: MOBILE_WIDTH, height: MOBILE_HEIGHT }}