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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright © 2026 Corey Ward
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,404 @@
1
+ # sanity-plugin-image-field
2
+
3
+ A custom image input for the Sanity Studio that gives you control over image
4
+ cropping and dimensions. Unlike Sanity's built-in image field, this plugin lets
5
+ you:
6
+
7
+ - **Constrain aspect ratios** — Force crops into specific ratios (like 16:9 for
8
+ hero images) or ranges (like 4:3 to 16:9 for flexible layouts)
9
+ - **Enforce dimension requirements** — Ensure cropped images meet minimum size
10
+ requirements for quality or performance
11
+ - **Provide real-time feedback** — Show content editors warnings when crops are
12
+ too small, and block saving when they don't meet requirements
13
+
14
+ ## How it works
15
+
16
+ This plugin integrates seamlessly with Sanity's existing image system:
17
+
18
+ - It replaces the UI component in the Studio responsible for uploading images
19
+ with a custom one that has cropping and validation built in
20
+ - It can also add custom validators to the field definition that will be
21
+ enforced by the Studio
22
+ - Crops and hotspot values are stored on the field in the standard `crop` and
23
+ `hotspot` properties
24
+ - Any front-end code using
25
+ [`@sanity/image-url`](https://www.sanity.io/docs/image-url),
26
+ [`@sanity-image/url-builder`](https://github.com/sanity-io/image-url),
27
+ [`sanity-image`](https://github.com/coreyward/sanity-image) or other crop +
28
+ hotspot respecting tooling applies the crop automatically without any
29
+ front-end changes
30
+
31
+ ## Installation
32
+
33
+ ```sh
34
+ npm install sanity-plugin-image-field
35
+ # or
36
+ pnpm add sanity-plugin-image-field
37
+ ```
38
+
39
+ This plugin also requires `@sanity/ui`. If you don't already have it:
40
+
41
+ ```sh
42
+ npm install @sanity/ui
43
+ # or:
44
+ pnpm add @sanity/ui
45
+ ```
46
+
47
+ ## Quick start
48
+
49
+ The fastest way to get started is using `defineImageField`, which sets up the
50
+ field with validation automatically:
51
+
52
+ ```ts
53
+ import { defineType } from "sanity"
54
+ import { defineImageField } from "sanity-plugin-image-field"
55
+
56
+ export const page = defineType({
57
+ name: "page",
58
+ type: "document",
59
+ fields: [
60
+ defineImageField({
61
+ name: "hero",
62
+ title: "Hero image",
63
+ // Constrain crop to landscape aspect ratios between 4:3 and 16:9
64
+ aspectRatio: { min: 4 / 3, max: 16 / 9 },
65
+ // Warn editors if crop is smaller than this
66
+ recommendedDimensions: { width: 1600, height: 900 },
67
+ // Block crops smaller than this (and validate at document level)
68
+ requiredDimensions: { width: 800, height: 450 },
69
+ validation: (Rule) => Rule.required(),
70
+ }),
71
+ ],
72
+ })
73
+ ```
74
+
75
+ ## Choosing an approach
76
+
77
+ This plugin provides a React component (`ImageFieldInput`) that replaces
78
+ Sanity's default image input. To use it, you need to:
79
+
80
+ 1. **Specify the component** — Tell Sanity when to use `ImageFieldInput` instead
81
+ of the default
82
+ 2. **Configure the constraints** — Set aspect ratio and dimension options that
83
+ the component reads
84
+
85
+ ### How to specify the component
86
+
87
+ You have two ways to tell Sanity to use `ImageFieldInput`:
88
+
89
+ **1. Per-field (explicit)** Specify `ImageFieldInput` as the input component on
90
+ individual image fields where you want to use it:
91
+
92
+ ```ts
93
+ import { defineField } from "sanity"
94
+ import { ImageFieldInput } from "sanity-plugin-image-field"
95
+
96
+ defineField({
97
+ name: "hero",
98
+ type: "image",
99
+ components: { input: ImageFieldInput }, // ← Use our component here
100
+ options: {
101
+ imageField: {
102
+ aspectRatio: 16 / 9,
103
+ },
104
+ },
105
+ })
106
+ ```
107
+
108
+ **2. Studio-wide default (implicit)** Register `imageFieldFormInput` as the
109
+ default input component for all image fields in your Sanity config:
110
+
111
+ ```ts
112
+ // sanity.config.ts
113
+ import { defineConfig } from "sanity"
114
+ import { imageFieldFormInput } from "sanity-plugin-image-field"
115
+
116
+ export default defineConfig({
117
+ // ...
118
+ form: { components: { input: imageFieldFormInput } }, // ← Use for all image fields
119
+ })
120
+ ```
121
+
122
+ ### How configuration works
123
+
124
+ Regardless of how you specify the component, configuration always happens the
125
+ same way: through `options.imageField` on your image field definition. The
126
+ `ImageFieldInput` component reads these options to determine how to constrain
127
+ cropping:
128
+
129
+ ```ts
130
+ defineField({
131
+ name: "hero",
132
+ type: "image",
133
+ options: {
134
+ imageField: {
135
+ // ← Configuration lives here
136
+ aspectRatio: 16 / 9,
137
+ recommendedDimensions: { width: 1600, height: 900 },
138
+ requiredDimensions: { width: 800, height: 450 },
139
+ },
140
+ },
141
+ })
142
+ ```
143
+
144
+ ### The `defineImageField` helper
145
+
146
+ `defineImageField` is a convenience function that:
147
+
148
+ - Uses the **per-field** approach to specify `ImageFieldInput`
149
+ - Adds your constraints to `options.imageField` automatically
150
+ - Builds document-level validation from your dimension requirements
151
+ - Merges any additional validation you provide
152
+
153
+ It's compatible with both per-field specification and studio-wide defaults.
154
+
155
+ ### Which approach should you use?
156
+
157
+ ### Use `defineImageField` when:
158
+
159
+ - You want the simplest setup with built-in validation
160
+ - You're adding image fields to individual schemas
161
+ - You want document-level validation that prevents saving invalid crops
162
+
163
+ **This is the recommended approach for most use cases.**
164
+
165
+ ### Use manual per-field configuration when:
166
+
167
+ - You need fine-grained control over the field definition
168
+ - You want to add custom validation logic alongside the image constraints
169
+ - You're integrating with existing field configurations
170
+ - You prefer the explicit, visible approach of specifying components per field
171
+
172
+ ### Use the studio-wide default when:
173
+
174
+ - You want ALL image fields in your Studio to use this plugin
175
+ - You have many image fields and don't want to configure each one individually
176
+ - You want to apply the same constraints globally
177
+ - You'll combine it with `defineImageField` where you need document-level
178
+ validation
179
+
180
+ ## Configuration options
181
+
182
+ All configuration lives under `options.imageField` in your field definition:
183
+
184
+ | Option | Type | Description |
185
+ | ----------------------- | ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
186
+ | `aspectRatio` | `number \| { min; max } \| number[]` | Allowed crop ratios as `width / height`. Use a single number for a fixed ratio, a `{ min, max }` range for flexibility, or an array of specific ratios to snap between. |
187
+ | `recommendedDimensions` | `{ width; height }` | Shows a non-blocking warning when the crop is smaller than this size. Helps editors maintain quality standards. |
188
+ | `requiredDimensions` | `{ width; height }` | Blocks confirmation when the crop is smaller than this size. With `defineImageField`, also adds document-level validation. |
189
+
190
+ ### Aspect ratio examples
191
+
192
+ ```ts
193
+ // Fixed ratio (e.g., for hero images that must be exactly 16:9)
194
+ aspectRatio: 16 / 9
195
+
196
+ // Flexible range (e.g., for cards that can be 4:3 to 16:9)
197
+ aspectRatio: { min: 4 / 3, max: 16 / 9 }
198
+
199
+ // Discrete options (e.g., for social media images that must be specific ratios)
200
+ aspectRatio: [16 / 9, 4 / 3, 1] // Snaps to nearest ratio as you drag
201
+ ```
202
+
203
+ ## Usage patterns
204
+
205
+ ### 1. Recommended: Using `defineImageField`
206
+
207
+ This helper creates a fully configured image field with automatic validation:
208
+
209
+ ```ts
210
+ import { defineType } from "sanity"
211
+ import { defineImageField } from "sanity-plugin-image-field"
212
+
213
+ export const page = defineType({
214
+ name: "page",
215
+ type: "document",
216
+ fields: [
217
+ defineImageField({
218
+ name: "hero",
219
+ title: "Hero image",
220
+ aspectRatio: { min: 4 / 3, max: 16 / 9 },
221
+ recommendedDimensions: { width: 1600, height: 900 },
222
+ requiredDimensions: { width: 800, height: 450 },
223
+ validation: (Rule) => Rule.required(),
224
+ }),
225
+ ],
226
+ })
227
+ ```
228
+
229
+ **Benefits:**
230
+
231
+ - Simplest API — all configuration in one place
232
+ - Automatic document-level validation from dimension constraints
233
+ - Type-safe and integrates with Sanity's field system
234
+
235
+ ### 2. Manual field configuration
236
+
237
+ For more control, specify `ImageFieldInput` as the component:
238
+
239
+ ```ts
240
+ import { defineField, defineType } from "sanity"
241
+ import { ImageFieldInput } from "sanity-plugin-image-field"
242
+
243
+ export const page = defineType({
244
+ name: "page",
245
+ type: "document",
246
+ fields: [
247
+ defineField({
248
+ name: "hero",
249
+ title: "Hero image",
250
+ type: "image",
251
+ options: {
252
+ hotspot: true, // Enable focal point editor (Sanity's standard option)
253
+ imageField: {
254
+ aspectRatio: { min: 4 / 3, max: 16 / 9 },
255
+ recommendedDimensions: { width: 1600, height: 900 },
256
+ requiredDimensions: { width: 800, height: 450 },
257
+ },
258
+ },
259
+ components: { input: ImageFieldInput },
260
+ }),
261
+ ],
262
+ })
263
+ ```
264
+
265
+ **Benefits:**
266
+
267
+ - Full control over field definition
268
+ - Can add custom validation logic
269
+ - Integrates with existing field configurations
270
+
271
+ **Note:** This approach doesn't automatically add document-level validation for
272
+ dimensions. Add it manually if needed, or use `defineImageField`.
273
+
274
+ ### 3. Form-level default
275
+
276
+ Apply this input to all image fields in your Studio:
277
+
278
+ ```ts
279
+ // sanity.config.ts
280
+ import { defineConfig } from "sanity"
281
+ import { imageFieldFormInput } from "sanity-plugin-image-field"
282
+
283
+ export default defineConfig({
284
+ // ... other config
285
+ form: { components: { input: imageFieldFormInput } },
286
+ })
287
+ ```
288
+
289
+ Then configure individual fields through `options.imageField`:
290
+
291
+ ```ts
292
+ defineField({
293
+ name: "hero",
294
+ type: "image",
295
+ options: {
296
+ imageField: {
297
+ aspectRatio: 16 / 9,
298
+ requiredDimensions: { width: 800, height: 450 },
299
+ },
300
+ },
301
+ })
302
+ ```
303
+
304
+ **Benefits:**
305
+
306
+ - Consistent behavior across all image fields
307
+ - Don't need to specify the component on each field
308
+ - Combine with `defineImageField` where you need validation
309
+
310
+ ## Editor experience
311
+
312
+ ### Upload
313
+
314
+ Content editors can upload images by:
315
+
316
+ - Using the file picker
317
+ - Dragging and dropping an image onto the field
318
+ - Pasting an image from the clipboard
319
+ - Selecting an existing asset from the Media library or other configured sources
320
+
321
+ The field honors any `options.accept` format restrictions you set.
322
+
323
+ ### Cropping
324
+
325
+ When an aspect ratio constraint is configured, the crop editor opens
326
+ automatically after upload. The editor:
327
+
328
+ - Defaults to the largest crop that fits your constraint
329
+ - Shows live pixel dimensions with warnings for size requirements
330
+ - Blocks confirmation while the crop is below the required size
331
+ - Supports keyboard controls (arrow keys to move, Shift+arrow to resize)
332
+ - Supports modifier keys:
333
+ - **Shift** while dragging a corner to lock the current aspect ratio
334
+ - **Option/Alt** while dragging to resize about the center
335
+ - When using an array of aspect ratios, snaps to the nearest ratio as you drag
336
+ (never showing in-between ratios)
337
+
338
+ ### Focal point
339
+
340
+ When you enable `options.hotspot: true`, editors can set a focal point for the
341
+ image. This is stored in Sanity's standard `hotspot` field and works with
342
+ front-end tools that support cover-mode framing. The focal point marker is
343
+ positioned relative to the crop area.
344
+
345
+ ### Confirmed state
346
+
347
+ After cropping, editors see:
348
+
349
+ - The cropped image preview
350
+ - Buttons to re-crop, replace, select a different asset, or remove the image
351
+ - Any dimension warnings based on your configuration
352
+
353
+ ## How cropping works with Sanity
354
+
355
+ This plugin uses Sanity's standard, non-destructive cropping system:
356
+
357
+ - **Storage format** — Crops are stored as `{ top, bottom, left, right }` insets
358
+ (0-1 range) on the standard `crop` field
359
+ - **Original preservation** — Your original images are never modified
360
+ - **Automatic application** — Front-end code using
361
+ [`@sanity/image-url`](https://www.sanity.io/docs/image-url),
362
+ [`@sanity-image/url-builder`](https://github.com/sanity-io/image-url), or
363
+ [`sanity-image`](https://github.com/coreyward/sanity-image) applies the crop
364
+ automatically
365
+ - **Dimension calculation** — Cropped dimensions are computed from the original
366
+ asset's metadata, so no additional API calls are needed
367
+
368
+ ### SVG handling
369
+
370
+ SVGs are handled specially since Sanity does not support crop transformations on
371
+ SVG images:
372
+
373
+ - Uploads work normally
374
+ - The crop dialog is not shown for SVG images
375
+ - Aspect ratio and dimension validation are not performed on SVG images, even
376
+ when the SVG has dimensions (from `viewBox` or `width`/`height` attributes)
377
+ - The asset reference is still stored normally
378
+
379
+ ## Playground
380
+
381
+ A runnable Sanity Studio is available in the `playground/` directory. To try it:
382
+
383
+ ```sh
384
+ # Build the package (or run `pnpm dev` to watch for changes)
385
+ pnpm build
386
+
387
+ # Configure the playground with your Sanity project
388
+ cp playground/.env.example playground/.env
389
+ # Edit playground/.env and set SANITY_STUDIO_PROJECT_ID
390
+
391
+ # Start the playground
392
+ pnpm playground:dev
393
+ ```
394
+
395
+ ## Future work
396
+
397
+ - **SVG validation** — Add validation for SVG images that have dimensions (from
398
+ `viewBox` or `width`/`height` attributes) to check aspect ratio and minimum
399
+ dimensions. Currently, all SVG images bypass validation since Sanity does not
400
+ support crop transformations on SVGs.
401
+
402
+ ## License
403
+
404
+ MIT