diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/image-preview.tsx b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/image-preview.tsx
index e4ba8033261..d65fd1d928b 100644
--- a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/image-preview.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/image-preview.tsx
@@ -1,12 +1,14 @@
'use client'
-import { memo } from 'react'
+import { memo, useState } from 'react'
import type { WorkspaceFileRecord } from '@/lib/uploads/contexts/workspace'
import { useFileContentSource } from '@/hooks/use-file-content-source'
+import { PREVIEW_LOADING_OVERLAY } from './preview-shared'
import { ZoomablePreview } from './zoomable-preview'
export const ImagePreview = memo(function ImagePreview({ file }: { file: WorkspaceFileRecord }) {
const source = useFileContentSource()
+ const [hasSettled, setHasSettled] = useState(false)
// Version the URL on updatedAt: overwrites keep the same storage key, so an unversioned
// URL would resolve to a previously cached copy instead of the rewritten bytes.
const serveUrl = source.buildUrl(file.key, {
@@ -14,14 +16,19 @@ export const ImagePreview = memo(function ImagePreview({ file }: { file: Workspa
})
return (
-
-
-
+
+
+
setHasSettled(true)}
+ onError={() => setHasSettled(true)}
+ />
+
+ {!hasSettled && PREVIEW_LOADING_OVERLAY}
+
)
})
diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/preview-shared.tsx b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/preview-shared.tsx
index 4796ae6bb1e..041a0225074 100644
--- a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/preview-shared.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/preview-shared.tsx
@@ -2,6 +2,7 @@
import { Component, type ErrorInfo, type ReactNode } from 'react'
import { cn } from '@sim/emcn'
+import { Loader } from '@sim/emcn/icons'
import { createLogger } from '@sim/logger'
const logger = createLogger('FilePreview')
@@ -87,13 +88,20 @@ export function resolvePreviewError(
return renderError
}
+/** Canonical content-area loading spinner, matching the rest of the app. */
+const PREVIEW_LOADING_SPINNER = (
+
+)
+
/**
- * Canonical blank loading overlay for previews that render into a
- * `--surface-1` canvas. Absolutely covers the canvas (with `z-10` so it
- * paints above in-flow render targets) until the preview is ready.
+ * Canonical loading overlay for previews that render into a `--surface-1`
+ * canvas. Absolutely covers the canvas (with `z-10` so it paints above
+ * in-flow render targets) with a centered spinner until the preview is ready.
*/
export const PREVIEW_LOADING_OVERLAY = (
-
+
+ {PREVIEW_LOADING_SPINNER}
+
)
interface PreviewLoadingFrameProps {
@@ -104,14 +112,21 @@ interface PreviewLoadingFrameProps {
}
/**
- * Canonical in-flow blank loading frame shown while a preview is fetching or
- * rendering. The `tone` must match the background of the loaded state it is
- * standing in for, so mount completion does not flash a different token.
+ * Canonical in-flow loading frame with a centered spinner, shown while a
+ * preview is fetching or rendering. The `tone` must match the background of
+ * the loaded state it is standing in for, so mount completion does not flash
+ * a different token.
*/
export function PreviewLoadingFrame({ className, tone = 'bg' }: PreviewLoadingFrameProps) {
return (
+ className={cn(
+ 'flex items-center justify-center',
+ tone === 'surface' ? 'bg-[var(--surface-1)]' : 'bg-[var(--bg)]',
+ className
+ )}
+ >
+ {PREVIEW_LOADING_SPINNER}
+
)
}