@grantbii/design-system 1.0.44 → 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.
@@ -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, { "$widthPixels": textWidthPixels, children: text }), onClickClose ? (_jsx(Button, { type: "button", onClick: onClickClose, children: _jsx(Icons.XIcon, { size: 12 }) })) : (_jsx(_Fragment, {}))] }));
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 BadgeText = styled.p `
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("."));
@@ -2,7 +2,7 @@ import { DetailedHTMLProps, InputHTMLAttributes } from "react";
2
2
  import { Option } from "../foundations";
3
3
  export type RadioOption = Option & DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>;
4
4
  type RadioButtonProps = {
5
- name: string;
5
+ name?: string;
6
6
  options: RadioOption[];
7
7
  };
8
8
  declare const RadioButtons: ({ name, options }: RadioButtonProps) => import("react/jsx-runtime").JSX.Element;
@@ -1,2 +1,3 @@
1
+ export { default as FileDrop, useFileDrop } from "./FileDrop";
1
2
  export { default as Modal, useModal } from "./Modal";
2
3
  export { default as RadioButtons, type RadioOption } from "./RadioButtons";
@@ -1,2 +1,3 @@
1
+ export { default as FileDrop, useFileDrop } from "./FileDrop";
1
2
  export { default as Modal, useModal } from "./Modal";
2
3
  export { default as RadioButtons } from "./RadioButtons";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@grantbii/design-system",
3
- "version": "1.0.44",
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
+ };
@@ -1,7 +1,10 @@
1
- import { RadioButtons } from "@/core/molecules";
2
1
  import { Meta, StoryObj } from "@storybook/nextjs-vite";
3
- declare const meta: Meta<typeof RadioButtons>;
2
+ type ExampleProps = {
3
+ controlled: boolean;
4
+ };
5
+ declare const RadioButtonsExample: ({ controlled }: ExampleProps) => import("react/jsx-runtime").JSX.Element;
6
+ declare const meta: Meta<typeof RadioButtonsExample>;
4
7
  export default meta;
5
8
  type Story = StoryObj<typeof meta>;
6
- export declare const NoneSelectedByDefault: Story;
7
- export declare const OneSelectedByDefault: Story;
9
+ export declare const Uncontrolled: Story;
10
+ export declare const Controlled: Story;
@@ -1,45 +1,59 @@
1
- import { RadioButtons } from "@/core/molecules";
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { RadioButtons } from "@/.";
3
+ import { useState } from "react";
4
+ const SINGAPORE = "Singapore";
5
+ const HONG_KONG = "Hong Kong";
6
+ const RadioButtonsExample = ({ controlled }) => {
7
+ const [location, setLocation] = useState("");
8
+ const controlledProps = {
9
+ options: [
10
+ {
11
+ label: SINGAPORE,
12
+ value: SINGAPORE,
13
+ checked: location === SINGAPORE,
14
+ onClick: () => setLocation(SINGAPORE),
15
+ },
16
+ {
17
+ label: HONG_KONG,
18
+ value: HONG_KONG,
19
+ checked: location === HONG_KONG,
20
+ onClick: () => setLocation(HONG_KONG),
21
+ },
22
+ ],
23
+ };
24
+ return (_jsx(RadioButtons, Object.assign({}, (controlled ? controlledProps : uncontrolledProps))));
25
+ };
26
+ const uncontrolledProps = {
27
+ name: "location",
28
+ options: [
29
+ {
30
+ label: SINGAPORE,
31
+ value: SINGAPORE,
32
+ onChange: () => alert(`Selected ${SINGAPORE}!`),
33
+ },
34
+ {
35
+ label: HONG_KONG,
36
+ value: HONG_KONG,
37
+ onChange: () => alert(`Selected ${HONG_KONG}!`),
38
+ },
39
+ ],
40
+ };
2
41
  const meta = {
3
42
  title: "Molecules/Radio Buttons",
4
- component: RadioButtons,
43
+ component: RadioButtonsExample,
5
44
  tags: ["autodocs"],
6
45
  parameters: {
7
46
  layout: "centered",
8
47
  },
9
48
  };
10
49
  export default meta;
11
- export const NoneSelectedByDefault = {
50
+ export const Uncontrolled = {
12
51
  args: {
13
- name: "location ",
14
- options: [
15
- {
16
- label: "Singapore",
17
- value: "Singapore",
18
- onChange: () => alert("Selected Singapore!"),
19
- },
20
- {
21
- label: "Hong Kong",
22
- value: "Hong Kong",
23
- onChange: () => alert("Selected Hong Kong!"),
24
- },
25
- ],
52
+ controlled: false,
26
53
  },
27
54
  };
28
- export const OneSelectedByDefault = {
55
+ export const Controlled = {
29
56
  args: {
30
- name: "location ",
31
- options: [
32
- {
33
- label: "Singapore",
34
- value: "Singapore",
35
- onChange: () => alert("Selected Singapore!"),
36
- defaultChecked: true,
37
- },
38
- {
39
- label: "Hong Kong",
40
- value: "Hong Kong",
41
- onChange: () => alert("Selected Hong Kong!"),
42
- },
43
- ],
57
+ controlled: true,
44
58
  },
45
59
  };