npm package: @publikit/imagepicker
Description: A flexible image picker with crop, presets, and upload support for React applications
Author: Pirimera
Latest version: 0.1.0

# @publikit/imagepicker

A flexible, customizable image picker component for React applications. Supports image upload with cropping, preset image selection, and both immediate-upload and deferred (onboarding) modes.

## Features

- **Image Upload** — File selection with type/size validation
- **Crop** — Built-in crop UI with aspect ratio locking or free-form cropping
- **Presets** — Optional grid of preset images (avatars, logos, etc.)
- **Two Modes** — "edit" (immediate upload) and "onboarding" (returns blob for deferred upload)
- **Upload Adapter** — Bring your own upload logic via `onUpload` callback
- **Error Adapter** — Bring your own toast/notification via `onError` callback
- **Composable** — Sub-components exported for custom layouts
- **Tailwind CSS** — Styled with Tailwind utility classes

## Installation

```bash
npm install @publikit/imagepicker @publikit/base react-image-crop
```

### Peer Dependencies

| Package | Version |
|---------|---------|
| `react` | `^18.0.0 \|\| ^19.0.0` |
| `react-dom` | `^18.0.0 \|\| ^19.0.0` |
| `react-image-crop` | `^11.0.0` |
| `lucide-react` | `>=0.300.0` |
| `tailwindcss` | `>=3.0.0` |
| `@publikit/base` | `^0.1.1` |

## Quick Start

### Edit Mode (Immediate Upload)

```tsx
import { ImagePicker } from '@publikit/imagepicker';
import { toast } from 'sonner';

function AvatarEditor({ userId, avatarUrl, onAvatarChange }) {
  return (
    <ImagePicker
      config={{
        circularCrop: true,
        borderRadius: 'full',
        aspectRatio: 1,
        labels: {
          changeTitle: 'Change Avatar',
          chooseTitle: 'Choose Avatar',
          cropTitle: 'Crop Your Photo',
          uploadLabel: 'Upload a photo',
          removeLabel: 'Remove Photo',
        },
      }}
      presets={[
        { id: '1', url: 'https://api.dicebear.com/7.x/lorelei/svg?seed=felix' },
        { id: '2', url: 'https://api.dicebear.com/7.x/lorelei/svg?seed=aneka' },
      ]}
      value={avatarUrl}
      onChange={onAvatarChange}
      onUpload={async (blob) => {
        // Your upload logic here — return the public URL
        const formData = new FormData();
        formData.append('file', blob);
        const res = await fetch('/api/upload', { method: 'POST', body: formData });
        const { url } = await res.json();
        return url;
      }}
      onError={(msg) => toast.error(msg)}
      initials="JD"
      alt="Profile photo"
    />
  );
}
```

### Onboarding Mode (Deferred Upload)

```tsx
import { ImagePicker, type ImagePickerData } from '@publikit/imagepicker';

function WorkspaceSetup() {
  const [logo, setLogo] = useState<ImagePickerData | null>(null);

  return (
    <ImagePicker
      mode="onboarding"
      config={{
        circularCrop: false,
        borderRadius: 'lg',
        aspectRatio: 1,
        labels: {
          changeTitle: 'Change Logo',
          chooseTitle: 'Choose Logo',
          cropTitle: 'Crop Your Logo',
          uploadLabel: 'Upload a logo',
          removeLabel: 'Remove Logo',
        },
      }}
      value={logo}
      onChange={setLogo}
      initials="W"
    />
  );
}
```

## API

### `<ImagePicker>` Props

#### Common Props

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `config` | `ImagePickerConfig` | **required** | Appearance and label configuration |
| `presets` | `ImagePickerPreset[]` | `[]` | Optional preset images for the presets tab |
| `initials` | `string` | `'?'` | Fallback initials when no image selected |
| `size` | `'default' \| 'large'` | `'default'` | Trigger button size |
| `alt` | `string` | `'Image'` | Alt text for the image |
| `onError` | `(message: string) => void` | `console.error` | Error notification callback |

#### Edit Mode Props

| Prop | Type | Description |
|------|------|-------------|
| `mode` | `'edit'` (or omitted) | Edit mode with immediate upload |
| `value` | `string \| null` | Current image URL |
| `onChange` | `(url: string \| null) => void` | Called with new URL or null |
| `onUpload` | `(blob: Blob) => Promise<string \| null>` | Upload adapter — receives cropped blob, returns URL |

#### Onboarding Mode Props

| Prop | Type | Description |
|------|------|-------------|
| `mode` | `'onboarding'` | Onboarding mode — returns blob data |
| `value` | `ImagePickerData \| null` | Current image data |
| `onChange` | `(data: ImagePickerData \| null) => void` | Called with image data or null |

### `ImagePickerConfig`

```typescript
interface ImagePickerConfig {
  circularCrop: boolean;
  borderRadius: 'full' | 'lg';
  aspectRatio?: number; // undefined = free-form
  labels: {
    changeTitle: string;
    chooseTitle: string;
    cropTitle: string;
    uploadLabel: string;
    removeLabel: string;
  };
}
```

## Advanced: Sub-components

For custom layouts, you can import individual components:

```tsx
import {
  ImagePickerButton,
  CropView,
  PresetView,
  UploadView,
  useImagePicker,
  useCropState,
  useFileSelection,
  getCroppedImg,
} from '@publikit/imagepicker';
```

## License

MIT