@@ -52,13 +52,24 @@ const ADDRESS_BAR_HEIGHT = 40
5252
5353const isLoading = ref (true )
5454const isIframeLoading = ref (false )
55+ // Flips true once the pane is mounted so the hide/show effect can run — a plain
56+ // `pane.isMounted` read isn't reactive.
57+ const paneReady = ref (false )
5558
5659// A devframe whose client assets are published as their own npm package
5760// answers with a fallback page when it can reach neither a local install nor
5861// the CDN they live on. That page reports itself over `postMessage`, so the
5962// failure renders as a hub panel — with the install command and a retry —
6063// rather than as a bare page inside the frame.
6164const assetsError = ref <RemoteAssetsErrorMessage | null >(null )
65+
66+ // The blank iframe paints white while its content loads, so a placeholder is
67+ // only useful when the pane steps aside (`pane.hide()`) to reveal it — the same
68+ // layering trick `ViewAssetsError` relies on. Show it during the initial load
69+ // and any hard navigation/refresh, but never on top of the assets-error panel.
70+ const showLoadingPlaceholder = computed (
71+ () => ! assetsError .value && (isLoading .value || isIframeLoading .value ),
72+ )
6273const viewFrame = useTemplateRef <HTMLDivElement >(' viewFrame' )
6374const urlInputRef = useTemplateRef <HTMLInputElement >(' urlInput' )
6475
@@ -236,6 +247,10 @@ onMounted(() => {
236247
237248 if (existed )
238249 updateCurrentUrl ()
250+ else
251+ // A freshly created pane is loading its initial content — reflect it so the
252+ // placeholder covers the first paint, not just later navigations.
253+ isIframeLoading .value = true
239254
240255 // Listen for iframe load events
241256 onIframeLoad = () => {
@@ -268,19 +283,23 @@ onMounted(() => {
268283 })
269284
270285 // The iframe lives in its own layer stacked over this view, so the error
271- // panel is only visible once the pane steps aside. `hide()` keeps the frame
272- // alive (and its state intact) for the retry.
286+ // panel and the loading placeholder are only visible once the pane steps
287+ // aside. `hide()` keeps the frame alive (and its state intact) so the content
288+ // keeps loading behind the placeholder and survives a retry.
273289 watchEffect (() => {
274- if (assetsError .value )
290+ if (! paneReady .value )
291+ return
292+ if (assetsError .value || isIframeLoading .value )
275293 pane .hide ()
276- else if ( pane . isMounted )
294+ else
277295 pane .show ()
278296 })
279297
280298 window .addEventListener (' message' , onWindowMessage )
281299
282300 pane .mount (viewFrame .value ! )
283301 isLoading .value = false
302+ paneReady .value = true
284303 nextTick (() => {
285304 pane .update ()
286305 })
@@ -364,8 +383,14 @@ onUnmounted(() => {
364383 ref =" viewFrame"
365384 class =" devframes-view-iframe relative w-full h-full flex-1 items-center justify-center"
366385 >
367- <div v-if =" isLoading" class =" op50 z--1" >
368- Loading iframe...
386+ <div
387+ v-if =" showLoadingPlaceholder"
388+ class =" devframes-view-iframe-loading absolute inset-0 flex flex-col items-center justify-center gap-2 bg-base"
389+ >
390+ <div class =" i-ph:circle-notch-duotone animate-spin text-3xl color-faint" />
391+ <div class =" text-sm color-muted" >
392+ Loading…
393+ </div >
369394 </div >
370395 <ViewAssetsError
371396 v-if =" assetsError "
0 commit comments