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,226 @@
1
+ import type { ComponentProps } from "react";
2
+ import type { Decorator, Meta, StoryObj } from "@storybook/react-vite";
3
+ import { ImageMagnify } from "./index";
4
+
5
+ type LayoutOption = "centered" | "left" | "right";
6
+ type ImageMagnifyStoryArgs = ComponentProps<typeof ImageMagnify> & {
7
+ layout: LayoutOption;
8
+ };
9
+
10
+ const withDynamicLayout: Decorator = (Story, context) => {
11
+ const layout = context.args?.layout ?? "centered";
12
+ const justifyContent =
13
+ layout === "left"
14
+ ? "flex-start"
15
+ : layout === "right"
16
+ ? "flex-end"
17
+ : "center";
18
+
19
+ return (
20
+ <div style={{ display: "flex", justifyContent, width: "100%" }}>
21
+ <div style={{ maxWidth: 420 }}>
22
+ <Story />
23
+ </div>
24
+ </div>
25
+ );
26
+ };
27
+
28
+ const meta: Meta<ImageMagnifyStoryArgs> = {
29
+ title: "Components/ImageMagnify",
30
+ component: ImageMagnify,
31
+ tags: ["autodocs"],
32
+ decorators: [withDynamicLayout],
33
+ parameters: {
34
+ docs: {
35
+ description: {
36
+ component:
37
+ "A magnifying glass component that allows users to zoom in on images with mouse hover or touch interactions.",
38
+ },
39
+ },
40
+ },
41
+ argTypes: {
42
+ src: {
43
+ control: "text",
44
+ description: "Image source URL",
45
+ },
46
+ alt: {
47
+ control: "text",
48
+ description: "Alt text for the image",
49
+ },
50
+ zoom: {
51
+ control: { type: "range", min: 1, max: 5, step: 0.5 },
52
+ description: "Zoom level multiplier",
53
+ },
54
+ layout: {
55
+ control: "inline-radio",
56
+ options: ["centered", "left", "right"],
57
+ description: "Horizontal alignment of the story canvas",
58
+ },
59
+ lensSize: {
60
+ control: { type: "range", min: 50, max: 300, step: 10 },
61
+ description: "Size of the magnifying lens in pixels",
62
+ },
63
+ zoomPanePosition: {
64
+ control: "inline-radio",
65
+ options: ["right", "left"],
66
+ description: "Position of the zoom pane relative to the source image",
67
+ },
68
+ zoomMode: {
69
+ control: "inline-radio",
70
+ options: ["pane", "inline"],
71
+ description: "Display mode: standalone zoom pane or inline lens",
72
+ },
73
+ touchToggle: {
74
+ control: "boolean",
75
+ description: "Enable touch toggle on mobile",
76
+ },
77
+ zoomPaneSize: {
78
+ control: { type: "range", min: 100, max: 800, step: 50 },
79
+ description: "Custom zoom pane size in pixels",
80
+ },
81
+ className: {
82
+ control: "text",
83
+ description: "Additional CSS class name",
84
+ },
85
+ },
86
+ args: {
87
+ layout: "centered",
88
+ },
89
+ };
90
+
91
+ export default meta;
92
+ type Story = StoryObj<ImageMagnifyStoryArgs>;
93
+
94
+ // Sample images for stories
95
+ const sampleImage =
96
+ "https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=400&h=300&fit=crop";
97
+
98
+ export const Default: Story = {
99
+ args: {
100
+ src: sampleImage,
101
+ alt: "Sample image for magnification",
102
+ zoom: 2,
103
+ lensSize: 140,
104
+ touchToggle: true,
105
+ zoomMode: "pane",
106
+ zoomPanePosition: "right",
107
+ },
108
+ };
109
+
110
+ export const HighZoom: Story = {
111
+ args: {
112
+ src: sampleImage,
113
+ alt: "High zoom magnification",
114
+ zoom: 3,
115
+ lensSize: 120,
116
+ touchToggle: true,
117
+ zoomMode: "pane",
118
+ zoomPanePosition: "right",
119
+ },
120
+ };
121
+
122
+ export const LargeLens: Story = {
123
+ args: {
124
+ src: sampleImage,
125
+ alt: "Large lens magnification",
126
+ zoom: 2,
127
+ lensSize: 200,
128
+ touchToggle: true,
129
+ zoomMode: "pane",
130
+ zoomPanePosition: "right",
131
+ },
132
+ };
133
+
134
+ export const LeftPosition: Story = {
135
+ args: {
136
+ src: sampleImage,
137
+ alt: "Zoom pane on the left",
138
+ zoom: 2,
139
+ lensSize: 140,
140
+ touchToggle: true,
141
+ zoomMode: "pane",
142
+ zoomPanePosition: "left",
143
+ },
144
+ };
145
+
146
+ export const CustomZoomPaneSize: Story = {
147
+ args: {
148
+ src: sampleImage,
149
+ alt: "Custom zoom pane size",
150
+ zoom: 2.5,
151
+ lensSize: 140,
152
+ touchToggle: true,
153
+ zoomPaneSize: 300,
154
+ zoomMode: "pane",
155
+ zoomPanePosition: "right",
156
+ },
157
+ };
158
+
159
+ export const InlineLens: Story = {
160
+ args: {
161
+ src: sampleImage,
162
+ alt: "Inline lens magnification",
163
+ zoom: 2.5,
164
+ lensSize: 160,
165
+ touchToggle: true,
166
+ zoomMode: "inline",
167
+ zoomPanePosition: "right",
168
+ },
169
+ };
170
+
171
+ export const NoTouchToggle: Story = {
172
+ args: {
173
+ src: sampleImage,
174
+ alt: "No touch toggle",
175
+ zoom: 2,
176
+ lensSize: 140,
177
+ touchToggle: false,
178
+ zoomMode: "pane",
179
+ zoomPanePosition: "right",
180
+ },
181
+ };
182
+
183
+ export const SmallLens: Story = {
184
+ args: {
185
+ src: sampleImage,
186
+ alt: "Small lens for precision",
187
+ zoom: 3,
188
+ lensSize: 80,
189
+ touchToggle: true,
190
+ zoomMode: "pane",
191
+ zoomPanePosition: "right",
192
+ },
193
+ };
194
+
195
+ export const WithCustomClass: Story = {
196
+ args: {
197
+ src: sampleImage,
198
+ alt: "With custom styling",
199
+ zoom: 2,
200
+ lensSize: 140,
201
+ touchToggle: true,
202
+ className: "border-2 border-blue-500 rounded-lg p-4",
203
+ zoomMode: "pane",
204
+ zoomPanePosition: "right",
205
+ },
206
+ };
207
+
208
+ export const Interactive: Story = {
209
+ args: {
210
+ src: sampleImage,
211
+ alt: "Interactive magnification",
212
+ zoom: 2,
213
+ lensSize: 140,
214
+ touchToggle: true,
215
+ zoomMode: "pane",
216
+ zoomPanePosition: "right",
217
+ },
218
+ parameters: {
219
+ docs: {
220
+ description: {
221
+ story:
222
+ "Hover over the image to see the magnification effect. On mobile devices, tap to toggle the zoom.",
223
+ },
224
+ },
225
+ },
226
+ };
@@ -0,0 +1,109 @@
1
+ // Tailwind CSS classes for ImageMagnify component
2
+
3
+ export const imageMagnifyClasses = {
4
+ // Main container
5
+ container: 'flex items-start gap-3',
6
+
7
+ // Image container
8
+ imageContainer: 'relative select-none touch-none',
9
+
10
+ // Image element
11
+ image: 'block max-w-full h-auto select-none',
12
+
13
+ // Lens styles
14
+ lens: 'pointer-events-none absolute rounded-full border-2 border-white shadow-lg bg-transparent z-10',
15
+ lensAnimated: 'pointer-events-none absolute rounded-full border-2 border-white shadow-lg bg-transparent z-10 transition-opacity duration-200 ease-in-out',
16
+
17
+ // Lens variants
18
+ lensGlow: 'pointer-events-none absolute rounded-full border-2 border-white shadow-lg bg-transparent z-10 shadow-[0_0_0_2px_white,0_0_20px_rgba(59,130,246,0.5),0_4px_6px_-1px_rgba(0,0,0,0.1)]',
19
+ lensDashed: 'pointer-events-none absolute rounded-full border-2 border-dashed border-white bg-white/10 z-10',
20
+
21
+ // Zoom pane
22
+ zoomPane: 'overflow-hidden rounded-lg border border-slate-200 shadow-sm relative flex-shrink-0',
23
+ zoomPaneAnimated: 'overflow-hidden rounded-lg border border-slate-200 shadow-sm relative flex-shrink-0 transition-opacity duration-200 ease-in-out',
24
+
25
+ // Zoom pane variants
26
+ zoomPaneRounded: 'overflow-hidden rounded-2xl border border-slate-200 shadow-sm relative flex-shrink-0',
27
+ zoomPaneSharp: 'overflow-hidden border border-slate-200 shadow-sm relative flex-shrink-0',
28
+ zoomPaneGlow: 'overflow-hidden rounded-lg border border-slate-200 shadow-sm relative flex-shrink-0 shadow-[0_4px_6px_-1px_rgba(0,0,0,0.1),0_2px_4px_-1px_rgba(0,0,0,0.06),0_0_0_1px_rgba(59,130,246,0.2)]',
29
+
30
+ // Responsive container
31
+ responsiveContainer: 'flex items-start gap-3 md:flex-row flex-col md:items-start items-center gap-4',
32
+
33
+ // Hidden/visible states
34
+ hidden: 'hidden',
35
+ visible: 'block',
36
+
37
+ // Touch action
38
+ touchNone: 'touch-none',
39
+
40
+ // Image drag prevention
41
+ noDrag: 'select-none',
42
+
43
+ // Lens positioning utilities
44
+ lensPosition: (x: number, y: number, size: number) => ({
45
+ left: Math.max(0, x - size / 2),
46
+ top: Math.max(0, y - size / 2),
47
+ width: size,
48
+ height: size,
49
+ }),
50
+
51
+ // Zoom pane background utilities
52
+ zoomPaneBackground: (imageUrl: string, size: string, position: string) => ({
53
+ backgroundImage: `url(${imageUrl})`,
54
+ backgroundRepeat: 'no-repeat',
55
+ backgroundSize: size,
56
+ backgroundPosition: position,
57
+ }),
58
+ };
59
+
60
+ // Utility functions for dynamic styles
61
+ export const getLensStyle = (
62
+ lensSize: number,
63
+ lensPos: { x: number; y: number },
64
+ visible: boolean
65
+ ) => ({
66
+ width: lensSize,
67
+ height: lensSize,
68
+ left: Math.max(0, lensPos.x - lensSize / 2),
69
+ top: Math.max(0, lensPos.y - lensSize / 2),
70
+ display: visible ? 'block' : 'none',
71
+ });
72
+
73
+ export const getZoomPaneStyle = (
74
+ width: number,
75
+ height: number,
76
+ backgroundImage: string,
77
+ backgroundSize: string,
78
+ backgroundPosition: string,
79
+ visible: boolean
80
+ ) => ({
81
+ width,
82
+ height,
83
+ backgroundImage: `url(${backgroundImage})`,
84
+ backgroundRepeat: 'no-repeat',
85
+ backgroundSize,
86
+ backgroundPosition,
87
+ display: visible ? 'block' : 'none',
88
+ });
89
+
90
+ // Predefined class combinations for common use cases
91
+ export const lensVariants = {
92
+ default: imageMagnifyClasses.lens,
93
+ animated: imageMagnifyClasses.lensAnimated,
94
+ glow: imageMagnifyClasses.lensGlow,
95
+ dashed: imageMagnifyClasses.lensDashed,
96
+ };
97
+
98
+ export const zoomPaneVariants = {
99
+ default: imageMagnifyClasses.zoomPane,
100
+ animated: imageMagnifyClasses.zoomPaneAnimated,
101
+ rounded: imageMagnifyClasses.zoomPaneRounded,
102
+ sharp: imageMagnifyClasses.zoomPaneSharp,
103
+ glow: imageMagnifyClasses.zoomPaneGlow,
104
+ };
105
+
106
+ export const containerVariants = {
107
+ default: imageMagnifyClasses.container,
108
+ responsive: imageMagnifyClasses.responsiveContainer,
109
+ };
@@ -0,0 +1,44 @@
1
+ export interface ImageMagnifyProps {
2
+ /** Image source URL */
3
+ src: string;
4
+ /** Alt text for the image */
5
+ alt?: string;
6
+ /** Zoom level multiplier (e.g., 2 = 200% zoom) */
7
+ zoom?: number;
8
+ /** Size of the magnifying lens in pixels (square) */
9
+ lensSize?: number;
10
+ /** Additional CSS class name */
11
+ className?: string;
12
+ /** Tailwind classes applied when lens is visible */
13
+ lensVisibleClassName?: string;
14
+ /** Tailwind classes applied when zoom pane is visible */
15
+ zoomPaneVisibleClassName?: string;
16
+ /** Position of the zoom pane relative to the image */
17
+ zoomPanePosition?: 'right' | 'left';
18
+ /** Enable touch toggle functionality on mobile devices */
19
+ touchToggle?: boolean;
20
+ /** Custom zoom pane size in pixels (optional) */
21
+ zoomPaneSize?: number;
22
+ /** Zoom mode: 'pane' for separate zoom pane, 'inline' for zoom directly on image */
23
+ zoomMode?: 'pane' | 'inline';
24
+ }
25
+
26
+ export interface LensPosition {
27
+ x: number;
28
+ y: number;
29
+ }
30
+
31
+ export interface ImageSize {
32
+ w: number;
33
+ h: number;
34
+ }
35
+
36
+ export interface ImageMagnifyState {
37
+ visible: boolean;
38
+ lensPos: LensPosition;
39
+ imgSize: ImageSize;
40
+ isTouchZoom: boolean;
41
+ }
42
+
43
+ export type ZoomPanePosition = 'right' | 'left';
44
+ export type ZoomMode = 'pane' | 'inline';
@@ -0,0 +1,204 @@
1
+ import React, { useEffect, useRef, useState } from "react";
2
+ import type { ImageMagnifyProps, LensPosition } from "./_.types";
3
+
4
+ const clamp = (value: number, min: number, max: number) =>
5
+ Math.min(Math.max(value, min), max);
6
+
7
+ const defaultLensPosition: LensPosition = { x: 0, y: 0 };
8
+
9
+ export function ImageMagnify({
10
+ src,
11
+ alt = "",
12
+ zoom = 2,
13
+ lensSize = 140,
14
+ className = "",
15
+ lensVisibleClassName = "hidden md:block",
16
+ zoomPaneVisibleClassName = "hidden md:block",
17
+ touchToggle = true,
18
+ zoomPaneSize,
19
+ zoomPanePosition = "right",
20
+ zoomMode = "pane",
21
+ }: ImageMagnifyProps) {
22
+ const containerRef = useRef<HTMLDivElement | null>(null);
23
+ const imgRef = useRef<HTMLImageElement | null>(null);
24
+
25
+ const [visible, setVisible] = useState(false);
26
+ const [lensPos, setLensPos] = useState<LensPosition>(defaultLensPosition);
27
+ const [imgSize, setImgSize] = useState({ w: 0, h: 0 });
28
+ const [isTouchZoom, setIsTouchZoom] = useState(false);
29
+
30
+ useEffect(() => {
31
+ const update = () => {
32
+ const img = imgRef.current;
33
+ if (!img) return;
34
+ setImgSize({ w: img.width, h: img.height });
35
+ };
36
+
37
+ const img = imgRef.current;
38
+ if (!img) return;
39
+
40
+ if (img.complete) update();
41
+ img.addEventListener("load", update);
42
+ window.addEventListener("resize", update);
43
+
44
+ return () => {
45
+ img.removeEventListener("load", update);
46
+ window.removeEventListener("resize", update);
47
+ };
48
+ }, [src]);
49
+
50
+ const getRelativePosition = (
51
+ clientX: number,
52
+ clientY: number
53
+ ): LensPosition => {
54
+ const img = imgRef.current;
55
+ if (!img) return defaultLensPosition;
56
+
57
+ const rect = img.getBoundingClientRect();
58
+ if (rect.width === 0 || rect.height === 0) return defaultLensPosition;
59
+
60
+ const x = clamp(clientX - rect.left, 0, rect.width);
61
+ const y = clamp(clientY - rect.top, 0, rect.height);
62
+
63
+ return { x, y };
64
+ };
65
+
66
+ const updateLensFromEvent = (event: React.MouseEvent | React.TouchEvent) => {
67
+ const nativeEvent =
68
+ "nativeEvent" in event ? (event.nativeEvent as any) : (event as any);
69
+
70
+ if ("touches" in nativeEvent && nativeEvent.touches?.length > 0) {
71
+ const touch = nativeEvent.touches[0];
72
+ setLensPos(getRelativePosition(touch.clientX, touch.clientY));
73
+ return;
74
+ }
75
+
76
+ if (typeof nativeEvent.clientX === "number") {
77
+ setLensPos(getRelativePosition(nativeEvent.clientX, nativeEvent.clientY));
78
+ }
79
+ };
80
+
81
+ const handleMove = (event: React.MouseEvent<HTMLDivElement>) => {
82
+ updateLensFromEvent(event);
83
+ };
84
+
85
+ const handleEnter = () => {
86
+ setVisible(true);
87
+ };
88
+
89
+ const handleLeave = () => {
90
+ setVisible(false);
91
+ if (isTouchZoom) setIsTouchZoom(false);
92
+ };
93
+
94
+ const handleTouchStart = (event: React.TouchEvent<HTMLDivElement>) => {
95
+ if (!touchToggle) return;
96
+
97
+ event.preventDefault();
98
+
99
+ if (!isTouchZoom) {
100
+ setIsTouchZoom(true);
101
+ setVisible(true);
102
+ updateLensFromEvent(event);
103
+ } else {
104
+ setIsTouchZoom(false);
105
+ setVisible(false);
106
+ }
107
+ };
108
+
109
+ const handleTouchMove = (event: React.TouchEvent<HTMLDivElement>) => {
110
+ if (!isTouchZoom) return;
111
+ event.preventDefault();
112
+ updateLensFromEvent(event);
113
+ };
114
+
115
+ const currentRect = imgRef.current?.getBoundingClientRect();
116
+ const displayW = currentRect?.width ?? imgSize.w;
117
+ const displayH = currentRect?.height ?? imgSize.h;
118
+ const safeDisplayW = displayW || 1;
119
+ const safeDisplayH = displayH || 1;
120
+
121
+ const backgroundSize = `${safeDisplayW * zoom}px ${safeDisplayH * zoom}px`;
122
+ const backgroundPosition = `${(lensPos.x / safeDisplayW) * 100}% ${
123
+ (lensPos.y / safeDisplayH) * 100
124
+ }%`;
125
+
126
+ const paneWidth = zoomPaneSize ?? safeDisplayW;
127
+ const paneHeight = zoomPaneSize ?? safeDisplayH;
128
+
129
+ const lensStyle: React.CSSProperties = {
130
+ width: lensSize,
131
+ height: lensSize,
132
+ left: clamp(
133
+ lensPos.x - lensSize / 2,
134
+ 0,
135
+ Math.max(safeDisplayW - lensSize, 0)
136
+ ),
137
+ top: clamp(
138
+ lensPos.y - lensSize / 2,
139
+ 0,
140
+ Math.max(safeDisplayH - lensSize, 0)
141
+ ),
142
+ backgroundImage: zoomMode === "inline" ? `url(${src})` : "none",
143
+ backgroundRepeat: "no-repeat",
144
+ backgroundSize: zoomMode === "inline" ? backgroundSize : undefined,
145
+ backgroundPosition: zoomMode === "inline" ? backgroundPosition : undefined,
146
+ };
147
+
148
+ const zoomPaneStyle: React.CSSProperties = {
149
+ width: paneWidth,
150
+ height: paneHeight,
151
+ backgroundImage: `url(${src})`,
152
+ backgroundRepeat: "no-repeat",
153
+ backgroundSize,
154
+ backgroundPosition,
155
+ };
156
+
157
+ const isPaneLeft = zoomPanePosition === "left";
158
+ const lensVisibilityClass = visible ? lensVisibleClassName : "hidden";
159
+ const zoomPaneVisibilityClass =
160
+ zoomMode === "pane" && visible ? zoomPaneVisibleClassName : "hidden";
161
+
162
+ return (
163
+ <div className={`relative w-fit inline-block align-top ${className}`}>
164
+ <div
165
+ ref={containerRef}
166
+ className="relative select-none touch-none"
167
+ onMouseMove={handleMove}
168
+ onMouseEnter={handleEnter}
169
+ onMouseLeave={handleLeave}
170
+ onTouchStart={handleTouchStart}
171
+ onTouchMove={handleTouchMove}
172
+ onTouchEnd={handleLeave}
173
+ >
174
+ <img
175
+ ref={imgRef}
176
+ src={src}
177
+ alt={alt}
178
+ className="block max-w-full h-auto select-none"
179
+ draggable={false}
180
+ onDragStart={(event) => event.preventDefault()}
181
+ />
182
+
183
+ <div
184
+ className={`pointer-events-none absolute rounded-full border-2 border-white shadow-lg ${lensVisibilityClass} ${
185
+ zoomMode === "inline"
186
+ ? "bg-white/10 backdrop-blur"
187
+ : "bg-transparent"
188
+ }`}
189
+ style={lensStyle}
190
+ />
191
+ </div>
192
+
193
+ {/* {zoomMode === "pane" && ( */}
194
+ <div
195
+ // aria-hidden={!visible}
196
+ className={`${zoomPaneVisibilityClass} absolute top-1/2 -translate-y-1/2 overflow-hidden rounded-lg border border-slate-200 shadow-sm
197
+ ${isPaneLeft ? "right-full mr-3" : "left-full ml-3"}
198
+ `}
199
+ style={zoomPaneStyle}
200
+ />
201
+ {/* )} */}
202
+ </div>
203
+ );
204
+ }