File Viewer

A component for displaying a file viewer

Default
Default file viewer
Explorer
src
app
layout.tsx
page.tsx
globals.css
api
auth
route.ts
users
route.ts
components
ui
button.tsx
card.tsx
file-tree.tsx
header.tsx
footer.tsx
lib
utils.ts
constants.ts
hooks
use-auth.ts
use-theme.ts
public
favicon.ico
logo.svg
images
hero.png
avatar.jpg
package.json
tsconfig.json
next.config.mjs
tailwind.config.ts
.env.local
.gitignore
README.md

Select a file to view its contents

1"use client";
2
3import { FileTree, FileTreeNode } from "@/registry/ui/file-tree";
4import {
5 FileExplorer,
6 FileExplorerContent,
7 FileExplorerSidebar,
8 FileViewer,
9} from "@/registry/ui/file-viewer";
10import type { FileContent } from "@/registry/ui/file-viewer";
11import { useState } from "react";
12
13const fileContents: Record<string, string> = {
14 "src/app/layout.tsx": `import type { Metadata } from "next"
15 import { Inter } from "next/font/google"
16 import "./globals.css"
17
18 const inter = Inter({ subsets: ["latin"] })
19
20 export const metadata: Metadata = {
21 title: "My App",
22 description: "A Next.js application",
23 }
24
25 export default function RootLayout({
26 children,
27 }: {
28 children: React.ReactNode
29 }) {
30 return (
31 <html lang="en">
32 <body className={inter.className}>{children}</body>
33 </html>
34 )
35 }`,
36 "src/app/page.tsx": `export default function Home() {
37 return (
38 <main className="flex min-h-screen flex-col items-center justify-center p-24">
39 <h1 className="text-4xl font-bold">Welcome to Next.js</h1>
40 <p className="mt-4 text-lg text-muted-foreground">
41 Get started by editing app/page.tsx
42 </p>
43 </main>
44 )
45 }`,
46 "src/app/globals.css": `@tailwind base;
47 @tailwind components;
48 @tailwind utilities;
49
50 :root {
51 --foreground-rgb: 0, 0, 0;
52 --background-start-rgb: 214, 219, 220;
53 --background-end-rgb: 255, 255, 255;
54 }
55
56 @media (prefers-color-scheme: dark) {
57 :root {
58 --foreground-rgb: 255, 255, 255;
59 --background-start-rgb: 0, 0, 0;
60 --background-end-rgb: 0, 0, 0;
61 }
62 }`,
63 "src/app/api/auth/route.ts": `import { NextResponse } from "next/server"
64
65 export async function POST(request: Request) {
66 const body = await request.json()
67 const { email, password } = body
68
69 // Validate credentials
70 if (!email || !password) {
71 return NextResponse.json(
72 { error: "Missing credentials" },
73 { status: 400 }
74 )
75 }
76
77 // TODO: Implement actual authentication
78 return NextResponse.json({ success: true })
79 }`,
80 "src/app/api/users/route.ts": `import { NextResponse } from "next/server"
81
82 const users = [
83 { id: 1, name: "Alice", email: "alice@example.com" },
84 { id: 2, name: "Bob", email: "bob@example.com" },
85 ]
86
87 export async function GET() {
88 return NextResponse.json(users)
89 }
90
91 export async function POST(request: Request) {
92 const body = await request.json()
93 const newUser = { id: users.length + 1, ...body }
94 users.push(newUser)
95 return NextResponse.json(newUser, { status: 201 })
96 }`,
97 "src/components/ui/button.tsx": `import * as React from "react"
98 import { cva, type VariantProps } from "class-variance-authority"
99 import { cn } from "@/lib/utils"
100
101 const buttonVariants = cva(
102 "inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
103 {
104 variants: {
105 variant: {
106 default: "bg-primary text-primary-foreground hover:bg-primary/90",
107 destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
108 outline: "border border-input hover:bg-accent hover:text-accent-foreground",
109 secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
110 ghost: "hover:bg-accent hover:text-accent-foreground",
111 link: "underline-offset-4 hover:underline text-primary",
112 },
113 size: {
114 default: "h-10 px-4 py-2",
115 sm: "h-9 rounded-md px-3",
116 lg: "h-11 rounded-md px-8",
117 icon: "h-10 w-10",
118 },
119 },
120 defaultVariants: {
121 variant: "default",
122 size: "default",
123 },
124 }
125 )
126
127 export interface ButtonProps
128 extends React.ButtonHTMLAttributes<HTMLButtonElement>,
129 VariantProps<typeof buttonVariants> {}
130
131 const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
132 ({ className, variant, size, ...props }, ref) => {
133 return (
134 <button
135 className={cn(buttonVariants({ variant, size, className }))}
136 ref={ref}
137 {...props}
138 />
139 )
140 }
141 )
142 Button.displayName = "Button"
143
144 export { Button, buttonVariants }`,
145 "src/components/ui/card.tsx": `import * as React from "react"
146 import { cn } from "@/lib/utils"
147
148 const Card = React.forwardRef<
149 HTMLDivElement,
150 React.HTMLAttributes<HTMLDivElement>
151 >(({ className, ...props }, ref) => (
152 <div
153 ref={ref}
154 className={cn(
155 "rounded-lg border bg-card text-card-foreground shadow-sm",
156 className
157 )}
158 {...props}
159 />
160 ))
161 Card.displayName = "Card"
162
163 const CardHeader = React.forwardRef<
164 HTMLDivElement,
165 React.HTMLAttributes<HTMLDivElement>
166 >(({ className, ...props }, ref) => (
167 <div
168 ref={ref}
169 className={cn("flex flex-col space-y-1.5 p-6", className)}
170 {...props}
171 />
172 ))
173 CardHeader.displayName = "CardHeader"
174
175 const CardTitle = React.forwardRef<
176 HTMLParagraphElement,
177 React.HTMLAttributes<HTMLHeadingElement>
178 >(({ className, ...props }, ref) => (
179 <h3
180 ref={ref}
181 className={cn("text-2xl font-semibold leading-none tracking-tight", className)}
182 {...props}
183 />
184 ))
185 CardTitle.displayName = "CardTitle"
186
187 export { Card, CardHeader, CardTitle }`,
188 "src/components/ui/file-tree.tsx": `// See the actual file-tree.tsx component
189 // This is a simplified preview
190
191 import { cva } from "class-variance-authority"
192
193 export const fileTreeVariants = cva(
194 "font-mono text-sm select-none",
195 {
196 variants: {
197 variant: {
198 default: "bg-background text-foreground",
199 ghost: "bg-transparent",
200 bordered: "bg-background border border-border rounded-lg p-2",
201 elevated: "bg-card text-card-foreground shadow-md rounded-lg p-3",
202 },
203 },
204 }
205 )`,
206 "src/components/header.tsx": `import Link from "next/link"
207 import { Button } from "@/components/ui/button"
208
209 export function Header() {
210 return (
211 <header className="sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur">
212 <div className="container flex h-14 items-center">
213 <Link href="/" className="mr-6 flex items-center space-x-2">
214 <span className="font-bold">My App</span>
215 </Link>
216 <nav className="flex flex-1 items-center space-x-6 text-sm font-medium">
217 <Link href="/about">About</Link>
218 <Link href="/docs">Docs</Link>
219 </nav>
220 <Button size="sm">Sign In</Button>
221 </div>
222 </header>
223 )
224 }`,
225 "src/components/footer.tsx": `export function Footer() {
226 return (
227 <footer className="border-t py-6 md:py-0">
228 <div className="container flex flex-col items-center justify-between gap-4 md:h-24 md:flex-row">
229 <p className="text-center text-sm leading-loose text-muted-foreground md:text-left">
230 Built with Next.js and Tailwind CSS.
231 </p>
232 </div>
233 </footer>
234 )
235 }`,
236 "src/lib/utils.ts": `import { type ClassValue, clsx } from "clsx"
237 import { twMerge } from "tailwind-merge"
238
239 export function cn(...inputs: ClassValue[]) {
240 return twMerge(clsx(inputs))
241 }`,
242 "src/lib/constants.ts": `export const APP_NAME = "My Application"
243 export const APP_VERSION = "1.0.0"
244 export const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3000/api"
245
246 export const ROUTES = {
247 home: "/",
248 about: "/about",
249 dashboard: "/dashboard",
250 settings: "/settings",
251 } as const`,
252 "src/hooks/use-auth.ts": `import { useState, useEffect, useCallback } from "react"
253
254 interface User {
255 id: string
256 email: string
257 name: string
258 }
259
260 export function useAuth() {
261 const [user, setUser] = useState<User | null>(null)
262 const [loading, setLoading] = useState(true)
263
264 useEffect(() => {
265 // Check for existing session
266 const checkAuth = async () => {
267 try {
268 const response = await fetch("/api/auth/me")
269 if (response.ok) {
270 const userData = await response.json()
271 setUser(userData)
272 }
273 } catch (error) {
274 console.error("Auth check failed:", error)
275 } finally {
276 setLoading(false)
277 }
278 }
279 checkAuth()
280 }, [])
281
282 const signOut = useCallback(async () => {
283 await fetch("/api/auth/signout", { method: "POST" })
284 setUser(null)
285 }, [])
286
287 return { user, loading, signOut }
288 }`,
289 "src/hooks/use-theme.ts": `import { useState, useEffect } from "react"
290
291 type Theme = "light" | "dark" | "system"
292
293 export function useTheme() {
294 const [theme, setTheme] = useState<Theme>("system")
295
296 useEffect(() => {
297 const stored = localStorage.getItem("theme") as Theme | null
298 if (stored) {
299 setTheme(stored)
300 }
301 }, [])
302
303 useEffect(() => {
304 const root = document.documentElement
305 root.classList.remove("light", "dark")
306
307 if (theme === "system") {
308 const systemTheme = window.matchMedia("(prefers-color-scheme: dark)").matches
309 ? "dark"
310 : "light"
311 root.classList.add(systemTheme)
312 } else {
313 root.classList.add(theme)
314 }
315
316 localStorage.setItem("theme", theme)
317 }, [theme])
318
319 return { theme, setTheme }
320 }`,
321 "package.json": `{
322 "name": "my-nextjs-app",
323 "version": "0.1.0",
324 "private": true,
325 "scripts": {
326 "dev": "next dev",
327 "build": "next build",
328 "start": "next start",
329 "lint": "next lint"
330 },
331 "dependencies": {
332 "next": "14.2.0",
333 "react": "^18",
334 "react-dom": "^18",
335 "class-variance-authority": "^0.7.0",
336 "clsx": "^2.1.0",
337 "tailwind-merge": "^2.2.0",
338 "lucide-react": "^0.344.0"
339 },
340 "devDependencies": {
341 "typescript": "^5",
342 "@types/node": "^20",
343 "@types/react": "^18",
344 "@types/react-dom": "^18",
345 "tailwindcss": "^3.4.1",
346 "postcss": "^8"
347 }
348 }`,
349 "tsconfig.json": `{
350 "compilerOptions": {
351 "lib": ["dom", "dom.iterable", "esnext"],
352 "allowJs": true,
353 "skipLibCheck": true,
354 "strict": true,
355 "noEmit": true,
356 "esModuleInterop": true,
357 "module": "esnext",
358 "moduleResolution": "bundler",
359 "resolveJsonModule": true,
360 "isolatedModules": true,
361 "jsx": "preserve",
362 "incremental": true,
363 "plugins": [{ "name": "next" }],
364 "paths": {
365 "@/*": ["./*"]
366 }
367 },
368 "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
369 "exclude": ["node_modules"]
370 }`,
371 "next.config.mjs": `/** @type {import('next').NextConfig} */
372 const nextConfig = {
373 reactStrictMode: true,
374 images: {
375 domains: [],
376 },
377 }
378
379 export default nextConfig`,
380 "tailwind.config.ts": `import type { Config } from "tailwindcss"
381
382 const config: Config = {
383 darkMode: ["class"],
384 content: [
385 "./pages/**/*.{js,ts,jsx,tsx,mdx}",
386 "./components/**/*.{js,ts,jsx,tsx,mdx}",
387 "./app/**/*.{js,ts,jsx,tsx,mdx}",
388 ],
389 theme: {
390 extend: {
391 colors: {
392 border: "hsl(var(--border))",
393 background: "hsl(var(--background))",
394 foreground: "hsl(var(--foreground))",
395 },
396 },
397 },
398 plugins: [],
399 }
400 export default config`,
401 ".env.local": `# Environment Variables
402 DATABASE_URL="postgresql://user:password@localhost:5432/mydb"
403 NEXT_PUBLIC_API_URL="http://localhost:3000/api"
404 AUTH_SECRET="your-secret-key-here"`,
405 ".gitignore": `# Dependencies
406 node_modules
407 .pnp
408 .pnp.js
409
410 # Testing
411 coverage
412
413 # Next.js
414 .next/
415 out/
416
417 # Production
418 build
419
420 # Misc
421 .DS_Store
422 *.pem
423
424 # Debug
425 npm-debug.log*
426
427 # Local env files
428 .env*.local
429
430 # Vercel
431 .vercel
432
433 # TypeScript
434 *.tsbuildinfo
435 next-env.d.ts`,
436 "README.md": `# My Next.js App
437
438 A modern web application built with Next.js 14, React, and Tailwind CSS.
439
440 ## Getting Started
441
442 First, install dependencies:
443
444 \`\`\`bash
445 npm install
446 # or
447 yarn install
448 # or
449 pnpm install
450 \`\`\`
451
452 Then, run the development server:
453
454 \`\`\`bash
455 npm run dev
456 \`\`\`
457
458 Open [http://localhost:3000](http://localhost:3000) to see the result.
459
460 ## Features
461
462 - Next.js 14 App Router
463 - TypeScript
464 - Tailwind CSS
465 - Component library with CVA variants
466 `,
467};
468
469const sampleData: FileTreeNode[] = [
470 {
471 name: "src",
472 type: "folder",
473 children: [
474 {
475 name: "app",
476 type: "folder",
477 children: [
478 { name: "layout.tsx", type: "file" },
479 { name: "page.tsx", type: "file" },
480 { name: "globals.css", type: "file" },
481 {
482 name: "api",
483 type: "folder",
484 children: [
485 {
486 name: "auth",
487 type: "folder",
488 children: [{ name: "route.ts", type: "file" }],
489 },
490 {
491 name: "users",
492 type: "folder",
493 children: [{ name: "route.ts", type: "file" }],
494 },
495 ],
496 },
497 ],
498 },
499 {
500 name: "components",
501 type: "folder",
502 children: [
503 {
504 name: "ui",
505 type: "folder",
506 children: [
507 { name: "button.tsx", type: "file" },
508 { name: "card.tsx", type: "file" },
509 { name: "file-tree.tsx", type: "file" },
510 ],
511 },
512 { name: "header.tsx", type: "file" },
513 { name: "footer.tsx", type: "file" },
514 ],
515 },
516 {
517 name: "lib",
518 type: "folder",
519 children: [
520 { name: "utils.ts", type: "file" },
521 { name: "constants.ts", type: "file" },
522 ],
523 },
524 {
525 name: "hooks",
526 type: "folder",
527 children: [
528 { name: "use-auth.ts", type: "file" },
529 { name: "use-theme.ts", type: "file" },
530 ],
531 },
532 ],
533 },
534 {
535 name: "public",
536 type: "folder",
537 children: [
538 { name: "favicon.ico", type: "file" },
539 { name: "logo.svg", type: "file" },
540 {
541 name: "images",
542 type: "folder",
543 children: [
544 { name: "hero.png", type: "file" },
545 { name: "avatar.jpg", type: "file" },
546 ],
547 },
548 ],
549 },
550 { name: "package.json", type: "file" },
551 { name: "tsconfig.json", type: "file" },
552 { name: "next.config.mjs", type: "file" },
553 { name: "tailwind.config.ts", type: "file" },
554 { name: ".env.local", type: "file" },
555 { name: ".gitignore", type: "file" },
556 { name: "README.md", type: "file" },
557];
558
559export const FileViewerDefault = () => {
560 const [selectedPath, setSelectedPath] = useState<string>("");
561 const [selectedFile, setSelectedFile] = useState<FileContent | null>(null);
562
563 const handleSelect = (node: FileTreeNode, path: string) => {
564 setSelectedPath(path);
565
566 if (node.type === "file") {
567 const content = fileContents[path];
568 if (content) {
569 setSelectedFile({
570 path,
571 name: node.name,
572 content,
573 });
574 } else {
575 setSelectedFile({
576 path,
577 name: node.name,
578 content: `// Content for ${node.name} not available in demo`,
579 });
580 }
581 }
582 };
583
584 const handleClose = () => {
585 setSelectedFile(null);
586 setSelectedPath("");
587 };
588 return (
589 <div className="w-full">
590 <FileExplorer
591 variant="elevated"
592 rounded="lg"
593 sidebarWidth="300px"
594 className="h-[600px]"
595 >
596 <FileExplorerSidebar className="p-3">
597 <div className="mb-3 px-2 text-xs font-semibold uppercase tracking-wider text-muted-foreground">
598 Explorer
599 </div>
600 <FileTree
601 data={sampleData}
602 variant="ghost"
603 size="default"
604 onSelect={handleSelect}
605 selectedPath={selectedPath}
606 />
607 </FileExplorerSidebar>
608 <FileExplorerContent>
609 <FileViewer
610 file={selectedFile}
611 variant="ghost"
612 size="default"
613 rounded="none"
614 maxHeight="100%"
615 onClose={handleClose}
616 className="h-full"
617 />
618 </FileExplorerContent>
619 </FileExplorer>
620 </div>
621 );
622};
623
624export const FileViewerStandalone = () => {
625 return (
626 <section className="grid gap-8 lg:grid-cols-2">
627 <div>
628 <FileViewer
629 file={{
630 path: "src/lib/utils.ts",
631 name: "utils.ts",
632 content: fileContents["src/lib/utils.ts"],
633 }}
634 variant="bordered"
635 size="default"
636 maxHeight="300px"
637 highlightedLines={{
638 4: "modified",
639 }}
640 />
641 </div>
642 <div>
643 <FileViewer
644 file={{
645 path: "package.json",
646 name: "package.json",
647 content: fileContents["package.json"],
648 }}
649 variant="elevated"
650 size="sm"
651 maxHeight="300px"
652 />
653 </div>
654 </section>
655 );
656};
657
658export const FileViewerElevated = () => {
659 return (
660 <FileViewer
661 file={{
662 path: "package.json",
663 name: "package.json",
664 content: fileContents["package.json"],
665 }}
666 variant="elevated"
667 size="sm"
668 maxHeight="300px"
669 />
670 );
671};
Standalone
Standalone file viewer
src/lib/utils.ts
1import { type ClassValue, clsx } from "clsx"
2 import { twMerge } from "tailwind-merge"
3
4 export function cn(...inputs: ClassValue[]) {
5 return twMerge(clsx(inputs))
6 }
package.json
1{
2 "name": "my-nextjs-app",
3 "version": "0.1.0",
4 "private": true,
5 "scripts": {
6 "dev": "next dev",
7 "build": "next build",
8 "start": "next start",
9 "lint": "next lint"
10 },
11 "dependencies": {
12 "next": "14.2.0",
13 "react": "^18",
14 "react-dom": "^18",
15 "class-variance-authority": "^0.7.0",
16 "clsx": "^2.1.0",
17 "tailwind-merge": "^2.2.0",
18 "lucide-react": "^0.344.0"
19 },
20 "devDependencies": {
21 "typescript": "^5",
22 "@types/node": "^20",
23 "@types/react": "^18",
24 "@types/react-dom": "^18",
25 "tailwindcss": "^3.4.1",
26 "postcss": "^8"
27 }
28 }
1import { FileViewerStandalone } from "@/components/examples/file-viewer-examples"
2
3<FileViewerStandalone />

Props

NameTypeDefaultDescription
fileFileContentundefinedThe file to display
showLineNumbersbooleantrueShow line numbers
highlightedLinesRecord<number, string>{}Highlighted lines
onClose() => voidundefinedCallback when the file viewer is closed
showHeaderbooleantrueShow the header
maxHeightstring100%The maximum height of the file viewer
emptyMessagestringSelect a file to view its contentsThe message to display when no file is selected