A client-side React component providing a drag-and-drop / click-to-upload image input with preview, validation, and loading states, wrapped in a `FieldWrapper` for consistent field labeling and error display. ## Key Components ### `ImageUploader` (main export) The primary component. Renders an empty dropzone or image preview depending on whether `value` is set. **Key behaviors:** - **Empty state** — shows a labeled dropzone with click, keyboard (`Enter`/`Space`), and drag-and-drop support - **Preview state** — displays the uploaded image with overlay action buttons (replace / remove) - **Loading state** — renders a spinner overlay (on preview) or centered spinner (on empty dropzone) - **Validation** — enforces `accept` MIME/extension matching and `maxSize` before calling `onChange` - **Auth-aware image rendering** — uses `useAuthedImageSrc` to resolve gateway URLs requiring `Authorization` headers ### `ActionIconButton` (internal) Small icon button rendered over the image preview for replace/remove actions. Propagation-safe (`e.stopPropagation()`). ### Helpers (internal) - `formatSize(bytes)` — formats byte counts as `KB`/`MB` strings - `matchesAccept(file, accept)` — validates a `File` against an `accept` attribute pattern ## Usage Example ```typescript import { ImageUploader } from "@/components/ui/image-uploader" import { useState } from "react" function CoverImageField() { const [imageUrl, setImageUrl] = useState() const [uploading, setUploading] = useState(false) const handleChange = async (file: File) => { setUploading(true) const url = await uploadToStorage(file) // your upload logic setImageUrl(url) setUploading(false) } return ( setImageUrl(undefined)} fieldLabel="Cover Image" description="Click or drag and drop" maxSize={10 * 1024 * 1024} // 10 MB aspectRatio="16 / 9" loading={uploading} /> ) } ``` ## Props Summary | Prop | Type | Default | Description | |---|---|---|---| | `value` | `string` | — | Current image URL; presence triggers preview state | | `onChange` | `(file: File) => void` | required | Called after validation passes | | `onRemove` | `() => void` | — | Shows remove button when provided | | `maxSize` | `number` | `25MB` | Max file size in bytes | | `accept` | `string` | `"image/*"` | MIME types or extensions | | `aspectRatio` | `string` | — | CSS aspect-ratio (overrides fixed height) | | `objectFit` | `string` | `"cover"` | Preview image fit mode | | `loading` | `boolean` | `false` | Spinner overlay + disables interaction | | `disabled` | `boolean` | `false` | Disables all interaction | | `dropzoneClassName` | `string` | — | Override dropzone sizing/styles | **Source:** [`image-uploader.tsx`](https://github.com/flamingo-stack/openframe-oss-lib/blob/main/image-uploader.tsx)