├── .eslintrc.cjs ├── .gitignore ├── LICENSE ├── README.md ├── dist ├── @litecode-ide │ ├── index.d.ts │ ├── virtual-file-system.es.js │ └── virtual-file-system.umd.js ├── style.css └── vite.svg ├── index.html ├── package-lock.json ├── package.json ├── postcss.config.js ├── public └── vite.svg ├── sample ├── breadcrumbs.gif ├── search.gif ├── structure.gif └── tabs.gif ├── src ├── App.tsx ├── assets │ ├── cross.svg │ ├── css.svg │ ├── delete.svg │ ├── down-arrow.svg │ ├── download.svg │ ├── error.png │ ├── folder.svg │ ├── js.svg │ ├── jsx.svg │ ├── left-arrow.svg │ ├── manifest.json │ ├── new-file-colored.svg │ ├── new-file.svg │ ├── new-folder.svg │ ├── new-project.svg │ ├── open-project.svg │ ├── opened-folder.svg │ ├── react.svg │ ├── readme.svg │ ├── rename.svg │ ├── three-dots.svg │ └── typescript.svg ├── components │ ├── file-structure │ │ ├── Folder.tsx │ │ ├── MiniFolder.tsx │ │ ├── Structure.tsx │ │ ├── search │ │ │ ├── HighlightedText.tsx │ │ │ ├── SearchContainer.tsx │ │ │ ├── SearchInput.tsx │ │ │ └── SearchResults.tsx │ │ ├── utils │ │ │ └── index.ts │ │ └── widgets │ │ │ ├── CollapseBtn.tsx │ │ │ ├── CustomInput.tsx │ │ │ ├── FileActions.tsx │ │ │ ├── ItemTitle.tsx │ │ │ └── ThreeDots.tsx │ └── menus │ │ ├── Breadcrumbs.tsx │ │ ├── Dialog.tsx │ │ ├── MenuContext.tsx │ │ ├── Tab.tsx │ │ └── Tabs.tsx ├── hooks │ ├── useOutsideAlerter.ts │ └── usePrependPortal.ts ├── index.css ├── index.d.ts ├── lib │ ├── index.d.ts │ └── index.tsx ├── main.tsx ├── state │ ├── context.ts │ ├── features │ │ ├── structure │ │ │ ├── miniStructureSlice.ts │ │ │ ├── structureSlice.ts │ │ │ └── utils │ │ │ │ ├── downloadZip.ts │ │ │ │ ├── getFileTree.ts │ │ │ │ ├── pathUtil.ts │ │ │ │ ├── sorting.ts │ │ │ │ └── traversal.ts │ │ └── tabs │ │ │ └── tabsSlice.ts │ ├── hooks.ts │ ├── provider.tsx │ └── store.ts ├── types │ ├── Breadcrumbs.d.ts │ ├── CollapseBtn.d.ts │ ├── CustomInput.d.ts │ ├── Dialog.d.ts │ ├── FileActions.d.ts │ ├── Folder.d.ts │ ├── HighlightedText.d.ts │ ├── ItemTitle.d.ts │ ├── MenuContext.d.ts │ ├── MiniFolder.d.ts │ ├── SearchContainer.d.ts │ ├── SearchInput.d.ts │ ├── SearchResults.d.ts │ ├── Structure.d.ts │ ├── Tab.d.ts │ ├── Tabs.d.ts │ ├── ThreeDots.d.ts │ └── index.d.ts └── vite-env.d.ts ├── tailwind.config.js ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:@typescript-eslint/recommended', 7 | 'plugin:react-hooks/recommended', 8 | ], 9 | ignorePatterns: ['dist', '.eslintrc.cjs'], 10 | parser: '@typescript-eslint/parser', 11 | plugins: ['react-refresh'], 12 | rules: { 13 | 'react-refresh/only-export-components': [ 14 | 'warn', 15 | { allowConstantExport: true }, 16 | ], 17 | }, 18 | } 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | # dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | src/App.tsx 27 | src/main.tsx -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 abel-tefera 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @litecode-ide/virtual-file-system 2 | 3 | **@litecode-ide/virtual-file-system** (from here onwards referred to as **_VFS_**) is a customizable, browser-based file system. 4 | 5 | ## Installation 6 | 7 | Use the package manager npm to install VFS. 8 | 9 | ```bash 10 | npm install @litecode-ide/virtual-file-system 11 | ``` 12 | 13 | ## Simple Usage 14 | 15 | ```js 16 | import React from "react"; 17 | import ReactDOM from "react-dom/client"; 18 | 19 | import { FileExplorer } from "@litecode-ide/virtual-file-system"; // FileExplorer component 20 | import "@litecode-ide/virtual-file-system/dist/style.css"; // Default styles 21 | 22 | const App = () => { 23 | return ( 24 | <> 25 | 26 | 27 | ); 28 | }; 29 | 30 | const root = ReactDOM.createRoot(document.getElementById("root")); 31 | root.render(); 32 | ``` 33 | 34 | To use logos with file types, create a css class `[extension]-logo` 35 | 36 | ```css 37 | .html-logo { 38 | background-image: url("./assets/html.svg"); 39 | } 40 | ``` 41 | 42 | You can clone a more detailed example from [GitHub](https://github.com/LiteCode-IDE/vfs-sample.git). Alternatively run the live example on [CodeSandbox](https://codesandbox.io/p/github/LiteCode-IDE/vfs-sample/main?layout=%257B%2522sidebarPanel%2522%253A%2522EXPLORER%2522%252C%2522rootPanelGroup%2522%253A%257B%2522direction%2522%253A%2522horizontal%2522%252C%2522contentType%2522%253A%2522UNKNOWN%2522%252C%2522type%2522%253A%2522PANEL_GROUP%2522%252C%2522id%2522%253A%2522ROOT_LAYOUT%2522%252C%2522panels%2522%253A%255B%257B%2522type%2522%253A%2522PANEL_GROUP%2522%252C%2522contentType%2522%253A%2522UNKNOWN%2522%252C%2522direction%2522%253A%2522vertical%2522%252C%2522id%2522%253A%2522clqci8er30006356vfnd2iny4%2522%252C%2522sizes%2522%253A%255B70%252C30%255D%252C%2522panels%2522%253A%255B%257B%2522type%2522%253A%2522PANEL_GROUP%2522%252C%2522contentType%2522%253A%2522EDITOR%2522%252C%2522direction%2522%253A%2522horizontal%2522%252C%2522id%2522%253A%2522EDITOR%2522%252C%2522panels%2522%253A%255B%257B%2522type%2522%253A%2522PANEL%2522%252C%2522contentType%2522%253A%2522EDITOR%2522%252C%2522id%2522%253A%2522clqci8er30002356v3zglr9d3%2522%257D%255D%257D%252C%257B%2522type%2522%253A%2522PANEL_GROUP%2522%252C%2522contentType%2522%253A%2522SHELLS%2522%252C%2522direction%2522%253A%2522horizontal%2522%252C%2522id%2522%253A%2522SHELLS%2522%252C%2522panels%2522%253A%255B%257B%2522type%2522%253A%2522PANEL%2522%252C%2522contentType%2522%253A%2522SHELLS%2522%252C%2522id%2522%253A%2522clqci8er30004356vrajed05p%2522%257D%255D%252C%2522sizes%2522%253A%255B100%255D%257D%255D%257D%252C%257B%2522type%2522%253A%2522PANEL_GROUP%2522%252C%2522contentType%2522%253A%2522DEVTOOLS%2522%252C%2522direction%2522%253A%2522vertical%2522%252C%2522id%2522%253A%2522DEVTOOLS%2522%252C%2522panels%2522%253A%255B%257B%2522type%2522%253A%2522PANEL%2522%252C%2522contentType%2522%253A%2522DEVTOOLS%2522%252C%2522id%2522%253A%2522clqci8er30005356vp8n5i57j%2522%257D%255D%252C%2522sizes%2522%253A%255B100%255D%257D%255D%252C%2522sizes%2522%253A%255B40%252C60%255D%257D%252C%2522tabbedPanels%2522%253A%257B%2522clqci8er30002356v3zglr9d3%2522%253A%257B%2522id%2522%253A%2522clqci8er30002356v3zglr9d3%2522%252C%2522tabs%2522%253A%255B%255D%257D%252C%2522clqci8er30005356vp8n5i57j%2522%253A%257B%2522id%2522%253A%2522clqci8er30005356vp8n5i57j%2522%252C%2522activeTabId%2522%253A%2522clqci9sgs00bu356vreccsav2%2522%252C%2522tabs%2522%253A%255B%257B%2522type%2522%253A%2522ENV_SETUP%2522%252C%2522id%2522%253A%2522clqci8etp000o356vpam546dt%2522%252C%2522mode%2522%253A%2522permanent%2522%257D%252C%257B%2522type%2522%253A%2522UNASSIGNED_PORT%2522%252C%2522port%2522%253A5173%252C%2522id%2522%253A%2522clqci9sgs00bu356vreccsav2%2522%252C%2522mode%2522%253A%2522permanent%2522%252C%2522path%2522%253A%2522%252F%2522%257D%255D%257D%252C%2522clqci8er30004356vrajed05p%2522%253A%257B%2522id%2522%253A%2522clqci8er30004356vrajed05p%2522%252C%2522activeTabId%2522%253A%2522clqci8er30003356v0nijrgme%2522%252C%2522tabs%2522%253A%255B%257B%2522id%2522%253A%2522clqci8er30003356v0nijrgme%2522%252C%2522mode%2522%253A%2522permanent%2522%252C%2522type%2522%253A%2522TERMINAL%2522%252C%2522shellId%2522%253A%2522clqci8f5r000redh5hdbxd7po%2522%257D%252C%257B%2522type%2522%253A%2522TASK_LOG%2522%252C%2522taskId%2522%253A%2522dev%2522%252C%2522id%2522%253A%2522clqci8f9w005c356vc7x0fxhk%2522%252C%2522mode%2522%253A%2522permanent%2522%257D%255D%257D%257D%252C%2522showDevtools%2522%253Atrue%252C%2522showShells%2522%253Atrue%252C%2522showSidebar%2522%253Afalse%252C%2522sidebarPanelSize%2522%253A0%257D). 43 | 44 | ## List of Exported Components 45 | 46 | - FileExplorer\ 47 | [](sample/structure.gif) 48 | 49 | - BreadCrumbs\ 50 | [](sample/breadcrumbs.gif) 51 | 52 | - Tabs\ 53 | [](sample/tabs.gif) 54 | 55 | - Search\ 56 | [](sample/search.gif) 57 | 58 | ## API 59 | 60 | VFS comes with a typescript definition file. 61 | 62 | ```ts 63 | declare module "@litecode-ide/virtual-file-system" { 64 | type ItemType = "file" | "folder"; 65 | 66 | interface BreadcrumbsProps { 67 | containerClassName?: string; 68 | textClassName?: string; 69 | miniFolderCollapseBtnClassName?: string; 70 | miniFolderCollapseBtnStyle?: React.CSSProperties; 71 | miniFolderContainerClassName?: string; 72 | itemTitleClassName?: string; 73 | onBreadcrumbFileClick?: (id: string) => void; 74 | } 75 | 76 | interface StructureProps { 77 | deleteConfirmationClassName?: string; 78 | fileInputClassName?: string; 79 | fileInputStyle?: React.CSSProperties; 80 | contextMenuClassName?: string; 81 | contextMenuHrColor?: string; 82 | contextMenuClickableAreaClassName?: string; 83 | fileActionsBtnClassName?: string; 84 | projectName?: string; 85 | fileActionsDisableCollapse?: true; 86 | fileActionsDisableTooltip?: true; 87 | fileActionsDisableDownload?: true; 88 | folderCollapseBtnClassname?: string; 89 | folderCollapseBtnStyle?: React.CSSProperties; 90 | folderThreeDotPrimaryClass?: string; 91 | folderThreeDotSecondaryClass?: string; 92 | folderClickableAreaClassName?: string; 93 | folderSelectedClickableAreaClassName?: string; 94 | folderContextSelectedClickableAreaClassName?: string; 95 | itemTitleClassName?: string; 96 | structureContainerClassName?: string; 97 | containerHeight?: string; 98 | onItemSelected?: (item: { id: string; type: ItemType }) => void; 99 | onNewItemClick?: (parentFolderId: string, type: ItemType) => void; 100 | onAreaCollapsed?: (collapsed: boolean) => void; 101 | onItemContextSelected?: (item: { id: string; type: ItemType }) => void; 102 | onNodeDeleted?: (id: string) => void; 103 | onNewItemCreated?: (id: string) => void; 104 | validExtensions: string[]; 105 | } 106 | 107 | interface TabsProps { 108 | containerClassName?: string; 109 | tabClassName?: string; 110 | selectedTabClassName?: string; 111 | onTabClick?: (id: string) => void; 112 | onTabClose?: (id: string) => void; 113 | } 114 | 115 | interface MatchingFile { 116 | id: string; 117 | name: string; 118 | extension: string; 119 | matches: MatchingLine[]; 120 | } 121 | 122 | interface MatchingLine { 123 | line: number; 124 | content: string; 125 | } 126 | 127 | interface SearchResults { 128 | files: MatchingFile[]; 129 | numOfResults: number; 130 | numOfLines: number; 131 | } 132 | 133 | interface SearchInputProps { 134 | className?: string; 135 | style?: React.CSSProperties; 136 | onSearchFiles?: (searchTerm: string, searchResults: SearchResults) => void; 137 | } 138 | 139 | interface SearchContainerProps { 140 | highlightedTextClassName?: string; 141 | headerClassName?: string; 142 | headerStyle?: React.CSSProperties; 143 | titleClassName?: string; 144 | searchResultClicked: (fileId: string, line: number) => void; 145 | } 146 | 147 | const FileExplorer: React.FC; 148 | const TabsList: React.FC; 149 | const SearchInput: React.FC; 150 | const Breadcrumbs: React.FC; 151 | const SearchResults: React.FC; 152 | const updateFile: (id: string, content: string) => void; 153 | const getFileTree: () => Record< 154 | string, 155 | { 156 | id: string; 157 | content: string; 158 | } 159 | >; 160 | const getSelectedFile: () => string; 161 | 162 | export { 163 | FileExplorer, 164 | TabsList, 165 | SearchResults, 166 | Breadcrumbs, 167 | SearchInput, 168 | getFileTree, 169 | updateFile, 170 | getSelectedFile, 171 | }; 172 | } 173 | ``` 174 | 175 | ## Contributing 176 | 177 | Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change. 178 | 179 | ## License 180 | 181 | [MIT](./LICENSE) 182 | -------------------------------------------------------------------------------- /dist/@litecode-ide/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module "@litecode-ide/virtual-file-system" { 2 | type ItemType = "file" | "folder"; 3 | 4 | interface BreadcrumbsProps { 5 | containerClassName?: string; 6 | textClassName?: string; 7 | miniFolderCollapseBtnClassName?: string; 8 | miniFolderCollapseBtnStyle?: React.CSSProperties; 9 | miniFolderContainerClassName?: string; 10 | itemTitleClassName?: string; 11 | onBreadcrumbFileClick?: (id: string) => void; 12 | } 13 | 14 | interface StructureProps { 15 | deleteConfirmationClassName?: string; 16 | fileInputClassName?: string; 17 | fileInputStyle?: React.CSSProperties; 18 | contextMenuClassName?: string; 19 | contextMenuHrColor?: string; 20 | contextMenuClickableAreaClassName?: string; 21 | fileActionsBtnClassName?: string; 22 | projectName?: string; 23 | fileActionsDisableCollapse?: true; 24 | fileActionsDisableTooltip?: true; 25 | fileActionsDisableDownload?: true; 26 | folderCollapseBtnClassname?: string; 27 | folderCollapseBtnStyle?: React.CSSProperties; 28 | folderThreeDotPrimaryClass?: string; 29 | folderThreeDotSecondaryClass?: string; 30 | folderClickableAreaClassName?: string; 31 | folderSelectedClickableAreaClassName?: string; 32 | folderContextSelectedClickableAreaClassName?: string; 33 | itemTitleClassName?: string; 34 | structureContainerClassName?: string; 35 | containerHeight?: string; 36 | onItemSelected?: (item: { id: string; type: ItemType }) => void; 37 | onNewItemClick?: (parentFolderId: string, type: ItemType) => void; 38 | onAreaCollapsed?: (collapsed: boolean) => void; 39 | onItemContextSelected?: (item: { id: string; type: ItemType }) => void; 40 | onNodeDeleted?: (id: string) => void; 41 | onNewItemCreated?: (id: string) => void; 42 | validExtensions: string[]; 43 | } 44 | 45 | interface TabsProps { 46 | containerClassName?: string; 47 | tabClassName?: string; 48 | selectedTabClassName?: string; 49 | onTabClick?: (id: string) => void; 50 | onTabClose?: (id: string) => void; 51 | } 52 | 53 | interface MatchingFile { 54 | id: string; 55 | name: string; 56 | extension: string; 57 | matches: MatchingLine[]; 58 | } 59 | 60 | interface MatchingLine { 61 | line: number; 62 | content: string; 63 | } 64 | 65 | interface SearchResultsType { 66 | files: MatchingFile[]; 67 | numOfResults: number; 68 | numOfLines: number; 69 | } 70 | 71 | interface SearchInputProps { 72 | className?: string; 73 | style?: React.CSSProperties; 74 | onSearchFiles?: (searchTerm: string, searchResults: SearchResultsType) => void; 75 | } 76 | 77 | interface SearchContainerProps { 78 | highlightedTextClassName?: string; 79 | headerClassName?: string; 80 | headerStyle?: React.CSSProperties; 81 | titleClassName?: string; 82 | searchResultClicked: (fileId: string, line: number) => void; 83 | } 84 | 85 | const FileExplorer: React.FC; 86 | const TabsList: React.FC; 87 | const SearchInput: React.FC; 88 | const Breadcrumbs: React.FC; 89 | const SearchResults: React.FC; 90 | const updateFile: (id: string, content: string) => void; 91 | const getFileTree: () => Record< 92 | string, 93 | { 94 | id: string; 95 | content: string; 96 | } 97 | >; 98 | const getSelectedFile: () => string; 99 | 100 | export { 101 | FileExplorer, 102 | TabsList, 103 | SearchResults, 104 | Breadcrumbs, 105 | SearchInput, 106 | getFileTree, 107 | updateFile, 108 | getSelectedFile, 109 | }; 110 | } 111 | -------------------------------------------------------------------------------- /dist/style.css: -------------------------------------------------------------------------------- 1 | @import"https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap";*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.\!container{width:100%!important}.container{width:100%}@media (min-width: 640px){.\!container{max-width:640px!important}.container{max-width:640px}}@media (min-width: 768px){.\!container{max-width:768px!important}.container{max-width:768px}}@media (min-width: 1024px){.\!container{max-width:1024px!important}.container{max-width:1024px}}@media (min-width: 1280px){.\!container{max-width:1280px!important}.container{max-width:1280px}}@media (min-width: 1536px){.\!container{max-width:1536px!important}.container{max-width:1536px}}.collapse{visibility:collapse}.absolute{position:absolute}.relative{position:relative}.bottom-7{bottom:1.75rem}.top-0{top:0}.top-7{top:1.75rem}.z-0{z-index:0}.z-10{z-index:10}.z-20{z-index:20}.z-50{z-index:50}.m-2{margin:.5rem}.mx-1{margin-left:.25rem;margin-right:.25rem}.mx-2{margin-left:.5rem;margin-right:.5rem}.mx-\[1px\]{margin-left:1px;margin-right:1px}.mx-\[2px\]{margin-left:2px;margin-right:2px}.mx-auto{margin-left:auto;margin-right:auto}.my-1{margin-top:.25rem;margin-bottom:.25rem}.my-2{margin-top:.5rem;margin-bottom:.5rem}.my-4{margin-top:1rem;margin-bottom:1rem}.mb-1{margin-bottom:.25rem}.ml-1{margin-left:.25rem}.ml-2{margin-left:.5rem}.ml-3{margin-left:.75rem}.ml-\[2px\]{margin-left:2px}.ml-\[3px\]{margin-left:3px}.mr-1{margin-right:.25rem}.mr-2{margin-right:.5rem}.mr-\[0\.375rem\]{margin-right:.375rem}.mr-\[2px\]{margin-right:2px}.mr-\[6px\]{margin-right:6px}.mt-2{margin-top:.5rem}.box-border{box-sizing:border-box}.block{display:block}.flex{display:flex}.hidden{display:none}.h-3{height:.75rem}.h-4{height:1rem}.h-5{height:1.25rem}.h-fit{height:-moz-fit-content;height:fit-content}.h-full{height:100%}.max-h-60{max-height:15rem}.min-h-\[8rem\]{min-height:8rem}.w-1\/2{width:50%}.w-1\/4{width:25%}.w-3{width:.75rem}.w-32{width:8rem}.w-4{width:1rem}.w-48{width:12rem}.w-5{width:1.25rem}.w-52{width:13rem}.w-96{width:24rem}.w-\[14px\]{width:14px}.w-\[80\%\]{width:80%}.w-fit{width:-moz-fit-content;width:fit-content}.w-full{width:100%}.max-w-\[10rem\]{max-width:10rem}.flex-shrink-0{flex-shrink:0}.rotate-180{--tw-rotate: 180deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.rotate-\[270deg\]{--tw-rotate: 270deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.cursor-default{cursor:default}.cursor-pointer{cursor:pointer}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.flex-row{flex-direction:row}.flex-col{flex-direction:column}.items-start{align-items:flex-start}.items-center{align-items:center}.justify-start{justify-content:flex-start}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.self-start{align-self:flex-start}.self-center{align-self:center}.overflow-clip{overflow:clip}.overflow-y-auto{overflow-y:auto}.overflow-x-hidden{overflow-x:hidden}.overflow-x-clip{overflow-x:clip}.overflow-x-scroll{overflow-x:scroll}.whitespace-nowrap{white-space:nowrap}.break-words{overflow-wrap:break-word}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-none{border-radius:0}.rounded-sm{border-radius:.125rem}.rounded-b-lg{border-bottom-right-radius:.5rem;border-bottom-left-radius:.5rem}.rounded-r-sm{border-top-right-radius:.125rem;border-bottom-right-radius:.125rem}.rounded-t-lg{border-top-left-radius:.5rem;border-top-right-radius:.5rem}.border{border-width:1px}.border-r{border-right-width:1px}.border-t{border-top-width:1px}.border-monaco-color{--tw-border-opacity: 1;border-color:rgb(60 60 60 / var(--tw-border-opacity))}.border-red-500{--tw-border-opacity: 1;border-color:rgb(239 68 68 / var(--tw-border-opacity))}.border-slate-600{--tw-border-opacity: 1;border-color:rgb(71 85 105 / var(--tw-border-opacity))}.border-transparent{border-color:transparent}.border-vscode-blue{--tw-border-opacity: 1;border-color:rgb(64 120 206 / var(--tw-border-opacity))}.border-t-dark-bg{--tw-border-opacity: 1;border-top-color:rgb(14 21 37 / var(--tw-border-opacity))}.border-t-slate-200{--tw-border-opacity: 1;border-top-color:rgb(226 232 240 / var(--tw-border-opacity))}.border-t-zinc-600{--tw-border-opacity: 1;border-top-color:rgb(82 82 91 / var(--tw-border-opacity))}.bg-dark-bg-2{--tw-bg-opacity: 1;background-color:rgb(28 35 51 / var(--tw-bg-opacity))}.bg-dark-hover{--tw-bg-opacity: 1;background-color:rgb(43 50 69 / var(--tw-bg-opacity))}.bg-monaco-color{--tw-bg-opacity: 1;background-color:rgb(60 60 60 / var(--tw-bg-opacity))}.bg-orange-400{--tw-bg-opacity: 1;background-color:rgb(251 146 60 / var(--tw-bg-opacity))}.bg-red-700{--tw-bg-opacity: 1;background-color:rgb(185 28 28 / var(--tw-bg-opacity))}.bg-red-900{--tw-bg-opacity: 1;background-color:rgb(127 29 29 / var(--tw-bg-opacity))}.bg-slate-100{--tw-bg-opacity: 1;background-color:rgb(241 245 249 / var(--tw-bg-opacity))}.bg-slate-600{--tw-bg-opacity: 1;background-color:rgb(71 85 105 / var(--tw-bg-opacity))}.bg-slate-700{--tw-bg-opacity: 1;background-color:rgb(51 65 85 / var(--tw-bg-opacity))}.p-1{padding:.25rem}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-\[2px\]{padding:2px}.px-1{padding-left:.25rem;padding-right:.25rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-7{padding-left:1.75rem;padding-right:1.75rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-\[0\.32rem\]{padding-top:.32rem;padding-bottom:.32rem}.pl-1{padding-left:.25rem}.pl-12{padding-left:3rem}.pl-2{padding-left:.5rem}.pl-3{padding-left:.75rem}.pl-\[0\.3rem\]{padding-left:.3rem}.pl-\[1\.3rem\]{padding-left:1.3rem}.pr-1{padding-right:.25rem}.pr-2{padding-right:.5rem}.pr-4{padding-right:1rem}.pt-6{padding-top:1.5rem}.text-center{text-align:center}.align-baseline{vertical-align:baseline}.text-2xl{font-size:1.5rem;line-height:2rem}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.font-semibold{font-weight:600}.text-black{--tw-text-opacity: 1;color:rgb(0 0 0 / var(--tw-text-opacity))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}.text-zinc-500{--tw-text-opacity: 1;color:rgb(113 113 122 / var(--tw-text-opacity))}.opacity-50{opacity:.5}.shadow-sm{--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / .05);--tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.outline-0{outline-width:0px}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.backdrop-brightness-50{--tw-backdrop-brightness: brightness(.5);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.transition-\[height\]{transition-property:height;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-opacity{transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-300{transition-duration:.3s}.ease-out{transition-timing-function:cubic-bezier(0,0,.2,1)}.custom-scrollbar::-webkit-scrollbar{width:5px;height:5px}.custom-scrollbar::-webkit-scrollbar-thumb{background:#4a5575;border-radius:5px;visibility:hidden}.show-on-hover{opacity:0}.hover-show:hover .show-on-hover{opacity:1}.custom-scrollbar::-webkit-scrollbar-track{background-color:#0e1525}.file-tabs:hover .custom-scrollbar::-webkit-scrollbar-thumb{visibility:visible}.bg-vscode-overlay{position:relative}.bg-vscode-overlay>*{z-index:1}.bg-vscode-overlay:before{content:"";position:absolute;top:0;left:0;background-color:#4078ce;opacity:.6;width:100%;height:100%;border-top-right-radius:.125rem;border-bottom-right-radius:.125rem}.dialog-cover:before{position:absolute;top:0;left:0;background-color:#000;opacity:.2;width:100%;height:100%}.display-none-c{display:none!important}.span-logo{background-size:contain;background-repeat:no-repeat;background-position-y:center}.span-logo-width{width:1rem}.custom-scrollbar::-webkit-scrollbar-button:vertical:start:increment,.custom-scrollbar::-webkit-scrollbar-button:vertical:end:decrement,.custom-scrollbar::-webkit-scrollbar-button:horizontal:start:increment,.custom-scrollbar::-webkit-scrollbar-button:horizontal:end:decrement{display:none}.new-file-logo{background-image:url("data:image/svg+xml,%3c!DOCTYPE%20svg%20PUBLIC%20'-//W3C//DTD%20SVG%201.1//EN'%20'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'%3e%3c!--%20Uploaded%20to:%20SVG%20Repo,%20www.svgrepo.com,%20Transformed%20by:%20SVG%20Repo%20Mixer%20Tools%20--%3e%3csvg%20fill='%23ffc742'%20height='800px'%20width='800px'%20version='1.1'%20id='Capa_1'%20xmlns='http://www.w3.org/2000/svg'%20xmlns:xlink='http://www.w3.org/1999/xlink'%20viewBox='0%200%20264%20264'%20xml:space='preserve'%20stroke='%23ffc742'%3e%3cg%20id='SVGRepo_bgCarrier'%20stroke-width='0'/%3e%3cg%20id='SVGRepo_tracerCarrier'%20stroke-linecap='round'%20stroke-linejoin='round'/%3e%3cg%20id='SVGRepo_iconCarrier'%3e%3cpath%20d='M209.1,33h-32.267V2.383L209.1,33z%20M190.167,180c23.159,0,42,18.841,42,42s-18.841,42-42,42c-23.159,0-42-18.841-42-42%20S167.007,180,190.167,180z%20M189.333,198c-4.143,0-7.5,3.357-7.5,7.5v9.5h-8.167c-4.143,0-7.5,3.357-7.5,7.5s3.357,7.5,7.5,7.5h8.167%20v8.5c0,4.143,3.357,7.5,7.5,7.5c4.143,0,7.5-3.357,7.5-7.5V230h9.833c4.143,0,7.5-3.357,7.5-7.5s-3.357-7.5-7.5-7.5h-9.833v-9.5%20C196.833,201.357,193.476,198,189.333,198z%20M138.801,246.701l-99.324-0.451c-4.134-0.013-7.644-3.617-7.644-7.75V7.5%20c0-4.143,3.69-7.5,7.833-7.5h122.167v40.5c0,4.143,3.69,7.5,7.833,7.5h42.167v121.421c-6-2.845-14.039-4.421-21.834-4.421%20c-31.43,0-56.917,25.57-56.917,57C133.083,230.844,135.191,239.224,138.801,246.701z%20M63.666,76.5c0,4.143,3.357,7.5,7.5,7.5h99%20c4.143,0,7.5-3.357,7.5-7.5s-3.357-7.5-7.5-7.5h-99C67.024,69,63.666,72.357,63.666,76.5z%20M63.666,107.5c0,4.143,3.357,7.5,7.5,7.5%20h99c4.143,0,7.5-3.357,7.5-7.5s-3.357-7.5-7.5-7.5h-99C67.024,100,63.666,103.357,63.666,107.5z%20M71.166,148h99%20c4.143,0,7.5-3.357,7.5-7.5s-3.357-7.5-7.5-7.5h-99c-4.143,0-7.5,3.357-7.5,7.5S67.024,148,71.166,148z'/%3e%3c/g%3e%3c/svg%3e")}.new-folder-logo{background-image:url("data:image/svg+xml,%3c?xml%20version='1.0'%20encoding='utf-8'?%3e%3c!--%20Uploaded%20to:%20SVG%20Repo,%20www.svgrepo.com,%20Generator:%20SVG%20Repo%20Mixer%20Tools%20--%3e%3csvg%20width='800px'%20height='800px'%20viewBox='0%200%2016%2016'%20xmlns='http://www.w3.org/2000/svg'%20fill='%23000'%3e%3cpath%20fill-rule='evenodd'%20clip-rule='evenodd'%20d='M14.5%202H7.71l-.85-.85L6.51%201h-5l-.5.5v11l.5.5H7v-1H1.99V6h4.49l.35-.15.86-.86H14v1.5l-.001.51h1.011V2.5L14.5%202zm-.51%202h-6.5l-.35.15-.86.86H2v-3h4.29l.85.85.36.15H14l-.01.99zM13%2016h-1v-3H9v-1h3V9h1v3h3v1h-3v3z'/%3e%3c/svg%3e")}.opened-folder{background-image:url("data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2048%2048'%20width='48px'%20height='48px'%3e%3clinearGradient%20id='xGIh33lbYX9pWIYWeZsuka'%20x1='24'%20x2='24'%20y1='6.955'%20y2='23.167'%20gradientUnits='userSpaceOnUse'%3e%3cstop%20offset='0'%20stop-color='%23eba600'/%3e%3cstop%20offset='1'%20stop-color='%23c28200'/%3e%3c/linearGradient%3e%3cpath%20fill='url(%23xGIh33lbYX9pWIYWeZsuka)'%20d='M24.414,10.414l-2.536-2.536C21.316,7.316,20.553,7,19.757,7H5C3.895,7,3,7.895,3,9v30%20c0,1.105,0.895,2,2,2h38c1.105,0,2-0.895,2-2V13c0-1.105-0.895-2-2-2H25.828C25.298,11,24.789,10.789,24.414,10.414z'/%3e%3clinearGradient%20id='xGIh33lbYX9pWIYWeZsukb'%20x1='24.066'%20x2='24.066'%20y1='19.228'%20y2='33.821'%20gradientTransform='matrix(-1%200%200%201%2048%200)'%20gradientUnits='userSpaceOnUse'%3e%3cstop%20offset='0'%20stop-color='%23ffd869'/%3e%3cstop%20offset='1'%20stop-color='%23fec52b'/%3e%3c/linearGradient%3e%3cpath%20fill='url(%23xGIh33lbYX9pWIYWeZsukb)'%20d='M24,23l3.854-3.854C27.947,19.053,28.074,19,28.207,19H44.81c1.176,0,2.098,1.01,1.992,2.181%20l-1.636,18C45.072,40.211,44.208,41,43.174,41H4.79c-1.019,0-1.875-0.766-1.988-1.779L1.062,23.555C1.029,23.259,1.261,23,1.559,23%20H24z'/%3e%3c/svg%3e")}.closed-folder{background-image:url("data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2048%2048'%20width='48px'%20height='48px'%3e%3clinearGradient%20id='WQEfvoQAcpQgQgyjQQ4Hqa'%20x1='24'%20x2='24'%20y1='6.708'%20y2='14.977'%20gradientUnits='userSpaceOnUse'%3e%3cstop%20offset='0'%20stop-color='%23eba600'/%3e%3cstop%20offset='1'%20stop-color='%23c28200'/%3e%3c/linearGradient%3e%3cpath%20fill='url(%23WQEfvoQAcpQgQgyjQQ4Hqa)'%20d='M24.414,10.414l-2.536-2.536C21.316,7.316,20.553,7,19.757,7L5,7C3.895,7,3,7.895,3,9l0,30%20c0,1.105,0.895,2,2,2l38,0c1.105,0,2-0.895,2-2V13c0-1.105-0.895-2-2-2l-17.172,0C25.298,11,24.789,10.789,24.414,10.414z'/%3e%3clinearGradient%20id='WQEfvoQAcpQgQgyjQQ4Hqb'%20x1='24'%20x2='24'%20y1='10.854'%20y2='40.983'%20gradientUnits='userSpaceOnUse'%3e%3cstop%20offset='0'%20stop-color='%23ffd869'/%3e%3cstop%20offset='1'%20stop-color='%23fec52b'/%3e%3c/linearGradient%3e%3cpath%20fill='url(%23WQEfvoQAcpQgQgyjQQ4Hqb)'%20d='M21.586,14.414l3.268-3.268C24.947,11.053,25.074,11,25.207,11H43c1.105,0,2,0.895,2,2v26%20c0,1.105-0.895,2-2,2H5c-1.105,0-2-0.895-2-2V15.5C3,15.224,3.224,15,3.5,15h16.672C20.702,15,21.211,14.789,21.586,14.414z'/%3e%3c/svg%3e")}.js-logo{background-image:url("data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20width='2500'%20height='2500'%20viewBox='0%200%201052%201052'%3e%3cpath%20fill='%23f0db4f'%20d='M0%200h1052v1052H0z'/%3e%3cpath%20d='M965.9%20801.1c-7.7-48-39-88.3-131.7-125.9-32.2-14.8-68.1-25.399-78.8-49.8-3.8-14.2-4.3-22.2-1.9-30.8%206.9-27.9%2040.2-36.6%2066.6-28.6%2017%205.7%2033.1%2018.801%2042.8%2039.7%2045.4-29.399%2045.3-29.2%2077-49.399-11.6-18-17.8-26.301-25.4-34-27.3-30.5-64.5-46.2-124-45-10.3%201.3-20.699%202.699-31%204-29.699%207.5-58%2023.1-74.6%2044-49.8%2056.5-35.6%20155.399%2025%20196.1%2059.7%2044.8%20147.4%2055%20158.6%2096.9%2010.9%2051.3-37.699%2067.899-86%2062-35.6-7.4-55.399-25.5-76.8-58.4-39.399%2022.8-39.399%2022.8-79.899%2046.1%209.6%2021%2019.699%2030.5%2035.8%2048.7%2076.2%2077.3%20266.899%2073.5%20301.1-43.5%201.399-4.001%2010.6-30.801%203.199-72.101zm-394-317.6h-98.4c0%2085-.399%20169.4-.399%20254.4%200%2054.1%202.8%20103.7-6%20118.9-14.4%2029.899-51.7%2026.2-68.7%2020.399-17.3-8.5-26.1-20.6-36.3-37.699-2.8-4.9-4.9-8.7-5.601-9-26.699%2016.3-53.3%2032.699-80%2049%2013.301%2027.3%2032.9%2051%2058%2066.399%2037.5%2022.5%2087.9%2029.4%20140.601%2017.3%2034.3-10%2063.899-30.699%2079.399-62.199%2022.4-41.3%2017.6-91.3%2017.4-146.6.5-90.2%200-180.4%200-270.9z'%20fill='%23323330'/%3e%3c/svg%3e")}.css-logo{background-image:url("data:image/svg+xml,%3csvg%20fill='none'%20height='2500'%20width='2183'%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%20124%20141.53'%3e%3cpath%20d='M10.383%20126.892L0%200l124%20.255-10.979%20126.637-50.553%2014.638z'%20fill='%231b73ba'/%3e%3cpath%20d='M62.468%20129.275V12.085l51.064.17-9.106%20104.85z'%20fill='%231c88c7'/%3e%3cpath%20d='M100.851%2027.064H22.298l2.128%2015.318h37.276l-36.68%2015.745%202.127%2014.808h54.043l-1.958%2020.68-18.298%203.575-16.595-4.255-1.277-11.745H27.83l2.042%2024.426%2032.681%209.106%2031.32-9.957%204-47.745H64.765l36.085-14.978z'%20fill='%23000'/%3e%3c/svg%3e")}.jsx-logo{background-image:url("data:image/svg+xml,%3csvg%20height='2500'%20viewBox='175.7%2078%20490.6%20436.9'%20width='2194'%20xmlns='http://www.w3.org/2000/svg'%3e%3cg%20fill='%2361dafb'%3e%3cpath%20d='m666.3%20296.5c0-32.5-40.7-63.3-103.1-82.4%2014.4-63.6%208-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6%200%208.3.9%2011.4%202.6%2013.6%207.8%2019.5%2037.5%2014.9%2075.7-1.1%209.4-2.9%2019.3-5.1%2029.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50%2032.6-30.3%2063.2-46.9%2084-46.9v-22.3c-27.5%200-63.5%2019.6-99.9%2053.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7%200%2051.4%2016.5%2084%2046.6-14%2014.7-28%2031.4-41.3%2049.9-22.6%202.4-44%206.1-63.6%2011-2.3-10-4-19.7-5.2-29-4.7-38.2%201.1-67.9%2014.6-75.8%203-1.8%206.9-2.6%2011.5-2.6v-22.3c-8.4%200-16%201.8-22.6%205.6-28.1%2016.2-34.4%2066.7-19.9%20130.1-62.2%2019.2-102.7%2049.9-102.7%2082.3%200%2032.5%2040.7%2063.3%20103.1%2082.4-14.4%2063.6-8%20114.2%2020.2%20130.4%206.5%203.8%2014.1%205.6%2022.5%205.6%2027.5%200%2063.5-19.6%2099.9-53.6%2036.4%2033.8%2072.4%2053.2%2099.9%2053.2%208.4%200%2016-1.8%2022.6-5.6%2028.1-16.2%2034.4-66.7%2019.9-130.1%2062-19.1%20102.5-49.9%20102.5-82.3zm-130.2-66.7c-3.7%2012.9-8.3%2026.2-13.5%2039.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4%2014.2%202.1%2027.9%204.7%2041%207.9zm-45.8%20106.5c-7.8%2013.5-15.8%2026.3-24.1%2038.2-14.9%201.3-30%202-45.2%202-15.1%200-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8%206.2-13.4%2013.2-26.8%2020.7-39.9%207.8-13.5%2015.8-26.3%2024.1-38.2%2014.9-1.3%2030-2%2045.2-2%2015.1%200%2030.2.7%2045%201.9%208.3%2011.9%2016.4%2024.6%2024.2%2038%207.6%2013.1%2014.5%2026.4%2020.8%2039.8-6.3%2013.4-13.2%2026.8-20.7%2039.9zm32.3-13c5.4%2013.4%2010%2026.8%2013.8%2039.8-13.1%203.2-26.9%205.9-41.2%208%204.9-7.7%209.8-15.6%2014.4-23.7%204.6-8%208.9-16.1%2013-24.1zm-101.4%20106.7c-9.3-9.6-18.6-20.3-27.8-32%209%20.4%2018.2.7%2027.5.7%209.4%200%2018.7-.2%2027.8-.7-9%2011.7-18.3%2022.4-27.5%2032zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9%203.7-12.9%208.3-26.2%2013.5-39.5%204.1%208%208.4%2016%2013.1%2024s9.5%2015.8%2014.4%2023.4zm73.9-208.1c9.3%209.6%2018.6%2020.3%2027.8%2032-9-.4-18.2-.7-27.5-.7-9.4%200-18.7.2-27.8.7%209-11.7%2018.3-22.4%2027.5-32zm-74%2058.9c-4.9%207.7-9.8%2015.6-14.4%2023.7-4.6%208-8.9%2016-13%2024-5.4-13.4-10-26.8-13.8-39.8%2013.1-3.1%2026.9-5.8%2041.2-7.9zm-90.5%20125.2c-35.4-15.1-58.3-34.9-58.3-50.6s22.9-35.6%2058.3-50.6c8.6-3.7%2018-7%2027.7-10.1%205.7%2019.6%2013.2%2040%2022.5%2060.9-9.2%2020.8-16.6%2041.1-22.2%2060.6-9.9-3.1-19.3-6.5-28-10.2zm53.8%20142.9c-13.6-7.8-19.5-37.5-14.9-75.7%201.1-9.4%202.9-19.3%205.1-29.4%2019.6%204.8%2041%208.5%2063.5%2010.9%2013.5%2018.5%2027.5%2035.3%2041.6%2050-32.6%2030.3-63.2%2046.9-84%2046.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7%2038.2-1.1%2067.9-14.6%2075.8-3%201.8-6.9%202.6-11.5%202.6-20.7%200-51.4-16.5-84-46.6%2014-14.7%2028-31.4%2041.3-49.9%2022.6-2.4%2044-6.1%2063.6-11%202.3%2010.1%204.1%2019.8%205.2%2029.1zm38.5-66.7c-8.6%203.7-18%207-27.7%2010.1-5.7-19.6-13.2-40-22.5-60.9%209.2-20.8%2016.6-41.1%2022.2-60.6%209.9%203.1%2019.3%206.5%2028.1%2010.2%2035.4%2015.1%2058.3%2034.9%2058.3%2050.6-.1%2015.7-23%2035.6-58.4%2050.6z'/%3e%3ccircle%20cx='420.9'%20cy='296.5'%20r='45.7'/%3e%3c/g%3e%3c/svg%3e")}.error-logo{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAADsQAAA7EB9YPtSQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAABCjSURBVHic7Z1rcFvHdcf/Zy8APShbDxIkxfgZu7JsEpRUWrZlUyBIkXKcKlXURnZSp860nqYznUnTd5t4OtPO1GmbdDxN02QyTT90VNVtLde1ZTfRiyQIypJjmxZFkJZkPSzrZQIgJUqWZBHA3dMPJFWVuOC9AO4L1P3NaDRzsXf3D55zd/cuzp4FPDw8PDw8PG5GyGkBVpC677FbeK5/BVS5jIDbmegOALcxcBsBcwHcAsAHYBEm/gZXAYxP/j8KwigxzjLxYTB9wFJ8cPUKH7n7ZPSaY1/KIsreARgQI6HWVRJyHQGrAawEcA/M/24SwCkQ3gNTtyLVrsqh3vdNbsN2ytIBzixfVzknoG5i5vUA2gBUOiQlAUY3EbpUyrxWO7Av6ZCOoikbBzi9Zs28OZcDG0B4hhnrCQg4rWkaKgHdkvhfVf+Vl+v6+q46LcgIrneA4VA4JJh+F4SnAcxzWo9Bxhh4SZH456qhnnecFjMTrnQABigRav1lQfKbYLQ6racUCNwJSc8Hh3q6ndaihescIBGKtBP4bwA0Oa3FZA4A+OtgvOdlAthpMVO4xgFSodYWhvwOgEed1mIxfQx8oybes99pIYALHCDRsK5GiOz3mPFVN+ixCSbC1qwi/3Dpgd6Uk0Ic+4MzQMnG8K8T0wtw7jXOaS6A+C+DA7Ef0MQ6g+044gDn65vvyAplK4C1TrTvOhh7faw+vWRo7ym7mxZ2N5hoCP9KRigH4Bn//yA0Z4UykGyMfMn+pm3i9Jo18+ZcmfMDMD9rV5tlCDPRC9X+T75FfX0ZOxq0xQGS9ZFaEvJVBj1sR3uzgLcUos2VA9EzVjdkuQMkGtY2CiG2M+NOq9uaZZyT4M/VxmNxKxuxdA6QDLU8QSTe9IxfFHUC1D3cGHnEykYsc4BUQ2QDgFcALLCqjZuASsHcmQyFP29VA5Y4QLIh/CQTv4KJ4AuP0pgP0KupxsiXrajc9DlAqjHyZWbeCkAxu+6bnAyAjdXxnp+ZWampDpBqiESYeAeAOWbW63GdT1mKjpqh7jfNqtA0BxhujDQI5l5MxNl5WMeoYF5bNRg7ZEZlpjjAaEP4dpVoP4DPmFGfhy6nFaJHzVgnKHkSyE1NfpXoP+AZ305uz0r+L66vLzksrmQHSI0v+HvM/t/wXQcRHhqhqr8quZ5Sbk42hJ/CxNPv4QwsITbVxrtfK7aCoh0gEWr+LEE5CG+hx2lGFeZVlYOx08XcXNQQwAAByk/gGd8NVKpEW7nIh7koBxgJhb9OExsyPNxBONUQ/o1ibizYa1KrHqvjrG8I3vu+2xjNBmh5XV90pJCbCu4BWPW/AM/4bqRSSaPgt4KCeoCRFWsfklK8Veh9HrYhGWguJOS8oB5ASvF38IzvZgQB3y/oBqMFEw0tm+AFcpYDq5OhlieMFjbsAET48+L0eDiAYVsZcoBEQ3g9gFVFyzGPcQbcnJTh48l/TrMmFWptMVLQkAMIoj8uTU/pMJAmwmZ/wP8oM952Wo8Gw4qU7STUCIBzTothyOeMlNOd0CXrIysh+EDpkkpiHMCmqWiYCysjizIq7wbwoLOyrnMWCrdW98eOAsBIKLJcgrsALHVSlJB4SC8/gW4PwIJ/xzxJRfH/jA8Ai/ujY36FOgC865ys65yGpMiU8QGgKh49LEBtcHg4UAXpbsKZsQeYTMtyDg4t/DCQFoQvBQd6Xtf6/HxT+8LMeGYXER6yW9skp5mV1prBruNaH6ZWNC9jqXQDqLNZ1xQXLn46f+kvHPvZeL4CM/YAcy/7N8GlxgeAJX17Lvrn+Nc7MSdg4NRMxgeA4MG9H5BQW+HcnGDxrfOvbZipwIwOwIxnzNVTEMd8gnr1Ci3p23Mx4KPHYe9wcFIq1DKT8aeoWrb0OIGdyxPEckYb5h0ChhsfrRbsPwdnw7vf9SvUsbg/OqZX0MaJ4YlsVmmrO9T1kV5BjkR8qfPYCuanLNY0E5lsgOry/UiUtwcg6X8czsf2P5hRefeFlRHdYcimieFxhTli2Pij/KLDxgcAvz/Nm/J9mN8BAMu2IxXIgxmVuy7Wr1miV3Bxf3TMF/C3WzQnOKkq1G4k8oY3b1aSo7wFwGYLdBQMEz6X7zNNB+DNmxUQOqyTVDCrxkVgjxEnsGRiyDgqhNK8tD96UrdoU5M/dTi5jYCvmNZ+qTDauanJr/WRpgMkjqRWw315e1aNi8BOI8OByRPDI+TPRqoOdp3VK8j19YGRdMU2AHm7XIe4NZleoDk30nQARcrHrNVTNHYPB0fIl20LHnhT9zWO6+sDI6LyJQZtLKE962DStKn2EAByyxKrFrb0BAy8D0kRI8Y/eu8Tc1Ki8hXXGh8AEWvu3dCeBDq3smYUq3uCwyIr26uHosN6Bbm+PrBw7tVtAP1SAfU7wQqtiznrAOeaIlW+NCe1PnMhVqwTxCVl2o2kfv/wrsjcigXyVRA9blSwg6hXPqEF0w+9yOkBfGn5AMrD+ID5PcHBbIDajBj/9Jo18ypu4e1lYnwAUObdinunX8wdAgh32yLHPMx5RST0ZwPUbiSs+lxT0/y5lwPbAVe9KutCoPumX8txAIYoNwcASnUCQn867TNsfH+6YjsD7SVqth2C1HcAAf6sPXJMp9i3g74Aq223He4c1btvuHF9hS+94H8YtM4MwbYjYaQHwG32qLGEgn87CEDtWBTfe0GvfLI+skDw+E8BRMwQ6gREnJPDIXcOwNDtRl1OQRNDI8YfblxfAcGvAwibotAhmChndVdrHeAWG7RYjeHhQI/Rex++VeDaTpTxk38dzl3e13KAxTZIsQPDPUE+zje1L1Tnzd2Rbxm1DMnZzq/lALNpz3/RPcH5pvaFajq9E8AaC3Q5RU76Pi0HKJdFIKMU3BNMGX8WZjc35ABZG4TYCxXu1Ayy/TANG8g5lkbrS6o2CLGPyUWehUP7zxu9ZUnfnot+hdbDHfsOzCTnNNPZ3gMcTKd97UYWeabjss0nZmHIAT6xQYgdDGQDVJTxp1jcHx3LBugJAAMm6nKST6df0HKAsjsBW4O46pOG1vb1qOuLjqg+2Q7A0pM7bOLy9AuzzgEIGJKUaTdyIOOFlZFFY6Fm3XWPpQd6U5Iy7QQMmaPSMXKimzQcgBJ2KLGII8jKDiO/559val+YzvLONJTOM8vX6QbA1g7sS6qUaQMwaIpSB2Ag51xCDQeQbkhwUAyHISkSPNSrq//CysiibDqzZ3JT6aqAP7vLyDpB7cC+JCR1ADhshmC7EUBOdnGtgJBy/HJHKCvbjMTw5QkN+8VxJdB5rilSpXd/9VB0mLKyDcCREvQ6AoNyNrXkxgMQm3IQgW0wjpIv22bkyZ/q9qEVF8hY6UtzLFkfqdWrJ3io92NmXwuAsvpbSY2HO8cBZFZ5Hy46316HY0JRWo2EbhvMJXA/BHel7l+rm9mjZrAzAUltKJ/hQAUCOW8yOQ5QPRS9zEBRmadt5rhC1Gpkx860MV+P++ETuxMN62r0ClYPRYcVog4AutvEXcDR2oFdV6Zf1FzvJuDn1uspHgZOqQq1GzkyZcZuP3/99UTZntSqx3Qze1QORM8ozK1wuRMw8J7Wde0fPBi6iRkc5KSaVcJGNmoW+ORP5z7O+jqNDAeVg7HT2ayyDoCuJqcQQF+e6xoovNdSNcVzmqGuM7I/v5gnX4Pl7BPRkRVtuuch1R3q+sgn1RYAH5bQnmVIKTq1rms6QHBZzQCAi5YqKhAGTknilpr43hN6ZUt88qezTEq1y4gTLBnae0oSr9NacHGYZPVQt+bvGdpzgG3bVDDvsVZTQZwBK221AzHdp8ukJ386y6RU9368MnKXXsHagdiHalYJw0XDAQOdlOfNLm/QA0O8ap2kgjgLSTNm45rC5Cd/OncpKu8ZbQjfrlew7lDXR5MrhrpvKHZAwO58n+V1gIAPbzCQtkaSYRKCuaN6KHpMr6BFT/507pGCeocbw7q7p6qHosegcCscdgIG0nNkOu+pYnkdYHF/dIyAHmtkGSKhSNlm5IhUOxNGMuNOYoomQs26O6iq+2NHJ53AydzBO2aKhtKLe/t3k8UUAqd9Pt1YPou7fU0IuEOQ0mWkJxCqUOBgoK0gzHiu44wOIGnOS3DubaBWUWU0uSKcN029k0mjr/cEDW335CvjgqTRl1XM2T5TgRkdoHZg1xVivGiupgIgVEFSVyLUkhObb9OYPyME3EGkRpP1kZx996kVzcskuBNOZgxnbNFa/r0R3dBnCflj8xQVxSICdo80hq+fU+iCJNE3chsE947Wr31g6oILkkQDAAvwP+oVMjQ2JUMtMTh/XtAVBn0xoOBdl50VMMU5ItFGzOSGswIA7KqO9+hmL/EZqYmFeJ6k3FG6ppKoIPDraRUnCHhAv7jt1DHL7snVFqeND2LxD4bKGa0wGWp5G8DqohV52ElfMN6zOt/q340Y3v4kIZ4vTZOHXTDzt40YHyigB2CAUqGWfQAeKVqZhw1wT3U8FjFa2vi5gQALIb+J8gkXuxlhZny7kBsK2gFbdbD3bYC2FKbJwz5oS81gbF8hdxS8BZqy6rcwe/YPzhoYOK/61ILPdyzYAYKHej9mwp8Wep+HtQjgz4xsh5tOUT9STE4IdwBYX8z9HmbDPcF4rI00EkDoUVQWjIkJofKbAHRTrHlYzoVs1ve1YowPFOkAAFB1sOssgb9R7P0e5sCMZ40EyeajpDw4wXjs3wD6YSl1eBQPEf+4ZrDnv0upo+RESMHAJ78PsJORQzcr+8auVvxeqZWYEqmSaFhXQ5R9F+WdZ7icOCkp87CRPAh6mJIKrWawM0EkN8JlewlmKZck0RfMMD5gkgMAQHCg9z2C2AiNREQepnGNQb9aOxA1LUuJqckQg/HuHmJ6EkDGzHo9JsK7ifBkTTxq6oYd07NhBgejbxDha5hd+QadJgPQU8GBntfNrtiycOVUY8sXmPGfAOZZ1cZNwjVmfrpmMPaKFZVbGq+eDIXDAG0HsNDKdmYxYwBvrI7HYlY1YGlC5Op4LAbh/PaoMuW0KkSzlcYHLHYAAKg+GDvA7GsCXJ10wm3sF0JZs/Rgt+WJKW1JiV4z2JkIVlIbQH9rR3vlDAP/FJQjhk4rNwPb96ylQuFf44nfD0o+z2eWcQlEz1YPRF+2s1FHNi0m6yO1UPgnYGxwon0XsiubVb5eyq96xeLYrlUGKNkQ+W0i/i5mx0llxTAG4A+C8Z5/MRrGbTaOnw+Uun/tUvaJ7wB4BjbNSVwAM+hF4cv8iZEkl1biuANMkQq1NjGp359FR7TlYz8z/1Gh0btW4RoHACaGhZHGlg1gfm4WntjVR0x/ERyMvuG0kBtxlQPcSKKhpYOInwOoxWktJbJLkvhu7UC3Zp4+p3GtA0yRaFjbSES/BdBXUT6vjhcZeIkk/ah6KNrvtJiZcL0DTHGuqWm+L1OxGRJfYaJWAgJOa5pGFsAuMG+5clm8dvfJ6DWnBRmhbBzgRs43tS9UM5nPM+OLmNib4FTPkCJgJwg/HU/7dpVyQplTlKUD3AgDIhEK1wsgDKZmEB4BcCfM/24SwAdEeIcl3hGMtyqHevqKjcd3C2XvAFoMN66vUDizHFCXM7AcoM+AEGSJaiIsxQ0HZDNQMWnES2BcBOESgFEAZ0F8nEAnVIgT/qtXD1Ue+/klp76Th4eHh4eHh4eZ/C8tHXQfCadsewAAAABJRU5ErkJggg==)}.md-logo{background-image:url("data:image/svg+xml,%3c!DOCTYPE%20svg%20PUBLIC%20'-//W3C//DTD%20SVG%201.1//EN'%20'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'%3e%3c!--%20Uploaded%20to:%20SVG%20Repo,%20www.svgrepo.com,%20Transformed%20by:%20SVG%20Repo%20Mixer%20Tools%20--%3e%3csvg%20width='800px'%20height='800px'%20viewBox='0%200%2024%2024'%20fill='none'%20xmlns='http://www.w3.org/2000/svg'%3e%3cg%20id='SVGRepo_bgCarrier'%20stroke-width='0'/%3e%3cg%20id='SVGRepo_tracerCarrier'%20stroke-linecap='round'%20stroke-linejoin='round'/%3e%3cg%20id='SVGRepo_iconCarrier'%3e%3cpath%20fill-rule='evenodd'%20clip-rule='evenodd'%20d='M4%205.5H9C10.1046%205.5%2011%206.39543%2011%207.5V16.5C11%2017.0523%2010.5523%2017.5%2010%2017.5H4C3.44772%2017.5%203%2017.0523%203%2016.5V6.5C3%205.94772%203.44772%205.5%204%205.5ZM14%2019.5C13.6494%2019.5%2013.3128%2019.4398%2013%2019.3293V19.5C13%2020.0523%2012.5523%2020.5%2012%2020.5C11.4477%2020.5%2011%2020.0523%2011%2019.5V19.3293C10.6872%2019.4398%2010.3506%2019.5%2010%2019.5H4C2.34315%2019.5%201%2018.1569%201%2016.5V6.5C1%204.84315%202.34315%203.5%204%203.5H9C10.1947%203.5%2011.2671%204.02376%2012%204.85418C12.7329%204.02376%2013.8053%203.5%2015%203.5H20C21.6569%203.5%2023%204.84315%2023%206.5V16.5C23%2018.1569%2021.6569%2019.5%2020%2019.5H14ZM13%207.5V16.5C13%2017.0523%2013.4477%2017.5%2014%2017.5H20C20.5523%2017.5%2021%2017.0523%2021%2016.5V6.5C21%205.94772%2020.5523%205.5%2020%205.5H15C13.8954%205.5%2013%206.39543%2013%207.5ZM5%207.5H9V9.5H5V7.5ZM15%207.5H19V9.5H15V7.5ZM19%2010.5H15V12.5H19V10.5ZM5%2010.5H9V12.5H5V10.5ZM19%2013.5H15V15.5H19V13.5ZM5%2013.5H9V15.5H5V13.5Z'%20fill='lightpink'/%3e%3c/g%3e%3c/svg%3e")}.rename-logo{background-image:url("data:image/svg+xml,%3c!DOCTYPE%20svg%20PUBLIC%20'-//W3C//DTD%20SVG%201.1//EN'%20'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'%3e%3c!--%20Uploaded%20to:%20SVG%20Repo,%20www.svgrepo.com,%20Transformed%20by:%20SVG%20Repo%20Mixer%20Tools%20--%3e%3csvg%20width='800px'%20height='800px'%20viewBox='0%200%2028%2028'%20fill='none'%20xmlns='http://www.w3.org/2000/svg'%3e%3cg%20id='SVGRepo_bgCarrier'%20stroke-width='0'/%3e%3cg%20id='SVGRepo_tracerCarrier'%20stroke-linecap='round'%20stroke-linejoin='round'/%3e%3cg%20id='SVGRepo_iconCarrier'%3e%3cpath%20d='M11.75%202C11.3358%202%2011%202.33579%2011%202.75C11%203.16421%2011.3358%203.5%2011.75%203.5H13.25V24.5H11.75C11.3358%2024.5%2011%2024.8358%2011%2025.25C11%2025.6642%2011.3358%2026%2011.75%2026H16.25C16.6642%2026%2017%2025.6642%2017%2025.25C17%2024.8358%2016.6642%2024.5%2016.25%2024.5H14.75V3.5H16.25C16.6642%203.5%2017%203.16421%2017%202.75C17%202.33579%2016.6642%202%2016.25%202H11.75Z'%20fill='%23ffc742'/%3e%3cpath%20d='M6.25%206.01959H12.25V22.0196H6.25C4.45507%2022.0196%203%2020.5645%203%2018.7696V9.26959C3%207.47467%204.45507%206.01959%206.25%206.01959Z'%20fill='%23ffc742'/%3e%3cpath%20d='M21.75%2022.0196H15.75V6.01959H21.75C23.5449%206.01959%2025%207.47467%2025%209.26959V18.7696C25%2020.5645%2023.5449%2022.0196%2021.75%2022.0196Z'%20fill='%23ffc742'/%3e%3c/g%3e%3c/svg%3e")}.ts-logo,.tsx-logo{background-image:url("data:image/svg+xml,%3csvg%20height='2500'%20viewBox='0%200%20640%20640'%20width='2500'%20xmlns='http://www.w3.org/2000/svg'%3e%3cpath%20d='m0%200h640v640h-640z'%20fill='%23017acb'/%3e%3cpath%20d='m307.3%20237h30.7v53h-83v235.8l-2.2.6c-3%20.8-42.5.8-51-.1l-6.8-.6v-235.7h-83v-53l26.3-.3c14.4-.2%2051.4-.2%2082.2%200s69.8.3%2086.8.3zm234.3%20263.8c-12.2%2012.9-25.3%2020.1-47.1%2026-9.5%202.6-11.1%202.7-32.5%202.6s-23.1-.1-33.5-2.8c-26.9-6.9-48.6-20.4-63.4-39.5-4.2-5.4-11.1-16.6-11.1-18%200-.4%201-1.3%202.3-1.9s4-2.3%206.2-3.6%206.2-3.7%208.9-5.1%2010.5-6%2017.3-10.1%2013-7.4%2013.7-7.4%202%201.4%203%203.1c6%2010.1%2020%2023%2029.9%2027.4%206.1%202.6%2019.6%205.5%2026.1%205.5%206%200%2017-2.6%2022.9-5.3%206.3-2.9%209.5-5.8%2013.3-11.6%202.6-4.1%202.9-5.2%202.8-13%200-7.2-.4-9.2-2.4-12.5-5.6-9.2-13.2-14-44-27.6-31.8-14.1-46.1-22.5-57.7-33.8-8.6-8.4-10.3-10.7-15.7-21.2-7-13.5-7.9-17.9-8-38-.1-14.1.2-18.7%201.7-23.5%202.1-7.2%208.9-21.1%2012-24.6%206.4-7.5%208.7-9.8%2013.2-13.5%2013.6-11.2%2034.8-18.6%2055.1-19.3%202.3%200%209.9.4%2017%20.9%2020.4%201.7%2034.3%206.7%2047.7%2017.4%2010.1%208%2025.4%2026.8%2023.9%2029.3-1%201.5-40.9%2028.1-43.5%2028.9-1.6.5-2.7-.1-4.9-2.7-13.6-16.3-19.1-19.8-32.3-20.6-9.4-.6-14.4.5-20.7%204.7-6.6%204.4-9.8%2011.1-9.8%2020.4.1%2013.6%205.3%2020%2024.5%2029.5%2012.4%206.1%2023%2011.1%2023.8%2011.1%201.2%200%2026.9%2012.8%2033.6%2016.8%2031.2%2018.3%2043.9%2037.1%2047.2%2069.5%202.4%2024.4-4.5%2046.7-19.5%2062.5z'%20fill='%23000'/%3e%3c/svg%3e")}.no-height{height:0!important}.file-sys-container{display:flex;flex-direction:column;overflow:auto;height:100%}.folder-container{display:flex;flex-direction:column}.custom-scrollbar-2::-webkit-scrollbar{width:5px;height:5px}.custom-scrollbar-2::-webkit-scrollbar-thumb{background:#4a5575;border-radius:5px;visibility:visible;-webkit-transition-property:all;transition-property:all}.folder-container-reverse{flex-direction:column-reverse}.folder,.file{cursor:pointer;border:1px solid #0e1525}.transformer{display:flex;align-items:center}.folder:hover,.file:hover{background:#2b3245}.hide-input{visibility:hidden;position:absolute}.span-text{display:flex;flex-direction:row;align-items:center}.measurable{width:235px}.three-dots{background-image:url("data:image/svg+xml,%3csvg%20viewBox='0%200%2024%2024'%20id='three-dots'%20xmlns='http://www.w3.org/2000/svg'%20fill='%23000000'%3e%3cg%20id='SVGRepo_bgCarrier'%20stroke-width='0'%3e%3c/g%3e%3cg%20id='SVGRepo_tracerCarrier'%20stroke-linecap='round'%20stroke-linejoin='round'%3e%3c/g%3e%3cg%20id='SVGRepo_iconCarrier'%3e%3cg%20id='_20x20_three-dots--grey'%20data-name='20x20/three-dots--grey'%20transform='translate(24)%20rotate(90)'%3e%3crect%20id='Rectangle'%20width='24'%20height='24'%20fill='none'%3e%3c/rect%3e%3ccircle%20id='Oval'%20cx='1'%20cy='1'%20r='1'%20transform='translate(5%2011)'%20stroke='%23000000'%20stroke-miterlimit='10'%20stroke-width='0.5'%3e%3c/circle%3e%3ccircle%20id='Oval-2'%20data-name='Oval'%20cx='1'%20cy='1'%20r='1'%20transform='translate(11%2011)'%20stroke='%23000000'%20stroke-miterlimit='10'%20stroke-width='0.5'%3e%3c/circle%3e%3ccircle%20id='Oval-3'%20data-name='Oval'%20cx='1'%20cy='1'%20r='1'%20transform='translate(17%2011)'%20stroke='%23000000'%20stroke-miterlimit='10'%20stroke-width='0.5'%3e%3c/circle%3e%3c/g%3e%3c/g%3e%3c/svg%3e");background-repeat:no-repeat;background-position:center;background-size:cover;opacity:0}.clickable:hover .three-dots{opacity:1}.not-seen{position:absolute;visibility:hidden}@media screen and (min-width: 768px){.measurable{width:200px}}.hover\:cursor-pointer:hover{cursor:pointer}.hover\:border-vscode-blue:hover{--tw-border-opacity: 1;border-color:rgb(64 120 206 / var(--tw-border-opacity))}.hover\:bg-blue-400:hover{--tw-bg-opacity: 1;background-color:rgb(96 165 250 / var(--tw-bg-opacity))}.hover\:bg-dark-hover:hover{--tw-bg-opacity: 1;background-color:rgb(43 50 69 / var(--tw-bg-opacity))}.hover\:bg-hover-blue:hover{--tw-bg-opacity: 1;background-color:rgb(4 57 94 / var(--tw-bg-opacity))}.hover\:bg-red-500:hover{--tw-bg-opacity: 1;background-color:rgb(239 68 68 / var(--tw-bg-opacity))}.hover\:bg-slate-200:hover{--tw-bg-opacity: 1;background-color:rgb(226 232 240 / var(--tw-bg-opacity))}.hover\:bg-slate-300:hover{--tw-bg-opacity: 1;background-color:rgb(203 213 225 / var(--tw-bg-opacity))}.hover\:bg-slate-500:hover{--tw-bg-opacity: 1;background-color:rgb(100 116 139 / var(--tw-bg-opacity))}.hover\:bg-slate-600:hover{--tw-bg-opacity: 1;background-color:rgb(71 85 105 / var(--tw-bg-opacity))}.hover\:bg-slate-700:hover{--tw-bg-opacity: 1;background-color:rgb(51 65 85 / var(--tw-bg-opacity))}.hover\:bg-vscode-blue:hover{--tw-bg-opacity: 1;background-color:rgb(64 120 206 / var(--tw-bg-opacity))}.hover\:text-blue-400:hover{--tw-text-opacity: 1;color:rgb(96 165 250 / var(--tw-text-opacity))}.hover\:text-white:hover{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}.hover\:underline:hover{text-decoration-line:underline}.focus\:border-cyan-500:focus{--tw-border-opacity: 1;border-color:rgb(6 182 212 / var(--tw-border-opacity))}.focus\:border-red-500:focus{--tw-border-opacity: 1;border-color:rgb(239 68 68 / var(--tw-border-opacity))}.focus\:bg-slate-300:focus{--tw-bg-opacity: 1;background-color:rgb(203 213 225 / var(--tw-bg-opacity))}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.active\:outline-none:active{outline:2px solid transparent;outline-offset:2px} 2 | -------------------------------------------------------------------------------- /dist/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@litecode-ide/virtual-file-system", 3 | "version": "1.1.1", 4 | "type": "module", 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "vite build", 8 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", 9 | "preview": "vite preview", 10 | "test": "echo \"Error: no test specified\"" 11 | }, 12 | "dependencies": { 13 | "@reduxjs/toolkit": "^2.0.1", 14 | "file-saver": "^2.0.5", 15 | "jszip": "^3.10.1", 16 | "localforage": "^1.10.0", 17 | "lodash.clonedeep": "^4.5.0", 18 | "np": "^9.2.0", 19 | "path": "^0.12.7", 20 | "react": "^18.2.0", 21 | "react-dom": "^18.2.0", 22 | "react-redux": "^9.0.2", 23 | "react-tooltip": "^5.25.0", 24 | "redux-persist": "^6.0.0", 25 | "uuid": "^9.0.1" 26 | }, 27 | "devDependencies": { 28 | "@types/file-saver": "^2.0.7", 29 | "@types/lodash.clonedeep": "^4.5.9", 30 | "@types/react": "^18.2.37", 31 | "@types/react-dom": "^18.2.15", 32 | "@types/uuid": "^9.0.7", 33 | "@typescript-eslint/eslint-plugin": "^6.10.0", 34 | "@typescript-eslint/parser": "^6.10.0", 35 | "@vitejs/plugin-react-swc": "^3.5.0", 36 | "autoprefixer": "^10.4.16", 37 | "eslint": "^8.53.0", 38 | "eslint-plugin-react-hooks": "^4.6.0", 39 | "eslint-plugin-react-refresh": "^0.4.4", 40 | "postcss": "^8.4.32", 41 | "tailwindcss": "^3.3.6", 42 | "typescript": "^5.2.2", 43 | "vite": "^5.0.0" 44 | }, 45 | "description": "This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.", 46 | "repository": { 47 | "type": "git", 48 | "url": "git+https://github.com/LiteCode-IDE/virtual-file-system.git" 49 | }, 50 | "main": "./dist/@litecode-ide/virtual-file-system.umd.js", 51 | "module": "./dist/@litecode-ide/virtual-file-system.es.js", 52 | "exports": { 53 | ".": { 54 | "types": "./dist/@litecode-ide/index.d.ts", 55 | "import": "./dist/@litecode-ide/virtual-file-system.es.js", 56 | "require": "./dist/@litecode-ide/virtual-file-system.umd.js" 57 | }, 58 | "./dist/style.css": { 59 | "import": "./dist/style.css" 60 | } 61 | }, 62 | "keywords": [ 63 | "virtual", 64 | "file", 65 | "system", 66 | "file", 67 | "system", 68 | "file", 69 | "explorer", 70 | "file", 71 | "tree", 72 | "folder", 73 | "tabs", 74 | "searching" 75 | ], 76 | "author": "abel-tefera", 77 | "license": "MIT", 78 | "bugs": { 79 | "url": "https://github.com/LiteCode-IDE/virtual-file-system/issues" 80 | }, 81 | "homepage": "https://github.com/LiteCode-IDE/virtual-file-system#readme" 82 | } 83 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: { config: './tailwind.config.js' }, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /sample/breadcrumbs.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiteCode-IDE/virtual-file-system/f79637f985663ba90d1617a7779fd3914eebd570/sample/breadcrumbs.gif -------------------------------------------------------------------------------- /sample/search.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiteCode-IDE/virtual-file-system/f79637f985663ba90d1617a7779fd3914eebd570/sample/search.gif -------------------------------------------------------------------------------- /sample/structure.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiteCode-IDE/virtual-file-system/f79637f985663ba90d1617a7779fd3914eebd570/sample/structure.gif -------------------------------------------------------------------------------- /sample/tabs.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiteCode-IDE/virtual-file-system/f79637f985663ba90d1617a7779fd3914eebd570/sample/tabs.gif -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Breadcrumbs, 3 | FileExplorer, 4 | SearchInput, 5 | SearchResults, 6 | TabsList, 7 | getFileTree, 8 | updateFile, 9 | } from "./lib"; 10 | 11 | function App() { 12 | return ( 13 |
14 |
15 |
16 | 25 | 32 |
33 | 34 | 35 |
36 |
37 | 38 | 39 |
40 |
41 | 42 |
43 |
44 | ); 45 | } 46 | 47 | export default App; 48 | -------------------------------------------------------------------------------- /src/assets/cross.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/css.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/delete.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #000000#000000#000000#000000#000000 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/assets/down-arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 9 | -------------------------------------------------------------------------------- /src/assets/download.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiteCode-IDE/virtual-file-system/f79637f985663ba90d1617a7779fd3914eebd570/src/assets/error.png -------------------------------------------------------------------------------- /src/assets/folder.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/js.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/jsx.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/left-arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | -------------------------------------------------------------------------------- /src/assets/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#000000" 25 | } 26 | -------------------------------------------------------------------------------- /src/assets/new-file-colored.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/assets/new-file.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/assets/new-folder.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/assets/new-project.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | #000000 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /src/assets/open-project.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/assets/opened-folder.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/readme.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/assets/rename.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/assets/three-dots.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/typescript.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/file-structure/Folder.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | collapseOrExpand, 4 | contextSelectedItem, 5 | selectedItem, 6 | contextClick, 7 | clipboard, 8 | setSelected, 9 | type Directory, 10 | type FileInFolder, 11 | ItemType, 12 | } from "../../state/features/structure/structureSlice"; 13 | import { type RootState } from "../../state/store"; 14 | import { useTypedDispatch, useTypedSelector } from "../../state/hooks"; 15 | import { setActiveTabAsync } from "../../state/features/tabs/tabsSlice"; 16 | import CollapseBtn from "./widgets/CollapseBtn"; 17 | import ThreeDots from "./widgets/ThreeDots"; 18 | import ItemTitle from "./widgets/ItemTitle"; 19 | 20 | interface FolderProps { 21 | data: Array; 22 | showBlue: boolean; 23 | setShowBlue: React.Dispatch>; 24 | showGray: boolean; 25 | setShowGray: React.Dispatch>; 26 | collapseBtnClassname?: string; 27 | collapseBtnStyle?: React.CSSProperties; 28 | threeDotPrimaryClass?: string; 29 | threeDotSecondaryClass?: string; 30 | clickableAreaClassName?: string; 31 | selectedClickableAreaClassName?: string; 32 | contextSelectedClickableAreaClassName?: string; 33 | itemTitleClassName?: string; 34 | onItemSelected?: (item: { id: string; type: ItemType }) => void; 35 | onItemContextSelected?: (item: { id: string; type: ItemType }) => void; 36 | } 37 | 38 | const Folder: React.FC = ({ 39 | data, 40 | showBlue, 41 | setShowBlue, 42 | showGray, 43 | setShowGray, 44 | collapseBtnClassname, 45 | collapseBtnStyle, 46 | threeDotPrimaryClass, 47 | threeDotSecondaryClass, 48 | clickableAreaClassName, 49 | selectedClickableAreaClassName, 50 | contextSelectedClickableAreaClassName, 51 | itemTitleClassName, 52 | onItemSelected = () => {}, 53 | onItemContextSelected = () => {}, 54 | }) => { 55 | const dispatch = useTypedDispatch(); 56 | const selected = useTypedSelector(selectedItem); 57 | const contextSelected = useTypedSelector(contextSelectedItem); 58 | const cutItem = useTypedSelector(clipboard); 59 | const children = useTypedSelector((state: RootState) => { 60 | const allData = data.map(({ id: itemId, type }) => { 61 | return state.structure.normalized[`${type}s`].byId[itemId]; 62 | }); 63 | return allData; 64 | }); 65 | 66 | return ( 67 |
0 && "w-full"}`}> 68 | {children.map((item) => { 69 | return ( 70 |
74 |
87 | { 90 | e.stopPropagation(); 91 | dispatch(setSelected({ id: item.id, type: item.type })); 92 | setShowBlue(true); 93 | setShowGray(false); 94 | if (item.type === "file") { 95 | dispatch(setActiveTabAsync(item.id)); 96 | } else { 97 | dispatch( 98 | collapseOrExpand({ 99 | item: { id: item.id, type: item.type }, 100 | collapse: true, 101 | }) 102 | ); 103 | } 104 | onItemSelected({ id: item.id, type: item.type }); 105 | }} 106 | className={itemTitleClassName} 107 | /> 108 | { 115 | e.stopPropagation(); 116 | setShowBlue(false); 117 | setShowGray(true); 118 | dispatch( 119 | contextClick({ 120 | id: item.id, 121 | type: item.type, 122 | threeDot: { x: e.clientY, y: e.clientX }, 123 | }) 124 | ); 125 | onItemContextSelected({ id: item.id, type: item.type }); 126 | }} 127 | /> 128 |
129 | <> 130 |
131 | {item.type === "folder" && !item.collapsed && ( 132 |
133 | { 138 | e.stopPropagation(); 139 | setShowBlue(true); 140 | setShowGray(false); 141 | dispatch(setSelected({ id: item.id, type: item.type })); 142 | dispatch( 143 | collapseOrExpand({ 144 | item: { id: item.id, type: item.type }, 145 | collapse: true, 146 | }) 147 | ); 148 | }} 149 | /> 150 | { 152 | const childFolder = data.find((newItem) => { 153 | return newItem.id === item.id; 154 | }); 155 | return childFolder?.subFoldersAndFiles as Directory[]; 156 | })()} 157 | showBlue={showBlue} 158 | setShowBlue={setShowBlue} 159 | showGray={showGray} 160 | setShowGray={setShowGray} 161 | collapseBtnClassname={collapseBtnClassname} 162 | collapseBtnStyle={collapseBtnStyle} 163 | threeDotPrimaryClass={threeDotPrimaryClass} 164 | threeDotSecondaryClass={threeDotSecondaryClass} 165 | clickableAreaClassName={clickableAreaClassName} 166 | selectedClickableAreaClassName={ 167 | selectedClickableAreaClassName 168 | } 169 | contextSelectedClickableAreaClassName={ 170 | contextSelectedClickableAreaClassName 171 | } 172 | itemTitleClassName={itemTitleClassName} 173 | onItemSelected={onItemSelected} 174 | onItemContextSelected={onItemContextSelected} 175 | /> 176 |
177 | )} 178 | 179 |
180 | ); 181 | })} 182 |
183 | ); 184 | }; 185 | 186 | export default Folder; 187 | -------------------------------------------------------------------------------- /src/components/file-structure/MiniFolder.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ItemTitle from "./widgets/ItemTitle"; 3 | import CollapseBtn from "./widgets/CollapseBtn"; 4 | import { type MiniStructure } from "../../state/features/structure/miniStructureSlice"; 5 | import { type Identifier } from "../../state/features/structure/structureSlice"; 6 | 7 | interface MiniFolderProps { 8 | data: MiniStructure; 9 | init: boolean; 10 | onClickItem: (item: Identifier) => void; 11 | onCollapseMiniStructure: (id: string) => void; 12 | collapseBtnClassName?: string; 13 | collapseBtnStyle?: React.CSSProperties; 14 | containerClassName?: string; 15 | titleClassName?: string; 16 | } 17 | 18 | const MiniFolder: React.FC = ({ 19 | init, 20 | data, 21 | onClickItem, 22 | onCollapseMiniStructure, 23 | collapseBtnClassName, 24 | collapseBtnStyle, 25 | containerClassName, 26 | titleClassName, 27 | }) => { 28 | const children = data.subFoldersAndFiles; 29 | 30 | return ( 31 |
32 | {children.map((item, i) => { 33 | return ( 34 |
35 |
45 | { 48 | onClickItem({ id: item.id, type: item.type }); 49 | }} 50 | className={titleClassName} 51 | /> 52 |
53 | <> 54 | {item.type === "folder" && !item.collapsed && ( 55 |
56 | { 61 | onCollapseMiniStructure(item.id); 62 | }} 63 | 64 | /> 65 | 74 |
75 | )} 76 | 77 |
78 | ); 79 | })} 80 |
81 | ); 82 | }; 83 | 84 | export default MiniFolder; 85 | -------------------------------------------------------------------------------- /src/components/file-structure/Structure.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useEffect, useState } from "react"; 2 | 3 | import { 4 | type ItemType, 5 | getInitialSet, 6 | setContextSelectedForFileAction, 7 | setSelected, 8 | setProjectName, 9 | } from "../../state/features/structure/structureSlice"; 10 | import Folder from "./Folder"; 11 | import useOutsideAlerter from "../../hooks/useOutsideAlerter"; 12 | 13 | import MenuContext from "../menus/MenuContext"; 14 | import CustomInput from "./widgets/CustomInput"; 15 | import { createPortal } from "react-dom"; 16 | 17 | import Dialog from "../menus/Dialog"; 18 | import { 19 | addNode, 20 | collapseOrExpand, 21 | contextClick, 22 | contextSelectedEvent, 23 | contextSelectedItem, 24 | removeNode, 25 | renameNode, 26 | getItem, 27 | setToCopy, 28 | copyNode, 29 | contextSelectedItemType, 30 | contextSelectedObj, 31 | clipboard, 32 | folderIds, 33 | fileIds, 34 | selectedItem, 35 | setParentItemId, 36 | getCurrentItems, 37 | } from "../../state/features/structure/structureSlice"; 38 | import { usePrependPortal } from "../../hooks/usePrependPortal"; 39 | import FileActions from "./widgets/FileActions"; 40 | import { useTypedDispatch, useTypedSelector } from "../../state/hooks"; 41 | import { removeTabAsync } from "../../state/features/tabs/tabsSlice"; 42 | import downloadZip from "../../state/features/structure/utils/downloadZip"; 43 | import { findParent } from "../../state/features/structure/utils/traversal"; 44 | import { store } from "../../state/store"; 45 | 46 | export interface StructureProps { 47 | deleteConfirmationClassName?: string; 48 | fileInputClassName?: string; 49 | fileInputStyle?: React.CSSProperties; 50 | contextMenuClassName?: string; 51 | contextMenuHrColor?: string; 52 | contextMenuClickableAreaClassName?: string; 53 | fileActionsBtnClassName?: string; 54 | projectName?: string; 55 | fileActionsDisableCollapse?: true; 56 | fileActionsDisableTooltip?: true; 57 | fileActionsDisableDownload?: true; 58 | folderCollapseBtnClassname?: string; 59 | folderCollapseBtnStyle?: React.CSSProperties; 60 | folderThreeDotPrimaryClass?: string; 61 | folderThreeDotSecondaryClass?: string; 62 | folderClickableAreaClassName?: string; 63 | folderSelectedClickableAreaClassName?: string; 64 | folderContextSelectedClickableAreaClassName?: string; 65 | itemTitleClassName?: string; 66 | structureContainerClassName?: string; 67 | containerHeight?: string; 68 | onItemSelected?: (item: { id: string; type: ItemType }) => void; 69 | onNewItemClick?: (parentFolderId: string, type: ItemType) => void; 70 | onAreaCollapsed?: (collapsed: boolean) => void; 71 | onItemContextSelected?: (item: { id: string; type: ItemType }) => void; 72 | onNodeDeleted?: (id: string) => void; 73 | onNewItemCreated?: (id: string) => void; 74 | validExtensions: string[]; 75 | } 76 | 77 | const Structure: React.FC = ({ 78 | deleteConfirmationClassName, 79 | fileInputClassName, 80 | fileInputStyle, 81 | contextMenuClassName, 82 | contextMenuHrColor, 83 | contextMenuClickableAreaClassName, 84 | fileActionsBtnClassName, 85 | projectName, 86 | fileActionsDisableCollapse, 87 | fileActionsDisableTooltip, 88 | fileActionsDisableDownload, 89 | folderCollapseBtnClassname, 90 | folderCollapseBtnStyle, 91 | folderThreeDotPrimaryClass, 92 | folderThreeDotSecondaryClass, 93 | folderClickableAreaClassName, 94 | folderSelectedClickableAreaClassName, 95 | folderContextSelectedClickableAreaClassName, 96 | itemTitleClassName, 97 | structureContainerClassName, 98 | containerHeight, 99 | onItemSelected = () => {}, 100 | onNewItemClick = () => {}, 101 | onAreaCollapsed = () => {}, 102 | onItemContextSelected = () => {}, 103 | onNodeDeleted = () => {}, 104 | onNewItemCreated = () => {}, 105 | validExtensions, 106 | }) => { 107 | const fileSysRef = useRef(null); 108 | const structureRef = useRef(null); 109 | const clickedRef = useRef(); 110 | const [structureCollapsed, setStructureCollapsed] = useState(false); 111 | 112 | const dispatch = useTypedDispatch(); 113 | const structureData = useTypedSelector(getInitialSet); 114 | const contextSelectedE = useTypedSelector(contextSelectedEvent); 115 | const contextSelectedItemProps = useTypedSelector(contextSelectedObj); 116 | const contextSelectedId = useTypedSelector(contextSelectedItem); 117 | const contextSelectedType = useTypedSelector(contextSelectedItemType); 118 | const selectedI = useTypedSelector(selectedItem); 119 | const thisItem = useTypedSelector(getItem); 120 | const clipboardExists = useTypedSelector(clipboard); 121 | const allFileIds = useTypedSelector(fileIds); 122 | const allFolderIds = useTypedSelector(folderIds); 123 | const currentItems = useTypedSelector(getCurrentItems); 124 | 125 | const [showBlue, setShowBlue] = useState(true); 126 | const [showGray, setShowGray] = useState(true); 127 | const [showContext, setShowContext] = useState(false); 128 | const [selectedType, setSelectedType] = useState< 129 | "file" | "folder" | "head" | "" 130 | >(""); 131 | 132 | const [points, setPoints] = useState({ 133 | x: 0, 134 | y: 0, 135 | }); 136 | 137 | const appendTo = useRef(null); 138 | 139 | const [showInput, setShowInput] = useState(false); 140 | const [inputPadding, setInputPadding] = useState(0); 141 | 142 | const [inputType, setInputType] = useState<"file" | "folder" | "">(""); 143 | const [isRename, setIsRename] = useState(false); 144 | 145 | const [showDialog, setShowDialog] = useState(false); 146 | 147 | const actions = [ 148 | { 149 | title: "New File", 150 | handler: () => { 151 | setInputType("file"); 152 | createFileInput(); 153 | }, 154 | disabled: selectedType === "file", 155 | }, 156 | { 157 | title: "New Folder", 158 | handler: () => { 159 | setInputType("folder"); 160 | createFileInput(); 161 | }, 162 | disabled: selectedType === "file", 163 | }, 164 | { 165 | type: "hr", 166 | handler: () => {}, 167 | }, 168 | { 169 | title: "Cut", 170 | handler: () => { 171 | dispatch( 172 | setToCopy({ 173 | id: contextSelectedId, 174 | type: contextSelectedType as ItemType, 175 | isCut: true, 176 | }) 177 | ); 178 | }, 179 | disabled: selectedType === "head", 180 | }, 181 | { 182 | title: "Copy", 183 | handler: () => { 184 | dispatch( 185 | setToCopy({ 186 | id: contextSelectedId, 187 | type: contextSelectedType as ItemType, 188 | isCut: false, 189 | }) 190 | ); 191 | }, 192 | disabled: selectedType === "head", 193 | }, 194 | { 195 | title: "Paste", 196 | handler: async () => { 197 | dispatch(copyNode()); 198 | if (clipboardExists !== null && clipboardExists.isCut) { 199 | await dispatch(removeTabAsync()); 200 | // await dispatch(setActiveEditorAsync({ id: '', line: 0 })); 201 | } 202 | }, 203 | disabled: selectedType === "file" || clipboardExists === null, 204 | }, 205 | { 206 | type: "hr", 207 | handler: () => {}, 208 | }, 209 | { 210 | title: "Rename", 211 | handler: () => { 212 | setInputType( 213 | clickedRef.current?.getAttribute("typeof-item") as 214 | | "file" 215 | | "folder" 216 | | "" 217 | ); 218 | createFileInputForRename(); 219 | setIsRename(true); 220 | }, 221 | disabled: selectedType === "head", 222 | }, 223 | { 224 | title: "Delete", 225 | handler: () => { 226 | setShowDialog(true); 227 | }, 228 | disabled: selectedType === "head", 229 | }, 230 | ]; 231 | 232 | const setClickedCurrent = (selectedItem: string = selectedI) => { 233 | let elem = fileSysRef.current?.querySelector(`#${selectedItem}`); 234 | if (!elem) { 235 | elem = fileSysRef.current; 236 | } 237 | clickedRef.current = elem as HTMLElement; 238 | }; 239 | 240 | const fileActions = { 241 | newFile: () => { 242 | setInputType("file"); 243 | const parentId = findParent(selectedI, allFileIds, structureData); 244 | 245 | dispatch(setContextSelectedForFileAction(parentId)); 246 | setClickedCurrent(parentId); 247 | createFileInput(parentId); 248 | onNewItemClick(parentId, "file"); 249 | }, 250 | 251 | newFolder: () => { 252 | setInputType("folder"); 253 | const parentId = findParent(selectedI, allFileIds, structureData); 254 | dispatch(setContextSelectedForFileAction(parentId)); 255 | setClickedCurrent(parentId); 256 | createFileInput(parentId); 257 | onNewItemClick(parentId, "folder"); 258 | }, 259 | 260 | download: () => { 261 | downloadZip(); 262 | }, 263 | collapseArea: () => { 264 | if (!fileSysRef.current) return; 265 | if (structureCollapsed) { 266 | fileSysRef.current.classList.remove("no-height"); 267 | } else { 268 | fileSysRef.current.classList.add("no-height"); 269 | } 270 | setStructureCollapsed(!structureCollapsed); 271 | onAreaCollapsed(!structureCollapsed); 272 | }, 273 | }; 274 | 275 | const prependForPortal = (isRename: boolean) => { 276 | if (!clickedRef.current) { 277 | setClickedCurrent(); 278 | } 279 | if (!clickedRef.current) { 280 | return; 281 | } 282 | if ( 283 | clickedRef.current === fileSysRef.current || 284 | (clickedRef.current.id.includes("file") && !isRename) 285 | ) { 286 | appendTo.current = fileSysRef.current as HTMLElement; 287 | 288 | setInputPadding(0); 289 | } else { 290 | if (!isRename) { 291 | dispatch( 292 | collapseOrExpand({ 293 | item: { id: clickedRef.current.id, type: "folder" }, 294 | collapse: false, 295 | }) 296 | ); 297 | } 298 | 299 | if (isRename) { 300 | appendTo.current = clickedRef.current.parentElement as HTMLElement; 301 | clickedRef.current.classList.add("hide-input"); 302 | setInputPadding(0); 303 | } else { 304 | appendTo.current = structureRef.current?.querySelector( 305 | "#ghost-input-" + clickedRef.current.id 306 | ) as HTMLElement; 307 | setInputPadding(1); 308 | } 309 | } 310 | }; 311 | 312 | const showInputHandler = (v: boolean) => { 313 | if (v === showInput) return; 314 | setShowInput(v); 315 | if (allFileIds.length === 0 && allFolderIds.length === 1) { 316 | const welcome = document.getElementById("welcome") as HTMLElement; 317 | if (v && !welcome.classList.contains("display-none-c")) { 318 | welcome.classList.add("display-none-c"); 319 | } else if (!v && welcome.classList.contains("display-none-c")) { 320 | welcome.classList.remove("display-none-c"); 321 | } 322 | } 323 | }; 324 | 325 | const createFileInput = (parentId: string = contextSelectedId) => { 326 | if (!fileSysRef.current) return; 327 | if (structureCollapsed) { 328 | fileSysRef.current.classList.remove("no-height"); 329 | setStructureCollapsed(false); 330 | } 331 | dispatch(setParentItemId(parentId)); 332 | prependForPortal(false); 333 | showInputHandler(true); 334 | }; 335 | 336 | const createFileInputForRename = () => { 337 | dispatch(setParentItemId("")); 338 | prependForPortal(true); 339 | showInputHandler(true); 340 | }; 341 | 342 | const inputSubmit = (value: string | false) => { 343 | if (!clickedRef.current) return; 344 | if (isRename || value === false) { 345 | showInputHandler(false); 346 | clickedRef.current?.classList.remove("hide-input"); 347 | if (isRename && value !== false) { 348 | dispatch(renameNode({ value })); 349 | } 350 | setIsRename(false); 351 | return; 352 | } else { 353 | dispatch(addNode({ value, inputType: inputType as ItemType })); 354 | } 355 | 356 | showInputHandler(false); 357 | const allFileIds = store.getState().structure.normalized.files.allIds; 358 | onNewItemCreated(allFileIds[allFileIds.length - 1]); 359 | }; 360 | 361 | useEffect(() => { 362 | if (projectName === undefined) return; 363 | dispatch(setProjectName(projectName)); 364 | }, [projectName]); 365 | 366 | useEffect(() => { 367 | if (isRename && !showInput) { 368 | clickedRef.current?.classList.remove("hide-input"); 369 | setIsRename(false); 370 | } 371 | }, [isRename, showInput]); 372 | 373 | const handleContext = ( 374 | e: { clientY: number; clientX: number }, 375 | elem: HTMLElement 376 | ) => { 377 | if (!fileSysRef.current || !elem) return; 378 | const type = elem.getAttribute("typeof-item") as "file" | "folder" | ""; 379 | const parentId = elem.getAttribute("parent-id") as string; 380 | if (type === null || parentId === null) { 381 | if ( 382 | !elem.classList.contains("welcome") && 383 | !elem.classList.contains("clickable-padding") 384 | ) { 385 | return; 386 | } else if (elem.classList.contains("file-sys-ref")) { 387 | clickedRef.current = elem; 388 | } 389 | } 390 | 391 | let item: HTMLElement | null = null; 392 | 393 | if (!elem.classList.contains("file-sys-container")) { 394 | item = fileSysRef.current.querySelector(`#${parentId}`); 395 | } else { 396 | item = fileSysRef.current; 397 | } 398 | 399 | clickedRef.current = item as HTMLElement; 400 | let x = e.clientY, 401 | y = e.clientX; 402 | 403 | if (e.clientY > window.innerHeight / 2) { 404 | x = e.clientY - 245; 405 | } 406 | if (e.clientX > window.innerWidth / 2) { 407 | y = e.clientX - 192; 408 | } 409 | 410 | setPoints({ 411 | x, 412 | y, 413 | }); 414 | 415 | setSelectedType(parentId === "head" ? "head" : type); 416 | setShowContext(true); 417 | }; 418 | const contextHandler = (e: React.MouseEvent) => { 419 | e.preventDefault(); 420 | if (!fileSysRef.current) return; 421 | const elem = e.target as HTMLElement; 422 | handleContext({ clientY: e.clientY, clientX: e.clientX }, elem); 423 | const parentId = elem.getAttribute("parent-id") as string; 424 | const type = elem.getAttribute("typeof-item") as "file" | "folder" | ""; 425 | 426 | dispatch(contextClick({ id: parentId, type, threeDot: false })); 427 | }; 428 | 429 | useEffect(() => { 430 | if (!contextSelectedE) return; 431 | let elem: HTMLElement; 432 | if (contextSelectedId === "head") { 433 | elem = document.querySelector(".main-nav") as HTMLElement; 434 | } else { 435 | elem = fileSysRef.current?.querySelector(`#${contextSelectedId}`) 436 | ?.childNodes[0] as HTMLElement; 437 | } 438 | handleContext( 439 | { clientY: contextSelectedE.x, clientX: contextSelectedE.y }, 440 | elem 441 | ); 442 | }, [contextSelectedE]); 443 | 444 | useOutsideAlerter(structureRef, () => { 445 | if (selectedI !== "head") { 446 | setShowBlue(false); 447 | setShowGray(false); 448 | } 449 | fileSysRef.current?.classList.add("border-transparent"); 450 | fileSysRef.current?.classList.remove("border-vscode-blue"); 451 | }); 452 | 453 | useEffect(() => { 454 | setShowBlue(true); 455 | }, [selectedI]); 456 | 457 | return ( 458 | <> 459 |
460 |
466 |
467 | 476 |
477 |
{ 488 | dispatch(setSelected({ id: "head", type: "folder" })); 489 | onItemSelected({ id: "head", type: "folder" }); 490 | }} 491 | onContextMenu={(e) => { 492 | contextHandler(e); 493 | }} 494 | // onClick={(e) => fileStructureClickHandler(e, fileSysRef)} 495 | > 496 |
502 | 523 | 524 | {allFileIds.length === 0 && allFolderIds.length === 1 && ( 525 |
e.stopPropagation()} 530 | onContextMenu={(e) => { 531 | contextHandler(e); 532 | onItemContextSelected({ id: "head", type: "folder" }); 533 | }} 534 | className="mx-auto flex items-center pl-3 pr-4" 535 | > 536 |
541 |
546 |
551 | Create a file or folder... 552 |
553 |
558 | 576 | 594 |
595 |
596 |
597 |
598 | )} 599 |
600 |
605 |   606 |
607 |
608 |
609 | {showDialog && 610 | createPortal( 611 | { 619 | onNodeDeleted(contextSelectedItemProps.id); 620 | dispatch(removeNode({ id: null, type: null })); 621 | await dispatch(removeTabAsync()); 622 | // await dispatch(setActiveEditorAsync({ id: '', line: 0 })); 623 | setShowDialog(false); 624 | }} 625 | />, 626 | document.getElementById("root") as HTMLElement 627 | )} 628 | 629 | {showContext && 630 | createPortal( 631 | , 641 | document.getElementById("root") as HTMLElement 642 | )} 643 |
644 | 645 | {usePrependPortal( 646 | { 650 | showInputHandler(false); 651 | }} 652 | submit={(value) => { 653 | inputSubmit(value); 654 | }} 655 | validExtensions={validExtensions} 656 | padding={inputPadding} 657 | show={clickedRef.current && showInput} 658 | item={{ 659 | type: inputType, 660 | rename: isRename 661 | ? { 662 | wholeName: 663 | thisItem.type === "file" 664 | ? `${thisItem.name}.${thisItem.extension}` 665 | : thisItem.name, 666 | } 667 | : undefined, 668 | }} 669 | container={fileSysRef.current} 670 | existingItems={(() => { 671 | const items = currentItems.map((item) => { 672 | return { 673 | id: item.id, 674 | type: item.type, 675 | wholeName: 676 | item.type === "file" 677 | ? `${item.name}.${item.extension}` 678 | : item.name, 679 | }; 680 | }); 681 | if (isRename) { 682 | return items.filter(({ id }) => id !== thisItem?.id); 683 | } else { 684 | return items; 685 | } 686 | })()} 687 | />, 688 | appendTo.current as HTMLElement 689 | )} 690 | 691 | ); 692 | }; 693 | 694 | export default Structure; 695 | -------------------------------------------------------------------------------- /src/components/file-structure/search/HighlightedText.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | interface HighlightedTextProps { 4 | hightlight: string; 5 | highlightClass?: string; 6 | lineOfText: string; 7 | lineNum: number; 8 | openAtLine: (lineNum: number) => void; 9 | } 10 | 11 | const HighlightedText: React.FC = ({ 12 | hightlight, 13 | lineOfText, 14 | lineNum, 15 | openAtLine, 16 | highlightClass 17 | }) => { 18 | 19 | const parts = lineOfText.split(new RegExp(`(${hightlight})`, "gi")); 20 | return ( 21 |
{ 23 | openAtLine(lineNum); 24 | }} 25 | className="whitespace-nowrap my-1 ml-3 pl-1 cursor-pointer hover:bg-slate-200"> 26 | {parts.map((part) => 27 | (() => { 28 | if (part === hightlight) { 29 | return {part}; 30 | } 31 | return part; 32 | })(), 33 | )} 34 |
35 | ); 36 | }; 37 | 38 | export default HighlightedText; 39 | -------------------------------------------------------------------------------- /src/components/file-structure/search/SearchContainer.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import SearchResults from "./SearchResults"; 3 | import { useTypedDispatch, useTypedSelector } from "../../../state/hooks"; 4 | import { 5 | getSearchResults, 6 | setSelected, 7 | } from "../../../state/features/structure/structureSlice"; 8 | import { 9 | selectedTab, 10 | setActiveTabAsync, 11 | } from "../../../state/features/tabs/tabsSlice"; 12 | 13 | export interface SearchContainerProps { 14 | highlightedTextClassName?: string; 15 | headerClassName?: string; 16 | headerStyle?: React.CSSProperties; 17 | titleClassName?: string; 18 | searchResultClicked: (fileId: string, line: number) => void; 19 | } 20 | 21 | const SearchContainer: React.FC = ({ 22 | highlightedTextClassName, 23 | headerClassName, 24 | headerStyle, 25 | titleClassName, 26 | searchResultClicked, 27 | }) => { 28 | const dispatch = useTypedDispatch(); 29 | const searchData = useTypedSelector(getSearchResults); 30 | const selected = useTypedSelector(selectedTab); 31 | 32 | const openResult = (id: string, line: number) => { 33 | if (selected !== id) { 34 | dispatch(setSelected({ id, type: "file" })); 35 | dispatch(setActiveTabAsync(id)); 36 | } 37 | searchResultClicked(id, line); 38 | }; 39 | 40 | return ( 41 |
42 |
43 | {searchData.numOfLines} result{searchData.numOfLines !== 1 && "s"} in{" "} 44 | {searchData.numOfResults} file{searchData.numOfResults !== 1 && "s"} 45 |
46 | {searchData.files.map((file) => ( 47 |
48 | 56 |
57 | ))} 58 |
59 | ); 60 | }; 61 | 62 | export default SearchContainer; 63 | -------------------------------------------------------------------------------- /src/components/file-structure/search/SearchInput.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef, useState } from "react"; 2 | import { useTypedDispatch, useTypedSelector } from "../../../state/hooks"; 3 | import { 4 | SearchResults, 5 | getSearchResults, 6 | search, 7 | } from "../../../state/features/structure/structureSlice"; 8 | 9 | export interface SearchInputProps { 10 | className?: string; 11 | style?: React.CSSProperties; 12 | onSearchFiles?: (searchTerm: string, searchResults: SearchResults) => void; 13 | } 14 | 15 | const SearchInput: React.FC = ({ 16 | className, 17 | style, 18 | onSearchFiles = () => {}, 19 | }) => { 20 | const [searchTerm, setSearchTerm] = useState(""); 21 | 22 | const searchInputRef = useRef(null); 23 | const dispatch = useTypedDispatch(); 24 | const searchResults = useTypedSelector(getSearchResults); 25 | 26 | const searchFiles = (searchTermNew: string) => { 27 | if (searchTermNew.length > 0) { 28 | const timer = setTimeout(() => { 29 | dispatch(search(searchTermNew)); 30 | }, 300); 31 | return () => { 32 | clearTimeout(timer); 33 | }; 34 | } else { 35 | dispatch(search("")); 36 | } 37 | }; 38 | 39 | useEffect(() => { 40 | if (searchTerm.length > 0) { 41 | onSearchFiles(searchTerm, searchResults); 42 | } 43 | }, [searchResults]); 44 | 45 | return ( 46 |
47 |
{ 49 | e.preventDefault(); 50 | searchFiles(searchTerm); 51 | }} 52 | > 53 | { 56 | const searchTerm = e.currentTarget.value; 57 | setSearchTerm(searchTerm); 58 | searchFiles(searchTerm); 59 | }} 60 | value={searchTerm} 61 | placeholder="Search" 62 | style={style} 63 | className={`w-full self-center rounded-lg p-2 bg-slate-100 hover:bg-slate-300 focus:bg-slate-300 focus:outline-none active:outline-none text-black ${className}`} 64 | /> 65 |
66 |
67 | ); 68 | }; 69 | 70 | export default SearchInput; 71 | -------------------------------------------------------------------------------- /src/components/file-structure/search/SearchResults.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import ItemTitle from "../widgets/ItemTitle"; 3 | import downArrowLogo from "../../../assets/left-arrow.svg"; 4 | import HighlightedText from "./HighlightedText"; 5 | import { 6 | type MatchingFile, 7 | getSearchTerm, 8 | } from "../../../state/features/structure/structureSlice"; 9 | import { useTypedSelector } from "../../../state/hooks"; 10 | 11 | interface SearchResultsProps { 12 | matchingFile: MatchingFile; 13 | fileAtLineClick: (id: string, lineNum: number) => void; 14 | headerClassName?: string; 15 | headerStyle?: React.CSSProperties; 16 | titleClassName?: string; 17 | highlightClass?: string; 18 | } 19 | 20 | const SearchResults: React.FC = ({ 21 | matchingFile, 22 | fileAtLineClick, 23 | headerClassName, 24 | headerStyle, 25 | titleClassName, 26 | highlightClass 27 | }) => { 28 | const [showResults, setShowResults] = useState(true); 29 | const searchTerm = useTypedSelector(getSearchTerm); 30 | return ( 31 |
32 |
{ 34 | setShowResults(!showResults); 35 | }} 36 | style={headerStyle} 37 | className={`flex items-center w-full cursor-pointer hover:bg-slate-200 ${headerClassName}`} 38 | > 39 | Right Arrow 46 | {}} 52 | className={titleClassName} 53 | /> 54 |
55 | {showResults && ( 56 |
57 | {matchingFile.matches.map(({ line, content }, i) => ( 58 | { 64 | fileAtLineClick(matchingFile.id, l); 65 | }} 66 | highlightClass={highlightClass} 67 | /> 68 | ))} 69 |
70 | )} 71 |
72 | ); 73 | }; 74 | 75 | export default SearchResults; 76 | -------------------------------------------------------------------------------- /src/components/file-structure/utils/index.ts: -------------------------------------------------------------------------------- 1 | const trimName = ( 2 | item: 3 | | { 4 | name: string; 5 | type: "folder"; 6 | } 7 | | { 8 | name: string; 9 | type: "file"; 10 | extension: string; 11 | } 12 | ) => { 13 | let newName = ""; 14 | if (item.type === "file") { 15 | const fullName = `${item.name}.${item.extension}`; 16 | // const [fname, ext] = fullName.split("."); 17 | // if (fname.length > 10) { 18 | // newName = `${fname.slice(0, 10)}...${ext}`; 19 | // } else { 20 | // newName = fullName; 21 | // } 22 | newName = fullName; 23 | } else if (item.type === "folder") { 24 | // if (item.name.length > 12) { 25 | // newName = `${item.name.slice(0, 12)}...`; 26 | // } else { 27 | // newName = item.name; 28 | // } 29 | newName = item.name; 30 | } 31 | return newName; 32 | }; 33 | 34 | const getLogo = (fileType: string) => { 35 | return `${fileType}-logo`; 36 | // let logo: string = ""; 37 | // switch (fileType) { 38 | // case "js": 39 | // logo = "js-logo"; 40 | // break; 41 | // case "jsx": 42 | // logo = "jsx-logo"; 43 | // break; 44 | // case "css": 45 | // logo = "css-logo"; 46 | // break; 47 | // case "md": 48 | // logo = "md-logo"; 49 | // break; 50 | // case "ts": 51 | // case "tsx": 52 | // logo = "typescript-logo"; 53 | // break; 54 | // default: 55 | // logo = "file-logo"; 56 | // break; 57 | // } 58 | // return logo; 59 | }; 60 | 61 | const validateFile = ( 62 | preValidate: true | undefined, 63 | value: string, 64 | existingItems: Array<{ wholeName: string; type: string }>, 65 | validExtensions: string[], 66 | item: { 67 | type: "file" | "folder" | ""; 68 | rename: 69 | | { 70 | wholeName?: string; 71 | } 72 | | undefined; 73 | } 74 | ): { error: boolean; errorMessage: string; ext?: string } => { 75 | // const regexp = new RegExp(/^(.*?)(\.[^.]*)?$/); 76 | const regex = /^([^\\]*)\.(\w+)$/; 77 | const regexFileName = 78 | /^[a-zA-Z0-9](?:[a-zA-Z0-9 ._-]*[a-zA-Z0-9])?\.[a-zA-Z0-9_-]+$/; 79 | 80 | const lettersNumbersSymbols = /^[a-z0-9._-]+$/i; 81 | 82 | const isValid = value.match(regexFileName); 83 | const matches = value.match(regex); 84 | const isLns = value.match(lettersNumbersSymbols); 85 | 86 | if (matches && isLns) { 87 | const validFiles = validExtensions; 88 | 89 | const filename = matches[1]; 90 | const ext = matches[2]; 91 | // setExtension(ext); 92 | if (isValid && isLns && validFiles.includes(ext)) { 93 | for (const { wholeName: name, type } of existingItems) { 94 | if ( 95 | name.toLowerCase() === value.toLowerCase() && 96 | type === item.type && 97 | name.split(".").reverse()[0] === ext 98 | ) { 99 | return { 100 | error: true, 101 | errorMessage: 102 | "A file with this name already exists. Please choose a different name.", 103 | }; 104 | 105 | // setError(true); 106 | // setLogo(errorIcon); 107 | // setErrorMessage( 108 | // "A file with this name already exists. Please choose a different name." 109 | // ); 110 | } 111 | } 112 | return { 113 | error: false, 114 | errorMessage: "", 115 | ext, 116 | }; 117 | // setLogo(getLogo(ext)); 118 | // setError(false); 119 | // setErrorMessage(""); 120 | } else if (ext !== "" || !isValid) { 121 | // setError(true); 122 | // setLogo(errorIcon); 123 | if (validFiles.includes(ext)) { 124 | if (filename === "") { 125 | return { 126 | error: true, 127 | errorMessage: 128 | "The file name cannot be empty. Please enter a valid file name.", 129 | }; 130 | // setErrorMessage( 131 | // "The file name cannot be empty. Please enter a valid file name." 132 | // ); 133 | } else { 134 | return { 135 | error: true, 136 | errorMessage: 137 | "This name is not valid as a file name. Please choose a different name.", 138 | }; 139 | // setErrorMessage( 140 | // "This name is not valid as a file name. Please choose a different name." 141 | // ); 142 | } 143 | } else { 144 | return { 145 | error: true, 146 | errorMessage: 147 | "This file type is not supported. Please choose a different file extension.", 148 | }; 149 | // setErrorMessage( 150 | // "This file type is not supported. Please choose a different file extension." 151 | // ); 152 | } 153 | } 154 | } else if (!isLns && value !== "") { 155 | return { 156 | error: true, 157 | errorMessage: 158 | "This name is not valid as a file name. Please choose a different name.", 159 | }; 160 | // setError(true); 161 | // setLogo(errorIcon); 162 | // setErrorMessage( 163 | // "This name is not valid as a file name. Please choose a different name." 164 | // ); 165 | } else if (preValidate) { 166 | return { 167 | error: true, 168 | errorMessage: 169 | "The file type cannot be empty. Please choose a valid file extension.", 170 | }; 171 | // setError(true); 172 | // setLogo(errorIcon); 173 | // setErrorMessage( 174 | // "The file type cannot be empty. Please choose a valid file extension." 175 | // ); 176 | } 177 | return { 178 | error: true, 179 | errorMessage: "", 180 | }; 181 | // setError(true); 182 | // setLogo(originalLogo); 183 | // setErrorMessage(""); 184 | }; 185 | 186 | const validateFolder = ( 187 | value: string, 188 | existingItems: Array<{ wholeName: string; type: string }> 189 | ): { error: boolean; errorMessage: string } => { 190 | const regex = /^[a-zA-Z0-9_[\]-\s]+$/; 191 | const isValid = value.match(regex); 192 | 193 | if (isValid || value === "") { 194 | for (const { wholeName: name, type } of existingItems) { 195 | if (name.toLowerCase() === value.toLowerCase() && type === "folder") { 196 | return { 197 | error: true, 198 | errorMessage: 199 | "A folder with this name already exists. Please choose a different name.", 200 | }; 201 | // setError(true); 202 | // setLogo(errorIcon); 203 | // setErrorMessage( 204 | // "A folder with this name already exists. Please choose a different name." 205 | // ); 206 | } 207 | } 208 | return { 209 | error: false, 210 | errorMessage: "", 211 | }; 212 | // setError(false); 213 | // setErrorMessage(""); 214 | // setLogo(originalLogo); 215 | } else { 216 | return { 217 | error: true, 218 | errorMessage: 219 | "This name is not valid as a folder name. Please choose a different name.", 220 | }; 221 | // setError(true); 222 | // setLogo(errorIcon); 223 | // setErrorMessage( 224 | // "This name is not valid as a folder name. Please choose a different name." 225 | // ); 226 | } 227 | }; 228 | 229 | const validate = ( 230 | preValidate: true | undefined, 231 | item: { 232 | type: "file" | "folder" | ""; 233 | rename: 234 | | { 235 | wholeName?: string; 236 | } 237 | | undefined; 238 | }, 239 | value: string, 240 | existingItems: Array<{ wholeName: string; type: string }>, 241 | validExtensions: string[] 242 | ) => { 243 | if (item.type === "file") { 244 | return validateFile( 245 | preValidate, 246 | value, 247 | existingItems, 248 | validExtensions, 249 | item 250 | ); 251 | } else if (item.type === "folder") { 252 | return validateFolder(value, existingItems); 253 | } 254 | return { 255 | error: true, 256 | errorMessage: "", 257 | }; 258 | }; 259 | 260 | export { getLogo, trimName, validate }; 261 | -------------------------------------------------------------------------------- /src/components/file-structure/widgets/CollapseBtn.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { type ItemType } from "../../../state/features/structure/structureSlice"; 3 | 4 | interface CollapseBtnProps { 5 | item: { 6 | id: string; 7 | name: string; 8 | type: ItemType; 9 | }; 10 | className?: string; 11 | style?: React.CSSProperties; 12 | onClickE: (e: React.MouseEvent) => void; 13 | } 14 | 15 | const CollapseBtn: React.FC = ({ 16 | item, 17 | onClickE, 18 | className, 19 | style, 20 | }) => { 21 | return ( 22 | 32 | ); 33 | }; 34 | 35 | export default CollapseBtn; 36 | -------------------------------------------------------------------------------- /src/components/file-structure/widgets/CustomInput.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useRef, useEffect } from "react"; 2 | import useOutsideAlerter from "../../../hooks/useOutsideAlerter"; 3 | 4 | import { getLogo, validate } from "../utils"; 5 | 6 | const newFileIcon = "new-file-logo"; 7 | const errorIcon = "error-logo"; 8 | const addFolderIcon = "closed-folder"; 9 | const renameIcon = "rename-logo"; 10 | 11 | interface CustomInputProps { 12 | closeCallback: React.Dispatch>; 13 | submit: (value: string | false) => void; 14 | padding: number; 15 | show: boolean | undefined; 16 | item: { 17 | type: "file" | "folder" | ""; 18 | rename: 19 | | { 20 | wholeName?: string; 21 | } 22 | | undefined; 23 | }; 24 | container: HTMLDivElement | null; 25 | validExtensions: string[]; 26 | existingItems: Array<{ wholeName: string; type: string }>; 27 | className?: string; 28 | style?: React.CSSProperties; 29 | } 30 | 31 | const CustomInput: React.FC = ({ 32 | closeCallback, 33 | submit, 34 | padding, 35 | show, 36 | item, 37 | container, 38 | validExtensions, 39 | existingItems, 40 | className, 41 | style, 42 | }) => { 43 | const [value, setValue] = useState( 44 | item.rename?.wholeName ? item.rename.wholeName : "" 45 | ); 46 | const containerRef = useRef(null); 47 | const inputRef = useRef(null); 48 | const errorRef = useRef(null); 49 | const [error, setError] = useState(false); 50 | const [errorMessage, setErrorMessage] = useState(""); 51 | // const [extension, setExtension] = useState(""); 52 | const originalLogo = item.rename 53 | ? renameIcon 54 | : item.type === "file" 55 | ? newFileIcon 56 | : addFolderIcon; 57 | const [logo, setLogo] = useState(originalLogo); 58 | 59 | const [position, setPosition] = useState<"top" | "bottom">("bottom"); 60 | 61 | const direction = ( 62 | container: HTMLDivElement | null 63 | ): "top" | "bottom" | "" => { 64 | if (!container) return ""; 65 | if (!containerRef.current) return ""; 66 | const containerTop = container.offsetTop; 67 | const containerScrollTop = container.scrollTop; 68 | 69 | const elementTop = containerRef.current.offsetTop; 70 | const elementRelativeTop = elementTop - containerTop; 71 | 72 | if ( 73 | !( 74 | elementRelativeTop - containerScrollTop < 393 && 75 | containerScrollTop < elementRelativeTop 76 | ) 77 | ) { 78 | return ""; 79 | } else if (elementRelativeTop - containerScrollTop < 196) { 80 | return "bottom"; 81 | } else if (containerScrollTop - 196 < elementRelativeTop) { 82 | return "top"; 83 | } else { 84 | return ""; 85 | } 86 | }; 87 | 88 | const setValidationResult = (res: { 89 | error: boolean; 90 | errorMessage: string; 91 | ext?: string; 92 | }) => { 93 | if (res.error) { 94 | if (res.errorMessage !== "") { 95 | setError(true); 96 | setLogo(errorIcon); 97 | setErrorMessage(res.errorMessage); 98 | } else { 99 | setError(true); 100 | setLogo(originalLogo); 101 | setErrorMessage(""); 102 | } 103 | } else { 104 | setError(false); 105 | if (item.type === "file") { 106 | setLogo(getLogo(res.ext!)); 107 | } else { 108 | setLogo(originalLogo); 109 | } 110 | setErrorMessage(""); 111 | } 112 | }; 113 | 114 | useEffect(() => { 115 | if (!errorRef.current || !error || errorMessage === "" || !container) { 116 | return; 117 | } 118 | const changeDirection = direction(container); 119 | if (changeDirection !== "" && changeDirection !== position) { 120 | setPosition(changeDirection); 121 | } 122 | }, [error, errorMessage, container, position]); 123 | 124 | useOutsideAlerter(containerRef, () => { 125 | if (!error && value.length > 0) { 126 | submit(value); 127 | } 128 | closeCallback(false); 129 | }); 130 | 131 | useEffect(() => { 132 | if (!inputRef.current) return; 133 | setTimeout(() => { 134 | inputRef.current?.focus(); 135 | if (item.rename) { 136 | const idx = item.rename.wholeName?.lastIndexOf("."); 137 | inputRef.current?.select(); 138 | if (idx !== undefined && idx !== -1) { 139 | inputRef.current?.setSelectionRange(0, idx); 140 | } 141 | } 142 | }, 0); 143 | }, [show, item.rename]); 144 | 145 | useEffect(() => { 146 | const res = validate( 147 | undefined, 148 | item, 149 | value, 150 | existingItems, 151 | validExtensions 152 | ); 153 | setValidationResult(res); 154 | }, [value]); 155 | 156 | return ( 157 |
164 |
165 | 166 |   167 | 168 |
169 | { 179 | setValue(e.target.value); 180 | }} 181 | onKeyDown={(e) => { 182 | if (e.key === "Enter") { 183 | if (!error && value.trim().length > 0) { 184 | submit(value); 185 | } else if (value.trim().length === 0) { 186 | setError(true); 187 | setLogo(errorIcon); 188 | setErrorMessage( 189 | `The ${item.type} name cannot be empty. Please enter a valid name.` 190 | ); 191 | } else { 192 | const res = validate( 193 | true, 194 | item, 195 | value, 196 | existingItems, 197 | validExtensions 198 | ); 199 | setValidationResult(res); 200 | } 201 | } else if (e.key === "Escape") { 202 | submit(false); 203 | } 204 | }} 205 | ref={inputRef} 206 | /> 207 | 208 | {error && errorMessage !== "" && ( 209 |
215 | {errorMessage} 216 |
217 | )} 218 |
219 |
220 |
221 | ); 222 | }; 223 | 224 | export default CustomInput; 225 | -------------------------------------------------------------------------------- /src/components/file-structure/widgets/FileActions.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import downArrowLogo from "../../../assets/left-arrow.svg"; 3 | import newFileIcon from "../../../assets/new-file.svg"; 4 | import newFolderIcon from "../../../assets/new-folder.svg"; 5 | import downloadIcon from "../../../assets/download.svg"; 6 | 7 | import { Tooltip } from "react-tooltip"; 8 | 9 | interface FileActionProps { 10 | newFile: () => void; 11 | newFolder: () => void; 12 | download: () => void; 13 | collapseArea: () => void; 14 | collapsed: boolean; 15 | projectName?: string; 16 | btnClassName?: string; 17 | disableTooltip?: true; 18 | disableCollapse?: true; 19 | disableDownload?: true; 20 | } 21 | 22 | const FileActions: React.FC = ({ 23 | newFile, 24 | newFolder, 25 | download, 26 | collapseArea, 27 | collapsed, 28 | btnClassName, 29 | projectName, 30 | disableCollapse, 31 | disableTooltip, 32 | disableDownload, 33 | }) => { 34 | return ( 35 |
{ 37 | if (!disableCollapse) { 38 | collapseArea(); 39 | } 40 | }} 41 | className={`flex w-full select-none flex-row items-center ${ 42 | !disableCollapse ? "cursor-pointer" : "cursor-default" 43 | }`} 44 | > 45 | Down Arrow 52 | 53 | 54 | {projectName ? projectName : "Files"} 55 | 56 | 57 | 58 | {!disableTooltip && ( 59 | 64 | )} 65 | 66 | 82 | 83 | 84 | {!disableTooltip && ( 85 | 90 | )} 91 | 92 | 108 | 109 | {!disableDownload && ( 110 | 111 | {!disableTooltip && ( 112 | 117 | )} 118 | 119 | 135 | 136 | )} 137 | 138 | 139 |
140 | ); 141 | }; 142 | 143 | export default FileActions; 144 | -------------------------------------------------------------------------------- /src/components/file-structure/widgets/ItemTitle.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { type ItemType } from "../../../state/features/structure/structureSlice"; 3 | import { getLogo } from "../utils"; 4 | 5 | interface ItemTitleProps { 6 | item: { 7 | id: string; 8 | name: string; 9 | type: ItemType; 10 | collapsed?: boolean; 11 | extension?: string; 12 | }; 13 | onClickE: (e: React.MouseEvent) => void; 14 | className?: string; 15 | } 16 | 17 | const ItemTitle: React.FC = ({ item, onClickE, className }) => { 18 | const findLogo = (item: { 19 | id: string; 20 | type: ItemType; 21 | collapsed?: boolean; 22 | extension?: string; 23 | }) => { 24 | if (item.type === "folder") { 25 | return item.collapsed ? "closed-folder" : "opened-folder"; 26 | } else if (item.type === "file" && item.extension) { 27 | return getLogo(item.extension); 28 | } 29 | }; 30 | 31 | return ( 32 |
{ 34 | onClickE(e); 35 | }} 36 | parent-id={item.id} 37 | typeof-item={item.type} 38 | className={`w-full py-[0.32rem] pl-3 flex flex-row justify-start items-center collapsable ${className}`} 39 | > 40 | { 41 | 46 |   47 | 48 | } 49 | 50 | {(() => { 51 | let newName = ""; 52 | if (item.type === "file") { 53 | const fullName = `${item.name}.${item.extension}`; 54 | newName = fullName; 55 | } else if (item.type === "folder") { 56 | newName = item.name; 57 | } 58 | return newName; 59 | })()} 60 | 61 |
62 | ); 63 | }; 64 | 65 | export default ItemTitle; 66 | -------------------------------------------------------------------------------- /src/components/file-structure/widgets/ThreeDots.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { type ItemType } from "../../../state/features/structure/structureSlice"; 3 | interface ThreeDotsProp { 4 | item: { id: string; type: ItemType }; 5 | selected: string; 6 | showBlue: boolean; 7 | primaryClass?: string; 8 | secondaryClass?: string; 9 | onClickE: (e: React.MouseEvent) => void; 10 | } 11 | const ThreeDots: React.FC = ({ 12 | item, 13 | selected, 14 | showBlue, 15 | onClickE, 16 | primaryClass, 17 | secondaryClass, 18 | }) => { 19 | return ( 20 | 35 | ); 36 | }; 37 | 38 | export default ThreeDots; 39 | -------------------------------------------------------------------------------- /src/components/menus/Breadcrumbs.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useRef } from "react"; 2 | import { useTypedDispatch, useTypedSelector } from "../../state/hooks"; 3 | import { 4 | collapseMiniStructure, 5 | getBreadcrumbs, 6 | selectMiniStructure, 7 | setMiniStructureAsync, 8 | } from "../../state/features/structure/miniStructureSlice"; 9 | import MiniFolder from "../file-structure/MiniFolder"; 10 | import { createPortal } from "react-dom"; 11 | import { setSelected } from "../../state/features/structure/structureSlice"; 12 | import useOutsideAlerter from "../../hooks/useOutsideAlerter"; 13 | import { setActiveTabAsync } from "../../state/features/tabs/tabsSlice"; 14 | import { getLogo } from "../file-structure/utils"; 15 | 16 | export interface BreadcrumbsProps { 17 | containerClassName?: string; 18 | textClassName?: string; 19 | miniFolderCollapseBtnClassName?: string; 20 | miniFolderCollapseBtnStyle?: React.CSSProperties; 21 | miniFolderContainerClassName?: string; 22 | itemTitleClassName?: string; 23 | onBreadcrumbFileClick?: (id: string ) => void; 24 | } 25 | 26 | const Breadcrumbs: React.FC = ({ 27 | containerClassName, 28 | textClassName, 29 | miniFolderCollapseBtnClassName, 30 | miniFolderCollapseBtnStyle, 31 | miniFolderContainerClassName, 32 | itemTitleClassName, 33 | onBreadcrumbFileClick = () => {}, 34 | }) => { 35 | const [clickedIndex, setClickedIndex] = useState(0); 36 | const [showMiniStructure, setShowMiniStructure] = useState(false); 37 | const miniStructure = useTypedSelector(selectMiniStructure); 38 | const breadcrumbsRef = useRef(null); 39 | const miniStructurePortalRef = useRef(null); 40 | const editorObj = useTypedSelector(getBreadcrumbs); 41 | 42 | const dispatch = useTypedDispatch(); 43 | 44 | useOutsideAlerter(miniStructurePortalRef, setShowMiniStructure); 45 | 46 | return ( 47 | <> 48 | {editorObj !== null && ( 49 | <> 50 |
55 |
56 | {editorObj.path.map((path, i) => ( 57 |
path.replace(/[.[\]|\s]+/g, "-")) 60 | .join("")}-${i}`} 61 | key={`${editorObj.path 62 | .map((path) => path.replace(/[.[\]|\s]+/g, "-")) 63 | .join("")}-${i}`} 64 | > 65 |
68 | {i === editorObj.path.length - 1 && ( 69 | 74 | )} 75 | { 77 | setClickedIndex(i); 78 | setShowMiniStructure(true); 79 | dispatch( 80 | setMiniStructureAsync(editorObj.unmappedPath[i]) 81 | ); 82 | }} 83 | className={`cursor-pointer hover:underline hover:text-blue-400 ${textClassName}`} 84 | > 85 | {path} 86 | 87 | {i < editorObj.path.length - 1 && ( 88 | 89 | {"/"} 90 | 91 | )} 92 |
93 |
94 | ))} 95 |
96 |
97 | {breadcrumbsRef.current && showMiniStructure && ( 98 | <> 99 | {(() => { 100 | const id = `${editorObj.path 101 | .map((path) => path.replace(/[.[\]|\s]+/g, "-")) 102 | .join("")}-${clickedIndex}`; 103 | 104 | const element = breadcrumbsRef.current.querySelector( 105 | `#${id}` 106 | ) as HTMLElement; 107 | if (element) { 108 | return createPortal( 109 |
113 | { 117 | if (item.type === "folder") { 118 | dispatch(collapseMiniStructure(item.id)); 119 | } else { 120 | dispatch(setSelected({ id, type: "file" })); 121 | dispatch(setActiveTabAsync(item.id)); 122 | setShowMiniStructure(false); 123 | onBreadcrumbFileClick(item.id); 124 | } 125 | }} 126 | onCollapseMiniStructure={(id) => { 127 | dispatch(collapseMiniStructure(id)); 128 | }} 129 | collapseBtnClassName={miniFolderCollapseBtnClassName} 130 | collapseBtnStyle={miniFolderCollapseBtnStyle} 131 | containerClassName={miniFolderContainerClassName} 132 | titleClassName={itemTitleClassName} 133 | /> 134 |
, 135 | element 136 | ); 137 | } 138 | })()} 139 | 140 | )} 141 | 142 | )} 143 | 144 | ); 145 | }; 146 | 147 | export default Breadcrumbs; 148 | -------------------------------------------------------------------------------- /src/components/menus/Dialog.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef } from "react"; 2 | import useOutsideAlerter from "../../hooks/useOutsideAlerter"; 3 | import deleteLogo from "../../assets/delete.svg"; 4 | import cross from "../../assets/cross.svg"; 5 | 6 | interface DialogProps { 7 | title: string; 8 | content: string; 9 | actionText: string; 10 | close: (show: boolean) => void; 11 | action: () => void; 12 | className?: string; 13 | } 14 | 15 | const Dialog: React.FC = ({ 16 | title, 17 | content, 18 | actionText, 19 | close, 20 | action, 21 | className 22 | }) => { 23 | const dialogRef = useRef(null); 24 | useOutsideAlerter(dialogRef, () => { 25 | close(false); 26 | }); 27 | return ( 28 |
29 |
32 |
33 | {title} 34 | 35 | { 38 | close(false); 39 | }} 40 | alt="close" 41 | className="transition-colors p-1 h-5 w-5 cursor-pointer hover:bg-slate-500 rounded-md align-baseline" 42 | /> 43 | 44 |
45 |
{content}
46 |
47 |
 
48 |
49 | 57 | 70 |
71 |
72 |
73 |
74 | ); 75 | }; 76 | 77 | export default Dialog; 78 | -------------------------------------------------------------------------------- /src/components/menus/MenuContext.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef } from "react"; 2 | import useOutsideAlerter from "../../hooks/useOutsideAlerter"; 3 | 4 | interface MenuContextProps { 5 | top: number; 6 | left: number; 7 | showContext: boolean; 8 | setShowContext: React.Dispatch>; 9 | actions: Array< 10 | | { 11 | title: string; 12 | handler: () => void; 13 | disabled: boolean; 14 | type?: undefined; 15 | } 16 | | { 17 | type: string; 18 | handler: () => void; 19 | title?: undefined; 20 | disabled?: undefined; 21 | } 22 | >; 23 | className?: string; 24 | clickableAreaClassName?: string; 25 | hrColor?: string; 26 | } 27 | 28 | const MenuContext: React.FC = ({ 29 | top, 30 | left, 31 | setShowContext, 32 | actions, 33 | className, 34 | clickableAreaClassName, 35 | hrColor, 36 | }) => { 37 | const contextRef = useRef(null); 38 | useOutsideAlerter(contextRef, setShowContext); 39 | 40 | return ( 41 |
46 |
    47 | {actions.map((action, index) => { 48 | if (action.type === "hr") { 49 | return ( 50 |
    55 | ); 56 | } else { 57 | return ( 58 |
  • { 61 | if (!action.disabled) { 62 | action.handler(); 63 | setShowContext(false); 64 | } 65 | }} 66 | className={`rounded-md px-7 py-1 ${ 67 | !action.disabled 68 | ? `hover:bg-hover-blue cursor-pointer text-white ${clickableAreaClassName}` 69 | : "text-zinc-500" 70 | } `} 71 | > 72 | {action.title} 73 |
  • 74 | ); 75 | } 76 | })} 77 |
78 |
79 | ); 80 | }; 81 | 82 | export default MenuContext; 83 | -------------------------------------------------------------------------------- /src/components/menus/Tab.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import cross from "../../assets/cross.svg"; 3 | import { getLogo } from "../file-structure/utils"; 4 | // import { Tooltip } from "react-tooltip"; 5 | 6 | interface TabProps { 7 | id: string; 8 | name: string; 9 | type: string; 10 | selected: boolean; 11 | onSelect: (id: string) => void; 12 | onClose: (id: string) => void; 13 | className?: string; 14 | selectedTabClassName?: string; 15 | } 16 | 17 | const Tab: React.FC = ({ 18 | id, 19 | name, 20 | type, 21 | selected, 22 | onSelect, 23 | onClose, 24 | className, 25 | selectedTabClassName 26 | }) => { 27 | const fileType = name.substring(name.lastIndexOf(".") + 1); 28 | const [logo, setLogo] = React.useState(getLogo(fileType)); 29 | 30 | useEffect(() => { 31 | setLogo(getLogo(type)); 32 | }, [type]); 33 | 34 | return ( 35 |
{ 37 | if (!selected) onSelect(id); 38 | }} 39 | className={`hover-show hover:bg-slate-700 hover:text-white border-t-dark-bg border-t transition-colors py-2 pl-3 pr-2 flex flex-row flex-shrink-0 cursor-pointer select-none items-center rounded-sm mx-[1px] ${className} ${ 40 | selected 41 | ? `bg-dark-hover text-white border-t-slate-200 ${selectedTabClassName}` 42 | : "" 43 | }`}> 44 |   45 | {name} 46 | 47 | {/* */} 48 | 63 | 64 |
65 | ); 66 | }; 67 | 68 | export default Tab; 69 | -------------------------------------------------------------------------------- /src/components/menus/Tabs.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Tab from "./Tab"; 3 | 4 | import { useTypedDispatch, useTypedSelector } from "../../state/hooks"; 5 | 6 | import { 7 | activeTabs, 8 | closeTab, 9 | selectTab, 10 | selectedTab, 11 | } from "../../state/features/tabs/tabsSlice"; 12 | 13 | export interface TabsProps { 14 | containerClassName?: string; 15 | tabClassName?: string; 16 | selectedTabClassName?: string; 17 | onTabClick?: (id: string) => void; 18 | onTabClose?: (id: string) => void; 19 | } 20 | 21 | const Tabs: React.FC = ({ 22 | containerClassName, 23 | tabClassName, 24 | selectedTabClassName, 25 | onTabClick = () => {}, 26 | onTabClose = () => {}, 27 | }) => { 28 | const dispatch = useTypedDispatch(); 29 | const tabs = useTypedSelector(activeTabs); 30 | const selected = useTypedSelector(selectedTab); 31 | 32 | const onSelect = (id: string) => { 33 | // alert(`Tab ${i} selected`); 34 | if (selected !== id) { 35 | dispatch(selectTab(id)); 36 | onTabClick(id); 37 | } 38 | }; 39 | 40 | const onClose = (id: string) => { 41 | dispatch(closeTab(id)); 42 | onTabClose(id); 43 | }; 44 | 45 | return ( 46 |
47 |
48 |
51 | {tabs.map((item) => ( 52 | 63 | ))} 64 |
65 |
66 |
67 | ); 68 | }; 69 | 70 | export default Tabs; 71 | -------------------------------------------------------------------------------- /src/hooks/useOutsideAlerter.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | 3 | export default function useOutsideAlerter( 4 | ref: React.RefObject, 5 | callback: React.Dispatch> | (() => void), 6 | ) { 7 | useEffect(() => { 8 | function handleClickOutside(event: MouseEvent) { 9 | if (ref.current && !ref.current.contains(event.target as Node)) { 10 | callback(false); 11 | } 12 | } 13 | document.addEventListener("mousedown", handleClickOutside); 14 | return () => { 15 | document.removeEventListener("mousedown", handleClickOutside); 16 | }; 17 | }, [ref, callback]); 18 | } 19 | -------------------------------------------------------------------------------- /src/hooks/usePrependPortal.ts: -------------------------------------------------------------------------------- 1 | import { type ReactNode, type ReactPortal, useEffect } from "react"; 2 | import { createPortal } from "react-dom"; 3 | 4 | export const usePrependPortal = ( 5 | component: ReactNode, 6 | container: Element, 7 | ): ReactPortal => { 8 | const portalContainer = document.createElement("div"); 9 | 10 | useEffect(() => { 11 | if (!container) return; 12 | container.prepend(portalContainer); 13 | return () => { 14 | container.removeChild(portalContainer); 15 | }; 16 | }, [container, portalContainer]); 17 | 18 | return createPortal(component, portalContainer); 19 | }; 20 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap"); 2 | 3 | @tailwind base; 4 | @tailwind components; 5 | @tailwind utilities; 6 | 7 | .custom-scrollbar::-webkit-scrollbar { 8 | width: 5px; 9 | height: 5px; 10 | 11 | /* transition-property: all; 12 | transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); 13 | transition-duration: 150ms; */ 14 | } 15 | 16 | .custom-scrollbar::-webkit-scrollbar-thumb { 17 | background: #4a5575; 18 | border-radius: 5px; 19 | visibility: hidden; 20 | } 21 | 22 | .show-on-hover { 23 | opacity: 0; 24 | } 25 | 26 | .hover-show:hover .show-on-hover { 27 | opacity: 1; 28 | } 29 | 30 | .custom-scrollbar::-webkit-scrollbar-track { 31 | background-color: #0e1525; 32 | } 33 | 34 | .file-tabs:hover .custom-scrollbar::-webkit-scrollbar-thumb { 35 | visibility: visible; 36 | } 37 | 38 | .bg-vscode-overlay { 39 | position: relative; 40 | } 41 | 42 | .bg-vscode-overlay>* { 43 | z-index: 1; 44 | } 45 | 46 | .bg-vscode-overlay::before { 47 | content: ""; 48 | position: absolute; 49 | top: 0; 50 | left: 0; 51 | background-color: #4078ce; 52 | opacity: 0.6; 53 | width: 100%; 54 | height: 100%; 55 | border-top-right-radius: 0.125rem 56 | /* 2px */ 57 | ; 58 | border-bottom-right-radius: 0.125rem 59 | /* 2px */ 60 | ; 61 | } 62 | 63 | .dialog-cover::before { 64 | position: absolute; 65 | top: 0; 66 | left: 0; 67 | background-color: #000; 68 | opacity: 0.2; 69 | width: 100%; 70 | height: 100%; 71 | } 72 | 73 | .display-none-c { 74 | display: none !important; 75 | } 76 | 77 | .span-logo { 78 | background-size: contain; 79 | background-repeat: no-repeat; 80 | background-position-y: center; 81 | } 82 | 83 | .span-logo-width { 84 | width: 1rem; 85 | } 86 | 87 | .custom-scrollbar::-webkit-scrollbar-button:vertical:start:increment, 88 | .custom-scrollbar::-webkit-scrollbar-button:vertical:end:decrement, 89 | .custom-scrollbar::-webkit-scrollbar-button:horizontal:start:increment, 90 | .custom-scrollbar::-webkit-scrollbar-button:horizontal:end:decrement { 91 | display: none; 92 | } 93 | 94 | .new-file-logo { 95 | background-image: url("./assets/new-file-colored.svg"); 96 | } 97 | 98 | .new-folder-logo { 99 | background-image: url("./assets/new-folder.svg"); 100 | } 101 | 102 | .opened-folder { 103 | background-image: url("./assets/opened-folder.svg"); 104 | } 105 | 106 | .closed-folder { 107 | background-image: url("./assets/folder.svg"); 108 | } 109 | 110 | .js-logo { 111 | background-image: url("./assets/js.svg"); 112 | } 113 | 114 | .css-logo { 115 | background-image: url("./assets/css.svg"); 116 | } 117 | 118 | .jsx-logo { 119 | background-image: url("./assets/jsx.svg"); 120 | } 121 | 122 | .error-logo { 123 | background-image: url("./assets/error.png"); 124 | } 125 | 126 | .md-logo { 127 | background-image: url("./assets/readme.svg"); 128 | } 129 | 130 | .rename-logo { 131 | background-image: url("./assets/rename.svg"); 132 | } 133 | 134 | .ts-logo, 135 | .tsx-logo { 136 | background-image: url("./assets/typescript.svg"); 137 | } 138 | 139 | /* iframe#webpack-dev-server-client-overlay { 140 | display: none !important 141 | } */ 142 | .no-height { 143 | height: 0 !important; 144 | } 145 | 146 | 147 | .file-sys-container { 148 | display: flex; 149 | flex-direction: column; 150 | 151 | /* width: 100%; */ 152 | overflow: auto; 153 | height: 100%; 154 | /* text-wrap: nowrap; */ 155 | 156 | /* background-color: orange; */ 157 | } 158 | 159 | .folder-container { 160 | display: flex; 161 | flex-direction: column; 162 | 163 | /* background-color: green; */ 164 | } 165 | 166 | .custom-scrollbar-2::-webkit-scrollbar { 167 | width: 5px; 168 | height: 5px; 169 | } 170 | 171 | .custom-scrollbar-2::-webkit-scrollbar-thumb { 172 | background: #4a5575; 173 | border-radius: 5px; 174 | visibility: visible; 175 | transition-property: all; 176 | } 177 | 178 | .folder-container-reverse { 179 | flex-direction: column-reverse; 180 | } 181 | 182 | .folder, 183 | .file { 184 | cursor: pointer; 185 | border: 1px solid #0e1525; 186 | } 187 | 188 | .transformer { 189 | display: flex; 190 | align-items: center; 191 | } 192 | 193 | .folder:hover, 194 | .file:hover { 195 | background: #2b3245; 196 | } 197 | 198 | .hide-input { 199 | visibility: hidden; 200 | position: absolute; 201 | } 202 | 203 | .span-text { 204 | display: flex; 205 | flex-direction: row; 206 | align-items: center; 207 | } 208 | 209 | .measurable { 210 | width: 235px; 211 | } 212 | 213 | .three-dots { 214 | background-image: url("./assets/three-dots.svg"); 215 | background-repeat: no-repeat; 216 | background-position: center; 217 | background-size: cover; 218 | opacity: 0; 219 | } 220 | 221 | .clickable:hover .three-dots { 222 | opacity: 1; 223 | } 224 | 225 | .not-seen { 226 | position: absolute; 227 | visibility: hidden; 228 | } 229 | 230 | @media screen and (min-width: 768px) { 231 | .measurable { 232 | width: 200px; 233 | } 234 | } -------------------------------------------------------------------------------- /src/index.d.ts: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { StructureProps } from "./components/file-structure/Structure"; 3 | import { TabsProps } from "./components/menus/Tabs"; 4 | import { SearchContainerProps } from "./components/file-structure/search/SearchContainer"; 5 | import { BreadcrumbsProps } from "./components/menus/Breadcrumbs"; 6 | import { SearchInputProps } from "./components/file-structure/search/SearchInput"; 7 | import { getFileTree, getSelectedFile } from "./state/features/structure/utils/getFileTree"; 8 | declare const FileExplorer: React.FC; 9 | declare const TabsList: React.FC; 10 | declare const SearchInput: React.FC; 11 | declare const Breadcrumbs: React.FC; 12 | declare const SearchResults: React.FC; 13 | declare const updateFile: (id: string, content: string) => void; 14 | export { FileExplorer, TabsList, SearchResults, Breadcrumbs, SearchInput, getFileTree, updateFile, getSelectedFile}; 15 | -------------------------------------------------------------------------------- /src/lib/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module "@litecode-ide/virtual-file-system" { 2 | type ItemType = "file" | "folder"; 3 | 4 | interface BreadcrumbsProps { 5 | containerClassName?: string; 6 | textClassName?: string; 7 | miniFolderCollapseBtnClassName?: string; 8 | miniFolderCollapseBtnStyle?: React.CSSProperties; 9 | miniFolderContainerClassName?: string; 10 | itemTitleClassName?: string; 11 | onBreadcrumbFileClick?: (id: string) => void; 12 | } 13 | 14 | interface StructureProps { 15 | deleteConfirmationClassName?: string; 16 | fileInputClassName?: string; 17 | fileInputStyle?: React.CSSProperties; 18 | contextMenuClassName?: string; 19 | contextMenuHrColor?: string; 20 | contextMenuClickableAreaClassName?: string; 21 | fileActionsBtnClassName?: string; 22 | projectName?: string; 23 | fileActionsDisableCollapse?: true; 24 | fileActionsDisableTooltip?: true; 25 | fileActionsDisableDownload?: true; 26 | folderCollapseBtnClassname?: string; 27 | folderCollapseBtnStyle?: React.CSSProperties; 28 | folderThreeDotPrimaryClass?: string; 29 | folderThreeDotSecondaryClass?: string; 30 | folderClickableAreaClassName?: string; 31 | folderSelectedClickableAreaClassName?: string; 32 | folderContextSelectedClickableAreaClassName?: string; 33 | itemTitleClassName?: string; 34 | structureContainerClassName?: string; 35 | containerHeight?: string; 36 | onItemSelected?: (item: { id: string; type: ItemType }) => void; 37 | onNewItemClick?: (parentFolderId: string, type: ItemType) => void; 38 | onAreaCollapsed?: (collapsed: boolean) => void; 39 | onItemContextSelected?: (item: { id: string; type: ItemType }) => void; 40 | onNodeDeleted?: (id: string) => void; 41 | onNewItemCreated?: (id: string) => void; 42 | validExtensions: string[]; 43 | } 44 | 45 | interface TabsProps { 46 | containerClassName?: string; 47 | tabClassName?: string; 48 | selectedTabClassName?: string; 49 | onTabClick?: (id: string) => void; 50 | onTabClose?: (id: string) => void; 51 | } 52 | 53 | interface MatchingFile { 54 | id: string; 55 | name: string; 56 | extension: string; 57 | matches: MatchingLine[]; 58 | } 59 | 60 | interface MatchingLine { 61 | line: number; 62 | content: string; 63 | } 64 | 65 | interface SearchResultsType { 66 | files: MatchingFile[]; 67 | numOfResults: number; 68 | numOfLines: number; 69 | } 70 | 71 | interface SearchInputProps { 72 | className?: string; 73 | style?: React.CSSProperties; 74 | onSearchFiles?: (searchTerm: string, searchResults: SearchResultsType) => void; 75 | } 76 | 77 | interface SearchContainerProps { 78 | highlightedTextClassName?: string; 79 | headerClassName?: string; 80 | headerStyle?: React.CSSProperties; 81 | titleClassName?: string; 82 | searchResultClicked: (fileId: string, line: number) => void; 83 | } 84 | 85 | const FileExplorer: React.FC; 86 | const TabsList: React.FC; 87 | const SearchInput: React.FC; 88 | const Breadcrumbs: React.FC; 89 | const SearchResults: React.FC; 90 | const updateFile: (id: string, content: string) => void; 91 | const getFileTree: () => Record< 92 | string, 93 | { 94 | id: string; 95 | content: string; 96 | } 97 | >; 98 | const getSelectedFile: () => string; 99 | 100 | export { 101 | FileExplorer, 102 | TabsList, 103 | SearchResults, 104 | Breadcrumbs, 105 | SearchInput, 106 | getFileTree, 107 | updateFile, 108 | getSelectedFile, 109 | }; 110 | } 111 | -------------------------------------------------------------------------------- /src/lib/index.tsx: -------------------------------------------------------------------------------- 1 | // import Structure from "./components/file-structure/Structure"; 2 | import React from "react"; 3 | import "../index.css"; 4 | import Structure, { 5 | StructureProps, 6 | } from "../components/file-structure/Structure"; 7 | import VFSProvider from "../state/provider"; 8 | import Tabs, { TabsProps } from "../components/menus/Tabs"; 9 | import SearchContainer, { 10 | SearchContainerProps, 11 | } from "../components/file-structure/search/SearchContainer"; 12 | import BC, { BreadcrumbsProps } from "../components/menus/Breadcrumbs"; 13 | import SI, { 14 | SearchInputProps, 15 | } from "../components/file-structure/search/SearchInput"; 16 | import { 17 | getFileTree, 18 | updateFileContents, 19 | getSelectedFile, 20 | } from "../state/features/structure/utils/getFileTree"; 21 | 22 | const FileExplorer: React.FC = ({ 23 | deleteConfirmationClassName, 24 | fileInputClassName, 25 | fileInputStyle, 26 | contextMenuClassName, 27 | contextMenuHrColor, 28 | contextMenuClickableAreaClassName, 29 | fileActionsBtnClassName, 30 | projectName, 31 | fileActionsDisableCollapse, 32 | fileActionsDisableTooltip, 33 | fileActionsDisableDownload, 34 | folderCollapseBtnClassname, 35 | folderCollapseBtnStyle, 36 | folderThreeDotPrimaryClass, 37 | folderThreeDotSecondaryClass, 38 | folderClickableAreaClassName, 39 | folderSelectedClickableAreaClassName, 40 | folderContextSelectedClickableAreaClassName, 41 | itemTitleClassName, 42 | structureContainerClassName, 43 | containerHeight, 44 | onItemSelected = () => {}, 45 | onNewItemClick = () => {}, 46 | onAreaCollapsed = () => {}, 47 | onItemContextSelected = () => {}, 48 | onNodeDeleted = () => {}, 49 | onNewItemCreated = () => {}, 50 | validExtensions, 51 | }) => { 52 | return ( 53 | 54 | 88 | 89 | ); 90 | }; 91 | 92 | const TabsList: React.FC = ({ 93 | containerClassName, 94 | tabClassName, 95 | selectedTabClassName, 96 | onTabClick = () => {}, 97 | onTabClose = () => {}, 98 | }) => { 99 | return ( 100 | 101 | 108 | 109 | ); 110 | }; 111 | 112 | const SearchInput: React.FC = ({ 113 | className, 114 | style, 115 | onSearchFiles = () => {}, 116 | }) => { 117 | return ( 118 | 119 | 120 | 121 | ); 122 | }; 123 | 124 | const Breadcrumbs: React.FC = ({ 125 | containerClassName, 126 | textClassName, 127 | miniFolderCollapseBtnClassName, 128 | miniFolderCollapseBtnStyle, 129 | miniFolderContainerClassName, 130 | itemTitleClassName, 131 | onBreadcrumbFileClick = () => {}, 132 | }) => { 133 | return ( 134 | 135 | 144 | 145 | ); 146 | }; 147 | 148 | const SearchResults: React.FC = ({ 149 | highlightedTextClassName, 150 | headerClassName, 151 | headerStyle, 152 | titleClassName, 153 | searchResultClicked 154 | }) => { 155 | return ( 156 | 157 | 164 | 165 | ); 166 | }; 167 | 168 | const updateFile = (id: string, content: string) => { 169 | updateFileContents(id, content); 170 | }; 171 | 172 | export { 173 | FileExplorer, 174 | TabsList, 175 | SearchResults, 176 | Breadcrumbs, 177 | SearchInput, 178 | getFileTree, 179 | updateFile, 180 | getSelectedFile, 181 | }; 182 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom/client' 3 | import App from './App.tsx' 4 | import './index.css' 5 | 6 | ReactDOM.createRoot(document.getElementById('root')!).render( 7 | 8 | 9 | , 10 | ) 11 | -------------------------------------------------------------------------------- /src/state/context.ts: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Context } from "react"; 3 | import { UnknownAction } from "@reduxjs/toolkit"; 4 | import type { ReactReduxContextValue } from "react-redux"; 5 | 6 | type contextType = Context | null>; 10 | 11 | export const VFSContext = React.createContext(null) as contextType; 12 | -------------------------------------------------------------------------------- /src/state/features/structure/miniStructureSlice.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type PayloadAction, 3 | createAsyncThunk, 4 | createSlice, 5 | createSelector, 6 | } from "@reduxjs/toolkit"; 7 | import type { RootState } from "../../store"; 8 | import { dfsCbOnEach, dfsNodeAction } from "./utils/traversal"; 9 | import { Normalized, type Directory } from "./structureSlice"; 10 | import cloneDeep from "lodash.clonedeep"; 11 | import { getPaths } from "./utils/pathUtil"; 12 | 13 | export interface MiniFile { 14 | id: string; 15 | type: "file"; 16 | name: string; 17 | wholeName: string; 18 | extension: string; 19 | subFoldersAndFiles: null; 20 | } 21 | 22 | export interface MiniStructure { 23 | id: string; 24 | type: "folder"; 25 | name: string; 26 | subFoldersAndFiles: Array; 27 | collapsed: boolean; 28 | } 29 | 30 | interface MiniStructureState { 31 | miniStructure: MiniStructure; 32 | } 33 | 34 | const initialState: MiniStructureState = { 35 | miniStructure: { 36 | id: "head", 37 | type: "folder", 38 | name: "head", 39 | collapsed: false, 40 | subFoldersAndFiles: [], 41 | }, 42 | }; 43 | 44 | export const setMiniStructureAsync = createAsyncThunk( 45 | "setMiniStructureAsync", 46 | async (selectedId: string, { getState }) => { 47 | const state = getState() as RootState; 48 | let mapped = { 49 | id: "head", 50 | type: "folder", 51 | name: "head", 52 | collapsed: false, 53 | subFoldersAndFiles: [], 54 | } as MiniStructure; 55 | dfsNodeAction( 56 | state.structure.initialFolder.subFoldersAndFiles as Directory[], 57 | selectedId, 58 | (_, parents) => { 59 | const structureCopy = cloneDeep( 60 | parents[parents.length - 1] 61 | ) as MiniStructure; 62 | dfsCbOnEach( 63 | structureCopy.subFoldersAndFiles as Directory[], 64 | (item) => { 65 | const copiedItem = item as MiniStructure | MiniFile; 66 | if (copiedItem.type === "folder") { 67 | copiedItem.name = 68 | state.structure.normalized.folders.byId[item.id].name; 69 | copiedItem.collapsed = true; 70 | } else if (copiedItem.type === "file") { 71 | const currentFile = 72 | state.structure.normalized.files.byId[item.id]; 73 | copiedItem.wholeName = `${currentFile.name}.${currentFile.extension}`; 74 | copiedItem.name = currentFile.name; 75 | copiedItem.extension = currentFile.extension; 76 | copiedItem.subFoldersAndFiles = null; 77 | } 78 | }, 79 | [], 80 | [structureCopy.id] 81 | ); 82 | mapped = structureCopy; 83 | }, 84 | [state.structure.initialFolder] 85 | ); 86 | return mapped; 87 | } 88 | ); 89 | 90 | export const miniStructureSlice = createSlice({ 91 | name: "miniStructure", 92 | initialState, 93 | reducers: { 94 | collapseMiniStructure: (state, action: PayloadAction) => { 95 | const findAndCollapse = ( 96 | structure: Array, 97 | id: string 98 | ) => { 99 | for (const item of structure) { 100 | if (item.id === id && item.type === "folder") { 101 | item.collapsed = !item.collapsed; 102 | return; 103 | } else if (item.type === "folder") { 104 | findAndCollapse(item.subFoldersAndFiles, id); 105 | } 106 | } 107 | }; 108 | findAndCollapse(state.miniStructure.subFoldersAndFiles, action.payload); 109 | }, 110 | }, 111 | extraReducers: (builder) => { 112 | builder.addCase(setMiniStructureAsync.fulfilled, (state, action) => { 113 | state.miniStructure = action.payload; 114 | }); 115 | }, 116 | }); 117 | 118 | export const { collapseMiniStructure } = miniStructureSlice.actions; 119 | 120 | export const selectMiniStructure = (state: RootState) => 121 | state.miniStructure.miniStructure; 122 | 123 | export const getBreadcrumbs = createSelector( 124 | (state: RootState) => state.structure.normalized, 125 | (state: RootState) => state.tabs.selected, 126 | (normalized: Normalized, selectedTab: string) => { 127 | if (selectedTab && selectedTab !== "") { 128 | const file = normalized.files.byId[selectedTab]; 129 | const [unmappedPath, path] = getPaths(file, normalized); 130 | return { 131 | id: file.id, 132 | path, 133 | unmappedPath, 134 | }; 135 | } else { 136 | return null; 137 | } 138 | } 139 | ); 140 | 141 | export default miniStructureSlice.reducer; 142 | -------------------------------------------------------------------------------- /src/state/features/structure/utils/downloadZip.ts: -------------------------------------------------------------------------------- 1 | import { store } from "../../../store"; 2 | import { type Directory } from "../structureSlice"; 3 | import { dfsCbOnEach } from "./traversal"; 4 | import JSZip from "jszip"; 5 | import { saveAs } from "file-saver"; 6 | 7 | const downloadZip = () => { 8 | const state = store.getState(); 9 | const zip = new JSZip(); 10 | const { 11 | structure: { normalized, initialFolder }, 12 | } = state; 13 | if ( 14 | normalized.files.allIds.length === 0 && 15 | normalized.folders.allIds.length === 1 16 | ) { 17 | alert("There is nothing to download, you haven't created any files yet."); 18 | return; 19 | } 20 | const projectName = state.structure.projectName; 21 | const folderMap = { 22 | [initialFolder.id]: zip, 23 | }; 24 | dfsCbOnEach( 25 | initialFolder.subFoldersAndFiles as Directory[], 26 | (node, parentIds) => { 27 | const item = normalized[`${node.type}s`].byId[node.id]; 28 | const parentId = parentIds[parentIds.length - 1]; 29 | const currentFolder = folderMap[parentId]; 30 | if (item.type === "file") { 31 | currentFolder.file(`${item.name}.${item.extension}`, item.content); 32 | } else { 33 | const folder = currentFolder.folder(item.name) as JSZip; 34 | folderMap[item.id] = folder; 35 | } 36 | }, 37 | [], 38 | [initialFolder.id] 39 | ); 40 | zip 41 | .generateAsync({ 42 | type: "blob", 43 | }) 44 | .then((content) => { 45 | saveAs(content, `${projectName}.zip`); 46 | }); 47 | }; 48 | 49 | export default downloadZip; 50 | -------------------------------------------------------------------------------- /src/state/features/structure/utils/getFileTree.ts: -------------------------------------------------------------------------------- 1 | import { getPaths } from "./pathUtil"; 2 | import { type Normalized } from "../structureSlice"; 3 | import { store } from "../../../store"; 4 | 5 | const getFileTree = ( 6 | allFileIds: string[] = store.getState().structure.normalized.files.allIds, 7 | normalized: Normalized = store.getState().structure.normalized 8 | ) => { 9 | const allFilesAndPaths = allFileIds.map((id) => { 10 | const file = normalized.files.byId[id]; 11 | const [_, actualPath] = getPaths(file, normalized); 12 | return { 13 | id, 14 | content: normalized.files.byId[id].content, 15 | path: actualPath, 16 | }; 17 | }); 18 | const tree = allFilesAndPaths.reduce< 19 | Record 20 | >( 21 | ( 22 | acc, 23 | file: { 24 | id: string; 25 | content: string; 26 | path: string[] | undefined; 27 | } 28 | ) => { 29 | if (file.path) { 30 | const key = `/${file.path?.join("/")}`; 31 | acc[key] = { id: file.id, content: file.content }; 32 | } 33 | return acc; 34 | }, 35 | {} 36 | ); 37 | return tree; 38 | }; 39 | 40 | const updateFileContents = (id: string, content: string) => { 41 | store.dispatch({ 42 | type: "structure/updateFileContents", 43 | payload: { id, value: content }, 44 | }); 45 | }; 46 | 47 | const getSelectedFile = () => { 48 | return store.getState().tabs.selected; 49 | }; 50 | 51 | export { getFileTree, updateFileContents, getSelectedFile }; 52 | -------------------------------------------------------------------------------- /src/state/features/structure/utils/pathUtil.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type FileStructure, 3 | type Normalized, 4 | } from "../../structure/structureSlice"; 5 | 6 | const getPaths = (file: FileStructure, normalized: Normalized) => { 7 | const unmappedPath = file.path.filter((id) => id !== "/" && id !== "head"); 8 | const actualPath = unmappedPath.map((id, i) => { 9 | if (i < unmappedPath.length - 1) { 10 | return normalized.folders.byId[id].name; 11 | } else { 12 | const file = normalized.files.byId[id]; 13 | return `${file.name}.${file.extension}`; 14 | } 15 | }); 16 | return [unmappedPath, actualPath]; 17 | }; 18 | 19 | export { getPaths }; 20 | -------------------------------------------------------------------------------- /src/state/features/structure/utils/sorting.ts: -------------------------------------------------------------------------------- 1 | import { type Directory } from "../structureSlice"; 2 | 3 | const findSortable = ( 4 | structure: Directory, 5 | callback: (structure: Directory) => void, 6 | id: string, 7 | ) => { 8 | if (structure.type === "folder" && id === structure.id) { 9 | callback(structure); 10 | return; 11 | } else if (structure.type === "folder") { 12 | callback(structure); 13 | } 14 | const children = structure.subFoldersAndFiles as Directory[]; 15 | for (const item of children) { 16 | if (item.type === "folder") { 17 | findSortable(item, callback, id); 18 | } 19 | } 20 | }; 21 | 22 | export { findSortable }; 23 | -------------------------------------------------------------------------------- /src/state/features/structure/utils/traversal.ts: -------------------------------------------------------------------------------- 1 | import { type Directory, type FileInFolder } from '../structureSlice'; 2 | 3 | const bfsNodeAction = ( 4 | currentItem: Directory | FileInFolder, 5 | id: string, 6 | callback: (currentItem: Directory | FileInFolder) => void, 7 | ) => { 8 | const queue: Array = [currentItem]; 9 | while (queue.length > 0) { 10 | currentItem = queue.shift() as Directory | FileInFolder; 11 | if (currentItem.id === id) { 12 | callback(currentItem); 13 | return; 14 | } else if (currentItem.type === 'folder') { 15 | queue.push(...currentItem.subFoldersAndFiles); 16 | } 17 | } 18 | }; 19 | 20 | const dfsNodeAction = ( 21 | structure: Directory[], 22 | id: string, 23 | callback: (item: Directory | FileInFolder, parents: Directory[]) => void, 24 | parents: Directory[], 25 | ) => { 26 | for (const item of structure) { 27 | if (item.id === id) { 28 | callback(item, parents); 29 | return; 30 | } else if (item.type === 'folder') { 31 | parents.push(item); 32 | dfsNodeAction( 33 | item.subFoldersAndFiles as Directory[], 34 | id, 35 | callback, 36 | parents, 37 | ); 38 | } 39 | } 40 | parents.pop(); 41 | }; 42 | 43 | const dfsCbOnEach = ( 44 | node: Directory[], 45 | callback: (item: Directory | FileInFolder, parentIds: string[]) => void, 46 | childrenIds: string[] = [], 47 | parentIds: string[], 48 | ) => { 49 | for (const item of node) { 50 | callback(item, parentIds); 51 | if (item.type === 'folder') { 52 | const childIds = item.subFoldersAndFiles.map(({ id }) => id); 53 | childrenIds.push(...childIds); 54 | parentIds.push(item.id); 55 | dfsCbOnEach( 56 | item.subFoldersAndFiles as Directory[], 57 | callback, 58 | childrenIds, 59 | parentIds, 60 | ); 61 | parentIds.pop(); 62 | } 63 | } 64 | return { childrenIds, parentIds }; 65 | }; 66 | 67 | const findParent = ( 68 | selectedItem: string, 69 | allFileIds: string[], 70 | initialSet: Directory 71 | ) => { 72 | if (allFileIds.includes(selectedItem)) { 73 | let parentId = ''; 74 | dfsNodeAction( 75 | initialSet.subFoldersAndFiles as Directory[], 76 | selectedItem, 77 | (_, parents) => { 78 | const parent = parents[parents.length - 1]; 79 | parentId = parent.id; 80 | }, 81 | [initialSet], 82 | ); 83 | return parentId; 84 | } else if (selectedItem.includes('folder')) { 85 | return selectedItem; 86 | } else { 87 | return 'head'; 88 | } 89 | }; 90 | 91 | export { dfsNodeAction, bfsNodeAction, dfsCbOnEach, findParent }; 92 | -------------------------------------------------------------------------------- /src/state/features/tabs/tabsSlice.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type PayloadAction, 3 | createAsyncThunk, 4 | createSelector, 5 | createSlice, 6 | } from '@reduxjs/toolkit'; 7 | import { type RootState } from '../../store'; 8 | import { 9 | type Normalized, 10 | } from '../structure/structureSlice'; 11 | 12 | export interface Tab { 13 | id: string; 14 | extension: string; 15 | } 16 | 17 | interface TabSlice { 18 | open: Tab[]; 19 | selected: string; 20 | selectionStack: string[]; 21 | } 22 | 23 | const initialState: TabSlice = { 24 | open: [], 25 | selected: '', 26 | selectionStack: [], 27 | }; 28 | 29 | export const removeTabAsync = createAsyncThunk( 30 | 'removeTabAsync', 31 | async (_, { getState }) => { 32 | const state = getState() as RootState; 33 | const normalized = state.structure.normalized; 34 | return normalized; 35 | }, 36 | ); 37 | 38 | export const setActiveTabAsync = createAsyncThunk( 39 | 'setActiveTabAsync', 40 | async (id: string, { getState }) => { 41 | const state = getState() as RootState; 42 | const normalized = state.structure.normalized; 43 | return { id, normalized }; 44 | }, 45 | ); 46 | 47 | export const tabsSlice = createSlice({ 48 | name: 'tabs', 49 | initialState, 50 | reducers: { 51 | selectTab: (state, action: PayloadAction) => { 52 | if ( 53 | state.selected !== '' && 54 | state.selectionStack[state.selectionStack.length - 1] !== state.selected 55 | ) { 56 | state.selectionStack = [...state.selectionStack, state.selected]; 57 | } 58 | state.selected = action.payload; 59 | // state.open = [...state.open, { id: action.payload, extension: "" }]; 60 | // state.open = state.open.map((tab) => { 61 | // if (tab.id !== action.payload) { 62 | // return { 63 | // ...tab, 64 | // isSelected: false, 65 | // }; 66 | // } 67 | // return { 68 | // ...tab, 69 | // isSelected: true, 70 | // }; 71 | // }); 72 | }, 73 | closeTab: (state, action: PayloadAction) => { 74 | state.open = state.open.filter(({ id }) => id !== action.payload); 75 | state.selectionStack = state.selectionStack.filter( 76 | (id) => id !== action.payload, 77 | ); 78 | 79 | if (state.selected === action.payload) { 80 | const newSelectedStack = state.selectionStack.filter( 81 | (id) => id !== action.payload, 82 | ); 83 | const lastSelected = newSelectedStack.pop(); 84 | state.selected = lastSelected || ''; 85 | state.selectionStack = newSelectedStack; 86 | } 87 | }, 88 | closeAllTabs: (state) => { 89 | state.open = []; 90 | state.selected = ''; 91 | state.selectionStack = []; 92 | }, 93 | }, 94 | extraReducers: (builder) => { 95 | builder 96 | .addCase(removeTabAsync.fulfilled, (state, action) => { 97 | const normalized = action.payload; 98 | state.open = state.open.filter( 99 | (tab) => 100 | normalized.files.allIds.find((id) => id === tab.id) !== undefined, 101 | ); 102 | state.selectionStack = state.selectionStack.filter( 103 | (selected) => 104 | normalized.files.allIds.find((id) => id === selected) !== undefined, 105 | ); 106 | if (!state.open.find(({ id }) => id === state.selected)) { 107 | state.selected = 108 | state.selectionStack[state.selectionStack.length - 1]; 109 | state.selectionStack = state.selectionStack.slice( 110 | 0, 111 | state.selectionStack.length - 1, 112 | ); 113 | } 114 | }) 115 | .addCase(setActiveTabAsync.fulfilled, (state, action) => { 116 | const normalized = action.payload.normalized; 117 | const tabId = action.payload.id; 118 | 119 | const item = normalized.files.byId[tabId]; 120 | 121 | if (state.open.filter(({ id }) => id === item.id).length === 0) { 122 | state.open = [ 123 | ...state.open, 124 | { id: item.id, extension: item.extension }, 125 | ]; 126 | } 127 | if ( 128 | (state.selected !== '' && 129 | state.selectionStack[state.selectionStack.length - 1] !== 130 | state.selected) || 131 | state.selectionStack.length === 0 132 | ) { 133 | state.selectionStack = [...state.selectionStack, state.selected]; 134 | } 135 | state.selected = item.id; 136 | }); 137 | }, 138 | }); 139 | 140 | export const { closeTab, selectTab, closeAllTabs } = tabsSlice.actions; 141 | 142 | export default tabsSlice.reducer; 143 | 144 | export const selectedTab = (state: RootState) => state.tabs.selected; 145 | 146 | export const activeTabs = createSelector( 147 | (state: RootState) => state.structure.normalized, 148 | (state: RootState) => state.tabs.open, 149 | (normalized: Normalized, openTabs: Tab[]) => { 150 | return openTabs.map((tab) => { 151 | const item = normalized.files.byId[tab.id]; 152 | return { 153 | ...tab, 154 | extension: item.extension, 155 | wholeName: `${item.name}.${item.extension}`, 156 | }; 157 | }); 158 | }, 159 | ); 160 | 161 | // export const selectedTab = createSelector( 162 | // (state: RootState) => state.structure.normalized, 163 | // (state: RootState) => state.tabs.open, 164 | // (state: RootState) => state.tabs.selected, 165 | // (normalized: Normalized, openTabs: Tab[], selected: string) => { 166 | // return openTabs 167 | // .map((tab) => { 168 | // const item = normalized.files.byId[tab.id]; 169 | // return { 170 | // ...tab, 171 | // extension: item.extension, 172 | // wholeName: `${item.name}.${item.extension}`, 173 | // }; 174 | // }) 175 | // .find(({ id }) => id === selected)?.id; 176 | // } 177 | // ); 178 | -------------------------------------------------------------------------------- /src/state/hooks.ts: -------------------------------------------------------------------------------- 1 | 2 | import { 3 | createDispatchHook, 4 | createSelectorHook, 5 | createStoreHook, 6 | } from "react-redux"; 7 | import type { TypedUseSelectorHook } from "react-redux"; 8 | import type { RootState, AppDispatch } from "./store"; 9 | import { VFSContext } from "./context"; 10 | 11 | 12 | // Use throughout your app instead of plain `useDispatch` and `useSelector` 13 | 14 | export const useStore = createStoreHook(VFSContext); 15 | const useDispatch = createDispatchHook(VFSContext); 16 | const useSelector = createSelectorHook(VFSContext); 17 | 18 | export const useTypedDispatch: () => AppDispatch = useDispatch; 19 | export const useTypedSelector: TypedUseSelectorHook = useSelector; 20 | -------------------------------------------------------------------------------- /src/state/provider.tsx: -------------------------------------------------------------------------------- 1 | import { Provider } from "react-redux"; 2 | import { store, persistor } from "./store"; 3 | import { PersistGate } from "redux-persist/integration/react"; 4 | import { VFSContext } from "./context"; 5 | import { PropsWithChildren } from "react"; 6 | 7 | const VFSProvider: React.FC = ({ children }) => { 8 | return ( 9 | 10 | Loading...} 12 | persistor={persistor} 13 | > 14 | {children} 15 | 16 | ); 17 | }; 18 | 19 | export default VFSProvider; -------------------------------------------------------------------------------- /src/state/store.ts: -------------------------------------------------------------------------------- 1 | import { combineReducers, configureStore } from "@reduxjs/toolkit"; 2 | import structureReducer from "./features/structure/structureSlice"; 3 | import tabsReducer from "./features/tabs/tabsSlice"; 4 | import storage from "redux-persist/lib/storage"; // defaults to localStorage for web 5 | // import { listenerMiddleware } from "./middleware/sendNormalized"; 6 | import { persistStore, persistReducer } from "redux-persist"; 7 | import miniStructureReducer from "./features/structure/miniStructureSlice"; 8 | 9 | const persistConfig = { 10 | key: "project", 11 | storage, 12 | whitelist: ["structure", "editor", "tabs"], 13 | }; 14 | 15 | const rootReducer = combineReducers({ 16 | structure: structureReducer, 17 | tabs: tabsReducer, 18 | miniStructure: miniStructureReducer, 19 | }); 20 | 21 | const persistedReducer = persistReducer(persistConfig, rootReducer); 22 | export const store = configureStore({ 23 | reducer: persistedReducer, 24 | middleware: getDefaultMiddleware => getDefaultMiddleware(), 25 | }); 26 | 27 | export const persistor = persistStore(store); 28 | 29 | export type RootState = ReturnType; 30 | export type AppDispatch = typeof store.dispatch; 31 | -------------------------------------------------------------------------------- /src/types/Breadcrumbs.d.ts: -------------------------------------------------------------------------------- 1 | export interface BreadcrumbsProps { 2 | containerClassName?: string; 3 | textClassName?: string; 4 | miniFolderCollapseBtnClassName?: string; 5 | miniFolderCollapseBtnStyle?: React.CSSProperties; 6 | miniFolderContainerClassName?: string; 7 | itemTitleClassName?: string; 8 | onBreadcrumbFileClick?: (id: string) => void; 9 | } 10 | declare const Breadcrumbs: React.FC; 11 | export default Breadcrumbs; 12 | -------------------------------------------------------------------------------- /src/types/CollapseBtn.d.ts: -------------------------------------------------------------------------------- 1 | import { type ItemType } from "../state/features/structure/structureSlice"; 2 | interface CollapseBtnProps { 3 | item: { 4 | id: string; 5 | name: string; 6 | type: ItemType; 7 | }; 8 | className?: string; 9 | style?: React.CSSProperties; 10 | onClickE: (e: React.MouseEvent) => void; 11 | } 12 | declare const CollapseBtn: React.FC; 13 | export default CollapseBtn; 14 | -------------------------------------------------------------------------------- /src/types/CustomInput.d.ts: -------------------------------------------------------------------------------- 1 | interface CustomInputProps { 2 | closeCallback: React.Dispatch>; 3 | submit: (value: string | false) => void; 4 | padding: number; 5 | show: boolean | undefined; 6 | item: { 7 | type: "file" | "folder" | ""; 8 | rename: { 9 | wholeName?: string; 10 | } | undefined; 11 | }; 12 | container: HTMLDivElement | null; 13 | validExtensions: string[]; 14 | existingItems: Array<{ 15 | wholeName: string; 16 | type: string; 17 | }>; 18 | className?: string; 19 | style?: React.CSSProperties; 20 | } 21 | declare const CustomInput: React.FC; 22 | export default CustomInput; 23 | -------------------------------------------------------------------------------- /src/types/Dialog.d.ts: -------------------------------------------------------------------------------- 1 | interface DialogProps { 2 | title: string; 3 | content: string; 4 | actionText: string; 5 | close: (show: boolean) => void; 6 | action: () => void; 7 | className?: string; 8 | } 9 | declare const Dialog: React.FC; 10 | export default Dialog; 11 | -------------------------------------------------------------------------------- /src/types/FileActions.d.ts: -------------------------------------------------------------------------------- 1 | interface FileActionProps { 2 | newFile: () => void; 3 | newFolder: () => void; 4 | download: () => void; 5 | collapseArea: () => void; 6 | collapsed: boolean; 7 | projectName?: string; 8 | btnClassName?: string; 9 | disableTooltip?: true; 10 | disableCollapse?: true; 11 | disableDownload?: true; 12 | } 13 | declare const FileActions: React.FC; 14 | export default FileActions; 15 | -------------------------------------------------------------------------------- /src/types/Folder.d.ts: -------------------------------------------------------------------------------- 1 | import { type Directory, type FileInFolder, ItemType } from "../state/features/structure/structureSlice"; 2 | interface FolderProps { 3 | data: Array; 4 | showBlue: boolean; 5 | setShowBlue: React.Dispatch>; 6 | showGray: boolean; 7 | setShowGray: React.Dispatch>; 8 | collapseBtnClassname?: string; 9 | collapseBtnStyle?: React.CSSProperties; 10 | threeDotPrimaryClass?: string; 11 | threeDotSecondaryClass?: string; 12 | clickableAreaClassName?: string; 13 | selectedClickableAreaClassName?: string; 14 | contextSelectedClickableAreaClassName?: string; 15 | itemTitleClassName?: string; 16 | onItemSelected?: (item: { 17 | id: string; 18 | type: ItemType; 19 | }) => void; 20 | onItemContextSelected?: (item: { 21 | id: string; 22 | type: ItemType; 23 | }) => void; 24 | } 25 | declare const Folder: React.FC; 26 | export default Folder; 27 | -------------------------------------------------------------------------------- /src/types/HighlightedText.d.ts: -------------------------------------------------------------------------------- 1 | interface HighlightedTextProps { 2 | hightlight: string; 3 | highlightClass?: string; 4 | lineOfText: string; 5 | lineNum: number; 6 | openAtLine: (lineNum: number) => void; 7 | } 8 | declare const HighlightedText: React.FC; 9 | export default HighlightedText; 10 | -------------------------------------------------------------------------------- /src/types/ItemTitle.d.ts: -------------------------------------------------------------------------------- 1 | import { type ItemType } from "../state/features/structure/structureSlice"; 2 | interface ItemTitleProps { 3 | item: { 4 | id: string; 5 | name: string; 6 | type: ItemType; 7 | collapsed?: boolean; 8 | extension?: string; 9 | }; 10 | onClickE: (e: React.MouseEvent) => void; 11 | className?: string; 12 | } 13 | declare const ItemTitle: React.FC; 14 | export default ItemTitle; 15 | -------------------------------------------------------------------------------- /src/types/MenuContext.d.ts: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | interface MenuContextProps { 3 | top: number; 4 | left: number; 5 | showContext: boolean; 6 | setShowContext: React.Dispatch>; 7 | actions: Array<{ 8 | title: string; 9 | handler: () => void; 10 | disabled: boolean; 11 | type?: undefined; 12 | } | { 13 | type: string; 14 | handler: () => void; 15 | title?: undefined; 16 | disabled?: undefined; 17 | }>; 18 | className?: string; 19 | clickableAreaClassName?: string; 20 | hrColor?: string; 21 | } 22 | declare const MenuContext: React.FC; 23 | export default MenuContext; 24 | -------------------------------------------------------------------------------- /src/types/MiniFolder.d.ts: -------------------------------------------------------------------------------- 1 | import { type MiniStructure } from "../state/features/structure/miniStructureSlice"; 2 | import { type Identifier } from "../state/features/structure/structureSlice"; 3 | interface MiniFolderProps { 4 | data: MiniStructure; 5 | init: boolean; 6 | onClickItem: (item: Identifier) => void; 7 | onCollapseMiniStructure: (id: string) => void; 8 | collapseBtnClassName?: string; 9 | collapseBtnStyle?: React.CSSProperties; 10 | containerClassName?: string; 11 | titleClassName?: string; 12 | } 13 | declare const MiniFolder: React.FC; 14 | export default MiniFolder; 15 | -------------------------------------------------------------------------------- /src/types/SearchContainer.d.ts: -------------------------------------------------------------------------------- 1 | export interface SearchContainerProps { 2 | highlightedTextClassName?: string; 3 | headerClassName?: string; 4 | headerStyle?: React.CSSProperties; 5 | titleClassName?: string; 6 | } 7 | declare const SearchContainer: React.FC; 8 | export default SearchContainer; 9 | -------------------------------------------------------------------------------- /src/types/SearchInput.d.ts: -------------------------------------------------------------------------------- 1 | import { SearchResults } from "../state/features/structure/structureSlice"; 2 | export interface SearchInputProps { 3 | className?: string; 4 | style?: React.CSSProperties; 5 | onSearchFiles?: (searchTerm: string, searchResults: SearchResults) => void; 6 | } 7 | declare const SearchInput: React.FC; 8 | export default SearchInput; 9 | -------------------------------------------------------------------------------- /src/types/SearchResults.d.ts: -------------------------------------------------------------------------------- 1 | import { type MatchingFile } from "../state/features/structure/structureSlice"; 2 | interface SearchResultsProps { 3 | matchingFile: MatchingFile; 4 | fileAtLineClick: (id: string, lineNum: number) => void; 5 | headerClassName?: string; 6 | headerStyle?: React.CSSProperties; 7 | titleClassName?: string; 8 | highlightClass?: string; 9 | } 10 | declare const SearchResults: React.FC; 11 | export default SearchResults; 12 | -------------------------------------------------------------------------------- /src/types/Structure.d.ts: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { type ItemType } from "../state/features/structure/structureSlice"; 3 | export interface StructureProps { 4 | deleteConfirmationClassName?: string; 5 | fileInputClassName?: string; 6 | fileInputStyle?: React.CSSProperties; 7 | contextMenuClassName?: string; 8 | contextMenuHrColor?: string; 9 | contextMenuClickableAreaClassName?: string; 10 | fileActionsBtnClassName?: string; 11 | projectName?: string; 12 | fileActionsDisableCollapse?: true; 13 | fileActionsDisableTooltip?: true; 14 | fileActionsDisableDownload?: true; 15 | folderCollapseBtnClassname?: string; 16 | folderCollapseBtnStyle?: React.CSSProperties; 17 | folderThreeDotPrimaryClass?: string; 18 | folderThreeDotSecondaryClass?: string; 19 | folderClickableAreaClassName?: string; 20 | folderSelectedClickableAreaClassName?: string; 21 | folderContextSelectedClickableAreaClassName?: string; 22 | itemTitleClassName?: string; 23 | structureContainerClassName?: string; 24 | containerHeight?: string; 25 | onItemSelected?: (item: { 26 | id: string; 27 | type: ItemType; 28 | }) => void; 29 | onNewItemClick?: (parentFolderId: string, type: ItemType) => void; 30 | onAreaCollapsed?: (collapsed: boolean) => void; 31 | onItemContextSelected?: (item: { 32 | id: string; 33 | type: ItemType; 34 | }) => void; 35 | onNodeDeleted?: (id: string) => void; 36 | onNewItemCreated?: (id: string) => void; 37 | storeContext?: any; 38 | validExtensions: string[]; 39 | } 40 | declare const Structure: React.FC; 41 | export default Structure; 42 | -------------------------------------------------------------------------------- /src/types/Tab.d.ts: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | interface TabProps { 3 | id: string; 4 | name: string; 5 | type: string; 6 | selected: boolean; 7 | onSelect: (id: string) => void; 8 | onClose: (id: string) => void; 9 | className?: string; 10 | selectedTabClassName?: string; 11 | } 12 | declare const Tab: React.FC; 13 | export default Tab; 14 | -------------------------------------------------------------------------------- /src/types/Tabs.d.ts: -------------------------------------------------------------------------------- 1 | export interface TabsProps { 2 | containerClassName?: string; 3 | tabClassName?: string; 4 | selectedTabClassName?: string; 5 | onTabClick?: (id: string) => void; 6 | onTabClose?: (id: string) => void; 7 | } 8 | declare const Tabs: React.FC; 9 | export default Tabs; 10 | -------------------------------------------------------------------------------- /src/types/ThreeDots.d.ts: -------------------------------------------------------------------------------- 1 | import { type ItemType } from "../state/features/structure/structureSlice"; 2 | interface ThreeDotsProp { 3 | item: { 4 | id: string; 5 | type: ItemType; 6 | }; 7 | selected: string; 8 | showBlue: boolean; 9 | primaryClass?: string; 10 | secondaryClass?: string; 11 | onClickE: (e: React.MouseEvent) => void; 12 | } 13 | declare const ThreeDots: React.FC; 14 | export default ThreeDots; 15 | -------------------------------------------------------------------------------- /src/types/index.d.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiteCode-IDE/virtual-file-system/f79637f985663ba90d1617a7779fd3914eebd570/src/types/index.d.ts -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: ["./src/**/*.{ts,tsx}"], 4 | theme: { 5 | extend: { 6 | colors: { 7 | // 'primary-green': '#50FF6C', 8 | "dark-bg": "#0E1525", 9 | "dark-bg-2": "#1C2333", 10 | "dark-hover": "#2B3245", 11 | "monaco-color": "#3C3C3C", 12 | "hover-blue": "#04395E", 13 | "vscode-blue": "#4078CE", 14 | "git-orange": "#F05033", 15 | "editor-bg": "#212733", 16 | "monaco-vs": "#1E1E1E", 17 | }, 18 | fontFamily: { 19 | roboto: ["Roboto", "sans-serif"], 20 | }, 21 | }, 22 | }, 23 | plugins: [], 24 | } 25 | 26 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true 22 | }, 23 | "include": ["src", "dist/@litecode-ide/index.d.ts"], 24 | "references": [{ "path": "./tsconfig.node.json" }] 25 | } 26 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import { defineConfig } from "vite"; 3 | import react from "@vitejs/plugin-react-swc"; 4 | 5 | export default defineConfig({ 6 | build: { 7 | lib: { 8 | entry: path.resolve(__dirname, "src/lib/index.tsx"), 9 | name: "Virtual File System", 10 | fileName: (format) => `@litecode-ide/virtual-file-system.${format}.js`, 11 | }, 12 | rollupOptions: { 13 | external: ["react", "react-dom"], 14 | output: { 15 | globals: { 16 | react: "React", 17 | }, 18 | }, 19 | }, 20 | }, 21 | plugins: [react()], 22 | 23 | }); 24 | --------------------------------------------------------------------------------