# 여러 파일 코드 뷰어

파일 탭으로 여러 소스를 보여 주고, 긴 파일은 접어 둬요.

- Type: block
- Theme: base
- Tags: arc, block, code, files, tabs, source, viewer
- English title: Source files
- URL: https://dsui.vercel.app/items/source-files

## Used items

- [탭](https://dsui.vercel.app/items/tabs): 탭을 바꾸면 패널 높이가 부드럽게 이어지며 내용이 바뀌어요.
- [코드 블록](https://dsui.vercel.app/items/code-block): 파일 이름과 함께 코드를 보여 주고 길면 접을 수 있어요.

## Controls

| Key | Type | Label | Default |
| --- | --- | --- | --- |
| maxLines | slider | 접는 줄 수 | 12 |
| label | text | 탭 이름 | 파일 |

## Source

### source-files.module.css

```css
.files { display: flex; flex-direction: column; gap: var(--space-3); }
.files pre { margin: 0; }

```

### source-files.tsx

```tsx
"use client";

import { useState } from "react";
import { CodeBlock } from "../../code-block/code-block";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../../tabs/tabs";
import styles from "./source-files.module.css";

export interface SourceFile {
  /** The file's name, shown on its tab. Must be unique within the set. */
  name: string;
  code: string;
  /** Used for the header label and the lightweight highlighting. Guessed from the extension when left out. */
  language?: string;
}

export interface SourceFilesProps {
  files: SourceFile[];
  /** The accessible name of the tab row, e.g. "파일". */
  label?: string;
  /** Collapse each file to this many lines, with a toggle that opens the rest in place. */
  maxLines?: number;
  className?: string;
}

const EXTENSIONS: Record<string, string> = {
  ts: "typescript", tsx: "tsx", js: "javascript", jsx: "jsx", css: "css",
  json: "json", md: "markdown", html: "html", sh: "bash", bash: "bash",
};

function languageOf(file: SourceFile): string | undefined {
  if (file.language) return file.language;
  const ext = file.name.split(".").pop()?.toLowerCase();
  return ext ? EXTENSIONS[ext] : undefined;
}

/**
 * One component spread over several files, as tabs over a code block each. CodeBlock shows one file at a time and
 * copies one file at a time, which is what a reader wants: they are going to paste these into separate files anyway.
 * With a single file it renders the block alone — a row of one tab says nothing.
 */
export function SourceFiles({ files, label = "파일", maxLines = 24, className }: SourceFilesProps) {
  const [active, setActive] = useState(files[0]?.name ?? "");
  if (files.length === 0) return null;
  if (files.length === 1) {
    const only = files[0];
    return (
      <div className={[styles.files, className].filter(Boolean).join(" ")}>
        <CodeBlock code={only.code} filename={only.name} language={languageOf(only)} maxLines={maxLines} />
      </div>
    );
  }
  return (
    <Tabs value={active} onValueChange={setActive} className={[styles.files, className].filter(Boolean).join(" ")}>
      <TabsList aria-label={label}>
        {files.map(file => <TabsTrigger key={file.name} value={file.name}>{file.name}</TabsTrigger>)}
      </TabsList>
      {files.map(file => (
        <TabsContent key={file.name} value={file.name}>
          <CodeBlock code={file.code} filename={file.name} language={languageOf(file)} maxLines={maxLines} />
        </TabsContent>
      ))}
    </Tabs>
  );
}

SourceFiles.displayName = "SourceFiles";

export default SourceFiles;

```
