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