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,142 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+
3
+ import { ArrowUpRight, EllipsisVertical } from "lucide-react";
4
+ import { Metrics } from ".";
5
+ import { Button } from "../Button";
6
+
7
+ const meta = {
8
+ title: "Components/Metrics",
9
+ component: Metrics,
10
+ tags: ["autodocs"],
11
+ argTypes: {
12
+ // Optionally add controls for props here
13
+ },
14
+ } satisfies Meta<typeof Metrics>;
15
+
16
+ export default meta;
17
+ type Story = StoryObj<typeof Metrics>;
18
+
19
+ export const Default: Story = {
20
+ args: {
21
+ title: "Total Sales",
22
+ value: "$32,000",
23
+ description: "12% increase from last month",
24
+ icon: <ArrowUpRight className="w-6 h-6 text-green-600" />,
25
+ iconType: "success",
26
+ toggleBtn: (
27
+ <button className="underline text-sm text-rtext-tertiary-600">
28
+ Details
29
+ </button>
30
+ ),
31
+ statistic: <span className="text-xs font-bold text-green-500">+12%</span>,
32
+ iconPosition: "topWithTitle",
33
+ statisticPosition: "row-between",
34
+ },
35
+ };
36
+
37
+ export const WithChartBottom: Story = {
38
+ args: {
39
+ title: "Active Users",
40
+ value: "8,450",
41
+ description: "Stable since last week",
42
+ toggleBtn: (
43
+ <button className="underline text-sm text-rtext-tertiary-600">
44
+ View
45
+ </button>
46
+ ),
47
+ chartImg: <img src="/test.png" alt="Chart" className="w-full h-full" />,
48
+ chartPosition: "bottom",
49
+ iconPosition: "hidden",
50
+ statisticPosition: "row-next",
51
+ },
52
+ };
53
+
54
+ export const WithChartBottomAndActions: Story = {
55
+ args: {
56
+ title: "Total Sales",
57
+ value: "$32,000",
58
+ icon: <ArrowUpRight className="w-6 h-6 text-green-600" />,
59
+ iconType: "secondary",
60
+ iconPosition: "topWithTitle",
61
+ statistic: (
62
+ <span className="text-xs font-medium text-green-700 bg-green-100 rounded px-2 py-0.5">
63
+ +12%
64
+ </span>
65
+ ),
66
+ toggleBtn: (
67
+ <Button variant={"primary"}
68
+ className="px-4 py-1.5"
69
+ size={"md"}>
70
+ Details
71
+ </Button>
72
+ ),
73
+ actionBtns: [
74
+ <Button
75
+ key="1"
76
+ variant={"primary"}
77
+ className="px-4 py-1.5"
78
+ size={"sm"}
79
+ >
80
+ View
81
+ </Button>,
82
+ <Button
83
+ variant={"secondaryColor"}
84
+ key="2"
85
+ className="px-4 py-1.5"
86
+ size={"sm"}
87
+ >
88
+ Export
89
+ </Button>,
90
+ ],
91
+ statisticPosition: "row-next",
92
+ dir: "rtl",
93
+ chartPosition: "bottom",
94
+ chartImg: <img src="/test.png" alt="" className="w-full h-full" />,
95
+ titleType: "bold",
96
+ },
97
+ };
98
+ export const WithChartSideAndActions: Story = {
99
+ args: {
100
+ title: "Total Sales",
101
+ value: "$32,000",
102
+ icon: <ArrowUpRight className="w-6 h-6 text-green-600" />,
103
+ iconType: "secondary",
104
+ iconPosition: "topWithTitle",
105
+ statistic: (
106
+ <span className="text-xs font-medium text-green-700 bg-green-100 rounded px-2 py-0.5">
107
+ +12%
108
+ </span>
109
+ ),
110
+ toggleBtn: (
111
+ <Button variant={"linkGray"}
112
+ className="px-4 py-1.5"
113
+ onClick={() => alert("dropdown")}
114
+ size={"md"}>
115
+ <EllipsisVertical />
116
+ </Button>
117
+ ),
118
+ actionBtns: [
119
+ <Button
120
+ key="1"
121
+ variant={"primary"}
122
+ className="px-4 py-1.5"
123
+ size={"sm"}
124
+ >
125
+ View
126
+ </Button>,
127
+ <Button
128
+ variant={"secondaryColor"}
129
+ key="2"
130
+ className="px-4 py-1.5"
131
+ size={"sm"}
132
+ >
133
+ Export
134
+ </Button>,
135
+ ],
136
+ statisticPosition: "row-next",
137
+ dir: "rtl",
138
+ chartPosition: "side",
139
+ chartImg: <img src="/test.png" alt="" className="w-full h-full" />,
140
+ titleType: "bold",
141
+ },
142
+ };
@@ -0,0 +1,14 @@
1
+ const baseStyles = "flex flex-col h-full items-end gap-2 w-full";
2
+ const iconStyles = {
3
+ success:
4
+ "w-12 h-12 flex items-center justify-center rounded-full bg-rbg-success-secondary",
5
+ notification:
6
+ "w-12 h-12 flex items-center justify-center rounded-full bg-rbg-brand-secondary",
7
+ secondary:
8
+ "w-12 h-12 flex items-center justify-center rounded-lg bg-rbg-primary border border-gray-light-200 shadow-xs-skeuomorphic",
9
+ };
10
+ const titleStyle = {
11
+ normal: "font-medium text-sm text-rtext-tertiary-600",
12
+ bold: "text-rtext-primary-900 font-normal text-md",
13
+ };
14
+ export { baseStyles, iconStyles, titleStyle };
@@ -0,0 +1,166 @@
1
+ import { render, screen } from "@testing-library/react";
2
+ import { ArrowUpRight } from "lucide-react";
3
+ import { describe, expect, it } from "vitest";
4
+ import { Metrics } from ".";
5
+
6
+ describe("Metrics Component", () => {
7
+ it("should render title, value, description and toggle button", () => {
8
+ render(
9
+ <Metrics
10
+ title="Test Title"
11
+ value="100"
12
+ description="Test Description"
13
+ toggleBtn={<button>Toggle</button>}
14
+ />
15
+ );
16
+ expect(screen.getByText("Test Title")).toBeDefined();
17
+ expect(screen.getByText("100")).toBeDefined();
18
+ expect(screen.getByText("Test Description")).toBeDefined();
19
+ expect(screen.getByRole("button", { name: /toggle/i })).toBeDefined();
20
+ });
21
+
22
+ it("should render icon when iconPosition is 'topWithTitle'", () => {
23
+ render(
24
+ <Metrics
25
+ title="Icon Test"
26
+ value="200"
27
+ toggleBtn={<button>Toggle</button>}
28
+ iconPosition="topWithTitle"
29
+ icon={<ArrowUpRight data-testid="icon" />}
30
+ />
31
+ );
32
+ // the icon should be rendered when iconPosition is either "topWithTitle" or "topAlone"
33
+ expect(screen.getByTestId("icon")).toBeDefined();
34
+ });
35
+ it("should not render icon when iconPosition is 'hidden'", () => {
36
+ render(
37
+ <Metrics
38
+ title="Icon Test"
39
+ value="200"
40
+ toggleBtn={<button>Toggle</button>}
41
+ iconPosition="hidden"
42
+ icon={<ArrowUpRight data-testid="icon" />}
43
+ />
44
+ );
45
+ // using queryByTestId returns null if the icon is not rendered
46
+ expect(screen.queryByTestId("icon")).toBeNull();
47
+ });
48
+
49
+ it("should render chart image when chartPosition is 'bottom'", () => {
50
+ render(
51
+ <Metrics
52
+ title="Chart Test"
53
+ value="300"
54
+ toggleBtn={<button>Toggle</button>}
55
+ chartPosition="bottom"
56
+ chartImg={<img src="/test.png" alt="chart" data-testid="chart" />}
57
+ />
58
+ );
59
+ expect(screen.getByTestId("chart")).toBeDefined();
60
+ expect(screen.getByTestId("chart")).toHaveAttribute("src", "/test.png");
61
+ });
62
+
63
+ it("should render chart image when chartPosition is 'side'", () => {
64
+ render(
65
+ <Metrics
66
+ title="Chart Side Test"
67
+ value="400"
68
+ toggleBtn={<button>Toggle</button>}
69
+ chartPosition="side"
70
+ chartImg={
71
+ <img src="/test.png" alt="chart side" data-testid="chart-side" />
72
+ }
73
+ />
74
+ );
75
+ expect(screen.getByTestId("chart-side")).toBeDefined();
76
+ expect(screen.getByTestId("chart-side")).toHaveAttribute(
77
+ "src",
78
+ "/test.png"
79
+ );
80
+ });
81
+
82
+ it("should render action buttons when provided", () => {
83
+ render(
84
+ <Metrics
85
+ title="Actions Test"
86
+ value="500"
87
+ toggleBtn={<button>Toggle</button>}
88
+ actionBtns={[
89
+ <button key="1">Button 1</button>,
90
+ <button key="2">Button 2</button>,
91
+ ]}
92
+ />
93
+ );
94
+ expect(screen.getByText("Button 1")).toBeDefined();
95
+ expect(screen.getByText("Button 2")).toBeDefined();
96
+ });
97
+ it("should render the icon correctly when iconPosition is 'iconSide' (Line 26)", () => {
98
+ render(
99
+ <Metrics
100
+ title="Test"
101
+ value="100"
102
+ toggleBtn={<button>Toggle</button>}
103
+ iconPosition="iconSide"
104
+ iconType="secondary"
105
+ icon={<div data-testid="icon">Icon</div>}
106
+ />
107
+ );
108
+ // Line 26: Icon should be rendered inside a div with "self-center" as part of its class.
109
+ const iconElement = screen.getByTestId("icon");
110
+ expect(iconElement).toBeInTheDocument();
111
+ expect(iconElement.parentElement).toHaveClass("self-center");
112
+ });
113
+
114
+ it("should render the title in a span when iconPosition is 'topAlone' (Line 51)", () => {
115
+ render(
116
+ <Metrics
117
+ title="Top Alone Title"
118
+ value="200"
119
+ toggleBtn={<button>Toggle</button>}
120
+ iconPosition="topAlone"
121
+ />
122
+ );
123
+ // Line 51: The title should appear in a span rendered solely for topAlone condition.
124
+ const titleElement = screen.getByText("Top Alone Title");
125
+ expect(titleElement).toBeInTheDocument();
126
+ expect(titleElement.tagName.toLowerCase()).toBe("span");
127
+ });
128
+
129
+ it("should apply 'row-below' classes when statisticPosition is 'row-below' (Line 59)", () => {
130
+ render(
131
+ <Metrics
132
+ title="Stat Test"
133
+ value="300"
134
+ toggleBtn={<button>Toggle</button>}
135
+ statisticPosition="row-below"
136
+ statistic={<div data-testid="statistic">Stat</div>}
137
+ description="Description"
138
+ />
139
+ );
140
+ // Locate one of the texts rendered inside the statistic container.
141
+ const descElement = screen.getByText("Description");
142
+ // The description is inside the container whose class is built on line 59
143
+ const statisticContainer = descElement.closest("div");
144
+ expect(statisticContainer).toBeDefined();
145
+ // Expect the container to include the classes from statisticPosition 'row-below'
146
+ expect(statisticContainer?.className).toContain("gap-2");
147
+ });
148
+
149
+ it("should apply 'row-next' classes when statisticPosition is 'row-next' (Line 63)", () => {
150
+ render(
151
+ <Metrics
152
+ title="Stat Next Test"
153
+ value="400"
154
+ toggleBtn={<button>Toggle</button>}
155
+ statisticPosition="row-next"
156
+ statistic={<div data-testid="statistic">Stat</div>}
157
+ description="Description"
158
+ />
159
+ );
160
+ const descElement = screen.getByText("Description");
161
+ const statisticContainer = descElement.closest("div");
162
+ expect(statisticContainer).toBeDefined();
163
+ // Expect classes from statisticPosition 'row-next'
164
+ expect(statisticContainer?.className).toContain("gap-2");
165
+ });
166
+ });
@@ -0,0 +1,18 @@
1
+ import React from "react";
2
+
3
+ export interface MetricProps {
4
+ title?: string;
5
+ value: string;
6
+ description?: string;
7
+ icon?: React.ReactNode;
8
+ statistic?: React.ReactNode;
9
+ chartImg?: React.ReactNode;
10
+ chartPosition?: "side" | "bottom";
11
+ actionBtns?: React.ReactNode[];
12
+ toggleBtn: React.ReactNode;
13
+ iconPosition?: "topWithTitle" | "topAlone" | "iconSide" | "hidden";
14
+ statisticPosition?: "row-between" | "row-next" | "row-below";
15
+ dir?: "rtl" | "ltr";
16
+ iconType?: "success" | "notification" | "secondary";
17
+ titleType?: "normal" | "bold";
18
+ }
@@ -0,0 +1,100 @@
1
+
2
+ import { Card, CardAction, CardHeader } from "../ui/card";
3
+ import { MetricProps } from "./_.types";
4
+ import { baseStyles, iconStyles, titleStyle } from "./_.style";
5
+ import React from "react";
6
+ import { cn } from "@/lib/utils";
7
+
8
+ export const Metrics = ({
9
+ title,
10
+ value,
11
+ description,
12
+ icon,
13
+ iconType = "secondary",
14
+ statistic,
15
+ chartImg,
16
+ chartPosition,
17
+ actionBtns,
18
+ toggleBtn,
19
+ iconPosition = "hidden",
20
+ statisticPosition,
21
+ titleType = "normal",
22
+ }: MetricProps) => {
23
+ return (
24
+ <Card className="flex flex-col gap-0 rounded-xl border border-rborder-secondary bg-rbg-primary shadow-xs w-full sm:max-w-[380px] max-w-[311px] h-fit justify-start p-0">
25
+ <div className="flex gap-5 w-full py-5 px-4 sm:p-6">
26
+ {iconPosition == "iconSide" && (
27
+ <div className={cn("self-center", iconStyles[iconType])}>{icon}</div>
28
+ )}
29
+ <div
30
+ className={cn(
31
+ baseStyles,
32
+ `${
33
+ (iconPosition == "topWithTitle" || iconPosition == "topAlone") &&
34
+ "gap-5 md:gap-6"
35
+ }`
36
+ )}
37
+ >
38
+ <CardHeader className="flex justify-between w-full px-0">
39
+ <div className="flex items-center gap-3 ">
40
+ {(iconPosition == "topWithTitle" ||
41
+ iconPosition == "topAlone") && (
42
+ <div className={iconStyles[iconType]}>{icon}</div>
43
+ )}
44
+ {iconPosition != "topAlone" && (
45
+ <span className={titleStyle[titleType]}>{title}</span>
46
+ )}
47
+ </div>
48
+ {toggleBtn}
49
+ </CardHeader>
50
+ <div className="flex flex-col w-full gap-2">
51
+ {iconPosition == "topAlone" && (
52
+ <span className={titleStyle[titleType]}>{title}</span>
53
+ )}
54
+ <div className={"w-full flex justify-between gap-4"}>
55
+ <div
56
+ className={cn(
57
+ "flex justify-between items-center w-full h-fit px-0",
58
+ `${
59
+ statisticPosition == "row-below" &&
60
+ "flex-col just-start items-start gap-4"
61
+ }`,
62
+ `${
63
+ statisticPosition == "row-next" &&
64
+ "flex justify-start items-start gap-2"
65
+ }`
66
+ )}
67
+ >
68
+ <span className="text-display-md font-normal text-rtext-primary-900">
69
+ {value}
70
+ </span>
71
+ <div className="flex gap-2 items-center">
72
+ {statistic}
73
+ {description && (
74
+ <span className="font-medium text-sm text-rtext-tertiary-600">
75
+ {description}
76
+ </span>
77
+ )}
78
+ </div>
79
+ </div>
80
+ {chartPosition == "side" && (
81
+ <div className="w-full">{chartImg}</div>
82
+ )}
83
+ </div>
84
+ </div>
85
+ {chartPosition == "bottom" && (
86
+ <div className="w-full">{chartImg}</div>
87
+ )}
88
+ </div>
89
+ </div>
90
+ {actionBtns && (
91
+ <CardAction className="flex flex-row-reverse items-center justify-between py-4 px-6 border-t border-rborder-secondary gap-1.5 w-full">
92
+ {actionBtns?.map((btn, index) => (
93
+ <React.Fragment key={index}>{btn}</React.Fragment>
94
+ ))}
95
+ </CardAction>
96
+ )}
97
+ </Card>
98
+ );
99
+ };
100
+
@@ -0,0 +1,93 @@
1
+ import type { StoryObj } from "@storybook/react-vite";
2
+ import { Meta } from "@storybook/react-vite";
3
+ import { Modal } from ".";
4
+ import { modalVariantProps } from "./_.types";
5
+
6
+ export default {
7
+ title: "Components/Modal",
8
+ component: Modal,
9
+ // tags: ["autodocs"],
10
+ argTypes: {
11
+ size: {
12
+ control: { type: "radio", defaultValue: "md" },
13
+ options: ["sm", "md", "lg"],
14
+ },
15
+ overflow: {
16
+ control: { type: "radio", defaultValue: "auto" },
17
+ options: ["hidden", "auto"],
18
+ },
19
+ position: {
20
+ control: { type: "radio", defaultValue: "center" },
21
+ options: ["center", "bottom"],
22
+ },
23
+ showModal: {
24
+ control: { type: "boolean", defaultValue: true },
25
+ },
26
+ modalTitle: {
27
+ control: { type: "text" },
28
+ },
29
+ onClick: {
30
+ control: { action: "close" },
31
+ }
32
+ },
33
+ args: {
34
+ size: "md",
35
+ overflow: "auto",
36
+ position: "center",
37
+ modalTitle: "Main Modal",
38
+ showModal: true,
39
+ modalContentStyle: "gap-4",
40
+ },
41
+ } as Meta<typeof Modal>;
42
+
43
+ type Story = StoryObj<modalVariantProps>;
44
+
45
+ export const Default: Story = {
46
+ args: {
47
+ children: "Modal Content",
48
+ overflow: "auto",
49
+ showModal: true,
50
+ },
51
+ };
52
+
53
+ export const Center = {
54
+ args: {
55
+ position: "center",
56
+ children: "Modal Content",
57
+ },
58
+ };
59
+
60
+ export const Bottom = {
61
+ args: {
62
+ position: "bottom",
63
+ children: "Modal Content",
64
+ },
65
+ };
66
+
67
+ export const WithTitle = {
68
+ args: {
69
+ modalTitle: "Modal with Title",
70
+ children: "Modal Content",
71
+ },
72
+ };
73
+
74
+ export const WithCustomCloseIcon = {
75
+ args: {
76
+ closeIconStroke: "red",
77
+ children: "Modal Content",
78
+ },
79
+ };
80
+
81
+ export const Size = {
82
+ args: {
83
+ size: "sm",
84
+ children: "Modal Content",
85
+ },
86
+ };
87
+
88
+ export const CloseIcon = {
89
+ args: {
90
+ showCloseIcon: false,
91
+ children: "Modal Content",
92
+ },
93
+ };
@@ -0,0 +1,31 @@
1
+ import { cva } from "class-variance-authority";
2
+
3
+ const baseStyles =
4
+ "modal-container fixed left-1/2 right-auto flex max-h-[80dvh] w-4/5 cursor-default flex-col rounded bg-white text-center";
5
+
6
+ const sizeStyle = {
7
+ sm: "max-w-rsm",
8
+ md: "max-w-rmd",
9
+ lg: "max-w-rlg",
10
+ }
11
+
12
+ export const modalVariants = cva(
13
+ baseStyles,
14
+ {
15
+ variants: {
16
+ size: sizeStyle,
17
+ overflow: {
18
+ hidden: "overflow-hidden",
19
+ auto: "overflow-auto",
20
+ },
21
+ position: {
22
+ center: "top-1/2 -translate-y-1/2 animate-slide-in-center",
23
+ bottom: "bottom-0 animate-slide-in-bottom"
24
+ }
25
+ },
26
+ defaultVariants: {
27
+ size: "md",
28
+ }
29
+ }
30
+ );
31
+
@@ -0,0 +1,90 @@
1
+ import { fireEvent, render, screen } from "@testing-library/react";
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
3
+ import { Modal } from ".";
4
+
5
+ // Mocking Radix Slot to make it render as span for testing purposes
6
+ vi.mock("@/icons/general/x-close", () => ({
7
+ default: () => <svg data-testid="x-close-icon" />,
8
+ }));
9
+ vi.mock("react-dom", async () => {
10
+ const actual = await vi.importActual<typeof import("react-dom")>("react-dom");
11
+ return {
12
+ ...actual,
13
+ default: actual, // ✅ add default for compatibility with ESM
14
+ createPortal: (node: React.ReactNode) => node,
15
+ };
16
+ });
17
+ vi.mock("@/lib/zIndexUtils", () => ({
18
+ getBackdropZIndex: (depth: number) => 40 + depth * 10,
19
+ getModalZIndex: (depth: number) => 50 + depth * 10,
20
+ }));
21
+
22
+ describe("Modal Component", () => {
23
+ const baseProps = {
24
+ showModal: true,
25
+ closeHandler: vi.fn(),
26
+ modalTitle: "Example Modal",
27
+ children: <div data-testid="modal-content">Test content</div>,
28
+ };
29
+
30
+ beforeEach(() => {
31
+ document.body.style.overflow = "";
32
+ });
33
+
34
+ afterEach(() => {
35
+ vi.clearAllMocks();
36
+ document.body.style.overflow = "";
37
+ });
38
+
39
+ it("renders modal correctly when showModal is true", () => {
40
+ render(<Modal {...baseProps} />);
41
+ expect(screen.getByRole("dialog")).toBeInTheDocument();
42
+ expect(screen.getByText("Example Modal")).toBeInTheDocument();
43
+ expect(screen.getByTestId("modal-content")).toBeInTheDocument();
44
+ });
45
+
46
+ it("does not render modal when showModal is false", () => {
47
+ render(<Modal {...baseProps} showModal={false} />);
48
+ expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
49
+ });
50
+
51
+ it("calls closeHandler on backdrop click", () => {
52
+ render(<Modal {...baseProps} />);
53
+ const backdrop = screen.getAllByRole("button");
54
+ fireEvent.click(backdrop[0]);
55
+ expect(baseProps.closeHandler).toHaveBeenCalled();
56
+ });
57
+
58
+ it("calls closeHandler on close icon click", () => {
59
+ render(<Modal {...baseProps} />);
60
+ const closeButton = screen.getByTestId("x-close-icon");
61
+ fireEvent.click(closeButton);
62
+ expect(baseProps.closeHandler).toHaveBeenCalled();
63
+ });
64
+
65
+ it("applies correct z-index from utility functions", () => {
66
+ render(<Modal {...baseProps} />);
67
+ const backdrop = screen.getAllByRole("button");
68
+ const dialog = screen.getByRole("dialog");
69
+ expect(backdrop[0]).toHaveStyle("z-index: 50");
70
+ expect(dialog).toHaveStyle("z-index: 60");
71
+ });
72
+
73
+ it("sets and resets document body overflow to prevent scrolling", () => {
74
+ const { unmount } = render(<Modal {...baseProps} />);
75
+ expect(document.body.style.overflow).toBe("hidden");
76
+ unmount();
77
+ expect(document.body.style.overflow).toBe("");
78
+ });
79
+
80
+ it("does not render close icon when showCloseIcon is false", () => {
81
+ render(<Modal {...baseProps} showCloseIcon={false} />);
82
+ expect(screen.queryByTestId("x-close-icon")).not.toBeInTheDocument();
83
+ });
84
+
85
+ it("renders close icon when showCloseIcon is true", () => {
86
+ render(<Modal {...baseProps} showCloseIcon={true} />);
87
+ expect(screen.getByTestId("x-close-icon")).toBeInTheDocument();
88
+ });
89
+
90
+ });
@@ -0,0 +1,14 @@
1
+ export type modalVariantProps = {
2
+ size?: "sm" | "md" | "lg";
3
+ overflow?: "hidden" | "auto";
4
+ closeHandler: () => void;
5
+ children: React.ReactNode;
6
+ modalTitle?: string;
7
+ closeIconStroke?: string;
8
+ position?: "center" | "bottom"
9
+ showModal: boolean,
10
+ icon?: React.ReactNode;
11
+ headerIconDir?: "flex-col" | "flex-row";
12
+ modalContentStyle?: string;
13
+ showCloseIcon?: boolean;
14
+ }