File Viewer
Displays file content with an optional FileExplorer layout that combines a file tree sidebar and content panel. Supports a collapsible sidebar when used with FileTree.
Default
File tree and viewer with collapsible explorer sidebar
Select a file to view its contents
default-example.tsx
1"use client";23import {4 FileTree,5 FileTreeNode,6 FileExplorer,7 FileExplorerContent,8 FileExplorerSidebar,9 FileExplorerSidebarToggle,10 useFileExplorer,11 FileViewer,12 FileContent,13} from "@/registry/ui";14import { useState } from "react";1516function FileExplorerSidebarToggleWhen({17 when,18 className,19}: {20 when: "open" | "closed";21 className?: string;22}) {23 const { collapsible, sidebarOpen } = useFileExplorer();24 if (!collapsible) return null;25 if (when === "open" && !sidebarOpen) return null;26 if (when === "closed" && sidebarOpen) return null;27 return <FileExplorerSidebarToggle className={className} />;28}2930const fileContents: Record<string, string> = {31 "src/app/layout.tsx": `import type { Metadata } from "next"32import { Inter } from "next/font/google"33import "./globals.css"3435const inter = Inter({ subsets: ["latin"] })3637export const metadata: Metadata = {38 title: "My App",39 description: "A Next.js application",40}4142export default function RootLayout({43 children,44}: {45 children: React.ReactNode46}) {47 return (48 <html lang="en">49 <body className={inter.className}>{children}</body>50 </html>51 )52}`,53 "src/app/page.tsx": `export default function Home() {54 return (55 <main className="flex min-h-screen flex-col items-center justify-center p-24">56 <h1 className="text-4xl font-bold">Welcome to Next.js</h1>57 <p className="mt-4 text-lg text-muted-foreground">58 Get started by editing app/page.tsx59 </p>60 </main>61 )62 }`,63 "src/app/globals.css": `@tailwind base;64 @tailwind components;65 @tailwind utilities;6667 :root {68 --foreground-rgb: 0, 0, 0;69 --background-start-rgb: 214, 219, 220;70 --background-end-rgb: 255, 255, 255;71 }7273 @media (prefers-color-scheme: dark) {74 :root {75 --foreground-rgb: 255, 255, 255;76 --background-start-rgb: 0, 0, 0;77 --background-end-rgb: 0, 0, 0;78 }79 }`,80 "src/app/api/auth/route.ts": `import { NextResponse } from "next/server"8182 export async function POST(request: Request) {83 const body = await request.json()84 const { email, password } = body8586 // Validate credentials87 if (!email || !password) {88 return NextResponse.json(89 { error: "Missing credentials" },90 { status: 400 }91 )92 }9394 // TODO: Implement actual authentication95 return NextResponse.json({ success: true })96 }`,97 "src/app/api/users/route.ts": `import { NextResponse } from "next/server"9899 const users = [100 { id: 1, name: "Alice", email: "alice@example.com" },101 { id: 2, name: "Bob", email: "bob@example.com" },102 ]103104 export async function GET() {105 return NextResponse.json(users)106 }107108 export async function POST(request: Request) {109 const body = await request.json()110 const newUser = { id: users.length + 1, ...body }111 users.push(newUser)112 return NextResponse.json(newUser, { status: 201 })113 }`,114 "src/components/ui/button.tsx": `import * as React from "react"115 import { cva, type VariantProps } from "class-variance-authority"116 import { cn } from "@/lib/utils"117118 const buttonVariants = cva(119 "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",120 {121 variants: {122 variant: {123 default: "bg-primary text-primary-foreground hover:bg-primary/90",124 destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",125 outline: "border border-input hover:bg-accent hover:text-accent-foreground",126 secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",127 ghost: "hover:bg-accent hover:text-accent-foreground",128 link: "underline-offset-4 hover:underline text-primary",129 },130 size: {131 default: "h-10 px-4 py-2",132 sm: "h-9 rounded-md px-3",133 lg: "h-11 rounded-md px-8",134 icon: "h-10 w-10",135 },136 },137 defaultVariants: {138 variant: "default",139 size: "default",140 },141 }142 )143144 export interface ButtonProps145 extends React.ButtonHTMLAttributes<HTMLButtonElement>,146 VariantProps<typeof buttonVariants> {}147148 const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(149 function Button({ className, variant, size, ...props }, ref) {150 return (151 <button152 className={cn(buttonVariants({ variant, size, className }))}153 ref={ref}154 {...props}155 />156 )157 }158 )159160 export { Button, buttonVariants }`,161 "src/components/ui/card.tsx": `import * as React from "react"162 import { cn } from "@/lib/utils"163164 const Card = React.forwardRef<165 HTMLDivElement,166 React.HTMLAttributes<HTMLDivElement>167 >(function Card({ className, ...props }, ref) {168 return (169 <div170 ref={ref}171 className={cn(172 "rounded-lg border bg-card text-card-foreground shadow-sm",173 className174 )}175 {...props}176 />177 )178 })179180 const CardHeader = React.forwardRef<181 HTMLDivElement,182 React.HTMLAttributes<HTMLDivElement>183 >(function CardHeader({ className, ...props }, ref) {184 return (185 <div186 ref={ref}187 className={cn("flex flex-col space-y-1.5 p-6", className)}188 {...props}189 />190 )191 })192193 const CardTitle = React.forwardRef<194 HTMLParagraphElement,195 React.HTMLAttributes<HTMLHeadingElement>196 >(function CardTitle({ className, ...props }, ref) {197 return (198 <h3199 ref={ref}200 className={cn("text-2xl font-semibold leading-none tracking-tight", className)}201 {...props}202 />203 )204 })205206 export { Card, CardHeader, CardTitle }`,207 "src/components/ui/file-tree.tsx": `// See the actual file-tree.tsx component208 // This is a simplified preview209210 import { cva } from "class-variance-authority"211212 export const fileTreeVariants = cva(213 "font-mono text-sm select-none",214 {215 variants: {216 variant: {217 default: "bg-background text-foreground",218 ghost: "bg-transparent",219 bordered: "bg-background border border-border rounded-lg p-2",220 elevated: "bg-card text-card-foreground shadow-md rounded-lg p-3",221 },222 },223 }224 )`,225 "src/components/header.tsx": `import Link from "next/link"226 import { Button } from "@/components/ui/button"227228 export function Header() {229 return (230 <header className="sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur">231 <div className="container flex h-14 items-center">232 <Link href="/" className="mr-6 flex items-center space-x-2">233 <span className="font-bold">My App</span>234 </Link>235 <nav className="flex flex-1 items-center space-x-6 text-sm font-medium">236 <Link href="/about">About</Link>237 <Link href="/docs">Docs</Link>238 </nav>239 <Button size="sm">Sign In</Button>240 </div>241 </header>242 )243 }`,244 "src/components/footer.tsx": `export function Footer() {245 return (246 <footer className="border-t py-6 md:py-0">247 <div className="container flex flex-col items-center justify-between gap-4 md:h-24 md:flex-row">248 <p className="text-center text-sm leading-loose text-muted-foreground md:text-left">249 Built with Next.js and Tailwind CSS.250 </p>251 </div>252 </footer>253 )254 }`,255 "src/lib/utils.ts": `import { type ClassValue, clsx } from "clsx"256 import { twMerge } from "tailwind-merge"257258 export function cn(...inputs: ClassValue[]) {259 return twMerge(clsx(inputs))260 }`,261 "src/lib/constants.ts": `export const APP_NAME = "My Application"262 export const APP_VERSION = "1.0.0"263 export const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3000/api"264265 export const ROUTES = {266 home: "/",267 about: "/about",268 dashboard: "/dashboard",269 settings: "/settings",270 } as const`,271 "src/hooks/use-auth.ts": `import { useState, useEffect, useCallback } from "react"272273 interface User {274 id: string275 email: string276 name: string277 }278279 export function useAuth() {280 const [user, setUser] = useState<User | null>(null)281 const [loading, setLoading] = useState(true)282283 useEffect(() => {284 // Check for existing session285 const checkAuth = async () => {286 try {287 const response = await fetch("/api/auth/me")288 if (response.ok) {289 const userData = await response.json()290 setUser(userData)291 }292 } catch (error) {293 console.error("Auth check failed:", error)294 } finally {295 setLoading(false)296 }297 }298 checkAuth()299 }, [])300301 const signOut = useCallback(async () => {302 await fetch("/api/auth/signout", { method: "POST" })303 setUser(null)304 }, [])305306 return { user, loading, signOut }307 }`,308 "src/hooks/use-theme.ts": `import { useState, useEffect } from "react"309310 type Theme = "light" | "dark" | "system"311312 export function useTheme() {313 const [theme, setTheme] = useState<Theme>("system")314315 useEffect(() => {316 const stored = localStorage.getItem("theme") as Theme | null317 if (stored) {318 setTheme(stored)319 }320 }, [])321322 useEffect(() => {323 const root = document.documentElement324 root.classList.remove("light", "dark")325326 if (theme === "system") {327 const systemTheme = window.matchMedia("(prefers-color-scheme: dark)").matches328 ? "dark"329 : "light"330 root.classList.add(systemTheme)331 } else {332 root.classList.add(theme)333 }334335 localStorage.setItem("theme", theme)336 }, [theme])337338 return { theme, setTheme }339 }`,340 "package.json": `{341 "name": "my-nextjs-app",342 "version": "0.1.0",343 "private": true,344 "scripts": {345 "dev": "next dev",346 "build": "next build",347 "start": "next start",348 "lint": "next lint"349 },350 "dependencies": {351 "next": "14.2.0",352 "react": "^18",353 "react-dom": "^18",354 "class-variance-authority": "^0.7.0",355 "clsx": "^2.1.0",356 "tailwind-merge": "^2.2.0",357 "lucide-react": "^0.344.0"358 },359 "devDependencies": {360 "typescript": "^5",361 "@types/node": "^20",362 "@types/react": "^18",363 "@types/react-dom": "^18",364 "tailwindcss": "^3.4.1",365 "postcss": "^8"366 }367 }`,368 "tsconfig.json": `{369 "compilerOptions": {370 "lib": ["dom", "dom.iterable", "esnext"],371 "allowJs": true,372 "skipLibCheck": true,373 "strict": true,374 "noEmit": true,375 "esModuleInterop": true,376 "module": "esnext",377 "moduleResolution": "bundler",378 "resolveJsonModule": true,379 "isolatedModules": true,380 "jsx": "preserve",381 "incremental": true,382 "plugins": [{ "name": "next" }],383 "paths": {384 "@/*": ["./*"]385 }386 },387 "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],388 "exclude": ["node_modules"]389 }`,390 "next.config.mjs": `/** @type {import('next').NextConfig} */391 const nextConfig = {392 reactStrictMode: true,393 images: {394 domains: [],395 },396 }397398 export default nextConfig`,399 "tailwind.config.ts": `import type { Config } from "tailwindcss"400401 const config: Config = {402 darkMode: ["class"],403 content: [404 "./pages/**/*.{js,ts,jsx,tsx,mdx}",405 "./components/**/*.{js,ts,jsx,tsx,mdx}",406 "./app/**/*.{js,ts,jsx,tsx,mdx}",407 ],408 theme: {409 extend: {410 colors: {411 border: "hsl(var(--border))",412 background: "hsl(var(--background))",413 foreground: "hsl(var(--foreground))",414 },415 },416 },417 plugins: [],418 }419 export default config`,420 ".env.local": `# Environment Variables421 DATABASE_URL="postgresql://user:password@localhost:5432/mydb"422 NEXT_PUBLIC_API_URL="http://localhost:3000/api"423 AUTH_SECRET="your-secret-key-here"`,424 ".gitignore": `# Dependencies425 node_modules426 .pnp427 .pnp.js428429 # Testing430 coverage431432 # Next.js433 .next/434 out/435436 # Production437 build438439 # Misc440 .DS_Store441 *.pem442443 # Debug444 npm-debug.log*445446 # Local env files447 .env*.local448449 # Vercel450 .vercel451452 # TypeScript453 *.tsbuildinfo454 next-env.d.ts`,455 "README.md": `# My Next.js App456457 A modern web application built with Next.js 14, React, and Tailwind CSS.458459 ## Getting Started460461 First, install dependencies:462463 \`\`\`bash464 npm install465 # or466 yarn install467 # or468 pnpm install469 \`\`\`470471 Then, run the development server:472473 \`\`\`bash474 npm run dev475 \`\`\`476477 Open [http://localhost:3000](http://localhost:3000) to see the result.478479 ## Features480481 - Next.js 14 App Router482 - TypeScript483 - Tailwind CSS484 - Component library with CVA variants485 `,486};487488const sampleData: FileTreeNode[] = [489 {490 name: "src",491 type: "folder",492 children: [493 {494 name: "app",495 type: "folder",496 children: [497 { name: "layout.tsx", type: "file" },498 { name: "page.tsx", type: "file" },499 { name: "globals.css", type: "file" },500 {501 name: "api",502 type: "folder",503 children: [504 {505 name: "auth",506 type: "folder",507 children: [{ name: "route.ts", type: "file" }],508 },509 {510 name: "users",511 type: "folder",512 children: [{ name: "route.ts", type: "file" }],513 },514 ],515 },516 ],517 },518 {519 name: "components",520 type: "folder",521 children: [522 {523 name: "ui",524 type: "folder",525 children: [526 { name: "button.tsx", type: "file" },527 { name: "card.tsx", type: "file" },528 { name: "file-tree.tsx", type: "file" },529 ],530 },531 { name: "header.tsx", type: "file" },532 { name: "footer.tsx", type: "file" },533 ],534 },535 {536 name: "lib",537 type: "folder",538 children: [539 { name: "utils.ts", type: "file" },540 { name: "constants.ts", type: "file" },541 ],542 },543 {544 name: "hooks",545 type: "folder",546 children: [547 { name: "use-auth.ts", type: "file" },548 { name: "use-theme.ts", type: "file" },549 ],550 },551 ],552 },553 {554 name: "public",555 type: "folder",556 children: [557 { name: "favicon.ico", type: "file" },558 { name: "logo.svg", type: "file" },559 {560 name: "images",561 type: "folder",562 children: [563 { name: "hero.png", type: "file" },564 { name: "avatar.jpg", type: "file" },565 ],566 },567 ],568 },569 { name: "package.json", type: "file" },570 { name: "tsconfig.json", type: "file" },571 { name: "next.config.mjs", type: "file" },572 { name: "tailwind.config.ts", type: "file" },573 { name: ".env.local", type: "file" },574 { name: ".gitignore", type: "file" },575 { name: "README.md", type: "file" },576];577578const imagePreviewUrls: Record<string, string> = {579 "public/images/hero.png":580 "https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?q=80&w=1200&auto=format&fit=crop",581 "public/images/avatar.jpg":582 "https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?q=80&w=400&auto=format&fit=crop",583 "public/logo.svg": "/file.svg",584 "public/favicon.ico": "https://picsum.photos/seed/favicon/64/64",585};586587export function FileViewerDefault() {588 const [selectedPath, setSelectedPath] = useState<string>("");589 const [selectedFile, setSelectedFile] = useState<FileContent | null>(null);590591 const handleSelect = (node: FileTreeNode, path: string) => {592 setSelectedPath(path);593594 if (node.type === "file") {595 const content = fileContents[path];596 const previewUrl = imagePreviewUrls[path];597 if (content) {598 setSelectedFile({599 path,600 name: node.name,601 content,602 previewUrl,603 });604 } else {605 setSelectedFile({606 path,607 name: node.name,608 content: previewUrl609 ? ""610 : `// Content for ${node.name} not available in demo`,611 previewUrl,612 });613 }614 }615 };616617 const handleClose = () => {618 setSelectedFile(null);619 setSelectedPath("");620 };621 return (622 <div className="w-full">623 <FileExplorer624 variant="elevated"625 rounded="lg"626 sidebarWidth="300px"627 collapsible628 className="h-[600px]"629 >630 <FileExplorerSidebar className="p-3">631 <div className="mb-3 flex items-center justify-between gap-2 px-2">632 <span className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">633 Explorer634 </span>635 <FileExplorerSidebarToggleWhen when="open" />636 </div>637 <FileTree638 data={sampleData}639 variant="ghost"640 size="default"641 onSelect={handleSelect}642 selectedPath={selectedPath}643 />644 </FileExplorerSidebar>645 <FileExplorerContent>646 <FileExplorerSidebarToggleWhen647 when="closed"648 className="absolute left-2 top-2 z-10"649 />650 <FileViewer651 file={selectedFile}652 variant="ghost"653 size="default"654 rounded="none"655 maxHeight="100%"656 onClose={handleClose}657 className="h-full"658 />659 </FileExplorerContent>660 </FileExplorer>661 </div>662 );663}
Standalone
Standalone file viewer
src/lib/utils.ts
1import { type ClassValue, clsx } from "clsx"2 import { twMerge } from "tailwind-merge"34 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 }
standalone-example.tsx
1"use client";23import { FileViewer } from "@/registry/ui";45const fileContents: Record<string, string> = {6 "src/app/layout.tsx": `import type { Metadata } from "next"7import { Inter } from "next/font/google"8import "./globals.css"910const inter = Inter({ subsets: ["latin"] })1112export const metadata: Metadata = {13 title: "My App",14 description: "A Next.js application",15}1617export default function RootLayout({18 children,19}: {20 children: React.ReactNode21}) {22 return (23 <html lang="en">24 <body className={inter.className}>{children}</body>25 </html>26 )27}`,28 "src/app/page.tsx": `export default function Home() {29 return (30 <main className="flex min-h-screen flex-col items-center justify-center p-24">31 <h1 className="text-4xl font-bold">Welcome to Next.js</h1>32 <p className="mt-4 text-lg text-muted-foreground">33 Get started by editing app/page.tsx34 </p>35 </main>36 )37 }`,38 "src/app/globals.css": `@tailwind base;39 @tailwind components;40 @tailwind utilities;4142 :root {43 --foreground-rgb: 0, 0, 0;44 --background-start-rgb: 214, 219, 220;45 --background-end-rgb: 255, 255, 255;46 }4748 @media (prefers-color-scheme: dark) {49 :root {50 --foreground-rgb: 255, 255, 255;51 --background-start-rgb: 0, 0, 0;52 --background-end-rgb: 0, 0, 0;53 }54 }`,55 "src/app/api/auth/route.ts": `import { NextResponse } from "next/server"5657 export async function POST(request: Request) {58 const body = await request.json()59 const { email, password } = body6061 // Validate credentials62 if (!email || !password) {63 return NextResponse.json(64 { error: "Missing credentials" },65 { status: 400 }66 )67 }6869 // TODO: Implement actual authentication70 return NextResponse.json({ success: true })71 }`,72 "src/app/api/users/route.ts": `import { NextResponse } from "next/server"7374 const users = [75 { id: 1, name: "Alice", email: "alice@example.com" },76 { id: 2, name: "Bob", email: "bob@example.com" },77 ]7879 export async function GET() {80 return NextResponse.json(users)81 }8283 export async function POST(request: Request) {84 const body = await request.json()85 const newUser = { id: users.length + 1, ...body }86 users.push(newUser)87 return NextResponse.json(newUser, { status: 201 })88 }`,89 "src/components/ui/button.tsx": `import * as React from "react"90 import { cva, type VariantProps } from "class-variance-authority"91 import { cn } from "@/lib/utils"9293 const buttonVariants = cva(94 "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",95 {96 variants: {97 variant: {98 default: "bg-primary text-primary-foreground hover:bg-primary/90",99 destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",100 outline: "border border-input hover:bg-accent hover:text-accent-foreground",101 secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",102 ghost: "hover:bg-accent hover:text-accent-foreground",103 link: "underline-offset-4 hover:underline text-primary",104 },105 size: {106 default: "h-10 px-4 py-2",107 sm: "h-9 rounded-md px-3",108 lg: "h-11 rounded-md px-8",109 icon: "h-10 w-10",110 },111 },112 defaultVariants: {113 variant: "default",114 size: "default",115 },116 }117 )118119 export interface ButtonProps120 extends React.ButtonHTMLAttributes<HTMLButtonElement>,121 VariantProps<typeof buttonVariants> {}122123 const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(124 function Button({ className, variant, size, ...props }, ref) {125 return (126 <button127 className={cn(buttonVariants({ variant, size, className }))}128 ref={ref}129 {...props}130 />131 )132 }133 )134135 export { Button, buttonVariants }`,136 "src/components/ui/card.tsx": `import * as React from "react"137 import { cn } from "@/lib/utils"138139 const Card = React.forwardRef<140 HTMLDivElement,141 React.HTMLAttributes<HTMLDivElement>142 >(function Card({ className, ...props }, ref) {143 return (144 <div145 ref={ref}146 className={cn(147 "rounded-lg border bg-card text-card-foreground shadow-sm",148 className149 )}150 {...props}151 />152 )153 })154155 const CardHeader = React.forwardRef<156 HTMLDivElement,157 React.HTMLAttributes<HTMLDivElement>158 >(function CardHeader({ className, ...props }, ref) {159 return (160 <div161 ref={ref}162 className={cn("flex flex-col space-y-1.5 p-6", className)}163 {...props}164 />165 )166 })167168 const CardTitle = React.forwardRef<169 HTMLParagraphElement,170 React.HTMLAttributes<HTMLHeadingElement>171 >(function CardTitle({ className, ...props }, ref) {172 return (173 <h3174 ref={ref}175 className={cn("text-2xl font-semibold leading-none tracking-tight", className)}176 {...props}177 />178 )179 })180181 export { Card, CardHeader, CardTitle }`,182 "src/components/ui/file-tree.tsx": `// See the actual file-tree.tsx component183 // This is a simplified preview184185 import { cva } from "class-variance-authority"186187 export const fileTreeVariants = cva(188 "font-mono text-sm select-none",189 {190 variants: {191 variant: {192 default: "bg-background text-foreground",193 ghost: "bg-transparent",194 bordered: "bg-background border border-border rounded-lg p-2",195 elevated: "bg-card text-card-foreground shadow-md rounded-lg p-3",196 },197 },198 }199 )`,200 "src/components/header.tsx": `import Link from "next/link"201 import { Button } from "@/components/ui/button"202203 export function Header() {204 return (205 <header className="sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur">206 <div className="container flex h-14 items-center">207 <Link href="/" className="mr-6 flex items-center space-x-2">208 <span className="font-bold">My App</span>209 </Link>210 <nav className="flex flex-1 items-center space-x-6 text-sm font-medium">211 <Link href="/about">About</Link>212 <Link href="/docs">Docs</Link>213 </nav>214 <Button size="sm">Sign In</Button>215 </div>216 </header>217 )218 }`,219 "src/components/footer.tsx": `export function Footer() {220 return (221 <footer className="border-t py-6 md:py-0">222 <div className="container flex flex-col items-center justify-between gap-4 md:h-24 md:flex-row">223 <p className="text-center text-sm leading-loose text-muted-foreground md:text-left">224 Built with Next.js and Tailwind CSS.225 </p>226 </div>227 </footer>228 )229 }`,230 "src/lib/utils.ts": `import { type ClassValue, clsx } from "clsx"231 import { twMerge } from "tailwind-merge"232233 export function cn(...inputs: ClassValue[]) {234 return twMerge(clsx(inputs))235 }`,236 "src/lib/constants.ts": `export const APP_NAME = "My Application"237 export const APP_VERSION = "1.0.0"238 export const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3000/api"239240 export const ROUTES = {241 home: "/",242 about: "/about",243 dashboard: "/dashboard",244 settings: "/settings",245 } as const`,246 "src/hooks/use-auth.ts": `import { useState, useEffect, useCallback } from "react"247248 interface User {249 id: string250 email: string251 name: string252 }253254 export function useAuth() {255 const [user, setUser] = useState<User | null>(null)256 const [loading, setLoading] = useState(true)257258 useEffect(() => {259 // Check for existing session260 const checkAuth = async () => {261 try {262 const response = await fetch("/api/auth/me")263 if (response.ok) {264 const userData = await response.json()265 setUser(userData)266 }267 } catch (error) {268 console.error("Auth check failed:", error)269 } finally {270 setLoading(false)271 }272 }273 checkAuth()274 }, [])275276 const signOut = useCallback(async () => {277 await fetch("/api/auth/signout", { method: "POST" })278 setUser(null)279 }, [])280281 return { user, loading, signOut }282 }`,283 "src/hooks/use-theme.ts": `import { useState, useEffect } from "react"284285 type Theme = "light" | "dark" | "system"286287 export function useTheme() {288 const [theme, setTheme] = useState<Theme>("system")289290 useEffect(() => {291 const stored = localStorage.getItem("theme") as Theme | null292 if (stored) {293 setTheme(stored)294 }295 }, [])296297 useEffect(() => {298 const root = document.documentElement299 root.classList.remove("light", "dark")300301 if (theme === "system") {302 const systemTheme = window.matchMedia("(prefers-color-scheme: dark)").matches303 ? "dark"304 : "light"305 root.classList.add(systemTheme)306 } else {307 root.classList.add(theme)308 }309310 localStorage.setItem("theme", theme)311 }, [theme])312313 return { theme, setTheme }314 }`,315 "package.json": `{316 "name": "my-nextjs-app",317 "version": "0.1.0",318 "private": true,319 "scripts": {320 "dev": "next dev",321 "build": "next build",322 "start": "next start",323 "lint": "next lint"324 },325 "dependencies": {326 "next": "14.2.0",327 "react": "^18",328 "react-dom": "^18",329 "class-variance-authority": "^0.7.0",330 "clsx": "^2.1.0",331 "tailwind-merge": "^2.2.0",332 "lucide-react": "^0.344.0"333 },334 "devDependencies": {335 "typescript": "^5",336 "@types/node": "^20",337 "@types/react": "^18",338 "@types/react-dom": "^18",339 "tailwindcss": "^3.4.1",340 "postcss": "^8"341 }342 }`,343 "tsconfig.json": `{344 "compilerOptions": {345 "lib": ["dom", "dom.iterable", "esnext"],346 "allowJs": true,347 "skipLibCheck": true,348 "strict": true,349 "noEmit": true,350 "esModuleInterop": true,351 "module": "esnext",352 "moduleResolution": "bundler",353 "resolveJsonModule": true,354 "isolatedModules": true,355 "jsx": "preserve",356 "incremental": true,357 "plugins": [{ "name": "next" }],358 "paths": {359 "@/*": ["./*"]360 }361 },362 "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],363 "exclude": ["node_modules"]364 }`,365 "next.config.mjs": `/** @type {import('next').NextConfig} */366 const nextConfig = {367 reactStrictMode: true,368 images: {369 domains: [],370 },371 }372373 export default nextConfig`,374 "tailwind.config.ts": `import type { Config } from "tailwindcss"375376 const config: Config = {377 darkMode: ["class"],378 content: [379 "./pages/**/*.{js,ts,jsx,tsx,mdx}",380 "./components/**/*.{js,ts,jsx,tsx,mdx}",381 "./app/**/*.{js,ts,jsx,tsx,mdx}",382 ],383 theme: {384 extend: {385 colors: {386 border: "hsl(var(--border))",387 background: "hsl(var(--background))",388 foreground: "hsl(var(--foreground))",389 },390 },391 },392 plugins: [],393 }394 export default config`,395 ".env.local": `# Environment Variables396 DATABASE_URL="postgresql://user:password@localhost:5432/mydb"397 NEXT_PUBLIC_API_URL="http://localhost:3000/api"398 AUTH_SECRET="your-secret-key-here"`,399 ".gitignore": `# Dependencies400 node_modules401 .pnp402 .pnp.js403404 # Testing405 coverage406407 # Next.js408 .next/409 out/410411 # Production412 build413414 # Misc415 .DS_Store416 *.pem417418 # Debug419 npm-debug.log*420421 # Local env files422 .env*.local423424 # Vercel425 .vercel426427 # TypeScript428 *.tsbuildinfo429 next-env.d.ts`,430 "README.md": `# My Next.js App431432 A modern web application built with Next.js 14, React, and Tailwind CSS.433434 ## Getting Started435436 First, install dependencies:437438 \`\`\`bash439 npm install440 # or441 yarn install442 # or443 pnpm install444 \`\`\`445446 Then, run the development server:447448 \`\`\`bash449 npm run dev450 \`\`\`451452 Open [http://localhost:3000](http://localhost:3000) to see the result.453454 ## Features455456 - Next.js 14 App Router457 - TypeScript458 - Tailwind CSS459 - Component library with CVA variants460 `,461};462463export function FileViewerStandalone() {464 return (465 <section className="grid gap-8 lg:grid-cols-2">466 <div>467 <FileViewer468 file={{469 path: "src/lib/utils.ts",470 name: "utils.ts",471 content: fileContents["src/lib/utils.ts"],472 }}473 variant="bordered"474 size="default"475 maxHeight="300px"476 highlightedLines={{477 4: "modified",478 }}479 />480 </div>481 <div>482 <FileViewer483 file={{484 path: "package.json",485 name: "package.json",486 content: fileContents["package.json"],487 }}488 variant="elevated"489 size="sm"490 maxHeight="300px"491 />492 </div>493 </section>494 );495}
With editor sidebar
Full workbench with activity rail, search, and extensions
Select a file to view its contents
with-editor-sidebar-example.tsx
1"use client";23import {4 FileTree,5 FileTreeNode,6 FileExplorer,7 FileExplorerContent,8 FileViewer,9 FileContent,10 EditorTabs,11 EditorTabsList,12 EditorTab,13 EditorTabsPanel,14 useEditorTabs,15 EditorSidebar,16 EditorSidebarRail,17 EditorSidebarTrigger,18 EditorSidebarContent,19 EditorSidebarPanel,20 useEditorSidebar,21} from "@/registry/ui";22import { useState, useMemo, type ReactNode } from "react";23import {24 File,25 FileCode,26 FileJson,27 FileText,28 Image as ImageIcon,29 Cog,30 Package,31 Files,32 Search,33 Blocks,34 PanelLeft,35 PanelLeftClose,36} from "lucide-react";37import { Button } from "@/components/ui/button";38import { cn } from "@/lib/utils";3940const fileContents: Record<string, string> = {41 "src/app/layout.tsx": `import type { Metadata } from "next"42import { Inter } from "next/font/google"43import "./globals.css"4445const inter = Inter({ subsets: ["latin"] })4647export const metadata: Metadata = {48 title: "My App",49 description: "A Next.js application",50}5152export default function RootLayout({53 children,54}: {55 children: React.ReactNode56}) {57 return (58 <html lang="en">59 <body className={inter.className}>{children}</body>60 </html>61 )62}`,63 "src/app/page.tsx": `export default function Home() {64 return (65 <main className="flex min-h-screen flex-col items-center justify-center p-24">66 <h1 className="text-4xl font-bold">Welcome to Next.js</h1>67 <p className="mt-4 text-lg text-muted-foreground">68 Get started by editing app/page.tsx69 </p>70 </main>71 )72 }`,73 "src/app/globals.css": `@tailwind base;74 @tailwind components;75 @tailwind utilities;7677 :root {78 --foreground-rgb: 0, 0, 0;79 --background-start-rgb: 214, 219, 220;80 --background-end-rgb: 255, 255, 255;81 }8283 @media (prefers-color-scheme: dark) {84 :root {85 --foreground-rgb: 255, 255, 255;86 --background-start-rgb: 0, 0, 0;87 --background-end-rgb: 0, 0, 0;88 }89 }`,90 "src/app/api/auth/route.ts": `import { NextResponse } from "next/server"9192 export async function POST(request: Request) {93 const body = await request.json()94 const { email, password } = body9596 // Validate credentials97 if (!email || !password) {98 return NextResponse.json(99 { error: "Missing credentials" },100 { status: 400 }101 )102 }103104 // TODO: Implement actual authentication105 return NextResponse.json({ success: true })106 }`,107 "src/app/api/users/route.ts": `import { NextResponse } from "next/server"108109 const users = [110 { id: 1, name: "Alice", email: "alice@example.com" },111 { id: 2, name: "Bob", email: "bob@example.com" },112 ]113114 export async function GET() {115 return NextResponse.json(users)116 }117118 export async function POST(request: Request) {119 const body = await request.json()120 const newUser = { id: users.length + 1, ...body }121 users.push(newUser)122 return NextResponse.json(newUser, { status: 201 })123 }`,124 "src/components/ui/button.tsx": `import * as React from "react"125 import { cva, type VariantProps } from "class-variance-authority"126 import { cn } from "@/lib/utils"127128 const buttonVariants = cva(129 "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",130 {131 variants: {132 variant: {133 default: "bg-primary text-primary-foreground hover:bg-primary/90",134 destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",135 outline: "border border-input hover:bg-accent hover:text-accent-foreground",136 secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",137 ghost: "hover:bg-accent hover:text-accent-foreground",138 link: "underline-offset-4 hover:underline text-primary",139 },140 size: {141 default: "h-10 px-4 py-2",142 sm: "h-9 rounded-md px-3",143 lg: "h-11 rounded-md px-8",144 icon: "h-10 w-10",145 },146 },147 defaultVariants: {148 variant: "default",149 size: "default",150 },151 }152 )153154 export interface ButtonProps155 extends React.ButtonHTMLAttributes<HTMLButtonElement>,156 VariantProps<typeof buttonVariants> {}157158 const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(159 function Button({ className, variant, size, ...props }, ref) {160 return (161 <button162 className={cn(buttonVariants({ variant, size, className }))}163 ref={ref}164 {...props}165 />166 )167 }168 )169170 export { Button, buttonVariants }`,171 "src/components/ui/card.tsx": `import * as React from "react"172 import { cn } from "@/lib/utils"173174 const Card = React.forwardRef<175 HTMLDivElement,176 React.HTMLAttributes<HTMLDivElement>177 >(function Card({ className, ...props }, ref) {178 return (179 <div180 ref={ref}181 className={cn(182 "rounded-lg border bg-card text-card-foreground shadow-sm",183 className184 )}185 {...props}186 />187 )188 })189190 const CardHeader = React.forwardRef<191 HTMLDivElement,192 React.HTMLAttributes<HTMLDivElement>193 >(function CardHeader({ className, ...props }, ref) {194 return (195 <div196 ref={ref}197 className={cn("flex flex-col space-y-1.5 p-6", className)}198 {...props}199 />200 )201 })202203 const CardTitle = React.forwardRef<204 HTMLParagraphElement,205 React.HTMLAttributes<HTMLHeadingElement>206 >(function CardTitle({ className, ...props }, ref) {207 return (208 <h3209 ref={ref}210 className={cn("text-2xl font-semibold leading-none tracking-tight", className)}211 {...props}212 />213 )214 })215216 export { Card, CardHeader, CardTitle }`,217 "src/components/ui/file-tree.tsx": `// See the actual file-tree.tsx component218 // This is a simplified preview219220 import { cva } from "class-variance-authority"221222 export const fileTreeVariants = cva(223 "font-mono text-sm select-none",224 {225 variants: {226 variant: {227 default: "bg-background text-foreground",228 ghost: "bg-transparent",229 bordered: "bg-background border border-border rounded-lg p-2",230 elevated: "bg-card text-card-foreground shadow-md rounded-lg p-3",231 },232 },233 }234 )`,235 "src/components/header.tsx": `import Link from "next/link"236 import { Button } from "@/components/ui/button"237238 export function Header() {239 return (240 <header className="sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur">241 <div className="container flex h-14 items-center">242 <Link href="/" className="mr-6 flex items-center space-x-2">243 <span className="font-bold">My App</span>244 </Link>245 <nav className="flex flex-1 items-center space-x-6 text-sm font-medium">246 <Link href="/about">About</Link>247 <Link href="/docs">Docs</Link>248 </nav>249 <Button size="sm">Sign In</Button>250 </div>251 </header>252 )253 }`,254 "src/components/footer.tsx": `export function Footer() {255 return (256 <footer className="border-t py-6 md:py-0">257 <div className="container flex flex-col items-center justify-between gap-4 md:h-24 md:flex-row">258 <p className="text-center text-sm leading-loose text-muted-foreground md:text-left">259 Built with Next.js and Tailwind CSS.260 </p>261 </div>262 </footer>263 )264 }`,265 "src/lib/utils.ts": `import { type ClassValue, clsx } from "clsx"266 import { twMerge } from "tailwind-merge"267268 export function cn(...inputs: ClassValue[]) {269 return twMerge(clsx(inputs))270 }`,271 "src/lib/constants.ts": `export const APP_NAME = "My Application"272 export const APP_VERSION = "1.0.0"273 export const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3000/api"274275 export const ROUTES = {276 home: "/",277 about: "/about",278 dashboard: "/dashboard",279 settings: "/settings",280 } as const`,281 "src/hooks/use-auth.ts": `import { useState, useEffect, useCallback } from "react"282283 interface User {284 id: string285 email: string286 name: string287 }288289 export function useAuth() {290 const [user, setUser] = useState<User | null>(null)291 const [loading, setLoading] = useState(true)292293 useEffect(() => {294 // Check for existing session295 const checkAuth = async () => {296 try {297 const response = await fetch("/api/auth/me")298 if (response.ok) {299 const userData = await response.json()300 setUser(userData)301 }302 } catch (error) {303 console.error("Auth check failed:", error)304 } finally {305 setLoading(false)306 }307 }308 checkAuth()309 }, [])310311 const signOut = useCallback(async () => {312 await fetch("/api/auth/signout", { method: "POST" })313 setUser(null)314 }, [])315316 return { user, loading, signOut }317 }`,318 "src/hooks/use-theme.ts": `import { useState, useEffect } from "react"319320 type Theme = "light" | "dark" | "system"321322 export function useTheme() {323 const [theme, setTheme] = useState<Theme>("system")324325 useEffect(() => {326 const stored = localStorage.getItem("theme") as Theme | null327 if (stored) {328 setTheme(stored)329 }330 }, [])331332 useEffect(() => {333 const root = document.documentElement334 root.classList.remove("light", "dark")335336 if (theme === "system") {337 const systemTheme = window.matchMedia("(prefers-color-scheme: dark)").matches338 ? "dark"339 : "light"340 root.classList.add(systemTheme)341 } else {342 root.classList.add(theme)343 }344345 localStorage.setItem("theme", theme)346 }, [theme])347348 return { theme, setTheme }349 }`,350 "package.json": `{351 "name": "my-nextjs-app",352 "version": "0.1.0",353 "private": true,354 "scripts": {355 "dev": "next dev",356 "build": "next build",357 "start": "next start",358 "lint": "next lint"359 },360 "dependencies": {361 "next": "14.2.0",362 "react": "^18",363 "react-dom": "^18",364 "class-variance-authority": "^0.7.0",365 "clsx": "^2.1.0",366 "tailwind-merge": "^2.2.0",367 "lucide-react": "^0.344.0"368 },369 "devDependencies": {370 "typescript": "^5",371 "@types/node": "^20",372 "@types/react": "^18",373 "@types/react-dom": "^18",374 "tailwindcss": "^3.4.1",375 "postcss": "^8"376 }377 }`,378 "tsconfig.json": `{379 "compilerOptions": {380 "lib": ["dom", "dom.iterable", "esnext"],381 "allowJs": true,382 "skipLibCheck": true,383 "strict": true,384 "noEmit": true,385 "esModuleInterop": true,386 "module": "esnext",387 "moduleResolution": "bundler",388 "resolveJsonModule": true,389 "isolatedModules": true,390 "jsx": "preserve",391 "incremental": true,392 "plugins": [{ "name": "next" }],393 "paths": {394 "@/*": ["./*"]395 }396 },397 "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],398 "exclude": ["node_modules"]399 }`,400 "next.config.mjs": `/** @type {import('next').NextConfig} */401 const nextConfig = {402 reactStrictMode: true,403 images: {404 domains: [],405 },406 }407408 export default nextConfig`,409 "tailwind.config.ts": `import type { Config } from "tailwindcss"410411 const config: Config = {412 darkMode: ["class"],413 content: [414 "./pages/**/*.{js,ts,jsx,tsx,mdx}",415 "./components/**/*.{js,ts,jsx,tsx,mdx}",416 "./app/**/*.{js,ts,jsx,tsx,mdx}",417 ],418 theme: {419 extend: {420 colors: {421 border: "hsl(var(--border))",422 background: "hsl(var(--background))",423 foreground: "hsl(var(--foreground))",424 },425 },426 },427 plugins: [],428 }429 export default config`,430 ".env.local": `# Environment Variables431 DATABASE_URL="postgresql://user:password@localhost:5432/mydb"432 NEXT_PUBLIC_API_URL="http://localhost:3000/api"433 AUTH_SECRET="your-secret-key-here"`,434 ".gitignore": `# Dependencies435 node_modules436 .pnp437 .pnp.js438439 # Testing440 coverage441442 # Next.js443 .next/444 out/445446 # Production447 build448449 # Misc450 .DS_Store451 *.pem452453 # Debug454 npm-debug.log*455456 # Local env files457 .env*.local458459 # Vercel460 .vercel461462 # TypeScript463 *.tsbuildinfo464 next-env.d.ts`,465 "README.md": `# My Next.js App466467 A modern web application built with Next.js 14, React, and Tailwind CSS.468469 ## Getting Started470471 First, install dependencies:472473 \`\`\`bash474 npm install475 # or476 yarn install477 # or478 pnpm install479 \`\`\`480481 Then, run the development server:482483 \`\`\`bash484 npm run dev485 \`\`\`486487 Open [http://localhost:3000](http://localhost:3000) to see the result.488489 ## Features490491 - Next.js 14 App Router492 - TypeScript493 - Tailwind CSS494 - Component library with CVA variants495 `,496};497498const sampleData: FileTreeNode[] = [499 {500 name: "src",501 type: "folder",502 children: [503 {504 name: "app",505 type: "folder",506 children: [507 { name: "layout.tsx", type: "file" },508 { name: "page.tsx", type: "file" },509 { name: "globals.css", type: "file" },510 {511 name: "api",512 type: "folder",513 children: [514 {515 name: "auth",516 type: "folder",517 children: [{ name: "route.ts", type: "file" }],518 },519 {520 name: "users",521 type: "folder",522 children: [{ name: "route.ts", type: "file" }],523 },524 ],525 },526 ],527 },528 {529 name: "components",530 type: "folder",531 children: [532 {533 name: "ui",534 type: "folder",535 children: [536 { name: "button.tsx", type: "file" },537 { name: "card.tsx", type: "file" },538 { name: "file-tree.tsx", type: "file" },539 ],540 },541 { name: "header.tsx", type: "file" },542 { name: "footer.tsx", type: "file" },543 ],544 },545 {546 name: "lib",547 type: "folder",548 children: [549 { name: "utils.ts", type: "file" },550 { name: "constants.ts", type: "file" },551 ],552 },553 {554 name: "hooks",555 type: "folder",556 children: [557 { name: "use-auth.ts", type: "file" },558 { name: "use-theme.ts", type: "file" },559 ],560 },561 ],562 },563 {564 name: "public",565 type: "folder",566 children: [567 { name: "favicon.ico", type: "file" },568 { name: "logo.svg", type: "file" },569 {570 name: "images",571 type: "folder",572 children: [573 { name: "hero.png", type: "file" },574 { name: "avatar.jpg", type: "file" },575 ],576 },577 ],578 },579 { name: "package.json", type: "file" },580 { name: "tsconfig.json", type: "file" },581 { name: "next.config.mjs", type: "file" },582 { name: "tailwind.config.ts", type: "file" },583 { name: ".env.local", type: "file" },584 { name: ".gitignore", type: "file" },585 { name: "README.md", type: "file" },586];587588const imagePreviewUrls: Record<string, string> = {589 "public/images/hero.png":590 "https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?q=80&w=1200&auto=format&fit=crop",591 "public/images/avatar.jpg":592 "https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?q=80&w=400&auto=format&fit=crop",593 "public/logo.svg": "/file.svg",594 "public/favicon.ico": "https://picsum.photos/seed/favicon/64/64",595};596597type OpenFileTab = FileContent & { id: string };598599function getTabFileIcon(filename: string) {600 const iconClass = "shrink-0";601 const ext = filename.split(".").pop()?.toLowerCase();602603 const iconMap: Record<string, ReactNode> = {604 js: <FileCode className={cn(iconClass, "text-yellow-500")} size={14} />,605 jsx: <FileCode className={cn(iconClass, "text-yellow-500")} size={14} />,606 ts: <FileCode className={cn(iconClass, "text-blue-500")} size={14} />,607 tsx: <FileCode className={cn(iconClass, "text-blue-500")} size={14} />,608 json: <FileJson className={cn(iconClass, "text-yellow-600")} size={14} />,609 md: (610 <FileText className={cn(iconClass, "text-muted-foreground")} size={14} />611 ),612 css: <FileCode className={cn(iconClass, "text-blue-400")} size={14} />,613 svg: <ImageIcon className={cn(iconClass, "text-orange-400")} size={14} />,614 png: <ImageIcon className={cn(iconClass, "text-green-500")} size={14} />,615 jpg: <ImageIcon className={cn(iconClass, "text-green-500")} size={14} />,616 };617618 if (filename === "package.json") {619 return <Package className={cn(iconClass, "text-green-600")} size={14} />;620 }621 if (filename.startsWith(".env")) {622 return <Cog className={cn(iconClass, "text-yellow-600")} size={14} />;623 }624 if (filename.includes("config")) {625 return <Cog className={cn(iconClass, "text-muted-foreground")} size={14} />;626 }627628 return (629 iconMap[ext || ""] || (630 <File className={cn(iconClass, "text-muted-foreground")} size={14} />631 )632 );633}634635function buildFileContent(node: FileTreeNode, path: string): FileContent {636 const content = fileContents[path];637 const previewUrl = imagePreviewUrls[path];638639 if (content) {640 return { path, name: node.name, content, previewUrl };641 }642643 return {644 path,645 name: node.name,646 content: previewUrl647 ? ""648 : `// Content for ${node.name} not available in demo`,649 previewUrl,650 };651}652653type FlatFileEntry = {654 path: string;655 name: string;656 node: FileTreeNode;657};658659function flattenFilePaths(nodes: FileTreeNode[], prefix = ""): FlatFileEntry[] {660 const entries: FlatFileEntry[] = [];661662 for (const node of nodes) {663 const path = prefix ? `${prefix}/${node.name}` : node.name;664 if (node.type === "file") {665 entries.push({ path, name: node.name, node });666 } else if (node.children?.length) {667 entries.push(...flattenFilePaths(node.children, path));668 }669 }670671 return entries;672}673674const mockExtensions = [675 {676 id: "eslint",677 name: "ESLint",678 publisher: "Microsoft",679 description: "Integrates ESLint into your workspace.",680 },681 {682 id: "prettier",683 name: "Prettier",684 publisher: "Prettier",685 description: "Code formatter using Prettier.",686 },687 {688 id: "tailwind",689 name: "Tailwind CSS IntelliSense",690 publisher: "Tailwind Labs",691 description: "Intelligent Tailwind class completion and linting.",692 },693 {694 id: "gitlens",695 name: "GitLens",696 publisher: "GitKraken",697 description: "Supercharge Git capabilities built into your editor.",698 },699];700701export function FileViewerWithEditorSidebar() {702 const { activeId: sidebarView, setActiveId: setSidebarView } =703 useEditorSidebar("explorer");704 const { tabs, activeId, setActiveId, openTab, closeTab, focusTab } =705 useEditorTabs<OpenFileTab>();706 const [searchQuery, setSearchQuery] = useState("");707 const [explorerPanelOpen, setExplorerPanelOpen] = useState(true);708709 const activeFile = useMemo(710 () => tabs.find((tab) => tab.id === activeId) ?? null,711 [tabs, activeId],712 );713714 const allFiles = useMemo(() => flattenFilePaths(sampleData), []);715716 const searchResults = useMemo(() => {717 const query = searchQuery.trim().toLowerCase();718 if (!query) return allFiles.slice(0, 8);719 return allFiles.filter(720 (file) =>721 file.path.toLowerCase().includes(query) ||722 file.name.toLowerCase().includes(query),723 );724 }, [allFiles, searchQuery]);725726 const handleSelect = (node: FileTreeNode, path: string) => {727 if (node.type !== "file") return;728729 const file = buildFileContent(node, path);730 const tab: OpenFileTab = { ...file, id: path };731732 if (tabs.some((item) => item.id === path)) {733 focusTab(path);734 return;735 }736737 openTab(tab);738 };739740 const handleSearchSelect = (entry: FlatFileEntry) => {741 handleSelect(entry.node, entry.path);742 setSidebarView("explorer");743 };744745 return (746 <div className="w-full">747 <FileExplorer variant="elevated" rounded="lg" className="h-[600px]">748 <EditorSidebar749 value={sidebarView}750 onValueChange={setSidebarView}751 variant="ghost"752 className="h-full"753 >754 <EditorSidebarRail className="h-full min-h-0 self-stretch">755 <div className="flex min-h-0 flex-1 flex-col gap-1">756 <EditorSidebarTrigger757 value="explorer"758 icon={<Files />}759 label="Explorer"760 />761 <EditorSidebarTrigger762 value="search"763 icon={<Search />}764 label="Search"765 />766 <EditorSidebarTrigger767 value="extensions"768 icon={<Blocks />}769 label="Extensions"770 badge771 />772 </div>773 <Button774 type="button"775 variant="ghost"776 size="icon"777 className="mt-1 size-9 shrink-0 text-muted-foreground"778 onClick={() => setExplorerPanelOpen((open) => !open)}779 aria-label={780 explorerPanelOpen781 ? "Collapse explorer panel"782 : "Expand explorer panel"783 }784 aria-expanded={explorerPanelOpen}785 title={explorerPanelOpen ? "Collapse panel" : "Expand panel"}786 >787 {explorerPanelOpen ? (788 <PanelLeftClose className="size-4" />789 ) : (790 <PanelLeft className="size-4" />791 )}792 </Button>793 </EditorSidebarRail>794 <div795 className={cn(796 "flex h-full min-h-0 shrink-0 flex-col overflow-hidden transition-[width] duration-200 ease-linear",797 explorerPanelOpen ? "w-[260px]" : "w-0",798 )}799 aria-hidden={!explorerPanelOpen}800 >801 <EditorSidebarContent802 width="260px"803 className="h-full min-h-0 min-w-[260px] flex-none"804 >805 <EditorSidebarPanel value="explorer" title="Explorer">806 <FileTree807 data={sampleData}808 variant="ghost"809 size="default"810 onSelect={handleSelect}811 selectedPath={activeId}812 />813 </EditorSidebarPanel>814 <EditorSidebarPanel value="search" title="Search">815 <div className="flex flex-col gap-3">816 <input817 type="search"818 value={searchQuery}819 onChange={(event) => setSearchQuery(event.target.value)}820 placeholder="Search files..."821 className="h-8 w-full rounded-md border border-border bg-background px-2.5 text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring/60"822 />823 <ul className="flex flex-col gap-0.5">824 {searchResults.length === 0 ? (825 <li className="px-1 py-2 text-xs text-muted-foreground">826 No files match your search.827 </li>828 ) : (829 searchResults.map((file) => (830 <li key={file.path}>831 <button832 type="button"833 onClick={() => handleSearchSelect(file)}834 className="flex w-full items-center gap-2 rounded-sm px-1.5 py-1 text-left text-xs hover:bg-accent"835 >836 {getTabFileIcon(file.name)}837 <span className="truncate font-mono">838 {file.path}839 </span>840 </button>841 </li>842 ))843 )}844 </ul>845 </div>846 </EditorSidebarPanel>847 <EditorSidebarPanel value="extensions" title="Extensions">848 <ul className="flex flex-col gap-2">849 {mockExtensions.map((extension) => (850 <li851 key={extension.id}852 className="rounded-md border border-border p-2.5"853 >854 <p className="text-sm font-medium">{extension.name}</p>855 <p className="text-xs text-muted-foreground">856 {extension.publisher}857 </p>858 <p className="mt-1 text-xs text-muted-foreground">859 {extension.description}860 </p>861 </li>862 ))}863 </ul>864 </EditorSidebarPanel>865 </EditorSidebarContent>866 </div>867 </EditorSidebar>868 <FileExplorerContent className="flex flex-col">869 <EditorTabs870 value={activeId}871 onValueChange={setActiveId}872 variant="ghost"873 className="h-full"874 >875 {tabs.length > 0 ? (876 <EditorTabsList>877 {tabs.map((tab) => (878 <EditorTab879 key={tab.id}880 value={tab.id}881 icon={getTabFileIcon(tab.name)}882 onClose={closeTab}883 >884 {tab.name}885 </EditorTab>886 ))}887 </EditorTabsList>888 ) : null}889 <EditorTabsPanel>890 <FileViewer891 file={activeFile}892 variant="ghost"893 size="default"894 rounded="none"895 maxHeight="100%"896 showHeader={false}897 className="h-full"898 />899 </EditorTabsPanel>900 </EditorTabs>901 </FileExplorerContent>902 </FileExplorer>903 </div>904 );905}
Installation & source
Install via the shadcn CLI or copy the registry files manually.
bash
npx shadcn@latest add @tt-ui/file-viewer
Props
| Name | Type | Default | Description |
|---|---|---|---|
| file | FileContent | undefined | The file to display |
| showLineNumbers | boolean | true | Show line numbers |
| highlightedLines | Record<number, string> | {} | Highlighted lines |
| onClose | () => void | undefined | Callback when the file viewer is closed |
| showHeader | boolean | true | Show the header |
| maxHeight | string | 100% | The maximum height of the file viewer |
| emptyMessage | string | Select a file to view its contents | The message to display when no file is selected |
| collapsible (FileExplorer) | boolean | false | When true, the explorer sidebar can collapse to give the viewer full width |
| defaultSidebarOpen (FileExplorer) | boolean | true | Initial open state when collapsible is enabled |
| sidebarOpen (FileExplorer) | boolean | undefined | Controlled sidebar open state |
| onSidebarOpenChange (FileExplorer) | (open: boolean) => void | undefined | Called when the collapsible sidebar opens or closes |
| sidebarWidth (FileExplorer) | string | 280px | Width of the explorer sidebar when expanded |