import { useLayoutEffect, useRef, useState } from 'react' import { cn } from '@/client/lib/cn' import { getWorkspaceThemeColorStyle, getWorkspaceThemeStyle } from '@/client/runtime/workspace-theme' import { useWorkspacePreview } from './api' type WorkspacePreviewProps = { workspaceId: string } const FOLDER = { width: 320, height: 180, radius: 32, topLeftRadius: 18, bodyTop: 24, tabEnd: 124, shoulder: { width: 48, roundness: 20 } } as const function folderBackdropPath(horizontalRadiusScale: number): string { const { width, height, radius: radiusY, topLeftRadius: topLeftRadiusY, bodyTop, tabEnd, shoulder } = FOLDER const radiusX = Math.min(width / 2, radiusY * horizontalRadiusScale) const topLeftRadiusX = Math.min(width / 2, topLeftRadiusY * horizontalRadiusScale) const shoulderWidth = Math.max(0, shoulder.width) const bodyStart = tabEnd + shoulderWidth const roundness = Math.max(0, Math.min(shoulder.roundness, shoulderWidth / 2)) return [ `M ${topLeftRadiusX} 0`, `H ${tabEnd}`, `C ${tabEnd + roundness} 0 ${bodyStart - roundness} ${bodyTop} ${bodyStart} ${bodyTop}`, `H ${width - radiusX}`, `Q ${width} ${bodyTop} ${width} ${bodyTop + radiusY}`, `V ${height - radiusY}`, `Q ${width} ${height} ${width - radiusX} ${height}`, `H ${radiusX}`, `Q 0 ${height} 0 ${height - radiusY}`, `V ${topLeftRadiusY}`, `Q 0 0 ${topLeftRadiusX} 0`, 'Z' ].join(' ') } function useFolderRadiusScale() { const ref = useRef(null) const [horizontalRadiusScale, setHorizontalRadiusScale] = useState(1) useLayoutEffect(() => { const element = ref.current if (!element) return const update = (width: number, height: number) => { if (width <= 0 || height <= 0) return const scaleX = width / FOLDER.width const scaleY = height / FOLDER.height const nextScale = scaleY / scaleX setHorizontalRadiusScale(current => Math.abs(current - nextScale) < 0.001 ? current : nextScale ) } const bounds = element.getBoundingClientRect() update(bounds.width, bounds.height) const observer = new ResizeObserver(([entry]) => { if (entry) update(entry.contentRect.width, entry.contentRect.height) }) observer.observe(element) return () => observer.disconnect() }, []) return { ref, horizontalRadiusScale } } // Bottom-to-top treatment for up to three applet thumbnails. The frame stays // opaque while only the image fades. const THUMBNAIL_STYLES = [ { frame: cn( '-translate-x-3 translate-y-3 -rotate-6', 'group-hover:translate-y-2 group-focus-visible:translate-y-2' ), img: 'opacity-40' }, { frame: cn( 'translate-x-3 translate-y-2 rotate-5', 'group-hover:translate-y-1 group-focus-visible:translate-y-1' ), img: 'opacity-50' }, { frame: cn( '-translate-x-1 translate-y-1 -rotate-2', 'group-hover:-translate-y-2 group-focus-visible:-translate-y-2' ), img: 'opacity-100' } ] as const export function WorkspacePreview({ workspaceId }: WorkspacePreviewProps) { const query = useWorkspacePreview(workspaceId) const thumbnails = query.data ? [...query.data.thumbnails].reverse() : [] const slots = THUMBNAIL_STYLES.slice(THUMBNAIL_STYLES.length - thumbnails.length) const firstUserMessage = query.data?.firstUserMessage const theme = query.data?.theme const { ref, horizontalRadiusScale } = useFolderRadiusScale() const backdropPath = folderBackdropPath(horizontalRadiusScale) const noiseFilterId = `folder-noise-${workspaceId}` const themeStyle = getWorkspaceThemeStyle(theme) const folderThemeStyle = getWorkspaceThemeColorStyle(theme) return (
{thumbnails.map((src, index) => (
))} {thumbnails.length === 0 && firstUserMessage && (
)}
) }