file renderers v1

This commit is contained in:
marko-kraemer 2025-04-16 09:19:18 +01:00
parent 17dc1bad4d
commit cd5018d1f4
11 changed files with 2271 additions and 109 deletions

File diff suppressed because it is too large Load Diff

View File

@ -27,8 +27,14 @@
"@radix-ui/react-slot": "^1.2.0",
"@radix-ui/react-tabs": "^1.1.4",
"@radix-ui/react-tooltip": "^1.2.0",
"@react-pdf/renderer": "^4.3.0",
"@supabase/ssr": "latest",
"@supabase/supabase-js": "latest",
"@tailwindcss/typography": "^0.5.16",
"@uiw/codemirror-extensions-langs": "^4.23.10",
"@uiw/codemirror-theme-vscode": "^4.23.10",
"@uiw/codemirror-theme-xcode": "^4.23.10",
"@uiw/react-codemirror": "^4.23.10",
"@usebasejump/shared": "^0.0.3",
"autoprefixer": "10.4.17",
"class-variance-authority": "^0.7.1",
@ -52,6 +58,8 @@
"react-hook-form": "^7.55.0",
"react-markdown": "^10.1.0",
"react-scan": "^0.3.2",
"rehype-raw": "^7.0.0",
"rehype-sanitize": "^6.0.0",
"remark-gfm": "^4.0.1",
"sonner": "^2.0.3",
"swr": "^2.2.5",
@ -72,7 +80,7 @@
"encoding": "^0.1.13",
"eslint": "^9",
"eslint-config-next": "15.2.2",
"shiki": "^3.2.1",
"shiki": "^3.2.2",
"tailwindcss": "^4",
"tw-animate-css": "^1.2.4",
"typescript": "^5"

View File

@ -83,7 +83,7 @@ export function SidebarLeft({
transform: state === "collapsed" ? "translateX(-10px)" : "translateX(0)",
pointerEvents: state === "collapsed" ? "none" : "auto"
}}>
<span className="font-semibold"> SUNA</span>
{/* <span className="font-semibold"> SUNA</span> */}
</div>
{state !== "collapsed" && (
<div className="ml-auto">

View File

@ -0,0 +1,53 @@
"use client";
import React from "react";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { Download, File } from "lucide-react";
interface BinaryRendererProps {
url: string;
fileName: string;
className?: string;
}
export function BinaryRenderer({ url, fileName, className }: BinaryRendererProps) {
const fileExtension = fileName.split('.').pop()?.toLowerCase() || '';
// Handle download
const handleDownload = () => {
const link = document.createElement('a');
link.href = url;
link.download = fileName;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
return (
<div className={cn("flex flex-col items-center justify-center p-10", className)}>
<div className="flex flex-col items-center text-center max-w-md">
<div className="relative mb-6">
<File className="h-24 w-24 text-muted-foreground/50" />
<div className="absolute bottom-1 right-1 bg-background rounded-sm px-1.5 py-0.5 text-xs font-medium text-muted-foreground border">
{fileExtension.toUpperCase()}
</div>
</div>
<h3 className="text-lg font-semibold mb-2">{fileName}</h3>
<p className="text-sm text-muted-foreground mb-6">
This binary file cannot be previewed in the browser
</p>
<Button
variant="default"
className="min-w-[150px]"
onClick={handleDownload}
>
<Download className="h-4 w-4 mr-2" />
Download
</Button>
</div>
</div>
);
}

View File

@ -0,0 +1,92 @@
"use client";
import React, { useEffect, useState } from "react";
import CodeMirror from "@uiw/react-codemirror";
import { vscodeDark } from "@uiw/codemirror-theme-vscode";
import { langs } from "@uiw/codemirror-extensions-langs";
import { cn } from "@/lib/utils";
import { ScrollArea } from "@/components/ui/scroll-area";
import { xcodeLight } from "@uiw/codemirror-theme-xcode";
import { useTheme } from "next-themes";
import { EditorView } from "@codemirror/view";
interface CodeRendererProps {
content: string;
language?: string;
className?: string;
}
// Map of language aliases to CodeMirror language support
const languageMap: Record<string, any> = {
js: langs.javascript,
jsx: langs.jsx,
ts: langs.typescript,
tsx: langs.tsx,
html: langs.html,
css: langs.css,
json: langs.json,
md: langs.markdown,
python: langs.python,
py: langs.python,
rust: langs.rust,
go: langs.go,
java: langs.java,
c: langs.c,
cpp: langs.cpp,
cs: langs.csharp,
php: langs.php,
ruby: langs.ruby,
sh: langs.shell,
bash: langs.shell,
sql: langs.sql,
yaml: langs.yaml,
yml: langs.yaml,
// Add more languages as needed
};
export function CodeRenderer({ content, language = "", className }: CodeRendererProps) {
// Get current theme
const { resolvedTheme } = useTheme();
const [mounted, setMounted] = useState(false);
// Set mounted state to true after component mounts
useEffect(() => {
setMounted(true);
}, []);
// Determine the language extension to use
const langExtension = language && languageMap[language]
? [languageMap[language]()]
: [];
// Add line wrapping extension
const extensions = [
...langExtension,
EditorView.lineWrapping,
];
// Select the theme based on the current theme
const theme = mounted && resolvedTheme === 'dark' ? vscodeDark : xcodeLight;
return (
<ScrollArea className={cn("w-full h-full", className)}>
<div className="w-full">
<CodeMirror
value={content}
theme={theme}
extensions={extensions}
basicSetup={{
lineNumbers: false,
highlightActiveLine: false,
highlightActiveLineGutter: false,
foldGutter: false,
}}
editable={false}
className="text-sm w-full min-h-full"
style={{ maxWidth: '100%' }}
height="auto"
/>
</div>
</ScrollArea>
);
}

View File

@ -0,0 +1,297 @@
"use client";
import React, { useState, useRef, useEffect } from "react";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { ZoomIn, ZoomOut, RotateCw, Download, Maximize2, Minimize2, Info } from "lucide-react";
interface ImageRendererProps {
url: string;
className?: string;
}
export function ImageRenderer({ url, className }: ImageRendererProps) {
const [zoom, setZoom] = useState(1);
const [rotation, setRotation] = useState(0);
const [isPanning, setIsPanning] = useState(false);
const [position, setPosition] = useState({ x: 0, y: 0 });
const [startPanPosition, setStartPanPosition] = useState({ x: 0, y: 0 });
const [isFitToScreen, setIsFitToScreen] = useState(true);
const [imgLoaded, setImgLoaded] = useState(false);
const [imgError, setImgError] = useState(false);
const [imgInfo, setImgInfo] = useState<{
width: number;
height: number;
type: string;
} | null>(null);
const containerRef = useRef<HTMLDivElement>(null);
const imageRef = useRef<HTMLImageElement>(null);
// Check if the url is an SVG
const isSvg = url?.toLowerCase().endsWith('.svg') || url?.includes('image/svg');
// Reset position when zoom changes
useEffect(() => {
if (isFitToScreen) {
setPosition({ x: 0, y: 0 });
}
}, [zoom, isFitToScreen]);
// Handle image load success
const handleImageLoad = () => {
setImgLoaded(true);
setImgError(false);
if (imageRef.current) {
setImgInfo({
width: imageRef.current.naturalWidth,
height: imageRef.current.naturalHeight,
type: isSvg ? 'SVG' : url.split('.').pop()?.toUpperCase() || 'Image'
});
}
};
// Handle image load error
const handleImageError = () => {
setImgLoaded(false);
setImgError(true);
};
// Functions for zooming
const handleZoomIn = () => {
setZoom(prev => Math.min(prev + 0.25, 3));
setIsFitToScreen(false);
};
const handleZoomOut = () => {
const newZoom = Math.max(zoom - 0.25, 0.5);
setZoom(newZoom);
if (newZoom === 0.5) {
setIsFitToScreen(true);
}
};
// Function for rotation
const handleRotate = () => {
setRotation(prev => (prev + 90) % 360);
};
// Function for download
const handleDownload = () => {
const link = document.createElement('a');
link.href = url;
const filename = url.split('/').pop() || 'image';
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
// Toggle fit to screen
const toggleFitToScreen = () => {
if (isFitToScreen) {
setZoom(1);
setIsFitToScreen(false);
} else {
setZoom(0.5);
setPosition({ x: 0, y: 0 });
setIsFitToScreen(true);
}
};
// Pan handlers
const handleMouseDown = (e: React.MouseEvent) => {
if (zoom > 0.5) {
setIsPanning(true);
setStartPanPosition({
x: e.clientX - position.x,
y: e.clientY - position.y
});
}
};
const handleMouseMove = (e: React.MouseEvent) => {
if (isPanning && zoom > 0.5) {
setPosition({
x: e.clientX - startPanPosition.x,
y: e.clientY - startPanPosition.y
});
}
};
const handleMouseUp = () => {
setIsPanning(false);
};
const handleMouseLeave = () => {
setIsPanning(false);
};
// Calculate transform styles
const imageTransform = `scale(${zoom}) rotate(${rotation}deg)`;
const translateTransform = `translate(${position.x}px, ${position.y}px)`;
// Show image info
const [showInfo, setShowInfo] = useState(false);
return (
<div className={cn("flex flex-col w-full h-full", className)}>
{/* Controls */}
<div className="flex items-center justify-between py-2 px-4 bg-muted/30 border-b mb-2 rounded-t-md">
<div className="flex items-center space-x-2">
<Button
variant="ghost"
size="sm"
className="h-8 w-8 p-0"
onClick={handleZoomOut}
title="Zoom out"
disabled={imgError}
>
<ZoomOut className="h-4 w-4" />
</Button>
<span className="text-xs font-medium">{Math.round(zoom * 100)}%</span>
<Button
variant="ghost"
size="sm"
className="h-8 w-8 p-0"
onClick={handleZoomIn}
title="Zoom in"
disabled={imgError}
>
<ZoomIn className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="sm"
className="h-8 w-8 p-0"
onClick={handleRotate}
title="Rotate"
disabled={imgError}
>
<RotateCw className="h-4 w-4" />
</Button>
</div>
<div className="flex items-center space-x-2">
<Button
variant="ghost"
size="sm"
className="h-8 w-8 p-0"
onClick={() => setShowInfo(!showInfo)}
title="Image information"
>
<Info className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="sm"
className="h-8 w-8 p-0"
onClick={toggleFitToScreen}
title={isFitToScreen ? "Actual size" : "Fit to screen"}
disabled={imgError}
>
{isFitToScreen ? (
<Maximize2 className="h-4 w-4" />
) : (
<Minimize2 className="h-4 w-4" />
)}
</Button>
<Button
variant="ghost"
size="sm"
className="h-8 w-8 p-0"
onClick={handleDownload}
title="Download"
>
<Download className="h-4 w-4" />
</Button>
</div>
</div>
{/* Image info overlay */}
{showInfo && imgInfo && (
<div className="absolute top-16 right-4 z-50 bg-background/80 backdrop-blur-sm p-3 rounded-md shadow-md border border-border text-xs">
<p><strong>Type:</strong> {imgInfo.type}</p>
<p><strong>Dimensions:</strong> {imgInfo.width} × {imgInfo.height}</p>
</div>
)}
{/* Image container */}
<div
ref={containerRef}
className="flex-1 overflow-hidden relative bg-grid-pattern rounded-b-md"
onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
onMouseLeave={handleMouseLeave}
style={{
cursor: isPanning ? 'grabbing' : (zoom > 0.5 ? 'grab' : 'default'),
backgroundColor: '#f5f5f5',
backgroundImage: 'linear-gradient(45deg, #e0e0e0 25%, transparent 25%), linear-gradient(-45deg, #e0e0e0 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #e0e0e0 75%), linear-gradient(-45deg, transparent 75%, #e0e0e0 75%)',
backgroundSize: '20px 20px',
backgroundPosition: '0 0, 0 10px, 10px -10px, -10px 0px',
}}
>
{imgError ? (
<div className="flex flex-col items-center justify-center h-full p-6 text-center">
<p className="text-destructive font-medium mb-2">Failed to load image</p>
<p className="text-sm text-muted-foreground">The image could not be displayed</p>
</div>
) : (
<div
className="absolute inset-0 flex items-center justify-center"
style={{
transform: isFitToScreen ? 'none' : translateTransform,
transition: isPanning ? 'none' : 'transform 0.1s ease',
}}
>
{isSvg ? (
// Special handling for SVG - embed it as an object for better rendering
<object
data={url}
type="image/svg+xml"
className="max-w-full max-h-full"
style={{
transform: imageTransform,
transition: 'transform 0.2s ease',
width: '100%',
height: '100%',
}}
>
{/* Fallback to img if object fails */}
<img
ref={imageRef}
src={url}
alt="SVG preview"
className="max-w-full max-h-full object-contain"
style={{
transform: imageTransform,
transition: 'transform 0.2s ease',
}}
draggable={false}
onLoad={handleImageLoad}
onError={handleImageError}
/>
</object>
) : (
<img
ref={imageRef}
src={url}
alt="Image preview"
className="max-w-full max-h-full object-contain"
style={{
transform: imageTransform,
transition: 'transform 0.2s ease',
}}
draggable={false}
onLoad={handleImageLoad}
onError={handleImageError}
/>
)}
</div>
)}
</div>
</div>
);
}

View File

@ -0,0 +1,123 @@
"use client";
import React from "react";
import { cn } from "@/lib/utils";
import { MarkdownRenderer } from "./markdown-renderer";
import { CodeRenderer } from "./code-renderer";
import { PdfRenderer } from "./pdf-renderer";
import { ImageRenderer } from "./image-renderer";
import { BinaryRenderer } from "./binary-renderer";
export type FileType =
| 'markdown'
| 'code'
| 'pdf'
| 'image'
| 'text'
| 'binary';
interface FileRendererProps {
content: string | null;
binaryUrl: string | null;
fileName: string;
className?: string;
}
// Helper function to determine file type from extension
export function getFileTypeFromExtension(fileName: string): FileType {
const extension = fileName.split('.').pop()?.toLowerCase() || '';
const markdownExtensions = ['md', 'markdown'];
const codeExtensions = [
'js', 'jsx', 'ts', 'tsx', 'html', 'css', 'json', 'py', 'python',
'java', 'c', 'cpp', 'h', 'cs', 'go', 'rs', 'php', 'rb', 'sh', 'bash',
'xml', 'yml', 'yaml', 'toml', 'sql', 'graphql', 'swift', 'kotlin',
'dart', 'r', 'lua', 'scala', 'perl', 'haskell', 'rust'
];
const imageExtensions = ['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg', 'bmp', 'ico'];
const pdfExtensions = ['pdf'];
const textExtensions = ['txt', 'csv', 'log', 'env', 'ini'];
if (markdownExtensions.includes(extension)) {
return 'markdown';
} else if (codeExtensions.includes(extension)) {
return 'code';
} else if (imageExtensions.includes(extension)) {
return 'image';
} else if (pdfExtensions.includes(extension)) {
return 'pdf';
} else if (textExtensions.includes(extension)) {
return 'text';
} else {
return 'binary';
}
}
// Helper function to get language from file extension for code highlighting
export function getLanguageFromExtension(fileName: string): string {
const extension = fileName.split('.').pop()?.toLowerCase() || '';
const extensionToLanguage: Record<string, string> = {
js: 'javascript',
jsx: 'jsx',
ts: 'typescript',
tsx: 'tsx',
html: 'html',
css: 'css',
json: 'json',
py: 'python',
python: 'python',
java: 'java',
c: 'c',
cpp: 'cpp',
h: 'c',
cs: 'csharp',
go: 'go',
rs: 'rust',
php: 'php',
rb: 'ruby',
sh: 'shell',
bash: 'shell',
xml: 'xml',
yml: 'yaml',
yaml: 'yaml',
sql: 'sql',
// Add more mappings as needed
};
return extensionToLanguage[extension] || '';
}
export function FileRenderer({ content, binaryUrl, fileName, className }: FileRendererProps) {
const fileType = getFileTypeFromExtension(fileName);
const language = getLanguageFromExtension(fileName);
return (
<div className={cn("w-full h-full", className)}>
{fileType === 'binary' ? (
<BinaryRenderer
url={binaryUrl || ''}
fileName={fileName}
/>
) : fileType === 'image' && binaryUrl ? (
<ImageRenderer url={binaryUrl} />
) : fileType === 'pdf' && binaryUrl ? (
<PdfRenderer url={binaryUrl} />
) : fileType === 'markdown' ? (
<MarkdownRenderer content={content || ''} />
) : fileType === 'code' || fileType === 'text' ? (
<CodeRenderer
content={content || ''}
language={language}
className="w-full h-full"
/>
) : (
<div className="w-full h-full p-4">
<pre className="text-sm font-mono whitespace-pre-wrap break-words leading-relaxed bg-muted/30 p-4 rounded-lg overflow-auto max-h-full">
{content || ''}
</pre>
</div>
)}
</div>
);
}

View File

@ -0,0 +1,64 @@
"use client";
import React from "react";
import ReactMarkdown from "react-markdown";
import rehypeRaw from "rehype-raw";
import rehypeSanitize from "rehype-sanitize";
import { ScrollArea } from "@/components/ui/scroll-area";
import { cn } from "@/lib/utils";
import { CodeRenderer } from "./code-renderer";
interface MarkdownRendererProps {
content: string;
className?: string;
}
export function MarkdownRenderer({ content, className }: MarkdownRendererProps) {
return (
<ScrollArea className={cn("w-full h-full rounded-md relative", className)}>
<div className="p-4 markdown prose prose-sm dark:prose-invert max-w-none">
<ReactMarkdown
rehypePlugins={[rehypeRaw, rehypeSanitize]}
components={{
code({ node, inline, className, children, ...props }) {
const match = /language-(\w+)/.exec(className || "");
if (inline) {
return (
<code className={className} {...props}>
{children}
</code>
);
}
return (
<CodeRenderer
content={String(children).replace(/\n$/, "")}
language={match ? match[1] : ""}
/>
);
},
// Style other elements as needed
h1: ({ node, ...props }) => <h1 className="text-2xl font-bold my-4" {...props} />,
h2: ({ node, ...props }) => <h2 className="text-xl font-bold my-3" {...props} />,
h3: ({ node, ...props }) => <h3 className="text-lg font-bold my-2" {...props} />,
a: ({ node, ...props }) => <a className="text-primary hover:underline" {...props} />,
p: ({ node, ...props }) => <p className="my-2" {...props} />,
ul: ({ node, ...props }) => <ul className="list-disc pl-5 my-2" {...props} />,
ol: ({ node, ...props }) => <ol className="list-decimal pl-5 my-2" {...props} />,
li: ({ node, ...props }) => <li className="my-1" {...props} />,
blockquote: ({ node, ...props }) => (
<blockquote className="border-l-4 border-muted pl-4 italic my-2" {...props} />
),
img: ({ node, ...props }) => (
<img className="max-w-full h-auto rounded-md my-2" {...props} alt={props.alt || ""} />
),
pre: ({ node, ...props }) => <pre className="p-0 my-2 bg-transparent" {...props} />,
}}
>
{content}
</ReactMarkdown>
</div>
</ScrollArea>
);
}

View File

@ -0,0 +1,125 @@
"use client";
import React, { useState } from "react";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import {
ZoomIn,
ZoomOut,
RotateCw,
Download,
ArrowLeft,
ArrowRight,
Fullscreen
} from "lucide-react";
interface PdfRendererProps {
url: string;
className?: string;
}
export function PdfRenderer({ url, className }: PdfRendererProps) {
// State for zoom and rotation controls
const [zoom, setZoom] = useState(100);
const [rotation, setRotation] = useState(0);
// Handle zoom in/out
const handleZoomIn = () => setZoom(prev => Math.min(prev + 25, 200));
const handleZoomOut = () => setZoom(prev => Math.max(prev - 25, 50));
// Handle rotation
const handleRotate = () => setRotation(prev => (prev + 90) % 360);
// Handle download
const handleDownload = () => {
const link = document.createElement('a');
link.href = url;
link.download = url.split('/').pop() || 'document.pdf';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
// Handle fullscreen
const handleFullscreen = () => {
const iframe = document.querySelector('iframe');
if (iframe) {
if (iframe.requestFullscreen) {
iframe.requestFullscreen();
}
}
};
return (
<div className={cn("flex flex-col w-full h-full", className)}>
{/* Controls */}
<div className="flex items-center justify-between py-2 px-4 bg-muted/30 border-b mb-2 rounded-t-md">
<div className="flex items-center space-x-2">
<Button
variant="ghost"
size="sm"
className="h-8 w-8 p-0"
onClick={handleZoomOut}
title="Zoom out"
>
<ZoomOut className="h-4 w-4" />
</Button>
<span className="text-xs font-medium">{zoom}%</span>
<Button
variant="ghost"
size="sm"
className="h-8 w-8 p-0"
onClick={handleZoomIn}
title="Zoom in"
>
<ZoomIn className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="sm"
className="h-8 w-8 p-0"
onClick={handleRotate}
title="Rotate"
>
<RotateCw className="h-4 w-4" />
</Button>
</div>
<div className="flex items-center space-x-2">
<Button
variant="ghost"
size="sm"
className="h-8 w-8 p-0"
onClick={handleFullscreen}
title="Fullscreen"
>
<Fullscreen className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="sm"
className="h-8 w-8 p-0"
onClick={handleDownload}
title="Download"
>
<Download className="h-4 w-4" />
</Button>
</div>
</div>
{/* PDF Viewer */}
<div className="flex-1 overflow-hidden rounded-b-md bg-white">
<iframe
src={url}
className="w-full h-full border-0"
style={{
transform: `scale(${zoom / 100}) rotate(${rotation}deg)`,
transformOrigin: 'center center',
transition: 'transform 0.2s ease'
}}
title="PDF Viewer"
/>
</div>
</div>
);
}

View File

@ -20,6 +20,7 @@ import { toast } from "sonner";
import { createClient } from "@/lib/supabase/client";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Separator } from "@/components/ui/separator";
import { FileRenderer } from "@/components/file-renderers";
// Define API_URL
const API_URL = process.env.NEXT_PUBLIC_BACKEND_URL || '';
@ -367,103 +368,18 @@ export function FileViewerModal({
}
};
// Render file content based on type
const renderFileContent = () => {
if (isLoadingContent) {
return (
<div className="space-y-3">
{[1, 2, 3, 4, 5].map((i) => (
<Skeleton key={i} className="h-5 w-full" />
))}
</div>
);
}
if (!selectedFile) {
return (
<div className="flex flex-col items-center justify-center min-h-[400px] text-muted-foreground">
<File className="h-16 w-16 mb-4 opacity-30" />
<p className="text-sm font-medium">Select a file to view its contents</p>
<p className="text-sm text-muted-foreground mt-1">Choose a file from the sidebar to preview or edit</p>
</div>
);
}
if (fileType === 'text' && fileContent) {
return (
<div className="w-full">
<pre className="text-sm font-mono whitespace-pre-wrap break-words leading-relaxed bg-muted/30 p-4 rounded-lg">
{fileContent}
</pre>
</div>
);
}
if (fileType === 'image' && binaryFileUrl) {
return (
<div className="flex flex-col items-center justify-center min-h-[400px] bg-muted/30 rounded-lg p-6">
<img
src={binaryFileUrl}
alt={selectedFile}
className="max-w-full object-contain rounded-md shadow-sm ring-1 ring-border/10"
/>
</div>
);
}
if (fileType === 'pdf' && binaryFileUrl) {
return (
<div className="w-full h-[600px]">
<iframe
src={binaryFileUrl}
className="w-full h-full border-0 rounded-lg shadow-sm bg-white ring-1 ring-border/10"
title={selectedFile}
/>
</div>
);
}
if (binaryFileUrl) {
return (
<div className="flex flex-col items-center justify-center min-h-[400px] gap-4 bg-muted/30 rounded-lg p-8">
<File className="h-24 w-24 opacity-40" />
<div className="text-center">
<p className="text-sm font-medium">Binary File</p>
<p className="text-sm text-muted-foreground mt-1">
This file cannot be previewed in the browser
</p>
</div>
<Button
size="sm"
className="mt-2"
onClick={handleDownload}
>
<Download className="h-4 w-4 mr-2" />
Download File
</Button>
</div>
);
}
return (
<div className="flex flex-col items-center justify-center min-h-[400px] text-muted-foreground">
<File className="h-16 w-16 mb-4 opacity-30" />
<p className="text-sm font-medium">No preview available</p>
<p className="text-sm text-muted-foreground mt-1">This file type cannot be previewed</p>
</div>
);
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-[1000px] h-[85vh] max-h-[800px] flex flex-col p-0 gap-0">
<DialogContent
className="sm:max-w-[90vw] md:max-w-[1200px] w-[95vw] h-[90vh] max-h-[900px] flex flex-col p-0 gap-0 overflow-hidden"
>
<DialogHeader className="px-6 py-3 border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
<DialogTitle className="text-lg font-semibold">Workspace Files</DialogTitle>
</DialogHeader>
<div className="flex flex-col sm:flex-row h-full overflow-hidden divide-x divide-border">
{/* File browser sidebar */}
<div className="w-full sm:w-80 flex flex-col h-full bg-muted/5">
<div className="w-full sm:w-[280px] lg:w-[320px] flex flex-col h-full bg-muted/5">
{/* Breadcrumb navigation */}
<div className="px-3 py-2 border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
{renderBreadcrumbs()}
@ -582,13 +498,27 @@ export function FileViewerModal({
<div className="w-full flex-1 flex flex-col h-full bg-muted/5">
{/* File content */}
<div className="flex-1 overflow-hidden">
<ScrollArea className="h-full">
<div className="p-4">
{renderFileContent()}
{isLoadingContent ? (
<div className="p-4 space-y-3">
{[1, 2, 3, 4, 5].map((i) => (
<Skeleton key={i} className="h-5 w-full" />
))}
</div>
</ScrollArea>
) : !selectedFile ? (
<div className="flex flex-col items-center justify-center min-h-[400px] text-muted-foreground">
<File className="h-16 w-16 mb-4 opacity-30" />
<p className="text-sm font-medium">Select a file to view its contents</p>
<p className="text-sm text-muted-foreground mt-1">Choose a file from the sidebar to preview or edit</p>
</div>
) : (
<FileRenderer
content={fileContent}
binaryUrl={binaryFileUrl}
fileName={selectedFile}
className="h-full"
/>
)}
</div>
</div>
</div>
</DialogContent>

View File

@ -0,0 +1,13 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: ["class"],
content: [
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
],
theme: {
extend: {},
},
plugins: [require("@tailwindcss/typography")],
}