11"use client" ;
22
3- import { useRef , useState } from "react" ;
3+ import { useRef , useState , useCallback } from "react" ;
44import { cn } from "appshell-react" ;
55
66const basePath = process . env . NEXT_PUBLIC_BASE_PATH || "" ;
77
8+ // iPhone 15 Pro logical dimensions
9+ const DEVICE_W = 393 ;
10+ const DEVICE_H = 852 ;
11+ // Safe area insets (Dynamic Island top + home indicator bottom)
12+ const SA_TOP = 59 ;
13+ const SA_BOTTOM = 34 ;
14+ // Display size
15+ const FRAME_W = 280 ;
16+ const SCALE = FRAME_W / DEVICE_W ;
17+ const FRAME_H = Math . round ( DEVICE_H * SCALE ) ;
18+
819interface PhoneMockupProps {
920 src : string ;
1021 className ?: string ;
@@ -16,73 +27,85 @@ export function PhoneMockup({ src, className }: PhoneMockupProps) {
1627
1728 const fullSrc = `${ basePath } ${ src } ` ;
1829
30+ const handleLoad = useCallback ( ( ) => {
31+ setLoaded ( true ) ;
32+ try {
33+ const doc = iframeRef . current ?. contentDocument ;
34+ if ( ! doc ) return ;
35+ const style = doc . createElement ( "style" ) ;
36+ style . textContent = `:root{--sa-top:${ SA_TOP } px;--sa-bottom:${ SA_BOTTOM } px;--sa-left:0px;--sa-right:0px}` ;
37+ doc . head . appendChild ( style ) ;
38+ } catch {
39+ // cross-origin — ignored in production
40+ }
41+ } , [ ] ) ;
42+
1943 return (
2044 < div
2145 className = { cn ( "relative mx-auto select-none" , className ) }
22- style = { { width : 300 , height : 612 } }
46+ style = { { width : FRAME_W + 16 , height : FRAME_H + 16 } }
2347 >
24- { /* Phone body — titanium finish */ }
25- < div className = "absolute inset-0 rounded-[3rem ] bg-gradient-to-b from-[#2a2a2e] via-[#1d1d1f] to-[#2a2a2e] shadow-2xl shadow-black/40" >
48+ { /* Phone body */ }
49+ < div className = "absolute inset-0 rounded-[2.8rem ] bg-gradient-to-b from-[#2a2a2e] via-[#1d1d1f] to-[#2a2a2e] shadow-2xl shadow-black/40" >
2650 { /* Side buttons */ }
27- < div className = "absolute -left-[2px] top-[100px] h-8 w-[3px] rounded-l-sm bg-[#3a3a3c]" />
28- < div className = "absolute -left-[2px] top-[145px] h-14 w-[3px] rounded-l-sm bg-[#3a3a3c]" />
29- < div className = "absolute -left-[2px] top-[195px] h-14 w-[3px] rounded-l-sm bg-[#3a3a3c]" />
30- < div className = "absolute -right-[2px] top-[140px] h-16 w-[3px] rounded-r-sm bg-[#3a3a3c]" />
51+ < div className = "absolute -left-[2px] top-[80px] h-7 w-[3px] rounded-l-sm bg-[#3a3a3c]" />
52+ < div className = "absolute -left-[2px] top-[118px] h-12 w-[3px] rounded-l-sm bg-[#3a3a3c]" />
53+ < div className = "absolute -left-[2px] top-[160px] h-12 w-[3px] rounded-l-sm bg-[#3a3a3c]" />
54+ < div className = "absolute -right-[2px] top-[115px] h-14 w-[3px] rounded-r-sm bg-[#3a3a3c]" />
55+
56+ { /* Screen area */ }
57+ < div className = "absolute inset-[8px] rounded-[2.2rem] overflow-hidden bg-black" >
58+ { /* Iframe at native resolution, scaled down */ }
59+ < div
60+ className = "origin-top-left"
61+ style = { { width : DEVICE_W , height : DEVICE_H , transform : `scale(${ SCALE } )` } }
62+ >
63+ { ! loaded && (
64+ < div className = "absolute inset-0 z-10 flex items-center justify-center bg-white dark:bg-zinc-950" >
65+ < div className = "size-6 animate-spin rounded-full border-2 border-zinc-300 border-t-zinc-600" />
66+ </ div >
67+ ) }
68+ < iframe
69+ ref = { iframeRef }
70+ src = { fullSrc }
71+ title = "Example preview"
72+ className = { cn (
73+ "h-full w-full border-0 transition-opacity duration-300" ,
74+ loaded ? "opacity-100" : "opacity-0"
75+ ) }
76+ onLoad = { handleLoad }
77+ />
78+ </ div >
3179
32- { /* Screen bezel */ }
33- < div className = "absolute inset-[6px] rounded-[2.5rem] overflow-hidden bg-black" >
34- { /* Status bar */ }
35- < div className = "relative z-20 flex h-[52px] items-end justify-between px-7 pb-1 text-white" >
36- { /* Time */ }
37- < span className = "text-[15px] font-semibold leading-none" > 9:41</ span >
38- { /* Dynamic Island */ }
39- < div className = "absolute left-1/2 top-[10px] -translate-x-1/2 h-[30px] w-[120px] rounded-full bg-black z-30" />
40- { /* Status icons */ }
41- < div className = "flex items-center gap-[5px]" >
42- { /* Signal */ }
43- < svg width = "17" height = "12" viewBox = "0 0 17 12" fill = "white" >
80+ { /* Status bar overlay */ }
81+ < div
82+ className = "pointer-events-none absolute inset-x-0 top-0 z-20 flex items-end justify-between px-6 pb-0.5 text-white"
83+ style = { { height : Math . round ( SA_TOP * SCALE ) } }
84+ >
85+ < span className = "text-[11px] font-semibold leading-none" > 9:41</ span >
86+ < div className = "absolute left-1/2 top-[3px] -translate-x-1/2 h-[22px] w-[90px] rounded-full bg-black" />
87+ < div className = "flex items-center gap-[4px]" >
88+ < svg width = "14" height = "10" viewBox = "0 0 17 12" fill = "white" >
4489 < rect x = "0" y = "9" width = "3" height = "3" rx = "0.5" opacity = "0.4" />
4590 < rect x = "4.5" y = "6" width = "3" height = "6" rx = "0.5" opacity = "0.6" />
4691 < rect x = "9" y = "3" width = "3" height = "9" rx = "0.5" opacity = "0.8" />
4792 < rect x = "13.5" y = "0" width = "3" height = "12" rx = "0.5" />
4893 </ svg >
49- { /* WiFi */ }
50- < svg width = "16" height = "12" viewBox = "0 0 16 12" fill = "white" >
94+ < svg width = "13" height = "10" viewBox = "0 0 16 12" fill = "white" >
5195 < path d = "M8 11.5a1.25 1.25 0 110-2.5 1.25 1.25 0 010 2.5z" />
5296 < path d = "M4.75 7.75a4.5 4.5 0 016.5 0" stroke = "white" strokeWidth = "1.5" fill = "none" strokeLinecap = "round" />
5397 < path d = "M2 4.75a8 8 0 0112 0" stroke = "white" strokeWidth = "1.5" fill = "none" strokeLinecap = "round" />
5498 </ svg >
55- { /* Battery */ }
56- < svg width = "27" height = "12" viewBox = "0 0 27 12" fill = "none" >
99+ < svg width = "22" height = "10" viewBox = "0 0 27 12" fill = "none" >
57100 < rect x = "0.5" y = "0.5" width = "22" height = "11" rx = "2.5" stroke = "white" strokeOpacity = "0.35" />
58101 < rect x = "2" y = "2" width = "19" height = "8" rx = "1.5" fill = "white" />
59102 < path d = "M24 4v4a2 2 0 000-4z" fill = "white" fillOpacity = "0.4" />
60103 </ svg >
61104 </ div >
62105 </ div >
63106
64- { /* Iframe screen content */ }
65- < div className = "absolute inset-0 top-0" >
66- { ! loaded && (
67- < div className = "absolute inset-0 z-10 flex items-center justify-center bg-background" >
68- < div className = "size-5 animate-spin rounded-full border-2 border-muted-foreground/30 border-t-muted-foreground" />
69- </ div >
70- ) }
71- < iframe
72- ref = { iframeRef }
73- src = { fullSrc }
74- title = "Example preview"
75- className = { cn (
76- "h-full w-full border-0 transition-opacity duration-300" ,
77- loaded ? "opacity-100" : "opacity-0"
78- ) }
79- style = { { pointerEvents : "auto" } }
80- onLoad = { ( ) => setLoaded ( true ) }
81- />
82- </ div >
83-
84107 { /* Home indicator */ }
85- < div className = "absolute bottom-[6px ] left-1/2 -translate-x-1/2 z-20 h-[5px ] w-[130px ] rounded-full bg-white/60" />
108+ < div className = "pointer-events-none absolute bottom-[4px ] left-1/2 -translate-x-1/2 z-20 h-[4px ] w-[100px ] rounded-full bg-white/60" />
86109 </ div >
87110 </ div >
88111 </ div >
0 commit comments