├── .eslintrc.json ├── .gitattributes ├── .github └── workflows │ ├── main.yml │ ├── publish.yml │ └── release.yml ├── .gitignore ├── .npmrc ├── .storybook ├── main.ts ├── preview-head.html ├── preview.ts └── preview.tsx ├── LICENSE ├── README.md ├── docs └── rfm.png ├── global.d.ts ├── index.html ├── lib ├── ReactFileManager.test.tsx ├── ReactFileManager.tsx ├── components │ ├── CommonModal.tsx │ ├── FileIcon.tsx │ ├── FolderPath.tsx │ ├── InfoModal.tsx │ ├── Navbar.tsx │ ├── NewFolderIcon.tsx │ ├── NewFolderModal.tsx │ ├── SvgIcon.tsx │ ├── UploadFileModal.tsx │ ├── Workspace.tsx │ └── index.ts ├── context │ ├── FileManagerContext.tsx │ └── index.ts ├── index.ts ├── tailwind.css └── types │ ├── Enums.ts │ ├── Types.ts │ └── index.ts ├── package-lock.json ├── package.json ├── postcss.config.cjs ├── src ├── App.css ├── App.tsx ├── main.tsx ├── stories │ └── RFM.stories.ts └── vite-env.d.ts ├── tailwind.config.cjs ├── tsconfig.json ├── tsup.config.ts └── vite.config.ts /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@frontside", "plugin:storybook/recommended"], 3 | "rules": { 4 | "eol-last": "off", 5 | "@typescript-eslint/no-extra-semi": "off", 6 | "@typescript-eslint/no-non-null-assertion": "off", 7 | "@typescript-eslint/no-explicit-any": "off", 8 | "@typescript-eslint/consistent-type-imports": "warn", 9 | "no-multi-spaces": "off", 10 | "no-trailing-spaces": "off", 11 | "object-curly-spacing": "off", 12 | "semi": "off", 13 | "no-console": "warn", 14 | "prefer-let/prefer-let": "off" 15 | }, 16 | "ignorePatterns": ["playground"] 17 | } 18 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.js linguist-detectable=false 2 | *.ts linguist-detectable=true 3 | *.tsx linguist-detectable=true -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - "**" 6 | pull_request: 7 | types: [opened, edited] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | - uses: actions/setup-node@v3 15 | with: 16 | node-version: 20.x 17 | cache: "npm" 18 | 19 | - run: npm install 20 | - run: npm run lint 21 | - run: npm run build 22 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish Package to npmjs 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v3 13 | with: 14 | fetch-depth: 0 15 | 16 | - name: Set node 17 | uses: actions/setup-node@v3 18 | with: 19 | node-version: 20.x 20 | registry-url: "https://registry.npmjs.org" 21 | - run: npm install 22 | - run: npm run build 23 | - run: npm publish --access public 24 | env: 25 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 26 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | permissions: 4 | contents: write 5 | 6 | on: 7 | push: 8 | tags: 9 | - "v*" 10 | 11 | jobs: 12 | release: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v3 16 | with: 17 | fetch-depth: 0 18 | 19 | - name: Set node 20 | uses: actions/setup-node@v3 21 | with: 22 | node-version: 20.x 23 | 24 | - run: npx changelogithub 25 | env: 26 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 27 | -------------------------------------------------------------------------------- /.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 | playground 10 | 11 | node_modules 12 | dist 13 | dist-ssr 14 | *.local 15 | 16 | # Editor directories and files 17 | .vscode/* 18 | !.vscode/extensions.json 19 | .idea 20 | .DS_Store 21 | *.suo 22 | *.ntvs* 23 | *.njsproj 24 | *.sln 25 | *.sw? 26 | 27 | *storybook.log -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.org/ -------------------------------------------------------------------------------- /.storybook/main.ts: -------------------------------------------------------------------------------- 1 | import type { StorybookConfig } from "@storybook/react-vite"; 2 | 3 | const config: StorybookConfig = { 4 | stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"], 5 | addons: [ 6 | "@storybook/addon-onboarding", 7 | "@storybook/addon-links", 8 | "@storybook/addon-essentials", 9 | "@chromatic-com/storybook", 10 | "@storybook/addon-interactions", 11 | "@storybook/addon-mdx-gfm", 12 | ], 13 | framework: { 14 | name: "@storybook/react-vite", 15 | options: {}, 16 | }, 17 | docs: { 18 | autodocs: "tag", 19 | }, 20 | }; 21 | export default config; 22 | -------------------------------------------------------------------------------- /.storybook/preview-head.html: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /.storybook/preview.ts: -------------------------------------------------------------------------------- 1 | import type { Preview } from "@storybook/react"; 2 | 3 | const preview: Preview = { 4 | parameters: { 5 | controls: { 6 | matchers: { 7 | color: /(background|color)$/i, 8 | date: /Date$/i, 9 | }, 10 | }, 11 | }, 12 | }; 13 | 14 | export default preview; 15 | -------------------------------------------------------------------------------- /.storybook/preview.tsx: -------------------------------------------------------------------------------- 1 | export const parameters = { 2 | actions: { argTypesRegex: "^on[A-Z].*" }, 3 | controls: { 4 | matchers: { 5 | color: /(background|color)$/i, 6 | date: /Date$/, 7 | }, 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) thelicato 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 |
3 | react-file-manager 4 |

5 |

6 | React File Manager 7 |

8 | 9 |

10 | 11 | 12 | 13 | 14 | 15 | 16 |

17 | 18 | `react-file-manager` is a comprehensive UI library for React that enables developers to quickly integrate a file management system into their web applications. This package provides a set of graphical components along with a suite of callback functions to handle various file actions such as refreshing, uploading, creating folders, and deleting files. 19 | 20 | ## Features 21 | 22 | - **Customizable UI**: Easy to integrate with existing projects and modify according to the theme of your application. 23 | - **Event Handling**: Built-in support for essential file management operations including refresh, upload, create folder, and delete. 24 | - **React Optimized**: Utilizes React's latest features for optimal performance and compatibility. 25 | 26 | ## Installation 27 | 28 | Install `react-file-manager` using npm: 29 | 30 | ```bash 31 | npm install @thelicato/react-file-manager 32 | ``` 33 | 34 | ## Usage 35 | 36 | Here's a quick example to get you started: 37 | 38 | ```javascript 39 | import React from "react"; 40 | import type { FileSystemType } from "@thelicato/react-file-manager"; 41 | import { ReactFileManager } from "@thelicato/react-file-manager"; 42 | 43 | export const dummyFileSystem: FileSystemType = [ 44 | { id: "0", name: "/", path: "/", isDir: true }, 45 | { 46 | id: "31258", 47 | name: "report.pdf", 48 | isDir: false, 49 | parentId: "0", 50 | }, 51 | { 52 | id: "31259", 53 | name: "Documents", 54 | isDir: true, 55 | parentId: "0", 56 | path: "/Documents", 57 | }, 58 | { 59 | id: "31261", 60 | name: "Personal", 61 | isDir: true, 62 | parentId: "31259", 63 | path: "/Documents/Personal", 64 | }, 65 | { 66 | id: "31260", 67 | name: "report.docx", 68 | isDir: false, 69 | parentId: "0", 70 | }, 71 | { 72 | id: "31267", 73 | name: "Images", 74 | isDir: true, 75 | parentId: "0", 76 | path: "/Images", 77 | }, 78 | { 79 | id: "31260", 80 | name: "logo.png", 81 | isDir: false, 82 | parentId: "31267", 83 | }, 84 | ]; 85 | 86 | function App() { 87 | return ( 88 |
89 | 90 |
91 | ); 92 | } 93 | 94 | export default App; 95 | ``` 96 | 97 | This example provide a dummy filesystem, of course you can also map a real file system as long as you create an array of ``FileSystemType``. 98 | 99 | ## License 100 | 101 | [MIT](./LICENSE) License © [Angelo Delicato](https://github.com/thelicato) -------------------------------------------------------------------------------- /docs/rfm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thelicato/react-file-manager/8ea934225587928dc40b417f44cbfd557f33e2bf/docs/rfm.png -------------------------------------------------------------------------------- /global.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.css"; 2 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | React File Manager 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /lib/ReactFileManager.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { render } from "@testing-library/react"; 3 | import { describe, it, expect } from "vitest"; 4 | import type { FileSystemType } from "."; 5 | import { ReactFileManager } from "."; 6 | 7 | export const dummyFileSystem: FileSystemType = [ 8 | { id: "0", name: "/", path: "/", isDir: true }, 9 | { 10 | id: "31258", 11 | name: "report.pdf", 12 | isDir: false, 13 | parentId: "0", 14 | }, 15 | { 16 | id: "31259", 17 | name: "Documents", 18 | isDir: true, 19 | parentId: "0", 20 | path: "/Documents", 21 | }, 22 | ]; 23 | 24 | describe("it", () => { 25 | it("renders without crashing", async () => { 26 | const result = render(); 27 | const workspace = result.container.querySelector( 28 | "#react-file-manager-workspace" 29 | ); 30 | expect(workspace).not.toBeNull(); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /lib/ReactFileManager.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | // Context 3 | import { FileManagerContext } from "./context"; 4 | // Components 5 | import { Navbar, Workspace } from "./components"; 6 | // Types 7 | import type { FileSystemType } from "./types"; 8 | import { ViewStyle } from "./types"; 9 | 10 | export interface IFileManagerProps { 11 | fs: FileSystemType; 12 | viewOnly?: boolean; 13 | onDoubleClick?: (id: string) => Promise; 14 | onRefresh?: (id: string) => Promise; 15 | onUpload?: (fileData: any, folderId: string) => Promise; 16 | onCreateFolder?: (folderName: string) => Promise; 17 | onDelete?: (fileId: string) => Promise; 18 | } 19 | 20 | export const ReactFileManager = ({ 21 | fs, 22 | viewOnly, 23 | onDoubleClick, 24 | onRefresh, 25 | onUpload, 26 | onCreateFolder, 27 | onDelete, 28 | }: IFileManagerProps) => { 29 | const [currentFolder, setCurrentFolder] = useState("0"); // Root folder ID must be "0" 30 | const [uploadedFileData, setUploadedFileData] = useState(); 31 | const [viewStyle, setViewStyle] = useState(ViewStyle.List); 32 | 33 | return ( 34 | 51 |
52 | 53 | 54 |
55 |
56 | ); 57 | }; 58 | -------------------------------------------------------------------------------- /lib/components/CommonModal.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Draggable from "react-draggable"; 3 | import SvgIcon from "./SvgIcon"; 4 | 5 | interface IModalProps { 6 | title: string; 7 | children: React.ReactNode; 8 | isVisible: boolean; 9 | onClose: () => void; 10 | } 11 | 12 | const CommonModal: React.FC = ({ 13 | children, 14 | title, 15 | isVisible, 16 | onClose, 17 | }: IModalProps) => { 18 | if (!isVisible) { 19 | return <>; 20 | } 21 | return ( 22 | 23 |
24 |
25 |

{title}

26 | 31 |
32 | {children} 33 |
34 |
35 | ); 36 | }; 37 | 38 | export default CommonModal; 39 | -------------------------------------------------------------------------------- /lib/components/FileIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useMemo } from "react"; 3 | // Context 4 | import { useFileManager } from "../context"; 5 | // Components 6 | import SvgIcon from "./SvgIcon"; 7 | 8 | interface IFileIcon { 9 | id: string; 10 | name: string; 11 | isDir: boolean; 12 | } 13 | 14 | const FileIcon = (props: IFileIcon) => { 15 | const { setCurrentFolder, onRefresh } = useFileManager(); 16 | 17 | const handleClick = async () => { 18 | if (props.isDir) { 19 | setCurrentFolder(props.id); 20 | if (onRefresh !== undefined) { 21 | try { 22 | await onRefresh(props.id); 23 | } catch (e) { 24 | throw new Error("Error during refresh"); 25 | } 26 | } 27 | } 28 | }; 29 | 30 | const fileExtension = useMemo((): string => { 31 | if (!props.name.includes(".")) { 32 | return ""; 33 | } 34 | 35 | const nameArray = props.name.split("."); 36 | return `.${nameArray[nameArray.length - 1]}`; 37 | }, [props.id]); 38 | 39 | return ( 40 | <> 41 |
42 | 46 | {fileExtension} 47 | {props.name} 48 |
49 | 50 | ); 51 | }; 52 | 53 | export default FileIcon; 54 | -------------------------------------------------------------------------------- /lib/components/FolderPath.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo } from "react"; 2 | // Context 3 | import { useFileManager } from "../context"; 4 | // Types 5 | import type { FileType } from "../types"; 6 | import { ViewStyle } from "../types"; 7 | // Components 8 | import SvgIcon from "./SvgIcon"; 9 | 10 | const FolderPath = () => { 11 | const { fs, currentFolder, setCurrentFolder, viewStyle, setViewStyle } = useFileManager(); 12 | 13 | const goUp = () => { 14 | const currentFolderInfo = fs.find((f: FileType) => f.id === currentFolder); 15 | if (currentFolderInfo && currentFolderInfo.parentId) { 16 | setCurrentFolder(currentFolderInfo.parentId); 17 | } 18 | }; 19 | 20 | const parentPath = useMemo((): string => { 21 | const parentId: string | undefined = fs.find( 22 | (f: FileType) => f.id === currentFolder 23 | )?.parentId; 24 | if (!parentId) { 25 | return ""; 26 | } 27 | const parentDir = fs.find((f: FileType) => f.id === parentId); 28 | if (!parentDir?.path) { 29 | return ""; 30 | } 31 | 32 | const _parentPath = 33 | parentDir.path.slice(-1) === "/" ? parentDir.path : `${parentDir.path}/`; 34 | return _parentPath; 35 | }, [fs, currentFolder]); 36 | 37 | const currentPath = useMemo((): string => { 38 | const currentFolderInfo = fs.find((f: FileType) => f.id === currentFolder); 39 | return currentFolderInfo ? currentFolderInfo.name : ""; 40 | }, [fs, currentFolder]); 41 | 42 | return ( 43 |
44 |
45 | 50 | 51 | {parentPath} 52 | {currentPath} 53 | 54 |
55 |
56 | setViewStyle(ViewStyle.List)} 60 | /> 61 | setViewStyle(ViewStyle.Icons)} 65 | /> 66 |
67 |
68 | ); 69 | }; 70 | 71 | export default FolderPath; 72 | -------------------------------------------------------------------------------- /lib/components/InfoModal.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const InfoModal = () => { 4 | return <>; 5 | }; 6 | 7 | export default InfoModal; 8 | -------------------------------------------------------------------------------- /lib/components/Navbar.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo } from "react"; 2 | import { useFileManager } from "../context"; 3 | import type { FileType } from "../types"; 4 | 5 | const Navbar = () => { 6 | const { fs, setCurrentFolder, onRefresh } = useFileManager(); 7 | 8 | const initialFolders = useMemo(() => { 9 | return fs.filter((f: FileType) => f.isDir && f.parentId === "0"); 10 | }, [fs]); 11 | 12 | const handleClick = async (id: string) => { 13 | setCurrentFolder(id); 14 | if (onRefresh !== undefined) { 15 | try { 16 | await onRefresh(id); 17 | } catch (e) { 18 | throw new Error("Error during refresh"); 19 | } 20 | } 21 | }; 22 | 23 | return ( 24 |
25 | setCurrentFolder("0")} 27 | className="rfm-navbar-root-link" 28 | > 29 | Root 30 | 31 | 32 |
    33 | {initialFolders.map((f: FileType) => { 34 | return ( 35 |
  • handleClick(f.id)} 37 | className="rfm-navbar-list-element" 38 | key={f.id} 39 | > 40 | {f.name} 41 |
  • 42 | ); 43 | })} 44 |
45 |
46 | ); 47 | }; 48 | 49 | export default Navbar; 50 | -------------------------------------------------------------------------------- /lib/components/NewFolderIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const NewFolderIcon = ({ 4 | onClick, 5 | }: React.AllHTMLAttributes) => { 6 | return ( 7 |
8 | + 9 |
10 | ); 11 | }; 12 | 13 | export default NewFolderIcon; 14 | -------------------------------------------------------------------------------- /lib/components/NewFolderModal.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef } from "react"; 2 | import { useFileManager } from "../context"; 3 | import CommonModal from "./CommonModal"; 4 | 5 | interface INewFolderModalProps { 6 | isVisible: boolean; 7 | onClose: () => void; 8 | } 9 | 10 | const NewFolderModal = (props: INewFolderModalProps) => { 11 | const { onCreateFolder } = useFileManager(); 12 | const folderName = useRef(); 13 | 14 | const onConfirm = async () => { 15 | if ( 16 | folderName && 17 | folderName.current && 18 | folderName.current.value && 19 | folderName.current.value.length > 0 && 20 | onCreateFolder 21 | ) { 22 | await onCreateFolder(folderName.current.value); 23 | } 24 | }; 25 | 26 | return ( 27 | 28 |
29 |
30 |
31 | 38 |
39 | 52 |
53 |
54 |
55 | ); 56 | }; 57 | 58 | export default NewFolderModal; 59 | -------------------------------------------------------------------------------- /lib/components/SvgIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | interface ISvgIconProps extends React.AllHTMLAttributes { 4 | svgType: "file" | "folder" | "arrow-up" | "arrow-down" | "close" | "list" | "icons"; 5 | } 6 | 7 | const SvgIcon: React.FC = ({ 8 | svgType, 9 | ...props 10 | }: ISvgIconProps) => { 11 | const svgContent = () => { 12 | switch (svgType) { 13 | case "file": { 14 | return ( 15 | 21 | 22 | 23 | 24 | ); 25 | } 26 | case "folder": { 27 | return ( 28 | 34 | 38 | 42 | 43 | ); 44 | } 45 | case "arrow-up": { 46 | return ( 47 | 48 | 49 | 50 | ); 51 | } 52 | case "arrow-down": { 53 | return ( 54 | 55 | 56 | 57 | 58 | ) 59 | } 60 | case "close": { 61 | return ( 62 | 63 | 64 | 65 | ); 66 | } 67 | case "list": { 68 | return ( 69 | 70 | 71 | 72 | 73 | ) 74 | } 75 | case "icons": { 76 | return ( 77 | 78 | 79 | 80 | 81 | ) 82 | } 83 | default: { 84 | return ""; 85 | } 86 | } 87 | }; 88 | 89 | return
{svgContent()}
; 90 | }; 91 | 92 | export default SvgIcon; 93 | -------------------------------------------------------------------------------- /lib/components/UploadFileModal.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useFileManager } from "../context"; 3 | import CommonModal from "./CommonModal"; 4 | 5 | interface IUploadFileModalProps { 6 | isVisible: boolean; 7 | onClose: () => void; 8 | } 9 | 10 | const UploadFileModal = (props: IUploadFileModalProps) => { 11 | const { onUpload, uploadedFileData, currentFolder } = useFileManager(); 12 | 13 | const onConfirm = async () => { 14 | if (onUpload && uploadedFileData) { 15 | await onUpload(uploadedFileData, currentFolder); 16 | } 17 | }; 18 | 19 | return ( 20 | 21 |
22 |

23 | Are you sure you want to upload the file? 24 |

25 |
26 | 33 | 40 |
41 |
42 |
43 | ); 44 | }; 45 | 46 | export default UploadFileModal; 47 | -------------------------------------------------------------------------------- /lib/components/Workspace.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useMemo, useCallback, useEffect } from "react"; 2 | import { useDropzone } from "react-dropzone"; 3 | import { useFileManager } from "../context"; 4 | import type { FileType } from "../types"; 5 | import { ViewStyle } from "../types"; 6 | 7 | // Components 8 | import FileIcon from "./FileIcon"; 9 | import NewFolderIcon from "./NewFolderIcon"; 10 | import FolderPath from "./FolderPath"; 11 | import NewFolderModal from "./NewFolderModal"; 12 | import UploadFileModal from "./UploadFileModal"; 13 | 14 | 15 | import { 16 | createColumnHelper, 17 | flexRender, 18 | getCoreRowModel, 19 | getSortedRowModel, 20 | useReactTable, 21 | } from '@tanstack/react-table' 22 | import SvgIcon from "./SvgIcon"; 23 | 24 | 25 | const columnHelper = createColumnHelper() 26 | 27 | const columns = [ 28 | columnHelper.accessor('name', { 29 | header: () => 'Name', 30 | cell: info => ( 31 |
32 | 33 |

{info.getValue()}

34 |
35 | ), 36 | }), 37 | columnHelper.accessor('lastModified', { 38 | header: () => 'Last Modified', 39 | cell: info => info.getValue() ? new Date((info.getValue() as number) * 1000).toLocaleString() : 'N/A', 40 | }), 41 | ] 42 | 43 | const Workspace = () => { 44 | const { currentFolder, fs, viewStyle, viewOnly, setCurrentFolder, setUploadedFileData, onDoubleClick, onRefresh } = useFileManager(); 45 | const [newFolderModalVisible, setNewFolderModalVisible] = 46 | useState(false); 47 | const [uploadFileModalVisible, setUploadFileModalVisible] = 48 | useState(false); 49 | 50 | const setUploadModalVisible = (value: boolean) => { 51 | if (viewOnly) { 52 | setUploadFileModalVisible(false); 53 | } else { 54 | setUploadFileModalVisible(value); 55 | } 56 | }; 57 | 58 | useEffect(() => { 59 | if (newFolderModalVisible) { 60 | setNewFolderModalVisible(false); 61 | } 62 | if (uploadFileModalVisible) { 63 | setUploadModalVisible(false); 64 | setUploadedFileData(undefined); 65 | } 66 | }, [currentFolder]); 67 | 68 | const onDrop = useCallback( 69 | (acceptedFiles: File[]) => { 70 | const file = acceptedFiles[0]; 71 | setUploadedFileData(file); 72 | setUploadModalVisible(true); 73 | }, 74 | [setUploadedFileData] 75 | ); 76 | 77 | const onCloseUploadFileModal = () => { 78 | setUploadModalVisible(false); 79 | setUploadedFileData(undefined); 80 | }; 81 | 82 | const { getRootProps, isDragAccept } = useDropzone({ 83 | noClick: true, 84 | noKeyboard: true, 85 | onDrop: onDrop, 86 | }); 87 | 88 | const currentFolderFiles = useMemo(() => { 89 | const files = fs.filter((f: FileType) => f.parentId === currentFolder); 90 | return files; 91 | }, [fs, currentFolder]); 92 | 93 | const table = useReactTable({data: currentFolderFiles, columns, getCoreRowModel: getCoreRowModel(), getSortedRowModel: getSortedRowModel(), 94 | initialState: { 95 | sorting: [{ id: 'name', desc: false }], 96 | },}) 97 | 98 | const handleClick = async (file: FileType) => { 99 | 100 | if (file.isDir) { 101 | setCurrentFolder(file.id); 102 | if (onRefresh !== undefined) { 103 | try { 104 | await onRefresh(file.id); 105 | } catch (e) { 106 | throw new Error("Error during refresh"); 107 | } 108 | } 109 | } 110 | 111 | }; 112 | 113 | const handleDoubleClick = (id: string) => { 114 | if (onDoubleClick) { 115 | onDoubleClick(id) 116 | } 117 | } 118 | 119 | return ( 120 |
127 | {/* Top bar with folder path */} 128 | 129 | 130 | {/* File listing */} 131 |
132 | 133 | {/* Icons File View */} 134 | {viewStyle === ViewStyle.Icons && ( 135 | <> 136 | {currentFolderFiles.map((f: FileType, key: number) => { 137 | return ( 138 | 141 | )} 142 | )} 143 | {!viewOnly && ( 144 | setNewFolderModalVisible(true)} /> 145 | )} 146 | 147 | )} 148 | 149 | {/* List File View */} 150 | {viewStyle === ViewStyle.List && ( 151 | <> 152 | 153 | 154 | {table.getHeaderGroups().map(headerGroup => ( 155 | 156 | {headerGroup.headers.map(header => ( 157 | 163 | ))} 164 | 165 | ))} 166 | 167 | 168 | {table.getRowModel().rows.map(row => ( 169 | 170 | {row.getVisibleCells().map(cell => ( 171 | 174 | ))} 175 | 176 | ))} 177 | 178 |
158 |
159 | {flexRender(header.column.columnDef.header, header.getContext())} 160 | {header.column.getIsSorted() ? (header.column.getIsSorted() === 'desc' ? : ) : ''} 161 |
162 |
handleClick(row.original)} onDoubleClick={() => handleDoubleClick(row.original.id)}> 172 | {flexRender(cell.column.columnDef.cell, cell.getContext())} 173 |
179 | {!viewOnly && ( 180 | 181 | )} 182 | 183 | )} 184 | 185 | 186 | {!viewOnly && ( 187 | <> 188 | setNewFolderModalVisible(false)} 191 | /> 192 | 196 | 197 | )} 198 |
199 |
200 | ); 201 | }; 202 | 203 | export default Workspace; 204 | -------------------------------------------------------------------------------- /lib/components/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Navbar } from "./Navbar"; 2 | export { default as Workspace } from "./Workspace"; 3 | export { default as FolderPath } from "./FolderPath"; 4 | export { default as FileIcon } from "./FileIcon"; 5 | export { default as NewFolderIcon } from "./NewFolderIcon"; 6 | export { default as SvgIcon } from "./SvgIcon"; 7 | export { default as CommonModal } from "./CommonModal"; 8 | export { default as NewFolderModal } from "./NewFolderModal"; 9 | export { default as UploadFileModal } from "./UploadFileModal"; 10 | -------------------------------------------------------------------------------- /lib/context/FileManagerContext.tsx: -------------------------------------------------------------------------------- 1 | import type { Dispatch } from "react"; 2 | import { createContext, useContext } from "react"; 3 | import type { FileSystemType, ViewStyle } from "../types"; 4 | 5 | interface ProviderInterface { 6 | fs: FileSystemType; 7 | currentFolder: string; 8 | setCurrentFolder: (id: string) => void; 9 | viewOnly?: boolean; 10 | onDoubleClick?: (id: string) => Promise; 11 | onRefresh?: (id: string) => Promise; 12 | onUpload?: (fileData: any, folderId: string) => Promise; 13 | onCreateFolder?: (folderName: string) => Promise; 14 | onDelete?: (fileId: string) => Promise; 15 | uploadedFileData: any; 16 | setUploadedFileData: Dispatch; 17 | viewStyle: ViewStyle, 18 | setViewStyle: Dispatch 19 | } 20 | 21 | export const FileManagerContext = createContext(null); 22 | 23 | export const useFileManager = () => { 24 | const context = useContext(FileManagerContext); 25 | if (!context) { 26 | throw new Error("useFileManager must be used within FileManagerProvider"); 27 | } 28 | return context; 29 | }; 30 | -------------------------------------------------------------------------------- /lib/context/index.ts: -------------------------------------------------------------------------------- 1 | export { FileManagerContext, useFileManager } from "./FileManagerContext"; 2 | -------------------------------------------------------------------------------- /lib/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./ReactFileManager"; 2 | export * from "./types"; 3 | -------------------------------------------------------------------------------- /lib/tailwind.css: -------------------------------------------------------------------------------- 1 | /* 2 | React File Manager | MIT License | https://github.com/thelicato/react-file-manager 3 | */ 4 | 5 | @tailwind base; 6 | @tailwind components; 7 | @tailwind utilities; 8 | 9 | @layer utilities { 10 | /* CommonModal */ 11 | .rfm-modal-container { 12 | @apply absolute; 13 | @apply h-auto; 14 | @apply rounded-lg; 15 | @apply shadow-lg; 16 | @apply bg-white; 17 | @apply z-[999]; 18 | @apply cursor-grab; 19 | @apply px-16; 20 | @apply py-4; 21 | @apply border-[1px]; 22 | @apply border-gray-200; 23 | @apply text-center; 24 | @apply top-[10%]; 25 | @apply left-[40%]; 26 | } 27 | .rfm-modal-title { 28 | @apply text-xl; 29 | @apply font-semibold; 30 | @apply text-slate-900; 31 | } 32 | .rfm-modal-icon { 33 | @apply absolute; 34 | @apply right-4; 35 | @apply top-4; 36 | @apply w-6; 37 | @apply h-6; 38 | @apply fill-gray-400; 39 | @apply cursor-pointer; 40 | @apply transition-colors; 41 | @apply duration-200; 42 | @apply hover:fill-gray-600; 43 | } 44 | 45 | /* FileIcon */ 46 | .rfm-file-icon-container { 47 | @apply flex; 48 | @apply flex-col; 49 | @apply justify-center; 50 | @apply items-center; 51 | @apply overflow-hidden; 52 | @apply cursor-pointer; 53 | @apply transition-colors; 54 | @apply duration-300; 55 | @apply hover:bg-sky-100; 56 | @apply active:bg-sky-300; 57 | @apply p-2; 58 | @apply rounded-lg; 59 | @apply dark:hover:bg-gray-600; 60 | } 61 | .rfm-file-icon-svg { 62 | @apply w-24; 63 | @apply h-24; 64 | } 65 | .rfm-file-icon-extension { 66 | @apply absolute; 67 | @apply text-slate-100; 68 | @apply select-none; 69 | } 70 | .rfm-file-icon-name { 71 | @apply w-24; 72 | @apply text-center; 73 | @apply text-ellipsis; 74 | @apply overflow-hidden; 75 | @apply whitespace-nowrap; 76 | @apply select-none; 77 | } 78 | 79 | /* Content header */ 80 | .rfm-workspace-header { 81 | @apply w-full; 82 | @apply h-6; 83 | @apply flex; 84 | @apply gap-4; 85 | @apply justify-between; 86 | } 87 | .rfm-header-container { 88 | @apply flex justify-center items-center gap-2 rounded-lg bg-gray-200 dark:bg-gray-600; 89 | } 90 | .rfm-header-icon { 91 | @apply w-6 h-6 transition-all duration-200 cursor-pointer; 92 | } 93 | .rfm-header-icon--selected { 94 | @apply bg-slate-700 dark:bg-sky-500 text-white rounded-lg; 95 | } 96 | .rfm-workspace-list-align-txt { 97 | @apply text-left last:text-right pr-2; 98 | } 99 | .rfm-workspace-list-th { 100 | @apply rfm-workspace-list-align-txt last:justify-end last:flex; 101 | } 102 | .rfm-workspace-list-th-content { 103 | @apply flex; 104 | } 105 | .rfm-header-sort-icon { 106 | @apply w-6 h-6; 107 | } 108 | 109 | .rfm-workspace-list-icon-row { 110 | @apply cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-600 active:bg-gray-200 transition-all duration-200; 111 | } 112 | .rfm-workspace-list-icon-td { 113 | @apply flex gap-1 py-2; 114 | } 115 | .rfm-workspace-list-icon { 116 | @apply w-6 h-6; 117 | } 118 | .rfm-workspace-list-add-folder { 119 | @apply w-full p-2 text-slate-100 rounded-lg bg-sky-500 hover:bg-sky-700 transition-all duration-200 cursor-pointer; 120 | } 121 | .rfm-folder-path-container { 122 | @apply flex gap-4; 123 | } 124 | .rfm-folder-path-svg { 125 | @apply w-6; 126 | @apply h-6; 127 | @apply fill-white; 128 | @apply bg-slate-400; 129 | @apply p-1; 130 | @apply rounded-full; 131 | @apply cursor-pointer; 132 | @apply transition-colors; 133 | @apply duration-200; 134 | @apply hover:bg-slate-600; 135 | } 136 | .rfm-folder-path-span { 137 | @apply select-none; 138 | } 139 | 140 | /* Navbar */ 141 | .rfm-navbar { 142 | @apply bg-gray-200; 143 | @apply dark:bg-gray-900; 144 | @apply w-1/6; 145 | @apply py-4; 146 | @apply select-none; 147 | } 148 | .rfm-navbar-root-link { 149 | @apply uppercase; 150 | @apply text-lg; 151 | @apply text-gray-500; 152 | @apply dark:text-gray-300; 153 | @apply transition-colors; 154 | @apply duration-200; 155 | @apply hover:text-gray-700; 156 | @apply active:text-gray-900; 157 | @apply font-semibold; 158 | @apply p-4; 159 | @apply cursor-pointer; 160 | } 161 | .rfm-navbar-list { 162 | @apply mt-4; 163 | } 164 | .rfm-navbar-list-element { 165 | @apply text-gray-700; 166 | @apply dark:text-slate-100 dark:hover:bg-gray-700; 167 | @apply w-full; 168 | @apply transition-colors; 169 | @apply duration-200; 170 | @apply px-4; 171 | @apply py-2; 172 | @apply hover:bg-gray-300; 173 | @apply cursor-pointer; 174 | @apply active:bg-gray-400; 175 | } 176 | 177 | /* NewFolderIcon */ 178 | .rfm-folder-icon-container { 179 | @apply border-dashed; 180 | @apply border-[3px]; 181 | @apply border-slate-300; 182 | @apply w-24; 183 | @apply h-24; 184 | @apply p-2; 185 | @apply flex; 186 | @apply justify-center; 187 | @apply items-center; 188 | @apply self-center; 189 | @apply rounded-lg; 190 | @apply cursor-pointer; 191 | @apply mb-10; 192 | } 193 | .rfm-folder-icon-span { 194 | @apply text-slate-300; 195 | @apply text-3xl; 196 | @apply select-none; 197 | } 198 | 199 | /* NewFolderModal */ 200 | .rfm-new-folder-modal-form { 201 | @apply flex; 202 | @apply flex-col; 203 | @apply gap-6; 204 | @apply my-6; 205 | } 206 | .rfm-new-folder-modal-input { 207 | @apply bg-gray-50; 208 | @apply border; 209 | @apply border-gray-300; 210 | @apply text-gray-900; 211 | @apply text-sm; 212 | @apply rounded-lg; 213 | @apply focus:ring-gray-500; 214 | @apply focus:border-gray-500; 215 | @apply block; 216 | @apply w-full; 217 | @apply p-2; 218 | } 219 | .rfm-new-folder-modal-btn { 220 | @apply text-white; 221 | @apply bg-sky-500; 222 | @apply hover:bg-sky-700; 223 | @apply focus:ring-4; 224 | @apply focus:outline-none; 225 | @apply focus:ring-sky-600; 226 | @apply font-medium; 227 | @apply rounded-lg; 228 | @apply text-sm; 229 | @apply w-full; 230 | @apply sm:w-auto; 231 | @apply px-5; 232 | @apply py-2.5; 233 | @apply text-center; 234 | } 235 | 236 | /* UploadFileModal */ 237 | .rfm-upload-file-modal-title { 238 | @apply text-base; 239 | } 240 | .rfm-upload-file-modal-container { 241 | @apply flex; 242 | @apply flex-row; 243 | @apply gap-4; 244 | @apply justify-center; 245 | @apply items-center; 246 | @apply mt-4; 247 | } 248 | .rfm-upload-file-modal-btn { 249 | @apply text-white; 250 | @apply focus:ring-4; 251 | @apply focus:outline-none; 252 | @apply font-medium; 253 | @apply rounded-lg; 254 | @apply text-sm; 255 | @apply w-full; 256 | @apply sm:w-auto; 257 | @apply px-5; 258 | @apply py-2.5; 259 | @apply text-center; 260 | } 261 | .rfm-upload-file-modal-btn-confirm { 262 | @apply bg-emerald-500; 263 | @apply hover:bg-emerald-700; 264 | @apply focus:ring-emerald-600; 265 | } 266 | .rfm-upload-file-modal-btn-cancel { 267 | @apply bg-rose-500; 268 | @apply hover:bg-rose-700; 269 | @apply focus:ring-rose-600; 270 | } 271 | 272 | /* Workspace */ 273 | .rfm-workspace { 274 | @apply bg-white; 275 | @apply relative; 276 | @apply flex; 277 | @apply flex-col; 278 | @apply w-full; 279 | @apply gap-8; 280 | @apply p-4; 281 | @apply transition-colors; 282 | @apply duration-200; 283 | @apply dark:bg-gray-800 dark:text-slate-100; 284 | } 285 | .rfm-workspace-dropzone { 286 | @apply bg-sky-100; 287 | } 288 | .rfm-workspace-file-listing { 289 | @apply flex flex-wrap gap-4; 290 | } 291 | 292 | /* FileManager */ 293 | .rfm-main-container { 294 | @apply flex box-border h-full; 295 | } 296 | } 297 | -------------------------------------------------------------------------------- /lib/types/Enums.ts: -------------------------------------------------------------------------------- 1 | export enum ViewStyle { 2 | List = 'list', 3 | Icons = 'icons', 4 | } -------------------------------------------------------------------------------- /lib/types/Types.ts: -------------------------------------------------------------------------------- 1 | // Be careful: even a folder is a file! 2 | export type FileType = { 3 | id: string; // Unique ID given to this file 4 | name: string; 5 | isDir: boolean; 6 | path?: string; // Optional because files inherit the path from the parentId folder 7 | parentId?: string; // Optional because the root folder does not have a parent 8 | lastModified?: number 9 | }; 10 | 11 | export type FileSystemType = FileType[]; 12 | -------------------------------------------------------------------------------- /lib/types/index.ts: -------------------------------------------------------------------------------- 1 | export type { FileType, FileSystemType } from "./Types"; 2 | export { ViewStyle } from './Enums'; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@thelicato/react-file-manager", 3 | "description": "A simple React component to build a Web File Manager", 4 | "version": "1.1.5", 5 | "license": "MIT", 6 | "type": "module", 7 | "main": "dist/index.cjs", 8 | "module": "dist/index.js", 9 | "types": "dist/index.d.ts", 10 | "files": [ 11 | "dist" 12 | ], 13 | "scripts": { 14 | "dev": "vite", 15 | "build": "run-s build:*", 16 | "build:lib": "tsup lib/index.ts --dts", 17 | "build:css": "tailwindcss build -i lib/tailwind.css -o dist/style.css", 18 | "test": "vitest run", 19 | "lint": "eslint . --ext .ts,.tsx,.js,.jsx", 20 | "format": "prettier --write .", 21 | "storybook": "storybook dev -p 6006", 22 | "build-storybook": "storybook build" 23 | }, 24 | "dependencies": { 25 | "@tanstack/react-table": "^8.16.0", 26 | "react": "^18.2.0", 27 | "react-dom": "^18.2.0", 28 | "react-draggable": "^4.4.6", 29 | "react-dropzone": "^14.2.3" 30 | }, 31 | "devDependencies": { 32 | "@chromatic-com/storybook": "^1.3.2", 33 | "@frontside/eslint-config": "^3.1.0", 34 | "@storybook/addon-essentials": "^8.0.8", 35 | "@storybook/addon-interactions": "^8.0.8", 36 | "@storybook/addon-links": "^8.0.8", 37 | "@storybook/addon-mdx-gfm": "^8.0.8", 38 | "@storybook/addon-onboarding": "^8.0.8", 39 | "@storybook/blocks": "^8.0.8", 40 | "@storybook/react": "^8.0.8", 41 | "@storybook/react-vite": "^8.0.8", 42 | "@storybook/test": "^8.0.8", 43 | "@testing-library/react": "^15.0.2", 44 | "@types/inquirer": "^9.0.3", 45 | "@types/react": "^18.0.26", 46 | "@types/react-dom": "^18.0.9", 47 | "@types/recursive-readdir": "^2.2.1", 48 | "@typescript-eslint/eslint-plugin": "^5.51.0", 49 | "@vitejs/plugin-react": "^3.0.1", 50 | "autoprefixer": "^10.4.19", 51 | "chalk": "^5.2.0", 52 | "esbuild-css-modules-plugin": "^2.7.1", 53 | "eslint": "^8.33.0", 54 | "eslint-plugin-storybook": "^0.8.0", 55 | "fetch-lite": "^1.1.0", 56 | "inquirer": "^9.1.4", 57 | "kolorist": "^1.6.0", 58 | "lightningcss": "^1.24.1", 59 | "npm-run-all": "^4.1.5", 60 | "postcss": "^8.4.38", 61 | "prettier": "^2.8.4", 62 | "prop-types": "^15.8.1", 63 | "recursive-readdir": "^2.2.3", 64 | "rollup-plugin-postcss": "^4.0.2", 65 | "storybook": "^8.0.8", 66 | "tailwindcss": "^3.4.3", 67 | "ts-node": "^10.9.1", 68 | "tsup": "^6.5.0", 69 | "type-fest": "^3.5.4", 70 | "typescript": "^4.9.3", 71 | "vite": "^4.0.0", 72 | "vite-plugin-css-injected-by-js": "^2.4.0", 73 | "vite-plugin-dts": "^1.7.1", 74 | "vitest": "^0.28.4" 75 | }, 76 | "bugs": { 77 | "url": "https://github.com/thelicato/react-file-manager/issues" 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | #root { 2 | width: 60%; 3 | margin: auto; 4 | margin-top: 24px; 5 | display: flex; 6 | flex-direction: column; 7 | gap: 1em; 8 | justify-content: center; 9 | align-items: center; 10 | } 11 | 12 | .switch-mode-btn { 13 | color: white; 14 | padding: 0.5rem; 15 | background-color: rgb(14 165 233); 16 | transition-property: all; 17 | transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); 18 | transition-duration: 150ms; 19 | transition-duration: 200ms; 20 | border-radius: 0.5rem; 21 | } 22 | .switch-mode-btn:hover { 23 | background-color: rgb(3 105 161); 24 | } 25 | 26 | .container { 27 | width: 100%; 28 | height: calc(100vh - 100px); 29 | border: 2px solid #efefef; 30 | border-radius: 20px; 31 | overflow: hidden; 32 | } 33 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import type { FileSystemType } from "../lib"; 3 | import { ReactFileManager } from "../lib"; 4 | import "./App.css"; 5 | import "../lib/tailwind.css"; 6 | 7 | export const dummyFileSystem: FileSystemType = [ 8 | { id: "0", name: "/", path: "/", isDir: true }, 9 | { 10 | id: "31258", 11 | name: "report.pdf", 12 | isDir: false, 13 | parentId: "0", 14 | lastModified: 1677021347 15 | }, 16 | { 17 | id: "31259", 18 | name: "Documents", 19 | isDir: true, 20 | parentId: "0", 21 | path: "/Documents", 22 | lastModified: 1704720512 23 | }, 24 | { 25 | id: "31261", 26 | name: "Personal", 27 | isDir: true, 28 | parentId: "31259", 29 | path: "/Documents/Personal", 30 | lastModified: 1686630289 31 | }, 32 | { 33 | id: "31260", 34 | name: "report.docx", 35 | isDir: false, 36 | parentId: "0", 37 | lastModified: 1679647141 38 | }, 39 | { 40 | id: "31267", 41 | name: "Images", 42 | isDir: true, 43 | parentId: "0", 44 | path: "/Images", 45 | }, 46 | { 47 | id: "31260", 48 | name: "logo.png", 49 | isDir: false, 50 | parentId: "31267", 51 | }, 52 | ]; 53 | 54 | enum Theme { 55 | Dark = 'dark', 56 | Light = 'light', 57 | } 58 | 59 | const getInitialTheme = (): Theme => { 60 | if (typeof window !== 'undefined' && window.localStorage) { 61 | const storedPrefs = window.localStorage.getItem('color-theme'); 62 | 63 | if (typeof storedPrefs === 'string') { 64 | return storedPrefs as Theme; 65 | } 66 | 67 | const userMedia = window.matchMedia('(prefers-color-scheme: dark)'); 68 | if (userMedia.matches) { 69 | return Theme.Dark; 70 | } 71 | } 72 | 73 | return Theme.Light; // light theme as the default; 74 | }; 75 | 76 | 77 | function App() { 78 | const [theme, setTheme] = React.useState(getInitialTheme); 79 | 80 | const rawSetTheme = (rawTheme: Theme) => { 81 | const root = window.document.documentElement; 82 | const isDark = rawTheme === Theme.Dark; 83 | 84 | root.classList.remove(isDark ? 'light' : 'dark'); 85 | root.classList.add(rawTheme); 86 | 87 | localStorage.setItem('color-theme', rawTheme); 88 | }; 89 | 90 | const toggleTheme = () => { 91 | const _theme: Theme = theme === Theme.Dark ? Theme.Light : Theme.Dark; 92 | setTheme(_theme); 93 | }; 94 | 95 | useEffect(() => { 96 | rawSetTheme(theme); 97 | }, [theme]); 98 | 99 | 100 | return ( 101 | <> 102 | 103 |
104 | 105 |
106 | 107 | ); 108 | } 109 | 110 | export default App; 111 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import App from "./App"; 4 | 5 | ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( 6 | 7 | 8 | 9 | ); 10 | -------------------------------------------------------------------------------- /src/stories/RFM.stories.ts: -------------------------------------------------------------------------------- 1 | import { Meta } from "@storybook/react"; 2 | import type { StoryObj } from "@storybook/react"; 3 | import { ReactFileManager } from "../../lib"; 4 | // Dummy data 5 | import { dummyFileSystem } from "../App"; 6 | // Styles 7 | import "../../lib/tailwind.css"; 8 | 9 | // More on how to set up stories at: https://storybook.js.org/docs/7.0/react/writing-stories/introduction 10 | const meta = { 11 | title: "Example/ReactFileManager", 12 | component: ReactFileManager, 13 | } satisfies Meta; 14 | 15 | export default meta; 16 | type Story = StoryObj; 17 | 18 | // More on writing stories with args: https://storybook.js.org/docs/7.0/react/writing-stories/args 19 | export const Base: Story = { 20 | args: { 21 | fs: dummyFileSystem, 22 | }, 23 | }; 24 | 25 | export const ViewOnly: Story = { 26 | args: { 27 | fs: dummyFileSystem, 28 | viewOnly: true, 29 | }, 30 | }; 31 | 32 | export const DoubleClickAlert: Story = { 33 | args: { 34 | fs: dummyFileSystem, 35 | viewOnly: true, 36 | onDoubleClick: async (id: string) => { 37 | alert(`Hello ${id}!`) 38 | }, 39 | }, 40 | }; 41 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /tailwind.config.cjs: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: ["./lib/**/*.ts", "./lib/**/*.tsx"], 4 | darkMode: "class", 5 | theme: { 6 | extend: {}, 7 | }, 8 | plugins: [], 9 | }; 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noUncheckedIndexedAccess": true, 17 | "noEmit": true, 18 | "jsx": "react" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "tsup"; 2 | import cssModulesPlugin from "esbuild-css-modules-plugin"; 3 | 4 | export default defineConfig({ 5 | // @ts-expect-error - bug due to inconsistent esbuild versions 6 | esbuildPlugins: [cssModulesPlugin()], 7 | format: ["cjs", "esm"], 8 | injectStyle: true, 9 | }); 10 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react"; 3 | 4 | export default defineConfig({ 5 | plugins: [react()], 6 | }); 7 | --------------------------------------------------------------------------------