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,63 @@
1
+ import { render, screen } from "@testing-library/react";
2
+ import { describe, expect, it, vi } from "vitest";
3
+ import { PriceSlider } from ".";
4
+
5
+ // Mock ResizeObserver globally for tests
6
+ global.ResizeObserver = vi.fn().mockImplementation(() => ({
7
+ observe: vi.fn(),
8
+ unobserve: vi.fn(),
9
+ disconnect: vi.fn(),
10
+ }));
11
+
12
+ describe("Slider Component", () => {
13
+
14
+ it("applies custom className to root", () => {
15
+ render(<PriceSlider className="custom-slider" />);
16
+ const root = document.querySelector(".custom-slider");
17
+ expect(root).toBeInTheDocument();
18
+ });
19
+
20
+ it("applies custom className to track", () => {
21
+ render(<PriceSlider trackClassName="custom-track" />);
22
+ const track = document.querySelector(".custom-track");
23
+ expect(track).toBeInTheDocument();
24
+ });
25
+
26
+ it("applies custom className to range", () => {
27
+ render(<PriceSlider rangeClassName="custom-range" />);
28
+ const range = document.querySelector(".custom-range");
29
+ expect(range).toBeInTheDocument();
30
+ });
31
+
32
+ it("applies custom className to thumb", () => {
33
+ render(<PriceSlider thumbClassName="custom-thumb" />);
34
+ const thumb = document.querySelector(".custom-thumb");
35
+ expect(thumb).toBeInTheDocument();
36
+ });
37
+
38
+ it("renders the correct values", () => {
39
+ render(<PriceSlider defaultValue={[25, 75]} showValue={true} />);
40
+ const thumb = screen.getAllByRole("slider");
41
+ expect(thumb).toHaveLength(2); // Two thumbs should be rendered
42
+ const valueTexts = screen.getAllByText(/%?\d+/); // Look for any numeric values
43
+ expect(valueTexts).toHaveLength(2); // Should show two values
44
+ });
45
+
46
+ it("displays value as percentage when isPercentage is true", () => {
47
+ render(
48
+ <PriceSlider defaultValue={[25, 50]} showValue={true} isPercentage={true} />
49
+ );
50
+ const valueText = screen.getByText("٪25");
51
+ expect(valueText).toBeInTheDocument();
52
+ });
53
+
54
+ it("does not display value as percentage when isPercentage is false", () => {
55
+ render(
56
+ <PriceSlider defaultValue={[25, 50]} showValue={true} isPercentage={false} />
57
+ );
58
+ const valueText = screen.getByText("25");
59
+ expect(valueText).toBeInTheDocument();
60
+ });
61
+
62
+
63
+ });
@@ -0,0 +1,19 @@
1
+ import * as SliderPrimitive from "@radix-ui/react-slider";
2
+ import { ComponentPropsWithoutRef } from "react";
3
+
4
+ export interface SliderProps
5
+ extends ComponentPropsWithoutRef<typeof SliderPrimitive.Root> {
6
+ defaultValue?: [number, number];
7
+ className?: string;
8
+ trackClassName?: string;
9
+ rangeClassName?: string;
10
+ thumbClassName?: string;
11
+ showValue?: boolean;
12
+ valueClassName?: string;
13
+ isPercentage?: boolean;
14
+ valuePosition?: "top" | "bottom";
15
+ min?: number;
16
+ max?: number;
17
+ onValueChange?: (value: [number, number]) => void;
18
+ dir?: "ltr" | "rtl";
19
+ }
@@ -0,0 +1,86 @@
1
+ import { cn } from "@/lib/utils";
2
+ import * as SliderPrimitive from "@radix-ui/react-slider";
3
+ import * as React from "react";
4
+ import { SliderProps } from "./_.type";
5
+
6
+ export function PriceSlider({
7
+ defaultValue = [0, 1000],
8
+ min = 0,
9
+ max = 1000,
10
+ className = "",
11
+ trackClassName = "",
12
+ rangeClassName = "",
13
+ thumbClassName = "",
14
+ showValue = false,
15
+ valueClassName = "",
16
+ isPercentage = false,
17
+ valuePosition = "bottom",
18
+ dir = "ltr",
19
+ onValueChange,
20
+ ...props
21
+ }: Readonly<SliderProps>) {
22
+ const [values, setValues] = React.useState<number[]>(defaultValue);
23
+
24
+ const handleValueChange = (newValues: [number, number]) => {
25
+ const [newMin, newMax] = newValues;
26
+ const clampedMin = Math.max(min, Math.min(newMin, max));
27
+ const clampedMax = Math.min(max, Math.max(newMax, min));
28
+ const finalValues: [number, number] = [clampedMin, clampedMax];
29
+ setValues(finalValues);
30
+
31
+ if (onValueChange) {
32
+ onValueChange(finalValues);
33
+ }
34
+ };
35
+ return (
36
+ <SliderPrimitive.Root
37
+ value={values}
38
+ onValueChange={handleValueChange}
39
+ min={min}
40
+ max={max}
41
+ dir={dir}
42
+ className={cn(
43
+ "relative flex w-full touch-none select-none items-center",
44
+ className
45
+ )}
46
+ {...props}
47
+ >
48
+ <SliderPrimitive.Track
49
+ className={cn(
50
+ "relative h-2 w-full grow overflow-hidden rounded-full bg-gray-light-200",
51
+ trackClassName
52
+ )}
53
+ >
54
+ <SliderPrimitive.Range
55
+ className={cn(
56
+ "absolute h-full bg-rfg-brand-primary-600",
57
+ rangeClassName
58
+ )}
59
+ />
60
+ </SliderPrimitive.Track>
61
+
62
+ {values.map((value, index) => (
63
+ <SliderPrimitive.Thumb
64
+ key={index}
65
+ className={cn(
66
+ "relative block h-5 w-5 rounded-full border-2 border-rfg-brand-primary-600 bg-white",
67
+ thumbClassName
68
+ )}
69
+ >
70
+ {showValue && (
71
+ <span
72
+ className={cn(
73
+ "absolute left-1/2 -translate-x-1/2 text-xs text-gray-700",
74
+ valuePosition === "top" ? "bottom-7" : "top-7",
75
+ valueClassName
76
+ )}
77
+ style={{ whiteSpace: "nowrap" }}
78
+ >
79
+ {isPercentage ? `٪${value}` : value.toLocaleString()}
80
+ </span>
81
+ )}
82
+ </SliderPrimitive.Thumb>
83
+ ))}
84
+ </SliderPrimitive.Root>
85
+ );
86
+ }
@@ -0,0 +1,93 @@
1
+ import { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { Progress } from ".";
3
+ import { ProgressProps } from "./_.types";
4
+
5
+ export default {
6
+ title: "Components/Progress",
7
+ component: Progress,
8
+ tags: ["autodocs"],
9
+ argTypes: {
10
+ type: {
11
+ control: { type: "radio" },
12
+ options: ["line", "circle", "half-circle"],
13
+ },
14
+ size: {
15
+ control: { type: "radio" },
16
+ options: ["sm", "md", "lg"],
17
+ },
18
+ className: {
19
+ control: { type: "text" },
20
+ description: "Progress value class",
21
+ },
22
+ value: {
23
+ control: { type: "number" },
24
+ description: "Progress value in percentage",
25
+ defaultValue: 50,
26
+ },
27
+ label: {
28
+ control: { type: "number" },
29
+ description: "Label to show progress value as percentage",
30
+ defaultValue: 50,
31
+ },
32
+ labelClassName: {
33
+ control: { type: "text" },
34
+ description: "Custom class for the label",
35
+ },
36
+ backgroundVariant: {
37
+ control: { type: "text" },
38
+ description: "Custom background variant class",
39
+ },
40
+ indicatorVariant: {
41
+ control: { type: "text" },
42
+ description: "Custom indicator variant class",
43
+ },
44
+ strokeWidth: {
45
+ control: { type: "number" },
46
+ description: "Width of the stroke for circle/half-circle progress",
47
+ defaultValue: 16,
48
+ },
49
+ svgClassName: {
50
+ control: { type: "text" },
51
+ description: "Custom class for the SVG",
52
+ },
53
+ },
54
+ } as Meta<typeof Progress>;
55
+
56
+ type ProgressStory = StoryObj<ProgressProps>;
57
+
58
+ export const LineProgress: ProgressStory = {
59
+ args: {
60
+ type: "line",
61
+ value: 75,
62
+ label: 75,
63
+ labelClassName: "text-blue-500",
64
+ backgroundVariant: "bg-gray-200",
65
+ indicatorVariant: "bg-blue-500",
66
+ className: "w-full h-4",
67
+ svgClassName: "rotate-[-90deg]",
68
+ },
69
+ };
70
+
71
+ export const CircleProgress: ProgressStory = {
72
+ args: {
73
+ type: "circle",
74
+ value: 60,
75
+ label: 60,
76
+ labelClassName: "text-green-500",
77
+ strokeWidth: 12,
78
+ svgClassName: "rotate-[-90deg]",
79
+ indicatorVariant: "stroke-green-500",
80
+ },
81
+ };
82
+
83
+ export const HalfCircleProgress: ProgressStory = {
84
+ args: {
85
+ type: "half-circle",
86
+ value: 50,
87
+ label: 50,
88
+ labelClassName: "text-yellow-500",
89
+ strokeWidth: 20,
90
+ svgClassName: "rotate-[0deg]",
91
+ indicatorVariant: "stroke-rose-500",
92
+ },
93
+ };
@@ -0,0 +1,15 @@
1
+ export const circleSizeClasses = {
2
+ xxs: "w-[64px] h-[64px]",
3
+ xs: "w-[160px] h-[160px]",
4
+ sm: "w-[200px] h-[200px]",
5
+ md: "w-[240px] h-[240px]",
6
+ lg: "w-[280px] h-[280px]",
7
+ };
8
+
9
+ export const circlePixelMap: Record<string, number> = {
10
+ xxs: 64,
11
+ xs: 160,
12
+ sm: 200,
13
+ md: 240,
14
+ lg: 280,
15
+ };
@@ -0,0 +1,34 @@
1
+ import { render, screen } from "@testing-library/react";
2
+ import { describe, it, expect } from "vitest";
3
+ import { Progress } from ".";
4
+
5
+ describe("Progress Component", () => {
6
+ it("renders circle type progress", () => {
7
+ render(<Progress type="circle" value={75} label={75} size="lg" />);
8
+ expect(screen.getByText("75%")).toBeInTheDocument();
9
+
10
+ const circle = document.querySelector("svg");
11
+ expect(circle).toBeInTheDocument();
12
+ });
13
+
14
+ it("renders half-circle type progress", () => {
15
+ render(<Progress type="half-circle" value={25} label={25} size="sm" />);
16
+ expect(screen.getByText("25%")).toBeInTheDocument();
17
+
18
+ const halfCircle = document.querySelector("svg");
19
+ expect(halfCircle).toBeInTheDocument();
20
+ });
21
+
22
+ it("does not render label when label is undefined", () => {
23
+ render(<Progress type="line" value={50} />);
24
+ expect(screen.queryByText("%")).not.toBeInTheDocument();
25
+ });
26
+
27
+ it("applies custom classNames correctly", () => {
28
+ render(
29
+ <Progress type="line" value={50} className="test-class" label={50} />
30
+ );
31
+ const progressBar = screen.getByRole("progressbar");
32
+ expect(progressBar).toHaveClass("test-class");
33
+ });
34
+ });
@@ -0,0 +1,17 @@
1
+ import * as ProgressPrimitive from "@radix-ui/react-progress";
2
+ import { circleSizeClasses } from "./_.style";
3
+
4
+ export type ProgressProps = React.ComponentPropsWithoutRef<
5
+ typeof ProgressPrimitive.Root
6
+ > & {
7
+ value?: number;
8
+ label?: number;
9
+ labelClassName?: string;
10
+ backgroundVariant?: string;
11
+ indicatorVariant?: string;
12
+ type?: "line" | "circle" | "half-circle";
13
+ size?: keyof typeof circleSizeClasses;
14
+ strokeWidth?: number;
15
+ svgClassName?: string;
16
+ className?: string;
17
+ };
@@ -0,0 +1,173 @@
1
+ import * as ProgressPrimitive from "@radix-ui/react-progress";
2
+ import { cn } from "@/lib/utils";
3
+ import * as React from "react";
4
+ import { circlePixelMap, circleSizeClasses } from "./_.style";
5
+ import { ProgressProps } from "./_.types";
6
+
7
+ export const Progress = React.forwardRef<
8
+ React.ComponentRef<typeof ProgressPrimitive.Root>,
9
+ ProgressProps
10
+ >(
11
+ (
12
+ {
13
+ type = "line",
14
+ value = 0,
15
+ label,
16
+ labelClassName,
17
+ backgroundVariant,
18
+ indicatorVariant,
19
+ className,
20
+ size = "md",
21
+ strokeWidth = 16,
22
+ svgClassName,
23
+ ...props
24
+ },
25
+ ref
26
+ ) => {
27
+ const progress = Math.min(Math.max(value ?? 0, 0), 100);
28
+ const pxSize = circlePixelMap[size] || 240;
29
+ const radius = (pxSize - strokeWidth) / 2;
30
+
31
+ if (type === "half-circle") {
32
+ const circumference = Math.PI * radius;
33
+ const offset = circumference - (progress / 100) * circumference;
34
+ return (
35
+ <div
36
+ className={cn(
37
+ "relative flex items-center justify-center",
38
+ circleSizeClasses[size],
39
+ className
40
+ )}
41
+ ref={ref as React.Ref<HTMLDivElement>}
42
+ {...props}
43
+ >
44
+ <svg
45
+ width={pxSize}
46
+ height={pxSize / 2}
47
+ className={cn("rotate-[-90deg]", svgClassName)}
48
+ >
49
+ <circle
50
+ stroke="#e5e7eb"
51
+ fill="transparent"
52
+ strokeWidth={strokeWidth}
53
+ r={radius}
54
+ cx={pxSize / 2}
55
+ cy={pxSize / 2}
56
+ />
57
+ <circle
58
+ fill="transparent"
59
+ strokeWidth={strokeWidth}
60
+ strokeLinecap="round"
61
+ strokeDasharray={circumference}
62
+ strokeDashoffset={circumference - offset}
63
+ r={radius}
64
+ cx={pxSize / 2}
65
+ cy={pxSize / 2}
66
+ style={{ transition: "stroke-dashoffset 0.5s ease" }}
67
+ className={indicatorVariant}
68
+ />
69
+ </svg>
70
+ {label !== undefined && (
71
+ <span
72
+ className={cn(
73
+ "absolute text-sm font-semibold text-muted-foreground",
74
+ labelClassName
75
+ )}
76
+ >
77
+ {label}%
78
+ </span>
79
+ )}
80
+ </div>
81
+ );
82
+ }
83
+
84
+ if (type === "circle") {
85
+ const circumference = 2 * Math.PI * radius;
86
+ const offset = circumference - (progress / 100) * circumference;
87
+
88
+ return (
89
+ <div
90
+ className={cn(
91
+ "relative flex items-center justify-center",
92
+ circleSizeClasses[size],
93
+ className
94
+ )}
95
+ ref={ref as React.Ref<HTMLDivElement>}
96
+ {...props}
97
+ >
98
+ <svg
99
+ width={pxSize}
100
+ height={pxSize}
101
+ className={cn("rotate-[-90deg]", svgClassName)}
102
+ >
103
+ <circle
104
+ stroke="#e5e7eb"
105
+ fill="transparent"
106
+ strokeWidth={strokeWidth}
107
+ r={radius}
108
+ cx={pxSize / 2}
109
+ cy={pxSize / 2}
110
+ />
111
+ <circle
112
+ fill="transparent"
113
+ strokeWidth={strokeWidth}
114
+ strokeLinecap="round"
115
+ strokeDasharray={circumference}
116
+ strokeDashoffset={offset}
117
+ r={radius}
118
+ cx={pxSize / 2}
119
+ cy={pxSize / 2}
120
+ style={{ transition: "stroke-dashoffset 0.5s ease" }}
121
+ className={indicatorVariant}
122
+ />
123
+ </svg>
124
+ {label !== undefined && (
125
+ <span
126
+ className={cn(
127
+ "absolute text-sm font-semibold text-muted-foreground",
128
+ labelClassName
129
+ )}
130
+ >
131
+ {label}%
132
+ </span>
133
+ )}
134
+ </div>
135
+ );
136
+ }
137
+
138
+ return (
139
+ <div className="flex items-center gap-4 w-full">
140
+ <ProgressPrimitive.Root
141
+ ref={ref}
142
+ className={cn(
143
+ "relative h-4 w-full overflow-hidden rounded-full bg-muted",
144
+ backgroundVariant,
145
+ className
146
+ )}
147
+ {...props}
148
+ >
149
+ <ProgressPrimitive.Indicator
150
+ className={cn(
151
+ "h-full transition-transform duration-300",
152
+ indicatorVariant
153
+ )}
154
+ style={{ transform: `translateX(-${100 - value}%)` }}
155
+ />
156
+ </ProgressPrimitive.Root>
157
+
158
+ {typeof label === "number" && (
159
+ <span
160
+ className={cn(
161
+ "text-sm text-muted-foreground min-w-[40px] text-right",
162
+ labelClassName
163
+ )}
164
+ >
165
+ {label}%
166
+ </span>
167
+ )}
168
+ </div>
169
+ );
170
+ }
171
+ );
172
+
173
+ Progress.displayName = "Progress";