diff --git a/.changeset/mosaic-image-cropper-primitive.md b/.changeset/mosaic-image-cropper-primitive.md new file mode 100644 index 00000000000..a845151cc84 --- /dev/null +++ b/.changeset/mosaic-image-cropper-primitive.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/packages/mosaic/AGENTS.md b/packages/mosaic/AGENTS.md index 6ce43ec589a..03e42907fa7 100644 --- a/packages/mosaic/AGENTS.md +++ b/packages/mosaic/AGENTS.md @@ -87,6 +87,7 @@ Most problems a feature hits already have a shared answer. Find the line for you - The seconds left on a resend cooldown, for display → `useResendSeconds` (`src/hooks/`), from a `resendAvailableAt` time kept in the machine - An avatar or logo row: picker, drop target, Upload button or manage menu → `AvatarUpload` (`src/blocks/avatar-upload/`), laid out by the caller - Any other file picker or drop zone, such as a metadata or key file → `FileUpload` (`src/primitives/file-upload/`), see its `README.md` +- Cropping an image to a square: pan, zoom, rotate, then draw the result → `ImageCropper` and `cropImage` (`src/primitives/image-cropper/`), see its `README.md` - An inline notice inside content: info, warning, error or success → `Banner` (`src/components/banner/`); a form-level error on a card uses `Card.Banner` - Siblings that slide when an item is added or removed → `useLayoutAnimation` (`src/primitives/hooks/`) - An input that reformats as the user types without moving the caret → `useFormattedInput` (`src/primitives/hooks/`) diff --git a/packages/mosaic/src/primitives/image-cropper/README.md b/packages/mosaic/src/primitives/image-cropper/README.md new file mode 100644 index 00000000000..04a5ed852d0 --- /dev/null +++ b/packages/mosaic/src/primitives/image-cropper/README.md @@ -0,0 +1,116 @@ +# ImageCropper + +A headless image cropper. The user pans the image by dragging or with the arrow keys, zooms with the +wheel, a pinch, the keyboard, a slider or step buttons, turns it a quarter at a time, and resets it. +The crop is a square. `cropImage` draws the square the user chose into a new image. + +## When to Use + +- Cropping a picked image to a square before uploading it, such as an avatar or a logo. + +The primitive emits zero styles. The caller sizes the frame, draws any mask over it, and styles the +controls through `data-*` attributes. + +## Usage + +```tsx +import { useState } from 'react'; + +import { type Crop, cropImage, ImageCropper, INITIAL_CROP } from '../../primitives/image-cropper'; + +function Cropper({ src, onSave }: { src: string; onSave: (blob: Blob) => void }) { + const [crop, setCrop] = useState(INITIAL_CROP); + + return ( + + + + + + + − + + + + Rotate + Reset + + + ); +} +``` + +`Area` is optional. Without it the whole `Viewport` is the crop. With it, the crop is the `Area`, and +the `Viewport` around it shows the rest of the image for context. Drags are measured against the +`Area`, so the image follows the pointer either way. + +## Parts + +| Part | Default Element | Description | +| ----------------------- | ---------------------- | ---------------------------------------------------------------------- | +| `ImageCropper.Root` | none | Owns the crop and the image size, provides context | +| `ImageCropper.Viewport` | `
` | The frame: pans on drag and arrow keys, zooms on wheel, pinch and keys | +| `ImageCropper.Area` | `
` | The square that is cropped, when it is smaller than the frame | +| `ImageCropper.Image` | `` | The image, placed by the crop. Reports its size on load | +| `ImageCropper.Zoom` | `` | Sets the zoom, from 1 to `maxZoom` | +| `ImageCropper.ZoomIn` | ` +
+ + {result ? ( + Cropped result + ) : null} +
+ ); +}