| 1 | import { CircleAlert, Loader2 } from 'lucide-react' |
| 2 | import { PreviewIframe } from '@renderer/components/preview/PreviewIframe' |
| 3 | import { cn } from '@renderer/lib/utils' |
| 4 | import type { GenerationPreviewPage } from './types' |
| 5 | import type { SlideSizePreset } from '@shared/slide-size' |
| 6 | |
| 7 | export function GenerationThumbnail({ |
| 8 | page, |
| 9 | previewEnabled = true, |
| 10 | slideSize |
| 11 | }: { |
| 12 | page: GenerationPreviewPage |
| 13 | previewEnabled?: boolean |
| 14 | slideSize: SlideSizePreset | null |
| 15 | }): React.JSX.Element { |
| 16 | if (!slideSize) { |
| 17 | return <div className="h-[195px] w-full rounded-xl bg-[#f5f1e8]/88" /> |
| 18 | } |
| 19 | const thumbnailFitStyle = |
| 20 | slideSize.width >= slideSize.height |
| 21 | ? { width: '100%', aspectRatio: `${slideSize.width}/${slideSize.height}` } |
| 22 | : { height: '100%', aspectRatio: `${slideSize.width}/${slideSize.height}` } |
| 23 | const hasPreview = |
| 24 | previewEnabled && page.status === 'completed' && (page.htmlPath || page.sourceUrl) |
| 25 | |
| 26 | return ( |
| 27 | <div |
| 28 | className={cn( |
| 29 | 'group relative flex w-full min-w-0 flex-col overflow-hidden rounded-xl border bg-[#fffaf1]/78 p-1.5 shadow-[0_16px_34px_rgba(70,82,58,0.12)] transition-all duration-500', |
| 30 | page.status === 'completed' && 'border-[#b8d3a6] translate-y-0 opacity-100', |
| 31 | page.status === 'generating' && |
| 32 | 'border-[#8fb873] bg-[#f6fbef]/88 shadow-[0_18px_40px_rgba(95,132,72,0.22)]', |
| 33 | page.status === 'failed' && 'border-[#d7b5ae] bg-[#fbf1ee]/92', |
| 34 | page.status === 'pending' && 'border-[#dfd4bf]/72 opacity-72' |
| 35 | )} |
| 36 | > |
| 37 | <div |
| 38 | className="relative flex h-[180px] w-full min-w-0 shrink-0 items-center justify-center overflow-hidden bg-[#f5f1e8]/88 shadow-[0_5px_14px_rgba(93,107,77,0.08)]" |
| 39 | style={{ contain: 'paint' }} |
| 40 | > |
| 41 | <div className="relative max-h-full max-w-full overflow-hidden" style={thumbnailFitStyle}> |
| 42 | {hasPreview ? ( |
| 43 | <PreviewIframe |
| 44 | key={`generating-thumb-${page.id}-${page.previewVersion ?? 0}`} |
| 45 | src={page.sourceUrl} |
| 46 | htmlPath={page.htmlPath} |
| 47 | pageId={page.pageId} |
| 48 | title={`generating-page-${page.pageNumber}`} |
| 49 | slideSize={slideSize} |
| 50 | inspectable={false} |
| 51 | thumbnail |
| 52 | /> |
| 53 | ) : ( |
| 54 | <div |
| 55 | className={cn( |
| 56 | 'flex h-full w-full flex-col justify-between p-3', |
| 57 | page.status === 'generating' |
| 58 | ? 'bg-[linear-gradient(135deg,#eef6e7_0%,#fff8ec_100%)]' |
| 59 | : page.status === 'failed' |
| 60 | ? 'bg-[#f7e7e2]' |
| 61 | : 'bg-[linear-gradient(135deg,#f5efe4_0%,#e9decb_100%)]' |
| 62 | )} |
| 63 | > |
| 64 | <div className="flex items-center justify-between"> |
| 65 | <span className="h-2 w-16 rounded-full bg-white/72" /> |
| 66 | <span className="h-5 w-5 rounded-md border border-white/80 bg-white/58" /> |
| 67 | </div> |
| 68 | <div className="space-y-2"> |
| 69 | <span className="block h-3 w-3/4 rounded-full bg-white/78" /> |
| 70 | <span className="block h-2 w-11/12 rounded-full bg-white/56" /> |
| 71 | <span className="block h-2 w-7/12 rounded-full bg-white/56" /> |
| 72 | </div> |
| 73 | <div className="grid grid-cols-3 gap-2"> |
| 74 | <span className="h-7 rounded-md bg-white/54" /> |
| 75 | <span className="h-7 rounded-md bg-white/42" /> |
| 76 | <span className="h-7 rounded-md bg-white/54" /> |
| 77 | </div> |
| 78 | </div> |
| 79 | )} |
| 80 | </div> |
| 81 | |
| 82 | {page.status === 'generating' && ( |
| 83 | <div className="absolute inset-0 border-2 border-[#83ad67]/70"> |
| 84 | <div className="absolute right-2 top-2 rounded-full bg-[#fffaf1]/90 p-1 shadow-sm"> |
| 85 | <Loader2 className="h-3.5 w-3.5 animate-spin text-[#5f8a43]" /> |
| 86 | </div> |
| 87 | </div> |
| 88 | )} |
| 89 | |
| 90 | {page.status === 'failed' && ( |
| 91 | <div className="absolute inset-0 flex items-center justify-center bg-[#fbf1ee]/76"> |
| 92 | <CircleAlert className="h-6 w-6 text-[#a45f58]" /> |
| 93 | </div> |
| 94 | )} |
| 95 | </div> |
| 96 | |
| 97 | <div className="flex w-full min-w-0 items-center justify-between gap-2 px-0.5 pb-0.5 pt-2"> |
| 98 | <span className="shrink-0 rounded-md bg-[#5d6b4d]/10 px-1.5 py-0.5 text-[10px] font-semibold text-[#4f613f]"> |
| 99 | P{page.pageNumber} |
| 100 | </span> |
| 101 | {page.status !== 'failed' && ( |
| 102 | <span className="min-w-0 truncate text-xs font-medium text-[#4d5b40]" title={page.title}> |
| 103 | {page.title} |
| 104 | </span> |
| 105 | )} |
| 106 | </div> |
| 107 | </div> |
| 108 | ) |
| 109 | } |
| 110 |