sanity-plugin-image-field 0.0.1

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.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/ImageFieldInput.tsx","../src/cropMath.ts","../src/utils.ts","../src/CropDialog.tsx","../src/CropArea.tsx","../src/useContentWidth.ts","../src/imageUrl.ts","../src/upload.ts","../src/defineImageField.ts","../src/formComponents.tsx"],"sourcesContent":["import { useEffect, useRef, useState } from \"react\"\nimport {\n Box,\n Button,\n Card,\n Flex,\n Inline,\n Menu,\n MenuButton,\n MenuItem,\n Spinner,\n Stack,\n Text,\n} from \"@sanity/ui\"\nimport {\n CropIcon,\n ImageIcon,\n ImagesIcon,\n TrashIcon,\n UploadIcon,\n} from \"@sanity/icons\"\nimport {\n type AssetFromSource,\n type AssetSource,\n type FormPatch,\n type ImageCrop,\n type ImageHotspot,\n type ImageSchemaType,\n type ImageValue,\n ObjectInputMembers,\n type ObjectInputProps,\n type ObjectMember,\n set,\n setIfMissing,\n unset,\n useClient,\n useWorkspace,\n} from \"sanity\"\nimport { dimensionsAtLeast, resolveAspectRatio } from \"./cropMath\"\nimport { CropDialog } from \"./CropDialog\"\nimport {\n croppedDimensionsFromValue,\n croppedImageUrl,\n isCroppable,\n isImageAssetRef,\n isSvg,\n type SanityImageSource,\n sourceDimensions,\n} from \"./imageUrl\"\nimport { acceptsType, isTextEntryTarget } from \"./upload\"\nimport { type Dimensions, type ResolvedImageFieldConfig } from \"./types\"\n\nconst API_VERSION = \"2025-02-19\"\n\ntype UploadStatus =\n | { status: \"idle\" }\n | { status: \"uploading\"; progress: number }\n | { status: \"error\"; message: string }\n\n/**\n * A custom image input that constrains cropping to a chosen range of aspect\n * ratios. It handles uploads from the file picker and drag-and-drop, shows a\n * ratio-aware crop editor, and saves the result as Sanity's non-destructive\n * crop. Existing image pipelines render it without changes.\n *\n * Extra fields on the image type, such as `alt` or `caption`, render below the\n * asset UI using the form's own field renderers.\n *\n * Set it on an `image` field with `components: { input: ImageFieldInput }`, and\n * configure it through `options.imageField` on that field. With no options set,\n * it is a plain uploader with a free-form cropper.\n */\nexport const ImageFieldInput = ({\n value,\n onChange,\n path,\n members,\n readOnly,\n schemaType,\n ...renderObjectProps\n}: ObjectInputProps<ImageValue, ImageSchemaType>) => {\n const client = useClient({ apiVersion: API_VERSION })\n const { projectId, dataset } = client.config()\n const assetSources = useWorkspace().form.image.assetSources\n\n const imageFieldOptions = schemaType.options?.imageField\n const aspectRatio = imageFieldOptions?.aspectRatio\n const hasAspectConstraint = aspectRatio !== undefined\n const hotspotEnabled = Boolean(schemaType.options?.hotspot)\n const resolved = resolveAspectRatio(aspectRatio)\n const config: ResolvedImageFieldConfig = {\n aspectRatioRange: resolved.aspectRatioRange,\n snapRatios: resolved.snapRatios,\n recommendedDimensions: imageFieldOptions?.recommendedDimensions,\n requiredDimensions: imageFieldOptions?.requiredDimensions,\n }\n const accept = schemaType.options?.accept ?? \"image/*\"\n\n const [upload, setUpload] = useState<UploadStatus>({ status: \"idle\" })\n const [dialogOpen, setDialogOpen] = useState(false)\n const [pendingAsset, setPendingAsset] = useState<string | null>(null)\n const [draggingOver, setDraggingOver] = useState(false)\n const [browsingSource, setBrowsingSource] = useState<AssetSource | null>(null)\n\n const fileInputRef = useRef<HTMLInputElement>(null)\n const dragDepth = useRef(0)\n const subscription = useRef<{ unsubscribe: () => void } | null>(null)\n\n useEffect(() => () => subscription.current?.unsubscribe(), [])\n\n const assetRef = value?.asset?._ref\n const assetCrop = value?.crop\n const assetHotspot = value?.hotspot\n const activeAssetRef = pendingAsset ?? assetRef\n const dialogSource =\n activeAssetRef && isCroppable(activeAssetRef)\n ? sourceDimensions(activeAssetRef)\n : null\n const imageSource: SanityImageSource | null =\n projectId && dataset ? { projectId, dataset } : null\n\n const customFieldMembers = members.filter(isCustomFieldMember)\n\n /**\n * Commit an asset reference to the document. The optional extra patches are\n * appended after the reference patches, in order.\n */\n const commitAsset = (ref: string, extraPatches: FormPatch[] = []) => {\n onChange([\n setIfMissing({ _type: \"image\" }),\n set({ _type: \"reference\", _ref: ref }, [\"asset\"]),\n ...extraPatches,\n ])\n setUpload({ status: \"idle\" })\n }\n\n /**\n * Handle an asset reference, whether just uploaded or picked from an existing\n * source. SVGs commit immediately, editable raster images open the crop\n * dialog, and non-editable raster images commit after the up-front size check.\n */\n const applyAsset = (selectedAssetRef: string) => {\n if (isSvg(selectedAssetRef)) {\n commitAsset(selectedAssetRef, [unset([\"crop\"])])\n return\n }\n\n const dimensions = isCroppable(selectedAssetRef)\n ? sourceDimensions(selectedAssetRef)\n : null\n const openEditor =\n Boolean(dimensions) && (hasAspectConstraint || hotspotEnabled)\n\n if (openEditor) {\n setPendingAsset(selectedAssetRef)\n setUpload({ status: \"idle\" })\n setDialogOpen(true)\n return\n }\n\n if (\n dimensions &&\n config.requiredDimensions &&\n !dimensionsAtLeast(dimensions, config.requiredDimensions)\n ) {\n setUpload({\n status: \"error\",\n message: `That image is too small. It must be at least ${config.requiredDimensions.width} × ${config.requiredDimensions.height} px.`,\n })\n return\n }\n\n commitAsset(selectedAssetRef, [unset([\"crop\"])])\n }\n\n const uploadFile = (file: File) => {\n if (!acceptsType(file.type, accept)) {\n setUpload({\n status: \"error\",\n message:\n \"That file type isn't accepted here. Choose a supported image.\",\n })\n return\n }\n\n setUpload({ status: \"uploading\", progress: 0 })\n subscription.current?.unsubscribe()\n subscription.current = client.observable.assets\n .upload(\"image\", file, { filename: file.name })\n .subscribe({\n next: (event) => {\n if (event.type === \"progress\") {\n setUpload({ status: \"uploading\", progress: event.percent })\n } else {\n applyAsset(event.body.document._id)\n }\n },\n error: () => {\n setUpload({\n status: \"error\",\n message: \"Upload failed. Please try again.\",\n })\n },\n })\n }\n\n /**\n * Handle a selection from an asset source. If the selection is an image\n * asset, apply it to the document. Otherwise, show an error message.\n */\n const selectAsset = (assets: AssetFromSource[]) => {\n setBrowsingSource(null)\n const chosen = assets.at(0)\n if (\n chosen?.kind === \"assetDocumentId\" &&\n typeof chosen.value === \"string\" &&\n isImageAssetRef(chosen.value)\n ) {\n applyAsset(chosen.value)\n return\n }\n setUpload({\n status: \"error\",\n message: \"That selection can't be used here. Choose an uploaded image.\",\n })\n }\n\n /**\n * Confirming a pending asset commits the asset reference before the crop and\n * hotspot patches. Re-cropping an existing image writes only crop metadata.\n */\n const confirmCropAndHotspot = ({\n crop,\n hotspot,\n }: {\n crop: ImageCrop\n hotspot?: ImageHotspot\n }) => {\n const assetPatches = pendingAsset\n ? [\n setIfMissing({ _type: \"image\" }),\n set({ _type: \"reference\", _ref: pendingAsset }, [\"asset\"]),\n ]\n : []\n onChange([\n ...assetPatches,\n set(crop, [\"crop\"]),\n ...(hotspot ? [set(hotspot, [\"hotspot\"])] : []),\n ])\n setPendingAsset(null)\n setDialogOpen(false)\n }\n\n const removeImage = () => {\n // Clear the asset reference and its crop/hotspot metadata, but keep sibling\n // fields such as `alt` and `caption`. When no sibling fields remain and this\n // is not an array element, clear the whole object so it does not become an\n // empty `{ _type }`.\n const keys = Object.keys(value ?? {})\n const hasMemberFields = keys.some((key) => !RESERVED_KEYS.has(key))\n const isArrayElement = typeof path.at(-1) !== \"string\"\n const removeKeys = [\"asset\", \"media\"]\n .concat(keys.filter((key) => ASSET_BOUND_KEYS.has(key)))\n .map((key) => unset([key]))\n onChange(hasMemberFields || isArrayElement ? removeKeys : unset())\n setUpload({ status: \"idle\" })\n }\n\n const onFilePicked = (event: React.ChangeEvent<HTMLInputElement>) => {\n const file = event.target.files?.[0]\n if (file) {\n uploadFile(file)\n }\n event.target.value = \"\"\n }\n\n /**\n * Paste an image from the clipboard. The custom `alt` and `caption` fields\n * share the outer Stack, so a paste into one of them also fires this handler.\n * `isTextEntryTarget` excludes those targets. The candidate filter uses\n * `acceptsType` so it honors `options.accept`, the same check the other\n * upload paths run. A paste that carries no accepted image is left alone,\n * because the handler fires on every paste over the field.\n */\n const handlePaste = (event: React.ClipboardEvent) => {\n if (readOnly || upload.status === \"uploading\") {\n return\n }\n if (isTextEntryTarget(event.target)) {\n return\n }\n const file = Array.from(event.clipboardData.files).find((item) =>\n acceptsType(item.type, accept)\n )\n if (!file) {\n return\n }\n event.preventDefault()\n uploadFile(file)\n }\n\n const dropHandlers = useDropHandlers({\n disabled: readOnly || upload.status === \"uploading\",\n dragDepth,\n setDraggingOver,\n onFile: uploadFile,\n })\n\n if (!imageSource) {\n return (\n <Card padding={4} radius={2} tone=\"critical\" border>\n <Text size={1}>\n Image input unavailable: missing project configuration.\n </Text>\n </Card>\n )\n }\n\n return (\n <Stack gap={3} {...dropHandlers} onPaste={handlePaste}>\n <input\n ref={fileInputRef}\n type=\"file\"\n accept={accept}\n hidden\n aria-label=\"Upload image\"\n onChange={onFilePicked}\n />\n\n {upload.status === \"uploading\" ? (\n <UploadingCard progress={upload.progress} />\n ) : value?.asset ? (\n <ConfirmedPreview\n value={value}\n imageSource={imageSource}\n config={config}\n croppable={Boolean(assetRef && isCroppable(assetRef))}\n readOnly={readOnly}\n draggingOver={draggingOver}\n onEditCrop={() => {\n setDialogOpen(true)\n }}\n onReplace={() => fileInputRef.current?.click()}\n onRemove={removeImage}\n selectAffordance={\n <SelectExistingButton\n sources={assetSources}\n disabled={readOnly}\n fontSize={1}\n onBrowse={setBrowsingSource}\n />\n }\n />\n ) : (\n <EmptyDropzone\n readOnly={readOnly}\n draggingOver={draggingOver}\n onUploadClick={() => fileInputRef.current?.click()}\n selectAffordance={\n <SelectExistingButton\n sources={assetSources}\n disabled={readOnly}\n onBrowse={setBrowsingSource}\n />\n }\n />\n )}\n\n {upload.status === \"error\" && (\n <Card padding={3} radius={2} tone=\"critical\" border>\n <Text size={1}>{upload.message}</Text>\n </Card>\n )}\n\n {customFieldMembers.length > 0 && (\n <ObjectInputMembers\n members={customFieldMembers}\n {...renderObjectProps}\n />\n )}\n\n {browsingSource && (\n <browsingSource.component\n assetSource={browsingSource}\n action=\"select\"\n assetType=\"image\"\n accept={accept}\n selectionType=\"single\"\n selectedAssets={[]}\n dialogHeaderTitle=\"Select image\"\n onClose={() => {\n setBrowsingSource(null)\n }}\n onSelect={selectAsset}\n />\n )}\n\n {dialogOpen && activeAssetRef && dialogSource && (\n <CropDialog\n // Key on the asset so swapping the image remounts the editor.\n key={activeAssetRef}\n assetRef={activeAssetRef}\n imageSource={imageSource}\n sourceDimensions={dialogSource}\n config={config}\n initialCrop={pendingAsset ? undefined : assetCrop}\n hotspotEnabled={hotspotEnabled}\n initialHotspot={pendingAsset ? undefined : assetHotspot}\n onConfirm={confirmCropAndHotspot}\n onClose={() => {\n setPendingAsset(null)\n setDialogOpen(false)\n }}\n />\n )}\n </Stack>\n )\n}\n\nconst UploadingCard = ({ progress }: { progress: number }) => (\n <Card padding={5} radius={2} border>\n <Flex direction=\"column\" align=\"center\" gap={3}>\n <Spinner muted />\n <Text size={1} muted>\n Uploading… {Math.round(progress)}%\n </Text>\n </Flex>\n </Card>\n)\n\nconst EmptyDropzone = ({\n readOnly,\n draggingOver,\n onUploadClick,\n selectAffordance,\n}: {\n readOnly?: boolean\n draggingOver: boolean\n onUploadClick: () => void\n selectAffordance: React.ReactNode\n}) => (\n <Card\n padding={5}\n radius={2}\n border\n tone={draggingOver ? \"primary\" : \"default\"}\n style={{ borderStyle: \"dashed\" }}\n >\n <Flex direction=\"column\" align=\"center\" gap={4}>\n <Text size={4} muted>\n <ImageIcon />\n </Text>\n <Stack gap={2}>\n <Text size={1} muted align=\"center\">\n {draggingOver\n ? \"Drop the image to upload\"\n : \"Drag and drop an image here, or\"}\n </Text>\n </Stack>\n <Inline gap={2}>\n <Button\n icon={UploadIcon}\n text=\"Upload\"\n mode=\"ghost\"\n disabled={readOnly}\n onClick={onUploadClick}\n />\n {selectAffordance}\n </Inline>\n </Flex>\n </Card>\n)\n\nconst ConfirmedPreview = ({\n value,\n imageSource,\n config,\n croppable,\n readOnly,\n draggingOver,\n onEditCrop,\n onReplace,\n onRemove,\n selectAffordance,\n}: {\n value: ImageValue\n imageSource: SanityImageSource\n config: ResolvedImageFieldConfig\n croppable?: boolean\n readOnly?: boolean\n draggingOver: boolean\n onEditCrop: () => void\n onReplace: () => void\n onRemove: () => void\n selectAffordance: React.ReactNode\n}) => {\n const previewUrl = croppedImageUrl(value, imageSource, 640)\n const dimensions = croppedDimensionsFromValue(value)\n const dimensionIssue = dimensions\n ? getDimensionIssue(dimensions, config)\n : undefined\n\n return (\n <Stack gap={3}>\n <Card\n radius={2}\n border\n tone={draggingOver ? \"primary\" : \"default\"}\n style={{ overflow: \"hidden\" }}\n >\n {previewUrl ? (\n <Flex justify=\"center\">\n <img\n src={previewUrl}\n alt=\"\"\n style={{ display: \"block\", maxWidth: \"100%\", maxHeight: 300 }}\n />\n </Flex>\n ) : (\n <Box padding={4}>\n <Text size={1} muted>\n Preview unavailable.\n </Text>\n </Box>\n )}\n </Card>\n\n <Flex justify=\"space-between\" align=\"center\" gap={3} wrap=\"wrap\">\n {dimensions && (\n <Text size={1} muted>\n {dimensions.width} × {dimensions.height} px\n </Text>\n )}\n <Inline gap={2}>\n {croppable && (\n <Button\n icon={CropIcon}\n text=\"Edit crop\"\n mode=\"ghost\"\n fontSize={1}\n disabled={readOnly}\n onClick={onEditCrop}\n />\n )}\n <Button\n icon={ImageIcon}\n text=\"Replace\"\n mode=\"ghost\"\n fontSize={1}\n disabled={readOnly}\n onClick={onReplace}\n />\n {selectAffordance}\n <Button\n icon={TrashIcon}\n text=\"Remove\"\n mode=\"bleed\"\n tone=\"critical\"\n fontSize={1}\n disabled={readOnly}\n onClick={onRemove}\n />\n </Inline>\n </Flex>\n\n {dimensionIssue && (\n <Card padding={3} radius={2} tone={dimensionIssue.tone} border>\n <Text size={1}>{dimensionIssue.message}</Text>\n </Card>\n )}\n </Stack>\n )\n}\n\n/**\n * The control for selecting an existing asset, shared by the empty and confirmed\n * states. It renders nothing for no sources, a plain button for a single source,\n * and a menu of sources otherwise. This matches the native image input.\n */\nconst SelectExistingButton = ({\n sources,\n disabled,\n fontSize,\n onBrowse,\n}: {\n sources: AssetSource[]\n disabled?: boolean\n fontSize?: number\n onBrowse: (source: AssetSource) => void\n}) => {\n const [firstSource] = sources\n if (!firstSource) {\n return null\n }\n\n if (sources.length === 1) {\n return (\n <Button\n icon={ImagesIcon}\n text=\"Select existing\"\n mode=\"ghost\"\n fontSize={fontSize}\n disabled={disabled}\n onClick={() => {\n onBrowse(firstSource)\n }}\n />\n )\n }\n\n return (\n <MenuButton\n id=\"image-field-select-existing\"\n button={\n <Button\n icon={ImagesIcon}\n text=\"Select existing\"\n mode=\"ghost\"\n fontSize={fontSize}\n disabled={disabled}\n />\n }\n menu={\n <Menu>\n {sources.map((source) => (\n <MenuItem\n key={source.name}\n // `title` is deprecated in favor of `i18nKey`, but rendering\n // `i18nKey` needs the i18n `t()` function, which is not wired up\n // here. ¯\\_(⊙︿⊙)_/¯\n // oxlint-disable-next-line typescript/no-deprecated\n text={source.title ?? source.name}\n icon={source.icon}\n fontSize={fontSize}\n onClick={() => {\n onBrowse(source)\n }}\n />\n ))}\n </Menu>\n }\n />\n )\n}\n\n/**\n * Fields this input manages itself, which are not rendered as editable members:\n *\n * - the asset reference\n * - the Media Library handle\n * - crop/hotspot metadata\n *\n * User-defined fields such as `alt` or `caption` render normally.\n */\nconst ASSET_MANAGED_FIELDS = new Set([\"asset\", \"media\", \"crop\", \"hotspot\"])\n\nconst isCustomFieldMember = (member: ObjectMember) =>\n member.kind !== \"field\" || !ASSET_MANAGED_FIELDS.has(member.name)\n\n/**\n * Keys reserved by the image object for the asset and its metadata. Every other\n * key is a user-defined field, such as `alt` or `caption`, that is retained when\n * the asset is removed. This matches the native image input.\n */\nconst RESERVED_KEYS = new Set([\n \"_type\",\n \"_key\",\n \"_upload\",\n \"asset\",\n \"crop\",\n \"hotspot\",\n \"media\",\n])\nconst ASSET_BOUND_KEYS = new Set([\"crop\", \"hotspot\", \"_upload\"])\n\n/**\n * Describes how the cropped size fails to meet the configured limits, if at\n * all. An issue with a required dimension takes priority over a recommended\n * one.\n */\nconst getDimensionIssue = (\n dimensions: Dimensions,\n config: ResolvedImageFieldConfig\n): { tone: \"critical\" | \"caution\"; message: string } | undefined => {\n const { requiredDimensions, recommendedDimensions } = config\n if (\n requiredDimensions &&\n !dimensionsAtLeast(dimensions, requiredDimensions)\n ) {\n return {\n tone: \"critical\",\n message: `Cropped image is below the required ${requiredDimensions.width} × ${requiredDimensions.height} px. Edit the crop to enlarge it.`,\n }\n }\n if (\n recommendedDimensions &&\n !dimensionsAtLeast(dimensions, recommendedDimensions)\n ) {\n return {\n tone: \"caution\",\n message: `Cropped image is below the recommended ${recommendedDimensions.width} × ${recommendedDimensions.height} px.`,\n }\n }\n return undefined\n}\n\nconst hasFiles = (event: React.DragEvent) =>\n Array.from(event.dataTransfer.types).includes(\"Files\")\n\n/**\n * Drag-and-drop handlers matching the native image input. A depth counter\n * prevents the highlight from flickering as the pointer crosses child elements.\n * Drops are ignored while the field is read-only or mid-upload.\n */\nconst useDropHandlers = ({\n disabled,\n dragDepth,\n setDraggingOver,\n onFile,\n}: {\n disabled: boolean\n dragDepth: React.RefObject<number>\n setDraggingOver: (value: boolean) => void\n onFile: (file: File) => void\n}) => {\n return {\n onDragEnter: (event: React.DragEvent) => {\n if (disabled || !hasFiles(event)) {\n return\n }\n event.preventDefault()\n dragDepth.current += 1\n setDraggingOver(true)\n },\n onDragOver: (event: React.DragEvent) => {\n if (disabled || !hasFiles(event)) {\n return\n }\n event.preventDefault()\n event.dataTransfer.dropEffect = \"copy\"\n },\n onDragLeave: (event: React.DragEvent) => {\n if (disabled) {\n return\n }\n event.preventDefault()\n dragDepth.current = Math.max(0, dragDepth.current - 1)\n if (dragDepth.current === 0) {\n setDraggingOver(false)\n }\n },\n onDrop: (event: React.DragEvent) => {\n if (disabled) {\n return\n }\n event.preventDefault()\n dragDepth.current = 0\n setDraggingOver(false)\n const file = event.dataTransfer.files.item(0)\n if (file) {\n onFile(file)\n }\n },\n }\n}\n","import { type ImageCrop, type ImageHotspot } from \"sanity\"\nimport { clamp, clampRatio, nearestRatio } from \"./utils\"\nimport {\n type AspectRatio,\n type AspectRatioRange,\n type Dimensions,\n} from \"./types\"\n\n/**\n * A crop rectangle in source-image pixels, with `(0, 0)` at the top-left. Every\n * function in this module keeps the rectangle inside the image bounds:\n * `[0, source.width]` by `[0, source.height]`.\n */\nexport type PixelRect = {\n x: number\n y: number\n width: number\n height: number\n}\n\n/**\n * Which edge or corner is being dragged. Each component is `-1`, `0`, or `+1`.\n *\n * On each axis, `+1` drags the far edge (east or south) and `-1` drags the near\n * edge (west or north). `0` leaves the axis free: it holds its current size while\n * the ratio stays in range, and grows evenly from the center when the ratio is\n * forced to change. Corners set both components (`se` is `{ dx: 1, dy: 1 }`);\n * sides set one and leave the other `0` (`e` is `{ dx: 1, dy: 0 }`).\n */\nexport type Direction = { dx: -1 | 0 | 1; dy: -1 | 0 | 1 }\n\n/**\n * The smallest a crop edge can shrink to during a drag, in source pixels. This\n * only stops the rectangle collapsing to nothing; warning about an undersized\n * crop is handled separately by the dimension checks.\n */\nconst MIN_CROP_SIZE = 24\n\n/**\n * The aspect-ratio range used when a field sets no constraint. It is wide enough\n * to admit any real image ratio, so the editor behaves as a free-form cropper.\n */\nexport const FREE_ASPECT_RATIO_RANGE: AspectRatioRange = {\n min: 0.01,\n max: 100,\n}\n\n/**\n * Resolve an author-supplied {@link AspectRatio} into the internal form the crop\n * math uses: an `aspectRatioRange`, plus `snapRatios` for the array form.\n *\n * A single number becomes a range with equal bounds. A range passes through\n * unchanged. An array becomes a range spanning its smallest and largest members,\n * with its values sorted and de-duplicated as `snapRatios`. `undefined` falls\n * back to {@link FREE_ASPECT_RATIO_RANGE}.\n */\nexport const resolveAspectRatio = (\n input: AspectRatio | undefined\n): { aspectRatioRange: AspectRatioRange; snapRatios?: number[] } => {\n if (input === undefined) {\n return { aspectRatioRange: FREE_ASPECT_RATIO_RANGE }\n }\n if (typeof input === \"number\") {\n return { aspectRatioRange: { min: input, max: input } }\n }\n if (Array.isArray(input)) {\n const snapRatios = Array.from(new Set(input)).toSorted((a, b) => a - b)\n return {\n aspectRatioRange: {\n min: Math.min(...snapRatios),\n max: Math.max(...snapRatios),\n },\n snapRatios,\n }\n }\n return { aspectRatioRange: { min: input.min, max: input.max } }\n}\n\n/**\n * The largest centered crop the source can hold at the given aspect ratio. Fills\n * the full width when possible, and falls back to the full height when the image\n * is too short to fit that width.\n */\nconst largestRectForRatio = (source: Dimensions, ratio: number): PixelRect => {\n let width = source.width\n let height = width / ratio\n if (height > source.height) {\n height = source.height\n width = height * ratio\n }\n return {\n x: (source.width - width) / 2,\n y: (source.height - height) / 2,\n width,\n height,\n }\n}\n\n/**\n * The crop shown when an image is first uploaded: the largest crop that fits the\n * aspect-ratio constraint.\n *\n * With `snapRatios`, it targets the allowed ratio nearest the image's natural\n * ratio. Otherwise it clamps the natural ratio into the range, which uses the\n * whole image when that ratio already fits.\n */\nexport const defaultCrop = (\n source: Dimensions,\n range: AspectRatioRange,\n snapRatios?: number[]\n): PixelRect => {\n const sourceRatio = source.width / source.height\n const ratio = snapRatios\n ? nearestRatio(sourceRatio, snapRatios)\n : clampRatio(sourceRatio, range)\n return largestRectForRatio(source, ratio)\n}\n\n/** Move a crop without resizing it, keeping it inside the image. */\nexport const moveRect = (\n rect: PixelRect,\n deltaX: number,\n deltaY: number,\n source: Dimensions\n): PixelRect => ({\n ...rect,\n x: clamp(rect.x + deltaX, 0, source.width - rect.width),\n y: clamp(rect.y + deltaY, 0, source.height - rect.height),\n})\n\n/**\n * Resize a crop by dragging an edge or corner toward `pointer`, in the direction\n * `dir` (see {@link Direction}). `pointer` is in source pixels.\n *\n * The opposite edge stays fixed while the dragged edge follows the pointer; on a\n * free axis, the midpoint stays fixed instead. A side handle fixes the dragged\n * dimension from the pointer and holds the perpendicular dimension at its current\n * size, so the ratio follows the dragged edge. The perpendicular axis only\n * changes when that ratio is clamped to the range or snapped to a different\n * member, at which point it grows centered to realize the new ratio. The result\n * is constrained in this order: it stays inside the image, its ratio stays within\n * `range`, and it does not shrink below the minimum size. Width is computed first\n * and then reconciled against the available height, so the result is always a\n * valid rectangle.\n *\n * Options:\n * - `lockRatio` holds the crop's current ratio, even on a corner. Without it, a\n * corner takes its ratio from the pointer.\n * - `mirror` anchors both axes to the center, so the crop grows evenly outward.\n * - `snapRatios` snaps the ratio to its nearest member on every move, so a\n * snapping crop never shows an in-between ratio mid-drag.\n *\n * With no options, a corner drag is the standard opposite-corner resize.\n */\nexport const resize = (\n rect: PixelRect,\n dir: Direction,\n pointer: { x: number; y: number },\n source: Dimensions,\n range: AspectRatioRange,\n opts: { lockRatio?: boolean; mirror?: boolean; snapRatios?: number[] } = {}\n): PixelRect => {\n const { lockRatio = false, mirror = false, snapRatios } = opts\n\n // An axis is \"centered\" when the crop grows evenly outward from its midpoint:\n // either `dir` leaves the axis free (component 0), or `mirror` centers both\n // axes regardless of direction.\n const centeredX = mirror || dir.dx === 0\n const centeredY = mirror || dir.dy === 0\n\n // The anchor is the point that stays fixed as the crop resizes: a `+1` axis\n // anchors its near edge, a `-1` axis its far edge, a centered axis its midpoint.\n const anchorX = centeredX\n ? rect.x + rect.width / 2\n : dir.dx > 0\n ? rect.x\n : rect.x + rect.width\n const anchorY = centeredY\n ? rect.y + rect.height / 2\n : dir.dy > 0\n ? rect.y\n : rect.y + rect.height\n\n const pointerX = clamp(pointer.x, 0, source.width)\n const pointerY = clamp(pointer.y, 0, source.height)\n\n // Room available from the anchor to the image edge, in the drag direction. A\n // centered axis grows both ways at once, so it is capped at twice the distance\n // to the nearer edge.\n const availWidth = centeredX\n ? 2 * Math.min(anchorX, source.width - anchorX)\n : dir.dx > 0\n ? source.width - anchorX\n : anchorX\n const availHeight = centeredY\n ? 2 * Math.min(anchorY, source.height - anchorY)\n : dir.dy > 0\n ? source.height - anchorY\n : anchorY\n\n // The size the pointer implies on each axis. A centered axis extends to both\n // sides of the anchor, so it is twice the pointer's distance from it.\n const desiredWidth = (centeredX ? 2 : 1) * Math.abs(pointerX - anchorX)\n const desiredHeight = (centeredY ? 2 : 1) * Math.abs(pointerY - anchorY)\n\n // The ratio the gesture implies.\n // - lockRatio (or a fixed ratio) holds the crop's current ratio.\n // - A side handle fixes one dimension from the pointer and holds the\n // perpendicular dimension at its current size, so the ratio follows the\n // dragged edge. When that ratio leaves the range or snaps to a different\n // member, the perpendicular axis adjusts to realize it.\n // - A corner reads the ratio from the pointer on both axes.\n const askedRatio = lockRatio\n ? rect.width / rect.height\n : dir.dx === 0\n ? desiredHeight === 0\n ? range.max\n : rect.width / desiredHeight\n : dir.dy === 0\n ? desiredWidth / rect.height\n : desiredHeight === 0\n ? range.max\n : desiredWidth / desiredHeight\n // With a snap set, snap to the nearest allowed ratio on every move so the crop\n // never shows an in-between ratio. Otherwise clamp the ratio to the range.\n const ratio = snapRatios\n ? nearestRatio(askedRatio, snapRatios)\n : clampRatio(askedRatio, range)\n\n // Derive the width first. An active horizontal axis follows the pointer\n // directly; when only the vertical axis is active, the width comes from the\n // pointer's height via the ratio.\n const targetWidth = dir.dx !== 0 ? desiredWidth : desiredHeight * ratio\n\n let width = Math.min(targetWidth, availWidth)\n let height = width / ratio\n if (height > availHeight) {\n height = availHeight\n width = height * ratio\n }\n\n // Apply the minimum size without breaking the ratio or the bounds. The minimum\n // is itself capped by the available space, so a valid rectangle always exists,\n // even for a tiny image.\n const minWidth = Math.min(MIN_CROP_SIZE, availWidth, availHeight * ratio)\n if (width < minWidth) {\n width = minWidth\n height = width / ratio\n }\n\n // Place the top-left corner relative to the anchors: a centered axis stays\n // centered on its anchor; a `+1` axis grows away from the anchor, a `-1` axis\n // grows back toward it.\n return {\n x: centeredX ? anchorX - width / 2 : dir.dx > 0 ? anchorX : anchorX - width,\n y: centeredY\n ? anchorY - height / 2\n : dir.dy > 0\n ? anchorY\n : anchorY - height,\n width,\n height,\n }\n}\n\n/**\n * Re-fit a rectangle to an exact aspect ratio, keeping its center and staying\n * inside the image. It only shrinks the longer axis to reach the ratio, so the\n * result still fits the source, then clamps the position back into bounds.\n *\n * Used to bring a stored crop onto an allowed ratio when the editor opens. During\n * a drag, {@link resize} does the snapping instead, since it keeps the dragged\n * edge's anchor fixed.\n */\nexport const applyRatio = (\n rect: PixelRect,\n ratio: number,\n source: Dimensions\n): PixelRect => {\n const centerX = rect.x + rect.width / 2\n const centerY = rect.y + rect.height / 2\n const width = Math.min(rect.width, rect.height * ratio)\n const height = width / ratio\n return {\n x: clamp(centerX - width / 2, 0, source.width - width),\n y: clamp(centerY - height / 2, 0, source.height - height),\n width,\n height,\n }\n}\n\n/**\n * Convert a pointer in client coordinates into a focal-point coordinate in\n * `[0, 1]`, measured against the rendered image box and clamped to that box.\n *\n * This is the focal point's coordinate space, the inverse of positioning the\n * marker at `hotspot * displaySize`. It is separate from the source-pixel crop\n * geometry above.\n */\nexport const toNormalizedPoint = (\n pointer: { x: number; y: number },\n box: { left: number; top: number; width: number; height: number }\n): { x: number; y: number } => ({\n x: clamp((pointer.x - box.left) / box.width, 0, 1),\n y: clamp((pointer.y - box.top) / box.height, 0, 1),\n})\n\n/**\n * Convert a stored hotspot into a focal point relative to the crop, in `[0, 1]`,\n * where `{ 0.5, 0.5 }` is the center of the crop. The stored x/y are\n * source-relative, so this re-projects them into the cropped frame the same way\n * the image URL builder does, then clamps the result so the point stays inside\n * the crop.\n */\nexport const hotspotToFocalPoint = (\n hotspot: { x: number; y: number },\n rect: PixelRect,\n source: Dimensions\n): { x: number; y: number } => ({\n x: clamp((hotspot.x * source.width - rect.x) / rect.width, 0, 1),\n y: clamp((hotspot.y * source.height - rect.y) / rect.height, 0, 1),\n})\n\n/**\n * Convert a crop-relative focal point back into a source-relative hotspot, the\n * form Sanity stores and the image URL builder reads. The builder re-projects\n * these x/y through the crop, so a centered focal point renders at the center of\n * the cropped image. `base` supplies the hotspot's `_type` and the focal\n * region's size.\n *\n * The focal region (width/height) is clamped to the crop's source-relative size.\n * The image URL builder ignores these dimensions, but other tools draw them as an\n * ellipse, and a region larger than the crop would spill past the cropped image.\n * Sanity's whole-image default of 1 always exceeds a real crop, so it is shrunk\n * to fit the crop; a region already inside the crop is left unchanged, and an\n * uncropped full-frame image keeps its region as-is.\n */\nexport const focalPointToHotspot = (\n point: { x: number; y: number },\n rect: PixelRect,\n source: Dimensions,\n base: ImageHotspot\n): ImageHotspot => ({\n ...base,\n x: (rect.x + point.x * rect.width) / source.width,\n y: (rect.y + point.y * rect.height) / source.height,\n width: Math.min(base.width, rect.width / source.width),\n height: Math.min(base.height, rect.height / source.height),\n})\n\n/**\n * Convert a pixel crop rectangle into Sanity's non-destructive crop format:\n * fractional insets from each edge of the source. Stored on the image value, so\n * any consumer using the image URL builder renders the crop automatically.\n */\nexport const pixelRectToCrop = (\n rect: PixelRect,\n source: Dimensions\n): ImageCrop => ({\n _type: \"sanity.imageCrop\",\n top: Math.max(0, rect.y / source.height),\n left: Math.max(0, rect.x / source.width),\n right: Math.max(0, (source.width - rect.x - rect.width) / source.width),\n bottom: Math.max(0, (source.height - rect.y - rect.height) / source.height),\n})\n\n/** Convert Sanity's fractional crop insets back into a pixel rectangle. */\nexport const cropToPixelRect = (\n crop: ImageCrop,\n source: Dimensions\n): PixelRect => ({\n x: crop.left * source.width,\n y: crop.top * source.height,\n width: (1 - crop.left - crop.right) * source.width,\n height: (1 - crop.top - crop.bottom) * source.height,\n})\n\n/** The cropped image's pixel size, rounded to whole pixels for display and comparison. */\nexport const croppedDimensions = (rect: PixelRect): Dimensions => ({\n width: Math.round(rect.width),\n height: Math.round(rect.height),\n})\n\n/** Whether dimensions clear a minimum on both axes. */\nexport const dimensionsAtLeast = (\n dimensions: Dimensions,\n threshold: Dimensions\n) =>\n dimensions.width >= threshold.width && dimensions.height >= threshold.height\n\n/**\n * Format an aspect ratio for display. Prefers a whole-number `w:h` label with a\n * small denominator (e.g. `16:9`), and falls back to a decimal (e.g. `2.35:1`)\n * when no small pair is close enough. The denominator cap is what keeps 2.35\n * from rendering as `47:20`.\n */\nexport const formatAspectRatio = (ratio: number): string => {\n for (let height = 1; height <= 16; height++) {\n const width = ratio * height\n const rounded = Math.round(width)\n if (rounded >= 1 && Math.abs(width - rounded) < 0.02) {\n return `${rounded}:${height}`\n }\n }\n return `${ratio.toFixed(2)}:1`\n}\n","import { type AspectRatio, type AspectRatioRange } from \"./types\"\n\n/**\n * Validate an authored {@link AspectRatio}, throwing an error when the\n * constraint is invalid. A single ratio must be finite and positive. An array\n * must be non-empty with every member finite and positive. A range adds checks\n * that min and max are finite, positive, and ordered.\n */\nexport const validateAspectRatio = (input: AspectRatio): void => {\n if (typeof input === \"number\") {\n if (!Number.isFinite(input) || input <= 0) {\n throw new Error(\"aspectRatio must be a finite number greater than 0.\")\n }\n return\n }\n\n if (Array.isArray(input)) {\n if (input.length === 0) {\n throw new Error(\"aspectRatio must not be an empty array.\")\n }\n if (input.some((ratio) => !Number.isFinite(ratio) || ratio <= 0)) {\n throw new Error(\n \"aspectRatio array values must be finite numbers greater than 0.\"\n )\n }\n return\n }\n\n if (!Number.isFinite(input.min) || !Number.isFinite(input.max)) {\n throw new Error(\"aspectRatio min/max must be finite numbers.\")\n }\n if (input.min <= 0 || input.max <= 0) {\n throw new Error(\"aspectRatio min and max must be greater than 0.\")\n }\n if (input.max < input.min) {\n throw new Error(\"aspectRatio max must be greater than or equal to min.\")\n }\n}\n\nexport const clamp = (value: number, min: number, max: number) =>\n Math.min(Math.max(value, min), max)\n\nexport const clampRatio = (ratio: number, range: AspectRatioRange) =>\n clamp(ratio, range.min, range.max)\n\n/**\n * The member of `ratios` closest to `ratio`. Snaps a freely-dragged crop to one\n * of the allowed ratios. `ratios` must be non-empty.\n */\nexport const nearestRatio = (ratio: number, ratios: number[]): number =>\n ratios.reduce((best, candidate) =>\n Math.abs(candidate - ratio) < Math.abs(best - ratio) ? candidate : best\n )\n","import { useState } from \"react\"\nimport { Box, Button, Card, Dialog, Flex, Stack, Text } from \"@sanity/ui\"\nimport {\n CheckmarkCircleIcon,\n ErrorOutlineIcon,\n ResetIcon,\n WarningOutlineIcon,\n} from \"@sanity/icons\"\nimport { type ImageCrop, type ImageHotspot } from \"sanity\"\nimport {\n applyRatio,\n croppedDimensions,\n cropToPixelRect,\n defaultCrop,\n dimensionsAtLeast,\n focalPointToHotspot,\n formatAspectRatio,\n hotspotToFocalPoint,\n type PixelRect,\n pixelRectToCrop,\n} from \"./cropMath\"\nimport { nearestRatio } from \"./utils\"\nimport { CropArea } from \"./CropArea\"\nimport { type SanityImageSource, sourceImageUrl } from \"./imageUrl\"\nimport { type Dimensions, type ResolvedImageFieldConfig } from \"./types\"\n\ntype CropDialogProps = {\n assetRef: string\n imageSource: SanityImageSource\n sourceDimensions: Dimensions\n config: ResolvedImageFieldConfig\n initialCrop?: ImageCrop\n hotspotEnabled?: boolean\n initialHotspot?: ImageHotspot\n onConfirm: (result: { crop: ImageCrop; hotspot?: ImageHotspot }) => void\n onClose: () => void\n}\n\n/**\n * The default hotspot stored when focal-point editing is enabled and no hotspot\n * exists yet: centered, with a minimal footprint. Width and height are\n * normalized fractions of the image's coordinate space, not pixels. The render\n * path reads only x/y and ignores the size, so these values only keep the stored\n * hotspot well-formed for tools that draw the focal ellipse. They are 0.01\n * rather than 1 because a width of 1 spans the whole image, which is valid only\n * when the focal region can fill the available rectangle. A focal point is a\n * small target, so it defaults to a small dot.\n */\nconst CENTERED_HOTSPOT: ImageHotspot = {\n _type: \"sanity.imageHotspot\",\n x: 0.5,\n y: 0.5,\n width: 0.01,\n height: 0.01,\n}\n\n/**\n * The modal crop editor. It opens on the existing crop when re-cropping, or on\n * the largest valid crop on first upload. It shows the current dimensions and\n * disables confirmation while the crop is under the required size. With\n * `hotspotEnabled`, it also edits a focal point.\n */\nexport const CropDialog = ({\n assetRef,\n imageSource,\n sourceDimensions,\n config,\n initialCrop,\n hotspotEnabled,\n initialHotspot,\n onConfirm,\n onClose,\n}: CropDialogProps) => {\n const {\n aspectRatioRange,\n snapRatios,\n recommendedDimensions,\n requiredDimensions,\n } = config\n\n const [rect, setRect] = useState<PixelRect>(() => {\n if (!initialCrop) {\n return defaultCrop(sourceDimensions, aspectRatioRange, snapRatios)\n }\n // Snap a stored crop to an allowed ratio on open, so a snap-set field never\n // shows an in-between ratio.\n const loaded = cropToPixelRect(initialCrop, sourceDimensions)\n return snapRatios\n ? applyRatio(\n loaded,\n nearestRatio(loaded.width / loaded.height, snapRatios),\n sourceDimensions\n )\n : loaded\n })\n\n const [focalPoint, setFocalPoint] = useState(() =>\n initialHotspot\n ? hotspotToFocalPoint(initialHotspot, rect, sourceDimensions)\n : { x: 0.5, y: 0.5 }\n )\n\n const dimensions = croppedDimensions(rect)\n const belowRequired =\n requiredDimensions && !dimensionsAtLeast(dimensions, requiredDimensions)\n const belowRecommended =\n recommendedDimensions &&\n !dimensionsAtLeast(dimensions, recommendedDimensions)\n\n return (\n <Dialog\n id=\"aspect-ratio-crop-dialog\"\n header=\"Crop image\"\n width={2}\n onClose={onClose}\n footer={\n <Flex padding={3} justify=\"space-between\" align=\"center\" gap={3}>\n <Button\n mode=\"bleed\"\n icon={ResetIcon}\n text=\"Reset crop\"\n fontSize={1}\n onClick={() => {\n setRect(\n defaultCrop(sourceDimensions, aspectRatioRange, snapRatios)\n )\n }}\n />\n <Flex gap={2}>\n <Button mode=\"ghost\" text=\"Cancel\" onClick={onClose} />\n <Button\n tone=\"primary\"\n text=\"Confirm crop\"\n disabled={belowRequired}\n onClick={() => {\n onConfirm({\n crop: pixelRectToCrop(rect, sourceDimensions),\n hotspot: hotspotEnabled\n ? focalPointToHotspot(\n focalPoint,\n rect,\n sourceDimensions,\n initialHotspot ?? CENTERED_HOTSPOT\n )\n : undefined,\n })\n }}\n />\n </Flex>\n </Flex>\n }\n >\n <Stack gap={3} padding={3}>\n <CropArea\n imageUrl={sourceImageUrl(assetRef, imageSource)}\n sourceDimensions={sourceDimensions}\n range={aspectRatioRange}\n snapRatios={snapRatios}\n rect={rect}\n onChange={setRect}\n focalPoint={hotspotEnabled ? focalPoint : undefined}\n onFocalPointChange={hotspotEnabled ? setFocalPoint : undefined}\n />\n\n <Flex justify=\"space-between\" align=\"center\" gap={3} wrap=\"wrap\">\n <Text size={1} muted>\n Allowed aspect ratio: {describeAspectRatio(config)}\n </Text>\n <Text size={1} weight=\"medium\">\n {dimensions.width} × {dimensions.height} px\n </Text>\n </Flex>\n\n {hotspotEnabled && (\n <Text size={1} muted>\n Drag the circle to set the focal point. It governs how cover-mode\n crops frame the image downstream, not this preview.\n </Text>\n )}\n\n <DimensionStatusMessage\n requiredDimensions={requiredDimensions}\n recommendedDimensions={recommendedDimensions}\n belowRequired={belowRequired}\n belowRecommended={belowRecommended}\n />\n </Stack>\n </Dialog>\n )\n}\n\nconst StatusMessage = ({\n tone,\n icon,\n message,\n}: {\n tone: \"critical\" | \"caution\" | \"positive\"\n icon: React.ReactNode\n message: string\n}) => (\n <Card tone={tone} padding={3} radius={2} border>\n <Flex gap={3} align=\"center\">\n <Text size={1}>{icon}</Text>\n <Box flex={1}>\n <Text size={1} textOverflow=\"ellipsis\">\n {message}\n </Text>\n </Box>\n </Flex>\n </Card>\n)\n\n/**\n * The size status shown beneath the crop. Renders a message when the field\n * sets a required or recommended size: a warning when the crop is too small,\n * or a confirmation when it meets both limits. Renders nothing when no size\n * limits are set. Required limits take priority over recommended ones.\n */\nconst DimensionStatusMessage = ({\n requiredDimensions,\n recommendedDimensions,\n belowRequired,\n belowRecommended,\n}: {\n requiredDimensions?: Dimensions\n recommendedDimensions?: Dimensions\n belowRequired?: boolean\n belowRecommended?: boolean\n}) => {\n if (requiredDimensions && belowRequired) {\n return (\n <StatusMessage\n tone=\"critical\"\n icon={<ErrorOutlineIcon />}\n message={`Crop must be at least ${describeDimensions(requiredDimensions)}.`}\n />\n )\n }\n if (recommendedDimensions && belowRecommended) {\n return (\n <StatusMessage\n tone=\"caution\"\n icon={<WarningOutlineIcon />}\n message={`For best results, keep the crop at least ${describeDimensions(\n recommendedDimensions\n )}.`}\n />\n )\n }\n if (requiredDimensions || recommendedDimensions) {\n // Keep the dialog height stable as the crop crosses size thresholds.\n return (\n <StatusMessage\n tone=\"positive\"\n icon={<CheckmarkCircleIcon />}\n message=\"Crop size looks good.\"\n />\n )\n }\n return null\n}\n\nconst describeDimensions = (dimensions: Dimensions) =>\n `${dimensions.width} × ${dimensions.height} px`\n\n/**\n * Build the \"Allowed aspect ratio\" label. A snap set of three or fewer ratios\n * lists them all, and a larger set shows its extremes and a count. A continuous\n * field shows a single ratio when min equals max, otherwise a range.\n */\nconst describeAspectRatio = ({\n aspectRatioRange,\n snapRatios,\n}: ResolvedImageFieldConfig): string => {\n if (snapRatios) {\n if (snapRatios.length <= 3) {\n return snapRatios.map(formatAspectRatio).join(\", \")\n }\n const lo = formatAspectRatio(Math.min(...snapRatios))\n const hi = formatAspectRatio(Math.max(...snapRatios))\n return `${snapRatios.length} ratios (${lo} – ${hi})`\n }\n return aspectRatioRange.min === aspectRatioRange.max\n ? formatAspectRatio(aspectRatioRange.min)\n : `${formatAspectRatio(aspectRatioRange.min)} – ${formatAspectRatio(\n aspectRatioRange.max\n )}`\n}\n","import { useCallback, useRef } from \"react\"\nimport { useContentWidth } from \"./useContentWidth\"\nimport {\n type Direction,\n moveRect,\n type PixelRect,\n resize,\n toNormalizedPoint,\n} from \"./cropMath\"\nimport { type AspectRatioRange, type Dimensions } from \"./types\"\n\ntype CropAreaProps = {\n imageUrl: string\n sourceDimensions: Dimensions\n range: AspectRatioRange\n /** Ratios to snap to while dragging; absent means continuous (free) dragging. */\n snapRatios?: number[]\n /** The current crop, in source-pixel coordinates. */\n rect: PixelRect\n onChange: (rect: PixelRect) => void\n /**\n * The current focal point, relative to the crop, in `[0, 1]`. `{ 0.5, 0.5 }`\n * is the center of the crop. Pass this with `onFocalPointChange` to show the\n * draggable marker.\n */\n focalPoint?: { x: number; y: number }\n onFocalPointChange?: (point: { x: number; y: number }) => void\n /** The tallest the image is allowed to get, in CSS pixels. */\n maxHeight?: number\n}\n\ntype Drag =\n | {\n mode: \"move\"\n startRect: PixelRect\n startPointer: { x: number; y: number }\n }\n | { mode: \"resize\"; dir: Direction; startRect: PixelRect }\n\n/**\n * The eight resize handles, as direction vectors. Four corners are drawn as\n * dots and four sides as bars. Each handle's position, shape, and cursor are\n * derived from its vector (see {@link Handle}).\n */\nconst HANDLES: Direction[] = [\n { dx: -1, dy: -1 },\n { dx: 0, dy: -1 },\n { dx: 1, dy: -1 },\n { dx: 1, dy: 0 },\n { dx: 1, dy: 1 },\n { dx: 0, dy: 1 },\n { dx: -1, dy: 1 },\n { dx: -1, dy: 0 },\n]\n\nconst HANDLE_SIZE = 14 // corner dot diameter, px\nconst BAR_LENGTH = 22 // side bar long edge, px\nconst BAR_THICKNESS = 8 // side bar short edge, px\nconst HOTSPOT_SIZE = 22 // focal-point marker diameter, px\nconst KEYBOARD_STEP = 0.02 // fraction of the source moved or resized per arrow press\n\nconst CROP_ARIA_LABEL =\n \"Crop selection. Use arrow keys to move, hold Shift with arrow keys to resize.\"\n\n/**\n * The pointer-drag lifecycle, built on pointer capture. The crop frame, the\n * resize handles, and the hotspot marker all use this hook. It works for touch\n * as well as mouse.\n *\n * `start` captures the pointer and stores the caller's drag state. `onMove` runs\n * on every move during a drag. Cleanup is bound to lostpointercapture rather\n * than pointerup because a touch gesture often ends with a pointercancel\n * instead, on scroll, palm rejection, or context menu. lostpointercapture fires\n * after all of those, so a canceled touch ends the drag rather than leaving it\n * active.\n */\nfunction usePointerDrag<T>(\n onMove: (state: T, event: React.PointerEvent) => void\n) {\n const dragRef = useRef<T | null>(null)\n\n const start = useCallback(\n (\n event: React.PointerEvent,\n state: T,\n { stopPropagation = false }: { stopPropagation?: boolean } = {}\n ) => {\n event.preventDefault()\n if (stopPropagation) {\n // Prevent a press on a handle or marker from also starting a crop move\n // on the frame underneath it.\n event.stopPropagation()\n }\n event.currentTarget.setPointerCapture(event.pointerId)\n dragRef.current = state\n },\n []\n )\n\n const handlePointerMove = useCallback(\n (event: React.PointerEvent) => {\n const state = dragRef.current\n if (state === null) {\n return\n }\n onMove(state, event)\n },\n [onMove]\n )\n\n const handleLostPointerCapture = useCallback(() => {\n dragRef.current = null\n }, [])\n\n return { start, handlePointerMove, handleLostPointerCapture }\n}\n\n/**\n * The interactive cropping surface. It shows a scaled-down preview of the source\n * image with a draggable, ratio-constrained crop rectangle, and a focal-point\n * marker when enabled. The geometry is computed in source-pixel space (see\n * cropMath) and only scaled for display, so the result is independent of display\n * resolution.\n */\nexport const CropArea = ({\n imageUrl,\n sourceDimensions,\n range,\n snapRatios,\n rect,\n onChange,\n focalPoint,\n onFocalPointChange,\n maxHeight = 460,\n}: CropAreaProps) => {\n const frameRef = useRef<HTMLDivElement>(null)\n const imageBoxRef = useRef<HTMLDivElement>(null)\n const frameWidth = useContentWidth(frameRef)\n\n const scale = displayScale(sourceDimensions, frameWidth, maxHeight)\n const displayWidth = sourceDimensions.width * scale\n const displayHeight = sourceDimensions.height * scale\n\n const cropBox = {\n left: rect.x * scale,\n top: rect.y * scale,\n width: rect.width * scale,\n height: rect.height * scale,\n }\n\n const pointerToSource = useCallback(\n (event: { clientX: number; clientY: number }) => {\n const box = imageBoxRef.current?.getBoundingClientRect()\n if (!box || scale === 0) {\n return { x: 0, y: 0 }\n }\n return {\n x: (event.clientX - box.left) / scale,\n y: (event.clientY - box.top) / scale,\n }\n },\n [scale]\n )\n\n const pointerToFocal = useCallback(\n (event: { clientX: number; clientY: number }) => {\n const box = imageBoxRef.current?.getBoundingClientRect()\n if (!box || cropBox.width === 0 || cropBox.height === 0) {\n return { x: 0, y: 0 }\n }\n return toNormalizedPoint(\n { x: event.clientX, y: event.clientY },\n {\n left: box.left + cropBox.left,\n top: box.top + cropBox.top,\n width: cropBox.width,\n height: cropBox.height,\n }\n )\n },\n [cropBox.left, cropBox.top, cropBox.width, cropBox.height]\n )\n\n const onCropMove = useCallback(\n (drag: Drag, event: React.PointerEvent) => {\n const pointer = pointerToSource(event)\n const next =\n drag.mode === \"move\"\n ? moveRect(\n drag.startRect,\n pointer.x - drag.startPointer.x,\n pointer.y - drag.startPointer.y,\n sourceDimensions\n )\n : // Modifier keys are read at move time so they can change mid-drag.\n // Shift locks the ratio; Option/Alt resizes from center. With\n // snapRatios, the crop snaps to the nearest allowed ratio.\n resize(drag.startRect, drag.dir, pointer, sourceDimensions, range, {\n lockRatio: event.shiftKey,\n mirror: event.altKey,\n snapRatios,\n })\n onChange(next)\n },\n [onChange, pointerToSource, range, snapRatios, sourceDimensions]\n )\n\n const cropDrag = usePointerDrag<Drag>(onCropMove)\n\n const onFocalMove = useCallback(\n (_active: boolean, event: React.PointerEvent) => {\n if (!onFocalPointChange) {\n return\n }\n onFocalPointChange(pointerToFocal(event))\n },\n [onFocalPointChange, pointerToFocal]\n )\n\n const focalDrag = usePointerDrag<boolean>(onFocalMove)\n\n const handleKeyDown = (event: React.KeyboardEvent) => {\n const stepX = sourceDimensions.width * KEYBOARD_STEP\n const stepY = sourceDimensions.height * KEYBOARD_STEP\n\n if (event.shiftKey) {\n // Shift+Arrow resizes the crop from the south-east corner with the ratio\n // locked, so the north-west corner stays fixed. Vertical steps are scaled\n // by the ratio so one press moves the south edge by a single step. resize\n // applies the bounds, the minimum size, and ratio snapping when snapRatios\n // is set.\n const ratio = rect.width / rect.height\n const widthSteps = new Map<string, number>([\n [\"ArrowRight\", stepX],\n [\"ArrowLeft\", -stepX],\n [\"ArrowDown\", stepY * ratio],\n [\"ArrowUp\", -stepY * ratio],\n ])\n const widthStep = widthSteps.get(event.key)\n if (widthStep === undefined) {\n return\n }\n event.preventDefault()\n const east = rect.x + rect.width\n const south = rect.y + rect.height\n onChange(\n resize(\n rect,\n { dx: 1, dy: 1 },\n { x: east + widthStep, y: south },\n sourceDimensions,\n range,\n { lockRatio: true, snapRatios }\n )\n )\n return\n }\n\n const moves = new Map<string, [number, number]>([\n [\"ArrowLeft\", [-stepX, 0]],\n [\"ArrowRight\", [stepX, 0]],\n [\"ArrowUp\", [0, -stepY]],\n [\"ArrowDown\", [0, stepY]],\n ])\n const delta = moves.get(event.key)\n if (!delta) {\n return\n }\n event.preventDefault()\n onChange(moveRect(rect, delta[0], delta[1], sourceDimensions))\n }\n\n return (\n <div\n ref={frameRef}\n style={{\n width: \"100%\",\n display: \"flex\",\n justifyContent: \"center\",\n userSelect: \"none\",\n touchAction: \"none\",\n }}\n >\n <div\n ref={imageBoxRef}\n style={{\n position: \"relative\",\n width: displayWidth,\n height: displayHeight,\n maxWidth: \"100%\",\n }}\n >\n <img\n src={imageUrl}\n alt=\"\"\n draggable={false}\n style={{\n display: \"block\",\n width: \"100%\",\n height: \"100%\",\n objectFit: \"fill\",\n }}\n />\n\n {scale > 0 && (\n <>\n {/*\n Darken everything outside the crop with a large box-shadow on a\n rectangle positioned at the crop. The shadow extends far in every\n direction, so the wrapper clips it (overflow: hidden, sized to the\n image box) to keep the darkening within the image and off the rest\n of the dialog. pointerEvents is none, so it does not intercept a\n crop drag.\n */}\n <div\n aria-hidden\n style={{\n position: \"absolute\",\n inset: 0,\n overflow: \"hidden\",\n pointerEvents: \"none\",\n }}\n >\n <div\n style={{\n position: \"absolute\",\n ...cropBox,\n boxShadow: \"0 0 0 9999px rgba(0, 0, 0, 0.55)\",\n }}\n />\n </div>\n\n {/* The frame is unclipped so edge handles stay visible. */}\n {/* oxlint-disable jsx-a11y/no-noninteractive-tabindex, jsx-a11y/no-noninteractive-element-interactions, jsx-a11y/prefer-tag-over-role */}\n <div\n role=\"group\"\n aria-label={CROP_ARIA_LABEL}\n tabIndex={0}\n onPointerDown={(event) => {\n cropDrag.start(event, {\n mode: \"move\",\n startRect: rect,\n startPointer: pointerToSource(event),\n })\n }}\n onPointerMove={cropDrag.handlePointerMove}\n onLostPointerCapture={cropDrag.handleLostPointerCapture}\n onKeyDown={handleKeyDown}\n style={{\n position: \"absolute\",\n ...cropBox,\n cursor: \"move\",\n outline: \"1px solid rgba(255, 255, 255, 0.9)\",\n }}\n >\n <ThirdsGuides />\n {HANDLES.map((dir) => (\n <Handle\n key={`${dir.dx},${dir.dy}`}\n dir={dir}\n onPointerDown={(event) => {\n cropDrag.start(\n event,\n { mode: \"resize\", dir, startRect: rect },\n { stopPropagation: true }\n )\n }}\n onPointerMove={cropDrag.handlePointerMove}\n onLostPointerCapture={cropDrag.handleLostPointerCapture}\n />\n ))}\n </div>\n {/* oxlint-enable jsx-a11y/no-noninteractive-tabindex, jsx-a11y/no-noninteractive-element-interactions, jsx-a11y/prefer-tag-over-role */}\n\n {focalPoint && onFocalPointChange && (\n <HotspotMarker\n left={cropBox.left + focalPoint.x * cropBox.width}\n top={cropBox.top + focalPoint.y * cropBox.height}\n onPointerDown={(event) => {\n focalDrag.start(event, true, { stopPropagation: true })\n }}\n onPointerMove={focalDrag.handlePointerMove}\n onLostPointerCapture={focalDrag.handleLostPointerCapture}\n />\n )}\n </>\n )}\n </div>\n </div>\n )\n}\n\n/**\n * Display pixels per source pixel: the largest scale that fits the image within\n * both the available width and the height cap. Zero before the frame has been\n * measured.\n */\nconst displayScale = (\n sourceDimensions: Dimensions,\n frameWidth: number,\n maxHeight: number\n) => {\n if (frameWidth === 0) {\n return 0\n }\n return Math.min(\n frameWidth / sourceDimensions.width,\n maxHeight / sourceDimensions.height\n )\n}\n\nconst ThirdsGuides = () => (\n <div\n style={{\n position: \"absolute\",\n inset: 0,\n pointerEvents: \"none\",\n backgroundImage: `\n linear-gradient(to right, transparent 33.33%, rgba(255, 255, 255, 0.35) 33.33%, rgba(255, 255, 255, 0.35) calc(33.33% + 1px), transparent calc(33.33% + 1px), transparent 66.66%, rgba(255, 255, 255, 0.35) 66.66%, rgba(255, 255, 255, 0.35) calc(66.66% + 1px), transparent calc(66.66% + 1px)),\n linear-gradient(to bottom, transparent 33.33%, rgba(255, 255, 255, 0.35) 33.33%, rgba(255, 255, 255, 0.35) calc(33.33% + 1px), transparent calc(33.33% + 1px), transparent 66.66%, rgba(255, 255, 255, 0.35) 66.66%, rgba(255, 255, 255, 0.35) calc(66.66% + 1px), transparent calc(66.66% + 1px))\n `,\n }}\n />\n)\n\nconst cursorForDirection = (dir: Direction): string => {\n if (dir.dx === 0) {\n return \"ns-resize\"\n }\n if (dir.dy === 0) {\n return \"ew-resize\"\n }\n return dir.dx === dir.dy ? \"nwse-resize\" : \"nesw-resize\"\n}\n\n/**\n * A single resize handle, derived from its {@link Direction}. Corners (both\n * components non-zero) are dots. Sides (one component zero) are bars along their\n * edge. Each handle is positioned at 0/50/100% and translated back by half its\n * size, so it is centered on the crop edge.\n */\nconst Handle = ({\n dir,\n onPointerDown,\n onPointerMove,\n onLostPointerCapture,\n}: {\n dir: Direction\n onPointerDown: (event: React.PointerEvent) => void\n onPointerMove: (event: React.PointerEvent) => void\n onLostPointerCapture: (event: React.PointerEvent) => void\n}) => {\n const isCorner = dir.dx !== 0 && dir.dy !== 0\n const horizontalBar = dir.dx === 0\n const width = isCorner\n ? HANDLE_SIZE\n : horizontalBar\n ? BAR_LENGTH\n : BAR_THICKNESS\n const height = isCorner\n ? HANDLE_SIZE\n : horizontalBar\n ? BAR_THICKNESS\n : BAR_LENGTH\n return (\n <div\n onPointerDown={onPointerDown}\n onPointerMove={onPointerMove}\n onLostPointerCapture={onLostPointerCapture}\n style={{\n position: \"absolute\",\n left: `${(dir.dx + 1) * 50}%`,\n top: `${(dir.dy + 1) * 50}%`,\n transform: \"translate(-50%, -50%)\",\n width,\n height,\n borderRadius: isCorner ? \"50%\" : BAR_THICKNESS / 2,\n background: \"#fff\",\n boxShadow: \"0 0 0 1px rgba(0, 0, 0, 0.4)\",\n cursor: cursorForDirection(dir),\n touchAction: \"none\",\n }}\n />\n )\n}\n\nconst HotspotMarker = ({\n left,\n top,\n onPointerDown,\n onPointerMove,\n onLostPointerCapture,\n}: {\n left: number\n top: number\n onPointerDown: (event: React.PointerEvent) => void\n onPointerMove: (event: React.PointerEvent) => void\n onLostPointerCapture: (event: React.PointerEvent) => void\n}) => (\n <div\n title=\"Focal point\"\n onPointerDown={onPointerDown}\n onPointerMove={onPointerMove}\n onLostPointerCapture={onLostPointerCapture}\n style={{\n position: \"absolute\",\n left,\n top,\n transform: \"translate(-50%, -50%)\",\n width: HOTSPOT_SIZE,\n height: HOTSPOT_SIZE,\n borderRadius: \"50%\",\n border: \"2px solid #fff\",\n background: \"rgba(255, 255, 255, 0.2)\",\n boxShadow: \"0 0 0 1px rgba(0, 0, 0, 0.5), 0 0 6px rgba(0, 0, 0, 0.5)\",\n cursor: \"grab\",\n touchAction: \"none\",\n }}\n >\n <div\n style={{\n position: \"absolute\",\n left: \"50%\",\n top: \"50%\",\n transform: \"translate(-50%, -50%)\",\n width: 4,\n height: 4,\n borderRadius: \"50%\",\n background: \"#fff\",\n boxShadow: \"0 0 0 1px rgba(0, 0, 0, 0.5)\",\n }}\n />\n </div>\n)\n","import { useLayoutEffect, useState } from \"react\"\n\n/**\n * Returns the content-box width of the referenced element, updating on resize.\n */\nexport function useContentWidth(\n ref: React.RefObject<HTMLElement | null>\n): number {\n const [width, setWidth] = useState(0)\n\n useLayoutEffect(() => {\n const element = ref.current\n if (!element) {\n return undefined\n }\n const observer = new ResizeObserver(([entry]) => {\n if (!entry) {\n return\n }\n setWidth(entry.contentRect.width)\n })\n observer.observe(element)\n return () => {\n observer.disconnect()\n }\n }, [ref])\n\n return width\n}\n","import { buildSrc, parseImageId } from \"@sanity-image/url-builder\"\nimport { type ImageValue } from \"sanity\"\nimport { croppedDimensions, cropToPixelRect } from \"./cropMath\"\nimport { type Dimensions } from \"./types\"\n\n/** Identifies a project/dataset so we can build CDN image URLs. */\nexport type SanityImageSource = {\n projectId: string\n dataset: string\n}\n\n/**\n * A Sanity image asset reference string with encoded dimensions.\n * Format: `image-<hash>-<width>x<height>-<format>`\n */\nexport type ImageAssetRef = `image-${string}-${number}x${number}-${string}`\n\n/**\n * An uploaded asset's original pixel size, read from its reference id\n * (`image-<hash>-<width>x<height>-<format>`). Only pass a ref that has passed\n * `isCroppable`. A reference without encoded dimensions, such as\n * `image-<hash>-svg`, makes `parseImageId` throw.\n */\nexport const sourceDimensions = (assetRef: ImageAssetRef): Dimensions =>\n parseImageId(assetRef).dimensions\n\n/**\n * Whether a reference is a Sanity SVG image asset. Sanity writes the format into\n * the reference id, and an SVG takes one of two shapes: `image-<hash>-svg`, or\n * `image-<hash>-<w>x<h>-svg` when Sanity read a size from the file's `viewBox`\n * or width and height. Both are vectors that the image API will not transform,\n * so neither can be raster-cropped. The format suffix is what marks a reference\n * an SVG, not the absence of dimensions.\n */\nexport const isSvg = (ref: string): boolean =>\n /^image-[a-f0-9]+(?:-\\d+x\\d+)?-svg$/.test(ref)\n\n/**\n * Whether a reference is a raster image this input can crop. It must carry\n * encoded `<w>x<h>` dimensions and must not be an SVG. A dimensioned SVG\n * (`image-<hash>-<w>x<h>-svg`) has the same `<w>x<h>-<format>` tail as a raster\n * id and parses cleanly, so the `isSvg` check is what keeps it out.\n */\nexport const isCroppable = (\n assetRef?: string | null\n): assetRef is ImageAssetRef =>\n Boolean(assetRef && /-\\d+x\\d+-\\w+$/.test(assetRef) && !isSvg(assetRef))\n\n/**\n * Whether a reference is a Sanity image asset this input supports: either a\n * raster id (`image-<hash>-<w>x<h>-<fmt>`) or an SVG (`image-<hash>-svg`). The\n * select-existing path uses this to reject unsupported references, such as a\n * Media Library global reference or a non-image asset.\n */\nexport const isImageAssetRef = (ref: string): boolean => {\n if (isSvg(ref)) {\n return true\n }\n try {\n const { assetId, format } = parseImageId(ref)\n return Boolean(assetId && format)\n } catch {\n return false\n }\n}\n\n/**\n * The cropped pixel size implied by an image value's stored crop, or the full\n * size when there's no crop. Returns null when the asset has no dimensions.\n * Shared by the input preview and the schema-level dimension validation.\n */\nexport const croppedDimensionsFromValue = (\n value: ImageValue | undefined\n): Dimensions | null => {\n if (!isCroppable(value?.asset?._ref)) {\n return null\n }\n\n const source = sourceDimensions(value.asset._ref)\n\n if (!value.crop) {\n return source\n }\n\n return croppedDimensions(cropToPixelRect(value.crop, source))\n}\n\n/** The base CDN path (with trailing slash) for a project/dataset's images. */\nconst cdnBaseUrl = (source: SanityImageSource) =>\n `https://cdn.sanity.io/images/${source.projectId}/${source.dataset}/`\n\n/** The CDN URL for the uncropped source image. */\nexport const sourceImageUrl = (assetRef: string, source: SanityImageSource) => {\n const { assetId, dimensions, format } = parseImageId(assetRef)\n const filename = `${assetId}-${dimensions.width}x${dimensions.height}.${format}`\n\n return `${cdnBaseUrl(source)}${filename}`\n}\n\n/**\n * A CDN URL for the cropped image. `buildSrc` applies the crop and fits the\n * width. The caller passes `displayWidth` to cap the delivered width so the\n * preview stays small.\n */\nexport const croppedImageUrl = (\n value: ImageValue,\n source: SanityImageSource,\n displayWidth: number\n): string | null => {\n const assetRef = value.asset?._ref\n if (!assetRef) {\n return null\n }\n\n // The image API does not transform SVGs, so serve the original file from the\n // CDN with no crop or width. This covers both SVG shapes: the text between\n // `image-` and `-svg` is the CDN filename stem, which is the bare hash for\n // `image-<hash>-svg` and `<hash>-<w>x<h>` for the dimensioned form.\n if (isSvg(assetRef)) {\n const assetId = assetRef.slice(\"image-\".length, -\"-svg\".length)\n return `${cdnBaseUrl(source)}${assetId}.svg`\n }\n\n const { src } = buildSrc({\n baseUrl: cdnBaseUrl(source),\n id: assetRef,\n width: Math.round(displayWidth),\n crop: value.crop,\n })\n\n return src\n}\n","/**\n * Whether a file's MIME type passes an `accept` string. This is Sanity's\n * `options.accept`, for example `\"image/png,image/jpeg\"` or `\"image/*\"`. The\n * browser applies `accept` only to the file picker. Drag-drop and paste do not\n * go through the picker, so every upload path is checked here instead.\n *\n * The list is comma-separated. A MIME type matches an explicit type\n * (`image/png`), a type glob (`image/*`), or the catch-all (`*` / `*​/​*`).\n * Matching ignores case. An empty `accept` matches every type.\n */\nexport const acceptsType = (mimeType: string, accept: string): boolean => {\n const type = mimeType.trim().toLowerCase()\n const patterns = accept\n .split(\",\")\n .map((entry) => entry.trim().toLowerCase())\n .filter(Boolean)\n\n if (patterns.length === 0) {\n return true\n }\n\n return patterns.some((pattern) => {\n if (pattern === \"*\" || pattern === \"*/*\") {\n return true\n }\n if (pattern.endsWith(\"/*\")) {\n return type.startsWith(pattern.slice(0, -1))\n }\n return type === pattern\n })\n}\n\n/**\n * Whether an event target is a text-entry control: `<input>`, `<textarea>`, or\n * a `contenteditable` element. Paste-to-upload uses this to skip pastes aimed at\n * the field's `alt` and `caption` inputs, which sit in the same container.\n */\nexport const isTextEntryTarget = (target: EventTarget | null): boolean => {\n if (!(target instanceof HTMLElement)) {\n return false\n }\n if (target.tagName === \"INPUT\" || target.tagName === \"TEXTAREA\") {\n return true\n }\n // `isContentEditable` reflects inherited contenteditable in real browsers but\n // is not implemented in jsdom, where it is `undefined`. The attribute checks\n // run last so the return value is a boolean in that case.\n const attribute = target.getAttribute(\"contenteditable\")\n return target.isContentEditable || attribute === \"\" || attribute === \"true\"\n}\n","import { defineField, type ImageDefinition, type ImageValue } from \"sanity\"\nimport { ImageFieldInput } from \"./ImageFieldInput\"\nimport { dimensionsAtLeast } from \"./cropMath\"\nimport { validateAspectRatio } from \"./utils\"\nimport { croppedDimensionsFromValue } from \"./imageUrl\"\nimport { type Dimensions, type ImageFieldOptions } from \"./types\"\n\n/**\n * Parameters for {@link defineImageField}. It is an image field definition\n * without `type` and `components`, which {@link defineImageField} sets. The\n * {@link ImageFieldOptions} constraints are added as top-level props. A\n * `validation` builder is optional. When given, it is merged with the rules\n * derived from the constraints, not replaced by them.\n */\nexport type DefineImageFieldParams = Omit<\n ImageDefinition,\n \"type\" | \"components\"\n> &\n ImageFieldOptions\n\n/**\n * Builder for an image field that uses {@link ImageFieldInput}. It sets\n * `components.input` and writes the constraints to `options.imageField`. It also\n * builds document-level `validation` from `requiredDimensions` and\n * `recommendedDimensions`. {@link ImageFieldInput} on its own does not build\n * that validation. A supplied `validation` is merged with those derived rules,\n * not replaced.\n *\n * The result goes in a `fields: [...]` array.\n *\n * @example\n * ```ts\n * defineType({\n * name: \"page\",\n * type: \"document\",\n * fields: [\n * defineImageField({\n * name: \"hero\",\n * title: \"Hero image\",\n * aspectRatio: { min: 4 / 3, max: 16 / 9 },\n * recommendedDimensions: { width: 1600, height: 900 },\n * requiredDimensions: { width: 800, height: 450 },\n * validation: (Rule) => Rule.required(),\n * }),\n * ],\n * })\n * ```\n */\nexport const defineImageField = ({\n aspectRatio,\n recommendedDimensions,\n requiredDimensions,\n options,\n validation,\n ...overrides\n}: DefineImageFieldParams) => {\n if (aspectRatio !== undefined) {\n validateAspectRatio(aspectRatio)\n }\n\n const imageField: ImageFieldOptions = {\n aspectRatio,\n recommendedDimensions,\n requiredDimensions,\n }\n\n return defineField({\n ...overrides,\n type: \"image\",\n options: { ...options, imageField },\n components: { input: ImageFieldInput },\n validation: (rule, context) => {\n const rules: ReturnType<typeof rule.required>[] = []\n\n if (requiredDimensions) {\n rules.push(\n rule.custom((value: ImageValue | undefined) =>\n validateCroppedSize(value, requiredDimensions, \"error\")\n )\n )\n }\n\n if (recommendedDimensions) {\n rules.push(\n rule\n .custom((value: ImageValue | undefined) =>\n validateCroppedSize(value, recommendedDimensions, \"warning\")\n )\n .warning()\n )\n }\n\n if (validation) {\n const userRules = validation(rule, context)\n rules.push(...(Array.isArray(userRules) ? userRules : [userRules]))\n }\n return rules\n },\n })\n}\n\n/**\n * Check the cropped image dimensions against the given limits. Returns true when\n * there is no asset or when the asset dimensions are unknown. An empty field\n * passes unless a separate `required` rule rejects it.\n */\nconst validateCroppedSize = (\n value: ImageValue | undefined,\n threshold: Dimensions,\n level: \"error\" | \"warning\"\n): true | string => {\n const dimensions = croppedDimensionsFromValue(value)\n if (!dimensions || dimensionsAtLeast(dimensions, threshold)) {\n return true\n }\n const verb = level === \"warning\" ? \"should\" : \"must\"\n const suffix = level === \"warning\" ? \" for best results\" : \"\"\n return `Cropped image ${verb} be at least ${threshold.width} × ${threshold.height} px${suffix}`\n}\n","import {\n type ImageSchemaType,\n type ImageValue,\n type InputProps,\n type ObjectInputProps,\n} from \"sanity\"\nimport { ImageFieldInput } from \"./ImageFieldInput\"\n\ntype SchemaTypeChain = { name: string; type?: SchemaTypeChain | null }\n\n/**\n * True when the input's schema type is, or extends, the built-in `image` type.\n * Lets {@link imageFieldFormInput} pass `props` to {@link ImageFieldInput}\n * without a cast.\n */\nconst isImageInput = (\n props: InputProps\n): props is ObjectInputProps<ImageValue, ImageSchemaType> => {\n for (\n let current: SchemaTypeChain | undefined = props.schemaType;\n current;\n current = current.type ?? undefined\n ) {\n if (current.name === \"image\") {\n return true\n }\n }\n return false\n}\n\n/**\n * A `form.components.input` that makes {@link ImageFieldInput} the default for\n * every `image` field in the Studio. Other field types use the default input.\n * Per-field `options.imageField` configures each image's constraints.\n *\n * @example\n * ```ts\n * // sanity.config.ts\n * import { defineConfig } from \"sanity\"\n * import { imageFieldFormInput } from \"sanity-plugin-image-field\"\n *\n * export default defineConfig({\n * // ...\n * form: { components: { input: imageFieldFormInput } },\n * })\n * ```\n */\nexport const imageFieldFormInput = (props: InputProps) =>\n isImageInput(props) ? (\n <ImageFieldInput {...props} />\n ) : (\n props.renderDefault(props)\n )\n"],"mappings":"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","names":["useRef","useState","Box","Button","Card","Flex","Stack","Text","useState","sourceDimensions","jsx","jsxs","sourceDimensions","useState","jsx","jsxs","useState","useRef","Card","Text","Stack","Flex","Button","Box","jsx"]}
package/package.json ADDED
@@ -0,0 +1,91 @@
1
+ {
2
+ "name": "sanity-plugin-image-field",
3
+ "version": "0.0.1",
4
+ "description": "A Sanity Studio image input with configurable aspect ratios and enforced minimum cropped dimensions.",
5
+ "keywords": [
6
+ "sanity",
7
+ "sanity-plugin",
8
+ "sanity-io",
9
+ "image",
10
+ "crop",
11
+ "aspect-ratio"
12
+ ],
13
+ "license": "MIT",
14
+ "type": "module",
15
+ "sideEffects": false,
16
+ "main": "./dist/index.cjs",
17
+ "module": "./dist/index.js",
18
+ "types": "./dist/index.d.ts",
19
+ "exports": {
20
+ ".": {
21
+ "types": "./dist/index.d.ts",
22
+ "import": "./dist/index.js",
23
+ "require": "./dist/index.cjs",
24
+ "default": "./dist/index.js"
25
+ },
26
+ "./package.json": "./package.json"
27
+ },
28
+ "files": [
29
+ "dist",
30
+ "README.md",
31
+ "LICENSE"
32
+ ],
33
+ "dependencies": {
34
+ "@sanity-image/url-builder": "^1.1.0",
35
+ "@sanity/icons": "^3.0.0"
36
+ },
37
+ "peerDependencies": {
38
+ "@sanity/ui": "^3.0.0",
39
+ "react": "^19",
40
+ "react-dom": "^19",
41
+ "sanity": "^6.0.0",
42
+ "styled-components": "^6.1.15"
43
+ },
44
+ "devDependencies": {
45
+ "@sanity/types": "^6.0.0",
46
+ "@sanity/ui": "^3.2.0",
47
+ "@testing-library/jest-dom": "^6.9.1",
48
+ "@testing-library/react": "^16.3.2",
49
+ "@testing-library/user-event": "^14.6.1",
50
+ "@types/node": "^24.13.2",
51
+ "@types/react": "^19.2.17",
52
+ "@types/react-dom": "^19.2.3",
53
+ "@vitejs/plugin-react": "^6.0.2",
54
+ "@vitest/coverage-v8": "4.1.8",
55
+ "@vitest/ui": "4.1.8",
56
+ "jsdom": "^29.1.1",
57
+ "oxlint": "^1.69.0",
58
+ "oxlint-tsgolint": "^0.23.0",
59
+ "prettier": "^3.8.4",
60
+ "react": "^19.2.7",
61
+ "react-dom": "^19.2.7",
62
+ "sanity": "^6.0.0",
63
+ "styled-components": "^6.4.0",
64
+ "tsup": "^8.5.1",
65
+ "typescript": "~6.0.3",
66
+ "vite": "^8.0.16",
67
+ "vitest": "^4.1.8"
68
+ },
69
+ "engines": {
70
+ "node": ">=22"
71
+ },
72
+ "prettier": {
73
+ "semi": false,
74
+ "proseWrap": "always",
75
+ "trailingComma": "es5"
76
+ },
77
+ "scripts": {
78
+ "build": "tsup",
79
+ "dev": "tsup --watch",
80
+ "typecheck": "tsc -b",
81
+ "test": "vitest run",
82
+ "test:watch": "vitest",
83
+ "test:ui": "vitest --ui",
84
+ "test:coverage": "vitest --coverage",
85
+ "lint": "oxlint",
86
+ "lint:fix": "oxlint --fix",
87
+ "format": "prettier --write .",
88
+ "format:check": "prettier --check .",
89
+ "playground:dev": "pnpm --dir playground dev"
90
+ }
91
+ }