dst-rg 1.0.0

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.
Files changed (249) hide show
  1. package/.gitlab-ci.yml +43 -0
  2. package/.storybook/main.ts +15 -0
  3. package/.storybook/preview.ts +15 -0
  4. package/README.md +254 -0
  5. package/components.json +21 -0
  6. package/dist/Avatar.png +0 -0
  7. package/dist/assets/index-CCq7hmG3.js +186 -0
  8. package/dist/assets/index-Mg-hjQGu.css +1 -0
  9. package/dist/index.html +15 -0
  10. package/dist/test.png +0 -0
  11. package/dist/vite.svg +1 -0
  12. package/eslint.config.js +29 -0
  13. package/index.html +14 -0
  14. package/package.json +102 -0
  15. package/postcss.config.mjs +11 -0
  16. package/rollup.config.mjs +55 -0
  17. package/src/assets/react.svg +1 -0
  18. package/src/assets/style/animation.css +27 -0
  19. package/src/assets/style/box-shadow.css +25 -0
  20. package/src/assets/style/colors.css +402 -0
  21. package/src/assets/style/dark-theme.css +288 -0
  22. package/src/assets/style/font-size.css +14 -0
  23. package/src/assets/style/gradient.css +3 -0
  24. package/src/assets/style/index.css +12 -0
  25. package/src/assets/style/light-theme.css +148 -0
  26. package/src/assets/style/line-height.css +13 -0
  27. package/src/assets/style/max-width.css +5 -0
  28. package/src/assets/style/radius.css +13 -0
  29. package/src/assets/style/utility-colors.css +166 -0
  30. package/src/components/Accordion/_.stories.tsx +75 -0
  31. package/src/components/Accordion/_.test.tsx +77 -0
  32. package/src/components/Accordion/index.tsx +47 -0
  33. package/src/components/Accordion/type.ts +24 -0
  34. package/src/components/Avatar/_.stories.tsx +179 -0
  35. package/src/components/Avatar/_.style.ts +40 -0
  36. package/src/components/Avatar/_.test.tsx +150 -0
  37. package/src/components/Avatar/_.types.ts +66 -0
  38. package/src/components/Avatar/index.tsx +63 -0
  39. package/src/components/Badge/_.stories.tsx +75 -0
  40. package/src/components/Badge/_.style.ts +53 -0
  41. package/src/components/Badge/_.test.tsx +27 -0
  42. package/src/components/Badge/_.types.ts +11 -0
  43. package/src/components/Badge/index.tsx +42 -0
  44. package/src/components/Breadcrumbs/_.stories.tsx +95 -0
  45. package/src/components/Breadcrumbs/_.test.tsx +29 -0
  46. package/src/components/Breadcrumbs/_.type.ts +15 -0
  47. package/src/components/Breadcrumbs/index.tsx +103 -0
  48. package/src/components/Button/_.stories.tsx +85 -0
  49. package/src/components/Button/_.style.ts +56 -0
  50. package/src/components/Button/_.test.tsx +103 -0
  51. package/src/components/Button/_.types.ts +14 -0
  52. package/src/components/Button/index.tsx +70 -0
  53. package/src/components/Checkbox/_.stories.tsx +96 -0
  54. package/src/components/Checkbox/_.style.ts +24 -0
  55. package/src/components/Checkbox/_.test.tsx +85 -0
  56. package/src/components/Checkbox/_.types.ts +23 -0
  57. package/src/components/Checkbox/index.tsx +93 -0
  58. package/src/components/CheckboxGroup/PaymentCard/_.stories.tsx +104 -0
  59. package/src/components/CheckboxGroup/PaymentCard/_.style.ts +28 -0
  60. package/src/components/CheckboxGroup/PaymentCard/_.test.tsx +58 -0
  61. package/src/components/CheckboxGroup/PaymentCard/_.types.ts +28 -0
  62. package/src/components/CheckboxGroup/PaymentCard/index.tsx +71 -0
  63. package/src/components/CheckboxGroup/PlanCard/_.stories.tsx +165 -0
  64. package/src/components/CheckboxGroup/PlanCard/_.style.ts +32 -0
  65. package/src/components/CheckboxGroup/PlanCard/_.test.tsx +54 -0
  66. package/src/components/CheckboxGroup/PlanCard/_.types.ts +35 -0
  67. package/src/components/CheckboxGroup/PlanCard/index.tsx +53 -0
  68. package/src/components/CheckboxGroup/UserCard/_.stories.tsx +89 -0
  69. package/src/components/CheckboxGroup/UserCard/_.style.ts +42 -0
  70. package/src/components/CheckboxGroup/UserCard/_.test.tsx +66 -0
  71. package/src/components/CheckboxGroup/UserCard/_.types.ts +26 -0
  72. package/src/components/CheckboxGroup/UserCard/index.tsx +75 -0
  73. package/src/components/Dropdown/_.stories.tsx +180 -0
  74. package/src/components/Dropdown/_.style.ts +108 -0
  75. package/src/components/Dropdown/_.test.tsx +334 -0
  76. package/src/components/Dropdown/_.types.ts +12 -0
  77. package/src/components/Dropdown/index.tsx +130 -0
  78. package/src/components/FileUpload/_.stories.tsx +74 -0
  79. package/src/components/FileUpload/_.style.ts +0 -0
  80. package/src/components/FileUpload/_.test.tsx +222 -0
  81. package/src/components/FileUpload/_.types.ts +53 -0
  82. package/src/components/FileUpload/index.tsx +44 -0
  83. package/src/components/ImageMagnify/_.stories.tsx +226 -0
  84. package/src/components/ImageMagnify/_.style.ts +109 -0
  85. package/src/components/ImageMagnify/_.types.ts +44 -0
  86. package/src/components/ImageMagnify/index.tsx +204 -0
  87. package/src/components/Input/_.stories.tsx +177 -0
  88. package/src/components/Input/_.style.ts +79 -0
  89. package/src/components/Input/_.test.tsx +146 -0
  90. package/src/components/Input/_.types.ts +66 -0
  91. package/src/components/Input/index.tsx +231 -0
  92. package/src/components/InputTags/_.stories.tsx +51 -0
  93. package/src/components/InputTags/_.style.ts +28 -0
  94. package/src/components/InputTags/_.test.tsx +123 -0
  95. package/src/components/InputTags/_.types.ts +26 -0
  96. package/src/components/InputTags/index.tsx +140 -0
  97. package/src/components/Message/_.stories.tsx +79 -0
  98. package/src/components/Message/_.style.ts +87 -0
  99. package/src/components/Message/_.test.tsx +73 -0
  100. package/src/components/Message/_.types.ts +13 -0
  101. package/src/components/Message/index.tsx +57 -0
  102. package/src/components/Metric/_.stories.tsx +142 -0
  103. package/src/components/Metric/_.style.ts +14 -0
  104. package/src/components/Metric/_.test.tsx +166 -0
  105. package/src/components/Metric/_.types.ts +18 -0
  106. package/src/components/Metric/index.tsx +100 -0
  107. package/src/components/Modal/_.stories.tsx +93 -0
  108. package/src/components/Modal/_.style.ts +31 -0
  109. package/src/components/Modal/_.test.tsx +90 -0
  110. package/src/components/Modal/_.types.ts +14 -0
  111. package/src/components/Modal/index.tsx +82 -0
  112. package/src/components/Pagination/_.stories.tsx +118 -0
  113. package/src/components/Pagination/_.test.tsx +51 -0
  114. package/src/components/Pagination/index.tsx +256 -0
  115. package/src/components/Pagination/type.ts +48 -0
  116. package/src/components/PriceSlider/_.stories.tsx +107 -0
  117. package/src/components/PriceSlider/_.test.tsx +63 -0
  118. package/src/components/PriceSlider/_.type.tsx +19 -0
  119. package/src/components/PriceSlider/index.tsx +86 -0
  120. package/src/components/Progress/_.stories.tsx +93 -0
  121. package/src/components/Progress/_.style.ts +15 -0
  122. package/src/components/Progress/_.test.tsx +34 -0
  123. package/src/components/Progress/_.types.ts +17 -0
  124. package/src/components/Progress/index.tsx +173 -0
  125. package/src/components/Radio/_.stories.tsx +391 -0
  126. package/src/components/Radio/_.style.ts +33 -0
  127. package/src/components/Radio/_.test.tsx +77 -0
  128. package/src/components/Radio/_.types.ts +14 -0
  129. package/src/components/Radio/index.tsx +59 -0
  130. package/src/components/Select/_.stories.tsx +308 -0
  131. package/src/components/Select/_.style.ts +5 -0
  132. package/src/components/Select/_.types.ts +24 -0
  133. package/src/components/Select/index.tsx +172 -0
  134. package/src/components/Switch/_.stories.tsx +61 -0
  135. package/src/components/Switch/_.test.tsx +69 -0
  136. package/src/components/Switch/_.type.ts +12 -0
  137. package/src/components/Switch/index.tsx +70 -0
  138. package/src/components/Tabs/_.stories.tsx +508 -0
  139. package/src/components/Tabs/_.style.ts +63 -0
  140. package/src/components/Tabs/_.test.tsx +174 -0
  141. package/src/components/Tabs/_.type.ts +19 -0
  142. package/src/components/Tabs/index.tsx +35 -0
  143. package/src/components/Tag/_.stories.tsx +78 -0
  144. package/src/components/Tag/_.style.ts +71 -0
  145. package/src/components/Tag/_.test.tsx +44 -0
  146. package/src/components/Tag/_.types.ts +27 -0
  147. package/src/components/Tag/index.tsx +46 -0
  148. package/src/components/TextArea/_.stories.tsx +62 -0
  149. package/src/components/TextArea/_.style.ts +11 -0
  150. package/src/components/TextArea/_.test.tsx +43 -0
  151. package/src/components/TextArea/_.types.ts +29 -0
  152. package/src/components/TextArea/index.tsx +83 -0
  153. package/src/components/Toast/_.style.tsx +27 -0
  154. package/src/components/Toast/_.type.ts +30 -0
  155. package/src/components/Toast/_.utils.ts +23 -0
  156. package/src/components/Toast/container.tsx +171 -0
  157. package/src/components/Toast/index.tsx +29 -0
  158. package/src/components/Tooltip/_.stories.tsx +106 -0
  159. package/src/components/Tooltip/_.style.ts +27 -0
  160. package/src/components/Tooltip/_.test.tsx +54 -0
  161. package/src/components/Tooltip/_.types.ts +31 -0
  162. package/src/components/Tooltip/index.tsx +80 -0
  163. package/src/components/developers/AmirHossein.tsx +149 -0
  164. package/src/components/developers/Fardin.tsx +130 -0
  165. package/src/components/developers/Maryam.tsx +260 -0
  166. package/src/components/developers/Milad.tsx +431 -0
  167. package/src/components/developers/Rasoul.tsx +198 -0
  168. package/src/components/index.ts +28 -0
  169. package/src/components/ui/accordion.tsx +162 -0
  170. package/src/components/ui/avatars-component/avatar-description.tsx +30 -0
  171. package/src/components/ui/avatars-component/avatar-groups.tsx +68 -0
  172. package/src/components/ui/avatars-component/avatar-single.tsx +50 -0
  173. package/src/components/ui/card.tsx +92 -0
  174. package/src/components/ui/checkbox-group/plan-card/basic/_.test.tsx +66 -0
  175. package/src/components/ui/checkbox-group/plan-card/basic/index.tsx +70 -0
  176. package/src/components/ui/checkbox-group/plan-card/with-header/_.test.tsx +110 -0
  177. package/src/components/ui/checkbox-group/plan-card/with-header/header.test.tsx +96 -0
  178. package/src/components/ui/checkbox-group/plan-card/with-header/header.tsx +74 -0
  179. package/src/components/ui/checkbox-group/plan-card/with-header/index.tsx +65 -0
  180. package/src/components/ui/file-content/File-content.tsx +43 -0
  181. package/src/components/ui/file-uploader-components/file-uploader-box.tsx +76 -0
  182. package/src/components/ui/file-uploader-components/file-uploader-item.tsx +64 -0
  183. package/src/components/ui/icon-wrapper/_.test.tsx +60 -0
  184. package/src/components/ui/icon-wrapper/index.tsx +19 -0
  185. package/src/components/ui/input-component/input-label.tsx +11 -0
  186. package/src/components/ui/number.tsx +18 -0
  187. package/src/components/ui/pagination/card-minimal-center-align.tsx +96 -0
  188. package/src/components/ui/pagination/card-minimal-right-aligne.tsx +90 -0
  189. package/src/components/ui/pagination/default-pagination.tsx +128 -0
  190. package/src/components/ui/pagination/get-pagination-item.tsx +36 -0
  191. package/src/components/ui/pagination/pagination-card-button-group-aligned.tsx +94 -0
  192. package/src/components/ui/pagination/pagination-card-minimal-left-aligned.tsx +90 -0
  193. package/src/components/ui/pagination/pagination-content.tsx +15 -0
  194. package/src/components/ui/pagination/pagination-item.tsx +11 -0
  195. package/src/components/ui/pagination/pagination-link.tsx +42 -0
  196. package/src/components/ui/tab-components/tabs-content.tsx +15 -0
  197. package/src/components/ui/tab-components/tabs-list.tsx +27 -0
  198. package/src/components/ui/tab-components/tabs-trigger.tsx +25 -0
  199. package/src/components/ui/text-content-wrapper.tsx +36 -0
  200. package/src/hooks/useClickOutside.ts +23 -0
  201. package/src/icons/general/ArrowLeft.tsx +31 -0
  202. package/src/icons/general/ArrowRight.tsx +31 -0
  203. package/src/icons/general/activity-heart.tsx +31 -0
  204. package/src/icons/general/activity.tsx +31 -0
  205. package/src/icons/general/anchor.tsx +31 -0
  206. package/src/icons/general/archive.tsx +31 -0
  207. package/src/icons/general/arrow-left.tsx +25 -0
  208. package/src/icons/general/arrow-right.tsx +25 -0
  209. package/src/icons/general/asterisk-01.tsx +31 -0
  210. package/src/icons/general/asterisk-02.tsx +31 -0
  211. package/src/icons/general/at-sign.tsx +31 -0
  212. package/src/icons/general/attention-mark.tsx +43 -0
  213. package/src/icons/general/bookmark-add.tsx +31 -0
  214. package/src/icons/general/bookmark.tsx +31 -0
  215. package/src/icons/general/chevron-left.tsx +25 -0
  216. package/src/icons/general/chevron-right.tsx +25 -0
  217. package/src/icons/general/circle-minues.tsx +25 -0
  218. package/src/icons/general/circle-plus.tsx +25 -0
  219. package/src/icons/general/circle-question-mark.tsx +32 -0
  220. package/src/icons/general/circle.tsx +32 -0
  221. package/src/icons/general/copy.tsx +43 -0
  222. package/src/icons/general/email.tsx +32 -0
  223. package/src/icons/general/home.tsx +25 -0
  224. package/src/icons/general/layer.tsx +36 -0
  225. package/src/icons/general/leading.tsx +19 -0
  226. package/src/icons/general/master-card.tsx +37 -0
  227. package/src/icons/general/minus.tsx +36 -0
  228. package/src/icons/general/plus.tsx +19 -0
  229. package/src/icons/general/remove.tsx +32 -0
  230. package/src/icons/general/slash-divider.tsx +26 -0
  231. package/src/icons/general/tick-box.tsx +37 -0
  232. package/src/icons/general/trailing.tsx +19 -0
  233. package/src/icons/general/unkown-format.tsx +25 -0
  234. package/src/icons/general/visa-card.tsx +38 -0
  235. package/src/icons/general/x-close.tsx +35 -0
  236. package/src/icons/icons.type.ts +7 -0
  237. package/src/index.css +21 -0
  238. package/src/index.ts +3 -0
  239. package/src/lib/utils.ts +6 -0
  240. package/src/lib/zIndexUtils.ts +2 -0
  241. package/src/main.tsx +50 -0
  242. package/src/vite-env.d.ts +1 -0
  243. package/tests/setup.ts +8 -0
  244. package/tsconfig.app.json +31 -0
  245. package/tsconfig.json +7 -0
  246. package/tsconfig.node.json +24 -0
  247. package/tsconfig.rollup.json +12 -0
  248. package/vite.config.ts +20 -0
  249. package/vitest.config.ts +47 -0
@@ -0,0 +1,130 @@
1
+ import { cn } from "@/lib/utils";
2
+ import { getBackdropZIndex } from "@/lib/zIndexUtils";
3
+ import { useEffect, useLayoutEffect, useRef, useState } from "react";
4
+ import { Button } from "../Button";
5
+ import { dropdownPlacementVariants } from "./_.style";
6
+ import { DropdownWrapperProps } from "./_.types";
7
+
8
+ export const Dropdown = ({
9
+ children,
10
+ toggleBtn,
11
+ direction = "down",
12
+ alignment = "right",
13
+ dropDownClassName = "",
14
+ autoAdjust = true,
15
+ open: openProp,
16
+ onOpenChange,
17
+ }: DropdownWrapperProps) => {
18
+ const [internalOpen, setInternalOpen] = useState(false);
19
+ const [placement, setPlacement] = useState<"down" | "up" | "left" | "right">(direction);
20
+ const [placementAlignment, setPlacementAlignment] = useState<"left" | "right" | "top" | "bottom">(alignment);
21
+ const isControlled =
22
+ openProp !== undefined && typeof onOpenChange === "function";
23
+ const open = openProp ?? internalOpen;
24
+ const wrapperRef = useRef<HTMLDivElement>(null);
25
+ const dropdownRef = useRef<HTMLDivElement>(null);
26
+ const depthRef = useRef<number>(0);
27
+
28
+ useEffect(() => {
29
+ const handleClickOutside = (e: MouseEvent) => {
30
+ if (
31
+ wrapperRef.current &&
32
+ !wrapperRef.current.contains(e.target as Node)
33
+ ) {
34
+ setOpen(false);
35
+ }
36
+ };
37
+ document.addEventListener("mousedown", handleClickOutside);
38
+ return () => document.removeEventListener("mousedown", handleClickOutside);
39
+ }, []);
40
+
41
+ useLayoutEffect(() => {
42
+ if (!autoAdjust) {
43
+ setPlacement(direction);
44
+ setPlacementAlignment(alignment);
45
+ return;
46
+ }
47
+ if (open && wrapperRef.current && dropdownRef.current) {
48
+ const rect = wrapperRef.current.getBoundingClientRect();
49
+ const dropdownRect = dropdownRef.current.getBoundingClientRect();
50
+ const estWidth = dropdownRect.width || 200;
51
+ const estHeight = dropdownRect.height || 200;
52
+
53
+ const spaceBelow = window.innerHeight - rect.bottom;
54
+ const spaceAbove = rect.top;
55
+ const spaceRight = window.innerWidth - rect.right;
56
+ const spaceLeft = rect.left;
57
+
58
+ // Determine best direction based on available space
59
+ if (direction === "down" || direction === "up") {
60
+ if (spaceBelow < estHeight && spaceAbove > spaceBelow) {
61
+ setPlacement("up");
62
+ } else {
63
+ setPlacement("down");
64
+ }
65
+ setPlacementAlignment(alignment);
66
+ } else if (direction === "left" || direction === "right") {
67
+ if (direction === "left") {
68
+ if (spaceLeft < estWidth && spaceRight > spaceLeft) {
69
+ setPlacement("right");
70
+ } else {
71
+ setPlacement("left");
72
+ }
73
+ } else if (direction === "right") {
74
+ if (spaceRight < estWidth && spaceLeft > spaceRight) {
75
+ setPlacement("left");
76
+ } else {
77
+ setPlacement("right");
78
+ }
79
+ }
80
+ setPlacementAlignment(alignment);
81
+ } else {
82
+ // Auto-detect best direction
83
+ const verticalSpace = Math.max(spaceBelow, spaceAbove);
84
+ const horizontalSpace = Math.max(spaceRight, spaceLeft);
85
+
86
+ if (verticalSpace > horizontalSpace) {
87
+ if (spaceBelow > spaceAbove) {
88
+ setPlacement("down");
89
+ } else {
90
+ setPlacement("up");
91
+ }
92
+ setPlacementAlignment(alignment);
93
+ } else {
94
+ if (spaceRight > spaceLeft) {
95
+ setPlacement("right");
96
+ } else {
97
+ setPlacement("left");
98
+ }
99
+ setPlacementAlignment(alignment);
100
+ }
101
+ }
102
+ }
103
+ }, [open, direction, alignment, autoAdjust]);
104
+
105
+ const setOpen = (val: boolean) => {
106
+ if (isControlled && onOpenChange) {
107
+ onOpenChange(val);
108
+ } else {
109
+ setInternalOpen(val);
110
+ }
111
+ };
112
+ return (
113
+ <div className="relative inline-block" ref={wrapperRef}>
114
+ <Button {...toggleBtn} onClick={() => setOpen(!open)} />
115
+ {open && (
116
+ <div
117
+ ref={dropdownRef}
118
+ style={{ zIndex: getBackdropZIndex(depthRef.current) }}
119
+ className={cn(
120
+ "absolute border border-rborder-secondary w-fit rounded-md shadow-lg bg-rbg-primary",
121
+ dropdownPlacementVariants({ direction: placement, alignment: placementAlignment }),
122
+ dropDownClassName
123
+ )}
124
+ >
125
+ {children}
126
+ </div>
127
+ )}
128
+ </div>
129
+ );
130
+ };
@@ -0,0 +1,74 @@
1
+ // FileUploader.stories.tsx
2
+
3
+ import React, { useState } from "react";
4
+ import type { Meta, StoryFn } from "@storybook/react-vite";
5
+ import { UploadCloud } from "lucide-react";
6
+ import { UploadItemProps, FileUploaderProps } from "./_.types";
7
+ import { FileUploader } from ".";
8
+
9
+ export default {
10
+ title: "Components/FileUploader",
11
+ component: FileUploader,
12
+ } as Meta<FileUploaderProps>;
13
+
14
+ const Template: StoryFn<FileUploaderProps> = (args) => {
15
+ const [items, setItems] = useState<UploadItemProps[]>([]);
16
+
17
+ // Handler to simulate file selection/upload
18
+ const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
19
+ const { files } = e.target;
20
+ if (!files) return;
21
+
22
+ // Create new items from selected files (dummy upload data)
23
+ const newItems: UploadItemProps[] = Array.from(files).map(
24
+ (file, index) => ({
25
+ id: Date.now() + index,
26
+ title: file.name,
27
+ progressValue: 50,
28
+ description: "در حال آپلود...", // "Uploading..."
29
+ percentage: 50,
30
+ error: false,
31
+ size: `${(file.size / 1024).toFixed(2)} KB`,
32
+ format: "jpeg", // Should be one of "video" | "jpeg" | "pdf" | "unkown"
33
+ icon: <UploadCloud size={16} />,
34
+ status: "uploading",
35
+ })
36
+ );
37
+
38
+ setItems((prev) => [...prev, ...newItems]);
39
+ };
40
+
41
+ // Handler to simulate deletion of an upload item
42
+ const handleDelete = (id: number) => {
43
+ setItems((prev) => prev.filter((item) => item.id !== id));
44
+ };
45
+
46
+ return (
47
+ <div className="p-4">
48
+ <FileUploader
49
+ {...args}
50
+ items={items}
51
+ onChange={handleChange}
52
+ onDelete={handleDelete}
53
+ />
54
+ </div>
55
+ );
56
+ };
57
+
58
+ export const Default = Template.bind({});
59
+ Default.args = {
60
+ icon: <UploadCloud size={20} />,
61
+ mainText: "برای آپلود کلیک کنید",
62
+ subText: "یا بکشید و رها کنید",
63
+ formatText: "SVG, PNG, JPG یا GIF (حداکثر 800x400 پیکسل)",
64
+ acceptableFormats: ["png", "jpeg", "svg"],
65
+ disable: false,
66
+ loadType: "progressBar",
67
+ multiItem: true,
68
+ };
69
+
70
+ export const Disabled = Template.bind({});
71
+ Disabled.args = {
72
+ ...Default.args,
73
+ disable: true,
74
+ };
File without changes
@@ -0,0 +1,222 @@
1
+ import { render, screen, fireEvent } from "@testing-library/react";
2
+ import { describe, it, expect, vi } from "vitest";
3
+ import { FileUploaderBox } from "../ui/file-uploader-components/file-uploader-box";
4
+ import { FileUploaderItem } from "../ui/file-uploader-components/file-uploader-item";
5
+ import { FileUploader } from ".";
6
+ import { UploadItemProps } from "./_.types";
7
+
8
+ describe("FileUploader", () => {
9
+ it("renders uploader box and no items initially", () => {
10
+ render(
11
+ <FileUploader
12
+ mainText="آپلود"
13
+ subText="کشیدن فایل"
14
+ formatText="png only"
15
+ acceptableFormats={["png"]}
16
+ onChange={() => {}}
17
+ onDelete={() => {}}
18
+ />
19
+ );
20
+
21
+ expect(screen.getByText("آپلود")).toBeInTheDocument();
22
+ expect(screen.getByText("کشیدن فایل")).toBeInTheDocument();
23
+ });
24
+
25
+ it("renders upload items if provided", () => {
26
+ render(
27
+ <FileUploader
28
+ mainText="آپلود"
29
+ subText="کشیدن فایل"
30
+ formatText="png only"
31
+ acceptableFormats={["png"]}
32
+ items={[
33
+ {
34
+ id: 1,
35
+ title: "file1.png",
36
+ size: "1MB",
37
+ description: "",
38
+ progressValue: 70,
39
+ percentage: 70,
40
+ icon: <div>📄</div>,
41
+ error: false,
42
+ status: "uploading",
43
+ },
44
+ ]}
45
+ onChange={() => {}}
46
+ onDelete={() => {}}
47
+ />
48
+ );
49
+
50
+ expect(screen.getByText("file1.png")).toBeInTheDocument();
51
+ });
52
+ });
53
+
54
+ describe("FileUploaderBox", () => {
55
+ const defaultProps = {
56
+ mainText: "آپلود کن",
57
+ subText: "یا بکش و رها کن",
58
+ formatText: "png, jpg",
59
+ acceptableFormats: ["png", "jpg"],
60
+ onChange: vi.fn(),
61
+ disable: false,
62
+ multiItem: false,
63
+ };
64
+ const baseProps = {
65
+ mainText: "Upload File",
66
+ subText: "Drag and drop file",
67
+ formatText: "Accepts PNG, JPG",
68
+ onChange: () => {},
69
+ };
70
+ it("renders mainText and subText", () => {
71
+ render(<FileUploaderBox {...defaultProps} />);
72
+ expect(screen.getByText("آپلود کن")).toBeInTheDocument();
73
+ expect(screen.getByText("یا بکش و رها کن")).toBeInTheDocument();
74
+ });
75
+
76
+ it("renders default icon when icon prop is not provided", () => {
77
+ render(<FileUploaderBox {...defaultProps} />);
78
+ const svgElement = screen.getByTestId("default-uploadcloud");
79
+ expect(svgElement).toBeInTheDocument();
80
+ });
81
+
82
+ it("renders custom icon when icon prop is provided", () => {
83
+ const CustomIcon = () => <div data-testid="custom-icon">Custom Icon</div>;
84
+ render(<FileUploaderBox {...defaultProps} icon={<CustomIcon />} />);
85
+ expect(screen.getByTestId("custom-icon")).toBeInTheDocument();
86
+ expect(screen.queryByTestId("default-uploadcloud")).toBeNull();
87
+ });
88
+
89
+ it("computes accept attribute correctly when acceptableFormats is an array", () => {
90
+ render(
91
+ <FileUploaderBox {...defaultProps} acceptableFormats={["png", "jpeg"]} />
92
+ );
93
+ const fileInput = screen.getByTestId("file-input") as HTMLInputElement;
94
+ expect(fileInput.getAttribute("accept")).toBe(".png, .jpeg");
95
+ });
96
+
97
+ it("computes accept attribute correctly when acceptableFormats is a string", () => {
98
+ render(<FileUploaderBox {...defaultProps} acceptableFormats="image/*" />);
99
+ const fileInput = screen.getByTestId("file-input") as HTMLInputElement;
100
+ expect(fileInput.getAttribute("accept")).toBe("image/*");
101
+ });
102
+
103
+ it("disables input and button when disable prop is true", () => {
104
+ render(<FileUploaderBox {...defaultProps} disable={true} />);
105
+ const fileInput = screen.getByTestId("file-input") as HTMLInputElement;
106
+ expect(fileInput.disabled).toBe(true);
107
+ const button = fileInput.closest("label")?.querySelector("button");
108
+ expect(button).toBeDefined();
109
+ if (button) {
110
+ expect(button).toBeDisabled();
111
+ }
112
+ });
113
+
114
+ it("calls onChange when file is uploaded", () => {
115
+ const onChange = vi.fn();
116
+ render(<FileUploaderBox {...defaultProps} onChange={onChange} />);
117
+ const fileInput = screen.getByTestId("file-input") as HTMLInputElement;
118
+ const file = new File(["test"], "test.png", { type: "image/png" });
119
+ fireEvent.change(fileInput, { target: { files: [file] } });
120
+ expect(onChange).toHaveBeenCalled();
121
+ });
122
+ it("sets accept attribute correctly when acceptableFormats is an array", () => {
123
+ const acceptableFormats = ["png", ".jpg"];
124
+ render(
125
+ <FileUploaderBox {...baseProps} acceptableFormats={acceptableFormats} />
126
+ );
127
+ const fileInput = screen.getByTestId("file-input") as HTMLInputElement;
128
+ expect(fileInput.getAttribute("accept")).toBe(".png, .jpg");
129
+ });
130
+
131
+ it("sets accept attribute correctly when acceptableFormats is a string", () => {
132
+ const acceptableFormats = "image/*";
133
+ render(
134
+ <FileUploaderBox {...baseProps} acceptableFormats={acceptableFormats} />
135
+ );
136
+ const fileInput = screen.getByTestId("file-input") as HTMLInputElement;
137
+ expect(fileInput.getAttribute("accept")).toBe("image/*");
138
+ });
139
+
140
+ it("does not set accept attribute when acceptableFormats is not provided", () => {
141
+ render(<FileUploaderBox {...baseProps} />);
142
+ const fileInput = screen.getByTestId("file-input") as HTMLInputElement;
143
+ expect(fileInput.getAttribute("accept")).toBeNull();
144
+ });
145
+ });
146
+ describe("FileUploaderItem", () => {
147
+ const mockItem: UploadItemProps = {
148
+ id: 1,
149
+ title: "sample.png",
150
+ description: "A sample image",
151
+ error: false,
152
+ size: "1MB",
153
+ icon: <div data-testid="icon">🖼️</div>,
154
+ status: "uploading",
155
+ progressValue: 50,
156
+ percentage: 50,
157
+ };
158
+
159
+ it("renders file info and icon", () => {
160
+ render(
161
+ <FileUploaderItem
162
+ item={mockItem}
163
+ loadType="progressBar"
164
+ onDelete={() => {}}
165
+ />
166
+ );
167
+ expect(screen.getByText("sample.png")).toBeInTheDocument();
168
+ expect(screen.getByText("1MB")).toBeInTheDocument();
169
+ expect(screen.getByTestId("icon")).toBeInTheDocument();
170
+ });
171
+
172
+ it("calls onDelete when trash icon is clicked", () => {
173
+ const onDelete = vi.fn();
174
+ render(
175
+ <FileUploaderItem
176
+ item={mockItem}
177
+ loadType="progressBar"
178
+ onDelete={onDelete}
179
+ />
180
+ );
181
+
182
+ const button = screen.getByRole("button");
183
+ fireEvent.click(button);
184
+ expect(onDelete).toHaveBeenCalledWith(1);
185
+ });
186
+ it("applies error class when item.status is 'failed'", () => {
187
+ const failedItem = { ...mockItem, status: "failed" };
188
+ const { container } = render(
189
+ <FileUploaderItem
190
+ item={failedItem}
191
+ loadType="progressBar"
192
+ onDelete={() => {}}
193
+ />
194
+ );
195
+ expect(container.firstChild).toHaveClass("border-rborder-error");
196
+ });
197
+ it("renders fallback SVG icon when item.icon is not provided", () => {
198
+ const mockItemWithNoIcon = {
199
+ ...mockItem,
200
+ icon: null,
201
+ status: "failed",
202
+ };
203
+
204
+ const { container } = render(
205
+ <FileUploaderItem
206
+ item={mockItemWithNoIcon}
207
+ loadType="progressBar"
208
+ onDelete={() => {}}
209
+ />
210
+ );
211
+ const svgs = container.querySelectorAll("svg");
212
+
213
+ const fallbackSvg = svgs[0];
214
+
215
+ expect(fallbackSvg).toBeTruthy();
216
+ expect(fallbackSvg.getAttribute("viewBox")).toBe("0 0 20 20");
217
+ expect(fallbackSvg.querySelector("path")?.getAttribute("stroke")).toBe(
218
+ "#98A2B3"
219
+ );
220
+ expect(container.firstChild).toHaveClass("border-rborder-error");
221
+ });
222
+ });
@@ -0,0 +1,53 @@
1
+ type AcceptableExtension =
2
+ | "png"
3
+ | "jpg"
4
+ | "jpeg"
5
+ | "svg"
6
+ | "gif"
7
+ | "webp"
8
+ | "pdf"
9
+ | "docx"
10
+ | "xlsx"
11
+ | "mp4"
12
+ | "mp3"
13
+ | "wav";
14
+ export interface UploadItemProps {
15
+ id: number;
16
+ title: string;
17
+ progressValue?: number;
18
+ description: string;
19
+ percentage?: number;
20
+ error: boolean;
21
+ size: string;
22
+ icon?: React.ReactNode;
23
+ status: string;
24
+ }
25
+ export interface FileUploaderProps {
26
+ icon?: React.ReactNode;
27
+ mainText: string;
28
+ subText: string;
29
+ formatText: string;
30
+ acceptableFormats?: AcceptableExtension[] | AcceptableExtension | "";
31
+ disable?: boolean;
32
+ items?: UploadItemProps[];
33
+ loadType?: "progressBar" | "progressFill";
34
+ multiItem?: boolean;
35
+ onDelete: (id: number) => void;
36
+ onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
37
+ }
38
+
39
+ export interface FileUploaderBoxProps {
40
+ icon?: React.ReactNode;
41
+ mainText: string;
42
+ subText: string;
43
+ formatText: string;
44
+ acceptableFormats?: string[] | string;
45
+ disable?: boolean;
46
+ multiItem?: boolean;
47
+ onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
48
+ }
49
+ export interface FileUploaderItemProps {
50
+ item: UploadItemProps;
51
+ loadType?: "progressBar" | "progressFill";
52
+ onDelete: (id: number) => void;
53
+ }
@@ -0,0 +1,44 @@
1
+ import { FileUploaderBox } from "../ui/file-uploader-components/file-uploader-box";
2
+ import { FileUploaderItem } from "../ui/file-uploader-components/file-uploader-item";
3
+ import { FileUploaderProps } from "./_.types";
4
+
5
+ export const FileUploader = ({
6
+ icon,
7
+ mainText,
8
+ subText,
9
+ formatText,
10
+ acceptableFormats = "",
11
+ disable = false,
12
+ items,
13
+ loadType = "progressBar",
14
+ multiItem = false,
15
+ onChange,
16
+ onDelete,
17
+ }: FileUploaderProps) => {
18
+ return (
19
+ <div className="w-full flex flex-col gap-4">
20
+ <FileUploaderBox
21
+ icon={icon}
22
+ mainText={mainText}
23
+ subText={subText}
24
+ formatText={formatText}
25
+ acceptableFormats={acceptableFormats}
26
+ disable={disable}
27
+ multiItem={multiItem}
28
+ onChange={onChange}
29
+ />
30
+ {items && items.length > 0 && (
31
+ <div className="flex flex-col gap-3">
32
+ {items.map((item) => (
33
+ <FileUploaderItem
34
+ key={item?.title}
35
+ item={item}
36
+ loadType={loadType}
37
+ onDelete={onDelete}
38
+ />
39
+ ))}
40
+ </div>
41
+ )}
42
+ </div>
43
+ );
44
+ };