xref: /expo/docs/ui/components/FileTree/index.tsx (revision 920d7a63)
1import { FileCode01Icon, LayoutAlt01Icon, FolderIcon } from '@expo/styleguide-icons';
2import { HTMLAttributes, ReactNode } from 'react';
3
4type FileTreeProps = HTMLAttributes<HTMLDivElement> & {
5  files?: (string | [string, string])[];
6};
7
8type FileObject = {
9  name: string;
10  note?: string;
11  files: FileObject[];
12};
13
14export function FileTree({ files = [], ...rest }: FileTreeProps) {
15  return (
16    <div
17      className="text-xs border border-default rounded-md bg-default mb-4 p-2 pr-4 pb-4 whitespace-nowrap overflow-x-auto"
18      {...rest}>
19      {renderStructure(generateStructure(files))}
20    </div>
21  );
22}
23
24/**
25 * Given an array of file paths, generate a tree structure.
26 * @param files
27 * @returns
28 */
29function generateStructure(files: (string | [string, string])[]): FileObject[] {
30  const structure: FileObject[] = [];
31
32  function modifyPath(path: string, note?: string) {
33    const parts = path.split('/');
34    let currentLevel = structure;
35    parts.forEach((part, index) => {
36      const existingPath = currentLevel.find(item => item.name === part);
37      if (existingPath) {
38        currentLevel = existingPath.files;
39      } else {
40        const newPart: FileObject = {
41          name: part,
42          files: [],
43        };
44        if (note && index === parts.length - 1) {
45          newPart.note = note;
46        }
47        currentLevel.push(newPart);
48        currentLevel = newPart.files;
49      }
50    });
51  }
52
53  files.forEach(path => {
54    if (Array.isArray(path)) {
55      return modifyPath(path[0], path[1]);
56    } else {
57      return modifyPath(path);
58    }
59  });
60
61  return structure;
62}
63
64function renderStructure(structure: FileObject[], level = 0): ReactNode {
65  return structure.map(({ name, note, files }, index) => {
66    const FileIcon = getIconForFile(name);
67    return files.length ? (
68      <div key={name + '_' + index} className="mt-1 pt-1 pl-2 rounded-sm flex flex-col">
69        <div className="flex items-center">
70          {' '.repeat(level)}
71          <FolderIcon className="text-icon-tertiary mr-2 opacity-60 min-w-[20px]" />
72          <TextWithNote name={name} note={note} className="text-secondary" />
73        </div>
74        {renderStructure(files, level + 1)}
75      </div>
76    ) : (
77      <div key={name + '_' + index} className="mt-1 pt-1 pl-2 rounded-sm flex items-center">
78        {' '.repeat(Math.max(level, 0))}
79        <FileIcon className="text-icon-tertiary mr-2 min-w-[20px]" />
80        <TextWithNote name={name} note={note} className="text-default" />
81      </div>
82    );
83  });
84}
85
86function TextWithNote({
87  name,
88  note,
89  className,
90}: {
91  name: string;
92  note?: string;
93  className: string;
94}) {
95  return (
96    <span className="flex items-center flex-1">
97      {/* File/folder name  */}
98      <code className={className}>{name}</code>
99
100      {note && (
101        <>
102          {/* divider pushing  */}
103          <span className="flex-1 border-b border-default opacity-60 mx-2 md:mx-3 min-w-[2rem]" />
104          {/* Optional note */}
105          <code className="text-default">{note}</code>
106        </>
107      )}
108    </span>
109  );
110}
111
112function getIconForFile(filename: string) {
113  if (/_layout\.[jt]sx?/.test(filename)) {
114    return LayoutAlt01Icon;
115  }
116  return FileCode01Icon;
117}
118