diff --git a/frontend/src/components/Runtime/RuntimeControls.tsx b/frontend/src/components/Runtime/RuntimeControls.tsx index 82f9736..5bba22c 100644 --- a/frontend/src/components/Runtime/RuntimeControls.tsx +++ b/frontend/src/components/Runtime/RuntimeControls.tsx @@ -34,6 +34,10 @@ interface RuntimeControlsProps { pages?: PreloadPage[]; isFullscreen: boolean; toggleFullscreen: () => void; + /** Canvas width in pixels (for positioning relative to canvas) */ + canvasWidth?: number; + /** Canvas height in pixels (for positioning relative to canvas) */ + canvasHeight?: number; } /** @@ -381,12 +385,31 @@ function useCounterZoom() { return scale; } +/** + * Hook to get viewport dimensions + */ +function useViewportSize() { + const [size, setSize] = useState({ width: 0, height: 0 }); + + useEffect(() => { + const update = () => { + setSize({ width: window.innerWidth, height: window.innerHeight }); + }; + update(); + window.addEventListener('resize', update); + return () => window.removeEventListener('resize', update); + }, []); + + return size; +} + /** * RuntimeControls - Main component for presentation controls * * Renders offline toggle and fullscreen button using fixed pixel values * to maintain usable size regardless of canvas scaling. * Uses visualViewport API to counter pinch-zoom scaling on mobile. + * Positions controls relative to the canvas area (not viewport edges). */ export default function RuntimeControls({ projectId, @@ -395,14 +418,28 @@ export default function RuntimeControls({ pages, isFullscreen, toggleFullscreen, + canvasWidth = 0, + canvasHeight = 0, }: RuntimeControlsProps) { // Counter-scale to resist pinch-zoom const counterScale = useCounterZoom(); + const viewport = useViewportSize(); + + // Calculate position relative to centered canvas + // Canvas is centered with: left: 50%, top: 50%, transform: translate(-50%, -50%) + // So we offset from viewport edge by (viewport - canvas) / 2 + padding + const padding = 16; + const rightOffset = canvasWidth > 0 && viewport.width > 0 + ? (viewport.width - canvasWidth) / 2 + padding + : padding; + const topOffset = canvasHeight > 0 && viewport.height > 0 + ? (viewport.height - canvasHeight) / 2 + padding + : padding; const containerStyle: CSSProperties = { position: 'fixed', - top: 16, - right: 16, + top: topOffset, + right: rightOffset, display: 'flex', alignItems: 'center', gap: 8, diff --git a/frontend/src/components/RuntimePresentation.tsx b/frontend/src/components/RuntimePresentation.tsx index f2b2ba1..14d4d48 100644 --- a/frontend/src/components/RuntimePresentation.tsx +++ b/frontend/src/components/RuntimePresentation.tsx @@ -92,7 +92,7 @@ export default function RuntimePresentation({ // Canvas scale for responsive UI elements and letterbox mode // Uses page's design dimensions (saved at constructor save time) for presentation isolation - const { cssVars, letterboxStyles, isPortrait, showRotatePrompt } = + const { cssVars, letterboxStyles, isPortrait, showRotatePrompt, canvasWidth, canvasHeight } = useCanvasScale({ designWidth: currentPage?.design_width ?? undefined, designHeight: currentPage?.design_height ?? undefined, @@ -774,6 +774,8 @@ export default function RuntimePresentation({ pages={pages} isFullscreen={isFullscreen} toggleFullscreen={toggleFullscreen} + canvasWidth={canvasWidth} + canvasHeight={canvasHeight} /> {/* Toast notifications for offline download status */}