# @akinon/pz-list-hover-image

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.

The plugin accepts only the data it needs (`images`, `href`, `alt`) instead of the entire `Product` object, preventing unnecessary DOM bloat and data serialization.

## Installation

```bash
npx @akinon/projectzero@latest --plugins
```

After selecting the plugin, make sure it is added to `plugins.js`:

```js
// src/plugins.js
module.exports = [
  // ...other plugins
  'pz-list-hover-image'
];
```

## Basic Usage

### With PluginModule (Recommended)

```tsx
import PluginModule, { Component } from '@akinon/next/components/plugin-module';

<PluginModule
  component={Component.ListHoverImage}
  props={{
    images: product.productimage_set,
    href: product.absolute_url,
    alt: product.name,
    fill: true,
    sizes: '(max-width: 768px) 50vw, 33vw',
    aspectRatio: 3 / 4
  }}
>
  {/* Fallback rendered when plugin is disabled */}
  <Image src={product.productimage_set[0]?.image} alt={product.name} />
</PluginModule>
```

### Direct Import

```tsx
import { ListHoverImage } from '@akinon/pz-list-hover-image';

<ListHoverImage
  images={product.productimage_set}
  href={product.absolute_url}
  alt={product.name}
  fill
  sizes="(max-width: 768px) 50vw, 33vw"
  aspectRatio={3 / 4}
/>
```

## Props

### `ListHoverImageProps`

| Property | Type | Required | Default | Description |
|---|---|---|---|---|
| `images` | `ProductImage[]` | Yes | - | Product image array. Each element must contain at least `{ image: string }`. You can pass `product.productimage_set` directly. |
| `href` | `string` | Yes | - | Product detail page URL. Hover zones link to this URL. |
| `alt` | `string` | Yes | - | Image alt text for accessibility. |
| `width` | `number` | No | - | Image width in pixels. Required when `fill` is not used. |
| `height` | `number` | No | - | Image height in pixels. Required when `fill` is not used. |
| `fill` | `boolean` | No | `false` | When `true`, the image fills its parent container. Must be used with `sizes` and `aspectRatio`. |
| `sizes` | `string` | No | - | Responsive image sizes attribute. Required in `fill` mode. |
| `aspectRatio` | `number` | No | - | Image aspect ratio. Required in `fill` mode. |
| `maxImages` | `number` | No | `3` | Maximum number of images to display. |
| `fallbackImage` | `string` | No | Akinon noimage URL | Fallback image URL shown when the product has no images. |
| `className` | `string` | No | - | CSS class added to the root container. |
| `settings` | `ListHoverImageSettings` | No | `defaultSettings` | Detailed configuration object. |

### `ProductImage`

```ts
interface ProductImage {
  image: string;
  [key: string]: any;
}
```

`product.productimage_set` is compatible with this interface and can be passed directly.

## Settings

Use the `settings` prop to customize the plugin's behavior and appearance in detail.

### `ListHoverImageSettings`

```ts
interface ListHoverImageSettings {
  maxImages?: number;
  fallbackImage?: string;
  customStyles?: ListHoverImageCustomStyles;
  customRenderers?: ListHoverImageCustomRenderers;
  theme?: {
    indicatorSize?: string;
    indicatorActiveColor?: string;
    indicatorBorderColor?: string;
    indicatorBorderRadius?: string;
  };
}
```

### Usage with Settings

#### Via settings.js (Global)

```js
// src/settings.js
module.exports = {
  plugins: {
    'pz-list-hover-image': {
      maxImages: 5,
      customStyles: {
        indicatorActive: 'bg-black'
      }
    }
  }
};
```

#### As a Prop (Per Component)

```tsx
<ListHoverImage
  images={product.productimage_set}
  href={product.absolute_url}
  alt={product.name}
  fill
  sizes="(max-width: 768px) 50vw, 33vw"
  aspectRatio={3 / 4}
  settings={{
    maxImages: 4,
    fallbackImage: '/custom-noimage.jpg',
    theme: {
      indicatorActiveColor: 'bg-black',
      indicatorBorderColor: 'border-gray-300',
      indicatorBorderRadius: 'rounded-sm'
    }
  }}
/>
```

## Custom Styles

Use `customStyles` to override CSS classes on each sub-element. All classes are merged via `tailwind-merge`, so you can safely override defaults.

### `ListHoverImageCustomStyles`

| Property | Target | Default |
|---|---|---|
| `container` | Root wrapper `div` | `relative` |
| `imageWrapper` | Wrapper `div` around the image | `relative` |
| `image` | `Image` component | - |
| `hoverZoneContainer` | Wrapper `div` for hover zones | `absolute inset-0 flex cursor-pointer justify-between` |
| `hoverZone` | Each hover zone (`Link`) | `relative w-full` |
| `indicatorContainer` | Indicator wrapper `div` | `absolute bottom-4 left-0 flex w-full justify-center` |
| `indicatorList` | Dot list `ul` | `flex gap-1.5 lg:gap-2` |
| `indicator` | Inactive dot `li` | `w-2.5 h-2.5 lg:w-3.5 lg:h-3.5 border border-white rounded-full` |
| `indicatorActive` | Active dot `li` | `bg-secondary border-transparent` |

### Example: Custom Styles

```tsx
<ListHoverImage
  images={product.productimage_set}
  href={product.absolute_url}
  alt={product.name}
  width={300}
  height={400}
  settings={{
    customStyles: {
      container: 'rounded-xl overflow-hidden shadow-lg',
      indicatorContainer: 'absolute bottom-2 left-0 flex w-full justify-center',
      indicatorList: 'flex gap-1 bg-black/30 rounded-full px-2 py-1',
      indicator: 'w-2 h-2 rounded-full bg-white/50',
      indicatorActive: 'w-2 h-2 rounded-full bg-white'
    }
  }}
/>
```

## Custom Renderers

For advanced customization, use `customRenderers` to completely replace component parts with your own render functions.

### `ListHoverImageCustomRenderers`

| Property | Description | Props |
|---|---|---|
| `renderImage` | Completely overrides the image component | `{ src, alt, width, height, fill, sizes, aspectRatio }` |
| `renderIndicator` | Overrides the entire indicator section | `{ count, activeIndex, onSelect }` |
| `renderIndicatorDot` | Overrides a single dot | `{ index, isActive, onClick }` |

### Example: Custom Image Renderer

```tsx
<ListHoverImage
  images={product.productimage_set}
  href={product.absolute_url}
  alt={product.name}
  fill
  sizes="50vw"
  aspectRatio={1}
  settings={{
    customRenderers: {
      renderImage: ({ src, alt }) => (
        <img
          src={src}
          alt={alt}
          className="w-full h-full object-cover"
          loading="lazy"
        />
      )
    }
  }}
/>
```

### Example: Custom Indicator Dots (Bar Style)

```tsx
<ListHoverImage
  images={product.productimage_set}
  href={product.absolute_url}
  alt={product.name}
  width={300}
  height={400}
  settings={{
    customRenderers: {
      renderIndicatorDot: ({ index, isActive, onClick }) => (
        <button
          onClick={onClick}
          className={`w-6 h-1 rounded-full transition-all ${
            isActive ? 'bg-primary w-10' : 'bg-gray-300'
          }`}
          aria-label={`Image ${index + 1}`}
        />
      )
    }
  }}
/>
```

### Example: Counter Indicator (1/3 Style)

```tsx
<ListHoverImage
  images={product.productimage_set}
  href={product.absolute_url}
  alt={product.name}
  width={300}
  height={400}
  settings={{
    customRenderers: {
      renderIndicator: ({ count, activeIndex }) => (
        <div className="absolute bottom-2 right-2 bg-black/60 text-white text-xs rounded px-2 py-1">
          {activeIndex + 1} / {count}
        </div>
      )
    }
  }}
/>
```

## Image Modes

The plugin supports two image modes:

### Fixed Dimensions (width/height)

Use with fixed width and height values:

```tsx
<ListHoverImage
  images={product.productimage_set}
  href={product.absolute_url}
  alt={product.name}
  width={300}
  height={400}
/>
```

### Responsive Fill Mode

Fills the parent container responsively:

```tsx
<div className="w-full">
  <ListHoverImage
    images={product.productimage_set}
    href={product.absolute_url}
    alt={product.name}
    fill
    sizes="(max-width: 768px) 50vw, (max-width: 1024px) 30vw, 25vw"
    aspectRatio={3 / 4}
  />
</div>
```

## Full Integration Example

Full integration within a product card:

```tsx
'use client';

import { Product } from '@akinon/next/types';
import { Price, Link } from '@theme/components';
import { Image } from '@akinon/next/components/image';
import PluginModule, { Component } from '@akinon/next/components/plugin-module';

interface ProductCardProps {
  product: Product;
}

export function ProductCard({ product }: ProductCardProps) {
  const imageUrl = product.productimage_set[0]?.image;

  return (
    <div className="group">
      <div className="relative mb-3 overflow-hidden rounded-lg">
        <PluginModule
          component={Component.ListHoverImage}
          props={{
            images: product.productimage_set,
            href: product.absolute_url,
            alt: product.name,
            fill: true,
            sizes: '(max-width: 768px) 50vw, 33vw',
            aspectRatio: 3 / 4,
            settings: {
              maxImages: 4,
              customStyles: {
                container: 'rounded-lg overflow-hidden',
                indicatorActive: 'bg-black'
              }
            }
          }}
        >
          {/* Fallback when plugin is disabled */}
          <Link href={product.absolute_url}>
            <Image
              fill
              src={imageUrl || '/noimage.jpg'}
              alt={product.name}
              aspectRatio={3 / 4}
              sizes="(max-width: 768px) 50vw, 33vw"
            />
          </Link>
        </PluginModule>
      </div>

      <Link href={product.absolute_url}>
        <h3 className="text-sm">{product.name}</h3>
      </Link>
      <Price value={product.price} className="font-semibold" />
    </div>
  );
}
```