@akinon/pz-list-hover-image 2.0.6-rc.2

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.
package/.prettierrc ADDED
@@ -0,0 +1,13 @@
1
+ {
2
+ "bracketSameLine": false,
3
+ "tabWidth": 2,
4
+ "singleQuote": true,
5
+ "jsxSingleQuote": false,
6
+ "bracketSpacing": true,
7
+ "semi": true,
8
+ "useTabs": false,
9
+ "arrowParens": "always",
10
+ "endOfLine": "lf",
11
+ "proseWrap": "never",
12
+ "trailingComma": "none"
13
+ }
package/CHANGELOG.md ADDED
@@ -0,0 +1,3 @@
1
+ # @akinon/pz-list-hover-image
2
+
3
+ ## 2.0.6-rc.2
package/README.md ADDED
@@ -0,0 +1,369 @@
1
+ # @akinon/pz-list-hover-image
2
+
3
+ A plugin that adds hover-to-switch image functionality to product cards on listing pages. When users hover over a product image, different product images are displayed with dot indicators at the bottom showing the currently active image.
4
+
5
+ The plugin accepts only the data it needs (`images`, `href`, `alt`) instead of the entire `Product` object, preventing unnecessary DOM bloat and data serialization.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npx @akinon/projectzero@latest --plugins
11
+ ```
12
+
13
+ After selecting the plugin, make sure it is added to `plugins.js`:
14
+
15
+ ```js
16
+ // src/plugins.js
17
+ module.exports = [
18
+ // ...other plugins
19
+ 'pz-list-hover-image'
20
+ ];
21
+ ```
22
+
23
+ ## Basic Usage
24
+
25
+ ### With PluginModule (Recommended)
26
+
27
+ ```tsx
28
+ import PluginModule, { Component } from '@akinon/next/components/plugin-module';
29
+
30
+ <PluginModule
31
+ component={Component.ListHoverImage}
32
+ props={{
33
+ images: product.productimage_set,
34
+ href: product.absolute_url,
35
+ alt: product.name,
36
+ fill: true,
37
+ sizes: '(max-width: 768px) 50vw, 33vw',
38
+ aspectRatio: 3 / 4
39
+ }}
40
+ >
41
+ {/* Fallback rendered when plugin is disabled */}
42
+ <Image src={product.productimage_set[0]?.image} alt={product.name} />
43
+ </PluginModule>
44
+ ```
45
+
46
+ ### Direct Import
47
+
48
+ ```tsx
49
+ import { ListHoverImage } from '@akinon/pz-list-hover-image';
50
+
51
+ <ListHoverImage
52
+ images={product.productimage_set}
53
+ href={product.absolute_url}
54
+ alt={product.name}
55
+ fill
56
+ sizes="(max-width: 768px) 50vw, 33vw"
57
+ aspectRatio={3 / 4}
58
+ />
59
+ ```
60
+
61
+ ## Props
62
+
63
+ ### `ListHoverImageProps`
64
+
65
+ | Property | Type | Required | Default | Description |
66
+ |---|---|---|---|---|
67
+ | `images` | `ProductImage[]` | Yes | - | Product image array. Each element must contain at least `{ image: string }`. You can pass `product.productimage_set` directly. |
68
+ | `href` | `string` | Yes | - | Product detail page URL. Hover zones link to this URL. |
69
+ | `alt` | `string` | Yes | - | Image alt text for accessibility. |
70
+ | `width` | `number` | No | - | Image width in pixels. Required when `fill` is not used. |
71
+ | `height` | `number` | No | - | Image height in pixels. Required when `fill` is not used. |
72
+ | `fill` | `boolean` | No | `false` | When `true`, the image fills its parent container. Must be used with `sizes` and `aspectRatio`. |
73
+ | `sizes` | `string` | No | - | Responsive image sizes attribute. Required in `fill` mode. |
74
+ | `aspectRatio` | `number` | No | - | Image aspect ratio. Required in `fill` mode. |
75
+ | `maxImages` | `number` | No | `3` | Maximum number of images to display. |
76
+ | `fallbackImage` | `string` | No | Akinon noimage URL | Fallback image URL shown when the product has no images. |
77
+ | `className` | `string` | No | - | CSS class added to the root container. |
78
+ | `settings` | `ListHoverImageSettings` | No | `defaultSettings` | Detailed configuration object. |
79
+
80
+ ### `ProductImage`
81
+
82
+ ```ts
83
+ interface ProductImage {
84
+ image: string;
85
+ [key: string]: any;
86
+ }
87
+ ```
88
+
89
+ `product.productimage_set` is compatible with this interface and can be passed directly.
90
+
91
+ ## Settings
92
+
93
+ Use the `settings` prop to customize the plugin's behavior and appearance in detail.
94
+
95
+ ### `ListHoverImageSettings`
96
+
97
+ ```ts
98
+ interface ListHoverImageSettings {
99
+ maxImages?: number;
100
+ fallbackImage?: string;
101
+ customStyles?: ListHoverImageCustomStyles;
102
+ customRenderers?: ListHoverImageCustomRenderers;
103
+ theme?: {
104
+ indicatorSize?: string;
105
+ indicatorActiveColor?: string;
106
+ indicatorBorderColor?: string;
107
+ indicatorBorderRadius?: string;
108
+ };
109
+ }
110
+ ```
111
+
112
+ ### Usage with Settings
113
+
114
+ #### Via settings.js (Global)
115
+
116
+ ```js
117
+ // src/settings.js
118
+ module.exports = {
119
+ plugins: {
120
+ 'pz-list-hover-image': {
121
+ maxImages: 5,
122
+ customStyles: {
123
+ indicatorActive: 'bg-black'
124
+ }
125
+ }
126
+ }
127
+ };
128
+ ```
129
+
130
+ #### As a Prop (Per Component)
131
+
132
+ ```tsx
133
+ <ListHoverImage
134
+ images={product.productimage_set}
135
+ href={product.absolute_url}
136
+ alt={product.name}
137
+ fill
138
+ sizes="(max-width: 768px) 50vw, 33vw"
139
+ aspectRatio={3 / 4}
140
+ settings={{
141
+ maxImages: 4,
142
+ fallbackImage: '/custom-noimage.jpg',
143
+ theme: {
144
+ indicatorActiveColor: 'bg-black',
145
+ indicatorBorderColor: 'border-gray-300',
146
+ indicatorBorderRadius: 'rounded-sm'
147
+ }
148
+ }}
149
+ />
150
+ ```
151
+
152
+ ## Custom Styles
153
+
154
+ Use `customStyles` to override CSS classes on each sub-element. All classes are merged via `tailwind-merge`, so you can safely override defaults.
155
+
156
+ ### `ListHoverImageCustomStyles`
157
+
158
+ | Property | Target | Default |
159
+ |---|---|---|
160
+ | `container` | Root wrapper `div` | `relative` |
161
+ | `imageWrapper` | Wrapper `div` around the image | `relative` |
162
+ | `image` | `Image` component | - |
163
+ | `hoverZoneContainer` | Wrapper `div` for hover zones | `absolute inset-0 flex cursor-pointer justify-between` |
164
+ | `hoverZone` | Each hover zone (`Link`) | `relative w-full` |
165
+ | `indicatorContainer` | Indicator wrapper `div` | `absolute bottom-4 left-0 flex w-full justify-center` |
166
+ | `indicatorList` | Dot list `ul` | `flex gap-1.5 lg:gap-2` |
167
+ | `indicator` | Inactive dot `li` | `w-2.5 h-2.5 lg:w-3.5 lg:h-3.5 border border-white rounded-full` |
168
+ | `indicatorActive` | Active dot `li` | `bg-secondary border-transparent` |
169
+
170
+ ### Example: Custom Styles
171
+
172
+ ```tsx
173
+ <ListHoverImage
174
+ images={product.productimage_set}
175
+ href={product.absolute_url}
176
+ alt={product.name}
177
+ width={300}
178
+ height={400}
179
+ settings={{
180
+ customStyles: {
181
+ container: 'rounded-xl overflow-hidden shadow-lg',
182
+ indicatorContainer: 'absolute bottom-2 left-0 flex w-full justify-center',
183
+ indicatorList: 'flex gap-1 bg-black/30 rounded-full px-2 py-1',
184
+ indicator: 'w-2 h-2 rounded-full bg-white/50',
185
+ indicatorActive: 'w-2 h-2 rounded-full bg-white'
186
+ }
187
+ }}
188
+ />
189
+ ```
190
+
191
+ ## Custom Renderers
192
+
193
+ For advanced customization, use `customRenderers` to completely replace component parts with your own render functions.
194
+
195
+ ### `ListHoverImageCustomRenderers`
196
+
197
+ | Property | Description | Props |
198
+ |---|---|---|
199
+ | `renderImage` | Completely overrides the image component | `{ src, alt, width, height, fill, sizes, aspectRatio }` |
200
+ | `renderIndicator` | Overrides the entire indicator section | `{ count, activeIndex, onSelect }` |
201
+ | `renderIndicatorDot` | Overrides a single dot | `{ index, isActive, onClick }` |
202
+
203
+ ### Example: Custom Image Renderer
204
+
205
+ ```tsx
206
+ <ListHoverImage
207
+ images={product.productimage_set}
208
+ href={product.absolute_url}
209
+ alt={product.name}
210
+ fill
211
+ sizes="50vw"
212
+ aspectRatio={1}
213
+ settings={{
214
+ customRenderers: {
215
+ renderImage: ({ src, alt }) => (
216
+ <img
217
+ src={src}
218
+ alt={alt}
219
+ className="w-full h-full object-cover"
220
+ loading="lazy"
221
+ />
222
+ )
223
+ }
224
+ }}
225
+ />
226
+ ```
227
+
228
+ ### Example: Custom Indicator Dots (Bar Style)
229
+
230
+ ```tsx
231
+ <ListHoverImage
232
+ images={product.productimage_set}
233
+ href={product.absolute_url}
234
+ alt={product.name}
235
+ width={300}
236
+ height={400}
237
+ settings={{
238
+ customRenderers: {
239
+ renderIndicatorDot: ({ index, isActive, onClick }) => (
240
+ <button
241
+ onClick={onClick}
242
+ className={`w-6 h-1 rounded-full transition-all ${
243
+ isActive ? 'bg-primary w-10' : 'bg-gray-300'
244
+ }`}
245
+ aria-label={`Image ${index + 1}`}
246
+ />
247
+ )
248
+ }
249
+ }}
250
+ />
251
+ ```
252
+
253
+ ### Example: Counter Indicator (1/3 Style)
254
+
255
+ ```tsx
256
+ <ListHoverImage
257
+ images={product.productimage_set}
258
+ href={product.absolute_url}
259
+ alt={product.name}
260
+ width={300}
261
+ height={400}
262
+ settings={{
263
+ customRenderers: {
264
+ renderIndicator: ({ count, activeIndex }) => (
265
+ <div className="absolute bottom-2 right-2 bg-black/60 text-white text-xs rounded px-2 py-1">
266
+ {activeIndex + 1} / {count}
267
+ </div>
268
+ )
269
+ }
270
+ }}
271
+ />
272
+ ```
273
+
274
+ ## Image Modes
275
+
276
+ The plugin supports two image modes:
277
+
278
+ ### Fixed Dimensions (width/height)
279
+
280
+ Use with fixed width and height values:
281
+
282
+ ```tsx
283
+ <ListHoverImage
284
+ images={product.productimage_set}
285
+ href={product.absolute_url}
286
+ alt={product.name}
287
+ width={300}
288
+ height={400}
289
+ />
290
+ ```
291
+
292
+ ### Responsive Fill Mode
293
+
294
+ Fills the parent container responsively:
295
+
296
+ ```tsx
297
+ <div className="w-full">
298
+ <ListHoverImage
299
+ images={product.productimage_set}
300
+ href={product.absolute_url}
301
+ alt={product.name}
302
+ fill
303
+ sizes="(max-width: 768px) 50vw, (max-width: 1024px) 30vw, 25vw"
304
+ aspectRatio={3 / 4}
305
+ />
306
+ </div>
307
+ ```
308
+
309
+ ## Full Integration Example
310
+
311
+ Full integration within a product card:
312
+
313
+ ```tsx
314
+ 'use client';
315
+
316
+ import { Product } from '@akinon/next/types';
317
+ import { Price, Link } from '@theme/components';
318
+ import { Image } from '@akinon/next/components/image';
319
+ import PluginModule, { Component } from '@akinon/next/components/plugin-module';
320
+
321
+ interface ProductCardProps {
322
+ product: Product;
323
+ }
324
+
325
+ export function ProductCard({ product }: ProductCardProps) {
326
+ const imageUrl = product.productimage_set[0]?.image;
327
+
328
+ return (
329
+ <div className="group">
330
+ <div className="relative mb-3 overflow-hidden rounded-lg">
331
+ <PluginModule
332
+ component={Component.ListHoverImage}
333
+ props={{
334
+ images: product.productimage_set,
335
+ href: product.absolute_url,
336
+ alt: product.name,
337
+ fill: true,
338
+ sizes: '(max-width: 768px) 50vw, 33vw',
339
+ aspectRatio: 3 / 4,
340
+ settings: {
341
+ maxImages: 4,
342
+ customStyles: {
343
+ container: 'rounded-lg overflow-hidden',
344
+ indicatorActive: 'bg-black'
345
+ }
346
+ }
347
+ }}
348
+ >
349
+ {/* Fallback when plugin is disabled */}
350
+ <Link href={product.absolute_url}>
351
+ <Image
352
+ fill
353
+ src={imageUrl || '/noimage.jpg'}
354
+ alt={product.name}
355
+ aspectRatio={3 / 4}
356
+ sizes="(max-width: 768px) 50vw, 33vw"
357
+ />
358
+ </Link>
359
+ </PluginModule>
360
+ </div>
361
+
362
+ <Link href={product.absolute_url}>
363
+ <h3 className="text-sm">{product.name}</h3>
364
+ </Link>
365
+ <Price value={product.price} className="font-semibold" />
366
+ </div>
367
+ );
368
+ }
369
+ ```
package/package.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "@akinon/pz-list-hover-image",
3
+ "version": "2.0.6-rc.2",
4
+ "license": "MIT",
5
+ "main": "src/index.ts",
6
+ "peerDependencies": {
7
+ "react": "^18.0.0",
8
+ "react-dom": "^18.0.0"
9
+ },
10
+ "dependencies": {
11
+ "clsx": "^2.0.0",
12
+ "tailwind-merge": "^2.0.0"
13
+ },
14
+ "devDependencies": {
15
+ "@types/node": "^18.7.8",
16
+ "@types/react": "^18.0.17",
17
+ "@types/react-dom": "^18.0.6",
18
+ "typescript": "^5.2.2"
19
+ }
20
+ }
package/src/index.ts ADDED
@@ -0,0 +1,19 @@
1
+ // Views
2
+ export { ListHoverImage } from './views/list-hover-image';
3
+
4
+ // Types
5
+ export type {
6
+ ListHoverImageProps,
7
+ ListHoverImageSettings,
8
+ ListHoverImageCustomStyles,
9
+ ListHoverImageCustomRenderers,
10
+ IndicatorProps,
11
+ HoverZoneProps,
12
+ ProductImage
13
+ } from './types';
14
+
15
+ // Utils
16
+ export { mergeSettings, defaultSettings } from './utils';
17
+
18
+ // Default export for PluginModule
19
+ export { ListHoverImage as default } from './views/list-hover-image';
@@ -0,0 +1,83 @@
1
+ import React from 'react';
2
+
3
+ export interface ProductImage {
4
+ image: string;
5
+ [key: string]: any;
6
+ }
7
+
8
+ export interface ListHoverImageProps {
9
+ images: ProductImage[];
10
+ href: string;
11
+ alt: string;
12
+ width?: number;
13
+ height?: number;
14
+ fill?: boolean;
15
+ sizes?: string;
16
+ aspectRatio?: number;
17
+ maxImages?: number;
18
+ fallbackImage?: string;
19
+ className?: string;
20
+ settings?: ListHoverImageSettings;
21
+ }
22
+
23
+ export interface IndicatorProps {
24
+ count: number;
25
+ activeIndex: number;
26
+ onSelect: (index: number) => void;
27
+ customStyles?: ListHoverImageCustomStyles;
28
+ customRenderers?: ListHoverImageCustomRenderers;
29
+ }
30
+
31
+ export interface HoverZoneProps {
32
+ count: number;
33
+ href: string;
34
+ onHover: (index: number) => void;
35
+ customStyles?: ListHoverImageCustomStyles;
36
+ }
37
+
38
+ export interface ListHoverImageCustomStyles {
39
+ container?: string;
40
+ imageWrapper?: string;
41
+ image?: string;
42
+ hoverZoneContainer?: string;
43
+ hoverZone?: string;
44
+ indicatorContainer?: string;
45
+ indicatorList?: string;
46
+ indicator?: string;
47
+ indicatorActive?: string;
48
+ }
49
+
50
+ export interface ListHoverImageCustomRenderers {
51
+ renderImage?: (props: {
52
+ src: string;
53
+ alt: string;
54
+ width?: number;
55
+ height?: number;
56
+ fill?: boolean;
57
+ sizes?: string;
58
+ aspectRatio?: number;
59
+ }) => React.ReactNode;
60
+ renderIndicator?: (props: {
61
+ count: number;
62
+ activeIndex: number;
63
+ onSelect: (index: number) => void;
64
+ }) => React.ReactNode;
65
+ renderIndicatorDot?: (props: {
66
+ index: number;
67
+ isActive: boolean;
68
+ onClick: () => void;
69
+ }) => React.ReactNode;
70
+ }
71
+
72
+ export interface ListHoverImageSettings {
73
+ maxImages?: number;
74
+ fallbackImage?: string;
75
+ customStyles?: ListHoverImageCustomStyles;
76
+ customRenderers?: ListHoverImageCustomRenderers;
77
+ theme?: {
78
+ indicatorSize?: string;
79
+ indicatorActiveColor?: string;
80
+ indicatorBorderColor?: string;
81
+ indicatorBorderRadius?: string;
82
+ };
83
+ }
@@ -0,0 +1,41 @@
1
+ import { ListHoverImageSettings } from '../types';
2
+
3
+ const DEFAULT_FALLBACK_IMAGE = '/noimage.jpg';
4
+
5
+ export const defaultSettings: Required<ListHoverImageSettings> = {
6
+ maxImages: 3,
7
+ fallbackImage: DEFAULT_FALLBACK_IMAGE,
8
+ customStyles: {},
9
+ customRenderers: {},
10
+ theme: {
11
+ indicatorSize: 'w-2.5 h-2.5 lg:w-3.5 lg:h-3.5',
12
+ indicatorActiveColor: 'bg-secondary',
13
+ indicatorBorderColor: 'border-white',
14
+ indicatorBorderRadius: 'rounded-full'
15
+ }
16
+ };
17
+
18
+ export function mergeSettings(
19
+ userSettings?: Partial<ListHoverImageSettings>
20
+ ): Required<ListHoverImageSettings> {
21
+ if (!userSettings) {
22
+ return defaultSettings;
23
+ }
24
+
25
+ return {
26
+ ...defaultSettings,
27
+ ...userSettings,
28
+ customStyles: {
29
+ ...defaultSettings.customStyles,
30
+ ...userSettings.customStyles
31
+ },
32
+ customRenderers: {
33
+ ...defaultSettings.customRenderers,
34
+ ...userSettings.customRenderers
35
+ },
36
+ theme: {
37
+ ...defaultSettings.theme,
38
+ ...userSettings.theme
39
+ }
40
+ };
41
+ }
@@ -0,0 +1,197 @@
1
+ 'use client';
2
+
3
+ import React, { useState, useCallback, useMemo } from 'react';
4
+ import { twMerge } from 'tailwind-merge';
5
+ import clsx from 'clsx';
6
+ import { Image } from '@akinon/next/components/image';
7
+ import { Link } from '@akinon/next/components/link';
8
+ import type {
9
+ ListHoverImageProps,
10
+ IndicatorProps,
11
+ HoverZoneProps
12
+ } from '../types';
13
+ import { mergeSettings } from '../utils';
14
+
15
+ function HoverZones({ count, href, onHover, customStyles }: HoverZoneProps) {
16
+ return (
17
+ <div
18
+ className={twMerge(
19
+ 'absolute inset-0 flex cursor-pointer justify-between',
20
+ customStyles?.hoverZoneContainer
21
+ )}
22
+ >
23
+ {Array.from({ length: count }, (_, index) => (
24
+ <Link
25
+ href={href}
26
+ key={index}
27
+ className={twMerge('relative w-full', customStyles?.hoverZone)}
28
+ onMouseEnter={() => onHover(index)}
29
+ aria-label={`View image ${index + 1}`}
30
+ />
31
+ ))}
32
+ </div>
33
+ );
34
+ }
35
+
36
+ function Indicators({
37
+ count,
38
+ activeIndex,
39
+ onSelect,
40
+ customStyles,
41
+ customRenderers
42
+ }: IndicatorProps) {
43
+ if (count <= 1) return null;
44
+
45
+ if (customRenderers?.renderIndicator) {
46
+ return (
47
+ <>
48
+ {customRenderers.renderIndicator({
49
+ count,
50
+ activeIndex,
51
+ onSelect
52
+ })}
53
+ </>
54
+ );
55
+ }
56
+
57
+ return (
58
+ <div
59
+ className={twMerge(
60
+ 'absolute bottom-4 left-0 flex w-full justify-center',
61
+ customStyles?.indicatorContainer
62
+ )}
63
+ >
64
+ <ul
65
+ className={twMerge(
66
+ 'flex gap-1.5 lg:gap-2',
67
+ customStyles?.indicatorList
68
+ )}
69
+ role="tablist"
70
+ aria-label="Product images"
71
+ >
72
+ {Array.from({ length: count }, (_, index) => {
73
+ if (customRenderers?.renderIndicatorDot) {
74
+ return (
75
+ <li key={index} role="tab" aria-selected={activeIndex === index}>
76
+ {customRenderers.renderIndicatorDot({
77
+ index,
78
+ isActive: activeIndex === index,
79
+ onClick: () => onSelect(index)
80
+ })}
81
+ </li>
82
+ );
83
+ }
84
+
85
+ return (
86
+ <li
87
+ key={index}
88
+ role="tab"
89
+ aria-selected={activeIndex === index}
90
+ className={twMerge(
91
+ clsx(
92
+ 'w-2.5 h-2.5 lg:w-3.5 lg:h-3.5 border rounded-full cursor-pointer transition-colors duration-200',
93
+ 'border-white',
94
+ {
95
+ 'bg-secondary border-transparent': activeIndex === index
96
+ }
97
+ ),
98
+ activeIndex === index
99
+ ? customStyles?.indicatorActive
100
+ : customStyles?.indicator
101
+ )}
102
+ onClick={() => onSelect(index)}
103
+ />
104
+ );
105
+ })}
106
+ </ul>
107
+ </div>
108
+ );
109
+ }
110
+
111
+ export function ListHoverImage({
112
+ images,
113
+ href,
114
+ alt,
115
+ width,
116
+ height,
117
+ fill,
118
+ sizes,
119
+ aspectRatio,
120
+ maxImages,
121
+ fallbackImage,
122
+ className,
123
+ settings: userSettings
124
+ }: ListHoverImageProps) {
125
+ const settings = mergeSettings(userSettings);
126
+
127
+ const resolvedMaxImages = maxImages ?? settings.maxImages;
128
+ const resolvedFallback = fallbackImage ?? settings.fallbackImage;
129
+
130
+ const visibleImages = useMemo(
131
+ () => images?.slice(0, resolvedMaxImages) ?? [],
132
+ [images, resolvedMaxImages]
133
+ );
134
+
135
+ const initialImageUrl = visibleImages[0]?.image || resolvedFallback;
136
+
137
+ const [activeIndex, setActiveIndex] = useState(0);
138
+ const [currentImageUrl, setCurrentImageUrl] = useState(initialImageUrl);
139
+
140
+ const handleImageChange = useCallback(
141
+ (index: number) => {
142
+ setActiveIndex(index);
143
+ setCurrentImageUrl(visibleImages[index]?.image || resolvedFallback);
144
+ },
145
+ [visibleImages, resolvedFallback]
146
+ );
147
+
148
+ const imageElement = settings.customRenderers.renderImage ? (
149
+ settings.customRenderers.renderImage({
150
+ src: currentImageUrl,
151
+ alt,
152
+ width,
153
+ height,
154
+ fill,
155
+ sizes,
156
+ aspectRatio
157
+ })
158
+ ) : (
159
+ <Image
160
+ src={currentImageUrl}
161
+ alt={alt}
162
+ {...(fill
163
+ ? { fill: true, sizes: sizes!, aspectRatio: aspectRatio! }
164
+ : { width: width!, height: height! })}
165
+ className={settings.customStyles.image}
166
+ />
167
+ );
168
+
169
+ return (
170
+ <div
171
+ className={twMerge(
172
+ 'relative',
173
+ settings.customStyles.container,
174
+ className
175
+ )}
176
+ >
177
+ <div className={twMerge('relative', settings.customStyles.imageWrapper)}>
178
+ {imageElement}
179
+ </div>
180
+
181
+ <HoverZones
182
+ count={visibleImages.length}
183
+ href={href}
184
+ onHover={handleImageChange}
185
+ customStyles={settings.customStyles}
186
+ />
187
+
188
+ <Indicators
189
+ count={visibleImages.length}
190
+ activeIndex={activeIndex}
191
+ onSelect={handleImageChange}
192
+ customStyles={settings.customStyles}
193
+ customRenderers={settings.customRenderers}
194
+ />
195
+ </div>
196
+ );
197
+ }