@grantbii/design-system 1.0.45 → 1.0.46
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/core/atoms/Badge.js +21 -2
- package/core/molecules/FileDrop.d.ts +16 -0
- package/core/molecules/FileDrop.js +121 -0
- package/core/molecules/index.d.ts +1 -0
- package/core/molecules/index.js +1 -0
- package/package.json +2 -1
- package/stories/molecules/FileDrop.stories.d.ts +6 -0
- package/stories/molecules/FileDrop.stories.js +22 -0
- package/tsconfig.tsbuildinfo +1 -1
package/core/atoms/Badge.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import styled from "styled-components";
|
|
3
3
|
import { Colors, Icons } from "../foundations";
|
|
4
|
-
const Badge = ({ text, Icon, iconSize = 20, iconWeight = "regular", onClickClose, textWidthPixels, backgroundColor, color, }) => (_jsxs(BaseBadge, { "$backgroundColor": backgroundColor, "$color": color, children: [Icon ? _jsx(Icon, { color: color, size: iconSize, weight: iconWeight }) : _jsx(_Fragment, {}), _jsx(BadgeText, {
|
|
4
|
+
const Badge = ({ text, Icon, iconSize = 20, iconWeight = "regular", onClickClose, textWidthPixels, backgroundColor, color, }) => (_jsxs(BaseBadge, { "$backgroundColor": backgroundColor, "$color": color, children: [_jsxs(BadgeContent, { "$isCloseable": !!onClickClose, "$widthPixels": textWidthPixels, children: [Icon ? (_jsx(IconContainer, { children: _jsx(Icon, { color: color, size: iconSize, weight: iconWeight }) })) : (_jsx(_Fragment, {})), _jsx(BadgeText, { children: text })] }), onClickClose ? (_jsx(Button, { type: "button", onClick: onClickClose, children: _jsx(Icons.XIcon, { size: 12 }) })) : (_jsx(_Fragment, {}))] }));
|
|
5
5
|
export default Badge;
|
|
6
6
|
const BaseBadge = styled.div `
|
|
7
7
|
display: flex;
|
|
8
8
|
align-items: center;
|
|
9
|
+
justify-content: space-between;
|
|
9
10
|
gap: 10px;
|
|
10
11
|
|
|
11
12
|
padding: 5px 16px;
|
|
@@ -14,8 +15,23 @@ const BaseBadge = styled.div `
|
|
|
14
15
|
color: ${({ $color = Colors.typography.blackHigh }) => $color};
|
|
15
16
|
background-color: ${({ $backgroundColor = Colors.neutral.grey3 }) => $backgroundColor};
|
|
16
17
|
`;
|
|
17
|
-
const
|
|
18
|
+
const BadgeContent = styled.div `
|
|
19
|
+
display: flex;
|
|
20
|
+
align-items: center;
|
|
21
|
+
gap: 10px;
|
|
22
|
+
|
|
18
23
|
width: ${({ $widthPixels }) => ($widthPixels ? `${$widthPixels}px` : "auto")};
|
|
24
|
+
max-width: ${({ $isCloseable }) => $isCloseable ? "calc(100% - 20px)" : "auto"};
|
|
25
|
+
`;
|
|
26
|
+
const IconContainer = styled.div `
|
|
27
|
+
display: flex;
|
|
28
|
+
flex-direction: column;
|
|
29
|
+
|
|
30
|
+
width: ${({ $iconSize = "auto" }) => $iconSize};
|
|
31
|
+
min-width: ${({ $iconSize = "auto" }) => $iconSize};
|
|
32
|
+
max-width: ${({ $iconSize = "auto" }) => $iconSize};
|
|
33
|
+
`;
|
|
34
|
+
const BadgeText = styled.p `
|
|
19
35
|
overflow-x: hidden;
|
|
20
36
|
white-space: nowrap;
|
|
21
37
|
text-overflow: ellipsis;
|
|
@@ -25,4 +41,7 @@ const BadgeText = styled.p `
|
|
|
25
41
|
`;
|
|
26
42
|
const Button = styled.button `
|
|
27
43
|
display: flex;
|
|
44
|
+
flex-direction: column;
|
|
45
|
+
|
|
46
|
+
min-width: 12px;
|
|
28
47
|
`;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
type FileDropzoneProps = {
|
|
2
|
+
uploadedFiles: File[];
|
|
3
|
+
uploadFiles: (acceptedFiles: File[]) => void;
|
|
4
|
+
removeFile: (fileName: string) => void;
|
|
5
|
+
errorMessage?: string;
|
|
6
|
+
maxFiles?: number;
|
|
7
|
+
maxSizeMB?: number;
|
|
8
|
+
};
|
|
9
|
+
declare const FileDrop: ({ uploadedFiles, uploadFiles, removeFile, errorMessage, maxFiles, maxSizeMB, }: FileDropzoneProps) => import("react/jsx-runtime").JSX.Element;
|
|
10
|
+
export default FileDrop;
|
|
11
|
+
export declare const useFileDrop: (initialFiles?: File[], maxFiles?: number, maxSizeMB?: number) => {
|
|
12
|
+
files: File[];
|
|
13
|
+
uploadFiles: (acceptedFiles: File[]) => void;
|
|
14
|
+
removeFile: (fileName: string) => void;
|
|
15
|
+
errorMessage: string | undefined;
|
|
16
|
+
};
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { useDropzone } from "react-dropzone";
|
|
4
|
+
import styled from "styled-components";
|
|
5
|
+
import { Badge } from "../atoms";
|
|
6
|
+
import { Colors, Icons } from "../foundations";
|
|
7
|
+
const DEFAULT_MAX_FILE_SIZE_MB = 5;
|
|
8
|
+
const DEFAULT_MAX_FILES = 5;
|
|
9
|
+
const FileDrop = ({ uploadedFiles, uploadFiles, removeFile, errorMessage, maxFiles = DEFAULT_MAX_FILES, maxSizeMB = DEFAULT_MAX_FILE_SIZE_MB, }) => {
|
|
10
|
+
const reachedMaxUploads = uploadedFiles.length >= maxFiles;
|
|
11
|
+
const { getInputProps, getRootProps } = useDropzone({
|
|
12
|
+
onDrop: uploadFiles,
|
|
13
|
+
accept: { "application/pdf": [".pdf"] },
|
|
14
|
+
disabled: reachedMaxUploads,
|
|
15
|
+
noClick: reachedMaxUploads,
|
|
16
|
+
noDrag: reachedMaxUploads,
|
|
17
|
+
multiple: true,
|
|
18
|
+
});
|
|
19
|
+
return (_jsxs(BaseFileDrop, { children: [_jsxs(Dropzone, Object.assign({}, getRootProps(), { "$reachedMaxUploads": reachedMaxUploads, "$hasError": !!errorMessage, children: [_jsx("input", Object.assign({}, getInputProps(), { type: "file" })), _jsx(DropzoneContent, { maxFiles: maxFiles, maxSizeMB: maxSizeMB })] })), errorMessage ? _jsx(ErrorMessage, { children: errorMessage }) : _jsx(_Fragment, {}), _jsx(UploadedFiles, { uploadedFiles: uploadedFiles, removeFile: removeFile })] }));
|
|
20
|
+
};
|
|
21
|
+
export default FileDrop;
|
|
22
|
+
const BaseFileDrop = styled.div `
|
|
23
|
+
width: 100%;
|
|
24
|
+
display: flex;
|
|
25
|
+
flex-direction: column;
|
|
26
|
+
gap: 6px;
|
|
27
|
+
`;
|
|
28
|
+
const Dropzone = styled.div `
|
|
29
|
+
padding: 40px;
|
|
30
|
+
border-radius: 6px;
|
|
31
|
+
border: 1px solid
|
|
32
|
+
${({ $hasError }) => $hasError ? Colors.accent.red1 : Colors.neutral.grey3};
|
|
33
|
+
|
|
34
|
+
&:hover {
|
|
35
|
+
cursor: ${({ $reachedMaxUploads }) => $reachedMaxUploads ? "not-allowed" : "pointer"};
|
|
36
|
+
}
|
|
37
|
+
`;
|
|
38
|
+
const DropzoneContent = ({ maxFiles, maxSizeMB }) => (_jsxs(BaseDropzoneContent, { children: [_jsx(Icons.FileDashedIcon, { weight: "thin", size: 48, color: Colors.neutral.grey1 }), _jsxs(DropzoneText, { children: [_jsx(DropzoneTitle, { children: `Drop up to ${maxFiles} files here ( ${maxSizeMB}MB each)` }), _jsx(DropzoneHighlightedSubtitle, { children: DROPZONE_BROWSE_TEXT }), _jsx(DropzoneSubtitle, { children: FILE_FORMAT_TEXT })] })] }));
|
|
39
|
+
const DROPZONE_BROWSE_TEXT = "or click to browse with your file explorer";
|
|
40
|
+
const FILE_FORMAT_TEXT = "Accepted file formats: .pdf (Coming soon: .doc, .ppt, .csv)";
|
|
41
|
+
const BaseDropzoneContent = styled.div `
|
|
42
|
+
display: flex;
|
|
43
|
+
flex-direction: column;
|
|
44
|
+
gap: 24px;
|
|
45
|
+
align-items: center;
|
|
46
|
+
`;
|
|
47
|
+
const DropzoneText = styled.div `
|
|
48
|
+
display: flex;
|
|
49
|
+
flex-direction: column;
|
|
50
|
+
gap: 4px;
|
|
51
|
+
`;
|
|
52
|
+
const DropzoneTitle = styled.h3 `
|
|
53
|
+
font-weight: 500;
|
|
54
|
+
font-size: 14px;
|
|
55
|
+
text-align: center;
|
|
56
|
+
`;
|
|
57
|
+
const DropzoneHighlightedSubtitle = styled.p `
|
|
58
|
+
font-weight: 400;
|
|
59
|
+
font-size: 12px;
|
|
60
|
+
text-align: center;
|
|
61
|
+
color: ${Colors.accent.yellow1};
|
|
62
|
+
`;
|
|
63
|
+
const DropzoneSubtitle = styled.p `
|
|
64
|
+
font-weight: 400;
|
|
65
|
+
font-size: 12px;
|
|
66
|
+
text-align: center;
|
|
67
|
+
color: ${Colors.typography.blackLow};
|
|
68
|
+
`;
|
|
69
|
+
const ErrorMessage = styled.p `
|
|
70
|
+
color: ${Colors.accent.red1};
|
|
71
|
+
`;
|
|
72
|
+
const UploadedFiles = ({ uploadedFiles, removeFile }) => (_jsx(BaseUploadedFiles, { children: uploadedFiles.map(({ name: fileName, type: fileType }) => {
|
|
73
|
+
var _a;
|
|
74
|
+
return (_jsx(Badge, { text: getFileNameWithoutExtension(fileName), onClickClose: () => removeFile(fileName), Icon: (_a = FILE_TYPE_ICON_MAP[fileType]) !== null && _a !== void 0 ? _a : Icons.FileIcon }, fileName));
|
|
75
|
+
}) }));
|
|
76
|
+
const BaseUploadedFiles = styled.div `
|
|
77
|
+
display: flex;
|
|
78
|
+
flex-direction: column;
|
|
79
|
+
gap: 4px;
|
|
80
|
+
`;
|
|
81
|
+
const FILE_TYPE_ICON_MAP = {
|
|
82
|
+
"application/pdf": Icons.FilePdfIcon,
|
|
83
|
+
};
|
|
84
|
+
export const useFileDrop = (initialFiles = [], maxFiles = DEFAULT_MAX_FILES, maxSizeMB = DEFAULT_MAX_FILE_SIZE_MB) => {
|
|
85
|
+
const [files, setFiles] = useState(() => initialFiles);
|
|
86
|
+
const [errorMessage, setErrorMessage] = useState();
|
|
87
|
+
const uploadFiles = (acceptedFiles) => {
|
|
88
|
+
if (files.length + acceptedFiles.length > maxFiles) {
|
|
89
|
+
setErrorMessage(() => `Maximum upload limit is ${maxFiles} files`);
|
|
90
|
+
}
|
|
91
|
+
else if (anyFileTooLarge(acceptedFiles, maxSizeMB)) {
|
|
92
|
+
setErrorMessage(() => `Maximum file size is ${maxSizeMB}MB`);
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
setErrorMessage(() => undefined);
|
|
96
|
+
setFiles((previousFiles) => combineFilesWithoutDuplicates(previousFiles, acceptedFiles));
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
const removeFile = (fileName) => {
|
|
100
|
+
setErrorMessage(() => undefined);
|
|
101
|
+
setFiles((previousFiles) => filterFilesByName(previousFiles, fileName));
|
|
102
|
+
};
|
|
103
|
+
return {
|
|
104
|
+
files,
|
|
105
|
+
uploadFiles,
|
|
106
|
+
removeFile,
|
|
107
|
+
errorMessage,
|
|
108
|
+
};
|
|
109
|
+
};
|
|
110
|
+
const anyFileTooLarge = (files, maxSizeMB) => files.some((file) => {
|
|
111
|
+
console.log("file size:", file.size);
|
|
112
|
+
return file.size > convertMegabytesToBytes(maxSizeMB);
|
|
113
|
+
});
|
|
114
|
+
const convertMegabytesToBytes = (megabytes) => megabytes * 1024 * 1024;
|
|
115
|
+
const combineFilesWithoutDuplicates = (oldFiles, newFiles) => {
|
|
116
|
+
const newFileNames = newFiles.map((file) => file.name);
|
|
117
|
+
const keptOldFiles = oldFiles.filter((oldFile) => !newFileNames.includes(oldFile.name));
|
|
118
|
+
return [...keptOldFiles, ...newFiles];
|
|
119
|
+
};
|
|
120
|
+
const filterFilesByName = (files, fileName) => files.filter((file) => file.name !== fileName);
|
|
121
|
+
const getFileNameWithoutExtension = (fileName) => fileName.substring(0, fileName.lastIndexOf("."));
|
package/core/molecules/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@grantbii/design-system",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.46",
|
|
4
4
|
"description": "Grantbii's Design System",
|
|
5
5
|
"homepage": "https://design.grantbii.com",
|
|
6
6
|
"repository": {
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
"next": "^15.3.5",
|
|
24
24
|
"react": "^19.1.0",
|
|
25
25
|
"react-dom": "^19.1.0",
|
|
26
|
+
"react-dropzone": "^14.3.8",
|
|
26
27
|
"styled-components": "^6.1.19"
|
|
27
28
|
},
|
|
28
29
|
"devDependencies": {
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { Meta, StoryObj } from "@storybook/nextjs-vite";
|
|
2
|
+
declare const FileDropExample: () => import("react/jsx-runtime").JSX.Element;
|
|
3
|
+
declare const meta: Meta<typeof FileDropExample>;
|
|
4
|
+
export default meta;
|
|
5
|
+
type Story = StoryObj<typeof meta>;
|
|
6
|
+
export declare const Example: Story;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { FileDrop, useFileDrop } from "@/.";
|
|
3
|
+
import styled from "styled-components";
|
|
4
|
+
const FileDropExample = () => {
|
|
5
|
+
const { files, uploadFiles, removeFile, errorMessage } = useFileDrop();
|
|
6
|
+
return (_jsx(Container, { children: _jsx(FileDrop, { uploadedFiles: files, uploadFiles: uploadFiles, removeFile: removeFile, errorMessage: errorMessage }) }));
|
|
7
|
+
};
|
|
8
|
+
const Container = styled.div `
|
|
9
|
+
width: 400px;
|
|
10
|
+
`;
|
|
11
|
+
const meta = {
|
|
12
|
+
title: "Molecules/File Drop",
|
|
13
|
+
component: FileDropExample,
|
|
14
|
+
tags: ["autodocs"],
|
|
15
|
+
parameters: {
|
|
16
|
+
layout: "centered",
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
export default meta;
|
|
20
|
+
export const Example = {
|
|
21
|
+
args: {},
|
|
22
|
+
};
|