Block
Source files
Several source files as tabs, each collapsed to a readable height.
- #arc
- #block
- #code
- #files
- #tabs
- #source
- #viewer
1280px
Install
One command installs the files and packages this item needs.
TerminalTerminalTerminalbash
npx shadcn@latest add https://dsui.vercel.app/r/source-files.jsonFiles
components/arc/blocks/source-files/source-files.module.csscomponents/arc/blocks/source-files/source-files.tsx
Registry items it pulls in
Source
View as Markdownsource-files.module.csssource-files.module.csssource-files.module.csscss
.files { display: flex; flex-direction: column; gap: var(--space-3); }
.files pre { margin: 0; }
source-files.tsxsource-files.tsxsource-files.tsxtsx
"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;
Used items
- TabsComponent
Tabs that switch between panels, morphing the panel height and direction of travel.
- Code blockComponent
A code snippet with an optional filename and line-limited collapse.