├── .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 |
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 |
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 |
24 | );
25 | }
26 | case "folder": {
27 | return (
28 |
43 | );
44 | }
45 | case "arrow-up": {
46 | return (
47 |
50 | );
51 | }
52 | case "arrow-down": {
53 | return (
54 |
58 | )
59 | }
60 | case "close": {
61 | return (
62 |
65 | );
66 | }
67 | case "list": {
68 | return (
69 |
73 | )
74 | }
75 | case "icons": {
76 | return (
77 |
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 |
158 |
159 | {flexRender(header.column.columnDef.header, header.getContext())}
160 | {header.column.getIsSorted() ? (header.column.getIsSorted() === 'desc' ? : ) : ''}
161 |
162 | |
163 | ))}
164 |
165 | ))}
166 |
167 |
168 | {table.getRowModel().rows.map(row => (
169 |
170 | {row.getVisibleCells().map(cell => (
171 | handleClick(row.original)} onDoubleClick={() => handleDoubleClick(row.original.id)}>
172 | {flexRender(cell.column.columnDef.cell, cell.getContext())}
173 | |
174 | ))}
175 |
176 | ))}
177 |
178 |
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 |
--------------------------------------------------------------------------------