improvement(files): show loading spinner in file preview content area (#5610)

This commit is contained in:
Waleed
2026-07-11 17:48:25 -07:00
committed by GitHub
parent 0aa23090e9
commit 83c532ce36
2 changed files with 41 additions and 19 deletions
@@ -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 (
<ZoomablePreview className='flex flex-1' contentClassName='h-full w-full'>
<img
src={serveUrl}
alt={file.name}
className='max-h-full max-w-full select-none rounded-md object-contain'
draggable={false}
loading='eager'
/>
</ZoomablePreview>
<div className='relative flex min-h-0 flex-1 flex-col'>
<ZoomablePreview className='flex flex-1' contentClassName='h-full w-full'>
<img
src={serveUrl}
alt={file.name}
className='max-h-full max-w-full select-none rounded-md object-contain'
draggable={false}
loading='eager'
onLoad={() => setHasSettled(true)}
onError={() => setHasSettled(true)}
/>
</ZoomablePreview>
{!hasSettled && PREVIEW_LOADING_OVERLAY}
</div>
)
})
@@ -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 = (
<Loader className='size-[20px] text-[var(--text-secondary)]' animate />
)
/**
* 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 = (
<div className='absolute inset-0 z-10 bg-[var(--surface-1)]' />
<div className='absolute inset-0 z-10 flex items-center justify-center bg-[var(--surface-1)]'>
{PREVIEW_LOADING_SPINNER}
</div>
)
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 (
<div
className={cn(tone === 'surface' ? 'bg-[var(--surface-1)]' : 'bg-[var(--bg)]', className)}
/>
className={cn(
'flex items-center justify-center',
tone === 'surface' ? 'bg-[var(--surface-1)]' : 'bg-[var(--bg)]',
className
)}
>
{PREVIEW_LOADING_SPINNER}
</div>
)
}